Compare commits

...

46 Commits

Author SHA1 Message Date
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
Slavi Pantaleev
ea760ce755 Release 1.8.2 2025-11-20 06:00:52 +02:00
Slavi Pantaleev
1528df6a55 Upgrade Rust (1.90.0 -> 1.91.1) 2025-11-20 05:51:45 +02:00
Slavi Pantaleev
b0fa024297 Update services 2025-11-20 05:50:36 +02:00
Slavi Pantaleev
3ec203128a Update dependencies 2025-11-20 05:49:15 +02:00
Slavi Pantaleev
da97361e1b Bump default OpenAI text-generation model (gpt-5 -> gpt-5.1) 2025-11-20 05:25:20 +02:00
Slavi Pantaleev
b430fe0189 Update sample OpenAI config (for gpt-5) misleading users into using max_response_tokens & remove openai-o1.yml sample config
No need to have both sample configs now.

Fixes https://github.com/etkecc/baibot/issues/57
2025-11-20 05:25:20 +02:00
Slavi Pantaleev
f03126a9e1 Upgrade Rust (1.89.0 -> 1.90.0) 2025-10-26 09:00:01 +02:00
Slavi Pantaleev
7d46b926c1 Update services 2025-10-26 08:30:42 +02:00
Slavi Pantaleev
6f3c048195 Release 1.8.1 2025-09-12 16:54:11 +03:00
Slavi Pantaleev
b47cf598b5 Update dependencies 2025-09-12 16:53:35 +03:00
Slavi Pantaleev
265ad7e1cb Release 1.8.0 2025-09-08 15:19:06 +03:00
Slavi Pantaleev
a159f67e45 Upgrade Rust (1.88.0 -> 1.89.0) and Debian base (12/bookworm -> 13/trixie) in Dockerfiles 2025-09-08 15:01:25 +03:00
Slavi Pantaleev
624b9de35b Upgrade mxlink (1.9.0 -> 1.10.0) and matrix-sdk (0.13.0 -> 0.14.0) 2025-09-08 14:26:37 +03:00
Slavi Pantaleev
941bf7ca42 Update sample configs for OpenAI (gpt-5) to specify max_completion_tokens, not max_response_tokens
Fixup for b43f61f5ff
2025-09-08 10:37:00 +03:00
Slavi Pantaleev
ef0f1671da Update services 2025-09-08 10:04:48 +03:00
Slavi Pantaleev
b43f61f5ff Change default OpenAI model (gpt-4.1 -> gpt-5)
Ref: https://openai.com/index/introducing-gpt-5/
2025-08-08 07:22:19 +03:00
Slavi Pantaleev
1967d2b34c Release 1.7.6 2025-07-11 16:43:34 +03:00
Slavi Pantaleev
bb3734ad24 Upgrade mxlink (1.8.1 -> 1.9.0) and matrix-sdk (0.12.0 -> 0.13.0) 2025-07-11 16:41:46 +03:00
Slavi Pantaleev
eb6db34177 Update dependencies 2025-07-11 16:23:58 +03:00
Slavi Pantaleev
6e845caa2e Release 1.7.5 2025-06-27 16:40:08 +03:00
Slavi Pantaleev
1004966785 Update Cargo.lock-pinned dependencies 2025-06-27 16:15:53 +03:00
Slavi Pantaleev
7ae1864c2e Upgrade Rust (1.86.0 -> 1.88.0)
Ref: https://releases.rs/docs/1.88.0/
2025-06-27 16:09:12 +03:00
Slavi Pantaleev
68a2fb161f Release 1.7.4 2025-06-10 16:38:03 +03:00
Slavi Pantaleev
3a3eb58d7b Update Cargo.lock-pinned dependencies 2025-06-10 16:37:13 +03:00
Slavi Pantaleev
74d988e650 Update mxlink (1.8.0 -> 1.8.1) 2025-06-10 16:33:44 +03:00
Slavi Pantaleev
ed8bedcd7e Release 1.7.3 2025-06-10 15:28:56 +03:00
Slavi Pantaleev
2842632969 Disable async-openai feature of tiktoken-rs crate
We don't make use of this feature, so it's not necessary.

