port of memejoin from js to rust
commit
7772a39526
|
@ -0,0 +1 @@
|
||||||
|
/target
|
File diff suppressed because it is too large
Load Diff
|
@ -0,0 +1,15 @@
|
||||||
|
[package]
|
||||||
|
name = "memejoin-rs"
|
||||||
|
version = "0.1.0"
|
||||||
|
edition = "2021"
|
||||||
|
|
||||||
|
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
|
||||||
|
|
||||||
|
[dependencies]
|
||||||
|
serde = "1.0.152"
|
||||||
|
serde_json = "1.0.93"
|
||||||
|
serenity = { version = "0.11.5", default-features = false, features = ["client", "gateway", "rustls_backend", "model", "cache", "voice"] }
|
||||||
|
songbird = "0.3.0"
|
||||||
|
tokio = { version = "1.25.0", features = ["rt-multi-thread", "macros", "signal"] }
|
||||||
|
tracing = "0.1.37"
|
||||||
|
tracing-subscriber = "0.3.16"
|
|
@ -0,0 +1,94 @@
|
||||||
|
{
|
||||||
|
"nodes": {
|
||||||
|
"flake-utils": {
|
||||||
|
"locked": {
|
||||||
|
"lastModified": 1667395993,
|
||||||
|
"narHash": "sha256-nuEHfE/LcWyuSWnS8t12N1wc105Qtau+/OdUAjtQ0rA=",
|
||||||
|
"owner": "numtide",
|
||||||
|
"repo": "flake-utils",
|
||||||
|
"rev": "5aed5285a952e0b949eb3ba02c12fa4fcfef535f",
|
||||||
|
"type": "github"
|
||||||
|
},
|
||||||
|
"original": {
|
||||||
|
"owner": "numtide",
|
||||||
|
"repo": "flake-utils",
|
||||||
|
"type": "github"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"flake-utils_2": {
|
||||||
|
"locked": {
|
||||||
|
"lastModified": 1659877975,
|
||||||
|
"narHash": "sha256-zllb8aq3YO3h8B/U0/J1WBgAL8EX5yWf5pMj3G0NAmc=",
|
||||||
|
"owner": "numtide",
|
||||||
|
"repo": "flake-utils",
|
||||||
|
"rev": "c0e246b9b83f637f4681389ecabcb2681b4f3af0",
|
||||||
|
"type": "github"
|
||||||
|
},
|
||||||
|
"original": {
|
||||||
|
"owner": "numtide",
|
||||||
|
"repo": "flake-utils",
|
||||||
|
"type": "github"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"nixpkgs": {
|
||||||
|
"locked": {
|
||||||
|
"lastModified": 1675942811,
|
||||||
|
"narHash": "sha256-/v4Z9mJmADTpXrdIlAjFa1e+gkpIIROR670UVDQFwIw=",
|
||||||
|
"owner": "nixos",
|
||||||
|
"repo": "nixpkgs",
|
||||||
|
"rev": "724bfc0892363087709bd3a5a1666296759154b1",
|
||||||
|
"type": "github"
|
||||||
|
},
|
||||||
|
"original": {
|
||||||
|
"owner": "nixos",
|
||||||
|
"ref": "nixos-unstable",
|
||||||
|
"repo": "nixpkgs",
|
||||||
|
"type": "github"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"nixpkgs_2": {
|
||||||
|
"locked": {
|
||||||
|
"lastModified": 1665296151,
|
||||||
|
"narHash": "sha256-uOB0oxqxN9K7XGF1hcnY+PQnlQJ+3bP2vCn/+Ru/bbc=",
|
||||||
|
"owner": "NixOS",
|
||||||
|
"repo": "nixpkgs",
|
||||||
|
"rev": "14ccaaedd95a488dd7ae142757884d8e125b3363",
|
||||||
|
"type": "github"
|
||||||
|
},
|
||||||
|
"original": {
|
||||||
|
"owner": "NixOS",
|
||||||
|
"ref": "nixpkgs-unstable",
|
||||||
|
"repo": "nixpkgs",
|
||||||
|
"type": "github"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"root": {
|
||||||
|
"inputs": {
|
||||||
|
"flake-utils": "flake-utils",
|
||||||
|
"nixpkgs": "nixpkgs",
|
||||||
|
"rust-overlay": "rust-overlay"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"rust-overlay": {
|
||||||
|
"inputs": {
|
||||||
|
"flake-utils": "flake-utils_2",
|
||||||
|
"nixpkgs": "nixpkgs_2"
|
||||||
|
},
|
||||||
|
"locked": {
|
||||||
|
"lastModified": 1676169013,
|
||||||
|
"narHash": "sha256-mhUWa6TUg6Qjba1OdxPuW1ctCuU4O4lSObVc6UUUE0E=",
|
||||||
|
"owner": "oxalica",
|
||||||
|
"repo": "rust-overlay",
|
||||||
|
"rev": "ef4cd733dc6b595cab5092f5004a489c5fd80b07",
|
||||||
|
"type": "github"
|
||||||
|
},
|
||||||
|
"original": {
|
||||||
|
"owner": "oxalica",
|
||||||
|
"repo": "rust-overlay",
|
||||||
|
"type": "github"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"root": "root",
|
||||||
|
"version": 7
|
||||||
|
}
|
|
@ -0,0 +1,36 @@
|
||||||
|
{
|
||||||
|
inputs = {
|
||||||
|
nixpkgs.url = "github:nixos/nixpkgs/nixos-unstable";
|
||||||
|
rust-overlay.url = "github:oxalica/rust-overlay";
|
||||||
|
flake-utils.url = "github:numtide/flake-utils";
|
||||||
|
};
|
||||||
|
|
||||||
|
outputs = { self, nixpkgs, rust-overlay, flake-utils, ... }:
|
||||||
|
flake-utils.lib.eachDefaultSystem (system:
|
||||||
|
let
|
||||||
|
overlays = [ (import rust-overlay) ];
|
||||||
|
pkgs = import nixpkgs {
|
||||||
|
inherit system overlays;
|
||||||
|
};
|
||||||
|
local-rust = (pkgs.rust-bin.fromRustupToolchainFile ./rust-toolchain).override {
|
||||||
|
extensions = [ "rust-analysis" ];
|
||||||
|
};
|
||||||
|
in
|
||||||
|
{
|
||||||
|
devShell = pkgs.mkShell {
|
||||||
|
buildInputs = with pkgs; [
|
||||||
|
local-rust
|
||||||
|
rust-analyzer
|
||||||
|
pkg-config
|
||||||
|
gcc
|
||||||
|
openssl
|
||||||
|
python3
|
||||||
|
ffmpeg
|
||||||
|
cmake
|
||||||
|
libopus
|
||||||
|
youtube-dl
|
||||||
|
];
|
||||||
|
};
|
||||||
|
}
|
||||||
|
);
|
||||||
|
}
|
|
@ -0,0 +1 @@
|
||||||
|
nightly
|
|
@ -0,0 +1,209 @@
|
||||||
|
#![feature(stmt_expr_attributes)]
|
||||||
|
#![feature(proc_macro_hygiene)]
|
||||||
|
#![feature(async_closure)]
|
||||||
|
|
||||||
|
use std::collections::HashMap;
|
||||||
|
use std::env;
|
||||||
|
use std::sync::Arc;
|
||||||
|
|
||||||
|
use serde::Deserialize;
|
||||||
|
use serenity::async_trait;
|
||||||
|
use serenity::model::prelude::{Channel, GuildId, Message, Ready};
|
||||||
|
use serenity::model::voice::VoiceState;
|
||||||
|
use serenity::prelude::GatewayIntents;
|
||||||
|
use serenity::prelude::*;
|
||||||
|
use songbird::SerenityInit;
|
||||||
|
use tracing::*;
|
||||||
|
|
||||||
|
struct Handler;
|
||||||
|
|
||||||
|
struct TrackEventHandler {
|
||||||
|
ctx: Context,
|
||||||
|
guild_id: GuildId,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[async_trait]
|
||||||
|
impl songbird::EventHandler for TrackEventHandler {
|
||||||
|
async fn act<'a, 'b, 'c>(
|
||||||
|
&'a self,
|
||||||
|
ctx: &'b songbird::EventContext<'c>,
|
||||||
|
) -> Option<songbird::Event> {
|
||||||
|
if let songbird::EventContext::Track(track) = ctx {
|
||||||
|
if let Some(context) = track.get(0) {
|
||||||
|
if context.0.playing == songbird::tracks::PlayMode::End
|
||||||
|
|| context.0.playing == songbird::tracks::PlayMode::Stop
|
||||||
|
{
|
||||||
|
let manager = songbird::get(&self.ctx).await.expect("should get manager");
|
||||||
|
if let Err(err) = manager.leave(self.guild_id).await {
|
||||||
|
error!("Failed to leave voice channel: {err:?}");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
None
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, Clone, Deserialize)]
|
||||||
|
struct Settings {
|
||||||
|
#[serde(alias = "userEnteredSoundDelay")]
|
||||||
|
sound_delay: u64,
|
||||||
|
channels: HashMap<String, ChannelSettings>,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl TypeMapKey for Settings {
|
||||||
|
type Value = Arc<Settings>;
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, Clone, Deserialize)]
|
||||||
|
struct ChannelSettings {
|
||||||
|
#[serde(alias = "enterUsers")]
|
||||||
|
users: HashMap<String, UserSettings>,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, Clone, Deserialize)]
|
||||||
|
struct UserSettings {
|
||||||
|
#[serde(rename = "type")]
|
||||||
|
ty: SoundType,
|
||||||
|
|
||||||
|
#[serde(alias = "enterSound")]
|
||||||
|
sound: String,
|
||||||
|
#[serde(alias = "youtubeVolume")]
|
||||||
|
volume: i32,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, Clone, Deserialize)]
|
||||||
|
enum SoundType {
|
||||||
|
#[serde(alias = "file")]
|
||||||
|
File,
|
||||||
|
#[serde(alias = "youtube")]
|
||||||
|
Youtube,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[async_trait]
|
||||||
|
impl EventHandler for Handler {
|
||||||
|
async fn ready(&self, _ctx: Context, ready: Ready) {
|
||||||
|
info!("{} is ready", ready.user.name);
|
||||||
|
}
|
||||||
|
|
||||||
|
async fn voice_state_update(&self, ctx: Context, old: Option<VoiceState>, new: VoiceState) {
|
||||||
|
if old.is_none() {
|
||||||
|
if let (Some(member), Some(channel_id)) = (new.member, new.channel_id) {
|
||||||
|
info!(
|
||||||
|
"{}#{} joined voice channel {:?} in {:?}",
|
||||||
|
member.user.name,
|
||||||
|
member.user.discriminator,
|
||||||
|
channel_id.name(&ctx.cache).await,
|
||||||
|
member
|
||||||
|
.guild_id
|
||||||
|
.name(&ctx.cache)
|
||||||
|
.unwrap_or("no_guild_name".to_string())
|
||||||
|
);
|
||||||
|
|
||||||
|
if member.user.name == "MemeJoin" {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
let settings = {
|
||||||
|
let data_read = ctx.data.read().await;
|
||||||
|
|
||||||
|
data_read
|
||||||
|
.get::<Settings>()
|
||||||
|
.expect("settings should exist")
|
||||||
|
.clone()
|
||||||
|
};
|
||||||
|
|
||||||
|
let Some(Channel::Guild(channel)) = channel_id.to_channel_cached(&ctx.cache) else {
|
||||||
|
error!("Failed to get cached channel from member!");
|
||||||
|
return;
|
||||||
|
};
|
||||||
|
|
||||||
|
let Some(user) = settings.channels.get(channel.name()).and_then(|c| c.users.get(&member.user.name)) else {
|
||||||
|
info!("No sound associated for {} in channel {}", member.user.name, channel.name());
|
||||||
|
return;
|
||||||
|
};
|
||||||
|
|
||||||
|
let Some(manager) = songbird::get(&ctx).await else {
|
||||||
|
error!("Failed to get songbird manager from context");
|
||||||
|
return;
|
||||||
|
};
|
||||||
|
|
||||||
|
match manager.join(member.guild_id, channel_id).await {
|
||||||
|
(handler_lock, Ok(())) => {
|
||||||
|
let mut handler = handler_lock.lock().await;
|
||||||
|
|
||||||
|
let source = match user.ty {
|
||||||
|
SoundType::Youtube => match songbird::ytdl(&user.sound).await {
|
||||||
|
Ok(source) => source,
|
||||||
|
Err(err) => {
|
||||||
|
error!("Error starting source: {err:?}");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
},
|
||||||
|
SoundType::File => {
|
||||||
|
error!("Playing files not supported yet");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
let track_handle = handler.play_source(source);
|
||||||
|
if let Err(err) = track_handle.add_event(
|
||||||
|
songbird::Event::Track(songbird::TrackEvent::End),
|
||||||
|
TrackEventHandler {
|
||||||
|
ctx,
|
||||||
|
guild_id: member.guild_id,
|
||||||
|
},
|
||||||
|
) {
|
||||||
|
error!("Failed to add event handler to track handle: {err:?}");
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
(_, Err(err)) => {
|
||||||
|
error!("Failed to join voice channel {}: {err:?}", channel.name());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[tokio::main]
|
||||||
|
#[instrument]
|
||||||
|
async fn main() {
|
||||||
|
tracing_subscriber::fmt::init();
|
||||||
|
|
||||||
|
let token = env::var("DISCORD_TOKEN").expect("expected DISCORD_TOKEN env var");
|
||||||
|
|
||||||
|
let settings = serde_json::from_str::<Settings>(
|
||||||
|
&std::fs::read_to_string("config/settings.json").expect("no config/settings.json"),
|
||||||
|
)
|
||||||
|
.expect("error parsing settings file");
|
||||||
|
|
||||||
|
info!("{settings:?}");
|
||||||
|
|
||||||
|
let intents = GatewayIntents::GUILDS
|
||||||
|
| GatewayIntents::GUILD_MESSAGES
|
||||||
|
| GatewayIntents::MESSAGE_CONTENT
|
||||||
|
| GatewayIntents::GUILD_VOICE_STATES;
|
||||||
|
let mut client = Client::builder(&token, intents)
|
||||||
|
.event_handler(Handler)
|
||||||
|
.register_songbird()
|
||||||
|
.await
|
||||||
|
.expect("Error creating client");
|
||||||
|
|
||||||
|
{
|
||||||
|
let mut data = client.data.write().await;
|
||||||
|
|
||||||
|
data.insert::<Settings>(Arc::new(settings));
|
||||||
|
}
|
||||||
|
|
||||||
|
info!("Starting bot with token '{token}'");
|
||||||
|
tokio::spawn(async move {
|
||||||
|
if let Err(err) = client.start().await {
|
||||||
|
error!("An error occurred while running the client: {err:?}");
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
let _ = tokio::signal::ctrl_c().await;
|
||||||
|
info!("Received Ctrl-C, shuttdown down.");
|
||||||
|
}
|
Loading…
Reference in New Issue