mirror of
https://github.com/revanced/revanced-discord-bot.git
synced 2025-04-29 22:14:28 +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