Initial Commit

This commit is contained in:
CanadianBaconBoi 2026-02-17 18:20:02 +01:00
commit e27a0d33d7
110 changed files with 6958 additions and 0 deletions

1
.gitignore vendored Normal file
View File

@ -0,0 +1 @@
/target

8
.idea/.gitignore vendored Normal file
View File

@ -0,0 +1,8 @@
# Default ignored files
/shelf/
/workspace.xml
# Editor-based HTTP Client requests
/httpRequests/
# Datasource local storage ignored files
/dataSources/
/dataSources.local.xml

17
.idea/cove-chat.iml Normal file
View File

@ -0,0 +1,17 @@
<?xml version="1.0" encoding="UTF-8"?>
<module type="EMPTY_MODULE" version="4">
<component name="NewModuleRootManager">
<content url="file://$MODULE_DIR$">
<sourceFolder url="file://$MODULE_DIR$/cove-net/client/src" isTestSource="false" />
<sourceFolder url="file://$MODULE_DIR$/cove-net/common/src" isTestSource="false" />
<sourceFolder url="file://$MODULE_DIR$/cove-net/server/src" isTestSource="false" />
<sourceFolder url="file://$MODULE_DIR$/cove-net/src" isTestSource="false" />
<sourceFolder url="file://$MODULE_DIR$/src" isTestSource="false" />
<sourceFolder url="file://$MODULE_DIR$/cove-db/src" isTestSource="false" />
<sourceFolder url="file://$MODULE_DIR$/bin-test/src" isTestSource="false" />
<excludeFolder url="file://$MODULE_DIR$/target" />
</content>
<orderEntry type="inheritedJdk" />
<orderEntry type="sourceFolder" forTests="false" />
</component>
</module>

8
.idea/modules.xml Normal file
View File

@ -0,0 +1,8 @@
<?xml version="1.0" encoding="UTF-8"?>
<project version="4">
<component name="ProjectModuleManager">
<modules>
<module fileurl="file://$PROJECT_DIR$/.idea/cove-chat.iml" filepath="$PROJECT_DIR$/.idea/cove-chat.iml" />
</modules>
</component>
</project>

23
.idea/sqldialects.xml Normal file
View File

@ -0,0 +1,23 @@
<?xml version="1.0" encoding="UTF-8"?>
<project version="4">
<component name="SqlDialectMappings">
<file url="file://$PROJECT_DIR$/bin-test/migrations/01_user.sql" dialect="PostgreSQL" />
<file url="file://$PROJECT_DIR$/bin-test/migrations/02_messages.sql" dialect="PostgreSQL" />
<file url="file://$PROJECT_DIR$/bin-test/migrations/03_nonce.sql" dialect="PostgreSQL" />
<file url="file://$PROJECT_DIR$/bin-test/migrations/04_channels.sql" dialect="PostgreSQL" />
<file url="file://$PROJECT_DIR$/bin-test/migrations/05_guilds.sql" dialect="PostgreSQL" />
<file url="file://$PROJECT_DIR$/bin-test/migrations/06_guild_members.sql" dialect="PostgreSQL" />
<file url="file://$PROJECT_DIR$/bin-test/migrations/101_user-foreign.sql" dialect="PostgreSQL" />
<file url="file://$PROJECT_DIR$/bin-test/migrations/102_messages-foreign.sql" dialect="PostgreSQL" />
<file url="file://$PROJECT_DIR$/bin-test/migrations/103_nonce-foreign.sql" dialect="PostgreSQL" />
<file url="file://$PROJECT_DIR$/bin-test/migrations/104_channels-foreign.sql" dialect="PostgreSQL" />
<file url="file://$PROJECT_DIR$/bin-test/migrations/105_guilds-foreign.sql" dialect="PostgreSQL" />
<file url="file://$PROJECT_DIR$/bin-test/migrations/106_guild_members-foreign.sql" dialect="PostgreSQL" />
<file url="file://$PROJECT_DIR$/bin-test/system-migrations/201_nonce-management.sql" dialect="PostgreSQL" />
<file url="file://$PROJECT_DIR$/cove-db/migrations/01_user.sql" dialect="PostgreSQL" />
<file url="file://$PROJECT_DIR$/cove-db/migrations/03_nonce.sql" dialect="PostgreSQL" />
<file url="file://$PROJECT_DIR$/cove-db/migrations/105_guilds-foreign.sql" dialect="PostgreSQL" />
<file url="file://$PROJECT_DIR$/cove-db/system-migrations/201_nonce-management.sql" dialect="PostgreSQL" />
<file url="PROJECT" dialect="PostgreSQL" />
</component>
</project>

6
.idea/vcs.xml Normal file
View File

@ -0,0 +1,6 @@
<?xml version="1.0" encoding="UTF-8"?>
<project version="4">
<component name="VcsDirectoryMappings">
<mapping directory="" vcs="Git" />
</component>
</project>

2809
Cargo.lock generated Normal file

File diff suppressed because it is too large Load Diff

28
Cargo.toml Normal file
View File

@ -0,0 +1,28 @@
[workspace]
resolver = "3"
members = [
"cove-net/common",
"cove-net/client",
"cove-net/server"
, "bin-test", "cove-db"]
[workspace.dependencies]
scc = "3.5.6"
async-trait = "0.1.89"
anyhow = "1.0.101"
hyper = { version = "1", features = ["full"] }
http-body-util = { version = "0.1.3", features = ["full"] }
cove-net-common = {path = "cove-net/common"}
serde_json = "1.0.149"
serde_with = "3.16.1"
serde = { version = "1.0.228", features = ["derive"] }
hex = "0.4.3"
cove-net-server = {path = "cove-net/server"}
tokio = { version = "1", features = ["full"] }
hyper-util = { version = "0.1", features = ["full"] }
cove-db = {path = "cove-db"}
sqlx = { version = "0.8.6", features = [ "runtime-tokio", "tls-rustls-ring", "postgres", "time", "uuid", "json", "derive" ]}

13
bin-test/Cargo.toml Normal file
View File

@ -0,0 +1,13 @@
[package]
name = "bin-test"
version = "0.1.0"
edition = "2024"
[dependencies]
cove-net-server.workspace = true
cove-net-common.workspace = true
tokio.workspace = true
cove-db.workspace = true
sqlx.workspace = true
anyhow = "1.0.101"
scc = "3.5.6"

View File

@ -0,0 +1,30 @@
-- user status enum values
DO $$ BEGIN
CREATE TYPE user_status AS ENUM ('online', 'idle', 'dnd', 'offline', 'invisible');
EXCEPTION
WHEN duplicate_object THEN null;
END $$;
-- main users table
CREATE TABLE users (
id BYTEA PRIMARY KEY NOT NULL CHECK(length(id) = 26),
username TEXT NOT NULL CHECK (char_length(username) BETWEEN 1 AND 32),
discriminator TEXT NOT NULL DEFAULT '0000' CHECK (discriminator ~ '^\d{4}$'),
avatar_hash TEXT CHECK (char_length(avatar_hash) <= 64), -- null if no avatar
email TEXT UNIQUE NOT NULL CHECK (email ~* '^[A-Za-z0-9._%+-]+@[A-Za-z0-9.-]+\.[A-Za-z]{2,}$'),
email_verified BOOLEAN NOT NULL DEFAULT FALSE,
password_hash TEXT NOT NULL, -- bcrypt/argon2 hash, never plaintext
mfa_enabled BOOLEAN NOT NULL DEFAULT FALSE,
mfa_secret TEXT CHECK (mfa_enabled = FALSE OR char_length(mfa_secret) > 0), -- encrypted if present
status user_status NOT NULL DEFAULT 'offline',
public_flags BIGINT NOT NULL DEFAULT 0, -- bitmask for flags
locale TEXT NOT NULL DEFAULT 'en-US' CHECK (locale ~ '^[a-z]{2}(-[A-Z]{2})?$'), -- BCP 47 pattern
premium_since TIMESTAMPTZ, -- e.g., Premium subscription start
premium_end TIMESTAMPTZ,
bot BOOLEAN NOT NULL DEFAULT FALSE,
bot_oauth_scopes JSONB NOT NULL DEFAULT '[]'::jsonb, -- e.g., ['bot', 'guilds.join']
preferences JSONB NOT NULL DEFAULT '{}'::jsonb, -- theme, sound, etc.
created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
updated_at TIMESTAMPTZ NOT NULL DEFAULT NOW()
);

View File

@ -0,0 +1,43 @@
CREATE TABLE messages (
id BYTEA NOT NULL CHECK(length(id) = 26),
channel_id BYTEA NOT NULL CHECK(length(channel_id) = 26),
guild_id BYTEA CHECK(length(guild_id) = 26), -- nullable for DMs
author_id BYTEA NOT NULL CHECK(length(author_id) = 26),
-- Message content and metadata
content TEXT NOT NULL DEFAULT '',
timestamp TIMESTAMPTZ NOT NULL DEFAULT NOW(),
edited_timestamp TIMESTAMPTZ,
tts BOOLEAN NOT NULL DEFAULT FALSE,
-- Mentions and embeds (as JSON for flexibility)
mentions BYTEA[], -- array of user/channel/role IDs (could also be TEXT[] if using base58)
mention_everyone BOOLEAN NOT NULL DEFAULT FALSE,
embeds JSONB[],
attachments BYTEA[],
-- Reply/reference data (optional)
reply_message_id BYTEA CHECK(length(reply_message_id) = 26),
application_id BYTEA CHECK(length(application_id) = 26), -- for slash commands
-- System/interaction message type
message_type INTEGER NOT NULL DEFAULT 0, -- 0 = default, 1 = reply, 2 = gateway ping, etc.
-- Thread support
thread_name TEXT,
auto_archive_duration INTEGER, -- minutes (360, 1440, 4320, 10080)
-- Audit & integrity
created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
updated_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
PRIMARY KEY (id)
);
-- Indexes for common query patterns
CREATE INDEX idx_messages_channel_id ON messages (channel_id);
CREATE INDEX idx_messages_channel_id_timestamp ON messages (channel_id, timestamp DESC);
CREATE INDEX idx_messages_author_id ON messages (author_id);
CREATE INDEX idx_messages_guild_id ON messages (guild_id) WHERE guild_id IS NOT NULL;
CREATE INDEX idx_messages_reference_message_id ON messages (reply_message_id);
CREATE INDEX idx_messages_timestamp ON messages (timestamp DESC);

View File

@ -0,0 +1,24 @@
-- Staging table for pending nonces (with TTL)
CREATE TABLE pending_nonces (
nonce TEXT NOT NULL PRIMARY KEY,
channel_id BYTEA NOT NULL CHECK (LENGTH(channel_id) = 26),
author_id BYTEA NOT NULL CHECK (LENGTH(author_id) = 26),
created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
expires_at TIMESTAMPTZ NOT NULL
);
-- Index for expiry queries
CREATE INDEX idx_pending_nonces_expires_at ON pending_nonces (expires_at);
CREATE OR REPLACE FUNCTION set_nonce_expiry()
RETURNS TRIGGER AS $$
BEGIN
NEW.expires_at := NOW() + INTERVAL '5 minutes';
RETURN NEW;
END;
$$ LANGUAGE plpgsql;
CREATE TRIGGER trig_pending_nonces_expiry
BEFORE INSERT ON pending_nonces
FOR EACH ROW
EXECUTE FUNCTION set_nonce_expiry();

View File

@ -0,0 +1,44 @@
CREATE TABLE channels (
id BYTEA NOT NULL PRIMARY KEY CHECK(length(id) = 26),
guild_id BYTEA CHECK(length(guild_id) = 26), -- NULL for DMs & group DMs
parent_id BYTEA CHECK(length(parent_id) = 26), -- for thread categories / channel groups
-- Core identity & type
name TEXT NOT NULL CHECK (LENGTH(name) >= 1 AND LENGTH(name) <= 100),
channel_type INTEGER NOT NULL DEFAULT 0, -- 0 = text, 1 = voice, 2 = category, 5 = news, etc.
-- Permissions & visibility
position INTEGER NOT NULL DEFAULT 0,
permission_overwrites JSONB NOT NULL DEFAULT '[]'::jsonb, -- array of overwrites
rate_limit_per_user INTEGER NOT NULL DEFAULT 0, -- slowmode in seconds (021600)
-- NSFW & visibility flags
nsfw BOOLEAN NOT NULL DEFAULT FALSE,
loud BOOLEAN NOT NULL DEFAULT FALSE, -- voice channel: triggers notifications
-- Thread-specific fields (for threads spawned from messages)
thread_metadata JSONB, -- { "archived": bool, "auto_archive_duration": int, "archive_timestamp": timestamptz, "locked": bool }
member_count INTEGER NOT NULL DEFAULT 0, -- approximate member count (not real-time)
message_count INTEGER NOT NULL DEFAULT 0, -- cached message count (for UI previews)
thread_owner_id BYTEA CHECK(length(thread_owner_id) = 26),
-- System metadata
created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
updated_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
-- Topic (for news, text channels)
topic TEXT CHECK (LENGTH(COALESCE(topic, '')) <= 1024),
-- Voice-specific
user_limit INTEGER NOT NULL DEFAULT 0, -- 0 = no limit
region TEXT -- voice region (e.g., "us-west"), NULL = automatic
);
-- Critical indexes for common operations
CREATE INDEX idx_channels_guild_id ON channels (guild_id) WHERE guild_id IS NOT NULL;
CREATE INDEX idx_channels_guild_id_position ON channels (guild_id, position);
CREATE INDEX idx_channels_parent_id ON channels (parent_id) WHERE parent_id IS NOT NULL;
CREATE INDEX idx_channels_channel_type ON channels (channel_type);
-- Optional: cover index for message listing (join + sort)
CREATE INDEX idx_channels_guild_id_created_at ON channels (guild_id, created_at) WHERE guild_id IS NOT NULL;

View File

@ -0,0 +1,56 @@
CREATE TABLE guilds (
id BYTEA NOT NULL PRIMARY KEY CHECK(length(id) = 26),
-- Core identity
name TEXT NOT NULL CHECK (LENGTH(name) >= 2 AND LENGTH(name) <= 100),
description TEXT CHECK (LENGTH(COALESCE(description, '')) <= 1024),
icon BYTEA CHECK(length(icon) = 26),
banner BYTEA CHECK(length(banner) = 26),
splash BYTEA CHECK(length(splash) = 26),
-- Ownership & verification
owner_id BYTEA NOT NULL CHECK(length(owner_id) = 26),
owner_permissions BYTEA NOT NULL DEFAULT 'xFFFFFFFFFFFFFFFF'::BYTEA, -- 8-byte bitmask (e.g., ADMINISTRATOR = 0x8)
-- Regions & voice
region TEXT NOT NULL DEFAULT 'us-west', -- voice region (e.g., 'us-west', 'eu-central')
-- Features (bitmask of enabled features)
features INTEGER NOT NULL DEFAULT 0, -- 0 = basic, 1 = ANIMATED_ICON, 2 = BANNER, 4 = COMMERCE, 8 = PUBLIC, etc.
-- Discovery & visibility
afk_channel_id BYTEA CHECK(length(afk_channel_id) = 26),
afk_timeout INTEGER NOT NULL DEFAULT 300, -- seconds (60, 300, 900, 1800, 3600)
verification_level INTEGER NOT NULL DEFAULT 0, -- 0 = none, 1 = low, 2 = medium, 3 = high, 4 = highest
default_message_notifications INTEGER NOT NULL DEFAULT 1, -- 0 = all, 1 = mentions only
-- Explicit content filter
explicit_content_filter INTEGER NOT NULL DEFAULT 0, -- 0 = disabled, 1 = members without role, 2 = all
-- System channels
system_channel_id BYTEA CHECK(length(system_channel_id) = 26),
system_channel_flags INTEGER NOT NULL DEFAULT 0, -- 1 = SUPPRESS_JOIN_NOTIFICATIONS, 2 = SUPPRESS_PREMIUM_SUBSCRIPTIONS, etc.
-- Boosting & nitro
premium_boosters BYTEA[] NOT NULL DEFAULT ARRAY[]::BYTEA[],
premium_tier INTEGER NOT NULL DEFAULT 0, -- 0 = None, 1 = Tier 1, 2 = Tier 2, 3 = Tier 3
premium_booster_count INTEGER NOT NULL DEFAULT 0, -- cached boost count
-- Safety & moderation
widget_enabled BOOLEAN NOT NULL DEFAULT FALSE,
widget_channel_id BYTEA CHECK(length(widget_channel_id) = 26),
preferred_locale TEXT NOT NULL DEFAULT 'en-US', -- IETF BCP 47 language tag
-- Audit & integrity
created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
updated_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
large BOOLEAN NOT NULL DEFAULT FALSE, -- >150 members
member_count INTEGER NOT NULL DEFAULT 0 -- cached member count
);
-- Critical indexes
CREATE INDEX idx_guilds_owner_id ON guilds (owner_id);
CREATE INDEX idx_guilds_region ON guilds (region);
CREATE INDEX idx_guilds_verification_level ON guilds (verification_level);
CREATE INDEX idx_guilds_large ON guilds (large) WHERE large = TRUE; -- for pagination
CREATE INDEX idx_guilds_created_at ON guilds (created_at DESC);

View File

