Compare commits

..

No commits in common. "nim" and "master" have entirely different histories.
nim ... master

16 changed files with 4158 additions and 91 deletions

5
.cargo/config.toml Normal file
View File

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

23
.gitea/workflows/nix.yml Normal file
View File

@ -0,0 +1,23 @@
name: nix-build-publish
on:
push:
branches:
- 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:
- uses: actions/checkout@v4
- uses: cachix/cachix-action@v14
with:
name: bonfire
authToken: "${{ secrets.CACHIX_AUTH_TOKEN }}"
- run: nix build -L

5
.gitignore vendored Normal file
View File

@ -0,0 +1,5 @@
/result
/target
Secrets*.toml
/temp
.tmp

2881
Cargo.lock generated Normal file

File diff suppressed because it is too large Load Diff

24
Cargo.toml Normal file
View File

@ -0,0 +1,24 @@
[package]
name = "oscuro"
version = "0.1.1"
edition = "2021"
description = "Oscuro is a fancy multibot"
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"] }
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 = []

View File

@ -1,6 +1,5 @@
# Oscuro - Discord Bot (Nim) # Oscuro - Discord Bot
# License # License
**oscuro** is licensed under the [MIT License](LICENSE). **oscuro** is licensed under the [MIT License](LICENSE).

View File

@ -1,15 +0,0 @@
# Package
version = "0.1.0"
author = "L-Nafaryus"
description = "Discord bot with nim"
license = "MIT"
srcDir = "src"
bin = @["dicord_bot"]
# Dependencies
requires "nim >= 1.6.8"
requires "dimscord"
requires "dimscmd"

535
flake.lock Normal file
View File

