gobot.io/x/gobot/v2@v2.1.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/v2" 14 "gobot.io/x/gobot/v2/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 func (a *API) StartWithoutDefaults() { 113 a.start(a) 114 } 115 116 // AddC3PIORoutes adds all of the standard C3PIO routes to the API. 117 // For more information, please see: 118 // http://cppp.io/ 119 func (a *API) AddC3PIORoutes() { 120 mcpCommandRoute := "/api/commands/:command" 121 robotDeviceCommandRoute := "/api/robots/:robot/devices/:device/commands/:command" 122 robotCommandRoute := "/api/robots/:robot/commands/:command" 123 124 a.Get("/api/commands", a.mcpCommands) 125 a.Get(mcpCommandRoute, a.executeMcpCommand) 126 a.Post(mcpCommandRoute, a.executeMcpCommand) 127 a.Get("/api/robots", a.robots) 128 a.Get("/api/robots/:robot", a.robot) 129 a.Get("/api/robots/:robot/commands", a.robotCommands) 130 a.Get(robotCommandRoute, a.executeRobotCommand) 131 a.Post(robotCommandRoute, a.executeRobotCommand) 132 a.Get("/api/robots/:robot/devices", a.robotDevices) 133 a.Get("/api/robots/:robot/devices/:device", a.robotDevice) 134 a.Get("/api/robots/:robot/devices/:device/events/:event", a.robotDeviceEvent) 135 a.Get("/api/robots/:robot/devices/:device/commands", a.robotDeviceCommands) 136 a.Get(robotDeviceCommandRoute, a.executeRobotDeviceCommand) 137 a.Post(robotDeviceCommandRoute, a.executeRobotDeviceCommand) 138 a.Get("/api/robots/:robot/connections", a.robotConnections) 139 a.Get("/api/robots/:robot/connections/:connection", a.robotConnection) 140 a.Get("/api/", a.mcp) 141 } 142 143 // AddRobeauxRoutes adds all of the robeaux web interface routes to the API. 144 // The Robeaux web interface requires the C3PIO API, so it is also 145 // activated when you call this method. 146 func (a *API) AddRobeauxRoutes() { 147 a.AddC3PIORoutes() 148 149 a.Get("/", func(res http.ResponseWriter, req *http.Request) { 150 http.Redirect(res, req, "/index.html", http.StatusMovedPermanently) 151 }) 152 a.Get("/index.html", a.robeaux) 153 a.Get("/images/:a", a.robeaux) 154 a.Get("/js/:a", a.robeaux) 155 a.Get("/js/:a/", a.robeaux) 156 a.Get("/js/:a/:b", a.robeaux) 157 a.Get("/css/:a", a.robeaux) 158 a.Get("/css/:a/", a.robeaux) 159 a.Get("/css/:a/:b", a.robeaux) 160 a.Get("/partials/:a", a.robeaux) 161 } 162 163 // robeaux returns handler for robeaux routes. 164 // Writes asset in response and sets correct header 165 func (a *API) robeaux(res http.ResponseWriter, req *http.Request) { 166 path := req.URL.Path 167 buf, err := robeaux.Asset(path[1:]) 168 if err != nil { 169 http.Error(res, err.Error(), http.StatusNotFound) 170 return 171 } 172 t := strings.Split(path, ".") 173 if t[len(t)-1] == "js" { 174 res.Header().Set("Content-Type", "text/javascript; charset=utf-8") 175 } else if t[len(t)-1] == "css" { 176 res.Header().Set("Content-Type", "text/css; charset=utf-8") 177 } else if t[len(t)-1] == "html" { 178 res.Header().Set("Content-Type", "text/html; charset=utf-8") 179 } 180 res.Write(buf) 181 } 182 183 // mcp returns MCP route handler. 184 // Writes JSON with gobot representation 185 func (a *API) mcp(res http.ResponseWriter, req *http.Request) { 186 a.writeJSON(map[string]interface{}{"MCP": gobot.NewJSONMaster(a.master)}, res) 187 } 188 189 // mcpCommands returns commands route handler. 190 // Writes JSON with global commands representation 191 func (a *API) mcpCommands(res http.ResponseWriter, req *http.Request) { 192 a.writeJSON(map[string]interface{}{"commands": gobot.NewJSONMaster(a.master).Commands}, res) 193 } 194 195 // robots returns route handler. 196 // Writes JSON with robots representation 197 func (a *API) robots(res http.ResponseWriter, req *http.Request) { 198 jsonRobots := []*gobot.JSONRobot{} 199 a.master.Robots().Each(func(r *gobot.Robot) { 200 jsonRobots = append(jsonRobots, gobot.NewJSONRobot(r)) 201 }) 202 a.writeJSON(map[string]interface{}{"robots": jsonRobots}, res) 203 } 204 205 // robot returns route handler. 206 // Writes JSON with robot representation 207 func (a *API) robot(res http.ResponseWriter, req *http.Request) { 208 if robot, err := a.jsonRobotFor(req.URL.Query().Get(":robot")); err != nil { 209 a.writeJSON(map[string]interface{}{"error": err.Error()}, res) 210 } else { 211 a.writeJSON(map[string]interface{}{"robot": robot}, res) 212 } 213 } 214 215 // robotCommands returns commands route handler 216 // Writes JSON with robot commands representation 217 func (a *API) robotCommands(res http.ResponseWriter, req *http.Request) { 218 if robot, err := a.jsonRobotFor(req.URL.Query().Get(":robot")); err != nil { 219 a.writeJSON(map[string]interface{}{"error": err.Error()}, res) 220 } else { 221 a.writeJSON(map[string]interface{}{"commands": robot.Commands}, res) 222 } 223 } 224 225 // robotDevices returns devices route handler. 226 // Writes JSON with robot devices representation 227 func (a *API) robotDevices(res http.ResponseWriter, req *http.Request) { 228 if robot := a.master.Robot(req.URL.Query().Get(":robot")); robot != nil { 229 jsonDevices := []*gobot.JSONDevice{} 230 robot.Devices().Each(func(d gobot.Device) { 231 jsonDevices = append(jsonDevices, gobot.NewJSONDevice(d)) 232 }) 233 a.writeJSON(map[string]interface{}{"devices": jsonDevices}, res) 234 } else { 235 a.writeJSON(map[string]interface{}{"error": "No Robot found with the name " + req.URL.Query().Get(":robot")}, res) 236 } 237 } 238 239 // robotDevice returns device route handler. 240 // Writes JSON with robot device representation 241 func (a *API) robotDevice(res http.ResponseWriter, req *http.Request) { 242 if device, err := a.jsonDeviceFor(req.URL.Query().Get(":robot"), req.URL.Query().Get(":device")); err != nil { 243 a.writeJSON(map[string]interface{}{"error": err.Error()}, res) 244 } else { 245 a.writeJSON(map[string]interface{}{"device": device}, res) 246 } 247 } 248 249 func (a *API) robotDeviceEvent(res http.ResponseWriter, req *http.Request) { 250 f, _ := res.(http.Flusher) 251 252 dataChan := make(chan string) 253 254 res.Header().Set("Content-Type", "text/event-stream") 255 res.Header().Set("Cache-Control", "no-cache") 256 res.Header().Set("Connection", "keep-alive") 257 258 device := a.master.Robot(req.URL.Query().Get(":robot")). 259 Device(req.URL.Query().Get(":device")) 260 261 if event := a.master.Robot(req.URL.Query().Get(":robot")). 262 Device(req.URL.Query().Get(":device")).(gobot.Eventer). 263 Event(req.URL.Query().Get(":event")); len(event) > 0 { 264 device.(gobot.Eventer).On(event, func(data interface{}) { 265 d, _ := json.Marshal(data) 266 dataChan <- string(d) 267 }) 268 269 for { 270 select { 271 case data := <-dataChan: 272 fmt.Fprintf(res, "data: %v\n\n", data) 273 f.Flush() 274 case <-req.Context().Done(): 275 log.Println("Closing connection") 276 return 277 } 278 } 279 } else { 280 a.writeJSON(map[string]interface{}{ 281 "error": "No Event found with the name " + req.URL.Query().Get(":event"), 282 }, res) 283 } 284 } 285 286 // robotDeviceCommands returns device commands route handler 287 // writes JSON with robot device commands representation 288 func (a *API) robotDeviceCommands(res http.ResponseWriter, req *http.Request) { 289 if device, err := a.jsonDeviceFor(req.URL.Query().Get(":robot"), req.URL.Query().Get(":device")); err != nil { 290 a.writeJSON(map[string]interface{}{"error": err.Error()}, res) 291 } else { 292 a.writeJSON(map[string]interface{}{"commands": device.Commands}, res) 293 } 294 } 295 296 // robotConnections returns connections route handler 297 // writes JSON with robot connections representation 298 func (a *API) robotConnections(res http.ResponseWriter, req *http.Request) { 299 jsonConnections := []*gobot.JSONConnection{} 300 if robot := a.master.Robot(req.URL.Query().Get(":robot")); robot != nil { 301 robot.Connections().Each(func(c gobot.Connection) { 302 jsonConnections = append(jsonConnections, gobot.NewJSONConnection(c)) 303 }) 304 a.writeJSON(map[string]interface{}{"connections": jsonConnections}, res) 305 } else { 306 a.writeJSON(map[string]interface{}{"error": "No Robot found with the name " + req.URL.Query().Get(":robot")}, res) 307 } 308 309 } 310 311 // robotConnection returns connection route handler 312 // writes JSON with robot connection representation 313 func (a *API) robotConnection(res http.ResponseWriter, req *http.Request) { 314 if conn, err := a.jsonConnectionFor(req.URL.Query().Get(":robot"), req.URL.Query().Get(":connection")); err != nil { 315 a.writeJSON(map[string]interface{}{"error": err.Error()}, res) 316 } else { 317 a.writeJSON(map[string]interface{}{"connection": conn}, res) 318 } 319 } 320 321 // executeMcpCommand calls a global command associated to requested route 322 func (a *API) executeMcpCommand(res http.ResponseWriter, req *http.Request) { 323 a.executeCommand(a.master.Command(req.URL.Query().Get(":command")), 324 res, 325 req, 326 ) 327 } 328 329 // executeRobotDeviceCommand calls a device command associated to requested route 330 func (a *API) executeRobotDeviceCommand(res http.ResponseWriter, req *http.Request) { 331 if _, err := a.jsonDeviceFor(req.URL.Query().Get(":robot"), 332 req.URL.Query().Get(":device")); err != nil { 333 a.writeJSON(map[string]interface{}{"error": err.Error()}, res) 334 } else { 335 a.executeCommand( 336 a.master.Robot(req.URL.Query().Get(":robot")). 337 Device(req.URL.Query().Get(":device")).(gobot.Commander). 338 Command(req.URL.Query().Get(":command")), 339 res, 340 req, 341 ) 342 } 343 } 344 345 // executeRobotCommand calls a robot command associated to requested route 346 func (a *API) executeRobotCommand(res http.ResponseWriter, req *http.Request) { 347 if _, err := a.jsonRobotFor(req.URL.Query().Get(":robot")); err != nil { 348 a.writeJSON(map[string]interface{}{"error": err.Error()}, res) 349 } else { 350 a.executeCommand( 351 a.master.Robot(req.URL.Query().Get(":robot")). 352 Command(req.URL.Query().Get(":command")), 353 res, 354 req, 355 ) 356 } 357 } 358 359 // executeCommand writes JSON response with `f` returned value. 360 func (a *API) executeCommand(f func(map[string]interface{}) interface{}, 361 res http.ResponseWriter, 362 req *http.Request, 363 ) { 364 365 body := make(map[string]interface{}) 366 json.NewDecoder(req.Body).Decode(&body) 367 368 if f != nil { 369 a.writeJSON(map[string]interface{}{"result": f(body)}, res) 370 } else { 371 a.writeJSON(map[string]interface{}{"error": "Unknown Command"}, res) 372 } 373 } 374 375 // writeJSON writes `j` as JSON in response 376 func (a *API) writeJSON(j interface{}, res http.ResponseWriter) { 377 data, _ := json.Marshal(j) 378 res.Header().Set("Content-Type", "application/json; charset=utf-8") 379 res.Write(data) 380 } 381 382 // Debug add handler to api that prints each request 383 func (a *API) Debug() { 384 a.AddHandler(func(res http.ResponseWriter, req *http.Request) { 385 log.Println(req) 386 }) 387 } 388 389 func (a *API) jsonRobotFor(name string) (jrobot *gobot.JSONRobot, err error) { 390 if robot := a.master.Robot(name); robot != nil { 391 jrobot = gobot.NewJSONRobot(robot) 392 } else { 393 err = errors.New("No Robot found with the name " + name) 394 } 395 return 396 } 397 398 func (a *API) jsonDeviceFor(robot string, name string) (jdevice *gobot.JSONDevice, err error) { 399 if device := a.master.Robot(robot).Device(name); device != nil { 400 jdevice = gobot.NewJSONDevice(device) 401 } else { 402 err = errors.New("No Device found with the name " + name) 403 } 404 return 405 } 406 407 func (a *API) jsonConnectionFor(robot string, name string) (jconnection *gobot.JSONConnection, err error) { 408 if connection := a.master.Robot(robot).Connection(name); connection != nil { 409 jconnection = gobot.NewJSONConnection(connection) 410 } else { 411 err = errors.New("No Connection found with the name " + name) 412 } 413 return 414 }