-
Notifications
You must be signed in to change notification settings - Fork 36
slack/bot: add moderate words bot #41
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -1 +1,2 @@ | ||
| .idea/ | ||
| /cluster/slack-moderator-words/secrets.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 | ||
| 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 | ||
| 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 |
| 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 | ||
| ) |
Large diffs are not rendered by default.
| 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 |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,4 @@ | ||
| config.json | ||
|
|
||
| # go binary | ||
| slack-moderate-words |
| 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"] |
| 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:" | ||
|
There was a problem hiding this comment. Choose a reason for hiding this commentThe 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? There was a problem hiding this comment. Choose a reason for hiding this commentThe 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 | ||
| 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" { | ||
|
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) | ||
| } | ||
| 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) | ||
| } | ||
| } |
| 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:" | ||
|
nikhita marked this conversation as resolved.
|
||
| 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"}`)) | ||
| } |
There was a problem hiding this comment.
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?
There was a problem hiding this comment.
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 :|There was a problem hiding this comment.
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?