Compare commits

...

21 Commits

Author SHA1 Message Date
Slavi Pantaleev
257ffae9e7 Release 1.12.0 2025-12-21 23:27:08 +02:00
Slavi Pantaleev
f7bf3d7b60 Upgrade async-openai (0.32.1 -> 0.32.2)
This brings in an important bugfix related to image generation.
Ref:
- https://github.com/64bit/async-openai/issues/507
- https://github.com/64bit/async-openai/pull/508
2025-12-21 23:21:03 +02:00
Slavi Pantaleev
3a88b0d656 Make gpt-image-1.5 the default image model for OpenAI 2025-12-21 12:53:44 +02:00
Slavi Pantaleev
08c689a889 Upgrade async-openai (0.31.1 -> 0.32.1) and adapt, adding support for gpt-image-1.5 2025-12-21 12:23:25 +02:00
Slavi Pantaleev
ae8e878817 Update services 2025-12-21 11:33:08 +02:00
Slavi Pantaleev
edbd72ece6 Release 1.11.0 2025-12-15 10:03:47 +02:00
Slavi Pantaleev
22906aa2d3 Upgrade Rust (1.91.1 -> 1.92.0) 2025-12-15 09:31:34 +02:00
Slavi Pantaleev
99bde53ef6 Upgrade services 2025-12-15 09:23:59 +02:00
Slavi Pantaleev
b3fd8e548f Minor documentation updates 2025-12-15 09:22:54 +02:00
Slavi Pantaleev
062fbbb8ef Add support for custom avatars (via file path) and for not touching the already-set avatar
This is based on the work done in https://github.com/etkecc/baibot/pull/60 by https://github.com/Fmstrat (Ben Curtis),
with various changes on top to make the code more idiomatic and flexible.

This commit squashes the following patches (newest first):

- Improve handling of `user.avatar` configuration (null & empty string being the same now) and add support for a special `keep` value
- Minor import reordering
- Simplify avatar configuration (`user.avatar.source` -> `user.avatar`)
- Combine `logo_bytes` and `mime_type` determination logic and do not fall back to default avatar if reading the custom avatar file fails
- Switch from deprecated `mime_guess::guess_mime_type(avatar_path)` to `mime_guess::from_path(avatar_path).first_or_octet_stream()`
- Relax `mime_guess` constraint and order alphabetically
- Use `mime` from `mxlink`
- Add mime-type support and switch to user.avatar.source
- (Original work by Fmstrat) Add support for custom avatars

Co-authored-by: Fmstrat <nospam@nowsci.com>
2025-12-15 08:07:24 +02:00
Slavi Pantaleev
2801c78ad9 Bump default OpenAI text-generation model (gpt-5.1 -> gpt-5.2) 2025-12-12 16:05:16 +02:00
Slavi Pantaleev
f4c698ad33 Release 1.10.0 2025-12-06 07:01:39 +02:00
Slavi Pantaleev
bd39001417 Update dependencies and matrix-sdk (0.14.0 -> 0.16.0) 2025-12-06 07:00:24 +02:00
Slavi Pantaleev
2692d0322e Release 1.9.0 2025-11-30 12:29:58 +02:00
Slavi Pantaleev
8eb70f0f2c Upgrade async-openai from our own etkecc fork to upstream's 0.31.1
Switches `async-openai` from our own etkecc fork (0.28.1-patched) to the
official crates.io version 0.31.1.

We adapt to async-openai's types reorganization and making use of crate
features to only enable what we need.
2025-11-30 11:48:56 +02:00
Slavi Pantaleev
5c0a7be7a2 Add changelog entry for v1.8.3 2025-11-28 14:42:48 +02:00
Slavi Pantaleev
0a8f9fc3e5 Release 1.8.3 2025-11-28 14:40:38 +02:00
Slavi Pantaleev
1ac3b2e060 Update dependencies 2025-11-28 14:23:07 +02:00
Slavi Pantaleev
a3ef9fd1bf Upgrade services 2025-11-28 14:19:59 +02:00
Slavi Pantaleev
df507eb201 Make use of the BAIBOT_USER_ENCRYPTION_RECOVERY_RESET_ALLOWED environment variable for configuring config.user.encryption.recovery_reset_allowed 2025-11-28 14:18:35 +02:00
Slavi Pantaleev
4dcd9eff40 Make use of the BAIBOT_PERSISTENCE_SESSION_ENCRYPTION_KEY environment variable for configuring config.persistence.session_encryption_key
Seems like we already had a constant defined, but weren't making use of it.
2025-11-28 14:17:23 +02:00
22 changed files with 691 additions and 934 deletions

View File

