Initial commit

This commit is contained in:
Slavi Pantaleev
2024-09-12 13:44:06 +03:00
commit 946aa9d9e9
220 changed files with 26033 additions and 0 deletions

View File

@@ -0,0 +1,181 @@
use std::sync::Arc;
use tokio::sync::Mutex;
use tokio::time::Duration;
use mxlink::helpers::account_data_config::ConfigError;
use super::CatchUpMarkerManager;
/// A service that records roughly until when we're caught up on processing events.
/// Roughly, because we account for potential federation delay and we don't persist the marker too often.
///
/// If the matrix-sdk's state-store is kept intact, we (usually) won't be given the same event twice.
/// In such a happy path, we don't need to keep track of anything and there's no problem.
///
/// If the state-store is lost (a very rare, but possible event), we can recover our encryption keys, etc.,
/// but the Matrix SDK would try to feed us the same events again.
/// Responding to many old events again is annoying to users and can be a huge waste of resources.
///
/// In order to handle state-store-loss better, we need to record until when we're caught up in storage that won't get lost (such as account data for the user).
/// Because state-store-loss is a very rare event, we don't need to be very exact about the specific timestamp we're caught up to.
/// In fact, being behind is necessary, to allow for federation delay (see `federation_delay_tolerance_duration`).
pub struct DelayedCatchUpMarkerManager {
catch_up_marker_manager: Arc<Mutex<CatchUpMarkerManager>>,
/// `persist_interval_duration` affects how often we persist the catch-up marker to Account Data
/// A too small value means there's needless overhead.
/// The downside to a larger interval value (and a larger federation delay tolerance value) is that that a state-store loss will mean that
/// we will reprocess some of the same events.
/// Since this is a very rare event and the downside is not so bad, a large value is recommended.
persist_interval_duration: Duration,
/// `federation_delay_tolerance_duration` affects what federation delay we will tolerate.
/// A larger delay than this may mean we ignore events that are actually new to us.
/// This is necessary because the timestamp given to us (see `catch_up()`) is based on the "origin server" timestamp.
/// If federation is slow, we may actually receive old events later on - they'd still be new to us,
/// but we may ignore them if we've marked this "origin server timestamp" value as "caught up".
federation_delay_tolerance_duration: Duration,
/// Holds the timestamp to use for updating the catch-up marker's `caught_up_until_event_origin_server_ts_millis`.
/// A value of `0` is used to indicate that no update is scheduled and the next iteration should skip updating the marker.
next_catch_up_marker_event_origin_server_ts_millis: Arc<tokio::sync::Mutex<i64>>,
}
impl DelayedCatchUpMarkerManager {
pub fn new(
catch_up_marker_manager: CatchUpMarkerManager,
persist_interval_duration: Duration,
federation_delay_tolerance_duration: Duration,
) -> Self {
let next_catch_up_marker_event_origin_server_ts_millis =
Arc::new(tokio::sync::Mutex::new(0));
let catch_up_marker_manager = Arc::new(Mutex::new(catch_up_marker_manager));
Self {
catch_up_marker_manager,
persist_interval_duration,
federation_delay_tolerance_duration,
next_catch_up_marker_event_origin_server_ts_millis,
}
}
#[tracing::instrument(name = "catch_up", skip(self))]
pub async fn catch_up(&self, event_origin_server_ts_millis: i64) {
tracing::trace!("Locking to catch-up..");
let mut next_catch_up_marker_event_origin_server_ts_millis_guard = self
.next_catch_up_marker_event_origin_server_ts_millis
.lock()
.await;
if *next_catch_up_marker_event_origin_server_ts_millis_guard > event_origin_server_ts_millis
{
tracing::trace!(
?next_catch_up_marker_event_origin_server_ts_millis_guard,
"Already have a more recent timestamp scheduled",
);
return;
}
*next_catch_up_marker_event_origin_server_ts_millis_guard = event_origin_server_ts_millis;
tracing::info!("Configured catch-up timestamp for the next update");
}
/// Tells if we're caught up until the given timestamp.
///
/// This intentionally uses the latest (cached) data stored in catch_up_marker_manager (Account Data), not the `next_catch_up_marker_event_origin_server_ts_millis` value.
/// `next_catch_up_marker_event_origin_server_ts_millis` is used for scheduling the next update only.
/// The actual timestamp that will get persisted durign the update will actually be adjusted by `federation_delay_tolerance_duration`,
/// so comparing against `next_catch_up_marker_event_origin_server_ts_millis` in its raw form would be incorrect.
#[tracing::instrument(name = "is_caught_up", skip(self))]
pub(crate) async fn is_caught_up(
&self,
event_origin_ts_millis: i64,
) -> Result<bool, ConfigError> {
tracing::trace!("Locking to check if caught up..");
let mut manager = self.catch_up_marker_manager.lock().await;
let marker = manager.get_or_create().await?;
let is_caught_up =
marker.caught_up_until_event_origin_server_ts_millis >= event_origin_ts_millis;
tracing::debug!(
?is_caught_up,
?marker.caught_up_until_event_origin_server_ts_millis,
"Determined caught-up status"
);
Ok(is_caught_up)
}
pub async fn start(&self) {
let inner = Arc::clone(&self.catch_up_marker_manager);
let persist_interval_duration = self.persist_interval_duration;
let federation_delay_tolerance = self.federation_delay_tolerance_duration;
let next_catch_up_marker_event_origin_server_ts_millis =
Arc::clone(&self.next_catch_up_marker_event_origin_server_ts_millis);
tokio::spawn(async move {
let mut interval = tokio::time::interval(persist_interval_duration);
loop {
interval.tick().await;
tracing::trace!("Catch-up manager doing work..");
let mut next_catch_up_marker_event_origin_server_ts_millis_guard =
next_catch_up_marker_event_origin_server_ts_millis
.lock()
.await;
if *next_catch_up_marker_event_origin_server_ts_millis_guard == 0 {
tracing::trace!("No scheduled updates to the catch-up marker");
continue;
}
let mut manager = inner.lock().await;
let marker = manager.get_or_create().await;
let mut marker = match marker {
Ok(marker) => marker,
Err(err) => {
tracing::error!(?err, "Failed to get or create catch-up marker");
continue;
}
};
// To allow for some federation delay (specified in federation_delay_tolerance),
// we adjust the value we'll actually persist with that delay duration.
// For more information, see the documentation for `Self`.
let caught_up_until_event_origin_server_ts_millis =
*next_catch_up_marker_event_origin_server_ts_millis_guard
- (federation_delay_tolerance.as_millis() as i64);
marker.caught_up_until_event_origin_server_ts_millis =
caught_up_until_event_origin_server_ts_millis;
tracing::debug!(
?caught_up_until_event_origin_server_ts_millis,
next_catch_up_marker_event_origin_server_ts_millis = format!(
"{:?}",
next_catch_up_marker_event_origin_server_ts_millis_guard
),
"Updating catch-up marker..",
);
let result = manager.persist(&marker).await;
if let Err(err) = result {
tracing::error!(?err, "Failed to persist catch-up marker");
}
*next_catch_up_marker_event_origin_server_ts_millis_guard = 0;
}
});
}
}

