use crate::errors::Result;
use crate::errors::{ParsePhoneNumberError, ParsePhoneNumberPrefixError};
use crate::locker::Locker;
use crate::support::Support;
use crate::symmetric_encryption::encrypt;
use crate::{with_status, EnableStatus, RuntimeErrorCode};
use perro::{ensure, MapToError};
use phonenumber::country::Id as CountryCode;
use phonenumber::metadata::DATABASE;
use phonenumber::ParseError;
use std::sync::Arc;
pub struct PhoneNumber {
support: Arc<Support>,
}
impl PhoneNumber {
pub(crate) fn new(support: Arc<Support>) -> Self {
Self { support }
}
pub fn get(&self) -> Result<Option<String>> {
Ok(self
.support
.data_store
.lock_unwrap()
.retrieve_lightning_addresses()?
.into_iter()
.filter_map(with_status(EnableStatus::Enabled))
.find(|a| a.starts_with('-'))
.and_then(|a| {
lightning_address_to_phone_number(
&a,
&self
.support
.node_config
.remote_services_config
.lipa_lightning_domain,
)
}))
}
pub fn register(&self, phone_number: String) -> Result<()> {
let phone_number = self
.parse_phone_number(phone_number)
.map_to_invalid_input("Invalid phone number")?;
let encrypted_number = encrypt(
phone_number.e164.as_bytes(),
&self.support.persistence_encryption_key,
)?;
let encrypted_number = hex::encode(encrypted_number);
self.support
.rt
.handle()
.block_on(pigeon::request_phone_number_verification(
&self.support.node_config.remote_services_config.backend_url,
&self.support.async_auth,
phone_number.e164,
encrypted_number,
))
.map_to_runtime_error(
RuntimeErrorCode::AuthServiceUnavailable,
"Failed to register phone number",
)
}
pub fn verify(&self, phone_number: String, otp: String) -> Result<()> {
let phone_number = self
.parse_phone_number(phone_number)
.map_to_invalid_input("Invalid phone number")?;
self.support
.rt
.handle()
.block_on(pigeon::verify_phone_number(
&self.support.node_config.remote_services_config.backend_url,
&self.support.async_auth,
phone_number.e164.clone(),
otp,
))
.map_to_runtime_error(
RuntimeErrorCode::AuthServiceUnavailable,
"Failed to submit phone number registration otp",
)?;
let address = phone_number.to_lightning_address(
&self
.support
.node_config
.remote_services_config
.lipa_lightning_domain,
);
self.support
.data_store
.lock_unwrap()
.store_lightning_address(&address)
}
pub fn parse_prefix(
&self,
phone_number_prefix: String,
) -> std::result::Result<(), ParsePhoneNumberPrefixError> {
self.support
.phone_number_prefix_parser
.parse(&phone_number_prefix)
}
pub fn parse_to_lightning_address(
&self,
phone_number: String,
) -> std::result::Result<String, ParsePhoneNumberError> {
let phone_number_recipient = self.parse_phone_number(phone_number)?;
Ok(phone_number_recipient.to_lightning_address(
&self
.support
.node_config
.remote_services_config
.lipa_lightning_domain,
))
}
fn parse_phone_number(
&self,
phone_number: String,
) -> std::result::Result<PhoneNumberRecipient, ParsePhoneNumberError> {
let phone_number_recipient = PhoneNumberRecipient::parse(&phone_number)?;
ensure!(
self.support
.allowed_countries_country_iso_3166_1_alpha_2
.contains(&phone_number_recipient.country_code.as_ref().to_string()),
ParsePhoneNumberError::UnsupportedCountry
);
Ok(phone_number_recipient)
}
}
#[derive(PartialEq, Debug)]
pub struct PhoneNumberRecipient {
pub e164: String,
pub country_code: CountryCode,
}
impl PhoneNumberRecipient {
pub(crate) fn parse(number: &str) -> std::result::Result<Self, ParsePhoneNumberError> {
let number = match phonenumber::parse(None, number) {
Ok(number) => number,
Err(ParseError::InvalidCountryCode) => {
return Err(ParsePhoneNumberError::MissingCountryCode)
}
Err(_) => return Err(ParsePhoneNumberError::ParsingError),
};
ensure!(number.is_valid(), ParsePhoneNumberError::InvalidPhoneNumber);
let e164 = number.format().mode(phonenumber::Mode::E164).to_string();
let country_code = number
.country()
.id()
.ok_or(ParsePhoneNumberError::InvalidCountryCode)?;
Ok(Self { e164, country_code })
}
pub(crate) fn to_lightning_address(&self, domain: &str) -> String {
self.e164.replacen('+', "-", 1) + domain
}
}
pub(crate) fn lightning_address_to_phone_number(address: &str, domain: &str) -> Option<String> {
let username = address
.strip_prefix('-')
.and_then(|s| s.strip_suffix(domain));
if let Some(username) = username {
if username.chars().all(|c| char::is_ascii_digit(&c)) {
return Some(format!("+{username}"));
}
}
None
}
#[derive(Clone)]
pub(crate) struct PhoneNumberPrefixParser {
allowed_country_codes: Vec<String>,
}
impl PhoneNumberPrefixParser {
pub fn new(allowed_countries_iso_3166_1_alpha_2: &[String]) -> Self {
let allowed_country_codes = allowed_countries_iso_3166_1_alpha_2
.iter()
.flat_map(|id| DATABASE.by_id(id))
.map(|m| m.country_code().to_string())
.collect::<Vec<_>>();
Self {
allowed_country_codes,
}
}
pub fn parse(&self, prefix: &str) -> std::result::Result<(), ParsePhoneNumberPrefixError> {
match parser::parse_phone_number(prefix) {
Ok(digits) => {
if self
.allowed_country_codes
.iter()
.any(|c| digits.starts_with(c))
{
Ok(())
} else if self
.allowed_country_codes
.iter()
.any(|c| c.starts_with(&digits))
{
Err(ParsePhoneNumberPrefixError::Incomplete)
} else {
Err(ParsePhoneNumberPrefixError::UnsupportedCountry)
}
}
Err(parser::ParseError::Incomplete) => Err(ParsePhoneNumberPrefixError::Incomplete),
Err(
parser::ParseError::UnexpectedCharacter(index)
| parser::ParseError::ExcessSuffix(index),
) => Err(ParsePhoneNumberPrefixError::InvalidCharacter { at: index as u32 }),
}
}
}
#[cfg(test)]
mod tests {
use super::*;
static LIPA_DOMAIN: &str = "@lipa.swiss";
#[test]
fn test_parse_phone_number_prefix() {
let ch = PhoneNumberPrefixParser::new(&["CH".to_string()]);
assert_eq!(ch.parse(""), Err(ParsePhoneNumberPrefixError::Incomplete));
assert_eq!(ch.parse("+"), Err(ParsePhoneNumberPrefixError::Incomplete));
assert_eq!(
ch.parse("+3"),
Err(ParsePhoneNumberPrefixError::UnsupportedCountry)
);
assert_eq!(ch.parse("+4"), Err(ParsePhoneNumberPrefixError::Incomplete));
assert_eq!(
ch.parse("+4 "),
Err(ParsePhoneNumberPrefixError::Incomplete)
);
assert_eq!(
ch.parse("+44"),
Err(ParsePhoneNumberPrefixError::UnsupportedCountry)
);
assert_eq!(ch.parse("+41"), Ok(()));
assert_eq!(ch.parse("+41 ("), Ok(()));
assert_eq!(ch.parse("+41 (935"), Ok(()));
assert_eq!(
ch.parse("+41a"),
Err(ParsePhoneNumberPrefixError::InvalidCharacter { at: 3 })
);
let us = PhoneNumberPrefixParser::new(&["US".to_string()]);
assert_eq!(us.parse("+"), Err(ParsePhoneNumberPrefixError::Incomplete));
assert_eq!(us.parse("+1"), Ok(()));
assert_eq!(us.parse("+12"), Ok(()));
let us_and_ch = PhoneNumberPrefixParser::new(&["US".to_string(), "CH".to_string()]);
assert_eq!(
us_and_ch.parse("+"),
Err(ParsePhoneNumberPrefixError::Incomplete)
);
assert_eq!(us_and_ch.parse("+1"), Ok(()));
assert_eq!(us_and_ch.parse("+12"), Ok(()));
assert_eq!(
us_and_ch.parse("+3"),
Err(ParsePhoneNumberPrefixError::UnsupportedCountry)
);
assert_eq!(
us_and_ch.parse("+4"),
Err(ParsePhoneNumberPrefixError::Incomplete)
);
assert_eq!(
us_and_ch.parse("+44"),
Err(ParsePhoneNumberPrefixError::UnsupportedCountry)
);
assert_eq!(us_and_ch.parse("+41"), Ok(()));
}
#[test]
fn test_parse_phone_number() {
let expected = PhoneNumberRecipient {
e164: "+41446681800".to_string(),
country_code: CountryCode::CH,
};
assert_eq!(
PhoneNumberRecipient::parse("+41 44 668 18 00").unwrap(),
expected
);
assert_eq!(
PhoneNumberRecipient::parse("tel:+41-44-668-18-00").unwrap(),
expected,
);
assert_eq!(
PhoneNumberRecipient::parse("+41446681800").unwrap(),
expected
);
assert_eq!(
PhoneNumberRecipient::parse("044 668 18 00").unwrap_err(),
ParsePhoneNumberError::MissingCountryCode
);
assert_eq!(
PhoneNumberRecipient::parse("446681800").unwrap_err(),
ParsePhoneNumberError::MissingCountryCode
);
assert_eq!(
PhoneNumberRecipient::parse("+41 44 668 18 0").unwrap_err(),
ParsePhoneNumberError::InvalidPhoneNumber
);
}
#[test]
fn test_to_from_lightning_address_e2e() {
let original = PhoneNumberRecipient::parse("+41 44 668 18 00").unwrap();
let address = original.to_lightning_address(LIPA_DOMAIN);
let e164 = lightning_address_to_phone_number(&address, LIPA_DOMAIN).unwrap();
let result = PhoneNumberRecipient::parse(&e164).unwrap();
assert_eq!(original.e164, result.e164);
}
#[test]
fn test_to_from_lightning_address() {
assert_eq!(
PhoneNumberRecipient::parse("+41 44 668 18 00")
.unwrap()
.to_lightning_address(LIPA_DOMAIN),
"-41446681800@lipa.swiss",
);
assert_eq!(
lightning_address_to_phone_number("-41446681800@lipa.swiss", LIPA_DOMAIN).unwrap(),
"+41446681800"
);
assert_eq!(
lightning_address_to_phone_number("41446681800@lipa.swiss", LIPA_DOMAIN),
None
);
assert_eq!(
lightning_address_to_phone_number("-41446681800@other.domain", LIPA_DOMAIN),
None
);
assert_eq!(
lightning_address_to_phone_number("-4144668aa1800@lipa.swiss", LIPA_DOMAIN),
None
);
}
}