@ -0,0 +1,535 @@
{
"nodes": {
"ags": {
"inputs": {
"nixpkgs": [
"bonfire",
"nixpkgs"
],
"systems": "systems"
},
"locked": {
"lastModified": 1721306136,
"narHash": "sha256-VKPsIGf3/a+RONBipx4lEE4LXG2sdMNkWQu22LNQItg=",
"owner": "Aylur",
"repo": "ags",
"rev": "344ea72cd3b8d4911f362fec34bce7d8fb37028c",
"type": "github"
},
"original": {
"owner": "Aylur",
"repo": "ags",
"type": "github"
}
},
"blobs": {
"flake": false,
"locked": {
"lastModified": 1604995301,
"narHash": "sha256-wcLzgLec6SGJA8fx1OEN1yV/Py5b+U5iyYpksUY/yLw=",
"owner": "simple-nixos-mailserver",
"repo": "blobs",
"rev": "2cccdf1ca48316f2cfd1c9a0017e8de5a7156265",
"type": "gitlab"
},
"original": {
"owner": "simple-nixos-mailserver",
"repo": "blobs",
"type": "gitlab"
}
},
"bonfire": {
"inputs": {
"ags": "ags",
"catppuccin": "catppuccin",
"crane": "crane",
"fenix": "fenix",
"home-manager": "home-manager",
"nixos-mailserver": "nixos-mailserver",
"nixpkgs": "nixpkgs",
"nixvim": "nixvim",
"obs-image-reaction": "obs-image-reaction",
"oscuro": [],
"sops-nix": "sops-nix",
"wezterm": "wezterm"
},
"locked": {
"lastModified": 1723204928,
"narHash": "sha256-r6oYWnHf5H9+5n80QocRTRwpZxsDK+BEq2Hg9QS4pxo=",
"owner": "L-Nafaryus",
"repo": "bonfire",
"rev": "bb7c9204f5070d47334da89adf611e5fda8a3f41",
"type": "github"
},
"original": {
"owner": "L-Nafaryus",
"repo": "bonfire",
"type": "github"
}
},
"catppuccin": {
"locked": {
"lastModified": 1720472194,
"narHash": "sha256-CYscFEts6tyvosc1T29nxhzIYJAj/1CCEkV3ZMzSN/c=",
"owner": "catppuccin",
"repo": "nix",
"rev": "d75d5803852fb0833767dc969a4581ac13204e22",
"type": "github"
},
"original": {
"owner": "catppuccin",
"repo": "nix",
"type": "github"
}
},
"crane": {
"inputs": {
"nixpkgs": [
"bonfire",
"nixpkgs"
]
},
"locked": {
"lastModified": 1721322122,
"narHash": "sha256-a0G1NvyXGzdwgu6e1HQpmK5R5yLsfxeBe07nNDyYd+g=",
"owner": "ipetkov",
"repo": "crane",
"rev": "8a68b987c476a33e90f203f0927614a75c3f47ea",
"type": "github"
},
"original": {
"owner": "ipetkov",
"repo": "crane",
"type": "github"
}
},
"fenix": {
"inputs": {
"nixpkgs": [
"bonfire",
"nixpkgs"
],
"rust-analyzer-src": [
"bonfire"
]
},
"locked": {
"lastModified": 1721629802,
"narHash": "sha256-GKlvM9M0mkKJrL6N1eMG4DrROO25Ds1apFw3/b8594w=",
"owner": "nix-community",
"repo": "fenix",
"rev": "1270fb024c6987dd825a20cd27319384a8d8569e",
"type": "github"
},
"original": {
"owner": "nix-community",
"repo": "fenix",
"type": "github"
}
},
"flake-compat": {
"flake": false,
"locked": {
"lastModified": 1696426674,
"narHash": "sha256-kvjfFW7WAETZlt09AgDn1MrtKzP7t90Vf7vypd3OL1U=",
"owner": "edolstra",
"repo": "flake-compat",
"rev": "0f9255e01c2351cc7d116c072cb317785dd33b33",
"type": "github"
},
"original": {
"owner": "edolstra",
"repo": "flake-compat",
"type": "github"
}
},
"flake-parts": {
"inputs": {
"nixpkgs-lib": [
"bonfire",
"nixvim",
"nixpkgs"
]
},
"locked": {
"lastModified": 1719994518,
"narHash": "sha256-pQMhCCHyQGRzdfAkdJ4cIWiw+JNuWsTX7f0ZYSyz0VY=",
"owner": "hercules-ci",
"repo": "flake-parts",
"rev": "9227223f6d922fee3c7b190b2cc238a99527bbb7",
"type": "github"
},
"original": {
"owner": "hercules-ci",
"repo": "flake-parts",
"type": "github"
}
},
"flake-utils": {
"inputs": {
"systems": "systems_2"
},
"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"
}
},
"freetype2": {
"flake": false,
"locked": {
"lastModified": 1687587065,
"narHash": "sha256-+Fh+/k+NWL5Ow9sDLtp8Cv/8rLNA1oByQQCIQS/bysY=",
"owner": "wez",
"repo": "freetype2",
"rev": "e4586d960f339cf75e2e0b34aee30a0ed8353c0d",
"type": "github"
},
"original": {
"owner": "wez",
"repo": "freetype2",
"rev": "e4586d960f339cf75e2e0b34aee30a0ed8353c0d",
"type": "github"
}
},
"harfbuzz": {
"flake": false,
"locked": {
"lastModified": 1711722720,
"narHash": "sha256-GdxcAPx5QyniSHPAN1ih28AD9JLUPR0ItqW9JEsl3pU=",
"owner": "harfbuzz",
"repo": "harfbuzz",
"rev": "63973005bc07aba599b47fdd4cf788647b601ccd",
"type": "github"
},
"original": {
"owner": "harfbuzz",
"ref": "8.4.0",
"repo": "harfbuzz",
"type": "github"
}
},
"home-manager": {
"inputs": {
"nixpkgs": [
"bonfire",
"nixpkgs"
]
},
"locked": {
"lastModified": 1721534365,
"narHash": "sha256-XpZOkaSJKdOsz1wU6JfO59Rx2fqtcarQ0y6ndIOKNpI=",
"owner": "nix-community",
"repo": "home-manager",
"rev": "635563f245309ef5320f80c7ebcb89b2398d2949",
"type": "github"
},
"original": {
"owner": "nix-community",
"repo": "home-manager",
"type": "github"
}
},
"libpng": {
"flake": false,
"locked": {
"lastModified": 1549245649,
"narHash": "sha256-1+cRp0Ungme/OGfc9kGJbklYIWAFxk8Il1M+NV4KSgw=",
"owner": "glennrp",
"repo": "libpng",
"rev": "8439534daa1d3a5705ba92e653eda9251246dd61",
"type": "github"
},
"original": {
"owner": "glennrp",
"repo": "libpng",
"rev": "8439534daa1d3a5705ba92e653eda9251246dd61",
"type": "github"
}
},
"nixos-mailserver": {
"inputs": {
"blobs": "blobs",
"flake-compat": "flake-compat",
"nixpkgs": [
"bonfire",
"nixpkgs"
],
"nixpkgs-24_05": "nixpkgs-24_05"
},
"locked": {
"lastModified": 1721121314,
"narHash": "sha256-zwc7YXga/1ppaZMWFreZykXtFwBgXodxUZiUx969r+g=",
"owner": "simple-nixos-mailserver",
"repo": "nixos-mailserver",
"rev": "059b50b2e729729ea00c6831124d3837c494f3d5",
"type": "gitlab"
},
"original": {
"owner": "simple-nixos-mailserver",
"repo": "nixos-mailserver",
"type": "gitlab"
}
},
"nixpkgs": {
"locked": {
"lastModified": 1721379653,
"narHash": "sha256-8MUgifkJ7lkZs3u99UDZMB4kbOxvMEXQZ31FO3SopZ0=",
"owner": "nixos",
"repo": "nixpkgs",
"rev": "1d9c2c9b3e71b9ee663d11c5d298727dace8d374",
"type": "github"
},
"original": {
"owner": "nixos",
"ref": "nixos-unstable",
"repo": "nixpkgs",
"type": "github"
}
},
"nixpkgs-24_05": {
"locked": {
"lastModified": 1717144377,
"narHash": "sha256-F/TKWETwB5RaR8owkPPi+SPJh83AQsm6KrQAlJ8v/uA=",
"owner": "NixOS",
"repo": "nixpkgs",
"rev": "805a384895c696f802a9bf5bf4720f37385df547",
"type": "github"
},
"original": {
"id": "nixpkgs",
"ref": "nixos-24.05",
"type": "indirect"
}
},
"nixpkgs-stable": {
"locked": {
"lastModified": 1721524707,
"narHash": "sha256-5NctRsoE54N86nWd0psae70YSLfrOek3Kv1e8KoXe/0=",
"owner": "NixOS",
"repo": "nixpkgs",
"rev": "556533a23879fc7e5f98dd2e0b31a6911a213171",
"type": "github"
},
"original": {
"owner": "NixOS",
"ref": "release-24.05",
"repo": "nixpkgs",
"type": "github"
}
},
"nixpkgs_2": {
"locked": {
"lastModified": 1719223410,
"narHash": "sha256-jtIo8xR0Zp4SalIwmD+OdCwHF4l7OU6PD63UUK4ckt4=",
"owner": "NixOS",
"repo": "nixpkgs",
"rev": "efb39c6052f3ce51587cf19733f5f4e5d515aa13",
"type": "github"
},
"original": {
"owner": "NixOS",
"ref": "nixpkgs-unstable",
"repo": "nixpkgs",
"type": "github"
}
},
"nixvim": {
"inputs": {
"devshell": [
"bonfire"
],
"flake-compat": [
"bonfire"
],
"flake-parts": "flake-parts",
"git-hooks": [
"bonfire"
],
"home-manager": [
"bonfire"
],
"nix-darwin": [
"bonfire"
],
"nixpkgs": [
"bonfire",
"nixpkgs"
],
"treefmt-nix": [
"bonfire"
]
},
"locked": {
"lastModified": 1721772245,
"narHash": "sha256-//9p3Qm8gLbPUTsSGN2EMYkDwE5Sqq9B9P2X/z2+npw=",
"owner": "nix-community",
"repo": "nixvim",
"rev": "ab67ee7e8b33e788fc53d26dc6f423f9358e3e66",
"type": "github"
},
"original": {
"owner": "nix-community",
"repo": "nixvim",
"type": "github"
}
},
"obs-image-reaction": {
"inputs": {
"nixpkgs": "nixpkgs_2"
},
"locked": {
"lastModified": 1719314544,
"narHash": "sha256-GZa3+2OELKp/9b2+EwwzaIMNvR9niCy/YZ5OERhG9Hg=",
"owner": "L-Nafaryus",
"repo": "obs-image-reaction",
"rev": "0dcb3c27de5782dfdf95cb047ccceb3e65360e6b",
"type": "github"
},
"original": {
"owner": "L-Nafaryus",
"repo": "obs-image-reaction",
"type": "github"
}
},
"root": {
"inputs": {
"bonfire": "bonfire",
"nixpkgs": [
"bonfire",
"nixpkgs"
]
}
},
"rust-overlay": {
"inputs": {
"nixpkgs": [
"bonfire",
"wezterm",
"nixpkgs"
]
},
"locked": {
"lastModified": 1721441897,
"narHash": "sha256-gYGX9/22tPNeF7dR6bWN5rsrpU4d06GnQNNgZ6ZiXz0=",
"owner": "oxalica",
"repo": "rust-overlay",
"rev": "b7996075da11a2d441cfbf4e77c2939ce51506fd",
"type": "github"
},
"original": {
"owner": "oxalica",
"repo": "rust-overlay",
"type": "github"
}
},
"sops-nix": {
"inputs": {
"nixpkgs": [
"bonfire",
"nixpkgs"
],
"nixpkgs-stable": "nixpkgs-stable"
},
"locked": {
"lastModified": 1721531171,
"narHash": "sha256-AsvPw7T0tBLb53xZGcUC3YPqlIpdxoSx56u8vPCr6gU=",
"owner": "Mic92",
"repo": "sops-nix",
"rev": "909e8cfb60d83321d85c8d17209d733658a21c95",
"type": "github"
},
"original": {
"owner": "Mic92",
"repo": "sops-nix",
"type": "github"
}
},
"systems": {
"locked": {
"lastModified": 1689347949,
"narHash": "sha256-12tWmuL2zgBgZkdoB6qXZsgJEH9LR3oUgpaQq2RbI80=",
"owner": "nix-systems",
"repo": "default-linux",
"rev": "31732fcf5e8fea42e59c2488ad31a0e651500f68",
"type": "github"
},
"original": {
"owner": "nix-systems",
"repo": "default-linux",
"type": "github"
}
},
"systems_2": {
"locked": {
"lastModified": 1681028828,
"narHash": "sha256-Vy1rq5AaRuLzOxct8nz4T6wlgyUR7zLU309k9mBC768=",
"owner": "nix-systems",
"repo": "default",
"rev": "da67096a3b9bf56a91d16901293e51ba5b49a27e",
"type": "github"
},
"original": {
"owner": "nix-systems",
"repo": "default",
"type": "github"
}
},
"wezterm": {
"inputs": {
"flake-utils": "flake-utils",
"freetype2": "freetype2",
"harfbuzz": "harfbuzz",
"libpng": "libpng",
"nixpkgs": [
"bonfire",
"nixpkgs"
],
"rust-overlay": "rust-overlay",
"zlib": "zlib"
},
"locked": {
"dir": "nix",
"lastModified": 1722353247,
"narHash": "sha256-pPH+IJ8pljR+PmeOdckoHvbQVfSBdStKbgXcaqdkTRk=",
"owner": "wez",
"repo": "wezterm",
"rev": "56a27e93a9ee50aab50ff4d78308f9b3154b5122",
"type": "github"
},
"original": {
"dir": "nix",
"owner": "wez",
"repo": "wezterm",
"type": "github"
}
},
"zlib": {
"flake": false,
"locked": {
"lastModified": 1484501380,
"narHash": "sha256-j5b6aki1ztrzfCqu8y729sPar8GpyQWIrajdzpJC+ww=",
"owner": "madler",
"repo": "zlib",
"rev": "cacf7f1d4e3d44d871b605da3b647f07d718623f",
"type": "github"
},
"original": {
"owner": "madler",
"ref": "v1.2.11",
"repo": "zlib",
"type": "github"
}
}
},
"root": "root",
"version": 7
}

