mirror of
https://github.com/fluxerapp/fluxer
synced 2026-10-07 19:22:14 +09:00
feat(profile): ship profile timezone to everyone (#3034)
This commit is contained in:
@@ -38,7 +38,6 @@ import {
|
||||
import {AltchaCaptchaConfigSchema} from '@fluxer/schema/src/domains/admin/AltchaCaptchaSchemas';
|
||||
import {DomainMigrationConfigSchema} from '@fluxer/schema/src/domains/admin/DomainMigrationSchemas';
|
||||
import {GatewayRolloutConfigSchema} from '@fluxer/schema/src/domains/admin/GatewayRolloutSchemas';
|
||||
import {ProfileTimezoneConfigSchema} from '@fluxer/schema/src/domains/admin/ProfileTimezoneSchemas';
|
||||
import type {PushRelayConfig, PushRelayConfigUpdateRequest} from '@fluxer/schema/src/domains/admin/PushRelaySchemas';
|
||||
import {UserIdParam} from '@fluxer/schema/src/domains/common/CommonParamSchemas';
|
||||
import {ExperimentDeliveryConfigSchema} from '@fluxer/schema/src/domains/experiment/ExperimentSchemas';
|
||||
@@ -69,7 +68,6 @@ async function buildInstanceConfigResponse(): Promise<InstanceConfigResponse> {
|
||||
pushRelay,
|
||||
domainMigration,
|
||||
altchaCaptcha,
|
||||
profileTimezone,
|
||||
experimentDelivery,
|
||||
registrationConfig,
|
||||
registrationUrls,
|
||||
@@ -80,7 +78,6 @@ async function buildInstanceConfigResponse(): Promise<InstanceConfigResponse> {
|
||||
instanceConfigRepository.getPushRelayConfig(),
|
||||
instanceConfigRepository.getDomainMigrationConfig(),
|
||||
instanceConfigRepository.getAltchaCaptchaConfig(),
|
||||
instanceConfigRepository.getProfileTimezoneConfig(),
|
||||
instanceConfigRepository.getExperimentDeliveryConfig(),
|
||||
instanceConfigRepository.getRegistrationConfig(),
|
||||
instanceConfigRepository.getRegistrationUrlsForAdmin(),
|
||||
@@ -115,7 +112,6 @@ async function buildInstanceConfigResponse(): Promise<InstanceConfigResponse> {
|
||||
push_relay: pushRelay,
|
||||
domain_migration: domainMigration,
|
||||
altcha_captcha: altchaCaptcha,
|
||||
profile_timezone: profileTimezone,
|
||||
experiment_delivery: experimentDelivery,
|
||||
registration: {
|
||||
...registrationConfig,
|
||||
@@ -404,18 +400,6 @@ export function InstanceConfigAdminController(app: HonoApp) {
|
||||
);
|
||||
}
|
||||
}
|
||||
if (data.profile_timezone) {
|
||||
const patch = omitUndefinedFields(data.profile_timezone);
|
||||
if (Object.keys(patch).length > 0) {
|
||||
await instanceConfigRepository.updateProfileTimezoneConfig((current) =>
|
||||
ProfileTimezoneConfigSchema.parse({
|
||||
...current,
|
||||
...patch,
|
||||
config_version: current.config_version + 1,
|
||||
}),
|
||||
);
|
||||
}
|
||||
}
|
||||
if (data.experiment_delivery) {
|
||||
const patch = data.experiment_delivery;
|
||||
await instanceConfigRepository.updateExperimentDeliveryConfig((current) =>
|
||||
|
||||
@@ -11,7 +11,6 @@ import {entityTagMatches} from '@app/api/utils/EntityTag';
|
||||
import {Headers as HttpHeaders} from '@fluxer/constants/src/Headers';
|
||||
import {resolveAltchaCaptchaAssignment} from '@fluxer/schema/src/domains/admin/AltchaCaptchaSchemas';
|
||||
import {resolveDomainMigrationAssignment} from '@fluxer/schema/src/domains/admin/DomainMigrationSchemas';
|
||||
import {resolveProfileTimezoneAssignment} from '@fluxer/schema/src/domains/admin/ProfileTimezoneSchemas';
|
||||
import {ExperimentAssignmentsResponse} from '@fluxer/schema/src/domains/experiment/ExperimentSchemas';
|
||||
|
||||
export function ExperimentController(app: HonoApp) {
|
||||
@@ -31,26 +30,20 @@ export function ExperimentController(app: HonoApp) {
|
||||
}),
|
||||
async (ctx) => {
|
||||
const instanceConfigRepository = ctx.get('instanceConfigRepository');
|
||||
const [delivery, domainMigrationConfig, altchaCaptchaConfig, profileTimezoneConfig] = await Promise.all([
|
||||
const [delivery, domainMigrationConfig, altchaCaptchaConfig] = await Promise.all([
|
||||
instanceConfigRepository.getExperimentDeliveryConfig(),
|
||||
instanceConfigRepository.getDomainMigrationConfig(),
|
||||
instanceConfigRepository.getAltchaCaptchaConfig(),
|
||||
instanceConfigRepository.getProfileTimezoneConfig(),
|
||||
]);
|
||||
const user = ctx.get('user');
|
||||
const userId = user.id.toString();
|
||||
const targeting = await resolveExperimentTargeting(user, [
|
||||
domainMigrationConfig,
|
||||
altchaCaptchaConfig,
|
||||
profileTimezoneConfig,
|
||||
]);
|
||||
const targeting = await resolveExperimentTargeting(user, [domainMigrationConfig, altchaCaptchaConfig]);
|
||||
const body: ExperimentAssignmentsResponse = {
|
||||
poll_interval_seconds: delivery.poll_interval_seconds,
|
||||
poll_jitter_percent: delivery.poll_jitter_percent,
|
||||
assignments: {
|
||||
domain_migration: resolveDomainMigrationAssignment(domainMigrationConfig, userId, targeting),
|
||||
altcha_captcha: resolveAltchaCaptchaAssignment(altchaCaptchaConfig, userId, targeting),
|
||||
profile_timezone: resolveProfileTimezoneAssignment(profileTimezoneConfig, userId, targeting),
|
||||
},
|
||||
};
|
||||
const etag = `"${createHash('sha256').update(JSON.stringify(body)).digest('hex')}"`;
|
||||
|
||||
@@ -17,10 +17,6 @@ import {
|
||||
DEFAULT_DOMAIN_MIGRATION_CONFIG,
|
||||
INERT_DOMAIN_MIGRATION_ASSIGNMENT,
|
||||
} from '@fluxer/schema/src/domains/admin/DomainMigrationSchemas';
|
||||
import {
|
||||
DEFAULT_PROFILE_TIMEZONE_CONFIG,
|
||||
INERT_PROFILE_TIMEZONE_ASSIGNMENT,
|
||||
} from '@fluxer/schema/src/domains/admin/ProfileTimezoneSchemas';
|
||||
import {
|
||||
DEFAULT_EXPERIMENT_POLL_INTERVAL_SECONDS,
|
||||
DEFAULT_EXPERIMENT_POLL_JITTER_PERCENT,
|
||||
@@ -63,7 +59,6 @@ describe('GET /experiments', () => {
|
||||
assignments: {
|
||||
domain_migration: INERT_DOMAIN_MIGRATION_ASSIGNMENT,
|
||||
altcha_captcha: INERT_ALTCHA_CAPTCHA_ASSIGNMENT,
|
||||
profile_timezone: INERT_PROFILE_TIMEZONE_ASSIGNMENT,
|
||||
},
|
||||
});
|
||||
});
|
||||
@@ -170,56 +165,6 @@ describe('GET /experiments', () => {
|
||||
expect(body.assignments.altcha_captcha).toEqual({enabled: true});
|
||||
});
|
||||
|
||||
it('resolves the profile timezone caller through the allowlist and the exclusion list', async () => {
|
||||
const targeted = await createTestAccount(harness);
|
||||
const excluded = await createTestAccount(harness);
|
||||
await getInstanceConfigRepository().setProfileTimezoneConfig({
|
||||
...DEFAULT_PROFILE_TIMEZONE_CONFIG,
|
||||
enabled: true,
|
||||
rollout_basis_points: 10000,
|
||||
included_user_ids: [targeted.userId],
|
||||
excluded_user_ids: [excluded.userId],
|
||||
});
|
||||
|
||||
const targetedBody = await createBuilder<ExperimentAssignmentsResponse>(harness, targeted.token)
|
||||
.get(ENDPOINT)
|
||||
.execute();
|
||||
expect(targetedBody.assignments.profile_timezone).toEqual({enabled: true});
|
||||
|
||||
const excludedBody = await createBuilder<ExperimentAssignmentsResponse>(harness, excluded.token)
|
||||
.get(ENDPOINT)
|
||||
.execute();
|
||||
expect(excludedBody.assignments.profile_timezone).toEqual({enabled: false});
|
||||
});
|
||||
|
||||
it('bumps the profile timezone config version on every admin update without the client sending one', async () => {
|
||||
const admin = await setUserACLs(harness, await createTestAccount(harness), [
|
||||
AdminACLs.AUTHENTICATE,
|
||||
AdminACLs.INSTANCE_CONFIG_VIEW,
|
||||
AdminACLs.INSTANCE_CONFIG_UPDATE,
|
||||
]);
|
||||
|
||||
const afterFirst = await createBuilder<{profile_timezone: {config_version: number; enabled: boolean}}>(
|
||||
harness,
|
||||
admin.token,
|
||||
)
|
||||
.patch('/admin/instance/config')
|
||||
.body({profile_timezone: {enabled: true, included_user_ids: [admin.userId]}})
|
||||
.execute();
|
||||
expect(afterFirst.profile_timezone).toMatchObject({config_version: 1, enabled: true});
|
||||
|
||||
const afterSecond = await createBuilder<{
|
||||
profile_timezone: {config_version: number; rollout_basis_points: number};
|
||||
}>(harness, admin.token)
|
||||
.patch('/admin/instance/config')
|
||||
.body({profile_timezone: {rollout_basis_points: 2500}})
|
||||
.execute();
|
||||
expect(afterSecond.profile_timezone).toMatchObject({config_version: 2, rollout_basis_points: 2500});
|
||||
|
||||
const body = await createBuilder<ExperimentAssignmentsResponse>(harness, admin.token).get(ENDPOINT).execute();
|
||||
expect(body.assignments.profile_timezone).toEqual({enabled: true});
|
||||
});
|
||||
|
||||
it('enrols members of an included guild in every experiment and leaves everyone else out', async () => {
|
||||
const owner = await createTestAccount(harness);
|
||||
const member = await createTestAccount(harness);
|
||||
@@ -239,25 +184,18 @@ describe('GET /experiments', () => {
|
||||
enabled: true,
|
||||
included_guild_ids: [guild.id],
|
||||
});
|
||||
await repository.setProfileTimezoneConfig({
|
||||
...DEFAULT_PROFILE_TIMEZONE_CONFIG,
|
||||
enabled: true,
|
||||
included_guild_ids: [guild.id],
|
||||
});
|
||||
|
||||
const memberBody = await createBuilder<ExperimentAssignmentsResponse>(harness, member.token)
|
||||
.get(ENDPOINT)
|
||||
.execute();
|
||||
expect(memberBody.assignments.domain_migration).toEqual({enabled: true});
|
||||
expect(memberBody.assignments.altcha_captcha).toEqual({enabled: true});
|
||||
expect(memberBody.assignments.profile_timezone).toEqual({enabled: true});
|
||||
|
||||
const outsiderBody = await createBuilder<ExperimentAssignmentsResponse>(harness, outsider.token)
|
||||
.get(ENDPOINT)
|
||||
.execute();
|
||||
expect(outsiderBody.assignments.domain_migration).toEqual({enabled: false});
|
||||
expect(outsiderBody.assignments.altcha_captcha).toEqual({enabled: false});
|
||||
expect(outsiderBody.assignments.profile_timezone).toEqual({enabled: false});
|
||||
});
|
||||
|
||||
it('enrols premium users, subscription and lifetime alike, when the switch is on', async () => {
|
||||
@@ -266,8 +204,8 @@ describe('GET /experiments', () => {
|
||||
const free = await createTestAccount(harness);
|
||||
await grantPremium(harness, subscriber.userId, UserPremiumTypes.SUBSCRIPTION);
|
||||
await grantPremium(harness, visionary.userId, UserPremiumTypes.LIFETIME);
|
||||
await getInstanceConfigRepository().setProfileTimezoneConfig({
|
||||
...DEFAULT_PROFILE_TIMEZONE_CONFIG,
|
||||
await getInstanceConfigRepository().setDomainMigrationConfig({
|
||||
...DEFAULT_DOMAIN_MIGRATION_CONFIG,
|
||||
enabled: true,
|
||||
include_premium_users: true,
|
||||
});
|
||||
@@ -277,7 +215,7 @@ describe('GET /experiments', () => {
|
||||
[free, false],
|
||||
] as const) {
|
||||
const body = await createBuilder<ExperimentAssignmentsResponse>(harness, account.token).get(ENDPOINT).execute();
|
||||
expect(body.assignments.profile_timezone).toEqual({enabled: expected});
|
||||
expect(body.assignments.domain_migration).toEqual({enabled: expected});
|
||||
}
|
||||
});
|
||||
|
||||
@@ -289,22 +227,17 @@ describe('GET /experiments', () => {
|
||||
]);
|
||||
const guildIds = ['1500000000000000001', '1500000000000000002'];
|
||||
const body = await createBuilder<
|
||||
Record<
|
||||
'domain_migration' | 'altcha_captcha' | 'profile_timezone',
|
||||
{included_guild_ids: Array<string>; include_premium_users: boolean}
|
||||
>
|
||||
Record<'domain_migration' | 'altcha_captcha', {included_guild_ids: Array<string>; include_premium_users: boolean}>
|
||||
>(harness, admin.token)
|
||||
.patch('/admin/instance/config')
|
||||
.body({
|
||||
domain_migration: {included_guild_ids: guildIds, include_premium_users: true},
|
||||
altcha_captcha: {included_guild_ids: guildIds, include_premium_users: true},
|
||||
profile_timezone: {included_guild_ids: guildIds, include_premium_users: true},
|
||||
})
|
||||
.execute();
|
||||
expect(body.domain_migration.included_guild_ids).toEqual(guildIds);
|
||||
expect(body.altcha_captcha.included_guild_ids).toEqual(guildIds);
|
||||
expect(body.profile_timezone.included_guild_ids).toEqual(guildIds);
|
||||
for (const section of [body.domain_migration, body.altcha_captcha, body.profile_timezone]) {
|
||||
for (const section of [body.domain_migration, body.altcha_captcha]) {
|
||||
expect(section.include_premium_users).toBe(true);
|
||||
}
|
||||
});
|
||||
|
||||
@@ -50,10 +50,6 @@ import {
|
||||
type StoredBillingConfig,
|
||||
StoredBillingConfigSchema,
|
||||
} from '@fluxer/schema/src/domains/admin/InstanceBillingSchemas';
|
||||
import {
|
||||
type ProfileTimezoneConfig,
|
||||
ProfileTimezoneConfigSchema,
|
||||
} from '@fluxer/schema/src/domains/admin/ProfileTimezoneSchemas';
|
||||
import {
|
||||
type LegacyPushServiceDeliveryWire,
|
||||
type PushRelayConfig,
|
||||
@@ -84,7 +80,6 @@ const GATEWAY_ROLLOUT_CONFIG_KEY = 'gateway_rollout_config';
|
||||
const PUSH_RELAY_CONFIG_KEY = 'push_service_delivery_config';
|
||||
const DOMAIN_MIGRATION_CONFIG_KEY = 'domain_migration_config';
|
||||
const ALTCHA_CAPTCHA_CONFIG_KEY = 'altcha_captcha_config';
|
||||
const PROFILE_TIMEZONE_CONFIG_KEY = 'profile_timezone_config';
|
||||
const EXPERIMENT_DELIVERY_CONFIG_KEY = 'experiment_delivery_config';
|
||||
const REGISTRATION_CONFIG_KEY = 'registration_config';
|
||||
const REGISTRATION_URLS_KEY = 'registration_urls';
|
||||
@@ -441,7 +436,6 @@ type StoredConfigSection =
|
||||
| 'push relay'
|
||||
| 'domain migration'
|
||||
| 'altcha captcha'
|
||||
| 'profile timezone'
|
||||
| 'experiment delivery'
|
||||
| 'instance policy'
|
||||
| 'integrations'
|
||||
@@ -606,10 +600,6 @@ function parseStoredAltchaCaptchaConfig(raw: string | null): AltchaCaptchaConfig
|
||||
return parseStoredConfigOrDefault(AltchaCaptchaConfigSchema, raw, 'altcha captcha');
|
||||
}
|
||||
|
||||
function parseStoredProfileTimezoneConfig(raw: string | null): ProfileTimezoneConfig {
|
||||
return parseStoredConfigOrDefault(ProfileTimezoneConfigSchema, raw, 'profile timezone');
|
||||
}
|
||||
|
||||
function parseStoredExperimentDeliveryConfig(raw: string | null): ExperimentDeliveryConfig {
|
||||
return parseStoredConfigOrDefault(ExperimentDeliveryConfigSchema, raw, 'experiment delivery');
|
||||
}
|
||||
@@ -1281,7 +1271,6 @@ export class InstanceConfigRepository {
|
||||
parseStoredPushRelayConfig(snapshot.get(PUSH_RELAY_CONFIG_KEY) ?? null);
|
||||
parseStoredDomainMigrationConfig(snapshot.get(DOMAIN_MIGRATION_CONFIG_KEY) ?? null);
|
||||
parseStoredAltchaCaptchaConfig(snapshot.get(ALTCHA_CAPTCHA_CONFIG_KEY) ?? null);
|
||||
parseStoredProfileTimezoneConfig(snapshot.get(PROFILE_TIMEZONE_CONFIG_KEY) ?? null);
|
||||
parseStoredExperimentDeliveryConfig(snapshot.get(EXPERIMENT_DELIVERY_CONFIG_KEY) ?? null);
|
||||
parseStoredInstancePolicyConfig(snapshot.get(INSTANCE_POLICY_CONFIG_KEY) ?? null);
|
||||
checkStoredConfig('registration', () =>
|
||||
@@ -1412,27 +1401,6 @@ export class InstanceConfigRepository {
|
||||
);
|
||||
}
|
||||
|
||||
async getProfileTimezoneConfig(): Promise<ProfileTimezoneConfig> {
|
||||
const raw = await this.getConfig(PROFILE_TIMEZONE_CONFIG_KEY);
|
||||
return parseStoredProfileTimezoneConfig(raw);
|
||||
}
|
||||
|
||||
async setProfileTimezoneConfig(config: ProfileTimezoneConfig): Promise<void> {
|
||||
await this.updateProfileTimezoneConfig(() => config);
|
||||
}
|
||||
|
||||
updateProfileTimezoneConfig(
|
||||
update: (current: ProfileTimezoneConfig) => ProfileTimezoneConfig,
|
||||
): Promise<ProfileTimezoneConfig> {
|
||||
return this.updateStoredConfig(PROFILE_TIMEZONE_CONFIG_KEY, (raw) =>
|
||||
validateStoredConfig(
|
||||
ProfileTimezoneConfigSchema,
|
||||
update(parseStoredProfileTimezoneConfig(raw)),
|
||||
'profile timezone',
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
async getExperimentDeliveryConfig(): Promise<ExperimentDeliveryConfig> {
|
||||
const raw = await this.getConfig(EXPERIMENT_DELIVERY_CONFIG_KEY);
|
||||
return parseStoredExperimentDeliveryConfig(raw);
|
||||
|
||||
@@ -25109,13 +25109,10 @@
|
||||
"anyOf": [{"$ref": "#/components/schemas/ColorType"}, {"type": "null"}]
|
||||
},
|
||||
"timezone": {
|
||||
"description": "IANA timezone identifier saved for profile local time. Ignored unless the profile_timezone experiment serves the user.",
|
||||
"description": "IANA timezone identifier saved for profile local time",
|
||||
"type": ["string", "null"]
|
||||
},
|
||||
"timezone_privacy_flags": {
|
||||
"description": "Bitfield controlling who can see the profile timezone. Ignored unless the profile_timezone experiment serves the user.",
|
||||
"$ref": "#/components/schemas/ProfileFieldPrivacyFlags"
|
||||
},
|
||||
"timezone_privacy_flags": {"$ref": "#/components/schemas/ProfileFieldPrivacyFlags"},
|
||||
"premium_badge_hidden": {"type": "boolean", "description": "Whether to hide the premium badge"},
|
||||
"premium_badge_masked": {"type": "boolean", "description": "Whether to mask the premium badge"},
|
||||
"premium_badge_timestamp_hidden": {
|
||||
@@ -27944,8 +27941,7 @@
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"domain_migration": {"$ref": "#/components/schemas/DomainMigrationAssignmentResponse"},
|
||||
"altcha_captcha": {"$ref": "#/components/schemas/AltchaCaptchaAssignmentResponse"},
|
||||
"profile_timezone": {"$ref": "#/components/schemas/ProfileTimezoneAssignmentResponse"}
|
||||
"altcha_captcha": {"$ref": "#/components/schemas/AltchaCaptchaAssignmentResponse"}
|
||||
},
|
||||
"additionalProperties": false
|
||||
}
|
||||
@@ -31644,12 +31640,6 @@
|
||||
"additionalProperties": false
|
||||
},
|
||||
"DonationCurrency": {"type": "string", "enum": ["usd", "eur", "brl", "inr", "pln", "try", "sek", "dkk", "nok"]},
|
||||
"ProfileTimezoneAssignmentResponse": {
|
||||
"type": "object",
|
||||
"properties": {"enabled": {"type": "boolean"}},
|
||||
"required": ["enabled"],
|
||||
"additionalProperties": false
|
||||
},
|
||||
"AltchaCaptchaAssignmentResponse": {
|
||||
"type": "object",
|
||||
"properties": {"enabled": {"type": "boolean"}},
|
||||
|
||||
@@ -2,10 +2,8 @@
|
||||
|
||||
import {Config} from '@app/api/Config';
|
||||
import type {UserRow} from '@app/api/database/types/UserTypes';
|
||||
import {resolveExperimentTargeting} from '@app/api/experiment/ExperimentTargeting';
|
||||
import {sharedListHas} from '@app/api/infrastructure/activity/SharedLists';
|
||||
import {getCachedInstancePremiumMode} from '@app/api/limits/InstancePremiumModeCache';
|
||||
import {getInstanceConfigRepository} from '@app/api/middleware/ServiceSingletons';
|
||||
import type {User} from '@app/api/models/User';
|
||||
import {extractEmailDomain} from '@app/api/utils/EmailDomainUtils';
|
||||
import {
|
||||
@@ -15,7 +13,6 @@ import {
|
||||
SuspiciousActivityFlags,
|
||||
UserFlags,
|
||||
} from '@fluxer/constants/src/UserConstants';
|
||||
import {resolveProfileTimezoneAssignment} from '@fluxer/schema/src/domains/admin/ProfileTimezoneSchemas';
|
||||
import type {RequiredAction} from '@fluxer/schema/src/domains/user/UserResponseSchemas';
|
||||
import {ms} from 'itty-time';
|
||||
|
||||
@@ -315,9 +312,3 @@ export function isProfileSubstringExempt(user: Pick<PremiumCheckable, 'flags'>):
|
||||
export function isBugHunterBotUser(user: Pick<User, 'flags' | 'isBot'>): boolean {
|
||||
return user.isBot && (user.flags & UserFlags.BUG_HUNTER) !== 0n;
|
||||
}
|
||||
|
||||
export async function canUseProfileTimezone(user: User): Promise<boolean> {
|
||||
const config = await getInstanceConfigRepository().getProfileTimezoneConfig();
|
||||
const targeting = await resolveExperimentTargeting(user, [config]);
|
||||
return resolveProfileTimezoneAssignment(config, user.id.toString(), targeting).enabled;
|
||||
}
|
||||
|
||||
@@ -13,7 +13,6 @@ import type {IUserAccountRepository} from '@app/api/user/repositories/IUserAccou
|
||||
import type {IUserChannelRepository} from '@app/api/user/repositories/IUserChannelRepository';
|
||||
import type {IUserRelationshipRepository} from '@app/api/user/repositories/IUserRelationshipRepository';
|
||||
import type {IUserSettingsRepository} from '@app/api/user/repositories/IUserSettingsRepository';
|
||||
import {canUseProfileTimezone} from '@app/api/user/UserHelpers';
|
||||
import {ChannelTypes} from '@fluxer/constants/src/ChannelConstants';
|
||||
import {
|
||||
PremiumFlags,
|
||||
@@ -129,8 +128,7 @@ export class UserAccountLookupService {
|
||||
!restrictProfile &&
|
||||
user.timezone != null &&
|
||||
profileFieldPrivacyContext != null &&
|
||||
this.canViewProfileField(user.timezonePrivacyFlags, profileFieldPrivacyContext) &&
|
||||
(await canUseProfileTimezone(user));
|
||||
this.canViewProfileField(user.timezonePrivacyFlags, profileFieldPrivacyContext);
|
||||
const [mutualFriends, mutualGuilds, connections] = await Promise.all([
|
||||
withMutualFriends && userId !== targetId ? this.getMutualFriends(userId, targetId) : undefined,
|
||||
withMutualGuilds && userId !== targetId ? this.getMutualGuilds(userId, targetId) : undefined,
|
||||
|
||||
@@ -10,7 +10,7 @@ import {createLimitMatchContext} from '@app/api/limits/LimitMatchContextBuilder'
|
||||
import {profileSubstringBlocklistCache} from '@app/api/middleware/ProfileSubstringBlocklistCache';
|
||||
import type {User} from '@app/api/models/User';
|
||||
import type {IUserAccountRepository} from '@app/api/user/repositories/IUserAccountRepository';
|
||||
import {canUseProfileTimezone, isProfileSubstringExempt} from '@app/api/user/UserHelpers';
|
||||
import {isProfileSubstringExempt} from '@app/api/user/UserHelpers';
|
||||
import {deriveDominantAvatarColor} from '@app/api/utils/AvatarColorUtils';
|
||||
import * as EmojiUtils from '@app/api/utils/EmojiUtils';
|
||||
import {MAX_BIO_LENGTH} from '@fluxer/constants/src/LimitConstants';
|
||||
@@ -83,15 +83,13 @@ export class UserAccountProfileService {
|
||||
if (data.accent_color !== undefined) {
|
||||
await this.processAccentColorUpdate({user, accentColor: data.accent_color, updates});
|
||||
}
|
||||
const canUpdateProfileTimezone =
|
||||
(data.timezone !== undefined || data.timezone_privacy_flags !== undefined) && (await canUseProfileTimezone(user));
|
||||
if (canUpdateProfileTimezone && data.timezone !== undefined) {
|
||||
if (data.timezone !== undefined) {
|
||||
const nextTimezone = this.processTimezoneUpdate({user, timezone: data.timezone, updates});
|
||||
if (nextTimezone !== null && user.timezone === null && data.timezone_privacy_flags === undefined) {
|
||||
updates.timezone_privacy_flags = ProfileFieldPrivacyFlags.EVERYONE;
|
||||
}
|
||||
}
|
||||
if (canUpdateProfileTimezone && data.timezone_privacy_flags !== undefined) {
|
||||
if (data.timezone_privacy_flags !== undefined) {
|
||||
this.processTimezonePrivacyFlagsUpdate({
|
||||
user,
|
||||
privacyFlags: data.timezone_privacy_flags,
|
||||
|
||||
@@ -19,7 +19,6 @@ import type {UserAccountService} from '@app/api/user/services/UserAccountService
|
||||
import type {UserChannelService} from '@app/api/user/services/UserChannelService';
|
||||
import {mapUserToPartialResponseWithCache} from '@app/api/user/UserCacheHelpers';
|
||||
import {
|
||||
canUseProfileTimezone,
|
||||
createPremiumClearPatch,
|
||||
getEffectiveSuspiciousFlags,
|
||||
shouldStripExpiredPremium,
|
||||
@@ -69,20 +68,6 @@ function hasProfileCustomizationUpdate(data: UserUpdatePayload): boolean {
|
||||
return EMAIL_VERIFICATION_REQUIRED_PROFILE_UPDATE_FIELDS.some((field) => data[field] !== undefined);
|
||||
}
|
||||
|
||||
async function stripUnauthorizedProfileTimezoneUpdate(
|
||||
user: User,
|
||||
body: UserUpdateWithVerificationRequest,
|
||||
): Promise<UserUpdateWithVerificationRequest> {
|
||||
if (body.timezone === undefined && body.timezone_privacy_flags === undefined) {
|
||||
return body;
|
||||
}
|
||||
if (await canUseProfileTimezone(user)) {
|
||||
return body;
|
||||
}
|
||||
const {timezone: _timezone, timezone_privacy_flags: _timezonePrivacyFlags, ...rest} = body;
|
||||
return rest;
|
||||
}
|
||||
|
||||
function hasDefinedUserUpdatePayload(data: UserUpdatePayload): boolean {
|
||||
return Object.values(data).some((value) => value !== undefined);
|
||||
}
|
||||
@@ -158,7 +143,6 @@ export class UserAccountRequestService {
|
||||
const {ctx, body, authSession} = params;
|
||||
const {user} = params;
|
||||
const oldEmail = user.email;
|
||||
const sanitizedBody = await stripUnauthorizedProfileTimezoneUpdate(user, body);
|
||||
const {
|
||||
mfa_method: _mfaMethod,
|
||||
mfa_code: _mfaCode,
|
||||
@@ -166,13 +150,13 @@ export class UserAccountRequestService {
|
||||
webauthn_challenge: _webauthnChallenge,
|
||||
email_token: emailToken,
|
||||
...userUpdateDataRest
|
||||
} = sanitizedBody;
|
||||
} = body;
|
||||
let userUpdateData: UserUpdatePayload = userUpdateDataRest;
|
||||
const emailTokenProvided = emailToken !== undefined;
|
||||
if (!emailTokenProvided && !hasDefinedUserUpdatePayload(userUpdateData)) {
|
||||
return mapUserToPrivateResponse(user);
|
||||
}
|
||||
this.enforceSuspiciousSelfUpdateAllowance(user, sanitizedBody);
|
||||
this.enforceSuspiciousSelfUpdateAllowance(user, body);
|
||||
if (userUpdateData.email !== undefined) {
|
||||
throw InputValidationError.fromCode('email', ValidationErrorCodes.EMAIL_MUST_BE_CHANGED_VIA_TOKEN);
|
||||
}
|
||||
@@ -198,7 +182,7 @@ export class UserAccountRequestService {
|
||||
const needsVerification = this.requiresSensitiveUserVerification(user, userUpdateData, emailTokenProvided);
|
||||
let sudoResult: SudoVerificationResult | null = null;
|
||||
if (needsVerification) {
|
||||
sudoResult = await requireSudoMode(ctx, user, sanitizedBody);
|
||||
sudoResult = await requireSudoMode(ctx, user, body);
|
||||
}
|
||||
if (emailTokenProvided && emailToken) {
|
||||
emailFromToken = await this.emailChangeService.getTokenEmail(user.id, emailToken);
|
||||
|
||||
@@ -3,7 +3,6 @@
|
||||
import {createTestAccount} from '@app/api/auth/tests/AuthTestUtils';
|
||||
import {createFriendship} from '@app/api/channel/tests/ChannelTestUtils';
|
||||
import {acceptInvite, createChannelInvite, createGuild, getChannel} from '@app/api/guild/tests/GuildTestUtils';
|
||||
import {getInstanceConfigRepository} from '@app/api/middleware/ServiceSingletons';
|
||||
import {type ApiTestHarness, createApiTestHarness} from '@app/api/test/ApiTestHarness';
|
||||
import {createBuilder} from '@app/api/test/TestRequestBuilder';
|
||||
import {
|
||||
@@ -12,7 +11,6 @@ import {
|
||||
ProfilePrivacyLevels,
|
||||
} from '@fluxer/constants/src/UserConstants';
|
||||
import {getCurrentTimeZoneOffsetMinutes} from '@fluxer/date_utils/src/TimeZoneUtils';
|
||||
import {DEFAULT_PROFILE_TIMEZONE_CONFIG} from '@fluxer/schema/src/domains/admin/ProfileTimezoneSchemas';
|
||||
import type {UserPrivateResponse, UserProfileFullResponse} from '@fluxer/schema/src/domains/user/UserResponseSchemas';
|
||||
import {afterAll, beforeAll, beforeEach, describe, expect, it} from 'vitest';
|
||||
|
||||
@@ -31,14 +29,6 @@ async function updateProfileTimezone(
|
||||
return createBuilder<UserPrivateResponse>(harness, token).patch('/users/@me').body(data).execute();
|
||||
}
|
||||
|
||||
async function setProfileTimezoneUsers(userIds: Array<string>): Promise<void> {
|
||||
await getInstanceConfigRepository().setProfileTimezoneConfig({
|
||||
...DEFAULT_PROFILE_TIMEZONE_CONFIG,
|
||||
enabled: true,
|
||||
included_user_ids: userIds,
|
||||
});
|
||||
}
|
||||
|
||||
async function updateProfilePrivacy(
|
||||
harness: ApiTestHarness,
|
||||
token: string,
|
||||
@@ -76,7 +66,6 @@ describe('User Profile Timezone Visibility', () => {
|
||||
it('defaults timezone visibility to everyone when a timezone is set', async () => {
|
||||
const targetAccount = await createTestAccount(harness);
|
||||
const viewerAccount = await createTestAccount(harness);
|
||||
await setProfileTimezoneUsers([targetAccount.userId]);
|
||||
const updated = await updateProfileTimezone(harness, targetAccount.token, {timezone: TEST_TIMEZONE});
|
||||
expect(updated.timezone).toBe(TEST_TIMEZONE);
|
||||
expect(updated.timezone_privacy_flags).toBe(ProfileFieldPrivacyFlags.EVERYONE);
|
||||
@@ -86,7 +75,6 @@ describe('User Profile Timezone Visibility', () => {
|
||||
});
|
||||
it('restores default timezone visibility when a timezone is set again without explicit flags', async () => {
|
||||
const targetAccount = await createTestAccount(harness);
|
||||
await setProfileTimezoneUsers([targetAccount.userId]);
|
||||
await updateProfileTimezone(harness, targetAccount.token, {
|
||||
timezone: TEST_TIMEZONE,
|
||||
timezone_privacy_flags: 0,
|
||||
@@ -97,7 +85,6 @@ describe('User Profile Timezone Visibility', () => {
|
||||
});
|
||||
it('hides timezone from the public profile when privacy flags are unset', async () => {
|
||||
const targetAccount = await createTestAccount(harness);
|
||||
await setProfileTimezoneUsers([targetAccount.userId]);
|
||||
await updateProfileTimezone(harness, targetAccount.token, {
|
||||
timezone: TEST_TIMEZONE,
|
||||
timezone_privacy_flags: 0,
|
||||
@@ -109,7 +96,6 @@ describe('User Profile Timezone Visibility', () => {
|
||||
const targetAccount = await createTestAccount(harness);
|
||||
const friendAccount = await createTestAccount(harness);
|
||||
const guildMemberAccount = await createTestAccount(harness);
|
||||
await setProfileTimezoneUsers([targetAccount.userId]);
|
||||
await updateProfileTimezone(harness, targetAccount.token, {
|
||||
timezone: TEST_TIMEZONE,
|
||||
timezone_privacy_flags: ProfileFieldPrivacyFlags.FRIENDS,
|
||||
@@ -125,7 +111,6 @@ describe('User Profile Timezone Visibility', () => {
|
||||
const targetAccount = await createTestAccount(harness);
|
||||
const friendAccount = await createTestAccount(harness);
|
||||
const guildMemberAccount = await createTestAccount(harness);
|
||||
await setProfileTimezoneUsers([targetAccount.userId]);
|
||||
await updateProfileTimezone(harness, targetAccount.token, {
|
||||
timezone: TEST_TIMEZONE,
|
||||
timezone_privacy_flags: ProfileFieldPrivacyFlags.MUTUAL_GUILDS,
|
||||
@@ -140,7 +125,6 @@ describe('User Profile Timezone Visibility', () => {
|
||||
it('hides timezone when full profile privacy restricts the viewer', async () => {
|
||||
const targetAccount = await createTestAccount(harness);
|
||||
const guildMemberAccount = await createTestAccount(harness);
|
||||
await setProfileTimezoneUsers([targetAccount.userId]);
|
||||
await updateProfileTimezone(harness, targetAccount.token, {timezone: TEST_TIMEZONE});
|
||||
await updateProfilePrivacy(harness, targetAccount.token, ProfilePrivacyLevels.FRIENDS_ONLY);
|
||||
await createSharedGuild(harness, targetAccount.token, guildMemberAccount.token);
|
||||
@@ -148,49 +132,4 @@ describe('User Profile Timezone Visibility', () => {
|
||||
expect(profile.profile_limited).toBe(true);
|
||||
expect(profile.timezone_offset).toBeNull();
|
||||
});
|
||||
it('ignores profile timezone updates from users outside the experiment', async () => {
|
||||
const targetAccount = await createTestAccount(harness, {skipEmailVerification: true});
|
||||
const updated = await updateProfileTimezone(harness, targetAccount.token, {
|
||||
timezone: TEST_TIMEZONE,
|
||||
timezone_privacy_flags: ProfileFieldPrivacyFlags.FRIENDS,
|
||||
});
|
||||
expect(updated.timezone).toBeNull();
|
||||
expect(updated.timezone_privacy_flags).toBe(ProfileFieldPrivacyFlags.EVERYONE);
|
||||
});
|
||||
it('ignores profile timezone updates from users excluded from a full rollout', async () => {
|
||||
const targetAccount = await createTestAccount(harness, {skipEmailVerification: true});
|
||||
await getInstanceConfigRepository().setProfileTimezoneConfig({
|
||||
...DEFAULT_PROFILE_TIMEZONE_CONFIG,
|
||||
enabled: true,
|
||||
rollout_basis_points: 10000,
|
||||
excluded_user_ids: [targetAccount.userId],
|
||||
});
|
||||
const updated = await updateProfileTimezone(harness, targetAccount.token, {timezone: TEST_TIMEZONE});
|
||||
expect(updated.timezone).toBeNull();
|
||||
});
|
||||
it('lets members of an included guild set and show a timezone', async () => {
|
||||
const targetAccount = await createTestAccount(harness);
|
||||
const viewerAccount = await createTestAccount(harness);
|
||||
const guild = await createGuild(harness, targetAccount.token, 'Timezone Rollout Guild');
|
||||
await getInstanceConfigRepository().setProfileTimezoneConfig({
|
||||
...DEFAULT_PROFILE_TIMEZONE_CONFIG,
|
||||
enabled: true,
|
||||
included_guild_ids: [guild.id],
|
||||
});
|
||||
const updated = await updateProfileTimezone(harness, targetAccount.token, {timezone: TEST_TIMEZONE});
|
||||
expect(updated.timezone).toBe(TEST_TIMEZONE);
|
||||
await createFriendship(harness, targetAccount, viewerAccount);
|
||||
const profile = await getUserProfile(harness, viewerAccount.token, targetAccount.userId);
|
||||
expect(profile.timezone_offset).toBe(TEST_TIMEZONE_OFFSET);
|
||||
});
|
||||
it('hides stored profile timezone after the user leaves the experiment', async () => {
|
||||
const targetAccount = await createTestAccount(harness);
|
||||
const viewerAccount = await createTestAccount(harness);
|
||||
await setProfileTimezoneUsers([targetAccount.userId]);
|
||||
await updateProfileTimezone(harness, targetAccount.token, {timezone: TEST_TIMEZONE});
|
||||
await setProfileTimezoneUsers([]);
|
||||
await createFriendship(harness, targetAccount, viewerAccount);
|
||||
const profile = await getUserProfile(harness, viewerAccount.token, targetAccount.userId);
|
||||
expect(profile.timezone_offset).toBeNull();
|
||||
});
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user