Skip to content

Commit 9c2694f

Browse files
authored
Improve bot (#8)
* handle gitlab error requests * update deps and fix linter issues * improve markup * update requests handling * update slack markup * handle network errors * fix empty line * update version
1 parent 56763af commit 9c2694f

17 files changed

+1143
-439
lines changed

.eslintrc.json

+7-3
Original file line numberDiff line numberDiff line change
@@ -1,12 +1,16 @@
11
{
22
"env": {
33
"node": true,
4-
"es6": true
4+
"es2021": true
55
},
6-
"parser": "babel-eslint",
6+
"parser": "@babel/eslint-parser",
7+
"extends": [
8+
"umbrellio"
9+
],
710
"plugins": ["promise"],
811
"parserOptions": {
9-
"ecmaVersion": 2018
12+
"ecmaVersion": 2021,
13+
"requireConfigFile": false
1014
},
1115
"rules": {
1216
"no-unused-vars": ["error", { "varsIgnorePattern": "^_", "argsIgnorePattern": "^_" }],

README.md

+4
Original file line numberDiff line numberDiff line change
@@ -40,6 +40,10 @@ Example of the config file:
4040
messenger:
4141
webhook: "<WEBHOOK URL>" # Mattermost / Slack webhook
4242
channel: "<CHANNEL>" # Mattermost / Slack channel where will be messages sent
43+
markup: "slack" # Messenger markup (default - "markdown").
44+
# Possible values:
45+
# - "markdown" (for Mattermost)
46+
# - "slack" (for Slack)
4347
sender:
4448
username: "@ubmrellio/gbot" # Sender's display name
4549
icon: "<icon url>" # Sender's icon url

api/GitLab.js

+2-2
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@ const url = require("../utils/url")
22
const network = require("../utils/network")
33

44
class GitLab {
5-
constructor({ gitlab }) {
5+
constructor ({ gitlab }) {
66
this.baseUrl = gitlab.url
77
this.token = gitlab.token
88
this.projects = gitlab.projects
@@ -47,7 +47,7 @@ class GitLab {
4747
__getPaginated = (uri, query = {}) => {
4848
return this.__get(uri, query).then(async results => {
4949
const { headers } = results
50-
const totalPages = parseInt(headers["x-total-pages"]) || 1
50+
const totalPages = parseInt(headers["x-total-pages"], 10) || 1
5151

5252
let page = 1
5353
let allResults = results

api/Messenger.js

+4-4
Original file line numberDiff line numberDiff line change
@@ -2,19 +2,19 @@ const _ = require("lodash")
22
const network = require("../utils/network")
33

44
class Messenger {
5-
constructor({ messenger }) {
5+
constructor ({ messenger }) {
66
this.channel = _.get(messenger, "channel")
77
this.webhook = _.get(messenger, "webhook")
88
this.username = _.get(messenger, "sender.username", "Gbot")
99
this.icon = _.get(messenger, "sender.icon", null)
1010
}
1111

12-
send = text => {
12+
send = content => {
1313
const message = {
14-
text,
14+
...content,
1515
channel: this.channel,
1616
username: this.username,
17-
icon_url: this.icon
17+
icon_url: this.icon,
1818
}
1919

2020
return network.post(this.webhook, message)

gbot

+6-4
Original file line numberDiff line numberDiff line change
@@ -7,13 +7,15 @@ const configUtils = require("./utils/config")
77

88
const Unapproved = require("./tasks/Unapproved")
99

10-
const runCommand = klass => argv => {
10+
const runCommand = Klass => argv => {
1111
const config = configUtils.load(argv.config)
12-
return new klass(config).perform()
12+
return new Klass(config).perform()
1313
}
1414

15-
yargs
16-
.command("unapproved", "sends unapproved requests to Mattermost / Slack", {}, runCommand(Unapproved))
15+
yargs // eslint-disable-line no-unused-expressions
16+
.command(
17+
"unapproved", "sends unapproved requests to Mattermost / Slack", {}, runCommand(Unapproved),
18+
)
1719
.demandCommand(1, "must provide a valid command")
1820
.options({
1921
config: {

gbot.example.yml

+1
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
messenger:
22
webhook: "<WEBHOOK URL>"
33
channel: "<CHANNEL>"
4+
markup: slack
45
sender:
56
username: "@ubmrellio/gbot"
67
icon: "<icon url>"

package.json

+14-8
Original file line numberDiff line numberDiff line change
@@ -1,23 +1,29 @@
11
{
22
"name": "@umbrellio/gbot",
3-
"version": "1.4.0",
3+
"version": "1.5.0",
44
"bin": "gbot",
55
"repository": "git@github.com:umbrellio/gbot.git",
66
"author": "Aleksei Bespalov <nulldefiner@gmail.com>",
77
"homepage": "https://github.com/umbrellio/gbot",
88
"license": "MIT",
99
"scripts": {
10-
"lint": "eslint ."
10+
"lint": "eslint .",
11+
"lint:fix": "eslint . --fix"
1112
},
1213
"dependencies": {
13-
"js-yaml": "^3.14.0",
14-
"lodash": "^4.17.20",
14+
"js-yaml": "^4.1.0",
15+
"lodash": "^4.17.21",
1516
"winston": "^3.3.3",
16-
"yargs": "^15.4.1"
17+
"yargs": "^17.2.1"
1718
},
1819
"devDependencies": {
19-
"babel-eslint": "^10.1.0",
20-
"eslint": "^7.7.0",
21-
"eslint-plugin-promise": "^4.2.1"
20+
"@babel/core": "^7.14.6",
21+
"@babel/eslint-parser": "^7.16.0",
22+
"eslint": "^8.1.0",
23+
"eslint-config-umbrellio": "^5.0.1",
24+
"eslint-plugin-import": "^2.25.2",
25+
"eslint-plugin-node": "^11.1.0",
26+
"eslint-plugin-prefer-object-spread": "^1.2.1",
27+
"eslint-plugin-promise": "^5.1.1"
2228
}
2329
}

tasks/BaseCommand.js

+1-1
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,7 @@ const GitLab = require("../api/GitLab")
66
const Messenger = require("../api/Messenger")
77

88
class BaseCommand {
9-
constructor(config) {
9+
constructor (config) {
1010
this.config = config
1111
this.logger = logger
1212
this.gitlab = new GitLab(config)

tasks/Unapproved.js

+30-8
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,10 @@ const _ = require("lodash")
33
const BaseCommand = require("./BaseCommand")
44
const UnapprovedRequestDescription = require("./unapproved/UnapprovedRequestDescription")
55

6+
const logger = require("../utils/logger")
7+
const markupUtils = require("../utils/markup")
8+
const { NetworkError } = require("../utils/errors")
9+
610
class Unapproved extends BaseCommand {
711
perform = () => {
812
const promises = this.projects.map(this.__getUnapprovedRequests)
@@ -13,23 +17,41 @@ class Unapproved extends BaseCommand {
1317
.then(this.__buildMessage)
1418
.then(message => {
1519
this.logger.info("Sending message")
16-
this.logger.info(message)
20+
this.logger.info(JSON.stringify(message))
1721
return message
1822
})
1923
.then(this.messenger.send)
24+
.catch(err => {
25+
if (err instanceof NetworkError) {
26+
logger.error(err)
27+
} else {
28+
console.error(err) // eslint-disable-line no-console
29+
}
30+
31+
process.exit(1)
32+
})
2033
}
2134

2235
__buildMessage = requests => {
36+
const markup = markupUtils[this.config.messenger.markup]
37+
2338
if (requests.length) {
24-
const list = requests.map(this.__buildRequestDescription).join("\n")
25-
const head = "#### Hey, there are a couple of requests waiting for your review"
39+
const list = requests.map(this.__buildRequestDescription).map(markup.addDivider)
40+
const headText = "Hey, there are a couple of requests waiting for your review"
41+
42+
const header = markup.makeHeader(headText)
43+
const bodyParts = markup.flatten(list)
2644

27-
return `${head}\n\n${list}`
45+
const message = markup.composeMsg(header, bodyParts)
46+
return message
2847
} else {
29-
return [
30-
"#### Hey, there is a couple of nothing",
31-
"There are no pending requests! Let's do a new one!"
32-
].join("\n\n")
48+
const headText = "Hey, there is a couple of nothing"
49+
const bodyText = "There are no pending requests! Let's do a new one!"
50+
51+
const header = markup.makeHeader(headText)
52+
const body = markup.makePrimaryInfo(markup.makeText(bodyText))
53+
const message = markup.composeMsg(header, body)
54+
return message
3355
}
3456
}
3557

tasks/unapproved/UnapprovedRequestDescription.js

+35-14
Original file line numberDiff line numberDiff line change
@@ -2,36 +2,57 @@ const _ = require("lodash")
22
const gitUtils = require("../../utils/git")
33
const timeUtils = require("../../utils/time")
44
const stringUtils = require("../../utils/strings")
5+
const markupUtils = require("../../utils/markup")
56

67
class UnapprovedRequestDescription {
7-
constructor(request, config) {
8+
constructor (request, config) {
89
this.config = config
910
this.request = request
1011
}
1112

1213
build = () => {
14+
const markup = markupUtils[this.config.messenger.markup]
15+
1316
const updated = new Date(this.request.updated_at)
14-
const reaction = this.__getEmoji(updated)
1517

16-
const link = `[${this.request.title}](${this.request.web_url})`
18+
const reaction = this.__getEmoji(updated)
19+
const link = markup.makeLink(this.request.title, this.request.web_url)
1720
const author = this.__authorString()
18-
const project = `[${this.request.project.name}](${this.request.project.web_url})`
21+
const projectLink = markup.makeLink(this.request.project.name, this.request.project.web_url)
1922
const unresolvedAuthors = this.__unresolvedAuthorsString()
2023
const approvedBy = this.__approvedByString()
2124
const optionalDiff = this.__optionalDiffString()
2225

23-
const parts = [reaction, `**${link}**`, optionalDiff, `(${project})`, `by **${author}**`]
24-
25-
let message = [_.compact(parts).join(" ")]
26+
const requestMessageParts = [
27+
reaction,
28+
markup.makeBold(link),
29+
optionalDiff,
30+
`(${projectLink})`,
31+
`by ${markup.makeBold(author)}`,
32+
]
33+
const requestMessageText = _.compact(requestMessageParts).join(" ")
34+
const primaryMessage = markup.makePrimaryInfo(
35+
markup.makeText(requestMessageText, { withMentions: false }),
36+
)
37+
const secondaryMessageParts = []
2638

2739
if (unresolvedAuthors.length > 0) {
28-
message.push(`unresolved threads by: ${unresolvedAuthors}`)
40+
const text = `unresolved threads by: ${unresolvedAuthors}`
41+
const msg = markup.makeText(text, { withMentions: true })
42+
43+
secondaryMessageParts.push(msg)
2944
}
3045
if (approvedBy.length > 0) {
31-
message.push(`already approved by: ${approvedBy}`)
46+
const text = `already approved by: ${approvedBy}`
47+
const msg = markup.makeText(text, { withMentions: false })
48+
49+
secondaryMessageParts.push(msg)
3250
}
3351

34-
return message.join("\n")
52+
const secondaryMessage = markup.makeAdditionalInfo(secondaryMessageParts)
53+
const message = markup.composeBody(primaryMessage, secondaryMessage)
54+
55+
return message
3556
}
3657

3758
__getConfigSetting = (settingName, defaultValue = null) => {
@@ -111,18 +132,18 @@ class UnapprovedRequestDescription {
111132
_.partialRight(
112133
_.filter,
113134
discussion => discussion.notes.some(
114-
note => note.resolvable && !note.resolved
115-
)
135+
note => note.resolvable && !note.resolved,
136+
),
116137
),
117138
_.partialRight(_.map, selectNotes),
118139
_.partialRight(
119140
_.map,
120-
notes => notes.map(note => note.author)
141+
notes => notes.map(note => note.author),
121142
),
122143
_.partialRight(_.flatten),
123144
_.partialRight(
124145
_.uniqBy,
125-
author => author.username
146+
author => author.username,
126147
),
127148
)
128149

utils/config.js

+8-2
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,12 @@ const yaml = require("js-yaml")
55

66
const PREFIX_REGEX = /^GBOT_/
77

8+
const defaultConfig = {
9+
messenger: {
10+
markup: "markdown",
11+
},
12+
}
13+
814
const parseEnvValue = value => {
915
try {
1016
return JSON.parse(value)
@@ -20,7 +26,7 @@ const parseEnvKey = key => {
2026
const getFileConfig = filePath => {
2127
const configPath = path.resolve(filePath)
2228
const content = fs.readFileSync(configPath)
23-
return yaml.safeLoad(content)
29+
return yaml.load(content)
2430
}
2531

2632
const getEnvConfig = () => Object
@@ -36,7 +42,7 @@ const load = filePath => {
3642
const fileConfig = getFileConfig(filePath)
3743
const envConfig = getEnvConfig()
3844

39-
return _.merge(fileConfig, envConfig)
45+
return _.merge(defaultConfig, fileConfig, envConfig)
4046
}
4147

4248
module.exports = { load }

utils/errors.js

+11
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,11 @@
1+
class NetworkError extends Error {
2+
constructor (message, status) {
3+
super(message)
4+
this.name = "NetworkError"
5+
this.status = status
6+
}
7+
}
8+
9+
module.exports = {
10+
NetworkError,
11+
}

utils/git.js

+1-1
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,7 @@ const DIFF_DELETION_REGEXP = /^-/
44
const diffToNumericMap = diff => {
55
const [, ...lines] = diff.split("\n")
66

7-
const numericMatch = (str, re) => str.match(re) ? 1 : 0
7+
const numericMatch = (str, re) => (str.match(re) ? 1 : 0)
88

99
return lines.map(line => {
1010
const isInsertion = numericMatch(line, DIFF_INSERTION_REGEXP)

utils/logger.js

+1-1
Original file line numberDiff line numberDiff line change
@@ -13,4 +13,4 @@ module.exports = winston.createLogger({
1313
format: winston.format.combine(winston.format.timestamp(), format),
1414
exceptionHandlers: transports,
1515
transports,
16-
});
16+
})

0 commit comments

Comments
 (0)