├── join-welcome ├── requirements.txt └── handler.py ├── .gitignore ├── .DEREK.yml ├── signup └── handler.go ├── external-ip └── handler.go ├── slack └── handler.go ├── README.md ├── LICENSE ├── pub-cert.pem ├── secrets.yml └── stack.yml /join-welcome/requirements.txt: -------------------------------------------------------------------------------- 1 | requests -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | template 2 | build 3 | */node_modules/*/* 4 | -------------------------------------------------------------------------------- /.DEREK.yml: -------------------------------------------------------------------------------- 1 | redirect: https://raw.githubusercontent.com/openfaas/faas/master/.DEREK.yml 2 | -------------------------------------------------------------------------------- /signup/handler.go: -------------------------------------------------------------------------------- 1 | package function 2 | 3 | import ( 4 | "net/http" 5 | "os" 6 | ) 7 | 8 | // Handle a request with your middleware 9 | func Handle(w http.ResponseWriter, r *http.Request) { 10 | if urlVal, ok := os.LookupEnv("gist_url"); ok && len(urlVal) > 0 { 11 | 12 | http.Redirect(w, r, urlVal, http.StatusPermanentRedirect) 13 | 14 | } else { 15 | http.Error(w, "Unable to find gist_url variable", http.StatusInternalServerError) 16 | 17 | } 18 | } 19 | -------------------------------------------------------------------------------- /external-ip/handler.go: -------------------------------------------------------------------------------- 1 | package function 2 | 3 | import ( 4 | "io" 5 | "net/http" 6 | ) 7 | 8 | func Handle(w http.ResponseWriter, r *http.Request) { 9 | 10 | if r.Body != nil { 11 | defer r.Body.Close() 12 | } 13 | 14 | res, err := http.Get("https://api.ipify.org") 15 | if err != nil { 16 | http.Error(w, err.Error(), http.StatusInternalServerError) 17 | return 18 | } 19 | 20 | if res.Body != nil { 21 | defer res.Body.Close() 22 | io.Copy(w, res.Body) 23 | } 24 | } 25 | -------------------------------------------------------------------------------- /slack/handler.go: -------------------------------------------------------------------------------- 1 | package function 2 | 3 | import ( 4 | "net/http" 5 | "os" 6 | ) 7 | 8 | // Handle a request with your middleware 9 | func Handle(w http.ResponseWriter, r *http.Request) { 10 | if urlVal, ok := os.LookupEnv("slack_url"); ok && len(urlVal) > 0 { 11 | w.WriteHeader(http.StatusOK) 12 | 13 | w.Write([]byte(` 14 |
15 | 16 | 17 | Redirecting.. click here. 18 | 19 | `)) 25 | 26 | } else { 27 | http.Error(w, "Unable to find slack_url variable", http.StatusInternalServerError) 28 | 29 | } 30 | } 31 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # cloud-functions 2 | 3 | Functions used by OpenFaaS Community 4 | 5 | ## join-welcome 6 | 7 | This function is a bot that welcomes anyone who joins the #openfaas channel. 8 | 9 | ```sh 10 | export slack_incoming_webhook_url="URL" 11 | export slack_signing_token="SIGNING_TOKEN" 12 | 13 | faas-cli cloud seal --name openfaas-cloud-functions-secrets \ 14 | --literal slack-incoming-webhook-url="$slack_incoming_webhook_url" \ 15 | --literal slack-signing-token="$slack_signing_token" \ 16 | --cert=./pub-cert.pem 17 | ``` 18 | 19 | ## signup 20 | 21 | This function redirects to the OpenFaaS Cloud Community Cluster sign-up page. 22 | 23 | ## slack 24 | 25 | Redirects to the Slack sign-up instructions. 26 | 27 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2018 OpenFaaS Author(s) 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /pub-cert.pem: -------------------------------------------------------------------------------- 1 | -----BEGIN CERTIFICATE----- 2 | MIIErjCCApagAwIBAgIRAKjDGfSb/bc4eTH2tHX9/PIwDQYJKoZIhvcNAQELBQAw 3 | ADAeFw0xOTAyMjAxNDE3MjBaFw0yOTAyMTcxNDE3MjBaMAAwggIiMA0GCSqGSIb3 4 | DQEBAQUAA4ICDwAwggIKAoICAQCltEgSeD/Xdk3RPQsHOAxVcU5DjSkrEQ2ET0d7 5 | 1lO214NOcvsOiO60dsGciJYsq2nH5KW7k/IUggamTD47YTIfHOzprNzrLtVf3sDA 6 | uIQnt6sDQEtA6BOyUXCAV1//+727bWIYwZttHQVLr131AfnCWeEWvnoLrps4d6Af 7 | NPEi3Iaq+eWIu+x6RMK4xsLsmN2I/bRQszUlddydbWde+LkH3CWizJJ2PATqWsyB 8 | fNOZHPrS2Fc20nD6YkQuqTlke904B7NAvtOzboNHUBgC0VyK9b2kCqLdNPyB6SeH 9 | VG8YqsIyJL3FZkD7CkwbR3v6ipE1QFD+XzNn/grmVPTv5+GJlrpR4/GmRuor0k9G 10 | IbM7NvebCMc2VGIA+/0/SY2EngR5GiJyR5oEp7C8WRB8C4C6qj8VUrwzAt5wSvLm 11 | MOzaNAaIBDAZm5osBSkqc9I1Ys3236ilYxqA5Lihbobm6SPzVIjwHzcrOxnuVEiq 12 | Jw45Yrdyq2uvR8yyypbL87u6PtII8CePv50n2tglrbb8TsqVCXVGuU7e0FgXQqyC 13 | EKZET95CbYlisCSAGRhJ5qHIzA50y2Rhyx6L7lX1Yijoi7D6JQA974IqMo9x3PdQ 14 | rj3nVaMQQMiJo3ibAhKmmHRb3yUVFpu2kF4uWZWcsr5Cp/8EIRXcgqmRG5hIP3iS 15 | yZV0yQIDAQABoyMwITAOBgNVHQ8BAf8EBAMCAAEwDwYDVR0TAQH/BAUwAwEB/zAN 16 | BgkqhkiG9w0BAQsFAAOCAgEACZ2uV8BiOXKLPXjRpsZ5ThGPEe9Rt0MRCiqGCnbl 17 | 5uNwzNW0IVb9fhWdNQx3yu0wSlRF5IdNxQgjWYLOxLOlMIv1x1Fwcm4U5kcftZZj 18 | VF98MYAbDpUIy/wchYxpfLKMbGD6zWCB77IjYdyo0XkkAotY8jTnTquS/4GGc3WX 19 | jgBK7RCgSPAk4LmzqCtYtbGeZussb1Mcmmnj9ewHvSDQ4NYC0AtX4oA2rrdZfbT6 20 | +Zz/ccz82D5wQT7/lGDpUvmGsxiM2InqxMMGRgbtFOQ/WFYRSAAo8iuGlIwDBjKH 21 | Nro2+BdZeqqFAsmYyV68w+kxd+dyNF3U5RDibU2gTmE9NyX39NqWla+9M/5Zfxnx 22 | Vcfu23jPUAlPUVaxWSrYWeAUNVpzVjIPFUBfLuAl+Es48Tjum6ujp9wWnEhgWXov 23 | xgjQodSdj6aluMJQWKJotUut89/uarwoPGnOEhVgawq+tDdTKOa/oub7Mghg5T1T 24 | IXxsUWjjTboAsSkV35gzOeO6zhuQQTV4PPHEAppqRb+4qNbDUy0GJh9EFFgIBBF/ 25 | eaPGCNw6I48boWj4uYIU5sjPAJBJHId74o4yK7AhkUCM7eRrhqjFd1EHBT0L1eYZ 26 | fYgiMev/yoMTB2yxV2ylpIGHMiJpBXHLstaityx6097zw1FCBFHrJ7K6jK0QG49U 27 | xSM= 28 | -----END CERTIFICATE----- 29 | -------------------------------------------------------------------------------- /secrets.yml: -------------------------------------------------------------------------------- 1 | apiVersion: bitnami.com/v1alpha1 2 | kind: SealedSecret 3 | metadata: 4 | creationTimestamp: null 5 | name: openfaas-cloud-functions-secrets 6 | namespace: openfaas-fn 7 | spec: 8 | encryptedData: 9 | slack-incoming-webhook-url: AgB7L37Zc81XxGga/FcfpuSjcDno/nePndQKtTMPqcQg0z8eEYSQvUX5IrggFL6vaRJvUe+tcZVnYsVI7DNnzm7Hju17u26nihw0Iqds2pwoAd1RiBcxjolZqtS8F8MMnv36fKJmmSFCgC+Qh6WUILXrLAwns8H/wiFTTpm7UWMFvSHSjp9GEM+dxUW+p8RJn70TwCZfLfvj7e/2FFF+OGgEB9dxT+SKWLIvmmL46BgKbGPg4sqCeOJDApbrdg+LdJw+U0LWSCn9oCWMyVGZnMN0H2bIENouw+YuqRBhnx5d/hUfB5oGcZ/eNYewzwDINZrILzXRNctbtZg2bUxNTW1EesUjJ9qfC6+Iwj95xyvGxdrXNBK1OpujIjeJ0w8DQQvJy9vDSH+nP4qDDYBFqiBsJ8H6reP+jNM1B6oP8wEhhl4STozecrO1Zh/YxQkUXj1jXo/u+GjqvVy0J3SK0Z0R/yDBZcahY5Y70QA2OOTm233TdzR1TvzFNvGt60261d2bl3R8I8Z9JwN8dlACX0xkgiU/STuanMvnFD+d6GpH0Jxtgcg3EvnYAH7oirdIXXxin9P+E0fWWR9NO3oRsZYOYgOUbFXChO3kNREQIWRKZurZOs8Tm71uK9LsI1hQZpY+4yCv6pq0d7yXZIS7SQ44/aLHnKv1qmt9lWxwGD29pJFezdeTLikpHaeEgeIqxxvPcCx8zXKPjKCz3VyIGhcT8FXFWQl0s3+xwvLDexR1+vaYIrUqss1GXBLh11Sd/1oprgUNYg+h2TuP9SFWmOVoELWLmX0MGrXnSMr+5Q== 10 | slack-signing-token: AgB8qZ4vwlJXvAdgno1ZP6SHiBLBJ+bENZ7zdkVGf8mVTqpiSDop71mc+q/aREzuSN8Rwux/zApqKMJDakkGYHmx+Q701o1wxYvUGU2xIi65QBeNIXh+GFJcO3ynXQM3N1nBG6EBdq4k5scwS1vK3lZ/oNvsh+R9LCstDLAB+ZlPyCWFFc1rrD1ZW3y+fgAEo6JCSHNyy60OmlhTzQI22R90F2ApvlVAbvc9ABwP/np6xZ0Asi4taRqE+nn9XhPevaEEF6JHYxZ3TVopLjBBVw0NH8u54e4+f65zi+RkLkLfxXsbMMjfazXBcl4On8+U4k8kUKSpv2ADvbg0PzEjAtZBKLTFJ4LE6ciOiBcIXjczzbsyPdEciNxqVN2QKBtb2520wo4Bi1puVVEmdBxGT1mlLMUieaG3n9AmqyFYocNIPQScQe3DBRydlrxzJHvz+A14comwlRoQJjzVpt7Nj1p/tCgA/ajvO8nmjKvCKdPGddCTWdrbnDpKCMJ8reeioWlnbhrMWAozLrVI/Mj3TSuEmXfk6sDZwtZEtzNyDOf4x284LA3cb8s+QNDwKUN4e+4lLm0sOkqiypYvH7dv2sh0ztru7nhMQmmoFoxF3pkkQVTEHxavaQRdUzsLs7h0HbV/gBhRZK/4fDq3DsFBbMe0on8DP21nrsW25IursZOgwJ5zAMGvs6ttmLU2iliZitNUzTU+ghd8I9zcFjqXyINj4M6Au0RY/2cYI3N9TYSNDg== 11 | 12 | -------------------------------------------------------------------------------- /stack.yml: -------------------------------------------------------------------------------- 1 | provider: 2 | name: openfaas 3 | gateway: http://127.0.0.1:8080 4 | 5 | functions: 6 | slack: 7 | lang: golang-middleware 8 | handler: ./slack 9 | image: ${OWNER:-alexellis2}:/slack:${TAG:-latest} 10 | labels: 11 | com.openfaas.scale.zero: false 12 | environment: 13 | slack_url: "https://docs.openfaas.com/community" 14 | 15 | zoom: 16 | lang: golang-middleware 17 | handler: ./slack 18 | image: ${OWNER:-alexellis2}:/slack:${TAG:-latest} 19 | labels: 20 | com.openfaas.scale.zero: false 21 | environment: 22 | slack_url: "https://zoom.us/j/4215401314" 23 | 24 | insiders: 25 | lang: golang-middleware 26 | handler: ./slack 27 | image: ${OWNER:-alexellis2}:/slack:${TAG:-latest} 28 | labels: 29 | com.openfaas.scale.zero: false 30 | environment: 31 | slack_url: "https://github.com/users/alexellis/sponsorship" 32 | 33 | cal: 34 | lang: golang-middleware 35 | handler: ./slack 36 | image: ${OWNER:-alexellis2}:/slack:${TAG:-latest} 37 | labels: 38 | com.openfaas.scale.zero: false 39 | environment: 40 | slack_url: "https://calendly.com/alexellis" 41 | 42 | join-welcome: 43 | lang: python3-http 44 | handler: ./join-welcome 45 | image: ${OWNER:-alexellis2}/join-welcome:${TAG:-latest} 46 | labels: 47 | com.openfaas.scale.zero: false 48 | environment: 49 | write_debug: false 50 | read_debug: false 51 | combine_output: false 52 | content_type: application/json 53 | target_channel: "C4XCQR8TY" 54 | log_env: "0" 55 | secrets: 56 | - cloud-functions-secrets 57 | 58 | external-ip: 59 | lang: golang-middleware 60 | handler: ./external-ip 61 | image: ${OWNER:-alexellis2}/external-ip:${TAG:-latest} 62 | 63 | configuration: 64 | templates: 65 | - name: golang-middleware 66 | -------------------------------------------------------------------------------- /join-welcome/handler.py: -------------------------------------------------------------------------------- 1 | import sys 2 | import os 3 | import random 4 | import json 5 | import hmac 6 | from time import perf_counter 7 | 8 | import requests 9 | from hashlib import sha256 10 | 11 | def handle(event, context): 12 | SLACK_SIG_HEADER = "X-Slack-Signature" 13 | SLACK_TIMESTAMP_HEADER = "X-Slack-Request-Timestamp" 14 | 15 | # Convert bytestring to python3 default encoding (utf-8) 16 | body = event.body.decode() 17 | 18 | log_event(body) 19 | 20 | if os.getenv("log_env", "0") == "1": 21 | log_env() 22 | 23 | r = None 24 | try: 25 | r = json.loads(body) 26 | except ValueError: 27 | sys.stderr.write("Error parsing request, invalid JSON") 28 | return { 29 | "statusCode": 400, 30 | "body": "Error parsing request, invalid JSON" 31 | } 32 | 33 | if "challenge" in r: 34 | res = challenge(r) 35 | return { 36 | "statusCode": 200, 37 | "body": res 38 | } 39 | if "event" not in r: 40 | return { 41 | "statusCode": 400, 42 | "body": "Nothing to do with webhook" 43 | } 44 | 45 | webhook_url = read_secret("slack-incoming-webhook-url") 46 | signing_secret = read_secret("slack-signing-token") 47 | 48 | target_channel = os.getenv("target_channel") 49 | digest = "" 50 | if SLACK_SIG_HEADER in event.headers: 51 | digest = event.headers[SLACK_SIG_HEADER] 52 | 53 | # Takes format of: "X-Slack-Signature v0=hash" 54 | slack_request_timestamp = "" 55 | if SLACK_TIMESTAMP_HEADER in event.headers: 56 | slack_request_timestamp = event.headers[SLACK_TIMESTAMP_HEADER] 57 | 58 | input = f"v0:{slack_request_timestamp}:{body}" 59 | 60 | sys.stderr.write("Input: '{}'\n".format(input)) 61 | 62 | start = perf_counter() 63 | is_valid_hmac = valid_hmac(signing_secret, input, get_hash(digest)) 64 | end = perf_counter() 65 | elapsed = end - start 66 | 67 | sys.stderr.write("valid_hmac took {}s\n".format(elapsed)) 68 | 69 | if is_valid_hmac == True: 70 | start = perf_counter() 71 | 72 | event_res = process_event(r, target_channel, webhook_url) 73 | 74 | end = perf_counter() 75 | elapsed = end - start 76 | sys.stderr.write("process_event took {}s\n".format(elapsed)) 77 | 78 | return event_res 79 | else: 80 | sys.stderr.write("Invalid HMAC in X-Slack-Signature header") 81 | # sys.exit(1) 82 | return { 83 | "statusCode": 401, 84 | "body": "Invalid HMAC in X-Slack-Signature header" 85 | } 86 | 87 | def challenge(r): 88 | if r["type"] == "url_verification": 89 | res = {"challenge": r["challenge"]} 90 | return res 91 | 92 | # valid_hmac("key", "value", "90fbfcf15e74a36b89dbdb2a721d9aecffdfdddc5c83e27f7592594f71932481") 93 | def valid_hmac(key, msg, digest): 94 | hash = hmac.new(key.encode('utf-8'), msg.encode('utf-8'), sha256) 95 | hexdigest = hash.hexdigest() 96 | res = digest == hexdigest 97 | if res == False: 98 | msg = "Hash - got: '{}' computed: '{}' {}\n".format(digest, hexdigest, str(res)) 99 | sys.stderr.write(msg) 100 | 101 | return res 102 | 103 | def read_secret(name): 104 | value = "" 105 | 106 | with open("/var/openfaas/secrets/" + name) as f: 107 | value = f.read().strip() 108 | 109 | return value 110 | 111 | # input = "v0=hash" 112 | # print(get_hash(input)) 113 | def get_hash(input): 114 | index = input.find("=") 115 | if index > -1: 116 | return input[index+1:] 117 | return input 118 | 119 | def process_event(r, target_channel, webhook_url): 120 | event_type = r["event"]["type"] 121 | 122 | if r["event"]["channel"] == target_channel: 123 | if event_type == "member_joined_channel": 124 | if "user" in r["event"]: 125 | user_name = r["event"]["user"] 126 | who = "<@{}>".format(user_name) 127 | 128 | positive_emoticons = [":openfaas:", ":whale:", ":thumbsup:", ":wave:", ":sunglasses:", ":ok_hand:", ":chart_with_upwards_trend:", ":sunrise:", ":smiley:", ":smiley_cat:", ":parrot:", ":rocket:", ":100:", ":muscle:", ":signal_strength:", ":man-cartwheeling:"] 129 | 130 | start = perf_counter() 131 | 132 | emoticons = build_emoticons(positive_emoticons) 133 | end = perf_counter() 134 | elapsed = end - start 135 | 136 | sys.stderr.write("Generating emoticons took {}s\n".format(elapsed)) 137 | 138 | msg = {"text": "Let's all welcome {} to the community! {} ".format(who, emoticons.strip())} 139 | 140 | start = perf_counter() 141 | out_req = requests.post(webhook_url, json=msg) 142 | end = perf_counter() 143 | elapsed = end - start 144 | 145 | sys.stderr.write("{} response from Slack: {} in {}s\n".format(str(out_req.status_code), out_req.text, elapsed)) 146 | return { 147 | "statusCode": 200, 148 | "body": ("{} response from Slack: {} in {}s".format(str(out_req.status_code), out_req.text, elapsed)) 149 | } 150 | return { 151 | "statusCode": 400, 152 | "body": "Cannot process event_type: {} or given channel is not target channel".format(event_type) 153 | } 154 | 155 | def build_emoticons(emoticons): 156 | sample = random.sample(emoticons, 5) 157 | return " ".join(sample) 158 | 159 | def log_event(req): 160 | sys.stderr.write("{}\n".format(req)) 161 | 162 | def log_env(): 163 | envs = os.environ 164 | for e in envs: 165 | sys.stderr.write("{} {}\n".format(e, envs[s])) 166 | --------------------------------------------------------------------------------