Initial open source commit
This commit is contained in:
@@ -0,0 +1,3 @@
|
||||
main.go
|
||||
*.swo
|
||||
*.swp
|
||||
@@ -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)
|
||||
@@ -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
|
||||
}
|
||||
@@ -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()
|
||||
}
|
||||
@@ -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"
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
||||
@@ -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"}}
|
||||
@@ -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)
|
||||
@@ -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-----
|
||||
@@ -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()
|
||||
}
|
||||
@@ -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)
|
||||
}
|
||||
@@ -0,0 +1,3 @@
|
||||
main.go
|
||||
*.swo
|
||||
*.swp
|
||||
@@ -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)
|
||||
@@ -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
|
||||
}
|
||||
@@ -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()
|
||||
}
|
||||
Reference in New Issue
Block a user