mirror of
https://github.com/modrinth/code.git
synced 2026-08-02 14:15:53 +00:00
Compare commits
9
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
9f4b433bd5 | ||
|
|
4612b8ddae | ||
|
|
796456e6bd | ||
|
|
b82811843a | ||
|
|
47e31db01f | ||
|
|
fca4b2135a | ||
|
|
3340bf368e | ||
|
|
6b569e4ee4 | ||
|
|
6b62e3b480 |
Generated
+44
@@ -2133,6 +2133,15 @@ dependencies = [
|
||||
"cc",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "cobs"
|
||||
version = "0.3.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "0fa961b519f0b462e3a3b4a34b64d119eeaca1d59af726fe450bbba07a9fc0a1"
|
||||
dependencies = [
|
||||
"thiserror 2.0.17",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "color-eyre"
|
||||
version = "0.6.5"
|
||||
@@ -3259,6 +3268,18 @@ version = "1.2.2"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "4ef6b89e5b37196644d8796de5268852ff179b44e96276cf4290264843743bb7"
|
||||
|
||||
[[package]]
|
||||
name = "embedded-io"
|
||||
version = "0.4.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "ef1a6892d9eef45c8fa6b9e0086428a2cca8491aca8f787c534a3d6d0bcb3ced"
|
||||
|
||||
[[package]]
|
||||
name = "embedded-io"
|
||||
version = "0.6.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "edd0f118536f44f5ccd48bcb8b111bdc3de888b58c74639dfb034a357d0f206d"
|
||||
|
||||
[[package]]
|
||||
name = "encode_unicode"
|
||||
version = "1.0.0"
|
||||
@@ -5440,13 +5461,16 @@ dependencies = [
|
||||
"json-patch 4.1.0",
|
||||
"labrinth",
|
||||
"lettre",
|
||||
"lz4_flex",
|
||||
"meilisearch-sdk",
|
||||
"modrinth-content-management",
|
||||
"modrinth-util",
|
||||
"muralpay",
|
||||
"murmur2",
|
||||
"neverbounce",
|
||||
"paste",
|
||||
"path-util",
|
||||
"postcard",
|
||||
"prometheus",
|
||||
"quick-xml 0.38.3",
|
||||
"rand 0.8.5",
|
||||
@@ -6224,6 +6248,14 @@ dependencies = [
|
||||
"jni-sys",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "neverbounce"
|
||||
version = "0.1.0"
|
||||
dependencies = [
|
||||
"reqwest 0.12.24",
|
||||
"serde",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "new_debug_unreachable"
|
||||
version = "1.0.6"
|
||||
@@ -7492,6 +7524,18 @@ version = "1.11.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "f84267b20a16ea918e43c6a88433c2d54fa145c92a811b5b047ccbe153674483"
|
||||
|
||||
[[package]]
|
||||
name = "postcard"
|
||||
version = "1.1.3"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "6764c3b5dd454e283a30e6dfe78e9b31096d9e32036b5d1eaac7a6119ccb9a24"
|
||||
dependencies = [
|
||||
"cobs",
|
||||
"embedded-io 0.4.0",
|
||||
"embedded-io 0.6.1",
|
||||
"serde",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "potential_utf"
|
||||
version = "0.1.3"
|
||||
|
||||
@@ -13,6 +13,7 @@ members = [
|
||||
"packages/modrinth-log",
|
||||
"packages/modrinth-maxmind",
|
||||
"packages/modrinth-util",
|
||||
"packages/neverbounce",
|
||||
"packages/path-util",
|
||||
]
|
||||
|
||||
@@ -118,6 +119,12 @@ lettre = { version = "0.11.19", default-features = false, features = [
|
||||
"tokio1",
|
||||
"tokio1-rustls",
|
||||
] }
|
||||
lz4_flex = { version = "0.11.5", default-features = false, features = [
|
||||
"checked-decode",
|
||||
"safe-decode",
|
||||
"safe-encode",
|
||||
"std",
|
||||
] }
|
||||
maxminddb = "0.26.0"
|
||||
meilisearch-sdk = { version = "0.30.0", default-features = false }
|
||||
modrinth-content-management = { path = "packages/modrinth-content-management" }
|
||||
@@ -126,6 +133,7 @@ modrinth-util = { path = "packages/modrinth-util" }
|
||||
muralpay = { path = "packages/muralpay" }
|
||||
murmur2 = "0.1.0"
|
||||
native-dialog = "0.9.2"
|
||||
neverbounce = { path = "packages/neverbounce" }
|
||||
notify = { version = "8.2.0", default-features = false }
|
||||
notify-debouncer-mini = { version = "0.7.0", default-features = false }
|
||||
objc2-app-kit = { version = "0.3.2", default-features = false }
|
||||
@@ -135,6 +143,7 @@ paste = "1.0.15"
|
||||
path-util = { path = "packages/path-util" }
|
||||
phf = { version = "0.13.1", features = ["macros"] }
|
||||
png = "0.18.0"
|
||||
postcard = { version = "1.1.3", default-features = false, features = ["alloc"] }
|
||||
proc-macro2 = { version = "1.0" }
|
||||
prometheus = "0.14.0"
|
||||
quartz_nbt = "0.2.9"
|
||||
|
||||
@@ -73,13 +73,16 @@ image = { workspace = true, features = [
|
||||
itertools = { workspace = true }
|
||||
json-patch = { workspace = true }
|
||||
lettre = { workspace = true }
|
||||
lz4_flex = { workspace = true }
|
||||
meilisearch-sdk = { workspace = true, features = ["reqwest"] }
|
||||
modrinth-content-management = { workspace = true }
|
||||
modrinth-util = { workspace = true, features = ["decimal", "sentry", "utoipa"] }
|
||||
muralpay = { workspace = true, features = ["client", "mock", "utoipa"] }
|
||||
murmur2 = { workspace = true }
|
||||
neverbounce = { workspace = true }
|
||||
paste = { workspace = true }
|
||||
path-util = { workspace = true }
|
||||
postcard = { workspace = true }
|
||||
prometheus = { workspace = true }
|
||||
quick-xml = { workspace = true }
|
||||
rand = { workspace = true }
|
||||
|
||||
@@ -11,7 +11,7 @@ use crate::{
|
||||
};
|
||||
use serde::{Deserialize, Serialize};
|
||||
|
||||
const ANALYTICS_EVENTS_NAMESPACE: &str = "analytics_events";
|
||||
const ANALYTICS_EVENTS_NAMESPACE: &str = "analytics_events:v1";
|
||||
const ANALYTICS_EVENTS_ALL_KEY: &str = "all";
|
||||
|
||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||
@@ -88,7 +88,7 @@ impl DBAnalyticsEvent {
|
||||
let mut redis = redis.connect().await?;
|
||||
|
||||
if let Some(events) = redis
|
||||
.get_deserialized_from_json(
|
||||
.get_deserialized(
|
||||
ANALYTICS_EVENTS_NAMESPACE,
|
||||
ANALYTICS_EVENTS_ALL_KEY,
|
||||
)
|
||||
@@ -119,7 +119,7 @@ impl DBAnalyticsEvent {
|
||||
.await?;
|
||||
|
||||
redis
|
||||
.set_serialized_to_json(
|
||||
.set_serialized(
|
||||
ANALYTICS_EVENTS_NAMESPACE,
|
||||
ANALYTICS_EVENTS_ALL_KEY,
|
||||
&events,
|
||||
|
||||
@@ -7,7 +7,7 @@ use super::ids::*;
|
||||
use futures::TryStreamExt;
|
||||
use serde::{Deserialize, Serialize};
|
||||
|
||||
const TAGS_NAMESPACE: &str = "tags";
|
||||
const TAGS_NAMESPACE: &str = "tags:v1";
|
||||
|
||||
pub struct ProjectType {
|
||||
pub id: ProjectTypeId,
|
||||
@@ -96,9 +96,8 @@ impl Category {
|
||||
{
|
||||
let mut redis = redis.connect().await?;
|
||||
|
||||
let res: Option<Vec<Category>> = redis
|
||||
.get_deserialized_from_json(TAGS_NAMESPACE, "category")
|
||||
.await?;
|
||||
let res: Option<Vec<Category>> =
|
||||
redis.get_deserialized(TAGS_NAMESPACE, "category").await?;
|
||||
|
||||
if let Some(res) = res {
|
||||
return Ok(res);
|
||||
@@ -127,7 +126,7 @@ impl Category {
|
||||
let mut redis = redis.connect().await?;
|
||||
|
||||
redis
|
||||
.set_serialized_to_json(TAGS_NAMESPACE, "category", &result, None)
|
||||
.set_serialized(TAGS_NAMESPACE, "category", &result, None)
|
||||
.await?;
|
||||
|
||||
Ok(result)
|
||||
@@ -166,7 +165,7 @@ impl LinkPlatform {
|
||||
let mut redis = redis.connect().await?;
|
||||
|
||||
let res: Option<Vec<LinkPlatform>> = redis
|
||||
.get_deserialized_from_json(TAGS_NAMESPACE, "link_platform")
|
||||
.get_deserialized(TAGS_NAMESPACE, "link_platform")
|
||||
.await?;
|
||||
|
||||
if let Some(res) = res {
|
||||
@@ -191,12 +190,7 @@ impl LinkPlatform {
|
||||
let mut redis = redis.connect().await?;
|
||||
|
||||
redis
|
||||
.set_serialized_to_json(
|
||||
TAGS_NAMESPACE,
|
||||
"link_platform",
|
||||
&result,
|
||||
None,
|
||||
)
|
||||
.set_serialized(TAGS_NAMESPACE, "link_platform", &result, None)
|
||||
.await?;
|
||||
|
||||
Ok(result)
|
||||
@@ -235,7 +229,7 @@ impl ReportType {
|
||||
let mut redis = redis.connect().await?;
|
||||
|
||||
let res: Option<Vec<String>> = redis
|
||||
.get_deserialized_from_json(TAGS_NAMESPACE, "report_type")
|
||||
.get_deserialized(TAGS_NAMESPACE, "report_type")
|
||||
.await?;
|
||||
|
||||
if let Some(res) = res {
|
||||
@@ -256,12 +250,7 @@ impl ReportType {
|
||||
let mut redis = redis.connect().await?;
|
||||
|
||||
redis
|
||||
.set_serialized_to_json(
|
||||
TAGS_NAMESPACE,
|
||||
"report_type",
|
||||
&result,
|
||||
None,
|
||||
)
|
||||
.set_serialized(TAGS_NAMESPACE, "report_type", &result, None)
|
||||
.await?;
|
||||
|
||||
Ok(result)
|
||||
@@ -300,7 +289,7 @@ impl ProjectType {
|
||||
let mut redis = redis.connect().await?;
|
||||
|
||||
let res: Option<Vec<String>> = redis
|
||||
.get_deserialized_from_json(TAGS_NAMESPACE, "project_type")
|
||||
.get_deserialized(TAGS_NAMESPACE, "project_type")
|
||||
.await?;
|
||||
|
||||
if let Some(res) = res {
|
||||
@@ -321,12 +310,7 @@ impl ProjectType {
|
||||
let mut redis = redis.connect().await?;
|
||||
|
||||
redis
|
||||
.set_serialized_to_json(
|
||||
TAGS_NAMESPACE,
|
||||
"project_type",
|
||||
&result,
|
||||
None,
|
||||
)
|
||||
.set_serialized(TAGS_NAMESPACE, "project_type", &result, None)
|
||||
.await?;
|
||||
|
||||
Ok(result)
|
||||
|
||||
@@ -8,7 +8,7 @@ use dashmap::DashMap;
|
||||
use futures::TryStreamExt;
|
||||
use serde::{Deserialize, Serialize};
|
||||
|
||||
const COLLECTIONS_NAMESPACE: &str = "collections";
|
||||
const COLLECTIONS_NAMESPACE: &str = "collections:v1";
|
||||
|
||||
#[derive(Clone)]
|
||||
pub struct CollectionBuilder {
|
||||
|
||||
@@ -13,10 +13,9 @@ use serde::{Deserialize, Serialize};
|
||||
use url::Url;
|
||||
use webauthn_rs::prelude::{DiscoverableAuthentication, PasskeyRegistration};
|
||||
|
||||
const FLOWS_NAMESPACE: &str = "flows";
|
||||
const FLOWS_NAMESPACE: &str = "flows:v1";
|
||||
|
||||
#[derive(Deserialize, Serialize)]
|
||||
#[serde(tag = "type", rename_all = "snake_case")]
|
||||
pub enum DBFlow {
|
||||
OAuth {
|
||||
user_id: Option<DBUserId>,
|
||||
@@ -78,7 +77,7 @@ impl DBFlow {
|
||||
let mut redis = redis.connect().await?;
|
||||
|
||||
redis
|
||||
.set_serialized_to_json(
|
||||
.set_serialized(
|
||||
FLOWS_NAMESPACE,
|
||||
&state,
|
||||
&self,
|
||||
@@ -109,7 +108,7 @@ impl DBFlow {
|
||||
) -> Result<Option<DBFlow>, DatabaseError> {
|
||||
let mut redis = redis.connect().await?;
|
||||
|
||||
redis.get_deserialized_from_json(FLOWS_NAMESPACE, id).await
|
||||
redis.get_deserialized(FLOWS_NAMESPACE, id).await
|
||||
}
|
||||
|
||||
/// Gets the flow and removes it from the cache, but only removes if the flow was present and the predicate returned true
|
||||
|
||||
@@ -6,7 +6,7 @@ use chrono::{DateTime, Utc};
|
||||
use dashmap::DashMap;
|
||||
use serde::{Deserialize, Serialize};
|
||||
|
||||
const IMAGES_NAMESPACE: &str = "images";
|
||||
const IMAGES_NAMESPACE: &str = "images:v1";
|
||||
|
||||
#[derive(Clone, Debug, Serialize, Deserialize)]
|
||||
pub struct DBImage {
|
||||
|
||||
@@ -12,13 +12,14 @@ use futures::TryStreamExt;
|
||||
use itertools::Itertools;
|
||||
use serde::{Deserialize, Serialize};
|
||||
|
||||
const GAMES_LIST_NAMESPACE: &str = "games";
|
||||
const LOADER_ID: &str = "loader_id";
|
||||
const LOADERS_LIST_NAMESPACE: &str = "loaders";
|
||||
const LOADER_FIELDS_NAMESPACE: &str = "loader_fields";
|
||||
const LOADER_FIELDS_NAMESPACE_ALL: &str = "loader_fields_all";
|
||||
const LOADER_FIELD_ENUMS_ID_NAMESPACE: &str = "loader_field_enums";
|
||||
pub const LOADER_FIELD_ENUM_VALUES_NAMESPACE: &str = "loader_field_enum_values";
|
||||
const GAMES_LIST_NAMESPACE: &str = "games:v1";
|
||||
const LOADER_ID: &str = "loader_id:v1";
|
||||
const LOADERS_LIST_NAMESPACE: &str = "loaders:v1";
|
||||
const LOADER_FIELDS_NAMESPACE: &str = "loader_fields:v1";
|
||||
const LOADER_FIELDS_NAMESPACE_ALL: &str = "loader_fields_all:v1";
|
||||
const LOADER_FIELD_ENUMS_ID_NAMESPACE: &str = "loader_field_enums:v1";
|
||||
pub const LOADER_FIELD_ENUM_VALUES_NAMESPACE: &str =
|
||||
"loader_field_enum_values:v1";
|
||||
|
||||
#[derive(Clone, Serialize, Deserialize, Debug)]
|
||||
pub struct Game {
|
||||
@@ -54,7 +55,7 @@ impl Game {
|
||||
{
|
||||
let mut redis = redis.connect().await?;
|
||||
let cached_games: Option<Vec<Game>> = redis
|
||||
.get_deserialized_from_json(GAMES_LIST_NAMESPACE, "games")
|
||||
.get_deserialized(GAMES_LIST_NAMESPACE, "games")
|
||||
.await?;
|
||||
if let Some(cached_games) = cached_games {
|
||||
return Ok(cached_games);
|
||||
@@ -80,12 +81,7 @@ impl Game {
|
||||
let mut redis = redis.connect().await?;
|
||||
|
||||
redis
|
||||
.set_serialized_to_json(
|
||||
GAMES_LIST_NAMESPACE,
|
||||
"games",
|
||||
&result,
|
||||
None,
|
||||
)
|
||||
.set_serialized(GAMES_LIST_NAMESPACE, "games", &result, None)
|
||||
.await?;
|
||||
|
||||
Ok(result)
|
||||
@@ -114,7 +110,7 @@ impl Loader {
|
||||
{
|
||||
let mut redis = redis.connect().await?;
|
||||
let cached_id: Option<i32> =
|
||||
redis.get_deserialized_from_json(LOADER_ID, name).await?;
|
||||
redis.get_deserialized(LOADER_ID, name).await?;
|
||||
if let Some(cached_id) = cached_id {
|
||||
return Ok(Some(LoaderId(cached_id)));
|
||||
}
|
||||
@@ -134,7 +130,7 @@ impl Loader {
|
||||
if let Some(result) = result {
|
||||
let mut redis = redis.connect().await?;
|
||||
redis
|
||||
.set_serialized_to_json(LOADER_ID, name, &result.0, None)
|
||||
.set_serialized(LOADER_ID, name, &result.0, None)
|
||||
.await?;
|
||||
}
|
||||
|
||||
@@ -151,7 +147,7 @@ impl Loader {
|
||||
{
|
||||
let mut redis = redis.connect().await?;
|
||||
let cached_loaders: Option<Vec<Loader>> = redis
|
||||
.get_deserialized_from_json(LOADERS_LIST_NAMESPACE, "all")
|
||||
.get_deserialized(LOADERS_LIST_NAMESPACE, "all")
|
||||
.await?;
|
||||
if let Some(cached_loaders) = cached_loaders {
|
||||
return Ok(cached_loaders);
|
||||
@@ -193,12 +189,7 @@ impl Loader {
|
||||
let mut redis = redis.connect().await?;
|
||||
|
||||
redis
|
||||
.set_serialized_to_json(
|
||||
LOADERS_LIST_NAMESPACE,
|
||||
"all",
|
||||
&result,
|
||||
None,
|
||||
)
|
||||
.set_serialized(LOADERS_LIST_NAMESPACE, "all", &result, None)
|
||||
.await?;
|
||||
|
||||
Ok(result)
|
||||
@@ -470,10 +461,9 @@ impl LoaderField {
|
||||
{
|
||||
let mut redis = redis.connect().await?;
|
||||
|
||||
let cached_fields: Option<Vec<LoaderField>> =
|
||||
redis.get(LOADER_FIELDS_NAMESPACE_ALL, "").await?.and_then(
|
||||
|x| serde_json::from_str::<Vec<LoaderField>>(&x).ok(),
|
||||
);
|
||||
let cached_fields: Option<Vec<LoaderField>> = redis
|
||||
.get_deserialized(LOADER_FIELDS_NAMESPACE_ALL, "")
|
||||
.await?;
|
||||
|
||||
if let Some(cached_fields) = cached_fields {
|
||||
return Ok(cached_fields);
|
||||
@@ -506,12 +496,7 @@ impl LoaderField {
|
||||
let mut redis = redis.connect().await?;
|
||||
|
||||
redis
|
||||
.set_serialized_to_json(
|
||||
LOADER_FIELDS_NAMESPACE_ALL,
|
||||
"",
|
||||
&result,
|
||||
None,
|
||||
)
|
||||
.set_serialized(LOADER_FIELDS_NAMESPACE_ALL, "", &result, None)
|
||||
.await?;
|
||||
|
||||
Ok(result)
|
||||
@@ -530,10 +515,7 @@ impl LoaderFieldEnum {
|
||||
let mut redis = redis.connect().await?;
|
||||
|
||||
let cached_enum = redis
|
||||
.get_deserialized_from_json(
|
||||
LOADER_FIELD_ENUMS_ID_NAMESPACE,
|
||||
enum_name,
|
||||
)
|
||||
.get_deserialized(LOADER_FIELD_ENUMS_ID_NAMESPACE, enum_name)
|
||||
.await?;
|
||||
if let Some(cached_enum) = cached_enum {
|
||||
return Ok(cached_enum);
|
||||
@@ -561,7 +543,7 @@ impl LoaderFieldEnum {
|
||||
let mut redis = redis.connect().await?;
|
||||
|
||||
redis
|
||||
.set_serialized_to_json(
|
||||
.set_serialized(
|
||||
LOADER_FIELD_ENUMS_ID_NAMESPACE,
|
||||
enum_name,
|
||||
&result,
|
||||
|
||||
@@ -76,6 +76,8 @@ pub enum DatabaseError {
|
||||
RedisPool(#[from] deadpool_redis::PoolError),
|
||||
#[error("Error while serializing with the cache: {0}")]
|
||||
SerdeCacheError(#[from] serde_json::Error),
|
||||
#[error("error while encoding or decoding the cache: {0}")]
|
||||
PostcardCacheError(#[from] postcard::Error),
|
||||
#[error("Schema error: {0}")]
|
||||
SchemaError(String),
|
||||
#[error(
|
||||
|
||||
@@ -7,9 +7,9 @@ use crate::database::redis::RedisPool;
|
||||
|
||||
use super::{DBOrganizationId, DBUserId, DatabaseError};
|
||||
|
||||
const MODERATION_NOTES_USERS_NAMESPACE: &str = "moderation_notes_users";
|
||||
const MODERATION_NOTES_USERS_NAMESPACE: &str = "moderation_notes_users:v1";
|
||||
const MODERATION_NOTES_ORGANIZATIONS_NAMESPACE: &str =
|
||||
"moderation_notes_organizations";
|
||||
"moderation_notes_organizations:v1";
|
||||
|
||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||
pub struct DBModerationNote {
|
||||
@@ -40,7 +40,7 @@ impl DBModerationNote {
|
||||
let cached = {
|
||||
let mut redis = redis.connect().await?;
|
||||
redis
|
||||
.get_many_deserialized_from_json::<Self>(
|
||||
.get_many_deserialized::<Self>(
|
||||
MODERATION_NOTES_USERS_NAMESPACE,
|
||||
&ids,
|
||||
)
|
||||
@@ -87,7 +87,7 @@ impl DBModerationNote {
|
||||
|
||||
if let Some(user_id) = note.user_id {
|
||||
redis
|
||||
.set_serialized_to_json(
|
||||
.set_serialized(
|
||||
MODERATION_NOTES_USERS_NAMESPACE,
|
||||
user_id.0,
|
||||
¬e,
|
||||
@@ -130,7 +130,7 @@ impl DBModerationNote {
|
||||
let cached = {
|
||||
let mut redis = redis.connect().await?;
|
||||
redis
|
||||
.get_many_deserialized_from_json::<Self>(
|
||||
.get_many_deserialized::<Self>(
|
||||
MODERATION_NOTES_ORGANIZATIONS_NAMESPACE,
|
||||
&ids,
|
||||
)
|
||||
@@ -177,7 +177,7 @@ impl DBModerationNote {
|
||||
|
||||
if let Some(organization_id) = note.organization_id {
|
||||
redis
|
||||
.set_serialized_to_json(
|
||||
.set_serialized(
|
||||
MODERATION_NOTES_ORGANIZATIONS_NAMESPACE,
|
||||
organization_id.0,
|
||||
¬e,
|
||||
|
||||
@@ -9,7 +9,7 @@ use chrono::{DateTime, Utc};
|
||||
use futures::TryStreamExt;
|
||||
use serde::{Deserialize, Serialize};
|
||||
|
||||
const USER_NOTIFICATIONS_NAMESPACE: &str = "user_notifications";
|
||||
const USER_NOTIFICATIONS_NAMESPACE: &str = "user_notifications:v1";
|
||||
|
||||
pub struct NotificationBuilder {
|
||||
pub body: NotificationBody,
|
||||
@@ -435,7 +435,7 @@ impl DBNotification {
|
||||
let mut redis = redis.connect().await?;
|
||||
|
||||
let cached_notifications: Option<Vec<DBNotification>> = redis
|
||||
.get_deserialized_from_json(
|
||||
.get_deserialized(
|
||||
USER_NOTIFICATIONS_NAMESPACE,
|
||||
&user_id.0.to_string(),
|
||||
)
|
||||
@@ -493,7 +493,7 @@ impl DBNotification {
|
||||
let mut redis = redis.connect().await?;
|
||||
|
||||
redis
|
||||
.set_serialized_to_json(
|
||||
.set_serialized(
|
||||
USER_NOTIFICATIONS_NAMESPACE,
|
||||
user_id.0,
|
||||
&db_notifications,
|
||||
|
||||
@@ -4,10 +4,11 @@ use crate::models::v3::notifications::{NotificationChannel, NotificationType};
|
||||
use crate::routes::ApiError;
|
||||
use serde::{Deserialize, Serialize};
|
||||
|
||||
const TEMPLATES_NAMESPACE: &str = "notifications_templates";
|
||||
const TEMPLATES_HTML_DATA_NAMESPACE: &str = "notifications_templates_html_data";
|
||||
const TEMPLATES_NAMESPACE: &str = "notifications_templates:v1";
|
||||
const TEMPLATES_HTML_DATA_NAMESPACE: &str =
|
||||
"notifications_templates_html_data:v1";
|
||||
const TEMPLATES_DYNAMIC_HTML_NAMESPACE: &str =
|
||||
"notifications_templates_dynamic_html";
|
||||
"notifications_templates_dynamic_html:v1";
|
||||
|
||||
const HTML_DATA_CACHE_EXPIRY: i64 = 60 * 15; // 15 minutes
|
||||
const TEMPLATES_CACHE_EXPIRY: i64 = 60 * 30; // 30 minutes
|
||||
@@ -56,10 +57,7 @@ impl NotificationTemplate {
|
||||
let mut redis = redis.connect().await?;
|
||||
|
||||
let maybe_cached_templates = redis
|
||||
.get_deserialized_from_json(
|
||||
TEMPLATES_NAMESPACE,
|
||||
channel.as_str(),
|
||||
)
|
||||
.get_deserialized(TEMPLATES_NAMESPACE, channel.as_str())
|
||||
.await?;
|
||||
|
||||
if let Some(cached) = maybe_cached_templates {
|
||||
@@ -82,7 +80,7 @@ impl NotificationTemplate {
|
||||
let mut redis = redis.connect().await?;
|
||||
|
||||
redis
|
||||
.set_serialized_to_json(
|
||||
.set_serialized(
|
||||
TEMPLATES_NAMESPACE,
|
||||
channel.as_str(),
|
||||
&templates,
|
||||
@@ -99,7 +97,7 @@ impl NotificationTemplate {
|
||||
) -> Result<Option<String>, DatabaseError> {
|
||||
let mut redis = redis.connect().await?;
|
||||
redis
|
||||
.get_deserialized_from_json(
|
||||
.get_deserialized(
|
||||
TEMPLATES_HTML_DATA_NAMESPACE,
|
||||
&self.id.to_string(),
|
||||
)
|
||||
@@ -113,7 +111,7 @@ impl NotificationTemplate {
|
||||
) -> Result<(), DatabaseError> {
|
||||
let mut redis = redis.connect().await?;
|
||||
redis
|
||||
.set_serialized_to_json(
|
||||
.set_serialized(
|
||||
TEMPLATES_HTML_DATA_NAMESPACE,
|
||||
&self.id.to_string(),
|
||||
&data,
|
||||
@@ -138,10 +136,7 @@ where
|
||||
|
||||
let mut redis_conn = redis.connect().await?;
|
||||
if let Some(body) = redis_conn
|
||||
.get_deserialized_from_json::<HtmlBody>(
|
||||
TEMPLATES_DYNAMIC_HTML_NAMESPACE,
|
||||
key,
|
||||
)
|
||||
.get_deserialized::<HtmlBody>(TEMPLATES_DYNAMIC_HTML_NAMESPACE, key)
|
||||
.await?
|
||||
{
|
||||
return Ok(body.html);
|
||||
@@ -153,7 +148,7 @@ where
|
||||
let mut redis_conn = redis.connect().await?;
|
||||
|
||||
redis_conn
|
||||
.set_serialized_to_json(
|
||||
.set_serialized(
|
||||
TEMPLATES_DYNAMIC_HTML_NAMESPACE,
|
||||
key,
|
||||
&cached,
|
||||
|
||||
@@ -3,7 +3,7 @@ use crate::database::redis::RedisPool;
|
||||
use crate::models::v3::notifications::NotificationType;
|
||||
use serde::{Deserialize, Serialize};
|
||||
|
||||
const NOTIFICATION_TYPES_NAMESPACE: &str = "notification_types";
|
||||
const NOTIFICATION_TYPES_NAMESPACE: &str = "notification_types:v1";
|
||||
|
||||
#[derive(Serialize, Deserialize)]
|
||||
pub struct NotificationTypeItem {
|
||||
@@ -43,7 +43,7 @@ impl NotificationTypeItem {
|
||||
let mut redis = redis.connect().await?;
|
||||
|
||||
let cached_types = redis
|
||||
.get_deserialized_from_json(NOTIFICATION_TYPES_NAMESPACE, "all")
|
||||
.get_deserialized(NOTIFICATION_TYPES_NAMESPACE, "all")
|
||||
.await?;
|
||||
|
||||
if let Some(types) = cached_types {
|
||||
@@ -63,12 +63,7 @@ impl NotificationTypeItem {
|
||||
let mut redis = redis.connect().await?;
|
||||
|
||||
redis
|
||||
.set_serialized_to_json(
|
||||
NOTIFICATION_TYPES_NAMESPACE,
|
||||
"all",
|
||||
&types,
|
||||
None,
|
||||
)
|
||||
.set_serialized(NOTIFICATION_TYPES_NAMESPACE, "all", &types, None)
|
||||
.await?;
|
||||
|
||||
Ok(types)
|
||||
|
||||
@@ -9,8 +9,8 @@ use std::hash::Hash;
|
||||
use super::{DBTeamMember, ids::*};
|
||||
use serde::{Deserialize, Serialize};
|
||||
|
||||
const ORGANIZATIONS_NAMESPACE: &str = "organizations";
|
||||
const ORGANIZATIONS_TITLES_NAMESPACE: &str = "organizations_titles";
|
||||
const ORGANIZATIONS_NAMESPACE: &str = "organizations:v1";
|
||||
const ORGANIZATIONS_TITLES_NAMESPACE: &str = "organizations_titles:v1";
|
||||
|
||||
#[derive(Deserialize, Serialize, Clone, Debug)]
|
||||
/// An organization of users who together control one or more projects and organizations.
|
||||
|
||||
@@ -11,9 +11,9 @@ use serde::{Deserialize, Serialize};
|
||||
use std::fmt::{Debug, Display};
|
||||
use std::hash::Hash;
|
||||
|
||||
const PATS_NAMESPACE: &str = "pats";
|
||||
const PATS_TOKENS_NAMESPACE: &str = "pats_tokens";
|
||||
const PATS_USERS_NAMESPACE: &str = "pats_users";
|
||||
const PATS_NAMESPACE: &str = "pats:v1";
|
||||
const PATS_TOKENS_NAMESPACE: &str = "pats_tokens:v1";
|
||||
const PATS_USERS_NAMESPACE: &str = "pats_users:v1";
|
||||
|
||||
#[derive(Deserialize, Serialize, Clone, Debug)]
|
||||
pub struct DBPersonalAccessToken {
|
||||
@@ -161,7 +161,7 @@ impl DBPersonalAccessToken {
|
||||
let mut redis = redis.connect().await?;
|
||||
|
||||
let res = redis
|
||||
.get_deserialized_from_json::<Vec<i64>>(
|
||||
.get_deserialized::<Vec<i64>>(
|
||||
PATS_USERS_NAMESPACE,
|
||||
&user_id.0.to_string(),
|
||||
)
|
||||
@@ -189,12 +189,7 @@ impl DBPersonalAccessToken {
|
||||
let mut redis = redis.connect().await?;
|
||||
|
||||
redis
|
||||
.set(
|
||||
PATS_USERS_NAMESPACE,
|
||||
&user_id.0.to_string(),
|
||||
&serde_json::to_string(&db_pats)?,
|
||||
None,
|
||||
)
|
||||
.set_serialized(PATS_USERS_NAMESPACE, user_id.0, &db_pats, None)
|
||||
.await?;
|
||||
Ok(db_pats)
|
||||
}
|
||||
|
||||
@@ -9,7 +9,7 @@ use serde::{Deserialize, Serialize};
|
||||
use std::convert::TryFrom;
|
||||
use std::convert::TryInto;
|
||||
|
||||
const PRODUCTS_NAMESPACE: &str = "products";
|
||||
const PRODUCTS_NAMESPACE: &str = "products:v1";
|
||||
|
||||
pub struct DBProduct {
|
||||
pub id: DBProductId,
|
||||
@@ -153,9 +153,8 @@ impl QueryProductWithPrices {
|
||||
{
|
||||
let mut redis = redis.connect().await?;
|
||||
|
||||
let res: Option<Vec<QueryProductWithPrices>> = redis
|
||||
.get_deserialized_from_json(PRODUCTS_NAMESPACE, "all")
|
||||
.await?;
|
||||
let res: Option<Vec<QueryProductWithPrices>> =
|
||||
redis.get_deserialized(PRODUCTS_NAMESPACE, "all").await?;
|
||||
|
||||
if let Some(res) = res {
|
||||
return Ok(res);
|
||||
@@ -196,7 +195,7 @@ impl QueryProductWithPrices {
|
||||
let mut redis = redis.connect().await?;
|
||||
|
||||
redis
|
||||
.set_serialized_to_json(PRODUCTS_NAMESPACE, "all", &products, None)
|
||||
.set_serialized(PRODUCTS_NAMESPACE, "all", &products, None)
|
||||
.await?;
|
||||
|
||||
Ok(products)
|
||||
|
||||
@@ -23,9 +23,9 @@ use serde::{Deserialize, Serialize};
|
||||
use std::fmt::{Debug, Display};
|
||||
use std::hash::Hash;
|
||||
|
||||
pub const PROJECTS_NAMESPACE: &str = "projects";
|
||||
pub const PROJECTS_SLUGS_NAMESPACE: &str = "projects_slugs";
|
||||
const PROJECTS_DEPENDENCIES_NAMESPACE: &str = "projects_dependencies";
|
||||
pub const PROJECTS_NAMESPACE: &str = "projects:v1";
|
||||
pub const PROJECTS_SLUGS_NAMESPACE: &str = "projects_slugs:v1";
|
||||
const PROJECTS_DEPENDENCIES_NAMESPACE: &str = "projects_dependencies:v1";
|
||||
|
||||
#[derive(Clone, Debug, Serialize, Deserialize)]
|
||||
pub struct LinkUrl {
|
||||
@@ -976,7 +976,7 @@ impl DBProject {
|
||||
let mut redis = redis.connect().await?;
|
||||
|
||||
let dependencies = redis
|
||||
.get_deserialized_from_json::<Dependencies>(
|
||||
.get_deserialized::<Dependencies>(
|
||||
PROJECTS_DEPENDENCIES_NAMESPACE,
|
||||
&id.0.to_string(),
|
||||
)
|
||||
@@ -1014,7 +1014,7 @@ impl DBProject {
|
||||
let mut redis = redis.connect().await?;
|
||||
|
||||
redis
|
||||
.set_serialized_to_json(
|
||||
.set_serialized(
|
||||
PROJECTS_DEPENDENCIES_NAMESPACE,
|
||||
id.0,
|
||||
&dependencies,
|
||||
|
||||
@@ -10,9 +10,9 @@ use serde::{Deserialize, Serialize};
|
||||
use std::fmt::{Debug, Display};
|
||||
use std::hash::Hash;
|
||||
|
||||
const SESSIONS_NAMESPACE: &str = "sessions";
|
||||
const SESSIONS_IDS_NAMESPACE: &str = "sessions_ids";
|
||||
const SESSIONS_USERS_NAMESPACE: &str = "sessions_users";
|
||||
const SESSIONS_NAMESPACE: &str = "sessions:v1";
|
||||
const SESSIONS_IDS_NAMESPACE: &str = "sessions_ids:v1";
|
||||
const SESSIONS_USERS_NAMESPACE: &str = "sessions_users:v1";
|
||||
|
||||
pub struct SessionBuilder {
|
||||
pub session: String,
|
||||
@@ -226,7 +226,7 @@ impl DBSession {
|
||||
let mut redis = redis.connect().await?;
|
||||
|
||||
let res = redis
|
||||
.get_deserialized_from_json::<Vec<i64>>(
|
||||
.get_deserialized::<Vec<i64>>(
|
||||
SESSIONS_USERS_NAMESPACE,
|
||||
&user_id.0.to_string(),
|
||||
)
|
||||
@@ -255,7 +255,7 @@ impl DBSession {
|
||||
let mut redis = redis.connect().await?;
|
||||
|
||||
redis
|
||||
.set_serialized_to_json(
|
||||
.set_serialized(
|
||||
SESSIONS_USERS_NAMESPACE,
|
||||
user_id.0,
|
||||
&db_sessions,
|
||||
|
||||
@@ -9,7 +9,7 @@ use itertools::Itertools;
|
||||
use rust_decimal::Decimal;
|
||||
use serde::{Deserialize, Serialize};
|
||||
|
||||
const TEAMS_NAMESPACE: &str = "teams";
|
||||
const TEAMS_NAMESPACE: &str = "teams:v1";
|
||||
|
||||
pub struct TeamBuilder {
|
||||
pub members: Vec<TeamMemberBuilder>,
|
||||
|
||||
@@ -16,9 +16,9 @@ use serde::{Deserialize, Serialize};
|
||||
use std::fmt::{Debug, Display};
|
||||
use std::hash::Hash;
|
||||
|
||||
const USERS_NAMESPACE: &str = "users";
|
||||
const USER_USERNAMES_NAMESPACE: &str = "users_usernames";
|
||||
const USERS_PROJECTS_NAMESPACE: &str = "users_projects";
|
||||
const USERS_NAMESPACE: &str = "users:v1";
|
||||
const USER_USERNAMES_NAMESPACE: &str = "users_usernames:v1";
|
||||
const USERS_PROJECTS_NAMESPACE: &str = "users_projects:v1";
|
||||
|
||||
#[derive(Deserialize, Serialize, Clone, Debug)]
|
||||
pub struct DBUser {
|
||||
@@ -391,7 +391,7 @@ impl DBUser {
|
||||
let mut redis = redis.connect().await?;
|
||||
|
||||
let cached_projects = redis
|
||||
.get_deserialized_from_json::<Vec<DBProjectId>>(
|
||||
.get_deserialized::<Vec<DBProjectId>>(
|
||||
USERS_PROJECTS_NAMESPACE,
|
||||
&user_id.0.to_string(),
|
||||
)
|
||||
@@ -419,7 +419,7 @@ impl DBUser {
|
||||
let mut redis = redis.connect().await?;
|
||||
|
||||
redis
|
||||
.set_serialized_to_json(
|
||||
.set_serialized(
|
||||
USERS_PROJECTS_NAMESPACE,
|
||||
user_id.0,
|
||||
&db_projects,
|
||||
|
||||
@@ -22,8 +22,8 @@ use std::collections::HashMap;
|
||||
use std::iter;
|
||||
use tracing::error;
|
||||
|
||||
pub const VERSIONS_NAMESPACE: &str = "versions";
|
||||
const VERSION_FILES_NAMESPACE: &str = "versions_files";
|
||||
pub const VERSIONS_NAMESPACE: &str = "versions:v1";
|
||||
const VERSION_FILES_NAMESPACE: &str = "versions_files:v1";
|
||||
|
||||
pub async fn cleanup_unused_attribution_files_and_groups(
|
||||
transaction: &mut PgTransaction<'_>,
|
||||
|
||||
@@ -12,12 +12,15 @@ use prometheus::{IntGauge, Registry};
|
||||
use redis::ToRedisArgs;
|
||||
use serde::de::DeserializeOwned;
|
||||
use serde::{Deserialize, Serialize};
|
||||
use std::borrow::Cow;
|
||||
use std::collections::HashMap;
|
||||
use std::fmt::{Debug, Display};
|
||||
use std::future::Future;
|
||||
use std::hash::Hash;
|
||||
use std::str::FromStr;
|
||||
use std::sync::Arc;
|
||||
use std::time::Duration;
|
||||
use thiserror::Error;
|
||||
use tracing::{Instrument, info, info_span};
|
||||
use util::{cmd, redis_pipe};
|
||||
|
||||
@@ -41,8 +44,112 @@ const MGET_CHUNK_SIZE: usize = 32;
|
||||
// BytesMut peak capacity that builds up under steady load.
|
||||
const REDIS_MAX_CONN_AGE: Duration = Duration::from_secs(120);
|
||||
|
||||
#[repr(u8)]
|
||||
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
|
||||
pub enum Codec {
|
||||
Raw = 0,
|
||||
Lz4 = 1,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
|
||||
pub enum EncodingFormat {
|
||||
Json,
|
||||
Postcard,
|
||||
}
|
||||
|
||||
#[derive(Debug, Error)]
|
||||
#[error("invalid redis codec")]
|
||||
pub struct InvalidCodec;
|
||||
|
||||
#[derive(Debug, Error)]
|
||||
#[error("invalid redis encoding format")]
|
||||
pub struct InvalidEncodingFormat;
|
||||
|
||||
impl TryFrom<u8> for Codec {
|
||||
type Error = InvalidCodec;
|
||||
|
||||
fn try_from(value: u8) -> Result<Self, Self::Error> {
|
||||
match value {
|
||||
0 => Ok(Self::Raw),
|
||||
1 => Ok(Self::Lz4),
|
||||
_ => Err(InvalidCodec),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl FromStr for Codec {
|
||||
type Err = InvalidCodec;
|
||||
|
||||
fn from_str(value: &str) -> Result<Self, Self::Err> {
|
||||
match value {
|
||||
"lz4" => Ok(Self::Lz4),
|
||||
_ => Err(InvalidCodec),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl FromStr for EncodingFormat {
|
||||
type Err = InvalidEncodingFormat;
|
||||
|
||||
fn from_str(value: &str) -> Result<Self, Self::Err> {
|
||||
match value {
|
||||
"json" => Ok(Self::Json),
|
||||
"postcard" => Ok(Self::Postcard),
|
||||
_ => Err(InvalidEncodingFormat),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn encode_value<T: Serialize>(value: &T) -> Result<Vec<u8>, DatabaseError> {
|
||||
let mut value = match ENV.REDIS_ENCODING_FORMAT {
|
||||
EncodingFormat::Json => serde_json::to_vec(value)?,
|
||||
EncodingFormat::Postcard => postcard::to_allocvec(value)?,
|
||||
};
|
||||
|
||||
if ENV.REDIS_COMPRESSION_LEVEL > 0
|
||||
&& ENV.REDIS_COMPRESSION_ALGORITHM == Codec::Lz4
|
||||
&& value.len() >= ENV.REDIS_COMPRESSION_THRESHOLD_BYTES
|
||||
{
|
||||
let compressed = lz4_flex::block::compress_prepend_size(&value);
|
||||
let savings_ratio = value.len().saturating_sub(compressed.len()) as f64
|
||||
/ value.len().max(1) as f64
|
||||
* 100.0;
|
||||
|
||||
if savings_ratio >= ENV.REDIS_COMPRESSION_MIN_SAVINGS_RATIO {
|
||||
let mut encoded = Vec::with_capacity(compressed.len() + 1);
|
||||
encoded.push(Codec::Lz4 as u8);
|
||||
encoded.extend(compressed);
|
||||
return Ok(encoded);
|
||||
}
|
||||
}
|
||||
|
||||
let mut encoded = Vec::with_capacity(value.len() + 1);
|
||||
encoded.push(Codec::Raw as u8);
|
||||
encoded.append(&mut value);
|
||||
Ok(encoded)
|
||||
}
|
||||
|
||||
fn decode_value<T>(value: &[u8]) -> Option<T>
|
||||
where
|
||||
T: for<'a> Deserialize<'a>,
|
||||
{
|
||||
let (codec, value) = value.split_first()?;
|
||||
let value = match Codec::try_from(*codec).ok()? {
|
||||
Codec::Raw => Cow::Borrowed(value),
|
||||
Codec::Lz4 => {
|
||||
Cow::Owned(lz4_flex::block::decompress_size_prepended(value).ok()?)
|
||||
}
|
||||
};
|
||||
|
||||
match ENV.REDIS_ENCODING_FORMAT {
|
||||
EncodingFormat::Json => serde_json::from_slice(&value).ok(),
|
||||
EncodingFormat::Postcard => postcard::from_bytes(&value).ok(),
|
||||
}
|
||||
}
|
||||
|
||||
fn cache_expiries(namespace: &str) -> (i64, i64) {
|
||||
match namespace {
|
||||
// Namespaces may embed a version suffix like `:v1`, so split it out.
|
||||
match namespace.split_once(':').map(|t| t.0).unwrap_or(namespace) {
|
||||
"versions" | "versions_files" => {
|
||||
(VERSION_DEFAULT_EXPIRY, VERSION_ACTUAL_EXPIRY)
|
||||
}
|
||||
@@ -367,12 +474,11 @@ impl RedisPool {
|
||||
for chunk in args.chunks(MGET_CHUNK_SIZE) {
|
||||
let part = cmd("MGET")
|
||||
.arg(chunk)
|
||||
.query_async::<Vec<Option<String>>>(&mut connection)
|
||||
.query_async::<Vec<Option<Vec<u8>>>>(&mut connection)
|
||||
.await?;
|
||||
cached_values.extend(part.into_iter().filter_map(|x| {
|
||||
x.and_then(|val| {
|
||||
serde_json::from_str::<RedisValue<T, K, S>>(&val)
|
||||
.ok()
|
||||
decode_value::<RedisValue<T, K, S>>(&val)
|
||||
})
|
||||
.map(|val| (val.key.clone(), val))
|
||||
}));
|
||||
@@ -492,7 +598,7 @@ impl RedisPool {
|
||||
"{}_{namespace}:{key}",
|
||||
self.meta_namespace
|
||||
),
|
||||
serde_json::to_string(&value)?,
|
||||
encode_value(&value)?,
|
||||
default_expiry as u64,
|
||||
);
|
||||
pipe_cmds += 1;
|
||||
@@ -624,30 +730,27 @@ impl<'a> Drop for LockSentinel<'a> {
|
||||
|
||||
impl RedisConnection {
|
||||
#[tracing::instrument(skip(self))]
|
||||
pub async fn set(
|
||||
pub async fn set<D>(
|
||||
&mut self,
|
||||
namespace: &str,
|
||||
id: &str,
|
||||
data: &str,
|
||||
data: D,
|
||||
expiry: Option<i64>,
|
||||
) -> Result<(), DatabaseError> {
|
||||
) -> Result<(), DatabaseError>
|
||||
where
|
||||
D: ToRedisArgs + Send + Sync + Debug,
|
||||
{
|
||||
let mut cmd = cmd("SET");
|
||||
redis_args(
|
||||
&mut cmd,
|
||||
vec![
|
||||
format!("{}_{}:{}", self.meta_namespace, namespace, id),
|
||||
data.to_string(),
|
||||
"EX".to_string(),
|
||||
expiry.unwrap_or(DEFAULT_EXPIRY).to_string(),
|
||||
]
|
||||
.as_slice(),
|
||||
);
|
||||
cmd.arg(format!("{}_{}:{}", self.meta_namespace, namespace, id))
|
||||
.arg(data)
|
||||
.arg("EX")
|
||||
.arg(expiry.unwrap_or(DEFAULT_EXPIRY));
|
||||
redis_execute::<()>(&mut cmd, &mut self.connection).await?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[tracing::instrument(skip(self, id, data))]
|
||||
pub async fn set_serialized_to_json<Id, D>(
|
||||
pub async fn set_serialized<Id, D>(
|
||||
&mut self,
|
||||
namespace: &str,
|
||||
id: Id,
|
||||
@@ -658,13 +761,8 @@ impl RedisConnection {
|
||||
Id: Display,
|
||||
D: serde::Serialize,
|
||||
{
|
||||
self.set(
|
||||
namespace,
|
||||
&id.to_string(),
|
||||
&serde_json::to_string(&data)?,
|
||||
expiry,
|
||||
)
|
||||
.await
|
||||
self.set(namespace, &id.to_string(), encode_value(&data)?, expiry)
|
||||
.await
|
||||
}
|
||||
|
||||
#[tracing::instrument(skip(self))]
|
||||
@@ -688,7 +786,7 @@ impl RedisConnection {
|
||||
&mut self,
|
||||
namespace: &str,
|
||||
ids: &[String],
|
||||
) -> Result<Vec<Option<String>>, DatabaseError> {
|
||||
) -> Result<Vec<Option<Vec<u8>>>, DatabaseError> {
|
||||
let mut cmd = cmd("MGET");
|
||||
redis_args(
|
||||
&mut cmd,
|
||||
@@ -702,7 +800,7 @@ impl RedisConnection {
|
||||
}
|
||||
|
||||
#[tracing::instrument(skip(self))]
|
||||
pub async fn get_deserialized_from_json<R>(
|
||||
pub async fn get_deserialized<R>(
|
||||
&mut self,
|
||||
namespace: &str,
|
||||
id: &str,
|
||||
@@ -710,14 +808,19 @@ impl RedisConnection {
|
||||
where
|
||||
R: for<'a> serde::Deserialize<'a>,
|
||||
{
|
||||
Ok(self
|
||||
.get(namespace, id)
|
||||
.await?
|
||||
.and_then(|x| serde_json::from_str(&x).ok()))
|
||||
let mut cmd = cmd("GET");
|
||||
redis_args(
|
||||
&mut cmd,
|
||||
vec![format!("{}_{}:{}", self.meta_namespace, namespace, id)]
|
||||
.as_slice(),
|
||||
);
|
||||
let value: Option<Vec<u8>> =
|
||||
redis_execute(&mut cmd, &mut self.connection).await?;
|
||||
Ok(value.and_then(|value| decode_value(&value)))
|
||||
}
|
||||
|
||||
#[tracing::instrument(skip(self))]
|
||||
pub async fn get_many_deserialized_from_json<R>(
|
||||
pub async fn get_many_deserialized<R>(
|
||||
&mut self,
|
||||
namespace: &str,
|
||||
ids: &[String],
|
||||
@@ -729,8 +832,8 @@ impl RedisConnection {
|
||||
.get_many(namespace, ids)
|
||||
.await?
|
||||
.into_iter()
|
||||
.map(|x| x.and_then(|val| serde_json::from_str::<R>(&val).ok()))
|
||||
.collect::<Vec<_>>())
|
||||
.map(|value| value.and_then(|value| decode_value::<R>(&value)))
|
||||
.collect())
|
||||
}
|
||||
|
||||
#[tracing::instrument(skip(self, id))]
|
||||
@@ -799,7 +902,7 @@ impl RedisConnection {
|
||||
namespace: &str,
|
||||
key: &str,
|
||||
timeout: Option<f64>,
|
||||
) -> Result<Option<[String; 2]>, DatabaseError> {
|
||||
) -> Result<Option<[Vec<u8>; 2]>, DatabaseError> {
|
||||
let key = format!("{}_{namespace}:{key}", self.meta_namespace);
|
||||
// a timeout of 0 is infinite
|
||||
let timeout = timeout.unwrap_or(0.0);
|
||||
@@ -829,12 +932,17 @@ impl RedisConnection {
|
||||
#[derive(Serialize, Deserialize)]
|
||||
pub struct RedisValue<T, K, S> {
|
||||
key: K,
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
alias: Option<S>,
|
||||
iat: i64,
|
||||
val: T,
|
||||
}
|
||||
|
||||
impl<T, K, S> RedisValue<T, K, S> {
|
||||
pub fn value(&self) -> &T {
|
||||
&self.val
|
||||
}
|
||||
}
|
||||
|
||||
pub fn redis_args(cmd: &mut util::InstrumentedCmd, args: &[String]) {
|
||||
for arg in args {
|
||||
cmd.arg(arg);
|
||||
|
||||
@@ -235,6 +235,9 @@ vars! {
|
||||
SENDY_LIST_ID: String = "none";
|
||||
SENDY_API_KEY: String = "none";
|
||||
|
||||
NEVERBOUNCE_API_KEY: String = "";
|
||||
NEVERBOUNCE_BASE_URL: String = neverbounce::DEFAULT_API_URL;
|
||||
|
||||
CLICKHOUSE_REPLICATED: bool = false;
|
||||
CLICKHOUSE_URL: String = "http://localhost:8123";
|
||||
CLICKHOUSE_USER: String = "default";
|
||||
@@ -289,6 +292,11 @@ vars! {
|
||||
REDIS_WAIT_TIMEOUT_MS: u64 = 15000u64;
|
||||
REDIS_MAX_CONNECTIONS: u32 = 10000u32;
|
||||
REDIS_MIN_CONNECTIONS: usize = 0usize;
|
||||
REDIS_ENCODING_FORMAT: crate::database::redis::EncodingFormat = crate::database::redis::EncodingFormat::Json;
|
||||
REDIS_COMPRESSION_LEVEL: i32 = 0i32;
|
||||
REDIS_COMPRESSION_ALGORITHM: crate::database::redis::Codec = crate::database::redis::Codec::Lz4;
|
||||
REDIS_COMPRESSION_THRESHOLD_BYTES: usize = 1024usize;
|
||||
REDIS_COMPRESSION_MIN_SAVINGS_RATIO: f64 = 12.5f64;
|
||||
|
||||
SEARCH_OPERATION_TIMEOUT: u64 = 300000u64;
|
||||
|
||||
|
||||
@@ -15,10 +15,7 @@ use labrinth::search;
|
||||
use labrinth::util::anrok;
|
||||
use labrinth::util::gotenberg::GotenbergClient;
|
||||
use labrinth::util::ratelimit::rate_limit_middleware;
|
||||
use labrinth::{app_data_config, app_fallback_config, env};
|
||||
use labrinth::{
|
||||
app_routes_config_internal, app_routes_config_v2, app_routes_config_v3,
|
||||
};
|
||||
use labrinth::{app_data_config, app_fallback_config, app_routes_config, env};
|
||||
use labrinth::{clickhouse, database, file_hosting};
|
||||
use scalar_api_reference::actix_web::config as scalar_config;
|
||||
use serde_json::json;
|
||||
@@ -275,11 +272,7 @@ async fn app() -> std::io::Result<()> {
|
||||
// own - See `sentry::SentryErrorReporting` for why.
|
||||
.wrap(labrinth::util::sentry::SentryErrorReporting)
|
||||
.configure(|cfg| app_data_config(cfg, labrinth_config.clone()))
|
||||
.configure(|cfg| app_routes_config_v2(cfg, labrinth_config.clone()))
|
||||
.configure(|cfg| app_routes_config_v3(cfg, labrinth_config.clone()))
|
||||
.configure(|cfg| {
|
||||
app_routes_config_internal(cfg, labrinth_config.clone())
|
||||
});
|
||||
.configure(|cfg| app_routes_config(cfg, labrinth_config.clone()));
|
||||
|
||||
let scalar_configuration = json!({
|
||||
"sources": [
|
||||
|
||||
@@ -258,7 +258,7 @@ pub async fn fetch_query_context(
|
||||
HashMap::new()
|
||||
} else {
|
||||
redis
|
||||
.get_many_deserialized_from_json::<minecraft::JavaServerPing>(
|
||||
.get_many_deserialized::<minecraft::JavaServerPing>(
|
||||
server_ping::REDIS_NAMESPACE,
|
||||
&minecraft_java_server_pings
|
||||
.iter()
|
||||
@@ -281,7 +281,7 @@ pub async fn fetch_query_context(
|
||||
HashMap::new()
|
||||
} else {
|
||||
redis
|
||||
.get_many_deserialized_from_json::<MinecraftServerAnalytics>(
|
||||
.get_many_deserialized::<MinecraftServerAnalytics>(
|
||||
MINECRAFT_SERVER_ANALYTICS,
|
||||
&minecraft_server_analytics
|
||||
.iter()
|
||||
|
||||
@@ -13,7 +13,7 @@ use crate::{
|
||||
util::error::Context,
|
||||
};
|
||||
|
||||
pub const MINECRAFT_SERVER_ANALYTICS: &str = "minecraft_server_analytics";
|
||||
pub const MINECRAFT_SERVER_ANALYTICS: &str = "minecraft_server_analytics:v1";
|
||||
|
||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||
pub struct MinecraftServerAnalytics {
|
||||
@@ -117,7 +117,7 @@ pub async fn cache_analytics(
|
||||
|
||||
debug!("Caching analytics for {project_id}: {analytics:?}");
|
||||
redis
|
||||
.set_serialized_to_json(
|
||||
.set_serialized(
|
||||
MINECRAFT_SERVER_ANALYTICS,
|
||||
project_id.to_string(),
|
||||
analytics,
|
||||
|
||||
@@ -13,9 +13,9 @@ use tracing::trace;
|
||||
|
||||
pub mod cache;
|
||||
|
||||
const DOWNLOADS_NAMESPACE: &str = "downloads";
|
||||
const VIEWS_NAMESPACE: &str = "views";
|
||||
const MINECRAFT_SERVER_PLAYS_NAMESPACE: &str = "minecraft_server_plays";
|
||||
const DOWNLOADS_NAMESPACE: &str = "downloads:v1";
|
||||
const VIEWS_NAMESPACE: &str = "views:v1";
|
||||
const MINECRAFT_SERVER_PLAYS_NAMESPACE: &str = "minecraft_server_plays:v1";
|
||||
const MINECRAFT_SERVER_PLAYS_EXPIRY: u64 = 86_400; // 24 hours
|
||||
const MINECRAFT_SERVER_PLAYS_LIMIT: u32 = 5;
|
||||
|
||||
|
||||
@@ -26,8 +26,9 @@ pub struct ServerPingQueue {
|
||||
pub incremental_search_queue: IncrementalSearchQueue,
|
||||
}
|
||||
|
||||
pub const REDIS_NAMESPACE: &str = "minecraft_java_server_ping";
|
||||
pub const REDIS_FAILURE_NAMESPACE: &str = "minecraft_java_server_ping_failures";
|
||||
pub const REDIS_NAMESPACE: &str = "minecraft_java_server_ping:v1";
|
||||
pub const REDIS_FAILURE_NAMESPACE: &str =
|
||||
"minecraft_java_server_ping_failures:v1";
|
||||
pub const CLICKHOUSE_TABLE: &str = "minecraft_java_server_pings";
|
||||
|
||||
impl ServerPingQueue {
|
||||
@@ -133,12 +134,7 @@ impl ServerPingQueue {
|
||||
// ping succeeded; immediately update its online status in redis
|
||||
|
||||
redis
|
||||
.set_serialized_to_json(
|
||||
REDIS_NAMESPACE,
|
||||
project_id,
|
||||
ping,
|
||||
None,
|
||||
)
|
||||
.set_serialized(REDIS_NAMESPACE, project_id, ping, None)
|
||||
.await
|
||||
.wrap_err("failed to set redis key")?;
|
||||
updated_project = true;
|
||||
@@ -160,7 +156,7 @@ impl ServerPingQueue {
|
||||
&& count >= ENV.SERVER_PING_MAX_FAIL_COUNT
|
||||
{
|
||||
redis
|
||||
.set_serialized_to_json(
|
||||
.set_serialized(
|
||||
REDIS_NAMESPACE,
|
||||
project_id,
|
||||
ping,
|
||||
@@ -254,7 +250,7 @@ impl ServerPingQueue {
|
||||
.collect::<Vec<_>>();
|
||||
|
||||
let all_server_last_pings = redis
|
||||
.get_many_deserialized_from_json::<exp::minecraft::JavaServerPing>(
|
||||
.get_many_deserialized::<exp::minecraft::JavaServerPing>(
|
||||
REDIS_NAMESPACE,
|
||||
&all_project_ids,
|
||||
)
|
||||
|
||||
@@ -68,7 +68,7 @@ pub struct CampaignInfo {
|
||||
cached_at: DateTime<Utc>,
|
||||
}
|
||||
|
||||
const CAMPAIGN_INFO_CACHE_NAMESPACE: &str = "campaign_info";
|
||||
const CAMPAIGN_INFO_CACHE_NAMESPACE: &str = "campaign_info:v1";
|
||||
const CAMPAIGN_INFO_CACHE_STALE_SECONDS: i64 = 15 * 60;
|
||||
const CAMPAIGN_INFO_CACHE_TTL_SECONDS: i64 = 24 * 60 * 60;
|
||||
|
||||
@@ -326,7 +326,7 @@ pub async fn pride_26(
|
||||
.wrap_internal_err("connecting to redis")?;
|
||||
|
||||
let cached = redis_connection
|
||||
.get_deserialized_from_json::<CampaignInfo>(
|
||||
.get_deserialized::<CampaignInfo>(
|
||||
CAMPAIGN_INFO_CACHE_NAMESPACE,
|
||||
campaign_id,
|
||||
)
|
||||
@@ -382,7 +382,7 @@ pub async fn pride_26(
|
||||
};
|
||||
|
||||
redis_connection
|
||||
.set_serialized_to_json(
|
||||
.set_serialized(
|
||||
CAMPAIGN_INFO_CACHE_NAMESPACE,
|
||||
campaign_id,
|
||||
&campaign_info,
|
||||
|
||||
@@ -427,7 +427,7 @@ async fn broadcast_notifications(
|
||||
redis,
|
||||
RedisFriendsMessage::Notification {
|
||||
to_user,
|
||||
notification,
|
||||
notification_id,
|
||||
},
|
||||
)
|
||||
.await
|
||||
|
||||
@@ -24,6 +24,7 @@ use crate::util::captcha::check_hcaptcha;
|
||||
use crate::util::error::Context;
|
||||
use crate::util::ext::get_image_ext;
|
||||
use crate::util::img::upload_image_optimized;
|
||||
use crate::util::neverbounce::{check_email, email_check_error_generic};
|
||||
use crate::util::validate::validation_errors_to_string;
|
||||
use actix_http::header::LOCATION;
|
||||
use actix_web::http::StatusCode;
|
||||
@@ -1440,7 +1441,7 @@ struct NewOAuthAccount {
|
||||
pub sign_up_newsletter: bool,
|
||||
}
|
||||
|
||||
/// Create account with OAuth.
|
||||
/// Create account with OAuth.
|
||||
#[utoipa::path(
|
||||
context_path = "/auth",
|
||||
tag = "auth",
|
||||
@@ -1481,6 +1482,10 @@ pub async fn create_oauth_account(
|
||||
return Err(ApiError::Internal(eyre!("invalid flow kind")));
|
||||
};
|
||||
|
||||
if let Some(email) = &user.email {
|
||||
ensure_email_is_usable(email).await?;
|
||||
}
|
||||
|
||||
let mut txn = db
|
||||
.begin()
|
||||
.await
|
||||
@@ -1527,7 +1532,7 @@ struct DiscordCommunityHandoffPayload {
|
||||
nonce: String,
|
||||
}
|
||||
|
||||
/// Link Discord community.
|
||||
/// Link Discord community.
|
||||
#[utoipa::path(
|
||||
context_path = "/auth",
|
||||
tag = "auth",
|
||||
@@ -1604,7 +1609,7 @@ pub async fn discord_community_link(
|
||||
Ok(web::Json(DiscordCommunityLinkResponse { url }))
|
||||
}
|
||||
|
||||
/// Remove an auth provider.
|
||||
/// Remove an auth provider.
|
||||
#[utoipa::path(
|
||||
context_path = "/auth",
|
||||
tag = "auth",
|
||||
@@ -1794,6 +1799,20 @@ impl From<NewAccount> for AccountRegisterFlow {
|
||||
}
|
||||
}
|
||||
|
||||
async fn ensure_email_is_usable(email: &str) -> Result<(), ApiError> {
|
||||
let result = check_email(email).await.map_err(ApiError::Request)?;
|
||||
|
||||
if matches!(
|
||||
result,
|
||||
neverbounce::VerificationResult::Invalid
|
||||
| neverbounce::VerificationResult::Disposable
|
||||
) {
|
||||
return Err(ApiError::Request(email_check_error_generic()));
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
impl AccountRegisterFlow {
|
||||
async fn validate(
|
||||
self,
|
||||
@@ -1947,7 +1966,7 @@ impl ReadyAccountRegisterFlow {
|
||||
}
|
||||
}
|
||||
|
||||
/// Validate password account creation.
|
||||
/// Validate password account creation.
|
||||
#[utoipa::path(
|
||||
context_path = "/auth",
|
||||
tag = "auth",
|
||||
@@ -1975,7 +1994,7 @@ pub async fn validate_create_account_with_password(
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Create account with a password.
|
||||
/// Create account with a password.
|
||||
#[utoipa::path(
|
||||
context_path = "/auth",
|
||||
tag = "auth",
|
||||
@@ -2002,6 +2021,8 @@ pub async fn create_account_with_password(
|
||||
return Err(ApiError::Turnstile);
|
||||
}
|
||||
|
||||
ensure_email_is_usable(&new_account.email).await?;
|
||||
|
||||
let mut transaction = pool.begin().await?;
|
||||
|
||||
let ready_flow = AccountRegisterFlow::from(new_account)
|
||||
@@ -2024,7 +2045,7 @@ pub struct Login {
|
||||
pub challenge: String,
|
||||
}
|
||||
|
||||
/// Log in with a password.
|
||||
/// Log in with a password.
|
||||
#[utoipa::path(
|
||||
context_path = "/auth",
|
||||
tag = "auth",
|
||||
@@ -2126,7 +2147,7 @@ async fn validate_2fa_code(
|
||||
)
|
||||
.map_err(|_| AuthenticationError::InvalidCredentials)?;
|
||||
|
||||
const TOTP_NAMESPACE: &str = "used_totp";
|
||||
const TOTP_NAMESPACE: &str = "used_totp:v1";
|
||||
let mut conn = redis.connect().await?;
|
||||
|
||||
// Check if TOTP has already been used
|
||||
@@ -2185,7 +2206,7 @@ async fn validate_2fa_code(
|
||||
}
|
||||
}
|
||||
|
||||
/// Complete login with 2FA.
|
||||
/// Complete login with 2FA.
|
||||
#[utoipa::path(
|
||||
context_path = "/auth",
|
||||
tag = "auth",
|
||||
@@ -2245,7 +2266,7 @@ pub async fn login_2fa(
|
||||
}
|
||||
}
|
||||
|
||||
/// Start 2FA setup.
|
||||
/// Start 2FA setup.
|
||||
#[utoipa::path(
|
||||
context_path = "/auth",
|
||||
tag = "auth",
|
||||
@@ -2296,7 +2317,7 @@ pub async fn begin_2fa_flow(
|
||||
}
|
||||
}
|
||||
|
||||
/// Finish 2FA setup.
|
||||
/// Finish 2FA setup.
|
||||
#[utoipa::path(
|
||||
context_path = "/auth",
|
||||
tag = "auth",
|
||||
@@ -2431,7 +2452,7 @@ pub struct Remove2FA {
|
||||
pub code: String,
|
||||
}
|
||||
|
||||
/// Remove 2FA.
|
||||
/// Remove 2FA.
|
||||
#[utoipa::path(
|
||||
context_path = "/auth",
|
||||
tag = "auth",
|
||||
@@ -2531,7 +2552,7 @@ pub struct ResetPassword {
|
||||
pub challenge: String,
|
||||
}
|
||||
|
||||
/// Start password reset.
|
||||
/// Start password reset.
|
||||
#[utoipa::path(
|
||||
context_path = "/auth",
|
||||
tag = "auth",
|
||||
@@ -2637,7 +2658,7 @@ pub struct ChangePassword {
|
||||
pub new_password: Option<String>,
|
||||
}
|
||||
|
||||
/// Change password.
|
||||
/// Change password.
|
||||
#[utoipa::path(
|
||||
context_path = "/auth",
|
||||
tag = "auth",
|
||||
@@ -2803,7 +2824,7 @@ pub struct SetEmail {
|
||||
pub email: String,
|
||||
}
|
||||
|
||||
/// Set email address.
|
||||
/// Set email address.
|
||||
#[utoipa::path(
|
||||
context_path = "/auth",
|
||||
tag = "auth",
|
||||
@@ -2856,6 +2877,8 @@ pub async fn set_email(
|
||||
));
|
||||
}
|
||||
|
||||
ensure_email_is_usable(&email_address.email).await?;
|
||||
|
||||
let mut transaction = pool.begin().await?;
|
||||
|
||||
sqlx::query!(
|
||||
@@ -2925,7 +2948,7 @@ pub async fn set_email(
|
||||
Ok(HttpResponse::Ok().finish())
|
||||
}
|
||||
|
||||
/// Resend verification email.
|
||||
/// Resend verification email.
|
||||
#[utoipa::path(
|
||||
context_path = "/auth",
|
||||
tag = "auth",
|
||||
@@ -3000,7 +3023,7 @@ pub struct VerifyEmail {
|
||||
pub flow: String,
|
||||
}
|
||||
|
||||
/// Verify email address.
|
||||
/// Verify email address.
|
||||
#[utoipa::path(
|
||||
context_path = "/auth",
|
||||
tag = "auth",
|
||||
@@ -3066,7 +3089,7 @@ pub async fn verify_email(
|
||||
}
|
||||
}
|
||||
|
||||
/// Subscribe to the newsletter.
|
||||
/// Subscribe to the newsletter.
|
||||
#[utoipa::path(
|
||||
context_path = "/auth",
|
||||
tag = "auth",
|
||||
@@ -3115,7 +3138,7 @@ pub async fn subscribe_newsletter(
|
||||
Ok(HttpResponse::NoContent().finish())
|
||||
}
|
||||
|
||||
/// Get newsletter subscription status.
|
||||
/// Get newsletter subscription status.
|
||||
#[utoipa::path(
|
||||
context_path = "/auth",
|
||||
tag = "auth",
|
||||
@@ -3165,7 +3188,7 @@ pub struct RegisterPasskeyResponse {
|
||||
pub flow: String,
|
||||
}
|
||||
|
||||
/// Start passkey registration.
|
||||
/// Start passkey registration.
|
||||
#[utoipa::path(
|
||||
context_path = "/auth",
|
||||
tag = "auth",
|
||||
@@ -3265,7 +3288,7 @@ pub struct PasskeyResponse {
|
||||
pub last_used: Option<chrono::DateTime<Utc>>,
|
||||
}
|
||||
|
||||
/// Finish passkey registration.
|
||||
/// Finish passkey registration.
|
||||
#[utoipa::path(
|
||||
context_path = "/auth",
|
||||
tag = "auth",
|
||||
@@ -3374,7 +3397,7 @@ pub struct AuthenticatePasskeyResponse {
|
||||
pub flow: String,
|
||||
}
|
||||
|
||||
/// Start passkey authentication.
|
||||
/// Start passkey authentication.
|
||||
#[utoipa::path(
|
||||
context_path = "/auth",
|
||||
tag = "auth",
|
||||
@@ -3417,7 +3440,7 @@ pub struct AuthenticatePasskeyFinish {
|
||||
pub credential: PublicKeyCredential,
|
||||
}
|
||||
|
||||
/// Finish passkey authentication.
|
||||
/// Finish passkey authentication.
|
||||
#[utoipa::path(
|
||||
context_path = "/auth",
|
||||
tag = "auth",
|
||||
@@ -3534,7 +3557,7 @@ pub async fn authenticate_passkey_finish(
|
||||
}
|
||||
}
|
||||
|
||||
/// List passkeys.
|
||||
/// List passkeys.
|
||||
#[utoipa::path(
|
||||
context_path = "/auth",
|
||||
tag = "auth",
|
||||
@@ -3584,7 +3607,7 @@ pub struct RenamePasskey {
|
||||
pub name: String,
|
||||
}
|
||||
|
||||
/// Rename a passkey.
|
||||
/// Rename a passkey.
|
||||
#[utoipa::path(
|
||||
context_path = "/auth",
|
||||
tag = "auth",
|
||||
@@ -3639,7 +3662,7 @@ pub async fn rename_passkey(
|
||||
Ok(HttpResponse::NoContent().finish())
|
||||
}
|
||||
|
||||
/// Delete a passkey.
|
||||
/// Delete a passkey.
|
||||
#[utoipa::path(
|
||||
context_path = "/auth",
|
||||
tag = "auth",
|
||||
|
||||
@@ -70,11 +70,11 @@ pub async fn success_callback(
|
||||
|
||||
let body = base64::engine::general_purpose::STANDARD.encode(&body);
|
||||
|
||||
let redis_msg = serde_json::to_string(&Ok::<
|
||||
let redis_msg = postcard::to_allocvec(&Ok::<
|
||||
GotenbergDocument,
|
||||
GotenbergError,
|
||||
>(GotenbergDocument { body }))
|
||||
.wrap_internal_err("failed to serialize document to JSON")?;
|
||||
.wrap_internal_err("failed to serialize Redis document response")?;
|
||||
|
||||
redis
|
||||
.lpush(
|
||||
@@ -139,11 +139,11 @@ pub async fn error_callback(
|
||||
.await
|
||||
.wrap_internal_err("failed to get Redis connection")?;
|
||||
|
||||
let redis_msg = serde_json::to_string(&Err::<
|
||||
let redis_msg = postcard::to_allocvec(&Err::<
|
||||
GotenbergDocument,
|
||||
GotenbergError,
|
||||
>(error_body))
|
||||
.wrap_internal_err("failed to serialize error to JSON")?;
|
||||
.wrap_internal_err("failed to serialize Redis error response")?;
|
||||
|
||||
redis
|
||||
.lpush(
|
||||
|
||||
@@ -22,8 +22,8 @@ use serde::{Deserialize, Serialize};
|
||||
use sha2::{Digest, Sha256};
|
||||
use std::collections::BTreeMap;
|
||||
|
||||
const CONTENT_RESOLVE_CACHE_NAMESPACE: &str = "content_resolve";
|
||||
const CONTENT_RESOLVE_CACHE_HEAT_NAMESPACE: &str = "content_resolve_heat";
|
||||
const CONTENT_RESOLVE_CACHE_NAMESPACE: &str = "content_resolve:v1";
|
||||
const CONTENT_RESOLVE_CACHE_HEAT_NAMESPACE: &str = "content_resolve_heat:v1";
|
||||
const CONTENT_RESOLVE_CACHE_SCHEMA_VERSION: &str = "v1";
|
||||
const CONTENT_RESOLVE_CACHE_HEAT_WINDOW_SECONDS: i64 = 60 * 60 * 24;
|
||||
|
||||
@@ -340,7 +340,7 @@ async fn get_cached_resolve_content_plan(
|
||||
};
|
||||
|
||||
match redis
|
||||
.get_deserialized_from_json(CONTENT_RESOLVE_CACHE_NAMESPACE, cache_key)
|
||||
.get_deserialized(CONTENT_RESOLVE_CACHE_NAMESPACE, cache_key)
|
||||
.await
|
||||
{
|
||||
Ok(cached) => cached,
|
||||
@@ -368,7 +368,7 @@ async fn set_cached_resolve_content_plan(
|
||||
};
|
||||
|
||||
if let Err(error) = redis
|
||||
.set_serialized_to_json(
|
||||
.set_serialized(
|
||||
CONTENT_RESOLVE_CACHE_NAMESPACE,
|
||||
cache_key,
|
||||
cached,
|
||||
|
||||
@@ -151,7 +151,7 @@ async fn hydrate_search_results(
|
||||
} else {
|
||||
let mut redis = redis_pool.connect().await?;
|
||||
let ping_results = redis
|
||||
.get_many_deserialized_from_json::<JavaServerPing>(
|
||||
.get_many_deserialized::<JavaServerPing>(
|
||||
server_ping::REDIS_NAMESPACE,
|
||||
&project_ids
|
||||
.iter()
|
||||
|
||||
@@ -1,4 +1,6 @@
|
||||
use crate::database::PgPool;
|
||||
use crate::database::models::notification_item::DBNotification;
|
||||
use crate::models::ids::NotificationId;
|
||||
use crate::models::notifications::Notification;
|
||||
use crate::queue::socket::ActiveSockets;
|
||||
use crate::routes::internal::statuses::{
|
||||
@@ -13,10 +15,9 @@ use redis::{RedisWrite, ToRedisArgs};
|
||||
use serde::{Deserialize, Serialize};
|
||||
use tokio_stream::StreamExt;
|
||||
|
||||
pub const FRIENDS_CHANNEL_NAME: &str = "friends";
|
||||
pub const FRIENDS_CHANNEL_NAME: &str = "friends:v1";
|
||||
|
||||
#[derive(Serialize, Deserialize)]
|
||||
#[serde(tag = "type", rename_all = "snake_case")]
|
||||
pub enum RedisFriendsMessage {
|
||||
StatusUpdate {
|
||||
status: UserStatus,
|
||||
@@ -30,7 +31,7 @@ pub enum RedisFriendsMessage {
|
||||
},
|
||||
Notification {
|
||||
to_user: UserId,
|
||||
notification: Notification,
|
||||
notification_id: NotificationId,
|
||||
},
|
||||
}
|
||||
|
||||
@@ -39,7 +40,7 @@ impl ToRedisArgs for RedisFriendsMessage {
|
||||
where
|
||||
W: ?Sized + RedisWrite,
|
||||
{
|
||||
out.write_arg(&serde_json::to_vec(&self).unwrap())
|
||||
out.write_arg(&postcard::to_allocvec(&self).unwrap())
|
||||
}
|
||||
}
|
||||
|
||||
@@ -54,7 +55,7 @@ pub async fn handle_pubsub(
|
||||
if message.get_channel_name() != FRIENDS_CHANNEL_NAME {
|
||||
continue;
|
||||
}
|
||||
let payload = serde_json::from_slice(message.get_payload_bytes());
|
||||
let payload = postcard::from_bytes(message.get_payload_bytes());
|
||||
|
||||
let pool = pool.clone();
|
||||
let sockets = sockets.clone();
|
||||
@@ -94,14 +95,18 @@ pub async fn handle_pubsub(
|
||||
|
||||
Ok(RedisFriendsMessage::Notification {
|
||||
to_user,
|
||||
notification,
|
||||
notification_id,
|
||||
}) => {
|
||||
let _ = send_notification_to_user(
|
||||
&sockets,
|
||||
to_user,
|
||||
¬ification,
|
||||
)
|
||||
.await;
|
||||
if let Ok(Some(notification)) =
|
||||
DBNotification::get(notification_id.into(), &pool).await
|
||||
{
|
||||
let _ = send_notification_to_user(
|
||||
&sockets,
|
||||
to_user,
|
||||
&Notification::from(notification),
|
||||
)
|
||||
.await;
|
||||
}
|
||||
}
|
||||
|
||||
Err(_) => {}
|
||||
|
||||
@@ -17,10 +17,10 @@ pub async fn get_user_status(
|
||||
|
||||
if let Ok(mut conn) = redis.pool.get().await
|
||||
&& let Ok(mut statuses) =
|
||||
conn.sscan::<_, String>(get_field_name(user)).await
|
||||
&& let Some(status_json) = statuses.next_item().await
|
||||
conn.sscan::<_, Vec<u8>>(get_field_name(user)).await
|
||||
&& let Some(status) = statuses.next_item().await
|
||||
{
|
||||
return serde_json::from_str::<UserStatus>(&status_json).ok();
|
||||
return postcard::from_bytes::<UserStatus>(&status).ok();
|
||||
}
|
||||
|
||||
None
|
||||
@@ -40,11 +40,11 @@ pub async fn replace_user_status(
|
||||
let mut pipe = redis::pipe();
|
||||
pipe.atomic();
|
||||
if let Some(status) = old_status {
|
||||
pipe.srem(&field_name, serde_json::to_string(&status).unwrap())
|
||||
pipe.srem(&field_name, postcard::to_allocvec(status).unwrap())
|
||||
.ignore();
|
||||
}
|
||||
if let Some(status) = new_status {
|
||||
pipe.sadd(&field_name, serde_json::to_string(&status).unwrap())
|
||||
pipe.sadd(&field_name, postcard::to_allocvec(status).unwrap())
|
||||
.ignore();
|
||||
pipe.expire(&field_name, EXPIRY_TIME_SECONDS).ignore();
|
||||
}
|
||||
@@ -65,5 +65,5 @@ pub async fn push_back_user_expiry(
|
||||
}
|
||||
|
||||
fn get_field_name(user: UserId) -> String {
|
||||
format!("user_status:{user}")
|
||||
format!("user_status:v1:{user}")
|
||||
}
|
||||
|
||||
@@ -14,7 +14,7 @@ pub const MODRINTH_GENERATED_PDF_TYPE: HeaderName =
|
||||
HeaderName::from_static("modrinth-generated-pdf-type");
|
||||
pub const MODRINTH_PAYMENT_ID: HeaderName =
|
||||
HeaderName::from_static("modrinth-payment-id");
|
||||
pub const PAYMENT_STATEMENTS_NAMESPACE: &str = "payment_statements";
|
||||
pub const PAYMENT_STATEMENTS_NAMESPACE: &str = "payment_statements:v1";
|
||||
|
||||
#[derive(Serialize, Deserialize, Debug, Clone)]
|
||||
pub struct PaymentStatement {
|
||||
@@ -203,7 +203,7 @@ impl GotenbergClient {
|
||||
.wrap_internal_err("failed to get document over Redis")?
|
||||
.wrap_internal_err("no document was returned from Redis")?;
|
||||
|
||||
let document = serde_json::from_str::<
|
||||
let document = postcard::from_bytes::<
|
||||
Result<GotenbergDocument, GotenbergError>,
|
||||
>(&document)
|
||||
.wrap_internal_err("failed to deserialize Redis document response")?
|
||||
|
||||
@@ -14,6 +14,7 @@ pub mod http;
|
||||
pub mod img;
|
||||
pub mod ip;
|
||||
pub mod kafka;
|
||||
pub mod neverbounce;
|
||||
pub mod ratelimit;
|
||||
pub mod redis;
|
||||
pub mod routes;
|
||||
|
||||
@@ -0,0 +1,94 @@
|
||||
use std::time::Instant;
|
||||
|
||||
use eyre::{WrapErr, eyre};
|
||||
use neverbounce::{
|
||||
ResponseStatus, SingleCheckParams, SingleCheckResponse, VerificationResult,
|
||||
};
|
||||
use tracing::{debug, error};
|
||||
|
||||
use crate::env::ENV;
|
||||
use crate::util::http::HTTP_CLIENT;
|
||||
|
||||
pub async fn check_email(email: &str) -> eyre::Result<VerificationResult> {
|
||||
if ENV.NEVERBOUNCE_API_KEY.is_empty() {
|
||||
debug!(
|
||||
result = "unknown",
|
||||
"NeverBounce email check skipped because API key is not set",
|
||||
);
|
||||
return Ok(VerificationResult::Unknown);
|
||||
}
|
||||
|
||||
let params = SingleCheckParams::new(&ENV.NEVERBOUNCE_API_KEY, email)
|
||||
.with_api_url(&ENV.NEVERBOUNCE_BASE_URL);
|
||||
|
||||
let check_time_start = Instant::now();
|
||||
|
||||
let response = match neverbounce::single_check(&HTTP_CLIENT, ¶ms).await
|
||||
{
|
||||
Ok(response) => response,
|
||||
Err(source) => {
|
||||
error!(
|
||||
result = "unknown",
|
||||
error = ?source,
|
||||
"NeverBounce email check failed",
|
||||
);
|
||||
return Err(eyre!(source)).wrap_err("failed to check email");
|
||||
}
|
||||
};
|
||||
|
||||
let SingleCheckResponse { status, result, .. } = response;
|
||||
|
||||
let check_time = check_time_start.elapsed();
|
||||
|
||||
match status {
|
||||
ResponseStatus::Success => {
|
||||
let result = result.ok_or_else(|| {
|
||||
error!(result = "unknown", "NeverBounce email check failed",);
|
||||
eyre!("")
|
||||
})?;
|
||||
|
||||
if matches!(result, VerificationResult::Unrecognized(_)) {
|
||||
error!(
|
||||
result = result.as_str(),
|
||||
request.time_ms = check_time.as_millis(),
|
||||
"NeverBounce email check failed",
|
||||
);
|
||||
return Err(email_check_error_generic());
|
||||
}
|
||||
|
||||
debug!(
|
||||
result = result.as_str(),
|
||||
request.time_ms = check_time.as_millis(),
|
||||
"NeverBounce email check succeeded",
|
||||
);
|
||||
Ok(result)
|
||||
}
|
||||
failure_type => {
|
||||
let result = result.unwrap_or(VerificationResult::Unknown);
|
||||
error!(
|
||||
failure_type = response_failure_type(&failure_type),
|
||||
result = result.as_str(),
|
||||
request.time_ms = check_time.as_millis(),
|
||||
"NeverBounce email check failed",
|
||||
);
|
||||
Err(email_check_error_generic())
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub fn email_check_error_generic() -> eyre::Error {
|
||||
eyre!("Please try a different email address!")
|
||||
}
|
||||
|
||||
fn response_failure_type(status: &ResponseStatus) -> &str {
|
||||
match status {
|
||||
ResponseStatus::Success => "success",
|
||||
ResponseStatus::GeneralFailure => "general_failure",
|
||||
ResponseStatus::AuthFailure => "auth_failure",
|
||||
ResponseStatus::TemporarilyUnavailable => "temp_unavail",
|
||||
ResponseStatus::ThrottleTriggered => "throttle_triggered",
|
||||
ResponseStatus::BadReferrer => "bad_referrer",
|
||||
ResponseStatus::Unrecognized(status) => status,
|
||||
_ => "unrecognized",
|
||||
}
|
||||
}
|
||||
@@ -11,7 +11,7 @@ use chrono::Utc;
|
||||
use std::str::FromStr;
|
||||
use std::sync::Arc;
|
||||
|
||||
const RATE_LIMIT_NAMESPACE: &str = "rate_limit";
|
||||
const RATE_LIMIT_NAMESPACE: &str = "rate_limit:v1";
|
||||
const RATE_LIMIT_EXPIRY: i64 = 300; // 5 minutes
|
||||
const MINUTE_IN_NANOS: i64 = 60_000_000_000;
|
||||
|
||||
|
||||
@@ -18,9 +18,11 @@ use common::environment::{
|
||||
use common::permissions::{PermissionsTest, PermissionsTestContext};
|
||||
use futures::StreamExt;
|
||||
use hex::ToHex;
|
||||
use labrinth::database::models::DBProjectId;
|
||||
use labrinth::database::models::project_item::{
|
||||
PROJECTS_NAMESPACE, PROJECTS_SLUGS_NAMESPACE,
|
||||
PROJECTS_NAMESPACE, PROJECTS_SLUGS_NAMESPACE, ProjectQueryResult,
|
||||
};
|
||||
use labrinth::database::redis::RedisValue;
|
||||
use labrinth::models::ids::ProjectId;
|
||||
use labrinth::models::teams::ProjectPermissions;
|
||||
use labrinth::util::actix::{MultipartSegment, MultipartSegmentData};
|
||||
@@ -67,19 +69,21 @@ async fn test_get_project() {
|
||||
Some(parse_base62(alpha_project_id).unwrap() as i64)
|
||||
);
|
||||
|
||||
let cached_project = redis_pool
|
||||
.get(
|
||||
let cached_project: RedisValue<
|
||||
ProjectQueryResult,
|
||||
DBProjectId,
|
||||
String,
|
||||
> = redis_pool
|
||||
.get_deserialized(
|
||||
PROJECTS_NAMESPACE,
|
||||
&parse_base62(alpha_project_id).unwrap().to_string(),
|
||||
)
|
||||
.await
|
||||
.unwrap()
|
||||
.unwrap();
|
||||
let cached_project: serde_json::Value =
|
||||
serde_json::from_str(&cached_project).unwrap();
|
||||
assert_eq!(
|
||||
cached_project["val"]["inner"]["slug"],
|
||||
json!(alpha_project_slug)
|
||||
cached_project.value().inner.slug.as_ref(),
|
||||
Some(alpha_project_slug)
|
||||
);
|
||||
|
||||
// Make the request again, this time it should be cached
|
||||
|
||||
@@ -14,7 +14,11 @@ use common::asserts::assert_common_version_ids;
|
||||
use common::database::USER_USER_PAT;
|
||||
use common::environment::{with_test_environment, with_test_environment_all};
|
||||
use futures::StreamExt;
|
||||
use labrinth::database::models::version_item::VERSIONS_NAMESPACE;
|
||||
use labrinth::database::models::DBVersionId;
|
||||
use labrinth::database::models::version_item::{
|
||||
VERSIONS_NAMESPACE, VersionQueryResult,
|
||||
};
|
||||
use labrinth::database::redis::RedisValue;
|
||||
use labrinth::models::ids::VersionId;
|
||||
use labrinth::models::projects::{
|
||||
Dependency, DependencyType, VersionStatus, VersionType,
|
||||
@@ -47,18 +51,20 @@ async fn test_get_version() {
|
||||
assert_eq!(&version.id.to_string(), alpha_version_id);
|
||||
|
||||
let mut redis_conn = test_env.db.redis_pool.connect().await.unwrap();
|
||||
let cached_project = redis_conn
|
||||
.get(
|
||||
let cached_version: RedisValue<
|
||||
VersionQueryResult,
|
||||
DBVersionId,
|
||||
String,
|
||||
> = redis_conn
|
||||
.get_deserialized(
|
||||
VERSIONS_NAMESPACE,
|
||||
&parse_base62(alpha_version_id).unwrap().to_string(),
|
||||
)
|
||||
.await
|
||||
.unwrap()
|
||||
.unwrap();
|
||||
let cached_project: serde_json::Value =
|
||||
serde_json::from_str(&cached_project).unwrap();
|
||||
assert_eq!(
|
||||
cached_project["val"]["inner"]["project_id"],
|
||||
cached_version.value().inner.project_id.0,
|
||||
json!(parse_base62(alpha_project_id).unwrap())
|
||||
);
|
||||
|
||||
|
||||
@@ -0,0 +1,17 @@
|
||||
[package]
|
||||
name = "neverbounce"
|
||||
version = "0.1.0"
|
||||
edition.workspace = true
|
||||
rust-version.workspace = true
|
||||
description = "NeverBounce API client"
|
||||
repository.workspace = true
|
||||
license = "MIT"
|
||||
keywords = []
|
||||
categories = ["api-bindings"]
|
||||
|
||||
[dependencies]
|
||||
reqwest = { workspace = true, features = ["json"] }
|
||||
serde = { workspace = true, features = ["derive"] }
|
||||
|
||||
[lints]
|
||||
workspace = true
|
||||
@@ -0,0 +1,232 @@
|
||||
use reqwest::Client;
|
||||
use serde::{Deserialize, Deserializer, Serialize};
|
||||
use std::time::Duration;
|
||||
|
||||
pub const DEFAULT_API_URL: &str = "https://api.neverbounce.com";
|
||||
pub const SINGLE_CHECK_PATH: &str = "/v4/single/check";
|
||||
pub const TIMEOUT: Duration = Duration::from_secs(10);
|
||||
|
||||
/// Authentication and email parameters for a single verification.
|
||||
///
|
||||
/// The API key is sent in the JSON request body.
|
||||
#[derive(Clone, Copy)]
|
||||
pub struct SingleCheckParams<'a> {
|
||||
pub api_url: &'a str,
|
||||
pub api_key: &'a str,
|
||||
pub email: &'a str,
|
||||
}
|
||||
|
||||
impl<'a> SingleCheckParams<'a> {
|
||||
#[must_use]
|
||||
pub const fn new(api_key: &'a str, email: &'a str) -> Self {
|
||||
Self {
|
||||
api_url: DEFAULT_API_URL,
|
||||
api_key,
|
||||
email,
|
||||
}
|
||||
}
|
||||
|
||||
#[must_use]
|
||||
pub const fn with_api_url(mut self, api_url: &'a str) -> Self {
|
||||
self.api_url = api_url;
|
||||
self
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, PartialEq, Eq, Deserialize)]
|
||||
#[non_exhaustive]
|
||||
pub struct SingleCheckResponse {
|
||||
pub status: ResponseStatus,
|
||||
#[serde(default)]
|
||||
pub result: Option<VerificationResult>,
|
||||
#[serde(default)]
|
||||
pub flags: Vec<VerificationFlag>,
|
||||
#[serde(default)]
|
||||
pub suggested_correction: Option<String>,
|
||||
#[serde(default)]
|
||||
pub retry_token: Option<String>,
|
||||
#[serde(default)]
|
||||
pub message: Option<String>,
|
||||
#[serde(default)]
|
||||
pub execution_time: Option<u64>,
|
||||
}
|
||||
|
||||
impl SingleCheckResponse {
|
||||
#[must_use]
|
||||
pub fn is_safe_to_send(&self) -> bool {
|
||||
matches!(self.status, ResponseStatus::Success)
|
||||
&& matches!(self.result.as_ref(), Some(VerificationResult::Valid))
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, PartialEq, Eq)]
|
||||
#[non_exhaustive]
|
||||
pub enum ResponseStatus {
|
||||
Success,
|
||||
GeneralFailure,
|
||||
AuthFailure,
|
||||
TemporarilyUnavailable,
|
||||
ThrottleTriggered,
|
||||
BadReferrer,
|
||||
Unrecognized(String),
|
||||
}
|
||||
|
||||
impl<'de> Deserialize<'de> for ResponseStatus {
|
||||
fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
|
||||
where
|
||||
D: Deserializer<'de>,
|
||||
{
|
||||
Ok(match String::deserialize(deserializer)?.as_str() {
|
||||
"success" => Self::Success,
|
||||
"general_failure" => Self::GeneralFailure,
|
||||
"auth_failure" => Self::AuthFailure,
|
||||
"temp_unavail" => Self::TemporarilyUnavailable,
|
||||
"throttle_triggered" => Self::ThrottleTriggered,
|
||||
"bad_referrer" => Self::BadReferrer,
|
||||
value => Self::Unrecognized(value.to_owned()),
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, PartialEq, Eq)]
|
||||
#[non_exhaustive]
|
||||
pub enum VerificationResult {
|
||||
Valid,
|
||||
Invalid,
|
||||
Disposable,
|
||||
CatchAll,
|
||||
Unknown,
|
||||
Unrecognized(String),
|
||||
}
|
||||
|
||||
impl VerificationResult {
|
||||
pub fn as_str(&self) -> &str {
|
||||
match self {
|
||||
VerificationResult::Valid => "valid",
|
||||
VerificationResult::Invalid => "invalid",
|
||||
VerificationResult::Disposable => "disposable",
|
||||
VerificationResult::CatchAll => "catchall",
|
||||
VerificationResult::Unknown => "unknown",
|
||||
VerificationResult::Unrecognized(other) => other,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl<'de> Deserialize<'de> for VerificationResult {
|
||||
fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
|
||||
where
|
||||
D: Deserializer<'de>,
|
||||
{
|
||||
Ok(match String::deserialize(deserializer)?.as_str() {
|
||||
"valid" => Self::Valid,
|
||||
"invalid" => Self::Invalid,
|
||||
"disposable" => Self::Disposable,
|
||||
"catchall" => Self::CatchAll,
|
||||
"unknown" => Self::Unknown,
|
||||
value => Self::Unrecognized(value.to_owned()),
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, PartialEq, Eq)]
|
||||
#[non_exhaustive]
|
||||
pub enum VerificationFlag {
|
||||
HasDns,
|
||||
HasDnsMx,
|
||||
BadSyntax,
|
||||
FreeEmailHost,
|
||||
Profanity,
|
||||
RoleAccount,
|
||||
DisposableEmail,
|
||||
GovernmentHost,
|
||||
AcademicHost,
|
||||
MilitaryHost,
|
||||
InternationalHost,
|
||||
SquatterHost,
|
||||
SpellingMistake,
|
||||
BadDns,
|
||||
TemporaryDnsError,
|
||||
ConnectFails,
|
||||
AcceptsAll,
|
||||
ContainsAlias,
|
||||
ContainsSubdomain,
|
||||
SmtpConnectable,
|
||||
SpamtrapNetwork,
|
||||
HistoricalResponse,
|
||||
Unrecognized(String),
|
||||
}
|
||||
|
||||
impl<'de> Deserialize<'de> for VerificationFlag {
|
||||
fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
|
||||
where
|
||||
D: Deserializer<'de>,
|
||||
{
|
||||
Ok(match String::deserialize(deserializer)?.as_str() {
|
||||
"has_dns" => Self::HasDns,
|
||||
"has_dns_mx" => Self::HasDnsMx,
|
||||
"bad_syntax" => Self::BadSyntax,
|
||||
"free_email_host" => Self::FreeEmailHost,
|
||||
"profanity" => Self::Profanity,
|
||||
"role_account" => Self::RoleAccount,
|
||||
"disposable_email" => Self::DisposableEmail,
|
||||
"government_host" => Self::GovernmentHost,
|
||||
"academic_host" => Self::AcademicHost,
|
||||
"military_host" => Self::MilitaryHost,
|
||||
"international_host" => Self::InternationalHost,
|
||||
"squatter_host" => Self::SquatterHost,
|
||||
"spelling_mistake" => Self::SpellingMistake,
|
||||
"bad_dns" => Self::BadDns,
|
||||
"temporary_dns_error" => Self::TemporaryDnsError,
|
||||
"connect_fails" => Self::ConnectFails,
|
||||
"accepts_all" => Self::AcceptsAll,
|
||||
"contains_alias" => Self::ContainsAlias,
|
||||
"contains_subdomain" => Self::ContainsSubdomain,
|
||||
"smtp_connectable" => Self::SmtpConnectable,
|
||||
"spamtrap_network" => Self::SpamtrapNetwork,
|
||||
"historical_response" => Self::HistoricalResponse,
|
||||
value => Self::Unrecognized(value.to_owned()),
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Serialize)]
|
||||
struct SingleCheckRequest<'a> {
|
||||
key: &'a str,
|
||||
email: &'a str,
|
||||
timeout: u64,
|
||||
}
|
||||
|
||||
/// Verifies one email address using NeverBounce's single-check endpoint.
|
||||
///
|
||||
/// This endpoint should only be called in response to an action such as a form
|
||||
/// submission. Existing lists and databases must use NeverBounce's bulk API.
|
||||
/// Both the server verification timeout and the complete HTTP request timeout
|
||||
/// are ten seconds.
|
||||
pub async fn single_check(
|
||||
client: &Client,
|
||||
params: &SingleCheckParams<'_>,
|
||||
) -> reqwest::Result<SingleCheckResponse> {
|
||||
single_check_request(client, params)
|
||||
.send()
|
||||
.await?
|
||||
.error_for_status()?
|
||||
.json()
|
||||
.await
|
||||
}
|
||||
|
||||
fn single_check_request(
|
||||
client: &Client,
|
||||
params: &SingleCheckParams<'_>,
|
||||
) -> reqwest::RequestBuilder {
|
||||
client
|
||||
.post(format!(
|
||||
"{}{SINGLE_CHECK_PATH}",
|
||||
params.api_url.trim_end_matches('/')
|
||||
))
|
||||
.timeout(TIMEOUT)
|
||||
.json(&SingleCheckRequest {
|
||||
key: params.api_key,
|
||||
email: params.email,
|
||||
timeout: TIMEOUT.as_secs(),
|
||||
})
|
||||
}
|
||||
Reference in New Issue
Block a user