peers.js 16 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591
  1. /* This Source Code Form is subject to the terms of the Mozilla Public
  2. * License, v. 2.0. If a copy of the MPL was not distributed with this file,
  3. * You can obtain one at http://mozilla.org/MPL/2.0/. */
  4. define(["util", "session", "storage", "require", "templates"], function (util, session, storage, require, templates) {
  5. var peers = util.Module("peers");
  6. var assert = util.assert;
  7. var CHECK_ACTIVITY_INTERVAL = 10*1000; // Every 10 seconds see if someone has gone idle
  8. var IDLE_TIME = 3*60*1000; // Idle time is 3 minutes
  9. var TAB_IDLE_TIME = 2*60*1000; // When you tab away, after two minutes you'll say you are idle
  10. var BYE_TIME = 10*60*1000; // After 10 minutes of inactivity the person is considered to be "gone"
  11. var ui;
  12. require(["ui"], function (uiModule) {
  13. ui = uiModule;
  14. });
  15. var DEFAULT_NICKNAMES = templates("names").split(/,\s*/g);
  16. var Peer = util.Class({
  17. isSelf: false,
  18. constructor: function (id, attrs) {
  19. attrs = attrs || {};
  20. assert(id);
  21. assert(! Peer.peers[id]);
  22. this.id = id;
  23. this.identityId = attrs.identityId || null;
  24. this.status = attrs.status || "live";
  25. this.idle = attrs.status || "active";
  26. this.name = attrs.name || null;
  27. this.avatar = attrs.avatar || null;
  28. this.color = attrs.color || "#00FF00";
  29. this.view = ui.PeerView(this);
  30. this.lastMessageDate = 0;
  31. this.following = attrs.following || false;
  32. Peer.peers[id] = this;
  33. var joined = attrs.joined || false;
  34. if (attrs.fromHelloMessage) {
  35. this.updateFromHello(attrs.fromHelloMessage);
  36. if (attrs.fromHelloMessage.type == "hello") {
  37. joined = true;
  38. }
  39. }
  40. peers.emit("new-peer", this);
  41. if (joined) {
  42. this.view.notifyJoined();
  43. }
  44. this.view.update();
  45. },
  46. repr: function () {
  47. return "Peer(" + JSON.stringify(this.id) + ")";
  48. },
  49. serialize: function () {
  50. return {
  51. id: this.id,
  52. status: this.status,
  53. idle: this.idle,
  54. url: this.url,
  55. hash: this.hash,
  56. title: this.title,
  57. identityId: this.identityId,
  58. rtcSupported: this.rtcSupported,
  59. name: this.name,
  60. avatar: this.avatar,
  61. color: this.color,
  62. following: this.following
  63. };
  64. },
  65. destroy: function () {
  66. this.view.destroy();
  67. delete Peer.peers[this.id];
  68. },
  69. updateMessageDate: function (msg) {
  70. if (this.idle == "inactive") {
  71. this.update({idle: "active"});
  72. }
  73. if (this.status == "bye") {
  74. this.unbye();
  75. }
  76. this.lastMessageDate = Date.now();
  77. },
  78. updateFromHello: function (msg) {
  79. var urlUpdated = false;
  80. var activeRTC = false;
  81. var identityUpdated = false;
  82. if (msg.url && msg.url != this.url) {
  83. this.url = msg.url;
  84. this.hash = null;
  85. this.title = null;
  86. urlUpdated = true;
  87. }
  88. if (msg.hash != this.hash) {
  89. this.hash = msg.urlHash;
  90. urlUpdated = true;
  91. }
  92. if (msg.title != this.title) {
  93. this.title = msg.title;
  94. urlUpdated = true;
  95. }
  96. if (msg.rtcSupported !== undefined) {
  97. this.rtcSupported = msg.rtcSupported;
  98. }
  99. if (msg.identityId !== undefined) {
  100. this.identityId = msg.identityId;
  101. }
  102. if (msg.name && msg.name != this.name) {
  103. this.name = msg.name;
  104. identityUpdated = true;
  105. }
  106. if (msg.avatar && msg.avatar != this.avatar) {
  107. util.assertValidUrl(msg.avatar);
  108. this.avatar = msg.avatar;
  109. identityUpdated = true;
  110. }
  111. if (msg.color && msg.color != this.color) {
  112. this.color = msg.color;
  113. identityUpdated = true;
  114. }
  115. if (msg.isClient !== undefined) {
  116. this.isCreator = ! msg.isClient;
  117. }
  118. if (this.status != "live") {
  119. this.status = "live";
  120. peers.emit("status-updated", this);
  121. }
  122. if (this.idle != "active") {
  123. this.idle = "active";
  124. peers.emit("idle-updated", this);
  125. }
  126. if (msg.rtcSupported) {
  127. peers.emit("rtc-supported", this);
  128. }
  129. if (urlUpdated) {
  130. peers.emit("url-updated", this);
  131. }
  132. if (identityUpdated) {
  133. peers.emit("identity-updated", this);
  134. }
  135. // FIXME: I can't decide if this is the only time we need to emit
  136. // this message (and not .update() or other methods)
  137. if (this.following) {
  138. session.emit("follow-peer", this);
  139. }
  140. },
  141. update: function (attrs) {
  142. // FIXME: should probably test that only a couple attributes are settable
  143. // particularly status and idle
  144. if (attrs.idle) {
  145. this.idle = attrs.idle;
  146. }
  147. if (attrs.status) {
  148. this.status = attrs.status;
  149. }
  150. this.view.update();
  151. },
  152. className: function (prefix) {
  153. prefix = prefix || "";
  154. return prefix + util.safeClassName(this.id);
  155. },
  156. bye: function () {
  157. if (this.status != "bye") {
  158. this.status = "bye";
  159. peers.emit("status-updated", this);
  160. }
  161. this.view.update();
  162. },
  163. unbye: function () {
  164. if (this.status == "bye") {
  165. this.status = "live";
  166. peers.emit("status-updated", this);
  167. }
  168. this.view.update();
  169. },
  170. nudge: function () {
  171. session.send({
  172. type: "url-change-nudge",
  173. url: location.href,
  174. to: this.id
  175. });
  176. },
  177. follow: function () {
  178. if (this.following) {
  179. return;
  180. }
  181. peers.getAllPeers().forEach(function (p) {
  182. if (p.following) {
  183. p.unfollow();
  184. }
  185. });
  186. this.following = true;
  187. // We have to make sure we remember this, even if we change URLs:
  188. storeSerialization();
  189. this.view.update();
  190. session.emit("follow-peer", this);
  191. },
  192. unfollow: function () {
  193. this.following = false;
  194. storeSerialization();
  195. this.view.update();
  196. }
  197. });
  198. // FIXME: I can't decide where this should actually go, seems weird
  199. // that it is emitted and handled in the same module
  200. session.on("follow-peer", function (peer) {
  201. if (peer.url != session.currentUrl()) {
  202. var url = peer.url;
  203. if (peer.urlHash) {
  204. url += peer.urlHash;
  205. }
  206. location.href = url;
  207. }
  208. });
  209. Peer.peers = {};
  210. Peer.deserialize = function (obj) {
  211. obj.fromStorage = true;
  212. var peer = Peer(obj.id, obj);
  213. };
  214. peers.Self = undefined;
  215. session.on("start", function () {
  216. if (peers.Self) {
  217. return;
  218. }
  219. /* Same interface as Peer, represents oneself (local user): */
  220. peers.Self = util.mixinEvents({
  221. isSelf: true,
  222. id: session.clientId,
  223. identityId: session.identityId,
  224. status: "live",
  225. idle: "active",
  226. name: null,
  227. avatar: null,
  228. color: null,
  229. defaultName: null,
  230. loaded: false,
  231. isCreator: ! session.isClient,
  232. update: function (attrs) {
  233. var updatePeers = false;
  234. var updateIdle = false;
  235. var updateMsg = {type: "peer-update"};
  236. if (typeof attrs.name == "string" && attrs.name != this.name) {
  237. this.name = attrs.name;
  238. updateMsg.name = this.name;
  239. if (! attrs.fromLoad) {
  240. storage.settings.set("name", this.name);
  241. updatePeers = true;
  242. }
  243. }
  244. if (attrs.avatar && attrs.avatar != this.avatar) {
  245. util.assertValidUrl(attrs.avatar);
  246. this.avatar = attrs.avatar;
  247. updateMsg.avatar = this.avatar;
  248. if (! attrs.fromLoad) {
  249. storage.settings.set("avatar", this.avatar);
  250. updatePeers = true;
  251. }
  252. }
  253. if (attrs.color && attrs.color != this.color) {
  254. this.color = attrs.color;
  255. updateMsg.color = this.color;
  256. if (! attrs.fromLoad) {
  257. storage.settings.set("color", this.color);
  258. updatePeers = true;
  259. }
  260. }
  261. if (attrs.defaultName && attrs.defaultName != this.defaultName) {
  262. this.defaultName = attrs.defaultName;
  263. if (! attrs.fromLoad) {
  264. storage.settings.set("defaultName", this.defaultName);
  265. updatePeers = true;
  266. }
  267. }
  268. if (attrs.status && attrs.status != this.status) {
  269. this.status = attrs.status;
  270. peers.emit("status-updated", this);
  271. }
  272. if (attrs.idle && attrs.idle != this.idle) {
  273. this.idle = attrs.idle;
  274. updateIdle = true;
  275. peers.emit("idle-updated", this);
  276. }
  277. this.view.update();
  278. if (updatePeers && ! attrs.fromLoad) {
  279. session.emit("self-updated");
  280. session.send(updateMsg);
  281. }
  282. if (updateIdle && ! attrs.fromLoad) {
  283. session.send({
  284. type: "idle-status",
  285. idle: this.idle
  286. });
  287. }
  288. },
  289. className: function (prefix) {
  290. prefix = prefix || "";
  291. return prefix + "self";
  292. },
  293. _loadFromSettings: function () {
  294. return util.resolveMany(
  295. storage.settings.get("name"),
  296. storage.settings.get("avatar"),
  297. storage.settings.get("defaultName"),
  298. storage.settings.get("color")).then((function (name, avatar, defaultName, color) {
  299. if (! defaultName) {
  300. defaultName = util.pickRandom(DEFAULT_NICKNAMES);
  301. storage.settings.set("defaultName", defaultName);
  302. }
  303. if (! color) {
  304. color = Math.floor(Math.random() * 0xffffff).toString(16);
  305. while (color.length < 6) {
  306. color = "0" + color;
  307. }
  308. color = "#" + color;
  309. storage.settings.set("color", color);
  310. }
  311. if (! avatar) {
  312. avatar = TogetherJS.baseUrl + "/togetherjs/images/default-avatar.png";
  313. }
  314. this.update({
  315. name: name,
  316. avatar: avatar,
  317. defaultName: defaultName,
  318. color: color,
  319. fromLoad: true
  320. });
  321. peers._SelfLoaded.resolve();
  322. }).bind(this)); // FIXME: ignoring error
  323. },
  324. _loadFromApp: function () {
  325. // FIXME: I wonder if these should be optionally functions?
  326. // We could test typeof==function to distinguish between a getter and a concrete value
  327. var getUserName = TogetherJS.config.get("getUserName");
  328. var getUserColor = TogetherJS.config.get("getUserColor");
  329. var getUserAvatar = TogetherJS.config.get("getUserAvatar");
  330. var name, color, avatar;
  331. if (getUserName) {
  332. if (typeof getUserName == "string") {
  333. name = getUserName;
  334. } else {
  335. name = getUserName();
  336. }
  337. if (name && typeof name != "string") {
  338. // FIXME: test for HTML safe? Not that we require it, but
  339. // <>'s are probably a sign something is wrong.
  340. console.warn("Error in getUserName(): should return a string (got", name, ")");
  341. name = null;
  342. }
  343. }
  344. if (getUserColor) {
  345. if (typeof getUserColor == "string") {
  346. color = getUserColor;
  347. } else {
  348. color = getUserColor();
  349. }
  350. if (color && typeof color != "string") {
  351. // FIXME: would be nice to test for color-ness here.
  352. console.warn("Error in getUserColor(): should return a string (got", color, ")");
  353. color = null;
  354. }
  355. }
  356. if (getUserAvatar) {
  357. if (typeof getUserAvatar == "string") {
  358. avatar = getUserAvatar;
  359. } else {
  360. avatar = getUserAvatar();
  361. }
  362. if (avatar && typeof avatar != "string") {
  363. console.warn("Error in getUserAvatar(): should return a string (got", avatar, ")");
  364. avatar = null;
  365. }
  366. }
  367. if (name || color || avatar) {
  368. this.update({
  369. name: name,
  370. color: color,
  371. avatar: avatar
  372. });
  373. }
  374. }
  375. });
  376. peers.Self.view = ui.PeerView(peers.Self);
  377. storage.tab.get("peerCache").then(deserialize);
  378. peers.Self._loadFromSettings().then(function() {
  379. peers.Self._loadFromApp();
  380. peers.Self.view.update();
  381. session.emit("self-updated");
  382. });
  383. });
  384. session.on("refresh-user-data", function () {
  385. if (peers.Self) {
  386. peers.Self._loadFromApp();
  387. }
  388. });
  389. TogetherJS.config.track(
  390. "getUserName",
  391. TogetherJS.config.track(
  392. "getUserColor",
  393. TogetherJS.config.track(
  394. "getUserAvatar",
  395. function () {
  396. if (peers.Self) {
  397. peers.Self._loadFromApp();
  398. }
  399. }
  400. )
  401. )
  402. );
  403. peers._SelfLoaded = util.Deferred();
  404. function serialize() {
  405. var peers = [];
  406. util.forEachAttr(Peer.peers, function (peer) {
  407. peers.push(peer.serialize());
  408. });
  409. return {
  410. peers: peers
  411. };
  412. }
  413. function deserialize(obj) {
  414. if (! obj) {
  415. return;
  416. }
  417. obj.peers.forEach(function (peer) {
  418. Peer.deserialize(peer);
  419. });
  420. }
  421. peers.getPeer = function getPeer(id, message, ignoreMissing) {
  422. assert(id);
  423. var peer = Peer.peers[id];
  424. if (id === session.clientId) {
  425. return peers.Self;
  426. }
  427. if (message && ! peer) {
  428. peer = Peer(id, {fromHelloMessage: message});
  429. return peer;
  430. }
  431. if (ignoreMissing && !peer) {
  432. return null;
  433. }
  434. assert(peer, "No peer with id:", id);
  435. if (message &&
  436. (message.type == "hello" || message.type == "hello-back" ||
  437. message.type == "peer-update")) {
  438. peer.updateFromHello(message);
  439. peer.view.update();
  440. }
  441. return Peer.peers[id];
  442. };
  443. peers.getAllPeers = function (liveOnly) {
  444. var result = [];
  445. util.forEachAttr(Peer.peers, function (peer) {
  446. if (liveOnly && peer.status != "live") {
  447. return;
  448. }
  449. result.push(peer);
  450. });
  451. return result;
  452. };
  453. function checkActivity() {
  454. var ps = peers.getAllPeers();
  455. var now = Date.now();
  456. ps.forEach(function (p) {
  457. if (p.idle == "active" && now - p.lastMessageDate > IDLE_TIME) {
  458. p.update({idle: "inactive"});
  459. }
  460. if (p.status != "bye" && now - p.lastMessageDate > BYE_TIME) {
  461. p.bye();
  462. }
  463. });
  464. }
  465. session.hub.on("bye", function (msg) {
  466. var peer = peers.getPeer(msg.clientId);
  467. peer.bye();
  468. });
  469. var checkActivityTask = null;
  470. session.on("start", function () {
  471. if (checkActivityTask) {
  472. console.warn("Old peers checkActivityTask left over?");
  473. clearTimeout(checkActivityTask);
  474. }
  475. checkActivityTask = setInterval(checkActivity, CHECK_ACTIVITY_INTERVAL);
  476. });
  477. session.on("close", function () {
  478. util.forEachAttr(Peer.peers, function (peer) {
  479. peer.destroy();
  480. });
  481. storage.tab.set("peerCache", undefined);
  482. clearTimeout(checkActivityTask);
  483. checkActivityTask = null;
  484. });
  485. var tabIdleTimeout = null;
  486. session.on("visibility-change", function (hidden) {
  487. if (hidden) {
  488. if (tabIdleTimeout) {
  489. clearTimeout(tabIdleTimeout);
  490. }
  491. tabIdleTimeout = setTimeout(function () {
  492. peers.Self.update({idle: "inactive"});
  493. }, TAB_IDLE_TIME);
  494. } else {
  495. if (tabIdleTimeout) {
  496. clearTimeout(tabIdleTimeout);
  497. }
  498. if (peers.Self.idle == "inactive") {
  499. peers.Self.update({idle: "active"});
  500. }
  501. }
  502. });
  503. session.hub.on("idle-status", function (msg) {
  504. msg.peer.update({idle: msg.idle});
  505. });
  506. // Pings are a straight alive check, and contain no more information:
  507. session.hub.on("ping", function () {
  508. session.send({type: "ping-back"});
  509. });
  510. window.addEventListener("pagehide", function () {
  511. // FIXME: not certain if this should be tab local or not:
  512. storeSerialization();
  513. }, false);
  514. function storeSerialization() {
  515. storage.tab.set("peerCache", serialize());
  516. }
  517. util.mixinEvents(peers);
  518. util.testExpose({
  519. setIdleTime: function (time) {
  520. IDLE_TIME = time;
  521. CHECK_ACTIVITY_INTERVAL = time / 2;
  522. if (TogetherJS.running) {
  523. clearTimeout(checkActivityTask);
  524. checkActivityTask = setInterval(checkActivity, CHECK_ACTIVITY_INTERVAL);
  525. }
  526. }
  527. });
  528. util.testExpose({
  529. setByeTime: function (time) {
  530. BYE_TIME = time;
  531. CHECK_ACTIVITY_INTERVAL = Math.min(CHECK_ACTIVITY_INTERVAL, time / 2);
  532. if (TogetherJS.running) {
  533. clearTimeout(checkActivityTask);
  534. checkActivityTask = setInterval(checkActivity, CHECK_ACTIVITY_INTERVAL);
  535. }
  536. }
  537. });
  538. return peers;
  539. });