@ -0,0 +1,36 @@
CREATE TABLE guild_members (
guild_id BYTEA NOT NULL CHECK(length(guild_id) = 26),
user_id BYTEA NOT NULL CHECK(length(user_id) = 26),
-- Core identity
nick TEXT CHECK (LENGTH(COALESCE(nick, '')) <= 32), -- display name in guild (NULL = uses user global name)
joined_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
-- Roles & permissions
roles BYTEA[] NOT NULL DEFAULT ARRAY[]::BYTEA[], -- array of role IDs (26-byte each)
boosting_since TIMESTAMPTZ, -- when they started boosting (NULL = not boosting)
-- Voice state (lightweight caching)
voice_channel_id BYTEA CHECK(length(voice_channel_id) = 26),
deafened BOOLEAN NOT NULL DEFAULT FALSE,
muted BOOLEAN NOT NULL DEFAULT FALSE,
-- Moderation & management
pending BOOLEAN NOT NULL DEFAULT FALSE, -- requires membership screening
timed_out_until TIMESTAMPTZ, -- NULL = not timed out
-- Audit & integrity
updated_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
-- Composite primary key
PRIMARY KEY (guild_id, user_id)
);
-- Critical indexes
CREATE INDEX idx_guild_members_user_id ON guild_members (user_id);
CREATE INDEX idx_guild_members_guild_id ON guild_members (guild_id);
CREATE INDEX idx_guild_members_roles ON guild_members USING GIN (roles); -- for role-based lookups
CREATE INDEX idx_guild_members_voice_channel_id ON guild_members (voice_channel_id) WHERE voice_channel_id IS NOT NULL;
-- For quick "all members in guild" queries (covering index)
CREATE INDEX idx_guild_members_guild_id_nick ON guild_members (guild_id, nick);

View File

@ -0,0 +1,9 @@
CREATE TABLE attachments (
id BYTEA NOT NULL CHECK(length(id) = 26),
uploader_id BYTEA NOT NULL CHECK(length(uploader_id) = 26),
channel_id BYTEA CHECK(length(channel_id) = 26),
file_size INT NOT NULL,
cdn_url TEXT NOT NULL,
PRIMARY KEY(id)
)

View File

View File

@ -0,0 +1,5 @@
ALTER TABLE messages
ADD FOREIGN KEY (channel_id) REFERENCES channels(id) ON DELETE CASCADE,
ADD FOREIGN KEY (author_id) REFERENCES users(id) ON DELETE CASCADE,
ADD FOREIGN KEY (guild_id) REFERENCES guilds(id) ON DELETE CASCADE,
ADD FOREIGN KEY (reply_message_id) REFERENCES messages(id) ON DELETE SET NULL

View File

@ -0,0 +1,4 @@
ALTER TABLE channels
ADD FOREIGN KEY (guild_id) REFERENCES guilds(id) ON DELETE CASCADE,
ADD FOREIGN KEY (parent_id) REFERENCES channels(id) ON DELETE SET NULL,
ADD FOREIGN KEY (thread_owner_id) REFERENCES users(id) ON DELETE SET NULL

View File

@ -0,0 +1,32 @@
ALTER TABLE guilds
ADD FOREIGN KEY (owner_id) REFERENCES users(id) ON DELETE CASCADE,
ADD FOREIGN KEY (afk_channel_id) REFERENCES channels(id) ON DELETE SET NULL,
ADD FOREIGN KEY (system_channel_id) REFERENCES channels(id) ON DELETE SET NULL,
ADD FOREIGN KEY (widget_channel_id) REFERENCES channels(id) ON DELETE SET NULL;
-- For "my guilds" query
CREATE INDEX idx_guild_members_guild_id_user_id ON guild_members (guild_id, user_id);
-- Ensure member_count is updated on membership changes
CREATE OR REPLACE FUNCTION update_guild_member_counts()
RETURNS TRIGGER AS $$
DECLARE
new_count INTEGER;
BEGIN
IF TG_OP = 'INSERT' THEN
new_count := (SELECT COUNT(*) FROM guild_members WHERE guild_id = NEW.guild_id);
UPDATE guilds SET member_count = new_count, large = (new_count > 150) WHERE id = NEW.guild_id;
RETURN NEW;
ELSIF TG_OP = 'DELETE' THEN
new_count := (SELECT COUNT(*) FROM guild_members WHERE guild_id = OLD.guild_id);
UPDATE guilds SET member_count = new_count, large = (new_count > 150) WHERE id = OLD.guild_id;
RETURN OLD;
END IF;
RETURN NULL;
END;
$$ LANGUAGE plpgsql;
CREATE TRIGGER update_guild_counts_on_member_change
AFTER INSERT OR DELETE ON guild_members
FOR EACH ROW
EXECUTE FUNCTION update_guild_member_counts();

View File

@ -0,0 +1,4 @@
ALTER TABLE guild_members
ADD FOREIGN KEY (guild_id) REFERENCES guilds(id) ON DELETE CASCADE,
ADD FOREIGN KEY (user_id) REFERENCES users(id) ON DELETE CASCADE,
ADD FOREIGN KEY (voice_channel_id) REFERENCES channels(id) ON DELETE SET NULL

View File

@ -0,0 +1,3 @@
ALTER TABLE attachments
ADD FOREIGN KEY (uploader_id) REFERENCES users(id) ON DELETE CASCADE,
ADD FOREIGN KEY (channel_id) REFERENCES channels(id) ON DELETE CASCADE

116
bin-test/src/main.rs Normal file
View File

@ -0,0 +1,116 @@
use cove_net_server::message::middleware::auth::AuthTokenMiddleware;
use std::net::{IpAddr, Ipv4Addr};
use std::sync::{Arc};
use std::time::Duration;
use sqlx::{Execute, Executor};
use sqlx::postgres::PgQueryResult;
use sqlx::types::time::OffsetDateTime;
use cove_db::{CoveDB, CoveDBImpl};
use cove_db::part::{BindQueryBuilder, SqlPart};
use cove_db::part::condition::ConditionType;
use cove_db::rows::{InsertableRow, SelectableRow, TableRow, WhereRow};
use cove_db::rows::user::{PartialUserRow, UserRow};
use cove_db::types::user_status::UserStatus;
use cove_net_common::id::message_type::MessageType;
use cove_net_common::id::SnowflakeID;
use cove_net_server::{register_routes, CoveServer, DatabaseMiddleware, RootHandler};
use cove_net_server::message::handlers::account::login::LoginMessageHandler;
use cove_net_server::message::handlers::account::register::RegisterMessageHandler;
use cove_net_server::message::handlers::Handler;
use cove_net_server::message::handlers::text::attachment::AttachmentMessageHandler;
use cove_net_server::message::handlers::text::reaction::ReactionMessageHandler;
use cove_net_server::message::handlers::text::text::TextMessageHandler;
#[tokio::main]
async fn main() -> Result<(), anyhow::Error> {
let db = CoveDB::new("localhost", 5432, "postgres", "password", "testdata").await?;
db.run_migrations().await?;
db.run_system_migrations().await?;
// let user_row = UserRow {
// id: SnowflakeID::new_random_hex_loc(MessageType::User, "beefcafe")?,
// username: "CanadianBacon".to_string(),
// discriminator: "0001".to_string(),
// avatar_hash: None,
// email: "bc.bacon.bits@gmail.com".to_string(),
// email_verified: true,
// password_hash: "1802vgu12890n7b489127".to_string(),
// mfa_enabled: false,
// mfa_secret: None,
// status: UserStatus::Online,
// public_flags: 0,
// locale: "en-US".to_string(),
// premium_since: Some(OffsetDateTime::now_utc() + Duration::from_hours(1)),
// premium_end: Some(OffsetDateTime::now_utc() + Duration::from_hours(24)),
// bot: false,
// bot_oauth_scopes: Default::default(),
// preferences: Default::default(),
// created_at: OffsetDateTime::now_utc(),
// updated_at: OffsetDateTime::now_utc(),
// };
//
// let mut query_builder = BindQueryBuilder::new();
// user_row.insert().encode(&mut query_builder)?;
//
// let query = query_builder.to_query();
// let query = user_row.bind(query)?;
//
// let res = db.run_query::<PgQueryResult>(query.sql_query).await?;
// println!("{} rows affected", res.rows_affected());
let request_row: <UserRow as TableRow>::PartialRow = PartialUserRow {
username: Some("CanadianBacon".to_string()),
..Default::default()
};
let mut query_builder = BindQueryBuilder::new();
request_row.select(vec!["id", "username"]).encode(&mut query_builder)?;
let sql_where = request_row.wheres(|w|{
w.cond_and::<String>("username", ConditionType::Equal(false))
})?;
sql_where.encode(&mut query_builder)?;
let query = request_row.bind(sql_where, query_builder.to_query())?;
println!("{}", query.sql_query.sql());
let out_partial_row: <UserRow as TableRow>::PartialRow = db.get_pool().fetch_one(query.sql_query).await?.into();
println!("{:?}, {:?}, {:?}", out_partial_row.id, out_partial_row.username, out_partial_row.email);
http_testing(db).await?;
Ok(())
}
async fn http_testing(db: CoveDB) -> Result<(), anyhow::Error> {
let mut root_handler = Handler::new(Box::new(RootHandler));
root_handler.add_middleware(AuthTokenMiddleware).await?;
root_handler.add_middleware(DatabaseMiddleware::new(Arc::new(db))).await?;
register_routes!(&mut root_handler,
"text" => TextMessageHandler => {
"attachment" => AttachmentMessageHandler,
"reaction" => ReactionMessageHandler
},
"login" => LoginMessageHandler,
"register" => RegisterMessageHandler
).await?;
let cove_server = match CoveServer::new(
IpAddr::V4(Ipv4Addr::new(127, 0, 0, 1)),
8088, Arc::new(root_handler)
).await {
Ok(server) => {server}
Err(err) => {panic!("Failed to construct CoveServer {}", err);}
};
match cove_server.run().await {
Ok(_) => {
println!("Cove server exited successfully!");
Ok(())
}
Err(err) => {
Err(err)
}
}
}

View File

@ -0,0 +1,21 @@
INSERT INTO pgagent.pga_jobclass (jclname)
VALUES ('Cleanup Nonces');
INSERT INTO pgagent.pga_job (jobjclid, jobname, jobdesc, jobenabled, jobhostagent)
SELECT jcl.jclid, 'Cleanup Pending Nonces', 'Remove expired pending_nonces rows older than 5 minutes', true, ''
from pgagent.pga_jobclass jcl WHERE jclname='Cleanup Nonces';
INSERT INTO pgagent.pga_jobstep (jstjobid, jstname, jstdesc, jstenabled, jstkind, jstonerror, jstcode, jstdbname)
SELECT job.jobid, 'Perform Cleanup', 'Delete pending nonces', true, 's', 'f', $$DELETE FROM pending_nonces WHERE expires_at < NOW()$$, 'testdata'
FROM pgagent.pga_job job where jobname='Cleanup Pending Nonces';
INSERT INTO pgagent.pga_schedule (jscjobid, jscname, jscenabled, jscstart, jscminutes)
VALUES (
(SELECT jobid
from pgagent.pga_job
where jobname = 'Cleanup Pending Nonces'),
'Every 5 minutes',
true,
CURRENT_TIMESTAMP, -- start anytime
'{t,f,f,f,f,t,f,f,f,f,t,f,f,f,f,t,f,f,f,f,t,f,f,f,f,t,f,f,f,f,t,f,f,f,f,t,f,f,f,f,t,f,f,f,f,t,f,f,f,f,t,f,f,f,f,t,f,f,f,f}' -- every 5th minute
);

1
cove-db/.env Normal file
View File

@ -0,0 +1 @@
DATABASE_URL="postgres://postgres:password@localhost:5432/"

11
cove-db/Cargo.toml Normal file
View File

@ -0,0 +1,11 @@
[package]
name = "cove-db"
version = "0.1.0"
edition = "2024"
[dependencies]
sqlx.workspace = true
anyhow.workspace = true
serde_json.workspace = true
cove-net-common.workspace = true

View File

@ -0,0 +1,30 @@
-- user status enum values
DO $$ BEGIN
CREATE TYPE user_status AS ENUM ('online', 'idle', 'dnd', 'offline', 'invisible');
EXCEPTION
WHEN duplicate_object THEN null;
END $$;
-- main users table
CREATE TABLE users (
id BYTEA PRIMARY KEY NOT NULL CHECK(length(id) = 26),
username TEXT NOT NULL CHECK (char_length(username) BETWEEN 1 AND 32),
discriminator TEXT NOT NULL DEFAULT '0000' CHECK (discriminator ~ '^\d{4}$'),
avatar_hash BYTEA CHECK(length(id) = 26), -- null if no avatar
email TEXT UNIQUE CHECK (email ~* '^[A-Za-z0-9._%+-]+@[A-Za-z0-9.-]+\.[A-Za-z]{2,}$'),
email_verified BOOLEAN NOT NULL DEFAULT FALSE,
password_hash TEXT NOT NULL, -- bcrypt/argon2 hash, never plaintext
mfa_enabled BOOLEAN NOT NULL DEFAULT FALSE,
mfa_secret TEXT CHECK (mfa_enabled = FALSE OR char_length(mfa_secret) > 0), -- encrypted if present
status user_status NOT NULL DEFAULT 'offline',
public_flags BIGINT NOT NULL DEFAULT 0, -- bitmask for flags
locale TEXT NOT NULL DEFAULT 'en-US' CHECK (locale ~ '^[a-z]{2}(-[A-Z]{2})?$'), -- BCP 47 pattern
premium_since TIMESTAMPTZ, -- e.g., Premium subscription start
premium_end TIMESTAMPTZ,
bot BOOLEAN NOT NULL DEFAULT FALSE,
bot_oauth_scopes JSONB NOT NULL DEFAULT '[]'::jsonb, -- e.g., ['bot', 'guilds.join']
preferences JSONB NOT NULL DEFAULT '{}'::jsonb, -- theme, sound, etc.
created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
updated_at TIMESTAMPTZ NOT NULL DEFAULT NOW()
);

View File

@ -0,0 +1,43 @@
CREATE TABLE messages (
id BYTEA NOT NULL CHECK(length(id) = 26),
channel_id BYTEA NOT NULL CHECK(length(channel_id) = 26),
guild_id BYTEA CHECK(length(guild_id) = 26), -- nullable for DMs
author_id BYTEA NOT NULL CHECK(length(author_id) = 26),
-- Message content and metadata
content TEXT NOT NULL DEFAULT '',
timestamp TIMESTAMPTZ NOT NULL DEFAULT NOW(),
edited_timestamp TIMESTAMPTZ,
tts BOOLEAN NOT NULL DEFAULT FALSE,
-- Mentions and embeds (as JSON for flexibility)
mentions BYTEA[], -- array of user IDs (could also be TEXT[] if using base58)
mention_everyone BOOLEAN NOT NULL DEFAULT FALSE,
embeds JSONB NOT NULL DEFAULT '[]'::jsonb,
attachments BYTEA[],
-- Reply/reference data (optional)
reply_message_id BYTEA CHECK(length(reply_message_id) = 26),
application_id BYTEA CHECK(length(application_id) = 26), -- for slash commands
-- System/interaction message type
message_type INTEGER NOT NULL DEFAULT 0, -- 0 = default, 1 = reply, 2 = gateway ping, etc.
-- Thread support
thread_name TEXT,
auto_archive_duration INTEGER, -- minutes (360, 1440, 4320, 10080)
-- Audit & integrity
created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
updated_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
PRIMARY KEY (id)
);
-- Indexes for common query patterns
CREATE INDEX idx_messages_channel_id ON messages (channel_id);
CREATE INDEX idx_messages_channel_id_timestamp ON messages (channel_id, timestamp DESC);
CREATE INDEX idx_messages_author_id ON messages (author_id);
CREATE INDEX idx_messages_guild_id ON messages (guild_id) WHERE guild_id IS NOT NULL;
CREATE INDEX idx_messages_reference_message_id ON messages (reply_message_id);
CREATE INDEX idx_messages_timestamp ON messages (timestamp DESC);

View File

@ -0,0 +1,24 @@
-- Staging table for pending nonces (with TTL)
CREATE TABLE pending_nonces (
nonce TEXT NOT NULL PRIMARY KEY,
channel_id BYTEA NOT NULL CHECK (LENGTH(channel_id) = 26),
author_id BYTEA NOT NULL CHECK (LENGTH(author_id) = 26),
created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
expires_at TIMESTAMPTZ NOT NULL
);
-- Index for expiry queries
CREATE INDEX idx_pending_nonces_expires_at ON pending_nonces (expires_at);
CREATE OR REPLACE FUNCTION set_nonce_expiry()
RETURNS TRIGGER AS $$
BEGIN
NEW.expires_at := NOW() + INTERVAL '5 minutes';
RETURN NEW;
END;
$$ LANGUAGE plpgsql;
CREATE TRIGGER trig_pending_nonces_expiry
BEFORE INSERT ON pending_nonces
FOR EACH ROW
EXECUTE FUNCTION set_nonce_expiry();

View File

@ -0,0 +1,45 @@
CREATE TABLE channels (
id BYTEA NOT NULL PRIMARY KEY CHECK(length(id) = 26),
guild_id BYTEA CHECK(length(guild_id) = 26), -- NULL for DMs & group DMs
parent_id BYTEA CHECK(length(parent_id) = 26), -- for thread categories / channel groups
-- Core identity & type
name TEXT NOT NULL CHECK (LENGTH(name) >= 1 AND LENGTH(name) <= 100),
channel_type INTEGER NOT NULL DEFAULT 0, -- 0 = text, 1 = voice, 2 = category, 5 = news, etc.
-- Permissions & visibility
position INTEGER NOT NULL DEFAULT 0,
permission_overwrites JSONB NOT NULL DEFAULT '[]'::jsonb, -- array of overwrites
rate_limit_per_user INTEGER NOT NULL DEFAULT 0, -- slowmode in seconds (021600)
-- NSFW & visibility flags
nsfw BOOLEAN NOT NULL DEFAULT FALSE,
loud BOOLEAN NOT NULL DEFAULT FALSE, -- voice channel: triggers notifications
-- Thread-specific fields (for threads spawned from messages)
thread_metadata JSONB, -- { "archived": bool, "auto_archive_duration": int, "archive_timestamp": timestamptz, "locked": bool }
member_count INTEGER NOT NULL DEFAULT 0, -- approximate member count (not real-time)
message_count INTEGER NOT NULL DEFAULT 0, -- cached message count (for UI previews)
thread_owner_id BYTEA CHECK(length(thread_owner_id) = 26),
-- System metadata
created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
updated_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
-- Topic (for news, text channels)
topic TEXT CHECK (LENGTH(COALESCE(topic, '')) <= 1024),
-- Voice-specific
user_limit INTEGER NOT NULL DEFAULT 0, -- 0 = no limit
region TEXT -- voice region (e.g., "us-west"), NULL = automatic
);
-- Critical indexes for common operations
CREATE INDEX idx_channels_guild_id ON channels (guild_id) WHERE guild_id IS NOT NULL;
CREATE INDEX idx_channels_guild_id_position ON channels (guild_id, position);
CREATE INDEX idx_channels_parent_id ON channels (parent_id) WHERE parent_id IS NOT NULL;
CREATE INDEX idx_channels_channel_type ON channels (channel_type);
CREATE UNIQUE INDEX uidx_channels_channel_pos ON channels (guild_id, position);
-- Optional: cover index for message listing (join + sort)
CREATE INDEX idx_channels_guild_id_created_at ON channels (guild_id, created_at) WHERE guild_id IS NOT NULL;

