Skip to content

Commit b90c24a

Browse files
committed
Add slack library and limit new qustions max 48h
Use stripped version of nlopes/slack library since one does not support chat.update with attachments
1 parent b15b3c7 commit b90c24a

12 files changed

+464
-30
lines changed

Makefile

+2-6
Original file line numberDiff line numberDiff line change
@@ -43,7 +43,7 @@ help: ## Show this help menu
4343
# Build
4444
slackoverflow: ## Build new SlackOverflow binary
4545
$(call log_info, build new SlackOverflow binary)
46-
go build -ldflags "${SLACKOVERFLOW_LDFLAGS}" -o ${SLACKOVERFLOW_BINARY}
46+
go build -a -ldflags "${SLACKOVERFLOW_LDFLAGS}" -o ${SLACKOVERFLOW_BINARY}
4747
$(call log_ok, new binary ready)
4848

4949
################################################################################
@@ -86,10 +86,6 @@ dependencies: ## Install SlackOverflow build dependencies
8686
@govendor fetch github.com/jessevdk/go-flags@=v1.1.0
8787
$(call log_ok, github.com/jessevdk/go-flags@=v1.1.0)
8888

89-
$(call log_info, Install Slack API in Go.)
90-
@govendor fetch github.com/nlopes/slack
91-
$(call log_ok, github.com/nlopes/slack)
92-
9389
################################################################################
9490
# Contributors
9591
contributors: ## Update contributors list
@@ -107,4 +103,4 @@ clean: ## Remove existing binary if exists
107103
if [ -f ${SLACKOVERFLOW_BINARY} ]; then rm ${SLACKOVERFLOW_BINARY}; fi
108104
$(call log_ok, SlackOverflow old binary removed)
109105

110-
.PHONY: clean install
106+
.PHONY: clean slackoverflow

README.md

+10
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,16 @@
33
> Web hook that posts tagged Stack Overflow questions to Slack, updated using reaction emojis
44
> Work in progress...
55
6+
## Experimental builds
7+
8+
```
9+
git clone https://github.com/mkungla/slackoverflow.git $GOPATH/src/github.com/aframevr/slackoverflow
10+
cd $GOPATH/src/github.com/aframevr/slackoverflow
11+
make dependencies
12+
make install
13+
```
14+
15+
616
[![GitHub license][license-image]][license-url]
717
[![Build Status][travis-ci-image]][travis-ci-url]
818

slack/attachments.go

+73
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,73 @@
1+
package slack
2+
3+
import "encoding/json"
4+
5+
// AttachmentField contains information for an attachment field
6+
// An Attachment can contain multiple of these
7+
type AttachmentField struct {
8+
Title string `json:"title"`
9+
Value string `json:"value"`
10+
Short bool `json:"short"`
11+
}
12+
13+
// AttachmentAction is a button to be included in the attachment. Required when
14+
// using message buttons and otherwise not useful. A maximum of 5 actions may be
15+
// provided per attachment.
16+
type AttachmentAction struct {
17+
Name string `json:"name"` // Required.
18+
Text string `json:"text"` // Required.
19+
Style string `json:"style,omitempty"` // Optional. Allowed values: "default", "primary", "danger"
20+
Type string `json:"type"` // Required. Must be set to "button"
21+
Value string `json:"value,omitempty"` // Optional.
22+
Confirm *ConfirmationField `json:"confirm,omitempty"` // Optional.
23+
}
24+
25+
// AttachmentActionCallback is sent from Slack when a user clicks a button in an interactive message (aka AttachmentAction)
26+
type AttachmentActionCallback struct {
27+
Actions []AttachmentAction `json:"actions"`
28+
CallbackID string `json:"callback_id"`
29+
Channel Channel `json:"channel"`
30+
ActionTs string `json:"action_ts"`
31+
MessageTs string `json:"message_ts"`
32+
AttachmentID string `json:"attachment_id"`
33+
Token string `json:"token"`
34+
ResponseURL string `json:"response_url"`
35+
}
36+
37+
// ConfirmationField are used to ask users to confirm actions
38+
type ConfirmationField struct {
39+
Title string `json:"title,omitempty"` // Optional.
40+
Text string `json:"text"` // Required.
41+
OkText string `json:"ok_text,omitempty"` // Optional. Defaults to "Okay"
42+
DismissText string `json:"dismiss_text,omitempty"` // Optional. Defaults to "Cancel"
43+
}
44+
45+
// Attachment contains all the information for an attachment
46+
type Attachment struct {
47+
Color string `json:"color,omitempty"`
48+
Fallback string `json:"fallback"`
49+
50+
CallbackID string `json:"callback_id,omitempty"`
51+
52+
AuthorName string `json:"author_name,omitempty"`
53+
AuthorSubname string `json:"author_subname,omitempty"`
54+
AuthorLink string `json:"author_link,omitempty"`
55+
AuthorIcon string `json:"author_icon,omitempty"`
56+
57+
Title string `json:"title,omitempty"`
58+
TitleLink string `json:"title_link,omitempty"`
59+
Pretext string `json:"pretext,omitempty"`
60+
Text string `json:"text"`
61+
62+
ImageURL string `json:"image_url,omitempty"`
63+
ThumbURL string `json:"thumb_url,omitempty"`
64+
65+
Fields []AttachmentField `json:"fields,omitempty"`
66+
Actions []AttachmentAction `json:"actions,omitempty"`
67+
MarkdownIn []string `json:"mrkdwn_in,omitempty"`
68+
69+
Footer string `json:"footer,omitempty"`
70+
FooterIcon string `json:"footer_icon,omitempty"`
71+
72+
Ts json.Number `json:"ts,omitempty"`
73+
}

