dev sync
This commit is contained in:
@@ -133,3 +133,4 @@ session.key
|
||||
salt.key
|
||||
config.json
|
||||
commands/admin/bomb.js
|
||||
commands/mod/uwuify.js
|
||||
|
||||
@@ -52,4 +52,3 @@ textarea {
|
||||
#chatform {
|
||||
border-color: #ee600d;
|
||||
}
|
||||
|
||||
|
||||
@@ -1,11 +1,17 @@
|
||||
/**
|
||||
* @author Marzavec ( https://github.com/marzavec )
|
||||
* @summary Create a new mod trip
|
||||
* @version 1.0.0
|
||||
* @version 1.1.0
|
||||
* @description Adds target trip to the config as a mod and upgrades the socket type
|
||||
* @module addmod
|
||||
*/
|
||||
|
||||
import {
|
||||
Info,
|
||||
} from '../utility/_Constants.js';
|
||||
import {
|
||||
legacyLevelToLabel,
|
||||
} from '../utility/_LegacyFunctions.js';
|
||||
import {
|
||||
isAdmin,
|
||||
isModerator,
|
||||
@@ -41,22 +47,23 @@ export async function run({
|
||||
...getUserDetails(newMod[0]),
|
||||
...{
|
||||
cmd: 'updateUser',
|
||||
uType: 'mod', // @todo use legacyLevelToLabel from _LegacyFunctions.js
|
||||
uType: legacyLevelToLabel(levels.moderator),
|
||||
level: levels.moderator,
|
||||
},
|
||||
};
|
||||
|
||||
for (let i = 0, l = newMod.length; i < l; i += 1) {
|
||||
// upgrade privileges
|
||||
newMod[i].uType = 'mod'; // @todo use legacyLevelToLabel from _LegacyFunctions.js
|
||||
newMod[i].uType = legacyLevelToLabel(levels.moderator);
|
||||
newMod[i].level = levels.moderator;
|
||||
newMod[i].color = color;
|
||||
newMod[i].flair = flair;
|
||||
|
||||
// inform new mod
|
||||
server.send({
|
||||
cmd: 'info', // @todo Add numeric info code as `id`
|
||||
cmd: 'info',
|
||||
text: 'You are now a mod.',
|
||||
id: Info.Admin.YOU_ARE_MOD,
|
||||
channel: newMod[i].channel, // @todo Multichannel
|
||||
}, newMod[i]);
|
||||
|
||||
@@ -72,15 +79,17 @@ export async function run({
|
||||
|
||||
// return success message
|
||||
server.reply({
|
||||
cmd: 'info', // @todo Add numeric info code as `id`
|
||||
cmd: 'info',
|
||||
text: `Added mod trip: ${payload.trip}, remember to run 'saveconfig' to make it permanent`,
|
||||
id: Info.Admin.MOD_ADDED,
|
||||
channel: socket.channel, // @todo Multichannel
|
||||
}, socket);
|
||||
|
||||
// notify all mods
|
||||
server.broadcast({
|
||||
cmd: 'info', // @todo Add numeric info code as `id`
|
||||
cmd: 'info',
|
||||
text: `Added mod: ${payload.trip}`,
|
||||
id: Info.Admin.MOD_ADDED_BROADCAST,
|
||||
channel: false, // @todo Multichannel, false for global info
|
||||
}, { level: isModerator });
|
||||
|
||||
|
||||
+37
-14
@@ -5,11 +5,14 @@
|
||||
/**
|
||||
* @author Marzavec ( https://github.com/marzavec )
|
||||
* @summary Show users and channels
|
||||
* @version 1.0.0
|
||||
* @version 1.1.0
|
||||
* @description Outputs all current channels and sockets in those channels
|
||||
* @module listusers
|
||||
*/
|
||||
|
||||
import {
|
||||
Info,
|
||||
} from '../utility/_Constants.js';
|
||||
import {
|
||||
isAdmin,
|
||||
} from '../utility/_UAC.js';
|
||||
@@ -28,31 +31,51 @@ export async function run({ server, socket }) {
|
||||
|
||||
// find all users currently in a channel
|
||||
const currentUsers = server.findSockets({
|
||||
channel: (channel) => true,
|
||||
channel: () => true,
|
||||
});
|
||||
|
||||
// compile channel and user list
|
||||
const channels = {};
|
||||
for (let i = 0, j = currentUsers.length; i < j; i += 1) {
|
||||
if (typeof channels[currentUsers[i].channel] === 'undefined') {
|
||||
channels[currentUsers[i].channel] = [];
|
||||
const user = currentUsers[i];
|
||||
if (typeof channels[user.channel] === 'undefined') {
|
||||
channels[user.channel] = [];
|
||||
}
|
||||
|
||||
channels[currentUsers[i].channel].push(
|
||||
`[${currentUsers[i].trip || 'null'}]${currentUsers[i].nick}`,
|
||||
);
|
||||
channels[user.channel].push(user);
|
||||
}
|
||||
|
||||
// build output
|
||||
const lines = [];
|
||||
for (const channel in channels) {
|
||||
lines.push(`?${channel} ${channels[channel].join(', ')}`);
|
||||
const channelList = Object.keys(channels).map((name) => ({
|
||||
name,
|
||||
users: channels[name],
|
||||
count: channels[name].length,
|
||||
}));
|
||||
|
||||
channelList.sort((a, b) => b.count - a.count);
|
||||
|
||||
let reply = '| Channel | Trip | Nick | Hash |\n';
|
||||
reply += '| :--- | :--- | :--- | :--- |\n';
|
||||
|
||||
for (let i = 0; i < channelList.length; i += 1) {
|
||||
const { name, users } = channelList[i];
|
||||
|
||||
for (let k = 0; k < users.length; k += 1) {
|
||||
const u = users[k];
|
||||
const trip = u.trip || '(none)';
|
||||
const hash = u.hash || '???';
|
||||
|
||||
reply += `| ?${name} | ${trip} | ${u.nick} | ${hash} |\n`;
|
||||
}
|
||||
}
|
||||
|
||||
reply += '\n---\n';
|
||||
reply += `**Total Channels:** ${channelList.length}\n`;
|
||||
reply += `**Total Users:** ${currentUsers.length}`;
|
||||
|
||||
// send reply
|
||||
server.reply({
|
||||
cmd: 'info', // @todo Add numeric info code as `id`
|
||||
text: lines.join('\n'),
|
||||
cmd: 'info',
|
||||
text: reply,
|
||||
id: Info.Admin.USER_LIST,
|
||||
channel: socket.channel, // @todo Multichannel
|
||||
}, socket);
|
||||
|
||||
|
||||
@@ -1,11 +1,14 @@
|
||||
/**
|
||||
* @author Marzavec ( https://github.com/marzavec )
|
||||
* @summary Refresh modules
|
||||
* @version 1.0.0
|
||||
* @version 1.1.0
|
||||
* @description Allows a remote user to clear and re-import the server command modules
|
||||
* @module reload
|
||||
*/
|
||||
|
||||
import {
|
||||
Info,
|
||||
} from '../utility/_Constants.js';
|
||||
import {
|
||||
isAdmin,
|
||||
isModerator,
|
||||
@@ -77,8 +80,9 @@ ${loadResult}\n\n`;
|
||||
|
||||
// send results to moderators (which the user using this command is higher than)
|
||||
server.broadcast({
|
||||
cmd: 'info', // @todo Add numeric info code as `id`
|
||||
cmd: 'info',
|
||||
text: loadReport,
|
||||
id: Info.Admin.RELOAD_STATUS,
|
||||
channel: false, // @todo Multichannel, false for global
|
||||
}, { level: isModerator });
|
||||
|
||||
|
||||
@@ -1,11 +1,17 @@
|
||||
/**
|
||||
* @author Marzavec ( https://github.com/marzavec )
|
||||
* @summary Removes a mod
|
||||
* @version 1.0.0
|
||||
* @version 1.1.0
|
||||
* @description Removes target trip from the config as a mod and downgrades the socket type
|
||||
* @module removemod
|
||||
*/
|
||||
|
||||
import {
|
||||
Info,
|
||||
} from '../utility/_Constants.js';
|
||||
import {
|
||||
legacyLevelToLabel,
|
||||
} from '../utility/_LegacyFunctions.js';
|
||||
import {
|
||||
isAdmin,
|
||||
isModerator,
|
||||
@@ -44,22 +50,23 @@ export async function run({
|
||||
...getUserDetails(targetMod[0]),
|
||||
...{
|
||||
cmd: 'updateUser',
|
||||
uType: 'user', // @todo use legacyLevelToLabel from _LegacyFunctions.js
|
||||
uType: legacyLevelToLabel(levels.default),
|
||||
level: levels.default,
|
||||
},
|
||||
};
|
||||
|
||||
for (let i = 0, l = targetMod.length; i < l; i += 1) {
|
||||
// downgrade privileges
|
||||
targetMod[i].uType = 'user';
|
||||
targetMod[i].uType = legacyLevelToLabel(levels.default);
|
||||
targetMod[i].level = levels.default;
|
||||
targetMod[i].color = color;
|
||||
targetMod[i].flair = flair;
|
||||
|
||||
// inform ex-mod
|
||||
server.send({
|
||||
cmd: 'info', // @todo Add numeric info code as `id`
|
||||
cmd: 'info',
|
||||
text: 'You are now a user.',
|
||||
id: Info.Admin.YOU_ARE_USER,
|
||||
channel: targetMod[i].channel, // @todo Multichannel
|
||||
}, targetMod[i]);
|
||||
|
||||
@@ -75,17 +82,19 @@ export async function run({
|
||||
|
||||
// return success message
|
||||
server.reply({
|
||||
cmd: 'info', // @todo Add numeric info code as `id`
|
||||
cmd: 'info',
|
||||
text: `Removed mod trip: ${
|
||||
payload.trip
|
||||
}, remember to run 'saveconfig' to make it permanent`,
|
||||
id: Info.Admin.MOD_REMOVED,
|
||||
channel: socket.channel, // @todo Multichannel
|
||||
}, socket);
|
||||
|
||||
// notify all mods
|
||||
server.broadcast({
|
||||
cmd: 'info', // @todo Add numeric info code as `id`
|
||||
cmd: 'info',
|
||||
text: `Removed mod: ${payload.trip}`,
|
||||
id: Info.Admin.MOD_REMOVED_BROADCAST,
|
||||
channel: false, // @todo Multichannel, false for global
|
||||
}, { level: isModerator });
|
||||
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
/**
|
||||
* @author Marzavec ( https://github.com/marzavec )
|
||||
* @summary Saves the config
|
||||
* @version 1.0.0
|
||||
* @version 1.1.0
|
||||
* @description Writes the current config to disk
|
||||
* @module saveconfig
|
||||
*/
|
||||
@@ -12,6 +12,7 @@ import {
|
||||
} from '../utility/_UAC.js';
|
||||
import {
|
||||
Errors,
|
||||
Info,
|
||||
} from '../utility/_Constants.js';
|
||||
|
||||
/**
|
||||
@@ -40,8 +41,9 @@ export async function run({ core, server, socket }) {
|
||||
|
||||
// return success message to moderators and admins
|
||||
server.broadcast({
|
||||
cmd: 'info', // @todo Add numeric info code as `id`
|
||||
cmd: 'info',
|
||||
text: 'Config saved!',
|
||||
id: Info.Admin.CONFIG_SAVED,
|
||||
channel: false, // @todo Multichannel
|
||||
}, { level: isModerator });
|
||||
|
||||
|
||||
@@ -1,11 +1,14 @@
|
||||
/**
|
||||
* @author Marzavec ( https://github.com/marzavec )
|
||||
* @summary Emit text everywhere
|
||||
* @version 1.0.0
|
||||
* @version 1.1.0
|
||||
* @description Displays passed text to every client connected
|
||||
* @module shout
|
||||
*/
|
||||
|
||||
import {
|
||||
Info,
|
||||
} from '../utility/_Constants.js';
|
||||
import {
|
||||
isAdmin,
|
||||
} from '../utility/_UAC.js';
|
||||
@@ -24,8 +27,9 @@ export async function run({ server, socket, payload }) {
|
||||
|
||||
// send text to all channels
|
||||
server.broadcast({
|
||||
cmd: 'info', // @todo Add numeric info code as `id`
|
||||
cmd: 'info',
|
||||
text: `Server Notice: ${payload.text}`,
|
||||
id: Info.Admin.SHOUT,
|
||||
channel: false, // @todo Multichannel, false for global
|
||||
}, {});
|
||||
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
/**
|
||||
* @author Marzavec
|
||||
* @summary Take channel ownership
|
||||
* @version 1.0.0
|
||||
* @version 1.1.0
|
||||
* @description Claim an unowned channel, enabling user management options
|
||||
* @module claimchannel
|
||||
*/
|
||||
@@ -15,6 +15,7 @@ import {
|
||||
} from '../utility/_UAC.js';
|
||||
import {
|
||||
Errors,
|
||||
Info,
|
||||
ClaimExpirationDays,
|
||||
} from '../utility/_Constants.js';
|
||||
import {
|
||||
@@ -29,7 +30,7 @@ import {
|
||||
* @return {void}
|
||||
*/
|
||||
export async function run({
|
||||
core, server, socket,
|
||||
server, socket,
|
||||
}) {
|
||||
// must be in a channel to run this command
|
||||
if (typeof socket.channel === 'undefined') {
|
||||
@@ -54,19 +55,37 @@ export async function run({
|
||||
}, socket);
|
||||
}
|
||||
|
||||
const channelSettings = getChannelSettings(core.appConfig.data, socket.channel);
|
||||
/* const channelSettings = getChannelSettings(core.appConfig.data, socket.channel);
|
||||
|
||||
if (channelSettings.owned) {
|
||||
return server.reply({
|
||||
cmd: 'warn',
|
||||
text: `Failed to take ownership: This channel is already owned by the trip "${channelSettings.ownerTrip}", until ${channelSettings.claimExpires}`,
|
||||
text: `Failed to take ownership:
|
||||
This channel is already owned by the trip "${channelSettings.ownerTrip}",
|
||||
until ${channelSettings.claimExpires}`,
|
||||
ownerTrip: channelSettings.ownerTrip,
|
||||
claimExpires: channelSettings.claimExpires,
|
||||
id: Errors.ClaimChannel.ALREADY_OWNED,
|
||||
channel: socket.channel, // @todo Multichannel
|
||||
}, socket);
|
||||
} */
|
||||
|
||||
if (typeof socket.wallet !== 'object' || typeof socket.wallet.address !== 'string') {
|
||||
return server.reply({
|
||||
cmd: 'warn',
|
||||
text: 'You must connect a wallet first',
|
||||
id: Errors.Global.LOGIN_REQUIRED,
|
||||
channel: socket.channel,
|
||||
}, socket);
|
||||
}
|
||||
|
||||
return server.reply({
|
||||
cmd: 'warn',
|
||||
text: 'This command is disabled, pending reviews and updates',
|
||||
id: Errors.Global.PERMISSION,
|
||||
channel: socket.channel, // @todo Multichannel
|
||||
}, socket);
|
||||
|
||||
socket.claimCaptcha = {
|
||||
solution: captcha.generateRandomText(7),
|
||||
};
|
||||
@@ -106,6 +125,8 @@ export function initHooks(server) {
|
||||
export function chatHook({
|
||||
core, server, socket, payload,
|
||||
}) {
|
||||
if (typeof payload === 'undefined') return false;
|
||||
|
||||
if (typeof payload.text !== 'string') {
|
||||
return false;
|
||||
}
|
||||
@@ -138,8 +159,9 @@ export function chatHook({
|
||||
console.log(`[${socket.trip}]${socket.nick} claimed ?${socket.channel}`);
|
||||
|
||||
server.broadcast({
|
||||
cmd: 'info', // @todo Add numeric info code as `id`
|
||||
cmd: 'info',
|
||||
text: `Channel now owned by "${socket.trip}", until ${channelSettings.claimExpires}`,
|
||||
id: Info.Admin.SHOUT,
|
||||
channel: socket.channel,
|
||||
}, { channel: socket.channel });
|
||||
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
/**
|
||||
* @author Marzavec
|
||||
* @summary Sets channel to private
|
||||
* @version 1.0.0
|
||||
* @version 1.1.0
|
||||
* @description Remove channel from being listed on the front page
|
||||
* @module makeprivate
|
||||
*/
|
||||
@@ -11,6 +11,7 @@ import {
|
||||
} from '../utility/_UAC.js';
|
||||
import {
|
||||
Errors,
|
||||
Info,
|
||||
} from '../utility/_Constants.js';
|
||||
|
||||
/**
|
||||
@@ -59,8 +60,9 @@ export async function run({
|
||||
core.appConfig.data.publicChannels.splice(listingIndex, 1);
|
||||
|
||||
server.reply({
|
||||
cmd: 'info', // @todo Add numeric info code as `id`
|
||||
cmd: 'info',
|
||||
text: 'This channel has been removed from the list of public channels',
|
||||
id: Info.Admin.CONFIG_SAVED,
|
||||
channel: socket.channel, // @todo Multichannel
|
||||
}, socket);
|
||||
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
/**
|
||||
* @author Marzavec
|
||||
* @summary Sets channel to public
|
||||
* @version 1.0.0
|
||||
* @version 1.1.0
|
||||
* @description Make channel publicly listed on the front page
|
||||
* @module makepublic
|
||||
*/
|
||||
@@ -13,6 +13,7 @@ import {
|
||||
} from '../utility/_UAC.js';
|
||||
import {
|
||||
Errors,
|
||||
Info,
|
||||
} from '../utility/_Constants.js';
|
||||
import {
|
||||
getChannelSettings,
|
||||
@@ -120,14 +121,16 @@ export function chatHook({
|
||||
core.appConfig.data.publicChannels.push(socket.channel);
|
||||
|
||||
server.broadcast({
|
||||
cmd: 'info', // @todo Add numeric info code as `id`
|
||||
cmd: 'info',
|
||||
text: `A new channel has been made public: ?${socket.channel}`,
|
||||
id: Info.Admin.SHOUT,
|
||||
channel: socket.channel, // @todo Multichannel
|
||||
}, { level: (level) => isModerator(level) });
|
||||
|
||||
server.reply({
|
||||
cmd: 'info', // @todo Add numeric info code as `id`
|
||||
cmd: 'info',
|
||||
text: 'This channel has been added to the list of public channels',
|
||||
id: Info.Admin.CONFIG_SAVED,
|
||||
channel: socket.channel, // @todo Multichannel
|
||||
}, socket);
|
||||
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
/**
|
||||
* @author Marzavec
|
||||
* @summary Renews the claim
|
||||
* @version 1.0.0
|
||||
* @version 1.1.0
|
||||
* @description Extend the ownership expiration date, before it expires
|
||||
* @module renewclaim
|
||||
*/
|
||||
@@ -12,6 +12,7 @@ import {
|
||||
} from '../utility/_UAC.js';
|
||||
import {
|
||||
Errors,
|
||||
Info,
|
||||
ClaimExpirationDays,
|
||||
} from '../utility/_Constants.js';
|
||||
import {
|
||||
@@ -141,8 +142,9 @@ export function chatHook({
|
||||
updateChannelSettings(core.appConfig.data, socket.channel, channelSettings);
|
||||
|
||||
server.reply({
|
||||
cmd: 'info', // @todo Add numeric info code as `id`
|
||||
cmd: 'info',
|
||||
text: `Your claim has been renewed until ${expirationDate}`,
|
||||
id: Info.Admin.CONFIG_SAVED,
|
||||
channel: socket.channel, // @todo Multichannel
|
||||
}, socket);
|
||||
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
/**
|
||||
* @author Marzavec ( https://github.com/marzavec )
|
||||
* @summary Change user level
|
||||
* @version 1.0.0
|
||||
* @version 1.1.0
|
||||
* @description Alter the permission level a trip is allowed within current channel
|
||||
* @module setlevel
|
||||
*/
|
||||
@@ -17,6 +17,7 @@ import {
|
||||
} from '../utility/_Channels.js';
|
||||
import {
|
||||
Errors,
|
||||
Info,
|
||||
} from '../utility/_Constants.js';
|
||||
|
||||
/**
|
||||
@@ -122,8 +123,9 @@ export async function run({
|
||||
}, { channel: socket.channel });
|
||||
|
||||
server.broadcast({
|
||||
cmd: 'info', // @todo Add numeric info code as `id`
|
||||
cmd: 'info',
|
||||
text: `Changed permission level of "${payload.trip}" to "${payload.level}"`,
|
||||
id: Info.Admin.SHOUT,
|
||||
channel: socket.channel, // @todo Multichannel
|
||||
}, { channel: socket.channel });
|
||||
}
|
||||
@@ -160,28 +162,24 @@ export function setlevelCheck({
|
||||
if (payload.text.startsWith('/setlevel')) {
|
||||
const input = payload.text.split(' ');
|
||||
|
||||
// If there is no trip parameter
|
||||
// if there is no trip parameter
|
||||
if (!input[1]) {
|
||||
server.reply({
|
||||
return server.reply({
|
||||
cmd: 'warn',
|
||||
text: 'Failed to set level: Missing trip. Refer to `/help setlevel` for instructions on how to use this command.',
|
||||
id: Errors.SetLevel.BAD_TRIP,
|
||||
channel: socket.channel, // @todo Multichannel
|
||||
}, socket);
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
// If there is no level parameter
|
||||
// if there is no level parameter
|
||||
if (!input[2]) {
|
||||
server.reply({
|
||||
return server.reply({
|
||||
cmd: 'warn',
|
||||
text: 'Failed to set level: Missing level label. Refer to `/help setlevel` for instructions on how to use this command.',
|
||||
id: Errors.SetLevel.BAD_LABEL,
|
||||
channel: socket.channel, // @todo Multichannel
|
||||
}, socket);
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
this.run({
|
||||
|
||||
@@ -1,10 +1,10 @@
|
||||
/**
|
||||
* @author Marzavec ( https://github.com/marzavec )
|
||||
* @summary Change motd
|
||||
* @version 1.0.0
|
||||
* @description Update the channel motd to something new
|
||||
* @module setmotd
|
||||
*/
|
||||
* @author Marzavec ( https://github.com/marzavec )
|
||||
* @summary Change motd
|
||||
* @version 1.1.0
|
||||
* @description Update the channel motd to something new
|
||||
* @module setmotd
|
||||
*/
|
||||
|
||||
import {
|
||||
isChannelModerator,
|
||||
@@ -15,6 +15,7 @@ import {
|
||||
} from '../utility/_Channels.js';
|
||||
import {
|
||||
Errors,
|
||||
Info,
|
||||
MaxMOTDLength,
|
||||
} from '../utility/_Constants.js';
|
||||
|
||||
@@ -63,16 +64,18 @@ export async function run({
|
||||
updateChannelSettings(core.appConfig.data, socket.channel, channelSettings);
|
||||
|
||||
server.broadcast({
|
||||
cmd: 'info', // @todo Add numeric info code as `id`
|
||||
cmd: 'info',
|
||||
text: `MOTD changed by [${socket.trip}]${socket.nick}, new motd:`,
|
||||
id: Info.Admin.SHOUT,
|
||||
channel: socket.channel, // @todo Multichannel
|
||||
}, { channel: socket.channel });
|
||||
}, { channel: socket.channel, level: isChannelModerator });
|
||||
|
||||
server.broadcast({
|
||||
cmd: 'info', // @todo Add numeric info code as `id`
|
||||
cmd: 'info',
|
||||
text: channelSettings.motd,
|
||||
id: Info.Core.MOTD,
|
||||
channel: socket.channel, // @todo Multichannel
|
||||
}, { channel: socket.channel });
|
||||
}, { channel: socket.channel, level: isChannelModerator });
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
/**
|
||||
* @author Marzavec
|
||||
* @summary Release channel ownership
|
||||
* @version 1.0.0
|
||||
* @version 1.1.0
|
||||
* @description Clear ownership info and channel settings
|
||||
* @module unclaimchannel
|
||||
*/
|
||||
@@ -14,6 +14,7 @@ import {
|
||||
} from '../utility/_UAC.js';
|
||||
import {
|
||||
Errors,
|
||||
Info,
|
||||
} from '../utility/_Constants.js';
|
||||
import {
|
||||
getChannelSettings,
|
||||
@@ -66,8 +67,9 @@ export async function run({
|
||||
deleteChannelSettings(core.appConfig.data, socket.channel);
|
||||
|
||||
server.broadcast({
|
||||
cmd: 'info', // @todo Add numeric info code as `id`
|
||||
cmd: 'info',
|
||||
text: 'Channel ownership has been removed and the channel settings have been reset',
|
||||
id: Info.Admin.SHOUT,
|
||||
channel: socket.channel,
|
||||
}, { channel: socket.channel });
|
||||
|
||||
|
||||
@@ -1,27 +1,24 @@
|
||||
/**
|
||||
* @author Marzavec ( https://github.com/marzavec )
|
||||
* @summary Update name color
|
||||
* @version 1.0.0
|
||||
* @version 1.1.0
|
||||
* @description Allows calling client to change their nickname color
|
||||
* @module changecolor
|
||||
*/
|
||||
|
||||
import {
|
||||
getSession,
|
||||
} from './session.js';
|
||||
import {
|
||||
getUserDetails,
|
||||
} from '../utility/_UAC.js';
|
||||
import {
|
||||
verifyColor,
|
||||
} from '../utility/_Text.js';
|
||||
import {
|
||||
Errors,
|
||||
} from '../utility/_Constants.js';
|
||||
|
||||
/**
|
||||
* Validate a string as a valid hex color string
|
||||
* @param {string} color - Color string to validate
|
||||
* @private
|
||||
* @todo Move into utility module
|
||||
* @return {boolean}
|
||||
*/
|
||||
const verifyColor = (color) => /(^[0-9A-F]{6}$)|(^[0-9A-F]{3}$)/i.test(color);
|
||||
|
||||
/**
|
||||
* Executes when invoked by a remote client
|
||||
* @param {Object} env - Environment object with references to core, server, socket & payload
|
||||
@@ -29,7 +26,7 @@ const verifyColor = (color) => /(^[0-9A-F]{6}$)|(^[0-9A-F]{3}$)/i.test(color);
|
||||
* @return {void}
|
||||
*/
|
||||
export async function run({
|
||||
server, socket, payload,
|
||||
core, server, socket, payload,
|
||||
}) {
|
||||
// must be in a channel to run this command
|
||||
if (typeof socket.channel === 'undefined') {
|
||||
@@ -84,6 +81,13 @@ export async function run({
|
||||
// @todo this should be sent to every channel the user is in (multichannel)
|
||||
server.broadcast(updateNotice, { channel: socket.channel });
|
||||
|
||||
server.reply({
|
||||
cmd: 'session',
|
||||
restored: false,
|
||||
token: getSession(socket, core),
|
||||
channels: socket.channels,
|
||||
}, socket);
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
@@ -115,7 +119,7 @@ export function colorCheck({
|
||||
if (payload.text.startsWith('/color ')) {
|
||||
const input = payload.text.split(' ');
|
||||
|
||||
// If there is no color target parameter
|
||||
// if there is no color target parameter
|
||||
if (input[1] === undefined) {
|
||||
server.reply({
|
||||
cmd: 'warn',
|
||||
|
||||
@@ -3,7 +3,7 @@
|
||||
/**
|
||||
* @author Marzavec ( https://github.com/marzavec )
|
||||
* @summary Update nickname
|
||||
* @version 1.0.0
|
||||
* @version 1.1.0
|
||||
* @description Allows calling client to change their current nickname
|
||||
* @module changenick
|
||||
*/
|
||||
@@ -14,6 +14,7 @@ import {
|
||||
} from '../utility/_UAC.js';
|
||||
import {
|
||||
Errors,
|
||||
Info,
|
||||
} from '../utility/_Constants.js';
|
||||
|
||||
/**
|
||||
@@ -72,13 +73,13 @@ export async function run({
|
||||
const userExists = server.findSockets({
|
||||
channel,
|
||||
nick: (targetNick) => targetNick.toLowerCase() === newNick.toLowerCase()
|
||||
// Allow them to rename themselves to a different case
|
||||
// allow them to rename themselves to a different case
|
||||
&& targetNick != previousNick,
|
||||
});
|
||||
|
||||
// return error if found
|
||||
if (userExists.length > 0) {
|
||||
// That nickname is already in that channel
|
||||
// that nickname is already in that channel
|
||||
return server.reply({
|
||||
cmd: 'warn',
|
||||
text: 'Nickname taken',
|
||||
@@ -130,8 +131,9 @@ export async function run({
|
||||
|
||||
// notify channel that the user has changed their name
|
||||
server.broadcast({
|
||||
cmd: 'info', // @todo Add numeric info code as `id`
|
||||
cmd: 'info',
|
||||
text: `${socket.nick} is now ${newNick}`,
|
||||
id: Info.Core.NICK_CHANGED,
|
||||
channel, // @todo Multichannel
|
||||
}, { channel });
|
||||
|
||||
@@ -169,16 +171,14 @@ export function nickCheck({
|
||||
if (payload.text.startsWith('/nick')) {
|
||||
const input = payload.text.split(' ');
|
||||
|
||||
// If there is no nickname target parameter
|
||||
// if there is no nickname target parameter
|
||||
if (!input[1]) {
|
||||
server.reply({
|
||||
return server.reply({
|
||||
cmd: 'warn',
|
||||
text: 'Nickname must consist of up to 24 letters, numbers, and underscores',
|
||||
id: Errors.Join.INVALID_NICK,
|
||||
channel: socket.channel, // @todo Multichannel
|
||||
}, socket);
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
const newNick = input[1].replace(/@/g, '');
|
||||
|
||||
+27
-15
@@ -15,6 +15,7 @@ import {
|
||||
} from '../utility/_UAC.js';
|
||||
import {
|
||||
Errors,
|
||||
Info,
|
||||
} from '../utility/_Constants.js';
|
||||
|
||||
/**
|
||||
@@ -24,51 +25,57 @@ import {
|
||||
export const MAX_MESSAGE_ID_LENGTH = 6;
|
||||
|
||||
/**
|
||||
* The time in milliseconds before a message is considered stale, and thus no longer allowed
|
||||
* to be edited.
|
||||
* The time in milliseconds before a message is considered stale
|
||||
* @type {number}
|
||||
*/
|
||||
const ACTIVE_TIMEOUT = 5 * 60 * 1000;
|
||||
|
||||
/**
|
||||
* The time in milliseconds that a check for stale messages should be performed.
|
||||
* The time in milliseconds that a check for stale messages should be performed
|
||||
* @type {number}
|
||||
*/
|
||||
const TIMEOUT_CHECK_INTERVAL = 30 * 1000;
|
||||
|
||||
/**
|
||||
* Stores active messages that can be edited.
|
||||
* Stores active messages that can be edited
|
||||
* @type {Array}
|
||||
*/
|
||||
export const ACTIVE_MESSAGES = [];
|
||||
|
||||
/**
|
||||
* Cleans up stale messages.
|
||||
* Interval reference for cleanup
|
||||
*/
|
||||
let cleanupInterval = null;
|
||||
|
||||
/**
|
||||
* Cleans up stale messages
|
||||
* @public
|
||||
* @return {void}
|
||||
*/
|
||||
export function cleanActiveMessages() {
|
||||
const now = Date.now();
|
||||
for (let i = 0; i < ACTIVE_MESSAGES.length; i += 1) {
|
||||
for (let i = ACTIVE_MESSAGES.length - 1; i >= 0; i -= 1) {
|
||||
const message = ACTIVE_MESSAGES[i];
|
||||
if (now - message.sent > ACTIVE_TIMEOUT || message.toDelete) {
|
||||
ACTIVE_MESSAGES.splice(i, 1);
|
||||
i -= 1;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// TODO: This won't get cleared on module reload.
|
||||
setInterval(cleanActiveMessages, TIMEOUT_CHECK_INTERVAL);
|
||||
if (!cleanupInterval) {
|
||||
cleanupInterval = setInterval(cleanActiveMessages, TIMEOUT_CHECK_INTERVAL);
|
||||
}
|
||||
|
||||
/**
|
||||
* Adds a message to the active messages map.
|
||||
* Adds a message to the active messages map
|
||||
* @public
|
||||
* @param {string} id
|
||||
* @param {string} customId
|
||||
* @param {number} userid
|
||||
* @return {void}
|
||||
*/
|
||||
export function addActiveMessage(customId, userid) {
|
||||
if (!customId) return;
|
||||
|
||||
ACTIVE_MESSAGES.push({
|
||||
customId,
|
||||
userid,
|
||||
@@ -112,11 +119,12 @@ export async function run({
|
||||
|
||||
const { customId } = payload;
|
||||
|
||||
if (typeof (customId) === 'string' && customId.length > MAX_MESSAGE_ID_LENGTH) {
|
||||
// There's a limit on the custom id length.
|
||||
if (typeof customId === 'string' && customId.length > MAX_MESSAGE_ID_LENGTH) {
|
||||
return server.police.frisk(socket, 13);
|
||||
}
|
||||
|
||||
const messageId = Math.floor(Math.random() * 999999) + 1;
|
||||
|
||||
// build chat payload
|
||||
const outgoingPayload = {
|
||||
cmd: 'chat',
|
||||
@@ -128,6 +136,7 @@ export async function run({
|
||||
level: socket.level,
|
||||
flair: socket.flair,
|
||||
customId,
|
||||
id: messageId,
|
||||
};
|
||||
|
||||
if (isAdmin(socket.level)) {
|
||||
@@ -144,7 +153,9 @@ export async function run({
|
||||
outgoingPayload.color = socket.color;
|
||||
}
|
||||
|
||||
if (outgoingPayload.customId) {
|
||||
addActiveMessage(outgoingPayload.customId, socket.userid);
|
||||
}
|
||||
|
||||
// broadcast to channel peers
|
||||
server.broadcast(outgoingPayload, { channel: socket.channel });
|
||||
@@ -181,13 +192,14 @@ export function commandCheckIn({ server, socket, payload }) {
|
||||
}
|
||||
|
||||
if (payload.text.startsWith('/shrug')) {
|
||||
payload.text = payload.text.replace('/shrug', String.fromCharCode(175)+String.fromCharCode(92)+String.fromCharCode(92)+String.fromCharCode(92)+String.fromCharCode(95)+String.fromCharCode(40)+String.fromCharCode(12484)+String.fromCharCode(41)+String.fromCharCode(92)+String.fromCharCode(95)+String.fromCharCode(47)+String.fromCharCode(175));
|
||||
payload.text = payload.text.replace('/shrug', String.fromCharCode(175, 92, 92, 92, 95, 40, 12484, 41, 92, 95, 47, 175));
|
||||
}
|
||||
|
||||
if (payload.text.startsWith('/myhash')) {
|
||||
server.reply({
|
||||
cmd: 'info', // @todo Add numeric info code as `id`
|
||||
cmd: 'info',
|
||||
text: `Your hash: ${socket.hash}`,
|
||||
id: Info.Core.MY_HASH,
|
||||
channel: socket.channel, // @todo Multichannel
|
||||
}, socket);
|
||||
|
||||
|
||||
@@ -9,6 +9,7 @@
|
||||
import {
|
||||
CodebaseVersion,
|
||||
Errors,
|
||||
Info,
|
||||
} from '../utility/_Constants.js';
|
||||
|
||||
/**
|
||||
@@ -75,8 +76,9 @@ export async function run({
|
||||
|
||||
// output reply
|
||||
server.reply({
|
||||
cmd: 'info', // @todo Add numeric info code as `id`
|
||||
cmd: 'info',
|
||||
text: reply,
|
||||
id: Info.Core.HELP_TEXT,
|
||||
channel: socket.channel, // @todo Multichannel
|
||||
}, socket);
|
||||
|
||||
|
||||
@@ -18,7 +18,7 @@ import {
|
||||
} from '../utility/_LegacyFunctions.js';
|
||||
|
||||
/**
|
||||
* Returns the channel that should be invited to.
|
||||
* Returns the channel that should be invited to
|
||||
* @param {any} channel
|
||||
* @private
|
||||
* @return {string}
|
||||
|
||||
+16
-13
@@ -4,7 +4,7 @@
|
||||
/**
|
||||
* @author Marzavec ( https://github.com/marzavec )
|
||||
* @summary Join target channel
|
||||
* @version 1.0.0
|
||||
* @version 1.1.0
|
||||
* @description Join the target channel using the supplied nick and password
|
||||
* @module join
|
||||
*/
|
||||
@@ -19,6 +19,7 @@ import {
|
||||
} from '../utility/_Channels.js';
|
||||
import {
|
||||
Errors,
|
||||
Info,
|
||||
SystemMOTDs,
|
||||
} from '../utility/_Constants.js';
|
||||
import {
|
||||
@@ -72,7 +73,7 @@ export async function run({
|
||||
}
|
||||
|
||||
// calling socket already in a channel
|
||||
// @todo multichannel update, will remove
|
||||
// @todo multichannel update: remove this block to allow multiple channels
|
||||
if (typeof socket.channel !== 'undefined') {
|
||||
return server.reply({
|
||||
cmd: 'warn',
|
||||
@@ -81,7 +82,6 @@ export async function run({
|
||||
channel: false, // @todo Multichannel, false for global event
|
||||
}, socket);
|
||||
}
|
||||
// end todo
|
||||
|
||||
// validates the user input for `nick`
|
||||
if (verifyNickname(nick, socket) !== true) {
|
||||
@@ -132,7 +132,6 @@ export async function run({
|
||||
});
|
||||
|
||||
if (userExists.length > 0) {
|
||||
// that nickname is already in that channel
|
||||
return server.reply({
|
||||
cmd: 'warn',
|
||||
text: 'Nickname taken',
|
||||
@@ -167,9 +166,10 @@ export async function run({
|
||||
socket.level = userInfo.level;
|
||||
socket.uType = userInfo.uType; /* @legacy */
|
||||
socket.channel = channel; /* @legacy */
|
||||
// @todo multi-channel patch
|
||||
// socket.channels.push(channel);
|
||||
socket.channels = [channel];
|
||||
|
||||
// initialize channels array if needed
|
||||
if (!socket.channels) socket.channels = [];
|
||||
if (!socket.channels.includes(channel)) socket.channels.push(channel);
|
||||
|
||||
// global mod perks
|
||||
if (isModerator(socket.level)) {
|
||||
@@ -177,14 +177,14 @@ export async function run({
|
||||
}
|
||||
|
||||
nicks.push(userInfo.nick); /* @legacy */
|
||||
users.push({ ...{ isme: true, isBot: socket.isBot }, ...userInfo });
|
||||
users.push({ ...{ isme: true, isBot: socket.isBot || false }, ...userInfo });
|
||||
|
||||
// reply with channel peer list
|
||||
server.reply({
|
||||
cmd: 'onlineSet',
|
||||
nicks, /* @legacy */
|
||||
users,
|
||||
channel, // @todo Multichannel (?)
|
||||
channel,
|
||||
}, socket);
|
||||
|
||||
let { motd } = channelSettings;
|
||||
@@ -193,8 +193,9 @@ export async function run({
|
||||
}
|
||||
|
||||
server.reply({
|
||||
cmd: 'info', // @todo Add numeric info code as `id`
|
||||
cmd: 'info',
|
||||
text: motd,
|
||||
id: Info.Core.MOTD,
|
||||
channel,
|
||||
}, socket);
|
||||
|
||||
@@ -236,6 +237,7 @@ export function restoreJoin({
|
||||
userid: socket.userid,
|
||||
isBot: socket.isBot,
|
||||
color: socket.color,
|
||||
flair: socket.flair,
|
||||
channel,
|
||||
};
|
||||
|
||||
@@ -274,18 +276,19 @@ export function restoreJoin({
|
||||
}
|
||||
|
||||
nicks.push(userInfo.nick); /* @legacy */
|
||||
users.push({ ...{ isme: true, isBot: socket.isBot }, ...userInfo });
|
||||
users.push({ ...{ isme: true, isBot: socket.isBot || false }, ...userInfo });
|
||||
|
||||
// reply with channel peer list
|
||||
server.reply({
|
||||
cmd: 'onlineSet',
|
||||
nicks, /* @legacy */
|
||||
users,
|
||||
channel, // @todo Multichannel (?)
|
||||
channel,
|
||||
}, socket);
|
||||
|
||||
socket.channel = channel; /* @legacy */
|
||||
socket.channels.push(channel);
|
||||
if (!socket.channels) socket.channels = [];
|
||||
if (!socket.channels.includes(channel)) socket.channels.push(channel);
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
@@ -0,0 +1,148 @@
|
||||
/* eslint no-param-reassign: 0 */
|
||||
/* eslint import/no-cycle: [0, { ignoreExternal: true }] */
|
||||
|
||||
/**
|
||||
* @author Marzavec ( https://github.com/marzavec )
|
||||
* @summary Leave target channel
|
||||
* @version 1.0.0
|
||||
* @description Leave the target channel
|
||||
* @module leave
|
||||
*/
|
||||
|
||||
import {
|
||||
getSession,
|
||||
} from './session.js';
|
||||
import {
|
||||
socketInChannel,
|
||||
} from '../utility/_Channels.js';
|
||||
import {
|
||||
Errors,
|
||||
} from '../utility/_Constants.js';
|
||||
|
||||
/**
|
||||
* Executes when invoked by a remote client
|
||||
* @param {Object} env - Environment object with references to core, server, socket & payload
|
||||
* @public
|
||||
* @return {void}
|
||||
*/
|
||||
export async function run({
|
||||
core, server, socket, payload,
|
||||
}) {
|
||||
// check for spam
|
||||
if (server.police.frisk(socket, 3)) {
|
||||
return server.reply({
|
||||
cmd: 'warn',
|
||||
text: 'You are leaving channels too fast. Wait a moment and try again.',
|
||||
id: Errors.Global.RATELIMIT,
|
||||
channel: false,
|
||||
}, socket);
|
||||
}
|
||||
|
||||
// check for required payload data
|
||||
if (typeof payload.channel !== 'string') {
|
||||
return server.reply({
|
||||
cmd: 'warn',
|
||||
text: 'Invalid channel specified.',
|
||||
id: Errors.Global.INVALID_PAYLOAD,
|
||||
channel: false,
|
||||
}, socket);
|
||||
}
|
||||
|
||||
const { channel } = payload;
|
||||
|
||||
// verify the user is actually in the channel they are trying to leave
|
||||
if (!socket.channels || !socket.channels.includes(channel)) {
|
||||
return server.reply({
|
||||
cmd: 'warn',
|
||||
text: 'You are not in that channel.',
|
||||
id: Errors.Global.INVALID_PAYLOAD,
|
||||
channel: false,
|
||||
}, socket);
|
||||
}
|
||||
|
||||
socket.channels = socket.channels.filter((c) => c !== channel);
|
||||
|
||||
// @todo Multichannel update
|
||||
if (socket.channel === channel) {
|
||||
socket.channel = socket.channels.length > 0 ? socket.channels[0] : undefined;
|
||||
}
|
||||
|
||||
const isDuplicate = socketInChannel(server, channel, socket);
|
||||
|
||||
if (isDuplicate === false) {
|
||||
server.broadcast({
|
||||
cmd: 'onlineRemove',
|
||||
nick: socket.nick,
|
||||
userid: socket.userid,
|
||||
channel,
|
||||
}, { channel });
|
||||
}
|
||||
|
||||
server.reply({
|
||||
cmd: 'session',
|
||||
restored: false,
|
||||
token: getSession(socket, core),
|
||||
channels: socket.channels,
|
||||
}, socket);
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
/**
|
||||
* Automatically executes once after server is ready to register this modules hooks
|
||||
* @param {Object} server - Reference to server environment object
|
||||
* @public
|
||||
* @return {void}
|
||||
*/
|
||||
export function initHooks(server) {
|
||||
server.registerHook('in', 'chat', this.runLeaveCheck.bind(this), 32);
|
||||
}
|
||||
|
||||
/**
|
||||
* Executes every time an incoming chat command is invoked
|
||||
* @param {Object} env - Environment object with references to core, server, socket & payload
|
||||
* @public
|
||||
* @return {(Object|boolean|string)} Object = same/altered payload,
|
||||
* false = suppress action,
|
||||
* string = error
|
||||
*/
|
||||
export function runLeaveCheck({
|
||||
core, server, socket, payload,
|
||||
}) {
|
||||
if (typeof payload.text !== 'string') {
|
||||
return false;
|
||||
}
|
||||
|
||||
if (payload.text.startsWith('/leave')) {
|
||||
this.run({
|
||||
core,
|
||||
server,
|
||||
socket,
|
||||
payload: {
|
||||
cmd: 'leave',
|
||||
channel: socket.channel, // @todo Mutlichannel
|
||||
},
|
||||
});
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
return payload;
|
||||
}
|
||||
|
||||
/**
|
||||
* Module meta information
|
||||
* @public
|
||||
* @typedef {Object} leave/info
|
||||
* @property {string} name - Module command name
|
||||
* @property {string} category - Module category name
|
||||
* @property {string} description - Information about module
|
||||
* @property {string} usage - Information about module usage
|
||||
*/
|
||||
export const info = {
|
||||
name: 'leave',
|
||||
category: 'core',
|
||||
description: 'Leave the target channel',
|
||||
usage: `
|
||||
API: { cmd: 'leave', channel: '<target channel>' }`,
|
||||
};
|
||||
+42
-26
@@ -1,11 +1,15 @@
|
||||
/**
|
||||
* @author Marzavec ( https://github.com/marzavec )
|
||||
* @summary Get stats
|
||||
* @version 1.0.0
|
||||
* @version 1.1.0
|
||||
* @description Sends back current server stats to the calling client
|
||||
* @module morestats
|
||||
*/
|
||||
|
||||
import {
|
||||
Info,
|
||||
} from '../utility/_Constants.js';
|
||||
|
||||
/**
|
||||
* Format input time into string
|
||||
* @param {Date} time - Subject date
|
||||
@@ -42,26 +46,20 @@ export async function run({ core, server, socket }) {
|
||||
// gather connection and channel count
|
||||
const ips = {};
|
||||
const channels = {};
|
||||
// @todo use public channel flag
|
||||
const publicChanCounts = {
|
||||
lounge: 0,
|
||||
meta: 0,
|
||||
math: 0,
|
||||
physics: 0,
|
||||
chemistry: 0,
|
||||
technology: 0,
|
||||
programming: 0,
|
||||
games: 0,
|
||||
banana: 0,
|
||||
chinese: 0,
|
||||
};
|
||||
const publicChanCounts = {};
|
||||
|
||||
if (core.appConfig.data.publicChannels) {
|
||||
core.appConfig.data.publicChannels.forEach((channel) => {
|
||||
publicChanCounts[channel] = 0;
|
||||
});
|
||||
}
|
||||
|
||||
// @todo code resuage between here and `session`; should share exported function
|
||||
server.clients.forEach((client) => {
|
||||
if (client.channel) {
|
||||
channels[client.channel] = true;
|
||||
ips[client.address] = true;
|
||||
if (typeof publicChanCounts[client.channel] !== 'undefined') {
|
||||
|
||||
if (Object.prototype.hasOwnProperty.call(publicChanCounts, client.channel)) {
|
||||
publicChanCounts[client.channel] += 1;
|
||||
}
|
||||
}
|
||||
@@ -77,9 +75,34 @@ export async function run({ core, server, socket }) {
|
||||
const stats = core.stats.get('stats-requested') || 0;
|
||||
const uptime = formatTime(process.hrtime(core.stats.get('start-time')));
|
||||
|
||||
let replyText = '# Server Statistics\n';
|
||||
replyText += '| Metric | Value |\n';
|
||||
replyText += '| :--- | --- |\n';
|
||||
replyText += `| **Current Connections** | ${uniqueClientCount} |\n`;
|
||||
replyText += `| **Current Channels** | ${uniqueChannels} |\n`;
|
||||
replyText += `| **Users Joined** | ${joins} |\n`;
|
||||
replyText += `| **Invites Sent** | ${invites} |\n`;
|
||||
replyText += `| **Messages Sent** | ${messages} |\n`;
|
||||
replyText += `| **Users Banned** | ${banned} |\n`;
|
||||
replyText += `| **Users Kicked** | ${kicked} |\n`;
|
||||
replyText += `| **Stats Requested** | ${stats} |\n`;
|
||||
replyText += `| **Server Uptime** | ${uptime} |\n\n`;
|
||||
|
||||
const sortedPublicChannels = Object.keys(publicChanCounts)
|
||||
.map((channel) => ({ name: channel, count: publicChanCounts[channel] }))
|
||||
.sort((a, b) => b.count - a.count);
|
||||
|
||||
replyText += '## Public Channels\n';
|
||||
replyText += '| Channel | Users |\n';
|
||||
replyText += '| :--- | --- |\n';
|
||||
|
||||
sortedPublicChannels.forEach((channelObj) => {
|
||||
replyText += `| ?${channelObj.name} | ${channelObj.count} |\n`;
|
||||
});
|
||||
|
||||
// dispatch info
|
||||
server.reply({
|
||||
cmd: 'info', // @todo Add numeric info code as `id`
|
||||
cmd: 'info',
|
||||
users: uniqueClientCount,
|
||||
chans: uniqueChannels,
|
||||
joins,
|
||||
@@ -90,15 +113,8 @@ export async function run({ core, server, socket }) {
|
||||
stats,
|
||||
uptime,
|
||||
public: publicChanCounts,
|
||||
text: `current-connections: ${uniqueClientCount}
|
||||
current-channels: ${uniqueChannels}
|
||||
users-joined: ${joins}
|
||||
invites-sent: ${invites}
|
||||
messages-sent: ${messages}
|
||||
users-banned: ${banned}
|
||||
users-kicked: ${kicked}
|
||||
stats-requested: ${stats}
|
||||
server-uptime: ${uptime}`,
|
||||
text: replyText,
|
||||
id: Info.Core.STATS_FULL,
|
||||
channel: socket.channel, // @todo Multichannel
|
||||
}, socket);
|
||||
|
||||
|
||||
+64
-57
@@ -3,7 +3,7 @@
|
||||
/**
|
||||
* @author Marzavec ( https://github.com/marzavec )
|
||||
* @summary Create or restore session
|
||||
* @version 1.0.0
|
||||
* @version 1.1.0
|
||||
* @description Restore previous state by session or create new session
|
||||
* @module session
|
||||
*/
|
||||
@@ -15,10 +15,8 @@ import jsonwebtoken from 'jsonwebtoken';
|
||||
import {
|
||||
isModerator,
|
||||
verifyNickname,
|
||||
levels,
|
||||
} from '../utility/_UAC.js';
|
||||
import {
|
||||
Errors,
|
||||
} from '../utility/_Constants.js';
|
||||
import {
|
||||
restoreJoin,
|
||||
} from './join.js';
|
||||
@@ -36,7 +34,7 @@ export function getSession(socket, core) {
|
||||
channel: socket.channel,
|
||||
channels: socket.channels,
|
||||
color: socket.color,
|
||||
isBot: socket.isBot,
|
||||
isBot: socket.isBot || false,
|
||||
level: socket.level,
|
||||
nick: socket.nick,
|
||||
flair: socket.flair,
|
||||
@@ -58,14 +56,39 @@ export function getSession(socket, core) {
|
||||
*/
|
||||
function notifyFailure(server, socket) {
|
||||
server.reply({
|
||||
cmd: 'error',
|
||||
id: Errors.Session.BAD_SESSION,
|
||||
text: 'Invalid session',
|
||||
cmd: 'session',
|
||||
restored: false,
|
||||
token: '',
|
||||
channels: [],
|
||||
}, socket);
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
/**
|
||||
* Re-validates the user's level against the current server config
|
||||
* Prevents clients from using old tokens to retain privileges
|
||||
* @param {string} trip
|
||||
* @param {object} appConfig
|
||||
* @returns {number}
|
||||
*/
|
||||
function validateLevel(trip, appConfig) {
|
||||
if (!trip) return levels.default;
|
||||
|
||||
// check admin
|
||||
if (trip === appConfig.adminTrip) {
|
||||
return levels.admin;
|
||||
}
|
||||
|
||||
// check global Mods
|
||||
const isGlobalMod = appConfig.globalMods.some((mod) => mod.trip === trip);
|
||||
if (isGlobalMod) {
|
||||
return levels.moderator;
|
||||
}
|
||||
|
||||
return levels.default;
|
||||
}
|
||||
|
||||
/**
|
||||
* Executes when invoked by a remote client
|
||||
* @param {Object} env - Environment object with references to core, server, socket & payload
|
||||
@@ -75,6 +98,14 @@ function notifyFailure(server, socket) {
|
||||
export async function run({
|
||||
core, server, socket, payload,
|
||||
}) {
|
||||
if (typeof socket.hcProtocol === 'undefined') socket.hcProtocol = 2;
|
||||
if (typeof socket.userid === 'undefined') socket.userid = Math.floor(Math.random() * 9999999999999);
|
||||
if (typeof socket.hash === 'undefined') socket.hash = server.getSocketHash(socket);
|
||||
|
||||
if (server.police.frisk(socket.address)) {
|
||||
return notifyFailure(server, socket);
|
||||
}
|
||||
|
||||
if (typeof payload.token === 'undefined') {
|
||||
return notifyFailure(server, socket);
|
||||
}
|
||||
@@ -86,49 +117,25 @@ export async function run({
|
||||
return notifyFailure(server, socket);
|
||||
}
|
||||
|
||||
// validate session
|
||||
if (typeof session.channel !== 'string') {
|
||||
return notifyFailure(server, socket);
|
||||
}
|
||||
if (typeof session.channel !== 'string') return notifyFailure(server, socket);
|
||||
if (Array.isArray(session.channels) === false) return notifyFailure(server, socket);
|
||||
if (typeof session.color !== 'string' && typeof session.color !== 'boolean') return notifyFailure(server, socket);
|
||||
if (typeof session.isBot !== 'boolean') return notifyFailure(server, socket);
|
||||
if (typeof session.level !== 'number') return notifyFailure(server, socket);
|
||||
if (verifyNickname(session.nick) === false) return notifyFailure(server, socket);
|
||||
if (typeof session.trip !== 'string') return notifyFailure(server, socket);
|
||||
if (typeof session.userid !== 'number') return notifyFailure(server, socket);
|
||||
if (typeof session.uType !== 'string') return notifyFailure(server, socket);
|
||||
if (typeof session.muzzled !== 'boolean') return notifyFailure(server, socket);
|
||||
if (typeof session.banned !== 'boolean') return notifyFailure(server, socket);
|
||||
|
||||
if (Array.isArray(session.channels) === false) {
|
||||
return notifyFailure(server, socket);
|
||||
}
|
||||
const realLevel = validateLevel(session.trip, core.appConfig.data);
|
||||
|
||||
if (typeof session.color !== 'string' && typeof session.color !== 'boolean') {
|
||||
return notifyFailure(server, socket);
|
||||
if (session.level >= levels.moderator) {
|
||||
if (realLevel < session.level) {
|
||||
session.level = realLevel;
|
||||
session.uType = 'user';
|
||||
}
|
||||
|
||||
if (typeof session.isBot !== 'boolean') {
|
||||
return notifyFailure(server, socket);
|
||||
}
|
||||
|
||||
if (typeof session.level !== 'number') {
|
||||
return notifyFailure(server, socket);
|
||||
}
|
||||
|
||||
if (verifyNickname(session.nick) === false) {
|
||||
return notifyFailure(server, socket);
|
||||
}
|
||||
|
||||
if (typeof session.trip !== 'string') {
|
||||
return notifyFailure(server, socket);
|
||||
}
|
||||
|
||||
if (typeof session.userid !== 'number') {
|
||||
return notifyFailure(server, socket);
|
||||
}
|
||||
|
||||
if (typeof session.uType !== 'string') {
|
||||
return notifyFailure(server, socket);
|
||||
}
|
||||
|
||||
if (typeof session.muzzled !== 'boolean') {
|
||||
return notifyFailure(server, socket);
|
||||
}
|
||||
|
||||
if (typeof session.banned !== 'boolean') {
|
||||
return notifyFailure(server, socket);
|
||||
}
|
||||
|
||||
// populate socket info with validated session
|
||||
@@ -152,23 +159,23 @@ export async function run({
|
||||
socket.hash = server.getSocketHash(socket);
|
||||
socket.hcProtocol = 2;
|
||||
|
||||
// dispatch info
|
||||
server.reply({
|
||||
cmd: 'session',
|
||||
restored: true,
|
||||
token: getSession(socket, core),
|
||||
channels: socket.channels,
|
||||
}, socket);
|
||||
|
||||
// attempt to restore all channels in the session
|
||||
for (let i = 0, j = session.channels.length; i < j; i += 1) {
|
||||
restoreJoin({
|
||||
core,
|
||||
server,
|
||||
socket,
|
||||
channel: session.channels[i],
|
||||
}, true);
|
||||
});
|
||||
}
|
||||
|
||||
server.reply({
|
||||
cmd: 'session',
|
||||
restored: true,
|
||||
token: getSession(socket, core),
|
||||
channels: socket.channels,
|
||||
}, socket);
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
|
||||
@@ -6,6 +6,10 @@
|
||||
* @module stats
|
||||
*/
|
||||
|
||||
import {
|
||||
Info,
|
||||
} from '../utility/_Constants.js';
|
||||
|
||||
/**
|
||||
* Executes when invoked by a remote client
|
||||
* @param {Object} env - Environment object with references to core, server, socket & payload
|
||||
@@ -21,7 +25,7 @@ export async function run({ core, server, socket }) {
|
||||
// gather connection and channel count
|
||||
let ips = {};
|
||||
let channels = {};
|
||||
// for (const client of server.clients) {
|
||||
|
||||
server.clients.forEach((client) => {
|
||||
if (client.channel) {
|
||||
channels[client.channel] = true;
|
||||
@@ -37,8 +41,9 @@ export async function run({ core, server, socket }) {
|
||||
|
||||
// dispatch info
|
||||
server.reply({
|
||||
cmd: 'info', // @todo Add numeric info code as `id`
|
||||
cmd: 'info',
|
||||
text: `${uniqueClientCount} unique IPs in ${uniqueChannels} channels`,
|
||||
id: Info.Core.STATS_BASIC,
|
||||
channel: socket.channel, // @todo Multichannel
|
||||
}, socket);
|
||||
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
/**
|
||||
* @author MinusGix ( https://github.com/MinusGix )
|
||||
* @summary Change target message
|
||||
* @version v1.0.0
|
||||
* @version v1.1.0
|
||||
* @description Will alter a previously sent message using that message's customId
|
||||
* @module updateMessage
|
||||
*/
|
||||
@@ -13,6 +13,9 @@ import {
|
||||
isAdmin,
|
||||
isModerator,
|
||||
} from '../utility/_UAC.js';
|
||||
import {
|
||||
Errors,
|
||||
} from '../utility/_Constants.js';
|
||||
import {
|
||||
ACTIVE_MESSAGES,
|
||||
MAX_MESSAGE_ID_LENGTH,
|
||||
@@ -52,39 +55,39 @@ export async function run({
|
||||
return server.police.frisk(socket, 13);
|
||||
}
|
||||
|
||||
if (mode === 'overwrite') {
|
||||
// sanitize input regardless of mode
|
||||
text = parseText(text);
|
||||
|
||||
if (text === '') {
|
||||
// allow empty overwrite (clearing message), otherwise block empty text
|
||||
if (mode === 'overwrite' && text === '') {
|
||||
text = '\u0000';
|
||||
}
|
||||
}
|
||||
|
||||
if (!text) {
|
||||
} else if (!text) {
|
||||
return server.police.frisk(socket, 13);
|
||||
}
|
||||
|
||||
// TODO: What score should we use for this? It isn't as space filling as chat messages.
|
||||
// But we also don't want a massive growing message.
|
||||
// Or flashing between huge and small. Etc.
|
||||
const score = text.length / 83 / 4;
|
||||
if (server.police.frisk(socket, score)) {
|
||||
return server.reply({
|
||||
cmd: 'warn',
|
||||
text: 'You are sending too much text. Wait a moment and try again.',
|
||||
id: Errors.Global.RATELIMIT,
|
||||
channel: socket.channel,
|
||||
}, socket);
|
||||
}
|
||||
|
||||
let message;
|
||||
for (let i = 0; i < ACTIVE_MESSAGES.length; i += 1) {
|
||||
const msg = ACTIVE_MESSAGES[i];
|
||||
|
||||
if (msg.userid === socket.userid && msg.customId === customId) {
|
||||
message = ACTIVE_MESSAGES[i];
|
||||
if (mode === 'complete') {
|
||||
ACTIVE_MESSAGES[i].toDelete = true;
|
||||
}
|
||||
break;
|
||||
}
|
||||
}
|
||||
// find the target message
|
||||
const message = ACTIVE_MESSAGES.find(
|
||||
(msg) => msg.userid === socket.userid && msg.customId === customId,
|
||||
);
|
||||
|
||||
if (!message) {
|
||||
return server.police.frisk(socket, 6);
|
||||
}
|
||||
|
||||
if (mode === 'complete') {
|
||||
message.toDelete = true;
|
||||
}
|
||||
|
||||
const outgoingPayload = {
|
||||
cmd: 'updateMessage',
|
||||
userid: socket.userid,
|
||||
|
||||
@@ -32,6 +32,7 @@ export async function run({ server, socket, payload }) {
|
||||
cmd: 'onlineRemove',
|
||||
nick: socket.nick,
|
||||
userid: socket.userid,
|
||||
channel: socket.channel,
|
||||
}, { channel: socket.channel });
|
||||
}
|
||||
}
|
||||
|
||||
+6
-3
@@ -1,7 +1,7 @@
|
||||
/**
|
||||
* @author Marzavec ( https://github.com/marzavec )
|
||||
* @summary Ban a user
|
||||
* @version 1.0.0
|
||||
* @version 1.1.0
|
||||
* @description Bans target user by name
|
||||
* @module ban
|
||||
*/
|
||||
@@ -12,6 +12,7 @@ import {
|
||||
} from '../utility/_UAC.js';
|
||||
import {
|
||||
Errors,
|
||||
Info,
|
||||
} from '../utility/_Constants.js';
|
||||
import {
|
||||
findUser,
|
||||
@@ -71,16 +72,18 @@ export async function run({
|
||||
|
||||
// notify normal users
|
||||
server.broadcast({
|
||||
cmd: 'info', // @todo Add numeric info code as `id`
|
||||
cmd: 'info',
|
||||
text: `Banned ${targetNick}`,
|
||||
id: Info.Mod.BANNED,
|
||||
user: getUserDetails(targetUser),
|
||||
channel: socket.channel, // @todo Multichannel
|
||||
}, { channel: socket.channel, level: (level) => isModerator(level) });
|
||||
|
||||
// notify mods
|
||||
server.broadcast({
|
||||
cmd: 'info', // @todo Add numeric info code as `id`
|
||||
cmd: 'info',
|
||||
text: `${socket.nick}#${socket.trip} banned ${targetNick} in ${payload.channel}, userhash: ${targetUser.hash}`,
|
||||
id: Info.Mod.BANNED_DETAILED,
|
||||
channel: socket.channel, // @todo Multichannel
|
||||
inChannel: payload.channel,
|
||||
user: getUserDetails(targetUser),
|
||||
|
||||
@@ -1,12 +1,15 @@
|
||||
/**
|
||||
* @author Marzavec ( https://github.com/marzavec )
|
||||
* @summary Disables the captcha
|
||||
* @version 1.0.0
|
||||
* @version 1.1.0
|
||||
* @description Disables the captcha on the channel specified in the channel property,
|
||||
* default is current channel
|
||||
* @module disablecaptcha
|
||||
*/
|
||||
|
||||
import {
|
||||
Info,
|
||||
} from '../utility/_Constants.js';
|
||||
import {
|
||||
isModerator,
|
||||
} from '../utility/_UAC.js';
|
||||
@@ -51,8 +54,9 @@ export async function run({
|
||||
|
||||
if (!core.captchas[targetChannel]) {
|
||||
return server.reply({
|
||||
cmd: 'info', // @todo Add numeric info code as `id`
|
||||
cmd: 'info',
|
||||
text: 'Captcha is not enabled.',
|
||||
id: Info.Captcha.NOT_ENABLED,
|
||||
channel: socket.channel, // @todo Multichannel
|
||||
}, socket);
|
||||
}
|
||||
@@ -60,8 +64,9 @@ export async function run({
|
||||
core.captchas[targetChannel] = false;
|
||||
|
||||
server.broadcast({
|
||||
cmd: 'info', // @todo Add numeric info code as `id`
|
||||
cmd: 'info',
|
||||
text: `Captcha disabled on: ${targetChannel}`,
|
||||
id: Info.Captcha.DISABLED,
|
||||
channel: false, // @todo Multichannel, false for global info
|
||||
}, { channel: targetChannel, level: isModerator });
|
||||
|
||||
|
||||
@@ -4,8 +4,8 @@
|
||||
/**
|
||||
* @author OpSimple ( https://github.com/OpSimple )
|
||||
* @summary Muzzle a user
|
||||
* @version 1.0.0
|
||||
* @description Globally shadow mute a connection. Optional allies array will see muted messages.
|
||||
* @version 1.1.0
|
||||
* @description Globally shadow mute a connection. Optional allies array will see muted messages
|
||||
* @module dumb
|
||||
*/
|
||||
|
||||
@@ -17,6 +17,7 @@ import {
|
||||
} from '../utility/_Channels.js';
|
||||
import {
|
||||
Errors,
|
||||
Info,
|
||||
} from '../utility/_Constants.js';
|
||||
import {
|
||||
legacyInviteReply,
|
||||
@@ -24,7 +25,7 @@ import {
|
||||
} from '../utility/_LegacyFunctions.js';
|
||||
|
||||
/**
|
||||
* Returns the channel that should be invited to.
|
||||
* Returns the channel that should be invited to
|
||||
* @param {any} channel
|
||||
* @private
|
||||
* @return {string}
|
||||
@@ -130,8 +131,9 @@ export async function run({
|
||||
|
||||
// notify mods
|
||||
server.broadcast({
|
||||
cmd: 'info', // @todo Add numeric info code as `id`
|
||||
cmd: 'info',
|
||||
text: `${socket.nick}#${socket.trip} muzzled ${targetUser.nick} in ${payload.channel}, userhash: ${targetUser.hash}`,
|
||||
id: Info.Mod.MUZZLED_DETAILED,
|
||||
channel: false, // @todo Multichannel, false for global
|
||||
}, { level: isModerator });
|
||||
|
||||
@@ -176,6 +178,7 @@ export function chatCheck({
|
||||
channel: socket.channel,
|
||||
text: payload.text,
|
||||
level: socket.level,
|
||||
flair: socket.flair,
|
||||
};
|
||||
|
||||
if (socket.trip) {
|
||||
@@ -201,7 +204,7 @@ export function chatCheck({
|
||||
}
|
||||
|
||||
/**
|
||||
* Blanket "spam" protection.
|
||||
* Blanket "spam" protection
|
||||
* May expose the ratelimiting lines from `chat` and use that
|
||||
* @todo one day #lazydev
|
||||
*/
|
||||
|
||||
@@ -3,7 +3,7 @@
|
||||
/**
|
||||
* @author Marzavec ( https://github.com/marzavec )
|
||||
* @summary Enables the captcha
|
||||
* @version 1.0.0
|
||||
* @version 1.1.0
|
||||
* @description Enables the captcha on the channel specified in the channel property,
|
||||
* default is current channel
|
||||
* @module enablecaptcha
|
||||
@@ -26,6 +26,7 @@ import {
|
||||
} from '../utility/_LegacyFunctions.js';
|
||||
import {
|
||||
Errors,
|
||||
Info,
|
||||
} from '../utility/_Constants.js';
|
||||
|
||||
/**
|
||||
@@ -68,8 +69,9 @@ export async function run({
|
||||
|
||||
if (core.captchas[targetChannel]) {
|
||||
return server.reply({
|
||||
cmd: 'info', // @todo Add numeric info code as `id`
|
||||
cmd: 'info',
|
||||
text: 'Captcha is already enabled.',
|
||||
id: Info.Captcha.ALREADY_ENABLED,
|
||||
channel: socket.channel, // @todo Multichannel
|
||||
}, socket);
|
||||
}
|
||||
@@ -77,8 +79,9 @@ export async function run({
|
||||
core.captchas[targetChannel] = true;
|
||||
|
||||
server.broadcast({
|
||||
cmd: 'info', // @todo Add numeric info code as `id`
|
||||
cmd: 'info',
|
||||
text: `Captcha enabled on: ${targetChannel}`,
|
||||
id: Info.Captcha.ENABLED,
|
||||
channel: socket.channel, // @todo Multichannel, false for global info
|
||||
}, { channel: socket.channel, level: isModerator });
|
||||
|
||||
@@ -163,6 +166,10 @@ export function chatCheck({
|
||||
export function joinCheck({
|
||||
core, server, socket, payload,
|
||||
}) {
|
||||
if (typeof payload === 'undefined' || typeof payload.channel === 'undefined') {
|
||||
return false;
|
||||
}
|
||||
|
||||
// check if channel has captcha enabled
|
||||
if (core.captchas[payload.channel] !== true) {
|
||||
return payload;
|
||||
@@ -170,7 +177,7 @@ export function joinCheck({
|
||||
|
||||
// `join` is the legacy entry point, check if it needs to be upgraded
|
||||
const origPayload = { ...payload };
|
||||
if (typeof socket.hcProtocol === 'undefined') {
|
||||
if (typeof socket.hcProtocol === 'undefined' || socket.hcProtocol === 1) {
|
||||
payload = upgradeLegacyJoin(server, socket, payload);
|
||||
}
|
||||
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
/**
|
||||
* @author Marzavec ( https://github.com/marzavec )
|
||||
* @summary Color a user
|
||||
* @version 1.0.0
|
||||
* @version 1.1.0
|
||||
* @description Forces a user nick to become a certain color
|
||||
* @module forcecolor
|
||||
*/
|
||||
@@ -16,15 +16,9 @@ import {
|
||||
import {
|
||||
findUser,
|
||||
} from '../utility/_Channels.js';
|
||||
|
||||
/**
|
||||
* Validate a string as a valid hex color string
|
||||
* @param {string} color - Color string to validate
|
||||
* @private
|
||||
* @todo Move into utility module
|
||||
* @return {boolean}
|
||||
*/
|
||||
const verifyColor = (color) => /(^[0-9A-F]{6}$)|(^[0-9A-F]{3}$)/i.test(color);
|
||||
import {
|
||||
verifyColor,
|
||||
} from '../utility/_Text.js';
|
||||
|
||||
/**
|
||||
* Executes when invoked by a remote client
|
||||
@@ -198,5 +192,5 @@ export const info = {
|
||||
description: 'Forces a user nick to become a certain color',
|
||||
usage: `
|
||||
API: { cmd: 'forcecolor', nick: '<target nick>', color: '<color as hex>' }
|
||||
Text: /forcecolor <target nick> <color as hex>`,
|
||||
Text: /forcecolor <target nick> <color as hex>`,
|
||||
};
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
/**
|
||||
* @author Marzavec ( https://github.com/marzavec )
|
||||
* @summary Sends a hack request to the target nick.
|
||||
* @summary Sends a hack request to the target nick
|
||||
* @version 1.0.0
|
||||
* @description Please note that the term 'hack' is used in jest here
|
||||
* @module hack
|
||||
@@ -39,7 +39,7 @@ export async function run({
|
||||
}
|
||||
|
||||
// check for spam
|
||||
if (server.police.frisk(socket, 6)) {
|
||||
if (server.police.frisk(socket, 4)) {
|
||||
return server.reply({
|
||||
cmd: 'warn',
|
||||
text: 'You are sending hack requests too fast. Wait a moment before trying again.',
|
||||
@@ -50,13 +50,13 @@ export async function run({
|
||||
|
||||
// verify user input
|
||||
// if this is a legacy client add missing params to payload
|
||||
if (typeof payload.channel !== 'string') payload.channel = socket.channel;
|
||||
|
||||
if (socket.hcProtocol === 1) {
|
||||
if (typeof socket.channel === 'undefined' || typeof payload.nick !== 'string') {
|
||||
return true;
|
||||
}
|
||||
|
||||
payload.channel = socket.channel; // eslint-disable-line no-param-reassign
|
||||
} else if (typeof payload.userid !== 'number' || typeof payload.channel !== 'string') {
|
||||
} else if (typeof payload.userid !== 'number') {
|
||||
return true;
|
||||
}
|
||||
|
||||
|
||||
+39
-40
@@ -1,17 +1,19 @@
|
||||
/**
|
||||
* @author Marzavec ( https://github.com/marzavec )
|
||||
* @summary Give da boot
|
||||
* @version 1.0.0
|
||||
* @version 1.1.0
|
||||
* @description Silently forces target client(s) into another channel
|
||||
* @module kick
|
||||
*/
|
||||
|
||||
import {
|
||||
isModerator,
|
||||
isChannelModerator,
|
||||
getUserDetails,
|
||||
} from '../utility/_UAC.js';
|
||||
import {
|
||||
Errors,
|
||||
Info,
|
||||
} from '../utility/_Constants.js';
|
||||
import {
|
||||
findUsers,
|
||||
@@ -31,23 +33,17 @@ export async function run({
|
||||
return server.police.frisk(socket, 10);
|
||||
}
|
||||
|
||||
// check user input
|
||||
if (socket.hcProtocol === 1) {
|
||||
if (typeof payload.nick !== 'string') {
|
||||
if (typeof payload.nick !== 'object' && !Array.isArray(payload.nick)) {
|
||||
return true;
|
||||
}
|
||||
payload.channel = socket.channel; // eslint-disable-line no-param-reassign
|
||||
}
|
||||
|
||||
payload.channel = socket.channel; // eslint-disable-line no-param-reassign
|
||||
} else if (typeof payload.userid !== 'number') {
|
||||
// @todo create multi-ban ui
|
||||
if (typeof payload.userid !== 'object' && !Array.isArray(payload.userid)) {
|
||||
if (typeof payload.nick !== 'string') {
|
||||
// check user input
|
||||
const hasValidNick = typeof payload.nick === 'string' || Array.isArray(payload.nick);
|
||||
const hasValidUserid = typeof payload.userid === 'number' || Array.isArray(payload.userid);
|
||||
|
||||
if (!hasValidNick && !hasValidUserid) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// find target user(s)
|
||||
const badClients = findUsers(server, payload);
|
||||
@@ -62,8 +58,8 @@ export async function run({
|
||||
|
||||
// check if found targets are kickable, add them to the list if they are
|
||||
const kicked = [];
|
||||
for (let i = 0, j = badClients.length; i < j; i += 1) {
|
||||
if (badClients[i].level >= socket.level) {
|
||||
badClients.forEach((client) => {
|
||||
if (client.level >= socket.level) {
|
||||
server.reply({
|
||||
cmd: 'warn',
|
||||
text: 'Cannot kick other users with the same level, how rude',
|
||||
@@ -71,61 +67,64 @@ export async function run({
|
||||
channel: socket.channel, // @todo Multichannel
|
||||
}, socket);
|
||||
} else {
|
||||
kicked.push(badClients[i]);
|
||||
}
|
||||
kicked.push(client);
|
||||
}
|
||||
});
|
||||
|
||||
if (kicked.length === 0) {
|
||||
return true;
|
||||
}
|
||||
|
||||
let destChannel;
|
||||
let destChannel = Math.random().toString(36).substr(2, 8);
|
||||
|
||||
if (typeof payload.to === 'string' && !!payload.to.trim()) {
|
||||
destChannel = payload.to;
|
||||
} else {
|
||||
destChannel = Math.random().toString(36).substr(2, 8);
|
||||
if (isModerator(socket.level)) {
|
||||
destChannel = payload.to.trim();
|
||||
}
|
||||
}
|
||||
|
||||
// Announce the kicked clients arrival in destChannel and that they were kicked
|
||||
// Before they arrive, so they don't see they got moved
|
||||
for (let i = 0; i < kicked.length; i += 1) {
|
||||
// announce the kicked clients arrival in destChannel and that they were kicked
|
||||
// before they arrive, so they don't see they got moved
|
||||
kicked.forEach((client) => {
|
||||
server.broadcast({
|
||||
...getUserDetails(kicked[i]),
|
||||
...getUserDetails(client),
|
||||
...{
|
||||
cmd: 'onlineAdd',
|
||||
channel: destChannel, // @todo Multichannel
|
||||
},
|
||||
}, { channel: destChannel });
|
||||
}
|
||||
});
|
||||
|
||||
// Move all kicked clients to the new channel
|
||||
for (let i = 0; i < kicked.length; i += 1) {
|
||||
// move all kicked clients to the new channel
|
||||
kicked.forEach((client) => {
|
||||
// @todo multi-channel update
|
||||
kicked[i].channel = destChannel;
|
||||
client.channel = destChannel;
|
||||
|
||||
server.broadcast({
|
||||
cmd: 'info', // @todo Add numeric info code as `id`
|
||||
text: `${kicked[i].nick} was banished to ?${destChannel}`,
|
||||
cmd: 'info',
|
||||
text: `${client.nick} was banished to ?${destChannel}`,
|
||||
id: Info.Mod.KICKED_DETAILED,
|
||||
channel: socket.channel, // @todo Multichannel
|
||||
}, { channel: socket.channel, level: isChannelModerator });
|
||||
|
||||
console.log(`${socket.nick} [${socket.trip}] kicked ${kicked[i].nick} in ${socket.channel} to ${destChannel} `);
|
||||
}
|
||||
console.log(`${socket.nick} [${socket.trip}] kicked ${client.nick} in ${socket.channel} to ${destChannel} `);
|
||||
});
|
||||
|
||||
// broadcast client leave event
|
||||
for (let i = 0, j = kicked.length; i < j; i += 1) {
|
||||
kicked.forEach((client) => {
|
||||
server.broadcast({
|
||||
cmd: 'onlineRemove',
|
||||
userid: kicked[i].userid,
|
||||
nick: kicked[i].nick,
|
||||
userid: client.userid,
|
||||
nick: client.nick,
|
||||
channel: socket.channel, // @todo Multichannel
|
||||
}, { channel: socket.channel });
|
||||
}
|
||||
});
|
||||
|
||||
// publicly broadcast kick event
|
||||
server.broadcast({
|
||||
cmd: 'info', // @todo Add numeric info code as `id`
|
||||
cmd: 'info',
|
||||
text: `Kicked ${kicked.map((k) => k.nick).join(', ')}`,
|
||||
id: Info.Mod.KICKED,
|
||||
channel: socket.channel, // @todo Multichannel
|
||||
}, { channel: socket.channel, level: (level) => isChannelModerator(level) });
|
||||
|
||||
@@ -163,13 +162,12 @@ export function runKickCheck({
|
||||
if (payload.text.startsWith('/kick ')) {
|
||||
const input = payload.text.split(' ');
|
||||
|
||||
// If there is no nick parameter
|
||||
const nick = input[1];
|
||||
if (!nick || !nick.replace(/[^a-zA-Z0-9_]/g, '')) {
|
||||
server.reply({
|
||||
cmd: 'warn',
|
||||
text: 'Failed to kick: Missing name. Refer to `/help kick` for instructions on how to use this command.',
|
||||
id: Errors.Global.UNKNOWN_USER, // this is wrong #lazydev
|
||||
id: Errors.Kick.MISSING_NICK,
|
||||
channel: socket.channel, // @todo Multichannel
|
||||
}, socket);
|
||||
|
||||
@@ -207,5 +205,6 @@ export const info = {
|
||||
description: 'Silently forces target client(s) into another channel. `nick` may be string or array of strings',
|
||||
usage: `
|
||||
API: { cmd: 'kick', nick: '<target nick>', to: '<optional target channel>' }
|
||||
API: { cmd: 'kick', userid: <target id>, to: '<optional target channel>' }
|
||||
Text: /kick <target nick>`,
|
||||
};
|
||||
|
||||
+92
-28
@@ -3,16 +3,17 @@
|
||||
/**
|
||||
* @author Marzavec ( https://github.com/marzavec )
|
||||
* @summary Locks the channel
|
||||
* @version 1.0.0
|
||||
* @version 1.1.0
|
||||
* @description Locks a channel preventing default levels from joining
|
||||
* @module lockroom
|
||||
*/
|
||||
|
||||
import {
|
||||
isTrustedUser,
|
||||
isModerator,
|
||||
isChannelModerator,
|
||||
verifyNickname,
|
||||
getUserPerms,
|
||||
levels,
|
||||
} from '../utility/_UAC.js';
|
||||
import {
|
||||
upgradeLegacyJoin,
|
||||
@@ -20,6 +21,7 @@ import {
|
||||
} from '../utility/_LegacyFunctions.js';
|
||||
import {
|
||||
Errors,
|
||||
Info,
|
||||
} from '../utility/_Constants.js';
|
||||
import {
|
||||
canJoinChannel,
|
||||
@@ -69,41 +71,74 @@ export async function init(core) {
|
||||
export async function run({
|
||||
core, server, socket, payload,
|
||||
}) {
|
||||
// increase rate limit chance and ignore if not admin or mod
|
||||
if (!isModerator(socket.level)) {
|
||||
return server.police.frisk(socket, 10);
|
||||
}
|
||||
|
||||
let targetChannel;
|
||||
|
||||
if (typeof payload.channel !== 'string') {
|
||||
if (typeof socket.channel !== 'string') { // @todo Multichannel
|
||||
return false; // silently fail
|
||||
}
|
||||
|
||||
targetChannel = socket.channel;
|
||||
} else {
|
||||
targetChannel = payload.channel;
|
||||
// increase rate limit chance and ignore if not admin or mod
|
||||
if (!isChannelModerator(socket.level)) {
|
||||
return server.police.frisk(socket, 10);
|
||||
}
|
||||
|
||||
if (core.locked[targetChannel]) {
|
||||
const targetChannel = socket.channel;
|
||||
|
||||
if (typeof core.locked[targetChannel] !== 'undefined' && core.locked[targetChannel] !== false) {
|
||||
return server.reply({
|
||||
cmd: 'info', // @todo Add numeric info code as `id`
|
||||
cmd: 'warn',
|
||||
text: 'Channel is already locked.',
|
||||
channel: socket.channel, // @todo Multichannel
|
||||
id: Errors.Global.INVALID_DATA,
|
||||
channel: targetChannel, // @todo Multichannel
|
||||
}, socket);
|
||||
}
|
||||
|
||||
// apply lock flag to channel list
|
||||
core.locked[targetChannel] = true;
|
||||
let lockLevel = socket.level;
|
||||
if (typeof payload.level !== 'undefined') {
|
||||
if (typeof payload.level === 'string') {
|
||||
if (typeof levels[payload.level] === 'number') {
|
||||
lockLevel = levels[payload.level];
|
||||
}
|
||||
} else if (typeof payload.level === 'number') {
|
||||
if (lockLevel > 1) {
|
||||
lockLevel = payload.level;
|
||||
}
|
||||
} else {
|
||||
return server.reply({
|
||||
cmd: 'warn',
|
||||
text: `Expected "level" to be a number or string label: ${Object.keys(levels).join(', ')}`,
|
||||
id: Errors.LockRoom.LEVEL_REQUIRED,
|
||||
channel: targetChannel, // @todo Multichannel
|
||||
}, socket);
|
||||
}
|
||||
}
|
||||
|
||||
if (lockLevel > socket.level) {
|
||||
return server.reply({
|
||||
cmd: 'warn',
|
||||
text: `Target level too high (${lockLevel}). You may only lock up to ${socket.level}`,
|
||||
id: Errors.LockRoom.LEVEL_TOO_HIGH,
|
||||
channel: targetChannel, // @todo Multichannel
|
||||
}, socket);
|
||||
}
|
||||
|
||||
core.locked[targetChannel] = lockLevel;
|
||||
|
||||
// inform mods
|
||||
server.broadcast({
|
||||
cmd: 'info', // @todo Add numeric info code as `id`
|
||||
text: `Channel: ?${targetChannel} lock enabled by [${socket.trip}]${socket.nick}`,
|
||||
cmd: 'info',
|
||||
text: `Channel: ?${targetChannel} locked to ${lockLevel} by [${socket.trip}]${socket.nick}`,
|
||||
id: Info.Mod.LOCKED_DETAILED,
|
||||
channel: targetChannel, // @todo Multichannel
|
||||
}, { channel: targetChannel, level: isChannelModerator });
|
||||
|
||||
server.broadcast({
|
||||
cmd: 'info',
|
||||
text: `Channel: ?${targetChannel} locked to ${lockLevel} by [${socket.trip}]${socket.nick}`,
|
||||
id: Info.Mod.LOCKED_GLOBAL_NOTIFY,
|
||||
channel: false, // @todo Multichannel, false for global info
|
||||
}, { level: isModerator });
|
||||
|
||||
console.log(`Channel: ?${targetChannel} locked to ${lockLevel} by [${socket.trip}]${socket.nick}`);
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
@@ -169,7 +204,7 @@ export function whisperCheck({
|
||||
* string = error
|
||||
*/
|
||||
export function chatCheck({
|
||||
socket, payload,
|
||||
core, server, socket, payload,
|
||||
}) {
|
||||
if (socket.channel === 'purgatory') {
|
||||
if (isModerator(socket.level)) {
|
||||
@@ -179,6 +214,32 @@ export function chatCheck({
|
||||
return false;
|
||||
}
|
||||
|
||||
if (typeof payload.text !== 'string') {
|
||||
return false;
|
||||
}
|
||||
|
||||
if (payload.text.startsWith('/lockroom')) {
|
||||
const [, levelArg] = payload.text.split(' ');
|
||||
|
||||
const newPayload = {
|
||||
cmd: 'lockroom',
|
||||
};
|
||||
|
||||
if (levelArg) {
|
||||
const parsedLevel = Number(levelArg);
|
||||
newPayload.level = !Number.isNaN(parsedLevel) ? parsedLevel : levelArg;
|
||||
}
|
||||
|
||||
run({
|
||||
core,
|
||||
server,
|
||||
socket,
|
||||
payload: newPayload,
|
||||
});
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
return payload;
|
||||
}
|
||||
|
||||
@@ -214,14 +275,14 @@ export function joinCheck({
|
||||
core, server, socket, payload,
|
||||
}) {
|
||||
// check if target channel is locked
|
||||
if (typeof core.locked[payload.channel] === 'undefined' || core.locked[payload.channel] !== true) {
|
||||
if (typeof core.locked[payload.channel] === 'undefined' || core.locked[payload.channel] === false) {
|
||||
if (payload.channel !== 'purgatory') {
|
||||
return payload;
|
||||
}
|
||||
}
|
||||
|
||||
// `join` is the legacy entry point, check if it needs to be upgraded
|
||||
if (typeof socket.hcProtocol === 'undefined') {
|
||||
if (typeof socket.hcProtocol === 'undefined' || socket.hcProtocol === 1) {
|
||||
payload = upgradeLegacyJoin(server, socket, payload);
|
||||
}
|
||||
|
||||
@@ -278,8 +339,8 @@ export function joinCheck({
|
||||
};
|
||||
|
||||
// check if trip is allowed
|
||||
if (userInfo.uType === 'user') {
|
||||
if (userInfo.trip == null || isTrustedUser(level) === false) {
|
||||
if (!isModerator(userInfo.level)) {
|
||||
if (core.locked[channel] > userInfo.level) {
|
||||
const origNick = userInfo.nick;
|
||||
const origChannel = payload.channel;
|
||||
|
||||
@@ -296,15 +357,17 @@ export function joinCheck({
|
||||
|
||||
setTimeout(() => {
|
||||
server.reply({
|
||||
cmd: 'info', // @todo Add numeric info code as `id`
|
||||
cmd: 'info',
|
||||
text: danteQuotes[Math.floor(Math.random() * danteQuotes.length)],
|
||||
id: Info.Core.PURGATORY_QUOTE,
|
||||
channel: 'purgatory', // @todo Multichannel
|
||||
}, socket);
|
||||
}, 100);
|
||||
|
||||
server.broadcast({
|
||||
cmd: 'info', // @todo Add numeric info code as `id`
|
||||
cmd: 'info',
|
||||
text: `${payload.nick} is: ${origNick}\ntrip: ${userInfo.trip || 'none'}\ntried to join: ?${origChannel}\nhash: ${userInfo.hash}`,
|
||||
id: Info.Core.PURGATORY_NOTIFY,
|
||||
channel: 'purgatory', // @todo Multichannel, false for global info
|
||||
}, { channel: 'purgatory', level: isModerator });
|
||||
}
|
||||
@@ -327,5 +390,6 @@ export const info = {
|
||||
category: 'moderators',
|
||||
description: 'Locks a channel preventing default levels from joining',
|
||||
usage: `
|
||||
API: { cmd: 'lockroom', channel: '<optional channel, defaults to your current channel>' }`,
|
||||
API: { cmd: 'lockroom', channel: '<optional channel, defaults to your current channel>', level: <optional string or number> }
|
||||
Text: /lockroom`,
|
||||
};
|
||||
|
||||
@@ -3,7 +3,7 @@
|
||||
/**
|
||||
* @author OpSimple ( https://github.com/OpSimple )
|
||||
* @summary Unmuzzle a user
|
||||
* @version 1.0.0
|
||||
* @version 1.1.0
|
||||
* @description Pardon a dumb user to be able to speak again
|
||||
* @module speak
|
||||
*/
|
||||
@@ -13,6 +13,7 @@ import {
|
||||
} from '../utility/_UAC.js';
|
||||
import {
|
||||
Errors,
|
||||
Info,
|
||||
} from '../utility/_Constants.js';
|
||||
|
||||
/**
|
||||
@@ -56,8 +57,9 @@ export async function run({
|
||||
core.muzzledHashes = {};
|
||||
|
||||
return server.broadcast({
|
||||
cmd: 'info', // @todo Add numeric info code as `id`
|
||||
cmd: 'info',
|
||||
text: `${socket.nick} unmuzzled all users`,
|
||||
id: Info.Mod.UNMUZZLED_ALL,
|
||||
channel: false, // @todo Multichannel, false for global
|
||||
}, { level: isModerator });
|
||||
}
|
||||
@@ -65,8 +67,9 @@ export async function run({
|
||||
core.muzzledHashes = {};
|
||||
|
||||
return server.broadcast({
|
||||
cmd: 'info', // @todo Add numeric info code as `id`
|
||||
cmd: 'info',
|
||||
text: `${socket.nick} unmuzzled all users`,
|
||||
id: Info.Mod.UNMUZZLED_ALL,
|
||||
channel: false, // @todo Multichannel, false for global
|
||||
}, { level: isModerator });
|
||||
}
|
||||
@@ -83,8 +86,9 @@ export async function run({
|
||||
|
||||
// notify mods
|
||||
server.broadcast({
|
||||
cmd: 'info', // @todo Add numeric info code as `id`
|
||||
cmd: 'info',
|
||||
text: `${socket.nick}#${socket.trip} unmuzzled : ${target}`,
|
||||
id: Info.Mod.UNMUZZLED_DETAILED,
|
||||
channel: false, // @todo Multichannel, false for global
|
||||
}, { level: isModerator });
|
||||
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
/**
|
||||
* @author Marzavec ( https://github.com/marzavec )
|
||||
* @summary Unban a user
|
||||
* @version 1.0.0
|
||||
* @version 1.1.0
|
||||
* @description Un-bans target user by ip or hash
|
||||
* @module unban
|
||||
*/
|
||||
@@ -11,6 +11,7 @@ import {
|
||||
} from '../utility/_UAC.js';
|
||||
import {
|
||||
Errors,
|
||||
Info,
|
||||
} from '../utility/_Constants.js';
|
||||
|
||||
/**
|
||||
@@ -59,15 +60,17 @@ export async function run({
|
||||
|
||||
// reply with success
|
||||
server.reply({
|
||||
cmd: 'info', // @todo Add numeric info code as `id`
|
||||
cmd: 'info',
|
||||
text: `Unbanned ${target}`,
|
||||
id: Info.Mod.UNBANNED,
|
||||
channel: socket.channel, // @todo Multichannel
|
||||
}, socket);
|
||||
|
||||
// notify mods
|
||||
server.broadcast({
|
||||
cmd: 'info', // @todo Add numeric info code as `id`
|
||||
cmd: 'info',
|
||||
text: `${socket.nick}#${socket.trip} unbanned: ${target}`,
|
||||
id: Info.Mod.UNBANNED_DETAILED,
|
||||
channel: false, // @todo Multichannel, false for global
|
||||
}, { level: isModerator });
|
||||
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
/**
|
||||
* @author Marzavec ( https://github.com/marzavec )
|
||||
* @summary Released them from the void
|
||||
* @version 1.0.0
|
||||
* @version 1.1.0
|
||||
* @description Clears all banned ip addresses
|
||||
* @module unbanall
|
||||
*/
|
||||
@@ -9,6 +9,9 @@
|
||||
import {
|
||||
isModerator,
|
||||
} from '../utility/_UAC.js';
|
||||
import {
|
||||
Info,
|
||||
} from '../utility/_Constants.js';
|
||||
|
||||
/**
|
||||
* Executes when invoked by a remote client
|
||||
@@ -31,15 +34,17 @@ export async function run({ core, server, socket }) {
|
||||
|
||||
// reply with success
|
||||
server.reply({
|
||||
cmd: 'info', // @todo Add numeric info code as `id`
|
||||
cmd: 'info',
|
||||
text: 'Unbanned all ip addresses',
|
||||
id: Info.Mod.UNBANNED_ALL,
|
||||
channel: socket.channel, // @todo Multichannel
|
||||
}, socket);
|
||||
|
||||
// notify mods
|
||||
server.broadcast({
|
||||
cmd: 'info', // @todo Add numeric info code as `id`
|
||||
cmd: 'info',
|
||||
text: `${socket.nick}#${socket.trip} unbanned all ip addresses`,
|
||||
id: Info.Mod.UNBANNED_ALL_DETAILED,
|
||||
channel: false, // @todo Multichannel, false for global
|
||||
}, { level: isModerator });
|
||||
|
||||
|
||||
+69
-22
@@ -3,14 +3,18 @@
|
||||
/**
|
||||
* @author Marzavec ( https://github.com/marzavec )
|
||||
* @summary Unlock target channel
|
||||
* @version 1.0.0
|
||||
* @version 1.1.0
|
||||
* @description Unlocks a channel allowing anyone to join
|
||||
* @module unlockroom
|
||||
*/
|
||||
|
||||
import {
|
||||
isModerator,
|
||||
isChannelModerator,
|
||||
} from '../utility/_UAC.js';
|
||||
import {
|
||||
Errors,
|
||||
Info,
|
||||
} from '../utility/_Constants.js';
|
||||
|
||||
/**
|
||||
* Automatically executes once after server is ready
|
||||
@@ -31,46 +35,89 @@ export async function init(core) {
|
||||
* @return {void}
|
||||
*/
|
||||
export async function run({
|
||||
core, server, socket, payload,
|
||||
core, server, socket,
|
||||
}) {
|
||||
// increase rate limit chance and ignore if not admin or mod
|
||||
if (!isModerator(socket.level)) {
|
||||
if (!isChannelModerator(socket.level)) {
|
||||
return server.police.frisk(socket, 10);
|
||||
}
|
||||
|
||||
let targetChannel;
|
||||
const targetChannel = socket.channel;
|
||||
|
||||
if (typeof payload.channel !== 'string') {
|
||||
if (typeof socket.channel !== 'string') { // @todo Multichannel
|
||||
return false; // silently fail
|
||||
}
|
||||
|
||||
targetChannel = socket.channel;
|
||||
} else {
|
||||
targetChannel = payload.channel;
|
||||
}
|
||||
|
||||
if (!core.locked[targetChannel]) {
|
||||
if (typeof core.locked[targetChannel] === 'undefined' || core.locked[targetChannel] === false) {
|
||||
return server.reply({
|
||||
cmd: 'info', // @todo Add numeric info code as `id`
|
||||
cmd: 'warn',
|
||||
text: 'Channel is not locked.',
|
||||
channel: socket.channel, // @todo Multichannel
|
||||
id: Errors.Global.INVALID_DATA,
|
||||
channel: targetChannel, // @todo Multichannel
|
||||
}, socket);
|
||||
}
|
||||
|
||||
if (core.locked[targetChannel] > socket.level) {
|
||||
return server.reply({
|
||||
cmd: 'warn',
|
||||
text: `Level ${core.locked[targetChannel]} required, you are level ${socket.level}`,
|
||||
id: Errors.LockRoom.LEVEL_REQUIRED,
|
||||
channel: targetChannel, // @todo Multichannel
|
||||
}, socket);
|
||||
}
|
||||
|
||||
core.locked[targetChannel] = false;
|
||||
|
||||
server.broadcast({
|
||||
cmd: 'info', // @todo Add numeric info code as `id`
|
||||
cmd: 'info',
|
||||
text: `Channel: ?${targetChannel} unlocked by [${socket.trip}]${socket.nick}`,
|
||||
channel: targetChannel, // @todo Multichannel, false for global info
|
||||
}, { channel: targetChannel, level: isModerator });
|
||||
id: Info.Mod.UNLOCKED_DETAILED,
|
||||
channel: targetChannel, // @todo Multichannel
|
||||
}, { channel: targetChannel, level: isChannelModerator });
|
||||
|
||||
console.log(`Channel: ?${targetChannel} unlocked by [${socket.trip}]${socket.nick} in ${socket.channel}`);
|
||||
console.log(`Channel: ?${targetChannel} unlocked by [${socket.trip}]${socket.nick}`);
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
/**
|
||||
* Automatically executes once after server is ready to register this modules hooks
|
||||
* @param {Object} server - Reference to server environment object
|
||||
* @public
|
||||
* @return {void}
|
||||
*/
|
||||
export function initHooks(server) {
|
||||
server.registerHook('in', 'chat', this.chatCheck.bind(this), 4);
|
||||
}
|
||||
|
||||
/**
|
||||
* Executes every time an incoming chat command is invoked;
|
||||
* hook incoming chat commands, reject them if the channel is 'purgatory'
|
||||
* @param {Object} env - Environment object with references to core, server, socket & payload
|
||||
* @public
|
||||
* @return {(Object|boolean|string)} Object = same/altered payload,
|
||||
* false = suppress action,
|
||||
* string = error
|
||||
*/
|
||||
export function chatCheck({
|
||||
core, server, socket, payload,
|
||||
}) {
|
||||
if (typeof payload.text !== 'string') {
|
||||
return false;
|
||||
}
|
||||
|
||||
if (payload.text.startsWith('/unlockroom')) {
|
||||
this.run({
|
||||
core,
|
||||
server,
|
||||
socket,
|
||||
payload: {
|
||||
cmd: 'unlockroom',
|
||||
},
|
||||
});
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
return payload;
|
||||
}
|
||||
|
||||
/**
|
||||
* Module meta information
|
||||
* @public
|
||||
|
||||
@@ -10,7 +10,7 @@
|
||||
* Internal version, used mainly for debugging
|
||||
* @typedef {object} CodebaseVersion
|
||||
*/
|
||||
export const CodebaseVersion = '2.2.25b';
|
||||
export const CodebaseVersion = '2.2.3b';
|
||||
|
||||
/* Base error ranges */
|
||||
const GlobalErrors = 10;
|
||||
@@ -34,6 +34,9 @@ const ForceColorErrors = WhisperErrors + 10;
|
||||
const ForceFlairErrors = ForceColorErrors + 10;
|
||||
const UsersErrors = ForceFlairErrors + 10;
|
||||
const HackRequest = UsersErrors + 10;
|
||||
const KickErrors = HackRequest + 10;
|
||||
const LockRoomErrors = KickErrors + 10;
|
||||
const WalletErrors = LockRoomErrors + 10;
|
||||
|
||||
/**
|
||||
* Holds the numeric id values for each error type
|
||||
@@ -47,6 +50,9 @@ export const Errors = {
|
||||
INTERNAL_ERROR: GlobalErrors + 4,
|
||||
MISSING_TRIPCODE: GlobalErrors + 5,
|
||||
UNKNOWN_CMD: GlobalErrors + 6,
|
||||
INVALID_PAYLOAD: GlobalErrors + 7,
|
||||
LOGIN_REQUIRED: GlobalErrors + 8,
|
||||
INVALID_DATA: GlobalErrors + 9,
|
||||
},
|
||||
|
||||
Captcha: {
|
||||
@@ -149,6 +155,93 @@ export const Errors = {
|
||||
TOO_LONG: HackRequest + 3,
|
||||
BAD_URL: HackRequest + 4,
|
||||
},
|
||||
|
||||
Kick: {
|
||||
MISSING_NICK: KickErrors + 1,
|
||||
},
|
||||
|
||||
LockRoom: {
|
||||
LEVEL_TOO_HIGH: LockRoomErrors + 1,
|
||||
LEVEL_REQUIRED: LockRoomErrors + 2,
|
||||
},
|
||||
|
||||
Wallet: {
|
||||
INVALID_AMOUNT: WalletErrors + 1,
|
||||
MISSING_AMOUNT: WalletErrors + 2,
|
||||
},
|
||||
};
|
||||
|
||||
/* Base Info Ranges */
|
||||
const InfoStart = 1000; // Start high to avoid collision
|
||||
const AdminInfo = InfoStart + 100;
|
||||
const ModInfo = AdminInfo + 100;
|
||||
const CoreInfo = ModInfo + 100;
|
||||
const CaptchaInfo = CoreInfo + 100;
|
||||
const WalletInfo = CaptchaInfo + 100;
|
||||
|
||||
/**
|
||||
* Holds the numeric id values for each info type
|
||||
* @typedef {object} Info
|
||||
*/
|
||||
export const Info = {
|
||||
Admin: {
|
||||
YOU_ARE_MOD: AdminInfo + 1,
|
||||
MOD_ADDED: AdminInfo + 2,
|
||||
MOD_ADDED_BROADCAST: AdminInfo + 3,
|
||||
BOMB_STATUS: AdminInfo + 4,
|
||||
USER_LIST: AdminInfo + 5,
|
||||
RELOAD_STATUS: AdminInfo + 6,
|
||||
YOU_ARE_USER: AdminInfo + 7,
|
||||
MOD_REMOVED: AdminInfo + 8,
|
||||
MOD_REMOVED_BROADCAST: AdminInfo + 9,
|
||||
CONFIG_SAVED: AdminInfo + 10,
|
||||
SHOUT: AdminInfo + 11,
|
||||
},
|
||||
|
||||
Mod: {
|
||||
BANNED: ModInfo + 1,
|
||||
BANNED_DETAILED: ModInfo + 2,
|
||||
MUZZLED_DETAILED: ModInfo + 3,
|
||||
KICKED_DETAILED: ModInfo + 4,
|
||||
KICKED: ModInfo + 5,
|
||||
LOCKED_DETAILED: ModInfo + 6,
|
||||
LOCKED_GLOBAL_NOTIFY: ModInfo + 7,
|
||||
UNMUZZLED_ALL: ModInfo + 8,
|
||||
UNMUZZLED_DETAILED: ModInfo + 9,
|
||||
UNBANNED: ModInfo + 10,
|
||||
UNBANNED_DETAILED: ModInfo + 11,
|
||||
UNBANNED_ALL: ModInfo + 12,
|
||||
UNBANNED_ALL_DETAILED: ModInfo + 13,
|
||||
UNLOCKED_DETAILED: ModInfo + 14,
|
||||
UWUIFY_ENABLED: ModInfo + 15,
|
||||
UWUIFY_DISABLED: ModInfo + 16,
|
||||
},
|
||||
|
||||
Core: {
|
||||
NICK_CHANGED: CoreInfo + 1,
|
||||
MY_HASH: CoreInfo + 2,
|
||||
HELP_TEXT: CoreInfo + 3,
|
||||
MOTD: CoreInfo + 4,
|
||||
STATS_FULL: CoreInfo + 5,
|
||||
STATS_BASIC: CoreInfo + 6,
|
||||
PURGATORY_QUOTE: CoreInfo + 7,
|
||||
PURGATORY_NOTIFY: CoreInfo + 8,
|
||||
},
|
||||
|
||||
Captcha: {
|
||||
NOT_ENABLED: CaptchaInfo + 1,
|
||||
DISABLED: CaptchaInfo + 2,
|
||||
ALREADY_ENABLED: CaptchaInfo + 3,
|
||||
ENABLED: CaptchaInfo + 4,
|
||||
},
|
||||
|
||||
Wallet: {
|
||||
DISCONNECTED: WalletInfo + 1,
|
||||
ADDRESS_REQUESTED: WalletInfo + 2,
|
||||
TX_RELAYED: WalletInfo + 3,
|
||||
VIEWED: WalletInfo + 4,
|
||||
CONNECTED: WalletInfo + 5,
|
||||
},
|
||||
};
|
||||
|
||||
/**
|
||||
@@ -182,13 +275,14 @@ export const SystemMOTDs = [
|
||||
'Protip: Use /getchannels anytime to see a list of public channels',
|
||||
'Protip: You can do ==/help== or ==/help command==',
|
||||
'Protip: Privately message with: /whisper @name The message',
|
||||
'Protip: A moderator can lock a channel with: /lockroom',
|
||||
];
|
||||
|
||||
/**
|
||||
* Maximum length of a channels MOTD string
|
||||
* @typedef {object} MaxMOTDLength
|
||||
*/
|
||||
export const MaxMOTDLength = 250;
|
||||
export const MaxMOTDLength = 500;
|
||||
|
||||
/**
|
||||
* Maximum number of specialized trip levels, per channel
|
||||
|
||||
@@ -24,10 +24,15 @@ import {
|
||||
export function upgradeLegacyJoin(server, socket, payload) {
|
||||
const newPayload = payload;
|
||||
|
||||
// this is why we can't have nice things
|
||||
if (typeof payload.nick === 'undefined' || !payload.nick) {
|
||||
payload.nick = `scarmiglione_${Math.floor(Math.random() * 99999)}`;
|
||||
}
|
||||
|
||||
// `join` is the legacy entry point, so apply protocol version
|
||||
socket.hcProtocol = 1;
|
||||
|
||||
// these would have been applied in the `session` module, apply it now
|
||||
// these would have been applied in the `session` module, apply them now
|
||||
socket.hash = server.getSocketHash(socket);
|
||||
socket.isBot = false;
|
||||
socket.color = false;
|
||||
|
||||
@@ -2,8 +2,9 @@
|
||||
|
||||
/**
|
||||
* @author MinusGix ( https://github.com/MinusGix )
|
||||
* @author Marzavec ( https://github.com/marzavec )
|
||||
* @summary General string helper functions
|
||||
* @version v1.0.0
|
||||
* @version v1.1.0
|
||||
* @description A library of several commonly used string functions
|
||||
* @module Text
|
||||
*/
|
||||
@@ -29,3 +30,11 @@ export const parseText = (text) => {
|
||||
|
||||
return sanitizedText;
|
||||
};
|
||||
|
||||
/**
|
||||
* Validate a string as a valid hex color string
|
||||
* @param {string} color - Color string to validate
|
||||
* @public
|
||||
* @return {boolean}
|
||||
*/
|
||||
export const verifyColor = (color) => /(^[0-9A-F]{6}$)|(^[0-9A-F]{3}$)/i.test(color);
|
||||
|
||||
@@ -0,0 +1,59 @@
|
||||
/**
|
||||
* @author Marzavec
|
||||
* @summary Disconnect a user's wallet
|
||||
* @version 1.0.0
|
||||
* @description Removes wallet session data and resets user state
|
||||
* @module disconnectwallet
|
||||
*/
|
||||
|
||||
import {
|
||||
Errors,
|
||||
Info,
|
||||
} from '../utility/_Constants.js';
|
||||
|
||||
/**
|
||||
* Executes when invoked by a remote client
|
||||
* @param {Object} env - Environment object with references to core, server, socket & payload
|
||||
* @public
|
||||
* @return {void}
|
||||
*/
|
||||
export async function run({
|
||||
server, socket,
|
||||
}) {
|
||||
// Check if wallet exists
|
||||
if (typeof socket.wallet === 'undefined') {
|
||||
return server.reply({
|
||||
cmd: 'warn',
|
||||
text: 'No wallet currently connected',
|
||||
id: Errors.Global.LOGIN_REQUIRED,
|
||||
channel: socket.channel,
|
||||
}, socket);
|
||||
}
|
||||
|
||||
const oldAddress = socket.wallet.address;
|
||||
delete socket.wallet;
|
||||
|
||||
return server.reply({
|
||||
cmd: 'info',
|
||||
text: `Wallet disconnected (${oldAddress.slice(0, 4)}...${oldAddress.slice(-4)})`,
|
||||
id: Info.Wallet.DISCONNECTED,
|
||||
channel: socket.channel,
|
||||
}, socket);
|
||||
}
|
||||
|
||||
/**
|
||||
* Module meta information
|
||||
* @public
|
||||
* @typedef {Object} disconnectwallet/info
|
||||
* @property {string} name - Module command name
|
||||
* @property {string} category - Module category name
|
||||
* @property {string} description - Information about module
|
||||
* @property {string} usage - Information about module usage
|
||||
*/
|
||||
export const info = {
|
||||
name: 'disconnectwallet',
|
||||
category: 'wallet',
|
||||
description: 'Disconnects the currently attached Solana wallet',
|
||||
usage: `
|
||||
API: { cmd: 'disconnectwallet' }`,
|
||||
};
|
||||
@@ -0,0 +1,117 @@
|
||||
/**
|
||||
* @author Marzavec
|
||||
* @summary Retrieve a user's wallet address
|
||||
* @version 1.0.0
|
||||
* @description Checks if a target user has a connected wallet and returns the address
|
||||
* @module getwallet
|
||||
*/
|
||||
|
||||
import {
|
||||
Errors,
|
||||
Info,
|
||||
} from '../utility/_Constants.js';
|
||||
import {
|
||||
findUser,
|
||||
} from '../utility/_Channels.js';
|
||||
|
||||
/**
|
||||
* Executes when invoked by a remote client
|
||||
* @param {Object} env - Enviroment object with references to core, server, socket & payload
|
||||
* @public
|
||||
* @return {void}
|
||||
*/
|
||||
export async function run({
|
||||
server, socket, payload,
|
||||
}) {
|
||||
// must be in a channel to run this command
|
||||
if (typeof socket.channel === 'undefined') {
|
||||
return server.police.frisk(socket, 1);
|
||||
}
|
||||
|
||||
server.police.frisk(socket, 2);
|
||||
|
||||
let targetUser = null;
|
||||
|
||||
if (typeof payload.userid === 'number') {
|
||||
targetUser = findUser(
|
||||
server,
|
||||
{
|
||||
channel: socket.channel,
|
||||
userid: payload.userid,
|
||||
},
|
||||
);
|
||||
} else if (typeof payload.nick === 'string') {
|
||||
targetUser = findUser(
|
||||
server,
|
||||
{
|
||||
channel: socket.channel,
|
||||
nick: payload.nick,
|
||||
},
|
||||
);
|
||||
} else {
|
||||
return server.reply({
|
||||
cmd: 'warn',
|
||||
text: 'Could not find user in that channel',
|
||||
id: Errors.Global.UNKNOWN_USER,
|
||||
channel: socket.channel,
|
||||
}, socket);
|
||||
}
|
||||
|
||||
if (!targetUser) {
|
||||
return server.reply({
|
||||
cmd: 'warn',
|
||||
text: 'Could not find user in that channel',
|
||||
id: Errors.Global.UNKNOWN_USER,
|
||||
channel: socket.channel,
|
||||
}, socket);
|
||||
}
|
||||
|
||||
if (typeof targetUser.wallet !== 'object' || typeof targetUser.wallet.address !== 'string') {
|
||||
return server.reply({
|
||||
cmd: 'warn',
|
||||
text: `@${targetUser.nick} has not connected a wallet`,
|
||||
id: Errors.Global.INVALID_DATA,
|
||||
channel: socket.channel,
|
||||
}, socket);
|
||||
}
|
||||
|
||||
server.send({
|
||||
cmd: 'info',
|
||||
text: `${socket.nick} requested your wallet address`,
|
||||
id: Info.Wallet.ADDRESS_REQUESTED,
|
||||
channel: socket.channel,
|
||||
}, targetUser);
|
||||
|
||||
return server.reply({
|
||||
cmd: 'walletInfo',
|
||||
userid: targetUser.userid,
|
||||
nick: targetUser.nick,
|
||||
address: targetUser.wallet.address,
|
||||
channel: socket.channel,
|
||||
}, socket);
|
||||
}
|
||||
|
||||
/**
|
||||
* The following payload properties are required to invoke this module:
|
||||
* "userid" OR "nick"
|
||||
* @public
|
||||
* @typedef {Array} getwallet/requiredData
|
||||
*/
|
||||
// export const requiredData = ['userid'];
|
||||
|
||||
/**
|
||||
* Module meta information
|
||||
* @public
|
||||
* @typedef {Object} getwallet/info
|
||||
* @property {string} name - Module command name
|
||||
* @property {string} description - Information about module
|
||||
* @property {string} usage - Information about module usage
|
||||
*/
|
||||
export const info = {
|
||||
name: 'getwallet',
|
||||
category: 'wallet',
|
||||
description: 'Retrieves the public wallet address of a specific user',
|
||||
usage: `
|
||||
API: { cmd: 'getwallet', userid: <target userid> }
|
||||
API: { cmd: 'getwallet', nick: <target nick> }`,
|
||||
};
|
||||
@@ -0,0 +1,144 @@
|
||||
/**
|
||||
* @author Marzavec
|
||||
* @summary Relay a transaction to another user for signing
|
||||
* @version 1.0.0
|
||||
* @description Accepts a base64 transaction and forwards it to a target user to sign
|
||||
* @module relaytx
|
||||
*/
|
||||
|
||||
import {
|
||||
Errors,
|
||||
Info,
|
||||
} from '../utility/_Constants.js';
|
||||
import {
|
||||
findUser,
|
||||
} from '../utility/_Channels.js';
|
||||
|
||||
/**
|
||||
* Executes when invoked by a remote client
|
||||
* @param {Object} env - Enviroment object with references to core, server, socket & payload
|
||||
* @public
|
||||
* @return {void}
|
||||
*/
|
||||
export async function run({
|
||||
server, socket, payload,
|
||||
}) {
|
||||
// must be in a channel to run this command
|
||||
if (typeof socket.channel === 'undefined') {
|
||||
return server.police.frisk(socket, 1);
|
||||
}
|
||||
|
||||
server.police.frisk(socket, 2);
|
||||
|
||||
if (typeof socket.wallet !== 'object' || typeof socket.wallet.address !== 'string') {
|
||||
return server.reply({
|
||||
cmd: 'warn',
|
||||
text: 'You must connect a wallet first',
|
||||
id: Errors.Global.LOGIN_REQUIRED,
|
||||
channel: socket.channel,
|
||||
}, socket);
|
||||
}
|
||||
|
||||
if (typeof payload.tx !== 'string' || payload.tx.length === 0) {
|
||||
return server.reply({
|
||||
cmd: 'warn',
|
||||
text: 'Missing or invalid transaction data',
|
||||
id: Errors.Global.INVALID_DATA,
|
||||
channel: socket.channel,
|
||||
}, socket);
|
||||
}
|
||||
|
||||
let targetUser = null;
|
||||
|
||||
if (typeof payload.userid === 'number') {
|
||||
targetUser = findUser(
|
||||
server,
|
||||
{
|
||||
channel: socket.channel,
|
||||
userid: payload.userid,
|
||||
},
|
||||
);
|
||||
} else if (typeof payload.nick === 'string') {
|
||||
targetUser = findUser(
|
||||
server,
|
||||
{
|
||||
channel: socket.channel,
|
||||
nick: payload.nick,
|
||||
},
|
||||
);
|
||||
} else {
|
||||
return server.reply({
|
||||
cmd: 'warn',
|
||||
text: 'You must specify a target user by nick or userid',
|
||||
id: Errors.Global.INVALID_DATA,
|
||||
channel: socket.channel,
|
||||
}, socket);
|
||||
}
|
||||
|
||||
if (!targetUser) {
|
||||
return server.reply({
|
||||
cmd: 'warn',
|
||||
text: 'Could not find user in that channel',
|
||||
id: Errors.Global.UNKNOWN_USER,
|
||||
channel: socket.channel,
|
||||
}, socket);
|
||||
}
|
||||
|
||||
if (targetUser.userid === socket.userid) {
|
||||
return server.reply({
|
||||
cmd: 'warn',
|
||||
text: 'You cannot relay transactions to yourself',
|
||||
id: Errors.Global.INVALID_DATA,
|
||||
channel: socket.channel,
|
||||
}, socket);
|
||||
}
|
||||
|
||||
if (typeof targetUser.wallet !== 'object' || typeof targetUser.wallet.address !== 'string') {
|
||||
return server.reply({
|
||||
cmd: 'warn',
|
||||
text: `@${targetUser.nick} does not have a connected wallet`,
|
||||
id: Errors.Global.UNKNOWN_USER,
|
||||
channel: socket.channel,
|
||||
}, socket);
|
||||
}
|
||||
|
||||
server.reply({
|
||||
cmd: 'signTransaction',
|
||||
tx: payload.tx,
|
||||
type: '3RD_PARTY_TRANSFER',
|
||||
from: socket.nick,
|
||||
channel: socket.channel,
|
||||
}, targetUser);
|
||||
|
||||
return server.reply({
|
||||
cmd: 'info',
|
||||
text: `TX sent to @${targetUser.nick}`,
|
||||
id: Info.Wallet.TX_RELAYED,
|
||||
channel: socket.channel,
|
||||
}, socket);
|
||||
}
|
||||
|
||||
/**
|
||||
* The following payload properties are required to invoke this module:
|
||||
* "tx", and either "userid" or "nick"
|
||||
* @public
|
||||
* @typedef {Array} relaytx/requiredData
|
||||
*/
|
||||
export const requiredData = ['tx'];
|
||||
|
||||
/**
|
||||
* Module meta information
|
||||
* @public
|
||||
* @typedef {Object} relaytx/info
|
||||
* @property {string} name - Module command name
|
||||
* @property {string} description - Information about module
|
||||
* @property {string} usage - Information about module usage
|
||||
*/
|
||||
export const info = {
|
||||
name: 'relaytx',
|
||||
category: 'wallet',
|
||||
description: 'Relays a base64 transaction to a target user for signature',
|
||||
usage: `
|
||||
API: { cmd: 'relaytx', tx: <base64 string>, userid: <target userid> }
|
||||
API: { cmd: 'relaytx', tx: <base64 string>, nick: <target nick> }`,
|
||||
};
|
||||
@@ -0,0 +1,307 @@
|
||||
/**
|
||||
* @author Marzavec
|
||||
* @summary Allow a Solana transfer
|
||||
* @version 1.0.0
|
||||
* @description Builds a transaction for a Solana transfer
|
||||
* @module sendsol
|
||||
*/
|
||||
|
||||
import {
|
||||
PublicKey,
|
||||
Transaction,
|
||||
SystemProgram,
|
||||
ComputeBudgetProgram,
|
||||
LAMPORTS_PER_SOL,
|
||||
} from '@solana/web3.js';
|
||||
import { createSolanaRpc } from '@solana/kit';
|
||||
|
||||
import {
|
||||
Errors,
|
||||
Info,
|
||||
} from '../utility/_Constants.js';
|
||||
import {
|
||||
findUser,
|
||||
} from '../utility/_Channels.js';
|
||||
// import { Buffer } from 'buffer';
|
||||
|
||||
const RPC_URL = 'https://solana-rpc.parafi.tech';
|
||||
const SERVER_WALLET = 'HACkoKCBiLiWjuVf4S4gTbFqJohVC6VkacBEBTtUCHat';
|
||||
|
||||
/**
|
||||
* Automatically executes once after server is ready or after a hot-reload
|
||||
* Ensures the shared Solana RPC client is initialized in the core environment
|
||||
* @param {Object} core - Reference to core environment object
|
||||
* @public
|
||||
* @return {void}
|
||||
*/
|
||||
export async function init(core) {
|
||||
if (typeof core.solanaRPC === 'undefined') {
|
||||
core.solanaRPC = createSolanaRpc(RPC_URL);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Executes when invoked by a remote client
|
||||
* @param {Object} env - Enviroment object with references to core, server, socket & payload
|
||||
* @public
|
||||
* @return {void}
|
||||
*/
|
||||
export async function run({
|
||||
core, server, socket, payload,
|
||||
}) {
|
||||
// must be in a channel to run this command
|
||||
if (typeof socket.channel === 'undefined') {
|
||||
return server.police.frisk(socket, 1);
|
||||
}
|
||||
|
||||
server.police.frisk(socket, 8);
|
||||
|
||||
// client must have a confirmed wallet
|
||||
if (typeof socket.wallet !== 'object' || typeof socket.wallet.address !== 'string') {
|
||||
return server.send({
|
||||
cmd: 'warn',
|
||||
text: 'You must connect a wallet first',
|
||||
id: Errors.Global.LOGIN_REQUIRED,
|
||||
channel: socket.channel, // @todo Multichannel
|
||||
}, socket);
|
||||
}
|
||||
|
||||
if (typeof payload.amount !== 'number' || payload.amount <= 0) {
|
||||
return server.reply({
|
||||
cmd: 'warn',
|
||||
text: 'Invalid amount',
|
||||
id: Errors.Wallet.INVALID_AMOUNT,
|
||||
channel: socket.channel, // @todo Multichannel
|
||||
}, socket);
|
||||
}
|
||||
|
||||
let targetUser = null;
|
||||
|
||||
if (typeof payload.userid === 'number') {
|
||||
targetUser = findUser(
|
||||
server,
|
||||
{
|
||||
channel: socket.channel,
|
||||
userid: payload.userid,
|
||||
},
|
||||
);
|
||||
} else if (typeof payload.nick === 'string') {
|
||||
targetUser = findUser(
|
||||
server,
|
||||
{
|
||||
channel: socket.channel,
|
||||
nick: payload.nick,
|
||||
},
|
||||
);
|
||||
} else {
|
||||
return server.reply({
|
||||
cmd: 'warn',
|
||||
text: 'Could not find user in that channel',
|
||||
id: Errors.Global.UNKNOWN_USER,
|
||||
channel: socket.channel, // @todo Multichannel
|
||||
}, socket);
|
||||
}
|
||||
|
||||
if (!targetUser) {
|
||||
return server.reply({
|
||||
cmd: 'warn',
|
||||
text: 'Could not find user in that channel',
|
||||
id: Errors.Global.UNKNOWN_USER,
|
||||
channel: socket.channel, // @todo Multichannel
|
||||
}, socket);
|
||||
}
|
||||
|
||||
if (targetUser.userid === socket.userid) {
|
||||
return server.reply({
|
||||
cmd: 'warn',
|
||||
text: 'You cannot relay transactions to yourself',
|
||||
id: Errors.Global.INVALID_DATA,
|
||||
channel: socket.channel,
|
||||
}, socket);
|
||||
}
|
||||
|
||||
// target user must have a confirmed wallet
|
||||
if (typeof targetUser.wallet !== 'object' || typeof targetUser.wallet.address !== 'string') {
|
||||
return server.send({
|
||||
cmd: 'warn',
|
||||
text: `@${targetUser.nick} must connect a wallet first`,
|
||||
id: Errors.Global.LOGIN_REQUIRED,
|
||||
channel: socket.channel, // @todo Multichannel
|
||||
}, socket);
|
||||
}
|
||||
|
||||
server.send({
|
||||
cmd: 'info',
|
||||
text: `${socket.nick} viewed your wallet`,
|
||||
id: Info.Wallet.VIEWED,
|
||||
channel: socket.channel, // @todo Multichannel
|
||||
}, targetUser);
|
||||
|
||||
const senderPubkey = new PublicKey(socket.wallet.address);
|
||||
const recipientPubkey = new PublicKey(targetUser.wallet.address);
|
||||
const SERVER_ADDRESS = new PublicKey(SERVER_WALLET);
|
||||
|
||||
const mainAmountLamports = Math.floor(payload.amount * LAMPORTS_PER_SOL);
|
||||
|
||||
const feePercentage = 0.01;
|
||||
const feeAmountLamports = Math.floor(mainAmountLamports * feePercentage);
|
||||
|
||||
if (mainAmountLamports < 1 || feeAmountLamports < 1) {
|
||||
return server.reply({
|
||||
cmd: 'warn',
|
||||
text: 'Transfer amount too low to cover required fees',
|
||||
id: Errors.Wallet.INVALID_AMOUNT,
|
||||
channel: socket.channel,
|
||||
}, socket);
|
||||
}
|
||||
|
||||
const transaction = new Transaction();
|
||||
|
||||
transaction.add(
|
||||
ComputeBudgetProgram.setComputeUnitLimit({ units: 300000 }),
|
||||
);
|
||||
|
||||
transaction.add(
|
||||
ComputeBudgetProgram.setComputeUnitPrice({ microLamports: 100000 }),
|
||||
);
|
||||
|
||||
transaction.add(
|
||||
SystemProgram.transfer({
|
||||
fromPubkey: senderPubkey,
|
||||
toPubkey: recipientPubkey,
|
||||
lamports: mainAmountLamports,
|
||||
}),
|
||||
);
|
||||
|
||||
transaction.add(
|
||||
SystemProgram.transfer({
|
||||
fromPubkey: senderPubkey,
|
||||
toPubkey: SERVER_ADDRESS,
|
||||
lamports: feeAmountLamports,
|
||||
}),
|
||||
);
|
||||
|
||||
const rpcResponse = await core.solanaRPC.getLatestBlockhash().send();
|
||||
const latestBlockhash = rpcResponse.value;
|
||||
|
||||
if (!latestBlockhash || !latestBlockhash.blockhash) {
|
||||
return server.reply({
|
||||
cmd: 'warn',
|
||||
text: 'RPC error, try again later',
|
||||
id: Errors.Global.INTERNAL_ERROR,
|
||||
channel: socket.channel,
|
||||
}, socket);
|
||||
}
|
||||
|
||||
transaction.feePayer = senderPubkey;
|
||||
transaction.recentBlockhash = latestBlockhash.blockhash;
|
||||
|
||||
const serializedTx = transaction.serialize({
|
||||
requireAllSignatures: false,
|
||||
verifySignatures: false,
|
||||
});
|
||||
|
||||
const base64Tx = serializedTx.toString('base64');
|
||||
|
||||
return server.reply({
|
||||
cmd: 'signTransaction',
|
||||
tx: base64Tx,
|
||||
type: 'STANDARD_TRANSFER',
|
||||
from: false,
|
||||
channel: socket.channel,
|
||||
}, socket);
|
||||
}
|
||||
|
||||
/**
|
||||
* Automatically executes once after server is ready to register this modules hooks
|
||||
* @param {Object} server - Reference to server environment object
|
||||
* @public
|
||||
* @return {void}
|
||||
*/
|
||||
export function initHooks(server) {
|
||||
server.registerHook('in', 'chat', this.runSendSolCheck.bind(this), 29);
|
||||
}
|
||||
|
||||
/**
|
||||
* Executes every time an incoming chat command is invoked
|
||||
* @param {Object} env - Environment object with references to core, server, socket & payload
|
||||
* @public
|
||||
* @return {(Object|boolean|string)} Object = same/altered payload,
|
||||
* false = suppress action,
|
||||
* string = error
|
||||
*/
|
||||
export function runSendSolCheck({
|
||||
core, server, socket, payload,
|
||||
}) {
|
||||
if (typeof payload.text !== 'string') {
|
||||
return false;
|
||||
}
|
||||
|
||||
if (payload.text.startsWith('/sendsol ')) {
|
||||
const input = payload.text.split(' ');
|
||||
|
||||
const nick = input[1];
|
||||
if (!nick || !nick.replace(/[^a-zA-Z0-9_]/g, '')) {
|
||||
server.reply({
|
||||
cmd: 'warn',
|
||||
text: 'Failed to send sol: Missing name. Refer to `/help sendsol` for instructions on how to use this command.',
|
||||
id: Errors.Global.UNKNOWN_USER,
|
||||
channel: socket.channel, // @todo Multichannel
|
||||
}, socket);
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
const amount = Number(input[2]);
|
||||
if (!amount || Number.isNaN(amount)) {
|
||||
server.reply({
|
||||
cmd: 'warn',
|
||||
text: 'Failed to send sol: Missing amount. Refer to `/help sendsol` for instructions on how to use this command.',
|
||||
id: Errors.Wallet.INVALID_AMOUNT,
|
||||
channel: socket.channel, // @todo Multichannel
|
||||
}, socket);
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
this.run({
|
||||
core,
|
||||
server,
|
||||
socket,
|
||||
payload: {
|
||||
cmd: 'sendsol',
|
||||
nick: nick.replace(/[^a-zA-Z0-9_]/g, ''),
|
||||
amount,
|
||||
},
|
||||
});
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
return payload;
|
||||
}
|
||||
|
||||
/**
|
||||
* The following payload properties are required to invoke this module:
|
||||
* "userid", "amount"
|
||||
* @public
|
||||
* @typedef {Array} sendsol/requiredData
|
||||
*/
|
||||
export const requiredData = ['userid', 'amount'];
|
||||
|
||||
/**
|
||||
* Module meta information
|
||||
* @public
|
||||
* @typedef {Object} sendsol/info
|
||||
* @property {string} name - Module command name
|
||||
* @property {string} description - Information about module
|
||||
* @property {string} usage - Information about module usage
|
||||
*/
|
||||
export const info = {
|
||||
name: 'sendsol',
|
||||
category: 'wallet',
|
||||
description: 'Constructs a Solana transaction to send funds to another user',
|
||||
usage: `
|
||||
API: { cmd: 'sendsol', userid: <target userid>, amount: <numeric amount in sol> }
|
||||
Text: /sendsol @nick 23`,
|
||||
};
|
||||
@@ -0,0 +1,249 @@
|
||||
/**
|
||||
* @author Marzavec
|
||||
* @summary Finalize a siw
|
||||
* @version 1.0.0
|
||||
* @description Finalize a siw, check for NFT ownership, and sync permissions
|
||||
* @module signsiw
|
||||
*/
|
||||
|
||||
import nacl from 'tweetnacl';
|
||||
import bs58 from 'bs58';
|
||||
import { PublicKey } from '@solana/web3.js';
|
||||
import { BorshCoder } from '@coral-xyz/anchor';
|
||||
import { createSolanaRpc } from '@solana/kit';
|
||||
|
||||
import {
|
||||
levels,
|
||||
getAppearance,
|
||||
getUserDetails,
|
||||
} from '../utility/_UAC.js';
|
||||
|
||||
import {
|
||||
getChannelSettings,
|
||||
} from '../utility/_Channels.js';
|
||||
|
||||
import {
|
||||
Info,
|
||||
} from '../utility/_Constants.js';
|
||||
|
||||
const RPC_URL = 'https://api.devnet.solana.com';
|
||||
const PROGRAM_ID = new PublicKey('HACkoKCBiLiWjuVf4S4gTbFqJohVC6VkacBEBTtUCHat'); // @todo
|
||||
const IDL = {
|
||||
address: 'HACkoKCBiLiWjuVf4S4gTbFqJohVC6VkacBEBTtUCHat', // @todo
|
||||
metadata: { name: 'hackchat_sc', version: '0.1.0', spec: '0.1.0' },
|
||||
accounts: [
|
||||
{
|
||||
name: 'ChannelState',
|
||||
discriminator: [74, 132, 141, 196, 64, 52, 83, 136],
|
||||
},
|
||||
],
|
||||
types: [
|
||||
{
|
||||
name: 'ChannelState',
|
||||
type: {
|
||||
kind: 'struct',
|
||||
fields: [
|
||||
{ name: 'channel_name', type: 'string' },
|
||||
{ name: 'owner_nft_mint', type: 'pubkey' },
|
||||
{ name: 'owner_wallet', type: 'pubkey' },
|
||||
{ name: 'moderator_trips', type: { vec: { array: ['u8', 6] } } },
|
||||
{ name: 'bump', type: 'u8' },
|
||||
],
|
||||
},
|
||||
},
|
||||
],
|
||||
};
|
||||
|
||||
/**
|
||||
* Automatically executes once after server is ready or after a hot-reload
|
||||
* @param {Object} core - Reference to core environment object
|
||||
* @public
|
||||
* @return {void}
|
||||
*/
|
||||
export async function init(core) {
|
||||
if (typeof core.solanaRPC === 'undefined') {
|
||||
core.solanaRPC = createSolanaRpc(RPC_URL);
|
||||
}
|
||||
|
||||
// core.hackchatCoder = new BorshCoder(IDL);
|
||||
}
|
||||
|
||||
/**
|
||||
* Checks Blockchain PDA for permissions
|
||||
* @param {string} channelName - The name of the channel (e.g., "general")
|
||||
* @param {string} walletAddress - The user's verified wallet address
|
||||
* @param {string} userTrip - The user's current trip code (if any)
|
||||
* @param {Object} core - Core environment (for RPC and Coder)
|
||||
* @returns {Promise<number|null>} - Returns the new level (number) or null if no perms found
|
||||
*/
|
||||
async function checkChainPermissions(channelName, walletAddress, userTrip, core) {
|
||||
try {
|
||||
const [pda] = PublicKey.findProgramAddressSync(
|
||||
[Buffer.from('channel'), Buffer.from(channelName)],
|
||||
PROGRAM_ID,
|
||||
);
|
||||
|
||||
const accountInfo = await core.solanaRPC.getAccountInfo(pda);
|
||||
|
||||
if (!accountInfo) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const accountData = core.hackchatCoder.accounts.decode(
|
||||
'ChannelState',
|
||||
accountInfo.data,
|
||||
);
|
||||
|
||||
if (accountData.ownerWallet.toString() === walletAddress) {
|
||||
return levels.channelOwner;
|
||||
}
|
||||
|
||||
if (userTrip && accountData.moderatorTrips) {
|
||||
const tripBuffer = Buffer.from(userTrip);
|
||||
|
||||
const isMod = accountData.moderatorTrips.some((modTripBytes) => {
|
||||
const modTripBuffer = Buffer.from(modTripBytes);
|
||||
return modTripBuffer.equals(tripBuffer);
|
||||
});
|
||||
|
||||
if (isMod) {
|
||||
return levels.channelModerator;
|
||||
}
|
||||
}
|
||||
|
||||
return null;
|
||||
} catch (err) {
|
||||
console.error('Error checking chain permissions:', err);
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Executes when invoked by a remote client
|
||||
* @param {Object} env - Enviroment object with references to core, server, socket & payload
|
||||
* @public
|
||||
* @return {void}
|
||||
*/
|
||||
export async function run({
|
||||
core, server, socket, payload,
|
||||
}) {
|
||||
// must be in a channel to run this command
|
||||
if (typeof socket.channel === 'undefined') {
|
||||
return server.police.frisk(socket, 1);
|
||||
}
|
||||
|
||||
if (typeof socket.siwMsg === 'undefined' || typeof socket.siwAddress === 'undefined') {
|
||||
return false;
|
||||
}
|
||||
|
||||
if (typeof payload.signature !== 'string' || typeof payload.signedMessage !== 'string') {
|
||||
return false;
|
||||
}
|
||||
|
||||
if (payload.signedMessage !== socket.siwMsg) {
|
||||
return false;
|
||||
}
|
||||
|
||||
const now = new Date();
|
||||
if (!socket.siwExpiry || socket.siwExpiry < now) {
|
||||
return false;
|
||||
}
|
||||
|
||||
const tempSiwAddress = socket.siwAddress;
|
||||
|
||||
socket.siwMsg = undefined;
|
||||
socket.siwAddress = undefined;
|
||||
socket.siwExpiry = undefined;
|
||||
|
||||
const messageBytes = new TextEncoder().encode(payload.signedMessage);
|
||||
const publicKeyBytes = bs58.decode(tempSiwAddress);
|
||||
const signatureBytes = bs58.decode(payload.signature);
|
||||
|
||||
let isVerified = false;
|
||||
try {
|
||||
isVerified = nacl.sign.detached.verify(
|
||||
messageBytes,
|
||||
signatureBytes,
|
||||
publicKeyBytes,
|
||||
);
|
||||
} catch (e) {
|
||||
return false;
|
||||
}
|
||||
|
||||
if (isVerified) {
|
||||
socket.wallet = {};
|
||||
socket.wallet.address = tempSiwAddress;
|
||||
|
||||
let replyText = `Now connected to: ${tempSiwAddress}`;
|
||||
|
||||
const channelSettings = getChannelSettings(core.appConfig.data, socket.channel);
|
||||
|
||||
let newLevel = null;
|
||||
|
||||
if (!channelSettings.owned) {
|
||||
/* newLevel = await checkChainPermissions(
|
||||
socket.channel,
|
||||
tempSiwAddress,
|
||||
socket.trip,
|
||||
core,
|
||||
); */
|
||||
}
|
||||
|
||||
// only update if the new level is higher than what they currently have
|
||||
if (newLevel !== null && newLevel > socket.level) {
|
||||
socket.level = newLevel;
|
||||
|
||||
const { color, flair } = getAppearance(newLevel);
|
||||
socket.color = color;
|
||||
socket.flair = flair;
|
||||
|
||||
server.broadcast({
|
||||
...getUserDetails(socket),
|
||||
...{
|
||||
cmd: 'updateUser',
|
||||
channel: socket.channel,
|
||||
},
|
||||
}, { channel: socket.channel });
|
||||
|
||||
if (newLevel === levels.channelOwner) {
|
||||
replyText += ' You are the verified owner of this channel';
|
||||
} else if (newLevel === levels.channelModerator) {
|
||||
replyText += ' You are a verified moderator of this channel';
|
||||
}
|
||||
}
|
||||
|
||||
return server.reply({
|
||||
cmd: 'info',
|
||||
text: replyText,
|
||||
id: Info.Wallet.CONNECTED,
|
||||
channel: socket.channel,
|
||||
}, socket);
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
/**
|
||||
* The following payload properties are required to invoke this module:
|
||||
* "signature", "signedMessage"
|
||||
* @public
|
||||
* @typedef {Array} signsiw/requiredData
|
||||
*/
|
||||
export const requiredData = ['signature', 'signedMessage'];
|
||||
|
||||
/**
|
||||
* Module meta information
|
||||
* @public
|
||||
* @typedef {Object} signsiw/info
|
||||
* @property {string} name - Module command name
|
||||
* @property {string} category - Module category name
|
||||
* @property {string} description - Information about module
|
||||
* @property {string} usage - Information about module usage
|
||||
*/
|
||||
export const info = {
|
||||
name: 'signsiw',
|
||||
category: 'wallet',
|
||||
description: 'Verifies the wallet signature and syncs on-chain channel permissions',
|
||||
usage: `
|
||||
API: { cmd: 'signsiw', signature: '<base58 signature>', signedMessage: '<original text>' }`,
|
||||
};
|
||||
@@ -0,0 +1,181 @@
|
||||
/**
|
||||
* @author Marzavec
|
||||
* @summary Allow a siw
|
||||
* @version 1.0.0
|
||||
* @description Initiates a Sign-In-With-Solana request
|
||||
* @module siw
|
||||
*/
|
||||
|
||||
import crypto from 'crypto';
|
||||
|
||||
const solanaAddressRegex = /^[1-9A-HJ-NP-Za-km-z]{32,44}$/;
|
||||
const isValidSolanaAddress = (address) => solanaAddressRegex.test(address);
|
||||
|
||||
const getMessage = (locale, address, nonce, expires) => {
|
||||
const now = new Date();
|
||||
let header = 'hack.chat wants you to sign in with your Solana account:';
|
||||
let body = 'This action will authenticate your session and grant you access to restricted features.';
|
||||
const footer = `Version: 1
|
||||
Chain ID: solana:mainnet
|
||||
Nonce: ${nonce}
|
||||
Issued At: ${now.toISOString()}
|
||||
Expiration Time: ${expires.toISOString()}`;
|
||||
|
||||
// @todo
|
||||
switch (locale) {
|
||||
case 'ar':
|
||||
header = '';
|
||||
body = '';
|
||||
break;
|
||||
case 'bn':
|
||||
header = '';
|
||||
body = '';
|
||||
break;
|
||||
case 'cn':
|
||||
header = '';
|
||||
body = '';
|
||||
break;
|
||||
case 'de':
|
||||
header = '';
|
||||
body = '';
|
||||
break;
|
||||
case 'el':
|
||||
header = '';
|
||||
body = '';
|
||||
break;
|
||||
case 'es':
|
||||
header = '';
|
||||
body = '';
|
||||
break;
|
||||
case 'fa':
|
||||
header = '';
|
||||
body = '';
|
||||
break;
|
||||
case 'fi':
|
||||
header = '';
|
||||
body = '';
|
||||
break;
|
||||
case 'fr':
|
||||
header = '';
|
||||
body = '';
|
||||
break;
|
||||
case 'hi':
|
||||
header = '';
|
||||
body = '';
|
||||
break;
|
||||
case 'id':
|
||||
header = '';
|
||||
body = '';
|
||||
break;
|
||||
case 'it':
|
||||
header = '';
|
||||
body = '';
|
||||
break;
|
||||
case 'ja':
|
||||
header = '';
|
||||
body = '';
|
||||
break;
|
||||
case 'pt':
|
||||
header = '';
|
||||
body = '';
|
||||
break;
|
||||
case 'ru':
|
||||
header = '';
|
||||
body = '';
|
||||
break;
|
||||
case 'tr':
|
||||
header = '';
|
||||
body = '';
|
||||
break;
|
||||
case 'zh':
|
||||
header = '';
|
||||
body = '';
|
||||
break;
|
||||
default:
|
||||
break;
|
||||
}
|
||||
|
||||
return `${header}
|
||||
${address}
|
||||
|
||||
${body}
|
||||
|
||||
${footer}`;
|
||||
};
|
||||
|
||||
/**
|
||||
* Executes when invoked by a remote client
|
||||
* @param {Object} env - Enviroment object with references to core, server, socket & payload
|
||||
* @public
|
||||
* @return {void}
|
||||
*/
|
||||
export async function run({
|
||||
server, socket, payload,
|
||||
}) {
|
||||
// must be in a channel to run this command
|
||||
if (typeof socket.channel === 'undefined') {
|
||||
return server.police.frisk(socket, 1);
|
||||
}
|
||||
|
||||
if (typeof payload.address !== 'string') {
|
||||
return false;
|
||||
}
|
||||
|
||||
if (!isValidSolanaAddress(payload.address)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
if (typeof payload.wallet !== 'string' || payload.wallet.length > 64) {
|
||||
return false;
|
||||
}
|
||||
|
||||
/* if (typeof payload.locale !== 'string' || payload.locale.length > 4) {
|
||||
return false;
|
||||
} */
|
||||
|
||||
const expires = new Date();
|
||||
expires.setMinutes(expires.getMinutes() + 5);
|
||||
const nonceBuffer = crypto.randomBytes(16);
|
||||
const nonce = nonceBuffer.toString('hex');
|
||||
|
||||
const message = getMessage(
|
||||
/* payload.locale, */ 'en',
|
||||
payload.address,
|
||||
nonce,
|
||||
expires,
|
||||
);
|
||||
|
||||
socket.siwMsg = message;
|
||||
socket.siwAddress = payload.address;
|
||||
socket.siwExpiry = expires;
|
||||
|
||||
return server.reply({
|
||||
cmd: 'signMessage',
|
||||
wallet: payload.wallet,
|
||||
message,
|
||||
}, socket);
|
||||
}
|
||||
|
||||
/**
|
||||
* The following payload properties are required to invoke this module:
|
||||
* "address", "locale", "wallet"
|
||||
* @public
|
||||
* @typedef {Array} siw/requiredData
|
||||
*/
|
||||
export const requiredData = ['address', /* 'locale', */ 'wallet'];
|
||||
|
||||
/**
|
||||
* Module meta information
|
||||
* @public
|
||||
* @typedef {Object} siw/info
|
||||
* @property {string} name - Module command name
|
||||
* @property {string} description - Information about module
|
||||
* @property {string} usage - Information about module usage
|
||||
*/
|
||||
export const info = {
|
||||
name: 'siw',
|
||||
category: 'wallet',
|
||||
description: 'Initiates a Solana wallet login request',
|
||||
usage: `
|
||||
API: { cmd: 'siw', address: '<solana pubkey>', wallet: '<provider name>' }`,
|
||||
};
|
||||
@@ -57,8 +57,6 @@ setInterval(() => {
|
||||
purgeInactiveChannels(server.appConfig.data);
|
||||
}, ChannelCheckInterval);
|
||||
|
||||
// @todo create storage management job
|
||||
|
||||
// start the server
|
||||
server.init();
|
||||
|
||||
|
||||
Generated
+2442
-45
File diff suppressed because it is too large
Load Diff
+8
-1
@@ -30,13 +30,20 @@
|
||||
"author": "marzavec",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@coral-xyz/anchor": "^0.32.1",
|
||||
"@solana/kit": "^5.1.0",
|
||||
"@solana/web3.js": "^1.98.4",
|
||||
"ascii-captcha": "^0.0.3",
|
||||
"bs58": "^6.0.0",
|
||||
"enquirer": "^2.3.6",
|
||||
"hackchat-server": "^2.3.2",
|
||||
"http-server": "^14.1.0",
|
||||
"jsonwebtoken": "^9.0.2",
|
||||
"lowdb": "^3.0.0",
|
||||
"pm2": "^5.3.0"
|
||||
"markdown-escape": "^2.0.0",
|
||||
"pm2": "^5.3.0",
|
||||
"tweetnacl": "^1.0.3",
|
||||
"uwuify": "^1.0.1"
|
||||
},
|
||||
"devDependencies": {
|
||||
"c8": "^7.11.0",
|
||||
|
||||
Reference in New Issue
Block a user