View File

@ -0,0 +1,56 @@
CREATE TABLE guilds (
id BYTEA NOT NULL PRIMARY KEY CHECK(length(id) = 26),
-- Core identity
name TEXT NOT NULL CHECK (LENGTH(name) >= 2 AND LENGTH(name) <= 100),
description TEXT CHECK (LENGTH(COALESCE(description, '')) <= 1024),
icon BYTEA CHECK(length(icon) = 26),
banner BYTEA CHECK(length(banner) = 26),
splash BYTEA CHECK(length(splash) = 26),
-- Ownership & verification
owner_id BYTEA NOT NULL CHECK(length(owner_id) = 26),
owner_permissions BYTEA NOT NULL DEFAULT 'xFFFFFFFFFFFFFFFF'::BYTEA, -- 8-byte bitmask (e.g., ADMINISTRATOR = 0x8)
-- Regions & voice
region TEXT NOT NULL DEFAULT 'us-west', -- voice region (e.g., 'us-west', 'eu-central')
-- Features (bitmask of enabled features)
features INTEGER NOT NULL DEFAULT 0, -- 0 = basic, 1 = ANIMATED_ICON, 2 = BANNER, 4 = COMMERCE, 8 = PUBLIC, etc.
-- Discovery & visibility
afk_channel_id BYTEA CHECK(length(afk_channel_id) = 26),
afk_timeout INTEGER NOT NULL DEFAULT 300, -- seconds (60, 300, 900, 1800, 3600)
verification_level INTEGER NOT NULL DEFAULT 0, -- 0 = none, 1 = low, 2 = medium, 3 = high, 4 = highest
default_message_notifications INTEGER NOT NULL DEFAULT 1, -- 0 = all, 1 = mentions only
-- Explicit content filter
explicit_content_filter INTEGER NOT NULL DEFAULT 0, -- 0 = disabled, 1 = members without role, 2 = all
-- System channels
system_channel_id BYTEA CHECK(length(system_channel_id) = 26),
system_channel_flags INTEGER NOT NULL DEFAULT 0, -- 1 = SUPPRESS_JOIN_NOTIFICATIONS, 2 = SUPPRESS_PREMIUM_SUBSCRIPTIONS, etc.
-- Boosting & nitro
premium_boosters INTEGER NOT NULL DEFAULT 0,
premium_tier INTEGER NOT NULL DEFAULT 0, -- 0 = None, 1 = Tier 1, 2 = Tier 2, 3 = Tier 3
premium_subscription_count INTEGER NOT NULL DEFAULT 0, -- cached boost count
-- Safety & moderation
widget_enabled BOOLEAN NOT NULL DEFAULT FALSE,
widget_channel_id BYTEA CHECK(length(widget_channel_id) = 26),
preferred_locale TEXT NOT NULL DEFAULT 'en-US', -- IETF BCP 47 language tag
-- Audit & integrity
created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
joined_at TIMESTAMPTZ, -- when bot joined (if applicable)
large BOOLEAN NOT NULL DEFAULT FALSE, -- >150 members
member_count INTEGER NOT NULL DEFAULT 0 -- cached member count
);
-- Critical indexes
CREATE INDEX idx_guilds_owner_id ON guilds (owner_id);
CREATE INDEX idx_guilds_region ON guilds (region);
CREATE INDEX idx_guilds_verification_level ON guilds (verification_level);
CREATE INDEX idx_guilds_large ON guilds (large) WHERE large = TRUE; -- for pagination
CREATE INDEX idx_guilds_created_at ON guilds (created_at DESC);

View File

@ -0,0 +1,36 @@
CREATE TABLE guild_members (
guild_id BYTEA NOT NULL CHECK(length(guild_id) = 26),
user_id BYTEA NOT NULL CHECK(length(user_id) = 26),
-- Core identity
nick TEXT CHECK (LENGTH(COALESCE(nick, '')) <= 32), -- display name in guild (NULL = uses user global name)
joined_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
-- Roles & permissions
roles BYTEA[] NOT NULL DEFAULT ARRAY[]::BYTEA[], -- array of role IDs (26-byte each)
boosting_since TIMESTAMPTZ, -- when they started boosting (NULL = not boosting)
-- Voice state (lightweight caching)
voice_channel_id BYTEA CHECK(length(voice_channel_id) = 26),
deafened BOOLEAN NOT NULL DEFAULT FALSE,
muted BOOLEAN NOT NULL DEFAULT FALSE,
-- Moderation & management
pending BOOLEAN NOT NULL DEFAULT FALSE, -- requires membership screening
timed_out_until TIMESTAMPTZ, -- NULL = not timed out
-- Audit & integrity
updated_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
-- Composite primary key
PRIMARY KEY (guild_id, user_id)
);
-- Critical indexes
CREATE INDEX idx_guild_members_user_id ON guild_members (user_id);
CREATE INDEX idx_guild_members_guild_id ON guild_members (guild_id);
CREATE INDEX idx_guild_members_roles ON guild_members USING GIN (roles); -- for role-based lookups
CREATE INDEX idx_guild_members_voice_channel_id ON guild_members (voice_channel_id) WHERE voice_channel_id IS NOT NULL;
-- For quick "all members in guild" queries (covering index)
CREATE INDEX idx_guild_members_guild_id_nick ON guild_members (guild_id, nick);

View File

View File

@ -0,0 +1,5 @@
ALTER TABLE messages
ADD FOREIGN KEY (channel_id) REFERENCES channels(id) ON DELETE CASCADE,
ADD FOREIGN KEY (author_id) REFERENCES users(id) ON DELETE CASCADE,
ADD FOREIGN KEY (guild_id) REFERENCES guilds(id) ON DELETE CASCADE,
ADD FOREIGN KEY (reply_message_id) REFERENCES messages(id) ON DELETE SET NULL

View File

View File

@ -0,0 +1,4 @@
ALTER TABLE channels
ADD FOREIGN KEY (guild_id) REFERENCES guilds(id) ON DELETE CASCADE,
ADD FOREIGN KEY (parent_id) REFERENCES channels(id) ON DELETE SET NULL,
ADD FOREIGN KEY (thread_owner_id) REFERENCES users(id) ON DELETE SET NULL

View File

@ -0,0 +1,32 @@
ALTER TABLE guilds
ADD FOREIGN KEY (owner_id) REFERENCES users(id) ON DELETE CASCADE,
ADD FOREIGN KEY (afk_channel_id) REFERENCES channels(id) ON DELETE SET NULL,
ADD FOREIGN KEY (system_channel_id) REFERENCES channels(id) ON DELETE SET NULL,
ADD FOREIGN KEY (widget_channel_id) REFERENCES channels(id) ON DELETE SET NULL;
-- For "my guilds" query
CREATE INDEX idx_guild_members_guild_id_user_id ON guild_members (guild_id, user_id);
-- Ensure member_count is updated on membership changes
CREATE OR REPLACE FUNCTION update_guild_member_counts()
RETURNS TRIGGER AS $$
DECLARE
new_count INTEGER;
BEGIN
IF TG_OP = 'INSERT' THEN
new_count := (SELECT COUNT(*) FROM guild_members WHERE guild_id = NEW.guild_id);
UPDATE guilds SET member_count = new_count, large = (new_count > 150) WHERE id = NEW.guild_id;
RETURN NEW;
ELSIF TG_OP = 'DELETE' THEN
new_count := (SELECT COUNT(*) FROM guild_members WHERE guild_id = OLD.guild_id);
UPDATE guilds SET member_count = new_count, large = (new_count > 150) WHERE id = OLD.guild_id;
RETURN OLD;
END IF;
RETURN NULL;
END;
$$ LANGUAGE plpgsql;
CREATE TRIGGER update_guild_counts_on_member_change
AFTER INSERT OR DELETE ON guild_members
FOR EACH ROW
EXECUTE FUNCTION update_guild_member_counts();

View File

@ -0,0 +1,4 @@
ALTER TABLE guild_members
ADD FOREIGN KEY (guild_id) REFERENCES guilds(id) ON DELETE CASCADE,
ADD FOREIGN KEY (user_id) REFERENCES users(id) ON DELETE CASCADE,
ADD FOREIGN KEY (voice_channel_id) REFERENCES channels(id) ON DELETE SET NULL

78
cove-db/src/lib.rs Normal file
View File

@ -0,0 +1,78 @@
pub mod query;
pub mod rows;
pub mod part;
pub mod types;
use anyhow::Context;
use sqlx::{PgConnection, PgPool, Postgres};
use sqlx::migrate::Migrator;
use sqlx::postgres::{PgArguments, PgConnectOptions, PgPoolOptions, PgQueryResult};
use sqlx::query::Query;
pub struct CoveDB {
pg_connect_options: PgConnectOptions,
db_pool: PgPool,
}
impl CoveDB {
pub async fn new(host: &str, port: u16, username: &str, password: &str, database: &str) -> Result<CoveDB, anyhow::Error> {
let pg_connect_options = <PgConnection as sqlx::Connection>::Options::new()
.host(host)
.port(port)
.username(username)
.password(password)
.database(database);
let db_pool = CoveDB::create_pool(10, pg_connect_options.clone()).await?;
let db = CoveDB {
pg_connect_options,
db_pool
};
Ok(db)
}
async fn create_pool(max_connections: u32, options: PgConnectOptions) -> Result<PgPool, anyhow::Error> {
PgPoolOptions::new()
.max_connections(max_connections)
.connect_with(options)
.await
.context("Failed to create database connection")
}
pub async fn run_migrations(&self) -> Result<(), anyhow::Error> {
let m = Migrator::new(std::path::Path::new("./migrations")).await?;
m.run(&self.db_pool).await?;
Ok(())
}
pub async fn run_system_migrations(&self) -> Result<(), anyhow::Error> {
let m = Migrator::new(std::path::Path::new("./system-migrations")).await?;
let db_pool = CoveDB::create_pool(5, self.pg_connect_options.clone().database("postgres")).await?;
m.run(&db_pool).await?;
Ok(())
}
}
impl CoveDBImpl for CoveDB {
fn get_pool(&self) -> &PgPool {
&self.db_pool
}
}
pub trait CoveDBImpl {
fn get_pool(&self) -> &PgPool;
async fn run_query<T: From<PgQueryResult>>(&self, query: Query<'_, Postgres, PgArguments>) -> Result<T, anyhow::Error> {
let result = query.execute(self.get_pool())
.await
.context("Failed to execute query")?;
Ok(result.into())
}
}

View File

@ -0,0 +1,83 @@
use std::marker::PhantomData;
use sqlx::{Encode, Postgres, Type};
use crate::part::{BindQueryBuilder, SqlPart};
#[derive(Clone)]
pub enum ConditionType {
GreaterThan,
GreaterOrEqual,
LessThan,
LessOrEqual,
Equal(bool),
In(bool),
Between(bool),
Null(bool),
Like(bool),
Exists(bool),
}
#[derive(Clone)]
pub enum ConditionJoin {
And,
Or
}
impl Into<&str> for ConditionType {
fn into(self) -> &'static str {
match self {
ConditionType::GreaterThan => {" > "}
ConditionType::GreaterOrEqual => {" >= "}
ConditionType::LessThan => {" < "}
ConditionType::LessOrEqual => {" <= "}
ConditionType::Equal(not) => {
if not { " != " } else { " = " }
}
ConditionType::In(not) => {
if not { " NOT IN " } else { " IN " }
}
ConditionType::Between(not) => {
if not { " NOT BETWEEN " } else { " BETWEEN " }
}
ConditionType::Null(not) => {
if not { " NOT NULL " } else { " NULL " }
}
ConditionType::Like(not) => {
if not { " NOT LIKE " } else { " LIKE " }
}
ConditionType::Exists(not) => {
if not { " NOT EXISTS " } else { " EXISTS " }
}
}
}
}
impl Into<&str> for ConditionJoin {
fn into(self) -> &'static str {
match self {
ConditionJoin::And => " AND ",
ConditionJoin::Or => " OR "
}
}
}
pub struct SqlCondition<T: Encode<'static, Postgres> + Type<Postgres> + Clone + 'static> {
pub key: String,
condition_type: ConditionType,
value_type: PhantomData<T>,
pub condition_join: ConditionJoin,
}
impl<T: Encode<'static, Postgres> + Type<Postgres> + Clone> SqlCondition<T> {
pub fn new(key: impl Into<String>, condition_type: ConditionType, condition_join: ConditionJoin) -> SqlCondition<T> {
SqlCondition { key: key.into(), condition_type, value_type: PhantomData::<T>, condition_join }
}
}
impl <T: 'static + Encode<'static, Postgres> + Type<Postgres> + Clone> SqlPart for SqlCondition<T> {
fn encode(&self, query_builder: &mut BindQueryBuilder) -> Result<(), anyhow::Error> {
query_builder.push(&self.key);
query_builder.push(<ConditionType as Into<&str>>::into(self.condition_type.clone()));
query_builder.create_bind::<T>();
Ok(())
}
}

View File

@ -0,0 +1,75 @@
use std::any::TypeId;
use anyhow::Error;
use sqlx::{Encode, Postgres, Type};
use crate::part::{BindQueryBuilder, SqlPart};
pub struct SqlInsert {
table_name: String,
columns: Vec<(String, TypeId)>
}
impl SqlInsert {
pub fn with_table(table_name: impl Into<String>) -> Self {
Self {table_name: table_name.into(), columns: Vec::new()}
}
pub fn col<T: Encode<'static, Postgres> + Type<Postgres> + Clone + 'static>(mut self, column_name: impl Into<String>) -> Self {
self.columns.push((column_name.into(), TypeId::of::<T>()));
self
}
pub fn opt_col<T: Encode<'static, Postgres> + Type<Postgres> + Clone + 'static>(mut self, column_name: impl Into<String>) -> Self {
self.columns.push((column_name.into(), TypeId::of::<Option<T>>()));
self
}
pub fn arr_col<T: Encode<'static, Postgres> + Type<Postgres> + Clone + 'static>(mut self, column_name: impl Into<String>) -> Self {
self.columns.push((column_name.into(), TypeId::of::<Vec<T>>()));
self
}
pub fn opt_arr_col<T: Encode<'static, Postgres> + Type<Postgres> + Clone + 'static>(mut self, column_name: impl Into<String>) -> Self {
self.columns.push((column_name.into(), TypeId::of::<Option<Vec<T>>>()));
self
}
}
impl SqlPart for SqlInsert {
fn encode(&self, arg_buffer: &mut BindQueryBuilder) -> Result<(), Error> {
arg_buffer.push("INSERT INTO ")
.push(&self.table_name)
.push("(");
let mut cols = vec![];
let mut types = vec![];
for (column_name, column_type) in &self.columns {
cols.push(column_name.as_str());
types.push(column_type.clone());
}
let end_idx = cols.len();
let mut i = 0;
for col in cols {
arg_buffer.push(col);
if i < end_idx-1 {
arg_buffer.push(",");
}
i += 1;
}
arg_buffer.push(") VALUES (");
i = 0;
for ty in types {
unsafe {
arg_buffer.create_bind_raw_unchecked(ty);
}
if i < end_idx-1 {
arg_buffer.push(",");
}
i += 1
}
arg_buffer.push(")");
Ok(())
}
}

95
cove-db/src/part/mod.rs Normal file
View File

@ -0,0 +1,95 @@
use std::fmt::Write;
use std::any::TypeId;
use std::collections::HashMap;
use std::fmt::Display;
use sqlx::postgres::{PgArguments};
use sqlx::{Encode, Postgres, Type};
use sqlx::query::Query;
pub mod condition;
pub mod select;
pub mod sql_where;
pub mod insert;
pub trait SqlPart {
fn encode(&self, arg_buffer: &mut BindQueryBuilder) -> Result<(), anyhow::Error>;
}
#[derive(Default, Clone)]
pub struct BindQueryBuilder {
pub sql_string: String,
type_map: HashMap<i32, TypeId>,
bind_index: i32,
}
impl BindQueryBuilder {
pub fn new() -> Self {
BindQueryBuilder {
..Default::default()
}
}
pub fn push(&mut self, sql: impl Display) -> &mut Self {
self.sql_string.push_str(sql.to_string().as_str());
self
}
pub fn create_bind<'args, T: 'args + Encode<'args, Postgres> + Type<Postgres> + 'static>(&mut self) -> &mut Self {
self.bind_index += 1;
write!(self.sql_string, "${}", self.bind_index).expect("error formatting `sql`");
self.type_map.insert(self.bind_index, TypeId::of::<T>());
self
}
pub unsafe fn create_bind_raw_unchecked(&mut self, type_id: TypeId) -> &mut Self {
self.bind_index += 1;
write!(self.sql_string, "${}", self.bind_index).expect("error formatting `sql`");
self.type_map.insert(self.bind_index, type_id);
self
}
pub fn to_query(&'_ self) -> BindQuery<'_> {
BindQuery::new(self)
}
}
pub struct BindQuery<'args> {
pub bind_query: &'args BindQueryBuilder,
pub sql_query: Query<'args, Postgres, PgArguments>,
index: i32
}
impl<'args> BindQuery<'args> {
fn new(bind_query: &'args BindQueryBuilder) -> Self {
BindQuery {
bind_query,
sql_query: sqlx::query(&bind_query.sql_string),
index: 1
}
}
pub fn bind<T: 'args + Encode<'args, Postgres> + Type<Postgres> + 'static>(mut self, value: &'args T) -> Result<Self, anyhow::Error> {
if let Some(type_id) = self.bind_query.type_map.get(&self.index) {
if type_id == &TypeId::of::<T>() {
match self.sql_query.try_bind(value) {
Err(e) => {
Err(anyhow::anyhow!("Failed to bind in PreparedBindQuery: {}", e))
}
_ => {
self.index += 1;
Ok(self)
}
}
} else {
Err(anyhow::anyhow!("Type not suitable for parameter with index {}", self.index))
}
} else {
Err(anyhow::anyhow!("Index {} is out of bounds for query.", self.index))
}
}
pub fn build(self) -> Query<'args, Postgres, PgArguments> {
self.sql_query
}
}

