1
0
mirror of https://github.com/hack-chat/main.git synced 2024-03-22 13:20:33 +08:00

Merge pull request #225 from hack-chat/version-2-dev

Version 2 dev
This commit is contained in:
marzavec 2023-12-29 21:45:01 -08:00 committed by GitHub
commit a3bca6d000
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
231 changed files with 63506 additions and 5122 deletions

9
.editorconfig Normal file
View File

@ -0,0 +1,9 @@
root = true
[*]
charset = utf-8
indent_style = space
indent_size = 2
end_of_line = lf
insert_final_newline = true
trim_trailing_whitespace = true

3
.eslintignore Normal file
View File

@ -0,0 +1,3 @@
/test/*.js
/client/*
/documentation/*

33
.eslintrc.cjs Normal file
View File

@ -0,0 +1,33 @@
module.exports = {
env: {
es2021: true,
node: true,
},
extends: [
'airbnb-base',
],
parserOptions: {
ecmaVersion: 'latest',
sourceType: 'module',
},
rules: {
"no-console": 0,
"no-param-reassign": [
"error",
{
"props": false,
}
],
"import/extensions": [
"error",
"ignorePackages",
{
js: "always",
jsx: "always",
ts: "always",
tsx: "always",
mjs: "always",
}
]
},
};

107
.gitattributes vendored Normal file
View File

@ -0,0 +1,107 @@
# From https://github.com/Danimoth/gitattributes/blob/master/Web.gitattributes
# Handle line endings automatically for files detected as text
# and leave all files detected as binary untouched.
* text=auto
#
# The above will handle all files NOT found below
#
#
## These files are text and should be normalized (Convert crlf => lf)
#
# source code
*.php text
*.css text
*.sass text
*.scss text
*.less text
*.styl text
*.js text eol=lf
*.coffee text
*.json text
*.htm text
*.html text
*.xml text
*.svg text
*.txt text
*.ini text
*.inc text
*.pl text
*.rb text
*.py text
*.scm text
*.sql text
*.sh text
*.bat text
# templates
*.ejs text
*.hbt text
*.jade text
*.haml text
*.hbs text
*.dot text
*.tmpl text
*.phtml text
# server config
.htaccess text
.nginx.conf text
# git config
.gitattributes text
.gitignore text
.gitconfig text
# code analysis config
.jshintrc text
.jscsrc text
.jshintignore text
.csslintrc text
# misc config
*.yaml text
*.yml text
.editorconfig text
# build config
*.npmignore text
*.bowerrc text
# Heroku
Procfile text
.slugignore text
# Documentation
*.md text
LICENSE text
AUTHORS text
#
## These files are binary and should be left untouched
#
# (binary is a macro for -text -diff)
*.png binary
*.jpg binary
*.jpeg binary
*.gif binary
*.ico binary
*.mov binary
*.mp4 binary
*.mp3 binary
*.flv binary
*.fla binary
*.swf binary
*.gz binary
*.zip binary
*.7z binary
*.ttf binary
*.eot binary
*.woff binary
*.pyc binary
*.pdf binary

85
.gitignore vendored
View File

@ -4,6 +4,11 @@ logs
npm-debug.log*
yarn-debug.log*
yarn-error.log*
lerna-debug.log*
.pnpm-debug.log*
# Diagnostic reports (https://nodejs.org/api/report.html)
report.[0-9]*.[0-9]*.[0-9]*.[0-9]*.json
# Runtime data
pids
@ -15,12 +20,13 @@ pids
lib-cov
# Coverage directory used by tools like istanbul
coverage
.coverage
*.lcov
# nyc test coverage
.nyc_output
# Grunt intermediate storage (http://gruntjs.com/creating-plugins#storing-task-files)
# Grunt intermediate storage (https://gruntjs.com/creating-plugins#storing-task-files)
.grunt
# Bower dependency directory (https://bower.io/)
@ -36,8 +42,11 @@ build/Release
node_modules/
jspm_packages/
# Typescript v1 declaration files
typings/
# Snowpack dependency directory (https://snowpack.dev/)
web_modules/
# TypeScript cache
*.tsbuildinfo
# Optional npm cache directory
.npm
@ -45,6 +54,15 @@ typings/
# Optional eslint cache
.eslintcache
# Optional stylelint cache
.stylelintcache
# Microbundle cache
.rpt2_cache/
.rts2_cache_cjs/
.rts2_cache_es/
.rts2_cache_umd/
# Optional REPL history
.node_repl_history
@ -54,10 +72,63 @@ typings/
# Yarn Integrity file
.yarn-integrity
# dotenv environment variables file
# dotenv environment variable files
.env
.env.development.local
.env.test.local
.env.production.local
.env.local
# next.js build output
# parcel-bundler cache (https://parceljs.org/)
.cache
.parcel-cache
# Next.js build output
.next
out
server/config/
# Nuxt.js build / generate output
.nuxt
dist
# Gatsby files
.cache/
# Comment in the public line in if your project uses Gatsby and not Next.js
# https://nextjs.org/blog/next-9-1#public-directory-support
# public
# vuepress build output
.vuepress/dist
# vuepress v2.x temp and cache directory
.temp
.cache
# Docusaurus cache and generated files
.docusaurus
# Serverless directories
.serverless/
# FuseBox cache
.fusebox/
# DynamoDB Local files
.dynamodb/
# TernJS port file
.tern-port
# Stores VSCode versions used for testing VSCode extensions
.vscode-test
# yarn v2
.yarn/cache
.yarn/unplugged
.yarn/build-state.yml
.yarn/install-state.gz
.pnp.*
session.key
salt.key
config.json

1
.hcserver.json Normal file
View File

@ -0,0 +1 @@
{"modulesPath":"./commands","websocketPort":"6060","rateLimit":{"halflife":"30000","threshold":"25"},"pulseSpeed":"16000"}

29
.nycrc Normal file
View File

@ -0,0 +1,29 @@
{
"check-coverage": true,
"per-file": true,
"lines": 80,
"statements": 80,
"functions": 80,
"branches": 80,
"include": [
"commands/**/*.js"
],
"exclude": [
"commands/**/*.spec.js"
],
"ignore-class-method": "methodToIgnore",
"reporter": [
"html",
"lcov",
"text",
"text-summary"
],
"require": [
"dotenv/config"
],
"extension": [],
"cache": true,
"all": true,
"temp-dir": "",
"report-dir": "./.coverage"
}

View File

@ -1,147 +0,0 @@
# Changelog
All notable changes to this project will be documented in this file.
## [Unreleased]
## [2.1.93 pre 2.2] - 2020-03-12
### Added
- (Source) `./pm2.config.js` PM2 ecosystem config handling both http-server and the websocket
- (Source) NPM new commands:
- "start": Starts or reloads the dev environment
- "stop": Stops and clears the dev environment
- "logs": Show / watch http & websocket logs for errors and events
- "clear": Clear all logged data
- "status": Show status of http and websocket
- "refresh": Clears logged data and stops http and websocket
- (Server) Numeric user levels / UAC, related to issue #86
- (Server) `join` module password property, related to V2 protocol update
- (Server) `users` array to `onlineSet` structure, related to V2 protocol update
- (Server) `session` module, related to V2 protocol update
- (Server) `/move` chat hook to `move` module
### Removed
- (Source) `./clientSource/` directory
### Changed
- (Server) Minor bug fixes
- (Server) Increased module abstraction to remove duplicate code (thanks @MinusGix)
## [2.1.92 pre 2.2] - 2019-11-06
### Added
- (Server) `./server/src/utility/` directory
- (Server) `Constants.js` class in `utility`
- (Server) `esm` module to transpile ES6
### Changed
- (Server) Changed ES5 styling to ES6
- (Server) And improved source comments
- (Server) Minor code format changes
- (Server) Updated all dependencies (be sure to update your local copy with the new packages)
## [2.1.91 pre 2.2] - 2019-08-17
### Added
- (Client) Markdown engine
- (Client) Imgur based image posting (through markdown)
### Changed
- (Client) Removed cloudflare references making hack.chat self-hosted again
- (Client) The way messages are pushed, closing an xss vuln in PRs 985dd6f and 9fcb235
- (Client) Side bar layout
- (Client) Fixed some options not storing
- (Client) Fixed firefox drop down menu bug
- (Client) Updated Katex lib
### Stretched
- The term "minimal"
## [2.1.9 pre 2.2] - 2019-03-18
### Changed
- Configuration script setup, making it more portable/sane
- Refactored naming scheme and entry point
### Removed
- Configuration setup from `./serverLib/ConfigManager`
- Unused feature allowing command modules to add to the configuration/setup process
- `deasync` dependency
## [2.1.9] - 2019-02-21
### Added
- `./server/src/commands/core/emote.js` module to provide action text
- `./server/src/core/server.js` priorities to command hooking
- Priority levels to all command modules
- `./server/src/commands/core/chat.js` Unknown '/' commands will now return a warning
- `./server/src/commands/internal/legacylayer.js` to provide compatibility to legacy connections
### Changed
- Updated all libraries to latest
- `./server/src/core/server.js` Removed unneeded function bindings
- `./server/src/core/server.js` Hook function layout
- `./server/src/managers/config.js` Documentation wording
## [2.1.0] - 2018-09-29
### Added
- Module hook framework, isolating modules and making them truly drop-to-install
- `./server/src/commands/core/whisper.js` module to send in-channel private messages, `/whisper` hook
- `muzzle` and `mute` aliases to `./server/src/commands/mod/dumb.js`
- `unmuzzle` and `unmute` aliases to `./server/src/commands/mod/speak.js`
- `./server/src/commands/admin/removemod.js` module to remove mods
- `./server/src/commands/mod/unbanall.js` module to clear all bans and ratelimiting
### Changed
- Further code cleanup on all modules
- Adjusted `ipSalt` entropy
- `./server/src/commands/core/help.js` output is now helpful, added `/help` hook
- `./server/src/commands/core/chat.js` added `/myhash` and `/me` hooks
- `./server/src/commands/core/morestats.js` added `/stats` hook
## [2.0.3] - 2018-06-03
### Added
- `./server/src/commands/mod/dumb.js` module for server-wide shadow muting
- `./server/src/commands/mod/speak.js` module unmuting
- `./server/src/commands/internal/socketreply.js` module to route warning to clients
- `./server/src/commands/core/ping.js` module to prevent `didYouMean` errors on legacy sources
### Changed
- Moved `disconnect.js` into servers internal modules directory
- Restructured `server.js` and `commands.js`, removing hardcoded protocol use
## [2.0.2] - 2018-05-19
### Added
- `./documentation/DOCUMENTATION.md` document which gives overview of the applications protocol
- `./documentation/DEPLOY.md` document which gives overview of deploying the server live
- `./LICENSE` License file
- Code highlighting, triggered with #
### Changed
- `README.md` wording and layout
### Removed
- Unneeded `use strict`
## [2.0.1] - 2018-04-18
### Added
- `users-kicked` tracking to `morestats` command
- Server-side ping interval
- `move` command to change channels without reconnecting
- `disconnect` command module to free core server from protocol dependency
- `changenick` command to change client nick without reconnecting
### Changed
- Filter object of the `findSockets` function now accepts more complex parameters, including functions and arrays
- `kick` command now accepts an array as the `nick` argument allowing multiple simultaneous kicks
- `join` command now takes advantage of the new filter object
- Core server disconnect handler now calls the `disconnect` module instead of broadcasting hard coded `onlineRemove`
### Removed
- Client-side ping interval
## [2.0.0] - 2018-04-12
### Added
- CHANGELOG.md
- `index.html` files to `katex` directories
### Changed
- Updated client html KaTeX libraries to v0.9.0
### Removed
- Uneeded files under `katex` directories

13
LICENSE
View File

@ -1,13 +0,0 @@
DO WHAT THE FUCK YOU WANT TO PUBLIC LICENSE
Version 2, December 2004
Copyright (C) 2004 Sam Hocevar <sam@hocevar.net>
Everyone is permitted to copy and distribute verbatim or modified
copies of this license document, and changing it is allowed as long
as the name is changed.
DO WHAT THE FUCK YOU WANT TO PUBLIC LICENSE
TERMS AND CONDITIONS FOR COPYING, DISTRIBUTION AND MODIFICATION
0. You just DO WHAT THE FUCK YOU WANT TO.

View File

@ -4,14 +4,14 @@
A list of software developed for the hack.chat framework can be found at the [3rd party software list](https://github.com/hack-chat/3rd-party-software-list) repository. This includes bots, clients, docker containers, etc.
This is a backwards compatible continuation of the [work by Andrew Belt](https://github.com/AndrewBelt/hack.chat). The server code has been updated to ES6 along with several new features including new commands and hot-reload of the commands/protocol. There is also [documentation](documentation/DOCUMENTATION.md) and a [changelog](CHANGELOG.md).
This is a backwards compatible continuation of the [work by Andrew Belt](https://github.com/AndrewBelt/hack.chat). The server code has been updated to ES6 along with several new features including new commands and hot-reload of the commands/protocol. There is also [documentation](documentation/index.html).
# Installation
## Prerequisites
- [node.js 8.10.0](https://nodejs.org/en/download/package-manager/#windows) or higher
- [npm 5.7.1](https://nodejs.org/en/download/package-manager/#windows) or higher
- [node.js v16.14.0](https://nodejs.org/) or higher
- [npm 8.5.4](https://nodejs.org/) or higher
## Developer Installation
@ -20,19 +20,14 @@ This is a backwards compatible continuation of the [work by Andrew Belt](https:/
1. Install the dependencies: `npm install`
1. Launch: `npm start`
If you change the `websocketPort` option during the config setup then these changes will need to be reflected on [line 60 of client.js](https://github.com/hack-chat/main/blob/master/client/client.js#L60).
## Live Deployment Installation
See [DEPLOY.md](documentation/DEPLOY.md)
# Contributing
- If you are modifying commands, make sure it is backwards compatible with the legacy client and you update the documentation accordingly.
- Use [the template](documentation/templateCommand.js) to learn how to create new commands.
- Use two space indents.
- Name files in camelCase.
- Scripts that do not default to strict mode (such as modules) must use the `'use strict'` directive.
# Credits
@ -41,10 +36,10 @@ See [DEPLOY.md](documentation/DEPLOY.md)
* [**Neel Kamath**](https://github.com/neelkamath) - *Base Documentation*
* [**Carlos Villavicencio**](https://github.com/po5i) - *Syntax Highlighting Integration*
* [**OpSimple**](https://github.com/OpSimple) - *Modules Added: dumb.js & speak.js*
* Andrew Belt, https://github.com/AndrewBelt, for original base work
* [wwandrew](https://github.com/wwandrew), for finding server flaws (including attack vectors) and submitting ~~___incredibly detailed___~~ bug reports
* [Everyone else](https://github.com/hack-chat/main/graphs/contributors) who participated in this project.
* [**Andrew Belt**](https://github.com/AndrewBelt), for original base work
* [**wwandrew**](https://github.com/wwandrew), for finding server flaws (including attack vectors) and submitting ~~___incredibly detailed___~~ bug reports
* [**Everyone else**](https://github.com/hack-chat/main/graphs/contributors) who participated in this project.
# License
This project is licensed under the [WTFPL License](LICENSE).
This project is licensed under the [MIT License](LICENSE).

View File

@ -23,7 +23,7 @@ var markdownOptions = {
langPrefix: '',
linkify: true,
linkTarget: '_blank" rel="noreferrer',
typographer: true,
typographer: true,
quotes: `""''`,
doHighlight: true,
@ -33,12 +33,12 @@ var markdownOptions = {
if (lang && hljs.getLanguage(lang)) {
try {
return hljs.highlight(lang, str).value;
} catch (__) {}
} catch (__) { }
}
try {
return hljs.highlightAuto(str).value;
} catch (__) {}
} catch (__) { }
return '';
}
@ -75,20 +75,20 @@ md.renderer.rules.image = function (tokens, idx, options) {
return '<a href="' + src + '" target="_blank" rel="noreferrer"><img' + scrollOnload + imgSrc + alt + title + suffix + '></a>';
}
return '<a href="' + src + '" target="_blank" rel="noreferrer">' + Remarkable.utils.escapeHtml(Remarkable.utils.replaceEntities(src)) + '</a>';
return '<a href="' + src + '" target="_blank" rel="noreferrer">' + Remarkable.utils.escapeHtml(Remarkable.utils.replaceEntities(src)) + '</a>';
};
md.renderer.rules.link_open = function (tokens, idx, options) {
var title = tokens[idx].title ? (' title="' + Remarkable.utils.escapeHtml(Remarkable.utils.replaceEntities(tokens[idx].title)) + '"') : '';
var target = options.linkTarget ? (' target="' + options.linkTarget + '"') : '';
return '<a rel="noreferrer" onclick="return verifyLink(this)" href="' + Remarkable.utils.escapeHtml(tokens[idx].href) + '"' + title + target + '>';
var target = options.linkTarget ? (' target="' + options.linkTarget + '"') : '';
return '<a rel="noreferrer" onclick="return verifyLink(this)" href="' + Remarkable.utils.escapeHtml(tokens[idx].href) + '"' + title + target + '>';
};
md.renderer.rules.text = function(tokens, idx) {
md.renderer.rules.text = function (tokens, idx) {
tokens[idx].content = Remarkable.utils.escapeHtml(tokens[idx].content);
if (tokens[idx].content.indexOf('?') !== -1) {
tokens[idx].content = tokens[idx].content.replace(/(^|\s)(\?)\S+?(?=[,.!?:)]?\s|$)/gm, function(match) {
tokens[idx].content = tokens[idx].content.replace(/(^|\s)(\?)\S+?(?=[,.!?:)]?\s|$)/gm, function (match) {
var channelLink = Remarkable.utils.escapeHtml(Remarkable.utils.replaceEntities(match.trim()));
var whiteSpace = '';
if (match[0] !== '?') {
@ -98,7 +98,7 @@ md.renderer.rules.text = function(tokens, idx) {
});
}
return tokens[idx].content;
return tokens[idx].content;
};
md.use(remarkableKatex);
@ -172,12 +172,40 @@ var myChannel = window.location.search.replace(/^\?/, '');
var lastSent = [""];
var lastSentPos = 0;
/**
* Stores active messages
* These are messages that can be edited.
* @type {{ customId: string, userid: number, sent: number, text: string, elem: HTMLElement }[]}
*/
var activeMessages = [];
setInterval(function () {
var editTimeout = 6 * 60 * 1000;
var now = Date.now();
for (var i = 0; i < activeMessages.length; i++) {
if (now - activeMessages[i].sent > editTimeout) {
activeMessages.splice(i, 1);
i--;
}
}
}, 30 * 1000);
function addActiveMessage(customId, userid, text, elem) {
activeMessages.push({
customId,
userid,
sent: Date.now(),
text,
elem,
});
}
/** Notification switch and local storage behavior **/
var notifySwitch = document.getElementById("notify-switch")
var notifySetting = localStorageGet("notify-api")
var notifyPermissionExplained = 0; // 1 = granted msg shown, -1 = denied message shown
// Inital request for notifications permission
// Initial request for notifications permission
function RequestNotifyPermission() {
try {
var notifyPromise = Notification.requestPermission();
@ -361,7 +389,9 @@ function join(channel) {
var args = JSON.parse(message.data);
var cmd = args.cmd;
var command = COMMANDS[cmd];
command.call(null, args);
if (command) {
command.call(null, args);
}
}
}
@ -370,7 +400,63 @@ var COMMANDS = {
if (ignoredUsers.indexOf(args.nick) >= 0) {
return;
}
pushMessage(args);
var elem = pushMessage(args);
if (typeof (args.customId) === 'string') {
addActiveMessage(args.customId, args.userid, args.text, elem);
}
},
updateMessage: function (args) {
var customId = args.customId;
var mode = args.mode;
if (!mode) {
return;
}
var message;
for (var i = 0; i < activeMessages.length; i++) {
var msg = activeMessages[i];
if (msg.userid === args.userid && msg.customId === customId) {
if (mode === 'complete') {
activeMessages.splice(i, 1);
return;
}
message = msg;
break;
}
}
if (!message) {
return;
}
var textElem = message.elem.querySelector('.text');
if (!textElem) {
return;
}
var newText = message.text;
if (mode === 'overwrite') {
newText = args.text;
} else if (mode === 'append') {
newText += args.text;
} else if (mode === 'prepend') {
newText = args.text + newText;
}
message.text = newText;
// Scroll to bottom if necessary
var atBottom = isAtBottom();
textElem.innerHTML = md.render(newText);
if (atBottom) {
window.scrollTo(0, document.body.scrollHeight);
}
},
info: function (args) {
@ -378,6 +464,11 @@ var COMMANDS = {
pushMessage(args);
},
emote: function (args) {
args.nick = '*';
pushMessage(args);
},
warn: function (args) {
args.nick = '!';
pushMessage(args);
@ -413,6 +504,30 @@ var COMMANDS = {
if ($('#joined-left').checked) {
pushMessage({ nick: '*', text: nick + " left" });
}
},
captcha: function (args) {
var messageEl = document.createElement('div');
messageEl.classList.add('info');
var nickSpanEl = document.createElement('span');
nickSpanEl.classList.add('nick');
messageEl.appendChild(nickSpanEl);
var nickLinkEl = document.createElement('a');
nickLinkEl.textContent = '#';
nickSpanEl.appendChild(nickLinkEl);
var textEl = document.createElement('pre');
textEl.style.fontSize = '4px';
textEl.classList.add('text');
textEl.innerHTML = args.text;
messageEl.appendChild(textEl);
$('#messages').appendChild(messageEl);
window.scrollTo(0, document.body.scrollHeight);
}
}
@ -450,7 +565,13 @@ function pushMessage(args) {
if (args.trip) {
var tripEl = document.createElement('span');
tripEl.textContent = args.trip + " ";
if (args.mod) {
tripEl.textContent = String.fromCodePoint(11088) + " " + args.trip + " ";
} else {
tripEl.textContent = args.trip + " ";
}
tripEl.classList.add('trip');
nickSpanEl.appendChild(tripEl);
}
@ -459,6 +580,10 @@ function pushMessage(args) {
var nickLinkEl = document.createElement('a');
nickLinkEl.textContent = args.nick;
if (args.color && /(^[0-9A-F]{6}$)|(^[0-9A-F]{3}$)/i.test(args.color)) {
nickLinkEl.setAttribute('style', 'color:#' + args.color + ' !important');
}
nickLinkEl.onclick = function () {
insertAtCursor("@" + args.nick + " ");
$('#chatinput').focus();
@ -485,6 +610,8 @@ function pushMessage(args) {
unread += 1;
updateTitle();
return messageEl;
}
function insertAtCursor(text) {
@ -641,12 +768,23 @@ $('#chatinput').onkeydown = function (e) {
}
}
// from https://stackoverflow.com/questions/11381673/detecting-a-mobile-browser
function checkIsMobileOrTablet() {
let check = false;
(function(a){if(/(android|bb\d+|meego).+mobile|avantgo|bada\/|blackberry|blazer|compal|elaine|fennec|hiptop|iemobile|ip(hone|od)|iris|kindle|lge |maemo|midp|mmp|mobile.+firefox|netfront|opera m(ob|in)i|palm( os)?|phone|p(ixi|re)\/|plucker|pocket|psp|series(4|6)0|symbian|treo|up\.(browser|link)|vodafone|wap|windows ce|xda|xiino|android|ipad|playbook|silk/i.test(a)||/1207|6310|6590|3gso|4thp|50[1-6]i|770s|802s|a wa|abac|ac(er|oo|s\-)|ai(ko|rn)|al(av|ca|co)|amoi|an(ex|ny|yw)|aptu|ar(ch|go)|as(te|us)|attw|au(di|\-m|r |s )|avan|be(ck|ll|nq)|bi(lb|rd)|bl(ac|az)|br(e|v)w|bumb|bw\-(n|u)|c55\/|capi|ccwa|cdm\-|cell|chtm|cldc|cmd\-|co(mp|nd)|craw|da(it|ll|ng)|dbte|dc\-s|devi|dica|dmob|do(c|p)o|ds(12|\-d)|el(49|ai)|em(l2|ul)|er(ic|k0)|esl8|ez([4-7]0|os|wa|ze)|fetc|fly(\-|_)|g1 u|g560|gene|gf\-5|g\-mo|go(\.w|od)|gr(ad|un)|haie|hcit|hd\-(m|p|t)|hei\-|hi(pt|ta)|hp( i|ip)|hs\-c|ht(c(\-| |_|a|g|p|s|t)|tp)|hu(aw|tc)|i\-(20|go|ma)|i230|iac( |\-|\/)|ibro|idea|ig01|ikom|im1k|inno|ipaq|iris|ja(t|v)a|jbro|jemu|jigs|kddi|keji|kgt( |\/)|klon|kpt |kwc\-|kyo(c|k)|le(no|xi)|lg( g|\/(k|l|u)|50|54|\-[a-w])|libw|lynx|m1\-w|m3ga|m50\/|ma(te|ui|xo)|mc(01|21|ca)|m\-cr|me(rc|ri)|mi(o8|oa|ts)|mmef|mo(01|02|bi|de|do|t(\-| |o|v)|zz)|mt(50|p1|v )|mwbp|mywa|n10[0-2]|n20[2-3]|n30(0|2)|n50(0|2|5)|n7(0(0|1)|10)|ne((c|m)\-|on|tf|wf|wg|wt)|nok(6|i)|nzph|o2im|op(ti|wv)|oran|owg1|p800|pan(a|d|t)|pdxg|pg(13|\-([1-8]|c))|phil|pire|pl(ay|uc)|pn\-2|po(ck|rt|se)|prox|psio|pt\-g|qa\-a|qc(07|12|21|32|60|\-[2-7]|i\-)|qtek|r380|r600|raks|rim9|ro(ve|zo)|s55\/|sa(ge|ma|mm|ms|ny|va)|sc(01|h\-|oo|p\-)|sdk\/|se(c(\-|0|1)|47|mc|nd|ri)|sgh\-|shar|sie(\-|m)|sk\-0|sl(45|id)|sm(al|ar|b3|it|t5)|so(ft|ny)|sp(01|h\-|v\-|v )|sy(01|mb)|t2(18|50)|t6(00|10|18)|ta(gt|lk)|tcl\-|tdg\-|tel(i|m)|tim\-|t\-mo|to(pl|sh)|ts(70|m\-|m3|m5)|tx\-9|up(\.b|g1|si)|utst|v400|v750|veri|vi(rg|te)|vk(40|5[0-3]|\-v)|vm40|voda|vulc|vx(52|53|60|61|70|80|81|83|85|98)|w3c(\-| )|webc|whit|wi(g |nc|nw)|wmlb|wonu|x700|yas\-|your|zeto|zte\-/i.test(a.substr(0,4))) check = true;})(navigator.userAgent||navigator.vendor||window.opera);
return check;
};
var isMobileOrTablet = checkIsMobileOrTablet();
function updateInputSize() {
var atBottom = isAtBottom();
if (!isMobileOrTablet) {
var input = $('#chatinput');
input.style.height = 0;
input.style.height = input.scrollHeight + 'px';
}
var input = $('#chatinput');
input.style.height = 0;
input.style.height = input.scrollHeight + 'px';
document.body.style.marginBottom = $('#footer').offsetHeight + 'px';
if (atBottom) {
@ -672,8 +810,8 @@ $('#sidebar').onmouseleave = document.ontouchstart = function (event) {
var e = event.toElement || event.relatedTarget;
try {
if (e.parentNode == this || e == this) {
return;
}
return;
}
} catch (e) { return; }
if (!$('#pin-sidebar').checked) {
@ -701,8 +839,8 @@ if (localStorageGet('joined-left') == 'false') {
if (localStorageGet('parse-latex') == 'false') {
$('#parse-latex').checked = false;
md.inline.ruler.disable([ 'katex' ]);
md.block.ruler.disable([ 'katex' ]);
md.inline.ruler.disable(['katex']);
md.block.ruler.disable(['katex']);
}
$('#pin-sidebar').onchange = function (e) {
@ -717,11 +855,11 @@ $('#parse-latex').onchange = function (e) {
var enabled = !!e.target.checked;
localStorageSet('parse-latex', enabled);
if (enabled) {
md.inline.ruler.enable([ 'katex' ]);
md.block.ruler.enable([ 'katex' ]);
md.inline.ruler.enable(['katex']);
md.block.ruler.enable(['katex']);
} else {
md.inline.ruler.disable([ 'katex' ]);
md.block.ruler.disable([ 'katex' ]);
md.inline.ruler.disable(['katex']);
md.block.ruler.disable(['katex']);
}
}
@ -830,15 +968,16 @@ var schemes = [
'pop',
'railscasts',
'solarized',
'tk-night',
'tomorrow',
'tk-night',
'carrot',
'lax',
'Ubuntu',
'gruvbox-light',
'fried-egg',
'rainbow',
'amoled'
'amoled',
'retro'
];
var highlights = [
@ -892,7 +1031,7 @@ $('#highlight-selector').onchange = function (e) {
setHighlight(e.target.value);
}
// Load sidebar configaration values from local storage if available
// Load sidebar configuration values from local storage if available
if (localStorageGet('scheme')) {
setScheme(localStorageGet('scheme'));
}

View File

@ -24,7 +24,7 @@
<source src="audio/notify.mp3" type="audio/ogg">
</audio>
<article class="container">
<div id="messages" class="messages"></div>
<div id="messages"></div>
</article>
<footer id="footer">
<div class="container">

View File

@ -1,69 +1,69 @@
:root {
color-scheme: dark!important;
scrollbar-color: #8e8e8e #151515;
}
body {
background: #151515;
color: #8e8e8e;
}
pre {
margin-top: 0.3rem;
margin-bottom: 0.3rem;
overflow: hidden;
padding: 0.1rem;
font-weight: 400;
background: #1d1f21;
border: 0;
border-radius: 3px;
}
code{
background: #444;
}
input,
textarea {
color: #8e8e8e;
}
.message {
border-left: 1px solid rgba(116, 115, 105, 0.1);
}
.refmessage {
border-left: 1px solid rgba(116, 115, 105, 1);
}
.nick {
color: #6699cc;
}
.trip {
color: #515151;
}
.text a {
color: #5c9c9f;
}
.admin .nick {
color: #f2777a;
}
.mod .nick {
color: #66cccc;
}
.me .nick {
color: #cc99cc;
}
.info .nick,
.info .text {
color: #3e9353;
}
.warn .nick,
.warn .text {
color: #ffcc66;
}
#footer {
background: #151515;
}
#sidebar {
background: #111;
border-color: #20201e;
}
#chatform {
border-color: rgba(116, 115, 105, 0.15);
}
:root {
color-scheme: dark!important;
scrollbar-color: #8e8e8e #151515;
}
body {
background: #151515;
color: #8e8e8e;
}
pre {
margin-top: 0.3rem;
margin-bottom: 0.3rem;
overflow: hidden;
padding: 0.1rem;
font-weight: 400;
background: #1d1f21;
border: 0;
border-radius: 3px;
}
code{
background: #444;
}
input,
textarea {
color: #8e8e8e;
}
.message {
border-left: 1px solid rgba(116, 115, 105, 0.1);
}
.refmessage {
border-left: 1px solid rgba(116, 115, 105, 1);
}
.nick {
color: #6699cc;
}
.trip {
color: #515151;
}
.text a {
color: #5c9c9f;
}
.admin .nick {
color: #f2777a;
}
.mod .nick {
color: #66cccc;
}
.me .nick {
color: #cc99cc;
}
.info .nick,
.info .text {
color: #3e9353;
}
.warn .nick,
.warn .text {
color: #ffcc66;
}
#footer {
background: #151515;
}
#sidebar {
background: #111;
border-color: #20201e;
}
#chatform {
border-color: rgba(116, 115, 105, 0.15);
}

View File

@ -221,10 +221,8 @@ ul ul, ol ol {
}
@media only screen and (max-width: 600px) {
.messages {
border: none;
}
#messages {
border: none;
padding: 0.5em;
}
.message {

108
commands/admin/addmod.js Normal file
View File

@ -0,0 +1,108 @@
/**
* @author Marzavec ( https://github.com/marzavec )
* @summary Create a new mod trip
* @version 1.0.0
* @description Adds target trip to the config as a mod and upgrades the socket type
* @module addmod
*/
import {
isAdmin,
isModerator,
levels,
getUserDetails,
} from '../utility/_UAC.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({
core, server, socket, payload,
}) {
// increase rate limit chance and ignore if not admin
if (!isAdmin(socket.level)) {
return server.police.frisk(socket, 20);
}
// add new trip to config
core.appConfig.data.globalMods.push({ trip: payload.trip });
// find targets current connections
const newMod = server.findSockets({ trip: payload.trip });
if (newMod.length !== 0) {
// build update notice with new privileges
const updateNotice = {
...getUserDetails(newMod[0]),
...{
cmd: 'updateUser',
uType: 'mod', // @todo use legacyLevelToLabel from _LegacyFunctions.js
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].level = levels.moderator;
// inform new mod
server.send({
cmd: 'info',
text: 'You are now a mod.',
channel: newMod[i].channel, // @todo Multichannel
}, newMod[i]);
// notify channel
server.broadcast({
...updateNotice,
...{
channel: newMod[i].channel,
},
}, { channel: newMod[i].channel });
}
}
// return success message
server.reply({
cmd: 'info',
text: `Added mod trip: ${payload.trip}, remember to run 'saveconfig' to make it permanent`,
channel: socket.channel, // @todo Multichannel
}, socket);
// notify all mods
server.broadcast({
cmd: 'info',
text: `Added mod: ${payload.trip}`,
channel: false, // @todo Multichannel, false for global info
}, { level: isModerator });
return true;
}
/**
* The following payload properties are required to invoke this module:
* "trip"
* @public
* @typedef {Array} addmod/requiredData
*/
export const requiredData = ['trip'];
/**
* Module meta information
* @public
* @typedef {Object} addmod/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: 'addmod',
category: 'admin',
description: 'Adds target trip to the config as a mod and upgrades the socket type',
usage: `
API: { cmd: 'addmod', trip: '<target trip>' }`,
};

View File

@ -0,0 +1,77 @@
/* eslint no-unused-vars: 0 */
/* eslint no-restricted-syntax: 0 */
/* eslint guard-for-in: 0 */
/**
* @author Marzavec ( https://github.com/marzavec )
* @summary Show users and channels
* @version 1.0.0
* @description Outputs all current channels and sockets in those channels
* @module listusers
*/
import {
isAdmin,
} from '../utility/_UAC.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 }) {
// increase rate limit chance and ignore if not admin
if (!isAdmin(socket.level)) {
return server.police.frisk(socket, 20);
}
// find all users currently in a channel
const currentUsers = server.findSockets({
channel: (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] = [];
}
channels[currentUsers[i].channel].push(
`[${currentUsers[i].trip || 'null'}]${currentUsers[i].nick}`,
);
}
// build output
const lines = [];
for (const channel in channels) {
lines.push(`?${channel} ${channels[channel].join(', ')}`);
}
// send reply
server.reply({
cmd: 'info',
text: lines.join('\n'),
channel: socket.channel, // @todo Multichannel
}, socket);
return true;
}
/**
* Module meta information
* @public
* @typedef {Object} listusers/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: 'listusers',
category: 'admin',
description: 'Outputs all current channels and sockets in those channels',
usage: `
API: { cmd: 'listusers' }`,
};

71
commands/admin/reload.js Normal file
View File

@ -0,0 +1,71 @@
/**
* @author Marzavec ( https://github.com/marzavec )
* @summary Refresh modules
* @version 1.0.0
* @description Allows a remote user to clear and re-import the server command modules
* @module reload
*/
import {
isAdmin,
isModerator,
} from '../utility/_UAC.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({
core, server, socket, payload,
}) {
// increase rate limit chance and ignore if not admin
if (!isAdmin(socket.level)) {
return server.police.frisk(socket, 20);
}
// do command reload and store results
let loadResult = await core.commands.reloadCommands();
// clear and rebuild all module hooks
server.loadHooks();
// build reply based on reload results
if (loadResult === '') {
loadResult = `Reloaded ${core.commands.commands.length} commands, 0 errors`;
} else {
loadResult = `Reloaded ${core.commands.commands.length} commands, error(s):
${loadResult}`;
}
if (typeof payload.reason !== 'undefined') {
loadResult += `\nReason: ${payload.reason}`;
}
// send results to moderators (which the user using this command is higher than)
server.broadcast({
cmd: 'info',
text: loadResult,
channel: false, // @todo Multichannel, false for global
}, { level: isModerator });
return true;
}
/**
* Module meta information
* @public
* @typedef {Object} reload/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: 'reload',
category: 'admin',
description: 'Allows a remote user to clear and re-import the server command modules',
usage: `
API: { cmd: 'reload', reason: '<optional reason append>' }`,
};

113
commands/admin/removemod.js Normal file
View File

@ -0,0 +1,113 @@
/**
* @author Marzavec ( https://github.com/marzavec )
* @summary Removes a mod
* @version 1.0.0
* @description Removes target trip from the config as a mod and downgrades the socket type
* @module removemod
*/
import {
isAdmin,
isModerator,
levels,
getUserDetails,
} from '../utility/_UAC.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({
core, server, socket, payload,
}) {
// increase rate limit chance and ignore if not admin
if (!isAdmin(socket.level)) {
return server.police.frisk(socket, 20);
}
// remove trip from config
// eslint-disable-next-line no-param-reassign
core.appConfig.data.globalMods = core.appConfig.data.globalMods.filter(
(mod) => mod.trip !== payload.trip,
);
// find targets current connections
const targetMod = server.findSockets({ trip: payload.trip });
if (targetMod.length !== 0) {
// build update notice with new privileges
const updateNotice = {
...getUserDetails(targetMod[0]),
...{
cmd: 'updateUser',
uType: 'user', // @todo use legacyLevelToLabel from _LegacyFunctions.js
level: levels.default,
},
};
for (let i = 0, l = targetMod.length; i < l; i += 1) {
// downgrade privileges
targetMod[i].uType = 'user';
targetMod[i].level = levels.default;
// inform ex-mod
server.send({
cmd: 'info',
text: 'You are now a user.',
channel: targetMod[i].channel, // @todo Multichannel
}, targetMod[i]);
// notify channel
server.broadcast({
...updateNotice,
...{
channel: targetMod[i].channel,
},
}, { channel: targetMod[i].channel });
}
}
// return success message
server.reply({
cmd: 'info',
text: `Removed mod trip: ${
payload.trip
}, remember to run 'saveconfig' to make it permanent`,
channel: socket.channel, // @todo Multichannel
}, socket);
// notify all mods
server.broadcast({
cmd: 'info',
text: `Removed mod: ${payload.trip}`,
channel: false, // @todo Multichannel, false for global
}, { level: isModerator });
return true;
}
/**
* The following payload properties are required to invoke this module:
* "trip"
* @public
* @typedef {Array} removemod/requiredData
*/
export const requiredData = ['trip'];
/**
* Module meta information
* @public
* @typedef {Object} removemod/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: 'removemod',
category: 'admin',
description: 'Removes target trip from the config as a mod and downgrades the socket type',
usage: `
API: { cmd: 'removemod', trip: '<target trip>' }`,
};

View File

@ -0,0 +1,62 @@
/**
* @author Marzavec ( https://github.com/marzavec )
* @summary Saves the config
* @version 1.0.0
* @description Writes the current config to disk
* @module saveconfig
*/
import {
isAdmin,
isModerator,
} from '../utility/_UAC.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({ core, server, socket }) {
// increase rate limit chance and ignore if not admin
if (!isAdmin(socket.level)) {
return server.police.frisk(socket, 20);
}
// attempt save, notify of failure
try {
await core.appConfig.write();
} catch (err) {
return server.reply({
cmd: 'warn', // @todo Add numeric error code as `id`
text: 'Failed to save config, check logs.',
channel: socket.channel, // @todo Multichannel
}, socket);
}
// return success message to moderators and admins
server.broadcast({
cmd: 'info',
text: 'Config saved!',
channel: false, // @todo Multichannel
}, { level: isModerator });
return true;
}
/**
* Module meta information
* @public
* @typedef {Object} saveconfig/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: 'saveconfig',
category: 'admin',
description: 'Writes the current config to disk',
usage: `
API: { cmd: 'saveconfig' }`,
};

58
commands/admin/shout.js Normal file
View File

@ -0,0 +1,58 @@
/**
* @author Marzavec ( https://github.com/marzavec )
* @summary Emit text everywhere
* @version 1.0.0
* @description Displays passed text to every client connected
* @module shout
*/
import {
isAdmin,
} from '../utility/_UAC.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 }) {
// increase rate limit chance and ignore if not admin
if (!isAdmin(socket.level)) {
return server.police.frisk(socket, 20);
}
// send text to all channels
server.broadcast({
cmd: 'info',
text: `Server Notice: ${payload.text}`,
channel: false, // @todo Multichannel, false for global
}, {});
return true;
}
/**
* The following payload properties are required to invoke this module:
* "text"
* @public
* @typedef {Array} shout/requiredData
*/
export const requiredData = ['text'];
/**
* Module meta information
* @public
* @typedef {Object} shout/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: 'shout',
category: 'admin',
description: 'Displays passed text to every client connected',
usage: `
API: { cmd: 'shout', text: '<shout text>' }`,
};

View File

@ -0,0 +1,158 @@
/**
* @author Marzavec ( https://github.com/marzavec )
* @summary Update name color
* @version 1.0.0
* @description Allows calling client to change their nickname color
* @module changecolor
*/
import {
getUserDetails,
} from '../utility/_UAC.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 - Enviroment object with references to core, server, socket & payload
* @public
* @return {void}
*/
export async function run({
server, socket, payload,
}) {
const { channel } = socket;
if (server.police.frisk(socket, 1)) {
return server.reply({
cmd: 'warn', // @todo Add numeric error code as `id`
text: 'You are changing colors too fast. Wait a moment before trying again.',
channel, // @todo Multichannel
}, socket);
}
// verify user data is string
if (typeof payload.color !== 'string') {
return false;
}
// make sure requested nickname meets standards
const newColor = payload.color.trim().toUpperCase().replace(/#/g, '');
if (newColor !== 'RESET' && !verifyColor(newColor)) {
return server.reply({
cmd: 'warn',
text: 'Invalid color! Color must be in hex value',
channel, // @todo Multichannel
}, socket);
}
if (newColor === 'RESET') {
socket.color = false; // eslint-disable-line no-param-reassign
} else {
socket.color = newColor; // eslint-disable-line no-param-reassign
}
// build update notice with new color
const updateNotice = {
...getUserDetails(socket),
...{
cmd: 'updateUser',
channel: socket.channel, // @todo Multichannel
},
};
// notify channel that the user has changed their name
// @todo this should be sent to every channel the user is in (multichannel)
server.broadcast(updateNotice, { channel: socket.channel });
return true;
}
/**
* Automatically executes once after server is ready to register this modules hooks
* @param {Object} server - Reference to server enviroment object
* @public
* @return {void}
*/
export function initHooks(server) {
server.registerHook('in', 'chat', this.colorCheck.bind(this), 29);
}
/**
* Executes every time an incoming chat command is invoked
* @param {Object} env - Enviroment object with references to core, server, socket & payload
* @public
* @return {(Object|boolean|string)} Object = same/altered payload,
* false = suppress action,
* string = error
*/
export function colorCheck({
core, server, socket, payload,
}) {
if (typeof payload.text !== 'string') {
return false;
}
if (payload.text.startsWith('/color ')) {
const input = payload.text.split(' ');
// If there is no color target parameter
if (input[1] === undefined) {
server.reply({
cmd: 'warn',
text: 'Refer to `/help changecolor` for instructions on how to use this command.',
channel: socket.channel, // @todo Multichannel
}, socket);
return false;
}
this.run({
core,
server,
socket,
payload: {
cmd: 'changecolor',
color: input[1],
},
});
return false;
}
return payload;
}
/**
* The following payload properties are required to invoke this module:
* "color"
* @public
* @typedef {Array} changecolor/requiredData
*/
export const requiredData = ['color'];
/**
* Module meta information
* @public
* @typedef {Object} changecolor/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: 'changecolor',
category: 'core',
description: 'Allows calling client to change their nickname color',
usage: `
API: { cmd: 'changecolor', color: '<new color as hex>' }
Text: /color <new color as hex>
Removal: /color reset`,
};

213
commands/core/changenick.js Normal file
View File

@ -0,0 +1,213 @@
/* eslint eqeqeq: 0 */
/**
* @author Marzavec ( https://github.com/marzavec )
* @summary Update nickname
* @version 1.0.0
* @description Allows calling client to change their current nickname
* @module changenick
*/
import {
verifyNickname,
getUserDetails,
} from '../utility/_UAC.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,
}) {
const { channel } = socket;
if (server.police.frisk(socket, 6)) {
return server.reply({
cmd: 'warn', // @todo Add numeric error code as `id`
text: 'You are changing nicknames too fast. Wait a moment before trying again.',
channel, // @todo Multichannel
}, socket);
}
// verify user data is string
if (typeof payload.nick !== 'string') {
return true;
}
const previousNick = socket.nick;
// make sure requested nickname meets standards
const newNick = payload.nick.trim();
if (!verifyNickname(newNick)) {
return server.reply({
cmd: 'warn', // @todo Add numeric error code as `id`
text: 'Nickname must consist of up to 24 letters, numbers, and underscores',
channel, // @todo Multichannel
}, socket);
}
if (newNick == previousNick) {
return server.reply({
cmd: 'warn', // @todo Add numeric error code as `id`
text: 'You already have that name',
channel, // @todo Multichannel
}, socket);
}
// find any sockets that have the same nickname
const userExists = server.findSockets({
channel,
nick: (targetNick) => targetNick.toLowerCase() === newNick.toLowerCase()
// 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
return server.reply({
cmd: 'warn', // @todo Add numeric error code as `id`
text: 'Nickname taken',
channel, // @todo Multichannel
}, socket);
}
// build update notice with new nickname
const updateNotice = {
...getUserDetails(socket),
...{
cmd: 'updateUser',
nick: newNick,
channel, // @todo Multichannel
},
};
// build join and leave notices for legacy clients
const leaveNotice = {
cmd: 'onlineRemove',
userid: socket.userid,
nick: socket.nick,
channel, // @todo Multichannel
};
const joinNotice = {
...getUserDetails(socket),
...{
cmd: 'onlineAdd',
nick: newNick,
channel, // @todo Multichannel
},
};
// gather channel peers
const peerList = server.findSockets({ channel });
for (let i = 0, l = peerList.length; i < l; i += 1) {
if (peerList[i].hcProtocol === 1) {
// send join/leave to legacy clients
server.send(leaveNotice, peerList[i]);
server.send(joinNotice, peerList[i]);
} else {
// send update info
// @todo this should be sent to every channel the client is in (multichannel)
server.send(updateNotice, peerList[i]);
}
}
// notify channel that the user has changed their name
server.broadcast({
cmd: 'info',
text: `${socket.nick} is now ${newNick}`,
channel, // @todo Multichannel
}, { channel });
// commit change to nickname
socket.nick = newNick; // eslint-disable-line no-param-reassign
return true;
}
/**
* Automatically executes once after server is ready to register this modules hooks
* @param {Object} server - Reference to server enviroment object
* @public
* @return {void}
*/
export function initHooks(server) {
server.registerHook('in', 'chat', this.nickCheck.bind(this), 29);
}
/**
* Executes every time an incoming chat command is invoked
* @param {Object} env - Enviroment object with references to core, server, socket & payload
* @public
* @return {(Object|boolean|string)} Object = same/altered payload,
* false = suppress action,
* string = error
*/
export function nickCheck({
core, server, socket, payload,
}) {
if (typeof payload.text !== 'string') {
return false;
}
if (payload.text.startsWith('/nick')) {
const input = payload.text.split(' ');
// If there is no nickname target parameter
if (!input[1]) {
server.reply({
cmd: 'warn', // @todo Add numeric error code as `id`
text: 'Refer to `/help nick` for instructions on how to use this command.',
channel: socket.channel, // @todo Multichannel
}, socket);
return false;
}
const newNick = input[1].replace(/@/g, '');
this.run({
core,
server,
socket,
payload: {
cmd: 'changenick',
nick: newNick,
},
});
return false;
}
return payload;
}
/**
* The following payload properties are required to invoke this module:
* "nick"
* @public
* @typedef {Array} changenick/requiredData
*/
export const requiredData = ['nick'];
/**
* Module meta information
* @public
* @typedef {Object} changenick/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: 'changenick',
category: 'core',
description: 'Allows calling client to change their current nickname',
usage: `
API: { cmd: 'changenick', nick: '<new nickname>' }
Text: /nick <new nickname>`,
};

245
commands/core/chat.js Normal file
View File

@ -0,0 +1,245 @@
/**
* @author Marzavec ( https://github.com/marzavec )
* @summary Send chat messages
* @version 1.0.0
* @description Broadcasts passed `text` field to the calling users channel
* @module chat
*/
import {
parseText,
} from '../utility/_Text.js';
import {
isAdmin,
isModerator,
} from '../utility/_UAC.js';
/**
* Maximum length of the customId property
* @type {number}
*/
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.
* @type {number}
*/
const ACTIVE_TIMEOUT = 5 * 60 * 1000;
/**
* 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.
* @type {Array}
*/
export const ACTIVE_MESSAGES = [];
/**
* Cleans up stale messages.
* @public
* @return {void}
*/
export function cleanActiveMessages() {
const now = Date.now();
for (let i = 0; i < ACTIVE_MESSAGES.length; 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);
/**
* Adds a message to the active messages map.
* @public
* @param {string} id
* @param {number} userid
* @return {void}
*/
export function addActiveMessage(customId, userid) {
ACTIVE_MESSAGES.push({
customId,
userid,
sent: Date.now(),
toDelete: false,
});
}
/**
* 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,
}) {
// check user input
const text = parseText(payload.text);
if (!text) {
// lets not send objects or empty text, yea?
return server.police.frisk(socket, 13);
}
// check for spam
const score = text.length / 83 / 4;
if (server.police.frisk(socket, score)) {
return server.reply({
cmd: 'warn', // @todo Add numeric error code as `id`
text: 'You are sending too much text. Wait a moment and try again.\nPress the up arrow key to restore your last message.',
channel: socket.channel, // @todo Multichannel
}, socket);
}
const { customId } = payload;
if (typeof (customId) === 'string' && customId.length > MAX_MESSAGE_ID_LENGTH) {
// There's a limit on the custom id length.
return server.police.frisk(socket, 13);
}
// build chat payload
const outgoingPayload = {
cmd: 'chat',
nick: socket.nick, /* @legacy */
uType: socket.uType, /* @legacy */
userid: socket.userid,
channel: socket.channel,
text,
level: socket.level,
customId,
};
if (isAdmin(socket.level)) {
outgoingPayload.admin = true;
} else if (isModerator(socket.level)) {
outgoingPayload.mod = true;
}
if (socket.trip) {
outgoingPayload.trip = socket.trip; /* @legacy */
}
if (socket.color) {
outgoingPayload.color = socket.color;
}
addActiveMessage(outgoingPayload.customId, socket.userid);
// broadcast to channel peers
server.broadcast(outgoingPayload, { channel: socket.channel });
// stats are fun
core.stats.increment('messages-sent');
return true;
}
/**
* Automatically executes once after server is ready to register this modules hooks
* @param {Object} server - Reference to server enviroment object
* @public
* @return {void}
*/
export function initHooks(server) {
server.registerHook('in', 'chat', this.commandCheckIn.bind(this), 20);
server.registerHook('in', 'chat', this.finalCmdCheck.bind(this), 254);
}
/**
* Executes every time an incoming chat command is invoked;
* checks for miscellaneous '/' based commands
* @param {Object} env - Enviroment object with references to core, server, socket & payload
* @public
* @return {(Object|boolean|string)} Object = same/altered payload,
* false = suppress action,
* string = error
*/
export function commandCheckIn({ server, socket, payload }) {
if (typeof payload.text !== 'string') {
return false;
}
if (payload.text.startsWith('/myhash')) {
server.reply({
cmd: 'info',
text: `Your hash: ${socket.hash}`,
channel: socket.channel, // @todo Multichannel
}, socket);
return false;
}
return payload;
}
/**
* Executes every time an incoming chat command is invoked;
* assumes a failed chat command invocation and will reject with notice
* @param {Object} env - Enviroment object with references to core, server, socket & payload
* @public
* @return {(Object|boolean|string)} Object = same/altered payload,
* false = suppress action,
* string = error
*/
export function finalCmdCheck({ server, socket, payload }) {
if (typeof payload.text !== 'string') {
return false;
}
if (!payload.text.startsWith('/')) {
return payload;
}
if (payload.text.startsWith('//')) {
payload.text = payload.text.substr(1); // eslint-disable-line no-param-reassign
return payload;
}
server.reply({
cmd: 'warn', // @todo Add numeric error code as `id`
text: `Unknown command: ${payload.text}`,
channel: socket.channel, // @todo Multichannel
}, socket);
return false;
}
/**
* The following payload properties are required to invoke this module:
* "text"
* @public
* @typedef {Array} chat/requiredData
*/
export const requiredData = ['text'];
/**
* Module meta information
* @public
* @typedef {Object} chat/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: 'chat',
category: 'core',
description: 'Broadcasts passed `text` field to the calling users channel',
usage: `
API: { cmd: 'chat', text: '<text to send>' }
Text: Uuuuhm. Just kind type in that little box at the bottom and hit enter.\n
Bonus super secret hidden commands:
/myhash`,
};

162
commands/core/emote.js Normal file
View File

@ -0,0 +1,162 @@
/**
* @author Marzavec ( https://github.com/marzavec )
* @summary Emote / action text
* @version 1.0.0
* @description Broadcasts an emote to the current channel
* @module emote
*/
/**
* Check and trim string provided by remote client
* @param {string} text - Subject string
* @private
* @todo Move into utility module
* @return {string|boolean}
*/
const parseText = (text) => {
// verifies user input is text
if (typeof text !== 'string') {
return false;
}
let sanitizedText = text;
// strip newlines from beginning and end
sanitizedText = sanitizedText.replace(/^\s*\n|^\s+$|\n\s*$/g, '');
// replace 3+ newlines with just 2 newlines
sanitizedText = sanitizedText.replace(/\n{3,}/g, '\n\n');
return sanitizedText;
};
/**
* 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 }) {
// check user input
let text = parseText(payload.text);
if (!text) {
// lets not send objects or empty text, yea?
return server.police.frisk(socket, 8);
}
// check for spam
const score = text.length / 83 / 4;
if (server.police.frisk(socket, score)) {
return server.reply({
cmd: 'warn', // @todo Add numeric error code as `id`
text: 'You are sending too much text. Wait a moment and try again.\nPress the up arrow key to restore your last message.',
channel: socket.channel, // @todo Multichannel
}, socket);
}
if (!text.startsWith("'")) {
text = ` ${text}`;
}
const newPayload = {
cmd: 'emote',
nick: socket.nick,
userid: socket.userid,
text: `@${socket.nick}${text}`,
channel: socket.channel, // @todo Multichannel
};
if (socket.trip) {
newPayload.trip = socket.trip;
}
// broadcast to channel peers
server.broadcast(newPayload, { channel: socket.channel });
return true;
}
/**
* Automatically executes once after server is ready to register this modules hooks
* @param {Object} server - Reference to server enviroment object
* @public
* @return {void}
*/
export function initHooks(server) {
server.registerHook('in', 'chat', this.emoteCheck.bind(this), 30);
}
/**
* Executes every time an incoming chat command is invoked;
* hooks chat commands checking for /me
* @param {Object} env - Enviroment object with references to core, server, socket & payload
* @public
* @return {(Object|boolean|string)} Object = same/altered payload,
* false = suppress action,
* string = error
*/
export function emoteCheck({
core, server, socket, payload,
}) {
if (typeof payload.text !== 'string') {
return false;
}
if (payload.text.startsWith('/me ')) {
const input = payload.text.split(' ');
// If there is no emote target parameter
if (input[1] === undefined) {
server.reply({
cmd: 'warn', // @todo Add numeric error code as `id`
text: 'Refer to `/help emote` for instructions on how to use this command.',
channel: socket.channel, // @todo Multichannel
}, socket);
return false;
}
input.splice(0, 1);
const actionText = input.join(' ');
this.run({
core,
server,
socket,
payload: {
cmd: 'emote',
text: actionText,
},
});
return false;
}
return payload;
}
/**
* The following payload properties are required to invoke this module:
* "text"
* @public
* @typedef {Array} emote/requiredData
*/
export const requiredData = ['text'];
/**
* Module meta information
* @public
* @typedef {Object} emote/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: 'emote',
category: 'core',
description: 'Broadcasts an emote to the current channel',
usage: `
API: { cmd: 'emote', text: '<emote/action text>' }
Text: /me <emote/action text>`,
};

View File

@ -1,14 +1,26 @@
/*
Description: Outputs the current command module list or command categories
*/
/**
* @author Marzavec ( https://github.com/marzavec )
* @summary Get help
* @version 1.0.0
* @description Outputs information about the servers current protocol
* @module help
*/
// module main
export async function run(core, server, socket, payload) {
/**
* 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,
}) {
// check for spam
if (server.police.frisk(socket.address, 2)) {
if (server.police.frisk(socket, 2)) {
return server.reply({
cmd: 'warn',
cmd: 'warn', // @todo Add numeric error code as `id`
text: 'You are sending too much text. Wait a moment and try again.\nPress the up arrow key to restore your last message.',
channel: socket.channel, // @todo Multichannel
}, socket);
}
@ -24,7 +36,9 @@ export async function run(core, server, socket, payload) {
const categories = core.commands.categoriesList.sort();
for (let i = 0, j = categories.length; i < j; i += 1) {
reply += `|${categories[i].replace('../src/commands/', '').replace(/^\w/, (c) => c.toUpperCase())}:|`;
const catCommands = core.commands.all(categories[i]).sort((a, b) => a.info.name.localeCompare(b.info.name));
const catCommands = core.commands.all(categories[i]).sort(
(a, b) => a.info.name.localeCompare(b.info.name),
);
reply += `${catCommands.map((c) => `${c.info.name}`).join(', ')}|\n`;
}
@ -37,9 +51,11 @@ export async function run(core, server, socket, payload) {
} else {
reply += `# ${command.info.name} command:\n| | |\n|---:|---|\n`;
reply += `|**Name:**|${command.info.name}|\n`;
reply += `|**Hash:**|${command.info.srcHash}|\n`;
reply += `|**Aliases:**|${typeof command.info.aliases !== 'undefined' ? command.info.aliases.join(', ') : 'None'}|\n`;
reply += `|**Category:**|${command.info.category.replace('../src/commands/', '').replace(/^\w/, (c) => c.toUpperCase())}|\n`;
reply += `|**Required Parameters:**|${command.requiredData || 'None'}|\n`;
// eslint-disable-next-line no-useless-escape
reply += `|**Description:**|${command.info.description || '¯\_(ツ)_/¯'}|\n\n`;
reply += `**Usage:** ${command.info.usage || command.info.name}`;
}
@ -49,18 +65,34 @@ export async function run(core, server, socket, payload) {
server.reply({
cmd: 'info',
text: reply,
channel: socket.channel, // @todo Multichannel
}, socket);
return true;
}
// module hook functions
/**
* Automatically executes once after server is ready to register this modules hooks
* @param {Object} server - Reference to server enviroment object
* @public
* @return {void}
*/
export function initHooks(server) {
server.registerHook('in', 'chat', this.helpCheck.bind(this), 28);
}
// hooks chat commands checking for /whisper
export function helpCheck(core, server, socket, payload) {
/**
* Executes every time an incoming chat command is invoked;
* hooks chat commands checking for /help
* @param {Object} env - Enviroment object with references to core, server, socket & payload
* @public
* @return {(Object|boolean|string)} Object = same/altered payload,
* false = suppress action,
* string = error
*/
export function helpCheck({
core, server, socket, payload,
}) {
if (typeof payload.text !== 'string') {
return false;
}
@ -68,9 +100,14 @@ export function helpCheck(core, server, socket, payload) {
if (payload.text.startsWith('/help')) {
const input = payload.text.substr(1).split(' ', 2);
this.run(core, server, socket, {
cmd: input[0],
command: input[1],
this.run({
core,
server,
socket,
payload: {
cmd: input[0],
command: input[1],
},
});
return false;
@ -79,8 +116,18 @@ export function helpCheck(core, server, socket, payload) {
return payload;
}
/**
* Module meta information
* @public
* @typedef {Object} help/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: 'help',
category: 'core',
description: 'Outputs information about the servers current protocol',
usage: `
API: { cmd: 'help', command: '<optional command name>' }

123
commands/core/invite.js Normal file
View File

@ -0,0 +1,123 @@
/**
* @author Marzavec ( https://github.com/marzavec )
* @summary Send an invite
* @version 1.0.0
* @description Sends an invite to the target client with the provided channel, or a random channel
* @module invite
*/
import {
findUser,
} from '../utility/_Channels.js';
import {
Errors,
} from '../utility/_Constants.js';
import {
legacyInviteOut,
legacyInviteReply,
} from '../utility/_LegacyFunctions.js';
/**
* Returns the channel that should be invited to.
* @param {any} channel
* @private
* @return {string}
*/
export function getChannel(channel = undefined) {
if (typeof channel === 'string') {
return channel;
}
return Math.random().toString(36).substr(2, 8);
}
/**
* 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,
}) {
// check for spam
if (server.police.frisk(socket, 2)) {
return server.reply({
cmd: 'warn',
text: 'You are sending invites too fast. Wait a moment before trying again.',
id: Errors.Invite.RATELIMIT,
channel: socket.channel, // @todo Multichannel
}, socket);
}
// verify user input
// if this is a legacy client add missing params to payload
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') {
return true;
}
// @todo Verify this socket is part of payload.channel - multichannel patch
// find target user
const targetUser = findUser(server, payload);
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);
}
// generate common channel
const channel = getChannel(payload.to);
// build invite
const outgoingPayload = {
cmd: 'invite',
channel: socket.channel, // @todo Multichannel
from: socket.userid,
to: targetUser.userid,
inviteChannel: channel,
};
// send invite notice to target client
if (targetUser.hcProtocol === 1) {
server.reply(legacyInviteOut(outgoingPayload, socket.nick), targetUser);
} else {
server.reply(outgoingPayload, targetUser);
}
// send invite notice to this client
if (socket.hcProtocol === 1) {
server.reply(legacyInviteReply(outgoingPayload, targetUser.nick), socket);
} else {
server.reply(outgoingPayload, socket);
}
// stats are fun
core.stats.increment('invites-sent');
return true;
}
/**
* Module meta information
* @public
* @typedef {Object} invite/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: 'invite',
category: 'core',
description: 'Sends an invite to the target client with the provided channel, or a random channel.',
usage: `
API: { cmd: 'invite', nick: '<target nickname>', to: '<optional destination channel>' }`,
};

278
commands/core/join.js Normal file
View File

@ -0,0 +1,278 @@
/* eslint no-param-reassign: 0 */
/* eslint import/no-cycle: [0, { ignoreExternal: true }] */
/**
* @author Marzavec ( https://github.com/marzavec )
* @summary Join target channel
* @version 1.0.0
* @description Join the target channel using the supplied nick and password
* @module join
*/
import {
getSession,
} from './session.js';
import {
canJoinChannel,
socketInChannel,
} from '../utility/_Channels.js';
import {
Errors,
} from '../utility/_Constants.js';
import {
upgradeLegacyJoin,
legacyLevelToLabel,
} from '../utility/_LegacyFunctions.js';
import {
verifyNickname,
getUserPerms,
getUserDetails,
isModerator,
} from '../utility/_UAC.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({
core, server, socket, payload,
}) {
// check for spam
if (server.police.frisk(socket, 3)) {
return server.reply({
cmd: 'warn',
text: 'You are joining channels too fast. Wait a moment and try again.',
id: Errors.Join.RATELIMIT,
channel: false, // @todo Multichannel, false for global event
}, socket);
}
// `join` is the legacy entry point, check if it needs to be upgraded
if (typeof socket.hcProtocol === 'undefined' || socket.hcProtocol === 1) {
payload = upgradeLegacyJoin(server, socket, payload);
}
// store payload values
const { channel, nick, pass } = payload;
// check if a client is able to join target channel
const mayJoin = canJoinChannel(channel, socket);
if (mayJoin !== true) {
return server.reply({
cmd: 'warn',
text: 'You may not join that channel.',
id: mayJoin,
channel: false, // @todo Multichannel, false for global event
}, socket);
}
// calling socket already in a channel
// @todo multichannel update, will remove
if (typeof socket.channel !== 'undefined') {
return server.reply({
cmd: 'warn', // @todo Remove this
text: 'Joining more than one channel is not currently supported',
id: Errors.Join.ALREADY_JOINED,
channel: false, // @todo Multichannel, false for global event
}, socket);
}
// end todo
// validates the user input for `nick`
if (verifyNickname(nick, socket) !== true) {
return server.reply({
cmd: 'warn',
text: 'Nickname must consist of up to 24 letters, numbers, and underscores',
id: Errors.Join.INVALID_NICK,
channel: false, // @todo Multichannel, false for global event
}, socket);
}
// get trip and level
const { trip, level } = getUserPerms(pass, core.saltKey, core.appConfig.data, channel);
// store the user values
const userInfo = {
nick,
trip,
uType: legacyLevelToLabel(level),
hash: socket.hash,
level,
userid: socket.userid,
isBot: socket.isBot,
color: socket.color,
channel,
};
// check if the nickname already exists in the channel
const userExists = server.findSockets({
channel,
nick: (targetNick) => targetNick.toLowerCase() === userInfo.nick.toLowerCase(),
});
if (userExists.length > 0) {
// that nickname is already in that channel
return server.reply({
cmd: 'warn',
text: 'Nickname taken',
id: Errors.Join.NAME_TAKEN,
channel: false, // @todo Multichannel, false for global event
}, socket);
}
// prepare to notify channel peers
const newPeerList = server.findSockets({ channel });
const nicks = []; /* @legacy */
const users = [];
const joinAnnouncement = { ...{ cmd: 'onlineAdd' }, ...userInfo };
// send join announcement and prep online set reply
for (let i = 0, l = newPeerList.length; i < l; i += 1) {
server.reply(joinAnnouncement, newPeerList[i]);
nicks.push(newPeerList[i].nick); /* @legacy */
users.push({
...{
channel,
isme: false,
},
...getUserDetails(newPeerList[i]),
});
}
// store user info
socket.nick = userInfo.nick;
socket.trip = userInfo.trip;
socket.level = userInfo.level;
socket.uType = userInfo.uType; /* @legacy */
socket.channel = channel; /* @legacy */
// @todo multi-channel patch
// socket.channels.push(channel);
socket.channels = [channel];
// global mod perks
if (isModerator(socket.level)) {
socket.ratelimitImmune = true;
}
nicks.push(userInfo.nick); /* @legacy */
users.push({ ...{ isme: true, isBot: socket.isBot }, ...userInfo });
// reply with channel peer list
server.reply({
cmd: 'onlineSet',
nicks, /* @legacy */
users,
channel, // @todo Multichannel (?)
}, socket);
// update client with new session info
server.reply({
cmd: 'session',
restored: false,
token: getSession(socket, core),
channels: socket.channels,
}, socket);
// stats are fun
core.stats.increment('users-joined');
return true;
}
export function restoreJoin({
server, socket, channel,
}) {
// check if a client is able to join target channel
const mayJoin = canJoinChannel(channel, socket);
if (mayJoin !== true) {
return server.reply({
cmd: 'warn',
text: 'You may not join that channel.',
id: mayJoin,
channel: false, // @todo Multichannel, false for global event
}, socket);
}
// store the user values
const userInfo = {
nick: socket.nick,
trip: socket.trip,
uType: legacyLevelToLabel(socket.level),
hash: socket.hash,
level: socket.level,
userid: socket.userid,
isBot: socket.isBot,
color: socket.color,
channel,
};
// prepare to notify channel peers
const newPeerList = server.findSockets({ channel });
const nicks = []; /* @legacy */
const users = [];
const joinAnnouncement = { ...{ cmd: 'onlineAdd' }, ...userInfo };
// build update notice with new privileges
const updateAnnouncement = {
...getUserDetails(socket),
...{
cmd: 'updateUser',
online: true,
},
};
const isDuplicate = socketInChannel(server, channel, socket);
// send join announcement and prep online set reply
for (let i = 0, l = newPeerList.length; i < l; i += 1) {
if (isDuplicate) {
server.reply(updateAnnouncement, newPeerList[i]);
} else {
server.reply(joinAnnouncement, newPeerList[i]);
}
nicks.push(newPeerList[i].nick); /* @legacy */
users.push({
...{
channel,
isme: false,
},
...getUserDetails(newPeerList[i]),
});
}
nicks.push(userInfo.nick); /* @legacy */
users.push({ ...{ isme: true, isBot: socket.isBot }, ...userInfo });
// reply with channel peer list
server.reply({
cmd: 'onlineSet',
nicks, /* @legacy */
users,
channel, // @todo Multichannel (?)
}, socket);
socket.channel = channel; /* @legacy */
socket.channels.push(channel);
return true;
}
/**
* Module meta information
* @public
* @typedef {Object} join/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: 'join',
category: 'core',
description: 'Join the target channel using the supplied nick and password',
usage: `
API: { cmd: 'join', nick: '<your nickname>', pass: '<optional password>', channel: '<target channel>' }`,
};

164
commands/core/morestats.js Normal file
View File

@ -0,0 +1,164 @@
/**
* @author Marzavec ( https://github.com/marzavec )
* @summary Get stats
* @version 1.0.0
* @description Sends back current server stats to the calling client
* @module morestats
*/
/**
* Format input time into string
* @param {Date} time - Subject date
* @private
* @return {string}
*/
const formatTime = (time) => {
let seconds = time[0] + time[1] / 1e9;
let minutes = Math.floor(seconds / 60);
seconds %= 60;
let hours = Math.floor(minutes / 60);
minutes %= 60;
const days = Math.floor(hours / 24);
hours %= 24;
return `${days.toFixed(0)}d ${hours.toFixed(0)}h ${minutes.toFixed(0)}m ${seconds.toFixed(0)}s`;
};
/**
* 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 }) {
// 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,
};
// @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') {
publicChanCounts[client.channel] += 1;
}
}
});
const uniqueClientCount = Object.keys(ips).length;
const uniqueChannels = Object.keys(channels).length;
const joins = core.stats.get('users-joined') || 0;
const invites = core.stats.get('invites-sent') || 0;
const messages = core.stats.get('messages-sent') || 0;
const banned = core.stats.get('users-banned') || 0;
const kicked = core.stats.get('users-kicked') || 0;
const stats = core.stats.get('stats-requested') || 0;
const uptime = formatTime(process.hrtime(core.stats.get('start-time')));
// dispatch info
server.reply({
cmd: 'info',
users: uniqueClientCount,
chans: uniqueChannels,
joins,
invites,
messages,
banned,
kicked,
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}`,
channel: socket.channel, // @todo Multichannel
}, socket);
// stats are fun
core.stats.increment('stats-requested');
return true;
}
/**
* Automatically executes once after server is ready to register this modules hooks
* @param {Object} server - Reference to server enviroment object
* @public
* @return {void}
*/
export function initHooks(server) {
server.registerHook('in', 'chat', this.statsCheck.bind(this), 26);
}
/**
* Executes every time an incoming chat command is invoked;
* hooks chat commands checking for /stats
* @param {Object} env - Enviroment object with references to core, server, socket & payload
* @public
* @return {(Object|boolean|string)} Object = same/altered payload,
* false = suppress action,
* string = error
*/
export function statsCheck({
core, server, socket, payload,
}) {
if (typeof payload.text !== 'string') {
return false;
}
if (payload.text.startsWith('/stats')) {
this.run({
core,
server,
socket,
payload: {
cmd: 'morestats',
},
});
return false;
}
return payload;
}
/**
* Module meta information
* @public
* @typedef {Object} morestats/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: 'morestats',
category: 'core',
description: 'Sends back current server stats to the calling client',
usage: `
API: { cmd: 'morestats' }
Text: /stats`,
};

32
commands/core/ping.js Normal file
View File

@ -0,0 +1,32 @@
/* eslint no-empty-function: 0 */
/**
* @author Marzavec ( https://github.com/marzavec )
* @summary Legacy support module
* @version 1.0.0
* @description This module is only in place to supress error notices legacy clients may get
* @module ping
*/
/**
* Executes when invoked by a remote client
* @public
* @return {void}
*/
export async function run() { }
/**
* Module meta information
* @public
* @typedef {Object} ping/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: 'ping',
category: 'core',
description: 'This module is only in place to supress error notices legacy clients may get',
usage: 'none',
};

199
commands/core/session.js Normal file
View File

@ -0,0 +1,199 @@
/* eslint import/no-cycle: [0, { ignoreExternal: true }] */
/**
* @author Marzavec ( https://github.com/marzavec )
* @summary Create or restore session
* @version 1.0.0
* @description Restore previous state by session or create new session
* @module session
*/
import fs from 'fs';
import jsonwebtoken from 'jsonwebtoken';
import {
isModerator,
verifyNickname,
} from '../utility/_UAC.js';
import {
Errors,
} from '../utility/_Constants.js';
import {
restoreJoin,
} from './join.js';
const SessionLocation = './session.key';
/**
* Get a new json web token for the provided socket
* @param {*} socket
* @param {*} core
* @returns {object}
*/
export function getSession(socket, core) {
return jsonwebtoken.sign({
channel: socket.channel,
channels: socket.channels,
color: socket.color,
isBot: socket.isBot,
level: socket.level,
nick: socket.nick,
trip: socket.trip,
userid: socket.userid,
uType: socket.uType,
muzzled: socket.muzzled || false,
banned: socket.banned || false,
}, core.sessionKey, {
expiresIn: '7 days',
});
}
/**
* Reply to target socket with session failure notice
* @param {*} server
* @param {*} socket
* @returns {boolean}
*/
function notifyFailure(server, socket) {
server.reply({
cmd: 'error',
id: Errors.Session.BAD_SESSION,
text: 'Invalid session',
}, socket);
return false;
}
/**
* 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,
}) {
if (typeof payload.token === 'undefined') {
return notifyFailure(server, socket);
}
let session = false;
try {
session = jsonwebtoken.verify(payload.token, core.sessionKey);
} catch (err) {
return notifyFailure(server, socket);
}
// validate session
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);
}
// populate socket info with validated session
socket.channels = [];
socket.color = session.color;
socket.isBot = session.isBot;
socket.level = session.level;
socket.nick = session.nick;
socket.trip = session.trip;
socket.userid = session.userid;
socket.uType = session.uType;
socket.muzzled = session.muzzled;
socket.banned = session.banned;
// global mod perks
if (isModerator(socket.level)) {
socket.ratelimitImmune = true;
}
socket.hash = server.getSocketHash(socket);
socket.hcProtocol = 2;
// dispatch info
server.reply({
cmd: 'session',
restored: true,
token: getSession(socket, core),
channels: socket.channels,
}, socket);
for (let i = 0, j = session.channels.length; i < j; i += 1) {
restoreJoin({
core,
server,
socket,
channel: session.channels[i],
}, true);
}
return true;
}
/**
* Automatically executes once after server is ready
* @param {Object} core - Reference to core enviroment object
* @public
* @return {void}
*/
export function init(core) {
// load the encryption key if required
if (typeof core.sessionKey === 'undefined') {
core.sessionKey = fs.readFileSync(SessionLocation);
}
}
/**
* Module meta information
* @public
* @typedef {Object} session/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: 'session',
category: 'core',
description: 'Restore previous state by session or create new session',
usage: "API: { cmd: 'session', id: '<previous session>' }",
};

59
commands/core/stats.js Normal file
View File

@ -0,0 +1,59 @@
/**
* @author Marzavec ( https://github.com/marzavec )
* @summary Simple stats
* @version 1.0.0
* @description Sends back legacy server stats to the calling client
* @module stats
*/
/**
* 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 }) {
// 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;
ips[client.address] = true;
}
});
const uniqueClientCount = Object.keys(ips).length;
const uniqueChannels = Object.keys(channels).length;
ips = null;
channels = null;
// dispatch info
server.reply({
cmd: 'info',
text: `${uniqueClientCount} unique IPs in ${uniqueChannels} channels`,
channel: socket.channel, // @todo Multichannel
}, socket);
// stats are fun
core.stats.increment('stats-requested');
}
/**
* Module meta information
* @public
* @typedef {Object} stats/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: 'stats',
category: 'core',
description: 'Sends back legacy server stats to the calling client',
usage: `
API: { cmd: 'stats' }`,
};

View File

@ -0,0 +1,127 @@
/**
* @author MinusGix ( https://github.com/MinusGix )
* @summary Change target message
* @version v1.0.0
* @description Will alter a previously sent message using that message's customId
* @module updateMessage
*/
import {
parseText,
} from '../utility/_Text.js';
import {
isAdmin,
isModerator,
} from '../utility/_UAC.js';
import {
ACTIVE_MESSAGES,
MAX_MESSAGE_ID_LENGTH,
} from './chat.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,
}) {
// undefined | "overwrite" | "append" | "prepend" | "complete"
const { customId } = payload;
let { mode, text } = payload;
if (!mode) {
mode = 'overwrite';
}
if (mode !== 'overwrite' && mode !== 'append' && mode !== 'prepend' && mode !== 'complete') {
return server.police.frisk(socket, 13);
}
if (!customId || typeof customId !== 'string' || customId.length > MAX_MESSAGE_ID_LENGTH) {
return server.police.frisk(socket, 13);
}
if (typeof (text) !== 'string') {
return server.police.frisk(socket, 13);
}
if (mode === 'overwrite') {
text = parseText(text);
if (text === '') {
text = '\u0000';
}
}
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.
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;
}
}
if (!message) {
return server.police.frisk(socket, 6);
}
const outgoingPayload = {
cmd: 'updateMessage',
userid: socket.userid,
channel: socket.channel,
level: socket.level,
mode,
text,
customId: message.customId,
};
if (isAdmin(socket.level)) {
outgoingPayload.admin = true;
} else if (isModerator(socket.level)) {
outgoingPayload.mod = true;
}
server.broadcast(outgoingPayload, { channel: socket.channel });
return true;
}
/**
* The following payload properties are required to invoke this module:
* "text", "customId"
* @public
* @typedef {Array} addmod/requiredData
*/
export const requiredData = ['text', 'customId'];
/**
* Module meta information
* @public
* @typedef {Object} updateMessage/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: 'updateMessage',
category: 'core',
description: 'Update a message you have sent.',
usage: `
API: { cmd: 'updateMessage', mode: 'overwrite'|'append'|'prepend'|'complete', text: '<text to apply>', customId: '<customId sent with the chat message>' }`,
};

231
commands/core/whisper.js Normal file
View File

@ -0,0 +1,231 @@
/**
* @author Marzavec ( https://github.com/marzavec )
* @summary Send whisper
* @version 1.0.0
* @description Display text on target users screen that only they can see
* @module whisper
* @todo This should be changed to it's own event type, instead of `info`
and accept a `userid` rather than `nick`
*/
import {
findUser,
} from '../utility/_Channels.js';
import {
Errors,
} from '../utility/_Constants.js';
import {
legacyWhisperOut,
legacyWhisperReply,
} from '../utility/_LegacyFunctions.js';
/**
* Check and trim string provided by remote client
* @param {string} text - Subject string
* @private
* @todo Move into utility module
* @return {string|boolean}
*/
const parseText = (text) => {
// verifies user input is text
if (typeof text !== 'string') {
return false;
}
let sanitizedText = text;
// strip newlines from beginning and end
sanitizedText = sanitizedText.replace(/^\s*\n|^\s+$|\n\s*$/g, '');
// replace 3+ newlines with just 2 newlines
sanitizedText = sanitizedText.replace(/\n{3,}/g, '\n\n');
return sanitizedText;
};
/**
* 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 }) {
// if this is a legacy client add missing params to payload
if (socket.hcProtocol === 1) {
payload.channel = socket.channel; // eslint-disable-line no-param-reassign
}
// verify user input
const text = parseText(payload.text);
if (!text) {
// lets not send objects or empty text, yea?
return server.police.frisk(socket, 13);
}
// check for spam
const score = text.length / 83 / 4;
if (server.police.frisk(socket, score)) {
return server.reply({
cmd: 'warn', // @todo Add numeric error code as `id`
text: 'You are sending too much text. Wait a moment and try again.\nPress the up arrow key to restore your last message.',
channel: socket.channel, // @todo Multichannel
}, socket);
}
const targetUser = findUser(server, payload);
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);
}
const outgoingPayload = {
cmd: 'whisper',
channel: socket.channel, // @todo Multichannel
from: socket.userid,
to: targetUser.userid,
text,
};
// send invite notice to target client
if (targetUser.hcProtocol === 1) {
server.reply(legacyWhisperOut(outgoingPayload, socket), targetUser);
} else {
server.reply(outgoingPayload, targetUser);
}
// send invite notice to this client
if (socket.hcProtocol === 1) {
server.reply(legacyWhisperReply(outgoingPayload, targetUser.nick), socket);
} else {
server.reply(outgoingPayload, socket);
}
targetUser.whisperReply = socket.nick;
return true;
}
/**
* Automatically executes once after server is ready to register this modules hooks
* @param {Object} server - Reference to server enviroment object
* @public
* @return {void}
*/
export function initHooks(server) {
server.registerHook('in', 'chat', this.whisperCheck.bind(this), 20);
}
/**
* Executes every time an incoming chat command is invoked;
* hooks chat commands checking for /whisper
* @param {Object} env - Enviroment object with references to core, server, socket & payload
* @public
* @return {(Object|boolean|string)} Object = same/altered payload,
* false = suppress action,
* string = error
*/
export function whisperCheck({
core, server, socket, payload,
}) {
if (typeof payload.text !== 'string') {
return false;
}
if (payload.text.startsWith('/whisper ') || payload.text.startsWith('/w ')) {
const input = payload.text.split(' ');
// If there is no nickname target parameter
if (!input[1]) {
server.reply({
cmd: 'warn', // @todo Add numeric error code as `id`
text: 'Refer to `/help whisper` for instructions on how to use this command.',
channel: socket.channel, // @todo Multichannel
}, socket);
return false;
}
const target = input[1].replace(/@/g, '');
input.splice(0, 2);
const whisperText = input.join(' ');
this.run({
core,
server,
socket,
payload: {
cmd: 'whisper',
channel: socket.channel, // @todo Multichannel
nick: target,
text: whisperText,
},
});
return false;
}
if (payload.text.startsWith('/reply ') || payload.text.startsWith('/r ')) {
if (typeof socket.whisperReply === 'undefined') {
server.reply({
cmd: 'warn', // @todo Add numeric error code as `id`
text: 'Cannot reply to nobody',
channel: socket.channel, // @todo Multichannel
}, socket);
return false;
}
const input = payload.text.split(' ');
input.splice(0, 1);
const whisperText = input.join(' ');
this.run({
core,
server,
socket,
payload: {
cmd: 'whisper',
nick: socket.whisperReply,
channel: socket.channel, // @todo Multichannel
text: whisperText,
},
});
return false;
}
return payload;
}
/**
* The following payload properties are required to invoke this module:
* "nick", "text"
* @public
* @typedef {Array} whisper/requiredData
*/
export const requiredData = ['nick', 'text'];
/**
* Module meta information
* @public
* @typedef {Object} whisper/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: 'whisper',
category: 'core',
description: 'Display text on target users screen that only they can see',
usage: `
API: { cmd: 'whisper', nick: '<target name>', text: '<text to whisper>' }
Text: /whisper <target name> <text to whisper>
Text: /w <target name> <text to whisper>
Alt Text: /reply <text to whisper, this will auto reply to the last person who whispered to you>
Alt Text: /r <text to whisper, this will auto reply to the last person who whispered to you>`,
};

View File

@ -0,0 +1,66 @@
/**
* @author Marzavec ( https://github.com/marzavec )
* @summary Disconnection handler
* @version 1.0.0
* @description The server invokes this module each time a websocket connection is disconnected
* @module disconnect
*/
import {
socketInChannel,
} 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 }) {
if (payload.cmdKey !== server.cmdKey) {
// internal command attempt by client, increase rate limit chance and ignore
return server.police.frisk(socket, 20);
}
// send leave notice to client peers
// @todo Multichannel update
if (socket.channel) {
const isDuplicate = socketInChannel(server, socket.channel, socket);
if (isDuplicate === false) {
server.broadcast({
cmd: 'onlineRemove',
nick: socket.nick,
}, { channel: socket.channel });
}
}
// commit close just in case
socket.terminate();
return true;
}
/**
* The following payload properties are required to invoke this module:
* "cmdKey"
* @public
* @typedef {Array} disconnect/requiredData
*/
export const requiredData = ['cmdKey'];
/**
* Module meta information
* @public
* @typedef {Object} disconnect/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: 'disconnect',
category: 'internal',
description: 'Internally used to relay disconnect events to clients',
usage: 'Internal Use Only',
};

View File

@ -0,0 +1,51 @@
/**
* @author Marzavec ( https://github.com/marzavec )
* @summary Bridge warning events to a user
* @version 1.0.0
* @description If a warning occurs within the server, this module will relay the warning to the
* client
* @module socketreply
*/
/**
* 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 }) {
if (payload.cmdKey !== server.cmdKey) {
// internal command attempt by client, increase rate limit chance and ignore
return server.police.frisk(socket, 20);
}
// send warning to target socket
return server.reply({
cmd: 'warn',
text: payload.text,
}, socket);
}
/**
* The following payload properties are required to invoke this module:
* "cmdKey", "text"
* @public
* @typedef {Array} socketreply/requiredData
*/
export const requiredData = ['cmdKey', 'text'];
/**
* Module meta information
* @public
* @typedef {Object} socketreply/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: 'socketreply',
category: 'internal',
description: 'Internally used to relay warnings to clients',
usage: 'Internal Use Only',
};

115
commands/mod/ban.js Normal file
View File

@ -0,0 +1,115 @@
/**
* @author Marzavec ( https://github.com/marzavec )
* @summary Ban a user
* @version 1.0.0
* @description Bans target user by name
* @module ban
*/
import {
isModerator,
getUserDetails,
levels,
} from '../utility/_UAC.js';
import {
Errors,
} 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({
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);
}
// check user input
if (socket.hcProtocol === 1) {
if (typeof payload.nick !== 'string') {
return false;
}
payload.channel = socket.channel; // eslint-disable-line no-param-reassign
} else if (typeof payload.userid !== 'number') {
return false;
}
// find target user
const targetUser = findUser(server, payload);
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);
}
const targetNick = targetUser.nick;
// i guess banning mods or admins isn't the best idea?
if (targetUser.level >= socket.level) {
return server.reply({
cmd: 'warn',
text: 'Cannot ban other users of the same level, how rude',
id: Errors.Global.PERMISSION,
channel: socket.channel, // @todo Multichannel
}, socket);
}
// commit arrest record
server.police.arrest(targetUser.address, targetUser.hash);
console.log(`${socket.nick} [${socket.trip}] banned ${targetNick} in ${socket.channel}`);
// notify normal users
server.broadcast({
cmd: 'info',
text: `Banned ${targetNick}`,
user: getUserDetails(targetUser),
channel: socket.channel, // @todo Multichannel
}, { channel: socket.channel, level: (level) => level < levels.moderator });
// notify mods
server.broadcast({
cmd: 'info',
text: `${socket.nick}#${socket.trip} banned ${targetNick} in ${payload.channel}, userhash: ${targetUser.hash}`,
channel: socket.channel, // @todo Multichannel
inChannel: payload.channel,
user: getUserDetails(targetUser),
banner: getUserDetails(socket),
}, { level: isModerator });
// force connection closed
targetUser.terminate();
// stats are fun
core.stats.increment('users-banned');
return true;
}
/**
* Module meta information
* @public
* @typedef {Object} ban/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: 'ban',
category: 'moderators',
description: 'Bans target user by name',
usage: `
API: { cmd: 'ban', nick: '<target nickname>' }`,
};

View File

@ -0,0 +1,86 @@
/**
* @author Marzavec ( https://github.com/marzavec )
* @summary Disables the captcha
* @version 1.0.0
* @description Disables the captcha on the channel specified in the channel property,
* default is current channel
* @module disablecaptcha
*/
import {
isModerator,
} from '../utility/_UAC.js';
/**
* Automatically executes once after server is ready
* @param {Object} core - Reference to core enviroment object
* @public
* @return {void}
*/
export async function init(core) {
if (typeof core.captchas === 'undefined') {
core.captchas = {};
}
}
/**
* 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,
}) {
// 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;
}
if (!core.captchas[targetChannel]) {
return server.reply({
cmd: 'info',
text: 'Captcha is not enabled.',
channel: socket.channel, // @todo Multichannel
}, socket);
}
core.captchas[targetChannel] = false;
server.broadcast({
cmd: 'info',
text: `Captcha disabled on: ${targetChannel}`,
channel: false, // @todo Multichannel, false for global info
}, { channel: targetChannel, level: isModerator });
return true;
}
/**
* Module meta information
* @public
* @typedef {Object} disablecaptcha/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: 'disablecaptcha',
category: 'moderators',
description: 'Disables the captcha on the channel specified in the channel property, default is current channel',
usage: `
API: { cmd: 'disablecaptcha', channel: '<optional channel, defaults to your current channel' }`,
};

374
commands/mod/dumb.js Normal file
View File

@ -0,0 +1,374 @@
/* eslint no-param-reassign: 0 */
/* eslint no-multi-assign: 0 */
/**
* @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.
* @module dumb
*/
import {
isModerator,
} from '../utility/_UAC.js';
import {
findUser,
} from '../utility/_Channels.js';
import {
Errors,
} from '../utility/_Constants.js';
import {
legacyInviteReply,
legacyWhisperReply,
} from '../utility/_LegacyFunctions.js';
/**
* Returns the channel that should be invited to.
* @param {any} channel
* @private
* @return {string}
*/
export function getChannel(channel = undefined) {
if (typeof channel === 'string') {
return channel;
}
return Math.random().toString(36).substr(2, 8);
}
/**
* Check and trim string provided by remote client
* @param {string} text - Subject string
* @private
* @todo Move into utility module
* @return {string|boolean}
*/
const parseText = (text) => {
// verifies user input is text
if (typeof text !== 'string') {
return false;
}
let sanitizedText = text;
// strip newlines from beginning and end
sanitizedText = sanitizedText.replace(/^\s*\n|^\s+$|\n\s*$/g, '');
// replace 3+ newlines with just 2 newlines
sanitizedText = sanitizedText.replace(/\n{3,}/g, '\n\n');
return sanitizedText;
};
/**
* Automatically executes once after server is ready
* @param {Object} core - Reference to core enviroment object
* @public
* @return {void}
*/
export function init(core) {
if (typeof core.muzzledHashes === 'undefined') {
core.muzzledHashes = {};
}
}
/**
* 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,
}) {
// increase rate limit chance and ignore if not admin or mod
if (!isModerator(socket.level)) {
return server.police.frisk(socket, 10);
}
// check user input
if (socket.hcProtocol === 1) {
if (typeof payload.nick !== 'string') {
return true;
}
payload.channel = socket.channel;
} else if (typeof payload.userid !== 'number') {
return true;
}
// find target user
const targetUser = findUser(server, payload);
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);
}
// likely dont need this, muting mods and admins is fine
if (targetUser.level >= socket.level) {
return server.reply({
cmd: 'warn',
text: 'This trick wont work on users of the same level',
id: Errors.Global.PERMISSION,
channel: socket.channel, // @todo Multichannel
}, socket);
}
// store hash in mute list
const record = core.muzzledHashes[targetUser.hash] = {
dumb: true,
};
// store allies if needed
if (payload.allies && Array.isArray(payload.allies)) {
record.allies = payload.allies;
}
// notify mods
server.broadcast({
cmd: 'info',
text: `${socket.nick}#${socket.trip} muzzled ${targetUser.nick} in ${payload.channel}, userhash: ${targetUser.hash}`,
channel: false, // @todo Multichannel, false for global
}, { level: isModerator });
return true;
}
/**
* Automatically executes once after server is ready to register this modules hooks
* @param {Object} server - Reference to server enviroment object
* @public
* @return {void}
*/
export function initHooks(server) {
server.registerHook('in', 'chat', this.chatCheck.bind(this), 10);
server.registerHook('in', 'invite', this.inviteCheck.bind(this), 10);
server.registerHook('in', 'whisper', this.whisperCheck.bind(this), 10);
}
/**
* Executes every time an incoming chat command is invoked;
* hook incoming chat commands, shadow-prevent chat if they are muzzled
* @param {Object} env - Enviroment 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 (core.muzzledHashes[socket.hash]) {
// build fake chat payload
const outgoingPayload = {
cmd: 'chat',
nick: socket.nick, /* @legacy */
uType: socket.uType, /* @legacy */
userid: socket.userid,
channel: socket.channel,
text: payload.text,
level: socket.level,
};
if (socket.trip) {
outgoingPayload.trip = socket.trip;
}
if (socket.color) {
outgoingPayload.color = socket.color;
}
// broadcast to any duplicate connections in channel
server.broadcast(outgoingPayload, { channel: socket.channel, hash: socket.hash });
// broadcast to allies, if any
if (core.muzzledHashes[socket.hash].allies) {
server.broadcast(
outgoingPayload,
{
channel: socket.channel,
nick: core.muzzledHashes[socket.hash].allies,
},
);
}
/**
* Blanket "spam" protection.
* May expose the ratelimiting lines from `chat` and use that
* @todo one day #lazydev
*/
server.police.frisk(socket, 9);
return false;
}
return payload;
}
/**
* Executes every time an incoming chat command is invoked;
* shadow-prevent all invites from muzzled users
* @param {Object} env - Enviroment object with references to core, server, socket & payload
* @public
* @return {(Object|boolean|string)} Object = same/altered payload,
* false = suppress action,
* string = error
*/
export function inviteCheck({
core, server, socket, payload,
}) {
if (core.muzzledHashes[socket.hash]) {
// check for spam
if (server.police.frisk(socket, 2)) {
return server.reply({
cmd: 'warn',
text: 'You are sending invites too fast. Wait a moment before trying again.',
id: Errors.Invite.RATELIMIT,
channel: socket.channel, // @todo Multichannel
}, socket);
}
// verify user input
// if this is a legacy client add missing params to payload
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') {
return true;
}
// @todo Verify this socket is part of payload.channel - multichannel patch
// find target user
const targetUser = findUser(server, payload);
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);
}
// generate common channel
const channel = getChannel(payload.to);
// build invite
const outgoingPayload = {
cmd: 'invite',
channel: socket.channel, // @todo Multichannel
from: socket.userid,
to: targetUser.userid,
inviteChannel: channel,
};
// send invite notice to this client
if (socket.hcProtocol === 1) {
server.reply(legacyInviteReply(outgoingPayload, targetUser.nick), socket);
} else {
server.reply(outgoingPayload, socket);
}
return false;
}
return payload;
}
/**
* Executes every time an incoming chat command is invoked;
* shadow-prevent all whispers from muzzled users
* @param {Object} env - Enviroment object with references to core, server, socket & payload
* @public
* @return {(Object|boolean|string)} Object = same/altered payload,
* false = suppress action,
* string = error
*/
export function whisperCheck({
core, server, socket, payload,
}) {
if (core.muzzledHashes[socket.hash]) {
// if this is a legacy client add missing params to payload
if (socket.hcProtocol === 1) {
payload.channel = socket.channel; // eslint-disable-line no-param-reassign
}
// verify user input
const text = parseText(payload.text);
if (!text) {
// lets not send objects or empty text, yea?
return server.police.frisk(socket, 13);
}
// check for spam
const score = text.length / 83 / 4;
if (server.police.frisk(socket, score)) {
return server.reply({
cmd: 'warn', // @todo Add numeric error code as `id`
text: 'You are sending too much text. Wait a moment and try again.\nPress the up arrow key to restore your last message.',
channel: socket.channel, // @todo Multichannel
}, socket);
}
const targetUser = findUser(server, payload);
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);
}
const outgoingPayload = {
cmd: 'whisper',
channel: socket.channel, // @todo Multichannel
from: socket.userid,
to: targetUser.userid,
text,
};
// send invite notice to this client
if (socket.hcProtocol === 1) {
server.reply(legacyWhisperReply(outgoingPayload, targetUser.nick), socket);
} else {
server.reply(outgoingPayload, socket);
}
targetUser.whisperReply = socket.nick;
return false;
}
return payload;
}
/**
* Module meta information
* @public
* @typedef {Object} dumb/info
* @property {string} name - Module command name
* @property {string} category - Module category name
* @property {string} description - Information about module
* @property {Array} aliases - An array of alternative cmd names
* @property {string} usage - Information about module usage
*/
export const info = {
name: 'dumb',
category: 'moderators',
description: 'Globally shadow mute a connection. Optional allies array will see muted messages.',
aliases: ['muzzle', 'mute'],
usage: `
API: { cmd: 'dumb', nick: '<target nick>', allies: ['<optional nick array>', ...] }`,
};

View File

@ -0,0 +1,279 @@
/* eslint no-param-reassign: 0 */
/**
* @author Marzavec ( https://github.com/marzavec )
* @summary Enables the captcha
* @version 1.0.0
* @description Enables the captcha on the channel specified in the channel property,
* default is current channel
* @module enablecaptcha
*/
import captcha from 'ascii-captcha';
import {
isTrustedUser,
isModerator,
verifyNickname,
getUserPerms,
} from '../utility/_UAC.js';
import {
canJoinChannel,
} from '../utility/_Channels.js';
import {
upgradeLegacyJoin,
legacyLevelToLabel,
} from '../utility/_LegacyFunctions.js';
import {
Errors,
} from '../utility/_Constants.js';
/**
* Automatically executes once after server is ready
* @param {Object} core - Reference to core enviroment object
* @public
* @return {void}
*/
export async function init(core) {
if (typeof core.captchas === 'undefined') {
core.captchas = {};
}
}
/**
* 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,
}) {
// 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;
}
if (core.captchas[targetChannel]) {
return server.reply({
cmd: 'info',
text: 'Captcha is already enabled.',
channel: socket.channel, // @todo Multichannel
}, socket);
}
core.captchas[targetChannel] = true;
server.broadcast({
cmd: 'info',
text: `Captcha enabled on: ${targetChannel}`,
channel: socket.channel, // @todo Multichannel, false for global info
}, { channel: socket.channel, level: isModerator });
return true;
}
/**
* Automatically executes once after server is ready to register this modules hooks
* @param {Object} server - Reference to server enviroment object
* @public
* @return {void}
*/
export function initHooks(server) {
server.registerHook('in', 'chat', this.chatCheck.bind(this), 5);
server.registerHook('in', 'join', this.joinCheck.bind(this), 5);
}
/**
* Executes every time an incoming chat command is invoked;
* hook incoming chat commands, check if they are answering a captcha
* @param {Object} env - Enviroment 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,
}) {
// always verifiy user input
if (typeof payload.text !== 'string') {
return false;
}
if (typeof socket.captcha !== 'undefined') {
if (socket.captcha.awaiting === true) {
if (payload.text === socket.captcha.solution) {
if (typeof socket.captcha.whitelist === 'undefined') {
socket.captcha.whitelist = [];
}
socket.captcha.whitelist.push(socket.captcha.origChannel);
socket.captcha.awaiting = false;
if (socket.hcProtocol === 1) {
core.commands.handleCommand(server, socket, {
cmd: 'join',
nick: `${socket.captcha.origNick}#${socket.captcha.origPass}`,
channel: socket.captcha.origChannel,
});
} else {
core.commands.handleCommand(server, socket, {
cmd: 'join',
nick: socket.captcha.origNick,
pass: socket.captcha.origPass,
channel: socket.captcha.origChannel,
});
}
return false;
}
server.police.frisk(socket, 7);
socket.terminate();
return false;
}
}
return payload;
}
/**
* Executes every time an incoming join command is invoked;
* hook incoming join commands, check if they are joining a captcha protected channel
* @param {Object} env - Enviroment object with references to core, server, socket & payload
* @public
* @return {(Object|boolean|string)} Object = same/altered payload,
* false = suppress action,
* string = error
*/
export function joinCheck({
core, server, socket, payload,
}) {
// check if channel has captcha enabled
if (core.captchas[payload.channel] !== true) {
return payload;
}
// `join` is the legacy entry point, check if it needs to be upgraded
const origPayload = { ...payload };
if (typeof socket.hcProtocol === 'undefined') {
payload = upgradeLegacyJoin(server, socket, payload);
}
// store payload values
const { channel, nick, pass } = payload;
// check if a client is able to join target channel
const mayJoin = canJoinChannel(channel, socket);
if (mayJoin !== true) {
return server.reply({
cmd: 'warn',
text: 'You may not join that channel.',
id: mayJoin,
channel: false, // @todo Multichannel, false for global event
}, socket);
}
// calling socket already in a channel
// @todo multichannel update, will remove
if (typeof socket.channel !== 'undefined') {
return server.reply({
cmd: 'warn', // @todo Remove this
text: 'Joining more than one channel is not currently supported',
id: Errors.Join.ALREADY_JOINED,
channel: false, // @todo Multichannel, false for global event
}, socket);
}
// end todo
// validates the user input for `nick`
if (verifyNickname(nick, socket) !== true) {
return server.reply({
cmd: 'warn',
text: 'Nickname must consist of up to 24 letters, numbers, and underscores',
id: Errors.Join.INVALID_NICK,
channel: false, // @todo Multichannel, false for global event
}, socket);
}
// get trip and level
const { trip, level } = getUserPerms(pass, core.saltKey, core.appConfig.data, channel);
// store the user values
const userInfo = {
nick,
trip,
uType: legacyLevelToLabel(level),
hash: socket.hash,
level,
userid: socket.userid,
isBot: socket.isBot,
color: socket.color,
channel,
};
if (userInfo.uType === 'user') {
if (userInfo.trip == null || isTrustedUser(level) === false) {
if (typeof socket.captcha === 'undefined') {
socket.captcha = {
awaiting: true,
origChannel: payload.channel,
origNick: payload.nick,
origPass: pass,
solution: captcha.generateRandomText(6),
};
server.reply({
cmd: 'warn',
text: 'Enter the following to join (case-sensitive):',
channel: payload.channel, // @todo Multichannel
}, socket);
server.reply({
cmd: 'captcha',
text: captcha.word2Transformedstr(socket.captcha.solution),
channel: payload.channel, // @todo Multichannel
}, socket);
return false;
}
socket.terminate();
return false;
}
}
return origPayload;
}
/**
* Module meta information
* @public
* @typedef {Object} enablecaptcha/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: 'enablecaptcha',
category: 'moderators',
description: 'Enables a captcha in the current channel you are in',
usage: `
API: { cmd: 'enablecaptcha', channel: '<optional channel, defaults to your current channel>' }`,
};

199
commands/mod/forcecolor.js Normal file
View File

@ -0,0 +1,199 @@
/**
* @author Marzavec ( https://github.com/marzavec )
* @summary Color a user
* @version 1.0.0
* @description Forces a user nick to become a certain color
* @module forcecolor
*/
import {
isModerator,
getUserDetails,
} from '../utility/_UAC.js';
import {
Errors,
} from '../utility/_Constants.js';
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);
/**
* 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,
}) {
// increase rate limit chance and ignore if not admin or mod
if (!isModerator(socket.level)) {
return server.police.frisk(socket, 10);
}
const { channel } = socket;
if (typeof payload.channel === 'undefined') {
payload.channel = channel;
}
// check user input
if (typeof payload.nick !== 'string') {
return true;
}
if (typeof payload.color !== 'string') {
return true;
}
// make sure requested nickname meets standards
const newColor = payload.color.trim().toUpperCase().replace(/#/g, '');
if (newColor !== 'RESET' && !verifyColor(newColor)) {
return server.reply({
cmd: 'warn',
text: 'Invalid color! Color must be in hex value',
channel, // @todo Multichannel
}, socket);
}
// find target user
const targetUser = findUser(server, payload);
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);
}
// TODO: Change this uType to use level / uac
// i guess coloring mods or admins isn't the best idea?
if (targetUser.uType !== 'user') {
return true;
}
if (newColor === 'RESET') {
targetUser.color = false;
} else {
targetUser.color = newColor;
}
// build update notice with new color
const updateNotice = {
...getUserDetails(targetUser),
...{
cmd: 'updateUser',
channel: socket.channel, // @todo Multichannel
},
};
// notify channel that the user has changed their name
// @todo this should be sent to every channel the user is in (multichannel)
server.broadcast(updateNotice, { channel: socket.channel });
return true;
}
/**
* Automatically executes once after server is ready to register this modules hooks
* @param {Object} server - Reference to server enviroment object
* @public
* @return {void}
*/
export function initHooks(server) {
server.registerHook('in', 'chat', this.colorCheck.bind(this), 20);
}
/**
* Executes every time an incoming chat command is invoked;
* hooks chat commands checking for /forcecolor
* @param {Object} env - Enviroment object with references to core, server, socket & payload
* @public
* @return {(Object|boolean|string)} Object = same/altered payload,
* false = suppress action,
* string = error
*/
export function colorCheck({
core, server, socket, payload,
}) {
if (typeof payload.text !== 'string') {
return false;
}
if (payload.text.startsWith('/forcecolor ')) {
const input = payload.text.split(' ');
// If there is no nickname target parameter
if (input[1] === undefined) {
server.reply({
cmd: 'warn',
text: 'Refer to `/help forcecolor` for instructions on how to use this command.',
channel: socket.channel, // @todo Multichannel
}, socket);
return false;
}
if (input[2] === undefined) {
server.reply({
cmd: 'warn',
text: 'Refer to `/help forcecolor` for instructions on how to use this command.',
channel: socket.channel, // @todo Multichannel
}, socket);
return false;
}
const target = input[1].replace(/@/g, '');
this.run({
core,
server,
socket,
payload: {
cmd: 'forcecolor',
nick: target,
color: input[2],
},
});
return false;
}
return payload;
}
/**
* The following payload properties are required to invoke this module:
* "nick", "color"
* @public
* @typedef {Array} forcecolor/requiredData
*/
export const requiredData = ['nick', 'color'];
/**
* Module meta information
* @public
* @typedef {Object} forcecolor/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: 'forcecolor',
category: 'moderators',
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>`,
};

152
commands/mod/kick.js Normal file
View File

@ -0,0 +1,152 @@
/**
* @author Marzavec ( https://github.com/marzavec )
* @summary Give da boot
* @version 1.0.0
* @description Silently forces target client(s) into another channel
* @module kick
*/
import {
isModerator,
levels,
getUserDetails,
} from '../utility/_UAC.js';
import {
Errors,
} from '../utility/_Constants.js';
import {
findUsers,
} 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({
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);
}
// 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
} else if (typeof payload.userid !== 'number') {
// @todo create multi-ban ui
if (typeof payload.userid !== 'object' && !Array.isArray(payload.userid)) {
return true;
}
}
// find target user(s)
const badClients = findUsers(server, payload);
if (badClients.length === 0) {
return server.reply({
cmd: 'warn',
text: 'Could not find user(s) in that channel',
id: Errors.Global.UNKNOWN_USER,
channel: socket.channel, // @todo Multichannel
}, socket);
}
// 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) {
server.reply({
cmd: 'warn',
text: 'Cannot kick other users with the same level, how rude',
id: Errors.Global.PERMISSION,
channel: socket.channel, // @todo Multichannel
}, socket);
} else {
kicked.push(badClients[i]);
}
}
if (kicked.length === 0) {
return true;
}
let destChannel;
if (typeof payload.to === 'string' && !!payload.to.trim()) {
destChannel = payload.to;
} else {
destChannel = Math.random().toString(36).substr(2, 8);
}
// 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) {
server.broadcast({
...getUserDetails(kicked[i]),
...{
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) {
// @todo multi-channel update
kicked[i].channel = destChannel;
server.broadcast({
cmd: 'info',
text: `${kicked[i].nick} was banished to ?${destChannel}`,
channel: socket.channel, // @todo Multichannel
}, { channel: socket.channel, level: isModerator });
console.log(`${socket.nick} [${socket.trip}] kicked ${kicked[i].nick} in ${socket.channel} to ${destChannel} `);
}
// broadcast client leave event
for (let i = 0, j = kicked.length; i < j; i += 1) {
server.broadcast({
cmd: 'onlineRemove',
userid: kicked[i].userid,
nick: kicked[i].nick,
channel: socket.channel, // @todo Multichannel
}, { channel: socket.channel });
}
// publicly broadcast kick event
server.broadcast({
cmd: 'info',
text: `Kicked ${kicked.map((k) => k.nick).join(', ')}`,
channel: socket.channel, // @todo Multichannel
}, { channel: socket.channel, level: (level) => level < levels.moderator });
// stats are fun
core.stats.increment('users-kicked', kicked.length);
return true;
}
/**
* Module meta information
* @public
* @typedef {Object} kick/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: 'kick',
category: 'moderators',
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>' }`,
};

331
commands/mod/lockroom.js Normal file
View File

@ -0,0 +1,331 @@
/* eslint no-param-reassign: 0 */
/**
* @author Marzavec ( https://github.com/marzavec )
* @summary Locks the channel
* @version 1.0.0
* @description Locks a channel preventing default levels from joining
* @module lockroom
*/
import {
isTrustedUser,
isModerator,
verifyNickname,
getUserPerms,
} from '../utility/_UAC.js';
import {
upgradeLegacyJoin,
legacyLevelToLabel,
} from '../utility/_LegacyFunctions.js';
import {
Errors,
} from '../utility/_Constants.js';
import {
canJoinChannel,
} from '../utility/_Channels.js';
const danteQuotes = [
'Do not be afraid; our fate cannot be taken from us; it is a gift.',
'In the middle of the journey of our life I found myself within a dark woods where the straight way was lost.',
'There is no greater sorrow then to recall our times of joy in wretchedness.',
'They yearn for what they fear for.',
'Through me you go into a city of weeping; through me you go into eternal pain; through me you go amongst the lost people',
'From there we came outside and saw the stars',
'But the stars that marked our starting fall away. We must go deeper into greater pain, for it is not permitted that we stay.',
'Hope not ever to see Heaven. I have come to lead you to the other shore; into eternal darkness; into fire and into ice.',
'As little flowers, which the chill of night has bent and huddled, when the white sun strikes, grow straight and open fully on their stems, so did I, too, with my exhausted force.',
'At grief so deep the tongue must wag in vain; the language of our sense and memory lacks the vocabulary of such pain.',
'Thence we came forth to rebehold the stars.',
'He is, most of all, l\'amor che move il sole e l\'altre stelle.',
'The poets leave hell and again behold the stars.',
'One ought to be afraid of nothing other then things possessed of power to do us harm, but things innoucuous need not be feared.',
'As phantoms frighten beasts when shadows fall.',
'We were men once, though we\'ve become trees',
'Here pity only lives when it is dead',
'Lasciate ogne speranza, voi ch\'intrate.',
'There is no greater sorrow than thinking back upon a happy time in misery',
'My thoughts were full of other things When I wandered off the path.',
];
/**
* Automatically executes once after server is ready
* @param {Object} core - Reference to core enviroment object
* @public
* @return {void}
*/
export async function init(core) {
if (typeof core.locked === 'undefined') {
core.locked = {};
}
}
/**
* 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,
}) {
// 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;
}
if (core.locked[targetChannel]) {
return server.reply({
cmd: 'info',
text: 'Channel is already locked.',
channel: socket.channel, // @todo Multichannel
}, socket);
}
// apply lock flag to channel list
core.locked[targetChannel] = true;
// inform mods
server.broadcast({
cmd: 'info',
text: `Channel: ?${targetChannel} lock enabled by [${socket.trip}]${socket.nick}`,
channel: false, // @todo Multichannel, false for global info
}, { level: isModerator });
return true;
}
/**
* Automatically executes once after server is ready to register this modules hooks
* @param {Object} server - Reference to server enviroment object
* @public
* @return {void}
*/
export function initHooks(server) {
server.registerHook('in', 'changenick', this.changeNickCheck.bind(this), 1);
server.registerHook('in', 'whisper', this.whisperCheck.bind(this), 1);
server.registerHook('in', 'chat', this.chatCheck.bind(this), 1);
server.registerHook('in', 'invite', this.inviteCheck.bind(this), 1);
server.registerHook('in', 'join', this.joinCheck.bind(this), 1);
}
/**
* Executes every time an incoming changenick command is invoked;
* hook incoming changenick commands, reject them if the channel is 'purgatory'
* @param {Object} env - Enviroment object with references to core, server, socket & payload
* @public
* @return {(Object|boolean|string)} Object = same/altered payload,
* false = suppress action,
* string = error
*/
export function changeNickCheck({
socket, payload,
}) {
if (socket.channel === 'purgatory') { // @todo Multichannel update
return false;
}
return payload;
}
/**
* Executes every time an incoming whisper command is invoked;
* hook incoming whisper commands, reject them if the channel is 'purgatory'
* @param {Object} env - Enviroment object with references to core, server, socket & payload
* @public
* @return {(Object|boolean|string)} Object = same/altered payload,
* false = suppress action,
* string = error
*/
export function whisperCheck({
socket, payload,
}) {
if (socket.channel === 'purgatory') { // @todo Multichannel update
return false;
}
return payload;
}
/**
* Executes every time an incoming chat command is invoked;
* hook incoming chat commands, reject them if the channel is 'purgatory'
* @param {Object} env - Enviroment 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({
socket, payload,
}) {
if (socket.channel === 'purgatory') {
if (isModerator(socket.level)) {
return payload;
}
return false;
}
return payload;
}
/**
* Executes every time an incoming invite command is invoked;
* hook incoming invite commands, reject them if the channel is 'purgatory'
* @param {Object} env - Enviroment object with references to core, server, socket & payload
* @public
* @return {(Object|boolean|string)} Object = same/altered payload,
* false = suppress action,
* string = error
*/
export function inviteCheck({
socket, payload,
}) {
if (socket.channel === 'purgatory') {
return false;
}
return payload;
}
/**
* Executes every time an incoming join command is invoked;
* hook incoming join commands, shunt them to purgatory if needed
* @param {Object} env - Enviroment object with references to core, server, socket & payload
* @public
* @return {(Object|boolean|string)} Object = same/altered payload,
* false = suppress action,
* string = error
*/
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 (payload.channel !== 'purgatory') {
return payload;
}
}
// `join` is the legacy entry point, check if it needs to be upgraded
if (typeof socket.hcProtocol === 'undefined') {
payload = upgradeLegacyJoin(server, socket, payload);
}
// store payload values
const { channel, nick, pass } = payload;
// check if a client is able to join target channel
const mayJoin = canJoinChannel(channel, socket);
if (mayJoin !== true) {
return server.reply({
cmd: 'warn',
text: 'You may not join that channel.',
id: mayJoin,
channel: false, // @todo Multichannel, false for global event
}, socket);
}
// calling socket already in a channel
// @todo multichannel update, will remove
if (typeof socket.channel !== 'undefined') {
return server.reply({
cmd: 'warn', // @todo Remove this
text: 'Joining more than one channel is not currently supported',
id: Errors.Join.ALREADY_JOINED,
channel: false, // @todo Multichannel, false for global event
}, socket);
}
// end todo
// validates the user input for `nick`
if (verifyNickname(nick, socket) !== true) {
return server.reply({
cmd: 'warn',
text: 'Nickname must consist of up to 24 letters, numbers, and underscores',
id: Errors.Join.INVALID_NICK,
channel: false, // @todo Multichannel, false for global event
}, socket);
}
// get trip and level
const { trip, level } = getUserPerms(pass, core.saltKey, core.appConfig.data, channel);
// store the user values
const userInfo = {
nick,
trip,
uType: legacyLevelToLabel(level),
hash: socket.hash,
level,
userid: socket.userid,
isBot: socket.isBot,
color: socket.color,
channel,
};
// check if trip is allowed
if (userInfo.uType === 'user') {
if (userInfo.trip == null || isTrustedUser(level) === false) {
const origNick = userInfo.nick;
const origChannel = payload.channel;
// not allowed, shunt to purgatory
payload.channel = 'purgatory';
// lost souls have no names
if (origChannel === 'purgatory') {
// someone is pulling a Dante
payload.nick = `Dante_${Math.random().toString(36).substr(2, 8)}`;
} else {
payload.nick = `${Math.random().toString(36).substr(2, 8)}${Math.random().toString(36).substr(2, 8)}`;
}
setTimeout(() => {
server.reply({
cmd: 'info',
text: danteQuotes[Math.floor(Math.random() * danteQuotes.length)],
channel: 'purgatory', // @todo Multichannel
}, socket);
}, 100);
server.broadcast({
cmd: 'info',
text: `${payload.nick} is: ${origNick}\ntrip: ${userInfo.trip || 'none'}\ntried to join: ?${origChannel}\nhash: ${userInfo.hash}`,
channel: 'purgatory', // @todo Multichannel, false for global info
}, { channel: 'purgatory', level: isModerator });
}
}
return payload;
}
/**
* Module meta information
* @public
* @typedef {Object} kick/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: 'lockroom',
category: 'moderators',
description: 'Locks a channel preventing default levels from joining',
usage: `
API: { cmd: 'lockroom', channel: '<optional channel, defaults to your current channel>' }`,
};

107
commands/mod/speak.js Normal file
View File

@ -0,0 +1,107 @@
/* eslint no-param-reassign: 0 */
/**
* @author OpSimple ( https://github.com/OpSimple )
* @summary Unmuzzle a user
* @version 1.0.0
* @description Pardon a dumb user to be able to speak again
* @module speak
*/
import {
isModerator,
} from '../utility/_UAC.js';
/**
* Automatically executes once after server is ready
* @param {Object} core - Reference to core enviroment object
* @public
* @return {void}
*/
export function init(core) {
if (typeof core.muzzledHashes === 'undefined') {
core.muzzledHashes = {};
}
}
/**
* 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,
}) {
// increase rate limit chance and ignore if not admin or mod
if (!isModerator(socket.level)) {
return server.police.frisk(socket, 10);
}
// check user input
if (typeof payload.ip !== 'string' && typeof payload.hash !== 'string') {
return server.reply({
cmd: 'warn', // @todo Add numeric error code as `id`
text: "hash:'targethash' or ip:'1.2.3.4' is required",
channel: socket.channel, // @todo Multichannel
}, socket);
}
if (typeof payload.ip === 'string') {
if (payload.ip === '*') {
core.muzzledHashes = {};
return server.broadcast({
cmd: 'info',
text: `${socket.nick} unmuzzled all users`,
channel: false, // @todo Multichannel, false for global
}, { level: isModerator });
}
} else if (payload.hash === '*') {
core.muzzledHashes = {};
return server.broadcast({
cmd: 'info',
text: `${socket.nick} unmuzzled all users`,
channel: false, // @todo Multichannel, false for global
}, { level: isModerator });
}
// find target & remove mute status
let target;
if (typeof payload.ip === 'string') {
target = server.getSocketHash(payload.ip);
} else {
target = payload.hash;
}
delete core.muzzledHashes[target];
// notify mods
server.broadcast({
cmd: 'info',
text: `${socket.nick}#${socket.trip} unmuzzled : ${target}`,
channel: false, // @todo Multichannel, false for global
}, { level: isModerator });
return true;
}
/**
* Module meta information
* @public
* @typedef {Object} speak/info
* @property {string} name - Module command name
* @property {string} category - Module category name
* @property {string} description - Information about module
* @property {Array} aliases - An array of alternative cmd names
* @property {string} usage - Information about module usage
*/
export const info = {
name: 'speak',
category: 'moderators',
description: 'Pardon a dumb user to be able to speak again',
aliases: ['unmuzzle', 'unmute'],
usage: `
API: { cmd: 'speak', ip/hash: '<target ip or hash>' }`,
};

91
commands/mod/unban.js Normal file
View File

@ -0,0 +1,91 @@
/**
* @author Marzavec ( https://github.com/marzavec )
* @summary Unban a user
* @version 1.0.0
* @description Un-bans target user by ip or hash
* @module unban
*/
import {
isModerator,
} from '../utility/_UAC.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({
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);
}
// check user input
if (typeof payload.ip !== 'string' && typeof payload.hash !== 'string') {
return server.reply({
cmd: 'warn', // @todo Add numeric error code as `id`
text: "hash:'targethash' or ip:'1.2.3.4' is required",
channel: socket.channel, // @todo Multichannel
}, socket);
}
// find target
let mode;
let target;
if (typeof payload.ip === 'string') {
mode = 'ip';
target = payload.ip;
} else {
mode = 'hash';
target = payload.hash;
}
// remove arrest record
server.police.pardon(target);
// mask ip if used
if (mode === 'ip') {
target = server.getSocketHash(target);
}
console.log(`${socket.nick} [${socket.trip}] unbanned ${target} in ${socket.channel}`);
// reply with success
server.reply({
cmd: 'info',
text: `Unbanned ${target}`,
channel: socket.channel, // @todo Multichannel
}, socket);
// notify mods
server.broadcast({
cmd: 'info',
text: `${socket.nick}#${socket.trip} unbanned: ${target}`,
channel: false, // @todo Multichannel, false for global
}, { level: isModerator });
// stats are fun
core.stats.decrement('users-banned');
return true;
}
/**
* Module meta information
* @public
* @typedef {Object} unban/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: 'unban',
category: 'moderators',
description: 'Un-bans target user by ip or hash',
usage: `
API: { cmd: 'unban', ip/hash: '<target ip or hash>' }`,
};

64
commands/mod/unbanall.js Normal file
View File

@ -0,0 +1,64 @@
/**
* @author Marzavec ( https://github.com/marzavec )
* @summary Released them from the void
* @version 1.0.0
* @description Clears all banned ip addresses
* @module unbanall
*/
import {
isModerator,
} from '../utility/_UAC.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({ core, server, socket }) {
// increase rate limit chance and ignore if not admin or mod
if (!isModerator(socket.level)) {
return server.police.frisk(socket, 10);
}
// remove arrest records
server.police.clear();
core.stats.set('users-banned', 0);
console.log(`${socket.nick} [${socket.trip}] unbanned all`);
// reply with success
server.reply({
cmd: 'info',
text: 'Unbanned all ip addresses',
channel: socket.channel, // @todo Multichannel
}, socket);
// notify mods
server.broadcast({
cmd: 'info',
text: `${socket.nick}#${socket.trip} unbanned all ip addresses`,
channel: false, // @todo Multichannel, false for global
}, { level: isModerator });
return true;
}
/**
* Module meta information
* @public
* @typedef {Object} unbanall/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: 'unbanall',
category: 'moderators',
description: 'Clears all banned ip addresses',
usage: `
API: { cmd: 'unbanall' }`,
};

View File

@ -0,0 +1,89 @@
/* eslint no-console: 0 */
/**
* @author Marzavec ( https://github.com/marzavec )
* @summary Unlock target channel
* @version 1.0.0
* @description Unlocks a channel allowing anyone to join
* @module unlockroom
*/
import {
isModerator,
} from '../utility/_UAC.js';
/**
* Automatically executes once after server is ready
* @param {Object} core - Reference to core enviroment object
* @public
* @return {void}
*/
export async function init(core) {
if (typeof core.locked === 'undefined') {
core.locked = {};
}
}
/**
* 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,
}) {
// 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;
}
if (!core.locked[targetChannel]) {
return server.reply({
cmd: 'info',
text: 'Channel is not locked.',
channel: socket.channel, // @todo Multichannel
}, socket);
}
core.locked[targetChannel] = false;
server.broadcast({
cmd: 'info',
text: `Channel: ?${targetChannel} unlocked by [${socket.trip}]${socket.nick}`,
channel: targetChannel, // @todo Multichannel, false for global info
}, { channel: targetChannel, level: isModerator });
console.log(`Channel: ?${targetChannel} unlocked by [${socket.trip}]${socket.nick} in ${socket.channel}`);
return true;
}
/**
* Module meta information
* @public
* @typedef {Object} unlockroom/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: 'unlockroom',
category: 'moderators',
description: 'Unlock the current channel you are in or target channel as specified',
usage: `
API: { cmd: 'unlockroom', channel: '<optional target channel>' }`,
};

View File

@ -0,0 +1,108 @@
/**
* @author Marzavec ( https://github.com/marzavec )
* @summary Channel helper
* @version 1.0.0
* @description Functions to assist with channel manipulation
* @module Channels
*/
import {
Errors,
} from './_Constants.js';
/**
* Checks if a client can join `channel`, returns numeric error code or true if
* able to join
* @public
* @param {string} channel Target channel
* @param {object} socket Target client to evaluate
* @return {boolean||error id}
*/
export function canJoinChannel(channel, socket) {
if (typeof channel !== 'string') return Errors.Channel.INVALID_NAME;
if (channel === '') return Errors.Channel.INVALID_NAME;
if (channel.length > 120) return Errors.Channel.INVALID_LENGTH;
if (typeof socket.banned !== 'undefined' && socket.banned) return Errors.Channel.DEY_BANNED;
return true;
}
/**
* Returns an object containing info about the specified channel,
* including if it is owned, mods, permissions
* @public
* @param {string} config Server config object
* @param {string} channel Target channel
* @return {object}
*/
export function getChannelSettings(config, channel) {
if (typeof config.permissions !== 'undefined') {
if (typeof config.permissions[channel] !== 'undefined') {
return config.permissions[channel];
}
}
return {
owned: false,
};
}
/**
* Returns an object containing info about the specified channel,
* including if it is owned, mods, permissions
* @public
* @param {MainServer} server Main server reference
* @param {object} payload Object containing `userid` or `nick`
* @param {number} limit Optional return limit
* @return {array}
*/
export function findUsers(server, payload, limit = 0) {
let targetClients;
if (typeof payload.userid !== 'undefined') {
targetClients = server.findSockets({
channel: payload.channel,
userid: payload.userid,
});
} else if (typeof payload.nick !== 'undefined') {
targetClients = server.findSockets({
channel: payload.channel,
nick: payload.nick,
});
} else {
return [];
}
if (limit !== 0 && targetClients.length > limit) {
return targetClients.splice(0, limit);
}
return targetClients;
}
/**
* Overload for `findUsers` when only 1 user is expected
* @public
* @param {MainServer} server Main server reference
* @param {object} payload Object containing `userid` or `nick`
* @param {number} limit Optional return limit
* @return {boolean||object}
*/
export function findUser(server, payload) {
return findUsers(server, payload, 1)[0] || false;
}
/**
* Check if the target socket's userid is already in target channel
* @param {MainServer} server Main server reference
* @param {string} channel Target channel
* @param {object} socket Target client to evaluate
* @return {boolean||object}
*/
export function socketInChannel(server, channel, socket) {
return findUser(server, {
channel,
userid: socket.userid,
});
}

View File

@ -0,0 +1,49 @@
/**
* @author Marzavec ( https://github.com/marzavec )
* @summary Error ID list
* @version 1.0.0
* @description Exports an object that hold common global error IDs
* @module Constants
*/
/* Base error ranges */
const GlobalErrors = 10;
const JoinErrors = 20;
const ChannelErrors = 30;
const InviteErrors = 40;
const SessionErrors = 50;
/**
* Holds the numeric id values for each error type
* @typedef {object} Errors
*/
export const Errors = {
Global: {
RATELIMIT: GlobalErrors + 1,
UNKNOWN_USER: GlobalErrors + 2,
PERMISSION: GlobalErrors + 3,
},
Join: {
RATELIMIT: JoinErrors + 1,
INVALID_NICK: JoinErrors + 2,
ALREADY_JOINED: JoinErrors + 3,
NAME_TAKEN: JoinErrors + 4,
},
Channel: {
INVALID_NAME: ChannelErrors + 1,
INVALID_LENGTH: ChannelErrors + 2,
DEY_BANNED: ChannelErrors + 3,
},
Invite: {
RATELIMIT: InviteErrors + 1,
},
Session: {
BAD_SESSION: SessionErrors + 1,
},
};
export default Errors;

View File

@ -0,0 +1,138 @@
/* eslint no-param-reassign: 0 */
/**
* @author Marzavec ( https://github.com/marzavec )
* @summary Support functions for old clients
* @version 1.0.0
* @description Functions to bridge the older v1 clients with the latest protocol
* @module LegacyFunctions
*/
import {
isAdmin,
isModerator,
} from './_UAC.js';
/**
* Marks the socket as using the legacy protocol and
* applies the missing `pass` property to the payload
* @param {MainServer} server Main server reference
* @param {WebSocket} socket Target client socket
* @param {object} payload The original `join` payload
* @returns {object}
*/
export function upgradeLegacyJoin(server, socket, payload) {
const newPayload = payload;
// `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
socket.hash = server.getSocketHash(socket);
socket.isBot = false;
socket.color = false;
// pull the password from the nick
const nickArray = payload.nick.split('#', 2);
newPayload.nick = nickArray[0].trim();
if (nickArray[1] && typeof payload.pass === 'undefined') {
newPayload.pass = nickArray[1]; // eslint-disable-line prefer-destructuring
}
// dunno how this happened on the legacy version
if (typeof payload.password !== 'undefined') {
newPayload.pass = payload.password;
}
// apply the missing `userid` prop
if (typeof socket.userid === 'undefined') {
socket.userid = Math.floor(Math.random() * 9999999999999);
}
return newPayload;
}
/**
* Return the correct `uType` label for the specific level
* @param {number} level Numeric level to find the label for
*/
export function legacyLevelToLabel(level) {
if (isAdmin(level)) return 'admin';
if (isModerator(level)) return 'mod';
return 'user';
}
/**
* Alter the outgoing payload to an `info` cmd and add/change missing props
* @param {object} payload Original payload
* @param {string} nick Sender nick
* @return {object}
*/
export function legacyInviteOut(payload, nick) {
return {
...payload,
...{
cmd: 'info',
type: 'invite',
from: nick,
text: `${nick} invited you to ?${payload.inviteChannel}`,
channel: payload.channel, // @todo Multichannel
},
};
}
/**
* Alter the outgoing payload to an `info` cmd and add/change missing props
* @param {object} payload Original payload
* @param {string} nick Receiver nick
* @return {object}
*/
export function legacyInviteReply(payload, nick) {
return {
...payload,
...{
cmd: 'info',
type: 'invite',
from: '',
text: `You invited ${nick} to ?${payload.inviteChannel}`,
channel: payload.channel, // @todo Multichannel
},
};
}
/**
* Alter the outgoing payload to a `whisper` cmd and add/change missing props
* @param {object} payload Original payload
* @param {string} nick Sender nick
* @return {object}
*/
export function legacyWhisperOut(payload, from) {
return {
...payload,
...{
cmd: 'info',
type: 'whisper',
from: from.nick,
trip: from.trip || 'null',
text: `${from.nick} whispered: ${payload.text}`,
},
};
}
/**
* Alter the outgoing payload to a `whisper` cmd and add/change missing props
* @param {object} payload Original payload
* @param {string} nick Receiver nick
* @return {object}
*/
export function legacyWhisperReply(payload, nick) {
return {
...payload,
...{
cmd: 'info',
type: 'whisper',
text: `You whispered to @${nick}: ${payload.text}`,
},
};
}

31
commands/utility/_Text.js Normal file
View File

@ -0,0 +1,31 @@
/* eslint import/prefer-default-export: 0 */
/**
* @author MinusGix ( https://github.com/MinusGix )
* @summary General string helper functions
* @version v1.0.0
* @description A library of several commonly used string functions
* @module Text
*/
/**
* Check and trim string provided by remote client
* @public
* @param {string} text - Subject string
* @return {string|null}
*/
export const parseText = (text) => {
// verifies user input is text
if (typeof text !== 'string') {
return null;
}
let sanitizedText = text;
// strip newlines from beginning and end
sanitizedText = sanitizedText.replace(/^\s*\n|^\s+$|\n\s*$/g, '');
// replace 3+ newlines with just 2 newlines
sanitizedText = sanitizedText.replace(/\n{3,}/g, '\n\n');
return sanitizedText;
};

View File

@ -4,9 +4,17 @@
* @property {Object} levels - Defines labels for default permission ranges
* @author MinusGix ( https://github.com/MinusGix )
* @version v1.0.0
* @license WTFPL ( http://www.wtfpl.net/txt/copying/ )
* @module UAC
*/
import {
getChannelSettings,
} from './_Channels.js';
const {
createHash,
} = await import('crypto');
/**
* Object defining labels for default permission ranges
* @typedef {Object} levels
@ -98,12 +106,15 @@ export function isTrustedUser(level) {
*/
export function getUserDetails(socket) {
return {
uType: socket.uType,
nick: socket.nick,
trip: socket.trip || 'null',
trip: socket.trip || '',
uType: socket.uType,
hash: socket.hash,
level: socket.level,
userid: socket.userid,
isBot: socket.isBot,
color: socket.color,
online: true,
};
}
@ -114,5 +125,67 @@ export function getUserDetails(socket) {
* @return {boolean}
*/
export function verifyNickname(nick) {
if (typeof nick === 'undefined') return false;
return /^[a-zA-Z0-9_]{1,24}$/.test(nick);
}
/**
* Hashes a user's password, returning a trip code
* or a blank string
* @public
* @param {string} pass User's password
* @param {buffer} salt Server salt data
* @param {string} config Server config object
* @param {string} channel Channel-level permissions check
* @return {string}
*/
export function getUserPerms(pass, salt, config, channel) {
if (!pass) {
return {
trip: '',
level: levels.default,
};
}
const sha = createHash('sha256');
sha.update(pass + salt);
const trip = sha.digest('base64').substr(0, 6);
// check if user is global admin
if (trip === config.adminTrip) {
return {
trip: 'Admin',
level: levels.admin,
};
}
let level = levels.default;
// check if user is global mod
config.globalMods.forEach((mod) => { // eslint-disable-line consistent-return
if (trip === mod.trip) {
level = levels.moderator;
}
});
const channelSettings = getChannelSettings(config, channel);
if (channelSettings.owned) {
// check if user is channel owner
// @todo channel ownership patch
// check if user is channel mod
// @todo channel ownership patch
// check if user is channel trusted
// @todo channel ownership patch
}
// check if user is global trusted
// @todo channel ownership patch
return {
trip,
level,
};
}

View File

@ -1,48 +0,0 @@
You can programmatically access hack.chat using the following commands via a websocket. A list of wrappers written for accessing hack.chat can be found [here](https://github.com/hack-chat/3rd-party-software-list#libraries).
The commands are to be sent through a websocket to the URL `wss://hack.chat/chat-ws` (everything sent and received are JSON). If you are sending messages locally or to another domain, replace 'hack.chat' with the respective domain. If you're running your own instance of hack.chat, you can retain backwards-compatibility in order to ensure that software created for the main server will work on yours too.
All commands sent must be JSON objects with the command specified in the `"cmd"` key. For example:
```json
{
"cmd": "join",
"channel": "programming",
"nick": "john#doe"
}
```
hack.chat has three permission levels. When you access a command, hack.chat automatically knows your permission level from your trip code. The lowest permission level is `user`. `mod` is above `user`, so it can access `user` commands in addition to `mod` commands. `admin` is similarly above `mod`.
# `user`
|Command|Parameters|Explanation|
|-------|----------|-----------|
|`changenick`|`nick`|Changes the current connection's nickname.|
|`chat`|`text`|This broadcasts `text` to the channel the user is connected to.|
|`disconnect`||An event handler or forced disconnect.|
|`invite`|`nick`|Generates a pseudo-unique channel name and passes it to both the calling user and `nick`.|
|`join`|`channel`, `nick`|Places the calling socket into the target channel with the target nick and broadcasts the event to the channel.|
|`morestats`||Sends back the current server's stats to the calling client.|
|`move`|`channel`|This will change the current channel to `channel`.|
|`stats`||Sends back legacy server stats to the calling client. Use `morestats` when possible.|
|`help`|`category` or `command`|Gives documentation programmatically. If `category` (the permission level, such as `mod`) is sent, a list of commands available to that permission level will be sent back (as a `string` and not an `array`). This list only includes what is unique to that category and not every command a user with that permission level could perform. If `command` (e.g., `chat`) is sent, a description of the command will be sent back.|
# `mod`
|Command|Parameters|Explanation|
|-------|----------|-----------|
|`ban`|`nick`|Disconnects the target nickname in the same channel as the calling socket and adds it to the rate limiter.|
|`kick`|`nick`|Silently forces target client(s) into another channel. `nick` may be `string` or `array` of `string`s.|
|`unban`|`ip` or `hash`|Removes the target ip from the rate limiter.|
|`dumb`|`nick`|Mutes a user's (spammer's) texts such that it is displayable to the user only.|
|`speak`|`ip` or `hash`|Unmutes the user's (spammer's) texts and makes it displayable to everyone again.|
# `admin`
|Command|Parameters|Explanation|
|-------|----------|-----------|
|`addmod`|`nick`|Adds the target trip to the config as a mod and upgrades the socket type.|
|`listusers`||Outputs all current channels and sockets in those channels.|
|`reload`||(Re)loads any new commands into memory and outputs errors, if any.|
|`saveconfig`||Saves the current config.|
|`shout`|`text`|Displays the passed text to each client connected.|

View File

@ -0,0 +1,159 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="utf-8">
<title>JSDoc: Source: admin/addmod.js</title>
<script src="scripts/prettify/prettify.js"> </script>
<script src="scripts/prettify/lang-css.js"> </script>
<!--[if lt IE 9]>
<script src="//html5shiv.googlecode.com/svn/trunk/html5.js"></script>
<![endif]-->
<link type="text/css" rel="stylesheet" href="styles/prettify-tomorrow.css">
<link type="text/css" rel="stylesheet" href="styles/jsdoc-default.css">
</head>
<body>
<div id="main">
<h1 class="page-title">Source: admin/addmod.js</h1>
<section>
<article>
<pre class="prettyprint source linenums"><code>/**
* @author Marzavec ( https://github.com/marzavec )
* @summary Create a new mod trip
* @version 1.0.0
* @description Adds target trip to the config as a mod and upgrades the socket type
* @module addmod
*/
import {
isAdmin,
isModerator,
levels,
getUserDetails,
} from '../utility/_UAC.js';
/**
* Executes when invoked by a remote client
* @param {Object} env - Enviroment object with references to core, server, socket &amp; payload
* @public
* @return {void}
*/
export async function run({
core, server, socket, payload,
}) {
// increase rate limit chance and ignore if not admin
if (!isAdmin(socket.level)) {
return server.police.frisk(socket, 20);
}
// add new trip to config
core.appConfig.data.globalMods.push({ trip: payload.trip });
// find targets current connections
const newMod = server.findSockets({ trip: payload.trip });
if (newMod.length !== 0) {
// build update notice with new privileges
const updateNotice = {
...getUserDetails(newMod[0]),
...{
cmd: 'updateUser',
uType: 'mod', // @todo use legacyLevelToLabel from _LegacyFunctions.js
level: levels.moderator,
},
};
for (let i = 0, l = newMod.length; i &lt; l; i += 1) {
// upgrade privileges
newMod[i].uType = 'mod'; // @todo use legacyLevelToLabel from _LegacyFunctions.js
newMod[i].level = levels.moderator;
// inform new mod
server.send({
cmd: 'info',
text: 'You are now a mod.',
channel: newMod[i].channel, // @todo Multichannel
}, newMod[i]);
// notify channel
server.broadcast({
...updateNotice,
...{
channel: newMod[i].channel,
},
}, { channel: newMod[i].channel });
}
}
// return success message
server.reply({
cmd: 'info',
text: `Added mod trip: ${payload.trip}, remember to run 'saveconfig' to make it permanent`,
channel: socket.channel, // @todo Multichannel
}, socket);
// notify all mods
server.broadcast({
cmd: 'info',
text: `Added mod: ${payload.trip}`,
channel: false, // @todo Multichannel, false for global info
}, { level: isModerator });
return true;
}
/**
* The following payload properties are required to invoke this module:
* "trip"
* @public
* @typedef {Array} addmod/requiredData
*/
export const requiredData = ['trip'];
/**
* Module meta information
* @public
* @typedef {Object} addmod/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: 'addmod',
category: 'admin',
description: 'Adds target trip to the config as a mod and upgrades the socket type',
usage: `
API: { cmd: 'addmod', trip: '&lt;target trip>' }`,
};
</code></pre>
</article>
</section>
</div>
<nav>
<h2><a href="index.html">Home</a></h2><h3>Modules</h3><ul><li><a href="module-addmod.html">addmod</a></li><li><a href="module-ban.html">ban</a></li><li><a href="module-changecolor.html">changecolor</a></li><li><a href="module-changenick.html">changenick</a></li><li><a href="module-chat.html">chat</a></li><li><a href="module-disablecaptcha.html">disablecaptcha</a></li><li><a href="module-disconnect.html">disconnect</a></li><li><a href="module-dumb.html">dumb</a></li><li><a href="module-emote.html">emote</a></li><li><a href="module-enablecaptcha.html">enablecaptcha</a></li><li><a href="module-forcecolor.html">forcecolor</a></li><li><a href="module-help.html">help</a></li><li><a href="module-invite.html">invite</a></li><li><a href="module-join.html">join</a></li><li><a href="module-kick.html">kick</a></li><li><a href="module-listusers.html">listusers</a></li><li><a href="module-lockroom.html">lockroom</a></li><li><a href="module-morestats.html">morestats</a></li><li><a href="module-ping.html">ping</a></li><li><a href="module-reload.html">reload</a></li><li><a href="module-removemod.html">removemod</a></li><li><a href="module-saveconfig.html">saveconfig</a></li><li><a href="module-session.html">session</a></li><li><a href="module-shout.html">shout</a></li><li><a href="module-socketreply.html">socketreply</a></li><li><a href="module-speak.html">speak</a></li><li><a href="module-stats.html">stats</a></li><li><a href="module-unban.html">unban</a></li><li><a href="module-unbanall.html">unbanall</a></li><li><a href="module-unlockroom.html">unlockroom</a></li><li><a href="module-updateMessage.html">updateMessage</a></li><li><a href="module-whisper.html">whisper</a></li></ul>
</nav>
<br class="clear">
<footer>
Documentation generated by <a href="https://github.com/jsdoc/jsdoc">JSDoc 3.6.10</a> on Fri Dec 29 2023 23:22:35 GMT-0800 (Pacific Standard Time)
</footer>
<script> prettyPrint(); </script>
<script src="scripts/linenumber.js"> </script>
</body>
</html>

View File

@ -0,0 +1,128 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="utf-8">
<title>JSDoc: Source: admin/listusers.js</title>
<script src="scripts/prettify/prettify.js"> </script>
<script src="scripts/prettify/lang-css.js"> </script>
<!--[if lt IE 9]>
<script src="//html5shiv.googlecode.com/svn/trunk/html5.js"></script>
<![endif]-->
<link type="text/css" rel="stylesheet" href="styles/prettify-tomorrow.css">
<link type="text/css" rel="stylesheet" href="styles/jsdoc-default.css">
</head>
<body>
<div id="main">
<h1 class="page-title">Source: admin/listusers.js</h1>
<section>
<article>
<pre class="prettyprint source linenums"><code>/* eslint no-unused-vars: 0 */
/* eslint no-restricted-syntax: 0 */
/* eslint guard-for-in: 0 */
/**
* @author Marzavec ( https://github.com/marzavec )
* @summary Show users and channels
* @version 1.0.0
* @description Outputs all current channels and sockets in those channels
* @module listusers
*/
import {
isAdmin,
} from '../utility/_UAC.js';
/**
* Executes when invoked by a remote client
* @param {Object} env - Enviroment object with references to core, server, socket &amp; payload
* @public
* @return {void}
*/
export async function run({ server, socket }) {
// increase rate limit chance and ignore if not admin
if (!isAdmin(socket.level)) {
return server.police.frisk(socket, 20);
}
// find all users currently in a channel
const currentUsers = server.findSockets({
channel: (channel) => true,
});
// compile channel and user list
const channels = {};
for (let i = 0, j = currentUsers.length; i &lt; j; i += 1) {
if (typeof channels[currentUsers[i].channel] === 'undefined') {
channels[currentUsers[i].channel] = [];
}
channels[currentUsers[i].channel].push(
`[${currentUsers[i].trip || 'null'}]${currentUsers[i].nick}`,
);
}
// build output
const lines = [];
for (const channel in channels) {
lines.push(`?${channel} ${channels[channel].join(', ')}`);
}
// send reply
server.reply({
cmd: 'info',
text: lines.join('\n'),
channel: socket.channel, // @todo Multichannel
}, socket);
return true;
}
/**
* Module meta information
* @public
* @typedef {Object} listusers/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: 'listusers',
category: 'admin',
description: 'Outputs all current channels and sockets in those channels',
usage: `
API: { cmd: 'listusers' }`,
};
</code></pre>
</article>
</section>
</div>
<nav>
<h2><a href="index.html">Home</a></h2><h3>Modules</h3><ul><li><a href="module-addmod.html">addmod</a></li><li><a href="module-ban.html">ban</a></li><li><a href="module-changecolor.html">changecolor</a></li><li><a href="module-changenick.html">changenick</a></li><li><a href="module-chat.html">chat</a></li><li><a href="module-disablecaptcha.html">disablecaptcha</a></li><li><a href="module-disconnect.html">disconnect</a></li><li><a href="module-dumb.html">dumb</a></li><li><a href="module-emote.html">emote</a></li><li><a href="module-enablecaptcha.html">enablecaptcha</a></li><li><a href="module-forcecolor.html">forcecolor</a></li><li><a href="module-help.html">help</a></li><li><a href="module-invite.html">invite</a></li><li><a href="module-join.html">join</a></li><li><a href="module-kick.html">kick</a></li><li><a href="module-listusers.html">listusers</a></li><li><a href="module-lockroom.html">lockroom</a></li><li><a href="module-morestats.html">morestats</a></li><li><a href="module-ping.html">ping</a></li><li><a href="module-reload.html">reload</a></li><li><a href="module-removemod.html">removemod</a></li><li><a href="module-saveconfig.html">saveconfig</a></li><li><a href="module-session.html">session</a></li><li><a href="module-shout.html">shout</a></li><li><a href="module-socketreply.html">socketreply</a></li><li><a href="module-speak.html">speak</a></li><li><a href="module-stats.html">stats</a></li><li><a href="module-unban.html">unban</a></li><li><a href="module-unbanall.html">unbanall</a></li><li><a href="module-unlockroom.html">unlockroom</a></li><li><a href="module-updateMessage.html">updateMessage</a></li><li><a href="module-whisper.html">whisper</a></li></ul>
</nav>
<br class="clear">
<footer>
Documentation generated by <a href="https://github.com/jsdoc/jsdoc">JSDoc 3.6.10</a> on Fri Dec 29 2023 23:22:35 GMT-0800 (Pacific Standard Time)
</footer>
<script> prettyPrint(); </script>
<script src="scripts/linenumber.js"> </script>
</body>
</html>

View File

@ -0,0 +1,122 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="utf-8">
<title>JSDoc: Source: admin/reload.js</title>
<script src="scripts/prettify/prettify.js"> </script>
<script src="scripts/prettify/lang-css.js"> </script>
<!--[if lt IE 9]>
<script src="//html5shiv.googlecode.com/svn/trunk/html5.js"></script>
<![endif]-->
<link type="text/css" rel="stylesheet" href="styles/prettify-tomorrow.css">
<link type="text/css" rel="stylesheet" href="styles/jsdoc-default.css">
</head>
<body>
<div id="main">
<h1 class="page-title">Source: admin/reload.js</h1>
<section>
<article>
<pre class="prettyprint source linenums"><code>/**
* @author Marzavec ( https://github.com/marzavec )
* @summary Refresh modules
* @version 1.0.0
* @description Allows a remote user to clear and re-import the server command modules
* @module reload
*/
import {
isAdmin,
isModerator,
} from '../utility/_UAC.js';
/**
* Executes when invoked by a remote client
* @param {Object} env - Enviroment object with references to core, server, socket &amp; payload
* @public
* @return {void}
*/
export async function run({
core, server, socket, payload,
}) {
// increase rate limit chance and ignore if not admin
if (!isAdmin(socket.level)) {
return server.police.frisk(socket, 20);
}
// do command reload and store results
let loadResult = await core.commands.reloadCommands();
// clear and rebuild all module hooks
server.loadHooks();
// build reply based on reload results
if (loadResult === '') {
loadResult = `Reloaded ${core.commands.commands.length} commands, 0 errors`;
} else {
loadResult = `Reloaded ${core.commands.commands.length} commands, error(s):
${loadResult}`;
}
if (typeof payload.reason !== 'undefined') {
loadResult += `\nReason: ${payload.reason}`;
}
// send results to moderators (which the user using this command is higher than)
server.broadcast({
cmd: 'info',
text: loadResult,
channel: false, // @todo Multichannel, false for global
}, { level: isModerator });
return true;
}
/**
* Module meta information
* @public
* @typedef {Object} reload/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: 'reload',
category: 'admin',
description: 'Allows a remote user to clear and re-import the server command modules',
usage: `
API: { cmd: 'reload', reason: '&lt;optional reason append>' }`,
};
</code></pre>
</article>
</section>
</div>
<nav>
<h2><a href="index.html">Home</a></h2><h3>Modules</h3><ul><li><a href="module-addmod.html">addmod</a></li><li><a href="module-ban.html">ban</a></li><li><a href="module-changecolor.html">changecolor</a></li><li><a href="module-changenick.html">changenick</a></li><li><a href="module-chat.html">chat</a></li><li><a href="module-disablecaptcha.html">disablecaptcha</a></li><li><a href="module-disconnect.html">disconnect</a></li><li><a href="module-dumb.html">dumb</a></li><li><a href="module-emote.html">emote</a></li><li><a href="module-enablecaptcha.html">enablecaptcha</a></li><li><a href="module-forcecolor.html">forcecolor</a></li><li><a href="module-help.html">help</a></li><li><a href="module-invite.html">invite</a></li><li><a href="module-join.html">join</a></li><li><a href="module-kick.html">kick</a></li><li><a href="module-listusers.html">listusers</a></li><li><a href="module-lockroom.html">lockroom</a></li><li><a href="module-morestats.html">morestats</a></li><li><a href="module-ping.html">ping</a></li><li><a href="module-reload.html">reload</a></li><li><a href="module-removemod.html">removemod</a></li><li><a href="module-saveconfig.html">saveconfig</a></li><li><a href="module-session.html">session</a></li><li><a href="module-shout.html">shout</a></li><li><a href="module-socketreply.html">socketreply</a></li><li><a href="module-speak.html">speak</a></li><li><a href="module-stats.html">stats</a></li><li><a href="module-unban.html">unban</a></li><li><a href="module-unbanall.html">unbanall</a></li><li><a href="module-unlockroom.html">unlockroom</a></li><li><a href="module-updateMessage.html">updateMessage</a></li><li><a href="module-whisper.html">whisper</a></li></ul>
</nav>
<br class="clear">
<footer>
Documentation generated by <a href="https://github.com/jsdoc/jsdoc">JSDoc 3.6.10</a> on Fri Dec 29 2023 23:22:35 GMT-0800 (Pacific Standard Time)
</footer>
<script> prettyPrint(); </script>
<script src="scripts/linenumber.js"> </script>
</body>
</html>

View File

@ -0,0 +1,164 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="utf-8">
<title>JSDoc: Source: admin/removemod.js</title>
<script src="scripts/prettify/prettify.js"> </script>
<script src="scripts/prettify/lang-css.js"> </script>
<!--[if lt IE 9]>
<script src="//html5shiv.googlecode.com/svn/trunk/html5.js"></script>
<![endif]-->
<link type="text/css" rel="stylesheet" href="styles/prettify-tomorrow.css">
<link type="text/css" rel="stylesheet" href="styles/jsdoc-default.css">
</head>
<body>
<div id="main">
<h1 class="page-title">Source: admin/removemod.js</h1>
<section>
<article>
<pre class="prettyprint source linenums"><code>/**
* @author Marzavec ( https://github.com/marzavec )
* @summary Removes a mod
* @version 1.0.0
* @description Removes target trip from the config as a mod and downgrades the socket type
* @module removemod
*/
import {
isAdmin,
isModerator,
levels,
getUserDetails,
} from '../utility/_UAC.js';
/**
* Executes when invoked by a remote client
* @param {Object} env - Enviroment object with references to core, server, socket &amp; payload
* @public
* @return {void}
*/
export async function run({
core, server, socket, payload,
}) {
// increase rate limit chance and ignore if not admin
if (!isAdmin(socket.level)) {
return server.police.frisk(socket, 20);
}
// remove trip from config
// eslint-disable-next-line no-param-reassign
core.appConfig.data.globalMods = core.appConfig.data.globalMods.filter(
(mod) => mod.trip !== payload.trip,
);
// find targets current connections
const targetMod = server.findSockets({ trip: payload.trip });
if (targetMod.length !== 0) {
// build update notice with new privileges
const updateNotice = {
...getUserDetails(targetMod[0]),
...{
cmd: 'updateUser',
uType: 'user', // @todo use legacyLevelToLabel from _LegacyFunctions.js
level: levels.default,
},
};
for (let i = 0, l = targetMod.length; i &lt; l; i += 1) {
// downgrade privileges
targetMod[i].uType = 'user';
targetMod[i].level = levels.default;
// inform ex-mod
server.send({
cmd: 'info',
text: 'You are now a user.',
channel: targetMod[i].channel, // @todo Multichannel
}, targetMod[i]);
// notify channel
server.broadcast({
...updateNotice,
...{
channel: targetMod[i].channel,
},
}, { channel: targetMod[i].channel });
}
}
// return success message
server.reply({
cmd: 'info',
text: `Removed mod trip: ${
payload.trip
}, remember to run 'saveconfig' to make it permanent`,
channel: socket.channel, // @todo Multichannel
}, socket);
// notify all mods
server.broadcast({
cmd: 'info',
text: `Removed mod: ${payload.trip}`,
channel: false, // @todo Multichannel, false for global
}, { level: isModerator });
return true;
}
/**
* The following payload properties are required to invoke this module:
* "trip"
* @public
* @typedef {Array} removemod/requiredData
*/
export const requiredData = ['trip'];
/**
* Module meta information
* @public
* @typedef {Object} removemod/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: 'removemod',
category: 'admin',
description: 'Removes target trip from the config as a mod and downgrades the socket type',
usage: `
API: { cmd: 'removemod', trip: '&lt;target trip>' }`,
};
</code></pre>
</article>
</section>
</div>
<nav>
<h2><a href="index.html">Home</a></h2><h3>Modules</h3><ul><li><a href="module-addmod.html">addmod</a></li><li><a href="module-ban.html">ban</a></li><li><a href="module-changecolor.html">changecolor</a></li><li><a href="module-changenick.html">changenick</a></li><li><a href="module-chat.html">chat</a></li><li><a href="module-disablecaptcha.html">disablecaptcha</a></li><li><a href="module-disconnect.html">disconnect</a></li><li><a href="module-dumb.html">dumb</a></li><li><a href="module-emote.html">emote</a></li><li><a href="module-enablecaptcha.html">enablecaptcha</a></li><li><a href="module-forcecolor.html">forcecolor</a></li><li><a href="module-help.html">help</a></li><li><a href="module-invite.html">invite</a></li><li><a href="module-join.html">join</a></li><li><a href="module-kick.html">kick</a></li><li><a href="module-listusers.html">listusers</a></li><li><a href="module-lockroom.html">lockroom</a></li><li><a href="module-morestats.html">morestats</a></li><li><a href="module-ping.html">ping</a></li><li><a href="module-reload.html">reload</a></li><li><a href="module-removemod.html">removemod</a></li><li><a href="module-saveconfig.html">saveconfig</a></li><li><a href="module-session.html">session</a></li><li><a href="module-shout.html">shout</a></li><li><a href="module-socketreply.html">socketreply</a></li><li><a href="module-speak.html">speak</a></li><li><a href="module-stats.html">stats</a></li><li><a href="module-unban.html">unban</a></li><li><a href="module-unbanall.html">unbanall</a></li><li><a href="module-unlockroom.html">unlockroom</a></li><li><a href="module-updateMessage.html">updateMessage</a></li><li><a href="module-whisper.html">whisper</a></li></ul>
</nav>
<br class="clear">
<footer>
Documentation generated by <a href="https://github.com/jsdoc/jsdoc">JSDoc 3.6.10</a> on Fri Dec 29 2023 23:22:35 GMT-0800 (Pacific Standard Time)
</footer>
<script> prettyPrint(); </script>
<script src="scripts/linenumber.js"> </script>
</body>
</html>

View File

@ -0,0 +1,113 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="utf-8">
<title>JSDoc: Source: admin/saveconfig.js</title>
<script src="scripts/prettify/prettify.js"> </script>
<script src="scripts/prettify/lang-css.js"> </script>
<!--[if lt IE 9]>
<script src="//html5shiv.googlecode.com/svn/trunk/html5.js"></script>
<![endif]-->
<link type="text/css" rel="stylesheet" href="styles/prettify-tomorrow.css">
<link type="text/css" rel="stylesheet" href="styles/jsdoc-default.css">
</head>
<body>
<div id="main">
<h1 class="page-title">Source: admin/saveconfig.js</h1>
<section>
<article>
<pre class="prettyprint source linenums"><code>/**
* @author Marzavec ( https://github.com/marzavec )
* @summary Saves the config
* @version 1.0.0
* @description Writes the current config to disk
* @module saveconfig
*/
import {
isAdmin,
isModerator,
} from '../utility/_UAC.js';
/**
* Executes when invoked by a remote client
* @param {Object} env - Enviroment object with references to core, server, socket &amp; payload
* @public
* @return {void}
*/
export async function run({ core, server, socket }) {
// increase rate limit chance and ignore if not admin
if (!isAdmin(socket.level)) {
return server.police.frisk(socket, 20);
}
// attempt save, notify of failure
try {
await core.appConfig.write();
} catch (err) {
return server.reply({
cmd: 'warn', // @todo Add numeric error code as `id`
text: 'Failed to save config, check logs.',
channel: socket.channel, // @todo Multichannel
}, socket);
}
// return success message to moderators and admins
server.broadcast({
cmd: 'info',
text: 'Config saved!',
channel: false, // @todo Multichannel
}, { level: isModerator });
return true;
}
/**
* Module meta information
* @public
* @typedef {Object} saveconfig/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: 'saveconfig',
category: 'admin',
description: 'Writes the current config to disk',
usage: `
API: { cmd: 'saveconfig' }`,
};
</code></pre>
</article>
</section>
</div>
<nav>
<h2><a href="index.html">Home</a></h2><h3>Modules</h3><ul><li><a href="module-addmod.html">addmod</a></li><li><a href="module-ban.html">ban</a></li><li><a href="module-changecolor.html">changecolor</a></li><li><a href="module-changenick.html">changenick</a></li><li><a href="module-chat.html">chat</a></li><li><a href="module-disablecaptcha.html">disablecaptcha</a></li><li><a href="module-disconnect.html">disconnect</a></li><li><a href="module-dumb.html">dumb</a></li><li><a href="module-emote.html">emote</a></li><li><a href="module-enablecaptcha.html">enablecaptcha</a></li><li><a href="module-forcecolor.html">forcecolor</a></li><li><a href="module-help.html">help</a></li><li><a href="module-invite.html">invite</a></li><li><a href="module-join.html">join</a></li><li><a href="module-kick.html">kick</a></li><li><a href="module-listusers.html">listusers</a></li><li><a href="module-lockroom.html">lockroom</a></li><li><a href="module-morestats.html">morestats</a></li><li><a href="module-ping.html">ping</a></li><li><a href="module-reload.html">reload</a></li><li><a href="module-removemod.html">removemod</a></li><li><a href="module-saveconfig.html">saveconfig</a></li><li><a href="module-session.html">session</a></li><li><a href="module-shout.html">shout</a></li><li><a href="module-socketreply.html">socketreply</a></li><li><a href="module-speak.html">speak</a></li><li><a href="module-stats.html">stats</a></li><li><a href="module-unban.html">unban</a></li><li><a href="module-unbanall.html">unbanall</a></li><li><a href="module-unlockroom.html">unlockroom</a></li><li><a href="module-updateMessage.html">updateMessage</a></li><li><a href="module-whisper.html">whisper</a></li></ul>
</nav>
<br class="clear">
<footer>
Documentation generated by <a href="https://github.com/jsdoc/jsdoc">JSDoc 3.6.10</a> on Fri Dec 29 2023 23:22:35 GMT-0800 (Pacific Standard Time)
</footer>
<script> prettyPrint(); </script>
<script src="scripts/linenumber.js"> </script>
</body>
</html>

View File

@ -0,0 +1,109 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="utf-8">
<title>JSDoc: Source: admin/shout.js</title>
<script src="scripts/prettify/prettify.js"> </script>
<script src="scripts/prettify/lang-css.js"> </script>
<!--[if lt IE 9]>
<script src="//html5shiv.googlecode.com/svn/trunk/html5.js"></script>
<![endif]-->
<link type="text/css" rel="stylesheet" href="styles/prettify-tomorrow.css">
<link type="text/css" rel="stylesheet" href="styles/jsdoc-default.css">
</head>
<body>
<div id="main">
<h1 class="page-title">Source: admin/shout.js</h1>
<section>
<article>
<pre class="prettyprint source linenums"><code>/**
* @author Marzavec ( https://github.com/marzavec )
* @summary Emit text everywhere
* @version 1.0.0
* @description Displays passed text to every client connected
* @module shout
*/
import {
isAdmin,
} from '../utility/_UAC.js';
/**
* Executes when invoked by a remote client
* @param {Object} env - Enviroment object with references to core, server, socket &amp; payload
* @public
* @return {void}
*/
export async function run({ server, socket, payload }) {
// increase rate limit chance and ignore if not admin
if (!isAdmin(socket.level)) {
return server.police.frisk(socket, 20);
}
// send text to all channels
server.broadcast({
cmd: 'info',
text: `Server Notice: ${payload.text}`,
channel: false, // @todo Multichannel, false for global
}, {});
return true;
}
/**
* The following payload properties are required to invoke this module:
* "text"
* @public
* @typedef {Array} shout/requiredData
*/
export const requiredData = ['text'];
/**
* Module meta information
* @public
* @typedef {Object} shout/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: 'shout',
category: 'admin',
description: 'Displays passed text to every client connected',
usage: `
API: { cmd: 'shout', text: '&lt;shout text>' }`,
};
</code></pre>
</article>
</section>
</div>
<nav>
<h2><a href="index.html">Home</a></h2><h3>Modules</h3><ul><li><a href="module-addmod.html">addmod</a></li><li><a href="module-ban.html">ban</a></li><li><a href="module-changecolor.html">changecolor</a></li><li><a href="module-changenick.html">changenick</a></li><li><a href="module-chat.html">chat</a></li><li><a href="module-disablecaptcha.html">disablecaptcha</a></li><li><a href="module-disconnect.html">disconnect</a></li><li><a href="module-dumb.html">dumb</a></li><li><a href="module-emote.html">emote</a></li><li><a href="module-enablecaptcha.html">enablecaptcha</a></li><li><a href="module-forcecolor.html">forcecolor</a></li><li><a href="module-help.html">help</a></li><li><a href="module-invite.html">invite</a></li><li><a href="module-join.html">join</a></li><li><a href="module-kick.html">kick</a></li><li><a href="module-listusers.html">listusers</a></li><li><a href="module-lockroom.html">lockroom</a></li><li><a href="module-morestats.html">morestats</a></li><li><a href="module-ping.html">ping</a></li><li><a href="module-reload.html">reload</a></li><li><a href="module-removemod.html">removemod</a></li><li><a href="module-saveconfig.html">saveconfig</a></li><li><a href="module-session.html">session</a></li><li><a href="module-shout.html">shout</a></li><li><a href="module-socketreply.html">socketreply</a></li><li><a href="module-speak.html">speak</a></li><li><a href="module-stats.html">stats</a></li><li><a href="module-unban.html">unban</a></li><li><a href="module-unbanall.html">unbanall</a></li><li><a href="module-unlockroom.html">unlockroom</a></li><li><a href="module-updateMessage.html">updateMessage</a></li><li><a href="module-whisper.html">whisper</a></li></ul>
</nav>
<br class="clear">
<footer>
Documentation generated by <a href="https://github.com/jsdoc/jsdoc">JSDoc 3.6.10</a> on Fri Dec 29 2023 23:22:35 GMT-0800 (Pacific Standard Time)
</footer>
<script> prettyPrint(); </script>
<script src="scripts/linenumber.js"> </script>
</body>
</html>

View File

@ -0,0 +1,209 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="utf-8">
<title>JSDoc: Source: core/changecolor.js</title>
<script src="scripts/prettify/prettify.js"> </script>
<script src="scripts/prettify/lang-css.js"> </script>
<!--[if lt IE 9]>
<script src="//html5shiv.googlecode.com/svn/trunk/html5.js"></script>
<![endif]-->
<link type="text/css" rel="stylesheet" href="styles/prettify-tomorrow.css">
<link type="text/css" rel="stylesheet" href="styles/jsdoc-default.css">
</head>
<body>
<div id="main">
<h1 class="page-title">Source: core/changecolor.js</h1>
<section>
<article>
<pre class="prettyprint source linenums"><code>/**
* @author Marzavec ( https://github.com/marzavec )
* @summary Update name color
* @version 1.0.0
* @description Allows calling client to change their nickname color
* @module changecolor
*/
import {
getUserDetails,
} from '../utility/_UAC.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 - Enviroment object with references to core, server, socket &amp; payload
* @public
* @return {void}
*/
export async function run({
server, socket, payload,
}) {
const { channel } = socket;
if (server.police.frisk(socket, 1)) {
return server.reply({
cmd: 'warn', // @todo Add numeric error code as `id`
text: 'You are changing colors too fast. Wait a moment before trying again.',
channel, // @todo Multichannel
}, socket);
}
// verify user data is string
if (typeof payload.color !== 'string') {
return false;
}
// make sure requested nickname meets standards
const newColor = payload.color.trim().toUpperCase().replace(/#/g, '');
if (newColor !== 'RESET' &amp;&amp; !verifyColor(newColor)) {
return server.reply({
cmd: 'warn',
text: 'Invalid color! Color must be in hex value',
channel, // @todo Multichannel
}, socket);
}
if (newColor === 'RESET') {
socket.color = false; // eslint-disable-line no-param-reassign
} else {
socket.color = newColor; // eslint-disable-line no-param-reassign
}
// build update notice with new color
const updateNotice = {
...getUserDetails(socket),
...{
cmd: 'updateUser',
channel: socket.channel, // @todo Multichannel
},
};
// notify channel that the user has changed their name
// @todo this should be sent to every channel the user is in (multichannel)
server.broadcast(updateNotice, { channel: socket.channel });
return true;
}
/**
* Automatically executes once after server is ready to register this modules hooks
* @param {Object} server - Reference to server enviroment object
* @public
* @return {void}
*/
export function initHooks(server) {
server.registerHook('in', 'chat', this.colorCheck.bind(this), 29);
}
/**
* Executes every time an incoming chat command is invoked
* @param {Object} env - Enviroment object with references to core, server, socket &amp; payload
* @public
* @return {(Object|boolean|string)} Object = same/altered payload,
* false = suppress action,
* string = error
*/
export function colorCheck({
core, server, socket, payload,
}) {
if (typeof payload.text !== 'string') {
return false;
}
if (payload.text.startsWith('/color ')) {
const input = payload.text.split(' ');
// If there is no color target parameter
if (input[1] === undefined) {
server.reply({
cmd: 'warn',
text: 'Refer to `/help changecolor` for instructions on how to use this command.',
channel: socket.channel, // @todo Multichannel
}, socket);
return false;
}
this.run({
core,
server,
socket,
payload: {
cmd: 'changecolor',
color: input[1],
},
});
return false;
}
return payload;
}
/**
* The following payload properties are required to invoke this module:
* "color"
* @public
* @typedef {Array} changecolor/requiredData
*/
export const requiredData = ['color'];
/**
* Module meta information
* @public
* @typedef {Object} changecolor/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: 'changecolor',
category: 'core',
description: 'Allows calling client to change their nickname color',
usage: `
API: { cmd: 'changecolor', color: '&lt;new color as hex>' }
Text: /color &lt;new color as hex>
Removal: /color reset`,
};
</code></pre>
</article>
</section>
</div>
<nav>
<h2><a href="index.html">Home</a></h2><h3>Modules</h3><ul><li><a href="module-addmod.html">addmod</a></li><li><a href="module-ban.html">ban</a></li><li><a href="module-changecolor.html">changecolor</a></li><li><a href="module-changenick.html">changenick</a></li><li><a href="module-chat.html">chat</a></li><li><a href="module-disablecaptcha.html">disablecaptcha</a></li><li><a href="module-disconnect.html">disconnect</a></li><li><a href="module-dumb.html">dumb</a></li><li><a href="module-emote.html">emote</a></li><li><a href="module-enablecaptcha.html">enablecaptcha</a></li><li><a href="module-forcecolor.html">forcecolor</a></li><li><a href="module-help.html">help</a></li><li><a href="module-invite.html">invite</a></li><li><a href="module-join.html">join</a></li><li><a href="module-kick.html">kick</a></li><li><a href="module-listusers.html">listusers</a></li><li><a href="module-lockroom.html">lockroom</a></li><li><a href="module-morestats.html">morestats</a></li><li><a href="module-ping.html">ping</a></li><li><a href="module-reload.html">reload</a></li><li><a href="module-removemod.html">removemod</a></li><li><a href="module-saveconfig.html">saveconfig</a></li><li><a href="module-session.html">session</a></li><li><a href="module-shout.html">shout</a></li><li><a href="module-socketreply.html">socketreply</a></li><li><a href="module-speak.html">speak</a></li><li><a href="module-stats.html">stats</a></li><li><a href="module-unban.html">unban</a></li><li><a href="module-unbanall.html">unbanall</a></li><li><a href="module-unlockroom.html">unlockroom</a></li><li><a href="module-updateMessage.html">updateMessage</a></li><li><a href="module-whisper.html">whisper</a></li></ul>
</nav>
<br class="clear">
<footer>
Documentation generated by <a href="https://github.com/jsdoc/jsdoc">JSDoc 3.6.10</a> on Fri Dec 29 2023 23:22:35 GMT-0800 (Pacific Standard Time)
</footer>
<script> prettyPrint(); </script>
<script src="scripts/linenumber.js"> </script>
</body>
</html>

View File

@ -0,0 +1,264 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="utf-8">
<title>JSDoc: Source: core/changenick.js</title>
<script src="scripts/prettify/prettify.js"> </script>
<script src="scripts/prettify/lang-css.js"> </script>
<!--[if lt IE 9]>
<script src="//html5shiv.googlecode.com/svn/trunk/html5.js"></script>
<![endif]-->
<link type="text/css" rel="stylesheet" href="styles/prettify-tomorrow.css">
<link type="text/css" rel="stylesheet" href="styles/jsdoc-default.css">
</head>
<body>
<div id="main">
<h1 class="page-title">Source: core/changenick.js</h1>
<section>
<article>
<pre class="prettyprint source linenums"><code>/* eslint eqeqeq: 0 */
/**
* @author Marzavec ( https://github.com/marzavec )
* @summary Update nickname
* @version 1.0.0
* @description Allows calling client to change their current nickname
* @module changenick
*/
import {
verifyNickname,
getUserDetails,
} from '../utility/_UAC.js';
/**
* Executes when invoked by a remote client
* @param {Object} env - Enviroment object with references to core, server, socket &amp; payload
* @public
* @return {void}
*/
export async function run({
server, socket, payload,
}) {
const { channel } = socket;
if (server.police.frisk(socket, 6)) {
return server.reply({
cmd: 'warn', // @todo Add numeric error code as `id`
text: 'You are changing nicknames too fast. Wait a moment before trying again.',
channel, // @todo Multichannel
}, socket);
}
// verify user data is string
if (typeof payload.nick !== 'string') {
return true;
}
const previousNick = socket.nick;
// make sure requested nickname meets standards
const newNick = payload.nick.trim();
if (!verifyNickname(newNick)) {
return server.reply({
cmd: 'warn', // @todo Add numeric error code as `id`
text: 'Nickname must consist of up to 24 letters, numbers, and underscores',
channel, // @todo Multichannel
}, socket);
}
if (newNick == previousNick) {
return server.reply({
cmd: 'warn', // @todo Add numeric error code as `id`
text: 'You already have that name',
channel, // @todo Multichannel
}, socket);
}
// find any sockets that have the same nickname
const userExists = server.findSockets({
channel,
nick: (targetNick) => targetNick.toLowerCase() === newNick.toLowerCase()
// Allow them to rename themselves to a different case
&amp;&amp; targetNick != previousNick,
});
// return error if found
if (userExists.length > 0) {
// That nickname is already in that channel
return server.reply({
cmd: 'warn', // @todo Add numeric error code as `id`
text: 'Nickname taken',
channel, // @todo Multichannel
}, socket);
}
// build update notice with new nickname
const updateNotice = {
...getUserDetails(socket),
...{
cmd: 'updateUser',
nick: newNick,
channel, // @todo Multichannel
},
};
// build join and leave notices for legacy clients
const leaveNotice = {
cmd: 'onlineRemove',
userid: socket.userid,
nick: socket.nick,
channel, // @todo Multichannel
};
const joinNotice = {
...getUserDetails(socket),
...{
cmd: 'onlineAdd',
nick: newNick,
channel, // @todo Multichannel
},
};
// gather channel peers
const peerList = server.findSockets({ channel });
for (let i = 0, l = peerList.length; i &lt; l; i += 1) {
if (peerList[i].hcProtocol === 1) {
// send join/leave to legacy clients
server.send(leaveNotice, peerList[i]);
server.send(joinNotice, peerList[i]);
} else {
// send update info
// @todo this should be sent to every channel the client is in (multichannel)
server.send(updateNotice, peerList[i]);
}
}
// notify channel that the user has changed their name
server.broadcast({
cmd: 'info',
text: `${socket.nick} is now ${newNick}`,
channel, // @todo Multichannel
}, { channel });
// commit change to nickname
socket.nick = newNick; // eslint-disable-line no-param-reassign
return true;
}
/**
* Automatically executes once after server is ready to register this modules hooks
* @param {Object} server - Reference to server enviroment object
* @public
* @return {void}
*/
export function initHooks(server) {
server.registerHook('in', 'chat', this.nickCheck.bind(this), 29);
}
/**
* Executes every time an incoming chat command is invoked
* @param {Object} env - Enviroment object with references to core, server, socket &amp; payload
* @public
* @return {(Object|boolean|string)} Object = same/altered payload,
* false = suppress action,
* string = error
*/
export function nickCheck({
core, server, socket, payload,
}) {
if (typeof payload.text !== 'string') {
return false;
}
if (payload.text.startsWith('/nick')) {
const input = payload.text.split(' ');
// If there is no nickname target parameter
if (!input[1]) {
server.reply({
cmd: 'warn', // @todo Add numeric error code as `id`
text: 'Refer to `/help nick` for instructions on how to use this command.',
channel: socket.channel, // @todo Multichannel
}, socket);
return false;
}
const newNick = input[1].replace(/@/g, '');
this.run({
core,
server,
socket,
payload: {
cmd: 'changenick',
nick: newNick,
},
});
return false;
}
return payload;
}
/**
* The following payload properties are required to invoke this module:
* "nick"
* @public
* @typedef {Array} changenick/requiredData
*/
export const requiredData = ['nick'];
/**
* Module meta information
* @public
* @typedef {Object} changenick/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: 'changenick',
category: 'core',
description: 'Allows calling client to change their current nickname',
usage: `
API: { cmd: 'changenick', nick: '&lt;new nickname>' }
Text: /nick &lt;new nickname>`,
};
</code></pre>
</article>
</section>
</div>
<nav>
<h2><a href="index.html">Home</a></h2><h3>Modules</h3><ul><li><a href="module-addmod.html">addmod</a></li><li><a href="module-ban.html">ban</a></li><li><a href="module-changecolor.html">changecolor</a></li><li><a href="module-changenick.html">changenick</a></li><li><a href="module-chat.html">chat</a></li><li><a href="module-disablecaptcha.html">disablecaptcha</a></li><li><a href="module-disconnect.html">disconnect</a></li><li><a href="module-dumb.html">dumb</a></li><li><a href="module-emote.html">emote</a></li><li><a href="module-enablecaptcha.html">enablecaptcha</a></li><li><a href="module-forcecolor.html">forcecolor</a></li><li><a href="module-help.html">help</a></li><li><a href="module-invite.html">invite</a></li><li><a href="module-join.html">join</a></li><li><a href="module-kick.html">kick</a></li><li><a href="module-listusers.html">listusers</a></li><li><a href="module-lockroom.html">lockroom</a></li><li><a href="module-morestats.html">morestats</a></li><li><a href="module-ping.html">ping</a></li><li><a href="module-reload.html">reload</a></li><li><a href="module-removemod.html">removemod</a></li><li><a href="module-saveconfig.html">saveconfig</a></li><li><a href="module-session.html">session</a></li><li><a href="module-shout.html">shout</a></li><li><a href="module-socketreply.html">socketreply</a></li><li><a href="module-speak.html">speak</a></li><li><a href="module-stats.html">stats</a></li><li><a href="module-unban.html">unban</a></li><li><a href="module-unbanall.html">unbanall</a></li><li><a href="module-unlockroom.html">unlockroom</a></li><li><a href="module-updateMessage.html">updateMessage</a></li><li><a href="module-whisper.html">whisper</a></li></ul>
</nav>
<br class="clear">
<footer>
Documentation generated by <a href="https://github.com/jsdoc/jsdoc">JSDoc 3.6.10</a> on Fri Dec 29 2023 23:22:35 GMT-0800 (Pacific Standard Time)
</footer>
<script> prettyPrint(); </script>
<script src="scripts/linenumber.js"> </script>
</body>
</html>

View File

@ -0,0 +1,296 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="utf-8">
<title>JSDoc: Source: core/chat.js</title>
<script src="scripts/prettify/prettify.js"> </script>
<script src="scripts/prettify/lang-css.js"> </script>
<!--[if lt IE 9]>
<script src="//html5shiv.googlecode.com/svn/trunk/html5.js"></script>
<![endif]-->
<link type="text/css" rel="stylesheet" href="styles/prettify-tomorrow.css">
<link type="text/css" rel="stylesheet" href="styles/jsdoc-default.css">
</head>
<body>
<div id="main">
<h1 class="page-title">Source: core/chat.js</h1>
<section>
<article>
<pre class="prettyprint source linenums"><code>/**
* @author Marzavec ( https://github.com/marzavec )
* @summary Send chat messages
* @version 1.0.0
* @description Broadcasts passed `text` field to the calling users channel
* @module chat
*/
import {
parseText,
} from '../utility/_Text.js';
import {
isAdmin,
isModerator,
} from '../utility/_UAC.js';
/**
* Maximum length of the customId property
* @type {number}
*/
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.
* @type {number}
*/
const ACTIVE_TIMEOUT = 5 * 60 * 1000;
/**
* 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.
* @type {Array}
*/
export const ACTIVE_MESSAGES = [];
/**
* Cleans up stale messages.
* @public
* @return {void}
*/
export function cleanActiveMessages() {
const now = Date.now();
for (let i = 0; i &lt; ACTIVE_MESSAGES.length; 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);
/**
* Adds a message to the active messages map.
* @public
* @param {string} id
* @param {number} userid
* @return {void}
*/
export function addActiveMessage(customId, userid) {
ACTIVE_MESSAGES.push({
customId,
userid,
sent: Date.now(),
toDelete: false,
});
}
/**
* Executes when invoked by a remote client
* @param {Object} env - Enviroment object with references to core, server, socket &amp; payload
* @public
* @return {void}
*/
export async function run({
core, server, socket, payload,
}) {
// check user input
const text = parseText(payload.text);
if (!text) {
// lets not send objects or empty text, yea?
return server.police.frisk(socket, 13);
}
// check for spam
const score = text.length / 83 / 4;
if (server.police.frisk(socket, score)) {
return server.reply({
cmd: 'warn', // @todo Add numeric error code as `id`
text: 'You are sending too much text. Wait a moment and try again.\nPress the up arrow key to restore your last message.',
channel: socket.channel, // @todo Multichannel
}, socket);
}
const { customId } = payload;
if (typeof (customId) === 'string' &amp;&amp; customId.length > MAX_MESSAGE_ID_LENGTH) {
// There's a limit on the custom id length.
return server.police.frisk(socket, 13);
}
// build chat payload
const outgoingPayload = {
cmd: 'chat',
nick: socket.nick, /* @legacy */
uType: socket.uType, /* @legacy */
userid: socket.userid,
channel: socket.channel,
text,
level: socket.level,
customId,
};
if (isAdmin(socket.level)) {
outgoingPayload.admin = true;
} else if (isModerator(socket.level)) {
outgoingPayload.mod = true;
}
if (socket.trip) {
outgoingPayload.trip = socket.trip; /* @legacy */
}
if (socket.color) {
outgoingPayload.color = socket.color;
}
addActiveMessage(outgoingPayload.customId, socket.userid);
// broadcast to channel peers
server.broadcast(outgoingPayload, { channel: socket.channel });
// stats are fun
core.stats.increment('messages-sent');
return true;
}
/**
* Automatically executes once after server is ready to register this modules hooks
* @param {Object} server - Reference to server enviroment object
* @public
* @return {void}
*/
export function initHooks(server) {
server.registerHook('in', 'chat', this.commandCheckIn.bind(this), 20);
server.registerHook('in', 'chat', this.finalCmdCheck.bind(this), 254);
}
/**
* Executes every time an incoming chat command is invoked;
* checks for miscellaneous '/' based commands
* @param {Object} env - Enviroment object with references to core, server, socket &amp; payload
* @public
* @return {(Object|boolean|string)} Object = same/altered payload,
* false = suppress action,
* string = error
*/
export function commandCheckIn({ server, socket, payload }) {
if (typeof payload.text !== 'string') {
return false;
}
if (payload.text.startsWith('/myhash')) {
server.reply({
cmd: 'info',
text: `Your hash: ${socket.hash}`,
channel: socket.channel, // @todo Multichannel
}, socket);
return false;
}
return payload;
}
/**
* Executes every time an incoming chat command is invoked;
* assumes a failed chat command invocation and will reject with notice
* @param {Object} env - Enviroment object with references to core, server, socket &amp; payload
* @public
* @return {(Object|boolean|string)} Object = same/altered payload,
* false = suppress action,
* string = error
*/
export function finalCmdCheck({ server, socket, payload }) {
if (typeof payload.text !== 'string') {
return false;
}
if (!payload.text.startsWith('/')) {
return payload;
}
if (payload.text.startsWith('//')) {
payload.text = payload.text.substr(1); // eslint-disable-line no-param-reassign
return payload;
}
server.reply({
cmd: 'warn', // @todo Add numeric error code as `id`
text: `Unknown command: ${payload.text}`,
channel: socket.channel, // @todo Multichannel
}, socket);
return false;
}
/**
* The following payload properties are required to invoke this module:
* "text"
* @public
* @typedef {Array} chat/requiredData
*/
export const requiredData = ['text'];
/**
* Module meta information
* @public
* @typedef {Object} chat/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: 'chat',
category: 'core',
description: 'Broadcasts passed `text` field to the calling users channel',
usage: `
API: { cmd: 'chat', text: '&lt;text to send>' }
Text: Uuuuhm. Just kind type in that little box at the bottom and hit enter.\n
Bonus super secret hidden commands:
/myhash`,
};
</code></pre>
</article>
</section>
</div>
<nav>
<h2><a href="index.html">Home</a></h2><h3>Modules</h3><ul><li><a href="module-addmod.html">addmod</a></li><li><a href="module-ban.html">ban</a></li><li><a href="module-changecolor.html">changecolor</a></li><li><a href="module-changenick.html">changenick</a></li><li><a href="module-chat.html">chat</a></li><li><a href="module-disablecaptcha.html">disablecaptcha</a></li><li><a href="module-disconnect.html">disconnect</a></li><li><a href="module-dumb.html">dumb</a></li><li><a href="module-emote.html">emote</a></li><li><a href="module-enablecaptcha.html">enablecaptcha</a></li><li><a href="module-forcecolor.html">forcecolor</a></li><li><a href="module-help.html">help</a></li><li><a href="module-invite.html">invite</a></li><li><a href="module-join.html">join</a></li><li><a href="module-kick.html">kick</a></li><li><a href="module-listusers.html">listusers</a></li><li><a href="module-lockroom.html">lockroom</a></li><li><a href="module-morestats.html">morestats</a></li><li><a href="module-ping.html">ping</a></li><li><a href="module-reload.html">reload</a></li><li><a href="module-removemod.html">removemod</a></li><li><a href="module-saveconfig.html">saveconfig</a></li><li><a href="module-session.html">session</a></li><li><a href="module-shout.html">shout</a></li><li><a href="module-socketreply.html">socketreply</a></li><li><a href="module-speak.html">speak</a></li><li><a href="module-stats.html">stats</a></li><li><a href="module-unban.html">unban</a></li><li><a href="module-unbanall.html">unbanall</a></li><li><a href="module-unlockroom.html">unlockroom</a></li><li><a href="module-updateMessage.html">updateMessage</a></li><li><a href="module-whisper.html">whisper</a></li></ul>
</nav>
<br class="clear">
<footer>
Documentation generated by <a href="https://github.com/jsdoc/jsdoc">JSDoc 3.6.10</a> on Fri Dec 29 2023 23:22:35 GMT-0800 (Pacific Standard Time)
</footer>
<script> prettyPrint(); </script>
<script src="scripts/linenumber.js"> </script>
</body>
</html>

View File

@ -0,0 +1,213 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="utf-8">
<title>JSDoc: Source: core/emote.js</title>
<script src="scripts/prettify/prettify.js"> </script>
<script src="scripts/prettify/lang-css.js"> </script>
<!--[if lt IE 9]>
<script src="//html5shiv.googlecode.com/svn/trunk/html5.js"></script>
<![endif]-->
<link type="text/css" rel="stylesheet" href="styles/prettify-tomorrow.css">
<link type="text/css" rel="stylesheet" href="styles/jsdoc-default.css">
</head>
<body>
<div id="main">
<h1 class="page-title">Source: core/emote.js</h1>
<section>
<article>
<pre class="prettyprint source linenums"><code>/**
* @author Marzavec ( https://github.com/marzavec )
* @summary Emote / action text
* @version 1.0.0
* @description Broadcasts an emote to the current channel
* @module emote
*/
/**
* Check and trim string provided by remote client
* @param {string} text - Subject string
* @private
* @todo Move into utility module
* @return {string|boolean}
*/
const parseText = (text) => {
// verifies user input is text
if (typeof text !== 'string') {
return false;
}
let sanitizedText = text;
// strip newlines from beginning and end
sanitizedText = sanitizedText.replace(/^\s*\n|^\s+$|\n\s*$/g, '');
// replace 3+ newlines with just 2 newlines
sanitizedText = sanitizedText.replace(/\n{3,}/g, '\n\n');
return sanitizedText;
};
/**
* Executes when invoked by a remote client
* @param {Object} env - Enviroment object with references to core, server, socket &amp; payload
* @public
* @return {void}
*/
export async function run({ server, socket, payload }) {
// check user input
let text = parseText(payload.text);
if (!text) {
// lets not send objects or empty text, yea?
return server.police.frisk(socket, 8);
}
// check for spam
const score = text.length / 83 / 4;
if (server.police.frisk(socket, score)) {
return server.reply({
cmd: 'warn', // @todo Add numeric error code as `id`
text: 'You are sending too much text. Wait a moment and try again.\nPress the up arrow key to restore your last message.',
channel: socket.channel, // @todo Multichannel
}, socket);
}
if (!text.startsWith("'")) {
text = ` ${text}`;
}
const newPayload = {
cmd: 'emote',
nick: socket.nick,
userid: socket.userid,
text: `@${socket.nick}${text}`,
channel: socket.channel, // @todo Multichannel
};
if (socket.trip) {
newPayload.trip = socket.trip;
}
// broadcast to channel peers
server.broadcast(newPayload, { channel: socket.channel });
return true;
}
/**
* Automatically executes once after server is ready to register this modules hooks
* @param {Object} server - Reference to server enviroment object
* @public
* @return {void}
*/
export function initHooks(server) {
server.registerHook('in', 'chat', this.emoteCheck.bind(this), 30);
}
/**
* Executes every time an incoming chat command is invoked;
* hooks chat commands checking for /me
* @param {Object} env - Enviroment object with references to core, server, socket &amp; payload
* @public
* @return {(Object|boolean|string)} Object = same/altered payload,
* false = suppress action,
* string = error
*/
export function emoteCheck({
core, server, socket, payload,
}) {
if (typeof payload.text !== 'string') {
return false;
}
if (payload.text.startsWith('/me ')) {
const input = payload.text.split(' ');
// If there is no emote target parameter
if (input[1] === undefined) {
server.reply({
cmd: 'warn', // @todo Add numeric error code as `id`
text: 'Refer to `/help emote` for instructions on how to use this command.',
channel: socket.channel, // @todo Multichannel
}, socket);
return false;
}
input.splice(0, 1);
const actionText = input.join(' ');
this.run({
core,
server,
socket,
payload: {
cmd: 'emote',
text: actionText,
},
});
return false;
}
return payload;
}
/**
* The following payload properties are required to invoke this module:
* "text"
* @public
* @typedef {Array} emote/requiredData
*/
export const requiredData = ['text'];
/**
* Module meta information
* @public
* @typedef {Object} emote/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: 'emote',
category: 'core',
description: 'Broadcasts an emote to the current channel',
usage: `
API: { cmd: 'emote', text: '&lt;emote/action text>' }
Text: /me &lt;emote/action text>`,
};
</code></pre>
</article>
</section>
</div>
<nav>
<h2><a href="index.html">Home</a></h2><h3>Modules</h3><ul><li><a href="module-addmod.html">addmod</a></li><li><a href="module-ban.html">ban</a></li><li><a href="module-changecolor.html">changecolor</a></li><li><a href="module-changenick.html">changenick</a></li><li><a href="module-chat.html">chat</a></li><li><a href="module-disablecaptcha.html">disablecaptcha</a></li><li><a href="module-disconnect.html">disconnect</a></li><li><a href="module-dumb.html">dumb</a></li><li><a href="module-emote.html">emote</a></li><li><a href="module-enablecaptcha.html">enablecaptcha</a></li><li><a href="module-forcecolor.html">forcecolor</a></li><li><a href="module-help.html">help</a></li><li><a href="module-invite.html">invite</a></li><li><a href="module-join.html">join</a></li><li><a href="module-kick.html">kick</a></li><li><a href="module-listusers.html">listusers</a></li><li><a href="module-lockroom.html">lockroom</a></li><li><a href="module-morestats.html">morestats</a></li><li><a href="module-ping.html">ping</a></li><li><a href="module-reload.html">reload</a></li><li><a href="module-removemod.html">removemod</a></li><li><a href="module-saveconfig.html">saveconfig</a></li><li><a href="module-session.html">session</a></li><li><a href="module-shout.html">shout</a></li><li><a href="module-socketreply.html">socketreply</a></li><li><a href="module-speak.html">speak</a></li><li><a href="module-stats.html">stats</a></li><li><a href="module-unban.html">unban</a></li><li><a href="module-unbanall.html">unbanall</a></li><li><a href="module-unlockroom.html">unlockroom</a></li><li><a href="module-updateMessage.html">updateMessage</a></li><li><a href="module-whisper.html">whisper</a></li></ul>
</nav>
<br class="clear">
<footer>
Documentation generated by <a href="https://github.com/jsdoc/jsdoc">JSDoc 3.6.10</a> on Fri Dec 29 2023 23:22:35 GMT-0800 (Pacific Standard Time)
</footer>
<script> prettyPrint(); </script>
<script src="scripts/linenumber.js"> </script>
</body>
</html>

View File

@ -0,0 +1,186 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="utf-8">
<title>JSDoc: Source: core/help.js</title>
<script src="scripts/prettify/prettify.js"> </script>
<script src="scripts/prettify/lang-css.js"> </script>
<!--[if lt IE 9]>
<script src="//html5shiv.googlecode.com/svn/trunk/html5.js"></script>
<![endif]-->
<link type="text/css" rel="stylesheet" href="styles/prettify-tomorrow.css">
<link type="text/css" rel="stylesheet" href="styles/jsdoc-default.css">
</head>
<body>
<div id="main">
<h1 class="page-title">Source: core/help.js</h1>
<section>
<article>
<pre class="prettyprint source linenums"><code>/**
* @author Marzavec ( https://github.com/marzavec )
* @summary Get help
* @version 1.0.0
* @description Outputs information about the servers current protocol
* @module help
*/
/**
* Executes when invoked by a remote client
* @param {Object} env - Enviroment object with references to core, server, socket &amp; payload
* @public
* @return {void}
*/
export async function run({
core, server, socket, payload,
}) {
// check for spam
if (server.police.frisk(socket, 2)) {
return server.reply({
cmd: 'warn', // @todo Add numeric error code as `id`
text: 'You are sending too much text. Wait a moment and try again.\nPress the up arrow key to restore your last message.',
channel: socket.channel, // @todo Multichannel
}, socket);
}
// verify user input
if (typeof payload.command !== 'undefined' &amp;&amp; typeof payload.command !== 'string') {
return true;
}
let reply = '';
if (typeof payload.command === 'undefined') {
reply += '# All commands:\n|Category:|Name:|\n|---:|---|\n';
const categories = core.commands.categoriesList.sort();
for (let i = 0, j = categories.length; i &lt; j; i += 1) {
reply += `|${categories[i].replace('../src/commands/', '').replace(/^\w/, (c) => c.toUpperCase())}:|`;
const catCommands = core.commands.all(categories[i]).sort(
(a, b) => a.info.name.localeCompare(b.info.name),
);
reply += `${catCommands.map((c) => `${c.info.name}`).join(', ')}|\n`;
}
reply += '---\nFor specific help on certain commands, use either:\nText: `/help &lt;command name>`\nAPI: `{cmd: \'help\', command: \'&lt;command name>\'}`';
} else {
const command = core.commands.get(payload.command);
if (typeof command === 'undefined') {
reply += 'Unknown command';
} else {
reply += `# ${command.info.name} command:\n| | |\n|---:|---|\n`;
reply += `|**Name:**|${command.info.name}|\n`;
reply += `|**Hash:**|${command.info.srcHash}|\n`;
reply += `|**Aliases:**|${typeof command.info.aliases !== 'undefined' ? command.info.aliases.join(', ') : 'None'}|\n`;
reply += `|**Category:**|${command.info.category.replace('../src/commands/', '').replace(/^\w/, (c) => c.toUpperCase())}|\n`;
reply += `|**Required Parameters:**|${command.requiredData || 'None'}|\n`;
// eslint-disable-next-line no-useless-escape
reply += `|**Description:**|${command.info.description || '¯\_(ツ)_/¯'}|\n\n`;
reply += `**Usage:** ${command.info.usage || command.info.name}`;
}
}
// output reply
server.reply({
cmd: 'info',
text: reply,
channel: socket.channel, // @todo Multichannel
}, socket);
return true;
}
/**
* Automatically executes once after server is ready to register this modules hooks
* @param {Object} server - Reference to server enviroment object
* @public
* @return {void}
*/
export function initHooks(server) {
server.registerHook('in', 'chat', this.helpCheck.bind(this), 28);
}
/**
* Executes every time an incoming chat command is invoked;
* hooks chat commands checking for /help
* @param {Object} env - Enviroment object with references to core, server, socket &amp; payload
* @public
* @return {(Object|boolean|string)} Object = same/altered payload,
* false = suppress action,
* string = error
*/
export function helpCheck({
core, server, socket, payload,
}) {
if (typeof payload.text !== 'string') {
return false;
}
if (payload.text.startsWith('/help')) {
const input = payload.text.substr(1).split(' ', 2);
this.run({
core,
server,
socket,
payload: {
cmd: input[0],
command: input[1],
},
});
return false;
}
return payload;
}
/**
* Module meta information
* @public
* @typedef {Object} help/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: 'help',
category: 'core',
description: 'Outputs information about the servers current protocol',
usage: `
API: { cmd: 'help', command: '&lt;optional command name>' }
Text: /help &lt;optional command name>`,
};
</code></pre>
</article>
</section>
</div>
<nav>
<h2><a href="index.html">Home</a></h2><h3>Modules</h3><ul><li><a href="module-addmod.html">addmod</a></li><li><a href="module-ban.html">ban</a></li><li><a href="module-changecolor.html">changecolor</a></li><li><a href="module-changenick.html">changenick</a></li><li><a href="module-chat.html">chat</a></li><li><a href="module-disablecaptcha.html">disablecaptcha</a></li><li><a href="module-disconnect.html">disconnect</a></li><li><a href="module-dumb.html">dumb</a></li><li><a href="module-emote.html">emote</a></li><li><a href="module-enablecaptcha.html">enablecaptcha</a></li><li><a href="module-forcecolor.html">forcecolor</a></li><li><a href="module-help.html">help</a></li><li><a href="module-invite.html">invite</a></li><li><a href="module-join.html">join</a></li><li><a href="module-kick.html">kick</a></li><li><a href="module-listusers.html">listusers</a></li><li><a href="module-lockroom.html">lockroom</a></li><li><a href="module-morestats.html">morestats</a></li><li><a href="module-ping.html">ping</a></li><li><a href="module-reload.html">reload</a></li><li><a href="module-removemod.html">removemod</a></li><li><a href="module-saveconfig.html">saveconfig</a></li><li><a href="module-session.html">session</a></li><li><a href="module-shout.html">shout</a></li><li><a href="module-socketreply.html">socketreply</a></li><li><a href="module-speak.html">speak</a></li><li><a href="module-stats.html">stats</a></li><li><a href="module-unban.html">unban</a></li><li><a href="module-unbanall.html">unbanall</a></li><li><a href="module-unlockroom.html">unlockroom</a></li><li><a href="module-updateMessage.html">updateMessage</a></li><li><a href="module-whisper.html">whisper</a></li></ul>
</nav>
<br class="clear">
<footer>
Documentation generated by <a href="https://github.com/jsdoc/jsdoc">JSDoc 3.6.10</a> on Fri Dec 29 2023 23:22:35 GMT-0800 (Pacific Standard Time)
</footer>
<script> prettyPrint(); </script>
<script src="scripts/linenumber.js"> </script>
</body>
</html>

View File

@ -0,0 +1,174 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="utf-8">
<title>JSDoc: Source: core/invite.js</title>
<script src="scripts/prettify/prettify.js"> </script>
<script src="scripts/prettify/lang-css.js"> </script>
<!--[if lt IE 9]>
<script src="//html5shiv.googlecode.com/svn/trunk/html5.js"></script>
<![endif]-->
<link type="text/css" rel="stylesheet" href="styles/prettify-tomorrow.css">
<link type="text/css" rel="stylesheet" href="styles/jsdoc-default.css">
</head>
<body>
<div id="main">
<h1 class="page-title">Source: core/invite.js</h1>
<section>
<article>
<pre class="prettyprint source linenums"><code>/**
* @author Marzavec ( https://github.com/marzavec )
* @summary Send an invite
* @version 1.0.0
* @description Sends an invite to the target client with the provided channel, or a random channel
* @module invite
*/
import {
findUser,
} from '../utility/_Channels.js';
import {
Errors,
} from '../utility/_Constants.js';
import {
legacyInviteOut,
legacyInviteReply,
} from '../utility/_LegacyFunctions.js';
/**
* Returns the channel that should be invited to.
* @param {any} channel
* @private
* @return {string}
*/
export function getChannel(channel = undefined) {
if (typeof channel === 'string') {
return channel;
}
return Math.random().toString(36).substr(2, 8);
}
/**
* Executes when invoked by a remote client
* @param {Object} env - Enviroment object with references to core, server, socket &amp; payload
* @public
* @return {void}
*/
export async function run({
core, server, socket, payload,
}) {
// check for spam
if (server.police.frisk(socket, 2)) {
return server.reply({
cmd: 'warn',
text: 'You are sending invites too fast. Wait a moment before trying again.',
id: Errors.Invite.RATELIMIT,
channel: socket.channel, // @todo Multichannel
}, socket);
}
// verify user input
// if this is a legacy client add missing params to payload
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') {
return true;
}
// @todo Verify this socket is part of payload.channel - multichannel patch
// find target user
const targetUser = findUser(server, payload);
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);
}
// generate common channel
const channel = getChannel(payload.to);
// build invite
const outgoingPayload = {
cmd: 'invite',
channel: socket.channel, // @todo Multichannel
from: socket.userid,
to: targetUser.userid,
inviteChannel: channel,
};
// send invite notice to target client
if (targetUser.hcProtocol === 1) {
server.reply(legacyInviteOut(outgoingPayload, socket.nick), targetUser);
} else {
server.reply(outgoingPayload, targetUser);
}
// send invite notice to this client
if (socket.hcProtocol === 1) {
server.reply(legacyInviteReply(outgoingPayload, targetUser.nick), socket);
} else {
server.reply(outgoingPayload, socket);
}
// stats are fun
core.stats.increment('invites-sent');
return true;
}
/**
* Module meta information
* @public
* @typedef {Object} invite/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: 'invite',
category: 'core',
description: 'Sends an invite to the target client with the provided channel, or a random channel.',
usage: `
API: { cmd: 'invite', nick: '&lt;target nickname>', to: '&lt;optional destination channel>' }`,
};
</code></pre>
</article>
</section>
</div>
<nav>
<h2><a href="index.html">Home</a></h2><h3>Modules</h3><ul><li><a href="module-addmod.html">addmod</a></li><li><a href="module-ban.html">ban</a></li><li><a href="module-changecolor.html">changecolor</a></li><li><a href="module-changenick.html">changenick</a></li><li><a href="module-chat.html">chat</a></li><li><a href="module-disablecaptcha.html">disablecaptcha</a></li><li><a href="module-disconnect.html">disconnect</a></li><li><a href="module-dumb.html">dumb</a></li><li><a href="module-emote.html">emote</a></li><li><a href="module-enablecaptcha.html">enablecaptcha</a></li><li><a href="module-forcecolor.html">forcecolor</a></li><li><a href="module-help.html">help</a></li><li><a href="module-invite.html">invite</a></li><li><a href="module-join.html">join</a></li><li><a href="module-kick.html">kick</a></li><li><a href="module-listusers.html">listusers</a></li><li><a href="module-lockroom.html">lockroom</a></li><li><a href="module-morestats.html">morestats</a></li><li><a href="module-ping.html">ping</a></li><li><a href="module-reload.html">reload</a></li><li><a href="module-removemod.html">removemod</a></li><li><a href="module-saveconfig.html">saveconfig</a></li><li><a href="module-session.html">session</a></li><li><a href="module-shout.html">shout</a></li><li><a href="module-socketreply.html">socketreply</a></li><li><a href="module-speak.html">speak</a></li><li><a href="module-stats.html">stats</a></li><li><a href="module-unban.html">unban</a></li><li><a href="module-unbanall.html">unbanall</a></li><li><a href="module-unlockroom.html">unlockroom</a></li><li><a href="module-updateMessage.html">updateMessage</a></li><li><a href="module-whisper.html">whisper</a></li></ul>
</nav>
<br class="clear">
<footer>
Documentation generated by <a href="https://github.com/jsdoc/jsdoc">JSDoc 3.6.10</a> on Fri Dec 29 2023 23:22:35 GMT-0800 (Pacific Standard Time)
</footer>
<script> prettyPrint(); </script>
<script src="scripts/linenumber.js"> </script>
</body>
</html>

View File

@ -0,0 +1,329 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="utf-8">
<title>JSDoc: Source: core/join.js</title>
<script src="scripts/prettify/prettify.js"> </script>
<script src="scripts/prettify/lang-css.js"> </script>
<!--[if lt IE 9]>
<script src="//html5shiv.googlecode.com/svn/trunk/html5.js"></script>
<![endif]-->
<link type="text/css" rel="stylesheet" href="styles/prettify-tomorrow.css">
<link type="text/css" rel="stylesheet" href="styles/jsdoc-default.css">
</head>
<body>
<div id="main">
<h1 class="page-title">Source: core/join.js</h1>
<section>
<article>
<pre class="prettyprint source linenums"><code>/* eslint no-param-reassign: 0 */
/* eslint import/no-cycle: [0, { ignoreExternal: true }] */
/**
* @author Marzavec ( https://github.com/marzavec )
* @summary Join target channel
* @version 1.0.0
* @description Join the target channel using the supplied nick and password
* @module join
*/
import {
getSession,
} from './session.js';
import {
canJoinChannel,
socketInChannel,
} from '../utility/_Channels.js';
import {
Errors,
} from '../utility/_Constants.js';
import {
upgradeLegacyJoin,
legacyLevelToLabel,
} from '../utility/_LegacyFunctions.js';
import {
verifyNickname,
getUserPerms,
getUserDetails,
isModerator,
} from '../utility/_UAC.js';
/**
* Executes when invoked by a remote client
* @param {Object} env - Enviroment object with references to core, server, socket &amp; 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 joining channels too fast. Wait a moment and try again.',
id: Errors.Join.RATELIMIT,
channel: false, // @todo Multichannel, false for global event
}, socket);
}
// `join` is the legacy entry point, check if it needs to be upgraded
if (typeof socket.hcProtocol === 'undefined' || socket.hcProtocol === 1) {
payload = upgradeLegacyJoin(server, socket, payload);
}
// store payload values
const { channel, nick, pass } = payload;
// check if a client is able to join target channel
const mayJoin = canJoinChannel(channel, socket);
if (mayJoin !== true) {
return server.reply({
cmd: 'warn',
text: 'You may not join that channel.',
id: mayJoin,
channel: false, // @todo Multichannel, false for global event
}, socket);
}
// calling socket already in a channel
// @todo multichannel update, will remove
if (typeof socket.channel !== 'undefined') {
return server.reply({
cmd: 'warn', // @todo Remove this
text: 'Joining more than one channel is not currently supported',
id: Errors.Join.ALREADY_JOINED,
channel: false, // @todo Multichannel, false for global event
}, socket);
}
// end todo
// validates the user input for `nick`
if (verifyNickname(nick, socket) !== true) {
return server.reply({
cmd: 'warn',
text: 'Nickname must consist of up to 24 letters, numbers, and underscores',
id: Errors.Join.INVALID_NICK,
channel: false, // @todo Multichannel, false for global event
}, socket);
}
// get trip and level
const { trip, level } = getUserPerms(pass, core.saltKey, core.appConfig.data, channel);
// store the user values
const userInfo = {
nick,
trip,
uType: legacyLevelToLabel(level),
hash: socket.hash,
level,
userid: socket.userid,
isBot: socket.isBot,
color: socket.color,
channel,
};
// check if the nickname already exists in the channel
const userExists = server.findSockets({
channel,
nick: (targetNick) => targetNick.toLowerCase() === userInfo.nick.toLowerCase(),
});
if (userExists.length > 0) {
// that nickname is already in that channel
return server.reply({
cmd: 'warn',
text: 'Nickname taken',
id: Errors.Join.NAME_TAKEN,
channel: false, // @todo Multichannel, false for global event
}, socket);
}
// prepare to notify channel peers
const newPeerList = server.findSockets({ channel });
const nicks = []; /* @legacy */
const users = [];
const joinAnnouncement = { ...{ cmd: 'onlineAdd' }, ...userInfo };
// send join announcement and prep online set reply
for (let i = 0, l = newPeerList.length; i &lt; l; i += 1) {
server.reply(joinAnnouncement, newPeerList[i]);
nicks.push(newPeerList[i].nick); /* @legacy */
users.push({
...{
channel,
isme: false,
},
...getUserDetails(newPeerList[i]),
});
}
// store user info
socket.nick = userInfo.nick;
socket.trip = userInfo.trip;
socket.level = userInfo.level;
socket.uType = userInfo.uType; /* @legacy */
socket.channel = channel; /* @legacy */
// @todo multi-channel patch
// socket.channels.push(channel);
socket.channels = [channel];
// global mod perks
if (isModerator(socket.level)) {
socket.ratelimitImmune = true;
}
nicks.push(userInfo.nick); /* @legacy */
users.push({ ...{ isme: true, isBot: socket.isBot }, ...userInfo });
// reply with channel peer list
server.reply({
cmd: 'onlineSet',
nicks, /* @legacy */
users,
channel, // @todo Multichannel (?)
}, socket);
// update client with new session info
server.reply({
cmd: 'session',
restored: false,
token: getSession(socket, core),
channels: socket.channels,
}, socket);
// stats are fun
core.stats.increment('users-joined');
return true;
}
export function restoreJoin({
server, socket, channel,
}) {
// check if a client is able to join target channel
const mayJoin = canJoinChannel(channel, socket);
if (mayJoin !== true) {
return server.reply({
cmd: 'warn',
text: 'You may not join that channel.',
id: mayJoin,
channel: false, // @todo Multichannel, false for global event
}, socket);
}
// store the user values
const userInfo = {
nick: socket.nick,
trip: socket.trip,
uType: legacyLevelToLabel(socket.level),
hash: socket.hash,
level: socket.level,
userid: socket.userid,
isBot: socket.isBot,
color: socket.color,
channel,
};
// prepare to notify channel peers
const newPeerList = server.findSockets({ channel });
const nicks = []; /* @legacy */
const users = [];
const joinAnnouncement = { ...{ cmd: 'onlineAdd' }, ...userInfo };
// build update notice with new privileges
const updateAnnouncement = {
...getUserDetails(socket),
...{
cmd: 'updateUser',
online: true,
},
};
const isDuplicate = socketInChannel(server, channel, socket);
// send join announcement and prep online set reply
for (let i = 0, l = newPeerList.length; i &lt; l; i += 1) {
if (isDuplicate) {
server.reply(updateAnnouncement, newPeerList[i]);
} else {
server.reply(joinAnnouncement, newPeerList[i]);
}
nicks.push(newPeerList[i].nick); /* @legacy */
users.push({
...{
channel,
isme: false,
},
...getUserDetails(newPeerList[i]),
});
}
nicks.push(userInfo.nick); /* @legacy */
users.push({ ...{ isme: true, isBot: socket.isBot }, ...userInfo });
// reply with channel peer list
server.reply({
cmd: 'onlineSet',
nicks, /* @legacy */
users,
channel, // @todo Multichannel (?)
}, socket);
socket.channel = channel; /* @legacy */
socket.channels.push(channel);
return true;
}
/**
* Module meta information
* @public
* @typedef {Object} join/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: 'join',
category: 'core',
description: 'Join the target channel using the supplied nick and password',
usage: `
API: { cmd: 'join', nick: '&lt;your nickname>', pass: '&lt;optional password>', channel: '&lt;target channel>' }`,
};
</code></pre>
</article>
</section>
</div>
<nav>
<h2><a href="index.html">Home</a></h2><h3>Modules</h3><ul><li><a href="module-addmod.html">addmod</a></li><li><a href="module-ban.html">ban</a></li><li><a href="module-changecolor.html">changecolor</a></li><li><a href="module-changenick.html">changenick</a></li><li><a href="module-chat.html">chat</a></li><li><a href="module-disablecaptcha.html">disablecaptcha</a></li><li><a href="module-disconnect.html">disconnect</a></li><li><a href="module-dumb.html">dumb</a></li><li><a href="module-emote.html">emote</a></li><li><a href="module-enablecaptcha.html">enablecaptcha</a></li><li><a href="module-forcecolor.html">forcecolor</a></li><li><a href="module-help.html">help</a></li><li><a href="module-invite.html">invite</a></li><li><a href="module-join.html">join</a></li><li><a href="module-kick.html">kick</a></li><li><a href="module-listusers.html">listusers</a></li><li><a href="module-lockroom.html">lockroom</a></li><li><a href="module-morestats.html">morestats</a></li><li><a href="module-ping.html">ping</a></li><li><a href="module-reload.html">reload</a></li><li><a href="module-removemod.html">removemod</a></li><li><a href="module-saveconfig.html">saveconfig</a></li><li><a href="module-session.html">session</a></li><li><a href="module-shout.html">shout</a></li><li><a href="module-socketreply.html">socketreply</a></li><li><a href="module-speak.html">speak</a></li><li><a href="module-stats.html">stats</a></li><li><a href="module-unban.html">unban</a></li><li><a href="module-unbanall.html">unbanall</a></li><li><a href="module-unlockroom.html">unlockroom</a></li><li><a href="module-updateMessage.html">updateMessage</a></li><li><a href="module-whisper.html">whisper</a></li></ul>
</nav>
<br class="clear">
<footer>
Documentation generated by <a href="https://github.com/jsdoc/jsdoc">JSDoc 3.6.10</a> on Fri Dec 29 2023 23:22:35 GMT-0800 (Pacific Standard Time)
</footer>
<script> prettyPrint(); </script>
<script src="scripts/linenumber.js"> </script>
</body>
</html>

View File

@ -0,0 +1,215 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="utf-8">
<title>JSDoc: Source: core/morestats.js</title>
<script src="scripts/prettify/prettify.js"> </script>
<script src="scripts/prettify/lang-css.js"> </script>
<!--[if lt IE 9]>
<script src="//html5shiv.googlecode.com/svn/trunk/html5.js"></script>
<![endif]-->
<link type="text/css" rel="stylesheet" href="styles/prettify-tomorrow.css">
<link type="text/css" rel="stylesheet" href="styles/jsdoc-default.css">
</head>
<body>
<div id="main">
<h1 class="page-title">Source: core/morestats.js</h1>
<section>
<article>
<pre class="prettyprint source linenums"><code>/**
* @author Marzavec ( https://github.com/marzavec )
* @summary Get stats
* @version 1.0.0
* @description Sends back current server stats to the calling client
* @module morestats
*/
/**
* Format input time into string
* @param {Date} time - Subject date
* @private
* @return {string}
*/
const formatTime = (time) => {
let seconds = time[0] + time[1] / 1e9;
let minutes = Math.floor(seconds / 60);
seconds %= 60;
let hours = Math.floor(minutes / 60);
minutes %= 60;
const days = Math.floor(hours / 24);
hours %= 24;
return `${days.toFixed(0)}d ${hours.toFixed(0)}h ${minutes.toFixed(0)}m ${seconds.toFixed(0)}s`;
};
/**
* Executes when invoked by a remote client
* @param {Object} env - Enviroment object with references to core, server, socket &amp; payload
* @public
* @return {void}
*/
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,
};
// @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') {
publicChanCounts[client.channel] += 1;
}
}
});
const uniqueClientCount = Object.keys(ips).length;
const uniqueChannels = Object.keys(channels).length;
const joins = core.stats.get('users-joined') || 0;
const invites = core.stats.get('invites-sent') || 0;
const messages = core.stats.get('messages-sent') || 0;
const banned = core.stats.get('users-banned') || 0;
const kicked = core.stats.get('users-kicked') || 0;
const stats = core.stats.get('stats-requested') || 0;
const uptime = formatTime(process.hrtime(core.stats.get('start-time')));
// dispatch info
server.reply({
cmd: 'info',
users: uniqueClientCount,
chans: uniqueChannels,
joins,
invites,
messages,
banned,
kicked,
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}`,
channel: socket.channel, // @todo Multichannel
}, socket);
// stats are fun
core.stats.increment('stats-requested');
return true;
}
/**
* Automatically executes once after server is ready to register this modules hooks
* @param {Object} server - Reference to server enviroment object
* @public
* @return {void}
*/
export function initHooks(server) {
server.registerHook('in', 'chat', this.statsCheck.bind(this), 26);
}
/**
* Executes every time an incoming chat command is invoked;
* hooks chat commands checking for /stats
* @param {Object} env - Enviroment object with references to core, server, socket &amp; payload
* @public
* @return {(Object|boolean|string)} Object = same/altered payload,
* false = suppress action,
* string = error
*/
export function statsCheck({
core, server, socket, payload,
}) {
if (typeof payload.text !== 'string') {
return false;
}
if (payload.text.startsWith('/stats')) {
this.run({
core,
server,
socket,
payload: {
cmd: 'morestats',
},
});
return false;
}
return payload;
}
/**
* Module meta information
* @public
* @typedef {Object} morestats/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: 'morestats',
category: 'core',
description: 'Sends back current server stats to the calling client',
usage: `
API: { cmd: 'morestats' }
Text: /stats`,
};
</code></pre>
</article>
</section>
</div>
<nav>
<h2><a href="index.html">Home</a></h2><h3>Modules</h3><ul><li><a href="module-addmod.html">addmod</a></li><li><a href="module-ban.html">ban</a></li><li><a href="module-changecolor.html">changecolor</a></li><li><a href="module-changenick.html">changenick</a></li><li><a href="module-chat.html">chat</a></li><li><a href="module-disablecaptcha.html">disablecaptcha</a></li><li><a href="module-disconnect.html">disconnect</a></li><li><a href="module-dumb.html">dumb</a></li><li><a href="module-emote.html">emote</a></li><li><a href="module-enablecaptcha.html">enablecaptcha</a></li><li><a href="module-forcecolor.html">forcecolor</a></li><li><a href="module-help.html">help</a></li><li><a href="module-invite.html">invite</a></li><li><a href="module-join.html">join</a></li><li><a href="module-kick.html">kick</a></li><li><a href="module-listusers.html">listusers</a></li><li><a href="module-lockroom.html">lockroom</a></li><li><a href="module-morestats.html">morestats</a></li><li><a href="module-ping.html">ping</a></li><li><a href="module-reload.html">reload</a></li><li><a href="module-removemod.html">removemod</a></li><li><a href="module-saveconfig.html">saveconfig</a></li><li><a href="module-session.html">session</a></li><li><a href="module-shout.html">shout</a></li><li><a href="module-socketreply.html">socketreply</a></li><li><a href="module-speak.html">speak</a></li><li><a href="module-stats.html">stats</a></li><li><a href="module-unban.html">unban</a></li><li><a href="module-unbanall.html">unbanall</a></li><li><a href="module-unlockroom.html">unlockroom</a></li><li><a href="module-updateMessage.html">updateMessage</a></li><li><a href="module-whisper.html">whisper</a></li></ul>
</nav>
<br class="clear">
<footer>
Documentation generated by <a href="https://github.com/jsdoc/jsdoc">JSDoc 3.6.10</a> on Fri Dec 29 2023 23:22:35 GMT-0800 (Pacific Standard Time)
</footer>
<script> prettyPrint(); </script>
<script src="scripts/linenumber.js"> </script>
</body>
</html>

View File

@ -0,0 +1,83 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="utf-8">
<title>JSDoc: Source: core/ping.js</title>
<script src="scripts/prettify/prettify.js"> </script>
<script src="scripts/prettify/lang-css.js"> </script>
<!--[if lt IE 9]>
<script src="//html5shiv.googlecode.com/svn/trunk/html5.js"></script>
<![endif]-->
<link type="text/css" rel="stylesheet" href="styles/prettify-tomorrow.css">
<link type="text/css" rel="stylesheet" href="styles/jsdoc-default.css">
</head>
<body>
<div id="main">
<h1 class="page-title">Source: core/ping.js</h1>
<section>
<article>
<pre class="prettyprint source linenums"><code>/* eslint no-empty-function: 0 */
/**
* @author Marzavec ( https://github.com/marzavec )
* @summary Legacy support module
* @version 1.0.0
* @description This module is only in place to supress error notices legacy clients may get
* @module ping
*/
/**
* Executes when invoked by a remote client
* @public
* @return {void}
*/
export async function run() { }
/**
* Module meta information
* @public
* @typedef {Object} ping/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: 'ping',
category: 'core',
description: 'This module is only in place to supress error notices legacy clients may get',
usage: 'none',
};
</code></pre>
</article>
</section>
</div>
<nav>
<h2><a href="index.html">Home</a></h2><h3>Modules</h3><ul><li><a href="module-addmod.html">addmod</a></li><li><a href="module-ban.html">ban</a></li><li><a href="module-changecolor.html">changecolor</a></li><li><a href="module-changenick.html">changenick</a></li><li><a href="module-chat.html">chat</a></li><li><a href="module-disablecaptcha.html">disablecaptcha</a></li><li><a href="module-disconnect.html">disconnect</a></li><li><a href="module-dumb.html">dumb</a></li><li><a href="module-emote.html">emote</a></li><li><a href="module-enablecaptcha.html">enablecaptcha</a></li><li><a href="module-forcecolor.html">forcecolor</a></li><li><a href="module-help.html">help</a></li><li><a href="module-invite.html">invite</a></li><li><a href="module-join.html">join</a></li><li><a href="module-kick.html">kick</a></li><li><a href="module-listusers.html">listusers</a></li><li><a href="module-lockroom.html">lockroom</a></li><li><a href="module-morestats.html">morestats</a></li><li><a href="module-ping.html">ping</a></li><li><a href="module-reload.html">reload</a></li><li><a href="module-removemod.html">removemod</a></li><li><a href="module-saveconfig.html">saveconfig</a></li><li><a href="module-session.html">session</a></li><li><a href="module-shout.html">shout</a></li><li><a href="module-socketreply.html">socketreply</a></li><li><a href="module-speak.html">speak</a></li><li><a href="module-stats.html">stats</a></li><li><a href="module-unban.html">unban</a></li><li><a href="module-unbanall.html">unbanall</a></li><li><a href="module-unlockroom.html">unlockroom</a></li><li><a href="module-updateMessage.html">updateMessage</a></li><li><a href="module-whisper.html">whisper</a></li></ul>
</nav>
<br class="clear">
<footer>
Documentation generated by <a href="https://github.com/jsdoc/jsdoc">JSDoc 3.6.10</a> on Fri Dec 29 2023 23:22:35 GMT-0800 (Pacific Standard Time)
</footer>
<script> prettyPrint(); </script>
<script src="scripts/linenumber.js"> </script>
</body>
</html>

View File

@ -0,0 +1,250 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="utf-8">
<title>JSDoc: Source: core/session.js</title>
<script src="scripts/prettify/prettify.js"> </script>
<script src="scripts/prettify/lang-css.js"> </script>
<!--[if lt IE 9]>
<script src="//html5shiv.googlecode.com/svn/trunk/html5.js"></script>
<![endif]-->
<link type="text/css" rel="stylesheet" href="styles/prettify-tomorrow.css">
<link type="text/css" rel="stylesheet" href="styles/jsdoc-default.css">
</head>
<body>
<div id="main">
<h1 class="page-title">Source: core/session.js</h1>
<section>
<article>
<pre class="prettyprint source linenums"><code>/* eslint import/no-cycle: [0, { ignoreExternal: true }] */
/**
* @author Marzavec ( https://github.com/marzavec )
* @summary Create or restore session
* @version 1.0.0
* @description Restore previous state by session or create new session
* @module session
*/
import fs from 'fs';
import jsonwebtoken from 'jsonwebtoken';
import {
isModerator,
verifyNickname,
} from '../utility/_UAC.js';
import {
Errors,
} from '../utility/_Constants.js';
import {
restoreJoin,
} from './join.js';
const SessionLocation = './session.key';
/**
* Get a new json web token for the provided socket
* @param {*} socket
* @param {*} core
* @returns {object}
*/
export function getSession(socket, core) {
return jsonwebtoken.sign({
channel: socket.channel,
channels: socket.channels,
color: socket.color,
isBot: socket.isBot,
level: socket.level,
nick: socket.nick,
trip: socket.trip,
userid: socket.userid,
uType: socket.uType,
muzzled: socket.muzzled || false,
banned: socket.banned || false,
}, core.sessionKey, {
expiresIn: '7 days',
});
}
/**
* Reply to target socket with session failure notice
* @param {*} server
* @param {*} socket
* @returns {boolean}
*/
function notifyFailure(server, socket) {
server.reply({
cmd: 'error',
id: Errors.Session.BAD_SESSION,
text: 'Invalid session',
}, socket);
return false;
}
/**
* Executes when invoked by a remote client
* @param {Object} env - Enviroment object with references to core, server, socket &amp; payload
* @public
* @return {void}
*/
export async function run({
core, server, socket, payload,
}) {
if (typeof payload.token === 'undefined') {
return notifyFailure(server, socket);
}
let session = false;
try {
session = jsonwebtoken.verify(payload.token, core.sessionKey);
} catch (err) {
return notifyFailure(server, socket);
}
// validate session
if (typeof session.channel !== 'string') {
return notifyFailure(server, socket);
}
if (Array.isArray(session.channels) === false) {
return notifyFailure(server, socket);
}
if (typeof session.color !== 'string' &amp;&amp; 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);
}
// populate socket info with validated session
socket.channels = [];
socket.color = session.color;
socket.isBot = session.isBot;
socket.level = session.level;
socket.nick = session.nick;
socket.trip = session.trip;
socket.userid = session.userid;
socket.uType = session.uType;
socket.muzzled = session.muzzled;
socket.banned = session.banned;
// global mod perks
if (isModerator(socket.level)) {
socket.ratelimitImmune = true;
}
socket.hash = server.getSocketHash(socket);
socket.hcProtocol = 2;
// dispatch info
server.reply({
cmd: 'session',
restored: true,
token: getSession(socket, core),
channels: socket.channels,
}, socket);
for (let i = 0, j = session.channels.length; i &lt; j; i += 1) {
restoreJoin({
core,
server,
socket,
channel: session.channels[i],
}, true);
}
return true;
}
/**
* Automatically executes once after server is ready
* @param {Object} core - Reference to core enviroment object
* @public
* @return {void}
*/
export function init(core) {
// load the encryption key if required
if (typeof core.sessionKey === 'undefined') {
core.sessionKey = fs.readFileSync(SessionLocation);
}
}
/**
* Module meta information
* @public
* @typedef {Object} session/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: 'session',
category: 'core',
description: 'Restore previous state by session or create new session',
usage: "API: { cmd: 'session', id: '&lt;previous session>' }",
};
</code></pre>
</article>
</section>
</div>
<nav>
<h2><a href="index.html">Home</a></h2><h3>Modules</h3><ul><li><a href="module-addmod.html">addmod</a></li><li><a href="module-ban.html">ban</a></li><li><a href="module-changecolor.html">changecolor</a></li><li><a href="module-changenick.html">changenick</a></li><li><a href="module-chat.html">chat</a></li><li><a href="module-disablecaptcha.html">disablecaptcha</a></li><li><a href="module-disconnect.html">disconnect</a></li><li><a href="module-dumb.html">dumb</a></li><li><a href="module-emote.html">emote</a></li><li><a href="module-enablecaptcha.html">enablecaptcha</a></li><li><a href="module-forcecolor.html">forcecolor</a></li><li><a href="module-help.html">help</a></li><li><a href="module-invite.html">invite</a></li><li><a href="module-join.html">join</a></li><li><a href="module-kick.html">kick</a></li><li><a href="module-listusers.html">listusers</a></li><li><a href="module-lockroom.html">lockroom</a></li><li><a href="module-morestats.html">morestats</a></li><li><a href="module-ping.html">ping</a></li><li><a href="module-reload.html">reload</a></li><li><a href="module-removemod.html">removemod</a></li><li><a href="module-saveconfig.html">saveconfig</a></li><li><a href="module-session.html">session</a></li><li><a href="module-shout.html">shout</a></li><li><a href="module-socketreply.html">socketreply</a></li><li><a href="module-speak.html">speak</a></li><li><a href="module-stats.html">stats</a></li><li><a href="module-unban.html">unban</a></li><li><a href="module-unbanall.html">unbanall</a></li><li><a href="module-unlockroom.html">unlockroom</a></li><li><a href="module-updateMessage.html">updateMessage</a></li><li><a href="module-whisper.html">whisper</a></li></ul>
</nav>
<br class="clear">
<footer>
Documentation generated by <a href="https://github.com/jsdoc/jsdoc">JSDoc 3.6.10</a> on Fri Dec 29 2023 23:22:35 GMT-0800 (Pacific Standard Time)
</footer>
<script> prettyPrint(); </script>
<script src="scripts/linenumber.js"> </script>
</body>
</html>

View File

@ -0,0 +1,110 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="utf-8">
<title>JSDoc: Source: core/stats.js</title>
<script src="scripts/prettify/prettify.js"> </script>
<script src="scripts/prettify/lang-css.js"> </script>
<!--[if lt IE 9]>
<script src="//html5shiv.googlecode.com/svn/trunk/html5.js"></script>
<![endif]-->
<link type="text/css" rel="stylesheet" href="styles/prettify-tomorrow.css">
<link type="text/css" rel="stylesheet" href="styles/jsdoc-default.css">
</head>
<body>
<div id="main">
<h1 class="page-title">Source: core/stats.js</h1>
<section>
<article>
<pre class="prettyprint source linenums"><code>/**
* @author Marzavec ( https://github.com/marzavec )
* @summary Simple stats
* @version 1.0.0
* @description Sends back legacy server stats to the calling client
* @module stats
*/
/**
* Executes when invoked by a remote client
* @param {Object} env - Enviroment object with references to core, server, socket &amp; payload
* @public
* @return {void}
*/
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;
ips[client.address] = true;
}
});
const uniqueClientCount = Object.keys(ips).length;
const uniqueChannels = Object.keys(channels).length;
ips = null;
channels = null;
// dispatch info
server.reply({
cmd: 'info',
text: `${uniqueClientCount} unique IPs in ${uniqueChannels} channels`,
channel: socket.channel, // @todo Multichannel
}, socket);
// stats are fun
core.stats.increment('stats-requested');
}
/**
* Module meta information
* @public
* @typedef {Object} stats/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: 'stats',
category: 'core',
description: 'Sends back legacy server stats to the calling client',
usage: `
API: { cmd: 'stats' }`,
};
</code></pre>
</article>
</section>
</div>
<nav>
<h2><a href="index.html">Home</a></h2><h3>Modules</h3><ul><li><a href="module-addmod.html">addmod</a></li><li><a href="module-ban.html">ban</a></li><li><a href="module-changecolor.html">changecolor</a></li><li><a href="module-changenick.html">changenick</a></li><li><a href="module-chat.html">chat</a></li><li><a href="module-disablecaptcha.html">disablecaptcha</a></li><li><a href="module-disconnect.html">disconnect</a></li><li><a href="module-dumb.html">dumb</a></li><li><a href="module-emote.html">emote</a></li><li><a href="module-enablecaptcha.html">enablecaptcha</a></li><li><a href="module-forcecolor.html">forcecolor</a></li><li><a href="module-help.html">help</a></li><li><a href="module-invite.html">invite</a></li><li><a href="module-join.html">join</a></li><li><a href="module-kick.html">kick</a></li><li><a href="module-listusers.html">listusers</a></li><li><a href="module-lockroom.html">lockroom</a></li><li><a href="module-morestats.html">morestats</a></li><li><a href="module-ping.html">ping</a></li><li><a href="module-reload.html">reload</a></li><li><a href="module-removemod.html">removemod</a></li><li><a href="module-saveconfig.html">saveconfig</a></li><li><a href="module-session.html">session</a></li><li><a href="module-shout.html">shout</a></li><li><a href="module-socketreply.html">socketreply</a></li><li><a href="module-speak.html">speak</a></li><li><a href="module-stats.html">stats</a></li><li><a href="module-unban.html">unban</a></li><li><a href="module-unbanall.html">unbanall</a></li><li><a href="module-unlockroom.html">unlockroom</a></li><li><a href="module-updateMessage.html">updateMessage</a></li><li><a href="module-whisper.html">whisper</a></li></ul>
</nav>
<br class="clear">
<footer>
Documentation generated by <a href="https://github.com/jsdoc/jsdoc">JSDoc 3.6.10</a> on Fri Dec 29 2023 23:22:35 GMT-0800 (Pacific Standard Time)
</footer>
<script> prettyPrint(); </script>
<script src="scripts/linenumber.js"> </script>
</body>
</html>

View File

@ -0,0 +1,178 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="utf-8">
<title>JSDoc: Source: core/updateMessage.js</title>
<script src="scripts/prettify/prettify.js"> </script>
<script src="scripts/prettify/lang-css.js"> </script>
<!--[if lt IE 9]>
<script src="//html5shiv.googlecode.com/svn/trunk/html5.js"></script>
<![endif]-->
<link type="text/css" rel="stylesheet" href="styles/prettify-tomorrow.css">
<link type="text/css" rel="stylesheet" href="styles/jsdoc-default.css">
</head>
<body>
<div id="main">
<h1 class="page-title">Source: core/updateMessage.js</h1>
<section>
<article>
<pre class="prettyprint source linenums"><code>/**
* @author MinusGix ( https://github.com/MinusGix )
* @summary Change target message
* @version v1.0.0
* @description Will alter a previously sent message using that message's customId
* @module updateMessage
*/
import {
parseText,
} from '../utility/_Text.js';
import {
isAdmin,
isModerator,
} from '../utility/_UAC.js';
import {
ACTIVE_MESSAGES,
MAX_MESSAGE_ID_LENGTH,
} from './chat.js';
/**
* Executes when invoked by a remote client
* @param {Object} env - Enviroment object with references to core, server, socket &amp; payload
* @public
* @return {void}
*/
export async function run({
server, socket, payload,
}) {
// undefined | "overwrite" | "append" | "prepend" | "complete"
const { customId } = payload;
let { mode, text } = payload;
if (!mode) {
mode = 'overwrite';
}
if (mode !== 'overwrite' &amp;&amp; mode !== 'append' &amp;&amp; mode !== 'prepend' &amp;&amp; mode !== 'complete') {
return server.police.frisk(socket, 13);
}
if (!customId || typeof customId !== 'string' || customId.length > MAX_MESSAGE_ID_LENGTH) {
return server.police.frisk(socket, 13);
}
if (typeof (text) !== 'string') {
return server.police.frisk(socket, 13);
}
if (mode === 'overwrite') {
text = parseText(text);
if (text === '') {
text = '\u0000';
}
}
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.
let message;
for (let i = 0; i &lt; ACTIVE_MESSAGES.length; i += 1) {
const msg = ACTIVE_MESSAGES[i];
if (msg.userid === socket.userid &amp;&amp; msg.customId === customId) {
message = ACTIVE_MESSAGES[i];
if (mode === 'complete') {
ACTIVE_MESSAGES[i].toDelete = true;
}
break;
}
}
if (!message) {
return server.police.frisk(socket, 6);
}
const outgoingPayload = {
cmd: 'updateMessage',
userid: socket.userid,
channel: socket.channel,
level: socket.level,
mode,
text,
customId: message.customId,
};
if (isAdmin(socket.level)) {
outgoingPayload.admin = true;
} else if (isModerator(socket.level)) {
outgoingPayload.mod = true;
}
server.broadcast(outgoingPayload, { channel: socket.channel });
return true;
}
/**
* The following payload properties are required to invoke this module:
* "text", "customId"
* @public
* @typedef {Array} addmod/requiredData
*/
export const requiredData = ['text', 'customId'];
/**
* Module meta information
* @public
* @typedef {Object} updateMessage/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: 'updateMessage',
category: 'core',
description: 'Update a message you have sent.',
usage: `
API: { cmd: 'updateMessage', mode: 'overwrite'|'append'|'prepend'|'complete', text: '&lt;text to apply>', customId: '&lt;customId sent with the chat message>' }`,
};
</code></pre>
</article>
</section>
</div>
<nav>
<h2><a href="index.html">Home</a></h2><h3>Modules</h3><ul><li><a href="module-addmod.html">addmod</a></li><li><a href="module-ban.html">ban</a></li><li><a href="module-changecolor.html">changecolor</a></li><li><a href="module-changenick.html">changenick</a></li><li><a href="module-chat.html">chat</a></li><li><a href="module-disablecaptcha.html">disablecaptcha</a></li><li><a href="module-disconnect.html">disconnect</a></li><li><a href="module-dumb.html">dumb</a></li><li><a href="module-emote.html">emote</a></li><li><a href="module-enablecaptcha.html">enablecaptcha</a></li><li><a href="module-forcecolor.html">forcecolor</a></li><li><a href="module-help.html">help</a></li><li><a href="module-invite.html">invite</a></li><li><a href="module-join.html">join</a></li><li><a href="module-kick.html">kick</a></li><li><a href="module-listusers.html">listusers</a></li><li><a href="module-lockroom.html">lockroom</a></li><li><a href="module-morestats.html">morestats</a></li><li><a href="module-ping.html">ping</a></li><li><a href="module-reload.html">reload</a></li><li><a href="module-removemod.html">removemod</a></li><li><a href="module-saveconfig.html">saveconfig</a></li><li><a href="module-session.html">session</a></li><li><a href="module-shout.html">shout</a></li><li><a href="module-socketreply.html">socketreply</a></li><li><a href="module-speak.html">speak</a></li><li><a href="module-stats.html">stats</a></li><li><a href="module-unban.html">unban</a></li><li><a href="module-unbanall.html">unbanall</a></li><li><a href="module-unlockroom.html">unlockroom</a></li><li><a href="module-updateMessage.html">updateMessage</a></li><li><a href="module-whisper.html">whisper</a></li></ul>
</nav>
<br class="clear">
<footer>
Documentation generated by <a href="https://github.com/jsdoc/jsdoc">JSDoc 3.6.10</a> on Fri Dec 29 2023 23:22:35 GMT-0800 (Pacific Standard Time)
</footer>
<script> prettyPrint(); </script>
<script src="scripts/linenumber.js"> </script>
</body>
</html>

View File

@ -0,0 +1,282 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="utf-8">
<title>JSDoc: Source: core/whisper.js</title>
<script src="scripts/prettify/prettify.js"> </script>
<script src="scripts/prettify/lang-css.js"> </script>
<!--[if lt IE 9]>
<script src="//html5shiv.googlecode.com/svn/trunk/html5.js"></script>
<![endif]-->
<link type="text/css" rel="stylesheet" href="styles/prettify-tomorrow.css">
<link type="text/css" rel="stylesheet" href="styles/jsdoc-default.css">
</head>
<body>
<div id="main">
<h1 class="page-title">Source: core/whisper.js</h1>
<section>
<article>
<pre class="prettyprint source linenums"><code>/**
* @author Marzavec ( https://github.com/marzavec )
* @summary Send whisper
* @version 1.0.0
* @description Display text on target users screen that only they can see
* @module whisper
* @todo This should be changed to it's own event type, instead of `info`
and accept a `userid` rather than `nick`
*/
import {
findUser,
} from '../utility/_Channels.js';
import {
Errors,
} from '../utility/_Constants.js';
import {
legacyWhisperOut,
legacyWhisperReply,
} from '../utility/_LegacyFunctions.js';
/**
* Check and trim string provided by remote client
* @param {string} text - Subject string
* @private
* @todo Move into utility module
* @return {string|boolean}
*/
const parseText = (text) => {
// verifies user input is text
if (typeof text !== 'string') {
return false;
}
let sanitizedText = text;
// strip newlines from beginning and end
sanitizedText = sanitizedText.replace(/^\s*\n|^\s+$|\n\s*$/g, '');
// replace 3+ newlines with just 2 newlines
sanitizedText = sanitizedText.replace(/\n{3,}/g, '\n\n');
return sanitizedText;
};
/**
* Executes when invoked by a remote client
* @param {Object} env - Enviroment object with references to core, server, socket &amp; payload
* @public
* @return {void}
*/
export async function run({ server, socket, payload }) {
// if this is a legacy client add missing params to payload
if (socket.hcProtocol === 1) {
payload.channel = socket.channel; // eslint-disable-line no-param-reassign
}
// verify user input
const text = parseText(payload.text);
if (!text) {
// lets not send objects or empty text, yea?
return server.police.frisk(socket, 13);
}
// check for spam
const score = text.length / 83 / 4;
if (server.police.frisk(socket, score)) {
return server.reply({
cmd: 'warn', // @todo Add numeric error code as `id`
text: 'You are sending too much text. Wait a moment and try again.\nPress the up arrow key to restore your last message.',
channel: socket.channel, // @todo Multichannel
}, socket);
}
const targetUser = findUser(server, payload);
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);
}
const outgoingPayload = {
cmd: 'whisper',
channel: socket.channel, // @todo Multichannel
from: socket.userid,
to: targetUser.userid,
text,
};
// send invite notice to target client
if (targetUser.hcProtocol === 1) {
server.reply(legacyWhisperOut(outgoingPayload, socket), targetUser);
} else {
server.reply(outgoingPayload, targetUser);
}
// send invite notice to this client
if (socket.hcProtocol === 1) {
server.reply(legacyWhisperReply(outgoingPayload, targetUser.nick), socket);
} else {
server.reply(outgoingPayload, socket);
}
targetUser.whisperReply = socket.nick;
return true;
}
/**
* Automatically executes once after server is ready to register this modules hooks
* @param {Object} server - Reference to server enviroment object
* @public
* @return {void}
*/
export function initHooks(server) {
server.registerHook('in', 'chat', this.whisperCheck.bind(this), 20);
}
/**
* Executes every time an incoming chat command is invoked;
* hooks chat commands checking for /whisper
* @param {Object} env - Enviroment object with references to core, server, socket &amp; payload
* @public
* @return {(Object|boolean|string)} Object = same/altered payload,
* false = suppress action,
* string = error
*/
export function whisperCheck({
core, server, socket, payload,
}) {
if (typeof payload.text !== 'string') {
return false;
}
if (payload.text.startsWith('/whisper ') || payload.text.startsWith('/w ')) {
const input = payload.text.split(' ');
// If there is no nickname target parameter
if (!input[1]) {
server.reply({
cmd: 'warn', // @todo Add numeric error code as `id`
text: 'Refer to `/help whisper` for instructions on how to use this command.',
channel: socket.channel, // @todo Multichannel
}, socket);
return false;
}
const target = input[1].replace(/@/g, '');
input.splice(0, 2);
const whisperText = input.join(' ');
this.run({
core,
server,
socket,
payload: {
cmd: 'whisper',
channel: socket.channel, // @todo Multichannel
nick: target,
text: whisperText,
},
});
return false;
}
if (payload.text.startsWith('/reply ') || payload.text.startsWith('/r ')) {
if (typeof socket.whisperReply === 'undefined') {
server.reply({
cmd: 'warn', // @todo Add numeric error code as `id`
text: 'Cannot reply to nobody',
channel: socket.channel, // @todo Multichannel
}, socket);
return false;
}
const input = payload.text.split(' ');
input.splice(0, 1);
const whisperText = input.join(' ');
this.run({
core,
server,
socket,
payload: {
cmd: 'whisper',
nick: socket.whisperReply,
channel: socket.channel, // @todo Multichannel
text: whisperText,
},
});
return false;
}
return payload;
}
/**
* The following payload properties are required to invoke this module:
* "nick", "text"
* @public
* @typedef {Array} whisper/requiredData
*/
export const requiredData = ['nick', 'text'];
/**
* Module meta information
* @public
* @typedef {Object} whisper/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: 'whisper',
category: 'core',
description: 'Display text on target users screen that only they can see',
usage: `
API: { cmd: 'whisper', nick: '&lt;target name>', text: '&lt;text to whisper>' }
Text: /whisper &lt;target name> &lt;text to whisper>
Text: /w &lt;target name> &lt;text to whisper>
Alt Text: /reply &lt;text to whisper, this will auto reply to the last person who whispered to you>
Alt Text: /r &lt;text to whisper, this will auto reply to the last person who whispered to you>`,
};
</code></pre>
</article>
</section>
</div>
<nav>
<h2><a href="index.html">Home</a></h2><h3>Modules</h3><ul><li><a href="module-addmod.html">addmod</a></li><li><a href="module-ban.html">ban</a></li><li><a href="module-changecolor.html">changecolor</a></li><li><a href="module-changenick.html">changenick</a></li><li><a href="module-chat.html">chat</a></li><li><a href="module-disablecaptcha.html">disablecaptcha</a></li><li><a href="module-disconnect.html">disconnect</a></li><li><a href="module-dumb.html">dumb</a></li><li><a href="module-emote.html">emote</a></li><li><a href="module-enablecaptcha.html">enablecaptcha</a></li><li><a href="module-forcecolor.html">forcecolor</a></li><li><a href="module-help.html">help</a></li><li><a href="module-invite.html">invite</a></li><li><a href="module-join.html">join</a></li><li><a href="module-kick.html">kick</a></li><li><a href="module-listusers.html">listusers</a></li><li><a href="module-lockroom.html">lockroom</a></li><li><a href="module-morestats.html">morestats</a></li><li><a href="module-ping.html">ping</a></li><li><a href="module-reload.html">reload</a></li><li><a href="module-removemod.html">removemod</a></li><li><a href="module-saveconfig.html">saveconfig</a></li><li><a href="module-session.html">session</a></li><li><a href="module-shout.html">shout</a></li><li><a href="module-socketreply.html">socketreply</a></li><li><a href="module-speak.html">speak</a></li><li><a href="module-stats.html">stats</a></li><li><a href="module-unban.html">unban</a></li><li><a href="module-unbanall.html">unbanall</a></li><li><a href="module-unlockroom.html">unlockroom</a></li><li><a href="module-updateMessage.html">updateMessage</a></li><li><a href="module-whisper.html">whisper</a></li></ul>
</nav>
<br class="clear">
<footer>
Documentation generated by <a href="https://github.com/jsdoc/jsdoc">JSDoc 3.6.10</a> on Fri Dec 29 2023 23:22:35 GMT-0800 (Pacific Standard Time)
</footer>
<script> prettyPrint(); </script>
<script src="scripts/linenumber.js"> </script>
</body>
</html>

Binary file not shown.

File diff suppressed because it is too large Load Diff

After

Width:  |  Height:  |  Size: 116 KiB

Binary file not shown.

Binary file not shown.

File diff suppressed because it is too large Load Diff

After

Width:  |  Height:  |  Size: 118 KiB

Binary file not shown.

Binary file not shown.

File diff suppressed because it is too large Load Diff

After

Width:  |  Height:  |  Size: 120 KiB

Binary file not shown.

Binary file not shown.

File diff suppressed because it is too large Load Diff

After

Width:  |  Height:  |  Size: 114 KiB

Binary file not shown.

Binary file not shown.

File diff suppressed because it is too large Load Diff

After

Width:  |  Height:  |  Size: 120 KiB

Binary file not shown.

Binary file not shown.

File diff suppressed because it is too large Load Diff

After

Width:  |  Height:  |  Size: 117 KiB

Binary file not shown.

105
documentation/index.html Normal file
View File

@ -0,0 +1,105 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="utf-8">
<title>JSDoc: Home</title>
<script src="scripts/prettify/prettify.js"> </script>
<script src="scripts/prettify/lang-css.js"> </script>
<!--[if lt IE 9]>
<script src="//html5shiv.googlecode.com/svn/trunk/html5.js"></script>
<![endif]-->
<link type="text/css" rel="stylesheet" href="styles/prettify-tomorrow.css">
<link type="text/css" rel="stylesheet" href="styles/jsdoc-default.css">
</head>
<body>
<div id="main">
<h1 class="page-title">Home</h1>
<h3> </h3>
<section>
<article><h1>hack.chat</h1>
<p><a href="https://hack.chat/">hack.chat</a> is a minimal, distraction-free, accountless, logless, disappearing chat service which is easily deployable as your own service. The current client comes bundled with LaTeX rendering provided by <a href="https://github.com/Khan/KaTeX">KaTeX</a> and code syntax highlighting provided by <a href="https://github.com/isagalaev/highlight.js">highlight.js</a>.</p>
<p>A list of software developed for the hack.chat framework can be found at the <a href="https://github.com/hack-chat/3rd-party-software-list">3rd party software list</a> repository. This includes bots, clients, docker containers, etc.</p>
<p>This is a backwards compatible continuation of the <a href="https://github.com/AndrewBelt/hack.chat">work by Andrew Belt</a>. The server code has been updated to ES6 along with several new features including new commands and hot-reload of the commands/protocol. There is also <a href="documentation/index.html">documentation</a>.</p>
<h1>Installation</h1>
<h2>Prerequisites</h2>
<ul>
<li><a href="https://nodejs.org/">node.js v16.14.0</a> or higher</li>
<li><a href="https://nodejs.org/">npm 8.5.4</a> or higher</li>
</ul>
<h2>Developer Installation</h2>
<ol>
<li><a href="https://help.github.com/articles/cloning-a-repository/">Clone</a> the repository: <code>git clone https://github.com/hack-chat/main.git</code></li>
<li>Change the directory: <code>cd main</code></li>
<li>Install the dependencies: <code>npm install</code></li>
<li>Launch: <code>npm start</code></li>
</ol>
<h2>Live Deployment Installation</h2>
<p>See <a href="documentation/DEPLOY.md">DEPLOY.md</a></p>
<h1>Contributing</h1>
<ul>
<li>Use two space indents.</li>
<li>Name files in camelCase.</li>
</ul>
<h1>Credits</h1>
<ul>
<li><a href="https://github.com/marzavec"><strong>Marzavec</strong></a> - <em>Initial work</em></li>
<li><a href="https://github.com/MinusGix"><strong>MinusGix</strong></a> - <em>Base updates</em></li>
<li><a href="https://github.com/neelkamath"><strong>Neel Kamath</strong></a> - <em>Base Documentation</em></li>
<li><a href="https://github.com/po5i"><strong>Carlos Villavicencio</strong></a> - <em>Syntax Highlighting Integration</em></li>
<li><a href="https://github.com/OpSimple"><strong>OpSimple</strong></a> - <em>Modules Added: dumb.js &amp; speak.js</em></li>
<li><a href="https://github.com/AndrewBelt"><strong>Andrew Belt</strong></a>, for original base work</li>
<li><a href="https://github.com/wwandrew"><strong>wwandrew</strong></a>, for finding server flaws (including attack vectors) and submitting <s><em><strong>incredibly detailed</strong></em></s> bug reports</li>
<li><a href="https://github.com/hack-chat/main/graphs/contributors"><strong>Everyone else</strong></a> who participated in this project.</li>
</ul>
<h1>License</h1>
<p>This project is licensed under the <a href="LICENSE">MIT License</a>.</p></article>
</section>
</div>
<nav>
<h2><a href="index.html">Home</a></h2><h3>Modules</h3><ul><li><a href="module-addmod.html">addmod</a></li><li><a href="module-ban.html">ban</a></li><li><a href="module-changecolor.html">changecolor</a></li><li><a href="module-changenick.html">changenick</a></li><li><a href="module-chat.html">chat</a></li><li><a href="module-disablecaptcha.html">disablecaptcha</a></li><li><a href="module-disconnect.html">disconnect</a></li><li><a href="module-dumb.html">dumb</a></li><li><a href="module-emote.html">emote</a></li><li><a href="module-enablecaptcha.html">enablecaptcha</a></li><li><a href="module-forcecolor.html">forcecolor</a></li><li><a href="module-help.html">help</a></li><li><a href="module-invite.html">invite</a></li><li><a href="module-join.html">join</a></li><li><a href="module-kick.html">kick</a></li><li><a href="module-listusers.html">listusers</a></li><li><a href="module-lockroom.html">lockroom</a></li><li><a href="module-morestats.html">morestats</a></li><li><a href="module-ping.html">ping</a></li><li><a href="module-reload.html">reload</a></li><li><a href="module-removemod.html">removemod</a></li><li><a href="module-saveconfig.html">saveconfig</a></li><li><a href="module-session.html">session</a></li><li><a href="module-shout.html">shout</a></li><li><a href="module-socketreply.html">socketreply</a></li><li><a href="module-speak.html">speak</a></li><li><a href="module-stats.html">stats</a></li><li><a href="module-unban.html">unban</a></li><li><a href="module-unbanall.html">unbanall</a></li><li><a href="module-unlockroom.html">unlockroom</a></li><li><a href="module-updateMessage.html">updateMessage</a></li><li><a href="module-whisper.html">whisper</a></li></ul>
</nav>
<br class="clear">
<footer>
Documentation generated by <a href="https://github.com/jsdoc/jsdoc">JSDoc 3.6.10</a> on Fri Dec 29 2023 23:22:35 GMT-0800 (Pacific Standard Time)
</footer>
<script> prettyPrint(); </script>
<script src="scripts/linenumber.js"> </script>
</body>
</html>

View File

@ -0,0 +1,117 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="utf-8">
<title>JSDoc: Source: internal/disconnect.js</title>
<script src="scripts/prettify/prettify.js"> </script>
<script src="scripts/prettify/lang-css.js"> </script>
<!--[if lt IE 9]>
<script src="//html5shiv.googlecode.com/svn/trunk/html5.js"></script>
<![endif]-->
<link type="text/css" rel="stylesheet" href="styles/prettify-tomorrow.css">
<link type="text/css" rel="stylesheet" href="styles/jsdoc-default.css">
</head>
<body>
<div id="main">
<h1 class="page-title">Source: internal/disconnect.js</h1>
<section>
<article>
<pre class="prettyprint source linenums"><code>/**
* @author Marzavec ( https://github.com/marzavec )
* @summary Disconnection handler
* @version 1.0.0
* @description The server invokes this module each time a websocket connection is disconnected
* @module disconnect
*/
import {
socketInChannel,
} from '../utility/_Channels.js';
/**
* Executes when invoked by a remote client
* @param {Object} env - Enviroment object with references to core, server, socket &amp; payload
* @public
* @return {void}
*/
export async function run({ server, socket, payload }) {
if (payload.cmdKey !== server.cmdKey) {
// internal command attempt by client, increase rate limit chance and ignore
return server.police.frisk(socket, 20);
}
// send leave notice to client peers
// @todo Multichannel update
if (socket.channel) {
const isDuplicate = socketInChannel(server, socket.channel, socket);
if (isDuplicate === false) {
server.broadcast({
cmd: 'onlineRemove',
nick: socket.nick,
}, { channel: socket.channel });
}
}
// commit close just in case
socket.terminate();
return true;
}
/**
* The following payload properties are required to invoke this module:
* "cmdKey"
* @public
* @typedef {Array} disconnect/requiredData
*/
export const requiredData = ['cmdKey'];
/**
* Module meta information
* @public
* @typedef {Object} disconnect/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: 'disconnect',
category: 'internal',
description: 'Internally used to relay disconnect events to clients',
usage: 'Internal Use Only',
};
</code></pre>
</article>
</section>
</div>
<nav>
<h2><a href="index.html">Home</a></h2><h3>Modules</h3><ul><li><a href="module-addmod.html">addmod</a></li><li><a href="module-ban.html">ban</a></li><li><a href="module-changecolor.html">changecolor</a></li><li><a href="module-changenick.html">changenick</a></li><li><a href="module-chat.html">chat</a></li><li><a href="module-disablecaptcha.html">disablecaptcha</a></li><li><a href="module-disconnect.html">disconnect</a></li><li><a href="module-dumb.html">dumb</a></li><li><a href="module-emote.html">emote</a></li><li><a href="module-enablecaptcha.html">enablecaptcha</a></li><li><a href="module-forcecolor.html">forcecolor</a></li><li><a href="module-help.html">help</a></li><li><a href="module-invite.html">invite</a></li><li><a href="module-join.html">join</a></li><li><a href="module-kick.html">kick</a></li><li><a href="module-listusers.html">listusers</a></li><li><a href="module-lockroom.html">lockroom</a></li><li><a href="module-morestats.html">morestats</a></li><li><a href="module-ping.html">ping</a></li><li><a href="module-reload.html">reload</a></li><li><a href="module-removemod.html">removemod</a></li><li><a href="module-saveconfig.html">saveconfig</a></li><li><a href="module-session.html">session</a></li><li><a href="module-shout.html">shout</a></li><li><a href="module-socketreply.html">socketreply</a></li><li><a href="module-speak.html">speak</a></li><li><a href="module-stats.html">stats</a></li><li><a href="module-unban.html">unban</a></li><li><a href="module-unbanall.html">unbanall</a></li><li><a href="module-unlockroom.html">unlockroom</a></li><li><a href="module-updateMessage.html">updateMessage</a></li><li><a href="module-whisper.html">whisper</a></li></ul>
</nav>
<br class="clear">
<footer>
Documentation generated by <a href="https://github.com/jsdoc/jsdoc">JSDoc 3.6.10</a> on Fri Dec 29 2023 23:22:35 GMT-0800 (Pacific Standard Time)
</footer>
<script> prettyPrint(); </script>
<script src="scripts/linenumber.js"> </script>
</body>
</html>

View File

@ -0,0 +1,102 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="utf-8">
<title>JSDoc: Source: internal/socketreply.js</title>
<script src="scripts/prettify/prettify.js"> </script>
<script src="scripts/prettify/lang-css.js"> </script>
<!--[if lt IE 9]>
<script src="//html5shiv.googlecode.com/svn/trunk/html5.js"></script>
<![endif]-->
<link type="text/css" rel="stylesheet" href="styles/prettify-tomorrow.css">
<link type="text/css" rel="stylesheet" href="styles/jsdoc-default.css">
</head>
<body>
<div id="main">
<h1 class="page-title">Source: internal/socketreply.js</h1>
<section>
<article>
<pre class="prettyprint source linenums"><code>/**
* @author Marzavec ( https://github.com/marzavec )
* @summary Bridge warning events to a user
* @version 1.0.0
* @description If a warning occurs within the server, this module will relay the warning to the
* client
* @module socketreply
*/
/**
* Executes when invoked by a remote client
* @param {Object} env - Enviroment object with references to core, server, socket &amp; payload
* @public
* @return {void}
*/
export async function run({ server, socket, payload }) {
if (payload.cmdKey !== server.cmdKey) {
// internal command attempt by client, increase rate limit chance and ignore
return server.police.frisk(socket, 20);
}
// send warning to target socket
return server.reply({
cmd: 'warn',
text: payload.text,
}, socket);
}
/**
* The following payload properties are required to invoke this module:
* "cmdKey", "text"
* @public
* @typedef {Array} socketreply/requiredData
*/
export const requiredData = ['cmdKey', 'text'];
/**
* Module meta information
* @public
* @typedef {Object} socketreply/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: 'socketreply',
category: 'internal',
description: 'Internally used to relay warnings to clients',
usage: 'Internal Use Only',
};
</code></pre>
</article>
</section>
</div>
<nav>
<h2><a href="index.html">Home</a></h2><h3>Modules</h3><ul><li><a href="module-addmod.html">addmod</a></li><li><a href="module-ban.html">ban</a></li><li><a href="module-changecolor.html">changecolor</a></li><li><a href="module-changenick.html">changenick</a></li><li><a href="module-chat.html">chat</a></li><li><a href="module-disablecaptcha.html">disablecaptcha</a></li><li><a href="module-disconnect.html">disconnect</a></li><li><a href="module-dumb.html">dumb</a></li><li><a href="module-emote.html">emote</a></li><li><a href="module-enablecaptcha.html">enablecaptcha</a></li><li><a href="module-forcecolor.html">forcecolor</a></li><li><a href="module-help.html">help</a></li><li><a href="module-invite.html">invite</a></li><li><a href="module-join.html">join</a></li><li><a href="module-kick.html">kick</a></li><li><a href="module-listusers.html">listusers</a></li><li><a href="module-lockroom.html">lockroom</a></li><li><a href="module-morestats.html">morestats</a></li><li><a href="module-ping.html">ping</a></li><li><a href="module-reload.html">reload</a></li><li><a href="module-removemod.html">removemod</a></li><li><a href="module-saveconfig.html">saveconfig</a></li><li><a href="module-session.html">session</a></li><li><a href="module-shout.html">shout</a></li><li><a href="module-socketreply.html">socketreply</a></li><li><a href="module-speak.html">speak</a></li><li><a href="module-stats.html">stats</a></li><li><a href="module-unban.html">unban</a></li><li><a href="module-unbanall.html">unbanall</a></li><li><a href="module-unlockroom.html">unlockroom</a></li><li><a href="module-updateMessage.html">updateMessage</a></li><li><a href="module-whisper.html">whisper</a></li></ul>
</nav>
<br class="clear">
<footer>
Documentation generated by <a href="https://github.com/jsdoc/jsdoc">JSDoc 3.6.10</a> on Fri Dec 29 2023 23:22:35 GMT-0800 (Pacific Standard Time)
</footer>
<script> prettyPrint(); </script>
<script src="scripts/linenumber.js"> </script>
</body>
</html>

View File

@ -0,0 +1,166 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="utf-8">
<title>JSDoc: Source: mod/ban.js</title>
<script src="scripts/prettify/prettify.js"> </script>
<script src="scripts/prettify/lang-css.js"> </script>
<!--[if lt IE 9]>
<script src="//html5shiv.googlecode.com/svn/trunk/html5.js"></script>
<![endif]-->
<link type="text/css" rel="stylesheet" href="styles/prettify-tomorrow.css">
<link type="text/css" rel="stylesheet" href="styles/jsdoc-default.css">
</head>
<body>
<div id="main">
<h1 class="page-title">Source: mod/ban.js</h1>
<section>
<article>
<pre class="prettyprint source linenums"><code>/**
* @author Marzavec ( https://github.com/marzavec )
* @summary Ban a user
* @version 1.0.0
* @description Bans target user by name
* @module ban
*/
import {
isModerator,
getUserDetails,
levels,
} from '../utility/_UAC.js';
import {
Errors,
} 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 &amp; payload
* @public
* @return {void}
*/
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);
}
// check user input
if (socket.hcProtocol === 1) {
if (typeof payload.nick !== 'string') {
return false;
}
payload.channel = socket.channel; // eslint-disable-line no-param-reassign
} else if (typeof payload.userid !== 'number') {
return false;
}
// find target user
const targetUser = findUser(server, payload);
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);
}
const targetNick = targetUser.nick;
// i guess banning mods or admins isn't the best idea?
if (targetUser.level >= socket.level) {
return server.reply({
cmd: 'warn',
text: 'Cannot ban other users of the same level, how rude',
id: Errors.Global.PERMISSION,
channel: socket.channel, // @todo Multichannel
}, socket);
}
// commit arrest record
server.police.arrest(targetUser.address, targetUser.hash);
console.log(`${socket.nick} [${socket.trip}] banned ${targetNick} in ${socket.channel}`);
// notify normal users
server.broadcast({
cmd: 'info',
text: `Banned ${targetNick}`,
user: getUserDetails(targetUser),
channel: socket.channel, // @todo Multichannel
}, { channel: socket.channel, level: (level) => level &lt; levels.moderator });
// notify mods
server.broadcast({
cmd: 'info',
text: `${socket.nick}#${socket.trip} banned ${targetNick} in ${payload.channel}, userhash: ${targetUser.hash}`,
channel: socket.channel, // @todo Multichannel
inChannel: payload.channel,
user: getUserDetails(targetUser),
banner: getUserDetails(socket),
}, { level: isModerator });
// force connection closed
targetUser.terminate();
// stats are fun
core.stats.increment('users-banned');
return true;
}
/**
* Module meta information
* @public
* @typedef {Object} ban/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: 'ban',
category: 'moderators',
description: 'Bans target user by name',
usage: `
API: { cmd: 'ban', nick: '&lt;target nickname>' }`,
};
</code></pre>
</article>
</section>
</div>
<nav>
<h2><a href="index.html">Home</a></h2><h3>Modules</h3><ul><li><a href="module-addmod.html">addmod</a></li><li><a href="module-ban.html">ban</a></li><li><a href="module-changecolor.html">changecolor</a></li><li><a href="module-changenick.html">changenick</a></li><li><a href="module-chat.html">chat</a></li><li><a href="module-disablecaptcha.html">disablecaptcha</a></li><li><a href="module-disconnect.html">disconnect</a></li><li><a href="module-dumb.html">dumb</a></li><li><a href="module-emote.html">emote</a></li><li><a href="module-enablecaptcha.html">enablecaptcha</a></li><li><a href="module-forcecolor.html">forcecolor</a></li><li><a href="module-help.html">help</a></li><li><a href="module-invite.html">invite</a></li><li><a href="module-join.html">join</a></li><li><a href="module-kick.html">kick</a></li><li><a href="module-listusers.html">listusers</a></li><li><a href="module-lockroom.html">lockroom</a></li><li><a href="module-morestats.html">morestats</a></li><li><a href="module-ping.html">ping</a></li><li><a href="module-reload.html">reload</a></li><li><a href="module-removemod.html">removemod</a></li><li><a href="module-saveconfig.html">saveconfig</a></li><li><a href="module-session.html">session</a></li><li><a href="module-shout.html">shout</a></li><li><a href="module-socketreply.html">socketreply</a></li><li><a href="module-speak.html">speak</a></li><li><a href="module-stats.html">stats</a></li><li><a href="module-unban.html">unban</a></li><li><a href="module-unbanall.html">unbanall</a></li><li><a href="module-unlockroom.html">unlockroom</a></li><li><a href="module-updateMessage.html">updateMessage</a></li><li><a href="module-whisper.html">whisper</a></li></ul>
</nav>
<br class="clear">
<footer>
Documentation generated by <a href="https://github.com/jsdoc/jsdoc">JSDoc 3.6.10</a> on Fri Dec 29 2023 23:22:35 GMT-0800 (Pacific Standard Time)
</footer>
<script> prettyPrint(); </script>
<script src="scripts/linenumber.js"> </script>
</body>
</html>

View File

@ -0,0 +1,137 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="utf-8">
<title>JSDoc: Source: mod/disablecaptcha.js</title>
<script src="scripts/prettify/prettify.js"> </script>
<script src="scripts/prettify/lang-css.js"> </script>
<!--[if lt IE 9]>
<script src="//html5shiv.googlecode.com/svn/trunk/html5.js"></script>
<![endif]-->
<link type="text/css" rel="stylesheet" href="styles/prettify-tomorrow.css">
<link type="text/css" rel="stylesheet" href="styles/jsdoc-default.css">
</head>
<body>
<div id="main">
<h1 class="page-title">Source: mod/disablecaptcha.js</h1>
<section>
<article>
<pre class="prettyprint source linenums"><code>/**
* @author Marzavec ( https://github.com/marzavec )
* @summary Disables the captcha
* @version 1.0.0
* @description Disables the captcha on the channel specified in the channel property,
* default is current channel
* @module disablecaptcha
*/
import {
isModerator,
} from '../utility/_UAC.js';
/**
* Automatically executes once after server is ready
* @param {Object} core - Reference to core enviroment object
* @public
* @return {void}
*/
export async function init(core) {
if (typeof core.captchas === 'undefined') {
core.captchas = {};
}
}
/**
* Executes when invoked by a remote client
* @param {Object} env - Enviroment object with references to core, server, socket &amp; payload
* @public
* @return {void}
*/
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;
}
if (!core.captchas[targetChannel]) {
return server.reply({
cmd: 'info',
text: 'Captcha is not enabled.',
channel: socket.channel, // @todo Multichannel
}, socket);
}
core.captchas[targetChannel] = false;
server.broadcast({
cmd: 'info',
text: `Captcha disabled on: ${targetChannel}`,
channel: false, // @todo Multichannel, false for global info
}, { channel: targetChannel, level: isModerator });
return true;
}
/**
* Module meta information
* @public
* @typedef {Object} disablecaptcha/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: 'disablecaptcha',
category: 'moderators',
description: 'Disables the captcha on the channel specified in the channel property, default is current channel',
usage: `
API: { cmd: 'disablecaptcha', channel: '&lt;optional channel, defaults to your current channel' }`,
};
</code></pre>
</article>
</section>
</div>
<nav>
<h2><a href="index.html">Home</a></h2><h3>Modules</h3><ul><li><a href="module-addmod.html">addmod</a></li><li><a href="module-ban.html">ban</a></li><li><a href="module-changecolor.html">changecolor</a></li><li><a href="module-changenick.html">changenick</a></li><li><a href="module-chat.html">chat</a></li><li><a href="module-disablecaptcha.html">disablecaptcha</a></li><li><a href="module-disconnect.html">disconnect</a></li><li><a href="module-dumb.html">dumb</a></li><li><a href="module-emote.html">emote</a></li><li><a href="module-enablecaptcha.html">enablecaptcha</a></li><li><a href="module-forcecolor.html">forcecolor</a></li><li><a href="module-help.html">help</a></li><li><a href="module-invite.html">invite</a></li><li><a href="module-join.html">join</a></li><li><a href="module-kick.html">kick</a></li><li><a href="module-listusers.html">listusers</a></li><li><a href="module-lockroom.html">lockroom</a></li><li><a href="module-morestats.html">morestats</a></li><li><a href="module-ping.html">ping</a></li><li><a href="module-reload.html">reload</a></li><li><a href="module-removemod.html">removemod</a></li><li><a href="module-saveconfig.html">saveconfig</a></li><li><a href="module-session.html">session</a></li><li><a href="module-shout.html">shout</a></li><li><a href="module-socketreply.html">socketreply</a></li><li><a href="module-speak.html">speak</a></li><li><a href="module-stats.html">stats</a></li><li><a href="module-unban.html">unban</a></li><li><a href="module-unbanall.html">unbanall</a></li><li><a href="module-unlockroom.html">unlockroom</a></li><li><a href="module-updateMessage.html">updateMessage</a></li><li><a href="module-whisper.html">whisper</a></li></ul>
</nav>
<br class="clear">
<footer>
Documentation generated by <a href="https://github.com/jsdoc/jsdoc">JSDoc 3.6.10</a> on Fri Dec 29 2023 23:22:35 GMT-0800 (Pacific Standard Time)
</footer>
<script> prettyPrint(); </script>
<script src="scripts/linenumber.js"> </script>
</body>
</html>

View File

@ -0,0 +1,425 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="utf-8">
<title>JSDoc: Source: mod/dumb.js</title>
<script src="scripts/prettify/prettify.js"> </script>
<script src="scripts/prettify/lang-css.js"> </script>
<!--[if lt IE 9]>
<script src="//html5shiv.googlecode.com/svn/trunk/html5.js"></script>
<![endif]-->
<link type="text/css" rel="stylesheet" href="styles/prettify-tomorrow.css">
<link type="text/css" rel="stylesheet" href="styles/jsdoc-default.css">
</head>
<body>
<div id="main">
<h1 class="page-title">Source: mod/dumb.js</h1>
<section>
<article>
<pre class="prettyprint source linenums"><code>/* eslint no-param-reassign: 0 */
/* eslint no-multi-assign: 0 */
/**
* @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.
* @module dumb
*/
import {
isModerator,
} from '../utility/_UAC.js';
import {
findUser,
} from '../utility/_Channels.js';
import {
Errors,
} from '../utility/_Constants.js';
import {
legacyInviteReply,
legacyWhisperReply,
} from '../utility/_LegacyFunctions.js';
/**
* Returns the channel that should be invited to.
* @param {any} channel
* @private
* @return {string}
*/
export function getChannel(channel = undefined) {
if (typeof channel === 'string') {
return channel;
}
return Math.random().toString(36).substr(2, 8);
}
/**
* Check and trim string provided by remote client
* @param {string} text - Subject string
* @private
* @todo Move into utility module
* @return {string|boolean}
*/
const parseText = (text) => {
// verifies user input is text
if (typeof text !== 'string') {
return false;
}
let sanitizedText = text;
// strip newlines from beginning and end
sanitizedText = sanitizedText.replace(/^\s*\n|^\s+$|\n\s*$/g, '');
// replace 3+ newlines with just 2 newlines
sanitizedText = sanitizedText.replace(/\n{3,}/g, '\n\n');
return sanitizedText;
};
/**
* Automatically executes once after server is ready
* @param {Object} core - Reference to core enviroment object
* @public
* @return {void}
*/
export function init(core) {
if (typeof core.muzzledHashes === 'undefined') {
core.muzzledHashes = {};
}
}
/**
* Executes when invoked by a remote client
* @param {Object} env - Enviroment object with references to core, server, socket &amp; payload
* @public
* @return {void}
*/
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);
}
// check user input
if (socket.hcProtocol === 1) {
if (typeof payload.nick !== 'string') {
return true;
}
payload.channel = socket.channel;
} else if (typeof payload.userid !== 'number') {
return true;
}
// find target user
const targetUser = findUser(server, payload);
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);
}
// likely dont need this, muting mods and admins is fine
if (targetUser.level >= socket.level) {
return server.reply({
cmd: 'warn',
text: 'This trick wont work on users of the same level',
id: Errors.Global.PERMISSION,
channel: socket.channel, // @todo Multichannel
}, socket);
}
// store hash in mute list
const record = core.muzzledHashes[targetUser.hash] = {
dumb: true,
};
// store allies if needed
if (payload.allies &amp;&amp; Array.isArray(payload.allies)) {
record.allies = payload.allies;
}
// notify mods
server.broadcast({
cmd: 'info',
text: `${socket.nick}#${socket.trip} muzzled ${targetUser.nick} in ${payload.channel}, userhash: ${targetUser.hash}`,
channel: false, // @todo Multichannel, false for global
}, { level: isModerator });
return true;
}
/**
* Automatically executes once after server is ready to register this modules hooks
* @param {Object} server - Reference to server enviroment object
* @public
* @return {void}
*/
export function initHooks(server) {
server.registerHook('in', 'chat', this.chatCheck.bind(this), 10);
server.registerHook('in', 'invite', this.inviteCheck.bind(this), 10);
server.registerHook('in', 'whisper', this.whisperCheck.bind(this), 10);
}
/**
* Executes every time an incoming chat command is invoked;
* hook incoming chat commands, shadow-prevent chat if they are muzzled
* @param {Object} env - Enviroment object with references to core, server, socket &amp; 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 (core.muzzledHashes[socket.hash]) {
// build fake chat payload
const outgoingPayload = {
cmd: 'chat',
nick: socket.nick, /* @legacy */
uType: socket.uType, /* @legacy */
userid: socket.userid,
channel: socket.channel,
text: payload.text,
level: socket.level,
};
if (socket.trip) {
outgoingPayload.trip = socket.trip;
}
if (socket.color) {
outgoingPayload.color = socket.color;
}
// broadcast to any duplicate connections in channel
server.broadcast(outgoingPayload, { channel: socket.channel, hash: socket.hash });
// broadcast to allies, if any
if (core.muzzledHashes[socket.hash].allies) {
server.broadcast(
outgoingPayload,
{
channel: socket.channel,
nick: core.muzzledHashes[socket.hash].allies,
},
);
}
/**
* Blanket "spam" protection.
* May expose the ratelimiting lines from `chat` and use that
* @todo one day #lazydev
*/
server.police.frisk(socket, 9);
return false;
}
return payload;
}
/**
* Executes every time an incoming chat command is invoked;
* shadow-prevent all invites from muzzled users
* @param {Object} env - Enviroment object with references to core, server, socket &amp; payload
* @public
* @return {(Object|boolean|string)} Object = same/altered payload,
* false = suppress action,
* string = error
*/
export function inviteCheck({
core, server, socket, payload,
}) {
if (core.muzzledHashes[socket.hash]) {
// check for spam
if (server.police.frisk(socket, 2)) {
return server.reply({
cmd: 'warn',
text: 'You are sending invites too fast. Wait a moment before trying again.',
id: Errors.Invite.RATELIMIT,
channel: socket.channel, // @todo Multichannel
}, socket);
}
// verify user input
// if this is a legacy client add missing params to payload
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') {
return true;
}
// @todo Verify this socket is part of payload.channel - multichannel patch
// find target user
const targetUser = findUser(server, payload);
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);
}
// generate common channel
const channel = getChannel(payload.to);
// build invite
const outgoingPayload = {
cmd: 'invite',
channel: socket.channel, // @todo Multichannel
from: socket.userid,
to: targetUser.userid,
inviteChannel: channel,
};
// send invite notice to this client
if (socket.hcProtocol === 1) {
server.reply(legacyInviteReply(outgoingPayload, targetUser.nick), socket);
} else {
server.reply(outgoingPayload, socket);
}
return false;
}
return payload;
}
/**
* Executes every time an incoming chat command is invoked;
* shadow-prevent all whispers from muzzled users
* @param {Object} env - Enviroment object with references to core, server, socket &amp; payload
* @public
* @return {(Object|boolean|string)} Object = same/altered payload,
* false = suppress action,
* string = error
*/
export function whisperCheck({
core, server, socket, payload,
}) {
if (core.muzzledHashes[socket.hash]) {
// if this is a legacy client add missing params to payload
if (socket.hcProtocol === 1) {
payload.channel = socket.channel; // eslint-disable-line no-param-reassign
}
// verify user input
const text = parseText(payload.text);
if (!text) {
// lets not send objects or empty text, yea?
return server.police.frisk(socket, 13);
}
// check for spam
const score = text.length / 83 / 4;
if (server.police.frisk(socket, score)) {
return server.reply({
cmd: 'warn', // @todo Add numeric error code as `id`
text: 'You are sending too much text. Wait a moment and try again.\nPress the up arrow key to restore your last message.',
channel: socket.channel, // @todo Multichannel
}, socket);
}
const targetUser = findUser(server, payload);
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);
}
const outgoingPayload = {
cmd: 'whisper',
channel: socket.channel, // @todo Multichannel
from: socket.userid,
to: targetUser.userid,
text,
};
// send invite notice to this client
if (socket.hcProtocol === 1) {
server.reply(legacyWhisperReply(outgoingPayload, targetUser.nick), socket);
} else {
server.reply(outgoingPayload, socket);
}
targetUser.whisperReply = socket.nick;
return false;
}
return payload;
}
/**
* Module meta information
* @public
* @typedef {Object} dumb/info
* @property {string} name - Module command name
* @property {string} category - Module category name
* @property {string} description - Information about module
* @property {Array} aliases - An array of alternative cmd names
* @property {string} usage - Information about module usage
*/
export const info = {
name: 'dumb',
category: 'moderators',
description: 'Globally shadow mute a connection. Optional allies array will see muted messages.',
aliases: ['muzzle', 'mute'],
usage: `
API: { cmd: 'dumb', nick: '&lt;target nick>', allies: ['&lt;optional nick array>', ...] }`,
};
</code></pre>
</article>
</section>
</div>
<nav>
<h2><a href="index.html">Home</a></h2><h3>Modules</h3><ul><li><a href="module-addmod.html">addmod</a></li><li><a href="module-ban.html">ban</a></li><li><a href="module-changecolor.html">changecolor</a></li><li><a href="module-changenick.html">changenick</a></li><li><a href="module-chat.html">chat</a></li><li><a href="module-disablecaptcha.html">disablecaptcha</a></li><li><a href="module-disconnect.html">disconnect</a></li><li><a href="module-dumb.html">dumb</a></li><li><a href="module-emote.html">emote</a></li><li><a href="module-enablecaptcha.html">enablecaptcha</a></li><li><a href="module-forcecolor.html">forcecolor</a></li><li><a href="module-help.html">help</a></li><li><a href="module-invite.html">invite</a></li><li><a href="module-join.html">join</a></li><li><a href="module-kick.html">kick</a></li><li><a href="module-listusers.html">listusers</a></li><li><a href="module-lockroom.html">lockroom</a></li><li><a href="module-morestats.html">morestats</a></li><li><a href="module-ping.html">ping</a></li><li><a href="module-reload.html">reload</a></li><li><a href="module-removemod.html">removemod</a></li><li><a href="module-saveconfig.html">saveconfig</a></li><li><a href="module-session.html">session</a></li><li><a href="module-shout.html">shout</a></li><li><a href="module-socketreply.html">socketreply</a></li><li><a href="module-speak.html">speak</a></li><li><a href="module-stats.html">stats</a></li><li><a href="module-unban.html">unban</a></li><li><a href="module-unbanall.html">unbanall</a></li><li><a href="module-unlockroom.html">unlockroom</a></li><li><a href="module-updateMessage.html">updateMessage</a></li><li><a href="module-whisper.html">whisper</a></li></ul>
</nav>
<br class="clear">
<footer>
Documentation generated by <a href="https://github.com/jsdoc/jsdoc">JSDoc 3.6.10</a> on Fri Dec 29 2023 23:22:35 GMT-0800 (Pacific Standard Time)
</footer>
<script> prettyPrint(); </script>
<script src="scripts/linenumber.js"> </script>
</body>
</html>

View File

@ -0,0 +1,330 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="utf-8">
<title>JSDoc: Source: mod/enablecaptcha.js</title>
<script src="scripts/prettify/prettify.js"> </script>
<script src="scripts/prettify/lang-css.js"> </script>
<!--[if lt IE 9]>
<script src="//html5shiv.googlecode.com/svn/trunk/html5.js"></script>
<![endif]-->
<link type="text/css" rel="stylesheet" href="styles/prettify-tomorrow.css">
<link type="text/css" rel="stylesheet" href="styles/jsdoc-default.css">
</head>
<body>
<div id="main">
<h1 class="page-title">Source: mod/enablecaptcha.js</h1>
<section>
<article>
<pre class="prettyprint source linenums"><code>/* eslint no-param-reassign: 0 */
/**
* @author Marzavec ( https://github.com/marzavec )
* @summary Enables the captcha
* @version 1.0.0
* @description Enables the captcha on the channel specified in the channel property,
* default is current channel
* @module enablecaptcha
*/
import captcha from 'ascii-captcha';
import {
isTrustedUser,
isModerator,
verifyNickname,
getUserPerms,
} from '../utility/_UAC.js';
import {
canJoinChannel,
} from '../utility/_Channels.js';
import {
upgradeLegacyJoin,
legacyLevelToLabel,
} from '../utility/_LegacyFunctions.js';
import {
Errors,
} from '../utility/_Constants.js';
/**
* Automatically executes once after server is ready
* @param {Object} core - Reference to core enviroment object
* @public
* @return {void}
*/
export async function init(core) {
if (typeof core.captchas === 'undefined') {
core.captchas = {};
}
}
/**
* Executes when invoked by a remote client
* @param {Object} env - Enviroment object with references to core, server, socket &amp; payload
* @public
* @return {void}
*/
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;
}
if (core.captchas[targetChannel]) {
return server.reply({
cmd: 'info',
text: 'Captcha is already enabled.',
channel: socket.channel, // @todo Multichannel
}, socket);
}
core.captchas[targetChannel] = true;
server.broadcast({
cmd: 'info',
text: `Captcha enabled on: ${targetChannel}`,
channel: socket.channel, // @todo Multichannel, false for global info
}, { channel: socket.channel, level: isModerator });
return true;
}
/**
* Automatically executes once after server is ready to register this modules hooks
* @param {Object} server - Reference to server enviroment object
* @public
* @return {void}
*/
export function initHooks(server) {
server.registerHook('in', 'chat', this.chatCheck.bind(this), 5);
server.registerHook('in', 'join', this.joinCheck.bind(this), 5);
}
/**
* Executes every time an incoming chat command is invoked;
* hook incoming chat commands, check if they are answering a captcha
* @param {Object} env - Enviroment object with references to core, server, socket &amp; payload
* @public
* @return {(Object|boolean|string)} Object = same/altered payload,
* false = suppress action,
* string = error
*/
export function chatCheck({
core, server, socket, payload,
}) {
// always verifiy user input
if (typeof payload.text !== 'string') {
return false;
}
if (typeof socket.captcha !== 'undefined') {
if (socket.captcha.awaiting === true) {
if (payload.text === socket.captcha.solution) {
if (typeof socket.captcha.whitelist === 'undefined') {
socket.captcha.whitelist = [];
}
socket.captcha.whitelist.push(socket.captcha.origChannel);
socket.captcha.awaiting = false;
if (socket.hcProtocol === 1) {
core.commands.handleCommand(server, socket, {
cmd: 'join',
nick: `${socket.captcha.origNick}#${socket.captcha.origPass}`,
channel: socket.captcha.origChannel,
});
} else {
core.commands.handleCommand(server, socket, {
cmd: 'join',
nick: socket.captcha.origNick,
pass: socket.captcha.origPass,
channel: socket.captcha.origChannel,
});
}
return false;
}
server.police.frisk(socket, 7);
socket.terminate();
return false;
}
}
return payload;
}
/**
* Executes every time an incoming join command is invoked;
* hook incoming join commands, check if they are joining a captcha protected channel
* @param {Object} env - Enviroment object with references to core, server, socket &amp; payload
* @public
* @return {(Object|boolean|string)} Object = same/altered payload,
* false = suppress action,
* string = error
*/
export function joinCheck({
core, server, socket, payload,
}) {
// check if channel has captcha enabled
if (core.captchas[payload.channel] !== true) {
return payload;
}
// `join` is the legacy entry point, check if it needs to be upgraded
const origPayload = { ...payload };
if (typeof socket.hcProtocol === 'undefined') {
payload = upgradeLegacyJoin(server, socket, payload);
}
// store payload values
const { channel, nick, pass } = payload;
// check if a client is able to join target channel
const mayJoin = canJoinChannel(channel, socket);
if (mayJoin !== true) {
return server.reply({
cmd: 'warn',
text: 'You may not join that channel.',
id: mayJoin,
channel: false, // @todo Multichannel, false for global event
}, socket);
}
// calling socket already in a channel
// @todo multichannel update, will remove
if (typeof socket.channel !== 'undefined') {
return server.reply({
cmd: 'warn', // @todo Remove this
text: 'Joining more than one channel is not currently supported',
id: Errors.Join.ALREADY_JOINED,
channel: false, // @todo Multichannel, false for global event
}, socket);
}
// end todo
// validates the user input for `nick`
if (verifyNickname(nick, socket) !== true) {
return server.reply({
cmd: 'warn',
text: 'Nickname must consist of up to 24 letters, numbers, and underscores',
id: Errors.Join.INVALID_NICK,
channel: false, // @todo Multichannel, false for global event
}, socket);
}
// get trip and level
const { trip, level } = getUserPerms(pass, core.saltKey, core.appConfig.data, channel);
// store the user values
const userInfo = {
nick,
trip,
uType: legacyLevelToLabel(level),
hash: socket.hash,
level,
userid: socket.userid,
isBot: socket.isBot,
color: socket.color,
channel,
};
if (userInfo.uType === 'user') {
if (userInfo.trip == null || isTrustedUser(level) === false) {
if (typeof socket.captcha === 'undefined') {
socket.captcha = {
awaiting: true,
origChannel: payload.channel,
origNick: payload.nick,
origPass: pass,
solution: captcha.generateRandomText(6),
};
server.reply({
cmd: 'warn',
text: 'Enter the following to join (case-sensitive):',
channel: payload.channel, // @todo Multichannel
}, socket);
server.reply({
cmd: 'captcha',
text: captcha.word2Transformedstr(socket.captcha.solution),
channel: payload.channel, // @todo Multichannel
}, socket);
return false;
}
socket.terminate();
return false;
}
}
return origPayload;
}
/**
* Module meta information
* @public
* @typedef {Object} enablecaptcha/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: 'enablecaptcha',
category: 'moderators',
description: 'Enables a captcha in the current channel you are in',
usage: `
API: { cmd: 'enablecaptcha', channel: '&lt;optional channel, defaults to your current channel>' }`,
};
</code></pre>
</article>
</section>
</div>
<nav>
<h2><a href="index.html">Home</a></h2><h3>Modules</h3><ul><li><a href="module-addmod.html">addmod</a></li><li><a href="module-ban.html">ban</a></li><li><a href="module-changecolor.html">changecolor</a></li><li><a href="module-changenick.html">changenick</a></li><li><a href="module-chat.html">chat</a></li><li><a href="module-disablecaptcha.html">disablecaptcha</a></li><li><a href="module-disconnect.html">disconnect</a></li><li><a href="module-dumb.html">dumb</a></li><li><a href="module-emote.html">emote</a></li><li><a href="module-enablecaptcha.html">enablecaptcha</a></li><li><a href="module-forcecolor.html">forcecolor</a></li><li><a href="module-help.html">help</a></li><li><a href="module-invite.html">invite</a></li><li><a href="module-join.html">join</a></li><li><a href="module-kick.html">kick</a></li><li><a href="module-listusers.html">listusers</a></li><li><a href="module-lockroom.html">lockroom</a></li><li><a href="module-morestats.html">morestats</a></li><li><a href="module-ping.html">ping</a></li><li><a href="module-reload.html">reload</a></li><li><a href="module-removemod.html">removemod</a></li><li><a href="module-saveconfig.html">saveconfig</a></li><li><a href="module-session.html">session</a></li><li><a href="module-shout.html">shout</a></li><li><a href="module-socketreply.html">socketreply</a></li><li><a href="module-speak.html">speak</a></li><li><a href="module-stats.html">stats</a></li><li><a href="module-unban.html">unban</a></li><li><a href="module-unbanall.html">unbanall</a></li><li><a href="module-unlockroom.html">unlockroom</a></li><li><a href="module-updateMessage.html">updateMessage</a></li><li><a href="module-whisper.html">whisper</a></li></ul>
</nav>
<br class="clear">
<footer>
Documentation generated by <a href="https://github.com/jsdoc/jsdoc">JSDoc 3.6.10</a> on Fri Dec 29 2023 23:22:35 GMT-0800 (Pacific Standard Time)
</footer>
<script> prettyPrint(); </script>
<script src="scripts/linenumber.js"> </script>
</body>
</html>

View File

@ -0,0 +1,250 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="utf-8">
<title>JSDoc: Source: mod/forcecolor.js</title>
<script src="scripts/prettify/prettify.js"> </script>
<script src="scripts/prettify/lang-css.js"> </script>
<!--[if lt IE 9]>
<script src="//html5shiv.googlecode.com/svn/trunk/html5.js"></script>
<![endif]-->
<link type="text/css" rel="stylesheet" href="styles/prettify-tomorrow.css">
<link type="text/css" rel="stylesheet" href="styles/jsdoc-default.css">
</head>
<body>
<div id="main">
<h1 class="page-title">Source: mod/forcecolor.js</h1>
<section>
<article>
<pre class="prettyprint source linenums"><code>/**
* @author Marzavec ( https://github.com/marzavec )
* @summary Color a user
* @version 1.0.0
* @description Forces a user nick to become a certain color
* @module forcecolor
*/
import {
isModerator,
getUserDetails,
} from '../utility/_UAC.js';
import {
Errors,
} from '../utility/_Constants.js';
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);
/**
* Executes when invoked by a remote client
* @param {Object} env - Enviroment object with references to core, server, socket &amp; payload
* @public
* @return {void}
*/
export async function run({
server, socket, payload,
}) {
// increase rate limit chance and ignore if not admin or mod
if (!isModerator(socket.level)) {
return server.police.frisk(socket, 10);
}
const { channel } = socket;
if (typeof payload.channel === 'undefined') {
payload.channel = channel;
}
// check user input
if (typeof payload.nick !== 'string') {
return true;
}
if (typeof payload.color !== 'string') {
return true;
}
// make sure requested nickname meets standards
const newColor = payload.color.trim().toUpperCase().replace(/#/g, '');
if (newColor !== 'RESET' &amp;&amp; !verifyColor(newColor)) {
return server.reply({
cmd: 'warn',
text: 'Invalid color! Color must be in hex value',
channel, // @todo Multichannel
}, socket);
}
// find target user
const targetUser = findUser(server, payload);
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);
}
// TODO: Change this uType to use level / uac
// i guess coloring mods or admins isn't the best idea?
if (targetUser.uType !== 'user') {
return true;
}
if (newColor === 'RESET') {
targetUser.color = false;
} else {
targetUser.color = newColor;
}
// build update notice with new color
const updateNotice = {
...getUserDetails(targetUser),
...{
cmd: 'updateUser',
channel: socket.channel, // @todo Multichannel
},
};
// notify channel that the user has changed their name
// @todo this should be sent to every channel the user is in (multichannel)
server.broadcast(updateNotice, { channel: socket.channel });
return true;
}
/**
* Automatically executes once after server is ready to register this modules hooks
* @param {Object} server - Reference to server enviroment object
* @public
* @return {void}
*/
export function initHooks(server) {
server.registerHook('in', 'chat', this.colorCheck.bind(this), 20);
}
/**
* Executes every time an incoming chat command is invoked;
* hooks chat commands checking for /forcecolor
* @param {Object} env - Enviroment object with references to core, server, socket &amp; payload
* @public
* @return {(Object|boolean|string)} Object = same/altered payload,
* false = suppress action,
* string = error
*/
export function colorCheck({
core, server, socket, payload,
}) {
if (typeof payload.text !== 'string') {
return false;
}
if (payload.text.startsWith('/forcecolor ')) {
const input = payload.text.split(' ');
// If there is no nickname target parameter
if (input[1] === undefined) {
server.reply({
cmd: 'warn',
text: 'Refer to `/help forcecolor` for instructions on how to use this command.',
channel: socket.channel, // @todo Multichannel
}, socket);
return false;
}
if (input[2] === undefined) {
server.reply({
cmd: 'warn',
text: 'Refer to `/help forcecolor` for instructions on how to use this command.',
channel: socket.channel, // @todo Multichannel
}, socket);
return false;
}
const target = input[1].replace(/@/g, '');
this.run({
core,
server,
socket,
payload: {
cmd: 'forcecolor',
nick: target,
color: input[2],
},
});
return false;
}
return payload;
}
/**
* The following payload properties are required to invoke this module:
* "nick", "color"
* @public
* @typedef {Array} forcecolor/requiredData
*/
export const requiredData = ['nick', 'color'];
/**
* Module meta information
* @public
* @typedef {Object} forcecolor/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: 'forcecolor',
category: 'moderators',
description: 'Forces a user nick to become a certain color',
usage: `
API: { cmd: 'forcecolor', nick: '&lt;target nick>', color: '&lt;color as hex>' }
Text: /forcecolor &lt;target nick> &lt;color as hex>`,
};
</code></pre>
</article>
</section>
</div>
<nav>
<h2><a href="index.html">Home</a></h2><h3>Modules</h3><ul><li><a href="module-addmod.html">addmod</a></li><li><a href="module-ban.html">ban</a></li><li><a href="module-changecolor.html">changecolor</a></li><li><a href="module-changenick.html">changenick</a></li><li><a href="module-chat.html">chat</a></li><li><a href="module-disablecaptcha.html">disablecaptcha</a></li><li><a href="module-disconnect.html">disconnect</a></li><li><a href="module-dumb.html">dumb</a></li><li><a href="module-emote.html">emote</a></li><li><a href="module-enablecaptcha.html">enablecaptcha</a></li><li><a href="module-forcecolor.html">forcecolor</a></li><li><a href="module-help.html">help</a></li><li><a href="module-invite.html">invite</a></li><li><a href="module-join.html">join</a></li><li><a href="module-kick.html">kick</a></li><li><a href="module-listusers.html">listusers</a></li><li><a href="module-lockroom.html">lockroom</a></li><li><a href="module-morestats.html">morestats</a></li><li><a href="module-ping.html">ping</a></li><li><a href="module-reload.html">reload</a></li><li><a href="module-removemod.html">removemod</a></li><li><a href="module-saveconfig.html">saveconfig</a></li><li><a href="module-session.html">session</a></li><li><a href="module-shout.html">shout</a></li><li><a href="module-socketreply.html">socketreply</a></li><li><a href="module-speak.html">speak</a></li><li><a href="module-stats.html">stats</a></li><li><a href="module-unban.html">unban</a></li><li><a href="module-unbanall.html">unbanall</a></li><li><a href="module-unlockroom.html">unlockroom</a></li><li><a href="module-updateMessage.html">updateMessage</a></li><li><a href="module-whisper.html">whisper</a></li></ul>
</nav>
<br class="clear">
<footer>
Documentation generated by <a href="https://github.com/jsdoc/jsdoc">JSDoc 3.6.10</a> on Fri Dec 29 2023 23:22:35 GMT-0800 (Pacific Standard Time)
</footer>
<script> prettyPrint(); </script>
<script src="scripts/linenumber.js"> </script>
</body>
</html>

View File

@ -0,0 +1,203 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="utf-8">
<title>JSDoc: Source: mod/kick.js</title>
<script src="scripts/prettify/prettify.js"> </script>
<script src="scripts/prettify/lang-css.js"> </script>
<!--[if lt IE 9]>
<script src="//html5shiv.googlecode.com/svn/trunk/html5.js"></script>
<![endif]-->
<link type="text/css" rel="stylesheet" href="styles/prettify-tomorrow.css">
<link type="text/css" rel="stylesheet" href="styles/jsdoc-default.css">
</head>
<body>
<div id="main">
<h1 class="page-title">Source: mod/kick.js</h1>
<section>
<article>
<pre class="prettyprint source linenums"><code>/**
* @author Marzavec ( https://github.com/marzavec )
* @summary Give da boot
* @version 1.0.0
* @description Silently forces target client(s) into another channel
* @module kick
*/
import {
isModerator,
levels,
getUserDetails,
} from '../utility/_UAC.js';
import {
Errors,
} from '../utility/_Constants.js';
import {
findUsers,
} from '../utility/_Channels.js';
/**
* Executes when invoked by a remote client
* @param {Object} env - Enviroment object with references to core, server, socket &amp; payload
* @public
* @return {void}
*/
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);
}
// check user input
if (socket.hcProtocol === 1) {
if (typeof payload.nick !== 'string') {
if (typeof payload.nick !== 'object' &amp;&amp; !Array.isArray(payload.nick)) {
return true;
}
}
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' &amp;&amp; !Array.isArray(payload.userid)) {
return true;
}
}
// find target user(s)
const badClients = findUsers(server, payload);
if (badClients.length === 0) {
return server.reply({
cmd: 'warn',
text: 'Could not find user(s) in that channel',
id: Errors.Global.UNKNOWN_USER,
channel: socket.channel, // @todo Multichannel
}, socket);
}
// check if found targets are kickable, add them to the list if they are
const kicked = [];
for (let i = 0, j = badClients.length; i &lt; j; i += 1) {
if (badClients[i].level >= socket.level) {
server.reply({
cmd: 'warn',
text: 'Cannot kick other users with the same level, how rude',
id: Errors.Global.PERMISSION,
channel: socket.channel, // @todo Multichannel
}, socket);
} else {
kicked.push(badClients[i]);
}
}
if (kicked.length === 0) {
return true;
}
let destChannel;
if (typeof payload.to === 'string' &amp;&amp; !!payload.to.trim()) {
destChannel = payload.to;
} else {
destChannel = Math.random().toString(36).substr(2, 8);
}
// 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 &lt; kicked.length; i += 1) {
server.broadcast({
...getUserDetails(kicked[i]),
...{
cmd: 'onlineAdd',
channel: destChannel, // @todo Multichannel
},
}, { channel: destChannel });
}
// Move all kicked clients to the new channel
for (let i = 0; i &lt; kicked.length; i += 1) {
// @todo multi-channel update
kicked[i].channel = destChannel;
server.broadcast({
cmd: 'info',
text: `${kicked[i].nick} was banished to ?${destChannel}`,
channel: socket.channel, // @todo Multichannel
}, { channel: socket.channel, level: isModerator });
console.log(`${socket.nick} [${socket.trip}] kicked ${kicked[i].nick} in ${socket.channel} to ${destChannel} `);
}
// broadcast client leave event
for (let i = 0, j = kicked.length; i &lt; j; i += 1) {
server.broadcast({
cmd: 'onlineRemove',
userid: kicked[i].userid,
nick: kicked[i].nick,
channel: socket.channel, // @todo Multichannel
}, { channel: socket.channel });
}
// publicly broadcast kick event
server.broadcast({
cmd: 'info',
text: `Kicked ${kicked.map((k) => k.nick).join(', ')}`,
channel: socket.channel, // @todo Multichannel
}, { channel: socket.channel, level: (level) => level &lt; levels.moderator });
// stats are fun
core.stats.increment('users-kicked', kicked.length);
return true;
}
/**
* Module meta information
* @public
* @typedef {Object} kick/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: 'kick',
category: 'moderators',
description: 'Silently forces target client(s) into another channel. `nick` may be string or array of strings',
usage: `
API: { cmd: 'kick', nick: '&lt;target nick>', to: '&lt;optional target channel>' }`,
};
</code></pre>
</article>
</section>
</div>
<nav>
<h2><a href="index.html">Home</a></h2><h3>Modules</h3><ul><li><a href="module-addmod.html">addmod</a></li><li><a href="module-ban.html">ban</a></li><li><a href="module-changecolor.html">changecolor</a></li><li><a href="module-changenick.html">changenick</a></li><li><a href="module-chat.html">chat</a></li><li><a href="module-disablecaptcha.html">disablecaptcha</a></li><li><a href="module-disconnect.html">disconnect</a></li><li><a href="module-dumb.html">dumb</a></li><li><a href="module-emote.html">emote</a></li><li><a href="module-enablecaptcha.html">enablecaptcha</a></li><li><a href="module-forcecolor.html">forcecolor</a></li><li><a href="module-help.html">help</a></li><li><a href="module-invite.html">invite</a></li><li><a href="module-join.html">join</a></li><li><a href="module-kick.html">kick</a></li><li><a href="module-listusers.html">listusers</a></li><li><a href="module-lockroom.html">lockroom</a></li><li><a href="module-morestats.html">morestats</a></li><li><a href="module-ping.html">ping</a></li><li><a href="module-reload.html">reload</a></li><li><a href="module-removemod.html">removemod</a></li><li><a href="module-saveconfig.html">saveconfig</a></li><li><a href="module-session.html">session</a></li><li><a href="module-shout.html">shout</a></li><li><a href="module-socketreply.html">socketreply</a></li><li><a href="module-speak.html">speak</a></li><li><a href="module-stats.html">stats</a></li><li><a href="module-unban.html">unban</a></li><li><a href="module-unbanall.html">unbanall</a></li><li><a href="module-unlockroom.html">unlockroom</a></li><li><a href="module-updateMessage.html">updateMessage</a></li><li><a href="module-whisper.html">whisper</a></li></ul>
</nav>
<br class="clear">
<footer>
Documentation generated by <a href="https://github.com/jsdoc/jsdoc">JSDoc 3.6.10</a> on Fri Dec 29 2023 23:22:35 GMT-0800 (Pacific Standard Time)
</footer>
<script> prettyPrint(); </script>
<script src="scripts/linenumber.js"> </script>
</body>
</html>

View File

@ -0,0 +1,382 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="utf-8">
<title>JSDoc: Source: mod/lockroom.js</title>
<script src="scripts/prettify/prettify.js"> </script>
<script src="scripts/prettify/lang-css.js"> </script>
<!--[if lt IE 9]>
<script src="//html5shiv.googlecode.com/svn/trunk/html5.js"></script>
<![endif]-->
<link type="text/css" rel="stylesheet" href="styles/prettify-tomorrow.css">
<link type="text/css" rel="stylesheet" href="styles/jsdoc-default.css">
</head>
<body>
<div id="main">
<h1 class="page-title">Source: mod/lockroom.js</h1>
<section>
<article>
<pre class="prettyprint source linenums"><code>/* eslint no-param-reassign: 0 */
/**
* @author Marzavec ( https://github.com/marzavec )
* @summary Locks the channel
* @version 1.0.0
* @description Locks a channel preventing default levels from joining
* @module lockroom
*/
import {
isTrustedUser,
isModerator,
verifyNickname,
getUserPerms,
} from '../utility/_UAC.js';
import {
upgradeLegacyJoin,
legacyLevelToLabel,
} from '../utility/_LegacyFunctions.js';
import {
Errors,
} from '../utility/_Constants.js';
import {
canJoinChannel,
} from '../utility/_Channels.js';
const danteQuotes = [
'Do not be afraid; our fate cannot be taken from us; it is a gift.',
'In the middle of the journey of our life I found myself within a dark woods where the straight way was lost.',
'There is no greater sorrow then to recall our times of joy in wretchedness.',
'They yearn for what they fear for.',
'Through me you go into a city of weeping; through me you go into eternal pain; through me you go amongst the lost people',
'From there we came outside and saw the stars',
'But the stars that marked our starting fall away. We must go deeper into greater pain, for it is not permitted that we stay.',
'Hope not ever to see Heaven. I have come to lead you to the other shore; into eternal darkness; into fire and into ice.',
'As little flowers, which the chill of night has bent and huddled, when the white sun strikes, grow straight and open fully on their stems, so did I, too, with my exhausted force.',
'At grief so deep the tongue must wag in vain; the language of our sense and memory lacks the vocabulary of such pain.',
'Thence we came forth to rebehold the stars.',
'He is, most of all, l\'amor che move il sole e l\'altre stelle.',
'The poets leave hell and again behold the stars.',
'One ought to be afraid of nothing other then things possessed of power to do us harm, but things innoucuous need not be feared.',
'As phantoms frighten beasts when shadows fall.',
'We were men once, though we\'ve become trees',
'Here pity only lives when it is dead',
'Lasciate ogne speranza, voi ch\'intrate.',
'There is no greater sorrow than thinking back upon a happy time in misery',
'My thoughts were full of other things When I wandered off the path.',
];
/**
* Automatically executes once after server is ready
* @param {Object} core - Reference to core enviroment object
* @public
* @return {void}
*/
export async function init(core) {
if (typeof core.locked === 'undefined') {
core.locked = {};
}
}
/**
* Executes when invoked by a remote client
* @param {Object} env - Enviroment object with references to core, server, socket &amp; payload
* @public
* @return {void}
*/
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;
}
if (core.locked[targetChannel]) {
return server.reply({
cmd: 'info',
text: 'Channel is already locked.',
channel: socket.channel, // @todo Multichannel
}, socket);
}
// apply lock flag to channel list
core.locked[targetChannel] = true;
// inform mods
server.broadcast({
cmd: 'info',
text: `Channel: ?${targetChannel} lock enabled by [${socket.trip}]${socket.nick}`,
channel: false, // @todo Multichannel, false for global info
}, { level: isModerator });
return true;
}
/**
* Automatically executes once after server is ready to register this modules hooks
* @param {Object} server - Reference to server enviroment object
* @public
* @return {void}
*/
export function initHooks(server) {
server.registerHook('in', 'changenick', this.changeNickCheck.bind(this), 1);
server.registerHook('in', 'whisper', this.whisperCheck.bind(this), 1);
server.registerHook('in', 'chat', this.chatCheck.bind(this), 1);
server.registerHook('in', 'invite', this.inviteCheck.bind(this), 1);
server.registerHook('in', 'join', this.joinCheck.bind(this), 1);
}
/**
* Executes every time an incoming changenick command is invoked;
* hook incoming changenick commands, reject them if the channel is 'purgatory'
* @param {Object} env - Enviroment object with references to core, server, socket &amp; payload
* @public
* @return {(Object|boolean|string)} Object = same/altered payload,
* false = suppress action,
* string = error
*/
export function changeNickCheck({
socket, payload,
}) {
if (socket.channel === 'purgatory') { // @todo Multichannel update
return false;
}
return payload;
}
/**
* Executes every time an incoming whisper command is invoked;
* hook incoming whisper commands, reject them if the channel is 'purgatory'
* @param {Object} env - Enviroment object with references to core, server, socket &amp; payload
* @public
* @return {(Object|boolean|string)} Object = same/altered payload,
* false = suppress action,
* string = error
*/
export function whisperCheck({
socket, payload,
}) {
if (socket.channel === 'purgatory') { // @todo Multichannel update
return false;
}
return payload;
}
/**
* Executes every time an incoming chat command is invoked;
* hook incoming chat commands, reject them if the channel is 'purgatory'
* @param {Object} env - Enviroment object with references to core, server, socket &amp; payload
* @public
* @return {(Object|boolean|string)} Object = same/altered payload,
* false = suppress action,
* string = error
*/
export function chatCheck({
socket, payload,
}) {
if (socket.channel === 'purgatory') {
if (isModerator(socket.level)) {
return payload;
}
return false;
}
return payload;
}
/**
* Executes every time an incoming invite command is invoked;
* hook incoming invite commands, reject them if the channel is 'purgatory'
* @param {Object} env - Enviroment object with references to core, server, socket &amp; payload
* @public
* @return {(Object|boolean|string)} Object = same/altered payload,
* false = suppress action,
* string = error
*/
export function inviteCheck({
socket, payload,
}) {
if (socket.channel === 'purgatory') {
return false;
}
return payload;
}
/**
* Executes every time an incoming join command is invoked;
* hook incoming join commands, shunt them to purgatory if needed
* @param {Object} env - Enviroment object with references to core, server, socket &amp; payload
* @public
* @return {(Object|boolean|string)} Object = same/altered payload,
* false = suppress action,
* string = error
*/
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 (payload.channel !== 'purgatory') {
return payload;
}
}
// `join` is the legacy entry point, check if it needs to be upgraded
if (typeof socket.hcProtocol === 'undefined') {
payload = upgradeLegacyJoin(server, socket, payload);
}
// store payload values
const { channel, nick, pass } = payload;
// check if a client is able to join target channel
const mayJoin = canJoinChannel(channel, socket);
if (mayJoin !== true) {
return server.reply({
cmd: 'warn',
text: 'You may not join that channel.',
id: mayJoin,
channel: false, // @todo Multichannel, false for global event
}, socket);
}
// calling socket already in a channel
// @todo multichannel update, will remove
if (typeof socket.channel !== 'undefined') {
return server.reply({
cmd: 'warn', // @todo Remove this
text: 'Joining more than one channel is not currently supported',
id: Errors.Join.ALREADY_JOINED,
channel: false, // @todo Multichannel, false for global event
}, socket);
}
// end todo
// validates the user input for `nick`
if (verifyNickname(nick, socket) !== true) {
return server.reply({
cmd: 'warn',
text: 'Nickname must consist of up to 24 letters, numbers, and underscores',
id: Errors.Join.INVALID_NICK,
channel: false, // @todo Multichannel, false for global event
}, socket);
}
// get trip and level
const { trip, level } = getUserPerms(pass, core.saltKey, core.appConfig.data, channel);
// store the user values
const userInfo = {
nick,
trip,
uType: legacyLevelToLabel(level),
hash: socket.hash,
level,
userid: socket.userid,
isBot: socket.isBot,
color: socket.color,
channel,
};
// check if trip is allowed
if (userInfo.uType === 'user') {
if (userInfo.trip == null || isTrustedUser(level) === false) {
const origNick = userInfo.nick;
const origChannel = payload.channel;
// not allowed, shunt to purgatory
payload.channel = 'purgatory';
// lost souls have no names
if (origChannel === 'purgatory') {
// someone is pulling a Dante
payload.nick = `Dante_${Math.random().toString(36).substr(2, 8)}`;
} else {
payload.nick = `${Math.random().toString(36).substr(2, 8)}${Math.random().toString(36).substr(2, 8)}`;
}
setTimeout(() => {
server.reply({
cmd: 'info',
text: danteQuotes[Math.floor(Math.random() * danteQuotes.length)],
channel: 'purgatory', // @todo Multichannel
}, socket);
}, 100);
server.broadcast({
cmd: 'info',
text: `${payload.nick} is: ${origNick}\ntrip: ${userInfo.trip || 'none'}\ntried to join: ?${origChannel}\nhash: ${userInfo.hash}`,
channel: 'purgatory', // @todo Multichannel, false for global info
}, { channel: 'purgatory', level: isModerator });
}
}
return payload;
}
/**
* Module meta information
* @public
* @typedef {Object} kick/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: 'lockroom',
category: 'moderators',
description: 'Locks a channel preventing default levels from joining',
usage: `
API: { cmd: 'lockroom', channel: '&lt;optional channel, defaults to your current channel>' }`,
};
</code></pre>
</article>
</section>
</div>
<nav>
<h2><a href="index.html">Home</a></h2><h3>Modules</h3><ul><li><a href="module-addmod.html">addmod</a></li><li><a href="module-ban.html">ban</a></li><li><a href="module-changecolor.html">changecolor</a></li><li><a href="module-changenick.html">changenick</a></li><li><a href="module-chat.html">chat</a></li><li><a href="module-disablecaptcha.html">disablecaptcha</a></li><li><a href="module-disconnect.html">disconnect</a></li><li><a href="module-dumb.html">dumb</a></li><li><a href="module-emote.html">emote</a></li><li><a href="module-enablecaptcha.html">enablecaptcha</a></li><li><a href="module-forcecolor.html">forcecolor</a></li><li><a href="module-help.html">help</a></li><li><a href="module-invite.html">invite</a></li><li><a href="module-join.html">join</a></li><li><a href="module-kick.html">kick</a></li><li><a href="module-listusers.html">listusers</a></li><li><a href="module-lockroom.html">lockroom</a></li><li><a href="module-morestats.html">morestats</a></li><li><a href="module-ping.html">ping</a></li><li><a href="module-reload.html">reload</a></li><li><a href="module-removemod.html">removemod</a></li><li><a href="module-saveconfig.html">saveconfig</a></li><li><a href="module-session.html">session</a></li><li><a href="module-shout.html">shout</a></li><li><a href="module-socketreply.html">socketreply</a></li><li><a href="module-speak.html">speak</a></li><li><a href="module-stats.html">stats</a></li><li><a href="module-unban.html">unban</a></li><li><a href="module-unbanall.html">unbanall</a></li><li><a href="module-unlockroom.html">unlockroom</a></li><li><a href="module-updateMessage.html">updateMessage</a></li><li><a href="module-whisper.html">whisper</a></li></ul>
</nav>
<br class="clear">
<footer>
Documentation generated by <a href="https://github.com/jsdoc/jsdoc">JSDoc 3.6.10</a> on Fri Dec 29 2023 23:22:35 GMT-0800 (Pacific Standard Time)
</footer>
<script> prettyPrint(); </script>
<script src="scripts/linenumber.js"> </script>
</body>
</html>

View File

@ -0,0 +1,158 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="utf-8">
<title>JSDoc: Source: mod/speak.js</title>
<script src="scripts/prettify/prettify.js"> </script>
<script src="scripts/prettify/lang-css.js"> </script>
<!--[if lt IE 9]>
<script src="//html5shiv.googlecode.com/svn/trunk/html5.js"></script>
<![endif]-->
<link type="text/css" rel="stylesheet" href="styles/prettify-tomorrow.css">
<link type="text/css" rel="stylesheet" href="styles/jsdoc-default.css">
</head>
<body>
<div id="main">
<h1 class="page-title">Source: mod/speak.js</h1>
<section>
<article>
<pre class="prettyprint source linenums"><code>/* eslint no-param-reassign: 0 */
/**
* @author OpSimple ( https://github.com/OpSimple )
* @summary Unmuzzle a user
* @version 1.0.0
* @description Pardon a dumb user to be able to speak again
* @module speak
*/
import {
isModerator,
} from '../utility/_UAC.js';
/**
* Automatically executes once after server is ready
* @param {Object} core - Reference to core enviroment object
* @public
* @return {void}
*/
export function init(core) {
if (typeof core.muzzledHashes === 'undefined') {
core.muzzledHashes = {};
}
}
/**
* Executes when invoked by a remote client
* @param {Object} env - Enviroment object with references to core, server, socket &amp; payload
* @public
* @return {void}
*/
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);
}
// check user input
if (typeof payload.ip !== 'string' &amp;&amp; typeof payload.hash !== 'string') {
return server.reply({
cmd: 'warn', // @todo Add numeric error code as `id`
text: "hash:'targethash' or ip:'1.2.3.4' is required",
channel: socket.channel, // @todo Multichannel
}, socket);
}
if (typeof payload.ip === 'string') {
if (payload.ip === '*') {
core.muzzledHashes = {};
return server.broadcast({
cmd: 'info',
text: `${socket.nick} unmuzzled all users`,
channel: false, // @todo Multichannel, false for global
}, { level: isModerator });
}
} else if (payload.hash === '*') {
core.muzzledHashes = {};
return server.broadcast({
cmd: 'info',
text: `${socket.nick} unmuzzled all users`,
channel: false, // @todo Multichannel, false for global
}, { level: isModerator });
}
// find target &amp; remove mute status
let target;
if (typeof payload.ip === 'string') {
target = server.getSocketHash(payload.ip);
} else {
target = payload.hash;
}
delete core.muzzledHashes[target];
// notify mods
server.broadcast({
cmd: 'info',
text: `${socket.nick}#${socket.trip} unmuzzled : ${target}`,
channel: false, // @todo Multichannel, false for global
}, { level: isModerator });
return true;
}
/**
* Module meta information
* @public
* @typedef {Object} speak/info
* @property {string} name - Module command name
* @property {string} category - Module category name
* @property {string} description - Information about module
* @property {Array} aliases - An array of alternative cmd names
* @property {string} usage - Information about module usage
*/
export const info = {
name: 'speak',
category: 'moderators',
description: 'Pardon a dumb user to be able to speak again',
aliases: ['unmuzzle', 'unmute'],
usage: `
API: { cmd: 'speak', ip/hash: '&lt;target ip or hash>' }`,
};
</code></pre>
</article>
</section>
</div>
<nav>
<h2><a href="index.html">Home</a></h2><h3>Modules</h3><ul><li><a href="module-addmod.html">addmod</a></li><li><a href="module-ban.html">ban</a></li><li><a href="module-changecolor.html">changecolor</a></li><li><a href="module-changenick.html">changenick</a></li><li><a href="module-chat.html">chat</a></li><li><a href="module-disablecaptcha.html">disablecaptcha</a></li><li><a href="module-disconnect.html">disconnect</a></li><li><a href="module-dumb.html">dumb</a></li><li><a href="module-emote.html">emote</a></li><li><a href="module-enablecaptcha.html">enablecaptcha</a></li><li><a href="module-forcecolor.html">forcecolor</a></li><li><a href="module-help.html">help</a></li><li><a href="module-invite.html">invite</a></li><li><a href="module-join.html">join</a></li><li><a href="module-kick.html">kick</a></li><li><a href="module-listusers.html">listusers</a></li><li><a href="module-lockroom.html">lockroom</a></li><li><a href="module-morestats.html">morestats</a></li><li><a href="module-ping.html">ping</a></li><li><a href="module-reload.html">reload</a></li><li><a href="module-removemod.html">removemod</a></li><li><a href="module-saveconfig.html">saveconfig</a></li><li><a href="module-session.html">session</a></li><li><a href="module-shout.html">shout</a></li><li><a href="module-socketreply.html">socketreply</a></li><li><a href="module-speak.html">speak</a></li><li><a href="module-stats.html">stats</a></li><li><a href="module-unban.html">unban</a></li><li><a href="module-unbanall.html">unbanall</a></li><li><a href="module-unlockroom.html">unlockroom</a></li><li><a href="module-updateMessage.html">updateMessage</a></li><li><a href="module-whisper.html">whisper</a></li></ul>
</nav>
<br class="clear">
<footer>
Documentation generated by <a href="https://github.com/jsdoc/jsdoc">JSDoc 3.6.10</a> on Fri Dec 29 2023 23:22:35 GMT-0800 (Pacific Standard Time)
</footer>
<script> prettyPrint(); </script>
<script src="scripts/linenumber.js"> </script>
</body>
</html>

Some files were not shown because too many files have changed in this diff Show More