diff --git a/xmtp_mls/migrations/2024-08-07-213816_create-private-preference-store/down.sql b/xmtp_mls/migrations/2024-08-07-213816_create-private-preference-store/down.sql new file mode 100644 index 000000000..dd3043403 --- /dev/null +++ b/xmtp_mls/migrations/2024-08-07-213816_create-private-preference-store/down.sql @@ -0,0 +1,2 @@ +-- This file should undo anything in `up.sql` +DROP TABLE IF EXISTS "consent_records"; diff --git a/xmtp_mls/migrations/2024-08-07-213816_create-private-preference-store/up.sql b/xmtp_mls/migrations/2024-08-07-213816_create-private-preference-store/up.sql new file mode 100644 index 000000000..76e7f7f27 --- /dev/null +++ b/xmtp_mls/migrations/2024-08-07-213816_create-private-preference-store/up.sql @@ -0,0 +1,9 @@ +CREATE TABLE "consent_records"( + -- Enum of the CONSENT_TYPE (GROUP_ID, INBOX_ID, etc..) + "entity_type" int NOT NULL, + -- Enum of CONSENT_STATE (ALLOWED, DENIED, etc..) + "state" int NOT NULL, + -- The entity of what has consent (0x00 etc..) + "entity" text NOT NULL, + PRIMARY KEY (entity_type, entity) +); \ No newline at end of file diff --git a/xmtp_mls/src/storage/encrypted_store/consent_record.rs b/xmtp_mls/src/storage/encrypted_store/consent_record.rs new file mode 100644 index 000000000..d124c3490 --- /dev/null +++ b/xmtp_mls/src/storage/encrypted_store/consent_record.rs @@ -0,0 +1,189 @@ +use crate::{impl_store, storage::StorageError}; + +use super::{ + db_connection::DbConnection, + schema::consent_records::{self, dsl}, +}; +use diesel::{ + backend::Backend, + deserialize::{self, FromSql, FromSqlRow}, + expression::AsExpression, + prelude::*, + serialize::{self, IsNull, Output, ToSql}, + sql_types::Integer, + sqlite::Sqlite, + upsert::excluded, +}; +use serde::{Deserialize, Serialize}; + +/// StoredConsentRecord holds a serialized ConsentRecord +#[derive(Insertable, Queryable, Debug, Clone, PartialEq, Eq)] +#[diesel(table_name = consent_records)] +#[diesel(primary_key(entity_type, entity))] +pub struct StoredConsentRecord { + /// Enum, [`ConsentType`] representing the type of consent (group_id inbox_id, etc..) + pub entity_type: ConsentType, + /// Enum, [`ConsentState`] representing the state of consent (allowed, denied, etc..) + pub state: ConsentState, + /// The entity of what was consented (0x00 etc..) + pub entity: String, +} + +impl StoredConsentRecord { + pub fn new(entity_type: ConsentType, state: ConsentState, entity: String) -> Self { + Self { + entity_type, + state, + entity, + } + } +} + +impl_store!(StoredConsentRecord, consent_records); + +impl DbConnection { + /// Returns the consent_records for the given entity up + pub fn get_consent_record( + &self, + entity: String, + entity_type: ConsentType, + ) -> Result, StorageError> { + Ok(self.raw_query(|conn| { + dsl::consent_records + .filter(dsl::entity.eq(entity)) + .filter(dsl::entity_type.eq(entity_type)) + .first(conn) + .optional() + })?) + } + + /// Insert consent_record, and replace existing entries + pub fn insert_or_replace_consent_record( + &self, + record: StoredConsentRecord, + ) -> Result<(), StorageError> { + self.raw_query(|conn| { + diesel::insert_into(dsl::consent_records) + .values(&record) + .on_conflict((dsl::entity_type, dsl::entity)) + .do_update() + .set(dsl::state.eq(excluded(dsl::state))) + .execute(conn)?; + Ok(()) + })?; + + Ok(()) + } +} + +#[repr(i32)] +#[derive(Debug, Copy, Clone, Serialize, Deserialize, Eq, PartialEq, AsExpression, FromSqlRow)] +#[diesel(sql_type = Integer)] +/// Type of consent record stored +pub enum ConsentType { + /// Consent is for a group + GroupId = 1, + /// Consent is for an inbox + InboxId = 2, + /// Consent is for an address + Address = 3, +} + +impl ToSql for ConsentType +where + i32: ToSql, +{ + fn to_sql<'b>(&'b self, out: &mut Output<'b, '_, Sqlite>) -> serialize::Result { + out.set_value(*self as i32); + Ok(IsNull::No) + } +} + +impl FromSql for ConsentType +where + i32: FromSql, +{ + fn from_sql(bytes: ::RawValue<'_>) -> deserialize::Result { + match i32::from_sql(bytes)? { + 1 => Ok(ConsentType::GroupId), + 2 => Ok(ConsentType::InboxId), + 3 => Ok(ConsentType::Address), + x => Err(format!("Unrecognized variant {}", x).into()), + } + } +} + +#[repr(i32)] +#[derive(Debug, Copy, Clone, Serialize, Deserialize, Eq, PartialEq, AsExpression, FromSqlRow)] +#[diesel(sql_type = Integer)] +/// The state of the consent +pub enum ConsentState { + /// Consent is allowed + Allowed = 1, + /// Consent is denied + Denied = 2, +} + +impl ToSql for ConsentState +where + i32: ToSql, +{ + fn to_sql<'b>(&'b self, out: &mut Output<'b, '_, Sqlite>) -> serialize::Result { + out.set_value(*self as i32); + Ok(IsNull::No) + } +} + +impl FromSql for ConsentState +where + i32: FromSql, +{ + fn from_sql(bytes: ::RawValue<'_>) -> deserialize::Result { + match i32::from_sql(bytes)? { + 1 => Ok(ConsentState::Allowed), + 2 => Ok(ConsentState::Denied), + x => Err(format!("Unrecognized variant {}", x).into()), + } + } +} + +#[cfg(test)] +mod tests { + use crate::storage::encrypted_store::tests::with_connection; + + use super::*; + + fn generate_consent_record( + entity_type: ConsentType, + state: ConsentState, + entity: String, + ) -> StoredConsentRecord { + StoredConsentRecord { + entity_type, + state, + entity, + } + } + + #[test] + fn insert_and_read() { + with_connection(|conn| { + let inbox_id = "inbox_1"; + let consent_record = generate_consent_record( + ConsentType::InboxId, + ConsentState::Denied, + inbox_id.to_string(), + ); + let consent_record_entity = consent_record.entity.clone(); + + conn.insert_or_replace_consent_record(consent_record) + .expect("should store without error"); + + let consent_record = conn + .get_consent_record(inbox_id.to_owned(), ConsentType::InboxId) + .expect("query should work"); + + assert_eq!(consent_record.unwrap().entity, consent_record_entity); + }); + } +} diff --git a/xmtp_mls/src/storage/encrypted_store/mod.rs b/xmtp_mls/src/storage/encrypted_store/mod.rs index f5d2f843d..371f060c6 100644 --- a/xmtp_mls/src/storage/encrypted_store/mod.rs +++ b/xmtp_mls/src/storage/encrypted_store/mod.rs @@ -11,6 +11,7 @@ //! `diesel print-schema` or use `cargo run update-schema` which will update the files for you. pub mod association_state; +pub mod consent_record; pub mod db_connection; pub mod group; pub mod group_intent; diff --git a/xmtp_mls/src/storage/encrypted_store/schema.rs b/xmtp_mls/src/storage/encrypted_store/schema.rs index 84d3c69ec..026969582 100644 --- a/xmtp_mls/src/storage/encrypted_store/schema.rs +++ b/xmtp_mls/src/storage/encrypted_store/schema.rs @@ -8,6 +8,14 @@ diesel::table! { } } +diesel::table! { + consent_records (entity_type, entity) { + entity_type -> Integer, + state -> Integer, + entity -> Text, + } +} + diesel::table! { group_intents (id) { id -> Integer, @@ -94,6 +102,7 @@ diesel::joinable!(group_messages -> groups (group_id)); diesel::allow_tables_to_appear_in_same_query!( association_state, + consent_records, group_intents, group_messages, groups,