Initial Commit
This commit is contained in:
commit
e27a0d33d7
1
.gitignore
vendored
Normal file
1
.gitignore
vendored
Normal file
@ -0,0 +1 @@
|
|||||||
|
/target
|
||||||
8
.idea/.gitignore
vendored
Normal file
8
.idea/.gitignore
vendored
Normal 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
17
.idea/cove-chat.iml
Normal 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
8
.idea/modules.xml
Normal 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
23
.idea/sqldialects.xml
Normal 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
6
.idea/vcs.xml
Normal 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
2809
Cargo.lock
generated
Normal file
File diff suppressed because it is too large
Load Diff
28
Cargo.toml
Normal file
28
Cargo.toml
Normal 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
13
bin-test/Cargo.toml
Normal 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"
|
||||||
30
bin-test/migrations/01_user.sql
Normal file
30
bin-test/migrations/01_user.sql
Normal 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()
|
||||||
|
);
|
||||||
43
bin-test/migrations/02_messages.sql
Normal file
43
bin-test/migrations/02_messages.sql
Normal 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);
|
||||||
24
bin-test/migrations/03_nonce.sql
Normal file
24
bin-test/migrations/03_nonce.sql
Normal 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();
|
||||||
44
bin-test/migrations/04_channels.sql
Normal file
44
bin-test/migrations/04_channels.sql
Normal 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 (0–21600)
|
||||||
|
|
||||||
|
-- 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;
|
||||||
56
bin-test/migrations/05_guilds.sql
Normal file
56
bin-test/migrations/05_guilds.sql
Normal 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);
|
||||||
36
bin-test/migrations/06_guild_members.sql
Normal file
36
bin-test/migrations/06_guild_members.sql
Normal 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);
|
||||||
9
bin-test/migrations/07_attachments.sql
Normal file
9
bin-test/migrations/07_attachments.sql
Normal 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)
|
||||||
|
)
|
||||||
0
bin-test/migrations/101_user-foreign.sql
Normal file
0
bin-test/migrations/101_user-foreign.sql
Normal file
5
bin-test/migrations/102_messages-foreign.sql
Normal file
5
bin-test/migrations/102_messages-foreign.sql
Normal 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
|
||||||
0
bin-test/migrations/103_nonce-foreign.sql
Normal file
0
bin-test/migrations/103_nonce-foreign.sql
Normal file
4
bin-test/migrations/104_channels-foreign.sql
Normal file
4
bin-test/migrations/104_channels-foreign.sql
Normal 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
|
||||||
32
bin-test/migrations/105_guilds-foreign.sql
Normal file
32
bin-test/migrations/105_guilds-foreign.sql
Normal 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();
|
||||||
4
bin-test/migrations/106_guild_members-foreign.sql
Normal file
4
bin-test/migrations/106_guild_members-foreign.sql
Normal 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
|
||||||
3
bin-test/migrations/107_attachments-foreign.sql
Normal file
3
bin-test/migrations/107_attachments-foreign.sql
Normal 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
116
bin-test/src/main.rs
Normal 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)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
21
bin-test/system-migrations/201_nonce-management.sql
Normal file
21
bin-test/system-migrations/201_nonce-management.sql
Normal 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
1
cove-db/.env
Normal file
@ -0,0 +1 @@
|
|||||||
|
DATABASE_URL="postgres://postgres:password@localhost:5432/"
|
||||||
11
cove-db/Cargo.toml
Normal file
11
cove-db/Cargo.toml
Normal 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
|
||||||
30
cove-db/migrations/01_user.sql
Normal file
30
cove-db/migrations/01_user.sql
Normal 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()
|
||||||
|
);
|
||||||
43
cove-db/migrations/02_messages.sql
Normal file
43
cove-db/migrations/02_messages.sql
Normal 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);
|
||||||
24
cove-db/migrations/03_nonce.sql
Normal file
24
cove-db/migrations/03_nonce.sql
Normal 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();
|
||||||
45
cove-db/migrations/04_channels.sql
Normal file
45
cove-db/migrations/04_channels.sql
Normal 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 (0–21600)
|
||||||
|
|
||||||
|
-- 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;
|
||||||
56
cove-db/migrations/05_guilds.sql
Normal file
56
cove-db/migrations/05_guilds.sql
Normal 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);
|
||||||
36
cove-db/migrations/06_guild_members.sql
Normal file
36
cove-db/migrations/06_guild_members.sql
Normal 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);
|
||||||
0
cove-db/migrations/101_user-foreign.sql
Normal file
0
cove-db/migrations/101_user-foreign.sql
Normal file
5
cove-db/migrations/102_messages-foreign.sql
Normal file
5
cove-db/migrations/102_messages-foreign.sql
Normal 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
|
||||||
0
cove-db/migrations/103_nonce-foreign.sql
Normal file
0
cove-db/migrations/103_nonce-foreign.sql
Normal file
4
cove-db/migrations/104_channels-foreign.sql
Normal file
4
cove-db/migrations/104_channels-foreign.sql
Normal 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
|
||||||
32
cove-db/migrations/105_guilds-foreign.sql
Normal file
32
cove-db/migrations/105_guilds-foreign.sql
Normal 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();
|
||||||
4
cove-db/migrations/106_guild_members-foreign.sql
Normal file
4
cove-db/migrations/106_guild_members-foreign.sql
Normal 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
78
cove-db/src/lib.rs
Normal 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())
|
||||||
|
}
|
||||||
|
}
|
||||||
83
cove-db/src/part/condition.rs
Normal file
83
cove-db/src/part/condition.rs
Normal 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(())
|
||||||
|
}
|
||||||
|
}
|
||||||
75
cove-db/src/part/insert.rs
Normal file
75
cove-db/src/part/insert.rs
Normal 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
95
cove-db/src/part/mod.rs
Normal 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
|
||||||
|
}
|
||||||
|
}
|
||||||
39
cove-db/src/part/select.rs
Normal file
39
cove-db/src/part/select.rs
Normal 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(())
|
||||||
|
}
|
||||||
|
}
|
||||||
164
cove-db/src/part/sql_where.rs
Normal file
164
cove-db/src/part/sql_where.rs
Normal 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
1
cove-db/src/query/mod.rs
Normal file
@ -0,0 +1 @@
|
|||||||
|
pub mod text;
|
||||||
59
cove-db/src/query/text.rs
Normal file
59
cove-db/src/query/text.rs
Normal 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 {}
|
||||||
77
cove-db/src/rows/attachment.rs
Normal file
77
cove-db/src/rows/attachment.rs
Normal 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
156
cove-db/src/rows/channel.rs
Normal 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
200
cove-db/src/rows/guild.rs
Normal 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")?,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
117
cove-db/src/rows/guild_member.rs
Normal file
117
cove-db/src/rows/guild_member.rs
Normal 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
155
cove-db/src/rows/message.rs
Normal 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
45
cove-db/src/rows/mod.rs
Normal 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
77
cove-db/src/rows/nonce.rs
Normal 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
205
cove-db/src/rows/user.rs
Normal 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
1
cove-db/src/types/mod.rs
Normal file
@ -0,0 +1 @@
|
|||||||
|
pub mod user_status;
|
||||||
54
cove-db/src/types/user_status.rs
Normal file
54
cove-db/src/types/user_status.rs
Normal 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",
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
21
cove-db/system-migrations/201_nonce-management.sql
Normal file
21
cove-db/system-migrations/201_nonce-management.sql
Normal 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
|
||||||
|
);
|
||||||
6
cove-net/client/Cargo.toml
Normal file
6
cove-net/client/Cargo.toml
Normal file
@ -0,0 +1,6 @@
|
|||||||
|
[package]
|
||||||
|
name = "cove-net-client"
|
||||||
|
version = "0.1.0"
|
||||||
|
edition = "2024"
|
||||||
|
|
||||||
|
[dependencies]
|
||||||
14
cove-net/client/src/lib.rs
Normal file
14
cove-net/client/src/lib.rs
Normal 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);
|
||||||
|
}
|
||||||
|
}
|
||||||
13
cove-net/common/Cargo.toml
Normal file
13
cove-net/common/Cargo.toml
Normal 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
|
||||||
53
cove-net/common/src/guild/component/default_notification.rs
Normal file
53
cove-net/common/src/guild/component/default_notification.rs
Normal 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)?
|
||||||
|
}
|
||||||
|
}
|
||||||
52
cove-net/common/src/guild/component/explicit_filter.rs
Normal file
52
cove-net/common/src/guild/component/explicit_filter.rs
Normal 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)?
|
||||||
|
}
|
||||||
|
}
|
||||||
3
cove-net/common/src/guild/component/mod.rs
Normal file
3
cove-net/common/src/guild/component/mod.rs
Normal file
@ -0,0 +1,3 @@
|
|||||||
|
pub mod verification_level;
|
||||||
|
pub mod default_notification;
|
||||||
|
pub mod explicit_filter;
|
||||||
56
cove-net/common/src/guild/component/verification_level.rs
Normal file
56
cove-net/common/src/guild/component/verification_level.rs
Normal 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)?
|
||||||
|
}
|
||||||
|
}
|
||||||
1
cove-net/common/src/guild/mod.rs
Normal file
1
cove-net/common/src/guild/mod.rs
Normal file
@ -0,0 +1 @@
|
|||||||
|
pub mod component;
|
||||||
76
cove-net/common/src/id/message_type.rs
Normal file
76
cove-net/common/src/id/message_type.rs
Normal 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])),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
176
cove-net/common/src/id/mod.rs
Normal file
176
cove-net/common/src/id/mod.rs
Normal 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)
|
||||||
|
} }
|
||||||
|
}
|
||||||
75
cove-net/common/src/id/types/channel.rs
Normal file
75
cove-net/common/src/id/types/channel.rs
Normal 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))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
2
cove-net/common/src/id/types/mod.rs
Normal file
2
cove-net/common/src/id/types/mod.rs
Normal file
@ -0,0 +1,2 @@
|
|||||||
|
pub mod text;
|
||||||
|
pub mod channel;
|
||||||
72
cove-net/common/src/id/types/text.rs
Normal file
72
cove-net/common/src/id/types/text.rs
Normal 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))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
3
cove-net/common/src/lib.rs
Normal file
3
cove-net/common/src/lib.rs
Normal file
@ -0,0 +1,3 @@
|
|||||||
|
pub mod id;
|
||||||
|
pub mod message;
|
||||||
|
pub mod guild;
|
||||||
21
cove-net/common/src/message/c2s/account/login.rs
Normal file
21
cove-net/common/src/message/c2s/account/login.rs
Normal 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
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
2
cove-net/common/src/message/c2s/account/mod.rs
Normal file
2
cove-net/common/src/message/c2s/account/mod.rs
Normal file
@ -0,0 +1,2 @@
|
|||||||
|
pub mod login;
|
||||||
|
pub mod register;
|
||||||
21
cove-net/common/src/message/c2s/account/register.rs
Normal file
21
cove-net/common/src/message/c2s/account/register.rs
Normal 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
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
12
cove-net/common/src/message/c2s/mod.rs
Normal file
12
cove-net/common/src/message/c2s/mod.rs
Normal 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;
|
||||||
|
}
|
||||||
|
|
||||||
29
cove-net/common/src/message/c2s/text/attachment.rs
Normal file
29
cove-net/common/src/message/c2s/text/attachment.rs
Normal 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(),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
3
cove-net/common/src/message/c2s/text/mod.rs
Normal file
3
cove-net/common/src/message/c2s/text/mod.rs
Normal file
@ -0,0 +1,3 @@
|
|||||||
|
pub mod attachment;
|
||||||
|
pub mod reaction;
|
||||||
|
pub mod text;
|
||||||
26
cove-net/common/src/message/c2s/text/reaction.rs
Normal file
26
cove-net/common/src/message/c2s/text/reaction.rs
Normal 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(),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
91
cove-net/common/src/message/c2s/text/text.rs
Normal file
91
cove-net/common/src/message/c2s/text/text.rs
Normal 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
|
||||||
|
}
|
||||||
|
}
|
||||||
5
cove-net/common/src/message/component/channel.rs
Normal file
5
cove-net/common/src/message/component/channel.rs
Normal file
@ -0,0 +1,5 @@
|
|||||||
|
use crate::id::SnowflakeID;
|
||||||
|
|
||||||
|
pub trait ChannelIdMessageComponent {
|
||||||
|
fn get_channel_id(&self) -> &SnowflakeID;
|
||||||
|
}
|
||||||
5
cove-net/common/src/message/component/guild.rs
Normal file
5
cove-net/common/src/message/component/guild.rs
Normal file
@ -0,0 +1,5 @@
|
|||||||
|
use crate::id::SnowflakeID;
|
||||||
|
|
||||||
|
pub trait GuildIdMessageComponent {
|
||||||
|
fn get_guild_id(&self) -> &Option<SnowflakeID>;
|
||||||
|
}
|
||||||
2
cove-net/common/src/message/component/mod.rs
Normal file
2
cove-net/common/src/message/component/mod.rs
Normal file
@ -0,0 +1,2 @@
|
|||||||
|
pub mod guild;
|
||||||
|
pub mod channel;
|
||||||
3
cove-net/common/src/message/mod.rs
Normal file
3
cove-net/common/src/message/mod.rs
Normal file
@ -0,0 +1,3 @@
|
|||||||
|
pub mod c2s;
|
||||||
|
pub mod s2c;
|
||||||
|
pub mod component;
|
||||||
13
cove-net/common/src/message/s2c/account/login.rs
Normal file
13
cove-net/common/src/message/s2c/account/login.rs
Normal 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;
|
||||||
|
}
|
||||||
2
cove-net/common/src/message/s2c/account/mod.rs
Normal file
2
cove-net/common/src/message/s2c/account/mod.rs
Normal file
@ -0,0 +1,2 @@
|
|||||||
|
pub mod login;
|
||||||
|
pub mod register;
|
||||||
13
cove-net/common/src/message/s2c/account/register.rs
Normal file
13
cove-net/common/src/message/s2c/account/register.rs
Normal 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;
|
||||||
|
}
|
||||||
8
cove-net/common/src/message/s2c/mod.rs
Normal file
8
cove-net/common/src/message/s2c/mod.rs
Normal file
@ -0,0 +1,8 @@
|
|||||||
|
use crate::message::c2s::ClientToServerMessage;
|
||||||
|
|
||||||
|
pub mod text;
|
||||||
|
pub mod account;
|
||||||
|
|
||||||
|
pub trait ServerToClientMessage {
|
||||||
|
type ClientToServerMessage: ClientToServerMessage;
|
||||||
|
}
|
||||||
15
cove-net/common/src/message/s2c/text/attachment.rs
Normal file
15
cove-net/common/src/message/s2c/text/attachment.rs
Normal 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;
|
||||||
|
}
|
||||||
3
cove-net/common/src/message/s2c/text/mod.rs
Normal file
3
cove-net/common/src/message/s2c/text/mod.rs
Normal file
@ -0,0 +1,3 @@
|
|||||||
|
pub mod attachment;
|
||||||
|
pub mod reaction;
|
||||||
|
pub mod text;
|
||||||
14
cove-net/common/src/message/s2c/text/reaction.rs
Normal file
14
cove-net/common/src/message/s2c/text/reaction.rs
Normal 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;
|
||||||
|
}
|
||||||
20
cove-net/common/src/message/s2c/text/text.rs
Normal file
20
cove-net/common/src/message/s2c/text/text.rs
Normal 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;
|
||||||
|
}
|
||||||
17
cove-net/server/Cargo.toml
Normal file
17
cove-net/server/Cargo.toml
Normal 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
138
cove-net/server/src/lib.rs
Normal 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)
|
||||||
|
}
|
||||||
|
}
|
||||||
31
cove-net/server/src/message/handlers/account/login.rs
Normal file
31
cove-net/server/src/message/handlers/account/login.rs
Normal 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)?)
|
||||||
|
}
|
||||||
|
}
|
||||||
2
cove-net/server/src/message/handlers/account/mod.rs
Normal file
2
cove-net/server/src/message/handlers/account/mod.rs
Normal file
@ -0,0 +1,2 @@
|
|||||||
|
pub mod login;
|
||||||
|
pub mod register;
|
||||||
31
cove-net/server/src/message/handlers/account/register.rs
Normal file
31
cove-net/server/src/message/handlers/account/register.rs
Normal 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)?)
|
||||||
|
}
|
||||||
|
}
|
||||||
19
cove-net/server/src/message/handlers/base/handler.rs
Normal file
19
cove-net/server/src/message/handlers/base/handler.rs
Normal 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>;
|
||||||
|
}
|
||||||
45
cove-net/server/src/message/handlers/base/middleware.rs
Normal file
45
cove-net/server/src/message/handlers/base/middleware.rs
Normal 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()
|
||||||
|
}
|
||||||
|
}
|
||||||
2
cove-net/server/src/message/handlers/base/mod.rs
Normal file
2
cove-net/server/src/message/handlers/base/mod.rs
Normal 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
Loading…
Reference in New Issue
Block a user