use crate::twitch::Capability;
use std::collections::BTreeSet;
#[derive(Debug, Clone, PartialEq)]
#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
pub struct UserConfig {
pub name: String,
pub token: String,
pub capabilities: Vec<Capability>,
}
impl UserConfig {
pub fn builder() -> UserConfigBuilder {
UserConfigBuilder::default()
}
pub fn is_anonymous(&self) -> bool {
self.name == crate::JUSTINFAN1234 && self.token == crate::JUSTINFAN1234
}
}
#[non_exhaustive]
#[derive(Debug, Copy, Clone)]
pub enum UserConfigError {
InvalidName,
InvalidToken,
PartialAnonymous,
}
impl std::fmt::Display for UserConfigError {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
match self {
Self::InvalidName => f.write_str("invalid name"),
Self::InvalidToken => {
f.write_str("invalid token. token must start with oauth: and be 36 characters")
}
Self::PartialAnonymous => f.write_str(
"user provided name or token provided when an anonymous login was requested",
),
}
}
}
impl std::error::Error for UserConfigError {}
#[derive(Default, Debug)]
#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
pub struct UserConfigBuilder {
capabilities: BTreeSet<Capability>,
name: Option<String>,
token: Option<String>,
}
impl UserConfigBuilder {
pub fn name(mut self, name: impl ToString) -> Self {
self.name.replace(name.to_string());
self
}
pub fn token(mut self, token: impl ToString) -> Self {
self.token.replace(token.to_string());
self
}
pub fn anonymous(self) -> Self {
let (name, token) = crate::ANONYMOUS_LOGIN;
self.name(name).token(token)
}
pub fn capabilities(mut self, caps: &[Capability]) -> Self {
self.capabilities.extend(caps);
self
}
pub fn enable_all_capabilities(self) -> Self {
self.capabilities(&[
Capability::Membership,
Capability::Tags,
Capability::Commands,
])
}
pub fn build(self) -> Result<UserConfig, UserConfigError> {
let name = self
.name
.filter(|s| validate_name(s))
.ok_or(UserConfigError::InvalidName)?;
let token = self
.token
.filter(|s| validate_token(s))
.ok_or(UserConfigError::InvalidToken)?;
match (name.as_str(), token.as_str()) {
(crate::JUSTINFAN1234, crate::JUSTINFAN1234) => {
}
(crate::JUSTINFAN1234, ..) | (.., crate::JUSTINFAN1234) => {
return Err(UserConfigError::PartialAnonymous)
}
_ => {}
}
Ok(UserConfig {
name,
token,
capabilities: self.capabilities.into_iter().collect(),
})
}
}
#[inline]
const fn validate_name(s: &str) -> bool {
!s.is_empty()
}
#[inline]
fn validate_token(s: &str) -> bool {
if s == crate::JUSTINFAN1234 {
return true;
}
!s.is_empty() && s.len() == 36 && &s[..6] == "oauth:"
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn valid_user_config_no_caps() {
let config = UserConfig::builder()
.name("foo")
.token(format!("oauth:{}", "a".repeat(30)))
.build()
.unwrap();
assert_eq!(
config,
UserConfig {
name: "foo".to_string(),
token: format!("oauth:{}", "a".repeat(30)),
capabilities: vec![],
}
)
}
#[test]
fn valid_user_config() {
let config = UserConfig::builder()
.name("foo")
.token(format!("oauth:{}", "a".repeat(30)))
.capabilities(&[Capability::Tags, Capability::Tags])
.capabilities(&[Capability::Membership])
.build()
.unwrap();
assert_eq!(
config,
UserConfig {
name: "foo".to_string(),
token: format!("oauth:{}", "a".repeat(30)),
capabilities: vec![Capability::Membership, Capability::Tags,],
}
)
}
#[test]
fn valid_user_config_anonymous() {
let config = UserConfig::builder().anonymous().build().unwrap();
assert_eq!(
config,
UserConfig {
name: crate::JUSTINFAN1234.to_string(),
token: crate::JUSTINFAN1234.to_string(),
capabilities: vec![],
}
);
assert!(config.is_anonymous());
}
#[test]
fn invalid_name_missing() {
let err = UserConfig::builder().build().unwrap_err();
matches!(err, UserConfigError::InvalidName);
}
#[test]
fn invalid_partial_login_name() {
let err = UserConfig::builder()
.anonymous()
.name("foo")
.build()
.unwrap_err();
matches!(err, UserConfigError::PartialAnonymous);
}
#[test]
fn invalid_partial_login_token() {
let err = UserConfig::builder()
.anonymous()
.token(format!("oauth:{}", "a".repeat(30)))
.build()
.unwrap_err();
matches!(err, UserConfigError::PartialAnonymous);
}
#[test]
fn invalid_token_missing() {
let err = UserConfig::builder().name("foobar").build().unwrap_err();
matches!(err, UserConfigError::InvalidToken);
}
#[test]
fn invalid_token_empty() {
let err = UserConfig::builder()
.name("foobar")
.token("")
.build()
.unwrap_err();
matches!(err, UserConfigError::InvalidToken);
}
#[test]
fn invalid_token_short() {
let err = UserConfig::builder()
.name("foobar")
.token("foo")
.build()
.unwrap_err();
matches!(err, UserConfigError::InvalidToken);
}
#[test]
fn invalid_token_no_oauth() {
let err = UserConfig::builder()
.name("foobar")
.token("a".repeat(36))
.build()
.unwrap_err();
matches!(err, UserConfigError::InvalidToken);
}
}