aboutsummaryrefslogtreecommitdiff
path: root/src/game.rs
diff options
context:
space:
mode:
authorNathan Perry <np@nathanperry.dev>2024-08-06 10:45:06 -0400
committerNathan Perry <np@nathanperry.dev>2024-08-06 10:45:06 -0400
commit72d9bbe15220c21909dec8e30fb80729a24cec72 (patch)
tree5025c799e3065553c1e6a91b82cb2eae8e00c43e /src/game.rs
parent9319e0b9987114ffef2cc2be2d00f127925ba3a8 (diff)
first pass convert to poise
Diffstat (limited to 'src/game.rs')
-rw-r--r--src/game.rs606
1 files changed, 0 insertions, 606 deletions
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<u64>,
-
- #[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<String, ProfileInfo> = {
- let v: Vec<UserInfo> = serde_json::from_str(&USER_MAP_STR).unwrap();
-
- let result = v
- .into_iter()
- .map(|ui| {
- let UserInfo {
- name,
- profile,
- } = ui;
-
- (name, profile)
- })
- .collect::<FnvHashMap<_, _>>();
-
- info!(
- "loaded user info for {} users ({:#?})",
- result.len(),
- result.keys().collect::<Vec<_>>()
- );
-
- result
- };
- static ref DISCORD_MAP: FnvHashMap<UserId, String> = {
- USER_INFO_MAP
- .clone()
- .into_iter()
- .map(|(name, profile)| (UserId::new(profile.discord_user_id), name))
- .collect::<FnvHashMap<_, _>>()
- };
- static ref STEAM_MAP: FnvHashMap<UserId, u64> = {
- USER_INFO_MAP
- .clone()
- .into_iter()
- .filter_map(|(_, profile)| {
- profile.steam_id.map(|sid| (UserId::new(profile.discord_user_id), sid))
- })
- .collect::<FnvHashMap<_, _>>()
- };
- static ref ALPHABET: Vec<char> = (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<Self> {
- 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<S: AsRef<str>>(g: &Guild, s: S) -> StdResult<UserId, UserLookupError> {
- 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::<FnvHashSet<_>>();
-
- 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<String> = if args.rest().is_empty() {
- Vec::new()
- } else {
- args.quoted().iter::<String>().collect::<StdResult<Vec<_>, ArgError<Infallible>>>()?
- };
-
- 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::<FnvHashSet<_>>()
- .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::<FnvHashMap<_, _>>();
-
- 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::<FnvHashSet<_>>();
- }
-
- 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::<HttpKey>().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::<FnvHashMap<_, _>>();
-
- 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::<FnvHashMap<_, _>>();
-
- (1..data[*col].len()).for_each(|i| {
- let status =
- &data_ref[*col][i].parse::<GameStatus>().unwrap_or(GameStatus::Unknown);
- let game = &data_ref[0][i];
-
- game_map.get_mut(status).unwrap().insert(game);
- });
-
- (user, game_map)
- })
- .collect::<FnvHashMap<_, _>>();
-
- let statuses = vec![
- GameStatus::Installed,
- GameStatus::NotOwned,
- GameStatus::NotInstalled,
- GameStatus::Unknown,
- ]
- .into_iter()
- .filter(|s| s <= &min_status)
- .collect::<Vec<_>>();
-
- let mut games_in_common = {
- let game_map = user_games.values().next().unwrap();
-
- statuses.iter().fold(iter::empty().collect::<FnvHashSet<_>>(), |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::<FnvHashSet<_>>(), |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<Vec<Vec<String>>> {
- 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<Inner>,
- }
-
- #[derive(Deserialize)]
- struct Inner {
- values: Vec<Vec<String>>,
- }
-
- let resp = resp.json::<Resp>().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::<HttpKey>().unwrap().clone()
- };
-
- let arg_user = args.single_quoted::<String>();
-
- 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::<GameStatus>().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::<u64>().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<SteamGameEntry>,
- }
-
- #[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::<HttpKey>().unwrap().clone()
- };
-
- let games_owned = client
- .get(u)
- .send()
- .await?
- .json::<SteamResp>()
- .await?
- .response
- .games
- .into_iter()
- .map(|ge| ge.app_id)
- .collect::<FnvHashSet<_>>();
-
- 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(())
-}