191
flake.nix Normal file
View File

@ -0,0 +1,191 @@
{
description = "Oscuro is a fancy multibot";
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="
];
};
inputs = {
bonfire = {
url = "github:L-Nafaryus/bonfire";
inputs = {
oscuro.follows = "";
};
};
nixpkgs.follows = "bonfire/nixpkgs";
};
outputs = {
self,
nixpkgs,
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;
in {
packages.x86_64-linux = rec {
oscuro = let
common = {
src = pkgs.lib.cleanSourceWith {
src = ./.;
filter = path: type: (craneLib.filterCargoSources path type);
};
strictDeps = true;
nativeBuildInputs = [pkgs.pkg-config pkgs.makeWrapper];
buildInputs = [pkgs.openssl];
};
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 = oscuro;
};
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 {
oscuro = {
config,
lib,
pkgs,
...
}:
with lib; let
cfg = config.services.oscuro;
opt = options.services.oscuro;
pkg = self.packages.${pkgs.system}.oscuro;
configFile = pkgs.writeText "config.toml" ''
discord_token = "#discord_token#"
'';
in {
options.services.oscuro = {
enable = mkEnableOption "Enables the Oscuro bot";
package = mkPackageOption pkgs "oscuro" {};
dataDir = mkOption {
type = types.path;
default = "/var/lib/oscuro";
description = lib.mdDoc "Directory to store Oscuro files";
};
discordToken = mkOption {
type = types.nullOr types.str;
default = null;
example = "Bot TOKENTOKENTOKEN";
};
discordTokenFile = mkOption {
type = types.nullOr types.str;
default = null;
example = "/var/lib/secrets/oscuro/discord_token";
};
};
config = mkIf cfg.enable {
assertions = [
{
assertion = cfg.discordToken != null || cfg.discordTokenFile != null;
message = "Discord token must be set. Use `services.oscuro.discordToken` or `services.oscuro.discordTokenFile`.";
}
];
users.users.oscuro = {
description = "Oscuro bot service user";
home = cfg.dataDir;
createHome = true;
isSystemUser = true;
group = "oscuro";
};
users.groups.oscuro = {};
systemd.services.oscuro = {
description = "Oscuro";
wantedBy = ["multi-user.target"];
after = ["network.target"];
serviceConfig = {
Restart = "always";
ExecStart = "${pkg}/bin/oscuro";
User = "oscuro";
WorkingDirectory = cfg.dataDir;
};
preStart = let
runConfig = "${cfg.dataDir}/config.toml";
replaceSecret = "${pkgs.replace-secret}/bin/replace-secret";
in ''
cp -f '${configFile}' '${runConfig}'
chmod u+w '${runConfig}'
${lib.optionalString (cfg.discordTokenFile != null) ''
${replaceSecret} '#discord_token#' '${cfg.discordTokenFile}' '${runConfig}'
''}
${lib.optionalString (cfg.discordToken != null) ''
sed -i 's/#discord_token#/${cfg.discordToken}/g' '${runConfig}'
''}
'';
};
};
};
default = oscuro;
};
nixosConfigurations.oscuro = nixpkgs.lib.nixosSystem {
system = "x86_64-linux";
modules = [
self.nixosModules.oscuro
({pkgs, ...}: {
boot.isContainer = true;
networking.hostName = "oscuro";
networking.useDHCP = false;
services.oscuro = {
enable = true;
discordToken = ""; # insert token
};
system.stateVersion = "24.05";
})
];
};
};
}

