diff --git a/Cargo.lock b/Cargo.lock
index 755b84a..694711c 100644
--- a/Cargo.lock
+++ b/Cargo.lock
@@ -310,9 +310,9 @@ dependencies = [
[[package]]
name = "base32"
-version = "0.4.0"
+version = "0.5.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "23ce669cd6c8588f79e15cf450314f9638f967fc5770ff1c7c1deb0925ea7cfa"
+checksum = "022dfe9eb35f19ebbcb51e0b40a5ab759f46ad60cadf7297e0bd085afb50e076"
[[package]]
name = "base64"
@@ -623,9 +623,9 @@ dependencies = [
[[package]]
name = "comrak"
-version = "0.37.0"
+version = "0.38.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "2a4f05e73ca9a30af27bebc13600f91fd1651b2ec7d139ca82a89df7ca583af1"
+checksum = "f690706b5db081dccea6206d7f6d594bb9895599abea9d1a0539f13888781ae8"
dependencies = [
"bon",
"caseless",
@@ -642,9 +642,9 @@ dependencies = [
[[package]]
name = "constant_time_eq"
-version = "0.2.6"
+version = "0.3.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "21a53c0a4d288377e7415b53dcfc3c04da5cdc2cc95c8d5ac178b58f0b861ad6"
+checksum = "7c74b8349d32d297c9134b8c88677813a227df8f779daa29bfc29c183fe3dca6"
[[package]]
name = "cookie"
@@ -1649,9 +1649,9 @@ checksum = "8355be11b20d696c8f18f6cc018c4e372165b1fa8126cef092399c9951984ffa"
[[package]]
name = "libsqlite3-sys"
-version = "0.32.0"
+version = "0.33.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "fbb8270bb4060bd76c6e96f20c52d80620f1d82a3470885694e41e0f81ef6fe7"
+checksum = "947e6816f7825b2b45027c2c32e7085da9934defa535de4a6a46b10a4d5257fa"
dependencies = [
"pkg-config",
"vcpkg",
@@ -2212,7 +2212,7 @@ dependencies = [
"hmac",
"md-5",
"memchr",
- "rand 0.9.0",
+ "rand 0.9.1",
"sha2",
"stringprep",
]
@@ -2356,13 +2356,12 @@ dependencies = [
[[package]]
name = "rand"
-version = "0.9.0"
+version = "0.9.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "3779b94aeb87e8bd4e834cee3650289ee9e0d5677f976ecdb6d219e5f4f6cd94"
+checksum = "9fbfd9d094a40bf3ae768db9361049ace4c0e04a4fd6b359518bd7b73a73dd97"
dependencies = [
"rand_chacha 0.9.0",
"rand_core 0.9.3",
- "zerocopy",
]
[[package]]
@@ -2475,9 +2474,9 @@ dependencies = [
[[package]]
name = "redis"
-version = "0.29.2"
+version = "0.29.5"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "b110459d6e323b7cda23980c46c77157601199c9da6241552b284cd565a7a133"
+checksum = "1bc42f3a12fd4408ce64d8efef67048a924e543bd35c6591c0447fda9054695f"
dependencies = [
"arc-swap",
"combine",
@@ -2611,9 +2610,9 @@ dependencies = [
[[package]]
name = "rusqlite"
-version = "0.34.0"
+version = "0.35.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "37e34486da88d8e051c7c0e23c3f15fd806ea8546260aa2fec247e97242ec143"
+checksum = "a22715a5d6deef63c637207afbe68d0c72c3f8d0022d7cf9714c442d6157606b"
dependencies = [
"bitflags 2.9.0",
"fallible-iterator 0.3.0",
@@ -3155,7 +3154,7 @@ dependencies = [
[[package]]
name = "tetratto"
-version = "1.0.5"
+version = "1.0.6"
dependencies = [
"ammonia",
"axum",
@@ -3180,7 +3179,7 @@ dependencies = [
[[package]]
name = "tetratto-core"
-version = "1.0.5"
+version = "1.0.6"
dependencies = [
"async-recursion",
"bb8-postgres",
@@ -3199,7 +3198,7 @@ dependencies = [
[[package]]
name = "tetratto-l10n"
-version = "1.0.5"
+version = "1.0.6"
dependencies = [
"pathbufd",
"serde",
@@ -3208,14 +3207,14 @@ dependencies = [
[[package]]
name = "tetratto-shared"
-version = "1.0.5"
+version = "1.0.6"
dependencies = [
"ammonia",
"chrono",
"comrak",
"hex_fmt",
"num-bigint",
- "rand 0.9.0",
+ "rand 0.9.1",
"serde",
"sha2",
"uuid",
@@ -3395,7 +3394,7 @@ dependencies = [
"pin-project-lite",
"postgres-protocol",
"postgres-types",
- "rand 0.9.0",
+ "rand 0.9.1",
"socket2",
"tokio",
"tokio-util",
@@ -3461,15 +3460,15 @@ dependencies = [
[[package]]
name = "totp-rs"
-version = "5.6.0"
+version = "5.7.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "17b2f27dad992486c26b4e7455f38aa487e838d6d61b57e72906ee2b8c287a90"
+checksum = "f124352108f58ef88299e909f6e9470f1cdc8d2a1397963901b4a6366206bf72"
dependencies = [
"base32",
"constant_time_eq",
"hmac",
"qrcodegen-image",
- "rand 0.8.5",
+ "rand 0.9.1",
"sha1",
"sha2",
"url",
diff --git a/crates/app/Cargo.toml b/crates/app/Cargo.toml
index 7da2012..e46c7c8 100644
--- a/crates/app/Cargo.toml
+++ b/crates/app/Cargo.toml
@@ -1,6 +1,6 @@
[package]
name = "tetratto"
-version = "1.0.5"
+version = "1.0.6"
edition = "2024"
[features]
diff --git a/crates/app/src/langs/en-US.toml b/crates/app/src/langs/en-US.toml
index 2376c76..cd6fb41 100644
--- a/crates/app/src/langs/en-US.toml
+++ b/crates/app/src/langs/en-US.toml
@@ -41,10 +41,11 @@ version = "1.0.0"
"auth:action.login" = "Login"
"auth:action.register" = "Register"
"auth:action.logout" = "Logout"
-"auto:action.follow" = "Follow"
-"auto:action.unfollow" = "Unfollow"
-"auto:action.block" = "Block"
-"auto:action.unblock" = "Unblock"
+"auth:action.follow" = "Follow"
+"auth:action.unfollow" = "Unfollow"
+"auth:action.block" = "Block"
+"auth:action.ip_block" = "IP block"
+"auth:action.unblock" = "Unblock"
"auth:link.my_profile" = "My profile"
"auth:link.settings" = "Settings"
"auth:label.followers" = "Followers"
@@ -55,8 +56,8 @@ version = "1.0.0"
"auth:label.before_you_view" = "Before you view"
"auth:label.private_profile" = "Private profile"
"auth:label.private_profile_message" = "This profile is private, meaning you can only view it if they follow you."
-"auto:action.request_to_follow" = "Request to follow"
-"auto:action.cancel_follow_request" = "Cancel follow request"
+"auth:action.request_to_follow" = "Request to follow"
+"auth:action.cancel_follow_request" = "Cancel follow request"
"communities:action.create" = "Create"
"communities:action.select" = "Select"
diff --git a/crates/app/src/public/html/misc/requests.html b/crates/app/src/public/html/misc/requests.html
index 0715c85..8e96287 100644
--- a/crates/app/src/public/html/misc/requests.html
+++ b/crates/app/src/public/html/misc/requests.html
@@ -110,7 +110,8 @@
-
+
+
diff --git a/crates/app/src/public/html/profile/base.html b/crates/app/src/public/html/profile/base.html
index f70ad7b..b971af6 100644
--- a/crates/app/src/public/html/profile/base.html
+++ b/crates/app/src/public/html/profile/base.html
@@ -156,7 +156,7 @@
atto_tag="user.follow"
>
{{ icon "user-plus" }}
- {{ text "auto:action.follow" }}
+ {{ text "auth:action.follow" }}
{% else %}
{% endif %} {% if is_helper %}
{{ components::create_question_form(receiver=profile.id,
header=profile.settings.motivational_header) }}
diff --git a/crates/app/src/public/html/profile/private.html b/crates/app/src/public/html/profile/private.html
index f74f8ad..d31e924 100644
--- a/crates/app/src/public/html/profile/private.html
+++ b/crates/app/src/public/html/profile/private.html
@@ -25,7 +25,7 @@
atto_tag="user.follow_request"
>
{{ icon "user-plus" }}
- {{ text "auto:action.request_to_follow" }}
+ {{ text "auth:action.request_to_follow" }}
{% else %}
{% endif %}
diff --git a/crates/app/src/public/html/profile/settings.html b/crates/app/src/public/html/profile/settings.html
index 5e8e338..632ee0f 100644
--- a/crates/app/src/public/html/profile/settings.html
+++ b/crates/app/src/public/html/profile/settings.html
@@ -748,20 +748,6 @@
settings.warning,
"textarea",
],
- [[], "Questions", "title"],
- [
- [
- "enable_questions",
- "Allow users to ask you questions",
- ],
- "{{ profile.settings.enable_questions }}",
- "checkbox",
- ],
- [
- ["motivational_header", "Motivational header"],
- settings.motivational_header,
- "input",
- ],
],
settings,
);
@@ -791,6 +777,28 @@
"{{ profile.settings.private_last_seen }}",
"checkbox",
],
+ [[], "Questions", "title"],
+ [
+ [
+ "enable_questions",
+ "Allow users to ask you questions",
+ ],
+ "{{ profile.settings.enable_questions }}",
+ "checkbox",
+ ],
+ [
+ [
+ "allow_anonymous_questions",
+ "Allow anonymous questions",
+ ],
+ "{{ profile.settings.allow_anonymous_questions }}",
+ "checkbox",
+ ],
+ [
+ ["motivational_header", "Motivational header"],
+ settings.motivational_header,
+ "input",
+ ],
],
settings,
);
diff --git a/crates/app/src/public/js/me.js b/crates/app/src/public/js/me.js
index 19105bf..0dd48fe 100644
--- a/crates/app/src/public/js/me.js
+++ b/crates/app/src/public/js/me.js
@@ -214,6 +214,27 @@
});
});
+ self.define("ip_block_question", async (_, id) => {
+ if (
+ !(await trigger("atto::confirm", [
+ "Are you sure you want to do this?",
+ ]))
+ ) {
+ return;
+ }
+
+ fetch(`/api/v1/questions/${id}/block_ip`, {
+ method: "POST",
+ })
+ .then((res) => res.json())
+ .then((res) => {
+ trigger("atto::toast", [
+ res.ok ? "success" : "error",
+ res.message,
+ ]);
+ });
+ });
+
// token switcher
self.define(
"set_login_account_tokens",
diff --git a/crates/app/src/routes/api/v1/auth/social.rs b/crates/app/src/routes/api/v1/auth/social.rs
index b9c91b5..e6d3bbc 100644
--- a/crates/app/src/routes/api/v1/auth/social.rs
+++ b/crates/app/src/routes/api/v1/auth/social.rs
@@ -4,7 +4,7 @@ use crate::{
};
use axum::{Extension, Json, extract::Path, response::IntoResponse};
use axum_extra::extract::CookieJar;
-use tetratto_core::model::auth::{FollowResult, Notification, UserBlock, UserFollow};
+use tetratto_core::model::auth::{FollowResult, IpBlock, Notification, UserBlock, UserFollow};
/// Toggle following on the given user.
pub async fn follow_request(
@@ -197,3 +197,38 @@ pub async fn block_request(
}
}
}
+
+/// Toggle IP blocking on the given IP.
+pub async fn ip_block_request(
+ jar: CookieJar,
+ Path(ip): Path,
+ Extension(data): Extension,
+) -> impl IntoResponse {
+ let data = &(data.read().await).0;
+ let user = match get_user_from_token!(jar, data) {
+ Some(ua) => ua,
+ None => return Json(Error::NotAllowed.into()),
+ };
+
+ if let Ok(ipblock) = data.get_ipblock_by_initiator_receiver(user.id, &ip).await {
+ // delete
+ match data.delete_ipblock(ipblock.id, user).await {
+ Ok(_) => Json(ApiReturn {
+ ok: true,
+ message: "IP unblocked".to_string(),
+ payload: (),
+ }),
+ Err(e) => Json(e.into()),
+ }
+ } else {
+ // create
+ match data.create_ipblock(IpBlock::new(user.id, ip)).await {
+ Ok(_) => Json(ApiReturn {
+ ok: true,
+ message: "IP blocked".to_string(),
+ payload: (),
+ }),
+ Err(e) => Json(e.into()),
+ }
+ }
+}
diff --git a/crates/app/src/routes/api/v1/communities/questions.rs b/crates/app/src/routes/api/v1/communities/questions.rs
index 2e26fa5..b4b27cc 100644
--- a/crates/app/src/routes/api/v1/communities/questions.rs
+++ b/crates/app/src/routes/api/v1/communities/questions.rs
@@ -1,27 +1,49 @@
-use axum::{Extension, Json, extract::Path, response::IntoResponse};
+use axum::{
+ extract::Path,
+ http::{HeaderMap, HeaderValue},
+ response::IntoResponse,
+ Extension, Json,
+};
use axum_extra::extract::CookieJar;
-use tetratto_core::model::{communities::Question, ApiReturn, Error};
+use tetratto_core::model::{auth::IpBlock, communities::Question, ApiReturn, Error};
use crate::{get_user_from_token, routes::api::v1::CreateQuestion, State};
pub async fn create_request(
jar: CookieJar,
+ headers: HeaderMap,
Extension(data): Extension,
Json(req): Json,
) -> impl IntoResponse {
let data = &(data.read().await).0;
- let user = match get_user_from_token!(jar, data) {
- Some(ua) => ua,
- None => return Json(Error::NotAllowed.into()),
- };
+ let user = get_user_from_token!(jar, data);
+ if req.is_global && user.is_none() {
+ return Json(Error::NotAllowed.into());
+ }
+
+ // get real ip
+ let real_ip = headers
+ .get(data.0.security.real_ip_header.to_owned())
+ .unwrap_or(&HeaderValue::from_static(""))
+ .to_str()
+ .unwrap_or("")
+ .to_string();
+
+ // check for ip ban
+ if data.get_ipban_by_ip(&real_ip).await.is_ok() {
+ return Json(Error::NotAllowed.into());
+ }
+
+ // ...
let mut props = Question::new(
- user.id,
+ if let Some(ref ua) = user { ua.id } else { 0 },
match req.receiver.parse::() {
Ok(x) => x,
Err(e) => return Json(Error::MiscError(e.to_string()).into()),
},
req.content,
req.is_global,
+ real_ip,
);
if !req.community.is_empty() {
@@ -63,3 +85,43 @@ pub async fn delete_request(
Err(e) => Json(e.into()),
}
}
+
+pub async fn ip_block_request(
+ jar: CookieJar,
+ Extension(data): Extension,
+ Path(id): Path,
+) -> impl IntoResponse {
+ let data = &(data.read().await).0;
+ let user = match get_user_from_token!(jar, data) {
+ Some(ua) => ua,
+ None => return Json(Error::NotAllowed.into()),
+ };
+
+ // get question
+ let question = match data.get_question_by_id(id).await {
+ Ok(q) => q,
+ Err(e) => return Json(e.into()),
+ };
+
+ // check for an existing ip block
+ if data
+ .get_ipblock_by_initiator_receiver(user.id, &question.ip)
+ .await
+ .is_ok()
+ {
+ return Json(Error::NotAllowed.into());
+ }
+
+ // create ip block
+ match data
+ .create_ipblock(IpBlock::new(user.id, question.ip))
+ .await
+ {
+ Ok(_) => Json(ApiReturn {
+ ok: true,
+ message: "IP blocked".to_string(),
+ payload: (),
+ }),
+ Err(e) => Json(e.into()),
+ }
+}
diff --git a/crates/app/src/routes/api/v1/mod.rs b/crates/app/src/routes/api/v1/mod.rs
index 52c20df..557d940 100644
--- a/crates/app/src/routes/api/v1/mod.rs
+++ b/crates/app/src/routes/api/v1/mod.rs
@@ -100,6 +100,10 @@ pub fn routes() -> Router {
"/questions/{id}",
delete(communities::questions::delete_request),
)
+ .route(
+ "/questions/{id}/block_ip",
+ post(communities::questions::ip_block_request),
+ )
// auth
// global
.route("/auth/register", post(auth::register_request))
@@ -177,6 +181,7 @@ pub fn routes() -> Router {
"/auth/user/find_by_ip/{ip}",
get(auth::profile::redirect_from_ip),
)
+ .route("/auth/ip/{ip}/block", post(auth::social::ip_block_request))
// warnings
.route("/warnings/{id}", post(auth::user_warnings::create_request))
.route(
diff --git a/crates/app/src/routes/mod.rs b/crates/app/src/routes/mod.rs
index bf73c9d..120eeb5 100644
--- a/crates/app/src/routes/mod.rs
+++ b/crates/app/src/routes/mod.rs
@@ -20,6 +20,10 @@ pub fn routes(config: &Config) -> Router {
get_service(tower_http::services::ServeDir::new(&config.dirs.assets)),
)
.route("/public/favicon.svg", get(assets::favicon_request))
+ .route_service(
+ "/robots.txt",
+ tower_http::services::ServeFile::new(format!("{}/robots.txt", config.dirs.assets)),
+ )
// api
.nest("/api/v1", api::v1::routes())
// pages
diff --git a/crates/core/Cargo.toml b/crates/core/Cargo.toml
index 976a5f0..aa1766d 100644
--- a/crates/core/Cargo.toml
+++ b/crates/core/Cargo.toml
@@ -1,6 +1,6 @@
[package]
name = "tetratto-core"
-version = "1.0.5"
+version = "1.0.6"
edition = "2024"
[features]
@@ -16,11 +16,11 @@ toml = "0.8.20"
tetratto-shared = { path = "../shared" }
tetratto-l10n = { path = "../l10n" }
serde_json = "1.0.140"
-totp-rs = { version = "5.6.0", features = ["qr", "gen_secret"] }
+totp-rs = { version = "5.7.0", features = ["qr", "gen_secret"] }
-redis = { version = "0.29.2", optional = true }
+redis = { version = "0.29.5", optional = true }
-rusqlite = { version = "0.34.0", optional = true }
+rusqlite = { version = "0.35.0", optional = true }
tokio-postgres = { version = "0.7.13", optional = true }
bb8-postgres = { version = "0.9.0", optional = true }
diff --git a/crates/core/src/config.rs b/crates/core/src/config.rs
index 6eff384..1fe4ea6 100644
--- a/crates/core/src/config.rs
+++ b/crates/core/src/config.rs
@@ -257,6 +257,7 @@ fn default_banned_usernames() -> Vec {
"notification".to_string(),
"post".to_string(),
"void".to_string(),
+ "anonymous".to_string(),
]
}
diff --git a/crates/core/src/database/auth.rs b/crates/core/src/database/auth.rs
index af5c98a..188974a 100644
--- a/crates/core/src/database/auth.rs
+++ b/crates/core/src/database/auth.rs
@@ -9,7 +9,6 @@ use crate::model::{
use crate::{auto_method, execute, get, query_row, params};
use pathbufd::PathBufD;
use std::fs::{exists, remove_file};
-use std::usize;
use tetratto_shared::hash::{hash_salted, salt};
use tetratto_shared::unix_epoch_timestamp;
@@ -259,6 +258,16 @@ impl DataManager {
return Err(Error::DatabaseError(e.to_string()));
}
+ let res = execute!(
+ &conn,
+ "DELETE FROM ipblocks WHERE initiator = $1",
+ &[&(id as i64)]
+ );
+
+ if let Err(e) = res {
+ return Err(Error::DatabaseError(e.to_string()));
+ }
+
// delete reactions
// reactions counts will remain the same :)
let res = execute!(
diff --git a/crates/core/src/database/common.rs b/crates/core/src/database/common.rs
index 15bbaad..6926f2b 100644
--- a/crates/core/src/database/common.rs
+++ b/crates/core/src/database/common.rs
@@ -27,6 +27,7 @@ impl DataManager {
execute!(&conn, common::CREATE_TABLE_USER_WARNINGS).unwrap();
execute!(&conn, common::CREATE_TABLE_REQUESTS).unwrap();
execute!(&conn, common::CREATE_TABLE_QUESTIONS).unwrap();
+ execute!(&conn, common::CREATE_TABLE_IPBLOCKS).unwrap();
Ok(())
}
@@ -338,7 +339,7 @@ macro_rules! auto_method {
if !user.permissions.check(FinePermission::$permission) {
return Err(Error::NotAllowed);
} else {
- self.create_audit_log_entry(crate::model::moderation::AuditLogEntry::new(
+ self.create_audit_log_entry($crate::model::moderation::AuditLogEntry::new(
user.id,
format!("invoked `{}` with x value `{x:?}`", stringify!($name)),
))
@@ -493,7 +494,7 @@ macro_rules! auto_method {
if !user.permissions.check(FinePermission::$permission) {
return Err(Error::NotAllowed);
} else {
- self.create_audit_log_entry(crate::model::moderation::AuditLogEntry::new(
+ self.create_audit_log_entry($crate::model::moderation::AuditLogEntry::new(
user.id,
format!("invoked `{}` with x value `{id}`", stringify!($name)),
))
diff --git a/crates/core/src/database/drivers/common.rs b/crates/core/src/database/drivers/common.rs
index 1dc424a..cb16682 100644
--- a/crates/core/src/database/drivers/common.rs
+++ b/crates/core/src/database/drivers/common.rs
@@ -12,3 +12,4 @@ pub const CREATE_TABLE_REPORTS: &str = include_str!("./sql/create_reports.sql");
pub const CREATE_TABLE_USER_WARNINGS: &str = include_str!("./sql/create_user_warnings.sql");
pub const CREATE_TABLE_REQUESTS: &str = include_str!("./sql/create_requests.sql");
pub const CREATE_TABLE_QUESTIONS: &str = include_str!("./sql/create_questions.sql");
+pub const CREATE_TABLE_IPBLOCKS: &str = include_str!("./sql/create_ipblocks.sql");
diff --git a/crates/core/src/database/drivers/sql/create_ipblocks.sql b/crates/core/src/database/drivers/sql/create_ipblocks.sql
new file mode 100644
index 0000000..8381d7d
--- /dev/null
+++ b/crates/core/src/database/drivers/sql/create_ipblocks.sql
@@ -0,0 +1,6 @@
+CREATE TABLE IF NOT EXISTS ipblocks (
+ id BIGINT NOT NULL PRIMARY KEY,
+ created BIGINT NOT NULL,
+ initiator BIGINT NOT NULL,
+ receiver TEXT NOT NULL
+)
diff --git a/crates/core/src/database/drivers/sql/create_questions.sql b/crates/core/src/database/drivers/sql/create_questions.sql
index d7dba2c..ab23661 100644
--- a/crates/core/src/database/drivers/sql/create_questions.sql
+++ b/crates/core/src/database/drivers/sql/create_questions.sql
@@ -9,5 +9,8 @@ CREATE TABLE IF NOT EXISTS questions (
community BIGINT NOT NULL,
-- likes
likes INT NOT NULL,
- dislikes INT NOT NULL
+ dislikes INT NOT NULL,
+ -- ...
+ context TEXT NOT NULL,
+ ip TEXT NOT NULL
)
diff --git a/crates/core/src/database/ipblocks.rs b/crates/core/src/database/ipblocks.rs
new file mode 100644
index 0000000..ffb14a0
--- /dev/null
+++ b/crates/core/src/database/ipblocks.rs
@@ -0,0 +1,133 @@
+use super::*;
+use crate::cache::Cache;
+use crate::model::{Error, Result, auth::User, auth::IpBlock, permissions::FinePermission};
+use crate::{auto_method, execute, get, query_row, params};
+
+#[cfg(feature = "sqlite")]
+use rusqlite::Row;
+
+#[cfg(feature = "postgres")]
+use tokio_postgres::Row;
+
+impl DataManager {
+ /// Get a [`UserBlock`] from an SQL row.
+ pub(crate) fn get_ipblock_from_row(
+ #[cfg(feature = "sqlite")] x: &Row<'_>,
+ #[cfg(feature = "postgres")] x: &Row,
+ ) -> IpBlock {
+ IpBlock {
+ id: get!(x->0(i64)) as usize,
+ created: get!(x->1(i64)) as usize,
+ initiator: get!(x->2(i64)) as usize,
+ receiver: get!(x->3(String)),
+ }
+ }
+
+ auto_method!(get_ipblock_by_id()@get_ipblock_from_row -> "SELECT * FROM ipblocks WHERE id = $1" --name="ip block" --returns=IpBlock --cache-key-tmpl="atto.ipblock:{}");
+
+ /// Get a user block by `initiator` and `receiver` (in that order).
+ pub async fn get_ipblock_by_initiator_receiver(
+ &self,
+ initiator: usize,
+ receiver: &str,
+ ) -> Result {
+ let conn = match self.connect().await {
+ Ok(c) => c,
+ Err(e) => return Err(Error::DatabaseConnection(e.to_string())),
+ };
+
+ let res = query_row!(
+ &conn,
+ "SELECT * FROM ipblocks WHERE initiator = $1 AND receiver = $2",
+ params![&(initiator as i64), &receiver],
+ |x| { Ok(Self::get_ipblock_from_row(x)) }
+ );
+
+ if res.is_err() {
+ return Err(Error::GeneralNotFound("user block".to_string()));
+ }
+
+ Ok(res.unwrap())
+ }
+
+ /// Get a user block by `receiver` and `initiator` (in that order).
+ pub async fn get_ipblock_by_receiver_initiator(
+ &self,
+ receiver: &str,
+ initiator: usize,
+ ) -> Result {
+ let conn = match self.connect().await {
+ Ok(c) => c,
+ Err(e) => return Err(Error::DatabaseConnection(e.to_string())),
+ };
+
+ let res = query_row!(
+ &conn,
+ "SELECT * FROM ipblocks WHERE receiver = $1 AND initiator = $2",
+ params![&receiver, &(initiator as i64)],
+ |x| { Ok(Self::get_ipblock_from_row(x)) }
+ );
+
+ if res.is_err() {
+ return Err(Error::GeneralNotFound("user block".to_string()));
+ }
+
+ Ok(res.unwrap())
+ }
+
+ /// Create a new user block in the database.
+ ///
+ /// # Arguments
+ /// * `data` - a mock [`UserBlock`] object to insert
+ pub async fn create_ipblock(&self, data: IpBlock) -> Result<()> {
+ let conn = match self.connect().await {
+ Ok(c) => c,
+ Err(e) => return Err(Error::DatabaseConnection(e.to_string())),
+ };
+
+ let res = execute!(
+ &conn,
+ "INSERT INTO ipblocks VALUES ($1, $2, $3, $4)",
+ params![
+ &(data.id as i64),
+ &(data.created as i64),
+ &(data.initiator as i64),
+ &data.receiver
+ ]
+ );
+
+ if let Err(e) = res {
+ return Err(Error::DatabaseError(e.to_string()));
+ }
+
+ // return
+ Ok(())
+ }
+
+ pub async fn delete_ipblock(&self, id: usize, user: User) -> Result<()> {
+ let block = self.get_ipblock_by_id(id).await?;
+
+ if user.id != block.initiator {
+ // only the initiator (or moderators) can delete user blocks!
+ if !user.permissions.check(FinePermission::MANAGE_FOLLOWS) {
+ return Err(Error::NotAllowed);
+ }
+ }
+
+ let conn = match self.connect().await {
+ Ok(c) => c,
+ Err(e) => return Err(Error::DatabaseConnection(e.to_string())),
+ };
+
+ let res = execute!(&conn, "DELETE FROM ipblocks WHERE id = $1", &[&(id as i64)]);
+
+ if let Err(e) = res {
+ return Err(Error::DatabaseError(e.to_string()));
+ }
+
+ self.2.remove(format!("atto.ipblock:{}", id)).await;
+
+ // return
+ Ok(())
+ }
+}
diff --git a/crates/core/src/database/mod.rs b/crates/core/src/database/mod.rs
index e602b5a..b43d53d 100644
--- a/crates/core/src/database/mod.rs
+++ b/crates/core/src/database/mod.rs
@@ -4,6 +4,7 @@ mod common;
mod communities;
mod drivers;
mod ipbans;
+mod ipblocks;
mod memberships;
mod notifications;
mod posts;
diff --git a/crates/core/src/database/posts.rs b/crates/core/src/database/posts.rs
index 3fc5e6a..856a12b 100644
--- a/crates/core/src/database/posts.rs
+++ b/crates/core/src/database/posts.rs
@@ -105,7 +105,12 @@ impl DataManager {
pub async fn get_post_question(&self, post: &Post) -> Result