diff --git a/arma/server/addons/phone/README.md b/arma/server/addons/phone/README.md index 0dc3bce..b88555e 100644 --- a/arma/server/addons/phone/README.md +++ b/arma/server/addons/phone/README.md @@ -8,13 +8,12 @@ Server State Phone contacts, messages, and emails are owned by the server extension. SQF phone stores act as bridge objects for CBA events and UI sync only. -Persistent Redis keys: +Persistent SurrealDB tables: -- `phone:{uid}:contacts`: set of contact actor UIDs -- `phone:message:{messageId}`: hash containing message record fields -- `phone:{uid}:messages`: list of message IDs visible to the user -- `phone:{uid}:thread:{otherUid}`: list of message IDs for a conversation -- `phone:{uid}:message_read`: hash of message ID to per-user read state -- `phone:email:{emailId}`: hash containing email record fields -- `phone:{uid}:emails`: list of email IDs visible to the user -- `phone:{uid}:email_read`: hash of email ID to per-user read state +- `phone_user`: owner row for an initialized phone profile. +- `phone_contact`: per-owner contact rows keyed by owner UID and contact UID. +- `phone_message`: shared message records. +- `phone_message_index`: per-owner message visibility and read state. +- `phone_email`: shared email records. +- `phone_email_index`: per-owner email visibility and read state. +- `phone_sequence`: global sequence state for generated message and email IDs. diff --git a/arma/server/extension/src/lib.rs b/arma/server/extension/src/lib.rs index c818631..b7b7ce6 100644 --- a/arma/server/extension/src/lib.rs +++ b/arma/server/extension/src/lib.rs @@ -23,6 +23,7 @@ mod log; pub mod org; pub mod phone; pub mod redis; +pub mod schema; pub mod storage; pub mod store; pub mod surreal; diff --git a/arma/server/extension/src/schema/actor.surql b/arma/server/extension/src/schema/actor.surql new file mode 100644 index 0000000..e2f2a29 --- /dev/null +++ b/arma/server/extension/src/schema/actor.surql @@ -0,0 +1,15 @@ +DEFINE TABLE IF NOT EXISTS actor SCHEMALESS; +DEFINE FIELD IF NOT EXISTS uid ON actor TYPE string; +DEFINE FIELD IF NOT EXISTS name ON actor TYPE option; +DEFINE FIELD IF NOT EXISTS loadout ON actor TYPE any; +DEFINE FIELD IF NOT EXISTS position ON actor TYPE option; +DEFINE FIELD IF NOT EXISTS direction ON actor TYPE number; +DEFINE FIELD IF NOT EXISTS stance ON actor TYPE option; +DEFINE FIELD IF NOT EXISTS email ON actor TYPE string; +DEFINE FIELD IF NOT EXISTS phone_number ON actor TYPE string; +DEFINE FIELD IF NOT EXISTS state ON actor TYPE string; +DEFINE FIELD IF NOT EXISTS holster ON actor TYPE bool; +DEFINE FIELD IF NOT EXISTS rank ON actor TYPE option; +DEFINE FIELD IF NOT EXISTS organization ON actor TYPE string; +DEFINE FIELD OVERWRITE updated_at ON actor TYPE option; +DEFINE INDEX IF NOT EXISTS actor_uid ON actor COLUMNS uid UNIQUE; diff --git a/arma/server/extension/src/schema/bank.surql b/arma/server/extension/src/schema/bank.surql new file mode 100644 index 0000000..18812b4 --- /dev/null +++ b/arma/server/extension/src/schema/bank.surql @@ -0,0 +1,17 @@ +DEFINE TABLE IF NOT EXISTS bank SCHEMALESS; +DEFINE FIELD IF NOT EXISTS uid ON bank TYPE string; +DEFINE FIELD IF NOT EXISTS name ON bank TYPE string; +DEFINE FIELD IF NOT EXISTS bank ON bank TYPE number; +DEFINE FIELD IF NOT EXISTS cash ON bank TYPE number; +DEFINE FIELD IF NOT EXISTS earnings ON bank TYPE number; +DEFINE FIELD IF NOT EXISTS pin ON bank TYPE int; +DEFINE FIELD OVERWRITE updated_at ON bank TYPE option; +DEFINE INDEX IF NOT EXISTS bank_uid ON bank COLUMNS uid UNIQUE; + +DEFINE TABLE IF NOT EXISTS bank_transaction SCHEMAFULL; +DEFINE FIELD IF NOT EXISTS uid ON bank_transaction TYPE string; +DEFINE FIELD IF NOT EXISTS ordinal ON bank_transaction TYPE int; +DEFINE FIELD IF NOT EXISTS message ON bank_transaction TYPE string; +DEFINE FIELD OVERWRITE created_at ON bank_transaction TYPE option; +DEFINE INDEX IF NOT EXISTS bank_transaction_owner ON bank_transaction COLUMNS uid; +DEFINE INDEX IF NOT EXISTS bank_transaction_unique ON bank_transaction COLUMNS uid, ordinal UNIQUE; diff --git a/arma/server/extension/src/schema/garage.surql b/arma/server/extension/src/schema/garage.surql new file mode 100644 index 0000000..9a679e7 --- /dev/null +++ b/arma/server/extension/src/schema/garage.surql @@ -0,0 +1,29 @@ +DEFINE TABLE IF NOT EXISTS garage SCHEMALESS; +DEFINE FIELD IF NOT EXISTS uid ON garage TYPE string; +DEFINE FIELD OVERWRITE updated_at ON garage TYPE option; +DEFINE INDEX IF NOT EXISTS garage_uid ON garage COLUMNS uid UNIQUE; + +DEFINE TABLE IF NOT EXISTS garage_vehicle SCHEMAFULL; +DEFINE FIELD IF NOT EXISTS uid ON garage_vehicle TYPE string; +DEFINE FIELD IF NOT EXISTS plate ON garage_vehicle TYPE string; +DEFINE FIELD IF NOT EXISTS classname ON garage_vehicle TYPE string; +DEFINE FIELD IF NOT EXISTS fuel ON garage_vehicle TYPE number; +DEFINE FIELD IF NOT EXISTS damage ON garage_vehicle TYPE number; +DEFINE FIELD IF NOT EXISTS hit_points ON garage_vehicle TYPE object; +DEFINE FIELD OVERWRITE updated_at ON garage_vehicle TYPE option; +DEFINE INDEX IF NOT EXISTS garage_vehicle_owner ON garage_vehicle COLUMNS uid; +DEFINE INDEX IF NOT EXISTS garage_vehicle_unique ON garage_vehicle COLUMNS uid, plate UNIQUE; + +DEFINE TABLE IF NOT EXISTS owned_garage SCHEMALESS; +DEFINE FIELD IF NOT EXISTS uid ON owned_garage TYPE string; +DEFINE FIELD OVERWRITE updated_at ON owned_garage TYPE option; +DEFINE INDEX IF NOT EXISTS owned_garage_uid ON owned_garage COLUMNS uid UNIQUE; + +DEFINE TABLE IF NOT EXISTS garage_unlock SCHEMAFULL; +DEFINE FIELD IF NOT EXISTS uid ON garage_unlock TYPE string; +DEFINE FIELD IF NOT EXISTS category ON garage_unlock TYPE string; +DEFINE FIELD IF NOT EXISTS classname ON garage_unlock TYPE string; +DEFINE FIELD IF NOT EXISTS source ON garage_unlock TYPE option; +DEFINE FIELD OVERWRITE unlocked_at ON garage_unlock TYPE option; +DEFINE INDEX IF NOT EXISTS garage_unlock_owner ON garage_unlock COLUMNS uid; +DEFINE INDEX IF NOT EXISTS garage_unlock_unique ON garage_unlock COLUMNS uid, category, classname UNIQUE; diff --git a/arma/server/extension/src/schema/locker.surql b/arma/server/extension/src/schema/locker.surql new file mode 100644 index 0000000..7796c4b --- /dev/null +++ b/arma/server/extension/src/schema/locker.surql @@ -0,0 +1,27 @@ +DEFINE TABLE IF NOT EXISTS locker SCHEMALESS; +DEFINE FIELD IF NOT EXISTS uid ON locker TYPE string; +DEFINE FIELD OVERWRITE updated_at ON locker TYPE option; +DEFINE INDEX IF NOT EXISTS locker_uid ON locker COLUMNS uid UNIQUE; + +DEFINE TABLE IF NOT EXISTS locker_item SCHEMAFULL; +DEFINE FIELD IF NOT EXISTS uid ON locker_item TYPE string; +DEFINE FIELD IF NOT EXISTS category ON locker_item TYPE string; +DEFINE FIELD IF NOT EXISTS classname ON locker_item TYPE string; +DEFINE FIELD IF NOT EXISTS amount ON locker_item TYPE int; +DEFINE FIELD OVERWRITE updated_at ON locker_item TYPE option; +DEFINE INDEX IF NOT EXISTS locker_item_owner ON locker_item COLUMNS uid; +DEFINE INDEX IF NOT EXISTS locker_item_unique ON locker_item COLUMNS uid, classname UNIQUE; + +DEFINE TABLE IF NOT EXISTS owned_locker SCHEMALESS; +DEFINE FIELD IF NOT EXISTS uid ON owned_locker TYPE string; +DEFINE FIELD OVERWRITE updated_at ON owned_locker TYPE option; +DEFINE INDEX IF NOT EXISTS owned_locker_uid ON owned_locker COLUMNS uid UNIQUE; + +DEFINE TABLE IF NOT EXISTS locker_unlock SCHEMAFULL; +DEFINE FIELD IF NOT EXISTS uid ON locker_unlock TYPE string; +DEFINE FIELD IF NOT EXISTS category ON locker_unlock TYPE string; +DEFINE FIELD IF NOT EXISTS classname ON locker_unlock TYPE string; +DEFINE FIELD IF NOT EXISTS source ON locker_unlock TYPE option; +DEFINE FIELD OVERWRITE unlocked_at ON locker_unlock TYPE option; +DEFINE INDEX IF NOT EXISTS locker_unlock_owner ON locker_unlock COLUMNS uid; +DEFINE INDEX IF NOT EXISTS locker_unlock_unique ON locker_unlock COLUMNS uid, category, classname UNIQUE; diff --git a/arma/server/extension/src/schema/mod.rs b/arma/server/extension/src/schema/mod.rs new file mode 100644 index 0000000..d64b1ba --- /dev/null +++ b/arma/server/extension/src/schema/mod.rs @@ -0,0 +1,48 @@ +use crate::log; +use crate::surreal::SurrealDb; + +const SCHEMAS: &[(&str, &str)] = &[ + ("actor", include_str!("actor.surql")), + ("bank", include_str!("bank.surql")), + ("org", include_str!("org.surql")), + ("locker", include_str!("locker.surql")), + ("garage", include_str!("garage.surql")), + ("phone", include_str!("phone.surql")), +]; + +pub async fn apply_all(db: &SurrealDb) -> Result<(), String> { + for (name, schema) in SCHEMAS { + for statement in schema_statements(schema) { + db.query(statement) + .await + .map_err(|error| { + format!( + "SurrealDB {} schema bootstrap failed for statement '{}': {}", + name, statement, error + ) + })? + .check() + .map_err(|error| { + format!( + "SurrealDB {} schema bootstrap failed for statement '{}': {}", + name, statement, error + ) + })?; + } + + log::log( + "surreal", + "DEBUG", + &format!("Applied SurrealDB {} schema", name), + ); + } + + Ok(()) +} + +fn schema_statements(schema: &'static str) -> impl Iterator { + schema + .split(';') + .map(str::trim) + .filter(|statement| !statement.is_empty()) +} diff --git a/arma/server/extension/src/schema/org.surql b/arma/server/extension/src/schema/org.surql new file mode 100644 index 0000000..7a423ad --- /dev/null +++ b/arma/server/extension/src/schema/org.surql @@ -0,0 +1,51 @@ +DEFINE TABLE IF NOT EXISTS org SCHEMALESS; +DEFINE FIELD IF NOT EXISTS org_id ON org TYPE string; +DEFINE FIELD IF NOT EXISTS owner ON org TYPE string; +DEFINE FIELD IF NOT EXISTS name ON org TYPE string; +DEFINE FIELD IF NOT EXISTS funds ON org TYPE number; +DEFINE FIELD IF NOT EXISTS reputation ON org TYPE int; +DEFINE FIELD OVERWRITE updated_at ON org TYPE option; +DEFINE INDEX IF NOT EXISTS org_org_id ON org COLUMNS org_id UNIQUE; + +DEFINE TABLE IF NOT EXISTS org_member SCHEMAFULL; +DEFINE FIELD IF NOT EXISTS org_id ON org_member TYPE string; +DEFINE FIELD IF NOT EXISTS member_uid ON org_member TYPE string; +DEFINE FIELD OVERWRITE joined_at ON org_member TYPE option; +DEFINE INDEX IF NOT EXISTS org_member_org ON org_member COLUMNS org_id; +DEFINE INDEX IF NOT EXISTS org_member_unique ON org_member COLUMNS org_id, member_uid UNIQUE; + +DEFINE TABLE IF NOT EXISTS org_credit_line SCHEMAFULL; +DEFINE FIELD IF NOT EXISTS org_id ON org_credit_line TYPE string; +DEFINE FIELD IF NOT EXISTS uid ON org_credit_line TYPE string; +DEFINE FIELD IF NOT EXISTS name ON org_credit_line TYPE string; +DEFINE FIELD IF NOT EXISTS approved_amount ON org_credit_line TYPE number; +DEFINE FIELD IF NOT EXISTS available_amount ON org_credit_line TYPE number; +DEFINE FIELD IF NOT EXISTS outstanding_principal ON org_credit_line TYPE number; +DEFINE FIELD IF NOT EXISTS interest_rate ON org_credit_line TYPE number; +DEFINE FIELD IF NOT EXISTS amount_due ON org_credit_line TYPE number; +DEFINE FIELD IF NOT EXISTS amount ON org_credit_line TYPE number; +DEFINE FIELD OVERWRITE updated_at ON org_credit_line TYPE option; +DEFINE INDEX IF NOT EXISTS org_credit_line_org ON org_credit_line COLUMNS org_id; +DEFINE INDEX IF NOT EXISTS org_credit_line_unique ON org_credit_line COLUMNS org_id, uid UNIQUE; + +DEFINE TABLE IF NOT EXISTS org_asset SCHEMAFULL; +DEFINE FIELD IF NOT EXISTS org_id ON org_asset TYPE string; +DEFINE FIELD IF NOT EXISTS category ON org_asset TYPE string; +DEFINE FIELD IF NOT EXISTS classname ON org_asset TYPE string; +DEFINE FIELD IF NOT EXISTS asset_type ON org_asset TYPE string; +DEFINE FIELD IF NOT EXISTS quantity ON org_asset TYPE int; +DEFINE FIELD OVERWRITE updated_at ON org_asset TYPE option; +DEFINE INDEX IF NOT EXISTS org_asset_org ON org_asset COLUMNS org_id; +DEFINE INDEX IF NOT EXISTS org_asset_unique ON org_asset COLUMNS org_id, category, classname UNIQUE; + +DEFINE TABLE IF NOT EXISTS org_fleet_vehicle SCHEMAFULL; +DEFINE FIELD IF NOT EXISTS org_id ON org_fleet_vehicle TYPE string; +DEFINE FIELD IF NOT EXISTS fleet_key ON org_fleet_vehicle TYPE string; +DEFINE FIELD IF NOT EXISTS classname ON org_fleet_vehicle TYPE string; +DEFINE FIELD IF NOT EXISTS name ON org_fleet_vehicle TYPE string; +DEFINE FIELD IF NOT EXISTS fleet_type ON org_fleet_vehicle TYPE string; +DEFINE FIELD IF NOT EXISTS status ON org_fleet_vehicle TYPE string; +DEFINE FIELD IF NOT EXISTS damage ON org_fleet_vehicle TYPE string; +DEFINE FIELD OVERWRITE updated_at ON org_fleet_vehicle TYPE option; +DEFINE INDEX IF NOT EXISTS org_fleet_vehicle_org ON org_fleet_vehicle COLUMNS org_id; +DEFINE INDEX IF NOT EXISTS org_fleet_vehicle_unique ON org_fleet_vehicle COLUMNS org_id, fleet_key UNIQUE; diff --git a/arma/server/extension/src/schema/phone.surql b/arma/server/extension/src/schema/phone.surql new file mode 100644 index 0000000..16cdd7f --- /dev/null +++ b/arma/server/extension/src/schema/phone.surql @@ -0,0 +1,53 @@ +DEFINE TABLE IF NOT EXISTS phone_user SCHEMALESS; +DEFINE FIELD IF NOT EXISTS uid ON phone_user TYPE string; +DEFINE FIELD OVERWRITE updated_at ON phone_user TYPE option; +DEFINE INDEX IF NOT EXISTS phone_user_uid ON phone_user COLUMNS uid UNIQUE; + +DEFINE TABLE IF NOT EXISTS phone_contact SCHEMAFULL; +DEFINE FIELD IF NOT EXISTS uid ON phone_contact TYPE string; +DEFINE FIELD IF NOT EXISTS contact_uid ON phone_contact TYPE string; +DEFINE FIELD OVERWRITE created_at ON phone_contact TYPE option; +DEFINE INDEX IF NOT EXISTS phone_contact_owner ON phone_contact COLUMNS uid; +DEFINE INDEX IF NOT EXISTS phone_contact_unique ON phone_contact COLUMNS uid, contact_uid UNIQUE; + +DEFINE TABLE IF NOT EXISTS phone_message SCHEMAFULL; +DEFINE FIELD IF NOT EXISTS message_id ON phone_message TYPE string; +DEFINE FIELD IF NOT EXISTS from_uid ON phone_message TYPE string; +DEFINE FIELD IF NOT EXISTS to_uid ON phone_message TYPE string; +DEFINE FIELD IF NOT EXISTS message ON phone_message TYPE string; +DEFINE FIELD IF NOT EXISTS timestamp ON phone_message TYPE number; +DEFINE FIELD OVERWRITE created_at ON phone_message TYPE option; +DEFINE INDEX IF NOT EXISTS phone_message_message_id ON phone_message COLUMNS message_id UNIQUE; + +DEFINE TABLE IF NOT EXISTS phone_message_index SCHEMAFULL; +DEFINE FIELD IF NOT EXISTS uid ON phone_message_index TYPE string; +DEFINE FIELD IF NOT EXISTS message_id ON phone_message_index TYPE string; +DEFINE FIELD IF NOT EXISTS is_read ON phone_message_index TYPE bool; +DEFINE FIELD OVERWRITE updated_at ON phone_message_index TYPE option; +DEFINE INDEX IF NOT EXISTS phone_message_index_owner ON phone_message_index COLUMNS uid; +DEFINE INDEX IF NOT EXISTS phone_message_index_message ON phone_message_index COLUMNS message_id; +DEFINE INDEX IF NOT EXISTS phone_message_index_unique ON phone_message_index COLUMNS uid, message_id UNIQUE; + +DEFINE TABLE IF NOT EXISTS phone_email SCHEMAFULL; +DEFINE FIELD IF NOT EXISTS email_id ON phone_email TYPE string; +DEFINE FIELD IF NOT EXISTS from_uid ON phone_email TYPE string; +DEFINE FIELD IF NOT EXISTS to_uid ON phone_email TYPE string; +DEFINE FIELD IF NOT EXISTS subject ON phone_email TYPE string; +DEFINE FIELD IF NOT EXISTS body ON phone_email TYPE string; +DEFINE FIELD IF NOT EXISTS timestamp ON phone_email TYPE number; +DEFINE FIELD OVERWRITE created_at ON phone_email TYPE option; +DEFINE INDEX IF NOT EXISTS phone_email_email_id ON phone_email COLUMNS email_id UNIQUE; + +DEFINE TABLE IF NOT EXISTS phone_email_index SCHEMAFULL; +DEFINE FIELD IF NOT EXISTS uid ON phone_email_index TYPE string; +DEFINE FIELD IF NOT EXISTS email_id ON phone_email_index TYPE string; +DEFINE FIELD IF NOT EXISTS is_read ON phone_email_index TYPE bool; +DEFINE FIELD OVERWRITE updated_at ON phone_email_index TYPE option; +DEFINE INDEX IF NOT EXISTS phone_email_index_owner ON phone_email_index COLUMNS uid; +DEFINE INDEX IF NOT EXISTS phone_email_index_email ON phone_email_index COLUMNS email_id; +DEFINE INDEX IF NOT EXISTS phone_email_index_unique ON phone_email_index COLUMNS uid, email_id UNIQUE; + +DEFINE TABLE IF NOT EXISTS phone_sequence SCHEMALESS; +DEFINE FIELD IF NOT EXISTS sequence_id ON phone_sequence TYPE string; +DEFINE FIELD IF NOT EXISTS value ON phone_sequence TYPE int; +DEFINE INDEX IF NOT EXISTS phone_sequence_id ON phone_sequence COLUMNS sequence_id UNIQUE; diff --git a/arma/server/extension/src/storage.rs b/arma/server/extension/src/storage.rs index 4ff3e24..ca98bd2 100644 --- a/arma/server/extension/src/storage.rs +++ b/arma/server/extension/src/storage.rs @@ -1,8 +1,29 @@ //! Durable repository selection for the extension. +mod actor; +mod bank; +mod common; +mod garage; +mod locker; +mod org; +mod phone; + +pub use actor::{ActorStorageRepository, SurrealActorRepository}; +pub use bank::{BankStorageRepository, SurrealBankRepository}; +pub use garage::{ + GarageStorageRepository, SurrealGarageRepository, SurrealVGarageRepository, + VGarageStorageRepository, +}; +pub use locker::{ + LockerStorageRepository, SurrealLockerRepository, SurrealVLockerRepository, + VLockerStorageRepository, +}; +pub use org::{OrgStorageRepository, SurrealOrgRepository}; +pub use phone::{PhoneStorageRepository, SurrealPhoneRepository}; + use forge_models::{ - Actor, Bank, CreditLineSummary, Garage, Locker, MemberSummary, Org, OrgAssetEntry, - OrgFleetEntry, PhoneEmail, PhoneMessage, VGarage, VLocker, + Actor, Bank, CreditLineSummary, Garage, HitPoints, Item, Locker, MemberSummary, Org, + OrgAssetEntry, OrgFleetEntry, PhoneEmail, PhoneMessage, VGarage, VLocker, Vehicle, }; use forge_repositories::{ ActorRepository, BankRepository, GarageRepository, LockerRepository, OrgRepository, @@ -18,1321 +39,3 @@ use crate::RUNTIME; use crate::adapters::ExtensionRedisClient; use crate::redis::config::{StorageBackend, load}; use crate::surreal; - -fn surreal_select(table: &'static str, id: &str, label: &str) -> Result, String> -where - T: DeserializeOwned, -{ - let id = id.to_string(); - RUNTIME.block_on(async move { - surreal::client() - .await? - .select((table, id.as_str())) - .await - .map_err(|error| format!("SurrealDB {} select failed: {}", label, error)) - }) -} - -fn surreal_select_all(table: &'static str, label: &str) -> Result, String> -where - T: DeserializeOwned, -{ - RUNTIME.block_on(async move { - surreal::client() - .await? - .select(table) - .await - .map_err(|error| format!("SurrealDB {} select all failed: {}", label, error)) - }) -} - -fn surreal_upsert(table: &'static str, id: &str, label: &str, record: &T) -> Result<(), String> -where - T: Serialize + DeserializeOwned, -{ - let id = id.to_string(); - let record = serde_json::to_value(record) - .map_err(|error| format!("SurrealDB {} serialize failed: {}", label, error))?; - RUNTIME.block_on(async move { - let _: Option = surreal::client() - .await? - .upsert((table, id.as_str())) - .content(record) - .await - .map_err(|error| format!("SurrealDB {} upsert failed: {}", label, error))?; - Ok(()) - }) -} - -fn surreal_delete(table: &'static str, id: &str, label: &str) -> Result<(), String> -where - T: DeserializeOwned, -{ - let id = id.to_string(); - RUNTIME.block_on(async move { - let _: Option = surreal::client() - .await? - .delete((table, id.as_str())) - .await - .map_err(|error| format!("SurrealDB {} delete failed: {}", label, error))?; - Ok(()) - }) -} - -pub enum ActorStorageRepository { - Redis(RedisActorRepository), - Surreal(SurrealActorRepository), -} - -impl ActorStorageRepository { - pub fn configured() -> Self { - match load().storage.backend { - StorageBackend::Surreal => Self::Surreal(SurrealActorRepository), - StorageBackend::Redis => { - Self::Redis(RedisActorRepository::new(ExtensionRedisClient::new())) - } - } - } -} - -impl ActorRepository for ActorStorageRepository { - fn create(&self, actor: &Actor) -> Result<(), String> { - match self { - Self::Redis(repository) => repository.create(actor), - Self::Surreal(repository) => repository.create(actor), - } - } - - fn get_by_id(&self, id: &str) -> Result, String> { - match self { - Self::Redis(repository) => repository.get_by_id(id), - Self::Surreal(repository) => repository.get_by_id(id), - } - } - - fn update(&self, actor: &Actor) -> Result<(), String> { - match self { - Self::Redis(repository) => repository.update(actor), - Self::Surreal(repository) => repository.update(actor), - } - } - - fn delete(&self, id: &str) -> Result<(), String> { - match self { - Self::Redis(repository) => repository.delete(id), - Self::Surreal(repository) => repository.delete(id), - } - } - - fn exists(&self, id: &str) -> Result { - match self { - Self::Redis(repository) => repository.exists(id), - Self::Surreal(repository) => repository.exists(id), - } - } -} - -pub enum BankStorageRepository { - Redis(RedisBankRepository), - Surreal(SurrealBankRepository), -} - -impl BankStorageRepository { - pub fn configured() -> Self { - match load().storage.backend { - StorageBackend::Surreal => Self::Surreal(SurrealBankRepository), - StorageBackend::Redis => { - Self::Redis(RedisBankRepository::new(ExtensionRedisClient::new())) - } - } - } -} - -impl BankRepository for BankStorageRepository { - fn create(&self, bank: &Bank) -> Result<(), String> { - match self { - Self::Redis(repository) => repository.create(bank), - Self::Surreal(repository) => repository.create(bank), - } - } - - fn get_by_id(&self, id: &str) -> Result, String> { - match self { - Self::Redis(repository) => repository.get_by_id(id), - Self::Surreal(repository) => repository.get_by_id(id), - } - } - - fn update(&self, bank: &Bank) -> Result<(), String> { - match self { - Self::Redis(repository) => repository.update(bank), - Self::Surreal(repository) => repository.update(bank), - } - } - - fn delete(&self, id: &str) -> Result<(), String> { - match self { - Self::Redis(repository) => repository.delete(id), - Self::Surreal(repository) => repository.delete(id), - } - } - - fn exists(&self, id: &str) -> Result { - match self { - Self::Redis(repository) => repository.exists(id), - Self::Surreal(repository) => repository.exists(id), - } - } -} - -pub struct SurrealActorRepository; - -impl ActorRepository for SurrealActorRepository { - fn create(&self, actor: &Actor) -> Result<(), String> { - self.update(actor) - } - - fn get_by_id(&self, id: &str) -> Result, String> { - surreal_select("actor", id, "actor") - } - - fn update(&self, actor: &Actor) -> Result<(), String> { - surreal_upsert("actor", actor.uid.as_str(), "actor", actor) - } - - fn delete(&self, id: &str) -> Result<(), String> { - surreal_delete::("actor", id, "actor") - } - - fn exists(&self, id: &str) -> Result { - self.get_by_id(id).map(|actor| actor.is_some()) - } -} - -pub struct SurrealBankRepository; - -impl BankRepository for SurrealBankRepository { - fn create(&self, bank: &Bank) -> Result<(), String> { - self.update(bank) - } - - fn get_by_id(&self, id: &str) -> Result, String> { - surreal_select("bank", id, "bank") - } - - fn update(&self, bank: &Bank) -> Result<(), String> { - surreal_upsert("bank", bank.uid.as_str(), "bank", bank) - } - - fn delete(&self, id: &str) -> Result<(), String> { - surreal_delete::("bank", id, "bank") - } - - fn exists(&self, id: &str) -> Result { - self.get_by_id(id).map(|bank| bank.is_some()) - } -} - -pub enum PhoneStorageRepository { - Redis(RedisPhoneRepository), - Surreal(SurrealPhoneRepository), -} - -impl PhoneStorageRepository { - pub fn configured() -> Self { - match load().storage.backend { - StorageBackend::Surreal => Self::Surreal(SurrealPhoneRepository), - StorageBackend::Redis => { - Self::Redis(RedisPhoneRepository::new(ExtensionRedisClient::new())) - } - } - } -} - -impl PhoneRepository for PhoneStorageRepository { - fn init(&self, uid: &str) -> Result<(), String> { - match self { - Self::Redis(repository) => repository.init(uid), - Self::Surreal(repository) => repository.init(uid), - } - } - - fn add_contact(&self, uid: &str, contact_uid: &str) -> Result { - match self { - Self::Redis(repository) => repository.add_contact(uid, contact_uid), - Self::Surreal(repository) => repository.add_contact(uid, contact_uid), - } - } - - fn remove_contact(&self, uid: &str, contact_uid: &str) -> Result { - match self { - Self::Redis(repository) => repository.remove_contact(uid, contact_uid), - Self::Surreal(repository) => repository.remove_contact(uid, contact_uid), - } - } - - fn list_contacts(&self, uid: &str) -> Result, String> { - match self { - Self::Redis(repository) => repository.list_contacts(uid), - Self::Surreal(repository) => repository.list_contacts(uid), - } - } - - fn remove_phone(&self, uid: &str) -> Result<(), String> { - match self { - Self::Redis(repository) => repository.remove_phone(uid), - Self::Surreal(repository) => repository.remove_phone(uid), - } - } - - fn append_message(&self, uid: &str, message: PhoneMessage) -> Result<(), String> { - match self { - Self::Redis(repository) => repository.append_message(uid, message), - Self::Surreal(repository) => repository.append_message(uid, message), - } - } - - fn list_messages(&self, uid: &str) -> Result, String> { - match self { - Self::Redis(repository) => repository.list_messages(uid), - Self::Surreal(repository) => repository.list_messages(uid), - } - } - - fn mark_message_read(&self, uid: &str, message_id: &str) -> Result { - match self { - Self::Redis(repository) => repository.mark_message_read(uid, message_id), - Self::Surreal(repository) => repository.mark_message_read(uid, message_id), - } - } - - fn delete_message(&self, uid: &str, message_id: &str) -> Result { - match self { - Self::Redis(repository) => repository.delete_message(uid, message_id), - Self::Surreal(repository) => repository.delete_message(uid, message_id), - } - } - - fn append_email(&self, uid: &str, email: PhoneEmail) -> Result<(), String> { - match self { - Self::Redis(repository) => repository.append_email(uid, email), - Self::Surreal(repository) => repository.append_email(uid, email), - } - } - - fn list_emails(&self, uid: &str) -> Result, String> { - match self { - Self::Redis(repository) => repository.list_emails(uid), - Self::Surreal(repository) => repository.list_emails(uid), - } - } - - fn mark_email_read(&self, uid: &str, email_id: &str) -> Result { - match self { - Self::Redis(repository) => repository.mark_email_read(uid, email_id), - Self::Surreal(repository) => repository.mark_email_read(uid, email_id), - } - } - - fn delete_email(&self, uid: &str, email_id: &str) -> Result { - match self { - Self::Redis(repository) => repository.delete_email(uid, email_id), - Self::Surreal(repository) => repository.delete_email(uid, email_id), - } - } - - fn next_sequence(&self) -> Result { - match self { - Self::Redis(repository) => repository.next_sequence(), - Self::Surreal(repository) => repository.next_sequence(), - } - } -} - -#[derive(Debug, Default, Serialize, Deserialize)] -struct PhoneUserRecord { - #[serde(default)] - contacts: Vec, - #[serde(default)] - message_ids: Vec, - #[serde(default)] - email_ids: Vec, - #[serde(default)] - message_read: HashMap, - #[serde(default)] - email_read: HashMap, -} - -#[derive(Debug, Default, Serialize, Deserialize)] -struct PhoneMessageRecord { - #[serde(default)] - message_id: String, - #[serde(default)] - from: String, - #[serde(default)] - to: String, - #[serde(default)] - message: String, - #[serde(default)] - timestamp: f64, -} - -impl PhoneMessageRecord { - fn into_message(self, fallback_id: &str, read: bool) -> PhoneMessage { - let id = if self.message_id.trim().is_empty() { - fallback_id.to_string() - } else { - self.message_id - }; - - PhoneMessage { - id, - from: self.from, - to: self.to, - message: self.message, - timestamp: self.timestamp, - read, - } - } -} - -impl From<&PhoneMessage> for PhoneMessageRecord { - fn from(message: &PhoneMessage) -> Self { - Self { - message_id: message.id.clone(), - from: message.from.clone(), - to: message.to.clone(), - message: message.message.clone(), - timestamp: message.timestamp, - } - } -} - -#[derive(Debug, Default, Serialize, Deserialize)] -struct PhoneEmailRecord { - #[serde(default)] - email_id: String, - #[serde(default)] - from: String, - #[serde(default)] - to: String, - #[serde(default)] - subject: String, - #[serde(default)] - body: String, - #[serde(default)] - timestamp: f64, -} - -impl PhoneEmailRecord { - fn into_email(self, fallback_id: &str, read: bool) -> PhoneEmail { - let id = if self.email_id.trim().is_empty() { - fallback_id.to_string() - } else { - self.email_id - }; - - PhoneEmail { - id, - from: self.from, - to: self.to, - subject: self.subject, - body: self.body, - timestamp: self.timestamp, - read, - } - } -} - -impl From<&PhoneEmail> for PhoneEmailRecord { - fn from(email: &PhoneEmail) -> Self { - Self { - email_id: email.id.clone(), - from: email.from.clone(), - to: email.to.clone(), - subject: email.subject.clone(), - body: email.body.clone(), - timestamp: email.timestamp, - } - } -} - -#[derive(Debug, Default, Serialize, Deserialize)] -struct PhoneSequenceRecord { - #[serde(default)] - value: u64, -} - -pub struct SurrealPhoneRepository; - -impl SurrealPhoneRepository { - fn load_user(&self, uid: &str) -> Result { - Ok(surreal_select::("phone_user", uid, "phone user")?.unwrap_or_default()) - } - - fn save_user(&self, uid: &str, record: &PhoneUserRecord) -> Result<(), String> { - surreal_upsert("phone_user", uid, "phone user", record) - } - - fn message_is_referenced(&self, message_id: &str) -> Result { - Ok( - surreal_select_all::("phone_user", "phone users")? - .into_iter() - .any(|record| record.message_ids.iter().any(|id| id == message_id)), - ) - } - - fn email_is_referenced(&self, email_id: &str) -> Result { - Ok( - surreal_select_all::("phone_user", "phone users")? - .into_iter() - .any(|record| record.email_ids.iter().any(|id| id == email_id)), - ) - } - - fn cleanup_orphaned_records(&self) -> Result<(), String> { - let users = surreal_select_all::("phone_user", "phone users")?; - let referenced_messages = users - .iter() - .flat_map(|record| record.message_ids.iter().cloned()) - .collect::>(); - let referenced_emails = users - .iter() - .flat_map(|record| record.email_ids.iter().cloned()) - .collect::>(); - - for record in surreal_select_all::("phone_message", "phone messages")? { - let message_id = record.message_id.trim(); - if !message_id.is_empty() && !referenced_messages.contains(message_id) { - surreal_delete::("phone_message", message_id, "phone message")?; - } - } - - for record in surreal_select_all::("phone_email", "phone emails")? { - let email_id = record.email_id.trim(); - if !email_id.is_empty() && !referenced_emails.contains(email_id) { - surreal_delete::("phone_email", email_id, "phone email")?; - } - } - - Ok(()) - } -} - -impl PhoneRepository for SurrealPhoneRepository { - fn init(&self, uid: &str) -> Result<(), String> { - if surreal_select::("phone_user", uid, "phone user")?.is_none() { - self.save_user(uid, &PhoneUserRecord::default())?; - } - self.cleanup_orphaned_records()?; - Ok(()) - } - - fn add_contact(&self, uid: &str, contact_uid: &str) -> Result { - let mut record = self.load_user(uid)?; - if !record.contacts.iter().any(|contact| contact == contact_uid) { - record.contacts.push(contact_uid.to_string()); - } - self.save_user(uid, &record)?; - Ok(true) - } - - fn remove_contact(&self, uid: &str, contact_uid: &str) -> Result { - let mut record = self.load_user(uid)?; - let original_len = record.contacts.len(); - record.contacts.retain(|contact| contact != contact_uid); - self.save_user(uid, &record)?; - Ok(record.contacts.len() != original_len) - } - - fn list_contacts(&self, uid: &str) -> Result, String> { - let mut contacts = self.load_user(uid)?.contacts; - contacts.sort(); - contacts.dedup(); - Ok(contacts) - } - - fn remove_phone(&self, uid: &str) -> Result<(), String> { - // Message/email records can be shared by other user indexes. Removing a - // phone only drops this user's index and read state, matching Redis behavior. - surreal_delete::("phone_user", uid, "phone user") - } - - fn append_message(&self, uid: &str, message: PhoneMessage) -> Result<(), String> { - let mut user = self.load_user(uid)?; - if !user.message_ids.iter().any(|id| id == &message.id) { - user.message_ids.push(message.id.clone()); - } - user.message_read - .insert(message.id.clone(), message.from == uid); - - let record = PhoneMessageRecord::from(&message); - surreal_upsert("phone_message", &message.id, "phone message", &record)?; - self.save_user(uid, &user) - } - - fn list_messages(&self, uid: &str) -> Result, String> { - let user = self.load_user(uid)?; - let mut messages = Vec::with_capacity(user.message_ids.len()); - - for message_id in user.message_ids { - if message_id.trim().is_empty() { - continue; - } - - let read = user.message_read.get(&message_id).copied().unwrap_or(false); - if let Some(record) = - surreal_select::("phone_message", &message_id, "phone message")? - { - messages.push(record.into_message(&message_id, read)); - } - } - - messages.sort_by(|left, right| { - left.timestamp - .partial_cmp(&right.timestamp) - .unwrap_or(std::cmp::Ordering::Equal) - }); - Ok(messages) - } - - fn mark_message_read(&self, uid: &str, message_id: &str) -> Result { - let mut user = self.load_user(uid)?; - if !user.message_ids.iter().any(|id| id == message_id) { - return Ok(false); - } - user.message_read.insert(message_id.to_string(), true); - self.save_user(uid, &user)?; - Ok(true) - } - - fn delete_message(&self, uid: &str, message_id: &str) -> Result { - let mut user = self.load_user(uid)?; - let original_len = user.message_ids.len(); - user.message_ids.retain(|id| id != message_id); - if user.message_ids.len() == original_len { - return Ok(false); - } - - user.message_read.remove(message_id); - self.save_user(uid, &user)?; - if !self.message_is_referenced(message_id)? { - surreal_delete::("phone_message", message_id, "phone message")?; - } - Ok(true) - } - - fn append_email(&self, uid: &str, email: PhoneEmail) -> Result<(), String> { - let mut user = self.load_user(uid)?; - if !user.email_ids.iter().any(|id| id == &email.id) { - user.email_ids.push(email.id.clone()); - } - user.email_read.insert(email.id.clone(), false); - - let record = PhoneEmailRecord::from(&email); - surreal_upsert("phone_email", &email.id, "phone email", &record)?; - self.save_user(uid, &user) - } - - fn list_emails(&self, uid: &str) -> Result, String> { - let user = self.load_user(uid)?; - let mut emails = Vec::with_capacity(user.email_ids.len()); - - for email_id in user.email_ids { - if email_id.trim().is_empty() { - continue; - } - - let read = user.email_read.get(&email_id).copied().unwrap_or(false); - if let Some(record) = - surreal_select::("phone_email", &email_id, "phone email")? - { - emails.push(record.into_email(&email_id, read)); - } - } - - emails.sort_by(|left, right| { - right - .timestamp - .partial_cmp(&left.timestamp) - .unwrap_or(std::cmp::Ordering::Equal) - }); - Ok(emails) - } - - fn mark_email_read(&self, uid: &str, email_id: &str) -> Result { - let mut user = self.load_user(uid)?; - if !user.email_ids.iter().any(|id| id == email_id) { - return Ok(false); - } - user.email_read.insert(email_id.to_string(), true); - self.save_user(uid, &user)?; - Ok(true) - } - - fn delete_email(&self, uid: &str, email_id: &str) -> Result { - let mut user = self.load_user(uid)?; - let original_len = user.email_ids.len(); - user.email_ids.retain(|id| id != email_id); - if user.email_ids.len() == original_len { - return Ok(false); - } - - user.email_read.remove(email_id); - self.save_user(uid, &user)?; - if !self.email_is_referenced(email_id)? { - surreal_delete::("phone_email", email_id, "phone email")?; - } - Ok(true) - } - - fn next_sequence(&self) -> Result { - let mut record = - surreal_select::("phone_sequence", "global", "phone sequence")? - .unwrap_or_default(); - record.value = record - .value - .checked_add(1) - .ok_or_else(|| "Phone sequence overflowed.".to_string())?; - surreal_upsert("phone_sequence", "global", "phone sequence", &record)?; - Ok(record.value) - } -} - -pub enum GarageStorageRepository { - Redis(RedisGarageRepository), - Surreal(SurrealGarageRepository), -} - -impl GarageStorageRepository { - pub fn configured() -> Self { - match load().storage.backend { - StorageBackend::Surreal => Self::Surreal(SurrealGarageRepository), - StorageBackend::Redis => { - Self::Redis(RedisGarageRepository::new(ExtensionRedisClient::new())) - } - } - } -} - -impl GarageRepository for GarageStorageRepository { - fn create(&self, uid: &str, garage: &Garage) -> Result<(), String> { - match self { - Self::Redis(repository) => repository.create(uid, garage), - Self::Surreal(repository) => repository.create(uid, garage), - } - } - - fn update(&self, uid: &str, garage: &Garage) -> Result<(), String> { - match self { - Self::Redis(repository) => repository.update(uid, garage), - Self::Surreal(repository) => repository.update(uid, garage), - } - } - - fn get(&self, uid: &str) -> Result, String> { - match self { - Self::Redis(repository) => repository.get(uid), - Self::Surreal(repository) => repository.get(uid), - } - } - - fn delete(&self, uid: &str) -> Result<(), String> { - match self { - Self::Redis(repository) => repository.delete(uid), - Self::Surreal(repository) => repository.delete(uid), - } - } - - fn exists(&self, uid: &str) -> Result { - match self { - Self::Redis(repository) => repository.exists(uid), - Self::Surreal(repository) => repository.exists(uid), - } - } -} - -pub struct SurrealGarageRepository; - -impl GarageRepository for SurrealGarageRepository { - fn create(&self, uid: &str, garage: &Garage) -> Result<(), String> { - self.update(uid, garage) - } - - fn update(&self, uid: &str, garage: &Garage) -> Result<(), String> { - surreal_upsert("garage", uid, "garage", garage) - } - - fn get(&self, uid: &str) -> Result, String> { - surreal_select("garage", uid, "garage") - } - - fn delete(&self, uid: &str) -> Result<(), String> { - surreal_delete::("garage", uid, "garage") - } - - fn exists(&self, uid: &str) -> Result { - self.get(uid).map(|garage| garage.is_some()) - } -} - -pub enum LockerStorageRepository { - Redis(RedisLockerRepository), - Surreal(SurrealLockerRepository), -} - -impl LockerStorageRepository { - pub fn configured() -> Self { - match load().storage.backend { - StorageBackend::Surreal => Self::Surreal(SurrealLockerRepository), - StorageBackend::Redis => { - Self::Redis(RedisLockerRepository::new(ExtensionRedisClient::new())) - } - } - } -} - -impl LockerRepository for LockerStorageRepository { - fn create(&self, uid: &str, locker: &Locker) -> Result<(), String> { - match self { - Self::Redis(repository) => repository.create(uid, locker), - Self::Surreal(repository) => repository.create(uid, locker), - } - } - - fn update(&self, uid: &str, locker: &Locker) -> Result<(), String> { - match self { - Self::Redis(repository) => repository.update(uid, locker), - Self::Surreal(repository) => repository.update(uid, locker), - } - } - - fn get(&self, uid: &str) -> Result, String> { - match self { - Self::Redis(repository) => repository.get(uid), - Self::Surreal(repository) => repository.get(uid), - } - } - - fn delete(&self, uid: &str) -> Result<(), String> { - match self { - Self::Redis(repository) => repository.delete(uid), - Self::Surreal(repository) => repository.delete(uid), - } - } - - fn exists(&self, uid: &str) -> Result { - match self { - Self::Redis(repository) => repository.exists(uid), - Self::Surreal(repository) => repository.exists(uid), - } - } -} - -pub struct SurrealLockerRepository; - -impl LockerRepository for SurrealLockerRepository { - fn create(&self, uid: &str, locker: &Locker) -> Result<(), String> { - self.update(uid, locker) - } - - fn update(&self, uid: &str, locker: &Locker) -> Result<(), String> { - surreal_upsert("locker", uid, "locker", locker) - } - - fn get(&self, uid: &str) -> Result, String> { - surreal_select("locker", uid, "locker") - } - - fn delete(&self, uid: &str) -> Result<(), String> { - surreal_delete::("locker", uid, "locker") - } - - fn exists(&self, uid: &str) -> Result { - self.get(uid).map(|locker| locker.is_some()) - } -} - -pub enum VGarageStorageRepository { - Redis(RedisVGarageRepository), - Surreal(SurrealVGarageRepository), -} - -impl VGarageStorageRepository { - pub fn configured() -> Self { - match load().storage.backend { - StorageBackend::Surreal => Self::Surreal(SurrealVGarageRepository), - StorageBackend::Redis => { - Self::Redis(RedisVGarageRepository::new(ExtensionRedisClient::new())) - } - } - } -} - -impl VGarageRepository for VGarageStorageRepository { - fn create(&self, uid: &str, garage: &VGarage) -> Result<(), String> { - match self { - Self::Redis(repository) => repository.create(uid, garage), - Self::Surreal(repository) => repository.create(uid, garage), - } - } - - fn update(&self, uid: &str, garage: &VGarage) -> Result<(), String> { - match self { - Self::Redis(repository) => repository.update(uid, garage), - Self::Surreal(repository) => repository.update(uid, garage), - } - } - - fn fetch(&self, uid: &str) -> Result, String> { - match self { - Self::Redis(repository) => repository.fetch(uid), - Self::Surreal(repository) => repository.fetch(uid), - } - } - - fn get(&self, uid: &str, field: &str) -> Result, String> { - match self { - Self::Redis(repository) => repository.get(uid, field), - Self::Surreal(repository) => repository.get(uid, field), - } - } - - fn delete(&self, uid: &str) -> Result<(), String> { - match self { - Self::Redis(repository) => repository.delete(uid), - Self::Surreal(repository) => repository.delete(uid), - } - } - - fn exists(&self, uid: &str) -> Result { - match self { - Self::Redis(repository) => repository.exists(uid), - Self::Surreal(repository) => repository.exists(uid), - } - } -} - -pub struct SurrealVGarageRepository; - -impl VGarageRepository for SurrealVGarageRepository { - fn create(&self, uid: &str, garage: &VGarage) -> Result<(), String> { - self.update(uid, garage) - } - - fn update(&self, uid: &str, garage: &VGarage) -> Result<(), String> { - surreal_upsert("owned_garage", uid, "virtual garage", garage) - } - - fn fetch(&self, uid: &str) -> Result, String> { - surreal_select("owned_garage", uid, "virtual garage") - } - - fn get(&self, uid: &str, field: &str) -> Result, String> { - let garage = self.fetch(uid)?.unwrap_or_else(VGarage::new); - match field { - "cars" => Ok(garage.cars), - "armor" => Ok(garage.armor), - "helis" => Ok(garage.helis), - "planes" => Ok(garage.planes), - "naval" => Ok(garage.naval), - "other" => Ok(garage.other), - _ => Err(format!("Unknown virtual garage field '{}'", field)), - } - } - - fn delete(&self, uid: &str) -> Result<(), String> { - surreal_delete::("owned_garage", uid, "virtual garage") - } - - fn exists(&self, uid: &str) -> Result { - self.fetch(uid).map(|garage| garage.is_some()) - } -} - -pub enum VLockerStorageRepository { - Redis(RedisVLockerRepository), - Surreal(SurrealVLockerRepository), -} - -impl VLockerStorageRepository { - pub fn configured() -> Self { - match load().storage.backend { - StorageBackend::Surreal => Self::Surreal(SurrealVLockerRepository), - StorageBackend::Redis => { - Self::Redis(RedisVLockerRepository::new(ExtensionRedisClient::new())) - } - } - } -} - -pub enum OrgStorageRepository { - Redis(RedisOrgRepository), - Surreal(SurrealOrgRepository), -} - -impl OrgStorageRepository { - pub fn configured() -> Self { - match load().storage.backend { - StorageBackend::Surreal => Self::Surreal(SurrealOrgRepository), - StorageBackend::Redis => { - Self::Redis(RedisOrgRepository::new(ExtensionRedisClient::new())) - } - } - } -} - -impl OrgRepository for OrgStorageRepository { - fn create(&self, org: &Org) -> Result<(), String> { - match self { - Self::Redis(repository) => repository.create(org), - Self::Surreal(repository) => repository.create(org), - } - } - - fn get_by_id(&self, id: &str) -> Result, String> { - match self { - Self::Redis(repository) => repository.get_by_id(id), - Self::Surreal(repository) => repository.get_by_id(id), - } - } - - fn update(&self, org: &Org) -> Result<(), String> { - match self { - Self::Redis(repository) => repository.update(org), - Self::Surreal(repository) => repository.update(org), - } - } - - fn delete(&self, id: &str) -> Result<(), String> { - match self { - Self::Redis(repository) => repository.delete(id), - Self::Surreal(repository) => repository.delete(id), - } - } - - fn exists(&self, id: &str) -> Result { - match self { - Self::Redis(repository) => repository.exists(id), - Self::Surreal(repository) => repository.exists(id), - } - } - - fn add_member(&self, org_id: &str, member_uid: &str) -> Result<(), String> { - match self { - Self::Redis(repository) => repository.add_member(org_id, member_uid), - Self::Surreal(repository) => repository.add_member(org_id, member_uid), - } - } - - fn get_members(&self, org_id: &str) -> Result, String> { - match self { - Self::Redis(repository) => repository.get_members(org_id), - Self::Surreal(repository) => repository.get_members(org_id), - } - } - - fn remove_member(&self, org_id: &str, member_uid: &str) -> Result<(), String> { - match self { - Self::Redis(repository) => repository.remove_member(org_id, member_uid), - Self::Surreal(repository) => repository.remove_member(org_id, member_uid), - } - } - - fn get_assets( - &self, - org_id: &str, - ) -> Result>, String> { - match self { - Self::Redis(repository) => repository.get_assets(org_id), - Self::Surreal(repository) => repository.get_assets(org_id), - } - } - - fn update_assets( - &self, - org_id: &str, - assets: &HashMap>, - ) -> Result<(), String> { - match self { - Self::Redis(repository) => repository.update_assets(org_id, assets), - Self::Surreal(repository) => repository.update_assets(org_id, assets), - } - } - - fn get_fleet(&self, org_id: &str) -> Result, String> { - match self { - Self::Redis(repository) => repository.get_fleet(org_id), - Self::Surreal(repository) => repository.get_fleet(org_id), - } - } - - fn update_fleet( - &self, - org_id: &str, - fleet: &HashMap, - ) -> Result<(), String> { - match self { - Self::Redis(repository) => repository.update_fleet(org_id, fleet), - Self::Surreal(repository) => repository.update_fleet(org_id, fleet), - } - } -} - -#[derive(Debug, Default, Serialize, Deserialize)] -struct OrgMemberRecord { - #[serde(default)] - members: Vec, -} - -#[derive(Debug, Default, Serialize, Deserialize)] -struct OrgAssetRecord { - #[serde(default)] - assets: HashMap>, -} - -#[derive(Debug, Default, Serialize, Deserialize)] -struct OrgFleetRecord { - #[serde(default)] - fleet: HashMap, -} - -#[derive(Debug, Default, Serialize, Deserialize)] -struct SurrealOrgRecord { - #[serde(default)] - org_id: String, - #[serde(default)] - owner: String, - #[serde(default)] - name: String, - #[serde(default)] - funds: f64, - #[serde(default)] - reputation: i64, - #[serde(default)] - credit_lines: HashMap, -} - -impl SurrealOrgRecord { - fn into_org(self, fallback_id: &str) -> Org { - let id = if self.org_id.trim().is_empty() { - fallback_id.to_string() - } else { - self.org_id - }; - - Org { - id, - owner: self.owner, - name: self.name, - funds: self.funds, - reputation: self.reputation, - credit_lines: self.credit_lines, - } - } -} - -impl From<&Org> for SurrealOrgRecord { - fn from(org: &Org) -> Self { - Self { - org_id: org.id.clone(), - owner: org.owner.clone(), - name: org.name.clone(), - funds: org.funds, - reputation: org.reputation, - credit_lines: org.credit_lines.clone(), - } - } -} - -pub struct SurrealOrgRepository; - -impl OrgRepository for SurrealOrgRepository { - fn create(&self, org: &Org) -> Result<(), String> { - self.update(org) - } - - fn get_by_id(&self, id: &str) -> Result, String> { - Ok(surreal_select::("org", id, "org")?.map(|record| record.into_org(id))) - } - - fn update(&self, org: &Org) -> Result<(), String> { - let record = SurrealOrgRecord::from(org); - surreal_upsert("org", org.id.as_str(), "org", &record) - } - - fn delete(&self, id: &str) -> Result<(), String> { - surreal_delete::("org", id, "org")?; - surreal_delete::("org_members", id, "org members")?; - surreal_delete::("org_assets", id, "org assets")?; - surreal_delete::("org_fleet", id, "org fleet") - } - - fn exists(&self, id: &str) -> Result { - self.get_by_id(id).map(|org| org.is_some()) - } - - fn add_member(&self, org_id: &str, member_uid: &str) -> Result<(), String> { - if !self.exists(org_id)? { - return Err(format!("Organization {} does not exist", org_id)); - } - - let mut record = surreal_select::("org_members", org_id, "org members")? - .unwrap_or_default(); - if !record.members.iter().any(|uid| uid == member_uid) { - record.members.push(member_uid.to_string()); - } - surreal_upsert("org_members", org_id, "org members", &record) - } - - fn get_members(&self, org_id: &str) -> Result, String> { - let record = surreal_select::("org_members", org_id, "org members")? - .unwrap_or_default(); - let mut members = Vec::with_capacity(record.members.len()); - - for uid in record.members { - if uid.trim().is_empty() { - continue; - } - - let name = match surreal_select::("actor", &uid, "actor")? { - Some(actor) => actor - .name - .filter(|name| !name.trim().is_empty()) - .unwrap_or_else(|| "Unknown".to_string()), - None => "Unknown".to_string(), - }; - - members.push(MemberSummary { uid, name }); - } - - Ok(members) - } - - fn remove_member(&self, org_id: &str, member_uid: &str) -> Result<(), String> { - let mut record = surreal_select::("org_members", org_id, "org members")? - .unwrap_or_default(); - record.members.retain(|uid| uid != member_uid); - surreal_upsert("org_members", org_id, "org members", &record) - } - - fn get_assets( - &self, - org_id: &str, - ) -> Result>, String> { - Ok( - surreal_select::("org_assets", org_id, "org assets")? - .unwrap_or_default() - .assets, - ) - } - - fn update_assets( - &self, - org_id: &str, - assets: &HashMap>, - ) -> Result<(), String> { - let record = OrgAssetRecord { - assets: assets.clone(), - }; - surreal_upsert("org_assets", org_id, "org assets", &record) - } - - fn get_fleet(&self, org_id: &str) -> Result, String> { - Ok( - surreal_select::("org_fleet", org_id, "org fleet")? - .unwrap_or_default() - .fleet, - ) - } - - fn update_fleet( - &self, - org_id: &str, - fleet: &HashMap, - ) -> Result<(), String> { - let record = OrgFleetRecord { - fleet: fleet.clone(), - }; - surreal_upsert("org_fleet", org_id, "org fleet", &record) - } -} - -impl VLockerRepository for VLockerStorageRepository { - fn create(&self, uid: &str, locker: &VLocker) -> Result<(), String> { - match self { - Self::Redis(repository) => repository.create(uid, locker), - Self::Surreal(repository) => repository.create(uid, locker), - } - } - - fn update(&self, uid: &str, locker: &VLocker) -> Result<(), String> { - match self { - Self::Redis(repository) => repository.update(uid, locker), - Self::Surreal(repository) => repository.update(uid, locker), - } - } - - fn fetch(&self, uid: &str) -> Result, String> { - match self { - Self::Redis(repository) => repository.fetch(uid), - Self::Surreal(repository) => repository.fetch(uid), - } - } - - fn get(&self, uid: &str, field: &str) -> Result, String> { - match self { - Self::Redis(repository) => repository.get(uid, field), - Self::Surreal(repository) => repository.get(uid, field), - } - } - - fn delete(&self, uid: &str) -> Result<(), String> { - match self { - Self::Redis(repository) => repository.delete(uid), - Self::Surreal(repository) => repository.delete(uid), - } - } - - fn exists(&self, uid: &str) -> Result { - match self { - Self::Redis(repository) => repository.exists(uid), - Self::Surreal(repository) => repository.exists(uid), - } - } -} - -pub struct SurrealVLockerRepository; - -impl VLockerRepository for SurrealVLockerRepository { - fn create(&self, uid: &str, locker: &VLocker) -> Result<(), String> { - self.update(uid, locker) - } - - fn update(&self, uid: &str, locker: &VLocker) -> Result<(), String> { - surreal_upsert("owned_locker", uid, "virtual locker", locker) - } - - fn fetch(&self, uid: &str) -> Result, String> { - surreal_select("owned_locker", uid, "virtual locker") - } - - fn get(&self, uid: &str, field: &str) -> Result, String> { - let locker = self.fetch(uid)?.unwrap_or_else(VLocker::new); - match field { - "items" => Ok(locker.items), - "weapons" => Ok(locker.weapons), - "magazines" => Ok(locker.magazines), - "backpacks" => Ok(locker.backpacks), - _ => Err(format!("Unknown virtual locker field '{}'", field)), - } - } - - fn delete(&self, uid: &str) -> Result<(), String> { - surreal_delete::("owned_locker", uid, "virtual locker") - } - - fn exists(&self, uid: &str) -> Result { - self.fetch(uid).map(|locker| locker.is_some()) - } -} diff --git a/arma/server/extension/src/storage/actor.rs b/arma/server/extension/src/storage/actor.rs new file mode 100644 index 0000000..8b42e59 --- /dev/null +++ b/arma/server/extension/src/storage/actor.rs @@ -0,0 +1,135 @@ +use super::common::*; +use super::*; + +pub enum ActorStorageRepository { + Redis(RedisActorRepository), + Surreal(SurrealActorRepository), +} + +impl ActorStorageRepository { + pub fn configured() -> Self { + match load().storage.backend { + StorageBackend::Surreal => Self::Surreal(SurrealActorRepository), + StorageBackend::Redis => { + Self::Redis(RedisActorRepository::new(ExtensionRedisClient::new())) + } + } + } +} + +impl ActorRepository for ActorStorageRepository { + fn create(&self, actor: &Actor) -> Result<(), String> { + match self { + Self::Redis(repository) => repository.create(actor), + Self::Surreal(repository) => repository.create(actor), + } + } + + fn get_by_id(&self, id: &str) -> Result, String> { + match self { + Self::Redis(repository) => repository.get_by_id(id), + Self::Surreal(repository) => repository.get_by_id(id), + } + } + + fn update(&self, actor: &Actor) -> Result<(), String> { + match self { + Self::Redis(repository) => repository.update(actor), + Self::Surreal(repository) => repository.update(actor), + } + } + + fn delete(&self, id: &str) -> Result<(), String> { + match self { + Self::Redis(repository) => repository.delete(id), + Self::Surreal(repository) => repository.delete(id), + } + } + + fn exists(&self, id: &str) -> Result { + match self { + Self::Redis(repository) => repository.exists(id), + Self::Surreal(repository) => repository.exists(id), + } + } +} + +pub struct SurrealActorRepository; + +#[derive(Debug, Clone, Serialize, Deserialize)] +struct ActorRecord { + uid: String, + name: Option, + loadout: serde_json::Value, + position: Option>, + direction: f64, + stance: Option, + email: String, + phone_number: String, + state: String, + holster: bool, + rank: Option, + organization: String, +} + +impl ActorRecord { + fn into_actor(self) -> Actor { + Actor { + uid: self.uid, + name: self.name, + loadout: self.loadout, + position: self.position, + direction: self.direction, + stance: self.stance, + email: self.email, + phone_number: self.phone_number, + state: self.state, + holster: self.holster, + rank: self.rank, + organization: self.organization, + } + } +} + +impl From<&Actor> for ActorRecord { + fn from(actor: &Actor) -> Self { + Self { + uid: actor.uid.clone(), + name: actor.name.clone(), + loadout: actor.loadout.clone(), + position: actor.position.clone(), + direction: actor.direction, + stance: actor.stance.clone(), + email: actor.email.clone(), + phone_number: actor.phone_number.clone(), + state: actor.state.clone(), + holster: actor.holster, + rank: actor.rank.clone(), + organization: actor.organization.clone(), + } + } +} + +impl ActorRepository for SurrealActorRepository { + fn create(&self, actor: &Actor) -> Result<(), String> { + self.update(actor) + } + + fn get_by_id(&self, id: &str) -> Result, String> { + surreal_select::("actor", id, "actor") + .map(|record| record.map(ActorRecord::into_actor)) + } + + fn update(&self, actor: &Actor) -> Result<(), String> { + let record = ActorRecord::from(actor); + surreal_upsert("actor", actor.uid.as_str(), "actor", &record) + } + + fn delete(&self, id: &str) -> Result<(), String> { + surreal_delete::("actor", id, "actor") + } + + fn exists(&self, id: &str) -> Result { + self.get_by_id(id).map(|actor| actor.is_some()) + } +} diff --git a/arma/server/extension/src/storage/bank.rs b/arma/server/extension/src/storage/bank.rs new file mode 100644 index 0000000..8bad0e8 --- /dev/null +++ b/arma/server/extension/src/storage/bank.rs @@ -0,0 +1,165 @@ +use super::common::*; +use super::*; + +pub enum BankStorageRepository { + Redis(RedisBankRepository), + Surreal(SurrealBankRepository), +} + +impl BankStorageRepository { + pub fn configured() -> Self { + match load().storage.backend { + StorageBackend::Surreal => Self::Surreal(SurrealBankRepository), + StorageBackend::Redis => { + Self::Redis(RedisBankRepository::new(ExtensionRedisClient::new())) + } + } + } +} + +impl BankRepository for BankStorageRepository { + fn create(&self, bank: &Bank) -> Result<(), String> { + match self { + Self::Redis(repository) => repository.create(bank), + Self::Surreal(repository) => repository.create(bank), + } + } + + fn get_by_id(&self, id: &str) -> Result, String> { + match self { + Self::Redis(repository) => repository.get_by_id(id), + Self::Surreal(repository) => repository.get_by_id(id), + } + } + + fn update(&self, bank: &Bank) -> Result<(), String> { + match self { + Self::Redis(repository) => repository.update(bank), + Self::Surreal(repository) => repository.update(bank), + } + } + + fn delete(&self, id: &str) -> Result<(), String> { + match self { + Self::Redis(repository) => repository.delete(id), + Self::Surreal(repository) => repository.delete(id), + } + } + + fn exists(&self, id: &str) -> Result { + match self { + Self::Redis(repository) => repository.exists(id), + Self::Surreal(repository) => repository.exists(id), + } + } +} + +pub struct SurrealBankRepository; + +#[derive(Debug, Clone, Serialize, Deserialize)] +struct BankAccountRecord { + uid: String, + name: String, + bank: f64, + cash: f64, + earnings: f64, + pin: u64, +} + +#[derive(Debug, Clone, Serialize, Deserialize)] +struct BankTransactionRecord { + uid: String, + ordinal: u64, + message: String, +} + +impl BankAccountRecord { + fn into_bank(self, transactions: Vec) -> Bank { + Bank { + uid: self.uid, + name: self.name, + bank: self.bank, + cash: self.cash, + earnings: self.earnings, + pin: self.pin, + transactions, + } + } +} + +impl From<&Bank> for BankAccountRecord { + fn from(bank: &Bank) -> Self { + Self { + uid: bank.uid.clone(), + name: bank.name.clone(), + bank: bank.bank, + cash: bank.cash, + earnings: bank.earnings, + pin: bank.pin, + } + } +} + +fn bank_transaction_id(uid: &str, ordinal: usize) -> String { + format!("{}:{}", uid, ordinal) +} + +fn bank_transactions_from_records(mut records: Vec) -> Vec { + records.sort_by_key(|record| record.ordinal); + records + .into_iter() + .map(|record| record.message) + .collect::>() +} + +impl BankRepository for SurrealBankRepository { + fn create(&self, bank: &Bank) -> Result<(), String> { + self.update(bank) + } + + fn get_by_id(&self, id: &str) -> Result, String> { + let Some(record) = surreal_select::("bank", id, "bank")? else { + return Ok(None); + }; + + let transaction_records = surreal_select_by_uid::( + "bank_transaction", + "bank transactions", + id, + )?; + let transactions = bank_transactions_from_records(transaction_records); + + Ok(Some(record.into_bank(transactions))) + } + + fn update(&self, bank: &Bank) -> Result<(), String> { + let account = BankAccountRecord::from(bank); + surreal_upsert("bank", bank.uid.as_str(), "bank", &account)?; + surreal_delete_by_uid("bank_transaction", "bank transactions", &bank.uid)?; + + for (ordinal, message) in bank.transactions.iter().enumerate() { + let record = BankTransactionRecord { + uid: bank.uid.clone(), + ordinal: ordinal as u64, + message: message.clone(), + }; + surreal_upsert( + "bank_transaction", + &bank_transaction_id(&bank.uid, ordinal), + "bank transaction", + &record, + )?; + } + + Ok(()) + } + + fn delete(&self, id: &str) -> Result<(), String> { + surreal_delete_by_uid("bank_transaction", "bank transactions", id)?; + surreal_delete::("bank", id, "bank") + } + + fn exists(&self, id: &str) -> Result { + self.get_by_id(id).map(|bank| bank.is_some()) + } +} diff --git a/arma/server/extension/src/storage/common.rs b/arma/server/extension/src/storage/common.rs new file mode 100644 index 0000000..800ef50 --- /dev/null +++ b/arma/server/extension/src/storage/common.rs @@ -0,0 +1,132 @@ +use super::*; + +pub(super) fn surreal_select( + table: &'static str, + id: &str, + label: &str, +) -> Result, String> +where + T: DeserializeOwned, +{ + let id = id.to_string(); + RUNTIME.block_on(async move { + surreal::client() + .await? + .select((table, id.as_str())) + .await + .map_err(|error| format!("SurrealDB {} select failed: {}", label, error)) + }) +} + +pub(super) fn surreal_select_all(table: &'static str, label: &str) -> Result, String> +where + T: DeserializeOwned, +{ + RUNTIME.block_on(async move { + surreal::client() + .await? + .select(table) + .await + .map_err(|error| format!("SurrealDB {} select all failed: {}", label, error)) + }) +} + +pub(super) fn surreal_upsert( + table: &'static str, + id: &str, + label: &str, + record: &T, +) -> Result<(), String> +where + T: Serialize + DeserializeOwned, +{ + let id = id.to_string(); + let record = serde_json::to_value(record) + .map_err(|error| format!("SurrealDB {} serialize failed: {}", label, error))?; + RUNTIME.block_on(async move { + let _: Option = surreal::client() + .await? + .upsert((table, id.as_str())) + .content(record) + .await + .map_err(|error| format!("SurrealDB {} upsert failed: {}", label, error))?; + Ok(()) + }) +} + +pub(super) fn surreal_delete(table: &'static str, id: &str, label: &str) -> Result<(), String> +where + T: DeserializeOwned, +{ + let id = id.to_string(); + RUNTIME.block_on(async move { + let _: Option = surreal::client() + .await? + .delete((table, id.as_str())) + .await + .map_err(|error| format!("SurrealDB {} delete failed: {}", label, error))?; + Ok(()) + }) +} + +pub(super) fn surreal_select_by_uid( + table: &'static str, + label: &str, + uid: &str, +) -> Result, String> +where + T: DeserializeOwned, +{ + surreal_select_by_field(table, label, "uid", uid) +} + +pub(super) fn surreal_select_by_field( + table: &'static str, + label: &str, + field: &'static str, + value: &str, +) -> Result, String> +where + T: DeserializeOwned, +{ + let value = value.to_string(); + RUNTIME.block_on(async move { + let mut response = surreal::client() + .await? + .query(format!("SELECT * FROM {} WHERE {} = $value", table, field)) + .bind(("value", value)) + .await + .map_err(|error| format!("SurrealDB {} select by field failed: {}", label, error))?; + response + .take(0) + .map_err(|error| format!("SurrealDB {} select by field failed: {}", label, error)) + }) +} + +pub(super) fn surreal_delete_by_uid( + table: &'static str, + label: &str, + uid: &str, +) -> Result<(), String> { + surreal_delete_by_field(table, label, "uid", uid) +} + +pub(super) fn surreal_delete_by_field( + table: &'static str, + label: &str, + field: &'static str, + value: &str, +) -> Result<(), String> { + let value = value.to_string(); + RUNTIME.block_on(async move { + surreal::client() + .await? + .query(format!("DELETE {} WHERE {} = $value", table, field)) + .bind(("value", value)) + .await + .map_err(|error| format!("SurrealDB {} delete by field failed: {}", label, error))? + .check() + .map_err(|error| format!("SurrealDB {} delete by field failed: {}", label, error))?; + Ok(()) + }) +} diff --git a/arma/server/extension/src/storage/garage.rs b/arma/server/extension/src/storage/garage.rs new file mode 100644 index 0000000..bbff0b6 --- /dev/null +++ b/arma/server/extension/src/storage/garage.rs @@ -0,0 +1,339 @@ +use super::common::*; +use super::*; + +pub enum GarageStorageRepository { + Redis(RedisGarageRepository), + Surreal(SurrealGarageRepository), +} + +impl GarageStorageRepository { + pub fn configured() -> Self { + match load().storage.backend { + StorageBackend::Surreal => Self::Surreal(SurrealGarageRepository), + StorageBackend::Redis => { + Self::Redis(RedisGarageRepository::new(ExtensionRedisClient::new())) + } + } + } +} + +impl GarageRepository for GarageStorageRepository { + fn create(&self, uid: &str, garage: &Garage) -> Result<(), String> { + match self { + Self::Redis(repository) => repository.create(uid, garage), + Self::Surreal(repository) => repository.create(uid, garage), + } + } + + fn update(&self, uid: &str, garage: &Garage) -> Result<(), String> { + match self { + Self::Redis(repository) => repository.update(uid, garage), + Self::Surreal(repository) => repository.update(uid, garage), + } + } + + fn get(&self, uid: &str) -> Result, String> { + match self { + Self::Redis(repository) => repository.get(uid), + Self::Surreal(repository) => repository.get(uid), + } + } + + fn delete(&self, uid: &str) -> Result<(), String> { + match self { + Self::Redis(repository) => repository.delete(uid), + Self::Surreal(repository) => repository.delete(uid), + } + } + + fn exists(&self, uid: &str) -> Result { + match self { + Self::Redis(repository) => repository.exists(uid), + Self::Surreal(repository) => repository.exists(uid), + } + } +} + +pub struct SurrealGarageRepository; + +#[derive(Debug, Clone, Serialize, Deserialize)] +struct GarageOwnerRecord { + uid: String, +} + +#[derive(Debug, Clone, Serialize, Deserialize)] +struct GarageVehicleRecord { + uid: String, + plate: String, + classname: String, + fuel: f64, + damage: f64, + hit_points: HitPoints, +} + +fn garage_vehicle_id(uid: &str, plate: &str) -> String { + format!("{}:{}", uid, plate) +} + +fn garage_from_vehicle_records(records: Vec) -> Garage { + let vehicles = records + .into_iter() + .map(|record| { + let vehicle = Vehicle { + plate: record.plate.clone(), + classname: record.classname, + fuel: record.fuel, + damage: record.damage, + hit_points: record.hit_points, + }; + (record.plate, vehicle) + }) + .collect(); + + Garage { vehicles } +} + +impl GarageRepository for SurrealGarageRepository { + fn create(&self, uid: &str, garage: &Garage) -> Result<(), String> { + self.update(uid, garage) + } + + fn update(&self, uid: &str, garage: &Garage) -> Result<(), String> { + let owner = GarageOwnerRecord { + uid: uid.to_string(), + }; + surreal_upsert("garage", uid, "garage owner", &owner)?; + surreal_delete_by_uid("garage_vehicle", "garage vehicles", uid)?; + + for (plate_key, vehicle) in &garage.vehicles { + let plate = if vehicle.plate.trim().is_empty() { + plate_key.clone() + } else { + vehicle.plate.clone() + }; + let record = GarageVehicleRecord { + uid: uid.to_string(), + plate: plate.clone(), + classname: vehicle.classname.clone(), + fuel: vehicle.fuel, + damage: vehicle.damage, + hit_points: vehicle.hit_points.clone(), + }; + surreal_upsert( + "garage_vehicle", + &garage_vehicle_id(uid, &plate), + "garage vehicle", + &record, + )?; + } + + Ok(()) + } + + fn get(&self, uid: &str) -> Result, String> { + if surreal_select::("garage", uid, "garage owner")?.is_none() { + return Ok(None); + } + + let vehicle_records = + surreal_select_by_uid::("garage_vehicle", "garage vehicles", uid)?; + Ok(Some(garage_from_vehicle_records(vehicle_records))) + } + + fn delete(&self, uid: &str) -> Result<(), String> { + surreal_delete_by_uid("garage_vehicle", "garage vehicles", uid)?; + surreal_delete::("garage", uid, "garage owner") + } + + fn exists(&self, uid: &str) -> Result { + surreal_select::("garage", uid, "garage owner") + .map(|garage| garage.is_some()) + } +} + +pub enum VGarageStorageRepository { + Redis(RedisVGarageRepository), + Surreal(SurrealVGarageRepository), +} + +impl VGarageStorageRepository { + pub fn configured() -> Self { + match load().storage.backend { + StorageBackend::Surreal => Self::Surreal(SurrealVGarageRepository), + StorageBackend::Redis => { + Self::Redis(RedisVGarageRepository::new(ExtensionRedisClient::new())) + } + } + } +} + +impl VGarageRepository for VGarageStorageRepository { + fn create(&self, uid: &str, garage: &VGarage) -> Result<(), String> { + match self { + Self::Redis(repository) => repository.create(uid, garage), + Self::Surreal(repository) => repository.create(uid, garage), + } + } + + fn update(&self, uid: &str, garage: &VGarage) -> Result<(), String> { + match self { + Self::Redis(repository) => repository.update(uid, garage), + Self::Surreal(repository) => repository.update(uid, garage), + } + } + + fn fetch(&self, uid: &str) -> Result, String> { + match self { + Self::Redis(repository) => repository.fetch(uid), + Self::Surreal(repository) => repository.fetch(uid), + } + } + + fn get(&self, uid: &str, field: &str) -> Result, String> { + match self { + Self::Redis(repository) => repository.get(uid, field), + Self::Surreal(repository) => repository.get(uid, field), + } + } + + fn delete(&self, uid: &str) -> Result<(), String> { + match self { + Self::Redis(repository) => repository.delete(uid), + Self::Surreal(repository) => repository.delete(uid), + } + } + + fn exists(&self, uid: &str) -> Result { + match self { + Self::Redis(repository) => repository.exists(uid), + Self::Surreal(repository) => repository.exists(uid), + } + } +} + +pub struct SurrealVGarageRepository; + +#[derive(Debug, Clone, Serialize, Deserialize)] +struct VGarageOwnerRecord { + uid: String, +} + +#[derive(Debug, Clone, Serialize, Deserialize)] +struct GarageUnlockRecord { + uid: String, + category: String, + classname: String, + source: Option, +} + +fn garage_unlock_id(uid: &str, category: &str, classname: &str) -> String { + format!("{}:{}:{}", uid, category, classname) +} + +fn push_garage_unlock(garage: &mut VGarage, category: &str, classname: String) { + let target = match category { + "cars" => &mut garage.cars, + "armor" => &mut garage.armor, + "helis" => &mut garage.helis, + "planes" => &mut garage.planes, + "naval" => &mut garage.naval, + "other" => &mut garage.other, + _ => return, + }; + + if !target.contains(&classname) { + target.push(classname); + } +} + +fn vgarage_from_unlock_records(records: Vec) -> VGarage { + let mut garage = VGarage { + cars: Vec::new(), + armor: Vec::new(), + helis: Vec::new(), + planes: Vec::new(), + naval: Vec::new(), + other: Vec::new(), + }; + + for record in records { + push_garage_unlock(&mut garage, &record.category, record.classname); + } + + garage +} + +fn upsert_garage_unlocks(uid: &str, category: &str, classnames: &[String]) -> Result<(), String> { + for classname in classnames { + let record = GarageUnlockRecord { + uid: uid.to_string(), + category: category.to_string(), + classname: classname.clone(), + source: None, + }; + surreal_upsert( + "garage_unlock", + &garage_unlock_id(uid, category, classname), + "garage unlock", + &record, + )?; + } + + Ok(()) +} + +impl VGarageRepository for SurrealVGarageRepository { + fn create(&self, uid: &str, garage: &VGarage) -> Result<(), String> { + self.update(uid, garage) + } + + fn update(&self, uid: &str, garage: &VGarage) -> Result<(), String> { + let owner = VGarageOwnerRecord { + uid: uid.to_string(), + }; + surreal_upsert("owned_garage", uid, "virtual garage owner", &owner)?; + surreal_delete_by_uid("garage_unlock", "garage unlocks", uid)?; + upsert_garage_unlocks(uid, "cars", &garage.cars)?; + upsert_garage_unlocks(uid, "armor", &garage.armor)?; + upsert_garage_unlocks(uid, "helis", &garage.helis)?; + upsert_garage_unlocks(uid, "planes", &garage.planes)?; + upsert_garage_unlocks(uid, "naval", &garage.naval)?; + upsert_garage_unlocks(uid, "other", &garage.other)?; + Ok(()) + } + + fn fetch(&self, uid: &str) -> Result, String> { + if surreal_select::("owned_garage", uid, "virtual garage owner")? + .is_none() + { + return Ok(None); + } + + let unlock_records = + surreal_select_by_uid::("garage_unlock", "garage unlocks", uid)?; + Ok(Some(vgarage_from_unlock_records(unlock_records))) + } + + fn get(&self, uid: &str, field: &str) -> Result, String> { + let garage = self.fetch(uid)?.unwrap_or_else(VGarage::new); + match field { + "cars" => Ok(garage.cars), + "armor" => Ok(garage.armor), + "helis" => Ok(garage.helis), + "planes" => Ok(garage.planes), + "naval" => Ok(garage.naval), + "other" => Ok(garage.other), + _ => Err(format!("Unknown virtual garage field '{}'", field)), + } + } + + fn delete(&self, uid: &str) -> Result<(), String> { + surreal_delete_by_uid("garage_unlock", "garage unlocks", uid)?; + surreal_delete::("owned_garage", uid, "virtual garage owner") + } + + fn exists(&self, uid: &str) -> Result { + surreal_select::("owned_garage", uid, "virtual garage owner") + .map(|garage| garage.is_some()) + } +} diff --git a/arma/server/extension/src/storage/locker.rs b/arma/server/extension/src/storage/locker.rs new file mode 100644 index 0000000..69ccf7d --- /dev/null +++ b/arma/server/extension/src/storage/locker.rs @@ -0,0 +1,320 @@ +use super::common::*; +use super::*; + +pub enum LockerStorageRepository { + Redis(RedisLockerRepository), + Surreal(SurrealLockerRepository), +} + +impl LockerStorageRepository { + pub fn configured() -> Self { + match load().storage.backend { + StorageBackend::Surreal => Self::Surreal(SurrealLockerRepository), + StorageBackend::Redis => { + Self::Redis(RedisLockerRepository::new(ExtensionRedisClient::new())) + } + } + } +} + +impl LockerRepository for LockerStorageRepository { + fn create(&self, uid: &str, locker: &Locker) -> Result<(), String> { + match self { + Self::Redis(repository) => repository.create(uid, locker), + Self::Surreal(repository) => repository.create(uid, locker), + } + } + + fn update(&self, uid: &str, locker: &Locker) -> Result<(), String> { + match self { + Self::Redis(repository) => repository.update(uid, locker), + Self::Surreal(repository) => repository.update(uid, locker), + } + } + + fn get(&self, uid: &str) -> Result, String> { + match self { + Self::Redis(repository) => repository.get(uid), + Self::Surreal(repository) => repository.get(uid), + } + } + + fn delete(&self, uid: &str) -> Result<(), String> { + match self { + Self::Redis(repository) => repository.delete(uid), + Self::Surreal(repository) => repository.delete(uid), + } + } + + fn exists(&self, uid: &str) -> Result { + match self { + Self::Redis(repository) => repository.exists(uid), + Self::Surreal(repository) => repository.exists(uid), + } + } +} + +pub struct SurrealLockerRepository; + +#[derive(Debug, Clone, Serialize, Deserialize)] +struct LockerOwnerRecord { + uid: String, +} + +#[derive(Debug, Clone, Serialize, Deserialize)] +struct LockerItemRecord { + uid: String, + category: String, + classname: String, + amount: u32, +} + +fn locker_item_id(uid: &str, classname: &str) -> String { + format!("{}:{}", uid, classname) +} + +fn locker_from_item_records(records: Vec) -> Locker { + let items = records + .into_iter() + .map(|record| { + let item = Item { + category: record.category, + classname: record.classname.clone(), + amount: record.amount, + }; + (record.classname, item) + }) + .collect(); + + Locker { items } +} + +impl LockerRepository for SurrealLockerRepository { + fn create(&self, uid: &str, locker: &Locker) -> Result<(), String> { + self.update(uid, locker) + } + + fn update(&self, uid: &str, locker: &Locker) -> Result<(), String> { + let owner = LockerOwnerRecord { + uid: uid.to_string(), + }; + surreal_upsert("locker", uid, "locker owner", &owner)?; + surreal_delete_by_uid("locker_item", "locker items", uid)?; + + for item in locker.items.values() { + let record = LockerItemRecord { + uid: uid.to_string(), + category: item.category.clone(), + classname: item.classname.clone(), + amount: item.amount, + }; + surreal_upsert( + "locker_item", + &locker_item_id(uid, &item.classname), + "locker item", + &record, + )?; + } + + Ok(()) + } + + fn get(&self, uid: &str) -> Result, String> { + if surreal_select::("locker", uid, "locker owner")?.is_none() { + return Ok(None); + } + + let item_records = + surreal_select_by_uid::("locker_item", "locker items", uid)?; + Ok(Some(locker_from_item_records(item_records))) + } + + fn delete(&self, uid: &str) -> Result<(), String> { + surreal_delete_by_uid("locker_item", "locker items", uid)?; + surreal_delete::("locker", uid, "locker owner") + } + + fn exists(&self, uid: &str) -> Result { + surreal_select::("locker", uid, "locker owner") + .map(|locker| locker.is_some()) + } +} + +pub enum VLockerStorageRepository { + Redis(RedisVLockerRepository), + Surreal(SurrealVLockerRepository), +} + +impl VLockerStorageRepository { + pub fn configured() -> Self { + match load().storage.backend { + StorageBackend::Surreal => Self::Surreal(SurrealVLockerRepository), + StorageBackend::Redis => { + Self::Redis(RedisVLockerRepository::new(ExtensionRedisClient::new())) + } + } + } +} + +impl VLockerRepository for VLockerStorageRepository { + fn create(&self, uid: &str, locker: &VLocker) -> Result<(), String> { + match self { + Self::Redis(repository) => repository.create(uid, locker), + Self::Surreal(repository) => repository.create(uid, locker), + } + } + + fn update(&self, uid: &str, locker: &VLocker) -> Result<(), String> { + match self { + Self::Redis(repository) => repository.update(uid, locker), + Self::Surreal(repository) => repository.update(uid, locker), + } + } + + fn fetch(&self, uid: &str) -> Result, String> { + match self { + Self::Redis(repository) => repository.fetch(uid), + Self::Surreal(repository) => repository.fetch(uid), + } + } + + fn get(&self, uid: &str, field: &str) -> Result, String> { + match self { + Self::Redis(repository) => repository.get(uid, field), + Self::Surreal(repository) => repository.get(uid, field), + } + } + + fn delete(&self, uid: &str) -> Result<(), String> { + match self { + Self::Redis(repository) => repository.delete(uid), + Self::Surreal(repository) => repository.delete(uid), + } + } + + fn exists(&self, uid: &str) -> Result { + match self { + Self::Redis(repository) => repository.exists(uid), + Self::Surreal(repository) => repository.exists(uid), + } + } +} + +pub struct SurrealVLockerRepository; + +#[derive(Debug, Clone, Serialize, Deserialize)] +struct VLockerOwnerRecord { + uid: String, +} + +#[derive(Debug, Clone, Serialize, Deserialize)] +struct LockerUnlockRecord { + uid: String, + category: String, + classname: String, + source: Option, +} + +fn locker_unlock_id(uid: &str, category: &str, classname: &str) -> String { + format!("{}:{}:{}", uid, category, classname) +} + +fn push_locker_unlock(locker: &mut VLocker, category: &str, classname: String) { + let target = match category { + "items" => &mut locker.items, + "weapons" => &mut locker.weapons, + "magazines" => &mut locker.magazines, + "backpacks" => &mut locker.backpacks, + _ => return, + }; + + if !target.contains(&classname) { + target.push(classname); + } +} + +fn vlocker_from_unlock_records(records: Vec) -> VLocker { + let mut locker = VLocker { + items: Vec::new(), + weapons: Vec::new(), + magazines: Vec::new(), + backpacks: Vec::new(), + }; + + for record in records { + push_locker_unlock(&mut locker, &record.category, record.classname); + } + + locker +} + +fn upsert_locker_unlocks(uid: &str, category: &str, classnames: &[String]) -> Result<(), String> { + for classname in classnames { + let record = LockerUnlockRecord { + uid: uid.to_string(), + category: category.to_string(), + classname: classname.clone(), + source: None, + }; + surreal_upsert( + "locker_unlock", + &locker_unlock_id(uid, category, classname), + "locker unlock", + &record, + )?; + } + + Ok(()) +} + +impl VLockerRepository for SurrealVLockerRepository { + fn create(&self, uid: &str, locker: &VLocker) -> Result<(), String> { + self.update(uid, locker) + } + + fn update(&self, uid: &str, locker: &VLocker) -> Result<(), String> { + let owner = VLockerOwnerRecord { + uid: uid.to_string(), + }; + surreal_upsert("owned_locker", uid, "virtual locker owner", &owner)?; + surreal_delete_by_uid("locker_unlock", "locker unlocks", uid)?; + upsert_locker_unlocks(uid, "items", &locker.items)?; + upsert_locker_unlocks(uid, "weapons", &locker.weapons)?; + upsert_locker_unlocks(uid, "magazines", &locker.magazines)?; + upsert_locker_unlocks(uid, "backpacks", &locker.backpacks)?; + Ok(()) + } + + fn fetch(&self, uid: &str) -> Result, String> { + if surreal_select::("owned_locker", uid, "virtual locker owner")? + .is_none() + { + return Ok(None); + } + + let unlock_records = + surreal_select_by_uid::("locker_unlock", "locker unlocks", uid)?; + Ok(Some(vlocker_from_unlock_records(unlock_records))) + } + + fn get(&self, uid: &str, field: &str) -> Result, String> { + let locker = self.fetch(uid)?.unwrap_or_else(VLocker::new); + match field { + "items" => Ok(locker.items), + "weapons" => Ok(locker.weapons), + "magazines" => Ok(locker.magazines), + "backpacks" => Ok(locker.backpacks), + _ => Err(format!("Unknown virtual locker field '{}'", field)), + } + } + + fn delete(&self, uid: &str) -> Result<(), String> { + surreal_delete_by_uid("locker_unlock", "locker unlocks", uid)?; + surreal_delete::("owned_locker", uid, "virtual locker owner") + } + + fn exists(&self, uid: &str) -> Result { + surreal_select::("owned_locker", uid, "virtual locker owner") + .map(|locker| locker.is_some()) + } +} diff --git a/arma/server/extension/src/storage/org.rs b/arma/server/extension/src/storage/org.rs new file mode 100644 index 0000000..50e46e0 --- /dev/null +++ b/arma/server/extension/src/storage/org.rs @@ -0,0 +1,501 @@ +use super::common::*; +use super::*; + +pub enum OrgStorageRepository { + Redis(RedisOrgRepository), + Surreal(SurrealOrgRepository), +} + +impl OrgStorageRepository { + pub fn configured() -> Self { + match load().storage.backend { + StorageBackend::Surreal => Self::Surreal(SurrealOrgRepository), + StorageBackend::Redis => { + Self::Redis(RedisOrgRepository::new(ExtensionRedisClient::new())) + } + } + } +} + +impl OrgRepository for OrgStorageRepository { + fn create(&self, org: &Org) -> Result<(), String> { + match self { + Self::Redis(repository) => repository.create(org), + Self::Surreal(repository) => repository.create(org), + } + } + + fn get_by_id(&self, id: &str) -> Result, String> { + match self { + Self::Redis(repository) => repository.get_by_id(id), + Self::Surreal(repository) => repository.get_by_id(id), + } + } + + fn update(&self, org: &Org) -> Result<(), String> { + match self { + Self::Redis(repository) => repository.update(org), + Self::Surreal(repository) => repository.update(org), + } + } + + fn delete(&self, id: &str) -> Result<(), String> { + match self { + Self::Redis(repository) => repository.delete(id), + Self::Surreal(repository) => repository.delete(id), + } + } + + fn exists(&self, id: &str) -> Result { + match self { + Self::Redis(repository) => repository.exists(id), + Self::Surreal(repository) => repository.exists(id), + } + } + + fn add_member(&self, org_id: &str, member_uid: &str) -> Result<(), String> { + match self { + Self::Redis(repository) => repository.add_member(org_id, member_uid), + Self::Surreal(repository) => repository.add_member(org_id, member_uid), + } + } + + fn get_members(&self, org_id: &str) -> Result, String> { + match self { + Self::Redis(repository) => repository.get_members(org_id), + Self::Surreal(repository) => repository.get_members(org_id), + } + } + + fn remove_member(&self, org_id: &str, member_uid: &str) -> Result<(), String> { + match self { + Self::Redis(repository) => repository.remove_member(org_id, member_uid), + Self::Surreal(repository) => repository.remove_member(org_id, member_uid), + } + } + + fn get_assets( + &self, + org_id: &str, + ) -> Result>, String> { + match self { + Self::Redis(repository) => repository.get_assets(org_id), + Self::Surreal(repository) => repository.get_assets(org_id), + } + } + + fn update_assets( + &self, + org_id: &str, + assets: &HashMap>, + ) -> Result<(), String> { + match self { + Self::Redis(repository) => repository.update_assets(org_id, assets), + Self::Surreal(repository) => repository.update_assets(org_id, assets), + } + } + + fn get_fleet(&self, org_id: &str) -> Result, String> { + match self { + Self::Redis(repository) => repository.get_fleet(org_id), + Self::Surreal(repository) => repository.get_fleet(org_id), + } + } + + fn update_fleet( + &self, + org_id: &str, + fleet: &HashMap, + ) -> Result<(), String> { + match self { + Self::Redis(repository) => repository.update_fleet(org_id, fleet), + Self::Surreal(repository) => repository.update_fleet(org_id, fleet), + } + } +} + +#[derive(Debug, Default, Serialize, Deserialize)] +struct SurrealOrgRecord { + #[serde(default)] + org_id: String, + #[serde(default)] + owner: String, + #[serde(default)] + name: String, + #[serde(default)] + funds: f64, + #[serde(default)] + reputation: i64, +} + +#[derive(Debug, Clone, Serialize, Deserialize)] +struct OrgMemberRow { + org_id: String, + member_uid: String, +} + +#[derive(Debug, Clone, Serialize, Deserialize)] +struct OrgCreditLineRow { + org_id: String, + uid: String, + name: String, + approved_amount: f64, + available_amount: f64, + outstanding_principal: f64, + interest_rate: f64, + amount_due: f64, + amount: f64, +} + +#[derive(Debug, Clone, Serialize, Deserialize)] +struct OrgAssetRow { + org_id: String, + category: String, + classname: String, + asset_type: String, + quantity: i64, +} + +#[derive(Debug, Clone, Serialize, Deserialize)] +struct OrgFleetRow { + org_id: String, + fleet_key: String, + classname: String, + name: String, + fleet_type: String, + status: String, + damage: String, +} + +impl SurrealOrgRecord { + fn into_org(self, fallback_id: &str, credit_lines: HashMap) -> Org { + let id = if self.org_id.trim().is_empty() { + fallback_id.to_string() + } else { + self.org_id + }; + + let mut org = Org { + id, + owner: self.owner, + name: self.name, + funds: self.funds, + reputation: self.reputation, + credit_lines, + }; + org.normalize_credit_lines(); + org + } +} + +impl From<&Org> for SurrealOrgRecord { + fn from(org: &Org) -> Self { + Self { + org_id: org.id.clone(), + owner: org.owner.clone(), + name: org.name.clone(), + funds: org.funds, + reputation: org.reputation, + } + } +} + +pub struct SurrealOrgRepository; + +fn org_member_id(org_id: &str, member_uid: &str) -> String { + format!("{}:{}", org_id, member_uid) +} + +fn org_credit_line_id(org_id: &str, uid: &str) -> String { + format!("{}:{}", org_id, uid) +} + +fn org_asset_id(org_id: &str, category: &str, classname: &str) -> String { + format!("{}:{}:{}", org_id, category, classname) +} + +fn org_fleet_id(org_id: &str, fleet_key: &str) -> String { + format!("{}:{}", org_id, fleet_key) +} + +fn org_credit_lines_from_rows(rows: Vec) -> HashMap { + rows.into_iter() + .map(|row| { + let mut credit_line = CreditLineSummary { + uid: row.uid.clone(), + name: row.name, + approved_amount: row.approved_amount, + available_amount: row.available_amount, + outstanding_principal: row.outstanding_principal, + interest_rate: row.interest_rate, + amount_due: row.amount_due, + amount: row.amount, + }; + credit_line.normalize(); + (row.uid, credit_line) + }) + .collect() +} + +fn org_member_uids(org_id: &str) -> Result, String> { + let rows = + surreal_select_by_field::("org_member", "org members", "org_id", org_id)?; + let mut uids = rows + .into_iter() + .map(|row| row.member_uid) + .filter(|uid| !uid.trim().is_empty()) + .collect::>(); + uids.sort(); + uids.dedup(); + Ok(uids) +} + +fn upsert_org_member(org_id: &str, member_uid: &str) -> Result<(), String> { + let row = OrgMemberRow { + org_id: org_id.to_string(), + member_uid: member_uid.to_string(), + }; + surreal_upsert( + "org_member", + &org_member_id(org_id, member_uid), + "org member", + &row, + ) +} + +fn org_assets_from_rows(rows: Vec) -> HashMap> { + let mut assets = HashMap::new(); + for row in rows { + let category_assets = assets + .entry(row.category.clone()) + .or_insert_with(HashMap::new); + category_assets.insert( + row.classname.clone(), + OrgAssetEntry { + classname: row.classname, + asset_type: row.asset_type, + quantity: row.quantity, + }, + ); + } + assets +} + +fn org_fleet_from_rows(rows: Vec) -> HashMap { + rows.into_iter() + .map(|row| { + ( + row.fleet_key, + OrgFleetEntry { + classname: row.classname, + name: row.name, + fleet_type: row.fleet_type, + status: row.status, + damage: row.damage, + }, + ) + }) + .collect() +} + +impl OrgRepository for SurrealOrgRepository { + fn create(&self, org: &Org) -> Result<(), String> { + self.update(org) + } + + fn get_by_id(&self, id: &str) -> Result, String> { + let Some(record) = surreal_select::("org", id, "org")? else { + return Ok(None); + }; + + let credit_line_rows = surreal_select_by_field::( + "org_credit_line", + "org credit lines", + "org_id", + id, + )?; + let credit_lines = org_credit_lines_from_rows(credit_line_rows); + + Ok(Some(record.into_org(id, credit_lines))) + } + + fn update(&self, org: &Org) -> Result<(), String> { + let record = SurrealOrgRecord::from(org); + surreal_upsert("org", org.id.as_str(), "org", &record)?; + surreal_delete_by_field("org_credit_line", "org credit lines", "org_id", &org.id)?; + + for (uid, credit_line) in &org.credit_lines { + let resolved_uid = if credit_line.uid.trim().is_empty() { + uid.clone() + } else { + credit_line.uid.clone() + }; + let mut normalized = credit_line.clone(); + normalized.uid = resolved_uid.clone(); + normalized.normalize(); + let row = OrgCreditLineRow { + org_id: org.id.clone(), + uid: resolved_uid.clone(), + name: normalized.name, + approved_amount: normalized.approved_amount, + available_amount: normalized.available_amount, + outstanding_principal: normalized.outstanding_principal, + interest_rate: normalized.interest_rate, + amount_due: normalized.amount_due, + amount: normalized.amount, + }; + surreal_upsert( + "org_credit_line", + &org_credit_line_id(&org.id, &resolved_uid), + "org credit line", + &row, + )?; + } + + Ok(()) + } + + fn delete(&self, id: &str) -> Result<(), String> { + surreal_delete::("org", id, "org")?; + surreal_delete_by_field("org_member", "org members", "org_id", id)?; + surreal_delete_by_field("org_credit_line", "org credit lines", "org_id", id)?; + surreal_delete_by_field("org_asset", "org assets", "org_id", id)?; + surreal_delete_by_field("org_fleet_vehicle", "org fleet", "org_id", id) + } + + fn exists(&self, id: &str) -> Result { + self.get_by_id(id).map(|org| org.is_some()) + } + + fn add_member(&self, org_id: &str, member_uid: &str) -> Result<(), String> { + if !self.exists(org_id)? { + return Err(format!("Organization {} does not exist", org_id)); + } + + let mut member_uids = org_member_uids(org_id)?; + if !member_uids.iter().any(|uid| uid == member_uid) { + member_uids.push(member_uid.to_string()); + } + surreal_delete_by_field("org_member", "org members", "org_id", org_id)?; + for uid in member_uids { + upsert_org_member(org_id, &uid)?; + } + Ok(()) + } + + fn get_members(&self, org_id: &str) -> Result, String> { + let member_uids = org_member_uids(org_id)?; + let mut members = Vec::with_capacity(member_uids.len()); + let actor_repository = SurrealActorRepository; + + for uid in member_uids { + if uid.trim().is_empty() { + continue; + } + + let name = match actor_repository.get_by_id(&uid)? { + Some(actor) => actor + .name + .filter(|name| !name.trim().is_empty()) + .unwrap_or_else(|| "Unknown".to_string()), + None => "Unknown".to_string(), + }; + + members.push(MemberSummary { uid, name }); + } + + Ok(members) + } + + fn remove_member(&self, org_id: &str, member_uid: &str) -> Result<(), String> { + let mut member_uids = org_member_uids(org_id)?; + member_uids.retain(|uid| uid != member_uid); + surreal_delete_by_field("org_member", "org members", "org_id", org_id)?; + for uid in member_uids { + upsert_org_member(org_id, &uid)?; + } + Ok(()) + } + + fn get_assets( + &self, + org_id: &str, + ) -> Result>, String> { + let rows = + surreal_select_by_field::("org_asset", "org assets", "org_id", org_id)?; + Ok(org_assets_from_rows(rows)) + } + + fn update_assets( + &self, + org_id: &str, + assets: &HashMap>, + ) -> Result<(), String> { + surreal_delete_by_field("org_asset", "org assets", "org_id", org_id)?; + + for (category, category_assets) in assets { + for (classname, asset) in category_assets { + let row = OrgAssetRow { + org_id: org_id.to_string(), + category: category.clone(), + classname: if asset.classname.trim().is_empty() { + classname.clone() + } else { + asset.classname.clone() + }, + asset_type: asset.asset_type.clone(), + quantity: asset.quantity, + }; + surreal_upsert( + "org_asset", + &org_asset_id(org_id, category, &row.classname), + "org asset", + &row, + )?; + } + } + + Ok(()) + } + + fn get_fleet(&self, org_id: &str) -> Result, String> { + let rows = surreal_select_by_field::( + "org_fleet_vehicle", + "org fleet", + "org_id", + org_id, + )?; + if !rows.is_empty() { + return Ok(org_fleet_from_rows(rows)); + } + Ok(HashMap::new()) + } + + fn update_fleet( + &self, + org_id: &str, + fleet: &HashMap, + ) -> Result<(), String> { + surreal_delete_by_field("org_fleet_vehicle", "org fleet", "org_id", org_id)?; + + for (fleet_key, entry) in fleet { + let row = OrgFleetRow { + org_id: org_id.to_string(), + fleet_key: fleet_key.clone(), + classname: entry.classname.clone(), + name: entry.name.clone(), + fleet_type: entry.fleet_type.clone(), + status: entry.status.clone(), + damage: entry.damage.clone(), + }; + surreal_upsert( + "org_fleet_vehicle", + &org_fleet_id(org_id, fleet_key), + "org fleet", + &row, + )?; + } + + Ok(()) + } +} diff --git a/arma/server/extension/src/storage/phone.rs b/arma/server/extension/src/storage/phone.rs new file mode 100644 index 0000000..68f6461 --- /dev/null +++ b/arma/server/extension/src/storage/phone.rs @@ -0,0 +1,536 @@ +use super::common::*; +use super::*; + +pub enum PhoneStorageRepository { + Redis(RedisPhoneRepository), + Surreal(SurrealPhoneRepository), +} + +impl PhoneStorageRepository { + pub fn configured() -> Self { + match load().storage.backend { + StorageBackend::Surreal => Self::Surreal(SurrealPhoneRepository), + StorageBackend::Redis => { + Self::Redis(RedisPhoneRepository::new(ExtensionRedisClient::new())) + } + } + } +} + +impl PhoneRepository for PhoneStorageRepository { + fn init(&self, uid: &str) -> Result<(), String> { + match self { + Self::Redis(repository) => repository.init(uid), + Self::Surreal(repository) => repository.init(uid), + } + } + + fn add_contact(&self, uid: &str, contact_uid: &str) -> Result { + match self { + Self::Redis(repository) => repository.add_contact(uid, contact_uid), + Self::Surreal(repository) => repository.add_contact(uid, contact_uid), + } + } + + fn remove_contact(&self, uid: &str, contact_uid: &str) -> Result { + match self { + Self::Redis(repository) => repository.remove_contact(uid, contact_uid), + Self::Surreal(repository) => repository.remove_contact(uid, contact_uid), + } + } + + fn list_contacts(&self, uid: &str) -> Result, String> { + match self { + Self::Redis(repository) => repository.list_contacts(uid), + Self::Surreal(repository) => repository.list_contacts(uid), + } + } + + fn remove_phone(&self, uid: &str) -> Result<(), String> { + match self { + Self::Redis(repository) => repository.remove_phone(uid), + Self::Surreal(repository) => repository.remove_phone(uid), + } + } + + fn append_message(&self, uid: &str, message: PhoneMessage) -> Result<(), String> { + match self { + Self::Redis(repository) => repository.append_message(uid, message), + Self::Surreal(repository) => repository.append_message(uid, message), + } + } + + fn list_messages(&self, uid: &str) -> Result, String> { + match self { + Self::Redis(repository) => repository.list_messages(uid), + Self::Surreal(repository) => repository.list_messages(uid), + } + } + + fn mark_message_read(&self, uid: &str, message_id: &str) -> Result { + match self { + Self::Redis(repository) => repository.mark_message_read(uid, message_id), + Self::Surreal(repository) => repository.mark_message_read(uid, message_id), + } + } + + fn delete_message(&self, uid: &str, message_id: &str) -> Result { + match self { + Self::Redis(repository) => repository.delete_message(uid, message_id), + Self::Surreal(repository) => repository.delete_message(uid, message_id), + } + } + + fn append_email(&self, uid: &str, email: PhoneEmail) -> Result<(), String> { + match self { + Self::Redis(repository) => repository.append_email(uid, email), + Self::Surreal(repository) => repository.append_email(uid, email), + } + } + + fn list_emails(&self, uid: &str) -> Result, String> { + match self { + Self::Redis(repository) => repository.list_emails(uid), + Self::Surreal(repository) => repository.list_emails(uid), + } + } + + fn mark_email_read(&self, uid: &str, email_id: &str) -> Result { + match self { + Self::Redis(repository) => repository.mark_email_read(uid, email_id), + Self::Surreal(repository) => repository.mark_email_read(uid, email_id), + } + } + + fn delete_email(&self, uid: &str, email_id: &str) -> Result { + match self { + Self::Redis(repository) => repository.delete_email(uid, email_id), + Self::Surreal(repository) => repository.delete_email(uid, email_id), + } + } + + fn next_sequence(&self) -> Result { + match self { + Self::Redis(repository) => repository.next_sequence(), + Self::Surreal(repository) => repository.next_sequence(), + } + } +} + +#[derive(Debug, Clone, Serialize, Deserialize)] +struct PhoneUserRecord { + uid: String, +} + +impl PhoneUserRecord { + fn new(uid: &str) -> Self { + Self { + uid: uid.to_string(), + } + } +} + +#[derive(Debug, Clone, Serialize, Deserialize)] +struct PhoneContactRecord { + uid: String, + contact_uid: String, +} + +#[derive(Debug, Clone, Serialize, Deserialize)] +struct PhoneMessageIndexRecord { + uid: String, + message_id: String, + is_read: bool, +} + +#[derive(Debug, Clone, Serialize, Deserialize)] +struct PhoneEmailIndexRecord { + uid: String, + email_id: String, + is_read: bool, +} + +#[derive(Debug, Clone, Serialize, Deserialize)] +struct PhoneMessageRecord { + message_id: String, + from_uid: String, + to_uid: String, + message: String, + timestamp: f64, +} + +impl PhoneMessageRecord { + fn into_message(self, read: bool) -> PhoneMessage { + PhoneMessage { + id: self.message_id, + from: self.from_uid, + to: self.to_uid, + message: self.message, + timestamp: self.timestamp, + read, + } + } +} + +impl From<&PhoneMessage> for PhoneMessageRecord { + fn from(message: &PhoneMessage) -> Self { + Self { + message_id: message.id.clone(), + from_uid: message.from.clone(), + to_uid: message.to.clone(), + message: message.message.clone(), + timestamp: message.timestamp, + } + } +} + +#[derive(Debug, Clone, Serialize, Deserialize)] +struct PhoneEmailRecord { + email_id: String, + from_uid: String, + to_uid: String, + subject: String, + body: String, + timestamp: f64, +} + +impl PhoneEmailRecord { + fn into_email(self, read: bool) -> PhoneEmail { + PhoneEmail { + id: self.email_id, + from: self.from_uid, + to: self.to_uid, + subject: self.subject, + body: self.body, + timestamp: self.timestamp, + read, + } + } +} + +impl From<&PhoneEmail> for PhoneEmailRecord { + fn from(email: &PhoneEmail) -> Self { + Self { + email_id: email.id.clone(), + from_uid: email.from.clone(), + to_uid: email.to.clone(), + subject: email.subject.clone(), + body: email.body.clone(), + timestamp: email.timestamp, + } + } +} + +#[derive(Debug, Default, Clone, Serialize, Deserialize)] +struct PhoneSequenceRecord { + #[serde(default)] + sequence_id: String, + #[serde(default)] + value: u64, +} + +pub struct SurrealPhoneRepository; + +impl SurrealPhoneRepository { + fn save_user(&self, uid: &str) -> Result<(), String> { + let record = PhoneUserRecord::new(uid); + surreal_upsert("phone_user", uid, "phone user", &record) + } + + fn message_is_referenced(&self, message_id: &str) -> Result { + Ok(!surreal_select_by_field::( + "phone_message_index", + "phone message indexes", + "message_id", + message_id, + )? + .is_empty()) + } + + fn email_is_referenced(&self, email_id: &str) -> Result { + Ok(!surreal_select_by_field::( + "phone_email_index", + "phone email indexes", + "email_id", + email_id, + )? + .is_empty()) + } + + fn cleanup_orphaned_records(&self) -> Result<(), String> { + let referenced_messages = surreal_select_all::( + "phone_message_index", + "phone message indexes", + )? + .into_iter() + .map(|record| record.message_id) + .collect::>(); + let referenced_emails = surreal_select_all::( + "phone_email_index", + "phone email indexes", + )? + .into_iter() + .map(|record| record.email_id) + .collect::>(); + + for record in surreal_select_all::("phone_message", "phone messages")? { + let message_id = record.message_id.trim(); + if !message_id.is_empty() && !referenced_messages.contains(message_id) { + surreal_delete::("phone_message", message_id, "phone message")?; + } + } + + for record in surreal_select_all::("phone_email", "phone emails")? { + let email_id = record.email_id.trim(); + if !email_id.is_empty() && !referenced_emails.contains(email_id) { + surreal_delete::("phone_email", email_id, "phone email")?; + } + } + + Ok(()) + } + + fn contact_id(uid: &str, contact_uid: &str) -> String { + format!("{}:{}", uid, contact_uid) + } + + fn message_index_id(uid: &str, message_id: &str) -> String { + format!("{}:{}", uid, message_id) + } + + fn email_index_id(uid: &str, email_id: &str) -> String { + format!("{}:{}", uid, email_id) + } +} + +impl PhoneRepository for SurrealPhoneRepository { + fn init(&self, uid: &str) -> Result<(), String> { + if surreal_select::("phone_user", uid, "phone user")?.is_none() { + self.save_user(uid)?; + } + self.cleanup_orphaned_records()?; + Ok(()) + } + + fn add_contact(&self, uid: &str, contact_uid: &str) -> Result { + self.save_user(uid)?; + let record = PhoneContactRecord { + uid: uid.to_string(), + contact_uid: contact_uid.to_string(), + }; + surreal_upsert( + "phone_contact", + &Self::contact_id(uid, contact_uid), + "phone contact", + &record, + )?; + Ok(true) + } + + fn remove_contact(&self, uid: &str, contact_uid: &str) -> Result { + let id = Self::contact_id(uid, contact_uid); + let exists = + surreal_select::("phone_contact", &id, "phone contact")?.is_some(); + if !exists { + return Ok(false); + } + + surreal_delete::("phone_contact", &id, "phone contact")?; + Ok(true) + } + + fn list_contacts(&self, uid: &str) -> Result, String> { + let mut contacts = + surreal_select_by_uid::("phone_contact", "phone contacts", uid)? + .into_iter() + .map(|record| record.contact_uid) + .collect::>(); + contacts.sort(); + contacts.dedup(); + Ok(contacts) + } + + fn remove_phone(&self, uid: &str) -> Result<(), String> { + surreal_delete_by_uid("phone_contact", "phone contacts", uid)?; + surreal_delete_by_uid("phone_message_index", "phone message indexes", uid)?; + surreal_delete_by_uid("phone_email_index", "phone email indexes", uid)?; + surreal_delete::("phone_user", uid, "phone user")?; + self.cleanup_orphaned_records() + } + + fn append_message(&self, uid: &str, message: PhoneMessage) -> Result<(), String> { + self.save_user(uid)?; + + let record = PhoneMessageRecord::from(&message); + surreal_upsert("phone_message", &message.id, "phone message", &record)?; + let index = PhoneMessageIndexRecord { + uid: uid.to_string(), + message_id: message.id.clone(), + is_read: message.from == uid, + }; + surreal_upsert( + "phone_message_index", + &Self::message_index_id(uid, &message.id), + "phone message index", + &index, + ) + } + + fn list_messages(&self, uid: &str) -> Result, String> { + let indexes = surreal_select_by_uid::( + "phone_message_index", + "phone message indexes", + uid, + )?; + let mut messages = Vec::with_capacity(indexes.len()); + + for index in indexes { + if index.message_id.trim().is_empty() { + continue; + } + + if let Some(record) = surreal_select::( + "phone_message", + &index.message_id, + "phone message", + )? { + messages.push(record.into_message(index.is_read)); + } + } + + messages.sort_by(|left, right| { + left.timestamp + .partial_cmp(&right.timestamp) + .unwrap_or(std::cmp::Ordering::Equal) + }); + Ok(messages) + } + + fn mark_message_read(&self, uid: &str, message_id: &str) -> Result { + let id = Self::message_index_id(uid, message_id); + let Some(mut index) = surreal_select::( + "phone_message_index", + &id, + "phone message index", + )? + else { + return Ok(false); + }; + + index.is_read = true; + surreal_upsert("phone_message_index", &id, "phone message index", &index)?; + Ok(true) + } + + fn delete_message(&self, uid: &str, message_id: &str) -> Result { + let id = Self::message_index_id(uid, message_id); + let exists = surreal_select::( + "phone_message_index", + &id, + "phone message index", + )? + .is_some(); + if !exists { + return Ok(false); + } + + surreal_delete::( + "phone_message_index", + &id, + "phone message index", + )?; + if !self.message_is_referenced(message_id)? { + surreal_delete::("phone_message", message_id, "phone message")?; + } + Ok(true) + } + + fn append_email(&self, uid: &str, email: PhoneEmail) -> Result<(), String> { + self.save_user(uid)?; + + let record = PhoneEmailRecord::from(&email); + surreal_upsert("phone_email", &email.id, "phone email", &record)?; + let index = PhoneEmailIndexRecord { + uid: uid.to_string(), + email_id: email.id.clone(), + is_read: false, + }; + surreal_upsert( + "phone_email_index", + &Self::email_index_id(uid, &email.id), + "phone email index", + &index, + ) + } + + fn list_emails(&self, uid: &str) -> Result, String> { + let indexes = surreal_select_by_uid::( + "phone_email_index", + "phone email indexes", + uid, + )?; + let mut emails = Vec::with_capacity(indexes.len()); + + for index in indexes { + if index.email_id.trim().is_empty() { + continue; + } + + if let Some(record) = + surreal_select::("phone_email", &index.email_id, "phone email")? + { + emails.push(record.into_email(index.is_read)); + } + } + + emails.sort_by(|left, right| { + right + .timestamp + .partial_cmp(&left.timestamp) + .unwrap_or(std::cmp::Ordering::Equal) + }); + Ok(emails) + } + + fn mark_email_read(&self, uid: &str, email_id: &str) -> Result { + let id = Self::email_index_id(uid, email_id); + let Some(mut index) = + surreal_select::("phone_email_index", &id, "phone email index")? + else { + return Ok(false); + }; + + index.is_read = true; + surreal_upsert("phone_email_index", &id, "phone email index", &index)?; + Ok(true) + } + + fn delete_email(&self, uid: &str, email_id: &str) -> Result { + let id = Self::email_index_id(uid, email_id); + let exists = + surreal_select::("phone_email_index", &id, "phone email index")? + .is_some(); + if !exists { + return Ok(false); + } + + surreal_delete::("phone_email_index", &id, "phone email index")?; + if !self.email_is_referenced(email_id)? { + surreal_delete::("phone_email", email_id, "phone email")?; + } + Ok(true) + } + + fn next_sequence(&self) -> Result { + let mut record = + surreal_select::("phone_sequence", "global", "phone sequence")? + .unwrap_or_default(); + record.sequence_id = "global".to_string(); + record.value = record + .value + .checked_add(1) + .ok_or_else(|| "Phone sequence overflowed.".to_string())?; + surreal_upsert("phone_sequence", "global", "phone sequence", &record)?; + Ok(record.value) + } +} diff --git a/arma/server/extension/src/surreal.rs b/arma/server/extension/src/surreal.rs index 6faf422..6cb1aac 100644 --- a/arma/server/extension/src/surreal.rs +++ b/arma/server/extension/src/surreal.rs @@ -9,6 +9,7 @@ use tokio::time::{Duration, sleep, timeout}; use crate::log; use crate::redis::config::SurrealConfig; +use crate::schema; pub type SurrealDb = Surreal; @@ -120,6 +121,8 @@ async fn connect(config: SurrealConfig) -> Result { .await .map_err(|error| error.to_string())?; + schema::apply_all(&db).await?; + Ok(db) } diff --git a/arma/server/extension/src/transport.rs b/arma/server/extension/src/transport.rs index 17ce310..1ee2749 100644 --- a/arma/server/extension/src/transport.rs +++ b/arma/server/extension/src/transport.rs @@ -10,11 +10,12 @@ use std::collections::HashMap; use std::sync::atomic::{AtomicU64, Ordering}; use std::sync::{LazyLock, Mutex as StdMutex}; -use crate::{actor, bank, cad, garage, locker, org, phone, v_garage, v_locker}; +mod routes; + +use routes::route_command; const CHUNK_PREFIX: &str = "FORGE_TRANSPORT_CHUNK:"; const RESPONSE_CHUNK_SIZE: usize = 12_000; -const UNSUPPORTED_ROUTE_PREFIX: &str = "Unsupported transport route"; static REQUEST_STORE: LazyLock>> = LazyLock::new(|| StdMutex::new(HashMap::new())); @@ -143,903 +144,6 @@ fn parse_transport_argument_value(value: serde_json::Value) -> Result, -) -> Result { - match function_name { - "actor:get" => { - expect_arg_count(function_name, &arguments, 1)?; - Ok(actor::get_actor(call_context, arguments[0].clone())) - } - "actor:create" => { - expect_arg_count(function_name, &arguments, 2)?; - Ok(actor::create_actor( - call_context, - arguments[0].clone(), - arguments[1].clone(), - )) - } - "actor:update" => { - expect_arg_count(function_name, &arguments, 2)?; - Ok(actor::update_actor( - call_context, - arguments[0].clone(), - arguments[1].clone(), - )) - } - "actor:exists" => { - expect_arg_count(function_name, &arguments, 1)?; - Ok(actor::actor_exists(call_context, arguments[0].clone())) - } - "actor:delete" => { - expect_arg_count(function_name, &arguments, 1)?; - Ok(actor::delete_actor(call_context, arguments[0].clone())) - } - "actor:hot:init" => { - expect_arg_count(function_name, &arguments, 1)?; - Ok(actor::init_hot_actor(call_context, arguments[0].clone())) - } - "actor:hot:get" => { - expect_arg_count(function_name, &arguments, 1)?; - Ok(actor::get_hot_actor(call_context, arguments[0].clone())) - } - "actor:hot:keys" => { - expect_arg_count(function_name, &arguments, 0)?; - Ok(actor::list_hot_actor_keys()) - } - "actor:hot:override" => { - expect_arg_count(function_name, &arguments, 2)?; - Ok(actor::override_hot_actor( - call_context, - arguments[0].clone(), - arguments[1].clone(), - )) - } - "actor:hot:save" => { - expect_arg_count(function_name, &arguments, 1)?; - Ok(actor::save_hot_actor(call_context, arguments[0].clone())) - } - "actor:hot:remove" => { - expect_arg_count(function_name, &arguments, 1)?; - Ok(actor::remove_hot_actor(call_context, arguments[0].clone())) - } - "bank:get" => { - expect_arg_count(function_name, &arguments, 1)?; - Ok(bank::get_bank(call_context, arguments[0].clone())) - } - "bank:create" => { - expect_arg_count(function_name, &arguments, 2)?; - Ok(bank::create_bank( - call_context, - arguments[0].clone(), - arguments[1].clone(), - )) - } - "bank:update" => { - expect_arg_count(function_name, &arguments, 2)?; - Ok(bank::update_bank( - call_context, - arguments[0].clone(), - arguments[1].clone(), - )) - } - "bank:exists" => { - expect_arg_count(function_name, &arguments, 1)?; - Ok(bank::bank_exists(call_context, arguments[0].clone())) - } - "bank:delete" => { - expect_arg_count(function_name, &arguments, 1)?; - Ok(bank::delete_bank(call_context, arguments[0].clone())) - } - "bank:hot:init" => { - expect_arg_count(function_name, &arguments, 1)?; - Ok(bank::init_hot_bank(call_context, arguments[0].clone())) - } - "bank:hot:get" => { - expect_arg_count(function_name, &arguments, 1)?; - Ok(bank::get_hot_bank(call_context, arguments[0].clone())) - } - "bank:hot:override" => { - expect_arg_count(function_name, &arguments, 2)?; - Ok(bank::override_hot_bank( - call_context, - arguments[0].clone(), - arguments[1].clone(), - )) - } - "bank:hot:patch" => { - expect_arg_count(function_name, &arguments, 2)?; - Ok(bank::patch_hot_bank( - call_context, - arguments[0].clone(), - arguments[1].clone(), - )) - } - "bank:hot:charge_checkout" => { - expect_arg_count(function_name, &arguments, 3)?; - Ok(bank::charge_checkout_hot_bank( - call_context, - arguments[0].clone(), - arguments[1].clone(), - arguments[2].clone(), - )) - } - "bank:hot:deposit" => { - expect_arg_count(function_name, &arguments, 3)?; - Ok(bank::deposit_hot_bank( - call_context, - arguments[0].clone(), - arguments[1].clone(), - arguments[2].clone(), - )) - } - "bank:hot:withdraw" => { - expect_arg_count(function_name, &arguments, 3)?; - Ok(bank::withdraw_hot_bank( - call_context, - arguments[0].clone(), - arguments[1].clone(), - arguments[2].clone(), - )) - } - "bank:hot:deposit_earnings" => { - expect_arg_count(function_name, &arguments, 3)?; - Ok(bank::deposit_earnings_hot_bank( - call_context, - arguments[0].clone(), - arguments[1].clone(), - arguments[2].clone(), - )) - } - "bank:hot:transfer" => { - expect_arg_count(function_name, &arguments, 4)?; - Ok(bank::transfer_hot_bank( - call_context, - arguments[0].clone(), - arguments[1].clone(), - arguments[2].clone(), - arguments[3].clone(), - )) - } - "bank:hot:validate_pin" => { - expect_arg_count(function_name, &arguments, 3)?; - Ok(bank::validate_pin_hot_bank( - call_context, - arguments[0].clone(), - arguments[1].clone(), - arguments[2].clone(), - )) - } - "bank:hot:save" => { - expect_arg_count(function_name, &arguments, 1)?; - Ok(bank::save_hot_bank(call_context, arguments[0].clone())) - } - "bank:hot:remove" => { - expect_arg_count(function_name, &arguments, 1)?; - Ok(bank::remove_hot_bank(call_context, arguments[0].clone())) - } - "org:get" => { - expect_arg_count(function_name, &arguments, 1)?; - Ok(org::get_org(arguments[0].clone())) - } - "org:create" => { - expect_arg_count(function_name, &arguments, 2)?; - Ok(org::create_org(arguments[0].clone(), arguments[1].clone())) - } - "org:update" => { - expect_arg_count(function_name, &arguments, 2)?; - Ok(org::update_org(arguments[0].clone(), arguments[1].clone())) - } - "org:exists" => { - expect_arg_count(function_name, &arguments, 1)?; - Ok(org::org_exists(arguments[0].clone())) - } - "org:delete" => { - expect_arg_count(function_name, &arguments, 1)?; - Ok(org::delete_org(arguments[0].clone())) - } - "org:hot:init" => { - expect_arg_count(function_name, &arguments, 1)?; - Ok(org::init_hot_org(arguments[0].clone())) - } - "org:hot:get" => { - expect_arg_count(function_name, &arguments, 1)?; - Ok(org::get_hot_org(arguments[0].clone())) - } - "org:hot:override" => { - expect_arg_count(function_name, &arguments, 2)?; - Ok(org::override_hot_org( - arguments[0].clone(), - arguments[1].clone(), - )) - } - "org:hot:ensure_member" => { - expect_arg_count(function_name, &arguments, 1)?; - Ok(org::ensure_hot_org_member(arguments[0].clone())) - } - "org:hot:member_invites" => { - expect_arg_count(function_name, &arguments, 1)?; - Ok(org::get_hot_org_member_invites(arguments[0].clone())) - } - "org:hot:register" => { - expect_arg_count(function_name, &arguments, 1)?; - Ok(org::register_hot_org(arguments[0].clone())) - } - "org:hot:invite_member" => { - expect_arg_count(function_name, &arguments, 1)?; - Ok(org::invite_hot_org_member(arguments[0].clone())) - } - "org:hot:accept_invite" => { - expect_arg_count(function_name, &arguments, 1)?; - Ok(org::accept_hot_org_invite(arguments[0].clone())) - } - "org:hot:decline_invite" => { - expect_arg_count(function_name, &arguments, 1)?; - Ok(org::decline_hot_org_invite(arguments[0].clone())) - } - "org:hot:assign_credit_line" => { - expect_arg_count(function_name, &arguments, 1)?; - Ok(org::assign_credit_line_hot_org(arguments[0].clone())) - } - "org:hot:repay_credit_line" => { - expect_arg_count(function_name, &arguments, 1)?; - Ok(org::repay_credit_line_hot_org(arguments[0].clone())) - } - "org:hot:charge_checkout" => { - expect_arg_count(function_name, &arguments, 1)?; - Ok(org::charge_checkout_hot_org(arguments[0].clone())) - } - "org:hot:add_assets" => { - expect_arg_count(function_name, &arguments, 2)?; - Ok(org::add_assets_hot_org( - arguments[0].clone(), - arguments[1].clone(), - )) - } - "org:hot:add_fleet" => { - expect_arg_count(function_name, &arguments, 2)?; - Ok(org::add_fleet_hot_org( - arguments[0].clone(), - arguments[1].clone(), - )) - } - "org:hot:leave" => { - expect_arg_count(function_name, &arguments, 1)?; - Ok(org::leave_hot_org(arguments[0].clone())) - } - "org:hot:disband" => { - expect_arg_count(function_name, &arguments, 1)?; - Ok(org::disband_hot_org(arguments[0].clone())) - } - "org:hot:save" => { - expect_arg_count(function_name, &arguments, 1)?; - Ok(org::save_hot_org(arguments[0].clone())) - } - "org:hot:remove" => { - expect_arg_count(function_name, &arguments, 1)?; - Ok(org::remove_hot_org(arguments[0].clone())) - } - "org:assets:get" => { - expect_arg_count(function_name, &arguments, 1)?; - Ok(org::get_assets(arguments[0].clone())) - } - "org:assets:update" => { - expect_arg_count(function_name, &arguments, 2)?; - Ok(org::update_assets( - arguments[0].clone(), - arguments[1].clone(), - )) - } - "org:fleet:get" => { - expect_arg_count(function_name, &arguments, 1)?; - Ok(org::get_fleet(arguments[0].clone())) - } - "org:fleet:update" => { - expect_arg_count(function_name, &arguments, 2)?; - Ok(org::update_fleet( - arguments[0].clone(), - arguments[1].clone(), - )) - } - "org:members:get" => { - expect_arg_count(function_name, &arguments, 1)?; - Ok(org::get_members(arguments[0].clone())) - } - "org:members:add" => { - expect_arg_count(function_name, &arguments, 2)?; - Ok(org::add_member(arguments[0].clone(), arguments[1].clone())) - } - "org:members:remove" => { - expect_arg_count(function_name, &arguments, 2)?; - Ok(org::remove_member( - arguments[0].clone(), - arguments[1].clone(), - )) - } - "store:checkout" => { - expect_arg_count(function_name, &arguments, 1)?; - Ok(crate::store::checkout(arguments[0].clone())) - } - "garage:create" => { - expect_arg_count(function_name, &arguments, 1)?; - Ok(garage::create_garage(call_context, arguments[0].clone())) - } - "garage:get" => { - expect_arg_count(function_name, &arguments, 1)?; - Ok(garage::get_garage(call_context, arguments[0].clone())) - } - "garage:add" => { - expect_arg_count(function_name, &arguments, 2)?; - Ok(garage::add_vehicle( - call_context, - arguments[0].clone(), - arguments[1].clone(), - )) - } - "garage:update" => { - expect_arg_count(function_name, &arguments, 2)?; - Ok(garage::update_garage( - call_context, - arguments[0].clone(), - arguments[1].clone(), - )) - } - "garage:patch" => { - expect_arg_count(function_name, &arguments, 2)?; - Ok(garage::patch_vehicle( - call_context, - arguments[0].clone(), - arguments[1].clone(), - )) - } - "garage:remove" => { - expect_arg_count(function_name, &arguments, 2)?; - Ok(garage::remove_vehicle( - call_context, - arguments[0].clone(), - arguments[1].clone(), - )) - } - "garage:delete" => { - expect_arg_count(function_name, &arguments, 1)?; - Ok(garage::delete_garage(call_context, arguments[0].clone())) - } - "garage:exists" => { - expect_arg_count(function_name, &arguments, 1)?; - Ok(garage::garage_exists(call_context, arguments[0].clone())) - } - "garage:hot:init" => { - expect_arg_count(function_name, &arguments, 1)?; - Ok(garage::init_hot_garage(call_context, arguments[0].clone())) - } - "garage:hot:get" => { - expect_arg_count(function_name, &arguments, 1)?; - Ok(garage::get_hot_garage(call_context, arguments[0].clone())) - } - "garage:hot:override" => { - expect_arg_count(function_name, &arguments, 2)?; - Ok(garage::override_hot_garage( - call_context, - arguments[0].clone(), - arguments[1].clone(), - )) - } - "garage:hot:save" => { - expect_arg_count(function_name, &arguments, 1)?; - Ok(garage::save_hot_garage(call_context, arguments[0].clone())) - } - "garage:hot:remove" => { - expect_arg_count(function_name, &arguments, 1)?; - Ok(garage::remove_hot_garage( - call_context, - arguments[0].clone(), - )) - } - "garage:hot:add" => { - expect_arg_count(function_name, &arguments, 2)?; - Ok(garage::add_hot_vehicle( - call_context, - arguments[0].clone(), - arguments[1].clone(), - )) - } - "garage:hot:remove_vehicle" => { - expect_arg_count(function_name, &arguments, 2)?; - Ok(garage::remove_hot_vehicle( - call_context, - arguments[0].clone(), - arguments[1].clone(), - )) - } - "locker:create" => { - expect_arg_count(function_name, &arguments, 1)?; - Ok(locker::create_locker(call_context, arguments[0].clone())) - } - "locker:get" => { - expect_arg_count(function_name, &arguments, 1)?; - Ok(locker::get_locker(call_context, arguments[0].clone())) - } - "locker:add" => { - expect_arg_count(function_name, &arguments, 2)?; - Ok(locker::add_item( - call_context, - arguments[0].clone(), - arguments[1].clone(), - )) - } - "locker:update" => { - expect_arg_count(function_name, &arguments, 2)?; - Ok(locker::update_locker( - call_context, - arguments[0].clone(), - arguments[1].clone(), - )) - } - "locker:patch" => { - expect_arg_count(function_name, &arguments, 2)?; - Ok(locker::patch_item( - call_context, - arguments[0].clone(), - arguments[1].clone(), - )) - } - "locker:remove" => { - expect_arg_count(function_name, &arguments, 2)?; - Ok(locker::remove_item( - call_context, - arguments[0].clone(), - arguments[1].clone(), - )) - } - "locker:delete" => { - expect_arg_count(function_name, &arguments, 1)?; - Ok(locker::delete_locker(call_context, arguments[0].clone())) - } - "locker:exists" => { - expect_arg_count(function_name, &arguments, 1)?; - Ok(locker::locker_exists(call_context, arguments[0].clone())) - } - "locker:hot:init" => { - expect_arg_count(function_name, &arguments, 1)?; - Ok(locker::init_hot_locker(call_context, arguments[0].clone())) - } - "locker:hot:get" => { - expect_arg_count(function_name, &arguments, 1)?; - Ok(locker::get_hot_locker(call_context, arguments[0].clone())) - } - "locker:hot:override" => { - expect_arg_count(function_name, &arguments, 2)?; - Ok(locker::override_hot_locker( - call_context, - arguments[0].clone(), - arguments[1].clone(), - )) - } - "locker:hot:save" => { - expect_arg_count(function_name, &arguments, 1)?; - Ok(locker::save_hot_locker(call_context, arguments[0].clone())) - } - "locker:hot:remove" => { - expect_arg_count(function_name, &arguments, 1)?; - Ok(locker::remove_hot_locker( - call_context, - arguments[0].clone(), - )) - } - "owned:garage:create" => { - expect_arg_count(function_name, &arguments, 1)?; - Ok(v_garage::create_vgarage(call_context, arguments[0].clone())) - } - "owned:garage:fetch" => { - expect_arg_count(function_name, &arguments, 1)?; - Ok(v_garage::fetch_vgarage(call_context, arguments[0].clone())) - } - "owned:garage:get" => { - expect_arg_count(function_name, &arguments, 2)?; - Ok(v_garage::get_vgarage( - call_context, - arguments[0].clone(), - arguments[1].clone(), - )) - } - "owned:garage:add" => { - expect_arg_count(function_name, &arguments, 3)?; - Ok(v_garage::add_vgarage( - call_context, - arguments[0].clone(), - arguments[1].clone(), - arguments[2].clone(), - )) - } - "owned:garage:remove" => { - expect_arg_count(function_name, &arguments, 3)?; - Ok(v_garage::remove_vgarage( - call_context, - arguments[0].clone(), - arguments[1].clone(), - arguments[2].clone(), - )) - } - "owned:garage:delete" => { - expect_arg_count(function_name, &arguments, 1)?; - Ok(v_garage::delete_vgarage(call_context, arguments[0].clone())) - } - "owned:garage:exists" => { - expect_arg_count(function_name, &arguments, 1)?; - Ok(v_garage::vgarage_exists(call_context, arguments[0].clone())) - } - "owned:garage:hot:init" => { - expect_arg_count(function_name, &arguments, 1)?; - Ok(v_garage::init_hot_vgarage( - call_context, - arguments[0].clone(), - )) - } - "owned:garage:hot:fetch" => { - expect_arg_count(function_name, &arguments, 1)?; - Ok(v_garage::fetch_hot_vgarage( - call_context, - arguments[0].clone(), - )) - } - "owned:garage:hot:get" => { - expect_arg_count(function_name, &arguments, 2)?; - Ok(v_garage::get_hot_vgarage( - call_context, - arguments[0].clone(), - arguments[1].clone(), - )) - } - "owned:garage:hot:override" => { - expect_arg_count(function_name, &arguments, 2)?; - Ok(v_garage::override_hot_vgarage( - call_context, - arguments[0].clone(), - arguments[1].clone(), - )) - } - "owned:garage:hot:save" => { - expect_arg_count(function_name, &arguments, 1)?; - Ok(v_garage::save_hot_vgarage( - call_context, - arguments[0].clone(), - )) - } - "owned:garage:hot:remove" => { - expect_arg_count(function_name, &arguments, 1)?; - Ok(v_garage::remove_hot_vgarage( - call_context, - arguments[0].clone(), - )) - } - "owned:garage:hot:add" => { - expect_arg_count(function_name, &arguments, 3)?; - Ok(v_garage::add_hot_vgarage( - call_context, - arguments[0].clone(), - arguments[1].clone(), - arguments[2].clone(), - )) - } - "owned:garage:hot:remove_item" => { - expect_arg_count(function_name, &arguments, 3)?; - Ok(v_garage::remove_hot_vgarage_item( - call_context, - arguments[0].clone(), - arguments[1].clone(), - arguments[2].clone(), - )) - } - "owned:locker:create" => { - expect_arg_count(function_name, &arguments, 1)?; - Ok(v_locker::create_vlocker(call_context, arguments[0].clone())) - } - "owned:locker:fetch" => { - expect_arg_count(function_name, &arguments, 1)?; - Ok(v_locker::fetch_vlocker(call_context, arguments[0].clone())) - } - "owned:locker:get" => { - expect_arg_count(function_name, &arguments, 2)?; - Ok(v_locker::get_vlocker( - call_context, - arguments[0].clone(), - arguments[1].clone(), - )) - } - "owned:locker:add" => { - expect_arg_count(function_name, &arguments, 3)?; - Ok(v_locker::add_vlocker( - call_context, - arguments[0].clone(), - arguments[1].clone(), - arguments[2].clone(), - )) - } - "owned:locker:remove" => { - expect_arg_count(function_name, &arguments, 3)?; - Ok(v_locker::remove_vlocker( - call_context, - arguments[0].clone(), - arguments[1].clone(), - arguments[2].clone(), - )) - } - "owned:locker:delete" => { - expect_arg_count(function_name, &arguments, 1)?; - Ok(v_locker::delete_vlocker(call_context, arguments[0].clone())) - } - "owned:locker:exists" => { - expect_arg_count(function_name, &arguments, 1)?; - Ok(v_locker::vlocker_exists(call_context, arguments[0].clone())) - } - "owned:locker:hot:init" => { - expect_arg_count(function_name, &arguments, 1)?; - Ok(v_locker::init_hot_vlocker( - call_context, - arguments[0].clone(), - )) - } - "owned:locker:hot:fetch" => { - expect_arg_count(function_name, &arguments, 1)?; - Ok(v_locker::fetch_hot_vlocker( - call_context, - arguments[0].clone(), - )) - } - "owned:locker:hot:get" => { - expect_arg_count(function_name, &arguments, 2)?; - Ok(v_locker::get_hot_vlocker( - call_context, - arguments[0].clone(), - arguments[1].clone(), - )) - } - "owned:locker:hot:override" => { - expect_arg_count(function_name, &arguments, 2)?; - Ok(v_locker::override_hot_vlocker( - call_context, - arguments[0].clone(), - arguments[1].clone(), - )) - } - "owned:locker:hot:save" => { - expect_arg_count(function_name, &arguments, 1)?; - Ok(v_locker::save_hot_vlocker( - call_context, - arguments[0].clone(), - )) - } - "owned:locker:hot:remove" => { - expect_arg_count(function_name, &arguments, 1)?; - Ok(v_locker::remove_hot_vlocker( - call_context, - arguments[0].clone(), - )) - } - "cad:activity:append" => { - expect_arg_count(function_name, &arguments, 1)?; - Ok(cad::append_activity(arguments[0].clone())) - } - "cad:activity:recent" => { - expect_arg_count(function_name, &arguments, 1)?; - Ok(cad::recent_activity(arguments[0].clone())) - } - "cad:assignments:list" => { - expect_arg_count(function_name, &arguments, 0)?; - Ok(cad::list_assignments()) - } - "cad:assignments:assign" => { - expect_arg_count(function_name, &arguments, 2)?; - Ok(cad::assign_assignment( - arguments[0].clone(), - arguments[1].clone(), - )) - } - "cad:assignments:acknowledge" => { - expect_arg_count(function_name, &arguments, 2)?; - Ok(cad::acknowledge_assignment( - arguments[0].clone(), - arguments[1].clone(), - )) - } - "cad:assignments:decline" => { - expect_arg_count(function_name, &arguments, 2)?; - Ok(cad::decline_assignment( - arguments[0].clone(), - arguments[1].clone(), - )) - } - "cad:assignments:upsert" => { - expect_arg_count(function_name, &arguments, 2)?; - Ok(cad::upsert_assignment( - arguments[0].clone(), - arguments[1].clone(), - )) - } - "cad:assignments:delete" => { - expect_arg_count(function_name, &arguments, 1)?; - Ok(cad::delete_assignment(arguments[0].clone())) - } - "cad:orders:list" => { - expect_arg_count(function_name, &arguments, 0)?; - Ok(cad::list_orders()) - } - "cad:orders:create" => { - expect_arg_count(function_name, &arguments, 1)?; - Ok(cad::create_order(arguments[0].clone())) - } - "cad:orders:create_from_context" => { - expect_arg_count(function_name, &arguments, 1)?; - Ok(cad::create_order_from_context(arguments[0].clone())) - } - "cad:orders:close" => { - expect_arg_count(function_name, &arguments, 1)?; - Ok(cad::close_order(arguments[0].clone())) - } - "cad:orders:upsert" => { - expect_arg_count(function_name, &arguments, 2)?; - Ok(cad::upsert_order( - arguments[0].clone(), - arguments[1].clone(), - )) - } - "cad:orders:delete" => { - expect_arg_count(function_name, &arguments, 1)?; - Ok(cad::delete_order(arguments[0].clone())) - } - "cad:requests:list" => { - expect_arg_count(function_name, &arguments, 0)?; - Ok(cad::list_requests()) - } - "cad:requests:submit" => { - expect_arg_count(function_name, &arguments, 1)?; - Ok(cad::submit_request(arguments[0].clone())) - } - "cad:requests:submit_from_context" => { - expect_arg_count(function_name, &arguments, 1)?; - Ok(cad::submit_request_from_context(arguments[0].clone())) - } - "cad:requests:close" => { - expect_arg_count(function_name, &arguments, 1)?; - Ok(cad::close_request(arguments[0].clone())) - } - "cad:requests:upsert" => { - expect_arg_count(function_name, &arguments, 2)?; - Ok(cad::upsert_request( - arguments[0].clone(), - arguments[1].clone(), - )) - } - "cad:requests:delete" => { - expect_arg_count(function_name, &arguments, 1)?; - Ok(cad::delete_request(arguments[0].clone())) - } - "cad:profiles:list" => { - expect_arg_count(function_name, &arguments, 0)?; - Ok(cad::list_profiles()) - } - "cad:profiles:update_from_context" => { - expect_arg_count(function_name, &arguments, 1)?; - Ok(cad::update_profile_from_context(arguments[0].clone())) - } - "cad:profiles:upsert" => { - expect_arg_count(function_name, &arguments, 2)?; - Ok(cad::upsert_profile( - arguments[0].clone(), - arguments[1].clone(), - )) - } - "cad:profiles:delete" => { - expect_arg_count(function_name, &arguments, 1)?; - Ok(cad::delete_profile(arguments[0].clone())) - } - "cad:groups:build" => { - expect_arg_count(function_name, &arguments, 1)?; - Ok(cad::build_groups(arguments[0].clone())) - } - "cad:view:hydrate" => { - expect_arg_count(function_name, &arguments, 1)?; - Ok(cad::hydrate_view(arguments[0].clone())) - } - "phone:init" => { - expect_arg_count(function_name, &arguments, 1)?; - Ok(phone::init_phone(arguments[0].clone())) - } - "phone:contacts:list" => { - expect_arg_count(function_name, &arguments, 1)?; - Ok(phone::list_contacts(arguments[0].clone())) - } - "phone:contacts:add" => { - expect_arg_count(function_name, &arguments, 2)?; - Ok(phone::add_contact( - arguments[0].clone(), - arguments[1].clone(), - )) - } - "phone:contacts:remove" => { - expect_arg_count(function_name, &arguments, 2)?; - Ok(phone::remove_contact( - arguments[0].clone(), - arguments[1].clone(), - )) - } - "phone:messages:list" => { - expect_arg_count(function_name, &arguments, 1)?; - Ok(phone::list_messages(arguments[0].clone())) - } - "phone:messages:thread" => { - expect_arg_count(function_name, &arguments, 2)?; - Ok(phone::message_thread( - arguments[0].clone(), - arguments[1].clone(), - )) - } - "phone:messages:send" => { - expect_arg_count(function_name, &arguments, 4)?; - Ok(phone::send_message( - arguments[0].clone(), - arguments[1].clone(), - arguments[2].clone(), - arguments[3].clone(), - )) - } - "phone:messages:mark_read" => { - expect_arg_count(function_name, &arguments, 2)?; - Ok(phone::mark_message_read( - arguments[0].clone(), - arguments[1].clone(), - )) - } - "phone:emails:list" => { - expect_arg_count(function_name, &arguments, 1)?; - Ok(phone::list_emails(arguments[0].clone())) - } - "phone:emails:send" => { - expect_arg_count(function_name, &arguments, 5)?; - Ok(phone::send_email( - arguments[0].clone(), - arguments[1].clone(), - arguments[2].clone(), - arguments[3].clone(), - arguments[4].clone(), - )) - } - "phone:emails:mark_read" => { - expect_arg_count(function_name, &arguments, 2)?; - Ok(phone::mark_email_read( - arguments[0].clone(), - arguments[1].clone(), - )) - } - "phone:remove" => { - expect_arg_count(function_name, &arguments, 1)?; - Ok(phone::remove_phone(arguments[0].clone())) - } - _ => Err(format!( - "{UNSUPPORTED_ROUTE_PREFIX} for function '{function_name}'" - )), - } -} - -fn expect_arg_count( - function_name: &str, - arguments: &[String], - expected_count: usize, -) -> Result<(), String> { - if arguments.len() == expected_count { - return Ok(()); - } - - Err(format!( - "Transport route '{}' expected {} arguments but received {}", - function_name, - expected_count, - arguments.len() - )) -} - fn chunk_response_if_needed(result: String) -> String { if result.len() <= RESPONSE_CHUNK_SIZE { return result; diff --git a/arma/server/extension/src/transport/routes.rs b/arma/server/extension/src/transport/routes.rs new file mode 100644 index 0000000..1462286 --- /dev/null +++ b/arma/server/extension/src/transport/routes.rs @@ -0,0 +1,74 @@ +use arma_rs::CallContext; + +mod actor; +mod bank; +mod cad; +mod garage; +mod locker; +mod org; +mod phone; +mod store; +mod v_garage; +mod v_locker; + +const UNSUPPORTED_ROUTE_PREFIX: &str = "Unsupported transport route"; + +pub(super) fn route_command( + call_context: CallContext, + function_name: &str, + arguments: Vec, +) -> Result { + if function_name.starts_with("actor:") { + return actor::route(call_context, function_name, arguments); + } + if function_name.starts_with("bank:") { + return bank::route(call_context, function_name, arguments); + } + if function_name.starts_with("org:") { + return org::route(call_context, function_name, arguments); + } + if function_name == "store:checkout" { + return store::route(call_context, function_name, arguments); + } + if function_name.starts_with("garage:") { + return garage::route(call_context, function_name, arguments); + } + if function_name.starts_with("locker:") { + return locker::route(call_context, function_name, arguments); + } + if function_name.starts_with("owned:garage:") { + return v_garage::route(call_context, function_name, arguments); + } + if function_name.starts_with("owned:locker:") { + return v_locker::route(call_context, function_name, arguments); + } + if function_name.starts_with("cad:") { + return cad::route(call_context, function_name, arguments); + } + if function_name.starts_with("phone:") { + return phone::route(call_context, function_name, arguments); + } + + Err(unsupported_route(function_name)) +} + +pub(super) fn expect_arg_count( + function_name: &str, + arguments: &[String], + expected_count: usize, +) -> Result<(), String> { + if arguments.len() == expected_count { + return Ok(()); + } + + Err(format!( + "Transport route '{}' expected {} arguments but received {}", + function_name, + expected_count, + arguments.len() + )) +} + +pub(super) fn unsupported_route(function_name: &str) -> String { + format!("{UNSUPPORTED_ROUTE_PREFIX} for function '{function_name}'") +} diff --git a/arma/server/extension/src/transport/routes/actor.rs b/arma/server/extension/src/transport/routes/actor.rs new file mode 100644 index 0000000..93a6e28 --- /dev/null +++ b/arma/server/extension/src/transport/routes/actor.rs @@ -0,0 +1,72 @@ +use arma_rs::CallContext; + +use super::expect_arg_count; +use crate::actor; + +pub(super) fn route( + call_context: CallContext, + function_name: &str, + arguments: Vec, +) -> Result { + let _ = &call_context; + + match function_name { + "actor:get" => { + expect_arg_count(function_name, &arguments, 1)?; + Ok(actor::get_actor(call_context, arguments[0].clone())) + } + "actor:create" => { + expect_arg_count(function_name, &arguments, 2)?; + Ok(actor::create_actor( + call_context, + arguments[0].clone(), + arguments[1].clone(), + )) + } + "actor:update" => { + expect_arg_count(function_name, &arguments, 2)?; + Ok(actor::update_actor( + call_context, + arguments[0].clone(), + arguments[1].clone(), + )) + } + "actor:exists" => { + expect_arg_count(function_name, &arguments, 1)?; + Ok(actor::actor_exists(call_context, arguments[0].clone())) + } + "actor:delete" => { + expect_arg_count(function_name, &arguments, 1)?; + Ok(actor::delete_actor(call_context, arguments[0].clone())) + } + "actor:hot:init" => { + expect_arg_count(function_name, &arguments, 1)?; + Ok(actor::init_hot_actor(call_context, arguments[0].clone())) + } + "actor:hot:get" => { + expect_arg_count(function_name, &arguments, 1)?; + Ok(actor::get_hot_actor(call_context, arguments[0].clone())) + } + "actor:hot:keys" => { + expect_arg_count(function_name, &arguments, 0)?; + Ok(actor::list_hot_actor_keys()) + } + "actor:hot:override" => { + expect_arg_count(function_name, &arguments, 2)?; + Ok(actor::override_hot_actor( + call_context, + arguments[0].clone(), + arguments[1].clone(), + )) + } + "actor:hot:save" => { + expect_arg_count(function_name, &arguments, 1)?; + Ok(actor::save_hot_actor(call_context, arguments[0].clone())) + } + "actor:hot:remove" => { + expect_arg_count(function_name, &arguments, 1)?; + Ok(actor::remove_hot_actor(call_context, arguments[0].clone())) + } + _ => Err(super::unsupported_route(function_name)), + } +} diff --git a/arma/server/extension/src/transport/routes/bank.rs b/arma/server/extension/src/transport/routes/bank.rs new file mode 100644 index 0000000..d073f76 --- /dev/null +++ b/arma/server/extension/src/transport/routes/bank.rs @@ -0,0 +1,131 @@ +use arma_rs::CallContext; + +use super::expect_arg_count; +use crate::bank; + +pub(super) fn route( + call_context: CallContext, + function_name: &str, + arguments: Vec, +) -> Result { + let _ = &call_context; + + match function_name { + "bank:get" => { + expect_arg_count(function_name, &arguments, 1)?; + Ok(bank::get_bank(call_context, arguments[0].clone())) + } + "bank:create" => { + expect_arg_count(function_name, &arguments, 2)?; + Ok(bank::create_bank( + call_context, + arguments[0].clone(), + arguments[1].clone(), + )) + } + "bank:update" => { + expect_arg_count(function_name, &arguments, 2)?; + Ok(bank::update_bank( + call_context, + arguments[0].clone(), + arguments[1].clone(), + )) + } + "bank:exists" => { + expect_arg_count(function_name, &arguments, 1)?; + Ok(bank::bank_exists(call_context, arguments[0].clone())) + } + "bank:delete" => { + expect_arg_count(function_name, &arguments, 1)?; + Ok(bank::delete_bank(call_context, arguments[0].clone())) + } + "bank:hot:init" => { + expect_arg_count(function_name, &arguments, 1)?; + Ok(bank::init_hot_bank(call_context, arguments[0].clone())) + } + "bank:hot:get" => { + expect_arg_count(function_name, &arguments, 1)?; + Ok(bank::get_hot_bank(call_context, arguments[0].clone())) + } + "bank:hot:override" => { + expect_arg_count(function_name, &arguments, 2)?; + Ok(bank::override_hot_bank( + call_context, + arguments[0].clone(), + arguments[1].clone(), + )) + } + "bank:hot:patch" => { + expect_arg_count(function_name, &arguments, 2)?; + Ok(bank::patch_hot_bank( + call_context, + arguments[0].clone(), + arguments[1].clone(), + )) + } + "bank:hot:charge_checkout" => { + expect_arg_count(function_name, &arguments, 3)?; + Ok(bank::charge_checkout_hot_bank( + call_context, + arguments[0].clone(), + arguments[1].clone(), + arguments[2].clone(), + )) + } + "bank:hot:deposit" => { + expect_arg_count(function_name, &arguments, 3)?; + Ok(bank::deposit_hot_bank( + call_context, + arguments[0].clone(), + arguments[1].clone(), + arguments[2].clone(), + )) + } + "bank:hot:withdraw" => { + expect_arg_count(function_name, &arguments, 3)?; + Ok(bank::withdraw_hot_bank( + call_context, + arguments[0].clone(), + arguments[1].clone(), + arguments[2].clone(), + )) + } + "bank:hot:deposit_earnings" => { + expect_arg_count(function_name, &arguments, 3)?; + Ok(bank::deposit_earnings_hot_bank( + call_context, + arguments[0].clone(), + arguments[1].clone(), + arguments[2].clone(), + )) + } + "bank:hot:transfer" => { + expect_arg_count(function_name, &arguments, 4)?; + Ok(bank::transfer_hot_bank( + call_context, + arguments[0].clone(), + arguments[1].clone(), + arguments[2].clone(), + arguments[3].clone(), + )) + } + "bank:hot:validate_pin" => { + expect_arg_count(function_name, &arguments, 3)?; + Ok(bank::validate_pin_hot_bank( + call_context, + arguments[0].clone(), + arguments[1].clone(), + arguments[2].clone(), + )) + } + "bank:hot:save" => { + expect_arg_count(function_name, &arguments, 1)?; + Ok(bank::save_hot_bank(call_context, arguments[0].clone())) + } + "bank:hot:remove" => { + expect_arg_count(function_name, &arguments, 1)?; + Ok(bank::remove_hot_bank(call_context, arguments[0].clone())) + } + _ => Err(super::unsupported_route(function_name)), + } +} diff --git a/arma/server/extension/src/transport/routes/cad.rs b/arma/server/extension/src/transport/routes/cad.rs new file mode 100644 index 0000000..e9668ef --- /dev/null +++ b/arma/server/extension/src/transport/routes/cad.rs @@ -0,0 +1,141 @@ +use arma_rs::CallContext; + +use super::expect_arg_count; +use crate::cad; + +pub(super) fn route( + call_context: CallContext, + function_name: &str, + arguments: Vec, +) -> Result { + let _ = &call_context; + + match function_name { + "cad:activity:append" => { + expect_arg_count(function_name, &arguments, 1)?; + Ok(cad::append_activity(arguments[0].clone())) + } + "cad:activity:recent" => { + expect_arg_count(function_name, &arguments, 1)?; + Ok(cad::recent_activity(arguments[0].clone())) + } + "cad:assignments:list" => { + expect_arg_count(function_name, &arguments, 0)?; + Ok(cad::list_assignments()) + } + "cad:assignments:assign" => { + expect_arg_count(function_name, &arguments, 2)?; + Ok(cad::assign_assignment( + arguments[0].clone(), + arguments[1].clone(), + )) + } + "cad:assignments:acknowledge" => { + expect_arg_count(function_name, &arguments, 2)?; + Ok(cad::acknowledge_assignment( + arguments[0].clone(), + arguments[1].clone(), + )) + } + "cad:assignments:decline" => { + expect_arg_count(function_name, &arguments, 2)?; + Ok(cad::decline_assignment( + arguments[0].clone(), + arguments[1].clone(), + )) + } + "cad:assignments:upsert" => { + expect_arg_count(function_name, &arguments, 2)?; + Ok(cad::upsert_assignment( + arguments[0].clone(), + arguments[1].clone(), + )) + } + "cad:assignments:delete" => { + expect_arg_count(function_name, &arguments, 1)?; + Ok(cad::delete_assignment(arguments[0].clone())) + } + "cad:orders:list" => { + expect_arg_count(function_name, &arguments, 0)?; + Ok(cad::list_orders()) + } + "cad:orders:create" => { + expect_arg_count(function_name, &arguments, 1)?; + Ok(cad::create_order(arguments[0].clone())) + } + "cad:orders:create_from_context" => { + expect_arg_count(function_name, &arguments, 1)?; + Ok(cad::create_order_from_context(arguments[0].clone())) + } + "cad:orders:close" => { + expect_arg_count(function_name, &arguments, 1)?; + Ok(cad::close_order(arguments[0].clone())) + } + "cad:orders:upsert" => { + expect_arg_count(function_name, &arguments, 2)?; + Ok(cad::upsert_order( + arguments[0].clone(), + arguments[1].clone(), + )) + } + "cad:orders:delete" => { + expect_arg_count(function_name, &arguments, 1)?; + Ok(cad::delete_order(arguments[0].clone())) + } + "cad:requests:list" => { + expect_arg_count(function_name, &arguments, 0)?; + Ok(cad::list_requests()) + } + "cad:requests:submit" => { + expect_arg_count(function_name, &arguments, 1)?; + Ok(cad::submit_request(arguments[0].clone())) + } + "cad:requests:submit_from_context" => { + expect_arg_count(function_name, &arguments, 1)?; + Ok(cad::submit_request_from_context(arguments[0].clone())) + } + "cad:requests:close" => { + expect_arg_count(function_name, &arguments, 1)?; + Ok(cad::close_request(arguments[0].clone())) + } + "cad:requests:upsert" => { + expect_arg_count(function_name, &arguments, 2)?; + Ok(cad::upsert_request( + arguments[0].clone(), + arguments[1].clone(), + )) + } + "cad:requests:delete" => { + expect_arg_count(function_name, &arguments, 1)?; + Ok(cad::delete_request(arguments[0].clone())) + } + "cad:profiles:list" => { + expect_arg_count(function_name, &arguments, 0)?; + Ok(cad::list_profiles()) + } + "cad:profiles:update_from_context" => { + expect_arg_count(function_name, &arguments, 1)?; + Ok(cad::update_profile_from_context(arguments[0].clone())) + } + "cad:profiles:upsert" => { + expect_arg_count(function_name, &arguments, 2)?; + Ok(cad::upsert_profile( + arguments[0].clone(), + arguments[1].clone(), + )) + } + "cad:profiles:delete" => { + expect_arg_count(function_name, &arguments, 1)?; + Ok(cad::delete_profile(arguments[0].clone())) + } + "cad:groups:build" => { + expect_arg_count(function_name, &arguments, 1)?; + Ok(cad::build_groups(arguments[0].clone())) + } + "cad:view:hydrate" => { + expect_arg_count(function_name, &arguments, 1)?; + Ok(cad::hydrate_view(arguments[0].clone())) + } + _ => Err(super::unsupported_route(function_name)), + } +} diff --git a/arma/server/extension/src/transport/routes/garage.rs b/arma/server/extension/src/transport/routes/garage.rs new file mode 100644 index 0000000..6d7d7c4 --- /dev/null +++ b/arma/server/extension/src/transport/routes/garage.rs @@ -0,0 +1,107 @@ +use arma_rs::CallContext; + +use super::expect_arg_count; +use crate::garage; + +pub(super) fn route( + call_context: CallContext, + function_name: &str, + arguments: Vec, +) -> Result { + let _ = &call_context; + + match function_name { + "garage:create" => { + expect_arg_count(function_name, &arguments, 1)?; + Ok(garage::create_garage(call_context, arguments[0].clone())) + } + "garage:get" => { + expect_arg_count(function_name, &arguments, 1)?; + Ok(garage::get_garage(call_context, arguments[0].clone())) + } + "garage:add" => { + expect_arg_count(function_name, &arguments, 2)?; + Ok(garage::add_vehicle( + call_context, + arguments[0].clone(), + arguments[1].clone(), + )) + } + "garage:update" => { + expect_arg_count(function_name, &arguments, 2)?; + Ok(garage::update_garage( + call_context, + arguments[0].clone(), + arguments[1].clone(), + )) + } + "garage:patch" => { + expect_arg_count(function_name, &arguments, 2)?; + Ok(garage::patch_vehicle( + call_context, + arguments[0].clone(), + arguments[1].clone(), + )) + } + "garage:remove" => { + expect_arg_count(function_name, &arguments, 2)?; + Ok(garage::remove_vehicle( + call_context, + arguments[0].clone(), + arguments[1].clone(), + )) + } + "garage:delete" => { + expect_arg_count(function_name, &arguments, 1)?; + Ok(garage::delete_garage(call_context, arguments[0].clone())) + } + "garage:exists" => { + expect_arg_count(function_name, &arguments, 1)?; + Ok(garage::garage_exists(call_context, arguments[0].clone())) + } + "garage:hot:init" => { + expect_arg_count(function_name, &arguments, 1)?; + Ok(garage::init_hot_garage(call_context, arguments[0].clone())) + } + "garage:hot:get" => { + expect_arg_count(function_name, &arguments, 1)?; + Ok(garage::get_hot_garage(call_context, arguments[0].clone())) + } + "garage:hot:override" => { + expect_arg_count(function_name, &arguments, 2)?; + Ok(garage::override_hot_garage( + call_context, + arguments[0].clone(), + arguments[1].clone(), + )) + } + "garage:hot:save" => { + expect_arg_count(function_name, &arguments, 1)?; + Ok(garage::save_hot_garage(call_context, arguments[0].clone())) + } + "garage:hot:remove" => { + expect_arg_count(function_name, &arguments, 1)?; + Ok(garage::remove_hot_garage( + call_context, + arguments[0].clone(), + )) + } + "garage:hot:add" => { + expect_arg_count(function_name, &arguments, 2)?; + Ok(garage::add_hot_vehicle( + call_context, + arguments[0].clone(), + arguments[1].clone(), + )) + } + "garage:hot:remove_vehicle" => { + expect_arg_count(function_name, &arguments, 2)?; + Ok(garage::remove_hot_vehicle( + call_context, + arguments[0].clone(), + arguments[1].clone(), + )) + } + _ => Err(super::unsupported_route(function_name)), + } +} diff --git a/arma/server/extension/src/transport/routes/locker.rs b/arma/server/extension/src/transport/routes/locker.rs new file mode 100644 index 0000000..8696764 --- /dev/null +++ b/arma/server/extension/src/transport/routes/locker.rs @@ -0,0 +1,91 @@ +use arma_rs::CallContext; + +use super::expect_arg_count; +use crate::locker; + +pub(super) fn route( + call_context: CallContext, + function_name: &str, + arguments: Vec, +) -> Result { + let _ = &call_context; + + match function_name { + "locker:create" => { + expect_arg_count(function_name, &arguments, 1)?; + Ok(locker::create_locker(call_context, arguments[0].clone())) + } + "locker:get" => { + expect_arg_count(function_name, &arguments, 1)?; + Ok(locker::get_locker(call_context, arguments[0].clone())) + } + "locker:add" => { + expect_arg_count(function_name, &arguments, 2)?; + Ok(locker::add_item( + call_context, + arguments[0].clone(), + arguments[1].clone(), + )) + } + "locker:update" => { + expect_arg_count(function_name, &arguments, 2)?; + Ok(locker::update_locker( + call_context, + arguments[0].clone(), + arguments[1].clone(), + )) + } + "locker:patch" => { + expect_arg_count(function_name, &arguments, 2)?; + Ok(locker::patch_item( + call_context, + arguments[0].clone(), + arguments[1].clone(), + )) + } + "locker:remove" => { + expect_arg_count(function_name, &arguments, 2)?; + Ok(locker::remove_item( + call_context, + arguments[0].clone(), + arguments[1].clone(), + )) + } + "locker:delete" => { + expect_arg_count(function_name, &arguments, 1)?; + Ok(locker::delete_locker(call_context, arguments[0].clone())) + } + "locker:exists" => { + expect_arg_count(function_name, &arguments, 1)?; + Ok(locker::locker_exists(call_context, arguments[0].clone())) + } + "locker:hot:init" => { + expect_arg_count(function_name, &arguments, 1)?; + Ok(locker::init_hot_locker(call_context, arguments[0].clone())) + } + "locker:hot:get" => { + expect_arg_count(function_name, &arguments, 1)?; + Ok(locker::get_hot_locker(call_context, arguments[0].clone())) + } + "locker:hot:override" => { + expect_arg_count(function_name, &arguments, 2)?; + Ok(locker::override_hot_locker( + call_context, + arguments[0].clone(), + arguments[1].clone(), + )) + } + "locker:hot:save" => { + expect_arg_count(function_name, &arguments, 1)?; + Ok(locker::save_hot_locker(call_context, arguments[0].clone())) + } + "locker:hot:remove" => { + expect_arg_count(function_name, &arguments, 1)?; + Ok(locker::remove_hot_locker( + call_context, + arguments[0].clone(), + )) + } + _ => Err(super::unsupported_route(function_name)), + } +} diff --git a/arma/server/extension/src/transport/routes/org.rs b/arma/server/extension/src/transport/routes/org.rs new file mode 100644 index 0000000..e80584f --- /dev/null +++ b/arma/server/extension/src/transport/routes/org.rs @@ -0,0 +1,154 @@ +use arma_rs::CallContext; + +use super::expect_arg_count; +use crate::org; + +pub(super) fn route( + call_context: CallContext, + function_name: &str, + arguments: Vec, +) -> Result { + let _ = &call_context; + + match function_name { + "org:get" => { + expect_arg_count(function_name, &arguments, 1)?; + Ok(org::get_org(arguments[0].clone())) + } + "org:create" => { + expect_arg_count(function_name, &arguments, 2)?; + Ok(org::create_org(arguments[0].clone(), arguments[1].clone())) + } + "org:update" => { + expect_arg_count(function_name, &arguments, 2)?; + Ok(org::update_org(arguments[0].clone(), arguments[1].clone())) + } + "org:exists" => { + expect_arg_count(function_name, &arguments, 1)?; + Ok(org::org_exists(arguments[0].clone())) + } + "org:delete" => { + expect_arg_count(function_name, &arguments, 1)?; + Ok(org::delete_org(arguments[0].clone())) + } + "org:hot:init" => { + expect_arg_count(function_name, &arguments, 1)?; + Ok(org::init_hot_org(arguments[0].clone())) + } + "org:hot:get" => { + expect_arg_count(function_name, &arguments, 1)?; + Ok(org::get_hot_org(arguments[0].clone())) + } + "org:hot:override" => { + expect_arg_count(function_name, &arguments, 2)?; + Ok(org::override_hot_org( + arguments[0].clone(), + arguments[1].clone(), + )) + } + "org:hot:ensure_member" => { + expect_arg_count(function_name, &arguments, 1)?; + Ok(org::ensure_hot_org_member(arguments[0].clone())) + } + "org:hot:member_invites" => { + expect_arg_count(function_name, &arguments, 1)?; + Ok(org::get_hot_org_member_invites(arguments[0].clone())) + } + "org:hot:register" => { + expect_arg_count(function_name, &arguments, 1)?; + Ok(org::register_hot_org(arguments[0].clone())) + } + "org:hot:invite_member" => { + expect_arg_count(function_name, &arguments, 1)?; + Ok(org::invite_hot_org_member(arguments[0].clone())) + } + "org:hot:accept_invite" => { + expect_arg_count(function_name, &arguments, 1)?; + Ok(org::accept_hot_org_invite(arguments[0].clone())) + } + "org:hot:decline_invite" => { + expect_arg_count(function_name, &arguments, 1)?; + Ok(org::decline_hot_org_invite(arguments[0].clone())) + } + "org:hot:assign_credit_line" => { + expect_arg_count(function_name, &arguments, 1)?; + Ok(org::assign_credit_line_hot_org(arguments[0].clone())) + } + "org:hot:repay_credit_line" => { + expect_arg_count(function_name, &arguments, 1)?; + Ok(org::repay_credit_line_hot_org(arguments[0].clone())) + } + "org:hot:charge_checkout" => { + expect_arg_count(function_name, &arguments, 1)?; + Ok(org::charge_checkout_hot_org(arguments[0].clone())) + } + "org:hot:add_assets" => { + expect_arg_count(function_name, &arguments, 2)?; + Ok(org::add_assets_hot_org( + arguments[0].clone(), + arguments[1].clone(), + )) + } + "org:hot:add_fleet" => { + expect_arg_count(function_name, &arguments, 2)?; + Ok(org::add_fleet_hot_org( + arguments[0].clone(), + arguments[1].clone(), + )) + } + "org:hot:leave" => { + expect_arg_count(function_name, &arguments, 1)?; + Ok(org::leave_hot_org(arguments[0].clone())) + } + "org:hot:disband" => { + expect_arg_count(function_name, &arguments, 1)?; + Ok(org::disband_hot_org(arguments[0].clone())) + } + "org:hot:save" => { + expect_arg_count(function_name, &arguments, 1)?; + Ok(org::save_hot_org(arguments[0].clone())) + } + "org:hot:remove" => { + expect_arg_count(function_name, &arguments, 1)?; + Ok(org::remove_hot_org(arguments[0].clone())) + } + "org:assets:get" => { + expect_arg_count(function_name, &arguments, 1)?; + Ok(org::get_assets(arguments[0].clone())) + } + "org:assets:update" => { + expect_arg_count(function_name, &arguments, 2)?; + Ok(org::update_assets( + arguments[0].clone(), + arguments[1].clone(), + )) + } + "org:fleet:get" => { + expect_arg_count(function_name, &arguments, 1)?; + Ok(org::get_fleet(arguments[0].clone())) + } + "org:fleet:update" => { + expect_arg_count(function_name, &arguments, 2)?; + Ok(org::update_fleet( + arguments[0].clone(), + arguments[1].clone(), + )) + } + "org:members:get" => { + expect_arg_count(function_name, &arguments, 1)?; + Ok(org::get_members(arguments[0].clone())) + } + "org:members:add" => { + expect_arg_count(function_name, &arguments, 2)?; + Ok(org::add_member(arguments[0].clone(), arguments[1].clone())) + } + "org:members:remove" => { + expect_arg_count(function_name, &arguments, 2)?; + Ok(org::remove_member( + arguments[0].clone(), + arguments[1].clone(), + )) + } + _ => Err(super::unsupported_route(function_name)), + } +} diff --git a/arma/server/extension/src/transport/routes/phone.rs b/arma/server/extension/src/transport/routes/phone.rs new file mode 100644 index 0000000..3c7c480 --- /dev/null +++ b/arma/server/extension/src/transport/routes/phone.rs @@ -0,0 +1,104 @@ +use arma_rs::CallContext; + +use super::expect_arg_count; +use crate::phone; + +pub(super) fn route( + call_context: CallContext, + function_name: &str, + arguments: Vec, +) -> Result { + let _ = &call_context; + + match function_name { + "phone:init" => { + expect_arg_count(function_name, &arguments, 1)?; + Ok(phone::init_phone(arguments[0].clone())) + } + "phone:contacts:list" => { + expect_arg_count(function_name, &arguments, 1)?; + Ok(phone::list_contacts(arguments[0].clone())) + } + "phone:contacts:add" => { + expect_arg_count(function_name, &arguments, 2)?; + Ok(phone::add_contact( + arguments[0].clone(), + arguments[1].clone(), + )) + } + "phone:contacts:remove" => { + expect_arg_count(function_name, &arguments, 2)?; + Ok(phone::remove_contact( + arguments[0].clone(), + arguments[1].clone(), + )) + } + "phone:messages:list" => { + expect_arg_count(function_name, &arguments, 1)?; + Ok(phone::list_messages(arguments[0].clone())) + } + "phone:messages:thread" => { + expect_arg_count(function_name, &arguments, 2)?; + Ok(phone::message_thread( + arguments[0].clone(), + arguments[1].clone(), + )) + } + "phone:messages:send" => { + expect_arg_count(function_name, &arguments, 4)?; + Ok(phone::send_message( + arguments[0].clone(), + arguments[1].clone(), + arguments[2].clone(), + arguments[3].clone(), + )) + } + "phone:messages:mark_read" => { + expect_arg_count(function_name, &arguments, 2)?; + Ok(phone::mark_message_read( + arguments[0].clone(), + arguments[1].clone(), + )) + } + "phone:messages:delete" => { + expect_arg_count(function_name, &arguments, 2)?; + Ok(phone::delete_message( + arguments[0].clone(), + arguments[1].clone(), + )) + } + "phone:emails:list" => { + expect_arg_count(function_name, &arguments, 1)?; + Ok(phone::list_emails(arguments[0].clone())) + } + "phone:emails:send" => { + expect_arg_count(function_name, &arguments, 5)?; + Ok(phone::send_email( + arguments[0].clone(), + arguments[1].clone(), + arguments[2].clone(), + arguments[3].clone(), + arguments[4].clone(), + )) + } + "phone:emails:mark_read" => { + expect_arg_count(function_name, &arguments, 2)?; + Ok(phone::mark_email_read( + arguments[0].clone(), + arguments[1].clone(), + )) + } + "phone:emails:delete" => { + expect_arg_count(function_name, &arguments, 2)?; + Ok(phone::delete_email( + arguments[0].clone(), + arguments[1].clone(), + )) + } + "phone:remove" => { + expect_arg_count(function_name, &arguments, 1)?; + Ok(phone::remove_phone(arguments[0].clone())) + } + _ => Err(super::unsupported_route(function_name)), + } +} diff --git a/arma/server/extension/src/transport/routes/store.rs b/arma/server/extension/src/transport/routes/store.rs new file mode 100644 index 0000000..2ab3ec7 --- /dev/null +++ b/arma/server/extension/src/transport/routes/store.rs @@ -0,0 +1,18 @@ +use arma_rs::CallContext; + +use super::expect_arg_count; +pub(super) fn route( + call_context: CallContext, + function_name: &str, + arguments: Vec, +) -> Result { + let _ = &call_context; + + match function_name { + "store:checkout" => { + expect_arg_count(function_name, &arguments, 1)?; + Ok(crate::store::checkout(arguments[0].clone())) + } + _ => Err(super::unsupported_route(function_name)), + } +} diff --git a/arma/server/extension/src/transport/routes/v_garage.rs b/arma/server/extension/src/transport/routes/v_garage.rs new file mode 100644 index 0000000..9e76f13 --- /dev/null +++ b/arma/server/extension/src/transport/routes/v_garage.rs @@ -0,0 +1,120 @@ +use arma_rs::CallContext; + +use super::expect_arg_count; +use crate::v_garage; + +pub(super) fn route( + call_context: CallContext, + function_name: &str, + arguments: Vec, +) -> Result { + let _ = &call_context; + + match function_name { + "owned:garage:create" => { + expect_arg_count(function_name, &arguments, 1)?; + Ok(v_garage::create_vgarage(call_context, arguments[0].clone())) + } + "owned:garage:fetch" => { + expect_arg_count(function_name, &arguments, 1)?; + Ok(v_garage::fetch_vgarage(call_context, arguments[0].clone())) + } + "owned:garage:get" => { + expect_arg_count(function_name, &arguments, 2)?; + Ok(v_garage::get_vgarage( + call_context, + arguments[0].clone(), + arguments[1].clone(), + )) + } + "owned:garage:add" => { + expect_arg_count(function_name, &arguments, 3)?; + Ok(v_garage::add_vgarage( + call_context, + arguments[0].clone(), + arguments[1].clone(), + arguments[2].clone(), + )) + } + "owned:garage:remove" => { + expect_arg_count(function_name, &arguments, 3)?; + Ok(v_garage::remove_vgarage( + call_context, + arguments[0].clone(), + arguments[1].clone(), + arguments[2].clone(), + )) + } + "owned:garage:delete" => { + expect_arg_count(function_name, &arguments, 1)?; + Ok(v_garage::delete_vgarage(call_context, arguments[0].clone())) + } + "owned:garage:exists" => { + expect_arg_count(function_name, &arguments, 1)?; + Ok(v_garage::vgarage_exists(call_context, arguments[0].clone())) + } + "owned:garage:hot:init" => { + expect_arg_count(function_name, &arguments, 1)?; + Ok(v_garage::init_hot_vgarage( + call_context, + arguments[0].clone(), + )) + } + "owned:garage:hot:fetch" => { + expect_arg_count(function_name, &arguments, 1)?; + Ok(v_garage::fetch_hot_vgarage( + call_context, + arguments[0].clone(), + )) + } + "owned:garage:hot:get" => { + expect_arg_count(function_name, &arguments, 2)?; + Ok(v_garage::get_hot_vgarage( + call_context, + arguments[0].clone(), + arguments[1].clone(), + )) + } + "owned:garage:hot:override" => { + expect_arg_count(function_name, &arguments, 2)?; + Ok(v_garage::override_hot_vgarage( + call_context, + arguments[0].clone(), + arguments[1].clone(), + )) + } + "owned:garage:hot:save" => { + expect_arg_count(function_name, &arguments, 1)?; + Ok(v_garage::save_hot_vgarage( + call_context, + arguments[0].clone(), + )) + } + "owned:garage:hot:remove" => { + expect_arg_count(function_name, &arguments, 1)?; + Ok(v_garage::remove_hot_vgarage( + call_context, + arguments[0].clone(), + )) + } + "owned:garage:hot:add" => { + expect_arg_count(function_name, &arguments, 3)?; + Ok(v_garage::add_hot_vgarage( + call_context, + arguments[0].clone(), + arguments[1].clone(), + arguments[2].clone(), + )) + } + "owned:garage:hot:remove_item" => { + expect_arg_count(function_name, &arguments, 3)?; + Ok(v_garage::remove_hot_vgarage_item( + call_context, + arguments[0].clone(), + arguments[1].clone(), + arguments[2].clone(), + )) + } + _ => Err(super::unsupported_route(function_name)), + } +} diff --git a/arma/server/extension/src/transport/routes/v_locker.rs b/arma/server/extension/src/transport/routes/v_locker.rs new file mode 100644 index 0000000..d1b83fe --- /dev/null +++ b/arma/server/extension/src/transport/routes/v_locker.rs @@ -0,0 +1,102 @@ +use arma_rs::CallContext; + +use super::expect_arg_count; +use crate::v_locker; + +pub(super) fn route( + call_context: CallContext, + function_name: &str, + arguments: Vec, +) -> Result { + let _ = &call_context; + + match function_name { + "owned:locker:create" => { + expect_arg_count(function_name, &arguments, 1)?; + Ok(v_locker::create_vlocker(call_context, arguments[0].clone())) + } + "owned:locker:fetch" => { + expect_arg_count(function_name, &arguments, 1)?; + Ok(v_locker::fetch_vlocker(call_context, arguments[0].clone())) + } + "owned:locker:get" => { + expect_arg_count(function_name, &arguments, 2)?; + Ok(v_locker::get_vlocker( + call_context, + arguments[0].clone(), + arguments[1].clone(), + )) + } + "owned:locker:add" => { + expect_arg_count(function_name, &arguments, 3)?; + Ok(v_locker::add_vlocker( + call_context, + arguments[0].clone(), + arguments[1].clone(), + arguments[2].clone(), + )) + } + "owned:locker:remove" => { + expect_arg_count(function_name, &arguments, 3)?; + Ok(v_locker::remove_vlocker( + call_context, + arguments[0].clone(), + arguments[1].clone(), + arguments[2].clone(), + )) + } + "owned:locker:delete" => { + expect_arg_count(function_name, &arguments, 1)?; + Ok(v_locker::delete_vlocker(call_context, arguments[0].clone())) + } + "owned:locker:exists" => { + expect_arg_count(function_name, &arguments, 1)?; + Ok(v_locker::vlocker_exists(call_context, arguments[0].clone())) + } + "owned:locker:hot:init" => { + expect_arg_count(function_name, &arguments, 1)?; + Ok(v_locker::init_hot_vlocker( + call_context, + arguments[0].clone(), + )) + } + "owned:locker:hot:fetch" => { + expect_arg_count(function_name, &arguments, 1)?; + Ok(v_locker::fetch_hot_vlocker( + call_context, + arguments[0].clone(), + )) + } + "owned:locker:hot:get" => { + expect_arg_count(function_name, &arguments, 2)?; + Ok(v_locker::get_hot_vlocker( + call_context, + arguments[0].clone(), + arguments[1].clone(), + )) + } + "owned:locker:hot:override" => { + expect_arg_count(function_name, &arguments, 2)?; + Ok(v_locker::override_hot_vlocker( + call_context, + arguments[0].clone(), + arguments[1].clone(), + )) + } + "owned:locker:hot:save" => { + expect_arg_count(function_name, &arguments, 1)?; + Ok(v_locker::save_hot_vlocker( + call_context, + arguments[0].clone(), + )) + } + "owned:locker:hot:remove" => { + expect_arg_count(function_name, &arguments, 1)?; + Ok(v_locker::remove_hot_vlocker( + call_context, + arguments[0].clone(), + )) + } + _ => Err(super::unsupported_route(function_name)), + } +} diff --git a/history.txt b/history.txt new file mode 100644 index 0000000..12ef808 --- /dev/null +++ b/history.txt @@ -0,0 +1,101 @@ +#V2 +DEFINE FIELD OVERWRITE updated_at ON org_fleet_vehicle TYPE option; +DEFINE INDEX IF NOT EXISTS org_fleet_vehicle_org ON org_fleet_vehicle COLUMNS org_id; +DEFINE INDEX IF NOT EXISTS org_fleet_vehicle_unique ON org_fleet_vehicle COLUMNS org_id, fleet_key UNIQUE; +DEFINE TABLE IF NOT EXISTS locker SCHEMALESS; +DEFINE FIELD IF NOT EXISTS uid ON locker TYPE string; +DEFINE FIELD OVERWRITE updated_at ON locker TYPE option; +DEFINE INDEX IF NOT EXISTS locker_uid ON locker COLUMNS uid UNIQUE; +DEFINE TABLE IF NOT EXISTS locker_item SCHEMAFULL; +DEFINE FIELD IF NOT EXISTS uid ON locker_item TYPE string; +DEFINE FIELD IF NOT EXISTS category ON locker_item TYPE string; +DEFINE FIELD IF NOT EXISTS classname ON locker_item TYPE string; +DEFINE FIELD IF NOT EXISTS amount ON locker_item TYPE int; +DEFINE FIELD OVERWRITE updated_at ON locker_item TYPE option; +DEFINE INDEX IF NOT EXISTS locker_item_owner ON locker_item COLUMNS uid; +DEFINE INDEX IF NOT EXISTS locker_item_unique ON locker_item COLUMNS uid, classname UNIQUE; +DEFINE TABLE IF NOT EXISTS owned_locker SCHEMALESS; +DEFINE FIELD IF NOT EXISTS uid ON owned_locker TYPE string; +DEFINE FIELD OVERWRITE updated_at ON owned_locker TYPE option; +DEFINE INDEX IF NOT EXISTS owned_locker_uid ON owned_locker COLUMNS uid UNIQUE; +DEFINE TABLE IF NOT EXISTS locker_unlock SCHEMAFULL; +DEFINE FIELD IF NOT EXISTS uid ON locker_unlock TYPE string; +DEFINE FIELD IF NOT EXISTS category ON locker_unlock TYPE string; +DEFINE FIELD IF NOT EXISTS classname ON locker_unlock TYPE string; +DEFINE FIELD IF NOT EXISTS source ON locker_unlock TYPE option; +DEFINE FIELD OVERWRITE unlocked_at ON locker_unlock TYPE option; +DEFINE INDEX IF NOT EXISTS locker_unlock_owner ON locker_unlock COLUMNS uid; +DEFINE INDEX IF NOT EXISTS locker_unlock_unique ON locker_unlock COLUMNS uid, category, classname UNIQUE; +DEFINE TABLE IF NOT EXISTS garage SCHEMALESS; +DEFINE FIELD IF NOT EXISTS uid ON garage TYPE string; +DEFINE FIELD OVERWRITE updated_at ON garage TYPE option; +DEFINE INDEX IF NOT EXISTS garage_uid ON garage COLUMNS uid UNIQUE; +DEFINE TABLE IF NOT EXISTS garage_vehicle SCHEMAFULL; +DEFINE FIELD IF NOT EXISTS uid ON garage_vehicle TYPE string; +DEFINE FIELD IF NOT EXISTS plate ON garage_vehicle TYPE string; +DEFINE FIELD IF NOT EXISTS classname ON garage_vehicle TYPE string; +DEFINE FIELD IF NOT EXISTS fuel ON garage_vehicle TYPE number; +DEFINE FIELD IF NOT EXISTS damage ON garage_vehicle TYPE number; +DEFINE FIELD IF NOT EXISTS hit_points ON garage_vehicle TYPE object; +DEFINE FIELD OVERWRITE updated_at ON garage_vehicle TYPE option; +DEFINE INDEX IF NOT EXISTS garage_vehicle_owner ON garage_vehicle COLUMNS uid; +DEFINE INDEX IF NOT EXISTS garage_vehicle_unique ON garage_vehicle COLUMNS uid, plate UNIQUE; +DEFINE TABLE IF NOT EXISTS owned_garage SCHEMALESS; +DEFINE FIELD IF NOT EXISTS uid ON owned_garage TYPE string; +DEFINE FIELD OVERWRITE updated_at ON owned_garage TYPE option; +DEFINE INDEX IF NOT EXISTS owned_garage_uid ON owned_garage COLUMNS uid UNIQUE; +DEFINE TABLE IF NOT EXISTS garage_unlock SCHEMAFULL; +DEFINE FIELD IF NOT EXISTS uid ON garage_unlock TYPE string; +DEFINE FIELD IF NOT EXISTS category ON garage_unlock TYPE string; +DEFINE FIELD IF NOT EXISTS classname ON garage_unlock TYPE string; +DEFINE FIELD IF NOT EXISTS source ON garage_unlock TYPE option; +DEFINE FIELD OVERWRITE unlocked_at ON garage_unlock TYPE option; +DEFINE INDEX IF NOT EXISTS garage_unlock_owner ON garage_unlock COLUMNS uid; +DEFINE INDEX IF NOT EXISTS garage_unlock_unique ON garage_unlock COLUMNS uid, category, classname UNIQUE; +DEFINE TABLE IF NOT EXISTS phone_user SCHEMALESS; +DEFINE FIELD IF NOT EXISTS uid ON phone_user TYPE string; +DEFINE FIELD OVERWRITE updated_at ON phone_user TYPE option; +DEFINE INDEX IF NOT EXISTS phone_user_uid ON phone_user COLUMNS uid UNIQUE; +DEFINE TABLE IF NOT EXISTS phone_contact SCHEMAFULL; +DEFINE FIELD IF NOT EXISTS uid ON phone_contact TYPE string; +DEFINE FIELD IF NOT EXISTS contact_uid ON phone_contact TYPE string; +DEFINE FIELD OVERWRITE created_at ON phone_contact TYPE option; +DEFINE INDEX IF NOT EXISTS phone_contact_owner ON phone_contact COLUMNS uid; +DEFINE INDEX IF NOT EXISTS phone_contact_unique ON phone_contact COLUMNS uid, contact_uid UNIQUE; +DEFINE TABLE IF NOT EXISTS phone_message SCHEMAFULL; +DEFINE FIELD IF NOT EXISTS message_id ON phone_message TYPE string; +DEFINE FIELD IF NOT EXISTS from_uid ON phone_message TYPE string; +DEFINE FIELD IF NOT EXISTS to_uid ON phone_message TYPE string; +DEFINE FIELD IF NOT EXISTS message ON phone_message TYPE string; +DEFINE FIELD IF NOT EXISTS timestamp ON phone_message TYPE number; +DEFINE FIELD OVERWRITE created_at ON phone_message TYPE option; +DEFINE INDEX IF NOT EXISTS phone_message_message_id ON phone_message COLUMNS message_id UNIQUE; +DEFINE TABLE IF NOT EXISTS phone_message_index SCHEMAFULL; +DEFINE FIELD IF NOT EXISTS uid ON phone_message_index TYPE string; +DEFINE FIELD IF NOT EXISTS message_id ON phone_message_index TYPE string; +DEFINE FIELD IF NOT EXISTS is_read ON phone_message_index TYPE bool; +DEFINE FIELD OVERWRITE updated_at ON phone_message_index TYPE option; +DEFINE INDEX IF NOT EXISTS phone_message_index_owner ON phone_message_index COLUMNS uid; +DEFINE INDEX IF NOT EXISTS phone_message_index_message ON phone_message_index COLUMNS message_id; +DEFINE INDEX IF NOT EXISTS phone_message_index_unique ON phone_message_index COLUMNS uid, message_id UNIQUE; +DEFINE TABLE IF NOT EXISTS phone_email SCHEMAFULL; +DEFINE FIELD IF NOT EXISTS email_id ON phone_email TYPE string; +DEFINE FIELD IF NOT EXISTS from_uid ON phone_email TYPE string; +DEFINE FIELD IF NOT EXISTS to_uid ON phone_email TYPE string; +DEFINE FIELD IF NOT EXISTS subject ON phone_email TYPE string; +DEFINE FIELD IF NOT EXISTS body ON phone_email TYPE string; +DEFINE FIELD IF NOT EXISTS timestamp ON phone_email TYPE number; +DEFINE FIELD OVERWRITE created_at ON phone_email TYPE option; +DEFINE INDEX IF NOT EXISTS phone_email_email_id ON phone_email COLUMNS email_id UNIQUE; +DEFINE TABLE IF NOT EXISTS phone_email_index SCHEMAFULL; +DEFINE FIELD IF NOT EXISTS uid ON phone_email_index TYPE string; +DEFINE FIELD IF NOT EXISTS email_id ON phone_email_index TYPE string; +DEFINE FIELD IF NOT EXISTS is_read ON phone_email_index TYPE bool; +DEFINE FIELD OVERWRITE updated_at ON phone_email_index TYPE option; +DEFINE INDEX IF NOT EXISTS phone_email_index_owner ON phone_email_index COLUMNS uid; +DEFINE INDEX IF NOT EXISTS phone_email_index_email ON phone_email_index COLUMNS email_id; +DEFINE INDEX IF NOT EXISTS phone_email_index_unique ON phone_email_index COLUMNS uid, email_id UNIQUE; +DEFINE TABLE IF NOT EXISTS phone_sequence SCHEMALESS; +DEFINE FIELD IF NOT EXISTS sequence_id ON phone_sequence TYPE string; +DEFINE FIELD IF NOT EXISTS value ON phone_sequence TYPE int; +DEFINE INDEX IF NOT EXISTS phone_sequence_id ON phone_sequence COLUMNS sequence_id UNIQUE;