gobot.io/x/gobot@v1.16.0/api/api.go (about) 1 package api 2 3 import ( 4 "encoding/json" 5 "errors" 6 "fmt" 7 "log" 8 "net/http" 9 "net/http/httptest" 10 "strings" 11 12 "github.com/bmizerany/pat" 13 "gobot.io/x/gobot" 14 "gobot.io/x/gobot/api/robeaux" 15 ) 16 17 // API represents an API server 18 type API struct { 19 master *gobot.Master 20 router *pat.PatternServeMux 21 Host string 22 Port string 23 Cert string 24 Key string 25 handlers []func(http.ResponseWriter, *http.Request) 26 start func(*API) 27 } 28 29 // NewAPI returns a new api instance 30 func NewAPI(m *gobot.Master) *API { 31 return &API{ 32 master: m, 33 router: pat.New(), 34 Port: "3000", 35 start: func(a *API) { 36 log.Println("Initializing API on " + a.Host + ":" + a.Port + "...") 37 http.Handle("/", a) 38 39 go func() { 40 if a.Cert != "" && a.Key != "" { 41 http.ListenAndServeTLS(a.Host+":"+a.Port, a.Cert, a.Key, nil) 42 } else { 43 log.Println("WARNING: API using insecure connection. " + 44 "We recommend using an SSL certificate with Gobot.") 45 http.ListenAndServe(a.Host+":"+a.Port, nil) 46 } 47 }() 48 }, 49 } 50 } 51 52 // ServeHTTP calls api handlers and then serves request using api router 53 func (a *API) ServeHTTP(res http.ResponseWriter, req *http.Request) { 54 for _, handler := range a.handlers { 55 rec := httptest.NewRecorder() 56 handler(rec, req) 57 for k, v := range rec.Header() { 58 res.Header()[k] = v 59 } 60 if rec.Code == http.StatusUnauthorized { 61 http.Error(res, "Not Authorized", http.StatusUnauthorized) 62 return 63 } 64 } 65 a.router.ServeHTTP(res, req) 66 } 67 68 // Post wraps api router Post call 69 func (a *API) Post(path string, f func(http.ResponseWriter, *http.Request)) { 70 a.router.Post(path, http.HandlerFunc(f)) 71 } 72 73 // Put wraps api router Put call 74 func (a *API) Put(path string, f func(http.ResponseWriter, *http.Request)) { 75 a.router.Put(path, http.HandlerFunc(f)) 76 } 77 78 // Delete wraps api router Delete call 79 func (a *API) Delete(path string, f func(http.ResponseWriter, *http.Request)) { 80 a.router.Del(path, http.HandlerFunc(f)) 81 } 82 83 // Options wraps api router Options call 84 func (a *API) Options(path string, f func(http.ResponseWriter, *http.Request)) { 85 a.router.Options(path, http.HandlerFunc(f)) 86 } 87 88 // Get wraps api router Get call 89 func (a *API) Get(path string, f func(http.ResponseWriter, *http.Request)) { 90 a.router.Get(path, http.HandlerFunc(f)) 91 } 92 93 // Head wraps api router Head call 94 func (a *API) Head(path string, f func(http.ResponseWriter, *http.Request)) { 95 a.router.Head(path, http.HandlerFunc(f)) 96 } 97 98 // AddHandler appends handler to api handlers 99 func (a *API) AddHandler(f func(http.ResponseWriter, *http.Request)) { 100 a.handlers = append(a.handlers, f) 101 } 102 103 // Start initializes the api by setting up Robeaux web interface. 104 func (a *API) Start() { 105 a.AddRobeauxRoutes() 106 107 a.start(a) 108 } 109 110 // StartWithoutDefaults initializes the api without setting up the default routes. 111 // Good for custom web interfaces. 112 // 113 func (a *API) StartWithoutDefaults() { 114 a.start(a) 115 } 116 117 // AddC3PIORoutes adds all of the standard C3PIO routes to the API. 118 // For more information, please see: 119 // http://cppp.io/ 120 // 121 func (a *API) AddC3PIORoutes() { 122 mcpCommandRoute := "/api/commands/:command" 123 robotDeviceCommandRoute := "/api/robots/:robot/devices/:device/commands/:command" 124 robotCommandRoute := "/api/robots/:robot/commands/:command" 125 126 a.Get("/api/commands", a.mcpCommands) 127 a.Get(mcpCommandRoute, a.executeMcpCommand) 128 a.Post(mcpCommandRoute, a.executeMcpCommand) 129 a.Get("/api/robots", a.robots) 130 a.Get("/api/robots/:robot", a.robot) 131 a.Get("/api/robots/:robot/commands", a.robotCommands) 132 a.Get(robotCommandRoute, a.executeRobotCommand) 133 a.Post(robotCommandRoute, a.executeRobotCommand) 134 a.Get("/api/robots/:robot/devices", a.robotDevices) 135 a.Get("/api/robots/:robot/devices/:device", a.robotDevice) 136 a.Get("/api/robots/:robot/devices/:device/events/:event", a.robotDeviceEvent) 137 a.Get("/api/robots/:robot/devices/:device/commands", a.robotDeviceCommands) 138 a.Get(robotDeviceCommandRoute, a.executeRobotDeviceCommand) 139 a.Post(robotDeviceCommandRoute, a.executeRobotDeviceCommand) 140 a.Get("/api/robots/:robot/connections", a.robotConnections) 141 a.Get("/api/robots/:robot/connections/:connection", a.robotConnection) 142 a.Get("/api/", a.mcp) 143 } 144 145 // AddRobeauxRoutes adds all of the robeaux web interface routes to the API. 146 // The Robeaux web interface requires the C3PIO API, so it is also 147 // activated when you call this method. 148 func (a *API) AddRobeauxRoutes() { 149 a.AddC3PIORoutes() 150 151 a.Get("/", func(res http.ResponseWriter, req *http.Request) { 152 http.Redirect(res, req, "/index.html", http.StatusMovedPermanently) 153 }) 154 a.Get("/index.html", a.robeaux) 155 a.Get("/images/:a", a.robeaux) 156 a.Get("/js/:a", a.robeaux) 157 a.Get("/js/:a/", a.robeaux) 158 a.Get("/js/:a/:b", a.robeaux) 159 a.Get("/css/:a", a.robeaux) 160 a.Get("/css/:a/", a.robeaux) 161 a.Get("/css/:a/:b", a.robeaux) 162 a.Get("/partials/:a", a.robeaux) 163 } 164 165 // robeaux returns handler for robeaux routes. 166 // Writes asset in response and sets correct header 167 func (a *API) robeaux(res http.ResponseWriter, req *http.Request) { 168 path := req.URL.Path 169 buf, err := robeaux.Asset(path[1:]) 170 if err != nil { 171 http.Error(res, err.Error(), http.StatusNotFound) 172 return 173 } 174 t := strings.Split(path, ".") 175 if t[len(t)-1] == "js" { 176 res.Header().Set("Content-Type", "text/javascript; charset=utf-8") 177 } else if t[len(t)-1] == "css" { 178 res.Header().Set("Content-Type", "text/css; charset=utf-8") 179 } else if t[len(t)-1] == "html" { 180 res.Header().Set("Content-Type", "text/html; charset=utf-8") 181 } 182 res.Write(buf) 183 } 184 185 // mcp returns MCP route handler. 186 // Writes JSON with gobot representation 187 func (a *API) mcp(res http.ResponseWriter, req *http.Request) { 188 a.writeJSON(map[string]interface{}{"MCP": gobot.NewJSONMaster(a.master)}, res) 189 } 190 191 // mcpCommands returns commands route handler. 192 // Writes JSON with global commands representation 193 func (a *API) mcpCommands(res http.ResponseWriter, req *http.Request) { 194 a.writeJSON(map[string]interface{}{"commands": gobot.NewJSONMaster(a.master).Commands}, res) 195 } 196 197 // robots returns route handler. 198 // Writes JSON with robots representation 199 func (a *API) robots(res http.ResponseWriter, req *http.Request) { 200 jsonRobots := []*gobot.JSONRobot{} 201 a.master.Robots().Each(func(r *gobot.Robot) { 202 jsonRobots = append(jsonRobots, gobot.NewJSONRobot(r)) 203 }) 204 a.writeJSON(map[string]interface{}{"robots": jsonRobots}, res) 205 } 206 207 // robot returns route handler. 208 // Writes JSON with robot representation 209 func (a *API) robot(res http.ResponseWriter, req *http.Request) { 210 if robot, err := a.jsonRobotFor(req.URL.Query().Get(":robot")); err != nil { 211 a.writeJSON(map[string]interface{}{"error": err.Error()}, res) 212 } else { 213 a.writeJSON(map[string]interface{}{"robot": robot}, res) 214 } 215 } 216 217 // robotCommands returns commands route handler 218 // Writes JSON with robot commands representation 219 func (a *API) robotCommands(res http.ResponseWriter, req *http.Request) { 220 if robot, err := a.jsonRobotFor(req.URL.Query().Get(":robot")); err != nil { 221 a.writeJSON(map[string]interface{}{"error": err.Error()}, res) 222 } else { 223 a.writeJSON(map[string]interface{}{"commands": robot.Commands}, res) 224 } 225 } 226 227 // robotDevices returns devices route handler. 228 // Writes JSON with robot devices representation 229 func (a *API) robotDevices(res http.ResponseWriter, req *http.Request) { 230 if robot := a.master.Robot(req.URL.Query().Get(":robot")); robot != nil { 231 jsonDevices := []*gobot.JSONDevice{} 232 robot.Devices().Each(func(d gobot.Device) { 233 jsonDevices = append(jsonDevices, gobot.NewJSONDevice(d)) 234 }) 235 a.writeJSON(map[string]interface{}{"devices": jsonDevices}, res) 236 } else { 237 a.writeJSON(map[string]interface{}{"error": "No Robot found with the name " + req.URL.Query().Get(":robot")}, res) 238 } 239 } 240 241 // robotDevice returns device route handler. 242 // Writes JSON with robot device representation 243 func (a *API) robotDevice(res http.ResponseWriter, req *http.Request) { 244 if device, err := a.jsonDeviceFor(req.URL.Query().Get(":robot"), req.URL.Query().Get(":device")); err != nil { 245 a.writeJSON(map[string]interface{}{"error": err.Error()}, res) 246 } else { 247 a.writeJSON(map[string]interface{}{"device": device}, res) 248 } 249 } 250 251 func (a *API) robotDeviceEvent(res http.ResponseWriter, req *http.Request) { 252 f, _ := res.(http.Flusher) 253 c, _ := res.(http.CloseNotifier) 254 255 dataChan := make(chan string) 256 closer := c.CloseNotify() 257 258 res.Header().Set("Content-Type", "text/event-stream") 259 res.Header().Set("Cache-Control", "no-cache") 260 res.Header().Set("Connection", "keep-alive") 261 262 device := a.master.Robot(req.URL.Query().Get(":robot")). 263 Device(req.URL.Query().Get(":device")) 264 265 if event := a.master.Robot(req.URL.Query().Get(":robot")). 266 Device(req.URL.Query().Get(":device")).(gobot.Eventer). 267 Event(req.URL.Query().Get(":event")); len(event) > 0 { 268 device.(gobot.Eventer).On(event, func(data interface{}) { 269 d, _ := json.Marshal(data) 270 dataChan <- string(d) 271 }) 272 273 for { 274 select { 275 case data := <-dataChan: 276 fmt.Fprintf(res, "data: %v\n\n", data) 277 f.Flush() 278 case <-closer: 279 log.Println("Closing connection") 280 return 281 } 282 } 283 } else { 284 a.writeJSON(map[string]interface{}{ 285 "error": "No Event found with the name " + req.URL.Query().Get(":event"), 286 }, res) 287 } 288 } 289 290 // robotDeviceCommands returns device commands route handler 291 // writes JSON with robot device commands representation 292 func (a *API) robotDeviceCommands(res http.ResponseWriter, req *http.Request) { 293 if device, err := a.jsonDeviceFor(req.URL.Query().Get(":robot"), req.URL.Query().Get(":device")); err != nil { 294 a.writeJSON(map[string]interface{}{"error": err.Error()}, res) 295 } else { 296 a.writeJSON(map[string]interface{}{"commands": device.Commands}, res) 297 } 298 } 299 300 // robotConnections returns connections route handler 301 // writes JSON with robot connections representation 302 func (a *API) robotConnections(res http.ResponseWriter, req *http.Request) { 303 jsonConnections := []*gobot.JSONConnection{} 304 if robot := a.master.Robot(req.URL.Query().Get(":robot")); robot != nil { 305 robot.Connections().Each(func(c gobot.Connection) { 306 jsonConnections = append(jsonConnections, gobot.NewJSONConnection(c)) 307 }) 308 a.writeJSON(map[string]interface{}{"connections": jsonConnections}, res) 309 } else { 310 a.writeJSON(map[string]interface{}{"error": "No Robot found with the name " + req.URL.Query().Get(":robot")}, res) 311 } 312 313 } 314 315 // robotConnection returns connection route handler 316 // writes JSON with robot connection representation 317 func (a *API) robotConnection(res http.ResponseWriter, req *http.Request) { 318 if conn, err := a.jsonConnectionFor(req.URL.Query().Get(":robot"), req.URL.Query().Get(":connection")); err != nil { 319 a.writeJSON(map[string]interface{}{"error": err.Error()}, res) 320 } else { 321 a.writeJSON(map[string]interface{}{"connection": conn}, res) 322 } 323 } 324 325 // executeMcpCommand calls a global command associated to requested route 326 func (a *API) executeMcpCommand(res http.ResponseWriter, req *http.Request) { 327 a.executeCommand(a.master.Command(req.URL.Query().Get(":command")), 328 res, 329 req, 330 ) 331 } 332 333 // executeRobotDeviceCommand calls a device command associated to requested route 334 func (a *API) executeRobotDeviceCommand(res http.ResponseWriter, req *http.Request) { 335 if _, err := a.jsonDeviceFor(req.URL.Query().Get(":robot"), 336 req.URL.Query().Get(":device")); err != nil { 337 a.writeJSON(map[string]interface{}{"error": err.Error()}, res) 338 } else { 339 a.executeCommand( 340 a.master.Robot(req.URL.Query().Get(":robot")). 341 Device(req.URL.Query().Get(":device")).(gobot.Commander). 342 Command(req.URL.Query().Get(":command")), 343 res, 344 req, 345 ) 346 } 347 } 348 349 // executeRobotCommand calls a robot command associated to requested route 350 func (a *API) executeRobotCommand(res http.ResponseWriter, req *http.Request) { 351 if _, err := a.jsonRobotFor(req.URL.Query().Get(":robot")); err != nil { 352 a.writeJSON(map[string]interface{}{"error": err.Error()}, res) 353 } else { 354 a.executeCommand( 355 a.master.Robot(req.URL.Query().Get(":robot")). 356 Command(req.URL.Query().Get(":command")), 357 res, 358 req, 359 ) 360 } 361 } 362 363 // executeCommand writes JSON response with `f` returned value. 364 func (a *API) executeCommand(f func(map[string]interface{}) interface{}, 365 res http.ResponseWriter, 366 req *http.Request, 367 ) { 368 369 body := make(map[string]interface{}) 370 json.NewDecoder(req.Body).Decode(&body) 371 372 if f != nil { 373 a.writeJSON(map[string]interface{}{"result": f(body)}, res) 374 } else { 375 a.writeJSON(map[string]interface{}{"error": "Unknown Command"}, res) 376 } 377 } 378 379 // writeJSON writes `j` as JSON in response 380 func (a *API) writeJSON(j interface{}, res http.ResponseWriter) { 381 data, _ := json.Marshal(j) 382 res.Header().Set("Content-Type", "application/json; charset=utf-8") 383 res.Write(data) 384 } 385 386 // Debug add handler to api that prints each request 387 func (a *API) Debug() { 388 a.AddHandler(func(res http.ResponseWriter, req *http.Request) { 389 log.Println(req) 390 }) 391 } 392 393 func (a *API) jsonRobotFor(name string) (jrobot *gobot.JSONRobot, err error) { 394 if robot := a.master.Robot(name); robot != nil { 395 jrobot = gobot.NewJSONRobot(robot) 396 } else { 397 err = errors.New("No Robot found with the name " + name) 398 } 399 return 400 } 401 402 func (a *API) jsonDeviceFor(robot string, name string) (jdevice *gobot.JSONDevice, err error) { 403 if device := a.master.Robot(robot).Device(name); device != nil { 404 jdevice = gobot.NewJSONDevice(device) 405 } else { 406 err = errors.New("No Device found with the name " + name) 407 } 408 return 409 } 410 411 func (a *API) jsonConnectionFor(robot string, name string) (jconnection *gobot.JSONConnection, err error) { 412 if connection := a.master.Robot(robot).Connection(name); connection != nil { 413 jconnection = gobot.NewJSONConnection(connection) 414 } else { 415 err = errors.New("No Connection found with the name " + name) 416 } 417 return 418 }