From e3c378cbf1d502263c61d3b9c31cd270bc3ae239 Mon Sep 17 00:00:00 2001
From: Vitaly Takmazov
Date: Fri, 13 Jan 2023 10:28:31 +0300
Subject: vnext: Telegram bot (WIP)
---
vnext/src/utils/embed.js | 206 ++++++++++++++++++++++++-----------------------
1 file changed, 106 insertions(+), 100 deletions(-)
(limited to 'vnext/src/utils')
diff --git a/vnext/src/utils/embed.js b/vnext/src/utils/embed.js
index 0c6fbd9c..7db46866 100644
--- a/vnext/src/utils/embed.js
+++ b/vnext/src/utils/embed.js
@@ -28,15 +28,15 @@ function formatText(txt, rules) {
function nextId() { return idCounter++; }
function ft(txt, rules) {
let matches = rules.map(r => { r.re.lastIndex = 0; return [r, r.re.exec(txt)]; })
- .filter(([,m]) => m !== null)
- .sort(([r1,m1],[r2,m2]) => (r1.pr - r2.pr) || (m1.index - m2.index));
+ .filter(([, m]) => m !== null)
+ .sort(([r1, m1], [r2, m2]) => (r1.pr - r2.pr) || (m1.index - m2.index));
if (matches && matches.length > 0) {
let [rule, match] = matches[0];
let subsequentRules = rules.filter(r => r.pr >= rule.pr);
let idStr = `<>(${nextId()})<>`;
let outerStr = txt.substring(0, match.index) + idStr + txt.substring(rule.re.lastIndex);
let innerStr = (rule.brackets)
- ? (() => { let [l ,r ,f] = rule.with; return l + ft((f ? f(match[1]) : match[1]), subsequentRules) + r; })()
+ ? (() => { let [l, r, f] = rule.with; return l + ft((f ? f(match[1]) : match[1]), subsequentRules) + r; })()
: match[0].replace(rule.re, rule.with);
return ft(outerStr, subsequentRules).replace(idStr, innerStr);
}
@@ -59,7 +59,7 @@ function makeNewNode(embedType, aNode, reResult) {
return embedType.makeNode(aNode, reResult, withClasses(document.createElement('div')));
}
-function makeIframe(src, w, h, scrolling='no') {
+function makeIframe(src, w, h, scrolling = 'no') {
let iframe = document.createElement('iframe');
iframe.style.width = w;
iframe.style.height = h;
@@ -120,13 +120,13 @@ function messageReplyReplace(messageId) {
* Given "txt" message in unescaped plaintext with Juick markup, this function
* returns escaped formatted HTML string.
*
- * @param {string} txt
- * @param {string} messageId - current message id
- * @param {boolean} isCode
+ * @param {string} txt text message
+ * @param {string} messageId current message id
+ * @param {boolean} isCode set when message contains *code tag
* @returns {string} formatted message
*/
function juickFormat(txt, messageId, isCode) {
- const urlRe = /(?:\[([^\][]+)\](?:\[([^\]]+)\]|\(((?:[a-z]+:\/\/|www\.|ftp\.)(?:\([-\w+*&@#/%=~|$?!:;,.]*\)|[-\w+*&@#/%=~|$?!:;,.])*(?:\([-\w+*&@#/%=~|$?!:;,.]*\)|[\w+*&@#/%=~|$]))\))|\b(?:[a-z]+:\/\/|www\.|ftp\.)(?:\([-\w+*&@#/%=~|$?!:;,.]*\)|[-\w+*&@#/%=~|$?!:;,.])*(?:\([-\w+*&@#/%=~|$?!:;,.]*\)|[\w+*&@#/%=~|$]))/gi;
+ const urlRe = /(?:\[([^\]\[]+)\](?:\[([^\]]+)\]|\(((?:[a-z]+:\/\/|www\.|ftp\.)(?:\([-\S+*&@#/%=~|$?!:;,.]*\)|[-\S+*&@#/%=~|$?!:;,.])*(?:\([-\S+*&@#/%=~|$?!:;,.]*\)|[\S+*&@#/%=~|$]))\))|\b(?:[a-z]+:\/\/|www\.|ftp\.)(?:\([-\S+*&@#/%=~|$?!:;,.]*\)|[-\S+*&@#/%=~|$?!:;,.])*(?:\([-\S+*&@#/%=~|$?!:;,.]*\)|[\S+*&@#/%=~|$]))/gi;
const bqReplace = m => m.replace(/^(?:>|>)\s?/gmi, '');
return (isCode)
? formatText(txt, [
@@ -139,23 +139,50 @@ function juickFormat(txt, messageId, isCode) {
{ pr: 1, re: urlRe, with: urlReplace },
{ pr: 1, re: /\B(?:#(\d+))?(?:\/(\d+))?\b/g, with: messageReplyReplace(messageId) },
{ pr: 1, re: /\B@([\w-]+)\b/gi, with: '@$1' },
- { pr: 2, re: /\B\*([^\n]+?)\*((?=\s)|(?=$)|(?=[!"#$%&'*+,\-./:;<=>?@[\]^_`{|}~()]+))/g, brackets: true, with: ['', ''] },
- { pr: 2, re: /\B\/([^\n]+?)\/((?=\s)|(?=$)|(?=[!"#$%&'*+,\-./:;<=>?@[\]^_`{|}~()]+))/g, brackets: true, with: ['', ''] },
- { pr: 2, re: /\b_([^\n]+?)_((?=\s)|(?=$)|(?=[!"#$%&'*+,\-./:;<=>?@[\]^_`{|}~()]+))/g, brackets: true, with: ['', ''] },
+ { pr: 2, re: /\B\*([^\n]+?)\*((?=\s)|(?=$)|(?=[!\"#$%&'*+,\-./:;<=>?@[\]^_`{|}~()]+))/g, brackets: true, with: ['', ''] },
+ { pr: 2, re: /\B\/([^\n]+?)\/((?=\s)|(?=$)|(?=[!\"#$%&'*+,\-./:;<=>?@[\]^_`{|}~()]+))/g, brackets: true, with: ['', ''] },
+ { pr: 2, re: /\b\_([^\n]+?)\_((?=\s)|(?=$)|(?=[!\"#$%&'*+,\-./:;<=>?@[\]^_`{|}~()]+))/g, brackets: true, with: ['', ''] },
{ pr: 3, re: /\n/g, with: '
' },
]);
}
+/**
+ * @external RegExpExecArray
+ */
+
+/**
+ * @callback MakeNodeCallback
+ * @param { HTMLAnchorElement } aNode a DOM node of the link
+ * @param { RegExpExecArray } reResult Result of RegExp execution
+ * @param { HTMLDivElement} div target DOM element which can be updated by callback function
+ * @returns { HTMLDivElement } updated DOM element
+ */
+
+/**
+ * @typedef { object } LinkFormatData
+ * @property { string } id Format identifier
+ * @property { string } name Format description
+ * @property { RegExp } re Regular expression to match expected hyperlinks
+ * @property { string } className list of CSS classes which
+ * will be added to the target DOM element
+ * @property { MakeNodeCallback } makeNode callback function called when a target link is matched
+ */
+/**
+ * Get supported embeddable formats
+ *
+ * @returns {LinkFormatData[]} list of supported formats
+ */
function getEmbeddableLinkTypes() {
return [
{
name: 'Jpeg and png images',
id: 'embed_jpeg_and_png_images',
className: 'picture compact',
- ctsDefault: false,
- re: /\.(jpe?g|png|svg)(:[a-zA-Z]+)?(?:\?[\w&;?=]*)?$/i,
- makeNode: function(aNode, _reResult, div) {
- div.innerHTML = ``;
+ re: /\.(jpe?g|png|svg)(:[a-zA-Z]+)?(?:\?[\w&;\?=]*)?$/i,
+ makeNode: function(aNode, reResult, div) {
+ // dirty fix for dropbox urls
+ let url = aNode.href.endsWith('dl=0') ? aNode.href.replace('dl=0', 'raw=1') : aNode.href;
+ div.innerHTML = ``;
return div;
}
},
@@ -163,9 +190,8 @@ function getEmbeddableLinkTypes() {
name: 'Gif images',
id: 'embed_gif_images',
className: 'picture compact',
- ctsDefault: true,
- re: /\.gif(:[a-zA-Z]+)?(?:\?[\w&;?=]*)?$/i,
- makeNode: function(aNode, _reResult, div) {
+ re: /\.gif(:[a-zA-Z]+)?(?:\?[\w&;\?=]*)?$/i,
+ makeNode: function(aNode, reResult, div) {
div.innerHTML = ``;
return div;
}
@@ -174,9 +200,8 @@ function getEmbeddableLinkTypes() {
name: 'Video (webm, mp4, ogv)',
id: 'embed_webm_and_mp4_videos',
className: 'video compact',
- ctsDefault: false,
- re: /\.(webm|mp4|m4v|ogv)(?:\?[\w&;?=]*)?$/i,
- makeNode: function(aNode, _reResult, div) {
+ re: /\.(webm|mp4|m4v|ogv)(?:\?[\w&;\?=]*)?$/i,
+ makeNode: function(aNode, reResult, div) {
div.innerHTML = ``;
return div;
}
@@ -185,9 +210,8 @@ function getEmbeddableLinkTypes() {
name: 'Audio (mp3, ogg, weba, opus, m4a, oga, wav)',
id: 'embed_sound_files',
className: 'audio singleColumn',
- ctsDefault: false,
- re: /\.(mp3|ogg|weba|opus|m4a|oga|wav)(?:\?[\w&;?=]*)?$/i,
- makeNode: function(aNode, _reResult, div) {
+ re: /\.(mp3|ogg|weba|opus|m4a|oga|wav)(?:\?[\w&;\?=]*)?$/i,
+ makeNode: function(aNode, reResult, div) {
div.innerHTML = ``;
return div;
}
@@ -196,18 +220,17 @@ function getEmbeddableLinkTypes() {
name: 'YouTube videos (and playlists)',
id: 'embed_youtube_videos',
className: 'youtube resizableV singleColumn',
- ctsDefault: false,
- re: /^(?:https?:)?\/\/(?:www\.|m\.|gaming\.)?(?:youtu(?:(?:\.be\/|be\.com\/(?:v|embed)\/)([-\w]+)|be\.com\/watch)((?:(?:\?|&(?:amp;)?)(?:\w+=[-.\w]*[-\w]))*)|youtube\.com\/playlist\?list=([-\w]*)(&(amp;)?[-\w?=]*)?)/i,
- makeNode: function(_aNode, reResult, div) {
- let [, v, args, plist] = reResult;
+ re: /^(?:https?:)?\/\/(?:www\.|m\.|gaming\.)?(?:youtu(?:(?:\.be\/|be\.com\/(?:v|embed)\/)([-\w]+)|be\.com\/watch)((?:(?:\?|&(?:amp;)?)(?:\w+=[-\.\w]*[-\w]))*)|youtube\.com\/playlist\?list=([-\w]*)(&(amp;)?[-\w\?=]*)?)/i,
+ makeNode: function(aNode, reResult, div) {
+ let [url, v, args, plist] = reResult;
let iframeUrl;
if (plist) {
iframeUrl = '//www.youtube-nocookie.com/embed/videoseries?list=' + plist;
} else {
let pp = {}; args.replace(/^\?/, '')
- .split('&')
- .map(s => s.split('='))
- .forEach(z => pp[z[0]] = z[1]);
+ .split('&')
+ .map(s => s.split('='))
+ .forEach(z => pp[z[0]] = z[1]);
let embedArgs = {
rel: '0',
enablejsapi: '1',
@@ -215,16 +238,16 @@ function getEmbeddableLinkTypes() {
};
if (pp.t) {
const tre = /^(?:(\d+)|(?:(\d+)h)?(?:(\d+)m)?(\d+)s|(?:(\d+)h)?(\d+)m|(\d+)h)$/i;
- let [, t, h, m, s, h1, m1, h2] = tre.exec(pp.t) || [];
- embedArgs['start'] = (+t) || ((+(h || h1 || h2 || 0))*60*60 + (+(m || m1 || 0))*60 + (+(s || 0)));
+ let [, t, h, m, s, h1, m1, h2] = tre.exec(pp.t);
+ embedArgs['start'] = (+t) || ((+(h || h1 || h2 || 0)) * 60 * 60 + (+(m || m1 || 0)) * 60 + (+(s || 0)));
}
if (pp.list) {
embedArgs['list'] = pp.list;
}
v = v || pp.v;
let argsStr = Object.keys(embedArgs)
- .map(k => `${k}=${embedArgs[k]}`)
- .join('&');
+ .map(k => `${k}=${embedArgs[k]}`)
+ .join('&');
iframeUrl = `//www.youtube-nocookie.com/embed/${v}?${argsStr}`;
}
let iframe = makeIframe(iframeUrl, '100%', '360px');
@@ -236,7 +259,6 @@ function getEmbeddableLinkTypes() {
name: 'Vimeo videos',
id: 'embed_vimeo_videos',
className: 'vimeo resizableV',
- ctsDefault: false,
re: /^(?:https?:)?\/\/(?:www\.)?(?:player\.)?vimeo\.com\/(?:video\/|album\/[\d]+\/video\/)?([\d]+)/i,
makeNode: function(aNode, reResult, div) {
let iframe = makeIframe('//player.vimeo.com/video/' + reResult[1], '100%', '360px');
@@ -248,25 +270,25 @@ function getEmbeddableLinkTypes() {
name: 'Twitter',
id: 'embed_twitter_status',
className: 'twi compact',
- ctsDefault: false,
re: /^(?:https?:)?\/\/(?:www\.)?(?:mobile\.)?twitter\.com\/([\w-]+)\/status(?:es)?\/([\d]+)/i,
makeNode: function(aNode, reResult, div) {
- let [url] = reResult;
- url = url.replace('mobile.','');
-
- div.innerHTML = `
`; - - if (window['twttr']) { - // https://developer.twitter.com/en/docs/twitter-for-websites/javascript-api/guides/scripting-loading-and-initialization - window['twttr'].widgets.load(div); - } else { - // innerHTML cannot insert scripts, so... - let script = document.createElement('script'); - script.src = 'https://platform.twitter.com/widgets.js'; - script.charset = 'utf-8'; - div.appendChild(script); - } - + fetch('https://beta.juick.com/api/oembed?url=' + reResult[0]) + .then(response => response.json()) + .then(json => { + div.innerHTML = json.html; + }); + return div; + } + }, + { + name: 'Instagram media', + id: 'embed_instagram_images', + className: 'picture compact', + re: /https?:\/\/www\.?instagram\.com(\/p\/\w+)\/?/i, + makeNode: function(aNode, reResult, div) { + let [url, postId] = reResult; + let mediaUrl = `https://instagr.am${postId}/media`; + div.innerHTML = ``; return div; } }, @@ -276,7 +298,7 @@ function getEmbeddableLinkTypes() { className: 'tg compact', re: /https?:\/\/t\.me\/(\S+)/i, makeNode: function(aNode, reResult, div) { - let [, post] = reResult; + let [url, post] = reResult; // innerHTML cannot insert scripts, so... let script = document.createElement('script'); script.src = 'https://telegram.org/js/telegram-widget.js?18'; @@ -287,30 +309,39 @@ function getEmbeddableLinkTypes() { div.appendChild(script); return div; } - } + }, ]; } -function embedLink(aNode, linkTypes, container, afterNode) { +/** + * Embed a link + * + * @param { HTMLAnchorElement } aNode a DOM node of the link + * @param { LinkFormatData[] } linkTypes supported link types + * @param { HTMLElement } container a target DOM element with the link content + * @param { boolean } afterNode where to insert new DOM node + * @returns { boolean } `true` when some link was embedded + */ +function embedLink(aNode, linkTypes, container, afterNode = false) { let anyEmbed = false; - let linkId = (aNode.href.replace(/^https?:/i, '').replace(/'/gi,'')); + let linkId = (aNode.href.replace(/^https?:/i, '').replace(/\'/gi, '')); let sameEmbed = container.querySelector(`*[data-linkid='${linkId}']`); // do not embed the same thing twice - if (sameEmbed === null) { - anyEmbed = [].some.call(linkTypes, function(linkType) { - let reResult = linkType.re.exec(aNode.href); - if (reResult) { - if (linkType.match && (linkType.match(aNode, reResult) === false)) { return false; } - let newNode = makeNewNode(linkType, aNode, reResult); - if (!newNode) { return false; } - newNode.setAttribute('data-linkid', linkId); - if (afterNode) { - insertAfter(newNode, afterNode); - } else { - container.appendChild(newNode); - } - aNode.classList.add('embedLink'); - return true; + if (!sameEmbed) { + anyEmbed = linkTypes.some((linkType) => { + let reResult = linkType.re.exec(aNode.href); + if (reResult) { + if (linkType.match && (linkType.match(aNode, reResult) === false)) { return false; } + let newNode = makeNewNode(linkType, aNode, reResult); + if (!newNode) { return false; } + newNode.setAttribute('data-linkid', linkId); + if (afterNode) { + insertAfter(newNode, afterNode); + } else { + container.appendChild(newNode); } + aNode.classList.add('embedLink'); + return true; + } }); } return anyEmbed; @@ -337,6 +368,7 @@ function embedLinks(aNodes, container) { * @param {string} allLinksSelector */ export function embedLinksToX(x, beforeNodeSelector, allLinksSelector) { + let isCtsPost = false; let allLinks = x.querySelectorAll(allLinksSelector); let existingContainer = x.querySelector('div.embedContainer'); @@ -355,31 +387,15 @@ export function embedLinksToX(x, beforeNodeSelector, allLinksSelector) { } /** - * Embed links to articles - */ -function embedLinksToArticles() { - let beforeNodeSelector = 'nav.l'; - let allLinksSelector = 'p:not(.ir) a, pre a'; - Array.from(document.querySelectorAll('#content article')).forEach(article => { - embedLinksToX(article, beforeNodeSelector, allLinksSelector); - }); -} - -/** - * Embed links to post + * Embed all the links in all messages/replies on the page. */ -function embedLinksToPost() { +export function embedAll() { let beforeNodeSelector = '.msg-txt + *'; let allLinksSelector = '.msg-txt a'; Array.from(document.querySelectorAll('#content .msg-cont')).forEach(msg => { embedLinksToX(msg, beforeNodeSelector, allLinksSelector); }); } - -/** - * @external NodeListOf - */ - /** * Embed URLs to container * @@ -390,15 +406,5 @@ export function embedUrls(urls, embedContainer) { embedLinks(urls, embedContainer); } -/** - * Embed all the links in all messages/replies on the page. - */ -export function embedAll() { - if (document.querySelector('#content article[data-mid]')) { - embedLinksToArticles(); - } else { - embedLinksToPost(); - } -} export const format = juickFormat; -- cgit v1.2.3