View File

@@ -0,0 +1,37 @@
use mxlink::matrix_sdk::ruma::events::macros::EventContent;
use serde::{Deserialize, Serialize};
use mxlink::helpers::account_data_config::GlobalConfig;
use mxlink::helpers::account_data_config::GlobalConfigCarrierContent;
#[derive(Clone, Debug, Default, Deserialize, Serialize, EventContent)]
#[ruma_event(type = "cc.etke.baibot.catch_up_marker", kind = GlobalAccountData)]
pub struct CatchUpMarkerCarrierContent {
pub payload: String,
}
impl GlobalConfigCarrierContent for CatchUpMarkerCarrierContent {
fn payload(&self) -> &str {
&self.payload
}
fn new(payload: String) -> Self {
Self { payload }
}
}
#[derive(Clone, Debug, Default, Deserialize, Serialize)]
pub struct CatchUpMarker {
pub caught_up_until_event_origin_server_ts_millis: i64,
}
impl CatchUpMarker {
pub fn new(caught_up_until_event_origin_server_ts_millis: i64) -> Self {
Self {
caught_up_until_event_origin_server_ts_millis,
}
}
}
impl GlobalConfig for CatchUpMarker {}

View File

@@ -0,0 +1,11 @@
mod delayed_catch_up_marker_manager;
mod entity;
use mxlink::helpers::account_data_config::GlobalConfigManager as AccountDataGlobalConfigManager;
pub use entity::{CatchUpMarker, CatchUpMarkerCarrierContent};
pub type CatchUpMarkerManager =
AccountDataGlobalConfigManager<CatchUpMarker, CatchUpMarkerCarrierContent>;
pub use delayed_catch_up_marker_manager::DelayedCatchUpMarkerManager;

401
src/entity/cfg/config.rs Normal file
View File

