monitor/core/modules/Database/dao/actions.ts
2025-04-16 22:30:27 +07:00

241 lines
8.5 KiB
TypeScript

import { cloneDeep } from 'lodash-es';
import { DbInstance, SavePriority } from "../instance";
import { DatabaseActionBanType, DatabaseActionType, DatabaseActionWarnType } from "../databaseTypes";
import { genActionID } from "../dbUtils";
import { now } from '@lib/misc';
import consoleFactory from '@lib/console';
const console = consoleFactory('DatabaseDao');
/**
* Data access object for the database "actions" collection.
*/
export default class ActionsDao {
constructor(private readonly db: DbInstance) { }
private get dbo() {
if (!this.db.obj || !this.db.isReady) throw new Error(`database not ready yet`);
return this.db.obj;
}
private get chain() {
if (!this.db.obj || !this.db.isReady) throw new Error(`database not ready yet`);
return this.db.obj.chain;
}
/**
* Searches for an action in the database by the id, returns action or null if not found
*/
findOne(actionId: string): DatabaseActionType | null {
if (typeof actionId !== 'string' || !actionId.length) throw new Error('Invalid actionId.');
//Performing search
const a = this.chain.get('actions')
.find({ id: actionId })
.cloneDeep()
.value();
return (typeof a === 'undefined') ? null : a;
}
/**
* Searches for any registered action in the database by a list of identifiers and optional filters
* Usage example: findMany(['license:xxx'], undefined, {type: 'ban', revocation.timestamp: null})
*/
findMany<T extends DatabaseActionType>(
idsArray: string[],
hwidsArray?: string[],
customFilter: ((action: DatabaseActionType) => action is T) | object = {}
): T[] {
if (!Array.isArray(idsArray)) throw new Error('idsArray should be an array');
if (hwidsArray && !Array.isArray(hwidsArray)) throw new Error('hwidsArray should be an array or undefined');
const idsFilter = (action: DatabaseActionType) => idsArray.some((fi) => action.ids.includes(fi))
const hwidsFilter = (action: DatabaseActionType) => {
if ('hwids' in action && action.hwids) {
const count = hwidsArray!.filter((fi) => action.hwids?.includes(fi)).length;
return count >= txConfig.banlist.requiredHwidMatches;
} else {
return false;
}
}
try {
//small optimization
const idsMatchFilter = hwidsArray && hwidsArray.length && txConfig.banlist.requiredHwidMatches
? (a: DatabaseActionType) => idsFilter(a) || hwidsFilter(a)
: (a: DatabaseActionType) => idsFilter(a)
return this.chain.get('actions')
.filter(customFilter as (a: DatabaseActionType) => a is T)
.filter(idsMatchFilter)
.cloneDeep()
.value();
} catch (error) {
const msg = `Failed to search for a registered action database with error: ${(error as Error).message}`;
console.verbose.error(msg);
throw new Error(msg);
}
}
/**
* Registers a ban action and returns its id
*/
registerBan(
ids: string[],
author: string,
reason: string,
expiration: number | false,
playerName: string | false = false,
hwids?: string[], //only used for bans
): string {
//Sanity check
if (!Array.isArray(ids) || !ids.length) throw new Error('Invalid ids array.');
if (typeof author !== 'string' || !author.length) throw new Error('Invalid author.');
if (typeof reason !== 'string' || !reason.length) throw new Error('Invalid reason.');
if (expiration !== false && (typeof expiration !== 'number')) throw new Error('Invalid expiration.');
if (playerName !== false && (typeof playerName !== 'string' || !playerName.length)) throw new Error('Invalid playerName.');
if (hwids && !Array.isArray(hwids)) throw new Error('Invalid hwids array.');
//Saves it to the database
const timestamp = now();
try {
const actionID = genActionID(this.dbo, 'ban');
const toDB: DatabaseActionBanType = {
id: actionID,
type: 'ban',
ids,
hwids,
playerName,
reason,
author,
timestamp,
expiration,
revocation: {
timestamp: null,
author: null,
},
};
this.chain.get('actions')
.push(toDB)
.value();
this.db.writeFlag(SavePriority.HIGH);
return actionID;
} catch (error) {
let msg = `Failed to register ban to database with message: ${(error as Error).message}`;
console.error(msg);
console.verbose.dir(error);
throw error;
}
}
/**
* Registers a warn action and returns its id
*/
registerWarn(
ids: string[],
author: string,
reason: string,
playerName: string | false = false,
): string {
//Sanity check
if (!Array.isArray(ids) || !ids.length) throw new Error('Invalid ids array.');
if (typeof author !== 'string' || !author.length) throw new Error('Invalid author.');
if (typeof reason !== 'string' || !reason.length) throw new Error('Invalid reason.');
if (playerName !== false && (typeof playerName !== 'string' || !playerName.length)) throw new Error('Invalid playerName.');
//Saves it to the database
const timestamp = now();
try {
const actionID = genActionID(this.dbo, 'warn');
const toDB: DatabaseActionWarnType = {
id: actionID,
type: 'warn',
ids,
playerName,
reason,
author,
timestamp,
expiration: false,
acked: false,
revocation: {
timestamp: null,
author: null,
},
};
this.chain.get('actions')
.push(toDB)
.value();
this.db.writeFlag(SavePriority.HIGH);
return actionID;
} catch (error) {
let msg = `Failed to register warn to database with message: ${(error as Error).message}`;
console.error(msg);
console.verbose.dir(error);
throw error;
}
}
/**
* Marks a warning as acknowledged
*/
ackWarn(actionId: string) {
if (typeof actionId !== 'string' || !actionId.length) throw new Error('Invalid actionId.');
try {
const action = this.chain.get('actions')
.find({ id: actionId })
.value();
if (!action) throw new Error(`action not found`);
if (action.type !== 'warn') throw new Error(`action is not a warn`);
action.acked = true;
this.db.writeFlag(SavePriority.MEDIUM);
} catch (error) {
const msg = `Failed to ack warn with message: ${(error as Error).message}`;
console.error(msg);
console.verbose.dir(error);
throw error;
}
}
/**
* Revoke an action (ban, warn)
*/
revoke(
actionId: string,
author: string,
allowedTypes: string[] | true = true
): DatabaseActionType {
if (typeof actionId !== 'string' || !actionId.length) throw new Error('Invalid actionId.');
if (typeof author !== 'string' || !author.length) throw new Error('Invalid author.');
if (allowedTypes !== true && !Array.isArray(allowedTypes)) throw new Error('Invalid allowedTypes.');
try {
const action = this.chain.get('actions')
.find({ id: actionId })
.value();
if (!action) throw new Error(`action not found`);
if (allowedTypes !== true && !allowedTypes.includes(action.type)) {
throw new Error(`you do not have permission to revoke this action`);
}
action.revocation = {
timestamp: now(),
author,
};
this.db.writeFlag(SavePriority.HIGH);
return cloneDeep(action);
} catch (error) {
const msg = `Failed to revoke action with message: ${(error as Error).message}`;
console.error(msg);
console.verbose.dir(error);
throw error;
}
}
}