node-red-contrib-chatbot
Version:
REDBot a Chat bot for a full featured chat bot for Telegram, Facebook Messenger and Slack. Almost no coding skills required
233 lines (189 loc) • 7.63 kB
JavaScript
/* eslint-disable no-console */
const marked = require('marked');
const clc = require('cli-color');
const fs = require('fs');
const path = require('path');
const crypto = require('crypto');
const fetch = require('node-fetch');
const { Client } = require('@notionhq/client')
const { NotionToMarkdown } = require('notion-to-md');
const green = clc.greenBright;
// const white = clc.white;
const grey = clc.blackBright;
const cyan = clc.cyanBright;
const yellow = clc.yellowBright;
// const orange = clc.xterm(214);
const nodeDefinitions = require('./nodes');
// notion api key
let notionAuthToken = process.env.NOTION_API_KEY;
if (fs.existsSync(`${__dirname}/../.env-notion`)) {
const raw = fs.readFileSync(`${__dirname}/../.env-notion`, 'utf8').trim();
const match = raw.match(/^\s*(?:NOTION_API_KEY\s*=\s*)?["']?([^"'\s]+)["']?\s*$/m);
notionAuthToken = match != null ? match[1] : raw;
}
if (notionAuthToken == null || notionAuthToken === '') {
console.log('Missing notion token, skipping');
process.exit();
}
// Initializing a client
const notion = new Client({ auth: notionAuthToken });
const n2m = new NotionToMarkdown({ notionClient: notion });
const extractNotionId = url => {
const cleanUrl = url.split('?')[0].split('#')[0];
const matched = cleanUrl.match(/\-([a-z0-9]{32,32})$/);
if (matched != null) {
let notionId = String(matched[1]);
return notionId.substring(0, 8) + '-'
+ notionId.substring(8, 12) + '-'
+ notionId.substring(12, 16) + '-'
+ notionId.substring(16, 20) + '-'
+ notionId.substring(20, 32);
}
};
const assetsDir = path.join(__dirname, '..', 'docs', 'assets');
const assetsRelative = './docs/assets';
const mimeTypeFor = ext => {
switch (ext.toLowerCase()) {
case 'jpg':
case 'jpeg':
return 'image/jpeg';
case 'svg':
return 'image/svg+xml';
case 'webp':
return 'image/webp';
case 'gif':
return 'image/gif';
default:
return 'image/png';
}
};
// Scan markdown for image URLs, download each one to ./docs/assets and
// return a map of { originalUrl: relativeLocalPath } so we can rewrite the
// markdown / HTML to point at the local copies instead of the Notion S3
// links that embed short-lived access tokens.
const extractImages = async function(mdSource) {
if (!fs.existsSync(assetsDir)) {
fs.mkdirSync(assetsDir, { recursive: true });
}
const urlToLocal = {};
const imageRegex = /!\[[^\]]*\]\(([^)]+)\)/g;
let match;
while ((match = imageRegex.exec(mdSource)) !== null) {
const rawUrl = match[1].trim().replace(/\s+".*"$/, '');
if (!/^https?:\/\//i.test(rawUrl) || urlToLocal[rawUrl]) {
continue;
}
const cleanUrl = rawUrl.split('?')[0];
const hash = crypto.createHash('md5').update(cleanUrl).digest('hex').substring(0, 16);
const extMatch = cleanUrl.match(/\.([a-zA-Z0-9]{2,4})$/);
const ext = extMatch ? extMatch[1].toLowerCase() : 'png';
const filename = `${hash}.${ext}`;
const localPath = path.join(assetsDir, filename);
if (!fs.existsSync(localPath)) {
const displayUrl = cleanUrl.replace(/^https?:\/\//, '');
const shortUrl = displayUrl.length > 70 ? displayUrl.substring(0, 67) + '...' : displayUrl;
console.log(` ${cyan('↓')} ${yellow(filename)} ${grey('← ' + shortUrl)}`);
try {
const response = await fetch(rawUrl);
if (!response.ok) {
console.log(` ${clc.redBright('✗')} ${grey('HTTP ' + response.status)}`);
continue;
}
const buffer = await response.buffer();
fs.writeFileSync(localPath, buffer);
} catch (err) {
console.log(` ${clc.redBright('✗')} ${grey(err.message)}`);
continue;
}
}
urlToLocal[rawUrl] = `${assetsRelative}/${filename}`;
}
return urlToLocal;
};
const parseMarkdownWithImages = async function(mdSource) {
const urlToLocal = await extractImages(mdSource);
let markdown = mdSource;
for (const [originalUrl, localPath] of Object.entries(urlToLocal)) {
markdown = markdown.split(originalUrl).join(localPath);
}
return { markdown, html: marked.parse(markdown) };
};
const inlineAssetsAsBase64 = htmlSource => {
const assetRegex = /\.\/docs\/assets\/([A-Za-z0-9_-]+\.[A-Za-z0-9]{2,4})/g;
return htmlSource.replace(assetRegex, (full, filename) => {
const localPath = path.join(assetsDir, filename);
if (!fs.existsSync(localPath)) {
return full;
}
const ext = path.extname(filename).slice(1);
const base64 = fs.readFileSync(localPath).toString('base64');
return `data:${mimeTypeFor(ext)};base64,${base64}`;
});
};
const getMarkdownPage = async function(url) {
// parse url
const notionId = extractNotionId(url);
// TODO check if valid
const mdblocks = await n2m.pageToMarkdown(notionId);
const mdString = n2m.toMarkdownString(mdblocks);
return mdString;
};
const runner = async function() {
console.log('# ' + grey('Building changelog:'));
console.log('');
const mdChangeLog = await getMarkdownPage('https://www.notion.so/redbot/Change-log-b46a94ab6bbc4c7d8a586cbc21af7d78');
const mdReleases = mdChangeLog.match(/\|(.*)\|/gm);
let changelog = '';
mdReleases.forEach(row => {
const splitted = row.split(' | ');
if (splitted.length >= 2 && splitted[0].indexOf('------') === -1) {
const version = splitted[0].replace(/^\|/, '').trim();
const description = splitted[1].replace(/\\n$/, '').replace(/\|$/, '').trim();
changelog += `- **${version}** - ${description}\n`;
}
});
fs.writeFileSync(
__dirname + '/../CHANGELOG.md',
changelog,
'utf8'
);
console.log('# ' + grey('Downloading nodes help:'));
console.log('');
const nodesDocsDir = __dirname + '/../docs/nodes';
fs.mkdirSync(nodesDocsDir, { recursive: true });
const docsIndex = [];
// download all nodes documentation from notion
let idx = 0;
for(idx = 0; idx < nodeDefinitions.length; idx++) {
const node = nodeDefinitions[idx];
console.log('- ' + grey(node.notionUrl) + ' (' + node.nodeType + ')');
const mdSource = await getMarkdownPage(node.notionUrl);
const { markdown: safeMd, html: htmlSource } = await parseMarkdownWithImages(mdSource);
// write markdown locally (with image URLs rewritten to ./docs/assets)
fs.writeFileSync(nodesDocsDir + '/' + node.nodeType + '.md', safeMd, 'utf8');
const titleMatch = safeMd.match(/^#\s+(.+)$/m);
const title = titleMatch != null ? titleMatch[1].trim() : node.nodeType;
docsIndex.push({ nodeType: node.nodeType, title });
let nodeSource;
try {
nodeSource = fs.readFileSync(__dirname + '/../nodes/' + node.nodeFile, 'utf8');
} catch(e) {
console.log(`Unable to find file ${node.nodeFile}`);
}
const inlinedHtml = inlineAssetsAsBase64(htmlSource);
const regexp = new RegExp('<script type=\"(text\/x-red|text\/html)\" data-help-name=\"' + node.nodeType + '\">[\\s\\S]*?<\/script>', 'g');
nodeSource = nodeSource.replace(regexp, (_match, scriptType) => {
return '<script type="' + scriptType + '" data-help-name="' + node.nodeType + '">' + inlinedHtml + '</script>';
});
fs.writeFileSync(__dirname + '/../nodes/' + node.nodeFile, nodeSource, 'utf8');
}
docsIndex.sort((a, b) => a.title.localeCompare(b.title));
const indexContent = '# Nodes\n\n'
+ docsIndex.map(({ nodeType, title }) => `- [${title}](nodes/${nodeType}.md)`).join('\n')
+ '\n';
fs.writeFileSync(__dirname + '/../docs/nodes.md', indexContent, 'utf8');
// end
console.log(green('All done.'));
console.log('');
};
runner();