@@ -0,0 +1,401 @@
use std::path::PathBuf;
use mxlink::helpers::encryption::EncryptionKey;
use serde::{Deserialize, Serialize};
use crate::{
agent::{AgentDefinition, AgentPurpose, PublicIdentifier},
entity::{globalconfig::GlobalConfig, roomconfig::RoomSettingsHandler},
};
#[derive(Debug, Deserialize)]
pub struct Config {
pub homeserver: ConfigHomeserver,
pub user: ConfigUser,
pub persistence: PersistenceConfig,
#[serde(default = "super::defaults::command_prefix")]
pub command_prefix: String,
pub access: ConfigAccess,
pub agents: ConfigAgents,
// Contains the initial global configuration values.
// Not all properties of the object make sense to be configured statically,
// so not all of them will be reflected onto the actual global configuration.
pub initial_global_config: ConfigInitialGlobalConfig,
#[serde(default = "super::defaults::logging")]
pub logging: String,
}
impl Config {
pub fn validate(&self) -> anyhow::Result<()> {
self.homeserver.validate()?;
self.user.validate()?;
self.persistence.validate()?;
self.access.validate()?;
if self.command_prefix.is_empty() {
return Err(anyhow::anyhow!(
"The command_prefix ({}) configuration must be set",
super::env::BAIBOT_COMMAND_PREFIX
));
}
self.agents.validate()?;
self.initial_global_config.clone().validate()?;
Ok(())
}
}
#[derive(Debug, Serialize, Deserialize)]
pub struct ConfigHomeserver {
pub server_name: String,
pub url: String,
}
impl ConfigHomeserver {
pub fn validate(&self) -> anyhow::Result<()> {
if self.server_name.is_empty() {
return Err(anyhow::anyhow!(
"The homeserver.server_name ({}) configuration must be set",
super::env::BAIBOT_HOMESERVER_SERVER_NAME
));
}
if self.url.is_empty() {
return Err(anyhow::anyhow!(
"The homeserver.url ({}) configuration must be set",
super::env::BAIBOT_HOMESERVER_URL
));
}
Ok(())
}
}
#[derive(Debug, Serialize, Deserialize)]
pub struct ConfigUser {
pub mxid_localpart: String,
pub password: String,
#[serde(default = "super::defaults::name")]
pub name: String,
#[serde(default)]
pub encryption: ConfigUserEncryption,
}
impl ConfigUser {
pub fn validate(&self) -> anyhow::Result<()> {
if self.mxid_localpart.is_empty() {
return Err(anyhow::anyhow!(
"The user.mxid_localpart ({}) configuration must be set",
super::env::BAIBOT_USER_MXID_LOCALPART
));
}
if self.password.is_empty() {
return Err(anyhow::anyhow!(
"The user.password ({}) configuration must be set",
super::env::BAIBOT_USER_PASSWORD
));
}
if self.name.is_empty() {
return Err(anyhow::anyhow!(
"The name ({}) configuration must be set",
super::env::BAIBOT_USER_NAME
));
}
self.encryption.validate()?;
Ok(())
}
}
#[derive(Debug, Default, Serialize, Deserialize)]
pub struct ConfigUserEncryption {
pub recovery_passphrase: Option<String>,
pub recovery_reset_allowed: bool,
}
impl ConfigUserEncryption {
pub fn validate(&self) -> anyhow::Result<()> {
if let Some(passphrase) = &self.recovery_passphrase {
if passphrase.is_empty() {
return Err(anyhow::anyhow!(
"The user.encryption.recovery_passphrase ({}) configuration must either be null or set to a non-empty passphrase",
super::env::BAIBOT_USER_ENCRYPTION_RECOVERY_PASSPHRASE
));
}
}
Ok(())
}
}
#[derive(Debug, Serialize, Deserialize)]
pub struct PersistenceConfig {
#[serde(default = "super::defaults::persistence_data_dir_path")]
pub data_dir_path: Option<String>,
#[serde(default = "super::defaults::persistence_session_file_name")]
session_file_name: String,
#[serde(default = "super::defaults::persistence_db_dir_name")]
db_dir_name: String,
pub session_encryption_key: Option<String>,
pub config_encryption_key: Option<String>,
}
impl PersistenceConfig {
pub fn validate(&self) -> anyhow::Result<()> {
if let Some(data_dir_path) = &self.data_dir_path {
let path = PathBuf::from(data_dir_path);
if !path.exists() {
return Err(anyhow::anyhow!(
"The persistence.data_dir_path ({}) directory ({}) must exist",
super::env::BAIBOT_PERSISTENCE_DATA_DIR_PATH,
data_dir_path,
));
}
}
self.config_encryption_key()
.map_err(|e| anyhow::anyhow!(e))?;
Ok(())
}
pub fn session_file_path(&self) -> anyhow::Result<PathBuf> {
let Some(data_dir_path) = &self.data_dir_path else {
return Err(anyhow::anyhow!(
"The persistence.data_dir_path ({}) directory must be set",
super::env::BAIBOT_PERSISTENCE_DATA_DIR_PATH
));
};
let mut path = PathBuf::from(data_dir_path);
path.push(&self.session_file_name);
Ok(path)
}
pub fn db_dir_path(&self) -> anyhow::Result<PathBuf> {
let Some(data_dir_path) = &self.data_dir_path else {
return Err(anyhow::anyhow!(
"The persistence.data_dir_path ({}) directory must be set",
super::env::BAIBOT_PERSISTENCE_DATA_DIR_PATH
));
};
let mut path = PathBuf::from(data_dir_path);
path.push(&self.db_dir_name);
Ok(path)
}
pub fn session_encryption_key(&self) -> anyhow::Result<Option<EncryptionKey>> {
self.parse_encryption_key(&self.session_encryption_key).map_err(|err| {
anyhow::anyhow!(
"Encryption key specified in persistence.session_encryption_key ({}) is not valid: {}",
super::env::BAIBOT_PERSISTENCE_SESSION_ENCRYPTION_KEY,
err
)
})
}
pub fn config_encryption_key(&self) -> anyhow::Result<Option<EncryptionKey>> {
self.parse_encryption_key(&self.config_encryption_key).map_err(|err| {
anyhow::anyhow!(
"Encryption key specified in persistence.config_encryption_key ({}) is not valid: {}",
super::env::BAIBOT_PERSISTENCE_CONFIG_ENCRYPTION_KEY,
err
)
})
}
fn parse_encryption_key(
&self,
value: &Option<String>,
) -> anyhow::Result<Option<EncryptionKey>, String> {
let key = match value {
Some(key) => {
if key.is_empty() {
None
} else {
Some(EncryptionKey::from_hex_str(key)?)
}
}
None => None,
};
Ok(key)
}
}
#[derive(Debug, Serialize, Deserialize)]
pub struct ConfigAccess {
// Contains the admin whitelist patterns before parsing into regex.
// Example: `["@*:example.com"]`
pub admin_patterns: Vec<String>,
}
impl ConfigAccess {
// Returns the the mxidwc-parsed regexes for the admin whitelist.
// Example: `["^@\.*:example\.com$"]`
pub fn admin_pattern_regexes(&self) -> anyhow::Result<Vec<regex::Regex>> {
mxidwc::parse_patterns_vector(&self.admin_patterns).map_err(|e| {
anyhow::anyhow!(
"Failed parsing access.admin_patterns ({}): {:?}",
super::env::BAIBOT_ACCESS_ADMIN_PATTERNS,
e
)
})
}
pub fn validate(&self) -> anyhow::Result<()> {
if self.admin_patterns.is_empty() {
return Err(anyhow::anyhow!(
"The access.admin_patterns ({}) configuration must contain at least one pattern",
super::env::BAIBOT_ACCESS_ADMIN_PATTERNS
));
}
self.admin_pattern_regexes()?;
Ok(())
}
}
#[derive(Debug, Serialize, Deserialize)]
pub struct ConfigAgents {
pub static_definitions: Vec<AgentDefinition>,
}
impl ConfigAgents {
pub fn validate(&self) -> anyhow::Result<()> {
Ok(())
}
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct ConfigInitialGlobalConfig {
#[serde(default)]
pub handler: RoomSettingsHandler,
pub user_patterns: Option<Vec<String>>,
}
impl ConfigInitialGlobalConfig {
fn user_pattern_regexes(&self) -> anyhow::Result<Option<Vec<regex::Regex>>> {
match &self.user_patterns {
Some(user_patterns) => {
let user_patterns = mxidwc::parse_patterns_vector(user_patterns).map_err(|e| {
anyhow::anyhow!(
"Failed parsing initial_global_config.user_patterns ({}): {}",
super::env::BAIBOT_INITIAL_GLOBAL_CONFIG_USER_PATTERNS,
e
)
})?;
Ok(Some(user_patterns))
}
None => Ok(None),
}
}
pub fn validate(self) -> anyhow::Result<()> {
self.user_pattern_regexes()?;
for purpose in AgentPurpose::choices() {
let agent_id = self.handler.get_by_purpose(*purpose);
let Some(agent_id) = agent_id else {
// None is OK
continue;
};
let config_key = format!(
"initial_global_config.handler.{}",
purpose.as_str().replace("-", "_")
);
if agent_id.is_empty() {
return Err(anyhow::anyhow!(
"The {} configuration key must be pointing to a valid agent id or be set to null",
config_key,
));
}
let agent_identifier = PublicIdentifier::from_str(&agent_id);
let Some(agent_identifier) = agent_identifier else {
return Err(anyhow::anyhow!(
"The {} configuration key specifies an agent id (`{}`) that cannot be parsed. {}",
config_key,
agent_id,
crate::strings::agent::invalid_id_generic()
));
};
// We only allow statically-defined agents for now, although DynamicGlobal may make sense too.
let PublicIdentifier::Static(_) = agent_identifier else {
return Err(anyhow::anyhow!(
"The {} configuration key specifies an agent id (`{}`) which does not refer to a static agent.",
config_key,
agent_id,
));
};
}
let _: GlobalConfig = self.try_into()?;
Ok(())
}
}
impl TryInto<GlobalConfig> for ConfigInitialGlobalConfig {
type Error = anyhow::Error;
fn try_into(self) -> anyhow::Result<GlobalConfig> {
let mut entity = GlobalConfig::default();
if let Some(user_patterns) = self.user_patterns {
// We'd rather fail parsing this during startup than at runtime
let _ = mxidwc::parse_patterns_vector(&user_patterns).map_err(|err| {
anyhow::anyhow!(
"Bad initial_global_config.user_patterns ({}): {}",
super::env::BAIBOT_INITIAL_GLOBAL_CONFIG_USER_PATTERNS,
err
)
})?;
entity.access.user_patterns = if user_patterns.is_empty() {
None
} else {
Some(user_patterns)
};
}
for purpose in AgentPurpose::choices() {
let agent_id = self.handler.get_by_purpose(*purpose);
entity
.fallback_room_settings
.handler
.set_by_purpose(*purpose, agent_id);
}
Ok(entity)
}
}

View File

@@ -0,0 +1,35 @@
const CONFIG_FILE_PATH: &str = "config.yml";
const NAME: &str = "baibot";
const COMMAND_PREFIX: &str = "!bai";
const PERSISTENCE_SESSION_FILE_NAME: &str = "session.json";
const PERSISTENCE_DB_DIR_NAME: &str = "db";
pub(crate) fn name() -> String {
NAME.to_owned()
}
pub(crate) fn config_file_path() -> String {
CONFIG_FILE_PATH.to_owned()
}
pub(super) fn command_prefix() -> String {
COMMAND_PREFIX.to_owned()
}
pub(super) fn persistence_data_dir_path() -> Option<String> {
None
}
pub(super) fn persistence_session_file_name() -> String {
PERSISTENCE_SESSION_FILE_NAME.to_owned()
}
pub(super) fn persistence_db_dir_name() -> String {
PERSISTENCE_DB_DIR_NAME.to_owned()
}
pub(super) fn logging() -> String {
"warn,mxlink=debug,baibot=debug".to_owned()
}

38
src/entity/cfg/env.rs Normal file
View File

@@ -0,0 +1,38 @@
pub const BAIBOT_CONFIG_FILE_PATH: &str = "BAIBOT_CONFIG_FILE_PATH";
pub const BAIBOT_HOMESERVER_SERVER_NAME: &str = "BAIBOT_HOMESERVER_SERVER_NAME";
pub const BAIBOT_HOMESERVER_URL: &str = "BAIBOT_HOMESERVER_URL";
pub const BAIBOT_USER_MXID_LOCALPART: &str = "BAIBOT_USER_MXID_LOCALPART";
pub const BAIBOT_USER_PASSWORD: &str = "BAIBOT_USER_PASSWORD";
pub const BAIBOT_USER_NAME: &str = "BAIBOT_USER_NAME";
pub const BAIBOT_USER_ENCRYPTION_RECOVERY_PASSPHRASE: &str =
"BAIBOT_USER_ENCRYPTION_RECOVERY_PASSPHRASE";
pub const BAIBOT_COMMAND_PREFIX: &str = "BAIBOT_COMMAND_PREFIX";
pub const BAIBOT_LOGGING: &str = "BAIBOT_LOGGING";
pub const BAIBOT_ACCESS_ADMIN_PATTERNS: &str = "BAIBOT_ACCESS_ADMIN_PATTERNS";
pub const BAIBOT_INITIAL_GLOBAL_CONFIG_HANDLER_CATCH_ALL: &str =
"BAIBOT_INITIAL_GLOBAL_CONFIG_HANDLER_CATCH_ALL";
pub const BAIBOT_INITIAL_GLOBAL_CONFIG_HANDLER_TEXT_GENERATION: &str =
"BAIBOT_INITIAL_GLOBAL_CONFIG_HANDLER_TEXT_GENERATION";
pub const BAIBOT_INITIAL_GLOBAL_CONFIG_HANDLER_TEXT_TO_SPEECH: &str =
"BAIBOT_INITIAL_GLOBAL_CONFIG_HANDLER_TEXT_TO_SPEECH";
pub const BAIBOT_INITIAL_GLOBAL_CONFIG_HANDLER_SPEECH_TO_TEXT: &str =
"BAIBOT_INITIAL_GLOBAL_CONFIG_HANDLER_SPEECH_TO_TEXT";
pub const BAIBOT_INITIAL_GLOBAL_CONFIG_HANDLER_IMAGE_GENERATION: &str =
"BAIBOT_INITIAL_GLOBAL_CONFIG_HANDLER_IMAGE_GENERATION";
pub const BAIBOT_INITIAL_GLOBAL_CONFIG_USER_PATTERNS: &str =
"BAIBOT_INITIAL_GLOBAL_CONFIG_USER_PATTERNS";
pub const BAIBOT_PERSISTENCE_DATA_DIR_PATH: &str = "BAIBOT_PERSISTENCE_DATA_DIR_PATH";
pub const BAIBOT_PERSISTENCE_SESSION_ENCRYPTION_KEY: &str =
"BAIBOT_PERSISTENCE_SESSION_ENCRYPTION_KEY";
pub const BAIBOT_PERSISTENCE_CONFIG_ENCRYPTION_KEY: &str =
"BAIBOT_PERSISTENCE_CONFIG_ENCRYPTION_KEY";

5
src/entity/cfg/mod.rs Normal file
View File

@@ -0,0 +1,5 @@
mod config;
pub mod defaults;
pub mod env;
pub use config::Config;

View File

@@ -0,0 +1,64 @@
use mxlink::matrix_sdk::ruma::events::macros::EventContent;
use serde::{Deserialize, Serialize};
use mxlink::helpers::account_data_config::GlobalConfig as GlobalConfigTrait;
use mxlink::helpers::account_data_config::GlobalConfigCarrierContent as GlobalConfigCarrierContentTrait;
use crate::agent::AgentDefinition;
use crate::entity::roomconfig::RoomSettings;
#[derive(Clone, Debug, Default, Deserialize, Serialize, EventContent)]
#[ruma_event(type = "cc.etke.baibot.global_config", kind = GlobalAccountData)]
pub struct GlobalConfigCarrierContent {
pub payload: String,
}
impl GlobalConfigCarrierContentTrait for GlobalConfigCarrierContent {
fn payload(&self) -> &str {
&self.payload
}
fn new(payload: String) -> Self {
Self { payload }
}
}
#[derive(Clone, Debug, Default, Deserialize, Serialize)]
pub struct GlobalConfig {
pub fallback_room_settings: RoomSettings,
pub access: GlobalConfigAccess,
pub agents: Vec<AgentDefinition>,
}
impl GlobalConfig {
pub fn new(user_patterns: Option<Vec<String>>) -> Self {
Self {
fallback_room_settings: RoomSettings::default(),
access: GlobalConfigAccess {
user_patterns,
room_local_agent_manager_patterns: None,
},
agents: vec![],
}
}
}
impl GlobalConfigTrait for GlobalConfig {}
#[derive(Clone, Debug, Default, Deserialize, Serialize)]
pub struct GlobalConfigAccess {
// Contains a list of patterns that will be used to specify the "allowed bot users".
// These remain as patterns and are turned into regex and made use of on demand.
// Example: `["@*:example.com"]`
pub user_patterns: Option<Vec<String>>,
// Contains a list of patterns that will be used to specify "allowed room-local agent managers".
// These remain as patterns and are turned into regex and made use of on demand.
// Example: `["@*:example.com"]`
pub room_local_agent_manager_patterns: Option<Vec<String>>,
}

View File

@@ -0,0 +1,8 @@
mod entity;
use mxlink::helpers::account_data_config::GlobalConfigManager as AccountDataGlobalConfigManager;
pub use entity::{GlobalConfig, GlobalConfigCarrierContent};
pub type GlobalConfigurationManager =
AccountDataGlobalConfigManager<GlobalConfig, GlobalConfigCarrierContent>;

View File

@@ -0,0 +1,122 @@
use mxlink::matrix_sdk::ruma::{OwnedEventId, OwnedUserId, RoomId};
use mxlink::matrix_sdk::Room;
use mxlink::ThreadInfo;
use super::{
globalconfig::GlobalConfig, roomconfig::RoomConfig, MessagePayload, RoomConfigContext,
TriggerEventInfo,
};
#[derive(Debug)]
pub struct MessageContext {
room: Room,
room_config_context: RoomConfigContext,
admin_whitelist_regexes: Vec<regex::Regex>,
trigger_event_info: TriggerEventInfo,
thread_info: ThreadInfo,
}
impl MessageContext {
pub fn new(
room: Room,
room_config_context: RoomConfigContext,
admin_whitelist_regexes: Vec<regex::Regex>,
trigger_event_info: TriggerEventInfo,
thread_info: ThreadInfo,
) -> Self {
Self {
room,
room_config_context,
admin_whitelist_regexes,
trigger_event_info,
thread_info,
}
}
pub fn room(&self) -> &Room {
&self.room
}
pub fn room_id(&self) -> &RoomId {
self.room.room_id()
}
pub fn global_config(&self) -> &GlobalConfig {
&self.room_config_context.global_config
}
pub fn room_config(&self) -> &RoomConfig {
&self.room_config_context.room_config
}
pub fn room_config_context(&self) -> &RoomConfigContext {
&self.room_config_context
}
pub fn event_id(&self) -> &OwnedEventId {
&self.trigger_event_info.event_id
}
pub fn sender_id(&self) -> &OwnedUserId {
&self.trigger_event_info.sender
}
pub fn payload(&self) -> &MessagePayload {
&self.trigger_event_info.payload
}
pub fn thread_info(&self) -> &ThreadInfo {
&self.thread_info
}
pub fn sender_can_manage_global_config(&self) -> anyhow::Result<bool> {
Ok(self.trigger_event_info.sender_is_admin)
}
pub fn sender_can_manage_room_local_agents(&self) -> anyhow::Result<bool> {
Ok(self.sender_can_manage_global_config()?
|| self.sender_is_allowed_room_local_agent_manager()?)
}
pub fn combined_admin_and_user_regexes(&self) -> Vec<regex::Regex> {
let mut combined = self.admin_whitelist_regexes.clone();
if let Some(user_patterns) = &self.global_config().access.user_patterns {
let user_regexes = mxidwc::parse_patterns_vector(user_patterns);
match user_regexes {
Ok(user_regexes) => {
combined.extend(user_regexes);
}
Err(err) => {
tracing::warn!(
"Error parsing user patterns for room {}: {:?}",
self.room.room_id(),
err
);
}
}
}
combined
}
fn sender_is_allowed_room_local_agent_manager(&self) -> anyhow::Result<bool> {
match &self
.global_config()
.access
.room_local_agent_manager_patterns
{
None => Ok(false),
Some(patterns) => {
let allowed_regexes = mxidwc::parse_patterns_vector(patterns)?;
Ok(mxidwc::match_user_id(
self.sender_id().as_str(),
&allowed_regexes,
))
}
}
}
}

View File

@@ -0,0 +1,47 @@
use mxlink::matrix_sdk::ruma::events::room::message::{
AudioMessageEventContent, MessageType, TextMessageEventContent,
};
use mxlink::matrix_sdk::ruma::{OwnedEventId, OwnedUserId};
use mxlink::ThreadInfo;
/// MessagePayload is like matrix-sdk's MessageType, but represents only message types that the bot deals with and payloads are massaged a bit.
#[derive(Debug, Clone)]
pub enum MessagePayload {
Text(TextMessageEventContent),
Audio(AudioMessageEventContent),
Reaction {
key: String,
reacted_to_event_payload: Box<Self>,
reacted_to_event_id: OwnedEventId,
reacted_to_event_sender_id: OwnedUserId,
},
/// Represents an encrypted message
Encrypted(ThreadInfo),
}
impl TryInto<MessagePayload> for MessageType {
type Error = String;
fn try_into(self) -> Result<MessagePayload, Self::Error> {
let payload = match self {
MessageType::Text(text_content) => MessagePayload::Text(text_content),
MessageType::Audio(audio_content) => {
// We can consider inspecting `audio_content.voice.is_some()` and ignoring audio which is not a voice message.
//
// However, at the time of this writing (2024-09-10), certain popular clients (Element iOS) send voice messages
// as regular audio messages, without voice annotation as per MSC3245.
// For this reason, we handle all audio.
MessagePayload::Audio(audio_content)
}
other => {
return Err(format!("Unsupported message type: {:?}", other));
}
};
Ok(payload)
}
}

15
src/entity/mod.rs Normal file
View File

@@ -0,0 +1,15 @@
pub mod catch_up_marker;
pub mod cfg;
pub mod globalconfig;
mod message_context;
mod message_payload;
mod room_config_context;
pub mod roomconfig;
mod thread_context;
mod trigger_event_info;
pub use message_context::MessageContext;
pub use message_payload::MessagePayload;
pub use room_config_context::RoomConfigContext;
pub use thread_context::{ThreadContext, ThreadContextFirstMessage};
pub use trigger_event_info::TriggerEventInfo;

View File

@@ -0,0 +1,183 @@
use super::globalconfig::GlobalConfig;
use super::roomconfig::RoomConfig;
use crate::entity::roomconfig::{
defaults as roomconfig_defaults, SpeechToTextFlowType, TextGenerationAutoUsage,
TextGenerationPrefixRequirementType, TextToSpeechBotMessagesFlowType,
TextToSpeechUserMessagesFlowType,
};
#[derive(Debug)]
pub struct RoomConfigContext {
pub(crate) global_config: GlobalConfig,
pub(crate) room_config: RoomConfig,
}
impl RoomConfigContext {
pub fn new(global_config: GlobalConfig, room_config: RoomConfig) -> RoomConfigContext {
Self {
global_config,
room_config,
}
}
pub fn speech_to_text_flow_type(&self) -> SpeechToTextFlowType {
self.room_config
.settings
.speech_to_text
.flow_type
.or({
self.global_config
.fallback_room_settings
.speech_to_text
.flow_type
})
.unwrap_or(roomconfig_defaults::SPEECH_TO_TEXT_FLOW_TYPE)
}
pub fn speech_to_text_language(&self) -> Option<String> {
self.room_config
.settings
.speech_to_text
.language
.clone()
.or({
self.global_config
.fallback_room_settings
.speech_to_text
.language
.clone()
})
}
pub fn auto_text_generation_usage(&self) -> TextGenerationAutoUsage {
self.room_config
.settings
.text_generation
.auto_usage
.or({
self.global_config
.fallback_room_settings
.text_generation
.auto_usage
})
.unwrap_or(roomconfig_defaults::TEXT_GENERATION_AUTO_USAGE)
}
pub fn should_auto_text_generate(&self, original_message_is_audio: bool) -> bool {
match self.auto_text_generation_usage() {
TextGenerationAutoUsage::Never => false,
TextGenerationAutoUsage::Always => true,
TextGenerationAutoUsage::OnlyForVoice => original_message_is_audio,
TextGenerationAutoUsage::OnlyForText => !original_message_is_audio,
}
}
pub fn text_generation_prompt_override(&self) -> Option<String> {
self.room_config
.settings
.text_generation
.prompt_override
.clone()
.or_else(|| {
self.global_config
.fallback_room_settings
.text_generation
.prompt_override
.clone()
})
}
pub fn text_generation_temperature_override(&self) -> Option<f32> {
self.room_config
.settings
.text_generation
.temperature_override
.or({
self.global_config
.fallback_room_settings
.text_generation
.temperature_override
})
}
pub fn text_generation_context_management_enabled(&self) -> bool {
self.room_config
.settings
.text_generation
.context_management_enabled
.or({
self.global_config
.fallback_room_settings
.text_generation
.context_management_enabled
})
.unwrap_or(false)
}
pub fn text_generation_prefix_requirement_type(&self) -> TextGenerationPrefixRequirementType {
self.room_config
.settings
.text_generation
.prefix_requirement_type
.or({
self.global_config
.fallback_room_settings
.text_generation
.prefix_requirement_type
})
.unwrap_or(roomconfig_defaults::TEXT_GENERATION_PREFIX_REQUIREMENT_TYPE)
}
pub fn text_to_speech_bot_messages_flow_type(&self) -> TextToSpeechBotMessagesFlowType {
self.room_config
.settings
.text_to_speech
.bot_msgs_flow_type
.or({
self.global_config
.fallback_room_settings
.text_to_speech
.bot_msgs_flow_type
})
.unwrap_or(roomconfig_defaults::TEXT_TO_SPEECH_BOT_MESSAGES_FLOW_TYPE)
}
pub fn text_to_speech_user_messages_flow_type(&self) -> TextToSpeechUserMessagesFlowType {
self.room_config
.settings
.text_to_speech
.user_msgs_flow_type
.or({
self.global_config
.fallback_room_settings
.text_to_speech
.user_msgs_flow_type
})
.unwrap_or(roomconfig_defaults::TEXT_TO_SPEECH_USER_MESSAGES_FLOW_TYPE)
}
pub fn text_to_speech_speed_override(&self) -> Option<f32> {
self.room_config.settings.text_to_speech.speed_override.or({
self.global_config
.fallback_room_settings
.text_to_speech
.speed_override
})
}
pub fn text_to_speech_voice_override(&self) -> Option<String> {
self.room_config
.settings
.text_to_speech
.voice_override
.clone()
.or_else(|| {
self.global_config
.fallback_room_settings
.text_to_speech
.voice_override
.clone()
})
}
}

View File

@@ -0,0 +1,17 @@
use super::SpeechToTextFlowType;
use super::{TextGenerationAutoUsage, TextGenerationPrefixRequirementType};
use super::{TextToSpeechBotMessagesFlowType, TextToSpeechUserMessagesFlowType};
pub const TEXT_GENERATION_PREFIX_REQUIREMENT_TYPE: TextGenerationPrefixRequirementType =
TextGenerationPrefixRequirementType::No;
pub const TEXT_GENERATION_AUTO_USAGE: TextGenerationAutoUsage = TextGenerationAutoUsage::Always;
pub const TEXT_TO_SPEECH_BOT_MESSAGES_FLOW_TYPE: TextToSpeechBotMessagesFlowType =
TextToSpeechBotMessagesFlowType::OnDemandForVoice;
pub const TEXT_TO_SPEECH_USER_MESSAGES_FLOW_TYPE: TextToSpeechUserMessagesFlowType =
TextToSpeechUserMessagesFlowType::OnDemand;
pub const SPEECH_TO_TEXT_FLOW_TYPE: SpeechToTextFlowType =
SpeechToTextFlowType::TranscribeAndGenerateText;

View File

@@ -0,0 +1,60 @@
use serde::{Deserialize, Serialize};
use crate::agent::AgentPurpose;
#[derive(Clone, Debug, Default, Deserialize, Serialize)]
pub struct RoomSettingsHandler {
/// The agent used for any of the tasks which do not have a dedicated agent for them
catch_all: Option<String>,
/// The agent used for text generation
text_generation: Option<String>,
/// The agent used for transcribing audio (voice) to text
speech_to_text: Option<String>,
/// The agent used for converting text to audio (voice)
text_to_speech: Option<String>,
/// The agent used for generating images
image_generation: Option<String>,
}
impl RoomSettingsHandler {
pub fn get_by_purpose(&self, purpose: AgentPurpose) -> Option<String> {
match purpose {
AgentPurpose::CatchAll => self.catch_all.clone(),
AgentPurpose::TextGeneration => self.text_generation.clone(),
AgentPurpose::SpeechToText => self.speech_to_text.clone(),
AgentPurpose::TextToSpeech => self.text_to_speech.clone(),
AgentPurpose::ImageGeneration => self.image_generation.clone(),
}
}
pub fn get_by_purpose_with_catch_all_fallback(&self, purpose: AgentPurpose) -> Option<String> {
match self.get_by_purpose(purpose) {
Some(agent_id) => Some(agent_id),
None => self.catch_all.clone(),
}
}
pub fn set_by_purpose(&mut self, purpose: AgentPurpose, agent_id: Option<String>) {
match purpose {
AgentPurpose::CatchAll => {
self.catch_all = agent_id;
}
AgentPurpose::TextGeneration => {
self.text_generation = agent_id;
}
AgentPurpose::SpeechToText => {
self.speech_to_text = agent_id;
}
AgentPurpose::TextToSpeech => {
self.text_to_speech = agent_id;
}
AgentPurpose::ImageGeneration => {
self.image_generation = agent_id;
}
};
}
}

View File

@@ -0,0 +1,89 @@
use mxlink::helpers::account_data_config::RoomConfig as RoomConfigTrait;
use mxlink::helpers::account_data_config::RoomConfigCarrierContent as RoomConfigCarrierContentTrait;
use mxlink::matrix_sdk::ruma::events::macros::EventContent;
use mxlink::matrix_sdk::{Room, RoomMemberships};
use serde::{Deserialize, Serialize};
use crate::agent::AgentDefinition;
mod handler;
mod speech_to_text;
mod text_generation;
mod text_to_speech;
pub use handler::RoomSettingsHandler;
pub use speech_to_text::SpeechToTextFlowType;
pub use text_generation::{TextGenerationAutoUsage, TextGenerationPrefixRequirementType};
pub use text_to_speech::{TextToSpeechBotMessagesFlowType, TextToSpeechUserMessagesFlowType};
#[derive(Clone, Debug, Deserialize, Serialize, EventContent)]
#[ruma_event(type = "cc.etke.baibot.room_config", kind = RoomAccountData)]
pub struct RoomConfigCarrierContent {
pub payload: String,
}
impl RoomConfigCarrierContentTrait for RoomConfigCarrierContent {
fn payload(&self) -> &str {
&self.payload
}
fn new(payload: String) -> Self {
Self { payload }
}
}
#[derive(Clone, Debug, Default, Deserialize, Serialize)]
pub struct RoomConfig {
pub settings: RoomSettings,
pub agents: Vec<AgentDefinition>,
}
impl RoomConfigTrait for RoomConfig {}
impl RoomConfig {
pub async fn with_room(mut self, room: Room) -> Self {
tracing::trace!("Determining room members count to decide on a suitable text-generation/prefix-requirement-type default");
let members = room.members(RoomMemberships::ACTIVE).await;
let prefix_requirement_type = match members {
Ok(members) => {
let members_count = members.len();
let prefix_requirement_type = if members.len() > 2 {
text_generation::TextGenerationPrefixRequirementType::CommandPrefix
} else {
text_generation::TextGenerationPrefixRequirementType::No
};
tracing::info!(?members_count, ?prefix_requirement_type, "Determined text-generation/prefix-requirement-type based on room members count");
prefix_requirement_type
}
Err(err) => {
tracing::error!(?err, "Failed to get members of room - will default text-generation/prefix-requirement-type to No");
text_generation::TextGenerationPrefixRequirementType::No
}
};
self.settings.text_generation.prefix_requirement_type = Some(prefix_requirement_type);
self
}
}
#[derive(Clone, Debug, Default, Deserialize, Serialize)]
pub struct RoomSettings {
pub handler: handler::RoomSettingsHandler,
#[serde(default)]
pub text_generation: text_generation::RoomSettingsTextGeneration,
#[serde(default)]
pub speech_to_text: speech_to_text::RoomSettingsSpeechToText,
#[serde(default)]
pub text_to_speech: text_to_speech::RoomSettingsTextToSpeech,
}

View File

@@ -0,0 +1,59 @@
use serde::{Deserialize, Serialize};
#[derive(Clone, Debug, Default, Deserialize, Serialize)]
pub struct RoomSettingsSpeechToText {
pub flow_type: Option<SpeechToTextFlowType>,
/// The language of the input audio.
/// Supplying the input language in [ISO-639-1](https://en.wikipedia.org/wiki/List_of_ISO_639-1_codes) format will improve accuracy and latency.
pub language: Option<String>,
}
#[derive(Clone, Copy, Debug, Deserialize, Serialize, PartialEq)]
pub enum SpeechToTextFlowType {
/// Voice messages are to be ignored.
#[serde(rename = "ignore")]
Ignore,
/// Voice messages are to trigger text-generation.
/// This may potentially trigger speech-to-text, but that's not what we care about here.
#[serde(rename = "transcribe_and_generate_text")]
TranscribeAndGenerateText,
// Voices messages are to trigger transcription.
#[serde(rename = "only_transcribe")]
OnlyTranscribe,
}
impl SpeechToTextFlowType {
pub fn choices() -> Vec<Self> {
vec![
Self::Ignore,
Self::TranscribeAndGenerateText,
Self::OnlyTranscribe,
]
}
pub fn from_str(s: &str) -> Option<Self> {
match s {
"ignore" => Some(Self::Ignore),
"transcribe_and_generate_text" => Some(Self::TranscribeAndGenerateText),
"only_transcribe" => Some(Self::OnlyTranscribe),
_ => None,
}
}
}
impl std::fmt::Display for SpeechToTextFlowType {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
match self {
SpeechToTextFlowType::Ignore => {
write!(f, "ignore")
}
SpeechToTextFlowType::TranscribeAndGenerateText => {
write!(f, "transcribe_and_generate_text")
}
SpeechToTextFlowType::OnlyTranscribe => write!(f, "only_transcribe"),
}
}
}

