Skip to content

Commit aa4c20e

Browse files
committed
Initial commit
0 parents  commit aa4c20e

File tree

4 files changed

+193
-0
lines changed

4 files changed

+193
-0
lines changed

.gitignore

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
/stack2slack

LICENSE

Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,19 @@
1+
Copyright (c) 2017 Kévin Dunglas
2+
3+
Permission is hereby granted, free of charge, to any person obtaining a copy
4+
of this software and associated documentation files (the "Software"), to deal
5+
in the Software without restriction, including without limitation the rights
6+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
7+
copies of the Software, and to permit persons to whom the Software is furnished
8+
to do so, subject to the following conditions:
9+
10+
The above copyright notice and this permission notice shall be included in all
11+
copies or substantial portions of the Software.
12+
13+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
14+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
15+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
16+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
17+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
18+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
19+
THE SOFTWARE.

README.md

Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,25 @@
1+
# Stack to Slack
2+
3+
This [Slack](https://slack.com/) bot monitors [StackExchange](https://stackexchange.com/) tags and automatically
4+
publishes new questions in configured Slack channels.
5+
6+
It has been initially created to post all questions related to the [API Platform](https://api-platform.com) framework
7+
in a dedicated channel of the Slack of [Les-Tilleuls.coop](https://les-tilleuls.coop) (the company behind the framework).
8+
9+
[![Go Report Card](https://goreportcard.com/badge/github.com/dunglas/stack2slack)](https://goreportcard.com/report/github.com/dunglas/stack2slack)
10+
11+
## Installing
12+
13+
This bot is written in [Go](https://golang.org/) (golang), you need a proper install of Go to compile it from sources.
14+
15+
1. [Create new Slack bot](https://my.slack.com/services/new/bot) and grab the generated API token
16+
2. Clone this repository: `git clone https://github.com/dunglas/stack2slack.git`
17+
3. Get the dependencies `go get`
18+
4. Compile the app: `go build`
19+
5. Start the daemon: `DEBUG=1 SLACK_API_TOKEN=<your-API-token> TAG_TO_CHANNEL='{"stackoverflow-tag": "slack-channel"}' ./stack2slack`
20+
6. Finally, you need to invite the bot in channels it will post: `/invite @bot-name`
21+
22+
## Credits
23+
24+
Written by [Kévin Dunglas](https://dunglas.fr).
25+
Sponsored by [Les-Tilleuls.coop](https://les-tilleuls.coop).

stack2slack.go

Lines changed: 148 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,148 @@
1+
package main
2+
3+
import (
4+
"encoding/json"
5+
"fmt"
6+
"log"
7+
"net/http"
8+
"os"
9+
"strings"
10+
"time"
11+
12+
"github.com/nlopes/slack"
13+
)
14+
15+
const waitBetweenChecks = time.Minute * 5
16+
17+
func main() {
18+
slackApiToken := os.Getenv("SLACK_API_TOKEN")
19+
if "" == slackApiToken {
20+
log.Fatalln("The environment variable \"SLACK_API_TOKEN\" is not set.")
21+
}
22+
23+
tagToChannel := os.Getenv("TAG_TO_CHANNEL")
24+
if "" == tagToChannel {
25+
log.Fatalln("The environment variable \"TAG_TO_CHANNEL\" is not set.")
26+
}
27+
28+
var tagToChannelName map[string]string
29+
err := json.Unmarshal([]byte(tagToChannel), &tagToChannelName)
30+
if err != nil {
31+
log.Panicln("Unable to parse JSON data provided in \"TAG_TO_CHANNEL\".")
32+
log.Fatal(err)
33+
}
34+
35+
stackSite := os.Getenv("STACK_SITE")
36+
if stackSite == "" {
37+
stackSite = "stackoverflow"
38+
}
39+
40+
runSlackClient(slackApiToken, stackSite, tagToChannelName, os.Getenv("DEBUG") == "1")
41+
}
42+
43+
func runSlackClient(slackApiToken string, stackSite string, tagToChannelName map[string]string, debug bool) {
44+
api := slack.New(slackApiToken)
45+
logger := log.New(os.Stdout, "slack-bot: ", log.Lshortfile|log.LstdFlags)
46+
47+
slack.SetLogger(logger)
48+
api.SetDebug(debug)
49+
50+
rtm := api.NewRTM()
51+
go rtm.ManageConnection()
52+
53+
for msg := range rtm.IncomingEvents {
54+
switch ev := msg.Data.(type) {
55+
case *slack.ConnectedEvent:
56+
if ev.ConnectionCount != 1 {
57+
log.Fatalln("This bot is already connected")
58+
}
59+
60+
tagToChannelId := make(map[string]string, len(tagToChannelName))
61+
62+
OUTER:
63+
for tagName, channelName := range tagToChannelName {
64+
for _, channel := range ev.Info.Channels {
65+
if channelName == channel.Name {
66+
tagToChannelId[tagName] = channel.ID
67+
68+
continue OUTER
69+
}
70+
}
71+
72+
log.Fatalf("The channel \"%s\" doesn't exist.", channelName)
73+
}
74+
75+
watchStack(rtm, tagToChannelId, stackSite)
76+
}
77+
}
78+
}
79+
80+
func watchStack(rtm *slack.RTM, tagToChannelId map[string]string, stackSite string) {
81+
lastCreationDate := 0
82+
83+
tags := make([]string, len(tagToChannelId))
84+
i := 0
85+
for tag := range tagToChannelId {
86+
tags[i] = tag
87+
i++
88+
}
89+
90+
baseUrl := fmt.Sprintf("https://api.stackexchange.com/2.2/search?order=desc&sort=creation&tagged=%s&site=%s", strings.Join(tags, ";"), stackSite)
91+
for {
92+
var url string
93+
if lastCreationDate > 0 {
94+
url = fmt.Sprintf("%s&min=%d", baseUrl, lastCreationDate)
95+
} else {
96+
url= baseUrl
97+
}
98+
99+
resp, err := http.Get(url)
100+
if err != nil {
101+
log.Print(err)
102+
103+
time.Sleep(waitBetweenChecks)
104+
continue
105+
}
106+
107+
type Owner struct {
108+
DisplayName string `json:"display_name"`
109+
}
110+
111+
type Item struct {
112+
Tags []string `json:tags`
113+
Owner Owner `json:owner`
114+
CreationDate int `json:"creation_date"`
115+
Title string `json:title`
116+
Link string `json:link`
117+
}
118+
119+
type Response struct {
120+
Items []Item `json:items`
121+
}
122+
123+
var stackResponse = new(Response)
124+
err = json.NewDecoder(resp.Body).Decode(&stackResponse)
125+
if err != nil {
126+
log.Print(err)
127+
128+
time.Sleep(waitBetweenChecks)
129+
continue
130+
}
131+
132+
for _, item := range stackResponse.Items {
133+
for _, tag := range item.Tags {
134+
if channelId, ok := tagToChannelId[tag]; ok {
135+
fmt.Printf("%v", channelId)
136+
137+
rtm.SendMessage(rtm.NewOutgoingMessage(fmt.Sprintf("%s (%s) by %s. Tags: %s\n", item.Title, item.Link, item.Owner.DisplayName, strings.Join(item.Tags, ", ")), channelId))
138+
}
139+
}
140+
141+
if item.CreationDate > lastCreationDate {
142+
lastCreationDate = item.CreationDate
143+
}
144+
}
145+
146+
time.Sleep(waitBetweenChecks)
147+
}
148+
}

0 commit comments

Comments
 (0)