fix(api): strip expired premium flags using read repair (#24)
This commit is contained in:
parent
b22c6733c3
commit
c4be1d424c
@ -43,6 +43,7 @@ import {
|
||||
} from '~/Constants';
|
||||
import {mapChannelToResponse, mapMessageToResponse} from '~/channel/ChannelModel';
|
||||
import type {IChannelRepository} from '~/channel/IChannelRepository';
|
||||
import type {UserRow} from '~/database/types/UserTypes';
|
||||
import {RateLimitError, UnauthorizedError, UnknownGuildError} from '~/Errors';
|
||||
import {mapFavoriteMemeToResponse} from '~/favorite_meme/FavoriteMemeModel';
|
||||
import type {IFavoriteMemeRepository} from '~/favorite_meme/IFavoriteMemeRepository';
|
||||
@ -87,6 +88,7 @@ import type {RpcRequest, RpcResponse, RpcResponseGuildData, RpcResponseSessionDa
|
||||
import type {IUserRepository} from '~/user/IUserRepository';
|
||||
import {CustomStatusValidator} from '~/user/services/CustomStatusValidator';
|
||||
import {getCachedUserPartialResponse} from '~/user/UserCacheHelpers';
|
||||
import {mapExpiredPremiumFields, shouldStripExpiredPremium} from '~/user/UserHelpers';
|
||||
import {
|
||||
mapRelationshipToResponse,
|
||||
mapUserGuildSettingsToResponse,
|
||||
@ -567,8 +569,24 @@ export class RpcService {
|
||||
|
||||
const hadPremium = user.premiumType != null && user.premiumType > 0;
|
||||
const isPremium = user.isPremium();
|
||||
const needsPremiumStrip = shouldStripExpiredPremium(user);
|
||||
const hasBeenSanitized = !!(user.flags & UserFlags.PREMIUM_PERKS_SANITIZED);
|
||||
|
||||
if (needsPremiumStrip) {
|
||||
try {
|
||||
const strippedUser = await this.userRepository.patchUpsert(
|
||||
user.id,
|
||||
mapExpiredPremiumFields(() => null) as Partial<UserRow>,
|
||||
);
|
||||
if (strippedUser) {
|
||||
user = strippedUser;
|
||||
userData.user = strippedUser;
|
||||
}
|
||||
} catch (error) {
|
||||
Logger.warn({userId: user.id.toString(), error}, 'Failed to strip expired premium on RPC session start');
|
||||
}
|
||||
}
|
||||
|
||||
if (hadPremium && !isPremium && !hasBeenSanitized) {
|
||||
if (user.flags & UserFlags.PREMIUM_DISCRIMINATOR) {
|
||||
try {
|
||||
|
||||
@ -55,3 +55,29 @@ export function checkIsPremium(user: PremiumCheckable): boolean {
|
||||
|
||||
return nowMs <= untilMs + GRACE_MS;
|
||||
}
|
||||
|
||||
export const PREMIUM_CLEAR_FIELDS = [
|
||||
'premium_type',
|
||||
'premium_since',
|
||||
'premium_until',
|
||||
'premium_will_cancel',
|
||||
'premium_billing_cycle',
|
||||
] as const;
|
||||
|
||||
export type PremiumClearField = (typeof PREMIUM_CLEAR_FIELDS)[number];
|
||||
|
||||
export function shouldStripExpiredPremium(user: PremiumCheckable): boolean {
|
||||
if ((user.premiumType ?? 0) <= 0) {
|
||||
return false;
|
||||
}
|
||||
|
||||
return !checkIsPremium(user);
|
||||
}
|
||||
|
||||
export function mapExpiredPremiumFields<T>(mapper: (field: PremiumClearField) => T): Record<PremiumClearField, T> {
|
||||
const result = {} as Record<PremiumClearField, T>;
|
||||
for (const field of PREMIUM_CLEAR_FIELDS) {
|
||||
result[field] = mapper(field);
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
@ -20,8 +20,10 @@
|
||||
import {createUserID, type UserID} from '~/BrandedTypes';
|
||||
import {buildPatchFromData, Db, executeVersionedUpdate, fetchMany, fetchOne} from '~/database/Cassandra';
|
||||
import {EMPTY_USER_ROW, USER_COLUMNS, type UserRow} from '~/database/CassandraTypes';
|
||||
import {Logger} from '~/Logger';
|
||||
import {User} from '~/Models';
|
||||
import {Users} from '~/Tables';
|
||||
import {shouldStripExpiredPremium} from '~/user/UserHelpers';
|
||||
|
||||
const FETCH_USERS_BY_IDS_CQL = Users.selectCql({
|
||||
where: Users.where.in('user_id', 'user_ids'),
|
||||
@ -68,8 +70,26 @@ export class UserDataRepository {
|
||||
});
|
||||
}
|
||||
|
||||
const user = await fetchOne<UserRow>(FETCH_USER_BY_ID_CQL, {user_id: userId});
|
||||
return user ? new User(user) : null;
|
||||
const userRow = await fetchOne<UserRow>(FETCH_USER_BY_ID_CQL, {user_id: userId});
|
||||
if (!userRow) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const user = new User(userRow);
|
||||
|
||||
if (shouldStripExpiredPremium(user)) {
|
||||
try {
|
||||
await this.readRepairExpiredPremium(user);
|
||||
const repairedRow = await fetchOne<UserRow>(FETCH_USER_BY_ID_CQL, {
|
||||
user_id: userId,
|
||||
});
|
||||
return repairedRow ? new User(repairedRow) : null;
|
||||
} catch (error) {
|
||||
Logger.warn({userId: user.id.toString(), error}, 'Failed to repair expired premium fields while reading user');
|
||||
}
|
||||
}
|
||||
|
||||
return user;
|
||||
}
|
||||
|
||||
async findUniqueAssert(userId: UserID): Promise<User> {
|
||||
@ -148,4 +168,18 @@ export class UserDataRepository {
|
||||
|
||||
await this.patchUser(userId, patch);
|
||||
}
|
||||
|
||||
private async readRepairExpiredPremium(user: User): Promise<void> {
|
||||
await this.patchUser(user.id, createPremiumClearPatch());
|
||||
}
|
||||
}
|
||||
|
||||
function createPremiumClearPatch(): UserPatch {
|
||||
return {
|
||||
premium_type: Db.clear<number | null>(),
|
||||
premium_since: Db.clear<Date | null>(),
|
||||
premium_until: Db.clear<Date | null>(),
|
||||
premium_will_cancel: Db.clear<boolean | null>(),
|
||||
premium_billing_cycle: Db.clear<string | null>(),
|
||||
};
|
||||
}
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user