Compare commits

...
Sign in to create a new pull request.

90 commits

Author SHA1 Message Date
447c577a2a style: cargo clippy & fmt
All checks were successful
ci/woodpecker/push/build-and-publish Pipeline was successful
ci/woodpecker/push/publish-docs Pipeline was successful
2025-08-05 03:28:35 +02:00
5b24d0052b feat: return member with messages 2025-08-05 03:28:24 +02:00
8a7711cabc feat: Make me optional in Member::fetch_one() 2025-08-05 03:28:02 +02:00
4cb89645fe Merge pull request 'Member Improvements' (#44) from wip/member-improvements into main
All checks were successful
ci/woodpecker/push/publish-docs Pipeline was successful
ci/woodpecker/push/build-and-publish Pipeline was successful
Reviewed-on: #44
2025-08-05 00:09:19 +02:00
ac1678bfa8 fix: use dedicated function for member count
All checks were successful
ci/woodpecker/push/build-and-publish Pipeline was successful
ci/woodpecker/pr/build-and-publish Pipeline was successful
ci/woodpecker/pull_request_closed/build-and-publish Pipeline was successful
2025-08-05 00:02:30 +02:00
642dbe5270 fix: remove order_by on single fetches 2025-08-05 00:02:21 +02:00
8d91ec78a6 refactor: rename fetch_one_with_member
All checks were successful
ci/woodpecker/push/build-and-publish Pipeline was successful
ci/woodpecker/pr/build-and-publish Pipeline was successful
Renamed to fetch_one_with_uuid
2025-08-04 22:55:48 +02:00
e9cc2a3f0e feat: faster member fetching and pagination 2025-08-04 22:55:22 +02:00
946085a18f Merge pull request 'Online status' (#43) from wip/status into main
All checks were successful
ci/woodpecker/push/build-and-publish Pipeline was successful
ci/woodpecker/push/publish-docs Pipeline was successful
Reviewed-on: #43
2025-08-04 19:22:59 +00:00
53451e67c7 fix: 4 is always bigger than 0
All checks were successful
ci/woodpecker/push/build-and-publish Pipeline was successful
ci/woodpecker/pr/build-and-publish Pipeline was successful
ci/woodpecker/pull_request_closed/build-and-publish Pipeline was successful
2025-08-04 20:52:19 +02:00
027649a060 feat: added online status
All checks were successful
ci/woodpecker/push/build-and-publish Pipeline was successful
ci/woodpecker/pr/build-and-publish Pipeline was successful
2025-08-04 20:46:49 +02:00
314b9ee011 feat: added online_status column to users table
All checks were successful
ci/woodpecker/push/build-and-publish Pipeline was successful
2025-08-04 20:07:46 +02:00
3816af56e3 feat: return role with member
All checks were successful
ci/woodpecker/push/build-and-publish Pipeline was successful
ci/woodpecker/push/publish-docs Pipeline was successful
2025-07-31 16:01:10 +02:00
5cb6b1d495 Merge pull request 'Improve database access' (#41) from improve-database-access into main
All checks were successful
ci/woodpecker/push/build-and-publish Pipeline was successful
ci/woodpecker/push/publish-docs Pipeline was successful
Reviewed-on: #41
Reviewed-by: baaboe <baaboe@gorb.app>
2025-07-30 13:57:36 +00:00
3a28a8d34a Merge branch 'main' into improve-database-access
All checks were successful
ci/woodpecker/push/build-and-publish Pipeline was successful
ci/woodpecker/pr/build-and-publish Pipeline was successful
ci/woodpecker/pull_request_closed/build-and-publish Pipeline was successful
2025-07-30 15:26:30 +02:00
d64dc92b99 Merge branch 'wip/ban'
All checks were successful
ci/woodpecker/push/build-and-publish Pipeline was successful
ci/woodpecker/push/publish-docs Pipeline was successful
2025-07-25 00:31:34 +02:00
4a2f98a180 style: cargo clippy --fix && cargo fmt
All checks were successful
ci/woodpecker/push/build-and-publish Pipeline was successful
ci/woodpecker/pr/build-and-publish Pipeline was successful
ci/woodpecker/pull_request_closed/build-and-publish Pipeline was successful
2025-07-25 00:21:51 +02:00
b38b5360f6 style: style 2025-07-25 00:21:20 +02:00
b28d5b840f style: cargo clippy --fix && cargo fmt 2025-07-24 02:30:52 +02:00
0e0c590e4d feat: added unband endpoint
All checks were successful
ci/woodpecker/push/build-and-publish Pipeline was successful
ci/woodpecker/pr/build-and-publish Pipeline was successful
2025-07-24 02:14:29 +02:00
ba2442e786 style: updated to use the new ban object 2025-07-24 02:13:53 +02:00
c2b5f6568f style: renaming parameter, ban_time -> banned_since
All checks were successful
ci/woodpecker/push/build-and-publish Pipeline was successful
ci/woodpecker/pr/build-and-publish Pipeline was successful
2025-07-24 01:58:10 +02:00
26f528819e style: cargo clippy --fix && cargo fmt
All checks were successful
ci/woodpecker/push/build-and-publish Pipeline was successful
ci/woodpecker/pr/build-and-publish Pipeline was successful
2025-07-23 19:10:17 +02:00
7e10086753 feat: retrive all banned users in a guild 2025-07-23 19:08:54 +02:00
3ad73f28fa feat: added ban time 2025-07-23 16:30:38 +02:00
bb8927840d style: formatting
All checks were successful
ci/woodpecker/push/build-and-publish Pipeline was successful
ci/woodpecker/pr/build-and-publish Pipeline was successful
2025-07-23 15:04:02 +02:00
475e008105 fix: move owner check to correct function 2025-07-23 15:03:56 +02:00
a1857a1939 Merge pull request 'Check if you are trying to kick owner' (#38) from wip/kick into main
All checks were successful
ci/woodpecker/push/build-and-publish Pipeline was successful
ci/woodpecker/push/publish-docs Pipeline was successful
Reviewed-on: #38
Reviewed-by: Radical <radical@radical.fun>
2025-07-23 12:59:34 +00:00
bf5cc600b9 Merge branch 'main' into wip/kick
All checks were successful
ci/woodpecker/push/build-and-publish Pipeline was successful
ci/woodpecker/pr/build-and-publish Pipeline was successful
ci/woodpecker/pull_request_closed/build-and-publish Pipeline was successful
2025-07-23 09:10:40 +00:00
BAaboe
ac5ca90974 fix: self not member
All checks were successful
ci/woodpecker/push/build-and-publish Pipeline was successful
ci/woodpecker/pr/build-and-publish Pipeline was successful
2025-07-23 02:03:06 +02:00
BAaboe
e074ca89f9 feat: idiot proofing
Some checks failed
ci/woodpecker/push/build-and-publish Pipeline failed
ci/woodpecker/pr/build-and-publish Pipeline failed
2025-07-23 01:50:55 +02:00
BAaboe
cbdf6f79e2 feat: idiot(goin) proofing
All checks were successful
ci/woodpecker/push/build-and-publish Pipeline was successful
ci/woodpecker/pr/build-and-publish Pipeline was successful
2025-07-23 01:48:31 +02:00
BAaboe
ceaa37cbe2 fix: plural fix thing
All checks were successful
ci/woodpecker/push/build-and-publish Pipeline was successful
ci/woodpecker/pr/build-and-publish Pipeline was successful
2025-07-23 01:37:35 +02:00
BAaboe
c725d13ca8 feat: kick permission
All checks were successful
ci/woodpecker/push/build-and-publish Pipeline was successful
ci/woodpecker/pr/build-and-publish Pipeline was successful
2025-07-23 01:35:28 +02:00
BAaboe
5fe5186142 feat: ban permission
All checks were successful
ci/woodpecker/push/build-and-publish Pipeline was successful
ci/woodpecker/pr/build-and-publish Pipeline was successful
2025-07-23 01:31:28 +02:00
BAaboe
2bc702f8d2 fix: baner :)
All checks were successful
ci/woodpecker/push/build-and-publish Pipeline was successful
ci/woodpecker/pr/build-and-publish Pipeline was successful
2025-07-23 01:18:08 +02:00
BAaboe
9e5d2daeab fix: fixed error error
All checks were successful
ci/woodpecker/push/build-and-publish Pipeline was successful
ci/woodpecker/pr/build-and-publish Pipeline was successful
2025-07-23 01:07:41 +02:00
BAaboe
6999b4120e fix: Updated error message when banning owner
All checks were successful
ci/woodpecker/push/build-and-publish Pipeline was successful
ci/woodpecker/pr/build-and-publish Pipeline was successful
2025-07-23 01:00:17 +02:00
BAaboe
71d44f6c21 fix: Can not kick owner
Some checks failed
ci/woodpecker/push/build-and-publish Pipeline failed
2025-07-23 00:58:51 +02:00
BAaboe
8e31dc7aca style: cargo clippy --fix && cargo fmt
All checks were successful
ci/woodpecker/push/build-and-publish Pipeline was successful
ci/woodpecker/pr/build-and-publish Pipeline was successful
2025-07-23 00:52:19 +02:00
BAaboe
ad24215fef feat: added endpoint for banning 2025-07-23 00:50:50 +02:00
BAaboe
f175c19325 fix: let you pass None to fetch_one_with_member for me 2025-07-22 23:18:21 +02:00
BAaboe
ade45780fa fix: forgot to add the change on schema 2025-07-22 22:04:48 +02:00
BAaboe
af71937506 fix: fixed the ban table names 2025-07-22 22:01:48 +02:00
BAaboe
b2e6d3f553 feat: added ban table to the database 2025-07-22 21:58:14 +02:00
2996c6f108 Merge pull request 'wip/kick' (#36) from wip/kick into main
All checks were successful
ci/woodpecker/push/build-and-publish Pipeline was successful
ci/woodpecker/push/publish-docs Pipeline was successful
Reviewed-on: #36
2025-07-22 16:56:36 +00:00
45978bb41a ci: only run on push
Some checks are pending
ci/woodpecker/push/build-and-publish Pipeline is pending
ci/woodpecker/push/publish-docs Pipeline is pending
prevents duplicate CIs from running at the same time
2025-07-22 18:55:26 +02:00
BAaboe
c26ec49e05 fix: cargo clippy --fix && cargo fmt
All checks were successful
ci/woodpecker/push/build-and-publish Pipeline was successful
ci/woodpecker/pr/build-and-publish Pipeline was successful
ci/woodpecker/pull_request_closed/build-and-publish Pipeline was successful
2025-07-22 18:50:17 +02:00
BAaboe
a3c460a611 fix: Only people in a server should see its members list
All checks were successful
ci/woodpecker/push/build-and-publish Pipeline was successful
ci/woodpecker/pr/build-and-publish Pipeline was successful
2025-07-22 18:33:41 +02:00
BAaboe
6dd8ddb0df fix: members in router_with_auth
Some checks failed
ci/woodpecker/push/build-and-publish Pipeline was successful
ci/woodpecker/pr/build-and-publish Pipeline failed
2025-07-22 18:27:36 +02:00
BAaboe
31596c6bfe fix: memebrs not member as endpoint 2025-07-22 18:26:04 +02:00
BAaboe
ea33230e58 fix: reduced numder of function calls to get conn 2025-07-22 18:10:37 +02:00
BAaboe
0468d1adca fix: Unecessary merge of routers 2025-07-22 18:05:19 +02:00
BAaboe
228bc68327 more path name fix
All checks were successful
ci/woodpecker/push/build-and-publish Pipeline was successful
ci/woodpecker/pr/build-and-publish Pipeline was successful
2025-07-22 17:38:04 +02:00
BAaboe
4ec36c1cda path name fix
Some checks failed
ci/woodpecker/push/build-and-publish Pipeline failed
2025-07-22 17:35:02 +02:00
BAaboe
82f4388dab New endpoint 'members' with get and delete
All checks were successful
ci/woodpecker/push/build-and-publish Pipeline was successful
2025-07-22 17:29:55 +02:00
fa52412b43 feat: make database access more uniform and stop locking as many pool connections
All checks were successful
ci/woodpecker/push/build-and-publish Pipeline was successful
2025-07-21 04:15:04 +02:00
f5d4211fad fix: force rust 1.88 in builds
All checks were successful
ci/woodpecker/push/build-and-publish Pipeline was successful
ci/woodpecker/push/publish-docs Pipeline was successful
2025-07-20 20:29:28 +02:00
8a58774359 fix: get cache correctly from redis
Some checks failed
ci/woodpecker/push/build-and-publish Pipeline failed
ci/woodpecker/push/publish-docs Pipeline failed
2025-07-20 20:25:15 +02:00
eb7e5503de Merge pull request 'axum rewrite' (#35) from staging into main
All checks were successful
ci/woodpecker/push/build-and-publish Pipeline was successful
ci/woodpecker/push/publish-docs Pipeline was successful
Reviewed-on: #35
2025-07-20 17:25:03 +00:00
1c07957c4e refactor: small dependency optimizations
All checks were successful
ci/woodpecker/push/build-and-publish Pipeline was successful
ci/woodpecker/pr/build-and-publish Pipeline was successful
ci/woodpecker/pull_request_closed/build-and-publish Pipeline was successful
2025-07-20 18:45:50 +02:00
8ec1610b2e feat: remove dependency on socket.io
All checks were successful
ci/woodpecker/push/build-and-publish Pipeline was successful
Keeping stuff commented so we can revisit, currently just need a working version
2025-07-20 18:11:31 +02:00
2fb7e7781f feat: reimplement old websocket 2025-07-20 18:11:08 +02:00
a602c2624f style: cargo fmt & clippy fixes
All checks were successful
ci/woodpecker/push/build-and-publish Pipeline was successful
2025-07-20 16:30:46 +02:00
969b517e18 Merge branch 'generate-device-name' into staging
Some checks failed
ci/woodpecker/push/build-and-publish Pipeline failed
2025-07-20 16:28:02 +02:00
1ad88725bd feat: use custom middleware for authorization
All checks were successful
ci/woodpecker/push/build-and-publish Pipeline was successful
2025-07-20 14:12:57 +02:00
dada230e08 fix: remove the rest of the leftover code from access_token cookies 2025-07-20 13:04:08 +02:00
9bf435b535 fix: revert changes to access_token made during refactor
All checks were successful
ci/woodpecker/push/build-and-publish Pipeline was successful
2025-07-19 23:39:56 +02:00
d2fec66ddb fix: try not setting path on access token
All checks were successful
ci/woodpecker/push/build-and-publish Pipeline was successful
2025-07-19 23:20:16 +02:00
252b9a3dc6 fix: add more cors shit
All checks were successful
ci/woodpecker/push/build-and-publish Pipeline was successful
can someone please just make cors disappear? god i hate this shit.
2025-07-19 23:03:23 +02:00
2fbf41ba8c fix: use .append() and not Set-Cookie2
All checks were successful
ci/woodpecker/push/build-and-publish Pipeline was successful
web dev is too confusing..
2025-07-19 19:10:36 +02:00
d67a7ce0ca fix: try explicitly setting methods and headers
All checks were successful
ci/woodpecker/push/build-and-publish Pipeline was successful
2025-07-18 12:00:28 +02:00
8f53c9f718 fix: try to fix up cors
All checks were successful
ci/woodpecker/push/build-and-publish Pipeline was successful
Login still not working, unsure of where failure point is
2025-07-17 21:34:35 +02:00
9a0ebf2b2f fix: use merge instead of nesting
All checks were successful
ci/woodpecker/push/build-and-publish Pipeline was successful
2025-07-17 16:48:34 +02:00
1946080716 ci: remove parentheses from name
All checks were successful
ci/woodpecker/push/build-and-publish Pipeline was successful
2025-07-17 16:07:09 +02:00
21560cc051 Merge branch 'main' into wip/axum
Some checks failed
ci/woodpecker/push/build-and-publish Pipeline failed
2025-07-17 15:56:56 +02:00
c9dd66dd80 ci: add staging images
All checks were successful
ci/woodpecker/push/build-and-publish Pipeline was successful
ci/woodpecker/push/publish-docs Pipeline was successful
2025-07-17 15:52:11 +02:00
324137ce8b refactor: rewrite entire codebase in axum instead of actix
Replaces actix with axum for web, allows us to use socket.io and gives us access to the tower ecosystem of middleware

breaks compatibility with our current websocket implementation, needs to be reimplemented for socket.io
2025-07-16 16:36:22 +02:00
fc061738fa
feat: finish adding device name to login, register, and refresh endpoints 2025-07-15 02:42:53 +02:00
7872d2ec24
fix: increase length of refresh token field 2025-07-15 02:30:07 +02:00
e7bc53f858
feat: try reading the device name from the table 2025-07-14 01:02:03 +02:00
8656115dc9
feat: start implementing device name generation in the backend 2025-07-14 00:36:15 +02:00
3647086adb feat: add endpoint to get logged in devices
All checks were successful
ci/woodpecker/push/build-and-publish Pipeline was successful
ci/woodpecker/push/publish-docs Pipeline was successful
2025-07-13 18:06:23 +02:00
671fc42555 fix: make (user_uuid, guild_uuid) unique in members
Some checks failed
ci/woodpecker/push/publish-docs Pipeline is pending
ci/woodpecker/push/build-and-publish Pipeline failed
2025-07-13 18:03:28 +02:00
284d5e45fa Merge pull request 'Update POST me/friend endpoint to take username instead of UUID' (#31) from friends-endpoint-username into main
All checks were successful
ci/woodpecker/push/build-and-publish Pipeline was successful
ci/woodpecker/push/publish-docs Pipeline was successful
Reviewed-on: #31
Reviewed-by: Radical <radical@radical.fun>
2025-07-13 14:22:31 +00:00
384f5e404f
fix: change function paramater name to match function name 2025-07-13 16:21:07 +02:00
d775723b7b
fix: require username, instead of username OR email 2025-07-13 16:20:03 +02:00
b87adf896f
fix: linter :( why you one line the import D: 2025-07-13 16:17:54 +02:00
e17fc9fff0
fix: add a friend via uesrname instead of their UUID 2025-07-13 16:11:47 +02:00
5f8d0271e7 fix: use correct permission for channel deletion
All checks were successful
ci/woodpecker/push/build-and-publish Pipeline was successful
ci/woodpecker/push/publish-docs Pipeline was successful
2025-07-12 20:16:09 +02:00
67 changed files with 3422 additions and 1436 deletions

View file

@ -1,14 +1,13 @@
when:
- event: push
branch: main
steps: steps:
- name: build-x86_64 - name: build-x86_64
image: rust:bookworm image: rust:1.88-bookworm
commands: commands:
- cargo build --release - cargo build --release
when:
- event: push
- name: build-arm64 - name: build-arm64
image: rust:bookworm image: rust:1.88-bookworm
commands: commands:
- dpkg --add-architecture arm64 - dpkg --add-architecture arm64
- apt-get update -y && apt-get install -y crossbuild-essential-arm64 libssl-dev:arm64 - apt-get update -y && apt-get install -y crossbuild-essential-arm64 libssl-dev:arm64
@ -18,6 +17,9 @@ steps:
CARGO_TARGET_AARCH64_UNKNOWN_LINUX_GNU_LINKER: aarch64-linux-gnu-gcc CARGO_TARGET_AARCH64_UNKNOWN_LINUX_GNU_LINKER: aarch64-linux-gnu-gcc
PKG_CONFIG_ALLOW_CROSS: 1 PKG_CONFIG_ALLOW_CROSS: 1
PKG_CONFIG_PATH: /usr/aarch64-linux-gnu/lib/pkgconfig PKG_CONFIG_PATH: /usr/aarch64-linux-gnu/lib/pkgconfig
when:
- event: push
- name: container-build-and-publish - name: container-build-and-publish
image: docker image: docker
commands: commands:
@ -28,3 +30,20 @@ steps:
from_secret: docker_password from_secret: docker_password
volumes: volumes:
- /var/run/podman/podman.sock:/var/run/docker.sock - /var/run/podman/podman.sock:/var/run/docker.sock
when:
- branch: main
event: push
- name: container-build-and-publish-staging
image: docker
commands:
- docker login --username radical --password $PASSWORD git.gorb.app
- docker buildx build --platform linux/amd64,linux/arm64 --rm --push -t git.gorb.app/gorb/backend:staging .
environment:
PASSWORD:
from_secret: docker_password
volumes:
- /var/run/podman/podman.sock:/var/run/docker.sock
when:
- branch: staging
event: push

View file

@ -4,7 +4,7 @@ when:
steps: steps:
- name: build-docs - name: build-docs
image: rust:bookworm image: rust:1.88-bookworm
commands: commands:
- cargo doc --release --no-deps - cargo doc --release --no-deps

View file

@ -20,29 +20,28 @@ thiserror = "2.0.12"
# CLI # CLI
clap = { version = "4.5", features = ["derive"] } clap = { version = "4.5", features = ["derive"] }
log = "0.4" log = "0.4"
simple_logger = "5.0.0"
# async # async
futures = "0.3"
tokio = { version = "1.46", features = ["full"] } tokio = { version = "1.46", features = ["full"] }
futures-util = "0.3.31" futures-util = "0.3.31"
# Data (de)serialization # Data (de)serialization
serde = { version = "1.0", features = ["derive"] } serde = { version = "1.0", features = ["derive"] }
serde_json = "1.0" serde_json = "1.0"
toml = "0.8" toml = "0.9"
bytes = "1.10.1"
# File Storage # File Storage
bindet = "0.3.2" bindet = "0.3.2"
bunny-api-tokio = { version = "0.4", features = ["edge_storage"], default-features = false } bunny-api-tokio = { version = "0.4", features = ["edge_storage"], default-features = false }
# Web Server # Web Server
actix-web = "4.11" axum = { version = "0.8.4", features = ["multipart", "ws"] }
actix-cors = "0.7.1" axum-extra = { version = "0.10.1", features = ["cookie", "typed-header"] }
actix-ws = "0.3.0" tower-http = { version = "0.6.6", features = ["cors"] }
actix-multipart = "0.7.2" #socketioxide = { version = "0.17.2", features = ["state"] }
url = { version = "2.5", features = ["serde"] } url = { version = "2.5", features = ["serde"] }
tokio-tungstenite = { version = "0.27", features = ["native-tls", "url"] } time = "0.3.41"
# Database # Database
uuid = { version = "1.17", features = ["serde", "v7"] } uuid = { version = "1.17", features = ["serde", "v7"] }
@ -60,5 +59,5 @@ regex = "1.11"
random-string = "1.1" random-string = "1.1"
lettre = { version = "0.11", features = ["tokio1", "tokio1-native-tls"] } lettre = { version = "0.11", features = ["tokio1", "tokio1-native-tls"] }
chrono = { version = "0.4.41", features = ["serde"] } chrono = { version = "0.4.41", features = ["serde"] }
tracing-subscriber = "0.3.19"
rand = "0.9.1"

View file

@ -0,0 +1,2 @@
-- This file should undo anything in `up.sql`
ALTER TABLE guild_members DROP CONSTRAINT guild_members_user_uuid_guild_uuid_key;

View file

@ -0,0 +1,2 @@
-- Your SQL goes here
ALTER TABLE guild_members ADD UNIQUE (user_uuid, guild_uuid)

View file

@ -0,0 +1,2 @@
-- This file should undo anything in `up.sql`
ALTER TABLE refresh_tokens ALTER COLUMN device_name TYPE varchar(16);

View file

@ -0,0 +1,2 @@
-- Your SQL goes here
ALTER TABLE refresh_tokens ALTER COLUMN device_name TYPE varchar(64);

View file

@ -0,0 +1,2 @@
-- This file should undo anything in `up.sql`
DROP TABLE guild_bans;

View file

@ -0,0 +1,8 @@
-- Your SQL goes here
CREATE TABLE guild_bans (
guild_uuid uuid NOT NULL REFERENCES guilds(uuid) ON DELETE CASCADE,
user_uuid uuid NOT NULL REFERENCES users(uuid),
reason VARCHAR(200) DEFAULT NULL,
banned_since TIMESTAMPTZ NOT NULL DEFAULT NOW(),
PRIMARY KEY (user_uuid, guild_uuid)
);

View file

@ -0,0 +1,5 @@
-- This file should undo anything in `up.sql`
DROP INDEX roles_guuid_uuid;
ALTER TABLE roles DROP CONSTRAINT roles_pkey;
CREATE UNIQUE INDEX roles_pkey ON roles (uuid, guild_uuid);
ALTER TABLE roles ADD PRIMARY KEY USING INDEX roles_pkey;

View file

@ -0,0 +1,5 @@
-- Your SQL goes here
ALTER TABLE roles DROP CONSTRAINT roles_pkey;
CREATE UNIQUE INDEX roles_pkey ON roles (uuid);
ALTER TABLE roles ADD PRIMARY KEY USING INDEX roles_pkey;
CREATE UNIQUE INDEX roles_guuid_uuid ON roles (uuid, guild_uuid);

View file

@ -0,0 +1,2 @@
-- This file should undo anything in `up.sql`
ALTER TABLE users DROP COLUMN online_status;

View file

@ -0,0 +1,2 @@
-- Your SQL goes here
ALTER TABLE users ADD COLUMN online_status INT2 NOT NULL DEFAULT 0;

View file

@ -1,13 +1,16 @@
//! `/api` Contains the entire API //! `/api` Contains the entire API
use actix_web::Scope; use std::sync::Arc;
use actix_web::web;
use axum::{Router, routing::get};
use crate::AppState;
mod v1; mod v1;
mod versions; mod versions;
pub fn web(path: &str) -> Scope { pub fn router(path: &str, app_state: Arc<AppState>) -> Router<Arc<AppState>> {
web::scope(path.trim_end_matches('/')) Router::new()
.service(v1::web()) .route(&format!("{path}/versions"), get(versions::versions))
.service(versions::get) .nest(&format!("{path}/v1"), v1::router(app_state))
} }

View file

@ -0,0 +1,51 @@
//! `/api/v1/auth/devices` Returns list of logged in devices
use std::sync::Arc;
use axum::{Extension, Json, extract::State, http::StatusCode, response::IntoResponse};
use diesel::{ExpressionMethods, QueryDsl, Queryable, Selectable, SelectableHelper};
use diesel_async::RunQueryDsl;
use serde::Serialize;
use uuid::Uuid;
use crate::{
AppState,
api::v1::auth::CurrentUser,
error::Error,
schema::refresh_tokens::{self, dsl},
};
#[derive(Serialize, Selectable, Queryable)]
#[diesel(table_name = refresh_tokens)]
#[diesel(check_for_backend(diesel::pg::Pg))]
struct Device {
device_name: String,
created_at: i64,
}
/// `GET /api/v1/auth/devices` Returns list of logged in devices
///
/// requires auth: no
///
/// ### Response Example
/// ```
/// json!([
/// {
/// "device_name": "My Device!"
/// "created_at": "1752418856"
/// }
///
/// ]);
/// ```
pub async fn get(
State(app_state): State<Arc<AppState>>,
Extension(CurrentUser(uuid)): Extension<CurrentUser<Uuid>>,
) -> Result<impl IntoResponse, Error> {
let devices: Vec<Device> = dsl::refresh_tokens
.filter(dsl::uuid.eq(uuid))
.select(Device::as_select())
.get_results(&mut app_state.pool.get().await?)
.await?;
Ok((StatusCode::OK, Json(devices)))
}

View file

@ -1,39 +1,47 @@
use std::time::{SystemTime, UNIX_EPOCH}; use std::{
sync::Arc,
time::{SystemTime, UNIX_EPOCH},
};
use actix_web::{HttpResponse, post, web};
use argon2::{PasswordHash, PasswordVerifier}; use argon2::{PasswordHash, PasswordVerifier};
use axum::{
Json,
extract::State,
http::{HeaderValue, StatusCode},
response::IntoResponse,
};
use diesel::{ExpressionMethods, QueryDsl, dsl::insert_into}; use diesel::{ExpressionMethods, QueryDsl, dsl::insert_into};
use diesel_async::RunQueryDsl; use diesel_async::RunQueryDsl;
use serde::Deserialize; use serde::Deserialize;
use super::Response;
use crate::{ use crate::{
Data, AppState,
error::Error, error::Error,
schema::*, schema::*,
utils::{PASSWORD_REGEX, generate_token, new_refresh_token_cookie, user_uuid_from_identifier}, utils::{
PASSWORD_REGEX, generate_device_name, generate_token, new_refresh_token_cookie,
user_uuid_from_identifier,
},
}; };
use super::Response;
#[derive(Deserialize)] #[derive(Deserialize)]
struct LoginInformation { pub struct LoginInformation {
username: String, username: String,
password: String, password: String,
device_name: String,
} }
#[post("/login")]
pub async fn response( pub async fn response(
login_information: web::Json<LoginInformation>, State(app_state): State<Arc<AppState>>,
data: web::Data<Data>, Json(login_information): Json<LoginInformation>,
) -> Result<HttpResponse, Error> { ) -> Result<impl IntoResponse, Error> {
if !PASSWORD_REGEX.is_match(&login_information.password) { if !PASSWORD_REGEX.is_match(&login_information.password) {
return Ok(HttpResponse::Forbidden().json(r#"{ "password_hashed": false }"#)); return Err(Error::BadRequest("Bad password".to_string()));
} }
use users::dsl; use users::dsl;
let mut conn = data.pool.get().await?; let mut conn = app_state.pool.get().await?;
let uuid = user_uuid_from_identifier(&mut conn, &login_information.username).await?; let uuid = user_uuid_from_identifier(&mut conn, &login_information.username).await?;
@ -46,7 +54,7 @@ pub async fn response(
let parsed_hash = PasswordHash::new(&database_password) let parsed_hash = PasswordHash::new(&database_password)
.map_err(|e| Error::PasswordHashError(e.to_string()))?; .map_err(|e| Error::PasswordHashError(e.to_string()))?;
if data if app_state
.argon2 .argon2
.verify_password(login_information.password.as_bytes(), &parsed_hash) .verify_password(login_information.password.as_bytes(), &parsed_hash)
.is_err() .is_err()
@ -63,12 +71,14 @@ pub async fn response(
use refresh_tokens::dsl as rdsl; use refresh_tokens::dsl as rdsl;
let device_name = generate_device_name();
insert_into(refresh_tokens::table) insert_into(refresh_tokens::table)
.values(( .values((
rdsl::token.eq(&refresh_token), rdsl::token.eq(&refresh_token),
rdsl::uuid.eq(uuid), rdsl::uuid.eq(uuid),
rdsl::created_at.eq(current_time), rdsl::created_at.eq(current_time),
rdsl::device_name.eq(&login_information.device_name), rdsl::device_name.eq(&device_name),
)) ))
.execute(&mut conn) .execute(&mut conn)
.await?; .await?;
@ -85,7 +95,21 @@ pub async fn response(
.execute(&mut conn) .execute(&mut conn)
.await?; .await?;
Ok(HttpResponse::Ok() let mut response = (
.cookie(new_refresh_token_cookie(&data.config, refresh_token)) StatusCode::OK,
.json(Response { access_token })) Json(Response {
access_token,
device_name,
}),
)
.into_response();
response.headers_mut().append(
"Set-Cookie",
HeaderValue::from_str(
&new_refresh_token_cookie(&app_state.config, refresh_token).to_string(),
)?,
);
Ok(response)
} }

View file

@ -1,9 +1,16 @@
use actix_web::{HttpRequest, HttpResponse, get, web}; use std::sync::Arc;
use axum::{
extract::State,
http::{HeaderValue, StatusCode},
response::IntoResponse,
};
use axum_extra::extract::CookieJar;
use diesel::{ExpressionMethods, delete}; use diesel::{ExpressionMethods, delete};
use diesel_async::RunQueryDsl; use diesel_async::RunQueryDsl;
use crate::{ use crate::{
Data, AppState,
error::Error, error::Error,
schema::refresh_tokens::{self, dsl}, schema::refresh_tokens::{self, dsl},
}; };
@ -20,28 +27,39 @@ use crate::{
/// ///
/// 401 Unauthorized (no refresh token found) /// 401 Unauthorized (no refresh token found)
/// ///
#[get("/logout")] pub async fn res(
pub async fn res(req: HttpRequest, data: web::Data<Data>) -> Result<HttpResponse, Error> { State(app_state): State<Arc<AppState>>,
let mut refresh_token_cookie = req.cookie("refresh_token").ok_or(Error::Unauthorized( jar: CookieJar,
) -> Result<impl IntoResponse, Error> {
let mut refresh_token_cookie = jar
.get("refresh_token")
.ok_or(Error::Unauthorized(
"request has no refresh token".to_string(), "request has no refresh token".to_string(),
))?; ))?
.to_owned();
let refresh_token = String::from(refresh_token_cookie.value()); let refresh_token = String::from(refresh_token_cookie.value_trimmed());
let mut conn = data.pool.get().await?; let mut conn = app_state.pool.get().await?;
let deleted = delete(refresh_tokens::table) let deleted = delete(refresh_tokens::table)
.filter(dsl::token.eq(refresh_token)) .filter(dsl::token.eq(refresh_token))
.execute(&mut conn) .execute(&mut conn)
.await?; .await?;
refresh_token_cookie.make_removal(); let mut response;
if deleted == 0 { if deleted == 0 {
return Ok(HttpResponse::NotFound() response = StatusCode::NOT_FOUND.into_response();
.cookie(refresh_token_cookie) } else {
.finish()); response = StatusCode::OK.into_response();
} }
Ok(HttpResponse::Ok().cookie(refresh_token_cookie).finish()) refresh_token_cookie.make_removal();
response.headers_mut().append(
"Set-Cookie",
HeaderValue::from_str(&refresh_token_cookie.to_string())?,
);
Ok(response)
} }

View file

@ -1,13 +1,27 @@
use std::time::{SystemTime, UNIX_EPOCH}; use std::{
sync::Arc,
time::{SystemTime, UNIX_EPOCH},
};
use actix_web::{Scope, web}; use axum::{
Router,
extract::{Request, State},
middleware::{Next, from_fn_with_state},
response::IntoResponse,
routing::{delete, get, post},
};
use axum_extra::{
TypedHeader,
headers::{Authorization, authorization::Bearer},
};
use diesel::{ExpressionMethods, QueryDsl}; use diesel::{ExpressionMethods, QueryDsl};
use diesel_async::RunQueryDsl; use diesel_async::RunQueryDsl;
use serde::Serialize; use serde::Serialize;
use uuid::Uuid; use uuid::Uuid;
use crate::{Conn, error::Error, schema::access_tokens::dsl}; use crate::{AppState, Conn, error::Error, schema::access_tokens::dsl};
mod devices;
mod login; mod login;
mod logout; mod logout;
mod refresh; mod refresh;
@ -17,21 +31,27 @@ mod revoke;
mod verify_email; mod verify_email;
#[derive(Serialize)] #[derive(Serialize)]
struct Response { pub struct Response {
access_token: String, access_token: String,
device_name: String,
} }
pub fn web() -> Scope { pub fn router(app_state: Arc<AppState>) -> Router<Arc<AppState>> {
web::scope("/auth") let router_with_auth = Router::new()
.service(register::res) .route("/verify-email", get(verify_email::get))
.service(login::response) .route("/verify-email", post(verify_email::post))
.service(logout::res) .route("/revoke", post(revoke::post))
.service(refresh::res) .route("/devices", get(devices::get))
.service(revoke::res) .layer(from_fn_with_state(app_state, CurrentUser::check_auth_layer));
.service(verify_email::get)
.service(verify_email::post) Router::new()
.service(reset_password::get) .route("/register", post(register::post))
.service(reset_password::post) .route("/login", post(login::response))
.route("/logout", delete(logout::res))
.route("/refresh", post(refresh::post))
.route("/reset-password", get(reset_password::get))
.route("/reset-password", post(reset_password::post))
.merge(router_with_auth)
} }
pub async fn check_access_token(access_token: &str, conn: &mut Conn) -> Result<Uuid, Error> { pub async fn check_access_token(access_token: &str, conn: &mut Conn) -> Result<Uuid, Error> {
@ -58,3 +78,21 @@ pub async fn check_access_token(access_token: &str, conn: &mut Conn) -> Result<U
Ok(uuid) Ok(uuid)
} }
#[derive(Clone)]
pub struct CurrentUser<Uuid>(pub Uuid);
impl CurrentUser<Uuid> {
pub async fn check_auth_layer(
State(app_state): State<Arc<AppState>>,
TypedHeader(auth): TypedHeader<Authorization<Bearer>>,
mut req: Request,
next: Next,
) -> Result<impl IntoResponse, Error> {
let current_user =
CurrentUser(check_access_token(auth.token(), &mut app_state.pool.get().await?).await?);
req.extensions_mut().insert(current_user);
Ok(next.run(req).await)
}
}

View file

@ -1,11 +1,21 @@
use actix_web::{HttpRequest, HttpResponse, post, web}; use axum::{
Json,
extract::State,
http::{HeaderValue, StatusCode},
response::IntoResponse,
};
use axum_extra::extract::CookieJar;
use diesel::{ExpressionMethods, QueryDsl, delete, update}; use diesel::{ExpressionMethods, QueryDsl, delete, update};
use diesel_async::RunQueryDsl; use diesel_async::RunQueryDsl;
use log::error; use log::error;
use std::time::{SystemTime, UNIX_EPOCH}; use std::{
sync::Arc,
time::{SystemTime, UNIX_EPOCH},
};
use super::Response;
use crate::{ use crate::{
Data, AppState,
error::Error, error::Error,
schema::{ schema::{
access_tokens::{self, dsl}, access_tokens::{self, dsl},
@ -14,19 +24,22 @@ use crate::{
utils::{generate_token, new_refresh_token_cookie}, utils::{generate_token, new_refresh_token_cookie},
}; };
use super::Response; pub async fn post(
State(app_state): State<Arc<AppState>>,
#[post("/refresh")] jar: CookieJar,
pub async fn res(req: HttpRequest, data: web::Data<Data>) -> Result<HttpResponse, Error> { ) -> Result<impl IntoResponse, Error> {
let mut refresh_token_cookie = req.cookie("refresh_token").ok_or(Error::Unauthorized( let mut refresh_token_cookie = jar
.get("refresh_token")
.ok_or(Error::Unauthorized(
"request has no refresh token".to_string(), "request has no refresh token".to_string(),
))?; ))?
.to_owned();
let mut refresh_token = String::from(refresh_token_cookie.value()); let mut refresh_token = String::from(refresh_token_cookie.value_trimmed());
let current_time = SystemTime::now().duration_since(UNIX_EPOCH)?.as_secs() as i64; let current_time = SystemTime::now().duration_since(UNIX_EPOCH)?.as_secs() as i64;
let mut conn = data.pool.get().await?; let mut conn = app_state.pool.get().await?;
if let Ok(created_at) = rdsl::refresh_tokens if let Ok(created_at) = rdsl::refresh_tokens
.filter(rdsl::token.eq(&refresh_token)) .filter(rdsl::token.eq(&refresh_token))
@ -45,14 +58,19 @@ pub async fn res(req: HttpRequest, data: web::Data<Data>) -> Result<HttpResponse
error!("{error}"); error!("{error}");
} }
refresh_token_cookie.make_removal(); let mut response = StatusCode::UNAUTHORIZED.into_response();
return Ok(HttpResponse::Unauthorized() refresh_token_cookie.make_removal();
.cookie(refresh_token_cookie) response.headers_mut().append(
.finish()); "Set-Cookie",
HeaderValue::from_str(&refresh_token_cookie.to_string())?,
);
return Ok(response);
} }
let current_time = SystemTime::now().duration_since(UNIX_EPOCH)?.as_secs() as i64; let current_time = SystemTime::now().duration_since(UNIX_EPOCH)?.as_secs() as i64;
let mut device_name: String = String::new();
if lifetime > 1987200 { if lifetime > 1987200 {
let new_refresh_token = generate_token::<32>()?; let new_refresh_token = generate_token::<32>()?;
@ -63,11 +81,13 @@ pub async fn res(req: HttpRequest, data: web::Data<Data>) -> Result<HttpResponse
rdsl::token.eq(&new_refresh_token), rdsl::token.eq(&new_refresh_token),
rdsl::created_at.eq(current_time), rdsl::created_at.eq(current_time),
)) ))
.execute(&mut conn) .returning(rdsl::device_name)
.get_result::<String>(&mut conn)
.await .await
{ {
Ok(_) => { Ok(existing_device_name) => {
refresh_token = new_refresh_token; refresh_token = new_refresh_token;
device_name = existing_device_name;
} }
Err(error) => { Err(error) => {
error!("{error}"); error!("{error}");
@ -86,14 +106,33 @@ pub async fn res(req: HttpRequest, data: web::Data<Data>) -> Result<HttpResponse
.execute(&mut conn) .execute(&mut conn)
.await?; .await?;
return Ok(HttpResponse::Ok() let mut response = (
.cookie(new_refresh_token_cookie(&data.config, refresh_token)) StatusCode::OK,
.json(Response { access_token })); Json(Response {
access_token,
device_name,
}),
)
.into_response();
// TODO: Dont set this when refresh token is unchanged
response.headers_mut().append(
"Set-Cookie",
HeaderValue::from_str(
&new_refresh_token_cookie(&app_state.config, refresh_token).to_string(),
)?,
);
return Ok(response);
} }
let mut response = StatusCode::UNAUTHORIZED.into_response();
refresh_token_cookie.make_removal(); refresh_token_cookie.make_removal();
response.headers_mut().append(
"Set-Cookie",
HeaderValue::from_str(&refresh_token_cookie.to_string())?,
);
Ok(HttpResponse::Unauthorized() Ok(response)
.cookie(refresh_token_cookie)
.finish())
} }

View file

@ -1,10 +1,18 @@
use std::time::{SystemTime, UNIX_EPOCH}; use std::{
sync::Arc,
time::{SystemTime, UNIX_EPOCH},
};
use actix_web::{HttpResponse, post, web};
use argon2::{ use argon2::{
PasswordHasher, PasswordHasher,
password_hash::{SaltString, rand_core::OsRng}, password_hash::{SaltString, rand_core::OsRng},
}; };
use axum::{
Json,
extract::State,
http::{HeaderValue, StatusCode},
response::IntoResponse,
};
use diesel::{ExpressionMethods, dsl::insert_into}; use diesel::{ExpressionMethods, dsl::insert_into};
use diesel_async::RunQueryDsl; use diesel_async::RunQueryDsl;
use serde::{Deserialize, Serialize}; use serde::{Deserialize, Serialize};
@ -12,7 +20,7 @@ use uuid::Uuid;
use super::Response; use super::Response;
use crate::{ use crate::{
Data, AppState,
error::Error, error::Error,
objects::Member, objects::Member,
schema::{ schema::{
@ -21,30 +29,26 @@ use crate::{
users::{self, dsl as udsl}, users::{self, dsl as udsl},
}, },
utils::{ utils::{
EMAIL_REGEX, PASSWORD_REGEX, USERNAME_REGEX, generate_token, new_refresh_token_cookie, EMAIL_REGEX, PASSWORD_REGEX, USERNAME_REGEX, generate_device_name, generate_token,
new_refresh_token_cookie,
}, },
}; };
#[derive(Deserialize)] #[derive(Deserialize)]
struct AccountInformation { pub struct AccountInformation {
identifier: String, identifier: String,
email: String, email: String,
password: String, password: String,
device_name: String,
} }
#[derive(Serialize)] #[derive(Serialize)]
struct ResponseError { pub struct ResponseError {
signups_enabled: bool, signups_enabled: bool,
gorb_id_valid: bool, gorb_id_valid: bool,
gorb_id_available: bool, gorb_id_available: bool,
email_valid: bool, email_valid: bool,
email_available: bool, email_available: bool,
password_hashed: bool, password_strength: bool,
password_minimum_length: bool,
password_special_characters: bool,
password_letters: bool,
password_numbers: bool,
} }
impl Default for ResponseError { impl Default for ResponseError {
@ -55,21 +59,16 @@ impl Default for ResponseError {
gorb_id_available: true, gorb_id_available: true,
email_valid: true, email_valid: true,
email_available: true, email_available: true,
password_hashed: true, password_strength: true,
password_minimum_length: true,
password_special_characters: true,
password_letters: true,
password_numbers: true,
} }
} }
} }
#[post("/register")] pub async fn post(
pub async fn res( State(app_state): State<Arc<AppState>>,
account_information: web::Json<AccountInformation>, Json(account_information): Json<AccountInformation>,
data: web::Data<Data>, ) -> Result<impl IntoResponse, Error> {
) -> Result<HttpResponse, Error> { if !app_state.config.instance.registration {
if !data.config.instance.registration {
return Err(Error::Forbidden( return Err(Error::Forbidden(
"registration is disabled on this instance".to_string(), "registration is disabled on this instance".to_string(),
)); ));
@ -78,36 +77,48 @@ pub async fn res(
let uuid = Uuid::now_v7(); let uuid = Uuid::now_v7();
if !EMAIL_REGEX.is_match(&account_information.email) { if !EMAIL_REGEX.is_match(&account_information.email) {
return Ok(HttpResponse::Forbidden().json(ResponseError { return Ok((
StatusCode::FORBIDDEN,
Json(ResponseError {
email_valid: false, email_valid: false,
..Default::default() ..Default::default()
})); }),
)
.into_response());
} }
if !USERNAME_REGEX.is_match(&account_information.identifier) if !USERNAME_REGEX.is_match(&account_information.identifier)
|| account_information.identifier.len() < 3 || account_information.identifier.len() < 3
|| account_information.identifier.len() > 32 || account_information.identifier.len() > 32
{ {
return Ok(HttpResponse::Forbidden().json(ResponseError { return Ok((
StatusCode::FORBIDDEN,
Json(ResponseError {
gorb_id_valid: false, gorb_id_valid: false,
..Default::default() ..Default::default()
})); }),
)
.into_response());
} }
if !PASSWORD_REGEX.is_match(&account_information.password) { if !PASSWORD_REGEX.is_match(&account_information.password) {
return Ok(HttpResponse::Forbidden().json(ResponseError { return Ok((
password_hashed: false, StatusCode::FORBIDDEN,
Json(ResponseError {
password_strength: false,
..Default::default() ..Default::default()
})); }),
)
.into_response());
} }
let salt = SaltString::generate(&mut OsRng); let salt = SaltString::generate(&mut OsRng);
if let Ok(hashed_password) = data if let Ok(hashed_password) = app_state
.argon2 .argon2
.hash_password(account_information.password.as_bytes(), &salt) .hash_password(account_information.password.as_bytes(), &salt)
{ {
let mut conn = data.pool.get().await?; let mut conn = app_state.pool.get().await?;
// TODO: Check security of this implementation // TODO: Check security of this implementation
insert_into(users::table) insert_into(users::table)
@ -125,12 +136,14 @@ pub async fn res(
let current_time = SystemTime::now().duration_since(UNIX_EPOCH)?.as_secs() as i64; let current_time = SystemTime::now().duration_since(UNIX_EPOCH)?.as_secs() as i64;
let device_name = generate_device_name();
insert_into(refresh_tokens::table) insert_into(refresh_tokens::table)
.values(( .values((
rdsl::token.eq(&refresh_token), rdsl::token.eq(&refresh_token),
rdsl::uuid.eq(uuid), rdsl::uuid.eq(uuid),
rdsl::created_at.eq(current_time), rdsl::created_at.eq(current_time),
rdsl::device_name.eq(&account_information.device_name), rdsl::device_name.eq(&device_name),
)) ))
.execute(&mut conn) .execute(&mut conn)
.await?; .await?;
@ -145,14 +158,28 @@ pub async fn res(
.execute(&mut conn) .execute(&mut conn)
.await?; .await?;
if let Some(initial_guild) = data.config.instance.initial_guild { if let Some(initial_guild) = app_state.config.instance.initial_guild {
Member::new(&data, uuid, initial_guild).await?; Member::new(&mut conn, &app_state.cache_pool, uuid, initial_guild).await?;
} }
return Ok(HttpResponse::Ok() let mut response = (
.cookie(new_refresh_token_cookie(&data.config, refresh_token)) StatusCode::OK,
.json(Response { access_token })); Json(Response {
access_token,
device_name,
}),
)
.into_response();
response.headers_mut().append(
"Set-Cookie",
HeaderValue::from_str(
&new_refresh_token_cookie(&app_state.config, refresh_token).to_string(),
)?,
);
return Ok(response);
} }
Ok(HttpResponse::InternalServerError().finish()) Ok(StatusCode::INTERNAL_SERVER_ERROR.into_response())
} }

View file

@ -1,13 +1,20 @@
//! `/api/v1/auth/reset-password` Endpoints for resetting user password //! `/api/v1/auth/reset-password` Endpoints for resetting user password
use actix_web::{HttpResponse, get, post, web}; use std::sync::Arc;
use axum::{
Json,
extract::{Query, State},
http::StatusCode,
response::IntoResponse,
};
use chrono::{Duration, Utc}; use chrono::{Duration, Utc};
use serde::Deserialize; use serde::Deserialize;
use crate::{Data, error::Error, objects::PasswordResetToken}; use crate::{AppState, error::Error, objects::PasswordResetToken};
#[derive(Deserialize)] #[derive(Deserialize)]
struct Query { pub struct QueryParams {
identifier: String, identifier: String,
} }
@ -20,17 +27,28 @@ struct Query {
/// ///
/// ### Responses /// ### Responses
/// 200 Email sent /// 200 Email sent
///
/// 429 Too Many Requests /// 429 Too Many Requests
///
/// 404 Not found /// 404 Not found
///
/// 400 Bad request /// 400 Bad request
/// ///
#[get("/reset-password")] pub async fn get(
pub async fn get(query: web::Query<Query>, data: web::Data<Data>) -> Result<HttpResponse, Error> { State(app_state): State<Arc<AppState>>,
if let Ok(password_reset_token) = query: Query<QueryParams>,
PasswordResetToken::get_with_identifier(&data, query.identifier.clone()).await ) -> Result<impl IntoResponse, Error> {
let mut conn = app_state.pool.get().await?;
if let Ok(password_reset_token) = PasswordResetToken::get_with_identifier(
&mut conn,
&app_state.cache_pool,
query.identifier.clone(),
)
.await
{ {
if Utc::now().signed_duration_since(password_reset_token.created_at) > Duration::hours(1) { if Utc::now().signed_duration_since(password_reset_token.created_at) > Duration::hours(1) {
password_reset_token.delete(&data).await?; password_reset_token.delete(&app_state.cache_pool).await?;
} else { } else {
return Err(Error::TooManyRequests( return Err(Error::TooManyRequests(
"Please allow 1 hour before sending a new email".to_string(), "Please allow 1 hour before sending a new email".to_string(),
@ -38,13 +56,13 @@ pub async fn get(query: web::Query<Query>, data: web::Data<Data>) -> Result<Http
} }
} }
PasswordResetToken::new(&data, query.identifier.clone()).await?; PasswordResetToken::new(&mut conn, &app_state, query.identifier.clone()).await?;
Ok(HttpResponse::Ok().finish()) Ok(StatusCode::OK)
} }
#[derive(Deserialize)] #[derive(Deserialize)]
struct ResetPassword { pub struct ResetPassword {
password: String, password: String,
token: String, token: String,
} }
@ -63,20 +81,27 @@ struct ResetPassword {
/// ///
/// ### Responses /// ### Responses
/// 200 Success /// 200 Success
///
/// 410 Token Expired /// 410 Token Expired
///
/// 404 Not Found /// 404 Not Found
///
/// 400 Bad Request /// 400 Bad Request
/// ///
#[post("/reset-password")]
pub async fn post( pub async fn post(
reset_password: web::Json<ResetPassword>, State(app_state): State<Arc<AppState>>,
data: web::Data<Data>, reset_password: Json<ResetPassword>,
) -> Result<HttpResponse, Error> { ) -> Result<impl IntoResponse, Error> {
let password_reset_token = PasswordResetToken::get(&data, reset_password.token.clone()).await?; let password_reset_token =
PasswordResetToken::get(&app_state.cache_pool, reset_password.token.clone()).await?;
password_reset_token password_reset_token
.set_password(&data, reset_password.password.clone()) .set_password(
&mut app_state.pool.get().await?,
&app_state,
reset_password.password.clone(),
)
.await?; .await?;
Ok(HttpResponse::Ok().finish()) Ok(StatusCode::OK)
} }

View file

@ -1,38 +1,35 @@
use actix_web::{HttpRequest, HttpResponse, post, web}; use std::sync::Arc;
use argon2::{PasswordHash, PasswordVerifier}; use argon2::{PasswordHash, PasswordVerifier};
use axum::{Extension, Json, extract::State, http::StatusCode, response::IntoResponse};
use diesel::{ExpressionMethods, QueryDsl, delete}; use diesel::{ExpressionMethods, QueryDsl, delete};
use diesel_async::RunQueryDsl; use diesel_async::RunQueryDsl;
use serde::Deserialize; use serde::Deserialize;
use uuid::Uuid;
use crate::{ use crate::{
Data, AppState,
api::v1::auth::check_access_token, api::v1::auth::CurrentUser,
error::Error, error::Error,
schema::refresh_tokens::{self, dsl as rdsl}, schema::{
schema::users::dsl as udsl, refresh_tokens::{self, dsl as rdsl},
utils::get_auth_header, users::dsl as udsl,
},
}; };
#[derive(Deserialize)] #[derive(Deserialize)]
struct RevokeRequest { pub struct RevokeRequest {
password: String, password: String,
device_name: String, device_name: String,
} }
// TODO: Should maybe be a delete request? // TODO: Should maybe be a delete request?
#[post("/revoke")] pub async fn post(
pub async fn res( State(app_state): State<Arc<AppState>>,
req: HttpRequest, Extension(CurrentUser(uuid)): Extension<CurrentUser<Uuid>>,
revoke_request: web::Json<RevokeRequest>, Json(revoke_request): Json<RevokeRequest>,
data: web::Data<Data>, ) -> Result<impl IntoResponse, Error> {
) -> Result<HttpResponse, Error> { let mut conn = app_state.pool.get().await?;
let headers = req.headers();
let auth_header = get_auth_header(headers)?;
let mut conn = data.pool.get().await?;
let uuid = check_access_token(auth_header, &mut conn).await?;
let database_password: String = udsl::users let database_password: String = udsl::users
.filter(udsl::uuid.eq(uuid)) .filter(udsl::uuid.eq(uuid))
@ -43,7 +40,7 @@ pub async fn res(
let hashed_password = PasswordHash::new(&database_password) let hashed_password = PasswordHash::new(&database_password)
.map_err(|e| Error::PasswordHashError(e.to_string()))?; .map_err(|e| Error::PasswordHashError(e.to_string()))?;
if data if app_state
.argon2 .argon2
.verify_password(revoke_request.password.as_bytes(), &hashed_password) .verify_password(revoke_request.password.as_bytes(), &hashed_password)
.is_err() .is_err()
@ -59,5 +56,5 @@ pub async fn res(
.execute(&mut conn) .execute(&mut conn)
.await?; .await?;
Ok(HttpResponse::Ok().finish()) Ok(StatusCode::OK)
} }

View file

@ -1,19 +1,26 @@
//! `/api/v1/auth/verify-email` Endpoints for verifying user emails //! `/api/v1/auth/verify-email` Endpoints for verifying user emails
use actix_web::{HttpRequest, HttpResponse, get, post, web}; use std::sync::Arc;
use axum::{
Extension,
extract::{Query, State},
http::StatusCode,
response::IntoResponse,
};
use chrono::{Duration, Utc}; use chrono::{Duration, Utc};
use serde::Deserialize; use serde::Deserialize;
use uuid::Uuid;
use crate::{ use crate::{
Data, AppState,
api::v1::auth::check_access_token, api::v1::auth::CurrentUser,
error::Error, error::Error,
objects::{EmailToken, Me}, objects::{EmailToken, Me},
utils::get_auth_header,
}; };
#[derive(Deserialize)] #[derive(Deserialize)]
struct Query { pub struct QueryParams {
token: String, token: String,
} }
@ -35,37 +42,30 @@ struct Query {
/// ///
/// 401 Unauthorized /// 401 Unauthorized
/// ///
#[get("/verify-email")]
pub async fn get( pub async fn get(
req: HttpRequest, State(app_state): State<Arc<AppState>>,
query: web::Query<Query>, Query(query): Query<QueryParams>,
data: web::Data<Data>, Extension(CurrentUser(uuid)): Extension<CurrentUser<Uuid>>,
) -> Result<HttpResponse, Error> { ) -> Result<impl IntoResponse, Error> {
let headers = req.headers(); let mut conn = app_state.pool.get().await?;
let auth_header = get_auth_header(headers)?;
let mut conn = data.pool.get().await?;
let uuid = check_access_token(auth_header, &mut conn).await?;
let me = Me::get(&mut conn, uuid).await?; let me = Me::get(&mut conn, uuid).await?;
if me.email_verified { if me.email_verified {
return Ok(HttpResponse::NoContent().finish()); return Ok(StatusCode::NO_CONTENT);
} }
let email_token = EmailToken::get(&data, me.uuid).await?; let email_token = EmailToken::get(&app_state.cache_pool, me.uuid).await?;
if query.token != email_token.token { if query.token != email_token.token {
return Ok(HttpResponse::Unauthorized().finish()); return Ok(StatusCode::UNAUTHORIZED);
} }
me.verify_email(&mut conn).await?; me.verify_email(&mut conn).await?;
email_token.delete(&data).await?; email_token.delete(&app_state.cache_pool).await?;
Ok(HttpResponse::Ok().finish()) Ok(StatusCode::OK)
} }
/// `POST /api/v1/auth/verify-email` Sends user verification email /// `POST /api/v1/auth/verify-email` Sends user verification email
@ -81,25 +81,19 @@ pub async fn get(
/// ///
/// 401 Unauthorized /// 401 Unauthorized
/// ///
#[post("/verify-email")] pub async fn post(
pub async fn post(req: HttpRequest, data: web::Data<Data>) -> Result<HttpResponse, Error> { State(app_state): State<Arc<AppState>>,
let headers = req.headers(); Extension(CurrentUser(uuid)): Extension<CurrentUser<Uuid>>,
) -> Result<impl IntoResponse, Error> {
let auth_header = get_auth_header(headers)?; let me = Me::get(&mut app_state.pool.get().await?, uuid).await?;
let mut conn = data.pool.get().await?;
let uuid = check_access_token(auth_header, &mut conn).await?;
let me = Me::get(&mut conn, uuid).await?;
if me.email_verified { if me.email_verified {
return Ok(HttpResponse::NoContent().finish()); return Ok(StatusCode::NO_CONTENT);
} }
if let Ok(email_token) = EmailToken::get(&data, me.uuid).await { if let Ok(email_token) = EmailToken::get(&app_state.cache_pool, me.uuid).await {
if Utc::now().signed_duration_since(email_token.created_at) > Duration::hours(1) { if Utc::now().signed_duration_since(email_token.created_at) > Duration::hours(1) {
email_token.delete(&data).await?; email_token.delete(&app_state.cache_pool).await?;
} else { } else {
return Err(Error::TooManyRequests( return Err(Error::TooManyRequests(
"Please allow 1 hour before sending a new email".to_string(), "Please allow 1 hour before sending a new email".to_string(),
@ -107,7 +101,7 @@ pub async fn post(req: HttpRequest, data: web::Data<Data>) -> Result<HttpRespons
} }
} }
EmailToken::new(&data, me).await?; EmailToken::new(&app_state, me).await?;
Ok(HttpResponse::Ok().finish()) Ok(StatusCode::OK)
} }

View file

@ -1,12 +1,25 @@
use actix_web::{Scope, web}; use std::sync::Arc;
use axum::{
Router,
middleware::from_fn_with_state,
routing::{any, delete, get, patch},
};
//use socketioxide::SocketIo;
use crate::{AppState, api::v1::auth::CurrentUser};
mod uuid; mod uuid;
pub fn web() -> Scope { pub fn router(app_state: Arc<AppState>) -> Router<Arc<AppState>> {
web::scope("/channels") let router_with_auth = Router::new()
.service(uuid::get) .route("/{uuid}", get(uuid::get))
.service(uuid::delete) .route("/{uuid}", delete(uuid::delete))
.service(uuid::patch) .route("/{uuid}", patch(uuid::patch))
.service(uuid::messages::get) .route("/{uuid}/messages", get(uuid::messages::get))
.service(uuid::socket::ws) .layer(from_fn_with_state(app_state, CurrentUser::check_auth_layer));
Router::new()
.route("/{uuid}/socket", any(uuid::socket::ws))
.merge(router_with_auth)
} }

View file

@ -1,18 +1,25 @@
//! `/api/v1/channels/{uuid}/messages` Endpoints related to channel messages //! `/api/v1/channels/{uuid}/messages` Endpoints related to channel messages
use std::sync::Arc;
use crate::{ use crate::{
Data, AppState,
api::v1::auth::check_access_token, api::v1::auth::CurrentUser,
error::Error, error::Error,
objects::{Channel, Member}, objects::{Channel, Member},
utils::{get_auth_header, global_checks}, utils::global_checks,
}; };
use ::uuid::Uuid; use ::uuid::Uuid;
use actix_web::{HttpRequest, HttpResponse, get, web}; use axum::{
Extension, Json,
extract::{Path, Query, State},
http::StatusCode,
response::IntoResponse,
};
use serde::Deserialize; use serde::Deserialize;
#[derive(Deserialize)] #[derive(Deserialize)]
struct MessageRequest { pub struct MessageRequest {
amount: i64, amount: i64,
offset: i64, offset: i64,
} }
@ -47,32 +54,28 @@ struct MessageRequest {
/// }); /// });
/// ``` /// ```
/// ///
#[get("/{uuid}/messages")]
pub async fn get( pub async fn get(
req: HttpRequest, State(app_state): State<Arc<AppState>>,
path: web::Path<(Uuid,)>, Path(channel_uuid): Path<Uuid>,
message_request: web::Query<MessageRequest>, Query(message_request): Query<MessageRequest>,
data: web::Data<Data>, Extension(CurrentUser(uuid)): Extension<CurrentUser<Uuid>>,
) -> Result<HttpResponse, Error> { ) -> Result<impl IntoResponse, Error> {
let headers = req.headers(); let mut conn = app_state.pool.get().await?;
let auth_header = get_auth_header(headers)?; global_checks(&mut conn, &app_state.config, uuid).await?;
let channel_uuid = path.into_inner().0; let channel = Channel::fetch_one(&mut conn, &app_state.cache_pool, channel_uuid).await?;
let mut conn = data.pool.get().await?;
let uuid = check_access_token(auth_header, &mut conn).await?;
global_checks(&data, uuid).await?;
let channel = Channel::fetch_one(&data, channel_uuid).await?;
Member::check_membership(&mut conn, uuid, channel.guild_uuid).await?; Member::check_membership(&mut conn, uuid, channel.guild_uuid).await?;
let messages = channel let messages = channel
.fetch_messages(&data, message_request.amount, message_request.offset) .fetch_messages(
&mut conn,
&app_state.cache_pool,
message_request.amount,
message_request.offset,
)
.await?; .await?;
Ok(HttpResponse::Ok().json(messages)) Ok((StatusCode::OK, Json(messages)))
} }

View file

@ -3,75 +3,65 @@
pub mod messages; pub mod messages;
pub mod socket; pub mod socket;
use std::sync::Arc;
use crate::{ use crate::{
Data, AppState,
api::v1::auth::check_access_token, api::v1::auth::CurrentUser,
error::Error, error::Error,
objects::{Channel, Member, Permissions}, objects::{Channel, Member, Permissions},
utils::{get_auth_header, global_checks}, utils::global_checks,
}; };
use actix_web::{HttpRequest, HttpResponse, delete, get, patch, web}; use axum::{
Extension, Json,
extract::{Path, State},
http::StatusCode,
response::IntoResponse,
};
use serde::Deserialize; use serde::Deserialize;
use uuid::Uuid; use uuid::Uuid;
#[get("/{uuid}")]
pub async fn get( pub async fn get(
req: HttpRequest, State(app_state): State<Arc<AppState>>,
path: web::Path<(Uuid,)>, Path(channel_uuid): Path<Uuid>,
data: web::Data<Data>, Extension(CurrentUser(uuid)): Extension<CurrentUser<Uuid>>,
) -> Result<HttpResponse, Error> { ) -> Result<impl IntoResponse, Error> {
let headers = req.headers(); let mut conn = app_state.pool.get().await?;
let auth_header = get_auth_header(headers)?; global_checks(&mut conn, &app_state.config, uuid).await?;
let channel_uuid = path.into_inner().0; let channel = Channel::fetch_one(&mut conn, &app_state.cache_pool, channel_uuid).await?;
let mut conn = data.pool.get().await?;
let uuid = check_access_token(auth_header, &mut conn).await?;
global_checks(&data, uuid).await?;
let channel = Channel::fetch_one(&data, channel_uuid).await?;
Member::check_membership(&mut conn, uuid, channel.guild_uuid).await?; Member::check_membership(&mut conn, uuid, channel.guild_uuid).await?;
Ok(HttpResponse::Ok().json(channel)) Ok((StatusCode::OK, Json(channel)))
} }
#[delete("/{uuid}")]
pub async fn delete( pub async fn delete(
req: HttpRequest, State(app_state): State<Arc<AppState>>,
path: web::Path<(Uuid,)>, Path(channel_uuid): Path<Uuid>,
data: web::Data<Data>, Extension(CurrentUser(uuid)): Extension<CurrentUser<Uuid>>,
) -> Result<HttpResponse, Error> { ) -> Result<impl IntoResponse, Error> {
let headers = req.headers(); let mut conn = app_state.pool.get().await?;
let auth_header = get_auth_header(headers)?; global_checks(&mut conn, &app_state.config, uuid).await?;
let channel_uuid = path.into_inner().0; let channel = Channel::fetch_one(&mut conn, &app_state.cache_pool, channel_uuid).await?;
let mut conn = data.pool.get().await?;
let uuid = check_access_token(auth_header, &mut conn).await?;
global_checks(&data, uuid).await?;
let channel = Channel::fetch_one(&data, channel_uuid).await?;
let member = Member::check_membership(&mut conn, uuid, channel.guild_uuid).await?; let member = Member::check_membership(&mut conn, uuid, channel.guild_uuid).await?;
member member
.check_permission(&data, Permissions::DeleteChannel) .check_permission(&mut conn, &app_state.cache_pool, Permissions::ManageChannel)
.await?; .await?;
channel.delete(&data).await?; channel.delete(&mut conn, &app_state.cache_pool).await?;
Ok(HttpResponse::Ok().finish()) Ok(StatusCode::OK)
} }
#[derive(Deserialize)] #[derive(Deserialize)]
struct NewInfo { pub struct NewInfo {
name: Option<String>, name: Option<String>,
description: Option<String>, description: Option<String>,
is_above: Option<String>, is_above: Option<String>,
@ -108,48 +98,45 @@ struct NewInfo {
/// }); /// });
/// ``` /// ```
/// NOTE: UUIDs in this response are made using `uuidgen`, UUIDs made by the actual backend will be UUIDv7 and have extractable timestamps /// NOTE: UUIDs in this response are made using `uuidgen`, UUIDs made by the actual backend will be UUIDv7 and have extractable timestamps
#[patch("/{uuid}")]
pub async fn patch( pub async fn patch(
req: HttpRequest, State(app_state): State<Arc<AppState>>,
path: web::Path<(Uuid,)>, Path(channel_uuid): Path<Uuid>,
new_info: web::Json<NewInfo>, Extension(CurrentUser(uuid)): Extension<CurrentUser<Uuid>>,
data: web::Data<Data>, Json(new_info): Json<NewInfo>,
) -> Result<HttpResponse, Error> { ) -> Result<impl IntoResponse, Error> {
let headers = req.headers(); let mut conn = app_state.pool.get().await?;
let auth_header = get_auth_header(headers)?; global_checks(&mut conn, &app_state.config, uuid).await?;
let channel_uuid = path.into_inner().0; let mut channel = Channel::fetch_one(&mut conn, &app_state.cache_pool, channel_uuid).await?;
let mut conn = data.pool.get().await?;
let uuid = check_access_token(auth_header, &mut conn).await?;
global_checks(&data, uuid).await?;
let mut channel = Channel::fetch_one(&data, channel_uuid).await?;
let member = Member::check_membership(&mut conn, uuid, channel.guild_uuid).await?; let member = Member::check_membership(&mut conn, uuid, channel.guild_uuid).await?;
member member
.check_permission(&data, Permissions::ManageChannel) .check_permission(&mut conn, &app_state.cache_pool, Permissions::ManageChannel)
.await?; .await?;
if let Some(new_name) = &new_info.name { if let Some(new_name) = &new_info.name {
channel.set_name(&data, new_name.to_string()).await?; channel
.set_name(&mut conn, &app_state.cache_pool, new_name.to_string())
.await?;
} }
if let Some(new_description) = &new_info.description { if let Some(new_description) = &new_info.description {
channel channel
.set_description(&data, new_description.to_string()) .set_description(
&mut conn,
&app_state.cache_pool,
new_description.to_string(),
)
.await?; .await?;
} }
if let Some(new_is_above) = &new_info.is_above { if let Some(new_is_above) = &new_info.is_above {
channel channel
.set_description(&data, new_is_above.to_string()) .set_description(&mut conn, &app_state.cache_pool, new_is_above.to_string())
.await?; .await?;
} }
Ok(HttpResponse::Ok().json(channel)) Ok((StatusCode::OK, Json(channel)))
} }

View file

@ -1,18 +1,20 @@
use actix_web::{ use std::sync::Arc;
Error, HttpRequest, HttpResponse, get,
http::header::{HeaderValue, SEC_WEBSOCKET_PROTOCOL}, use axum::{
rt, web, extract::{Path, State, WebSocketUpgrade, ws::Message},
http::HeaderMap,
response::IntoResponse,
}; };
use actix_ws::AggregatedMessage; use futures_util::{SinkExt, StreamExt};
use futures_util::StreamExt as _;
use serde::Deserialize; use serde::Deserialize;
use uuid::Uuid; use uuid::Uuid;
use crate::{ use crate::{
Data, AppState,
api::v1::auth::check_access_token, api::v1::auth::check_access_token,
error::Error,
objects::{Channel, Member}, objects::{Channel, Member},
utils::{get_ws_protocol_header, global_checks}, utils::global_checks,
}; };
#[derive(Deserialize)] #[derive(Deserialize)]
@ -21,100 +23,115 @@ struct MessageBody {
reply_to: Option<Uuid>, reply_to: Option<Uuid>,
} }
#[get("/{uuid}/socket")]
pub async fn ws( pub async fn ws(
req: HttpRequest, ws: WebSocketUpgrade,
path: web::Path<(Uuid,)>, State(app_state): State<Arc<AppState>>,
stream: web::Payload, Path(channel_uuid): Path<Uuid>,
data: web::Data<Data>, headers: HeaderMap,
) -> Result<HttpResponse, Error> { ) -> Result<impl IntoResponse, Error> {
// Get all headers
let headers = req.headers();
// Retrieve auth header // Retrieve auth header
let auth_header = get_ws_protocol_header(headers)?; let auth_token = headers.get(axum::http::header::SEC_WEBSOCKET_PROTOCOL);
// Get uuid from path if auth_token.is_none() {
let channel_uuid = path.into_inner().0; return Err(Error::Unauthorized(
"No authorization header provided".to_string(),
));
}
let mut conn = data.pool.get().await.map_err(crate::error::Error::from)?; let auth_raw = auth_token.unwrap().to_str()?;
let mut auth = auth_raw.split_whitespace();
let response_proto = auth.next();
let auth_value = auth.next();
if response_proto.is_none() {
return Err(Error::BadRequest(
"Sec-WebSocket-Protocol header is empty".to_string(),
));
} else if response_proto.is_some_and(|rp| rp != "Authorization,") {
return Err(Error::BadRequest(
"First protocol should be Authorization".to_string(),
));
}
if auth_value.is_none() {
return Err(Error::BadRequest("No token provided".to_string()));
}
let auth_header = auth_value.unwrap();
let mut conn = app_state
.pool
.get()
.await
.map_err(crate::error::Error::from)?;
// Authorize client using auth header // Authorize client using auth header
let uuid = check_access_token(auth_header, &mut conn).await?; let uuid = check_access_token(auth_header, &mut conn).await?;
global_checks(&data, uuid).await?; global_checks(&mut conn, &app_state.config, uuid).await?;
let channel = Channel::fetch_one(&data, channel_uuid).await?; let channel = Channel::fetch_one(&mut conn, &app_state.cache_pool, channel_uuid).await?;
Member::check_membership(&mut conn, uuid, channel.guild_uuid).await?; Member::check_membership(&mut conn, uuid, channel.guild_uuid).await?;
let (mut res, mut session_1, stream) = actix_ws::handle(&req, stream)?; let mut pubsub = app_state
let mut stream = stream
.aggregate_continuations()
// aggregate continuation frames up to 1MiB
.max_continuation_size(2_usize.pow(20));
let mut pubsub = data
.cache_pool .cache_pool
.get_async_pubsub() .get_async_pubsub()
.await .await
.map_err(crate::error::Error::from)?; .map_err(crate::error::Error::from)?;
let mut session_2 = session_1.clone(); let mut res = ws.on_upgrade(async move |socket| {
let (mut sender, mut receiver) = socket.split();
rt::spawn(async move { tokio::spawn(async move {
pubsub.subscribe(channel_uuid.to_string()).await?; pubsub.subscribe(channel_uuid.to_string()).await?;
while let Some(msg) = pubsub.on_message().next().await { while let Some(msg) = pubsub.on_message().next().await {
let payload: String = msg.get_payload()?; let payload: String = msg.get_payload()?;
session_1.text(payload).await?; sender.send(payload.into()).await?;
} }
Ok::<(), crate::error::Error>(()) Ok::<(), crate::error::Error>(())
}); });
// start task but don't wait for it tokio::spawn(async move {
rt::spawn(async move { while let Some(msg) = receiver.next().await {
// receive messages from websocket if let Ok(Message::Text(text)) = msg {
while let Some(msg) = stream.next().await {
match msg {
Ok(AggregatedMessage::Text(text)) => {
let mut conn = data.cache_pool.get_multiplexed_tokio_connection().await?;
let message_body: MessageBody = serde_json::from_str(&text)?; let message_body: MessageBody = serde_json::from_str(&text)?;
let message = channel let message = channel
.new_message(&data, uuid, message_body.message, message_body.reply_to) .new_message(
&mut conn,
&app_state.cache_pool,
uuid,
message_body.message,
message_body.reply_to,
)
.await?; .await?;
redis::cmd("PUBLISH") redis::cmd("PUBLISH")
.arg(&[channel_uuid.to_string(), serde_json::to_string(&message)?]) .arg(&[channel_uuid.to_string(), serde_json::to_string(&message)?])
.exec_async(&mut conn) .exec_async(
&mut app_state
.cache_pool
.get_multiplexed_tokio_connection()
.await?,
)
.await?; .await?;
} }
Ok(AggregatedMessage::Binary(bin)) => {
// echo binary message
session_2.binary(bin).await?;
}
Ok(AggregatedMessage::Ping(msg)) => {
// respond to PING frame with PONG frame
session_2.pong(&msg).await?;
}
_ => {}
}
} }
Ok::<(), crate::error::Error>(()) Ok::<(), crate::error::Error>(())
}); });
});
let headers = res.headers_mut(); let headers = res.headers_mut();
headers.append( headers.append(
SEC_WEBSOCKET_PROTOCOL, axum::http::header::SEC_WEBSOCKET_PROTOCOL,
HeaderValue::from_str("Authorization")?, "Authorization".parse()?,
); );
// respond immediately with response connected to WS session // respond immediately with response connected to WS session

View file

@ -1,28 +1,37 @@
//! `/api/v1/guilds` Guild related endpoints //! `/api/v1/guilds` Guild related endpoints
use actix_web::{HttpRequest, HttpResponse, Scope, get, post, web}; use std::sync::Arc;
use ::uuid::Uuid;
use axum::{
Extension, Json, Router,
extract::State,
http::StatusCode,
response::IntoResponse,
routing::{get, post},
};
use serde::Deserialize; use serde::Deserialize;
mod uuid; mod uuid;
use crate::{ use crate::{
Data, AppState,
api::v1::auth::check_access_token, api::v1::auth::CurrentUser,
error::Error, error::Error,
objects::{Guild, StartAmountQuery}, objects::{Guild, StartAmountQuery},
utils::{get_auth_header, global_checks}, utils::global_checks,
}; };
#[derive(Deserialize)] #[derive(Deserialize)]
struct GuildInfo { pub struct GuildInfo {
name: String, name: String,
} }
pub fn web() -> Scope { pub fn router() -> Router<Arc<AppState>> {
web::scope("/guilds") Router::new()
.service(post) .route("/", post(new))
.service(get) .route("/", get(get_guilds))
.service(uuid::web()) .nest("/{uuid}", uuid::router())
} }
/// `POST /api/v1/guilds` Creates a new guild /// `POST /api/v1/guilds` Creates a new guild
@ -49,23 +58,19 @@ pub fn web() -> Scope {
/// }); /// });
/// ``` /// ```
/// NOTE: UUIDs in this response are made using `uuidgen`, UUIDs made by the actual backend will be UUIDv7 and have extractable timestamps /// NOTE: UUIDs in this response are made using `uuidgen`, UUIDs made by the actual backend will be UUIDv7 and have extractable timestamps
#[post("")] pub async fn new(
pub async fn post( State(app_state): State<Arc<AppState>>,
req: HttpRequest, Extension(CurrentUser(uuid)): Extension<CurrentUser<Uuid>>,
guild_info: web::Json<GuildInfo>, Json(guild_info): Json<GuildInfo>,
data: web::Data<Data>, ) -> Result<impl IntoResponse, Error> {
) -> Result<HttpResponse, Error> { let guild = Guild::new(
let headers = req.headers(); &mut app_state.pool.get().await?,
guild_info.name.clone(),
uuid,
)
.await?;
let auth_header = get_auth_header(headers)?; Ok((StatusCode::OK, Json(guild)))
let mut conn = data.pool.get().await?;
let uuid = check_access_token(auth_header, &mut conn).await?;
let guild = Guild::new(&mut conn, guild_info.name.clone(), uuid).await?;
Ok(HttpResponse::Ok().json(guild))
} }
/// `GET /api/v1/servers` Fetches all guilds /// `GET /api/v1/servers` Fetches all guilds
@ -115,25 +120,19 @@ pub async fn post(
/// ]); /// ]);
/// ``` /// ```
/// NOTE: UUIDs in this response are made using `uuidgen`, UUIDs made by the actual backend will be UUIDv7 and have extractable timestamps /// NOTE: UUIDs in this response are made using `uuidgen`, UUIDs made by the actual backend will be UUIDv7 and have extractable timestamps
#[get("")] pub async fn get_guilds(
pub async fn get( State(app_state): State<Arc<AppState>>,
req: HttpRequest, Extension(CurrentUser(uuid)): Extension<CurrentUser<Uuid>>,
request_query: web::Query<StartAmountQuery>, Json(request_query): Json<StartAmountQuery>,
data: web::Data<Data>, ) -> Result<impl IntoResponse, Error> {
) -> Result<HttpResponse, Error> {
let headers = req.headers();
let auth_header = get_auth_header(headers)?;
let start = request_query.start.unwrap_or(0); let start = request_query.start.unwrap_or(0);
let amount = request_query.amount.unwrap_or(10); let amount = request_query.amount.unwrap_or(10);
let uuid = check_access_token(auth_header, &mut data.pool.get().await?).await?; let mut conn = app_state.pool.get().await?;
global_checks(&data, uuid).await?; global_checks(&mut conn, &app_state.config, uuid).await?;
let guilds = Guild::fetch_amount(&data.pool, start, amount).await?; let guilds = Guild::fetch_amount(&mut conn, start, amount).await?;
Ok(HttpResponse::Ok().json(guilds)) Ok((StatusCode::OK, Json(guilds)))
} }

View file

@ -0,0 +1,57 @@
use std::sync::Arc;
use axum::{
Extension, Json,
extract::{Path, State},
http::StatusCode,
response::IntoResponse,
};
use uuid::Uuid;
use crate::{
AppState,
api::v1::auth::CurrentUser,
error::Error,
objects::{GuildBan, Member, Permissions},
utils::global_checks,
};
pub async fn get(
State(app_state): State<Arc<AppState>>,
Path(guild_uuid): Path<Uuid>,
Extension(CurrentUser(uuid)): Extension<CurrentUser<Uuid>>,
) -> Result<impl IntoResponse, Error> {
let mut conn = app_state.pool.get().await?;
global_checks(&mut conn, &app_state.config, uuid).await?;
let caller = Member::check_membership(&mut conn, uuid, guild_uuid).await?;
caller
.check_permission(&mut conn, &app_state.cache_pool, Permissions::BanMember)
.await?;
let all_guild_bans = GuildBan::fetch_all(&mut conn, guild_uuid).await?;
Ok((StatusCode::OK, Json(all_guild_bans)))
}
pub async fn unban(
State(app_state): State<Arc<AppState>>,
Path((guild_uuid, user_uuid)): Path<(Uuid, Uuid)>,
Extension(CurrentUser(uuid)): Extension<CurrentUser<Uuid>>,
) -> Result<impl IntoResponse, Error> {
let mut conn = app_state.pool.get().await?;
global_checks(&mut conn, &app_state.config, uuid).await?;
let caller = Member::check_membership(&mut conn, uuid, guild_uuid).await?;
caller
.check_permission(&mut conn, &app_state.cache_pool, Permissions::BanMember)
.await?;
let ban = GuildBan::fetch_one(&mut conn, guild_uuid, user_uuid).await?;
ban.unban(&mut conn).await?;
Ok(StatusCode::OK)
}

View file

@ -1,92 +1,87 @@
use crate::{ use std::sync::Arc;
Data,
api::v1::auth::check_access_token,
error::Error,
objects::{Channel, Member, Permissions},
utils::{get_auth_header, global_checks, order_by_is_above},
};
use ::uuid::Uuid; use ::uuid::Uuid;
use actix_web::{HttpRequest, HttpResponse, get, post, web}; use axum::{
Extension, Json,
extract::{Path, State},
http::StatusCode,
response::IntoResponse,
};
use serde::Deserialize; use serde::Deserialize;
use crate::{
AppState,
api::v1::auth::CurrentUser,
error::Error,
objects::{Channel, Member, Permissions},
utils::{CacheFns, global_checks, order_by_is_above},
};
#[derive(Deserialize)] #[derive(Deserialize)]
struct ChannelInfo { pub struct ChannelInfo {
name: String, name: String,
description: Option<String>, description: Option<String>,
} }
#[get("{uuid}/channels")]
pub async fn get( pub async fn get(
req: HttpRequest, State(app_state): State<Arc<AppState>>,
path: web::Path<(Uuid,)>, Path(guild_uuid): Path<Uuid>,
data: web::Data<Data>, Extension(CurrentUser(uuid)): Extension<CurrentUser<Uuid>>,
) -> Result<HttpResponse, Error> { ) -> Result<impl IntoResponse, Error> {
let headers = req.headers(); let mut conn = app_state.pool.get().await?;
let auth_header = get_auth_header(headers)?; global_checks(&mut conn, &app_state.config, uuid).await?;
let guild_uuid = path.into_inner().0;
let mut conn = data.pool.get().await?;
let uuid = check_access_token(auth_header, &mut conn).await?;
global_checks(&data, uuid).await?;
Member::check_membership(&mut conn, uuid, guild_uuid).await?; Member::check_membership(&mut conn, uuid, guild_uuid).await?;
if let Ok(cache_hit) = data.get_cache_key(format!("{guild_uuid}_channels")).await { if let Ok(cache_hit) = app_state
return Ok(HttpResponse::Ok() .cache_pool
.content_type("application/json") .get_cache_key::<Vec<Channel>>(format!("{guild_uuid}_channels"))
.body(cache_hit)); .await
{
return Ok((StatusCode::OK, Json(cache_hit)).into_response());
} }
let channels = Channel::fetch_all(&data.pool, guild_uuid).await?; let channels = Channel::fetch_all(&mut conn, guild_uuid).await?;
let channels_ordered = order_by_is_above(channels).await?; let channels_ordered = order_by_is_above(channels).await?;
data.set_cache_key( app_state
.cache_pool
.set_cache_key(
format!("{guild_uuid}_channels"), format!("{guild_uuid}_channels"),
channels_ordered.clone(), channels_ordered.clone(),
1800, 1800,
) )
.await?; .await?;
Ok(HttpResponse::Ok().json(channels_ordered)) Ok((StatusCode::OK, Json(channels_ordered)).into_response())
} }
#[post("{uuid}/channels")]
pub async fn create( pub async fn create(
req: HttpRequest, State(app_state): State<Arc<AppState>>,
channel_info: web::Json<ChannelInfo>, Path(guild_uuid): Path<Uuid>,
path: web::Path<(Uuid,)>, Extension(CurrentUser(uuid)): Extension<CurrentUser<Uuid>>,
data: web::Data<Data>, Json(channel_info): Json<ChannelInfo>,
) -> Result<HttpResponse, Error> { ) -> Result<impl IntoResponse, Error> {
let headers = req.headers(); let mut conn = app_state.pool.get().await?;
let auth_header = get_auth_header(headers)?; global_checks(&mut conn, &app_state.config, uuid).await?;
let guild_uuid = path.into_inner().0;
let mut conn = data.pool.get().await?;
let uuid = check_access_token(auth_header, &mut conn).await?;
global_checks(&data, uuid).await?;
let member = Member::check_membership(&mut conn, uuid, guild_uuid).await?; let member = Member::check_membership(&mut conn, uuid, guild_uuid).await?;
member member
.check_permission(&data, Permissions::ManageChannel) .check_permission(&mut conn, &app_state.cache_pool, Permissions::ManageChannel)
.await?; .await?;
let channel = Channel::new( let channel = Channel::new(
data.clone(), &mut conn,
&app_state.cache_pool,
guild_uuid, guild_uuid,
channel_info.name.clone(), channel_info.name.clone(),
channel_info.description.clone(), channel_info.description.clone(),
) )
.await?; .await?;
Ok(HttpResponse::Ok().json(channel)) Ok((StatusCode::OK, Json(channel)))
} }

View file

@ -1,62 +0,0 @@
//! `/api/v1/guilds/{uuid}/icon` icon related endpoints, will probably be replaced by a multipart post to above endpoint
use actix_web::{HttpRequest, HttpResponse, put, web};
use futures_util::StreamExt as _;
use uuid::Uuid;
use crate::{
Data,
api::v1::auth::check_access_token,
error::Error,
objects::{Guild, Member, Permissions},
utils::{get_auth_header, global_checks},
};
/// `PUT /api/v1/guilds/{uuid}/icon` Icon upload
///
/// requires auth: no
///
/// put request expects a file and nothing else
#[put("{uuid}/icon")]
pub async fn upload(
req: HttpRequest,
path: web::Path<(Uuid,)>,
mut payload: web::Payload,
data: web::Data<Data>,
) -> Result<HttpResponse, Error> {
let headers = req.headers();
let auth_header = get_auth_header(headers)?;
let guild_uuid = path.into_inner().0;
let mut conn = data.pool.get().await?;
let uuid = check_access_token(auth_header, &mut conn).await?;
global_checks(&data, uuid).await?;
let member = Member::check_membership(&mut conn, uuid, guild_uuid).await?;
member
.check_permission(&data, Permissions::ManageGuild)
.await?;
let mut guild = Guild::fetch_one(&mut conn, guild_uuid).await?;
let mut bytes = web::BytesMut::new();
while let Some(item) = payload.next().await {
bytes.extend_from_slice(&item?);
}
guild
.set_icon(
&data.bunny_storage,
&mut conn,
data.config.bunny.cdn_url.clone(),
bytes,
)
.await?;
Ok(HttpResponse::Ok().finish())
}

View file

@ -1,37 +1,35 @@
use actix_web::{HttpRequest, HttpResponse, get, post, web}; use std::sync::Arc;
use axum::{
Extension, Json,
extract::{Path, State},
http::StatusCode,
response::IntoResponse,
};
use serde::Deserialize; use serde::Deserialize;
use uuid::Uuid; use uuid::Uuid;
use crate::{ use crate::{
Data, AppState,
api::v1::auth::check_access_token, api::v1::auth::CurrentUser,
error::Error, error::Error,
objects::{Guild, Member, Permissions}, objects::{Guild, Member, Permissions},
utils::{get_auth_header, global_checks}, utils::global_checks,
}; };
#[derive(Deserialize)] #[derive(Deserialize)]
struct InviteRequest { pub struct InviteRequest {
custom_id: Option<String>, custom_id: Option<String>,
} }
#[get("{uuid}/invites")]
pub async fn get( pub async fn get(
req: HttpRequest, State(app_state): State<Arc<AppState>>,
path: web::Path<(Uuid,)>, Path(guild_uuid): Path<Uuid>,
data: web::Data<Data>, Extension(CurrentUser(uuid)): Extension<CurrentUser<Uuid>>,
) -> Result<HttpResponse, Error> { ) -> Result<impl IntoResponse, Error> {
let headers = req.headers(); let mut conn = app_state.pool.get().await?;
let auth_header = get_auth_header(headers)?; global_checks(&mut conn, &app_state.config, uuid).await?;
let guild_uuid = path.into_inner().0;
let mut conn = data.pool.get().await?;
let uuid = check_access_token(auth_header, &mut conn).await?;
global_checks(&data, uuid).await?;
Member::check_membership(&mut conn, uuid, guild_uuid).await?; Member::check_membership(&mut conn, uuid, guild_uuid).await?;
@ -39,32 +37,23 @@ pub async fn get(
let invites = guild.get_invites(&mut conn).await?; let invites = guild.get_invites(&mut conn).await?;
Ok(HttpResponse::Ok().json(invites)) Ok((StatusCode::OK, Json(invites)))
} }
#[post("{uuid}/invites")]
pub async fn create( pub async fn create(
req: HttpRequest, State(app_state): State<Arc<AppState>>,
path: web::Path<(Uuid,)>, Path(guild_uuid): Path<Uuid>,
invite_request: web::Json<InviteRequest>, Extension(CurrentUser(uuid)): Extension<CurrentUser<Uuid>>,
data: web::Data<Data>, Json(invite_request): Json<InviteRequest>,
) -> Result<HttpResponse, Error> { ) -> Result<impl IntoResponse, Error> {
let headers = req.headers(); let mut conn = app_state.pool.get().await?;
let auth_header = get_auth_header(headers)?; global_checks(&mut conn, &app_state.config, uuid).await?;
let guild_uuid = path.into_inner().0;
let mut conn = data.pool.get().await?;
let uuid = check_access_token(auth_header, &mut conn).await?;
global_checks(&data, uuid).await?;
let member = Member::check_membership(&mut conn, uuid, guild_uuid).await?; let member = Member::check_membership(&mut conn, uuid, guild_uuid).await?;
member member
.check_permission(&data, Permissions::CreateInvite) .check_permission(&mut conn, &app_state.cache_pool, Permissions::CreateInvite)
.await?; .await?;
let guild = Guild::fetch_one(&mut conn, guild_uuid).await?; let guild = Guild::fetch_one(&mut conn, guild_uuid).await?;
@ -73,5 +62,5 @@ pub async fn create(
.create_invite(&mut conn, uuid, invite_request.custom_id.clone()) .create_invite(&mut conn, uuid, invite_request.custom_id.clone())
.await?; .await?;
Ok(HttpResponse::Ok().json(invite)) Ok((StatusCode::OK, Json(invite)))
} }

View file

@ -1,36 +1,43 @@
use crate::{ use std::sync::Arc;
Data,
api::v1::auth::check_access_token,
error::Error,
objects::{Me, Member},
utils::{get_auth_header, global_checks},
};
use ::uuid::Uuid; use ::uuid::Uuid;
use actix_web::{HttpRequest, HttpResponse, get, web}; use axum::{
Extension, Json,
extract::{Path, Query, State},
http::StatusCode,
response::IntoResponse,
};
use crate::{
AppState,
api::v1::auth::CurrentUser,
error::Error,
objects::{Me, Member, PaginationRequest},
utils::global_checks,
};
#[get("{uuid}/members")]
pub async fn get( pub async fn get(
req: HttpRequest, State(app_state): State<Arc<AppState>>,
path: web::Path<(Uuid,)>, Path(guild_uuid): Path<Uuid>,
data: web::Data<Data>, Query(pagination): Query<PaginationRequest>,
) -> Result<HttpResponse, Error> { Extension(CurrentUser(uuid)): Extension<CurrentUser<Uuid>>,
let headers = req.headers(); ) -> Result<impl IntoResponse, Error> {
let mut conn = app_state.pool.get().await?;
let auth_header = get_auth_header(headers)?; global_checks(&mut conn, &app_state.config, uuid).await?;
let guild_uuid = path.into_inner().0;
let mut conn = data.pool.get().await?;
let uuid = check_access_token(auth_header, &mut conn).await?;
global_checks(&data, uuid).await?;
Member::check_membership(&mut conn, uuid, guild_uuid).await?; Member::check_membership(&mut conn, uuid, guild_uuid).await?;
let me = Me::get(&mut conn, uuid).await?; let me = Me::get(&mut conn, uuid).await?;
let members = Member::fetch_all(&data, &me, guild_uuid).await?; let members = Member::fetch_page(
&mut conn,
&app_state.cache_pool,
&me,
guild_uuid,
pagination,
)
.await?;
Ok(HttpResponse::Ok().json(members)) Ok((StatusCode::OK, Json(members)))
} }

View file

@ -1,40 +1,51 @@
//! `/api/v1/guilds/{uuid}` Specific server endpoints //! `/api/v1/guilds/{uuid}` Specific server endpoints
use actix_web::{HttpRequest, HttpResponse, Scope, get, web}; use std::sync::Arc;
use axum::{
Extension, Json, Router,
extract::{Multipart, Path, State},
http::StatusCode,
response::IntoResponse,
routing::{delete, get, patch, post},
};
use bytes::Bytes;
use uuid::Uuid; use uuid::Uuid;
mod bans;
mod channels; mod channels;
mod icon;
mod invites; mod invites;
mod members; mod members;
mod roles; mod roles;
use crate::{ use crate::{
Data, AppState,
api::v1::auth::check_access_token, api::v1::auth::CurrentUser,
error::Error, error::Error,
objects::{Guild, Member}, objects::{Guild, Member, Permissions},
utils::{get_auth_header, global_checks}, utils::global_checks,
}; };
pub fn web() -> Scope { pub fn router() -> Router<Arc<AppState>> {
web::scope("") Router::new()
// Servers // Servers
.service(get) .route("/", get(get_guild))
.route("/", patch(edit))
// Channels // Channels
.service(channels::get) .route("/channels", get(channels::get))
.service(channels::create) .route("/channels", post(channels::create))
// Roles // Roles
.service(roles::get) .route("/roles", get(roles::get))
.service(roles::create) .route("/roles", post(roles::create))
.service(roles::uuid::get) .route("/roles/{role_uuid}", get(roles::uuid::get))
// Invites // Invites
.service(invites::get) .route("/invites", get(invites::get))
.service(invites::create) .route("/invites", post(invites::create))
// Icon
.service(icon::upload)
// Members // Members
.service(members::get) .route("/members", get(members::get))
// Bans
.route("/bans", get(bans::get))
.route("/bans/{uuid}", delete(bans::unban))
} }
/// `GET /api/v1/guilds/{uuid}` DESCRIPTION /// `GET /api/v1/guilds/{uuid}` DESCRIPTION
@ -70,27 +81,58 @@ pub fn web() -> Scope {
/// "member_count": 20 /// "member_count": 20
/// }); /// });
/// ``` /// ```
#[get("/{uuid}")] pub async fn get_guild(
pub async fn get( State(app_state): State<Arc<AppState>>,
req: HttpRequest, Path(guild_uuid): Path<Uuid>,
path: web::Path<(Uuid,)>, Extension(CurrentUser(uuid)): Extension<CurrentUser<Uuid>>,
data: web::Data<Data>, ) -> Result<impl IntoResponse, Error> {
) -> Result<HttpResponse, Error> { let mut conn = app_state.pool.get().await?;
let headers = req.headers();
let auth_header = get_auth_header(headers)?; global_checks(&mut conn, &app_state.config, uuid).await?;
let guild_uuid = path.into_inner().0;
let mut conn = data.pool.get().await?;
let uuid = check_access_token(auth_header, &mut conn).await?;
global_checks(&data, uuid).await?;
Member::check_membership(&mut conn, uuid, guild_uuid).await?; Member::check_membership(&mut conn, uuid, guild_uuid).await?;
let guild = Guild::fetch_one(&mut conn, guild_uuid).await?; let guild = Guild::fetch_one(&mut conn, guild_uuid).await?;
Ok(HttpResponse::Ok().json(guild)) Ok((StatusCode::OK, Json(guild)))
}
/// `PATCH /api/v1/guilds/{uuid}` change guild settings
///
/// requires auth: yes
pub async fn edit(
State(app_state): State<Arc<AppState>>,
Path(guild_uuid): Path<Uuid>,
Extension(CurrentUser(uuid)): Extension<CurrentUser<Uuid>>,
mut multipart: Multipart,
) -> Result<impl IntoResponse, Error> {
let mut conn = app_state.pool.get().await?;
global_checks(&mut conn, &app_state.config, uuid).await?;
let member = Member::check_membership(&mut conn, uuid, guild_uuid).await?;
member
.check_permission(&mut conn, &app_state.cache_pool, Permissions::ManageGuild)
.await?;
let mut guild = Guild::fetch_one(&mut conn, guild_uuid).await?;
let mut icon: Option<Bytes> = None;
while let Some(field) = multipart.next_field().await.unwrap() {
let name = field
.name()
.ok_or(Error::BadRequest("Field has no name".to_string()))?;
if name == "icon" {
icon = Some(field.bytes().await?);
}
}
if let Some(icon) = icon {
guild.set_icon(&mut conn, &app_state, icon).await?;
}
Ok(StatusCode::OK)
} }

View file

@ -1,82 +1,77 @@
use std::sync::Arc;
use ::uuid::Uuid; use ::uuid::Uuid;
use actix_web::{HttpRequest, HttpResponse, get, post, web}; use axum::{
Extension, Json,
extract::{Path, State},
http::StatusCode,
response::IntoResponse,
};
use serde::Deserialize; use serde::Deserialize;
use crate::{ use crate::{
Data, AppState,
api::v1::auth::check_access_token, api::v1::auth::CurrentUser,
error::Error, error::Error,
objects::{Member, Permissions, Role}, objects::{Member, Permissions, Role},
utils::{get_auth_header, global_checks, order_by_is_above}, utils::{CacheFns, global_checks, order_by_is_above},
}; };
pub mod uuid; pub mod uuid;
#[derive(Deserialize)] #[derive(Deserialize)]
struct RoleInfo { pub struct RoleInfo {
name: String, name: String,
} }
#[get("{uuid}/roles")]
pub async fn get( pub async fn get(
req: HttpRequest, State(app_state): State<Arc<AppState>>,
path: web::Path<(Uuid,)>, Path(guild_uuid): Path<Uuid>,
data: web::Data<Data>, Extension(CurrentUser(uuid)): Extension<CurrentUser<Uuid>>,
) -> Result<HttpResponse, Error> { ) -> Result<impl IntoResponse, Error> {
let headers = req.headers(); let mut conn = app_state.pool.get().await?;
let auth_header = get_auth_header(headers)?; global_checks(&mut conn, &app_state.config, uuid).await?;
let guild_uuid = path.into_inner().0;
let mut conn = data.pool.get().await?;
let uuid = check_access_token(auth_header, &mut conn).await?;
Member::check_membership(&mut conn, uuid, guild_uuid).await?; Member::check_membership(&mut conn, uuid, guild_uuid).await?;
if let Ok(cache_hit) = data.get_cache_key(format!("{guild_uuid}_roles")).await { if let Ok(cache_hit) = app_state
return Ok(HttpResponse::Ok() .cache_pool
.content_type("application/json") .get_cache_key::<Vec<Role>>(format!("{guild_uuid}_roles"))
.body(cache_hit)); .await
{
return Ok((StatusCode::OK, Json(cache_hit)).into_response());
} }
let roles = Role::fetch_all(&mut conn, guild_uuid).await?; let roles = Role::fetch_all(&mut conn, guild_uuid).await?;
let roles_ordered = order_by_is_above(roles).await?; let roles_ordered = order_by_is_above(roles).await?;
data.set_cache_key(format!("{guild_uuid}_roles"), roles_ordered.clone(), 1800) app_state
.cache_pool
.set_cache_key(format!("{guild_uuid}_roles"), roles_ordered.clone(), 1800)
.await?; .await?;
Ok(HttpResponse::Ok().json(roles_ordered)) Ok((StatusCode::OK, Json(roles_ordered)).into_response())
} }
#[post("{uuid}/roles")]
pub async fn create( pub async fn create(
req: HttpRequest, State(app_state): State<Arc<AppState>>,
role_info: web::Json<RoleInfo>, Path(guild_uuid): Path<Uuid>,
path: web::Path<(Uuid,)>, Extension(CurrentUser(uuid)): Extension<CurrentUser<Uuid>>,
data: web::Data<Data>, Json(role_info): Json<RoleInfo>,
) -> Result<HttpResponse, Error> { ) -> Result<impl IntoResponse, Error> {
let headers = req.headers(); let mut conn = app_state.pool.get().await?;
let auth_header = get_auth_header(headers)?; global_checks(&mut conn, &app_state.config, uuid).await?;
let guild_uuid = path.into_inner().0;
let mut conn = data.pool.get().await?;
let uuid = check_access_token(auth_header, &mut conn).await?;
global_checks(&data, uuid).await?;
let member = Member::check_membership(&mut conn, uuid, guild_uuid).await?; let member = Member::check_membership(&mut conn, uuid, guild_uuid).await?;
member member
.check_permission(&data, Permissions::ManageRole) .check_permission(&mut conn, &app_state.cache_pool, Permissions::ManageRole)
.await?; .await?;
let role = Role::new(&mut conn, guild_uuid, role_info.name.clone()).await?; let role = Role::new(&mut conn, guild_uuid, role_info.name.clone()).await?;
Ok(HttpResponse::Ok().json(role)) Ok((StatusCode::OK, Json(role)).into_response())
} }

View file

@ -1,43 +1,46 @@
use std::sync::Arc;
use ::uuid::Uuid;
use axum::{
Extension, Json,
extract::{Path, State},
http::StatusCode,
response::IntoResponse,
};
use crate::{ use crate::{
Data, AppState,
api::v1::auth::check_access_token, api::v1::auth::CurrentUser,
error::Error, error::Error,
objects::{Member, Role}, objects::{Member, Role},
utils::{get_auth_header, global_checks}, utils::{CacheFns, global_checks},
}; };
use ::uuid::Uuid;
use actix_web::{HttpRequest, HttpResponse, get, web};
#[get("{uuid}/roles/{role_uuid}")]
pub async fn get( pub async fn get(
req: HttpRequest, State(app_state): State<Arc<AppState>>,
path: web::Path<(Uuid, Uuid)>, Path((guild_uuid, role_uuid)): Path<(Uuid, Uuid)>,
data: web::Data<Data>, Extension(CurrentUser(uuid)): Extension<CurrentUser<Uuid>>,
) -> Result<HttpResponse, Error> { ) -> Result<impl IntoResponse, Error> {
let headers = req.headers(); let mut conn = app_state.pool.get().await?;
let auth_header = get_auth_header(headers)?; global_checks(&mut conn, &app_state.config, uuid).await?;
let (guild_uuid, role_uuid) = path.into_inner();
let mut conn = data.pool.get().await?;
let uuid = check_access_token(auth_header, &mut conn).await?;
global_checks(&data, uuid).await?;
Member::check_membership(&mut conn, uuid, guild_uuid).await?; Member::check_membership(&mut conn, uuid, guild_uuid).await?;
if let Ok(cache_hit) = data.get_cache_key(format!("{role_uuid}")).await { if let Ok(cache_hit) = app_state
return Ok(HttpResponse::Ok() .cache_pool
.content_type("application/json") .get_cache_key::<Role>(format!("{role_uuid}"))
.body(cache_hit)); .await
{
return Ok((StatusCode::OK, Json(cache_hit)).into_response());
} }
let role = Role::fetch_one(&mut conn, role_uuid).await?; let role = Role::fetch_one(&mut conn, role_uuid).await?;
data.set_cache_key(format!("{role_uuid}"), role.clone(), 60) app_state
.cache_pool
.set_cache_key(format!("{role_uuid}"), role.clone(), 60)
.await?; .await?;
Ok(HttpResponse::Ok().json(role)) Ok((StatusCode::OK, Json(role)).into_response())
} }

View file

@ -1,49 +1,48 @@
use actix_web::{HttpRequest, HttpResponse, get, post, web}; use std::sync::Arc;
use axum::{
Extension, Json,
extract::{Path, State},
http::StatusCode,
response::IntoResponse,
};
use uuid::Uuid;
use crate::{ use crate::{
Data, AppState,
api::v1::auth::check_access_token, api::v1::auth::CurrentUser,
error::Error, error::Error,
objects::{Guild, Invite, Member}, objects::{Guild, Invite, Member},
utils::{get_auth_header, global_checks}, utils::global_checks,
}; };
#[get("{id}")] pub async fn get(
pub async fn get(path: web::Path<(String,)>, data: web::Data<Data>) -> Result<HttpResponse, Error> { State(app_state): State<Arc<AppState>>,
let mut conn = data.pool.get().await?; Path(invite_id): Path<String>,
) -> Result<impl IntoResponse, Error> {
let invite_id = path.into_inner().0; let mut conn = app_state.pool.get().await?;
let invite = Invite::fetch_one(&mut conn, invite_id).await?; let invite = Invite::fetch_one(&mut conn, invite_id).await?;
let guild = Guild::fetch_one(&mut conn, invite.guild_uuid).await?; let guild = Guild::fetch_one(&mut conn, invite.guild_uuid).await?;
Ok(HttpResponse::Ok().json(guild)) Ok((StatusCode::OK, Json(guild)))
} }
#[post("{id}")]
pub async fn join( pub async fn join(
req: HttpRequest, State(app_state): State<Arc<AppState>>,
path: web::Path<(String,)>, Path(invite_id): Path<String>,
data: web::Data<Data>, Extension(CurrentUser(uuid)): Extension<CurrentUser<Uuid>>,
) -> Result<HttpResponse, Error> { ) -> Result<impl IntoResponse, Error> {
let headers = req.headers(); let mut conn = app_state.pool.get().await?;
let auth_header = get_auth_header(headers)?; global_checks(&mut conn, &app_state.config, uuid).await?;
let invite_id = path.into_inner().0;
let mut conn = data.pool.get().await?;
let uuid = check_access_token(auth_header, &mut conn).await?;
global_checks(&data, uuid).await?;
let invite = Invite::fetch_one(&mut conn, invite_id).await?; let invite = Invite::fetch_one(&mut conn, invite_id).await?;
let guild = Guild::fetch_one(&mut conn, invite.guild_uuid).await?; let guild = Guild::fetch_one(&mut conn, invite.guild_uuid).await?;
Member::new(&data, uuid, guild.uuid).await?; Member::new(&mut conn, &app_state.cache_pool, uuid, guild.uuid).await?;
Ok(HttpResponse::Ok().json(guild)) Ok((StatusCode::OK, Json(guild)))
} }

View file

@ -1,7 +1,16 @@
use actix_web::{Scope, web}; use std::sync::Arc;
use axum::{
Router,
routing::{get, post},
};
use crate::AppState;
mod id; mod id;
pub fn web() -> Scope { pub fn router() -> Router<Arc<AppState>> {
web::scope("/invites").service(id::get).service(id::join) Router::new()
.route("/{id}", get(id::get))
.route("/{id}", post(id::join))
} }

View file

@ -1,40 +1,38 @@
use std::sync::Arc;
use ::uuid::Uuid; use ::uuid::Uuid;
use actix_web::{HttpRequest, HttpResponse, get, post, web}; use axum::{Extension, Json, extract::State, http::StatusCode, response::IntoResponse};
use serde::Deserialize; use serde::Deserialize;
pub mod uuid; pub mod uuid;
use crate::{ use crate::{
Data, AppState,
api::v1::auth::check_access_token, api::v1::auth::CurrentUser,
error::Error, error::Error,
objects::Me, objects::Me,
utils::{get_auth_header, global_checks}, utils::{global_checks, user_uuid_from_username},
}; };
/// Returns a list of users that are your friends /// Returns a list of users that are your friends
#[get("/friends")] pub async fn get(
pub async fn get(req: HttpRequest, data: web::Data<Data>) -> Result<HttpResponse, Error> { State(app_state): State<Arc<AppState>>,
let headers = req.headers(); Extension(CurrentUser(uuid)): Extension<CurrentUser<Uuid>>,
) -> Result<impl IntoResponse, Error> {
let mut conn = app_state.pool.get().await?;
let auth_header = get_auth_header(headers)?; global_checks(&mut conn, &app_state.config, uuid).await?;
let mut conn = data.pool.get().await?;
let uuid = check_access_token(auth_header, &mut conn).await?;
global_checks(&data, uuid).await?;
let me = Me::get(&mut conn, uuid).await?; let me = Me::get(&mut conn, uuid).await?;
let friends = me.get_friends(&data).await?; let friends = me.get_friends(&mut conn, &app_state.cache_pool).await?;
Ok(HttpResponse::Ok().json(friends)) Ok((StatusCode::OK, Json(friends)))
} }
#[derive(Deserialize)] #[derive(Deserialize)]
struct UserReq { pub struct UserReq {
uuid: Uuid, username: String,
} }
/// `POST /api/v1/me/friends` Send friend request /// `POST /api/v1/me/friends` Send friend request
@ -56,25 +54,19 @@ struct UserReq {
/// ///
/// 400 Bad Request (usually means users are already friends) /// 400 Bad Request (usually means users are already friends)
/// ///
#[post("/friends")]
pub async fn post( pub async fn post(
req: HttpRequest, State(app_state): State<Arc<AppState>>,
json: web::Json<UserReq>, Extension(CurrentUser(uuid)): Extension<CurrentUser<Uuid>>,
data: web::Data<Data>, Json(user_request): Json<UserReq>,
) -> Result<HttpResponse, Error> { ) -> Result<impl IntoResponse, Error> {
let headers = req.headers(); let mut conn = app_state.pool.get().await?;
let auth_header = get_auth_header(headers)?; global_checks(&mut conn, &app_state.config, uuid).await?;
let mut conn = data.pool.get().await?;
let uuid = check_access_token(auth_header, &mut conn).await?;
global_checks(&data, uuid).await?;
let me = Me::get(&mut conn, uuid).await?; let me = Me::get(&mut conn, uuid).await?;
me.add_friend(&mut conn, json.uuid).await?; let target_uuid = user_uuid_from_username(&mut conn, &user_request.username).await?;
me.add_friend(&mut conn, target_uuid).await?;
Ok(HttpResponse::Ok().finish()) Ok(StatusCode::OK)
} }

View file

@ -1,33 +1,29 @@
use actix_web::{HttpRequest, HttpResponse, delete, web}; use std::sync::Arc;
use axum::{
Extension,
extract::{Path, State},
http::StatusCode,
response::IntoResponse,
};
use uuid::Uuid; use uuid::Uuid;
use crate::{ use crate::{
Data, AppState, api::v1::auth::CurrentUser, error::Error, objects::Me, utils::global_checks,
api::v1::auth::check_access_token,
error::Error,
objects::Me,
utils::{get_auth_header, global_checks},
}; };
#[delete("/friends/{uuid}")]
pub async fn delete( pub async fn delete(
req: HttpRequest, State(app_state): State<Arc<AppState>>,
path: web::Path<(Uuid,)>, Path(friend_uuid): Path<Uuid>,
data: web::Data<Data>, Extension(CurrentUser(uuid)): Extension<CurrentUser<Uuid>>,
) -> Result<HttpResponse, Error> { ) -> Result<impl IntoResponse, Error> {
let headers = req.headers(); let mut conn = app_state.pool.get().await?;
let auth_header = get_auth_header(headers)?; global_checks(&mut conn, &app_state.config, uuid).await?;
let mut conn = data.pool.get().await?;
let uuid = check_access_token(auth_header, &mut conn).await?;
global_checks(&data, uuid).await?;
let me = Me::get(&mut conn, uuid).await?; let me = Me::get(&mut conn, uuid).await?;
me.remove_friend(&mut conn, path.0).await?; me.remove_friend(&mut conn, friend_uuid).await?;
Ok(HttpResponse::Ok().finish()) Ok(StatusCode::OK)
} }

View file

@ -1,13 +1,12 @@
//! `/api/v1/me/guilds` Contains endpoint related to guild memberships //! `/api/v1/me/guilds` Contains endpoint related to guild memberships
use actix_web::{HttpRequest, HttpResponse, get, web}; use std::sync::Arc;
use axum::{Extension, Json, extract::State, http::StatusCode, response::IntoResponse};
use uuid::Uuid;
use crate::{ use crate::{
Data, AppState, api::v1::auth::CurrentUser, error::Error, objects::Me, utils::global_checks,
api::v1::auth::check_access_token,
error::Error,
objects::Me,
utils::{get_auth_header, global_checks},
}; };
/// `GET /api/v1/me/guilds` Returns all guild memberships in a list /// `GET /api/v1/me/guilds` Returns all guild memberships in a list
@ -55,21 +54,17 @@ use crate::{
/// ]); /// ]);
/// ``` /// ```
/// NOTE: UUIDs in this response are made using `uuidgen`, UUIDs made by the actual backend will be UUIDv7 and have extractable timestamps /// NOTE: UUIDs in this response are made using `uuidgen`, UUIDs made by the actual backend will be UUIDv7 and have extractable timestamps
#[get("/guilds")] pub async fn get(
pub async fn get(req: HttpRequest, data: web::Data<Data>) -> Result<HttpResponse, Error> { State(app_state): State<Arc<AppState>>,
let headers = req.headers(); Extension(CurrentUser(uuid)): Extension<CurrentUser<Uuid>>,
) -> Result<impl IntoResponse, Error> {
let mut conn = app_state.pool.get().await?;
let auth_header = get_auth_header(headers)?; global_checks(&mut conn, &app_state.config, uuid).await?;
let mut conn = data.pool.get().await?;
let uuid = check_access_token(auth_header, &mut conn).await?;
global_checks(&data, uuid).await?;
let me = Me::get(&mut conn, uuid).await?; let me = Me::get(&mut conn, uuid).await?;
let memberships = me.fetch_memberships(&mut conn).await?; let memberships = me.fetch_memberships(&mut conn).await?;
Ok(HttpResponse::Ok().json(memberships)) Ok((StatusCode::OK, Json(memberships)))
} }

View file

@ -1,108 +1,120 @@
use actix_multipart::form::{MultipartForm, json::Json as MpJson, tempfile::TempFile}; use std::sync::Arc;
use actix_web::{HttpRequest, HttpResponse, Scope, get, patch, web};
use axum::{
Extension, Json, Router,
extract::{DefaultBodyLimit, Multipart, State},
http::StatusCode,
response::IntoResponse,
routing::{delete, get, patch, post},
};
use bytes::Bytes;
use serde::Deserialize; use serde::Deserialize;
use uuid::Uuid;
use crate::{ use crate::{
Data, AppState, api::v1::auth::CurrentUser, error::Error, objects::Me, utils::global_checks,
api::v1::auth::check_access_token,
error::Error,
objects::Me,
utils::{get_auth_header, global_checks},
}; };
mod friends; mod friends;
mod guilds; mod guilds;
pub fn web() -> Scope { pub fn router() -> Router<Arc<AppState>> {
web::scope("/me") Router::new()
.service(get) .route("/", get(get_me))
.service(update) .route(
.service(guilds::get) "/",
.service(friends::get) patch(update).layer(DefaultBodyLimit::max(
.service(friends::post) 100 * 1024 * 1024, /* limit is in bytes */
.service(friends::uuid::delete) )),
)
.route("/guilds", get(guilds::get))
.route("/friends", get(friends::get))
.route("/friends", post(friends::post))
.route("/friends/{uuid}", delete(friends::uuid::delete))
} }
#[get("")] pub async fn get_me(
pub async fn get(req: HttpRequest, data: web::Data<Data>) -> Result<HttpResponse, Error> { State(app_state): State<Arc<AppState>>,
let headers = req.headers(); Extension(CurrentUser(uuid)): Extension<CurrentUser<Uuid>>,
) -> Result<impl IntoResponse, Error> {
let me = Me::get(&mut app_state.pool.get().await?, uuid).await?;
let auth_header = get_auth_header(headers)?; Ok((StatusCode::OK, Json(me)))
let mut conn = data.pool.get().await?;
let uuid = check_access_token(auth_header, &mut conn).await?;
let me = Me::get(&mut conn, uuid).await?;
Ok(HttpResponse::Ok().json(me))
} }
#[derive(Debug, Deserialize, Clone)] #[derive(Default, Debug, Deserialize, Clone)]
struct NewInfo { struct NewInfo {
username: Option<String>, username: Option<String>,
display_name: Option<String>, display_name: Option<String>,
//password: Option<String>, will probably be handled through a reset password link
email: Option<String>, email: Option<String>,
pronouns: Option<String>, pronouns: Option<String>,
about: Option<String>, about: Option<String>,
online_status: Option<i16>,
} }
#[derive(Debug, MultipartForm)]
struct UploadForm {
#[multipart(limit = "100MB")]
avatar: Option<TempFile>,
json: MpJson<NewInfo>,
}
#[patch("")]
pub async fn update( pub async fn update(
req: HttpRequest, State(app_state): State<Arc<AppState>>,
MultipartForm(form): MultipartForm<UploadForm>, Extension(CurrentUser(uuid)): Extension<CurrentUser<Uuid>>,
data: web::Data<Data>, mut multipart: Multipart,
) -> Result<HttpResponse, Error> { ) -> Result<impl IntoResponse, Error> {
let headers = req.headers(); let mut json_raw: Option<NewInfo> = None;
let mut avatar: Option<Bytes> = None;
let auth_header = get_auth_header(headers)?; while let Some(field) = multipart.next_field().await.unwrap() {
let name = field
.name()
.ok_or(Error::BadRequest("Field has no name".to_string()))?;
let mut conn = data.pool.get().await?; if name == "avatar" {
avatar = Some(field.bytes().await?);
} else if name == "json" {
json_raw = Some(serde_json::from_str(&field.text().await?)?)
}
}
let uuid = check_access_token(auth_header, &mut conn).await?; let json = json_raw.unwrap_or_default();
if form.avatar.is_some() || form.json.username.is_some() || form.json.display_name.is_some() { let mut conn = app_state.pool.get().await?;
global_checks(&data, uuid).await?;
if avatar.is_some() || json.username.is_some() || json.display_name.is_some() {
global_checks(&mut conn, &app_state.config, uuid).await?;
} }
let mut me = Me::get(&mut conn, uuid).await?; let mut me = Me::get(&mut conn, uuid).await?;
if let Some(avatar) = form.avatar { if let Some(avatar) = avatar {
let bytes = tokio::fs::read(avatar.file).await?; me.set_avatar(&mut conn, &app_state, avatar).await?;
}
let byte_slice: &[u8] = &bytes; if let Some(username) = &json.username {
me.set_username(&mut conn, &app_state.cache_pool, username.clone())
me.set_avatar(&data, data.config.bunny.cdn_url.clone(), byte_slice.into())
.await?; .await?;
} }
if let Some(username) = &form.json.username { if let Some(display_name) = &json.display_name {
me.set_username(&data, username.clone()).await?; me.set_display_name(&mut conn, &app_state.cache_pool, display_name.clone())
.await?;
} }
if let Some(display_name) = &form.json.display_name { if let Some(email) = &json.email {
me.set_display_name(&data, display_name.clone()).await?; me.set_email(&mut conn, &app_state.cache_pool, email.clone())
.await?;
} }
if let Some(email) = &form.json.email { if let Some(pronouns) = &json.pronouns {
me.set_email(&data, email.clone()).await?; me.set_pronouns(&mut conn, &app_state.cache_pool, pronouns.clone())
.await?;
} }
if let Some(pronouns) = &form.json.pronouns { if let Some(about) = &json.about {
me.set_pronouns(&data, pronouns.clone()).await?; me.set_about(&mut conn, &app_state.cache_pool, about.clone())
.await?;
} }
if let Some(about) = &form.json.about { if let Some(online_status) = &json.online_status {
me.set_about(&data, about.clone()).await?; me.set_online_status(&mut conn, &app_state.cache_pool, *online_status)
.await?;
} }
Ok(HttpResponse::Ok().finish()) Ok(StatusCode::OK)
} }

17
src/api/v1/members/mod.rs Normal file
View file

@ -0,0 +1,17 @@
use std::sync::Arc;
use axum::{
Router,
routing::{delete, get, post},
};
use crate::AppState;
mod uuid;
pub fn router() -> Router<Arc<AppState>> {
Router::new()
.route("/{uuid}", get(uuid::get))
.route("/{uuid}", delete(uuid::delete))
.route("/{uuid}/ban", post(uuid::ban::post))
}

View file

@ -0,0 +1,48 @@
use std::sync::Arc;
use axum::{
Extension,
extract::{Json, Path, State},
http::StatusCode,
response::IntoResponse,
};
use serde::Deserialize;
use crate::{
AppState,
api::v1::auth::CurrentUser,
error::Error,
objects::{Member, Permissions},
utils::global_checks,
};
use uuid::Uuid;
#[derive(Deserialize)]
pub struct RequstBody {
reason: String,
}
pub async fn post(
State(app_state): State<Arc<AppState>>,
Path(member_uuid): Path<Uuid>,
Extension(CurrentUser(uuid)): Extension<CurrentUser<Uuid>>,
Json(payload): Json<RequstBody>,
) -> Result<impl IntoResponse, Error> {
let mut conn = app_state.pool.get().await?;
global_checks(&mut conn, &app_state.config, uuid).await?;
let member =
Member::fetch_one_with_uuid(&mut conn, &app_state.cache_pool, None, member_uuid).await?;
let caller = Member::check_membership(&mut conn, uuid, member.guild_uuid).await?;
caller
.check_permission(&mut conn, &app_state.cache_pool, Permissions::BanMember)
.await?;
member.ban(&mut conn, &payload.reason).await?;
Ok(StatusCode::OK)
}

View file

@ -0,0 +1,66 @@
//! `/api/v1/members/{uuid}` Member specific endpoints
pub mod ban;
use std::sync::Arc;
use crate::{
AppState,
api::v1::auth::CurrentUser,
error::Error,
objects::{Me, Member, Permissions},
utils::global_checks,
};
use axum::{
Extension, Json,
extract::{Path, State},
http::StatusCode,
response::IntoResponse,
};
use uuid::Uuid;
pub async fn get(
State(app_state): State<Arc<AppState>>,
Path(member_uuid): Path<Uuid>,
Extension(CurrentUser(uuid)): Extension<CurrentUser<Uuid>>,
) -> Result<impl IntoResponse, Error> {
let mut conn = app_state.pool.get().await?;
global_checks(&mut conn, &app_state.config, uuid).await?;
let me = Me::get(&mut conn, uuid).await?;
let member =
Member::fetch_one_with_uuid(&mut conn, &app_state.cache_pool, Some(&me), member_uuid)
.await?;
Member::check_membership(&mut conn, uuid, member.guild_uuid).await?;
Ok((StatusCode::OK, Json(member)))
}
pub async fn delete(
State(app_state): State<Arc<AppState>>,
Path(member_uuid): Path<Uuid>,
Extension(CurrentUser(uuid)): Extension<CurrentUser<Uuid>>,
) -> Result<impl IntoResponse, Error> {
let mut conn = app_state.pool.get().await?;
global_checks(&mut conn, &app_state.config, uuid).await?;
let me = Me::get(&mut conn, uuid).await?;
let member =
Member::fetch_one_with_uuid(&mut conn, &app_state.cache_pool, Some(&me), member_uuid)
.await?;
let deleter = Member::check_membership(&mut conn, uuid, member.guild_uuid).await?;
deleter
.check_permission(&mut conn, &app_state.cache_pool, Permissions::KickMember)
.await?;
member.delete(&mut conn).await?;
Ok(StatusCode::OK)
}

View file

@ -1,22 +1,35 @@
//! `/api/v1` Contains version 1 of the api //! `/api/v1` Contains version 1 of the api
use actix_web::{Scope, web}; use std::sync::Arc;
use axum::{Router, middleware::from_fn_with_state, routing::get};
use crate::{AppState, api::v1::auth::CurrentUser};
mod auth; mod auth;
mod channels; mod channels;
mod guilds; mod guilds;
mod invites; mod invites;
mod me; mod me;
mod members;
mod stats; mod stats;
mod users; mod users;
pub fn web() -> Scope { pub fn router(app_state: Arc<AppState>) -> Router<Arc<AppState>> {
web::scope("/v1") let router_with_auth = Router::new()
.service(stats::res) .nest("/users", users::router())
.service(auth::web()) .nest("/guilds", guilds::router())
.service(users::web()) .nest("/invites", invites::router())
.service(channels::web()) .nest("/members", members::router())
.service(guilds::web()) .nest("/me", me::router())
.service(invites::web()) .layer(from_fn_with_state(
.service(me::web()) app_state.clone(),
CurrentUser::check_auth_layer,
));
Router::new()
.route("/stats", get(stats::res))
.nest("/auth", auth::router(app_state.clone()))
.nest("/channels", channels::router(app_state))
.merge(router_with_auth)
} }

View file

@ -1,13 +1,17 @@
//! `/api/v1/stats` Returns stats about the server //! `/api/v1/stats` Returns stats about the server
use std::sync::Arc;
use std::time::SystemTime; use std::time::SystemTime;
use actix_web::{HttpResponse, get, web}; use axum::Json;
use axum::extract::State;
use axum::http::StatusCode;
use axum::response::IntoResponse;
use diesel::QueryDsl; use diesel::QueryDsl;
use diesel_async::RunQueryDsl; use diesel_async::RunQueryDsl;
use serde::Serialize; use serde::Serialize;
use crate::Data; use crate::AppState;
use crate::error::Error; use crate::error::Error;
use crate::schema::users::dsl::{users, uuid}; use crate::schema::users::dsl::{users, uuid};
@ -39,27 +43,26 @@ struct Response {
/// "build_number": "39d01bb" /// "build_number": "39d01bb"
/// }); /// });
/// ``` /// ```
#[get("/stats")] pub async fn res(State(app_state): State<Arc<AppState>>) -> Result<impl IntoResponse, Error> {
pub async fn res(data: web::Data<Data>) -> Result<HttpResponse, Error> {
let accounts: i64 = users let accounts: i64 = users
.select(uuid) .select(uuid)
.count() .count()
.get_result(&mut data.pool.get().await?) .get_result(&mut app_state.pool.get().await?)
.await?; .await?;
let response = Response { let response = Response {
// TODO: Get number of accounts from db // TODO: Get number of accounts from db
accounts, accounts,
uptime: SystemTime::now() uptime: SystemTime::now()
.duration_since(data.start_time) .duration_since(app_state.start_time)
.expect("Seriously why dont you have time??") .expect("Seriously why dont you have time??")
.as_secs(), .as_secs(),
version: String::from(VERSION.unwrap_or("UNKNOWN")), version: String::from(VERSION.unwrap_or("UNKNOWN")),
registration_enabled: data.config.instance.registration, registration_enabled: app_state.config.instance.registration,
email_verification_required: data.config.instance.require_email_verification, email_verification_required: app_state.config.instance.require_email_verification,
// TODO: Get build number from git hash or remove this from the spec // TODO: Get build number from git hash or remove this from the spec
build_number: String::from(GIT_SHORT_HASH), build_number: String::from(GIT_SHORT_HASH),
}; };
Ok(HttpResponse::Ok().json(response)) Ok((StatusCode::OK, Json(response)))
} }

View file

@ -1,19 +1,30 @@
//! `/api/v1/users` Contains endpoints related to all users //! `/api/v1/users` Contains endpoints related to all users
use actix_web::{HttpRequest, HttpResponse, Scope, get, web}; use std::sync::Arc;
use ::uuid::Uuid;
use axum::{
Extension, Json, Router,
extract::{Query, State},
http::StatusCode,
response::IntoResponse,
routing::get,
};
use crate::{ use crate::{
Data, AppState,
api::v1::auth::check_access_token, api::v1::auth::CurrentUser,
error::Error, error::Error,
objects::{StartAmountQuery, User}, objects::{StartAmountQuery, User},
utils::{get_auth_header, global_checks}, utils::global_checks,
}; };
mod uuid; mod uuid;
pub fn web() -> Scope { pub fn router() -> Router<Arc<AppState>> {
web::scope("/users").service(get).service(uuid::get) Router::new()
.route("/", get(users))
.route("/{uuid}", get(uuid::get))
} }
/// `GET /api/v1/users` Returns all users on this instance /// `GET /api/v1/users` Returns all users on this instance
@ -46,31 +57,24 @@ pub fn web() -> Scope {
/// ]); /// ]);
/// ``` /// ```
/// NOTE: UUIDs in this response are made using `uuidgen`, UUIDs made by the actual backend will be UUIDv7 and have extractable timestamps /// NOTE: UUIDs in this response are made using `uuidgen`, UUIDs made by the actual backend will be UUIDv7 and have extractable timestamps
#[get("")] pub async fn users(
pub async fn get( State(app_state): State<Arc<AppState>>,
req: HttpRequest, Query(request_query): Query<StartAmountQuery>,
request_query: web::Query<StartAmountQuery>, Extension(CurrentUser(uuid)): Extension<CurrentUser<Uuid>>,
data: web::Data<Data>, ) -> Result<impl IntoResponse, Error> {
) -> Result<HttpResponse, Error> {
let headers = req.headers();
let auth_header = get_auth_header(headers)?;
let start = request_query.start.unwrap_or(0); let start = request_query.start.unwrap_or(0);
let amount = request_query.amount.unwrap_or(10); let amount = request_query.amount.unwrap_or(10);
if amount > 100 { if amount > 100 {
return Ok(HttpResponse::BadRequest().finish()); return Ok(StatusCode::BAD_REQUEST.into_response());
} }
let mut conn = data.pool.get().await?; let mut conn = app_state.pool.get().await?;
let uuid = check_access_token(auth_header, &mut conn).await?; global_checks(&mut conn, &app_state.config, uuid).await?;
global_checks(&data, uuid).await?;
let users = User::fetch_amount(&mut conn, start, amount).await?; let users = User::fetch_amount(&mut conn, start, amount).await?;
Ok(HttpResponse::Ok().json(users)) Ok((StatusCode::OK, Json(users)).into_response())
} }

View file

@ -1,14 +1,21 @@
//! `/api/v1/users/{uuid}` Specific user endpoints //! `/api/v1/users/{uuid}` Specific user endpoints
use actix_web::{HttpRequest, HttpResponse, get, web}; use std::sync::Arc;
use axum::{
Extension, Json,
extract::{Path, State},
http::StatusCode,
response::IntoResponse,
};
use uuid::Uuid; use uuid::Uuid;
use crate::{ use crate::{
Data, AppState,
api::v1::auth::check_access_token, api::v1::auth::CurrentUser,
error::Error, error::Error,
objects::{Me, User}, objects::{Me, User},
utils::{get_auth_header, global_checks}, utils::global_checks,
}; };
/// `GET /api/v1/users/{uuid}` Returns user with the given UUID /// `GET /api/v1/users/{uuid}` Returns user with the given UUID
@ -27,27 +34,19 @@ use crate::{
/// }); /// });
/// ``` /// ```
/// NOTE: UUIDs in this response are made using `uuidgen`, UUIDs made by the actual backend will be UUIDv7 and have extractable timestamps /// NOTE: UUIDs in this response are made using `uuidgen`, UUIDs made by the actual backend will be UUIDv7 and have extractable timestamps
#[get("/{uuid}")]
pub async fn get( pub async fn get(
req: HttpRequest, State(app_state): State<Arc<AppState>>,
path: web::Path<(Uuid,)>, Path(user_uuid): Path<Uuid>,
data: web::Data<Data>, Extension(CurrentUser(uuid)): Extension<CurrentUser<Uuid>>,
) -> Result<HttpResponse, Error> { ) -> Result<impl IntoResponse, Error> {
let headers = req.headers(); let mut conn = app_state.pool.get().await?;
let user_uuid = path.into_inner().0; global_checks(&mut conn, &app_state.config, uuid).await?;
let auth_header = get_auth_header(headers)?;
let mut conn = data.pool.get().await?;
let uuid = check_access_token(auth_header, &mut conn).await?;
global_checks(&data, uuid).await?;
let me = Me::get(&mut conn, uuid).await?; let me = Me::get(&mut conn, uuid).await?;
let user = User::fetch_one_with_friendship(&data, &me, user_uuid).await?; let user =
User::fetch_one_with_friendship(&mut conn, &app_state.cache_pool, &me, user_uuid).await?;
Ok(HttpResponse::Ok().json(user)) Ok((StatusCode::OK, Json(user)))
} }

View file

@ -1,5 +1,5 @@
//! `/api/v1/versions` Returns info about api versions //! `/api/v1/versions` Returns info about api versions
use actix_web::{HttpResponse, Responder, get}; use axum::{Json, http::StatusCode, response::IntoResponse};
use serde::Serialize; use serde::Serialize;
#[derive(Serialize)] #[derive(Serialize)]
@ -24,13 +24,12 @@ struct UnstableFeatures;
/// ] /// ]
/// }); /// });
/// ``` /// ```
#[get("/versions")] pub async fn versions() -> impl IntoResponse {
pub async fn get() -> impl Responder {
let response = Response { let response = Response {
unstable_features: UnstableFeatures, unstable_features: UnstableFeatures,
// TODO: Find a way to dynamically update this possibly? // TODO: Find a way to dynamically update this possibly?
versions: vec![String::from("1")], versions: vec![String::from("1")],
}; };
HttpResponse::Ok().json(response) (StatusCode::OK, Json(response))
} }

View file

@ -1,12 +1,16 @@
use std::{io, time::SystemTimeError}; use std::{io, time::SystemTimeError};
use actix_web::{ use axum::{
HttpResponse, Json,
error::{PayloadError, ResponseError}, extract::{
multipart::MultipartError,
rejection::{JsonRejection, QueryRejection},
},
http::{ http::{
StatusCode, StatusCode,
header::{ContentType, ToStrError}, header::{InvalidHeaderValue, ToStrError},
}, },
response::IntoResponse,
}; };
use bunny_api_tokio::error::Error as BunnyError; use bunny_api_tokio::error::Error as BunnyError;
use deadpool::managed::{BuildError, PoolError}; use deadpool::managed::{BuildError, PoolError};
@ -54,9 +58,13 @@ pub enum Error {
#[error(transparent)] #[error(transparent)]
UrlParseError(#[from] url::ParseError), UrlParseError(#[from] url::ParseError),
#[error(transparent)] #[error(transparent)]
PayloadError(#[from] PayloadError), JsonRejection(#[from] JsonRejection),
#[error(transparent)] #[error(transparent)]
WsClosed(#[from] actix_ws::Closed), QueryRejection(#[from] QueryRejection),
#[error(transparent)]
MultipartError(#[from] MultipartError),
#[error(transparent)]
InvalidHeaderValue(#[from] InvalidHeaderValue),
#[error(transparent)] #[error(transparent)]
EmailError(#[from] EmailError), EmailError(#[from] EmailError),
#[error(transparent)] #[error(transparent)]
@ -75,28 +83,45 @@ pub enum Error {
TooManyRequests(String), TooManyRequests(String),
#[error("{0}")] #[error("{0}")]
InternalServerError(String), InternalServerError(String),
// TODO: remove when doing socket.io
#[error(transparent)]
AxumError(#[from] axum::Error),
} }
impl ResponseError for Error { impl IntoResponse for Error {
fn error_response(&self) -> HttpResponse { fn into_response(self) -> axum::response::Response {
let error = match self {
Error::SqlError(DieselError::NotFound) => {
(StatusCode::NOT_FOUND, Json(WebError::new(self.to_string())))
}
Error::BunnyError(BunnyError::NotFound(_)) => {
(StatusCode::NOT_FOUND, Json(WebError::new(self.to_string())))
}
Error::BadRequest(_) => (
StatusCode::BAD_REQUEST,
Json(WebError::new(self.to_string())),
),
Error::Unauthorized(_) => (
StatusCode::UNAUTHORIZED,
Json(WebError::new(self.to_string())),
),
Error::Forbidden(_) => (StatusCode::FORBIDDEN, Json(WebError::new(self.to_string()))),
Error::TooManyRequests(_) => (
StatusCode::TOO_MANY_REQUESTS,
Json(WebError::new(self.to_string())),
),
_ => (
StatusCode::INTERNAL_SERVER_ERROR,
Json(WebError::new(self.to_string())),
),
};
let (code, _) = error;
debug!("{self:?}"); debug!("{self:?}");
error!("{}: {}", self.status_code(), self); error!("{code}: {self}");
HttpResponse::build(self.status_code()) error.into_response()
.insert_header(ContentType::json())
.json(WebError::new(self.to_string()))
}
fn status_code(&self) -> StatusCode {
match *self {
Error::SqlError(DieselError::NotFound) => StatusCode::NOT_FOUND,
Error::BunnyError(BunnyError::NotFound(_)) => StatusCode::NOT_FOUND,
Error::BadRequest(_) => StatusCode::BAD_REQUEST,
Error::Unauthorized(_) => StatusCode::UNAUTHORIZED,
Error::Forbidden(_) => StatusCode::FORBIDDEN,
Error::TooManyRequests(_) => StatusCode::TOO_MANY_REQUESTS,
_ => StatusCode::INTERNAL_SERVER_ERROR,
}
} }
} }

View file

@ -1,16 +1,17 @@
use actix_cors::Cors;
use actix_web::{App, HttpServer, web};
use argon2::Argon2; use argon2::Argon2;
use axum::{
Router,
http::{Method, header},
};
use clap::Parser; use clap::Parser;
use config::{Config, ConfigBuilder};
use diesel_async::pooled_connection::AsyncDieselConnectionManager; use diesel_async::pooled_connection::AsyncDieselConnectionManager;
use diesel_async::pooled_connection::deadpool::Pool; use diesel_async::pooled_connection::deadpool::Pool;
use diesel_migrations::{EmbeddedMigrations, MigrationHarness, embed_migrations};
use error::Error; use error::Error;
use objects::MailClient; use objects::MailClient;
use simple_logger::SimpleLogger; use std::{sync::Arc, time::SystemTime};
use std::time::SystemTime; use tower_http::cors::{AllowOrigin, CorsLayer};
mod config;
use config::{Config, ConfigBuilder};
use diesel_migrations::{EmbeddedMigrations, MigrationHarness, embed_migrations};
pub const MIGRATIONS: EmbeddedMigrations = embed_migrations!(); pub const MIGRATIONS: EmbeddedMigrations = embed_migrations!();
@ -18,10 +19,13 @@ type Conn =
deadpool::managed::Object<AsyncDieselConnectionManager<diesel_async::AsyncPgConnection>>; deadpool::managed::Object<AsyncDieselConnectionManager<diesel_async::AsyncPgConnection>>;
mod api; mod api;
mod config;
pub mod error; pub mod error;
pub mod objects; pub mod objects;
pub mod schema; pub mod schema;
//mod socket;
pub mod utils; pub mod utils;
mod wordlist;
#[derive(Parser, Debug)] #[derive(Parser, Debug)]
#[command(version, about, long_about = None)] #[command(version, about, long_about = None)]
@ -31,7 +35,7 @@ struct Args {
} }
#[derive(Clone)] #[derive(Clone)]
pub struct Data { pub struct AppState {
pub pool: deadpool::managed::Pool< pub pool: deadpool::managed::Pool<
AsyncDieselConnectionManager<diesel_async::AsyncPgConnection>, AsyncDieselConnectionManager<diesel_async::AsyncPgConnection>,
Conn, Conn,
@ -46,12 +50,8 @@ pub struct Data {
#[tokio::main] #[tokio::main]
async fn main() -> Result<(), Error> { async fn main() -> Result<(), Error> {
SimpleLogger::new() tracing_subscriber::fmt::init();
.with_level(log::LevelFilter::Info)
.with_colors(true)
.env()
.init()
.unwrap();
let args = Args::parse(); let args = Args::parse();
let config = ConfigBuilder::load(args.config).await?.build(); let config = ConfigBuilder::load(args.config).await?.build();
@ -112,7 +112,7 @@ async fn main() -> Result<(), Error> {
) )
*/ */
let data = Data { let app_state = Arc::new(AppState {
pool, pool,
cache_pool, cache_pool,
config, config,
@ -121,42 +121,56 @@ async fn main() -> Result<(), Error> {
start_time: SystemTime::now(), start_time: SystemTime::now(),
bunny_storage, bunny_storage,
mail_client, mail_client,
}; });
HttpServer::new(move || { let cors = CorsLayer::new()
// Set CORS headers // Allow any origin (equivalent to allowed_origin_fn returning true)
let cors = Cors::default() .allow_origin(AllowOrigin::predicate(|_origin, _request_head| true))
/* .allow_methods(vec![
Set Allowed-Control-Allow-Origin header to whatever Method::GET,
the request's Origin header is. Must be done like this Method::POST,
rather than setting it to "*" due to CORS not allowing Method::PUT,
sending of credentials (cookies) with wildcard origin. Method::DELETE,
*/ Method::HEAD,
.allowed_origin_fn(|_origin, _req_head| true) Method::OPTIONS,
/* Method::CONNECT,
Allows any request method in CORS preflight requests. Method::PATCH,
This will be restricted to only ones actually in use later. Method::TRACE,
*/ ])
.allow_any_method() .allow_headers(vec![
/* header::ACCEPT,
Allows any header(s) in request in CORS preflight requests. header::ACCEPT_LANGUAGE,
This wll be restricted to only ones actually in use later. header::AUTHORIZATION,
*/ header::CONTENT_LANGUAGE,
.allow_any_header() header::CONTENT_TYPE,
/* header::ORIGIN,
Allows browser to include cookies in requests. header::ACCEPT,
This is needed for receiving the secure HttpOnly refresh_token cookie. header::COOKIE,
*/ "x-requested-with".parse().unwrap(),
.supports_credentials(); ])
// Allow credentials
.allow_credentials(true);
App::new() /*let (socket_io, io) = SocketIo::builder()
.app_data(web::Data::new(data.clone())) .with_state(app_state.clone())
.wrap(cors) .build_layer();
.service(api::web(data.config.web.backend_url.path()))
}) io.ns("/", socket::on_connect);
.bind((web.ip, web.port))? */
.run() // build our application with a route
.await?; let app = Router::new()
// `GET /` goes to `root`
.merge(api::router(
web.backend_url.path().trim_end_matches("/"),
app_state.clone(),
))
.with_state(app_state)
//.layer(socket_io)
.layer(cors);
// run our app with hyper, listening globally on port 3000
let listener = tokio::net::TcpListener::bind(web.ip + ":" + &web.port.to_string()).await?;
axum::serve(listener, app).await?;
Ok(()) Ok(())
} }

57
src/objects/bans.rs Normal file
View file

@ -0,0 +1,57 @@
use diesel::{ExpressionMethods, QueryDsl, Queryable, Selectable, SelectableHelper};
use serde::{Deserialize, Serialize};
use uuid::Uuid;
use diesel_async::RunQueryDsl;
use crate::{Conn, error::Error, objects::load_or_empty, schema::guild_bans};
#[derive(Selectable, Queryable, Serialize, Deserialize)]
#[diesel(table_name = guild_bans)]
#[diesel(check_for_backend(diesel::pg::Pg))]
pub struct GuildBan {
pub guild_uuid: Uuid,
pub user_uuid: Uuid,
pub reason: Option<String>,
pub banned_since: chrono::DateTime<chrono::Utc>,
}
impl GuildBan {
pub async fn fetch_one(
conn: &mut Conn,
guild_uuid: Uuid,
user_uuid: Uuid,
) -> Result<GuildBan, Error> {
use guild_bans::dsl;
let guild_ban = dsl::guild_bans
.filter(dsl::guild_uuid.eq(guild_uuid))
.filter(dsl::user_uuid.eq(user_uuid))
.select(GuildBan::as_select())
.get_result(conn)
.await?;
Ok(guild_ban)
}
pub async fn fetch_all(conn: &mut Conn, guild_uuid: Uuid) -> Result<Vec<Self>, Error> {
use guild_bans::dsl;
let all_guild_bans = load_or_empty(
dsl::guild_bans
.filter(dsl::guild_uuid.eq(guild_uuid))
.load(conn)
.await,
)?;
Ok(all_guild_bans)
}
pub async fn unban(self, conn: &mut Conn) -> Result<(), Error> {
use guild_bans::dsl;
diesel::delete(guild_bans::table)
.filter(dsl::guild_uuid.eq(self.guild_uuid))
.filter(dsl::user_uuid.eq(self.user_uuid))
.execute(conn)
.await?;
Ok(())
}
}

View file

@ -2,15 +2,15 @@ use diesel::{
ExpressionMethods, Insertable, QueryDsl, Queryable, Selectable, SelectableHelper, delete, ExpressionMethods, Insertable, QueryDsl, Queryable, Selectable, SelectableHelper, delete,
insert_into, update, insert_into, update,
}; };
use diesel_async::{RunQueryDsl, pooled_connection::AsyncDieselConnectionManager}; use diesel_async::RunQueryDsl;
use serde::{Deserialize, Serialize}; use serde::{Deserialize, Serialize};
use uuid::Uuid; use uuid::Uuid;
use crate::{ use crate::{
Conn, Data, Conn,
error::Error, error::Error,
schema::{channel_permissions, channels, messages}, schema::{channel_permissions, channels, messages},
utils::{CHANNEL_REGEX, order_by_is_above}, utils::{CHANNEL_REGEX, CacheFns, order_by_is_above},
}; };
use super::{HasIsAbove, HasUuid, Message, load_or_empty, message::MessageBuilder}; use super::{HasIsAbove, HasUuid, Message, load_or_empty, message::MessageBuilder};
@ -79,56 +79,53 @@ impl HasIsAbove for Channel {
} }
impl Channel { impl Channel {
pub async fn fetch_all( pub async fn fetch_all(conn: &mut Conn, guild_uuid: Uuid) -> Result<Vec<Self>, Error> {
pool: &deadpool::managed::Pool<
AsyncDieselConnectionManager<diesel_async::AsyncPgConnection>,
Conn,
>,
guild_uuid: Uuid,
) -> Result<Vec<Self>, Error> {
let mut conn = pool.get().await?;
use channels::dsl; use channels::dsl;
let channel_builders: Vec<ChannelBuilder> = load_or_empty( let channel_builders: Vec<ChannelBuilder> = load_or_empty(
dsl::channels dsl::channels
.filter(dsl::guild_uuid.eq(guild_uuid)) .filter(dsl::guild_uuid.eq(guild_uuid))
.select(ChannelBuilder::as_select()) .select(ChannelBuilder::as_select())
.load(&mut conn) .load(conn)
.await, .await,
)?; )?;
let channel_futures = channel_builders.iter().map(async move |c| { let mut channels = vec![];
let mut conn = pool.get().await?;
c.clone().build(&mut conn).await
});
futures::future::try_join_all(channel_futures).await for builder in channel_builders {
channels.push(builder.build(conn).await?);
} }
pub async fn fetch_one(data: &Data, channel_uuid: Uuid) -> Result<Self, Error> { Ok(channels)
if let Ok(cache_hit) = data.get_cache_key(channel_uuid.to_string()).await {
return Ok(serde_json::from_str(&cache_hit)?);
} }
let mut conn = data.pool.get().await?; pub async fn fetch_one(
conn: &mut Conn,
cache_pool: &redis::Client,
channel_uuid: Uuid,
) -> Result<Self, Error> {
if let Ok(cache_hit) = cache_pool.get_cache_key(channel_uuid.to_string()).await {
return Ok(cache_hit);
}
use channels::dsl; use channels::dsl;
let channel_builder: ChannelBuilder = dsl::channels let channel_builder: ChannelBuilder = dsl::channels
.filter(dsl::uuid.eq(channel_uuid)) .filter(dsl::uuid.eq(channel_uuid))
.select(ChannelBuilder::as_select()) .select(ChannelBuilder::as_select())
.get_result(&mut conn) .get_result(conn)
.await?; .await?;
let channel = channel_builder.build(&mut conn).await?; let channel = channel_builder.build(conn).await?;
data.set_cache_key(channel_uuid.to_string(), channel.clone(), 60) cache_pool
.set_cache_key(channel_uuid.to_string(), channel.clone(), 60)
.await?; .await?;
Ok(channel) Ok(channel)
} }
pub async fn new( pub async fn new(
data: actix_web::web::Data<Data>, conn: &mut Conn,
cache_pool: &redis::Client,
guild_uuid: Uuid, guild_uuid: Uuid,
name: String, name: String,
description: Option<String>, description: Option<String>,
@ -137,11 +134,9 @@ impl Channel {
return Err(Error::BadRequest("Channel name is invalid".to_string())); return Err(Error::BadRequest("Channel name is invalid".to_string()));
} }
let mut conn = data.pool.get().await?;
let channel_uuid = Uuid::now_v7(); let channel_uuid = Uuid::now_v7();
let channels = Self::fetch_all(&data.pool, guild_uuid).await?; let channels = Self::fetch_all(conn, guild_uuid).await?;
let channels_ordered = order_by_is_above(channels).await?; let channels_ordered = order_by_is_above(channels).await?;
@ -157,7 +152,7 @@ impl Channel {
insert_into(channels::table) insert_into(channels::table)
.values(new_channel.clone()) .values(new_channel.clone())
.execute(&mut conn) .execute(conn)
.await?; .await?;
if let Some(old_last_channel) = last_channel { if let Some(old_last_channel) = last_channel {
@ -165,7 +160,7 @@ impl Channel {
update(channels::table) update(channels::table)
.filter(dsl::uuid.eq(old_last_channel.uuid)) .filter(dsl::uuid.eq(old_last_channel.uuid))
.set(dsl::is_above.eq(new_channel.uuid)) .set(dsl::is_above.eq(new_channel.uuid))
.execute(&mut conn) .execute(conn)
.await?; .await?;
} }
@ -179,28 +174,29 @@ impl Channel {
permissions: vec![], permissions: vec![],
}; };
data.set_cache_key(channel_uuid.to_string(), channel.clone(), 1800) cache_pool
.set_cache_key(channel_uuid.to_string(), channel.clone(), 1800)
.await?; .await?;
if data if cache_pool
.get_cache_key(format!("{guild_uuid}_channels")) .get_cache_key::<Vec<Channel>>(format!("{guild_uuid}_channels"))
.await .await
.is_ok() .is_ok()
{ {
data.del_cache_key(format!("{guild_uuid}_channels")).await?; cache_pool
.del_cache_key(format!("{guild_uuid}_channels"))
.await?;
} }
Ok(channel) Ok(channel)
} }
pub async fn delete(self, data: &Data) -> Result<(), Error> { pub async fn delete(self, conn: &mut Conn, cache_pool: &redis::Client) -> Result<(), Error> {
let mut conn = data.pool.get().await?;
use channels::dsl; use channels::dsl;
match update(channels::table) match update(channels::table)
.filter(dsl::is_above.eq(self.uuid)) .filter(dsl::is_above.eq(self.uuid))
.set(dsl::is_above.eq(None::<Uuid>)) .set(dsl::is_above.eq(None::<Uuid>))
.execute(&mut conn) .execute(conn)
.await .await
{ {
Ok(r) => Ok(r), Ok(r) => Ok(r),
@ -210,13 +206,13 @@ impl Channel {
delete(channels::table) delete(channels::table)
.filter(dsl::uuid.eq(self.uuid)) .filter(dsl::uuid.eq(self.uuid))
.execute(&mut conn) .execute(conn)
.await?; .await?;
match update(channels::table) match update(channels::table)
.filter(dsl::is_above.eq(self.uuid)) .filter(dsl::is_above.eq(self.uuid))
.set(dsl::is_above.eq(self.is_above)) .set(dsl::is_above.eq(self.is_above))
.execute(&mut conn) .execute(conn)
.await .await
{ {
Ok(r) => Ok(r), Ok(r) => Ok(r),
@ -224,16 +220,21 @@ impl Channel {
Err(e) => Err(e), Err(e) => Err(e),
}?; }?;
if data.get_cache_key(self.uuid.to_string()).await.is_ok() { if cache_pool
data.del_cache_key(self.uuid.to_string()).await?; .get_cache_key::<Channel>(self.uuid.to_string())
}
if data
.get_cache_key(format!("{}_channels", self.guild_uuid))
.await .await
.is_ok() .is_ok()
{ {
data.del_cache_key(format!("{}_channels", self.guild_uuid)) cache_pool.del_cache_key(self.uuid.to_string()).await?;
}
if cache_pool
.get_cache_key::<Vec<Channel>>(format!("{}_channels", self.guild_uuid))
.await
.is_ok()
{
cache_pool
.del_cache_key(format!("{}_channels", self.guild_uuid))
.await?; .await?;
} }
@ -242,32 +243,36 @@ impl Channel {
pub async fn fetch_messages( pub async fn fetch_messages(
&self, &self,
data: &Data, conn: &mut Conn,
cache_pool: &redis::Client,
amount: i64, amount: i64,
offset: i64, offset: i64,
) -> Result<Vec<Message>, Error> { ) -> Result<Vec<Message>, Error> {
let mut conn = data.pool.get().await?;
use messages::dsl; use messages::dsl;
let messages: Vec<MessageBuilder> = load_or_empty( let message_builders: Vec<MessageBuilder> = load_or_empty(
dsl::messages dsl::messages
.filter(dsl::channel_uuid.eq(self.uuid)) .filter(dsl::channel_uuid.eq(self.uuid))
.select(MessageBuilder::as_select()) .select(MessageBuilder::as_select())
.order(dsl::uuid.desc()) .order(dsl::uuid.desc())
.limit(amount) .limit(amount)
.offset(offset) .offset(offset)
.load(&mut conn) .load(conn)
.await, .await,
)?; )?;
let message_futures = messages.iter().map(async move |b| b.build(data).await); let mut messages = vec![];
futures::future::try_join_all(message_futures).await for builder in message_builders {
messages.push(builder.build(conn, cache_pool).await?);
}
Ok(messages)
} }
pub async fn new_message( pub async fn new_message(
&self, &self,
data: &Data, conn: &mut Conn,
cache_pool: &redis::Client,
user_uuid: Uuid, user_uuid: Uuid,
message: String, message: String,
reply_to: Option<Uuid>, reply_to: Option<Uuid>,
@ -282,62 +287,101 @@ impl Channel {
reply_to, reply_to,
}; };
let mut conn = data.pool.get().await?;
insert_into(messages::table) insert_into(messages::table)
.values(message.clone()) .values(message.clone())
.execute(&mut conn) .execute(conn)
.await?; .await?;
message.build(data).await message.build(conn, cache_pool).await
} }
pub async fn set_name(&mut self, data: &Data, new_name: String) -> Result<(), Error> { pub async fn set_name(
&mut self,
conn: &mut Conn,
cache_pool: &redis::Client,
new_name: String,
) -> Result<(), Error> {
if !CHANNEL_REGEX.is_match(&new_name) { if !CHANNEL_REGEX.is_match(&new_name) {
return Err(Error::BadRequest("Channel name is invalid".to_string())); return Err(Error::BadRequest("Channel name is invalid".to_string()));
} }
let mut conn = data.pool.get().await?;
use channels::dsl; use channels::dsl;
update(channels::table) update(channels::table)
.filter(dsl::uuid.eq(self.uuid)) .filter(dsl::uuid.eq(self.uuid))
.set(dsl::name.eq(&new_name)) .set(dsl::name.eq(&new_name))
.execute(&mut conn) .execute(conn)
.await?; .await?;
self.name = new_name; self.name = new_name;
if cache_pool
.get_cache_key::<Channel>(self.uuid.to_string())
.await
.is_ok()
{
cache_pool.del_cache_key(self.uuid.to_string()).await?;
}
if cache_pool
.get_cache_key::<Vec<Channel>>(format!("{}_channels", self.guild_uuid))
.await
.is_ok()
{
cache_pool
.del_cache_key(format!("{}_channels", self.guild_uuid))
.await?;
}
Ok(()) Ok(())
} }
pub async fn set_description( pub async fn set_description(
&mut self, &mut self,
data: &Data, conn: &mut Conn,
cache_pool: &redis::Client,
new_description: String, new_description: String,
) -> Result<(), Error> { ) -> Result<(), Error> {
let mut conn = data.pool.get().await?;
use channels::dsl; use channels::dsl;
update(channels::table) update(channels::table)
.filter(dsl::uuid.eq(self.uuid)) .filter(dsl::uuid.eq(self.uuid))
.set(dsl::description.eq(&new_description)) .set(dsl::description.eq(&new_description))
.execute(&mut conn) .execute(conn)
.await?; .await?;
self.description = Some(new_description); self.description = Some(new_description);
if cache_pool
.get_cache_key::<Channel>(self.uuid.to_string())
.await
.is_ok()
{
cache_pool.del_cache_key(self.uuid.to_string()).await?;
}
if cache_pool
.get_cache_key::<Vec<Channel>>(format!("{}_channels", self.guild_uuid))
.await
.is_ok()
{
cache_pool
.del_cache_key(format!("{}_channels", self.guild_uuid))
.await?;
}
Ok(()) Ok(())
} }
pub async fn move_channel(&mut self, data: &Data, new_is_above: Uuid) -> Result<(), Error> { pub async fn move_channel(
let mut conn = data.pool.get().await?; &mut self,
conn: &mut Conn,
cache_pool: &redis::Client,
new_is_above: Uuid,
) -> Result<(), Error> {
use channels::dsl; use channels::dsl;
let old_above_uuid: Option<Uuid> = match dsl::channels let old_above_uuid: Option<Uuid> = match dsl::channels
.filter(dsl::is_above.eq(self.uuid)) .filter(dsl::is_above.eq(self.uuid))
.select(dsl::uuid) .select(dsl::uuid)
.get_result(&mut conn) .get_result(conn)
.await .await
{ {
Ok(r) => Ok(Some(r)), Ok(r) => Ok(Some(r)),
@ -349,14 +393,14 @@ impl Channel {
update(channels::table) update(channels::table)
.filter(dsl::uuid.eq(uuid)) .filter(dsl::uuid.eq(uuid))
.set(dsl::is_above.eq(None::<Uuid>)) .set(dsl::is_above.eq(None::<Uuid>))
.execute(&mut conn) .execute(conn)
.await?; .await?;
} }
match update(channels::table) match update(channels::table)
.filter(dsl::is_above.eq(new_is_above)) .filter(dsl::is_above.eq(new_is_above))
.set(dsl::is_above.eq(self.uuid)) .set(dsl::is_above.eq(self.uuid))
.execute(&mut conn) .execute(conn)
.await .await
{ {
Ok(r) => Ok(r), Ok(r) => Ok(r),
@ -367,19 +411,37 @@ impl Channel {
update(channels::table) update(channels::table)
.filter(dsl::uuid.eq(self.uuid)) .filter(dsl::uuid.eq(self.uuid))
.set(dsl::is_above.eq(new_is_above)) .set(dsl::is_above.eq(new_is_above))
.execute(&mut conn) .execute(conn)
.await?; .await?;
if let Some(uuid) = old_above_uuid { if let Some(uuid) = old_above_uuid {
update(channels::table) update(channels::table)
.filter(dsl::uuid.eq(uuid)) .filter(dsl::uuid.eq(uuid))
.set(dsl::is_above.eq(self.is_above)) .set(dsl::is_above.eq(self.is_above))
.execute(&mut conn) .execute(conn)
.await?; .await?;
} }
self.is_above = Some(new_is_above); self.is_above = Some(new_is_above);
if cache_pool
.get_cache_key::<Channel>(self.uuid.to_string())
.await
.is_ok()
{
cache_pool.del_cache_key(self.uuid.to_string()).await?;
}
if cache_pool
.get_cache_key::<Vec<Channel>>(format!("{}_channels", self.guild_uuid))
.await
.is_ok()
{
cache_pool
.del_cache_key(format!("{}_channels", self.guild_uuid))
.await?;
}
Ok(()) Ok(())
} }
} }

View file

@ -3,7 +3,11 @@ use lettre::message::MultiPart;
use serde::{Deserialize, Serialize}; use serde::{Deserialize, Serialize};
use uuid::Uuid; use uuid::Uuid;
use crate::{Data, error::Error, utils::generate_token}; use crate::{
AppState,
error::Error,
utils::{CacheFns, generate_token},
};
use super::Me; use super::Me;
@ -15,18 +19,16 @@ pub struct EmailToken {
} }
impl EmailToken { impl EmailToken {
pub async fn get(data: &Data, user_uuid: Uuid) -> Result<EmailToken, Error> { pub async fn get(cache_pool: &redis::Client, user_uuid: Uuid) -> Result<EmailToken, Error> {
let email_token = serde_json::from_str( let email_token = cache_pool
&data
.get_cache_key(format!("{user_uuid}_email_verify")) .get_cache_key(format!("{user_uuid}_email_verify"))
.await?, .await?;
)?;
Ok(email_token) Ok(email_token)
} }
#[allow(clippy::new_ret_no_self)] #[allow(clippy::new_ret_no_self)]
pub async fn new(data: &Data, me: Me) -> Result<(), Error> { pub async fn new(app_state: &AppState, me: Me) -> Result<(), Error> {
let token = generate_token::<32>()?; let token = generate_token::<32>()?;
let email_token = EmailToken { let email_token = EmailToken {
@ -36,30 +38,33 @@ impl EmailToken {
created_at: Utc::now(), created_at: Utc::now(),
}; };
data.set_cache_key(format!("{}_email_verify", me.uuid), email_token, 86400) app_state
.cache_pool
.set_cache_key(format!("{}_email_verify", me.uuid), email_token, 86400)
.await?; .await?;
let mut verify_endpoint = data.config.web.frontend_url.join("verify-email")?; let mut verify_endpoint = app_state.config.web.frontend_url.join("verify-email")?;
verify_endpoint.set_query(Some(&format!("token={token}"))); verify_endpoint.set_query(Some(&format!("token={token}")));
let email = data let email = app_state
.mail_client .mail_client
.message_builder() .message_builder()
.to(me.email.parse()?) .to(me.email.parse()?)
.subject(format!("{} E-mail Verification", data.config.instance.name)) .subject(format!("{} E-mail Verification", app_state.config.instance.name))
.multipart(MultiPart::alternative_plain_html( .multipart(MultiPart::alternative_plain_html(
format!("Verify your {} account\n\nHello, {}!\nThanks for creating a new account on Gorb.\nThe final step to create your account is to verify your email address by visiting the page, within 24 hours.\n\n{}\n\nIf you didn't ask to verify this address, you can safely ignore this email\n\nThanks, The gorb team.", data.config.instance.name, me.username, verify_endpoint), format!("Verify your {} account\n\nHello, {}!\nThanks for creating a new account on Gorb.\nThe final step to create your account is to verify your email address by visiting the page, within 24 hours.\n\n{}\n\nIf you didn't ask to verify this address, you can safely ignore this email\n\nThanks, The gorb team.", app_state.config.instance.name, me.username, verify_endpoint),
format!(r#"<html lang="en"><head><meta charset="UTF-8"><meta name="viewport" content="width=device-width, initial-scale=1.0"><style>:root{{--header-text-colour: #ffffff;--footer-text-colour: #7f7f7f;--button-text-colour: #170e08;--text-colour: #170e08;--background-colour: #fbf6f2;--primary-colour: #df5f0b;--secondary-colour: #e8ac84;--accent-colour: #e68b4e;}}@media (prefers-color-scheme: dark){{:root{{--header-text-colour: #ffffff;--footer-text-colour: #585858;--button-text-colour: #ffffff;--text-colour: #f7eee8;--background-colour: #0c0704;--primary-colour: #f4741f;--secondary-colour: #7c4018;--accent-colour: #b35719;}}}}@media (max-width: 600px){{.container{{width: 100%;}}}}body{{font-family: Arial, sans-serif;align-content: center;text-align: center;margin: 0;padding: 0;background-color: var(--background-colour);color: var(--text-colour);width: 100%;max-width: 600px;margin: 0 auto;border-radius: 5px;}}.header{{background-color: var(--primary-colour);color: var(--header-text-colour);padding: 20px;}}.verify-button{{background-color: var(--accent-colour);color: var(--button-text-colour);padding: 12px 30px;margin: 16px;font-size: 20px;transition: background-color 0.3s;cursor: pointer;border: none;border-radius: 14px;text-decoration: none;display: inline-block;}}.verify-button:hover{{background-color: var(--secondary-colour);}}.content{{padding: 20px 30px;}}.footer{{padding: 10px;font-size: 12px;color: var(--footer-text-colour);}}</style></head><body><div class="container"><div class="header"><h1>Verify your {} Account</h1></div><div class="content"><h2>Hello, {}!</h2><p>Thanks for creating a new account on Gorb.</p><p>The final step to create your account is to verify your email address by clicking the button below, within 24 hours.</p><a href="{}" class="verify-button">VERIFY ACCOUNT</a><p>If you didn't ask to verify this address, you can safely ignore this email.</p><div class="footer"><p>Thanks<br>The gorb team.</p></div></div></div></body></html>"#, data.config.instance.name, me.username, verify_endpoint) format!(r#"<html lang="en"><head><meta charset="UTF-8"><meta name="viewport" content="width=device-width, initial-scale=1.0"><style>:root{{--header-text-colour: #ffffff;--footer-text-colour: #7f7f7f;--button-text-colour: #170e08;--text-colour: #170e08;--background-colour: #fbf6f2;--primary-colour: #df5f0b;--secondary-colour: #e8ac84;--accent-colour: #e68b4e;}}@media (prefers-color-scheme: dark){{:root{{--header-text-colour: #ffffff;--footer-text-colour: #585858;--button-text-colour: #ffffff;--text-colour: #f7eee8;--background-colour: #0c0704;--primary-colour: #f4741f;--secondary-colour: #7c4018;--accent-colour: #b35719;}}}}@media (max-width: 600px){{.container{{width: 100%;}}}}body{{font-family: Arial, sans-serif;align-content: center;text-align: center;margin: 0;padding: 0;background-color: var(--background-colour);color: var(--text-colour);width: 100%;max-width: 600px;margin: 0 auto;border-radius: 5px;}}.header{{background-color: var(--primary-colour);color: var(--header-text-colour);padding: 20px;}}.verify-button{{background-color: var(--accent-colour);color: var(--button-text-colour);padding: 12px 30px;margin: 16px;font-size: 20px;transition: background-color 0.3s;cursor: pointer;border: none;border-radius: 14px;text-decoration: none;display: inline-block;}}.verify-button:hover{{background-color: var(--secondary-colour);}}.content{{padding: 20px 30px;}}.footer{{padding: 10px;font-size: 12px;color: var(--footer-text-colour);}}</style></head><body><div class="container"><div class="header"><h1>Verify your {} Account</h1></div><div class="content"><h2>Hello, {}!</h2><p>Thanks for creating a new account on Gorb.</p><p>The final step to create your account is to verify your email address by clicking the button below, within 24 hours.</p><a href="{}" class="verify-button">VERIFY ACCOUNT</a><p>If you didn't ask to verify this address, you can safely ignore this email.</p><div class="footer"><p>Thanks<br>The gorb team.</p></div></div></div></body></html>"#, app_state.config.instance.name, me.username, verify_endpoint)
))?; ))?;
data.mail_client.send_mail(email).await?; app_state.mail_client.send_mail(email).await?;
Ok(()) Ok(())
} }
pub async fn delete(&self, data: &Data) -> Result<(), Error> { pub async fn delete(&self, cache_pool: &redis::Client) -> Result<(), Error> {
data.del_cache_key(format!("{}_email_verify", self.user_uuid)) cache_pool
.del_cache_key(format!("{}_email_verify", self.user_uuid))
.await?; .await?;
Ok(()) Ok(())

View file

@ -1,16 +1,16 @@
use actix_web::web::BytesMut; use axum::body::Bytes;
use diesel::{ use diesel::{
ExpressionMethods, Insertable, QueryDsl, Queryable, Selectable, SelectableHelper, insert_into, ExpressionMethods, Insertable, QueryDsl, Queryable, Selectable, SelectableHelper, insert_into,
update, update,
}; };
use diesel_async::{RunQueryDsl, pooled_connection::AsyncDieselConnectionManager}; use diesel_async::RunQueryDsl;
use serde::Serialize; use serde::Serialize;
use tokio::task; use tokio::task;
use url::Url; use url::Url;
use uuid::Uuid; use uuid::Uuid;
use crate::{ use crate::{
Conn, AppState, Conn,
error::Error, error::Error,
schema::{guild_members, guilds, invites}, schema::{guild_members, guilds, invites},
utils::image_check, utils::image_check,
@ -68,16 +68,11 @@ impl Guild {
} }
pub async fn fetch_amount( pub async fn fetch_amount(
pool: &deadpool::managed::Pool< conn: &mut Conn,
AsyncDieselConnectionManager<diesel_async::AsyncPgConnection>,
Conn,
>,
offset: i64, offset: i64,
amount: i64, amount: i64,
) -> Result<Vec<Self>, Error> { ) -> Result<Vec<Self>, Error> {
// Fetch guild data from database // Fetch guild data from database
let mut conn = pool.get().await?;
use guilds::dsl; use guilds::dsl;
let guild_builders: Vec<GuildBuilder> = load_or_empty( let guild_builders: Vec<GuildBuilder> = load_or_empty(
dsl::guilds dsl::guilds
@ -85,18 +80,17 @@ impl Guild {
.order_by(dsl::uuid) .order_by(dsl::uuid)
.offset(offset) .offset(offset)
.limit(amount) .limit(amount)
.load(&mut conn) .load(conn)
.await, .await,
)?; )?;
// Process each guild concurrently let mut guilds = vec![];
let guild_futures = guild_builders.iter().map(async move |g| {
let mut conn = pool.get().await?;
g.clone().build(&mut conn).await
});
// Execute all futures concurrently and collect results for builder in guild_builders {
futures::future::try_join_all(guild_futures).await guilds.push(builder.build(conn).await?);
}
Ok(guilds)
} }
pub async fn new(conn: &mut Conn, name: String, owner_uuid: Uuid) -> Result<Self, Error> { pub async fn new(conn: &mut Conn, name: String, owner_uuid: Uuid) -> Result<Self, Error> {
@ -188,10 +182,9 @@ impl Guild {
// FIXME: Horrible security // FIXME: Horrible security
pub async fn set_icon( pub async fn set_icon(
&mut self, &mut self,
bunny_storage: &bunny_api_tokio::EdgeStorageClient,
conn: &mut Conn, conn: &mut Conn,
cdn_url: Url, app_state: &AppState,
icon: BytesMut, icon: Bytes,
) -> Result<(), Error> { ) -> Result<(), Error> {
let icon_clone = icon.clone(); let icon_clone = icon.clone();
let image_type = task::spawn_blocking(move || image_check(icon_clone)).await??; let image_type = task::spawn_blocking(move || image_check(icon_clone)).await??;
@ -199,14 +192,14 @@ impl Guild {
if let Some(icon) = &self.icon { if let Some(icon) = &self.icon {
let relative_url = icon.path().trim_start_matches('/'); let relative_url = icon.path().trim_start_matches('/');
bunny_storage.delete(relative_url).await?; app_state.bunny_storage.delete(relative_url).await?;
} }
let path = format!("icons/{}/{}.{}", self.uuid, Uuid::now_v7(), image_type); let path = format!("icons/{}/{}.{}", self.uuid, Uuid::now_v7(), image_type);
bunny_storage.upload(path.clone(), icon.into()).await?; app_state.bunny_storage.upload(path.clone(), icon).await?;
let icon_url = cdn_url.join(&path)?; let icon_url = app_state.config.bunny.cdn_url.join(&path)?;
use guilds::dsl; use guilds::dsl;
update(guilds::table) update(guilds::table)

View file

@ -1,4 +1,4 @@
use actix_web::web::BytesMut; use axum::body::Bytes;
use diesel::{ use diesel::{
ExpressionMethods, QueryDsl, Queryable, Selectable, SelectableHelper, delete, insert_into, ExpressionMethods, QueryDsl, Queryable, Selectable, SelectableHelper, delete, insert_into,
update, update,
@ -10,11 +10,11 @@ use url::Url;
use uuid::Uuid; use uuid::Uuid;
use crate::{ use crate::{
Conn, Data, AppState, Conn,
error::Error, error::Error,
objects::{Friend, FriendRequest, User}, objects::{Friend, FriendRequest, User},
schema::{friend_requests, friends, guild_members, guilds, users}, schema::{friend_requests, friends, guild_members, guilds, users},
utils::{EMAIL_REGEX, USERNAME_REGEX, image_check}, utils::{CacheFns, EMAIL_REGEX, USERNAME_REGEX, image_check},
}; };
use super::{Guild, guild::GuildBuilder, load_or_empty, member::MemberBuilder}; use super::{Guild, guild::GuildBuilder, load_or_empty, member::MemberBuilder};
@ -29,6 +29,7 @@ pub struct Me {
avatar: Option<String>, avatar: Option<String>,
pronouns: Option<String>, pronouns: Option<String>,
about: Option<String>, about: Option<String>,
online_status: i16,
pub email: String, pub email: String,
pub email_verified: bool, pub email_verified: bool,
} }
@ -75,40 +76,44 @@ impl Me {
pub async fn set_avatar( pub async fn set_avatar(
&mut self, &mut self,
data: &Data, conn: &mut Conn,
cdn_url: Url, app_state: &AppState,
avatar: BytesMut, avatar: Bytes,
) -> Result<(), Error> { ) -> Result<(), Error> {
let avatar_clone = avatar.clone(); let avatar_clone = avatar.clone();
let image_type = task::spawn_blocking(move || image_check(avatar_clone)).await??; let image_type = task::spawn_blocking(move || image_check(avatar_clone)).await??;
let mut conn = data.pool.get().await?;
if let Some(avatar) = &self.avatar { if let Some(avatar) = &self.avatar {
let avatar_url: Url = avatar.parse()?; let avatar_url: Url = avatar.parse()?;
let relative_url = avatar_url.path().trim_start_matches('/'); let relative_url = avatar_url.path().trim_start_matches('/');
data.bunny_storage.delete(relative_url).await?; app_state.bunny_storage.delete(relative_url).await?;
} }
let path = format!("avatar/{}/{}.{}", self.uuid, Uuid::now_v7(), image_type); let path = format!("avatar/{}/{}.{}", self.uuid, Uuid::now_v7(), image_type);
data.bunny_storage app_state.bunny_storage.upload(path.clone(), avatar).await?;
.upload(path.clone(), avatar.into())
.await?;
let avatar_url = cdn_url.join(&path)?; let avatar_url = app_state.config.bunny.cdn_url.join(&path)?;
use users::dsl; use users::dsl;
update(users::table) update(users::table)
.filter(dsl::uuid.eq(self.uuid)) .filter(dsl::uuid.eq(self.uuid))
.set(dsl::avatar.eq(avatar_url.as_str())) .set(dsl::avatar.eq(avatar_url.as_str()))
.execute(&mut conn) .execute(conn)
.await?; .await?;
if data.get_cache_key(self.uuid.to_string()).await.is_ok() { if app_state
data.del_cache_key(self.uuid.to_string()).await? .cache_pool
.get_cache_key::<User>(self.uuid.to_string())
.await
.is_ok()
{
app_state
.cache_pool
.del_cache_key(self.uuid.to_string())
.await?
} }
self.avatar = Some(avatar_url.to_string()); self.avatar = Some(avatar_url.to_string());
@ -127,7 +132,12 @@ impl Me {
Ok(()) Ok(())
} }
pub async fn set_username(&mut self, data: &Data, new_username: String) -> Result<(), Error> { pub async fn set_username(
&mut self,
conn: &mut Conn,
cache_pool: &redis::Client,
new_username: String,
) -> Result<(), Error> {
if !USERNAME_REGEX.is_match(&new_username) if !USERNAME_REGEX.is_match(&new_username)
|| new_username.len() < 3 || new_username.len() < 3
|| new_username.len() > 32 || new_username.len() > 32
@ -135,17 +145,19 @@ impl Me {
return Err(Error::BadRequest("Invalid username".to_string())); return Err(Error::BadRequest("Invalid username".to_string()));
} }
let mut conn = data.pool.get().await?;
use users::dsl; use users::dsl;
update(users::table) update(users::table)
.filter(dsl::uuid.eq(self.uuid)) .filter(dsl::uuid.eq(self.uuid))
.set(dsl::username.eq(new_username.as_str())) .set(dsl::username.eq(new_username.as_str()))
.execute(&mut conn) .execute(conn)
.await?; .await?;
if data.get_cache_key(self.uuid.to_string()).await.is_ok() { if cache_pool
data.del_cache_key(self.uuid.to_string()).await? .get_cache_key::<User>(self.uuid.to_string())
.await
.is_ok()
{
cache_pool.del_cache_key(self.uuid.to_string()).await?
} }
self.username = new_username; self.username = new_username;
@ -155,11 +167,10 @@ impl Me {
pub async fn set_display_name( pub async fn set_display_name(
&mut self, &mut self,
data: &Data, conn: &mut Conn,
cache_pool: &redis::Client,
new_display_name: String, new_display_name: String,
) -> Result<(), Error> { ) -> Result<(), Error> {
let mut conn = data.pool.get().await?;
let new_display_name_option = if new_display_name.is_empty() { let new_display_name_option = if new_display_name.is_empty() {
None None
} else { } else {
@ -170,11 +181,15 @@ impl Me {
update(users::table) update(users::table)
.filter(dsl::uuid.eq(self.uuid)) .filter(dsl::uuid.eq(self.uuid))
.set(dsl::display_name.eq(&new_display_name_option)) .set(dsl::display_name.eq(&new_display_name_option))
.execute(&mut conn) .execute(conn)
.await?; .await?;
if data.get_cache_key(self.uuid.to_string()).await.is_ok() { if cache_pool
data.del_cache_key(self.uuid.to_string()).await? .get_cache_key::<User>(self.uuid.to_string())
.await
.is_ok()
{
cache_pool.del_cache_key(self.uuid.to_string()).await?
} }
self.display_name = new_display_name_option; self.display_name = new_display_name_option;
@ -182,13 +197,16 @@ impl Me {
Ok(()) Ok(())
} }
pub async fn set_email(&mut self, data: &Data, new_email: String) -> Result<(), Error> { pub async fn set_email(
&mut self,
conn: &mut Conn,
cache_pool: &redis::Client,
new_email: String,
) -> Result<(), Error> {
if !EMAIL_REGEX.is_match(&new_email) { if !EMAIL_REGEX.is_match(&new_email) {
return Err(Error::BadRequest("Invalid username".to_string())); return Err(Error::BadRequest("Invalid username".to_string()));
} }
let mut conn = data.pool.get().await?;
use users::dsl; use users::dsl;
update(users::table) update(users::table)
.filter(dsl::uuid.eq(self.uuid)) .filter(dsl::uuid.eq(self.uuid))
@ -196,11 +214,15 @@ impl Me {
dsl::email.eq(new_email.as_str()), dsl::email.eq(new_email.as_str()),
dsl::email_verified.eq(false), dsl::email_verified.eq(false),
)) ))
.execute(&mut conn) .execute(conn)
.await?; .await?;
if data.get_cache_key(self.uuid.to_string()).await.is_ok() { if cache_pool
data.del_cache_key(self.uuid.to_string()).await? .get_cache_key::<User>(self.uuid.to_string())
.await
.is_ok()
{
cache_pool.del_cache_key(self.uuid.to_string()).await?
} }
self.email = new_email; self.email = new_email;
@ -208,35 +230,78 @@ impl Me {
Ok(()) Ok(())
} }
pub async fn set_pronouns(&mut self, data: &Data, new_pronouns: String) -> Result<(), Error> { pub async fn set_pronouns(
let mut conn = data.pool.get().await?; &mut self,
conn: &mut Conn,
cache_pool: &redis::Client,
new_pronouns: String,
) -> Result<(), Error> {
use users::dsl; use users::dsl;
update(users::table) update(users::table)
.filter(dsl::uuid.eq(self.uuid)) .filter(dsl::uuid.eq(self.uuid))
.set((dsl::pronouns.eq(new_pronouns.as_str()),)) .set((dsl::pronouns.eq(new_pronouns.as_str()),))
.execute(&mut conn) .execute(conn)
.await?; .await?;
if data.get_cache_key(self.uuid.to_string()).await.is_ok() { if cache_pool
data.del_cache_key(self.uuid.to_string()).await? .get_cache_key::<User>(self.uuid.to_string())
.await
.is_ok()
{
cache_pool.del_cache_key(self.uuid.to_string()).await?
} }
Ok(()) Ok(())
} }
pub async fn set_about(&mut self, data: &Data, new_about: String) -> Result<(), Error> { pub async fn set_about(
let mut conn = data.pool.get().await?; &mut self,
conn: &mut Conn,
cache_pool: &redis::Client,
new_about: String,
) -> Result<(), Error> {
use users::dsl; use users::dsl;
update(users::table) update(users::table)
.filter(dsl::uuid.eq(self.uuid)) .filter(dsl::uuid.eq(self.uuid))
.set((dsl::about.eq(new_about.as_str()),)) .set((dsl::about.eq(new_about.as_str()),))
.execute(&mut conn) .execute(conn)
.await?; .await?;
if data.get_cache_key(self.uuid.to_string()).await.is_ok() { if cache_pool
data.del_cache_key(self.uuid.to_string()).await? .get_cache_key::<User>(self.uuid.to_string())
.await
.is_ok()
{
cache_pool.del_cache_key(self.uuid.to_string()).await?
}
Ok(())
}
pub async fn set_online_status(
&mut self,
conn: &mut Conn,
cache_pool: &redis::Client,
new_status: i16,
) -> Result<(), Error> {
if !(0..=4).contains(&new_status) {
return Err(Error::BadRequest("Invalid status code".to_string()));
}
self.online_status = new_status;
use users::dsl;
update(users::table)
.filter(dsl::uuid.eq(self.uuid))
.set(dsl::online_status.eq(new_status))
.execute(conn)
.await?;
if cache_pool
.get_cache_key::<User>(self.uuid.to_string())
.await
.is_ok()
{
cache_pool.del_cache_key(self.uuid.to_string()).await?
} }
Ok(()) Ok(())
@ -352,16 +417,18 @@ impl Me {
Ok(()) Ok(())
} }
pub async fn get_friends(&self, data: &Data) -> Result<Vec<User>, Error> { pub async fn get_friends(
&self,
conn: &mut Conn,
cache_pool: &redis::Client,
) -> Result<Vec<User>, Error> {
use friends::dsl; use friends::dsl;
let mut conn = data.pool.get().await?;
let friends1 = load_or_empty( let friends1 = load_or_empty(
dsl::friends dsl::friends
.filter(dsl::uuid1.eq(self.uuid)) .filter(dsl::uuid1.eq(self.uuid))
.select(Friend::as_select()) .select(Friend::as_select())
.load(&mut conn) .load(conn)
.await, .await,
)?; )?;
@ -369,21 +436,21 @@ impl Me {
dsl::friends dsl::friends
.filter(dsl::uuid2.eq(self.uuid)) .filter(dsl::uuid2.eq(self.uuid))
.select(Friend::as_select()) .select(Friend::as_select())
.load(&mut conn) .load(conn)
.await, .await,
)?; )?;
let friend_futures = friends1.iter().map(async move |friend| { let mut friends = vec![];
User::fetch_one_with_friendship(data, self, friend.uuid2).await
});
let mut friends = futures::future::try_join_all(friend_futures).await?; for friend in friends1 {
friends
.push(User::fetch_one_with_friendship(conn, cache_pool, self, friend.uuid2).await?);
}
let friend_futures = friends2.iter().map(async move |friend| { for friend in friends2 {
User::fetch_one_with_friendship(data, self, friend.uuid1).await friends
}); .push(User::fetch_one_with_friendship(conn, cache_pool, self, friend.uuid1).await?);
}
friends.append(&mut futures::future::try_join_all(friend_futures).await?);
Ok(friends) Ok(friends)
} }

View file

@ -1,21 +1,31 @@
use diesel::{ use diesel::{
ExpressionMethods, Insertable, QueryDsl, Queryable, Selectable, SelectableHelper, insert_into, Associations, BoolExpressionMethods, ExpressionMethods, Identifiable, Insertable, JoinOnDsl,
QueryDsl, Queryable, Selectable, SelectableHelper, define_sql_function, delete, insert_into,
sql_types::{Nullable, VarChar},
}; };
use diesel_async::RunQueryDsl; use diesel_async::RunQueryDsl;
use serde::{Deserialize, Serialize}; use serde::{Deserialize, Serialize};
use uuid::Uuid; use uuid::Uuid;
use crate::{ use crate::{
Conn, Data, Conn,
error::Error, error::Error,
objects::{Me, Permissions, Role}, objects::PaginationRequest,
schema::guild_members, schema::{friends, guild_bans, guild_members, users},
}; };
use super::{User, load_or_empty}; use super::{
Friend, Guild, GuildBan, Me, Pagination, Permissions, Role, User, load_or_empty,
user::UserBuilder,
};
#[derive(Serialize, Queryable, Selectable, Insertable)] define_sql_function! { fn coalesce(x: Nullable<VarChar>, y: Nullable<VarChar>, z: VarChar) -> Text; }
#[derive(Serialize, Queryable, Identifiable, Selectable, Insertable, Associations)]
#[diesel(table_name = guild_members)] #[diesel(table_name = guild_members)]
#[diesel(belongs_to(UserBuilder, foreign_key = user_uuid))]
#[diesel(belongs_to(Guild, foreign_key = guild_uuid))]
#[diesel(primary_key(uuid))]
#[diesel(check_for_backend(diesel::pg::Pg))] #[diesel(check_for_backend(diesel::pg::Pg))]
pub struct MemberBuilder { pub struct MemberBuilder {
pub uuid: Uuid, pub uuid: Uuid,
@ -26,15 +36,22 @@ pub struct MemberBuilder {
} }
impl MemberBuilder { impl MemberBuilder {
pub async fn build(&self, data: &Data, me: Option<&Me>) -> Result<Member, Error> { pub async fn build(
&self,
conn: &mut Conn,
cache_pool: &redis::Client,
me: Option<&Me>,
) -> Result<Member, Error> {
let user; let user;
if let Some(me) = me { if let Some(me) = me {
user = User::fetch_one_with_friendship(data, me, self.user_uuid).await?; user = User::fetch_one_with_friendship(conn, cache_pool, me, self.user_uuid).await?;
} else { } else {
user = User::fetch_one(data, self.user_uuid).await?; user = User::fetch_one(conn, cache_pool, self.user_uuid).await?;
} }
let roles = Role::fetch_from_member(conn, cache_pool, self).await?;
Ok(Member { Ok(Member {
uuid: self.uuid, uuid: self.uuid,
nickname: self.nickname.clone(), nickname: self.nickname.clone(),
@ -42,16 +59,44 @@ impl MemberBuilder {
guild_uuid: self.guild_uuid, guild_uuid: self.guild_uuid,
is_owner: self.is_owner, is_owner: self.is_owner,
user, user,
roles,
})
}
async fn build_with_parts(
&self,
conn: &mut Conn,
cache_pool: &redis::Client,
user_builder: UserBuilder,
friend: Option<Friend>,
) -> Result<Member, Error> {
let mut user = user_builder.build();
if let Some(friend) = friend {
user.friends_since = Some(friend.accepted_at);
}
let roles = Role::fetch_from_member(conn, cache_pool, self).await?;
Ok(Member {
uuid: self.uuid,
nickname: self.nickname.clone(),
user_uuid: self.user_uuid,
guild_uuid: self.guild_uuid,
is_owner: self.is_owner,
user,
roles,
}) })
} }
pub async fn check_permission( pub async fn check_permission(
&self, &self,
data: &Data, conn: &mut Conn,
cache_pool: &redis::Client,
permission: Permissions, permission: Permissions,
) -> Result<(), Error> { ) -> Result<(), Error> {
if !self.is_owner { if !self.is_owner {
let roles = Role::fetch_from_member(data, self.uuid).await?; let roles = Role::fetch_from_member(conn, cache_pool, self).await?;
let allowed = roles.iter().any(|r| r.permissions & permission as i64 != 0); let allowed = roles.iter().any(|r| r.permissions & permission as i64 != 0);
if !allowed { if !allowed {
return Err(Error::Forbidden("Not allowed".to_string())); return Err(Error::Forbidden("Not allowed".to_string()));
@ -62,14 +107,16 @@ impl MemberBuilder {
} }
} }
#[derive(Serialize, Deserialize)] #[derive(Serialize, Deserialize, Clone)]
pub struct Member { pub struct Member {
pub uuid: Uuid, pub uuid: Uuid,
pub nickname: Option<String>, pub nickname: Option<String>,
#[serde(skip)]
pub user_uuid: Uuid, pub user_uuid: Uuid,
pub guild_uuid: Uuid, pub guild_uuid: Uuid,
pub is_owner: bool, pub is_owner: bool,
user: User, user: User,
roles: Vec<Role>,
} }
impl Member { impl Member {
@ -101,47 +148,173 @@ impl Member {
} }
pub async fn fetch_one( pub async fn fetch_one(
data: &Data, conn: &mut Conn,
me: &Me, cache_pool: &redis::Client,
me: Option<&Me>,
user_uuid: Uuid, user_uuid: Uuid,
guild_uuid: Uuid, guild_uuid: Uuid,
) -> Result<Self, Error> { ) -> Result<Self, Error> {
let mut conn = data.pool.get().await?; let member: MemberBuilder;
let user: UserBuilder;
let friend: Option<Friend>;
use friends::dsl as fdsl;
use guild_members::dsl; use guild_members::dsl;
let member: MemberBuilder = dsl::guild_members if let Some(me) = me {
.filter(dsl::user_uuid.eq(user_uuid)) (member, user, friend) = dsl::guild_members
.filter(dsl::guild_uuid.eq(guild_uuid)) .filter(dsl::guild_uuid.eq(guild_uuid))
.select(MemberBuilder::as_select()) .filter(dsl::user_uuid.eq(user_uuid))
.get_result(&mut conn) .inner_join(users::table)
.left_join(
fdsl::friends.on(fdsl::uuid1
.eq(me.uuid)
.and(fdsl::uuid2.eq(users::uuid))
.or(fdsl::uuid2.eq(me.uuid).and(fdsl::uuid1.eq(users::uuid)))),
)
.select((
MemberBuilder::as_select(),
UserBuilder::as_select(),
Option::<Friend>::as_select(),
))
.get_result(conn)
.await?;
} else {
(member, user) = dsl::guild_members
.filter(dsl::guild_uuid.eq(guild_uuid))
.filter(dsl::user_uuid.eq(user_uuid))
.inner_join(users::table)
.select((MemberBuilder::as_select(), UserBuilder::as_select()))
.get_result(conn)
.await?; .await?;
member.build(data, Some(me)).await friend = None;
} }
pub async fn fetch_all(data: &Data, me: &Me, guild_uuid: Uuid) -> Result<Vec<Self>, Error> { member
let mut conn = data.pool.get().await?; .build_with_parts(conn, cache_pool, user, friend)
.await
}
pub async fn fetch_one_with_uuid(
conn: &mut Conn,
cache_pool: &redis::Client,
me: Option<&Me>,
uuid: Uuid,
) -> Result<Self, Error> {
let member: MemberBuilder;
let user: UserBuilder;
let friend: Option<Friend>;
use friends::dsl as fdsl;
use guild_members::dsl; use guild_members::dsl;
let member_builders: Vec<MemberBuilder> = load_or_empty( if let Some(me) = me {
(member, user, friend) = dsl::guild_members
.filter(dsl::uuid.eq(uuid))
.inner_join(users::table)
.left_join(
fdsl::friends.on(fdsl::uuid1
.eq(me.uuid)
.and(fdsl::uuid2.eq(users::uuid))
.or(fdsl::uuid2.eq(me.uuid).and(fdsl::uuid1.eq(users::uuid)))),
)
.select((
MemberBuilder::as_select(),
UserBuilder::as_select(),
Option::<Friend>::as_select(),
))
.get_result(conn)
.await?;
} else {
(member, user) = dsl::guild_members
.filter(dsl::uuid.eq(uuid))
.inner_join(users::table)
.select((MemberBuilder::as_select(), UserBuilder::as_select()))
.get_result(conn)
.await?;
friend = None;
}
member
.build_with_parts(conn, cache_pool, user, friend)
.await
}
pub async fn fetch_page(
conn: &mut Conn,
cache_pool: &redis::Client,
me: &Me,
guild_uuid: Uuid,
pagination: PaginationRequest,
) -> Result<Pagination<Self>, Error> {
let per_page = pagination.per_page.unwrap_or(50);
let page_multiplier: i64 = ((pagination.page - 1) * per_page).into();
if !(10..=100).contains(&per_page) {
return Err(Error::BadRequest(
"Invalid amount per page requested".to_string(),
));
}
use friends::dsl as fdsl;
use guild_members::dsl;
let member_builders: Vec<(MemberBuilder, UserBuilder, Option<Friend>)> = load_or_empty(
dsl::guild_members dsl::guild_members
.filter(dsl::guild_uuid.eq(guild_uuid)) .filter(dsl::guild_uuid.eq(guild_uuid))
.select(MemberBuilder::as_select()) .inner_join(users::table)
.load(&mut conn) .left_join(
fdsl::friends.on(fdsl::uuid1
.eq(me.uuid)
.and(fdsl::uuid2.eq(users::uuid))
.or(fdsl::uuid2.eq(me.uuid).and(fdsl::uuid1.eq(users::uuid)))),
)
.limit(per_page.into())
.offset(page_multiplier)
.order_by(coalesce(
dsl::nickname,
users::display_name,
users::username,
))
.select((
MemberBuilder::as_select(),
UserBuilder::as_select(),
Option::<Friend>::as_select(),
))
.load(conn)
.await, .await,
)?; )?;
let mut members = vec![]; let pages = Member::count(conn, guild_uuid).await? as f32 / per_page as f32;
for builder in member_builders { let mut members = Pagination::<Member> {
members.push(builder.build(&data, Some(me)).await?); objects: Vec::with_capacity(member_builders.len()),
amount: member_builders.len() as i32,
pages: pages.ceil() as i32,
page: pagination.page,
};
for (member, user, friend) in member_builders {
members.objects.push(
member
.build_with_parts(conn, cache_pool, user, friend)
.await?,
);
} }
Ok(members) Ok(members)
} }
pub async fn new(data: &Data, user_uuid: Uuid, guild_uuid: Uuid) -> Result<Self, Error> { pub async fn new(
let mut conn = data.pool.get().await?; conn: &mut Conn,
cache_pool: &redis::Client,
user_uuid: Uuid,
guild_uuid: Uuid,
) -> Result<Self, Error> {
let banned = GuildBan::fetch_one(conn, guild_uuid, user_uuid).await;
match banned {
Ok(_) => Err(Error::Forbidden("User banned".to_string())),
Err(Error::SqlError(diesel::result::Error::NotFound)) => Ok(()),
Err(e) => Err(e),
}?;
let member_uuid = Uuid::now_v7(); let member_uuid = Uuid::now_v7();
@ -155,9 +328,51 @@ impl Member {
insert_into(guild_members::table) insert_into(guild_members::table)
.values(&member) .values(&member)
.execute(&mut conn) .execute(conn)
.await?; .await?;
member.build(data, None).await member.build(conn, cache_pool, None).await
}
pub async fn delete(self, conn: &mut Conn) -> Result<(), Error> {
if self.is_owner {
return Err(Error::Forbidden("Can not kick owner".to_string()));
}
delete(guild_members::table)
.filter(guild_members::uuid.eq(self.uuid))
.execute(conn)
.await?;
Ok(())
}
pub async fn ban(self, conn: &mut Conn, reason: &String) -> Result<(), Error> {
if self.is_owner {
return Err(Error::Forbidden("Can not ban owner".to_string()));
}
use guild_bans::dsl;
insert_into(guild_bans::table)
.values((
dsl::guild_uuid.eq(self.guild_uuid),
dsl::user_uuid.eq(self.user_uuid),
dsl::reason.eq(reason),
))
.execute(conn)
.await?;
self.delete(conn).await?;
Ok(())
}
pub fn to_builder(&self) -> MemberBuilder {
MemberBuilder {
uuid: self.uuid,
nickname: self.nickname.clone(),
user_uuid: self.user_uuid,
guild_uuid: self.guild_uuid,
is_owner: self.is_owner,
}
} }
} }

View file

@ -1,10 +1,15 @@
use diesel::{Insertable, Queryable, Selectable}; use diesel::{ExpressionMethods, Insertable, QueryDsl, Queryable, Selectable};
use diesel_async::RunQueryDsl;
use serde::Serialize; use serde::Serialize;
use uuid::Uuid; use uuid::Uuid;
use crate::{Data, error::Error, schema::messages}; use crate::{
Conn,
error::Error,
schema::{channels, guilds, messages},
};
use super::User; use super::Member;
#[derive(Clone, Queryable, Selectable, Insertable)] #[derive(Clone, Queryable, Selectable, Insertable)]
#[diesel(table_name = messages)] #[diesel(table_name = messages)]
@ -18,8 +23,21 @@ pub struct MessageBuilder {
} }
impl MessageBuilder { impl MessageBuilder {
pub async fn build(&self, data: &Data) -> Result<Message, Error> { pub async fn build(
let user = User::fetch_one(data, self.user_uuid).await?; &self,
conn: &mut Conn,
cache_pool: &redis::Client,
) -> Result<Message, Error> {
use channels::dsl;
let guild_uuid = dsl::channels
.filter(dsl::uuid.eq(self.channel_uuid))
.inner_join(guilds::table)
.select(guilds::uuid)
.get_result(conn)
.await?;
let member = Member::fetch_one(conn, cache_pool, None, self.user_uuid, guild_uuid).await?;
Ok(Message { Ok(Message {
uuid: self.uuid, uuid: self.uuid,
@ -27,7 +45,7 @@ impl MessageBuilder {
user_uuid: self.user_uuid, user_uuid: self.user_uuid,
message: self.message.clone(), message: self.message.clone(),
reply_to: self.reply_to, reply_to: self.reply_to,
user, member,
}) })
} }
} }
@ -39,5 +57,5 @@ pub struct Message {
user_uuid: Uuid, user_uuid: Uuid,
message: String, message: String,
reply_to: Option<Uuid>, reply_to: Option<Uuid>,
user: User, member: Member,
} }

View file

@ -4,9 +4,10 @@ use lettre::{
transport::smtp::authentication::Credentials, transport::smtp::authentication::Credentials,
}; };
use log::debug; use log::debug;
use serde::Deserialize; use serde::{Deserialize, Serialize};
use uuid::Uuid; use uuid::Uuid;
mod bans;
mod channel; mod channel;
mod email_token; mod email_token;
mod friends; mod friends;
@ -19,6 +20,7 @@ mod password_reset_token;
mod role; mod role;
mod user; mod user;
pub use bans::GuildBan;
pub use channel::Channel; pub use channel::Channel;
pub use email_token::EmailToken; pub use email_token::EmailToken;
pub use friends::Friend; pub use friends::Friend;
@ -42,6 +44,51 @@ pub trait HasUuid {
pub trait HasIsAbove { pub trait HasIsAbove {
fn is_above(&self) -> Option<&Uuid>; fn is_above(&self) -> Option<&Uuid>;
} }
/*
pub trait Cookies {
fn cookies(&self) -> CookieJar;
fn cookie<T: AsRef<str>>(&self, cookie: T) -> Option<Cookie>;
}
impl Cookies for Request<Body> {
fn cookies(&self) -> CookieJar {
let cookies = self.headers()
.get(axum::http::header::COOKIE)
.and_then(|value| value.to_str().ok())
.map(|s| Cookie::split_parse(s.to_string()))
.and_then(|c| c.collect::<Result<Vec<Cookie>, cookie::ParseError>>().ok())
.unwrap_or(vec![]);
let mut cookie_jar = CookieJar::new();
for cookie in cookies {
cookie_jar.add(cookie)
}
cookie_jar
}
fn cookie<T: AsRef<str>>(&self, cookie: T) -> Option<Cookie> {
self.cookies()
.get(cookie.as_ref())
.and_then(|c| Some(c.to_owned()))
}
}
*/
#[derive(Serialize)]
pub struct Pagination<T> {
objects: Vec<T>,
amount: i32,
pages: i32,
page: i32,
}
#[derive(Deserialize)]
pub struct PaginationRequest {
pub page: i32,
pub per_page: Option<i32>,
}
fn load_or_empty<T>( fn load_or_empty<T>(
query_result: Result<Vec<T>, diesel::result::Error>, query_result: Result<Vec<T>, diesel::result::Error>,

View file

@ -10,10 +10,10 @@ use serde::{Deserialize, Serialize};
use uuid::Uuid; use uuid::Uuid;
use crate::{ use crate::{
Data, AppState, Conn,
error::Error, error::Error,
schema::users, schema::users,
utils::{PASSWORD_REGEX, generate_token, global_checks, user_uuid_from_identifier}, utils::{CacheFns, PASSWORD_REGEX, generate_token, global_checks, user_uuid_from_identifier},
}; };
#[derive(Serialize, Deserialize)] #[derive(Serialize, Deserialize)]
@ -24,49 +24,49 @@ pub struct PasswordResetToken {
} }
impl PasswordResetToken { impl PasswordResetToken {
pub async fn get(data: &Data, token: String) -> Result<PasswordResetToken, Error> { pub async fn get(
let user_uuid: Uuid = serde_json::from_str(&data.get_cache_key(token.to_string()).await?)?; cache_pool: &redis::Client,
let password_reset_token = serde_json::from_str( token: String,
&data ) -> Result<PasswordResetToken, Error> {
let user_uuid: Uuid = cache_pool.get_cache_key(token.to_string()).await?;
let password_reset_token = cache_pool
.get_cache_key(format!("{user_uuid}_password_reset")) .get_cache_key(format!("{user_uuid}_password_reset"))
.await?, .await?;
)?;
Ok(password_reset_token) Ok(password_reset_token)
} }
pub async fn get_with_identifier( pub async fn get_with_identifier(
data: &Data, conn: &mut Conn,
cache_pool: &redis::Client,
identifier: String, identifier: String,
) -> Result<PasswordResetToken, Error> { ) -> Result<PasswordResetToken, Error> {
let mut conn = data.pool.get().await?; let user_uuid = user_uuid_from_identifier(conn, &identifier).await?;
let user_uuid = user_uuid_from_identifier(&mut conn, &identifier).await?; let password_reset_token = cache_pool
let password_reset_token = serde_json::from_str(
&data
.get_cache_key(format!("{user_uuid}_password_reset")) .get_cache_key(format!("{user_uuid}_password_reset"))
.await?, .await?;
)?;
Ok(password_reset_token) Ok(password_reset_token)
} }
#[allow(clippy::new_ret_no_self)] #[allow(clippy::new_ret_no_self)]
pub async fn new(data: &Data, identifier: String) -> Result<(), Error> { pub async fn new(
conn: &mut Conn,
app_state: &AppState,
identifier: String,
) -> Result<(), Error> {
let token = generate_token::<32>()?; let token = generate_token::<32>()?;
let mut conn = data.pool.get().await?; let user_uuid = user_uuid_from_identifier(conn, &identifier).await?;
let user_uuid = user_uuid_from_identifier(&mut conn, &identifier).await?; global_checks(conn, &app_state.config, user_uuid).await?;
global_checks(data, user_uuid).await?;
use users::dsl as udsl; use users::dsl as udsl;
let (username, email_address): (String, String) = udsl::users let (username, email_address): (String, String) = udsl::users
.filter(udsl::uuid.eq(user_uuid)) .filter(udsl::uuid.eq(user_uuid))
.select((udsl::username, udsl::email)) .select((udsl::username, udsl::email))
.get_result(&mut conn) .get_result(conn)
.await?; .await?;
let password_reset_token = PasswordResetToken { let password_reset_token = PasswordResetToken {
@ -75,34 +75,44 @@ impl PasswordResetToken {
created_at: Utc::now(), created_at: Utc::now(),
}; };
data.set_cache_key( app_state
.cache_pool
.set_cache_key(
format!("{user_uuid}_password_reset"), format!("{user_uuid}_password_reset"),
password_reset_token, password_reset_token,
86400, 86400,
) )
.await?; .await?;
data.set_cache_key(token.clone(), user_uuid, 86400).await?; app_state
.cache_pool
.set_cache_key(token.clone(), user_uuid, 86400)
.await?;
let mut reset_endpoint = data.config.web.frontend_url.join("reset-password")?; let mut reset_endpoint = app_state.config.web.frontend_url.join("reset-password")?;
reset_endpoint.set_query(Some(&format!("token={token}"))); reset_endpoint.set_query(Some(&format!("token={token}")));
let email = data let email = app_state
.mail_client .mail_client
.message_builder() .message_builder()
.to(email_address.parse()?) .to(email_address.parse()?)
.subject(format!("{} Password Reset", data.config.instance.name)) .subject(format!("{} Password Reset", app_state.config.instance.name))
.multipart(MultiPart::alternative_plain_html( .multipart(MultiPart::alternative_plain_html(
format!("{} Password Reset\n\nHello, {}!\nSomeone requested a password reset for your Gorb account.\nClick the button below within 24 hours to reset your password.\n\n{}\n\nIf you didn't request a password reset, don't worry, your account is safe and you can safely ignore this email.\n\nThanks, The gorb team.", data.config.instance.name, username, reset_endpoint), format!("{} Password Reset\n\nHello, {}!\nSomeone requested a password reset for your Gorb account.\nClick the button below within 24 hours to reset your password.\n\n{}\n\nIf you didn't request a password reset, don't worry, your account is safe and you can safely ignore this email.\n\nThanks, The gorb team.", app_state.config.instance.name, username, reset_endpoint),
format!(r#"<html lang="en"><head><meta charset="UTF-8"><meta name="viewport" content="width=device-width, initial-scale=1.0"><style>:root {{--header-text-colour: #ffffff;--footer-text-colour: #7f7f7f;--button-text-colour: #170e08;--text-colour: #170e08;--background-colour: #fbf6f2;--primary-colour: #df5f0b;--secondary-colour: #e8ac84;--accent-colour: #e68b4e;}}@media (prefers-color-scheme: dark) {{:root {{--header-text-colour: #ffffff;--footer-text-colour: #585858;--button-text-colour: #ffffff;--text-colour: #f7eee8;--background-colour: #0c0704;--primary-colour: #f4741f;--secondary-colour: #7c4018;--accent-colour: #b35719;}}}}@media (max-width: 600px) {{.container {{width: 100%;}}}}body {{font-family: Arial, sans-serif;align-content: center;text-align: center;margin: 0;padding: 0;background-color: var(--background-colour);color: var(--text-colour);width: 100%;max-width: 600px;margin: 0 auto;border-radius: 5px;}}.header {{background-color: var(--primary-colour);color: var(--header-text-colour);padding: 20px;}}.verify-button {{background-color: var(--accent-colour);color: var(--button-text-colour);padding: 12px 30px;margin: 16px;font-size: 20px;transition: background-color 0.3s;cursor: pointer;border: none;border-radius: 14px;text-decoration: none;display: inline-block;}}.verify-button:hover {{background-color: var(--secondary-colour);}}.content {{padding: 20px 30px;}}.footer {{padding: 10px;font-size: 12px;color: var(--footer-text-colour);}}</style></head><body><div class="container"><div class="header"><h1>{} Password Reset</h1></div><div class="content"><h2>Hello, {}!</h2><p>Someone requested a password reset for your Gorb account.</p><p>Click the button below within 24 hours to reset your password.</p><a href="{}" class="verify-button">RESET PASSWORD</a><p>If you didn't request a password reset, don't worry, your account is safe and you can safely ignore this email.</p><div class="footer"><p>Thanks<br>The gorb team.</p></div></div></div></body></html>"#, data.config.instance.name, username, reset_endpoint) format!(r#"<html lang="en"><head><meta charset="UTF-8"><meta name="viewport" content="width=device-width, initial-scale=1.0"><style>:root {{--header-text-colour: #ffffff;--footer-text-colour: #7f7f7f;--button-text-colour: #170e08;--text-colour: #170e08;--background-colour: #fbf6f2;--primary-colour: #df5f0b;--secondary-colour: #e8ac84;--accent-colour: #e68b4e;}}@media (prefers-color-scheme: dark) {{:root {{--header-text-colour: #ffffff;--footer-text-colour: #585858;--button-text-colour: #ffffff;--text-colour: #f7eee8;--background-colour: #0c0704;--primary-colour: #f4741f;--secondary-colour: #7c4018;--accent-colour: #b35719;}}}}@media (max-width: 600px) {{.container {{width: 100%;}}}}body {{font-family: Arial, sans-serif;align-content: center;text-align: center;margin: 0;padding: 0;background-color: var(--background-colour);color: var(--text-colour);width: 100%;max-width: 600px;margin: 0 auto;border-radius: 5px;}}.header {{background-color: var(--primary-colour);color: var(--header-text-colour);padding: 20px;}}.verify-button {{background-color: var(--accent-colour);color: var(--button-text-colour);padding: 12px 30px;margin: 16px;font-size: 20px;transition: background-color 0.3s;cursor: pointer;border: none;border-radius: 14px;text-decoration: none;display: inline-block;}}.verify-button:hover {{background-color: var(--secondary-colour);}}.content {{padding: 20px 30px;}}.footer {{padding: 10px;font-size: 12px;color: var(--footer-text-colour);}}</style></head><body><div class="container"><div class="header"><h1>{} Password Reset</h1></div><div class="content"><h2>Hello, {}!</h2><p>Someone requested a password reset for your Gorb account.</p><p>Click the button below within 24 hours to reset your password.</p><a href="{}" class="verify-button">RESET PASSWORD</a><p>If you didn't request a password reset, don't worry, your account is safe and you can safely ignore this email.</p><div class="footer"><p>Thanks<br>The gorb team.</p></div></div></div></body></html>"#, app_state.config.instance.name, username, reset_endpoint)
))?; ))?;
data.mail_client.send_mail(email).await?; app_state.mail_client.send_mail(email).await?;
Ok(()) Ok(())
} }
pub async fn set_password(&self, data: &Data, password: String) -> Result<(), Error> { pub async fn set_password(
&self,
conn: &mut Conn,
app_state: &AppState,
password: String,
) -> Result<(), Error> {
if !PASSWORD_REGEX.is_match(&password) { if !PASSWORD_REGEX.is_match(&password) {
return Err(Error::BadRequest( return Err(Error::BadRequest(
"Please provide a valid password".to_string(), "Please provide a valid password".to_string(),
@ -111,47 +121,46 @@ impl PasswordResetToken {
let salt = SaltString::generate(&mut OsRng); let salt = SaltString::generate(&mut OsRng);
let hashed_password = data let hashed_password = app_state
.argon2 .argon2
.hash_password(password.as_bytes(), &salt) .hash_password(password.as_bytes(), &salt)
.map_err(|e| Error::PasswordHashError(e.to_string()))?; .map_err(|e| Error::PasswordHashError(e.to_string()))?;
let mut conn = data.pool.get().await?;
use users::dsl; use users::dsl;
update(users::table) update(users::table)
.filter(dsl::uuid.eq(self.user_uuid)) .filter(dsl::uuid.eq(self.user_uuid))
.set(dsl::password.eq(hashed_password.to_string())) .set(dsl::password.eq(hashed_password.to_string()))
.execute(&mut conn) .execute(conn)
.await?; .await?;
let (username, email_address): (String, String) = dsl::users let (username, email_address): (String, String) = dsl::users
.filter(dsl::uuid.eq(self.user_uuid)) .filter(dsl::uuid.eq(self.user_uuid))
.select((dsl::username, dsl::email)) .select((dsl::username, dsl::email))
.get_result(&mut conn) .get_result(conn)
.await?; .await?;
let login_page = data.config.web.frontend_url.join("login")?; let login_page = app_state.config.web.frontend_url.join("login")?;
let email = data let email = app_state
.mail_client .mail_client
.message_builder() .message_builder()
.to(email_address.parse()?) .to(email_address.parse()?)
.subject(format!("Your {} Password has been Reset", data.config.instance.name)) .subject(format!("Your {} Password has been Reset", app_state.config.instance.name))
.multipart(MultiPart::alternative_plain_html( .multipart(MultiPart::alternative_plain_html(
format!("{} Password Reset Confirmation\n\nHello, {}!\nYour password has been successfully reset for your Gorb account.\nIf you did not initiate this change, please click the link below to reset your password <strong>immediately</strong>.\n\n{}\n\nThanks, The gorb team.", data.config.instance.name, username, login_page), format!("{} Password Reset Confirmation\n\nHello, {}!\nYour password has been successfully reset for your Gorb account.\nIf you did not initiate this change, please click the link below to reset your password <strong>immediately</strong>.\n\n{}\n\nThanks, The gorb team.", app_state.config.instance.name, username, login_page),
format!(r#"<html lang="en"><head><meta charset="UTF-8"><meta name="viewport" content="width=device-width, initial-scale=1.0"><style>:root {{--header-text-colour: #ffffff;--footer-text-colour: #7f7f7f;--button-text-colour: #170e08;--text-colour: #170e08;--background-colour: #fbf6f2;--primary-colour: #df5f0b;--secondary-colour: #e8ac84;--accent-colour: #e68b4e;}}@media (prefers-color-scheme: dark) {{:root {{--header-text-colour: #ffffff;--footer-text-colour: #585858;--button-text-colour: #ffffff;--text-colour: #f7eee8;--background-colour: #0c0704;--primary-colour: #f4741f;--secondary-colour: #7c4018;--accent-colour: #b35719;}}}}@media (max-width: 600px) {{.container {{width: 100%;}}}}body {{font-family: Arial, sans-serif;align-content: center;text-align: center;margin: 0;padding: 0;background-color: var(--background-colour);color: var(--text-colour);width: 100%;max-width: 600px;margin: 0 auto;border-radius: 5px;}}.header {{background-color: var(--primary-colour);color: var(--header-text-colour);padding: 20px;}}.verify-button {{background-color: var(--accent-colour);color: var(--button-text-colour);padding: 12px 30px;margin: 16px;font-size: 20px;transition: background-color 0.3s;cursor: pointer;border: none;border-radius: 14px;text-decoration: none;display: inline-block;}}.verify-button:hover {{background-color: var(--secondary-colour);}}.content {{padding: 20px 30px;}}.footer {{padding: 10px;font-size: 12px;color: var(--footer-text-colour);}}</style></head><body><div class="container"><div class="header"><h1>{} Password Reset Confirmation</h1></div><div class="content"><h2>Hello, {}!</h2><p>Your password has been successfully reset for your Gorb account.</p><p>If you did not initiate this change, please click the button below to reset your password <strong>immediately</strong>.</p><a href="{}" class="verify-button">RESET PASSWORD</a><div class="footer"><p>Thanks<br>The gorb team.</p></div></div></div></body></html>"#, data.config.instance.name, username, login_page) format!(r#"<html lang="en"><head><meta charset="UTF-8"><meta name="viewport" content="width=device-width, initial-scale=1.0"><style>:root {{--header-text-colour: #ffffff;--footer-text-colour: #7f7f7f;--button-text-colour: #170e08;--text-colour: #170e08;--background-colour: #fbf6f2;--primary-colour: #df5f0b;--secondary-colour: #e8ac84;--accent-colour: #e68b4e;}}@media (prefers-color-scheme: dark) {{:root {{--header-text-colour: #ffffff;--footer-text-colour: #585858;--button-text-colour: #ffffff;--text-colour: #f7eee8;--background-colour: #0c0704;--primary-colour: #f4741f;--secondary-colour: #7c4018;--accent-colour: #b35719;}}}}@media (max-width: 600px) {{.container {{width: 100%;}}}}body {{font-family: Arial, sans-serif;align-content: center;text-align: center;margin: 0;padding: 0;background-color: var(--background-colour);color: var(--text-colour);width: 100%;max-width: 600px;margin: 0 auto;border-radius: 5px;}}.header {{background-color: var(--primary-colour);color: var(--header-text-colour);padding: 20px;}}.verify-button {{background-color: var(--accent-colour);color: var(--button-text-colour);padding: 12px 30px;margin: 16px;font-size: 20px;transition: background-color 0.3s;cursor: pointer;border: none;border-radius: 14px;text-decoration: none;display: inline-block;}}.verify-button:hover {{background-color: var(--secondary-colour);}}.content {{padding: 20px 30px;}}.footer {{padding: 10px;font-size: 12px;color: var(--footer-text-colour);}}</style></head><body><div class="container"><div class="header"><h1>{} Password Reset Confirmation</h1></div><div class="content"><h2>Hello, {}!</h2><p>Your password has been successfully reset for your Gorb account.</p><p>If you did not initiate this change, please click the button below to reset your password <strong>immediately</strong>.</p><a href="{}" class="verify-button">RESET PASSWORD</a><div class="footer"><p>Thanks<br>The gorb team.</p></div></div></div></body></html>"#, app_state.config.instance.name, username, login_page)
))?; ))?;
data.mail_client.send_mail(email).await?; app_state.mail_client.send_mail(email).await?;
self.delete(data).await self.delete(&app_state.cache_pool).await
} }
pub async fn delete(&self, data: &Data) -> Result<(), Error> { pub async fn delete(&self, cache_pool: &redis::Client) -> Result<(), Error> {
data.del_cache_key(format!("{}_password_reset", &self.user_uuid)) cache_pool
.del_cache_key(format!("{}_password_reset", &self.user_uuid))
.await?; .await?;
data.del_cache_key(self.token.to_string()).await?; cache_pool.del_cache_key(self.token.to_string()).await?;
Ok(()) Ok(())
} }

View file

@ -1,22 +1,24 @@
use diesel::query_dsl::BelongingToDsl;
use diesel::{ use diesel::{
ExpressionMethods, Insertable, QueryDsl, Queryable, Selectable, SelectableHelper, insert_into, Associations, ExpressionMethods, Identifiable, Insertable, QueryDsl, Queryable, Selectable,
update, SelectableHelper, insert_into, update,
}; };
use diesel_async::RunQueryDsl; use diesel_async::RunQueryDsl;
use serde::{Deserialize, Serialize}; use serde::{Deserialize, Serialize};
use uuid::Uuid; use uuid::Uuid;
use crate::{ use crate::{
Conn, Data, Conn,
error::Error, error::Error,
schema::{role_members, roles}, schema::{role_members, roles},
utils::order_by_is_above, utils::{CacheFns, order_by_is_above},
}; };
use super::{HasIsAbove, HasUuid, load_or_empty}; use super::{HasIsAbove, HasUuid, load_or_empty, member::MemberBuilder};
#[derive(Deserialize, Serialize, Clone, Queryable, Selectable, Insertable)] #[derive(Deserialize, Serialize, Clone, Identifiable, Queryable, Selectable, Insertable)]
#[diesel(table_name = roles)] #[diesel(table_name = roles)]
#[diesel(primary_key(uuid))]
#[diesel(check_for_backend(diesel::pg::Pg))] #[diesel(check_for_backend(diesel::pg::Pg))]
pub struct Role { pub struct Role {
uuid: Uuid, uuid: Uuid,
@ -27,27 +29,17 @@ pub struct Role {
pub permissions: i64, pub permissions: i64,
} }
#[derive(Serialize, Clone, Queryable, Selectable, Insertable)] #[derive(Serialize, Clone, Identifiable, Queryable, Selectable, Insertable, Associations)]
#[diesel(table_name = role_members)] #[diesel(table_name = role_members)]
#[diesel(belongs_to(MemberBuilder, foreign_key = member_uuid))]
#[diesel(belongs_to(Role, foreign_key = role_uuid))]
#[diesel(primary_key(role_uuid, member_uuid))]
#[diesel(check_for_backend(diesel::pg::Pg))] #[diesel(check_for_backend(diesel::pg::Pg))]
pub struct RoleMember { pub struct RoleMember {
role_uuid: Uuid, role_uuid: Uuid,
member_uuid: Uuid, member_uuid: Uuid,
} }
impl RoleMember {
async fn fetch_role(&self, conn: &mut Conn) -> Result<Role, Error> {
use roles::dsl;
let role: Role = dsl::roles
.filter(dsl::uuid.eq(self.role_uuid))
.select(Role::as_select())
.get_result(conn)
.await?;
Ok(role)
}
}
impl HasUuid for Role { impl HasUuid for Role {
fn uuid(&self) -> &Uuid { fn uuid(&self) -> &Uuid {
self.uuid.as_ref() self.uuid.as_ref()
@ -74,29 +66,28 @@ impl Role {
Ok(roles) Ok(roles)
} }
pub async fn fetch_from_member(data: &Data, member_uuid: Uuid) -> Result<Vec<Self>, Error> { pub async fn fetch_from_member(
if let Ok(roles) = data.get_cache_key(format!("{member_uuid}_roles")).await { conn: &mut Conn,
return Ok(serde_json::from_str(&roles)?); cache_pool: &redis::Client,
member: &MemberBuilder,
) -> Result<Vec<Self>, Error> {
if let Ok(roles) = cache_pool
.get_cache_key(format!("{}_roles", member.uuid))
.await
{
return Ok(roles);
} }
let mut conn = data.pool.get().await?; let roles: Vec<Role> = load_or_empty(
RoleMember::belonging_to(member)
use role_members::dsl; .inner_join(roles::table)
let role_memberships: Vec<RoleMember> = load_or_empty( .select(Role::as_select())
dsl::role_members .load(conn)
.filter(dsl::member_uuid.eq(member_uuid))
.select(RoleMember::as_select())
.load(&mut conn)
.await, .await,
)?; )?;
let mut roles = vec![]; cache_pool
.set_cache_key(format!("{}_roles", member.uuid), roles.clone(), 300)
for membership in role_memberships {
roles.push(membership.fetch_role(&mut conn).await?);
}
data.set_cache_key(format!("{member_uuid}_roles"), roles.clone(), 300)
.await?; .await?;
Ok(roles) Ok(roles)
@ -169,6 +160,10 @@ pub enum Permissions {
ManageGuild = 32, ManageGuild = 32,
/// Lets users change member settings (nickname, etc) /// Lets users change member settings (nickname, etc)
ManageMember = 64, ManageMember = 64,
/// Lets users ban members
BanMember = 128,
/// Lets users kick members
KickMember = 256,
} }
impl Permissions { impl Permissions {
@ -181,6 +176,8 @@ impl Permissions {
Self::ManageInvite, Self::ManageInvite,
Self::ManageGuild, Self::ManageGuild,
Self::ManageMember, Self::ManageMember,
Self::BanMember,
Self::KickMember,
]; ];
all_perms all_perms

View file

@ -4,7 +4,7 @@ use diesel_async::RunQueryDsl;
use serde::{Deserialize, Serialize}; use serde::{Deserialize, Serialize};
use uuid::Uuid; use uuid::Uuid;
use crate::{Conn, Data, error::Error, objects::Me, schema::users}; use crate::{Conn, error::Error, objects::Me, schema::users, utils::CacheFns};
use super::load_or_empty; use super::load_or_empty;
@ -18,10 +18,11 @@ pub struct UserBuilder {
avatar: Option<String>, avatar: Option<String>,
pronouns: Option<String>, pronouns: Option<String>,
about: Option<String>, about: Option<String>,
online_status: i16,
} }
impl UserBuilder { impl UserBuilder {
fn build(self) -> User { pub fn build(self) -> User {
User { User {
uuid: self.uuid, uuid: self.uuid,
username: self.username, username: self.username,
@ -29,6 +30,7 @@ impl UserBuilder {
avatar: self.avatar, avatar: self.avatar,
pronouns: self.pronouns, pronouns: self.pronouns,
about: self.about, about: self.about,
online_status: self.online_status,
friends_since: None, friends_since: None,
} }
} }
@ -36,48 +38,51 @@ impl UserBuilder {
#[derive(Deserialize, Serialize, Clone)] #[derive(Deserialize, Serialize, Clone)]
pub struct User { pub struct User {
uuid: Uuid, pub uuid: Uuid,
username: String, username: String,
display_name: Option<String>, display_name: Option<String>,
avatar: Option<String>, avatar: Option<String>,
pronouns: Option<String>, pronouns: Option<String>,
about: Option<String>, about: Option<String>,
friends_since: Option<DateTime<Utc>>, online_status: i16,
pub friends_since: Option<DateTime<Utc>>,
} }
impl User { impl User {
pub async fn fetch_one(data: &Data, user_uuid: Uuid) -> Result<Self, Error> { pub async fn fetch_one(
let mut conn = data.pool.get().await?; conn: &mut Conn,
cache_pool: &redis::Client,
if let Ok(cache_hit) = data.get_cache_key(user_uuid.to_string()).await { user_uuid: Uuid,
return Ok(serde_json::from_str(&cache_hit)?); ) -> Result<Self, Error> {
if let Ok(cache_hit) = cache_pool.get_cache_key(user_uuid.to_string()).await {
return Ok(cache_hit);
} }
use users::dsl; use users::dsl;
let user_builder: UserBuilder = dsl::users let user_builder: UserBuilder = dsl::users
.filter(dsl::uuid.eq(user_uuid)) .filter(dsl::uuid.eq(user_uuid))
.select(UserBuilder::as_select()) .select(UserBuilder::as_select())
.get_result(&mut conn) .get_result(conn)
.await?; .await?;
let user = user_builder.build(); let user = user_builder.build();
data.set_cache_key(user_uuid.to_string(), user.clone(), 1800) cache_pool
.set_cache_key(user_uuid.to_string(), user.clone(), 1800)
.await?; .await?;
Ok(user) Ok(user)
} }
pub async fn fetch_one_with_friendship( pub async fn fetch_one_with_friendship(
data: &Data, conn: &mut Conn,
cache_pool: &redis::Client,
me: &Me, me: &Me,
user_uuid: Uuid, user_uuid: Uuid,
) -> Result<Self, Error> { ) -> Result<Self, Error> {
let mut conn = data.pool.get().await?; let mut user = Self::fetch_one(conn, cache_pool, user_uuid).await?;
let mut user = Self::fetch_one(data, user_uuid).await?; if let Some(friend) = me.friends_with(conn, user_uuid).await? {
if let Some(friend) = me.friends_with(&mut conn, user_uuid).await? {
user.friends_since = Some(friend.accepted_at); user.friends_since = Some(friend.accepted_at);
} }

View file

@ -47,6 +47,16 @@ diesel::table! {
} }
} }
diesel::table! {
guild_bans (user_uuid, guild_uuid) {
guild_uuid -> Uuid,
user_uuid -> Uuid,
#[max_length = 200]
reason -> Nullable<Varchar>,
banned_since -> Timestamptz,
}
}
diesel::table! { diesel::table! {
guild_members (uuid) { guild_members (uuid) {
uuid -> Uuid, uuid -> Uuid,
@ -103,7 +113,7 @@ diesel::table! {
token -> Varchar, token -> Varchar,
uuid -> Uuid, uuid -> Uuid,
created_at -> Int8, created_at -> Int8,
#[max_length = 16] #[max_length = 64]
device_name -> Varchar, device_name -> Varchar,
} }
} }
@ -116,7 +126,7 @@ diesel::table! {
} }
diesel::table! { diesel::table! {
roles (uuid, guild_uuid) { roles (uuid) {
uuid -> Uuid, uuid -> Uuid,
guild_uuid -> Uuid, guild_uuid -> Uuid,
#[max_length = 50] #[max_length = 50]
@ -147,13 +157,17 @@ diesel::table! {
pronouns -> Nullable<Varchar>, pronouns -> Nullable<Varchar>,
#[max_length = 200] #[max_length = 200]
about -> Nullable<Varchar>, about -> Nullable<Varchar>,
online_status -> Int2,
} }
} }
diesel::joinable!(access_tokens -> refresh_tokens (refresh_token)); diesel::joinable!(access_tokens -> refresh_tokens (refresh_token));
diesel::joinable!(access_tokens -> users (uuid)); diesel::joinable!(access_tokens -> users (uuid));
diesel::joinable!(channel_permissions -> channels (channel_uuid)); diesel::joinable!(channel_permissions -> channels (channel_uuid));
diesel::joinable!(channel_permissions -> roles (role_uuid));
diesel::joinable!(channels -> guilds (guild_uuid)); diesel::joinable!(channels -> guilds (guild_uuid));
diesel::joinable!(guild_bans -> guilds (guild_uuid));
diesel::joinable!(guild_bans -> users (user_uuid));
diesel::joinable!(guild_members -> guilds (guild_uuid)); diesel::joinable!(guild_members -> guilds (guild_uuid));
diesel::joinable!(guild_members -> users (user_uuid)); diesel::joinable!(guild_members -> users (user_uuid));
diesel::joinable!(instance_permissions -> users (uuid)); diesel::joinable!(instance_permissions -> users (uuid));
@ -163,6 +177,7 @@ diesel::joinable!(messages -> channels (channel_uuid));
diesel::joinable!(messages -> users (user_uuid)); diesel::joinable!(messages -> users (user_uuid));
diesel::joinable!(refresh_tokens -> users (uuid)); diesel::joinable!(refresh_tokens -> users (uuid));
diesel::joinable!(role_members -> guild_members (member_uuid)); diesel::joinable!(role_members -> guild_members (member_uuid));
diesel::joinable!(role_members -> roles (role_uuid));
diesel::joinable!(roles -> guilds (guild_uuid)); diesel::joinable!(roles -> guilds (guild_uuid));
diesel::allow_tables_to_appear_in_same_query!( diesel::allow_tables_to_appear_in_same_query!(
@ -171,6 +186,7 @@ diesel::allow_tables_to_appear_in_same_query!(
channels, channels,
friend_requests, friend_requests,
friends, friends,
guild_bans,
guild_members, guild_members,
guilds, guilds,
instance_permissions, instance_permissions,

28
src/socket.rs Normal file
View file

@ -0,0 +1,28 @@
use std::sync::Arc;
use log::info;
use rmpv::Value;
use socketioxide::extract::{AckSender, Data, SocketRef, State};
use crate::AppState;
pub async fn on_connect(
State(_app_state): State<Arc<AppState>>,
socket: SocketRef,
Data(data): Data<Value>,
) {
socket.emit("auth", &data).ok();
socket.on("message", async |socket: SocketRef, Data::<Value>(data)| {
info!("{data}");
socket.emit("message-back", &data).ok();
});
socket.on(
"message-with-ack",
async |Data::<Value>(data), ack: AckSender| {
info!("{data}");
ack.send(&data).ok();
},
);
}

View file

@ -1,26 +1,25 @@
use rand::seq::IndexedRandom;
use std::sync::LazyLock; use std::sync::LazyLock;
use actix_web::{ use axum::body::Bytes;
cookie::{Cookie, SameSite, time::Duration}, use axum_extra::extract::cookie::{Cookie, SameSite};
http::header::HeaderMap,
web::BytesMut,
};
use bindet::FileType; use bindet::FileType;
use diesel::{ExpressionMethods, QueryDsl}; use diesel::{ExpressionMethods, QueryDsl};
use diesel_async::RunQueryDsl; use diesel_async::RunQueryDsl;
use getrandom::fill; use getrandom::fill;
use hex::encode; use hex::encode;
use redis::RedisError;
use regex::Regex; use regex::Regex;
use serde::Serialize; use serde::{Serialize, de::DeserializeOwned};
use time::Duration;
use uuid::Uuid; use uuid::Uuid;
use crate::{ use crate::{
Conn, Data, Conn,
config::Config, config::Config,
error::Error, error::Error,
objects::{HasIsAbove, HasUuid}, objects::{HasIsAbove, HasUuid},
schema::users, schema::users,
wordlist::{ADJECTIVES, ANIMALS},
}; };
pub static EMAIL_REGEX: LazyLock<Regex> = LazyLock::new(|| { pub static EMAIL_REGEX: LazyLock<Regex> = LazyLock::new(|| {
@ -33,86 +32,16 @@ pub static USERNAME_REGEX: LazyLock<Regex> =
pub static CHANNEL_REGEX: LazyLock<Regex> = pub static CHANNEL_REGEX: LazyLock<Regex> =
LazyLock::new(|| Regex::new(r"^[a-z0-9_.-]+$").unwrap()); LazyLock::new(|| Regex::new(r"^[a-z0-9_.-]+$").unwrap());
// Password is expected to be hashed using SHA3-384
pub static PASSWORD_REGEX: LazyLock<Regex> = LazyLock::new(|| Regex::new(r"[0-9a-f]{96}").unwrap()); pub static PASSWORD_REGEX: LazyLock<Regex> = LazyLock::new(|| Regex::new(r"[0-9a-f]{96}").unwrap());
pub fn get_auth_header(headers: &HeaderMap) -> Result<&str, Error> { pub fn new_refresh_token_cookie(config: &Config, refresh_token: String) -> Cookie {
let auth_token = headers.get(actix_web::http::header::AUTHORIZATION); Cookie::build(("refresh_token", refresh_token))
if auth_token.is_none() {
return Err(Error::Unauthorized(
"No authorization header provided".to_string(),
));
}
let auth_raw = auth_token.unwrap().to_str()?;
let mut auth = auth_raw.split_whitespace();
let auth_type = auth.next();
let auth_value = auth.next();
if auth_type.is_none() {
return Err(Error::BadRequest(
"Authorization header is empty".to_string(),
));
} else if auth_type.is_some_and(|at| at != "Bearer") {
return Err(Error::BadRequest(
"Only token auth is supported".to_string(),
));
}
if auth_value.is_none() {
return Err(Error::BadRequest("No token provided".to_string()));
}
Ok(auth_value.unwrap())
}
pub fn get_ws_protocol_header(headers: &HeaderMap) -> Result<&str, Error> {
let auth_token = headers.get(actix_web::http::header::SEC_WEBSOCKET_PROTOCOL);
if auth_token.is_none() {
return Err(Error::Unauthorized(
"No authorization header provided".to_string(),
));
}
let auth_raw = auth_token.unwrap().to_str()?;
let mut auth = auth_raw.split_whitespace();
let response_proto = auth.next();
let auth_value = auth.next();
if response_proto.is_none() {
return Err(Error::BadRequest(
"Sec-WebSocket-Protocol header is empty".to_string(),
));
} else if response_proto.is_some_and(|rp| rp != "Authorization,") {
return Err(Error::BadRequest(
"First protocol should be Authorization".to_string(),
));
}
if auth_value.is_none() {
return Err(Error::BadRequest("No token provided".to_string()));
}
Ok(auth_value.unwrap())
}
pub fn new_refresh_token_cookie(config: &Config, refresh_token: String) -> Cookie<'static> {
Cookie::build("refresh_token", refresh_token)
.http_only(true) .http_only(true)
.secure(true) .secure(true)
.same_site(SameSite::None) .same_site(SameSite::None)
//.domain(config.web.backend_url.domain().unwrap().to_string())
.path(config.web.backend_url.path().to_string()) .path(config.web.backend_url.path().to_string())
.max_age(Duration::days(30)) .max_age(Duration::days(30))
.finish() .build()
} }
pub fn generate_token<const N: usize>() -> Result<String, getrandom::Error> { pub fn generate_token<const N: usize>() -> Result<String, getrandom::Error> {
@ -121,7 +50,7 @@ pub fn generate_token<const N: usize>() -> Result<String, getrandom::Error> {
Ok(encode(buf)) Ok(encode(buf))
} }
pub fn image_check(icon: BytesMut) -> Result<String, Error> { pub fn image_check(icon: Bytes) -> Result<String, Error> {
let buf = std::io::Cursor::new(icon); let buf = std::io::Cursor::new(icon);
let detect = bindet::detect(buf).map_err(|e| e.kind()); let detect = bindet::detect(buf).map_err(|e| e.kind());
@ -168,15 +97,30 @@ pub async fn user_uuid_from_identifier(
} }
} }
pub async fn global_checks(data: &Data, user_uuid: Uuid) -> Result<(), Error> { pub async fn user_uuid_from_username(conn: &mut Conn, username: &String) -> Result<Uuid, Error> {
if data.config.instance.require_email_verification { if USERNAME_REGEX.is_match(username) {
let mut conn = data.pool.get().await?; use users::dsl;
let user_uuid = dsl::users
.filter(dsl::username.eq(username))
.select(dsl::uuid)
.get_result(conn)
.await?;
Ok(user_uuid)
} else {
Err(Error::BadRequest(
"Please provide a valid username".to_string(),
))
}
}
pub async fn global_checks(conn: &mut Conn, config: &Config, user_uuid: Uuid) -> Result<(), Error> {
if config.instance.require_email_verification {
use users::dsl; use users::dsl;
let email_verified: bool = dsl::users let email_verified: bool = dsl::users
.filter(dsl::uuid.eq(user_uuid)) .filter(dsl::uuid.eq(user_uuid))
.select(dsl::email_verified) .select(dsl::email_verified)
.get_result(&mut conn) .get_result(conn)
.await?; .await?;
if !email_verified { if !email_verified {
@ -214,14 +158,28 @@ where
Ok(ordered) Ok(ordered)
} }
impl Data { #[allow(async_fn_in_trait)]
pub async fn set_cache_key( pub trait CacheFns {
async fn set_cache_key(
&self,
key: String,
value: impl Serialize,
expire: u32,
) -> Result<(), Error>;
async fn get_cache_key<T>(&self, key: String) -> Result<T, Error>
where
T: DeserializeOwned;
async fn del_cache_key(&self, key: String) -> Result<(), Error>;
}
impl CacheFns for redis::Client {
async fn set_cache_key(
&self, &self,
key: String, key: String,
value: impl Serialize, value: impl Serialize,
expire: u32, expire: u32,
) -> Result<(), Error> { ) -> Result<(), Error> {
let mut conn = self.cache_pool.get_multiplexed_tokio_connection().await?; let mut conn = self.get_multiplexed_tokio_connection().await?;
let key_encoded = encode(key); let key_encoded = encode(key);
@ -240,25 +198,39 @@ impl Data {
Ok(()) Ok(())
} }
pub async fn get_cache_key(&self, key: String) -> Result<String, RedisError> { async fn get_cache_key<T>(&self, key: String) -> Result<T, Error>
let mut conn = self.cache_pool.get_multiplexed_tokio_connection().await?; where
T: DeserializeOwned,
{
let mut conn = self.get_multiplexed_tokio_connection().await?;
let key_encoded = encode(key); let key_encoded = encode(key);
redis::cmd("GET") let res: String = redis::cmd("GET")
.arg(key_encoded) .arg(key_encoded)
.query_async(&mut conn) .query_async(&mut conn)
.await .await?;
Ok(serde_json::from_str(&res)?)
} }
pub async fn del_cache_key(&self, key: String) -> Result<(), RedisError> { async fn del_cache_key(&self, key: String) -> Result<(), Error> {
let mut conn = self.cache_pool.get_multiplexed_tokio_connection().await?; let mut conn = self.get_multiplexed_tokio_connection().await?;
let key_encoded = encode(key); let key_encoded = encode(key);
redis::cmd("DEL") Ok(redis::cmd("DEL")
.arg(key_encoded) .arg(key_encoded)
.query_async(&mut conn) .query_async(&mut conn)
.await .await?)
} }
} }
pub fn generate_device_name() -> String {
let mut rng = rand::rng();
let adjective = ADJECTIVES.choose(&mut rng).unwrap();
let animal = ANIMALS.choose(&mut rng).unwrap();
[*adjective, *animal].join(" ")
}

993
src/wordlist.rs Normal file
View file

@ -0,0 +1,993 @@
pub const ANIMALS: [&str; 223] = [
"Aardvark",
"Albatross",
"Alligator",
"Alpaca",
"Ant",
"Anteater",
"Antelope",
"Ape",
"Armadillo",
"Donkey",
"Baboon",
"Badger",
"Barracuda",
"Bat",
"Bear",
"Beaver",
"Bee",
"Bison",
"Boar",
"Buffalo",
"Butterfly",
"Camel",
"Capybara",
"Caribou",
"Cassowary",
"Cat",
"Caterpillar",
"Cattle",
"Chamois",
"Cheetah",
"Chicken",
"Chimpanzee",
"Chinchilla",
"Chough",
"Clam",
"Cobra",
"Cockroach",
"Cod",
"Cormorant",
"Coyote",
"Crab",
"Crane",
"Crocodile",
"Crow",
"Curlew",
"Deer",
"Dinosaur",
"Dog",
"Dogfish",
"Dolphin",
"Dotterel",
"Dove",
"Dragonfly",
"Duck",
"Dugong",
"Dunlin",
"Eagle",
"Echidna",
"Eel",
"Eland",
"Elephant",
"Elk",
"Emu",
"Falcon",
"Ferret",
"Finch",
"Fish",
"Flamingo",
"Fly",
"Fox",
"Frog",
"Gaur",
"Gazelle",
"Gerbil",
"Giraffe",
"Gnat",
"Gnu",
"Goat",
"Goldfinch",
"Goldfish",
"Goose",
"Gorilla",
"Goshawk",
"Grasshopper",
"Grouse",
"Guanaco",
"Gull",
"Hamster",
"Hare",
"Hawk",
"Hedgehog",
"Heron",
"Herring",
"Hippopotamus",
"Hornet",
"Horse",
"Hummingbird",
"Hyena",
"Ibex",
"Ibis",
"Jackal",
"Jaguar",
"Jay",
"Jellyfish",
"Kangaroo",
"Kingfisher",
"Koala",
"Kookabura",
"Kouprey",
"Kudu",
"Lapwing",
"Lark",
"Lemur",
"Leopard",
"Lion",
"Llama",
"Lobster",
"Locust",
"Loris",
"Louse",
"Lyrebird",
"Magpie",
"Mallard",
"Manatee",
"Mandrill",
"Mantis",
"Marten",
"Meerkat",
"Mink",
"Mole",
"Mongoose",
"Monkey",
"Moose",
"Mosquito",
"Mouse",
"Mule",
"Narwhal",
"Newt",
"Nightingale",
"Octopus",
"Okapi",
"Opossum",
"Oryx",
"Ostrich",
"Otter",
"Owl",
"Oyster",
"Panther",
"Parrot",
"Partridge",
"Peafowl",
"Pelican",
"Penguin",
"Pheasant",
"Pig",
"Pigeon",
"Pony",
"Porcupine",
"Porpoise",
"Quail",
"Quelea",
"Quetzal",
"Rabbit",
"Raccoon",
"Rail",
"Ram",
"Rat",
"Raven",
"Red Deer",
"Red Panda",
"Reindeer",
"Rhinoceros",
"Rook",
"Salamander",
"Salmon",
"Sand Dollar",
"Sandpiper",
"Sardine",
"Scorpion",
"Seahorse",
"Seal",
"Shark",
"Sheep",
"Shrew",
"Skunk",
"Snail",
"Snake",
"Sparrow",
"Spider",
"Spoonbill",
"Squid",
"Squirrel",
"Starling",
"Stingray",
"Stinkbug",
"Stork",
"Swallow",
"Swan",
"Tapir",
"Tarsier",
"Termite",
"Tiger",
"Toad",
"Trout",
"Turkey",
"Turtle",
"Viper",
"Vulture",
"Wallaby",
"Walrus",
"Wasp",
"Weasel",
"Whale",
"Wildcat",
"Wolf",
"Wolverine",
"Wombat",
"Woodcock",
"Woodpecker",
"Worm",
"Wren",
"Yak",
"Zebra",
];
pub const ADJECTIVES: [&str; 765] = [
"Other",
"Such",
"First",
"Many",
"New",
"More",
"Same",
"Own",
"Good",
"Different",
"Great",
"Long",
"High",
"Social",
"Little",
"Much",
"Important",
"Small",
"Most",
"Large",
"Old",
"Few",
"General",
"Second",
"Public",
"Last",
"Several",
"Early",
"Certain",
"Economic",
"Least",
"Common",
"Present",
"Next",
"Local",
"Best",
"Particular",
"Young",
"Various",
"Necessary",
"Whole",
"Only",
"True",
"Able",
"Major",
"Full",
"Low",
"Available",
"Real",
"Similar",
"Total",
"Special",
"Less",
"Short",
"Specific",
"Single",
"Self",
"National",
"Individual",
"Clear",
"Personal",
"Higher",
"Better",
"Third",
"Natural",
"Greater",
"Open",
"Difficult",
"Current",
"Further",
"Main",
"Physical",
"Foreign",
"Lower",
"Strong",
"Private",
"Likely",
"International",
"Significant",
"Late",
"Basic",
"Hard",
"Modern",
"Simple",
"Normal",
"Sure",
"Central",
"Original",
"Effective",
"Following",
"Direct",
"Final",
"Cultural",
"Big",
"Recent",
"Complete",
"Financial",
"Positive",
"Primary",
"Appropriate",
"Legal",
"European",
"Equal",
"Larger",
"Average",
"Historical",
"Critical",
"Wide",
"Traditional",
"Additional",
"Active",
"Complex",
"Former",
"Independent",
"Entire",
"Actual",
"Close",
"Constant",
"Previous",
"Easy",
"Serious",
"Potential",
"Fine",
"Industrial",
"Subject",
"Future",
"Internal",
"Initial",
"Well",
"Essential",
"Dark",
"Popular",
"Successful",
"Standard",
"Year",
"Past",
"Ready",
"Professional",
"Wrong",
"Very",
"Proper",
"Separate",
"Heavy",
"Civil",
"Responsible",
"Considerable",
"Light",
"Cold",
"Above",
"Older",
"Practical",
"External",
"Sufficient",
"Interesting",
"Upper",
"Scientific",
"Key",
"Annual",
"Limited",
"Smaller",
"Southern",
"Earlier",
"Commercial",
"Powerful",
"Later",
"Like",
"Clinical",
"Ancient",
"Educational",
"Typical",
"Technical",
"Environmental",
"Formal",
"Aware",
"Beautiful",
"Variable",
"Obvious",
"Secondary",
"Enough",
"Urban",
"Regular",
"Relevant",
"Greatest",
"Spiritual",
"Time",
"Double",
"Happy",
"Term",
"Multiple",
"Dependent",
"Correct",
"Northern",
"Middle",
"Rural",
"Official",
"Fundamental",
"Numerous",
"Overall",
"Usual",
"Native",
"Regional",
"Highest",
"North",
"Agricultural",
"Literary",
"Broad",
"Perfect",
"Experimental",
"Fourth",
"Global",
"Ordinary",
"Related",
"Apparent",
"Daily",
"Principal",
"Contemporary",
"Severe",
"Reasonable",
"Subsequent",
"Worth",
"Longer",
"Emotional",
"Intellectual",
"Unique",
"Pure",
"Familiar",
"American",
"Solid",
"Brief",
"Famous",
"Fresh",
"Day",
"Corresponding",
"Characteristic",
"Maximum",
"Detailed",
"Outside",
"Theoretical",
"Fair",
"Opposite",
"Capable",
"Visual",
"Interested",
"Joint",
"Adequate",
"Based",
"Substantial",
"Unable",
"Structural",
"Soft",
"False",
"Largest",
"Inner",
"Mean",
"Extensive",
"Excellent",
"Rapid",
"Absolute",
"Consistent",
"Continuous",
"Administrative",
"Strange",
"Willing",
"Alternative",
"Slow",
"Distinct",
"Safe",
"Permanent",
"Front",
"Corporate",
"Academic",
"Thin",
"Nineteenth",
"Universal",
"Functional",
"Unknown",
"Careful",
"Narrow",
"Evident",
"Sound",
"Classical",
"Minor",
"Weak",
"Suitable",
"Chief",
"Extreme",
"Yellow",
"Warm",
"Mixed",
"Flat",
"Huge",
"Vast",
"Stable",
"Valuable",
"Rare",
"Visible",
"Sensitive",
"Mechanical",
"State",
"Radical",
"Extra",
"Superior",
"Conventional",
"Thick",
"Dominant",
"Post",
"Collective",
"Younger",
"Efficient",
"Linear",
"Organic",
"Oral",
"Century",
"Creative",
"Vertical",
"Dynamic",
"Empty",
"Minimum",
"Cognitive",
"Logical",
"Afraid",
"Equivalent",
"Quick",
"Near",
"Concrete",
"Mass",
"Acute",
"Sharp",
"Easier",
"Quiet",
"Adult",
"Accurate",
"Ideal",
"Partial",
"Bright",
"Identical",
"Conservative",
"Magnetic",
"Frequent",
"Electronic",
"Fixed",
"Square",
"Cross",
"Clean",
"Back",
"Organizational",
"Constitutional",
"Genetic",
"Ultimate",
"Secret",
"Vital",
"Dramatic",
"Objective",
"Round",
"Alive",
"Straight",
"Unusual",
"Rational",
"Electric",
"Mutual",
"Class",
"Competitive",
"Revolutionary",
"Statistical",
"Random",
"Musical",
"Crucial",
"Racial",
"Sudden",
"Acid",
"Content",
"Temporary",
"Line",
"Remarkable",
"Exact",
"Valid",
"Helpful",
"Nice",
"Comprehensive",
"United",
"Level",
"Fifth",
"Nervous",
"Expensive",
"Prominent",
"Healthy",
"Liquid",
"Institutional",
"Silent",
"Sweet",
"Strategic",
"Molecular",
"Comparative",
"Called",
"Electrical",
"Raw",
"Acceptable",
"Scale",
"Violent",
"All",
"Desirable",
"Tall",
"Steady",
"Wonderful",
"Sub",
"Distant",
"Progressive",
"Enormous",
"Horizontal",
"And",
"Intense",
"Smooth",
"Applicable",
"Over",
"Animal",
"Abstract",
"Wise",
"Worst",
"Gold",
"Precise",
"Legislative",
"Remote",
"Technological",
"Outer",
"Uniform",
"Slight",
"Attractive",
"Evil",
"Tiny",
"Royal",
"Angry",
"Advanced",
"Friendly",
"Dear",
"Busy",
"Spatial",
"Rough",
"Primitive",
"Judicial",
"Systematic",
"Lateral",
"Sorry",
"Plain",
"Off",
"Comfortable",
"Definite",
"Massive",
"Firm",
"Widespread",
"Prior",
"Twentieth",
"Mathematical",
"Verbal",
"Marginal",
"Excessive",
"Stronger",
"Gross",
"World",
"Productive",
"Wider",
"Glad",
"Linguistic",
"Patient",
"Symbolic",
"Earliest",
"Plastic",
"Type",
"Prime",
"Eighteenth",
"Blind",
"Neutral",
"Guilty",
"Hand",
"Extraordinary",
"Metal",
"Surprising",
"Fellow",
"York",
"Grand",
"Thermal",
"Artificial",
"Five",
"Lowest",
"Genuine",
"Dimensional",
"Optical",
"Unlikely",
"Developmental",
"Reliable",
"Executive",
"Comparable",
"Satisfactory",
"Golden",
"Diverse",
"Preliminary",
"Wooden",
"Noble",
"Part",
"Striking",
"Cool",
"Classic",
"Elderly",
"Four",
"Temporal",
"Indirect",
"Romantic",
"Intermediate",
"Differential",
"Passive",
"Life",
"Voluntary",
"Out",
"Adjacent",
"Behavioral",
"Exclusive",
"Closed",
"Inherent",
"Inevitable",
"Complicated",
"Quantitative",
"Respective",
"Artistic",
"Probable",
"Anxious",
"Informal",
"Strict",
"Fiscal",
"Ideological",
"Profound",
"Extended",
"Eternal",
"Known",
"Infinite",
"Proud",
"Honest",
"Peculiar",
"Absent",
"Pleasant",
"Optimal",
"Renal",
"Static",
"Outstanding",
"Presidential",
"Digital",
"Integrated",
"Legitimate",
"Curious",
"Aggressive",
"Deeper",
"Elementary",
"History",
"Surgical",
"Occasional",
"Flexible",
"Convenient",
"Solar",
"Atomic",
"Isolated",
"Latest",
"Sad",
"Conceptual",
"Underlying",
"Everyday",
"Cost",
"Intensive",
"Odd",
"Subjective",
"Mid",
"Worthy",
"Pale",
"Meaningful",
"Therapeutic",
"Making",
"Circular",
"Realistic",
"Multi",
"Child",
"Sophisticated",
"Down",
"Leading",
"Intelligent",
"Governmental",
"Numerical",
"Minimal",
"Diagnostic",
"Indigenous",
"Aesthetic",
"Distinctive",
"Operational",
"Sole",
"Material",
"Fast",
"Bitter",
"Broader",
"Brilliant",
"Peripheral",
"Rigid",
"Automatic",
"Lesser",
"Routine",
"Favorable",
"Cooperative",
"Cardiac",
"Arbitrary",
"Loose",
"Favorite",
"Subtle",
"Uncertain",
"Hostile",
"Monthly",
"Naval",
"Physiological",
"Historic",
"Developed",
"Skilled",
"Anterior",
"Pro",
"Gentle",
"Loud",
"Pulmonary",
"Innocent",
"Provincial",
"Mild",
"Page",
"Specialized",
"Bare",
"Excess",
"Inter",
"Shaped",
"Theological",
"Sensory",
"The",
"Stress",
"Novel",
"Working",
"Shorter",
"Secular",
"Geographical",
"Intimate",
"Liable",
"Selective",
"Influential",
"Modest",
"Successive",
"Continued",
"Water",
"Expert",
"Municipal",
"Marine",
"Thirty",
"Adverse",
"Wacky",
"Closer",
"Virtual",
"Peaceful",
"Mobile",
"Sixth",
"Immune",
"Coastal",
"Representative",
"Lead",
"Forward",
"Faithful",
"Crystal",
"Protective",
"Elaborate",
"Tremendous",
"Welcoming",
"Abnormal",
"Grateful",
"Proportional",
"Dual",
"Operative",
"Precious",
"Sympathetic",
"Accessible",
"Lovely",
"Spinal",
"Even",
"Marked",
"Observed",
"Point",
"Mature",
"Competent",
"Residential",
"Impressive",
"Unexpected",
"Nearby",
"Unnecessary",
"Generous",
"Cerebral",
"Unpublished",
"Delicate",
"Analytical",
"Tropical",
"Statutory",
"Cell",
"Weekly",
"End",
"Online",
"Beneficial",
"Aged",
"Tough",
"Eager",
"Ongoing",
"Silver",
"Persistent",
"Calm",
"Nearest",
"Hidden",
"Magic",
"Pretty",
"Wealthy",
"Exciting",
"Decisive",
"Confident",
"Invisible",
"Notable",
"Medium",
"Manual",
"Select",
"Thorough",
"Causal",
"Giant",
"Bigger",
"Pink",
"Improved",
"Immense",
"Hour",
"Intact",
"Grade",
"Dense",
"Hungry",
"Biggest",
"Abundant",
"Handsome",
"Retail",
"Insufficient",
"Irregular",
"Intrinsic",
"Residual",
"Follow",
"Fluid",
"Mysterious",
"Descriptive",
"Elastic",
"Destructive",
"Architectural",
"Synthetic",
"Continental",
"Evolutionary",
"Lucky",
"Bold",
"Funny",
"Peak",
"Smallest",
"Reluctant",
"Suspicious",
"Smart",
"Mighty",
"Brave",
"Humble",
"Vocal",
"Obscure",
"Innovative",
];