Merge remote-tracking branch 'origin/development' into M2ys4U-server-time
[KiwiIRC.git] / client / src / models / network.js
1 (function () {
2
3 _kiwi.model.Network = Backbone.Model.extend({
4 defaults: {
5 connection_id: 0,
6 /**
7 * The name of the network
8 * @type String
9 */
10 name: 'Network',
11
12 /**
13 * The address (URL) of the network
14 * @type String
15 */
16 address: '',
17
18 /**
19 * The port for the network
20 * @type Int
21 */
22 port: 6667,
23
24 /**
25 * If this network uses SSL
26 * @type Bool
27 */
28 ssl: false,
29
30 /**
31 * The password to connect to this network
32 * @type String
33 */
34 password: '',
35
36 /**
37 * The current nickname
38 * @type String
39 */
40 nick: '',
41
42 /**
43 * The channel prefix for this network
44 * @type String
45 */
46 channel_prefix: '#',
47
48 /**
49 * The user prefixes for channel owner/admin/op/voice etc. on this network
50 * @type Array
51 */
52 user_prefixes: ['~', '&', '@', '+']
53 },
54
55
56 initialize: function () {
57 // If we already have a connection, bind our events
58 if (typeof this.get('connection_id') !== 'undefined') {
59 this.gateway = _kiwi.global.components.Network(this.get('connection_id'));
60 this.bindGatewayEvents();
61 }
62
63 // Create our panel list (tabs)
64 this.panels = new _kiwi.model.PanelList([], this);
65 //this.panels.network = this;
66
67 // Automatically create a server tab
68 var server_panel = new _kiwi.model.Server({name: 'Server'});
69 this.panels.add(server_panel);
70 this.panels.server = this.panels.active = server_panel;
71 },
72
73
74 reconnect: function(callback_fn) {
75 var that = this,
76 server_info = {
77 nick: this.get('nick'),
78 host: this.get('address'),
79 port: this.get('port'),
80 ssl: this.get('ssl'),
81 password: this.get('password')
82 };
83
84 _kiwi.gateway.makeIrcConnection(server_info, function(err, connection_id) {
85 if (!err) {
86 that.gateway.dispose();
87
88 that.set('connection_id', connection_id);
89 that.gateway = _kiwi.global.components.Network(that.get('connection_id'));
90 that.bindGatewayEvents();
91
92 callback_fn && callback_fn(err);
93
94 } else {
95 console.log("_kiwi.gateway.socket.on('error')", {reason: err});
96 callback_fn && callback_fn(err);
97 }
98 });
99 },
100
101
102 bindGatewayEvents: function () {
103 //this.gateway.on('all', function() {console.log('ALL', this.get('connection_id'), arguments);});
104
105 this.gateway.on('connect', onConnect, this);
106 this.gateway.on('disconnect', onDisconnect, this);
107
108 this.gateway.on('nick', function(event) {
109 if (event.nick === this.get('nick')) {
110 this.set('nick', event.newnick);
111 }
112 }, this);
113
114 this.gateway.on('options', onOptions, this);
115 this.gateway.on('motd', onMotd, this);
116 this.gateway.on('join', onJoin, this);
117 this.gateway.on('part', onPart, this);
118 this.gateway.on('quit', onQuit, this);
119 this.gateway.on('kick', onKick, this);
120 this.gateway.on('msg', onMsg, this);
121 this.gateway.on('nick', onNick, this);
122 this.gateway.on('ctcp_request', onCtcpRequest, this);
123 this.gateway.on('ctcp_response', onCtcpResponse, this);
124 this.gateway.on('notice', onNotice, this);
125 this.gateway.on('action', onAction, this);
126 this.gateway.on('topic', onTopic, this);
127 this.gateway.on('topicsetby', onTopicSetBy, this);
128 this.gateway.on('userlist', onUserlist, this);
129 this.gateway.on('userlist_end', onUserlistEnd, this);
130 this.gateway.on('mode', onMode, this);
131 this.gateway.on('whois', onWhois, this);
132 this.gateway.on('whowas', onWhowas, this);
133 this.gateway.on('away', onAway, this);
134 this.gateway.on('list_start', onListStart, this);
135 this.gateway.on('irc_error', onIrcError, this);
136 this.gateway.on('unknown_command', onUnknownCommand, this);
137 },
138
139
140 /**
141 * Create panels and join the channel
142 * This will not wait for the join event to create a panel. This
143 * increases responsiveness in case of network lag
144 */
145 createAndJoinChannels: function (channels) {
146 var that = this,
147 panels = [];
148
149 // Multiple channels may come as comma-delimited
150 if (typeof channels === 'string') {
151 channels = channels.split(',');
152 }
153
154 $.each(channels, function (index, channel_name_key) {
155 // We may have a channel key so split it off
156 var spli = channel_name_key.trim().split(' '),
157 channel_name = spli[0],
158 channel_key = spli[1] || '';
159
160 // Trim any whitespace off the name
161 channel_name = channel_name.trim();
162
163 // If not a valid channel name, display a warning
164 if (!_kiwi.app.isChannelName(channel_name)) {
165 that.panels.server.addMsg('', _kiwi.global.i18n.translate('client_models_network_channel_invalid_name').fetch(channel_name));
166 _kiwi.app.message.text(_kiwi.global.i18n.translate('client_models_network_channel_invalid_name').fetch(channel_name), {timeout: 5000});
167 return;
168 }
169
170 // Check if we have the panel already. If not, create it
171 channel = that.panels.getByName(channel_name);
172 if (!channel) {
173 channel = new _kiwi.model.Channel({name: channel_name});
174 that.panels.add(channel);
175 }
176
177 panels.push(channel);
178
179 that.gateway.join(channel_name, channel_key);
180 });
181
182 return panels;
183 },
184
185
186 /**
187 * Join all the open channels we have open
188 * Reconnecting to a network would typically call this.
189 */
190 rejoinAllChannels: function() {
191 var that = this;
192
193 this.panels.forEach(function(panel) {
194 if (!panel.isChannel())
195 return;
196
197 that.gateway.join(panel.get('name'));
198 });
199 }
200 });
201
202
203
204 function onDisconnect(event) {
205 $.each(this.panels.models, function (index, panel) {
206 panel.addMsg('', _kiwi.global.i18n.translate('client_models_network_disconnected').fetch(), 'action quit');
207 });
208 }
209
210
211
212 function onConnect(event) {
213 var panels, channel_names;
214
215 // Update our nick with what the network gave us
216 this.set('nick', event.nick);
217
218 // If this is a re-connection then we may have some channels to re-join
219 this.rejoinAllChannels();
220
221 // Auto joining channels
222 if (this.auto_join && this.auto_join.channel) {
223 panels = this.createAndJoinChannels(this.auto_join.channel + ' ' + (this.auto_join.key || ''));
224
225 // Show the last channel if we have one
226 if (panels)
227 panels[panels.length - 1].view.show();
228
229 delete this.auto_join;
230 }
231 }
232
233
234
235 function onOptions(event) {
236 var that = this;
237
238 $.each(event.options, function (name, value) {
239 switch (name) {
240 case 'CHANTYPES':
241 that.set('channel_prefix', value.join(''));
242 break;
243 case 'NETWORK':
244 that.set('name', value);
245 break;
246 case 'PREFIX':
247 that.set('user_prefixes', value);
248 break;
249 }
250 });
251
252 this.set('cap', event.cap);
253 }
254
255
256
257 function onMotd(event) {
258 this.panels.server.addMsg(this.get('name'), event.msg, 'motd');
259 }
260
261
262
263 function onJoin(event) {
264 var c, members, user;
265 c = this.panels.getByName(event.channel);
266 if (!c) {
267 c = new _kiwi.model.Channel({name: event.channel});
268 this.panels.add(c);
269 }
270
271 members = c.get('members');
272 if (!members) return;
273
274 user = new _kiwi.model.Member({nick: event.nick, ident: event.ident, hostname: event.hostname});
275 members.add(user, {kiwi: event});
276 }
277
278
279
280 function onPart(event) {
281 var channel, members, user,
282 part_options = {};
283
284 part_options.type = 'part';
285 part_options.message = event.message || '';
286 part_options.time = event.time;
287
288 channel = this.panels.getByName(event.channel);
289 if (!channel) return;
290
291 // If this is us, close the panel
292 if (event.nick === this.get('nick')) {
293 channel.close();
294 return;
295 }
296
297 members = channel.get('members');
298 if (!members) return;
299
300 user = members.getByNick(event.nick);
301 if (!user) return;
302
303 members.remove(user, {kiwi: part_options});
304 }
305
306
307
308 function onQuit(event) {
309 var member, members,
310 quit_options = {};
311
312 quit_options.type = 'quit';
313 quit_options.message = event.message || '';
314 quit_options.time = event.time;
315
316 $.each(this.panels.models, function (index, panel) {
317 if (!panel.isChannel()) return;
318
319 member = panel.get('members').getByNick(event.nick);
320 if (member) {
321 panel.get('members').remove(member, {kiwi: quit_options});
322 }
323 });
324 }
325
326
327
328 function onKick(event) {
329 var channel, members, user,
330 part_options = {};
331
332 part_options.type = 'kick';
333 part_options.by = event.nick;
334 part_options.message = event.message || '';
335 part_options.current_user_kicked = (event.kicked == this.get('nick'));
336 part_options.current_user_initiated = (event.nick == this.get('nick'));
337 part_options.time = event.time;
338
339 channel = this.panels.getByName(event.channel);
340 if (!channel) return;
341
342 members = channel.get('members');
343 if (!members) return;
344
345 user = members.getByNick(event.kicked);
346 if (!user) return;
347
348
349 members.remove(user, {kiwi: part_options});
350
351 if (part_options.current_user_kicked) {
352 members.reset([]);
353 }
354 }
355
356
357
358 function onMsg(event) {
359 var panel,
360 is_pm = (event.channel.toLowerCase() == this.get('nick').toLowerCase());
361
362 // An ignored user? don't do anything with it
363 if (_kiwi.gateway.isNickIgnored(event.nick)) {
364 return;
365 }
366
367 if (is_pm) {
368 // If a panel isn't found for this PM, create one
369 panel = this.panels.getByName(event.nick);
370 if (!panel) {
371 panel = new _kiwi.model.Query({name: event.nick});
372 this.panels.add(panel);
373 }
374
375 } else {
376 // If a panel isn't found for this channel, reroute to the
377 // server panel
378 panel = this.panels.getByName(event.channel);
379 if (!panel) {
380 panel = this.panels.server;
381 }
382 }
383
384 panel.addMsg(event.nick, event.msg, 'privmsg', {time: event.time});
385 }
386
387
388
389 function onNick(event) {
390 var member;
391
392 $.each(this.panels.models, function (index, panel) {
393 if (panel.get('name') == event.nick)
394 panel.set('name', event.newnick);
395
396 if (!panel.isChannel()) return;
397
398 member = panel.get('members').getByNick(event.nick);
399 if (member) {
400 member.set('nick', event.newnick);
401 panel.addMsg('', '== ' + _kiwi.global.i18n.translate('client_models_network_nickname_changed').fetch(event.nick, event.newnick) , 'action nick', {time: event.time});
402 }
403 });
404 }
405
406
407
408 function onCtcpRequest(event) {
409 // An ignored user? don't do anything with it
410 if (_kiwi.gateway.isNickIgnored(event.nick)) {
411 return;
412 }
413
414 // Reply to a TIME ctcp
415 if (event.msg.toUpperCase() === 'TIME') {
416 this.gateway.ctcp(false, event.type, event.nick, (new Date()).toString());
417 }
418 }
419
420
421
422 function onCtcpResponse(event) {
423 // An ignored user? don't do anything with it
424 if (_kiwi.gateway.isNickIgnored(event.nick)) {
425 return;
426 }
427
428 this.panels.server.addMsg('[' + event.nick + ']', 'CTCP ' + event.msg, 'ctcp', {time: event.time});
429 }
430
431
432
433 function onNotice(event) {
434 var panel, channel_name;
435
436 // An ignored user? don't do anything with it
437 if (!event.from_server && event.nick && _kiwi.gateway.isNickIgnored(event.nick)) {
438 return;
439 }
440
441 // Find a panel for the destination(channel) or who its from
442 if (!event.from_server) {
443 panel = this.panels.getByName(event.target) || this.panels.getByName(event.nick);
444
445 // Forward ChanServ messages to its associated channel
446 if (event.nick && event.nick.toLowerCase() == 'chanserv' && event.msg.charAt(0) == '[') {
447 channel_name = /\[([^ \]]+)\]/gi.exec(event.msg);
448 if (channel_name && channel_name[1]) {
449 channel_name = channel_name[1];
450
451 panel = this.panels.getByName(channel_name);
452 }
453 }
454
455 if (!panel) {
456 panel = this.panels.server;
457 }
458 } else {
459 panel = this.panels.server;
460 }
461
462 panel.addMsg('[' + (event.nick||'') + ']', event.msg, 'notice', {time: event.time});
463
464 // Show this notice to the active panel if it didn't have a set target
465 if (!event.from_server && panel === this.panels.server && _kiwi.app.panels().active !== this.panels.server)
466 _kiwi.app.panels().active.addMsg('[' + (event.nick||'') + ']', event.msg, 'notice', {time: event.time});
467 }
468
469
470
471 function onAction(event) {
472 var panel,
473 is_pm = (event.channel.toLowerCase() == this.get('nick').toLowerCase());
474
475 // An ignored user? don't do anything with it
476 if (_kiwi.gateway.isNickIgnored(event.nick)) {
477 return;
478 }
479
480 if (is_pm) {
481 // If a panel isn't found for this PM, create one
482 panel = this.panels.getByName(event.nick);
483 if (!panel) {
484 panel = new _kiwi.model.Channel({name: event.nick});
485 this.panels.add(panel);
486 }
487
488 } else {
489 // If a panel isn't found for this channel, reroute to the
490 // server panel
491 panel = this.panels.getByName(event.channel);
492 if (!panel) {
493 panel = this.panels.server;
494 }
495 }
496
497 panel.addMsg('', '* ' + event.nick + ' ' + event.msg, 'action', {time: event.time});
498 }
499
500
501
502 function onTopic(event) {
503 var c;
504 c = this.panels.getByName(event.channel);
505 if (!c) return;
506
507 // Set the channels topic
508 c.set('topic', event.topic);
509
510 // If this is the active channel, update the topic bar too
511 if (c.get('name') === this.panels.active.get('name')) {
512 _kiwi.app.topicbar.setCurrentTopic(event.topic);
513 }
514 }
515
516
517
518 function onTopicSetBy(event) {
519 var c, when;
520 c = this.panels.getByName(event.channel);
521 if (!c) return;
522
523 when = formatDate(new Date(event.when * 1000));
524 c.addMsg('', _kiwi.global.i18n.translate('client_models_network_topic').fetch(event.nick, when), 'topic');
525 }
526
527
528
529 function onUserlist(event) {
530 var channel;
531 channel = this.panels.getByName(event.channel);
532
533 // If we didn't find a channel for this, may aswell leave
534 if (!channel) return;
535
536 channel.temp_userlist = channel.temp_userlist || [];
537 _.each(event.users, function (item) {
538 var user = new _kiwi.model.Member({nick: item.nick, modes: item.modes});
539 channel.temp_userlist.push(user);
540 });
541 }
542
543
544
545 function onUserlistEnd(event) {
546 var channel;
547 channel = this.panels.getByName(event.channel);
548
549 // If we didn't find a channel for this, may aswell leave
550 if (!channel) return;
551
552 // Update the members list with the new list
553 channel.get('members').reset(channel.temp_userlist || []);
554
555 // Clear the temporary userlist
556 delete channel.temp_userlist;
557 }
558
559
560
561 function onMode(event) {
562 var channel, i, prefixes, members, member, find_prefix;
563
564 // Build a nicely formatted string to be displayed to a regular human
565 function friendlyModeString (event_modes, alt_target) {
566 var modes = {}, return_string;
567
568 // If no default given, use the main event info
569 if (!event_modes) {
570 event_modes = event.modes;
571 alt_target = event.target;
572 }
573
574 // Reformat the mode object to make it easier to work with
575 _.each(event_modes, function (mode){
576 var param = mode.param || alt_target || '';
577
578 // Make sure we have some modes for this param
579 if (!modes[param]) {
580 modes[param] = {'+':'', '-':''};
581 }
582
583 modes[param][mode.mode[0]] += mode.mode.substr(1);
584 });
585
586 // Put the string together from each mode
587 return_string = [];
588 _.each(modes, function (modeset, param) {
589 var str = '';
590 if (modeset['+']) str += '+' + modeset['+'];
591 if (modeset['-']) str += '-' + modeset['-'];
592 return_string.push(str + ' ' + param);
593 });
594 return_string = return_string.join(', ');
595
596 return return_string;
597 }
598
599
600 channel = this.panels.getByName(event.target);
601 if (channel) {
602 prefixes = this.get('user_prefixes');
603 find_prefix = function (p) {
604 return event.modes[i].mode[1] === p.mode;
605 };
606 for (i = 0; i < event.modes.length; i++) {
607 if (_.any(prefixes, find_prefix)) {
608 if (!members) {
609 members = channel.get('members');
610 }
611 member = members.getByNick(event.modes[i].param);
612 if (!member) {
613 console.log('MODE command recieved for unknown member %s on channel %s', event.modes[i].param, event.target);
614 return;
615 } else {
616 if (event.modes[i].mode[0] === '+') {
617 member.addMode(event.modes[i].mode[1]);
618 } else if (event.modes[i].mode[0] === '-') {
619 member.removeMode(event.modes[i].mode[1]);
620 }
621 members.sort();
622 //channel.addMsg('', '== ' + event.nick + ' set mode ' + event.modes[i].mode + ' ' + event.modes[i].param, 'action mode');
623 }
624 } else {
625 // Channel mode being set
626 // TODO: Store this somewhere?
627 //channel.addMsg('', 'CHANNEL === ' + event.nick + ' set mode ' + event.modes[i].mode + ' on ' + event.target, 'action mode');
628 }
629 }
630
631 channel.addMsg('', '== ' + _kiwi.global.i18n.translate('client_models_network_mode').fetch(event.nick, friendlyModeString()), 'action mode', {time: event.time});
632 } else {
633 // This is probably a mode being set on us.
634 if (event.target.toLowerCase() === this.get("nick").toLowerCase()) {
635 this.panels.server.addMsg('', '== ' + _kiwi.global.i18n.translate('client_models_network_selfmode').fetch(event.nick, friendlyModeString()), 'action mode');
636 } else {
637 console.log('MODE command recieved for unknown target %s: ', event.target, event);
638 }
639 }
640 }
641
642
643
644 function onWhois(event) {
645 var logon_date, idle_time = '', panel;
646
647 if (event.end)
648 return;
649
650 if (typeof event.idle !== 'undefined') {
651 idle_time = secondsToTime(parseInt(event.idle, 10));
652 idle_time = idle_time.h.toString().lpad(2, "0") + ':' + idle_time.m.toString().lpad(2, "0") + ':' + idle_time.s.toString().lpad(2, "0");
653 }
654
655 panel = _kiwi.app.panels().active;
656 if (event.ident) {
657 panel.addMsg(event.nick, event.nick + ' [' + event.nick + '!' + event.ident + '@' + event.host + '] * ' + event.msg, 'whois');
658 } else if (event.chans) {
659 panel.addMsg(event.nick, _kiwi.global.i18n.translate('client_models_network_channels').fetch(event.chans), 'whois');
660 } else if (event.irc_server) {
661 panel.addMsg(event.nick, _kiwi.global.i18n.translate('client_models_network_server').fetch(event.irc_server, event.server_info), 'whois');
662 } else if (event.msg) {
663 panel.addMsg(event.nick, event.msg, 'whois');
664 } else if (event.logon) {
665 logon_date = new Date();
666 logon_date.setTime(event.logon * 1000);
667 logon_date = formatDate(logon_date);
668
669 panel.addMsg(event.nick, _kiwi.global.i18n.translate('client_models_network_idle_and_signon').fetch(idle_time, logon_date), 'whois');
670 } else if (event.away_reason) {
671 panel.addMsg(event.nick, _kiwi.global.i18n.translate('client_models_network_away').fetch(event.away_reason), 'whois');
672 } else {
673 panel.addMsg(event.nick, _kiwi.global.i18n.translate('client_models_network_idle').fetch(idle_time), 'whois');
674 }
675 }
676
677 function onWhowas(event) {
678 var panel;
679
680 if (event.end)
681 return;
682
683 panel = _kiwi.app.panels().active;
684 if (event.host) {
685 panel.addMsg(event.nick, event.nick + ' [' + event.nick + ((event.ident)? '!' + event.ident : '') + '@' + event.host + '] * ' + event.real_name, 'whois');
686 } else {
687 panel.addMsg(event.nick, _kiwi.global.i18n.translate('client_models_network_nickname_notfound').fetch(), 'whois');
688 }
689 }
690
691
692 function onAway(event) {
693 $.each(this.panels.models, function (index, panel) {
694 if (!panel.isChannel()) return;
695
696 member = panel.get('members').getByNick(event.nick);
697 if (member) {
698 member.set('away', !(!event.trailing));
699 }
700 });
701 }
702
703
704
705 function onListStart(event) {
706 var chanlist = _kiwi.model.Applet.loadOnce('kiwi_chanlist');
707 chanlist.view.show();
708 }
709
710
711
712 function onIrcError(event) {
713 var panel, tmp;
714
715 if (event.channel !== undefined && !(panel = this.panels.getByName(event.channel))) {
716 panel = this.panels.server;
717 }
718
719 switch (event.error) {
720 case 'banned_from_channel':
721 panel.addMsg(' ', '== ' + _kiwi.global.i18n.translate('client_models_network_banned').fetch(event.channel, event.reason), 'status');
722 _kiwi.app.message.text(_kiwi.global.i18n.translate('client_models_network_banned').fetch(event.channel, event.reason));
723 break;
724 case 'bad_channel_key':
725 panel.addMsg(' ', '== ' + _kiwi.global.i18n.translate('client_models_network_channel_badkey').fetch(event.channel), 'status');
726 _kiwi.app.message.text(_kiwi.global.i18n.translate('client_models_network_channel_badkey').fetch(event.channel));
727 break;
728 case 'invite_only_channel':
729 panel.addMsg(' ', '== ' + _kiwi.global.i18n.translate('client_models_network_channel_inviteonly').fetch(event.channel), 'status');
730 _kiwi.app.message.text(_kiwi.global.i18n.translate('client_models_network_channel_inviteonly').fetch(event.channel));
731 break;
732 case 'user_on_channel':
733 panel.addMsg(' ', '== ' + event.nick + ' is already on this channel');
734 break;
735 case 'channel_is_full':
736 panel.addMsg(' ', '== ' + _kiwi.global.i18n.translate('client_models_network_channel_limitreached').fetch(event.channel), 'status');
737 _kiwi.app.message.text(_kiwi.global.i18n.translate('client_models_network_channel_limitreached').fetch(event.channel));
738 break;
739 case 'chanop_privs_needed':
740 panel.addMsg(' ', '== ' + event.reason, 'status');
741 _kiwi.app.message.text(event.reason + ' (' + event.channel + ')');
742 break;
743 case 'no_such_nick':
744 tmp = this.panels.getByName(event.nick);
745 if (tmp) {
746 tmp.addMsg(' ', '== ' + event.nick + ': ' + event.reason, 'status');
747 } else {
748 this.panels.server.addMsg(' ', '== ' + event.nick + ': ' + event.reason, 'status');
749 }
750 break;
751 case 'nickname_in_use':
752 this.panels.server.addMsg(' ', '== ' + _kiwi.global.i18n.translate('client_models_network_nickname_alreadyinuse').fetch( event.nick), 'status');
753 if (this.panels.server !== this.panels.active) {
754 _kiwi.app.message.text(_kiwi.global.i18n.translate('client_models_network_nickname_alreadyinuse').fetch(event.nick));
755 }
756
757 // Only show the nickchange component if the controlbox is open
758 if (_kiwi.app.controlbox.$el.css('display') !== 'none') {
759 (new _kiwi.view.NickChangeBox()).render();
760 }
761
762 break;
763
764 case 'password_mismatch':
765 this.panels.server.addMsg(' ', '== ' + _kiwi.global.i18n.translate('client_models_network_badpassword').fetch(), 'status');
766 break;
767 default:
768 // We don't know what data contains, so don't do anything with it.
769 //_kiwi.front.tabviews.server.addMsg(null, ' ', '== ' + data, 'status');
770 }
771 }
772
773
774 function onUnknownCommand(event) {
775 var display_params = _.clone(event.params);
776
777 // A lot of commands have our nick as the first parameter. This is redundant for us
778 if (display_params[0] && display_params[0] == this.get('nick')) {
779 display_params.shift();
780 }
781
782 if (event.trailing)
783 display_params.push(event.trailing);
784
785 this.panels.server.addMsg('', '[' + event.command + '] ' + display_params.join(', ', ''));
786 }
787 }
788
789 )();