View File

@@ -0,0 +1,111 @@
use serde::{Deserialize, Serialize};
#[derive(Clone, Debug, Default, Deserialize, Serialize)]
pub struct RoomSettingsTextGeneration {
/// Controls whether initial text messages require a prefix to trigger text generation.
/// This could have been a bool, using an enum allows us to add more options (e.g. CustomPrefix) in the future.
/// Even if set to "required", prefixless-triggering could still happen via an initial voice message (see auto_usage).
pub prefix_requirement_type: Option<TextGenerationPrefixRequirementType>,
/// Controls whether text generation is automatically triggered (depending on message type).
pub auto_usage: Option<TextGenerationAutoUsage>,
/// Controls whether conversation context management is enabled.
/// When enabled, the bot will automatically tokenize messages and try to shorten the message context intelligently.
pub context_management_enabled: Option<bool>,
/// Allows customizing the system prompt that the agent would use
pub prompt_override: Option<String>,
/// Allows customizing the temperature that the agent would use
pub temperature_override: Option<f32>,
}
#[derive(Clone, Copy, Debug, Deserialize, Serialize, PartialEq)]
pub enum TextGenerationPrefixRequirementType {
/// Text Generation is to be triggered for any text message
#[serde(rename = "no")]
No,
/// Text Generation is to be triggered only for messages that are prefixed with the command prefix
#[serde(rename = "command_prefix")]
CommandPrefix,
}
impl TextGenerationPrefixRequirementType {
pub fn choices() -> Vec<Self> {
vec![Self::No, Self::CommandPrefix]
}
pub fn from_str(s: &str) -> Option<Self> {
match s {
"no" => Some(Self::No),
"command_prefix" => Some(Self::CommandPrefix),
_ => None,
}
}
}
impl std::fmt::Display for TextGenerationPrefixRequirementType {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
match self {
TextGenerationPrefixRequirementType::No => write!(f, "no"),
TextGenerationPrefixRequirementType::CommandPrefix => {
write!(f, "command_prefix")
}
}
}
}
#[derive(Clone, Copy, Debug, Deserialize, Serialize, PartialEq)]
pub enum TextGenerationAutoUsage {
/// Text Generation is to never be performed
#[serde(rename = "never")]
Never,
/// Text Generation is to always be performed
#[serde(rename = "always")]
Always,
/// Text Generation is to be performed when the original message was sent as audio (voice).
/// The voice message would be transcribed to text (subject to other configuration)
/// and text generation would be triggered.
#[serde(rename = "only_for_voice")]
OnlyForVoice,
/// Text Generation is to be performed when the original message was sent as text
#[serde(rename = "only_for_text")]
OnlyForText,
}
impl TextGenerationAutoUsage {
pub fn choices() -> Vec<Self> {
vec![
Self::Never,
Self::Always,
Self::OnlyForVoice,
Self::OnlyForText,
]
}
pub fn from_str(s: &str) -> Option<Self> {
match s {
"never" => Some(Self::Never),
"always" => Some(Self::Always),
"only_for_voice" => Some(Self::OnlyForVoice),
"only_for_text" => Some(Self::OnlyForText),
_ => None,
}
}
}
impl std::fmt::Display for TextGenerationAutoUsage {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
match self {
TextGenerationAutoUsage::Never => write!(f, "never"),
TextGenerationAutoUsage::Always => write!(f, "always"),
TextGenerationAutoUsage::OnlyForVoice => write!(f, "only_for_voice"),
TextGenerationAutoUsage::OnlyForText => write!(f, "only_for_text"),
}
}
}

