Skip to content

Commit 3bfe401

Browse files
authored
Merge pull request #28 from sustained/feat-rfcs
feat: RFC commands
2 parents 60de957 + b90d102 commit 3bfe401

File tree

16 files changed

+680
-8
lines changed

16 files changed

+680
-8
lines changed

.env.example

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
BOT_TOKEN=
22
CLIENT_ID=
33
OWNERS_IDS=
4-
COMMAND_PREFIX=!
4+
COMMAND_PREFIX=!
5+
GITHUB_TOKEN=

data/rfcs/.gitignore

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
rfcs.json

package-lock.json

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

package.json

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -32,6 +32,7 @@
3232
"eslint-plugin-prettier": "^3.1.0",
3333
"eslint-plugin-vue": "^5.2.3",
3434
"esm": "^3.2.25",
35+
"github-api": "^3.3.0",
3536
"fuse.js": "^3.4.6",
3637
"hjson": "^3.1.2",
3738
"prettier": "^1.18.2"

src/client.js

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -61,6 +61,10 @@ client.registry.registerGroups([
6161
id: 'jobs',
6262
name: 'Jobs',
6363
},
64+
{
65+
id: 'rfcs',
66+
name: 'RFCs',
67+
},
6468
])
6569

6670
if (NODE_ENV === 'development') {

src/commands/rfcs/reload.js

Lines changed: 51 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,51 @@
1+
import { Command } from 'discord.js-commando'
2+
import { RichEmbed } from 'discord.js'
3+
import { reloadCache } from '../../services/rfcs'
4+
import { EMPTY_MESSAGE, ROLES } from '../../utils/constants'
5+
import { cleanupErrorResponse, cleanupInvocation } from '../../utils/messages'
6+
7+
const ALLOWED_ROLES = [ROLES.MODERATORS, ROLES.CORE_TEAM, ROLES.BOT_DEVELOPERS]
8+
9+
module.exports = class RFCsCommand extends Command {
10+
constructor(client) {
11+
super(client, {
12+
name: 'reload-rfcs',
13+
group: 'rfcs',
14+
guildOnly: true,
15+
memberName: 'reload',
16+
description: 'Reload the RFCs from the Github API and recache them.',
17+
})
18+
}
19+
20+
hasPermission(msg) {
21+
if (msg.member.roles.some(role => ALLOWED_ROLES.includes(role.id))) {
22+
return true
23+
}
24+
25+
return false
26+
}
27+
28+
async run(msg) {
29+
const embed = new RichEmbed('Reload RFCs')
30+
31+
try {
32+
await reloadCache()
33+
embed
34+
.setDescription(
35+
'✅ Fetched RFC PRs from Github and re-cached them to disc.'
36+
)
37+
.setColor('GREEN')
38+
} catch (error) {
39+
console.error(error)
40+
embed
41+
.setDescription(
42+
'❎ An error occured while fetching and recaching the RFC PRs.'
43+
)
44+
.setColor('RED')
45+
} finally {
46+
const reply = await msg.channel.send(EMPTY_MESSAGE, { embed })
47+
cleanupInvocation(msg)
48+
cleanupErrorResponse(reply)
49+
}
50+
}
51+
}

src/commands/rfcs/rfc.js

Lines changed: 209 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,209 @@
1+
import { Command } from 'discord.js-commando'
2+
import { RichEmbed } from 'discord.js'
3+
import {
4+
findRFCs,
5+
filterRFCsBy,
6+
RFCDoesNotExistError,
7+
} from '../../services/rfcs'
8+
import {
9+
EMPTY_MESSAGE,
10+
DISCORD_EMBED_DESCRIPTION_LIMIT,
11+
} from '../../utils/constants'
12+
import { cleanupErrorResponse, cleanupInvocation } from '../../utils/messages'
13+
import { inlineCode, addEllipsis } from '../../utils/string'
14+
import {
15+
respondWithPaginatedEmbed,
16+
DEFAULT_EMBED_COLOUR,
17+
} from '../../utils/embed'
18+
19+
module.exports = class RFCsCommand extends Command {
20+
constructor(client) {
21+
super(client, {
22+
args: [
23+
{
24+
key: 'query',
25+
type: 'optional-kv-pair',
26+
prompt: 'an RFC number, title, body, author or label to search for?',
27+
validate(val) {
28+
if (Array.isArray(val)) {
29+
return ['id', 'title', 'body', 'author', 'label'].includes(val[0])
30+
}
31+
32+
return true
33+
},
34+
},
35+
],
36+
name: 'rfc',
37+
examples: [
38+
inlineCode('!rfcs'),
39+
inlineCode('!rfc #23'),
40+
inlineCode('!rfc initial placeholder'),
41+
inlineCode('!rfc empty node'),
42+
inlineCode('!rfc yyx'),
43+
inlineCode('!rfc core'),
44+
inlineCode('!rfc id:29'),
45+
inlineCode('!rfc title:initial placeholder'),
46+
inlineCode('!rfc body:empty node'),
47+
inlineCode('!rfc author:yyx'),
48+
inlineCode('!rfc label:core'),
49+
inlineCode('!rfc label:breaking change,router'),
50+
inlineCode('!rfc label:3.x | core'),
51+
],
52+
group: 'rfcs',
53+
guildOnly: false,
54+
memberName: 'rfc',
55+
description: 'Search for and view a Vue RFC.',
56+
argsPromptLimit: 1,
57+
})
58+
}
59+
60+
hasPermission() {
61+
return true
62+
}
63+
64+
async run(msg, args) {
65+
let { query } = args
66+
let success = false
67+
let [filter, value] = query
68+
69+
let embed
70+
71+
try {
72+
let rfcs
73+
74+
if (filter === 'empty') {
75+
rfcs = await findRFCs(value)
76+
} else {
77+
rfcs = await filterRFCsBy(filter, value)
78+
}
79+
80+
if (rfcs.length === 0) {
81+
throw new RFCDoesNotExistError()
82+
} else if (rfcs.length === 1) {
83+
embed = this.buildResponseEmbed(msg, rfcs[0])
84+
} else {
85+
return respondWithPaginatedEmbed(
86+
msg,
87+
this.buildDisambiguationEmbed(msg, rfcs, filter, value),
88+
rfcs.map(rfc => this.buildResponseEmbed(msg, rfc, filter, value))
89+
)
90+
}
91+
success = true // For finally block.
92+
} catch (error) {
93+
if (error instanceof RFCDoesNotExistError) {
94+
embed = this.buildErrorEmbed(
95+
msg,
96+
"Sorry, I couldn't find any matches for your query on the RFC repo.",
97+
query
98+
)
99+
} else {
100+
console.error(error)
101+
embed = this.buildErrorEmbed(
102+
msg,
103+
'Sorry, an unspecified error occured!',
104+
query
105+
)
106+
}
107+
} finally {
108+
const reply = await msg.channel.send(EMPTY_MESSAGE, embed)
109+
cleanupInvocation(msg)
110+
111+
if (!success) {
112+
cleanupErrorResponse(reply)
113+
}
114+
}
115+
}
116+
117+
buildErrorEmbed(msg, error, query = []) {
118+
let [filter, value] = query
119+
let lookup = filter === 'empty' ? value : `${filter}:${value}`
120+
121+
return new RichEmbed()
122+
.setTitle(`RFC Lookup - ${inlineCode(lookup)}`)
123+
.setDescription(error)
124+
.setAuthor(
125+
(msg.member ? msg.member.displayName : msg.author.username) +
126+
' requested:',
127+
msg.author.avatarURL
128+
)
129+
.setColor('RED')
130+
}
131+
132+
buildResponseEmbed(msg, rfc) {
133+
const embed = new RichEmbed()
134+
.setTitle(`RFC #${rfc.number} - ${rfc.title}`)
135+
.setURL(rfc.html_url)
136+
.setThumbnail('attachment://vue.png')
137+
.attachFile({
138+
attachment: 'assets/images/icons/vue.png',
139+
name: 'vue.png',
140+
})
141+
.addField('Author', rfc.user.login, true)
142+
.addField('Status', rfc.state, true)
143+
144+
embed.setDescription(
145+
addEllipsis(
146+
rfc.body.replace(/(?<=```)[ ]*(?=\w+)/g, ''),
147+
DISCORD_EMBED_DESCRIPTION_LIMIT
148+
)
149+
)
150+
151+
let footerSections = []
152+
153+
if (rfc.created_at) {
154+
footerSections.push(
155+
'Created: ' + new Date(rfc.created_at).toLocaleDateString()
156+
)
157+
}
158+
159+
if (rfc.updated_at) {
160+
footerSections.push(
161+
'Updated: ' + new Date(rfc.updated_at).toLocaleDateString()
162+
)
163+
}
164+
165+
if (footerSections.length) {
166+
embed.setFooter(footerSections.join(' | '))
167+
}
168+
169+
if (rfc.labels.length) {
170+
embed.addField(
171+
'Labels',
172+
rfc.labels.map(label => label.name).join(', '),
173+
true
174+
)
175+
}
176+
177+
let labelsWithColours = rfc.labels.filter(label =>
178+
['core', 'vuex', 'router'].includes(label.name)
179+
)
180+
181+
if (labelsWithColours.length) {
182+
embed.setColor(`#${labelsWithColours[0].color}`)
183+
} else {
184+
embed.setColor(DEFAULT_EMBED_COLOUR)
185+
}
186+
187+
return embed
188+
}
189+
190+
buildDisambiguationEmbed(msg, rfcs, filter, value) {
191+
let query = filter === 'empty' ? value : `${filter}:${value}`
192+
193+
return new RichEmbed()
194+
.setTitle(`RFC Request - ${inlineCode(query)}`)
195+
.setDescription(
196+
`Sorry, I couldn't find an exact match for your query on the RFC repo.`
197+
)
198+
.setThumbnail('attachment://vue.png')
199+
.attachFile({
200+
attachment: 'assets/images/icons/vue.png',
201+
name: 'vue.png',
202+
})
203+
.addField(
204+
'Perhaps you meant one of these:',
205+
rfcs.map(rfc => inlineCode('#' + rfc.number)).join(', ')
206+
)
207+
.setColor('BLUE')
208+
}
209+
}

0 commit comments

Comments
 (0)