diff --git a/.eslintrc b/.eslintrc
new file mode 100644
index 0000000..abd292a
--- /dev/null
+++ b/.eslintrc
@@ -0,0 +1,3 @@
+{
+ "extends": "nodebb"
+}
diff --git a/client/.eslintrc b/client/.eslintrc
new file mode 100644
index 0000000..603607f
--- /dev/null
+++ b/client/.eslintrc
@@ -0,0 +1,3 @@
+{
+ "extends": "nodebb/public"
+}
\ No newline at end of file
diff --git a/client/admin.js b/client/admin.js
new file mode 100644
index 0000000..d6cc2b8
--- /dev/null
+++ b/client/admin.js
@@ -0,0 +1,38 @@
+'use strict';
+
+define('admin/plugins/iframely', ['settings'], function (Settings) {
+ var ACP = {};
+
+ ACP.init = function () {
+ Settings.load('iframely', $('.iframely-settings'), function () {
+ function tagifyInput(selector) {
+ var input = $(selector).tagsinput({
+ confirmKeys: [13, 44],
+ trimValue: true,
+ });
+ if (input[0]) {
+ $(input[0].$input).addClass('form-control').parent().css('display', 'block');
+ }
+ }
+
+ tagifyInput('#blacklist');
+ });
+ $('#save').on('click', saveSettings);
+ };
+
+ function saveSettings() {
+ Settings.save('iframely', $('.iframely-settings'), function () {
+ app.alert({
+ type: 'success',
+ alert_id: 'iframely-saved',
+ title: 'Settings Saved',
+ message: 'Please reload your NodeBB to apply these settings',
+ clickfn: function () {
+ socket.emit('admin.reload');
+ },
+ });
+ });
+ }
+
+ return ACP;
+});
diff --git a/client/client.js b/client/client.js
new file mode 100644
index 0000000..5570eb8
--- /dev/null
+++ b/client/client.js
@@ -0,0 +1,36 @@
+'use strict';
+
+/* global iframely */
+
+$(document).ready(function () {
+ $(window).on('action:ajaxify.end action:posts.loaded action:composer.preview', function () {
+ /**
+ * Iframely requires to call `iframely.load();` after widgets loaded to page.
+ * `action:ajaxify.end` - triggered when posts rendered on page.
+ * `action:posts.loaded` - triggered when you are on a topic page and a new post is sent to the client.
+ * `action:composer.preview` - triggered when new preview rendered in composer.
+ * In both cases Iframely need to initialize widgets.
+ */
+ iframely.load();
+ });
+
+ $(window).on('action:composer.preview', function () {
+ /**
+ * This logic prevents widget flickering while editing post in composer.
+ * When user opens post editor, widget will be collapsed and `click to prevew` button will be shown.
+ * Click event on that button will show widget expanded.
+ * Click button rendered in template:
+ * `static/templates/partials/iframely-widget-wrapper.tpl`
+ */
+ $('.iframely-container a[data-iframely-show-preview]').one('click', function (e) {
+ e.stopPropagation();
+ var $parent = $(this).parent();
+ var html = $parent.attr('data-html');
+ $parent.html(html);
+ return false;
+ });
+ });
+});
+
+
+
diff --git a/public/style.less b/less/style.less
similarity index 100%
rename from public/style.less
rename to less/style.less
diff --git a/lib/api.js b/lib/api.js
new file mode 100644
index 0000000..42755bf
--- /dev/null
+++ b/lib/api.js
@@ -0,0 +1,49 @@
+'use strict';
+
+const undici = require('undici');
+
+const { API_BASE } = require('./constants');
+const logger = require('./logger');
+
+
+const IframelyAPI = {};
+
+IframelyAPI.query = async function (data, endpoint = '') {
+ if (!endpoint) {
+ logger.error('No API key or endpoint configured, skipping Iframely');
+ return null;
+ }
+
+ const custom_endpoint = /^https?:\/\//i.test(endpoint);
+ let iframelyAPI = custom_endpoint ? endpoint : `${API_BASE}&api_key=${endpoint}`;
+ iframelyAPI += `${iframelyAPI.indexOf('?') > -1 ? '&' : '?'}url=${encodeURIComponent(data.url)}`;
+ if (custom_endpoint) {
+ iframelyAPI += '&group=true';
+ }
+
+ try {
+ const response = await undici.request(iframelyAPI);
+ if (response.statusCode === 404) {
+ logger.verbose(`Not found: ${data.url}`);
+ return null;
+ }
+
+ const json = await response.body.json();
+ if (response.statusCode !== 200 || !json) {
+ logger.verbose(`Iframely responded with error: ${JSON.stringify(json)}. Url: ${data.url}. Api call: ${iframelyAPI}`);
+ return null;
+ }
+
+ if (!json.meta || !json.links) {
+ logger.error(`Invalid Iframely API response. Url: ${data.url}. Api call: ${iframelyAPI}. Body: ${JSON.stringify(json)}`);
+ return null;
+ }
+
+ return json;
+ } catch (err) {
+ logger.error(`Encountered error querying Iframely API: ${err.message}. Url: ${data.url}. Api call: ${iframelyAPI}`);
+ return null;
+ }
+};
+
+module.exports = IframelyAPI;
diff --git a/lib/camo.js b/lib/camo.js
new file mode 100644
index 0000000..685d8e4
--- /dev/null
+++ b/lib/camo.js
@@ -0,0 +1,42 @@
+'use strict';
+
+const crypto = require('crypto');
+
+
+/**
+ * @param {string} html
+ * @param {{ key: string, host: string }} { key: camoProxyKey, host: camoProxyHost }
+ * @returns {string}
+ */
+function wrapHtmlImages(html, { key, host }) {
+ if (html && key && host) {
+ return html.replace(/]+src=["'][^'"]+["']/gi, (item) => {
+ const m = item.match(/(
]+src=["'])([^'"]+)(["'])/i);
+ const url = wrapImage(m[2]);
+ return m[1] + url + m[3];
+ });
+ }
+ return html;
+}
+
+/**
+ * @param {string} url
+ * @param {{ key: string, host: string }} { key: camoProxyKey, host: camoProxyHost }
+ * @returns {string}
+ */
+function wrapImage(url, { key, host }) {
+ if (url && key && host && url.indexOf(host) === -1) {
+ const hexDigest = crypto.createHmac('sha1', key).update(url).digest('hex');
+ const hexEncodedPath = Buffer.from(url).toString('hex');
+
+ return [
+ host.replace(/\/$/, ''), // Remove tail '/'
+ hexDigest,
+ hexEncodedPath,
+ ].join('/');
+ }
+
+ return url;
+}
+
+module.exports = { wrapHtmlImages, wrapImage };
diff --git a/lib/constants.js b/lib/constants.js
new file mode 100644
index 0000000..31a4184
--- /dev/null
+++ b/lib/constants.js
@@ -0,0 +1,17 @@
+'use strict';
+
+const url = require('url');
+const { nconf } = require('./nodebb');
+
+
+module.exports = {
+ HTML_REGEX: /(?:
]*>|
|^)
|<\/p>)?/gm,
+
+ ONE_DAY_MS: 1000 * 60 * 60 * 24,
+ DEFAULT_CACHE_MAX_AGE_DAYS: 1,
+
+ FORUM_URL: url.parse(nconf.get('url')),
+ UPLOADS_URL: url.parse(url.resolve(nconf.get('url'), nconf.get('upload_url'))),
+
+ API_BASE: 'https://iframe.ly/api/iframely?origin=nodebb&align=left',
+};
diff --git a/lib/controllers.js b/lib/controllers.js
index 19f9c65..531dbf9 100644
--- a/lib/controllers.js
+++ b/lib/controllers.js
@@ -1,20 +1,9 @@
'use strict';
-var Controllers = {};
-
-Controllers.renderAdminPage = function (req, res, next) {
- /*
- Make sure the route matches your path to template exactly.
-
- If your route was:
- myforum.com/some/complex/route/
- your template should be:
- templates/some/complex/route.tpl
- and you would render it like so:
- res.render('some/complex/route');
- */
+const Controllers = {};
+Controllers.renderAdminPage = function (req, res) {
res.render('admin/plugins/iframely', {});
};
-module.exports = Controllers;
\ No newline at end of file
+module.exports = Controllers;
diff --git a/lib/embed.js b/lib/embed.js
new file mode 100644
index 0000000..b99c0aa
--- /dev/null
+++ b/lib/embed.js
@@ -0,0 +1,197 @@
+'use strict';
+
+const { wrapHtmlImages, wrapImage } = require('./camo');
+const logger = require('./logger');
+const { validator } = require('./nodebb');
+const { getDate, getDuration, getViews, shortenText } = require('./utils');
+
+/**
+ * @param {*} embed
+ * @returns {string}
+ */
+function getDomain(embed) {
+ const domain = embed.meta.site;
+ if (domain) {
+ return domain;
+ }
+
+ const url = embed.meta.canonical;
+ const m = url.match(/(?:https?:\/\/)?(?:www\.)?([^/]+)/i);
+ return m ? m[1] : url;
+}
+
+/**
+ * @param {*} embed
+ * @returns {string | boolean}
+ */
+function getImage(embed) {
+ if (!embed || !embed.links) {
+ return '';
+ }
+
+ const image = (
+ (embed.links.thumbnail &&
+ embed.links.thumbnail.length &&
+ embed.links.thumbnail[0]) ||
+
+ (embed.links.image &&
+ embed.links.image.length &&
+ embed.links.image[0])
+ );
+
+ return image && image.href;
+}
+
+/**
+ * @param {*} embed
+ * @returns {string | boolean}
+ */
+function getIcon(embed) {
+ const icon =
+ embed &&
+ embed.links &&
+ embed.links.icon &&
+ embed.links.icon.length &&
+ embed.links.icon[0];
+
+ return icon && icon.href;
+}
+
+/**
+ * @param {string} html
+ * @returns {string}
+ */
+function getScriptSrc(html) {
+ const scriptMatch = html && html.match(/
\ No newline at end of file
+
\ No newline at end of file
diff --git a/public/templates/partials/iframely-link-title.tpl b/templates/partials/iframely-link-title.tpl
similarity index 100%
rename from public/templates/partials/iframely-link-title.tpl
rename to templates/partials/iframely-link-title.tpl
diff --git a/public/templates/partials/iframely-widget-card.tpl b/templates/partials/iframely-widget-card.tpl
similarity index 100%
rename from public/templates/partials/iframely-widget-card.tpl
rename to templates/partials/iframely-widget-card.tpl
diff --git a/public/templates/partials/iframely-widget-wrapper.tpl b/templates/partials/iframely-widget-wrapper.tpl
similarity index 100%
rename from public/templates/partials/iframely-widget-wrapper.tpl
rename to templates/partials/iframely-widget-wrapper.tpl