jegolize 0.1.0 → 0.1.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (34) hide show
  1. data/VERSION +1 -1
  2. data/app/controllers/jegol_controller.rb +43 -0
  3. data/app/views/_jegol.html.erb +49 -0
  4. data/app/views/jebol_demo.html.erb +3 -0
  5. data/config/initializers/jegol.rb +3 -0
  6. data/config/jegol.yml +31 -0
  7. data/jegolize.gemspec +31 -1
  8. data/lib/jegolize.rb +31 -0
  9. data/lib/ruby_bosh.rb +161 -0
  10. data/public/javascripts/jegol.js +571 -0
  11. data/public/javascripts/jegol.plugin/jegol.plugin.update.subject_docs/images/doc_background.gif +0 -0
  12. data/public/javascripts/jegol.plugin/jegol.plugin.update.subject_docs/images/shared.css +360 -0
  13. data/public/javascripts/jegol.plugin/jegol.plugin.update.subject_docs/index.html +1 -0
  14. data/public/javascripts/jegol.plugin/jegol.plugin.update.subject_docs/index.html.xml +2 -0
  15. data/public/javascripts/jegol.plugin/jegol.plugin.update.tag_docs/images/doc_background.gif +0 -0
  16. data/public/javascripts/jegol.plugin/jegol.plugin.update.tag_docs/images/shared.css +360 -0
  17. data/public/javascripts/jegol.plugin/jegol.plugin.update.tag_docs/index.html +1 -0
  18. data/public/javascripts/jegol.plugin/jegol.plugin.update.tag_docs/index.html.xml +2 -0
  19. data/public/javascripts/jegol.plugin/update.notify.js +53 -0
  20. data/public/javascripts/jegol.plugin/update.subject.js +17 -0
  21. data/public/javascripts/jegol.plugin/update.tag.js +84 -0
  22. data/public/javascripts/jegol.plugin/viewer.default.js +17 -0
  23. data/public/javascripts/jegol.plugin/viewer.image.js +7 -0
  24. data/public/javascripts/jegol.plugin/viewer.poll.js +6 -0
  25. data/public/javascripts/jegol.plugin/viewer.youtube.js +7 -0
  26. data/public/javascripts/jegol.tag.js +102 -0
  27. data/public/javascripts/jegol_docs/images/doc_background.gif +0 -0
  28. data/public/javascripts/jegol_docs/images/shared.css +360 -0
  29. data/public/javascripts/jegol_docs/index.html +1 -0
  30. data/public/javascripts/jegol_docs/index.html.xml +200 -0
  31. data/public/javascripts/strophe.js +3543 -0
  32. data/public/stylesheets/.gitkeep +0 -0
  33. data/public/stylesheets/jegol.css +181 -0
  34. metadata +32 -2
