Compare commits

..

No commits in common. "d2c22bcb407df746fbbf5279dcccd57e6409427e" and "6b4eb7c271022af12b099e1d0cb70ef2be670d92" have entirely different histories.

20 changed files with 1767 additions and 985 deletions

View File

@ -1,5 +1,5 @@
[registry]
default = "crates-io"
[registries.elnafo-vcs]
[registries.vcs-elnafo]
index = "sparse+https://vcs.elnafo.ru/api/packages/L-Nafaryus/cargo/"

1
.envrc Normal file
View File

@ -0,0 +1 @@
use flake

View File

@ -6,12 +6,6 @@ on:
- master
jobs:
check:
runs-on: nix-runner
steps:
- uses: actions/checkout@v4
- run: |
NIXPKGS_ALLOW_BROKEN=1 nix flake check --allow-import-from-derivation --keep-going --impure
build:
runs-on: nix-runner
steps:

1104
Cargo.lock generated

File diff suppressed because it is too large Load Diff

View File

@ -1,24 +1,18 @@
[package]
name = "oscuro"
version = "0.1.1"
version = "0.1.0"
edition = "2021"
description = "Oscuro is a fancy multibot"
description = "Oscuro - a fancy discord bot"
license = "MIT"
repository = "https://vcs.elnafo.ru/L-Nafaryus/oscuro"
publish = ["vcs-elnafo"]
[dependencies]
async-process = "2.2.3"
poise = "0.6.1"
rand = "0.8.5"
serde = { version = "1.0.204", features = ["derive"] }
serde_json = "1.0.120"
teloxide = { version = "0.12.2", features = ["macros"] }
oscuro-core = { version = "0.1.0", path = "crates/oscuro-core", registry = "vcs-elnafo" }
tokio = { version = "1.26.0", features = ["macros", "rt-multi-thread"] }
toml = "0.8.15"
tracing = "0.1.40"
tracing-subscriber = { version = "0.3.18", features = ["env-filter"] }
[workspace]
resolver = "2"
members = []
members = ["crates/oscuro-core", "crates/oscuro-shuttle", "crates/oscuro-telegram"]

View File

@ -0,0 +1,18 @@
[package]
name = "oscuro-core"
version = "0.1.0"
edition = "2021"
description = "Core of a fancy discord bot Oscuro"
license = "MIT"
repository = "https://vcs.elnafo.ru/L-Nafaryus/oscuro"
publish = ["vcs-elnafo"]
[dependencies]
poise = "0.6.1"
rand = "0.8.5"
serde = { version = "1.0.198", features = ["derive"] }
serde_json = "1.0.116"
teloxide = { version = "0.12.2", features = ["macros"] }
tokio = { version = "1.26.0", features = ["macros", "rt-multi-thread"] }
toml = "0.8.12"
tracing = "0.1.37"

View File

@ -0,0 +1,40 @@
use poise::serenity_prelude as serenity;
use rand::Rng;
use super::errors::BoxedError;
use super::Context;
#[poise::command(prefix_command)]
pub async fn register(ctx: Context<'_>) -> Result<(), BoxedError> {
poise::builtins::register_application_commands_buttons(ctx).await?;
Ok(())
}
#[poise::command(slash_command, prefix_command)]
pub async fn age(
ctx: Context<'_>,
#[description = "Ooph user"] user: Option<serenity::User>,
) -> Result<(), BoxedError> {
let u = user.as_ref().unwrap_or_else(|| ctx.author());
let response = format!("{}'s account was created at {}", u.name, u.created_at());
ctx.say(response).await?;
Ok(())
}
#[poise::command(slash_command, prefix_command)]
pub async fn dice(ctx: Context<'_>) -> Result<(), BoxedError> {
let number = {
let mut rng = rand::thread_rng();
rng.gen_range(1..21)
};
let response = format!("{} throws {}.", ctx.author(), number);
let response = match number {
20 => format!("{} Critical success.", response),
1 => format!("{} Critical failure.", response),
_ => response,
};
ctx.say(response).await?;
Ok(())
}

View File

