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