use std::sync::Arc; use anyhow::Result; use chrono::prelude::*; use evtclib::{Log, Outcome}; use serenity::client::bridge::gateway::ShardManager; use serenity::model::id::*; use serenity::prelude::*; use tokio::runtime::Runtime; use log::info; use super::categories::Categorizable; const MAX_HOURS: i64 = 5; const MAX_MESSAGE_LENGTH: usize = 2000; struct ShardManagerContainer; impl TypeMapKey for ShardManagerContainer { type Value = Arc>; } struct PostLinkResult; impl TypeMapKey for PostLinkResult { type Value = Result<()>; } #[derive(Debug, Clone)] struct Handler { channel_id: u64, log: Log, link: String, } impl Handler { async fn do_link_update(&self, ctx: &Context) -> Result<()> { let mut messages = ChannelId(self.channel_id) .messages(&ctx, |r| r.limit(25)) .await?; messages.sort_by_key(|m| m.timestamp); // Retain does not work with async predicates, so we have to do it the old-fashioned way. // This is slower than a proper implementation because we do more element shifts than // needed, but it is also the easiest way to implement it and shouldn't matter for the 25 // messages that we load. let mut i = 0; while i < messages.len() { let is_good = messages[i].is_own(ctx).await && Utc::now().signed_duration_since(messages[i].timestamp) < chrono::Duration::hours(MAX_HOURS); if is_good { i += 1; } else { messages.remove(i); } } if let Some(mut m) = messages.pop() { let new_text = insert_link(&m.content, &self.log, &self.link); if new_text.len() <= MAX_MESSAGE_LENGTH { m.edit(ctx, |m| m.content(new_text)).await?; return Ok(()); } } let new_text = insert_link("", &self.log, &self.link); ChannelId(self.channel_id).say(ctx, new_text).await?; Ok(()) } } #[serenity::async_trait] impl EventHandler for Handler { async fn ready(&self, ctx: Context, _ready: serenity::model::gateway::Ready) { info!("Discord client is ready"); let result = self.do_link_update(&ctx).await; let mut data = ctx.data.write().await; data.insert::(result); if let Some(manager) = data.get::() { manager.lock().await.shutdown_all().await; } } } pub fn post_link(discord_token: &str, channel_id: u64, log: &Log, link: &str) -> Result<()> { let link = link.to_owned(); let log = log.clone(); let mut rt = Runtime::new()?; rt.block_on(async { let mut client = Client::builder(discord_token) .event_handler(Handler { channel_id, log, link, }) .await?; { let mut data = client.data.write().await; data.insert::(Arc::clone(&client.shard_manager)); } client.start().await?; let mut data = client.data.write().await; data.remove::().unwrap_or(Ok(())) }) } fn find_insertion(text: &str, category: &str) -> Option { let cat_pos = text.find(&format!("**{}**", category))?; let empty_line = text[cat_pos..].find("\n\n")?; Some(cat_pos + empty_line + 1) } fn insert_link(text: &str, log: &Log, link: &str) -> String { let mut text = format!("\n\n{}\n\n", text); let point = find_insertion(&text, log.category()); let link_line = format!("{} {}\n", state_emoji(log), link); if let Some(i) = point { text.insert_str(i, &link_line); } else { text.push_str(&format!("**{}**\n{}", log.category(), link_line)); } text.trim().into() } fn state_emoji(log: &Log) -> &'static str { let outcome = log.analyzer().and_then(|a| a.outcome()); match outcome { Some(Outcome::Success) => "✅", Some(Outcome::Failure) => "❌", None => "❓", } }