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/config/test.json | 1 + vnext/server/common/MessageUtils.js | 16 ++ vnext/server/common/MessageUtils.spec.js | 2 +- .../common/__snapshots__/MessageUtils.spec.js.snap | 2 +- vnext/server/durov.js | 33 ++++ vnext/server/http.js | 8 +- vnext/server/index.js | 2 +- vnext/server/middleware/event.js | 33 +++- vnext/server/sender.js | 15 +- vnext/src/utils/embed.js | 206 +++++++++++---------- 10 files changed, 199 insertions(+), 119 deletions(-) create mode 100644 vnext/config/test.json create mode 100644 vnext/server/durov.js diff --git a/vnext/config/test.json b/vnext/config/test.json new file mode 100644 index 00000000..0967ef42 --- /dev/null +++ b/vnext/config/test.json @@ -0,0 +1 @@ +{} diff --git a/vnext/server/common/MessageUtils.js b/vnext/server/common/MessageUtils.js index f766faf2..74638c02 100644 --- a/vnext/server/common/MessageUtils.js +++ b/vnext/server/common/MessageUtils.js @@ -1,3 +1,5 @@ +import config from 'config'; + /** * check if message is PM * @@ -64,3 +66,17 @@ export function formatQuote(msg) { export function formatMessage(msg) { return msg.body || 'Sent an image'; } + +const baseURL = 'https://juick.com'; + +/** + * @param {import('../api').Message} msg message + */ +export function formatUrl(msg) { + if (isReply(msg)) { + return `${baseURL}/m/${msg.mid}#${msg.rid}`; + } else if (isPM(msg)) { + return `${baseURL}/pm/inbox`; + } + return `${baseURL}/m/${msg.mid}`; +} diff --git a/vnext/server/common/MessageUtils.spec.js b/vnext/server/common/MessageUtils.spec.js index 766b1882..5e6f64c2 100644 --- a/vnext/server/common/MessageUtils.spec.js +++ b/vnext/server/common/MessageUtils.spec.js @@ -27,7 +27,7 @@ describe('Message formatting', () => { 'uname': 'ugnich' }, 'replyQuote': '> The message', - 'body': 'The reply' + 'body': 'The #reply #bla' }; expect(formatTitle(msg)).toMatchSnapshot(); expect(formatQuote(msg)).toMatchSnapshot(); diff --git a/vnext/server/common/__snapshots__/MessageUtils.spec.js.snap b/vnext/server/common/__snapshots__/MessageUtils.spec.js.snap index ea58aebc..fb3c3513 100644 --- a/vnext/server/common/__snapshots__/MessageUtils.spec.js.snap +++ b/vnext/server/common/__snapshots__/MessageUtils.spec.js.snap @@ -14,4 +14,4 @@ exports[`Message formatting Reply message 1`] = `"Reply by ugnich:"`; exports[`Message formatting Reply message 2`] = `"> The message"`; -exports[`Message formatting Reply message 3`] = `"The reply"`; +exports[`Message formatting Reply message 3`] = `"The #reply #bla"`; diff --git a/vnext/server/durov.js b/vnext/server/durov.js new file mode 100644 index 00000000..0f70133d --- /dev/null +++ b/vnext/server/durov.js @@ -0,0 +1,33 @@ +import TelegramBot from 'node-telegram-bot-api'; +import debug from 'debug'; +var log = debug('durov'); +import config from 'config'; + + +import { formatMessage, formatQuote, formatTitle } from './common/MessageUtils'; +import { format } from '../src/utils/embed'; + +const sender = new TelegramBot(config.get('service.durov.token'), { polling: true }); +const demouser = config.get('service.durov.demouser'); + +sender.on('message', msg => { + log(`MESSAGE: ${JSON.stringify(msg)}`); +}); + +/** + * @param {import('../src/api').Message} msg + * @param {string[]} subscribers + */ +export const sendTelegramNotification = (msg, subscribers) => { + log(`Telegram event: ${JSON.stringify(msg)}, ${subscribers} ${subscribers.length}`); + if (!msg.service) { + if (subscribers && subscribers.includes(demouser)) { + const message = `${formatTitle(msg)}\n${formatQuote(msg)}\n${format(msg.body, msg.uid, false)}`; + log(message); + sender.sendMessage(demouser, message, { + parse_mode: 'HTML', + disable_web_page_preview: true + }).then(log).catch(log); + } + } +}; diff --git a/vnext/server/http.js b/vnext/server/http.js index c2557108..0ffa8bfe 100644 --- a/vnext/server/http.js +++ b/vnext/server/http.js @@ -1,6 +1,7 @@ import axios from 'axios'; import config from 'config'; -var debug = require('debug')('http'); +import debug from 'debug'; +var log = debug('http'); /** @@ -17,6 +18,7 @@ export function subscribers(params) { return new Promise((resolve, reject) => { client.get(`/notifications?${params.toString()}`) .then(response => { + log(`CODE: ${response.status}`); resolve(response.data); }) .catch(reason => { reject(reason); }); @@ -33,7 +35,7 @@ export const deleteSubscribers = async (tokens) => { return response.data; }; -const baseURL = config.get('service.baseURL') || process.env.JUICK_SERVICE_URL; +const baseURL = config.get('service.baseURL') + '/api'; const user = config.get('service.user') || process.env.JUICK_SERVICE_USER; const password = config.get('service.password') || process.env.JUICK_SERVICE_PASSWORD; @@ -44,4 +46,4 @@ let client = axios.create({ 'Authorization': 'Basic ' + Buffer.from(user + ':' + password).toString('base64') } : {} }); -debug(`HTTP client initialized with base URL ${baseURL} ${ user? `and ${user} user` : ''}`); +log(`HTTP client initialized with base URL ${baseURL} ${ user? `and ${user} user` : ''}`); diff --git a/vnext/server/index.js b/vnext/server/index.js index 95f88cd1..89122069 100644 --- a/vnext/server/index.js +++ b/vnext/server/index.js @@ -5,7 +5,6 @@ import config from 'config'; import debug from 'debug'; const log = debug('http'); -// we'll talk about this in a minute: import serverRenderer from './middleware/renderer'; import event from './middleware/event'; import oembed from './middleware/oembed'; @@ -37,6 +36,7 @@ router.use('*', serverRenderer); app.use(router); + // start the app app.listen(PORT, (error) => { if (error) { diff --git a/vnext/server/middleware/event.js b/vnext/server/middleware/event.js index 54611280..1267d1c4 100644 --- a/vnext/server/middleware/event.js +++ b/vnext/server/middleware/event.js @@ -1,8 +1,27 @@ import { simpleParser } from 'mailparser'; import { isPM, isReply, isService } from '../common/MessageUtils'; +import { sendTelegramNotification } from '../durov'; import { subscribers } from '../http'; import { sendNotification, buildNotification } from '../sender'; -var debug = require('debug')('event'); +import debug from 'debug'; +var log = debug('event'); +import config from 'config'; +import EventSource from 'eventsource'; + +const es = new EventSource(config.get('service.baseURL')+ '/api/events'); +es.addEventListener('msg', (msg) => { + log(msg.data); + processMessageEvent(JSON.parse(msg.data)); +}); +es.addEventListener('read', (msg) => { + log(msg); +}); +es.addEventListener('open', () => { + log('online'); +}); +es.onerror = (evt) => { + log(`err: ${JSON.stringify(evt)}`); +}; /** @type {number[]} */ const allSandboxIds = []; @@ -23,7 +42,7 @@ function processMessageEvent(msg) { } subscribers(new URLSearchParams(JSON.parse(JSON.stringify(params)))).then(users => { users.forEach(user => { - debug(`${user.uname}: ${user.unreadCount}`); + log(`${user.uname}: ${user.unreadCount}`); let [sandboxTokens, productionTokens] = (user.tokens || []) .filter(t => ['mpns', 'apns', 'gcm'].includes(t.type)) .map(t => t.token) @@ -34,8 +53,12 @@ function processMessageEvent(msg) { return result; }, [[], []]); sendNotification(productionTokens, sandboxTokens, buildNotification(user, msg)); + let durovIds = (user.tokens || []) + .filter(t => ['durov'].includes(t.type)) + .map(t => t.token); + sendTelegramNotification(msg, durovIds); }); - }).catch(console.error); + }).catch(log); } /** @@ -47,7 +70,7 @@ export default function handleMessage(req, res) { return simpleParser(req.body, {}) .then(parsed => { const new_version = parsed.headers.get('x-event-version') == '1.0'; - debug(`New event: ${parsed.text}, new version: ${new_version}`); + log(`New event: ${parsed.text}, new version: ${new_version}`); if (new_version) { /** @type {import('../../client').SystemEvent} */ const event = JSON.parse(parsed.text || ''); @@ -71,5 +94,5 @@ export default function handleMessage(req, res) { } res.end(); }) - .catch(err => { console.error(err); res.status(400).send('Invalid request'); }); + .catch(err => { log(err); res.status(400).send('Invalid request'); }); } diff --git a/vnext/server/sender.js b/vnext/server/sender.js index 6ece8eaa..f33eadcf 100644 --- a/vnext/server/sender.js +++ b/vnext/server/sender.js @@ -1,5 +1,6 @@ import PushNotifications from 'node-pushnotifications'; -var debug = require('debug')('sender'); +import debug from 'debug'; +const log = debug('sender'); import { deleteSubscribers } from './http'; import { formatMessage, formatTitle, formatQuote } from './common/MessageUtils'; import config from 'config'; @@ -51,7 +52,7 @@ export function sendNotification(productionIds, sandboxIds, data) { sender.send(registrationIds, data) .then((results) => { results.forEach(result => { - debug(`${result.method}: ${result.success} success, ${result.failure} failure`); + log(`${result.method}: ${result.success} success, ${result.failure} failure`); if (result.failure) { console.error(`${result.method} failure: ${JSON.stringify(result)}`); console.error(`Failed data: ${JSON.stringify(data)}`); @@ -59,13 +60,13 @@ export function sendNotification(productionIds, sandboxIds, data) { }); results.filter(r => r.method === 'apn') .forEach(async r => { - debug(`Response message: ${JSON.stringify(r.message)}`); + log(`Response message: ${JSON.stringify(r.message)}`); let badTokens = r.message.filter(m => m.errorMsg === 'BadDeviceToken').map(m => { return { 'type': 'apns', 'token': m.regId }; }); if (badTokens.length > 0) { await deleteSubscribers(badTokens); - debug(`${badTokens.length} APNS tokens deleted`); + log(`${badTokens.length} APNS tokens deleted`); } }); results.filter(r => r.method === 'gcm') @@ -75,7 +76,7 @@ export function sendNotification(productionIds, sandboxIds, data) { }); if (badTokens.length > 0) { await deleteSubscribers(badTokens); - debug(`${badTokens.length} GCM tokens deleted`); + log(`${badTokens.length} GCM tokens deleted`); } }); results.filter(r => r.method === 'mpns') @@ -85,7 +86,7 @@ export function sendNotification(productionIds, sandboxIds, data) { }); if (badTokens.length > 0) { await deleteSubscribers(badTokens); - debug(`${badTokens.length} MPNS tokens deleted`); + log(`${badTokens.length} MPNS tokens deleted`); } }); }) @@ -136,7 +137,5 @@ export function buildNotification(user, msg) { template.tag = `${tag}`; template.android_channel_id = 'default'; } - // FIXME: wrong type definition in node-pushnotifications: title and body and not required for silent pushes - // @ts-ignore return template; } 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