├── go.sum ├── .go-version ├── go.mod ├── .buildkite ├── scripts │ ├── gofmt.sh │ ├── pre-install-command.sh │ ├── prepare-report.sh │ └── test.sh ├── pull-requests.json ├── pipeline.yml └── hooks │ └── post-checkout ├── .gitignore ├── example_test.go ├── examples └── main.go ├── LICENSE ├── README.md ├── catalog-info.yaml └── bayeux.go /go.sum: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /.go-version: -------------------------------------------------------------------------------- 1 | 1.17.8 2 | -------------------------------------------------------------------------------- /go.mod: -------------------------------------------------------------------------------- 1 | module github.com/elastic/bayeux 2 | 3 | go 1.17 4 | -------------------------------------------------------------------------------- /.buildkite/scripts/gofmt.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | set -euo pipefail 3 | 4 | if [ -z "$(gofmt -d .)" ]; then 5 | true 6 | else 7 | gofmt -d . && false 8 | fi 9 | -------------------------------------------------------------------------------- /.buildkite/scripts/pre-install-command.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | set -euo pipefail 3 | 4 | add_bin_path(){ 5 | mkdir -p "${WORKSPACE}/bin" 6 | export PATH="${WORKSPACE}/bin:${PATH}" 7 | } 8 | 9 | with_go_junit_report() { 10 | go get -v -u github.com/jstemmer/go-junit-report 11 | } 12 | 13 | WORKSPACE=${WORKSPACE:-"$(pwd)"} 14 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Compiled Object files, Static and Dynamic libs (Shared Objects) 2 | *.o 3 | *.a 4 | *.so 5 | 6 | # Folders 7 | _obj 8 | _test 9 | 10 | # Architecture specific extensions/prefixes 11 | *.[568vq] 12 | [568vq].out 13 | 14 | *.cgo1.go 15 | *.cgo2.c 16 | _cgo_defun.c 17 | _cgo_gotypes.go 18 | _cgo_export.* 19 | 20 | _testmain.go 21 | 22 | *.exe 23 | *.test 24 | *.prof 25 | -------------------------------------------------------------------------------- /.buildkite/scripts/prepare-report.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | set -euo pipefail 4 | 5 | echo "--- Pre install" 6 | source .buildkite/scripts/pre-install-command.sh 7 | add_bin_path 8 | with_go_junit_report 9 | 10 | # Create Junit report for junit annotation plugin 11 | build_folder="/build" 12 | mkdir $build_folder 13 | buildkite-agent artifact download "build/test-report-*" "${build_folder}" --step test-matrix 14 | find ./build -name "test-report-*" -exec sh -c 'f=$1; go-junit-report < ${f} >> ${f}.xml' shell {} \; 15 | -------------------------------------------------------------------------------- /.buildkite/scripts/test.sh: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | set -euo pipefail 3 | 4 | echo "--- Prepare enviroment" 5 | source .buildkite/scripts/pre-install-command.sh 6 | add_bin_path 7 | with_go_junit_report 8 | 9 | echo "--- Run the tests" 10 | export OUT_FILE="build/test-report-${GO_VERSION}" 11 | mkdir -p build 12 | set +e 13 | go test -v -race ./... > "${OUT_FILE}" 14 | status=$? 15 | set -e 16 | 17 | # Buildkite collapse logs under --- symbols 18 | # need to change --- to anything else or switch off collapsing (note: not available at the moment of this commit) 19 | awk '{gsub("---", "----"); print }' ${OUT_FILE} 20 | 21 | exit ${status} 22 | -------------------------------------------------------------------------------- /.buildkite/pull-requests.json: -------------------------------------------------------------------------------- 1 | { 2 | "jobs": [ 3 | { 4 | "enabled": true, 5 | "pipelineSlug": "bayeux", 6 | "allow_org_users": true, 7 | "allowed_repo_permissions": ["admin", "write"], 8 | "allowed_list": [ ], 9 | "set_commit_status": true, 10 | "build_on_commit": true, 11 | "build_on_comment": true, 12 | "trigger_comment_regex": "^(?:(?:buildkite\\W+)?(?:build|test)\\W+(?:this|it))|^/test$", 13 | "always_trigger_comment_regex": "^(?:(?:buildkite\\W+)?(?:build|test)\\W+(?:this|it))|^/test$", 14 | "skip_ci_labels": [ ], 15 | "skip_target_branches": [ ], 16 | "skip_ci_on_only_changed": [ ], 17 | "always_require_ci_on_changed": [ ] 18 | } 19 | ] 20 | } 21 | -------------------------------------------------------------------------------- /example_test.go: -------------------------------------------------------------------------------- 1 | package bayeux 2 | 3 | import ( 4 | "context" 5 | "fmt" 6 | ) 7 | 8 | func Example() { 9 | ctx := context.Background() 10 | out := make(chan MaybeMsg) 11 | replay := "-1" 12 | b := Bayeux{} 13 | // Create a variable of type AuthenticationParameters and set the values 14 | var ap AuthenticationParameters 15 | ap.ClientID = "3MVG9pRsdbjsbdjfm1I.fz3f7zBuH4xdKCJcM9B5XLgxXh2AFTmQmr8JMn1vsadjsadjjsadakd_C" 16 | ap.ClientSecret = "E9FE118633BC7SGDADUHUE81F19C1D4529D09CB7231754AD2F2CA668400619" 17 | ap.Username = "salesforce.user@email.com" 18 | ap.Password = "foobar" 19 | ap.TokenURL = "https://login.salesforce.com/services/oauth2/token" 20 | creds, _ := GetSalesforceCredentials(ap) 21 | c := b.Channel(ctx, out, replay, *creds, "channel") 22 | for { 23 | select { 24 | case e := <-c: 25 | fmt.Printf("TriggerEvent Received: %+v", e) 26 | } 27 | } 28 | } 29 | -------------------------------------------------------------------------------- /examples/main.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "context" 5 | "fmt" 6 | 7 | bay "github.com/elastic/bayeux" 8 | ) 9 | 10 | func Example() { 11 | ctx := context.Background() 12 | out := make(chan bay.MaybeMsg) 13 | b := bay.Bayeux{} 14 | var ap bay.AuthenticationParameters 15 | ap.ClientID = "3MVG9pRsdbjsbdjfm1I.fz3f7zBuH4xdKCJcM9B5XLgxXh2AFTmQmr8JMn1vsadjsadjjsadakd_C" 16 | ap.ClientSecret = "E9FE118633BC7SGDADUHUE81F19C1D4529D09CB7231754AD2F2CA668400619" 17 | ap.Username = "salesforce.user@email.com" 18 | ap.Password = "foobar" 19 | ap.TokenURL = "https://login.salesforce.com/services/oauth2/token" 20 | creds, _ := bay.GetSalesforceCredentials(ap) 21 | replay := "-1" 22 | c := b.Channel(ctx, out, replay, *creds, "channel") 23 | for { 24 | select { 25 | case e := <-c: 26 | fmt.Printf("TriggerEvent Received: %+v", e) 27 | } 28 | } 29 | } 30 | 31 | func main() { 32 | Example() 33 | } 34 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2017 Zander Hill 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # bayeux 2 | Bayeux Client Protocol implemented in Golang (as specified by Salesforce Realtime API) 3 | 4 | Fork from zph/bayeux: 5 | 6 | Changes to accept both 'payload' and 'sobject'. 7 | All the channels (user-created or generic) should be accepted. 8 | Make it so user will pass chan from main function then they can close it anytime. 9 | User can mention replay mechanism from Channel method. 10 | 11 | # Usage 12 | See `examples/main.go` 13 | ```golang 14 | package main 15 | 16 | import ( 17 | "fmt" 18 | 19 | bay "github.com/elastic/bayeux" 20 | ) 21 | 22 | func Example() { 23 | out := make(chan bay.TriggerEvent) 24 | b := bay.Bayeux{} 25 | creds := bay.GetSalesforceCredentials() 26 | replay := "-1" 27 | c := b.Channel(out, replay, creds, "channel") 28 | for { 29 | select { 30 | case e := <-c: 31 | fmt.Printf("TriggerEvent Received: %+v", e) 32 | } 33 | } 34 | } 35 | 36 | func main() { 37 | Example() 38 | } 39 | ``` 40 | 41 | See annotations in code for cases where the Salesforce implementation of Bayeux seems to differ from official spec. 42 | 43 | Salesforce documentation on Realtime API: https://resources.docs.salesforce.com/sfdc/pdf/api_streaming.pdf 44 | 45 | # Stability 46 | 47 | This code or variant thereof has been steadily running in production since Nov 2016. 48 | 49 | The API is very new. So only a few functions are exposed publicly and these can be expected to remain stable until 12/2018, pending massive changes to Salesforce API. Reasonable attempts will be made to maintain API past that deadline. 50 | -------------------------------------------------------------------------------- /.buildkite/pipeline.yml: -------------------------------------------------------------------------------- 1 | # yaml-language-server: $schema=https://raw.githubusercontent.com/buildkite/pipeline-schema/main/schema.json 2 | 3 | env: 4 | GO111MODULE: "on" 5 | 6 | steps: 7 | - label: ":linux: gofmt" 8 | command: 9 | - ".buildkite/scripts/gofmt.sh" 10 | agents: 11 | image: golang:1.20 12 | cpu: "8" 13 | memory: "4G" 14 | 15 | - label: ":linux: Test matrix. Go {{matrix.go_version}}" 16 | key: test-matrix 17 | matrix: 18 | setup: 19 | go_version: 20 | - "1.20" 21 | command: 22 | - ".buildkite/scripts/test.sh" 23 | env: 24 | GO_VERSION: "{{matrix.go_version}}" 25 | agents: 26 | image: golang:{{matrix.go_version}} 27 | cpu: "8" 28 | memory: "4G" 29 | artifact_paths: 30 | - "build/test-report-*" 31 | 32 | - label: ":buildkite: Prepare reports" 33 | key: prepare-report 34 | command: 35 | - ".buildkite/scripts/prepare-report.sh" 36 | agents: 37 | image: golang:1.20 38 | cpu: "8" 39 | memory: "4G" 40 | artifact_paths: 41 | - "build/test-report-*.xml" 42 | depends_on: 43 | - step: "test-matrix" 44 | allow_failure: true 45 | 46 | - label: ":junit: Junit annotate" 47 | plugins: 48 | - junit-annotate#v2.4.1: 49 | artifacts: "build/test-report-*.xml" 50 | fail-build-on-error: true 51 | agents: 52 | provider: "gcp" #junit plugin requires docker 53 | depends_on: 54 | - step: "prepare-report" 55 | allow_failure: true 56 | -------------------------------------------------------------------------------- /.buildkite/hooks/post-checkout: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | set -euo pipefail 4 | 5 | checkout_merge() { 6 | local target_branch=$1 7 | local pr_commit=$2 8 | local merge_branch=$3 9 | 10 | if [[ -z "${target_branch}" ]]; then 11 | echo "No pull request target branch" 12 | exit 1 13 | fi 14 | 15 | git fetch -v origin "${target_branch}" 16 | git checkout FETCH_HEAD 17 | echo "Current branch: $(git rev-parse --abbrev-ref HEAD)" 18 | 19 | # create temporal branch to merge the PR with the target branch 20 | git checkout -b ${merge_branch} 21 | echo "New branch created: $(git rev-parse --abbrev-ref HEAD)" 22 | 23 | # set author identity so it can be run git merge 24 | git config user.name "github-merged-pr-post-checkout" 25 | git config user.email "auto-merge@buildkite" 26 | 27 | git merge --no-edit "${BUILDKITE_COMMIT}" || { 28 | local merge_result=$? 29 | echo "Merge failed: ${merge_result}" 30 | git merge --abort 31 | exit ${merge_result} 32 | } 33 | } 34 | 35 | pull_request="${BUILDKITE_PULL_REQUEST:-false}" 36 | 37 | if [[ "${pull_request}" == "false" ]]; then 38 | echo "Not a pull request, skipping" 39 | exit 0 40 | fi 41 | 42 | TARGET_BRANCH="${BUILDKITE_PULL_REQUEST_BASE_BRANCH:-master}" 43 | PR_COMMIT="${BUILDKITE_COMMIT}" 44 | PR_ID=${BUILDKITE_PULL_REQUEST} 45 | MERGE_BRANCH="pr_merge_${PR_ID}" 46 | 47 | checkout_merge "${TARGET_BRANCH}" "${PR_COMMIT}" "${MERGE_BRANCH}" 48 | 49 | echo "Commit information" 50 | git --no-pager log --format=%B -n 1 51 | 52 | # Ensure buildkite groups are rendered 53 | echo "" 54 | -------------------------------------------------------------------------------- /catalog-info.yaml: -------------------------------------------------------------------------------- 1 | # Declare a Backstage Component that represents your application. 2 | --- 3 | # yaml-language-server: $schema=https://json.schemastore.org/catalog-info.json 4 | apiVersion: backstage.io/v1alpha1 5 | kind: Component 6 | metadata: 7 | name: bayeux 8 | description: bayeux - Bayeux Client Protocol as specified by Salesforce Realtime API 9 | 10 | spec: 11 | type: tool 12 | owner: group:ingest-fp 13 | system: platform-ingest 14 | lifecycle: production 15 | 16 | 17 | --- 18 | # yaml-language-server: $schema=https://gist.githubusercontent.com/elasticmachine/988b80dae436cafea07d9a4a460a011d/raw/e57ee3bed7a6f73077a3f55a38e76e40ec87a7cf/rre.schema.json 19 | apiVersion: backstage.io/v1alpha1 20 | kind: Resource 21 | metadata: 22 | name: buildkite-pipeline-bayeux 23 | description: Buildkite pipeline for the bayeux project 24 | links: 25 | - title: Pipeline 26 | url: https://buildkite.com/elastic/bayeux 27 | 28 | spec: 29 | type: buildkite-pipeline 30 | owner: group:ingest-fp 31 | system: platform-ingest 32 | implementation: 33 | apiVersion: buildkite.elastic.dev/v1 34 | kind: Pipeline 35 | metadata: 36 | name: bayeux 37 | description: Buildkite pipeline for the bayeux project 38 | spec: 39 | repository: elastic/bayeux 40 | pipeline_file: ".buildkite/pipeline.yml" 41 | branch_configuration: "master" 42 | provider_settings: 43 | build_pull_request_forks: false 44 | build_pull_requests: true # requires filter_enabled and filter_condition settings as below when used with buildkite-pr-bot 45 | build_tags: true 46 | filter_enabled: true 47 | filter_condition: >- 48 | build.pull_request.id == null || (build.creator.name == 'elasticmachine' && build.pull_request.id != null) 49 | env: 50 | ELASTIC_SLACK_NOTIFICATIONS_ENABLED: 'true' 51 | SLACK_NOTIFICATIONS_CHANNEL: '#ingest-notifications' 52 | SLACK_NOTIFICATIONS_ALL_BRANCHES: 'false' 53 | SLACK_NOTIFICATIONS_ON_SUCCESS: 'false' 54 | teams: 55 | ingest-fp: 56 | access_level: MANAGE_BUILD_AND_READ 57 | everyone: 58 | access_level: READ_ONLY 59 | -------------------------------------------------------------------------------- /bayeux.go: -------------------------------------------------------------------------------- 1 | package bayeux 2 | 3 | import ( 4 | "bytes" 5 | "context" 6 | "encoding/json" 7 | "errors" 8 | "fmt" 9 | "io" 10 | "io/ioutil" 11 | "log" 12 | "net/http" 13 | "net/url" 14 | "os" 15 | "sync" 16 | "time" 17 | ) 18 | 19 | type MaybeMsg struct { 20 | Err error 21 | Msg TriggerEvent 22 | } 23 | 24 | func (e MaybeMsg) Failed() bool { return e.Err != nil } 25 | 26 | func (e MaybeMsg) Error() string { return e.Err.Error() } 27 | 28 | // TriggerEvent describes an event received from Bayeaux Endpoint 29 | type TriggerEvent struct { 30 | ClientID string `json:"clientId"` 31 | Data struct { 32 | Event struct { 33 | CreatedDate time.Time `json:"createdDate"` 34 | ReplayID int `json:"replayId"` 35 | Type string `json:"type"` 36 | } `json:"event"` 37 | Object json.RawMessage `json:"sobject"` 38 | Payload json.RawMessage `json:"payload"` 39 | } `json:"data,omitempty"` 40 | Channel string `json:"channel"` 41 | Successful bool `json:"successful,omitempty"` 42 | } 43 | 44 | // Status is the state of success and subscribed channels 45 | type status struct { 46 | connected bool 47 | clientID string 48 | channels []string 49 | connectCount int 50 | } 51 | 52 | func (st *status) connect() { 53 | st.connectCount++ 54 | } 55 | 56 | func (st *status) disconnect() { 57 | st.connectCount-- 58 | } 59 | 60 | type BayeuxHandshake []struct { 61 | Ext struct { 62 | Replay bool `json:"replay"` 63 | } `json:"ext"` 64 | MinimumVersion string `json:"minimumVersion"` 65 | ClientID string `json:"clientId"` 66 | SupportedConnectionTypes []string `json:"supportedConnectionTypes"` 67 | Channel string `json:"channel"` 68 | Version string `json:"version"` 69 | Successful bool `json:"successful"` 70 | } 71 | 72 | type Subscription struct { 73 | ClientID string `json:"clientId"` 74 | Channel string `json:"channel"` 75 | Subscription string `json:"subscription"` 76 | Successful bool `json:"successful"` 77 | } 78 | 79 | type Credentials struct { 80 | AccessToken string `json:"access_token"` 81 | InstanceURL string `json:"instance_url"` 82 | IssuedAt int 83 | ID string 84 | TokenType string `json:"token_type"` 85 | Signature string 86 | } 87 | 88 | func (c Credentials) bayeuxUrl() string { 89 | return c.InstanceURL + "/cometd/38.0" 90 | } 91 | 92 | type clientIDAndCookies struct { 93 | clientID string 94 | cookies []*http.Cookie 95 | } 96 | 97 | type AuthenticationParameters struct { 98 | ClientID string // consumer key from Salesforce (e.g. 3MVG9pRsdbjsbdjfm1I.fz3f7zBuH4xdKCJcM9B5XLgxXh2AFTmQmr8JMn1vsadjsadjjsadakd_C) 99 | ClientSecret string // consumer secret from Salesforce (e.g. E9FE118633BC7SGDADUHUE81F19C1D4529D09CB7231754AD2F2CA668400619) 100 | Username string // Salesforce user email (e.g. salesforce.user@email.com) 101 | Password string // Salesforce password 102 | TokenURL string // Salesforce token endpoint (e.g. https://login.salesforce.com/services/oauth2/token) 103 | } 104 | 105 | // Bayeux struct allow for centralized storage of creds, ids, and cookies 106 | type Bayeux struct { 107 | creds Credentials 108 | id clientIDAndCookies 109 | } 110 | 111 | var wg sync.WaitGroup 112 | var logger = log.New(os.Stdout, "", log.Ldate|log.Ltime|log.Lmicroseconds|log.Lshortfile) 113 | var st = status{false, "", []string{}, 0} 114 | 115 | // newHTTPRequest is to create requests with context 116 | func (b *Bayeux) newHTTPRequest(ctx context.Context, body string, route string) (*http.Request, error) { 117 | var jsonStr = []byte(body) 118 | req, err := http.NewRequest("POST", route, bytes.NewBuffer(jsonStr)) 119 | if err != nil { 120 | return nil, fmt.Errorf("bad Call request: %w", err) 121 | } 122 | select { 123 | case <-ctx.Done(): 124 | return nil, ctx.Err() 125 | default: 126 | req = req.WithContext(ctx) 127 | 128 | req.Header.Add("Content-Type", "application/json") 129 | req.Header.Add("Authorization", fmt.Sprintf("Bearer %s", b.creds.AccessToken)) 130 | // Per Stackexchange comment, passing back cookies is required though undocumented in Salesforce API 131 | // We were unable to get process working without passing cookies back to SF server. 132 | // SF Reference: https://developer.salesforce.com/docs/atlas.en-us.api_streaming.meta/api_streaming/intro_client_specs.htm 133 | for _, cookie := range b.id.cookies { 134 | req.AddCookie(cookie) 135 | } 136 | } 137 | return req, nil 138 | } 139 | 140 | // Call is the base function for making bayeux requests 141 | func (b *Bayeux) call(ctx context.Context, body string, route string) (resp *http.Response, e error) { 142 | req, err := b.newHTTPRequest(ctx, body, route) 143 | if err != nil { 144 | return nil, err 145 | } 146 | 147 | client := &http.Client{} 148 | resp, err = client.Do(req) 149 | if err == io.EOF { 150 | // Right way to handle EOF? 151 | return nil, fmt.Errorf("bad bayeuxCall io.EOF: %w", err) 152 | } else if err != nil { 153 | return nil, fmt.Errorf("bad unrecoverable call: %w", err) 154 | } 155 | return resp, nil 156 | } 157 | 158 | func (b *Bayeux) getClientID(ctx context.Context) error { 159 | handshake := `{"channel": "/meta/handshake", "supportedConnectionTypes": ["long-polling"], "version": "1.0"}` 160 | // Stub out clientIDAndCookies for first bayeuxCall 161 | resp, err := b.call(ctx, handshake, b.creds.bayeuxUrl()) 162 | if err != nil { 163 | return fmt.Errorf("cannot get client id: %s", err) 164 | } 165 | defer resp.Body.Close() 166 | 167 | decoder := json.NewDecoder(resp.Body) 168 | var h BayeuxHandshake 169 | if err := decoder.Decode(&h); err == io.EOF { 170 | return err 171 | } else if err != nil { 172 | return err 173 | } 174 | creds := clientIDAndCookies{h[0].ClientID, resp.Cookies()} 175 | b.id = creds 176 | return nil 177 | } 178 | 179 | // ReplayAll replay for past 24 hrs 180 | const ReplayAll = -2 181 | 182 | // ReplayNone start playing events at current moment 183 | const ReplayNone = -1 184 | 185 | // Replay accepts the following values 186 | // Value 187 | // -2: replay all events from past 24 hrs 188 | // -1: start at current 189 | // >= 0: start from this event number 190 | type Replay struct { 191 | Value int 192 | } 193 | 194 | func (b *Bayeux) subscribe(ctx context.Context, channel string, replay string) error { 195 | handshake := fmt.Sprintf(`{ 196 | "channel": "/meta/subscribe", 197 | "subscription": "%s", 198 | "clientId": "%s", 199 | "ext": { 200 | "replay": {"%s": "%s"} 201 | } 202 | }`, channel, b.id.clientID, channel, replay) 203 | resp, err := b.call(ctx, handshake, b.creds.bayeuxUrl()) 204 | if err != nil { 205 | return fmt.Errorf("cannot subscribe: %w", err) 206 | } 207 | 208 | defer resp.Body.Close() 209 | if os.Getenv("DEBUG") != "" { 210 | logger.Printf("Response: %+v", resp) 211 | var b []byte 212 | if resp.Body != nil { 213 | b, _ = ioutil.ReadAll(resp.Body) 214 | } 215 | // Restore the io.ReadCloser to its original state 216 | resp.Body = ioutil.NopCloser(bytes.NewBuffer(b)) 217 | // Use the content 218 | s := string(b) 219 | logger.Printf("Response Body: %s", s) 220 | } 221 | 222 | if resp.StatusCode > 299 { 223 | return fmt.Errorf("received non 2XX response: %w", err) 224 | } 225 | decoder := json.NewDecoder(resp.Body) 226 | var h []Subscription 227 | if err := decoder.Decode(&h); err == io.EOF { 228 | return err 229 | } else if err != nil { 230 | return err 231 | } 232 | sub := &h[0] 233 | st.connected = sub.Successful 234 | st.clientID = sub.ClientID 235 | st.channels = append(st.channels, channel) 236 | st.connect() 237 | if os.Getenv("DEBUG") != "" { 238 | logger.Printf("Established connection(s): %+v", st) 239 | } 240 | return nil 241 | } 242 | 243 | func (b *Bayeux) connect(ctx context.Context, out chan MaybeMsg) chan MaybeMsg { 244 | var waitMsgs sync.WaitGroup 245 | wg.Add(1) 246 | go func() { 247 | defer func() { 248 | waitMsgs.Wait() 249 | close(out) 250 | st.disconnect() 251 | wg.Done() 252 | }() 253 | for { 254 | select { 255 | case <-ctx.Done(): 256 | return 257 | default: 258 | postBody := fmt.Sprintf(`{"channel": "/meta/connect", "connectionType": "long-polling", "clientId": "%s"} `, b.id.clientID) 259 | resp, err := b.call(ctx, postBody, b.creds.bayeuxUrl()) 260 | if err != nil { 261 | if errors.Is(err, context.Canceled) { 262 | return 263 | } 264 | out <- MaybeMsg{Err: fmt.Errorf("cannot connect to bayeux: %s, trying again", err)} 265 | } else { 266 | if os.Getenv("DEBUG") != "" { 267 | var b []byte 268 | if resp.Body != nil { 269 | b, _ = ioutil.ReadAll(resp.Body) 270 | } 271 | // Restore the io.ReadCloser to its original state 272 | resp.Body = ioutil.NopCloser(bytes.NewBuffer(b)) 273 | // Use the content 274 | s := string(b) 275 | logger.Printf("Response Body: %s", s) 276 | } 277 | var x []TriggerEvent 278 | decoder := json.NewDecoder(resp.Body) 279 | if err := decoder.Decode(&x); err != nil && err == io.EOF { 280 | out <- MaybeMsg{Err: err} 281 | return 282 | } 283 | for i := range x { 284 | waitMsgs.Add(1) 285 | go func(e TriggerEvent) { 286 | defer waitMsgs.Done() 287 | out <- MaybeMsg{Msg: e} 288 | }(x[i]) 289 | } 290 | } 291 | } 292 | } 293 | }() 294 | return out 295 | } 296 | 297 | // GetConnectedCount returns count of subcriptions 298 | func GetConnectedCount() int { 299 | return st.connectCount 300 | } 301 | 302 | func GetSalesforceCredentials(ap AuthenticationParameters) (creds *Credentials, err error) { 303 | params := url.Values{"grant_type": {"password"}, 304 | "client_id": {ap.ClientID}, 305 | "client_secret": {ap.ClientSecret}, 306 | "username": {ap.Username}, 307 | "password": {ap.Password}} 308 | res, err := http.PostForm(ap.TokenURL, params) 309 | if err != nil { 310 | return nil, err 311 | } 312 | decoder := json.NewDecoder(res.Body) 313 | if err := decoder.Decode(&creds); err == io.EOF { 314 | return nil, err 315 | } else if err != nil { 316 | return nil, err 317 | } else if creds.AccessToken == "" { 318 | return nil, fmt.Errorf("unable to fetch access token: %w", err) 319 | } 320 | return creds, nil 321 | } 322 | 323 | func (b *Bayeux) Channel(ctx context.Context, out chan MaybeMsg, r string, creds Credentials, channel string) chan MaybeMsg { 324 | b.creds = creds 325 | err := b.getClientID(ctx) 326 | if err != nil { 327 | out <- MaybeMsg{Err: err} 328 | close(out) 329 | return out 330 | } 331 | err = b.subscribe(ctx, channel, r) 332 | if err != nil { 333 | out <- MaybeMsg{Err: err} 334 | close(out) 335 | return out 336 | } 337 | c := b.connect(ctx, out) 338 | return c 339 | } 340 | --------------------------------------------------------------------------------