Skip to content

Commit

Permalink
Add Delete User option
Browse files Browse the repository at this point in the history
  • Loading branch information
themrrobert committed May 12, 2024
1 parent 8329ced commit f530e12
Show file tree
Hide file tree
Showing 4 changed files with 220 additions and 8 deletions.
12 changes: 12 additions & 0 deletions prisma/schema.prisma
Original file line number Diff line number Diff line change
Expand Up @@ -1145,3 +1145,15 @@ model UserEvent {
@@map("user_event")
}

enum MigrationType {
Migration
Deletion
}

model MigratedUsers {
id Int @id @unique @default(autoincrement())
source_id String @db.Text
dest_id String @db.Text
type MigrationType
}
137 changes: 137 additions & 0 deletions src/lib/util/deleteUser.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,137 @@
import { randomSnowflake } from '@oldschoolgg/toolkit';
import { UserError } from '@oldschoolgg/toolkit/dist/lib/UserError';

import { cancelUsersListings } from '../../mahoji/lib/abstracted_commands/cancelGEListingCommand';
import { prisma } from '../settings/prisma';
import { logError } from './logError';

export async function deleteUser(_source: string | MUser, options?: { robochimp?: boolean }) {
const sourceUser = typeof _source === 'string' ? await mUserFetch(_source) : _source;

try {
await deleteBotUser(sourceUser);
if (options?.robochimp) await deleteRobochimpUser(sourceUser);
} catch (err: any) {
throw err;
} finally {
// This regenerates a default users table row for the now-clean sourceUser
await mUserFetch(sourceUser.id);
}
return true;
}
export async function deleteBotUser(userToDelete: MUser) {
// First check for + cancel active GE Listings:
await cancelUsersListings(userToDelete);

const dummyUser = await mUserFetch(randomSnowflake());

let transactions = [];
transactions.push(prisma.$executeRaw`SET CONSTRAINTS ALL DEFERRED`);

// Delete Queries
// Slayer task must come before new_user since it's linked to new_users 500 IQ.
transactions.push(prisma.slayerTask.deleteMany({ where: { user_id: userToDelete.id } }));

transactions.push(prisma.newUser.deleteMany({ where: { id: userToDelete.id } }));

transactions.push(prisma.gearPreset.deleteMany({ where: { user_id: userToDelete.id } }));
transactions.push(prisma.botItemSell.deleteMany({ where: { user_id: userToDelete.id } }));
transactions.push(prisma.historicalData.deleteMany({ where: { user_id: userToDelete.id } }));
transactions.push(prisma.farmedCrop.deleteMany({ where: { user_id: userToDelete.id } }));
transactions.push(prisma.minigame.deleteMany({ where: { user_id: userToDelete.id } }));
transactions.push(prisma.playerOwnedHouse.deleteMany({ where: { user_id: userToDelete.id } }));
transactions.push(prisma.pinnedTrip.deleteMany({ where: { user_id: userToDelete.id } }));
transactions.push(prisma.reclaimableItem.deleteMany({ where: { user_id: userToDelete.id } }));

transactions.push(prisma.activity.deleteMany({ where: { user_id: BigInt(userToDelete.id) } }));
transactions.push(prisma.xPGain.deleteMany({ where: { user_id: BigInt(userToDelete.id) } }));
transactions.push(prisma.lastManStandingGame.deleteMany({ where: { user_id: BigInt(userToDelete.id) } }));
transactions.push(prisma.userStats.deleteMany({ where: { user_id: BigInt(userToDelete.id) } }));
transactions.push(prisma.lootTrack.deleteMany({ where: { user_id: BigInt(userToDelete.id) } }));
transactions.push(prisma.buyCommandTransaction.deleteMany({ where: { user_id: BigInt(userToDelete.id) } }));
transactions.push(prisma.stashUnit.deleteMany({ where: { user_id: BigInt(userToDelete.id) } }));
transactions.push(prisma.bingoParticipant.deleteMany({ where: { user_id: userToDelete.id } }));

transactions.push(
prisma.bingo.updateMany({ where: { creator_id: userToDelete.id }, data: { creator_id: dummyUser.id } })
);

// Without this, the user_id will be set to null when the Key'd users row is deleted:
transactions.push(
prisma.gEListing.updateMany({ where: { user_id: userToDelete.id }, data: { user_id: dummyUser.id } })
);

// Delete destUser.id user:
transactions.push(prisma.user.deleteMany({ where: { id: userToDelete.id } }));

// Preserve giveaway history
transactions.push(
prisma.giveaway.updateMany({ where: { user_id: userToDelete.id }, data: { user_id: dummyUser.id } })
);

// CommandUsage/EconomyTx aren't wiped on the destUser.id first, so we can preserve that data:
transactions.push(
prisma.commandUsage.updateMany({
where: { user_id: BigInt(userToDelete.id) },
data: { user_id: BigInt(dummyUser.id) }
})
);
transactions.push(
prisma.economyTransaction.updateMany({
where: { sender: BigInt(userToDelete.id) },
data: { sender: BigInt(dummyUser.id) }
})
);
transactions.push(
prisma.economyTransaction.updateMany({
where: { recipient: BigInt(userToDelete.id) },
data: { recipient: BigInt(dummyUser.id) }
})
);
// GE Listing isn't wiped for destUser.id as that could mess up the GE
transactions.push(
prisma.gEListing.updateMany({ where: { user_id: userToDelete.id }, data: { user_id: dummyUser.id } })
);

// Update Users in group activities:
const updateUsers = `UPDATE activity
SET data = data::jsonb
|| CONCAT('{"users":', REPLACE(data->>'users', '${userToDelete.id}', '${dummyUser.id}'),'}')::jsonb
|| CONCAT('{"leader":"', REPLACE(data->>'leader', '${userToDelete.id}', '${dummyUser.id}'), '"}')::jsonb
WHERE (data->'users')::jsonb ? '${userToDelete.id}'`;
transactions.push(prisma.$queryRawUnsafe(updateUsers));

// Update `detailedUsers` in ToA
const updateToAUsers = `UPDATE activity SET data = data::jsonb || CONCAT('{"detailedUsers":', REPLACE(data->>'detailedUsers', '${userToDelete.id}', '${dummyUser.id}'),'}')::jsonb WHERE type = 'TombsOfAmascut' AND data->>'detailedUsers' LIKE '%${userToDelete.id}%'`;
transactions.push(prisma.$queryRawUnsafe(updateToAUsers));

transactions.push(
prisma.migratedUsers.create({
data: {
source_id: userToDelete.id,
dest_id: dummyUser.id,
type: 'Deletion'
}
})
);

try {
await prisma.$transaction(transactions);
} catch (err: any) {
logError(err);
throw new UserError('Error deleting user. Sorry about that!');
}
}

