add: anonymous questions
This commit is contained in:
parent
2266afde01
commit
3db7f2699c
34 changed files with 473 additions and 98 deletions
49
Cargo.lock
generated
49
Cargo.lock
generated
|
@ -310,9 +310,9 @@ dependencies = [
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "base32"
|
name = "base32"
|
||||||
version = "0.4.0"
|
version = "0.5.1"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "23ce669cd6c8588f79e15cf450314f9638f967fc5770ff1c7c1deb0925ea7cfa"
|
checksum = "022dfe9eb35f19ebbcb51e0b40a5ab759f46ad60cadf7297e0bd085afb50e076"
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "base64"
|
name = "base64"
|
||||||
|
@ -623,9 +623,9 @@ dependencies = [
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "comrak"
|
name = "comrak"
|
||||||
version = "0.37.0"
|
version = "0.38.0"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "2a4f05e73ca9a30af27bebc13600f91fd1651b2ec7d139ca82a89df7ca583af1"
|
checksum = "f690706b5db081dccea6206d7f6d594bb9895599abea9d1a0539f13888781ae8"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"bon",
|
"bon",
|
||||||
"caseless",
|
"caseless",
|
||||||
|
@ -642,9 +642,9 @@ dependencies = [
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "constant_time_eq"
|
name = "constant_time_eq"
|
||||||
version = "0.2.6"
|
version = "0.3.1"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "21a53c0a4d288377e7415b53dcfc3c04da5cdc2cc95c8d5ac178b58f0b861ad6"
|
checksum = "7c74b8349d32d297c9134b8c88677813a227df8f779daa29bfc29c183fe3dca6"
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "cookie"
|
name = "cookie"
|
||||||
|
@ -1649,9 +1649,9 @@ checksum = "8355be11b20d696c8f18f6cc018c4e372165b1fa8126cef092399c9951984ffa"
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "libsqlite3-sys"
|
name = "libsqlite3-sys"
|
||||||
version = "0.32.0"
|
version = "0.33.0"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "fbb8270bb4060bd76c6e96f20c52d80620f1d82a3470885694e41e0f81ef6fe7"
|
checksum = "947e6816f7825b2b45027c2c32e7085da9934defa535de4a6a46b10a4d5257fa"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"pkg-config",
|
"pkg-config",
|
||||||
"vcpkg",
|
"vcpkg",
|
||||||
|
@ -2212,7 +2212,7 @@ dependencies = [
|
||||||
"hmac",
|
"hmac",
|
||||||
"md-5",
|
"md-5",
|
||||||
"memchr",
|
"memchr",
|
||||||
"rand 0.9.0",
|
"rand 0.9.1",
|
||||||
"sha2",
|
"sha2",
|
||||||
"stringprep",
|
"stringprep",
|
||||||
]
|
]
|
||||||
|
@ -2356,13 +2356,12 @@ dependencies = [
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "rand"
|
name = "rand"
|
||||||
version = "0.9.0"
|
version = "0.9.1"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "3779b94aeb87e8bd4e834cee3650289ee9e0d5677f976ecdb6d219e5f4f6cd94"
|
checksum = "9fbfd9d094a40bf3ae768db9361049ace4c0e04a4fd6b359518bd7b73a73dd97"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"rand_chacha 0.9.0",
|
"rand_chacha 0.9.0",
|
||||||
"rand_core 0.9.3",
|
"rand_core 0.9.3",
|
||||||
"zerocopy",
|
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
|
@ -2475,9 +2474,9 @@ dependencies = [
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "redis"
|
name = "redis"
|
||||||
version = "0.29.2"
|
version = "0.29.5"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "b110459d6e323b7cda23980c46c77157601199c9da6241552b284cd565a7a133"
|
checksum = "1bc42f3a12fd4408ce64d8efef67048a924e543bd35c6591c0447fda9054695f"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"arc-swap",
|
"arc-swap",
|
||||||
"combine",
|
"combine",
|
||||||
|
@ -2611,9 +2610,9 @@ dependencies = [
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "rusqlite"
|
name = "rusqlite"
|
||||||
version = "0.34.0"
|
version = "0.35.0"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "37e34486da88d8e051c7c0e23c3f15fd806ea8546260aa2fec247e97242ec143"
|
checksum = "a22715a5d6deef63c637207afbe68d0c72c3f8d0022d7cf9714c442d6157606b"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"bitflags 2.9.0",
|
"bitflags 2.9.0",
|
||||||
"fallible-iterator 0.3.0",
|
"fallible-iterator 0.3.0",
|
||||||
|
@ -3155,7 +3154,7 @@ dependencies = [
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "tetratto"
|
name = "tetratto"
|
||||||
version = "1.0.5"
|
version = "1.0.6"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"ammonia",
|
"ammonia",
|
||||||
"axum",
|
"axum",
|
||||||
|
@ -3180,7 +3179,7 @@ dependencies = [
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "tetratto-core"
|
name = "tetratto-core"
|
||||||
version = "1.0.5"
|
version = "1.0.6"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"async-recursion",
|
"async-recursion",
|
||||||
"bb8-postgres",
|
"bb8-postgres",
|
||||||
|
@ -3199,7 +3198,7 @@ dependencies = [
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "tetratto-l10n"
|
name = "tetratto-l10n"
|
||||||
version = "1.0.5"
|
version = "1.0.6"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"pathbufd",
|
"pathbufd",
|
||||||
"serde",
|
"serde",
|
||||||
|
@ -3208,14 +3207,14 @@ dependencies = [
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "tetratto-shared"
|
name = "tetratto-shared"
|
||||||
version = "1.0.5"
|
version = "1.0.6"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"ammonia",
|
"ammonia",
|
||||||
"chrono",
|
"chrono",
|
||||||
"comrak",
|
"comrak",
|
||||||
"hex_fmt",
|
"hex_fmt",
|
||||||
"num-bigint",
|
"num-bigint",
|
||||||
"rand 0.9.0",
|
"rand 0.9.1",
|
||||||
"serde",
|
"serde",
|
||||||
"sha2",
|
"sha2",
|
||||||
"uuid",
|
"uuid",
|
||||||
|
@ -3395,7 +3394,7 @@ dependencies = [
|
||||||
"pin-project-lite",
|
"pin-project-lite",
|
||||||
"postgres-protocol",
|
"postgres-protocol",
|
||||||
"postgres-types",
|
"postgres-types",
|
||||||
"rand 0.9.0",
|
"rand 0.9.1",
|
||||||
"socket2",
|
"socket2",
|
||||||
"tokio",
|
"tokio",
|
||||||
"tokio-util",
|
"tokio-util",
|
||||||
|
@ -3461,15 +3460,15 @@ dependencies = [
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "totp-rs"
|
name = "totp-rs"
|
||||||
version = "5.6.0"
|
version = "5.7.0"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "17b2f27dad992486c26b4e7455f38aa487e838d6d61b57e72906ee2b8c287a90"
|
checksum = "f124352108f58ef88299e909f6e9470f1cdc8d2a1397963901b4a6366206bf72"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"base32",
|
"base32",
|
||||||
"constant_time_eq",
|
"constant_time_eq",
|
||||||
"hmac",
|
"hmac",
|
||||||
"qrcodegen-image",
|
"qrcodegen-image",
|
||||||
"rand 0.8.5",
|
"rand 0.9.1",
|
||||||
"sha1",
|
"sha1",
|
||||||
"sha2",
|
"sha2",
|
||||||
"url",
|
"url",
|
||||||
|
|
|
@ -1,6 +1,6 @@
|
||||||
[package]
|
[package]
|
||||||
name = "tetratto"
|
name = "tetratto"
|
||||||
version = "1.0.5"
|
version = "1.0.6"
|
||||||
edition = "2024"
|
edition = "2024"
|
||||||
|
|
||||||
[features]
|
[features]
|
||||||
|
|
|
@ -41,10 +41,11 @@ version = "1.0.0"
|
||||||
"auth:action.login" = "Login"
|
"auth:action.login" = "Login"
|
||||||
"auth:action.register" = "Register"
|
"auth:action.register" = "Register"
|
||||||
"auth:action.logout" = "Logout"
|
"auth:action.logout" = "Logout"
|
||||||
"auto:action.follow" = "Follow"
|
"auth:action.follow" = "Follow"
|
||||||
"auto:action.unfollow" = "Unfollow"
|
"auth:action.unfollow" = "Unfollow"
|
||||||
"auto:action.block" = "Block"
|
"auth:action.block" = "Block"
|
||||||
"auto:action.unblock" = "Unblock"
|
"auth:action.ip_block" = "IP block"
|
||||||
|
"auth:action.unblock" = "Unblock"
|
||||||
"auth:link.my_profile" = "My profile"
|
"auth:link.my_profile" = "My profile"
|
||||||
"auth:link.settings" = "Settings"
|
"auth:link.settings" = "Settings"
|
||||||
"auth:label.followers" = "Followers"
|
"auth:label.followers" = "Followers"
|
||||||
|
@ -55,8 +56,8 @@ version = "1.0.0"
|
||||||
"auth:label.before_you_view" = "Before you view"
|
"auth:label.before_you_view" = "Before you view"
|
||||||
"auth:label.private_profile" = "Private profile"
|
"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."
|
"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"
|
"auth:action.request_to_follow" = "Request to follow"
|
||||||
"auto:action.cancel_follow_request" = "Cancel follow request"
|
"auth:action.cancel_follow_request" = "Cancel follow request"
|
||||||
|
|
||||||
"communities:action.create" = "Create"
|
"communities:action.create" = "Create"
|
||||||
"communities:action.select" = "Select"
|
"communities:action.select" = "Select"
|
||||||
|
|
|
@ -110,7 +110,8 @@
|
||||||
|
|
||||||
<div class="flex gap-2">
|
<div class="flex gap-2">
|
||||||
<button class="primary">{{ text "requests:label.answer" }}</button>
|
<button class="primary">{{ text "requests:label.answer" }}</button>
|
||||||
<button class="red quaternary" onclick="trigger('me::remove_question', ['{{ question[0].id }}'])">{{ text "general:action.delete" }}</button>
|
<button type="button" class="red quaternary" onclick="trigger('me::remove_question', ['{{ question[0].id }}'])">{{ text "general:action.delete" }}</button>
|
||||||
|
<button type="button" class="red quaternary" onclick="trigger('me::ip_block_question', ['{{ question[0].id }}'])">{{ text "auth:action.ip_block" }}</button>
|
||||||
</div>
|
</div>
|
||||||
</form>
|
</form>
|
||||||
</div>
|
</div>
|
||||||
|
|
|
@ -156,7 +156,7 @@
|
||||||
atto_tag="user.follow"
|
atto_tag="user.follow"
|
||||||
>
|
>
|
||||||
{{ icon "user-plus" }}
|
{{ icon "user-plus" }}
|
||||||
<span>{{ text "auto:action.follow" }}</span>
|
<span>{{ text "auth:action.follow" }}</span>
|
||||||
</button>
|
</button>
|
||||||
|
|
||||||
<button
|
<button
|
||||||
|
@ -165,7 +165,7 @@
|
||||||
atto_tag="user.unfollow"
|
atto_tag="user.unfollow"
|
||||||
>
|
>
|
||||||
{{ icon "user-minus" }}
|
{{ icon "user-minus" }}
|
||||||
<span>{{ text "auto:action.unfollow" }}</span>
|
<span>{{ text "auth:action.unfollow" }}</span>
|
||||||
</button>
|
</button>
|
||||||
|
|
||||||
<button
|
<button
|
||||||
|
@ -173,7 +173,7 @@
|
||||||
class="quaternary red"
|
class="quaternary red"
|
||||||
>
|
>
|
||||||
{{ icon "shield" }}
|
{{ icon "shield" }}
|
||||||
<span>{{ text "auto:action.block" }}</span>
|
<span>{{ text "auth:action.block" }}</span>
|
||||||
</button>
|
</button>
|
||||||
{% else %}
|
{% else %}
|
||||||
<button
|
<button
|
||||||
|
@ -181,7 +181,7 @@
|
||||||
class="quaternary red"
|
class="quaternary red"
|
||||||
>
|
>
|
||||||
{{ icon "shield-off" }}
|
{{ icon "shield-off" }}
|
||||||
<span>{{ text "auto:action.unblock" }}</span>
|
<span>{{ text "auth:action.unblock" }}</span>
|
||||||
</button>
|
</button>
|
||||||
{% endif %} {% if is_helper %}
|
{% endif %} {% if is_helper %}
|
||||||
<a
|
<a
|
||||||
|
|
|
@ -1,5 +1,6 @@
|
||||||
{% extends "profile/base.html" %} {% block content %} {% if
|
{% extends "profile/base.html" %} {% block content %} {% if
|
||||||
profile.settings.enable_questions and user %}
|
profile.settings.enable_questions and (user or
|
||||||
|
profile.settings.allow_anonymous_questions) %}
|
||||||
<div style="display: contents">
|
<div style="display: contents">
|
||||||
{{ components::create_question_form(receiver=profile.id,
|
{{ components::create_question_form(receiver=profile.id,
|
||||||
header=profile.settings.motivational_header) }}
|
header=profile.settings.motivational_header) }}
|
||||||
|
|
|
@ -25,7 +25,7 @@
|
||||||
atto_tag="user.follow_request"
|
atto_tag="user.follow_request"
|
||||||
>
|
>
|
||||||
{{ icon "user-plus" }}
|
{{ icon "user-plus" }}
|
||||||
<span>{{ text "auto:action.request_to_follow" }}</span>
|
<span>{{ text "auth:action.request_to_follow" }}</span>
|
||||||
</button>
|
</button>
|
||||||
|
|
||||||
<button
|
<button
|
||||||
|
@ -34,7 +34,7 @@
|
||||||
atto_tag="user.cancel_request"
|
atto_tag="user.cancel_request"
|
||||||
>
|
>
|
||||||
{{ icon "user-minus" }}
|
{{ icon "user-minus" }}
|
||||||
<span>{{ text "auto:action.cancel_follow_request" }}</span>
|
<span>{{ text "auth:action.cancel_follow_request" }}</span>
|
||||||
</button>
|
</button>
|
||||||
{% else %}
|
{% else %}
|
||||||
<button
|
<button
|
||||||
|
@ -43,7 +43,7 @@
|
||||||
atto_tag="user.unfollow"
|
atto_tag="user.unfollow"
|
||||||
>
|
>
|
||||||
{{ icon "user-minus" }}
|
{{ icon "user-minus" }}
|
||||||
<span>{{ text "auto:action.unfollow" }}</span>
|
<span>{{ text "auth:action.unfollow" }}</span>
|
||||||
</button>
|
</button>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
|
|
||||||
|
|
|
@ -748,20 +748,6 @@
|
||||||
settings.warning,
|
settings.warning,
|
||||||
"textarea",
|
"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,
|
settings,
|
||||||
);
|
);
|
||||||
|
@ -791,6 +777,28 @@
|
||||||
"{{ profile.settings.private_last_seen }}",
|
"{{ profile.settings.private_last_seen }}",
|
||||||
"checkbox",
|
"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,
|
settings,
|
||||||
);
|
);
|
||||||
|
|
|
@ -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
|
// token switcher
|
||||||
self.define(
|
self.define(
|
||||||
"set_login_account_tokens",
|
"set_login_account_tokens",
|
||||||
|
|
|
@ -4,7 +4,7 @@ use crate::{
|
||||||
};
|
};
|
||||||
use axum::{Extension, Json, extract::Path, response::IntoResponse};
|
use axum::{Extension, Json, extract::Path, response::IntoResponse};
|
||||||
use axum_extra::extract::CookieJar;
|
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.
|
/// Toggle following on the given user.
|
||||||
pub async fn follow_request(
|
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<String>,
|
||||||
|
Extension(data): Extension<State>,
|
||||||
|
) -> 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()),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
|
@ -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 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};
|
use crate::{get_user_from_token, routes::api::v1::CreateQuestion, State};
|
||||||
|
|
||||||
pub async fn create_request(
|
pub async fn create_request(
|
||||||
jar: CookieJar,
|
jar: CookieJar,
|
||||||
|
headers: HeaderMap,
|
||||||
Extension(data): Extension<State>,
|
Extension(data): Extension<State>,
|
||||||
Json(req): Json<CreateQuestion>,
|
Json(req): Json<CreateQuestion>,
|
||||||
) -> impl IntoResponse {
|
) -> impl IntoResponse {
|
||||||
let data = &(data.read().await).0;
|
let data = &(data.read().await).0;
|
||||||
let user = match get_user_from_token!(jar, data) {
|
let user = get_user_from_token!(jar, data);
|
||||||
Some(ua) => ua,
|
|
||||||
None => return Json(Error::NotAllowed.into()),
|
|
||||||
};
|
|
||||||
|
|
||||||
|
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(
|
let mut props = Question::new(
|
||||||
user.id,
|
if let Some(ref ua) = user { ua.id } else { 0 },
|
||||||
match req.receiver.parse::<usize>() {
|
match req.receiver.parse::<usize>() {
|
||||||
Ok(x) => x,
|
Ok(x) => x,
|
||||||
Err(e) => return Json(Error::MiscError(e.to_string()).into()),
|
Err(e) => return Json(Error::MiscError(e.to_string()).into()),
|
||||||
},
|
},
|
||||||
req.content,
|
req.content,
|
||||||
req.is_global,
|
req.is_global,
|
||||||
|
real_ip,
|
||||||
);
|
);
|
||||||
|
|
||||||
if !req.community.is_empty() {
|
if !req.community.is_empty() {
|
||||||
|
@ -63,3 +85,43 @@ pub async fn delete_request(
|
||||||
Err(e) => Json(e.into()),
|
Err(e) => Json(e.into()),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pub async fn ip_block_request(
|
||||||
|
jar: CookieJar,
|
||||||
|
Extension(data): Extension<State>,
|
||||||
|
Path(id): Path<usize>,
|
||||||
|
) -> 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()),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
|
@ -100,6 +100,10 @@ pub fn routes() -> Router {
|
||||||
"/questions/{id}",
|
"/questions/{id}",
|
||||||
delete(communities::questions::delete_request),
|
delete(communities::questions::delete_request),
|
||||||
)
|
)
|
||||||
|
.route(
|
||||||
|
"/questions/{id}/block_ip",
|
||||||
|
post(communities::questions::ip_block_request),
|
||||||
|
)
|
||||||
// auth
|
// auth
|
||||||
// global
|
// global
|
||||||
.route("/auth/register", post(auth::register_request))
|
.route("/auth/register", post(auth::register_request))
|
||||||
|
@ -177,6 +181,7 @@ pub fn routes() -> Router {
|
||||||
"/auth/user/find_by_ip/{ip}",
|
"/auth/user/find_by_ip/{ip}",
|
||||||
get(auth::profile::redirect_from_ip),
|
get(auth::profile::redirect_from_ip),
|
||||||
)
|
)
|
||||||
|
.route("/auth/ip/{ip}/block", post(auth::social::ip_block_request))
|
||||||
// warnings
|
// warnings
|
||||||
.route("/warnings/{id}", post(auth::user_warnings::create_request))
|
.route("/warnings/{id}", post(auth::user_warnings::create_request))
|
||||||
.route(
|
.route(
|
||||||
|
|
|
@ -20,6 +20,10 @@ pub fn routes(config: &Config) -> Router {
|
||||||
get_service(tower_http::services::ServeDir::new(&config.dirs.assets)),
|
get_service(tower_http::services::ServeDir::new(&config.dirs.assets)),
|
||||||
)
|
)
|
||||||
.route("/public/favicon.svg", get(assets::favicon_request))
|
.route("/public/favicon.svg", get(assets::favicon_request))
|
||||||
|
.route_service(
|
||||||
|
"/robots.txt",
|
||||||
|
tower_http::services::ServeFile::new(format!("{}/robots.txt", config.dirs.assets)),
|
||||||
|
)
|
||||||
// api
|
// api
|
||||||
.nest("/api/v1", api::v1::routes())
|
.nest("/api/v1", api::v1::routes())
|
||||||
// pages
|
// pages
|
||||||
|
|
|
@ -1,6 +1,6 @@
|
||||||
[package]
|
[package]
|
||||||
name = "tetratto-core"
|
name = "tetratto-core"
|
||||||
version = "1.0.5"
|
version = "1.0.6"
|
||||||
edition = "2024"
|
edition = "2024"
|
||||||
|
|
||||||
[features]
|
[features]
|
||||||
|
@ -16,11 +16,11 @@ toml = "0.8.20"
|
||||||
tetratto-shared = { path = "../shared" }
|
tetratto-shared = { path = "../shared" }
|
||||||
tetratto-l10n = { path = "../l10n" }
|
tetratto-l10n = { path = "../l10n" }
|
||||||
serde_json = "1.0.140"
|
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 }
|
tokio-postgres = { version = "0.7.13", optional = true }
|
||||||
bb8-postgres = { version = "0.9.0", optional = true }
|
bb8-postgres = { version = "0.9.0", optional = true }
|
||||||
|
|
|
@ -257,6 +257,7 @@ fn default_banned_usernames() -> Vec<String> {
|
||||||
"notification".to_string(),
|
"notification".to_string(),
|
||||||
"post".to_string(),
|
"post".to_string(),
|
||||||
"void".to_string(),
|
"void".to_string(),
|
||||||
|
"anonymous".to_string(),
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -9,7 +9,6 @@ use crate::model::{
|
||||||
use crate::{auto_method, execute, get, query_row, params};
|
use crate::{auto_method, execute, get, query_row, params};
|
||||||
use pathbufd::PathBufD;
|
use pathbufd::PathBufD;
|
||||||
use std::fs::{exists, remove_file};
|
use std::fs::{exists, remove_file};
|
||||||
use std::usize;
|
|
||||||
use tetratto_shared::hash::{hash_salted, salt};
|
use tetratto_shared::hash::{hash_salted, salt};
|
||||||
use tetratto_shared::unix_epoch_timestamp;
|
use tetratto_shared::unix_epoch_timestamp;
|
||||||
|
|
||||||
|
@ -259,6 +258,16 @@ impl DataManager {
|
||||||
return Err(Error::DatabaseError(e.to_string()));
|
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
|
// delete reactions
|
||||||
// reactions counts will remain the same :)
|
// reactions counts will remain the same :)
|
||||||
let res = execute!(
|
let res = execute!(
|
||||||
|
|
|
@ -27,6 +27,7 @@ impl DataManager {
|
||||||
execute!(&conn, common::CREATE_TABLE_USER_WARNINGS).unwrap();
|
execute!(&conn, common::CREATE_TABLE_USER_WARNINGS).unwrap();
|
||||||
execute!(&conn, common::CREATE_TABLE_REQUESTS).unwrap();
|
execute!(&conn, common::CREATE_TABLE_REQUESTS).unwrap();
|
||||||
execute!(&conn, common::CREATE_TABLE_QUESTIONS).unwrap();
|
execute!(&conn, common::CREATE_TABLE_QUESTIONS).unwrap();
|
||||||
|
execute!(&conn, common::CREATE_TABLE_IPBLOCKS).unwrap();
|
||||||
|
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
@ -338,7 +339,7 @@ macro_rules! auto_method {
|
||||||
if !user.permissions.check(FinePermission::$permission) {
|
if !user.permissions.check(FinePermission::$permission) {
|
||||||
return Err(Error::NotAllowed);
|
return Err(Error::NotAllowed);
|
||||||
} else {
|
} else {
|
||||||
self.create_audit_log_entry(crate::model::moderation::AuditLogEntry::new(
|
self.create_audit_log_entry($crate::model::moderation::AuditLogEntry::new(
|
||||||
user.id,
|
user.id,
|
||||||
format!("invoked `{}` with x value `{x:?}`", stringify!($name)),
|
format!("invoked `{}` with x value `{x:?}`", stringify!($name)),
|
||||||
))
|
))
|
||||||
|
@ -493,7 +494,7 @@ macro_rules! auto_method {
|
||||||
if !user.permissions.check(FinePermission::$permission) {
|
if !user.permissions.check(FinePermission::$permission) {
|
||||||
return Err(Error::NotAllowed);
|
return Err(Error::NotAllowed);
|
||||||
} else {
|
} else {
|
||||||
self.create_audit_log_entry(crate::model::moderation::AuditLogEntry::new(
|
self.create_audit_log_entry($crate::model::moderation::AuditLogEntry::new(
|
||||||
user.id,
|
user.id,
|
||||||
format!("invoked `{}` with x value `{id}`", stringify!($name)),
|
format!("invoked `{}` with x value `{id}`", stringify!($name)),
|
||||||
))
|
))
|
||||||
|
|
|
@ -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_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_REQUESTS: &str = include_str!("./sql/create_requests.sql");
|
||||||
pub const CREATE_TABLE_QUESTIONS: &str = include_str!("./sql/create_questions.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");
|
||||||
|
|
6
crates/core/src/database/drivers/sql/create_ipblocks.sql
Normal file
6
crates/core/src/database/drivers/sql/create_ipblocks.sql
Normal file
|
@ -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
|
||||||
|
)
|
|
@ -9,5 +9,8 @@ CREATE TABLE IF NOT EXISTS questions (
|
||||||
community BIGINT NOT NULL,
|
community BIGINT NOT NULL,
|
||||||
-- likes
|
-- likes
|
||||||
likes INT NOT NULL,
|
likes INT NOT NULL,
|
||||||
dislikes INT NOT NULL
|
dislikes INT NOT NULL,
|
||||||
|
-- ...
|
||||||
|
context TEXT NOT NULL,
|
||||||
|
ip TEXT NOT NULL
|
||||||
)
|
)
|
||||||
|
|
133
crates/core/src/database/ipblocks.rs
Normal file
133
crates/core/src/database/ipblocks.rs
Normal file
|
@ -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<IpBlock> {
|
||||||
|
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<IpBlock> {
|
||||||
|
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(())
|
||||||
|
}
|
||||||
|
}
|
|
@ -4,6 +4,7 @@ mod common;
|
||||||
mod communities;
|
mod communities;
|
||||||
mod drivers;
|
mod drivers;
|
||||||
mod ipbans;
|
mod ipbans;
|
||||||
|
mod ipblocks;
|
||||||
mod memberships;
|
mod memberships;
|
||||||
mod notifications;
|
mod notifications;
|
||||||
mod posts;
|
mod posts;
|
||||||
|
|
|
@ -105,7 +105,12 @@ impl DataManager {
|
||||||
pub async fn get_post_question(&self, post: &Post) -> Result<Option<(Question, User)>> {
|
pub async fn get_post_question(&self, post: &Post) -> Result<Option<(Question, User)>> {
|
||||||
if post.context.answering != 0 {
|
if post.context.answering != 0 {
|
||||||
let question = self.get_question_by_id(post.context.answering).await?;
|
let question = self.get_question_by_id(post.context.answering).await?;
|
||||||
let user = self.get_user_by_id_with_void(question.owner).await?;
|
let user = if question.owner == 0 {
|
||||||
|
User::anonymous()
|
||||||
|
} else {
|
||||||
|
self.get_user_by_id_with_void(question.owner).await?
|
||||||
|
};
|
||||||
|
|
||||||
Ok(Some((question, user)))
|
Ok(Some((question, user)))
|
||||||
} else {
|
} else {
|
||||||
Ok(None)
|
Ok(None)
|
||||||
|
@ -563,7 +568,7 @@ impl DataManager {
|
||||||
.get_membership_by_owner_community(uid, community.id)
|
.get_membership_by_owner_community(uid, community.id)
|
||||||
.await
|
.await
|
||||||
{
|
{
|
||||||
Ok(m) => !(!m.role.check_member()),
|
Ok(m) => m.role.check_member(),
|
||||||
Err(_) => false,
|
Err(_) => false,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -630,7 +635,7 @@ impl DataManager {
|
||||||
|
|
||||||
// create notification for question owner
|
// create notification for question owner
|
||||||
// (if the current user isn't the owner)
|
// (if the current user isn't the owner)
|
||||||
if question.owner != data.owner {
|
if (question.owner != data.owner) && (question.owner != 0) {
|
||||||
self.create_notification(Notification::new(
|
self.create_notification(Notification::new(
|
||||||
"Your question has received a new answer!".to_string(),
|
"Your question has received a new answer!".to_string(),
|
||||||
format!(
|
format!(
|
||||||
|
@ -682,9 +687,10 @@ impl DataManager {
|
||||||
}
|
}
|
||||||
|
|
||||||
// check blocked status
|
// check blocked status
|
||||||
if let Ok(_) = self
|
if self
|
||||||
.get_userblock_by_initiator_receiver(rt.owner, data.owner)
|
.get_userblock_by_initiator_receiver(rt.owner, data.owner)
|
||||||
.await
|
.await
|
||||||
|
.is_ok()
|
||||||
{
|
{
|
||||||
return Err(Error::NotAllowed);
|
return Err(Error::NotAllowed);
|
||||||
}
|
}
|
||||||
|
@ -703,9 +709,10 @@ impl DataManager {
|
||||||
}
|
}
|
||||||
|
|
||||||
// check blocked status
|
// check blocked status
|
||||||
if let Ok(_) = self
|
if self
|
||||||
.get_userblock_by_initiator_receiver(rt.owner, data.owner)
|
.get_userblock_by_initiator_receiver(rt.owner, data.owner)
|
||||||
.await
|
.await
|
||||||
|
.is_ok()
|
||||||
{
|
{
|
||||||
return Err(Error::NotAllowed);
|
return Err(Error::NotAllowed);
|
||||||
}
|
}
|
||||||
|
|
|
@ -39,6 +39,7 @@ impl DataManager {
|
||||||
dislikes: get!(x->9(i32)) as isize,
|
dislikes: get!(x->9(i32)) as isize,
|
||||||
// ...
|
// ...
|
||||||
context: serde_json::from_str(&get!(x->10(String))).unwrap(),
|
context: serde_json::from_str(&get!(x->10(String))).unwrap(),
|
||||||
|
ip: get!(x->11(String)),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -53,7 +54,12 @@ impl DataManager {
|
||||||
if let Some(ua) = seen_users.get(&question.owner) {
|
if let Some(ua) = seen_users.get(&question.owner) {
|
||||||
out.push((question, ua.to_owned()));
|
out.push((question, ua.to_owned()));
|
||||||
} else {
|
} else {
|
||||||
let user = self.get_user_by_id_with_void(question.owner).await?;
|
let user = if question.owner == 0 {
|
||||||
|
User::anonymous()
|
||||||
|
} else {
|
||||||
|
self.get_user_by_id_with_void(question.owner).await?
|
||||||
|
};
|
||||||
|
|
||||||
seen_users.insert(question.owner, user.clone());
|
seen_users.insert(question.owner, user.clone());
|
||||||
out.push((question, user));
|
out.push((question, user));
|
||||||
}
|
}
|
||||||
|
@ -311,6 +317,15 @@ impl DataManager {
|
||||||
if !receiver.settings.enable_questions {
|
if !receiver.settings.enable_questions {
|
||||||
return Err(Error::QuestionsDisabled);
|
return Err(Error::QuestionsDisabled);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// check for ip block
|
||||||
|
if self
|
||||||
|
.get_ipblock_by_initiator_receiver(receiver.id, &data.ip)
|
||||||
|
.await
|
||||||
|
.is_ok()
|
||||||
|
{
|
||||||
|
return Err(Error::NotAllowed);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
let receiver = self.get_user_by_id(data.receiver).await?;
|
let receiver = self.get_user_by_id(data.receiver).await?;
|
||||||
|
@ -318,6 +333,19 @@ impl DataManager {
|
||||||
if !receiver.settings.enable_questions {
|
if !receiver.settings.enable_questions {
|
||||||
return Err(Error::QuestionsDisabled);
|
return Err(Error::QuestionsDisabled);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if !receiver.settings.allow_anonymous_questions && data.owner == 0 {
|
||||||
|
return Err(Error::NotAllowed);
|
||||||
|
}
|
||||||
|
|
||||||
|
// check for ip block
|
||||||
|
if self
|
||||||
|
.get_ipblock_by_initiator_receiver(receiver.id, &data.ip)
|
||||||
|
.await
|
||||||
|
.is_ok()
|
||||||
|
{
|
||||||
|
return Err(Error::NotAllowed);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// ...
|
// ...
|
||||||
|
@ -328,7 +356,7 @@ impl DataManager {
|
||||||
|
|
||||||
let res = execute!(
|
let res = execute!(
|
||||||
&conn,
|
&conn,
|
||||||
"INSERT INTO questions VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9, $10, $11)",
|
"INSERT INTO questions VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9, $10, $11, $12)",
|
||||||
params![
|
params![
|
||||||
&(data.id as i64),
|
&(data.id as i64),
|
||||||
&(data.created as i64),
|
&(data.created as i64),
|
||||||
|
@ -340,7 +368,8 @@ impl DataManager {
|
||||||
&(data.community as i64),
|
&(data.community as i64),
|
||||||
&0_i32,
|
&0_i32,
|
||||||
&0_i32,
|
&0_i32,
|
||||||
&serde_json::to_string(&data.context).unwrap()
|
&serde_json::to_string(&data.context).unwrap(),
|
||||||
|
&data.ip
|
||||||
]
|
]
|
||||||
);
|
);
|
||||||
|
|
||||||
|
|
|
@ -130,11 +130,9 @@ impl DataManager {
|
||||||
.get_request_by_id_linked_asset(id, linked_asset)
|
.get_request_by_id_linked_asset(id, linked_asset)
|
||||||
.await?;
|
.await?;
|
||||||
|
|
||||||
if !force {
|
if !force && user.id != y.owner && !user.permissions.check(FinePermission::MANAGE_REQUESTS) {
|
||||||
if user.id != y.owner && !user.permissions.check(FinePermission::MANAGE_REQUESTS) {
|
|
||||||
return Err(Error::NotAllowed);
|
return Err(Error::NotAllowed);
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
|
||||||
let conn = match self.connect().await {
|
let conn = match self.connect().await {
|
||||||
Ok(c) => c,
|
Ok(c) => c,
|
||||||
|
|
|
@ -136,6 +136,9 @@ pub struct UserSettings {
|
||||||
/// A header shown in the place of "Ask question" if `enable_questions` is true.
|
/// A header shown in the place of "Ask question" if `enable_questions` is true.
|
||||||
#[serde(default)]
|
#[serde(default)]
|
||||||
pub motivational_header: String,
|
pub motivational_header: String,
|
||||||
|
/// If questions from anonymous users are allowed. Requires `enable_questions`.
|
||||||
|
#[serde(default)]
|
||||||
|
pub allow_anonymous_questions: bool,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl Default for User {
|
impl Default for User {
|
||||||
|
@ -192,6 +195,15 @@ impl User {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Anonymous user profile.
|
||||||
|
pub fn anonymous() -> Self {
|
||||||
|
Self {
|
||||||
|
username: "anonymous".to_string(),
|
||||||
|
id: 0,
|
||||||
|
..Default::default()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
/// Create a new token
|
/// Create a new token
|
||||||
///
|
///
|
||||||
/// # Returns
|
/// # Returns
|
||||||
|
@ -356,6 +368,29 @@ impl UserBlock {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[derive(Serialize, Deserialize)]
|
||||||
|
pub struct IpBlock {
|
||||||
|
pub id: usize,
|
||||||
|
pub created: usize,
|
||||||
|
pub initiator: usize,
|
||||||
|
pub receiver: String,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl IpBlock {
|
||||||
|
/// Create a new [`IpBlock`].
|
||||||
|
pub fn new(initiator: usize, receiver: String) -> Self {
|
||||||
|
Self {
|
||||||
|
id: AlmostSnowflake::new(1234567890)
|
||||||
|
.to_string()
|
||||||
|
.parse::<usize>()
|
||||||
|
.unwrap(),
|
||||||
|
created: unix_epoch_timestamp() as usize,
|
||||||
|
initiator,
|
||||||
|
receiver,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
#[derive(Serialize, Deserialize)]
|
#[derive(Serialize, Deserialize)]
|
||||||
pub struct IpBan {
|
pub struct IpBan {
|
||||||
pub ip: String,
|
pub ip: String,
|
||||||
|
|
|
@ -307,13 +307,23 @@ pub struct Question {
|
||||||
pub likes: isize,
|
pub likes: isize,
|
||||||
#[serde(default)]
|
#[serde(default)]
|
||||||
pub dislikes: isize,
|
pub dislikes: isize,
|
||||||
|
// ...
|
||||||
#[serde(default)]
|
#[serde(default)]
|
||||||
pub context: QuestionContext,
|
pub context: QuestionContext,
|
||||||
|
/// The IP of the question creator for IP blocking and identifying anonymous users.
|
||||||
|
#[serde(default)]
|
||||||
|
pub ip: String,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl Question {
|
impl Question {
|
||||||
/// Create a new [`Question`].
|
/// Create a new [`Question`].
|
||||||
pub fn new(owner: usize, receiver: usize, content: String, is_global: bool) -> Self {
|
pub fn new(
|
||||||
|
owner: usize,
|
||||||
|
receiver: usize,
|
||||||
|
content: String,
|
||||||
|
is_global: bool,
|
||||||
|
ip: String,
|
||||||
|
) -> Self {
|
||||||
Self {
|
Self {
|
||||||
id: AlmostSnowflake::new(1234567890)
|
id: AlmostSnowflake::new(1234567890)
|
||||||
.to_string()
|
.to_string()
|
||||||
|
@ -329,18 +339,15 @@ impl Question {
|
||||||
likes: 0,
|
likes: 0,
|
||||||
dislikes: 0,
|
dislikes: 0,
|
||||||
context: QuestionContext::default(),
|
context: QuestionContext::default(),
|
||||||
|
ip,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||||
|
#[derive(Default)]
|
||||||
pub struct QuestionContext {
|
pub struct QuestionContext {
|
||||||
#[serde(default)]
|
#[serde(default)]
|
||||||
pub is_nsfw: bool,
|
pub is_nsfw: bool,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl Default for QuestionContext {
|
|
||||||
fn default() -> Self {
|
|
||||||
Self { is_nsfw: false }
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
|
@ -6,6 +6,8 @@ pub mod permissions;
|
||||||
pub mod reactions;
|
pub mod reactions;
|
||||||
pub mod requests;
|
pub mod requests;
|
||||||
|
|
||||||
|
use std::fmt::Display;
|
||||||
|
|
||||||
use serde::{Deserialize, Serialize};
|
use serde::{Deserialize, Serialize};
|
||||||
|
|
||||||
#[derive(Serialize, Deserialize)]
|
#[derive(Serialize, Deserialize)]
|
||||||
|
@ -37,9 +39,9 @@ pub enum Error {
|
||||||
Unknown,
|
Unknown,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl ToString for Error {
|
impl Display for Error {
|
||||||
fn to_string(&self) -> String {
|
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
||||||
match self {
|
f.write_str(&match self {
|
||||||
Self::MiscError(msg) => msg.to_owned(),
|
Self::MiscError(msg) => msg.to_owned(),
|
||||||
Self::DatabaseConnection(msg) => msg.to_owned(),
|
Self::DatabaseConnection(msg) => msg.to_owned(),
|
||||||
Self::DatabaseError(msg) => format!("Database error: {msg}"),
|
Self::DatabaseError(msg) => format!("Database error: {msg}"),
|
||||||
|
@ -55,7 +57,7 @@ impl ToString for Error {
|
||||||
Self::TitleInUse => "Title in use".to_string(),
|
Self::TitleInUse => "Title in use".to_string(),
|
||||||
Self::QuestionsDisabled => "You are not allowed to ask questions there".to_string(),
|
Self::QuestionsDisabled => "You are not allowed to ask questions there".to_string(),
|
||||||
_ => format!("An unknown error as occurred: ({:?})", self),
|
_ => format!("An unknown error as occurred: ({:?})", self),
|
||||||
}
|
})
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -1,6 +1,6 @@
|
||||||
[package]
|
[package]
|
||||||
name = "tetratto-l10n"
|
name = "tetratto-l10n"
|
||||||
version = "1.0.5"
|
version = "1.0.6"
|
||||||
edition = "2024"
|
edition = "2024"
|
||||||
authors.workspace = true
|
authors.workspace = true
|
||||||
repository.workspace = true
|
repository.workspace = true
|
||||||
|
|
|
@ -1,6 +1,6 @@
|
||||||
[package]
|
[package]
|
||||||
name = "tetratto-shared"
|
name = "tetratto-shared"
|
||||||
version = "1.0.5"
|
version = "1.0.6"
|
||||||
edition = "2024"
|
edition = "2024"
|
||||||
authors.workspace = true
|
authors.workspace = true
|
||||||
repository.workspace = true
|
repository.workspace = true
|
||||||
|
@ -9,10 +9,10 @@ license.workspace = true
|
||||||
[dependencies]
|
[dependencies]
|
||||||
ammonia = "4.0.0"
|
ammonia = "4.0.0"
|
||||||
chrono = "0.4.40"
|
chrono = "0.4.40"
|
||||||
comrak = "0.37.0"
|
comrak = "0.38.0"
|
||||||
hex_fmt = "0.3.0"
|
hex_fmt = "0.3.0"
|
||||||
num-bigint = "0.4.6"
|
num-bigint = "0.4.6"
|
||||||
rand = "0.9.0"
|
rand = "0.9.1"
|
||||||
serde = "1.0.219"
|
serde = "1.0.219"
|
||||||
sha2 = "0.10.8"
|
sha2 = "0.10.8"
|
||||||
uuid = { version = "1.16.0", features = ["v4"] }
|
uuid = { version = "1.16.0", features = ["v4"] }
|
||||||
|
|
2
example/.gitignore
vendored
2
example/.gitignore
vendored
|
@ -1,7 +1,7 @@
|
||||||
atto.db*
|
atto.db*
|
||||||
|
|
||||||
html/*
|
html/*
|
||||||
public/*
|
# public/*
|
||||||
media/*
|
media/*
|
||||||
icons/*
|
icons/*
|
||||||
langs/*
|
langs/*
|
||||||
|
|
2
example/public/robots.txt
Normal file
2
example/public/robots.txt
Normal file
|
@ -0,0 +1,2 @@
|
||||||
|
User-Agent: *
|
||||||
|
Disallow: /api
|
|
@ -15,6 +15,7 @@ banned_usernames = [
|
||||||
"notification",
|
"notification",
|
||||||
"post",
|
"post",
|
||||||
"void",
|
"void",
|
||||||
|
"anonymous"
|
||||||
]
|
]
|
||||||
town_square = 166340372315581657
|
town_square = 166340372315581657
|
||||||
|
|
||||||
|
|
2
sql_changes/questions_ip.sql
Normal file
2
sql_changes/questions_ip.sql
Normal file
|
@ -0,0 +1,2 @@
|
||||||
|
ALTER TABLE questions
|
||||||
|
ADD COLUMN ip TEXT NOT NULL DEFAULT '';
|
Loading…
Add table
Add a link
Reference in a new issue