99
src/config.rs Normal file
View File

@ -0,0 +1,99 @@
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>,
}
impl Config {
pub fn data_dir() -> Result<std::path::PathBuf, ConfigError> {
let cwd = std::env::current_dir()?;
if cfg!(debug_assertions) {
Ok(cwd.join("temp"))
} else {
Ok(cwd)
}
}
pub fn open(path: &std::path::Path) -> Result<Config, ConfigError> {
fs::read_to_string(path)?.parse()
}
pub fn to_string(&self) -> Result<String, ConfigError> {
Ok(toml::to_string(self)?)
}
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()),
}
}
}
impl std::str::FromStr for Config {
type Err = ConfigError;
fn from_str(s: &str) -> Result<Self, ConfigError> {
toml::from_str(s).map_err(|_| ConfigError::Parse)
}
}
#[derive(Debug)]
pub enum ConfigError {
Parse,
StringParse,
Serialize,
IO,
}
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::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"),
}
}
}
impl From<toml::ser::Error> for ConfigError {
fn from(_: toml::ser::Error) -> Self {
ConfigError::Serialize
}
}
impl From<std::io::Error> for ConfigError {
fn from(_: std::io::Error) -> Self {
ConfigError::IO
}
}
impl From<std::num::ParseIntError> for ConfigError {
fn from(_: std::num::ParseIntError) -> Self {
ConfigError::StringParse
}
}

