diff --git a/backend/app_sdk/app_base.py b/backend/app_sdk/app_base.py index b9eee023..4db578cb 100644 --- a/backend/app_sdk/app_base.py +++ b/backend/app_sdk/app_base.py @@ -1743,7 +1743,7 @@ class AppBase: # Custom format for ${name[0,1,2,...]}$ #submatch = "([${]{2}([0-9a-zA-Z_-]+)(\[.*\])[}$]{2})" - print(f"Returnedvalue: {value}") + #print(f"Returnedvalue: {value}") # OLD: Used until 13.03.2021: submatch = "([${]{2}#?([0-9a-zA-Z_-]+)#?(\[.*\])[}$]{2})" # \${[0-9a-zA-Z_-]+#?(\[.*?]}\$) submatch = "([${]{2}#?([0-9a-zA-Z_-]+)#?(\[.*?]}\$))" diff --git a/backend/app_sdk/build.sh b/backend/app_sdk/build.sh index 258a65db..eaee980c 100644 --- a/backend/app_sdk/build.sh +++ b/backend/app_sdk/build.sh @@ -1,6 +1,6 @@ #!/bin/bash NAME=shuffle-app_sdk -VERSION=0.8.63 +VERSION=0.8.64 docker rmi docker.pkg.github.com/frikky/shuffle/$NAME:$VERSION --force docker build . -t frikky/shuffle:app_sdk -t frikky/$NAME:$VERSION -t docker.pkg.github.com/frikky/shuffle/$NAME:$VERSION -t ghcr.io/frikky/$NAME:$VERSION diff --git a/backend/go-app/main.go b/backend/go-app/main.go index 29e1432e..1b6be031 100644 --- a/backend/go-app/main.go +++ b/backend/go-app/main.go @@ -73,11 +73,13 @@ var bucketName = "shuffler.appspot.com" var baseAppPath = "/home/frikky/git/shaffuru/tmp/apps" var baseDockerName = "frikky/shuffle" var registryName = "registry.hub.docker.com" +var runningEnvironment = "onprem" -//var syncUrl = "http://192.168.102.54:5002" var syncUrl = "https://shuffler.io" +var syncSubUrl = "https://shuffler.io" //var syncUrl = "http://localhost:5002" +//var syncSubUrl = "https://050196912a9d.ngrok.io" var dbclient *datastore.Client var requestCache *cache.Cache @@ -3478,10 +3480,12 @@ func handleWebhookCallback(resp http.ResponseWriter, request *http.Request) { // OrgId: activeOrgs[0].Id, workflowExecution, executionResp, err := handleExecution(item, workflow, newRequest) if err == nil { - err = increaseStatisticsField(ctx, "total_webhooks_ran", workflowExecution.Workflow.ID, 1, workflowExecution.ExecutionOrg) - if err != nil { - log.Printf("Failed to increase total apps loaded stats: %s", err) - } + /* + err = increaseStatisticsField(ctx, "total_webhooks_ran", workflowExecution.Workflow.ID, 1, workflowExecution.ExecutionOrg) + if err != nil { + log.Printf("Failed to increase total apps loaded stats: %s", err) + } + */ resp.WriteHeader(200) resp.Write([]byte(fmt.Sprintf(`{"success": true, "execution_id": "%s", "authorization": "%s"}`, workflowExecution.ExecutionId, workflowExecution.Authorization))) @@ -4918,145 +4922,6 @@ func handleGetSpecificStats(resp http.ResponseWriter, request *http.Request) { resp.Write([]byte(b)) } -func handleDeleteOutlookSub(resp http.ResponseWriter, request *http.Request) { - cors := handleCors(resp, request) - if cors { - return - } - - location := strings.Split(request.URL.String(), "/") - - var workflowId string - var triggerId string - if location[1] == "api" { - if len(location) <= 6 { - resp.WriteHeader(401) - resp.Write([]byte(`{"success": false}`)) - return - } - - workflowId = location[4] - triggerId = location[6] - } - - if len(workflowId) == 0 || len(triggerId) == 0 { - log.Printf("Ids can't be zero when deleting %s", workflowId) - resp.WriteHeader(401) - resp.Write([]byte(`{"success": false}`)) - return - } - - ctx := context.Background() - workflow, err := getWorkflow(ctx, workflowId) - if err != nil { - log.Printf("Failed getting the workflow locally (delete outlook): %s", err) - resp.WriteHeader(401) - resp.Write([]byte(`{"success": false}`)) - return - } - - user, err := handleApiAuthentication(resp, request) - if err != nil { - log.Printf("Api authentication failed in outlook deploy: %s", err) - resp.WriteHeader(401) - resp.Write([]byte(`{"success": false}`)) - return - } - - // FIXME - have a check for org etc too.. - if user.Id != workflow.Owner && user.Role != "admin" { - log.Printf("Wrong user (%s) for workflow %s when deploying outlook", user.Username, workflow.ID) - resp.WriteHeader(401) - resp.Write([]byte(`{"success": false}`)) - return - } - - // Check what kind of sub it is - err = handleOutlookSubRemoval(ctx, workflowId, triggerId) - if err != nil { - log.Printf("Failed sub removal: %s", err) - resp.WriteHeader(401) - resp.Write([]byte(`{"success": false}`)) - return - } - - resp.WriteHeader(200) - resp.Write([]byte(`{"success": true}`)) -} - -func removeOutlookSubscription(outlookClient *http.Client, subscriptionId string) error { - // DELETE https://graph.microsoft.com/v1.0/subscriptions/{id} - fullUrl := fmt.Sprintf("https://graph.microsoft.com/v1.0/subscriptions/%s", subscriptionId) - req, err := http.NewRequest( - "DELETE", - fullUrl, - nil, - ) - req.Header.Add("Content-Type", "application/json") - res, err := outlookClient.Do(req) - if err != nil { - log.Printf("Client: %s", err) - return err - } - - if res.StatusCode != 200 && res.StatusCode != 201 && res.StatusCode != 204 { - return errors.New(fmt.Sprintf("Bad status code when deleting subscription: %d", res.StatusCode)) - } - - body, err := ioutil.ReadAll(res.Body) - if err != nil { - log.Printf("Body: %s", err) - return err - } - - _ = body - - return nil -} - -// Remove AUTH -// Remove function -// Remove subscription -func handleOutlookSubRemoval(ctx context.Context, workflowId, triggerId string) error { - // 1. Get the auth for trigger - // 2. Stop the subscription - // 3. Remove the function - // 4. Remove the database entry for auth - trigger, err := getTriggerAuth(ctx, triggerId) - if err != nil { - log.Printf("Trigger auth %s doesn't exist - outlook sub removal.", triggerId) - return err - } - - url := fmt.Sprintf("https://shuffler.io") - outlookClient, _, err := getOutlookClient(ctx, "", trigger.OauthToken, url) - if err != nil { - log.Printf("Oauth client failure - triggerauth sub removal: %s", err) - return err - } - - notificationURL := fmt.Sprintf("https://%s-%s.cloudfunctions.net/outlooktrigger_%s", defaultLocation, gceProject, trigger.Id) - curSubscriptions, err := getOutlookSubscriptions(outlookClient) - if err == nil { - for _, sub := range curSubscriptions.Value { - if sub.NotificationURL == notificationURL { - log.Printf("Removing existing subscription %s", sub.Id) - removeOutlookSubscription(outlookClient, sub.Id) - } - } - } else { - log.Printf("Failed to get subscriptions - need to overwrite") - } - - // FIXME - not removing the function, as the trigger still exists - //err = removeOutlookTriggerFunction(triggerId) - //if err != nil { - // return err - //} - - return nil -} - func getOpenapi(resp http.ResponseWriter, request *http.Request) { cors := handleCors(resp, request) if cors { @@ -5971,8 +5836,58 @@ func handleCloudExecutionOnprem(workflowId, startNode, executionSource, executio func handleCloudJob(job CloudSyncJob) error { // May need authentication in all of these..? - log.Printf("Handle job with type %s and action %s", job.Type, job.Action) - if job.Type == "webhook" { + log.Printf("[INFO] Handle job with type %s and action %s", job.Type, job.Action) + if job.Type == "outlook" { + if job.Action == "execute" { + // FIXME: Get the email + ctx := context.Background() + maildata := MailData{} + err := json.Unmarshal([]byte(job.ThirdItem), &maildata) + if err != nil { + log.Printf("Maildata unmarshal error: %s", err) + return err + } + + hookId := job.Id + hook, err := getTriggerAuth(ctx, hookId) + if err != nil { + log.Printf("[INFO] Failed getting trigger %s (callback cloud): %s", hookId, err) + return err + } + + redirectDomain := "localhost:5001" + redirectUrl := fmt.Sprintf("http://%s/api/v1/triggers/outlook/register", redirectDomain) + outlookClient, _, err := getOutlookClient(ctx, "", hook.OauthToken, redirectUrl) + if err != nil { + log.Printf("Oauth client failure - triggerauth: %s", err) + return err + } + + emails, err := getOutlookEmail(outlookClient, maildata) + log.Printf("EMAILS: %d", len(emails)) + log.Printf("INSIDE GET OUTLOOK EMAIL!: %#v, %s", emails, err) + + //type FullEmail struct { + email := FullEmail{} + if len(emails) == 1 { + email = emails[0] + } + + emailBytes, err := json.Marshal(email) + if err != nil { + log.Printf("[INFO] Failed email marshaling: %s", err) + return err + } + + log.Printf("Should handle webhook for workflow %s with start node %s and data %s", job.PrimaryItemId, job.SecondaryItem, job.ThirdItem) + err = handleCloudExecutionOnprem(job.PrimaryItemId, job.SecondaryItem, "outlook", string(emailBytes)) + if err != nil { + log.Printf("Failed executing workflow from cloud outlook hook: %s", err) + } else { + log.Printf("Successfully executed workflow from cloud outlook hook!") + } + } + } else if job.Type == "webhook" { if job.Action == "execute" { log.Printf("Should handle webhook for workflow %s with start node %s and data %s", job.PrimaryItemId, job.SecondaryItem, job.ThirdItem) err := handleCloudExecutionOnprem(job.PrimaryItemId, job.SecondaryItem, "webhook", job.ThirdItem) @@ -6138,7 +6053,7 @@ func remoteOrgJobController(org Org, body []byte) error { } if len(responseData.Jobs) > 0 { - log.Printf("Remote JOB ret: %s", string(body)) + log.Printf("[INFO] Remote JOB ret: %s", string(body)) log.Printf("Got job with reason %s and %d job(s)", responseData.Reason, len(responseData.Jobs)) } @@ -7887,6 +7802,7 @@ func initHandlers() { r.HandleFunc("/api/v1/triggers/outlook/register", handleNewOutlookRegister).Methods("GET", "OPTIONS") r.HandleFunc("/api/v1/triggers/outlook/getFolders", handleGetOutlookFolders).Methods("GET", "OPTIONS") r.HandleFunc("/api/v1/triggers/outlook/{key}", handleGetSpecificTrigger).Methods("GET", "OPTIONS") + //r.HandleFunc("/api/v1/triggers/outlook/{key}/callback", handleOutlookCallback).Methods("POST", "OPTIONS") //r.HandleFunc("/api/v1/stats/{key}", handleGetSpecificStats).Methods("GET", "OPTIONS") http.Handle("/", r) diff --git a/backend/go-app/oauth2.go b/backend/go-app/oauth2.go index 598c7b27..04bb845c 100644 --- a/backend/go-app/oauth2.go +++ b/backend/go-app/oauth2.go @@ -9,6 +9,7 @@ import ( "io/ioutil" "log" "net/http" + "net/url" "strings" "time" @@ -16,6 +17,77 @@ import ( "golang.org/x/oauth2" ) +type FullEmail struct { + OdataContext string `json:"@odata.context"` + OdataEtag string `json:"@odata.etag"` + ID string `json:"id"` + Createddatetime time.Time `json:"createdDateTime"` + Lastmodifieddatetime time.Time `json:"lastModifiedDateTime"` + Changekey string `json:"changeKey"` + Categories []interface{} `json:"categories"` + Receiveddatetime time.Time `json:"receivedDateTime"` + Sentdatetime time.Time `json:"sentDateTime"` + Hasattachments bool `json:"hasAttachments"` + Internetmessageid string `json:"internetMessageId"` + Subject string `json:"subject"` + Bodypreview string `json:"bodyPreview"` + Importance string `json:"importance"` + Parentfolderid string `json:"parentFolderId"` + Conversationid string `json:"conversationId"` + Conversationindex string `json:"conversationIndex"` + Isdeliveryreceiptrequested interface{} `json:"isDeliveryReceiptRequested"` + Isreadreceiptrequested bool `json:"isReadReceiptRequested"` + Isread bool `json:"isRead"` + Isdraft bool `json:"isDraft"` + Weblink string `json:"webLink"` + Inferenceclassification string `json:"inferenceClassification"` + Body struct { + Contenttype string `json:"contentType"` + Content string `json:"content"` + } `json:"body"` + Sender struct { + Emailaddress struct { + Name string `json:"name"` + Address string `json:"address"` + } `json:"emailAddress"` + } `json:"sender"` + From struct { + Emailaddress struct { + Name string `json:"name"` + Address string `json:"address"` + } `json:"emailAddress"` + } `json:"from"` + Torecipients []struct { + Emailaddress struct { + Name string `json:"name"` + Address string `json:"address"` + } `json:"emailAddress"` + } `json:"toRecipients"` + Ccrecipients []interface{} `json:"ccRecipients"` + Bccrecipients []interface{} `json:"bccRecipients"` + Replyto []interface{} `json:"replyTo"` + Flag struct { + Flagstatus string `json:"flagStatus"` + } `json:"flag"` +} + +type MailData struct { + Value []struct { + Subscriptionid string `json:"subscriptionId"` + Subscriptionexpirationdatetime string `json:"subscriptionExpirationDateTime"` + 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"` + Clientstate string `json:"clientState"` + Tenantid string `json:"tenantId"` + } `json:"value"` +} + type OutlookProfile struct { OdataContext string `json:"@odata.context"` BusinessPhones []string `json:"businessPhones"` @@ -46,6 +118,50 @@ type OutlookFolders struct { Value []OutlookFolder `json:"value"` } +func getOutlookEmail(client *http.Client, maildata MailData) ([]FullEmail, error) { + //requestUrl := fmt.Sprintf("https://graph.microsoft.com/v1.0/users/ec03b4f2-fccf-4c35-b0eb-be85a0f5dd43/mailFolders") + + emails := []FullEmail{} + for _, email := range maildata.Value { + //messageId := email.Resourcedata.ID + //requestUrl := fmt.Sprintf("https://graph.microsoft.com/v1.0/me/%s", messageId) + requestUrl := fmt.Sprintf("https://graph.microsoft.com/v1.0/%s", email.Resource) + log.Printf("URL: %#v", requestUrl) + + ret, err := client.Get(requestUrl) + if err != nil { + log.Printf("[INFO] OutlookErr: %s", err) + return []FullEmail{}, err + } + + body, err := ioutil.ReadAll(ret.Body) + if err != nil { + log.Printf("[WARNING] Failed body decoding from outlook email") + return []FullEmail{}, err + } + + //type FullEmail struct { + log.Printf("[INFO] EMAIL Body: %s", string(body)) + log.Printf("[INFO] Status email: %d", ret.StatusCode) + if ret.StatusCode != 200 { + return []FullEmail{}, err + } + + //log.Printf("Body: %s", string(body)) + + parsedmail := FullEmail{} + err = json.Unmarshal(body, &parsedmail) + if err != nil { + log.Printf("[INFO] Email unmarshal error: %s", err) + return []FullEmail{}, err + } + + emails = append(emails, parsedmail) + } + + return emails, nil +} + func getOutlookFolders(client *http.Client) (OutlookFolders, error) { //requestUrl := fmt.Sprintf("https://graph.microsoft.com/v1.0/users/ec03b4f2-fccf-4c35-b0eb-be85a0f5dd43/mailFolders") requestUrl := fmt.Sprintf("https://graph.microsoft.com/v1.0/me/mailFolders") @@ -127,7 +243,7 @@ func handleNewOutlookRegister(resp http.ResponseWriter, request *http.Request) { url := fmt.Sprintf("http://%s%s", request.Host, request.URL.EscapedPath()) log.Println(url) ctx := context.Background() - client, accessToken, err := getOutlookClient(ctx, code, OauthToken{}, url) + _, accessToken, err := getOutlookClient(ctx, code, OauthToken{}, url) if err != nil { log.Printf("Oauth client failure - outlook register: %s", err) resp.WriteHeader(401) @@ -135,12 +251,15 @@ func handleNewOutlookRegister(resp http.ResponseWriter, request *http.Request) { } // This should be possible, and will also give the actual username - profile, err := getOutlookProfile(client) - if err != nil { - log.Printf("Outlook profile failure: %s", err) - resp.WriteHeader(401) - return - } + + /* + profile, err := getOutlookProfile(client) + if err != nil { + log.Printf("Outlook profile failure: %s", err) + resp.WriteHeader(401) + return + } + */ // This is a state workaround, which should really be for CSRF checks lol state := request.URL.Query().Get("state") @@ -168,7 +287,7 @@ func handleNewOutlookRegister(resp http.ResponseWriter, request *http.Request) { continue } - log.Printf("ITEM: %#v", itemsplit) + //log.Printf("ITEM: %#v", itemsplit) // Do something here if itemsplit[0] == "workflow_id" { @@ -177,6 +296,8 @@ func handleNewOutlookRegister(resp http.ResponseWriter, request *http.Request) { trigger.Id = itemsplit[1] } else if itemsplit[0] == "type" { trigger.Type = itemsplit[1] + } else if itemsplit[0] == "start" { + trigger.Start = itemsplit[1] } else if itemsplit[0] == "username" { trigger.Username = itemsplit[1] trigger.Owner = itemsplit[1] @@ -185,7 +306,12 @@ func handleNewOutlookRegister(resp http.ResponseWriter, request *http.Request) { } // THis is an override based on the user in oauth return - trigger.Username = profile.Mail + /* + if len(profile.Mail) > 0 { + trigger.Username = profile.Mail + } + */ + trigger.Code = code trigger.OauthToken = OauthToken{ AccessToken: accessToken.AccessToken, @@ -199,6 +325,7 @@ func handleNewOutlookRegister(resp http.ResponseWriter, request *http.Request) { log.Println(trigger.Id) log.Println(senderUser) log.Println(trigger.Type) + log.Printf("STARTNODE: %s", trigger.Start) log.Printf("[INFO] Attempting to set up outlook trigger for %s", senderUser) if trigger.WorkflowId == "" || trigger.Id == "" || senderUser == "" || trigger.Type == "" { log.Printf("[INFO] All oauth items need to contain data to register a new state") @@ -264,6 +391,7 @@ type OauthToken struct { RefreshToken string `json:"RefreshToken" datastore:"RefreshToken,noindex"` Expiry time.Time `json:"Expiry" datastore:"Expiry,noindex"` } + type TriggerAuth struct { Id string `json:"id" datastore:"id"` SubscriptionId string `json:"subscriptionId" datastore:"subscriptionId"` @@ -273,6 +401,7 @@ type TriggerAuth struct { Owner string `json:"owner" datastore:"owner"` Type string `json:"type" datastore:"type"` Code string `json:"code,omitempty" datastore:"code,noindex"` + Start string `json:"start" datastore:"start"` OauthToken OauthToken `json:"oauth_token,omitempty" datastore:"oauth_token"` } @@ -520,7 +649,7 @@ func createOutlookSub(resp http.ResponseWriter, request *http.Request) { return } - log.Println("Handle outlook subscription for trigger") + log.Println("[INFO] Handle outlook subscription for trigger") // Should already be authorized at this point, as the workflow is shared body, err := ioutil.ReadAll(request.Body) @@ -531,8 +660,6 @@ func createOutlookSub(resp http.ResponseWriter, request *http.Request) { return } - log.Println(string(body)) - // Based on the input data from frontend type CurTrigger struct { Name string `json:"name"` @@ -540,6 +667,7 @@ func createOutlookSub(resp http.ResponseWriter, request *http.Request) { ID string `json:"id"` } + //log.Println(string(body)) var curTrigger CurTrigger err = json.Unmarshal(body, &curTrigger) if err != nil { @@ -566,44 +694,74 @@ func createOutlookSub(resp http.ResponseWriter, request *http.Request) { // First - lets regenerate an oauth token for outlook.office.com from the original items trigger, err := getTriggerAuth(ctx, curTrigger.ID) if err != nil { - log.Printf("Trigger %s doesn't exist - outlook sub.", curTrigger.ID) + log.Printf("[INFO] Trigger %s doesn't exist - outlook sub.", curTrigger.ID) resp.WriteHeader(401) resp.Write([]byte(`{"success": false, "reason": ""}`)) return } // url doesn't really matter here - url := fmt.Sprintf("https://shuffler.io") + //url := fmt.Sprintf("https://shuffler.io") + redirectDomain := "localhost:5001" + url := fmt.Sprintf("http://%s/api/v1/triggers/outlook/register", redirectDomain) outlookClient, _, err := getOutlookClient(ctx, "", trigger.OauthToken, url) if err != nil { log.Printf("Oauth client failure - triggerauth: %s", err) + resp.Write([]byte(`{"success": false, "reason": ""}`)) resp.WriteHeader(401) return } // Location + - notificationURL := fmt.Sprintf("https://%s-%s.cloudfunctions.net/outlooktrigger_%s", defaultLocation, gceProject, curTrigger.ID) - log.Println(notificationURL) // This is here simply to let the function start // Usually takes 10 attempts minimum :O // 10 * 5 = 50 seconds. That's waaay too much :( - //notificationURL = "https://europe-west1-shuffler.cloudfunctions.net/outlooktrigger_e2ce43b0-997e-4980-9617-6eadbc68cf88" - //notificationURL = "https://de4fc12b.ngrok.io" + if runningEnvironment != "cloud" { + org, err := getOrg(ctx, user.ActiveOrg.Id) + if err != nil { + log.Printf("Failed finding org %s: %s", org.Id, err) + return + } + log.Printf("[INFO] Starting cloud configuration TO STOP trigger %s in org %s", trigger.Id, org.Id) + + action := CloudSyncJob{ + Type: "outlook", + Action: "start", + OrgId: org.Id, + PrimaryItemId: trigger.Id, + SecondaryItem: trigger.Start, + ThirdItem: trigger.WorkflowId, + } + + err = executeCloudAction(action, org.SyncConfig.Apikey) + if err != nil { + log.Printf("[INFO] Failed cloud action START outlook execution: %s", err) + resp.WriteHeader(401) + resp.Write([]byte(fmt.Sprintf(`{"success": false, "reason": "%s"}`, err))) + return + } else { + log.Printf("[INFO] Successfully set up cloud action trigger") + } + } else { + log.Printf("Should configure a running environment for CLOUD") + } + + notificationURL := fmt.Sprintf("%s/api/v1/hooks/webhook_%s", syncSubUrl, trigger.Id) curSubscriptions, err := getOutlookSubscriptions(outlookClient) if err == nil { for _, sub := range curSubscriptions.Value { if sub.NotificationURL == notificationURL { - log.Printf("Removing existing subscription %s", sub.Id) + log.Printf("[INFO] Removing existing subscription %s", sub.Id) removeOutlookSubscription(outlookClient, sub.Id) } } } else { - log.Printf("Failed to get subscriptions - need to overwrite") + log.Printf("[INFO] Failed to get subscriptions - need to overwrite") } - maxFails := 15 + maxFails := 5 failCnt := 0 log.Println(curTrigger.Folders) for { @@ -631,7 +789,7 @@ func createOutlookSub(resp http.ResponseWriter, request *http.Request) { break } - log.Printf("Successfully handled outlook subscription for trigger %s in workflow %s", curTrigger.ID, workflow.ID) + log.Printf("[INFO] Successfully handled outlook subscription for trigger %s in workflow %s", curTrigger.ID, workflow.ID) //log.Printf("%#v", user) resp.WriteHeader(200) @@ -686,19 +844,19 @@ func makeOutlookSubscription(client *http.Client, folderIds []string, notificati fullUrl := "https://graph.microsoft.com/v1.0/subscriptions" // FIXME - this expires rofl - t := time.Now().Local().Add(time.Minute * time.Duration(4300)) + t := time.Now().Local().Add(time.Minute * time.Duration(4200)) timeFormat := fmt.Sprintf("%d-%02d-%02dT%02d:%02d:%02d.0000000Z", t.Year(), t.Month(), t.Day(), t.Hour(), t.Minute(), t.Second()) - log.Println(timeFormat) resource := fmt.Sprintf("me/mailfolders('%s')/messages", strings.Join(folderIds, "','")) - log.Println(resource) + log.Printf("[INFO] Subscription resource to get(s): %s", resource) sub := Subscription{ ChangeType: "created", + ClientState: "Shuffle subscription", NotificationURL: notificationURL, ExpirationDateTime: timeFormat, - ClientState: "This is a test", Resource: resource, } + //ClientState: "This is a test", data, err := json.Marshal(sub) if err != nil { @@ -719,7 +877,7 @@ func makeOutlookSubscription(client *http.Client, folderIds []string, notificati return "", err } - log.Printf("Status: %d", res.StatusCode) + log.Printf("[INFO] Subscription Status: %d", res.StatusCode) body, err := ioutil.ReadAll(res.Body) if err != nil { log.Printf("Body: %s", err) @@ -739,3 +897,310 @@ func makeOutlookSubscription(client *http.Client, folderIds []string, notificati return newSub.Id, nil } + +// Basically the same as a webhook +func handleOutlookCallback(resp http.ResponseWriter, request *http.Request) { + path := strings.Split(request.URL.String(), "/") + if len(path) < 4 { + log.Printf("[INFO] Bad outlook callback URL: %s", path) + resp.WriteHeader(403) + resp.Write([]byte(`{"success": false}`)) + return + } + + // 1. Get config with hookId + //fmt.Sprintf("%s/api/v1/hooks/%s", callbackUrl, hookId) + ctx := context.Background() + location := strings.Split(request.URL.String(), "/") + + var hookId string + if location[1] == "api" { + if len(location) <= 4 { + resp.WriteHeader(401) + resp.Write([]byte(`{"success": false}`)) + return + } + + hookId = location[5] + } + + // ID: webhook_ + if len(hookId) != 36 { + log.Printf("[WARNING] Bad hook ID: %s (%d)", hookId, len(hookId)) + resp.WriteHeader(401) + resp.Write([]byte(`{"success": false, "message": "ID not valid"}`)) + return + } + + //func getTriggerAuth(ctx context.Context, id string) (*TriggerAuth, error) { + hook, err := getTriggerAuth(ctx, hookId) + if err != nil { + log.Printf("[INFO] Failed getting trigger %s (callback): %s", hookId, err) + resp.WriteHeader(401) + resp.Write([]byte(`{"success": false}`)) + return + } + + body, err := ioutil.ReadAll(request.Body) + if err != nil { + log.Printf("[INFO] Body data error: %s", err) + resp.WriteHeader(401) + resp.Write([]byte(`{"success": false}`)) + return + } + + //log.Printf("[INFO] BODY: %s. Len: %d", string(body), len(body)) + //key, ok := request.URL.Query()["validationToken"] + //if ok { + //} + token := request.URL.Query().Get("validationToken") + if len(body) == 0 && len(token) > 0 { + log.Printf("[INFO] Should handle trigger token %s", token) + resp.WriteHeader(200) + resp.Write([]byte(string(token))) + return + } + + // 1. Take the body and parse data -> Get the email itself + + maildata := MailData{} + err = json.Unmarshal(body, &maildata) + if err != nil { + log.Printf("Maildata unmarshal error: %s", err) + resp.WriteHeader(401) + resp.Write([]byte(`{"success": false}`)) + return + } + + redirectDomain := "localhost:5001" + redirectUrl := fmt.Sprintf("http://%s/api/v1/triggers/outlook/register", redirectDomain) + outlookClient, _, err := getOutlookClient(ctx, "", hook.OauthToken, redirectUrl) + if err != nil { + log.Printf("Oauth client failure - triggerauth: %s", err) + resp.WriteHeader(401) + return + } + + emails, err := getOutlookEmail(outlookClient, maildata) + log.Printf("EMAILS: %d", len(emails)) + log.Printf("INSIDE GET OUTLOOK EMAIL!: %#v, %s", emails, err) + + //type FullEmail struct { + email := FullEmail{} + if len(emails) == 1 { + email = emails[0] + } + + emailBytes, err := json.Marshal(email) + if err != nil { + log.Printf("[INFO] Failed email marshaling: %s", err) + resp.WriteHeader(401) + resp.Write([]byte(`{"success": false}`)) + return + } + + type ExecutionStruct struct { + Start string `json:"start"` + ExecutionSource string `json:"execution_source"` + ExecutionArgument string `json:"execution_argument"` + } + + newBody := ExecutionStruct{ + Start: hook.Start, + ExecutionSource: "outlook", + ExecutionArgument: string(emailBytes), + } + + b, err := json.Marshal(newBody) + if err != nil { + log.Printf("[INFO] Failed newBody marshaling: %s", err) + resp.WriteHeader(401) + resp.Write([]byte(`{"success": false}`)) + return + } + + baseUrl := &url.URL{} + newRequest := &http.Request{ + URL: baseUrl, + Method: "POST", + Body: ioutil.NopCloser(bytes.NewReader(b)), + } + + workflow := Workflow{ + ID: "", + } + + // OrgId: activeOrgs[0].Id, + workflowExecution, executionResp, err := handleExecution(hook.WorkflowId, workflow, newRequest) + if err == nil { + resp.WriteHeader(200) + resp.Write([]byte(fmt.Sprintf(`{"success": true, "execution_id": "%s", "authorization": "%s"}`, workflowExecution.ExecutionId, workflowExecution.Authorization))) + return + } + + resp.WriteHeader(500) + resp.Write([]byte(fmt.Sprintf(`{"success": false, "reason": "%s"}`, executionResp))) +} + +func removeOutlookSubscription(outlookClient *http.Client, subscriptionId string) error { + // DELETE https://graph.microsoft.com/v1.0/subscriptions/{id} + fullUrl := fmt.Sprintf("https://graph.microsoft.com/v1.0/subscriptions/%s", subscriptionId) + req, err := http.NewRequest( + "DELETE", + fullUrl, + nil, + ) + req.Header.Add("Content-Type", "application/json") + res, err := outlookClient.Do(req) + if err != nil { + log.Printf("Client: %s", err) + return err + } + + if res.StatusCode != 200 && res.StatusCode != 201 && res.StatusCode != 204 { + return errors.New(fmt.Sprintf("Bad status code when deleting subscription: %d", res.StatusCode)) + } + + body, err := ioutil.ReadAll(res.Body) + if err != nil { + log.Printf("Body: %s", err) + return err + } + + _ = body + + return nil +} + +// Remove AUTH +// Remove function +// Remove subscription +func handleOutlookSubRemoval(ctx context.Context, user User, workflowId, triggerId string) error { + // 1. Get the auth for trigger + // 2. Stop the subscription + // 3. Remove the function + // 4. Remove the database entry for auth + trigger, err := getTriggerAuth(ctx, triggerId) + if err != nil { + log.Printf("Trigger auth %s doesn't exist - outlook sub removal.", triggerId) + return err + } + + if runningEnvironment != "cloud" { + log.Printf("[INFO] SHOULD STOP OUTLOOK SUB ONPREM SYNC WITH CLOUD") + org, err := getOrg(ctx, user.ActiveOrg.Id) + if err != nil { + log.Printf("[INFO] Failed finding org %s during outlook removal: %s", org.Id, err) + return err + } + + log.Printf("[INFO] Stopping cloud configuration for trigger %s in org %s", trigger.Id, org.Id) + action := CloudSyncJob{ + Type: "outlook", + Action: "stop", + OrgId: org.Id, + PrimaryItemId: trigger.Id, + SecondaryItem: trigger.Start, + ThirdItem: trigger.WorkflowId, + } + + err = executeCloudAction(action, org.SyncConfig.Apikey) + if err != nil { + log.Printf("[INFO] Failed cloud action STOP outlook execution: %s", err) + return err + } else { + log.Printf("[INFO] Successfully set STOPPED outlook execution trigger") + } + } else { + log.Printf("SHOULD STOP OUTLOOK SUB IN CLOUD") + } + + // Actually delete the thing + redirectDomain := "localhost:5001" + url := fmt.Sprintf("http://%s/api/v1/triggers/outlook/register", redirectDomain) + outlookClient, _, err := getOutlookClient(ctx, "", trigger.OauthToken, url) + if err != nil { + log.Printf("[WARNING] Oauth client failure - outlook folders: %s", err) + return err + } + notificationURL := fmt.Sprintf("%s/api/v1/hooks/webhook_%s", syncSubUrl, trigger.Id) + curSubscriptions, err := getOutlookSubscriptions(outlookClient) + if err == nil { + for _, sub := range curSubscriptions.Value { + if sub.NotificationURL == notificationURL { + log.Printf("[INFO] Removing subscription %s from o365", sub.Id) + removeOutlookSubscription(outlookClient, sub.Id) + } + } + } else { + log.Printf("Failed to get subscriptions - need to overwrite") + } + + return nil +} + +func handleDeleteOutlookSub(resp http.ResponseWriter, request *http.Request) { + cors := handleCors(resp, request) + if cors { + return + } + + location := strings.Split(request.URL.String(), "/") + + var workflowId string + var triggerId string + if location[1] == "api" { + if len(location) <= 6 { + resp.WriteHeader(401) + resp.Write([]byte(`{"success": false}`)) + return + } + + workflowId = location[4] + triggerId = location[6] + } + + if len(workflowId) == 0 || len(triggerId) == 0 { + log.Printf("Ids can't be zero when deleting %s", workflowId) + resp.WriteHeader(401) + resp.Write([]byte(`{"success": false}`)) + return + } + + ctx := context.Background() + workflow, err := getWorkflow(ctx, workflowId) + if err != nil { + log.Printf("Failed getting the workflow locally (delete outlook): %s", err) + resp.WriteHeader(401) + resp.Write([]byte(`{"success": false}`)) + return + } + + user, err := handleApiAuthentication(resp, request) + if err != nil { + log.Printf("Api authentication failed in outlook deploy: %s", err) + resp.WriteHeader(401) + resp.Write([]byte(`{"success": false}`)) + return + } + + // FIXME - have a check for org etc too.. + if user.Id != workflow.Owner && user.Role != "admin" { + log.Printf("Wrong user (%s) for workflow %s when deploying outlook", user.Username, workflow.ID) + resp.WriteHeader(401) + resp.Write([]byte(`{"success": false}`)) + return + } + + // Check what kind of sub it is + err = handleOutlookSubRemoval(ctx, user, workflowId, triggerId) + if err != nil { + log.Printf("Failed sub removal: %s", err) + resp.WriteHeader(401) + resp.Write([]byte(`{"success": false}`)) + return + } + + resp.WriteHeader(200) + resp.Write([]byte(`{"success": true}`)) +} diff --git a/backend/go-app/walkoff.go b/backend/go-app/walkoff.go index ac3265de..b89d079d 100644 --- a/backend/go-app/walkoff.go +++ b/backend/go-app/walkoff.go @@ -2034,7 +2034,7 @@ func deleteWorkflow(resp http.ResponseWriter, request *http.Request) { // log.Printf("Failed to delete webhook: %s", err) //} } else if item.TriggerType == "EMAIL" { - err = handleOutlookSubRemoval(ctx, workflow.ID, item.ID) + err = handleOutlookSubRemoval(ctx, user, workflow.ID, item.ID) if err != nil { log.Printf("Failed to delete email sub: %s", err) } @@ -2224,7 +2224,7 @@ func saveWorkflow(resp http.ResponseWriter, request *http.Request) { return } - log.Printf("PRE BODY") + //log.Printf("PRE BODY") body, err := ioutil.ReadAll(request.Body) if err != nil { log.Printf("Failed hook unmarshaling: %s", err) @@ -2268,7 +2268,7 @@ func saveWorkflow(resp http.ResponseWriter, request *http.Request) { allNodes := []string{} workflow.Categories = Categories{} - log.Printf("PRE APPS") + //log.Printf("PRE APPS") workflowapps, apperr := getAllWorkflowApps(ctx, 500) //log.Printf("Action: %#v", action.Authentication) @@ -2553,7 +2553,7 @@ func saveWorkflow(resp http.ResponseWriter, request *http.Request) { workflow.Triggers = newTriggers - log.Printf("PRE VARIABLES") + //log.Printf("PRE VARIABLES") for _, variable := range workflow.WorkflowVariables { if len(variable.Value) == 0 { log.Printf("Can't have an empty variable: %s", variable.Name) @@ -2601,7 +2601,7 @@ func saveWorkflow(resp http.ResponseWriter, request *http.Request) { } // FIXME - append all nodes (actions, triggers etc) to one single array here - log.Printf("PRE VARIABLES") + //log.Printf("PRE VARIABLES") if len(foundNodes) != len(allNodes) || len(workflow.Actions) <= 0 { // This shit takes a few seconds lol if !workflow.IsValid { @@ -2675,7 +2675,7 @@ func saveWorkflow(resp http.ResponseWriter, request *http.Request) { } // Check every app action and param to see whether they exist - log.Printf("PRE ACTIONS 2") + //log.Printf("PRE ACTIONS 2") newActions = []Action{} for _, action := range workflow.Actions { reservedApps := []string{ diff --git a/frontend/src/views/AngularWorkflow.jsx b/frontend/src/views/AngularWorkflow.jsx index bde4fff9..321d664f 100644 --- a/frontend/src/views/AngularWorkflow.jsx +++ b/frontend/src/views/AngularWorkflow.jsx @@ -2221,7 +2221,7 @@ const AngularWorkflow = (props) => { "description": "Add your email provider", "trigger_type": "EMAIL", "errors": null, - "is_valid": true ? false : cloudSyncEnabled, + "is_valid": cloudSyncEnabled || isCloud ? true : false, "label": "Email", "environment": "cloud", "large_image": 'data:image/png;base64,/9j/4AAQSkZJRgABAQAAAQABAAD/4QAYRXhpZgAASUkqAAgAAAAAAAAAAAAAAP/hAytodHRwOi8vbnMuYWRvYmUuY29tL3hhcC8xLjAvADw/eHBhY2tldCBiZWdpbj0i77u/IiBpZD0iVzVNME1wQ2VoaUh6cmVTek5UY3prYzlkIj8+IDx4OnhtcG1ldGEgeG1sbnM6eD0iYWRvYmU6bnM6bWV0YS8iIHg6eG1wdGs9IkFkb2JlIFhNUCBDb3JlIDUuMy1jMDExIDY2LjE0NTY2MSwgMjAxMi8wMi8wNi0xNDo1NjoyNyAgICAgICAgIj4gPHJkZjpSREYgeG1sbnM6cmRmPSJodHRwOi8vd3d3LnczLm9yZy8xOTk5LzAyLzIyLXJkZi1zeW50YXgtbnMjIj4gPHJkZjpEZXNjcmlwdGlvbiByZGY6YWJvdXQ9IiIgeG1sbnM6eG1wPSJodHRwOi8vbnMuYWRvYmUuY29tL3hhcC8xLjAvIiB4bWxuczp4bXBNTT0iaHR0cDovL25zLmFkb2JlLmNvbS94YXAvMS4wL21tLyIgeG1sbnM6c3RSZWY9Imh0dHA6Ly9ucy5hZG9iZS5jb20veGFwLzEuMC9zVHlwZS9SZXNvdXJjZVJlZiMiIHhtcDpDcmVhdG9yVG9vbD0iQWRvYmUgUGhvdG9zaG9wIENTNiAoV2luZG93cykiIHhtcE1NOkluc3RhbmNlSUQ9InhtcC5paWQ6Nzg4QTJBMjVEMDI1MTFFN0EwQUVDODc5QjYyQkFCMUQiIHhtcE1NOkRvY3VtZW50SUQ9InhtcC5kaWQ6Nzg4QTJBMjZEMDI1MTFFN0EwQUVDODc5QjYyQkFCMUQiPiA8eG1wTU06RGVyaXZlZEZyb20gc3RSZWY6aW5zdGFuY2VJRD0ieG1wLmlpZDo3ODhBMkEyM0QwMjUxMUU3QTBBRUM4NzlCNjJCQUIxRCIgc3RSZWY6ZG9jdW1lbnRJRD0ieG1wLmRpZDo3ODhBMkEyNEQwMjUxMUU3QTBBRUM4NzlCNjJCQUIxRCIvPiA8L3JkZjpEZXNjcmlwdGlvbj4gPC9yZGY6UkRGPiA8L3g6eG1wbWV0YT4gPD94cGFja2V0IGVuZD0iciI/Pv/bAEMAAwICAgICAwICAgMDAwMEBgQEBAQECAYGBQYJCAoKCQgJCQoM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', @@ -4990,50 +4990,59 @@ const AngularWorkflow = (props) => { } const outlookButton = - @@ -5044,20 +5053,25 @@ const AngularWorkflow = (props) => { if (triggerAuthentication.type === "outlook") { triggerInfo =
-
-
-
- Login: -
-
- {outlookButton} + {selectedTrigger.status === "running" ? null : + +
+
+
+ Login +
+
+ {outlookButton} + + } - {triggerFolders === undefined || triggerFolders === null ? null : + {triggerFolders === undefined || triggerFolders === null ? + null :
- Folders: (hold CTRL to select multiple) + Select a folder