@@ -0,0 +1,571 @@
1
+ var BOSH_SERVICE = '/http-bind/';//'http://dev.qworky.net:5280/http-bind/';//
2
+
3
+ var JeGol = {
4
+ connection: null,
5
+ room: null,
6
+ nickname: null,
7
+ joined: null,
8
+ participants: null,
9
+ lastmessagefrom: null,
10
+ autoReconnect: true,
11
+ viewerPlugins: {},
12
+ updatePlugins: {},
13
+ /**
14
+ * Registers viewer plugins
15
+ */
16
+ addViewerPlugins: function (pluginName, pluginPrototype)
17
+ {
18
+ JeGol.viewerPlugins[pluginName] = pluginPrototype;
19
+ },
20
+ /**
21
+ * Registers update plugin's
22
+ */
23
+ addUpdatePlugins: function (pluginName, pluginPrototype)
24
+ {
25
+ JeGol.updatePlugins[pluginName] = pluginPrototype;
26
+ },
27
+ /**
28
+ * Initialize components
29
+ */
30
+ init : function()
31
+ {
32
+ // init registered plugin's
33
+ for (var pluginName in JeGol.viewerPlugins) {
34
+ if (JeGol.viewerPlugins.hasOwnProperty(pluginName)) {
35
+ var pluginPrototype = JeGol.viewerPlugins[pluginName];
36
+ var pluginObject = function () {};
37
+ pluginObject.prototype = pluginPrototype;
38
+ this[pluginName] = new pluginObject();
39
+ }
40
+ }
41
+ for (var pluginName in JeGol.updatePlugins) {
42
+ if (JeGol.updatePlugins.hasOwnProperty(pluginName)) {
43
+ var pluginPrototype = JeGol.updatePlugins[pluginName];
44
+ var pluginObject = function () {};
45
+ pluginObject.prototype = pluginPrototype;
46
+ this[pluginName] = new pluginObject();
47
+ }
48
+ }
49
+ },
50
+ /**
51
+ * Send message based on type ('chat' or 'groupchat')
52
+ */
53
+ execCommand : function(body){
54
+ if(!body) return false;
55
+
56
+ // clean out extra white space
57
+ body = $.trim(body);
58
+
59
+ // check if it is command type stanza "/{command} {parameter}
60
+ var commandExists = body.match(/^\/(.+)\s/);
61
+ if(commandExists){
62
+ // separate command from parameter
63
+ var commandName = body.substring(1, body.indexOf(' '));
64
+ var parameter = body.substring(body.indexOf(' ') + 1, body.length);
65
+
66
+ switch (commandName)
67
+ {
68
+ case 'nickname' : // /nickname ... is a special command
69
+ return JeGol.changeNickname(parameter);
70
+ default:
71
+ return JeGol.sendStanza(parameter, commandName);
72
+ }
73
+ }else{
74
+ // default command is /body ....
75
+ return JeGol.sendStanza(body, 'body');
76
+ }
77
+ },
78
+ /**
79
+ * Sends stanza of XMPP-type groupchat.
80
+ * The inner body tag is added for the purpose of graceful degradation
81
+ * for other unaware XMPP clients who look for the <body> tag.
82
+ * If the command passed is already 'body' type, it will not be duplicated.
83
+ * <body ...>
84
+ * <{command}>{message}</{command}>
85
+ * <body>/{command} {message}</body>
86
+ * <id>{psudo-GUID}</id>
87
+ * </body>
88
+ */
89
+ sendStanza : function(message, type){
90
+ Strophe.debug('Sending ' + type + '...');
91
+ if(JeGol._isNullOrEmpty(message) ) return false;
92
+
93
+ var msg = $msg({
94
+ to: JeGol.room,
95
+ type: "groupchat"});
96
+
97
+ msg.c(type).t(message);
98
+ //for graceful degradation on other client
99
+ if(type != 'body') {
100
+ msg.up();
101
+ msg.c('body').t('/' + type + ' ' + message);
102
+ }
103
+ msg.up();
104
+ msg.c('id').t(JeGol.psudoGuid());
105
+ JeGol.connection.send(msg);
106
+
107
+ return false;
108
+ },
109
+ /**
110
+ * Reconnect to chat room requesting full chat history
111
+ */
112
+ getFullHistory : function(){
113
+ // first make sure I am logged out
114
+ JeGol.logout();
115
+ // Clear out UI chat log
116
+ $('#jegol_chatlog').empty();
117
+ // Fetch log from begining of time
118
+ JeGol.joinMUC({"since" : '1970-01-01T00:00:00Z'}); //get full chat history
119
+ },
120
+
121
+ /**
122
+ * Establish connection
123
+ * option 1 - from hidden fields with authenticated SID from server side
124
+ */
125
+ loginSIDOnPage : function(data){
126
+ Strophe.info('Login started...');
127
+ JeGol.room = data.room;
128
+ JeGol.nickname = data.nickname;
129
+ JeGol.connection.attach(data.jid, data.sid, data.rid, JeGol.onConnect);
130
+ Strophe.info('Login complete.');
131
+ },
132
+ /**
133
+ * Establish connection
134
+ * option 2- from server side json store request for SID
135
+ */
136
+ loginSIDFromServer : function(serviceURL){
137
+ Strophe.info('Login started...');
138
+
139
+ Strophe.debug('Getting SID from SID service...');
140
+
141
+ $.getJSON(serviceURL.jsonURL, function(data) {
142
+ Strophe.debug('SID service returned...');
143
+ try {
144
+ JeGol.room = data.room;
145
+ JeGol.nickname = data.nickname;
146
+ JeGol.connection.attach(data.jid, data.sid, data.rid, JeGol.onConnect);
147
+
148
+ Strophe.info('Login complete.');
149
+
150
+ }
151
+ catch(e){
152
+ Strophe.error('Login failed: ' + e.message);
153
+ }
154
+
155
+ });
156
+ },
157
+ /**
158
+ * Establish connection
159
+ * option 3 - from hidden fields with username/password
160
+ */
161
+ loginUsernamePassword : function(data){
162
+ Strophe.info('Login started...');
163
+ JeGol.room = data.room;
164
+ JeGol.nickname = data.nickname;
165
+ JeGol.connection.connect(data.jid, data.password, JeGol.onConnect);
166
+ Strophe.info('Login complete.');
167
+ },
168
+ /**
169
+ * Sends 'unavailable' XMPP-presence stanza and disconnect.
170
+ */
171
+ logout : function(){
172
+ Strophe.info('Logout...');
173
+ JeGol.connection.send($pres({to: JeGol.room + "/" + JeGol.nickname, type: 'unavailable'}).c('x', {xmlns: Strophe.NS.MUC}));
174
+ JeGol.connection.disconnect();
175
+ },
176
+
177
+ /**
178
+ * Re-login
179
+ */
180
+ refreshconnection : function(){
181
+ Strophe.info('Refreshing connection...');
182
+
183
+ if(JeGol.status == Strophe.Status.CONNECTED)
184
+ JeGol.logout();
185
+ else
186
+ $(document).trigger('connect');
187
+
188
+ return false;
189
+ },
190
+ /**
191
+ * Changes nickname by sending XMPP-presence type stanza
192
+ */
193
+ changeNickname : function(newNickname)
194
+ {
195
+ JeGol.setCookie("nickname", newNickname,1);
196
+ var msg = Strophe.xmlElement("presence", [
197
+ ["from", JeGol.connection.jid],
198
+ ["to", JeGol.room + "/" + newNickname]
199
+ ]);
200
+ var x = Strophe.xmlElement("x", [["xmlns", Strophe.NS.MUC]]);
201
+ msg.appendChild(x);
202
+ JeGol.connection.send(msg);
203
+
204
+ return false;
205
+ },
206
+
207
+ /**
208
+ * Listener for connection status change.
209
+ * On Connected: Join chat room
210
+ * On Disconnect: auto reconnect if so configured
211
+ */
212
+ onConnect : function (status){
213
+ var ready = false;
214
+ Strophe.info('Connection status: ' + status);
215
+ switch (status)
216
+ {
217
+ case Strophe.Status.DISCONNECTED:
218
+ Strophe.info('Connection status: disconnected.');
219
+ $('#jegol_connection_status').text('disconnected');
220
+ $(document).trigger('disconnected');
221
+ break;
222
+ case Strophe.Status.CONNECTED:
223
+ case Strophe.Status.ATTACHED:
224
+ Strophe.info('Strophe is connected.');
225
+ $('#jegol_connection_status').text('connected');
226
+ $(document).trigger('connected');
227
+ break;
228
+ }
229
+ },
230
+
231
+ /**
232
+ * Listener for presence. Updates roaster on UI
233
+ */
234
+ onPresence : function(pres){
235
+ var from = $(pres).attr('from');
236
+ var room = Strophe.getBareJidFromJid(from);
237
+
238
+ if(room.toLowerCase() === JeGol.room.toLowerCase()){
239
+ var nickname = Strophe.getResourceFromJid(from);
240
+ var nickname_cleaned = JeGol._stripTimeStampFromNickname(nickname);
241
+
242
+ if($(pres).attr('type') === 'error' && !JeGol.joined){
243
+ JeGol.connection.disconnect();
244
+ } else if (!JeGol.participants[nickname] && $(pres).attr('type') !== 'unavailable'){
245
+ var user_jid = $(pres).find('item').attr('jid');
246
+ JeGol.participants[nickname] = user_jid || true;
247
+ $('#jegol_roster').append('<li id="li_' + nickname_cleaned + '">' + nickname_cleaned + '</li>');
248
+
249
+ if(JeGol.joined){
250
+ $(document).trigger('user_joined', nick);
251
+ }
252
+ } else if (JeGol.participants[nickname] && $(pres).attr('type') === 'unavailable'){
253
+ $('#jegol_roster').find('#li_' + nickname_cleaned).remove();
254
+ JeGol.participants[nickname] = false;
255
+ $(document).trigger('user_left', nickname);
256
+ }
257
+ }
258
+
259
+ if($(pres).attr('type') !== 'error' && !JeGol.joined){
260
+ if($(pres).find("status[code='110']").length > 0){
261
+ //check if server changed our nickname
262
+ if($(pres).find("status[code='210']").length > 0){
263
+ JeGol.nickname = Strophe.getResourceFromJid(from);
264
+ }
265
+
266
+ $(document).trigger('room_joined');
267
+ }
268
+ }
269
+ return true;
270
+ },
271
+ /**
272
+ * Listener for in-bound messages.
273
+ */
274
+ onPublicMessage : function(msg) {
275
+ var from = $(msg).attr('from');
276
+ var room = Strophe.getBareJidFromJid(from);
277
+ var nickname = Strophe.getResourceFromJid(from);
278
+
279
+ if(room.toLowerCase() === JeGol.room.toLowerCase()){
280
+ //message from room or user?
281
+ var notice = !nickname;
282
+
283
+ var body = $(msg).children('body').text();
284
+ var msgID = $(msg).children('id').text();
285
+
286
+ //timestamp = now if message just came in, or get it from historical timestamp
287
+ var delayed = $(msg).children('delay').length > 0 ||
288
+ $(msg).children("x[xmlns='jabber:x:delay']").length > 0;
289
+
290
+ var timestamp = new Date().toTimeString();
291
+ if(delayed){
292
+ timestamp = $(msg).children('delay').attr('stamp');
293
+ }
294
+
295
+
296
+ var subject = $(msg).children('subject').text();
297
+ if(!notice){
298
+ // 1 [Viewer plug-in]- First chance to handle the message goes to the registered VIEWER type plug-ins
299
+ // Check if any of them could handle this message. If so, no other handler should bother
300
+ var commandIsNotHandledYet = true;
301
+ for (var pluginName in JeGol.viewerPlugins) {
302
+ if (JeGol.viewerPlugins.hasOwnProperty(pluginName) && $(msg).children(pluginName).text()) {
303
+ var commandParam = $(msg).children(pluginName).text();
304
+ var log = '<div><div class="jegol_log_timestamp">' + timestamp + '</div>';
305
+ log += JeGol._logNickname(nickname);
306
+ log += '<div id="imBody" class="jegol_log_message">';
307
+ log += JeGol[pluginName].WriteLog(commandParam);
308
+ log += '</div></div>';
309
+ var imDiv = $(log);
310
+ imDiv.hover(TagMenuHelper.tagMenuPopIn, TagMenuHelper.tagMenuPopOut);
311
+ JeGol.addMessage(imDiv);
312
+ // No other shouldn't bother if viewer handles it
313
+ commandIsNotHandledYet = false;
314
+ break;
315
+ }
316
+ }
317
+
318
+ // 2 [Update plug-in] - Second change to handle message goes to the UPDATE plug-ins
319
+ // Check if any of them could handle it. Also let the default handler log the message even if it is handled here
320
+ if(commandIsNotHandledYet)
321
+ {
322
+ for (var pluginName in JeGol.updatePlugins) {
323
+ if (JeGol.updatePlugins.hasOwnProperty(pluginName) && $(msg).children(pluginName).text()) {
324
+ var commandParam = $(msg).children(pluginName).text();
325
+ JeGol[pluginName].DoUpdate(commandParam);
326
+ // Do not change set commandIsNotHandledYet to true because we want the default
327
+ // handler to log it as well.
328
+ break;
329
+ }
330
+ }
331
+ }
332
+
333
+ // 3 [Default] - Use default viewer if no other viewer plugin handled the message
334
+ if (commandIsNotHandledYet){
335
+
336
+ var logmsg = JeGol['default'].WriteLog(timestamp, JeGol._logNickname(nickname), msgID, body);
337
+
338
+ JeGol.addMessage(logmsg);
339
+ }
340
+
341
+ // audio visual indicator
342
+ JeGol['notify'].DoUpdate($(msg));
343
+
344
+ }
345
+ else{
346
+ JeGol.addMessage('<div>***' + body + '</div>');
347
+ }
348
+ }
349
+ return true;
350
+ },
351
+ /**
352
+ * Append to message log to UI
353
+ */
354
+ addMessage : function(msg){
355
+ var chat = $('#jegol_chatlog').get(0);
356
+ var isAtBottom = chat.scrollTop >= chat.scrollHeight - chat.clientHeight;
357
+
358
+ $('#jegol_chatlog').append(msg);
359
+
360
+ if(isAtBottom){
361
+ chat.scrollTop = chat.scrollHeight;
362
+ }
363
+ },
364
+ /**
365
+ * Helper: HTML encode
366
+ */
367
+ _htmlEncode : function (value){
368
+ return $('<div/>').text(value).html();
369
+ },
370
+ /**
371
+ * Helper: HTML dencode
372
+ */
373
+ _htmlDecode : function(value){
374
+ return $('<div/>').html(value).text();
375
+ },
376
+ /**
377
+ * Helper: True if string is null or empty or line break
378
+ */
379
+ _isNullOrEmpty : function(value){
380
+ if(!value
381
+ ||
382
+ value.lengh == 0
383
+ ||
384
+ value == '\n'
385
+ ){
386
+ return true;
387
+ }
388
+ return false;
389
+ },
390
+ /*
391
+ _log : function (level, msg) {
392
+ try{
393
+ var dateTime = new Date();
394
+ msg = '[' + dateTime.getHours() + ':' + dateTime.getMinutes() + ':' + dateTime.getSeconds() + ':' + dateTime.getMilliseconds() +'] ' + msg;
395
+ if(console)
396
+ console.log(msg);
397
+ }catch(e){}
398
+ },
399
+ */
400
+ /**
401
+ * To support multiple browser/client with same nickname, a timestamp is added after a ":~:" pattern.
402
+ * This helper strips the nickname to bare name. e.g. Guest1324:~:1278889735, alem:~:1278889735
403
+ */
404
+ _stripTimeStampFromNickname : function(nickname){
405
+ return nickname.split(':~:')[0];
406
+ },
407
+ /**
408
+ * If the same JID/nickname sends multiple messages, do not display it repeatedly.
409
+ * The first message will have the nickname, subsequent messages will not until the sequence is broken by
410
+ * someone else sending a message in between.
411
+ */
412
+ _logNickname : function(nickname){
413
+ if(JeGol.lastmessagefrom != nickname) //skip nickname if from same nickname
414
+ {
415
+ JeGol.lastmessagefrom = nickname;
416
+ var tempName = JeGol._stripTimeStampFromNickname(nickname);
417
+ return '<div id="fromJID" from="' + tempName + '" class="jegol_log_nickname">' + tempName + ': </div>';
418
+ }
419
+ return '';
420
+ },
421
+ /**
422
+ * Helper: Generate psudo GUID
423
+ */
424
+ psudoGuid : function(){
425
+ return (JeGol.S4()+JeGol.S4()
426
+ +"-"+
427
+ JeGol.S4()+JeGol.S4()
428
+ +"-"+
429
+ JeGol.S4()+JeGol.S4()
430
+ +"-"+
431
+ JeGol.S4()+JeGol.S4());
432
+ },
433
+ S4 : function() {
434
+ return (((1+Math.random())*0x10000)|0).toString(16).substring(1);
435
+ },
436
+ /**
437
+ * Helper: Set value in cookie
438
+ */
439
+ setCookie: function (c_name,value,expiredays)
440
+ {
441
+ var exdate=new Date();
442
+ exdate.setDate(exdate.getDate()+expiredays);
443
+ document.cookie=c_name+ "=" +escape(value)+
444
+ ((expiredays==null) ? "" : ";expires="+exdate.toUTCString());
445
+ },
446
+ /**
447
+ * Helper: Get value from cookie
448
+ */
449
+ getCookie: function (c_name)
450
+ {
451
+ if (document.cookie.length>0)
452
+ {
453
+ c_start=document.cookie.indexOf(c_name + "=");
454
+ if (c_start!=-1)
455
+ {
456
+ c_start=c_start + c_name.length+1;
457
+ c_end=document.cookie.indexOf(";",c_start);
458
+ if (c_end==-1) c_end=document.cookie.length;
459
+ return unescape(document.cookie.substring(c_start,c_end));
460
+ }
461
+ }
462
+ return "";
463
+ }
464
+ }
465
+
466
+ /**
467
+ * Bind event: jegol_init
468
+ */
469
+ $(document).bind('jegol_init', function(e, d){
470
+ JeGol.init();
471
+ $(document).trigger('connect');
472
+ });
473
+
474
+
475
+ /**
476
+ * On Connect event - initialize a strope connection and login from authenticated session service URL
477
+ */
478
+ $(document).bind('connect', function(e, d){
479
+ JeGol.connection = new Strophe.Connection(BOSH_SERVICE);
480
+ $('#jegol_connection_status').text('connecting...');
481
+ JeGol.loginSIDFromServer({jsonURL: $('#jegol_service_url').val()});
482
+ });
483
+
484
+ /**
485
+ * After connection is made, initialize properties
486
+ */
487
+ $(document).bind('connected', function(){
488
+ // Chat room is not yet joined
489
+ JeGol.joined = false;
490
+ // Empty participants list. Haven't gotten them yet.
491
+ JeGol.participants = {};
492
+ // Make my presence known to XMPP server
493
+ JeGol.connection.send($pres().c('priority').t('-1'));
494
+
495
+ // Register listeners
496
+ JeGol.connection.addHandler(JeGol.onPresence, null, 'presence', null, null, null);
497
+ // TODO: Private IM is not supported yet
498
+ //JeGol.connection.addHandler(JeGol.onPublicMessage, null, 'message', 'chat', null, null);
499
+ JeGol.connection.addHandler(JeGol.onPublicMessage, null, 'message', 'groupchat', null, null);
500
+
501
+ // If nickname is known from cookie, pick that up.
502
+ if(JeGol.getCookie("nickname") != ''){
503
+ JeGol.nickname = JeGol.getCookie("nickname");
504
+ }
505
+
506
+ // Make my presence known to chat room
507
+ JeGol.connection.send($pres({to: JeGol.room + '/' + JeGol.nickname}).c('x', {xmlns: Strophe.NS.MUC}));
508
+ });
509
+
510
+ /**
511
+ * On disconnect, clear out UI elements, kill connection and trigger reconnection if specified
512
+ */
513
+ $(document).bind('disconnected', function(){
514
+ JeGol.connection = null;
515
+ $('#jegol_topic').empty();
516
+ $('#jegol_roster').empty();
517
+ $('#jegol_chatlog').empty();
518
+
519
+ if(JeGol.autoReconnect == true)
520
+ {
521
+ $(document).trigger('connect');
522
+ }
523
+ });
524
+
525
+ $(document).bind('room_joined', function(){
526
+ JeGol.joined = true;
527
+ JeGol.addMessage("<div class='jegol_log_i_joined'>*** Room joined.</div>")
528
+ });
529
+
530
+ $(document).bind('user_joined', function(e, nickname){
531
+ JeGol.addMessage("<div class='jegol_log_user_joined'>" + JeGol._stripTimeStampFromNickname(nickname) + " joined.</div>")
532
+ });
533
+
534
+ $(document).bind('user_left', function(e, nickname){
535
+ JeGol.addMessage("<div class='jegol_log_user_left'>" + JeGol._stripTimeStampFromNickname(nickname) + " left.</div>")
536
+ });
537
+
538
+ /**
539
+ * UI action listener - on enter, do post
540
+ */
541
+ $('#jegol_msgArea').live('keypress', function(e) {
542
+ if(e.keyCode == 13) {
543
+ e.preventDefault();
544
+ JeGol.execCommand($(this).val());
545
+ $(this).val('');
546
+ }
547
+ });
548
+
549
+ $('#jegol_postButton').live('click', function(e){
550
+ JeGol.execCommand($('#jegol_msgArea').val());
551
+ $('#jegol_msgArea').val('');
552
+ });
553
+
554
+ /**
555
+ * Start connection on HTML page load
556
+ */
557
+ window.onload = function() {
558
+ $(document).trigger('jegol_init');
559
+ };
560
+
561
+ /**
562
+ * Close connection on HTML page unload
563
+ */
564
+ window.onbeforeunload = function(){
565
+ //var r=confirm("Are you sure you want to leave the chat room?");
566
+ //if (r==true)
567
+ //{
568
+ JeGol.autoReconnect = false;
569
+ JeGol.logout();
570
+ //}
571
+ };