##// END OF EJS Templates
Added centrifuge (websocket) support for thread autoupdate. Only websocket version is supported for now
neko259 -
r853:ea46532a default
parent child Browse files
Show More
This diff has been collapsed as it changes many lines, (1256 lines changed) Show them Hide them
@@ -0,0 +1,1256 b''
1 /**
2 * Centrifuge javascript client
3 * v0.5.2
4 */
5 ;(function () {
6 'use strict';
7
8 /**
9 * Oliver Caldwell
10 * http://oli.me.uk/2013/06/01/prototypical-inheritance-done-right/
11 */
12
13 if (!Object.create) {
14 Object.create = (function(){
15 function F(){}
16
17 return function(o){
18 if (arguments.length != 1) {
19 throw new Error('Object.create implementation only accepts one parameter.');
20 }
21 F.prototype = o;
22 return new F()
23 }
24 })()
25 }
26
27 if (!Array.prototype.indexOf) {
28 Array.prototype.indexOf = function (searchElement /*, fromIndex */) {
29 'use strict';
30 if (this == null) {
31 throw new TypeError();
32 }
33 var n, k, t = Object(this),
34 len = t.length >>> 0;
35
36 if (len === 0) {
37 return -1;
38 }
39 n = 0;
40 if (arguments.length > 1) {
41 n = Number(arguments[1]);
42 if (n != n) { // shortcut for verifying if it's NaN
43 n = 0;
44 } else if (n != 0 && n != Infinity && n != -Infinity) {
45 n = (n > 0 || -1) * Math.floor(Math.abs(n));
46 }
47 }
48 if (n >= len) {
49 return -1;
50 }
51 for (k = n >= 0 ? n : Math.max(len - Math.abs(n), 0); k < len; k++) {
52 if (k in t && t[k] === searchElement) {
53 return k;
54 }
55 }
56 return -1;
57 };
58 }
59
60 function extend(destination, source) {
61 destination.prototype = Object.create(source.prototype);
62 destination.prototype.constructor = destination;
63 return source.prototype;
64 }
65
66 /**
67 * EventEmitter v4.2.3 - git.io/ee
68 * Oliver Caldwell
69 * MIT license
70 * @preserve
71 */
72
73 /**
74 * Class for managing events.
75 * Can be extended to provide event functionality in other classes.
76 *
77 * @class EventEmitter Manages event registering and emitting.
78 */
79 function EventEmitter() {}
80
81 // Shortcuts to improve speed and size
82
83 // Easy access to the prototype
84 var proto = EventEmitter.prototype;
85
86 /**
87 * Finds the index of the listener for the event in it's storage array.
88 *
89 * @param {Function[]} listeners Array of listeners to search through.
90 * @param {Function} listener Method to look for.
91 * @return {Number} Index of the specified listener, -1 if not found
92 * @api private
93 */
94 function indexOfListener(listeners, listener) {
95 var i = listeners.length;
96 while (i--) {
97 if (listeners[i].listener === listener) {
98 return i;
99 }
100 }
101
102 return -1;
103 }
104
105 /**
106 * Alias a method while keeping the context correct, to allow for overwriting of target method.
107 *
108 * @param {String} name The name of the target method.
109 * @return {Function} The aliased method
110 * @api private
111 */
112 function alias(name) {
113 return function aliasClosure() {
114 return this[name].apply(this, arguments);
115 };
116 }
117
118 /**
119 * Returns the listener array for the specified event.
120 * Will initialise the event object and listener arrays if required.
121 * Will return an object if you use a regex search. The object contains keys for each matched event. So /ba[rz]/ might return an object containing bar and baz. But only if you have either defined them with defineEvent or added some listeners to them.
122 * Each property in the object response is an array of listener functions.
123 *
124 * @param {String|RegExp} evt Name of the event to return the listeners from.
125 * @return {Function[]|Object} All listener functions for the event.
126 */
127 proto.getListeners = function getListeners(evt) {
128 var events = this._getEvents();
129 var response;
130 var key;
131
132 // Return a concatenated array of all matching events if
133 // the selector is a regular expression.
134 if (typeof evt === 'object') {
135 response = {};
136 for (key in events) {
137 if (events.hasOwnProperty(key) && evt.test(key)) {
138 response[key] = events[key];
139 }
140 }
141 }
142 else {
143 response = events[evt] || (events[evt] = []);
144 }
145
146 return response;
147 };
148
149 /**
150 * Takes a list of listener objects and flattens it into a list of listener functions.
151 *
152 * @param {Object[]} listeners Raw listener objects.
153 * @return {Function[]} Just the listener functions.
154 */
155 proto.flattenListeners = function flattenListeners(listeners) {
156 var flatListeners = [];
157 var i;
158
159 for (i = 0; i < listeners.length; i += 1) {
160 flatListeners.push(listeners[i].listener);
161 }
162
163 return flatListeners;
164 };
165
166 /**
167 * Fetches the requested listeners via getListeners but will always return the results inside an object. This is mainly for internal use but others may find it useful.
168 *
169 * @param {String|RegExp} evt Name of the event to return the listeners from.
170 * @return {Object} All listener functions for an event in an object.
171 */
172 proto.getListenersAsObject = function getListenersAsObject(evt) {
173 var listeners = this.getListeners(evt);
174 var response;
175
176 if (listeners instanceof Array) {
177 response = {};
178 response[evt] = listeners;
179 }
180
181 return response || listeners;
182 };
183
184 /**
185 * Adds a listener function to the specified event.
186 * The listener will not be added if it is a duplicate.
187 * If the listener returns true then it will be removed after it is called.
188 * If you pass a regular expression as the event name then the listener will be added to all events that match it.
189 *
190 * @param {String|RegExp} evt Name of the event to attach the listener to.
191 * @param {Function} listener Method to be called when the event is emitted. If the function returns true then it will be removed after calling.
192 * @return {Object} Current instance of EventEmitter for chaining.
193 */
194 proto.addListener = function addListener(evt, listener) {
195 var listeners = this.getListenersAsObject(evt);
196 var listenerIsWrapped = typeof listener === 'object';
197 var key;
198
199 for (key in listeners) {
200 if (listeners.hasOwnProperty(key) && indexOfListener(listeners[key], listener) === -1) {
201 listeners[key].push(listenerIsWrapped ? listener : {
202 listener: listener,
203 once: false
204 });
205 }
206 }
207
208 return this;
209 };
210
211 /**
212 * Alias of addListener
213 */
214 proto.on = alias('addListener');
215
216 /**
217 * Semi-alias of addListener. It will add a listener that will be
218 * automatically removed after it's first execution.
219 *
220 * @param {String|RegExp} evt Name of the event to attach the listener to.
221 * @param {Function} listener Method to be called when the event is emitted. If the function returns true then it will be removed after calling.
222 * @return {Object} Current instance of EventEmitter for chaining.
223 */
224 proto.addOnceListener = function addOnceListener(evt, listener) {
225 //noinspection JSValidateTypes
226 return this.addListener(evt, {
227 listener: listener,
228 once: true
229 });
230 };
231
232 /**
233 * Alias of addOnceListener.
234 */
235 proto.once = alias('addOnceListener');
236
237 /**
238 * Defines an event name. This is required if you want to use a regex to add a listener to multiple events at once. If you don't do this then how do you expect it to know what event to add to? Should it just add to every possible match for a regex? No. That is scary and bad.
239 * You need to tell it what event names should be matched by a regex.
240 *
241 * @param {String} evt Name of the event to create.
242 * @return {Object} Current instance of EventEmitter for chaining.
243 */
244 proto.defineEvent = function defineEvent(evt) {
245 this.getListeners(evt);
246 return this;
247 };
248
249 /**
250 * Uses defineEvent to define multiple events.
251 *
252 * @param {String[]} evts An array of event names to define.
253 * @return {Object} Current instance of EventEmitter for chaining.
254 */
255 proto.defineEvents = function defineEvents(evts) {
256 for (var i = 0; i < evts.length; i += 1) {
257 this.defineEvent(evts[i]);
258 }
259 return this;
260 };
261
262 /**
263 * Removes a listener function from the specified event.
264 * When passed a regular expression as the event name, it will remove the listener from all events that match it.
265 *
266 * @param {String|RegExp} evt Name of the event to remove the listener from.
267 * @param {Function} listener Method to remove from the event.
268 * @return {Object} Current instance of EventEmitter for chaining.
269 */
270 proto.removeListener = function removeListener(evt, listener) {
271 var listeners = this.getListenersAsObject(evt);
272 var index;
273 var key;
274
275 for (key in listeners) {
276 if (listeners.hasOwnProperty(key)) {
277 index = indexOfListener(listeners[key], listener);
278
279 if (index !== -1) {
280 listeners[key].splice(index, 1);
281 }
282 }
283 }
284
285 return this;
286 };
287
288 /**
289 * Alias of removeListener
290 */
291 proto.off = alias('removeListener');
292
293 /**
294 * Adds listeners in bulk using the manipulateListeners method.
295 * If you pass an object as the second argument you can add to multiple events at once. The object should contain key value pairs of events and listeners or listener arrays. You can also pass it an event name and an array of listeners to be added.
296 * You can also pass it a regular expression to add the array of listeners to all events that match it.
297 * Yeah, this function does quite a bit. That's probably a bad thing.
298 *
299 * @param {String|Object|RegExp} evt An event name if you will pass an array of listeners next. An object if you wish to add to multiple events at once.
300 * @param {Function[]} [listeners] An optional array of listener functions to add.
301 * @return {Object} Current instance of EventEmitter for chaining.
302 */
303 proto.addListeners = function addListeners(evt, listeners) {
304 // Pass through to manipulateListeners
305 return this.manipulateListeners(false, evt, listeners);
306 };
307
308 /**
309 * Removes listeners in bulk using the manipulateListeners method.
310 * If you pass an object as the second argument you can remove from multiple events at once. The object should contain key value pairs of events and listeners or listener arrays.
311 * You can also pass it an event name and an array of listeners to be removed.
312 * You can also pass it a regular expression to remove the listeners from all events that match it.
313 *
314 * @param {String|Object|RegExp} evt An event name if you will pass an array of listeners next. An object if you wish to remove from multiple events at once.
315 * @param {Function[]} [listeners] An optional array of listener functions to remove.
316 * @return {Object} Current instance of EventEmitter for chaining.
317 */
318 proto.removeListeners = function removeListeners(evt, listeners) {
319 // Pass through to manipulateListeners
320 return this.manipulateListeners(true, evt, listeners);
321 };
322
323 /**
324 * Edits listeners in bulk. The addListeners and removeListeners methods both use this to do their job. You should really use those instead, this is a little lower level.
325 * The first argument will determine if the listeners are removed (true) or added (false).
326 * If you pass an object as the second argument you can add/remove from multiple events at once. The object should contain key value pairs of events and listeners or listener arrays.
327 * You can also pass it an event name and an array of listeners to be added/removed.
328 * You can also pass it a regular expression to manipulate the listeners of all events that match it.
329 *
330 * @param {Boolean} remove True if you want to remove listeners, false if you want to add.
331 * @param {String|Object|RegExp} evt An event name if you will pass an array of listeners next. An object if you wish to add/remove from multiple events at once.
332 * @param {Function[]} [listeners] An optional array of listener functions to add/remove.
333 * @return {Object} Current instance of EventEmitter for chaining.
334 */
335 proto.manipulateListeners = function manipulateListeners(remove, evt, listeners) {
336 var i;
337 var value;
338 var single = remove ? this.removeListener : this.addListener;
339 var multiple = remove ? this.removeListeners : this.addListeners;
340
341 // If evt is an object then pass each of it's properties to this method
342 if (typeof evt === 'object' && !(evt instanceof RegExp)) {
343 for (i in evt) {
344 if (evt.hasOwnProperty(i) && (value = evt[i])) {
345 // Pass the single listener straight through to the singular method
346 if (typeof value === 'function') {
347 single.call(this, i, value);
348 }
349 else {
350 // Otherwise pass back to the multiple function
351 multiple.call(this, i, value);
352 }
353 }
354 }
355 }
356 else {
357 // So evt must be a string
358 // And listeners must be an array of listeners
359 // Loop over it and pass each one to the multiple method
360 i = listeners.length;
361 while (i--) {
362 single.call(this, evt, listeners[i]);
363 }
364 }
365
366 return this;
367 };
368
369 /**
370 * Removes all listeners from a specified event.
371 * If you do not specify an event then all listeners will be removed.
372 * That means every event will be emptied.
373 * You can also pass a regex to remove all events that match it.
374 *
375 * @param {String|RegExp} [evt] Optional name of the event to remove all listeners for. Will remove from every event if not passed.
376 * @return {Object} Current instance of EventEmitter for chaining.
377 */
378 proto.removeEvent = function removeEvent(evt) {
379 var type = typeof evt;
380 var events = this._getEvents();
381 var key;
382
383 // Remove different things depending on the state of evt
384 if (type === 'string') {
385 // Remove all listeners for the specified event
386 delete events[evt];
387 }
388 else if (type === 'object') {
389 // Remove all events matching the regex.
390 for (key in events) {
391 //noinspection JSUnresolvedFunction
392 if (events.hasOwnProperty(key) && evt.test(key)) {
393 delete events[key];
394 }
395 }
396 }
397 else {
398 // Remove all listeners in all events
399 delete this._events;
400 }
401
402 return this;
403 };
404
405 /**
406 * Emits an event of your choice.
407 * When emitted, every listener attached to that event will be executed.
408 * If you pass the optional argument array then those arguments will be passed to every listener upon execution.
409 * Because it uses `apply`, your array of arguments will be passed as if you wrote them out separately.
410 * So they will not arrive within the array on the other side, they will be separate.
411 * You can also pass a regular expression to emit to all events that match it.
412 *
413 * @param {String|RegExp} evt Name of the event to emit and execute listeners for.
414 * @param {Array} [args] Optional array of arguments to be passed to each listener.
415 * @return {Object} Current instance of EventEmitter for chaining.
416 */
417 proto.emitEvent = function emitEvent(evt, args) {
418 var listeners = this.getListenersAsObject(evt);
419 var listener;
420 var i;
421 var key;
422 var response;
423
424 for (key in listeners) {
425 if (listeners.hasOwnProperty(key)) {
426 i = listeners[key].length;
427
428 while (i--) {
429 // If the listener returns true then it shall be removed from the event
430 // The function is executed either with a basic call or an apply if there is an args array
431 listener = listeners[key][i];
432
433 if (listener.once === true) {
434 this.removeListener(evt, listener.listener);
435 }
436
437 response = listener.listener.apply(this, args || []);
438
439 if (response === this._getOnceReturnValue()) {
440 this.removeListener(evt, listener.listener);
441 }
442 }
443 }
444 }
445
446 return this;
447 };
448
449 /**
450 * Alias of emitEvent
451 */
452 proto.trigger = alias('emitEvent');
453
454 //noinspection JSValidateJSDoc,JSCommentMatchesSignature
455 /**
456 * Subtly different from emitEvent in that it will pass its arguments on to the listeners, as opposed to taking a single array of arguments to pass on.
457 * As with emitEvent, you can pass a regex in place of the event name to emit to all events that match it.
458 *
459 * @param {String|RegExp} evt Name of the event to emit and execute listeners for.
460 * @param {...*} Optional additional arguments to be passed to each listener.
461 * @return {Object} Current instance of EventEmitter for chaining.
462 */
463 proto.emit = function emit(evt) {
464 var args = Array.prototype.slice.call(arguments, 1);
465 return this.emitEvent(evt, args);
466 };
467
468 /**
469 * Sets the current value to check against when executing listeners. If a
470 * listeners return value matches the one set here then it will be removed
471 * after execution. This value defaults to true.
472 *
473 * @param {*} value The new value to check for when executing listeners.
474 * @return {Object} Current instance of EventEmitter for chaining.
475 */
476 proto.setOnceReturnValue = function setOnceReturnValue(value) {
477 this._onceReturnValue = value;
478 return this;
479 };
480
481 /**
482 * Fetches the current value to check against when executing listeners. If
483 * the listeners return value matches this one then it should be removed
484 * automatically. It will return true by default.
485 *
486 * @return {*|Boolean} The current value to check for or the default, true.
487 * @api private
488 */
489 proto._getOnceReturnValue = function _getOnceReturnValue() {
490 if (this.hasOwnProperty('_onceReturnValue')) {
491 return this._onceReturnValue;
492 }
493 else {
494 return true;
495 }
496 };
497
498 /**
499 * Fetches the events object and creates one if required.
500 *
501 * @return {Object} The events storage object.
502 * @api private
503 */
504 proto._getEvents = function _getEvents() {
505 return this._events || (this._events = {});
506 };
507
508 /**
509 * Mixes in the given objects into the target object by copying the properties.
510 * @param deep if the copy must be deep
511 * @param target the target object
512 * @param objects the objects whose properties are copied into the target
513 */
514 function mixin(deep, target, objects) {
515 var result = target || {};
516
517 // Skip first 2 parameters (deep and target), and loop over the others
518 for (var i = 2; i < arguments.length; ++i) {
519 var object = arguments[i];
520
521 if (object === undefined || object === null) {
522 continue;
523 }
524
525 for (var propName in object) {
526 //noinspection JSUnfilteredForInLoop
527 var prop = fieldValue(object, propName);
528 //noinspection JSUnfilteredForInLoop
529 var targ = fieldValue(result, propName);
530
531 // Avoid infinite loops
532 if (prop === target) {
533 continue;
534 }
535 // Do not mixin undefined values
536 if (prop === undefined) {
537 continue;
538 }
539
540 if (deep && typeof prop === 'object' && prop !== null) {
541 if (prop instanceof Array) {
542 //noinspection JSUnfilteredForInLoop
543 result[propName] = mixin(deep, targ instanceof Array ? targ : [], prop);
544 } else {
545 var source = typeof targ === 'object' && !(targ instanceof Array) ? targ : {};
546 //noinspection JSUnfilteredForInLoop
547 result[propName] = mixin(deep, source, prop);
548 }
549 } else {
550 //noinspection JSUnfilteredForInLoop
551 result[propName] = prop;
552 }
553 }
554 }
555
556 return result;
557 }
558
559 function fieldValue(object, name) {
560 try {
561 return object[name];
562 } catch (x) {
563 return undefined;
564 }
565 }
566
567 function endsWith(value, suffix) {
568 return value.indexOf(suffix, value.length - suffix.length) !== -1;
569 }
570
571 function stripSlash(value) {
572 if (value.substring(value.length - 1) == "/") {
573 value = value.substring(0, value.length - 1);
574 }
575 return value;
576 }
577
578 function isString(value) {
579 if (value === undefined || value === null) {
580 return false;
581 }
582 return typeof value === 'string' || value instanceof String;
583 }
584
585 function isFunction(value) {
586 if (value === undefined || value === null) {
587 return false;
588 }
589 return typeof value === 'function';
590 }
591
592 function log(level, args) {
593 if (window.console) {
594 var logger = window.console[level];
595 if (isFunction(logger)) {
596 logger.apply(window.console, args);
597 }
598 }
599 }
600
601 function Centrifuge(options) {
602 this._sockjs = false;
603 this._status = 'disconnected';
604 this._reconnect = true;
605 this._transport = null;
606 this._messageId = 0;
607 this._clientId = null;
608 this._subscriptions = {};
609 this._messages = [];
610 this._isBatching = false;
611 this._config = {
612 retry: 3000,
613 info: null,
614 debug: false,
615 server: null,
616 protocols_whitelist: [
617 'websocket',
618 'xdr-streaming',
619 'xhr-streaming',
620 'iframe-eventsource',
621 'iframe-htmlfile',
622 'xdr-polling',
623 'xhr-polling',
624 'iframe-xhr-polling',
625 'jsonp-polling'
626 ]
627 };
628 if (options) {
629 this.configure(options);
630 }
631 }
632
633 extend(Centrifuge, EventEmitter);
634
635 var centrifuge_proto = Centrifuge.prototype;
636
637 centrifuge_proto._debug = function () {
638 if (this._config.debug === true) {
639 log('debug', arguments);
640 }
641 };
642
643 centrifuge_proto._configure = function (configuration) {
644 this._debug('Configuring centrifuge object with', configuration);
645
646 if (!configuration) {
647 configuration = {};
648 }
649
650 this._config = mixin(false, this._config, configuration);
651
652 if (!this._config.url) {
653 throw 'Missing required configuration parameter \'url\' specifying the Centrifuge server URL';
654 }
655
656 if (!this._config.token) {
657 throw 'Missing required configuration parameter \'token\' specifying the sign of authorization request';
658 }
659
660 if (!this._config.project) {
661 throw 'Missing required configuration parameter \'project\' specifying project ID in Centrifuge';
662 }
663
664 if (!this._config.user && this._config.user !== '') {
665 throw 'Missing required configuration parameter \'user\' specifying user\'s unique ID in your application';
666 }
667
668 if (!this._config.timestamp) {
669 throw 'Missing required configuration parameter \'timestamp\'';
670 }
671
672 this._config.url = stripSlash(this._config.url);
673
674 if (endsWith(this._config.url, 'connection')) {
675 //noinspection JSUnresolvedVariable
676 if (typeof window.SockJS === 'undefined') {
677 throw 'You need to include SockJS client library before Centrifuge javascript client library or use pure Websocket connection endpoint';
678 }
679 this._sockjs = true;
680 }
681 };
682
683 centrifuge_proto._setStatus = function (newStatus) {
684 if (this._status !== newStatus) {
685 this._debug('Status', this._status, '->', newStatus);
686 this._status = newStatus;
687 }
688 };
689
690 centrifuge_proto._isDisconnected = function () {
691 return this._isConnected() === false;
692 };
693
694 centrifuge_proto._isConnected = function () {
695 return this._status === 'connected';
696 };
697
698 centrifuge_proto._nextMessageId = function () {
699 return ++this._messageId;
700 };
701
702 centrifuge_proto._clearSubscriptions = function () {
703 this._subscriptions = {};
704 };
705
706 centrifuge_proto._send = function (messages) {
707 // We must be sure that the messages have a clientId.
708 // This is not guaranteed since the handshake may take time to return
709 // (and hence the clientId is not known yet) and the application
710 // may create other messages.
711 for (var i = 0; i < messages.length; ++i) {
712 var message = messages[i];
713 message.uid = '' + this._nextMessageId();
714
715 if (this._clientId) {
716 message.clientId = this._clientId;
717 }
718
719 this._debug('Send', message);
720 this._transport.send(JSON.stringify(message));
721 }
722 };
723
724 centrifuge_proto._connect = function (callback) {
725
726 this._clientId = null;
727
728 this._reconnect = true;
729
730 this._clearSubscriptions();
731
732 this._setStatus('connecting');
733
734 var self = this;
735
736 if (callback) {
737 this.on('connect', callback);
738 }
739
740 if (this._sockjs === true) {
741 //noinspection JSUnresolvedFunction
742 var sockjs_options = {
743 protocols_whitelist: this._config.protocols_whitelist
744 };
745 if (this._config.server !== null) {
746 sockjs_options['server'] = this._config.server;
747 }
748
749 this._transport = new SockJS(this._config.url, null, sockjs_options);
750
751 } else {
752 this._transport = new WebSocket(this._config.url);
753 }
754
755 this._setStatus('connecting');
756
757 this._transport.onopen = function () {
758
759 var centrifugeMessage = {
760 'method': 'connect',
761 'params': {
762 'token': self._config.token,
763 'user': self._config.user,
764 'project': self._config.project,
765 'timestamp': self._config.timestamp
766 }
767 };
768
769 if (self._config.info !== null) {
770 self._debug("connect using additional info");
771 centrifugeMessage['params']['info'] = self._config.info;
772 } else {
773 self._debug("connect without additional info");
774 }
775 self.send(centrifugeMessage);
776 };
777
778 this._transport.onerror = function (error) {
779 self._debug(error);
780 };
781
782 this._transport.onclose = function () {
783 self._setStatus('disconnected');
784 self.trigger('disconnect');
785 if (self._reconnect === true) {
786 window.setTimeout(function () {
787 if (self._reconnect === true) {
788 self._connect.call(self);
789 }
790 }, self._config.retry);
791 }
792 };
793
794 this._transport.onmessage = function (event) {
795 var data;
796 data = JSON.parse(event.data);
797 self._debug('Received', data);
798 self._receive(data);
799 };
800 };
801
802 centrifuge_proto._disconnect = function () {
803 this._clientId = null;
804 this._setStatus('disconnected');
805 this._subscriptions = {};
806 this._reconnect = false;
807 this._transport.close();
808 };
809
810 centrifuge_proto._getSubscription = function (channel) {
811 var subscription;
812 subscription = this._subscriptions[channel];
813 if (!subscription) {
814 return null;
815 }
816 return subscription;
817 };
818
819 centrifuge_proto._removeSubscription = function (channel) {
820 try {
821 delete this._subscriptions[channel];
822 } catch (e) {
823 this._debug('nothing to delete for channel ', channel);
824 }
825 };
826
827 centrifuge_proto._connectResponse = function (message) {
828 if (message.error === null) {
829 this._clientId = message.body;
830 this._setStatus('connected');
831 this.trigger('connect', [message]);
832 } else {
833 this.trigger('error', [message]);
834 this.trigger('connect:error', [message]);
835 }
836 };
837
838 centrifuge_proto._disconnectResponse = function (message) {
839 if (message.error === null) {
840 this.disconnect();
841 //this.trigger('disconnect', [message]);
842 //this.trigger('disconnect:success', [message]);
843 } else {
844 this.trigger('error', [message]);
845 this.trigger('disconnect:error', [message.error]);
846 }
847 };
848
849 centrifuge_proto._subscribeResponse = function (message) {
850 if (message.error !== null) {
851 this.trigger('error', [message]);
852 }
853 var body = message.body;
854 if (body === null) {
855 return;
856 }
857 var channel = body.channel;
858 var subscription = this.getSubscription(channel);
859 if (!subscription) {
860 return;
861 }
862 if (message.error === null) {
863 subscription.trigger('subscribe:success', [body]);
864 subscription.trigger('ready', [body]);
865 } else {
866 subscription.trigger('subscribe:error', [message.error]);
867 subscription.trigger('error', [message]);
868 }
869 };
870
871 centrifuge_proto._unsubscribeResponse = function (message) {
872 var body = message.body;
873 var channel = body.channel;
874 var subscription = this.getSubscription(channel);
875 if (!subscription) {
876 return;
877 }
878 if (message.error === null) {
879 subscription.trigger('unsubscribe', [body]);
880 this._centrifuge._removeSubscription(channel);
881 }
882 };
883
884 centrifuge_proto._publishResponse = function (message) {
885 var body = message.body;
886 var channel = body.channel;
887 var subscription = this.getSubscription(channel);
888 if (!subscription) {
889 return;
890 }
891 if (message.error === null) {
892 subscription.trigger('publish:success', [body]);
893 } else {
894 subscription.trigger('publish:error', [message.error]);
895 this.trigger('error', [message]);
896 }
897 };
898
899 centrifuge_proto._presenceResponse = function (message) {
900 var body = message.body;
901 var channel = body.channel;
902 var subscription = this.getSubscription(channel);
903 if (!subscription) {
904 return;
905 }
906 if (message.error === null) {
907 subscription.trigger('presence', [body]);
908 subscription.trigger('presence:success', [body]);
909 } else {
910 subscription.trigger('presence:error', [message.error]);
911 this.trigger('error', [message]);
912 }
913 };
914
915 centrifuge_proto._historyResponse = function (message) {
916 var body = message.body;
917 var channel = body.channel;
918 var subscription = this.getSubscription(channel);
919 if (!subscription) {
920 return;
921 }
922 if (message.error === null) {
923 subscription.trigger('history', [body]);
924 subscription.trigger('history:success', [body]);
925 } else {
926 subscription.trigger('history:error', [message.error]);
927 this.trigger('error', [message]);
928 }
929 };
930
931 centrifuge_proto._joinResponse = function(message) {
932 var body = message.body;
933 var channel = body.channel;
934 var subscription = this.getSubscription(channel);
935 if (!subscription) {
936 return;
937 }
938 subscription.trigger('join', [body]);
939 };
940
941 centrifuge_proto._leaveResponse = function(message) {
942 var body = message.body;
943 var channel = body.channel;
944 var subscription = this.getSubscription(channel);
945 if (!subscription) {
946 return;
947 }
948 subscription.trigger('leave', [body]);
949 };
950
951 centrifuge_proto._messageResponse = function (message) {
952 var body = message.body;
953 var channel = body.channel;
954 var subscription = this.getSubscription(channel);
955 if (subscription === null) {
956 return;
957 }
958 subscription.trigger('message', [body]);
959 };
960
961 centrifuge_proto._dispatchMessage = function(message) {
962 if (message === undefined || message === null) {
963 return;
964 }
965
966 var method = message.method;
967
968 if (!method) {
969 return;
970 }
971
972 switch (method) {
973 case 'connect':
974 this._connectResponse(message);
975 break;
976 case 'disconnect':
977 this._disconnectResponse(message);
978 break;
979 case 'subscribe':
980 this._subscribeResponse(message);
981 break;
982 case 'unsubscribe':
983 this._unsubscribeResponse(message);
984 break;
985 case 'publish':
986 this._publishResponse(message);
987 break;
988 case 'presence':
989 this._presenceResponse(message);
990 break;
991 case 'history':
992 this._historyResponse(message);
993 break;
994 case 'join':
995 this._joinResponse(message);
996 break;
997 case 'leave':
998 this._leaveResponse(message);
999 break;
1000 case 'ping':
1001 break;
1002 case 'message':
1003 this._messageResponse(message);
1004 break;
1005 default:
1006 break;
1007 }
1008 };
1009
1010 centrifuge_proto._receive = function (data) {
1011 if (Object.prototype.toString.call(data) === Object.prototype.toString.call([])) {
1012 for (var i in data) {
1013 if (data.hasOwnProperty(i)) {
1014 var msg = data[i];
1015 this._dispatchMessage(msg);
1016 }
1017 }
1018 } else if (Object.prototype.toString.call(data) === Object.prototype.toString.call({})) {
1019 this._dispatchMessage(data);
1020 }
1021 };
1022
1023 centrifuge_proto._flush = function() {
1024 var messages = this._messages.slice(0);
1025 this._messages = [];
1026 this._send(messages);
1027 };
1028
1029 centrifuge_proto._ping = function () {
1030 var centrifugeMessage = {
1031 "method": "ping",
1032 "params": {}
1033 };
1034 this.send(centrifugeMessage);
1035 };
1036
1037 /* PUBLIC API */
1038
1039 centrifuge_proto.getClientId = function () {
1040 return this._clientId;
1041 };
1042
1043 centrifuge_proto.isConnected = centrifuge_proto._isConnected;
1044
1045 centrifuge_proto.isDisconnected = centrifuge_proto._isDisconnected;
1046
1047 centrifuge_proto.configure = function (configuration) {
1048 this._configure.call(this, configuration);
1049 };
1050
1051 centrifuge_proto.connect = centrifuge_proto._connect;
1052
1053 centrifuge_proto.disconnect = centrifuge_proto._disconnect;
1054
1055 centrifuge_proto.getSubscription = centrifuge_proto._getSubscription;
1056
1057 centrifuge_proto.ping = centrifuge_proto._ping;
1058
1059 centrifuge_proto.send = function (message) {
1060 if (this._isBatching === true) {
1061 this._messages.push(message);
1062 } else {
1063 this._send([message]);
1064 }
1065 };
1066
1067 centrifuge_proto.startBatching = function () {
1068 // start collecting messages without sending them to Centrifuge until flush
1069 // method called
1070 this._isBatching = true;
1071 };
1072
1073 centrifuge_proto.stopBatching = function(flush) {
1074 // stop collecting messages
1075 flush = flush || false;
1076 this._isBatching = false;
1077 if (flush === true) {
1078 this.flush();
1079 }
1080 };
1081
1082 centrifuge_proto.flush = function() {
1083 this._flush();
1084 };
1085
1086 centrifuge_proto.subscribe = function (channel, callback) {
1087
1088 if (arguments.length < 1) {
1089 throw 'Illegal arguments number: required 1, got ' + arguments.length;
1090 }
1091 if (!isString(channel)) {
1092 throw 'Illegal argument type: channel must be a string';
1093 }
1094 if (this.isDisconnected()) {
1095 throw 'Illegal state: already disconnected';
1096 }
1097
1098 var current_subscription = this.getSubscription(channel);
1099
1100 if (current_subscription !== null) {
1101 return current_subscription;
1102 } else {
1103 var subscription = new Subscription(this, channel);
1104 this._subscriptions[channel] = subscription;
1105 subscription.subscribe(callback);
1106 return subscription;
1107 }
1108 };
1109
1110 centrifuge_proto.unsubscribe = function (channel) {
1111 if (arguments.length < 1) {
1112 throw 'Illegal arguments number: required 1, got ' + arguments.length;
1113 }
1114 if (!isString(channel)) {
1115 throw 'Illegal argument type: channel must be a string';
1116 }
1117 if (this.isDisconnected()) {
1118 return;
1119 }
1120
1121 var subscription = this.getSubscription(channel);
1122 if (subscription !== null) {
1123 subscription.unsubscribe();
1124 }
1125 };
1126
1127 centrifuge_proto.publish = function (channel, data, callback) {
1128 var subscription = this.getSubscription(channel);
1129 if (subscription === null) {
1130 this._debug("subscription not found for channel " + channel);
1131 return null;
1132 }
1133 subscription.publish(data, callback);
1134 return subscription;
1135 };
1136
1137 centrifuge_proto.presence = function (channel, callback) {
1138 var subscription = this.getSubscription(channel);
1139 if (subscription === null) {
1140 this._debug("subscription not found for channel " + channel);
1141 return null;
1142 }
1143 subscription.presence(callback);
1144 return subscription;
1145 };
1146
1147 centrifuge_proto.history = function (channel, callback) {
1148 var subscription = this.getSubscription(channel);
1149 if (subscription === null) {
1150 this._debug("subscription not found for channel " + channel);
1151 return null;
1152 }
1153 subscription.history(callback);
1154 return subscription;
1155 };
1156
1157 function Subscription(centrifuge, channel) {
1158 /**
1159 * The constructor for a centrifuge object, identified by an optional name.
1160 * The default name is the string 'default'.
1161 * @param name the optional name of this centrifuge object
1162 */
1163 this._centrifuge = centrifuge;
1164 this.channel = channel;
1165 }
1166
1167 extend(Subscription, EventEmitter);
1168
1169 var sub_proto = Subscription.prototype;
1170
1171 sub_proto.getChannel = function () {
1172 return this.channel;
1173 };
1174
1175 sub_proto.getCentrifuge = function () {
1176 return this._centrifuge;
1177 };
1178
1179 sub_proto.subscribe = function (callback) {
1180 var centrifugeMessage = {
1181 "method": "subscribe",
1182 "params": {
1183 "channel": this.channel
1184 }
1185 };
1186 this._centrifuge.send(centrifugeMessage);
1187 if (callback) {
1188 this.on('message', callback);
1189 }
1190 };
1191
1192 sub_proto.unsubscribe = function () {
1193 this._centrifuge._removeSubscription(this.channel);
1194 var centrifugeMessage = {
1195 "method": "unsubscribe",
1196 "params": {
1197 "channel": this.channel
1198 }
1199 };
1200 this._centrifuge.send(centrifugeMessage);
1201 };
1202
1203 sub_proto.publish = function (data, callback) {
1204 var centrifugeMessage = {
1205 "method": "publish",
1206 "params": {
1207 "channel": this.channel,
1208 "data": data
1209 }
1210 };
1211 if (callback) {
1212 this.on('publish:success', callback);
1213 }
1214 this._centrifuge.send(centrifugeMessage);
1215 };
1216
1217 sub_proto.presence = function (callback) {
1218 var centrifugeMessage = {
1219 "method": "presence",
1220 "params": {
1221 "channel": this.channel
1222 }
1223 };
1224 if (callback) {
1225 this.on('presence', callback);
1226 }
1227 this._centrifuge.send(centrifugeMessage);
1228 };
1229
1230 sub_proto.history = function (callback) {
1231 var centrifugeMessage = {
1232 "method": "history",
1233 "params": {
1234 "channel": this.channel
1235 }
1236 };
1237 if (callback) {
1238 this.on('history', callback);
1239 }
1240 this._centrifuge.send(centrifugeMessage);
1241 };
1242
1243 // Expose the class either via AMD, CommonJS or the global object
1244 if (typeof define === 'function' && define.amd) {
1245 define(function () {
1246 return Centrifuge;
1247 });
1248 } else if (typeof module === 'object' && module.exports) {
1249 //noinspection JSUnresolvedVariable
1250 module.exports = Centrifuge;
1251 } else {
1252 //noinspection JSUnusedGlobalSymbols
1253 this.Centrifuge = Centrifuge;
1254 }
1255
1256 }.call(this));
@@ -3,7 +3,6 b' from boards import utils'
3 from boards.models import Ban
3 from boards.models import Ban
4 from django.utils.html import strip_spaces_between_tags
4 from django.utils.html import strip_spaces_between_tags
5 from django.conf import settings
5 from django.conf import settings
6 from boards.views.banned import BannedView
7
6
8 RESPONSE_CONTENT_TYPE = 'Content-Type'
7 RESPONSE_CONTENT_TYPE = 'Content-Type'
9
8
@@ -21,7 +20,7 b' class BanMiddleware:'
21
20
22 def process_view(self, request, view_func, view_args, view_kwargs):
21 def process_view(self, request, view_func, view_args, view_kwargs):
23
22
24 if view_func != BannedView.as_view:
23 if request.path != '/banned/':
25 ip = utils.get_client_ip(request)
24 ip = utils.get_client_ip(request)
26 bans = Ban.objects.filter(ip=ip)
25 bans = Ban.objects.filter(ip=ip)
27
26
@@ -1,19 +1,25 b''
1 from datetime import datetime, timedelta, date
1 from datetime import datetime, timedelta, date
2 from datetime import time as dtime
2 from datetime import time as dtime
3 from adjacent import Client
3 import logging
4 import logging
4 import re
5 import re
5
6
6 from django.core.cache import cache
7 from django.core.cache import cache
7 from django.core.urlresolvers import reverse
8 from django.core.urlresolvers import reverse
8 from django.db import models, transaction
9 from django.db import models, transaction
10 from django.shortcuts import get_object_or_404
11 from django.template import RequestContext
9 from django.template.loader import render_to_string
12 from django.template.loader import render_to_string
10 from django.utils import timezone
13 from django.utils import timezone
11 from markupfield.fields import MarkupField
14 from markupfield.fields import MarkupField
15 from boards import settings
12
16
13 from boards.models import PostImage
17 from boards.models import PostImage
14 from boards.models.base import Viewable
18 from boards.models.base import Viewable
15 from boards.models.thread import Thread
19 from boards.models.thread import Thread
20 from boards.utils import datetime_to_epoch
16
21
22 WS_CHANNEL_THREAD = "thread:"
17
23
18 APP_LABEL_BOARDS = 'boards'
24 APP_LABEL_BOARDS = 'boards'
19
25
@@ -38,6 +44,14 b" UNKNOWN_UA = ''"
38
44
39 REGEX_REPLY = re.compile(r'\[post\](\d+)\[/post\]')
45 REGEX_REPLY = re.compile(r'\[post\](\d+)\[/post\]')
40
46
47 PARAMETER_TRUNCATED = 'truncated'
48 PARAMETER_TAG = 'tag'
49 PARAMETER_OFFSET = 'offset'
50 PARAMETER_DIFF_TYPE = 'type'
51
52 DIFF_TYPE_HTML = 'html'
53 DIFF_TYPE_JSON = 'json'
54
41 logger = logging.getLogger(__name__)
55 logger = logging.getLogger(__name__)
42
56
43
57
@@ -117,7 +131,7 b' class PostManager(models.Manager):'
117 for post in posts:
131 for post in posts:
118 self.delete_post(post)
132 self.delete_post(post)
119
133
120 def connect_replies(self, post):
134 def connect_replies(self, post):
121 """
135 """
122 Connects replies to a post to show them as a reflink map
136 Connects replies to a post to show them as a reflink map
123 """
137 """
@@ -344,3 +358,60 b' class Post(models.Model, Viewable):'
344 self.images.all().delete()
358 self.images.all().delete()
345
359
346 super(Post, self).delete(using)
360 super(Post, self).delete(using)
361
362 def get_post_data(self, format_type=DIFF_TYPE_JSON, request=None,
363 include_last_update=False):
364 """
365 Gets post HTML or JSON data that can be rendered on a page or used by
366 API.
367 """
368
369 if format_type == DIFF_TYPE_HTML:
370 context = RequestContext(request)
371 context['post'] = self
372 if PARAMETER_TRUNCATED in request.GET:
373 context[PARAMETER_TRUNCATED] = True
374
375 return render_to_string('boards/api_post.html', context)
376 elif format_type == DIFF_TYPE_JSON:
377 post_json = {
378 'id': self.id,
379 'title': self.title,
380 'text': self.text.rendered,
381 }
382 if self.images.exists():
383 post_image = self.get_first_image()
384 post_json['image'] = post_image.image.url
385 post_json['image_preview'] = post_image.image.url_200x150
386 if include_last_update:
387 post_json['bump_time'] = datetime_to_epoch(
388 self.thread_new.bump_time)
389 return post_json
390
391 def send_to_websocket(self, request, recursive=True):
392 """
393 Sends post HTML data to the thread web socket.
394 """
395
396 if not settings.WEBSOCKETS_ENABLED:
397 return
398
399 client = Client()
400
401 channel_name = WS_CHANNEL_THREAD + str(self.get_thread().get_opening_post_id())
402 client.publish(channel_name, {
403 'html': self.get_post_data(
404 format_type=DIFF_TYPE_HTML,
405 request=request),
406 'diff_type': 'added' if recursive else 'updated',
407 })
408 client.send()
409
410 logger.info('Sent post #{} to channel {}'.format(self.id, channel_name))
411
412 if recursive:
413 for reply_number in re.finditer(REGEX_REPLY, self.text.raw):
414 post_id = reply_number.group(1)
415 ref_post = Post.objects.filter(id=post_id)[0]
416
417 ref_post.send_to_websocket(request, recursive=False) No newline at end of file
@@ -18,3 +18,5 b' LAST_REPLIES_COUNT = 3'
18 ARCHIVE_THREADS = True
18 ARCHIVE_THREADS = True
19 # Limit posting speed
19 # Limit posting speed
20 LIMIT_POSTING_SPEED = False
20 LIMIT_POSTING_SPEED = False
21 # Thread update
22 WEBSOCKETS_ENABLED = True No newline at end of file
@@ -23,12 +23,90 b''
23 for the JavaScript code in this page.
23 for the JavaScript code in this page.
24 */
24 */
25
25
26 var THREAD_UPDATE_DELAY = 10000;
26 var wsUrl = 'ws://localhost:9090/connection/websocket';
27 var wsUser = '';
27
28
28 var loading = false;
29 var loading = false;
29 var lastUpdateTime = null;
30 var lastUpdateTime = null;
30 var unreadPosts = 0;
31 var unreadPosts = 0;
31
32
33 // Thread ID does not change, can be stored one time
34 var threadId = $('div.thread').children('.post').first().attr('id');
35
36 function connectWebsocket() {
37 var metapanel = $('.metapanel')[0];
38
39 var wsHost = metapanel.getAttribute('data-ws-host');
40 var wsPort = metapanel.getAttribute('data-ws-port');
41
42 if (wsHost.length > 0 && wsPort.length > 0)
43 var centrifuge = new Centrifuge({
44 "url": 'ws://' + wsHost + ':' + wsPort + "/connection/websocket",
45 "project": metapanel.getAttribute('data-ws-project'),
46 "user": wsUser,
47 "timestamp": metapanel.getAttribute('data-last-update'),
48 "token": metapanel.getAttribute('data-ws-token'),
49 "debug": true
50 });
51
52 centrifuge.on('error', function(error_message) {
53 alert("Error connecting to websocket server.");
54 });
55
56 centrifuge.on('connect', function() {
57 var channelName = 'thread:' + threadId;
58 centrifuge.subscribe(channelName, function(message) {
59 var postHtml = message.data['html'];
60 var isAdded = (message.data['diff_type'] === 'added');
61
62 if (postHtml) {
63 updatePost(postHtml, isAdded);
64 }
65 });
66
67 $('#autoupdate').text('[+]');
68 });
69
70 centrifuge.connect();
71 }
72
73 function updatePost(postHtml, isAdded) {
74 // This needs to be set on start because the page is scrolled after posts
75 // are added or updated
76 var bottom = isPageBottom();
77
78 var post = $(postHtml);
79
80 var threadPosts = $('div.thread').children('.post');
81
82 var lastUpdate = '';
83
84 if (isAdded) {
85 var lastPost = threadPosts.last();
86
87 post.appendTo(lastPost.parent());
88
89 updateBumplimitProgress(1);
90 showNewPostsTitle(1);
91
92 lastUpdate = post.children('.post-info').first()
93 .children('.pub_time').first().text();
94
95 if (bottom) {
96 scrollToBottom();
97 }
98 } else {
99 var postId = post.attr('id');
100
101 var oldPost = $('div.thread').children('.post[id=' + postId + ']');
102
103 oldPost.replaceWith(post);
104 }
105
106 processNewPost(post);
107 updateMetadataPanel(lastUpdate)
108 }
109
32 function blink(node) {
110 function blink(node) {
33 var blinkCount = 2;
111 var blinkCount = 2;
34
112
@@ -38,90 +116,6 b' function blink(node) {'
38 }
116 }
39 }
117 }
40
118
41 function updateThread() {
42 if (loading) {
43 return;
44 }
45
46 loading = true;
47
48 var threadPosts = $('div.thread').children('.post');
49
50 var lastPost = threadPosts.last();
51 var threadId = threadPosts.first().attr('id');
52
53 var diffUrl = '/api/diff_thread/' + threadId + '/' + lastUpdateTime + '/';
54 $.getJSON(diffUrl)
55 .success(function(data) {
56 var bottom = isPageBottom();
57
58 var lastUpdate = '';
59
60 var addedPosts = data.added;
61 for (var i = 0; i < addedPosts.length; i++) {
62 var postText = addedPosts[i];
63
64 var post = $(postText);
65
66 if (lastUpdate === '') {
67 lastUpdate = post.find('.pub_time').text();
68 }
69
70 post.appendTo(lastPost.parent());
71 processNewPost(post);
72
73 lastPost = post;
74 blink(post);
75 }
76
77 var updatedPosts = data.updated;
78 for (var i = 0; i < updatedPosts.length; i++) {
79 var postText = updatedPosts[i];
80
81 var post = $(postText);
82
83 if (lastUpdate === '') {
84 lastUpdate = post.find('.pub_time').text();
85 }
86
87 var postId = post.attr('id');
88
89 var oldPost = $('div.thread').children('.post[id=' + postId + ']');
90
91 oldPost.replaceWith(post);
92 processNewPost(post);
93
94 blink(post);
95 }
96
97 // TODO Process deleted posts
98
99 lastUpdateTime = data.last_update;
100 loading = false;
101
102 if (bottom) {
103 scrollToBottom();
104 }
105
106 var hasPostChanges = (updatedPosts.length > 0)
107 || (addedPosts.length > 0);
108 if (hasPostChanges) {
109 updateMetadataPanel(lastUpdate);
110 }
111
112 updateBumplimitProgress(data.added.length);
113
114 if (data.added.length + data.updated.length > 0) {
115 showNewPostsTitle(data.added.length);
116 }
117 })
118 .error(function(data) {
119 // TODO Show error message that server is unavailable?
120
121 loading = false;
122 });
123 }
124
125 function isPageBottom() {
119 function isPageBottom() {
126 var scroll = $(window).scrollTop() / ($(document).height()
120 var scroll = $(window).scrollTop() / ($(document).height()
127 - $(window).height())
121 - $(window).height())
@@ -130,11 +124,7 b' function isPageBottom() {'
130 }
124 }
131
125
132 function initAutoupdate() {
126 function initAutoupdate() {
133 loading = false;
127 connectWebsocket()
134
135 lastUpdateTime = $('.metapanel').attr('data-last-update');
136
137 setInterval(updateThread, THREAD_UPDATE_DELAY);
138 }
128 }
139
129
140 function getReplyCount() {
130 function getReplyCount() {
@@ -230,7 +220,6 b' function updateOnPost(response, statusTe'
230
220
231 if (status === 'ok') {
221 if (status === 'ok') {
232 resetForm(form);
222 resetForm(form);
233 updateThread();
234 } else {
223 } else {
235 var errors = json.errors;
224 var errors = json.errors;
236 for (var i = 0; i < errors.length; i++) {
225 for (var i = 0; i < errors.length; i++) {
@@ -264,6 +253,7 b' function showAsErrors(form, text) {'
264 function processNewPost(post) {
253 function processNewPost(post) {
265 addRefLinkPreview(post[0]);
254 addRefLinkPreview(post[0]);
266 highlightCode(post);
255 highlightCode(post);
256 blink(post);
267 }
257 }
268
258
269 $(document).ready(function(){
259 $(document).ready(function(){
@@ -68,8 +68,11 b''
68 </div>
68 </div>
69 </div>
69 </div>
70
70
71 <script src="{% static 'js/jquery.form.min.js' %}"></script>
71 <script src="{% static 'js/jquery.form.min.js' %}"></script>
72 <script src="{% static 'js/thread_update.js' %}"></script>
72 {% compress js %}
73 <script src="{% static 'js/thread_update.js' %}"></script>
74 <script src="{% static 'js/3party/centrifuge.js' %}"></script>
75 {% endcompress %}
73 {% endif %}
76 {% endif %}
74
77
75 {% compress js %}
78 {% compress js %}
@@ -86,8 +89,14 b''
86
89
87 {% get_current_language as LANGUAGE_CODE %}
90 {% get_current_language as LANGUAGE_CODE %}
88
91
89 <span class="metapanel" data-last-update="{{ last_update }}">
92 <span class="metapanel"
93 data-last-update="{{ last_update }}"
94 data-ws-token="{{ ws_token }}"
95 data-ws-project="{{ ws_project }}"
96 data-ws-host="{{ ws_host }}"
97 data-ws-port="{{ ws_port }}">
90 {% cache 600 thread_meta thread.last_edit_time moderator LANGUAGE_CODE %}
98 {% cache 600 thread_meta thread.last_edit_time moderator LANGUAGE_CODE %}
99 <span id="autoupdate">[-]</span>
91 <span id="reply-count">{{ thread.get_reply_count }}</span>/{{ max_replies }} {% trans 'messages' %},
100 <span id="reply-count">{{ thread.get_reply_count }}</span>/{{ max_replies }} {% trans 'messages' %},
92 <span id="image-count">{{ thread.get_images_count }}</span> {% trans 'images' %}.
101 <span id="image-count">{{ thread.get_images_count }}</span> {% trans 'images' %}.
93 {% trans 'Last update: ' %}<span id="last-update">{{ thread.last_edit_time }}</span>
102 {% trans 'Last update: ' %}<span id="last-update">{{ thread.last_edit_time }}</span>
@@ -1,8 +1,8 b''
1 """
1 """
2 This module contains helper functions and helper classes.
2 This module contains helper functions and helper classes.
3 """
3 """
4 import hashlib
5 import time
4 import time
5 import hmac
6
6
7 from django.utils import timezone
7 from django.utils import timezone
8
8
@@ -76,3 +76,17 b' def datetime_to_epoch(datetime):'
76 return int(time.mktime(timezone.localtime(
76 return int(time.mktime(timezone.localtime(
77 datetime,timezone.get_current_timezone()).timetuple())
77 datetime,timezone.get_current_timezone()).timetuple())
78 * 1000000 + datetime.microsecond)
78 * 1000000 + datetime.microsecond)
79
80
81 def get_websocket_token(user_id='', timestamp=''):
82 """
83 Create token to validate information provided by new connection.
84 """
85
86 sign = hmac.new(settings.CENTRIFUGE_PROJECT_SECRET.encode())
87 sign.update(settings.CENTRIFUGE_PROJECT_ID.encode())
88 sign.update(user_id.encode())
89 sign.update(timestamp.encode())
90 token = sign.hexdigest()
91
92 return token No newline at end of file
@@ -126,6 +126,8 b' class AllThreadsView(PostMixin, BaseBoar'
126
126
127 post = Post.objects.create_post(title=title, text=text, image=image,
127 post = Post.objects.create_post(title=title, text=text, image=image,
128 ip=ip, tags=tags)
128 ip=ip, tags=tags)
129 # FIXME
130 post.send_to_websocket(request)
129
131
130 if html_response:
132 if html_response:
131 return redirect(post.get_url())
133 return redirect(post.get_url())
@@ -61,9 +61,9 b' def api_get_threaddiff(request, thread_i'
61 diff_type = request.GET[PARAMETER_DIFF_TYPE]
61 diff_type = request.GET[PARAMETER_DIFF_TYPE]
62
62
63 for post in added_posts:
63 for post in added_posts:
64 json_data['added'].append(_get_post_data(post.id, diff_type, request))
64 json_data['added'].append(get_post_data(post.id, diff_type, request))
65 for post in updated_posts:
65 for post in updated_posts:
66 json_data['updated'].append(_get_post_data(post.id, diff_type, request))
66 json_data['updated'].append(get_post_data(post.id, diff_type, request))
67 json_data['last_update'] = datetime_to_epoch(thread.last_edit_time)
67 json_data['last_update'] = datetime_to_epoch(thread.last_edit_time)
68
68
69 return HttpResponse(content=json.dumps(json_data))
69 return HttpResponse(content=json.dumps(json_data))
@@ -156,7 +156,7 b' def api_get_threads(request, count):'
156 opening_post = thread.get_opening_post()
156 opening_post = thread.get_opening_post()
157
157
158 # TODO Add tags, replies and images count
158 # TODO Add tags, replies and images count
159 opening_posts.append(_get_post_data(opening_post.id,
159 opening_posts.append(get_post_data(opening_post.id,
160 include_last_update=True))
160 include_last_update=True))
161
161
162 return HttpResponse(content=json.dumps(opening_posts))
162 return HttpResponse(content=json.dumps(opening_posts))
@@ -196,7 +196,7 b' def api_get_thread_posts(request, openin'
196 json_post_list = []
196 json_post_list = []
197
197
198 for post in posts:
198 for post in posts:
199 json_post_list.append(_get_post_data(post.id))
199 json_post_list.append(get_post_data(post.id))
200 json_data['last_update'] = datetime_to_epoch(thread.last_edit_time)
200 json_data['last_update'] = datetime_to_epoch(thread.last_edit_time)
201 json_data['posts'] = json_post_list
201 json_data['posts'] = json_post_list
202
202
@@ -219,30 +219,9 b' def api_get_post(request, post_id):'
219 return HttpResponse(content=json)
219 return HttpResponse(content=json)
220
220
221
221
222 # TODO Add pub time and replies
222 # TODO Remove this method and use post method directly
223 def _get_post_data(post_id, format_type=DIFF_TYPE_JSON, request=None,
223 def get_post_data(post_id, format_type=DIFF_TYPE_JSON, request=None,
224 include_last_update=False):
224 include_last_update=False):
225 if format_type == DIFF_TYPE_HTML:
225 post = get_object_or_404(Post, id=post_id)
226 post = get_object_or_404(Post, id=post_id)
226 return post.get_post_data(format_type=format_type, request=request,
227
227 include_last_update=include_last_update)
228 context = RequestContext(request)
229 context['post'] = post
230 if PARAMETER_TRUNCATED in request.GET:
231 context[PARAMETER_TRUNCATED] = True
232
233 return render_to_string('boards/api_post.html', context)
234 elif format_type == DIFF_TYPE_JSON:
235 post = get_object_or_404(Post, id=post_id)
236 post_json = {
237 'id': post.id,
238 'title': post.title,
239 'text': post.text.rendered,
240 }
241 if post.images.exists():
242 post_image = post.get_first_image()
243 post_json['image'] = post_image.image.url
244 post_json['image_preview'] = post_image.image.url_200x150
245 if include_last_update:
246 post_json['bump_time'] = datetime_to_epoch(
247 post.thread_new.bump_time)
248 return post_json
@@ -10,6 +10,7 b' from boards.models import Post, Ban'
10 from boards.views.banned import BannedView
10 from boards.views.banned import BannedView
11 from boards.views.base import BaseBoardView, CONTEXT_FORM
11 from boards.views.base import BaseBoardView, CONTEXT_FORM
12 from boards.views.posting_mixin import PostMixin
12 from boards.views.posting_mixin import PostMixin
13 import neboard
13
14
14 TEMPLATE_GALLERY = 'boards/thread_gallery.html'
15 TEMPLATE_GALLERY = 'boards/thread_gallery.html'
15 TEMPLATE_NORMAL = 'boards/thread.html'
16 TEMPLATE_NORMAL = 'boards/thread.html'
@@ -22,6 +23,10 b' CONTEXT_LASTUPDATE = "last_update"'
22 CONTEXT_MAX_REPLIES = 'max_replies'
23 CONTEXT_MAX_REPLIES = 'max_replies'
23 CONTEXT_THREAD = 'thread'
24 CONTEXT_THREAD = 'thread'
24 CONTEXT_BUMPABLE = 'bumpable'
25 CONTEXT_BUMPABLE = 'bumpable'
26 CONTEXT_WS_TOKEN = 'ws_token'
27 CONTEXT_WS_PROJECT = 'ws_project'
28 CONTEXT_WS_HOST = 'ws_host'
29 CONTEXT_WS_PORT = 'ws_port'
25
30
26 FORM_TITLE = 'title'
31 FORM_TITLE = 'title'
27 FORM_TEXT = 'text'
32 FORM_TEXT = 'text'
@@ -51,11 +56,18 b' class ThreadView(BaseBoardView, PostMixi'
51 context = self.get_context_data(request=request)
56 context = self.get_context_data(request=request)
52
57
53 context[CONTEXT_FORM] = form
58 context[CONTEXT_FORM] = form
54 context[CONTEXT_LASTUPDATE] = utils.datetime_to_epoch(
59 context[CONTEXT_LASTUPDATE] = str(utils.datetime_to_epoch(
55 thread_to_show.last_edit_time)
60 thread_to_show.last_edit_time))
56 context[CONTEXT_THREAD] = thread_to_show
61 context[CONTEXT_THREAD] = thread_to_show
57 context[CONTEXT_MAX_REPLIES] = settings.MAX_POSTS_PER_THREAD
62 context[CONTEXT_MAX_REPLIES] = settings.MAX_POSTS_PER_THREAD
58
63
64 if settings.WEBSOCKETS_ENABLED:
65 context[CONTEXT_WS_TOKEN] = utils.get_websocket_token(
66 timestamp=context[CONTEXT_LASTUPDATE])
67 context[CONTEXT_WS_PROJECT] = neboard.settings.CENTRIFUGE_PROJECT_ID
68 context[CONTEXT_WS_HOST] = request.get_host().split(':')[0]
69 context[CONTEXT_WS_PORT] = neboard.settings.CENTRIFUGE_PORT
70
59 if MODE_NORMAL == mode:
71 if MODE_NORMAL == mode:
60 bumpable = thread_to_show.can_bump()
72 bumpable = thread_to_show.can_bump()
61 context[CONTEXT_BUMPABLE] = bumpable
73 context[CONTEXT_BUMPABLE] = bumpable
@@ -130,6 +142,7 b' class ThreadView(BaseBoardView, PostMixi'
130
142
131 post = Post.objects.create_post(title=title, text=text, image=image,
143 post = Post.objects.create_post(title=title, text=text, image=image,
132 thread=post_thread, ip=ip, tags=tags)
144 thread=post_thread, ip=ip, tags=tags)
145 post.send_to_websocket(request)
133
146
134 thread_to_show = (opening_post.id if opening_post else post.id)
147 thread_to_show = (opening_post.id if opening_post else post.id)
135
148
@@ -212,11 +212,18 b' THEMES = ['
212 ('pg', 'Photon Gray'),
212 ('pg', 'Photon Gray'),
213 ]
213 ]
214
214
215 POPULAR_TAGS = 10
216
217 POSTING_DELAY = 20 # seconds
215 POSTING_DELAY = 20 # seconds
218
216
219 COMPRESS_HTML = True
217 COMPRESS_HTML = False
218
219 # Websocket settins
220 CENTRIFUGE_HOST = 'localhost'
221 CENTRIFUGE_PORT = '9090'
222
223 CENTRIFUGE_ADDRESS = 'http://{}:{}'.format(CENTRIFUGE_HOST, CENTRIFUGE_PORT)
224 CENTRIFUGE_PROJECT_ID = '<project id here>'
225 CENTRIFUGE_PROJECT_SECRET = '<project secret here>'
226 CENTRIFUGE_TIMEOUT = 5
220
227
221 # Debug mode middlewares
228 # Debug mode middlewares
222 if DEBUG:
229 if DEBUG:
@@ -235,5 +242,4 b' if DEBUG:'
235 # FIXME Uncommenting this fails somehow. Need to investigate this
242 # FIXME Uncommenting this fails somehow. Need to investigate this
236 #DEBUG_TOOLBAR_PANELS += (
243 #DEBUG_TOOLBAR_PANELS += (
237 # 'debug_toolbar.panels.profiling.ProfilingDebugPanel',
244 # 'debug_toolbar.panels.profiling.ProfilingDebugPanel',
238 #)
245 #) No newline at end of file
239
@@ -1,3 +1,4 b''
1 adjacent
1 south>=0.8.4
2 south>=0.8.4
2 haystack
3 haystack
3 pillow
4 pillow
General Comments 0
You need to be logged in to leave comments. Login now