slack/channels.go

+86
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,86 @@
1+
package slack
2+
3+
import (
4+
"errors"
5+
"net/url"
6+
)
7+
8+
type channelResponseFull struct {
9+
Channel Channel `json:"channel"`
10+
Channels []Channel `json:"channels"`
11+
Purpose string `json:"purpose"`
12+
Topic string `json:"topic"`
13+
NotInChannel bool `json:"not_in_channel"`
14+
APIresponse
15+
}
16+
17+
// Channel contains information about the channel
18+
type Channel struct {
19+
groupConversation
20+
IsChannel bool `json:"is_channel"`
21+
IsGeneral bool `json:"is_general"`
22+
IsMember bool `json:"is_member"`
23+
}
24+
25+
// GetChannels retrieves all the channels
26+
func (client *Client) GetChannels(excludeArchived bool) ([]Channel, error) {
27+
values := url.Values{
28+
"token": {client.APIToken},
29+
}
30+
31+
if excludeArchived {
32+
values.Add("exclude_archived", "1")
33+
}
34+
response, err := channelRequest("channels.list", values)
35+
if err != nil {
36+
return nil, err
37+
}
38+
return response.Channels, nil
39+
}
40+
41+
// Conversation is the foundation for IM and BaseGroupConversation
42+
type conversation struct {
43+
ID string `json:"id"`
44+
Created JSONTime `json:"created"`
45+
IsOpen bool `json:"is_open"`
46+
LastRead string `json:"last_read,omitempty"`
47+
UnreadCount int `json:"unread_count,omitempty"`
48+
UnreadCountDisplay int `json:"unread_count_display,omitempty"`
49+
}
50+
51+
// GroupConversation is the foundation for Group and Channel
52+
type groupConversation struct {
53+
conversation
54+
Name string `json:"name"`
55+
Creator string `json:"creator"`
56+
IsArchived bool `json:"is_archived"`
57+
Members []string `json:"members"`
58+
Topic Topic `json:"topic"`
59+
Purpose Purpose `json:"purpose"`
60+
}
61+
62+
// Topic contains information about the topic
63+
type Topic struct {
64+
Value string `json:"value"`
65+
Creator string `json:"creator"`
66+
LastSet JSONTime `json:"last_set"`
67+
}
68+
69+
// Purpose contains information about the purpose
70+
type Purpose struct {
71+
Value string `json:"value"`
72+
Creator string `json:"creator"`
73+
LastSet JSONTime `json:"last_set"`
74+
}
75+
76+
func channelRequest(path string, values url.Values) (*channelResponseFull, error) {
77+
response := &channelResponseFull{}
78+
err := post(path, values, response)
79+
if err != nil {
80+
return nil, err
81+
}
82+
if !response.Ok {
83+
return nil, errors.New(response.Error)
84+
}
85+
return response, nil
86+
}

