Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
- Loading branch information
Showing
18 changed files
with
1,060 additions
and
21 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,152 @@ | ||
const modulename = 'WebServer:HistorySearch'; | ||
import { DatabaseActionType } from '@core/components/PlayerDatabase/databaseTypes'; | ||
import consoleFactory from '@extras/console'; | ||
import { AuthedCtx } from '@core/components/WebServer/ctxTypes'; | ||
import cleanPlayerName from '@shared/cleanPlayerName'; | ||
import { chain as createChain } from 'lodash-es'; | ||
import Fuse from 'fuse.js'; | ||
import { now, parseLaxIdsArrayInput } from '@extras/helpers'; | ||
import { HistoryTableActionType, HistoryTableSearchResp } from '@shared/historyApiTypes'; | ||
const console = consoleFactory(modulename); | ||
|
||
//Helpers | ||
const DEFAULT_LIMIT = 100; //cant override it for now | ||
const ALLOWED_SORTINGS = ['timestamp']; | ||
|
||
|
||
/** | ||
* Returns the players stats for the Players page table | ||
*/ | ||
export default async function HistorySearch(ctx: AuthedCtx) { | ||
//Sanity check | ||
if (typeof ctx.query === 'undefined') { | ||
return ctx.utils.error(400, 'Invalid Request'); | ||
} | ||
const { | ||
searchValue, | ||
searchType, | ||
filterbyType, | ||
filterbyAdmin, | ||
sortingKey, | ||
sortingDesc, | ||
offsetParam, | ||
offsetActionId | ||
} = ctx.query; | ||
const sendTypedResp = (data: HistoryTableSearchResp) => ctx.send(data); | ||
const dbo = ctx.txAdmin.playerDatabase.getDb(); | ||
let chain = dbo.chain.get('actions'); | ||
|
||
//sort the actions by the sortingKey/sortingDesc | ||
const parsedSortingDesc = sortingDesc === 'true'; | ||
if (typeof sortingKey !== 'string' || !ALLOWED_SORTINGS.includes(sortingKey)) { | ||
return sendTypedResp({ error: 'Invalid sorting key' }); | ||
} | ||
chain = chain.sort((a, b) => { | ||
// @ts-ignore | ||
return parsedSortingDesc ? b[sortingKey] - a[sortingKey] : a[sortingKey] - b[sortingKey]; | ||
}); | ||
|
||
//offset the actions by the offsetParam/offsetActionId | ||
if (offsetParam !== undefined && offsetActionId !== undefined) { | ||
const parsedOffsetParam = parseInt(offsetParam as string); | ||
if (isNaN(parsedOffsetParam) || typeof offsetActionId !== 'string' || !offsetActionId.length) { | ||
return sendTypedResp({ error: 'Invalid offsetParam or offsetActionId' }); | ||
} | ||
chain = chain.takeRightWhile((a) => { | ||
return a.id !== offsetActionId && parsedSortingDesc | ||
? a[sortingKey as keyof DatabaseActionType] as number <= parsedOffsetParam | ||
: a[sortingKey as keyof DatabaseActionType] as number >= parsedOffsetParam | ||
}); | ||
} | ||
|
||
//filter the actions by the simple filters (lightweight) | ||
const effectiveTypeFilter = typeof filterbyType === 'string' && filterbyType.length ? filterbyType : undefined; | ||
const effectiveAdminFilter = typeof filterbyAdmin === 'string' && filterbyAdmin.length ? filterbyAdmin : undefined; | ||
console.dir({ effectiveTypeFilter, effectiveAdminFilter }); | ||
if (effectiveTypeFilter || effectiveAdminFilter) { | ||
chain = chain.filter((a) => { | ||
if (effectiveTypeFilter && a.type !== effectiveTypeFilter) { | ||
return false; | ||
} | ||
if (effectiveAdminFilter && a.author !== effectiveAdminFilter) { | ||
return false; | ||
} | ||
return true; | ||
}); | ||
} | ||
|
||
// filter the actions by the searchValue/searchType (VERY HEAVY!) | ||
if (typeof searchType === 'string') { | ||
if (typeof searchValue !== 'string' || !searchValue.length) { | ||
return sendTypedResp({ error: 'Invalid searchValue' }); | ||
} | ||
|
||
if (searchType === 'actionId') { | ||
//Searching by action ID | ||
const cleanId = searchValue.toUpperCase().trim(); | ||
if (!cleanId.length) { | ||
return sendTypedResp({ error: 'This action ID is unsearchable (empty?).' }); | ||
} | ||
const actions = chain.value(); | ||
const fuse = new Fuse(actions, { | ||
isCaseSensitive: true, //maybe that's an optimization?! | ||
keys: ['id'], | ||
threshold: 0.3 | ||
}); | ||
const filtered = fuse.search(cleanId).map(x => x.item); | ||
chain = createChain(filtered); | ||
} else if (searchType === 'reason') { | ||
//Searching by player notes | ||
const actions = chain.value(); | ||
const fuse = new Fuse(actions, { | ||
keys: ['reason'], | ||
threshold: 0.3 | ||
}); | ||
const filtered = fuse.search(searchValue).map(x => x.item); | ||
chain = createChain(filtered); | ||
} else if (searchType === 'identifiers') { | ||
//Searching by target identifiers | ||
const { validIds, validHwids, invalids } = parseLaxIdsArrayInput(searchValue); | ||
if (invalids.length) { | ||
return sendTypedResp({ error: `Invalid identifiers (${invalids.join(',')}). Prefix any identifier with their type, like 'fivem:123456' instead of just '123456'.` }); | ||
} | ||
if (!validIds.length && !validHwids.length) { | ||
return sendTypedResp({ error: `No valid identifiers found.` }); | ||
} | ||
chain = chain.filter((a) => { | ||
if (validIds.length && !validIds.some((id) => a.ids.includes(id))) { | ||
return false; | ||
} | ||
if (validHwids.length && a.hwids !== undefined && !validHwids.some((hwid) => a.hwids!.includes(hwid))) { | ||
return false; | ||
} | ||
return true; | ||
}); | ||
} else { | ||
return sendTypedResp({ error: 'Unknown searchType' }); | ||
} | ||
} | ||
|
||
//filter players by the limit - taking 1 more to check if we reached the end | ||
chain = chain.take(DEFAULT_LIMIT + 1); | ||
const actions = chain.value(); | ||
const hasReachedEnd = actions.length <= DEFAULT_LIMIT; | ||
const currTs = now(); | ||
const processedActions: HistoryTableActionType[] = actions.slice(0, DEFAULT_LIMIT).map((a) => { | ||
return { | ||
id: a.id, | ||
type: a.type, | ||
playerName: a.playerName, | ||
author: a.author, | ||
reason: a.reason, | ||
timestamp: a.timestamp, | ||
isExpired: typeof a.expiration === 'number' && a.expiration < currTs, | ||
isRevoked: !!a.revocation.timestamp, | ||
}; | ||
}); | ||
|
||
return sendTypedResp({ | ||
history: processedActions, | ||
hasReachedEnd, | ||
}); | ||
}; |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,34 @@ | ||
const modulename = 'WebServer:HistoryStats'; | ||
import consoleFactory from '@extras/console'; | ||
import { AuthedCtx } from '@core/components/WebServer/ctxTypes'; | ||
import { HistoryStatsResp } from '@shared/historyApiTypes'; | ||
import { union } from 'lodash-es'; | ||
const console = consoleFactory(modulename); | ||
|
||
|
||
/** | ||
* Returns the players stats for the Players page callouts | ||
*/ | ||
export default async function HistoryStats(ctx: AuthedCtx) { | ||
const sendTypedResp = (data: HistoryStatsResp) => ctx.send(data); | ||
try { | ||
const dbStats = ctx.txAdmin.playerDatabase.getActionStats(); | ||
const dbAdmins = Object.keys(dbStats.groupedByAdmins); | ||
// @ts-ignore i don't wanna type this | ||
const vaultAdmins = ctx.txAdmin.adminVault.getAdminsList().map(a => a.name); | ||
const adminStats = union(dbAdmins, vaultAdmins) | ||
.sort((a, b) => a.localeCompare(b)) | ||
.map(admin => ({ | ||
name: admin, | ||
actions: dbStats.groupedByAdmins[admin] ?? 0 | ||
})); | ||
return sendTypedResp({ | ||
...dbStats, | ||
groupedByAdmins: adminStats, | ||
}); | ||
} catch (error) { | ||
const msg = `getStats failed with error: ${(error as Error).message}`; | ||
console.verbose.error(msg); | ||
return sendTypedResp({ error: msg }); | ||
} | ||
}; |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Oops, something went wrong.