mirror of
https://github.com/revanced/revanced-discord-bot.git
synced 2025-04-30 06:24:27 +02:00
feat: initial commit
This commit is contained in:
commit
c07ad28a60
2
.gitignore
vendored
Normal file
2
.gitignore
vendored
Normal file
@ -0,0 +1,2 @@
|
|||||||
|
/target
|
||||||
|
configuration.json
|
3
.gitmodules
vendored
Normal file
3
.gitmodules
vendored
Normal file
@ -0,0 +1,3 @@
|
|||||||
|
[submodule "submodules/serenity"]
|
||||||
|
path = submodules/serenity
|
||||||
|
url = git@github.com:serenity-rs/serenity.git
|
41
.vscode/launch.json
vendored
Normal file
41
.vscode/launch.json
vendored
Normal file
@ -0,0 +1,41 @@
|
|||||||
|
"version": "0.2.0",
|
||||||
|
"configurations": [
|
||||||
|
{
|
||||||
|
"type": "lldb",
|
||||||
|
"request": "launch",
|
||||||
|
"name": "Debug executable 'revanced-discord-bot'",
|
||||||
|
"cargo": {
|
||||||
|
"args": [
|
||||||
|
"build",
|
||||||
|
"--bin=revanced-discord-bot",
|
||||||
|
"--package=revanced-discord-bot"
|
||||||
|
],
|
||||||
|
"filter": {
|
||||||
|
"name": "revanced-discord-bot",
|
||||||
|
"kind": "bin"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"args": [],
|
||||||
|
"cwd": "${workspaceFolder}"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": "lldb",
|
||||||
|
"request": "launch",
|
||||||
|
"name": "Debug unit tests in executable 'revanced-discord-bot'",
|
||||||
|
"cargo": {
|
||||||
|
"args": [
|
||||||
|
"test",
|
||||||
|
"--no-run",
|
||||||
|
"--bin=revanced-discord-bot",
|
||||||
|
"--package=revanced-discord-bot"
|
||||||
|
],
|
||||||
|
"filter": {
|
||||||
|
"name": "revanced-discord-bot",
|
||||||
|
"kind": "bin"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"args": [],
|
||||||
|
"cwd": "${workspaceFolder}"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
1277
Cargo.lock
generated
Normal file
1277
Cargo.lock
generated
Normal file
File diff suppressed because it is too large
Load Diff
29
Cargo.toml
Normal file
29
Cargo.toml
Normal file
@ -0,0 +1,29 @@
|
|||||||
|
[package]
|
||||||
|
authors = ["oSumAtrIX"]
|
||||||
|
description = "The official Discord bot assisting the ReVanced Discord server"
|
||||||
|
homepage = "https://revanced.app"
|
||||||
|
keywords = ["ReVanced"]
|
||||||
|
license = "GPL-3.0"
|
||||||
|
name = "revanced-discord-bot"
|
||||||
|
repository = "https://github.com/revanced/revanced-discord-bot"
|
||||||
|
version = "0.1.0"
|
||||||
|
edition = "2021"
|
||||||
|
|
||||||
|
[profile.release]
|
||||||
|
strip = true
|
||||||
|
opt-level = 3
|
||||||
|
lto = true
|
||||||
|
codegen-units = 1
|
||||||
|
panic = "abort"
|
||||||
|
|
||||||
|
[dependencies]
|
||||||
|
serde = { version = "1.0", features = ["derive"] }
|
||||||
|
serde_json = "1.0"
|
||||||
|
tokio = { version = "1.0", features = ["rt-multi-thread"] }
|
||||||
|
log = "0.4"
|
||||||
|
chrono = "0.4"
|
||||||
|
|
||||||
|
[dependencies.serenity]
|
||||||
|
default-features = false
|
||||||
|
features = ["client", "gateway", "rustls_backend", "model"]
|
||||||
|
path = "submodules/serenity" # submodule due to serenity just recently including global slash commands
|
32
configuration.example.json
Normal file
32
configuration.example.json
Normal file
@ -0,0 +1,32 @@
|
|||||||
|
{
|
||||||
|
"$schema": "./configuration.schema.json",
|
||||||
|
"discord-authorization-token": "",
|
||||||
|
"administrators": {
|
||||||
|
"roles": [0],
|
||||||
|
"users": [0]
|
||||||
|
},
|
||||||
|
"thread-introductions": [
|
||||||
|
{
|
||||||
|
"channels": [0],
|
||||||
|
"message": ""
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"message-responders": [
|
||||||
|
{
|
||||||
|
"includes": {
|
||||||
|
"channels": [0],
|
||||||
|
"match": [""]
|
||||||
|
},
|
||||||
|
"excludes": {
|
||||||
|
"roles": [0],
|
||||||
|
"match": [""]
|
||||||
|
},
|
||||||
|
"condition": {
|
||||||
|
"user": {
|
||||||
|
"server-age": 2
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"message": ""
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
136
configuration.schema.json
Normal file
136
configuration.schema.json
Normal file
@ -0,0 +1,136 @@
|
|||||||
|
{
|
||||||
|
"$schema": "https://json-schema.org/draft-07/schema",
|
||||||
|
"$id": "https://github.com/ReVancedTeam/revanced-signatures/blob/main/signatures.schema.json",
|
||||||
|
"title": "Configuration schema",
|
||||||
|
"description": "The Revanced Discord bot configuration schema.",
|
||||||
|
"type": "object",
|
||||||
|
"required": ["discord-authorization-token"],
|
||||||
|
"properties": {
|
||||||
|
"discord-authorization-token": {
|
||||||
|
"type": "string",
|
||||||
|
"description": "The authorization token for the Discord bot."
|
||||||
|
},
|
||||||
|
"administrators": {
|
||||||
|
"type": "object",
|
||||||
|
"properties": {
|
||||||
|
"roles": {
|
||||||
|
"$ref": "#/$defs/roles",
|
||||||
|
"description": "A list of role ids. Users with these roles have administrative privileges over this Discord bot."
|
||||||
|
},
|
||||||
|
"users": {
|
||||||
|
"type": "array",
|
||||||
|
"items": {
|
||||||
|
"type": "integer"
|
||||||
|
},
|
||||||
|
"description": "A list of user ids. Users with these ids have administrative privileges over this Discord bot.",
|
||||||
|
"minItems": 1,
|
||||||
|
"uniqueItems": true
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"description": "The list of administrators to control the Discord bot."
|
||||||
|
},
|
||||||
|
"thread-introductions": {
|
||||||
|
"type": "array",
|
||||||
|
"items": {
|
||||||
|
"type": "object",
|
||||||
|
"required": ["channels", "message"],
|
||||||
|
"properties": {
|
||||||
|
"channels": {
|
||||||
|
"$ref": "#/$defs/channels",
|
||||||
|
"description": "A list of channel ids. The bot will only introduce in threads under these channels."
|
||||||
|
},
|
||||||
|
"message": {
|
||||||
|
"$ref": "#/$defs/message",
|
||||||
|
"description": "The message to send when the thread has been created."
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"description": "Introduce new threads with a message.",
|
||||||
|
"minItems": 1,
|
||||||
|
"uniqueItems": true
|
||||||
|
},
|
||||||
|
"message-responders": {
|
||||||
|
"type": "array",
|
||||||
|
"items": {
|
||||||
|
"type": "object",
|
||||||
|
"properties": {
|
||||||
|
"includes": {
|
||||||
|
"type": "object",
|
||||||
|
"channels": {
|
||||||
|
"$ref": "#/$defs/channels",
|
||||||
|
"description": "A list of channel ids. The bot will only respond to messages in these channels."
|
||||||
|
},
|
||||||
|
"match": {
|
||||||
|
"$ref": "#/$defs/match",
|
||||||
|
"description": "The message must match this regex to be responded to."
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"excludes": {
|
||||||
|
"type": "object",
|
||||||
|
"roles": {
|
||||||
|
"$ref": "#/$defs/roles",
|
||||||
|
"description": "A list of role ids. The bot will not respond to messages from users with these roles."
|
||||||
|
},
|
||||||
|
"match": {
|
||||||
|
"$ref": "#/$defs/match",
|
||||||
|
"description": "Messages matching this regex will not be responded to."
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"condition": {
|
||||||
|
"type": "object",
|
||||||
|
"properties": {
|
||||||
|
"user": {
|
||||||
|
"type": "object",
|
||||||
|
"properties": {
|
||||||
|
"server-age": {
|
||||||
|
"type": "integer",
|
||||||
|
"description": "The user must be at least this many days old on the server."
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"description": "User condition."
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"description": "The conditions to respond to the message."
|
||||||
|
},
|
||||||
|
"message": {
|
||||||
|
"$ref": "#/$defs/message",
|
||||||
|
"description": "The message to send when the message is responded to."
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"description": "The conditions to respond to a message."
|
||||||
|
},
|
||||||
|
"description": "A list of responses the Discord bot should send based on given conditions."
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"$defs": {
|
||||||
|
"roles": {
|
||||||
|
"type": "array",
|
||||||
|
"items": {
|
||||||
|
"type": "integer"
|
||||||
|
},
|
||||||
|
"description": "A list of role ids.",
|
||||||
|
"uniqueItems": true,
|
||||||
|
"minItems": 1
|
||||||
|
},
|
||||||
|
"channels": {
|
||||||
|
"type": "array",
|
||||||
|
"items": {
|
||||||
|
"type": "integer"
|
||||||
|
},
|
||||||
|
"uniqueItems": true,
|
||||||
|
"minItems": 1
|
||||||
|
},
|
||||||
|
"match": {
|
||||||
|
"type": "array",
|
||||||
|
"items": {
|
||||||
|
"type": "string"
|
||||||
|
},
|
||||||
|
"description": "A list of regex strings.",
|
||||||
|
"uniqueItems": true,
|
||||||
|
"minItems": 1
|
||||||
|
},
|
||||||
|
"message": {
|
||||||
|
"type": "string"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
16
rustfmt.toml
Normal file
16
rustfmt.toml
Normal file
@ -0,0 +1,16 @@
|
|||||||
|
edition = "2018"
|
||||||
|
match_block_trailing_comma = true
|
||||||
|
newline_style = "Unix"
|
||||||
|
use_field_init_shorthand = true
|
||||||
|
use_small_heuristics = "Max"
|
||||||
|
use_try_shorthand = true
|
||||||
|
|
||||||
|
hard_tabs = true
|
||||||
|
format_code_in_doc_comments = true
|
||||||
|
group_imports = "StdExternalCrate"
|
||||||
|
imports_granularity = "Module"
|
||||||
|
imports_layout = "HorizontalVertical"
|
||||||
|
match_arm_blocks = true
|
||||||
|
normalize_comments = true
|
||||||
|
overflow_delimited_expr = true
|
||||||
|
struct_lit_single_line = false
|
92
src/configuration.rs
Normal file
92
src/configuration.rs
Normal file
@ -0,0 +1,92 @@
|
|||||||
|
use std::fs::File;
|
||||||
|
use std::io::{Error, Read, Write};
|
||||||
|
|
||||||
|
use serde::{Deserialize, Serialize};
|
||||||
|
|
||||||
|
#[derive(Default, Serialize, Deserialize)]
|
||||||
|
#[serde(rename_all = "camelCase")]
|
||||||
|
pub struct BotConfiguration {
|
||||||
|
#[serde(rename = "discord-authorization-token")]
|
||||||
|
pub discord_authorization_token: String,
|
||||||
|
pub administrators: Administrators,
|
||||||
|
#[serde(rename = "thread-introductions")]
|
||||||
|
pub thread_introductions: Option<Vec<Introduction>>,
|
||||||
|
#[serde(rename = "message-responders")]
|
||||||
|
pub message_responders: Option<Vec<MessageResponder>>,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Default, Serialize, Deserialize)]
|
||||||
|
#[serde(rename_all = "camelCase")]
|
||||||
|
pub struct Administrators {
|
||||||
|
pub roles: Vec<u64>,
|
||||||
|
pub users: Option<Vec<u64>>,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Serialize, Deserialize)]
|
||||||
|
#[serde(rename_all = "camelCase")]
|
||||||
|
pub struct Introduction {
|
||||||
|
pub channels: Vec<u64>,
|
||||||
|
pub message: String,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Serialize, Deserialize)]
|
||||||
|
#[serde(rename_all = "camelCase")]
|
||||||
|
pub struct MessageResponder {
|
||||||
|
pub includes: Option<Includes>,
|
||||||
|
pub excludes: Option<Excludes>,
|
||||||
|
pub condition: Option<Condition>,
|
||||||
|
pub message: String,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Serialize, Deserialize)]
|
||||||
|
#[serde(rename_all = "camelCase")]
|
||||||
|
pub struct Includes {
|
||||||
|
pub channels: Option<Vec<u64>>,
|
||||||
|
#[serde(rename = "match")]
|
||||||
|
pub match_field: Option<Vec<String>>,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Serialize, Deserialize)]
|
||||||
|
#[serde(rename_all = "camelCase")]
|
||||||
|
pub struct Excludes {
|
||||||
|
pub roles: Option<Vec<u64>>,
|
||||||
|
#[serde(rename = "match")]
|
||||||
|
pub match_field: Option<Vec<String>>,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Serialize, Deserialize)]
|
||||||
|
#[serde(rename_all = "camelCase")]
|
||||||
|
pub struct Condition {
|
||||||
|
pub user: User,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Serialize, Deserialize)]
|
||||||
|
#[serde(rename_all = "camelCase")]
|
||||||
|
pub struct User {
|
||||||
|
#[serde(rename = "server-age")]
|
||||||
|
pub server_age: Option<i16>,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl BotConfiguration {
|
||||||
|
fn save(&self) -> Result<(), Error> {
|
||||||
|
let mut file = File::create("configuration.json")?;
|
||||||
|
let json = serde_json::to_string_pretty(&self)?;
|
||||||
|
file.write(json.as_bytes())?;
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn load() -> Result<BotConfiguration, Error> {
|
||||||
|
let mut file = match File::open("configuration.json") {
|
||||||
|
Ok(file) => file,
|
||||||
|
Err(_) => {
|
||||||
|
let configuration = BotConfiguration::default();
|
||||||
|
configuration.save()?;
|
||||||
|
return Ok(configuration);
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
let mut buf = String::new();
|
||||||
|
file.read_to_string(&mut buf)?;
|
||||||
|
Ok(serde_json::from_str(&buf)?)
|
||||||
|
}
|
||||||
|
}
|
17
src/logger/logging.rs
Normal file
17
src/logger/logging.rs
Normal file
@ -0,0 +1,17 @@
|
|||||||
|
use log::{Level, Metadata, Record};
|
||||||
|
|
||||||
|
pub struct SimpleLogger;
|
||||||
|
|
||||||
|
impl log::Log for SimpleLogger {
|
||||||
|
fn enabled(&self, metadata: &Metadata) -> bool {
|
||||||
|
metadata.level() <= Level::Info
|
||||||
|
}
|
||||||
|
|
||||||
|
fn log(&self, record: &Record) {
|
||||||
|
if self.enabled(record.metadata()) {
|
||||||
|
println!("{} - {}", record.level(), record.args());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn flush(&self) {}
|
||||||
|
}
|
1
src/logger/mod.rs
Normal file
1
src/logger/mod.rs
Normal file
@ -0,0 +1 @@
|
|||||||
|
pub mod logging;
|
165
src/main.rs
Normal file
165
src/main.rs
Normal file
@ -0,0 +1,165 @@
|
|||||||
|
use std::sync::Arc;
|
||||||
|
use std::time::{Duration, SystemTime};
|
||||||
|
|
||||||
|
use chrono::{DateTime, Datelike, NaiveDateTime, Utc};
|
||||||
|
use configuration::BotConfiguration;
|
||||||
|
use log::{error, info, trace, LevelFilter};
|
||||||
|
use logger::logging::SimpleLogger;
|
||||||
|
use serenity::client::{Context, EventHandler};
|
||||||
|
use serenity::model::application::command::Command;
|
||||||
|
use serenity::model::channel::{GuildChannel, Message};
|
||||||
|
use serenity::model::gateway::Ready;
|
||||||
|
use serenity::model::prelude::interaction::application_command::CommandDataOptionValue;
|
||||||
|
use serenity::model::prelude::interaction::{Interaction, InteractionResponseType};
|
||||||
|
use serenity::model::Timestamp;
|
||||||
|
use serenity::prelude::{GatewayIntents, RwLock, TypeMapKey};
|
||||||
|
use serenity::{async_trait, Client};
|
||||||
|
mod configuration;
|
||||||
|
mod logger;
|
||||||
|
|
||||||
|
static LOGGER: SimpleLogger = SimpleLogger;
|
||||||
|
|
||||||
|
struct Configuration;
|
||||||
|
|
||||||
|
impl TypeMapKey for Configuration {
|
||||||
|
type Value = Arc<RwLock<BotConfiguration>>;
|
||||||
|
}
|
||||||
|
|
||||||
|
pub struct Handler;
|
||||||
|
|
||||||
|
async fn get_configuration_lock(ctx: &Context) -> Arc<RwLock<BotConfiguration>> {
|
||||||
|
ctx.data
|
||||||
|
.read()
|
||||||
|
.await
|
||||||
|
.get::<Configuration>()
|
||||||
|
.expect("Expected Configuration in TypeMap.")
|
||||||
|
.clone()
|
||||||
|
}
|
||||||
|
|
||||||
|
#[async_trait]
|
||||||
|
impl EventHandler for Handler {
|
||||||
|
async fn interaction_create(&self, ctx: Context, interaction: Interaction) {
|
||||||
|
trace!("Created an interaction: {:?}", interaction);
|
||||||
|
|
||||||
|
if let Interaction::ApplicationCommand(command) = interaction {
|
||||||
|
let content = match command.data.name.as_str() {
|
||||||
|
"reload" => {
|
||||||
|
trace!("{:?} reloading configuration.", command.user);
|
||||||
|
|
||||||
|
let configuration_lock = get_configuration_lock(&ctx).await;
|
||||||
|
|
||||||
|
let mut configuration = configuration_lock.write().await;
|
||||||
|
|
||||||
|
let new_config =
|
||||||
|
BotConfiguration::load().expect("Could not load configuration.");
|
||||||
|
|
||||||
|
configuration.administrators = new_config.administrators;
|
||||||
|
configuration.message_responders = new_config.message_responders;
|
||||||
|
configuration.thread_introductions = new_config.thread_introductions;
|
||||||
|
|
||||||
|
"Successfully reload configuration.".to_string()
|
||||||
|
},
|
||||||
|
_ => "Unknown command.".to_string(),
|
||||||
|
};
|
||||||
|
|
||||||
|
if let Err(why) = command
|
||||||
|
.create_interaction_response(&ctx.http, |response| {
|
||||||
|
response
|
||||||
|
.kind(InteractionResponseType::ChannelMessageWithSource)
|
||||||
|
.interaction_response_data(|message| message.content(content))
|
||||||
|
})
|
||||||
|
.await
|
||||||
|
{
|
||||||
|
error!("Cannot respond to slash command: {}", why);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async fn message(&self, ctx: Context, msg: Message) {
|
||||||
|
trace!("Received message: {}", msg.content);
|
||||||
|
|
||||||
|
let configuration_lock = get_configuration_lock(&ctx).await;
|
||||||
|
let configuration = configuration_lock.read().await;
|
||||||
|
|
||||||
|
if let Some(message_responders) = &configuration.message_responders {
|
||||||
|
if let Some(responder) = message_responders.iter().find(|responder| {
|
||||||
|
responder.includes.iter().any(|include| {
|
||||||
|
include.channels.iter().any(|channel| todo!("Implement inclusion check"))
|
||||||
|
}) && responder.excludes.iter().all(|exclude| todo!("Implement exclusion check"))
|
||||||
|
}) {
|
||||||
|
if let Some(condition) = &responder.condition {
|
||||||
|
let join_date = ctx
|
||||||
|
.http
|
||||||
|
.get_member(msg.guild_id.unwrap().0, msg.author.id.0)
|
||||||
|
.await
|
||||||
|
.unwrap()
|
||||||
|
.joined_at
|
||||||
|
.unwrap();
|
||||||
|
|
||||||
|
let member_age = Timestamp::now().unix_timestamp() - join_date.unix_timestamp();
|
||||||
|
|
||||||
|
if let Some(age) = condition.user.server_age {
|
||||||
|
todo!("Implement age check")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async fn thread_create(&self, ctx: Context, thread: GuildChannel) {
|
||||||
|
trace!("Thread created: {}", thread.name);
|
||||||
|
|
||||||
|
let configuration_lock = get_configuration_lock(&ctx).await;
|
||||||
|
let configuration = configuration_lock.read().await;
|
||||||
|
|
||||||
|
if let Some(introducers) = &configuration.thread_introductions {
|
||||||
|
if let Some(introducer) = introducers.iter().find(|introducer| {
|
||||||
|
introducer
|
||||||
|
.channels
|
||||||
|
.iter()
|
||||||
|
.any(|channel_id| *channel_id == thread.parent_id.unwrap().0)
|
||||||
|
}) {
|
||||||
|
if let Err(why) = thread.say(&ctx.http, &introducer.message).await {
|
||||||
|
error!("Error sending message: {:?}", why);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async fn ready(&self, ctx: Context, ready: Ready) {
|
||||||
|
info!("Connected as {}", ready.user.name);
|
||||||
|
|
||||||
|
Command::create_global_application_command(&ctx.http, |command| {
|
||||||
|
command.name("reload").description("Reloads the configuration.")
|
||||||
|
})
|
||||||
|
.await
|
||||||
|
.expect("Could not create command.");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[tokio::main]
|
||||||
|
async fn main() {
|
||||||
|
log::set_logger(&LOGGER)
|
||||||
|
.map(|()| log::set_max_level(LevelFilter::Info))
|
||||||
|
.expect("Could not set logger.");
|
||||||
|
|
||||||
|
let configuration = BotConfiguration::load().expect("Failed to load configuration");
|
||||||
|
|
||||||
|
let mut client = Client::builder(
|
||||||
|
&configuration.discord_authorization_token,
|
||||||
|
GatewayIntents::GUILDS | GatewayIntents::GUILD_MESSAGES | GatewayIntents::MESSAGE_CONTENT,
|
||||||
|
)
|
||||||
|
.event_handler(Handler)
|
||||||
|
.await
|
||||||
|
.expect("Failed to create client");
|
||||||
|
|
||||||
|
client.data.write().await.insert::<Configuration>(Arc::new(RwLock::new(
|
||||||
|
BotConfiguration::load().expect("Failed to load configuration"),
|
||||||
|
)));
|
||||||
|
|
||||||
|
if let Err(why) = client.start().await {
|
||||||
|
error!("{:?}", why);
|
||||||
|
} else {
|
||||||
|
info!("Client started.");
|
||||||
|
}
|
||||||
|
}
|
Loading…
x
Reference in New Issue
Block a user