Skip to content

Commit 823c2d2

Browse files
alexander-bazalexander.baz
and
alexander.baz
authored
Feature: Allow to split message by merge requests count (#14)
* dev * dev * dev * dev * dev * dev * dev * dev * dev * dev * dev * dev * dev * dev * dev * dev * revu * revu --------- Co-authored-by: alexander.baz <alexander.baz@okwork.io>
1 parent be8b4e9 commit 823c2d2

9 files changed

+1275
-849
lines changed

README.md

+7-1
Original file line numberDiff line numberDiff line change
@@ -38,7 +38,8 @@ Example of the config file:
3838

3939
```yml
4040
messenger:
41-
webhook: "<WEBHOOK URL>" # Mattermost / Slack webhook
41+
url: "<chat.postMessage URL>" # Slack chat.postMessage endpoint
42+
token: "<TOKEN>" # Slack token with chat:write scope
4243
channel: "<CHANNEL>" # Mattermost / Slack channel where will be messages sent
4344
markup: "slack" # Messenger markup (default - "markdown").
4445
# Possible values:
@@ -47,6 +48,9 @@ messenger:
4748
sender:
4849
username: "@umbrellio/gbot" # Sender's display name
4950
icon: "<icon url>" # Sender's icon url
51+
slack:
52+
usernameMapping:
53+
pavel: "U020DSB741G" # Mapping of Gitlab username to Slack ID
5054
gitlab:
5155
token: "<TOKEN>" # GitLab Private Access Token
5256
url: "<gitlab api url>" # Gitlab API base url
@@ -72,8 +76,10 @@ unapproved: # Config for `unapproved` command
7276
approvers: false # Tag approvers or not (default - false)
7377
author: false # Tag author of PR or not (default - false)
7478
commenters: false # Tag thread commenters or not (default - false)
79+
onThreadsOpen: false # Whether to tag thread authors and PR author when threads are present
7580
diffs: false # Show changed lines count or not (default - false)
7681
splitByReviewProgress: false # Whether to split the requests into those completely without review and those that under review
82+
requestsPerMessage: 15 # Merge requests count per message
7783
```
7884
7985
Groups in the config are [Gitlab project groups](https://docs.gitlab.com/ee/user/group/). You must specify the group or the project, or both.

api/Messenger.js

+13-5
Original file line numberDiff line numberDiff line change
@@ -3,21 +3,29 @@ const network = require("../utils/network")
33

44
class Messenger {
55
constructor ({ messenger }) {
6+
this.url = _.get(messenger, "url")
7+
this.token = _.get(messenger, "token")
8+
this.headers = { Authorization: `Bearer ${this.token}` }
69
this.channel = _.get(messenger, "channel")
7-
this.webhook = _.get(messenger, "webhook")
810
this.username = _.get(messenger, "sender.username", "Gbot")
911
this.icon = _.get(messenger, "sender.icon", null)
1012
}
1113

12-
send = content => {
13-
const message = {
14-
...content,
14+
send = message => {
15+
const content = {
16+
...message,
1517
channel: this.channel,
1618
username: this.username,
1719
icon_url: this.icon,
1820
}
1921

20-
return network.post(this.webhook, message)
22+
return network.post(this.url, content, this.headers)
23+
}
24+
25+
sendMany = messages => {
26+
_.castArray(messages).forEach((message, idx) => {
27+
_.delay(() => this.send(message), 100 * idx)
28+
})
2129
}
2230
}
2331

gbot.example.yml

+7-1
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,14 @@
11
messenger:
2-
webhook: "<WEBHOOK URL>"
2+
url: https://slack.com/api/chat.postMessage
3+
token: "<TOKEN>"
34
channel: "<CHANNEL>"
45
markup: slack
56
sender:
67
username: "@ubmrellio/gbot"
78
icon: "<icon url>"
9+
slack:
10+
usernameMapping:
11+
pavel: U020DSB741G
812
gitlab:
913
token: "<TOKEN>"
1014
url: "<gitlab api url>"
@@ -29,5 +33,7 @@ unapproved:
2933
approvers: false
3034
author: false
3135
commenters: false
36+
onThreadsOpen: false
3237
diffs: false
3338
splitByReviewProgress: false
39+
requestsPerMessage: 15

package.json

+1-1
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
{
22
"name": "@umbrellio/gbot",
3-
"version": "1.8.0",
3+
"version": "2.0.0",
44
"bin": "gbot",
55
"repository": "git@github.com:umbrellio/gbot.git",
66
"author": "Aleksei Bespalov <nulldefiner@gmail.com>",

tasks/Unapproved.js

+52-27
Original file line numberDiff line numberDiff line change
@@ -13,13 +13,9 @@ class Unapproved extends BaseCommand {
1313
return this.projects
1414
.then(projects => Promise.all(projects.map(this.__getUnapprovedRequests)))
1515
.then(this.__sortRequests)
16-
.then(this.__buildMessage)
17-
.then(message => {
18-
this.logger.info("Sending message")
19-
this.logger.info(JSON.stringify(message))
20-
return message
21-
})
22-
.then(this.messenger.send)
16+
.then(this.__buildMessages)
17+
.then(this.__logMessages)
18+
.then(this.messenger.sendMany)
2319
.catch(err => {
2420
if (err instanceof NetworkError) {
2521
logger.error(err)
@@ -31,35 +27,49 @@ class Unapproved extends BaseCommand {
3127
})
3228
}
3329

34-
__buildMessage = requests => {
30+
__buildMessages = requests => {
3531
const markup = markupUtils[this.__getConfigSetting("messenger.markup")]
3632

3733
if (requests.length) {
38-
return this.__buildListMessage(requests, markup)
34+
return this.__buildListMessages(requests, markup)
3935
} else {
40-
return this.__buildEmptyListMessage(requests, markup)
36+
return this.__buildEmptyListMessage(markup)
4137
}
4238
}
4339

44-
__buildListMessage = (requests, markup) => {
45-
const headText = "Hey, there are a couple of requests waiting for your review"
46-
const list = this.__buildRequestsMessage(requests, markup)
40+
__logMessages = messages => {
41+
this.logger.info("Sending messages")
42+
this.logger.info(JSON.stringify(messages))
43+
return messages
44+
}
4745

46+
__buildListMessages = (requests, markup) => {
47+
const headText = "Hey, there are a couple of requests waiting for your review"
48+
const messages = this.__buildRequestsMessages(requests, markup)
4849
const header = markup.makeHeader(headText)
49-
const bodyParts = markup.flatten(list)
5050

51-
return markup.composeMsg(header, bodyParts)
51+
return messages.map((message, idx) => {
52+
const parts = markup.flatten(message)
53+
54+
if (idx === 0) {
55+
return markup.composeMsg(
56+
markup.withHeader(header, parts),
57+
)
58+
}
59+
60+
return markup.composeMsg(parts)
61+
})
5262
}
5363

54-
__buildRequestsMessage = (requests, markup) => {
64+
__buildRequestsMessages = (requests, markup) => {
5565
const splitByReviewProgress =
5666
this.__getConfigSetting("unapproved.splitByReviewProgress")
5767

5868
if (splitByReviewProgress) {
59-
return this.__buildByReviewProgressMessage(requests, markup)
69+
return this.__buildByReviewProgressMessages(requests, markup)
6070
}
6171

62-
return this.__buildGeneralRequestsMessage(requests, markup)
72+
return this.__buildGeneralRequestsMessages(requests, markup)
6373
}
6474

6575
__buildEmptyListMessage = markup => {
@@ -69,13 +79,10 @@ class Unapproved extends BaseCommand {
6979
const header = markup.makeHeader(headText)
7080
const body = markup.makePrimaryInfo(markup.makeText(bodyText))
7181

72-
return markup.composeMsg(header, body)
82+
return markup.composeMsg(markup.withHeader(header, body))
7383
}
7484

75-
__buildGeneralRequestsMessage = (requests, markup) => requests
76-
.map(this.__buildRequestDescription).map(markup.addDivider)
77-
78-
__buildByReviewProgressMessage = (requests, markup) => {
85+
__buildByReviewProgressMessages = (requests, markup) => {
7986
const messages = []
8087
const [toReviewRequests, underReviewRequests] = _.partition(requests, req => (
8188
req.approvals_left > 0 && !this.__isRequestUnderReview(req)
@@ -90,15 +97,33 @@ class Unapproved extends BaseCommand {
9097
const toReviewSection = makeSection("Unapproved")
9198
const underReviewSection = makeSection("Under review")
9299

93-
const toReviewMessage = this.__buildGeneralRequestsMessage(toReviewRequests, markup)
94-
const underReviewMessage = this.__buildGeneralRequestsMessage(underReviewRequests, markup)
100+
const toReviewMessages = this.__buildGeneralRequestsMessages(toReviewRequests, markup)
101+
const underReviewMessages = this.__buildGeneralRequestsMessages(underReviewRequests, markup)
102+
103+
toReviewMessages.forEach((chunk, idx) => {
104+
messages.push(idx === 0 ? [toReviewSection, ...chunk] : chunk)
105+
})
95106

96-
toReviewMessage.length && messages.push(toReviewSection, ...toReviewMessage)
97-
underReviewMessage.length && messages.push(underReviewSection, ...underReviewMessage)
107+
underReviewMessages.forEach((chunk, idx) => {
108+
messages.push(idx === 0 ? [underReviewSection, ...chunk] : chunk)
109+
})
98110

99111
return messages
100112
}
101113

114+
__buildGeneralRequestsMessages = (requests, markup) => (
115+
this.__chunkRequests(requests).map(chunk => (
116+
chunk.map(this.__buildRequestDescription).map(markup.addDivider)
117+
))
118+
)
119+
120+
__chunkRequests = requests => {
121+
const requestsPerMessage = this.__getConfigSetting("unapproved.requestsPerMessage")
122+
if (!requestsPerMessage) return [requests]
123+
124+
return _.chunk(requests, requestsPerMessage)
125+
}
126+
102127
__buildRequestDescription = request =>
103128
new UnapprovedRequestDescription(request, this.config).build()
104129

tasks/unapproved/UnapprovedRequestDescription.js

+32-35
Original file line numberDiff line numberDiff line change
@@ -12,23 +12,28 @@ class UnapprovedRequestDescription {
1212

1313
build = () => {
1414
const markup = markupUtils[this.config.messenger.markup]
15+
const tagAuthor = this.__getConfigSetting("unapproved.tag.author", false)
16+
const tagOnThreadsOpen = this.__getConfigSetting("unapproved.tag.onThreadsOpen", false)
1517

16-
const updated = new Date(this.request.updated_at)
18+
const { author, updated } = this.request
1719

18-
const reaction = this.__getEmoji(updated)
20+
const reaction = this.__getEmoji(new Date(updated))
1921
const link = markup.makeLink(this.request.title, this.request.web_url)
20-
const author = this.__authorString()
2122
const projectLink = markup.makeLink(this.request.project.name, this.request.project.web_url)
22-
const unresolvedAuthors = this.__unresolvedAuthorsString()
23-
const approvedBy = this.__approvedByString()
23+
const unresolvedAuthors = this.__unresolvedAuthorsString(markup)
24+
const tagAuthorOnThread = tagOnThreadsOpen && unresolvedAuthors.length > 0
25+
const authorString = this.__authorString(
26+
markup, author.username, { tag: tagAuthor || tagAuthorOnThread },
27+
)
28+
const approvedBy = this.__approvedByString(markup)
2429
const optionalDiff = this.__optionalDiffString()
2530

2631
const requestMessageParts = [
2732
reaction,
2833
markup.makeBold(link),
2934
`(${projectLink})`,
3035
optionalDiff,
31-
`by ${markup.makeBold(author)}`,
36+
`by ${authorString}`,
3237
]
3338
const requestMessageText = _.compact(requestMessageParts).join(" ")
3439
const primaryMessage = markup.makePrimaryInfo(
@@ -38,10 +43,11 @@ class UnapprovedRequestDescription {
3843

3944
if (unresolvedAuthors.length > 0) {
4045
const text = `unresolved threads by: ${unresolvedAuthors}`
41-
const msg = markup.makeText(text, { withMentions: true })
46+
const msg = markup.makeText(text, { withMentions: tagOnThreadsOpen })
4247

4348
secondaryMessageParts.push(msg)
4449
}
50+
4551
if (approvedBy.length > 0) {
4652
const text = `already approved by: ${approvedBy}`
4753
const msg = markup.makeText(text, { withMentions: false })
@@ -50,9 +56,7 @@ class UnapprovedRequestDescription {
5056
}
5157

5258
const secondaryMessage = markup.makeAdditionalInfo(secondaryMessageParts)
53-
const message = markup.composeBody(primaryMessage, secondaryMessage)
54-
55-
return message
59+
return markup.composeBody(primaryMessage, secondaryMessage)
5660
}
5761

5862
__getConfigSetting = (settingName, defaultValue = null) => {
@@ -74,43 +78,38 @@ class UnapprovedRequestDescription {
7478
return findEmoji(emoji) || emoji.default || ""
7579
}
7680

77-
__unresolvedAuthorsString = () => {
78-
return this.__unresolvedAuthorsFor(this.request).map(author => {
79-
return `@${author.username}`
80-
}).join(", ")
81+
__unresolvedAuthorsString = (markup) => {
82+
return this.__unresolvedAuthorsFor(this.request).map(author => (
83+
this.__authorString(markup, author.username, { tag: true })
84+
)).join(", ")
8185
}
8286

83-
__approvedByString = () => {
84-
const tagApprovers = this.__getConfigSetting("unapproved.tag.approvers", false)
87+
__approvedByString = markup => {
88+
const tag = this.__getConfigSetting("unapproved.tag.approvers", false)
8589

86-
return this.request.approved_by.map(approve => {
87-
const { user } = approve
88-
let message = `@${user.username}`
90+
return this.request.approved_by.map(approve => (
91+
this.__authorString(markup, approve.user.username, { tag })
92+
)).join(", ")
93+
}
8994

90-
if (!tagApprovers) {
91-
message = stringUtils.wrapString(message)
92-
}
95+
__authorString = (markup, username, { tag = false } = {}) => {
96+
if (tag) {
97+
return this.__getMentionString(markup, username)
98+
}
9399

94-
return message
95-
}).join(", ")
100+
return stringUtils.wrapString(`@${username}`)
96101
}
97102

98-
__authorString = () => {
99-
const tagAuthor = this.__getConfigSetting("unapproved.tag.author", false)
100-
let message = `@${this.request.author.username}`
101-
102-
if (!tagAuthor) {
103-
message = stringUtils.wrapString(message)
104-
}
105-
return message
103+
__getMentionString = (markup, username) => {
104+
const mapping = this.__getConfigSetting(`messenger.${markup.type}.usernameMapping`, {})
105+
return markup.mention(username, mapping)
106106
}
107107

108108
__optionalDiffString = () => {
109109
const showDiff = this.__getConfigSetting("unapproved.diffs", false)
110110

111111
if (showDiff) {
112112
const [ insertions, deletions ] = this.__getTotalDiff()
113-
114113
return stringUtils.wrapString(`+${insertions} -${deletions}`)
115114
}
116115

@@ -119,12 +118,10 @@ class UnapprovedRequestDescription {
119118

120119
__unresolvedAuthorsFor = () => {
121120
const tagCommenters = this.__getConfigSetting("unapproved.tag.commenters", false)
122-
123121
const { discussions } = this.request
124122

125123
const selectNotes = discussion => {
126124
const [issueNote, ...comments] = discussion.notes
127-
128125
return tagCommenters ? [issueNote, ...comments] : [issueNote]
129126
}
130127

0 commit comments

Comments
 (0)