Skip to content

Commit 5900238

Browse files
authored
Merge pull request #142 from codeforboston/newsletter-template-updates
Newsletter template updates
2 parents 0777943 + c1f0c02 commit 5900238

File tree

8 files changed

+1268
-872
lines changed

8 files changed

+1268
-872
lines changed

.github/actions/render-newsletter/action.yml

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,10 @@ inputs:
88
description: 'Path to directory containing posts'
99
required: false
1010

11+
after_date:
12+
description: 'Only render posts that have a send date >= this ISO 8601-formatted date'
13+
required: false
14+
1115
out_path:
1216
description: 'Path where the output should be written'
1317
required: false

.github/actions/render-newsletter/dist/index.js

Lines changed: 2 additions & 2 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

.github/actions/render-newsletter/src/index.ts

Lines changed: 35 additions & 47 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,8 @@ import * as marked from 'marked';
88
import * as yaml from 'js-yaml';
99
import fetch from 'node-fetch';
1010

11+
import * as SG from './sendgrid';
12+
1113
const readFile = promisify(fs.readFile);
1214
const writeFile = promisify(fs.writeFile);
1315
const readdir = promisify(fs.readdir);
@@ -45,45 +47,6 @@ async function postToSlack(slackUrl: string, url: string) {
4547
});
4648
}
4749

