- **Member Management**: Upsert and list organization members via TSV data
- **Admin Controls**: Restrict commands to authorized administrators
- **E2E Encryption**: Full Matrix end-to-end encryption support
-- **Device Verification**: Verify devices for secure communication
- **SQLite Database**: Persistent storage of member data and events
- **Environment Configuration**: Easy configuration via environment variables
- `!list` - List all members
- `!setadmin <member_id>` - Set a member as admin
- `!removeadmin <member_id>` - Remove admin status from a member
- - `!verify <device_id>` - Verify a device for E2E encryption
### TSV Format for `!upsert`
- `members`: Stores member information (ID, email, Matrix user, admin status)
- `events`: Logs bot interactions and events
-- `device_verifications`: Tracks E2E encryption device verifications
## Security
- **Admin-only commands**: Only users marked as `is_admin = TRUE` in the database can use commands
- **E2E Encryption**: Full Matrix end-to-end encryption using `matrix-sdk-crypto`
-- **Device Verification**: Manual device verification via `/verify` command
- **Event Logging**: All bot interactions are logged for audit purposes
## Development
--- /dev/null
+DROP INDEX IF EXISTS idx_device_verifications_device_id;
+DROP INDEX IF EXISTS idx_device_verifications_verified;
+
+DROP TABLE IF EXISTS device_verifications;
!list - List all members
!setadmin <member_id> - Set a member as admin
!removeadmin <member_id> - Remove admin status from a member
-!verify <device_id> - Verify a device for E2E encryption
"#;
if let Err(e) = send_reply(&room, help_text).await {
error!("Failed to send help reply: {}", e);
"!removeadmin" => {
handle_setadmin_command(&room, args, &sender, db, false).await?;
}
- "!verify" => {
- handle_verify_command(&room, args, &sender, db).await?;
- }
_ => {
let reply = format!("Unknown command: {}. Type !help for available commands.", command);
if let Err(e) = send_reply(&room, &reply).await {
Ok(())
}
-async fn handle_verify_command(
- room: &Room,
- args: &[&str],
- sender: &UserId,
- db: &Database,
-) -> Result<()> {
- // Check if user is admin
- let matrix_user = sender.to_string();
- let is_admin = if let Some(member) = db.get_member_by_matrix_user(&matrix_user).await? {
- member.is_admin
- } else {
- false
- };
-
- if !is_admin {
- let reply = "Error: You must be an admin to use this command.";
- send_reply(room, reply).await?;
- return Ok(());
- }
-
- if args.is_empty() {
- let reply = "Error: Please provide a device_id. Usage: /verify <device_id>";
- send_reply(room, reply).await?;
- return Ok(());
- }
-
- let device_id = args[0];
-
- match db.verify_device(device_id).await {
- Ok(_) => {
- let reply = format!("Successfully verified device: {}", device_id);
- send_reply(room, &reply).await?;
- }
- Err(e) => {
- let reply = format!("Error: {}", e);
- send_reply(room, &reply).await?;
- }
- }
-
- Ok(())
-}
async fn handle_verification_request(client: Client, request: VerificationRequest) {
info!("Processing verification request from {}", request.other_user_id());
use sqlx::{sqlite::SqlitePoolOptions, Row, SqlitePool};
use tracing::info;
-use crate::models::{DeviceVerification, Member, MemberUpsert};
+use crate::models::{Member, MemberUpsert};
pub struct Database {
pool: SqlitePool,
Ok(())
}
-
- // Device verification operations
- pub async fn save_device_verification(
- &self,
- device_id: &str,
- public_key: &str,
- ) -> Result<()> {
- sqlx::query(
- r#"
- INSERT INTO device_verifications (device_id, public_key)
- VALUES (?, ?)
- "#,
- )
- .bind(device_id)
- .bind(public_key)
- .execute(&self.pool)
- .await
- .context("Failed to save device verification")?;
-
- Ok(())
- }
-
- pub async fn verify_device(&self, device_id: &str) -> Result<()> {
- sqlx::query(
- r#"
- UPDATE device_verifications
- SET verified = TRUE, verified_at = CURRENT_TIMESTAMP
- WHERE device_id = ?
- "#,
- )
- .bind(device_id)
- .execute(&self.pool)
- .await
- .context("Failed to verify device")?;
-
- // Log event
- self.log_event(
- "device_verified",
- None,
- None,
- Some(&format!("Verified device: {}", device_id)),
- )
- .await?;
-
- Ok(())
- }
-
- pub async fn get_device_verification(
- &self,
- device_id: &str,
- ) -> Result<Option<DeviceVerification>> {
- let row = sqlx::query(
- "SELECT * FROM device_verifications WHERE device_id = ?",
- )
- .bind(device_id)
- .fetch_optional(&self.pool)
- .await
- .context("Failed to get device verification")?;
-
- if let Some(row) = row {
- Ok(Some(DeviceVerification {
- id: row.get("id"),
- device_id: row.get("device_id"),
- public_key: row.get("public_key"),
- verified: row.get("verified"),
- created_at: row.get("created_at"),
- verified_at: row.get("verified_at"),
- }))
- } else {
- Ok(None)
- }
- }
-
- pub async fn is_device_verified(&self, device_id: &str) -> Result<bool> {
- let verification = self.get_device_verification(device_id).await?;
- Ok(verification.map(|v| v.verified).unwrap_or(false))
- }
}
pub created_at: String,
}
-#[derive(Debug, Clone, Serialize, Deserialize)]
-pub struct DeviceVerification {
- pub id: i64,
- pub device_id: String,
- pub public_key: String,
- pub verified: bool,
- pub created_at: String,
- pub verified_at: Option<String>,
-}
-
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct MemberUpsert {
pub member_id: String,