@@ -1,3 +1,37 @@
# (2025-12-21) Version 1.12.0
- (**Improvement**) Upgrade [async-openai](https://crates.io/crates/async-openai) (0.31.1 -> 0.32.2) and add support for OpenAI's `gpt-image-1.5` model ([08c689a](https://github.com/etkecc/baibot/commit/08c689a), [f7bf3d7](https://github.com/etkecc/baibot/commit/f7bf3d7))
- (**Internal Improvement**) Dependency updates
# (2025-12-15) Version 1.11.0
- (**Feature**) Add support for custom avatars via file path and for keeping the already-set avatar (for those who wish to manage it by themselves via other means). See the [sample config](./etc/app/config.yml.dist) for details. ([062fbbb](https://github.com/etkecc/baibot/commit/062fbbb8ef9ad600db483a431c5c782402191023))
- (**Internal Improvement**) Dependency updates ([99bde53](https://github.com/etkecc/baibot/commit/99bde53ef648a5a9086a96778fde4a9dbc1ede58))
- (**Internal Improvement**) Documentation updates ([b3fd8e5](https://github.com/etkecc/baibot/commit/b3fd8e548f83fe46398ced4760d7e2bb7588c24d))
- (**Internal Improvement**) Upgrade Rust compiler (1.91.1 -> 1.92.0) ([22906aa](https://github.com/etkecc/baibot/commit/22906aa2d3cae51815fad2560a545eaa69c247b6))
# (2025-12-06) Version 1.10.0
- (**Internal Improvement**) Dependency updates. This version is based on [mxlink](https://crates.io/crates/mxlink)@1.11.0 (which is based on the newly released [matrix-sdk](https://crates.io/crates/matrix-sdk)@[0.16.0](https://github.com/matrix-org/matrix-rust-sdk/releases/tag/matrix-sdk-0.16.0).
# (2025-11-30) Version 1.9.0
- (**Internal Improvement**) Upgrade [async-openai](https://crates.io/crates/async-openai) from our own etkecc fork (0.28.1-patched) to the official upstream version 0.31.1. This upgrade required some code adaptations to the new module structure, etc. While tested, regressions are possible.
# (2025-11-28) Version 1.8.3
- (**Improvement**) Add support for the `BAIBOT_PERSISTENCE_SESSION_ENCRYPTION_KEY` environment variable for configuring `persistence.session_encryption_key`
- (**Improvement**) Add support for the `BAIBOT_USER_ENCRYPTION_RECOVERY_RESET_ALLOWED` environment variable for configuring `user.encryption.recovery_reset_allowed`
- (**Internal Improvement**) Dependency updates.
# (2025-11-20) Version 1.8.2 # (2025-11-20) Version 1.8.2
- (**Internal Improvement**) Dependency and compiler updates (Rust 1.89.0 -> 1.91.1). - (**Internal Improvement**) Dependency and compiler updates (Rust 1.89.0 -> 1.91.1).

1193
Cargo.lock generated

File diff suppressed because it is too large Load Diff

View File

@@ -7,7 +7,7 @@ license = "AGPL-3.0-or-later"
readme = "README.md" readme = "README.md"
keywords = ["matrix", "chat", "bot", "AI", "LLM"] keywords = ["matrix", "chat", "bot", "AI", "LLM"]
include = ["/etc/assets/baibot-torso-768.png", "/src", "/README.md", "/CHANGELOG.md", "/LICENSE"] include = ["/etc/assets/baibot-torso-768.png", "/src", "/README.md", "/CHANGELOG.md", "/LICENSE"]
version = "1.8.2" version = "1.12.0"
edition = "2024" edition = "2024"
[lib] [lib]
@@ -17,14 +17,15 @@ path = "src/lib.rs"
[dependencies] [dependencies]
anthropic = { git = "https://github.com/etkecc/anthropic-rs.git", branch = "fix-content-block-image" } anthropic = { git = "https://github.com/etkecc/anthropic-rs.git", branch = "fix-content-block-image" }
anyhow = "1.0.*" anyhow = "1.0.*"
async-openai = { git = "https://github.com/etkecc/async-openai", branch = "async-openai-v0.28.1-patched" } async-openai = { version = "0.32.2", features = ["audio", "chat-completion", "image"] }
base64 = "0.22.*" base64 = "0.22.*"
chrono = { version = "0.4.*", default-features = false, features = ["std", "now"] } chrono = { version = "0.4.*", default-features = false, features = ["std", "now"] }
# We'd rather not depend on this, but we cannot use the ruma-events EventContent macro without it. # We'd rather not depend on this, but we cannot use the ruma-events EventContent macro without it.
# We add the `native-tls` feature, because of https://github.com/etkecc/rust-mxlink/issues/1 # We add the `native-tls` feature, because of https://github.com/etkecc/rust-mxlink/issues/1
matrix-sdk = { version = "0.14.0", default-features = false, features = ["native-tls"] } matrix-sdk = { version = "0.16.0", default-features = false, features = ["native-tls"] }
mime_guess = "2.0.*"
mxidwc = "1.0.*" mxidwc = "1.0.*"
mxlink = ">=1.10.0" mxlink = ">=1.11.0"
etke_openai_api_rust = "0.1.*" etke_openai_api_rust = "0.1.*"
quick_cache = "0.6.*" quick_cache = "0.6.*"
regex = "1.12.*" regex = "1.12.*"

View File

@@ -4,7 +4,7 @@
# # # #
####################################### #######################################
FROM docker.io/rust:1.91.1-slim-trixie AS build FROM docker.io/rust:1.92.0-slim-trixie AS build
RUN apt-get update && apt-get install -y build-essential pkg-config libssl-dev libsqlite3-dev RUN apt-get update && apt-get install -y build-essential pkg-config libssl-dev libsqlite3-dev

View File

@@ -4,7 +4,7 @@
# # # #
####################################### #######################################
FROM docker.io/rust:1.91.1-slim-trixie AS build FROM docker.io/rust:1.92.0-slim-trixie AS build
RUN apt-get update && apt-get install -y build-essential pkg-config libssl-dev libsqlite3-dev RUN apt-get update && apt-get install -y build-essential pkg-config libssl-dev libsqlite3-dev

View File

@@ -43,7 +43,8 @@ Administrators cannot be changed without adjusting the bot's configuration on th
Room-local agent managers are users privileged to **create their own [agents](./agents.md)** (see `!bai agent`) in rooms. Room-local agent managers are users privileged to **create their own [agents](./agents.md)** (see `!bai agent`) in rooms.
**⚠️ WARNING**: Letting regular users create agents which contact arbitrary network services **may be a security issue**. > [!WARNING]
> Letting regular users create agents which contact arbitrary network services **may be a security issue**.
The following commands are available: The following commands are available:
- **Show** the currently allowed users: `!bai access room-local-agent-managers` - **Show** the currently allowed users: `!bai access room-local-agent-managers`

View File

@@ -12,12 +12,15 @@ This file is created from the template found in [etc/app/config.yml.dist](../../
Certain keys can be left unset, in which case [📝 hardcoded defaults](../../src/entity/cfg/defaults.rs) would be used. Certain keys can be left unset, in which case [📝 hardcoded defaults](../../src/entity/cfg/defaults.rs) would be used.
Each configuration key found in the YAML configuration can be overridden by setting an environment variable (dots should be replaced with `_`). Example: Some configuration keys found in the YAML configuration can be overridden by setting an environment variable (dots should be replaced with `_`). Example:
- to override `command_prefix`, set an environment variable `BAIBOT_COMMAND_PREFIX` - to override `command_prefix`, set an environment variable `BAIBOT_COMMAND_PREFIX`
- to override `homeserver.server_name`, set an environment variable `BAIBOT_HOMESERVER_SERVER_NAME` - to override `homeserver.server_name`, set an environment variable `BAIBOT_HOMESERVER_SERVER_NAME`
The static configuration contains an `initial_global_config` key, which is used to populate the bot's global configuration (stored as [dynamic configuration](#dynamic-configuration)) the first time the bot starts. Modifying this subsequently will not have any effect. After initial global configuration creation, it's expected to be managed dynamically via chat commands. You can see the list of supported environment variables in the [🦀 src/entity/cfg/env.rs](../../src/entity/cfg/env.rs) file.
> [!WARNING]
> The static configuration contains an `initial_global_config` key, which is used to populate the bot's global configuration (stored as [dynamic configuration](#dynamic-configuration)) the first time the bot starts. Modifying this subsequently will not have any effect. After initial global configuration creation, it's expected to be managed dynamically via chat commands.
### Dynamic configuration ### Dynamic configuration

View File

@@ -1,7 +1,7 @@
base_url: https://api.openai.com/v1 base_url: https://api.openai.com/v1
api_key: YOUR_API_KEY_HERE api_key: YOUR_API_KEY_HERE
text_generation: text_generation:
model_id: gpt-5.1 model_id: gpt-5.2
prompt: "You are a brief, but helpful bot called {{ baibot_name }} powered by the {{ baibot_model_id }} model. The date/time of this conversation's start is: {{ baibot_conversation_start_time_utc }}." prompt: "You are a brief, but helpful bot called {{ baibot_name }} powered by the {{ baibot_model_id }} model. The date/time of this conversation's start is: {{ baibot_conversation_start_time_utc }}."
temperature: 1.0 temperature: 1.0
# Reasoning models need to use `max_completion_tokens` instead of `max_response_tokens`. # Reasoning models need to use `max_completion_tokens` instead of `max_response_tokens`.
@@ -17,7 +17,7 @@ text_to_speech:
speed: 1.0 speed: 1.0
response_format: opus response_format: opus
image_generation: image_generation:
model_id: gpt-image-1 model_id: gpt-image-1.5
style: null style: null
size: null size: null
quality: null quality: null

View File

@@ -11,6 +11,12 @@ user:
# Leave empty to use the default (baibot). # Leave empty to use the default (baibot).
name: baibot name: baibot
# An optional path to an image file to be used as a custom avatar image.
# - null or empty string: use the default avatar
# - "keep": don't touch the avatar, keep whatever is already set
# - any other value: path to a custom avatar image file
avatar: null
encryption: encryption:
# An optional passphrase to use for backing up and recovering the bot's encryption keys. # An optional passphrase to use for backing up and recovering the bot's encryption keys.
# You can use any string here. # You can use any string here.
@@ -76,7 +82,7 @@ agents:
# base_url: https://api.openai.com/v1 # base_url: https://api.openai.com/v1
# api_key: "" # api_key: ""
# text_generation: # text_generation:
# model_id: gpt-5.1 # model_id: gpt-5.2
# prompt: "You are a brief, but helpful bot called {{ baibot_name }} powered by the {{ baibot_model_id }} model. The date/time of this conversation's start is: {{ baibot_conversation_start_time_utc }}." # prompt: "You are a brief, but helpful bot called {{ baibot_name }} powered by the {{ baibot_model_id }} model. The date/time of this conversation's start is: {{ baibot_conversation_start_time_utc }}."
# temperature: 1.0 # temperature: 1.0
# # Reasoning models need to use `max_completion_tokens` instead of `max_response_tokens`. # # Reasoning models need to use `max_completion_tokens` instead of `max_response_tokens`.
@@ -92,7 +98,7 @@ agents:
# speed: 1.0 # speed: 1.0
# response_format: opus # response_format: opus
# image_generation: # image_generation:
# model_id: gpt-image-1 # model_id: gpt-image-1.5
# style: null # style: null
# size: null # size: null
# quality: null # quality: null

View File

@@ -14,7 +14,7 @@ services:
- /etc/passwd:/etc/passwd:ro - /etc/passwd:/etc/passwd:ro
synapse: synapse:
image: ghcr.io/element-hq/synapse:v1.142.1 image: ghcr.io/element-hq/synapse:v1.144.0
user: "${UID}:${GID}" user: "${UID}:${GID}"
restart: unless-stopped restart: unless-stopped
entrypoint: python entrypoint: python
@@ -27,7 +27,7 @@ services:
- ./synapse/media-store:/media-store - ./synapse/media-store:/media-store
element-web: element-web:
image: ghcr.io/element-hq/element-web:v1.12.4 image: ghcr.io/element-hq/element-web:v1.12.7
user: "${UID}:${GID}" user: "${UID}:${GID}"
restart: unless-stopped restart: unless-stopped
environment: environment:

View File

@@ -1,6 +1,6 @@
services: services:
ollama: ollama:
image: docker.io/ollama/ollama:0.13.0 image: docker.io/ollama/ollama:0.13.5
restart: unless-stopped restart: unless-stopped
ports: ports:
- "${SERVICE_OLLAMA_BIND_PORT_HTTP}:11434" - "${SERVICE_OLLAMA_BIND_PORT_HTTP}:11434"

View File

@@ -56,12 +56,8 @@ impl ImageSource {
} }
} }
impl From<ImageSource> for async_openai::types::ImageInput { impl From<ImageSource> for async_openai::types::images::ImageInput {
fn from(value: ImageSource) -> Self { fn from(value: ImageSource) -> Self {
async_openai::types::ImageInput::from_vec_u8( async_openai::types::images::ImageInput::from_vec_u8(value.filename, value.bytes)
value.filename,
value.bytes,
value.mime_type.to_string(),
)
} }
} }

View File

@@ -1,6 +1,6 @@
use serde::{Deserialize, Serialize}; use serde::{Deserialize, Serialize};
use super::OPENAI_IMAGE_MODEL_GPT_IMAGE_1; use super::OPENAI_IMAGE_MODEL_GPT_IMAGE_1_DOT_5;
use crate::agent::{default_prompt, provider::ConfigTrait}; use crate::agent::{default_prompt, provider::ConfigTrait};
#[derive(Debug, Clone, Serialize, Deserialize)] #[derive(Debug, Clone, Serialize, Deserialize)]
@@ -80,7 +80,7 @@ impl Default for TextGenerationConfig {
} }
fn default_text_model_id() -> String { fn default_text_model_id() -> String {
"gpt-5.1".to_owned() "gpt-5.2".to_owned()
} }
#[derive(Debug, Clone, Serialize, Deserialize)] #[derive(Debug, Clone, Serialize, Deserialize)]
@@ -104,16 +104,16 @@ fn default_speech_to_text_model_id() -> String {
#[derive(Debug, Clone, Serialize, Deserialize)] #[derive(Debug, Clone, Serialize, Deserialize)]
pub struct TextToSpeechConfig { pub struct TextToSpeechConfig {
#[serde(default = "default_text_to_speech_model_id")] #[serde(default = "default_text_to_speech_model_id")]
pub model_id: async_openai::types::SpeechModel, pub model_id: async_openai::types::audio::SpeechModel,
#[serde(default = "default_text_to_speech_voice")] #[serde(default = "default_text_to_speech_voice")]
pub voice: async_openai::types::Voice, pub voice: async_openai::types::audio::Voice,
#[serde(default = "default_text_to_speech_speed")] #[serde(default = "default_text_to_speech_speed")]
pub speed: f32, pub speed: f32,
#[serde(default = "default_text_to_speech_response_format")] #[serde(default = "default_text_to_speech_response_format")]
pub response_format: async_openai::types::SpeechResponseFormat, pub response_format: async_openai::types::audio::SpeechResponseFormat,
} }
impl Default for TextToSpeechConfig { impl Default for TextToSpeechConfig {
@@ -127,22 +127,22 @@ impl Default for TextToSpeechConfig {
} }
} }
fn default_text_to_speech_model_id() -> async_openai::types::SpeechModel { fn default_text_to_speech_model_id() -> async_openai::types::audio::SpeechModel {
async_openai::types::SpeechModel::Tts1Hd async_openai::types::audio::SpeechModel::Tts1Hd
} }
fn default_text_to_speech_voice() -> async_openai::types::Voice { fn default_text_to_speech_voice() -> async_openai::types::audio::Voice {
async_openai::types::Voice::Onyx async_openai::types::audio::Voice::Onyx
} }
fn default_text_to_speech_speed() -> f32 { fn default_text_to_speech_speed() -> f32 {
1.0 1.0
} }
fn default_text_to_speech_response_format() -> async_openai::types::SpeechResponseFormat { fn default_text_to_speech_response_format() -> async_openai::types::audio::SpeechResponseFormat {
// The API defaults to mp3, but we prefer Opus because it's smaller. // The API defaults to mp3, but we prefer Opus because it's smaller.
// Our clients should all have support for it. // Our clients should all have support for it.
async_openai::types::SpeechResponseFormat::Opus async_openai::types::audio::SpeechResponseFormat::Opus
} }
#[derive(Debug, Clone, Serialize, Deserialize)] #[derive(Debug, Clone, Serialize, Deserialize)]
@@ -150,19 +150,19 @@ pub struct ImageGenerationConfig {
pub model_id: String, pub model_id: String,
#[serde(default = "default_image_style")] #[serde(default = "default_image_style")]
pub style: Option<async_openai::types::ImageStyle>, pub style: Option<async_openai::types::images::ImageStyle>,
#[serde(default = "default_image_size")] #[serde(default = "default_image_size")]
pub size: Option<async_openai::types::ImageSize>, pub size: Option<async_openai::types::images::ImageSize>,
#[serde(default = "default_image_quality")] #[serde(default = "default_image_quality")]
pub quality: Option<async_openai::types::ImageQuality>, pub quality: Option<async_openai::types::images::ImageQuality>,
} }
impl Default for ImageGenerationConfig { impl Default for ImageGenerationConfig {
fn default() -> Self { fn default() -> Self {
Self { Self {
model_id: OPENAI_IMAGE_MODEL_GPT_IMAGE_1.to_owned(), model_id: OPENAI_IMAGE_MODEL_GPT_IMAGE_1_DOT_5.to_owned(),
style: default_image_style(), style: default_image_style(),
size: default_image_size(), size: default_image_size(),
quality: default_image_quality(), quality: default_image_quality(),
@@ -173,23 +173,28 @@ impl Default for ImageGenerationConfig {
impl ImageGenerationConfig { impl ImageGenerationConfig {
pub fn model_id_as_openai_image_model( pub fn model_id_as_openai_image_model(
&self, &self,
) -> Result<async_openai::types::ImageModel, String> { ) -> Result<async_openai::types::images::ImageModel, String> {
match self.model_id.as_str() { match self.model_id.as_str() {
"dall-e-2" => Ok(async_openai::types::ImageModel::DallE2), "dall-e-2" => Ok(async_openai::types::images::ImageModel::DallE2),
"dall-e-3" => Ok(async_openai::types::ImageModel::DallE3), "dall-e-3" => Ok(async_openai::types::images::ImageModel::DallE3),
other => Ok(async_openai::types::ImageModel::Other(other.to_owned())), "gpt-image-1" => Ok(async_openai::types::images::ImageModel::GptImage1),
"gpt-image-1.5" => Ok(async_openai::types::images::ImageModel::GptImage1dot5),
"gpt-image-1-mini" => Ok(async_openai::types::images::ImageModel::GptImage1Mini),
other => Ok(async_openai::types::images::ImageModel::Other(
other.to_owned(),
)),
} }
} }
} }
fn default_image_style() -> Option<async_openai::types::ImageStyle> { fn default_image_style() -> Option<async_openai::types::images::ImageStyle> {
None None
} }
fn default_image_size() -> Option<async_openai::types::ImageSize> { fn default_image_size() -> Option<async_openai::types::images::ImageSize> {
None None
} }
fn default_image_quality() -> Option<async_openai::types::ImageQuality> { fn default_image_quality() -> Option<async_openai::types::images::ImageQuality> {
None None
} }

View File

@@ -4,9 +4,12 @@ use async_openai::{
Client as OpenAIClient, Client as OpenAIClient,
config::OpenAIConfig, config::OpenAIConfig,
types::{ types::{
ChatCompletionRequestMessage, CreateChatCompletionRequestArgs, CreateImageEditRequestArgs, audio::{AudioInput, CreateSpeechRequestArgs, CreateTranscriptionRequestArgs},
CreateImageRequestArgs, CreateSpeechRequestArgs, CreateTranscriptionRequestArgs, chat::{ChatCompletionRequestMessage, CreateChatCompletionRequestArgs},
DallE2ImageSize, Image, ImageModel, ImageResponseFormat, images::{
CreateImageEditRequestArgs, CreateImageRequestArgs,
Image, ImageInput, ImageModel, ImageResponseFormat,
},
}, },
}; };
@@ -38,8 +41,6 @@ use crate::{
use super::config::Config; use super::config::Config;
use super::OPENAI_IMAGE_MODEL_GPT_IMAGE_1;
#[derive(Debug, Clone)] #[derive(Debug, Clone)]
pub struct Controller { pub struct Controller {
config: Config, config: Config,
@@ -209,11 +210,7 @@ impl ControllerTrait for Controller {
let request = CreateTranscriptionRequestArgs::default() let request = CreateTranscriptionRequestArgs::default()
.model(&speech_to_text_config.model_id) .model(&speech_to_text_config.model_id)
.file(async_openai::types::AudioInput::from_vec_u8( .file(AudioInput::from_vec_u8(filename, media))
filename,
media,
mime_type.to_string(),
))
.language(language.clone()) .language(language.clone())
.build()?; .build()?;
@@ -223,7 +220,7 @@ impl ControllerTrait for Controller {
"Sending OpenAI speech-to-text API request" "Sending OpenAI speech-to-text API request"
); );
let response = self.client.audio().transcribe(request).await?; let response = self.client.audio().transcription().create(request).await?;
tracing::trace!( tracing::trace!(
?response, ?response,
@@ -255,11 +252,12 @@ impl ControllerTrait for Controller {
let model = if params.cheaper_model_switching_allowed { let model = if params.cheaper_model_switching_allowed {
// Switch to a cheaper model // Switch to a cheaper model
match original_model { match original_model {
async_openai::types::ImageModel::DallE2 => async_openai::types::ImageModel::DallE2, ImageModel::DallE2 => ImageModel::DallE2,
async_openai::types::ImageModel::DallE3 => async_openai::types::ImageModel::DallE2, ImageModel::DallE3 => ImageModel::DallE2,
async_openai::types::ImageModel::Other(_) => { ImageModel::Other(_) => {
async_openai::types::ImageModel::DallE2 ImageModel::DallE2
} }
_ => original_model.clone(),
} }
} else { } else {
original_model original_model
@@ -269,11 +267,24 @@ impl ControllerTrait for Controller {
// Switch to a cheaper quality // Switch to a cheaper quality
match &image_generation_config.quality { match &image_generation_config.quality {
Some(quality) => match quality { Some(quality) => match quality {
async_openai::types::ImageQuality::Standard => { async_openai::types::images::ImageQuality::Standard => {
Some(async_openai::types::ImageQuality::Standard) Some(async_openai::types::images::ImageQuality::Standard)
} }
async_openai::types::ImageQuality::HD => { async_openai::types::images::ImageQuality::HD => {
Some(async_openai::types::ImageQuality::Standard) Some(async_openai::types::images::ImageQuality::Standard)
}
// New quality levels - keep as-is or downgrade to Standard
async_openai::types::images::ImageQuality::High => {
Some(async_openai::types::images::ImageQuality::Standard)
}
async_openai::types::images::ImageQuality::Medium => {
Some(async_openai::types::images::ImageQuality::Medium)
}
async_openai::types::images::ImageQuality::Low => {
Some(async_openai::types::images::ImageQuality::Low)
}
async_openai::types::images::ImageQuality::Auto => {
Some(async_openai::types::images::ImageQuality::Auto)
} }
}, },
None => None, None => None,
@@ -284,18 +295,18 @@ impl ControllerTrait for Controller {
let size = params let size = params
.size_override .size_override
.map(|s| convert_string_to_enum::<async_openai::types::ImageSize>(&s).unwrap()) .map(|s| convert_string_to_enum::<async_openai::types::images::ImageSize>(&s).unwrap())
.or(image_generation_config.size); .or(image_generation_config.size);
let response_format = match model.clone() { let response_format = match model.clone() {
ImageModel::DallE2 => Some(ImageResponseFormat::B64Json), ImageModel::DallE2 => Some(ImageResponseFormat::B64Json),
ImageModel::DallE3 => Some(ImageResponseFormat::B64Json), ImageModel::DallE3 => Some(ImageResponseFormat::B64Json),
ImageModel::Other(model_str) => match model_str.as_str() { // gpt-image-1 only outputs base64 and we don't need to specify the response format.
// gpt-image-1 only outputs base64 and we don't need to specify the response format. // In fact, specifying the response format results in an error.
// In fact, specifying the response format results in an error. ImageModel::GptImage1 => None,
OPENAI_IMAGE_MODEL_GPT_IMAGE_1 => None, ImageModel::GptImage1Mini => None,
_ => Some(ImageResponseFormat::B64Json), ImageModel::GptImage1dot5 => None,
}, ImageModel::Other(_) => Some(ImageResponseFormat::B64Json),
}; };
let mut request_builder = CreateImageRequestArgs::default(); let mut request_builder = CreateImageRequestArgs::default();
@@ -329,15 +340,15 @@ impl ControllerTrait for Controller {
"Sending OpenAI image generation API request" "Sending OpenAI image generation API request"
); );
let response = self.client.images().create(request).await?; let response = self.client.images().generate(request).await?;
if let Some(image) = response.data.into_iter().next() { if let Some(image) = response.data.into_iter().next() {
match image.deref() { match image.deref() {
async_openai::types::Image::B64Json { Image::B64Json {
b64_json, b64_json,
revised_prompt, revised_prompt,
} => { } => {
let bytes = base64_decode(b64_json)?; let bytes = base64_decode(b64_json.as_ref())?;
return Ok(ImageGenerationResult { return Ok(ImageGenerationResult {
bytes, bytes,
@@ -374,15 +385,15 @@ impl ControllerTrait for Controller {
return Err(anyhow::anyhow!("No image sources provided")); return Err(anyhow::anyhow!("No image sources provided"));
} }
let mut image_inputs = Vec::new(); let mut image_inputs: Vec<ImageInput> = Vec::new();
for image in images { for image in images {
image_inputs.push(image.into()); image_inputs.push(image.into());
} }
let dalle2_size = match image_generation_config.size { let dalle2_size = match image_generation_config.size {
Some(async_openai::types::ImageSize::S256x256) => Some(DallE2ImageSize::S256x256), Some(async_openai::types::images::ImageSize::S256x256) => Some(async_openai::types::images::ImageSize::S256x256),
Some(async_openai::types::ImageSize::S512x512) => Some(DallE2ImageSize::S512x512), Some(async_openai::types::images::ImageSize::S512x512) => Some(async_openai::types::images::ImageSize::S512x512),
Some(async_openai::types::ImageSize::S1024x1024) => Some(DallE2ImageSize::S1024x1024), Some(async_openai::types::images::ImageSize::S1024x1024) => Some(async_openai::types::images::ImageSize::S1024x1024),
_ => None, _ => None,
}; };
@@ -391,16 +402,18 @@ impl ControllerTrait for Controller {
.map_err(|err| anyhow::anyhow!(err))?; .map_err(|err| anyhow::anyhow!(err))?;
let response_format = match model.clone() { let response_format = match model.clone() {
async_openai::types::ImageModel::DallE2 => { ImageModel::DallE2 => {
Some(async_openai::types::ImageResponseFormat::B64Json) Some(ImageResponseFormat::B64Json)
} }
async_openai::types::ImageModel::DallE3 => { ImageModel::DallE3 => {
Some(async_openai::types::ImageResponseFormat::B64Json) Some(ImageResponseFormat::B64Json)
} }
async_openai::types::ImageModel::Other(model_str) => match model_str.as_str() { // gpt-image-1 only outputs base64 and we don't need to specify the response format.
OPENAI_IMAGE_MODEL_GPT_IMAGE_1 => None, // In fact, specifying the response format results in an error.
_ => Some(async_openai::types::ImageResponseFormat::B64Json), ImageModel::GptImage1 => None,
}, ImageModel::GptImage1Mini => None,
ImageModel::GptImage1dot5 => None,
ImageModel::Other(_) => Some(ImageResponseFormat::B64Json),
}; };
let mut request_builder = CreateImageEditRequestArgs::default(); let mut request_builder = CreateImageEditRequestArgs::default();
@@ -429,12 +442,12 @@ impl ControllerTrait for Controller {
"Sending OpenAI image edit API request" "Sending OpenAI image edit API request"
); );
let response = self.client.images().create_edit(request).await?; let response = self.client.images().edit(request).await?;
if let Some(image_data) = response.data.into_iter().next() { if let Some(image_data) = response.data.into_iter().next() {
match image_data.deref() { match image_data.deref() {
Image::B64Json { b64_json, .. } => { Image::B64Json { b64_json, .. } => {
let bytes = base64_decode(b64_json)?; let bytes = base64_decode(b64_json.as_ref())?;
return Ok(ImageEditResult { return Ok(ImageEditResult {
bytes, bytes,
mime_type: mxlink::mime::IMAGE_PNG, mime_type: mxlink::mime::IMAGE_PNG,
@@ -471,7 +484,7 @@ impl ControllerTrait for Controller {
let voice = if let Some(voice_string) = params.voice_override { let voice = if let Some(voice_string) = params.voice_override {
// This is a hacky way to construct a Voice enum from the string we have. // This is a hacky way to construct a Voice enum from the string we have.
let voice: serde_json::Result<async_openai::types::Voice> = let voice: serde_json::Result<async_openai::types::audio::Voice> =
serde_json::from_str(&format!("\"{}\"", voice_string)); serde_json::from_str(&format!("\"{}\"", voice_string));
match voice { match voice {
Ok(voice) => voice, Ok(voice) => voice,
@@ -511,7 +524,7 @@ impl ControllerTrait for Controller {
"Sending OpenAI text-to-speech API request" "Sending OpenAI text-to-speech API request"
); );
let result = self.client.audio().speech(request).await?; let result = self.client.audio().speech().create(request).await?;
Ok(TextToSpeechResult { Ok(TextToSpeechResult {
bytes: result.bytes.into(), bytes: result.bytes.into(),
@@ -570,15 +583,15 @@ impl ControllerTrait for Controller {
} }
fn response_format_to_mime_type( fn response_format_to_mime_type(
response_format: &async_openai::types::SpeechResponseFormat, response_format: &async_openai::types::audio::SpeechResponseFormat,
) -> Option<mxlink::mime::Mime> { ) -> Option<mxlink::mime::Mime> {
let content_type = match response_format { let content_type = match response_format {
async_openai::types::SpeechResponseFormat::Mp3 => "audio/mp3".to_owned(), async_openai::types::audio::SpeechResponseFormat::Mp3 => "audio/mp3".to_owned(),
async_openai::types::SpeechResponseFormat::Wav => "audio/wav".to_owned(), async_openai::types::audio::SpeechResponseFormat::Wav => "audio/wav".to_owned(),
async_openai::types::SpeechResponseFormat::Opus => "audio/ogg".to_owned(), async_openai::types::audio::SpeechResponseFormat::Opus => "audio/ogg".to_owned(),
async_openai::types::SpeechResponseFormat::Aac => "audio/aac".to_owned(), async_openai::types::audio::SpeechResponseFormat::Aac => "audio/aac".to_owned(),
async_openai::types::SpeechResponseFormat::Flac => "audio/flac".to_owned(), async_openai::types::audio::SpeechResponseFormat::Flac => "audio/flac".to_owned(),
async_openai::types::SpeechResponseFormat::Pcm => "audio/L8".to_owned(), async_openai::types::audio::SpeechResponseFormat::Pcm => "audio/L8".to_owned(),
}; };
match content_type.parse() { match content_type.parse() {

View File

@@ -16,7 +16,7 @@ use super::super::AgentInstantiationResult;
use super::ConfigTrait; use super::ConfigTrait;
use super::controller::ControllerType; use super::controller::ControllerType;
pub const OPENAI_IMAGE_MODEL_GPT_IMAGE_1: &str = "gpt-image-1"; pub const OPENAI_IMAGE_MODEL_GPT_IMAGE_1_DOT_5: &str = "gpt-image-1.5";
pub fn create_controller_from_yaml_value_config( pub fn create_controller_from_yaml_value_config(
agent_id: &str, agent_id: &str,

View File

@@ -1,8 +1,12 @@
use async_openai::types::{ use async_openai::types::{
ChatCompletionRequestAssistantMessageArgs, ChatCompletionRequestMessage, chat::{
ChatCompletionRequestMessageContentPartImage, ChatCompletionRequestSystemMessageArgs, ChatCompletionRequestAssistantMessageArgs, ChatCompletionRequestMessage,
ChatCompletionRequestUserMessageArgs, ChatCompletionRequestUserMessageContent, ChatCompletionRequestMessageContentPartImage,
ChatCompletionRequestUserMessageContentPart, ImageUrlArgs, ChatCompletionRequestSystemMessageArgs,
ChatCompletionRequestUserMessageArgs, ChatCompletionRequestUserMessageContent,
ChatCompletionRequestUserMessageContentPart,
ImageUrlArgs,
},
}; };
use crate::conversation::llm::{ use crate::conversation::llm::{

View File

@@ -161,11 +161,11 @@ impl TryInto<OpenAITextToSpeechConfig> for TextToSpeechConfig {
type Error = String; type Error = String;
fn try_into(self) -> Result<OpenAITextToSpeechConfig, Self::Error> { fn try_into(self) -> Result<OpenAITextToSpeechConfig, Self::Error> {
let model_id = convert_string_to_enum::<async_openai::types::SpeechModel>(&self.model_id)?; let model_id = convert_string_to_enum::<async_openai::types::audio::SpeechModel>(&self.model_id)?;
let voice = convert_string_to_enum::<async_openai::types::Voice>(&self.voice)?; let voice = convert_string_to_enum::<async_openai::types::audio::Voice>(&self.voice)?;
let response_format = convert_string_to_enum::<async_openai::types::SpeechResponseFormat>( let response_format = convert_string_to_enum::<async_openai::types::audio::SpeechResponseFormat>(
&self.response_format, &self.response_format,
)?; )?;
@@ -224,7 +224,7 @@ impl TryInto<OpenAIImageGenerationConfig> for ImageGenerationConfig {
fn try_into(self) -> Result<OpenAIImageGenerationConfig, Self::Error> { fn try_into(self) -> Result<OpenAIImageGenerationConfig, Self::Error> {
let size = if let Some(size) = &self.size { let size = if let Some(size) = &self.size {
Some(convert_string_to_enum::<async_openai::types::ImageSize>( Some(convert_string_to_enum::<async_openai::types::images::ImageSize>(
size, size,
)?) )?)
} else { } else {
@@ -232,7 +232,7 @@ impl TryInto<OpenAIImageGenerationConfig> for ImageGenerationConfig {
}; };
let style = if let Some(style) = &self.style { let style = if let Some(style) = &self.style {
Some(convert_string_to_enum::<async_openai::types::ImageStyle>( Some(convert_string_to_enum::<async_openai::types::images::ImageStyle>(
style, style,
)?) )?)
} else { } else {
@@ -240,7 +240,7 @@ impl TryInto<OpenAIImageGenerationConfig> for ImageGenerationConfig {
}; };
let quality = if let Some(quality) = &self.quality { let quality = if let Some(quality) = &self.quality {
Some(convert_string_to_enum::<async_openai::types::ImageQuality>( Some(convert_string_to_enum::<async_openai::types::images::ImageQuality>(
quality, quality,
)?) )?)
} else { } else {

View File

@@ -1,3 +1,4 @@
use std::fs;
use std::sync::Arc; use std::sync::Arc;
use std::{future::Future, pin::Pin}; use std::{future::Future, pin::Pin};
@@ -18,12 +19,13 @@ use mxlink::helpers::account_data_config::{
RoomConfigManager as AccountDataRoomConfigManager, RoomConfigManager as AccountDataRoomConfigManager,
}; };
use mxlink::helpers::encryption::Manager as EncryptionManager; use mxlink::helpers::encryption::Manager as EncryptionManager;
use mxlink::mime::Mime;
use crate::agent::Manager as AgentManager; use crate::agent::Manager as AgentManager;
use crate::entity::catch_up_marker::{ use crate::entity::catch_up_marker::{
CatchUpMarker, CatchUpMarkerManager, DelayedCatchUpMarkerManager, CatchUpMarker, CatchUpMarkerManager, DelayedCatchUpMarkerManager,
}; };
use crate::entity::cfg::Config; use crate::entity::cfg::{Avatar, Config};
use crate::entity::globalconfig::{GlobalConfig, GlobalConfigurationManager}; use crate::entity::globalconfig::{GlobalConfig, GlobalConfigurationManager};
use crate::entity::roomconfig::{RoomConfig, RoomConfigurationManager}; use crate::entity::roomconfig::{RoomConfig, RoomConfigurationManager};
@@ -316,34 +318,72 @@ impl Bot {
} }
} }
let should_update_avatar = match &current_avatar_url { let desired_avatar: Option<(Vec<u8>, Mime)> = match &self.inner.config.user.avatar {
Some(avatar_url) => { Avatar::Keep => {
let request = MediaRequestParameters { tracing::info!("Avatar configured to keep current, skipping avatar management");
source: MediaSource::Plain(avatar_url.to_owned()), None
format: MediaFormat::File, }
}; Avatar::Default => {
tracing::info!("Avatar configured to use default");
let content = media Some((
.get_media_content(&request, true) LOGO_BYTES.to_vec(),
.await LOGO_MIME_TYPE
.map_err(|e| anyhow::anyhow!("Failed fetching existing avatar: {:?}", e))?; .parse()
.expect("Failed parsing mime type for logo"),
content.as_slice() != LOGO_BYTES ))
}
Avatar::Custom(avatar_path) => {
tracing::info!(?avatar_path, "Avatar configured to use custom path");
let bytes = fs::read(avatar_path).map_err(|e| {
anyhow::anyhow!("Failed reading avatar from {:?}: {:?}", avatar_path, e)
})?;
let mime = mime_guess::from_path(avatar_path).first_or_octet_stream();
tracing::debug!(?mime, bytes_len = bytes.len(), "Loaded custom avatar");
Some((bytes, mime))
} }
None => true,
}; };
if should_update_avatar { if let Some((desired_bytes, mime_type)) = desired_avatar {
tracing::info!("Updating avatar.."); let should_update_avatar = match &current_avatar_url {
Some(avatar_url) => {
tracing::debug!(?avatar_url, "Fetching current avatar to compare");
let request = MediaRequestParameters {
source: MediaSource::Plain(avatar_url.to_owned()),
format: MediaFormat::File,
};
let mime_type = LOGO_MIME_TYPE let content = media
.parse() .get_media_content(&request, true)
.expect("Failed parsing mime type for logo"); .await
.map_err(|e| anyhow::anyhow!("Failed fetching existing avatar: {:?}", e))?;
account let needs_update = content.as_slice() != desired_bytes;
.upload_avatar(&mime_type, LOGO_BYTES.to_vec())
.await tracing::debug!(
.map_err(|e| anyhow::anyhow!("Failed uploading avatar: {:?}", e))?; current_bytes_len = content.len(),
desired_bytes_len = desired_bytes.len(),
?needs_update,
"Compared current and desired avatar"
);
needs_update
}
None => {
tracing::debug!("No current avatar set, will upload");
true
}
};
if should_update_avatar {
tracing::info!("Updating avatar..");
account
.upload_avatar(&mime_type, desired_bytes)
.await
.map_err(|e| anyhow::anyhow!("Failed uploading avatar: {:?}", e))?;
tracing::info!("Avatar updated successfully");
} else {
tracing::debug!("Avatar already up to date, skipping upload");
}
} }
Ok(()) Ok(())

View File

@@ -5,7 +5,7 @@ use anyhow::anyhow;
use crate::agent::AgentPurpose; use crate::agent::AgentPurpose;
pub use crate::entity::cfg::{Config, defaults as cfg_defaults, env as cfg_env}; pub use crate::entity::cfg::{Avatar, Config, defaults as cfg_defaults, env as cfg_env};
pub fn load() -> anyhow::Result<Config> { pub fn load() -> anyhow::Result<Config> {
let config_file_path = env::var(cfg_env::BAIBOT_CONFIG_FILE_PATH) let config_file_path = env::var(cfg_env::BAIBOT_CONFIG_FILE_PATH)
@@ -33,7 +33,13 @@ pub fn load() -> anyhow::Result<Config> {
cfg_env::BAIBOT_USER_ENCRYPTION_RECOVERY_PASSPHRASE => { cfg_env::BAIBOT_USER_ENCRYPTION_RECOVERY_PASSPHRASE => {
config.user.encryption.recovery_passphrase = Some(value); config.user.encryption.recovery_passphrase = Some(value);
} }
cfg_env::BAIBOT_USER_ENCRYPTION_RECOVERY_RESET_ALLOWED => {
config.user.encryption.recovery_reset_allowed = value.parse::<bool>()?;
}
cfg_env::BAIBOT_USER_NAME => config.user.name = value, cfg_env::BAIBOT_USER_NAME => config.user.name = value,
cfg_env::BAIBOT_USER_AVATAR => {
config.user.avatar = Avatar::from_string(value);
}
cfg_env::BAIBOT_COMMAND_PREFIX => config.command_prefix = value, cfg_env::BAIBOT_COMMAND_PREFIX => config.command_prefix = value,
cfg_env::BAIBOT_ROOM_POST_JOIN_SELF_INTRODUCTION_ENABLED => { cfg_env::BAIBOT_ROOM_POST_JOIN_SELF_INTRODUCTION_ENABLED => {
config.room.post_join_self_introduction_enabled = value.parse::<bool>()?; config.room.post_join_self_introduction_enabled = value.parse::<bool>()?;
@@ -51,6 +57,9 @@ pub fn load() -> anyhow::Result<Config> {
cfg_env::BAIBOT_PERSISTENCE_DATA_DIR_PATH => { cfg_env::BAIBOT_PERSISTENCE_DATA_DIR_PATH => {
config.persistence.data_dir_path = Some(value); config.persistence.data_dir_path = Some(value);
} }
cfg_env::BAIBOT_PERSISTENCE_SESSION_ENCRYPTION_KEY => {
config.persistence.session_encryption_key = Some(value);
}
cfg_env::BAIBOT_PERSISTENCE_CONFIG_ENCRYPTION_KEY => { cfg_env::BAIBOT_PERSISTENCE_CONFIG_ENCRYPTION_KEY => {
config.persistence.config_encryption_key = Some(value); config.persistence.config_encryption_key = Some(value);
} }

View File

@@ -1,7 +1,7 @@
use std::path::PathBuf; use std::path::PathBuf;
use mxlink::helpers::encryption::EncryptionKey; use mxlink::helpers::encryption::EncryptionKey;
use serde::{Deserialize, Serialize}; use serde::{Deserialize, Deserializer, Serialize};
use crate::{ use crate::{
agent::{AgentDefinition, AgentPurpose, PublicIdentifier}, agent::{AgentDefinition, AgentPurpose, PublicIdentifier},
@@ -83,6 +83,52 @@ impl ConfigHomeserver {
} }
} }
/// Configuration for the bot's avatar.
///
/// - `Default`: Use the built-in default avatar (null, empty string, or missing in config)
/// - `Keep`: Don't touch the avatar, keep whatever is already set ("keep" in config)
/// - `Custom(String)`: Use a custom avatar from the specified file path
#[derive(Debug, Clone, PartialEq, Serialize)]
pub enum Avatar {
/// Use the built-in default avatar
Default,
/// Keep the current avatar, don't change it
Keep,
/// Use a custom avatar from the specified file path
Custom(String),
}
impl Default for Avatar {
fn default() -> Self {
Avatar::Default
}
}
impl<'de> Deserialize<'de> for Avatar {
fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
where
D: Deserializer<'de>,
{
let value: Option<String> = Option::deserialize(deserializer)?;
Ok(match value {
None => Avatar::Default,
Some(s) => Avatar::from_string(s),
})
}
}
impl Avatar {
pub fn from_string(value: String) -> Self {
if value.is_empty() {
Avatar::Default
} else if value.eq_ignore_ascii_case("keep") {
Avatar::Keep
} else {
Avatar::Custom(value)
}
}
}
#[derive(Debug, Serialize, Deserialize)] #[derive(Debug, Serialize, Deserialize)]
pub struct ConfigUser { pub struct ConfigUser {
pub mxid_localpart: String, pub mxid_localpart: String,
@@ -93,6 +139,9 @@ pub struct ConfigUser {
#[serde(default)] #[serde(default)]
pub encryption: ConfigUserEncryption, pub encryption: ConfigUserEncryption,
#[serde(default)]
pub avatar: Avatar,
} }
impl ConfigUser { impl ConfigUser {

View File

@@ -6,8 +6,11 @@ pub const BAIBOT_HOMESERVER_URL: &str = "BAIBOT_HOMESERVER_URL";
pub const BAIBOT_USER_MXID_LOCALPART: &str = "BAIBOT_USER_MXID_LOCALPART"; pub const BAIBOT_USER_MXID_LOCALPART: &str = "BAIBOT_USER_MXID_LOCALPART";
pub const BAIBOT_USER_PASSWORD: &str = "BAIBOT_USER_PASSWORD"; pub const BAIBOT_USER_PASSWORD: &str = "BAIBOT_USER_PASSWORD";
pub const BAIBOT_USER_NAME: &str = "BAIBOT_USER_NAME"; pub const BAIBOT_USER_NAME: &str = "BAIBOT_USER_NAME";
pub const BAIBOT_USER_AVATAR: &str = "BAIBOT_USER_AVATAR";
pub const BAIBOT_USER_ENCRYPTION_RECOVERY_PASSPHRASE: &str = pub const BAIBOT_USER_ENCRYPTION_RECOVERY_PASSPHRASE: &str =
"BAIBOT_USER_ENCRYPTION_RECOVERY_PASSPHRASE"; "BAIBOT_USER_ENCRYPTION_RECOVERY_PASSPHRASE";
pub const BAIBOT_USER_ENCRYPTION_RECOVERY_RESET_ALLOWED: &str =
"BAIBOT_USER_ENCRYPTION_RECOVERY_RESET_ALLOWED";
pub const BAIBOT_COMMAND_PREFIX: &str = "BAIBOT_COMMAND_PREFIX"; pub const BAIBOT_COMMAND_PREFIX: &str = "BAIBOT_COMMAND_PREFIX";

View File

@@ -2,4 +2,4 @@ mod config;
pub mod defaults; pub mod defaults;
pub mod env; pub mod env;
pub use config::Config; pub use config::{Avatar, Config};