From 72d9bbe15220c21909dec8e30fb80729a24cec72 Mon Sep 17 00:00:00 2001 From: Nathan Perry Date: Tue, 6 Aug 2024 10:45:06 -0400 Subject: first pass convert to poise --- src/game.rs | 606 ------------------------------------------------------------ 1 file changed, 606 deletions(-) delete mode 100644 src/game.rs (limited to 'src/game.rs') diff --git a/src/game.rs b/src/game.rs deleted file mode 100644 index 4011a5d..0000000 --- a/src/game.rs +++ /dev/null @@ -1,606 +0,0 @@ -use std::{ - convert::Infallible, - fs, - iter, - path::PathBuf, - result::Result as StdResult, - str::{ - self, - FromStr, - }, -}; - -use anyhow::{ - anyhow, - Error, -}; -use fnv::{ - FnvHashMap, - FnvHashSet, -}; -use itertools::Itertools; -use lazy_static::lazy_static; -use log::{ - debug, - error, - info, -}; -use serde::Deserialize; -use serenity::{ - framework::standard::{ - macros::{ - command, - group, - }, - ArgError, - Args, - CommandError, - CommandResult, - }, - futures::TryFutureExt, - model::{ - channel::Message, - guild::Guild, - id::UserId, - }, - prelude::*, -}; -use tap::Pipe; -use url::Url; - -use crate::{ - bot::HttpKey, - util, - Result, - CONFIG, -}; - -#[group] -#[commands(game, installedgame, ownedgame, updategaem)] -pub struct Game; - -lazy_static! { - static ref SPREADSHEET_URL: Url = Url::parse(&format!( - "https://sheets.googleapis.com/v4/spreadsheets/{}/values:batchGet", - &CONFIG.sheets.spreadsheet, - )) - .expect("parsing spreadsheet url"); -} - -#[derive(Deserialize, Debug, Clone, PartialEq, Eq, Hash)] -struct UserInfo { - name: String, - - #[serde(flatten)] - profile: ProfileInfo, -} - -#[derive(Deserialize, Debug, Clone, Copy, PartialEq, Eq, Hash)] -struct ProfileInfo { - #[serde(rename = "steam")] - steam_id: Option, - - #[serde(rename = "discord")] - discord_user_id: u64, -} - -lazy_static! { - static ref USER_MAP_STR: String = { - let default_path = PathBuf::from_str("user_id_mapping").unwrap(); - let mapping_path = CONFIG.user_id_mapping.as_ref().unwrap_or(&default_path); - - fs::read_to_string(mapping_path).unwrap_or("{}".to_owned()) - }; - static ref USER_INFO_MAP: FnvHashMap = { - let v: Vec = serde_json::from_str(&USER_MAP_STR).unwrap(); - - let result = v - .into_iter() - .map(|ui| { - let UserInfo { - name, - profile, - } = ui; - - (name, profile) - }) - .collect::>(); - - info!( - "loaded user info for {} users ({:#?})", - result.len(), - result.keys().collect::>() - ); - - result - }; - static ref DISCORD_MAP: FnvHashMap = { - USER_INFO_MAP - .clone() - .into_iter() - .map(|(name, profile)| (UserId::new(profile.discord_user_id), name)) - .collect::>() - }; - static ref STEAM_MAP: FnvHashMap = { - USER_INFO_MAP - .clone() - .into_iter() - .filter_map(|(_, profile)| { - profile.steam_id.map(|sid| (UserId::new(profile.discord_user_id), sid)) - }) - .collect::>() - }; - static ref ALPHABET: Vec = (0..26).map(|x| (x + b'a') as char).collect(); -} - -#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, PartialOrd)] -enum GameStatus { - Installed, - NotInstalled, - NotOwned, - Unknown, -} - -impl FromStr for GameStatus { - type Err = Error; - - fn from_str(s: &str) -> Result { - use std::char; - - if s.starts_with('y') { - Ok(GameStatus::Installed) - } else if s.starts_with("n/i") { - Ok(GameStatus::NotInstalled) - } else if s.starts_with('n') { - Ok(GameStatus::NotOwned) - } else if s.chars().all(char::is_whitespace) { - Ok(GameStatus::Unknown) - } else { - Err(anyhow!(format!("unexpected status '{}'", s))) - } - } -} - -#[command] -#[aliases("installedgaem")] -pub async fn installedgame(ctx: &Context, msg: &Message, args: Args) -> CommandResult { - _game(ctx, msg, args, GameStatus::Installed).await -} - -#[command] -#[aliases("ownedgaem")] -pub async fn ownedgame(ctx: &Context, msg: &Message, args: Args) -> CommandResult { - _game(ctx, msg, args, GameStatus::NotInstalled).await -} - -#[derive(Copy, Clone, Debug, thiserror::Error, PartialEq, Eq, Hash)] -pub enum UserLookupError { - #[error("too many possible options ({}) for query", _0)] - Ambiguous(usize), - - #[error("user wasn't found in the guild")] - NotFound, -} - -pub fn get_user_id>(g: &Guild, s: S) -> StdResult { - let s = s.as_ref().trim_start_matches('@').to_lowercase(); - - if let Some(info) = USER_INFO_MAP.get(&s) { - return Ok(UserId::new(info.discord_user_id)); - } - - let nicks = g.members_nick_containing(&s, false, false); - - { - let exact_match = nicks.iter().find(|(m, _)| m.display_name().to_lowercase() == s); - - if let Some((m, _)) = exact_match { - return Ok(m.user.id); - } - } - - let usernames = g.members_username_containing(&s, false, false); - - { - let exact_match = usernames.iter().find(|(m, _)| m.user.name.to_lowercase() == s); - - if let Some((m, _)) = exact_match { - return Ok(m.user.id); - } - } - - let opts = nicks - .into_iter() - .chain(usernames) - .map(|(member, _)| member.user.id) - .collect::>(); - - match opts.len() { - 0 => Err(UserLookupError::NotFound), - 1 => Ok(opts.into_iter().next().unwrap()), - x => Err(UserLookupError::Ambiguous(x)), - } -} - -#[command] -#[aliases("gaem")] -async fn game(ctx: &Context, msg: &Message, args: Args) -> CommandResult { - _game(ctx, msg, args, GameStatus::Installed).await -} - -async fn _game( - ctx: &Context, - msg: &Message, - mut args: Args, - min_status: GameStatus, -) -> CommandResult { - let users = { - let guild = - msg.channel_id.to_channel(&ctx).await?.guild().ok_or(anyhow!("couldn't find guild"))?; - - let user_args: Vec = if args.rest().is_empty() { - Vec::new() - } else { - args.quoted().iter::().collect::, ArgError>>()? - }; - - use serenity::futures::StreamExt; - - let mut users = user_args - .into_iter() - .pipe(serenity::futures::stream::iter) - .filter_map(|u| { - let guild = &guild; - async move { - use std::borrow::Borrow; - - let possible = { - let Ok(guild) = - guild.guild(&ctx).ok_or_else(|| anyhow!("couldn't find guild")) - else { - error!("failed retrieving guild"); - return None; - }; - - get_user_id(guild.borrow(), &u) - }; - - debug!("parsed userid {:?}", possible); - - match possible { - Err(UserLookupError::NotFound) => { - let _ = util::send( - ctx, - msg.channel_id, - &format!("didn't recognize {}", &u), - msg.tts, - ) - .await; - None - }, - Ok(x) => Some(x), - Err(UserLookupError::Ambiguous(x)) => { - let _ = util::send( - ctx, - msg.channel_id, - &format!("too many matches ({}) for {}", x, &u), - msg.tts, - ) - .await; - None - }, - } - } - }) - .filter_map(|uid| async move { - let res = DISCORD_MAP.get(&uid).map(|s| s.to_lowercase()); - - if res.is_none() { - info!("user {} is not recognized", uid); - } - - res - }) - .collect::>() - .await; - - if users.is_empty() { - let guild = guild.guild(&ctx).ok_or_else(|| anyhow!("couldn't find guild"))?; - - let pairs = guild - .voice_states - .iter() - .filter_map(|(uid, voice)| voice.channel_id.map(|cid| (*uid, cid))) - .collect::>(); - - let channel = - pairs.get(&msg.author.id).cloned().unwrap_or(CONFIG.discord.voice_channel()); - - users = pairs - .iter() - .filter_map(|(uid, cid)| { - if *cid == channel { - DISCORD_MAP.get(uid).map(|s| s.to_lowercase()) - } else { - None - } - }) - .collect::>(); - } - - users - }; - - let inferred = users.is_empty(); - - if inferred && users.len() < 2 || !inferred && users.is_empty() { - info!("too few known users to make game comparison"); - util::send(ctx, msg.channel_id, "yer too lonely", msg.tts).await?; - return Ok(()); - } - - let client = { - let data = ctx.data.read().await; - data.get::().unwrap().clone() - }; - let data = load_spreadsheet(&client).await?; - - let user_indexes = (0..data.len()) - .filter_map(|i| { - let user = data[i][0].to_lowercase(); - - if users.contains(&user) { - Some((user, i)) - } else { - None - } - }) - .collect::>(); - - let data_ref = &data; - let user_games = user_indexes - .iter() - .map(|(user, col)| { - let empty_hash_set: FnvHashSet<_> = vec![].into_iter().collect(); - - let mut game_map = vec![ - (GameStatus::Installed, empty_hash_set.clone()), - (GameStatus::NotInstalled, empty_hash_set.clone()), - (GameStatus::NotOwned, empty_hash_set.clone()), - (GameStatus::Unknown, empty_hash_set), - ] - .into_iter() - .collect::>(); - - (1..data[*col].len()).for_each(|i| { - let status = - &data_ref[*col][i].parse::().unwrap_or(GameStatus::Unknown); - let game = &data_ref[0][i]; - - game_map.get_mut(status).unwrap().insert(game); - }); - - (user, game_map) - }) - .collect::>(); - - let statuses = vec![ - GameStatus::Installed, - GameStatus::NotOwned, - GameStatus::NotInstalled, - GameStatus::Unknown, - ] - .into_iter() - .filter(|s| s <= &min_status) - .collect::>(); - - let mut games_in_common = { - let game_map = user_games.values().next().unwrap(); - - statuses.iter().fold(iter::empty().collect::>(), |acc, s| { - acc.union(&game_map[s]).cloned().collect() - }) - }; - - for (_user, game_map) in user_games.iter() { - let relevant_games = - statuses.iter().fold(iter::empty().collect::>(), |acc, s| { - acc.union(&game_map[s]).cloned().collect() - }); - - games_in_common = games_in_common.intersection(&relevant_games).cloned().collect(); - } - - let mut games_formatted = - games_in_common.iter().sorted_by(|a, b| a.to_lowercase().cmp(&b.to_lowercase())).join("\n"); - - if games_formatted.is_empty() { - games_formatted = "**LITERALLY NOTHING**".to_owned(); - } - - util::send(ctx, msg.channel_id, &games_formatted, msg.tts).await?; - - Ok(()) -} - -async fn load_spreadsheet(client: &reqwest::Client) -> Result>> { - let mut u = SPREADSHEET_URL.clone(); - - u.query_pairs_mut() - .append_pair("ranges", &format!("a1:{}", &CONFIG.sheets.max_column)) - .append_pair("valueRenderOption", "FORMATTED_VALUE") - .append_pair("majorDimension", "COLUMNS") - .append_pair("key", &CONFIG.sheets.api_key); - - let req = reqwest::Request::new(reqwest::Method::GET, u); - let resp = client.execute(req).await?; - - #[derive(Deserialize)] - struct Resp { - #[serde(rename = "valueRanges")] - value_ranges: Vec, - } - - #[derive(Deserialize)] - struct Inner { - values: Vec>, - } - - let resp = resp.json::().await?; - - Ok(resp.value_ranges.into_iter().next().unwrap().values) -} - -#[command] -#[aliases("updategame")] -pub async fn updategaem(ctx: &Context, msg: &Message, mut args: Args) -> CommandResult { - use regex::Regex; - - let client = { - let data = ctx.data.read().await; - data.get::().unwrap().clone() - }; - - let arg_user = args.single_quoted::(); - - let user = if arg_user.is_err() { - msg.author.id - } else { - use std::borrow::Borrow; - - let guild = - msg.channel_id.to_channel(&ctx).await?.guild().ok_or(anyhow!("couldn't find guild"))?; - - let guild = guild.guild(&ctx).ok_or(anyhow!("couldn't find guild"))?; - - get_user_id(guild.borrow(), arg_user.unwrap()).map_err(Error::from)? - }; - - debug!("parsed userid {:?}", user); - - let username = match DISCORD_MAP.get(&user) { - Some(s) => s, - None => { - return util::send(ctx, msg.channel_id, "WHO THE FUCK ARE YE", msg.tts) - .map_err(CommandError::from) - .await; - }, - }; - - let steam_id = match STEAM_MAP.get(&user) { - Some(u) => u, - None => { - return util::send(ctx, msg.channel_id, "WHO ARE YE ON STEAM", msg.tts) - .map_err(CommandError::from) - .await; - }, - }; - - let spreadsheet = load_spreadsheet(&client).await?; - - let user_column = (0..spreadsheet.len()) - .find(|x| spreadsheet[*x][0].to_lowercase() == username.to_lowercase()); - - let user_column = match user_column { - Some(c) => &spreadsheet[c][1..], - None => { - return util::send(ctx, msg.channel_id, "YER NOT IN THE SPREADSHEET", msg.tts) - .map_err(CommandError::from) - .await; - }, - }; - - lazy_static! { - static ref APPID_REGEX: Regex = Regex::new(r#"(?i)^\s*app\s*id\s*$"#).unwrap(); - } - - let appid_column = (0..spreadsheet.len()).find(|x| APPID_REGEX.is_match(&spreadsheet[*x][0])); - - let appid_column = match appid_column { - Some(c) => &spreadsheet[c][1..], - None => { - error!("didn't find an appid column in the spreadsheet"); - return util::send(ctx, msg.channel_id, "SPREADSHEET BROKE", msg.tts) - .map_err(CommandError::from) - .await; - }, - }; - - let missing_appids = (0..user_column.len()) - .filter_map(|x| user_column[x].parse::().ok().map(|s| (x, s))) - .filter(|(_, s)| *s == GameStatus::Unknown || *s == GameStatus::NotOwned) - .filter_map(|(x, _)| { - appid_column.get(x).and_then(|s| s.parse::().ok().map(|appid| (appid, x))) - }); - - let mut u = Url::parse("https://api.steampowered.com/IPlayerService/GetOwnedGames/v1")?; - - u.query_pairs_mut() - .append_pair("key", &CONFIG.steam_api_key) - .append_pair("include_played_free_games", "1") - .append_pair("steamid", &steam_id.to_string()); - - #[derive(Deserialize, Clone, Debug, PartialEq, Eq, Hash)] - struct SteamResp { - response: SteamInner, - } - - #[derive(Deserialize, Clone, Debug, PartialEq, Eq, Hash)] - struct SteamInner { - games: Vec, - } - - #[derive(Deserialize, Clone, Copy, Debug, PartialEq, Eq, Hash)] - struct SteamGameEntry { - #[serde(rename = "appid")] - app_id: u64, - - #[serde(rename = "playtime_forever")] - play_time: u64, - } - - let client = { - let data = ctx.data.read().await; - data.get::().unwrap().clone() - }; - - let games_owned = client - .get(u) - .send() - .await? - .json::() - .await? - .response - .games - .into_iter() - .map(|ge| ge.app_id) - .collect::>(); - - let found_games = missing_appids - .filter_map(|(ai, x)| { - if games_owned.contains(&ai) { - Some(&spreadsheet[0][x + 1]) - } else { - None - } - }) - .join("\n"); - - if !found_games.is_empty() { - util::send( - ctx, - msg.channel_id, - &format!( - "{} games owned on steam that are missing from the list:\n{}", - found_games.chars().filter(|x| *x == '\n').count() + 1, - found_games - ), - msg.tts, - ) - .await?; - } else { - util::send(ctx, msg.channel_id, "up to date", msg.tts).await?; - } - - Ok(()) -} -- cgit v1.3.1