commit a40113661478296e0ae8e0d8dfde4aab2c2bf00b
parent 1fd6eefe7973a1295de01c4aa3500dd97b8494c8
Author: quantumish <freifeld.david@gmail.com>
Date: Thu, 8 Dec 2022 22:29:59 -0800
Ported.
Diffstat:
11 files changed, 1205 insertions(+), 1248 deletions(-)
diff --git a/README.md b/README.md
@@ -15,7 +15,12 @@ brew install cassandra-cpp-driver
```
> **Warning**
-> This is a little wonky on M1 Macs: you'll need to follow the advice of [this page](https://stackoverflow.com/questions/69486339/nativelibrarydarwin-java64-failed-to-link-the-c-library-against-jna-native-m) when you face the inevitable JNA link error. Download for more recent JNA version is [here](https://search.maven.org/artifact/net.java.dev.jna/jna/5.8.0/jar). I personally ran `sudo mv jna-5.8.0.jar /opt/homebrew/Cellar/cassandra/4.0.6/libexec/jna-5.6.0.jar` (I think...) once I downloaded the new version, and that fixed it.
+> This is a little wonky on M1 Macs: you'll need to follow the advice of
+> [this page](https://stackoverflow.com/questions/69486339/nativelibrarydarwin-java64-failed-to-link-the-c-library-against-jna-native-m)
+> when you face the inevitable JNA link error. Download for more recent JNA version is
+> [here](https://search.maven.org/artifact/net.java.dev.jna/jna/5.8.0/jar). I personally ran
+> `sudo mv jna-5.8.0.jar /opt/homebrew/Cellar/cassandra/4.0.6/libexec/jna-5.6.0.jar` (I
+> think...) once I downloaded the new version, and that fixed it.
You also need to install the Rust language:
@@ -25,40 +30,52 @@ curl --proto '=https' --tlsv1.2 -sSf https://sh.rustup.rs | sh
# Usage
Start by launching the database in the background with
+
```
cassandra -f
```
-Then, launch `chatterbox` - the service for sending/getting messages - and `scuttlebutt` - the service for everything else.
+
+Then, launch `chatterbox` - the service for sending/getting messages - and `scuttlebutt` -
+the service for everything else.
+
```
cargo run -p chatterbox &
cargo run -p scuttlebutt &
```
## Features
-Beyond basic text messaging, `blatherskite` has support for:
+Beyond basic text messaging, `blatherskite` has support for:
- Discord-esque servers
- Threads
- Direct messages
- Basic permissioning (owner/admin/none)
### Terminology
-Here's a quick guide to to the terms used by the service (that you might see in the `scuttlebutt` documentation):
+Here's a quick guide to to the terms used by the service (that you might see in the
+`scuttlebutt` documentation):
- Users can create or be invited to *groups* which contain *channels*.
- Groups have:
- *members*, the users who are part of the group
- an *owner*, who made the group and is permitted to do specific actions (like deleting it)
- *admin*, users who have elevated permissions for a group (like adding/removing channels)
-- *DMs* are a special kind of group that are made between users directly and limit certain functionality. DMs only have one channel and have no admin.
-- Channels also have *members* (which can be a subset of the group!). Channels by default are *public*, which means when a user is invited to a group they will be added to the channel. You can set them to *private* with another API call.
+- *DMs* are a special kind of group that are made between users directly and limit certain
+ functionality. DMs only have one channel and have no admin.
+- Channels also have *members* (which can be a subset of the group!). Channels by default
+ are *public*, which means when a user is invited to a group they will be added to the
+ channel. You can set them to *private* with another API call.
## Scuttlebutt
-Scuttlebutt is an HTTP service that handles the creation, deletion, and updating of groups/channels/users as well as misc other actions.
+Scuttlebutt is an HTTP service that handles the creation, deletion, and updating of
+groups/channels/users as well as misc other actions.
-The various methods and objects are documented at `localhost:3000`, and the basic usage flow is something like:
+The various methods and objects are documented at `localhost:3000`, and the basic usage
+flow is something like:
- `POST /api/user` to make a user, which will return a User object (see Schemas on the docs)
-- `GET /api/login` to login with said user. This will return a JWT that you'll use to authenticate future requests. This token will expire in a day!
-- Whatever requests you'd like at that point! Authenticate by including a `ScuttleKey` header with the token you got.
+- `GET /api/login` to login with said user. This will return a JWT that you'll use to
+ authenticate future requests. This token will expire in a day!
+- Whatever requests you'd like at that point! Authenticate by including a `ScuttleKey`
+ header with the token you got.
## Chatterbox
Chatterbox is a websocket service used for sending and receiving messages. To use:
diff --git a/scuttlebutt/Cargo.toml b/scuttlebutt/Cargo.toml
@@ -11,7 +11,7 @@ base64 = "0.13.0"
cassandra-cpp = "1.1.0"
chrono = { version = "0.4.22", features = ["serde"] }
ctor = "0.1.23"
-error-stack = "0.2.3"
+# error-stack = "0.1"
hex = "0.4.3"
hmac = "0.12.1"
jwt = "0.16.0"
@@ -28,3 +28,6 @@ sha2 = "0.10.6"
thiserror = "1.0.37"
tokio = { version = "1", features = ["full"] }
tracing-subscriber = "0.3.15"
+diesel = { version = "2.0.0", features = ["postgres"] }
+dotenvy = "0.15.6"
+html-escape = "0.2.12"
diff --git a/scuttlebutt/diesel.toml b/scuttlebutt/diesel.toml
@@ -0,0 +1,8 @@
+# For documentation on how to configure this file,
+# see https://diesel.rs/guides/configuring-diesel-cli
+
+[print_schema]
+file = "src/schema.rs"
+
+[migrations_directory]
+dir = "migrations"
diff --git a/scuttlebutt/migrations/2022-11-29-172636_test/down.sql b/scuttlebutt/migrations/2022-11-29-172636_test/down.sql
@@ -0,0 +1,6 @@
+DROP TABLE users;
+DROP TABLE groups;
+DROP TABLE channels;
+DROP TABLE user_groups;
+DROP TABLE user_dms;
+DROP TABLE messages;
diff --git a/scuttlebutt/migrations/2022-11-29-172636_test/up.sql b/scuttlebutt/migrations/2022-11-29-172636_test/up.sql
@@ -0,0 +1,42 @@
+CREATE TABLE users (
+ id BIGINT PRIMARY KEY,
+ name TEXT NOT NULL,
+ email TEXT NOT NULL,
+ hash TEXT NOT NULL
+);
+
+CREATE TABLE groups (
+ id BIGINT PRIMARY KEY,
+ name TEXT NOT NULL,
+ members BIGINT[] NOT NULL,
+ is_dm BOOLEAN NOT NULL,
+ channels BIGINT[] NOT NULL,
+ admin BIGINT[] NOT NULL,
+ owner BIGINT NOT NULL
+);
+
+CREATE TABLE channels (
+ id BIGINT PRIMARY KEY,
+ src_group BIGINT NOT NULL,
+ name TEXT NOT NULL,
+ members BIGINT[] NOT NULL,
+ private BOOLEAN NOT NULL
+);
+
+CREATE TABLE user_groups (
+ id BIGINT PRIMARY KEY,
+ groups BIGINT[] NOT NULL
+);
+
+CREATE TABLE user_dms (
+ id BIGINT PRIMARY KEY,
+ dms BIGINT[] NOT NULL
+);
+
+CREATE TABLE messages (
+ channel BIGINT PRIMARY KEY,
+ id BIGINT NOT NULL,
+ author BIGINT NOT NULL,
+ content TEXT
+ thread BIGINT
+);
diff --git a/scuttlebutt/src/db.rs b/scuttlebutt/src/db.rs
@@ -1,562 +0,0 @@
-use cassandra_cpp::{Value, SetIterator, Session, AsRustType, BindRustType, Result, Cluster, stmt};
-use crate::responses::*;
-
-#[derive(Debug)]
-pub enum IdType {
- User,
- Group,
- Channel,
- Message
-}
-
-/// Trait for the back-end database that contains all CRUD database operations.
-///
-/// **Every method (outside of `valid_id`) assumes that the IDs passed are valid.**
-///
-/// Meant to enable switching backends, but right now the Result type is hardcoded
-/// to a `cassandra_cpp::Result` due to other concerns (see [Issue #2](https://github.com/quantumish/blatherskite/issues/2) discussion on repo).
-pub trait Database: Sync + Send {
- fn valid_id(&self, kind: IdType, id: i64) -> Result<bool>;
-
- fn create_user(&self, id: i64, name: String, email: String, hash: String) -> Result<()>;
- fn update_user(&self, id: i64, name: String, email: String) -> Result<()>;
- fn get_user(&self, id: i64) -> Result<User>;
- fn get_user_hash(&self, id: i64) -> Result<String>;
- fn delete_user(&self, id: i64) -> Result<()>;
-
- fn create_group(&self, gid: i64, uid: i64, name: String, dm: bool) -> Result<()>;
- fn get_group(&self, id: i64) -> Result<Group>;
- fn update_group(&self, id: i64, name: String) -> Result<()>;
- fn delete_group(&self, id: i64) -> Result<()>;
-
- fn get_group_members(&self, gid: i64) -> Result<Vec<i64>>;
- fn add_group_member(&self, gid: i64, uid: i64) -> Result<()>;
- fn remove_group_member(&self, gid: i64, uid: i64) -> Result<()>;
-
- fn get_group_channels(&self, gid: i64) -> Result<Vec<i64>>;
- fn add_group_channel(&self, gid: i64, uid: i64) -> Result<()>;
- fn remove_group_channel(&self, gid: i64, uid: i64) -> Result<()>;
-
- fn get_group_admin(&self, gid: i64) -> Result<Vec<i64>>;
- fn add_group_admin(&self, gid: i64, uid: i64) -> Result<()>;
- fn remove_group_admin(&self, gid: i64, uid: i64) -> Result<()>;
-
- fn get_group_owner(&self, gid: i64) -> Result<i64>;
-
- fn is_group_dm(&self, gid: i64) -> Result<bool>;
-
- fn create_channel(&self, cid: i64, gid: i64, uid: i64, name: String) -> Result<()>;
- fn get_channel(&self, id: i64) -> Result<Channel>;
- fn update_channel(&self, id: i64, name: String) -> Result<()>;
- fn delete_channel(&self, id: i64) -> Result<()>;
-
- fn get_channel_members(&self, gid: i64) -> Result<Vec<i64>>;
- fn add_channel_member(&self,cid: i64, id: i64) -> Result<()>;
- fn remove_channel_member(&self, cid: i64, id: i64) -> Result<()>;
-
- fn is_channel_private(&self, id: i64) -> Result<bool>;
- fn set_channel_private(&self, id: i64, value: bool) -> Result<bool>;
-
- fn create_user_groups(&self, id: i64) -> Result<()>;
- fn get_user_groups(&self, id: i64) -> Result<Vec<i64>>;
- fn delete_user_groups(&self, id: i64) -> Result<()>;
- fn add_user_group(&self, uid: i64, gid: i64) -> Result<()>;
- fn remove_user_group(&self, uid: i64, gid: i64) -> Result<()>;
-
- fn create_user_dms(&self, id: i64) -> Result<()>;
- fn get_user_dms(&self, id: i64) -> Result<Vec<i64>>;
- fn delete_user_dms(&self, id: i64) -> Result<()>;
- fn add_user_dm(&self, uid: i64, gid: i64) -> Result<()>;
-
- fn get_message(&self, id: i64) -> Result<Message>;
- fn get_messages(&self, cid: i64, num: u64) -> Result<Vec<Message>>;
- fn delete_message(&self, id: i64) -> Result<()>;
- fn set_thread(&self, id: i64, cid: i64) -> Result<()>;
-}
-
-/// Cassandra backend struct
-pub struct Cassandra {
- kspc: String, // keyspace
- sess: Session
-}
-
-impl Cassandra {
- /// Initialize the database session and creates tables
- ///
- /// Arguments:
- /// - `keyspc`: the keyspace to use for all database queries.
- pub fn new(keyspc: &str) -> Self {
- let contact_points = "127.0.0.1"; // NOTE: generalize me
- let mut cluster = Cluster::default();
- cluster.set_contact_points(contact_points).unwrap();
- cluster.set_load_balance_round_robin();
- let session = cluster.connect().unwrap();
-
- session.execute(&stmt!(&format!(
- "CREATE KEYSPACE IF NOT EXISTS {keyspc} \
- WITH replication = {{'class':'SimpleStrategy', 'replication_factor': 1}}"
- ))).wait().unwrap();
-
- session.execute(&stmt!(&format!(
- "CREATE TABLE IF NOT EXISTS {keyspc}.users \
- (id bigint PRIMARY KEY, name text, email text, hash text);"
- ))).wait().unwrap();
-
- session.execute(&stmt!(&format!(
- "CREATE TABLE IF NOT EXISTS {keyspc}.groups \
- (id bigint PRIMARY KEY, name text, members set<bigint>, is_dm boolean, \
- channels set<bigint>, admin set<bigint>, owner bigint);"
- ))).wait().unwrap();
-
- session.execute(&stmt!(&format!(
- "CREATE TABLE IF NOT EXISTS {keyspc}.channels \
- (id bigint PRIMARY KEY, group bigint, name text, \
- members set<bigint>, private boolean);"
- ))).wait().unwrap();
-
- session.execute(&stmt!(&format!(
- "CREATE TABLE IF NOT EXISTS {keyspc}.user_groups \
- (id bigint PRIMARY KEY, groups set<bigint>);"
- ))).wait().unwrap();
-
- session.execute(&stmt!(&format!(
- "CREATE TABLE IF NOT EXISTS {keyspc}.user_dms \
- (id bigint PRIMARY KEY, dms set<bigint>);"
- ))).wait().unwrap();
-
- session.execute(&stmt!(&format!(
- "CREATE TABLE IF NOT EXISTS {keyspc}.messages \
- (channel bigint, id bigint, author bigint, \
- content text, group bigint, thread bigint, \
- PRIMARY KEY (channel, id)) \
- WITH CLUSTERING ORDER BY (id DESC);"
- ))).wait().unwrap();
-
- Self {
- kspc: keyspc.to_string(),
- sess: session
- }
- }
-
- /// Delete a row from the database.
- ///
- /// Arguments:
- /// - `table`: the table to delete the row from
- /// - `id`: the id of the row to delete
- fn delete_row(&self, table: &str, id: i64) -> Result<()> {
- self.sess.execute(&stmt!(&format!(
- "DELETE FROM {}.{table} WHERE id={id};", self.kspc
- ))).wait().unwrap();
- Ok(())
- }
-
- /// Extract a set from a database row
- ///
- /// Arguments:
- /// - `table`: the table with the desired row
- /// - `set`: the name of the column with the set in it
- /// - `id`: the id of the row to get the set from
- fn get_set(&self, table: &str, set: &str, id: i64) -> Result<Vec<i64>> {
- let res = self.sess.execute(&stmt!(&format!(
- "SELECT {set} FROM {}.{table} WHERE id = {id};", self.kspc
- ))).wait()?;
- let row = res.first_row().unwrap();
- let set: Value = row.get_column(0)?;
- Ok(match set.is_null() {
- true => Vec::new(),
- false => set.get_set()?.map(|i| i.get_i64().unwrap()).collect()
- })
- }
-
- /// Remove an element from a set in a database row
- ///
- /// Arguments:
- /// - `table`: the table with the desired row
- /// - `set`: the name of the column with the set in it
- /// - `id`: the id of the row to get the set from
- /// - `elem`: the element to remove from the set
- fn pop_set(&self, table: &str, set: &str, id: i64, elem: i64) -> Result<()> {
- self.sess.execute(&stmt!(&format!(
- "UPDATE {}.{table} SET {set} = {set} - {{{elem}}} WHERE ID={id};", self.kspc
- ))).wait()?;
- Ok(())
- }
-
- /// Add an element to a set in a database row
- ///
- /// Arguments:
- /// - `table`: the table with the desired row
- /// - `set`: the name of the column with the set in it
- /// - `id`: the id of the row to get the set from
- /// - `elem`: the element to add to the set
- fn push_set(&self, table: &str, set: &str, id: i64, elem: i64) -> Result<()> {
- self.sess.execute(&stmt!(&format!(
- "UPDATE {}.{table} SET {set} = {set} + {{{elem}}} WHERE ID={id};", self.kspc
- ))).wait()?;
- Ok(())
- }
-}
-
-impl Database for Cassandra {
- fn valid_id(&self, kind: IdType, id: i64) -> Result<bool> {
- let table = match kind {
- IdType::User => "users",
- IdType::Group => "groups",
- IdType::Channel => "channels",
- IdType::Message => "messages",
- };
- let res = self.sess.execute(&stmt!(&format!(
- "SELECT * FROM {}.{table} WHERE ID={id};", self.kspc
- ))).wait()?;
- if let Some(_row) = res.first_row() {
- return Ok(true)
- } else { return Ok(false) };
- }
-
- fn create_user(&self, id: i64, name: String, email: String, hash: String) -> Result<()> {
- let mut stmt = stmt!(&format!(
- "INSERT INTO {}.users (id, name, email, hash) VALUES ({id}, ?, ?, ?);", self.kspc
- ));
- stmt.bind(0, name.as_str())?;
- stmt.bind(1, email.as_str())?;
- stmt.bind(2, hash.as_str())?;
- self.sess.execute(&stmt).wait()?;
- Ok(())
- }
-
- fn get_user(&self, id: i64) -> Result<User> {
- let res = self.sess.execute(&stmt!(&format!(
- "SELECT name, email FROM {}.users WHERE ID={id};", self.kspc
- ))).wait()?;
- let row = res.first_row().unwrap();
- Ok(User {
- id,
- username: row.get(0)?,
- email: row.get(1)?
- })
- }
-
- fn get_user_hash(&self, id: i64) -> Result<String> {
- let res = self.sess.execute(&stmt!(&format!(
- "SELECT hash FROM {}.users WHERE ID={id};", self.kspc
- ))).wait()?;
- let row = res.first_row().unwrap();
- Ok(row.get(0)?)
- }
-
- fn update_user(&self, id: i64, name: String, email: String) -> Result<()> {
- let mut stmt = stmt!(&format!(
- "UPDATE {}.users SET name=?, email=? WHERE ID={id};", self.kspc
- ));
- stmt.bind(0, name.as_str())?;
- stmt.bind(1, email.as_str())?;
- self.sess.execute(&stmt).wait()?;
- Ok(())
- }
-
- fn delete_user(&self, id: i64) -> Result<()> {
- self.delete_row("users", id)
- }
-
- fn get_group(&self, id: i64) -> Result<Group> {
- let res = self.sess.execute(&stmt!(&format!(
- "SELECT name, members, channels, owner, is_dm FROM {}.groups WHERE ID={id};", self.kspc
- ))).wait()?;
- let row = res.first_row().unwrap();
- let members: SetIterator = row.get(1)?;
- let channels: SetIterator = row.get(2)?;
- Ok(Group {
- id,
- name: row.get(0)?,
- members: members.map(|i| i.get_i64().unwrap()).collect(),
- channels: channels.map(|i| i.get_i64().unwrap()).collect(),
- admin: self.get_set("groups", "admin", id)?, // HACK
- owner: row.get(3)?,
- is_dm: row.get(4)?,
- })
- }
-
- fn create_group(&self, gid: i64, uid: i64, name: String, dm: bool) -> Result<()> {
- let mut stmt = stmt!(&format!(
- "INSERT INTO {}.groups (id, name, channels, \
- members, is_dm, owner) VALUES ({gid}, ?, {{}}, {{{uid}}}, {dm}, {uid});", self.kspc
- ));
- stmt.bind(0, name.as_str())?;
- self.sess.execute(&stmt).wait()?;
- Ok(())
- }
-
- fn delete_group(&self, id: i64) -> Result<()> {
- self.delete_row("groups", id)
- }
-
- fn update_group(&self, id: i64, name: String) -> Result<()> {
- let mut stmt = stmt!(&format!(
- "UPDATE {}.groups SET name = ? WHERE id = {id};", self.kspc
- ));
- stmt.bind(0, name.as_str())?;
- self.sess.execute(&stmt).wait()?;
- Ok(())
- }
-
- fn get_group_members(&self, gid: i64) -> Result<Vec<i64>> {
- self.get_set("groups", "members", gid)
- }
-
- fn add_group_member(&self, gid: i64, uid: i64) -> Result<()> {
- self.push_set("groups", "members", gid, uid)
- }
-
- fn remove_group_member(&self, gid: i64, uid: i64) -> Result<()> {
- self.pop_set("groups", "members", gid, uid)
- }
-
- fn get_group_channels(&self, gid: i64) -> Result<Vec<i64>> {
- self.get_set("groups", "channels", gid)
- }
-
- fn add_group_channel(&self, gid: i64, cid: i64) -> Result<()> {
- self.push_set("groups", "channels", gid, cid)
- }
-
- fn remove_group_channel(&self, gid: i64, cid: i64) -> Result<()> {
- self.pop_set("groups", "channels", gid, cid)
- }
-
- fn get_group_admin(&self, gid: i64) -> Result<Vec<i64>> {
- self.get_set("groups", "admin", gid)
- }
-
- fn add_group_admin(&self, gid: i64, uid: i64) -> Result<()> {
- self.push_set("groups", "admin", gid, uid)
- }
-
-
- fn remove_group_admin(&self, gid: i64, uid: i64) -> Result<()> {
- self.pop_set("groups", "admin", gid, uid)
- }
-
- fn get_group_owner(&self, gid: i64) -> Result<i64> {
- let res = self.sess.execute(&stmt!(&format!(
- "SELECT owner FROM {}.groups WHERE id={gid};", self.kspc
- ))).wait()?;
- let row = res.first_row().unwrap();
- Ok(row.get(0)?)
- }
-
- fn is_group_dm(&self, gid: i64) -> Result<bool> {
- let res = self.sess.execute(&stmt!(&format!(
- "SELECT is_dm FROM {}.groups WHERE id={gid};", self.kspc
- ))).wait()?;
- let row = res.first_row().unwrap();
- Ok(row.get(0)?)
- }
-
- fn get_channel(&self, id: i64) -> Result<Channel> {
- let res = self.sess.execute(&stmt!(&format!(
- "SELECT group, name, members, private FROM {}.channels WHERE ID={id};", self.kspc
- ))).wait()?;
- let row = res.first_row().unwrap();
- let members: SetIterator = row.get(2)?;
- Ok(Channel {
- id,
- group: row.get(0)?,
- name: row.get(1)?,
- members: members.map(|i| i.get_i64().unwrap()).collect(),
- private: row.get(3)?
- })
- }
-
- fn create_channel(&self, cid: i64, gid: i64, uid: i64, name: String) -> Result<()> {
- let mut stmt = stmt!(&format!(
- "INSERT INTO {}.channels (id, group, name, members, private) VALUES ({cid}, {gid}, ?, {{{uid}}}, false);", self.kspc
- ));
- stmt.bind(0, name.as_str())?;
- self.sess.execute(&stmt).wait()?;
- Ok(())
- }
-
- fn delete_channel(&self, id: i64) -> Result<()> {
- self.delete_row("channels", id)
- }
-
- fn update_channel(&self, id: i64, name: String) -> Result<()> {
- let mut stmt = stmt!(&format!(
- "UPDATE {}.channels SET name = ? WHERE id = {id};", self.kspc
- ));
- stmt.bind(0, name.as_str())?;
- self.sess.execute(&stmt).wait()?;
- Ok(())
- }
-
- fn get_channel_members(&self, cid: i64) -> Result<Vec<i64>> {
- self.get_set("channels", "members", cid)
- }
-
- fn add_channel_member(&self, gid: i64, uid: i64) -> Result<()> {
- self.push_set("channels", "members", gid, uid)
- }
-
- fn remove_channel_member(&self, gid: i64, uid: i64) -> Result<()> {
- self.pop_set("channels", "members", gid, uid)
- }
-
- fn is_channel_private(&self, id: i64) -> Result<bool> {
- let res = self.sess.execute(&stmt!(&format!(
- "SELECT private FROM {}.channels WHERE id={id};", self.kspc
- ))).wait()?;
- let row = res.first_row().unwrap();
- Ok(row.get(0)?)
- }
-
- fn set_channel_private(&self, id: i64, value: bool) -> Result<bool> {
- let res = self.sess.execute(&stmt!(&format!(
- "UPDATE {}.channels SET private = {value} WHERE id={id};", self.kspc
- ))).wait()?;
- let row = res.first_row().unwrap();
- Ok(row.get(0)?)
- }
-
- fn create_user_dms(&self, id: i64) -> Result<()> {
- self.sess.execute(&stmt!(&format!(
- "INSERT INTO {}.user_dms (id, dms) VALUES ({id}, {{}});", self.kspc
- ))).wait()?;
- Ok(())
- }
-
- fn get_user_dms(&self, id: i64) -> Result<Vec<i64>> {
- self.get_set("user_dms", "dms", id)
- }
-
- fn add_user_dm(&self, uid: i64, gid: i64) -> Result<()> {
- self.push_set("user_dms", "dms", uid, gid)
- }
-
- fn delete_user_dms(&self, id: i64) -> Result<()> {
- self.delete_row("user_dms", id)
- }
-
- fn create_user_groups(&self, id: i64) -> Result<()> {
- self.sess.execute(&stmt!(&format!(
- "INSERT INTO {}.user_groups (id, groups) VALUES ({id}, {{}});", self.kspc
- ))).wait()?;
- Ok(())
- }
-
- fn get_user_groups(&self, id: i64) -> Result<Vec<i64>> {
- self.get_set("user_groups", "groups", id)
- }
-
- fn add_user_group(&self, uid: i64, gid: i64) -> Result<()> {
- self.push_set("user_groups", "groups", uid, gid)
- }
-
- fn remove_user_group(&self, uid: i64, gid: i64) -> Result<()> {
- self.pop_set("user_groups", "groups", uid, gid)
- }
-
- fn delete_user_groups(&self, id: i64) -> Result<()> {
- self.delete_row("user_groups", id)
- }
-
- // TODO the unwraps here are not great
- fn get_messages(&self, cid: i64, num: u64) -> Result<Vec<Message>> {
- let res = self.sess.execute(&stmt!(&format!(
- "SELECT * FROM {}.messages WHERE channel={cid} LIMIT {num};", self.kspc
- ))).wait()?;
- Ok(res.iter().map(|row| {
- let maybe_thread: Value = row.get_column(5).unwrap();
- Message {
- id: row.get(0).unwrap(),
- author: row.get(2).unwrap(),
- channel: row.get(1).unwrap(),
- content: row.get(4).unwrap(),
- thread: match maybe_thread.is_null() {
- true => None,
- false => Some(maybe_thread.get_i64().unwrap())
- }
- }
- }).collect::<Vec<Message>>())
- }
-
- fn get_message(&self, id: i64) -> Result<Message> {
- let res = self.sess.execute(&stmt!(&format!(
- "SELECT channel, author, content, thread FROM {}.messages WHERE ID={id};", self.kspc
- ))).wait()?;
- let row = res.first_row().unwrap();
- let thread: Value = row.get_column(3)?;
- Ok(Message {
- id,
- channel: row.get(0)?,
- author: row.get(1)?,
- content: row.get(2)?,
- thread: match thread.is_null() {
- true => None,
- false => Some(thread.get_i64().unwrap())
- }
- })
- }
-
- fn delete_message(&self, id: i64) -> Result<()> {
- self.delete_row("messages", id)
- }
-
- fn set_thread(&self, id: i64, cid: i64) -> Result<()> {
- self.sess.execute(&stmt!(&format!(
- "UPDATE {}.messages SET thread = {cid} WHERE id = {id};", self.kspc
- ))).wait()?;
- Ok(())
- }
-}
-
-#[cfg(test)]
-pub mod tests {
- use pretty_assertions::assert_eq;
-
- use super::*;
-
- #[test]
- fn test_delete_row() {
- let db = Cassandra::new("test");
- db.sess.execute(&stmt!(
- "INSERT INTO test.users (id, name, email, hash) VALUES (11, 'fred', '', '');"
- )).wait().unwrap();
- db.delete_row("users", 11).unwrap();
- let res = db.sess.execute(&stmt!(
- "SELECT * FROM test.users WHERE id=11;"
- )).wait().unwrap();
- assert_eq!(res.row_count(), 0);
- }
-
- #[test]
- fn test_get_set() {
- let db = Cassandra::new("test");
- db.sess.execute(&stmt!(
- "INSERT INTO test.user_groups (id, groups) VALUES (12, {1,2,3});"
- )).wait().unwrap();
- assert_eq!(db.get_set("user_groups", "groups", 12).unwrap(), vec![1,2,3]);
- db.delete_row("user_groups", 12).unwrap();
- }
-
- #[test]
- fn test_push_set() {
- let db = Cassandra::new("test");
- db.sess.execute(&stmt!(
- "INSERT INTO test.user_groups (id, groups) VALUES (13, {1,2,3});"
- )).wait().unwrap();
- db.push_set("user_groups", "groups", 13, 4).unwrap();
- assert_eq!(db.get_set("user_groups", "groups", 13).unwrap(), vec![1,2,3,4]);
- db.delete_row("user_groups", 13).unwrap();
- }
-
- #[test]
- fn test_pop_set() {
- let db = Cassandra::new("test");
- db.sess.execute(&stmt!(
- "INSERT INTO test.user_groups (id, groups) VALUES (14, {1,2,3});"
- )).wait().unwrap();
- db.pop_set("user_groups", "groups", 14, 3).unwrap();
- assert_eq!(db.get_set("user_groups", "groups", 14).unwrap(), vec![1,2]);
- db.delete_row("user_groups", 14).unwrap();
- }
-}
diff --git a/scuttlebutt/src/error.rs b/scuttlebutt/src/error.rs
@@ -0,0 +1,186 @@
+use poem::http::StatusCode;
+use diesel::result::{QueryResult, Error};
+
+#[derive(Debug, Clone)]
+pub struct UserFacingPropagatedError {
+ code: StatusCode,
+ backtrace: Option<String>,
+ error_kind: String,
+ error: String,
+ context: Option<String>,
+}
+
+impl std::fmt::Display for UserFacingPropagatedError {
+ fn fmt(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result {
+ writeln!(f, "<h1>{}</h1>", self.code)?;
+ writeln!(f, "Received <code>{}: {}</code>", self.error_kind, self.error)?;
+ if let Some(ctx) = &self.context {
+ writeln!(f, " while {}.", ctx)?;
+ }
+ if let Some(trace) = &self.backtrace {
+ writeln!(f, "<pre>{}</pre>", html_escape::encode_text(&trace))?;
+ }
+ Ok(())
+ }
+}
+
+impl std::error::Error for UserFacingPropagatedError {}
+
+#[derive(Debug, Clone)]
+pub struct UserFacingError {
+ code: StatusCode,
+ reason: Option<String>,
+}
+
+impl UserFacingError {
+ pub fn new(code: StatusCode, reason: &str) -> Self {
+ Self {
+ code,
+ reason: Some(String::from(reason))
+ }
+ }
+ pub fn terse(code: StatusCode) -> Self {
+ Self {
+ code,
+ reason: None,
+ }
+ }
+}
+
+impl Into<poem::Error> for UserFacingError {
+ fn into(self) -> poem::Error {
+ let code = self.code;
+ poem::Error::new(self, code)
+ }
+}
+
+impl std::fmt::Display for UserFacingError {
+ fn fmt(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result {
+ write!(f, "<h1>{}</h1>", self.code)?;
+ if let Some(reason) = &self.reason {
+ write!(f, "{}", reason)?;
+ }
+ writeln!(f, "")
+ }
+}
+
+impl std::error::Error for UserFacingError {}
+
+#[derive(Debug)]
+pub struct WithBacktrace<E>
+where E: std::error::Error
+{
+ backtrace: std::backtrace::Backtrace,
+ pub err: E,
+ pub context: Option<String>,
+}
+
+impl<E> std::fmt::Display for WithBacktrace<E>
+ where E: std::error::Error
+{
+ fn fmt(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result {
+ write!(f, "{}", self.err)
+ }
+}
+
+impl<E> std::error::Error for WithBacktrace<E>
+where E: std::error::Error {}
+
+pub trait WithBacktraceExt<E> where E: std::error::Error {
+ type Success;
+ fn with_backtrace(self) -> Result<Self::Success, WithBacktrace<E>>;
+}
+
+impl<T,E> WithBacktraceExt<E> for Result<T, E>
+where E: std::error::Error
+{
+ type Success = T;
+ fn with_backtrace(self) -> Result<Self::Success, WithBacktrace<E>> {
+ self.map_err(|err| WithBacktrace {
+ err,
+ backtrace: std::backtrace::Backtrace::capture(),
+ context: None
+ })
+ }
+}
+
+pub trait InternalResultExt {
+ type Success;
+ fn poemify(self, ctx: &str) -> poem::Result<Self::Success>;
+}
+
+
+#[derive(Debug)]
+pub struct StdAnyhowError(pub anyhow::Error);
+
+impl std::fmt::Display for StdAnyhowError {
+ fn fmt(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result {
+ std::fmt::Display::fmt(&self.0, f)
+ }
+}
+
+impl std::error::Error for StdAnyhowError {
+ fn source(&self) -> Option<&(dyn std::error::Error + 'static)> {
+ Some(self.0.as_ref())
+ }
+}
+
+impl From<anyhow::Error> for StdAnyhowError {
+ fn from(err: anyhow::Error) -> Self {
+ Self(err)
+ }
+}
+
+impl<T> InternalResultExt for QueryResult<T> {
+ type Success = T;
+
+ fn poemify(self, ctx: &str) -> poem::Result<Self::Success> {
+ self.map_err(|err| {
+ log::error!("{:#?}", err);
+ let code = match err {
+ diesel::result::Error::NotFound => StatusCode::NOT_FOUND,
+ _ => StatusCode::INTERNAL_SERVER_ERROR,
+ };
+ poem::error::Error::new(UserFacingPropagatedError {
+ code,
+ backtrace: None,
+ context: Some(String::from(ctx)),
+ error_kind: format!("{:?}", err),
+ error: err.to_string(),
+ }, code)
+ })
+ }
+}
+
+
+impl<T> InternalResultExt for anyhow::Result<T> {
+ type Success = T;
+
+ fn poemify(self, ctx: &str) -> poem::Result<Self::Success> {
+ self.map_err(|err| {
+ log::error!("{:#?}", err);
+ poem::error::InternalServerError(StdAnyhowError::from(err))
+ })
+ }
+
+}
+
+impl<T, E> InternalResultExt for std::result::Result<T, WithBacktrace<E>>
+ where E: std::error::Error
+{
+ type Success = T;
+
+ fn poemify(self, ctx: &str) -> poem::Result<Self::Success> {
+ self.map_err(|err| {
+ log::error!("{:#?}", err.err);
+ poem::error::InternalServerError( UserFacingPropagatedError {
+ code: StatusCode::INTERNAL_SERVER_ERROR,
+ backtrace: Some(err.backtrace.to_string()),
+ context: Some(String::from(ctx)),
+ error_kind: String::from(std::any::type_name::<E>()),
+ error: err.to_string(),
+ })
+ })
+ }
+
+}
diff --git a/scuttlebutt/src/main.rs b/scuttlebutt/src/main.rs
@@ -1,15 +1,17 @@
+#![allow(warnings, unused)]
+
use chrono::{DateTime, Duration, Local, Utc};
use hmac::Hmac;
use jwt::{SignWithKey, VerifyWithKey};
use poem::{
- listener::TcpListener, web::Data, EndpointExt, Request, Result,
- Route, Server,
+ listener::TcpListener, web::Data, EndpointExt, Request, Result,
+ Route, Server, http::StatusCode,
};
use poem_openapi::{
- auth::ApiKey,
- param::Query,
- payload::{Json, PlainText},
- *,
+ auth::ApiKey,
+ param::Query,
+ payload::{Json, PlainText},
+ *,
};
use std::sync::Mutex;
use rand::{distributions::Alphanumeric, Rng};
@@ -17,11 +19,17 @@ use rustflake::Snowflake;
use serde::{Deserialize, Serialize};
use sha2::Sha256;
-pub mod responses;
-pub use responses::*;
+use diesel::pg::PgConnection;
+use diesel::prelude::*;
+use dotenvy::dotenv;
+use std::env;
-pub mod db;
-pub use db::*;
+pub mod models;
+use models::*;
+pub mod schema;
+use schema::*;
+pub mod error;
+use error::*;
type ServerKey = Hmac<Sha256>;
@@ -29,703 +37,809 @@ type ServerKey = Hmac<Sha256>;
/// The serialized form of this struct forms the content portion of the JWT returned by /login
#[derive(Serialize, Deserialize)]
struct Claims {
- id: i64,
- exp: DateTime<Local>,
+ id: i64,
+ exp: DateTime<Local>,
}
/// API key authorization scheme
#[derive(SecurityScheme)]
#[oai(
- type = "api_key",
- key_name = "Authorization", // header to look for API key in
- in = "header",
- checker = "api_checker" // hook to run when checking authorization
+ type = "api_key",
+ key_name = "Authorization", // header to look for API key in
+ in = "header",
+ checker = "api_checker" // hook to run when checking authorization
)]
struct Authorization(Claims);
+fn append_array(conn: &mut PgConnection, table: &str, field: &str, id: i64, val: i64) -> Result<usize, WithBacktrace<diesel::result::Error>> {
+ diesel::sql_query(format!(
+ "UPDATE {table} SET {field} = array_append({field},$1) WHERE id = $2;"
+ ))
+ .bind::<diesel::sql_types::BigInt, _>(val)
+ .bind::<diesel::sql_types::BigInt, _>(id)
+ .execute(conn).with_backtrace()
+}
+
+
+fn remove_array(conn: &mut PgConnection, table: &str, field: &str, id: i64, val: i64) -> Result<usize, WithBacktrace<diesel::result::Error>> {
+ diesel::sql_query(format!(
+ "UPDATE {table} SET {field} = array_remove({field},$1) WHERE id = $2;"
+ ))
+ .bind::<diesel::sql_types::BigInt, _>(val)
+ .bind::<diesel::sql_types::BigInt, _>(id)
+ .execute(conn).with_backtrace()
+}
+
+
/// Check if a user has supplied a valid authorization token.
///
/// Returns None if the token was invalid or if it fails to parse the given token
/// (which will then be handled by Poem to throw a 401), otherwise returns the
/// Claims struct.
async fn api_checker(req: &Request, api_key: ApiKey) -> Option<Claims> {
- let encoded_claims_str = match api_key.key.split(".").nth(1) {
- None => return None,
- Some(s) => s,
- };
- let claims_str = match base64::decode(encoded_claims_str) {
- Err(_) => return None,
- Ok(s) => s,
- };
- let claims: Claims = match serde_json::from_str(&String::from_utf8(claims_str).unwrap()) {
- Err(_) => return None,
- Ok(c) => c
- };
- if claims.exp < Local::now() {
- return None;
- }
- let server_key = req.data::<ServerKey>().unwrap(); // get server secret
- VerifyWithKey::<Claims>::verify_with_key(api_key.key.as_str(), server_key).ok()
+ let encoded_claims_str = match api_key.key.split(".").nth(1) {
+ None => return None,
+ Some(s) => s,
+ };
+ let claims_str = match base64::decode(encoded_claims_str) {
+ Err(_) => return None,
+ Ok(s) => s,
+ };
+ let claims: Claims = match serde_json::from_str(&String::from_utf8(claims_str).unwrap()) {
+ Err(_) => return None,
+ Ok(c) => c
+ };
+ if claims.exp < Local::now() {
+ return None;
+ }
+ let server_key = req.data::<ServerKey>().unwrap(); // get server secret
+ VerifyWithKey::<Claims>::verify_with_key(api_key.key.as_str(), server_key).ok()
}
/// Wrapper struct for the API functions
-struct Api {
- // The backend.
- db: Box<dyn Database>,
-}
+struct Api {}
/// Generates a unique i64 for ID generation
// FIXME: Very bad performance - acts as a chokehold for parallelism since
// every request that sends a message / makes a channel / etc. has to contest
// a global mutex.
pub fn gen_id() -> i64 {
- static STATE: Mutex<Option<Snowflake>> = Mutex::new(None);
+ static STATE: Mutex<Option<Snowflake>> = Mutex::new(None);
- STATE
- .lock()
- .unwrap()
- .get_or_insert_with(|| Snowflake::default())
- .generate()
+ STATE
+ .lock()
+ .unwrap()
+ .get_or_insert_with(|| Snowflake::default())
+ .generate()
}
pub fn check_name(name: String) -> String{
- let name_chars = name.chars();
- let fixed_name_chars = name_chars.filter(|i| !i.is_whitespace());
- let mut fixed_name: String = "".to_string();
- for i in fixed_name_chars{
- fixed_name.push(i);
- };
- assert!(fixed_name != "".to_string(), "name is empty or contains only illegal characters");
- return fixed_name;
+ let name_chars = name.chars();
+ let fixed_name_chars = name_chars.filter(|i| !i.is_whitespace());
+ let mut fixed_name: String = "".to_string();
+ for i in fixed_name_chars{
+ fixed_name.push(i);
+ };
+ // assert!(fixed_name != "".to_string(), "name is empty or contains only illegal characters");
+ return fixed_name;
+}
+
+pub fn open_db_conn() -> PgConnection {
+ let database_url = env::var("DATABASE_URL").expect("DATABASE_URL must be set");
+ PgConnection::establish(&database_url)
+ .unwrap_or_else(|_| panic!("Error connecting to {}", database_url))
}
#[OpenApi]
#[allow(unused_variables)]
impl Api {
- fn new(db: Box<dyn Database>) -> Api {
- Api { db }
- }
-
- fn __remove_group_member(&self, gid: i64, uid: i64) {
- self.db.remove_group_member(gid, uid).unwrap();
- let channels = self.db.get_group_channels(gid).unwrap();
- for channel in channels {
- self.db.remove_channel_member(channel, uid).unwrap();
- }
- self.db.remove_user_group(uid, gid).unwrap();
- }
-
- #[oai(path = "/login", method = "post")]
- /// Log in as a user. Returns an authentication token given id and hash.
- ///
- /// Expects hash of user's password to be given in the request body.
- /// Checks validity of hash, then signs JWT with a server secret key.
- async fn login(&self, key: Data<&ServerKey>, id: Query<i64>, hash: PlainText<String>) -> LoginResponse {
- use LoginResponse::*;
- if hash.0.len() != 64 {
- return BadRequest;
- } else if !self.db.valid_id(IdType::User, id.0).unwrap() {
- return NotFound;
- }
- let db_hash = self.db.get_user_hash(id.0).unwrap();
- if hex::decode(db_hash.clone()).unwrap() != hex::decode(hash.0.clone()).unwrap() {
-
- Unauthorized
- } else {
- let token = Claims {
- id: id.0,
- exp: Local::now() + Duration::days(1),
- }
- .sign_with_key(key.0);
- Success(PlainText(token.unwrap()))
- }
- }
-
- #[oai(path = "/user", method = "get")]
- /// Get the user with the given ID
- ///
- /// Does not require any authorization.
- async fn get_user(&self, id: Query<i64>) -> UserResponse {
- use UserResponse::*;
- if !self.db.valid_id(IdType::User, id.0).unwrap() { return NotFound; }
- match self.db.get_user(id.0) {
- Ok(user) => Success(Json(user)),
- Err(e) => InternalError(PlainText(e.to_string()))
- }
- }
-
- #[oai(path = "/user", method = "post")]
- /// Create a new user.
- ///
- /// Expects hash of user's password to be given in the request body.
- /// Does not require any authorization.
- async fn make_user(&self, name: Query<String>, email: Query<String>, hash: PlainText<String>) -> CreateUserResponse {
- use CreateUserResponse::*;
- if hash.0.len() != 64 {
- return BadRequest(PlainText("Invalid hash provided.".to_string()));
- }
- let checked_name = check_name(name.0.clone());
- let id = gen_id();
- self.db.create_user(id, checked_name.clone(), email.0.clone(), hash.0).unwrap();
- self.db.create_user_groups(id).unwrap();
- self.db.create_user_dms(id).unwrap();
- Success(Json(User {
- id,
- username: name.0,
- email: email.0,
- }))
- }
-
- #[oai(path = "/user", method = "put")]
- /// Update your name and email.
- async fn update_user(&self, auth: Authorization, name: Query<String>, email: Query<String>) -> GenericResponse {
- use GenericResponse::*;
- let checked_name = check_name(name.0.clone());
- self.db.update_user(auth.0.id, checked_name, email.0).unwrap();
- Success
- }
-
- #[oai(path = "/user", method = "delete")]
- /// Delete your user.
- ///
- /// Has the side effects of removing your user from every group, channel, or DM
- /// it is a member of.
- async fn delete_user(&self, auth: Authorization) -> DeleteResponse {
- use DeleteResponse::*;
- self.db.delete_user(auth.0.id).unwrap();
- for group in self.db.get_user_groups(auth.0.id).unwrap() {
- self.__remove_group_member(group, auth.0.id);
- }
- for dm in self.db.get_user_dms(auth.0.id).unwrap() {
- self.__remove_group_member(dm, auth.0.id);
- }
- self.db.delete_user_groups(auth.0.id).unwrap();
- Success
- }
-
- #[oai(path = "/user/groups", method = "get")]
- /// Get all groups accessible to you.
- async fn get_groups(&self, auth: Authorization) -> GroupsResponse {
- use GroupsResponse::*;
- let groups = self.db.get_user_groups(auth.0.id).unwrap();
- let group_vec = groups.iter().map(|i| {
- self.db.get_group(*i).unwrap()
- }).collect();
- Success(Json(group_vec))
- }
-
- #[oai(path = "/user/dms", method = "get")]
- /// Get all DMs accessible to you.
- async fn get_dms(&self, auth: Authorization) -> GroupsResponse {
- use GroupsResponse::*;
- let groups = self.db.get_user_dms(auth.0.id).unwrap();
- let group_vec = groups.iter().map(|i| {
- self.db.get_group(*i).unwrap()
- }).collect();
- Success(Json(group_vec))
- }
-
-
- #[oai(path = "/user/groups", method = "delete")]
- /// Leave a group accessible to you
- async fn leave_group(&self, auth: Authorization, gid: Query<i64>) -> GenericResponse {
- use GenericResponse::*;
- if !self.db.valid_id(IdType::Group, gid.0).unwrap() {
- return NotFound(PlainText("Group not found".to_string()));
- }
- self.__remove_group_member(gid.0, auth.0.id);
- Success
- }
-
- #[oai(path = "/group", method = "get")]
- /// Gets the group with the given ID
- async fn get_group(&self, auth: Authorization, id: Query<i64>) -> GroupResponse {
- use GroupResponse::*;
- if !self.db.valid_id(IdType::Group, id.0).unwrap() ||
- !self.db.get_group_members(id.0).unwrap().contains(&auth.0.id)
- {
- return NotFound;
- }
- Success(Json(self.db.get_group(id.0).unwrap()))
- }
-
- #[oai(path = "/group", method = "post")]
- /// Create a new group.
- ///
- /// The group created...
- /// - will have a default public "main" channel
- /// - will have your user as the owner
- /// - will have your user as an admin
- async fn make_group(&self, auth: Authorization, name: Query<String>) -> CreateGroupResponse {
- use CreateGroupResponse::*;
- let gid = gen_id();
- if name.0 == "" {
- return BadRequest(PlainText("Empty string not allowed for name".to_string()))
- }
- let fixed_name = check_name(name.0.clone());
- self.db.create_group(gid, auth.0.id, fixed_name.clone(), false).unwrap();
- self.db.add_user_group(auth.0.id, gid).unwrap();
- self.db.add_group_admin(gid, auth.0.id).unwrap();
- let cid = gen_id();
- self.db.create_channel(cid, gid, auth.0.id, String::from("main")).unwrap();
- self.db.add_group_channel(gid, cid).unwrap();
- Success(Json(Group {
- id: gid,
- name: name.0,
- members: vec![auth.0.id],
- channels: vec![cid],
- admin: vec![auth.0.id],
- owner: auth.0.id,
- is_dm: false
- }))
- }
-
- #[oai(path = "/dm", method = "post")]
- /// Create a new DM with a user `uid`.
- ///
- /// The group created...
- /// - will have the `is_dm` attribute set to true.
- /// - will have only one channel "main" with you and `uid`
- /// - will have no owner or admins
- async fn make_dm(&self, auth: Authorization, uid: Query<i64>) -> CreateGroupResponse {
- use CreateGroupResponse::*;
- if !self.db.valid_id(IdType::User, uid.0).unwrap() {
- return NotFound;
- }
- let gid = gen_id();
- self.db.create_group(gid, auth.0.id, String::from(""), true).unwrap();
- self.db.add_group_member(gid, uid.0).unwrap();
- self.db.add_user_dm(auth.0.id, gid).unwrap();
- self.db.add_user_dm(uid.0, gid).unwrap();
- let cid = gen_id();
- self.db.create_channel(cid, gid, auth.0.id, String::from("main")).unwrap();
- self.db.add_group_channel(gid, cid).unwrap();
- self.db.add_channel_member(cid, uid.0).unwrap();
- Success(Json(Group {
- id: gid,
- name: String::from(""),
- members: vec![auth.0.id, uid.0],
- channels: vec![cid],
- admin: vec![],
- owner: auth.0.id,
- is_dm: true
- }))
- }
-
- #[oai(path = "/group", method = "put")]
- /// Update the name of an existing group.
- ///
- /// Only authorized for the owner of a group.
- async fn update_group(&self, auth: Authorization, id: Query<i64>, name: Query<String>) -> GenericResponse {
- use GenericResponse::*;
- if name.0 == "" {
- return BadRequest(PlainText("Empty string not allowed for name".to_string()))
- } else if !self.db.valid_id(IdType::Group, id.0).unwrap() {
- return NotFound(PlainText("Didn't find group or experienced database error.".to_string()));
- } else if self.db.get_group_owner(id.0).unwrap() != auth.0.id {
- return Unauthorized;
- }
- let fixed_name = check_name(name.0.clone());
- self.db.update_group(id.0, fixed_name).unwrap();
- Success
- }
-
- #[oai(path = "/group", method = "delete")]
- /// Delete a group.
- ///
- /// Only authorized for the owner of a group.
- async fn delete_group(&self, auth: Authorization, id: Query<i64>) -> DeleteResponse {
- use DeleteResponse::*;
- if !self.db.valid_id(IdType::Group, id.0).unwrap() {
- return NotFound(PlainText("Group not found".to_string()));
- } else if self.db.get_group_owner(id.0).unwrap() != auth.0.id {
- return Unauthorized;
- }
- let group = self.db.get_group(id.0).unwrap();
- for member in group.members {
- self.db.remove_user_group(member, id.0).unwrap();
- }
- for channel in group.channels {
- self.db.delete_channel(channel).unwrap();
- }
- self.db.delete_group(id.0).unwrap();
- Success
- }
-
- #[oai(path = "/group/members", method = "get")]
- /// Get the members of the specified group.
- ///
- /// No specific order for the list is guaranteed.
- async fn get_group_members(&self, auth: Authorization, id: Query<i64>) -> MembersResponse {
- use MembersResponse::*;
- if !self.db.valid_id(IdType::Group, id.0).unwrap() {
- return NotFound;
- }
- let members = self.db.get_group_members(id.0).unwrap();
- Success(Json(members.iter().map(|m| {
- self.db.get_user(*m).unwrap()
- }).collect::<Vec<User>>()))
- }
-
- #[oai(path = "/group/members", method = "put")]
- /// Add a member to an existing group
- ///
- /// Only authorized for group admins.
- /// Has the side effect of adding that member to all public channels.
- async fn add_group_member(&self, auth: Authorization, gid: Query<i64>, uid: Query<i64>) -> GenericResponse {
- use GenericResponse::*;
- if !self.db.valid_id(IdType::Group, gid.0).unwrap() {
- return NotFound(PlainText("Group not found".to_string()));
- } else if !self.db.get_group_admin(gid.0).unwrap().contains(&auth.0.id) &&
- self.db.get_group_owner(gid.0).unwrap() != auth.0.id
- {
- return Unauthorized;
- }
- self.db.add_group_member(gid.0, uid.0).unwrap();
- let channels = self.db.get_group_channels(gid.0).unwrap();
- for channel in channels {
- if self.db.is_channel_private(channel).unwrap() { continue; }
- self.db.add_channel_member(channel, uid.0).unwrap();
- }
- if !self.db.is_group_dm(gid.0).unwrap() {
- self.db.add_user_group(uid.0, gid.0).unwrap();
- } else {
- self.db.add_user_dm(uid.0, gid.0).unwrap();
- }
- Success
- }
-
- #[oai(path = "/group/members", method = "delete")]
- /// Remove a member from an existing group
- ///
- /// Only authorized for group admin.
- /// Attempting to remove the owner from their group will always be unauthorized.
- ///
- /// Has the side effect of removing the member from all channels.
- async fn remove_group_member(&self, auth: Authorization, gid: Query<i64>, uid: Query<i64>) -> DeleteResponse {
- use DeleteResponse::*;
- if !self.db.valid_id(IdType::Group, gid.0).unwrap() {
- return NotFound(PlainText("Group not found".to_string()))
- } else if !self.db.valid_id(IdType::User, uid.0).unwrap() {
- return NotFound(PlainText("User not found".to_string()))
- } else if !self.db.get_group_admin(gid.0).unwrap().contains(&auth.0.id)
- || self.db.get_group_owner(gid.0).unwrap() == uid.0
- {
- return Unauthorized;
- }
- self.__remove_group_member(gid.0, uid.0);
- Success
- }
-
- #[oai(path = "/group/admin", method = "get")]
- /// Get the admins of the specified group.
- ///
- /// No specific order for the list is guaranteed.
- async fn get_group_admin(&self, auth: Authorization, id: Query<i64>) -> MembersResponse {
- use MembersResponse::*;
- if !self.db.valid_id(IdType::Group, id.0).unwrap() {
- return NotFound;
- }
- let members = self.db.get_group_admin(id.0).unwrap();
- Success(Json(members.iter().map(|m| {
- self.db.get_user(*m).unwrap()
- }).collect::<Vec<User>>()))
- }
-
- #[oai(path = "/group/admin", method = "put")]
- /// Add an admin to an existing group
- ///
- /// Only authorized for the owner of a group.
- async fn add_group_admin(&self, auth: Authorization, gid: Query<i64>, uid: Query<i64>) -> GenericResponse {
- use GenericResponse::*;
- if !self.db.valid_id(IdType::Group, gid.0).unwrap() {
- return NotFound(PlainText("Group not found".to_string()));
- } else if !self.db.valid_id(IdType::User, uid.0).unwrap() {
- return NotFound(PlainText("User not found".to_string()))
- } else if self.db.get_group_owner(gid.0).unwrap() != auth.0.id {
- return Unauthorized;
- }
- self.db.add_group_admin(gid.0, uid.0).unwrap();
- Success
- }
-
- #[oai(path = "/group/admin", method = "delete")]
- /// Remove an admin from an existing group
- ///
- /// Only authorized for the owner of a group.
- async fn remove_group_admin(&self, auth: Authorization, gid: Query<i64>, uid: Query<i64>) -> DeleteResponse {
- use DeleteResponse::*;
- if !self.db.valid_id(IdType::Group, gid.0).unwrap() {
- return NotFound(PlainText("Group not found".to_string()))
- } else if !self.db.valid_id(IdType::User, uid.0).unwrap() {
- return NotFound(PlainText("User not found".to_string()))
- } else if self.db.get_group_owner(gid.0).unwrap() != auth.0.id {
- return Unauthorized;
- }
- self.db.remove_group_admin(gid.0, uid.0).unwrap();
- Success
- }
-
- #[oai(path = "/group/channels", method = "get")]
- /// Gets all channels in a group that are accessible to you
- async fn get_channels(&self, auth: Authorization, gid: Query<i64>) -> ChannelsResponse {
- use ChannelsResponse::*;
- if !self.db.valid_id(IdType::Group, gid.0).unwrap() {
- return NotFound;
- }
- let channels = self.db.get_group_channels(gid.0).unwrap();
- Success(Json(channels.iter().map(|c| {
- self.db.get_channel(*c).unwrap()
- }).filter(|c| c.members.contains(&auth.0.id)).collect::<Vec<Channel>>()))
- }
-
- #[oai(path = "/group/channels", method = "post")]
- /// Create a channel in a group.
- ///
- /// Only authorized for a group admin.
- /// Defaults to a public channel with no members but yourself.
- // TODO add some mechanism for auto-inviting current members
- async fn make_channel(&self, auth: Authorization, gid: Query<i64>, name: Query<String>) -> CreateChannelResponse {
- use CreateChannelResponse::*;
- if name.0 == "" {
- return BadRequest(PlainText("Empty string not allowed for name".to_string()))
- } else if !self.db.valid_id(IdType::Group, gid.0).unwrap() {
- return NotFound(PlainText("Group not found".to_string()));
- } else if !self.db.get_group_admin(gid.0).unwrap().contains(&auth.0.id) {
- return Unauthorized;
- }
- let cid = gen_id();
- let fixed_name = check_name(name.0.clone());
- self.db.create_channel(cid, gid.0, auth.0.id, fixed_name.clone()).unwrap();
- self.db.add_group_channel(gid.0, cid).unwrap();
- Success(Json(Channel {
- id: cid,
- group: gid.0,
- name: name.0,
- members: vec![auth.0.id],
- private: false
- }))
- }
-
- #[oai(path = "/channel", method = "put")]
- /// Update the name of a channel.
- ///
- /// Only authorized for group admins.
- async fn update_channel(&self, auth: Authorization, id: Query<i64>, name: Query<String>) -> GenericResponse {
- use GenericResponse::*;
- if !self.db.valid_id(IdType::Channel, id.0).unwrap() {
- return NotFound(PlainText("Channel not found".to_string()));
- }
- let channel = self.db.get_channel(id.0).unwrap();
- if !self.db.get_group_admin(channel.group).unwrap().contains(&auth.0.id) {
- return Unauthorized;
- }
- let fixed_name = check_name(name.0.clone());
- self.db.update_channel(id.0, fixed_name).unwrap();
- Success
- }
-
- #[oai(path = "/channel/private", method = "put")]
- /// Make a channel private.
- ///
- /// Only authorized for group admins.
- async fn make_channel_private(&self, auth: Authorization, id: Query<i64>, val: Query<bool>) -> GenericResponse {
- use GenericResponse::*;
- if !self.db.valid_id(IdType::Channel, id.0).unwrap() {
- return NotFound(PlainText("Channel not found".to_string()));
- }
- let channel = self.db.get_channel(id.0).unwrap();
- if !self.db.get_group_admin(channel.group).unwrap().contains(&auth.0.id) {
- return Unauthorized;
- }
- self.db.set_channel_private(id.0, val.0).unwrap();
- Success
- }
-
- #[oai(path = "/channel", method = "get")]
- /// Get a channel.
- async fn get_channel(&self, auth: Authorization, id: Query<i64>) -> ChannelResponse {
- use ChannelResponse::*;
- if !self.db.valid_id(IdType::Channel, id.0).unwrap() ||
- !self.db.get_channel_members(id.0).unwrap().contains(&auth.0.id)
- {
- return NotFound;
- }
- Success(Json(self.db.get_channel(id.0).unwrap()))
- }
-
- #[oai(path = "/channel", method = "delete")]
- /// Delete a channel.
- ///
- /// Only authorized for group admins.
- async fn delete_channel(&self, auth: Authorization, id: Query<i64>) -> DeleteResponse {
- use DeleteResponse::*;
- if !self.db.valid_id(IdType::Channel, id.0).unwrap() {
- return NotFound(PlainText("Channel not found".to_string()));
- }
- let channel = self.db.get_channel(id.0).unwrap();
- if !self.db.get_group_admin(channel.group).unwrap().contains(&auth.0.id) {
- return Unauthorized;
- }
- self.db.remove_group_channel(channel.group, id.0).unwrap();
- self.db.delete_channel(id.0).unwrap();
- Success
- }
-
- #[oai(path = "/channel/members", method = "get")]
- /// Get the members that can access a channel.
- ///
- /// No specific order for the list is guaranteed.
- async fn get_channel_members(&self, auth: Authorization, id: Query<i64>) -> MembersResponse {
- use MembersResponse::*;
- let members = self.db.get_channel_members(id.0).unwrap();
- Success(Json(members.iter().map(|m| {
- self.db.get_user(*m).unwrap()
- }).collect::<Vec<User>>()))
- }
-
- #[oai(path = "/channel/members", method = "put")]
- /// Add a member to a channel
- ///
- /// Only authorized for group admins.
- async fn add_channel_member(&self, auth: Authorization, cid: Query<i64>, uid: Query<i64>) -> GenericResponse {
- use GenericResponse::*;
- if !self.db.valid_id(IdType::Channel, cid.0).unwrap() {
- return NotFound(PlainText("Channel not found".to_string()))
- } else if !self.db.valid_id(IdType::User, uid.0).unwrap() {
- return NotFound(PlainText("User not found".to_string()))
- }
- let channel = self.db.get_channel(cid.0).unwrap();
- if !self.db.get_group_admin(channel.group).unwrap().contains(&auth.0.id) {
- return Unauthorized;
- }
- self.db.add_channel_member(cid.0, uid.0).unwrap();
- Success
- }
-
- #[oai(path = "/channel/members", method = "delete")]
- /// Remove a member from a channel.
- ///
- /// Only authorized for group admins.
- async fn remove_channel_member(&self, auth: Authorization, cid: Query<i64>, uid: Query<i64>) -> DeleteResponse {
- use DeleteResponse::*;
- if !self.db.valid_id(IdType::Channel, cid.0).unwrap() {
- return NotFound(PlainText("Channel not found".to_string()))
- } else if !self.db.valid_id(IdType::User, uid.0).unwrap() {
- return NotFound(PlainText("User not found".to_string()))
- }
- let channel = self.db.get_channel(cid.0).unwrap();
- if !self.db.get_group_admin(channel.group).unwrap().contains(&auth.0.id) {
- return Unauthorized;
- }
- self.db.remove_channel_member(cid.0, uid.0).unwrap();
- Success
- }
-
- #[oai(path = "/channel/term", method = "get")]
- /// Get a batch of messages in channel containing `term` in the last 100 messages
- ///
- /// Will not search for `term` in any messages older than the last 100.
- async fn search_channel(&self, auth: Authorization, cid: Query<i64>, term: Query<String>, off: Query<u64>) -> MessagesResponse {
- use MessagesResponse::*;
- if !self.db.valid_id(IdType::Channel, cid.0).unwrap() {
- return NotFound(PlainText("Channel not found".to_string()))
- }
- let mut messages = self.db.get_messages(cid.0, 100).unwrap();
- messages.retain(|msg| msg.content.contains(&term.0));
- Success(Json(messages))
- }
-
- #[oai(path = "/channel/messages", method = "get")]
- /// Returns batch of messages in channel. Do not use for small batches.
- ///
- /// For small batches, use `chatterbox`, the websocket service for messaging, instead.
- async fn get_channel_messages(&self, auth: Authorization, cid: Query<i64>, num_msgs: Query<u64>) -> MessagesResponse {
- use MessagesResponse::*;
- if !self.db.valid_id(IdType::Channel, cid.0).unwrap() {
- return NotFound(PlainText("Channel not found".to_string()))
- }
- Success(Json(self.db.get_messages(cid.0, num_msgs.0).unwrap()))
- }
-
- #[oai(path = "/message/thread", method = "put")]
- /// Make a thread for a given message.
- ///
- /// Thread will be private with you as its sole member
- async fn make_thread(&self, auth: Authorization, id: Query<i64>, name: Query<String>) -> CreateChannelResponse {
- use CreateChannelResponse::*;
- if name.0 == "" {
- return BadRequest(PlainText("Empty string not allowed for name".to_string()))
- } else if !self.db.valid_id(IdType::Message, id.0).unwrap() {
- return NotFound(PlainText("Message not found".to_string()))
- }
- let fixed_name = check_name(name.0.clone());
- let tid = gen_id();
- let msg = self.db.get_message(id.0).unwrap();
- let chan = self.db.get_channel(msg.channel).unwrap();
- self.db.create_channel(tid, chan.group, auth.0.id, fixed_name.clone()).unwrap();
- self.db.set_channel_private(tid, true).unwrap();
- self.db.set_thread(id.0, tid).unwrap();
- Success(Json(Channel {
- id: tid,
- group: chan.group,
- members: vec![auth.0.id],
- name: name.0,
- private: true
- }))
- }
-
- #[oai(path = "/message", method = "delete")]
- /// Delete a message
- ///
- /// Only authorized for the message author or a group admin.
- async fn delete_message(&self, auth: Authorization, id: Query<i64>) -> DeleteResponse {
- use DeleteResponse::*;
- if !self.db.valid_id(IdType::Message, id.0).unwrap() {
- return NotFound(PlainText("Message not found".to_string()))
- }
- let msg = self.db.get_message(id.0).unwrap();
- let chan = self.db.get_channel(msg.channel).unwrap();
- if msg.author != auth.0.id && !self.db.get_group_admin(chan.group).unwrap().contains(&auth.0.id) {
- return Unauthorized;
- }
- self.db.delete_message(id.0).unwrap();
- Success
- }
+ fn __remove_group_member(&self, conn: &mut PgConnection, gid: i64, uid: i64) -> Result<usize, WithBacktrace<diesel::result::Error>> {
+ remove_array(conn, "groups", "members", gid, uid)?;
+
+ let channels = groups::table.select(groups::dsl::channels)
+ .filter(groups::dsl::id.eq(gid)).first::<Vec<i64>>(conn)
+ .with_backtrace()?;
+
+ for channel in channels {
+ remove_array(conn, "channels", "members", channel, uid)?;
+ }
+ remove_array(conn, "user_groups", "groups", uid, gid)
+ }
+
+ #[oai(path = "/login", method = "post")]
+ /// Log in as a user. Returns an authentication token given id and hash.
+ ///
+ /// Expects hash of user's password to be given in the request body.
+ /// Checks validity of hash, then signs JWT with a server secret key.
+ // good
+ async fn login(&self, key: Data<&ServerKey>, id: Query<i64>, hash: PlainText<String>) -> Result<PlainText<String>> {
+ if hash.0.len() != 64 {
+ return Err(UserFacingError::new(StatusCode::BAD_REQUEST, "Hash is not of length 24!").into())
+ }
+ let conn = &mut open_db_conn();
+ let user: User = users::table.find(id.0).first(conn)
+ .poemify("retrieving specified user")?;
+ if hex::decode(&user.hash).unwrap() != hex::decode(&hash.0).unwrap() {
+ return Err(UserFacingError::new(StatusCode::UNAUTHORIZED, "Incorrect hash provided.").into())
+ } else {
+ let token = Claims {
+ id: id.0,
+ exp: Local::now() + Duration::days(1),
+ }
+ .sign_with_key(key.0);
+ Ok(PlainText(token.unwrap()))
+ }
+ }
+
+ #[oai(path = "/user", method = "get")]
+ /// Get the user with the given ID
+ ///
+ /// Does not require any authorization.
+ // good
+ async fn get_user(&self, id: Query<i64>) -> Result<Json<User>> {
+ let conn = &mut open_db_conn();
+ let user: User = users::table.find(id.0).first(conn)
+ .poemify("retrieving specified user")?;
+ Ok(Json(user))
+ }
+
+ #[oai(path = "/user", method = "post")]
+ /// Create a new user.
+ ///
+ /// Expects hash of user's password to be given in the request body.
+ /// Does not require any authorization.
+ // good
+ async fn make_user(&self, name: Query<String>, email: Query<String>, hash: PlainText<String>) -> Result<Json<User>> {
+ let conn = &mut open_db_conn();
+
+ if hash.0.len() != 64 {
+ return Err(UserFacingError::new(StatusCode::BAD_REQUEST, "Hash is not of length 24!").into())
+ }
+
+ let user = User {
+ id: gen_id(),
+ name: check_name(name.0),
+ email: email.0,
+ hash: hash.0,
+ };
+
+ diesel::insert_into(users::table).values(&user)
+ .execute(conn).poemify("adding user to database")?;
+ // diesel::update(users::table).set(users::dsl::hash.eq(hash.0))
+ // .execute(conn).poemify("setting user hash");
+ diesel::insert_into(user_groups::table)
+ .values((user_groups::dsl::id.eq(user.id), user_groups::dsl::groups.eq(Vec::<i64>::new())))
+ .execute(conn).poemify("initializing associated database entries")?;
+ diesel::insert_into(user_dms::table)
+ .values((user_dms::dsl::id.eq(user.id), user_dms::dsl::dms.eq(Vec::<i64>::new())))
+ .execute(conn).poemify("initializing associated database entries")?;
+
+ Ok(Json(user))
+ }
+
+ #[oai(path = "/user", method = "put")]
+ /// Update your name and email.
+ // good
+ async fn update_user(&self, auth: Authorization, name: Query<String>, email: Query<String>) -> Result<()> {
+ let conn = &mut open_db_conn();
+ let checked_name = check_name(name.0.clone());
+ diesel::update(users::table.find(auth.0.id))
+ .set((users::dsl::name.eq(checked_name), users::dsl::email.eq(email.0)))
+ .execute(conn).poemify("updating database")?;
+ Ok(())
+ }
+
+ #[oai(path = "/user", method = "delete")]
+ /// Delete your user.
+ ///
+ /// Has the side effects of removing your user from every group, channel, or DM
+ /// it is a member of.
+ // good
+ async fn delete_user(&self, auth: Authorization) -> Result<()> {
+ let conn = &mut open_db_conn();
+ diesel::delete(users::table.find(auth.0.id)).execute(conn).poemify("deleting your user")?;
+ let your_groups: Vec<i64> = user_groups::table
+ .select(user_groups::dsl::groups).find(auth.0.id)
+ .first(conn).poemify("getting your groups")?;
+ let your_dms: Vec<i64> = user_dms::table
+ .select(user_dms::dsl::dms).find(auth.0.id)
+ .first(conn).poemify("getting your DMs")?;
+ for group in your_groups {
+ self.__remove_group_member(conn, group, auth.0.id);
+ }
+ for dm in your_dms {
+ self.__remove_group_member(conn, dm, auth.0.id);
+ }
+ diesel::delete(user_dms::table.find(auth.0.id)).execute(conn).poemify("deleting your groups list")?;
+ diesel::delete(user_groups::table.find(auth.0.id)).execute(conn).poemify("deleting your DM list")?;
+ Ok(())
+ }
+
+ #[oai(path = "/user/groups", method = "get")]
+ /// Get all groups accessible to you.
+ // good
+ async fn get_groups(&self, auth: Authorization) -> Result<Json<Vec<Group>>> {
+ let conn = &mut open_db_conn();
+ let res: UserGroup = user_groups::table.find(auth.0.id)
+ .first(conn).poemify("retrieving your groups")?;
+ Ok(Json(res.groups.iter().map(|i| {
+ groups::table.find(*i).first(conn).unwrap()
+ }).collect()))
+ }
+
+ #[oai(path = "/user/dms", method = "get")]
+ /// Get all DMs accessible to you.
+ // good
+ async fn get_dms(&self, auth: Authorization) -> Result<Json<Vec<Group>>> {
+ let conn = &mut open_db_conn();
+ let res: UserDm = user_dms::table.find(auth.0.id)
+ .first(conn).poemify("retrieving your dms")?;
+ Ok(Json(res.dms.iter().map(|i| {
+ groups::table.find(*i).first(conn).unwrap()
+ }).collect()))
+ }
+
+ #[oai(path = "/user/groups", method = "delete")]
+ /// Leave a group accessible to you
+ // good
+ async fn leave_group(&self, auth: Authorization, gid: Query<i64>) -> Result<()> {
+ let conn = &mut open_db_conn();
+ self.__remove_group_member(conn, gid.0, auth.0.id).poemify("removing you from group")?;
+ Ok(())
+ }
+
+
+ #[oai(path = "/group", method = "get")]
+ /// Gets the group with the given ID
+ // good
+ async fn get_group(&self, auth: Authorization, id: Query<i64>) -> Result<Json<Group>> {
+ let conn = &mut open_db_conn();
+ let group: Group = groups::table.find(id.0).first(conn)
+ .poemify("retrieving specified group")?;
+ if !group.members.contains(&auth.0.id) {
+ return Err(UserFacingError::terse(StatusCode::FORBIDDEN).into())
+ }
+ Ok(Json(group))
+ }
+
+
+ #[oai(path = "/group", method = "post")]
+ /// Create a new group.
+ ///
+ /// The group created...
+ /// - will have a default public "main" channel
+ /// - will have your user as the owner
+ /// - will have your user as an admin
+ // good
+ async fn make_group(&self, auth: Authorization, name: Query<String>) -> Result<Json<Group>> {
+ let conn = &mut open_db_conn();
+ let gid = gen_id();
+ let channel = Channel {
+ id: gen_id(),
+ src_group: gid,
+ name: String::from("main"),
+ members: vec![auth.0.id],
+ private: false
+ };
+ diesel::insert_into(channels::table).values(&channel)
+ .execute(conn).poemify("adding 'main' channel to database")?;
+ let group = Group {
+ id: gid,
+ name: check_name(name.0),
+ members: vec![auth.0.id],
+ admin: vec![auth.0.id],
+ owner: auth.0.id,
+ is_dm: false,
+ channels: vec![channel.id]
+ };
+ diesel::insert_into(groups::table).values(&group)
+ .execute(conn).poemify("adding group to database")?;
+ append_array(conn, "user_groups", "groups", auth.0.id, gid)
+ .poemify("adding group to your groups")?;
+ Ok(Json(group))
+ }
+
+ #[oai(path = "/dm", method = "post")]
+ /// Create a new DM with a user `uid`.
+ ///
+ /// The group created...
+ /// - will have the `is_dm` attribute set to true.
+ /// - will have only one channel "main" with you and `uid`
+ /// - will have no owner or admins
+ // good
+ async fn make_dm(&self, auth: Authorization, uid: Query<i64>) -> Result<Json<Group>> {
+ let conn = &mut open_db_conn();
+ let user: User = users::table.find(uid.0).first(conn)
+ .poemify("retreiving specified user")?;
+ let gid = gen_id();
+ let channel = Channel {
+ id: gen_id(),
+ src_group: gid,
+ name: String::from("main"),
+ members: vec![auth.0.id, uid.0],
+ private: false
+ };
+ diesel::insert_into(channels::table).values(&channel)
+ .execute(conn).poemify("adding 'main' channel to database")?;
+ let group = Group {
+ id: gid,
+ name: String::from(""),
+ members: vec![auth.0.id, uid.0],
+ admin: vec![],
+ owner: auth.0.id,
+ is_dm: false,
+ channels: vec![channel.id]
+ };
+ diesel::insert_into(groups::table).values(&group)
+ .execute(conn).poemify("adding group to database")?;
+ append_array(conn, "user_dms", "groups", auth.0.id, gid)
+ .poemify("adding DM to your DMs")?;
+ append_array(conn, "user_dms", "groups", uid.0, gid)
+ .poemify("adding DM to their DMs")?;
+ Ok(Json(group))
+ }
+
+ #[oai(path = "/group", method = "put")]
+ /// Update the name of an existing group.
+ ///
+ /// Only authorized for the owner of a group.
+ // good
+ async fn update_group(&self, auth: Authorization, id: Query<i64>, name: Query<String>) -> Result<()> {
+ let conn = &mut open_db_conn();
+ let mut group: Group = groups::table.find(id.0).first(conn)
+ .poemify("retreiving specified group")?;
+ if group.owner != auth.0.id {
+ return Err(UserFacingError::terse(StatusCode::FORBIDDEN).into());
+ }
+ group.name = check_name(name.0);
+ diesel::update(groups::table.find(id.0)).set(&group)
+ .execute(conn).poemify("updating group in database");
+ Ok(())
+ }
+
+ #[oai(path = "/group", method = "delete")]
+ /// Delete a group.
+ ///
+ /// Only authorized for the owner of a group.
+ // good
+ async fn delete_group(&self, auth: Authorization, id: Query<i64>) -> Result<()> {
+ let conn = &mut open_db_conn();
+ let group: Group = groups::table.find(id.0).first(conn)
+ .poemify("retreiving specified group")?;
+ if group.owner != auth.0.id {
+ return Err(UserFacingError::terse(StatusCode::FORBIDDEN).into());
+ }
+ for member in group.members {
+ remove_array(conn, "user_groups", "groups", member, id.0)
+ .poemify("removing member from group")?;
+ }
+ for channel in group.channels {
+ diesel::delete(channels::table.find(channel)).execute(conn)
+ .poemify("deleting channel from database")?;
+ }
+ diesel::delete(groups::table.find(id.0)).execute(conn)
+ .poemify("deleting group from database")?;
+ Ok(())
+ }
+
+ #[oai(path = "/group/members", method = "get")]
+ /// Get the members of the specified group.
+ ///
+ /// No specific order for the list is guaranteed.
+ // good
+ async fn get_group_members(&self, auth: Authorization, id: Query<i64>) -> Result<Json<Vec<User>>> {
+ let conn = &mut open_db_conn();
+ let group: Group = groups::table.find(id.0).first(conn)
+ .poemify("retreiving specified group")?;
+ Ok(Json(group.members.iter().map(|m| {
+ users::table.find(*m).first(conn).unwrap()
+ }).collect()))
+ }
+
+ #[oai(path = "/group/members", method = "put")]
+ /// Add a member to an existing group
+ ///
+ /// Only authorized for group admins.
+ /// Has the side effect of adding that member to all public channels.
+ // good
+ async fn add_group_member(&self, auth: Authorization, gid: Query<i64>, uid: Query<i64>) -> Result<()> {
+ let conn = &mut open_db_conn();
+ let mut group: Group = groups::table.find(gid.0).first(conn)
+ .poemify("retreiving specified group")?;
+ if !group.admin.contains(&auth.0.id) {
+ return Err(UserFacingError::terse(StatusCode::FORBIDDEN).into());
+ }
+ group.members.push(uid.0);
+ for chan_id in group.channels {
+ let mut channel: Channel = channels::table.find(chan_id).first(conn)
+ .poemify(&format!("retreiving channel (id {}) of group", chan_id))?;
+ if channel.private { continue; }
+ diesel::update(channels::table.find(chan_id)).set(&channel)
+ .execute(conn).poemify("adding member to channel");
+ }
+ if !group.is_dm {
+ append_array(conn, "user_groups", "groups", auth.0.id, gid.0)
+ .poemify("adding group to your groups")?;
+ } else {
+ append_array(conn, "user_dms", "dms", auth.0.id, gid.0)
+ .poemify("adding DM to your DMs")?;
+ }
+ Ok(())
+ }
+
+ #[oai(path = "/group/members", method = "delete")]
+ /// Remove a member from an existing group
+ ///
+ /// Only authorized for group admin.
+ /// Attempting to remove the owner from their group will always be unauthorized.
+ ///
+ /// Has the side effect of removing the member from all channels.
+ // good
+ async fn remove_group_member(&self, auth: Authorization, gid: Query<i64>, uid: Query<i64>) -> Result<()> {
+ let conn = &mut open_db_conn();
+ let admin: Vec<i64> = groups::table.select(groups::dsl::admin)
+ .find(gid.0).first(conn).poemify("getting group admin")?;
+ if !admin.contains(&auth.0.id) {
+ return Err(UserFacingError::terse(StatusCode::FORBIDDEN).into());
+ }
+ self.__remove_group_member(conn, gid.0, uid.0).poemify("removing group member")?;
+ Ok(())
+ }
+
+ #[oai(path = "/group/admin", method = "get")]
+ /// Get the admins of the specified group.
+ ///
+ /// No specific order for the list is guaranteed.
+ // good
+ async fn get_group_admin(&self, auth: Authorization, id: Query<i64>) -> Result<Json<Vec<User>>> {
+ let conn = &mut open_db_conn();
+ Ok(Json(
+ groups::table
+ .select(groups::dsl::admin)
+ .filter(groups::dsl::id.eq(id.0))
+ .first::<Vec<i64>>(conn)
+ .poemify("retreiving admin of group")?
+ .iter().map(|a| {
+ users::table.find(*a).first(conn).unwrap()
+ }).collect()
+ ))
+ }
+
+ #[oai(path = "/group/admin", method = "put")]
+ /// Add an admin to an existing group
+ ///
+ /// Only authorized for the owner of a group.
+ // good
+ async fn add_group_admin(&self, auth: Authorization, gid: Query<i64>, uid: Query<i64>) -> Result<()> {
+ let conn = &mut open_db_conn();
+ let mut group: Group = groups::table.find(gid.0).first(conn)
+ .poemify("retreiving specified group")?;
+ let _user: User = users::table.find(uid.0).first(conn)
+ .poemify("retreiving specified user")?; // prevent adding invalid UID
+ if auth.0.id != group.owner {
+ return Err(UserFacingError::terse(StatusCode::FORBIDDEN).into());
+ }
+ group.admin.push(uid.0);
+ diesel::update(groups::table.find(gid.0)).set(&group)
+ .execute(conn).poemify("updating group in database");
+ Ok(())
+ }
+
+ #[oai(path = "/group/admin", method = "delete")]
+ /// Remove an admin from an existing group
+ ///
+ /// Only authorized for the owner of a group.
+ // good
+ async fn remove_group_admin(&self, auth: Authorization, gid: Query<i64>, uid: Query<i64>) -> Result<()> {
+ let conn = &mut open_db_conn();
+ let mut group: Group = groups::table.find(gid.0).first(conn)
+ .poemify("retreiving specified group")?;
+ let _user: User = users::table.find(uid.0).first(conn)
+ .poemify("retreiving specified user")?; // prevent adding invalid UID
+ if auth.0.id != group.owner {
+ return Err(UserFacingError::terse(StatusCode::FORBIDDEN).into());
+ }
+ group.admin.retain(|x| *x != uid.0);
+ diesel::update(groups::table.find(gid.0)).set(&group)
+ .execute(conn).poemify("updating group in database");
+ Ok(())
+ }
+
+ #[oai(path = "/group/channels", method = "get")]
+ /// Gets all channels in a group that are accessible to you
+ // good
+ async fn get_channels(&self, auth: Authorization, gid: Query<i64>) -> Result<Json<Vec<Channel>>> {
+ let conn = &mut open_db_conn();
+ Ok(Json(
+ groups::table
+ .select(groups::dsl::channels)
+ .filter(groups::dsl::id.eq(gid.0))
+ .first::<Vec<i64>>(conn)
+ .poemify("retreiving group channels")?
+ .iter()
+ .map(|c| channels::table.find(*c).first(conn).unwrap())
+ .filter(|c: &Channel| c.members.contains(&auth.0.id))
+ .collect()
+ ))
+ }
+
+ #[oai(path = "/group/channels", method = "post")]
+ // good
+ async fn make_channel(&self, auth: Authorization, gid: Query<i64>, name: Query<String>) -> Result<Json<Channel>> {
+ let conn = &mut open_db_conn();
+ let out: Group = groups::table.find(gid.0).first(conn)
+ .with_backtrace().poemify("retrieving specified group")?;
+
+ if !out.admin.contains(&auth.0.id) {
+ return Err(UserFacingError::new(StatusCode::FORBIDDEN,
+ "You do not have permission to perform the requested action."
+ ).into())
+ }
+
+ let chan = Channel {
+ id: gen_id(),
+ src_group: gid.0,
+ name: check_name(name.0.clone()),
+ members: vec![auth.0.id],
+ private: false
+ };
+
+ diesel::insert_into(channels::table).values(&chan)
+ .execute(conn).poemify("adding channel to database")?;
+
+ append_array(conn, "groups", "channels", gid.0, chan.id)
+ .poemify("adding channel to group")?;
+
+ Ok(Json(chan))
+ }
+
+ #[oai(path = "/channel", method = "put")]
+ /// Update the name of a channel.
+ ///
+ /// Only authorized for group admins.
+ // good
+ async fn update_channel(&self, auth: Authorization, id: Query<i64>, name: Query<String>) -> Result<()> {
+ let conn = &mut open_db_conn();
+ diesel::update(channels::table.find(auth.0.id))
+ .set(channels::dsl::name.eq(check_name(name.0)))
+ .execute(conn).poemify("updating database");
+ Ok(())
+ }
+
+ #[oai(path = "/channel/private", method = "put")]
+ /// Make a channel private.
+ ///
+ /// Only authorized for group admins.
+ // good
+ async fn make_channel_private(&self, auth: Authorization, id: Query<i64>, val: Query<bool>) -> Result<()> {
+ let conn = &mut open_db_conn();
+ let group: i64 = channels::table.select(channels::dsl::src_group).find(id.0)
+ .first(conn).poemify("getting channel group")?;
+ let admin: Vec<i64> = groups::table.select(groups::dsl::admin)
+ .find(group).first(conn).poemify("getting group admin")?;
+ if !admin.contains(&auth.0.id) {
+ return Err(UserFacingError::terse(StatusCode::FORBIDDEN).into())
+ }
+ diesel::update(channels::table.find(id.0))
+ .set(channels::dsl::private.eq(val.0))
+ .execute(conn).poemify("setting channel privacy in database");
+ Ok(())
+ }
+
+ #[oai(path = "/channel", method = "get")]
+ /// Get a channel.
+ // good
+ async fn get_channel(&self, auth: Authorization, id: Query<i64>) -> Result<Json<Channel>> {
+ let conn = &mut open_db_conn();
+ Ok(Json(channels::table.find(id.0).first(conn).poemify("retrieving specified channel")?))
+ }
+
+ #[oai(path = "/channel", method = "delete")]
+ /// Delete a channel.
+ ///
+ /// Only authorized for group admins.
+ // good
+ async fn delete_channel(&self, auth: Authorization, id: Query<i64>) -> Result<()> {
+ let conn = &mut open_db_conn();
+ let group: i64 = channels::table.select(channels::dsl::src_group).find(id.0)
+ .first(conn).poemify("getting channel group")?;
+ let admin: Vec<i64> = groups::table.select(groups::dsl::admin)
+ .find(group).first(conn).poemify("getting group admin")?;
+ if !admin.contains(&auth.0.id) {
+ return Err(UserFacingError::terse(StatusCode::FORBIDDEN).into())
+ }
+ diesel::delete(channels::table.find(id.0)).execute(conn)
+ .poemify("deleting channel from database")?;
+ remove_array(conn, "groups", "channels", group, id.0)
+ .poemify("removing channel from group")?;
+ Ok(())
+ }
+
+ #[oai(path = "/channel/members", method = "get")]
+ /// Get the members that can access a channel.
+ ///
+ /// No specific order for the list is guaranteed.
+ // good
+ async fn get_channel_members(&self, auth: Authorization, id: Query<i64>) -> Result<Json<Vec<User>>> {
+ let conn = &mut open_db_conn();
+ Ok(Json(
+ channels::table
+ .select(channels::dsl::members)
+ .filter(channels::dsl::id.eq(id.0))
+ .first::<Vec<i64>>(conn)
+ .poemify("retreiving channel members")?
+ .iter().map(|u| {
+ users::table.find(*u).first(conn).unwrap()
+ }).collect()
+ ))
+ }
+
+ #[oai(path = "/channel/members", method = "put")]
+ /// Add a member to a channel
+ ///
+ /// Only authorized for group admins.
+ // good
+ async fn add_channel_member(&self, auth: Authorization, cid: Query<i64>, uid: Query<i64>) -> Result<()> {
+ let conn = &mut open_db_conn();
+ let mut chan: Channel = channels::table.find(cid.0).first(conn)
+ .poemify("retreiving specified channel")?;
+ let group: Group = groups::table.find(chan.src_group).first(conn)
+ .poemify("retreiving channel's group")?;
+ let _user: User = users::table.find(uid.0).first(conn)
+ .poemify("retreiving specified user")?; // prevent adding invalid UID
+ if !group.admin.contains(&auth.0.id) {
+ return Err(UserFacingError::terse(StatusCode::FORBIDDEN).into());
+ }
+ chan.members.push(uid.0);
+ diesel::update(channels::table.find(cid.0)).set(&chan)
+ .execute(conn).poemify("updating channel in database");
+ Ok(())
+ }
+
+ #[oai(path = "/channel/members", method = "delete")]
+ /// Remove a member from a channel.
+ ///
+ /// Only authorized for group admins.
+ // good
+ async fn remove_channel_member(&self, auth: Authorization, cid: Query<i64>, uid: Query<i64>) -> Result<()> {
+ let conn = &mut open_db_conn();
+ let mut chan: Channel = channels::table.find(cid.0).first(conn)
+ .poemify("retreiving specified channel")?;
+ let group: Group = groups::table.find(chan.src_group).first(conn)
+ .poemify("retreiving channel's group")?;
+ let _user: User = users::table.find(uid.0).first(conn)
+ .poemify("retreiving specified user")?; // prevent adding invalid UID
+ if !group.admin.contains(&auth.0.id) {
+ return Err(UserFacingError::terse(StatusCode::FORBIDDEN).into());
+ }
+ chan.members.retain(|x| *x != uid.0);
+ diesel::update(channels::table.find(cid.0)).set(&chan)
+ .execute(conn).poemify("updating channel in database");
+ Ok(())
+ }
+
+ #[oai(path = "/channel/term", method = "get")]
+ /// Get a batch of messages in channel containing `term` in the last 100 messages
+ ///
+ /// Will not search for `term` in any messages older than the last 100.
+ // good
+ async fn search_channel(&self, auth: Authorization, cid: Query<i64>, term: Query<String>) -> Result<Json<Vec<Message>>> {
+ let conn = &mut open_db_conn();
+ let mut messages = messages::table.limit(100).load::<Message>(conn)
+ .poemify("retrieving past 100 messages")?;
+ messages.retain(|msg| {
+ if let Some(content) = &msg.content {
+ return content.contains(&term.0)
+ } else { return false }
+ });
+ Ok(Json(messages))
+ }
+
+ #[oai(path = "/channel/messages", method = "get")]
+ /// Returns batch of messages in channel. Do not use for small batches.
+ ///
+ /// For small batches, use `chatterbox`, the websocket service for messaging, instead.
+ // good
+ async fn get_channel_messages(&self, auth: Authorization, cid: Query<i64>, num_msgs: Query<i64>) -> Result<Json<Vec<Message>>> {
+ let conn = &mut open_db_conn();
+ Ok(Json(
+ messages::table.limit(num_msgs.0).load::<Message>(conn)
+ .poemify("retrieving past messages")?
+ ))
+ }
+
+ #[oai(path = "/message/thread", method = "put")]
+ /// Make a thread for a given message.
+ ///
+ /// Thread will be private with you as its sole member
+ async fn make_thread(&self, auth: Authorization, id: Query<i64>, name: Query<String>) -> Result<Json<Channel>> {
+ let conn = &mut open_db_conn();
+ let mut msg: Message = messages::table.find(id.0).first(conn)
+ .poemify("retreiving specified message")?;
+ let group: i64 = channels::table.select(channels::dsl::src_group).find(msg.channel)
+ .first(conn).poemify("getting channel group")?;
+ let thread = Channel {
+ id: gen_id(),
+ name: check_name(name.0),
+ src_group: group,
+ members: vec![auth.0.id],
+ private: true,
+ };
+ msg.thread = Some(thread.id);
+ diesel::insert_into(channels::table).values(&thread)
+ .execute(conn).poemify("adding thread to database")?;
+ diesel::update(messages::table.find(id.0)).set(&msg)
+ .execute(conn).poemify("updating message in database")?;
+ Ok(Json(thread))
+ }
+
+ #[oai(path = "/message", method = "delete")]
+ /// Delete a message
+ ///
+ /// Only authorized for the message author or a group admin.
+ async fn delete_message(&self, auth: Authorization, id: Query<i64>) -> Result<()> {
+ let conn = &mut open_db_conn();
+ let msg: Message = messages::table.find(id.0).first(conn)
+ .poemify("retreiving specified message")?;
+ let group: i64 = channels::table.select(channels::dsl::src_group).find(msg.channel)
+ .first(conn).poemify("getting channel group")?;
+ let admin: Vec<i64> = groups::table.select(groups::dsl::admin)
+ .find(group).first(conn).poemify("getting group admin")?;
+ if msg.author != auth.0.id && !admin.contains(&auth.0.id) {
+ return Err(UserFacingError::terse(StatusCode::FORBIDDEN).into());;
+ }
+ diesel::delete(messages::table.find(id.0)).execute(conn)
+ .poemify("deleting message")?;
+ Ok(())
+ }
+
+
}
#[tokio::main]
async fn main() -> Result<(), std::io::Error> {
- use hmac::Mac;
- if std::env::var_os("RUST_LOG").is_none() {
- std::env::set_var("RUST_LOG", "poem=debug");
- }
- tracing_subscriber::fmt::init();
-
- let db = Box::new(Cassandra::new("bsk"));
- let api_service = OpenApiService::new(Api::new(db), "Scuttlebutt", "1.0")
- .description(
- "Scuttlebutt is the REST API for managing everything but sending/receiving messages \
- - which means creating/updating/deleting all of your users/groups/channels.",
- )
- .server("http://localhost:3000/api");
-
- // API documentation
- let ui = api_service.swagger_ui();
-
- // Generate server-side secret key used for signing the JWTs
- let key: String = rand::thread_rng()
- .sample_iter(&Alphanumeric)
- .take(7)
- .map(char::from)
- .collect();
-
- let app = Route::new()
- .nest("/api", api_service)
- .nest("/", ui)
- .data(ServerKey::new_from_slice(&key.as_bytes()).unwrap());
-
- Server::new(TcpListener::bind("127.0.0.1:3000")).run(app).await
+ use hmac::Mac;
+
+ dotenv().ok();
+
+ if std::env::var_os("RUST_LOG").is_none() {
+ std::env::set_var("RUST_LOG", "poem=debug");
+ }
+ tracing_subscriber::fmt::init();
+
+ let api_service = OpenApiService::new(Api {}, "Scuttlebutt", "1.0")
+ .description(
+ "Scuttlebutt is the REST API for managing everything but sending/receiving messages \
+ - which means creating/updating/deleting all of your users/groups/channels.",
+ )
+ .server("http://localhost:3000/api");
+
+ // API documentation
+ // let ui = api_service.swagger_ui();
+
+ // Generate server-side secret key used for signing the JWTs
+ let key: String = rand::thread_rng()
+ .sample_iter(&Alphanumeric)
+ .take(7)
+ .map(char::from)
+ .collect();
+
+ let app = Route::new()
+ .nest("/api", api_service)
+ // .nest("/", ui)
+ .data(ServerKey::new_from_slice(&key.as_bytes()).unwrap())
+ .catch_error(|_: poem::error::NotFoundError| async move {
+ poem::Response::builder()
+ .status(StatusCode::NOT_FOUND)
+ .body("<h1>404 Not Found</h1>Path not found.")
+ })
+ .catch_error(|err: poem_openapi::error::ParseParamError| async move {
+ poem::Response::builder()
+ .status(StatusCode::BAD_REQUEST)
+ .body(format!("<h1>400 Bad Request</h1><pre>{}.</pre>", err))
+ });
+
+ Server::new(TcpListener::bind("127.0.0.1:3000")).run(app).await
}
#[cfg(test)]
diff --git a/scuttlebutt/src/models.rs b/scuttlebutt/src/models.rs
@@ -0,0 +1,61 @@
+use diesel::prelude::*;
+use crate::schema::*;
+use poem_openapi::{
+ payload::{Json, PlainText},
+ ApiResponse, Object,
+};
+use serde::{Deserialize, Serialize};
+
+
+#[derive(Object, Serialize, Deserialize, Queryable, Identifiable, Insertable, AsChangeset, Debug)]
+pub struct Channel {
+ pub id: i64,
+ pub src_group: i64,
+ pub name: String,
+ pub members: Vec<i64>,
+ pub private: bool,
+}
+
+#[derive(Object, Serialize, Deserialize, Queryable, Identifiable, Insertable, AsChangeset, Debug)]
+pub struct Group {
+ pub id: i64,
+ pub name: String,
+ pub members: Vec<i64>,
+ pub is_dm: bool,
+ pub channels: Vec<i64>,
+ pub admin: Vec<i64>,
+ pub owner: i64
+}
+
+#[derive(Object, Serialize, Deserialize, Queryable, Identifiable, Insertable, AsChangeset, Debug)]
+pub struct User {
+ pub id: i64,
+ pub name: String,
+ pub email: String,
+ pub hash: String,
+}
+
+
+#[derive(Serialize, Deserialize, Queryable, Identifiable, Insertable, AsChangeset, Debug)]
+pub struct UserGroup {
+ pub id: i64,
+ pub groups: Vec<i64>
+}
+
+
+#[derive(Serialize, Deserialize, Queryable, Identifiable, Insertable, AsChangeset, Debug)]
+pub struct UserDm {
+ pub id: i64,
+ pub dms: Vec<i64>
+}
+
+
+#[derive(Object, Deserialize, Queryable, Identifiable, Insertable, AsChangeset, Debug)]
+pub struct Message {
+ pub channel: i64,
+ pub id: i64,
+ pub author: i64,
+ pub content: Option<String>,
+ pub thread: Option<i64>
+}
+
diff --git a/scuttlebutt/src/responses.rs b/scuttlebutt/src/responses.rs
@@ -4,6 +4,8 @@ use poem_openapi::{
};
use serde::{Deserialize, Serialize};
+use crate::models;
+
#[derive(Object, Serialize, Deserialize, Clone, Debug, Eq, PartialEq)]
/// Object representing a user
pub struct User {
@@ -33,7 +35,7 @@ pub struct Group {
#[derive(Object, Serialize, Deserialize, Clone, Debug, Eq, PartialEq)]
/// Object representing a group's channel.
/// No guarantees are made for the order of any vector in this struct.
-pub struct Channel {
+pub struct ChannelOrig {
pub id: i64,
pub name: String,
// ID of the group the channel is in
@@ -154,7 +156,21 @@ pub enum CreateGroupResponse {
pub enum ChannelResponse {
/// Returns the channel requested
#[oai(status = 200)]
- Success(Json<Channel>),
+ Success(Json<ChannelOrig>),
+ /// Invalid ID or user is not a member of specified channel.
+ #[oai(status = 404)]
+ NotFound,
+ /// Internal server error: likely due to a database operation failing
+ #[oai(status = 500)]
+ InternalError(PlainText<String>),
+}
+
+
+#[derive(ApiResponse)]
+pub enum ChannelResponse2 {
+ /// Returns the channel requested
+ #[oai(status = 200)]
+ Success(Json<models::Channel>),
/// Invalid ID or user is not a member of specified channel.
#[oai(status = 404)]
NotFound,
@@ -163,11 +179,12 @@ pub enum ChannelResponse {
InternalError(PlainText<String>),
}
+
#[derive(ApiResponse)]
pub enum CreateChannelResponse {
/// Returns the channel requested
#[oai(status = 200)]
- Success(Json<Channel>),
+ Success(Json<ChannelOrig>),
/// You are not authorized to perform the action
#[oai(status = 401)]
Unauthorized,
@@ -240,7 +257,7 @@ pub enum GroupsResponse {
pub enum ChannelsResponse {
/// Returns the channels in a group
#[oai(status = 200)]
- Success(Json<Vec<Channel>>),
+ Success(Json<Vec<ChannelOrig>>),
/// Invalid group ID
#[oai(status = 404)]
NotFound,
diff --git a/scuttlebutt/src/schema.rs b/scuttlebutt/src/schema.rs
@@ -0,0 +1,65 @@
+// @generated automatically by Diesel CLI.
+
+diesel::table! {
+ channels (id) {
+ id -> Int8,
+ src_group -> Int8,
+ name -> Text,
+ members -> Array<Int8>,
+ private -> Bool,
+ }
+}
+
+diesel::table! {
+ groups (id) {
+ id -> Int8,
+ name -> Text,
+ members -> Array<Int8>,
+ is_dm -> Bool,
+ channels -> Array<Int8>,
+ admin -> Array<Int8>,
+ owner -> Int8,
+ }
+}
+
+diesel::table! {
+ messages (channel) {
+ channel -> Int8,
+ id -> Int8,
+ author -> Int8,
+ content -> Nullable<Text>,
+ thread -> Nullable<Int8>,
+ }
+}
+
+diesel::table! {
+ user_dms (id) {
+ id -> Int8,
+ dms -> Array<Int8>,
+ }
+}
+
+diesel::table! {
+ user_groups (id) {
+ id -> Int8,
+ groups -> Array<Int8>,
+ }
+}
+
+diesel::table! {
+ users (id) {
+ id -> Int8,
+ name -> Text,
+ email -> Text,
+ hash -> Text,
+ }
+}
+
+diesel::allow_tables_to_appear_in_same_query!(
+ channels,
+ groups,
+ messages,
+ user_dms,
+ user_groups,
+ users,
+);