Move postman and web inside src
							
								
								
									
										896
									
								
								Cargo.lock
									
									
									
										generated
									
									
									
								
							
							
						
						
							
								
								
									
										15
									
								
								Cargo.toml
									
									
									
									
									
								
							
							
						
						@@ -10,7 +10,7 @@ description = "Reminder Bot for Discord, now in Rust"
 | 
				
			|||||||
poise = "0.6.1"
 | 
					poise = "0.6.1"
 | 
				
			||||||
dotenv = "0.15"
 | 
					dotenv = "0.15"
 | 
				
			||||||
tokio = { version = "1", features = ["process", "full"] }
 | 
					tokio = { version = "1", features = ["process", "full"] }
 | 
				
			||||||
reqwest = "0.11"
 | 
					reqwest = { version = "0.12", features = ["json"] }
 | 
				
			||||||
lazy-regex = "3.1"
 | 
					lazy-regex = "3.1"
 | 
				
			||||||
regex = "1.10"
 | 
					regex = "1.10"
 | 
				
			||||||
log = "0.4"
 | 
					log = "0.4"
 | 
				
			||||||
@@ -29,12 +29,13 @@ sqlx = { version = "0.7", features = ["runtime-tokio-rustls", "macros", "mysql",
 | 
				
			|||||||
base64 = "0.21"
 | 
					base64 = "0.21"
 | 
				
			||||||
secrecy = "0.8.0"
 | 
					secrecy = "0.8.0"
 | 
				
			||||||
futures = "0.3.30"
 | 
					futures = "0.3.30"
 | 
				
			||||||
 | 
					prometheus = "0.13.3"
 | 
				
			||||||
[dependencies.postman]
 | 
					rocket = { version = "0.5.0", features = ["tls", "secrets", "json"] }
 | 
				
			||||||
path = "postman"
 | 
					rocket_dyn_templates = { version = "0.1.0", features = ["tera"] }
 | 
				
			||||||
 | 
					serenity = { version = "0.12", default-features = false, features = ["builder", "cache", "client", "gateway", "http", "model", "utils", "rustls_backend"] }
 | 
				
			||||||
[dependencies.reminder_web]
 | 
					oauth2 = "4"
 | 
				
			||||||
path = "web"
 | 
					csv = "1.2"
 | 
				
			||||||
 | 
					axum = "0.7"
 | 
				
			||||||
 | 
					
 | 
				
			||||||
[dependencies.extract_derive]
 | 
					[dependencies.extract_derive]
 | 
				
			||||||
path = "extract_derive"
 | 
					path = "extract_derive"
 | 
				
			||||||
 
 | 
				
			|||||||
							
								
								
									
										22
									
								
								Rocket.toml
									
									
									
									
									
								
							
							
						
						@@ -1,28 +1,28 @@
 | 
				
			|||||||
[default]
 | 
					[default]
 | 
				
			||||||
address = "0.0.0.0"
 | 
					address = "0.0.0.0"
 | 
				
			||||||
port = 18920
 | 
					port = 18920
 | 
				
			||||||
template_dir = "web/templates"
 | 
					template_dir = "templates"
 | 
				
			||||||
limits = { json = "10MiB" }
 | 
					limits = { json = "10MiB" }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
[debug]
 | 
					[debug]
 | 
				
			||||||
secret_key = "tR8krio5FXTnnyIZNiJDXPondz0kI1v6X6BXZcBGIRY="
 | 
					secret_key = "tR8krio5FXTnnyIZNiJDXPondz0kI1v6X6BXZcBGIRY="
 | 
				
			||||||
 | 
					
 | 
				
			||||||
[debug.tls]
 | 
					[debug.tls]
 | 
				
			||||||
certs = "web/private/rsa_sha256_cert.pem"
 | 
					certs = "private/rsa_sha256_cert.pem"
 | 
				
			||||||
key = "web/private/rsa_sha256_key.pem"
 | 
					key = "private/rsa_sha256_key.pem"
 | 
				
			||||||
 | 
					
 | 
				
			||||||
[debug.rsa_sha256.tls]
 | 
					[debug.rsa_sha256.tls]
 | 
				
			||||||
certs = "web/private/rsa_sha256_cert.pem"
 | 
					certs = "private/rsa_sha256_cert.pem"
 | 
				
			||||||
key = "web/private/rsa_sha256_key.pem"
 | 
					key = "private/rsa_sha256_key.pem"
 | 
				
			||||||
 | 
					
 | 
				
			||||||
[debug.ecdsa_nistp256_sha256.tls]
 | 
					[debug.ecdsa_nistp256_sha256.tls]
 | 
				
			||||||
certs = "web/private/ecdsa_nistp256_sha256_cert.pem"
 | 
					certs = "private/ecdsa_nistp256_sha256_cert.pem"
 | 
				
			||||||
key = "web/private/ecdsa_nistp256_sha256_key_pkcs8.pem"
 | 
					key = "private/ecdsa_nistp256_sha256_key_pkcs8.pem"
 | 
				
			||||||
 | 
					
 | 
				
			||||||
[debug.ecdsa_nistp384_sha384.tls]
 | 
					[debug.ecdsa_nistp384_sha384.tls]
 | 
				
			||||||
certs = "web/private/ecdsa_nistp384_sha384_cert.pem"
 | 
					certs = "private/ecdsa_nistp384_sha384_cert.pem"
 | 
				
			||||||
key = "web/private/ecdsa_nistp384_sha384_key_pkcs8.pem"
 | 
					key = "private/ecdsa_nistp384_sha384_key_pkcs8.pem"
 | 
				
			||||||
 | 
					
 | 
				
			||||||
[debug.ed25519.tls]
 | 
					[debug.ed25519.tls]
 | 
				
			||||||
certs = "web/private/ed25519_cert.pem"
 | 
					certs = "private/ed25519_cert.pem"
 | 
				
			||||||
key = "eb/private/ed25519_key.pem"
 | 
					key = "private/ed25519_key.pem"
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -1,16 +0,0 @@
 | 
				
			|||||||
[package]
 | 
					 | 
				
			||||||
name = "postman"
 | 
					 | 
				
			||||||
version = "0.1.0"
 | 
					 | 
				
			||||||
edition = "2021"
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
[dependencies]
 | 
					 | 
				
			||||||
tokio = { version = "1", features = ["process", "full"] }
 | 
					 | 
				
			||||||
regex = "1.9"
 | 
					 | 
				
			||||||
log = "0.4"
 | 
					 | 
				
			||||||
chrono = "0.4"
 | 
					 | 
				
			||||||
chrono-tz = { version = "0.8", features = ["serde"] }
 | 
					 | 
				
			||||||
lazy_static = "1.4"
 | 
					 | 
				
			||||||
num-integer = "0.1"
 | 
					 | 
				
			||||||
serde = "1.0"
 | 
					 | 
				
			||||||
sqlx = { version = "0.7", features = ["runtime-tokio-rustls", "macros", "mysql", "bigdecimal", "chrono", "json"]}
 | 
					 | 
				
			||||||
serenity = { version = "0.12", default-features = false, features = ["builder", "cache", "client", "gateway", "http", "model", "utils", "rustls_backend"] }
 | 
					 | 
				
			||||||
@@ -1,4 +1,4 @@
 | 
				
			|||||||
use chrono::NaiveDateTime;
 | 
					use chrono::DateTime;
 | 
				
			||||||
use serde::{Deserialize, Serialize};
 | 
					use serde::{Deserialize, Serialize};
 | 
				
			||||||
 | 
					
 | 
				
			||||||
use crate::{
 | 
					use crate::{
 | 
				
			||||||
@@ -24,10 +24,10 @@ impl Recordable for Options {
 | 
				
			|||||||
                let parsed = natural_parser(&until, &timezone.to_string()).await;
 | 
					                let parsed = natural_parser(&until, &timezone.to_string()).await;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
                if let Some(timestamp) = parsed {
 | 
					                if let Some(timestamp) = parsed {
 | 
				
			||||||
                    match NaiveDateTime::from_timestamp_opt(timestamp, 0) {
 | 
					                    match DateTime::from_timestamp(timestamp, 0) {
 | 
				
			||||||
                        Some(dt) => {
 | 
					                        Some(dt) => {
 | 
				
			||||||
                            channel.paused = true;
 | 
					                            channel.paused = true;
 | 
				
			||||||
                            channel.paused_until = Some(dt);
 | 
					                            channel.paused_until = Some(dt.naive_utc());
 | 
				
			||||||
 | 
					
 | 
				
			||||||
                            channel.commit_changes(&ctx.data().database).await;
 | 
					                            channel.commit_changes(&ctx.data().database).await;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 
 | 
				
			|||||||
							
								
								
									
										14
									
								
								src/main.rs
									
									
									
									
									
								
							
							
						
						@@ -12,12 +12,15 @@ mod event_handlers;
 | 
				
			|||||||
#[cfg(not(test))]
 | 
					#[cfg(not(test))]
 | 
				
			||||||
mod hooks;
 | 
					mod hooks;
 | 
				
			||||||
mod interval_parser;
 | 
					mod interval_parser;
 | 
				
			||||||
 | 
					mod metrics;
 | 
				
			||||||
#[cfg(not(test))]
 | 
					#[cfg(not(test))]
 | 
				
			||||||
mod models;
 | 
					mod models;
 | 
				
			||||||
 | 
					mod postman;
 | 
				
			||||||
#[cfg(test)]
 | 
					#[cfg(test)]
 | 
				
			||||||
mod test;
 | 
					mod test;
 | 
				
			||||||
mod time_parser;
 | 
					mod time_parser;
 | 
				
			||||||
mod utils;
 | 
					mod utils;
 | 
				
			||||||
 | 
					mod web;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
use std::{
 | 
					use std::{
 | 
				
			||||||
    collections::HashMap,
 | 
					    collections::HashMap,
 | 
				
			||||||
@@ -28,7 +31,7 @@ use std::{
 | 
				
			|||||||
};
 | 
					};
 | 
				
			||||||
 | 
					
 | 
				
			||||||
use chrono_tz::Tz;
 | 
					use chrono_tz::Tz;
 | 
				
			||||||
use log::{error, warn};
 | 
					use log::warn;
 | 
				
			||||||
use poise::serenity_prelude::{
 | 
					use poise::serenity_prelude::{
 | 
				
			||||||
    model::{
 | 
					    model::{
 | 
				
			||||||
        gateway::GatewayIntents,
 | 
					        gateway::GatewayIntents,
 | 
				
			||||||
@@ -39,6 +42,7 @@ use poise::serenity_prelude::{
 | 
				
			|||||||
use sqlx::{MySql, Pool};
 | 
					use sqlx::{MySql, Pool};
 | 
				
			||||||
use tokio::sync::{broadcast, broadcast::Sender, RwLock};
 | 
					use tokio::sync::{broadcast, broadcast::Sender, RwLock};
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					use crate::metrics::init_metrics;
 | 
				
			||||||
#[cfg(test)]
 | 
					#[cfg(test)]
 | 
				
			||||||
use crate::test::TestContext;
 | 
					use crate::test::TestContext;
 | 
				
			||||||
#[cfg(not(test))]
 | 
					#[cfg(not(test))]
 | 
				
			||||||
@@ -206,6 +210,10 @@ async fn _main(tx: Sender<()>) -> Result<(), Box<dyn StdError + Send + Sync>> {
 | 
				
			|||||||
        ..Default::default()
 | 
					        ..Default::default()
 | 
				
			||||||
    };
 | 
					    };
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    // Start metrics
 | 
				
			||||||
 | 
					    init_metrics();
 | 
				
			||||||
 | 
					    tokio::spawn(async { metrics::serve().await });
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    let database =
 | 
					    let database =
 | 
				
			||||||
        Pool::connect(&env::var("DATABASE_URL").expect("No database URL provided")).await.unwrap();
 | 
					        Pool::connect(&env::var("DATABASE_URL").expect("No database URL provided")).await.unwrap();
 | 
				
			||||||
 | 
					
 | 
				
			||||||
@@ -249,7 +257,7 @@ async fn _main(tx: Sender<()>) -> Result<(), Box<dyn StdError + Send + Sync>> {
 | 
				
			|||||||
                        match postman::initialize(kill_recv, ctx1, &pool1).await {
 | 
					                        match postman::initialize(kill_recv, ctx1, &pool1).await {
 | 
				
			||||||
                            Ok(_) => {}
 | 
					                            Ok(_) => {}
 | 
				
			||||||
                            Err(e) => {
 | 
					                            Err(e) => {
 | 
				
			||||||
                                error!("postman exiting: {}", e);
 | 
					                                panic!("postman exiting: {}", e);
 | 
				
			||||||
                            }
 | 
					                            }
 | 
				
			||||||
                        };
 | 
					                        };
 | 
				
			||||||
                    });
 | 
					                    });
 | 
				
			||||||
@@ -259,7 +267,7 @@ async fn _main(tx: Sender<()>) -> Result<(), Box<dyn StdError + Send + Sync>> {
 | 
				
			|||||||
 | 
					
 | 
				
			||||||
                if !run_settings.contains("web") {
 | 
					                if !run_settings.contains("web") {
 | 
				
			||||||
                    tokio::spawn(async move {
 | 
					                    tokio::spawn(async move {
 | 
				
			||||||
                        reminder_web::initialize(kill_tx, ctx2, pool2).await.unwrap();
 | 
					                        web::initialize(kill_tx, ctx2, pool2).await.unwrap();
 | 
				
			||||||
                    });
 | 
					                    });
 | 
				
			||||||
                } else {
 | 
					                } else {
 | 
				
			||||||
                    warn!("Not running web");
 | 
					                    warn!("Not running web");
 | 
				
			||||||
 
 | 
				
			|||||||
							
								
								
									
										36
									
								
								src/metrics.rs
									
									
									
									
									
										Normal file
									
								
							
							
						
						@@ -0,0 +1,36 @@
 | 
				
			|||||||
 | 
					use axum::{routing::get, Router};
 | 
				
			||||||
 | 
					use lazy_static::lazy_static;
 | 
				
			||||||
 | 
					use log::warn;
 | 
				
			||||||
 | 
					use prometheus::{IntCounterVec, Opts, Registry};
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					lazy_static! {
 | 
				
			||||||
 | 
					    pub static ref REGISTRY: Registry = Registry::new();
 | 
				
			||||||
 | 
					    pub static ref REQUEST_COUNTER: IntCounterVec =
 | 
				
			||||||
 | 
					        IntCounterVec::new(Opts::new("requests", "Requests"), &["method", "status", "route"])
 | 
				
			||||||
 | 
					            .unwrap();
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					pub fn init_metrics() {
 | 
				
			||||||
 | 
					    REGISTRY.register(Box::new(REQUEST_COUNTER.clone())).unwrap();
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					pub async fn serve() {
 | 
				
			||||||
 | 
					    let app = Router::new().route("/metrics", get(metrics));
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    let listener = tokio::net::TcpListener::bind("localhost:31756").await.unwrap();
 | 
				
			||||||
 | 
					    axum::serve(listener, app).await.unwrap();
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					async fn metrics() -> String {
 | 
				
			||||||
 | 
					    let encoder = prometheus::TextEncoder::new();
 | 
				
			||||||
 | 
					    let res_custom = encoder.encode_to_string(®ISTRY.gather());
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    match res_custom {
 | 
				
			||||||
 | 
					        Ok(s) => s,
 | 
				
			||||||
 | 
					        Err(e) => {
 | 
				
			||||||
 | 
					            warn!("Error encoding metrics: {:?}", e);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					            String::new()
 | 
				
			||||||
 | 
					        }
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
@@ -1,6 +1,6 @@
 | 
				
			|||||||
use std::collections::HashSet;
 | 
					use std::collections::HashSet;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
use chrono::{Duration, NaiveDateTime, Utc};
 | 
					use chrono::{DateTime, NaiveDateTime, TimeDelta, Utc};
 | 
				
			||||||
use chrono_tz::Tz;
 | 
					use chrono_tz::Tz;
 | 
				
			||||||
use poise::serenity_prelude::{
 | 
					use poise::serenity_prelude::{
 | 
				
			||||||
    model::id::{ChannelId, GuildId, UserId},
 | 
					    model::id::{ChannelId, GuildId, UserId},
 | 
				
			||||||
@@ -73,7 +73,7 @@ impl ReminderBuilder {
 | 
				
			|||||||
 | 
					
 | 
				
			||||||
        match queried_time.utc_time {
 | 
					        match queried_time.utc_time {
 | 
				
			||||||
            Some(utc_time) => {
 | 
					            Some(utc_time) => {
 | 
				
			||||||
                if utc_time < (Utc::now() - Duration::seconds(60)).naive_local() {
 | 
					                if utc_time < (Utc::now() - TimeDelta::try_minutes(1).unwrap()).naive_local() {
 | 
				
			||||||
                    Err(ReminderError::PastTime)
 | 
					                    Err(ReminderError::PastTime)
 | 
				
			||||||
                } else {
 | 
					                } else {
 | 
				
			||||||
                    sqlx::query!(
 | 
					                    sqlx::query!(
 | 
				
			||||||
@@ -165,7 +165,7 @@ impl<'a> MultiReminderBuilder<'a> {
 | 
				
			|||||||
    }
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    pub fn time<T: Into<i64>>(mut self, time: T) -> Self {
 | 
					    pub fn time<T: Into<i64>>(mut self, time: T) -> Self {
 | 
				
			||||||
        if let Some(utc_time) = NaiveDateTime::from_timestamp_opt(time.into(), 0) {
 | 
					        if let Some(utc_time) = DateTime::from_timestamp(time.into(), 0).map(|d| d.naive_utc()) {
 | 
				
			||||||
            self.utc_time = utc_time;
 | 
					            self.utc_time = utc_time;
 | 
				
			||||||
        }
 | 
					        }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
@@ -173,7 +173,8 @@ impl<'a> MultiReminderBuilder<'a> {
 | 
				
			|||||||
    }
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    pub fn expires<T: Into<i64>>(mut self, time: Option<T>) -> Self {
 | 
					    pub fn expires<T: Into<i64>>(mut self, time: Option<T>) -> Self {
 | 
				
			||||||
        self.expires = time.map(|t| NaiveDateTime::from_timestamp_opt(t.into(), 0)).flatten();
 | 
					        self.expires =
 | 
				
			||||||
 | 
					            time.map(|t| DateTime::from_timestamp(t.into(), 0)).flatten().map(|d| d.naive_utc());
 | 
				
			||||||
 | 
					
 | 
				
			||||||
        self
 | 
					        self
 | 
				
			||||||
    }
 | 
					    }
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -38,6 +38,7 @@ use crate::{
 | 
				
			|||||||
};
 | 
					};
 | 
				
			||||||
 | 
					
 | 
				
			||||||
#[derive(Debug, Clone)]
 | 
					#[derive(Debug, Clone)]
 | 
				
			||||||
 | 
					#[allow(dead_code)]
 | 
				
			||||||
pub struct Reminder {
 | 
					pub struct Reminder {
 | 
				
			||||||
    pub id: u32,
 | 
					    pub id: u32,
 | 
				
			||||||
    pub uid: String,
 | 
					    pub uid: String,
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -4,6 +4,7 @@ use sqlx::MySqlPool;
 | 
				
			|||||||
pub struct Timer {
 | 
					pub struct Timer {
 | 
				
			||||||
    pub name: String,
 | 
					    pub name: String,
 | 
				
			||||||
    pub start_time: DateTime<Utc>,
 | 
					    pub start_time: DateTime<Utc>,
 | 
				
			||||||
 | 
					    #[allow(dead_code)]
 | 
				
			||||||
    pub owner: u64,
 | 
					    pub owner: u64,
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -7,6 +7,7 @@ use crate::consts::LOCAL_TIMEZONE;
 | 
				
			|||||||
 | 
					
 | 
				
			||||||
pub struct UserData {
 | 
					pub struct UserData {
 | 
				
			||||||
    pub id: u32,
 | 
					    pub id: u32,
 | 
				
			||||||
 | 
					    #[allow(dead_code)]
 | 
				
			||||||
    pub user: u64,
 | 
					    pub user: u64,
 | 
				
			||||||
    pub dm_channel: u32,
 | 
					    pub dm_channel: u32,
 | 
				
			||||||
    pub timezone: String,
 | 
					    pub timezone: String,
 | 
				
			||||||
@@ -22,7 +23,7 @@ impl UserData {
 | 
				
			|||||||
 | 
					
 | 
				
			||||||
        match sqlx::query!(
 | 
					        match sqlx::query!(
 | 
				
			||||||
            "
 | 
					            "
 | 
				
			||||||
SELECT IFNULL(timezone, 'UTC') AS timezone FROM users WHERE user = ?
 | 
					            SELECT IFNULL(timezone, 'UTC') AS timezone FROM users WHERE user = ?
 | 
				
			||||||
            ",
 | 
					            ",
 | 
				
			||||||
            user_id
 | 
					            user_id
 | 
				
			||||||
        )
 | 
					        )
 | 
				
			||||||
 
 | 
				
			|||||||
							
								
								
									
										40
									
								
								src/postman/metrics.rs
									
									
									
									
									
										Normal file
									
								
							
							
						
						@@ -0,0 +1,40 @@
 | 
				
			|||||||
 | 
					use axum::{routing::get, Router};
 | 
				
			||||||
 | 
					use lazy_static;
 | 
				
			||||||
 | 
					use log::warn;
 | 
				
			||||||
 | 
					use prometheus::{register_int_counter, IntCounter, Registry};
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					lazy_static! {
 | 
				
			||||||
 | 
					    static ref REGISTRY: Registry = Registry::new();
 | 
				
			||||||
 | 
					    pub static ref REMINDERS_SENT: IntCounter =
 | 
				
			||||||
 | 
					        register_int_counter!("reminders_sent", "Number of reminders sent").unwrap();
 | 
				
			||||||
 | 
					    pub static ref REMINDERS_FAILED: IntCounter =
 | 
				
			||||||
 | 
					        register_int_counter!("reminders_failed", "Number of reminders failed").unwrap();
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					pub fn init_metrics() {
 | 
				
			||||||
 | 
					    REGISTRY.register(Box::new(REMINDERS_SENT.clone())).unwrap();
 | 
				
			||||||
 | 
					    REGISTRY.register(Box::new(REMINDERS_FAILED.clone())).unwrap();
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					pub async fn serve() {
 | 
				
			||||||
 | 
					    let app = Router::new().route("/metrics", get(metrics));
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    let metrics_port = std::env("PROMETHEUS_PORT").unwrap();
 | 
				
			||||||
 | 
					    let listener =
 | 
				
			||||||
 | 
					        tokio::net::TcpListener::bind(format!("localhost:{}", metrics_port)).await.unwrap();
 | 
				
			||||||
 | 
					    axum::serve(listener, app).await.unwrap();
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					async fn metrics() -> String {
 | 
				
			||||||
 | 
					    let encoder = prometheus::TextEncoder::new();
 | 
				
			||||||
 | 
					    let res_custom = encoder.encode_to_string(®ISTRY.gather());
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    match res_custom {
 | 
				
			||||||
 | 
					        Ok(s) => s,
 | 
				
			||||||
 | 
					        Err(e) => {
 | 
				
			||||||
 | 
					            warn!("Error encoding metrics: {:?}", e);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					            String::new()
 | 
				
			||||||
 | 
					        }
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
@@ -3,7 +3,7 @@ mod sender;
 | 
				
			|||||||
use std::env;
 | 
					use std::env;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
use log::{info, warn};
 | 
					use log::{info, warn};
 | 
				
			||||||
use serenity::client::Context;
 | 
					use poise::serenity_prelude::client::Context;
 | 
				
			||||||
use sqlx::{Executor, MySql};
 | 
					use sqlx::{Executor, MySql};
 | 
				
			||||||
use tokio::{
 | 
					use tokio::{
 | 
				
			||||||
    sync::broadcast::Receiver,
 | 
					    sync::broadcast::Receiver,
 | 
				
			||||||
@@ -1,13 +1,11 @@
 | 
				
			|||||||
use std::env;
 | 
					use std::env;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
use chrono::{DateTime, Days, Duration, Months};
 | 
					use chrono::{DateTime, Days, Months, TimeDelta};
 | 
				
			||||||
use chrono_tz::Tz;
 | 
					use chrono_tz::Tz;
 | 
				
			||||||
use lazy_static::lazy_static;
 | 
					use lazy_static::lazy_static;
 | 
				
			||||||
use log::{error, info, warn};
 | 
					use log::{error, info, warn};
 | 
				
			||||||
use num_integer::Integer;
 | 
					use num_integer::Integer;
 | 
				
			||||||
use regex::{Captures, Regex};
 | 
					use poise::serenity_prelude::{
 | 
				
			||||||
use serde::Deserialize;
 | 
					 | 
				
			||||||
use serenity::{
 | 
					 | 
				
			||||||
    all::{CreateAttachment, CreateEmbedFooter},
 | 
					    all::{CreateAttachment, CreateEmbedFooter},
 | 
				
			||||||
    builder::{CreateEmbed, CreateEmbedAuthor, CreateMessage, ExecuteWebhook},
 | 
					    builder::{CreateEmbed, CreateEmbedAuthor, CreateMessage, ExecuteWebhook},
 | 
				
			||||||
    http::{CacheHttp, Http, HttpError},
 | 
					    http::{CacheHttp, Http, HttpError},
 | 
				
			||||||
@@ -18,6 +16,8 @@ use serenity::{
 | 
				
			|||||||
    },
 | 
					    },
 | 
				
			||||||
    Error, Result,
 | 
					    Error, Result,
 | 
				
			||||||
};
 | 
					};
 | 
				
			||||||
 | 
					use regex::{Captures, Regex};
 | 
				
			||||||
 | 
					use serde::Deserialize;
 | 
				
			||||||
use sqlx::{
 | 
					use sqlx::{
 | 
				
			||||||
    types::{
 | 
					    types::{
 | 
				
			||||||
        chrono::{NaiveDateTime, Utc},
 | 
					        chrono::{NaiveDateTime, Utc},
 | 
				
			||||||
@@ -66,15 +66,15 @@ pub fn substitute(string: &str) -> String {
 | 
				
			|||||||
        let format = caps.name("format").map(|m| m.as_str());
 | 
					        let format = caps.name("format").map(|m| m.as_str());
 | 
				
			||||||
 | 
					
 | 
				
			||||||
        if let (Some(final_time), Some(format)) = (final_time, format) {
 | 
					        if let (Some(final_time), Some(format)) = (final_time, format) {
 | 
				
			||||||
            match NaiveDateTime::from_timestamp_opt(final_time, 0) {
 | 
					            match DateTime::from_timestamp(final_time, 0) {
 | 
				
			||||||
                Some(dt) => {
 | 
					                Some(dt) => {
 | 
				
			||||||
                    let now = Utc::now().naive_utc();
 | 
					                    let now = Utc::now();
 | 
				
			||||||
 | 
					
 | 
				
			||||||
                    let difference = {
 | 
					                    let difference = {
 | 
				
			||||||
                        if now < dt {
 | 
					                        if now < dt {
 | 
				
			||||||
                            dt - Utc::now().naive_utc()
 | 
					                            dt - Utc::now()
 | 
				
			||||||
                        } else {
 | 
					                        } else {
 | 
				
			||||||
                            Utc::now().naive_utc() - dt
 | 
					                            Utc::now() - dt
 | 
				
			||||||
                        }
 | 
					                        }
 | 
				
			||||||
                    };
 | 
					                    };
 | 
				
			||||||
 | 
					
 | 
				
			||||||
@@ -397,7 +397,13 @@ impl Reminder {
 | 
				
			|||||||
                }
 | 
					                }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
                if let Some(interval) = self.interval_seconds {
 | 
					                if let Some(interval) = self.interval_seconds {
 | 
				
			||||||
                    updated_reminder_time += Duration::seconds(interval as i64);
 | 
					                    updated_reminder_time += TimeDelta::try_seconds(interval as i64)
 | 
				
			||||||
 | 
					                        .unwrap_or_else(|| {
 | 
				
			||||||
 | 
					                            warn!("{}: Could not add {} seconds to a reminder", self.id, interval);
 | 
				
			||||||
 | 
					                            fail_count += 1;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					                            TimeDelta::zero()
 | 
				
			||||||
 | 
					                        });
 | 
				
			||||||
                }
 | 
					                }
 | 
				
			||||||
            }
 | 
					            }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
@@ -1,9 +1,9 @@
 | 
				
			|||||||
use std::collections::HashMap;
 | 
					use std::collections::HashMap;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
use rocket::serde::json::json;
 | 
					use rocket::{catch, serde::json::json};
 | 
				
			||||||
use rocket_dyn_templates::Template;
 | 
					use rocket_dyn_templates::Template;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
use crate::JsonValue;
 | 
					use crate::web::JsonValue;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
#[catch(403)]
 | 
					#[catch(403)]
 | 
				
			||||||
pub(crate) async fn forbidden() -> Template {
 | 
					pub(crate) async fn forbidden() -> Template {
 | 
				
			||||||
@@ -20,14 +20,14 @@ pub const DAY: usize = 24 * HOUR;
 | 
				
			|||||||
 | 
					
 | 
				
			||||||
pub const CHARACTERS: &str = "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789_";
 | 
					pub const CHARACTERS: &str = "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789_";
 | 
				
			||||||
 | 
					
 | 
				
			||||||
use std::{collections::HashSet, env, iter::FromIterator};
 | 
					use std::{collections::HashSet, env};
 | 
				
			||||||
 | 
					
 | 
				
			||||||
use lazy_static::lazy_static;
 | 
					use lazy_static::lazy_static;
 | 
				
			||||||
use serenity::builder::CreateAttachment;
 | 
					use serenity::builder::CreateAttachment;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
lazy_static! {
 | 
					lazy_static! {
 | 
				
			||||||
    pub static ref DEFAULT_AVATAR: CreateAttachment = CreateAttachment::bytes(
 | 
					    pub static ref DEFAULT_AVATAR: CreateAttachment = CreateAttachment::bytes(
 | 
				
			||||||
        include_bytes!(concat!(env!("CARGO_MANIFEST_DIR"), "/../assets/webhook.jpg")) as &[u8],
 | 
					        include_bytes!(concat!(env!("CARGO_MANIFEST_DIR"), "/assets/webhook.jpg")) as &[u8],
 | 
				
			||||||
        "webhook.jpg",
 | 
					        "webhook.jpg",
 | 
				
			||||||
    );
 | 
					    );
 | 
				
			||||||
    pub static ref SUBSCRIPTION_ROLES: HashSet<u64> = HashSet::from_iter(
 | 
					    pub static ref SUBSCRIPTION_ROLES: HashSet<u64> = HashSet::from_iter(
 | 
				
			||||||
@@ -20,6 +20,7 @@ impl Transaction<'_> {
 | 
				
			|||||||
}
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
#[derive(Debug)]
 | 
					#[derive(Debug)]
 | 
				
			||||||
 | 
					#[allow(dead_code)]
 | 
				
			||||||
pub enum TransactionError {
 | 
					pub enum TransactionError {
 | 
				
			||||||
    Error(sqlx::Error),
 | 
					    Error(sqlx::Error),
 | 
				
			||||||
    Missing,
 | 
					    Missing,
 | 
				
			||||||
							
								
								
									
										27
									
								
								src/web/metrics.rs
									
									
									
									
									
										Normal file
									
								
							
							
						
						@@ -0,0 +1,27 @@
 | 
				
			|||||||
 | 
					use rocket::{
 | 
				
			||||||
 | 
					    fairing::{Fairing, Info, Kind},
 | 
				
			||||||
 | 
					    Request, Response,
 | 
				
			||||||
 | 
					};
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					use crate::metrics::REQUEST_COUNTER;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					pub struct MetricProducer;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					#[rocket::async_trait]
 | 
				
			||||||
 | 
					impl Fairing for MetricProducer {
 | 
				
			||||||
 | 
					    fn info(&self) -> Info {
 | 
				
			||||||
 | 
					        Info { name: "Metrics fairing", kind: Kind::Response }
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    async fn on_response<'r>(&self, req: &'r Request<'_>, resp: &mut Response<'r>) {
 | 
				
			||||||
 | 
					        if let Some(route) = req.route() {
 | 
				
			||||||
 | 
					            REQUEST_COUNTER
 | 
				
			||||||
 | 
					                .with_label_values(&[
 | 
				
			||||||
 | 
					                    req.method().as_str(),
 | 
				
			||||||
 | 
					                    &resp.status().code.to_string(),
 | 
				
			||||||
 | 
					                    &route.uri.to_string(),
 | 
				
			||||||
 | 
					                ])
 | 
				
			||||||
 | 
					                .inc();
 | 
				
			||||||
 | 
					        }
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
@@ -1,6 +1,3 @@
 | 
				
			|||||||
#[macro_use]
 | 
					 | 
				
			||||||
extern crate rocket;
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
mod consts;
 | 
					mod consts;
 | 
				
			||||||
#[macro_use]
 | 
					#[macro_use]
 | 
				
			||||||
mod macros;
 | 
					mod macros;
 | 
				
			||||||
@@ -11,24 +8,27 @@ mod routes;
 | 
				
			|||||||
 | 
					
 | 
				
			||||||
use std::{env, path::Path};
 | 
					use std::{env, path::Path};
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					use log::{error, info, warn};
 | 
				
			||||||
use oauth2::{basic::BasicClient, AuthUrl, ClientId, ClientSecret, RedirectUrl, TokenUrl};
 | 
					use oauth2::{basic::BasicClient, AuthUrl, ClientId, ClientSecret, RedirectUrl, TokenUrl};
 | 
				
			||||||
use rocket::{
 | 
					use poise::serenity_prelude::{
 | 
				
			||||||
    fs::FileServer,
 | 
					 | 
				
			||||||
    http::CookieJar,
 | 
					 | 
				
			||||||
    serde::json::{json, Value as JsonValue},
 | 
					 | 
				
			||||||
    tokio::sync::broadcast::Sender,
 | 
					 | 
				
			||||||
};
 | 
					 | 
				
			||||||
use rocket_dyn_templates::Template;
 | 
					 | 
				
			||||||
use serenity::{
 | 
					 | 
				
			||||||
    client::Context,
 | 
					    client::Context,
 | 
				
			||||||
    http::CacheHttp,
 | 
					    http::CacheHttp,
 | 
				
			||||||
    model::id::{GuildId, UserId},
 | 
					    model::id::{GuildId, UserId},
 | 
				
			||||||
};
 | 
					};
 | 
				
			||||||
 | 
					use rocket::{
 | 
				
			||||||
 | 
					    catchers,
 | 
				
			||||||
 | 
					    fs::FileServer,
 | 
				
			||||||
 | 
					    http::CookieJar,
 | 
				
			||||||
 | 
					    routes,
 | 
				
			||||||
 | 
					    serde::json::{json, Value as JsonValue},
 | 
				
			||||||
 | 
					    tokio::sync::broadcast::Sender,
 | 
				
			||||||
 | 
					};
 | 
				
			||||||
 | 
					use rocket_dyn_templates::Template;
 | 
				
			||||||
use sqlx::{MySql, Pool};
 | 
					use sqlx::{MySql, Pool};
 | 
				
			||||||
 | 
					
 | 
				
			||||||
use crate::{
 | 
					use crate::web::{
 | 
				
			||||||
    consts::{CNC_GUILD, DISCORD_OAUTH_AUTHORIZE, DISCORD_OAUTH_TOKEN, SUBSCRIPTION_ROLES},
 | 
					    consts::{CNC_GUILD, DISCORD_OAUTH_AUTHORIZE, DISCORD_OAUTH_TOKEN, SUBSCRIPTION_ROLES},
 | 
				
			||||||
    metrics::{init_metrics, MetricProducer},
 | 
					    metrics::MetricProducer,
 | 
				
			||||||
};
 | 
					};
 | 
				
			||||||
 | 
					
 | 
				
			||||||
type Database = MySql;
 | 
					type Database = MySql;
 | 
				
			||||||
@@ -68,9 +68,7 @@ pub async fn initialize(
 | 
				
			|||||||
    let reqwest_client = reqwest::Client::new();
 | 
					    let reqwest_client = reqwest::Client::new();
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    let static_path =
 | 
					    let static_path =
 | 
				
			||||||
        if Path::new("web/static").exists() { "web/static" } else { "/lib/reminder-rs/static" };
 | 
					        if Path::new("static").exists() { "static" } else { "/lib/reminder-rs/static" };
 | 
				
			||||||
 | 
					 | 
				
			||||||
    init_metrics();
 | 
					 | 
				
			||||||
 | 
					
 | 
				
			||||||
    rocket::build()
 | 
					    rocket::build()
 | 
				
			||||||
        .attach(MetricProducer)
 | 
					        .attach(MetricProducer)
 | 
				
			||||||
@@ -96,7 +94,6 @@ pub async fn initialize(
 | 
				
			|||||||
            routes![
 | 
					            routes![
 | 
				
			||||||
                routes::cookies,
 | 
					                routes::cookies,
 | 
				
			||||||
                routes::index,
 | 
					                routes::index,
 | 
				
			||||||
                routes::metrics::metrics,
 | 
					 | 
				
			||||||
                routes::privacy,
 | 
					                routes::privacy,
 | 
				
			||||||
                routes::report::report_error,
 | 
					                routes::report::report_error,
 | 
				
			||||||
                routes::return_to_same_site,
 | 
					                routes::return_to_same_site,
 | 
				
			||||||
@@ -154,7 +151,6 @@ pub async fn initialize(
 | 
				
			|||||||
                routes::dashboard::export::import_todos,
 | 
					                routes::dashboard::export::import_todos,
 | 
				
			||||||
            ],
 | 
					            ],
 | 
				
			||||||
        )
 | 
					        )
 | 
				
			||||||
        .mount("/admin", routes![routes::admin::admin_dashboard_home, routes::admin::bot_data])
 | 
					 | 
				
			||||||
        .launch()
 | 
					        .launch()
 | 
				
			||||||
        .await?;
 | 
					        .await?;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
@@ -1,4 +1,4 @@
 | 
				
			|||||||
use rocket::{http::CookieJar, serde::json::json, State};
 | 
					use rocket::{get, http::CookieJar, serde::json::json, State};
 | 
				
			||||||
use serde::Serialize;
 | 
					use serde::Serialize;
 | 
				
			||||||
use serenity::{
 | 
					use serenity::{
 | 
				
			||||||
    client::Context,
 | 
					    client::Context,
 | 
				
			||||||
@@ -8,7 +8,7 @@ use serenity::{
 | 
				
			|||||||
    },
 | 
					    },
 | 
				
			||||||
};
 | 
					};
 | 
				
			||||||
 | 
					
 | 
				
			||||||
use crate::{check_authorization, routes::JsonResult};
 | 
					use crate::web::{check_authorization, routes::JsonResult};
 | 
				
			||||||
 | 
					
 | 
				
			||||||
#[derive(Serialize)]
 | 
					#[derive(Serialize)]
 | 
				
			||||||
struct ChannelInfo {
 | 
					struct ChannelInfo {
 | 
				
			||||||
@@ -7,7 +7,7 @@ use std::env;
 | 
				
			|||||||
 | 
					
 | 
				
			||||||
pub use channels::*;
 | 
					pub use channels::*;
 | 
				
			||||||
pub use reminders::*;
 | 
					pub use reminders::*;
 | 
				
			||||||
use rocket::{http::CookieJar, serde::json::json, State};
 | 
					use rocket::{get, http::CookieJar, serde::json::json, State};
 | 
				
			||||||
pub use roles::*;
 | 
					pub use roles::*;
 | 
				
			||||||
use serenity::{
 | 
					use serenity::{
 | 
				
			||||||
    client::Context,
 | 
					    client::Context,
 | 
				
			||||||
@@ -15,7 +15,7 @@ use serenity::{
 | 
				
			|||||||
};
 | 
					};
 | 
				
			||||||
pub use templates::*;
 | 
					pub use templates::*;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
use crate::{check_authorization, routes::JsonResult};
 | 
					use crate::web::{check_authorization, routes::JsonResult};
 | 
				
			||||||
 | 
					
 | 
				
			||||||
#[get("/api/guild/<id>")]
 | 
					#[get("/api/guild/<id>")]
 | 
				
			||||||
pub async fn get_guild_info(id: u64, cookies: &CookieJar<'_>, ctx: &State<Context>) -> JsonResult {
 | 
					pub async fn get_guild_info(id: u64, cookies: &CookieJar<'_>, ctx: &State<Context>) -> JsonResult {
 | 
				
			||||||
@@ -1,5 +1,8 @@
 | 
				
			|||||||
 | 
					use log::warn;
 | 
				
			||||||
use rocket::{
 | 
					use rocket::{
 | 
				
			||||||
 | 
					    get,
 | 
				
			||||||
    http::CookieJar,
 | 
					    http::CookieJar,
 | 
				
			||||||
 | 
					    patch, post,
 | 
				
			||||||
    serde::json::{json, Json},
 | 
					    serde::json::{json, Json},
 | 
				
			||||||
    State,
 | 
					    State,
 | 
				
			||||||
};
 | 
					};
 | 
				
			||||||
@@ -9,7 +12,7 @@ use serenity::{
 | 
				
			|||||||
};
 | 
					};
 | 
				
			||||||
use sqlx::{MySql, Pool};
 | 
					use sqlx::{MySql, Pool};
 | 
				
			||||||
 | 
					
 | 
				
			||||||
use crate::{
 | 
					use crate::web::{
 | 
				
			||||||
    check_authorization, check_guild_subscription, check_subscription,
 | 
					    check_authorization, check_guild_subscription, check_subscription,
 | 
				
			||||||
    consts::MIN_INTERVAL,
 | 
					    consts::MIN_INTERVAL,
 | 
				
			||||||
    guards::transaction::Transaction,
 | 
					    guards::transaction::Transaction,
 | 
				
			||||||
@@ -1,8 +1,9 @@
 | 
				
			|||||||
use rocket::{http::CookieJar, serde::json::json, State};
 | 
					use log::warn;
 | 
				
			||||||
 | 
					use rocket::{get, http::CookieJar, serde::json::json, State};
 | 
				
			||||||
use serde::Serialize;
 | 
					use serde::Serialize;
 | 
				
			||||||
use serenity::client::Context;
 | 
					use serenity::client::Context;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
use crate::{check_authorization, routes::JsonResult};
 | 
					use crate::web::{check_authorization, routes::JsonResult};
 | 
				
			||||||
 | 
					
 | 
				
			||||||
#[derive(Serialize)]
 | 
					#[derive(Serialize)]
 | 
				
			||||||
struct RoleInfo {
 | 
					struct RoleInfo {
 | 
				
			||||||
@@ -1,12 +1,15 @@
 | 
				
			|||||||
 | 
					use log::warn;
 | 
				
			||||||
use rocket::{
 | 
					use rocket::{
 | 
				
			||||||
 | 
					    delete, get,
 | 
				
			||||||
    http::CookieJar,
 | 
					    http::CookieJar,
 | 
				
			||||||
 | 
					    post,
 | 
				
			||||||
    serde::json::{json, Json},
 | 
					    serde::json::{json, Json},
 | 
				
			||||||
    State,
 | 
					    State,
 | 
				
			||||||
};
 | 
					};
 | 
				
			||||||
use serenity::client::Context;
 | 
					use serenity::client::Context;
 | 
				
			||||||
use sqlx::{MySql, Pool};
 | 
					use sqlx::{MySql, Pool};
 | 
				
			||||||
 | 
					
 | 
				
			||||||
use crate::{
 | 
					use crate::web::{
 | 
				
			||||||
    check_authorization,
 | 
					    check_authorization,
 | 
				
			||||||
    consts::{
 | 
					    consts::{
 | 
				
			||||||
        MAX_CONTENT_LENGTH, MAX_EMBED_AUTHOR_LENGTH, MAX_EMBED_DESCRIPTION_LENGTH,
 | 
					        MAX_CONTENT_LENGTH, MAX_EMBED_AUTHOR_LENGTH, MAX_EMBED_DESCRIPTION_LENGTH,
 | 
				
			||||||
@@ -1,14 +1,16 @@
 | 
				
			|||||||
pub mod guild;
 | 
					pub mod guild;
 | 
				
			||||||
pub mod user;
 | 
					pub mod user;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					use log::warn;
 | 
				
			||||||
use rocket::{
 | 
					use rocket::{
 | 
				
			||||||
 | 
					    delete,
 | 
				
			||||||
    http::CookieJar,
 | 
					    http::CookieJar,
 | 
				
			||||||
    serde::json::{json, Json},
 | 
					    serde::json::{json, Json},
 | 
				
			||||||
    State,
 | 
					    State,
 | 
				
			||||||
};
 | 
					};
 | 
				
			||||||
use sqlx::{MySql, Pool};
 | 
					use sqlx::{MySql, Pool};
 | 
				
			||||||
 | 
					
 | 
				
			||||||
use crate::routes::{dashboard::DeleteReminder, JsonResult};
 | 
					use crate::web::routes::{dashboard::DeleteReminder, JsonResult};
 | 
				
			||||||
 | 
					
 | 
				
			||||||
#[delete("/api/reminders", data = "<reminder>")]
 | 
					#[delete("/api/reminders", data = "<reminder>")]
 | 
				
			||||||
pub async fn delete_reminder(
 | 
					pub async fn delete_reminder(
 | 
				
			||||||
@@ -1,5 +1,7 @@
 | 
				
			|||||||
 | 
					use log::warn;
 | 
				
			||||||
use reqwest::Client;
 | 
					use reqwest::Client;
 | 
				
			||||||
use rocket::{
 | 
					use rocket::{
 | 
				
			||||||
 | 
					    get,
 | 
				
			||||||
    http::CookieJar,
 | 
					    http::CookieJar,
 | 
				
			||||||
    serde::json::{json, Value as JsonValue},
 | 
					    serde::json::{json, Value as JsonValue},
 | 
				
			||||||
    State,
 | 
					    State,
 | 
				
			||||||
@@ -7,7 +9,7 @@ use rocket::{
 | 
				
			|||||||
use serde::{Deserialize, Serialize};
 | 
					use serde::{Deserialize, Serialize};
 | 
				
			||||||
use serenity::model::{id::GuildId, permissions::Permissions};
 | 
					use serenity::model::{id::GuildId, permissions::Permissions};
 | 
				
			||||||
 | 
					
 | 
				
			||||||
use crate::consts::DISCORD_API;
 | 
					use crate::web::consts::DISCORD_API;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
#[derive(Serialize)]
 | 
					#[derive(Serialize)]
 | 
				
			||||||
struct GuildInfo {
 | 
					struct GuildInfo {
 | 
				
			||||||
@@ -8,7 +8,9 @@ use chrono_tz::Tz;
 | 
				
			|||||||
pub use guilds::*;
 | 
					pub use guilds::*;
 | 
				
			||||||
pub use reminders::*;
 | 
					pub use reminders::*;
 | 
				
			||||||
use rocket::{
 | 
					use rocket::{
 | 
				
			||||||
 | 
					    get,
 | 
				
			||||||
    http::CookieJar,
 | 
					    http::CookieJar,
 | 
				
			||||||
 | 
					    patch,
 | 
				
			||||||
    serde::json::{json, Json, Value as JsonValue},
 | 
					    serde::json::{json, Json, Value as JsonValue},
 | 
				
			||||||
    State,
 | 
					    State,
 | 
				
			||||||
};
 | 
					};
 | 
				
			||||||
@@ -1,11 +1,12 @@
 | 
				
			|||||||
use chrono::{naive::NaiveDateTime, Utc};
 | 
					use chrono::{naive::NaiveDateTime, Utc};
 | 
				
			||||||
use futures::TryFutureExt;
 | 
					use futures::TryFutureExt;
 | 
				
			||||||
 | 
					use log::warn;
 | 
				
			||||||
use rocket::serde::json::json;
 | 
					use rocket::serde::json::json;
 | 
				
			||||||
use serde::{Deserialize, Serialize};
 | 
					use serde::{Deserialize, Serialize};
 | 
				
			||||||
use serenity::{client::Context, futures, model::id::UserId};
 | 
					use serenity::{client::Context, model::id::UserId};
 | 
				
			||||||
use sqlx::types::Json;
 | 
					use sqlx::types::Json;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
use crate::{
 | 
					use crate::web::{
 | 
				
			||||||
    check_subscription,
 | 
					    check_subscription,
 | 
				
			||||||
    consts::{
 | 
					    consts::{
 | 
				
			||||||
        DAY, MAX_CONTENT_LENGTH, MAX_EMBED_AUTHOR_LENGTH, MAX_EMBED_DESCRIPTION_LENGTH,
 | 
					        DAY, MAX_CONTENT_LENGTH, MAX_EMBED_AUTHOR_LENGTH, MAX_EMBED_DESCRIPTION_LENGTH,
 | 
				
			||||||
@@ -15,10 +16,7 @@ use crate::{
 | 
				
			|||||||
    },
 | 
					    },
 | 
				
			||||||
    guards::transaction::Transaction,
 | 
					    guards::transaction::Transaction,
 | 
				
			||||||
    routes::{
 | 
					    routes::{
 | 
				
			||||||
        dashboard::{
 | 
					        dashboard::{create_database_channel, generate_uid, name_default, Attachment, EmbedField},
 | 
				
			||||||
            create_database_channel, deserialize_optional_field, generate_uid, interval_default,
 | 
					 | 
				
			||||||
            name_default, Attachment, EmbedField, Unset,
 | 
					 | 
				
			||||||
        },
 | 
					 | 
				
			||||||
        JsonResult,
 | 
					        JsonResult,
 | 
				
			||||||
    },
 | 
					    },
 | 
				
			||||||
    Error,
 | 
					    Error,
 | 
				
			||||||
@@ -231,60 +229,3 @@ pub async fn create_reminder(
 | 
				
			|||||||
        }
 | 
					        }
 | 
				
			||||||
    }
 | 
					    }
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 | 
					 | 
				
			||||||
#[derive(Deserialize)]
 | 
					 | 
				
			||||||
pub struct PatchReminder {
 | 
					 | 
				
			||||||
    pub uid: String,
 | 
					 | 
				
			||||||
    #[serde(default)]
 | 
					 | 
				
			||||||
    #[serde(deserialize_with = "deserialize_optional_field")]
 | 
					 | 
				
			||||||
    pub attachment: Unset<Option<Attachment>>,
 | 
					 | 
				
			||||||
    #[serde(default)]
 | 
					 | 
				
			||||||
    #[serde(deserialize_with = "deserialize_optional_field")]
 | 
					 | 
				
			||||||
    pub attachment_name: Unset<Option<String>>,
 | 
					 | 
				
			||||||
    #[serde(default)]
 | 
					 | 
				
			||||||
    pub content: Unset<String>,
 | 
					 | 
				
			||||||
    #[serde(default)]
 | 
					 | 
				
			||||||
    pub embed_author: Unset<String>,
 | 
					 | 
				
			||||||
    #[serde(default)]
 | 
					 | 
				
			||||||
    #[serde(deserialize_with = "deserialize_optional_field")]
 | 
					 | 
				
			||||||
    pub embed_author_url: Unset<Option<String>>,
 | 
					 | 
				
			||||||
    #[serde(default)]
 | 
					 | 
				
			||||||
    pub embed_color: Unset<u32>,
 | 
					 | 
				
			||||||
    #[serde(default)]
 | 
					 | 
				
			||||||
    pub embed_description: Unset<String>,
 | 
					 | 
				
			||||||
    #[serde(default)]
 | 
					 | 
				
			||||||
    pub embed_footer: Unset<String>,
 | 
					 | 
				
			||||||
    #[serde(default)]
 | 
					 | 
				
			||||||
    #[serde(deserialize_with = "deserialize_optional_field")]
 | 
					 | 
				
			||||||
    pub embed_footer_url: Unset<Option<String>>,
 | 
					 | 
				
			||||||
    #[serde(default)]
 | 
					 | 
				
			||||||
    #[serde(deserialize_with = "deserialize_optional_field")]
 | 
					 | 
				
			||||||
    pub embed_image_url: Unset<Option<String>>,
 | 
					 | 
				
			||||||
    #[serde(default)]
 | 
					 | 
				
			||||||
    #[serde(deserialize_with = "deserialize_optional_field")]
 | 
					 | 
				
			||||||
    pub embed_thumbnail_url: Unset<Option<String>>,
 | 
					 | 
				
			||||||
    #[serde(default)]
 | 
					 | 
				
			||||||
    pub embed_title: Unset<String>,
 | 
					 | 
				
			||||||
    #[serde(default)]
 | 
					 | 
				
			||||||
    pub embed_fields: Unset<Json<Vec<EmbedField>>>,
 | 
					 | 
				
			||||||
    #[serde(default)]
 | 
					 | 
				
			||||||
    pub enabled: Unset<bool>,
 | 
					 | 
				
			||||||
    #[serde(default)]
 | 
					 | 
				
			||||||
    #[serde(deserialize_with = "deserialize_optional_field")]
 | 
					 | 
				
			||||||
    pub expires: Unset<Option<NaiveDateTime>>,
 | 
					 | 
				
			||||||
    #[serde(default = "interval_default")]
 | 
					 | 
				
			||||||
    #[serde(deserialize_with = "deserialize_optional_field")]
 | 
					 | 
				
			||||||
    pub interval_seconds: Unset<Option<u32>>,
 | 
					 | 
				
			||||||
    #[serde(default = "interval_default")]
 | 
					 | 
				
			||||||
    #[serde(deserialize_with = "deserialize_optional_field")]
 | 
					 | 
				
			||||||
    pub interval_days: Unset<Option<u32>>,
 | 
					 | 
				
			||||||
    #[serde(default = "interval_default")]
 | 
					 | 
				
			||||||
    #[serde(deserialize_with = "deserialize_optional_field")]
 | 
					 | 
				
			||||||
    pub interval_months: Unset<Option<u32>>,
 | 
					 | 
				
			||||||
    #[serde(default)]
 | 
					 | 
				
			||||||
    pub name: Unset<String>,
 | 
					 | 
				
			||||||
    #[serde(default)]
 | 
					 | 
				
			||||||
    pub tts: Unset<bool>,
 | 
					 | 
				
			||||||
    #[serde(default)]
 | 
					 | 
				
			||||||
    pub utc_time: Unset<NaiveDateTime>,
 | 
					 | 
				
			||||||
}
 | 
					 | 
				
			||||||
@@ -1,12 +1,15 @@
 | 
				
			|||||||
 | 
					use log::warn;
 | 
				
			||||||
use rocket::{
 | 
					use rocket::{
 | 
				
			||||||
 | 
					    get,
 | 
				
			||||||
    http::CookieJar,
 | 
					    http::CookieJar,
 | 
				
			||||||
 | 
					    patch, post,
 | 
				
			||||||
    serde::json::{json, Json},
 | 
					    serde::json::{json, Json},
 | 
				
			||||||
    State,
 | 
					    State,
 | 
				
			||||||
};
 | 
					};
 | 
				
			||||||
use serenity::{client::Context, model::id::UserId};
 | 
					use serenity::{client::Context, model::id::UserId};
 | 
				
			||||||
use sqlx::{MySql, Pool};
 | 
					use sqlx::{MySql, Pool};
 | 
				
			||||||
 | 
					
 | 
				
			||||||
use crate::{
 | 
					use crate::web::{
 | 
				
			||||||
    check_subscription,
 | 
					    check_subscription,
 | 
				
			||||||
    guards::transaction::Transaction,
 | 
					    guards::transaction::Transaction,
 | 
				
			||||||
    routes::{
 | 
					    routes::{
 | 
				
			||||||
@@ -1,7 +1,11 @@
 | 
				
			|||||||
 | 
					use base64::{prelude::BASE64_STANDARD, Engine};
 | 
				
			||||||
use csv::{QuoteStyle, WriterBuilder};
 | 
					use csv::{QuoteStyle, WriterBuilder};
 | 
				
			||||||
 | 
					use log::warn;
 | 
				
			||||||
use rocket::{
 | 
					use rocket::{
 | 
				
			||||||
 | 
					    get,
 | 
				
			||||||
    http::CookieJar,
 | 
					    http::CookieJar,
 | 
				
			||||||
    serde::json::{json, serde_json, Json},
 | 
					    put,
 | 
				
			||||||
 | 
					    serde::json::{json, Json},
 | 
				
			||||||
    State,
 | 
					    State,
 | 
				
			||||||
};
 | 
					};
 | 
				
			||||||
use serenity::{
 | 
					use serenity::{
 | 
				
			||||||
@@ -10,7 +14,7 @@ use serenity::{
 | 
				
			|||||||
};
 | 
					};
 | 
				
			||||||
use sqlx::{MySql, Pool};
 | 
					use sqlx::{MySql, Pool};
 | 
				
			||||||
 | 
					
 | 
				
			||||||
use crate::{
 | 
					use crate::web::{
 | 
				
			||||||
    check_authorization,
 | 
					    check_authorization,
 | 
				
			||||||
    guards::transaction::Transaction,
 | 
					    guards::transaction::Transaction,
 | 
				
			||||||
    routes::{
 | 
					    routes::{
 | 
				
			||||||
@@ -134,7 +138,7 @@ pub(crate) async fn import_reminders(
 | 
				
			|||||||
    let user_id =
 | 
					    let user_id =
 | 
				
			||||||
        cookies.get_private("userid").map(|c| c.value().parse::<u64>().ok()).flatten().unwrap();
 | 
					        cookies.get_private("userid").map(|c| c.value().parse::<u64>().ok()).flatten().unwrap();
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    match base64::decode(&body.body) {
 | 
					    match BASE64_STANDARD.decode(&body.body) {
 | 
				
			||||||
        Ok(body) => {
 | 
					        Ok(body) => {
 | 
				
			||||||
            let mut reader = csv::Reader::from_reader(body.as_slice());
 | 
					            let mut reader = csv::Reader::from_reader(body.as_slice());
 | 
				
			||||||
            let mut count = 0;
 | 
					            let mut count = 0;
 | 
				
			||||||
@@ -292,7 +296,7 @@ pub async fn import_todos(
 | 
				
			|||||||
    let channels_res = GuildId::new(id).channels(&ctx.inner()).await;
 | 
					    let channels_res = GuildId::new(id).channels(&ctx.inner()).await;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    match channels_res {
 | 
					    match channels_res {
 | 
				
			||||||
        Ok(channels) => match base64::decode(&body.body) {
 | 
					        Ok(channels) => match BASE64_STANDARD.decode(&body.body) {
 | 
				
			||||||
            Ok(body) => {
 | 
					            Ok(body) => {
 | 
				
			||||||
                let mut reader = csv::Reader::from_reader(body.as_slice());
 | 
					                let mut reader = csv::Reader::from_reader(body.as_slice());
 | 
				
			||||||
 | 
					
 | 
				
			||||||
@@ -1,8 +1,12 @@
 | 
				
			|||||||
use std::path::Path;
 | 
					use std::path::Path;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					use base64::{prelude::BASE64_STANDARD, Engine};
 | 
				
			||||||
use chrono::{naive::NaiveDateTime, Utc};
 | 
					use chrono::{naive::NaiveDateTime, Utc};
 | 
				
			||||||
 | 
					use log::warn;
 | 
				
			||||||
use rand::{rngs::OsRng, seq::IteratorRandom};
 | 
					use rand::{rngs::OsRng, seq::IteratorRandom};
 | 
				
			||||||
use rocket::{fs::NamedFile, http::CookieJar, response::Redirect, serde::json::json};
 | 
					use rocket::{
 | 
				
			||||||
 | 
					    fs::NamedFile, get, http::CookieJar, response::Redirect, serde::json::json, Responder,
 | 
				
			||||||
 | 
					};
 | 
				
			||||||
use rocket_dyn_templates::Template;
 | 
					use rocket_dyn_templates::Template;
 | 
				
			||||||
use secrecy::ExposeSecret;
 | 
					use secrecy::ExposeSecret;
 | 
				
			||||||
use serde::{de, Deserialize, Deserializer, Serialize, Serializer};
 | 
					use serde::{de, Deserialize, Deserializer, Serialize, Serializer};
 | 
				
			||||||
@@ -14,7 +18,7 @@ use serenity::{
 | 
				
			|||||||
};
 | 
					};
 | 
				
			||||||
use sqlx::types::Json;
 | 
					use sqlx::types::Json;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
use crate::{
 | 
					use crate::web::{
 | 
				
			||||||
    catchers::internal_server_error,
 | 
					    catchers::internal_server_error,
 | 
				
			||||||
    check_guild_subscription, check_subscription,
 | 
					    check_guild_subscription, check_subscription,
 | 
				
			||||||
    consts::{
 | 
					    consts::{
 | 
				
			||||||
@@ -63,7 +67,7 @@ impl<'de> Deserialize<'de> for Attachment {
 | 
				
			|||||||
        D: Deserializer<'de>,
 | 
					        D: Deserializer<'de>,
 | 
				
			||||||
    {
 | 
					    {
 | 
				
			||||||
        let string = String::deserialize(deserializer)?;
 | 
					        let string = String::deserialize(deserializer)?;
 | 
				
			||||||
        Ok(Attachment(base64::decode(string).map_err(de::Error::custom)?))
 | 
					        Ok(Attachment(BASE64_STANDARD.decode(string).map_err(de::Error::custom)?))
 | 
				
			||||||
    }
 | 
					    }
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
@@ -72,7 +76,7 @@ impl Serialize for Attachment {
 | 
				
			|||||||
    where
 | 
					    where
 | 
				
			||||||
        S: Serializer,
 | 
					        S: Serializer,
 | 
				
			||||||
    {
 | 
					    {
 | 
				
			||||||
        serializer.collect_str(&base64::encode(&self.0))
 | 
					        serializer.collect_str(&BASE64_STANDARD.encode(&self.0))
 | 
				
			||||||
    }
 | 
					    }
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
@@ -595,7 +599,7 @@ async fn create_database_channel(
 | 
				
			|||||||
    ctx: impl CacheHttp,
 | 
					    ctx: impl CacheHttp,
 | 
				
			||||||
    channel: ChannelId,
 | 
					    channel: ChannelId,
 | 
				
			||||||
    transaction: &mut Transaction<'_>,
 | 
					    transaction: &mut Transaction<'_>,
 | 
				
			||||||
) -> Result<u32, crate::Error> {
 | 
					) -> Result<u32, Error> {
 | 
				
			||||||
    let row = sqlx::query!(
 | 
					    let row = sqlx::query!(
 | 
				
			||||||
        "SELECT webhook_token, webhook_id FROM channels WHERE channel = ?",
 | 
					        "SELECT webhook_token, webhook_id FROM channels WHERE channel = ?",
 | 
				
			||||||
        channel.get()
 | 
					        channel.get()
 | 
				
			||||||
@@ -5,13 +5,14 @@ use oauth2::{
 | 
				
			|||||||
};
 | 
					};
 | 
				
			||||||
use reqwest::Client;
 | 
					use reqwest::Client;
 | 
				
			||||||
use rocket::{
 | 
					use rocket::{
 | 
				
			||||||
 | 
					    get,
 | 
				
			||||||
    http::{private::cookie::Expiration, Cookie, CookieJar, SameSite},
 | 
					    http::{private::cookie::Expiration, Cookie, CookieJar, SameSite},
 | 
				
			||||||
    response::{Flash, Redirect},
 | 
					    response::{Flash, Redirect},
 | 
				
			||||||
    uri, State,
 | 
					    uri, State,
 | 
				
			||||||
};
 | 
					};
 | 
				
			||||||
use serenity::model::user::User;
 | 
					use serenity::model::user::User;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
use crate::{consts::DISCORD_API, routes};
 | 
					use crate::web::{consts::DISCORD_API, routes};
 | 
				
			||||||
 | 
					
 | 
				
			||||||
#[get("/discord")]
 | 
					#[get("/discord")]
 | 
				
			||||||
pub async fn discord_login(
 | 
					pub async fn discord_login(
 | 
				
			||||||
@@ -1,12 +1,10 @@
 | 
				
			|||||||
pub mod admin;
 | 
					 | 
				
			||||||
pub mod dashboard;
 | 
					pub mod dashboard;
 | 
				
			||||||
pub mod login;
 | 
					pub mod login;
 | 
				
			||||||
pub mod metrics;
 | 
					 | 
				
			||||||
pub mod report;
 | 
					pub mod report;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
use std::collections::HashMap;
 | 
					use std::collections::HashMap;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
use rocket::{request::FlashMessage, serde::json::Value as JsonValue};
 | 
					use rocket::{get, request::FlashMessage, serde::json::Value as JsonValue};
 | 
				
			||||||
use rocket_dyn_templates::Template;
 | 
					use rocket_dyn_templates::Template;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
pub type JsonResult = Result<JsonValue, JsonValue>;
 | 
					pub type JsonResult = Result<JsonValue, JsonValue>;
 | 
				
			||||||
@@ -1,12 +1,14 @@
 | 
				
			|||||||
 | 
					use log::error;
 | 
				
			||||||
use rocket::{
 | 
					use rocket::{
 | 
				
			||||||
    http::CookieJar,
 | 
					    http::CookieJar,
 | 
				
			||||||
 | 
					    post,
 | 
				
			||||||
    serde::{
 | 
					    serde::{
 | 
				
			||||||
        json::{json, Json},
 | 
					        json::{json, Json},
 | 
				
			||||||
        Deserialize,
 | 
					        Deserialize,
 | 
				
			||||||
    },
 | 
					    },
 | 
				
			||||||
};
 | 
					};
 | 
				
			||||||
 | 
					
 | 
				
			||||||
use crate::routes::JsonResult;
 | 
					use crate::web::routes::JsonResult;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
#[derive(Deserialize)]
 | 
					#[derive(Deserialize)]
 | 
				
			||||||
pub struct ClientError {
 | 
					pub struct ClientError {
 | 
				
			||||||
| 
		 Before Width: | Height: | Size: 7.2 KiB After Width: | Height: | Size: 7.2 KiB  | 
| 
		 Before Width: | Height: | Size: 20 KiB After Width: | Height: | Size: 20 KiB  | 
| 
		 Before Width: | Height: | Size: 6.8 KiB After Width: | Height: | Size: 6.8 KiB  | 
| 
		 Before Width: | Height: | Size: 1.8 KiB After Width: | Height: | Size: 1.8 KiB  | 
| 
		 Before Width: | Height: | Size: 2.3 KiB After Width: | Height: | Size: 2.3 KiB  | 
| 
		 Before Width: | Height: | Size: 7.2 KiB After Width: | Height: | Size: 7.2 KiB  | 
| 
		 Before Width: | Height: | Size: 4.9 KiB After Width: | Height: | Size: 4.9 KiB  | 
| 
		 Before Width: | Height: | Size: 762 B After Width: | Height: | Size: 762 B  | 
| 
		 Before Width: | Height: | Size: 11 KiB After Width: | Height: | Size: 11 KiB  | 
| 
		 Before Width: | Height: | Size: 323 KiB After Width: | Height: | Size: 323 KiB  | 
| 
		 Before Width: | Height: | Size: 61 KiB After Width: | Height: | Size: 61 KiB  | 
| 
		 Before Width: | Height: | Size: 81 KiB After Width: | Height: | Size: 81 KiB  | 
| 
		 Before Width: | Height: | Size: 23 KiB After Width: | Height: | Size: 23 KiB  | 
| 
		 Before Width: | Height: | Size: 35 KiB After Width: | Height: | Size: 35 KiB  | 
| 
		 Before Width: | Height: | Size: 55 KiB After Width: | Height: | Size: 55 KiB  | 
| 
		 Before Width: | Height: | Size: 3.6 KiB After Width: | Height: | Size: 3.6 KiB  | 
| 
		 Before Width: | Height: | Size: 17 KiB After Width: | Height: | Size: 17 KiB  | 
| 
		 Before Width: | Height: | Size: 14 KiB After Width: | Height: | Size: 14 KiB  | 
| 
		 Before Width: | Height: | Size: 24 KiB After Width: | Height: | Size: 24 KiB  | 
| 
		 Before Width: | Height: | Size: 17 KiB After Width: | Height: | Size: 17 KiB  | 
| 
		 Before Width: | Height: | Size: 44 KiB After Width: | Height: | Size: 44 KiB  | 
| 
		 Before Width: | Height: | Size: 40 KiB After Width: | Height: | Size: 40 KiB  | 
| 
		 Before Width: | Height: | Size: 44 KiB After Width: | Height: | Size: 44 KiB  | 
| 
		 Before Width: | Height: | Size: 18 KiB After Width: | Height: | Size: 18 KiB  | 
| 
		 Before Width: | Height: | Size: 20 KiB After Width: | Height: | Size: 20 KiB  | 
| 
		 Before Width: | Height: | Size: 65 KiB After Width: | Height: | Size: 65 KiB  | 
							
								
								
									
										1
									
								
								static/index.html
									
									
									
									
									
										Symbolic link
									
								
							
							
						
						@@ -0,0 +1 @@
 | 
				
			|||||||
 | 
					/home/jude/reminder-bot/reminder-dashboard/dist/index.html
 | 
				
			||||||
| 
		 Before Width: | Height: | Size: 712 KiB After Width: | Height: | Size: 712 KiB  |