First commit in rust
This commit is contained in:
18
backchannel-common/Cargo.toml
Normal file
18
backchannel-common/Cargo.toml
Normal file
@@ -0,0 +1,18 @@
|
||||
[package]
|
||||
name = "backchannel-common"
|
||||
version = "0.1.0"
|
||||
edition = "2021"
|
||||
|
||||
[dependencies]
|
||||
serde = { workspace = true }
|
||||
serde_json = { workspace = true }
|
||||
uuid = { workspace = true }
|
||||
chrono = { workspace = true }
|
||||
thiserror = { workspace = true }
|
||||
base64 = { workspace = true }
|
||||
rand = { workspace = true }
|
||||
ed25519-dalek = { workspace = true }
|
||||
x25519-dalek = { workspace = true }
|
||||
chacha20poly1305 = { workspace = true }
|
||||
hkdf = { workspace = true }
|
||||
sha2 = { workspace = true }
|
||||
59
backchannel-common/src/crypto/aead.rs
Normal file
59
backchannel-common/src/crypto/aead.rs
Normal file
@@ -0,0 +1,59 @@
|
||||
use base64::{engine::general_purpose::STANDARD as B64, Engine};
|
||||
use chacha20poly1305::{
|
||||
aead::{Aead, KeyInit},
|
||||
ChaCha20Poly1305, Key, Nonce,
|
||||
};
|
||||
use hkdf::Hkdf;
|
||||
use rand::RngCore;
|
||||
use rand::rngs::OsRng;
|
||||
use sha2::Sha256;
|
||||
|
||||
use crate::error::{BackchannelError, Result};
|
||||
|
||||
const HKDF_INFO: &[u8] = b"backchannel-dm-v1";
|
||||
const HKDF_SALT: &[u8] = b"backchannel-salt-v1";
|
||||
|
||||
/// Derive a 32-byte ChaCha20-Poly1305 key from a raw X25519 shared secret
|
||||
/// using HKDF-SHA256. The raw DH output should never be used directly.
|
||||
pub fn derive_dm_key(raw_secret: &[u8]) -> [u8; 32] {
|
||||
let hk = Hkdf::<Sha256>::new(Some(HKDF_SALT), raw_secret);
|
||||
let mut okm = [0u8; 32];
|
||||
hk.expand(HKDF_INFO, &mut okm)
|
||||
.expect("32-byte output always fits HKDF-SHA256");
|
||||
okm
|
||||
}
|
||||
|
||||
/// Encrypt `plaintext` with a 32-byte key.
|
||||
///
|
||||
/// Returns `(ciphertext_b64, nonce_b64)`. A fresh random nonce is generated
|
||||
/// for every call — callers must transmit both values to the recipient.
|
||||
pub fn encrypt(key_bytes: &[u8; 32], plaintext: &[u8]) -> Result<(String, String)> {
|
||||
let key = Key::from_slice(key_bytes);
|
||||
let cipher = ChaCha20Poly1305::new(key);
|
||||
|
||||
let mut nonce_bytes = [0u8; 12];
|
||||
OsRng.fill_bytes(&mut nonce_bytes);
|
||||
let nonce = Nonce::from(nonce_bytes);
|
||||
|
||||
let ciphertext = cipher
|
||||
.encrypt(&nonce, plaintext)
|
||||
.map_err(|_| BackchannelError::Crypto("encryption failed".into()))?;
|
||||
|
||||
Ok((B64.encode(&ciphertext), B64.encode(nonce_bytes)))
|
||||
}
|
||||
|
||||
/// Decrypt `ciphertext_b64` using `key_bytes` and `nonce_b64`.
|
||||
pub fn decrypt(key_bytes: &[u8; 32], nonce_b64: &str, ciphertext_b64: &str) -> Result<Vec<u8>> {
|
||||
let key = Key::from_slice(key_bytes);
|
||||
let cipher = ChaCha20Poly1305::new(key);
|
||||
|
||||
let nonce_bytes = B64.decode(nonce_b64)?;
|
||||
let nonce_arr: [u8; 12] = nonce_bytes
|
||||
.try_into()
|
||||
.map_err(|_| BackchannelError::InvalidKeyLength)?;
|
||||
let nonce = Nonce::from(nonce_arr);
|
||||
|
||||
let ciphertext = B64.decode(ciphertext_b64)?;
|
||||
let plaintext = cipher.decrypt(&nonce, ciphertext.as_ref())?;
|
||||
Ok(plaintext)
|
||||
}
|
||||
37
backchannel-common/src/crypto/ecdh.rs
Normal file
37
backchannel-common/src/crypto/ecdh.rs
Normal file
@@ -0,0 +1,37 @@
|
||||
use base64::{engine::general_purpose::STANDARD as B64, Engine};
|
||||
use rand::rngs::OsRng;
|
||||
use x25519_dalek::{EphemeralSecret, PublicKey};
|
||||
|
||||
use crate::error::{BackchannelError, Result};
|
||||
|
||||
/// Generate an X25519 ephemeral keypair for one DM key exchange.
|
||||
///
|
||||
/// The `EphemeralSecret` is consumed by `diffie_hellman`, ensuring it cannot
|
||||
/// be reused. Call `derive_dm_key` (in `aead`) on the resulting shared secret.
|
||||
pub fn generate_ephemeral() -> (EphemeralSecret, PublicKey) {
|
||||
let secret = EphemeralSecret::random_from_rng(OsRng);
|
||||
let public = PublicKey::from(&secret);
|
||||
(secret, public)
|
||||
}
|
||||
|
||||
/// Encode an X25519 public key to base64.
|
||||
pub fn pubkey_to_b64(key: &PublicKey) -> String {
|
||||
B64.encode(key.as_bytes())
|
||||
}
|
||||
|
||||
/// Decode a base64 string to an X25519 public key.
|
||||
pub fn b64_to_pubkey(s: &str) -> Result<PublicKey> {
|
||||
let bytes = B64.decode(s)?;
|
||||
let arr: [u8; 32] = bytes
|
||||
.try_into()
|
||||
.map_err(|_| BackchannelError::InvalidKeyLength)?;
|
||||
Ok(PublicKey::from(arr))
|
||||
}
|
||||
|
||||
/// Perform X25519 Diffie-Hellman and return the 32-byte shared secret.
|
||||
///
|
||||
/// `secret` is consumed (ephemeral), preventing reuse.
|
||||
pub fn diffie_hellman(secret: EphemeralSecret, their_pubkey: &PublicKey) -> [u8; 32] {
|
||||
let shared = secret.diffie_hellman(their_pubkey);
|
||||
*shared.as_bytes()
|
||||
}
|
||||
67
backchannel-common/src/crypto/identity.rs
Normal file
67
backchannel-common/src/crypto/identity.rs
Normal file
@@ -0,0 +1,67 @@
|
||||
use base64::{engine::general_purpose::STANDARD as B64, Engine};
|
||||
use ed25519_dalek::{Signature, SigningKey, VerifyingKey};
|
||||
use rand::rngs::OsRng;
|
||||
|
||||
use crate::error::{BackchannelError, Result};
|
||||
|
||||
/// Generate a new Ed25519 identity keypair.
|
||||
pub fn generate_keypair() -> (SigningKey, VerifyingKey) {
|
||||
let signing_key = SigningKey::generate(&mut OsRng);
|
||||
let verifying_key = signing_key.verifying_key();
|
||||
(signing_key, verifying_key)
|
||||
}
|
||||
|
||||
/// Encode an Ed25519 public key to a base64 string (32 bytes → ~44 chars).
|
||||
pub fn pubkey_to_b64(key: &VerifyingKey) -> String {
|
||||
B64.encode(key.as_bytes())
|
||||
}
|
||||
|
||||
/// Decode a base64 string back to an Ed25519 public key.
|
||||
pub fn b64_to_pubkey(s: &str) -> Result<VerifyingKey> {
|
||||
let bytes = B64.decode(s)?;
|
||||
let arr: [u8; 32] = bytes
|
||||
.try_into()
|
||||
.map_err(|_| BackchannelError::InvalidKeyLength)?;
|
||||
VerifyingKey::from_bytes(&arr).map_err(|e| BackchannelError::Crypto(e.to_string()))
|
||||
}
|
||||
|
||||
/// Encode a signing key (private key bytes) to base64 for keystore persistence.
|
||||
pub fn signing_key_to_b64(key: &SigningKey) -> String {
|
||||
B64.encode(key.to_bytes())
|
||||
}
|
||||
|
||||
/// Decode a base64 string back to a signing key.
|
||||
pub fn b64_to_signing_key(s: &str) -> Result<SigningKey> {
|
||||
let bytes = B64.decode(s)?;
|
||||
let arr: [u8; 32] = bytes
|
||||
.try_into()
|
||||
.map_err(|_| BackchannelError::InvalidKeyLength)?;
|
||||
Ok(SigningKey::from_bytes(&arr))
|
||||
}
|
||||
|
||||
/// Sign a message with an Ed25519 signing key.
|
||||
pub fn sign(key: &SigningKey, message: &[u8]) -> Signature {
|
||||
use ed25519_dalek::Signer;
|
||||
key.sign(message)
|
||||
}
|
||||
|
||||
/// Verify an Ed25519 signature.
|
||||
pub fn verify(key: &VerifyingKey, message: &[u8], sig: &Signature) -> Result<()> {
|
||||
use ed25519_dalek::Verifier;
|
||||
key.verify(message, sig)?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Encode a `Signature` to base64.
|
||||
pub fn sig_to_b64(sig: &Signature) -> String {
|
||||
B64.encode(sig.to_bytes())
|
||||
}
|
||||
|
||||
/// Decode a base64 string back to a `Signature`.
|
||||
pub fn b64_to_sig(s: &str) -> Result<Signature> {
|
||||
let bytes = B64.decode(s)?;
|
||||
let arr: [u8; 64] = bytes
|
||||
.try_into()
|
||||
.map_err(|_| BackchannelError::InvalidKeyLength)?;
|
||||
Ok(Signature::from_bytes(&arr))
|
||||
}
|
||||
3
backchannel-common/src/crypto/mod.rs
Normal file
3
backchannel-common/src/crypto/mod.rs
Normal file
@@ -0,0 +1,3 @@
|
||||
pub mod aead;
|
||||
pub mod ecdh;
|
||||
pub mod identity;
|
||||
39
backchannel-common/src/error.rs
Normal file
39
backchannel-common/src/error.rs
Normal file
@@ -0,0 +1,39 @@
|
||||
use thiserror::Error;
|
||||
|
||||
pub type Result<T> = std::result::Result<T, BackchannelError>;
|
||||
|
||||
#[derive(Debug, Error)]
|
||||
pub enum BackchannelError {
|
||||
#[error("cryptographic error: {0}")]
|
||||
Crypto(String),
|
||||
|
||||
#[error("base64 decode error: {0}")]
|
||||
Base64(#[from] base64::DecodeError),
|
||||
|
||||
#[error("serialization error: {0}")]
|
||||
Serialization(#[from] serde_json::Error),
|
||||
|
||||
#[error("invalid key length")]
|
||||
InvalidKeyLength,
|
||||
|
||||
#[error("decryption failed: authentication tag mismatch")]
|
||||
DecryptionFailed,
|
||||
|
||||
#[error("signature verification failed")]
|
||||
InvalidSignature,
|
||||
|
||||
#[error("{0}")]
|
||||
Other(String),
|
||||
}
|
||||
|
||||
impl From<chacha20poly1305::Error> for BackchannelError {
|
||||
fn from(_: chacha20poly1305::Error) -> Self {
|
||||
BackchannelError::DecryptionFailed
|
||||
}
|
||||
}
|
||||
|
||||
impl From<ed25519_dalek::SignatureError> for BackchannelError {
|
||||
fn from(_: ed25519_dalek::SignatureError) -> Self {
|
||||
BackchannelError::InvalidSignature
|
||||
}
|
||||
}
|
||||
6
backchannel-common/src/lib.rs
Normal file
6
backchannel-common/src/lib.rs
Normal file
@@ -0,0 +1,6 @@
|
||||
pub mod crypto;
|
||||
pub mod error;
|
||||
pub mod protocol;
|
||||
pub mod types;
|
||||
|
||||
pub use error::{BackchannelError, Result};
|
||||
121
backchannel-common/src/protocol/client.rs
Normal file
121
backchannel-common/src/protocol/client.rs
Normal file
@@ -0,0 +1,121 @@
|
||||
use serde::{Deserialize, Serialize};
|
||||
use uuid::Uuid;
|
||||
|
||||
/// Every message the client can send to the server over the WebSocket.
|
||||
///
|
||||
/// Serialised as JSON with an inline `"type"` discriminant, e.g.:
|
||||
/// `{"type":"login","username":"alice","password":"...","identity_pubkey":"..."}`
|
||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||
#[serde(tag = "type", rename_all = "snake_case")]
|
||||
pub enum ClientMessage {
|
||||
// ── Authentication ────────────────────────────────────────────────────
|
||||
/// First-time authentication with credentials.
|
||||
Login {
|
||||
username: String,
|
||||
/// Sent in plaintext; the TLS layer protects this in transit.
|
||||
password: String,
|
||||
/// Base64-encoded Ed25519 public key (32 bytes).
|
||||
/// Stored/updated server-side as the user's cryptographic identity.
|
||||
identity_pubkey: String,
|
||||
},
|
||||
|
||||
/// Register a new account.
|
||||
Register {
|
||||
username: String,
|
||||
password: String,
|
||||
identity_pubkey: String,
|
||||
},
|
||||
|
||||
/// Re-establish an authenticated session using a previously issued JWT.
|
||||
/// Avoids re-entering the password on reconnect.
|
||||
ResumeSession {
|
||||
token: String,
|
||||
},
|
||||
|
||||
/// Gracefully end the session; server invalidates the JWT.
|
||||
Logout,
|
||||
|
||||
// ── Channel messaging ─────────────────────────────────────────────────
|
||||
/// Post a plaintext message to a channel.
|
||||
SendChannelMessage {
|
||||
channel_id: Uuid,
|
||||
content: String,
|
||||
},
|
||||
|
||||
/// Request recent message history for a channel.
|
||||
FetchChannelHistory {
|
||||
channel_id: Uuid,
|
||||
/// Pagination: fetch messages older than this ID.
|
||||
before_message_id: Option<Uuid>,
|
||||
limit: u32,
|
||||
},
|
||||
|
||||
// ── Direct messages (E2EE) ────────────────────────────────────────────
|
||||
/// Phase 1 of DM key exchange: initiator sends their X25519 ephemeral
|
||||
/// public key to the server, addressed to the target recipient.
|
||||
InitDmKeyExchange {
|
||||
recipient_id: Uuid,
|
||||
/// Base64-encoded X25519 ephemeral public key (32 bytes).
|
||||
sender_ephemeral_pubkey: String,
|
||||
},
|
||||
|
||||
/// Phase 2 of DM key exchange: recipient responds with their ephemeral
|
||||
/// public key. Both sides can now derive the same shared secret.
|
||||
AcceptDmKeyExchange {
|
||||
initiator_id: Uuid,
|
||||
/// Base64-encoded X25519 ephemeral public key (32 bytes).
|
||||
recipient_ephemeral_pubkey: String,
|
||||
},
|
||||
|
||||
/// Send an E2EE DM. The server relays the ciphertext without decrypting.
|
||||
SendDm {
|
||||
recipient_id: Uuid,
|
||||
/// Base64-encoded ChaCha20-Poly1305 ciphertext.
|
||||
ciphertext: String,
|
||||
/// Base64-encoded nonce (12 bytes).
|
||||
nonce: String,
|
||||
},
|
||||
|
||||
/// Request DM history with a specific peer.
|
||||
FetchDmHistory {
|
||||
peer_id: Uuid,
|
||||
before_message_id: Option<Uuid>,
|
||||
limit: u32,
|
||||
},
|
||||
|
||||
// ── Channel management ────────────────────────────────────────────────
|
||||
CreateChannel {
|
||||
name: String,
|
||||
topic: Option<String>,
|
||||
},
|
||||
|
||||
DeleteChannel {
|
||||
channel_id: Uuid,
|
||||
},
|
||||
|
||||
// ── Role & permission management ──────────────────────────────────────
|
||||
CreateRole {
|
||||
name: String,
|
||||
/// Permission bitmask; see `PermissionFlags` in `types.rs`.
|
||||
permissions: u64,
|
||||
},
|
||||
|
||||
AssignRole {
|
||||
user_id: Uuid,
|
||||
role_id: Uuid,
|
||||
},
|
||||
|
||||
RevokeRole {
|
||||
user_id: Uuid,
|
||||
role_id: Uuid,
|
||||
},
|
||||
|
||||
// ── Key directory ─────────────────────────────────────────────────────
|
||||
/// Look up another user's registered Ed25519 identity public key.
|
||||
QueryIdentityKey {
|
||||
user_id: Uuid,
|
||||
},
|
||||
|
||||
// ── Keepalive ─────────────────────────────────────────────────────────
|
||||
Ping,
|
||||
}
|
||||
5
backchannel-common/src/protocol/mod.rs
Normal file
5
backchannel-common/src/protocol/mod.rs
Normal file
@@ -0,0 +1,5 @@
|
||||
pub mod client;
|
||||
pub mod server;
|
||||
|
||||
pub use client::ClientMessage;
|
||||
pub use server::{HistoricChannelMessage, HistoricDm, ServerMessage};
|
||||
141
backchannel-common/src/protocol/server.rs
Normal file
141
backchannel-common/src/protocol/server.rs
Normal file
@@ -0,0 +1,141 @@
|
||||
use chrono::{DateTime, Utc};
|
||||
use serde::{Deserialize, Serialize};
|
||||
use uuid::Uuid;
|
||||
|
||||
/// Every message the server can push to a client over the WebSocket.
|
||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||
#[serde(tag = "type", rename_all = "snake_case")]
|
||||
pub enum ServerMessage {
|
||||
// ── Auth responses ────────────────────────────────────────────────────
|
||||
AuthSuccess {
|
||||
user_id: Uuid,
|
||||
username: String,
|
||||
/// Signed JWT. Client stores this for reconnect via `ResumeSession`.
|
||||
session_token: String,
|
||||
},
|
||||
|
||||
AuthError {
|
||||
reason: String,
|
||||
},
|
||||
|
||||
// ── Channel events ────────────────────────────────────────────────────
|
||||
/// Broadcast to all connected clients when a channel message is posted.
|
||||
ChannelMessage {
|
||||
message_id: Uuid,
|
||||
channel_id: Uuid,
|
||||
author_id: Uuid,
|
||||
author_username: String,
|
||||
content: String,
|
||||
timestamp: DateTime<Utc>,
|
||||
},
|
||||
|
||||
/// Response to `FetchChannelHistory`.
|
||||
ChannelHistory {
|
||||
channel_id: Uuid,
|
||||
messages: Vec<HistoricChannelMessage>,
|
||||
},
|
||||
|
||||
ChannelCreated {
|
||||
channel_id: Uuid,
|
||||
name: String,
|
||||
topic: Option<String>,
|
||||
},
|
||||
|
||||
ChannelDeleted {
|
||||
channel_id: Uuid,
|
||||
},
|
||||
|
||||
// ── DM key exchange relay ─────────────────────────────────────────────
|
||||
/// Server relays the initiator's ephemeral pubkey to the recipient.
|
||||
DmKeyExchangeRequest {
|
||||
initiator_id: Uuid,
|
||||
initiator_username: String,
|
||||
sender_ephemeral_pubkey: String,
|
||||
},
|
||||
|
||||
/// Server relays the recipient's ephemeral pubkey back to the initiator.
|
||||
DmKeyExchangeResponse {
|
||||
recipient_id: Uuid,
|
||||
recipient_username: String,
|
||||
recipient_ephemeral_pubkey: String,
|
||||
},
|
||||
|
||||
// ── DM messages ───────────────────────────────────────────────────────
|
||||
/// Relayed E2EE DM. Server cannot read `ciphertext`.
|
||||
DirectMessage {
|
||||
message_id: Uuid,
|
||||
sender_id: Uuid,
|
||||
sender_username: String,
|
||||
ciphertext: String,
|
||||
nonce: String,
|
||||
timestamp: DateTime<Utc>,
|
||||
},
|
||||
|
||||
DmHistory {
|
||||
peer_id: Uuid,
|
||||
messages: Vec<HistoricDm>,
|
||||
},
|
||||
|
||||
// ── Presence ──────────────────────────────────────────────────────────
|
||||
Pong,
|
||||
|
||||
UserOnline {
|
||||
user_id: Uuid,
|
||||
username: String,
|
||||
},
|
||||
|
||||
UserOffline {
|
||||
user_id: Uuid,
|
||||
username: String,
|
||||
},
|
||||
|
||||
// ── Role events ───────────────────────────────────────────────────────
|
||||
RoleCreated {
|
||||
role_id: Uuid,
|
||||
name: String,
|
||||
permissions: u64,
|
||||
},
|
||||
|
||||
RoleAssigned {
|
||||
user_id: Uuid,
|
||||
role_id: Uuid,
|
||||
},
|
||||
|
||||
RoleRevoked {
|
||||
user_id: Uuid,
|
||||
role_id: Uuid,
|
||||
},
|
||||
|
||||
// ── Key directory ─────────────────────────────────────────────────────
|
||||
IdentityKeyResponse {
|
||||
user_id: Uuid,
|
||||
/// Base64-encoded Ed25519 public key, or null if unknown.
|
||||
identity_pubkey: Option<String>,
|
||||
},
|
||||
|
||||
// ── Generic error ─────────────────────────────────────────────────────
|
||||
Error {
|
||||
code: u16,
|
||||
message: String,
|
||||
},
|
||||
}
|
||||
|
||||
/// A channel message returned in history responses.
|
||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||
pub struct HistoricChannelMessage {
|
||||
pub message_id: Uuid,
|
||||
pub author_id: Uuid,
|
||||
pub author_username: String,
|
||||
pub content: String,
|
||||
pub timestamp: DateTime<Utc>,
|
||||
}
|
||||
|
||||
/// A DM returned in history responses. Ciphertext is opaque to the server.
|
||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||
pub struct HistoricDm {
|
||||
pub message_id: Uuid,
|
||||
pub sender_id: Uuid,
|
||||
pub ciphertext: String,
|
||||
pub nonce: String,
|
||||
pub timestamp: DateTime<Utc>,
|
||||
}
|
||||
72
backchannel-common/src/types.rs
Normal file
72
backchannel-common/src/types.rs
Normal file
@@ -0,0 +1,72 @@
|
||||
use serde::{Deserialize, Serialize};
|
||||
use uuid::Uuid;
|
||||
|
||||
// Typed UUID wrappers to prevent accidental mixing of IDs.
|
||||
|
||||
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)]
|
||||
pub struct UserId(pub Uuid);
|
||||
|
||||
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)]
|
||||
pub struct ChannelId(pub Uuid);
|
||||
|
||||
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)]
|
||||
pub struct MessageId(pub Uuid);
|
||||
|
||||
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)]
|
||||
pub struct RoleId(pub Uuid);
|
||||
|
||||
impl UserId {
|
||||
pub fn new() -> Self {
|
||||
Self(Uuid::new_v4())
|
||||
}
|
||||
pub fn inner(self) -> Uuid {
|
||||
self.0
|
||||
}
|
||||
}
|
||||
|
||||
impl ChannelId {
|
||||
pub fn new() -> Self {
|
||||
Self(Uuid::new_v4())
|
||||
}
|
||||
pub fn inner(self) -> Uuid {
|
||||
self.0
|
||||
}
|
||||
}
|
||||
|
||||
impl MessageId {
|
||||
pub fn new() -> Self {
|
||||
Self(Uuid::new_v4())
|
||||
}
|
||||
pub fn inner(self) -> Uuid {
|
||||
self.0
|
||||
}
|
||||
}
|
||||
|
||||
impl RoleId {
|
||||
pub fn new() -> Self {
|
||||
Self(Uuid::new_v4())
|
||||
}
|
||||
pub fn inner(self) -> Uuid {
|
||||
self.0
|
||||
}
|
||||
}
|
||||
|
||||
/// Permission bit-flags for server roles.
|
||||
/// The `ADMINISTRATOR` flag bypasses all individual permission checks.
|
||||
pub struct PermissionFlags;
|
||||
|
||||
impl PermissionFlags {
|
||||
pub const READ_MESSAGES: u64 = 1 << 0;
|
||||
pub const SEND_MESSAGES: u64 = 1 << 1;
|
||||
pub const MANAGE_CHANNELS: u64 = 1 << 2;
|
||||
pub const MANAGE_ROLES: u64 = 1 << 3;
|
||||
pub const KICK_MEMBERS: u64 = 1 << 4;
|
||||
pub const BAN_MEMBERS: u64 = 1 << 5;
|
||||
/// Bypasses all permission checks.
|
||||
pub const ADMINISTRATOR: u64 = 1 << 63;
|
||||
}
|
||||
|
||||
/// Check whether `perms` satisfies a required flag, honoring ADMINISTRATOR.
|
||||
pub fn has_permission(perms: u64, required: u64) -> bool {
|
||||
(perms & PermissionFlags::ADMINISTRATOR) != 0 || (perms & required) == required
|
||||
}
|
||||
Reference in New Issue
Block a user