View File

@@ -0,0 +1,110 @@
use serde::{Deserialize, Serialize};
#[derive(Clone, Debug, Default, Deserialize, Serialize)]
pub struct RoomSettingsTextToSpeech {
pub bot_msgs_flow_type: Option<TextToSpeechBotMessagesFlowType>,
pub user_msgs_flow_type: Option<TextToSpeechUserMessagesFlowType>,
pub speed_override: Option<f32>,
pub voice_override: Option<String>,
}
#[derive(Clone, Copy, Debug, Deserialize, Serialize, PartialEq)]
pub enum TextToSpeechBotMessagesFlowType {
/// Never do text-to-speech for bot messages automatically and don't offer it
#[serde(rename = "never")]
Never,
/// Never do text-to-speech for bot messages automatically, but offer it via an emoji reaction for all messages
#[serde(rename = "on_demand_always")]
OnDemandAlways,
/// Never do text-to-speech for bot messages automatically, but offer it via an emoji reaction if the user message that prompted the bot message was audio (voice)
#[serde(rename = "on_demand_for_voice")]
OnDemandForVoice,
/// Convert all bot text messages to audio (voice) automatically if the user message that prompted the bot message was audio (voice)
#[serde(rename = "only_for_voice")]
OnlyForVoice,
/// Convert all bot text messages to audio (voice) automatically
#[serde(rename = "always")]
Always,
}
impl TextToSpeechBotMessagesFlowType {
pub fn choices() -> Vec<Self> {
vec![
Self::Never,
Self::OnDemandAlways,
Self::OnDemandForVoice,
Self::Always,
Self::OnlyForVoice,
]
}
pub fn from_str(s: &str) -> Option<Self> {
match s {
"never" => Some(Self::Never),
"on_demand_always" => Some(Self::OnDemandAlways),
"on_demand_for_voice" => Some(Self::OnDemandForVoice),
"only_for_voice" => Some(Self::OnlyForVoice),
"always" => Some(Self::Always),
_ => None,
}
}
}
impl std::fmt::Display for TextToSpeechBotMessagesFlowType {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
match self {
TextToSpeechBotMessagesFlowType::Never => write!(f, "never"),
TextToSpeechBotMessagesFlowType::OnDemandAlways => write!(f, "on_demand_always"),
TextToSpeechBotMessagesFlowType::OnDemandForVoice => write!(f, "on_demand_for_voice"),
TextToSpeechBotMessagesFlowType::Always => write!(f, "always"),
TextToSpeechBotMessagesFlowType::OnlyForVoice => write!(f, "only_for_voice"),
}
}
}
#[derive(Clone, Copy, Debug, Deserialize, Serialize, PartialEq)]
pub enum TextToSpeechUserMessagesFlowType {
/// Never do text-to-speech for user messages automatically and don't offer it
#[serde(rename = "never")]
Never,
/// Never do text-to-speech for user messages automatically, but offer it via an emoji reaction
#[serde(rename = "on_demand")]
OnDemand,
/// Convert all user text messages to audio (voice) automatically
#[serde(rename = "always")]
Always,
}
impl TextToSpeechUserMessagesFlowType {
pub fn choices() -> Vec<Self> {
vec![Self::Never, Self::OnDemand, Self::Always]
}
pub fn from_str(s: &str) -> Option<Self> {
match s {
"never" => Some(Self::Never),
"on_demand" => Some(Self::OnDemand),
"always" => Some(Self::Always),
_ => None,
}
}
}
impl std::fmt::Display for TextToSpeechUserMessagesFlowType {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
match self {
TextToSpeechUserMessagesFlowType::Never => write!(f, "never"),
TextToSpeechUserMessagesFlowType::OnDemand => write!(f, "on_demand"),
TextToSpeechUserMessagesFlowType::Always => write!(f, "always"),
}
}
}

