initial commit
This commit is contained in:
commit
9bf77013fd
9
.env.example
Normal file
9
.env.example
Normal file
|
@ -0,0 +1,9 @@
|
|||
homeserver_url="https://matrix.org"
|
||||
username="bot"
|
||||
password="bot"
|
||||
|
||||
ollama_host="http://localhost"
|
||||
ollama_port=1111
|
||||
ollama_model="neural-chat:latest"
|
||||
|
||||
prefix=".ask"
|
3
.gitignore
vendored
Normal file
3
.gitignore
vendored
Normal file
|
@ -0,0 +1,3 @@
|
|||
/target
|
||||
result*
|
||||
.env
|
3476
Cargo.lock
generated
Normal file
3476
Cargo.lock
generated
Normal file
File diff suppressed because it is too large
Load diff
19
Cargo.toml
Normal file
19
Cargo.toml
Normal file
|
@ -0,0 +1,19 @@
|
|||
[package]
|
||||
name = "celestial"
|
||||
version = "0.1.0"
|
||||
edition = "2021"
|
||||
license = "MIT"
|
||||
|
||||
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
|
||||
|
||||
[dependencies]
|
||||
anyhow = "1.0.82"
|
||||
dotenv = "0.15.0"
|
||||
env_logger = "0.11.3"
|
||||
envy = "0.4.2"
|
||||
log = "0.4.21"
|
||||
matrix-sdk = "0.7.1"
|
||||
ollama-rs = "0.1.9"
|
||||
serde = "1.0.200"
|
||||
tokio = { version = "1.24.2", features = ["macros", "rt-multi-thread"] }
|
||||
tracing-subscriber = "0.3.15"
|
20
default.nix
Normal file
20
default.nix
Normal file
|
@ -0,0 +1,20 @@
|
|||
{ lib, rustPlatform, fetchFromGitea, pkgs, stdenv, ... }:
|
||||
|
||||
rustPlatform.buildRustPackage rec {
|
||||
pname = "celestial";
|
||||
version = "1.1.1";
|
||||
|
||||
nativeBuildInputs = lib.optionals stdenv.isLinux [ pkgs.pkg-config ];
|
||||
OPENSSL_NO_VENDOR = 1;
|
||||
buildInputs = lib.optionals stdenv.isLinux [ pkgs.openssl ];
|
||||
|
||||
src = "./.";
|
||||
# TODO
|
||||
#cargoHash = "sha256-dpN7hHpqSur6KjtEikVjQMqnF8PW27ax7ccpQNU5vdA=";
|
||||
|
||||
meta = with lib; {
|
||||
description = "Celestial ai bot";
|
||||
homepage = "https://git.4o1x5.dev/4o1x5/celestial";
|
||||
license = licenses.mit;
|
||||
};
|
||||
}
|
121
flake.lock
Normal file
121
flake.lock
Normal file
|
@ -0,0 +1,121 @@
|
|||
{
|
||||
"nodes": {
|
||||
"advisory-db": {
|
||||
"flake": false,
|
||||
"locked": {
|
||||
"lastModified": 1713971667,
|
||||
"narHash": "sha256-VtlITecqZHZcm/Fzfa+0IP7I3gcFe5AS+AG0Tv1qIHk=",
|
||||
"owner": "rustsec",
|
||||
"repo": "advisory-db",
|
||||
"rev": "1d1431ceb4d312c0d5c98be63d518b5d472a1149",
|
||||
"type": "github"
|
||||
},
|
||||
"original": {
|
||||
"owner": "rustsec",
|
||||
"repo": "advisory-db",
|
||||
"type": "github"
|
||||
}
|
||||
},
|
||||
"crane": {
|
||||
"inputs": {
|
||||
"nixpkgs": [
|
||||
"nixpkgs"
|
||||
]
|
||||
},
|
||||
"locked": {
|
||||
"lastModified": 1713979152,
|
||||
"narHash": "sha256-apdecPuh8SOQnkEET/kW/UcfjCRb8JbV5BKjoH+DcP4=",
|
||||
"owner": "ipetkov",
|
||||
"repo": "crane",
|
||||
"rev": "a5eca68a2cf11adb32787fc141cddd29ac8eb79c",
|
||||
"type": "github"
|
||||
},
|
||||
"original": {
|
||||
"owner": "ipetkov",
|
||||
"repo": "crane",
|
||||
"type": "github"
|
||||
}
|
||||
},
|
||||
"fenix": {
|
||||
"inputs": {
|
||||
"nixpkgs": [
|
||||
"nixpkgs"
|
||||
],
|
||||
"rust-analyzer-src": []
|
||||
},
|
||||
"locked": {
|
||||
"lastModified": 1714026264,
|
||||
"narHash": "sha256-rIRsxOZ/eUnWVHfbJlXXQtYriPICFgHGyao5jxm1FMQ=",
|
||||
"owner": "nix-community",
|
||||
"repo": "fenix",
|
||||
"rev": "4e14e4f21fbd7afae40a492b7d937cbf16f76b11",
|
||||
"type": "github"
|
||||
},
|
||||
"original": {
|
||||
"owner": "nix-community",
|
||||
"repo": "fenix",
|
||||
"type": "github"
|
||||
}
|
||||
},
|
||||
"flake-utils": {
|
||||
"inputs": {
|
||||
"systems": "systems"
|
||||
},
|
||||
"locked": {
|
||||
"lastModified": 1710146030,
|
||||
"narHash": "sha256-SZ5L6eA7HJ/nmkzGG7/ISclqe6oZdOZTNoesiInkXPQ=",
|
||||
"owner": "numtide",
|
||||
"repo": "flake-utils",
|
||||
"rev": "b1d9ab70662946ef0850d488da1c9019f3a9752a",
|
||||
"type": "github"
|
||||
},
|
||||
"original": {
|
||||
"owner": "numtide",
|
||||
"repo": "flake-utils",
|
||||
"type": "github"
|
||||
}
|
||||
},
|
||||
"nixpkgs": {
|
||||
"locked": {
|
||||
"lastModified": 1713805509,
|
||||
"narHash": "sha256-YgSEan4CcrjivCNO5ZNzhg7/8ViLkZ4CB/GrGBVSudo=",
|
||||
"owner": "NixOS",
|
||||
"repo": "nixpkgs",
|
||||
"rev": "1e1dc66fe68972a76679644a5577828b6a7e8be4",
|
||||
"type": "github"
|
||||
},
|
||||
"original": {
|
||||
"owner": "NixOS",
|
||||
"ref": "nixpkgs-unstable",
|
||||
"repo": "nixpkgs",
|
||||
"type": "github"
|
||||
}
|
||||
},
|
||||
"root": {
|
||||
"inputs": {
|
||||
"advisory-db": "advisory-db",
|
||||
"crane": "crane",
|
||||
"fenix": "fenix",
|
||||
"flake-utils": "flake-utils",
|
||||
"nixpkgs": "nixpkgs"
|
||||
}
|
||||
},
|
||||
"systems": {
|
||||
"locked": {
|
||||
"lastModified": 1681028828,
|
||||
"narHash": "sha256-Vy1rq5AaRuLzOxct8nz4T6wlgyUR7zLU309k9mBC768=",
|
||||
"owner": "nix-systems",
|
||||
"repo": "default",
|
||||
"rev": "da67096a3b9bf56a91d16901293e51ba5b49a27e",
|
||||
"type": "github"
|
||||
},
|
||||
"original": {
|
||||
"owner": "nix-systems",
|
||||
"repo": "default",
|
||||
"type": "github"
|
||||
}
|
||||
}
|
||||
},
|
||||
"root": "root",
|
||||
"version": 7
|
||||
}
|
140
flake.nix
Normal file
140
flake.nix
Normal file
|
@ -0,0 +1,140 @@
|
|||
{
|
||||
description = "Build a cargo project";
|
||||
|
||||
inputs = {
|
||||
nixpkgs.url = "github:NixOS/nixpkgs/nixpkgs-unstable";
|
||||
|
||||
crane = {
|
||||
url = "github:ipetkov/crane";
|
||||
inputs.nixpkgs.follows = "nixpkgs";
|
||||
};
|
||||
|
||||
fenix = {
|
||||
url = "github:nix-community/fenix";
|
||||
inputs.nixpkgs.follows = "nixpkgs";
|
||||
inputs.rust-analyzer-src.follows = "";
|
||||
};
|
||||
|
||||
flake-utils.url = "github:numtide/flake-utils";
|
||||
|
||||
advisory-db = {
|
||||
url = "github:rustsec/advisory-db";
|
||||
flake = false;
|
||||
};
|
||||
};
|
||||
|
||||
outputs = { self, nixpkgs, crane, fenix, flake-utils, advisory-db, ... }:
|
||||
flake-utils.lib.eachDefaultSystem (system:
|
||||
let
|
||||
pkgs = nixpkgs.legacyPackages.${system};
|
||||
|
||||
inherit (pkgs) lib;
|
||||
|
||||
craneLib = crane.lib.${system};
|
||||
src = craneLib.cleanCargoSource (craneLib.path ./.);
|
||||
|
||||
commonArgs = {
|
||||
inherit src;
|
||||
strictDeps = true;
|
||||
|
||||
buildInputs = with pkgs; [
|
||||
pkg-config
|
||||
openssl
|
||||
] ++ lib.optionals pkgs.stdenv.isDarwin [
|
||||
pkgs.libiconv
|
||||
];
|
||||
|
||||
};
|
||||
|
||||
craneLibLLvmTools = craneLib.overrideToolchain
|
||||
(fenix.packages.${system}.complete.withComponents [
|
||||
"cargo"
|
||||
"llvm-tools"
|
||||
"rustc"
|
||||
]);
|
||||
|
||||
# Build *just* the cargo dependencies, so we can reuse
|
||||
# all of that work (e.g. via cachix) when running in CI
|
||||
cargoArtifacts = craneLib.buildDepsOnly commonArgs;
|
||||
|
||||
# Build the actual crate itself, reusing the dependency
|
||||
# artifacts from above.
|
||||
celestial = craneLib.buildPackage (commonArgs // {
|
||||
inherit cargoArtifacts;
|
||||
});
|
||||
in
|
||||
{
|
||||
checks = {
|
||||
# Build the crate as part of `nix flake check` for convenience
|
||||
inherit celestial;
|
||||
|
||||
# Run clippy (and deny all warnings) on the crate source,
|
||||
# again, reusing the dependency artifacts from above.
|
||||
#
|
||||
# Note that this is done as a separate derivation so that
|
||||
# we can block the CI if there are issues here, but not
|
||||
# prevent downstream consumers from building our crate by itself.
|
||||
celestial-clippy = craneLib.cargoClippy (commonArgs // {
|
||||
inherit cargoArtifacts;
|
||||
cargoClippyExtraArgs = "--all-targets -- --deny warnings";
|
||||
});
|
||||
|
||||
celestial-doc = craneLib.cargoDoc (commonArgs // {
|
||||
inherit cargoArtifacts;
|
||||
});
|
||||
|
||||
# Check formatting
|
||||
celestial-fmt = craneLib.cargoFmt {
|
||||
inherit src;
|
||||
};
|
||||
|
||||
# Audit dependencies
|
||||
celestial-audit = craneLib.cargoAudit {
|
||||
inherit src advisory-db;
|
||||
};
|
||||
|
||||
# Audit licenses
|
||||
celestial-deny = craneLib.cargoDeny {
|
||||
inherit src;
|
||||
};
|
||||
|
||||
# Run tests with cargo-nextest
|
||||
# Consider setting `doCheck = false` on `celestial` if you do not want
|
||||
# the tests to run twice
|
||||
celestial-nextest = craneLib.cargoNextest (commonArgs // {
|
||||
inherit cargoArtifacts;
|
||||
partitions = 1;
|
||||
partitionType = "count";
|
||||
});
|
||||
};
|
||||
|
||||
packages = {
|
||||
default = celestial;
|
||||
} // lib.optionalAttrs (!pkgs.stdenv.isDarwin) {
|
||||
celestial-llvm-coverage = craneLibLLvmTools.cargoLlvmCov (commonArgs // {
|
||||
inherit cargoArtifacts;
|
||||
});
|
||||
};
|
||||
|
||||
apps.default = flake-utils.lib.mkApp {
|
||||
drv = celestial;
|
||||
};
|
||||
|
||||
devShells.default = craneLib.devShell {
|
||||
# Inherit inputs from checks.
|
||||
checks = self.checks.${system};
|
||||
|
||||
LIBCLANG_PATH = "${pkgs.llvmPackages_17.libclang.lib}/lib";
|
||||
RUST_BACKTRACE = 1;
|
||||
RUST_SRC_PATH = "${pkgs.rust.packages.stable.rustPlatform.rustLibSrc}";
|
||||
|
||||
packages = with pkgs; [
|
||||
rustfmt
|
||||
rust-analyzer
|
||||
clippy
|
||||
openssl
|
||||
sqlite
|
||||
];
|
||||
};
|
||||
});
|
||||
}
|
4
readme.md
Normal file
4
readme.md
Normal file
|
@ -0,0 +1,4 @@
|
|||
### Celestial
|
||||
|
||||
This is just a funny project I slapped together in a span of around 30 minutes.
|
||||
I plan on expanding it with other features in the future.
|
188
src/main.rs
Normal file
188
src/main.rs
Normal file
|
@ -0,0 +1,188 @@
|
|||
use std::time::Instant;
|
||||
|
||||
use matrix_sdk::{
|
||||
config::SyncSettings,
|
||||
event_handler::Ctx,
|
||||
ruma::events::room::{
|
||||
member::StrippedRoomMemberEvent,
|
||||
message::{RoomMessageEventContent, SyncRoomMessageEvent},
|
||||
},
|
||||
Client, Room,
|
||||
};
|
||||
use ollama_rs::{generation::completion::request::GenerationRequest, Ollama};
|
||||
use serde::Deserialize;
|
||||
use tokio::time::{sleep, Duration};
|
||||
|
||||
#[derive(Deserialize, Debug)]
|
||||
struct Config {
|
||||
homeserver_url: String,
|
||||
username: String,
|
||||
password: String,
|
||||
|
||||
ollama_host: String,
|
||||
ollama_port: i16,
|
||||
ollama_model: String,
|
||||
|
||||
prefix: String,
|
||||
}
|
||||
#[tokio::main]
|
||||
async fn main() -> anyhow::Result<()> {
|
||||
tracing_subscriber::fmt::init();
|
||||
let _ = dotenv::dotenv();
|
||||
log::info!("Bot has started");
|
||||
let c = envy::from_env::<Config>().expect("Please provide variables");
|
||||
|
||||
login_and_sync(
|
||||
c.homeserver_url.to_string(),
|
||||
&c.username,
|
||||
&c.password,
|
||||
c.prefix,
|
||||
c.ollama_host,
|
||||
c.ollama_port,
|
||||
c.ollama_model,
|
||||
)
|
||||
.await?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
// The core sync loop we have running.
|
||||
async fn login_and_sync(
|
||||
homeserver_url: String,
|
||||
username: &str,
|
||||
password: &str,
|
||||
prefix: String,
|
||||
ollama_host: String,
|
||||
ollama_port: i16,
|
||||
ollama_model: String,
|
||||
) -> anyhow::Result<()> {
|
||||
let client = Client::builder()
|
||||
.homeserver_url(homeserver_url)
|
||||
.build()
|
||||
.await?;
|
||||
|
||||
client
|
||||
.matrix_auth()
|
||||
.login_username(username, password)
|
||||
.initial_device_display_name("getting started bot")
|
||||
.await?;
|
||||
|
||||
println!("logged in as {username}");
|
||||
|
||||
let ollama = Ollama::new(ollama_host, ollama_port as u16);
|
||||
|
||||
client.add_event_handler(on_stripped_state_member);
|
||||
let sync_token = client
|
||||
.sync_once(SyncSettings::default())
|
||||
.await
|
||||
.unwrap()
|
||||
.next_batch;
|
||||
|
||||
#[derive(Debug, Clone)]
|
||||
struct MyContext {
|
||||
botId: String,
|
||||
ai: Ollama,
|
||||
}
|
||||
client.add_event_handler_context(MyContext {
|
||||
botId: client.clone().user_id().unwrap().to_string(),
|
||||
ai: ollama.clone(),
|
||||
});
|
||||
|
||||
client.add_event_handler(
|
||||
|ev: SyncRoomMessageEvent, room: Room, context: Ctx<MyContext>| async move {
|
||||
if ev.as_original().unwrap().sender.to_string() == context.botId {
|
||||
return;
|
||||
};
|
||||
|
||||
// add . prefix
|
||||
if !ev
|
||||
.as_original()
|
||||
.unwrap()
|
||||
.content
|
||||
.body()
|
||||
.to_string()
|
||||
.contains(prefix.as_str())
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
// Send seen
|
||||
room.send_single_receipt(
|
||||
matrix_sdk::ruma::api::client::receipt::create_receipt::v3::ReceiptType::Read,
|
||||
matrix_sdk::ruma::events::receipt::ReceiptThread::Main,
|
||||
ev.event_id().to_owned(),
|
||||
)
|
||||
.await;
|
||||
|
||||
room.typing_notice(true).await;
|
||||
|
||||
let prompt = format!(
|
||||
"{} says: {}",
|
||||
room.get_member(&ev.sender())
|
||||
.await
|
||||
.unwrap()
|
||||
.unwrap()
|
||||
.display_name()
|
||||
.unwrap(),
|
||||
ev.as_original()
|
||||
.unwrap()
|
||||
.content
|
||||
.body()
|
||||
.replace(prefix.as_str(), "")
|
||||
);
|
||||
let now = Instant::now();
|
||||
let res = ollama
|
||||
.generate(GenerationRequest::new(ollama_model, prompt))
|
||||
.await;
|
||||
|
||||
if let Ok(res) = res {
|
||||
let mut asd = res.response.to_string();
|
||||
asd.push_str(format!("\n prompt took {:?}", now.elapsed()).as_str());
|
||||
|
||||
let content = RoomMessageEventContent::text_plain(asd);
|
||||
println!("Got res from ai: {}", res.clone().response);
|
||||
room.send(content).await.unwrap();
|
||||
|
||||
room.typing_notice(false).await;
|
||||
}
|
||||
},
|
||||
);
|
||||
|
||||
let settings = SyncSettings::default().token(sync_token);
|
||||
client.sync(settings).await?;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
async fn on_stripped_state_member(
|
||||
room_member: StrippedRoomMemberEvent,
|
||||
client: Client,
|
||||
room: Room,
|
||||
) {
|
||||
if room_member.state_key != client.user_id().unwrap() {
|
||||
return;
|
||||
}
|
||||
|
||||
tokio::spawn(async move {
|
||||
println!("Autojoining room {}", room.room_id());
|
||||
let mut delay = 2;
|
||||
|
||||
while let Err(err) = room.join().await {
|
||||
// retry autojoin due to synapse sending invites, before the
|
||||
// invited user can join for more information see
|
||||
// https://github.com/matrix-org/synapse/issues/4345
|
||||
eprintln!(
|
||||
"Failed to join room {} ({err:?}), retrying in {delay}s",
|
||||
room.room_id()
|
||||
);
|
||||
|
||||
sleep(Duration::from_secs(delay)).await;
|
||||
delay *= 2;
|
||||
|
||||
if delay > 3600 {
|
||||
eprintln!("Can't join room {} ({err:?})", room.room_id());
|
||||
break;
|
||||
}
|
||||
}
|
||||
println!("Successfully joined room {}", room.room_id());
|
||||
});
|
||||
}
|
Loading…
Reference in a new issue