This commit is contained in:
marzavec
2026-01-15 13:06:02 -06:00
parent 6915f8a543
commit 7435d0a5c4
54 changed files with 4422 additions and 437 deletions
+1
View File
@@ -133,3 +133,4 @@ session.key
salt.key
config.json
commands/admin/bomb.js
commands/mod/uwuify.js
-1
View File
@@ -52,4 +52,3 @@ textarea {
#chatform {
border-color: #ee600d;
}
+15 -6
View File
@@ -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
View File
@@ -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);
+6 -2
View File
@@ -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 });
+15 -6
View File
@@ -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 });
+4 -2
View File
@@ -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 });
+6 -2
View File
@@ -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
}, {});
+27 -5
View File
@@ -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 });
+4 -2
View File
@@ -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);
+6 -3
View File
@@ -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);
+4 -2
View File
@@ -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);
+8 -10
View File
@@ -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({
+13 -10
View File
@@ -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;
}
+4 -2
View File
@@ -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 });
+16 -12
View File
@@ -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',
+8 -8
View File
@@ -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
View File
@@ -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);
+3 -1
View File
@@ -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);
+1 -1
View File
@@ -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
View File
@@ -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;
}
+148
View File
@@ -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
View File
@@ -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
View File
@@ -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;
}
+7 -2
View File
@@ -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);
+25 -22
View File
@@ -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,
+1
View File
@@ -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
View File
@@ -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),
+8 -3
View File
@@ -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 });
+8 -5
View File
@@ -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
*/
+11 -4
View File
@@ -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);
}
+5 -11
View File
@@ -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>`,
};
+5 -5
View File
@@ -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
View File
@@ -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
View File
@@ -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`,
};
+8 -4
View File
@@ -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 });
+6 -3
View File
@@ -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 });
+8 -3
View File
@@ -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
View File
@@ -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
+96 -2
View File
@@ -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
+6 -1
View File
@@ -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;
+10 -1
View File
@@ -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);
+59
View File
@@ -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' }`,
};
+117
View File
@@ -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> }`,
};
+144
View File
@@ -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> }`,
};
+307
View File
@@ -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`,
};
+249
View File
@@ -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>' }`,
};
+181
View File
@@ -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>' }`,
};
-2
View File
@@ -57,8 +57,6 @@ setInterval(() => {
purgeInactiveChannels(server.appConfig.data);
}, ChannelCheckInterval);
// @todo create storage management job
// start the server
server.init();
+2442 -45
View File
File diff suppressed because it is too large Load Diff
+8 -1
View File
@@ -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",