@ -1,12 +1,10 @@
use std::env;
use std::fs;
use serde::{Deserialize, Serialize};
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct Config {
pub discord_token: Option<String>,
pub telegram_token: Option<String>,
pub discord_token: String,
}
impl Config {
@ -30,24 +28,12 @@ impl Config {
pub fn write(&self, path: &std::path::Path) -> Result<(), ConfigError> {
Ok(fs::write(path, self.to_string()?)?)
}
pub fn with_env(mut self) -> Self {
if let Ok(token) = env::var("OSCURO_DISCORD_TOKEN") {
self.discord_token = Some(token);
};
if let Ok(token) = env::var("OSCURO_TELEGRAM_TOKEN") {
self.telegram_token = Some(token);
};
self
}
}
impl Default for Config {
fn default() -> Self {
Self {
discord_token: Some(String::new()),
telegram_token: Some(String::new()),
discord_token: String::new(),
}
}
}
@ -72,10 +58,10 @@ impl std::error::Error for ConfigError {}
impl std::fmt::Display for ConfigError {
fn fmt(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result {
match self {
Self::Parse => write!(f, "Failed to parse config from string"),
Self::Parse => write!(f, "Failed to parse Config from string"),
Self::StringParse => write!(f, "Failed to parse environment variable"),
Self::Serialize => write!(f, "Failed to serialize Config to TOML"),
Self::IO => write!(f, "Failed to write file"),
Self::IO => write!(f, "Faild to write file"),
}
}
}

View File

@ -0,0 +1,76 @@
pub mod commands;
pub mod config;
pub mod errors;
use errors::BoxedError;
use poise::serenity_prelude::{self as serenity, prelude::TypeMapKey, Client};
use teloxide::prelude::*;
use teloxide::types::Recipient;
#[derive(Debug, Clone)]
pub struct AppState {
pub config: config::Config,
}
impl TypeMapKey for AppState {
type Value = AppState;
}
type Context<'a> = poise::Context<'a, AppState, BoxedError>;
pub async fn client(state: AppState) -> Result<Client, BoxedError> {
let intents = serenity::GatewayIntents::non_privileged();
let state_copy = state.clone();
let framework = poise::Framework::builder()
.options(poise::FrameworkOptions {
commands: vec![commands::register(), commands::age(), commands::dice()],
event_handler: |ctx, event, framework, data| {
Box::pin(event_handler(ctx, event, framework, data))
},
..Default::default()
})
.setup(|ctx, _ready, framework| {
Box::pin(async move {
poise::builtins::register_globally(ctx, &framework.options().commands).await?;
Ok(state_copy)
})
})
.build();
let client = serenity::ClientBuilder::new(state.clone().config.discord_token, intents)
.framework(framework)
.await?;
{
let mut data = client.data.write().await;
data.insert::<AppState>(state);
}
Ok(client)
}
async fn event_handler(
_ctx: &serenity::Context,
event: &serenity::FullEvent,
_framework: poise::FrameworkContext<'_, AppState, BoxedError>,
_state: &AppState,
) -> Result<(), BoxedError> {
match event {
serenity::FullEvent::Ready { data_about_bot, .. } => {
println!("Logged in as {}", data_about_bot.user.name);
}
serenity::FullEvent::Message { new_message } => {
println!("{:?}", new_message.clone());
let bot = Bot::from_env();
bot.send_message(
Recipient::Id(ChatId(-4221527632)),
new_message.author.name.clone(),
)
.await
.expect("err");
}
_ => {}
}
Ok(())
}

View File

@ -0,0 +1,13 @@
[package]
name = "oscuro-shuttle"
version = "0.1.0"
edition = "2021"
publish = false
[package.metadata.release]
release = false
[dependencies]
oscuro-core = { version = "0.1.0", path = "../oscuro-core" }
shuttle-runtime = "0.43.0"
shuttle-serenity = "0.43.0"

View File

@ -0,0 +1,20 @@
use oscuro_core::{client, config::Config, AppState};
#[shuttle_runtime::main]
async fn main(
#[shuttle_runtime::Secrets] secrets: shuttle_runtime::SecretStore,
) -> shuttle_serenity::ShuttleSerenity {
let token = secrets
.get("discord_token")
.expect("Variable 'DISCORD_TOKEN' must be set");
let state = AppState {
config: Config {
discord_token: token,
},
};
let client = client(state).await.expect("Failed to create client");
Ok(client.into())
}

View File

@ -0,0 +1,12 @@
[package]
name = "oscuro-telegram"
version = "0.1.0"
edition = "2021"
publish = ["vcs-elnafo"]
[dependencies]
log = "0.4.22"
pretty_env_logger = "0.5.0"
serenity = "0.12.2"
teloxide = { version = "0.12.2", features = ["macros"] }
tokio = { version = "1.38.0", features = ["rt-multi-thread", "macros"] }

View File

@ -0,0 +1,31 @@
use serenity::builder::ExecuteWebhook;
use serenity::http::Http;
use serenity::model::webhook::Webhook;
use teloxide::prelude::*;
use teloxide::types::Recipient;
#[tokio::main]
async fn main() {
pretty_env_logger::init();
log::info!("Starting throw dice bot...");
let bot = Bot::from_env();
/*let http = Http::new("");
let webhook = Webhook::from_url(&http, "https://discord.com/api/webhooks/1259860143579987999/whI0ozB5uc17Wdzkb2-HSrVGi8h_MyR2_4eyCsGuGpQN4KcjMhq7rfQH1JIdbD1HNaW_")
.await
.expect("Replace the webhook with your own");
let builder = ExecuteWebhook::new().content("hello there").username("Webhook test");
webhook.execute(&http, false, builder).await.expect("Could not execute webhook.");
*/
teloxide::repl(bot, |bot: Bot, msg: Message| async move {
bot.send_dice(msg.chat.id).await?;
Ok(())
})
.await;
/*bot.send_message(Recipient::Id(ChatId(-4221527632)), "Heya!")
.await
.expect("err");*/
}

File diff suppressed because it is too large Load Diff

View File

@ -1,23 +1,14 @@
{
description = "Oscuro is a fancy multibot";
description = "Oscuro - a fancy discord bot";
nixConfig = {
extra-substituters = [
"https://cache.elnafo.ru"
"https://bonfire.cachix.org"
];
extra-trusted-public-keys = [
"cache.elnafo.ru:j3VD+Hn+is2Qk3lPXDSdPwHJQSatizk7V82iJ2RP1yo="
"bonfire.cachix.org-1:mzAGBy/Crdf8NhKail5ciK7ZrGRbPJJobW6TwFb7WYM="
];
extra-substituters = ["https://bonfire.cachix.org"];
extra-trusted-public-keys = ["bonfire.cachix.org-1:mzAGBy/Crdf8NhKail5ciK7ZrGRbPJJobW6TwFb7WYM="];
};
inputs = {
bonfire = {
url = "github:L-Nafaryus/bonfire";
inputs = {
oscuro.follows = "";
};
};
nixpkgs.follows = "bonfire/nixpkgs";
};
@ -28,57 +19,49 @@
bonfire,
...
}: let
pkgs = nixpkgs.legacyPackages.x86_64-linux;
lib = pkgs.lib;
fenixPkgs = bonfire.inputs.fenix.packages.x86_64-linux;
craneLib = (bonfire.inputs.crane.mkLib pkgs).overrideToolchain fenixPkgs.complete.toolchain;
forAllSystems = nixpkgs.lib.genAttrs ["x86_64-linux"];
nixpkgsFor = forAllSystems (system: import nixpkgs {inherit system;});
in {
packages.x86_64-linux = rec {
oscuro = let
common = {
src = pkgs.lib.cleanSourceWith {
src = ./.;
filter = path: type: (craneLib.filterCargoSources path type);
};
packages = forAllSystems (system: let
pkgs = nixpkgsFor.${system};
crane-lib = bonfire.inputs.crane.lib.${system};
strictDeps = true;
src = pkgs.lib.cleanSourceWith {
src = ./.;
filter = path: type: (crane-lib.filterCargoSources path type);
};
nativeBuildInputs = [pkgs.pkg-config pkgs.makeWrapper];
common = {
inherit src;
pname = "oscuro";
version = "0.1.0";
strictDeps = true;
};
buildInputs = [pkgs.openssl];
};
cargoArtifacts = crane-lib.buildDepsOnly common;
in {
oscuro = crane-lib.buildPackage (common // {inherit cargoArtifacts;});
cargoArtifacts = craneLib.buildDepsOnly common;
in
craneLib.buildPackage (common
// rec {
pname = "oscuro";
version = "0.1.0";
inherit cargoArtifacts;
postInstall = ''
wrapProgram $out/bin/${pname} \
--prefix LD_LIBRARY_PATH : ${pkgs.lib.makeLibraryPath common.buildInputs} \
'';
});
default = self.packages.${system}.oscuro;
});
default = oscuro;
};
devShells = forAllSystems (system: let
pkgs = nixpkgsFor.${system};
bonfire-pkgs = bonfire.packages.${system};
fenix-pkgs = bonfire.inputs.fenix.packages.${system};
in {
default = pkgs.mkShell {
buildInputs = [
fenix-pkgs.complete.toolchain
bonfire-pkgs.cargo-shuttle
pkgs.cargo-release
pkgs.pkg-config
pkgs.openssl
];
};
});
hydraJobs = {
packages = self.packages;
};
devShells.x86_64-linux.default = pkgs.mkShell {
nativeBuildInputs = [pkgs.pkg-config];
buildInputs = [
fenixPkgs.complete.toolchain
pkgs.cargo-release
pkgs.openssl
];
LD_LIBRARY_PATH = lib.makeLibraryPath [pkgs.openssl];
};
nixosModules = rec {
nixosModules = {
oscuro = {
config,
lib,
@ -165,7 +148,7 @@
};
};
default = oscuro;
default = self.nixosModules.oscuro;
};
nixosConfigurations.oscuro = nixpkgs.lib.nixosSystem {

View File

@ -1,84 +0,0 @@
use async_process::Command;
use poise::serenity_prelude as serenity;
use rand::Rng;
use std::collections::HashMap;
use std::str;
use super::Context;
use crate::errors::BoxedError;
#[poise::command(prefix_command)]
pub async fn register(ctx: Context<'_>) -> Result<(), BoxedError> {
poise::builtins::register_application_commands_buttons(ctx).await?;
Ok(())
}
#[poise::command(slash_command, prefix_command)]
pub async fn age(
ctx: Context<'_>,
#[description = "Ooph user"] user: Option<serenity::User>,
) -> Result<(), BoxedError> {
let u = user.as_ref().unwrap_or_else(|| ctx.author());
let response = format!("{}'s account was created at {}", u.name, u.created_at());
ctx.say(response).await?;
Ok(())
}
#[poise::command(slash_command, prefix_command)]
pub async fn dice(ctx: Context<'_>) -> Result<(), BoxedError> {
let number = {
let mut rng = rand::thread_rng();
rng.gen_range(1..21)
};
let response = format!("{} throws {}.", ctx.author(), number);
let response = match number {
20 => format!("{} Critical success.", response),
1 => format!("{} Critical failure.", response),
_ => response,
};
ctx.say(response).await?;
Ok(())
}
#[derive(Debug, poise::ChoiceParameter)]
pub enum ServiceChoice {
#[name = "Elnafo VCS"]
ElnafoVcs,
#[name = "Elnafo Mail"]
ElnafoMail,
}
#[poise::command(slash_command, prefix_command)]
pub async fn status(
ctx: Context<'_>,
#[description = "Check service status"] service: ServiceChoice,
) -> Result<(), BoxedError> {
let mut systemctl = Command::new("systemctl");
let service_info = match service {
ServiceChoice::ElnafoVcs => systemctl.arg("show").arg("gitea.service"),
ServiceChoice::ElnafoMail => systemctl.arg("show").arg("acpid.service"),
};
let output = service_info.output().await?;
let mut data: HashMap<&str, &str> = HashMap::new();
for line in str::from_utf8(&output.stdout)?.lines() {
let kv: Vec<&str> = line.split('=').collect();
data.insert(kv[0], kv[1]);
}
println!("{:?} {:?}", data["LoadState"], data["SubState"]);
if data["LoadState"] == "loaded" && data["SubState"] == "running" {
ctx.say(format!(
"{:?} is up and running for {}",
service, data["ExecMainStartTimestamp"]
))
.await?;
} else {
ctx.say(format!("{:?} is dead", service)).await?;
}
Ok(())
}

View File

@ -1,148 +0,0 @@
pub mod commands;
use crate::config::Config;
use crate::errors::BoxedError;
use std::sync::Arc;
use poise::serenity_prelude::{
self as serenity,
builder::{CreateEmbed, CreateMessage},
model::id::ChannelId,
prelude::TypeMapKey,
//Client,
};
use serenity::GatewayIntents;
use teloxide::prelude::*;
use teloxide::types::Recipient;
use crate::telegram;
#[derive(Debug, Clone)]
pub struct BotState {
pub config: Config,
pub telegram_agent: Option<telegram::Client>,
}
impl TypeMapKey for BotState {
type Value = BotState;
}
type Context<'a> = poise::Context<'a, BotState, BoxedError>;
pub struct Client {
client: serenity::Client,
}
impl Client {
pub async fn new(config: Config) -> Result<Self, BoxedError> {
let telegram_agent = if config.clone().telegram_token.is_some() {
Some(telegram::Client::new(config.clone()))
} else {
None
};
let state = BotState {
config: config.clone(),
telegram_agent: telegram_agent,
};
let intents = GatewayIntents::GUILDS
| GatewayIntents::GUILD_MESSAGES
| GatewayIntents::MESSAGE_CONTENT;
let state_copy = state.clone();
let framework = poise::Framework::builder()
.options(poise::FrameworkOptions {
commands: vec![
commands::register(),
commands::age(),
commands::dice(),
commands::status(),
],
event_handler: |ctx, event, framework, data| {
Box::pin(event_handler(ctx, event, framework, data))
},
..Default::default()
})
.setup(|ctx, _ready, framework| {
Box::pin(async move {
poise::builtins::register_globally(ctx, &framework.options().commands).await?;
Ok(state_copy)
})
})
.build();
let client =
serenity::ClientBuilder::new(state.clone().config.discord_token.unwrap(), intents)
.framework(framework)
.await?;
{
let mut data = client.data.write().await;
data.insert::<BotState>(state);
}
Ok(Self { client })
}
pub async fn start(&mut self) -> Result<(), BoxedError> {
self.client.start().await;
Ok(())
}
pub async fn send(&self, chat_id: u64, msg: String) -> Result<(), BoxedError> {
let builder = CreateMessage::new().content(msg);
let message = ChannelId::new(chat_id)
.send_message(&self.client.http, builder)
.await?;
Ok(())
}
}
async fn event_handler(
ctx: &serenity::Context,
event: &serenity::FullEvent,
_framework: poise::FrameworkContext<'_, BotState, BoxedError>,
_state: &BotState,
) -> Result<(), BoxedError> {
match event {
serenity::FullEvent::Ready { data_about_bot, .. } => {
tracing::info!("discord: Logged in as {}", data_about_bot.user.name);
// We can use ChannelId directly to send a message to a specific channel; in this case, the
// message would be sent to the #testing channel on the discord server.
/*let embed = CreateEmbed::new().title("System Resource Load").field(
"CPU Load Average",
format!("{:.2}%", 10.0),
false,
);
let builder = CreateMessage::new().embed(embed);
let message = ChannelId::new(1145642256443904002)
.send_message(&ctx, builder)
.await;
if let Err(why) = message {
eprintln!("Error sending message: {why:?}");
};*/
}
serenity::FullEvent::Message { new_message } => {
let mut data = ctx.data.write().await;
let state = data.get_mut::<BotState>().unwrap();
println!("{:?}", new_message);
let author = new_message
.author
.global_name
.clone()
.or(Some(new_message.author.name.clone()))
.unwrap();
if let Some(agent) = &state.telegram_agent {
agent
.send(-4221527632, format!("{}: {}", author, new_message.content))
.await;
}
}
_ => {}
}
Ok(())
}

View File

@ -1,76 +1,34 @@
mod config;
mod discord;
mod errors;
mod telegram;
use config::Config;
use tokio::signal;
use tokio::task::JoinSet;
use oscuro_core::{client, config::Config, AppState};
use std::env;
#[tokio::main]
async fn main() -> Result<(), errors::BoxedError> {
async fn main() -> Result<(), Box<dyn std::error::Error>> {
tracing_subscriber::fmt()
.with_target(false)
.compact()
.init();
tracing::info!("Working directory: {:?}", Config::data_dir()?);
let config = match Config::open(Config::data_dir()?.join("config.toml").as_path()) {
let mut config = match Config::open(Config::data_dir()?.join("config.toml").as_path()) {
Ok(config) => config,
Err(err) => {
tracing::debug!("{}", err);
tracing::info!("Using default configuration");
Config::default()
}
}
.with_env();
Err(_) => Config::default(),
};
let mut runset = JoinSet::new();
if let Ok(token) = env::var("DISCORD_TOKEN") {
config.discord_token = token;
};
if !config.clone().discord_token.is_some() {
tracing::warn!("Missing discord token");
} else {
let mut discord_client = discord::Client::new(config.clone())
.await
.expect("Failed to create discord client");
runset.spawn(async move {
let res = discord_client.start().await;
if let Err(err) = res {
tracing::error!("{}", err);
}
});
if config.discord_token.is_empty() {
tracing::error!("Missing discord token");
}
if !config.clone().telegram_token.is_some() {
tracing::warn!("Missing telegram token");
} else {
let telegram_client = telegram::Client::new(config);
let state = AppState { config };
runset.spawn(async move {
let res = telegram_client.start().await;
if let Err(err) = res {
tracing::error!("{}", err);
}
});
}
while let Some(res) = runset.join_next().await {
if let Err(err) = res {
tracing::error!("{}", err);
}
}
match signal::ctrl_c().await {
Ok(()) => {}
Err(err) => {
eprintln!("Unable to listen for shutdown signal: {}", err);
// we also shut down in case of error
}
}
client(state)
.await
.expect("Failed to create client")
.start()
.await
.expect("Failed to start client");
Ok(())
}

View File

@ -1,85 +0,0 @@
use teloxide::prelude::*;
use teloxide::types::Recipient;
use teloxide::utils::command::BotCommands;
use crate::config::Config;
use crate::errors;
use rand::Rng;
async fn main() {
let bot = Bot::from_env();
/*let http = Http::new("");
let webhook = Webhook::from_url(&http, "https://discord.com/api/webhooks/1259860143579987999/whI0ozB5uc17Wdzkb2-HSrVGi8h_MyR2_4eyCsGuGpQN4KcjMhq7rfQH1JIdbD1HNaW_")
.await
.expect("Replace the webhook with your own");
let builder = ExecuteWebhook::new().content("hello there").username("Webhook test");
webhook.execute(&http, false, builder).await.expect("Could not execute webhook.");
*/
teloxide::repl(bot, |bot: Bot, msg: Message| async move {
bot.send_dice(msg.chat.id).await?;
Ok(())
})
.await;
/*bot.send_message(Recipient::Id(ChatId(-4221527632)), "Heya!")
.await
.expect("err");*/
}
#[derive(Clone, Debug)]
pub struct Client {
bot: Bot,
}
impl Client {
pub fn new(config: Config) -> Self {
Self {
bot: Bot::new(config.telegram_token.unwrap()),
}
}
pub async fn start(&self) -> Result<(), errors::BoxedError> {
Command::repl(self.bot.clone(), event_handler).await;
Ok(())
}
pub async fn send(&self, chat_id: i64, msg: String) -> ResponseResult<()> {
self.bot
.send_message(Recipient::Id(ChatId(chat_id)), msg)
.await?;
Ok(())
}
}
#[derive(BotCommands, Clone)]
#[command(rename_rule = "lowercase")]
pub enum Command {
#[command()]
Dice,
}
async fn event_handler(bot: Bot, msg: Message, cmd: Command) -> ResponseResult<()> {
match cmd {
Command::Dice => {
let number = {
let mut rng = rand::thread_rng();
rng.gen_range(1..21)
};
let response = format!("{} throws {}.", "test", number);
let response = match number {
20 => format!("{} Critical success.", response),
1 => format!("{} Critical failure.", response),
_ => response,
};
// -4221527632
bot.send_message(Recipient::Id(msg.chat.id), response)
.await?;
}
};
Ok(())
}