From 0b50ac575b814f96b20b74b421ec57d680e80c92 Mon Sep 17 00:00:00 2001 From: sad4whattt Date: Thu, 22 Jan 2026 02:58:07 -0500 Subject: [PATCH] Add files via upload --- assets/chat/js/chat.js | 121 ++++++--- assets/chat/js/notification.js | 254 +++++++++---------- assets/chat/js/regex.js | 3 + assets/chat/js/services/XComOEmbedService.js | 42 +++ assets/chat/js/services/index.js | 1 + 5 files changed, 258 insertions(+), 163 deletions(-) create mode 100644 assets/chat/js/services/XComOEmbedService.js diff --git a/assets/chat/js/chat.js b/assets/chat/js/chat.js index d5aaebf8..38a48d1c 100644 --- a/assets/chat/js/chat.js +++ b/assets/chat/js/chat.js @@ -47,7 +47,11 @@ import { isMuteActive, MutedTimer } from './mutedtimer'; import EmoteService from './emotes'; import UserFeatures from './features'; import UserRoles from './roles'; -import { UserMessageService, YouTubeOEmbedService } from './services'; +import { + UserMessageService, + YouTubeOEmbedService, + XComOEmbedService, +} from './services'; import makeSafeForRegex, { regextime, nickmessageregex, @@ -55,6 +59,7 @@ import makeSafeForRegex, { nsfwregex, nsflregex, youtubeidregex, + xcomregex, } from './regex'; import { HashLinkConverter, MISSING_ARG_ERROR } from './hashlinkconverter'; import ChatCommands, { getSlashCommand, removeSlashCommand } from './commands'; @@ -95,6 +100,7 @@ class Chat { this.emoteService = new EmoteService(); this.userMessageService = new UserMessageService(); this.youtubeOEmbedService = new YouTubeOEmbedService(); + this.xComOEmbedService = new XComOEmbedService(); this.user = new ChatUser(); this.users = new Map(); @@ -448,7 +454,7 @@ class Chat { this.focusIfNothingSelected(); }); - // Youtube oEmbed tooltip + // Youtube oEmbed tooltip and X.com post tooltip this.ui.on('mouseover', 'a.externallink', async (e) => { const { target } = e; @@ -462,50 +468,93 @@ class Chat { return; } - const match = target.href.match(youtubeidregex); - - // Not a youtube id - if (!match) { - return; - } + // Check for YouTube URLs + const youtubeMatch = target.href.match(youtubeidregex); + if (youtubeMatch) { + try { + const result = await this.youtubeOEmbedService.getOEmbed( + youtubeMatch[1], + ); - try { - const result = await this.youtubeOEmbedService.getOEmbed(match[1]); + const container = document.createElement('div'); + container.style.display = 'flex'; + container.style.flexDirection = 'column'; + container.style.marginTop = '4px'; + container.style.gap = '0.25em'; - const container = document.createElement('div'); - container.style.display = 'flex'; - container.style.flexDirection = 'column'; - container.style.marginTop = '4px'; - container.style.gap = '0.25em'; + const img = document.createElement('img'); + img.src = result.thumbnail_url; - const img = document.createElement('img'); - img.src = result.thumbnail_url; + const title = document.createElement('strong'); + title.textContent = result.title; - const title = document.createElement('strong'); - title.textContent = result.title; + const author = document.createElement('span'); + author.textContent = result.author_name; - const author = document.createElement('span'); - author.textContent = result.author_name; + container.append(img, title, author); - container.append(img, title, author); + const youtubeTippy = tippy(target, { + content: container, + allowHTML: true, + arrow: roundArrow, + duration: 0, + theme: 'dgg', + maxWidth: 250, + }); - const youtubeTippy = tippy(target, { - content: container, - allowHTML: true, - arrow: roundArrow, - duration: 0, - theme: 'dgg', - maxWidth: 250, - }); + target.dataset.tipped = true; - target.dataset.tipped = true; + // If still hovering show immediately. + if (target.matches(':hover')) { + youtubeTippy.show(); + } + } catch (error) { + /* Do nothing */ + } + return; + } - // If still hovering show immediately. - if (target.matches(':hover')) { - youtubeTippy.show(); + // Check for X.com URLs + const xcomMatch = target.href.match(xcomregex); + if (xcomMatch) { + try { + const result = await this.xComOEmbedService.getOEmbed(xcomMatch[1]); + + const container = document.createElement('div'); + container.style.display = 'flex'; + container.style.flexDirection = 'column'; + container.style.marginTop = '4px'; + container.style.gap = '0.25em'; + container.style.padding = '8px'; + + const author = document.createElement('strong'); + author.textContent = result.author_name; + author.style.color = '#ffffff'; + + const title = document.createElement('div'); + title.innerHTML = result.html; + title.style.marginTop = '4px'; + + container.append(author, title); + + const xcomTippy = tippy(target, { + content: container, + allowHTML: true, + arrow: roundArrow, + duration: 0, + theme: 'dgg', + maxWidth: 250, + }); + + target.dataset.tipped = true; + + // If still hovering show immediately. + if (target.matches(':hover')) { + xcomTippy.show(); + } + } catch (error) { + /* Do nothing */ } - } catch (error) { - /* Do nothing */ } }); diff --git a/assets/chat/js/notification.js b/assets/chat/js/notification.js index db502d0c..8025752c 100644 --- a/assets/chat/js/notification.js +++ b/assets/chat/js/notification.js @@ -1,127 +1,127 @@ -/* eslint-disable */ -/* -Copyright (C) 2013 Hendrik Beskow -Permission is hereby granted, free of charge, to any person obtaining a copy -of this software and associated documentation files (the "Software"), to -deal in the Software without restriction, including without limitation the -rights to use, copy, modify, merge, publish, distribute, sublicense, and/or -sell copies of the Software, and to permit persons to whom the Software is -furnished to do so, subject to the following conditions: -The above copyright notice and this permission notice shall be included in -all copies or substantial portions of the Software. -THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR -IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, -FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL -THE AUTHORS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER -IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN -CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. -*/ -const Notification = (function (win, doc, nav) { - if ( /* Safari 6, Firefox 22 */ !('Notification' in win && 'permission' in win.Notification && 'requestPermission' in win.Notification)) { - const PERMISSION_DEFAULT = 'default', - PERMISSION_GRANTED = 'granted', - PERMISSION_DENIED = 'denied', - PERMISSION = [PERMISSION_GRANTED, PERMISSION_DEFAULT, PERMISSION_DENIED], - isString = function (value) { - return (value && (value).constructor === String); - }, - isFunction = function (value) { - return (value && (value).constructor === Function); - }, - isObject = function (value) { - return (value && (value).constructor === Object); - }, - noop = function () { - }; - const localStorage = window.localStorage || { - setItem: noop, - getItem: noop - }; - - const checkPermission = function () { - let permission; - if ( /* Chrome, Firefox < 22 && ff-html5notifications */ !!('webkitNotifications' in win && 'checkPermission' in win.webkitNotifications)) { - permission = PERMISSION[win.webkitNotifications.checkPermission()]; - } else if ( /* Firefox Mobile */ 'mozNotification' in nav) { - permission = PERMISSION_GRANTED; - } else { - permission = (localStorage.getItem('notifications') === null) ? PERMISSION_DEFAULT : localStorage.getItem('notifications'); - } - return permission; - } - - const requestPermission = function (callback) { - let callbackFunction = isFunction(callback) ? callback : noop; - if ( /* Chrome, Firefox < 22 && ff-html5notifications */ !!('webkitNotifications' in win && 'requestPermission' in win.webkitNotifications)) { - win.webkitNotifications.requestPermission(callbackFunction); - } else { - if (checkPermission() === PERMISSION_DEFAULT) { - if (confirm('Do you want to allow ' + doc.domain + ' to display Notifications?')) { - localStorage.setItem('notifications', PERMISSION_GRANTED); - Notification.permission = PERMISSION_GRANTED; - } else { - localStorage.setItem('notifications', PERMISSION_DENIED); - Notification.permission = PERMISSION_DENIED; - } - } - callbackFunction(); - } - } - - const Notification = function (title, params) { - let notification; - if (isString(title) && isObject(params) && checkPermission() === PERMISSION_GRANTED) { - if ( /* Firefox Mobile */ 'mozNotification' in nav) { - notification = nav.mozNotification.createNotification(title, params.body, params.icon); - if (isFunction(params.onclick)) { - notification.onclick = params.onclick; - } - if (isFunction(params.onclose)) { - notification.onclose = params.onclose; - } - notification.show(); - } else if ( /* Chrome, Firefox < 22 && ff-html5notifications */ 'webkitNotifications' in win && 'createNotification' in win.webkitNotifications) { - notification = win.webkitNotifications.createNotification(params.icon, title, params.body); - if (isFunction(params.onclick)) { - notification.onclick = params.onclick; - } - if (isFunction(params.onshow)) { - notification.onshow = params.onshow; - } - if (isFunction(params.onerror)) { - notification.onerror = params.onerror; - } - if (isFunction(params.onclose)) { - notification.onclose = params.onclose; - } - notification.show(); - } else { - // Your custom code goes here - } - } - return notification; - } - - if (!('Notification' in win)) { - Notification.requestPermission = requestPermission; - win.Notification = Notification; - } - - if (!!('webkitNotifications' in win && 'checkPermission' in win.webkitNotifications)) { - Object.defineProperty(win.Notification, 'permission', { - get: function () { - return PERMISSION[win.webkitNotifications.checkPermission()]; - } - }); - } else { - Notification.permission = checkPermission(); - } - - if (!('requestPermission' in win.Notification)) { - win.Notification.requestPermission = requestPermission; - } - } - return win.Notification -}(window, document, navigator)); - -export { Notification }; +/* eslint-disable */ +/* +Copyright (C) 2013 Hendrik Beskow +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to +deal in the Software without restriction, including without limitation the +rights to use, copy, modify, merge, publish, distribute, sublicense, and/or +sell copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: +The above copyright notice and this permission notice shall be included in +all copies or substantial portions of the Software. +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL +THE AUTHORS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER +IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN +CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. +*/ +const Notification = (function (win, doc, nav) { + if ( /* Safari 6, Firefox 22 */ !('Notification' in win && 'permission' in win.Notification && 'requestPermission' in win.Notification)) { + const PERMISSION_DEFAULT = 'default', + PERMISSION_GRANTED = 'granted', + PERMISSION_DENIED = 'denied', + PERMISSION = [PERMISSION_GRANTED, PERMISSION_DEFAULT, PERMISSION_DENIED], + isString = function (value) { + return (value && (value).constructor === String); + }, + isFunction = function (value) { + return (value && (value).constructor === Function); + }, + isObject = function (value) { + return (value && (value).constructor === Object); + }, + noop = function () { + }; + const localStorage = window.localStorage || { + setItem: noop, + getItem: noop + }; + + const checkPermission = function () { + let permission; + if ( /* Chrome, Firefox < 22 && ff-html5notifications */ !!('webkitNotifications' in win && 'checkPermission' in win.webkitNotifications)) { + permission = PERMISSION[win.webkitNotifications.checkPermission()]; + } else if ( /* Firefox Mobile */ 'mozNotification' in nav) { + permission = PERMISSION_GRANTED; + } else { + permission = (localStorage.getItem('notifications') === null) ? PERMISSION_DEFAULT : localStorage.getItem('notifications'); + } + return permission; + } + + const requestPermission = function (callback) { + let callbackFunction = isFunction(callback) ? callback : noop; + if ( /* Chrome, Firefox < 22 && ff-html5notifications */ !!('webkitNotifications' in win && 'requestPermission' in win.webkitNotifications)) { + win.webkitNotifications.requestPermission(callbackFunction); + } else { + if (checkPermission() === PERMISSION_DEFAULT) { + if (confirm('Do you want to allow ' + doc.domain + ' to display Notifications?')) { + localStorage.setItem('notifications', PERMISSION_GRANTED); + Notification.permission = PERMISSION_GRANTED; + } else { + localStorage.setItem('notifications', PERMISSION_DENIED); + Notification.permission = PERMISSION_DENIED; + } + } + callbackFunction(); + } + } + + const Notification = function (title, params) { + let notification; + if (isString(title) && isObject(params) && checkPermission() === PERMISSION_GRANTED) { + if ( /* Firefox Mobile */ 'mozNotification' in nav) { + notification = nav.mozNotification.createNotification(title, params.body, params.icon); + if (isFunction(params.onclick)) { + notification.onclick = params.onclick; + } + if (isFunction(params.onclose)) { + notification.onclose = params.onclose; + } + notification.show(); + } else if ( /* Chrome, Firefox < 22 && ff-html5notifications */ 'webkitNotifications' in win && 'createNotification' in win.webkitNotifications) { + notification = win.webkitNotifications.createNotification(params.icon, title, params.body); + if (isFunction(params.onclick)) { + notification.onclick = params.onclick; + } + if (isFunction(params.onshow)) { + notification.onshow = params.onshow; + } + if (isFunction(params.onerror)) { + notification.onerror = params.onerror; + } + if (isFunction(params.onclose)) { + notification.onclose = params.onclose; + } + notification.show(); + } else { + // Your custom code goes here + } + } + return notification; + } + + if (!('Notification' in win)) { + Notification.requestPermission = requestPermission; + win.Notification = Notification; + } + + if (!!('webkitNotifications' in win && 'checkPermission' in win.webkitNotifications)) { + Object.defineProperty(win.Notification, 'permission', { + get: function () { + return PERMISSION[win.webkitNotifications.checkPermission()]; + } + }); + } else { + Notification.permission = checkPermission(); + } + + if (!('requestPermission' in win.Notification)) { + win.Notification.requestPermission = requestPermission; + } + } + return win.Notification +}(window, document, navigator)); + +export { Notification }; diff --git a/assets/chat/js/regex.js b/assets/chat/js/regex.js index 3d9221e2..a2961392 100644 --- a/assets/chat/js/regex.js +++ b/assets/chat/js/regex.js @@ -8,6 +8,8 @@ const nsflregex = /\bNSFL\b/i; const spoilersregex = /\bSPOILERS\b/i; const youtubeidregex = /(?:youtube\.com\/(?:watch\?v=|shorts\/|live\/)|youtu\.be\/|#youtube\/)([A-Za-z0-9_-]{11})/; +const xcomregex = + /(?:https?:\/\/)?(?:www\.)?(?:twitter\.com|x\.com)\/\w{1,15}\/status\/(\d{2,19})/i; export { regexslashcmd, @@ -18,6 +20,7 @@ export { nsflregex, spoilersregex, youtubeidregex, + xcomregex, }; export default function makeSafeForRegex(str) { diff --git a/assets/chat/js/services/XComOEmbedService.js b/assets/chat/js/services/XComOEmbedService.js new file mode 100644 index 00000000..e247b227 --- /dev/null +++ b/assets/chat/js/services/XComOEmbedService.js @@ -0,0 +1,42 @@ +// @ts-check + +/** + * @typedef {Object} XComOembedResponse + * @property {string} author_name + * @property {string} author_url + * @property {string} html + * @property {number} width + * @property {number} height + * @property {string} title + * @property {string} url + */ + +const BASE_URI = 'https://publish.twitter.com'; + +export default class XComOEmbedService { + /** + * @param {string} statusId + * @return {Promise} + * @throws {Error} + */ + async getOEmbed(statusId) { + const url = new URL('/oembed', BASE_URI); + url.searchParams.set('url', `https://x.com/i/status/${statusId}`); + url.searchParams.set('omit_script', 'true'); + url.searchParams.set('dnt', 'true'); + url.searchParams.set('maxwidth', '250'); + + const response = await fetch(url.toString()); + + if (!response.ok) { + throw new Error(`HTTP error! status: ${response.status}`); + } + + try { + /** @type {XComOembedResponse} */ + return await response.json(); + } catch (error) { + throw new Error('Invalid JSON'); + } + } +} diff --git a/assets/chat/js/services/index.js b/assets/chat/js/services/index.js index 1e25fd86..12b1a1e7 100644 --- a/assets/chat/js/services/index.js +++ b/assets/chat/js/services/index.js @@ -1,3 +1,4 @@ export { default as RustleSearchApiClient } from './RustleSearchApiClient'; export { default as UserMessageService } from './UserMessageService'; export { default as YouTubeOEmbedService } from './YouTubeOEmbedService'; +export { default as XComOEmbedService } from './XComOEmbedService';