use crate::bit_iterator::BitIterator;
use regex::Regex;
#[cfg(feature = "serialization")]
use serde::de::{self, Visitor};
use std::collections::HashMap;
use std::convert::TryFrom;
use std::convert::TryInto;
use std::str::FromStr;
const DEFAULT_STEAM_ACCOUNT_TYPE: u8 = 1;
const DEFAULT_STEAM_ACCOUNT_INSTANCE: u8 = 1;
#[derive(Debug, Copy, Clone, PartialEq, Eq)]
#[cfg_attr(feature = "serialization", derive(serde::Deserialize))]
#[cfg_attr(feature = "serialization", serde(rename_all = "lowercase"))]
pub enum OnlineState {
Offline,
Online,
#[cfg_attr(feature = "serialization", serde(rename = "in-game"))]
InGame,
#[cfg_attr(feature = "serialization", serde(other))]
Other,
}
impl std::fmt::Display for OnlineState {
fn fmt(&self, fmt: &mut std::fmt::Formatter) -> std::fmt::Result {
fmt.write_str(match *self {
OnlineState::Offline => "Offline",
OnlineState::Online => "Online",
OnlineState::InGame => "In game",
OnlineState::Other => "Other",
})
}
}
#[derive(Debug, Copy, Clone, Ord, PartialOrd, Hash, PartialEq, Eq)]
#[cfg_attr(
feature = "serialization",
derive(serde::Serialize, serde::Deserialize)
)]
pub enum Universe {
IndividualOrUnspecified = 0,
Public = 1,
Beta = 2,
Internal = 3,
Developer = 4,
Rc = 5,
}
impl std::fmt::Display for Universe {
fn fmt(&self, fmt: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
fmt.write_str(match self {
Universe::IndividualOrUnspecified => "Individual or unspecified",
Universe::Public => "Public",
Universe::Beta => "Beta",
Universe::Internal => "Internal",
Universe::Developer => "Developer",
Universe::Rc => "RC",
})
}
}
impl std::convert::TryFrom<u64> for Universe {
type Error = crate::error::Error;
fn try_from(value: u64) -> crate::error::Result<Self> {
Ok(match value {
0 => Universe::IndividualOrUnspecified,
1 => Universe::Public,
2 => Universe::Beta,
3 => Universe::Internal,
4 => Universe::Developer,
5 => Universe::Rc,
_ => return Err("The number doesn't represent a correct steam id universe.".into()),
})
}
}
impl std::convert::TryFrom<u32> for Universe {
type Error = crate::error::Error;
fn try_from(value: u32) -> crate::error::Result<Self> {
Self::try_from(u64::from(value))
}
}
impl std::convert::TryFrom<u16> for Universe {
type Error = crate::error::Error;
fn try_from(value: u16) -> crate::error::Result<Self> {
Self::try_from(u64::from(value))
}
}
impl std::convert::TryFrom<u8> for Universe {
type Error = crate::error::Error;
fn try_from(value: u8) -> crate::error::Result<Self> {
Self::try_from(u64::from(value))
}
}
#[derive(Debug, Copy, Clone, Ord, PartialOrd, Hash, PartialEq, Eq)]
#[cfg_attr(
feature = "serialization",
derive(serde::Serialize, serde::Deserialize)
)]
pub enum AccountType {
Individual = 1,
Multiseat = 2,
GameServer = 3,
AnonymousGameServer = 4,
Pending = 5,
ContentServer = 6,
Clan = 7,
Chat = 8,
PeerToPeerSuperSeeder = 9,
AnonymousUser = 10,
#[cfg_attr(feature = "serialization", serde(other))]
Invalid = 0,
}
impl std::fmt::Display for AccountType {
fn fmt(&self, fmt: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
fmt.write_str(match self {
AccountType::Individual => "Individual",
AccountType::Multiseat => "Multiseat",
AccountType::GameServer => "Game server",
AccountType::AnonymousGameServer => "Anonymous game server",
AccountType::Pending => "Pending",
AccountType::ContentServer => "Content server",
AccountType::Clan => "Clan",
AccountType::Chat => "Chat",
AccountType::PeerToPeerSuperSeeder => "Peer to peer superseeder",
AccountType::AnonymousUser => "Anonymous user",
AccountType::Invalid => "Invalid",
})
}
}
lazy_static::lazy_static! {
static ref ACCOUNT_TYPE_MAP: HashMap<char, AccountType> = vec![
('I', AccountType::Invalid),
('U', AccountType::Individual),
('M', AccountType::Multiseat),
('G', AccountType::GameServer),
('A', AccountType::AnonymousGameServer),
('P', AccountType::Pending),
('C', AccountType::ContentServer),
('g', AccountType::Clan),
('T', AccountType::Chat),
('L', AccountType::Chat),
('c', AccountType::Chat),
('a', AccountType::AnonymousUser),
].into_iter().collect();
static ref ID32_REGEXP: Regex = {
Regex::new(r"^STEAM_(\d):(\d):(\d+)$").unwrap()
};
static ref ID3_REGEXP: Regex = {
Regex::new(r"^(\w):(\d):(\d+)$").unwrap()
};
}
impl std::str::FromStr for AccountType {
type Err = crate::error::Error;
fn from_str(s: &str) -> crate::error::Result<Self> {
if s.len() > 1 {
Err("The passed string is too long to represent a steam account type.".into())
} else {
Self::try_from(s.chars().next().unwrap()) }
}
}
impl std::convert::TryFrom<char> for AccountType {
type Error = crate::error::Error;
fn try_from(value: char) -> crate::error::Result<Self> {
Ok(*ACCOUNT_TYPE_MAP
.get(&value)
.ok_or_else(|| "The string doesn't contain a correct id account type.".to_owned())?)
}
}
impl std::convert::TryFrom<u64> for AccountType {
type Error = crate::error::Error;
fn try_from(value: u64) -> crate::error::Result<Self> {
Ok(match value {
0 => AccountType::Invalid,
1 => AccountType::Individual,
2 => AccountType::Multiseat,
3 => AccountType::GameServer,
4 => AccountType::AnonymousGameServer,
5 => AccountType::Pending,
6 => AccountType::ContentServer,
7 => AccountType::Clan,
8 => AccountType::Chat,
9 => AccountType::PeerToPeerSuperSeeder,
10 => AccountType::AnonymousUser,
_ => return Err("The number doesn't represent a correct steam id universe.".into()),
})
}
}
impl std::convert::TryFrom<u32> for AccountType {
type Error = crate::error::Error;
fn try_from(value: u32) -> crate::error::Result<Self> {
Self::try_from(u64::from(value))
}
}
impl std::convert::TryFrom<u16> for AccountType {
type Error = crate::error::Error;
fn try_from(value: u16) -> crate::error::Result<Self> {
Self::try_from(u64::from(value))
}
}
impl std::convert::TryFrom<u8> for AccountType {
type Error = crate::error::Error;
fn try_from(value: u8) -> crate::error::Result<Self> {
Self::try_from(u64::from(value))
}
}
#[derive(Debug, Copy, Clone, Ord, PartialOrd, Hash, PartialEq, Eq)]
#[cfg_attr(
feature = "serialization",
derive(serde::Serialize, serde::Deserialize)
)]
pub struct Info {
pub universe: Universe,
pub account_type: AccountType,
pub instance: u32,
pub account: u32,
pub authentication_server: u8,
}
#[allow(clippy::clippy::module_name_repetitions)]
#[derive(Debug, Copy, Clone, Ord, PartialOrd, Hash, PartialEq, Eq)]
#[cfg_attr(
feature = "serialization",
derive(serde::Serialize, serde::Deserialize)
)]
pub struct Id64(pub u64);
impl Id64 {
pub fn info(self) -> crate::error::Result<Info> {
let mut iter = BitIterator::new(self.0, 8);
Ok(Info {
universe: iter.next().unwrap().try_into()?,
account_type: iter.next_bits::<u8>(4).unwrap().try_into()?,
instance: iter.next_bits::<u32>(20).unwrap(),
account: iter.next_bits::<u32>(31).unwrap(),
authentication_server: iter.next_bits::<u8>(1).unwrap(),
})
}
pub fn new_simple(
universe: Universe,
authentication_server: u8,
account: u32,
) -> crate::error::Result<Id64> {
Id64::new_full(
universe,
AccountType::try_from(DEFAULT_STEAM_ACCOUNT_TYPE)?,
DEFAULT_STEAM_ACCOUNT_INSTANCE.into(),
authentication_server,
account,
)
}
pub fn new_full(
universe: Universe,
account_type: AccountType,
account_instance: u32,
authentication_server: u8,
account: u32,
) -> crate::error::Result<Id64> {
let num = u64::from_str_radix(
&format!(
"{:08b}{:04b}{:020b}{:031b}{:b}",
universe as u8,
account_type as u8,
account_instance,
account,
authentication_server
),
2,
)?;
Ok(Id64(num))
}
}
#[allow(clippy::clippy::module_name_repetitions)]
#[derive(Debug, Clone, Ord, PartialOrd, Hash, PartialEq, Eq)]
#[cfg_attr(
feature = "serialization",
derive(serde::Serialize, serde::Deserialize)
)]
pub struct Id32(pub String);
#[allow(clippy::clippy::module_name_repetitions)]
#[derive(Debug, Clone, Ord, PartialOrd, Hash, PartialEq, Eq)]
#[cfg_attr(
feature = "serialization",
derive(serde::Serialize, serde::Deserialize)
)]
pub struct Id3(pub String);
impl Id3 {
pub fn info(&self) -> crate::error::Result<Info> {
let split: Vec<&str> = self.0.split(':').collect();
if split.len() < 3 {
return Err(crate::error::ErrorKind::InvalidSteamId(self.0.clone()).into());
}
let authentication_server: u8 = split[1].parse()?;
let account: u32 = split[2].parse()?;
Ok(Info {
universe: Universe::IndividualOrUnspecified,
account_type: AccountType::from_str(split[0])?,
instance: u32::from(DEFAULT_STEAM_ACCOUNT_INSTANCE),
account,
authentication_server,
})
}
}
#[allow(clippy::clippy::module_name_repetitions)]
#[derive(Debug, Clone, Eq, PartialEq, Ord, PartialOrd, Hash)]
pub enum Id {
Id64(Id64),
Id32(Id32),
Id3(Id3),
}
impl std::convert::TryFrom<u64> for Id {
type Error = crate::error::Error;
fn try_from(id: u64) -> crate::error::Result<Id> {
let id = Id64(id);
if let Err(e) = id.info() {
return Err(e);
}
Ok(Id::Id64(id))
}
}
impl std::convert::TryFrom<Id32> for Id3 {
type Error = crate::error::Error;
fn try_from(id: Id32) -> crate::error::Result<Id3> {
if ID32_REGEXP.is_match(&id.0) {
let split: Vec<&str> = id.0.split(':').collect();
let first: u64 = split[1].parse()?;
let second: u64 = split[2].parse()?;
let num = second * 2 + first;
return Ok(Id3(format!("U:1:{}", num)));
}
Err("The steam id provided is not in the SteamID32 format.".into())
}
}
impl std::convert::TryFrom<Id3> for Id32 {
type Error = crate::error::Error;
fn try_from(id: Id3) -> crate::error::Result<Id32> {
if let Some(captures) = ID3_REGEXP.captures(&id.0) {
if captures.len() < 4 {
return Err("The steam id provided is not in the SteamID3 format.".into());
}
let _account_type = AccountType::from_str(captures.get(1).unwrap().as_str())?;
let _authentication_server: u8 = captures.get(2).unwrap().as_str().parse()?;
let account: u32 = captures.get(3).unwrap().as_str().parse()?;
if account % 2 == 0 {
return Ok(Id32(format!("STEAM_0:0:{}", account / 2)));
}
return Ok(Id32(format!("STEAM_0:1:{}", (account - 1) / 2)));
}
Err("The steam id provided is not in the SteamID3 format.".into())
}
}
impl TryFrom<Id64> for Id32 {
type Error = crate::error::Error;
fn try_from(id: Id64) -> crate::error::Result<Id32> {
let info = id.info()?;
Ok(Id32(format!(
"STEAM_0:{}:{}",
info.authentication_server, info.account
)))
}
}
impl TryFrom<Id32> for Id64 {
type Error = crate::error::Error;
fn try_from(id: Id32) -> crate::error::Result<Id64> {
if let Some(captures) = ID32_REGEXP.captures(&id.0) {
if captures.len() < 4 {
return Err("The steam id provided is not in the SteamID32 format.".into());
}
let mut universe: u8 = captures.get(1).unwrap().as_str().parse()?;
let authentication_server: u8 = captures.get(2).unwrap().as_str().parse()?;
let account: u32 = captures.get(3).unwrap().as_str().parse()?;
if universe == 0 {
universe = 1;
}
return Id64::new_simple(
Universe::try_from(universe)?,
authentication_server,
account,
);
}
Err("The steam id provided is not in the SteamID32 format.".into())
}
}
impl TryFrom<Id3> for Id64 {
type Error = crate::error::Error;
fn try_from(id: Id3) -> crate::error::Result<Id64> {
Id64::try_from(Id32::try_from(id)?)
}
}
impl Id {
pub fn id64(&self) -> crate::error::Result<Id64> {
match self {
Id::Id64(num) => Ok(*num),
Id::Id32(id) => Id64::try_from(id.clone()),
Id::Id3(id) => Id64::try_from(id.clone()),
}
}
pub fn id32(&self) -> crate::error::Result<Id32> {
match self {
Id::Id64(num) => Id32::try_from(*num),
Id::Id32(id) => Ok(id.clone()),
Id::Id3(id) => Id32::try_from(id.clone()),
}
}
pub fn into_id64(self) -> crate::error::Result<Id> {
Ok(Id::Id64(self.id64()?))
}
pub fn into_id32(self) -> crate::error::Result<Id> {
Ok(Id::Id32(self.id32()?))
}
pub fn is_same(&self, other: &Id) -> crate::error::Result<bool> {
Ok(self.id64()? == other.id64()?)
}
}
impl std::str::FromStr for Id {
type Err = crate::error::Error;
fn from_str(value: &str) -> std::result::Result<Self, Self::Err> {
if let Ok(id64) = value.parse::<u64>() {
return Ok(Id::Id64(Id64(id64)));
}
if ID32_REGEXP.is_match(value) {
return Ok(Id::Id32(Id32(value.to_owned())));
}
if ID3_REGEXP.is_match(value) {
return Ok(Id::Id3(Id3(value.to_owned())));
}
Err(crate::error::ErrorKind::InvalidSteamId(format!(
"Not a valid steam id value: {}",
value
))
.into())
}
}
impl std::fmt::Display for Id {
fn fmt(&self, fmt: &mut std::fmt::Formatter) -> std::fmt::Result {
match self {
Id::Id64(num) => fmt.write_str(&num.0.to_string()),
Id::Id32(s) => fmt.write_str(&s.0),
Id::Id3(s) => fmt.write_str(&s.0),
}
}
}
#[cfg(feature = "serialization")]
impl serde::Serialize for Id {
fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
where
S: serde::Serializer,
{
serializer.serialize_u64(
self.id64()
.map_err(|e| serde::ser::Error::custom(e.description()))?
.0,
)
}
}
#[cfg(feature = "serialization")]
struct IdVisitor;
#[cfg(feature = "serialization")]
impl<'de> Visitor<'de> for IdVisitor {
type Value = Id;
fn expecting(&self, formatter: &mut std::fmt::Formatter) -> std::fmt::Result {
formatter.write_str("a steam id in the format of ID64, ID, or ID 3.")
}
fn visit_u64<E>(self, value: u64) -> std::result::Result<Self::Value, E>
where
E: de::Error,
{
Ok(Id::Id64(Id64(value)))
}
fn visit_str<E>(self, value: &str) -> std::result::Result<Self::Value, E>
where
E: de::Error,
{
Id::from_str(value).map_err(E::custom)
}
}
#[cfg(feature = "serialization")]
impl<'de> serde::Deserialize<'de> for Id {
fn deserialize<D>(deserializer: D) -> std::result::Result<Id, D::Error>
where
D: serde::Deserializer<'de>,
{
deserializer.deserialize_any(IdVisitor)
}
}
#[cfg(test)]
mod tests {
use super::*;
#[cfg(feature = "serialization")]
#[allow(clippy::unreadable_literal)]
#[test]
fn steam_id_enum_serialize() {
assert_eq!(
serde_json::to_string(&Id::Id64(Id64(76561197983318796))).unwrap(),
"76561197983318796"
);
assert_eq!(
serde_json::to_string(&Id::Id32(Id32("STEAM_0:0:11526534".to_owned()))).unwrap(),
"76561197983318796"
);
assert_eq!(
serde_json::to_string(&Id::Id3(Id3("U:1:23053068".to_owned()))).unwrap(),
"76561197983318796"
);
}
#[cfg(feature = "serialization")]
#[allow(clippy::unreadable_literal)]
#[test]
fn steam_id_enum_deserialize() {
let strid32 = "\"STEAM_0:0:11526534\"";
let id32 = serde_json::from_str::<Id>(strid32).unwrap();
assert_eq!(id32, Id::Id32(Id32("STEAM_0:0:11526534".to_owned())));
let strid64 = "76561197983318796";
let id64 = serde_json::from_str::<Id>(strid64).unwrap();
assert_eq!(id64, Id::Id64(Id64(76561197983318796)));
let strid64s = "\"76561197983318796\"";
let id64s = serde_json::from_str::<Id>(strid64s).unwrap();
assert_eq!(id64s, Id::Id64(Id64(76561197983318796)));
let strid3 = "\"U:1:23053068\"";
let id3 = serde_json::from_str::<Id>(strid3).unwrap();
assert_eq!(id3, Id::Id3(Id3("U:1:23053068".to_owned())));
}
}