Api.js 5.1 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150
  1. import {
  2. fetchLinesIterator,
  3. maybeWithAuth,
  4. topicShortUrl,
  5. topicUrl,
  6. topicUrlAuth,
  7. topicUrlJsonPoll,
  8. topicUrlJsonPollWithSince,
  9. accountWebPushUrl,
  10. } from "./utils";
  11. import userManager from "./UserManager";
  12. import { fetchOrThrow } from "./errors";
  13. class Api {
  14. async poll(baseUrl, topic, since) {
  15. const user = await userManager.get(baseUrl);
  16. const shortUrl = topicShortUrl(baseUrl, topic);
  17. const url = since ? topicUrlJsonPollWithSince(baseUrl, topic, since) : topicUrlJsonPoll(baseUrl, topic);
  18. const messages = [];
  19. const headers = maybeWithAuth({}, user);
  20. console.log(`[Api] Polling ${url}`);
  21. for await (const line of fetchLinesIterator(url, headers)) {
  22. const message = JSON.parse(line);
  23. if (message.id) {
  24. console.log(`[Api, ${shortUrl}] Received message ${line}`);
  25. messages.push(message);
  26. }
  27. }
  28. return messages;
  29. }
  30. async publish(baseUrl, topic, message, options) {
  31. const user = await userManager.get(baseUrl);
  32. console.log(`[Api] Publishing message to ${topicUrl(baseUrl, topic)}`);
  33. const headers = {};
  34. const body = {
  35. topic,
  36. message,
  37. ...options,
  38. };
  39. await fetchOrThrow(baseUrl, {
  40. method: "PUT",
  41. body: JSON.stringify(body),
  42. headers: maybeWithAuth(headers, user),
  43. });
  44. }
  45. /**
  46. * Publishes to a topic using XMLHttpRequest (XHR), and returns a Promise with the active request.
  47. * Unfortunately, fetch() does not support a progress hook, which is why XHR has to be used.
  48. *
  49. * Firefox XHR bug:
  50. * Firefox has a bug(?), which returns 0 and "" for all fields of the XHR response in the case of an error,
  51. * so we cannot determine the exact error. It also sometimes complains about CORS violations, even when the
  52. * correct headers are clearly set. It's quite the odd behavior.
  53. *
  54. * There is an example, and the bug report here:
  55. * - https://bugzilla.mozilla.org/show_bug.cgi?id=1733755
  56. * - https://gist.github.com/binwiederhier/627f146d1959799be207ad8c17a8f345
  57. */
  58. publishXHR(url, body, headers, onProgress) {
  59. console.log(`[Api] Publishing message to ${url}`);
  60. const xhr = new XMLHttpRequest();
  61. const send = new Promise((resolve, reject) => {
  62. xhr.open("PUT", url);
  63. if (body.type) {
  64. xhr.overrideMimeType(body.type);
  65. }
  66. for (const [key, value] of Object.entries(headers)) {
  67. xhr.setRequestHeader(key, value);
  68. }
  69. xhr.upload.addEventListener("progress", onProgress);
  70. xhr.addEventListener("readystatechange", () => {
  71. if (xhr.readyState === 4 && xhr.status >= 200 && xhr.status <= 299) {
  72. console.log(`[Api] Publish successful (HTTP ${xhr.status})`, xhr.response);
  73. resolve(xhr.response);
  74. } else if (xhr.readyState === 4) {
  75. // Firefox bug; see description above!
  76. console.log(`[Api] Publish failed (HTTP ${xhr.status})`, xhr.responseText);
  77. let errorText;
  78. try {
  79. const error = JSON.parse(xhr.responseText);
  80. if (error.code && error.error) {
  81. errorText = `Error ${error.code}: ${error.error}`;
  82. }
  83. } catch (e) {
  84. // Nothing
  85. }
  86. xhr.abort();
  87. reject(errorText ?? "An error occurred");
  88. }
  89. });
  90. xhr.send(body);
  91. });
  92. send.abort = () => {
  93. console.log(`[Api] Publish aborted by user`);
  94. xhr.abort();
  95. };
  96. return send;
  97. }
  98. async topicAuth(baseUrl, topic, user) {
  99. const url = topicUrlAuth(baseUrl, topic);
  100. console.log(`[Api] Checking auth for ${url}`);
  101. const response = await fetch(url, {
  102. headers: maybeWithAuth({}, user),
  103. });
  104. if (response.status >= 200 && response.status <= 299) {
  105. return true;
  106. }
  107. if (response.status === 401 || response.status === 403) {
  108. // See server/server.go
  109. return false;
  110. }
  111. throw new Error(`Unexpected server response ${response.status}`);
  112. }
  113. async updateWebPush(pushSubscription, topics) {
  114. const user = await userManager.get(config.base_url);
  115. const url = accountWebPushUrl(config.base_url);
  116. console.log(`[Api] Updating Web Push subscription`, { url, topics, endpoint: pushSubscription.endpoint });
  117. const serializedSubscription = JSON.parse(JSON.stringify(pushSubscription)); // Ugh ... https://stackoverflow.com/a/40525434/1440785
  118. await fetchOrThrow(url, {
  119. method: "POST",
  120. headers: maybeWithAuth({}, user),
  121. body: JSON.stringify({
  122. endpoint: serializedSubscription.endpoint,
  123. auth: serializedSubscription.keys.auth,
  124. p256dh: serializedSubscription.keys.p256dh,
  125. topics,
  126. }),
  127. });
  128. }
  129. async deleteWebPush(pushSubscription) {
  130. const user = await userManager.get(config.base_url);
  131. const url = accountWebPushUrl(config.base_url);
  132. console.log(`[Api] Deleting Web Push subscription`, { url, endpoint: pushSubscription.endpoint });
  133. await fetchOrThrow(url, {
  134. method: "DELETE",
  135. headers: maybeWithAuth({}, user),
  136. body: JSON.stringify({
  137. endpoint: pushSubscription.endpoint,
  138. }),
  139. });
  140. }
  141. }
  142. const api = new Api();
  143. export default api;