mirror of
https://github.com/fluxerapp/fluxer
synced 2026-10-17 04:55:42 +09:00
feat(admin): one threads toggle, hide hosted-only rollouts (#3257)
This commit is contained in:
1 parent
1997850d55
commit
002502a7fa
3 files changed
+90
-355
No files matched your search
@@ -229,10 +229,11 @@ pub async fn instance_config_post(
|
|||||||
Ok(update) => instance_config_result(client.update_instance_config(&update).await),
|
Ok(update) => instance_config_result(client.update_instance_config(&update).await),
|
||||||
Err(message) => FlashData::error(message),
|
Err(message) => FlashData::error(message),
|
||||||
},
|
},
|
||||||
"update_channel_threads" => match build_channel_threads_update(&form) {
|
"update_channel_threads" => instance_config_result(
|
||||||
Ok(update) => instance_config_result(client.update_instance_config(&update).await),
|
client
|
||||||
Err(message) => FlashData::error(message),
|
.update_instance_config(&build_channel_threads_update(&form))
|
||||||
},
|
.await,
|
||||||
|
),
|
||||||
"update_experiment_delivery" => match build_experiment_delivery_update(&form) {
|
"update_experiment_delivery" => match build_experiment_delivery_update(&form) {
|
||||||
Ok(update) => instance_config_result(client.update_instance_config(&update).await),
|
Ok(update) => instance_config_result(client.update_instance_config(&update).await),
|
||||||
Err(message) => FlashData::error(message),
|
Err(message) => FlashData::error(message),
|
||||||
@@ -676,51 +677,26 @@ fn build_captcha_update(form: &MultiValueForm) -> Result<InstanceConfigUpdateReq
|
|||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
fn build_channel_threads_update(
|
fn build_channel_threads_update(form: &MultiValueForm) -> InstanceConfigUpdateRequest {
|
||||||
form: &MultiValueForm,
|
let channel_threads = if form.bool_value("channel_threads_everyone") {
|
||||||
) -> Result<InstanceConfigUpdateRequest, String> {
|
ChannelThreadsConfigUpdateRequest {
|
||||||
Ok(InstanceConfigUpdateRequest {
|
enabled: Some(true),
|
||||||
channel_threads: Some(ChannelThreadsConfigUpdateRequest {
|
guild_basis_points: Some(EXPERIMENT_ROLLOUT_BASIS_POINTS_MAX),
|
||||||
enabled: Some(form.bool_value("channel_threads_enabled")),
|
user_basis_points: Some(EXPERIMENT_ROLLOUT_BASIS_POINTS_MAX),
|
||||||
guild_basis_points: parse_form_number(
|
disabled_guild_ids: Some(Vec::new()),
|
||||||
form,
|
excluded_user_ids: Some(Vec::new()),
|
||||||
"channel_threads_guild_basis_points",
|
|
||||||
"Guild rollout basis points",
|
|
||||||
0,
|
|
||||||
EXPERIMENT_ROLLOUT_BASIS_POINTS_MAX,
|
|
||||||
)?,
|
|
||||||
guild_salt: parse_experiment_rollout_salt(form, "channel_threads_guild_salt")?,
|
|
||||||
enabled_guild_ids: Some(parse_experiment_user_ids(
|
|
||||||
form.first("channel_threads_enabled_guild_ids")
|
|
||||||
.unwrap_or_default(),
|
|
||||||
"Enabled guild IDs",
|
|
||||||
)?),
|
|
||||||
disabled_guild_ids: Some(parse_experiment_user_ids(
|
|
||||||
form.first("channel_threads_disabled_guild_ids")
|
|
||||||
.unwrap_or_default(),
|
|
||||||
"Disabled guild IDs",
|
|
||||||
)?),
|
|
||||||
user_basis_points: parse_form_number(
|
|
||||||
form,
|
|
||||||
"channel_threads_user_basis_points",
|
|
||||||
"User rollout basis points",
|
|
||||||
0,
|
|
||||||
EXPERIMENT_ROLLOUT_BASIS_POINTS_MAX,
|
|
||||||
)?,
|
|
||||||
user_salt: parse_experiment_rollout_salt(form, "channel_threads_user_salt")?,
|
|
||||||
included_user_ids: Some(parse_experiment_user_ids(
|
|
||||||
form.first("channel_threads_included_user_ids")
|
|
||||||
.unwrap_or_default(),
|
|
||||||
"Included user IDs",
|
|
||||||
)?),
|
|
||||||
excluded_user_ids: Some(parse_experiment_user_ids(
|
|
||||||
form.first("channel_threads_excluded_user_ids")
|
|
||||||
.unwrap_or_default(),
|
|
||||||
"Excluded user IDs",
|
|
||||||
)?),
|
|
||||||
}),
|
|
||||||
..Default::default()
|
..Default::default()
|
||||||
})
|
}
|
||||||
|
} else {
|
||||||
|
ChannelThreadsConfigUpdateRequest {
|
||||||
|
enabled: Some(false),
|
||||||
|
..Default::default()
|
||||||
|
}
|
||||||
|
};
|
||||||
|
InstanceConfigUpdateRequest {
|
||||||
|
channel_threads: Some(channel_threads),
|
||||||
|
..Default::default()
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fn build_experiment_delivery_update(
|
fn build_experiment_delivery_update(
|
||||||
@@ -1489,109 +1465,28 @@ mod tests {
|
|||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn build_channel_threads_update_reads_both_rollout_dimensions() {
|
fn build_channel_threads_update_turns_threads_on_for_everyone() {
|
||||||
let form = MultiValueForm::parse(
|
let form = MultiValueForm::parse(b"_csrf=token&channel_threads_everyone=true");
|
||||||
b"channel_threads_enabled=true&channel_threads_guild_basis_points=%2010%20&channel_threads_guild_salt=%20channel-threads-guild-v2%20&channel_threads_enabled_guild_ids=1600000000000000001%0A1600000000000000002%0A1600000000000000001&channel_threads_disabled_guild_ids=1600000000000000003&channel_threads_user_basis_points=10000&channel_threads_user_salt=channel-threads-user-v2&channel_threads_included_user_ids=1500000000000000001&channel_threads_excluded_user_ids=1500000000000000003%2C%201500000000000000004",
|
|
||||||
);
|
|
||||||
let update = build_channel_threads_update(&form)
|
|
||||||
.expect("valid form")
|
|
||||||
.channel_threads
|
|
||||||
.expect("channel threads update");
|
|
||||||
assert_eq!(update.enabled, Some(true));
|
|
||||||
assert_eq!(update.guild_basis_points, Some(10));
|
|
||||||
assert_eq!(
|
assert_eq!(
|
||||||
update.guild_salt,
|
serde_json::to_value(build_channel_threads_update(&form)).expect("serializable update"),
|
||||||
Some("channel-threads-guild-v2".to_owned())
|
|
||||||
);
|
|
||||||
assert_eq!(
|
|
||||||
update.enabled_guild_ids,
|
|
||||||
Some(vec![
|
|
||||||
"1600000000000000001".to_owned(),
|
|
||||||
"1600000000000000002".to_owned()
|
|
||||||
])
|
|
||||||
);
|
|
||||||
assert_eq!(
|
|
||||||
update.disabled_guild_ids,
|
|
||||||
Some(vec!["1600000000000000003".to_owned()])
|
|
||||||
);
|
|
||||||
assert_eq!(update.user_basis_points, Some(10000));
|
|
||||||
assert_eq!(update.user_salt, Some("channel-threads-user-v2".to_owned()));
|
|
||||||
assert_eq!(
|
|
||||||
update.included_user_ids,
|
|
||||||
Some(vec!["1500000000000000001".to_owned()])
|
|
||||||
);
|
|
||||||
assert_eq!(
|
|
||||||
update.excluded_user_ids,
|
|
||||||
Some(vec![
|
|
||||||
"1500000000000000003".to_owned(),
|
|
||||||
"1500000000000000004".to_owned()
|
|
||||||
])
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
#[test]
|
|
||||||
fn build_channel_threads_update_leaves_the_experiment_off_when_nothing_is_submitted() {
|
|
||||||
let form = MultiValueForm::parse(b"_csrf=token");
|
|
||||||
let request = build_channel_threads_update(&form).expect("valid form");
|
|
||||||
assert_eq!(
|
|
||||||
serde_json::to_value(request).expect("serializable update"),
|
|
||||||
serde_json::json!({"channel_threads": {
|
serde_json::json!({"channel_threads": {
|
||||||
"enabled": false,
|
"enabled": true,
|
||||||
"enabled_guild_ids": [],
|
"guild_basis_points": 10000,
|
||||||
|
"user_basis_points": 10000,
|
||||||
"disabled_guild_ids": [],
|
"disabled_guild_ids": [],
|
||||||
"included_user_ids": [],
|
|
||||||
"excluded_user_ids": [],
|
"excluded_user_ids": [],
|
||||||
}})
|
}})
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn build_channel_threads_update_rejects_invalid_targeting() {
|
fn build_channel_threads_update_only_turns_threads_off_when_unchecked() {
|
||||||
let too_many_guilds = (0..=EXPERIMENT_MAX_TARGETED_USERS)
|
let form = MultiValueForm::parse(b"_csrf=token");
|
||||||
.map(|index| index.to_string())
|
|
||||||
.collect::<Vec<_>>()
|
|
||||||
.join("%2C");
|
|
||||||
for (form, message) in [
|
|
||||||
(
|
|
||||||
"channel_threads_guild_basis_points=10001".to_owned(),
|
|
||||||
"Guild rollout basis points must be a whole number between 0 and 10000",
|
|
||||||
),
|
|
||||||
(
|
|
||||||
"channel_threads_user_basis_points=-1".to_owned(),
|
|
||||||
"User rollout basis points must be a whole number between 0 and 10000",
|
|
||||||
),
|
|
||||||
(
|
|
||||||
"channel_threads_guild_salt=%20%20".to_owned(),
|
|
||||||
"Rollout salt must be between 1 and 64 characters",
|
|
||||||
),
|
|
||||||
(
|
|
||||||
"channel_threads_user_salt=caf%C3%A9".to_owned(),
|
|
||||||
"Rollout salt must use printable ASCII",
|
|
||||||
),
|
|
||||||
(
|
|
||||||
"channel_threads_enabled_guild_ids=123%2Cinvalid".to_owned(),
|
|
||||||
"Enabled guild IDs entry 2 must contain 1 to 20 decimal digits",
|
|
||||||
),
|
|
||||||
(
|
|
||||||
"channel_threads_disabled_guild_ids=123456789012345678901".to_owned(),
|
|
||||||
"Disabled guild IDs entry 1 must contain 1 to 20 decimal digits",
|
|
||||||
),
|
|
||||||
(
|
|
||||||
"channel_threads_excluded_user_ids=abc".to_owned(),
|
|
||||||
"Excluded user IDs entry 1 must contain 1 to 20 decimal digits",
|
|
||||||
),
|
|
||||||
(
|
|
||||||
format!("channel_threads_enabled_guild_ids={too_many_guilds}"),
|
|
||||||
"Enabled guild IDs must contain at most 1000 unique IDs",
|
|
||||||
),
|
|
||||||
] {
|
|
||||||
let form = MultiValueForm::parse(form.as_bytes());
|
|
||||||
assert_eq!(
|
assert_eq!(
|
||||||
build_channel_threads_update(&form).expect_err("invalid targeting"),
|
serde_json::to_value(build_channel_threads_update(&form)).expect("serializable update"),
|
||||||
message
|
serde_json::json!({"channel_threads": {"enabled": false}})
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn build_push_relay_update_reads_the_consent_checkbox() {
|
fn build_push_relay_update_reads_the_consent_checkbox() {
|
||||||
|
|||||||
@@ -3,9 +3,8 @@
|
|||||||
use crate::{
|
use crate::{
|
||||||
api::types::{
|
api::types::{
|
||||||
AccountIdentityConfigResponse, AccountIdentityMode, AppPublicConfigResponse,
|
AccountIdentityConfigResponse, AccountIdentityMode, AppPublicConfigResponse,
|
||||||
CAPTCHA_COST_RANGE, CAPTCHA_MAX_COUNTER_RANGE, CHANNEL_THREADS_DEFAULT_GUILD_SALT,
|
CAPTCHA_COST_RANGE, CAPTCHA_MAX_COUNTER_RANGE, CaptchaConfigResponse,
|
||||||
CHANNEL_THREADS_DEFAULT_USER_SALT, CaptchaConfigResponse, ChannelThreadsConfigResponse,
|
ChannelThreadsConfigResponse, DOMAIN_MIGRATION_DEFAULT_SALT, DomainMigrationConfigResponse,
|
||||||
DOMAIN_MIGRATION_DEFAULT_SALT, DomainMigrationConfigResponse,
|
|
||||||
EXPERIMENT_MAX_TARGETED_USERS, ExperimentDeliveryConfigResponse,
|
EXPERIMENT_MAX_TARGETED_USERS, ExperimentDeliveryConfigResponse,
|
||||||
GatewayRolloutConfigResponse, InstanceConfigResponse, InstanceIntegrationsResponse,
|
GatewayRolloutConfigResponse, InstanceConfigResponse, InstanceIntegrationsResponse,
|
||||||
InstanceMediaResponse, InstancePolicyResponse, InstanceRegistrationResponse,
|
InstanceMediaResponse, InstancePolicyResponse, InstanceRegistrationResponse,
|
||||||
@@ -17,7 +16,6 @@ use crate::{
|
|||||||
middleware::auth::AuthContext,
|
middleware::auth::AuthContext,
|
||||||
templates::{
|
templates::{
|
||||||
components::{
|
components::{
|
||||||
alert::alert_warning,
|
|
||||||
badge::{BadgeVariant, badge},
|
badge::{BadgeVariant, badge},
|
||||||
form::{
|
form::{
|
||||||
FORM_INPUT_CLASS, checkbox, csrf_input, danger_button, form_actions,
|
FORM_INPUT_CLASS, checkbox, csrf_input, danger_button, form_actions,
|
||||||
@@ -192,9 +190,13 @@ pub fn instance_config_page(
|
|||||||
"Gateway rollout behavior and the limit rules applied to users and guilds.",
|
"Gateway rollout behavior and the limit rules applied to users and guilds.",
|
||||||
html! {
|
html! {
|
||||||
(gateway_rollout_section(base, csrf_token, &instance_config.gateway_rollout))
|
(gateway_rollout_section(base, csrf_token, &instance_config.gateway_rollout))
|
||||||
|
@if !instance_config.self_hosted {
|
||||||
(domain_migration_section(base, csrf_token, &instance_config.domain_migration))
|
(domain_migration_section(base, csrf_token, &instance_config.domain_migration))
|
||||||
|
}
|
||||||
(channel_threads_section(base, csrf_token, &instance_config.channel_threads))
|
(channel_threads_section(base, csrf_token, &instance_config.channel_threads))
|
||||||
|
@if !instance_config.self_hosted {
|
||||||
(plutonium_page_section(base, csrf_token, &instance_config.plutonium_page))
|
(plutonium_page_section(base, csrf_token, &instance_config.plutonium_page))
|
||||||
|
}
|
||||||
(experiment_delivery_section(base, csrf_token, &instance_config.experiment_delivery))
|
(experiment_delivery_section(base, csrf_token, &instance_config.experiment_delivery))
|
||||||
@if let Some(limit_config) = limit_config {
|
@if let Some(limit_config) = limit_config {
|
||||||
(limit_config_section(base, limit_config))
|
(limit_config_section(base, limit_config))
|
||||||
@@ -1487,182 +1489,41 @@ fn captcha_section(base: &str, csrf_token: &str, captcha: &CaptchaConfigResponse
|
|||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fn channel_threads_available_to_everyone(channel_threads: &ChannelThreadsConfigResponse) -> bool {
|
||||||
|
channel_threads.enabled
|
||||||
|
&& channel_threads.guild_basis_points >= 10_000
|
||||||
|
&& channel_threads.user_basis_points >= 10_000
|
||||||
|
&& channel_threads.disabled_guild_ids.is_empty()
|
||||||
|
&& channel_threads.excluded_user_ids.is_empty()
|
||||||
|
}
|
||||||
|
|
||||||
fn channel_threads_section(
|
fn channel_threads_section(
|
||||||
base: &str,
|
base: &str,
|
||||||
csrf_token: &str,
|
csrf_token: &str,
|
||||||
channel_threads: &ChannelThreadsConfigResponse,
|
channel_threads: &ChannelThreadsConfigResponse,
|
||||||
) -> Markup {
|
) -> Markup {
|
||||||
let status = if channel_threads.enabled {
|
let everyone = channel_threads_available_to_everyone(channel_threads);
|
||||||
("Live", BadgeVariant::Success)
|
|
||||||
} else {
|
|
||||||
("Inert", BadgeVariant::Default)
|
|
||||||
};
|
|
||||||
let enabled_guild_ids = channel_threads.enabled_guild_ids.join("\n");
|
|
||||||
let disabled_guild_ids = channel_threads.disabled_guild_ids.join("\n");
|
|
||||||
let included_user_ids = channel_threads.included_user_ids.join("\n");
|
|
||||||
let excluded_user_ids = channel_threads.excluded_user_ids.join("\n");
|
|
||||||
section_card_with_description(
|
section_card_with_description(
|
||||||
"Channel threads",
|
"Threads and forums",
|
||||||
"Threads, forum channels and media channels. A guild gets the feature only when the guild \
|
"Threads, forum channels and media channels in every community.",
|
||||||
is selected, and a member sees it only when they are also selected and use a client \
|
|
||||||
that supports threads. Bots follow the guild selection.",
|
|
||||||
html! {
|
html! {
|
||||||
form method="post" action={(base) "/instance-config?action=update_channel_threads"} {
|
form method="post" action={(base) "/instance-config?action=update_channel_threads"} {
|
||||||
(csrf_input(csrf_token))
|
(csrf_input(csrf_token))
|
||||||
div class="space-y-6" {
|
div class="space-y-6" {
|
||||||
(alert_warning("Before enabling", html! {
|
|
||||||
p class="text-sm" {
|
|
||||||
"Enable only after every gateway role, the api, the workers and the \
|
|
||||||
messages service run the gate build and the bit 34-38 overwrite audit \
|
|
||||||
is clean."
|
|
||||||
}
|
|
||||||
}))
|
|
||||||
div class="flex flex-wrap items-center gap-2" {
|
|
||||||
h3 class="text-sm font-semibold text-neutral-900" { "Master switch" }
|
|
||||||
(badge(status.0, status.1))
|
|
||||||
@if channel_threads.ever_enabled {
|
|
||||||
(badge("Ever enabled", BadgeVariant::Warning))
|
|
||||||
}
|
|
||||||
span class="text-xs text-neutral-500" {
|
|
||||||
"Config version " (channel_threads.config_version)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
(checkbox(
|
(checkbox(
|
||||||
"channel_threads_enabled",
|
"channel_threads_everyone",
|
||||||
"true",
|
"true",
|
||||||
"Turn on threads for the selected guilds and users",
|
"Available to everyone",
|
||||||
channel_threads.enabled,
|
everyone,
|
||||||
true,
|
true,
|
||||||
))
|
))
|
||||||
|
@if channel_threads.enabled && !everyone {
|
||||||
p class="text-xs text-neutral-500" {
|
p class="text-xs text-neutral-500" {
|
||||||
"Off is the safe state and the kill switch. With this unchecked no guild \
|
"Currently on for part of this instance. Saving applies the setting above to everyone."
|
||||||
has threads, and existing threads and forums stay stored but hidden \
|
|
||||||
until it is turned back on."
|
|
||||||
}
|
|
||||||
|
|
||||||
h3 class="text-sm font-semibold text-neutral-900" { "Guilds" }
|
|
||||||
(number_field(
|
|
||||||
"channel_threads_guild_basis_points",
|
|
||||||
"Guild rollout (basis points)",
|
|
||||||
&channel_threads.guild_basis_points.to_string(),
|
|
||||||
Some(0), Some(10000), "1",
|
|
||||||
Some("Share of guilds bucketed into the experiment, in basis points: 0 is nobody, 100 is 1%, 10000 is every guild."),
|
|
||||||
))
|
|
||||||
div class="flex flex-col gap-2" {
|
|
||||||
(text_input(
|
|
||||||
"channel_threads_guild_salt",
|
|
||||||
"Guild rollout salt",
|
|
||||||
&channel_threads.guild_salt,
|
|
||||||
CHANNEL_THREADS_DEFAULT_GUILD_SALT,
|
|
||||||
))
|
|
||||||
p class="text-xs text-neutral-500" {
|
|
||||||
"Seeds the guild bucketing hash. Printable ASCII only. Changing it \
|
|
||||||
reshuffles which guilds fall inside the percentage above."
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
div class="flex flex-col gap-2" {
|
|
||||||
(textarea_input(
|
|
||||||
"channel_threads_enabled_guild_ids",
|
|
||||||
"Always-on Guild IDs",
|
|
||||||
"1600000000000000001\n1600000000000000002",
|
|
||||||
&enabled_guild_ids,
|
|
||||||
4,
|
|
||||||
false,
|
|
||||||
))
|
|
||||||
(entry_count_hint(
|
|
||||||
channel_threads.enabled_guild_ids.len(),
|
|
||||||
EXPERIMENT_MAX_TARGETED_USERS,
|
|
||||||
))
|
|
||||||
p class="text-xs text-neutral-500" {
|
|
||||||
"One snowflake per line, or comma separated. These guilds are targeted \
|
|
||||||
regardless of the percentage above. IDs must contain 1 to 20 decimal \
|
|
||||||
digits. Invalid entries prevent the save. Blank entries and duplicate \
|
|
||||||
IDs are ignored."
|
|
||||||
}
|
|
||||||
}
|
|
||||||
div class="flex flex-col gap-2" {
|
|
||||||
(textarea_input(
|
|
||||||
"channel_threads_disabled_guild_ids",
|
|
||||||
"Never-on Guild IDs",
|
|
||||||
"1600000000000000003\n1600000000000000004",
|
|
||||||
&disabled_guild_ids,
|
|
||||||
4,
|
|
||||||
false,
|
|
||||||
))
|
|
||||||
(entry_count_hint(
|
|
||||||
channel_threads.disabled_guild_ids.len(),
|
|
||||||
EXPERIMENT_MAX_TARGETED_USERS,
|
|
||||||
))
|
|
||||||
p class="text-xs text-neutral-500" {
|
|
||||||
"Same format. Exclusion wins over both the always-on list and the \
|
|
||||||
percentage. This is the per-guild kill switch."
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
h3 class="text-sm font-semibold text-neutral-900" { "Users" }
|
|
||||||
(number_field(
|
|
||||||
"channel_threads_user_basis_points",
|
|
||||||
"User rollout (basis points)",
|
|
||||||
&channel_threads.user_basis_points.to_string(),
|
|
||||||
Some(0), Some(10000), "1",
|
|
||||||
Some("Share of users bucketed into the experiment, in basis points. Set 10000 before enrolling any guild outside staff, so every member of that guild, moderators included, sees its threads."),
|
|
||||||
))
|
|
||||||
div class="flex flex-col gap-2" {
|
|
||||||
(text_input(
|
|
||||||
"channel_threads_user_salt",
|
|
||||||
"User rollout salt",
|
|
||||||
&channel_threads.user_salt,
|
|
||||||
CHANNEL_THREADS_DEFAULT_USER_SALT,
|
|
||||||
))
|
|
||||||
p class="text-xs text-neutral-500" {
|
|
||||||
"Seeds the user bucketing hash. Printable ASCII only. Changing it \
|
|
||||||
reshuffles which users fall inside the percentage above."
|
|
||||||
}
|
|
||||||
}
|
|
||||||
div class="flex flex-col gap-2" {
|
|
||||||
(textarea_input(
|
|
||||||
"channel_threads_included_user_ids",
|
|
||||||
"Always-on User IDs",
|
|
||||||
"1500000000000000001\n1500000000000000002",
|
|
||||||
&included_user_ids,
|
|
||||||
4,
|
|
||||||
false,
|
|
||||||
))
|
|
||||||
(entry_count_hint(
|
|
||||||
channel_threads.included_user_ids.len(),
|
|
||||||
EXPERIMENT_MAX_TARGETED_USERS,
|
|
||||||
))
|
|
||||||
p class="text-xs text-neutral-500" {
|
|
||||||
"Same format as the guild lists. These users are targeted regardless \
|
|
||||||
of the percentage above."
|
|
||||||
}
|
|
||||||
}
|
|
||||||
div class="flex flex-col gap-2" {
|
|
||||||
(textarea_input(
|
|
||||||
"channel_threads_excluded_user_ids",
|
|
||||||
"Never-on User IDs",
|
|
||||||
"1500000000000000003\n1500000000000000004",
|
|
||||||
&excluded_user_ids,
|
|
||||||
4,
|
|
||||||
false,
|
|
||||||
))
|
|
||||||
(entry_count_hint(
|
|
||||||
channel_threads.excluded_user_ids.len(),
|
|
||||||
EXPERIMENT_MAX_TARGETED_USERS,
|
|
||||||
))
|
|
||||||
p class="text-xs text-neutral-500" {
|
|
||||||
"Same format. Exclusion wins over both the always-on list and the \
|
|
||||||
percentage, and it also applies to bots."
|
|
||||||
}
|
|
||||||
}
|
|
||||||
(alert_warning("Excluded bots", html! {
|
|
||||||
p class="text-sm" {
|
|
||||||
"Excluded bots are blind to threads, including moderation bots."
|
|
||||||
}
|
|
||||||
}))
|
|
||||||
|
|
||||||
(form_actions(html! {
|
(form_actions(html! {
|
||||||
(submit_button("Save channel threads configuration"))
|
(submit_button("Save"))
|
||||||
}))
|
}))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -2414,49 +2275,35 @@ mod tests {
|
|||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn channel_threads_section_shows_both_dimensions_and_the_warnings() {
|
fn channel_threads_section_is_a_single_everyone_toggle() {
|
||||||
let channel_threads = ChannelThreadsConfigResponse {
|
let everyone = ChannelThreadsConfigResponse {
|
||||||
enabled: true,
|
enabled: true,
|
||||||
config_version: 7,
|
guild_basis_points: 10_000,
|
||||||
ever_enabled: true,
|
user_basis_points: 10_000,
|
||||||
guild_basis_points: 25,
|
|
||||||
enabled_guild_ids: vec!["1600000000000000001".to_owned()],
|
|
||||||
excluded_user_ids: vec![
|
|
||||||
"1500000000000000002".to_owned(),
|
|
||||||
"1500000000000000003".to_owned(),
|
|
||||||
],
|
|
||||||
..ChannelThreadsConfigResponse::default()
|
..ChannelThreadsConfigResponse::default()
|
||||||
};
|
};
|
||||||
let markup = channel_threads_section("/admin", "csrf", &channel_threads).into_string();
|
let on = channel_threads_section("/admin", "csrf", &everyone).into_string();
|
||||||
assert!(markup.contains("action=update_channel_threads"));
|
assert!(on.contains("action=update_channel_threads"));
|
||||||
for name in [
|
assert!(on.contains("name=\"channel_threads_everyone\""));
|
||||||
"channel_threads_enabled",
|
assert!(on.contains("checked"));
|
||||||
"channel_threads_guild_basis_points",
|
assert!(!on.contains("basis_points"));
|
||||||
"channel_threads_guild_salt",
|
assert!(!on.contains("part of this instance"));
|
||||||
"channel_threads_enabled_guild_ids",
|
|
||||||
"channel_threads_disabled_guild_ids",
|
let partial = ChannelThreadsConfigResponse {
|
||||||
"channel_threads_user_basis_points",
|
enabled: true,
|
||||||
"channel_threads_user_salt",
|
enabled_guild_ids: vec!["1600000000000000001".to_owned()],
|
||||||
"channel_threads_included_user_ids",
|
user_basis_points: 10_000,
|
||||||
"channel_threads_excluded_user_ids",
|
..ChannelThreadsConfigResponse::default()
|
||||||
] {
|
};
|
||||||
assert!(markup.contains(&format!("name=\"{name}\"")), "{name}");
|
let partial = channel_threads_section("/admin", "csrf", &partial).into_string();
|
||||||
}
|
assert!(!partial.contains("checked"));
|
||||||
assert!(markup.contains("value=\"25\""));
|
assert!(partial.contains("part of this instance"));
|
||||||
assert!(markup.contains("Config version 7"));
|
|
||||||
assert!(markup.contains("Ever enabled"));
|
|
||||||
assert!(markup.contains("bit 34-38 overwrite audit is clean"));
|
|
||||||
assert!(markup.contains("Excluded bots are blind to threads, including moderation bots."));
|
|
||||||
assert!(markup.contains("1 of 1000 stored"));
|
|
||||||
assert!(markup.contains("2 of 1000 stored"));
|
|
||||||
|
|
||||||
let off =
|
let off =
|
||||||
channel_threads_section("/admin", "csrf", &ChannelThreadsConfigResponse::default())
|
channel_threads_section("/admin", "csrf", &ChannelThreadsConfigResponse::default())
|
||||||
.into_string();
|
.into_string();
|
||||||
assert!(off.contains("Inert"));
|
assert!(!off.contains("checked"));
|
||||||
assert!(!off.contains("Ever enabled"));
|
assert!(!off.contains("part of this instance"));
|
||||||
assert!(off.contains(CHANNEL_THREADS_DEFAULT_GUILD_SALT));
|
|
||||||
assert!(off.contains(CHANNEL_THREADS_DEFAULT_USER_SALT));
|
|
||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
|
|||||||
@@ -491,13 +491,14 @@ async fn channel_threads_section_renders_and_saves_through_htmx_toasts() {
|
|||||||
let app = setup().await;
|
let app = setup().await;
|
||||||
let (headers, body) = get_with_headers(&app, "/instance-config", &[]).await;
|
let (headers, body) = get_with_headers(&app, "/instance-config", &[]).await;
|
||||||
assert_full_layout(&body);
|
assert_full_layout(&body);
|
||||||
assert!(body.contains("Channel threads"), "{body}");
|
assert!(body.contains("Threads and forums"), "{body}");
|
||||||
assert!(body.contains("Config version 3"), "{body}");
|
assert!(body.contains("Available to everyone"), "{body}");
|
||||||
assert!(body.contains("Ever enabled"), "{body}");
|
|
||||||
assert!(body.contains("1600000000000000001"), "{body}");
|
|
||||||
assert!(body.contains("1500000000000000009"), "{body}");
|
|
||||||
assert!(
|
assert!(
|
||||||
body.contains("Excluded bots are blind to threads, including moderation bots."),
|
body.contains(r#"name="channel_threads_everyone""#),
|
||||||
|
"{body}"
|
||||||
|
);
|
||||||
|
assert!(
|
||||||
|
!body.contains("channel_threads_guild_basis_points"),
|
||||||
"{body}"
|
"{body}"
|
||||||
);
|
);
|
||||||
let csrf_token = csrf_cookie(&headers)
|
let csrf_token = csrf_cookie(&headers)
|
||||||
@@ -509,17 +510,9 @@ async fn channel_threads_section_renders_and_saves_through_htmx_toasts() {
|
|||||||
("Cookie", cookie.as_str()),
|
("Cookie", cookie.as_str()),
|
||||||
];
|
];
|
||||||
|
|
||||||
for (form, expected) in [
|
for form in [
|
||||||
(
|
format!("_csrf={csrf_token}&channel_threads_everyone=true"),
|
||||||
format!(
|
format!("_csrf={csrf_token}"),
|
||||||
"_csrf={csrf_token}&channel_threads_enabled=true&channel_threads_guild_basis_points=0&channel_threads_enabled_guild_ids=1600000000000000001&channel_threads_user_basis_points=10000"
|
|
||||||
),
|
|
||||||
"Instance config updated",
|
|
||||||
),
|
|
||||||
(
|
|
||||||
format!("_csrf={csrf_token}&channel_threads_enabled_guild_ids=not-a-guild"),
|
|
||||||
"Enabled guild IDs entry 1 must contain 1 to 20 decimal digits",
|
|
||||||
),
|
|
||||||
] {
|
] {
|
||||||
let (status, response_headers, response_body) = post_form_with_headers(
|
let (status, response_headers, response_body) = post_form_with_headers(
|
||||||
&app,
|
&app,
|
||||||
@@ -533,7 +526,7 @@ async fn channel_threads_section_renders_and_saves_through_htmx_toasts() {
|
|||||||
.get("X-Fluxer-Admin-Toast")
|
.get("X-Fluxer-Admin-Toast")
|
||||||
.and_then(|value| value.to_str().ok())
|
.and_then(|value| value.to_str().ok())
|
||||||
.unwrap_or_else(|| panic!("missing toast header\n{response_body}"));
|
.unwrap_or_else(|| panic!("missing toast header\n{response_body}"));
|
||||||
assert!(toast.contains(expected), "{toast}");
|
assert!(toast.contains("Instance config updated"), "{toast}");
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
Reference in new issue
Block a user