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.

424 lines
8.0 KiB

1 year ago
1 year ago
  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) DeleteResource(ctx context.Context, link ResourceLink) error {
  80. return c.delete(ctx, link.Path(), nil)
  81. }
  82. func (c *Client) LegacyDiscover(ctx context.Context, kind string) error {
  83. return c.legacyPost(ctx, kind, nil, nil)
  84. }
  85. func (c *Client) SSE(ctx context.Context) <-chan SSEUpdate {
  86. ch := make(chan SSEUpdate, 4)
  87. go func() {
  88. defer close(ch)
  89. reader, err := c.getReader(ctx, "/eventstream/clip/v2", map[string]string{
  90. "Accept": "text/event-stream",
  91. })
  92. if err != nil {
  93. log.Println("SSE Connect error:", err)
  94. return
  95. }
  96. defer reader.Close()
  97. br := bufio.NewReader(reader)
  98. for {
  99. line, err := br.ReadString('\n')
  100. if err != nil {
  101. log.Println("SSE Read error:", err)
  102. return
  103. }
  104. line = strings.Trim(line, "  \t\r\n")
  105. kv := strings.SplitN(line, ": ", 2)
  106. if len(kv) < 2 {
  107. continue
  108. }
  109. switch kv[0] {
  110. case "data":
  111. var data []SSEUpdate
  112. err := json.Unmarshal([]byte(kv[1]), &data)
  113. if err != nil {
  114. log.Println("Parsing SSE event failed:", err)
  115. log.Println(" json:", kv[1])
  116. return
  117. }
  118. for _, obj := range data {
  119. ch <- obj
  120. }
  121. }
  122. }
  123. }()
  124. return ch
  125. }
  126. func (c *Client) getReader(ctx context.Context, path string, headers map[string]string) (io.ReadCloser, error) {
  127. select {
  128. case <-ctx.Done():
  129. return nil, ctx.Err()
  130. case <-c.ch:
  131. defer func() {
  132. c.ch <- struct{}{}
  133. }()
  134. }
  135. req, err := http.NewRequest("GET", fmt.Sprintf("https://%s/%s", c.host, path), nil)
  136. if err != nil {
  137. return nil, err
  138. }
  139. for key, value := range headers {
  140. req.Header.Set(key, value)
  141. }
  142. req.Header.Set("hue-application-key", c.token)
  143. client := httpClient
  144. if headers["Accept"] == "text/event-stream" {
  145. client = sseClient
  146. }
  147. res, err := client.Do(req.WithContext(ctx))
  148. if err != nil {
  149. return nil, err
  150. }
  151. if res.StatusCode != 200 {
  152. _ = res.Body.Close()
  153. return nil, errors.New(res.Status)
  154. }
  155. return res.Body, nil
  156. }
  157. func (c *Client) get(ctx context.Context, path string, target interface{}) error {
  158. body, err := c.getReader(ctx, path, map[string]string{})
  159. if err != nil {
  160. return err
  161. }
  162. defer body.Close()
  163. if target == nil {
  164. return nil
  165. }
  166. return json.NewDecoder(body).Decode(&target)
  167. }
  168. func (c *Client) put(ctx context.Context, path string, body interface{}, target interface{}) error {
  169. select {
  170. case <-ctx.Done():
  171. return ctx.Err()
  172. case <-c.ch:
  173. defer func() {
  174. c.ch <- struct{}{}
  175. }()
  176. }
  177. rb, err := reqBody(body)
  178. if err != nil {
  179. return err
  180. }
  181. req, err := http.NewRequest("PUT", fmt.Sprintf("https://%s/%s", c.host, path), rb)
  182. if err != nil {
  183. return err
  184. }
  185. req.Header.Set("hue-application-key", c.token)
  186. res, err := httpClient.Do(req.WithContext(ctx))
  187. if err != nil {
  188. return err
  189. }
  190. defer res.Body.Close()
  191. if target == nil {
  192. return nil
  193. }
  194. return json.NewDecoder(res.Body).Decode(&target)
  195. }
  196. func (c *Client) post(ctx context.Context, path string, body interface{}, target interface{}) error {
  197. select {
  198. case <-ctx.Done():
  199. return ctx.Err()
  200. case <-c.ch:
  201. defer func() {
  202. c.ch <- struct{}{}
  203. }()
  204. }
  205. rb, err := reqBody(body)
  206. if err != nil {
  207. return err
  208. }
  209. req, err := http.NewRequest("POST", fmt.Sprintf("https://%s/%s", c.host, path), rb)
  210. if err != nil {
  211. return err
  212. }
  213. if c.token != "" {
  214. req.Header.Set("hue-application-key", c.token)
  215. }
  216. res, err := httpClient.Do(req.WithContext(ctx))
  217. if err != nil {
  218. return err
  219. }
  220. defer res.Body.Close()
  221. if target == nil {
  222. return nil
  223. }
  224. return json.NewDecoder(res.Body).Decode(&target)
  225. }
  226. func (c *Client) delete(ctx context.Context, path string, target interface{}) error {
  227. select {
  228. case <-ctx.Done():
  229. return ctx.Err()
  230. case <-c.ch:
  231. defer func() {
  232. c.ch <- struct{}{}
  233. }()
  234. }
  235. req, err := http.NewRequest("PUT", fmt.Sprintf("https://%s/%s", c.host, path), nil)
  236. if err != nil {
  237. return err
  238. }
  239. req.Header.Set("hue-application-key", c.token)
  240. res, err := httpClient.Do(req.WithContext(ctx))
  241. if err != nil {
  242. return err
  243. }
  244. defer res.Body.Close()
  245. if target == nil {
  246. return nil
  247. }
  248. return json.NewDecoder(res.Body).Decode(&target)
  249. }
  250. func (c *Client) legacyPost(ctx context.Context, resource string, body interface{}, target interface{}) error {
  251. select {
  252. case <-ctx.Done():
  253. return ctx.Err()
  254. case <-c.ch:
  255. defer func() {
  256. c.ch <- struct{}{}
  257. }()
  258. }
  259. rb, err := reqBody(body)
  260. if err != nil {
  261. return err
  262. }
  263. if c.token != "" {
  264. resource = c.token + "/" + resource
  265. }
  266. req, err := http.NewRequest("POST", fmt.Sprintf("http://%s/api/%s", c.host, resource), rb)
  267. if err != nil {
  268. return err
  269. }
  270. res, err := httpClient.Do(req.WithContext(ctx))
  271. if err != nil {
  272. return err
  273. }
  274. defer res.Body.Close()
  275. if target == nil {
  276. return nil
  277. }
  278. return json.NewDecoder(res.Body).Decode(target)
  279. }
  280. func (c *Client) legacyDelete(ctx context.Context, resource string, target interface{}) error {
  281. select {
  282. case <-ctx.Done():
  283. return ctx.Err()
  284. case <-c.ch:
  285. defer func() {
  286. c.ch <- struct{}{}
  287. }()
  288. }
  289. if c.token != "" {
  290. resource = c.token + "/" + resource
  291. }
  292. req, err := http.NewRequest("DELETE", fmt.Sprintf("http://%s/api/%s", c.host, resource), nil)
  293. if err != nil {
  294. return err
  295. }
  296. res, err := httpClient.Do(req.WithContext(ctx))
  297. if err != nil {
  298. return err
  299. }
  300. defer res.Body.Close()
  301. if target == nil {
  302. return nil
  303. }
  304. return json.NewDecoder(res.Body).Decode(target)
  305. }
  306. func reqBody(body interface{}) (io.Reader, error) {
  307. if body == nil {
  308. return nil, nil
  309. }
  310. switch v := body.(type) {
  311. case []byte:
  312. return bytes.NewReader(v), nil
  313. case string:
  314. return strings.NewReader(v), nil
  315. case io.Reader:
  316. return v, nil
  317. default:
  318. jsonData, err := json.Marshal(v)
  319. if err != nil {
  320. return nil, err
  321. }
  322. return bytes.NewReader(jsonData), nil
  323. }
  324. }
  325. var sseClient = &http.Client{
  326. Transport: &http.Transport{
  327. Proxy: http.ProxyFromEnvironment,
  328. DialContext: (&net.Dialer{
  329. Timeout: time.Hour * 1000000,
  330. KeepAlive: 30 * time.Second,
  331. }).DialContext,
  332. MaxIdleConns: 5,
  333. MaxIdleConnsPerHost: 1,
  334. IdleConnTimeout: 0,
  335. TLSClientConfig: &tls.Config{InsecureSkipVerify: true},
  336. },
  337. Timeout: time.Hour * 1000000,
  338. }
  339. var httpClient = &http.Client{
  340. Transport: &http.Transport{
  341. Proxy: http.ProxyFromEnvironment,
  342. DialContext: (&net.Dialer{
  343. Timeout: 30 * time.Second,
  344. KeepAlive: 30 * time.Second,
  345. }).DialContext,
  346. MaxIdleConns: 256,
  347. MaxIdleConnsPerHost: 16,
  348. IdleConnTimeout: 10 * time.Minute,
  349. TLSClientConfig: &tls.Config{InsecureSkipVerify: true},
  350. },
  351. Timeout: time.Minute,
  352. }