Initial open source commit

This commit is contained in:
frikky
2020-05-11 19:17:35 +02:00
commit f3587ac821
205 changed files with 63293 additions and 0 deletions
+3
View File
@@ -0,0 +1,3 @@
main.go
*.swo
*.swp
+17
View File
@@ -0,0 +1,17 @@
# Local testing
1. Change hook.go package to main
```bash
mv ../main.go .
go run main.go hook.go
```
# Deploy local
```bash
gcloud functions deploy webhook --runtime go111 --entry-point Authorization --trigger-http --project shuffle-241517 --memory=128 --set-env-vars=FUNCTION_APIKEY=asdasd,CALLBACKURL=shuffler.io,HOOKID=test123
```
# Build and deploy from gui
1. rm webhook.zip
2. zip webhook.zip hook.go
3. Upload to bucket https://console.cloud.google.com/storage/browser/shuffle-241517.appspot.com?project=shuffle-241517
4. Restart hook(s) (https://shuffler.io/webhooks)
+415
View File
@@ -0,0 +1,415 @@
package main
// APPS:
// apps.dev.microsoft.com
// REMOVE ACCESS:
// https://portal.office.com/account/#
// Developer:
// https://developer.microsoft.com/en-us/graph/docs/concepts/permissions_reference
// Bots:
// https://dev.botframework.com/bots
// Connectors
// https://outlook.office.com/connectors/home/login/#/new
// https://go.microsoft.com/fwlink/?linkid=857599
import (
"bytes"
"encoding/json"
"errors"
"fmt"
"io/ioutil"
"log"
"net/http"
"os"
"strings"
"time"
)
type Info struct {
Url string `json:"url" datastore:"url"`
Name string `json:"name" datastore:"name"`
Description string `json:"description" datastore:"description"`
}
// Actions to be done by webhooks etc
// Field is the actual field to use from json
type HookAction struct {
Type string `json:"type" datastore:"type"`
Name string `json:"name" datastore:"name"`
Id string `json:"id" datastore:"id"`
Field string `json:"field" datastore:"field"`
}
type Hook struct {
Id string `json:"id" datastore:"id"`
Info Info `json:"info" datastore:"info"`
Actions []HookAction `json:"actions" datastore:"actions"`
Type string `json:"type" datastore:"type"`
Status string `json:"status" datastore:"status"`
Running bool `json:"running" datastore:"running"`
}
type TeamsHook struct {
MembersAdded []struct {
ID string `json:"id"`
} `json:"membersAdded"`
Type string `json:"type"`
Timestamp time.Time `json:"timestamp"`
LocalTimestamp string `json:"localTimestamp"`
ID string `json:"id"`
ChannelID string `json:"channelId"`
ServiceURL string `json:"serviceUrl"`
From struct {
ID string `json:"id"`
Name string `json:"name"`
} `json:"from"`
Conversation struct {
IsGroup bool `json:"isGroup"`
ConversationType string `json:"conversationType"`
ID string `json:"id"`
TenantID string `json:"tenantId"`
} `json:"conversation"`
Recipient struct {
ID string `json:"id"`
Name string `json:"name"`
} `json:"recipient"`
ChannelData struct {
Team struct {
ID string `json:"id"`
} `json:"team"`
EventType string `json:"eventType"`
Tenant struct {
ID string `json:"id"`
} `json:"tenant"`
} `json:"channelData"`
}
var hook Hook
var baseUrl = "https://shuffler.io"
type OauthToken struct {
TokenType string `json:"token_type"`
ExpiresIn int `json:"expires_in"`
ExtExpiresIn int `json:"ext_expires_in"`
AccessToken string `json:"access_token"`
}
type TeamsResponse struct {
Conversation struct {
ID string `json:"id"`
} `json:"conversation"`
From struct {
ID string `json:"id"`
Name string `json:"name"`
} `json:"from"`
Recipient struct {
ID string `json:"id"`
Name string `json:"name"`
} `json:"recipient"`
ReplyToId string `json:"replyToId"`
Type string `json:"type"`
Text string `json:"text"`
}
// This should be in a token thingy, to be controlled in workflow
func sendRequest(token OauthToken, message TeamsHook) error {
//POST https://smba.trafficmanager.net/apis/v3/conversations/12345/activities
//Authorization: Bearer eyJhbGciOiJIUzI1Ni...
//
//(JSON-serialized Activity message goes here)
tmpData := TeamsResponse{}
tmpData.Conversation.ID = message.Conversation.ID
tmpData.From = message.Recipient
tmpData.Recipient = message.From
tmpData.ReplyToId = message.ID
tmpData.Type = "message"
tmpData.Text = "HELO"
data, err := json.Marshal(tmpData)
if err != nil {
return err
}
// /v3/conversations/{conversationId}/activities/{activityId}
fullurl := fmt.Sprintf("%sv3/conversations/%s/activities", message.ServiceURL, message.Conversation.ID)
log.Println(fullurl)
log.Println(string(data))
req, err := http.NewRequest(
http.MethodPost,
fullurl,
bytes.NewBuffer([]byte(data)),
)
req.Header.Add("Authorization", fmt.Sprintf("Bearer %s", token.AccessToken))
req.Header.Add("Content-Type", "application/json")
if err != nil {
return err
}
client := http.Client{}
res, err := client.Do(req)
if err != nil {
return err
}
log.Printf("Status: %d", res.StatusCode)
body, err := ioutil.ReadAll(res.Body)
if err != nil {
return err
}
log.Println(string(body))
return nil
}
func get_accesstoken() (OauthToken, error) {
client_id := "9a2a2a63-c63c-4487-baf0-4ff3f4873a7f"
client_secret := ":3]D6oFimiXbuV20xH?Dzu@LR*6IFVbq"
fullurl := fmt.Sprintf("https://login.microsoftonline.com/botframework.com/oauth2/v2.0/token")
data := fmt.Sprintf("grant_type=client_credentials&client_id=%s&client_secret=%s&scope=https://api.botframework.com/.default", client_id, client_secret)
log.Println(data)
req, err := http.NewRequest(
http.MethodPost,
fullurl,
bytes.NewBuffer([]byte(data)),
)
if err != nil {
return OauthToken{}, err
}
client := http.Client{}
res, err := client.Do(req)
if err != nil {
return OauthToken{}, err
}
log.Printf("Status: %d", res.StatusCode)
body, err := ioutil.ReadAll(res.Body)
if err != nil {
return OauthToken{}, err
}
token := OauthToken{}
err = json.Unmarshal(body, &token)
if err != nil {
return OauthToken{}, err
}
return token, nil
}
//func CheckTenantId(message TeamsHook) {
// fullurl := fmt.Sprintf("%s/api/v1/functions/tenants/%s", baseUrl, message.Conversation.TenantID)
// req, err := http.NewRequest(
// http.MethodPost,
// fullurl,
// bytes.NewBuffer([]byte(data)),
// )
//
// req.Header.Add("Authorization", fmt.Sprintf(`Bearer %s`, baseApikey))
// req.Header.Add("Content-Type", "application/json")
// if err != nil {
// return []string{}, err
// }
//
// client := http.Client{}
// res, err := client.Do(req)
// if err != nil {
// return []string{}, err
// }
//
// log.Printf("Status: %d", res.StatusCode)
// body, err := ioutil.ReadAll(res.Body)
// if err != nil {
// return []string{}, err
// }
//}
func Authorization(resp http.ResponseWriter, request *http.Request) {
// FIXME - don't have this here, but before loops etc
// How to keep it refreshed?
token, err := get_accesstoken()
if err != nil {
log.Printf("Failed: %s", err)
}
body, err := ioutil.ReadAll(request.Body)
if err != nil {
return
}
log.Println("Data")
log.Println(string(body))
hook := TeamsHook{}
err = json.Unmarshal(body, &hook)
if err != nil {
resp.WriteHeader(200)
resp.Write([]byte(`{"success": false}`))
return
}
// Only handle messages currently
if hook.Type != "message" {
resp.WriteHeader(200)
resp.Write([]byte(`{"success": false}`))
return
}
// Find the ORG based on the above info. How?
// MSTeams hook should have it attached somehow?
log.Printf(string(body))
//log.Printf(hook.ServiceURL)
//log.Printf(hook.ChannelID)
//log.Printf(hook.ID)
//log.Printf("%#v", hook.Conversation)
err = sendRequest(token, hook)
if err != nil {
log.Printf("Failed: %s", err)
}
resp.WriteHeader(200)
resp.Write([]byte(fmt.Sprintf(`{"success": true}`)))
}
func loadConfiguration(fullUrl string, apikey string) (Hook, error) {
client := &http.Client{}
req, err := http.NewRequest(
"GET",
fullUrl,
nil,
)
if err != nil {
log.Printf("Error making http request: %s", req)
return Hook{}, err
}
req.Header.Add("Authorization", fmt.Sprintf(`Bearer %s`, apikey))
req.Header.Add("Content-Type", "application/json")
resp, err := client.Do(req)
if err != nil {
log.Printf("Error in http request: %s", req)
return Hook{}, err
}
body, err := ioutil.ReadAll(resp.Body)
if err != nil {
log.Printf("Error reading response: %s", req)
return Hook{}, err
}
err = json.Unmarshal(body, &hook)
if err != nil {
log.Printf("Failed unmarshaling hook API", req)
return Hook{}, err
}
return hook, nil
}
// GetUserDetails - Get one user's details from randomuser.me API
func ForwardRequest(resp http.ResponseWriter, request *http.Request) error {
callbackUrl := os.Getenv("CALLBACKURL")
hookId := os.Getenv("HOOKID")
apikey := os.Getenv("FUNCTION_APIKEY")
hook, err := loadConfiguration(
fmt.Sprintf("%s/api/v1/hooks/%s", callbackUrl, hookId),
apikey,
)
log.Println("Done loading!")
if err != nil {
return err
}
log.Printf("%#v", hook)
// Find all things to execute
workflowUrls := []string{}
for _, item := range hook.Actions {
if item.Type == "" {
log.Printf("CONTINUE AAS EMPTY ITEM: %#v", item)
continue
}
if item.Type == "workflow" {
workflowUrls = append(workflowUrls, item.Id)
}
}
if len(workflowUrls) == 0 {
return errors.New("No actions to do yet")
}
log.Printf("Should send data to the following: %s", strings.Join(workflowUrls, ", "))
randomUserClient := http.Client{
Timeout: time.Second * 3,
}
body, err := ioutil.ReadAll(request.Body)
if err != nil {
return err
}
// Prepare data
type arg struct {
ExecutionArgument string `json:"execution_argument"`
}
data := arg{
ExecutionArgument: string(body),
}
newjson, err := json.Marshal(data)
if err != nil {
return err
}
// Loop all executions to run
for _, item := range workflowUrls {
fullUrl := fmt.Sprintf("%s/api/v1/workflows/%s/execute", callbackUrl, item)
log.Printf("Sending data to %s", fullUrl)
req, err := http.NewRequest(
http.MethodPost,
fullUrl,
bytes.NewBuffer(newjson),
)
req.Header.Add("Authorization", fmt.Sprintf(`Bearer %s`, apikey))
req.Header.Add("Content-Type", "application/json")
if err != nil {
return err
}
res, err := randomUserClient.Do(req)
if err != nil {
return err
}
log.Printf("Status: %d", res.StatusCode)
body, err := ioutil.ReadAll(res.Body)
if err != nil {
return err
}
log.Printf(string(body))
}
//log.Println(string(newbody))
return nil
}
+41
View File
@@ -0,0 +1,41 @@
package main
import (
"log"
"net/http"
"os"
"github.com/gorilla/handlers"
"github.com/gorilla/mux"
)
func webhook() {
// FIXME - remove static
port := ":8080"
baseFilePath := "/"
mux := mux.NewRouter()
mux.SkipClean(true)
// FIXME - Add path for updating the hook? Can be a specific POST requeuest from backend
mux.HandleFunc(baseFilePath, Authorization).Methods("POST")
mux.HandleFunc("/test", Authorization).Methods("POST")
handlers.LoggingHandler(os.Stdout, mux)
loggedRouter := handlers.LoggingHandler(os.Stdout, mux)
log.Printf("Starting on http://localhost%s", port)
err := http.ListenAndServe(
port,
loggedRouter,
)
if err != nil {
log.Fatal("ListenAndServer: ", err)
}
}
func main() {
webhook()
}
+50
View File
@@ -0,0 +1,50 @@
{
"$schema": "https://developer.microsoft.com/en-us/json-schemas/teams/v1.5/MicrosoftTeams.schema.json",
"manifestVersion": "1.5",
"version": "1.0.0",
"id": "9a2a2a63-c63c-4487-baf0-4ff3f4873a7f",
"packageName": "com.example.myapp",
"devicePermissions" : [],
"developer": {
"name": "@frikkylikeme",
"websiteUrl": "https://shuffler.io/",
"privacyUrl": "https://shuffler.io/privacy",
"termsOfUseUrl": "https://shuffler.io/tos"
},
"localizationInfo": {
"defaultLanguageTag": "en-us"
},
"name": {
"short": "Shuffle",
"full": "Shuffle"
},
"description": {
"short": "Shuffle is a workflow automation platform",
"full": "Shuffle is a workflow automation platform. Find more info at https://shuffler.io"
},
"icons": {
"outline": "outline.png",
"color": "color.png"
},
"accentColor": "#15202b",
"bots": [
{
"botId": "9a2a2a63-c63c-4487-baf0-4ff3f4873a7f",
"needsChannelSelector": false,
"isNotificationOnly": false,
"scopes": [ "team", "personal", "groupchat" ],
"supportsFiles": false,
"commandLists": [
{
"scopes": [ "team", "groupchat", "personal" ],
"commands": [
{
"title": "test",
"description": "THIS IS FOR TESTING"
}
]
}
]
}
]
}
+35
View File
@@ -0,0 +1,35 @@
curl -XPOST http://localhost:8080 -d '{
"membersAdded": [
{
"id": "28:f5d48856-5b42-41a0-8c3a-c5f944b679b0"
}
],
"type": "conversationUpdate",
"timestamp": "2017-02-23T19:38:35.312Z",
"localTimestamp": "2017-02-23T12:38:35.312-07:00",
"id": "f:5f85c2ad",
"channelId": "msteams",
"serviceUrl": "https://smba.trafficmanager.net/amer-client-ss.msg/",
"from": {
"id": "29:1I9Is_Sx0OIy2rQ7Xz1lcaPKlO9eqmBRTBuW6XzkFtcjqxTjPaCMij8BVMdBcL9L_RwWNJyAHFQb0TRzXgyQvA"
},
"conversation": {
"isGroup": true,
"conversationType": "channel",
"id": "19:efa9296d959346209fea44151c742e73@thread.skype"
},
"recipient": {
"id": "28:f5d48856-5b42-41a0-8c3a-c5f944b679b0",
"name": "SongsuggesterBot"
},
"channelData": {
"team": {
"id": "19:efa9296d959346209fea44151c742e73@thread.skype"
},
"eventType": "teamMemberAdded",
"tenant": {
"id": "72f988bf-86f1-41af-91ab-2d7cd011db47"
}
}
}'
#{"type":"message","id":"4oN7bHB4dit7scwHygF1pf-h|0000000","timestamp":"2019-09-06T15:21:21.9035613Z","serviceUrl":"https://webchat.botframework.com/","channelId":"webchat","from":{"id":"4ccfb6b9-5755-426e-914d-641dd74f5e0f"},"conversation":{"id":"4oN7bHB4dit7scwHygF1pf-h"},"recipient":{"id":"Shuffle@qKw6tMx9fE8","name":"Shuffler"},"textFormat":"plain","locale":"en-US","text":"hi","entities":[{"type":"ClientCapabilities","requiresBotState":true,"supportsListening":true,"supportsTts":true}],"channelData":{"clientActivityID":"15677832808420.ishosdmdfbd"}}
+57
View File
@@ -0,0 +1,57 @@
# Outlook trigger
Makes it possible to trigger a workflow based on an email
## Local testing - Same as ../webhook
```bash
mv ../main.go
go run main.go hook.go
```
# Deploy gcloud
gcloud functions deploy outlooktrigger --runtime go111 --entry-point Authorization --trigger-http --project shuffler --memory=128 --set-env-vars=FUNCTION_APIKEY=asdasd,CALLBACKURL=shuffler.io,TRIGGERID=test123,WORKFLOW_ID=YOUR_WORKFLOW_ID
# Build and deploy
1. Set hook.go line 1 from "package main" to "package function"
2. zip outlooktrigger.tar hook.go
3. Upload to bucket https://console.cloud.google.com/storage/browser/shuffler.appspot.com?project=shuffler
4. Go to the functions https://console.cloud.google.com/functions/list?project=shuffler
## How it works (from frontend to backend)
### Choose mailfolders
1. Use microsoft graph api to get the folders the user wants to listen to
* Have the user write their primary email (default) or another one
* Have it show the folders for the email with chooseable buttons somehow
|inbox
|-subinbox
|--subsubinbox <-- choose e.g. this one
|otherfolder
API:
// requestUrl := fmt.Sprintf("https://graph.microsoft.com/v1.0/users/me/mailfolders")
### Add callback subscription
2. Make an APIcall to ("https://outlook.office.com/api/v2.0/me/mailfolders('inbox')/messages") with callback url defined as "https://shuffler.io/api/v1/workflows/{key}/email/authorize"
* Should this be set up whenever the user clicked start or when the workflow is created?
* Start click ->
1. Add another cloud function for the item
2. When it's ready, deploy it to authorize
3. Show it as ready
### Remove a subscription
* Since everything is already generated above, one would need to
* https://docs.microsoft.com/en-us/graph/api/subscription-delete?view=graph-rest-1.0&tabs=http
* DELETE https://graph.microsoft.com/v1.0/subscriptions/{id}
## CREATE - Fixme: LIST all current subscriptions, and stop them if they're towards the same endpoint
* POST /api/v1/workflows/{key}/outlook
* createOutlookSub(resp, request)
* getOutlookSubscriptions(client) // Used to remove all existing for same endpoint
* makeOutlookSubscription(client, folderIds, notificationUrl)
* Add data from ^ to triggerAuth
## DELETE
* DELETE /api/v1/workflows/{key}/outlook/{triggerId}
* handleDeleteOutlookSub(resp, request)
* handleOutlookSubRemoval(workflowId, triggerId)
+222
View File
@@ -0,0 +1,222 @@
package function
// Shuffle:
// https://portal.azure.com/#blade/Microsoft_AAD_RegisteredApps/ApplicationMenuBlade/Authentication/appId/e080cbf4-5dba-44b4-8643-a7c982189c16/isMSAApp//defaultBlade/Overview/servicePrincipalCreated/true
// Oauth playground:
// https://oauthplay.azurewebsites.net/
// APPS:
// https://apps.dev.microsoft.com
// REMOVE ACCESS:
// https://portal.office.com/account/#
// Developer:
// https://developer.microsoft.com/en-us/graph/docs/concepts/permissions_reference
// Bots:
// https://dev.botframework.com/bots
// Connectors
// https://outlook.office.com/connectors/home/login/#/new
// https://go.microsoft.com/fwlink/?linkid=857599
import (
//"encoding/json"
"bytes"
"fmt"
"io/ioutil"
"log"
"net/http"
"os"
"time"
)
type Info struct {
Url string `json:"url" datastore:"url"`
Name string `json:"name" datastore:"name"`
Description string `json:"description" datastore:"description"`
}
// Actions to be done by webhooks etc
// Field is the actual field to use from json
type HookAction struct {
Type string `json:"type" datastore:"type"`
Name string `json:"name" datastore:"name"`
Id string `json:"id" datastore:"id"`
Field string `json:"field" datastore:"field"`
}
type Hook struct {
Id string `json:"id" datastore:"id"`
Info Info `json:"info" datastore:"info"`
Actions []HookAction `json:"actions" datastore:"actions"`
Type string `json:"type" datastore:"type"`
Status string `json:"status" datastore:"status"`
Running bool `json:"running" datastore:"running"`
}
type TeamsHook struct {
MembersAdded []struct {
ID string `json:"id"`
} `json:"membersAdded"`
Type string `json:"type"`
Timestamp time.Time `json:"timestamp"`
LocalTimestamp string `json:"localTimestamp"`
ID string `json:"id"`
ChannelID string `json:"channelId"`
ServiceURL string `json:"serviceUrl"`
From struct {
ID string `json:"id"`
Name string `json:"name"`
} `json:"from"`
Conversation struct {
IsGroup bool `json:"isGroup"`
ConversationType string `json:"conversationType"`
ID string `json:"id"`
TenantID string `json:"tenantId"`
} `json:"conversation"`
Recipient struct {
ID string `json:"id"`
Name string `json:"name"`
} `json:"recipient"`
ChannelData struct {
Team struct {
ID string `json:"id"`
} `json:"team"`
EventType string `json:"eventType"`
Tenant struct {
ID string `json:"id"`
} `json:"tenant"`
} `json:"channelData"`
}
var hook Hook
var baseUrl = "https://shuffler.io"
type OauthToken struct {
TokenType string `json:"token_type"`
ExpiresIn int `json:"expires_in"`
ExtExpiresIn int `json:"ext_expires_in"`
AccessToken string `json:"access_token"`
}
type TeamsResponse struct {
Conversation struct {
ID string `json:"id"`
} `json:"conversation"`
From struct {
ID string `json:"id"`
Name string `json:"name"`
} `json:"from"`
Recipient struct {
ID string `json:"id"`
Name string `json:"name"`
} `json:"recipient"`
ReplyToId string `json:"replyToId"`
Type string `json:"type"`
Text string `json:"text"`
}
type O365hook struct {
OdataContext string `json:"@odata.context"`
Value []struct {
OdataType string `json:"@odata.type"`
ID interface{} `json:"Id"`
SubscriptionID string `json:"SubscriptionId"`
SubscriptionExpirationDateTime time.Time `json:"SubscriptionExpirationDateTime"`
SequenceNumber int `json:"SequenceNumber"`
ChangeType string `json:"ChangeType"`
Resource string `json:"Resource"`
ResourceData struct {
OdataType string `json:"@odata.type"`
OdataID string `json:"@odata.id"`
OdataEtag string `json:"@odata.etag"`
ID string `json:"Id"`
} `json:"ResourceData"`
} `json:"value"`
}
func Authorization(resp http.ResponseWriter, request *http.Request) {
body, err := ioutil.ReadAll(request.Body)
if err != nil {
log.Printf("Body: %s", err)
resp.WriteHeader(403)
return
}
if len(body) > 0 {
// In here - get the email data
// Check who it belongs to and run those workflows
// This should be set from run.go with the callback
// userId: {subscriptionID: {workflowID}}
// E.g. workflow: {auth: {userId
// workflow: {trigger: {
err = forwardRequest(body)
if err != nil {
log.Printf("Failed unmarshal: %s", err)
resp.WriteHeader(403)
return
}
resp.WriteHeader(200)
resp.Write([]byte("OK"))
return
}
token := request.URL.Query().Get("validationToken")
if len(token) == 0 {
log.Println("Validation token is missing")
resp.WriteHeader(403)
return
}
resp.WriteHeader(200)
resp.Write([]byte(string(token)))
}
// GetUserDetails - Get one user's details from randomuser.me API
func forwardRequest(body []byte) error {
callbackUrl := os.Getenv("CALLBACKURL")
workflowId := os.Getenv("WORKFLOW_ID")
apikey := os.Getenv("FUNCTION_APIKEY")
fullUrl := fmt.Sprintf("%s/api/v1/workflows/%s/execute", callbackUrl, workflowId)
//log.Printf("Sending data to %s", fullUrl)
data := fmt.Sprintf(`{"execution_argument": "%s"}`, string(body))
req, err := http.NewRequest(
http.MethodPost,
fullUrl,
bytes.NewBuffer([]byte(data)),
)
if err != nil {
return err
}
req.Header.Add("Authorization", fmt.Sprintf(`Bearer %s`, apikey))
req.Header.Add("Content-Type", "application/json")
randomUserClient := http.Client{
Timeout: time.Second * 5,
}
res, err := randomUserClient.Do(req)
if err != nil {
return err
}
log.Printf("Status: %d", res.StatusCode)
returnbody, err := ioutil.ReadAll(res.Body)
if err != nil {
return err
}
log.Printf("New body: %s", string(returnbody))
//log.Println(string(newbody))
return nil
}
@@ -0,0 +1,7 @@
{
"clientID": "70e37005-c954-4290-b573-d4b94e484336",
"clientSecret": ".eNw/A[kQFB5zL.agvRputdEJENeJ392",
"RedirectURL": "https://44d84ee7.ngrok.io/functions/outlook/register",
"AuthURL": "https://login.microsoftonline.com/common/oauth2/authorize",
"TokenURL": "https://login.microsoftonline.com/common/oauth2/token"
}
@@ -0,0 +1,21 @@
-----BEGIN CERTIFICATE-----
MIIDYDCCAkigAwIBAgIJAOvgxcclM1eyMA0GCSqGSIb3DQEBCwUAMEUxCzAJBgNV
BAYTAk5PMRMwEQYDVQQIDApTb21lLVN0YXRlMSEwHwYDVQQKDBhJbnRlcm5ldCBX
aWRnaXRzIFB0eSBMdGQwHhcNMTgwMzA1MTE1MTIwWhcNMjgwMzAyMTE1MTIwWjBF
MQswCQYDVQQGEwJOTzETMBEGA1UECAwKU29tZS1TdGF0ZTEhMB8GA1UECgwYSW50
ZXJuZXQgV2lkZ2l0cyBQdHkgTHRkMIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIB
CgKCAQEAowNqrvocJCLLcytYBfZhsqG3CkPsJ4PTicNyjuUGmlILPHlN1DE7jbDo
KuOKImfV4AfQANnttaPksZyuKJL8XGpC7YmF5mmInUWG48SmZjvRbBi1LnCrjJKS
ywh2lJqw4w30w2ItcpogDrYhh6+T3VyabcYngZjKSFgON3wo4I0c6aT19VVXGqnK
y1WEejZmiChV4iwEu4vMPIzt16QpIBr8NPSkBLLRDAGWMjFnIuYDEwgjVn6XYhM9
+NxhvY9es+qeqLQsZj2a1wcDGaw7G4iNZdltlPmlTCreRDBYBTsYCds/rJmPZ2xi
6jgiyNZj3xHG5Knw2YIw0OwHyT9mWQIDAQABo1MwUTAdBgNVHQ4EFgQUckr5tCzg
F1eBID7mtWTfeqyX4g0wHwYDVR0jBBgwFoAUckr5tCzgF1eBID7mtWTfeqyX4g0w
DwYDVR0TAQH/BAUwAwEB/zANBgkqhkiG9w0BAQsFAAOCAQEAAdH1aPcWBjJHF6n/
NgIiRSEU4mi5I6RUlPuR7dN7dcmF1Ho7quurNFzknXwks+a62oKSnkkFxxwrv/d6
dIH5kNibVs7oRxEpA3gUmXXKUW4RPrxAp2zN37t7zs5xbpTATxfJiIMP8Rjo0sOf
SilS22Sn0e0HxBi78t3DJEZvOQ9KSRuD1g9gOAY4lj/fni6rVJo8YCR2MyjmQoXB
luHDF4jTqi/TkXECfqQZu0pctx3maISpB1fAuaELwPDvqbLgoC97gl6bIEiKLkJ0
JkbGnb997K80ztFvFAKGyUtsvEDCupe/fdBPFqCruAQWI/BqVJFTdRD43dWEQANF
S8ApvA==
-----END CERTIFICATE-----
@@ -0,0 +1,27 @@
-----BEGIN RSA PRIVATE KEY-----
MIIEowIBAAKCAQEAowNqrvocJCLLcytYBfZhsqG3CkPsJ4PTicNyjuUGmlILPHlN
1DE7jbDoKuOKImfV4AfQANnttaPksZyuKJL8XGpC7YmF5mmInUWG48SmZjvRbBi1
LnCrjJKSywh2lJqw4w30w2ItcpogDrYhh6+T3VyabcYngZjKSFgON3wo4I0c6aT1
9VVXGqnKy1WEejZmiChV4iwEu4vMPIzt16QpIBr8NPSkBLLRDAGWMjFnIuYDEwgj
Vn6XYhM9+NxhvY9es+qeqLQsZj2a1wcDGaw7G4iNZdltlPmlTCreRDBYBTsYCds/
rJmPZ2xi6jgiyNZj3xHG5Knw2YIw0OwHyT9mWQIDAQABAoIBABJ+9L/dyQuglwz+
QgKLLhKinq4fftAM+ReMgZcNDW69GGFIMjh9TZCKHg2fu7Cjr3S37jXqhDoz2mL8
sBYSd2fU9rsU+4hlOQb/OIrnaSn4Z46oTwZx6kUM7HL1Bt9dnexlTPxOS3HRYwnI
SI2oslJPi4YhEaJ2v5ztwM8y20B/E/zSW2onWz5gB8/bdxSmuJaWfHioIEoac8Gf
BE7jiYMnx9kKeVfgkKPMBKXhAyE2lbAz7N5nDS/4HkbUMh389RBvakc4gkv/QkSW
LNuXpbcSqJiG0FcVutjYS87a/ul3IdhAYmZDTuvRUhNsWBiY3LSY4C9NlHhiJihg
RU1kknECgYEA1dPT6n4hy/OK2mT3ThGQQFMJWsnmcHSsvuK31+UYZOI9kNtBVxc6
HSHkh0G53o6o2wZLr2gMXy35O5ZxSbectA1Q4MJDlYBs1MfHdOVyE4z6mmA9TLN1
c+9pYOC0qx+6NwPdO7j2xdUMEUfzocWOeay0AzJg20BQYmKuy3Kc+tcCgYEAwyn6
n+XS0vodfJdHhvbW/jocQlHQOBK5HklZfq2PMgpRRDaBOvHP/f07egLc2inec2sC
yPSCEfRMMhFcU5NoBt4Unzz2Y8pbpL1L5kbM6B4IqK/5vYcbvkmBkhL3AFT6Fg/3
3XCdygPW9Vf1nRKr2KhT9dDvB+XO2B75JmKwMk8CgYApKzGf8kz7gZZ4WfwrccI+
QD6K1lihyjUAQ5J15Mv/kHeeDjjUVcqAlWf0irkImpr0IJAt43COWsGjsWF6efmX
yQCLZZuxixppFVXXsd122ivd0S28OMkiWzQEzP67+83Ujc/okcIhcNVz9lB4ExtN
Xe0CuI5haE6RwsI4tYZ33QKBgFq6ckPRcOAZ3IlmPp9Us3/+fdKq/BSFR7/3s347
q11FBKCkghFoBxx5lCPVntxhKIQZlHLdkHZOTvnbrkNAPNUsewPIMHcVxOLiCZ3k
/i9OfxIEtSJR5CjjPTQuUtu5pYWKKN2uE/ytKkpmeM1rt64CGv4lAmp2gGFijMs2
h9jrAoGBANPQO6cKqtnxvst3lnljVBoftlJgeHamUac+xeYKA5Hocv5VwLXMTzzu
09tAhQFvFwCWWrfdgtvIM6k5Sl9F5MdiO9VNflI0IVudIcm9FKorWogH02mtwxsw
hvk5VUk3awiZ/Nu9t38ukeqCetjQEf6yupy/14ZPLndN5naSeEwo
-----END RSA PRIVATE KEY-----
+41
View File
@@ -0,0 +1,41 @@
package main
import (
"log"
"net/http"
"os"
"github.com/gorilla/handlers"
"github.com/gorilla/mux"
)
func webhook() {
// FIXME - remove static
port := ":8080"
baseFilePath := "/"
mux := mux.NewRouter()
mux.SkipClean(true)
// FIXME - Add path for updating the hook? Can be a specific POST requeuest from backend
mux.HandleFunc(baseFilePath, Authorization).Methods("POST")
mux.HandleFunc("/authorize", Authorization).Methods("POST")
handlers.LoggingHandler(os.Stdout, mux)
loggedRouter := handlers.LoggingHandler(os.Stdout, mux)
log.Printf("Starting on http://localhost%s", port)
err := http.ListenAndServe(
port,
loggedRouter,
)
if err != nil {
log.Fatal("ListenAndServer: ", err)
}
}
func main() {
webhook()
}
+304
View File
@@ -0,0 +1,304 @@
package main
// This entire script should be part of the API backend
import (
"bytes"
"context"
"encoding/json"
"fmt"
"io/ioutil"
"log"
"net/http"
"time"
"golang.org/x/oauth2"
)
type Subscription struct {
ChangeType string `json:"changeType"`
NotificationURL string `json:"notificationUrl"`
Resource string `json:"resource"`
ExpirationDateTime string `json:"expirationDateTime"`
ClientState string `json:"clientState"`
}
// ClientState string `json:"ClientState,omitempty"`
// OdataType string `json:"@odata.type"`
//odata.type - Include "@odata.type":"#Microsoft.OutlookServices.PushSubscription". The PushSubscription entity defines NotificationURL.
//ChangeType- Specifies the types of events to monitor for that resource. See ChangeType for the supported types.
//ClientState - Optional property that indicates that each notification should be sent with a header by the same ClientState value. This lets the listener check the legitimacy of each notification.
//NotificationURL- Specifies where notifications should be sent to. This URL represents a web service typically implemented by the client.
//Resource- Specifies the resource to monitor and receive notifications on. You can use the optional query parameter $filter to refine the conditions for a notification, or use $select to include specific properties in a rich notification.
//https://outlook.office.com/mail.read
type Config struct {
ClientID string
ClientSecret string
RedirectUrl string
AuthUrl string
TokenUrl string
}
func getOfficeAppInfo() (Config, error) {
configpath := "integrations/config.json"
data, err := ioutil.ReadFile(configpath)
if err != nil {
//log.Fatal(err)
log.Printf("Error getting hive: %s\n", err)
}
config := Config{}
err = json.Unmarshal(data, &config)
if err != nil {
return Config{}, err
}
return config, nil
}
// This should be a popup for the user
func get_accesstoken() (*http.Client, OauthToken, error) {
ctx := context.Background()
config, err := getOfficeAppInfo()
if err != nil {
return nil, OauthToken{}, err
}
conf := &oauth2.Config{
ClientID: config.ClientID,
ClientSecret: config.ClientSecret,
Scopes: []string{
"Mail.Read",
"User.Read",
"https://outlook.office.com/mail.read",
},
RedirectURL: "https://localhost:8000",
Endpoint: oauth2.Endpoint{
AuthURL: config.AuthUrl,
TokenURL: config.TokenUrl,
},
}
//"Mail.Read.Shared",
//url := conf.AuthCodeURL("state", oauth2.SetAuthURLParam("resource", "https://outlook.office.com"))
// ADD DATA TO STATE HERE :O
url := conf.AuthCodeURL("workflow_id%3Dc2e0b50a-2957-427e-a97b-b989dc5a5408%26trigger_id%3D9e845679-5843-4959-a76c-a6d664e9df35%26username%3Drheyix.yt@gmail.com", oauth2.SetAuthURLParam("resource", "https://graph.microsoft.com"))
fmt.Printf("Visit the URL for the auth dialog: \n%v\n\n", url)
codechannel := make(chan string)
// Handles the server callback, listening on port 8000
go func() {
port := ":8000"
http.HandleFunc("/", func(response http.ResponseWriter, request *http.Request) {
tmpcode := request.URL.Query().Get("code")
if len(tmpcode) < 100 {
return
} else {
codechannel <- tmpcode
}
})
// FIX - might cause errors not being printed
err := http.ListenAndServeTLS(port, "integrations/server.crt", "integrations/server.key", nil)
if err != nil {
log.Printf("%s\n", err)
}
}()
code := <-codechannel
close(codechannel)
// https://stackoverflow.com/questions/52787420/multiple-resources-in-a-single-authorization-request
// Multi resource ^
access_token, err := conf.Exchange(ctx, code)
//log.Printf("%#v", access_token)
if err != nil {
return nil, OauthToken{}, err
}
//log.Printf("%#v", access_token)
outlookClient := conf.Client(ctx, access_token)
oauthToken := OauthToken{
AccessToken: access_token.AccessToken,
TokenType: access_token.TokenType,
RefreshToken: access_token.RefreshToken,
Expiry: access_token.Expiry,
}
return outlookClient, oauthToken, nil
}
type OauthToken struct {
AccessToken string `json:"AccessToken" datastore:"AccessToken,noindex"`
TokenType string `json:"TokenType" datastore:"TokenType,noindex"`
RefreshToken string `json:"RefreshToken" datastore:"RefreshToken,noindex"`
Expiry time.Time `json:"Expiry" datastore:"Expiry,noindex"`
}
type Mailfolders struct {
OdataContext string `json:"@odata.context"`
OdataNextLink string `json:"@odata.nextLink"`
Value []struct {
ID string `json:"id"`
DisplayName string `json:"displayName"`
ParentFolderID string `json:"parentFolderId"`
ChildFolderCount int `json:"childFolderCount"`
UnreadItemCount int `json:"unreadItemCount"`
TotalItemCount int `json:"totalItemCount"`
} `json:"value"`
}
func getFolders(client *http.Client) (Mailfolders, error) {
requestUrl := fmt.Sprintf("https://graph.microsoft.com/v1.0/users/frikky@shuffletest.onmicrosoft.com/mailfolders")
ret, err := client.Get(requestUrl)
if err != nil {
log.Printf("FolderErr: %s", err)
return Mailfolders{}, err
}
log.Printf("Status folders: %d", ret.StatusCode)
body, err := ioutil.ReadAll(ret.Body)
if err != nil {
log.Printf("Body: %s", err)
return Mailfolders{}, err
}
//log.Printf("Body: %s", string(body))
mailfolders := Mailfolders{}
err = json.Unmarshal(body, &mailfolders)
if err != nil {
log.Printf("Unmarshal: %s", err)
return Mailfolders{}, err
}
//fmt.Printf("%#v", mailfolders)
// FIXME - recursion for subfolders
// Recursive struct
// folderEndpoint := fmt.Sprintf("%s/%s/childfolders?$top=40", requestUrl, parentId)
for _, folder := range mailfolders.Value {
log.Println(folder.DisplayName)
}
return mailfolders, nil
}
// Subscribes to a mailbox based on some thingies
func makeSubscription(client *http.Client, folderIds []string) {
// FIXME - show the users folders from oauth and let them choose
fullUrl := "https://graph.microsoft.com/v1.0/subscriptions"
//resource := fmt.Sprintf("https://outlook.office.com/api/v2.0/me/mailfolders('inbox')/messages")
resource := fmt.Sprintf("me/mailfolders('inbox')/messages")
sub := Subscription{
ChangeType: "created",
NotificationURL: "https://de4fc12b.ngrok.io",
ExpirationDateTime: "2019-09-22T18:23:45.9356913Z",
ClientState: "This is a test",
Resource: resource,
}
data, err := json.Marshal(sub)
if err != nil {
log.Printf("Marshal: %s", err)
return
}
log.Printf(string(data))
req, err := http.NewRequest(
"POST",
fullUrl,
bytes.NewBuffer(data),
)
req.Header.Add("Content-Type", "application/json")
res, err := client.Do(req)
if err != nil {
log.Printf("Client: %s", err)
return
}
log.Printf("Status: %d", res.StatusCode)
body, err := ioutil.ReadAll(res.Body)
if err != nil {
log.Printf("Body: %s", err)
return
}
fmt.Println(string(body))
log.Println("Shoooould be set up :)")
}
func getOutlookClient(code string, accessToken OauthToken, redirectUri string) (*http.Client, *oauth2.Token, error) {
ctx := context.Background()
conf := &oauth2.Config{
ClientID: "70e37005-c954-4290-b573-d4b94e484336",
ClientSecret: ".eNw/A[kQFB5zL.agvRputdEJENeJ392",
Scopes: []string{
"Mail.Read",
"User.Read",
"https://outlook.office.com/mail.read",
},
RedirectURL: redirectUri,
Endpoint: oauth2.Endpoint{
AuthURL: "https://login.microsoftonline.com/common/oauth2/authorize",
TokenURL: "https://login.microsoftonline.com/common/oauth2/token",
},
}
if len(code) > 0 {
access_token, err := conf.Exchange(ctx, code)
if err != nil {
log.Printf("Access_token issue: %s", err)
return &http.Client{}, access_token, err
}
client := conf.Client(ctx, access_token)
return client, access_token, nil
} else {
// Manually recreate the oauthtoken
access_token := &oauth2.Token{
AccessToken: accessToken.AccessToken,
TokenType: accessToken.TokenType,
RefreshToken: accessToken.RefreshToken,
Expiry: accessToken.Expiry,
}
client := conf.Client(ctx, access_token)
return client, access_token, nil
}
}
func main() {
graphclient, oauthToken, err := get_accesstoken()
if err != nil {
log.Printf("Oauth setup: %s", err)
return
}
// FIXME - make this possible for alternative users (shared)
folders, err := getFolders(graphclient)
if err != nil {
log.Printf("Folder get error: %s", err)
return
}
_ = folders
folderIds := []string{"inbox"}
//log.Println(folders)
//log.Printf("%#v", oauthToken)
// Use oauthToken to generate data for outlook
outlookclient, _, err := getOutlookClient("", oauthToken, "https://localhost:8000")
makeSubscription(outlookclient, folderIds)
}
+3
View File
@@ -0,0 +1,3 @@
main.go
*.swo
*.swp
+17
View File
@@ -0,0 +1,17 @@
# Local testing
1. Change hook.go package to main
```bash
mv ../main.go .
go run main.go hook.go
```
# Deploy local
```bash
gcloud functions deploy webhook --runtime go111 --entry-point Authorization --trigger-http --project shuffler --memory=128 --set-env-vars=FUNCTION_APIKEY=asdasd,CALLBACKURL=shuffler.io,HOOKID=test123
```
# Build and deploy from gui
1. rm webhook.zip
2. zip webhook.zip hook.go
3. Upload to bucket https://console.cloud.google.com/storage/browser/shuffler.appspot.com?project=shuffler
4. Restart hook(s) (https://shuffler.io/webhooks)
+249
View File
@@ -0,0 +1,249 @@
package function
// BOTS
// https://dev.botframework.com/bots/channels?id=Shuffle
// APPS:
// apps.dev.microsoft.com
// REMOVE ACCESS:
// https://portal.office.com/account/#
// Developer:
// https://developer.microsoft.com/en-us/graph/docs/concepts/permissions_reference
import (
"bytes"
"encoding/json"
"errors"
"fmt"
"io/ioutil"
"log"
"net/http"
"os"
"strings"
"time"
)
type Info struct {
Url string `json:"url" datastore:"url"`
Name string `json:"name" datastore:"name"`
Description string `json:"description" datastore:"description"`
}
// Actions to be done by webhooks etc
// Field is the actual field to use from json
type HookAction struct {
Type string `json:"type" datastore:"type"`
Name string `json:"name" datastore:"name"`
Id string `json:"id" datastore:"id"`
Field string `json:"field" datastore:"field"`
}
type Hook struct {
Id string `json:"id" datastore:"id"`
Info Info `json:"info" datastore:"info"`
Actions []HookAction `json:"actions" datastore:"actions"`
Type string `json:"type" datastore:"type"`
Status string `json:"status" datastore:"status"`
Running bool `json:"running" datastore:"running"`
}
var hook Hook
func Authorization(resp http.ResponseWriter, request *http.Request) {
apikey := os.Getenv("FUNCTION_APIKEY")
callbackUrl := os.Getenv("CALLBACKURL")
hookId := os.Getenv("HOOKID")
if len(apikey) == 0 {
log.Println("Env FUNCTION_APIKEY not set")
resp.WriteHeader(401)
resp.Write([]byte(fmt.Sprintf(`{"success": false, "reason": "Internal error"}`)))
return
}
if len(callbackUrl) == 0 {
log.Println("Env CALLBACKURL not set")
resp.WriteHeader(401)
resp.Write([]byte(fmt.Sprintf(`{"success": false, "reason": "Internal error"}`)))
return
}
if len(hookId) == 0 {
log.Println("Env HOOKID not set")
resp.WriteHeader(401)
resp.Write([]byte(fmt.Sprintf(`{"success": false, "reason": "Internal error"}`)))
return
}
authorization := request.Header.Get("Authorization")
if len(authorization) == 0 {
resp.WriteHeader(401)
resp.Write([]byte(fmt.Sprintf(`{"success": false, "reason": "Authorization header required"}`)))
return
}
if !strings.HasPrefix(authorization, "Bearer") {
resp.WriteHeader(401)
resp.Write([]byte(fmt.Sprintf(`{"success": false, "reason": "Authorization header must start with Bearer"}`)))
return
}
apikeyCheck := strings.Split(authorization, " ")
if len(apikeyCheck) != 2 {
log.Println("Length is not 2 for apikey: %s vs %s", apikeyCheck[1], apikey)
resp.WriteHeader(401)
resp.Write([]byte(fmt.Sprintf(`{"success": false, "reason": "Invalid Apikey"}`)))
return
}
if apikeyCheck[1] != apikey {
log.Printf("Apikeys are not equal. Failed authentication.")
resp.WriteHeader(401)
resp.Write([]byte(fmt.Sprintf(`{"success": false, "reason": "Invalid Apikey"}`)))
return
}
err := ForwardRequest(resp, request)
if err != nil {
log.Printf("Error: %s", err)
resp.WriteHeader(401)
resp.Write([]byte(fmt.Sprintf(`{"success": false, "reason": "%s"}`, err)))
return
}
log.Println("Success?")
resp.WriteHeader(200)
resp.Write([]byte(fmt.Sprintf(`{"success": true}`)))
}
func loadConfiguration(fullUrl string, apikey string) (Hook, error) {
client := &http.Client{}
req, err := http.NewRequest(
"GET",
fullUrl,
nil,
)
if err != nil {
log.Printf("Error making http request: %s", req)
return Hook{}, err
}
req.Header.Add("Authorization", fmt.Sprintf(`Bearer %s`, apikey))
req.Header.Add("Content-Type", "application/json")
resp, err := client.Do(req)
if err != nil {
log.Printf("Error in http request: %s", req)
return Hook{}, err
}
body, err := ioutil.ReadAll(resp.Body)
if err != nil {
log.Printf("Error reading response: %s", req)
return Hook{}, err
}
err = json.Unmarshal(body, &hook)
if err != nil {
log.Printf("Failed unmarshaling hook API", req)
return Hook{}, err
}
return hook, nil
}
// GetUserDetails - Get one user's details from randomuser.me API
func ForwardRequest(resp http.ResponseWriter, request *http.Request) error {
callbackUrl := os.Getenv("CALLBACKURL")
hookId := os.Getenv("HOOKID")
apikey := os.Getenv("FUNCTION_APIKEY")
hook, err := loadConfiguration(
fmt.Sprintf("%s/api/v1/hooks/%s", callbackUrl, hookId),
apikey,
)
log.Println("Done loading!")
if err != nil {
return err
}
log.Printf("%#v", hook)
// Find all things to execute
workflowUrls := []string{}
for _, item := range hook.Actions {
if item.Type == "" {
log.Printf("CONTINUE AAS EMPTY ITEM: %#v", item)
continue
}
if item.Type == "workflow" {
workflowUrls = append(workflowUrls, item.Id)
}
}
if len(workflowUrls) == 0 {
return errors.New("No actions to do yet")
}
log.Printf("Should send data to the following: %s", strings.Join(workflowUrls, ", "))
randomUserClient := http.Client{
Timeout: time.Second * 3,
}
body, err := ioutil.ReadAll(request.Body)
if err != nil {
return err
}
// Prepare data
type arg struct {
ExecutionArgument string `json:"execution_argument"`
}
data := arg{
ExecutionArgument: string(body),
}
newjson, err := json.Marshal(data)
if err != nil {
return err
}
// Loop all executions to run
for _, item := range workflowUrls {
fullUrl := fmt.Sprintf("%s/api/v1/workflows/%s/execute", callbackUrl, item)
log.Printf("Sending data to %s", fullUrl)
req, err := http.NewRequest(
http.MethodPost,
fullUrl,
bytes.NewBuffer(newjson),
)
req.Header.Add("Authorization", fmt.Sprintf(`Bearer %s`, apikey))
req.Header.Add("Content-Type", "application/json")
if err != nil {
return err
}
res, err := randomUserClient.Do(req)
if err != nil {
return err
}
log.Printf("Status: %d", res.StatusCode)
body, err := ioutil.ReadAll(res.Body)
if err != nil {
return err
}
log.Printf(string(body))
}
//log.Println(string(newbody))
return nil
}
+39
View File
@@ -0,0 +1,39 @@
package main
import (
"log"
"net/http"
"os"
"github.com/gorilla/handlers"
"github.com/gorilla/mux"
)
func webhook() {
// FIXME - remove static
port := ":8080"
baseFilePath := "/"
mux := mux.NewRouter()
mux.SkipClean(true)
// FIXME - Add path for updating the hook? Can be a specific POST requeuest from backend
mux.HandleFunc(baseFilePath, Authorization).Methods("POST")
handlers.LoggingHandler(os.Stdout, mux)
loggedRouter := handlers.LoggingHandler(os.Stdout, mux)
err := http.ListenAndServe(
port,
loggedRouter,
)
if err != nil {
log.Fatal("ListenAndServer: ", err)
}
}
func main() {
webhook()
}