Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions .gitignore
Original file line number Diff line number Diff line change
@@ -1 +1,2 @@
.idea/
/cluster/slack-moderator-words/secrets.yaml
4 changes: 4 additions & 0 deletions cluster/ingress.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,10 @@ spec:
backend:
serviceName: slack-moderator
servicePort: 80
- path: /infra/moderator-words/*
backend:
serviceName: slack-moderator-words
servicePort: 80
- path: /infra/welcomer/*
backend:
serviceName: slack-welcomer
Expand Down
51 changes: 51 additions & 0 deletions cluster/slack-moderator-words/deployment.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,51 @@
apiVersion: apps/v1
kind: Deployment
metadata:
name: slack-moderator-words
labels:
app: slack-moderator-words
spec:
replicas: 2
selector:
matchLabels:
app: slack-moderator-words
template:
metadata:
labels:
app: slack-moderator-words
spec:
containers:
- name: slack-moderator-words
image: gcr.io/kubernetes-tools/slack-moderator-words:latest
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

version this or add imagePullPolicy: always?

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I forgot always is implicitly/magically the default for containers with image :latest, this is probably my least favorite thing in the API :|

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think after we merge we will need to update this right? because it might trigger a job to build the images or not?

imagePullPolicy: Always
args:
- --config-path=/etc/slack-moderator-words/config.json
- --filter-config-path=/etc/slack-moderator-words/filters.yaml
ports:
- containerPort: 8080
protocol: TCP
env:
- name: PATH_PREFIX
value: /infra/moderator-words
volumeMounts:
- mountPath: /etc/slack-moderator-words/
name: configs
readOnly: true
readinessProbe:
httpGet:
path: /healthz
scheme: HTTP
port: 8077
livenessProbe:
httpGet:
path: /healthz
scheme: HTTP
port: 8077
volumes:
- name: configs
projected:
sources:
- secret:
name: slack-moderator-words-config
- secret:
name: slack-moderator-words-filters
12 changes: 12 additions & 0 deletions cluster/slack-moderator-words/service.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
kind: Service
apiVersion: v1
metadata:
name: slack-moderator-words
spec:
selector:
app: slack-moderator-words
type: NodePort
ports:
- protocol: TCP
port: 80
targetPort: 8077
3 changes: 2 additions & 1 deletion go.mod
Original file line number Diff line number Diff line change
@@ -1,10 +1,11 @@
module sigs.k8s.io/slack-infra

go 1.12
go 1.15

require (
github.com/bmatcuk/doublestar v1.1.1
go4.org v0.0.0-20200411211856-f5505b9728dd
gopkg.in/yaml.v2 v2.2.2 // indirect
gopkg.in/yaml.v3 v3.0.0-20210107192922-496545a6307b
sigs.k8s.io/yaml v1.1.0
)
219 changes: 219 additions & 0 deletions go.sum

Large diffs are not rendered by default.

25 changes: 25 additions & 0 deletions slack-moderate-words/.gcloudignore
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
# This file specifies files that are *not* uploaded to Google Cloud Platform
# using gcloud. It follows the same syntax as .gitignore, with the addition of
# "#!include" directives (which insert the entries of the given .gitignore-style
# file at that point).
#
# For more information, run:
# $ gcloud topic gcloudignore
#
.gcloudignore
# If you would like to upload your .git directory, .gitignore file or files
# from your .gitignore file, remove the corresponding line
# below:
.git
.gitignore

# Binaries for programs and plugins
*.exe
*.exe~
*.dll
*.so
*.dylib
# Test binary, build with `go test -c`
*.test
# Output of the go coverage tool, specifically when used with LiteIDE
*.out
4 changes: 4 additions & 0 deletions slack-moderate-words/.gitignore
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
config.json

# go binary
slack-moderate-words
3 changes: 3 additions & 0 deletions slack-moderate-words/Dockerfile
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
FROM gcr.io/distroless/base
ADD slack-moderate-words /slack-moderate-words
ENTRYPOINT ["/slack-moderate-words"]
55 changes: 55 additions & 0 deletions slack-moderate-words/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,55 @@
# slack-moderator-words

slack-moderator-words provides a moderation when positng some specific words, and will let the user know how to write better messages.

## Configuration

slack-moderator-words requires a configuration file, by default called `config.json` in the working
directory. It must look like this:

```json
{
"signingSecret": "some_slack_signing_secret",
"accessToken": "xoxp-some-slack-access-token-these-are-very-long-and-start-with-xoxp",
}
```

`signingSecret`, `accessToken` are all values provided by Slack when creating and
installing the app. Check out the [slack app creation guide][app-creation] for more details.

Also, requires a filter file, by default called `filters.yaml` in the working
directory. It must look like this:

```yaml
- triggers:
- guys
action: chat.postEphemeral
message: "May I suggest \"all\" instead when addessing a group of people? Thank you. :slightly_smiling_face:"
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

'all' can sound very strange in some sentences depending on how it is used. Its not a 1:1 replacement as sometimes you need to address a subset of people that isn't everyone which all implies. Another recommendation, though an older word, might be 'folks'? (https://www.merriam-webster.com/dictionary/folk, definition 1). I think its a pretty neutral term?

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I don't think this PR is the right place to discuss this kind of thing. Maybe open a new issue or a thread in Slack.

```

### Slack setup

slack-moderator-words requires the following OAuth scopes on its Slack app:

- `channels:history`
- `chat:write`
- `chat:write.public`

Additionally, slack-moderator-words also requires the following event subscriptions (Subscribe to events on behalf of users):

- `message.channels`

slack-moderator-words does not require any interactive components.

The [slack app creation guide][app-creation] explains what to do with these values.

## Deployment

Kubernetes runs slack-moderator-words in a Kubernetes cluster; check out the [config](../cluster/slack-moderator-words).

slack-moderator-words can also run on Google App Engine. To do this, create a `config.json` file in this
directory as described above and then run `gcloud app deploy`, using a Google Cloud Platform project
that has [App Engine](https://console.cloud.google.com/appengine) enabled. For most Slack teams,
slack-moderator should fit in the free quota.

[app-creation]: ../docs/app-creation.md
111 changes: 111 additions & 0 deletions slack-moderate-words/events.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,111 @@
/*
Copyright 2021 The Kubernetes Authors.

Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at

http://www.apache.org/licenses/LICENSE-2.0

Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/

package main

import (
"bytes"
"encoding/json"
"fmt"
"io/ioutil"
"log"
"net/http"
"strings"

"sigs.k8s.io/slack-infra/slack"
"sigs.k8s.io/slack-infra/slack-moderate-words/model"
)

type handler struct {
client *slack.Client
filters model.FilterConfig
}

// ServeHTTP handles Slack webhook requests.
func (h *handler) ServeHTTP(rw http.ResponseWriter, r *http.Request) {
body, err := ioutil.ReadAll(r.Body)
if err != nil {
logError(rw, "Failed to read incoming request body: %v", err)
return
}
defer r.Body.Close()

if err := h.client.VerifySignature(body, r.Header); err != nil {
logError(rw, "Failed validation: %v", err)
return
}

event := &model.SlackEvent{}
err = json.NewDecoder(bytes.NewReader(body)).Decode(event)
if err != nil {
logError(rw, "Failed to unmarshal payload: %v", err)
panic(err)
}

if event.Type == "url_verification" {
Comment thread
cpanato marked this conversation as resolved.
resp := &model.Challenge{}
resp.Challenge = event.Challenge
challengeJson, err := json.Marshal(resp)
if err != nil {
logError(rw, "Failed to marshal challenge payload: %v", err)
}
rw.Header().Set("Content-Type", "application/json")
rw.WriteHeader(http.StatusOK)
_, _ = rw.Write(challengeJson)
return
}

// reply ok rigth away
rw.Header().Set("Content-Type", "application/json")
rw.WriteHeader(http.StatusOK)
_, _ = rw.Write([]byte(""))

// If come from Bot just ignore and not moderate
if event.Event.BotID != "" {
return
}

log.Printf("[EVENT] %+v", event)

if h.filters != nil {
for _, filter := range h.filters {
for _, word := range filter.Triggers {
if strings.Contains(event.Event.Text, word) {
req := map[string]interface{}{
"channel": event.Event.Channel,
"user": event.Event.User,
"text": filter.Message,
}

if event.Event.ThreadTS != "" {
req["thread_ts"] = event.Event.ThreadTS
}

err = h.client.CallMethod(filter.Action, req, nil)
if err != nil {
logError(rw, "Failed send message to slack: %v", err)
}
}
}
}
}
}

func logError(rw http.ResponseWriter, format string, args ...interface{}) {
s := fmt.Sprintf(format, args...)
log.Println(s)
http.Error(rw, s, 500)
}
51 changes: 51 additions & 0 deletions slack-moderate-words/events_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,51 @@
// handlers_test.go
package main

import (
"net/http"
"net/http/httptest"
"strings"
"testing"
"time"
)

func TestEventsHandler(t *testing.T) {
req, err := http.NewRequest("POST", "/webhook", strings.NewReader(`{
"token": "z26uFbvR1xHJEdHE1OQiO6t8",
"team_id": "T061EG9RZ",
"api_app_id": "A0FFV41KK",
"event": {
"type": "message",
"user": "U061F1EUR",
"channel": "C061EG9SL",
"text": "honk",
"ts": "1612790186.002000",
"channel_type": "channel"
},
"type": "event_callback",
"authed_users": [],
"authorizations": {},
"event_id": "Ev9UQ52YNA",
"event_context": "EC12345",
"event_time": 1234567890
}`))
if err != nil {
t.Fatal(err)
}

rr := httptest.NewRecorder()

h := &handler{client: nil, filters: nil}
handler := http.Handler(h)

// TODO: this is a failing test when the slack headers does not match
// rewrite to pass a fake and make a happy path :)
req.Header.Add("X-Slack-Signature", "v0=87fbffb089501ba823991cc20058df525767a8a2287b3809f9afff3e3b600dd8")
req.Header.Add("X-Slack-Request-Timestamp", time.Now().String())
handler.ServeHTTP(rr, req)

if status := rr.Code; status != http.StatusInternalServerError {
t.Errorf("handler returned wrong status code: got %v want %v",
status, http.StatusOK)
}
}
4 changes: 4 additions & 0 deletions slack-moderate-words/filters.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
- triggers:
- guys
action: chat.postEphemeral
message: "May I suggest \"all\" instead when addessing a group of people? Thank you. :slightly_smiling_face:"
Comment thread
nikhita marked this conversation as resolved.
25 changes: 25 additions & 0 deletions slack-moderate-words/healthz.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
/*
Copyright 2021 The Kubernetes Authors.

Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at

http://www.apache.org/licenses/LICENSE-2.0

Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/

package main

import "net/http"

func handleHealthz(w http.ResponseWriter, r *http.Request) {
w.Header().Set("Content-Type", "application/json")
w.WriteHeader(http.StatusOK)
_, _ = w.Write([]byte(`{"status": "ok"}`))
}
Loading