-
Notifications
You must be signed in to change notification settings - Fork 0
/
prepare.js
executable file
·305 lines (276 loc) · 9.25 KB
/
prepare.js
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
#!/usr/bin/env node
const fs = require("fs");
const {flatten, shuffle} = require("./util");
require("dotenv").config();
const {
subject,
html,
getTokens,
insertVariables,
getLetter,
getFallback,
getSender,
getExtra
} = require("./template");
const {
supabase,
getDigests,
getTopPics,
getTopComments,
getLastCount,
} = require("./api");
const { getTargets, filter } = require("./targets");
const color = require("cli-color");
const countries = require("i18n-iso-countries");
const { preview, initPreview } = require("./mailer");
const { getStats } = require("./server");
let csv = "name,email,saluation,gender,language,area,external_id";
const argv = require("minimist")(process.argv.slice(2), {
boolean: ["help", "dry-run", "verbose", "csv", "shuffle"],
string: ["file", "lang", "template", "fallback", "preview"],
unknown: (param) => {param[0] === '-' ? console.error("invalid parameter",param) || process.exit(1) : true},
default: { template: "default", csv: true, min: 0, shuffle: true },
});
const writeCsv = () => {
const filename = "/tmp/" + dateFormat(createdAt) + ".csv";
fs.writeFileSync(filename, csv);
console.log(color.green("saved", filename));
};
const help = () => {
console.log(
[
"--help (this command)",
"--template (template folder in config/email/digest/campaign_mame), by default default.xx.html",
"--file (source file in config/targets/digest/), by default campaign_name.json",
"--lang (default language if not specified in source)",
"--force process even if there are already pending digests waiting to be sent",
"--min skip the sending if supporter counts below min -or use fallback template",
"--fallback template to use if no tops or something else is missing or below min",
"--dry-run",
"--verbose",
"--csv|no-csv generate a csv with the targets + some variables",
"--preview ( 'etherhal' or 'mailhog')",
"--target= [email protected] or number of targets to process",
"{campaign_name}",
].join("\n")
);
process.exit(0);
};
const createdAt = new Date();
const dateFormat = (date) => {
const utc = "getUTC"; // 'get'?
return "%Y%m%d_%H%M%S".replace(/%[YmdHMS]/g, function (m) {
switch (m) {
case "%Y":
return date[utc + "FullYear"](); // no leading zeros required
case "%m":
m = 1 + date[utc + "Month"]();
break;
case "%d":
m = date[utc + "Date"]();
break;
case "%H":
m = date[utc + "Hours"]();
break;
case "%M":
m = date[utc + "Minutes"]();
break;
case "%S":
m = date[utc + "Seconds"]();
break;
default:
return m.slice(1); // unknown code, remove %
}
// add leading zero if required
return ("0" + m).slice(-2);
});
};
const checkToken = (tokens,variables) => {
const keys = Object.keys(flatten(variables));
const missing = tokens.filter ( k => { return !keys.includes(k.replace("- ",""))});
if (missing.length !== 0) {
console.error(color.red("incorrect or unknown tokens",missing));
process.exit(1);
}
return true;
}
const campaign = argv._[0];
if (argv.help || !campaign) return help();
if (!argv.file) argv.file = campaign;
let templateName = argv["template"]; // TODO: for each target, check if the target has received an email, "initial", otherwise, "default"
const sourceName = argv["file"];
const fallback = argv["fallback"] === "" ? "fallback" : argv["fallback"];
console.log(
color.green("timestamp of the digest", dateFormat(createdAt)),
createdAt.toString()
);
let targets = getTargets(sourceName, campaign);
if (targets.length === 0) {
console.error(color.red("no targets found", argv.file));
process.exit(1);
}
console.log("targetting", targets.length, "from", sourceName);
const sender = getSender(campaign);
const prepare = async (target, templateName, campaign, data, last) => {
if (!target.locale && target.lang && argv.lang) {
console.warn("no language for", target.name, target.email);
}
if (!fallback && data.country[target.area] < argv.min) {
console.warn(
color.yellow("skipping", target.name),
target.area,
"has only",
data.country[target.area],
"supporters"
);
return;
}
const locale = argv.locale || target.locale;
const pics = await getTopPics(campaign, target.area);
const comments = await getTopComments(campaign, target.area);
//extra supporters per country
let extra = getExtra(target.area, campaign);
let variables = {
target: { ...target },
country: {
code: target.area,
name: countries.getName(target.area, locale) || "",
total: data.country[target.area] + extra,
extra: extra,
},
total: data.total,
campaign: {
letter: getLetter(campaign, locale),
period: {
total: data.total - last.lastTotal,
country: data.country[target.area] + extra - last.lastCountryTotal,
},
},
top: {
pictures: pics.html,
comments: comments.html,
},
};
variables.comments = comments.data;
variables.pictures = pics.data;
delete variables.target.email;
delete variables.target.externalId;
delete variables.target.field;
console.log(`${target.area} new and old total`, variables.total, last.lastTotal);
console.log(`${target.area} new country total, old country total and country delta:`, data.country[target.area] + extra, last.lastCountryTotal, variables.campaign.period.country);
let s;
let template;
if (
(data.country[target.area] < argv.min ||
!variables.comments ||
!variables.pictures === 0) &&
fallback
) {
console.warn(
color.yellow("fallback for", target.name),
"from",
target.area,
data.country[target.area],
"supporters"
);
const fallbackSubject = subject(campaign, fallback, locale);
if (fallbackSubject) {
s = fallbackSubject;
} else {
s = subject(campaign, templateName, locale);
}
template = html(campaign, fallback, locale);
const pics = await getTopPics(campaign);
const comments = await getTopComments(campaign);
variables.top.comments = comments.html;
variables.top.pictures = pics.html;
variables.comments = comments.data;
variables.pictures = pics.data;
} else {
s = subject(campaign, templateName, locale);
template = html(campaign, templateName, locale);
}
// TODO: get the template and tokens higher in the code and conditionally build variables to only be set if the token is required by the template, for instance don't fetch the top pictures if there aren't any top.picture
const tokens = getTokens(template);
checkToken (tokens,variables); // stops if there is a token that isn't set in variables
if (argv.verbose) console.log("We need variables for each of these", tokens);
if (!s) {
console.error(color.red("Subject not found:", target.name));
throw new Error("Subject not found:", target);
}
if (!template) {
console.error("HTML not found:", target);
throw new Error("HTML not found:", target);
}
// fetch variables
// insert variables in template
const body = insertVariables(template, variables);
if (argv.verbose) console.log(target.email, locale, templateName, s);
delete variables.top;
const info = {
created_at: createdAt,
subject: s,
body: body,
status: "pending",
template: templateName,
campaign: campaign,
email: target.email,
target_id: target.externalId || target.email,
variables: variables,
};
if (argv["dry-run"]) return info;
const { error } = await supabase.from("digest").insert([info]);
if (error) {
console.error(color.red("error saving digest"), error, target);
throw error;
}
return info;
};
const main = async () => {
const pending = await getDigests(campaign, "pending");
const stats = await getStats(campaign);
if (pending.length > 0) {
console.log("targetted already", pending.length, "from", sourceName);
if (!argv.force && !argv["dry-run"]) {
console.error(
color.red(
"send the prepared digests before preparing new ones or run with --force"
)
);
process.exit(1);
}
}
targets = filter(targets, argv.target);
if (argv.preview !== undefined) {
await initPreview(argv.preview);
csv + ",preview";
}
if (argv.shuffle) shuffle(targets);
for (const i in targets) {
const target = targets[i];
// todo: if template not set, supabase.select email,target_id from digests where campaign=campaign and status='sent' group by email
// if in that list -> template= default, else -> initial
csv += `\n${target.name},${target.email},${target.salutation},${target.gender},${target.locale},${target.area},${target.externalId}`;
const last =
templateName === "initial"
? { lastTotal: 0, lastCountryTotal: 0 }
: await getLastCount(campaign, target.email);
const r = await prepare(
{ ...targets[i] },
templateName,
campaign,
stats,
last
);
if (argv.preview && r) {
const b = insertVariables(r.body, r.variables);
if (argv.preview !== undefined) {
const info = await preview(r.email, r.subject, b, sender);
info.url && console.log(color.green(info.url));
csv += "," + info.url;
}
}
}
writeCsv();
};
main().catch(console.error);