84
src/discord/commands.rs Normal file
View File

@ -0,0 +1,84 @@
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(())
}

148
src/discord/mod.rs Normal file
View File

@ -0,0 +1,148 @@
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,74 +0,0 @@
{.define: ssl.}
import os, httpclient, asyncdispatch
import times, options, strutils, options
import dimscord, dimscmd
let discord = newDiscordClient(getEnv("BOT_TOKEN"))
var cmd = discord.newHandler()
proc reply(msg: Message, text: string): Future[Message] {.async.} =
result = await discord.api.sendMessage(msg.channelId, text)
proc reply(i: Interaction, text: string) {.async.} =
let response = InteractionResponse(
kind: irtChannelMessageWithSource,
data: some InteractionApplicationCommandCallbackData(content: text)
)
await discord.api.createInteractionResponse(i.id, i.token, response)
cmd.addChat("hi") do ():
discard await msg.reply("hello")
cmd.addChat("notacat") do ():
## asd
let client = newHttpClient()
try:
let res = client.get("https://cataas.com/cat")
echo "Fetching a cat: ", res.status, ", ", res.headers["content-type"].string
echo "cat." & res.headers["content-type"].split("/")[1]
## Invalid JSON (50109)
#[let attach = Attachment(
filename: "cat." & res.headers["content-type"].split("/")[1],
content_type: some res.headers["content-type"].string,
file: res.body,
)
echo attach
let response = InteractionResponse(
kind: irtChannelMessageWithSource,
data: some InteractionApplicationCommandCallbackData(content: "asd", attachments: @[ attach ])
)
await discord.api.createInteractionResponse(i.id, i.token, response)]#
discard await discord.api.sendMessage(msg.channelId, files = @[DiscordFile(
name: "cat." & res.headers["content-type"].split("/")[1],
body: res.body
)])
except:
var error = getCurrentException()
echo "Exception occurred: ", error.msg
#[discard await discord.api.sendMessage(msg.channelId, "smh",
files = @[DiscordFile(body: res.body)]
)]#
# Handle event for on_ready.
proc onReady(s: Shard, r: Ready) {.event(discord).} =
await cmd.registerCommands()
echo "Ready as " & $r.user
proc interactionCreate(s: Shard, i: Interaction) {.event(discord).} =
discard await cmd.handleInteraction(s, i)
# Handle event for message_create.
proc messageCreate(s: Shard, msg: Message) {.event(discord).} =
if msg.author.bot: return
discard await cmd.handleMessage("./", s, msg)
# Connect to Discord and run the bot.
waitFor discord.startSession()

1
src/errors.rs Normal file
View File

@ -0,0 +1 @@
pub type BoxedError = Box<dyn std::error::Error + Send + Sync>;

76
src/main.rs Normal file
View File

@ -0,0 +1,76 @@
mod config;
mod discord;
mod errors;
mod telegram;
use config::Config;
use tokio::signal;
use tokio::task::JoinSet;
#[tokio::main]
async fn main() -> Result<(), errors::BoxedError> {
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()) {
Ok(config) => config,
Err(err) => {
tracing::debug!("{}", err);
tracing::info!("Using default configuration");
Config::default()
}
}
.with_env();
let mut runset = JoinSet::new();
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.clone().telegram_token.is_some() {
tracing::warn!("Missing telegram token");
} else {
let telegram_client = telegram::Client::new(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
}
}
Ok(())
}

85
src/telegram/mod.rs Normal file
View File

@ -0,0 +1,85 @@
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(())
}