use std::{
string::ToString,
time::{SystemTime, UNIX_EPOCH},
};
use anyhow::Result;
use diesel::prelude::*;
use gtk::{
gdk_pixbuf, gio,
glib::{self, clone},
prelude::*,
subclass::prelude::*,
};
use url::Url;
use crate::{
models::{database, Account, AccountsModel, Algorithm, Method, FAVICONS_PATH, OTP},
schema::providers,
};
pub struct ProviderPatch {
pub name: String,
pub website: Option<String>,
pub help_url: Option<String>,
pub image_uri: Option<String>,
pub period: i32,
pub digits: i32,
pub default_counter: i32,
pub algorithm: String,
pub method: String,
pub is_backup_restore: bool,
}
#[derive(Insertable)]
#[diesel(table_name = providers)]
struct NewProvider {
pub name: String,
pub website: Option<String>,
pub help_url: Option<String>,
pub image_uri: Option<String>,
pub period: i32,
pub digits: i32,
pub default_counter: i32,
pub algorithm: String,
pub method: String,
}
#[derive(Identifiable, Queryable)]
#[diesel(table_name = providers)]
pub struct DieselProvider {
pub id: i32,
pub name: String,
pub website: Option<String>,
pub help_url: Option<String>,
pub image_uri: Option<String>,
pub period: i32,
pub digits: i32,
pub default_counter: i32,
pub algorithm: String,
pub method: String,
}
mod imp {
use std::cell::{Cell, RefCell};
use super::*;
#[derive(glib::Properties)]
#[properties(wrapper_type = super::Provider)]
pub struct Provider {
#[property(get, set, construct_only)]
pub id: Cell<u32>,
#[property(get, set)]
pub name: RefCell<String>,
#[property(get, set, maximum = 1000, default = OTP::DEFAULT_PERIOD)]
pub period: Cell<u32>,
#[property(get, set, builder(Method::default()))]
pub method: Cell<Method>,
#[property(get, set, default = OTP::DEFAULT_COUNTER)]
pub default_counter: Cell<u32>,
#[property(get, set, builder(Algorithm::default()))]
pub algorithm: Cell<Algorithm>,
#[property(get, set, maximum = 1000, default = OTP::DEFAULT_DIGITS)]
pub digits: Cell<u32>,
#[property(get, set)]
pub website: RefCell<Option<String>>,
#[property(get, set)]
pub help_url: RefCell<Option<String>>,
#[property(get, set = Self::set_image_uri, explicit_notify)]
pub image_uri: RefCell<Option<String>>,
#[property(get, set)]
pub remaining_time: Cell<u64>,
#[property(get)]
pub accounts_model: AccountsModel,
pub filter_model: gtk::FilterListModel,
pub tick_callback: RefCell<Option<glib::SourceId>>,
}
#[glib::object_subclass]
impl ObjectSubclass for Provider {
const NAME: &'static str = "Provider";
type Type = super::Provider;
fn new() -> Self {
let model = AccountsModel::default();
Self {
id: Cell::default(),
default_counter: Cell::new(OTP::DEFAULT_COUNTER),
algorithm: Cell::new(Algorithm::default()),
digits: Cell::new(OTP::DEFAULT_DIGITS),
name: RefCell::default(),
website: RefCell::default(),
help_url: RefCell::default(),
image_uri: RefCell::default(),
method: Cell::new(Method::default()),
period: Cell::new(OTP::DEFAULT_PERIOD),
filter_model: gtk::FilterListModel::new(Some(model.clone()), None::<gtk::Filter>),
accounts_model: model,
tick_callback: RefCell::default(),
remaining_time: Cell::default(),
}
}
}
#[glib::derived_properties]
impl ObjectImpl for Provider {
fn dispose(&self) {
if let Some(source_id) = self.tick_callback.borrow_mut().take() {
source_id.remove();
}
}
}
impl Provider {
fn set_image_uri_inner(&self, id: i32, uri: Option<&str>) -> anyhow::Result<()> {
let db = database::connection();
let mut conn = db.get()?;
let target = providers::table.filter(providers::columns::id.eq(id));
diesel::update(target)
.set(providers::columns::image_uri.eq(uri))
.execute(&mut conn)?;
Ok(())
}
fn set_image_uri(&self, uri: Option<&str>) {
let obj = self.obj();
if let Err(err) = self.set_image_uri_inner(obj.id() as i32, uri) {
tracing::warn!("Failed to update provider image {}", err);
}
self.image_uri.replace(uri.map(ToOwned::to_owned));
obj.notify_image_uri();
}
}
}
glib::wrapper! {
pub struct Provider(ObjectSubclass<imp::Provider>);
}
impl Provider {
#[allow(clippy::too_many_arguments)]
pub fn create(
name: &str,
period: u32,
algorithm: Algorithm,
website: Option<String>,
method: Method,
digits: u32,
default_counter: u32,
help_url: Option<String>,
image_uri: Option<String>,
) -> Result<Self> {
let db = database::connection();
let mut conn = db.get()?;
diesel::insert_into(providers::table)
.values(NewProvider {
name: name.to_string(),
period: period as i32,
method: method.to_string(),
website,
algorithm: algorithm.to_string(),
digits: digits as i32,
default_counter: default_counter as i32,
help_url,
image_uri,
})
.execute(&mut conn)?;
providers::table
.order(providers::columns::id.desc())
.first::<DieselProvider>(&mut conn)
.map_err(From::from)
.map(From::from)
}
pub fn load() -> Result<impl Iterator<Item = Self>> {
use crate::schema::providers::dsl::*;
let db = database::connection();
let mut conn = db.get()?;
let results = providers
.load::<DieselProvider>(&mut conn)?
.into_iter()
.map(From::from)
.inspect(|p: &Provider| {
let accounts = Account::load(p).unwrap().collect::<Vec<_>>();
p.add_accounts(&accounts);
});
Ok(results)
}
#[allow(clippy::too_many_arguments)]
pub fn new(
id: u32,
name: &str,
period: u32,
method: Method,
algorithm: Algorithm,
digits: u32,
default_counter: u32,
website: Option<String>,
help_url: Option<String>,
image_uri: Option<String>,
) -> Provider {
glib::Object::builder()
.property("id", id)
.property("name", name)
.property("website", website)
.property("help-url", help_url)
.property("image-uri", image_uri)
.property("period", period)
.property("method", method)
.property("algorithm", algorithm)
.property("digits", digits)
.property("default-counter", default_counter)
.build()
}
pub async fn favicon(
website: String,
name: String,
id: u32,
) -> Result<String, Box<dyn std::error::Error>> {
let website_url = Url::parse(&website)?;
let favicon = favicon_scrapper::Scrapper::from_url(&website_url).await?;
tracing::debug!("Found the following icons {:#?} for {}", favicon, name);
let icon_name = format!("{id}_{}", name.replace(' ', "_"));
let icon_name = glib::base64_encode(icon_name.as_bytes());
let small_icon_name = format!("{icon_name}_32x32");
let large_icon_name = format!("{icon_name}_96x96");
if let Some(best_favicon) = favicon.find_best().await {
tracing::debug!("Largest favicon found is {:#?}", best_favicon);
let cache_path = FAVICONS_PATH.join(&*icon_name);
best_favicon.save(cache_path.clone()).await?;
if !best_favicon.metadata().format().is_svg() {
tracing::debug!("Creating scaled down variants for {:#?}", cache_path);
{
let pixbuf = gdk_pixbuf::Pixbuf::from_file(cache_path.clone())?;
tracing::debug!("Creating a 32x32 variant of the favicon");
let small_pixbuf = pixbuf
.scale_simple(32, 32, gdk_pixbuf::InterpType::Bilinear)
.unwrap();
let mut small_cache = cache_path.clone();
small_cache.set_file_name(small_icon_name);
small_pixbuf.savev(small_cache.clone(), "png", &[])?;
tracing::debug!("Creating a 96x96 variant of the favicon");
let large_pixbuf = pixbuf
.scale_simple(96, 96, gdk_pixbuf::InterpType::Bilinear)
.unwrap();
let mut large_cache = cache_path.clone();
large_cache.set_file_name(large_icon_name);
large_pixbuf.savev(large_cache.clone(), "png", &[])?;
};
tokio::fs::remove_file(cache_path).await?;
} else {
let mut small_cache = cache_path.clone();
small_cache.set_file_name(small_icon_name);
tokio::fs::symlink(&cache_path, small_cache).await?;
let mut large_cache = cache_path.clone();
large_cache.set_file_name(large_icon_name);
tokio::fs::symlink(&cache_path, large_cache).await?;
}
Ok(icon_name.to_string())
} else {
Err(Box::new(favicon_scrapper::Error::NoResults))
}
}
pub fn delete(&self) -> Result<()> {
let db = database::connection();
let mut conn = db.get()?;
diesel::delete(providers::table.filter(providers::columns::id.eq(self.id() as i32)))
.execute(&mut conn)?;
Ok(())
}
pub fn update(&self, patch: &ProviderPatch) -> Result<()> {
if patch.name == self.name()
&& patch.website == self.website()
&& patch.help_url == self.help_url()
&& patch.image_uri == self.image_uri()
&& patch.period == self.period() as i32
&& patch.digits == self.digits() as i32
&& patch.default_counter == self.default_counter() as i32
&& patch.algorithm == self.algorithm().to_string()
&& patch.method == self.method().to_string()
{
return Ok(());
}
let db = database::connection();
let mut conn = db.get()?;
let target = providers::table.filter(providers::columns::id.eq(self.id() as i32));
diesel::update(target)
.set((
providers::columns::algorithm.eq(&patch.algorithm),
providers::columns::method.eq(&patch.method),
providers::columns::digits.eq(&patch.digits),
providers::columns::period.eq(&patch.period),
providers::columns::default_counter.eq(&patch.default_counter),
providers::columns::name.eq(&patch.name),
))
.execute(&mut conn)?;
if !patch.is_backup_restore {
diesel::update(target)
.set((
providers::columns::image_uri.eq(&patch.image_uri),
providers::columns::website.eq(&patch.website),
providers::columns::help_url.eq(&patch.help_url),
))
.execute(&mut conn)?;
};
self.set_properties(&[
("name", &patch.name),
("period", &(patch.period as u32)),
("method", &patch.method.parse::<Method>()?),
("digits", &(patch.digits as u32)),
("algorithm", &patch.algorithm.parse::<Algorithm>()?),
("default-counter", &(patch.default_counter as u32)),
]);
if !patch.is_backup_restore {
self.set_properties(&[
("image-uri", &patch.image_uri),
("website", &patch.website),
("help-url", &patch.help_url),
]);
}
Ok(())
}
pub fn open_help(&self) {
if let Some(ref url) = self.help_url() {
gio::AppInfo::launch_default_for_uri(url, None::<&gio::AppLaunchContext>).unwrap();
}
}
fn tick(&self) {
let period = self.period() as u64;
let remaining_time: u64 = period
- SystemTime::now()
.duration_since(UNIX_EPOCH)
.unwrap()
.as_secs()
% period;
if period == remaining_time {
self.regenerate_otp();
}
self.set_remaining_time(remaining_time);
}
fn setup_tick_callback(&self) {
if self.imp().tick_callback.borrow().is_some() || self.method().is_event_based() {
return;
}
self.set_remaining_time(self.period() as u64);
match self.method() {
Method::TOTP | Method::Steam => {
let source_id = glib::timeout_add_seconds_local(
1,
clone!(
#[weak(rename_to = provider)]
self,
#[upgrade_or]
glib::ControlFlow::Break,
move || {
provider.tick();
glib::ControlFlow::Continue
}
),
);
self.imp().tick_callback.replace(Some(source_id));
}
_ => (),
};
}
fn regenerate_otp(&self) {
let accounts = self.accounts();
for i in 0..accounts.n_items() {
let item = accounts.item(i).unwrap();
let account = item.downcast_ref::<Account>().unwrap();
account.generate_otp();
}
}
pub fn has_accounts(&self) -> bool {
self.accounts_model().n_items() != 0
}
fn add_accounts(&self, accounts: &[Account]) {
self.accounts_model().splice(accounts);
self.setup_tick_callback();
}
pub fn add_account(&self, account: &Account) {
self.accounts_model().append(account);
self.setup_tick_callback();
}
fn tokenize_search(account_name: &str, provider_name: &str, term: &str) -> bool {
let term = term.to_ascii_lowercase();
let provider_name = provider_name.to_ascii_lowercase();
let account_name = account_name.to_ascii_lowercase();
account_name.split_ascii_whitespace().any(|x| x == term)
|| provider_name.split_ascii_whitespace().any(|x| x == term)
|| account_name.contains(term.as_str())
|| provider_name.contains(term.as_str())
}
pub fn find_accounts(&self, terms: &[String]) -> Vec<Account> {
let mut results = vec![];
let model = self.accounts_model();
let provider_name = self.name();
for pos in 0..model.n_items() {
let account = model.item(pos).and_downcast::<Account>().unwrap();
let account_name = account.name();
if terms
.iter()
.any(|term| Self::tokenize_search(&account_name, &provider_name, term))
{
results.push(account);
}
}
results
}
pub fn accounts(&self) -> >k::FilterListModel {
&self.imp().filter_model
}
pub fn filter(&self, text: String) {
let filter = gtk::CustomFilter::new(glib::clone!(
#[weak(rename_to = provider)]
self,
#[upgrade_or]
false,
move |obj| {
let account = obj.downcast_ref::<Account>().unwrap();
let account_name = account.name();
let provider_name = provider.name();
Self::tokenize_search(&account_name, &provider_name, &text)
}
));
self.imp().filter_model.set_filter(Some(&filter));
}
pub fn remove_account(&self, account: &Account) {
let imp = self.imp();
let model = self.accounts_model();
if let Some(pos) = model.find_position_by_id(account.id()) {
model.remove(pos);
if !self.has_accounts() && self.method().is_time_based() {
if let Some(source_id) = imp.tick_callback.borrow_mut().take() {
source_id.remove();
}
}
}
}
}
impl From<DieselProvider> for Provider {
fn from(p: DieselProvider) -> Self {
Self::new(
p.id as u32,
&p.name,
p.period as u32,
p.method.parse::<Method>().unwrap(),
p.algorithm.parse::<Algorithm>().unwrap(),
p.digits as u32,
p.default_counter as u32,
p.website,
p.help_url,
p.image_uri,
)
}
}
impl From<&Provider> for DieselProvider {
fn from(p: &Provider) -> Self {
Self {
id: p.id() as i32,
name: p.name(),
period: p.period() as i32,
method: p.method().to_string(),
algorithm: p.algorithm().to_string(),
digits: p.digits() as i32,
default_counter: p.default_counter() as i32,
website: p.website(),
help_url: p.help_url(),
image_uri: p.image_uri(),
}
}
}