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.

358 lines
6.8 KiB

  1. package hue2
  2. import (
  3. "bufio"
  4. "bytes"
  5. "context"
  6. "crypto/tls"
  7. "encoding/json"
  8. "errors"
  9. "fmt"
  10. "git.aiterp.net/lucifer/new-server/internal/lerrors"
  11. "io"
  12. "log"
  13. "net"
  14. "net/http"
  15. "strings"
  16. "time"
  17. )
  18. func NewClient(host, token string) *Client {
  19. ch := make(chan struct{}, 5)
  20. for i := 0; i < 2; i++ {
  21. ch <- struct{}{}
  22. }
  23. return &Client{
  24. host: host,
  25. token: token,
  26. ch: ch,
  27. }
  28. }
  29. type Client struct {
  30. host string
  31. token string
  32. ch chan struct{}
  33. }
  34. func (c *Client) Register(ctx context.Context) (string, error) {
  35. result := make([]CreateUserResponse, 0, 1)
  36. err := c.post(ctx, "api/", CreateUserInput{DeviceType: "git.aiterp.net/lucifer"}, &result)
  37. if err != nil {
  38. return "", err
  39. }
  40. if len(result) == 0 || result[0].Error != nil {
  41. select {
  42. case <-ctx.Done():
  43. return "", ctx.Err()
  44. case <-time.After(time.Second):
  45. return c.Register(ctx)
  46. }
  47. }
  48. if result[0].Success == nil {
  49. return "", lerrors.ErrUnexpectedResponse
  50. }
  51. c.token = result[0].Success.Username
  52. return result[0].Success.Username, nil
  53. }
  54. func (c *Client) AllResources(ctx context.Context) ([]ResourceData, error) {
  55. res := struct {
  56. Error interface{}
  57. Data []ResourceData
  58. }{}
  59. err := c.get(ctx, "clip/v2/resource", &res)
  60. if err != nil {
  61. return nil, err
  62. }
  63. return res.Data, nil
  64. }
  65. func (c *Client) Resources(ctx context.Context, kind string) ([]ResourceData, error) {
  66. res := struct {
  67. Error interface{}
  68. Data []ResourceData
  69. }{}
  70. err := c.get(ctx, "clip/v2/resource/"+kind, &res)
  71. if err != nil {
  72. return nil, err
  73. }
  74. return res.Data, nil
  75. }
  76. func (c *Client) UpdateResource(ctx context.Context, link ResourceLink, update ResourceUpdate) error {
  77. return c.put(ctx, link.Path(), update, nil)
  78. }
  79. func (c *Client) LegacyDiscover(ctx context.Context, kind string) error {
  80. return c.legacyPost(ctx, kind, nil, nil)
  81. }
  82. func (c *Client) SSE(ctx context.Context) <-chan SSEUpdate {
  83. ch := make(chan SSEUpdate, 4)
  84. go func() {
  85. defer close(ch)
  86. reader, err := c.getReader(ctx, "/eventstream/clip/v2", map[string]string{
  87. "Accept": "text/event-stream",
  88. })
  89. if err != nil {
  90. log.Println("SSE Connect error:", err)
  91. return
  92. }
  93. defer reader.Close()
  94. br := bufio.NewReader(reader)
  95. for {
  96. line, err := br.ReadString('\n')
  97. if err != nil {
  98. log.Println("SSE Read error:", err)
  99. return
  100. }
  101. line = strings.Trim(line, "  \t\r\n")
  102. kv := strings.SplitN(line, ": ", 2)
  103. if len(kv) < 2 {
  104. continue
  105. }
  106. switch kv[0] {
  107. case "data":
  108. var data []SSEUpdate
  109. err := json.Unmarshal([]byte(kv[1]), &data)
  110. if err != nil {
  111. log.Println("Parsing SSE event failed:", err)
  112. log.Println(" json:", kv[1])
  113. return
  114. }
  115. for _, obj := range data {
  116. ch <- obj
  117. }
  118. }
  119. }
  120. }()
  121. return ch
  122. }
  123. func (c *Client) getReader(ctx context.Context, path string, headers map[string]string) (io.ReadCloser, error) {
  124. select {
  125. case <-ctx.Done():
  126. return nil, ctx.Err()
  127. case <-c.ch:
  128. defer func() {
  129. c.ch <- struct{}{}
  130. }()
  131. }
  132. req, err := http.NewRequest("GET", fmt.Sprintf("https://%s/%s", c.host, path), nil)
  133. if err != nil {
  134. return nil, err
  135. }
  136. for key, value := range headers {
  137. req.Header.Set(key, value)
  138. }
  139. req.Header.Set("hue-application-key", c.token)
  140. client := httpClient
  141. if headers["Accept"] == "text/event-stream" {
  142. client = sseClient
  143. }
  144. res, err := client.Do(req.WithContext(ctx))
  145. if err != nil {
  146. return nil, err
  147. }
  148. if res.StatusCode != 200 {
  149. _ = res.Body.Close()
  150. return nil, errors.New(res.Status)
  151. }
  152. return res.Body, nil
  153. }
  154. func (c *Client) get(ctx context.Context, path string, target interface{}) error {
  155. body, err := c.getReader(ctx, path, map[string]string{})
  156. if err != nil {
  157. return err
  158. }
  159. defer body.Close()
  160. if target == nil {
  161. return nil
  162. }
  163. return json.NewDecoder(body).Decode(&target)
  164. }
  165. func (c *Client) put(ctx context.Context, path string, body interface{}, target interface{}) error {
  166. select {
  167. case <-ctx.Done():
  168. return ctx.Err()
  169. case <-c.ch:
  170. defer func() {
  171. c.ch <- struct{}{}
  172. }()
  173. }
  174. rb, err := reqBody(body)
  175. if err != nil {
  176. return err
  177. }
  178. req, err := http.NewRequest("PUT", fmt.Sprintf("https://%s/%s", c.host, path), rb)
  179. if err != nil {
  180. return err
  181. }
  182. req.Header.Set("hue-application-key", c.token)
  183. res, err := httpClient.Do(req.WithContext(ctx))
  184. if err != nil {
  185. return err
  186. }
  187. defer res.Body.Close()
  188. if target == nil {
  189. return nil
  190. }
  191. return json.NewDecoder(res.Body).Decode(&target)
  192. }
  193. func (c *Client) post(ctx context.Context, path string, body interface{}, target interface{}) error {
  194. select {
  195. case <-ctx.Done():
  196. return ctx.Err()
  197. case <-c.ch:
  198. defer func() {
  199. c.ch <- struct{}{}
  200. }()
  201. }
  202. rb, err := reqBody(body)
  203. if err != nil {
  204. return err
  205. }
  206. req, err := http.NewRequest("POST", fmt.Sprintf("https://%s/%s", c.host, path), rb)
  207. if err != nil {
  208. return err
  209. }
  210. if c.token != "" {
  211. req.Header.Set("hue-application-key", c.token)
  212. }
  213. res, err := httpClient.Do(req.WithContext(ctx))
  214. if err != nil {
  215. return err
  216. }
  217. defer res.Body.Close()
  218. if target == nil {
  219. return nil
  220. }
  221. return json.NewDecoder(res.Body).Decode(&target)
  222. }
  223. func (c *Client) legacyPost(ctx context.Context, resource string, body interface{}, target interface{}) error {
  224. select {
  225. case <-ctx.Done():
  226. return ctx.Err()
  227. case <-c.ch:
  228. defer func() {
  229. c.ch <- struct{}{}
  230. }()
  231. }
  232. rb, err := reqBody(body)
  233. if err != nil {
  234. return err
  235. }
  236. if c.token != "" {
  237. resource = c.token + "/" + resource
  238. }
  239. req, err := http.NewRequest("POST", fmt.Sprintf("http://%s/api/%s", c.host, resource), rb)
  240. if err != nil {
  241. return err
  242. }
  243. res, err := httpClient.Do(req.WithContext(ctx))
  244. if err != nil {
  245. return err
  246. }
  247. defer res.Body.Close()
  248. if target == nil {
  249. return nil
  250. }
  251. return json.NewDecoder(res.Body).Decode(target)
  252. }
  253. func reqBody(body interface{}) (io.Reader, error) {
  254. if body == nil {
  255. return nil, nil
  256. }
  257. switch v := body.(type) {
  258. case []byte:
  259. return bytes.NewReader(v), nil
  260. case string:
  261. return strings.NewReader(v), nil
  262. case io.Reader:
  263. return v, nil
  264. default:
  265. jsonData, err := json.Marshal(v)
  266. if err != nil {
  267. return nil, err
  268. }
  269. return bytes.NewReader(jsonData), nil
  270. }
  271. }
  272. var sseClient = &http.Client{
  273. Transport: &http.Transport{
  274. Proxy: http.ProxyFromEnvironment,
  275. DialContext: (&net.Dialer{
  276. Timeout: time.Hour * 1000000,
  277. KeepAlive: 30 * time.Second,
  278. }).DialContext,
  279. MaxIdleConns: 5,
  280. MaxIdleConnsPerHost: 1,
  281. IdleConnTimeout: 0,
  282. TLSClientConfig: &tls.Config{InsecureSkipVerify: true},
  283. },
  284. Timeout: time.Hour * 1000000,
  285. }
  286. var httpClient = &http.Client{
  287. Transport: &http.Transport{
  288. Proxy: http.ProxyFromEnvironment,
  289. DialContext: (&net.Dialer{
  290. Timeout: 30 * time.Second,
  291. KeepAlive: 30 * time.Second,
  292. }).DialContext,
  293. MaxIdleConns: 256,
  294. MaxIdleConnsPerHost: 16,
  295. IdleConnTimeout: 10 * time.Minute,
  296. TLSClientConfig: &tls.Config{InsecureSkipVerify: true},
  297. },
  298. Timeout: time.Minute,
  299. }