utils.js 5.0 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161
  1. import {rawEmojis} from "./emojis";
  2. export const topicUrl = (baseUrl, topic) => `${baseUrl}/${topic}`;
  3. export const topicUrlWs = (baseUrl, topic) => `${topicUrl(baseUrl, topic)}/ws`
  4. .replaceAll("https://", "wss://")
  5. .replaceAll("http://", "ws://");
  6. export const topicUrlJson = (baseUrl, topic) => `${topicUrl(baseUrl, topic)}/json`;
  7. export const topicUrlJsonPoll = (baseUrl, topic) => `${topicUrlJson(baseUrl, topic)}?poll=1`;
  8. export const topicUrlJsonPollWithSince = (baseUrl, topic, since) => `${topicUrlJson(baseUrl, topic)}?poll=1&since=${since}`;
  9. export const topicUrlAuth = (baseUrl, topic) => `${topicUrl(baseUrl, topic)}/auth`;
  10. export const topicShortUrl = (baseUrl, topic) => shortUrl(topicUrl(baseUrl, topic));
  11. export const shortUrl = (url) => url.replaceAll(/https?:\/\//g, "");
  12. export const expandUrl = (url) => [`https://${url}`, `http://${url}`];
  13. export const validUrl = (url) => {
  14. return url.match(/^https?:\/\//);
  15. }
  16. export const validTopic = (topic) => {
  17. return topic.match(/^([-_a-zA-Z0-9]{1,64})$/); // Regex must match Go & Android app!
  18. }
  19. // Format emojis (see emoji.js)
  20. const emojis = {};
  21. rawEmojis.forEach(emoji => {
  22. emoji.aliases.forEach(alias => {
  23. emojis[alias] = emoji.emoji;
  24. });
  25. });
  26. const toEmojis = (tags) => {
  27. if (!tags) return [];
  28. else return tags.filter(tag => tag in emojis).map(tag => emojis[tag]);
  29. }
  30. export const formatTitleWithDefault = (m, fallback) => {
  31. if (m.title) {
  32. return formatTitle(m);
  33. }
  34. return fallback;
  35. };
  36. export const formatTitle = (m) => {
  37. const emojiList = toEmojis(m.tags);
  38. if (emojiList.length > 0) {
  39. return `${emojiList.join(" ")} ${m.title}`;
  40. } else {
  41. return m.title;
  42. }
  43. };
  44. export const formatMessage = (m) => {
  45. if (m.title) {
  46. return m.message;
  47. } else {
  48. const emojiList = toEmojis(m.tags);
  49. if (emojiList.length > 0) {
  50. return `${emojiList.join(" ")} ${m.message}`;
  51. } else {
  52. return m.message;
  53. }
  54. }
  55. };
  56. export const unmatchedTags = (tags) => {
  57. if (!tags) return [];
  58. else return tags.filter(tag => !(tag in emojis));
  59. }
  60. export const maybeWithBasicAuth = (headers, user) => {
  61. if (user) {
  62. headers['Authorization'] = `Basic ${encodeBase64(`${user.username}:${user.password}`)}`;
  63. }
  64. return headers;
  65. }
  66. export const basicAuth = (username, password) => {
  67. return `Basic ${encodeBase64(`${username}:${password}`)}`;
  68. }
  69. export const encodeBase64 = (s) => {
  70. return new Buffer(s).toString('base64');
  71. }
  72. export const encodeBase64Url = (s) => {
  73. return encodeBase64(s)
  74. .replaceAll('+', '-')
  75. .replaceAll('/', '_')
  76. .replaceAll('=', '');
  77. }
  78. // https://jameshfisher.com/2017/10/30/web-cryptography-api-hello-world/
  79. export const sha256 = async (s) => {
  80. const buf = await crypto.subtle.digest("SHA-256", new TextEncoder("utf-8").encode(s));
  81. return Array.prototype.map.call(new Uint8Array(buf), x=>(('00'+x.toString(16)).slice(-2))).join('');
  82. }
  83. export const formatShortDateTime = (timestamp) => {
  84. return new Intl.DateTimeFormat('default', {dateStyle: 'short', timeStyle: 'short'})
  85. .format(new Date(timestamp * 1000));
  86. }
  87. export const formatBytes = (bytes, decimals = 2) => {
  88. if (bytes === 0) return '0 bytes';
  89. const k = 1024;
  90. const dm = decimals < 0 ? 0 : decimals;
  91. const sizes = ['bytes', 'KB', 'MB', 'GB', 'TB', 'PB', 'EB', 'ZB', 'YB'];
  92. const i = Math.floor(Math.log(bytes) / Math.log(k));
  93. return parseFloat((bytes / Math.pow(k, i)).toFixed(dm)) + ' ' + sizes[i];
  94. }
  95. export const openUrl = (url) => {
  96. window.open(url, "_blank", "noopener,noreferrer");
  97. };
  98. export const subscriptionRoute = (subscription) => {
  99. if (subscription.baseUrl !== window.location.origin) {
  100. return `/${shortUrl(subscription.baseUrl)}/${subscription.topic}`;
  101. }
  102. return `/${subscription.topic}`;
  103. }
  104. export const playSound = async (sound) => {
  105. const audio = new Audio(`/static/sounds/${sound}.mp3`);
  106. return audio.play();
  107. };
  108. // From: https://developer.mozilla.org/en-US/docs/Web/API/Fetch_API/Using_Fetch
  109. export async function* fetchLinesIterator(fileURL, headers) {
  110. const utf8Decoder = new TextDecoder('utf-8');
  111. const response = await fetch(fileURL, {
  112. headers: headers
  113. });
  114. const reader = response.body.getReader();
  115. let { value: chunk, done: readerDone } = await reader.read();
  116. chunk = chunk ? utf8Decoder.decode(chunk) : '';
  117. const re = /\n|\r|\r\n/gm;
  118. let startIndex = 0;
  119. for (;;) {
  120. let result = re.exec(chunk);
  121. if (!result) {
  122. if (readerDone) {
  123. break;
  124. }
  125. let remainder = chunk.substr(startIndex);
  126. ({ value: chunk, done: readerDone } = await reader.read());
  127. chunk = remainder + (chunk ? utf8Decoder.decode(chunk) : '');
  128. startIndex = re.lastIndex = 0;
  129. continue;
  130. }
  131. yield chunk.substring(startIndex, result.index);
  132. startIndex = re.lastIndex;
  133. }
  134. if (startIndex < chunk.length) {
  135. yield chunk.substr(startIndex); // last line didn't end in a newline char
  136. }
  137. }