init: initialization repository

This commit is contained in:
ParrotXray 2025-11-20 20:46:48 +08:00
commit 9225553e8d
Signed by: ParrotXray
SSH Key Fingerprint: SHA256:OEnKoo72UOfrmZ3LVSBn9K/UfpEzNrl+q8JMLG9CaAI
17 changed files with 4336 additions and 0 deletions

1
.gitignore vendored Normal file
View File

@ -0,0 +1 @@
/target

10
.idea/.gitignore generated vendored Normal file
View 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
View 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
View 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
View 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

File diff suppressed because it is too large Load Diff

34
Cargo.toml Normal file
View 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
View 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
View 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
View File

@ -0,0 +1,2 @@
pub mod ping;
pub mod music;

86
src/bot/commands/music.rs Normal file
View 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
View 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
View 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
View 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
View File

@ -0,0 +1 @@
pub mod config;

28
src/bot/utils/config.rs Normal file
View 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
View 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(())
}