export async function deleteRobochimpUser(sourceUser: MUser) {
const robochimpTx = [];
robochimpTx.push(roboChimpClient.user.deleteMany({ where: { id: BigInt(sourceUser.id) } }));

try {
await roboChimpClient.$transaction(robochimpTx);
} catch (err: any) {
err.message += ' - User already migrated! Robochimp deletion failed!';
logError(err);
throw new UserError('Robochimp deletion failed, but minion data deleted already!');
}
}
32 changes: 26 additions & 6 deletions src/lib/util/migrateUser.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,10 +4,23 @@ import { cancelUsersListings } from '../../mahoji/lib/abstracted_commands/cancel
import { prisma } from '../settings/prisma';
import { logError } from './logError';

export async function migrateUser(_source: string | MUser, _dest: string | MUser): Promise<string | true> {
export async function migrateUser(_source: string | MUser, _dest: string | MUser, options?: { noRobochimp?: boolean }) {
const sourceUser = typeof _source === 'string' ? await mUserFetch(_source) : _source;
const destUser = typeof _dest === 'string' ? await mUserFetch(_dest) : _dest;

try {
await migrateBotUser(sourceUser, destUser);
if (!options?.noRobochimp) await migrateRobochimpUser(sourceUser, destUser);
} catch (err: any) {
throw err;
} finally {
// This regenerates a default users table row for the now-clean sourceUser
await mUserFetch(sourceUser.id);
}
return true;
}

export async function migrateBotUser(sourceUser: MUser, destUser: MUser) {
// First check for + cancel active GE Listings:
await Promise.all([cancelUsersListings(sourceUser), cancelUsersListings(destUser)]);

Expand Down Expand Up @@ -170,13 +183,25 @@ export async function migrateUser(_source: string | MUser, _dest: string | MUser
const updateToAUsers = `UPDATE activity SET data = data::jsonb || CONCAT('{"detailedUsers":', REPLACE(data->>'detailedUsers', '${sourceUser.id}', '${destUser.id}'),'}')::jsonb WHERE type = 'TombsOfAmascut' AND data->>'detailedUsers' LIKE '%${sourceUser.id}%'`;
transactions.push(prisma.$queryRawUnsafe(updateToAUsers));

transactions.push(
prisma.migratedUsers.create({
data: {
source_id: sourceUser.id,
dest_id: destUser.id,
type: 'Migration'
}
})
);

try {
await prisma.$transaction(transactions);
} catch (err: any) {
logError(err);
throw new UserError('Error migrating user. Sorry about that!');
}
}

export async function migrateRobochimpUser(sourceUser: MUser, destUser: MUser) {
const roboChimpTarget = await roboChimpClient.user.findFirst({
select: { migrated_user_id: true },
where: { id: BigInt(destUser.id) }
Expand All @@ -203,9 +228,4 @@ export async function migrateUser(_source: string | MUser, _dest: string | MUser
throw new UserError('Robochimp migration failed, but minion data migrated already!');
}
}

// This regenerates a default users table row for the now-clean sourceUser
await mUserFetch(sourceUser.id);

return true;
}
47 changes: 45 additions & 2 deletions src/mahoji/commands/rp.ts
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,7 @@ import { prisma } from '../../lib/settings/prisma';
import { TeamLoot } from '../../lib/simulation/TeamLoot';
import { ItemBank } from '../../lib/types';
import { dateFm, isValidDiscordSnowflake, returnStringOrFile } from '../../lib/util';
import { deleteUser } from '../../lib/util/deleteUser';
import getOSItem from '../../lib/util/getOSItem';
import { handleMahojiConfirmation } from '../../lib/util/handleMahojiConfirmation';
import { deferInteraction } from '../../lib/util/interactionReply';
Expand Down Expand Up @@ -289,6 +290,24 @@ export const rpCommand: OSBMahojiCommand = {
}
]
},
{
type: ApplicationCommandOptionType.Subcommand,
name: 'delete_user',
description: 'Delete a user entirely',
options: [
{
type: ApplicationCommandOptionType.User,
name: 'user_to_delete',
description: 'Account to erase entirely!',
required: true
},
{
type: ApplicationCommandOptionType.String,
name: 'reason',
description: 'The reason'
}
]
},
{
type: ApplicationCommandOptionType.Subcommand,
name: 'list_trades',
Expand Down Expand Up @@ -478,6 +497,7 @@ export const rpCommand: OSBMahojiCommand = {
add_ironman_alt?: { main: MahojiUserOption; ironman_alt: MahojiUserOption };
view_user?: { user: MahojiUserOption };
migrate_user?: { source: MahojiUserOption; dest: MahojiUserOption; reason?: string };
delete_user?: { user_to_delete: MahojiUserOption; reason?: string };
list_trades?: {
user: MahojiUserOption;
partner?: MahojiUserOption;
Expand Down Expand Up @@ -805,6 +825,30 @@ ORDER BY item_id ASC;`);
return (await getUserInfo(userToView)).everythingString;
}

if (options.player?.delete_user) {
if (!isOwner && !isAdmin) {
return randArrItem(gifs);
}
const { user_to_delete: target, reason } = options.player.delete_user;
const userToDelete = await mUserFetch(target.user.id);
if (isProtectedAccount(userToDelete)) return 'You cannot clobber that account.';

await handleMahojiConfirmation(
interaction,
`Are you 1000%, totally, **REALLY** sure that \`${userToDelete.logName}\` is the account you want to PERMANENTLY DELETE??`
);

const result = await deleteUser(userToDelete);

if (result) {
await sendToChannelID(Channel.BotLogs, {
content: `${adminUser.logName} DELETED ${userToDelete.logName}${
reason ? `, because ${reason}` : ''
}`
});
return 'Done';
}
}
if (options.player?.migrate_user) {
if (!isOwner && !isAdmin) {
return randArrItem(gifs);
Expand Down Expand Up @@ -833,15 +877,14 @@ ORDER BY item_id ASC;`);
`Are you 1000%, totally, **REALLY** sure that \`${sourceUser.logName}\` is the account you want to preserve, and \`${destUser.logName}\` is the new account that will have ALL existing data destroyed?`
);
const result = await migrateUser(sourceUser, destUser);
if (result === true) {
if (result) {
await sendToChannelID(Channel.BotLogs, {
content: `${adminUser.logName} migrated ${sourceUser.logName} to ${destUser.logName}${
reason ? `, for ${reason}` : ''
}`
});
return 'Done';
}
return result;
}
if (options.player?.list_trades) {
const baseSql =
Expand Down

0 comments on commit f530e12

Please sign in to comment.