View File

@ -0,0 +1,39 @@
use crate::part::{BindQueryBuilder, SqlPart};
pub struct SqlSelect {
selected_columns: Vec<String>,
table_name: String
}
impl SqlSelect {
pub fn with_table(table_name: impl Into<String>) -> SqlSelect {
SqlSelect {selected_columns: vec![], table_name: table_name.into()}
}
pub fn add_column(&mut self, column: impl Into<String>) -> &mut Self {
self.selected_columns.push(column.into());
self
}
}
impl SqlPart for SqlSelect {
fn encode(&self, query_builder: &mut BindQueryBuilder) -> Result<(), anyhow::Error> {
query_builder.push("SELECT ");
let mut i = 0;
let total = self.selected_columns.len();
for column in &self.selected_columns {
query_builder.push(column);
if i < total - 1 {
query_builder.push(", ");
}
i += 1;
}
query_builder.push(" FROM ");
query_builder.push(&self.table_name);
query_builder.push(" ");
Ok(())
}
}

View File

@ -0,0 +1,164 @@
use std::sync::{Arc, Mutex};
use anyhow::{anyhow, Error};
use sqlx::{Encode, Postgres, Type};
use crate::part::condition::{ConditionJoin, ConditionType, SqlCondition};
use crate::part::{BindQueryBuilder, SqlPart};
pub struct SqlWhere {
selected_scope: Arc<Mutex<SqlWhereScope>>,
pub indexed_placeholders: Vec<(usize, String)>,
index: usize
}
impl SqlWhere {
pub fn new() -> Self {
let selected_scope = Arc::new(Mutex::new(SqlWhereScope::new(ConditionJoin::And)));
SqlWhere {
selected_scope,
indexed_placeholders: Vec::new(),
index: 1
}
}
pub fn add_condition<T: Encode<'static, Postgres> + Type<Postgres> + Clone + 'static>(mut self, condition: SqlCondition<T>) -> Result<Self, Error> {
match self.selected_scope.lock().map_err(|e|anyhow!("{}", e)) {
Ok(mut lock) => {
let idx = lock.get_index();
let join = condition.condition_join.clone();
self.indexed_placeholders.push((self.index, condition.key.clone()));
self.index += 1;
lock.conditions.push((idx, Box::new(move |ab|condition.encode(ab)), join));
}
Err(e) => {
return Err(e);
}
}
Ok(self)
}
pub fn scoped(mut self, condition_join: ConditionJoin, scope_fn: impl Fn(Self) -> Result<Self, Error>) -> Result<Self, Error> {
let mut scope = SqlWhereScope::new(condition_join);
scope.parent_scope = Some(self.selected_scope.clone());
let scope = Arc::new(Mutex::new(scope));
if let Ok(mut selected_scope) = self.selected_scope.lock().map_err(|e|anyhow!("{}", e)) {
let idx = selected_scope.get_index();
selected_scope.scopes.push((idx, scope.clone()));
}
self.selected_scope = scope;
scope_fn(self)?.escape_scope()
}
pub fn cond_and<T: Encode<'static, Postgres> + Type<Postgres> + Clone + 'static>(self, key: impl Into<String>, condition_type: ConditionType) -> Result<Self, Error> {
self.add_condition(SqlCondition::<T>::new(key, condition_type, ConditionJoin::And))
}
pub fn cond_or<T: Encode<'static, Postgres> + Type<Postgres> + Clone + 'static>(self, key: impl Into<String>, condition_type: ConditionType) -> Result<Self, Error> {
self.add_condition(SqlCondition::<T>::new(key, condition_type, ConditionJoin::Or))
}
pub fn cond_scope_and(self, scope_fn: impl Fn(Self) -> Result<Self, Error>) -> Result<Self, Error> {
self.scoped(ConditionJoin::And, scope_fn)
}
pub fn cond_scope_or(self, scope_fn: impl Fn(Self) -> Result<Self, Error>) -> Result<Self, Error> {
self.scoped(ConditionJoin::Or, scope_fn)
}
pub fn escape_scope(mut self) -> Result<Self, Error> {
let parent_scope = if let Ok(scope) =
self.selected_scope.lock().map_err(|e|anyhow!("{}", e))?
.get_parent_scope()
{
scope.clone()
} else {
return Err(anyhow!("Already in root scope for SqlWhere"));
};
self.selected_scope = parent_scope;
Ok(self)
}
}
impl SqlPart for SqlWhere {
fn encode(&self, arg_buffer: &mut BindQueryBuilder) -> Result<(), Error> {
arg_buffer.push("WHERE ");
self.selected_scope.lock().map_err(|e|anyhow!("{}", e))?.encode(arg_buffer)
}
}
pub struct SqlWhereScope {
conditions: Vec<(i32, Box<dyn Fn(&mut BindQueryBuilder) -> Result<(), Error>>, ConditionJoin)>,
scopes: Vec<(i32, Arc<Mutex<SqlWhereScope>>)>,
condition_join: ConditionJoin,
parent_scope: Option<Arc<Mutex<SqlWhereScope>>>,
index: i32
}
impl SqlWhereScope {
pub fn new(condition_join: ConditionJoin) -> Self {
SqlWhereScope {
conditions: vec![],
scopes: vec![],
condition_join,
parent_scope: None,
index: 0
}
}
fn get_parent_scope(&mut self) -> Result<Arc<Mutex<SqlWhereScope>>, Error> {
match self.parent_scope.clone() {
None => {
Err(anyhow!("Already in root scope for SqlWhere"))
}
Some(val) => {
Ok(val)
}
}
}
fn get_index(&mut self) -> i32 {
self.index += 1;
self.index - 1
}
}
impl SqlPart for SqlWhereScope {
fn encode(&self, arg_buffer: &mut BindQueryBuilder) -> Result<(), Error> {
let mut condition_found;
for i in 0..self.index {
condition_found = false;
for (j, condition, condition_join) in &self.conditions {
if i == *j {
condition_found = true;
condition(arg_buffer)?;
if i < self.index-1 {
let join: &str = condition_join.clone().into();
arg_buffer.push(join);
}
}
}
if condition_found {
continue;
}
for (j, scope) in &self.scopes {
if i == *j {
arg_buffer.push("(");
let lock = scope.lock().map_err(|e|anyhow!("{}", e))?;
lock.encode(arg_buffer)?;
arg_buffer.push(")");
if i < self.index-1 {
let join: &str = lock.condition_join.clone().into();
arg_buffer.push(join);
}
}
}
}
Ok(())
}
}

1
cove-db/src/query/mod.rs Normal file
View File

@ -0,0 +1 @@
pub mod text;

59
cove-db/src/query/text.rs Normal file
View File

@ -0,0 +1,59 @@
use anyhow::{Error};
use sqlx::{Execute, Executor};
use sqlx::postgres::{ PgQueryResult};
use cove_net_common::id::message_type::MessageType;
use cove_net_common::id::SnowflakeID;
use cove_net_common::id::types::text::TextMessageType;
use cove_net_common::message::c2s::text::text::{TextEmbed, TextMessage};
use crate::{CoveDB, CoveDBImpl};
use crate::part::{BindQueryBuilder, SqlPart};
use crate::part::insert::SqlInsert;
pub trait TextQueries: CoveDBImpl + Send + Sync {
async fn create_message(
&self, user_id: SnowflakeID,
message: TextMessage, mentions: Option<Vec<SnowflakeID>>, mention_everyone: bool,
message_type: &TextMessageType
) -> Result<PgQueryResult, Error> {
println!("creating message");
let message_id = SnowflakeID::new_random_hex_loc(
MessageType::Text(TextMessageType::Text), "beefcafe"
)?;
let mut query_builder = BindQueryBuilder::new();
SqlInsert::with_table("messages")
.col::<SnowflakeID>("id")
.col::<SnowflakeID>("channel_id")
.col::<SnowflakeID>("guild_id")
.col::<SnowflakeID>("author_id")
.col::<String>("content")
.col::<bool>("tts")
.col::<bool>("mention_everyone")
.col::<TextMessageType>("message_type")
.opt_arr_col::<SnowflakeID>("mentions")
.opt_arr_col::<TextEmbed>("embeds")
.opt_arr_col::<SnowflakeID>("attachments")
.encode(&mut query_builder)?;
let query = query_builder.to_query().build()
.bind(message_id)
.bind(message.channel_id)
.bind(message.guild_id)
.bind(user_id)
.bind(message.content)
.bind(message.tts)
.bind(mention_everyone)
.bind(message_type)
.bind(mentions)
.bind(message.embeds)
.bind(message.attachments);
println!("{:?}", query.sql());
let res: PgQueryResult = self.run_query(query).await?;
println!("Rows Affected: {}", res.rows_affected());
Ok(res)
}
}
impl TextQueries for CoveDB {}

View File

@ -0,0 +1,77 @@
use anyhow::anyhow;
use sqlx::postgres::PgRow;
use sqlx::Row;
use cove_net_common::id::SnowflakeID;
use crate::part::BindQuery;
use crate::part::insert::SqlInsert;
use crate::part::select::SqlSelect;
use crate::part::sql_where::SqlWhere;
use crate::rows::{InsertableRow, SelectableRow, TableRow, WhereRow};
pub struct AttachmentRow {
pub id: SnowflakeID,
pub uploader_id: SnowflakeID,
pub channel_id: Option<SnowflakeID>,
pub file_size: i32,
pub cdn_url: String
}
impl TableRow for AttachmentRow {
type Error = anyhow::Error;
fn get_table_name() -> &'static str {
"attachments"
}
}
impl InsertableRow for AttachmentRow {
fn insert(&'_ self) -> SqlInsert {
SqlInsert::with_table(Self::get_table_name())
.col::<SnowflakeID>("id")
.col::<SnowflakeID>("uploader_id")
.opt_col::<SnowflakeID>("channel_id")
.col::<i32>("file_size")
.col::<String>("cdn_url")
}
fn bind<'a>(&'a self, query: BindQuery<'a>) -> Result<BindQuery<'a>, Self::Error> {
query.bind(&self.id)?
.bind(&self.uploader_id)?
.bind(&self.channel_id)?
.bind(&self.file_size)?
.bind(&self.cdn_url)
}
}
impl SelectableRow for AttachmentRow {}
impl WhereRow for AttachmentRow {
fn bind<'a>(&'a self, wheres: SqlWhere, query: BindQuery<'a>) -> Result<BindQuery<'a>, Self::Error> {
let mut query = query;
for (_, col) in wheres.indexed_placeholders {
query = match col.as_str() {
"id" => query.bind(&self.id)?,
"uploader_id" => query.bind(&self.uploader_id)?,
"channel_id" => query.bind(&self.channel_id)?,
"file_size" => query.bind(&self.file_size)?,
"cdn_url" => query.bind(&self.cdn_url)?,
_ => return Err(anyhow!("No column {col} exists for table attachments"))
}
}
Ok(query)
}
}
impl TryFrom<PgRow> for AttachmentRow {
type Error = anyhow::Error;
fn try_from(value: PgRow) -> Result<Self, Self::Error> {
Ok(Self {
id: value.try_get("id")?,
uploader_id: value.try_get("uploader_id")?,
channel_id: value.try_get("channel_id")?,
file_size: value.try_get("file_size")?,
cdn_url: value.try_get("cdn_url")?,
})
}
}

156
cove-db/src/rows/channel.rs Normal file
View File

@ -0,0 +1,156 @@
use anyhow::anyhow;
use serde_json::Value;
use sqlx::postgres::PgRow;
use sqlx::Row;
use sqlx::types::time::OffsetDateTime;
use cove_net_common::id::SnowflakeID;
use cove_net_common::id::types::channel::ChannelMessageType;
use crate::part::BindQuery;
use crate::part::insert::SqlInsert;
use crate::part::sql_where::SqlWhere;
use crate::rows::{InsertableRow, SelectableRow, TableRow, WhereRow};
pub struct ChannelRow {
pub id: SnowflakeID,
pub guild_id: Option<SnowflakeID>,
pub parent_id: Option<SnowflakeID>,
pub name: String,
pub channel_type: ChannelMessageType,
pub position: i32,
pub permission_overwrites: Value,
pub rate_limit_per_user: i32,
pub nsfw: bool,
pub loud: bool,
pub thread_metadata: Option<Value>,
pub member_count: i32,
pub message_count: i32,
pub thread_owner_id: Option<SnowflakeID>,
pub created_at: OffsetDateTime,
pub updated_at: OffsetDateTime,
pub topic: Option<String>,
pub user_limit: i32,
pub region: Option<String>
}
impl TableRow for ChannelRow {
type Error = anyhow::Error;
fn get_table_name() -> &'static str {
"channels"
}
}
impl InsertableRow for ChannelRow {
fn insert(&'_ self) -> SqlInsert {
SqlInsert::with_table(Self::get_table_name())
.col::<SnowflakeID>("id")
.opt_col::<SnowflakeID>("guild_id")
.opt_col::<SnowflakeID>("parent_id")
.col::<String>("name")
.col::<ChannelMessageType>("channel_type")
.col::<i32>("position")
.col::<Value>("permission_overwrites")
.col::<i32>("rate_limit_per_user")
.col::<bool>("nsfw")
.col::<bool>("loud")
.opt_col::<Value>("thread_metadata")
.col::<i32>("member_count")
.col::<i32>("message_count")
.opt_col::<SnowflakeID>("thread_owner_id")
.col::<OffsetDateTime>("created_at")
.col::<SnowflakeID>("updated_at")
.opt_col::<String>("topic")
.col::<i32>("user_limit")
.opt_col::<String>("region")
}
fn bind<'a>(&'a self, query: BindQuery<'a>) -> Result<BindQuery<'a>, Self::Error> {
query.bind(&self.id)?
.bind(&self.guild_id)?
.bind(&self.parent_id)?
.bind(&self.name)?
.bind(&self.channel_type)?
.bind(&self.position)?
.bind(&self.permission_overwrites)?
.bind(&self.rate_limit_per_user)?
.bind(&self.nsfw)?
.bind(&self.loud)?
.bind(&self.thread_metadata)?
.bind(&self.member_count)?
.bind(&self.message_count)?
.bind(&self.thread_owner_id)?
.bind(&self.created_at)?
.bind(&self.updated_at)?
.bind(&self.topic)?
.bind(&self.user_limit)?
.bind(&self.region)
}
}
impl SelectableRow for ChannelRow {}
impl WhereRow for ChannelRow {
fn bind<'a>(&'a self, wheres: SqlWhere, query: BindQuery<'a>) -> Result<BindQuery<'a>, Self::Error> {
let mut query = query;
for (_, col) in wheres.indexed_placeholders {
query = match col.as_str() {
"id" => query.bind(&self.id)?,
"guild_id" => query.bind(&self.guild_id)?,
"parent_id" => query.bind(&self.parent_id)?,
"name" => query.bind(&self.name)?,
"channel_type" => query.bind(&self.channel_type)?,
"position" => query.bind(&self.position)?,
"permission_overwrites" => query.bind(&self.permission_overwrites)?,
"rate_limit_per_user" => query.bind(&self.rate_limit_per_user)?,
"nsfw" => query.bind(&self.nsfw)?,
"loud" => query.bind(&self.loud)?,
"thread_metadata" => query.bind(&self.thread_metadata)?,
"member_count" => query.bind(&self.member_count)?,
"message_count" => query.bind(&self.message_count)?,
"thread_owner_id" => query.bind(&self.thread_owner_id)?,
"created_at" => query.bind(&self.created_at)?,
"updated_at" => query.bind(&self.updated_at)?,
"topic" => query.bind(&self.topic)?,
"user_limit" => query.bind(&self.user_limit)?,
"region" => query.bind(&self.region)?,
_ => return Err(anyhow!("No column {col} exists for table attachments"))
}
}
Ok(query)
}
}
impl TryFrom<PgRow> for ChannelRow {
type Error = anyhow::Error;
fn try_from(value: PgRow) -> Result<Self, Self::Error> {
Ok(Self{
id: value.try_get("id")?,
guild_id: value.try_get("guild_id")?,
parent_id: value.try_get("parent_id")?,
name: value.try_get("name")?,
channel_type: value.try_get("channel_type")?,
position: value.try_get("position")?,
permission_overwrites: value.try_get("permission_overwrites")?,
rate_limit_per_user: value.try_get("rate_limit_per_user")?,
nsfw: value.try_get("nsfw")?,
loud: value.try_get("loud")?,
thread_metadata: value.try_get("thread_metadata")?,
member_count: value.try_get("member_count")?,
message_count: value.try_get("message_count")?,
thread_owner_id: value.try_get("thread_owner_id")?,
created_at: value.try_get("created_at")?,
updated_at: value.try_get("updated_at")?,
topic: value.try_get("topic")?,
user_limit: value.try_get("user_limit")?,
region: value.try_get("region")?,
})
}
}

200
cove-db/src/rows/guild.rs Normal file
View File