View File

@@ -0,0 +1,13 @@
pub mod defaults;
mod entity;
use mxlink::helpers::account_data_config::RoomConfigManager as AccountDataRoomConfigManager;
pub use entity::{RoomConfig, RoomConfigCarrierContent, RoomSettings, RoomSettingsHandler};
pub use entity::{
SpeechToTextFlowType, TextGenerationAutoUsage, TextGenerationPrefixRequirementType,
TextToSpeechBotMessagesFlowType, TextToSpeechUserMessagesFlowType,
};
pub type RoomConfigurationManager =
AccountDataRoomConfigManager<RoomConfig, RoomConfigCarrierContent>;

View File

@@ -0,0 +1,13 @@
use mxlink::ThreadInfo;
use super::MessagePayload;
pub struct ThreadContext {
pub info: ThreadInfo,
pub first_message: ThreadContextFirstMessage,
}
pub struct ThreadContextFirstMessage {
pub is_mentioning_bot: bool,
pub payload: MessagePayload,
}

View File

@@ -0,0 +1,27 @@
use mxlink::matrix_sdk::ruma::{OwnedEventId, OwnedUserId};
use super::MessagePayload;
#[derive(Debug)]
pub struct TriggerEventInfo {
pub event_id: OwnedEventId,
pub sender: OwnedUserId,
pub payload: MessagePayload,
pub sender_is_admin: bool,
}
impl TriggerEventInfo {
pub fn new(
event_id: OwnedEventId,
sender: OwnedUserId,
payload: MessagePayload,
sender_is_admin: bool,
) -> Self {
Self {
event_id,
sender,
payload,
sender_is_admin,
}
}
}