Ref: af52be12fa/tiktoken-rs/Cargo.toml (L21)
2025-06-10 15:26:25 +03:00
Slavi Pantaleev
10a5bd2abb Update OpenAI model in sample config (gpt-4o -> gpt-4.1) 2025-06-10 15:20:00 +03:00
Slavi Pantaleev
5308b75f52 Update dependencies 2025-06-10 15:18:58 +03:00
Slavi Pantaleev
dad61e1270 Adjust docker run command on installation instructions to ensure /tmp is writable
Ref: https://github.com/etkecc/baibot/issues/43
2025-06-09 10:54:11 +03:00
24 changed files with 1598 additions and 1104 deletions

View File

@@ -1,3 +1,62 @@
# (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
- (**Internal Improvement**) Dependency and compiler updates (Rust 1.89.0 -> 1.91.1).
# (2025-09-12) Version 1.8.1
- (**Internal Improvement**) Dependency updates.
# (2025-09-08) Version 1.8.0
- (**Internal Improvement**) Upgrade [mxlink](https://crates.io/crates/mxlink) (1.9.0 -> 1.10.0) and [matrix-sdk](https://crates.io/crates/matrix-sdk) (0.13.0 -> 0.14.0)
- (**Internal Improvement**) Upgrade [Rust](https://www.rust-lang.org/) (1.88.0 -> 1.89.0)
- (**Internal Improvement**) Upgrade Debian base for container images (12/bookworm -> 13/trixie)
# (2025-07-11) Version 1.7.6
- (**Internal Improvement**) Dependency updates. This version is based on [mxlink](https://crates.io/crates/mxlink)@1.9.0 (which is based on the newly released [matrix-sdk](https://crates.io/crates/matrix-sdk)@[0.13.0](https://github.com/matrix-org/matrix-rust-sdk/releases/tag/matrix-sdk-0.13.0), which contains fixes for some security vulnerabilities)
# (2025-06-10) Version 1.7.5
- (**Internal Improvement**) Dependency and compiler updates (Rust 1.86 -> 1.86).
# (2025-06-10) Version 1.7.4
- (**Internal Improvement**) Dependency updates.
# (2025-06-10) Version 1.7.3
- (**Internal Improvement**) Dependency updates. This version is based on [mxlink](https://crates.io/crates/mxlink)@1.8.0 (which is based on the newly released [matrix-sdk](https://crates.io/crates/matrix-sdk)@[0.12.0](https://github.com/matrix-org/matrix-rust-sdk/releases/tag/matrix-sdk-0.12.0), which contains fixes for important security vulnerabilities)
# (2025-05-11) Version 1.7.2
- (**Bugfix**) Allow `image_generation.size` configuration value for OpenAI to be `null` to allow the model to choose the size automatically and default to that

2181
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"
keywords = ["matrix", "chat", "bot", "AI", "LLM"]
include = ["/etc/assets/baibot-torso-768.png", "/src", "/README.md", "/CHANGELOG.md", "/LICENSE"]
version = "1.7.2"
version = "1.11.0"
edition = "2024"
[lib]
@@ -17,23 +17,24 @@ path = "src/lib.rs"
[dependencies]
anthropic = { git = "https://github.com/etkecc/anthropic-rs.git", branch = "fix-content-block-image" }
anyhow = "1.0.*"
async-openai = { git = "https://github.com/etkecc/async-openai", branch = "async-openai-v0.28.1-patched" }
async-openai = { version = "0.31.1", features = ["audio", "chat-completion", "image"] }
base64 = "0.22.*"
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 add the `native-tls` feature, because of https://github.com/etkecc/rust-mxlink/issues/1
matrix-sdk = { version = "0.11.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.*"
mxlink = ">=1.7.0"
mxlink = ">=1.11.0"
etke_openai_api_rust = "0.1.*"
quick_cache = "0.6.*"
regex = "1.11.*"
regex = "1.12.*"
serde = { version = "1.0.*", features = ["derive"], default-features = false }
serde_json = "1.0.*"
serde_yaml = "0.9.*"
tempfile = "3.19.*"
tiktoken-rs = { version = "0.6.*", features = ["async-openai"] }
tokio = { version = "1.45.*", features = ["rt", "rt-multi-thread", "macros"] }
tempfile = "3.23.*"
tiktoken-rs = { version = "0.9.*", default-features = false }
tokio = { version = "1.48.*", features = ["rt", "rt-multi-thread", "macros"] }
tracing = "0.1.*"
tracing-subscriber = { version = "0.3.*", features = ["env-filter"] }
url = "2.5.*"

View File

@@ -4,7 +4,7 @@
# #
#######################################
FROM docker.io/rust:1.86.0-slim-bookworm 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
@@ -39,7 +39,7 @@ RUN --mount=type=cache,target=/target,sharing=locked \
# #
#######################################
FROM docker.io/debian:bookworm-slim
FROM docker.io/debian:trixie-slim
RUN apt-get update && apt-get install -y ca-certificates sqlite3 && \
apt-get clean && \

View File

@@ -4,7 +4,7 @@
# #
#######################################
FROM docker.io/rust:1.86.0-slim-bookworm 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
@@ -20,7 +20,7 @@ RUN cargo build --release
# #
#######################################
FROM docker.io/debian:bookworm-slim
FROM docker.io/debian:trixie-slim
RUN apt-get update && apt-get install -y ca-certificates sqlite3 && \
apt-get clean && \

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.
**⚠️ 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:
- **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.
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 `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

View File

@@ -53,6 +53,7 @@ CONTAINER_IMAGE_NAME=ghcr.io/etkecc/baibot:v1.0.0
--env BAIBOT_PERSISTENCE_DATA_DIR_PATH=/data \
--mount type=bind,src=/path/to/config.yml,dst=/app/config.yml,ro \
--mount type=bind,src=/path/to/data,dst=/data \
--tmpfs=/tmp:rw,noexec,nosuid,size=1024m \
$CONTAINER_IMAGE_NAME
```

View File

@@ -125,10 +125,7 @@ For services which are not fully compatible with the OpenAI API, consider using
- create a room-local agent: `!bai agent create-room-local openai my-openai-agent`
- create a global agent: `!bai agent create-global openai my-openai-agent`
💡 When creating an agent, the bot will show you an up-to-date sample configuration for this provider which:
- in the general case looks [like this](./sample-provider-configs/openai.yml)
- for the [o1](https://platform.openai.com/docs/models/o1) models needs to look [like this](./sample-provider-configs/openai-o1.yml)
💡 When creating an agent, the bot will show you an up-to-date sample configuration for this provider which looks [like this](./sample-provider-configs/openai.yml).
### OpenAI Compatible

View File

@@ -1,24 +0,0 @@
base_url: https://api.openai.com/v1
api_key: YOUR_API_KEY_HERE
text_generation:
model_id: o1-mini
# o1 models do not support a system prompt
prompt: null
temperature: 1.0
# o1 models do not support max_response_tokens.
# They use `max_completion_tokens` as an alternative
max_response_tokens: null
max_completion_tokens: 16384
max_context_tokens: 128000
speech_to_text:
model_id: whisper-1
text_to_speech:
model_id: tts-1-hd
voice: onyx
speed: 1.0
response_format: opus
image_generation:
model_id: gpt-image-1
style: null
size: null
quality: null

View File

@@ -1,11 +1,14 @@
base_url: https://api.openai.com/v1
api_key: YOUR_API_KEY_HERE
text_generation:
model_id: gpt-4.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 }}."
temperature: 1.0
max_response_tokens: 16384
max_context_tokens: 128000
# Reasoning models need to use `max_completion_tokens` instead of `max_response_tokens`.
# If you're dealing with a non-reasoning model, specify `max_response_tokens` and unset `max_completion_tokens`.
max_response_tokens: null
max_completion_tokens: 128000
max_context_tokens: 400000
speech_to_text:
model_id: whisper-1
text_to_speech:

View File

@@ -11,6 +11,12 @@ user:
# Leave empty to use the default (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:
# An optional passphrase to use for backing up and recovering the bot's encryption keys.
# You can use any string here.
@@ -76,13 +82,14 @@ agents:
# base_url: https://api.openai.com/v1
# api_key: ""
# text_generation:
# model_id: gpt-4o
# 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 }}."
# temperature: 1.0
# max_response_tokens: 16384
# # Reasoning models need to use `max_completion_tokens` instead of `max_response_tokens`.
# max_completion_tokens: ~
# max_context_tokens: 128000
# # If you're dealing with a non-reasoning model, specify `max_response_tokens` and unset `max_completion_tokens`.
# max_response_tokens: null
# max_completion_tokens: 128000
# max_context_tokens: 400000
# speech_to_text:
# model_id: whisper-1
# text_to_speech:

View File

@@ -1,6 +1,6 @@
services:
postgres:
image: docker.io/postgres:16.8-alpine
image: docker.io/postgres:18.1-alpine
user: ${UID}:${GID}
restart: unless-stopped
environment:
@@ -8,12 +8,13 @@ services:
POSTGRES_PASSWORD: synapse-password
POSTGRES_DB: homeserver
POSTGRES_INITDB_ARGS: --lc-collate C --lc-ctype C --encoding UTF8
PGDATA: /data
volumes:
- ./postgres:/var/lib/postgresql/data
- ./postgres:/data
- /etc/passwd:/etc/passwd:ro
synapse:
image: ghcr.io/element-hq/synapse:v1.129.0
image: ghcr.io/element-hq/synapse:v1.144.0
user: "${UID}:${GID}"
restart: unless-stopped
entrypoint: python
@@ -26,7 +27,7 @@ services:
- ./synapse/media-store:/media-store
element-web:
image: ghcr.io/element-hq/element-web:v1.11.100
image: ghcr.io/element-hq/element-web:v1.12.6
user: "${UID}:${GID}"
restart: unless-stopped
environment:

View File

@@ -1,6 +1,6 @@
services:
ollama:
image: docker.io/ollama/ollama:0.6.8
image: docker.io/ollama/ollama:0.13.3
restart: unless-stopped
ports:
- "${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 {
async_openai::types::ImageInput::from_vec_u8(
value.filename,
value.bytes,
value.mime_type.to_string(),
)
async_openai::types::images::ImageInput::from_vec_u8(value.filename, value.bytes)
}
}

View File

@@ -72,15 +72,15 @@ impl Default for TextGenerationConfig {
model_id: default_text_model_id(),
prompt: Some(default_prompt().to_owned()),
temperature: super::super::default_temperature(),
max_response_tokens: Some(16_384),
max_completion_tokens: None,
max_context_tokens: 128_000,
max_response_tokens: None,
max_completion_tokens: Some(128_000),
max_context_tokens: 400_000,
}
}
}
fn default_text_model_id() -> String {
"gpt-4.1".to_owned()
"gpt-5.2".to_owned()
}
#[derive(Debug, Clone, Serialize, Deserialize)]
@@ -104,16 +104,16 @@ fn default_speech_to_text_model_id() -> String {
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct TextToSpeechConfig {
#[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")]
pub voice: async_openai::types::Voice,
pub voice: async_openai::types::audio::Voice,
#[serde(default = "default_text_to_speech_speed")]
pub speed: f32,
#[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 {
@@ -127,22 +127,22 @@ impl Default for TextToSpeechConfig {
}
}
fn default_text_to_speech_model_id() -> async_openai::types::SpeechModel {
async_openai::types::SpeechModel::Tts1Hd
fn default_text_to_speech_model_id() -> async_openai::types::audio::SpeechModel {
async_openai::types::audio::SpeechModel::Tts1Hd
}
fn default_text_to_speech_voice() -> async_openai::types::Voice {
async_openai::types::Voice::Onyx
fn default_text_to_speech_voice() -> async_openai::types::audio::Voice {
async_openai::types::audio::Voice::Onyx
}
fn default_text_to_speech_speed() -> f32 {
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.
// Our clients should all have support for it.
async_openai::types::SpeechResponseFormat::Opus
async_openai::types::audio::SpeechResponseFormat::Opus
}
#[derive(Debug, Clone, Serialize, Deserialize)]
@@ -150,13 +150,13 @@ pub struct ImageGenerationConfig {
pub model_id: String,
#[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")]
pub size: Option<async_openai::types::ImageSize>,
pub size: Option<async_openai::types::images::ImageSize>,
#[serde(default = "default_image_quality")]
pub quality: Option<async_openai::types::ImageQuality>,
pub quality: Option<async_openai::types::images::ImageQuality>,
}
impl Default for ImageGenerationConfig {
@@ -173,23 +173,25 @@ impl Default for ImageGenerationConfig {
impl ImageGenerationConfig {
pub fn model_id_as_openai_image_model(
&self,
) -> Result<async_openai::types::ImageModel, String> {
) -> Result<async_openai::types::images::ImageModel, String> {
match self.model_id.as_str() {
"dall-e-2" => Ok(async_openai::types::ImageModel::DallE2),
"dall-e-3" => Ok(async_openai::types::ImageModel::DallE3),
other => Ok(async_openai::types::ImageModel::Other(other.to_owned())),
"dall-e-2" => Ok(async_openai::types::images::ImageModel::DallE2),
"dall-e-3" => Ok(async_openai::types::images::ImageModel::DallE3),
"gpt-image-1" => Ok(async_openai::types::images::ImageModel::GptImage1),
"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
}
fn default_image_size() -> Option<async_openai::types::ImageSize> {
fn default_image_size() -> Option<async_openai::types::images::ImageSize> {
None
}
fn default_image_quality() -> Option<async_openai::types::ImageQuality> {
fn default_image_quality() -> Option<async_openai::types::images::ImageQuality> {
None
}

View File

@@ -4,9 +4,12 @@ use async_openai::{
Client as OpenAIClient,
config::OpenAIConfig,
types::{
ChatCompletionRequestMessage, CreateChatCompletionRequestArgs, CreateImageEditRequestArgs,
CreateImageRequestArgs, CreateSpeechRequestArgs, CreateTranscriptionRequestArgs,
DallE2ImageSize, Image, ImageModel, ImageResponseFormat,
audio::{AudioInput, CreateSpeechRequestArgs, CreateTranscriptionRequestArgs},
chat::{ChatCompletionRequestMessage, CreateChatCompletionRequestArgs},
images::{
CreateImageEditRequestArgs, CreateImageRequestArgs,
Image, ImageInput, ImageModel, ImageResponseFormat,
},
},
};
@@ -38,8 +41,6 @@ use crate::{
use super::config::Config;
use super::OPENAI_IMAGE_MODEL_GPT_IMAGE_1;
#[derive(Debug, Clone)]
pub struct Controller {
config: Config,
@@ -209,11 +210,7 @@ impl ControllerTrait for Controller {
let request = CreateTranscriptionRequestArgs::default()
.model(&speech_to_text_config.model_id)
.file(async_openai::types::AudioInput::from_vec_u8(
filename,
media,
mime_type.to_string(),
))
.file(AudioInput::from_vec_u8(filename, media))
.language(language.clone())
.build()?;
@@ -223,7 +220,7 @@ impl ControllerTrait for Controller {
"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!(
?response,
@@ -255,11 +252,12 @@ impl ControllerTrait for Controller {
let model = if params.cheaper_model_switching_allowed {
// Switch to a cheaper model
match original_model {
async_openai::types::ImageModel::DallE2 => async_openai::types::ImageModel::DallE2,
async_openai::types::ImageModel::DallE3 => async_openai::types::ImageModel::DallE2,
async_openai::types::ImageModel::Other(_) => {
async_openai::types::ImageModel::DallE2
ImageModel::DallE2 => ImageModel::DallE2,
ImageModel::DallE3 => ImageModel::DallE2,
ImageModel::Other(_) => {
ImageModel::DallE2
}
_ => original_model.clone(),
}
} else {
original_model
@@ -269,11 +267,24 @@ impl ControllerTrait for Controller {
// Switch to a cheaper quality
match &image_generation_config.quality {
Some(quality) => match quality {
async_openai::types::ImageQuality::Standard => {
Some(async_openai::types::ImageQuality::Standard)
async_openai::types::images::ImageQuality::Standard => {
Some(async_openai::types::images::ImageQuality::Standard)
}
async_openai::types::ImageQuality::HD => {
Some(async_openai::types::ImageQuality::Standard)
async_openai::types::images::ImageQuality::HD => {
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,
@@ -284,18 +295,17 @@ impl ControllerTrait for Controller {
let size = params
.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);
let response_format = match model.clone() {
ImageModel::DallE2 => 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.
// In fact, specifying the response format results in an error.
OPENAI_IMAGE_MODEL_GPT_IMAGE_1 => None,
_ => Some(ImageResponseFormat::B64Json),
},
// 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.
ImageModel::GptImage1 => None,
ImageModel::GptImage1Mini => None,
ImageModel::Other(_) => Some(ImageResponseFormat::B64Json),
};
let mut request_builder = CreateImageRequestArgs::default();
@@ -329,15 +339,15 @@ impl ControllerTrait for Controller {
"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() {
match image.deref() {
async_openai::types::Image::B64Json {
Image::B64Json {
b64_json,
revised_prompt,
} => {
let bytes = base64_decode(b64_json)?;
let bytes = base64_decode(b64_json.as_ref())?;
return Ok(ImageGenerationResult {
bytes,
@@ -374,15 +384,15 @@ impl ControllerTrait for Controller {
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 {
image_inputs.push(image.into());
}
let dalle2_size = match image_generation_config.size {
Some(async_openai::types::ImageSize::S256x256) => Some(DallE2ImageSize::S256x256),
Some(async_openai::types::ImageSize::S512x512) => Some(DallE2ImageSize::S512x512),
Some(async_openai::types::ImageSize::S1024x1024) => Some(DallE2ImageSize::S1024x1024),
Some(async_openai::types::images::ImageSize::S256x256) => Some(async_openai::types::images::ImageSize::S256x256),
Some(async_openai::types::images::ImageSize::S512x512) => Some(async_openai::types::images::ImageSize::S512x512),
Some(async_openai::types::images::ImageSize::S1024x1024) => Some(async_openai::types::images::ImageSize::S1024x1024),
_ => None,
};
@@ -391,16 +401,17 @@ impl ControllerTrait for Controller {
.map_err(|err| anyhow::anyhow!(err))?;
let response_format = match model.clone() {
async_openai::types::ImageModel::DallE2 => {
Some(async_openai::types::ImageResponseFormat::B64Json)
ImageModel::DallE2 => {
Some(ImageResponseFormat::B64Json)
}
async_openai::types::ImageModel::DallE3 => {
Some(async_openai::types::ImageResponseFormat::B64Json)
ImageModel::DallE3 => {
Some(ImageResponseFormat::B64Json)
}
async_openai::types::ImageModel::Other(model_str) => match model_str.as_str() {
OPENAI_IMAGE_MODEL_GPT_IMAGE_1 => None,
_ => Some(async_openai::types::ImageResponseFormat::B64Json),
},
// 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.
ImageModel::GptImage1 => None,
ImageModel::GptImage1Mini => None,
ImageModel::Other(_) => Some(ImageResponseFormat::B64Json),
};
let mut request_builder = CreateImageEditRequestArgs::default();
@@ -429,12 +440,12 @@ impl ControllerTrait for Controller {
"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() {
match image_data.deref() {
Image::B64Json { b64_json, .. } => {
let bytes = base64_decode(b64_json)?;
let bytes = base64_decode(b64_json.as_ref())?;
return Ok(ImageEditResult {
bytes,
mime_type: mxlink::mime::IMAGE_PNG,
@@ -471,7 +482,7 @@ impl ControllerTrait for Controller {
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.
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));
match voice {
Ok(voice) => voice,
@@ -511,7 +522,7 @@ impl ControllerTrait for Controller {
"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 {
bytes: result.bytes.into(),
@@ -570,15 +581,15 @@ impl ControllerTrait for Controller {
}
fn response_format_to_mime_type(
response_format: &async_openai::types::SpeechResponseFormat,
response_format: &async_openai::types::audio::SpeechResponseFormat,
) -> Option<mxlink::mime::Mime> {
let content_type = match response_format {
async_openai::types::SpeechResponseFormat::Mp3 => "audio/mp3".to_owned(),
async_openai::types::SpeechResponseFormat::Wav => "audio/wav".to_owned(),
async_openai::types::SpeechResponseFormat::Opus => "audio/ogg".to_owned(),
async_openai::types::SpeechResponseFormat::Aac => "audio/aac".to_owned(),
async_openai::types::SpeechResponseFormat::Flac => "audio/flac".to_owned(),
async_openai::types::SpeechResponseFormat::Pcm => "audio/L8".to_owned(),
async_openai::types::audio::SpeechResponseFormat::Mp3 => "audio/mp3".to_owned(),
async_openai::types::audio::SpeechResponseFormat::Wav => "audio/wav".to_owned(),
async_openai::types::audio::SpeechResponseFormat::Opus => "audio/ogg".to_owned(),
async_openai::types::audio::SpeechResponseFormat::Aac => "audio/aac".to_owned(),
async_openai::types::audio::SpeechResponseFormat::Flac => "audio/flac".to_owned(),
async_openai::types::audio::SpeechResponseFormat::Pcm => "audio/L8".to_owned(),
};
match content_type.parse() {

View File

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

View File

@@ -161,11 +161,11 @@ impl TryInto<OpenAITextToSpeechConfig> for TextToSpeechConfig {
type Error = String;
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,
)?;
@@ -224,7 +224,7 @@ impl TryInto<OpenAIImageGenerationConfig> for ImageGenerationConfig {
fn try_into(self) -> Result<OpenAIImageGenerationConfig, Self::Error> {
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,
)?)
} else {
@@ -232,7 +232,7 @@ impl TryInto<OpenAIImageGenerationConfig> for ImageGenerationConfig {
};
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,
)?)
} else {
@@ -240,7 +240,7 @@ impl TryInto<OpenAIImageGenerationConfig> for ImageGenerationConfig {
};
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,
)?)
} else {

View File

@@ -1,3 +1,4 @@
use std::fs;
use std::sync::Arc;
use std::{future::Future, pin::Pin};
@@ -6,6 +7,7 @@ use mxlink::matrix_sdk::media::{MediaFormat, MediaRequestParameters};
use mxlink::matrix_sdk::ruma::{
MilliSecondsSinceUnixEpoch, OwnedUserId, events::room::MediaSource,
};
use mxlink::matrix_sdk::ruma::api::client::profile::{AvatarUrl, DisplayName};
use mxlink::{
InitConfig, LoginConfig, LoginCredentials, LoginEncryption, MatrixLink, PersistenceConfig,
@@ -17,12 +19,13 @@ use mxlink::helpers::account_data_config::{
RoomConfigManager as AccountDataRoomConfigManager,
};
use mxlink::helpers::encryption::Manager as EncryptionManager;
use mxlink::mime::Mime;
use crate::agent::Manager as AgentManager;
use crate::entity::catch_up_marker::{
CatchUpMarker, CatchUpMarkerManager, DelayedCatchUpMarkerManager,
};
use crate::entity::cfg::Config;
use crate::entity::cfg::{Avatar, Config};
use crate::entity::globalconfig::{GlobalConfig, GlobalConfigurationManager};
use crate::entity::roomconfig::{RoomConfig, RoomConfigurationManager};
@@ -285,24 +288,27 @@ impl Bot {
async fn do_prepare_profile(&self) -> anyhow::Result<()> {
tracing::debug!("Preparing profile..");
let desired_display_name = self.inner.config.user.name.clone();
let account = self.inner.matrix_link.client().account();
let media = self.inner.matrix_link.client().media();
let desired_display_name = self.inner.config.user.name.clone();
let profile = account
.fetch_user_profile()
.await
.map_err(|e| anyhow::anyhow!("Failed fetching profile: {:?}", e))?;
let should_update_display_name = match &profile.displayname {
let current_display_name = profile.get_static::<DisplayName>()?;
let current_avatar_url = profile.get_static::<AvatarUrl>()?;
let should_update_display_name = match &current_display_name {
Some(displayname) => displayname != &desired_display_name,
None => true,
};
if should_update_display_name {
tracing::info!(
?profile.displayname,
?current_display_name,
?desired_display_name,
"Updating display name.."
);
@@ -312,34 +318,72 @@ impl Bot {
}
}
let should_update_avatar = match &profile.avatar_url {
Some(avatar_url) => {
let request = MediaRequestParameters {
source: MediaSource::Plain(avatar_url.to_owned()),
format: MediaFormat::File,
};
let content = media
.get_media_content(&request, true)
.await
.map_err(|e| anyhow::anyhow!("Failed fetching existing avatar: {:?}", e))?;
content.as_slice() != LOGO_BYTES
let desired_avatar: Option<(Vec<u8>, Mime)> = match &self.inner.config.user.avatar {
Avatar::Keep => {
tracing::info!("Avatar configured to keep current, skipping avatar management");
None
}
Avatar::Default => {
tracing::info!("Avatar configured to use default");
Some((
LOGO_BYTES.to_vec(),
LOGO_MIME_TYPE
.parse()
.expect("Failed parsing mime type for logo"),
))
}
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 {
tracing::info!("Updating avatar..");
if let Some((desired_bytes, mime_type)) = desired_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
.parse()
.expect("Failed parsing mime type for logo");
let content = media
.get_media_content(&request, true)
.await
.map_err(|e| anyhow::anyhow!("Failed fetching existing avatar: {:?}", e))?;
account
.upload_avatar(&mime_type, LOGO_BYTES.to_vec())
.await
.map_err(|e| anyhow::anyhow!("Failed uploading avatar: {:?}", e))?;
let needs_update = content.as_slice() != desired_bytes;
tracing::debug!(
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(())

View File

@@ -5,7 +5,7 @@ use anyhow::anyhow;
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> {
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 => {
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_AVATAR => {
config.user.avatar = Avatar::from_string(value);
}
cfg_env::BAIBOT_COMMAND_PREFIX => config.command_prefix = value,
cfg_env::BAIBOT_ROOM_POST_JOIN_SELF_INTRODUCTION_ENABLED => {
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 => {
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 => {
config.persistence.config_encryption_key = Some(value);
}

View File

@@ -1,7 +1,7 @@
use std::path::PathBuf;
use mxlink::helpers::encryption::EncryptionKey;
use serde::{Deserialize, Serialize};
use serde::{Deserialize, Deserializer, Serialize};
use crate::{
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)]
pub struct ConfigUser {
pub mxid_localpart: String,
@@ -93,6 +139,9 @@ pub struct ConfigUser {
#[serde(default)]
pub encryption: ConfigUserEncryption,
#[serde(default)]
pub avatar: Avatar,
}
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_PASSWORD: &str = "BAIBOT_USER_PASSWORD";
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 =
"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";

View File

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