@ -0,0 +1,200 @@
use anyhow::anyhow;
use sqlx::postgres::PgRow;
use sqlx::Row;
use sqlx::types::time::OffsetDateTime;
use cove_net_common::guild::component::default_notification::DefaultMessageNotificationSetting;
use cove_net_common::guild::component::explicit_filter::ExplicitContentFilterSetting;
use cove_net_common::guild::component::verification_level::VerificationLevelSetting;
use cove_net_common::id::SnowflakeID;
use crate::part::BindQuery;
use crate::part::insert::SqlInsert;
use crate::part::sql_where::SqlWhere;
use crate::rows::{InsertableRow, SelectableRow, TableRow, WhereRow};
struct GuildRow {
pub id: SnowflakeID,
pub name: String,
pub description: Option<String>,
pub icon: Option<SnowflakeID>,
pub banner: Option<SnowflakeID>,
pub splash: Option<SnowflakeID>,
pub owner_id: SnowflakeID,
pub owner_permissions: [u8; 8],
pub region: String,
pub features: i32,
pub afk_channel_id: Option<SnowflakeID>,
pub afk_timeout: i32,
pub verification_level: VerificationLevelSetting,
pub default_message_notifications: DefaultMessageNotificationSetting,
pub explicit_content_filter: ExplicitContentFilterSetting,
pub system_channel_id: Option<SnowflakeID>,
pub system_channel_flags: i32,
pub premium_boosters: Vec<SnowflakeID>,
pub premium_tier: i32,
pub premium_booster_count: i32,
pub widget_enabled: bool,
pub widget_channel_id: Option<SnowflakeID>,
pub preferred_locale: String,
pub created_at: OffsetDateTime,
pub updated_at: OffsetDateTime,
pub large: bool,
pub member_count: i32
}
impl TableRow for GuildRow {
type Error = anyhow::Error;
fn get_table_name() -> &'static str {
"guilds"
}
}
impl InsertableRow for GuildRow {
fn insert(&'_ self) -> SqlInsert {
SqlInsert::with_table(Self::get_table_name())
.col::<SnowflakeID>("id")
.col::<String>("name")
.opt_col::<String>("description")
.opt_col::<SnowflakeID>("icon")
.opt_col::<SnowflakeID>("banner")
.opt_col::<SnowflakeID>("splash")
.col::<SnowflakeID>("owner_id")
.opt_col::<[u8; 8]>("owner_permissions")
.col::<String>("region")
.col::<i32>("features")
.opt_col::<SnowflakeID>("afk_channel_id")
.col::<i32>("afk_timeout")
.col::<VerificationLevelSetting>("verification_level")
.col::<DefaultMessageNotificationSetting>("default_message_notifications")
.col::<ExplicitContentFilterSetting>("explicit_content_filter")
.opt_col::<SnowflakeID>("system_channel_id")
.opt_col::<i32>("system_channel_flags")
.arr_col::<SnowflakeID>("premium_boosters")
.col::<i32>("premium_tier")
.col::<i32>("premium_booster_count")
.col::<bool>("widget_enabled")
.opt_col::<SnowflakeID>("widget_channel_id")
.col::<String>("preferred_locale")
.col::<OffsetDateTime>("created_at")
.col::<OffsetDateTime>("updated_at")
.col::<bool>("large")
.col::<i32>("member_count")
}
fn bind<'a>(&'a self, query: BindQuery<'a>) -> Result<BindQuery<'a>, Self::Error> {
query.bind(&self.id)?
.bind(&self.name)?
.bind(&self.description)?
.bind(&self.icon)?
.bind(&self.banner)?
.bind(&self.splash)?
.bind(&self.owner_id)?
.bind(&self.owner_permissions)?
.bind(&self.region)?
.bind(&self.features)?
.bind(&self.afk_channel_id)?
.bind(&self.afk_timeout)?
.bind(&self.verification_level)?
.bind(&self.default_message_notifications)?
.bind(&self.explicit_content_filter)?
.bind(&self.system_channel_id)?
.bind(&self.system_channel_flags)?
.bind(&self.premium_boosters)?
.bind(&self.premium_tier)?
.bind(&self.premium_booster_count)?
.bind(&self.widget_enabled)?
.bind(&self.widget_channel_id)?
.bind(&self.preferred_locale)?
.bind(&self.created_at)?
.bind(&self.updated_at)?
.bind(&self.large)?
.bind(&self.member_count)
}
}
impl SelectableRow for GuildRow {}
impl WhereRow for GuildRow {
fn bind<'a>(&'a self, wheres: SqlWhere, query: BindQuery<'a>) -> Result<BindQuery<'a>, Self::Error> {
let mut query = query;
for (_, col) in wheres.indexed_placeholders {
query = match col.as_str() {
"id" => query.bind(&self.id)?,
"name" => query.bind(&self.name)?,
"description" => query.bind(&self.description)?,
"icon" => query.bind(&self.icon)?,
"banner" => query.bind(&self.banner)?,
"splash" => query.bind(&self.splash)?,
"owner_id" => query.bind(&self.owner_id)?,
"owner_permissions" => query.bind(&self.owner_permissions)?,
"region" => query.bind(&self.region)?,
"features" => query.bind(&self.features)?,
"afk_channel_id" => query.bind(&self.afk_channel_id)?,
"afk_timeout" => query.bind(&self.afk_timeout)?,
"verification_level" => query.bind(&self.verification_level)?,
"default_message_notifications" => query.bind(&self.default_message_notifications)?,
"explicit_content_filter" => query.bind(&self.explicit_content_filter)?,
"system_channel_id" => query.bind(&self.system_channel_id)?,
"system_channel_flags" => query.bind(&self.system_channel_flags)?,
"premium_boosters" => query.bind(&self.premium_boosters)?,
"premium_tier" => query.bind(&self.premium_tier)?,
"premium_booster_count" => query.bind(&self.premium_booster_count)?,
"widget_enabled" => query.bind(&self.widget_enabled)?,
"widget_channel_id" => query.bind(&self.widget_channel_id)?,
"preferred_locale" => query.bind(&self.preferred_locale)?,
"created_at" => query.bind(&self.created_at)?,
"updated_at" => query.bind(&self.updated_at)?,
"large" => query.bind(&self.large)?,
"member_count" => query.bind(&self.member_count)?,
_ => return Err(anyhow!("No column {col} exists for table attachments"))
}
}
Ok(query)
}
}
impl TryFrom<PgRow> for GuildRow {
type Error = anyhow::Error;
fn try_from(value: PgRow) -> Result<Self, Self::Error> {
Ok(Self {
id: value.try_get("id")?,
name: value.try_get("name")?,
description: value.try_get("description")?,
icon: value.try_get("icon")?,
banner: value.try_get("banner")?,
splash: value.try_get("splash")?,
owner_id: value.try_get("owner_id")?,
owner_permissions: value.try_get("owner_permissions")?,
region: value.try_get("region")?,
features: value.try_get("features")?,
afk_channel_id: value.try_get("afk_channel_id")?,
afk_timeout: value.try_get("afk_timeout")?,
verification_level: value.try_get("verification_level")?,
default_message_notifications: value.try_get("default_message_notifications")?,
explicit_content_filter: value.try_get("explicit_content_filter")?,
system_channel_id: value.try_get("system_channel_id")?,
system_channel_flags: value.try_get("system_channel_flags")?,
premium_boosters: value.try_get("premium_boosters")?,
premium_tier: value.try_get("premium_tier")?,
premium_booster_count: value.try_get("premium_booster_count")?,
widget_enabled: value.try_get("widget_enabled")?,
widget_channel_id: value.try_get("widget_channel_id")?,
preferred_locale: value.try_get("preferred_locale")?,
created_at: value.try_get("created_at")?,
updated_at: value.try_get("updated_at")?,
large: value.try_get("large")?,
member_count: value.try_get("member_count")?,
})
}
}

View File

@ -0,0 +1,117 @@
use anyhow::anyhow;
use sqlx::postgres::PgRow;
use sqlx::Row;
use sqlx::types::time::OffsetDateTime;
use cove_net_common::id::SnowflakeID;
use crate::part::BindQuery;
use crate::part::insert::SqlInsert;
use crate::part::sql_where::SqlWhere;
use crate::rows::{InsertableRow, SelectableRow, TableRow, WhereRow};
struct GuildMemberRow {
pub guild_id: SnowflakeID,
pub user_id: SnowflakeID,
pub nick: Option<String>,
pub joined_at: OffsetDateTime,
pub roles: Vec<SnowflakeID>,
pub boosting_since: Option<OffsetDateTime>,
pub voice_channel_id: Option<SnowflakeID>,
pub deafened: bool,
pub muted: bool,
pub pending: bool,
pub timed_out_until: Option<OffsetDateTime>,
pub updated_at: OffsetDateTime,
}
impl TableRow for GuildMemberRow {
type Error = anyhow::Error;
fn get_table_name() -> &'static str {
"guild_members"
}
}
impl InsertableRow for GuildMemberRow {
fn insert(&'_ self) -> SqlInsert {
SqlInsert::with_table(Self::get_table_name())
.col::<SnowflakeID>("guild_id")
.col::<SnowflakeID>("user_id")
.opt_col::<String>("nick")
.col::<OffsetDateTime>("joined_at")
.arr_col::<SnowflakeID>("roles")
.opt_col::<OffsetDateTime>("boosting_since")
.opt_col::<SnowflakeID>("voice_channel_id")
.col::<bool>("deafened")
.col::<bool>("muted")
.col::<bool>("pending")
.opt_col::<OffsetDateTime>("timed_out_until")
.col::<OffsetDateTime>("updated_at")
}
fn bind<'a>(&'a self, query: BindQuery<'a>) -> Result<BindQuery<'a>, Self::Error> {
query.bind(&self.guild_id)?
.bind(&self.user_id)?
.bind(&self.nick)?
.bind(&self.joined_at)?
.bind(&self.roles)?
.bind(&self.boosting_since)?
.bind(&self.voice_channel_id)?
.bind(&self.deafened)?
.bind(&self.muted)?
.bind(&self.pending)?
.bind(&self.timed_out_until)?
.bind(&self.updated_at)
}
}
impl SelectableRow for GuildMemberRow {}
impl WhereRow for GuildMemberRow {
fn bind<'a>(&'a self, wheres: SqlWhere, query: BindQuery<'a>) -> Result<BindQuery<'a>, Self::Error> {
let mut query = query;
for (_, col) in wheres.indexed_placeholders {
query = match col.as_str() {
"guild_id" => query.bind(&self.guild_id)?,
"user_id" => query.bind(&self.user_id)?,
"nick" => query.bind(&self.nick)?,
"joined_at" => query.bind(&self.joined_at)?,
"roles" => query.bind(&self.roles)?,
"boosting_since" => query.bind(&self.boosting_since)?,
"voice_channel_id" => query.bind(&self.voice_channel_id)?,
"deafened" => query.bind(&self.deafened)?,
"muted" => query.bind(&self.muted)?,
"pending" => query.bind(&self.pending)?,
"timed_out_until" => query.bind(&self.timed_out_until)?,
"updated_at" => query.bind(&self.updated_at)?,
_ => return Err(anyhow!("No column {col} exists for table attachments"))
}
}
Ok(query)
}
}
impl TryFrom<PgRow> for GuildMemberRow {
type Error = anyhow::Error;
fn try_from(value: PgRow) -> Result<Self, Self::Error> {
Ok(Self {
guild_id: value.try_get("guild_id")?,
user_id: value.try_get("user_id")?,
nick: value.try_get("nick")?,
joined_at: value.try_get("joined_at")?,
roles: value.try_get("roles")?,
boosting_since: value.try_get("boosting_since")?,
voice_channel_id: value.try_get("voice_channel_id")?,
deafened: value.try_get("deafened")?,
muted: value.try_get("muted")?,
pending: value.try_get("pending")?,
timed_out_until: value.try_get("timed_out_until")?,
updated_at: value.try_get("updated_at")?,
})
}
}

155
cove-db/src/rows/message.rs Normal file
View File

@ -0,0 +1,155 @@
use anyhow::anyhow;
use sqlx::postgres::PgRow;
use sqlx::Row;
use sqlx::types::time::OffsetDateTime;
use cove_net_common::id::SnowflakeID;
use cove_net_common::id::types::text::TextMessageType;
use cove_net_common::message::c2s::text::text::TextEmbed;
use crate::part::BindQuery;
use crate::part::insert::SqlInsert;
use crate::part::sql_where::SqlWhere;
use crate::rows::{InsertableRow, SelectableRow, TableRow, WhereRow};
pub struct MessageRow {
pub id: SnowflakeID,
pub channel_id: SnowflakeID,
pub guild_id: Option<SnowflakeID>,
pub author_id: SnowflakeID,
pub content: String,
pub timestamp: OffsetDateTime,
pub edited_timestamp: Option<OffsetDateTime>,
pub tts: bool,
pub mentions: Option<Vec<SnowflakeID>>,
pub mention_everyone: bool,
pub embeds: Option<Vec<TextEmbed>>,
pub attachments: Option<Vec<SnowflakeID>>,
pub reply_message_id: Option<SnowflakeID>,
pub application_id: Option<SnowflakeID>,
pub message_type: TextMessageType,
pub thread_name: Option<String>,
pub auto_archive_duration: Option<i32>,
pub created_at: OffsetDateTime,
pub updated_at: OffsetDateTime,
}
impl TableRow for MessageRow {
type Error = anyhow::Error;
fn get_table_name() -> &'static str {
"messages"
}
}
impl InsertableRow for MessageRow {
fn insert(&'_ self) -> SqlInsert {
SqlInsert::with_table(Self::get_table_name())
.col::<SnowflakeID>("id")
.col::<SnowflakeID>("channel_id")
.opt_col::<SnowflakeID>("guild_id")
.col::<SnowflakeID>("author_id")
.col::<String>("content")
.col::<OffsetDateTime>("timestamp")
.opt_col::<OffsetDateTime>("edited_timestamp")
.col::<bool>("tts")
.opt_arr_col::<SnowflakeID>("mentions")
.col::<bool>("mention_everyone")
.opt_arr_col::<TextEmbed>("embeds")
.opt_arr_col::<SnowflakeID>("attachments")
.opt_col::<SnowflakeID>("reply_message_id")
.opt_col::<SnowflakeID>("application_id")
.col::<TextMessageType>("message_type")
.opt_col::<String>("thread_name")
.opt_col::<i32>("auto_archive_duration")
.col::<OffsetDateTime>("created_at")
.col::<OffsetDateTime>("updated_at")
}
fn bind<'a>(&'a self, query: BindQuery<'a>) -> Result<BindQuery<'a>, Self::Error> {
query.bind(&self.id)?
.bind(&self.channel_id)?
.bind(&self.guild_id)?
.bind(&self.author_id)?
.bind(&self.content)?
.bind(&self.timestamp)?
.bind(&self.edited_timestamp)?
.bind(&self.tts)?
.bind(&self.mentions)?
.bind(&self.mention_everyone)?
.bind(&self.embeds)?
.bind(&self.attachments)?
.bind(&self.reply_message_id)?
.bind(&self.application_id)?
.bind(&self.message_type)?
.bind(&self.thread_name)?
.bind(&self.auto_archive_duration)?
.bind(&self.created_at)?
.bind(&self.updated_at)
}
}
impl SelectableRow for MessageRow {}
impl WhereRow for MessageRow {
fn bind<'a>(&'a self, wheres: SqlWhere, query: BindQuery<'a>) -> Result<BindQuery<'a>, Self::Error> {
let mut query = query;
for (_, col) in wheres.indexed_placeholders {
query = match col.as_str() {
"id" => query.bind(&self.id)?,
"channel_id" => query.bind(&self.channel_id)?,
"guild_id" => query.bind(&self.guild_id)?,
"author_id" => query.bind(&self.author_id)?,
"content" => query.bind(&self.content)?,
"timestamp" => query.bind(&self.timestamp)?,
"edited_timestamp" => query.bind(&self.edited_timestamp)?,
"tts" => query.bind(&self.tts)?,
"mentions" => query.bind(&self.mentions)?,
"mention_everyone" => query.bind(&self.mention_everyone)?,
"embeds" => query.bind(&self.embeds)?,
"attachments" => query.bind(&self.attachments)?,
"reply_message_id" => query.bind(&self.reply_message_id)?,
"application_id" => query.bind(&self.application_id)?,
"message_type" => query.bind(&self.message_type)?,
"thread_name" => query.bind(&self.thread_name)?,
"auto_archive_duration" => query.bind(&self.auto_archive_duration)?,
"created_at" => query.bind(&self.created_at)?,
"updated_at" => query.bind(&self.updated_at)?,
_ => return Err(anyhow!("No column {col} exists for table attachments"))
}
}
Ok(query)
}
}
impl TryFrom<PgRow> for MessageRow {
type Error = anyhow::Error;
fn try_from(value: PgRow) -> Result<Self, Self::Error> {
Ok(Self {
id: value.try_get("id")?,
channel_id: value.try_get("channel_id")?,
guild_id: value.try_get("guild_id")?,
author_id: value.try_get("author_id")?,
content: value.try_get("content")?,
timestamp: value.try_get("timestamp")?,
edited_timestamp: value.try_get("edited_timestamp")?,
tts: value.try_get("tts")?,
mentions: value.try_get("mentions")?,
mention_everyone: value.try_get("mention_everyone")?,
embeds: value.try_get("embeds")?,
attachments: value.try_get("attachments")?,
reply_message_id: value.try_get("reply_message_id")?,
application_id: value.try_get("application_id")?,
message_type: value.try_get("message_type")?,
thread_name: value.try_get("thread_name")?,
auto_archive_duration: value.try_get("auto_archive_duration")?,
created_at: value.try_get("created_at")?,
updated_at: value.try_get("updated_at")?,
})
}
}

45
cove-db/src/rows/mod.rs Normal file
View File