48-
const API_BASE = 'https://api.sendgrid.com/v3';
49-
type SingleSendParams = {
50-
html: string,
51-
listId: string,
52-
suppressionGroup: number,
53-
token: string,
54-
sendAt?: Date,
55-
subject: string,
56-
};
57-
async function singleSend(params: SingleSendParams) {
58-
return await fetch(`${API_BASE}/marketing/singlesends`, {
59-
method: 'POST',
60-
headers: {
61-
'Authorization': `Bearer ${params.token}`,
62-
'Content-type': 'application/json',
63-
},
64-
body: JSON.stringify({
65-
name: `Newsletter: ${params.subject}`,
66-
send_at: params.sendAt?.toISOString(),
67-
send_to: {
68-
list_ids: [params.listId]
69-
},
70-
email_config: {
71-
subject: params.subject,
72-
html_content: params.html,
73-
suppression_group_id: params.suppressionGroup
74-
}
75-
})
76-
});
77-
}
78-
79-
type GetSingleSendsParams = {
80-
81-
};
82-
83-
async function getSingleSends(params: GetSingleSendsParams) {
84-
85-
}
86-
8750
type Options = {
8851
apiKey?: string,
8952
filePath: string,
@@ -95,6 +58,7 @@ type Options = {
9558
siteYaml?: string,
9659
subject?: string,
9760
slackUrl?: string,
61+
index?: SG.SingleSendIndex,
9862
};
9963

10064
async function loadTemplate(path?: string, options?: CompileOptions) {
@@ -192,6 +156,9 @@ async function render(opts: Options) {
192156
};
193157
}
194158

159+
const dateStr =
160+
(d: Date | string) => ((typeof d === 'string' ? d : d.toISOString()).split('T', 1)[0]);
161+
195162
function getSendDate(c: TemplateContext) {
196163
let date = c.post.date;
197164
if (date.getTime() <= Date.now()) {
@@ -205,21 +172,42 @@ function getSendDate(c: TemplateContext) {
205172
return date;
206173
}
207174

175+
function singleSendId(context: TemplateContext, index?: SG.SingleSendIndex) {
176+
if (!index)
177+
return undefined;
178+
179+
const date = dateStr(context.post.date);
180+
181+
for (const ss of Object.values(index.byId)) {
182+
if (dateStr(ss.send_at) === date)
183+
return ss.id;
184+
185+
if (ss.name.includes(context.post.title))
186+
return ss.id;
187+
}
188+
}
189+
208190
async function run(options: Options) {
209191
const { text, context } = await render(options);
210-
// console.log(context);
211192

212193
if (options.output) {
213194
await writeFile(options.output, text);
214195
} else if (options.apiKey) {
215196
const sendAt = getSendDate(context);
216-
const response = await singleSend({
197+
const id = singleSendId(context, options.index);
198+
199+
if (id)
200+
console.log(`Updating existing Single Send ${id}`);
201+
202+
const response = await SG.singleSend({
217203
html: text,
218204
listId: options.listId,
219205
suppressionGroup: options.suppressionGroupId,
220206
token: options.apiKey,
221207
sendAt,
222208
subject: (options.subject ?? '%s').replace('%s', context.post.title),
209+
categories: ['newsletter'],
210+
id,
223211
});
224212

225213
const url = response.headers.get('location');
@@ -246,7 +234,7 @@ type RunOptions = Omit<Options, 'filePath'> & {
246234
function dateFilter(after: number) {
247235
return (path: string) => {
248236
const ctx = contextFromPath(path);
249-
return ctx.date.getTime() > after;
237+
return ctx.date.getTime() >= after;
250238
};
251239
}
252240

@@ -271,10 +259,13 @@ async function runAll(options: RunOptions) {
271259
return;
272260
}
273261

262+
const index = await SG.indexSingleSends({ token: options.apiKey });
263+
274264
for (const post of posts) {
275265
const result = await run({
276266
...options,
277-
filePath: post
267+
filePath: post,
268+
index,
278269
});
279270

280271
if (result?.url && options.slackUrl) {
@@ -299,7 +290,7 @@ async function runAction() {
299290
INPUT_SUBJECT_FORMAT: subject = '%s',
300291
INPUT_POSTS_DIR: postsDir,
301292
INPUT_SLACK_URL: slackUrl,
302-
TODAY_OVERRIDE: today,
293+
INPUT_AFTER_DATE: today,
303294
} = process.env;
304295

305296
if (!(path || postsDir)) {
@@ -325,7 +316,6 @@ async function runAction() {
325316
}
326317

327318
async function testRun() {
328-
// const apiKey = 'REAS-yuff0naum!krar';
329319
process.env['INPUT_SENDGRID_LIST_ID'] = "559adb5e-7164-4ac8-bbb5-1398d4ff0df9";
330320
// process.env['INPUT_SENDGRID_API_KEY'] = apiKey;
331321
// process.env['INPUT_TEXT_PATH'] = __dirname + '/../../../../_posts/2021-11-16-communications-lead.md';
@@ -334,8 +324,6 @@ async function testRun() {
334324
process.env['INPUT_CONTEXT'] = `{}`;
335325
process.env['INPUT_SUPPRESSION_GROUP_ID'] = '17889';
336326
process.env['INPUT_SITE_YAML'] = __dirname + '/../../../../_config.yml';
337-
process.env['INPUT_SLACK_URL'] = 'https://hooks.slack.com/services/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';
338-
process.env['TODAY_OVERRIDE'] = '2022-01-10';
339327

340328
await runAction();
341329
}
Lines changed: 119 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,119 @@
1+
import fetch from 'node-fetch';
2+
3+
export type ScheduledSend = {
4+
id: string,
5+
name: string,
6+
status: string,
7+
categories: string[],
8+
send_at: string,
9+
created_at: string,
10+
updated_at: string,
11+
is_abtest: boolean,
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+
categories?: string[],
23+
id?: string,
24+
};
25+
export async function singleSend(params: SingleSendParams) {
26+
const url = `${API_BASE}/marketing/singlesends` +
27+
(params.id ? `/${params.id}` : '');
28+
return await fetch(url, {
29+
method: params.id ? 'PATCH' : 'POST',
30+
headers: {
31+
'Authorization': `Bearer ${params.token}`,
32+
'Content-type': 'application/json',
33+
},
34+
body: JSON.stringify({
35+
name: `Newsletter: ${params.subject}`,
36+
send_at: params.sendAt?.toISOString(),
37+
send_to: {
38+
list_ids: [params.listId]
39+
},
40+
categories: params.categories,
41+
email_config: {
42+
subject: params.subject,
43+
html_content: params.html,
44+
suppression_group_id: params.suppressionGroup
45+
}
46+
})
47+
});
48+
}
49+
50+
export async function deleteSingleSend(params: any) {
51+
const { id, token } = params;
52+
return await fetch(`${API_BASE}/marketing/singlesends/${id}`, {
53+
method: 'DELETE',
54+
headers: {
55+
'Authorization': `Bearer ${token}`
56+
}
57+
});
58+
}
59+
60+
type GetSingleSendsParams = {
61+
token: string,
62+
categories?: string[]
63+
};
64+
65+
export async function *getSingleSends(params: GetSingleSendsParams) {
66+
let url = `${API_BASE}/marketing/singlesends/search`;
67+
while (url) {
68+
const response = await fetch(url, {
69+
method: 'POST',
70+
headers: {
71+
'Authorization': `Bearer ${params.token}`,
72+
'Content-type': 'application/json',
73+
},
74+
body: JSON.stringify({
75+
status: ['scheduled', 'draft'],
76+
categories: params.categories
77+
})
78+
});
79+
80+
const { result, _metadata: meta } = await response.json();
81+
yield *(result as ScheduledSend[]);
82+
url = meta.next;
83+
}
84+
}
85+
86+
export type SingleSendIndex = {
87+
byId: Record<string, ScheduledSend>,
88+
byName: Record<string, string>,
89+
byDate: Record<string, string[]>,
90+
}
91+
export async function indexSingleSends(params: GetSingleSendsParams) {
92+
const idx: SingleSendIndex = { byId: {}, byName: {}, byDate: {}, };
93+
94+
for await (const ss of getSingleSends(params)) {
95+
const { id } = ss;
96+
97+
idx.byId[id] = ss;
98+
idx.byName[ss.name] = id;
99+
100+
const date = ss.send_at.split('T', 1)[0];
101+
if (!idx.byDate[date])
102+
idx.byDate[date] = [];
103+
idx.byDate[date].push(id);
104+
}
105+
106+
return idx;
107+
}
108+
109+
export async function cleanup(params: GetSingleSendsParams) {
110+
const deleteBefore = new Date(2022, 2, 2).getTime();
111+
for await (const ss of getSingleSends(params)) {
112+
if (new Date(ss.send_at).getTime() < deleteBefore) {
113+
await deleteSingleSend({
114+
id: ss.id,
115+
token: params.token,
116+
});
117+
}
118+
}
119+
}
Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,18 @@
1+
#!/bin/bash
2+
3+
if [ -z "$INPUT_AFTER_DATE" ]; then
4+
export INPUT_AFTER_DATE=$(git log --pretty=format:%ai -1)
5+
fi
6+
7+
if [ -z "$INPUT_SENDGRID_API_KEY" ]; then
8+
export INPUT_SENDGRID_API_KEY=$(security find-generic-password -s 'newsletter-api-key' -a 'sendgrid-cfb' -w)
9+
fi
10+
11+
if [ -z "$INPUT_SLACK_URL" ]; then
12+
hackoncfb_slack_url=$(security find-generic-password -s 'slack-webhook' -a 'cfb-hackoncfb' -w)
13+
beacontest2_slack_url=$(security find-generic-password -s 'slack-webhook' -a 'cfb-beacontest2' -w)
14+
export INPUT_SLACK_URL="$hackoncfb_slack_url"
15+
# export INPUT_SLACK_URL="$beacontest2_slack_url"
16+
fi
17+
18+
NODE_ENV=test ts-node ./src/index.ts

.github/workflows/commit-date.sh

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
#!/bin/bash
2+
3+
commit_date=$(git log --pretty=format:%ai -1)
4+
5+
echo "::set-output name=commit_date::$commit_date"

0 commit comments

Comments
 (0)