slack/chat.go

+183
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,183 @@
1+
package slack
2+
3+
import (
4+
"encoding/json"
5+
"errors"
6+
"net/url"
7+
"strings"
8+
)
9+
10+
const (
11+
DEFAULT_MESSAGE_USERNAME = ""
12+
DEFAULT_MESSAGE_THREAD_TIMESTAMP = ""
13+
DEFAULT_MESSAGE_ASUSER = false
14+
DEFAULT_MESSAGE_PARSE = ""
15+
DEFAULT_MESSAGE_LINK_NAMES = 0
16+
DEFAULT_MESSAGE_UNFURL_LINKS = false
17+
DEFAULT_MESSAGE_UNFURL_MEDIA = true
18+
DEFAULT_MESSAGE_ICON_URL = ""
19+
DEFAULT_MESSAGE_ICON_EMOJI = ""
20+
DEFAULT_MESSAGE_MARKDOWN = true
21+
DEFAULT_MESSAGE_ESCAPE_TEXT = true
22+
)
23+
24+
// PostMessageParameters contains all the parameters necessary (including the optional ones) for a PostMessage() request
25+
type PostMessageParameters struct {
26+
Text string `json:"text"`
27+
Username string `json:"user_name"`
28+
AsUser bool `json:"as_user"`
29+
Parse string `json:"parse"`
30+
ThreadTimestamp string `json:"thread_ts"`
31+
LinkNames int `json:"link_names"`
32+
Attachments []Attachment `json:"attachments"`
33+
UnfurlLinks bool `json:"unfurl_links"`
34+
UnfurlMedia bool `json:"unfurl_media"`
35+
IconURL string `json:"icon_url"`
36+
IconEmoji string `json:"icon_emoji"`
37+
Markdown bool `json:"mrkdwn,omitempty"`
38+
EscapeText bool `json:"escape_text"`
39+
}
40+
41+
func escapeMessage(message string) string {
42+
replacer := strings.NewReplacer("&", "&amp;", "<", "&lt;", ">", "&gt;")
43+
return replacer.Replace(message)
44+
}
45+
46+
type chatResponseFull struct {
47+
Channel string `json:"channel"`
48+
Timestamp string `json:"ts"`
49+
Text string `json:"text"`
50+
APIresponse
51+
}
52+
53+
func chatRequest(path string, values url.Values) (*chatResponseFull, error) {
54+
response := &chatResponseFull{}
55+
err := post(path, values, response)
56+
if err != nil {
57+
return nil, err
58+
}
59+
if !response.Ok {
60+
return nil, errors.New(response.Error)
61+
}
62+
return response, nil
63+
}
64+
65+
// PostMessage sends a message to a channel.
66+
// Message is escaped by default according to https://api.slack.com/docs/formatting
67+
// Use http://davestevens.github.io/slack-message-builder/ to help crafting your message.
68+
func (api *Client) PostMessage(channel, text string, params PostMessageParameters) (string, string, error) {
69+
if params.EscapeText {
70+
text = escapeMessage(text)
71+
}
72+
values := url.Values{
73+
"token": {api.APIToken},
74+
"channel": {channel},
75+
"text": {text},
76+
}
77+
if params.Username != DEFAULT_MESSAGE_USERNAME {
78+
values.Set("username", string(params.Username))
79+
}
80+
if params.AsUser != DEFAULT_MESSAGE_ASUSER {
81+
values.Set("as_user", "true")
82+
}
83+
if params.Parse != DEFAULT_MESSAGE_PARSE {
84+
values.Set("parse", string(params.Parse))
85+
}
86+
if params.LinkNames != DEFAULT_MESSAGE_LINK_NAMES {
87+
values.Set("link_names", "1")
88+
}
89+
if params.Attachments != nil {
90+
attachments, err := json.Marshal(params.Attachments)
91+
if err != nil {
92+
return "", "", err
93+
}
94+
values.Set("attachments", string(attachments))
95+
}
96+
if params.UnfurlLinks != DEFAULT_MESSAGE_UNFURL_LINKS {
97+
values.Set("unfurl_links", "true")
98+
}
99+
// I want to send a message with explicit `as_user` `true` and `unfurl_links` `false` in request.
100+
// Because setting `as_user` to `true` will change the default value for `unfurl_links` to `true` on Slack API side.
101+
if params.AsUser != DEFAULT_MESSAGE_ASUSER && params.UnfurlLinks == DEFAULT_MESSAGE_UNFURL_LINKS {
102+
values.Set("unfurl_links", "false")
103+
}
104+
if params.UnfurlMedia != DEFAULT_MESSAGE_UNFURL_MEDIA {
105+
values.Set("unfurl_media", "false")
106+
}
107+
if params.IconURL != DEFAULT_MESSAGE_ICON_URL {
108+
values.Set("icon_url", params.IconURL)
109+
}
110+
if params.IconEmoji != DEFAULT_MESSAGE_ICON_EMOJI {
111+
values.Set("icon_emoji", params.IconEmoji)
112+
}
113+
if params.Markdown != DEFAULT_MESSAGE_MARKDOWN {
114+
values.Set("mrkdwn", "false")
115+
}
116+
if params.ThreadTimestamp != DEFAULT_MESSAGE_THREAD_TIMESTAMP {
117+
values.Set("thread_ts", params.ThreadTimestamp)
118+
}
119+
120+
response, err := chatRequest("chat.postMessage", values)
121+
if err != nil {
122+
return "", "", err
123+
}
124+
return response.Channel, response.Timestamp, nil
125+
}
126+
127+
// UpdateMessageParameters contains all the parameters necessary (including the optional ones) for a UpdateMessage() request
128+
type UpdateMessageParameters struct {
129+
Timestamp string `json:"ts"`
130+
Text string `json:"text"`
131+
Attachments []Attachment `json:"attachments"`
132+
Parse string `json:"parse"`
133+
LinkNames int `json:"link_names"`
134+
AsUser bool `json:"as_user"`
135+
}
136+
137+
// UpdateMessageWithAttachments updates a message in a channel with attachments
138+
func (api *Client) UpdateMessageWithAttachments(channel string, params UpdateMessageParameters) (string, string, string, error) {
139+
values := url.Values{
140+
"token": {api.APIToken},
141+
"channel": {channel},
142+
"text": {escapeMessage(params.Text)},
143+
"ts": {params.Timestamp},
144+
}
145+
if params.AsUser != DEFAULT_MESSAGE_ASUSER {
146+
values.Set("as_user", "true")
147+
}
148+
if params.Parse != DEFAULT_MESSAGE_PARSE {
149+
values.Set("parse", string(params.Parse))
150+
}
151+
if params.LinkNames != DEFAULT_MESSAGE_LINK_NAMES {
152+
values.Set("link_names", "1")
153+
}
154+
if params.Attachments != nil {
155+
attachments, err := json.Marshal(params.Attachments)
156+
if err != nil {
157+
return "", "", "", err
158+
}
159+
values.Set("attachments", string(attachments))
160+
}
161+
response, err := chatRequest("chat.update", values)
162+
if err != nil {
163+
return "", "", "", err
164+
}
165+
return response.Channel, response.Timestamp, response.Text, nil
166+
}
167+
168+
// NewPostMessageParameters provides an instance of PostMessageParameters with all the sane default values set
169+
func NewPostMessageParameters() PostMessageParameters {
170+
return PostMessageParameters{
171+
Username: DEFAULT_MESSAGE_USERNAME,
172+
AsUser: DEFAULT_MESSAGE_ASUSER,
173+
Parse: DEFAULT_MESSAGE_PARSE,
174+
LinkNames: DEFAULT_MESSAGE_LINK_NAMES,
175+
Attachments: nil,
176+
UnfurlLinks: DEFAULT_MESSAGE_UNFURL_LINKS,
177+
UnfurlMedia: DEFAULT_MESSAGE_UNFURL_MEDIA,
178+
IconURL: DEFAULT_MESSAGE_ICON_URL,
179+
IconEmoji: DEFAULT_MESSAGE_ICON_EMOJI,
180+
Markdown: DEFAULT_MESSAGE_MARKDOWN,
181+
EscapeText: DEFAULT_MESSAGE_ESCAPE_TEXT,
182+
}
183+
}

0 commit comments

Comments
 (0)