@ -0,0 +1,45 @@
use crate::part::BindQuery;
use crate::part::insert::SqlInsert;
use crate::part::select::SqlSelect;
use crate::part::sql_where::SqlWhere;
pub mod user;
pub trait TableRow {
type Error;
type PartialRow: PartialTableRow;
fn get_table_name() -> &'static str;
}
pub trait PartialTableRow {
type Error;
type FullTableRow: TableRow;
fn get_table_name() -> &'static str {
Self::FullTableRow::get_table_name()
}
}
pub trait InsertableRow: TableRow {
fn insert(&'_ self) -> SqlInsert;
fn bind<'a>(&'a self, query: BindQuery<'a>) -> Result<BindQuery<'a>, Self::Error>;
}
pub trait SelectableRow: PartialTableRow {
fn select(&'_ self, selected_columns: Vec<impl Into<String>>) -> SqlSelect {
let mut select = SqlSelect::with_table(Self::get_table_name());
for column in selected_columns {
select.add_column(column);
}
select
}
}
pub trait WhereRow: PartialTableRow {
fn wheres(&'_ self, where_fn: impl Fn(SqlWhere) -> Result<SqlWhere, Self::Error>) -> Result<SqlWhere, Self::Error> {
let wheres = SqlWhere::new();
let wheres = where_fn(wheres)?;
Ok(wheres)
}
fn bind<'a>(&'a self, wheres: SqlWhere, query: BindQuery<'a>) -> Result<BindQuery<'a>, Self::Error>;
}

77
cove-db/src/rows/nonce.rs Normal file
View File

@ -0,0 +1,77 @@
use anyhow::anyhow;
use sqlx::postgres::PgRow;
use sqlx::Row;
use sqlx::types::time::OffsetDateTime;
use cove_net_common::id::SnowflakeID;
use crate::part::BindQuery;
use crate::part::insert::SqlInsert;
use crate::part::sql_where::SqlWhere;
use crate::rows::{InsertableRow, SelectableRow, TableRow, WhereRow};
struct NonceRow {
pub nonce: String,
pub channel_id: SnowflakeID,
pub author_id: SnowflakeID,
pub created_at: OffsetDateTime,
pub expires_at: OffsetDateTime,
}
impl TableRow for NonceRow {
type Error = anyhow::Error;
fn get_table_name() -> &'static str {
"pending_nonces"
}
}
impl InsertableRow for NonceRow {
fn insert(&'_ self) -> SqlInsert {
SqlInsert::with_table(Self::get_table_name())
.col::<String>("nonce")
.col::<SnowflakeID>("channel_id")
.col::<SnowflakeID>("author_id")
.col::<OffsetDateTime>("created_at")
.col::<OffsetDateTime>("expires_at")
}
fn bind<'a>(&'a self, query: BindQuery<'a>) -> Result<BindQuery<'a>, Self::Error> {
query.bind(&self.nonce)?
.bind(&self.channel_id)?
.bind(&self.author_id)?
.bind(&self.created_at)?
.bind(&self.expires_at)
}
}
impl SelectableRow for NonceRow {}
impl WhereRow for NonceRow {
fn bind<'a>(&'a self, wheres: SqlWhere, query: BindQuery<'a>) -> Result<BindQuery<'a>, Self::Error> {
let mut query = query;
for (_, col) in wheres.indexed_placeholders {
query = match col.as_str() {
"nonce" => query.bind(&self.nonce)?,
"channel_id" => query.bind(&self.channel_id)?,
"author_id" => query.bind(&self.author_id)?,
"created_at" => query.bind(&self.created_at)?,
"expires_at" => query.bind(&self.expires_at)?,
_ => return Err(anyhow!("No column {col} exists for table attachments"))
}
}
Ok(query)
}
}
impl TryFrom<PgRow> for NonceRow {
type Error = anyhow::Error;
fn try_from(value: PgRow) -> Result<Self, Self::Error> {
Ok(Self {
nonce: value.try_get("nonce")?,
channel_id: value.try_get("channel_id")?,
author_id: value.try_get("author_id")?,
created_at: value.try_get("created_at")?,
expires_at: value.try_get("expires_at")?,
})
}
}

205
cove-db/src/rows/user.rs Normal file
View File

@ -0,0 +1,205 @@
use anyhow::anyhow;
use serde_json::Value;
use sqlx::postgres::PgRow;
use sqlx::Row;
use sqlx::types::time::OffsetDateTime;
use cove_net_common::id::SnowflakeID;
use crate::part::BindQuery;
use crate::part::insert::SqlInsert;
use crate::part::sql_where::SqlWhere;
use crate::rows::{InsertableRow, PartialTableRow, SelectableRow, TableRow, WhereRow};
use crate::types::user_status::UserStatus;
pub struct UserRow {
pub id: SnowflakeID,
pub username: String,
pub discriminator: String,
pub avatar_hash: Option<String>,
pub email: String,
pub email_verified: bool,
pub password_hash: String,
pub mfa_enabled: bool,
pub mfa_secret: Option<String>,
pub status: UserStatus,
pub public_flags: i64,
pub locale: String,
pub premium_since: Option<OffsetDateTime>,
pub premium_end: Option<OffsetDateTime>,
pub bot: bool,
pub bot_oauth_scopes: Value,
pub preferences: Value,
pub created_at: OffsetDateTime,
pub updated_at: OffsetDateTime,
}
#[derive(Default)]
pub struct PartialUserRow {
pub id: Option<SnowflakeID>,
pub username: Option<String>,
pub discriminator: Option<String>,
pub avatar_hash: Option<Option<String>>,
pub email: Option<String>,
pub email_verified: Option<bool>,
pub password_hash: Option<String>,
pub mfa_enabled: Option<bool>,
pub mfa_secret: Option<Option<String>>,
pub status: Option<UserStatus>,
pub public_flags: Option<i64>,
pub locale: Option<String>,
pub premium_since: Option<Option<OffsetDateTime>>,
pub premium_end: Option<Option<OffsetDateTime>>,
pub bot: Option<bool>,
pub bot_oauth_scopes: Option<Value>,
pub preferences: Option<Value>,
pub created_at: Option<OffsetDateTime>,
pub updated_at: Option<OffsetDateTime>,
}
impl TableRow for UserRow {
type Error = anyhow::Error;
type PartialRow = PartialUserRow;
fn get_table_name() -> &'static str {
"users"
}
}
impl PartialTableRow for PartialUserRow {
type Error = anyhow::Error;
type FullTableRow = UserRow;
}
impl InsertableRow for UserRow {
fn insert(&'_ self) -> SqlInsert {
SqlInsert::with_table(Self::get_table_name())
.col::<SnowflakeID>("id")
.col::<String>("username")
.col::<String>("discriminator")
.opt_col::<String>("avatar_hash")
.col::<String>("email")
.col::<bool>("email_verified")
.col::<String>("password_hash")
.col::<bool>("mfa_enabled")
.opt_col::<String>("mfa_secret")
.col::<UserStatus>("status")
.col::<i64>("public_flags")
.col::<String>("locale")
.opt_col::<OffsetDateTime>("premium_since")
.opt_col::<OffsetDateTime>("premium_end")
.col::<bool>("bot")
.col::<Value>("bot_oauth_scopes")
.col::<Value>("preferences")
.col::<OffsetDateTime>("created_at")
.col::<OffsetDateTime>("updated_at")
}
fn bind<'a>(&'a self, query: BindQuery<'a>) -> Result<BindQuery<'a>, Self::Error> {
query.bind(&self.id)?
.bind(&self.username)?
.bind(&self.discriminator)?
.bind(&self.avatar_hash)?
.bind(&self.email)?
.bind(&self.email_verified)?
.bind(&self.password_hash)?
.bind(&self.mfa_enabled)?
.bind(&self.mfa_secret)?
.bind(&self.status)?
.bind(&self.public_flags)?
.bind(&self.locale)?
.bind(&self.premium_since)?
.bind(&self.premium_end)?
.bind(&self.bot)?
.bind(&self.bot_oauth_scopes)?
.bind(&self.preferences)?
.bind(&self.created_at)?
.bind(&self.updated_at)
}
}
impl SelectableRow for PartialUserRow {}
impl WhereRow for PartialUserRow {
fn bind<'a>(&'a self, wheres: SqlWhere, query: BindQuery<'a>) -> Result<BindQuery<'a>, Self::Error> {
let mut query = query;
for (_, col) in wheres.indexed_placeholders {
query = match col.as_str() {
"id" => query.bind(self.id.as_ref().unwrap())?,
"username" => query.bind(self.username.as_ref().unwrap())?,
"discriminator" => query.bind(self.discriminator.as_ref().unwrap())?,
"avatar_hash" => query.bind(self.avatar_hash.as_ref().unwrap())?,
"email" => query.bind(self.email.as_ref().unwrap())?,
"email_verified" => query.bind(self.email_verified.as_ref().unwrap())?,
"password_hash" => query.bind(self.password_hash.as_ref().unwrap())?,
"mfa_enabled" => query.bind(self.mfa_enabled.as_ref().unwrap())?,
"mfa_secret" => query.bind(self.mfa_secret.as_ref().unwrap())?,
"status" => query.bind(self.status.as_ref().unwrap())?,
"public_flags" => query.bind(self.public_flags.as_ref().unwrap())?,
"locale" => query.bind(self.locale.as_ref().unwrap())?,
"premium_since" => query.bind(self.premium_since.as_ref().unwrap())?,
"premium_end" => query.bind(self.premium_end.as_ref().unwrap())?,
"bot" => query.bind(self.bot.as_ref().unwrap())?,
"bot_oauth_scopes" => query.bind(self.bot_oauth_scopes.as_ref().unwrap())?,
"preferences" => query.bind(self.preferences.as_ref().unwrap())?,
"created_at" => query.bind(self.created_at.as_ref().unwrap())?,
"updated_at" => query.bind(self.updated_at.as_ref().unwrap())?,
_ => return Err(anyhow!("No column {col} exists for table users"))
}
}
Ok(query)
}
}
impl TryFrom<PgRow> for UserRow {
type Error = anyhow::Error;
fn try_from(row: PgRow) -> Result<Self, anyhow::Error> {
Ok(Self {
id: row.try_get("id")?,
username: row.try_get("username")?,
discriminator: row.try_get("discriminator")?,
avatar_hash: row.try_get("avatar_hash")?,
email: row.try_get("email")?,
email_verified: row.try_get("email_verified")?,
password_hash: row.try_get("password_hash")?,
mfa_enabled: row.try_get("mfa_enabled")?,
mfa_secret: row.try_get("mfa_secret")?,
status: row.try_get("status")?,
public_flags: row.try_get("public_flags")?,
locale: row.try_get("locale")?,
premium_since: row.try_get("premium_since")?,
premium_end: row.try_get("premium_end")?,
bot: row.try_get("bot")?,
bot_oauth_scopes: row.try_get("bot_oauth_scopes")?,
preferences: row.try_get("preferences")?,
created_at: row.try_get("created_at")?,
updated_at: row.try_get("updated_at")?,
})
}
}
impl From<PgRow> for PartialUserRow {
fn from(row: PgRow) -> Self {
Self {
id: row.try_get("id").ok(),
username: row.try_get("username").ok(),
discriminator: row.try_get("discriminator").ok(),
avatar_hash: row.try_get("avatar_hash").ok(),
email: row.try_get("email").ok(),
email_verified: row.try_get("email_verified").ok(),
password_hash: row.try_get("password_hash").ok(),
mfa_enabled: row.try_get("mfa_enabled").ok(),
mfa_secret: row.try_get("mfa_secret").ok(),
status: row.try_get("status").ok(),
public_flags: row.try_get("public_flags").ok(),
locale: row.try_get("locale").ok(),
premium_since: row.try_get("premium_since").ok(),
premium_end: row.try_get("premium_end").ok(),
bot: row.try_get("bot").ok(),
bot_oauth_scopes: row.try_get("bot_oauth_scopes").ok(),
preferences: row.try_get("preferences").ok(),
created_at: row.try_get("created_at").ok(),
updated_at: row.try_get("updated_at").ok(),
}
}
}

1
cove-db/src/types/mod.rs Normal file
View File

@ -0,0 +1 @@
pub mod user_status;

View File

@ -0,0 +1,54 @@
use sqlx::{Database, Decode, Encode, Postgres, Type, TypeInfo};
use sqlx::encode::IsNull;
use sqlx::error::BoxDynError;
use sqlx::postgres::PgTypeInfo;
#[derive(Clone)]
pub enum UserStatus {
Online,
Idle,
Dnd,
Offline,
Invisible
}
impl Encode<'_, Postgres> for UserStatus {
fn encode_by_ref<'q>(&self, buf: &mut <Postgres as Database>::ArgumentBuffer<'q>) -> Result<IsNull, BoxDynError> {
<&str as Encode<Postgres>>::encode(self.clone().into(), buf)
}
}
impl Decode<'_, Postgres> for UserStatus {
fn decode(value: <Postgres as Database>::ValueRef<'_>) -> Result<Self, BoxDynError> {
match value.as_str()? {
"online" => Ok(UserStatus::Online),
"idle" => Ok(UserStatus::Idle),
"dnd" => Ok(UserStatus::Dnd),
"offline" => Ok(UserStatus::Offline),
"invisible" => Ok(UserStatus::Invisible),
_ => Err(BoxDynError::from("Unknown user status value"))
}
}
}
impl Type<Postgres> for UserStatus {
fn type_info() -> PgTypeInfo {
PgTypeInfo::with_name("user_status")
}
fn compatible(ty: &PgTypeInfo) -> bool {
PgTypeInfo::with_name("user_status").type_compatible(ty)
}
}
impl Into<&str> for UserStatus {
fn into(self) -> &'static str {
match self {
UserStatus::Online => "online",
UserStatus::Idle => "idle",
UserStatus::Dnd => "dnd",
UserStatus::Offline => "offline",
UserStatus::Invisible => "invisible",
}
}
}

View File

@ -0,0 +1,21 @@
INSERT INTO pgagent.pga_jobclass (jclname)
VALUES ('Cleanup Nonces');
INSERT INTO pgagent.pga_job (jobjclid, jobname, jobdesc, jobenabled, jobhostagent)
SELECT jcl.jclid, 'Cleanup Pending Nonces', 'Remove expired pending_nonces rows older than 5 minutes', true, ''
from pgagent.pga_jobclass jcl WHERE jclname='Cleanup Nonces';
INSERT INTO pgagent.pga_jobstep (jstjobid, jstname, jstdesc, jstenabled, jstkind, jstonerror, jstcode, jstdbname)
SELECT job.jobid, 'Perform Cleanup', 'Delete pending nonces', true, 's', 'f', $$DELETE FROM pending_nonces WHERE expires_at < NOW()$$, 'testdata'
FROM pgagent.pga_job job where jobname='Cleanup Pending Nonces';
INSERT INTO pgagent.pga_schedule (jscjobid, jscname, jscenabled, jscstart, jscminutes)
VALUES (
(SELECT jobid
from pgagent.pga_job
where jobname = 'Cleanup Pending Nonces'),
'Every 5 minutes',
true,
CURRENT_TIMESTAMP, -- start anytime
'{t,f,f,f,f,t,f,f,f,f,t,f,f,f,f,t,f,f,f,f,t,f,f,f,f,t,f,f,f,f,t,f,f,f,f,t,f,f,f,f,t,f,f,f,f,t,f,f,f,f,t,f,f,f,f,t,f,f,f,f}' -- every 5th minute
);

View File

@ -0,0 +1,6 @@
[package]
name = "cove-net-client"
version = "0.1.0"
edition = "2024"
[dependencies]

View File

@ -0,0 +1,14 @@
pub fn add(left: u64, right: u64) -> u64 {
left + right
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn it_works() {
let result = add(2, 2);
assert_eq!(result, 4);
}
}

View File

@ -0,0 +1,13 @@
[package]
name = "cove-net-common"
version = "0.1.0"
edition = "2024"
[dependencies]
anyhow.workspace = true
hex.workspace = true
rand = "0.10.0"
serde.workspace = true
serde_with.workspace = true
serde_json.workspace = true
sqlx.workspace = true

View File

@ -0,0 +1,53 @@
use sqlx::error::BoxDynError;
use sqlx::{Database, Decode, Encode, Postgres, Type};
use sqlx::encode::IsNull;
#[repr(i32)]
#[derive(Clone, Copy)]
pub enum DefaultMessageNotificationSetting {
All = 0,
MentionsOnly = 1,
None = 2,
}
impl Into<i32> for &DefaultMessageNotificationSetting {
fn into(self) -> i32 {
*self as i32
}
}
impl TryFrom<i32> for DefaultMessageNotificationSetting {
type Error = BoxDynError;
fn try_from(value: i32) -> Result<Self, Self::Error> {
match value {
0 => Ok(Self::All),
1 => Ok(Self::MentionsOnly),
2 => Ok(Self::None),
_ => Err(format!("Unknown value {}", value).into()),
}
}
}
impl Type<Postgres> for DefaultMessageNotificationSetting {
fn type_info() -> <Postgres as Database>::TypeInfo {
<i32 as Type<Postgres>>::type_info()
}
fn compatible(ty: &<Postgres as Database>::TypeInfo) -> bool {
<i32 as Type<Postgres>>::compatible(ty)
}
}
impl Encode<'_, Postgres> for DefaultMessageNotificationSetting {
fn encode_by_ref(&self, buf: &mut <Postgres as Database>::ArgumentBuffer<'_>) -> Result<IsNull, BoxDynError> {
<i32 as sqlx::Encode<Postgres>>::encode(self.into(), buf)
}
}
impl Decode<'_, Postgres> for DefaultMessageNotificationSetting {
fn decode(value: <Postgres as Database>::ValueRef<'_>) -> Result<Self, BoxDynError> {
<i32 as Decode<Postgres>>::decode(value).map(Self::try_from)?
}
}

View File

