├── .gitattributes ├── .gitignore ├── .goxc.json ├── .travis.yml ├── DEVELOPMENT.md ├── README.md ├── api ├── api.go ├── membership.go ├── messages.go ├── people.go └── rooms.go ├── sparkcli.go ├── sparkcli.toml ├── util ├── client.go ├── client_test.go ├── config.go ├── login.go └── util.go └── web ├── README.md ├── authorize.html └── code.html /.gitattributes: -------------------------------------------------------------------------------- 1 | *.go eol=lf 2 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | *.swp 2 | *.swo 3 | *~ 4 | .goxc.local.json 5 | -------------------------------------------------------------------------------- /.goxc.json: -------------------------------------------------------------------------------- 1 | { 2 | "BuildConstraints": "windows,amd64 linux,amd64 darwin,amd64", 3 | "PackageVersion": "0.7.0", 4 | "ConfigVersion": "0.9", 5 | "TaskSettings": { 6 | "bintray": { 7 | "apihost": "https://api.bintray.com/", 8 | "downloadshost": "https://dl.bintray.com/", 9 | "downloadspage": "bintray.md", 10 | "repository": "sparkcli", 11 | "package": "sparkcli", 12 | "subject": "tdeckers" 13 | } 14 | } 15 | 16 | } 17 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: go 2 | sudo: false 3 | 4 | go: 5 | - 1.3.3 6 | - 1.4.2 7 | - 1.5.1 8 | - 1.6.3 9 | - 1.7.1 10 | - tip 11 | 12 | matrix: 13 | allow_failures: 14 | - go: tip 15 | 16 | script: 17 | - go vet ./... 18 | - go test -v ./... 19 | -------------------------------------------------------------------------------- /DEVELOPMENT.md: -------------------------------------------------------------------------------- 1 | # Development 2 | 3 | Build and deploy using goxc: [See here.](https://github.com/laher/goxc/blob/master/README.md) 4 | 5 | ## Install go from source 6 | 7 | git clone https://go.googlesource.com/go 8 | git branch go1.5 9 | cd src 10 | ./all.bash 11 | go get golang.org/x/tools/cmd/... 12 | 13 | ## Install goxc` 14 | 15 | go install github.com/laher/goxc 16 | 17 | ## Run goxc 18 | 19 | Inside the project directory, run: 20 | 21 | goxc 22 | 23 | ## bintray uploads 24 | 25 | Add API key to .goxc.local.json 26 | 27 | goxc bintray 28 | 29 | Configuration for bintray plugin is in .goxc.yml. API key is in 30 | .goxc.local.yml (not checked in!). Format: 31 | 32 | { 33 | "ConfigVersion": "0.9", 34 | "TaskSettings": { 35 | "bintray": { 36 | "apikey": "5d1f300712a5da07b2f64109921cc0346622e14c" 37 | } 38 | } 39 | } 40 | 41 | # TODO 42 | 43 | * travis builds 44 | * gocover.io 45 | * godoc creation 46 | * unit testing - [gotests](https://github.com/cweill/gotests) 47 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | [![Build Status](https://travis-ci.org/tdeckers/sparkcli.svg?branch=master)](https://travis-ci.org/tdeckers/sparkcli) [ ![Download](https://api.bintray.com/packages/tdeckers/sparkcli/sparkcli/images/download.svg) ](https://bintray.com/tdeckers/sparkcli/sparkcli/_latestVersion) 2 | 3 | # Overview 4 | 5 | `sparkcli` (say 'sparkly' :) is a command line interface to Cisco Spark. 6 | 7 | # Setup - Configuration 8 | 9 | There are two ways to provide `sparkcli` with credentials: 10 | 11 | * [OAuth integration](https://developer.ciscospark.com/authentication.html) 12 | * [Bot account](https://developer.ciscospark.com/bots.html) 13 | 14 | Using a bot account is the simplest to setup. Check the [Cisco Spark docs](https://developer.ciscospark.com/bots.html) for more details on the difference between the two. 15 | Both mechanisms are explained, when in doubt or unsure, go for the Bot account option. 16 | 17 | ## Option 1: Use a bot account 18 | 19 | **1. Obtain a Cisco Spark bot account token** 20 | 21 | Define a new Cisco Spark bot account here: [https://developer.ciscospark.com](https://developer.ciscospark.com/apps.html). Make sure to select the Bot option on this page. Fill in the fields as desired. For app icon, feel free to use `http://files.ducbase.com/spark.png` 22 | 23 | **2. Configure** 24 | 25 | Create a configuration file called `sparkcli.toml`. This file is in 26 | [toml format](https://godoc.org/github.com/BurntSushi/toml). Sparkcli will look for the 27 | file in these locations (in order): 28 | 29 | * current working directory 30 | * `/etc/sparkcli` 31 | * users' home directory 32 | 33 | Add the `AccessToken` to the file: 34 | 35 | # cat /etc/sparkcli/sparkcli.toml 36 | AccessToken = "NzIxMzZkMzYtODQ22S00YqFkLWIzNjUtYTg2NWZmYmEz12d5MzJmM2NhZDYtMWM1" 37 | 38 | And you're done! Skip down to the usage section for more. 39 | 40 | ## Option 2: Use OAuth integration 41 | 42 | **1. Obtain a Cisco Spark developer API key and secret** 43 | 44 | Define a new Cisco Spark integration app here: [https://developer.ciscospark.com](https://developer.ciscospark.com/apps.html). Make sure to select the Integration option. Fill in the fields as desired, with the exception of: 45 | 46 | * App icon: Feel free to use `http://files.ducbase.com/spark.png` or use your own. 47 | * Redirect Url: `http://files.ducbase.com/code.html`. 48 | * Scopes: check all boxes. 49 | 50 | You'll be provided with a `ClientID` and `ClientSecret`. You'll need these for the 51 | next step. 52 | 53 | Next navigate to the 'OAuth Authorization URL' to obtain an `AuthCode`. 54 | 55 | **2. Configure** 56 | 57 | Create a configuration file called `sparkcli.toml`. This file is in 58 | [toml format](https://godoc.org/github.com/BurntSushi/toml). Sparkcli will look for the 59 | file in these locations (in order): 60 | 61 | * current working directory 62 | * `/etc/sparkcli` 63 | * users' home directory 64 | 65 | Add the `ClientID`, `ClientSecret` and the `AuthCode` from the previous steps in the file: 66 | 67 | # cat ./sparkcli.toml 68 | ClientId = "C23d70022b9e6c4b348897daac846xf694e7f8ffa3cd38986c6974433def69784" 69 | ClientSecret = "dcca20a5b5cc89fbea1f2b3cd41x80248ff698277583bce69fa63923ef02dc64" 70 | AuthCode = "46cd20fe32936af96ecb385772896ff84208x14dc3b2f1aebf98cc5b5a02accd" 71 | 72 | **3. Login** 73 | 74 | Download a copy of sparkcli: [ ![Download](https://api.bintray.com/packages/tdeckers/sparkcli/sparkcli/images/download.svg) ](https://bintray.com/tdeckers/sparkcli/sparkcli/_latestVersion). 75 | Then run 76 | 77 | sparkcli login 78 | 79 | This will update your configuration file with the neccesary tokens for Sparkcli 80 | to authenticate against the Cisco Spark service. If you use SparkCli frequent enough 81 | (once every 90 or so days at least), tokens will be refreshed and kept up to date 82 | as needed. 83 | 84 | _**Note**: If Sparkcli gets confused and can't login for some reason, likely the easiest solution is 85 | to remove followling fields - AuthCode, AccessToken, RefreshToken - from sparkcli.toml 86 | and restart from step 2 above._ 87 | 88 | # Usage 89 | 90 | You'll notice that most commands have a short hand script which is listed below 91 | the long version 92 | 93 | ## Global arguments 94 | 95 | sparkcli -h 96 | 97 | > Get help 98 | 99 | sparkcli -j=false ... 100 | 101 | > Formats the results (if any) in a human readable format. If this options is 102 | > set to true or not present the return value(s) as JSON. 103 | 104 | ## Rooms 105 | 106 | List all rooms 107 | 108 | sparkcli rooms list 109 | sparkcli r l 110 | 111 | > Lists all rooms you're subscribed too. 112 | 113 | Create room 114 | 115 | sparkcli rooms create 116 | sparkcli r c 117 | 118 | > Creates a room with the name specified. The room name can include multiple words. 119 | > If -j=false, only the room id is printed so it can be assigned to a variable. 120 | 121 | Get a specific room 122 | 123 | sparkcli rooms get 124 | sparkcli r g 125 | 126 | # using the default room 127 | sparkcli r g 128 | 129 | > Gets details for the room. If no id is provided, this command uses the default 130 | > room id if one is available in the config. See how to set a default room in 131 | > the config later. 132 | 133 | Delete a room 134 | 135 | sparkcli rooms delete 136 | sparkcli r d 137 | 138 | > Deletes the room. 139 | 140 | Set the default room 141 | 142 | sparkcli rooms default 143 | 144 | # no short for default! 145 | sparkcli r default 146 | 147 | > Saves a default room id to the config for use in other operations that support it. 148 | > This won't check if the room actually exists. If no id is provided, this will 149 | > just diplay the saved room id. 150 | 151 | ## Messages 152 | 153 | List messages 154 | 155 | sparkcli messages list 156 | sparkcli m l 157 | 158 | # to use the default room 159 | sparkcli m l 160 | 161 | > List the messages for a given room. If no room id is provided, the default room 162 | > will be used if one exists. 163 | 164 | Create message 165 | 166 | sparkcli messages create 167 | sparkcli m c 168 | 169 | # to post in the default room 170 | sparkcli m c - 171 | 172 | > Creates a message is the specified room. For posting to the default room, use 173 | > a dash (-). 174 | 175 | Get a message 176 | 177 | sparkcli messages get 178 | sparkcli m g 179 | 180 | > Gets a messages' details. 181 | 182 | Delete a message 183 | 184 | sparkcli messages delete 185 | sparkcli m d 186 | 187 | > Deletes a messages. 188 | 189 | ## People 190 | 191 | Get people details 192 | 193 | sparkcli people get 194 | sparkcli p g 195 | 196 | # get your details 197 | sparkcli people get 198 | sparkcli people get me 199 | 200 | > Gets people details. If no id is provided, or if _me_ is provided, then your 201 | > own account details are returned. 202 | 203 | List people 204 | 205 | sparkcli people list -email -name 206 | sparkcli p l -e -n 207 | 208 | > List people that match email or name (startsWith). You must provide one of the 209 | > two options. 210 | 211 | # Membership 212 | 213 | List memberships 214 | 215 | sparkcli memberships list -room -personid -email 216 | sparkcli m l -r -p -e 217 | 218 | # list rooms you're subscribed to (no params needed) 219 | sparkcli membership list 220 | 221 | # list members of a room 222 | sparkcli membership list -r 223 | 224 | # list members of the default room (from config) 225 | sparkcli membership list -r - 226 | 227 | # Check membership for a person in a room 228 | sparkcli membership list -r -email 229 | sparkcli membership list -r -p 230 | 231 | > List membership information. See examples for usage details. 232 | 233 | Create a membership 234 | 235 | sparkcli memberships create -room -personid -email 236 | sparkcli m c -r -p -e 237 | 238 | # Create membership for the default room 239 | sparkcli membership create -r - -e 240 | 241 | > Create a membership for the person (specify either id or email) to the room 242 | 243 | Get membership details 244 | 245 | sparkcli memberships get 246 | 247 | > Returns details for the membership. 248 | 249 | Update membership 250 | 251 | sparkcli memberships update -moderator=true|false 252 | sparkcli m u -m=true|false 253 | 254 | > Update membership. At this point only the moderator value of a membership can 255 | > be updated. 256 | 257 | Delete membership 258 | 259 | sparkcli memberships delete 260 | sparkcli m d 261 | 262 | > Delete membership. 263 | 264 | ## Other 265 | 266 | Login 267 | 268 | sparkcli login 269 | 270 | > Logs you into the Cisco Spark service, and stores access tokens on success. 271 | 272 | # Development 273 | 274 | See [Development](DEVELOPMENT.md) 275 | -------------------------------------------------------------------------------- /api/api.go: -------------------------------------------------------------------------------- 1 | // Package api provides functionality that matches the Cisco Spark API. 2 | // The Cisco Spark API is documented at 3 | // https://developer.ciscospark.com/resource-people.html 4 | package api 5 | -------------------------------------------------------------------------------- /api/membership.go: -------------------------------------------------------------------------------- 1 | package api 2 | 3 | import ( 4 | "errors" 5 | "github.com/tdeckers/sparkcli/util" 6 | "net/url" 7 | ) 8 | 9 | type MemberService struct { 10 | Client *util.Client 11 | } 12 | 13 | type Membership struct { 14 | Id string `json:"id,omitempty"` 15 | RoomId string `json:"roomId,omitempty"` 16 | PersonId string `json:"personId,omitempty"` 17 | PersonEmail string `json:"personEmail,omitempty"` 18 | PersonDisplayName string `json:"personDisplayName,omitempty"` 19 | IsModerator bool `json:"isModerator,omitempty"` 20 | IsMonitor bool `json:"isMonitor,omitempty"` 21 | Created string `json:"created,omitempty"` 22 | } 23 | 24 | type MembershipItems struct { 25 | Items []Membership `json:"items"` 26 | } 27 | 28 | func (m MemberService) List(roomId string, personId string, personEmail string) (*[]Membership, error) { 29 | v := url.Values{} 30 | if roomId != "" { 31 | v.Add("roomId", roomId) 32 | } 33 | if personId != "" { 34 | v.Add("personId", personId) 35 | } 36 | if personEmail != "" { 37 | v.Add("personEmail", personEmail) 38 | } 39 | req, err := m.Client.NewGetRequest("/memberships?" + v.Encode()) 40 | if err != nil { 41 | return nil, err 42 | } 43 | var result MembershipItems 44 | _, err = m.Client.Do(req, &result) 45 | if err != nil { 46 | return nil, err 47 | } 48 | return &result.Items, nil 49 | } 50 | 51 | func (m MemberService) Create(roomId, personId, personEmail string) (*Membership, error) { 52 | // check default room id 53 | config := util.GetConfiguration() 54 | if roomId == "-" { 55 | if config.DefaultRoomId != "" { 56 | roomId = config.DefaultRoomId 57 | } else { 58 | return nil, errors.New("No DefaultRoomId configured.") 59 | } 60 | } 61 | ms := Membership{RoomId: roomId, PersonId: personId, PersonEmail: personEmail} 62 | req, err := m.Client.NewPostRequest("/memberships", ms) 63 | if err != nil { 64 | return nil, err 65 | } 66 | var result Membership 67 | _, err = m.Client.Do(req, &result) 68 | if err != nil { 69 | return nil, err 70 | } 71 | return &result, nil 72 | } 73 | 74 | func (m MemberService) Get(id string) (*Membership, error) { 75 | req, err := m.Client.NewGetRequest("/memberships/" + id) 76 | if err != nil { 77 | return nil, err 78 | } 79 | var result Membership 80 | _, err = m.Client.Do(req, &result) 81 | if err != nil { 82 | return nil, err 83 | } 84 | return &result, nil 85 | } 86 | 87 | func (m MemberService) Update(id string, isModerator bool) (*Membership, error) { 88 | ms := Membership{IsModerator: isModerator} 89 | req, err := m.Client.NewPutRequest("/memberships/"+id, ms) 90 | if err != nil { 91 | return nil, err 92 | } 93 | var result Membership 94 | _, err = m.Client.Do(req, &result) 95 | if err != nil { 96 | return nil, err 97 | } 98 | return &result, nil 99 | } 100 | 101 | func (m MemberService) Delete(id string) error { 102 | req, err := m.Client.NewDeleteRequest("/memberships/" + id) 103 | if err != nil { 104 | return err 105 | } 106 | _, err = m.Client.Do(req, nil) 107 | if err != nil { 108 | return err 109 | } 110 | return nil 111 | } 112 | -------------------------------------------------------------------------------- /api/messages.go: -------------------------------------------------------------------------------- 1 | package api 2 | 3 | import ( 4 | "errors" 5 | "github.com/tdeckers/sparkcli/util" 6 | "log" 7 | ) 8 | 9 | type MessageService struct { 10 | Client *util.Client 11 | } 12 | 13 | type Message struct { 14 | Id string `json:"id,omitempty"` 15 | RoomId string `json:"roomId,omitempty"` 16 | Text string `json:"text,omitempty"` 17 | Files string `json:"files,omitempty"` 18 | ToPersonId string `json:"toPersonId,omitempty"` 19 | ToPersonEmail string `json:"toPersonEmail,omitempty"` 20 | PersonId string `json:"personId,omitempty"` 21 | PersonEmail string `json:"personEmail,omitempty"` 22 | Created string `json:"created,omitempty"` 23 | } 24 | 25 | type MessageItems struct { 26 | Items []Message `json:"items"` 27 | } 28 | 29 | func (m MessageService) list() (*[]Message, error) { 30 | log.Fatal("Not implemented") 31 | return nil, nil 32 | } 33 | 34 | func (m MessageService) List(roomId string) (*[]Message, error) { 35 | req, err := m.Client.NewGetRequest("/messages?roomId=" + roomId) 36 | if err != nil { 37 | return nil, err 38 | } 39 | var result MessageItems 40 | _, err = m.Client.Do(req, &result) 41 | if err != nil { 42 | return nil, err 43 | } 44 | return &result.Items, nil 45 | } 46 | 47 | // TODO: create different version, or update, to support direct msgs. 48 | func (m MessageService) Create(roomId string, txt string) (*Message, error) { 49 | // Check for default roomId 50 | config := util.GetConfiguration() 51 | if roomId == "-" { 52 | if config.DefaultRoomId != "" { 53 | roomId = config.DefaultRoomId 54 | } else { 55 | return nil, errors.New("No DefaultRoomId configured.") 56 | } 57 | } 58 | 59 | msg := Message{RoomId: roomId, Text: txt} 60 | req, err := m.Client.NewPostRequest("/messages", msg) 61 | if err != nil { 62 | return nil, err 63 | } 64 | var result Message 65 | _, err = m.Client.Do(req, &result) 66 | if err != nil { 67 | return nil, err 68 | } 69 | return &result, nil 70 | } 71 | 72 | func (m MessageService) CreateFile(roomId string, file string) (*Message, error) { 73 | // Check for default roomId 74 | config := util.GetConfiguration() 75 | if roomId == "-" { 76 | if config.DefaultRoomId != "" { 77 | roomId = config.DefaultRoomId 78 | } else { 79 | return nil, errors.New("No DefaultRoomId configured.") 80 | } 81 | } 82 | 83 | req, err := m.Client.NewFilePostRequest("/messages", roomId, file) 84 | if err != nil { 85 | return nil, err 86 | } 87 | var result Message 88 | _, err = m.Client.Do(req, &result) 89 | if err != nil { 90 | return nil, err 91 | } 92 | return &result, nil 93 | } 94 | 95 | func (m MessageService) Get(id string) (*Message, error) { 96 | if id == "" { 97 | return nil, errors.New("id can't be empty when getting message") 98 | } 99 | req, err := m.Client.NewGetRequest("/messages/" + id) 100 | if err != nil { 101 | return nil, err 102 | } 103 | var result Message 104 | _, err = m.Client.Do(req, &result) 105 | if err != nil { 106 | return nil, err 107 | } 108 | return &result, nil 109 | } 110 | 111 | func (m MessageService) Delete(id string) error { 112 | if id == "" { 113 | return errors.New("id can't be empty when deleting a message") 114 | } 115 | req, err := m.Client.NewDeleteRequest("/messages/" + id) 116 | if err != nil { 117 | return err 118 | } 119 | _, err = m.Client.Do(req, nil) 120 | if err != nil { 121 | return err 122 | } 123 | return nil //success 124 | } 125 | -------------------------------------------------------------------------------- /api/people.go: -------------------------------------------------------------------------------- 1 | package api 2 | 3 | import ( 4 | "errors" 5 | "github.com/tdeckers/sparkcli/util" 6 | "net/url" 7 | ) 8 | 9 | type PeopleService struct { 10 | Client *util.Client 11 | } 12 | 13 | // People data structure 14 | type People struct { 15 | Id string `json:"id,omitempty"` 16 | Emails []string `json:"emails,omitempty"` 17 | DisplayName string `json:"displayName,omitempty"` 18 | Avatar string `json:"avatar,omitempty"` 19 | Created string `json:"created,omitempty"` 20 | } 21 | 22 | type PeopleItems struct { 23 | Items []People `json:"items"` 24 | } 25 | 26 | func (p PeopleService) List(email string, displayName string) (*[]People, error) { 27 | if email == "" && displayName == "" { 28 | // TODO: don't need to create this message. Just return what service returns. 29 | //{ 30 | // "message": "Email or displayName should be specified.", 31 | // "errors": [ 32 | // { 33 | // "description": "Email or displayName should be specified." 34 | // } 35 | // ], 36 | // "trackingId": "NA_4de291c7-f857-4c3b-a02d-5129e7cea02c" 37 | //} 38 | return nil, errors.New("Email or displayName should be specified") 39 | } 40 | v := url.Values{} 41 | if email != "" { 42 | v.Add("email", email) 43 | } 44 | // TODO: check searching by name, doesn't seem to always work?! 45 | if displayName != "" { 46 | v.Add("displayName", displayName) 47 | } 48 | req, err := p.Client.NewGetRequest("/people?" + v.Encode()) 49 | if err != nil { 50 | return nil, err 51 | } 52 | var result PeopleItems 53 | _, err = p.Client.Do(req, &result) 54 | if err != nil { 55 | return nil, err 56 | } 57 | return &result.Items, nil 58 | } 59 | 60 | func (p PeopleService) Get(id string) (*People, error) { 61 | req, err := p.Client.NewGetRequest("/people/" + id) 62 | if err != nil { 63 | return nil, err 64 | } 65 | var result People 66 | _, err = p.Client.Do(req, &result) 67 | if err != nil { 68 | return nil, err 69 | } 70 | return &result, nil 71 | } 72 | 73 | func (p PeopleService) GetMe() (*People, error) { 74 | return p.Get("me") 75 | } 76 | -------------------------------------------------------------------------------- /api/rooms.go: -------------------------------------------------------------------------------- 1 | package api 2 | 3 | import ( 4 | "github.com/tdeckers/sparkcli/util" 5 | ) 6 | 7 | type RoomService struct { 8 | Client *util.Client 9 | } 10 | 11 | type Room struct { 12 | Id string `json:"id,omitempty"` 13 | Title string `json:"title,omitempty"` 14 | SipAddress string `json:"sipAddress,omitempty"` 15 | Created string `json:"created,omitempty"` 16 | LastActivity string `json:"lastActivity,omitempty"` 17 | IsLocked bool `json:"isLocked,omitempty"` 18 | } 19 | 20 | type RoomItems struct { 21 | Items []Room `json:"items"` 22 | } 23 | 24 | func (r RoomService) List() (*[]Room, error) { 25 | req, err := r.Client.NewGetRequest("/rooms") 26 | if err != nil { 27 | return nil, err 28 | } 29 | var result RoomItems 30 | _, err = r.Client.Do(req, &result) 31 | if err != nil { 32 | return nil, err 33 | } 34 | return &result.Items, nil 35 | } 36 | 37 | func (r RoomService) Create(name string) (*Room, error) { 38 | room := Room{Title: name} 39 | req, err := r.Client.NewPostRequest("/rooms", room) 40 | if err != nil { 41 | return nil, err 42 | } 43 | var result Room 44 | _, err = r.Client.Do(req, &result) 45 | if err != nil { 46 | return nil, err 47 | } 48 | return &result, nil 49 | } 50 | 51 | func (r RoomService) Get(id string) (*Room, error) { 52 | // for now, we're always returning the SIP address. 53 | req, err := r.Client.NewGetRequest("/rooms/" + id + "?showSipAddress=true") 54 | if err != nil { 55 | return nil, err 56 | } 57 | var result Room 58 | _, err = r.Client.Do(req, &result) 59 | if err != nil { 60 | return nil, err 61 | } 62 | return &result, nil 63 | } 64 | 65 | func (r RoomService) Update(id string, name string) (*Room, error) { 66 | room := Room{Title: name} 67 | req, err := r.Client.NewPutRequest("/rooms/"+id, room) 68 | if err != nil { 69 | return nil, err 70 | } 71 | var result Room 72 | _, err = r.Client.Do(req, &result) 73 | if err != nil { 74 | return nil, err 75 | } 76 | return &result, nil 77 | } 78 | 79 | func (r RoomService) Delete(id string) error { 80 | req, err := r.Client.NewDeleteRequest("/rooms/" + id) 81 | if err != nil { 82 | return err 83 | } 84 | _, err = r.Client.Do(req, nil) 85 | if err != nil { 86 | return err 87 | } 88 | return nil //success 89 | } 90 | -------------------------------------------------------------------------------- /sparkcli.go: -------------------------------------------------------------------------------- 1 | // Package main provides the main entry point for sparkcli. 2 | package main 3 | 4 | import ( 5 | "fmt" 6 | "github.com/codegangsta/cli" 7 | "github.com/tdeckers/sparkcli/api" 8 | "github.com/tdeckers/sparkcli/util" 9 | "log" // TODO: change to https://github.com/Sirupsen/logrus 10 | "os" 11 | "strings" 12 | ) 13 | 14 | // 15 | func main() { 16 | var jsonFlag bool 17 | 18 | config := util.GetConfiguration() 19 | config.Load() 20 | client := util.NewClient(config) 21 | app := cli.NewApp() 22 | app.Name = "sparkcli" 23 | app.Usage = "Command Line Interface for Cisco Spark" 24 | app.Version = "0.6.0" 25 | app.Flags = []cli.Flag{ 26 | cli.BoolTFlag{ 27 | Name: "j", 28 | Usage: "return results as json", 29 | Destination: &jsonFlag, 30 | }, 31 | } 32 | app.Commands = []cli.Command{ 33 | { 34 | Name: "login", 35 | Aliases: []string{"l"}, 36 | Usage: "login to Cisco Spark", 37 | Action: func(c *cli.Context) { 38 | log.Println("Logging in") 39 | login := util.NewLogin(config, client) 40 | login.Authorize() 41 | }, 42 | }, 43 | { 44 | Name: "rooms", 45 | Aliases: []string{"r"}, 46 | Usage: "operations on rooms", 47 | Subcommands: []cli.Command{ 48 | { 49 | Name: "list", 50 | Aliases: []string{"l"}, 51 | Usage: "list all rooms", 52 | Action: func(c *cli.Context) { 53 | roomService := api.RoomService{Client: client} 54 | rooms, err := roomService.List() 55 | if err != nil { 56 | log.Fatalln(err) 57 | } else { 58 | if jsonFlag { 59 | util.PrintJson(rooms) 60 | } else { 61 | // TODO: should I calculate room id length somehow? 62 | fmt.Print("Id" + strings.Repeat(" ", 76) + "Title\n") 63 | for _, room := range *rooms { 64 | fmt.Printf("%s: %s\n", room.Id, room.Title) 65 | } 66 | } 67 | } 68 | }, 69 | }, 70 | { 71 | Name: "create", 72 | Aliases: []string{"c"}, 73 | Usage: "create a new room", 74 | Action: func(c *cli.Context) { 75 | if c.NArg() != 1 { 76 | log.Fatal("Usage: sparkcli rooms create ") 77 | } 78 | name := c.Args().Get(0) 79 | roomService := api.RoomService{Client: client} 80 | room, err := roomService.Create(name) 81 | if err != nil { 82 | log.Fatalln(err) 83 | } else { 84 | if jsonFlag { 85 | util.PrintJson(room) 86 | } else { 87 | // Print just roomId, so can assign to env variable if desired. 88 | fmt.Print(room.Id) 89 | } 90 | } 91 | }, 92 | }, 93 | { 94 | Name: "get", 95 | Aliases: []string{"g"}, 96 | Usage: "get room details", 97 | Action: func(c *cli.Context) { 98 | if c.NArg() > 1 { 99 | log.Fatal("Usage: sparkcli rooms get ") 100 | } 101 | id := c.Args().Get(0) 102 | if id == "" { // try default room 103 | id = config.DefaultRoomId 104 | if id == "" { 105 | log.Fatal("Usage: sparkcli rooms get (no default room configured)") 106 | } 107 | } 108 | roomService := api.RoomService{Client: client} 109 | room, err := roomService.Get(id) 110 | if err != nil { 111 | log.Fatalln(err) 112 | } else { 113 | if jsonFlag { 114 | util.PrintJson(room) 115 | } else { 116 | fmt.Printf("Id: %s\n", room.Id) 117 | fmt.Printf("Title: %s\n", room.Title) 118 | fmt.Printf("Sip Address: %s\n", room.SipAddress) 119 | fmt.Printf("Created: %s\n", room.Created) 120 | } 121 | } 122 | }, 123 | }, 124 | { 125 | Name: "delete", 126 | Aliases: []string{"d"}, 127 | Usage: "delete a room", 128 | Action: func(c *cli.Context) { 129 | if c.NArg() != 1 { 130 | log.Fatal("Usage: sparkcli rooms delete ") 131 | } 132 | id := c.Args().Get(0) 133 | roomService := api.RoomService{Client: client} 134 | err := roomService.Delete(id) 135 | //TODO: if error is '400 Bad Request', try deleting by name? 136 | if err != nil { 137 | log.Fatalln(err) 138 | } else { 139 | if !jsonFlag { 140 | fmt.Println("Room deleted.") 141 | } 142 | // when json, just return empty. Exit code will tell it's ok. 143 | } 144 | }, 145 | }, 146 | // Convenience actions (not available in Cisco Spark API) 147 | { 148 | Name: "default", 149 | Usage: "save default room in config", 150 | Action: func(c *cli.Context) { 151 | if c.NArg() > 1 { 152 | log.Fatal("Usage: sparkcli rooms default ()") 153 | } 154 | if c.NArg() == 1 { 155 | id := c.Args().Get(0) 156 | config.DefaultRoomId = id 157 | config.Save() 158 | } else { 159 | // just display the room id 160 | fmt.Print(config.DefaultRoomId) 161 | } 162 | }, 163 | }, 164 | }, 165 | }, 166 | { 167 | Name: "messages", 168 | Aliases: []string{"m"}, 169 | Usage: "operations on messages", 170 | Subcommands: []cli.Command{ 171 | { 172 | Name: "list", 173 | Aliases: []string{"l"}, 174 | Usage: "list all messages", 175 | Action: func(c *cli.Context) { 176 | // TODO: add limiters (num, before, beforeMessage) 177 | // If no arg provided, also use default room. 178 | if c.NArg() > 1 { 179 | log.Fatal("Usage: sparkcli messages list ") 180 | } 181 | id := c.Args().Get(0) 182 | if id == "" { 183 | id = config.DefaultRoomId 184 | if id == "" { 185 | log.Println("No default room configured.") 186 | log.Fatal("Usage: sparkcli messages list ") 187 | } 188 | } 189 | msgService := api.MessageService{Client: client} 190 | msgs, err := msgService.List(id) 191 | if err != nil { 192 | log.Fatalln(err) 193 | } else { 194 | if jsonFlag { 195 | util.PrintJson(msgs) 196 | } else { 197 | for _, msg := range *msgs { 198 | fmt.Printf("[%v] %v: %v\n", msg.Created, msg.PersonEmail, msg.Text) 199 | } 200 | } 201 | } 202 | }, 203 | }, 204 | { 205 | Name: "create", 206 | Aliases: []string{"c"}, 207 | Usage: "create a new message", 208 | Subcommands: []cli.Command{ 209 | { 210 | Name: "text", 211 | Usage: "create a new text message", 212 | Action: func(c *cli.Context) { 213 | // TODO: change this to take all args after the second as additional text. 214 | if c.NArg() < 1 { 215 | log.Fatal("Usage: sparkcli messages create text ") 216 | } 217 | id := c.Args().Get(0) 218 | if id == "-" { 219 | id = config.DefaultRoomId 220 | if id == "" { 221 | log.Println("No default room configured.") 222 | log.Fatal("Usage: sparkcli messages list ") 223 | } 224 | } 225 | msgTxt := strings.Join(c.Args().Tail(), " ") 226 | msgService := api.MessageService{Client: client} 227 | msg, err := msgService.Create(id, msgTxt) 228 | if err != nil { 229 | log.Fatalln(err) 230 | } else { 231 | if jsonFlag { 232 | util.PrintJson(msg) 233 | } else { 234 | fmt.Print(msg.Id) 235 | } 236 | } 237 | }, 238 | }, 239 | { 240 | Name: "file", 241 | Usage: "send a attachment", 242 | Action: func(c *cli.Context) { 243 | if c.NArg() < 1 { 244 | log.Fatal("Usage: sparkcli messages create file ") 245 | } 246 | id := c.Args().Get(0) 247 | if id == "-" { 248 | id = config.DefaultRoomId 249 | if id == "" { 250 | log.Println("No default room configured.") 251 | log.Fatal("Usage: sparkcli messages list ") 252 | } 253 | } 254 | filePath := strings.Join(c.Args().Tail(), " ") 255 | msgService := api.MessageService{Client: client} 256 | msg, err := msgService.CreateFile(id, filePath) 257 | if err != nil { 258 | log.Fatalln(err) 259 | } else { 260 | if jsonFlag { 261 | util.PrintJson(msg) 262 | } else { 263 | fmt.Print(msg.Id) 264 | } 265 | } 266 | }, 267 | }, 268 | }, 269 | }, 270 | { 271 | Name: "get", 272 | Aliases: []string{"g"}, 273 | Usage: "get message details", 274 | Action: func(c *cli.Context) { 275 | if c.NArg() != 1 { 276 | log.Fatal("Usage: sparkcli messages get ") 277 | } 278 | id := c.Args().Get(0) 279 | msgService := api.MessageService{Client: client} 280 | msg, err := msgService.Get(id) 281 | if err != nil { 282 | log.Fatalln(err) 283 | } else { 284 | if jsonFlag { 285 | util.PrintJson(msg) 286 | } else { 287 | fmt.Printf("Id: %s\n", msg.Id) 288 | fmt.Printf("PersonId: %s\n", msg.PersonId) 289 | fmt.Printf("PersonEmail: %s\n", msg.PersonEmail) 290 | fmt.Printf("RoomId: %s\n", msg.RoomId) 291 | fmt.Printf("Text: %s\n", msg.Text) 292 | fmt.Printf("ToPersonId: %s\n", msg.ToPersonId) 293 | fmt.Printf("ToPersonEmail: %s\n", msg.ToPersonEmail) 294 | fmt.Printf("Created: %s\n", msg.Created) 295 | } 296 | } 297 | }, 298 | }, 299 | { 300 | Name: "delete", 301 | Aliases: []string{"d"}, 302 | Usage: "delete a message", 303 | Action: func(c *cli.Context) { 304 | if c.NArg() != 1 { 305 | log.Fatal("Usage: sparkcli messages delete ") 306 | } 307 | id := c.Args().Get(0) 308 | msgService := api.MessageService{Client: client} 309 | err := msgService.Delete(id) 310 | if err != nil { 311 | log.Fatalln(err) 312 | } else { 313 | if !jsonFlag { 314 | fmt.Print("Message deleted.") 315 | } // for json, don't print. Exit code = 0. 316 | } 317 | }, 318 | }, 319 | }, 320 | }, 321 | { 322 | Name: "people", 323 | Aliases: []string{"p"}, 324 | Usage: "operations on people", 325 | Subcommands: []cli.Command{ 326 | { 327 | Name: "get", 328 | Aliases: []string{"g"}, 329 | Usage: "get your details", 330 | Action: func(c *cli.Context) { 331 | id := "me" 332 | if c.NArg() == 1 { // if argument, use that as id 333 | id = c.Args().Get(0) 334 | } 335 | peopleService := api.PeopleService{Client: client} 336 | person, err := peopleService.Get(id) 337 | if err != nil { 338 | log.Fatalln(err) 339 | } else { 340 | if jsonFlag { 341 | util.PrintJson(person) 342 | } else { 343 | fmt.Printf("Id: %s\n", person.Id) 344 | fmt.Printf("Name: %s\n", person.DisplayName) 345 | for _, email := range person.Emails { 346 | fmt.Printf("Email: %s\n", email) 347 | } 348 | fmt.Printf("Avatar: %s\n", person.Avatar) 349 | fmt.Printf("Created: %s\n", person.Created) 350 | } 351 | } 352 | 353 | }, 354 | }, 355 | { 356 | Name: "list", 357 | Aliases: []string{"l"}, 358 | Usage: "list people", 359 | Flags: []cli.Flag{ 360 | cli.StringFlag{ 361 | Name: "email, e", 362 | Usage: "email to search for", 363 | }, 364 | cli.StringFlag{ 365 | Name: "name, n", 366 | Usage: "name to search for (startWith function)", 367 | }, 368 | }, 369 | Action: func(c *cli.Context) { 370 | email := c.String("email") 371 | name := c.String("name") 372 | peopleService := api.PeopleService{Client: client} 373 | people, err := peopleService.List(email, name) 374 | if err != nil { 375 | log.Fatalln(err) 376 | } else { 377 | if jsonFlag { 378 | util.PrintJson(people) 379 | } else { 380 | for _, person := range *people { 381 | fmt.Printf("%s:\n", person.Id) 382 | fmt.Printf(" Name: %s\n", person.DisplayName) 383 | fmt.Printf(" Email: ") 384 | for i, email := range person.Emails { 385 | if i == 0 { 386 | fmt.Print(email) 387 | } else { 388 | fmt.Printf(", %s", email) 389 | } 390 | } 391 | fmt.Println() 392 | fmt.Printf(" Avatar: %s\n", person.Avatar) 393 | fmt.Printf(" Created: %s\n", person.Created) 394 | } 395 | } 396 | 397 | } 398 | }, 399 | }, 400 | }, 401 | }, 402 | { 403 | Name: "memberships", 404 | Aliases: []string{"ms"}, 405 | Usage: "operations on memberships", 406 | Subcommands: []cli.Command{ 407 | { 408 | Name: "list", 409 | Aliases: []string{"l"}, 410 | Usage: "list memberships", 411 | Flags: []cli.Flag{ 412 | cli.StringFlag{ 413 | Name: "room, r", 414 | Usage: "search by room id", 415 | }, 416 | cli.StringFlag{ 417 | Name: "personid, p", 418 | Usage: "filter by person id", 419 | }, 420 | cli.StringFlag{ 421 | Name: "email, e", 422 | Usage: "filter by email", 423 | }, 424 | }, 425 | Action: func(c *cli.Context) { 426 | roomId := c.String("room") 427 | if roomId == "-" { 428 | roomId = config.DefaultRoomId 429 | if roomId == "" { 430 | log.Println("No default room configured.") 431 | log.Fatal("Usage: sparkcli memberships list -r ") 432 | } 433 | } 434 | personId := c.String("personid") 435 | personEmail := c.String("email") 436 | memberService := api.MemberService{Client: client} 437 | mss, err := memberService.List(roomId, personId, personEmail) 438 | if err != nil { 439 | log.Fatalln(err) 440 | } else { 441 | if jsonFlag { 442 | util.PrintJson(mss) 443 | } else { 444 | for _, ms := range *mss { 445 | fmt.Printf("%s:\n", ms.Id) 446 | fmt.Printf(" Name: %s\n", ms.PersonDisplayName) 447 | fmt.Printf(" Email: %s\n", ms.PersonEmail) 448 | fmt.Printf(" Room: %s\n", ms.RoomId) 449 | fmt.Printf(" Created: %s\n", ms.Created) 450 | } 451 | } 452 | } 453 | }, 454 | }, 455 | { 456 | Name: "create", 457 | Aliases: []string{"c"}, 458 | Usage: "create memberships", 459 | Flags: []cli.Flag{ 460 | cli.StringFlag{ 461 | Name: "room, r", 462 | Usage: "room to add person to", 463 | }, 464 | cli.StringFlag{ 465 | Name: "personid, p", 466 | Usage: "id of person to add", 467 | }, 468 | cli.StringFlag{ 469 | Name: "email, e", 470 | Usage: "email of person to add", 471 | }, 472 | }, 473 | Action: func(c *cli.Context) { 474 | roomId := c.String("room") 475 | if roomId == "-" { 476 | roomId = config.DefaultRoomId 477 | if roomId == "" { 478 | log.Println("No default room configured.") 479 | log.Fatal("Usage: sparkcli memberships create -r ...") 480 | } 481 | } 482 | 483 | personId := c.String("personid") 484 | personEmail := c.String("email") 485 | memberService := api.MemberService{Client: client} 486 | ms, err := memberService.Create(roomId, personId, personEmail) 487 | if err != nil { 488 | log.Fatalln(err) 489 | } else { 490 | if jsonFlag { 491 | util.PrintJson(ms) 492 | } else { 493 | fmt.Printf("Id: %s\n", ms.Id) 494 | fmt.Printf("Name: %s\n", ms.PersonDisplayName) 495 | fmt.Printf("Email: %s\n", ms.PersonEmail) 496 | fmt.Printf("Room: %s\n", ms.RoomId) 497 | fmt.Printf("Created: %s\n", ms.Created) 498 | } 499 | } 500 | 501 | }, 502 | }, 503 | { 504 | Name: "get", 505 | Aliases: []string{"g"}, 506 | Usage: "get membership details", 507 | Action: func(c *cli.Context) { 508 | if c.NArg() != 1 { 509 | log.Fatal("Usage: sparkcli memberships get ") 510 | } 511 | id := c.Args().Get(0) 512 | msService := api.MemberService{Client: client} 513 | ms, err := msService.Get(id) 514 | if err != nil { 515 | log.Fatalln(err) 516 | } else { 517 | if jsonFlag { 518 | util.PrintJson(ms) 519 | } else { 520 | fmt.Printf("Id: %s\n", ms.Id) 521 | fmt.Printf("Name: %s\n", ms.PersonDisplayName) 522 | fmt.Printf("Email: %s\n", ms.PersonEmail) 523 | fmt.Printf("Room: %s\n", ms.RoomId) 524 | fmt.Printf("Created: %s\n", ms.Created) 525 | } 526 | } 527 | 528 | }, 529 | }, 530 | { 531 | Name: "update", 532 | Aliases: []string{"u"}, 533 | Usage: "update membership", 534 | Flags: []cli.Flag{ 535 | cli.BoolFlag{ 536 | Name: "moderator, m", 537 | Usage: "set moderator role for the membership", 538 | }, 539 | }, 540 | Action: func(c *cli.Context) { 541 | if c.NArg() != 1 { 542 | log.Fatal("Usage: sparkcli memberships update -moderator ") 543 | } 544 | id := c.Args().Get(0) 545 | // TODO: avoid doing update if flag is not present. 546 | moderator := c.Bool("moderator") 547 | msService := api.MemberService{Client: client} 548 | ms, err := msService.Update(id, moderator) 549 | if err != nil { 550 | log.Fatalln(err) 551 | } else { 552 | if jsonFlag { 553 | util.PrintJson(ms) 554 | } else { 555 | fmt.Printf("Id: %s\n", ms.Id) 556 | fmt.Printf("Name: %s\n", ms.PersonDisplayName) 557 | fmt.Printf("Email: %s\n", ms.PersonEmail) 558 | fmt.Printf("Room: %s\n", ms.RoomId) 559 | fmt.Printf("Created: %s\n", ms.Created) 560 | } 561 | } 562 | }, 563 | }, 564 | { 565 | Name: "delete", 566 | Aliases: []string{"d"}, 567 | Usage: "delete membership", 568 | Action: func(c *cli.Context) { 569 | if c.NArg() != 1 { 570 | log.Fatal("Usage: sparkcli memberships delete ") 571 | } 572 | id := c.Args().Get(0) 573 | msService := api.MemberService{Client: client} 574 | err := msService.Delete(id) 575 | if err != nil { 576 | log.Fatalln(err) 577 | } else { 578 | if !jsonFlag { 579 | fmt.Println("Membership deleted.") 580 | } 581 | } 582 | 583 | }, 584 | }, 585 | }, 586 | }, 587 | } 588 | app.Run(os.Args) 589 | } 590 | -------------------------------------------------------------------------------- /sparkcli.toml: -------------------------------------------------------------------------------- 1 | AccessToken = "NzIxMzZkMzYtODQ2MS00YWFkLWIzNjUtYTg0NWZmYmEzOTU5MzJmM2NhZDYtMWM1" 2 | DefaultRoomId = "Y2lzY29zcGFyazovL3VzL1JPT00vMTRjNjE3ZjAtZmJlNC0xMWU1LWJlN2YtOGZmOTNiYmVkZGQ5" 3 | -------------------------------------------------------------------------------- /util/client.go: -------------------------------------------------------------------------------- 1 | package util 2 | 3 | import ( 4 | "bytes" 5 | "encoding/json" 6 | "errors" 7 | "io/ioutil" 8 | "log" 9 | "net/http" 10 | "net/url" 11 | "os" 12 | "mime/multipart" 13 | "path/filepath" 14 | "io" 15 | ) 16 | 17 | const ( 18 | userAgent = "spark-cli" 19 | ) 20 | 21 | type Client struct { 22 | client *http.Client 23 | 24 | userAgent string 25 | 26 | config *Configuration 27 | } 28 | 29 | func NewClient(config *Configuration) *Client { 30 | c := &Client{client: http.DefaultClient, userAgent: userAgent, config: config} 31 | return c 32 | } 33 | 34 | func (c *Client) NewRequest(method string, path string, body interface{}) (*http.Request, error) { 35 | // concat base url and request url 36 | reqUrl, err := url.Parse(c.config.BaseUrl + path) 37 | if err != nil { 38 | return nil, err 39 | } 40 | var bodyBuffer *bytes.Buffer 41 | var req *http.Request 42 | // if body is present (likely for POST), then marshal and create buffer 43 | if body != nil { 44 | bodyJson, err := json.Marshal(body) 45 | if err != nil { 46 | return nil, err 47 | } 48 | bodyBuffer = bytes.NewBuffer(bodyJson) 49 | log.Printf("Sending: %s", bodyBuffer) 50 | // Create request with body 51 | req, err = http.NewRequest(method, reqUrl.String(), bodyBuffer) 52 | if err != nil { 53 | return nil, err 54 | } 55 | req.Header.Set("Content-Type", "application/json") 56 | } else { 57 | // Create request without body 58 | req, err = http.NewRequest(method, reqUrl.String(), nil) 59 | if err != nil { 60 | return nil, err 61 | } 62 | } 63 | // Add other headers (that apply to all requests) 64 | req.Header.Set("Authorization", "Bearer "+c.config.AccessToken) 65 | return req, nil 66 | } 67 | 68 | func (c *Client) NewFileUploadRequest(path string, roomId string, fileLocation string) (*http.Request, error) { 69 | // concat base url and request url 70 | reqUrl, err := url.Parse(c.config.BaseUrl + path) 71 | if err != nil { 72 | return nil, err 73 | } 74 | 75 | file, err := os.Open(fileLocation) 76 | if err != nil { 77 | return nil, err 78 | } 79 | defer file.Close() 80 | 81 | body := &bytes.Buffer{} 82 | writer := multipart.NewWriter(body) 83 | part, err := writer.CreateFormFile("files", filepath.Base(fileLocation)) 84 | if err != nil { 85 | return nil, err 86 | } 87 | _, err = io.Copy(part, file) 88 | _ = writer.WriteField("roomId", roomId) 89 | 90 | err = writer.Close() 91 | if err != nil { 92 | return nil, err 93 | } 94 | 95 | req, err := http.NewRequest("POST", reqUrl.String(), body) 96 | req.Header.Set("Content-Type", writer.FormDataContentType()) 97 | 98 | // Add other headers (that apply to all requests) 99 | req.Header.Set("Authorization", "Bearer "+c.config.AccessToken) 100 | return req, nil 101 | } 102 | 103 | func (c *Client) NewGetRequest(path string) (*http.Request, error) { 104 | return c.NewRequest("GET", path, nil) 105 | } 106 | 107 | func (c *Client) NewPostRequest(path string, body interface{}) (*http.Request, error) { 108 | return c.NewRequest("POST", path, body) 109 | } 110 | 111 | func (c *Client) NewPutRequest(path string, body interface{}) (*http.Request, error) { 112 | return c.NewRequest("PUT", path, body) 113 | } 114 | 115 | func (c *Client) NewDeleteRequest(path string) (*http.Request, error) { 116 | return c.NewRequest("DELETE", path, nil) 117 | } 118 | 119 | func (c *Client) NewFilePostRequest(path string, roomId string, fileLocation string) (*http.Request, error) { 120 | return c.NewFileUploadRequest(path, roomId, fileLocation) 121 | } 122 | 123 | func (c *Client) Do(req *http.Request, to interface{}) (*http.Response, error) { 124 | var res *http.Response 125 | res, err := c.client.Do(req) 126 | 127 | if err != nil { 128 | return nil, err 129 | } 130 | defer res.Body.Close() 131 | // If 401, let's try to refresh tokens and try again. 132 | if res.StatusCode == 401 { 133 | login := Login{config: c.config, client: c} 134 | login.RefreshToken() 135 | // Update the request with new AccessToken. 136 | req.Header.Set("Authorization", "Bearer "+c.config.AccessToken) 137 | 138 | res, err = c.client.Do(req) 139 | if err != nil { 140 | return nil, err 141 | } 142 | defer res.Body.Close() 143 | } 144 | err = checkStatusOk(res) 145 | if err != nil { 146 | log.Printf("Status: %s", res.Status) 147 | return nil, err 148 | } 149 | if to != nil { 150 | decoder := json.NewDecoder(res.Body) 151 | err = decoder.Decode(&to) 152 | if err != nil { 153 | return nil, err 154 | } 155 | } 156 | return res, nil 157 | } 158 | 159 | // error if status code is not in 2XX range 160 | func checkStatusOk(res *http.Response) error { 161 | if 200 < res.StatusCode && res.StatusCode > 299 { 162 | // Read the body. For some HTTP codes there's more info (e.g. 401) 163 | // { 164 | // "message": "Failed to create room.", 165 | // "errors": [ 166 | // { 167 | // "description": "Failed to create room." 168 | // } 169 | // ], 170 | // "trackingId": "NA_f6e19aac-3a72-46d2-88ec-643f4d12fcbd" 171 | //} 172 | body, err := ioutil.ReadAll(res.Body) 173 | if err != nil { 174 | return errors.New(res.Status + " - " + err.Error()) 175 | } else { 176 | return errors.New(res.Status + "\n" + string(body)) 177 | } 178 | } 179 | return nil 180 | } 181 | -------------------------------------------------------------------------------- /util/client_test.go: -------------------------------------------------------------------------------- 1 | package util 2 | 3 | import ( 4 | "net/http" 5 | "testing" 6 | ) 7 | 8 | func Test_checkStatusOk(t *testing.T) { 9 | type args struct { 10 | res *http.Response 11 | } 12 | tests := []struct { 13 | name string 14 | args args 15 | wantErr bool 16 | }{ 17 | // TODO: Add test cases. 18 | } 19 | for _, tt := range tests { 20 | if err := checkStatusOk(tt.args.res); (err != nil) != tt.wantErr { 21 | t.Errorf("%q. checkStatusOk() error = %v, wantErr %v", tt.name, err, tt.wantErr) 22 | } 23 | } 24 | } 25 | -------------------------------------------------------------------------------- /util/config.go: -------------------------------------------------------------------------------- 1 | package util 2 | 3 | import ( 4 | "bufio" 5 | "bytes" 6 | "errors" 7 | "log" 8 | "net/url" 9 | "os" 10 | "os/user" 11 | 12 | "github.com/BurntSushi/toml" 13 | ) 14 | 15 | const ( 16 | // redirectUrl used for OAuth flow 17 | redirectUrl = "http://files.ducbase.com/code.html" 18 | // scope used for OAuth flow 19 | scope = "spark:people_read spark:rooms_read spark:rooms_write " + 20 | "spark:messages_read spark:messages_write spark:memberships_read " + 21 | "spark:memberships_write" 22 | // baseUrl for Cisco Spark API requests 23 | baseUrl = "https://api.ciscospark.com/v1" 24 | ) 25 | 26 | // Configuration provides access to the config file and keeps the values 27 | // available for use. 28 | // The configuration file is define in toml 29 | // (https://github.com/toml-lang/toml). 30 | type Configuration struct { 31 | BaseUrl string 32 | ClientId string 33 | ClientSecret string 34 | AuthCode string 35 | RedirectUri string 36 | Scope string 37 | AccessToken string 38 | AccessExpires float64 39 | RefreshToken string 40 | RefreshExpires float64 41 | DefaultRoomId string 42 | } 43 | 44 | var configFile string 45 | 46 | func init() { 47 | configFile = findConfigFile() 48 | } 49 | 50 | // instance is a singleton that ensures we're only using one copy of the 51 | // Configuration. 52 | // golang singletons: http://marcio.io/2015/07/singleton-pattern-in-go/ 53 | var instance *Configuration 54 | 55 | // GetConfiguration retrieves a the Configuration instance. The instance will 56 | // be initialized on first retrieval. 57 | func GetConfiguration() *Configuration { 58 | if instance == nil { 59 | instance = &Configuration{} 60 | } 61 | return instance 62 | } 63 | 64 | // Load the Configuration from the config file. 65 | func (c *Configuration) Load() { 66 | // TODO:change this to log to stderr, actuall all logs to stderr? 67 | //log.Printf("Using configuration at %s\n", configFile) 68 | 69 | if _, err := toml.DecodeFile(configFile, &c); err != nil { 70 | log.Fatalln("Failed to open file", err) 71 | return 72 | } 73 | 74 | //TODO: check if empty after loading, else initalize 75 | if c.RedirectUri == "" { 76 | c.RedirectUri = redirectUrl 77 | } 78 | if c.Scope == "" { 79 | c.Scope = scope 80 | } 81 | if c.BaseUrl == "" { 82 | c.BaseUrl = baseUrl 83 | } 84 | } 85 | 86 | // findConfigFile attempts to find the location of the config file. It will 87 | // search in: 88 | // ./sparkcli.toml 89 | // /etc/sparkcli/sparkcli.toml 90 | // ~/sparkcli.toml 91 | // 92 | // TODO: When the config file isn't found there, it will return a default value 93 | // which will likely cause the rest of the program to fail (e.g. when trying) 94 | // to load the configuration. 95 | // 96 | // TODO: support -c property to specify the config location? 97 | func findConfigFile() string { 98 | // Prepare list of directories 99 | wd, _ := os.Getwd() 100 | 101 | paths := []string{ 102 | wd, // current working directory 103 | "/etc/sparkcli", 104 | } 105 | 106 | // if there is a current user (or HOME environment) then append that 107 | user, err := user.Current() 108 | if err == nil { 109 | paths = append(paths, user.HomeDir) 110 | } else if homedir := os.Getenv("HOME"); homedir != "" { 111 | paths = append(paths, homedir) 112 | } 113 | 114 | for _, basepath := range paths { 115 | path := basepath + string(os.PathSeparator) + "sparkcli.toml" 116 | if _, err := os.Stat(path); err == nil { 117 | return path 118 | } 119 | } 120 | return "sparkcli.toml" 121 | } 122 | 123 | // Save writes c to the config file on disk. 124 | func (c Configuration) Save() { 125 | buf := new(bytes.Buffer) 126 | if err := toml.NewEncoder(buf).Encode(c); err != nil { 127 | log.Fatalln("Failed to encode config", err) 128 | } 129 | f, err := os.Create(configFile) 130 | if err != nil { 131 | log.Fatalln("Failed to create file", err) 132 | return 133 | } 134 | 135 | w := bufio.NewWriter(f) 136 | buf.WriteTo(w) 137 | w.Flush() 138 | } 139 | 140 | // checkClientConfig verifies if ClientId, ClientSecret and AuthCode are 141 | // available in the Configuration. 142 | func (c Configuration) checkClientConfig() error { 143 | if c.ClientId == "" { 144 | return errors.New("ClientId not configured") 145 | } 146 | if c.ClientSecret == "" { 147 | return errors.New("ClientSecret not configured") 148 | } 149 | if c.AuthCode == "" { 150 | c.PrintAuthUrl() 151 | return errors.New("AuthCode not configured") 152 | } 153 | return nil 154 | } 155 | 156 | // checkAccessToken verifies if AccessToken is available in the Configuration 157 | func (c Configuration) checkAccessToken() bool { 158 | if c.AccessToken == "" { 159 | return false 160 | } else { 161 | return true 162 | } 163 | } 164 | 165 | // PrintAuthUrl writes the OAuth authorize URL to stdout. 166 | func (c Configuration) PrintAuthUrl() { 167 | log.Printf("Visit \n%s/authorize?%s", 168 | c.BaseUrl, 169 | url.Values{"response_type": {"code"}, 170 | "client_id": {c.ClientId}, 171 | "redirect_uri": {c.RedirectUri}, 172 | "scope": {c.Scope}}.Encode(), 173 | ) 174 | 175 | } 176 | -------------------------------------------------------------------------------- /util/login.go: -------------------------------------------------------------------------------- 1 | package util 2 | 3 | import ( 4 | "encoding/json" 5 | "errors" 6 | "log" 7 | "net/http" 8 | "net/url" 9 | "os" 10 | ) 11 | 12 | // Login allows authorization against the Cisco Spark service. 13 | // It supports both bot and integration accounts. 14 | type Login struct { 15 | config *Configuration 16 | client *Client 17 | } 18 | 19 | // Tokens is used to parse responses from access token requests 20 | type Tokens struct { 21 | AccessToken string `json:"access_token"` 22 | AccessExpires float64 `json:"expires_in"` 23 | RefreshToken string `json:"refresh_token"` 24 | RefreshExpires float64 `json:"refresh_token_expires_in"` 25 | } 26 | 27 | // NewLogin creates a new Login and initializes it. 28 | func NewLogin(config *Configuration, client *Client) Login { 29 | return Login{config: config, client: client} 30 | } 31 | 32 | // Authorize will verify is proper a proper access token is available. If not 33 | // it will attempt to use the OAuth integration flow. to obtain an access token 34 | // based on the provided ClientId, ClientSecret and AuthCode in the 35 | // configuration. 36 | func (l Login) Authorize() { 37 | // Check if AccessToken is present 38 | tokenPresent := l.config.checkAccessToken() 39 | if tokenPresent { 40 | // Verify if token works. 41 | err := l.test() 42 | if err != nil { 43 | l.loginAsIntegration() 44 | } else { // Success! 45 | return 46 | } 47 | } else { // AccessToken not present 48 | l.loginAsIntegration() 49 | } 50 | } 51 | 52 | // loginAsIntegration implements the OAuth grant flow for integration accouns. 53 | // it expects a configuration file to be available with ClientId, ClientSecret 54 | // and AuthCode set. 55 | // On successful authentication it will store the AccessToken and RefreshToken 56 | // in the configuration file for further use. On failure it will exit the 57 | // program. 58 | func (l Login) loginAsIntegration() { 59 | // Check if client credentials are set. 60 | err := l.config.checkClientConfig() 61 | if err != nil { // If client credentials are not set... 62 | log.Fatalf("Not configured properly: %s", err) 63 | } 64 | // client credentials properly set, let's continue. 65 | 66 | log.Println("Authorizing...") 67 | // Post form to obtain access token based on authorization code (OAuth) 68 | res, err := http.PostForm(l.config.BaseUrl+"/access_token", 69 | url.Values{"grant_type": {"authorization_code"}, 70 | "client_id": {l.config.ClientId}, 71 | "client_secret": {l.config.ClientSecret}, 72 | "code": {l.config.AuthCode}, 73 | "redirect_uri": {l.config.RedirectUri}}) 74 | if err != nil { 75 | log.Fatal(err) 76 | } 77 | defer res.Body.Close() 78 | 79 | // if 401, reauthorize? or refresh key. 80 | if res.StatusCode == 401 { 81 | log.Print("Unauthorized (401) - trying to refresh token") 82 | l.RefreshToken() 83 | } else if res.StatusCode != 200 { 84 | log.Fatal("Unexpected status code ", res.StatusCode) 85 | } 86 | 87 | // Parse json code into Tokens struct 88 | decoder := json.NewDecoder(res.Body) 89 | tokens := new(Tokens) 90 | err = decoder.Decode(&tokens) 91 | if err != nil { 92 | log.Fatalf("Failed to decode: %s", err) 93 | } 94 | 95 | log.Printf("Access token: %s", tokens.AccessToken) 96 | log.Printf("Refresh token: %s", tokens.RefreshToken) 97 | 98 | l.storeToken(tokens, false) 99 | } 100 | 101 | // RefreshToken uses the ClientId, ClientSecret and RefreshToken from the 102 | // configuration file and attempt to obtain a new access token. 103 | // On success, the new AccessToken is written into the configuration 104 | // file. The RefreshToken remains the same, its expiry is reset. 105 | // Note that sparkcli doesn't track token expiry. 106 | func (l Login) RefreshToken() { 107 | log.Print("Refreshing token...") 108 | // Post form to obtain access token based on refresh token (OAuth) 109 | res, err := http.PostForm(l.config.BaseUrl+"/access_token", 110 | url.Values{"grant_type": {"refresh_token"}, 111 | "client_id": {l.config.ClientId}, 112 | "client_secret": {l.config.ClientSecret}, 113 | "refresh_token": {l.config.RefreshToken}}) 114 | if err != nil { 115 | log.Fatal(err) 116 | } 117 | defer res.Body.Close() 118 | 119 | // if 401, reauthorize? 120 | if res.StatusCode == 401 { 121 | log.Print("Unauthorized (401)") 122 | l.config.PrintAuthUrl() 123 | os.Exit(1) 124 | } else if res.StatusCode != 200 { 125 | log.Fatal("Unexpected status code ", res.StatusCode) 126 | } 127 | 128 | // Parse json code into Tokens struct 129 | decoder := json.NewDecoder(res.Body) 130 | tokens := new(Tokens) 131 | err = decoder.Decode(&tokens) 132 | if err != nil { 133 | log.Fatalf("Failed to decode: %s", err) 134 | } 135 | 136 | l.storeToken(tokens, true) 137 | 138 | log.Printf("Successfully refreshed token.") 139 | } 140 | 141 | // storeToken writes tokens to the configuration file. When refresh 142 | // is true, it will not overwrite RefreshToken and RefreshExpires (since 143 | // these will be empty during refresh) 144 | func (l Login) storeToken(tokens *Tokens, refresh bool) { 145 | 146 | // http://blog.golang.org/json-and-go#TOC_5. 147 | l.config.AccessToken = tokens.AccessToken 148 | // typically 14 days 149 | l.config.AccessExpires = tokens.AccessExpires 150 | // A refresh doesn't repeat the refresh token, so let's not 151 | // overwrite with an empty value here! 152 | if !refresh { 153 | l.config.RefreshToken = tokens.RefreshToken 154 | // typically 90 days 155 | l.config.RefreshExpires = tokens.RefreshExpires 156 | } 157 | log.Println("Saving config") 158 | l.config.Save() 159 | 160 | } 161 | 162 | // test access to the Cisco Spark service to ensure authentication works as 163 | // expected. Returns an error if the service request fails. 164 | func (l Login) test() error { 165 | req, err := l.client.NewGetRequest("/people/me") 166 | if err != nil { 167 | log.Fatalf("Error testing connection: %s", err) 168 | } 169 | var result interface{} 170 | res, err := l.client.Do(req, &result) 171 | if err != nil { 172 | log.Fatalf("Error testing connection: %s", err) 173 | } 174 | if res.StatusCode == 401 { 175 | return errors.New("401 Unauthorized") 176 | } 177 | if res.StatusCode != 200 { 178 | // TODO: what should we do in case of another error while testing? 179 | log.Printf("Got response code %v while testing.", res.StatusCode) 180 | } 181 | return nil 182 | } 183 | -------------------------------------------------------------------------------- /util/util.go: -------------------------------------------------------------------------------- 1 | // Various utilities in support on sparkcli 2 | package util 3 | 4 | import ( 5 | "encoding/json" 6 | "fmt" 7 | ) 8 | 9 | // PrintJson prints v with proper indents 10 | func PrintJson(v interface{}) error { 11 | jsonMsg, err := json.MarshalIndent(v, "", " ") 12 | if err != nil { 13 | return err 14 | } 15 | fmt.Print(string(jsonMsg)) 16 | return nil 17 | } 18 | -------------------------------------------------------------------------------- /web/README.md: -------------------------------------------------------------------------------- 1 | These files support the OAuth flows for authorizing Sparkcli to use your credentials for access to Cisco Spark. 2 | They're deployed at: [http://files.ducbase.com/authorize.html](http://files.ducbase.com/authorize.html) 3 | 4 | -------------------------------------------------------------------------------- /web/authorize.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | Sparkcli Authorization code 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 |
18 |

Sparkcli Authorization

19 |

Use the Client Id you got from developer.ciscospark.com 20 | to start the authorization process. 21 |

22 |
23 |
24 |
25 | 26 | 27 |
28 | 29 |
30 |
31 | 34 |
35 | 36 | 37 | 38 | 39 | 40 | 41 | 62 | 72 | 73 | 74 | 75 | -------------------------------------------------------------------------------- /web/code.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | Sparkcli Authorization code 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 |
18 |

Sparkcli Authorization Code

19 |

Authorization code:
20 |

21 |

Add this in your sparkcli.toml file:

22 |
23 | 		ClientId = "..."
24 | 		ClientSecret = "..."
25 | 		AuthCode = ""
26 | 	
27 |
28 | 29 | 30 | 31 | 32 | 33 | 34 | 52 | 62 | 63 | 64 | 65 | --------------------------------------------------------------------------------