├── tweet-fanclub ├── requirements.txt └── handler.py ├── tweet-stargazer ├── requirements.txt └── handler.py ├── .gitignore ├── stack.yml ├── LICENSE └── README.md /tweet-fanclub/requirements.txt: -------------------------------------------------------------------------------- 1 | requests 2 | redis 3 | -------------------------------------------------------------------------------- /tweet-stargazer/requirements.txt: -------------------------------------------------------------------------------- 1 | tweepy 2 | requests 3 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | master.zip 2 | twitter_secrets.yml 3 | build 4 | template 5 | -------------------------------------------------------------------------------- /stack.yml: -------------------------------------------------------------------------------- 1 | provider: 2 | name: faas 3 | gateway: http://localhost:8080 4 | network: func_functions 5 | 6 | functions: 7 | tweetstargazer: 8 | lang: python 9 | handler: ./tweet-stargazer 10 | image: alexellis2/tweet-stargazer 11 | environment_file: 12 | - twitter_secrets.yml 13 | 14 | tweetfanclub: 15 | lang: python 16 | handler: ./tweet-fanclub 17 | image: alexellis2/tweet-fanclub 18 | environment: 19 | cache-minutes: 60 20 | 21 | tweetpolaroid: 22 | skip_build: true 23 | image: alexellis2/faas-polaroid:0.1 24 | 25 | -------------------------------------------------------------------------------- /tweet-stargazer/handler.py: -------------------------------------------------------------------------------- 1 | import tweepy 2 | import os 3 | import requests 4 | import json 5 | 6 | def handle(st): 7 | req = json.loads(st) 8 | 9 | auth = tweepy.OAuthHandler(os.environ["consumer_key"], os.environ["consumer_secret"]) 10 | auth.set_access_token(os.environ["access_token"], os.environ["access_token_secret"]) 11 | 12 | api = tweepy.API(auth) 13 | 14 | file_name = req["filename"] 15 | 16 | r = requests.get("http://minio-shim:8080/get/" + file_name) 17 | 18 | polaroid_r = requests.post("http://gateway:8080/function/tweetpolaroid", r.content) 19 | 20 | f = open("/tmp/"+file_name, 'wb') 21 | f.write(polaroid_r.content) 22 | f.close() 23 | 24 | api.update_with_media("/tmp/"+file_name, req["login"] + " is a star-gazer for OpenFaaS") 25 | 26 | # Log the results 27 | print("Tweet sent") 28 | 29 | os.remove("/tmp/"+file_name) 30 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2018 Alex Ellis 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 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # faas-twitter-fanclub 2 | 3 | This is a set of Serverless functions for OpenFaaS that make a "Fanclub": 4 | 5 | Here's how it works: 6 | 7 | * Star a GitHub repository (configured by placing a webhook in your settings page) 8 | * twitterfanclub function receives the JSON, downloads the user's avatar and posts it to S3 9 | * twitterstargazer is called with the path in S3 - invokes a polaroid function with the image and Tweets it 10 | 11 | This is an example of function chaining and use of external storage for persistence. This is important because functions are stateless and ephermeral. 12 | 13 | See also: [Function Chaining](https://github.com/openfaas/faas/blob/master/guide/chaining_functions.md) 14 | 15 | Dependencies: 16 | 17 | * [OpenFaaS](https://github.com/openfaas/faas) 18 | * Minio 19 | * Redis (optional for rate limiting) 20 | * Minio-db https://github.com/alexellis/minio-db 21 | 22 | Functions: 23 | * Functions in stack.yml 24 | * twitterfanclub 25 | * twitterstargazer 26 | * get-avatar via https://github.com/alexellis/faas-dockercon 27 | * Polaroid function - https://github.com/faas-and-furious/faas-contributor-stamp/tree/master/polaroid 28 | 29 | Your Twitter API tokens need to go into a `twitter_secrets.yml` file. 30 | 31 | Example polaroids shared by [alexellisuk_bot](https://twitter.com/alexellisuk_bot): 32 | 33 | ![](https://pbs.twimg.com/media/DKgmEYoXoAku90i.jpg) 34 | 35 | ![](https://pbs.twimg.com/media/DKg1EJ6WsAAqAN0.png) 36 | 37 | https://twitter.com/alexellisuk_bot/status/912020783337279488 38 | 39 | * Got questions or want to find out more? 40 | 41 | Send an email over to alex@openfaas.com or try out the integration by clicking "Star" on [OpenFaaS](https://github.com/openfaas/faas) 42 | -------------------------------------------------------------------------------- /tweet-fanclub/handler.py: -------------------------------------------------------------------------------- 1 | import requests 2 | import json 3 | import base64 4 | import redis 5 | import os 6 | 7 | def handle(st): 8 | # parse Github event 9 | req = json.loads(st) 10 | 11 | minutes = 1 12 | minutes_val = os.getenv("cache-minutes") 13 | if minutes_val != None: 14 | minutes = int(minutes_val) 15 | 16 | loginName = req["sender"]["login"] 17 | 18 | try: 19 | redis_client = redis.StrictRedis("redis") 20 | redis_key = "tweet-" + loginName 21 | 22 | cached = redis_client.get(redis_key) 23 | 24 | if cached != None: 25 | print(loginName + " attempted to trigger event again before cache expired. Extending cache timeout.") 26 | redis_client.setex(redis_key, 60 * minutes, "1") 27 | return 28 | 29 | redis_client.setex(redis_key, 60 * minutes, "1") 30 | except Exception: 31 | print("Redis may be down or errored") 32 | 33 | # download the avatar binary using getavatar function 34 | r = requests.post("http://gateway:8080/function/get-avatar", json=req) 35 | 36 | res = r.json() 37 | 38 | # Figure out the correct extension for the avatar. 39 | ext = ".jpg" 40 | if res["contentType"] == "image/png": 41 | ext = ".png" 42 | 43 | # Take the encoded image and turn into binary bytes 44 | imageData = base64.standard_b64decode(res["content"]) 45 | put_url = "http://minio-shim:8080/put/" + loginName + ext 46 | # Store in the fan-club photo gallery 47 | r1 = requests.post(put_url, data= imageData) 48 | 49 | gazer = {} 50 | gazer["login"] = loginName 51 | gazer["filename"] = loginName + ext 52 | 53 | r2 = requests.post("http://gateway:8080/function/tweetstargazer", json.dumps(gazer)) 54 | 55 | club_res = {} 56 | club_res["put_url"] = put_url 57 | club_res["tweet_result"] = r2.text 58 | club_res["status"] = "success" 59 | club_res["username"] = req["sender"]["login"] 60 | club_res["bytes"] = len(imageData) 61 | 62 | # Useful for logging, GitHub's invoker will receive this string 63 | print(json.dumps(club_res)) 64 | --------------------------------------------------------------------------------