@ -0,0 +1,52 @@
use sqlx::error::BoxDynError;
use sqlx::{Database, Decode, Encode, Postgres, Type};
use sqlx::encode::IsNull;
#[repr(i32)]
#[derive(Clone, Copy)]
pub enum ExplicitContentFilterSetting {
Disabled = 0,
MembersWithoutRole = 1,
All = 2
}
impl Into<i32> for &ExplicitContentFilterSetting {
fn into(self) -> i32 {
*self as i32
}
}
impl TryFrom<i32> for ExplicitContentFilterSetting {
type Error = BoxDynError;
fn try_from(value: i32) -> Result<Self, Self::Error> {
match value {
0 => Ok(ExplicitContentFilterSetting::Disabled),
1 => Ok(ExplicitContentFilterSetting::MembersWithoutRole),
2 => Ok(ExplicitContentFilterSetting::All),
_ => Err(format!("Unknown value {}", value).into()),
}
}
}
impl Type<Postgres> for ExplicitContentFilterSetting {
fn type_info() -> <Postgres as Database>::TypeInfo {
<i32 as Type<Postgres>>::type_info()
}
fn compatible(ty: &<Postgres as Database>::TypeInfo) -> bool {
<i32 as Type<Postgres>>::compatible(ty)
}
}
impl Encode<'_, Postgres> for ExplicitContentFilterSetting {
fn encode_by_ref(&self, buf: &mut <Postgres as Database>::ArgumentBuffer<'_>) -> Result<IsNull, BoxDynError> {
<i32 as sqlx::Encode<Postgres>>::encode(self.into(), buf)
}
}
impl Decode<'_, Postgres> for ExplicitContentFilterSetting {
fn decode(value: <Postgres as Database>::ValueRef<'_>) -> Result<Self, BoxDynError> {
<i32 as Decode<Postgres>>::decode(value).map(Self::try_from)?
}
}

View File

@ -0,0 +1,3 @@
pub mod verification_level;
pub mod default_notification;
pub mod explicit_filter;

View File

@ -0,0 +1,56 @@
use sqlx::error::BoxDynError;
use sqlx::{Database, Decode, Encode, Postgres, Type};
use sqlx::encode::IsNull;
#[repr(i32)]
#[derive(Clone, Copy)]
pub enum VerificationLevelSetting {
None = 0,
Low = 1,
Medium = 2,
High = 3,
Highest = 4,
}
impl Into<i32> for &VerificationLevelSetting {
fn into(self) -> i32 {
*self as i32
}
}
impl TryFrom<i32> for VerificationLevelSetting {
type Error = BoxDynError;
fn try_from(value: i32) -> Result<Self, Self::Error> {
match value {
0 => Ok(Self::None),
1 => Ok(Self::Low),
2 => Ok(Self::Medium),
3 => Ok(Self::High),
4 => Ok(Self::Highest),
_ => Err(format!("Unknown value {}", value).into()),
}
}
}
impl Type<Postgres> for VerificationLevelSetting {
fn type_info() -> <Postgres as Database>::TypeInfo {
<i32 as Type<Postgres>>::type_info()
}
fn compatible(ty: &<Postgres as Database>::TypeInfo) -> bool {
<i32 as Type<Postgres>>::compatible(ty)
}
}
impl Encode<'_, Postgres> for VerificationLevelSetting {
fn encode_by_ref(&self, buf: &mut <Postgres as Database>::ArgumentBuffer<'_>) -> Result<IsNull, BoxDynError> {
<i32 as sqlx::Encode<Postgres>>::encode(self.into(), buf)
}
}
impl Decode<'_, Postgres> for VerificationLevelSetting {
fn decode(value: <Postgres as Database>::ValueRef<'_>) -> Result<Self, BoxDynError> {
<i32 as Decode<Postgres>>::decode(value).map(Self::try_from)?
}
}

View File

@ -0,0 +1 @@
pub mod component;

View File

@ -0,0 +1,76 @@
use std::fmt::Formatter;
use anyhow::anyhow;
use serde::{Deserialize, Serialize};
use crate::id::types::channel::ChannelMessageType;
use crate::id::types::text::TextMessageType;
#[repr(u8)]
#[derive(Debug, Serialize, Deserialize, Copy, Clone)]
pub enum MessageType {
Text(TextMessageType) = 0,
Channel(ChannelMessageType) = 1,
Guild = 2,
User = 3,
}
impl std::fmt::Display for MessageType {
fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
match &self {
MessageType::Text(msg_type) => write!(f, "Text({})", msg_type),
MessageType::Channel(msg_type) => write!(f, "Channel({})", msg_type),
MessageType::Guild => write!(f, "Guild"),
MessageType::User => write!(f, "User"),
}
}
}
impl Into<u8> for &MessageType {
fn into(self) -> u8 {
match self {
MessageType::Text(_) => 0,
MessageType::Channel(_) => 1,
MessageType::Guild => 2,
MessageType::User => 3,
}
}
}
impl Into<[u8; 2]> for &MessageType {
fn into(self) -> [u8; 2] {
match self {
MessageType::Text(subtype) => [
0, subtype.into(),
],
MessageType::Channel(subtype) => [
1, subtype.into(),
],
MessageType::Guild => [2, 0],
MessageType::User => [3, 0],
}
}
}
impl TryFrom<&[u8;26]> for MessageType {
type Error = anyhow::Error;
fn try_from(value: &[u8; 26]) -> Result<Self, Self::Error> {
match value[0] {
0 => {
match TextMessageType::try_from(value[1]) {
Ok(t) => Ok(MessageType::Text(t)),
Err(_) => Err(anyhow!("Unknown message subtype for Text: {}", value[1]))
}
}
1 => {
match ChannelMessageType::try_from(value[1]) {
Ok(t) => Ok(MessageType::Channel(t)),
Err(_) => Err(anyhow!("Unknown message type for Channel: {}", value[1]))
}
},
2 => Ok(MessageType::Guild),
3 => Ok(MessageType::User),
_ => Err(anyhow!("Unknown message type: {}", value[0])),
}
}
}

View File

@ -0,0 +1,176 @@
pub mod types;
pub mod message_type;
/*
Snowflake ID Format
Byte Idx.0 - Message Type
Byte Idx.1 - Message Subtype
Bytes Idx.2-5 - Message Storage Location
Bytes Idx.6-25 - Message ID
Hexadecimal text encoding
Type Location
| |
DE-AD-B33FCAFE-0102030405060708091011121314151617181920
| |
Subtype Unique ID
*/
use std::fmt::{Display, Formatter};
use std::mem::transmute;
use std::str::FromStr;
use anyhow::anyhow;
use hex::{FromHexError};
use serde_with::{DeserializeFromStr, SerializeDisplay};
use sqlx::{Database, Decode, Encode, Postgres, Type};
use sqlx::encode::IsNull;
use sqlx::error::BoxDynError;
use sqlx::postgres::{PgHasArrayType, PgTypeInfo};
use crate::id::message_type::MessageType;
#[repr(C, packed(1))]
#[derive(Debug, DeserializeFromStr, SerializeDisplay, Clone)]
pub struct SnowflakeID {
pub message_type: MessageType,
pub location: [u8;4],
pub id: [u8;20]
}
impl Display for SnowflakeID {
fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
f.write_str(hex::encode::<[u8;2]>((&self.message_type).into()).as_str())?;
f.write_str(hex::encode(self.location).as_str())?;
f.write_str(hex::encode(self.id).as_str())?;
Ok(())
}
}
impl FromStr for SnowflakeID {
type Err = anyhow::Error;
fn from_str(input: &str) -> Result<Self, Self::Err> {
let mut hex = [0u8; 26];
let result = if input.len() != 52 {
if input.len() == 55 {
let mut chars = input.chars();
if chars.nth(2).unwrap() == '-' &&
chars.nth(2).unwrap() == '-' &&
chars.nth(8).unwrap() == '-' {
let out = [&input[0..2], &input[3..5], &input[6..14], &input[15..]];
hex::decode_to_slice(out.concat(), &mut hex)
} else {
return Err(anyhow!("Bad ID Format: {}", &input))
}
} else {
return Err(anyhow!("Bad ID Format: {}", &input))
}
} else {
hex::decode_to_slice(input, &mut hex)
};
match result {
Ok(_) => {
Self::from_bytes(hex)
}
Err(_) => {
Err(anyhow!("Bad ID Format: {}", &input))
}
}
}
}
impl Into<[u8;26]> for &SnowflakeID {
fn into(self) -> [u8;26] {
let message_type: [u8; 2] = (&self.message_type).into();
let concat_buf: [&[u8]; 3] = [&message_type, &self.location, &self.id];
let mut ret = [0u8; 26];
concat_buf.concat().swap_with_slice(&mut ret);
ret
}
}
impl Encode<'_, Postgres> for SnowflakeID {
fn encode_by_ref(&self, buf: &mut <Postgres as Database>::ArgumentBuffer<'_>) -> Result<IsNull, BoxDynError> {
let bytes: [u8;26] = self.into();
<&[u8] as Encode<Postgres>>::encode(&bytes, buf)
}
fn size_hint(&self) -> usize {
26
}
}
impl Decode<'_, Postgres> for SnowflakeID {
fn decode(value: <Postgres as Database>::ValueRef<'_>) -> Result<Self, BoxDynError> {
let bytes = value.as_bytes()?;
let mut bytes_a = [0u8; 26];
bytes_a.copy_from_slice(bytes);
SnowflakeID::from_bytes(bytes_a).map_err(|err| BoxDynError::from(err))
}
}
impl PgHasArrayType for SnowflakeID {
fn array_type_info() -> PgTypeInfo {
<&[&[u8]] as Type<Postgres>>::type_info()
}
}
impl Type<Postgres> for SnowflakeID {
fn type_info() -> <Postgres as Database>::TypeInfo {
<&[u8] as Type<Postgres>>::type_info()
}
fn compatible(ty: &<Postgres as Database>::TypeInfo) -> bool {
<&[u8] as Type<Postgres>>::compatible(ty)
}
}
impl SnowflakeID {
pub fn new_random(message_type: MessageType, location: [u8; 4]) -> SnowflakeID {
let mut id = [0u8; 20];
rand::fill(&mut id);
SnowflakeID {
message_type,
location,
id
}
}
pub fn new_random_hex_loc(message_type: MessageType, location: &str) -> Result<SnowflakeID, FromHexError> {
let mut loc_out = [0u8; 4];
hex::decode_to_slice(location, &mut loc_out)?;
let mut id = [0u8; 20];
rand::fill(&mut id);
Ok(
SnowflakeID {
message_type,
location: loc_out,
id
}
)
}
pub fn from_bytes(input: [u8; 26]) -> Result<SnowflakeID, anyhow::Error> {
match MessageType::try_from(&input) {
Ok(_) => {
Ok(unsafe { transmute(input) })
}
Err(e) => {
let out_err = format!("Failed to convert to SnowflakeID: {}", e);
Err(anyhow!(out_err))
}
}
}
pub unsafe fn from_bytes_unchecked(input: [u8; 26]) -> SnowflakeID { unsafe {
transmute(input)
} }
}

View File

@ -0,0 +1,75 @@
use std::fmt::Formatter;
use anyhow::anyhow;
use serde::{Deserialize, Serialize};
use sqlx::{Database, Decode, Encode, Postgres, Type};
use sqlx::encode::IsNull;
use sqlx::error::BoxDynError;
use sqlx::postgres::PgValueRef;
#[repr(u8)]
#[derive(Debug, Serialize, Deserialize, Copy, Clone)]
pub enum ChannelMessageType {
Text = 0,
Voice = 1,
Category = 2,
Thread = 3,
}
impl Encode<'_, Postgres> for ChannelMessageType {
fn encode_by_ref<'q>(&self, buf: &mut <Postgres as Database>::ArgumentBuffer<'q>) -> Result<IsNull, BoxDynError> {
let u8_self: u8 = self.into();
<i32 as Encode<Postgres>>::encode(u8_self as i32, buf)
}
}
impl Decode<'_, Postgres> for ChannelMessageType {
fn decode(value: PgValueRef<'_>) -> Result<Self, BoxDynError> {
let value = <i32 as Decode<Postgres>>::decode(value)?;
Self::try_from(value as u8).map_err(|e| e.into())
}
}
impl Type<Postgres> for ChannelMessageType {
fn type_info() -> <Postgres as Database>::TypeInfo {
<i32 as Type<Postgres>>::type_info()
}
fn compatible(ty: &<Postgres as Database>::TypeInfo) -> bool {
<i32 as Type<Postgres>>::compatible(ty)
}
}
impl std::fmt::Display for ChannelMessageType {
fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
match &self {
ChannelMessageType::Text => write!(f, "Text"),
ChannelMessageType::Voice => write!(f, "Voice"),
ChannelMessageType::Category => write!(f, "Category"),
ChannelMessageType::Thread => write!(f, "Thread"),
}
}
}
impl Into<u8> for &ChannelMessageType {
fn into(self) -> u8 {
match self {
ChannelMessageType::Text => 0,
ChannelMessageType::Voice => 1,
ChannelMessageType::Category => 2,
ChannelMessageType::Thread => 3,
}
}
}
impl TryFrom<u8> for ChannelMessageType {
type Error = anyhow::Error;
fn try_from(value: u8) -> Result<Self, Self::Error> {
match value {
0 => Ok(ChannelMessageType::Text),
1 => Ok(ChannelMessageType::Voice),
2 => Ok(ChannelMessageType::Category),
3 => Ok(ChannelMessageType::Thread),
_ => Err(anyhow!("Unknown submessage type: {}", value))
}
}
}

View File

@ -0,0 +1,2 @@
pub mod text;
pub mod channel;

View File

@ -0,0 +1,72 @@
use std::fmt::Formatter;
use anyhow::anyhow;
use serde::{Deserialize, Serialize};
use sqlx::{Database, Decode, Encode, Postgres, Type};
use sqlx::encode::IsNull;
use sqlx::error::BoxDynError;
use sqlx::postgres::PgValueRef;
#[repr(u8)]
#[derive(Debug, Serialize, Deserialize, Copy, Clone)]
pub enum TextMessageType {
Text = 0,
Reaction = 1,
Attachment = 2
}
impl Encode<'_, Postgres> for TextMessageType {
fn encode_by_ref<'q>(&self, buf: &mut <Postgres as Database>::ArgumentBuffer<'q>) -> Result<IsNull, BoxDynError> {
let u8_self: u8 = self.into();
<i32 as Encode<Postgres>>::encode(u8_self as i32, buf)
}
}
impl Decode<'_, Postgres> for TextMessageType {
fn decode(value: PgValueRef<'_>) -> Result<Self, BoxDynError> {
let value = <i32 as Decode<Postgres>>::decode(value)?;
Self::try_from(value as u8).map_err(|e| e.into())
}
}
impl Type<Postgres> for TextMessageType {
fn type_info() -> <Postgres as Database>::TypeInfo {
<i32 as Type<Postgres>>::type_info()
}
fn compatible(ty: &<Postgres as Database>::TypeInfo) -> bool {
<i32 as Type<Postgres>>::compatible(ty)
}
}
impl std::fmt::Display for TextMessageType {
fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
match &self {
TextMessageType::Text => write!(f, "Text"),
TextMessageType::Reaction => write!(f, "Reaction"),
TextMessageType::Attachment => write!(f, "Attachment"),
}
}
}
impl Into<u8> for &TextMessageType {
fn into(self) -> u8 {
match self {
TextMessageType::Text => 0,
TextMessageType::Reaction => 1,
TextMessageType::Attachment => 2
}
}
}
impl TryFrom<u8> for TextMessageType {
type Error = anyhow::Error;
fn try_from(value: u8) -> Result<Self, Self::Error> {
match value {
0 => Ok(TextMessageType::Text),
1 => Ok(TextMessageType::Reaction),
2 => Ok(TextMessageType::Attachment),
_ => Err(anyhow!("Unknown submessage type: {}", value))
}
}
}

View File

@ -0,0 +1,3 @@
pub mod id;
pub mod message;
pub mod guild;

View File

@ -0,0 +1,21 @@
use serde::Deserialize;
use crate::message::c2s::ClientToServerMessage;
#[derive(Deserialize)]
pub struct LoginMessage {
pub username: String,
pub password: String,
}
impl ClientToServerMessage for LoginMessage {
type ServerToClientMessage = crate::message::s2c::account::login::LoginMessage;
fn create_s2c_message(&self) -> Self::ServerToClientMessage {
Self::ServerToClientMessage {
username: self.username.clone(),
login_successful: false,
login_token: None
}
}
}

View File

@ -0,0 +1,2 @@
pub mod login;
pub mod register;

View File

@ -0,0 +1,21 @@
use serde::Deserialize;
use crate::message::c2s::ClientToServerMessage;
#[derive(Deserialize)]
pub struct RegisterMessage {
pub username: String,
pub email: String,
pub password: String,
}
impl ClientToServerMessage for RegisterMessage {
type ServerToClientMessage = crate::message::s2c::account::register::RegisterMessage;
fn create_s2c_message(&self) -> Self::ServerToClientMessage {
Self::ServerToClientMessage {
username: self.username.clone(),
user_already_exists: true,
user_created: false
}
}
}

View File

@ -0,0 +1,12 @@
use crate::message::s2c::ServerToClientMessage;
pub mod text;
pub mod account;
pub trait ClientToServerMessage {
type ServerToClientMessage: ServerToClientMessage;
/// This should return a default message that must be filled before being returned to the client
fn create_s2c_message(&self) -> Self::ServerToClientMessage;
}

View File

@ -0,0 +1,29 @@
use serde::Deserialize;
use crate::id::message_type::MessageType;
use crate::id::SnowflakeID;
use crate::id::types::text::TextMessageType;
use crate::message::c2s::ClientToServerMessage;
#[derive(Deserialize)]
pub struct AttachmentMessage {
pub file_name: String,
pub content_type: String,
pub file_size: u64,
pub file_contents: Vec<u8>,
}
impl ClientToServerMessage for AttachmentMessage {
type ServerToClientMessage = crate::message::s2c::text::attachment::AttachmentMessage;
fn create_s2c_message(&self) -> Self::ServerToClientMessage {
Self::ServerToClientMessage {
id: SnowflakeID::new_random_hex_loc(
MessageType::Text(TextMessageType::Attachment),
"beefcafe"
).unwrap(),
file_name: self.file_name.clone(),
file_size: self.file_size,
content_type: self.content_type.clone(),
}
}
}

