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