You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.

234 lines
5.3 KiB

2 years ago
2 years ago
  1. package httpapiv1
  2. import (
  3. lucifer3 "git.aiterp.net/lucifer3/server"
  4. "git.aiterp.net/lucifer3/server/commands"
  5. "git.aiterp.net/lucifer3/server/effects"
  6. "git.aiterp.net/lucifer3/server/events"
  7. "git.aiterp.net/lucifer3/server/services/script"
  8. "git.aiterp.net/lucifer3/server/services/uistate"
  9. "github.com/google/uuid"
  10. "github.com/gorilla/websocket"
  11. "github.com/labstack/echo/v4"
  12. "github.com/labstack/echo/v4/middleware"
  13. "log"
  14. "net"
  15. "net/http"
  16. "sync"
  17. "time"
  18. )
  19. var zeroUUID = uuid.UUID{
  20. 0, 0, 0, 0,
  21. 0, 0, 0, 0,
  22. 0, 0, 0, 0,
  23. 0, 0, 0, 0,
  24. }
  25. func New(addr string) (lucifer3.Service, error) {
  26. svc := &service{}
  27. upgrader := websocket.Upgrader{
  28. CheckOrigin: func(r *http.Request) bool {
  29. return true
  30. },
  31. }
  32. e := echo.New()
  33. e.HideBanner = true
  34. e.HidePort = true
  35. e.Use(middleware.CORS())
  36. e.GET("/state", func(c echo.Context) error {
  37. svc.mu.Lock()
  38. data := svc.data
  39. svc.mu.Unlock()
  40. return c.JSON(200, data)
  41. })
  42. e.GET("/subscribe", func(c echo.Context) error {
  43. ws, err := upgrader.Upgrade(c.Response(), c.Request(), nil)
  44. if err != nil {
  45. return err
  46. }
  47. sub := make(chan uistate.Patch, 64)
  48. svc.addSub(sub)
  49. defer svc.removeSub(sub)
  50. defer ws.Close()
  51. lastSent := time.Now()
  52. patches := make([]uistate.Patch, 0, 128)
  53. for {
  54. patch := <-sub
  55. since := time.Now().Sub(lastSent)
  56. patches = append(patches[:0], patch)
  57. if since < time.Millisecond*950 {
  58. waitCh := time.NewTimer(time.Millisecond*1000 - since)
  59. waiting := true
  60. for waiting {
  61. select {
  62. case patch, ok := <-sub:
  63. patches = append(patches, patch)
  64. waiting = ok
  65. case <-waitCh.C:
  66. waiting = false
  67. }
  68. }
  69. }
  70. err := ws.WriteJSON(patches)
  71. if err != nil {
  72. break
  73. }
  74. lastSent = time.Now()
  75. }
  76. return nil
  77. })
  78. e.POST("/command", func(c echo.Context) error {
  79. var input commandInput
  80. err := c.Bind(&input)
  81. if err != nil {
  82. return err
  83. }
  84. svc.mu.Lock()
  85. bus := svc.bus
  86. svc.mu.Unlock()
  87. if bus == nil {
  88. return c.String(413, "Waiting for bus")
  89. }
  90. switch {
  91. case input.Assign != nil:
  92. bus.RunCommand(commands.Assign{
  93. ID: nil,
  94. Match: input.Assign.Match,
  95. Effect: input.Assign.Effect.Effect,
  96. })
  97. case input.PairDevice != nil:
  98. bus.RunCommand(*input.PairDevice)
  99. case input.ConnectDevice != nil:
  100. bus.RunCommand(*input.ConnectDevice)
  101. case input.ForgetDevice != nil:
  102. bus.RunCommand(*input.ForgetDevice)
  103. case input.SearchDevices != nil:
  104. bus.RunCommand(*input.SearchDevices)
  105. case input.AddAlias != nil:
  106. bus.RunCommand(*input.AddAlias)
  107. case input.RemoveAlias != nil:
  108. bus.RunCommand(*input.RemoveAlias)
  109. case input.UpdateScript != nil:
  110. bus.RunCommand(*input.UpdateScript)
  111. case input.ExecuteScript != nil:
  112. bus.RunCommand(*input.ExecuteScript)
  113. case input.UpdateTrigger != nil:
  114. if input.UpdateTrigger.ID == zeroUUID {
  115. input.UpdateTrigger.ID = uuid.New()
  116. }
  117. if input.UpdateTrigger.ScriptPre == nil {
  118. input.UpdateTrigger.ScriptPre = make([]script.Line, 0)
  119. }
  120. if input.UpdateTrigger.ScriptPost == nil {
  121. input.UpdateTrigger.ScriptPost = make([]script.Line, 0)
  122. }
  123. bus.RunCommand(*input.UpdateTrigger)
  124. case input.DeleteTrigger != nil:
  125. bus.RunCommand(*input.DeleteTrigger)
  126. default:
  127. return c.String(400, "No supported command found in input")
  128. }
  129. return c.JSON(200, input)
  130. })
  131. listener, err := net.Listen("tcp", addr)
  132. if err != nil {
  133. return nil, err
  134. }
  135. e.Listener = listener
  136. go func() {
  137. err := e.Start(addr)
  138. if err != nil {
  139. log.Fatalln("Failed to listen to webserver")
  140. }
  141. }()
  142. return svc, nil
  143. }
  144. type service struct {
  145. mu sync.Mutex
  146. data uistate.Data
  147. bus *lucifer3.EventBus
  148. subs []chan uistate.Patch
  149. }
  150. func (s *service) Active() bool {
  151. return true
  152. }
  153. func (s *service) HandleEvent(bus *lucifer3.EventBus, event lucifer3.Event) {
  154. switch event := event.(type) {
  155. case events.Started:
  156. s.mu.Lock()
  157. s.bus = bus
  158. s.mu.Unlock()
  159. case uistate.Patch:
  160. s.mu.Lock()
  161. s.data = s.data.WithPatch(event)
  162. subs := s.subs
  163. s.mu.Unlock()
  164. for _, sub := range subs {
  165. select {
  166. case sub <- event:
  167. default:
  168. }
  169. }
  170. }
  171. }
  172. func (s *service) addSub(ch chan uistate.Patch) {
  173. s.mu.Lock()
  174. s.subs = append(s.subs, ch)
  175. s.mu.Unlock()
  176. }
  177. func (s *service) removeSub(ch chan uistate.Patch) {
  178. s.mu.Lock()
  179. s.subs = append([]chan uistate.Patch{}, s.subs...)
  180. for i, sub := range s.subs {
  181. if sub == ch {
  182. s.subs = append(s.subs[:i], s.subs[i+1:]...)
  183. break
  184. }
  185. }
  186. s.mu.Unlock()
  187. }
  188. type commandInput struct {
  189. Assign *assignInput `json:"assign,omitempty"`
  190. AddAlias *commands.AddAlias `json:"addAlias,omitempty"`
  191. RemoveAlias *commands.RemoveAlias `json:"removeAlias,omitempty"`
  192. PairDevice *commands.PairDevice `json:"pairDevice,omitempty"`
  193. ConnectDevice *commands.ConnectDevice `json:"connectDevice,omitempty"`
  194. SearchDevices *commands.SearchDevices `json:"searchDevices,omitempty"`
  195. ForgetDevice *commands.ForgetDevice `json:"forgetDevice,omitempty"`
  196. UpdateScript *script.Update `json:"updateScript,omitempty"`
  197. ExecuteScript *script.Execute `json:"executeScript,omitempty"`
  198. UpdateTrigger *script.UpdateTrigger `json:"updateTrigger,omitempty"`
  199. DeleteTrigger *script.DeleteTrigger `json:"deleteTrigger,omitempty"`
  200. }
  201. type assignInput struct {
  202. Match string `json:"match"`
  203. Effect effects.Serializable `json:"effect"`
  204. }