View File

@ -0,0 +1,3 @@
pub mod attachment;
pub mod reaction;
pub mod text;

View File

@ -0,0 +1,26 @@
use serde::Deserialize;
use crate::id::message_type::MessageType;
use crate::id::SnowflakeID;
use crate::id::types::text::TextMessageType;
use crate::message::c2s::ClientToServerMessage;
#[derive(Deserialize)]
pub struct ReactionMessage {
pub emoji_id: SnowflakeID,
pub message_id: SnowflakeID,
}
impl ClientToServerMessage for ReactionMessage {
type ServerToClientMessage = crate::message::s2c::text::reaction::ReactionMessage;
fn create_s2c_message(&self) -> Self::ServerToClientMessage {
Self::ServerToClientMessage {
id: SnowflakeID::new_random_hex_loc(
MessageType::Text(TextMessageType::Reaction),
"beefcafe"
).unwrap(),
emoji_id: self.emoji_id.clone(),
message_id: self.message_id.clone(),
}
}
}

View File

@ -0,0 +1,91 @@
use serde::{Deserialize, Serialize};
use sqlx::{Database, Decode, Encode, Postgres, Type};
use sqlx::encode::IsNull;
use sqlx::error::BoxDynError;
use sqlx::postgres::{PgHasArrayType, PgTypeInfo};
use crate::id::message_type::MessageType;
use crate::id::SnowflakeID;
use crate::id::types::text::TextMessageType;
use crate::message::c2s::ClientToServerMessage;
use crate::message::component::channel::ChannelIdMessageComponent;
use crate::message::component::guild::GuildIdMessageComponent;
#[derive(Deserialize)]
pub struct TextMessage {
pub content: String,
pub channel_id: SnowflakeID,
pub guild_id: Option<SnowflakeID>,
pub attachments: Option<Vec<SnowflakeID>>,
pub reply_to_message_id: Option<SnowflakeID>,
pub tts: bool,
pub embeds: Option<Vec<TextEmbed>>
}
#[derive(Serialize, Deserialize, Clone)]
pub struct TextEmbed {
pub title: Option<String>,
pub content: Option<String>,
pub color: Option<String>,
}
impl PgHasArrayType for TextEmbed {
fn array_type_info() -> PgTypeInfo {
sqlx::types::JsonValue::array_type_info()
}
}
impl Decode<'_, Postgres> for TextEmbed {
fn decode(value: <Postgres as Database>::ValueRef<'_>) -> Result<Self, BoxDynError> {
let value = sqlx::types::JsonValue::decode(value)?;
serde_json::from_value(value).map_err(|e| e.into())
}
}
impl Encode<'_, Postgres> for TextEmbed {
fn encode_by_ref<'q>(&self, buf: &mut <Postgres as Database>::ArgumentBuffer<'q>) -> Result<IsNull, BoxDynError> {
let value = serde_json::to_value(self)?;
value.encode(buf)
}
}
impl Type<Postgres> for TextEmbed {
fn type_info() -> PgTypeInfo {
sqlx::types::JsonValue::type_info()
}
fn compatible(ty: &PgTypeInfo) -> bool {
sqlx::types::JsonValue::compatible(ty)
}
}
impl ClientToServerMessage for TextMessage {
type ServerToClientMessage = crate::message::s2c::text::text::TextMessage;
fn create_s2c_message(&self) -> Self::ServerToClientMessage {
Self::ServerToClientMessage {
id: SnowflakeID::new_random_hex_loc(
MessageType::Text(TextMessageType::Text),
"beefcafe"
).unwrap(),
content: self.content.clone(),
channel_id: self.channel_id.clone(),
guild_id: self.guild_id.clone(),
attachments: self.attachments.clone(),
reply_to_message_id: self.reply_to_message_id.clone(),
tts: self.tts,
embeds: self.embeds.clone(),
}
}
}
impl GuildIdMessageComponent for TextMessage {
fn get_guild_id(&self) -> &Option<SnowflakeID> {
&self.guild_id
}
}
impl ChannelIdMessageComponent for TextMessage {
fn get_channel_id(&self) -> &SnowflakeID {
&self.channel_id
}
}

View File

@ -0,0 +1,5 @@
use crate::id::SnowflakeID;
pub trait ChannelIdMessageComponent {
fn get_channel_id(&self) -> &SnowflakeID;
}

View File

@ -0,0 +1,5 @@
use crate::id::SnowflakeID;
pub trait GuildIdMessageComponent {
fn get_guild_id(&self) -> &Option<SnowflakeID>;
}

View File

@ -0,0 +1,2 @@
pub mod guild;
pub mod channel;

View File

@ -0,0 +1,3 @@
pub mod c2s;
pub mod s2c;
pub mod component;

View File

@ -0,0 +1,13 @@
use serde::Serialize;
use crate::message::s2c::ServerToClientMessage;
#[derive(Serialize)]
pub struct LoginMessage {
pub username: String,
pub login_successful: bool,
pub login_token: Option<String>,
}
impl ServerToClientMessage for LoginMessage {
type ClientToServerMessage = crate::message::c2s::account::login::LoginMessage;
}

View File

@ -0,0 +1,2 @@
pub mod login;
pub mod register;

View File

@ -0,0 +1,13 @@
use serde::Serialize;
use crate::message::s2c::ServerToClientMessage;
#[derive(Serialize)]
pub struct RegisterMessage {
pub username: String,
pub user_already_exists: bool,
pub user_created: bool,
}
impl ServerToClientMessage for RegisterMessage {
type ClientToServerMessage = crate::message::c2s::account::register::RegisterMessage;
}

View File

@ -0,0 +1,8 @@
use crate::message::c2s::ClientToServerMessage;
pub mod text;
pub mod account;
pub trait ServerToClientMessage {
type ClientToServerMessage: ClientToServerMessage;
}

View File

@ -0,0 +1,15 @@
use serde::Serialize;
use crate::id::SnowflakeID;
use crate::message::s2c::ServerToClientMessage;
#[derive(Serialize)]
pub struct AttachmentMessage {
pub id: SnowflakeID,
pub file_name: String,
pub content_type: String,
pub file_size: u64,
}
impl ServerToClientMessage for AttachmentMessage {
type ClientToServerMessage = crate::message::c2s::text::attachment::AttachmentMessage;
}

View File

@ -0,0 +1,3 @@
pub mod attachment;
pub mod reaction;
pub mod text;

View File

@ -0,0 +1,14 @@
use serde::Serialize;
use crate::id::SnowflakeID;
use crate::message::s2c::ServerToClientMessage;
#[derive(Serialize)]
pub struct ReactionMessage {
pub id: SnowflakeID,
pub emoji_id: SnowflakeID,
pub message_id: SnowflakeID,
}
impl ServerToClientMessage for ReactionMessage {
type ClientToServerMessage = crate::message::c2s::text::reaction::ReactionMessage;
}

View File

@ -0,0 +1,20 @@
use serde::Serialize;
use crate::id::SnowflakeID;
use crate::message::c2s::text::text::TextEmbed;
use crate::message::s2c::ServerToClientMessage;
#[derive(Serialize)]
pub struct TextMessage {
pub id: SnowflakeID,
pub content: String,
pub channel_id: SnowflakeID,
pub guild_id: Option<SnowflakeID>,
pub attachments: Option<Vec<SnowflakeID>>,
pub reply_to_message_id: Option<SnowflakeID>,
pub tts: bool,
pub embeds: Option<Vec<TextEmbed>>
}
impl ServerToClientMessage for TextMessage {
type ClientToServerMessage = crate::message::c2s::text::text::TextMessage;
}

View File

@ -0,0 +1,17 @@
[package]
name = "cove-net-server"
version = "0.1.0"
edition = "2024"
[dependencies]
anyhow.workspace = true
async-trait.workspace = true
http-body-util.workspace = true
hyper.workspace = true
hyper-util.workspace = true
scc.workspace = true
tokio.workspace = true
serde_json.workspace = true
cove-net-common.workspace = true
cove-db.workspace = true

138
cove-net/server/src/lib.rs Normal file
View File

@ -0,0 +1,138 @@
pub mod message;
use cove_db::CoveDB;
use std::any::{Any, TypeId};
use std::collections::VecDeque;
use std::net::{IpAddr, SocketAddr};
use http_body_util::{BodyExt, Full};
use hyper::body::{Bytes, Incoming};
use hyper::server::conn::http2;
use hyper::service::Service;
use hyper::{Request, Response, StatusCode};
use std::pin::Pin;
use std::sync::Arc;
use anyhow::Error;
use async_trait::async_trait;
use hyper::http::request::Parts;
use tokio::net::TcpListener;
use hyper_util::rt::TokioIo;
use crate::message::handlers::base::handler::CoveRequestHandler;
use crate::message::handlers::base::middleware::{AssociatedDataMap, CoveRequestMiddleware};
use crate::message::handlers::Handler;
use crate::message::middleware::auth::AuthTokenMiddlewareData;
#[derive(Clone)]
pub struct CoveServer {
pub root_handler: Arc<Handler>,
pub socket_addr: SocketAddr
}
impl CoveServer {
pub async fn new(ip_address: IpAddr, port: u16, root_handler: Arc<Handler>) -> Result<CoveServer, Error> {
let socket_addr = SocketAddr::new(ip_address, port);
Ok(CoveServer {
root_handler,
socket_addr
})
}
pub async fn run(&self) -> Result<(), Error> {
let tcp_listener = TcpListener::bind(self.socket_addr).await?;
loop {
let (stream, _) = tcp_listener.accept().await?;
let io = TokioIo::new(stream);
let cloned = self.clone();
tokio::task::spawn(async move {
if let Err(err) = http2::Builder::new(TokioExecutor)
.serve_connection(io, cloned)
.await
{
eprintln!("Error: {}", err);
}
});
}
}
}
impl Service<Request<Incoming>> for CoveServer {
type Response = Response<Full<Bytes>>;
type Error = Error;
type Future = Pin<Box<dyn Future<Output = Result<Self::Response, Self::Error>> + Send>>;
fn call(&self, request: Request<Incoming>) -> Self::Future {
let root_handler = self.root_handler.clone();
Box::pin(async move {
let uri = request.uri().clone();
let mut path_parts = uri.path().trim_start_matches('/').split('/').collect::<VecDeque<&str>>();
println!("{:?} {:?}", uri, path_parts);
let (parts, body) = request.into_parts();
let body = body.collect().await?.to_bytes();
match root_handler.handle_request_internal(parts, body, &mut path_parts, &mut AssociatedDataMap::new()).await {
Ok(response) => Ok(response),
Err(err) => {
eprintln!("Error: {}", err);
let res = Response::builder()
.status(StatusCode::INTERNAL_SERVER_ERROR)
.body("An error has occured, please try again later".into())?;
Ok(res)
}
}
})
}
}
#[derive(Clone)]
pub struct TokioExecutor;
impl<F> hyper::rt::Executor<F> for TokioExecutor
where
F: Future + Send + 'static,
F::Output: Send + 'static,
{
fn execute(&self, future: F) {
tokio::task::spawn(future);
}
}
pub struct RootHandler;
#[async_trait]
impl CoveRequestHandler for RootHandler {
async fn handle_request(&self, request_parts: Parts, body: Bytes, path: &mut VecDeque<&str>, associated_data: &mut AssociatedDataMap) -> Result<Response<Full<Bytes>>, Error> {
if let Some(data) = associated_data.get_data::<AuthTokenMiddlewareData>() {
println!("{}", data.user_id)
}
if let Some(db) = associated_data.get_data::<DatabaseMiddlewareData>() {
println!("Got db: {:?}", db.db.type_id());
}
Ok(Response::new(Full::new(Bytes::from("Root handler called"))))
}
}
pub struct DatabaseMiddleware {
db: Arc<CoveDB>
}
impl DatabaseMiddleware {
pub fn new(db: Arc<CoveDB>) -> DatabaseMiddleware {
DatabaseMiddleware { db }
}
}
pub struct DatabaseMiddlewareData {
pub db: Arc<CoveDB>
}
#[async_trait]
impl CoveRequestMiddleware for DatabaseMiddleware {
async fn transform_request(&self, _request_parts: &Parts, _body: &Bytes, associated_data: &mut AssociatedDataMap) -> Result<Option<Response<Full<Bytes>>>, Error> {
associated_data.insert(Box::new(DatabaseMiddlewareData {
db: self.db.clone()
}));
Ok(None)
}
}

View File

@ -0,0 +1,31 @@
use hyper::body::Buf;
use http_body_util::BodyExt;
use std::collections::VecDeque;
use anyhow::Error;
use async_trait::async_trait;
use http_body_util::Full;
use hyper::body::{Bytes, Incoming};
use hyper::{Request, Response};
use hyper::http::request::Parts;
use cove_net_common::message::c2s::account::login::LoginMessage;
use cove_net_common::message::c2s::ClientToServerMessage;
use crate::message::handlers::base::handler::{CoveBodyDeserializer, CoveRequestHandler};
use crate::message::handlers::base::middleware::AssociatedDataMap;
pub struct LoginMessageHandler;
#[async_trait]
impl CoveRequestHandler for LoginMessageHandler {
async fn handle_request(&self, request_parts: Parts, body: Bytes, path: &mut VecDeque<&str>, associated_data: &mut AssociatedDataMap) -> Result<Response<Full<Bytes>>, Error> {
let msg = self.body_to_message(body).await?;
Ok(Response::new(
serde_json::to_string(&msg.create_s2c_message())?.into()
))
}
}
#[async_trait]
impl CoveBodyDeserializer<LoginMessage> for LoginMessageHandler {
async fn body_to_message(&self, body: Bytes) -> Result<LoginMessage, Error> {
Ok(serde_json::from_slice(&body)?)
}
}

View File

@ -0,0 +1,2 @@
pub mod login;
pub mod register;

View File

@ -0,0 +1,31 @@
use hyper::body::Buf;
use http_body_util::BodyExt;
use std::collections::VecDeque;
use anyhow::Error;
use async_trait::async_trait;
use http_body_util::Full;
use hyper::body::{Bytes, Incoming};
use hyper::{Request, Response};
use hyper::http::request::Parts;
use cove_net_common::message::c2s::account::register::RegisterMessage;
use cove_net_common::message::c2s::ClientToServerMessage;
use crate::message::handlers::base::handler::{CoveBodyDeserializer, CoveRequestHandler};
use crate::message::handlers::base::middleware::AssociatedDataMap;
pub struct RegisterMessageHandler;
#[async_trait]
impl CoveRequestHandler for RegisterMessageHandler {
async fn handle_request(&self, request_parts: Parts, body: Bytes, path: &mut VecDeque<&str>, associated_data: &mut AssociatedDataMap) -> Result<Response<Full<Bytes>>, Error> {
let msg = self.body_to_message(body).await?;
Ok(Response::new(
serde_json::to_string(&msg.create_s2c_message())?.into()
))
}
}
#[async_trait]
impl CoveBodyDeserializer<RegisterMessage> for RegisterMessageHandler {
async fn body_to_message(&self, body: Bytes) -> Result<RegisterMessage, Error> {
Ok(serde_json::from_slice(&body)?)
}
}

View File

@ -0,0 +1,19 @@
use std::collections::VecDeque;
use anyhow::{Error};
use async_trait::async_trait;
use http_body_util::Full;
use hyper::body::{Bytes, Incoming};
use hyper::{Request, Response};
use hyper::http::request::Parts;
use cove_net_common::message::c2s::ClientToServerMessage;
use crate::message::handlers::base::middleware::{AssociatedDataMap};
#[async_trait]
pub trait CoveRequestHandler: Send + Sync {
async fn handle_request(&self, request_parts: Parts, body: Bytes, path: &mut VecDeque<&str>, associated_data: &mut AssociatedDataMap) -> Result<Response<Full<Bytes>>, Error>;
}
#[async_trait]
pub trait CoveBodyDeserializer<MT: ClientToServerMessage> where Self: CoveRequestHandler {
async fn body_to_message(&self, body: Bytes) -> Result<MT, Error>;
}

View File

@ -0,0 +1,45 @@
use std::any::{Any, TypeId};
use anyhow::Error;
use async_trait::async_trait;
use http_body_util::Full;
use hyper::body::{Bytes, Incoming};
use hyper::{Request, Response};
use hyper::http::request::Parts;
use crate::message::StdHashMap;
#[async_trait]
pub trait CoveRequestMiddleware {
async fn transform_request(&self, request: &Parts, body: &Bytes, associated_data: &mut AssociatedDataMap) -> Result<Option<Response<Full<Bytes>>>, Error>;
}
pub struct AssociatedDataMap {
internal_data: StdHashMap<TypeId, Box<dyn Any + Send + Sync>>,
}
impl AssociatedDataMap {
pub fn new() -> AssociatedDataMap {
AssociatedDataMap {
internal_data: StdHashMap::new(),
}
}
pub fn insert<T: 'static + Send + Sync> (&mut self, data: Box<T>) {
self.internal_data.insert(TypeId::of::<T>(), data);
}
pub fn get_data<T: 'static> (&self) -> Option<&T> {
self.internal_data.get(&TypeId::of::<T>()).map(|data| data.downcast_ref().unwrap())
}
pub fn has_data<T: 'static>(&self) -> bool {
self.internal_data.contains_key(&TypeId::of::<T>())
}
}
impl IntoIterator for AssociatedDataMap {
type Item = (TypeId, Box<dyn Any + Send + Sync>);
type IntoIter = std::collections::hash_map::IntoIter<TypeId, Box<dyn Any + Send + Sync>>;
fn into_iter(self) -> Self::IntoIter {
self.internal_data.into_iter()
}
}

View File

@ -0,0 +1,2 @@
pub mod middleware;
pub mod handler;

Some files were not shown because too many files have changed in this diff Show More