diff --git a/docker/node/modules/auth/actions.js b/docker/node/modules/auth/actions.js index f8f7791e..8e61ff84 100644 --- a/docker/node/modules/auth/actions.js +++ b/docker/node/modules/auth/actions.js @@ -29,4 +29,5 @@ exports.actions = { CAN_SEE_OBSCURED_POLLS: "canSeeObscuredPolls", CAN_SEE_PRIVILEGED_USER_DATA: "canSeePrivilegedUserData", CAN_SEE_SHADOWBANS: "canSeeShadowbans", + ACTION_POST_IMAGE: "postImage", }; diff --git a/docker/node/modules/auth/service.js b/docker/node/modules/auth/service.js index b2e3a271..f04f1fbc 100644 --- a/docker/node/modules/auth/service.js +++ b/docker/node/modules/auth/service.js @@ -35,6 +35,7 @@ exports.AuthService = class extends ServiceBase { [actions.CAN_SEE_PRIVILEGED_USER_DATA]: isMod, [actions.CAN_SEE_SHADOWBANS]: isMod, [actions.ACTION_CAN_RESET_PASSWORD]: isAdmin, + [actions.ACTION_POST_IMAGE]: isMod, }; function isBerry({ isBerry }) { diff --git a/docker/node/server.js b/docker/node/server.js index 31d47bce..68101aa4 100644 --- a/docker/node/server.js +++ b/docker/node/server.js @@ -1069,6 +1069,50 @@ const chatCommandMap = { ); } }), + ...withAliases(["img", "image", "video", "media"], (parsed, socket, messageData) => { + if (!authService.can(socket.session, actions.ACTION_POST_IMAGE)) { + kickForIllegalActivity(socket); + return doSuppressChat; + } + + let format = parsed.msg.trim().split('.').pop(); + + if (format.includes('?')) { + format = format.split('?')[0]; + } + + const supportedFormats = new Map([ + //images + ['png', {kind: 'image'}], + ['jpg', {kind: 'image'}], + ['jpeg', {kind: 'image'}], + ['gif', {kind: 'image'}], + ['svg', {kind: 'image'}], + ['webp', {kind: 'image'}], + ['avif', {kind: 'image'}], + + //videos + ['mp4', {kind: 'video'}], + ['webm', {kind: 'video'}], + ['gifv', {kind: 'video', real: 'mp4'}] + ]) + + + //couldn't get file extension or media not supported, don't show an attempt + if (format === '' || !supportedFormats.has(format)) { + return doSuppressChat; + } + + const info = supportedFormats.get(format); + + if (info.real) { + messageData.msg = parsed.msg.replace(`.${format}`, `.${info.real}`); + } + + messageData.emote = info.kind; + + return doNormalChatMessage; + }), }; function _sendChat(nick, type, incoming, socket) { diff --git a/web/css/layout-other.css b/web/css/layout-other.css index 36799e79..3a634272 100644 --- a/web/css/layout-other.css +++ b/web/css/layout-other.css @@ -664,7 +664,9 @@ body { } /* mod-embedded images */ -.img-filter > img { +#chatbuffer .img-filter > img, +#chatbuffer .image > img, +#chatbuffer video { max-height: 150px; max-width: 100%; } diff --git a/web/js/functions.js b/web/js/functions.js index da055e11..fb2e22be 100644 --- a/web/js/functions.js +++ b/web/js/functions.js @@ -1363,6 +1363,47 @@ function addChatMsg(data, _to) { newmsg.addClass("server").appendTo(msgwrap); $("").appendTo(newmsg).html(msgText); break; + case "image": + const linkAttrs = { + href: msgText, + class: 'image', + target: '_blank', + rel: 'noopener noreferrer' + }; + + newmsg.addClass("message").append( + $('', {class: `nick`, nick, text: `${nick}:`}), + $('', linkAttrs).append( + $('', { + src: msgText, + referrerpolicy: "no-referrer", + alt: 'Loading image...', + onload: "scrollBuffersToBottom()" + }) + ) + ).appendTo(msgwrap); + + includeTimestamp = true; + break; + case "video": { + const attributes = { + autoplay: '', + loop: '', + muted: '', + src: msgText, + referrerpolicy: 'noreferrer', + alt: 'Loading video...', + onload: "scrollBuffersToBottom()" + }; + + newmsg.addClass("message").append( + $('', {class: `nick`, nick, text: `${nick}:`}), + $('