aboutsummaryrefslogtreecommitdiff
path: root/firefox/content/module.jsm
diff options
context:
space:
mode:
Diffstat (limited to 'firefox/content/module.jsm')
-rw-r--r--firefox/content/module.jsm569
1 files changed, 569 insertions, 0 deletions
diff --git a/firefox/content/module.jsm b/firefox/content/module.jsm
new file mode 100644
index 0000000..dbadee1
--- /dev/null
+++ b/firefox/content/module.jsm
@@ -0,0 +1,569 @@
+var EXPORTED_SYMBOLS = ['VKPC'];
+
+try {
+ var console = (Components.utils.import("resource://gre/modules/devtools/Console.jsm", {})).console;
+} catch (e) {}
+
+var browser = {
+ id: 1,
+ chrome: false,
+ safari: false,
+ yandex: false,
+ firefox: true,
+ opera: false
+};
+
+var started = false;
+
+function createTimeout(callback, interval) {
+ return new function() {
+ var timer = Components.classes["@mozilla.org/timer;1"].createInstance(Components.interfaces.nsITimer);
+ timer.initWithCallback({
+ notify: function() {
+ callback();
+ }
+ }, interval, Components.interfaces.nsITimer.TYPE_ONE_SHOT);
+
+ this.cancel = function() {
+ timer.cancel();
+ timer = null;
+ };
+ }
+}
+function createInterval(callback, interval) {
+ return new function() {
+ var timer = Components.classes["@mozilla.org/timer;1"].createInstance(Components.interfaces.nsITimer);
+ timer.initWithCallback({
+ notify: function() {
+ callback();
+ }
+ }, interval, Components.interfaces.nsITimer.TYPE_REPEATING_SLACK);
+
+ this.cancel = function() {
+ timer.cancel();
+ timer = null;
+ };
+ }
+}
+function log() {
+ return; // comment for debugging
+
+ var msgs = [], i, tmp;
+ for (i = 0; i < arguments.length; i++) {
+ if (arguments[i] instanceof Error) tmp = [arguments[i], arguments[i].stack];
+ else tmp = arguments[i];
+ msgs.push(tmp);
+ }
+
+ msgs.unshift('[VKPC module.jsm]');
+ try {
+ console.log.apply(console, msgs);
+ } catch(e) {}
+}
+function extend(dest, source) {
+ for (var i in source) {
+ dest[i] = source[i];
+ }
+ return dest;
+}
+function createCData(data) {
+ var parser = Components.classes["@mozilla.org/xmlextras/domparser;1"]
+ .createInstance(Components.interfaces.nsIDOMParser);
+ var doc = parser.parseFromString('<xml></xml>', "application/xml");
+ var cdata = doc.createCDATASection(data);
+ doc.getElementsByTagName('xml')[0].appendChild(cdata);
+ return cdata;
+}
+function remove(element) {
+ element.parentNode.removeChild(element);
+}
+
+function InjectionResponses(id, count, callback) {
+ this.id = id;
+ this.results = [];
+ this.lsSource = null;
+ this.maxCount = count;
+ this.callback = callback || function() {};
+
+ Injections.register(this.id, this);
+}
+extend(InjectionResponses.prototype, {
+ addResponse: function(id, response) {
+ this.results.push({tab: id, data: response});
+ if (!this.lsSource && response && response.lastInstanceId) this.lsSource = response.lastInstanceId;
+ if (this.results.length == this.maxCount) {
+ this.callback();
+ }
+ },
+ unregister: function() {
+ Injections.unregister(this.id);
+ }
+});
+
+var Injections = {
+ id: 0,
+ objs: {},
+ getNextId: function() {
+ if (this.id == Number.MAX_VALUE) {
+ this.id = -1;
+ }
+ return ++this.id;
+ },
+ get: function(id) {
+ return this.objs[id] || false;
+ },
+ register: function(id, obj) {
+ this.objs[id] = obj;
+ },
+ unregister: function(id) {
+ if (this.objs[id] !== undefined) delete this.objs[id];
+ }
+};
+
+var WSC_STATE_NONE = 'NONE',
+ WSC_STATE_OK = 'OK',
+ WSC_STATE_CLOSED = 'CLOSED',
+ WSC_STATE_ERR = 'ERR';
+function WSClient(address, protocol, opts) {
+ this.state = WSC_STATE_NONE;
+ this._ws = null;
+
+ this.address = address;
+ this.protocol = protocol;
+
+ this._onmessage = opts.onmessage;
+ this._onclose = opts.onclose;
+ this._onerror = opts.onerror;
+ this._onopen = opts.onopen;
+
+ this._pingTimer = null;
+ this._reconnectTimer = null;
+}
+extend(WSClient.prototype, {
+ connect: function(callback) {
+ this.state = WSC_STATE_NONE;
+ var self = this;
+
+ this._waitForWebSocketAvailable(function(_websocket) {
+ log('_waitForWebSocketAvailable DONE');
+ self._ws = new _websocket(self.address, self.protocol);
+
+ if (!self._ws) {
+ log('websockets are not supported');
+ return;
+ }
+
+ self._ws.onopen = function() {
+ self.state = WSC_STATE_OK;
+ self._setTimers();
+ self._onopen && self._onopen.apply(self);
+ };
+ self._ws.onerror = function() {
+ self._unsetTimers();
+ if (self.state != WSC_STATE_ERR) {
+ self.state = WSC_STATE_ERR;
+ }
+ self._onerror && self._onerror.apply(self);
+ };
+ self._ws.onclose = function() {
+ self._unsetTimers();
+ if (self.state != WSC_STATE_ERR) {
+ self.state = WSC_STATE_ERR;
+ }
+ self._onclose && self._onclose.apply(self);
+ };
+ self._ws.onmessage = function(e) {
+ self._onmessage && self._onmessage.apply(self, [e.data]);
+ };
+
+ callback && callback();
+ }, 200);
+ },
+ close: function() {
+ this._unsetTimers();
+ if (this._ws) {
+ this.state = WSC_STATE_CLOSED;
+ this._ws.close();
+ }
+ },
+ reconnect: function() {
+ var self = this;
+ if (this.state == WSC_STATE_OK) {
+ try {
+ log('[WSClient reconnect] state = '+this.state+', why reconnect?');
+ } catch (e) {}
+ return;
+ }
+ if (this._reconnectTimer) {
+ this._reconnectTimer.cancel();
+ }
+ this._reconnectTimer = createTimeout(function() {
+ self.connect();
+ }, 3000);
+ },
+ send: function(obj) {
+ obj._browser = browser.id;
+ var self = this;
+ this._waitForConnection(function() {
+ self._ws.send(JSON.stringify(obj));
+ }, 200);
+ },
+ _setTimers: function() {
+ var self = this;
+ this._unsetTimers();
+ this._pingTimer = createInterval(function() {
+ try {
+ self._ws.send("PING");
+ } catch (e) {
+ log('[WSClient _pingTimer]', e);
+ }
+ }, 30000);
+ },
+ _unsetTimers: function() {
+ if (this._pingTimer)
+ this._pingTimer.cancel();
+ },
+ _waitForConnection: function(callback, interval) {
+ if (this._ws.readyState === 1) {
+ callback();
+ } else {
+ var self = this;
+ var timer = createTimeout(function() {
+ timer.cancel();
+ self._waitForConnection(callback, interval);
+ }, interval);
+ }
+ },
+ _waitForWebSocketAvailable: function(callback, interval) {
+ var win, self = this;
+ try {
+ win = Components.classes["@mozilla.org/appshell/appShellService;1"].
+ getService(Components.interfaces.nsIAppShellService).
+ hiddenDOMWindow;
+ } catch (e) {
+ var timer = createTimeout(function() {
+ timer.cancel();
+ self._waitForWebSocketAvailable(callback, interval);
+ }, interval);
+ } finally {
+ if (win) {
+ callback(win.WebSocket || win.MozWebSocket);
+ }
+ }
+ }
+});
+
+var Documents = {
+ list: [],
+ add: function(doc) {
+ this.cleanup();
+ this.list.push(doc);
+ },
+ cleanup: function() {
+ this.list = this.list.filter(function(t) {
+ return Object.prototype.toString.call(t) != '[object DeadObject]';
+ });
+ },
+ send: function(json) {
+ var self = this;
+ this.cleanup();
+
+ this.list.forEach(function(doc) {
+ self.sendToDoc(doc, json);
+ });
+ },
+ sendToDoc: function(doc, json) {
+ var cdata = createCData(JSON.stringify(json));
+ doc.getElementById('utils').appendChild(cdata);
+
+ var evt = doc.createEvent("Events");
+ evt.initEvent("VKPCBgMessage", true, false);
+ cdata.dispatchEvent(evt);
+ },
+ getCount: function() {
+ this.cleanup();
+ return this.list.length;
+ }
+};
+
+function sendClear() {
+ wsc.send({command: 'clearPlaylist', data: null});
+}
+
+function prepareWindow(win) {
+ function onPageLoaded(e) {
+ var doc = e.originalTarget, loc = doc.location;
+ if (!loc.href.match(/^https?:\/\/vk.com\/.*$/)) return;
+
+ doc.addEventListener("VKPCInjectedMessage", function(e) {
+ var target = e.target, json = JSON.parse(target.data || "{}"), doc = target.ownerDocument;
+ receiveMessage(json, doc, target);
+ }, false);
+
+ var loader = Components.classes["@mozilla.org/moz/jssubscript-loader;1"]
+ .getService(Components.interfaces.mozIJSSubScriptLoader);
+ loader.loadSubScript("chrome://vkpc/content/vkpc.js", doc);
+ }
+
+ var appcontent = win.document.getElementById("appcontent");
+ if (appcontent) {
+ appcontent.addEventListener("DOMContentLoaded", onPageLoaded, true);
+ }
+}
+
+// receive message from tab
+function receiveMessage(json, doc, target) {
+ switch (json.cmd) {
+ case "register":
+ Documents.add(doc);
+ break;
+
+ case "afterInjection":
+ var id = json.id;
+ var obj = Injections.get(id);
+ if (obj) {
+ obj.addResponse(doc, json.data);
+ }
+ break;
+
+ case "to_app":
+ wsc.send(json.data);
+ break;
+ }
+
+ try {
+ remove(target);
+ } catch (e) {}
+}
+
+// send message to tabs
+function sendMessage(data, tab) {
+ if (tab) {
+ Documents.sendToDoc(tab, data);
+ } else {
+ Documents.send(data);
+ }
+}
+
+function inject(command/*, callback*/) {
+ //log('inject', command);
+ var injId = Injections.getNextId();
+ var data = {
+ sid: Controller.sid,
+ command: command
+ };
+
+ var okTab_nowPlaying, okTab_playlistFound, okTab_lsSource, okTab_recentlyPlayed, okTab_havePlaylist,
+ activeTab, lastTab, outdatedTabs = [], tabsWithPlayingMusic = [];
+ var lsSourceId, appPlaylistFound = false;
+
+ var injResponses, injResults;
+
+ function onDone(step) {
+ var results = injResponses.results;
+ // var execCommand = getCode("VKPC.executeCommand('"+command+"', "+Controller.playlistId+")");
+ var vkpcCommand = {cmd: 'vkpc', command: command, playlistId: Controller.playlistId};
+
+ if (command == 'afterInjection') {
+ // log('[afterInjection onDone] results.length='+results.length);
+
+ for (var i = 0; i < results.length; i++) {
+ var data = results[i].data, tab = results[i].tab;
+
+ if (data.playlistId != 0 && data.playlistId == Controller.playlistId) {
+ appPlaylistFound = true;
+ }
+ if (data.havePlaylist && data.playlistId != 0 && data.playlistId != Controller.playlistId) {
+ outdatedTabs.push(tab);
+ }
+ if (data.havePlaylist) {
+ okTab_havePlaylist = tab;
+ }
+ if (data.isPlaying) {
+ okTab_nowPlaying = tab;
+ }
+ }
+
+ if (!appPlaylistFound) {
+ var okTab = okTab_nowPlaying || okTab_havePlaylist;
+ if (okTab !== undefined) {
+ sendMessage(vkpcCommand, okTab);
+ } else {
+ sendClear();
+ }
+ }
+
+ for (var i = 0; i < outdatedTabs.length; i++) {
+ sendMessage({cmd: 'vkpc', command: 'clearPlaylist'}, outdatedTabs[i]);
+ }
+ } else {
+ for (var i = 0; i < results.length; i++) {
+ var data = results[i].data;
+ if (!lsSourceId && data.lsSourceId) {
+ lsSourceId = data.lsSourceId;
+ break;
+ }
+ }
+
+ for (var i = 0; i < results.length; i++) {
+ var data = results[i].data, tab = results[i].tab;
+
+ if (data.playlistId == Controller.playlistId) {
+ okTab_playlistFound = tab;
+ }
+ if (data.havePlayer && (data.isPlaying || typeof data.trackId == 'string')) {
+ okTab_recentlyPlayed = tab;
+ }
+ if (data.isPlaying) {
+ okTab_nowPlaying = tab;
+ }
+ if (lsSourceId == data.tabId) {
+ okTab_lsSource = tab;
+ }
+
+ lastTab = tab;
+ }
+
+ var check = [okTab_nowPlaying, okTab_lsSource, okTab_recentlyPlayed, okTab_recentlyPlayed, okTab_havePlaylist, activeTab, lastTab];
+ for (var i = 0; i < check.length; i++) {
+ if (check[i] !== undefined) {
+ sendMessage(vkpcCommand, check[i]);
+ // chrome.tabs.executeScript(check[i], {code: execCommand});
+ break;
+ }
+ }
+ }
+
+ injResponses.unregister();
+ }
+
+ var count = Documents.getCount();
+ //log('vk tabs count: ' + count);
+
+ if (!count) {
+ log('vk tabs not found');
+ sendClear();
+ return;
+ }
+
+ injResponses = new InjectionResponses(injId, Documents.getCount(), onDone);
+ sendMessage({
+ cmd: "afterInjection",
+ id: injId,
+ data: data
+ });
+};
+
+var Controller = {
+ sid: 0,
+ playlistId: 0,
+ clear: function() {
+ this.sid = 0;
+ this.playlistId = 0;
+ }
+};
+
+var VKPC = new function() {
+ var timer;
+ log('VKPC()');
+
+ var windows = [];
+ this.addWindow = function(win, notWait) {
+ if (windows.indexOf(win) == -1) {
+ log('window added', win);
+ windows.push(win);
+
+ if (!notWait) {
+ win.addEventListener('load', function load(e) {
+ win.removeEventListener('load', load, false);
+ prepareWindow(win);
+ }, false);
+ } else {
+ prepareWindow(win);
+ }
+ }
+ };
+ this.removeWindow = function(win) {
+ var index;
+ if ((index = windows.indexOf(win)) != -1) {
+ log('window removed', win);
+ windows.splice(index, 1);
+ }
+ };
+
+ var self = this;
+ this.startup = function() {
+ if (started) {
+ log('already started, ignoring');
+ return;
+ }
+
+ wsc = new WSClient("wss://vkpc-local.ch1p.com:56130", "signaling-protocol", {
+ onopen: function() {
+ Controller.clear();
+ this.send({command: 'setBrowser'});
+
+ if (timer) {
+ timer.cancel();
+ }
+ timer = createInterval(function() {
+ inject('afterInjection');
+ }, 2000);
+ },
+ onmessage: function(cmd) {
+ log('[wsc onmessage] cmd:', cmd);
+
+ var json = JSON.parse(cmd);
+ switch (json.command) {
+ case 'set_sid':
+ Controller.sid = json.data;
+ break;
+
+ case 'set_playlist_id':
+ Controller.playlistId = json.data;
+ break;
+
+ case 'vkpc':
+ inject(json.data);
+ break;
+ }
+ },
+ onerror: function() {
+ if (timer) {
+ timer.cancel();
+ }
+ this.reconnect();
+ },
+ onclose: function() {
+ if (timer) {
+ timer.cancel();
+ }
+ if (started) {
+ this.reconnect();
+ }
+ }
+ });
+ wsc.connect();
+
+ self.wsc = wsc;
+ started = true;
+ };
+ this.shutdown = function() {
+ if (!started) {
+ return;
+ }
+
+ started = false;
+
+ if (wsc) {
+ wsc.close();
+ wsc = undefined;
+ }
+ if (timer) {
+ timer.cancel();
+ timer = undefined;
+ }
+ };
+
+ log('init finish');
+};