mirror of
https://github.com/Terncode/pixel.horse.git
synced 2026-09-24 21:55:52 +02:00
435 lines
13 KiB
TypeScript
435 lines
13 KiB
TypeScript
import * as moment from 'moment';
|
|
import { escapeRegExp, startsWith, range, uniq, compact } from 'lodash';
|
|
import { fromNow, toInt, hasFlag, compareDates, removeItem, includes } from './utils';
|
|
import { DAY } from './constants';
|
|
import {
|
|
Account, OriginInfo, Document, OriginRef, SupporterFlags, BannedMuted, AccountBase,
|
|
DuplicateResult, DuplicateBase, Duplicate, Auth, MergeInfo
|
|
} from './adminInterfaces';
|
|
import { hasRole } from './accountUtils';
|
|
import { filterBadWordsPartial } from './swears';
|
|
|
|
interface UpdatedAt {
|
|
updatedAt: Date;
|
|
}
|
|
|
|
export const compareUpdatedAt = (a: UpdatedAt, b: UpdatedAt) => compareDates(a.updatedAt, b.updatedAt);
|
|
export const compareOrigins = (a: OriginInfo, b: OriginInfo) => a.ip.localeCompare(b.ip);
|
|
export const compareOriginRefs = (a: OriginRef, b: OriginRef) =>
|
|
compareDates(b.last, a.last) || compareOrigins(a.origin, b.origin);
|
|
export const compareByName = <T extends { name: string; }>(a: T, b: T) => (a.name || '').localeCompare(b.name || '');
|
|
export const getId = (item: Document) => item._id;
|
|
export const tagBad = (s: string) => `<span class='bad'>${s}</span>`;
|
|
|
|
export function compareAccounts(a: Account, b: Account) {
|
|
return compareDates(a.createdAt, b.createdAt);
|
|
}
|
|
|
|
export function compareAuths(a: Auth, b: Auth) {
|
|
const aDeleted = a.disabled || a.banned || false;
|
|
const bDeleted = b.disabled || b.banned || false;
|
|
|
|
if (aDeleted && !bDeleted) {
|
|
return 1;
|
|
} else if (!aDeleted && bDeleted) {
|
|
return -1;
|
|
} else {
|
|
return compareByName(a, b);
|
|
}
|
|
}
|
|
|
|
export function highlightWords(text?: string) {
|
|
text = text || '';
|
|
text = filterBadWordsPartial(text, tagBad);
|
|
return text;
|
|
}
|
|
|
|
export function getAge(birthdate: Date) {
|
|
return moment().diff(birthdate, 'years');
|
|
}
|
|
|
|
// chat & events
|
|
|
|
export interface ChatDate {
|
|
value: string;
|
|
label: string;
|
|
}
|
|
|
|
export function createChatDate(date: moment.Moment): ChatDate {
|
|
return {
|
|
value: date.toISOString(),
|
|
label: date.format('MMMM Do YYYY'),
|
|
};
|
|
}
|
|
|
|
export function createDateRange(startDate: string | Date, days: number): ChatDate[] {
|
|
return range(days, 0)
|
|
.map(d => moment(startDate).subtract(d, 'days'))
|
|
.map(createChatDate);
|
|
}
|
|
|
|
// filtering
|
|
|
|
export function filterAccounts(items: Account[], search: string, showOnly: string, not: boolean) {
|
|
if (search) {
|
|
items = items.filter(createFilter(search));
|
|
}
|
|
|
|
const filter = createFilter2(showOnly);
|
|
|
|
if (filter) {
|
|
if (not) {
|
|
items = items.filter(i => !filter(i));
|
|
} else {
|
|
items = items.filter(filter);
|
|
}
|
|
}
|
|
|
|
return items;
|
|
}
|
|
|
|
export function createFilter(search: string): (account: Account) => boolean {
|
|
const regex = new RegExp(escapeRegExp(search), 'i');
|
|
|
|
function test(value: string): boolean {
|
|
return !!value && regex.test(value);
|
|
}
|
|
|
|
function testAuth(auth: Auth) {
|
|
return test(auth.name) || auth.provider === search || auth.url === search;
|
|
}
|
|
|
|
function testMerge(merge: MergeInfo) {
|
|
return merge.id === search;
|
|
}
|
|
|
|
function filter(account: Account): boolean {
|
|
if (account._id === search)
|
|
return true;
|
|
if (test(account.name))
|
|
return true;
|
|
if (test(account.note))
|
|
return true;
|
|
if (account.roles && account.roles.some(test))
|
|
return true;
|
|
if (account.emails && account.emails.some(test))
|
|
return true;
|
|
if (account.auths && account.auths.some(testAuth))
|
|
return true;
|
|
if (account.merges && account.merges.some(testMerge))
|
|
return true;
|
|
|
|
return false;
|
|
}
|
|
|
|
function prefixWith(prefix: string, action: (phrase: string) => (account: Account) => boolean) {
|
|
return startsWith(search, prefix) ? action(search.substr(prefix.length)) : undefined;
|
|
}
|
|
|
|
function prefixWithRegex(prefix: string, action: (regex: RegExp) => (account: Account) => boolean) {
|
|
return prefixWith(prefix, phrase => action(new RegExp(escapeRegExp(phrase), 'i')));
|
|
}
|
|
|
|
function prefixWithNumber(prefix: string, action: (count: number) => (account: Account) => boolean) {
|
|
return prefixWith(prefix, phrase => action(+phrase));
|
|
}
|
|
|
|
const exactMatch = (phrase: string) => (account: Account) => account.nameLower === phrase;
|
|
const isOld = (max: number) => (account: Account) => !account.lastVisit || account.lastVisit.getTime() < max;
|
|
|
|
return prefixWithRegex('name:', regex => account => regex.test(account.name))
|
|
|| prefixWithRegex('note:', regex => account => regex.test(account.note))
|
|
|| prefixWithRegex('email:', regex => account => !!account.emails && account.emails.some(e => regex.test(e)))
|
|
|| prefixWith('role:', role => account => hasRole(account, role))
|
|
|| prefixWith('exact:', phrase => exactMatch(phrase.toLowerCase()))
|
|
|| prefixWith('disabled!', () => account => !!account.auths && account.auths.some(a => !!a.disabled))
|
|
|| prefixWith('locked!', () => account => !!account.auths && account.auths.some(a => !!a.banned))
|
|
|| prefixWithNumber('ignores:', count => account => (account.ignoresCount || 0) >= count)
|
|
|| prefixWithNumber('ponies:', count => account => account.characterCount >= count)
|
|
|| prefixWithNumber('auths:', count => account => !!account.auths && account.auths.length >= count)
|
|
|| prefixWithNumber('old:', days => isOld(fromNow(-days * DAY).getTime()))
|
|
|| prefixWithNumber('spam:', count => account => !!account.counters && account.counters.spam! >= count)
|
|
|| prefixWithNumber('swearing:', count => account => !!account.counters && account.counters.swears! >= count)
|
|
|| prefixWithNumber('timeouts:', count => account => !!account.counters && account.counters.timeouts! >= count)
|
|
|| prefixWithNumber('limits:', count => account => !!account.counters && account.counters.inviteLimit! >= count)
|
|
|| filter;
|
|
}
|
|
|
|
function hasAnyBan(account: Account) {
|
|
return isBanned(account) || isMuted(account) || isShadowed(account);
|
|
}
|
|
|
|
export function createPotentialDuplicatesFilter(getAccountsByBrowserId: (id: string) => Account[] | undefined): (account: Account) => boolean {
|
|
return i => {
|
|
const name = i.nameLower;
|
|
|
|
if (name === 'anonymous' || !i.lastBrowserId)
|
|
return false;
|
|
|
|
const accounts = getAccountsByBrowserId(i.lastBrowserId);
|
|
|
|
if (accounts !== undefined && accounts.length > 1) {
|
|
for (const a of accounts) {
|
|
if (a !== i && a.nameLower === name) {
|
|
return true;
|
|
}
|
|
}
|
|
}
|
|
|
|
return false;
|
|
};
|
|
}
|
|
|
|
export function createFilter2(showOnly: string): ((account: Account) => boolean) | undefined {
|
|
const now = Date.now();
|
|
|
|
if (showOnly === 'banned') {
|
|
return hasAnyBan;
|
|
} else if (showOnly === 'timed out') {
|
|
return i => !!((i.mute && i.mute > now) || (i.shadow && i.shadow > now) || (i.ban && i.ban > now));
|
|
} else if (showOnly === 'with flags') {
|
|
return i => !!i.flags;
|
|
} else if (showOnly === 'notes') {
|
|
return i => !!i.note;
|
|
} else if (showOnly === 'supporters') {
|
|
return i => !!(i.patreon || i.supporter || i.supporterDeclinedSince);
|
|
} else {
|
|
return undefined;
|
|
}
|
|
}
|
|
|
|
export function getPotentialDuplicates(account: Account, getAccountsByBrowserId: (id: string) => Account[] | undefined) {
|
|
const accounts = account.lastBrowserId ? getAccountsByBrowserId(account.lastBrowserId) : undefined;
|
|
const name = account.nameLower;
|
|
|
|
if (accounts !== undefined && accounts.length > 1 && name !== 'anonymous') {
|
|
return accounts.filter(a => a !== account && a.nameLower === name);
|
|
} else {
|
|
return [];
|
|
}
|
|
}
|
|
|
|
// duplicates
|
|
|
|
export function compareDuplicates(a: DuplicateBase, b: DuplicateBase): number {
|
|
if (a.note !== b.note)
|
|
return b.note - a.note;
|
|
if (a.emails !== b.emails)
|
|
return b.emails - a.emails;
|
|
if (a.name !== b.name)
|
|
return b.name - a.name;
|
|
if (a.browserId !== b.browserId)
|
|
return a.browserId ? -1 : 1;
|
|
if (a.origins !== b.origins)
|
|
return b.origins - a.origins;
|
|
if (a.ponies !== b.ponies)
|
|
return (b.ponies ? b.ponies.length : 0) - (a.ponies ? a.ponies.length : 0);
|
|
return b.lastVisit.getTime() - a.lastVisit.getTime();
|
|
}
|
|
|
|
export function emailName(email: string): string {
|
|
return email.substr(0, email.indexOf('@')).toLowerCase();
|
|
}
|
|
|
|
export function createEmailMatcher(emails: string[]): ((email: string) => boolean) | undefined {
|
|
if (!emails || !emails.length) {
|
|
return undefined;
|
|
} else {
|
|
const match = emails.map(emailName).map(escapeRegExp).join('|');
|
|
const regex = new RegExp(`^(?:${match})@`, 'i');
|
|
return email => regex.test(email);
|
|
}
|
|
}
|
|
|
|
export function createDuplicate(account: Account, base: Account): Duplicate {
|
|
const indenticalEmail = account.emails && base.emails && account.emails.some(e => base.emails!.indexOf(e) !== -1);
|
|
|
|
const isMatch = createEmailMatcher(base.emails || []);
|
|
const duplicateEmails = isMatch && account.emails
|
|
&& account.emails.reduce((sum, e) => sum + (isMatch(e) ? 1 : 0), 0);
|
|
|
|
const duplicateOrigins = base.originsRefs && account.originsRefs
|
|
&& account.originsRefs.reduce((sum, o) => sum + (base.originsRefs!.some(r => o.origin.ip === r.origin.ip) ? 1 : 0), 0);
|
|
|
|
const name = account.nameLower !== 'anonymous' && account.nameLower === base.nameLower;
|
|
|
|
const note = (account.note && account.note.indexOf(base._id) !== -1)
|
|
|| (base.note && base.note.indexOf(account._id) !== -1);
|
|
|
|
const browserId = !!account.lastBrowserId && account.lastBrowserId === base.lastBrowserId;
|
|
const birthdate = !!(base.birthdate && account.birthdate && base.birthdate.getTime() === account.birthdate.getTime());
|
|
|
|
return {
|
|
account,
|
|
name: name ? 1 : 0,
|
|
note: note ? 1 : 0,
|
|
indenticalEmail: !!indenticalEmail,
|
|
emails: toInt(duplicateEmails),
|
|
origins: toInt(duplicateOrigins),
|
|
lastVisit: account.lastVisit || new Date(0),
|
|
browserId,
|
|
birthdate,
|
|
perma: isPermaBanned(account) || isPermaShadowed(account),
|
|
};
|
|
}
|
|
|
|
export function createDuplicateResult(account: Account, base: Account): DuplicateResult {
|
|
return { ...createDuplicate(account, base), account: account._id };
|
|
}
|
|
|
|
export function pushOrdered<T>(items: T[], item: T, compare: (a: T, b: T) => number) {
|
|
for (let i = 0; i < items.length; i++) {
|
|
if (compare(items[i], item) >= 0) {
|
|
items.splice(i, 0, item);
|
|
return;
|
|
}
|
|
}
|
|
|
|
items.push(item);
|
|
}
|
|
|
|
export function duplicatesCollector(duplicates: string[]) {
|
|
const set = new Set();
|
|
|
|
return (item: string) => {
|
|
if (set.has(item)) {
|
|
duplicates.push(item);
|
|
} else {
|
|
set.add(item);
|
|
}
|
|
};
|
|
}
|
|
|
|
export function patreonSupporterLevel(account: AccountBase<any>) {
|
|
return account.patreon! & 0xf;
|
|
}
|
|
|
|
export function supporterLevel(account: AccountBase<any>) {
|
|
const flags = account.supporter!;
|
|
const ignore = hasFlag(flags, SupporterFlags.IgnorePatreon);
|
|
const patreonSupporter = patreonSupporterLevel(account);
|
|
const flagsSupporter = flags & 0xf;
|
|
return Math.max(ignore ? 0 : patreonSupporter, flagsSupporter);
|
|
}
|
|
|
|
export function isPastSupporter(account: AccountBase<any>) {
|
|
const flags = account.supporter!;
|
|
return (hasFlag(flags, SupporterFlags.PastSupporter) || hasFlag(flags, SupporterFlags.ForcePastSupporter)) &&
|
|
!hasFlag(flags, SupporterFlags.IgnorePastSupporter);
|
|
}
|
|
|
|
const fieldToAction: { [key: string]: string | undefined; } = {
|
|
mute: 'Muted',
|
|
shadow: 'Shadowed',
|
|
ban: 'Banned',
|
|
};
|
|
|
|
export function banMessage(field: string, value: number) {
|
|
const action = fieldToAction[field] || 'Did';
|
|
|
|
if (value === 0) {
|
|
return `Un${action.toLowerCase()}`;
|
|
} else if (value === -1) {
|
|
return action;
|
|
} else {
|
|
return `${action} for (${moment.duration(value - Date.now()).humanize()})`;
|
|
}
|
|
}
|
|
|
|
export function isActive(value: number | undefined): boolean {
|
|
return !!value && (value === -1 || value > Date.now());
|
|
}
|
|
|
|
export function isPerma(value: number | undefined): boolean {
|
|
return value === -1;
|
|
}
|
|
|
|
export function isTemporarilyActive(value: number | undefined): boolean {
|
|
return !!value && value > Date.now();
|
|
}
|
|
|
|
export function isMuted(account: BannedMuted): boolean {
|
|
return isActive(account.mute);
|
|
}
|
|
|
|
export function isShadowed(account: BannedMuted): boolean {
|
|
return isActive(account.shadow);
|
|
}
|
|
|
|
export function isBanned(account: BannedMuted): boolean {
|
|
return isActive(account.ban);
|
|
}
|
|
|
|
export function isPermaShadowed(account: BannedMuted): boolean {
|
|
return isPerma(account.shadow);
|
|
}
|
|
|
|
export function isPermaBanned(account: BannedMuted): boolean {
|
|
return isPerma(account.ban);
|
|
}
|
|
|
|
export function isTemporarilyBanned(account: BannedMuted): boolean {
|
|
return isTemporarilyActive(account.ban);
|
|
}
|
|
|
|
export interface SupporterChange {
|
|
message: string;
|
|
date: Date;
|
|
icon: any;
|
|
class: string;
|
|
}
|
|
|
|
export function getIdsFromNote(note: string | undefined) {
|
|
return note ? uniq(note.match(/[0-9a-f]{24}/g)) : [];
|
|
}
|
|
|
|
export function addToMap<T>(map: Map<string, T[]>, key: string, item: T) {
|
|
const items = map.get(key);
|
|
|
|
if (items) {
|
|
items.push(item);
|
|
} else {
|
|
map.set(key, [item]);
|
|
}
|
|
}
|
|
|
|
export function removeFromMap<T>(map: Map<string, T[]>, key: string, item: T) {
|
|
const items = map.get(key);
|
|
|
|
if (items) {
|
|
removeItem(items, item);
|
|
|
|
if (items.length === 0) {
|
|
map.delete(key);
|
|
}
|
|
}
|
|
}
|
|
|
|
export function parsePonies(ponies: string, filterIds?: string[]) {
|
|
return compact(ponies
|
|
.split(/\n\r?/g)
|
|
.map(x => /\[system\] removed pony \[([a-f0-9]{24})\] "(.+)" (\S+)/.exec(x)))
|
|
.map(([_, id, name, info]) => ({ id, name, info }))
|
|
.filter(({ id }) => !filterIds || includes(filterIds, id));
|
|
}
|
|
|
|
export function createIdStore() {
|
|
const idsMap = new Map<string, string>();
|
|
|
|
return (id: string) => {
|
|
const result = idsMap.get(id);
|
|
|
|
if (result) {
|
|
return result;
|
|
} else {
|
|
idsMap.set(id, id);
|
|
return id;
|
|
}
|
|
};
|
|
}
|
|
|
|
export function getTranslationUrl(text: string) {
|
|
return `https://translate.google.com/#view=home&op=translate&sl=auto&tl=en&text=${encodeURIComponent(text)}`;
|
|
// return `https://translate.google.com/#auto/en/${encodeURIComponent(text)}`;
|
|
}
|