backend.js 4.8 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202
  1. /*
  2. * Sapphire
  3. *
  4. * Copyright (C) 2018 Florrie Haero
  5. * Copyright (C) 2018 Alyssa Rosenzweig
  6. *
  7. * This program is free software; you can redistribute it and/or modify
  8. * it under the terms of the GNU General Public License as published by
  9. * the Free Software Foundation; either version 2 of the License, or
  10. * (at your option) any later version.
  11. *
  12. * This program is distributed in the hope that it will be useful,
  13. * but WITHOUT ANY WARRANTY; without even the implied warranty of
  14. * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
  15. * GNU General Public License for more details.
  16. *
  17. * You should have received a copy of the GNU General Public License
  18. * along with this program; if not, write to the Free Software
  19. * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02111-1301 USA
  20. *
  21. */
  22. /* The active socket itself */
  23. let backendWS = null;
  24. /* Delay connecting to the websocket to prevent race conditions */
  25. const WS_PORT = 7070;
  26. window.backendConnect = function(username, passwordHash) {
  27. const protocol = 'ws' + (window.location.protocol === 'https:' ? 's': '') + '://';
  28. const url = protocol + window.BASE_HOST + ':' + WS_PORT + '/ws';
  29. /* Whether we ever managed to connect */
  30. let once_connected = false;
  31. console.log(`Connecting to ${url}...`);
  32. backendWS = new WebSocket(url);
  33. backendWS.onopen = function() {
  34. console.log('open');
  35. once_connected = true;
  36. backendWS.send(JSON.stringify({username, passwordHash}));
  37. };
  38. backendWS.onclose = function() {
  39. console.log('closed');
  40. sapphireCallHandler('wsclosed', once_connected);
  41. };
  42. backendWS.onmessage = sapphireGotMessage;
  43. };
  44. /* Maintain set of buddy/chats IDs */
  45. const backendIDType = {};
  46. /* Handlers to be filled in by the frontend */
  47. window.backendHooks = {
  48. message: null,
  49. batchMessage: null,
  50. newBuddy: null,
  51. newChat: null,
  52. newAccount: null,
  53. changeAvatar: null,
  54. joined: null,
  55. typing: null,
  56. topic: null,
  57. wsclosed: null
  58. };
  59. /* Serialize and send off an event to the server */
  60. function sendEvent(data) {
  61. backendWS.send(JSON.stringify(data));
  62. }
  63. function sendEventWithTarget(data, id) {
  64. data[backendIDType[id]] = id;
  65. sendEvent(data);
  66. }
  67. /* Events exposed to the frontend */
  68. window.backendMessage = (target, content) => {
  69. sendEventWithTarget({
  70. op: 'message', content
  71. }, target);
  72. };
  73. window.backendTyping = (target, state) => {
  74. sendEventWithTarget({
  75. op: 'typing', state
  76. }, target);
  77. };
  78. window.backendJoinChat = (id) => {
  79. sendEvent({
  80. op: 'joinChat', id
  81. });
  82. };
  83. window.backendMarkAsRead = (id) => {
  84. sendEvent({
  85. op: 'markAsRead', id
  86. });
  87. };
  88. window.backendRequestBuddy = (account, id, alias, invite) => {
  89. sendEvent({
  90. op: 'requestBuddy', account, id, alias, invite
  91. });
  92. };
  93. window.backendChangeAvatar = (account, base64) => {
  94. sendEvent({
  95. op: 'changeAvatar', account, base64
  96. });
  97. };
  98. /* Internal handlers */
  99. function sapphireGotWorld(data) {
  100. /* A world packet contains the buddy list and the account list. Iterate
  101. * these separately and defer to the usual handler */
  102. for (const account of data.accounts) {
  103. window.backendHooks.newAccount(account);
  104. }
  105. /* Make sure accounts are enabled before replaying messages */
  106. /* Function to replay missed messages, in the right (reversed) order */
  107. const unack = function(obj) {
  108. if (obj.unacked) {
  109. const unacked = obj.unacked.reverse();
  110. for (const message of unacked) {
  111. window.backendHooks.batchMessage(message);
  112. }
  113. }
  114. window.backendHooks.flushBatched();
  115. };
  116. for (const buddy of data.buddies) {
  117. backendIDType[buddy.id] = 'buddy';
  118. window.backendHooks.newBuddy(buddy);
  119. unack(buddy);
  120. }
  121. for (const chat of data.chats) {
  122. backendIDType[chat.id] = 'chat';
  123. window.backendHooks.newChat(chat);
  124. unack(chat);
  125. }
  126. }
  127. const sapphireCallHandler = (op, ...args) => {
  128. /* Handlers for internal use */
  129. const internalHandlers = {
  130. world: sapphireGotWorld
  131. };
  132. /* Invoke a handler */
  133. const handler = window.backendHooks[op] || internalHandlers[op];
  134. if (handler) {
  135. handler(...args);
  136. } else {
  137. console.error('Missing handler for op: ' + op);
  138. }
  139. };
  140. const sapphireGotMessage = (event) => {
  141. /* On receiving a message, we need to decode it (from JSON) and then
  142. * process it accordingly */
  143. let obj;
  144. const str = event.data;
  145. try {
  146. obj = JSON.parse(str);
  147. } catch(e) {
  148. console.error('Parse error\n');
  149. console.error('> ' + str);
  150. console.error(e);
  151. return;
  152. }
  153. console.log(obj);
  154. /* Parsed, now invoke a handler to actually do something with it */
  155. sapphireCallHandler(obj.op, obj);
  156. };