diff --git a/.idea/locationoverflow.iml b/.idea/locationoverflow.iml index 28c9e2d..7c69ed4 100644 --- a/.idea/locationoverflow.iml +++ b/.idea/locationoverflow.iml @@ -5,6 +5,7 @@ + diff --git a/Cargo.lock b/Cargo.lock index d0b3d07..45d1187 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -2450,6 +2450,26 @@ dependencies = [ "crypto-common", ] +[[package]] +name = "discord-worker" +version = "0.1.0" +dependencies = [ + "async-trait", + "chrono", + "common", + "log 0.4.19", + "reqwest", + "serde", + "serde_json", + "serenity", + "simple_logger", + "tokio", + "tokio-threadpool", + "toml", + "url 2.4.0", + "websocket", +] + [[package]] name = "dispatch" version = "0.2.0" diff --git a/Cargo.toml b/Cargo.toml index 1161903..e8f3863 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -3,5 +3,6 @@ members = [ "api", "common", "websocket-worker", - "azalea-worker" + "azalea-worker", + "discord-worker" ] \ No newline at end of file diff --git a/discord-worker/Cargo.toml b/discord-worker/Cargo.toml new file mode 100644 index 0000000..7295ec2 --- /dev/null +++ b/discord-worker/Cargo.toml @@ -0,0 +1,22 @@ +[package] +name = "discord-worker" +version = "0.1.0" +edition = "2021" + +# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html + +[dependencies] +log = "0.4" +simple_logger = "4.1" +serde = { version = "1", features = ["derive"] } +toml = "0.7" +url = { version = "2.3", features = ["serde"] } +tokio = { version = "1", features = ["full"] } +reqwest = { version = "0.11", features = ["json"] } +serde_json = "1" +async-trait = "0.1" +websocket = { version = "0.26", features = ["async"], no-default-features = true } +common = { path = "../common" } +serenity = "0.11" +tokio-threadpool = "0.1" +chrono = "0.4" \ No newline at end of file diff --git a/discord-worker/config.toml b/discord-worker/config.toml new file mode 100644 index 0000000..03e42fc --- /dev/null +++ b/discord-worker/config.toml @@ -0,0 +1,5 @@ +api_status_url = "https://data-api.locationoverflow.coredoes.dev/status" +api_token = "rw-minecraft-b1d648ab-498a-4499-b5a7-64aedcd7c836" +discord_token = "NzM3MzIzNDgzMTkyNzU0MjY3.GbGBcv.BchYB4l196mToItLuHwpWKKbc2L3LedBzUuyyM" +relay_channels = [1130340095606673458] +prefix = "c!" \ No newline at end of file diff --git a/discord-worker/src/config.rs b/discord-worker/src/config.rs new file mode 100644 index 0000000..71a2bd6 --- /dev/null +++ b/discord-worker/src/config.rs @@ -0,0 +1,11 @@ +use serde::{Deserialize, Serialize}; +use url::Url; + +#[derive(Serialize, Deserialize, Debug, Clone)] +pub struct Config { + pub api_status_url: Url, + pub api_token: String, + pub discord_token: String, + pub prefix: String, + pub relay_channels: Vec +} \ No newline at end of file diff --git a/discord-worker/src/main.rs b/discord-worker/src/main.rs new file mode 100644 index 0000000..b3a36c4 --- /dev/null +++ b/discord-worker/src/main.rs @@ -0,0 +1,186 @@ +use std::error::Error; +use std::fs; +use std::sync::Arc; +use std::time::SystemTime; +use chrono::DateTime; +use log::{debug, error, info}; +use tokio_threadpool::ThreadPool; +use url::Url; +use websocket::{ClientBuilder, Message as WsMessage, OwnedMessage, WebSocketError, client::sync::Client as WsClient}; +use websocket::websocket_base::result::WebSocketResult; +use common::message::{GatewayChatMessage, GatewayChatSource, GatewayPacketC2S, GatewayPacketS2C}; +use common::status::{DATA_API_VERSION, Status}; +use crate::config::Config; +use serenity::async_trait; +use serenity::builder::ParseValue; +use serenity::prelude::*; +use serenity::model::channel::{Message, Reaction, ReactionType}; +use serenity::framework::standard::macros::{command, group, hook}; +use serenity::framework::standard::{StandardFramework, CommandResult, Args}; +use serenity::http::Http; +use serenity::model::prelude::Webhook; +use websocket::stream::sync::NetworkStream; + +pub mod config; + +#[group] +struct General; + +struct Handler; + +#[async_trait] +impl EventHandler for Handler {} + +struct ConfigLock; + +impl TypeMapKey for ConfigLock { + type Value = Arc>; +} + +struct WsLock; + +impl TypeMapKey for WsLock { + type Value = Arc>>>; +} + + +#[tokio::main] +async fn main() -> Result<(), Box> { + simple_logger::init_with_env().unwrap(); + + info!("Loading config"); + + let mut args = std::env::args(); + if args.len() != 2 { + eprintln!("usage: ./discord-worker "); + std::process::exit(1); + } + + let file = args.nth(1).unwrap(); + + let config_str = fs::read_to_string(&file)?; + + let config: Config = toml::from_str(&config_str)?; + + info!("Config loaded from {}", file); + + info!("Loading status from the API ({})...", config.api_status_url); + + let status: Status = reqwest::get(config.api_status_url.clone()).await?.json().await?; + + debug!("{:?}", status); + + if status.data_api_version != DATA_API_VERSION { + error!("Data API is incompatible. This version of the websocket worker was compiled with Data API v{}, but your Data API is Data API v{}", DATA_API_VERSION, status.data_api_version); + std::process::exit(1); + } + + info!("Connecting to gateway uri {}", status.gateway_url.to_string()); + + let mut client = ClientBuilder::new(status.gateway_url.clone().as_str())?.connect(None)?; + + // send the authentication packet, and then start listen + + let message = WsMessage::text(serde_json::to_string(&GatewayPacketC2S::Authenticate { + token: config.api_token.clone(), + request_write_perms: true, + })?); + + client.send_message(&message)?; + + for msg in client.incoming_messages() { + match msg { + Ok(msg) => { + match msg { + OwnedMessage::Text(txt) => { + // decode + debug!("{}", txt); + + let packet: GatewayPacketS2C = serde_json::from_str(&txt)?; + + match packet { + GatewayPacketS2C::AuthenticationAccepted { write_perms } => { + info!("auth accepted by server"); + if !write_perms { + error!("Server did not grant us write permission to the gateway socket. This is required."); + error!("Check that you provided the proper token and the API has it read as write-allow."); + std::process::exit(1); + } + break; + } + GatewayPacketS2C::Disconnect { reason } => { + info!("disconnected by server: {:?}", reason); + return Err("Disconnected by server")?; + } + _ => () + } + } + OwnedMessage::Close(_) => { + info!("closing connection"); + return Ok(()); + } + _ => { + debug!("ignoring unknown type"); + } + } + } + Err(e) => { + if matches!(e, WebSocketError::NoDataAvailable) { + continue; + } + error!("rx error: {}", e); + return Err(e.into()); + } + } + } + + info!("Connected to gateway, starting the discord client"); + + let framework = StandardFramework::new().group(&GENERAL_GROUP).normal_message(normal_message); + + let intents = GatewayIntents::non_privileged() | GatewayIntents::MESSAGE_CONTENT; + let mut d_client = Client::builder(config.discord_token.clone(), intents).event_handler(Handler).framework(framework).await?; + + { + let mut data = d_client.data.write().await; + data.insert::(Arc::new(RwLock::new(config.clone()))); + data.insert::(Arc::new(Mutex::new(client))); + } + + if let Err(why) = d_client.start().await { + error!("Discord client error: {}", why); + } + + Ok(()) +} + +#[hook] +async fn normal_message(ctx: &Context, msg: &Message) { + let (config, ws) = { + let data_read = ctx.data.read().await; + + (data_read.get::().unwrap().clone(), data_read.get::().unwrap().clone()) + }; + + if !config.read().await.relay_channels.contains(msg.channel_id.as_u64()) { return; } + + let mut content = msg.content.clone(); + + if !msg.attachments.is_empty() { + for attachment in &msg.attachments { + content += " "; + content += &attachment.url; + } + } + + let message = WsMessage::text(serde_json::to_string(&GatewayPacketC2S::Relay { + msg: GatewayChatMessage { + source: GatewayChatSource::Discord, + username: format!("@{}", msg.author.name), + message: content, + timestamp: DateTime::from(SystemTime::now()) + } + }).unwrap()); + + ws.lock().await.send_message(&message).unwrap(); +} \ No newline at end of file