Skip to content

Instantly share code, notes, and snippets.

@max-degterev
Created October 8, 2020 16:37
Show Gist options
  • Save max-degterev/f6affc8a9d1ff325dedd9ee3a631fb1a to your computer and use it in GitHub Desktop.
Save max-degterev/f6affc8a9d1ff325dedd9ee3a631fb1a to your computer and use it in GitHub Desktop.
const got = require('got');
const sendMail = require('../mailer');
const render = require('./template');
const recipients = [
'email@email.email',
];
// berlin.de is new to the internet and wants every user of the API identify himself with a correct
// user agent 🙈 Let's see how they block these.
const userAgents = [
'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML like Gecko) Chrome/51.0.2704.79 Safari/537.36 Edge/14.14931',
'Mozilla/5.0 (Macintosh; Intel Mac OS X 10_14_2) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/71.0.3578.98 Safari/537.36',
'Mozilla/5.0 (Windows NT 6.1; WOW64; rv:64.0) Gecko/20100101 Firefox/64.0',
'Opera/9.80 (Macintosh; Intel Mac OS X 10.14.1) Presto/2.12.388 Version/12.16',
];
const getOptions = () => ({
responseType: 'json',
headers: { 'user-agent': userAgents[Math.floor(Math.random() * userAgents.length)] },
});
const COUNTRY_URL = 'https://rki-covid-api.now.sh/api/states';
const CITY_URL = 'https://www.berlin.de/lageso/gesundheit/infektionsepidemiologie-infektionsschutz/corona/tabelle-bezirke/index.php/index/all.json';
const CITY_EXTRAS_URL = 'https://knudmoeller.github.io/berlin_corona_cases/data/target/berlin_corona_traffic_light.json';
const OFFICIAL_STAT_ERROR_RATE_FACTOR = 8; // Latest number from the US
const NON_REPORTED_CASES_COMP_FACTOR = 10;
const COUNTRY_MORTALITY_RATE = .05;
const CITY_POPULATION_COUNT = 3562038;
const SYMPTOMS_ONSET_MIN_DIFF = 5;
const SYMPTOMS_ONSET_MAX_DIFF = 14;
const MULTIPLIER = OFFICIAL_STAT_ERROR_RATE_FACTOR * NON_REPORTED_CASES_COMP_FACTOR;
const padLeft = (number) => number.toString().padStart(2, '0');
const getDate = (diff) => {
const date = new Date();
date.setDate(date.getDate() + diff);
return `${date.getFullYear()}-${padLeft(date.getMonth() + 1)}-${padLeft(date.getDate())}`;
};
const sendEmail = ({ country, city, extras }) => {
const date = getDate(-1);
const infectionWindow = [
getDate(-SYMPTOMS_ONSET_MAX_DIFF),
getDate(-SYMPTOMS_ONSET_MIN_DIFF),
];
const props = { date, infectionWindow, country, city, extras };
const text = render(props);
if (!process.env.DEBUG) {
recipients.forEach((address) => (
sendMail(address, `Corona statistics for ${date}`, text)
));
} else {
console.log(text);
}
};
const addCountryFields = (target, item) => ['count', 'difference', 'deaths'].reduce((acc, key) => ({
...acc,
[key]: (target[key] || 0) + item[key],
}), {});
const formatCityFigures = ({ fallzahl, genesen }) => ({
count: parseInt(fallzahl, 10),
recovered: parseInt(genesen, 10),
// This figure is missing deaths count for a correct calculation
// Replacing with calculation based on Germany's death rate of 5%
approxActive: Math.max(
0,
parseInt(fallzahl, 10)
- parseInt(genesen, 10)
- Math.floor(parseInt(fallzahl, 10) * COUNTRY_MORTALITY_RATE),
),
});
const formatCityExtrasFigures = ({ indicators }) => ({
reproduction: indicators.basic_reproduction_number.value,
incidence: indicators.incidence_new_infections.value,
icu_occupancy: indicators.icu_occupancy_rate.value,
});
const mergeCityNumbers = (rki, berlin) => {
const { deaths, difference } = rki;
const { recovered } = berlin;
const count = Math.max(rki.count, berlin.count);
const active = count - deaths - recovered;
const realActive = active * MULTIPLIER;
const infectionRisk = (active / CITY_POPULATION_COUNT) * 100;
const realInfectionRisk = (realActive / CITY_POPULATION_COUNT) * 100;
const mortality = (deaths / count) * 100;
const recoveryRate = (recovered / count) * 100;
const headCount = Math.ceil(100 / infectionRisk);
const realHeadCount = Math.ceil(100 / realInfectionRisk);
return {
count,
difference,
deaths,
recovered,
active,
realActive,
infectionRisk,
realInfectionRisk,
mortality,
recoveryRate,
headCount,
realHeadCount,
mult: String(MULTIPLIER),
};
};
const getCountryNumbers = async() => {
const { body } = await got(COUNTRY_URL, getOptions());
const country = body.states.reduce((acc, item) => addCountryFields(acc, item), {});
const city = body.states.find(({ name }) => name === 'Berlin');
return { country, city };
};
const getCityNumbers = async() => {
const { body } = await got(CITY_URL, getOptions());
const city = formatCityFigures(body.index.find(({ bezirk }) => bezirk === 'Summe' || bezirk === 'Berlin'));
return city;
};
const getCityExtrasNumbers = async() => {
const { body } = await got(CITY_EXTRAS_URL, getOptions());
const extras = formatCityExtrasFigures(body[0]);
return extras;
};
const runJob = async() => {
const { country, city: rkiCity } = await getCountryNumbers();
const berlinCity = await getCityNumbers();
const extras = await getCityExtrasNumbers();
const city = mergeCityNumbers(rkiCity, berlinCity);
sendEmail({ country, city, extras });
};
module.exports = runJob;
const nodemailer = require('nodemailer');
const config = require('../config');
const credentials = [config.smtp.email, config.smtp.password].map(encodeURIComponent).join(':');
const transporter = nodemailer.createTransport(`smtps://${credentials}@smtp.gmail.com`);
const sendMail = (to, subject, text, done) => {
const options = {
to,
subject,
text,
from: config.contacts.from,
};
const complete = (error, result) => {
if (error) console.error(`Sending email failed: ${error}`);
else console.log(`Email sent: ${result.response}`);
if (done) done(error, result);
};
transporter.sendMail(options, complete);
};
module.exports = sendMail;
const LEVELS = ['critical', 'dangerous', 'elevated', 'acceptable', 'low'];
const getStatusLabel = ({ realInfectionRisk }) => {
if (realInfectionRisk > 10) return LEVELS[0];
if (realInfectionRisk > 6) return LEVELS[1];
if (realInfectionRisk > 4) return LEVELS[2];
if (realInfectionRisk > 2) return LEVELS[3];
return LEVELS[4];
};
// R-Wert < 1,1 = Grün
// R-Wert mindestens 3 Mal in Folge ≥ 1,1 = Gelb
// R-Wert mindestens 3 Mal in Folge ≥ 1,2 = Rot
const getReproductionStatusLabel = (value) => {
if (value > 1.2) return LEVELS[0];
if (value > 1.1) return LEVELS[1];
if (value > 1.0) return LEVELS[2];
return LEVELS[3];
};
// Zahl < 20 je 100.000 Einwohner*innen = Grün
// Zahl ≥ 20 je 100.000 Einwohner*innen = Gelb
// Zahl ≥ 30 je 100.000 Einwohner*innen = Rot
const getIncidenceStatusLabel = (value) => {
if (value > 30) return LEVELS[0];
if (value > 20) return LEVELS[1];
if (value > 10) return LEVELS[2];
return LEVELS[3];
};
// Anteil < 15 % = Grün
// Anteil ≥ 15 % = Gelb
// Anteil ≥ 25 % = Rot
const getICUStatusLabel = (value) => {
if (value > 25) return LEVELS[0];
if (value > 15) return LEVELS[1];
if (value > 10) return LEVELS[2];
return LEVELS[3];
};
const renderDetailedStats = ({
active, realActive,
infectionRisk, realInfectionRisk,
mortality, recoveryRate,
headCount, realHeadCount,
mult,
}) => `
Mortality rate: ${mortality.toFixed(2)}%
Recovery rate: ${recoveryRate.toFixed(2)}%
Total active cases: ${active}
Estimated real active cases (x${mult}): ${realActive}
Infection risk: ${infectionRisk.toFixed(2)}%
Estimated real infection risk (x${mult}): ${realInfectionRisk.toFixed(2)}%
Crowd factor: ${headCount}
Estimated real crowd factor (÷${mult}): ${realHeadCount}`;
const renderExtras = ({ recovered, deaths, difference, approxActive, ...detailed }) => (
[
[recovered, () => ` Recovered: ${recovered}`],
[difference, () => ` New active cases: +${difference}`],
[deaths, () => ` New deaths: +${deaths}`],
[approxActive, () => ` Total active cases (approx.): ${approxActive}`],
[typeof detailed.active === 'number', () => renderDetailedStats(detailed)],
]
.filter((item) => item[0])
.map((item) => item[1]())
.join('\n')
);
const renderNode = (label, { count, ...extras }) => `
Breakdown for ${label}:
Total reported cases: ${count}
${renderExtras(extras)}`;
module.exports = ({
date, country, city, infectionWindow, extras: { reproduction, incidence, icu_occupancy },
}) => `
Corona statistics for ${date}.
Current infection risk is ${city.realInfectionRisk.toFixed(2)}% (${getStatusLabel(city)}). Avoid groups of more than ${city.realHeadCount} people.
${renderNode('Germany', country)}
${renderNode('Berlin', city)}
Statistics over time (Berlin):
R: ${reproduction} (${getReproductionStatusLabel(reproduction)})
Incidence per 100k: ${incidence} (${getIncidenceStatusLabel(incidence)})
ICU Occupancy: ${icu_occupancy}% (${getICUStatusLabel(icu_occupancy)})
Infection window for these figures: ${infectionWindow[0]} -> ${infectionWindow[1]}.
`.trim();
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment