|
| 1 | +import * as fs from 'fs'; |
| 2 | +import * as path from 'path'; |
| 3 | +import { promisify } from 'util'; |
| 4 | +import * as gm from 'gray-matter'; |
| 5 | +import * as hb from 'handlebars'; |
| 6 | +import * as marked from 'marked'; |
| 7 | +import * as yaml from 'js-yaml'; |
| 8 | +import fetch from 'node-fetch'; |
| 9 | + |
| 10 | +const readFile = promisify(fs.readFile); |
| 11 | +const writeFile = promisify(fs.writeFile); |
| 12 | + |
| 13 | + |
| 14 | +const API_BASE = 'https://api.sendgrid.com/v3'; |
| 15 | +type SingleSendParams = { |
| 16 | + html: string, |
| 17 | + listId: string, |
| 18 | + suppressionGroup: number, |
| 19 | + token: string, |
| 20 | + sendAt?: Date, |
| 21 | + subject: string, |
| 22 | +}; |
| 23 | +async function singleSend(params: SingleSendParams) { |
| 24 | + return await fetch(`${API_BASE}/marketing/singlesends`, { |
| 25 | + method: 'POST', |
| 26 | + headers: { |
| 27 | + 'Authorization': `Bearer ${params.token}`, |
| 28 | + 'Content-type': 'application/json', |
| 29 | + }, |
| 30 | + body: JSON.stringify({ |
| 31 | + name: 'Test Send', |
| 32 | + send_at: params.sendAt?.toISOString(), |
| 33 | + send_to: { |
| 34 | + list_ids: [params.listId] |
| 35 | + }, |
| 36 | + email_config: { |
| 37 | + subject: params.subject, |
| 38 | + html_content: params.html, |
| 39 | + suppression_group_id: params.suppressionGroup |
| 40 | + } |
| 41 | + }) |
| 42 | + }); |
| 43 | +} |
| 44 | + |
| 45 | + |
| 46 | +type Options = { |
| 47 | + apiKey?: string, |
| 48 | + path: string, |
| 49 | + output?: string, |
| 50 | + template?: string, |
| 51 | + context?: any, |
| 52 | + listId?: string, |
| 53 | + suppressionGroupId?: number, |
| 54 | + siteYaml?: string, |
| 55 | +}; |
| 56 | + |
| 57 | +async function loadTemplate(path?: string, options?: CompileOptions) { |
| 58 | + const data = path ? await readFile(path) : '{{{ content }}}'; |
| 59 | + return hb.compile(data.toString(), options); |
| 60 | +} |
| 61 | + |
| 62 | +function splitTitleFromName(basename: string) { |
| 63 | + const m = basename.match(/^([^.]*)/); |
| 64 | + |
| 65 | + return [m[0], basename.slice(m[0].length)]; |
| 66 | +} |
| 67 | + |
| 68 | +function contextFromPath(filepath: string) { |
| 69 | + const basename = path.basename(filepath); |
| 70 | + const [title, ext] = splitTitleFromName(basename); |
| 71 | + const m = title.match(/(\d{4})-(\d\d?)-(\d\d?)-/); |
| 72 | + |
| 73 | + const slug = m ? title.slice(m.index + m[0].length) : title; |
| 74 | + const date = m ? |
| 75 | + new Date(parseInt(m[1]), parseInt(m[2])-1, parseInt(m[3])) |
| 76 | + : new Date(); |
| 77 | + |
| 78 | + return { |
| 79 | + title, |
| 80 | + slug, |
| 81 | + ext, |
| 82 | + basename, |
| 83 | + date, |
| 84 | + year: date.getFullYear(), |
| 85 | + month: date.getMonth()+1, |
| 86 | + day: date.getDate(), |
| 87 | + }; |
| 88 | +} |
| 89 | + |
| 90 | +function postUrl(post: ReturnType<typeof contextFromPath>, site: any) { |
| 91 | + const siteUrl = site.url; |
| 92 | + const basePath = site.baseurl ?? ''; |
| 93 | + |
| 94 | + return `${siteUrl}${basePath}/${post.year}/${post.month}/${post.day}/${post.slug}.html`; |
| 95 | +} |
| 96 | + |
| 97 | +function postContext(data: any, path: string, site?: any) { |
| 98 | + const post = contextFromPath(path); |
| 99 | + |
| 100 | + return Object.assign({ url: site ? postUrl(post, site) : '' }, |
| 101 | + post, data); |
| 102 | +} |
| 103 | + |
| 104 | +async function siteContext(path?: string) { |
| 105 | + if (!path) |
| 106 | + return {}; |
| 107 | + |
| 108 | + const contents = await readFile(path); |
| 109 | + return await yaml.load(contents.toString()); |
| 110 | +} |
| 111 | + |
| 112 | +async function render(opts: Options) { |
| 113 | + const pFile = readFile(opts.path); |
| 114 | + const pTemplate = loadTemplate(opts.template); |
| 115 | + const site = await siteContext(opts.siteYaml); |
| 116 | + |
| 117 | + // if () |
| 118 | + |
| 119 | + const raw = await pFile; |
| 120 | + const { content, data } = gm(raw.toString('utf8')); |
| 121 | + |
| 122 | + const rendered = marked(content, { |
| 123 | + headerPrefix: 'heading-', |
| 124 | + gfm: true, |
| 125 | + }); |
| 126 | + |
| 127 | + const template = await pTemplate; |
| 128 | + const context = Object.assign({ |
| 129 | + content: rendered, |
| 130 | + post: postContext(data, opts.path, site), |
| 131 | + site, |
| 132 | + }, opts.context); |
| 133 | + const text = template(context); |
| 134 | + |
| 135 | + return { |
| 136 | + text, |
| 137 | + context |
| 138 | + }; |
| 139 | +} |
| 140 | + |
| 141 | +async function run(options: Options) { |
| 142 | + const { text, context } = await render(options); |
| 143 | + // console.log(context); |
| 144 | + |
| 145 | + if (options.output) { |
| 146 | + await writeFile(options.output, text); |
| 147 | + } else if (options.apiKey) { |
| 148 | + const response = await singleSend({ |
| 149 | + html: text, |
| 150 | + listId: options.listId, |
| 151 | + suppressionGroup: options.suppressionGroupId, |
| 152 | + token: options.apiKey, |
| 153 | + sendAt: new Date(Date.now() + 60000), |
| 154 | + subject: `Test Newsletter: ${context.post.title}`, |
| 155 | + }); |
| 156 | + |
| 157 | + console.log(response.status, response.statusText, response.headers, await response.text()); |
| 158 | + } else { |
| 159 | + console.log(text); |
| 160 | + } |
| 161 | +} |
| 162 | + |
| 163 | +async function runAction() { |
| 164 | + const { |
| 165 | + INPUT_SENDGRID_LIST_ID: listId, |
| 166 | + INPUT_SENDGRID_API_KEY: apiKey, |
| 167 | + INPUT_TEMPLATE_PATH: template, |
| 168 | + INPUT_TEXT_PATH: path, |
| 169 | + INPUT_CONTEXT: context, |
| 170 | + INPUT_OUT_PATH: outPath, |
| 171 | + INPUT_SUPPRESSION_GROUP_ID: suppressionGroupId, |
| 172 | + INPUT_SITE_YAML: siteYaml, |
| 173 | + } = process.env; |
| 174 | + |
| 175 | + if (!path) { |
| 176 | + console.error( |
| 177 | + 'Missing required environment variable INPUT_TEXT_PATH' |
| 178 | + ); |
| 179 | + process.exit(1); |
| 180 | + } |
| 181 | + |
| 182 | + await run({ |
| 183 | + apiKey, |
| 184 | + path, |
| 185 | + template, |
| 186 | + output: outPath, |
| 187 | + context: context ? JSON.parse(context) : {}, |
| 188 | + listId: listId, |
| 189 | + suppressionGroupId: suppressionGroupId ? parseInt(suppressionGroupId) : undefined, |
| 190 | + siteYaml, |
| 191 | + }); |
| 192 | +} |
| 193 | + |
| 194 | +async function testRun() { |
| 195 | + process.env['INPUT_SENDGRID_LIST_ID'] = "559adb5e-7164-4ac8-bbb5-1398d4ff0df9"; |
| 196 | + // process.env['INPUT_SENDGRID_API_KEY'] = apiKey; |
| 197 | + process.env['INPUT_TEXT_PATH'] = __dirname + '/../../../../_posts/2021-11-16-communications-lead.md'; |
| 198 | + process.env['INPUT_TEMPLATE_PATH'] = __dirname + '/../../../workflows/newsletter_template.html'; |
| 199 | + process.env['INPUT_CONTEXT'] = `{}`; |
| 200 | + process.env['INPUT_SUPPRESSION_GROUP_ID'] = '17889'; |
| 201 | + process.env['INPUT_SITE_YAML'] = __dirname + '/../../../../_config.yml'; |
| 202 | + |
| 203 | + await runAction(); |
| 204 | +} |
| 205 | + |
| 206 | +runAction(); |
0 commit comments