You've already forked AstralRinth
forked from didirus/AstralRinth
Allow multiple labrinth instances (#3360)
* Move a lot of scheduled tasks to be runnable from the command-line * Use pubsub to handle sockets connected to multiple Labrinths * Clippy fix * Fix build and merge some stuff * Fix build fmt : --------- Signed-off-by: Jai Agrawal <18202329+Geometrically@users.noreply.github.com> Co-authored-by: Jai A <jaiagr+gpg@pm.me> Co-authored-by: Jai Agrawal <18202329+Geometrically@users.noreply.github.com>
This commit is contained in:
@@ -2091,323 +2091,331 @@ async fn get_or_create_customer(
|
||||
}
|
||||
}
|
||||
|
||||
pub async fn subscription_task(pool: PgPool, redis: RedisPool) {
|
||||
loop {
|
||||
info!("Indexing subscriptions");
|
||||
pub async fn index_subscriptions(pool: PgPool, redis: RedisPool) {
|
||||
info!("Indexing subscriptions");
|
||||
|
||||
let res = async {
|
||||
let mut transaction = pool.begin().await?;
|
||||
let mut clear_cache_users = Vec::new();
|
||||
let res = async {
|
||||
let mut transaction = pool.begin().await?;
|
||||
let mut clear_cache_users = Vec::new();
|
||||
|
||||
// If an active subscription has a canceled charge OR a failed charge more than two days ago, it should be cancelled
|
||||
let all_charges = ChargeItem::get_unprovision(&pool).await?;
|
||||
// If an active subscription has a canceled charge OR a failed charge more than two days ago, it should be cancelled
|
||||
let all_charges = ChargeItem::get_unprovision(&pool).await?;
|
||||
|
||||
let mut all_subscriptions =
|
||||
user_subscription_item::UserSubscriptionItem::get_many(
|
||||
&all_charges
|
||||
.iter()
|
||||
.filter_map(|x| x.subscription_id)
|
||||
.collect::<HashSet<_>>()
|
||||
.into_iter()
|
||||
.collect::<Vec<_>>(),
|
||||
&pool,
|
||||
)
|
||||
.await?;
|
||||
let subscription_prices = product_item::ProductPriceItem::get_many(
|
||||
&all_subscriptions
|
||||
let mut all_subscriptions =
|
||||
user_subscription_item::UserSubscriptionItem::get_many(
|
||||
&all_charges
|
||||
.iter()
|
||||
.map(|x| x.price_id)
|
||||
.filter_map(|x| x.subscription_id)
|
||||
.collect::<HashSet<_>>()
|
||||
.into_iter()
|
||||
.collect::<Vec<_>>(),
|
||||
&pool,
|
||||
)
|
||||
.await?;
|
||||
let subscription_products = product_item::ProductItem::get_many(
|
||||
&subscription_prices
|
||||
.iter()
|
||||
.map(|x| x.product_id)
|
||||
.collect::<HashSet<_>>()
|
||||
.into_iter()
|
||||
.collect::<Vec<_>>(),
|
||||
&pool,
|
||||
)
|
||||
.await?;
|
||||
let users = crate::database::models::User::get_many_ids(
|
||||
&all_subscriptions
|
||||
.iter()
|
||||
.map(|x| x.user_id)
|
||||
.collect::<HashSet<_>>()
|
||||
.into_iter()
|
||||
.collect::<Vec<_>>(),
|
||||
&pool,
|
||||
&redis,
|
||||
)
|
||||
.await?;
|
||||
let subscription_prices = product_item::ProductPriceItem::get_many(
|
||||
&all_subscriptions
|
||||
.iter()
|
||||
.map(|x| x.price_id)
|
||||
.collect::<HashSet<_>>()
|
||||
.into_iter()
|
||||
.collect::<Vec<_>>(),
|
||||
&pool,
|
||||
)
|
||||
.await?;
|
||||
let subscription_products = product_item::ProductItem::get_many(
|
||||
&subscription_prices
|
||||
.iter()
|
||||
.map(|x| x.product_id)
|
||||
.collect::<HashSet<_>>()
|
||||
.into_iter()
|
||||
.collect::<Vec<_>>(),
|
||||
&pool,
|
||||
)
|
||||
.await?;
|
||||
let users = crate::database::models::User::get_many_ids(
|
||||
&all_subscriptions
|
||||
.iter()
|
||||
.map(|x| x.user_id)
|
||||
.collect::<HashSet<_>>()
|
||||
.into_iter()
|
||||
.collect::<Vec<_>>(),
|
||||
&pool,
|
||||
&redis,
|
||||
)
|
||||
.await?;
|
||||
|
||||
for charge in all_charges {
|
||||
let subscription = if let Some(subscription) = all_subscriptions
|
||||
.iter_mut()
|
||||
.find(|x| Some(x.id) == charge.subscription_id)
|
||||
{
|
||||
subscription
|
||||
} else {
|
||||
continue;
|
||||
};
|
||||
for charge in all_charges {
|
||||
let subscription = if let Some(subscription) = all_subscriptions
|
||||
.iter_mut()
|
||||
.find(|x| Some(x.id) == charge.subscription_id)
|
||||
{
|
||||
subscription
|
||||
} else {
|
||||
continue;
|
||||
};
|
||||
|
||||
if subscription.status == SubscriptionStatus::Unprovisioned {
|
||||
continue;
|
||||
if subscription.status == SubscriptionStatus::Unprovisioned {
|
||||
continue;
|
||||
}
|
||||
|
||||
let product_price = if let Some(product_price) = subscription_prices
|
||||
.iter()
|
||||
.find(|x| x.id == subscription.price_id)
|
||||
{
|
||||
product_price
|
||||
} else {
|
||||
continue;
|
||||
};
|
||||
|
||||
let product = if let Some(product) = subscription_products
|
||||
.iter()
|
||||
.find(|x| x.id == product_price.product_id)
|
||||
{
|
||||
product
|
||||
} else {
|
||||
continue;
|
||||
};
|
||||
|
||||
let user = if let Some(user) =
|
||||
users.iter().find(|x| x.id == subscription.user_id)
|
||||
{
|
||||
user
|
||||
} else {
|
||||
continue;
|
||||
};
|
||||
|
||||
let unprovisioned = match product.metadata {
|
||||
ProductMetadata::Midas => {
|
||||
let badges = user.badges - Badges::MIDAS;
|
||||
|
||||
sqlx::query!(
|
||||
"
|
||||
UPDATE users
|
||||
SET badges = $1
|
||||
WHERE (id = $2)
|
||||
",
|
||||
badges.bits() as i64,
|
||||
user.id as crate::database::models::ids::UserId,
|
||||
)
|
||||
.execute(&mut *transaction)
|
||||
.await?;
|
||||
|
||||
true
|
||||
}
|
||||
ProductMetadata::Pyro { .. } => {
|
||||
if let Some(SubscriptionMetadata::Pyro { id }) =
|
||||
&subscription.metadata
|
||||
{
|
||||
let res = reqwest::Client::new()
|
||||
.post(format!(
|
||||
"{}/modrinth/v0/servers/{}/suspend",
|
||||
dotenvy::var("ARCHON_URL")?,
|
||||
id
|
||||
))
|
||||
.header("X-Master-Key", dotenvy::var("PYRO_API_KEY")?)
|
||||
.json(&serde_json::json!({
|
||||
"reason": if charge.status == ChargeStatus::Cancelled {
|
||||
"cancelled"
|
||||
} else {
|
||||
"paymentfailed"
|
||||
}
|
||||
}))
|
||||
.send()
|
||||
.await;
|
||||
|
||||
let product_price = if let Some(product_price) =
|
||||
subscription_prices
|
||||
.iter()
|
||||
.find(|x| x.id == subscription.price_id)
|
||||
{
|
||||
product_price
|
||||
} else {
|
||||
continue;
|
||||
};
|
||||
|
||||
let product = if let Some(product) = subscription_products
|
||||
.iter()
|
||||
.find(|x| x.id == product_price.product_id)
|
||||
{
|
||||
product
|
||||
} else {
|
||||
continue;
|
||||
};
|
||||
|
||||
let user = if let Some(user) =
|
||||
users.iter().find(|x| x.id == subscription.user_id)
|
||||
{
|
||||
user
|
||||
} else {
|
||||
continue;
|
||||
};
|
||||
|
||||
let unprovisioned = match product.metadata {
|
||||
ProductMetadata::Midas => {
|
||||
let badges = user.badges - Badges::MIDAS;
|
||||
|
||||
sqlx::query!(
|
||||
"
|
||||
UPDATE users
|
||||
SET badges = $1
|
||||
WHERE (id = $2)
|
||||
",
|
||||
badges.bits() as i64,
|
||||
user.id as crate::database::models::ids::UserId,
|
||||
)
|
||||
.execute(&mut *transaction)
|
||||
.await?;
|
||||
|
||||
true
|
||||
}
|
||||
ProductMetadata::Pyro { .. } => {
|
||||
if let Some(SubscriptionMetadata::Pyro { id }) =
|
||||
&subscription.metadata
|
||||
{
|
||||
let res = reqwest::Client::new()
|
||||
.post(format!(
|
||||
"{}/modrinth/v0/servers/{}/suspend",
|
||||
dotenvy::var("ARCHON_URL")?,
|
||||
id
|
||||
))
|
||||
.header("X-Master-Key", dotenvy::var("PYRO_API_KEY")?)
|
||||
.json(&serde_json::json!({
|
||||
"reason": if charge.status == ChargeStatus::Cancelled {
|
||||
"cancelled"
|
||||
} else {
|
||||
"paymentfailed"
|
||||
}
|
||||
}))
|
||||
.send()
|
||||
.await;
|
||||
|
||||
if let Err(e) = res {
|
||||
warn!("Error suspending pyro server: {:?}", e);
|
||||
false
|
||||
} else {
|
||||
true
|
||||
}
|
||||
if let Err(e) = res {
|
||||
warn!("Error suspending pyro server: {:?}", e);
|
||||
false
|
||||
} else {
|
||||
true
|
||||
}
|
||||
} else {
|
||||
true
|
||||
}
|
||||
};
|
||||
|
||||
if unprovisioned {
|
||||
subscription.status = SubscriptionStatus::Unprovisioned;
|
||||
subscription.upsert(&mut transaction).await?;
|
||||
}
|
||||
};
|
||||
|
||||
clear_cache_users.push(user.id);
|
||||
if unprovisioned {
|
||||
subscription.status = SubscriptionStatus::Unprovisioned;
|
||||
subscription.upsert(&mut transaction).await?;
|
||||
}
|
||||
|
||||
crate::database::models::User::clear_caches(
|
||||
&clear_cache_users
|
||||
.into_iter()
|
||||
.map(|x| (x, None))
|
||||
.collect::<Vec<_>>(),
|
||||
&redis,
|
||||
)
|
||||
.await?;
|
||||
transaction.commit().await?;
|
||||
|
||||
Ok::<(), ApiError>(())
|
||||
};
|
||||
|
||||
if let Err(e) = res.await {
|
||||
warn!("Error indexing billing queue: {:?}", e);
|
||||
clear_cache_users.push(user.id);
|
||||
}
|
||||
|
||||
info!("Done indexing billing queue");
|
||||
crate::database::models::User::clear_caches(
|
||||
&clear_cache_users
|
||||
.into_iter()
|
||||
.map(|x| (x, None))
|
||||
.collect::<Vec<_>>(),
|
||||
&redis,
|
||||
)
|
||||
.await?;
|
||||
transaction.commit().await?;
|
||||
|
||||
tokio::time::sleep(std::time::Duration::from_secs(60 * 5)).await;
|
||||
Ok::<(), ApiError>(())
|
||||
};
|
||||
|
||||
if let Err(e) = res.await {
|
||||
warn!("Error indexing subscriptions: {:?}", e);
|
||||
}
|
||||
|
||||
info!("Done indexing subscriptions");
|
||||
}
|
||||
|
||||
pub async fn task(
|
||||
pub async fn index_billing(
|
||||
stripe_client: stripe::Client,
|
||||
pool: PgPool,
|
||||
redis: RedisPool,
|
||||
) {
|
||||
loop {
|
||||
info!("Indexing billing queue");
|
||||
let res = async {
|
||||
// If a charge is open and due or has been attempted more than two days ago, it should be processed
|
||||
let charges_to_do =
|
||||
crate::database::models::charge_item::ChargeItem::get_chargeable(&pool).await?;
|
||||
|
||||
let prices = product_item::ProductPriceItem::get_many(
|
||||
&charges_to_do
|
||||
.iter()
|
||||
.map(|x| x.price_id)
|
||||
.collect::<HashSet<_>>()
|
||||
.into_iter()
|
||||
.collect::<Vec<_>>(),
|
||||
info!("Indexing billing queue");
|
||||
let res = async {
|
||||
// If a charge is open and due or has been attempted more than two days ago, it should be processed
|
||||
let charges_to_do =
|
||||
crate::database::models::charge_item::ChargeItem::get_chargeable(
|
||||
&pool,
|
||||
)
|
||||
.await?;
|
||||
|
||||
let users = crate::database::models::User::get_many_ids(
|
||||
&charges_to_do
|
||||
.iter()
|
||||
.map(|x| x.user_id)
|
||||
.collect::<HashSet<_>>()
|
||||
.into_iter()
|
||||
.collect::<Vec<_>>(),
|
||||
&pool,
|
||||
&redis,
|
||||
)
|
||||
.await?;
|
||||
let prices = product_item::ProductPriceItem::get_many(
|
||||
&charges_to_do
|
||||
.iter()
|
||||
.map(|x| x.price_id)
|
||||
.collect::<HashSet<_>>()
|
||||
.into_iter()
|
||||
.collect::<Vec<_>>(),
|
||||
&pool,
|
||||
)
|
||||
.await?;
|
||||
|
||||
let mut transaction = pool.begin().await?;
|
||||
let users = crate::database::models::User::get_many_ids(
|
||||
&charges_to_do
|
||||
.iter()
|
||||
.map(|x| x.user_id)
|
||||
.collect::<HashSet<_>>()
|
||||
.into_iter()
|
||||
.collect::<Vec<_>>(),
|
||||
&pool,
|
||||
&redis,
|
||||
)
|
||||
.await?;
|
||||
|
||||
for mut charge in charges_to_do {
|
||||
let product_price =
|
||||
if let Some(price) = prices.iter().find(|x| x.id == charge.price_id) {
|
||||
price
|
||||
let mut transaction = pool.begin().await?;
|
||||
|
||||
for mut charge in charges_to_do {
|
||||
let product_price = if let Some(price) =
|
||||
prices.iter().find(|x| x.id == charge.price_id)
|
||||
{
|
||||
price
|
||||
} else {
|
||||
continue;
|
||||
};
|
||||
|
||||
let user = if let Some(user) =
|
||||
users.iter().find(|x| x.id == charge.user_id)
|
||||
{
|
||||
user
|
||||
} else {
|
||||
continue;
|
||||
};
|
||||
|
||||
let price = match &product_price.prices {
|
||||
Price::OneTime { price } => Some(price),
|
||||
Price::Recurring { intervals } => {
|
||||
if let Some(ref interval) = charge.subscription_interval {
|
||||
intervals.get(interval)
|
||||
} else {
|
||||
warn!(
|
||||
"Could not find subscription for charge {:?}",
|
||||
charge.id
|
||||
);
|
||||
continue;
|
||||
};
|
||||
|
||||
let user = if let Some(user) = users.iter().find(|x| x.id == charge.user_id) {
|
||||
user
|
||||
} else {
|
||||
continue;
|
||||
};
|
||||
|
||||
let price = match &product_price.prices {
|
||||
Price::OneTime { price } => Some(price),
|
||||
Price::Recurring { intervals } => {
|
||||
if let Some(ref interval) = charge.subscription_interval {
|
||||
intervals.get(interval)
|
||||
} else {
|
||||
warn!("Could not find subscription for charge {:?}", charge.id);
|
||||
continue;
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
if let Some(price) = price {
|
||||
let customer_id = get_or_create_customer(
|
||||
user.id.into(),
|
||||
user.stripe_customer_id.as_deref(),
|
||||
user.email.as_deref(),
|
||||
&stripe_client,
|
||||
&pool,
|
||||
&redis,
|
||||
)
|
||||
.await?;
|
||||
|
||||
let customer =
|
||||
stripe::Customer::retrieve(&stripe_client, &customer_id, &[]).await?;
|
||||
|
||||
let currency =
|
||||
match Currency::from_str(&product_price.currency_code.to_lowercase()) {
|
||||
Ok(x) => x,
|
||||
Err(_) => {
|
||||
warn!(
|
||||
"Could not find currency for {}",
|
||||
product_price.currency_code
|
||||
);
|
||||
continue;
|
||||
}
|
||||
};
|
||||
|
||||
let mut intent = CreatePaymentIntent::new(*price as i64, currency);
|
||||
|
||||
let mut metadata = HashMap::new();
|
||||
metadata.insert(
|
||||
"modrinth_user_id".to_string(),
|
||||
to_base62(charge.user_id.0 as u64),
|
||||
);
|
||||
metadata.insert(
|
||||
"modrinth_charge_id".to_string(),
|
||||
to_base62(charge.id.0 as u64),
|
||||
);
|
||||
metadata.insert(
|
||||
"modrinth_charge_type".to_string(),
|
||||
charge.type_.as_str().to_string(),
|
||||
);
|
||||
|
||||
intent.metadata = Some(metadata);
|
||||
intent.customer = Some(customer.id);
|
||||
|
||||
if let Some(payment_method) = customer
|
||||
.invoice_settings
|
||||
.and_then(|x| x.default_payment_method.map(|x| x.id()))
|
||||
{
|
||||
intent.payment_method = Some(payment_method);
|
||||
intent.confirm = Some(true);
|
||||
intent.off_session = Some(PaymentIntentOffSession::Exists(true));
|
||||
|
||||
charge.status = ChargeStatus::Processing;
|
||||
|
||||
stripe::PaymentIntent::create(&stripe_client, intent).await?;
|
||||
} else {
|
||||
charge.status = ChargeStatus::Failed;
|
||||
charge.last_attempt = Some(Utc::now());
|
||||
}
|
||||
|
||||
charge.upsert(&mut transaction).await?;
|
||||
}
|
||||
};
|
||||
|
||||
if let Some(price) = price {
|
||||
let customer_id = get_or_create_customer(
|
||||
user.id.into(),
|
||||
user.stripe_customer_id.as_deref(),
|
||||
user.email.as_deref(),
|
||||
&stripe_client,
|
||||
&pool,
|
||||
&redis,
|
||||
)
|
||||
.await?;
|
||||
|
||||
let customer = stripe::Customer::retrieve(
|
||||
&stripe_client,
|
||||
&customer_id,
|
||||
&[],
|
||||
)
|
||||
.await?;
|
||||
|
||||
let currency = match Currency::from_str(
|
||||
&product_price.currency_code.to_lowercase(),
|
||||
) {
|
||||
Ok(x) => x,
|
||||
Err(_) => {
|
||||
warn!(
|
||||
"Could not find currency for {}",
|
||||
product_price.currency_code
|
||||
);
|
||||
continue;
|
||||
}
|
||||
};
|
||||
|
||||
let mut intent =
|
||||
CreatePaymentIntent::new(*price as i64, currency);
|
||||
|
||||
let mut metadata = HashMap::new();
|
||||
metadata.insert(
|
||||
"modrinth_user_id".to_string(),
|
||||
to_base62(charge.user_id.0 as u64),
|
||||
);
|
||||
metadata.insert(
|
||||
"modrinth_charge_id".to_string(),
|
||||
to_base62(charge.id.0 as u64),
|
||||
);
|
||||
metadata.insert(
|
||||
"modrinth_charge_type".to_string(),
|
||||
charge.type_.as_str().to_string(),
|
||||
);
|
||||
|
||||
intent.metadata = Some(metadata);
|
||||
intent.customer = Some(customer.id);
|
||||
|
||||
if let Some(payment_method) = customer
|
||||
.invoice_settings
|
||||
.and_then(|x| x.default_payment_method.map(|x| x.id()))
|
||||
{
|
||||
intent.payment_method = Some(payment_method);
|
||||
intent.confirm = Some(true);
|
||||
intent.off_session =
|
||||
Some(PaymentIntentOffSession::Exists(true));
|
||||
|
||||
charge.status = ChargeStatus::Processing;
|
||||
|
||||
stripe::PaymentIntent::create(&stripe_client, intent)
|
||||
.await?;
|
||||
} else {
|
||||
charge.status = ChargeStatus::Failed;
|
||||
charge.last_attempt = Some(Utc::now());
|
||||
}
|
||||
|
||||
charge.upsert(&mut transaction).await?;
|
||||
}
|
||||
|
||||
transaction.commit().await?;
|
||||
|
||||
Ok::<(), ApiError>(())
|
||||
}
|
||||
.await;
|
||||
|
||||
if let Err(e) = res {
|
||||
warn!("Error indexing billing queue: {:?}", e);
|
||||
}
|
||||
|
||||
info!("Done indexing billing queue");
|
||||
transaction.commit().await?;
|
||||
|
||||
tokio::time::sleep(std::time::Duration::from_secs(60 * 5)).await;
|
||||
Ok::<(), ApiError>(())
|
||||
}
|
||||
.await;
|
||||
|
||||
if let Err(e) = res {
|
||||
warn!("Error indexing billing queue: {:?}", e);
|
||||
}
|
||||
|
||||
info!("Done indexing billing queue");
|
||||
}
|
||||
|
||||
@@ -9,6 +9,10 @@ use crate::queue::socket::{
|
||||
ActiveSocket, ActiveSockets, SocketId, TunnelSocketType,
|
||||
};
|
||||
use crate::routes::ApiError;
|
||||
use crate::sync::friends::{RedisFriendsMessage, FRIENDS_CHANNEL_NAME};
|
||||
use crate::sync::status::{
|
||||
get_user_status, push_back_user_expiry, replace_user_status,
|
||||
};
|
||||
use actix_web::web::{Data, Payload};
|
||||
use actix_web::{get, web, HttpRequest, HttpResponse};
|
||||
use actix_ws::Message;
|
||||
@@ -19,10 +23,15 @@ use ariadne::networking::message::{
|
||||
use ariadne::users::UserStatus;
|
||||
use chrono::Utc;
|
||||
use either::Either;
|
||||
use futures_util::future::select;
|
||||
use futures_util::{StreamExt, TryStreamExt};
|
||||
use redis::AsyncCommands;
|
||||
use serde::Deserialize;
|
||||
use sqlx::PgPool;
|
||||
use std::pin::pin;
|
||||
use std::sync::atomic::Ordering;
|
||||
use tokio::sync::oneshot::error::TryRecvError;
|
||||
use tokio::time::{sleep, Duration};
|
||||
|
||||
pub fn config(cfg: &mut web::ServiceConfig) {
|
||||
cfg.service(ws_init);
|
||||
@@ -62,6 +71,7 @@ pub async fn ws_init(
|
||||
}
|
||||
|
||||
let user = User::from_full(db_user);
|
||||
let user_id = user.id;
|
||||
|
||||
let (res, mut session, msg_stream) = match actix_ws::handle(&req, body) {
|
||||
Ok(x) => x,
|
||||
@@ -79,19 +89,32 @@ pub async fn ws_init(
|
||||
.await?;
|
||||
|
||||
let friend_statuses = if !friends.is_empty() {
|
||||
friends
|
||||
.iter()
|
||||
.filter_map(|x| {
|
||||
db.get_status(
|
||||
if x.user_id == user.id.into() {
|
||||
x.friend_id
|
||||
} else {
|
||||
x.user_id
|
||||
let db = db.clone();
|
||||
let redis = redis.clone();
|
||||
tokio_stream::iter(friends.iter())
|
||||
.map(|x| {
|
||||
let db = db.clone();
|
||||
let redis = redis.clone();
|
||||
async move {
|
||||
async move {
|
||||
get_user_status(
|
||||
if x.user_id == user_id.into() {
|
||||
x.friend_id
|
||||
} else {
|
||||
x.user_id
|
||||
}
|
||||
.into(),
|
||||
&db,
|
||||
&redis,
|
||||
)
|
||||
.await
|
||||
}
|
||||
.into(),
|
||||
)
|
||||
}
|
||||
})
|
||||
.buffer_unordered(16)
|
||||
.filter_map(|x| x)
|
||||
.collect::<Vec<_>>()
|
||||
.await
|
||||
} else {
|
||||
Vec::new()
|
||||
};
|
||||
@@ -116,20 +139,42 @@ pub async fn ws_init(
|
||||
#[cfg(debug_assertions)]
|
||||
tracing::info!("Connection {socket_id} opened by {}", user.id);
|
||||
|
||||
broadcast_friends(
|
||||
user.id,
|
||||
ServerToClientMessage::StatusUpdate { status },
|
||||
&pool,
|
||||
&db,
|
||||
Some(friends),
|
||||
replace_user_status(None, Some(&status), &redis).await?;
|
||||
broadcast_friends_message(
|
||||
&redis,
|
||||
RedisFriendsMessage::StatusUpdate { status },
|
||||
)
|
||||
.await?;
|
||||
|
||||
let (shutdown_sender, mut shutdown_receiver) =
|
||||
tokio::sync::oneshot::channel::<()>();
|
||||
|
||||
{
|
||||
let db = db.clone();
|
||||
let redis = redis.clone();
|
||||
actix_web::rt::spawn(async move {
|
||||
while shutdown_receiver.try_recv() == Err(TryRecvError::Empty) {
|
||||
sleep(Duration::from_secs(30)).await;
|
||||
if let Some(socket) = db.sockets.get(&socket_id) {
|
||||
let _ = socket.socket.clone().ping(&[]).await;
|
||||
}
|
||||
let _ = push_back_user_expiry(user_id, &redis).await;
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
let mut stream = msg_stream.into_stream();
|
||||
|
||||
actix_web::rt::spawn(async move {
|
||||
// receive messages from websocket
|
||||
while let Some(msg) = stream.next().await {
|
||||
loop {
|
||||
let next = pin!(stream.next());
|
||||
let timeout = pin!(sleep(Duration::from_secs(30)));
|
||||
let futures_util::future::Either::Left((Some(msg), _)) =
|
||||
select(next, timeout).await
|
||||
else {
|
||||
break;
|
||||
};
|
||||
|
||||
let message = match msg {
|
||||
Ok(Message::Text(text)) => {
|
||||
ClientToServerMessage::deserialize(Either::Left(&text))
|
||||
@@ -139,10 +184,7 @@ pub async fn ws_init(
|
||||
ClientToServerMessage::deserialize(Either::Right(&bytes))
|
||||
}
|
||||
|
||||
Ok(Message::Close(_)) => {
|
||||
let _ = close_socket(socket_id, &pool, &db).await;
|
||||
continue;
|
||||
}
|
||||
Ok(Message::Close(_)) => break,
|
||||
|
||||
Ok(Message::Ping(msg)) => {
|
||||
if let Some(socket) = db.sockets.get(&socket_id) {
|
||||
@@ -162,8 +204,7 @@ pub async fn ws_init(
|
||||
#[cfg(debug_assertions)]
|
||||
if !message.is_binary() {
|
||||
tracing::info!(
|
||||
"Received message from {socket_id}: {:?}",
|
||||
message
|
||||
"Received message from {socket_id}: {message:?}"
|
||||
);
|
||||
}
|
||||
|
||||
@@ -172,6 +213,8 @@ pub async fn ws_init(
|
||||
if let Some(mut pair) = db.sockets.get_mut(&socket_id) {
|
||||
let ActiveSocket { status, .. } = pair.value_mut();
|
||||
|
||||
let old_status = status.clone();
|
||||
|
||||
if status
|
||||
.profile_name
|
||||
.as_ref()
|
||||
@@ -188,14 +231,17 @@ pub async fn ws_init(
|
||||
// We drop the pair to avoid holding the lock for too long
|
||||
drop(pair);
|
||||
|
||||
let _ = broadcast_friends(
|
||||
user.id,
|
||||
ServerToClientMessage::StatusUpdate {
|
||||
let _ = replace_user_status(
|
||||
Some(&old_status),
|
||||
Some(&user_status),
|
||||
&redis,
|
||||
)
|
||||
.await;
|
||||
let _ = broadcast_friends_message(
|
||||
&redis,
|
||||
RedisFriendsMessage::StatusUpdate {
|
||||
status: user_status,
|
||||
},
|
||||
&pool,
|
||||
&db,
|
||||
None,
|
||||
)
|
||||
.await;
|
||||
}
|
||||
@@ -247,12 +293,11 @@ pub async fn ws_init(
|
||||
};
|
||||
match tunnel_socket.socket_type {
|
||||
TunnelSocketType::Listening => {
|
||||
let _ = broadcast_friends(
|
||||
let _ = broadcast_to_local_friends(
|
||||
user.id,
|
||||
ServerToClientMessage::FriendSocketStoppedListening { user: user.id },
|
||||
&pool,
|
||||
&db,
|
||||
None,
|
||||
)
|
||||
.await;
|
||||
}
|
||||
@@ -308,25 +353,48 @@ pub async fn ws_init(
|
||||
}
|
||||
}
|
||||
|
||||
let _ = close_socket(socket_id, &pool, &db).await;
|
||||
let _ = shutdown_sender.send(());
|
||||
let _ = close_socket(socket_id, &pool, &db, &redis).await;
|
||||
});
|
||||
|
||||
Ok(res)
|
||||
}
|
||||
|
||||
pub async fn broadcast_friends(
|
||||
pub async fn broadcast_friends_message(
|
||||
redis: &RedisPool,
|
||||
message: RedisFriendsMessage,
|
||||
) -> Result<(), crate::database::models::DatabaseError> {
|
||||
let _: () = redis
|
||||
.pool
|
||||
.get()
|
||||
.await?
|
||||
.publish(FRIENDS_CHANNEL_NAME, message)
|
||||
.await?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub async fn broadcast_to_local_friends(
|
||||
user_id: UserId,
|
||||
message: ServerToClientMessage,
|
||||
pool: &PgPool,
|
||||
sockets: &ActiveSockets,
|
||||
friends: Option<Vec<FriendItem>>,
|
||||
) -> Result<(), crate::database::models::DatabaseError> {
|
||||
broadcast_to_known_local_friends(
|
||||
user_id,
|
||||
message,
|
||||
sockets,
|
||||
FriendItem::get_user_friends(user_id.into(), Some(true), pool).await?,
|
||||
)
|
||||
.await
|
||||
}
|
||||
|
||||
async fn broadcast_to_known_local_friends(
|
||||
user_id: UserId,
|
||||
message: ServerToClientMessage,
|
||||
sockets: &ActiveSockets,
|
||||
friends: Vec<FriendItem>,
|
||||
) -> Result<(), crate::database::models::DatabaseError> {
|
||||
// FIXME Probably shouldn't be using database errors for this. Maybe ApiError?
|
||||
let friends = if let Some(friends) = friends {
|
||||
friends
|
||||
} else {
|
||||
FriendItem::get_user_friends(user_id.into(), Some(true), pool).await?
|
||||
};
|
||||
|
||||
for friend in friends {
|
||||
let friend_id = if friend.user_id == user_id.into() {
|
||||
@@ -387,6 +455,7 @@ pub async fn close_socket(
|
||||
id: SocketId,
|
||||
pool: &PgPool,
|
||||
db: &ActiveSockets,
|
||||
redis: &RedisPool,
|
||||
) -> Result<(), crate::database::models::DatabaseError> {
|
||||
if let Some((_, socket)) = db.sockets.remove(&id) {
|
||||
let user_id = socket.status.user_id;
|
||||
@@ -397,12 +466,10 @@ pub async fn close_socket(
|
||||
|
||||
let _ = socket.socket.close(None).await;
|
||||
|
||||
broadcast_friends(
|
||||
user_id,
|
||||
ServerToClientMessage::UserOffline { id: user_id },
|
||||
pool,
|
||||
db,
|
||||
None,
|
||||
replace_user_status(Some(&socket.status), None, redis).await?;
|
||||
broadcast_friends_message(
|
||||
redis,
|
||||
RedisFriendsMessage::UserOffline { user: user_id },
|
||||
)
|
||||
.await?;
|
||||
|
||||
@@ -414,14 +481,13 @@ pub async fn close_socket(
|
||||
};
|
||||
match tunnel_socket.socket_type {
|
||||
TunnelSocketType::Listening => {
|
||||
let _ = broadcast_friends(
|
||||
let _ = broadcast_to_local_friends(
|
||||
user_id,
|
||||
ServerToClientMessage::SocketClosed {
|
||||
socket: owned_socket,
|
||||
},
|
||||
pool,
|
||||
db,
|
||||
None,
|
||||
)
|
||||
.await;
|
||||
}
|
||||
|
||||
@@ -95,6 +95,8 @@ pub enum ApiError {
|
||||
Database(#[from] crate::database::models::DatabaseError),
|
||||
#[error("Database Error: {0}")]
|
||||
SqlxDatabase(#[from] sqlx::Error),
|
||||
#[error("Database Error: {0}")]
|
||||
RedisDatabase(#[from] redis::RedisError),
|
||||
#[error("Clickhouse Error: {0}")]
|
||||
Clickhouse(#[from] clickhouse::error::Error),
|
||||
#[error("Internal server error: {0}")]
|
||||
@@ -148,8 +150,9 @@ impl ApiError {
|
||||
crate::models::error::ApiError {
|
||||
error: match self {
|
||||
ApiError::Env(..) => "environment_error",
|
||||
ApiError::SqlxDatabase(..) => "database_error",
|
||||
ApiError::Database(..) => "database_error",
|
||||
ApiError::SqlxDatabase(..) => "database_error",
|
||||
ApiError::RedisDatabase(..) => "database_error",
|
||||
ApiError::Authentication(..) => "unauthorized",
|
||||
ApiError::CustomAuthentication(..) => "unauthorized",
|
||||
ApiError::Xml(..) => "xml_error",
|
||||
@@ -186,6 +189,7 @@ impl actix_web::ResponseError for ApiError {
|
||||
ApiError::Env(..) => StatusCode::INTERNAL_SERVER_ERROR,
|
||||
ApiError::Database(..) => StatusCode::INTERNAL_SERVER_ERROR,
|
||||
ApiError::SqlxDatabase(..) => StatusCode::INTERNAL_SERVER_ERROR,
|
||||
ApiError::RedisDatabase(..) => StatusCode::INTERNAL_SERVER_ERROR,
|
||||
ApiError::Clickhouse(..) => StatusCode::INTERNAL_SERVER_ERROR,
|
||||
ApiError::Authentication(..) => StatusCode::UNAUTHORIZED,
|
||||
ApiError::CustomAuthentication(..) => StatusCode::UNAUTHORIZED,
|
||||
|
||||
@@ -5,8 +5,12 @@ use crate::models::pats::Scopes;
|
||||
use crate::models::users::UserFriend;
|
||||
use crate::queue::session::AuthQueue;
|
||||
use crate::queue::socket::ActiveSockets;
|
||||
use crate::routes::internal::statuses::send_message_to_user;
|
||||
use crate::routes::internal::statuses::{
|
||||
broadcast_friends_message, send_message_to_user,
|
||||
};
|
||||
use crate::routes::ApiError;
|
||||
use crate::sync::friends::RedisFriendsMessage;
|
||||
use crate::sync::status::get_user_status;
|
||||
use actix_web::{delete, get, post, web, HttpRequest, HttpResponse};
|
||||
use ariadne::networking::message::ServerToClientMessage;
|
||||
use chrono::Utc;
|
||||
@@ -76,14 +80,16 @@ pub async fn add_friend(
|
||||
user_id: UserId,
|
||||
friend_id: UserId,
|
||||
sockets: &ActiveSockets,
|
||||
redis: &RedisPool,
|
||||
) -> Result<(), ApiError> {
|
||||
if let Some(friend_status) = sockets.get_status(user_id.into())
|
||||
if let Some(friend_status) =
|
||||
get_user_status(user_id.into(), sockets, redis).await
|
||||
{
|
||||
send_message_to_user(
|
||||
sockets,
|
||||
friend_id.into(),
|
||||
&ServerToClientMessage::StatusUpdate {
|
||||
status: friend_status.clone(),
|
||||
broadcast_friends_message(
|
||||
redis,
|
||||
RedisFriendsMessage::DirectStatusUpdate {
|
||||
to_user: friend_id.into(),
|
||||
status: friend_status,
|
||||
},
|
||||
)
|
||||
.await?;
|
||||
@@ -92,8 +98,10 @@ pub async fn add_friend(
|
||||
Ok(())
|
||||
}
|
||||
|
||||
send_friend_status(friend.user_id, friend.friend_id, &db).await?;
|
||||
send_friend_status(friend.friend_id, friend.user_id, &db).await?;
|
||||
send_friend_status(friend.user_id, friend.friend_id, &db, &redis)
|
||||
.await?;
|
||||
send_friend_status(friend.friend_id, friend.user_id, &db, &redis)
|
||||
.await?;
|
||||
} else {
|
||||
if friend.id == user.id.into() {
|
||||
return Err(ApiError::InvalidInput(
|
||||
|
||||
Reference in New Issue
Block a user