init: initialization repository
This commit is contained in:
commit
9225553e8d
1
.gitignore
vendored
Normal file
1
.gitignore
vendored
Normal file
@ -0,0 +1 @@
|
||||
/target
|
||||
10
.idea/.gitignore
generated
vendored
Normal file
10
.idea/.gitignore
generated
vendored
Normal file
@ -0,0 +1,10 @@
|
||||
# 默认忽略的文件
|
||||
/shelf/
|
||||
/workspace.xml
|
||||
# 已忽略包含查询文件的默认文件夹
|
||||
/queries/
|
||||
# Datasource local storage ignored files
|
||||
/dataSources/
|
||||
/dataSources.local.xml
|
||||
# 基于编辑器的 HTTP 客户端请求
|
||||
/httpRequests/
|
||||
11
.idea/bot.iml
generated
Normal file
11
.idea/bot.iml
generated
Normal file
@ -0,0 +1,11 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<module type="EMPTY_MODULE" version="4">
|
||||
<component name="NewModuleRootManager">
|
||||
<content url="file://$MODULE_DIR$">
|
||||
<sourceFolder url="file://$MODULE_DIR$/src" isTestSource="false" />
|
||||
<excludeFolder url="file://$MODULE_DIR$/target" />
|
||||
</content>
|
||||
<orderEntry type="inheritedJdk" />
|
||||
<orderEntry type="sourceFolder" forTests="false" />
|
||||
</component>
|
||||
</module>
|
||||
8
.idea/modules.xml
generated
Normal file
8
.idea/modules.xml
generated
Normal file
@ -0,0 +1,8 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<project version="4">
|
||||
<component name="ProjectModuleManager">
|
||||
<modules>
|
||||
<module fileurl="file://$PROJECT_DIR$/.idea/bot.iml" filepath="$PROJECT_DIR$/.idea/bot.iml" />
|
||||
</modules>
|
||||
</component>
|
||||
</project>
|
||||
6
.idea/vcs.xml
generated
Normal file
6
.idea/vcs.xml
generated
Normal file
@ -0,0 +1,6 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<project version="4">
|
||||
<component name="VcsDirectoryMappings">
|
||||
<mapping directory="" vcs="Git" />
|
||||
</component>
|
||||
</project>
|
||||
3960
Cargo.lock
generated
Normal file
3960
Cargo.lock
generated
Normal file
File diff suppressed because it is too large
Load Diff
34
Cargo.toml
Normal file
34
Cargo.toml
Normal file
@ -0,0 +1,34 @@
|
||||
[package]
|
||||
name = "bot"
|
||||
version = "0.1.0"
|
||||
edition = "2024"
|
||||
|
||||
[dependencies]
|
||||
# Logging
|
||||
tracing = "0.1.41"
|
||||
tracing-subscriber = { version = "0.3.20", features = ["env-filter", "fmt", "time"] }
|
||||
tracing-actix-web = "0.7.19"
|
||||
|
||||
# Date/Time
|
||||
chrono = { version = "0.4", features = ["serde"] }
|
||||
|
||||
# Async Runtime
|
||||
tokio = { version = "1.48.0", features = ["full"] }
|
||||
|
||||
# Configuration
|
||||
config = "0.15.18"
|
||||
|
||||
# Validation & Regex
|
||||
regex = "1.12.2"
|
||||
|
||||
# Discord framework
|
||||
poise = { version = "0.6.1", features = ["cache", "chrono", "handle_panics"] }
|
||||
serenity = { version = "0.12", default-features = false, features = ["client", "gateway", "rustls_backend", "model", "cache"] }
|
||||
|
||||
# music
|
||||
songbird = { version = "0.4.6", features = ["gateway", "serenity", "rustls"], default-features = false }
|
||||
lavalink-rs = { version = "0.14.2", features = ["tungstenite-rustls-webpki-roots", "serenity", "songbird", "macros"], default-features = false }
|
||||
|
||||
# other
|
||||
anyhow = "1.0.100"
|
||||
serde = { version = "1.0.228", features = ["derive"] }
|
||||
7
config.toml
Normal file
7
config.toml
Normal file
@ -0,0 +1,7 @@
|
||||
[discord]
|
||||
token = "MTA2NTEwMzMyNDgwNzk2NjgxMA.GdbwSu.hU0qbczbmxE3NkovI7nYxxFqWry4dC34Dldu_8"
|
||||
|
||||
[lavalink]
|
||||
hostname = "192.168.1.118:30801"
|
||||
password = "F0TjWTza2p6iQcpPS6oXYPXA5O6xJrmT"
|
||||
is_ssl = false
|
||||
15
src/bot.rs
Normal file
15
src/bot.rs
Normal file
@ -0,0 +1,15 @@
|
||||
pub mod commands;
|
||||
pub mod utils;
|
||||
pub mod data;
|
||||
pub mod event;
|
||||
|
||||
pub use data::BotData;
|
||||
pub type Error = Box<dyn std::error::Error + Send + Sync>;
|
||||
pub type Context<'a> = poise::Context<'a, BotData, Error>;
|
||||
|
||||
pub fn commands() -> Vec<poise::Command<BotData, Error>> {
|
||||
vec![
|
||||
commands::ping::ping(),
|
||||
commands::music::join(),
|
||||
]
|
||||
}
|
||||
2
src/bot/commands.rs
Normal file
2
src/bot/commands.rs
Normal file
@ -0,0 +1,2 @@
|
||||
pub mod ping;
|
||||
pub mod music;
|
||||
86
src/bot/commands/music.rs
Normal file
86
src/bot/commands/music.rs
Normal file
@ -0,0 +1,86 @@
|
||||
use std::ops::Deref;
|
||||
use lavalink_rs::prelude::*;
|
||||
use crate::bot::{Context, Error};
|
||||
use poise::serenity_prelude as serenity;
|
||||
use serenity::{model::id::ChannelId, Http, Mentionable};
|
||||
|
||||
async fn _join(
|
||||
ctx: &Context<'_>,
|
||||
guild_id: serenity::GuildId,
|
||||
channel_id: Option<serenity::ChannelId>,
|
||||
) -> Result<bool, Error> {
|
||||
let lava_client = ctx.data().lavalink.clone();
|
||||
|
||||
let manager = songbird::get(ctx.serenity_context()).await.unwrap().clone();
|
||||
|
||||
if lava_client.get_player_context(guild_id).is_none() {
|
||||
let connect_to = match channel_id {
|
||||
Some(x) => x,
|
||||
None => {
|
||||
let guild = ctx.guild().unwrap().deref().clone();
|
||||
let user_channel_id = guild
|
||||
.voice_states
|
||||
.get(&ctx.author().id)
|
||||
.and_then(|voice_state| voice_state.channel_id);
|
||||
|
||||
match user_channel_id {
|
||||
Some(channel) => channel,
|
||||
None => {
|
||||
ctx.say("Not in a voice channel").await?;
|
||||
|
||||
return Err("Not in a voice channel".into());
|
||||
}
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
let handler = manager.join_gateway(guild_id, connect_to).await;
|
||||
|
||||
match handler {
|
||||
Ok((connection_info, _)) => {
|
||||
lava_client
|
||||
// The turbofish here is Optional, but it helps to figure out what type to
|
||||
// provide in `PlayerContext::data()`
|
||||
//
|
||||
// While a tuple is used here as an example, you are free to use a custom
|
||||
// public structure with whatever data you wish.
|
||||
// This custom data is also present in the Client if you wish to have the
|
||||
// shared data be more global, rather than centralized to each player.
|
||||
.create_player_context_with_data::<(ChannelId, std::sync::Arc<Http>)>(
|
||||
guild_id,
|
||||
connection_info,
|
||||
std::sync::Arc::new((
|
||||
ctx.channel_id(),
|
||||
ctx.serenity_context().http.clone(),
|
||||
)),
|
||||
)
|
||||
.await?;
|
||||
|
||||
ctx.say(format!("Joined {}", connect_to.mention())).await?;
|
||||
|
||||
return Ok(true);
|
||||
}
|
||||
Err(why) => {
|
||||
ctx.say(format!("Error joining the channel: {}", why))
|
||||
.await?;
|
||||
return Err(why.into());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Ok(false)
|
||||
}
|
||||
|
||||
#[poise::command(slash_command)]
|
||||
pub async fn join(
|
||||
ctx: Context<'_>,
|
||||
#[description = "The channel ID to join to."]
|
||||
#[channel_types("Voice")]
|
||||
channel_id: Option<serenity::ChannelId>,
|
||||
) -> Result<(), Error> {
|
||||
let guild_id = ctx.guild_id().ok_or("This command can only be used in a guild")?;
|
||||
|
||||
_join(&ctx, guild_id, channel_id).await?;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
31
src/bot/commands/ping.rs
Normal file
31
src/bot/commands/ping.rs
Normal file
@ -0,0 +1,31 @@
|
||||
use crate::bot::{Context, Error};
|
||||
use poise::serenity_prelude as serenity;
|
||||
use tracing::info;
|
||||
|
||||
#[poise::command(slash_command)]
|
||||
pub async fn ping(ctx: Context<'_>) -> Result<(), Error> {
|
||||
info!("{} use a command ping", ctx.author().name);
|
||||
|
||||
let shard_manager = ctx.framework().shard_manager();
|
||||
let latency = {
|
||||
let runners = shard_manager.runners.lock().await;
|
||||
runners
|
||||
.get(&ctx.serenity_context().shard_id)
|
||||
.and_then(|runner| runner.latency)
|
||||
};
|
||||
|
||||
let embed = serenity::CreateEmbed::new()
|
||||
.title("Ping")
|
||||
.color(serenity::Color::from_rgb(0, 250, 0))
|
||||
.field(
|
||||
"API latency",
|
||||
latency
|
||||
.map(|lat| format!("`{}ms`", lat.as_millis()))
|
||||
.unwrap_or_else(|| "`N/A`".to_string()),
|
||||
false
|
||||
)
|
||||
.timestamp(serenity::Timestamp::now());
|
||||
|
||||
ctx.send(poise::CreateReply::default().embed(embed)).await?;
|
||||
Ok(())
|
||||
}
|
||||
23
src/bot/data.rs
Normal file
23
src/bot/data.rs
Normal file
@ -0,0 +1,23 @@
|
||||
use std::sync::Arc;
|
||||
use tokio::sync::Mutex;
|
||||
use std::sync::atomic::{AtomicU32, AtomicU64, Ordering};
|
||||
use super::utils::config;
|
||||
use lavalink_rs::prelude::*;
|
||||
|
||||
pub struct BotData {
|
||||
pub config: Arc<config::BotConfig>,
|
||||
pub start_time: std::time::Instant,
|
||||
pub command_count: Arc<AtomicU64>,
|
||||
pub lavalink: LavalinkClient,
|
||||
}
|
||||
|
||||
impl BotData {
|
||||
pub fn new(config: Arc<config::BotConfig>, lavalink: LavalinkClient) -> Self {
|
||||
Self {
|
||||
config,
|
||||
start_time: std::time::Instant::now(),
|
||||
command_count: Arc::new(AtomicU64::new(0)),
|
||||
lavalink,
|
||||
}
|
||||
}
|
||||
}
|
||||
23
src/bot/event.rs
Normal file
23
src/bot/event.rs
Normal file
@ -0,0 +1,23 @@
|
||||
use poise::serenity_prelude as serenity;
|
||||
use tracing::info;
|
||||
use std::sync::atomic::Ordering;
|
||||
|
||||
type Error = Box<dyn std::error::Error + Send + Sync>;
|
||||
|
||||
pub async fn event_handler(
|
||||
ctx: &serenity::Context,
|
||||
event: &serenity::FullEvent,
|
||||
_framework: poise::FrameworkContext<'_, crate::bot::data::BotData, Error>,
|
||||
data: &crate::bot::data::BotData,
|
||||
) -> Result<(), Error> {
|
||||
match event {
|
||||
serenity::FullEvent::Ready { data_about_bot, .. } => {
|
||||
info!("Logged in as {}", data_about_bot.user.name);
|
||||
}
|
||||
serenity::FullEvent::Message { new_message } => {
|
||||
|
||||
}
|
||||
_ => {}
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
1
src/bot/utils.rs
Normal file
1
src/bot/utils.rs
Normal file
@ -0,0 +1 @@
|
||||
pub mod config;
|
||||
28
src/bot/utils/config.rs
Normal file
28
src/bot/utils/config.rs
Normal file
@ -0,0 +1,28 @@
|
||||
use serde::{Deserialize, Serialize};
|
||||
|
||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||
pub struct BotConfig {
|
||||
pub discord: DiscordConfig,
|
||||
pub lavalink: LavalinkConfig,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||
pub struct DiscordConfig {
|
||||
pub token: String,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||
pub struct LavalinkConfig {
|
||||
pub hostname: String,
|
||||
pub password: String,
|
||||
pub is_ssl: bool,
|
||||
}
|
||||
|
||||
impl BotConfig {
|
||||
pub fn load(file_path: &str) -> Result<Self, config::ConfigError> {
|
||||
config::Config::builder()
|
||||
.add_source(config::File::with_name(file_path))
|
||||
.build()?
|
||||
.try_deserialize()
|
||||
}
|
||||
}
|
||||
90
src/main.rs
Normal file
90
src/main.rs
Normal file
@ -0,0 +1,90 @@
|
||||
mod bot;
|
||||
|
||||
use anyhow::anyhow;
|
||||
use poise::serenity_prelude as serenity;
|
||||
use tracing::{error, info};
|
||||
use tracing_subscriber::{layer::SubscriberExt, util::SubscriberInitExt};
|
||||
use lavalink_rs::{model::events, prelude::*};
|
||||
use std::sync::Arc;
|
||||
use songbird::SerenityInit;
|
||||
use crate::bot::{data, event, commands};
|
||||
|
||||
#[tokio::main]
|
||||
async fn main() -> Result<(), Box<dyn std::error::Error>> {
|
||||
tracing_subscriber::registry()
|
||||
.with(
|
||||
tracing_subscriber::fmt::layer()
|
||||
.with_thread_ids(true)
|
||||
.with_file(true)
|
||||
.with_line_number(true)
|
||||
.with_target(true)
|
||||
)
|
||||
.with(tracing_subscriber::EnvFilter::from_default_env()
|
||||
.add_directive(tracing::Level::INFO.into()))
|
||||
.init();
|
||||
|
||||
let config = Arc::new(bot::utils::config::BotConfig::load("config")
|
||||
.map_err(|err| anyhow!("Config file does not exist: {}", err))?);
|
||||
|
||||
let token = Arc::clone(&config);
|
||||
let lava = Arc::clone(&config);
|
||||
|
||||
let intents = serenity::GatewayIntents::all();
|
||||
|
||||
info!("Starting gateway intents handler");
|
||||
|
||||
let framework = poise::Framework::builder()
|
||||
.options(poise::FrameworkOptions {
|
||||
commands: commands(),
|
||||
event_handler: |ctx, event, framework, data| {
|
||||
Box::pin(event::event_handler(ctx, event, framework, data))
|
||||
},
|
||||
|
||||
on_error: |error| {
|
||||
Box::pin(async move {
|
||||
error!("Command execution error: {}", error);
|
||||
})
|
||||
},
|
||||
..Default::default()
|
||||
})
|
||||
.setup(move |ctx, ready, framework| {
|
||||
Box::pin(async move {
|
||||
poise::builtins::register_globally(ctx, &framework.options().commands).await?;
|
||||
|
||||
let node_local = NodeBuilder {
|
||||
hostname: lava.lavalink.hostname.clone(),
|
||||
is_ssl: lava.lavalink.is_ssl,
|
||||
events: events::Events::default(),
|
||||
password: lava.lavalink.password.clone(),
|
||||
user_id: ready.user.id.get().into(),
|
||||
session_id: None,
|
||||
};
|
||||
|
||||
let lavalink = LavalinkClient::new(
|
||||
events::Events::default(),
|
||||
vec![node_local],
|
||||
NodeDistributionStrategy::round_robin(),
|
||||
).await;
|
||||
|
||||
info!("Lavalink node count: {}", lavalink.nodes.len());
|
||||
for (i, node) in lavalink.nodes.iter().enumerate() {
|
||||
info!("Node {}: {:?}", i + 1, node);
|
||||
}
|
||||
Ok(data::BotData::new(config, lavalink))
|
||||
})
|
||||
})
|
||||
.build();
|
||||
|
||||
let mut client = serenity::Client::builder(&token.discord.token, intents)
|
||||
.register_songbird()
|
||||
.framework(framework)
|
||||
.await
|
||||
.map_err(|e| anyhow!("Client creation failed:{}", e))?;
|
||||
|
||||
client
|
||||
.start()
|
||||
.await
|
||||
.map_err(|e| anyhow!("Startup failed: {}", e))?;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
Loading…
x
Reference in New Issue
Block a user