-
Notifications
You must be signed in to change notification settings - Fork 1
/
Copy pathcli.js
executable file
·143 lines (141 loc) · 5.31 KB
/
cli.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
#!/usr/bin/env -S npx nodejsscript
/* jshint esversion: 11,-W097, -W040, module: true, node: true, expr: true, undef: true *//* global echo, $, pipe, s, fetch, cyclicLoop */
import "nodejsscript";
import { randomMDN } from './index.js';
import { url_main, env_names } from './consts.js';
const { version, bin }= s.cat("./package.json").xargs(JSON.parse);
const [ name ]= Object.keys(bin);
$.api(name)
.version(version)
.describe([
"This script posts a new random article from MDN¹ to a given mastodon instance.",
"To post to the correct mastodon instance, use the `--url` and `--token` options.",
"The script has been highly inspired by the similar project² for Twitter.",
"",
`[1] ${url_main}`,
`[2] https://github.com/random-mdn/random-mdn-bot`
])
.command("json", "Print random article as JSON")
.action(()=> randomMDN().then(pipe( JSON.stringify, echo, $.exit.bind(null, 0) )))
.command("echo", "Print random article")
.action(()=> randomMDN().then(pipe( echo, $.exit.bind(null, 0) )))
.command("text", "Print random article as text – default", { default: true })
.action(()=> randomMDN().then(pipe( compose, echo, $.exit.bind(null, 0) )))
.command("mastodon", "Post to mastodon")
.option("--url", "instance url (e.g.: `https://mstdn.social`) – required")
.option("--token", "a token for the mastodon account – required")
.action(async function mastodon({
url= $.env[env_names.mastodon.url],
token= $.env[env_names.mastodon.token]
}){
if(!url) $.error(`Can't post without a URL, please use the '--url' option or enviroment variable '${env_names.mastodon.url}'.`);
if(!token) $.error(`Can't post without a token, please use the '--token' option or enviroment variable '${env_names.mastodon.token}'.`);
const status= await randomMDN().then(compose);
const res= await post({ url, token, status }).then(res=> res.json());
echo(res);
$.exit(0);
})
.command("rss", "Prints RSS feed for beeing used for example in [Newsboat, an RSS reader](https://newsboat.org/).")
.option("--limit, -l", "No. of articles to print – defaults to 3")
.action(async function rss({ limit= 3 }){
/** @type {import("./index.js").Article_object[]} */
const articles= await pipe(
length=> Array.from({ length }).map(randomMDN),
a=> Promise.all(a)
)(limit);
const articles_rss= articles.map(articleEncodeEntities).map(function({ title, description, link, github_file, updated }){
return [
"<item>",
"<title>"+title+"</title>",
"<description>"+description+"</description>",
"<link>"+link+"</link>",
"<guid>"+github_file+"</guid>",
"<lastBuildDate>"+(new Date(updated)).toUTCString()+"</lastBuildDate>",
"</item>"
].join("\n\t");
});
[
`<?xml version="1.0" encoding="UTF-8" ?>`,
`<rss version="2.0">`,
"<channel>",
`<title>🦖 Random MDN</title>`,
`<link>${url_main}</link>`,
...articles_rss,
"</channel>",
"</rss>"
].forEach(l=> echo(l));
$.exit(0);
})
.parse();
/** @param {{ url: string, token: script, status: string }} def */
async function post({ url, token, status }){
return fetch(new URL("api/v1/statuses", url), {
method: "POST",
headers: {
Authorization: "Bearer "+token,
'Content-Type': 'application/json'
},
body: JSON.stringify({ status, visibility: "public" })
});
}
/** @param {import("./index.js").Article_object} article @returns {string} */
function compose({ title, description, link, baseline }){
const limit= 500, reserve= 11; // see *
const { length }= description;
const baseline_text= !baseline ? " 🦖" : "\n"+getBaseline(baseline);
const hashtags= getHashtags(link);
const used_chars= title.length + link.length + hashtags.length + baseline_text.length;
description= description.slice(0, limit - reserve - used_chars);//cut last char in case '…'= 1 (*)
if(length - description.length) description+= "…";//….length= 1 (*)
return [
"🦖 " + title + baseline_text, //"🦖 ".length= 3 (*)
link,
description,
hashtags
].join("\n\n");//3×"\n\n"= 6 (*)
}
/** @param {import("./index.js").Baseline} baseline */
function getBaseline({ baseline, baseline_low_date }){
if(!baseline) return "🟧 Limited availability";
const intl= new Intl.DateTimeFormat("en-GB", { year: "numeric", month: "short" });
const date= new Date(baseline_low_date);
const label= baseline === "low"
? "☑️ Newly available"
: "✅ Widely available";
return `${label} (from ${intl.format(date)})`;
}
/** @param {import("./index.js").Article_object} article @returns {string} */
function articleEncodeEntities({ ...article }){
[ "title", "description" ]
.forEach(key=> article[key]= textEncodeEntities(article[key]));
return article;
}
function textEncodeEntities(text){//TODO: use lib?
const translate= {
" " : "nbsp",
"&" : "amp",
"\"": "quot",
"'" : "apos",
"<" : "lt",
">" : "gt"
};
return text.replace(new RegExp(`(${Object.keys(translate).join("|")})`, "g"), function(_, entity) {
return `&${translate[entity]};`;
});
}
/**
* Get appropriate hashtags for the URL
* (probably can be way smarter and better)
*
* @param {string} url
* @returns {string} fitting hashtags for the URL
*/
function getHashtags(url){
let hashtags= "#webdev";
const [ , section= "" ]= url.match(/Web\/(.*?)\//) || [];
if([ "Accessibility", "HTTP",
"CSS", "HTML", "JavaScript",
"MathML", "SVG" ].includes(section))
hashtags+= ` #${section}`;
return hashtags;
}