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

Impl updateMessage command

This commit is contained in:
MinusGix 2023-06-01 13:16:12 -05:00
parent 9982207983
commit 7c612871f4
4 changed files with 276 additions and 36 deletions

View File

@ -15,7 +15,7 @@ var markdownOptions = {
langPrefix: '', langPrefix: '',
linkify: true, linkify: true,
linkTarget: '_blank" rel="noreferrer', linkTarget: '_blank" rel="noreferrer',
typographer: true, typographer: true,
quotes: `""''`, quotes: `""''`,
doHighlight: true, doHighlight: true,
@ -25,12 +25,12 @@ var markdownOptions = {
if (lang && hljs.getLanguage(lang)) { if (lang && hljs.getLanguage(lang)) {
try { try {
return hljs.highlight(lang, str).value; return hljs.highlight(lang, str).value;
} catch (__) {} } catch (__) { }
} }
try { try {
return hljs.highlightAuto(str).value; return hljs.highlightAuto(str).value;
} catch (__) {} } catch (__) { }
return ''; return '';
} }
@ -67,20 +67,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"><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) { 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 title = tokens[idx].title ? (' title="' + Remarkable.utils.escapeHtml(Remarkable.utils.replaceEntities(tokens[idx].title)) + '"') : '';
var target = options.linkTarget ? (' target="' + options.linkTarget + '"') : ''; var target = options.linkTarget ? (' target="' + options.linkTarget + '"') : '';
return '<a rel="noreferrer" onclick="return verifyLink(this)" href="' + Remarkable.utils.escapeHtml(tokens[idx].href) + '"' + title + target + '>'; 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); tokens[idx].content = Remarkable.utils.escapeHtml(tokens[idx].content);
if (tokens[idx].content.indexOf('?') !== -1) { 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 channelLink = Remarkable.utils.escapeHtml(Remarkable.utils.replaceEntities(match.trim()));
var whiteSpace = ''; var whiteSpace = '';
if (match[0] !== '?') { if (match[0] !== '?') {
@ -90,7 +90,7 @@ md.renderer.rules.text = function(tokens, idx) {
}); });
} }
return tokens[idx].content; return tokens[idx].content;
}; };
md.use(remarkableKatex); md.use(remarkableKatex);
@ -164,6 +164,34 @@ var myChannel = window.location.search.replace(/^\?/, '');
var lastSent = [""]; var lastSent = [""];
var lastSentPos = 0; 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 **/ /** Notification switch and local storage behavior **/
var notifySwitch = document.getElementById("notify-switch") var notifySwitch = document.getElementById("notify-switch")
var notifySetting = localStorageGet("notify-api") var notifySetting = localStorageGet("notify-api")
@ -364,7 +392,59 @@ var COMMANDS = {
if (ignoredUsers.indexOf(args.nick) >= 0) { if (ignoredUsers.indexOf(args.nick) >= 0) {
return; 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) {
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) { info: function (args) {
@ -518,6 +598,8 @@ function pushMessage(args) {
unread += 1; unread += 1;
updateTitle(); updateTitle();
return messageEl;
} }
function insertAtCursor(text) { function insertAtCursor(text) {
@ -705,8 +787,8 @@ $('#sidebar').onmouseleave = document.ontouchstart = function (event) {
var e = event.toElement || event.relatedTarget; var e = event.toElement || event.relatedTarget;
try { try {
if (e.parentNode == this || e == this) { if (e.parentNode == this || e == this) {
return; return;
} }
} catch (e) { return; } } catch (e) { return; }
if (!$('#pin-sidebar').checked) { if (!$('#pin-sidebar').checked) {
@ -734,8 +816,8 @@ if (localStorageGet('joined-left') == 'false') {
if (localStorageGet('parse-latex') == 'false') { if (localStorageGet('parse-latex') == 'false') {
$('#parse-latex').checked = false; $('#parse-latex').checked = false;
md.inline.ruler.disable([ 'katex' ]); md.inline.ruler.disable(['katex']);
md.block.ruler.disable([ 'katex' ]); md.block.ruler.disable(['katex']);
} }
$('#pin-sidebar').onchange = function (e) { $('#pin-sidebar').onchange = function (e) {
@ -750,11 +832,11 @@ $('#parse-latex').onchange = function (e) {
var enabled = !!e.target.checked; var enabled = !!e.target.checked;
localStorageSet('parse-latex', enabled); localStorageSet('parse-latex', enabled);
if (enabled) { if (enabled) {
md.inline.ruler.enable([ 'katex' ]); md.inline.ruler.enable(['katex']);
md.block.ruler.enable([ 'katex' ]); md.block.ruler.enable(['katex']);
} else { } else {
md.inline.ruler.disable([ 'katex' ]); md.inline.ruler.disable(['katex']);
md.block.ruler.disable([ 'katex' ]); md.block.ruler.disable(['katex']);
} }
} }

View File

@ -6,33 +6,64 @@
* @module chat * @module chat
*/ */
import { parseText } from '../utility/_Text.js';
import { import {
isAdmin, isAdmin,
isModerator, isModerator,
} from '../utility/_UAC.js'; } from '../utility/_UAC.js';
export const MAX_MESSAGE_ID_LENGTH = 6;
/** /**
* Check and trim string provided by remote client * The time in milliseconds before a message is considered stale, and thus no longer allowed
* @param {string} text - Subject string * to be edited.
* @private * @type {number}
* @todo Move into utility module */
* @return {string|boolean} const ACTIVE_TIMEOUT = 5 * 60 * 1000;
*/ /**
const parseText = (text) => { * The time in milliseconds that a check for stale messages should be performed.
// verifies user input is text * @type {number}
if (typeof text !== 'string') { */
return false; const TIMEOUT_CHECK_INTERVAL = 30 * 1000;
/**
* Stores active messages that can be edited.
* @type {{ customId: string, userid: number, sent: number }[]}
*/
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++) {
const message = ACTIVE_MESSAGES[i];
if (now - message.sent > ACTIVE_TIMEOUT) {
ACTIVE_MESSAGES.splice(i, 1);
i--;
}
} }
}
let sanitizedText = text; // TODO: This won't get cleared on module reload.
setInterval(cleanActiveMessages, TIMEOUT_CHECK_INTERVAL);
// strip newlines from beginning and end /**
sanitizedText = sanitizedText.replace(/^\s*\n|^\s+$|\n\s*$/g, ''); * Adds a message to the active messages map.
// replace 3+ newlines with just 2 newlines * @public
sanitizedText = sanitizedText.replace(/\n{3,}/g, '\n\n'); * @param {string} id
* @param {number} userid
return sanitizedText; * @return {void}
}; */
export function addActiveMessage(customId, userid) {
ACTIVE_MESSAGES.push({
customId,
userid,
sent: Date.now(),
});
}
/** /**
* Executes when invoked by a remote client * Executes when invoked by a remote client
@ -61,6 +92,13 @@ export async function run({
}, socket); }, socket);
} }
const customId = payload.customId;
if (typeof (customId) === 'string' && customId.length > MAX_MESSAGE_ID_LENGTH) {
// There's a limit on the custom id length.
return server.police.frisk(socket.address, 13);
}
// build chat payload // build chat payload
const outgoingPayload = { const outgoingPayload = {
cmd: 'chat', cmd: 'chat',
@ -70,6 +108,7 @@ export async function run({
channel: socket.channel, channel: socket.channel,
text, text,
level: socket.level, level: socket.level,
customId,
}; };
if (isAdmin(socket.level)) { if (isAdmin(socket.level)) {
@ -86,6 +125,7 @@ export async function run({
outgoingPayload.color = socket.color; outgoingPayload.color = socket.color;
} }
addActiveMessage(outgoingPayload.customId, socket.userid);
// broadcast to channel peers // broadcast to channel peers
server.broadcast(outgoingPayload, { channel: socket.channel }); server.broadcast(outgoingPayload, { channel: socket.channel });

View File

@ -0,0 +1,97 @@
import { parseText } from "../utility/_Text.js";
import { isAdmin, isModerator } from "../utility/_UAC.js";
import { ACTIVE_MESSAGES, MAX_MESSAGE_ID_LENGTH } from "./chat.js";
export async function run({ core, server, socket, payload }) {
// undefined | "overwrite" | "append" | "prepend"
let mode = payload.mode;
if (!mode) {
mode = 'overwrite';
}
if (mode !== 'overwrite' && mode !== 'append' && mode !== 'prepend') {
return server.police.frisk(socket.address, 13);
}
const customId = payload.customId;
if (!customId || typeof customId !== "string" || customId.length > MAX_MESSAGE_ID_LENGTH) {
return server.police.frisk(socket.address, 13);
}
let text = payload.text;
if (typeof (text) !== 'string') {
return server.police.frisk(socket.address, 13);
}
if (mode === 'overwrite') {
text = parseText(text);
if (text === '') {
text = '\u0000';
}
}
if (!text) {
return server.police.frisk(socket.address, 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++) {
const msg = ACTIVE_MESSAGES[i];
if (msg.userid === socket.userid && msg.customId === customId) {
message = ACTIVE_MESSAGES[i];
break;
}
}
if (!message) {
return server.police.frisk(socket.address, 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;
}
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'|'prepand', text: '<text to apply>',customId: '<customId sent with the chat message>' }`,
};

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

@ -0,0 +1,21 @@
/**
* 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;
};