use std::sync::Arc; use anyhow::Result; use chrono::prelude::*; use evtclib::Log; use serenity::client::bridge::gateway::ShardManager; use serenity::model::id::*; use serenity::prelude::*; use tokio::runtime::Runtime; use log::info; use super::config::Config; use super::categories::Categorizable; use super::logbag::{state_emoji, LogBag}; 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, sort_logs: bool, } 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, self.sort_logs); 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, false); 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(config: &Config, discord_token: &str, channel_id: u64, log: &Log, link: &str) -> Result<()> { let link = link.to_owned(); let log = log.clone(); let rt = Runtime::new()?; rt.block_on(async { let mut client = Client::builder(discord_token) .event_handler(Handler { channel_id, log, link, sort_logs: config.sort_logs, }) .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 insert_link(text: &str, log: &Log, link: &str, sort_logs: bool) -> String { let mut logbag = LogBag::parse_markdown(text).unwrap(); let line = format!("{} {}", state_emoji(log), link); logbag.insert(log.category(), line); if sort_logs { logbag.sort(); } logbag.render_markdown() }