Jofthomas's picture
Jofthomas HF staff
Upload 4781 files
5c2ed06 verified
import { Utils, FS } from '../../lib';
export const nameList = new Set<string>(JSON.parse(
FS('config/chat-plugins/usersearch.json').readIfExistsSync() || "[]"
));
const ONLINE_SYMBOL = ` \u25C9 `;
const OFFLINE_SYMBOL = ` \u25CC `;
class PunishmentHTML extends Chat.JSX.Component<{ userid: ID, target: string }> {
render() {
const { userid, target } = { ...this.props };
const buf = [];
for (const cmdName of ['Forcerename', 'Namelock', 'Weeknamelock']) {
// We have to use dangerouslySetInnerHTML here because otherwise the `value`
// property of the button tag is auto escaped, making &#10; into &amp;#10;
buf.push(<span
dangerouslySetInnerHTML={{
__html: `<button class="button" name="send" value="/msgroom staff,/${toID(cmdName)} ${userid}` +
`&#10;/uspage ${target}">${cmdName}</button>`,
}}
/>);
}
return buf;
}
}
class SearchUsernames extends Chat.JSX.Component<{ target: string, page?: boolean }> {
render() {
const { target, page } = { ...this.props };
const results: { offline: string[], online: string[] } = {
offline: [],
online: [],
};
for (const curUser of Users.users.values()) {
if (!curUser.id.includes(target) || curUser.id.startsWith('guest')) continue;
if (Punishments.isGlobalBanned(curUser)) continue;
if (curUser.connected) {
results.online.push(`${!page ? ONLINE_SYMBOL : ''} ${curUser.name}`);
} else {
results.offline.push(`${!page ? OFFLINE_SYMBOL : ''} ${curUser.name}`);
}
}
for (const k in results) {
Utils.sortBy(results[k as keyof typeof results], result => toID(result));
}
if (!page) {
return <>
Users with a name matching '{target}':<br />
{!results.offline.length && !results.online.length ? (
<>No users found.</>
) : (
<>
{results.online.join('; ')}
{!!results.offline.length &&
<>{!!results.online.length && <><br /><br /></>}{results.offline.join('; ')}</>}
</>
)}
</>;
}
return <div class="pad">
<h2>Usernames containing "{target}"</h2>
{!results.online.length && !results.offline.length ? (
<p>No results found.</p>
) : (
<>{!!results.online.length && <div class="ladder pad">
<h3>Online users</h3>
<table>
<tr>
<th>Username</th>
<th>Punish</th>
</tr>
{(() => {
const online = [];
for (const username of results.online) {
online.push(<tr>
<td><username>{username}</username></td>
<td><PunishmentHTML userid={toID(username)} target={target} /></td>
</tr>);
}
return online;
})()}
</table>
</div>}
{!!(results.online.length && results.offline.length) && <hr />}
{!!results.offline.length && <div class="ladder pad">
<h3>Offline users</h3>
<table>
<tr>
<th>Username</th>
<th>Punish</th>
</tr>
{(() => {
const offline = [];
for (const username of results.offline) {
offline.push(<tr>
<td><username>{username}</username></td>
<td><PunishmentHTML userid={toID(username)} target={target} /></td>
</tr>);
}
return offline;
})()}
</table>
</div>}</>
)}
</div>;
}
}
function saveNames() {
FS('config/chat-plugins/usersearch.json').writeUpdate(() => JSON.stringify([...nameList]));
}
export const commands: Chat.ChatCommands = {
us: 'usersearch',
uspage: 'usersearch',
usersearchpage: 'usersearch',
usersearch(target, room, user, connection, cmd) {
this.checkCan('lock');
target = toID(target);
if (!target) { // just join directly if it's the page cmd, they're likely looking for the full list
if (cmd.includes('page')) return this.parse(`/j view-usersearch`);
return this.parse(`/help usersearch`);
}
if (target.length < 3) {
throw new Chat.ErrorMessage(`That's too short of a term to search for.`);
}
const showPage = cmd.includes('page');
if (showPage) {
this.parse(`/j view-usersearch-${target}`);
return;
}
return this.sendReplyBox(<SearchUsernames target={target} />);
},
usersearchhelp: [
`/usersearch [pattern]: Looks for all names matching the [pattern]. Requires: % @ ~`,
`Adding "page" to the end of the command, i.e. /usersearchpage OR /uspage will bring up a page.`,
`See also /usnames for a staff-curated list of the most commonly searched terms.`,
],
usnames: 'usersearchnames',
usersearchnames: {
'': 'list',
list() {
this.parse(`/join view-usersearch`);
},
add(target, room, user) {
this.checkCan('lock');
const targets = target.split(',').map(toID).filter(Boolean);
if (!targets.length) {
return this.errorReply(`Specify at least one term.`);
}
for (const [i, arg] of targets.entries()) {
if (nameList.has(arg)) {
targets.splice(i, 1);
this.errorReply(`Term ${arg} is already on the usersearch term list.`);
continue;
}
if (arg.length < 3) {
targets.splice(i, 1);
this.errorReply(`Term ${arg} is too short for the usersearch term list. Must be more than 3 characters.`);
continue;
}
nameList.add(arg);
}
if (!targets.length) {
// fuck you too, "mia added 0 term to the usersearch name list"
return this.errorReply(`No terms could be added.`);
}
const count = Chat.count(targets, 'terms');
Rooms.get('staff')?.addByUser(
user, `${user.name} added the ${count} "${targets.join(', ')}" to the usersearch name list.`
);
this.globalModlog(`USERSEARCH ADD`, null, targets.join(', '));
if (!room || room.roomid !== 'staff') {
this.sendReply(`You added the ${count} "${targets.join(', ')}" to the usersearch name list.`);
}
saveNames();
},
remove(target, room, user) {
this.checkCan('lock');
const targets = target.split(',').map(toID).filter(Boolean);
if (!targets.length) {
return this.errorReply(`Specify at least one term.`);
}
for (const [i, arg] of targets.entries()) {
if (!nameList.has(arg)) {
targets.splice(i, 1);
this.errorReply(`${arg} is not in the usersearch name list, and has been skipped.`);
continue;
}
nameList.delete(arg);
}
if (!targets.length) {
return this.errorReply(`No terms could be removed.`);
}
const count = Chat.count(targets, 'terms');
Rooms.get('staff')?.addByUser(
user, `${user.name} removed the ${count} "${targets.join(', ')}" from the usersearch name list.`
);
this.globalModlog(`USERSEARCH REMOVE`, null, targets.join(', '));
if (!room || room.roomid !== 'staff') {
this.sendReply(`You removed the ${count} "${targets.join(', ')}"" from the usersearch name list.`);
}
saveNames();
},
},
usnameshelp: [
`/usnames add [...terms]: Adds the given [terms] to the usersearch name list. Requires: % @ ~`,
`/usnames remove [...terms]: Removes the given [terms] from the usersearch name list. Requires: % @ ~`,
`/usnames OR /usnames list: Shows the usersearch name list.`,
],
};
export const pages: Chat.PageTable = {
usersearch(query, user) {
this.checkCan('lock');
const target = toID(query.shift());
if (!target) {
this.title = `[Usersearch Terms]`;
const sorted: { [k: string]: number } = {};
for (const curUser of Users.users.values()) {
for (const term of nameList) {
if (curUser.id.includes(term)) {
if (!(term in sorted)) sorted[term] = 0;
sorted[term]++;
}
}
}
return <div class="pad">
<strong>Usersearch term list</strong>
<button style={{ float: 'right' }} class="button" name="send" value="/uspage">
<i class="fa fa-refresh"></i> Refresh
</button>
<hr />
{!nameList.size ? (
<p>None found.</p>
) : (
<div class="ladder pad">
<table>
<tr>
<th>Term</th>
<th>Current Matches</th>
<th></th>
</tr>
{(() => {
const buf = [];
for (const k of Utils.sortBy(Object.keys(sorted), v => -sorted[v])) {
buf.push(<tr>
<td>{k}</td>
<td>{sorted[k]}</td>
<td><button class="button" name="send" value={`/uspage ${k}`}>Search</button></td>
</tr>);
}
if (!buf.length) return <tr><td colSpan={3} style={{ textAlign: 'center' }}>No names found.</td></tr>;
return buf;
})()}
</table>
</div>
)}
</div>;
}
this.title = `[Usersearch] ${target}`;
return <SearchUsernames target={target} page />;
},
};