From cd9dd26e14e7904de3575c3255815b2f8b8af2fb Mon Sep 17 00:00:00 2001 From: mbugeia Date: Tue, 25 Nov 2014 20:12:58 +0100 Subject: [PATCH] Update sources to Jappix 1.1.0 --- source/.gitignore | 11 + source/CHANGELOG.md | 51 + source/PROTOCOL.md | 81 +- source/README.md | 4 +- source/VERSION | 2 +- source/app/bundles/desktop.xml | 4 +- .../placeholders/jingle_audio_local.png | Bin 0 -> 1367 bytes .../placeholders/jingle_audio_remote.png | Bin 0 -> 5325 bytes .../placeholders/jingle_video_local.png | Bin 10577 -> 1112 bytes .../placeholders/jingle_video_remote.png | Bin 0 -> 4414 bytes source/app/images/sprites/call.png | Bin 0 -> 4230 bytes source/app/images/sprites/home.png | Bin 29420 -> 27023 bytes source/app/images/sprites/jingle.png | Bin 2060 -> 0 bytes source/app/images/sprites/talk.png | Bin 47001 -> 48232 bytes source/app/images/sprites/welcome.png | Bin 5394 -> 5371 bytes source/app/javascripts/adhoc.js | 4 +- source/app/javascripts/anonymous.js | 36 +- source/app/javascripts/attention.js | 216 + source/app/javascripts/audio.js | 91 +- source/app/javascripts/autocompletion.js | 146 +- source/app/javascripts/avatar.js | 34 +- source/app/javascripts/board.js | 311 +- source/app/javascripts/bubble.js | 10 +- source/app/javascripts/call.js | 972 + source/app/javascripts/caps.js | 754 +- source/app/javascripts/carbons.js | 7 +- source/app/javascripts/chat.js | 528 +- source/app/javascripts/chatstate.js | 42 +- source/app/javascripts/common.js | 153 +- source/app/javascripts/connection.js | 473 +- source/app/javascripts/constants.js | 182 +- source/app/javascripts/correction.js | 509 + source/app/javascripts/dataform.js | 2358 +- source/app/javascripts/datastore.js | 12 +- source/app/javascripts/date.js | 141 +- source/app/javascripts/datejs.js | 16 +- source/app/javascripts/directory.js | 3 +- source/app/javascripts/discovery.js | 3 +- source/app/javascripts/errors.js | 14 +- source/app/javascripts/favorites.js | 300 +- source/app/javascripts/features.js | 23 +- source/app/javascripts/filter.js | 487 +- source/app/javascripts/groupchat.js | 398 +- source/app/javascripts/home.js | 276 +- source/app/javascripts/httpauth.js | 20 +- source/app/javascripts/httpreply.js | 12 +- source/app/javascripts/inbox.js | 149 +- source/app/javascripts/integratebox.js | 3 +- source/app/javascripts/interface.js | 109 +- source/app/javascripts/iq.js | 489 +- source/app/javascripts/jingle.js | 876 +- source/app/javascripts/jquery.timers.js | 276 +- source/app/javascripts/jsjac.jingle.js | 23422 +++++++--- source/app/javascripts/jsjac.js | 104 +- source/app/javascripts/jxhr.js | 232 +- source/app/javascripts/links.js | 20 +- source/app/javascripts/mam.js | 124 +- source/app/javascripts/markers.js | 428 + source/app/javascripts/me.js | 6 +- source/app/javascripts/message.js | 2133 +- source/app/javascripts/microblog.js | 3802 +- source/app/javascripts/mini.js | 64 +- source/app/javascripts/mobile.js | 152 +- source/app/javascripts/mucadmin.js | 11 +- source/app/javascripts/muji.js | 1824 + source/app/javascripts/music.js | 115 +- source/app/javascripts/name.js | 39 +- source/app/javascripts/notification.js | 67 +- source/app/javascripts/oob.js | 64 +- source/app/javascripts/options.js | 111 +- source/app/javascripts/pep.js | 1366 +- source/app/javascripts/presence.js | 972 +- source/app/javascripts/privacy.js | 161 +- source/app/javascripts/receipts.js | 18 +- source/app/javascripts/roster.js | 251 +- source/app/javascripts/rosterx.js | 65 +- source/app/javascripts/search.js | 46 +- source/app/javascripts/smileys.js | 143 +- source/app/javascripts/storage.js | 61 +- source/app/javascripts/system.js | 10 +- source/app/javascripts/talk.js | 13 +- source/app/javascripts/tooltip.js | 137 +- source/app/javascripts/userinfos.js | 60 +- source/app/javascripts/utilities.js | 56 +- source/app/javascripts/vcard.js | 65 +- source/app/javascripts/welcome.js | 37 +- source/app/javascripts/xmpplinks.js | 10 +- source/app/sounds/catch-attention.mp3 | Bin 0 -> 28252 bytes source/app/sounds/catch-attention.oga | Bin 0 -> 27520 bytes source/app/stylesheets/call.css | 254 + source/app/stylesheets/home.css | 19 +- source/app/stylesheets/images.css | 4 +- source/app/stylesheets/jingle.css | 242 +- source/app/stylesheets/main.css | 14 + source/app/stylesheets/mobile.css | 5 +- source/app/stylesheets/muji.css | 700 + source/app/stylesheets/others.css | 3 +- source/app/stylesheets/pageengine.css | 161 +- source/app/stylesheets/roster.css | 28 +- source/app/stylesheets/tools.css | 97 +- .../placeholders/jingle_audio_local.psd | Bin 0 -> 55994 bytes .../placeholders/jingle_audio_remote.psd | Bin 0 -> 375575 bytes .../placeholders/jingle_video_local.psd | Bin 250647 -> 58201 bytes .../placeholders/jingle_video_remote.psd | Bin 0 -> 375103 bytes .../images/sprites/{jingle.psd => call.psd} | Bin 278265 -> 371797 bytes source/dev/images/sprites/talk.psd | Bin 300297 -> 305151 bytes source/i18n/ar/LC_MESSAGES/main.mo | Bin 12566 -> 12574 bytes source/i18n/ar/LC_MESSAGES/main.po | 11 +- source/i18n/bg/LC_MESSAGES/main.mo | Bin 61290 -> 61034 bytes source/i18n/bg/LC_MESSAGES/main.po | 13 +- source/i18n/cs/LC_MESSAGES/main.mo | Bin 55402 -> 57309 bytes source/i18n/cs/LC_MESSAGES/main.po | 72 +- source/i18n/de/LC_MESSAGES/main.mo | Bin 57474 -> 57297 bytes source/i18n/de/LC_MESSAGES/main.po | 15 +- source/i18n/en/LC_MESSAGES/main.pot | 96 + source/i18n/eo/LC_MESSAGES/main.mo | Bin 45689 -> 45524 bytes source/i18n/eo/LC_MESSAGES/main.po | 13 +- source/i18n/es/LC_MESSAGES/main.mo | Bin 54814 -> 54645 bytes source/i18n/es/LC_MESSAGES/main.po | 13 +- source/i18n/et/LC_MESSAGES/main.mo | Bin 54615 -> 54558 bytes source/i18n/et/LC_MESSAGES/main.po | 17 +- source/i18n/fa/LC_MESSAGES/main.mo | Bin 25942 -> 39086 bytes source/i18n/fa/LC_MESSAGES/main.po | 272 +- source/i18n/fr/LC_MESSAGES/main.mo | Bin 58718 -> 59368 bytes source/i18n/fr/LC_MESSAGES/main.po | 46 +- source/i18n/he/LC_MESSAGES/main.mo | Bin 62486 -> 63321 bytes source/i18n/he/LC_MESSAGES/main.po | 100 +- source/i18n/hu/LC_MESSAGES/main.mo | Bin 47621 -> 47429 bytes source/i18n/hu/LC_MESSAGES/main.po | 11 +- source/i18n/id/LC_MESSAGES/main.mo | Bin 47786 -> 47599 bytes source/i18n/id/LC_MESSAGES/main.po | 11 +- source/i18n/it/LC_MESSAGES/main.mo | Bin 54840 -> 54641 bytes source/i18n/it/LC_MESSAGES/main.po | 11 +- source/i18n/ja/LC_MESSAGES/main.mo | Bin 51826 -> 51552 bytes source/i18n/ja/LC_MESSAGES/main.po | 11 +- source/i18n/lb/LC_MESSAGES/main.mo | Bin 56067 -> 58066 bytes source/i18n/lb/LC_MESSAGES/main.po | 283 +- source/i18n/mn/LC_MESSAGES/main.mo | Bin 20935 -> 20877 bytes source/i18n/mn/LC_MESSAGES/main.po | 9 +- source/i18n/nl/LC_MESSAGES/main.mo | Bin 15498 -> 15498 bytes source/i18n/nl/LC_MESSAGES/main.po | 7 +- source/i18n/oc/LC_MESSAGES/main.mo | Bin 13147 -> 13070 bytes source/i18n/oc/LC_MESSAGES/main.po | 9 +- source/i18n/pl/LC_MESSAGES/main.mo | Bin 54700 -> 54503 bytes source/i18n/pl/LC_MESSAGES/main.po | 13 +- source/i18n/pt-br/LC_MESSAGES/main.mo | Bin 47012 -> 46839 bytes source/i18n/pt-br/LC_MESSAGES/main.po | 11 +- source/i18n/pt/LC_MESSAGES/main.mo | Bin 50801 -> 50622 bytes source/i18n/pt/LC_MESSAGES/main.po | 11 +- source/i18n/ru/LC_MESSAGES/main.mo | Bin 72186 -> 72193 bytes source/i18n/ru/LC_MESSAGES/main.po | 11 +- source/i18n/sk/LC_MESSAGES/main.mo | Bin 40960 -> 40779 bytes source/i18n/sk/LC_MESSAGES/main.po | 11 +- source/i18n/sv/LC_MESSAGES/main.mo | Bin 27972 -> 27897 bytes source/i18n/sv/LC_MESSAGES/main.po | 9 +- source/i18n/tr/LC_MESSAGES/main.mo | Bin 48252 -> 48018 bytes source/i18n/tr/LC_MESSAGES/main.po | 11 +- source/i18n/uk/LC_MESSAGES/main.mo | Bin 18746 -> 18746 bytes source/i18n/uk/LC_MESSAGES/main.po | 7 +- source/i18n/uz/LC_MESSAGES/main.mo | Bin 0 -> 29793 bytes source/i18n/uz/LC_MESSAGES/main.po | 2274 + source/i18n/vi/LC_MESSAGES/main.mo | Bin 0 -> 44866 bytes source/i18n/vi/LC_MESSAGES/main.po | 2387 + source/i18n/zh-cn/LC_MESSAGES/main.mo | Bin 47410 -> 47219 bytes source/i18n/zh-cn/LC_MESSAGES/main.po | 11 +- source/i18n/zh-tw/LC_MESSAGES/main.mo | Bin 47426 -> 47235 bytes source/i18n/zh-tw/LC_MESSAGES/main.po | 11 +- source/misc/certs/00673b5b.0 | 25 + source/misc/certs/024dc131.0 | 43 + source/misc/certs/02b73561.0 | 25 + source/misc/certs/039c618a.0 | 25 + source/misc/certs/03f0efa4.0 | 23 + source/misc/certs/052e396b.0 | 25 + source/misc/certs/062cdee6.0 | 21 + source/misc/certs/080911ac.0 | 34 + source/misc/certs/0810ba98.0 | 37 + source/misc/certs/08aef7bb.0 | 28 + source/misc/certs/09789157.0 | 24 + source/misc/certs/0b759015.0 | 22 + source/misc/certs/0c4c9b6c.0 | 41 + source/misc/certs/0d188d89.0 | 22 + source/misc/certs/0d1b923b.0 | 26 + source/misc/certs/10531352.0 | 24 + source/misc/certs/111e6273.0 | 22 + source/misc/certs/1155c94b.0 | 26 + source/misc/certs/116bf586.0 | 17 + source/misc/certs/119afc2e.0 | 29 + source/misc/certs/11a09b38.0 | 30 + source/misc/certs/11f154d6.0 | 24 + source/misc/certs/124bbd54.0 | 25 + source/misc/certs/128805a3.0 | 24 + source/misc/certs/12d55845.0 | 20 + source/misc/certs/157753a5.0 | 25 + source/misc/certs/1636090b.0 | 25 + source/misc/certs/17b51fe6.0 | 22 + source/misc/certs/18856ac4.0 | 21 + source/misc/certs/1dac3003.0 | 24 + source/misc/certs/1dcd6f4c.0 | 32 + source/misc/certs/1df5ec47.0 | 23 + source/misc/certs/1e1eab7c.0 | 23 + source/misc/certs/1e8e7201.0 | 21 + source/misc/certs/1eb37bdf.0 | 42 + source/misc/certs/1ec4d31a.0 | 19 + source/misc/certs/201cada0.0 | 33 + source/misc/certs/20d096ba.0 | 18 + source/misc/certs/219d9499.0 | 24 + source/misc/certs/2251b13a.0 | 22 + source/misc/certs/23f4c490.0 | 24 + source/misc/certs/244b5494.0 | 23 + source/misc/certs/24ad0b63.0 | 14 + source/misc/certs/27af790d.0 | 17 + source/misc/certs/2a4b3efc.0 | 37 + source/misc/certs/2ab3b959.0 | 31 + source/misc/certs/2afc57aa.0 | 27 + source/misc/certs/2b349938.0 | 20 + source/misc/certs/2c543cd1.0 | 20 + source/misc/certs/2cfc4974.0 | 30 + source/misc/certs/2d9dafe4.0 | 31 + source/misc/certs/2e4eed3c.0 | 25 + source/misc/certs/2e5ac55d.0 | 20 + source/misc/certs/2edf7016.0 | 14 + source/misc/certs/2fa87019.0 | 23 + source/misc/certs/2fb1850a.0 | 33 + source/misc/certs/33815e15.0 | 43 + source/misc/certs/343eb6cb.0 | 22 + source/misc/certs/349f2832.0 | 31 + source/misc/certs/3513523f.0 | 22 + source/misc/certs/381ce4dd.0 | 33 + source/misc/certs/399e7759.0 | 22 + source/misc/certs/3a3b02ce.0 | 24 + source/misc/certs/3ad48a91.0 | 21 + source/misc/certs/3b2716e5.0 | 34 + source/misc/certs/3bde41ac.0 | 35 + source/misc/certs/3c58f906.0 | 25 + source/misc/certs/3c860d51.0 | 33 + source/misc/certs/3d441de8.0 | 33 + source/misc/certs/3e45d192.0 | 20 + source/misc/certs/3e7271e8.0 | 26 + source/misc/certs/3ee7e181.0 | 24 + source/misc/certs/40547a79.0 | 25 + source/misc/certs/408e388a.0 | 24 + source/misc/certs/40dc992e.0 | 25 + source/misc/certs/415660c1.0 | 14 + source/misc/certs/418595b9.0 | 30 + source/misc/certs/4304c5e5.0 | 23 + source/misc/certs/442adcac.0 | 19 + source/misc/certs/4597689c.0 | 19 + source/misc/certs/46b2fd3b.0 | 33 + source/misc/certs/480720ec.0 | 21 + source/misc/certs/48a195d8.0 | 34 + source/misc/certs/48bec511.0 | 22 + source/misc/certs/4a6481c9.0 | 22 + source/misc/certs/4bfab552.0 | 23 + source/misc/certs/4d654d1d.0 | 15 + source/misc/certs/4e18c148.0 | 22 + source/misc/certs/4f316efb.0 | 33 + source/misc/certs/4fbd6bfa.0 | 26 + source/misc/certs/5021a0a2.0 | 23 + source/misc/certs/5046c355.0 | 33 + source/misc/certs/524d9b43.0 | 28 + source/misc/certs/5443e9e3.0 | 23 + source/misc/certs/54657681.0 | 31 + source/misc/certs/55a10908.0 | 18 + source/misc/certs/5620c4aa.0 | 27 + source/misc/certs/56657bde.0 | 25 + source/misc/certs/56b8a0b6.0 | 25 + source/misc/certs/56e29e75.0 | 46 + source/misc/certs/57692373.0 | 21 + source/misc/certs/578d5c04.0 | 19 + source/misc/certs/57bbd831.0 | 22 + source/misc/certs/57bcb2da.0 | 33 + source/misc/certs/58a44af1.0 | 24 + source/misc/certs/590d426f.0 | 83 + source/misc/certs/594f1775.0 | 19 + source/misc/certs/5a3f0ff8.0 | 25 + source/misc/certs/5a5372fc.0 | 31 + source/misc/certs/5ad8a5d6.0 | 21 + source/misc/certs/5c44d531.0 | 33 + source/misc/certs/5cf9d536.0 | 34 + source/misc/certs/5e4e69e7.0 | 21 + source/misc/certs/5ed36f99.0 | 83 + source/misc/certs/5f267794.0 | 28 + source/misc/certs/5f47b495.0 | 33 + source/misc/certs/60afe812.0 | 24 + source/misc/certs/635ccfd5.0 | 31 + source/misc/certs/6410666e.0 | 32 + source/misc/certs/653b494a.0 | 21 + source/misc/certs/656b3e35.0 | 25 + source/misc/certs/65b876bd.0 | 30 + source/misc/certs/667c66d4.0 | 34 + source/misc/certs/67495436.0 | 25 + source/misc/certs/67d559d1.0 | 19 + source/misc/certs/69105f4f.0 | 22 + source/misc/certs/6adf0799.0 | 23 + source/misc/certs/6b99d060.0 | 27 + source/misc/certs/6cc3c4c3.0 | 19 + source/misc/certs/6e52cc39.0 | 25 + source/misc/certs/6e8bf996.0 | 19 + source/misc/certs/6f2c1157.0 | 37 + source/misc/certs/6fcc125d.0 | 22 + source/misc/certs/706f604c.0 | 25 + source/misc/certs/72f369af.0 | 20 + source/misc/certs/72fa7371.0 | 19 + source/misc/certs/74c26bd0.0 | 16 + source/misc/certs/755f7420.0 | 19 + source/misc/certs/75680d2e.0 | 25 + source/misc/certs/7651b327.0 | 14 + source/misc/certs/76579174.0 | 25 + source/misc/certs/7672ac4b.0 | 32 + source/misc/certs/76cb8f92.0 | 22 + source/misc/certs/76faf6c0.0 | 38 + source/misc/certs/778e3cb0.0 | 26 + source/misc/certs/790a7190.0 | 24 + source/misc/certs/7999be0d.0 | 20 + source/misc/certs/79ad8b43.0 | 16 + source/misc/certs/7a481e66.0 | 27 + source/misc/certs/7a819ef2.0 | 33 + source/misc/certs/7d0b38bd.0 | 21 + source/misc/certs/7d3cd826.0 | 18 + source/misc/certs/7d453d8f.0 | 24 + source/misc/certs/7d5a75e4.0 | 28 + source/misc/certs/812e17de.0 | 22 + source/misc/certs/8160b96c.0 | 24 + source/misc/certs/81b9768f.0 | 23 + source/misc/certs/82223c44.0 | 31 + source/misc/certs/8317b10c.0 | 39 + source/misc/certs/8470719d.0 | 21 + source/misc/certs/84cba82f.0 | 24 + source/misc/certs/85cde254.0 | 23 + source/misc/certs/861a399d.0 | 24 + source/misc/certs/861e0100.0 | 39 + source/misc/certs/86212b19.0 | 20 + source/misc/certs/876f1e28.0 | 31 + source/misc/certs/87753b0d.0 | 31 + source/misc/certs/882de061.0 | 20 + source/misc/certs/8867006a.0 | 31 + source/misc/certs/88f89ea7.0 | 24 + source/misc/certs/895cad1a.0 | 20 + source/misc/certs/89c02a45.0 | 16 + source/misc/certs/8b59b1ad.0 | 24 + source/misc/certs/8d86cdd1.0 | 20 + source/misc/certs/8e52d3cd.0 | 20 + source/misc/certs/8f7b96c4.0 | 19 + source/misc/certs/91739615.0 | 24 + source/misc/certs/930ac5d2.0 | 33 + source/misc/certs/9339512a.0 | 38 + source/misc/certs/93bc0acc.0 | 20 + source/misc/certs/95aff9e3.0 | 22 + source/misc/certs/9685a493.0 | 20 + source/misc/certs/9772ca32.0 | 21 + source/misc/certs/9818ca0b.0 | 23 + source/misc/certs/988a38cb.0 | 24 + source/misc/certs/98ec67f0.0 | 19 + source/misc/certs/99d0fa06.0 | 83 + source/misc/certs/9a3db647.0 | 37 + source/misc/certs/9af9f759.0 | 18 + source/misc/certs/9b353c9a.0 | 25 + source/misc/certs/9c2e7d30.0 | 19 + source/misc/certs/9c472bf7.0 | 23 + source/misc/certs/9c8dfbd4.0 | 13 + source/misc/certs/9d520b32.0 | 21 + source/misc/certs/9d6523ce.0 | 33 + source/misc/certs/9dbefe7b.0 | 21 + source/misc/certs/9ec3a561.0 | 27 + source/misc/certs/9f533518.0 | 41 + source/misc/certs/9f541fb4.0 | 19 + source/misc/certs/A-Trust-nQual-03.pem | 23 + source/misc/certs/ACEDICOM_Root.pem | 33 + .../misc/certs/AC_Raíz_Certicámara_S.A..pem | 37 + .../certs/Actalis_Authentication_Root_CA.pem | 33 + source/misc/certs/AddTrust_External_Root.pem | 25 + .../AddTrust_Low-Value_Services_Root.pem | 24 + .../certs/AddTrust_Public_Services_Root.pem | 24 + .../AddTrust_Qualified_Certificates_Root.pem | 25 + source/misc/certs/AffirmTrust_Commercial.pem | 20 + source/misc/certs/AffirmTrust_Networking.pem | 20 + source/misc/certs/AffirmTrust_Premium.pem | 31 + source/misc/certs/AffirmTrust_Premium_ECC.pem | 13 + ..._Online_Root_Certification_Authority_1.pem | 22 + ..._Online_Root_Certification_Authority_2.pem | 33 + .../ApplicationCA_-_Japanese_Government.pem | 22 + ...icacion_Firmaprofesional_CIF_A62634068.pem | 35 + .../misc/certs/Baltimore_CyberTrust_Root.pem | 21 + source/misc/certs/Buypass_Class_2_CA_1.pem | 20 + source/misc/certs/Buypass_Class_2_Root_CA.pem | 31 + source/misc/certs/Buypass_Class_3_CA_1.pem | 20 + source/misc/certs/Buypass_Class_3_Root_CA.pem | 31 + source/misc/certs/CA_Disig.pem | 24 + source/misc/certs/CNNIC_ROOT.pem | 20 + .../certs/COMODO_Certification_Authority.pem | 25 + .../COMODO_ECC_Certification_Authority.pem | 16 + .../Camerfirma_Chambers_of_Commerce_Root.pem | 28 + .../Camerfirma_Global_Chambersign_Root.pem | 28 + source/misc/certs/Certigna.pem | 22 + .../certs/Certinomis_-_Autorité_Racine.pem | 32 + .../certs/Certplus_Class_2_Primary_CA.pem | 22 + source/misc/certs/Certum_Root_CA.pem | 19 + .../misc/certs/Certum_Trusted_Network_CA.pem | 22 + .../Chambers_of_Commerce_Root_-_2008.pem | 42 + source/misc/certs/ComSign_CA.pem | 22 + source/misc/certs/ComSign_Secured_CA.pem | 22 + .../misc/certs/Comodo_AAA_Services_root.pem | 25 + .../certs/Comodo_Secure_Services_root.pem | 25 + .../certs/Comodo_Trusted_Services_root.pem | 25 + source/misc/certs/Cybertrust_Global_Root.pem | 22 + source/misc/certs/DST_ACES_CA_X6.pem | 24 + source/misc/certs/DST_Root_CA_X3.pem | 20 + .../misc/certs/Deutsche_Telekom_Root_CA_2.pem | 22 + .../certs/DigiCert_Assured_ID_Root_CA.pem | 22 + source/misc/certs/DigiCert_Global_Root_CA.pem | 22 + .../DigiCert_High_Assurance_EV_Root_CA.pem | 23 + ...igital_Signature_Trust_Co._Global_CA_1.pem | 19 + ...igital_Signature_Trust_Co._Global_CA_3.pem | 19 + ...lektronik_Sertifika_Hizmet_Saglayicisi.pem | 22 + ...lektronik_Sertifika_Hizmet_Sağlayıcısı.pem | 34 + source/misc/certs/EC-ACC.pem | 31 + .../certs/EE_Certification_Centre_Root_CA.pem | 24 + ...rust.net_Premium_2048_Secure_Server_CA.pem | 26 + .../certs/Entrust.net_Secure_Server_CA.pem | 28 + .../Entrust_Root_Certification_Authority.pem | 27 + source/misc/certs/Equifax_Secure_CA.pem | 19 + .../Equifax_Secure_Global_eBusiness_CA.pem | 16 + .../certs/Equifax_Secure_eBusiness_CA_1.pem | 16 + .../certs/Equifax_Secure_eBusiness_CA_2.pem | 19 + .../misc/certs/Firmaprofesional_Root_CA.pem | 26 + .../misc/certs/GTE_CyberTrust_Global_Root.pem | 15 + source/misc/certs/GeoTrust_Global_CA.pem | 20 + source/misc/certs/GeoTrust_Global_CA_2.pem | 21 + ...oTrust_Primary_Certification_Authority.pem | 21 + ...t_Primary_Certification_Authority_-_G2.pem | 17 + ...t_Primary_Certification_Authority_-_G3.pem | 24 + source/misc/certs/GeoTrust_Universal_CA.pem | 31 + source/misc/certs/GeoTrust_Universal_CA_2.pem | 31 + source/misc/certs/GlobalSign_Root_CA.pem | 21 + source/misc/certs/GlobalSign_Root_CA_-_R2.pem | 22 + source/misc/certs/GlobalSign_Root_CA_-_R3.pem | 21 + .../certs/Global_Chambersign_Root_-_2008.pem | 41 + source/misc/certs/Go_Daddy_Class_2_CA.pem | 24 + ..._Daddy_Root_Certificate_Authority_-_G2.pem | 23 + ..._and_Research_Institutions_RootCA_2011.pem | 25 + source/misc/certs/Hongkong_Post_Root_CA_1.pem | 20 + source/misc/certs/IGC_A.pem | 24 + source/misc/certs/Izenpe.com.pem | 34 + source/misc/certs/Juur-SK.pem | 29 + .../misc/certs/Microsec_e-Szigno_Root_CA.pem | 43 + .../certs/Microsec_e-Szigno_Root_CA_2009.pem | 24 + ...tLock_Arany_=Class_Gold=_Főtanúsítvány.pem | 24 + .../certs/NetLock_Business_=Class_B=_Root.pem | 31 + .../certs/NetLock_Express_=Class_C=_Root.pem | 31 + .../certs/NetLock_Notary_=Class_A=_Root.pem | 37 + .../NetLock_Qualified_=Class_QA=_Root.pem | 39 + ...etwork_Solutions_Certificate_Authority.pem | 23 + .../certs/OISTE_WISeKey_Global_Root_GA_CA.pem | 24 + source/misc/certs/QuoVadis_Root_CA.pem | 34 + source/misc/certs/QuoVadis_Root_CA_2.pem | 33 + source/misc/certs/QuoVadis_Root_CA_3.pem | 38 + source/misc/certs/RSA_Root_Certificate_1.pem | 18 + source/misc/certs/RSA_Security_2048_v3.pem | 21 + .../certs/Root_CA_Generalitat_Valenciana.pem | 37 + ...ication_and_Encryption_Root_CA_2005_PN.pem | 26 + source/misc/certs/SecureSign_RootCA11.pem | 21 + source/misc/certs/SecureTrust_CA.pem | 22 + source/misc/certs/Secure_Global_CA.pem | 22 + .../Security_Communication_EV_RootCA1.pem | 21 + .../certs/Security_Communication_RootCA2.pem | 21 + .../certs/Security_Communication_Root_CA.pem | 20 + source/misc/certs/Sonera_Class_1_Root_CA.pem | 19 + source/misc/certs/Sonera_Class_2_Root_CA.pem | 19 + .../certs/Staat_der_Nederlanden_Root_CA.pem | 22 + .../Staat_der_Nederlanden_Root_CA_-_G2.pem | 33 + source/misc/certs/Starfield_Class_2_CA.pem | 24 + ...rfield_Root_Certificate_Authority_-_G2.pem | 23 + ...rvices_Root_Certificate_Authority_-_G2.pem | 24 + .../StartCom_Certification_Authority_G2.pem | 31 + .../Startcom_Certification_Authority.pem | 43 + source/misc/certs/SwissSign_Gold_CA_-_G2.pem | 33 + .../misc/certs/SwissSign_Platinum_CA_-_G2.pem | 33 + .../misc/certs/SwissSign_Silver_CA_-_G2.pem | 33 + source/misc/certs/Swisscom_Root_CA_1.pem | 34 + .../certs/T-TeleSec_GlobalRoot_Class_3.pem | 23 + .../certs/TC_TrustCenter_Class_2_CA_II.pem | 27 + .../certs/TC_TrustCenter_Class_3_CA_II.pem | 27 + .../certs/TC_TrustCenter_Universal_CA_I.pem | 23 + .../certs/TC_TrustCenter_Universal_CA_III.pem | 23 + source/misc/certs/TDC_Internet_Root_CA.pem | 25 + source/misc/certs/TDC_OCES_Root_CA.pem | 30 + ...T_Certificate_Services_Provider_Root_1.pem | 24 + ...T_Certificate_Services_Provider_Root_2.pem | 25 + ...Kök_Sertifika_Hizmet_Sağlayıcısı_-_Sürüm_3.pem | 30 + .../TWCA_Root_Certification_Authority.pem | 21 + source/misc/certs/Taiwan_GRCA.pem | 32 + .../misc/certs/Thawte_Premium_Server_CA.pem | 19 + source/misc/certs/Thawte_Server_CA.pem | 19 + source/misc/certs/Trustis_FPS_Root_CA.pem | 21 + .../misc/certs/UTN_DATACorp_SGC_Root_CA.pem | 26 + .../certs/UTN_USERFirst_Email_Root_CA.pem | 27 + .../certs/UTN_USERFirst_Hardware_Root_CA.pem | 26 + source/misc/certs/ValiCert_Class_1_VA.pem | 18 + source/misc/certs/ValiCert_Class_2_VA.pem | 18 + ...c_Primary_Certification_Authority_-_G4.pem | 21 + ...c_Primary_Certification_Authority_-_G5.pem | 28 + ...Universal_Root_Certification_Authority.pem | 28 + ...Public_Primary_Certification_Authority.pem | 14 + ...c_Primary_Certification_Authority_-_G2.pem | 19 + ...c_Primary_Certification_Authority_-_G3.pem | 24 + ...c_Primary_Certification_Authority_-_G2.pem | 19 + ...c_Primary_Certification_Authority_-_G3.pem | 24 + ...Public_Primary_Certification_Authority.pem | 14 + ...c_Primary_Certification_Authority_-_G2.pem | 19 + ...c_Primary_Certification_Authority_-_G3.pem | 24 + ...c_Primary_Certification_Authority_-_G3.pem | 24 + source/misc/certs/Visa_eCommerce_Root.pem | 22 + ...cure_Public_Root_Certificate_Authority.pem | 28 + source/misc/certs/Wells_Fargo_Root_CA.pem | 23 + source/misc/certs/XRamp_Global_CA_Root.pem | 25 + source/misc/certs/a0bc6fbb.0 | 28 + source/misc/certs/a15b3b6b.0 | 19 + source/misc/certs/a2df7ad7.0 | 24 + source/misc/certs/a3896b44.0 | 20 + source/misc/certs/a5fd78f0.0 | 27 + source/misc/certs/a6a593ba.0 | 19 + source/misc/certs/a7605362.0 | 19 + source/misc/certs/a760e1bd.0 | 22 + source/misc/certs/a7d2cf64.0 | 16 + source/misc/certs/a8dee976.0 | 33 + source/misc/certs/ab5346f4.0 | 21 + source/misc/certs/ad088e1d.0 | 31 + source/misc/certs/add67345.0 | 37 + source/misc/certs/ae8153b9.0 | 43 + source/misc/certs/aeb67534.0 | 31 + source/misc/certs/aee5f10d.0 | 26 + source/misc/certs/b097d71d.0 | 24 + source/misc/certs/b0f3e76e.0 | 21 + source/misc/certs/b1159c4c.0 | 22 + source/misc/certs/b13cc6df.0 | 26 + source/misc/certs/b1b8a7f3.0 | 24 + source/misc/certs/b204d74a.0 | 28 + source/misc/certs/b42ff584.0 | 22 + source/misc/certs/b66938e9.0 | 22 + source/misc/certs/b6c5745d.0 | 24 + source/misc/certs/b727005e.0 | 31 + source/misc/certs/b7a5b843.0 | 21 + source/misc/certs/b7db1890.0 | 21 + source/misc/certs/b7e7231a.0 | 31 + source/misc/certs/b8e83700.0 | 19 + source/misc/certs/ba89ed3b.0 | 25 + source/misc/certs/bad35b78.0 | 24 + source/misc/certs/bb2d49a0.0 | 22 + source/misc/certs/bc3f2570.0 | 23 + source/misc/certs/bcdd5959.0 | 18 + source/misc/certs/bd1910d4.0 | 20 + source/misc/certs/bda4cc84.0 | 22 + source/misc/certs/bdacca6f.0 | 22 + source/misc/certs/bf64f35b.0 | 27 + source/misc/certs/c01cdfa2.0 | 28 + source/misc/certs/c089bbbd.0 | 16 + source/misc/certs/c0ff1f52.0 | 24 + source/misc/certs/c19d42c7.0 | 19 + source/misc/certs/c215bc69.0 | 19 + source/misc/certs/c33a80d4.0 | 19 + source/misc/certs/c3a6a9ad.0 | 23 + source/misc/certs/c47d9980.0 | 42 + source/misc/certs/c51c224c.0 | 21 + source/misc/certs/c527e4ab.0 | 24 + source/misc/certs/c5e082db.0 | 27 + source/misc/certs/c692a373.0 | 15 + source/misc/certs/c7e2a638.0 | 24 + source/misc/certs/c8763593.0 | 37 + source/misc/certs/c8841d13.0 | 23 + source/misc/certs/c99398f3.0 | 21 + source/misc/certs/c9f83a1c.0 | 25 + source/misc/certs/ca-certificates.crt | 4025 ++ source/misc/certs/ca.pem | 25 + source/misc/certs/ca6e4ad9.0 | 33 + source/misc/certs/cacert.org.pem | 83 + source/misc/certs/cb357862.0 | 19 + source/misc/certs/cb59f961.0 | 28 + source/misc/certs/cbeee9e2.0 | 21 + source/misc/certs/cbf06781.0 | 23 + source/misc/certs/cc450945.0 | 34 + source/misc/certs/ccc52f49.0 | 13 + source/misc/certs/cd58d51e.0 | 21 + source/misc/certs/cdaebb72.0 | 22 + source/misc/certs/ce026bf8.0 | 26 + source/misc/certs/certSIGN_ROOT_CA.pem | 20 + source/misc/certs/cf701eeb.0 | 22 + source/misc/certs/cfa1c2ee.0 | 20 + source/misc/certs/d16a5865.0 | 35 + source/misc/certs/d537fba6.0 | 25 + source/misc/certs/d59297b8.0 | 21 + source/misc/certs/d64f06f3.0 | 24 + source/misc/certs/d78a75c7.0 | 24 + source/misc/certs/d7e8dc79.0 | 33 + source/misc/certs/d853d49e.0 | 21 + source/misc/certs/d957f522.0 | 32 + source/misc/certs/d9d12c58.0 | 37 + source/misc/certs/dbc54cab.0 | 31 + source/misc/certs/dc45b0bd.0 | 24 + source/misc/certs/ddc328ff.0 | 19 + source/misc/certs/e113c810.0 | 22 + source/misc/certs/e268a4c5.0 | 24 + source/misc/certs/e2799e36.0 | 24 + source/misc/certs/e48193cf.0 | 20 + source/misc/certs/e536d871.0 | 25 + source/misc/certs/e5662767.0 | 83 + source/misc/certs/e60bf0c0.0 | 34 + source/misc/certs/e775ed2d.0 | 31 + source/misc/certs/e7b8d656.0 | 16 + source/misc/certs/e8651083.0 | 24 + source/misc/certs/e8de2f56.0 | 31 + .../ePKI_Root_Certification_Authority.pem | 33 + source/misc/certs/ea169617.0 | 33 + source/misc/certs/eacdeb40.0 | 22 + source/misc/certs/eb375c3e.0 | 20 + source/misc/certs/ec87c655.0 | 46 + source/misc/certs/ed524cf5.0 | 28 + source/misc/certs/ed62f4e3.0 | 19 + source/misc/certs/ee1365c0.0 | 24 + source/misc/certs/ee64a828.0 | 25 + source/misc/certs/ee7cd6fb.0 | 28 + source/misc/certs/ee90b008.0 | 31 + source/misc/certs/eed8c118.0 | 16 + source/misc/certs/ef2f636c.0 | 16 + source/misc/certs/f060240e.0 | 22 + source/misc/certs/f081611a.0 | 24 + source/misc/certs/f3377b1b.0 | 20 + source/misc/certs/f387163d.0 | 24 + source/misc/certs/f39fc864.0 | 22 + source/misc/certs/f4996e82.0 | 18 + source/misc/certs/f559733c.0 | 26 + source/misc/certs/f58a60fe.0 | 22 + source/misc/certs/f61bff45.0 | 43 + source/misc/certs/f80cc7f6.0 | 34 + source/misc/certs/f90208f7.0 | 28 + source/misc/certs/fac084d7.0 | 22 + source/misc/certs/facacbc6.0 | 28 + source/misc/certs/fb126c6d.0 | 37 + source/misc/certs/fcac10e3.0 | 29 + source/misc/certs/fde84897.0 | 22 + source/misc/certs/ff588423.0 | 22 + source/misc/certs/ff783690.0 | 26 + source/misc/certs/gandi-ca.crt | 37 + source/misc/certs/spi-ca-2003.pem | 24 + source/misc/certs/spi-cacert-2008.pem | 46 + source/misc/certs/thawte_Primary_Root_CA.pem | 25 + .../certs/thawte_Primary_Root_CA_-_G2.pem | 16 + .../certs/thawte_Primary_Root_CA_-_G3.pem | 25 + source/server/desktop.php | 8 +- source/server/form-main.php | 9 + source/server/functions-manager.php | 7 +- source/server/functions.php | 64 +- source/server/generate-chat.php | 2 +- source/server/geolocation.php | 2 +- source/server/get.php | 14 +- source/server/jingle.php | 2 +- source/server/mobile-detect.php | 1606 +- source/server/mobile.php | 24 +- source/server/music-search.php | 2 +- source/server/post-main.php | 8 + source/server/read-main.php | 2 + source/server/vars-main.php | 1 + source/store/access/index.html | 14 - source/store/access/months.xml | 11 - source/store/access/total.xml | 5 - source/store/avatars/index.html | 14 - source/store/backgrounds/index.html | 14 - ...2ce9a8cf7acc9f869ced83c74adbad_plain.cache | 7381 --- ...b5a430d742f790a81fb4044add3483_plain.cache | 37142 ---------------- ...430d742f790a81fb4044add3483_plain_fr.cache | 37142 ---------------- ...8c9d7652050462941d708c6e94844e_plain.cache | 37142 ---------------- ...d7652050462941d708c6e94844e_plain_fr.cache | 37142 ---------------- ...062d1b61bdaeb174d8cea2191a9a46_plain.cache | 94 - ...d1b61bdaeb174d8cea2191a9a46_plain_en.cache | 94 - ...dd26a35a1dacfd1c8ec5ffd8ecaca2_plain.cache | 94 - ...6a35a1dacfd1c8ec5ffd8ecaca2_plain_en.cache | 94 - ...408617e52ef82c399677697d4baca6_plain.cache | 7381 --- source/store/cache/index.html | 14 - source/store/conf/hosts.xml | 14 - source/store/conf/index.html | 14 - source/store/conf/installed.xml | 4 - source/store/conf/main.xml | 32 - source/store/logos/index.html | 14 - source/store/logs/index.html | 14 - source/store/music/index.html | 14 - source/store/send/index.html | 14 - source/store/share/index.html | 14 - source/store/updates/index.html | 14 - source/test/package.json | 6 +- 688 files changed, 56044 insertions(+), 180105 deletions(-) create mode 100644 source/.gitignore create mode 100644 source/app/images/placeholders/jingle_audio_local.png create mode 100644 source/app/images/placeholders/jingle_audio_remote.png create mode 100644 source/app/images/placeholders/jingle_video_remote.png create mode 100644 source/app/images/sprites/call.png delete mode 100644 source/app/images/sprites/jingle.png create mode 100644 source/app/javascripts/attention.js create mode 100644 source/app/javascripts/call.js create mode 100644 source/app/javascripts/correction.js create mode 100644 source/app/javascripts/markers.js create mode 100644 source/app/javascripts/muji.js create mode 100644 source/app/sounds/catch-attention.mp3 create mode 100644 source/app/sounds/catch-attention.oga create mode 100644 source/app/stylesheets/call.css create mode 100644 source/app/stylesheets/muji.css create mode 100644 source/dev/images/placeholders/jingle_audio_local.psd create mode 100644 source/dev/images/placeholders/jingle_audio_remote.psd create mode 100644 source/dev/images/placeholders/jingle_video_remote.psd rename source/dev/images/sprites/{jingle.psd => call.psd} (71%) create mode 100644 source/i18n/uz/LC_MESSAGES/main.mo create mode 100644 source/i18n/uz/LC_MESSAGES/main.po create mode 100644 source/i18n/vi/LC_MESSAGES/main.mo create mode 100644 source/i18n/vi/LC_MESSAGES/main.po create mode 100644 source/misc/certs/00673b5b.0 create mode 100644 source/misc/certs/024dc131.0 create mode 100644 source/misc/certs/02b73561.0 create mode 100644 source/misc/certs/039c618a.0 create mode 100644 source/misc/certs/03f0efa4.0 create mode 100644 source/misc/certs/052e396b.0 create mode 100644 source/misc/certs/062cdee6.0 create mode 100644 source/misc/certs/080911ac.0 create mode 100644 source/misc/certs/0810ba98.0 create mode 100644 source/misc/certs/08aef7bb.0 create mode 100644 source/misc/certs/09789157.0 create mode 100644 source/misc/certs/0b759015.0 create mode 100644 source/misc/certs/0c4c9b6c.0 create mode 100644 source/misc/certs/0d188d89.0 create mode 100644 source/misc/certs/0d1b923b.0 create mode 100644 source/misc/certs/10531352.0 create mode 100644 source/misc/certs/111e6273.0 create mode 100644 source/misc/certs/1155c94b.0 create mode 100644 source/misc/certs/116bf586.0 create mode 100644 source/misc/certs/119afc2e.0 create mode 100644 source/misc/certs/11a09b38.0 create mode 100644 source/misc/certs/11f154d6.0 create mode 100644 source/misc/certs/124bbd54.0 create mode 100644 source/misc/certs/128805a3.0 create mode 100644 source/misc/certs/12d55845.0 create mode 100644 source/misc/certs/157753a5.0 create mode 100644 source/misc/certs/1636090b.0 create mode 100644 source/misc/certs/17b51fe6.0 create mode 100644 source/misc/certs/18856ac4.0 create mode 100644 source/misc/certs/1dac3003.0 create mode 100644 source/misc/certs/1dcd6f4c.0 create mode 100644 source/misc/certs/1df5ec47.0 create mode 100644 source/misc/certs/1e1eab7c.0 create mode 100644 source/misc/certs/1e8e7201.0 create mode 100644 source/misc/certs/1eb37bdf.0 create mode 100644 source/misc/certs/1ec4d31a.0 create mode 100644 source/misc/certs/201cada0.0 create mode 100644 source/misc/certs/20d096ba.0 create mode 100644 source/misc/certs/219d9499.0 create mode 100644 source/misc/certs/2251b13a.0 create mode 100644 source/misc/certs/23f4c490.0 create mode 100644 source/misc/certs/244b5494.0 create mode 100644 source/misc/certs/24ad0b63.0 create mode 100644 source/misc/certs/27af790d.0 create mode 100644 source/misc/certs/2a4b3efc.0 create mode 100644 source/misc/certs/2ab3b959.0 create mode 100644 source/misc/certs/2afc57aa.0 create mode 100644 source/misc/certs/2b349938.0 create mode 100644 source/misc/certs/2c543cd1.0 create mode 100644 source/misc/certs/2cfc4974.0 create mode 100644 source/misc/certs/2d9dafe4.0 create mode 100644 source/misc/certs/2e4eed3c.0 create mode 100644 source/misc/certs/2e5ac55d.0 create mode 100644 source/misc/certs/2edf7016.0 create mode 100644 source/misc/certs/2fa87019.0 create mode 100644 source/misc/certs/2fb1850a.0 create mode 100644 source/misc/certs/33815e15.0 create mode 100644 source/misc/certs/343eb6cb.0 create mode 100644 source/misc/certs/349f2832.0 create mode 100644 source/misc/certs/3513523f.0 create mode 100644 source/misc/certs/381ce4dd.0 create mode 100644 source/misc/certs/399e7759.0 create mode 100644 source/misc/certs/3a3b02ce.0 create mode 100644 source/misc/certs/3ad48a91.0 create mode 100644 source/misc/certs/3b2716e5.0 create mode 100644 source/misc/certs/3bde41ac.0 create mode 100644 source/misc/certs/3c58f906.0 create mode 100644 source/misc/certs/3c860d51.0 create mode 100644 source/misc/certs/3d441de8.0 create mode 100644 source/misc/certs/3e45d192.0 create mode 100644 source/misc/certs/3e7271e8.0 create mode 100644 source/misc/certs/3ee7e181.0 create mode 100644 source/misc/certs/40547a79.0 create mode 100644 source/misc/certs/408e388a.0 create mode 100644 source/misc/certs/40dc992e.0 create mode 100644 source/misc/certs/415660c1.0 create mode 100644 source/misc/certs/418595b9.0 create mode 100644 source/misc/certs/4304c5e5.0 create mode 100644 source/misc/certs/442adcac.0 create mode 100644 source/misc/certs/4597689c.0 create mode 100644 source/misc/certs/46b2fd3b.0 create mode 100644 source/misc/certs/480720ec.0 create mode 100644 source/misc/certs/48a195d8.0 create mode 100644 source/misc/certs/48bec511.0 create mode 100644 source/misc/certs/4a6481c9.0 create mode 100644 source/misc/certs/4bfab552.0 create mode 100644 source/misc/certs/4d654d1d.0 create mode 100644 source/misc/certs/4e18c148.0 create mode 100644 source/misc/certs/4f316efb.0 create mode 100644 source/misc/certs/4fbd6bfa.0 create mode 100644 source/misc/certs/5021a0a2.0 create mode 100644 source/misc/certs/5046c355.0 create mode 100644 source/misc/certs/524d9b43.0 create mode 100644 source/misc/certs/5443e9e3.0 create mode 100644 source/misc/certs/54657681.0 create mode 100644 source/misc/certs/55a10908.0 create mode 100644 source/misc/certs/5620c4aa.0 create mode 100644 source/misc/certs/56657bde.0 create mode 100644 source/misc/certs/56b8a0b6.0 create mode 100644 source/misc/certs/56e29e75.0 create mode 100644 source/misc/certs/57692373.0 create mode 100644 source/misc/certs/578d5c04.0 create mode 100644 source/misc/certs/57bbd831.0 create mode 100644 source/misc/certs/57bcb2da.0 create mode 100644 source/misc/certs/58a44af1.0 create mode 100644 source/misc/certs/590d426f.0 create mode 100644 source/misc/certs/594f1775.0 create mode 100644 source/misc/certs/5a3f0ff8.0 create mode 100644 source/misc/certs/5a5372fc.0 create mode 100644 source/misc/certs/5ad8a5d6.0 create mode 100644 source/misc/certs/5c44d531.0 create mode 100644 source/misc/certs/5cf9d536.0 create mode 100644 source/misc/certs/5e4e69e7.0 create mode 100644 source/misc/certs/5ed36f99.0 create mode 100644 source/misc/certs/5f267794.0 create mode 100644 source/misc/certs/5f47b495.0 create mode 100644 source/misc/certs/60afe812.0 create mode 100644 source/misc/certs/635ccfd5.0 create mode 100644 source/misc/certs/6410666e.0 create mode 100644 source/misc/certs/653b494a.0 create mode 100644 source/misc/certs/656b3e35.0 create mode 100644 source/misc/certs/65b876bd.0 create mode 100644 source/misc/certs/667c66d4.0 create mode 100644 source/misc/certs/67495436.0 create mode 100644 source/misc/certs/67d559d1.0 create mode 100644 source/misc/certs/69105f4f.0 create mode 100644 source/misc/certs/6adf0799.0 create mode 100644 source/misc/certs/6b99d060.0 create mode 100644 source/misc/certs/6cc3c4c3.0 create mode 100644 source/misc/certs/6e52cc39.0 create mode 100644 source/misc/certs/6e8bf996.0 create mode 100644 source/misc/certs/6f2c1157.0 create mode 100644 source/misc/certs/6fcc125d.0 create mode 100644 source/misc/certs/706f604c.0 create mode 100644 source/misc/certs/72f369af.0 create mode 100644 source/misc/certs/72fa7371.0 create mode 100644 source/misc/certs/74c26bd0.0 create mode 100644 source/misc/certs/755f7420.0 create mode 100644 source/misc/certs/75680d2e.0 create mode 100644 source/misc/certs/7651b327.0 create mode 100644 source/misc/certs/76579174.0 create mode 100644 source/misc/certs/7672ac4b.0 create mode 100644 source/misc/certs/76cb8f92.0 create mode 100644 source/misc/certs/76faf6c0.0 create mode 100644 source/misc/certs/778e3cb0.0 create mode 100644 source/misc/certs/790a7190.0 create mode 100644 source/misc/certs/7999be0d.0 create mode 100644 source/misc/certs/79ad8b43.0 create mode 100644 source/misc/certs/7a481e66.0 create mode 100644 source/misc/certs/7a819ef2.0 create mode 100644 source/misc/certs/7d0b38bd.0 create mode 100644 source/misc/certs/7d3cd826.0 create mode 100644 source/misc/certs/7d453d8f.0 create mode 100644 source/misc/certs/7d5a75e4.0 create mode 100644 source/misc/certs/812e17de.0 create mode 100644 source/misc/certs/8160b96c.0 create mode 100644 source/misc/certs/81b9768f.0 create mode 100644 source/misc/certs/82223c44.0 create mode 100644 source/misc/certs/8317b10c.0 create mode 100644 source/misc/certs/8470719d.0 create mode 100644 source/misc/certs/84cba82f.0 create mode 100644 source/misc/certs/85cde254.0 create mode 100644 source/misc/certs/861a399d.0 create mode 100644 source/misc/certs/861e0100.0 create mode 100644 source/misc/certs/86212b19.0 create mode 100644 source/misc/certs/876f1e28.0 create mode 100644 source/misc/certs/87753b0d.0 create mode 100644 source/misc/certs/882de061.0 create mode 100644 source/misc/certs/8867006a.0 create mode 100644 source/misc/certs/88f89ea7.0 create mode 100644 source/misc/certs/895cad1a.0 create mode 100644 source/misc/certs/89c02a45.0 create mode 100644 source/misc/certs/8b59b1ad.0 create mode 100644 source/misc/certs/8d86cdd1.0 create mode 100644 source/misc/certs/8e52d3cd.0 create mode 100644 source/misc/certs/8f7b96c4.0 create mode 100644 source/misc/certs/91739615.0 create mode 100644 source/misc/certs/930ac5d2.0 create mode 100644 source/misc/certs/9339512a.0 create mode 100644 source/misc/certs/93bc0acc.0 create mode 100644 source/misc/certs/95aff9e3.0 create mode 100644 source/misc/certs/9685a493.0 create mode 100644 source/misc/certs/9772ca32.0 create mode 100644 source/misc/certs/9818ca0b.0 create mode 100644 source/misc/certs/988a38cb.0 create mode 100644 source/misc/certs/98ec67f0.0 create mode 100644 source/misc/certs/99d0fa06.0 create mode 100644 source/misc/certs/9a3db647.0 create mode 100644 source/misc/certs/9af9f759.0 create mode 100644 source/misc/certs/9b353c9a.0 create mode 100644 source/misc/certs/9c2e7d30.0 create mode 100644 source/misc/certs/9c472bf7.0 create mode 100644 source/misc/certs/9c8dfbd4.0 create mode 100644 source/misc/certs/9d520b32.0 create mode 100644 source/misc/certs/9d6523ce.0 create mode 100644 source/misc/certs/9dbefe7b.0 create mode 100644 source/misc/certs/9ec3a561.0 create mode 100644 source/misc/certs/9f533518.0 create mode 100644 source/misc/certs/9f541fb4.0 create mode 100644 source/misc/certs/A-Trust-nQual-03.pem create mode 100644 source/misc/certs/ACEDICOM_Root.pem create mode 100644 source/misc/certs/AC_Raíz_Certicámara_S.A..pem create mode 100644 source/misc/certs/Actalis_Authentication_Root_CA.pem create mode 100644 source/misc/certs/AddTrust_External_Root.pem create mode 100644 source/misc/certs/AddTrust_Low-Value_Services_Root.pem create mode 100644 source/misc/certs/AddTrust_Public_Services_Root.pem create mode 100644 source/misc/certs/AddTrust_Qualified_Certificates_Root.pem create mode 100644 source/misc/certs/AffirmTrust_Commercial.pem create mode 100644 source/misc/certs/AffirmTrust_Networking.pem create mode 100644 source/misc/certs/AffirmTrust_Premium.pem create mode 100644 source/misc/certs/AffirmTrust_Premium_ECC.pem create mode 100644 source/misc/certs/America_Online_Root_Certification_Authority_1.pem create mode 100644 source/misc/certs/America_Online_Root_Certification_Authority_2.pem create mode 100644 source/misc/certs/ApplicationCA_-_Japanese_Government.pem create mode 100644 source/misc/certs/Autoridad_de_Certificacion_Firmaprofesional_CIF_A62634068.pem create mode 100644 source/misc/certs/Baltimore_CyberTrust_Root.pem create mode 100644 source/misc/certs/Buypass_Class_2_CA_1.pem create mode 100644 source/misc/certs/Buypass_Class_2_Root_CA.pem create mode 100644 source/misc/certs/Buypass_Class_3_CA_1.pem create mode 100644 source/misc/certs/Buypass_Class_3_Root_CA.pem create mode 100644 source/misc/certs/CA_Disig.pem create mode 100644 source/misc/certs/CNNIC_ROOT.pem create mode 100644 source/misc/certs/COMODO_Certification_Authority.pem create mode 100644 source/misc/certs/COMODO_ECC_Certification_Authority.pem create mode 100644 source/misc/certs/Camerfirma_Chambers_of_Commerce_Root.pem create mode 100644 source/misc/certs/Camerfirma_Global_Chambersign_Root.pem create mode 100644 source/misc/certs/Certigna.pem create mode 100644 source/misc/certs/Certinomis_-_Autorité_Racine.pem create mode 100644 source/misc/certs/Certplus_Class_2_Primary_CA.pem create mode 100644 source/misc/certs/Certum_Root_CA.pem create mode 100644 source/misc/certs/Certum_Trusted_Network_CA.pem create mode 100644 source/misc/certs/Chambers_of_Commerce_Root_-_2008.pem create mode 100644 source/misc/certs/ComSign_CA.pem create mode 100644 source/misc/certs/ComSign_Secured_CA.pem create mode 100644 source/misc/certs/Comodo_AAA_Services_root.pem create mode 100644 source/misc/certs/Comodo_Secure_Services_root.pem create mode 100644 source/misc/certs/Comodo_Trusted_Services_root.pem create mode 100644 source/misc/certs/Cybertrust_Global_Root.pem create mode 100644 source/misc/certs/DST_ACES_CA_X6.pem create mode 100644 source/misc/certs/DST_Root_CA_X3.pem create mode 100644 source/misc/certs/Deutsche_Telekom_Root_CA_2.pem create mode 100644 source/misc/certs/DigiCert_Assured_ID_Root_CA.pem create mode 100644 source/misc/certs/DigiCert_Global_Root_CA.pem create mode 100644 source/misc/certs/DigiCert_High_Assurance_EV_Root_CA.pem create mode 100644 source/misc/certs/Digital_Signature_Trust_Co._Global_CA_1.pem create mode 100644 source/misc/certs/Digital_Signature_Trust_Co._Global_CA_3.pem create mode 100644 source/misc/certs/E-Guven_Kok_Elektronik_Sertifika_Hizmet_Saglayicisi.pem create mode 100644 source/misc/certs/EBG_Elektronik_Sertifika_Hizmet_Sağlayıcısı.pem create mode 100644 source/misc/certs/EC-ACC.pem create mode 100644 source/misc/certs/EE_Certification_Centre_Root_CA.pem create mode 100644 source/misc/certs/Entrust.net_Premium_2048_Secure_Server_CA.pem create mode 100644 source/misc/certs/Entrust.net_Secure_Server_CA.pem create mode 100644 source/misc/certs/Entrust_Root_Certification_Authority.pem create mode 100644 source/misc/certs/Equifax_Secure_CA.pem create mode 100644 source/misc/certs/Equifax_Secure_Global_eBusiness_CA.pem create mode 100644 source/misc/certs/Equifax_Secure_eBusiness_CA_1.pem create mode 100644 source/misc/certs/Equifax_Secure_eBusiness_CA_2.pem create mode 100644 source/misc/certs/Firmaprofesional_Root_CA.pem create mode 100644 source/misc/certs/GTE_CyberTrust_Global_Root.pem create mode 100644 source/misc/certs/GeoTrust_Global_CA.pem create mode 100644 source/misc/certs/GeoTrust_Global_CA_2.pem create mode 100644 source/misc/certs/GeoTrust_Primary_Certification_Authority.pem create mode 100644 source/misc/certs/GeoTrust_Primary_Certification_Authority_-_G2.pem create mode 100644 source/misc/certs/GeoTrust_Primary_Certification_Authority_-_G3.pem create mode 100644 source/misc/certs/GeoTrust_Universal_CA.pem create mode 100644 source/misc/certs/GeoTrust_Universal_CA_2.pem create mode 100644 source/misc/certs/GlobalSign_Root_CA.pem create mode 100644 source/misc/certs/GlobalSign_Root_CA_-_R2.pem create mode 100644 source/misc/certs/GlobalSign_Root_CA_-_R3.pem create mode 100644 source/misc/certs/Global_Chambersign_Root_-_2008.pem create mode 100644 source/misc/certs/Go_Daddy_Class_2_CA.pem create mode 100644 source/misc/certs/Go_Daddy_Root_Certificate_Authority_-_G2.pem create mode 100644 source/misc/certs/Hellenic_Academic_and_Research_Institutions_RootCA_2011.pem create mode 100644 source/misc/certs/Hongkong_Post_Root_CA_1.pem create mode 100644 source/misc/certs/IGC_A.pem create mode 100644 source/misc/certs/Izenpe.com.pem create mode 100644 source/misc/certs/Juur-SK.pem create mode 100644 source/misc/certs/Microsec_e-Szigno_Root_CA.pem create mode 100644 source/misc/certs/Microsec_e-Szigno_Root_CA_2009.pem create mode 100644 source/misc/certs/NetLock_Arany_=Class_Gold=_Főtanúsítvány.pem create mode 100644 source/misc/certs/NetLock_Business_=Class_B=_Root.pem create mode 100644 source/misc/certs/NetLock_Express_=Class_C=_Root.pem create mode 100644 source/misc/certs/NetLock_Notary_=Class_A=_Root.pem create mode 100644 source/misc/certs/NetLock_Qualified_=Class_QA=_Root.pem create mode 100644 source/misc/certs/Network_Solutions_Certificate_Authority.pem create mode 100644 source/misc/certs/OISTE_WISeKey_Global_Root_GA_CA.pem create mode 100644 source/misc/certs/QuoVadis_Root_CA.pem create mode 100644 source/misc/certs/QuoVadis_Root_CA_2.pem create mode 100644 source/misc/certs/QuoVadis_Root_CA_3.pem create mode 100644 source/misc/certs/RSA_Root_Certificate_1.pem create mode 100644 source/misc/certs/RSA_Security_2048_v3.pem create mode 100644 source/misc/certs/Root_CA_Generalitat_Valenciana.pem create mode 100644 source/misc/certs/S-TRUST_Authentication_and_Encryption_Root_CA_2005_PN.pem create mode 100644 source/misc/certs/SecureSign_RootCA11.pem create mode 100644 source/misc/certs/SecureTrust_CA.pem create mode 100644 source/misc/certs/Secure_Global_CA.pem create mode 100644 source/misc/certs/Security_Communication_EV_RootCA1.pem create mode 100644 source/misc/certs/Security_Communication_RootCA2.pem create mode 100644 source/misc/certs/Security_Communication_Root_CA.pem create mode 100644 source/misc/certs/Sonera_Class_1_Root_CA.pem create mode 100644 source/misc/certs/Sonera_Class_2_Root_CA.pem create mode 100644 source/misc/certs/Staat_der_Nederlanden_Root_CA.pem create mode 100644 source/misc/certs/Staat_der_Nederlanden_Root_CA_-_G2.pem create mode 100644 source/misc/certs/Starfield_Class_2_CA.pem create mode 100644 source/misc/certs/Starfield_Root_Certificate_Authority_-_G2.pem create mode 100644 source/misc/certs/Starfield_Services_Root_Certificate_Authority_-_G2.pem create mode 100644 source/misc/certs/StartCom_Certification_Authority_G2.pem create mode 100644 source/misc/certs/Startcom_Certification_Authority.pem create mode 100644 source/misc/certs/SwissSign_Gold_CA_-_G2.pem create mode 100644 source/misc/certs/SwissSign_Platinum_CA_-_G2.pem create mode 100644 source/misc/certs/SwissSign_Silver_CA_-_G2.pem create mode 100644 source/misc/certs/Swisscom_Root_CA_1.pem create mode 100644 source/misc/certs/T-TeleSec_GlobalRoot_Class_3.pem create mode 100644 source/misc/certs/TC_TrustCenter_Class_2_CA_II.pem create mode 100644 source/misc/certs/TC_TrustCenter_Class_3_CA_II.pem create mode 100644 source/misc/certs/TC_TrustCenter_Universal_CA_I.pem create mode 100644 source/misc/certs/TC_TrustCenter_Universal_CA_III.pem create mode 100644 source/misc/certs/TDC_Internet_Root_CA.pem create mode 100644 source/misc/certs/TDC_OCES_Root_CA.pem create mode 100644 source/misc/certs/TURKTRUST_Certificate_Services_Provider_Root_1.pem create mode 100644 source/misc/certs/TURKTRUST_Certificate_Services_Provider_Root_2.pem create mode 100644 source/misc/certs/TÜBİTAK_UEKAE_Kök_Sertifika_Hizmet_Sağlayıcısı_-_Sürüm_3.pem create mode 100644 source/misc/certs/TWCA_Root_Certification_Authority.pem create mode 100644 source/misc/certs/Taiwan_GRCA.pem create mode 100644 source/misc/certs/Thawte_Premium_Server_CA.pem create mode 100644 source/misc/certs/Thawte_Server_CA.pem create mode 100644 source/misc/certs/Trustis_FPS_Root_CA.pem create mode 100644 source/misc/certs/UTN_DATACorp_SGC_Root_CA.pem create mode 100644 source/misc/certs/UTN_USERFirst_Email_Root_CA.pem create mode 100644 source/misc/certs/UTN_USERFirst_Hardware_Root_CA.pem create mode 100644 source/misc/certs/ValiCert_Class_1_VA.pem create mode 100644 source/misc/certs/ValiCert_Class_2_VA.pem create mode 100644 source/misc/certs/VeriSign_Class_3_Public_Primary_Certification_Authority_-_G4.pem create mode 100644 source/misc/certs/VeriSign_Class_3_Public_Primary_Certification_Authority_-_G5.pem create mode 100644 source/misc/certs/VeriSign_Universal_Root_Certification_Authority.pem create mode 100644 source/misc/certs/Verisign_Class_1_Public_Primary_Certification_Authority.pem create mode 100644 source/misc/certs/Verisign_Class_1_Public_Primary_Certification_Authority_-_G2.pem create mode 100644 source/misc/certs/Verisign_Class_1_Public_Primary_Certification_Authority_-_G3.pem create mode 100644 source/misc/certs/Verisign_Class_2_Public_Primary_Certification_Authority_-_G2.pem create mode 100644 source/misc/certs/Verisign_Class_2_Public_Primary_Certification_Authority_-_G3.pem create mode 100644 source/misc/certs/Verisign_Class_3_Public_Primary_Certification_Authority.pem create mode 100644 source/misc/certs/Verisign_Class_3_Public_Primary_Certification_Authority_-_G2.pem create mode 100644 source/misc/certs/Verisign_Class_3_Public_Primary_Certification_Authority_-_G3.pem create mode 100644 source/misc/certs/Verisign_Class_4_Public_Primary_Certification_Authority_-_G3.pem create mode 100644 source/misc/certs/Visa_eCommerce_Root.pem create mode 100644 source/misc/certs/WellsSecure_Public_Root_Certificate_Authority.pem create mode 100644 source/misc/certs/Wells_Fargo_Root_CA.pem create mode 100644 source/misc/certs/XRamp_Global_CA_Root.pem create mode 100644 source/misc/certs/a0bc6fbb.0 create mode 100644 source/misc/certs/a15b3b6b.0 create mode 100644 source/misc/certs/a2df7ad7.0 create mode 100644 source/misc/certs/a3896b44.0 create mode 100644 source/misc/certs/a5fd78f0.0 create mode 100644 source/misc/certs/a6a593ba.0 create mode 100644 source/misc/certs/a7605362.0 create mode 100644 source/misc/certs/a760e1bd.0 create mode 100644 source/misc/certs/a7d2cf64.0 create mode 100644 source/misc/certs/a8dee976.0 create mode 100644 source/misc/certs/ab5346f4.0 create mode 100644 source/misc/certs/ad088e1d.0 create mode 100644 source/misc/certs/add67345.0 create mode 100644 source/misc/certs/ae8153b9.0 create mode 100644 source/misc/certs/aeb67534.0 create mode 100644 source/misc/certs/aee5f10d.0 create mode 100644 source/misc/certs/b097d71d.0 create mode 100644 source/misc/certs/b0f3e76e.0 create mode 100644 source/misc/certs/b1159c4c.0 create mode 100644 source/misc/certs/b13cc6df.0 create mode 100644 source/misc/certs/b1b8a7f3.0 create mode 100644 source/misc/certs/b204d74a.0 create mode 100644 source/misc/certs/b42ff584.0 create mode 100644 source/misc/certs/b66938e9.0 create mode 100644 source/misc/certs/b6c5745d.0 create mode 100644 source/misc/certs/b727005e.0 create mode 100644 source/misc/certs/b7a5b843.0 create mode 100644 source/misc/certs/b7db1890.0 create mode 100644 source/misc/certs/b7e7231a.0 create mode 100644 source/misc/certs/b8e83700.0 create mode 100644 source/misc/certs/ba89ed3b.0 create mode 100644 source/misc/certs/bad35b78.0 create mode 100644 source/misc/certs/bb2d49a0.0 create mode 100644 source/misc/certs/bc3f2570.0 create mode 100644 source/misc/certs/bcdd5959.0 create mode 100644 source/misc/certs/bd1910d4.0 create mode 100644 source/misc/certs/bda4cc84.0 create mode 100644 source/misc/certs/bdacca6f.0 create mode 100644 source/misc/certs/bf64f35b.0 create mode 100644 source/misc/certs/c01cdfa2.0 create mode 100644 source/misc/certs/c089bbbd.0 create mode 100644 source/misc/certs/c0ff1f52.0 create mode 100644 source/misc/certs/c19d42c7.0 create mode 100644 source/misc/certs/c215bc69.0 create mode 100644 source/misc/certs/c33a80d4.0 create mode 100644 source/misc/certs/c3a6a9ad.0 create mode 100644 source/misc/certs/c47d9980.0 create mode 100644 source/misc/certs/c51c224c.0 create mode 100644 source/misc/certs/c527e4ab.0 create mode 100644 source/misc/certs/c5e082db.0 create mode 100644 source/misc/certs/c692a373.0 create mode 100644 source/misc/certs/c7e2a638.0 create mode 100644 source/misc/certs/c8763593.0 create mode 100644 source/misc/certs/c8841d13.0 create mode 100644 source/misc/certs/c99398f3.0 create mode 100644 source/misc/certs/c9f83a1c.0 create mode 100644 source/misc/certs/ca-certificates.crt create mode 100644 source/misc/certs/ca.pem create mode 100644 source/misc/certs/ca6e4ad9.0 create mode 100644 source/misc/certs/cacert.org.pem create mode 100644 source/misc/certs/cb357862.0 create mode 100644 source/misc/certs/cb59f961.0 create mode 100644 source/misc/certs/cbeee9e2.0 create mode 100644 source/misc/certs/cbf06781.0 create mode 100644 source/misc/certs/cc450945.0 create mode 100644 source/misc/certs/ccc52f49.0 create mode 100644 source/misc/certs/cd58d51e.0 create mode 100644 source/misc/certs/cdaebb72.0 create mode 100644 source/misc/certs/ce026bf8.0 create mode 100644 source/misc/certs/certSIGN_ROOT_CA.pem create mode 100644 source/misc/certs/cf701eeb.0 create mode 100644 source/misc/certs/cfa1c2ee.0 create mode 100644 source/misc/certs/d16a5865.0 create mode 100644 source/misc/certs/d537fba6.0 create mode 100644 source/misc/certs/d59297b8.0 create mode 100644 source/misc/certs/d64f06f3.0 create mode 100644 source/misc/certs/d78a75c7.0 create mode 100644 source/misc/certs/d7e8dc79.0 create mode 100644 source/misc/certs/d853d49e.0 create mode 100644 source/misc/certs/d957f522.0 create mode 100644 source/misc/certs/d9d12c58.0 create mode 100644 source/misc/certs/dbc54cab.0 create mode 100644 source/misc/certs/dc45b0bd.0 create mode 100644 source/misc/certs/ddc328ff.0 create mode 100644 source/misc/certs/e113c810.0 create mode 100644 source/misc/certs/e268a4c5.0 create mode 100644 source/misc/certs/e2799e36.0 create mode 100644 source/misc/certs/e48193cf.0 create mode 100644 source/misc/certs/e536d871.0 create mode 100644 source/misc/certs/e5662767.0 create mode 100644 source/misc/certs/e60bf0c0.0 create mode 100644 source/misc/certs/e775ed2d.0 create mode 100644 source/misc/certs/e7b8d656.0 create mode 100644 source/misc/certs/e8651083.0 create mode 100644 source/misc/certs/e8de2f56.0 create mode 100644 source/misc/certs/ePKI_Root_Certification_Authority.pem create mode 100644 source/misc/certs/ea169617.0 create mode 100644 source/misc/certs/eacdeb40.0 create mode 100644 source/misc/certs/eb375c3e.0 create mode 100644 source/misc/certs/ec87c655.0 create mode 100644 source/misc/certs/ed524cf5.0 create mode 100644 source/misc/certs/ed62f4e3.0 create mode 100644 source/misc/certs/ee1365c0.0 create mode 100644 source/misc/certs/ee64a828.0 create mode 100644 source/misc/certs/ee7cd6fb.0 create mode 100644 source/misc/certs/ee90b008.0 create mode 100644 source/misc/certs/eed8c118.0 create mode 100644 source/misc/certs/ef2f636c.0 create mode 100644 source/misc/certs/f060240e.0 create mode 100644 source/misc/certs/f081611a.0 create mode 100644 source/misc/certs/f3377b1b.0 create mode 100644 source/misc/certs/f387163d.0 create mode 100644 source/misc/certs/f39fc864.0 create mode 100644 source/misc/certs/f4996e82.0 create mode 100644 source/misc/certs/f559733c.0 create mode 100644 source/misc/certs/f58a60fe.0 create mode 100644 source/misc/certs/f61bff45.0 create mode 100644 source/misc/certs/f80cc7f6.0 create mode 100644 source/misc/certs/f90208f7.0 create mode 100644 source/misc/certs/fac084d7.0 create mode 100644 source/misc/certs/facacbc6.0 create mode 100644 source/misc/certs/fb126c6d.0 create mode 100644 source/misc/certs/fcac10e3.0 create mode 100644 source/misc/certs/fde84897.0 create mode 100644 source/misc/certs/ff588423.0 create mode 100644 source/misc/certs/ff783690.0 create mode 100644 source/misc/certs/gandi-ca.crt create mode 100644 source/misc/certs/spi-ca-2003.pem create mode 100644 source/misc/certs/spi-cacert-2008.pem create mode 100644 source/misc/certs/thawte_Primary_Root_CA.pem create mode 100644 source/misc/certs/thawte_Primary_Root_CA_-_G2.pem create mode 100644 source/misc/certs/thawte_Primary_Root_CA_-_G3.pem delete mode 100644 source/store/access/index.html delete mode 100644 source/store/access/months.xml delete mode 100644 source/store/access/total.xml delete mode 100644 source/store/avatars/index.html delete mode 100644 source/store/backgrounds/index.html delete mode 100644 source/store/cache/2d2ce9a8cf7acc9f869ced83c74adbad_plain.cache delete mode 100644 source/store/cache/35b5a430d742f790a81fb4044add3483_plain.cache delete mode 100644 source/store/cache/35b5a430d742f790a81fb4044add3483_plain_fr.cache delete mode 100644 source/store/cache/4c8c9d7652050462941d708c6e94844e_plain.cache delete mode 100644 source/store/cache/4c8c9d7652050462941d708c6e94844e_plain_fr.cache delete mode 100644 source/store/cache/50062d1b61bdaeb174d8cea2191a9a46_plain.cache delete mode 100644 source/store/cache/50062d1b61bdaeb174d8cea2191a9a46_plain_en.cache delete mode 100644 source/store/cache/50dd26a35a1dacfd1c8ec5ffd8ecaca2_plain.cache delete mode 100644 source/store/cache/50dd26a35a1dacfd1c8ec5ffd8ecaca2_plain_en.cache delete mode 100644 source/store/cache/80408617e52ef82c399677697d4baca6_plain.cache delete mode 100644 source/store/cache/index.html delete mode 100644 source/store/conf/hosts.xml delete mode 100644 source/store/conf/index.html delete mode 100644 source/store/conf/installed.xml delete mode 100644 source/store/conf/main.xml delete mode 100644 source/store/logos/index.html delete mode 100644 source/store/logs/index.html delete mode 100644 source/store/music/index.html delete mode 100644 source/store/send/index.html delete mode 100644 source/store/share/index.html delete mode 100644 source/store/updates/index.html diff --git a/source/.gitignore b/source/.gitignore new file mode 100644 index 0000000..f093047 --- /dev/null +++ b/source/.gitignore @@ -0,0 +1,11 @@ +mini/ +store/ +tmp/cache/ +tmp/jingle/ +tmp/avatar/ +tmp/archives/ +tmp/send/ +log/ +test/node_modules/ +.DS_Store +*.esproj diff --git a/source/CHANGELOG.md b/source/CHANGELOG.md index ec18f65..bd0d64f 100644 --- a/source/CHANGELOG.md +++ b/source/CHANGELOG.md @@ -4,6 +4,57 @@ Jappix Changelog Here's the log of what has changed over the Jappix releases. +Primo, v1.1.0 (Jun 2014) +------------------------ + + * XEP-0272: Multiparty Jingle (Muji) @valeriansaliou + * Prevent client crash on huge messages @valeriansaliou + * Beautified client code (JavaScript) @valeriansaliou + * Fix unavailable MUC rooms @emamirazavi + + +One, v1.0.7 (May 2014) +---------------------- + + * Fix BackLinks design @valeriansaliou + * Sort Jappix Mobile contacts alphabetically @valeriansaliou + * Display offline contacts in Jappix Mini @valeriansaliou + + +One, v1.0.6 (May 2014) +---------------------- + + * XEP-0308: Last Message Correction @valeriansaliou + * XEP-0333: Chat Markers @valeriansaliou + * XEP-0319: Last User Interaction into Presence @valeriansaliou + * XEP-0224: Attention @valeriansaliou + * XEP-0152: Reachability Addresses @valeriansaliou + * XEP-0334: Message Processing Hints @valeriansaliou + * Fix gateway contacts management @valeriansaliou + * Fix sounds in Jappix Mini @aryo, @valeriansaliou + + +One, v1.0.5 (May 2014) +---------------------- + + * Fix MUC bookmark shortcut button @valeriansaliou + * Fix HTML5 notifications in Firefox 22+ @valeriansaliou + * Fix server commands tool @valeriansaliou + * New translations added (Uzbek), and a few ones updated @nurkamol, @valeriansaliou + + +One, v1.0.4 (May 2014) +---------------------- + + * Fix update tool (on some environments) @valeriansaliou + * Fix MUC room join @maranda, @valeriansaliou + * Fix special chars in JIDs for Jappix Mini @dunger, @valeriansaliou + * Fix WebSocket session termination in JSJaC @sstrigler + * Enhance backend security (verify SSL certificates) @valeriansaliou + * Add assets client cache option @valeriansaliou + * Add SSO support to Jappix Mobile @valeriansaliou + + One, v1.0.3 (March 2014) ------------------------ diff --git a/source/PROTOCOL.md b/source/PROTOCOL.md index bced59f..7ac144f 100644 --- a/source/PROTOCOL.md +++ b/source/PROTOCOL.md @@ -11,56 +11,75 @@ Here are listed the XMPP Protocol Extensions that Jappix supports, as well as th * RFC-6122: Extensible Messaging and Presence Protocol (XMPP): Address Format -# XMPP Extensions +# XMPP Extensions (Standardized) - * XEP-0045: Multi-User Chat *v1.25* + * XEP-0004: Data Forms *v2.9* + * XEP-0012: Last Activity *v2.0* + * XEP-0016: Privacy Lists *v1.6* * XEP-0030: Service Discovery *v2.4* + * XEP-0045: Multi-User Chat *v1.25* + * XEP-0049: Private XML Storage *v1.2* + * XEP-0050: Ad-Hoc Commands *v1.2* + * XEP-0054: vcard-temp *v1.2* + * XEP-0055: Jabber Search *v1.3* * XEP-0060: Publish-Subscribe *v1.13* - * XEP-0124: Bidirectional-streams Over Synchronous HTTP (BOSH) *v1.10* - * XEP-0115: Entity Capabilities *v1.5* + * XEP-0066: Out of Band Data *v1.5* + * XEP-0071: XHTML-IM *v1.5* + * XEP-0072: SOAP Over XMPP *v1.0* + * XEP-0077: In-Band Registration *v2.4* + * XEP-0080: User Location *v1.7* + * XEP-0084: User Avatar *v1.1* + * XEP-0085: Chat State Notifications *v2.1* + * XEP-0092: Software Version *v1.1* * XEP-0107: User Mood *v1.2* * XEP-0108: User Activity *v1.3* + * XEP-0115: Entity Capabilities *v1.5* * XEP-0118: User Tune *v1.2* - * XEP-0080: User Location *v1.7* - * XEP-0172: User Nickname *v1.1* - * XEP-0084: User Avatar *v1.1* - * XEP-0277: Microblogging over XMPP *v0.6* - * XEP-xxxx: Notification Inbox *v0.1* - * Alternate URL: http://xmpp.org/extensions/inbox/notification-inbox.html - * XEP-0203: Delayed Delivery *v2.0* + * XEP-0124: Bidirectional-streams Over Synchronous HTTP (BOSH) *v1.10* * XEP-0144: Roster Item Exchange *v1.0* - * XEP-0072: SOAP Over XMPP *v1.0* - * XEP-0085: Chat State Notifications *v2.1* - * XEP-0071: XHTML-IM *v1.5* - * XEP-0313: Message Archive Management *v0.3* - * Alternate URL: https://demo.frenchtouch.pro/valerian.saliou/xmpp/extensions/xep-0313.html - * XEP-0012: Last Activity *v2.0* - * XEP-0049: Private XML Storage *v1.2* - * XEP-0077: In-Band Registration *v2.4* - * XEP-0055: Jabber Search *v1.3* - * XEP-0050: Ad-Hoc Commands *v1.2* - * XEP-0092: Software Version *v1.1* - * XEP-0004: Data Forms *v2.9* - * XEP-0054: vcard-temp *v1.2* - * XEP-0202: Entity Time *v2.0* - * XEP-0199: XMPP Ping *v2.0* - * XEP-0184: Message Delivery Receipts *v1.2* - * XEP-0016: Privacy Lists *v1.6* - * XEP-0066: Out of Band Data *v1.5* - * XEP-0280: Message Carbons *v0.9* - * XEP-0292: vCard4 Over XMPP *v0.10* + * XEP-0152: Reachability Addresses *v1.0* * XEP-0166: Jingle *v1.1* * XEP-0167: Jingle RTP Sessions *v1.1* + * XEP-0172: User Nickname *v1.1* * XEP-0176: Jingle ICE-UDP Transport Method *v1.0* + * XEP-0177: Jingle Raw UDP Transport Method *v1.1* + * XEP-0184: Message Delivery Receipts *v1.2* + * XEP-0199: XMPP Ping *v2.0* + * XEP-0202: Entity Time *v2.0* + * XEP-0203: Delayed Delivery *v2.0* + * XEP-0224: Attention *v1.0* * XEP-0215: External Service Discovery *v0.5* + * XEP-0249: Direct MUC Invitations *v1.2* * XEP-0262: Use of ZRTP in Jingle RTP Sessions *v1.0* * XEP-0266: Codecs for Jingle Audio *v1.0* * XEP-0269: Jingle Early Media *v0.1* + * XEP-0277: Microblogging over XMPP *v0.6* + * XEP-0278: Jingle Relay Nodes *v0.2* + * XEP-0280: Message Carbons *v0.9* + * XEP-0292: vCard4 Over XMPP *v0.10* * XEP-0293: Jingle RTP Feedback Negotiation *v0.1* * XEP-0294: Jingle RTP Header Extensions Negotiation *v0.1* * XEP-0299: Codecs for Jingle Video *v0.1* + * XEP-0308: Last Message Correction *v1.0* + * XEP-0319: Last User Interaction in Presence *v0.2* * XEP-0320: Use of DTLS-SRTP in Jingle Sessions *v0.2* + * XEP-0333: Chat Markers *v0.2* + * XEP-0334: Message Processing Hints *v0.1* * XEP-0338: Jingle Grouping Framework *v0.1* + * XEP-0339: Source-Specific Media Attributes in Jingle *v0.1* + + +# XMPP Extensions (Updated) + + * XEP-0272: Multiparty Jingle (Muji) *v0.2* + * Alternate URL: https://demo.frenchtouch.pro/valerian.saliou/xmpp/extensions/xep-0272.html + * XEP-0313: Message Archive Management *v0.3* + * Alternate URL: https://demo.frenchtouch.pro/valerian.saliou/xmpp/extensions/xep-0313.html + + +# XMPP Extensions (Proposed) + * XEP-xxxx: Notification Inbox *v0.1* + * Alternate URL: http://xmpp.org/extensions/inbox/notification-inbox.html # Others diff --git a/source/README.md b/source/README.md index 3741682..bd8e047 100644 --- a/source/README.md +++ b/source/README.md @@ -32,9 +32,9 @@ Start translating on https://www.transifex.com/projects/p/jappix/ (new translato Links ----- -* Jappix project website: http://jappix.org/ +* Jappix project website: https://jappix.org/ * Jappix project dev panel: https://github.com/jappix/jappix -* Jappix nodes list: http://jappix.net/ +* Jappix nodes list: https://jappix.net/ * Jappix main service: https://jappix.com/ * Jappix commercial support: https://jappix.pro/ diff --git a/source/VERSION b/source/VERSION index 9ac78f1..b01a2fc 100644 --- a/source/VERSION +++ b/source/VERSION @@ -1 +1 @@ -One [1.0.3] \ No newline at end of file +Primo [1.1.0] \ No newline at end of file diff --git a/source/app/bundles/desktop.xml b/source/app/bundles/desktop.xml index fce814b..4042d38 100644 --- a/source/app/bundles/desktop.xml +++ b/source/app/bundles/desktop.xml @@ -1,5 +1,5 @@ - fonts.css~main.css~images.css~board.css~home.css~others.css~tools.css~roster.css~myinfos.css~pageengine.css~channel.css~pageswitch.css~smileys.css~popup.css~vcard.css~options.css~favorites.css~discovery.css~directory.css~adhoc.css~privacy.css~inbox.css~mucadmin.css~integratebox.css~userinfos.css~search.css~welcome.css~me.css~rosterx.css~jingle.css - origin.js~jxhr.js~datejs.js~jquery.js~jquery.ui.js~jquery.json.js~jquery.form.js~jquery.timers.js~jquery.placeholder.js~jquery.textrange.js~base64.js~jsjac.js~jsjac.jingle.js~system.js~constants.js~datastore.js~browser-detect.js~home.js~talk.js~popup.js~audio.js~board.js~bubble.js~chat.js~groupchat.js~smileys.js~oob.js~avatar.js~mucadmin.js~connection.js~dataform.js~discovery.js~directory.js~adhoc.js~privacy.js~errors.js~name.js~favorites.js~features.js~interface.js~xmpplinks.js~iq.js~message.js~chatstate.js~receipts.js~tooltip.js~filter.js~links.js~inbox.js~microblog.js~music.js~notification.js~httpreply.js~options.js~integratebox.js~pubsub.js~pep.js~presence.js~roster.js~jingle.js~storage.js~console.js~common.js~utilities.js~date.js~caps.js~vcard.js~userinfos.js~search.js~autocompletion.js~welcome.js~me.js~rosterx.js~mam.js~carbons.js + fonts.css~main.css~images.css~board.css~home.css~others.css~tools.css~roster.css~myinfos.css~pageengine.css~channel.css~pageswitch.css~smileys.css~popup.css~vcard.css~options.css~favorites.css~discovery.css~directory.css~adhoc.css~privacy.css~inbox.css~mucadmin.css~integratebox.css~userinfos.css~search.css~welcome.css~me.css~rosterx.css~call.css~jingle.css~muji.css + origin.js~jxhr.js~datejs.js~jquery.js~jquery.ui.js~jquery.json.js~jquery.form.js~jquery.timers.js~jquery.placeholder.js~jquery.textrange.js~jquery.scrollto.js~base64.js~jsjac.js~jsjac.jingle.js~system.js~constants.js~datastore.js~browser-detect.js~home.js~talk.js~popup.js~audio.js~board.js~bubble.js~chat.js~groupchat.js~smileys.js~oob.js~avatar.js~mucadmin.js~connection.js~dataform.js~discovery.js~directory.js~adhoc.js~privacy.js~errors.js~name.js~favorites.js~features.js~interface.js~xmpplinks.js~iq.js~message.js~chatstate.js~receipts.js~tooltip.js~filter.js~links.js~inbox.js~microblog.js~music.js~notification.js~httpreply.js~options.js~integratebox.js~pubsub.js~pep.js~presence.js~roster.js~call.js~jingle.js~muji.js~storage.js~console.js~common.js~utilities.js~date.js~caps.js~vcard.js~userinfos.js~search.js~autocompletion.js~welcome.js~me.js~rosterx.js~mam.js~carbons.js~correction.js~markers.js~attention.js diff --git a/source/app/images/placeholders/jingle_audio_local.png b/source/app/images/placeholders/jingle_audio_local.png new file mode 100644 index 0000000000000000000000000000000000000000..582f5077eb37687f410680b3009ebb3b9059241b GIT binary patch literal 1367 zcmV-d1*rOoP)t@Q)V;yPoS~*!WNwCyll1iU z^YrwmtgupEXpNJZn4Y7itFKsNZSnH+N>^fqjFZO6%=Pv5@9^{a>B*P%g)f++}**%$I8slvbMTqZ+MoR zp`)m*nV+O}eT2Nf!keL{-QVEJ%g&0Cm#(t5>+J1ISYvyGidA50*xKBYmz~km)%*MW zU1)J=a(eIY@Tje^gNl%Qf{Dn>&F=5;V{LV7bbPhByv)wgqNl9z@bQzGpHp9H#K+04 zv9-U##oyrKR$*(Nqo`+bd7q`J%FWNPw7AyT+T`Wt+uYucm7BS|z>Jfbf{BpF%FNf; z+WGnU?CtKNr>pVu@~N(}fr*cZj+NKh+pn{?sjab4TxQkR+2Z5ml$xLJ?(gH|<%o}! zv9-DV{ry*BYz#;1CIA2fWl2OqRCodH+huPYR}_cw|5;x1nqzii<}foeGc$8a*_0VS zbJaAh6MOE=-JKV$G|%rNJi5|38l4dU00000000000000000000{}bulXTwV~h2^7% zR55g}8QW~{9N(tOVC9NIdrx~#mB5B3`_Qy1fNaVZ>Jz+ePuU_}dBM}wEf!#_;HUPI z1t}B-FRLurbHT^tgax}hCwSOrE_I5AhvqcZqDVMmZas>EUJLVD5m0Ag+VTp8;zX{Wn9k4K8)(YVo z%x&bb5Gi9Wtv?FUj+#ULF~z_lQ-51taKeJ5a`J-*7NjOEKNw0`kW=!6)11Y`bH)N? z4hw$0wRaYtUs_VSUr?S7>NYI|&fwr`Yd zFH+_UJx{q(c`@ymz>yZgMT1iNpcMt&>n ztz*~BsjxM##7f_?aObo03yf1&9|hfdPW_&%w+PcYB~^ONqBN&0Qu{XEU_8eUQLsuJ zD5dXPe72l@FrI>vY$j!KWt3s zZYkq2a*jA2zRM&$FUXnSAbM_xQlTz<76Gg8%>k0000000000000000000000915 Ze*)mcUxaLjqo@D?002ovPDHLkV1mC<(J%l2 literal 0 HcmV?d00001 diff --git a/source/app/images/placeholders/jingle_audio_remote.png b/source/app/images/placeholders/jingle_audio_remote.png new file mode 100644 index 0000000000000000000000000000000000000000..187b9db92946b8e54f16a5933820e60c22601ce7 GIT binary patch literal 5325 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 literal 0 HcmV?d00001 diff --git a/source/app/images/placeholders/jingle_video_local.png b/source/app/images/placeholders/jingle_video_local.png index f32d01b113995f4505aa417fc1844080da7f51b0..79ddff5143af397c9c86b3c56507d498bc3db331 100644 GIT binary patch delta 1102 zcmV-U1hMt*yfJs(iPg`Yxh>o$fx#s8S=jiEvhK%y_ z^Y!-jx4XV!Yjk>oiOtW_{r&yr=I35$a)^$V!^X*5W^Z+Vg@5er?(p#Ogo}}?uCv?S z-{0ZlU}|%LiI4R4_1fIs*xKAwUukoDgYWO~-rwOzQL`rwOM3t-QVC)TxMx=d&Nklm+E%H35#l&jsLM!F?fZCpNw&_JfnFIRG4UpNLW4+HO)m z%FAS!=YKFdTa4q!=J0q@KZ>+HV5PG`r0RvqgnrQWfai;AMC(Hs7x+m5r#jw3Ou=pr zs8nu}AHa)+RiaiOI>>Kwg&YCCc}?Utj~?K+OpXAjP7}TIpukTB*i|EXpM$_p1UObB zgMz?M1lSkIpl|;P{4{`$ObP-&3E;HP2vFcB0e|$>i~zd@ehNTGHiaPYQvf!|s37oD z01jjZn99#an2_F252#P?hc`bZp!wm=PYGy#c=J;NnjhZ$lz`@kH$Nqy`Qgn^321(J z^AiBJzn=kK`A7UH(4M^j><1oJ255i=Xc?dZ8lVAM255i=Xn>Xh8sL;Oz*H@!6Er}c z=zj%c0Ggo31eJg$=rKVhpb2_RPzh*)9uw38>IE-U3@}v-rHTQH1oeO>=rKV9OfG1E zWr3gqut-KV5C{qYt7KF_ASeKw$PQ2-C;(i|$}b=gQ~+Me2oTZ;+U~49(g+%0e-{El z3E-p4WYEYq0znBNJH)T&IS2$bfQ!>a?|svr>50y2B8hbWzS_YeevYCz^H)1>cK zM_7WSg3X*9rMkLRCsl5;xs-@|uL^Mc@pe7{;cf`eXtBzE*jyo5_xBgzj(b2}07rq* z(N~pUBrMeX!~Ro}SGw02*qP^ddvGiO0000m$lrPt`>GEB000000000000000KsNZe UoDdIVkN^Mx07*qoM6N<$g8OMF5C8xG literal 10577 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 diff --git a/source/app/images/placeholders/jingle_video_remote.png b/source/app/images/placeholders/jingle_video_remote.png new file mode 100644 index 0000000000000000000000000000000000000000..476138f155583003def98c3ca26de5d38ab4abb9 GIT binary patch literal 4414 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 literal 0 HcmV?d00001 diff --git a/source/app/images/sprites/home.png b/source/app/images/sprites/home.png index afe6304c80dfecf7beb55a411d4b0f79f211ae87..5b5dd00748e40412cb1ebc849d7cbe451c701c4f 100644 GIT binary patch literal 27023 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 B68ZoD literal 29420 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 S48AHNVs*jpe3ixZxc>vrab45^ diff --git a/source/app/images/sprites/jingle.png b/source/app/images/sprites/jingle.png deleted file mode 100644 index d97aba0b7c14c20db71a70e1dde6651b32a1de95..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 2060 zcmV+n2=n)eP)|y9LMp$S~JbI-7Z-YN=?aJB-kJeERZHWy%ILCjOdA?2Qg8)k$lk;YlcWnLj`RQ zM257KG=tWvWkbs}oY7WOcTdS}SjX13bZhkucA$%M&UNl{&UFq2#43EtsxEl5hcC%N$sWO9b~IA`GExn}aJQxLO1ej-#vD!L1;ut%%K~(Pfc`;*OB5 z43+AFnn)MA7X6E*@$Y+HU^1dOVKzVGZ4dbfMO2~h7*#A*km7_QcH!+j-|`#Y&hZ+z zBD$$>=d*?n39gE1h~K>L=HFzi0ztxa!b<#m1&PWD=kTl0gxiC@!-xsD3wSRV3?qzfh(t*1^9tq;^K-U4TLAaSl z+_mu(`TpEibj?HTQIEG?HgKJuTZgXc>O=IRYlMyDdAVY|9S`-|K|6EYoKT57=L6r1 ze;1hP=7js{K-GXa_CDFF)gR*uYS>RB4@dkDrNhGoY&qFF5tW_nm1AeoQ4IIdE>0CsNG`?7m(4=ko>rSLC&#@xPSw{WAouUWW;q`#Cwc&TnCH1iaRLoL|DUp ziuPUH_%1H5;RZoXl+6b z$%6|x96&NF5~cxI&`@oYtll=;#`}ij_+7S+K)-H7xWjRm_Po}Z$l4o%j^jXJV1mU& zA~*uH#^5+o6jN*#jgm2qxMsi!p`k^xE2x6g{-`!VR+UxuHGLP+-qtygeR46OqFq@L zZ{_00)XGJ1MXln*jtIWpvpRYdWTL%OBrX^?>>x0LYp_O=Fox@9ToEuYPQbi40rTPn z%!xaIZ{qMT;Tq;QafJvE4~{4qCr$!=H~}QMY=lin@kVj<+{3390TNumut^63EPV82 z^pE+;y-4U>knzkvnLM^kL?>^L9TnjzF5ZmDCZuhDs*Sn)u4KLm6|lh?J8*|Puo4_c zS9YLX$qJ+53fZ<%lxVl&0;cJDM!~e;n(=5?O{){S(hj4zLS(%aMsVHa+zl&kGx>_m zrrrPB&>mbsrm3BQ@=pedJD-`AV%ul0_gPpE5uCAe2N~rAXM;7ei3x7)of0iL{(lST zq`^!$;Cz{?0Tn0rNI8zjK*xr0Zz*t`J?fpSJ(Z!&1~iQvO68T+=i@%5Q;T<>kGxCkC` z4Yz~GzJklZ>d#&JaM_WsBubs9KKL5hH02rYyuqDMa24PDtQ8S@)0#Hatzp9 zIP$`B;>2L;3XTq*{Dy0m(L-Sd9MC@F3VH2FT;yj) ziyfF~DXRFpgX0>35ghsPqcYs2kHGg`Ve8ZFK#t?%J!*E~M7YI8{`KtjxtW^mAfKSK zqViQ>5*5hJN=0?-ys1#uaM!^e``dQNt2mxj!BOF_XV-A!@nx@5GkV*@3#3_>Jn7k) zQ0U~qEqtqq(Abyj`Yv;ntWM$)zVX*^?d6GHoz=gY66V;{y qlHU%LSrz25W(Q_;QGj8)vi=8rzV#TgQw2@{0000Khdv@#esqQXy zRXtC2J@{3)f}8{bEG{ev2nd4YPf;b{{U-Bk>^~Bf&DUi+Yg!p$Y1$r&Zjmpxc%MrEYUo*()j zi+Vr)oz4m6ZAKRNKDX33k3@tw4$#KTiR|;Pp4VVa)bewQ$GB|Y2{yM+u-thOwq_H7 zTah=iO;(LI`G9%3$62&b@1-RMX`>Z?jZSVES9Me*U*CzR$HXcMuNWJa^eVEHNzaKU z>(NKaO$LevvJR%>x1c7e+b0icBk|am4u*LA9BT3x$JkGUXd$c~@ud%RL5Z(@B7>g& zjNpW6ztnr*pq^=?q|e-|PMVicDN1SYiLdFz^Fg8I<@m+KHon6(llmTn_Q#a{=bn)v z>CY`XIsWflU+HAxi%8znr%Zkp*ZNTc=g>a|21SS<3mIN)=w4EdyL8g`S8j#2tZ^aa zMdsj`r{&`CIdGkAPjh<`5mr-8wj_-Zcn-fH1Z6@JriaNlQ%GKTMXY9eEUv!`bf%tz zr1NT?eLvFZoFmN?emvK==!*sO++k7(l@=65;9jD7N%d}gG6pEqyzq3hz#zY!-;Oj* z(DspieZ|Xz3~2pKE=*37np&0OJ4DJ5J$y|nE~GfQ`TXvQKI6|HKM)3vigc3XK4~B* zE03*joA{o71_f%AxBE7%!=w7E>J<%yNjC8vTxd=whg00>N6I4vXZ6>p+_s`%4uTa$ ztk%(Ye=ESX`6pf5=K!(b2kMo#%9T`g*8`=$hBQ??{<7~HQv2=ip7-i6D8XsT3}~_& zL!8xRlWFN)%_C#pW5-Dity}XEc(8?s-?Ojsu~GAc(8nXWTdQ|JW8a`5`9Npdffcu; zH@Cd^aWSE_3BgO#Z4Qh-Cj!!5F24J#5IR+b@2`o;gWLy^`PLP)>FQW!9m>xetN{H}VUW z89ho*09GHM+aaYLCE~~a8UOoq8*ouccKW4mq!7066jb-Kei*mx#V{Fu0Ihh~jp{~+A(H^}fs>2oht58sfN3xLa z`MmRKyyGBVf}&kjp(fF0M6)vDxo360q7f>DW4rO;rZJ! z53C3vue7)z_X5RC3ES(GI2nG-$Lr4m;nD^VMJXyV|hz9PWLn2!8pr={%;HkE7)u?W`OKZ7A zKPW8@QaIHlQc}lLQinsuWo<2SbzIqcCi9M3`Q=WucJXWbZ635aW<;H}r)%S@PwLvY z*zA;_MPkx-Fx#(AL@LWCQqq&jw^QH>qT3%Tbb4F}#znEQ)s;ceoc(;hJQwNZ`8Cq; z>&e}I?C(w;zhA}t>AQ>T_?Wsj{nU32wtdMu;3YzcJebLaZoiqkZPV57^I-!9ahp5O zv%aLecG@`Pp13E~@$fjl0TZ>w+~6?^L16@#x);7BQ@6LO^o7dP4qV(Lrqpf3i#rg2Lx^ z)sb8vhv1C+a(^TbDJ)|GKZ4w8NsHk|%Z61V-UyX_T_?V7$Ww9(HZu4nQuYviaye6x z^=vL_SAR;Dih?tJPcDXvkXE42V_@ehls{Jp;2h0s3CTuZ*`S-zdb?IrCM|PRJiVNg z&U7}nP^rH_VJ54DpD1K7CxMTvc0b>17_)Wtp~dSzl2{#2sauc+5y4T^gFE#jvx24S z_h2S9&0ZvB7NIKZ>vJr;%nsvf4cy3i_vTHTI|#xpSsY~>18dp|yM%T58S8`O6uNyD z)h1^Q?`{9&&Rb8@huyTgn*OM_>)svx7b+3)zX}0Hh`wALC<~Q>ldyD~@5E0C-5U?& z-Rqw?{D0pWyz6&FZa{bc6JBFWzjE=RqI^R772&bp*3-K2z=uE`1@V!W{aO;N))@`X z@9JAWuo)jcDx?vhqQH0kIf<*k?5g-K0zr*)8Yb_U41{Yat|CD9fEkEkk^d{4eYM)y z+6uILKAif9i1cI2%1%<3mzNiCVuV*#RS9r#aP-(47{qy;%oU-UnwrWJ5D?&VItWVM zJ5*Ly@-wrr&@L=5yLCD22-hbh%)z>PZ*}{inVOiai6IQn&D|0e7Z*dex3^!Oo}Nxc zL_{D}jN5LsKZG_}FRMjhP62P59L&sC3&5fZ%E}EY3JMyI&(}N4CZ?uAEjFw9&7Kcd z(_)dB;b_Q^d-BxS%ZiMyc?RAG`=g0GNCe#XQ`Nd1eH^xH&O%yRYuC&Aev<$mx66+F zAd%i@YuF)E^eB32?97qzar+Y6=5f~hpbQ%iG=2KZnqR^p%H%I7E zkyO~3N{maqj=KXj=u!H423cKp8|@9_DfA5)0)D=1y)P=x4pi8F(!H$#fpvV4d*(5O ze4bucn_cPXQM6Rpv<$lK_uRc@*?eBjoKMra2E4I>j^7~nLfKHLvGofLI^zTDRy#fJ zetnzbQ8L1S+{>ZD-YPM8XS`No{LBcfV}sl~*K9EBq9iv{Wb};=tfPc9MuHvES7rpr z^l~Qe%JxpAvk0W@=8r8hGutaL>iz8HjNg^z>g8fI9*XL5WY+ml_=={1U+*mc<7ceW zeHHS=87(2IusQLhnW~t27Pj6|(Zm@KOi8k0ww;;J2$eZHK)`WJiP=*j8*@$m1mrHb zEV<`42tz--0Dd-UZ>JdXT|v(FlgN9pZy46O*go1ir<9ZK_hCCPZ;0=e+7R)Hp#SR5_gfYEyeb@5a#mf&)boQBw+^2R#Ku_a|h{>Zj zzW^WK5S5ih8tjFF>0#CCoZj1;EFQ%OJPloYh5(dB&CIQCt4_uaB|J7JDR$k9n4QFNqq_Fn+30%;lSpGTWA~ z?zt@Nk8eY*X!cb8Z8#Osd_BgkC?4OuY>tewhNOK20A7dmOdQmpF)`CZI`;`duN%4w zu3J#5(;JKkT%pm+CEI5WN?rMojlJI3c7w;g>{56fq(64Fo*@Gpu_#E&S4??`a%iPg z{hXz&3N1kCIbJ&b0zM@^8l1N-aw=cczRbpR4DlE^Q+M}>{$D0WVD>1-?~^3`wj{?l zisHErX^KHz&=i@OnYr@_?sZ55rben7{M*xXtD_C9MM>Rb${y0&dU((#c^=cD= zMjjRe1B3Tz-RUGNN|ea^T8LY9f8g=ruTT@ZP%_tQi~|0Q|E9+gfoXZ)xAGdQKfpzS z06vhp;;-j*(S)S$eIsTXzFa81eN8+$_&?!6f7UrCM}SNj;A*e*%K8+3qI?yl4lv?K z*|LHME3^R^2F4{AJY&B8p_&3gqZhx)Tm+Tv;GyO#<%AKZ7H`FN17){VOEY*zlXrtZ z#!CHm0`MT5iRyrONOP+F*&bMz>bva^#k%TSS&ve+SS!~7dPl}`9Bg4Y5MJYfhBs4a zUBI7Z!7S{NHFbSId)hze$9r3J`}CK8M=0Yz|J$=3Qf%*cl5}S0uXMzVo)VEg7t>Py zVS6?xP$lu-m_s)OjKS@_Nq*ahq=^RFB|B(`Z?Hf>|V1vDYUIJ)rH+YTWg9 z;Jvs0TcG~1@aUI2{=VRakAchEeyV=9eB#nn$Yz`LpG;Q9+3TPwF>38vn8Z58UA@ia+9{Vlf%y*K;%fHbn za7R0o52VnnxxXT~9?Yk5FX{<<6(v?jm0L+3aP5u2$5K2W`ytBAlSPX3e)dQMoRYUo zrMm`$Sh4;mEc>14Rd&9MFq_uj*VotI&&??vBqX5vRxrWD{c>_*;pXOsu(q-ic64Ow z(IkMN$__A$!^Oj!-Q0}H&nE?3Vk29t!6OIF&&&{m(9qHC`$G*FP5B=+HZ^^VyS=@2 zN5cckN=Rjr)GRgBR8&8wCnr^>rlt=6H_3p+JNt^Dkcv8EdJUsRdqC*;JmaV~oF#lskb|hMrI?7B0ZvJPb4Ql}Qh^szLAb`cd*JL^ZVT#2BsCH(7 zHX!2>Cw_rsfhr@z;Rk;~WPv6UoI0?6!Z|>llR}dj33i6x5Kne^@GI=M;J%wBm+**E z0~p`ncB$3>hiKI$Rx#iB$?zna<7~1AFr{LAsLU8^5~KMp*2#|06|K;dlkfl;0L|2d z6h$n|9E?k39SgE64Tj-r!{rE+sfElu3>xb6msQm!SqsZ6ae4h7gGpOmsviB=k>^Gm z=^Sqi*Q!!trWyl>xq4VFZf>tX3!96PuPJs5@lCFeu)|tw9mtr8>%SiK5QA9NSeZiE zXb-u-9jr3i9eV2ER1uIW^YhRS9c{*quT zs~M8R6BI7@8Q#aYk}gQX}|l{yMw_SHAv(8H5fh=ZHXmZF?rbqiy@ zg#n)HGFo=xLWjM~?grB!1wvI* zFMGWDYaG|WeNm59)+WnG5TNMjLI3(j;yxnignNw(MbVOvz;$4Ie0d>Z;0&cAa}`1H z)6f0hELum(V%`z8`9rnpASmCvcXiw=|I%!tQX2BI0G> zYvUvDJq5dl2_R8h4e6tTLt74K6Kz`1@wzG`#V0DFsldxsTd1mUXBVcx`_R$bT6EfG>u?9+{~pD9PCBinI2qUsjX3q3dIyodo_>t zp#S`V{)?UKJ;4g~t{zjBPH&SVj;~WrZJ!cOZ98Z&W_L-77S1VdH6J0&wOVpigyg3j z@6~wnOJ<4q5crR)Yeghp9a-CdVl41T%A!%?`;{GR+|18t`$Ezub0E7YLqYa-I0*Ri zklL~rFzTRWLxxzuW`vw-D~*dmhGs1A==v(rcRgLq>Kcc{%q5nFtwwivT5p$iKvBz8 z%p}l7FIDy*Q+0;W@RX&%;+46Hf`{!!YspuGu~B+Cr(~n&Hk#UtX)l?v4%LV|y?eW# zR*D)=*aQ~(ABsXIZNw>|ArQk3g?BaC9hdXQ#^eWCOmBS$v6|G705s@;EMT(`Anc&c zVzUul1am0-lBH2(^` zK@;d6wcwSIY$2*rlPG%O+11Z3?K7&yIZY#3&{y~jW%j(PeTw!>r)?vNCGClyE{i8;u!z*Z-vJ24*tH6!W= z&skOKK8^kby3xVlWQLR6G(J8yt}xggLqwmV9fTpp!K16V`23^&R{RjU`*c!Qhh{{qK&LJ zy$M?8?%_=o=`rQp2o}b@IFSD7qiuhyfJ6JKe=^ zsCRJ&OTx_2b4Y;+5x@in;LBIw|Mk$;+S>Y=eJ2cTy2JsP7XB|9-E{&M=a=~Jx)#7; zbr~?Q(c@TY;mstF%5qfo9-Ajau=`TwnNbQ;TsFTT!yqOckf)d^5y^6d{ZL)uR3qRN=6%`DoRhE`WN)N;s?Cj zlrKaL91&WUy}}VklsUSlc)ZIA-6gBIoRH74hLu{s;*mg>Il9Mx52}Y4pt%2!JZ@9} zi#-2m6#2hKOYO;8ppn^#maf-{IQcYvY^&R|Ls2D%$& z{(?0(DZ9Kx`~;mz6#)Kgu^%{-vs@8%xCJ{sCaj%DND?s*Ho!^68596=JMQ-R{j za*vbVKbAp91ac|Lzep3vE&t(N{fBpSK5mH{-*cL0oY=s`*JTIpC$7$>9>MhGpZpT> zHJ-kKP{2OgtKyK;o1o(vn<>p_h$@KZ(OFd2)~1@Hzmy*h14;J($CEq_ykg+Mu?X|2 z4FCN8#{#kZd&(b8+Z2OyKAs`i?#1CNEPL^~@Q4o<81Nv?=+yUF87Wh%`8|^Ne?J4@7%)qd)3y_l|HR#QUQ45 zj=14`cyhhwil8f4Q8krj_51`WJ?qArrh>&8Olo39tQXc-ZyIn~`<6l3yE@4d@xOZf zU+YPL&f3~qr^6LNy;>(AK7MwH2jk3+Re?4lZU5& zb{6IF@v*kP9<}=7;Q&@3K7C>)ZT^-JJMtWP@i#yctpFakjRaI5UehUi= z-N22Lfq|j0un>l+G&ff;OlZOhJ8NQMA`~FsNI1Q1VPO%~FgM&Lj>&-_#{@!}4W6*;NBOBY`!FZ~sJmhIvZgn*xP&drt^TMpIuI6+-PvZpT zlLZRu3_wc@Iy4etCCVaFkMGB8%Yyd=@CzF9khZqAI;{=?4=RDnR8&;k1L0_#zxKnB zy&f+&{?gD0qDM{Fs{8nSh-hhPi9sCA6fEQOdq2;7e!NA(Oy%izxVnVFB5x>X=K-XS zrwR%R49(3c?^R0VviolKhJ_pq%*-g(&1RZbx_!K=sf;IH?v8>DDoabBp^?@a%%}6R zdEA&eocm%6Sw6ae!(I_QDR zeOelt-SAwS2(<`)j(!KKtO2qxGQ#V_p(ycr3)IFD<@fHbC9eERW{?f z+`r$Ib7inAYVP}S$={hcJ~OjG2U5^$l4=2>IPnr}Bqy&Wj%h<0VSwR8c3sjMVKOPYLFhEI0 zN?Zmahoaev)wlC5cduehT4eCn-q8`f8NGe)$QgUQRHf~v(K2 zXH#Uy8^Z@?B(F!khb=d~*D{9|JuLkkD&_XG7@Z4RU&SP%eL4KJghC%}@ z`LyD9&hJJ4L_l0?wBg^-HkC!wKT@hguHkebMcxt*^1(y| zK53Z=;I_CqCD{Hv+a)u8dK@#|YiVmoI<<@$de-39i?^|z^W=k~qwIPHA;3>|T5lt3 zA$^aVvXISi)s!HM*RmF<{-r!B ze-BmnGm8X2EN4`lLsRMk@Y>2_Nb$mGDr#zsX`NWMFGXs}#=6$Ygce0%-5cGfYQ;fBIi zL}EcxCnPcY61t?9A3ZdeX+b-i&sKHxr`mK21tFI^*6&{J%ueV9#c~0AJa=}FQEP7+ zhB2+~=^UHqpArmguq@og6}=*#dYjX#`doA{osBA9^LKu-6NK^ADwAuBZvVfg&~owU zPoz&mE8eRu`Ie4xUFqLq(m;H~iba-Y6S$G8R9=d@wyAm*Fr&K6hgY~CUpx?Tb~j@cE_g z9ZtvYT1?(9D_e7XLD@8+sAM|=11-Q!paCMaRWg>1J#RaG6VIul(8X0nCzHDi7EiYW zT`HM20$O8yGa3fL#Ws9N!8wAazX@TxFi@;JK`yZ<%}ZXjd}#m_i%XB*hV`a0mJu{- z6_$=vEbL8t-M`j6mRs3Am!B4d`Zp* zxV?@N(%nDC-Z4op&an&2Vv9CV5Oeu$h;W6=YL$-V7G5v%yEy3LY#Y+Ypa@yZM(>GU zC+RtEjchd&sBfcO6+M2KqPH;!o&#NuDIe^Oq8CEngV0*h5I(gPyiz0NJn@B8)0ncJ zVoxJE1X_(oz{e2R0}qwt$#OD(3lDSsGM zm|pWR5n}wvoW*G)BFKB2Arw^&==v5nZsZ)2CAQNP&6x3U`9Ok1wd!iAtr-ZZRAVR7 zIE1d^Whd%2La5>~_UCBd@uZgDZ%yA$`h(>199<{t{W2vF>3C`I zPH6d&cvuy@2=Oc~UjQz_n>8; zL`@gh{jgB_PEAL7jihUwe&C}hi|CoXNC7QIhO>TvTnonWwNChB$K3I4d|sE&58|73 zSPMV;MQ7KSH=;;0Rssu;n+B%}-;wBsRDe^`Oa1KIvCcroc~HxZ<8Pb`@rXmz0YdMJdEZ5C ziU{Q`cliak`=h0(bYG9fBP+9p&Bv~$?TfJSxvLIa+?(pdg(v-IRi~Sd>FeLfSj&DI znR2V$?xW1LD#P5RA)5kB(M)d1yV}qybk@xTEHv4SZhs^5gBvfOqXh&r+H^jv_{^&p zY`F8)xy=q=vBXX!@#M66N____R5s4_CKE2!kVs^;e;w1~cobx~yfsQm$Z3D%muCL0 zCwOHT)Bi=q<=R_qHdJqHFRR!3RAoDSZ>@IKrf@sIki7d_gxo=8#Ix)^#X!ZhJXN*b zc!;L2R#;kvr_>GT9_s>%MO`Z2irwamWohz32dom(haR>SS;8*@3}uku^V{=qR{}Pw zCs)oFqGU~gzG2kqdn`4XQR)UP_1!NR35I$+%C5h)^F=t!Q}io`>rwO&7MqZBpwEOGYwZl!29U~?%Gwfh&UXM06Ao=ZK4wI)C=W^mT z+d}X$o$9nP?#;XY&_N4QaayB^FbT?VW)o>7go2gr+1~>kWv&J%8})P|uR`+m@j$Km zXFIAmMdKb(h&5X<&&~Wtf_z^Nu>PynTIq{mSt$lPG8Yknax`2L0(7r6$1GxkTbq9V z+x#xOs+EH|DSs^{k6RO9`D(lrNxoL`n_;SAIBt~eR_rxNy@eR#r!~i8JG@7)#(3yD zgN=}jDcdww%LI3jx>MJX*^QYZ!ZYumlrAUPoOm+<^W6lTr(*uRf2pCGjAONVTw5EB zX+bj>4FlalW7V)J`*`abVK_jZ4Mdg(bNM4t5&+aFX%Wfbi@?B!@9 z=xwtS7NkdTn6l~aMts&uekJz~Z*5s!la`2Nlh|N0;GQbV)3!Nc&gMYPcINXN*y&vT1 z*jsY@YRBbpW4_m7q z(Bpt>=l5k9_IISl=zDRL9sgJPf)gfD7d;W1F_<&(+boJ;%rD#Y=_6$YJZRqn_iE*J zVa5dbeBbA|>|R^>AJhv!J?{R-M)&#Q{4IMpy!%47WtsOq3zm2JeB9+o)$=;!dR%?q zb3-)!;wA{#3pt{k&WzCB`ixw5{_vT8Jg;!w^0kqat?7RmX6P&{c}r8?*?pZpO*iZh znJnfTGU|Rg%zU{|x23y{8UU0)e4^9n;*M6ws4! zQqHIqoa^vnbP{k<;!Yb1upbnq#t98JX)+Z#d&+uVaxL(^RT_j(^9I4M#Rq2_FOz*j z9plV`jCowJanAlouVk%9!&s7g9u`n|pW>nNy^|g2x`oZM2zA)O?=p)(QJB=W@BQky zGI+fizJ9w`-uJux1}pFi19{aIDg$=#GK9zI{$lFh`AkYPQXmRldT`}8?Bm(4B^H}R zdqQnR-AnU%D~*m?>J66s^!YYIUNh$PX1l=m)!7PC7fqzO*g=W&SHHWUK%_PR)&nS` zLJpiRH|XYRXbLvlLi{z$woRK{*xH%~W#Vk)iQML@oDraXoqo(U&(Rbbo0^i#d+a&} zVFCep*~w4YNjnCm(J5d%$(ngb(*E#sXv( zewk6qX7mU60o|vJU6=6()NNr{G1<7-~ zLEkHfVNJK07YO+`nZ!OZ@(LpGUWb`o(1lVSPew_ zC!B740Fn zlDLoco(3vkq-zaMtf(wD{!G@=to&Ai)id7FkC}g+dNzF|ozQ88jVSJZ_PtPrI!+`( zvRF8Gd3NlRfq(ODt;O_xy+6Tb_a^Jk=>+Q+{|dYkm&LGIc@ls*Dw)Yxx(bQyTQK|DM>G+OMkTR~4wPQEp8@iU$FwU`k+4-_U=f(vM{e*eLTrNX1Q z?hlWE5bPnCli}J&T7ky#i`zhqebwPw27`44w0T?s+6$@j8d z6+z)<1%aOn{?$5dqyw4L^xW4kO^DuRt1;6xi5nI{e%??(Ak84cNMD~Y%#h75^h_3X z6cmz)V;Dq4aV4b$g=i^YUHzVztStO2c+Rkf#v9zXiQ@Lcs+FjL0nt2@)KXAQZEZNT zA}B981%)_9gjmr0{MKD46MOsEs3>?wg$buL=(=##^JLJ3)Kn}EVrXdS!jclWhEZ(d zL%z??&p;+YSXkIyDCf6F+rr}FzyPDW+*R7lU&DH6sHlI+p(1f-fF=~JmARnaAy3xF zF;6zwxsQCPLZBr^RF!j2ty%}C#cHuURGg#C(V3D+;^@asClpZHtKjA1+j4etx&vYZ z+XR_TJ>>Rd9lR|qRcId^CR>289RqXoVG3BQ&l&q!H5QL>I1+Q8MZID12(oRobaTq#?N8i z@NHJ*%VRL|+Z-%%eAppdx`vSyQ>6wl4|LRD<9TQ4h^Z?^&2dFJx6496#FHNy0P{|7})lOG{Z4a{OE3ofyC%;PljL zr?{!Pnh>@H)sdDA&CWM(W?|vH7&@Fq3^c5BaC&yO?hR>m?TnU3z#lxEK-O z;^OLu(Qqm=EM_Gn1Ood+-hb}w4+cX?U;culirabbasYjCj z8LoLRbMBH+s4&?X>~ceQS?fsvQ4+L$%@r=g%IupBm{zZU?gwKX%tX|>e#4+-`MI{X zR--JhD(R5Hh5yepfR+3bVCtjjDII6TlwbSDY#ka1Az9@}B^h^R;EpJm z)s&hZi!vh>bxA^_y!+!dlVKJ+wd>%VJoIfT{w&s_tH=i*vG=4BPRx8%cAw`$#5%vq z0AAtT^aK&e5ckdP1@vA>TBt~*qecX`1hJ%Ae$<>CUPkqLBcqKQndW4+BKtk_IWlZ_sqI#)gT!a>?Ht?s2=J4MHgq$4F+i4Sb$~ZCe z%nIVLnu)^Iu}E*%HM$`pKu_~$9jp4;Rnp}oMlRiUY9ILAv#_Uj7^^a05a+7xgthYt ztkCR9G@STDSv0Imz6dhSyCW;=_2CS>St+-6RQ{QoIGl-X{;DuT*9+IH)yvhtnb^IF zG*4l%q#cdgU?XoCX%DEr5$96j6LRf1jNT7l$ncCn*_^H(G<;q@c3A6S;bmcs^i|e3 zDlB5gX0@{xE|LFfV?+T!;?zn>_eU2(aFPWJ321?5W)&qabJ6)o%lO;Wm|NxMa>3wC z#{aa>HNSmIG)T=v91i3?9y}6ep+V#Bopq3V3iaK${He{*NsGy;Jkl<$6}UaS9sV~O z(5j9VwVK%Hw6|)U|DB$K9FE^$751_gq&as|_tv=}BnxiAwmF?ag?wB!&^l zLdr6>zmq1-N)Bx-?Lj~-LsWlpCez9h5!fU?gUw`pf1`=VgzQ?Rsqb0sv?A0uQ@gZ6 z?rNVA@HL|LCcYjLkcd%qc56=I1O9SJ~h>_eLO`M> zjQ0x9|D2e=9m2iqSg|Yv>-wYIiksU9g787V)Z$SIkOuDoELh3o-z5$jeu1y}ae&}e z7S#3DAzRxY8ixxpE^XH5=K7#^L@$4*nvUr`q10xGsGG^il-6kqlKaCq4?iYUl8e3@ z+SNize3lWO?Z#ZYn#I4h8o@_#W`J2Kj5g*L+3y9_xH}ZcVX>X+{kcgZs>ZeBDTS=E z-k*1j5|lXQ$Z2_nP{3*We4sOYlG%Zsgn>0k(jj8DU ztc5}Fw~m0ThM;C*?3!>I*~Gfg{g}4eOO;n3bh5A9EoBCqrKp2t_N$NPB?0;w9t7yM z5^0j4O$NzVi9IonckRz#Xib z6LeMyB5!vb_|GMR^N+}asR-2Ya09{=7x{!Yaw#!VOolaO^F|ZJxamXKNYr}HC5t3A zSsI#}27iA;8~Ap-P^K(QT+A&T3;D-r4q+|R_x^Zx7h<4%qi)byI)%=I&L8Lq3dAUS zk8jq+BG2s)MS_Dn)u>^kx|}GaP;d8s`DJMRO-numh{9`@lW{bsKmd`BCFC2!ybe@7 z$P)1D2JwHK<0PY`gvRVlKdOD5({8ru#nSh7)J1nlN=iaxqFS!jMN0{4*aaJoB^m}I zO>2~$*2Z(|P}peyOv3KgE8e#{?p>dh9Av9(gSUx z65f)auiE7(Dx0JC(`8xnYAobpNvtl1Q2sRvjnoH-!_}v*XXnZ9B-kmsNHa{ix*qKv zFeDlu;`{E&sjoBPY4%&Mxm#7sc;2L6r{V*EF`D?pOv{H3?al3yyKp*bzPZh;#rZ2Y zsrZed?K!-pi^l2b)YQwHES1?~r7f!htr0G8Sa0HqpaO8jqsK?-zH$T4bMuEQ`}Kk8 zs@%C@<#jFAZ7}-FLi*mJe>)?iD5On@Xp~~|_15I^Kx|n`dtax5*sBA!Kh~V9tB3mJ zw*^SFnly*gw9RD~rtVfLmWr+^u8uNR(ngf5`t9DPHun7}%mMz9>9W+Txz*8;eFJU3 z00$Ys%U*%Yu{MzM9hmhvuGhrKC+*j3wXW~v-UHS`#g_)A&+(OX{-aL^|E%OtC5(Wr zB>d;!^_eSCIEyr$v5Tr@scV4qeOnJhw5QAV^Iy2WLi3z4t9MHgqZ#dnph0z#w7}~$ zH}ye+Y&ZTI$Iq8e_FLQ&H%u{DFN;n-fpe!@+=I3f=C$HWX=)!Qvl+};&Y}*pZ}0nXMPHM2k(f9C-H;8 zV_hz>sdpEY{m)%UHgo&}GW^@IIZtm!`C0b;458k}!=szJ)R=;Yqhc37f1C=M%L{Oi ztaZH3tSm*I4%scRJ;CJgw7)6LF9h|yWq*81Ht}ULh%d(^J|v-l=T`3bxl-}0OL%eg zk|6Um<|}N?1zCa3t#8u$Tjwt)to_(mbM2!nBZT<2FPRP8;L=3>lkC*KA*V43K4rEm zZmoTHN4(3Wxh;M#m)lo!-fnbZ5}wCnlzY_c!3J>|K4?M7l>!B8&|RBtm9L@C_Bkf2 zv9h=*Djw`C0be~J;Ivc~?GRNpS_gC>gA#9j9(Sryt*PgyWgs`qwg0#U==FMOalb0) zEq@@(Ve(zX_2_|Q`ZGsuT#&#DhD}fix?%{=uSsq;Xj9_8!5h`($fz+u@ti!z2)UkH zA=3FoFuA67zi6p8IchkfO ztKAQc&*jt|L0~2AeL+IcLDNt{QG=Xr#p7oRWAtf}|KPJtlN9tx|KP%I=-0&e2UEiR zCKo;sLF5?=63Tn@+q4_x2qu8bCM8(J3}?nsW*9r2#|UH}e5(*DtC zJX~QvGx!<8F85NM$nSz2z*8#Ue(`@?GPyvM9y55&$X#G(*ZcU_8!$a73-12u1*1BD%Zz4WS*?;sa8`jxIJj z)H@$0EF|4UH8j#{hrMNG5z?TWT3X;b!W;NbR>v%E^I>KK^4RXC*V~+qaLUJL*Z(1K z%Sua;*@w+K!rWY3zIh2^p`*ieghoQaC+E4xoPzoYK|&VKyuPGEl;@}djN*V?3VxNo zy1M#p5eEna-1e4tIJevE)D-fX=4crEQ3;_A#6p09f&H%6v8ixT%a0r+SF4UxEkT9E zFK@o)9!%;;Pfr(Sv9h!jvb3aLX+EvQ1$Vg;Zu_AY9}!?UoUX_P=LnK7r9`X2M84z< z^auy%PP98iS%aXSjcO!HOG%+d0Ng!20~fJTA&JY$4z!h&0y{f-K_TG!;L*W{JS*>x zrh`1N?(!EFRPt`&Xs?H?;KRXY9?q5%HRK0(;qT(^@16Je_hI6I+Kj{PzGR*y0*Fzt zf`)Bg5HwN*IPxO+ukp!Aj3Mr)%MCAVH*7MfFbr*@YYjBxCXA-W#^12+m4G-Z5FA|G zYICOG=oFxlN&_B|g&onz)bSV-e0Fv=FchfW!t!O6mOCin%>9K&J?qt2H|)~T)J#V% zM1Y5fa5!hdBQBoh7g(UH^LcyxH4CDos_JtH+}`DYV3fe4T7$`51P@7p3VP?`8In9Z zoJHKescfE(Z*nNf$EaJ(z>`p!FrwcA!Zz!SL$`oF`IdkB_dmE(yjq%>PumKotYf^8 z)Fpn^a|%vI9sN98fm4aKIE~yQ%$L%FLrR9pPlg%!>J=}(Z_D73+vs{UX+`=Z&6%Qx zS^0j0oa6$QihU$C@qm)-mGXlWgqmkqjc0VB4JNem7UtY50K#ybQI?xjKPT7nk|VV4 zAf)B&K@!8^30V>g=Oq({0vT0_O`;=9B?N~^q+=h=w_Enn+S}>S*9)?!zXuxpc1QF# zmF!t1Ik}BTR=m7Mw2o`+(T8Ldv%e=r8(kh@T%Wb*5VZ${KgD@%f?(@*5;%tJ8Wm9y za&frp+ERSQTF(KId&_8!`&zt+`q9BI2K<#H`xAXcX_nY*9VThdCWh;$)@}KqteSkC z8|3X#6b=;eB?E?A$v*>85Q}}8HTM+GncykldVS{=I76_EDjVb70aj7Uur_WnbQnem zcS&6oh7jcNmSPVU7stInx&+S`5dI)CYX*UEiX!KlLK%VWr+O_iU}0Sq8WT<8C$hl~ zUXJ(vq_6xgxDhvd9}GX3eUaMfFh`1?+NH#3*9LIUSZPhvlgfMmcVsDqd5h{NsUQNY zSuwhQe1MasqprctT;v;BH#|%aS%j5?_^Hk-aAm!XYGZ70kv-;qO^8xr zdt}uSpThtr2*u@STnplGYmrqIghW=sNnHhf(Qq|$g+{@7$oSKoAy$TT3OHXZdL`#Y zSe^8=dX%=SP?iHBdrLBfeoQjE3Vuf)vDbbcHA}A3G_xThY9=AeyzOglG8AVS|D~#;qK8624E8Ha zw#Jx+%ObJB+e#^CP9+t~+1Xhym)@4Y=%RkiG6GO5l#rjl>o^OV@4g4LJbNH|Ox8+F z&)>Zn+U!zf5{fj8XNf|Gu>%lH(TYZ#R?t&ZiTLE5Q1vOMQ4W93|4_`!4^+l)N8qT@ zM>dO9OcVw@d7^t(2?reg+VA)wzL^a<(#fTsW;@h8GpDu|PS>Egn@*&HUysp6KYMH) zF6QTjI|KOEXa<@sW)du@jp&)VJFANoc!}%R%lzYum{3qG0PmXudOK`vLCr3yf+a+f zKIkcde|x528BY6LY#zBCK}LQ`{#!T0TvHD!Qm*h;(>_W3@fc4S_tv7t^_FiL8jT#;L2W$O+yg zUnyR(p=1|T|K$FR09MhEFRK2F)MfJV5U?;Pq3{F%aW0v&M)go4^SpY?t(YLlCjSvg zPUtJd>t9bl1P9(D)4ww^^M-h$r4QSsQoa`YQ_&Y|)O-^&LuD>pJ>LDEoQ%+u@EbG$ z<%oa!Kj-oOp_Ki^#AE;^Qt;Nt)D=8@Ut`6?Gh8VtDRuW*dWk8)ev)@wIcW6wo|_Ly zo{jipgbefhkUOBe{&iLimzS4KKulqh)j$LAWFv@PkrC)>0494ZK0ISyH6s5u%26rMH>|F-JFK6arv4gWQysueOcnPkSzlW{B-s=@nmr~V*!puJFvA>2YmWzuE`_oY# zH7#vWw(6A%P3_69QAzfYBXi*t(WV9D5>{-H6aAY5QWK#-2tY`M_Ao)GyCe|_?i`1;F zHxe8jOfoR!EctJOGRb=ip3+GoUb`8cZvW2Bp;#gu1}o)<$H%{LI3YgL{}vDaJ6}Bu z_>{@#C?E5Gnay9Sm-1W>GB8`aG2@QrqP-|^eS%+$J`s9kfn^4KmB zo5y>S@(BKvviQ^D@;&qpc#6HfKH}9Cz6A^%x9Y?Ljz7H<%XlAB9(RY)BpO>%kGR!* zsb~unFMeQh?jJ6K=L4?2?S6kb%b?3L?C`u+RrN&oQIS2MeK$J)1;kdDgfEHf0smuk z!J!Dw^HpwmxaY!3E0E2r^!g>x)I8l_J^xOF2wKp9S}_obCluoId%5F@7m`sHzKo`o zRnFi(F?3a!opv`Susi<+$Q{eV-e7N$8E5AUXMXds?*%XjK)_3x$Hsk^$)S(i|IgF2 zYxSahX56;3G#Zvq!29zNSALfciNM$W=zYbdh0bMW-KL7zKsU7*o7}*2A@9560J;C0 z6wquH?sb<2x$Z#7VG*r*oE8f!j+W!cqV7RfFS}|zAsnh}YLQ(-%5^`}$boh6X*dD_ zRU=C+?~|@6!=`*-us(B~eth8FppO`9vNz5c+9DZj%MDjyA zik~c}1x7+EW+-r(hA#ob)#V5~W`UzGwJIm>p)T6AKrz7IvY*e|B)VCvHm^oO`2`SGJt2%tOdmjYkdFV93QD@V3?dYvN$=JQ^`D&HYxB^o4no`0XdGYL zS;3G-fX=KiJ2yw^OUgf$LFVIU{mZ?B?{#JduHiecRzV_%m01jj5ZhjH{-_QlET^$@mBhoCc^0dYIZbnIG;N6`iF-*6cPOp=RUO)qW_qMnxX z)s=<+9)1^*AVc5K6`2j8y=aZO7P`+q7N7ppAl`AWhmddphopq9?qTrh%I}FqM56hSJIM37f4u)$#uJ_1375{DH-jp+Ok2q3D~q^835X6T+W z%s=%2hmyjL@imAhz+j?iB zO${i7O?pc4purs5azHRzGzMv7-MaolnO+y#*{OlPMCEjHS%F>7)ZH=Zma7rswXL~W zVTPR1C45GnkyM2+!70}Mz{qxF8tf%FM#*p}sD^c(n7&Kx>t44SvA)DqdvW}!!;|;3 zULY_XaT{|m$W+dz3~!+J`Q>J!qSfE;j5LQyat!7sc4QvD&Z&JOVZN95gc75e}+OjzDgM2tSK<{$xRGVrU~h!?YNY ziYbnq|1)K<0(lz{Wnn-Ba?RUz-H91LP%gFEWWN65`PGo7P>oyDtQ|;&38HXZWCvjZ zPlpdyj5o9p@(K+!>`VwFZofL(0)jbYrcf6!*9PqnomBYp_7XO{IzaS8HR#^w51y;Bx;OX5m4wz zGiOVcSP%#X-;Jaoy)ZiTANt0v>3n~o_RM)=LuN#p>e50~mlQlN_6IJk=8=V1kw|kO z%{1H)@E*>8xOGmE(Azg32s@#ml!ZQoEiiK6MqK(_^agv(;stA!PY+Ssm!u(s!52f< zcXLCp%})MO;1p^wxl(=&UC)^lRR9x_@xv$|YT$DCoaM0peNC@Upr)Od$Q_r<$S1aA zY!|P$qUsawtUkvcE5y|b>Qq#Xs=GHrP)ka<$_utX}Y9bf9~PEdYWt!_H;Xt z`=8c?fBXqmbO6`^>1<(fI>iMuURM2f&IRTO92VyN+Umj>?+4Om_A!&W=50ljf|Qhz zd9@@VVd-sX;0~bWZ^gwuY+za-0lfN$Xn|24WNIZT8}Yk5oVEE7g_V_ZKKC^jb*|QX z82W47M`1OJsVR#n;O}DZLZHER)Jx{g4olBLZ-NE;m9Slbxv!K~YIx{`git_g!5CL$ zz558z4f?`lWKhXG;Y&P+gGd^M4pch6o{jEGouMwsRm)uO8`g&jlSlE~>1_h46K{aswR@sK208#5|xxm5<$~uG4+FiZ` zOq022q9h!A+(Vz1?|8yh|QWcSvWD3I#=ERb`D4ws>_Vat+k@qHIWh`|0 z)j<{7uQw%pwtw|YAm#ILypEp_`}~I+$8E1D?}sv? z?I=sA`H(~9=0?VhlhAdKwQdrk3E zrK9L~%zF3**g8ai$KHRk@%LAiM3)F_sa27h(s9?Zf)DW=?OW^S;ZM>oN1pn#5OuB! z9fs<<>suekyLHzE-xq9j1)aL{2xvVp2?5c?OUXrnDMna6?;;V#d|I~!!-D{I)TTy3 zEz~%I)~s5;S2bE?MAt00s1-UaE;2SBvQS|%!Ye$|l7aiuYe)Lr{MRP|*xQv2_Q<3l z>pKIhm+XE6E?scb#K4<029ymg$l`G<*LRulVV|dHRw$3G79oS;EEl5#SCAsSb`XRV zrL|gOV@C%<*o9h<4V>s4b6$Fbtr}+f$mYw%9jqlmWAx$sA1zRS02%L@{Elx~^zJV*+#c|$6MDLnV)X1TDBqu6K~<_>Hq=o^T6<+KeLtC$BJ-H5 z*rz=+X8p9O{AA*{4qbp@(ObaWPJj5k)u1v`CCPZe9FN9NiV!V0x7q!$PqUDe{8d5LBk_(r#PfiO~_bzKsJ@pAF)5Ce_Mjqr&LQ4qxQa=B1N|4vS z*lqzeuOwho2q2Bc!`GbW~Ssny}YbMEf~hL7Rn= zkN@nX0L*ZdiQ?|i%1{2RR#=~8OB0uU3g+9kXltcfa(+3OVHx}oNCsCQWlG)7iJJXG8_=-0l$0cQ1u@98_`)m`%B{ETFN zcZpBJ5xjmA=|CwE=UyXH`E*e9?QXshP3+IiY3Ji6mLYNmm!%TL#Tb#i$Rd^xM{x4Z zW&2fF*W1JT?%5!`5@zqJjELq-vN!OJoYO<+^$^i65!@_nJwBg<9?ZRlJ#Hx6OJ(%_ zYc&qZMK`#+_3zi-GXJ+=^S6N4^{p?40dH=ZpAEdEe5p=qO^4m%@i|P0#Jx?}q!p3b zy8qnF;Td@5iMs~+znm0T0PaIKb!D8#LR)wx-rnB&Y0=E#TQWVp0G)cAkY?7Um>c~v z98KjVr6r0qUZ2=Y?J|PZ>$Ab%4Wo2@cg5$<0jjQnxFzACOS~QnOU>>NOS4?HZK+!o zP3bytoJ<*c$6CQzVXgMbk1gHQRjPNPzfTVzE;mQ=9VGD{$=eFyRHa1V+0H*YInZ@* zewkf`_B3mhnEi<_g#2|qAn_I*txyV&YAtiE4c%}p`r90-_l0-`L}@fBk4GO}K`g{Q zMOQxF?7@agyKZoLG1SGto;V(rAbzeS)OU@gkbyb_NUcxKe0?|V+dnqq`d%wPs@t9% z=jl}0rxjq&Q89vIEA?e*(B!BTD$gbA03(ZP^R`sizErj~i<7c7X70-kl2xCHh#^&^wj|i1*iNLFiJ%gA)N`}lY4wByEf~W+G3!_C}r8I#~NbQq<`&}sR&8vV3{D?I6J(52IA z@qlmoRSB&?v$V7reGLaa2yHbsx*eyM;o(VMh_+lGXqz7B1#2W!7Z+&I+ca-U@R!u?_N-UyVYT{4Y}u>- zubmA_LXfH18)8|eU?!5S&S!QLXanoxw z9fxi~%Qoh_ai(wZWC!bXbJRnb+j~WUECYIzIa~L**X$2lr zyFgCv7GBI_EXv#k6~rtC%=WK5RG=@w&-|eCqPjSIlZR9@Ns8&<#AFI~gskJ{vMRjxf*J^KTTllbER*yGH z=_(?gB0{O4E0TX?qwGIJzv(oeO)U*?8mK>>XasqbLfe=wBseq^*Gdz3PC*=T3 zjy9WJ)gLB-8T>ro;YcN`&TJ$Jl%zfEp=MH1G<*uWXW(Uv-?+!@W|NDh0z};9O6)(8ARm&N40kTEodKDb@=XOOT(#Lfi)4oI_tDbO$c-IS9 zMYMj)Fw&$>m!;CU7|%yvWP1pw4|piElAZ@VY;04m6@n=5gk$|03|3f$VeH&Lt@1f` zFgfj;$j|>Qv%ts*N#5VaOZq6|x_IrQ&3cQiN}nKKRYxvMjdq4a%%xB5PF~Eo7~ZtM zKuTHpi}C&0^?+PZ55)ivg5m!&9o2ce8E2~oif@nn_9Uv)4@V)N+DU03;Fyq;6TiJ}R8mrcJ!5c30hH6S&b9xk+EF01r~1io2U-Ed zM^!z*;GKZvJYAkB`3%s5E`qL_lWFAZ6VDux4d^w8C@#<$+`tBEK@y+~aCgN22qB89(Jdr;1tJ6;3M@H3IMmoA>@g=EpWWMCd{Kv`vmGArAmGd77PC;@_=#ngI?RLw0@%7%Z zh5IiH{giLJeSezAB0s_0CCAkW5t~>0mhXiSJ5@XL8!tC*S7?8C$LilR<$%u^BIL=2 zTfsCzt&%F-{^k$73wWK;dt95nLmZAgk)#PjY}Nha`^aOL`X3h-`}x`5^oi5oAomY_ zgY=WPE}H~fpJ)JE@YWw|eHl&Oj6Ow`%kq$YT}#ztFO2)p>zb`wbM>y<79O1@TkH3H z!6fEG%GKlW$$TiYnw{607knX&VS#*+(d_f0-(8}cq>l?-OeM$(GNw^*icO3%5@Ys~ zHj~wuDB#C{M!%gi2A|6F$9O5rUz)Ck0!T~QR?$5QVtKThrPbvNC@JC@R?2Q49aZt| znQ9e?qfa;Wt^Uq!9j%Y}3vZ!W(^R(ntbyUf!9_YO;K91LjykqQE0D8#Rl%z#OJCxY z<=E07GOhWONr85Bh2$V|JM>FeS4@~sm?d&pV4Cs8TU}C~>dXe~+ z$=cJ2gv9Q0pn1OK+{Pk)QmphcC7>D`fao~ZmFN29;wp#5g->(!nyQTwWfrG0-s+y9 zX7!z>h_yxg@1h!Cjs)EIEv7D-*RT?q`SPN!jl9}}cGAB{NZx-MEMeQn|2}=_jy>9a zsXjHW<1pLHG0903VUh-!3JM7k`FMN301DwXf59y~H}?V_Q{gqT64S-~ucf7jnT(8# znVFfu*T*ZQ1VDK+2Fl`sveWD!WRQ#u9H2ga`)t9&!eV7-C+X(a2vO46NrIf8p1uk} z?F?z+6dWA9y|)LAhK2?}3hEiyY8o5gB$AT{3N0l+L}aW=K%9$23qdPZHZ^JeE{uvo zyoNHtB_Lq<)Q$uXANb?Pk8L?1sow`vw%+VmP*tV$E0DAiQrYA>e6aBF(83wo1dGqq z!~`7!BWPwug@U#gA=(T;kVoBwacEM;%5Xuzn$Zy$OCamTGK=`>fI4D37A%zt$vSKph?>H}qcs0bb&ck@5ZvEMX-V%rZrphC=jI@+evqp)Xq!G= zvW%);G*oy9KCJYI6UQktY$0g?qUFbfTx>7&?9k@g!!=^&xtuC$LeP=$X%wZelkDU;y-A@Fb$6nup_VbgQ_{P>tv&=MwX@4DQ}Y*J zQun8uALta6@^Tw=ocyBkAsB zS?0dPj8SLXb^S(+`eEFtc&}_L$k=_r2l~_QLLNzOpxi0d;QRpU##&*+uqq+xGb%q$ z2Xr`e6Fi3{doFagt@HV&wCDbcaq6XHj3xj!Us2@fN3)sp~jQBSx#KMi^6^@M~6!qZYscdhb4)7(;9Rnc=&kVeyKA zSQW5o>h&8bng*-OGclQhMDOXk;^W!$>)3^8j}ce$t6Ka#g^r!qa_%o{X2uTpd=Ysf z91q%pU+;#pT+eku^ArTtz6$M}qtQ#VLViuzs1o9oo8I%^A^~1Wg_!V23{}eh}7Bo$8aV5IIHS#!Twpr zSmLTKnRX)jr{OldeJ>6T?{n`uOLBLHd4s)s&Sn?^zTL7b+HPGsAuA!{ZpD&wOVK(= zLrK9pN&Aisa z#%Bw%g1!-7FXcB9!Dx4k>E(@1+$AdOAF20f7prY2J>NMYLGwn$4mbN-UaptuqEx*ui9NJUj-GM z#WSZC=t0|uggnpw?c*on0Vy_tlC6yo=8yI&_rFl)6I@n2{SdoEnHGiicYM2MTo=ni zNbMcc{ZtD44m^e+GD8AU4*Q*$q|utVgS#-C5tjSd&$hRYdf8r+4f3YXqr4Bb@g6qd%MWS6}x(%-r^a=|B}+$zN2Ka1cW))a5m{q4;DwM zrI3M>UJS4&%T)rt4BFrKYZqgO>WlX9GCI|z^A7#mhqWs-QMYBq2A!0^N|%eov+!B$ zsDCKs48En85=S`&(oezk*81?jNuwha~C8voa)x}d|p94sQ#XP z@*=q-(BUJ0>f(eCd61PF zVdOPUivy%2!UJ^t!hh?W2mZq2O zw+@@Xmz$!!yw)#w?EKgPAVoCnA0TH0;KhKJxY%MC;vnK@x*~;(kw~hCFG4h*$b>CE z*aiVRN`d?eYLI59z63&!eB-wq`nG3cqlzBz5v241IX_L~MM>e~L1QJdN}l$1M_(|{ zsz_b+Xc;|gUVZVEhf^A!=Dc&%XZp6S2?+DZ4>Rp%jAl6sqPEBUG?cBmu-M5krZw`m zUlI*hVa@`XVT-fQBhnnjwGw%F8a;r206nLiDw8@Uy=CTj{mP9}Z5O40tVBn!#qbA% zznTk;@{F8lGV%|`%&?h~S-$|vKVCfslm$3>IXUsKUpbSKlD-LAB%P{XMUkR24KFS) zKf*CFF=3IbTC+M!m}K_{0v2xgY;_HdJ9JE;A)B;W+LhhVfeh@B-i}beBFv!c3Ij@} z!NE8#qlzc$6eMgq>O&rAav%-GQ;Ml|{NOLa5UidR>hj^khmVRfC}O(mX%?Wo2Af#hVLP&o=b(l6`r|6ypN*xd1)CYc0_2xi?3Dg+e{ynx+i|?v=~Ll*Q=A_qgKbEwzPR-O5QM@L8uTA}d-mX~ zUP^KsE4|o>os~lJl17qCXWWH|P-5QSZF5v%PBOqQQ3F`L(kJ!>;f|dCZ{;915 zV3EKlC9WSe^C&F&aJhRq1jd}LRj_zyjYg7A}#jaM6DN z_Xw_=mq8z;rcI<6TCrofG!`2l4vs!a*;4{yt~Vp%L{ab~{S$F7K(r@?`jRV#rDn36 zx?Wy~oX@uVl{tknc6OkS>Gl+Z`BtoD~^0jZZ(FA=S0It)3$>g5NOe7W^uuc?@5)d5@28Q$7JpT}=03p}u_qI5l zQuu$}B>%jh1xN`WykK8{KU3%CKM*t$J`j}s=K~a_R@4Xb?5`;i#d-k7@aw_9RI2~H z0KhEqem6^0{3`p+pY_;h3q^cv{Lk|QN>ceEZR07LC4JE{85l{7zPV@Zc($$q12C;< z4g4f?w6UusRi)lelkDhqkzf3U^3IwwAy2nmWuvXK{^*xMacMg+NF$>wr*SB*J*tKm zO}kq?+2L$kc{(1Da%;nURQz%#Wc7NEE*#!jH!TeNMmqPQ&SI&8x*Crk=b!rHf0wGi znxU0%`(`t{e9>1>vFi*}19a+}O;!bJoJ2G3thH$WUE8r8b5b4gD zr|_EgXYJ0~?lIyZ2Z#ypE(>}YsDcJPCkfj1r|}*y`eThd=VHBaE`k#whr*xNY$%}7 zzfWPQmC!M5-Vz^s5)?}y>fh#ly^E{%VcZv!xi7^_=-wlEV)O4>ww-b*7RU+@dNHIM zHAST9)Mg0p|6jD}!F$7*((%nTLfkw}I%EmLgV>Km%7oz`H`yDUdz1)2@R2S(%;B(* zqm0kIF7oiaRoatyf;AWUBxacl#Tx>Zl9t)gom(ICj+zNlIgpI8dEsu9Y)q z5pa`OBr4>QaFTJN7C6mP;HKPP?28-6=EG`i%|RBAd_1pTkL>iIHNKt1&^D=q!OK$P zCqHd`vCR~8Vj)L6eE$&9-h|Dk6c_y7No>__o*N92#e;_Df`xQaZEG`3QSOycpBAZ4 zt6rlSAiQK7*H?Diuc}H1V|@)JXpgO-r@IwVw4?+Swu8ceLO@2F7fn6;wfk*2cPA@D z2240(+BaUtj8Gm-XGT8+@!M)OM4PF|2_Wyd9TVVw_UEXZ?35+;pF4H0w#J~ z+milhM+N1dt5bnEH5Cob2LR?32Oc9&T&@6~E�S1CX00LPkag17@=ez*lk4Pm)wx zdcp{_E-*7Q0~s2M$;ikQv(VGSV~G&kC`~6Ve;XbeLY!n06#U{D8xaxM*5;j3 zvUA-2z06AJ&_hQkoC4r{b#-;s)Yta|hHf2#15QoN#f$vVf(t!wy*(|}ZUr-xsb3@E ziIrrmdM(>PBIW=nS(j^QXl&xKfL)qGhjARR-CHG>`nZZvmmnW&^@Tcr$e2dP>F#^{ zLMRW?)nIY>@HrU{2lS#RSWU#8yYa(?sLETjhxfN=T>6pm^{NJsl6k_Cb)loSKWnFR zkjdBa3iVOK|A>MADuVy`u>WFoD)FY;Var^5DpHsptm+CxZ2>A@5*>thq8-yJmkF#H zP0+4llP!AgPcyHi=}dqipo5U>j}S3T5YqVheA13mr_XeI1)zYG+zS&hL2LiT2U(ld z4a_`Ni6RnfI5}v;bqurh7s@}q> zb&FI_A6k*JJ=7#jK0GB9Z2$L;=l$zAW4evbPon74Lw~L*^MPPOW;T^O2r=okN~IL8 z{&ZC&y?h@Znp^2Su7d(`vTby%HOdyz|NgT!`*As!9t$?;1*FmqbuE7vYbdJ=At>H zXjgcsTY>Ckgv{qY#wUXFqacv`rxyF)qYgnM z$C+Cr!51A{7%FnI;jjFJ6Rm=gmF(p?ocwNytO&iJiX<+D1qk{+#C7DIrzBmU{PkOf z+RZavEfgVr^gPA=@OP%ng;grAZl#LAd>V-Tk*{Qst}3U!)JhNjDcX?wsSrl)uukG<2Qagib_^~Ly;@eqWbbG_Lqog+doa5#qA$b{OUX1SYmJieX(o;Clzzj$C~ z^1rW|o@2!w2Caye#b>sRB4x_nWi625)c~<|v9H|Sqn8*UkAF^y6>>I{&08#)8<|xF z2mxnoYDv6 zFM}g3L(YjGue09X{2*RV#4OM@A6+#HnANJvxz(y%dKh<=-M$YKGflaZuw?5z3QX-0 zzWNBw=oE!CLHRY{=;U`k@xJ&FMrbm<_~?#gkosPh!o9S$e+3U0N5UoaKOJ^m(gvGG zEOm@qDSZ~#pD@rEzLbzC6C_moKAYf5cpphO5|?1PArm*hf_n||m3Vn$-XJuLTVjDu zujg&Mz^a=}(O{nWn)G4qDHBfhcHEhG3&2CUW3`1o%`eL>q24ti7?Iv-X}{VHMZ(X-UhBVgUL4!UeN+zNYqNDMC_iNknw9jKC1^OhkkEag zZLnaLQaH7{YoewYR{IoK6?1id<{<5nCDeAgp0JFcSMaT)D9~$CsllR3Twx80n@(9; zy-)TIfWi%Hd=R}?MBiSeN9G)8v0q;G}%&&%K~N5jsp;uG-LF#<{~1nv2Qdc*(w?L5Q+WPt*b#(l(qoMh^3TVz4d3kwI^zH@<3gre85(Rg_ z!|@tuOx1hR9`Lto2Ap=Jk{ec8$pWxvEztB_4#rYeu|q)1h}nZ3kslld1X{a*lH3r# z-#<*A7-KQ87yP|;i|_M|1V~z1y54cKgTWKs)@0wl?|!|>9`JjXjK>wzYIwiB+`j-K zG9+fS1Q2JAX6NF{w70UN=jOl-(4N12co+iEFQ>pax{NF=By1Vll8UlffIl%}l?erV zXL}oh@DYBvWIepNxY+3vK-Ul!&lsJZRh^xhsuB|s(GTozb0OqMGpYaxOTne<>f`Jc&zY${(^!gfuyB8MFd)TQvGh4OX4HLPx-zw%fLdj}A%L z1eDSElygMim?6t86r(QtYXBFA4b39%DJ9~jLCHqo^zH|M5N{uv3*v?8owRvgt03wx^N#?m4oung}#`m@4(9qaMyFjw#oOxu}ba`Puj z{^(I3_k00kM8bR-BqvTy|G=wIXX|`hEjtlulDlZX zGd+wZ9E#0dJF0%s@;5ow5tW-Sb&8DgYsxG0!{v6g{5UC!Q@$9oE`>DAnfrpHUL6%N zPWp4knPos~nAmD#G^o^)^bXABJRyu)ue^DN%R){yeS?K7YVL$w9;gUHsPe|!uG8h6 z`a&nM!AdYb{j&R`Q)yljVR(V>G4u9v$8QPdaIEtqxH}znkVuZ-n+A7G3t^>-AcDb% zI977CqRLZ~pQz>8wKQy8VsH=(mQ`oQZEQe1+3I`_Wf0+(o>}-j3gN6WH3D&p=kM!u z<}aq{+B+6#F1PlxPRkHwqs}UYEAc&9dcEjDK{v5;F8D!i0-=~M?&e7)5Copz1M<(l z+>sx`q=Jr=a8Zdi&kbBgRsG-PGB*`%x)K&q`kEB{@tb!U!={(;mYXo~F87t&j?<8s z{+d|voAu0y5EZjv)g0ujP7`#$rTP5kOeOW`jk(}`GEQ}v^5Ejm!NbYr(jrj!UNQg8 z0sLX#^z*LM6fFi_m85ZaDy5Ro#bAs17m=;$8RvKRm(b6Wc)5?6v@O})>4;hD1pIb{ zu_E5aD_U;PuK+oNpgw^w^z~D`!`DQWd0GxU%l%q574(a(=#ceAjMN=?(WTBe(08 zb++i{B7&IO7Kj$la2~mTE3J!5YZ&bE`$afz{a=?s!lJ-NISntXzshnm`XQM?*Clm_ zVWYJrYO#c%ms0V-DARXH42TPXudsrLeB#?CYygxa7{1Fe;&jLNyDwwCL4pQSGFQ177os} zwi$-HQFOmNGwyfHR-5l9sYc%+KAYY2J-bj*y*F~flY1l;_ELh7Bn@TpLES6nz?xeX zxBZm`(6R?E`w=~#Q$G7heXttp7{rI*hS+H*lXmrcUu>TKb6XIwSl=TI5iyt# zc>7fKqP1zK)0kphRv;1~je1{(XZOUfk1E-^-dSTIO*((|0Mz_lr~%kOs$nI}V6H*p zgrn_(06$p7IWo15QNM%5WzZh!QYYG`D)_(iIE9J)F>tB|YfZT3fGG~YpP!%jV6zT_ zxr&##&unlW;k+YI_LtH6*xJwl<@I53WSmKowf+~>!qO5FkD(MoxP0nVF=mi?XlQ6j zRh6{sC(1r56AVJ!knI`zvI&C7igRJDDc57@e8wLA($Z4$cnn5|y5O}6ZcdzD9LB$O zH-!}fwU|FAOj!+izjbv%a&o#D+t>)+0dznV6h(SwW{is58vN%9fdf1dJEc`Ko<_Ie zVHlXO{$(-j69NGk`G8|+lluaKv_(W#vnp+uAskzrjT)^{lCvqmJXc4DOI9mmp_RSO z0i?5ATu(6P=IV9yUx5(K=_-;zf6w_WLcc*L{0M=haknutD&g=>f^{$iJr&jG-}sJO zMxI`96g)i1I0r!RMLWfCNupBIH5}|u{a_e31X-X>%o#asTrWw?ql^oI;u)j3;8xMq zh&&1tG1dxr%I)m>+Z>m`_-P@S>@x5ZwdD-_{w#_R-!mS1Of5FkVi3Zx6260jlIfID z{XiAPCP8U~giGMNth(GQFiS1*h~YH)KU-}NXVcs8OHBJ72KaY?HAzWHHF~;b1Akkl zre;%l#*}BGf%Q8R6VirGKLNk0q@+a8z#!r6O{=ChkCBtWj0`Vf1;WQ6iBH2{vkYOT zqKXzi#0SI2CEtp)vEukVoD%>8D~ryiloU;3drnMC6BA58IfW#;n$g0rbBtBO^_a4? z0=;#>GFR&&7^S8B_*}dD7Z|V1%+CwSilhWM$)?SG12QaYr8@dQD{a((!B?0>IBIPt zH|YMHf{H2xh?84H!J^uuTvr7JLB=}HscLIb#^fVe`%0>O@lpzre7zTzaXa7BR+b_S z5hum!Klx{z{g!$9|4RuLNpWZBSgtK772fna&8F0Lt7<41)q1dRv7OT{?v9UcOWLpq z3#Z#n+&lqNgHO4eN}jyk!nbqMYJGw>M3Kv=!%ZvxJcDcxdH55pYv>cIf0~^Dl z4NdIg&(3AbUcVt04*35=3Dy8-dh+!DQG$g|hVeQcAR7vOqH>z3>!JAZmPRe4BB54z*|(0T+d0;VU(&pn@@0YM5#L|a(va@ypx}V zA-!ru4XxU27RBRvnbxK)_*uJ zv|{RZ1hTNctr%^^Yc9XmtiWjdwxVgJUhX1}glLwCRqi&u7+0uA^|6u}t&7}{;~6Cr z)g*e&hCc!a2locgkpK1GCV%C0(bM$S>=kc<4dK#_SajU2$IiB1` zZNkx%W!S)X?Bta(~mUV37CG1et-I)PqE4vSI8 zqB85V^x8uRua@l7#;=y=3#dKBtC=b-+b|x=0m1t$z}}s)eRI3wh(Kpd0#xeohCcm? zjkAc-%t~Ny4=cGo8mZv3Sd8L!NV{T<;$>gAHmg#hm#1`G-@3m#Ft+*G-F>~Kp~A&i z%zxt&75M4o9#le?q%3xPGxl9S?vd7^?u4}chA`*L@V~K_0KM<0o{?Xt%dJGplYjj9 z4Es~en;vaLYMgIxV`IaywXJQ+&E4H!slus?j(@a5LrPN<2WSy8v%DO>xoNPlxR_T` z0uRi?TU?Hm=ntYrF-gd8GC-+v=T}$5$qV!I&||vw?SF`XtU>A0DbO;o7|LYS_si86 z7C{iH0c2xuZ*L!9pa_F)0H7!!ps&U3%1T6VFjSu4CqS|GIj158K?_*B96h}Ufw@KS z3lFvl8q9=AMn^}X@ZG^8AZ9kU@W#eQeh86_6w{8u+(r=P8|o9;$a4a!7N>SodEQP0k*|LCu09oP-IXRCj&Qgv6sAdGlr1| z+3%liR9f#+-C0b8XcL?03umHAC~E-_o8p7Q<qTRL6H8h%?mfP)S?X0~d@X=;V(k(pjYb|e6G=_FDd%cvguaB$+thu=A z`QRt$Ge$W+Dkz-MWS{+`=e}$$Tbk{`ty&U~I+_Sg$67LISulGIQHVgQ*xR$fg5=0W z`md3pxY@lA1G6El$QQ>T-{g>Y70;EuIWovKn}KqIAZei-BqXFD&{#(k=ohK^oxEJ_ z^XKZih6X(ej|*F1E?%~OaPXnPD^e3iNyc?DYOYu_o;%MHcJ(UGeyyQY{ye0w+xru? zx2p<&kll$PKP{mwRBqseAoh|Q%ANMQHr^KTS=44!#TJQ6yYln#R9~-1CO<^zArBRO z`;G_uTJmK+&E}iO@a3XUZ^4kdZ(u_(k@H&ly9ayQ1!~^9-NscMNjF>kp61Oi^xa6V zO5inK8~~Cj=)|gL2)M@Y?*F&H+FzE{n%G`~vs_h`B(jg>R zU<&6)`<2HbWqB(m>496#H0w8mwdd@gy!HHrNa=95bAtKk^VuK5=j6|oCqnJAksLl7 z_Ad!3L`$L_E43P2-AmiHESQV)=0mj03tzh&@SczVg2M?|HJF9|O5eF+>$^qhgT=+Z zRRA;oCy8TR0Q(hsYUlf-#Whs^2FKf~gWDI4`g`|P9J@HD!Ql4Ar`0xmuBugWwq4?- zG`4|QB0dli&2u!$6gMwgM&)<}L&U)IaaPUbZJ>!6P27hAaW*B=I;HXLto_)cB3ffY zQpHO!10jezYbs~bX9}R?SR{C2$O@aD~#k?eo>gO5?j$$hh^I$DJdy^ zzbtt>e-M<54#tR^s-)G9?oW)bc)=>@=y)CbU??I!PLGaabbMnmG3L@a!AM}&0psYH zU=6SK;Wpt>vJ_(NqWMf`tfzBXhKZ8%GU%0XN}aLc<2NlBL7+nSKX_Z*<`WP%`G?@n zAq*`=&$`s*8G7|jPND!d?Cx7a*bCKaV<&AX#wG?{-HaahCmgAJyodf(>TAIE*u(Aw z4ZPNr_8=qcLJkzc)YE$X-tGkksBczU7jbSI6^|wx{75|&mZsx5X|<#=1h<5|^Vz+| z{g7J2rX$dGwY9A>XI(2}B#a-dpqenKW1YW!bXbTD$sU&g?3F8zzX8A=&?^ZZ(6GpR z@^1JaJ^!Vb0o>!`&GW;b=@@bH@7I@fXGtB{ZF?e09^o5;dI$E-D}#gQchsu=2+sVD z;9Zxxa&95S#gymoucgH%P>u9V^5XI{VuxCDAvLmG*joo1`Gp>SuzQpb22k+VY8OwuTYcJPcL*eBr0@aNuZsapn z))#^^inPpraQCj!p-uIEXOr)M%XG0_pP0TPp&^2vCFu%Q{NI5fvBWZhv0?qxgm=ZYilH@ zkC^Hz^Uwo}?xx)a)KRw~#?JCU_xa0XJJu29PB@WpGngaZ{NP|EYZtyt*wBL9Dg@0= zkTKCVcnRXetrc}Be$Vk!9p)@VTUFO8=kb!9B=ymDumrCSJ^8W+^rWBJT~`pq;>_#( z((3Ob5==dm<&Re@@^C=db0&ev`e$2_2-6f|(<4Bxp+JfZMX}2BT$o#mHS?}^P(E09 z45#ch#0d+>^EhU*Bcr3C)mL40pS$e)AGN(zR2)(J?uom*yCo0^!QI{6-5r9vySqc< z1b3GZ+%>qn6Fm4-zVAP4);a6U+)Z87MOAlKvsX2>_xnE2@A+&hick5K2Ic5Wkz)`$ zCvOEV%mKa89~4Bfu3ie+gCijgzwQP)iFt7g_D}j;?Nx)nP5C^dxDmeDDkk_5*KZ;? zE@&h4Q%38!DpaXp-@j!f?zUV%50_jpk!mU1ez$evVR_KMT8`3rF5^(xZoU04JLSXT zcV*AxxILTJAmi_icH%J}oz)amZS5)BaXapAv3eoD`Sijt5yRq6)koEZe4YSx9!M82 zUEHccdD2JhN6_JMN7PHWhDUCmH{!|85IO8>_am`!?Q&a*%A1Gq0J?Y3^*#=3&T@#ng3dnaYRH6KV+!y(u>A(kdC$Ed zF!P@#FatjqNH-!@MF?kHw6L2?aT6OJ(sgISDtPJ(x^FL$n<_4Fej<5H1mI<4L7pjf zI}l%zt_PK7A4(>?Yw>WstaL`6F1JrTIUvOb#W{&bfg4ip`@@a}2H zTVf13zd-ZZ@mq0fTJPzV>k7h!D7Ga{-XQx+b4Cq6ya0WyuNEuX!?S&6^ULN?ZahhP z&qF1@jdD81)UI(*(l(ch8tt9DG7%*OC%;bVc}HjcuIh}RZF~3W_D?5-p=a+L_C4b+ z3fA3e!{`fZSta}}p6dzA>z|4Do4oGnM_~=Ovt^)MP|UsCxc4VKQe8jSz{5)(LMQ%9 zqy-ZNqvT0xS#^>n9wDQJ@xmx39=^q1ioFID+Sdb6dLhjH@%-{x3-$}2sUzbvm8yw? zaZxj?A3slv!>d(dAO6gc&RlKMkzsFH44MV&E*1#G8RGLSZQZ6dG~&?Po_s78ZH`di z-`@usIEKR)$cnLv;mQllRh>OVSTQfdxkTjloRHdvUvGbVa?k&esvaO1F98{@3#h-- zqFB^f`rMu_IAaR-wOLF==KqaziN}QA(~uzjNbLWgPAf+91FWq(i0A!?Gm_RaeyKEt z`0^~66Mn9Kgl|4Rkj|!5jw|d#v0aH~(UI>WjT3q>Zi^G;vgzjBRin4+4&Nhf< z&lTr5)f361)#5l(7v$SP9p`!8kCEK&SqSYyXIhpiRfwY7NbJaX$J)YG+c%^`?|8E* zMC6>_Pf^6xQ}_V#eP4^B+YH!6ZdunN{^sbjlKS+imtYDZvy$&(1<G>=*Cz)?ZZmgkkmR zAkrf6TuU}fu^$1%AJT`7ZPTrE-=8VDc#X#iIW#4QMNhOM0DfCO8VNsfn}iqq0YZLj6BYP# zWycyp@PC9TOK7v1^h$x&>?g6|vPoWU$3ypaOOa=5 z6_{(SQuL(`!R9Vot1?Qyl}w@!Qu*=6I;A+9t(t6A?$_Bmwj04!;rMp^b25u_o581HQ$1QYaU#7Kc|l^<{w!~XCh^MqYOV!6 z)uddHF9{kO;X$kXSWEW>RRyG}a&I7CNk!ESXxDHI({whYOm)?d1X!05PI$e2f|_f& zuMZ4jeQ%7cu6G|OvTSyYi$adlHl2>6KcK|@V$1C=OasE+)rUG=cA*4+9QZm$djn=+ zDZ-2V!JiAw5A&WCC7MgncInExi1q{B6Z#9y5l}N>x3dtSi3(U=b>~4TY~B zHRZw)md_%GhQEouT0BvWiFW{a1bp9>LI#Fpc^w+8%)*Ot<_eIR@B*Pe< z>oACn?0T=6Tb?k&#PqshH%1zLALMaX$}m4BcI#wUAzv8_11&ope-j169OyUsh(o}rm-YhJVhc1e-| zC-q=@?8nsn6rW1t!4QK_1sS%4qyD}q$?Q>-GFv_!Z@9aH)V0NbClMbs+Lb%^#F;3W zXPDzfx_|8(Y6iRXSMogcY%y#5{XT2%q-csVdZ03P?kU}^5c zV1DkvV1E6;zxRg0KdfAt(dQvHx+N4-Y-yV#V7Oq+lsH?Y<$wSZo#`4X6T*& ze~I4VM_<7?bkBEncJ{sn0I2-~2*WwscY^~1s^X%eK4=&i%36ww0k5wcAE|5Zl?=IYkoJ)g>w{ zD%#oG3kGD6yFkN#qDXKO1t>8EJ?eIODCw~pj!^E7dP@ff1)T!4UNlltRDh#XUQr=F zGCn@7uB@C@U0F$pPC)QE!1xFN%bv2avPw_q^56oYh3Gjw|9pd!k)9~f*2}A_!g6wU z4?o#}P~%MB5%Z3(^Ld^)w=_6i2mAZa0B?t2?=BUv8Zd{wIUbTPnCt+&I#h7rFP+?U zeme?hxfopwIRhYj?04Ww>BI_h<#*5bXZMkjkq}fk;mfiwZ;O~-?pt{Cog2ocAKRG^ z&Mxnf%{EJE00V3|ng&OSqeebZ8*bqI-iqa7*s5#ccCXzc4;`8aP;iRz@N~WdKwWT@ zv#F0U*_YaBY1c@IgxJ_vh_m+Pkxaorud>CmsV0GY<$H;;;sKRkdSYUOuiJgWzC3Q{ z<-AV21F--6IKpr7%sP*JvWu^TFw=mv?md-TX{w1m-@YqkkAnd=%}utCIE{*)Cxg*Y$VFtvPid)F%AkUl#)v{}GPvsKZEmRfr*G3(9lJ zciG8KuhSCTiaN)BNs=ovnyW_BlaHXXhSoUU;;UP`w^8@KD^bf@&=+(1K_1*|+vRu7 zM<2wI;cUZPH7a$L3lKHO+}EQdRCe9w5c{9-RdYHSG}?mE7l({)QXE0ne+u4<_=T2F zGXn3~PnLaT@WX9e9(6)2w{L?Ds<2%Z*q2Ph!tHoV)G#3n&e22X-3txzoKZA_RIlHz zjlg}#L%f&UEUi zzNW)P4b@I;hCLU$JNXRpEOQ=K)67E7{XKB?$jlL1>6VG;@WO*F?Tw~v6bM+)VXCva zS=&&j#f|Rd1w9=qoN-q)%wpLGeK#Pc+TxBDo{lbOiV1(qW6D;pxx#)NfDT693jaO4 zT);hrej)W>$`Sf_fBn$b@HWh;)bIJBv4-1|*RV9^_o3-Bq$ehk)o6GT+2Y8}jwA5i za5UBFJYj%No#T$X+I#o-#NI$U{V1X>MQ{I!p-(P!FsS~q0RPKh4|}l9Oe?29S1fV{ zfa|}PsqKD((AmN6%|!_>Rn|US4&RXqIUyFtG$`PUdth>$Jg4yuh;=Z_nT6+YJYE&x zLy{KPe#!2>!5}K~GRi_ak3S#I`{{T7=W^J5$-^U7JmH|c(N6l&>r&4;lU(UO`y2;;dTz| zf45Z|6S7rb1fL&48VKH%eEMI!({C=%_j62#oK3#Wk8Y2+jl`Y)x{dYW>7^b~9#&Ch z<=^M191;8}v{{D0>&0Awrm^98;c^mP3x6ukdf&iJMM z_V#|3!2jGJHg@)1z_76kJ)lnbjnm%AE{JQmWyj5hkf>1GvNs=xm6i49*qC%zK>2DA zkn^!B{Er!~Jh{7T45;J^>gp81Oab??zMyS0xs7yd(TEL&j-iCU3Gph#I0#1*inXYx zC$HKJn~4mdEP~G%*5cDCFe_#TriDb~CIB!-w211U%q7(Dp4_cc@*2L%)N{g9CFeoc{{J$pk#qubnG0HL~ni`Vjsx}79^x< zPE#8V==j8?6ij#ih!e@bHTV`3aTHx&HUAI*B6xsVj?m1_ZZm=H&VTH+*392{@?Ojtxhgzoz{~2>gp8X70Q7dUUeNps^l=XXrmD9Iucj=`&2dA z>gtU}1qJY-gC%qfp9|1k;1M#gu_*Hyn0g~R-i7zSjRQe_CVb`AF+iNBwjk2PF6D#& zXhXxTp^?$QxKSy7Ee5=fuN_0@4S?BPOIw=EU^K0EUlTV`haSHL2U zN~WF_%|`pM_joEMC@+9fR!c27{R#7Vij}4#6Ju`bmca zE4S-3zzQCvZl%~V`BN(rwxrZN>Z~SDt;M7p^W_?H)A=VSN;urifBXIK{ujCKs;NsY z52I3-qmz7Gw1Y)bam=%#n6Yg2VzVY<=R=#=49n1Bg=A!K;)>{^g4{s?Arv4@K}GZd z2+1qC;)pwnZeI7ANt??SF^5a{UMEx=J`-M({>^NGzp?FyURzH!Y`71c)I#H2S`WD) z1FVWKf_`B2+=}v-!5n#mNh&P$4qN=d5yal0AmQTTQgY@`*j6W63+G=jnClnvSZTn? zHTt)4OkpQgY)tP#%{24%;x_yO0bN~P6UV=phE`U%!bs4D6t@O+vr345JQ0?^Fu9Sg zIOe~2C3GEw7jRE18?_c*j+irCo(U?1%Zg&;*dRnl)m1lhhUz1U?l45`Q6o&5r2zf-IK` z1GxeTh5fwC`P0Jp)cN&UgGl};*;5vQ{3Q2M1@FWoi-gvVmf9~+#7Xn3muX^0;-J4{ z;;f1aOU43XSh*LW`zi(+@Z40{Vx$>-QN6-E{h?$vhsWMr)ROQ>|8`UDB+{gCb1*RIQVS5?8<8S$&Y0 zPlEt~e0|(SLSffxGBOmHE^dwdjHuzb`1v8%xSG3p!}x$|WNk&4nD!#Z$hssPa<~aC zV>eD@3Dvmny|pad%CCRAhV-^XKy=mN3H}Iak@B|+E=g9jQM(s{Er(pFOc$g4##Z!R zeWxKtCn5id{0!W3AhVA6sw3ox^~n;y*3xuOLc;W^Y|xYMsUmhIbde?UdHg2AMW;@I zby1LRcKhpkboXcSY=SRAYB}sEIa{*WL)Z;oGf@UKCV}-BQN-rAkjD!B3YI!nS03c# zsM=ky)^0NT9sHK}oA0>o0k>&|OJUGi^{yP!0R~-fpd%j~?X&_U0&Rui5$bzlv8|Y0jF55u zh+j<Uwm_%rapP}fV^|ba`5IZ;n9dlxb(8S#RQ>NHl^6&7n=BRt2>0eJ#IWn6+ z{cz8M^^2I-yxD<+y_66}42!VcBuSd2RCheqy*^KCEg(=eutMpW?CBXN?C#?CdDfcz zp+c&b%UNH&dpsMYY_j(UgRfi5r?1_`N@6V(G1X2ZcoqjIDoxOTp|gW#iHqCMz{KQM zE;Uqa7qfNVGB!$3A!pI1+3k#_Jvb_`5;@z6qZmXq~4!dxEq_LcsiUKu%n+Fz!V%O_vL zsL~Jt)ZsMIv1B?XIgL9N#JvYxizhN>?{(UiI@`@!X;s2?1fmF*=5zaxKjB%I zih47h9G>B+jmiok^92Nl-GObvU9|p6Sl#!oM zq^nTX$+>62s;F{y}m+?d^$4ZtSV~MNrj6a`8+q&uJ86m9=(sR@50eLw?ULsg68R zZi8Q*Z@X!ua~{vFL1HG?I0ARt{zb_tKnaL{Xk7n;Udp8*K{VFGeX3~w7pz)U!EgJR z>V6GVH^~@RPwTy1d!jtg<_e_&fr&obBGt9%DDCejgR5~)uC;cMy^bM#%ilD^1oh{3 z1@+;N31D&@qjJtz{JVu~tGcSCO-b9BP+#4NvIwX0!r9nsqU(B4F7HSrjD9c1iy`*nxpIOIyjKL?quE>5Y(qEuylxt2OZ)xgH6k>B0(r}dSX;TikDT^| zC3ShSay|O%dwLF46v2U+JoH~5u^fEdMBuF>>{~&iMtefDOLT3keY0vB$8S5Y4Ylygg zcIxjs^Ek%q=f2@qxa6tFo7HQ-ilFYLXSSMEe$)3&bK|9$nT+WD=KU+i%u#duz z>;Kfup7yH{8t9`wOJKSi(!rx%^>B)-Jf)wXp7fe7xo3M@6qLh`O0BS#xlLUYO>m{f zuF$Of75YMcss_vo)*nhV1*ucHMgQGdA@3Q7SI@55T82}}Zx)ROds1%GAuT%Ge*C!< zXI(!|QX)1UoM!4eQC+hwJ8(J>iC;!&&#&6RTt+y`Homn%edtx{gtR(?M~F?4*00VI z`b-#dUuylO)b{6$K^0uJ_-6Xy=xjrnH4-~n?J>6;wK{~Oy2MdbSc6`ohU}rL9{+2O zd!N4|)z{!a5so&1j+~{rskNx2S`Mb7palC~E*E4?psn;&3~$y9RndV?eSq9>Rm~3ls4W$tt#xFXmhm- znehB0Ltb#-jZ3k59|vC95Nr5o@~Zg#Ug0;nkac=A&7rFV7r#Ru+mnB8TMg_i-|yP& zc!`mk7>*3xqWf@-`bD}7^B-hd6#6X!XeOT=BJ)_^H7Ud z^N!@;J^QrQRdIB6{p|>5=9^vq@Jf3-aq~IdJ#B0`^-@BmslBk79_DB3l{d{JJ}oIT zWOHke+j*5NF?l&VVs-r0Z3_#9Y7r7Iq|IJpLp`Cum2{9++go+MNQ0<@p3OT z@9uGsKKC^FJEgqxq@_^WykioyML)Wp&(z`=|Cs36=}WOZ!D_Y^P10-Unp_SW>6I|^ zA=&A6A*~L`nbzt}B&I5R8V&m(R>!!@JcnZNt=0j0tsG!Bb6;*SLri1TdsfCIlFjGe zupLYJ^y%M8GEQs&zH7TeqP$Q?+tg)rl-dwBnLb8fT!c&@J�?lA5YRqmW0$;`i$D zuHFiyZ*c`~<5gBv(8PE6f$7*E2=39;op5gx`l3vtvYX5mOa0gzie2Le!WcX*!R(gn z$Q8uIsEvPDs9j4Z-hQPs#Zmn=S0CqvXp2-zgQU?2DlgCA1n9mISG`AnLb(5-Z`bLT z2CAd-U~`*Fmb1PBfvGmzW%TJ^cXV0d5$qRb>w%aBav3kc+WpQX2`zv_9ZDKO~u zy9PXXej~(WLCL({54Iv%KaAbP*^HQ~t(N8Z(pw~yds|&}{)V3zW zS#X~3#f2u9!cRYZAmbdeI2F*=%M2ZO&VOT^>V6`VCXG$C zJ>av`JSi8q5o7)K<|k~js>FKaUBv#IKZ`F^+0txhG4; zXfd%u>M`ROB^Nzntw0&aVQKU)2akbXt19XQF>$;$WM)i?$AOEsChN^atE&g#O5bYi z9lYQcsEbOzg0VUEg81LTJV_BCEyg$d_hbZnAi0$v3;1m*NDPSLv#=1?nMrjw4!}!b zCd%y{$OIL>!DCzy(<6$QM14BFvhUjityoU77Lhy-|7kpmUp~I!lde&a82#;89w!jC zW)9iO55f{SIAzBNv4YaVRIq<@jS0_WnMG#(p|5+_5*{|mWPD^{LyY3*M=H$58DnrdWO%j z+ZQUEuXINu`4P>j++oQfKKYpW+S+YdpA}2VtSYc2LIb0i0=25lr+=FWRVn-=)sE9e z3K<4h)#=YJ&GIJeuZsD-nU9}3#CjxXdiWoiz(Zqh&|oo`(95#2ORk*`hMpK^d&wK; zPn!wMQaPn#kC|{n%kIKtu;Oh-T%VUiy5*_wfI>85$G1y#6P{f(y;n7CF_Hm@wc4-q zH_o!n$AM16`~w8(^>y$ZKRa0F|1@vv{K&VW?;1bgTj@^jhuLh#+jTnn3yN#dbmr}< zisKokHEx?-le5t)+a7;IV1_!HM(CnHJ$9eB&wS_=XHhfVFX(Z8KK_*ATmNC_vZU@I zvA_$oc!2HQOpI+Q(v8mUmL{IEEpcWQ@h{l0o#<2^P(G$u!tM}+2SuM zy3o%fQ|%V8#rVp+b;;6xtw6%_#6g*uF`14nR-&Ni;cz+prnpZ%9a(MMG8}0WYzXxc}y<<8AOi8_MWPBdE zD@G%gej1juUuF(X9MlGc$ps&5sT%5xKgxg9S60;U5BlMV=Bb#;O-_!TT93#IAW}?2 zdLc+OkWqkj^B>J9BGBLwPxpHcv97mGNHLIqFye7FUVVp+m{fN_i15`wVrCu?O?uCV zNQy5;5!xR_95ieSMP9(jmY8^rkba9&7Biu}wVbfxoBm}b?_ARSh`TJ= z#xvYRu17eIW;khYByu0Ffsjk(nX%PzeYAhXZ{OoPMrQ68fgGQ}bFryIWQ<~RSFxa5 z{wEXo5wUM;O}}s$V^To5$?swGz>~YK1?wFnr!0X#ZTqX_&OXL>viJa^{TsOiqKq=f zWz4wYIa!zC*WbKj-iWmW^5|lbq7PhCMm#U(QWDgb@9rBx4WV zB9Bkle$6G4ic11M&}6ga7=oto>3)!y7uq~*{hmq<1lb(&KzhqF5HrFmhT{x>wm8)4UT zQ~6{!jyKQ^xLn!vlFEKu)M6;X|1&mLDu`hG?xH6or#L8&rW-&Ur#0zS{AX}%9jvob!aGI- zjo!1H%#tT<6^i-p*TIcdI9)Rb;NYjWMqMgk3aL1e<}|IwJ^o(@sKIn{?+Z+4&voj} z%)E~ZtsOQ~3Prd8=d{pRa*aQu)GQW7Ap9P|6yau7R8nQazip;_2C0ADLdK+rQh=VW8c5+;%^{4qiy5tF5okNGQ!%M@MgdbXJQ z9rhS#7?-3JCnhHc1I$y+)%JeoHZ(VlF+=pfk}!0Dm!6rK`Fuj;9ZX5MCnq(PIvS0A z!WiS5(=QHai79NdPepiuNA8;-07lK=acA@wi79n(%cX?ezq*PK34u~=lt)adTba4J zyBoMzYb~g(MC3lr=%u;|&nDIEiJ-e+#2kl*6_C&0o z@9qWx&G1L<9v`mbb5CH#NR0J`!TPJ$o#hPka5oTfK5WiBlX?3p_}$q#`2aV~dt!3Z zX@3MA$Q=~|kh25?1T%AUzkqZxQ;@2fnvtz-jDEM*_3cRb0e)J_3IjGi{^(^p8Wt85 zDJdy%VHgw?ly8PW?07nIGZ1XP*Er3|4Fv=|5=P@Pna^`%7koAP1+|h;=$t>>>+gSe zG7rn={V+hA?-K+x)IZFXUeGY^$&^t4w_Q-!k$~9|ulI`=P_`I96|!b5^5Z%3e9rly zy}lH@%r(t3^%@~pLkT)%Jw1?S$@=$1yezC~7oNg$|=)C7Q$^Il0$=dn@5&}nkKM8M0HC4#Ic^&m2r1<2HilPL9LT@Fk_85 zhOIj%Yf;i8@jEvw!TfpCC9jad(qArn=2(-ZbXo09&gHSWk*}nsf0Ely81>U)R;q`9 zyAK^3o4WG=4i#0x$A>qr!me*GpU#?Q>I{n2_S+3C-3{b1a3eGbXA0G^qilTzdwBzH zOG*7YarI(_j2=8}XBZf2@q|YSZNU$ks$v)=$Ua3xiNIVKQHnoaP*+jR5|@66@8)MJ%A(sLNu794HJ=)z8HL7rOVc6l(k!n0+hoH>?&>4Cn{0hzP+% zeQ7nRZMEM>>TU??&dVd&Y9(lDYK^OwO5wt`%Sy1t~%WV1O4qRLVmB8{&oewy3;1?D=tHrjs+E4H$+0Uq-1VF4^=*uMbh}l>^^c(%zvIi2?=Oh z%9K~s3fOe|HI=542_aBq;Kv`4e+Ue)m2M4cQb7A7-4+zal1R+pl3>pt#XECiyBZLo z|9P`wdP0K|XBS2k$d4bMXNuamQh}G)vxdoK4jY%Rrec3+kqfW`T@{s2U@|h;rurvvVxVf1Z`&h!>fT0hc zQO96ctSS)}$AOQ}r!{^-Ls-bVZU&J>;8L2V9{6t>ZFt{v%l%bRS#_OrR-s_dK+K*z zS&JY$w+TJIhoMTAWA1_~f%Mi6lL5`6 z=eGw(y?FXK%ie!YKA6IBz1=^82n8oO_`le8aKOj9(-!PZi7rUU37TY)5Zn^aCir6A zTXmGj8_6*#$N#^-6HPe1UFG@Kd5}$hu3}4Xre1r@#kYa?X*0!e41R)N?;FOOkQu- ztBOTU*f0qiLgh=J3xf;gga`+-V5~0q=f^7?)qL}iFkP0YoQ?7rO$I{q#z+5nnbEj} zr6*Avn23DD;Tz|TP5S$@#hJSi)Og5Y3fEd;^;3T6bf5H|4A3T`gvjEKV?-3v)Q%zc zJH>JRH0=C%kVN6n0*)m2Ps`MompLUE51(bXYOh;YLx-RH8BBv2zAw|i@?9)s%m zUlN2F`!e5OE!^Dzcw@_T|A3Q@fSpBul&;L-nN{U~szm>X-3)03tXZz2=s?gTI`>m< zoMA~TY0}dv)~)wFNcTlca)u~&DBAp3vOCDDb|Yewz3q$X7!QvqU&N`QoZPbObQH&cqBvYuS}Qz6y~;j*saH~_t{5L|)c^O?}TkVNzzbcXm5Mk{{?9yE8? z2=RIJDSHd%pg>p&wa{zkBG$jMyR)7&>hVG7$3ri`!dI%{u4Xrqb7;iJ%eV|!7i4kME#TH$fw_0l+oK{EPv2~6siR8q*9gb-#sBA zA&<|_L^L#T0lQyA(MwZPQyswQq2jp`u4v?Lm?jmMNz6FQ`#b>)@*L-RuR(emZHL*7rbTWUBCy`QLLIBZB$H<*Zq77nzKd&kFg z&RuVASQ`S-U5?JC;h9PCyGbj;Aq3OAuxNzJ4Av=eF~ePyET)Fb1ymEvfITO|ExB(L ziJgLQZ+G^otyrMs3^L1v4ZBfZcF3g>Uhx}oeC%aqftMGwIsPX=&!TnMnfBSkV+}u3 zBPIc-IJ^n*8SYzdxS-u&4XS17AXOWcb|3#cl&$fkA-)|#BhQegIS=W)%LxlEt|*B? zAS`+dlsN8>cW*`Xb{Je)^F9e@(Ir^ci(>&J_bsCEqB}#pdejaM)=_Um4;8{&26Mxc zK(RLa4mhr*>V)&M&{ukw@kCxkjX3$RqbVUh(wY7Ks9v#HsTA-SiibVHzxor*8bcv` zQI4*zViDK2rh055K}+&f9u?$FmR}%HKlQuH^wyvr=fg^fDEUK)+xW+$Ym5AukHpWg z<44|O8y?wiYi}o)G=RrXmqT+m$287SL*Hv42}~&>;_w-rW+*Kzq~KYyn48{XHeaH{ z$1`_3R0w$R7!!bnpWt3^HVE`avDtRugl_qqe+_8~)EFBgGRwK*^cBW`e(mh+?Dw9g zqk1_*&q-~w>xOf;P20)oqtdKC9YO_}ji>QpvLPWghKIxQE`(FeT977-%Tid96T@X3g(A%L?bGj%V@EMBu-{E z3s-5mdo%ie<-{DV=Pe=__uZc*vg{0Gzi)W*x#NM_+;cen^>x&>vZRaZ|K0|&f%TLh zg+HBS7f~Dn{yr!8Ub|#saQ`{Qwq)WjGI&lV2mR&f0LQ?Jk(4fLopfFGPgt bQvvWTubyVRu}fkwV3QJ)7p)OC4EnzS;1>nw literal 47001 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 diff --git a/source/app/javascripts/adhoc.js b/source/app/javascripts/adhoc.js index 125a369..b1cc518 100644 --- a/source/app/javascripts/adhoc.js +++ b/source/app/javascripts/adhoc.js @@ -142,9 +142,7 @@ var AdHoc = (function () { try { // Click event - $('#adhoc .bottom .finish').click( - self.close() - ); + $('#adhoc .bottom .finish').click(self.close); } catch(e) { Console.error('AdHoc.launch', e); } diff --git a/source/app/javascripts/anonymous.js b/source/app/javascripts/anonymous.js index dac4971..d472b80 100644 --- a/source/app/javascripts/anonymous.js +++ b/source/app/javascripts/anonymous.js @@ -20,6 +20,28 @@ var Anonymous = (function () { var self = {}; + /** + * Registers connection handlers + * @private + * @param {object} con + * @return {undefined} + */ + self._registerHandlers = function(con) { + + try { + con.registerHandler('message', Message.handle); + con.registerHandler('presence', Presence.handle); + con.registerHandler('iq', IQ.handle); + con.registerHandler('onconnect', self.connected); + con.registerHandler('onerror', Errors.handle); + con.registerHandler('ondisconnect', self.disconnected); + } catch(e) { + Console.error('Anonymous._registerHandlers', e); + } + + }; + + /** * Connected to an anonymous session * @public @@ -115,12 +137,7 @@ var Anonymous = (function () { } // And we handle everything that happen - con.registerHandler('message', Message.handle); - con.registerHandler('presence', Presence.handle); - con.registerHandler('iq', IQ.handle); - con.registerHandler('onconnect', self.connected); - con.registerHandler('onerror', Errors.handle); - con.registerHandler('ondisconnect', self.disconnected); + self._registerHandlers(con); // We set the anonymous connection parameters oArgs = {}; @@ -165,10 +182,13 @@ var Anonymous = (function () { Interface.showGeneralWait(); // Get the vars - if(XMPPLinks.links_var.r) + if(XMPPLinks.links_var.r) { ANONYMOUS_ROOM = XMPPLinks.links_var.r; - if(XMPPLinks.links_var.n) + } + + if(XMPPLinks.links_var.n) { ANONYMOUS_NICK = XMPPLinks.links_var.n; + } // Fire the login action self.login(HOST_ANONYMOUS); diff --git a/source/app/javascripts/attention.js b/source/app/javascripts/attention.js new file mode 100644 index 0000000..abcd44b --- /dev/null +++ b/source/app/javascripts/attention.js @@ -0,0 +1,216 @@ +/* + +Jappix - An open social platform +Implementation of XEP-0224: Attention + +------------------------------------------------- + +License: AGPL +Author: Valérian Saliou + +*/ + +// Bundle +var Attention = (function () { + + /** + * Alias of this + * @private + */ + var self = {}; + + + /** + * Displays attention message + * @private + * @param {string} xid + * @param {string} body + * @return {undefined} + */ + self._display = function(xid, body, mode) { + + try { + var name = Name.getBuddy(xid).htmlEnc(); + var hash = hex_md5(xid); + + // Compute some variables + var message = Common._e(Common.printf("You requested %s's attention to the conversation", name)); + + if(mode == 'him') { + message = Common._e(Common.printf("%s requested your attention to the conversation", name)); + } + + if(body) { + message += ' (' + body + ')'; + } + + // Display notification + Message.display( + 'chat', + xid, + hash, + name, + message, + DateUtils.getCompleteTime(), + DateUtils.getTimeStamp(), + 'system-message', + true, + undefined, + mode + ); + + // Add a marker to displayed message + $('#' + hash + ' .content .one-line.system-message:last').addClass('attention-notice'); + } catch(e) { + Console.error('Attention._display', e); + } + + }; + + + /** + * Sends attention stanza + * @private + * @param {string} xid + * @param {string} body + * @return {object} + */ + self._stanza = function(xid, body) { + + try { + var message = new JSJaCMessage(); + message.setType('headline'); + message.setTo(xid); + + if(body) { + message.setBody(body); + } + + // Attention node + message.appendNode('attention', { + 'xmlns': NS_URN_ATTENTION + }); + + con.send(message); + + return message; + } catch(e) { + Console.error('Attention._stanza', e); + } + + }; + + + /** + * Returns whether last attention message exists or not + * @private + * @param {string} xid + * @return {boolean} + */ + self._lastExists = function(xid, mode) { + + var last_exists = false; + + try { + var line_sel = $('#' + hex_md5(xid) + ' .content .one-line[data-mode="' + mode + '"]:last'); + last_exists = line_sel.is('.system-message.attention-notice') ? true : false; + } catch(e) { + Console.error('Attention._lastExists', e); + } finally { + return last_exists; + } + + }; + + + /** + * Return whether entity supports attention notifications + * @public + * @param {string} xid + * @return {boolean} + */ + self.hasSupport = function(xid) { + + var has_support = false; + + try { + has_support = true ? $('#' + hex_md5(xid)).attr('data-attention') == 'true' : false; + } catch(e) { + Console.error('Attention.hasSupport', e); + } finally { + return has_support; + } + + }; + + + /** + * Send an attention message + * @public + * @param {string} xid + * @param {string} body + * @return {undefined} + */ + self.send = function(xid, body) { + + try { + var mode = 'me'; + + // Don't send attention message twice + if(self._lastExists(xid, mode) === false) { + // Send message stanza + self._stanza(xid, body); + + // Display attention notification + self._display(xid, body, mode); + } else { + Console.debug('Attention.send', 'Not sending attention message to: ' + xid + ' because already sent.'); + } + } catch(e) { + Console.error('Attention.send', e); + } + + }; + + + /** + * Receive an attention notification + * @public + * @param {string} xid + * @return {undefined} + */ + self.receive = function(xid, body) { + + try { + var mode = 'him'; + var hash = hex_md5(xid); + + // Don't receive attention message twice + if((self._lastExists(xid, mode) === false) && Common.exists('#' + hash)) { + // Display attention notification + self._display(xid, body, mode); + + // Show a notification + Interface.messageNotify(hash, 'personal'); + Audio.play('catch-attention'); + + Board.quick( + xid, + 'chat', + Common._e("Attention to conversation requested."), + Name.getBuddy(xid) + ); + } + } catch(e) { + Console.error('Attention.receive', e); + } + + }; + + + /** + * Return class scope + */ + return self; + +})(); \ No newline at end of file diff --git a/source/app/javascripts/audio.js b/source/app/javascripts/audio.js index cc1fe51..fa7abc4 100644 --- a/source/app/javascripts/audio.js +++ b/source/app/javascripts/audio.js @@ -29,7 +29,7 @@ var Audio = (function () { * @private * @return {boolean} */ - self._is_supported = function() { + self._isSupported = function() { is_supported = true; @@ -38,7 +38,7 @@ var Audio = (function () { is_supported = false; } } catch(e) { - Console.error('Audio._is_supported', e); + Console.error('Audio._isSupported', e); } finally { return is_supported; } @@ -46,6 +46,57 @@ var Audio = (function () { }; + /** + * Append audio DOM code + * @private + * @return {undefined} + */ + self._appendDOM = function() { + + try { + // If the audio elements aren't yet in the DOM + if(!Common.exists('#audio')) { + $('body').append( + '
' + + '' + + + '' + + + '' + + + '' + + + '' + + + '' + + '
' + ); + } + } catch(e) { + Console.error('Audio._appendDOM', e); + } + + }; + + /** * Plays the given sound ID * @public @@ -58,43 +109,13 @@ var Audio = (function () { repeat = (typeof repeat === 'boolean') ? repeat : false; // Not supported? - if(!self._is_supported()) { + if(!self._isSupported()) { return false; } // If the sounds are enabled if(DataStore.getDB(Connection.desktop_hash, 'options', 'sounds') === '1') { - // If the audio elements aren't yet in the DOM - if(!Common.exists('#audio')) { - $('body').append( - '
' + - '' + - - '' + - - '' + - - '' + - - '' + - '
' - ); - } + self._appendDOM(); // We play the target sound var audio_raw_sel = $('#audio audio').filter('#' + name); @@ -146,7 +167,7 @@ var Audio = (function () { try { // Not supported? - if(!self._is_supported()) { + if(!self._isSupported()) { return false; } diff --git a/source/app/javascripts/autocompletion.js b/source/app/javascripts/autocompletion.js index 1b28e02..0fa63fc 100644 --- a/source/app/javascripts/autocompletion.js +++ b/source/app/javascripts/autocompletion.js @@ -32,14 +32,17 @@ var Autocompletion = (function () { try { // Put the two strings into lower case - var sA = a[0].toLowerCase(); - var sB = b[0].toLowerCase(); + var sort_a = a[0].toLowerCase(); + var sort_b = b[0].toLowerCase(); // Process the sort - if(sA > sB) + if(sort_a > sort_b) { return 1; - if(sA < sB) + } + + if(sort_a < sort_b) { return -1; + } } catch(e) { Console.error('Autocompletion.caseInsensitiveSort', e); } @@ -49,40 +52,51 @@ var Autocompletion = (function () { /** * Split a query into its subqueries ready to be used in autocompletion - * The function return an array containing two others : the first with subqueries - * and the second with remaining parts - * For example, if query is "A B C", the subqueries are ["C", "B C", "A B C"] and - * the remaining parts are ["A B ", "A ", ""] + * @public * @param {string} query - * @return {Array} + * @return {object} */ self.getSubQueries = function(query) { - var subqueries = []; - var remnants = []; + var result = []; - var queryLastCharPos = query.length - 1; - var spaceCounter = 0; - for (var i=queryLastCharPos; i>=0; i--) { - // Search from the end of the query - var iChar = query.charAt(i); - if (spaceCounter === 0 && iChar.search(/\s/) === 0) { - // the first "local" space was found - // add the subquery and its remnant to results - subqueries.push(query.slice(i+1)); - remnants.push(query.slice(0, i+1)); - spaceCounter++; - } else { - spaceCounter = 0; + try { + var subqueries = []; + var remnants = []; + + var query_last_char_pos = query.length - 1; + var space_counter = 0; + var cur_char; + + for(var i = query_last_char_pos; i >= 0; i--) { + // Search from the end of the query + cur_char = query.charAt(i); + + if(space_counter === 0 && cur_char.search(/\s/) === 0) { + // The first "local" space was found + // Add the subquery and its remnant to results + subqueries.push(query.slice(i+1)); + remnants.push(query.slice(0, i+1)); + + space_counter++; + } else { + space_counter = 0; + } } - } - if (spaceCounter === 0) { - // If the first char of the query is not a space, add the full query to results - subqueries.push(query); - remnants.push(""); + + if(space_counter === 0) { + // If the first char of the query is not a space, add the full query to results + subqueries.push(query); + remnants.push(''); + } + + result = [subqueries, remnants]; + } catch(e) { + Console.error('Autocompletion.getSubQueries', e); + } finally { + return result; } - return [subqueries, remnants]; }; @@ -102,25 +116,37 @@ var Autocompletion = (function () { try { // Replace forbidden characters in regex query = Common.escapeRegex(query); + // Build an array of regex to use - var queryRegExp = []; - for (i = 0; i » '; + + break; + + // Network error + case 2: + text = Common._e("Jappix has been interrupted by a network issue, a bug or bad login (check that you entered the right credentials), sorry for the inconvenience."); + + break; + + // List retrieving error + case 3: + text = Common._e("The element list on this server could not be obtained!"); + + break; + + // Attaching error + case 4: + text = Common.printf(Common._e("An error occured while uploading your file: maybe it is too big (%s maximum) or forbidden!"), JAPPIX_MAX_UPLOAD); + + break; + } + } catch(e) { + Console.error('Board._generateBoardError', e); + } finally { + return text; + } + + }; + + + /** + * Attaches board events + * @private + * @param {object} board_sel + * @return {undefined} + */ + self._attachEvents = function(board_sel) { + + try { + board_sel.click(function() { + self.closeThis(this); + }); + + board_sel.oneTime('5s', function() { + self.closeThis(this); + }); + + board_sel.slideDown(); + } catch(e) { + Console.error('Board._attachEvents', e); + } + + }; + + /** * Creates a board panel * @public @@ -35,93 +166,25 @@ var Board = (function () { // Info if(type == 'info') { - switch(id) { - // Password change - case 1: - text = Common._e("Your password has been changed, now you can connect to your account with your new login data."); - - break; - - // Account deletion - case 2: - text = Common._e("Your XMPP account has been removed, bye!"); - - break; - - // Account logout - case 3: - text = Common._e("You have been logged out of your XMPP account, have a nice day!"); - - break; - - // Groupchat join - case 4: - text = Common._e("The room you tried to join doesn't seem to exist."); - - break; - - // Groupchat removal - case 5: - text = Common._e("The groupchat has been removed."); - - break; - - // Non-existant groupchat user - case 6: - text = Common._e("The user that you want to reach is not present in the room."); - - break; - } - } - - // Error - else { - switch(id) { - // Custom error - case 1: - text = '' + Common._e("Error") + ' » '; - - break; - - // Network error - case 2: - text = Common._e("Jappix has been interrupted by a network issue, a bug or bad login (check that you entered the right credentials), sorry for the inconvenience."); - - break; - - // List retrieving error - case 3: - text = Common._e("The element list on this server could not be obtained!"); - - break; - - // Attaching error - case 4: - text = Common.printf(Common._e("An error occured while uploading your file: maybe it is too big (%s maximum) or forbidden!"), JAPPIX_MAX_UPLOAD); - - break; - } + text = self._generateBoardInfo(id); + } else { + text = self._generateBoardError(id); } // No text? - if(!text) + if(!text) { return false; + } // Append the content - $('#board').append('
' + text + '
'); + $('#board').append( + '
' + text + '
' + ); // Events (click and auto-hide) - $('#board .one-board.' + type + '[data-id="' + id + '"]') - - .click(function() { - self.closeThis(this); - }) - - .oneTime('5s', function() { - self.closeThis(this); - }) - - .slideDown(); + self._attachEvents( + $('#board .one-board.' + type + '[data-id="' + id + '"]') + ); return true; } catch(e) { @@ -235,7 +298,7 @@ var Board = (function () { try { // Cannot process? - if(Common.isFocused() || !content || !(window.webkitNotifications || window.Notification)) { + if(Common.isFocused() || !content || !self.NOTIFICATION) { return; } @@ -248,11 +311,13 @@ var Board = (function () { var avatar_xml = Common.XMLFromString( DataStore.getPersistent('global', 'avatar', xid) ); + var avatar_type = $(avatar_xml).find('type').text() || 'image/png'; var avatar_binval = $(avatar_xml).find('binval').text(); - if(avatar_binval && avatar_type) + if(avatar_binval && avatar_type) { icon = 'data:' + avatar_type + ';base64,' + avatar_binval; + } } } @@ -261,8 +326,17 @@ var Board = (function () { title = Common._e("New event!"); } - // Click callback - var cb_click_fn = function() { + // Create notification + var notification = new self.NOTIFICATION(title, { + dir: 'auto', + lang: '', + body: content, + tag: type, + icon: icon + }); + + // Click event + notification.onclick = function() { // Click action? switch(type) { case 'chat': @@ -281,53 +355,17 @@ var Board = (function () { window.focus(); // Remove notification - this.cancel(); + this.close(); }; - - // Check for notification permission - try { - if(Notification.permission == 'granted' || Notification.permission === undefined) { - var notification = new Notification(title, { - dir: 'auto', - lang: '', - body: content, - tag: type, - icon: icon - }); - notification.onclick = cb_click_fn; + // Show event + notification.onshow = function() { + setTimeout(function() { + notification.close(); + }, 10000); + }; - setTimeout(function() { - notification.close(); - }, 10000); - - if(notification.permission == 'granted') { - return notification; - } - } - } catch(_e) { - if(window.webkitNotifications.checkPermission() === 0) { - // Create notification - var notification = window.webkitNotifications.createNotification(icon, title, content); - - // Auto-hide after a while - notification.ondisplay = function(event) { - setTimeout(function() { - event.currentTarget.cancel(); - }, 10000); - }; - - // Click event - notification.onclick = cb_click_fn; - - // Show notification - notification.show(); - - return notification; - } - } - - return null; + return notification; } catch(e) { Console.error('Board.quick', e); } @@ -343,21 +381,7 @@ var Board = (function () { self.quickPermission = function() { try { - try { - // W3C Notification API (still a draft!) - if(Notification.permission !== 'granted') { - // Ask for permission - Notification.requestPermission(); - } - } catch (_e) { - // WebKit Notification API (fallback) - if(!window.webkitNotifications || (window.webkitNotifications.checkPermission() === 0)) { - return; - } - - // Ask for permission - window.webkitNotifications.requestPermission(); - } + self.NOTIFICATION.requestPermission(); } catch(e) { Console.error('Board.quickPermission', e); } @@ -376,8 +400,9 @@ var Board = (function () { // Fires quickPermission() on document click $(document).click(function() { // Ask for permission to use quick boards - if((typeof con != 'undefined') && con.connected()) + if((typeof con != 'undefined') && con.connected()) { self.quickPermission(); + } }); } catch(e) { Console.error('Board.launch', e); diff --git a/source/app/javascripts/bubble.js b/source/app/javascripts/bubble.js index f70d3d9..01ba13a 100644 --- a/source/app/javascripts/bubble.js +++ b/source/app/javascripts/bubble.js @@ -31,6 +31,7 @@ var Bubble = (function () { // Destroy all the elements $('.bubble.hidable:visible').hide(); $('.bubble.removable').remove(); + $('body').off('click'); } catch(e) { Console.error('Bubble.close', e); @@ -53,8 +54,9 @@ var Bubble = (function () { // Hidable bubbles special things if($(selector).is('.hidable')) { // This bubble is yet displayed? So abort! - if($(selector).is(':visible')) + if($(selector).is(':visible')) { return self.close(); + } // Close all the bubbles self.close(); @@ -66,8 +68,9 @@ var Bubble = (function () { // Removable bubbles special things else { // This bubble is yet added? So abort! - if(Common.exists(selector)) + if(Common.exists(selector)) { return self.close(); + } // Close all the bubbles self.close(); @@ -78,8 +81,9 @@ var Bubble = (function () { var target = evt.target; // If this is a click away from a bubble - if(!$(target).parents('.ibubble').size()) + if(!$(target).parents('.ibubble').size()) { self.close(); + } }); } catch(e) { Console.error('Bubble.show', e); diff --git a/source/app/javascripts/call.js b/source/app/javascripts/call.js new file mode 100644 index 0000000..df875d2 --- /dev/null +++ b/source/app/javascripts/call.js @@ -0,0 +1,972 @@ +/* + +Jappix - An open social platform +These are the call common management functions + +------------------------------------------------- + +License: AGPL +Author: Valérian Saliou + +*/ + +// Bundle +var Call = (function() { + + /** + * Alias of this + * @private + */ + var self = {}; + + + /* Variables */ + self._start_stamp = 0; + + + /** + * Provides an adapter to the JSJaCJingle console implementation which is different + * @private + * @return {object} + */ + self._consoleAdapter = (function() { + + /** + * Alias of this + * @private + */ + var _console = {}; + + + /** + * Console logging interface (adapted) + * @public + * @param {string} message + * @param {number} loglevel + * @return {undefined} + */ + _console.log = function(message, loglevel) { + + try { + if(!message) { + throw 'No message passed to console adapter!'; + } + + switch(loglevel) { + case 0: + Console.warn(message); break; + case 1: + Console.error(message); break; + case 2: + Console.info(message); break; + case 4: + Console.debug(message); break; + default: + Console.log(message); + } + } catch(e) { + Console.error('Call._consoleAdapter.log', e); + } + + }; + + + /** + * Return sub-class scope + */ + return _console; + + })(); + + + /** + * Initializes Jingle router + * @public + * @return {undefined} + */ + self.init = function() { + + try { + // Listen for incoming Jingle/Muji packet + JSJaCJingle.listen({ + connection: con, + debug: self._consoleAdapter, + + // TODO: setting a fallback fucks up some calls... + // fallback: './server/jingle.php', + + single_initiate: function(stanza) { + try { + // Already in a call? + if(self.is_ongoing()) { + // Try to restore SID there + var stanza_id = stanza.getID(); + var sid = null; + + if(stanza_id) { + var stanza_id_split = stanza_id.split('_'); + sid = stanza_id_split[1]; + } + + // Build a temporary Jingle session + var jingle_close = new JSJaCJingle.session( + JSJAC_JINGLE_SESSION_SINGLE, + { + to: stanza.getFrom(), + debug: JSJaCJingleStorage.get_debug() + } + ); + + if(sid) { + jingle_close._set_sid(sid); + } + + jingle_close.terminate(JSJAC_JINGLE_REASON_BUSY); + + Console.warn('session_initiate_success', 'Dropped incoming call because already in a call.'); + + return; + } + + var xid = Common.fullXID(Common.getStanzaFrom(stanza)); + + Console.info('Incoming call from: ' + xid); + + // Session values + Jingle.receive(xid, stanza); + } catch(e) { + Console.error('Call.init[single_initiate]', e); + } + }, + + // Receive a multiparty (Muji) call + muji_invite: function(stanza, args) { + try { + if(!self.is_ongoing()) { + // Session values + Muji.receive(args, stanza); + } + } catch(e) { + Console.error('Call.init[muji_invite]', e); + } + } + }); + + // Enable Jingle/Muji UI elements if plugin could start + if(JSJAC_JINGLE_AVAILABLE) { + $('.jingle-hidable, .muji-hidable').show(); + } + } catch(e) { + Console.error('Call.init', e); + } + + }; + + + /** + * Opens the call interface + * @public + * @return {undefined} + */ + self.open = function() { + + try { + if(Jingle.in_call()) { + Jingle.open(); + } else if(Muji.in_call()) { + Muji.open(); + } + } catch(e) { + Console.error('Call.open', e); + } + + }; + + + /** + * Stops current call + * @public + * @return {boolean} + */ + self.stop = function() { + + try { + Jingle.stop(); + Muji.stop(); + } catch(e) { + Console.error('Call.stop', e); + } finally { + return false; + } + + }; + + + /** + * Mutes current call + * @public + * @param {object} session + * @param {object} controls + * @return {undefined} + */ + self.mute = function(session, controls) { + + try { + if(session) { + // Toggle interface buttons + controls.filter('.mute').hide(); + controls.filter('.unmute').show(); + + // Actually mute audio stream + if(session.get_mute(JSJAC_JINGLE_MEDIA_AUDIO) === false) { + session.mute(JSJAC_JINGLE_MEDIA_AUDIO); + } + } + } catch(e) { + Console.error('Call.mute', e); + } + + }; + + + /** + * Unmutes current call + * @public + * @param {object} session + * @param {object} controls + * @return {undefined} + */ + self.unmute = function(session, controls) { + + try { + if(session) { + controls.filter('.unmute').hide(); + controls.filter('.mute').show(); + + if(session.get_mute(JSJAC_JINGLE_MEDIA_AUDIO) === true) { + session.unmute(JSJAC_JINGLE_MEDIA_AUDIO); + } + } + } catch(e) { + Console.error('Call.mute', e); + } + + }; + + + /** + * Checks whether user is in call or not + * @public + * @return {boolean} + */ + self.is_ongoing = function() { + + is_ongoing = false; + + try { + is_ongoing = (Jingle.in_call() === true || Muji.in_call() === true); + } catch(e) { + Console.error('Call.is_ongoing', e); + } finally { + return is_ongoing; + } + + }; + + + /** + * Checks if the given call SID is the same as the current call's one + * @public + * @param {object} session + * @param {object} compare_session + * @return {boolean} + */ + self.is_same_sid = function(session, compare_session) { + + is_same = false; + + try { + if(compare_session && session && + compare_session.get_sid() === session.get_sid()) { + is_same = true; + } + } catch(e) { + Console.error('Call.is_same_sid', e); + } finally { + return is_same; + } + + }; + + + /** + * Returns if current call is audio + * @public + * @param {object} session + * @return {boolean} + */ + self.is_audio = function(session) { + + audio = false; + + try { + if(session && session.get_media() === JSJAC_JINGLE_MEDIA_AUDIO) { + audio = true; + } + } catch(e) { + Console.error('Call.is_audio', e); + } finally { + return audio; + } + + }; + + + /** + * Returns if current call is video + * @public + * @param {object} session + * @return {boolean} + */ + self.is_video = function(session) { + + video = false; + + try { + if(session && session.get_media() === JSJAC_JINGLE_MEDIA_VIDEO) { + video = true; + } + } catch(e) { + Console.error('Call.is_video', e); + } finally { + return video; + } + + }; + + + /** + * Set the Muji session as started + * @public + * @param {string} mode + * @return {boolean} + */ + self.start_session = function(mode) { + + try { + if(!(mode in JSJAC_JINGLE_MEDIAS)) { + throw 'Unknown mode: ' + (mode || 'none'); + } + + var call_tool_sel = $('#top-content .tools.call'); + + call_tool_sel.removeClass('audio video active'); + call_tool_sel.addClass('streaming').addClass(mode); + + Console.info('Call session successfully started, mode: ' + (mode || 'none')); + } catch(e) { + Console.error('Call.start_session', e); + } finally { + return false; + } + + }; + + + /** + * Set the Jingle session as stopped + * @public + * @param {string} mode + * @return {boolean} + */ + self.stop_session = function() { + + try { + $('#top-content .tools.call').removeClass('audio video active streaming'); + + Console.info('Call session successfully stopped'); + } catch(e) { + Console.error('Call.stop_session', e); + } finally { + return false; + } + + }; + + + /** + * Generates ICE servers configuration + * @public + * @return {object} + */ + self.generate_ice_servers = function() { + + ice_servers = { + stun: [], + turn: [] + }; + + try { + if(HOST_STUN) { + ice_servers.stun.push({ + 'host': HOST_STUN + }); + } + + if(HOST_TURN) { + ice_servers.turn.push({ + 'host': HOST_TURN, + 'username': HOST_TURN_USERNAME, + 'credential': HOST_TURN_PASSWORD + }); + } + } catch(e) { + Console.error('Call.generate_ice_servers', e); + } finally { + return is_ongoing; + } + + }; + + + /** + * Returns the notification map (based on call type) + * @private + * @param {string} call_type + * @return {object} + */ + self._get_notify_map = function(call_type) { + + var map = {}; + + try { + switch(call_type) { + case JSJAC_JINGLE_SESSION_SINGLE: + map = Jingle._notify_map(); break; + case JSJAC_JINGLE_SESSION_MUJI: + map = Muji._notify_map(); break; + default: + return; + } + } catch(e) { + Console.error('Call._get_notify_map', e); + } finally { + return map; + } + + }; + + + /** + * Notify for something related to calls + * @public + * @param {string} call_type + * @param {string} xid + * @param {string} type + * @param {string} mode + * @return {boolean} + */ + self.notify = function(call_type, xid, type, mode, sender_xid) { + + try { + sender_xid = sender_xid || xid; + + // Notification data map + var map = self._get_notify_map(call_type); + + if(!(type in map)) { + throw 'Notification type not recognized!'; + } + + // Selectors + var call_tools_all_sel = $('#top-content .tools-all:has(.tools.call)'); + var call_tool_sel = call_tools_all_sel.find('.tools.call'); + var call_content_sel = call_tools_all_sel.find('.call-content'); + var call_subitem_sel = call_content_sel.find('.tools-content-subitem'); + + // Generate proper full name + var fullname; + + if(call_type === JSJAC_JINGLE_SESSION_MUJI && sender_xid === Common.getXID()) { + fullname = Common._e("Conference call"); + } else { + fullname = Name.getBuddy(sender_xid).htmlEnc(); + } + + // Generate buttons code + var buttons_html = ''; + var i = 0; + + if(typeof map[type].buttons === 'object') { + $.each(map[type].buttons, function(button, attrs) { + buttons_html += '' + attrs.text + ''; + }); + } + + // Append notification to DOM + call_subitem_sel.html( + '
' + + '
' + + '
' + + '' + + '
' + + + '' + + '
' + + + '
' + + '' + fullname + '' + + '' + map[type].text + '' + + + '
' + buttons_html + '
' + + '
' + + '
' + ); + + // Apply user avatar + Avatar.get(sender_xid, 'cache', 'true', 'forget'); + + // Apply button events + if(typeof map[type].buttons === 'object') { + $.each(map[type].buttons, function(button, attrs) { + call_tools_all_sel.find('a.reply-button[data-action="' + button + '"]').click(function() { + try { + // Remove notification + self._unnotify(xid); + + // Execute callback, if any + if(typeof attrs.cb === 'function') { + attrs.cb(xid, mode); + } + + Console.info('Closed call notification drawer'); + } catch(e) { + Console.error('Call.notify[async]', e); + } finally { + return false; + } + }); + }); + } + + // Enable notification box! + call_tool_sel.addClass('active'); + + // Open notification box! + call_content_sel.show(); + } catch(e) { + Console.error('Call.notify', e); + } finally { + return false; + } + + }; + + + /** + * Remove notification + * @private + * @return {boolean} + */ + self._unnotify = function() { + + try { + // Selectors + var call_tools_all_sel = $('#top-content .tools-all:has(.tools.call)'); + var call_tool_sel = call_tools_all_sel.find('.tools.call'); + var call_content_sel = call_tools_all_sel.find('.call-content'); + var call_subitem_sel = call_content_sel.find('.tools-content-subitem'); + + // Close & disable notification box + call_content_sel.hide(); + call_subitem_sel.empty(); + call_tool_sel.removeClass('active'); + + // Stop all sounds + Audio.stop('incoming-call'); + Audio.stop('outgoing-call'); + } catch(e) { + Console.error('Call._unnotify', e); + } finally { + return false; + } + + }; + + + /** + * Processes the video elements size + * @private + * @param {object} screen + * @param {object} video + * @return {object} + */ + self._process_size = function(screen, video) { + + try { + if(!(typeof screen === 'object' && typeof video === 'object')) { + throw 'Invalid object passed, aborting!'; + } + + // Get the intrinsic size of the video + var video_w = video[0].videoWidth || video.width(); + var video_h = video[0].videoHeight || video.height(); + + // Get the screen size of the video + var screen_w = screen.width(); + var screen_h = screen.height(); + + // Process resize ratios (2 cases) + var r_1 = screen_h / video_h; + var r_2 = screen_w / video_w; + + // Process resized video sizes + var video_w_1 = video_w * r_1; + var video_h_1 = video_h * r_1; + + var video_w_2 = video_w * r_2; + var video_h_2 = video_h * r_2; + + // DOM view modifiers + var dom_width = 'auto'; + var dom_height = 'auto'; + var dom_left = 0; + var dom_top = 0; + + // Landscape/Portrait/Equal container? + if(video_w > video_h || (video_h == video_w && screen_w < screen_h)) { + // Not sufficient? + if(video_w_1 < screen_w) { + dom_width = screen_w + 'px'; + dom_top = -1 * (video_h_2 - screen_h) / 2; + } else { + dom_height = screen_h + 'px'; + dom_left = -1 * (video_w_1 - screen_w) / 2; + } + } else if(video_h > video_w || (video_h == video_w && screen_w > screen_h)) { + // Not sufficient? + if(video_h_1 < screen_h) { + dom_height = screen_h + 'px'; + dom_left = -1 * (video_w_1 - screen_w) / 2; + } else { + dom_width = screen_w + 'px'; + dom_top = -1 * (video_h_2 - screen_h) / 2; + } + } else if(screen_w == screen_h) { + dom_width = screen_w + 'px'; + dom_height = screen_h + 'px'; + } + + return { + width : dom_width, + height : dom_height, + left : dom_left, + top : dom_top + }; + } catch(e) { + Console.error('Call._process_size', e); + } + + }; + + + /** + * Adapts the local video view + * @public + * @param {object} local_sel + * @return {undefined} + */ + self.adapt_local = function(local_sel) { + + try { + var local_video_sel = local_sel.find('video'); + + // Process new sizes + var sizes = Call._process_size( + local_sel, + local_video_sel + ); + + // Apply new sizes + local_video_sel.css({ + 'height': sizes.height, + 'width': sizes.width, + 'margin-top': sizes.top, + 'margin-left': sizes.left + }); + } catch(e) { + Console.error('Call.adapt_local', e); + } + + }; + + + /** + * Adapts the remote video view + * @public + * @param {object} videobox_sel + * @return {undefined} + */ + self.adapt_remote = function(videobox_sel) { + + try { + var remote_video_sel, sizes; + + videobox_sel.find('.remote_video').each(function() { + remote_video_sel = $(this).find('video'); + + if(remote_video_sel.size()) { + // Process new sizes + sizes = Call._process_size( + $(this), + remote_video_sel + ); + + // Apply new sizes + remote_video_sel.css({ + 'height': sizes.height, + 'width': sizes.width, + 'margin-top': sizes.top, + 'margin-left': sizes.left + }); + } + }); + } catch(e) { + Console.error('Call.adapt_remote', e); + } + + }; + + + /** + * Start call elpsed time counter + * @public + * @return {boolean} + */ + self.start_counter = function() { + + try { + // Initialize counter + self.stop_counter(); + self._start_stamp = DateUtils.getTimeStamp(); + self._fire_clock(); + + // Fire it every second + $('#top-content .tools.call .counter').everyTime('1s', self._fire_clock); + + Console.info('Call counter started'); + } catch(e) { + Console.error('Call.start_counter', e); + } finally { + return false; + } + + }; + + + /** + * Stop call elpsed time counter + * @public + * @return {boolean} + */ + self.stop_counter = function() { + + try { + // Reset stamp storage + self._start_stamp = 0; + + // Reset counter + var counter_sel = $('#top-content .tools.call .counter'); + var default_count = counter_sel.attr('data-default'); + + counter_sel.stopTime(); + + $('#top-content .tools.call .counter').text(default_count); + $('#jingle, #muji').find('.elapsed').text(default_count); + + Console.info('Call counter stopped'); + } catch(e) { + Console.error('Call.stop_counter', e); + } finally { + return false; + } + + }; + + + /** + * Fires the counter clock (once more) + * @private + * @return {undefined} + */ + self._fire_clock = function() { + + try { + // Process updated time + var count = DateUtils.difference( + DateUtils.getTimeStamp(), + self._start_stamp + ); + + if(count.getHours()) { + count = count.toString('H:mm:ss'); + } else { + count = count.toString('mm:ss'); + } + + // Display updated counter + $('#top-content .tools.call .counter').text(count); + $('#jingle, #muji').find('.elapsed').text(count); + } catch(e) { + Console.error('Call._fire_clock', e); + } + + }; + + + /** + * Destroy the call interface + * @public + * @return {undefined} + */ + self.destroy_interface = function(container_sel) { + + try { + container_sel.stopTime(); + container_sel.find('*').stopTime(); + + container_sel.remove(); + } catch(e) { + Console.error('Call.destroy_interface', e); + } + + }; + + + /** + * Show the call interface + * @public + * @param {object} manager + * @param {object} call_sel + * @param {object} video_container_sel + * @return {boolean} + */ + self.show_interface = function(manager, call_sel, video_container_sel) { + + try { + if(manager.in_call()) { + call_sel.filter(':hidden').show(); + + // Launch back some events + video_container_sel.mousemove(); + } + } catch(e) { + Console.error('Call.show_interface', e); + } finally { + return false; + } + + }; + + + /** + * Hide the call interface + * @public + * @param {object} call_sel + * @param {object} video_container_sel + * @return {boolean} + */ + self.hide_interface = function(call_sel, video_container_sel) { + + try { + call_sel.filter(':visible').hide(); + + // Reset some events + video_container_sel.find('.topbar').stopTime().hide(); + } catch(e) { + Console.error('Call.hide_interface', e); + } finally { + return false; + } + + }; + + + /** + * Attaches interface events + * @public + * @param {object} manager + * @param {object} call_sel + * @param {object} video_container_sel + * @return {undefined} + */ + self.events_interface = function(manager, call_sel, video_container_sel) { + + try { + call_sel.everyTime(50, function() { + manager._adapt(); + }); + + // Close interface on click on semi-transparent background + call_sel.click(function(evt) { + try { + // Click on lock background? + if($(evt.target).is('.lock')) { + return manager._hide_interface(); + } + } catch(e) { + Console.error('Call.events_interface[async]', e); + } + }); + + // Click on a control or action button + call_sel.find('.topbar').find('.controls a, .actions a').click(function() { + try { + switch($(this).data('type')) { + case 'close': + manager._hide_interface(); break; + case 'stop': + case 'leave': + manager.stop(); break; + case 'mute': + manager.mute(); break; + case 'unmute': + manager.unmute(); break; + } + } catch(e) { + Console.error('Call.events_interface[async]', e); + } finally { + return false; + } + }); + + // Auto Hide/Show interface topbar + video_container_sel.mousemove(function() { + try { + var topbar_sel = $(this).find('.topbar'); + + if(topbar_sel.is(':hidden')) { + topbar_sel.stop(true).fadeIn(250); + } + + topbar_sel.stopTime(); + topbar_sel.oneTime('5s', function() { + topbar_sel.stop(true).fadeOut(250); + }); + } catch(e) { + Console.error('Call.events_interface[async]', e); + } + }); + } catch(e) { + Console.error('Call.events_interface', e); + } + + }; + + + /** + * Return class scope + */ + return self; + +})(); \ No newline at end of file diff --git a/source/app/javascripts/caps.js b/source/app/javascripts/caps.js index 0e58f66..e267ed2 100644 --- a/source/app/javascripts/caps.js +++ b/source/app/javascripts/caps.js @@ -20,6 +20,444 @@ var Caps = (function () { var self = {}; + /* Constants */ + self.disco_infos = { + 'identity': { + 'category': 'client', + 'type': 'web', + 'name': 'Jappix' + }, + + 'items': [ + NS_MUC, + NS_MUC_USER, + NS_MUC_ADMIN, + NS_MUC_OWNER, + NS_MUC_CONFIG, + NS_DISCO_INFO, + NS_DISCO_ITEMS, + NS_PUBSUB_RI, + NS_BOSH, + NS_CAPS, + NS_MOOD, + NS_ACTIVITY, + NS_TUNE, + NS_GEOLOC, + NS_NICK, + NS_URN_MBLOG, + NS_URN_INBOX, + NS_MOOD + NS_NOTIFY, + NS_ACTIVITY + NS_NOTIFY, + NS_TUNE + NS_NOTIFY, + NS_GEOLOC + NS_NOTIFY, + NS_URN_MBLOG + NS_NOTIFY, + NS_URN_INBOX + NS_NOTIFY, + NS_URN_DELAY, + NS_ROSTER, + NS_ROSTERX, + NS_HTTP_AUTH, + NS_CHATSTATES, + NS_XHTML_IM, + NS_URN_MAM, + NS_IPV6, + NS_LAST, + NS_PRIVATE, + NS_REGISTER, + NS_SEARCH, + NS_COMMANDS, + NS_VERSION, + NS_XDATA, + NS_VCARD, + NS_IETF_VCARD4, + NS_URN_ADATA, + NS_URN_AMETA, + NS_URN_TIME, + NS_URN_PING, + NS_URN_RECEIPTS, + NS_PRIVACY, + NS_IQOOB, + NS_XOOB, + NS_URN_CARBONS, + NS_URN_CORRECT, + NS_URN_MARKERS, + NS_URN_IDLE, + NS_URN_ATTENTION, + NS_URN_REACH, + NS_URN_HINTS + ] + }; + + + /** + * Parse identities from disco infos query response + * @private + * @param {object} query + * @return {object} + */ + self._parseDiscoIdentities = function(query) { + + var identities = []; + + try { + var cur_category, cur_type, cur_lang, cur_name; + + $(query).find('identity').each(function() { + cur_category = $(this).attr('category') || ''; + cur_type = $(this).attr('type') || ''; + cur_lang = $(this).attr('xml:lang') || ''; + cur_name = $(this).attr('name') || ''; + + identities.push(cur_category + '/' + cur_type + '/' + cur_lang + '/' + cur_name); + }); + } catch(e) { + Console.error('Caps._parseDiscoIdentities', e); + } finally { + return identities; + } + + }; + + + /** + * Parse features from disco infos query response + * @private + * @param {object} query + * @return {object} + */ + self._parseDiscoFeatures = function(query) { + + var features = []; + + try { + var cur_var; + + $(query).find('feature').each(function() { + cur_var = $(this).attr('var'); + + // Add the current value to the array + if(cur_var) { + features.push(cur_var); + } + }); + } catch(e) { + Console.error('Caps._parseDiscoFatures', e); + } finally { + return features; + } + + }; + + + /** + * Parse data form from disco infos query response + * @private + * @param {object} query + * @return {object} + */ + self._parseDiscoDataForms = function(query) { + + var data_forms = []; + + try { + var cur_string, cur_sort_var, + cur_text, cur_var, cur_sort_val; + + $(query).find('x[xmlns="' + NS_XDATA + '"]').each(function() { + // Initialize some stuffs + cur_string = ''; + cur_sort_var = []; + + // Add the form type field + $(this).find('field[var="FORM_TYPE"] value').each(function() { + cur_text = $(this).text(); + + if(cur_text) { + cur_string += cur_text + '<'; + } + }); + + // Add the var attributes into an array + $(this).find('field:not([var="FORM_TYPE"])').each(function() { + cur_var = $(this).attr('var'); + + if(cur_var) { + cur_sort_var.push(cur_var); + } + }); + + // Sort the var attributes + cur_sort_var = cur_sort_var.sort(); + + // Loop this sorted var attributes + $.each(cur_sort_var, function(i) { + // Initialize the value sorting + cur_sort_val = []; + + // Append it to the string + cur_string += cur_sort_var[i] + '<'; + + // Add each value to the array + $(this).find('field[var=' + cur_sort_var[i] + '] value').each(function() { + cur_sort_val.push($(this).text()); + }); + + // Sort the values + cur_sort_val = cur_sort_val.sort(); + + // Append the values to the string + for(var j in cur_sort_val) { + cur_string += cur_sort_val[j] + '<'; + } + }); + + // Any string? + if(cur_string) { + // Remove the undesired double '<' from the string + if(cur_string.match(/(.+)(<)+$/)) { + cur_string = cur_string.substring(0, cur_string.length - 1); + } + + // Add the current string to the array + data_forms.push(cur_string); + } + }); + } catch(e) { + Console.error('Caps._parseDiscoDataForms', e); + } finally { + return data_forms; + } + + }; + + + /** + * Apply XHTML-IM features from disco infos + * @private + * @param {string} xid + * @param {object} features + * @param {object} style_sel + * @param {object} message_area_sel + * @return {undefined} + */ + self._applyDiscoXHTMLIM = function(xid, features, style_sel, message_area_sel) { + + try { + // Apply + if(NS_XHTML_IM in features) { + style_sel.show(); + } else { + // Remove the tooltip elements + style_sel.hide(); + style_sel.find('.bubble-style').remove(); + + // Reset the markers + message_area_sel.removeAttr('style') + .removeAttr('data-font') + .removeAttr('data-fontsize') + .removeAttr('data-color') + .removeAttr('data-bold') + .removeAttr('data-italic') + .removeAttr('data-underline'); + } + + } catch(e) { + Console.error('Caps._applyDiscoXHTMLIM', e); + } + + }; + + + /** + * Apply Jingle features from disco infos + * @private + * @param {string} xid + * @param {object} path_sel + * @param {object} roster_sel + * @return {undefined} + */ + self._applyDiscoJingle = function(xid, path_sel, roster_sel) { + + try { + // Selectors + var roster_jingle_sel = roster_sel.find('.buddy-infos .call-jingle'); + var jingle_audio = path_sel.find('.tools-jingle-audio'); + var roster_jingle_audio = roster_jingle_sel.find('a.audio'); + var jingle_video = path_sel.find('.tools-jingle-video'); + var roster_jingle_video = roster_jingle_sel.find('a.video'); + var roster_jingle_separator = roster_jingle_sel.find('span.separator'); + + // Apply + var jingle_local_supported = JSJAC_JINGLE_AVAILABLE; + var jingle_audio_xid = self.getFeatureResource(xid, NS_JINGLE_APPS_RTP_AUDIO); + var jingle_video_xid = self.getFeatureResource(xid, NS_JINGLE_APPS_RTP_VIDEO); + + if(jingle_audio_xid && jingle_local_supported) { + jingle_audio.show(); + roster_jingle_audio.show(); + } else { + jingle_audio.hide(); + roster_jingle_audio.hide(); + } + + if(jingle_video_xid && jingle_local_supported) { + jingle_video.show(); + roster_jingle_video.show(); + } else { + jingle_video.hide(); + roster_jingle_video.hide(); + } + + if(jingle_audio_xid && jingle_video_xid && jingle_local_supported) { + roster_jingle_separator.show(); + } else { + roster_jingle_separator.hide(); + } + + if((jingle_audio_xid || jingle_video_xid) && jingle_local_supported) { + roster_jingle_sel.show(); + } else { + roster_jingle_sel.hide(); + } + } catch(e) { + Console.error('Caps._applyDiscoJingle', e); + } + + }; + + + /** + * Apply Out of Band Data features from disco infos + * @private + * @param {string} xid + * @param {object} features + * @param {object} file_sel + * @return {undefined} + */ + self._applyDiscoOOB = function(xid, features, file_sel) { + + try { + // Apply + var iq_oob_xid = self.getFeatureResource(xid, NS_IQOOB); + + if(iq_oob_xid || NS_XOOB in features) { + file_sel.show(); + + // Set a marker + file_sel.attr( + 'data-oob', + iq_oob_xid ? 'iq' : 'x' + ); + } else { + // Remove the tooltip elements + file_sel.hide(); + file_sel.find('.bubble-style').remove(); + + // Reset the marker + file_sel.removeAttr('data-oob'); + } + } catch(e) { + Console.error('Caps._applyDiscoOOB', e); + } + + }; + + + /** + * Apply Receipts features from disco infos + * @private + * @param {string} xid + * @param {object} features + * @param {object} message_area_sel + * @return {undefined} + */ + self._applyDiscoReceipts = function(xid, features, message_area_sel) { + + try { + // Apply + if(NS_URN_RECEIPTS in features) { + message_area_sel.attr('data-receipts', 'true'); + } else { + message_area_sel.removeAttr('data-receipts'); + } + } catch(e) { + Console.error('Caps._applyDiscoReceipts', e); + } + + }; + + + /** + * Apply Last Message Correction features from disco infos + * @private + * @param {string} xid + * @param {object} features + * @param {object} path_sel + * @return {undefined} + */ + self._applyDiscoCorrection = function(xid, features, path_sel) { + + try { + // Apply + if(NS_URN_CORRECT in features) { + path_sel.attr('data-correction', 'true'); + } else { + path_sel.removeAttr('data-correction'); + } + } catch(e) { + Console.error('Caps._applyDiscoCorrection', e); + } + + }; + + + /** + * Apply Chat Markers features from disco infos + * @private + * @param {string} xid + * @param {object} features + * @param {object} path_sel + * @return {undefined} + */ + self._applyDiscoMarkers = function(xid, features, path_sel) { + + try { + // Apply + if(NS_URN_MARKERS in features) { + path_sel.attr('data-markers', 'true'); + } else { + path_sel.removeAttr('data-markers'); + } + } catch(e) { + Console.error('Caps._applyDiscoMarkers', e); + } + + }; + + + /** + * Apply Attention features from disco infos + * @private + * @param {string} xid + * @param {object} features + * @param {object} path_sel + * @return {undefined} + */ + self._applyDiscoAttention = function(xid, features, path_sel) { + + try { + // Apply + if(NS_URN_ATTENTION in features) { + path_sel.attr('data-attention', 'true'); + } else { + path_sel.removeAttr('data-attention'); + } + } catch(e) { + Console.error('Caps._applyDiscoAttention', e); + } + + }; + + /** * Reads a stored Caps * @public @@ -47,62 +485,12 @@ var Caps = (function () { self.myDiscoInfos = function() { try { - var disco_base = [ - NS_MUC, - NS_MUC_USER, - NS_MUC_ADMIN, - NS_MUC_OWNER, - NS_MUC_CONFIG, - NS_DISCO_INFO, - NS_DISCO_ITEMS, - NS_PUBSUB_RI, - NS_BOSH, - NS_CAPS, - NS_MOOD, - NS_ACTIVITY, - NS_TUNE, - NS_GEOLOC, - NS_NICK, - NS_URN_MBLOG, - NS_URN_INBOX, - NS_MOOD + NS_NOTIFY, - NS_ACTIVITY + NS_NOTIFY, - NS_TUNE + NS_NOTIFY, - NS_GEOLOC + NS_NOTIFY, - NS_URN_MBLOG + NS_NOTIFY, - NS_URN_INBOX + NS_NOTIFY, - NS_URN_DELAY, - NS_ROSTER, - NS_ROSTERX, - NS_HTTP_AUTH, - NS_CHATSTATES, - NS_XHTML_IM, - NS_URN_MAM, - NS_IPV6, - NS_LAST, - NS_PRIVATE, - NS_REGISTER, - NS_SEARCH, - NS_COMMANDS, - NS_VERSION, - NS_XDATA, - NS_VCARD, - NS_IETF_VCARD4, - NS_URN_ADATA, - NS_URN_AMETA, - NS_URN_TIME, - NS_URN_PING, - NS_URN_RECEIPTS, - NS_PRIVACY, - NS_IQOOB, - NS_XOOB, - NS_URN_CARBONS - ]; + var disco_base = self.disco_infos.items; - var disco_jingle = JSJaCJingle_disco(); + var disco_jingle = JSJaCJingle.disco(); var disco_all = disco_base.concat(disco_jingle); - return disco_all; + return Utils.uniqueArrayValues(disco_all); } catch(e) { Console.error('Caps.myDiscoInfos', e); } @@ -169,103 +557,17 @@ var Caps = (function () { self.handleDiscoInfos = function(iq) { try { - if(!iq || (iq.getType() == 'error')) + if(!iq || (iq.getType() == 'error')) { return; + } - // IQ received, get some values var from = Common.fullXID(Common.getStanzaFrom(iq)); var query = iq.getQuery(); - // Generate the CAPS-processing values - var identities = []; - var features = []; - var data_forms = []; - - // Identity values - $(query).find('identity').each(function() { - var pCategory = $(this).attr('category'); - var pType = $(this).attr('type'); - var pLang = $(this).attr('xml:lang'); - var pName = $(this).attr('name'); - - if(!pCategory) - pCategory = ''; - if(!pType) - pType = ''; - if(!pLang) - pLang = ''; - if(!pName) - pName = ''; - - identities.push(pCategory + '/' + pType + '/' + pLang + '/' + pName); - }); - - // Feature values - $(query).find('feature').each(function() { - var pVar = $(this).attr('var'); - - // Add the current value to the array - if(pVar) - features.push(pVar); - }); - - // Data-form values - $(query).find('x[xmlns="' + NS_XDATA + '"]').each(function() { - // Initialize some stuffs - var pString = ''; - var sortVar = []; - - // Add the form type field - $(this).find('field[var="FORM_TYPE"] value').each(function() { - var cText = $(this).text(); - - if(cText) - pString += cText + '<'; - }); - - // Add the var attributes into an array - $(this).find('field:not([var="FORM_TYPE"])').each(function() { - var cVar = $(this).attr('var'); - - if(cVar) - sortVar.push(cVar); - }); - - // Sort the var attributes - sortVar = sortVar.sort(); - - // Loop this sorted var attributes - $.each(sortVar, function(i) { - // Initialize the value sorting - var sortVal = []; - - // Append it to the string - pString += sortVar[i] + '<'; - - // Add each value to the array - $(this).find('field[var=' + sortVar[i] + '] value').each(function() { - sortVal.push($(this).text()); - }); - - // Sort the values - sortVal = sortVal.sort(); - - // Append the values to the string - for(var j in sortVal) { - pString += sortVal[j] + '<'; - } - }); - - // Any string? - if(pString) { - // Remove the undesired double '<' from the string - if(pString.match(/(.+)(<)+$/)) - pString = pString.substring(0, pString.length - 1); - - // Add the current string to the array - data_forms.push(pString); - } - }); + // Parse values + var identities = self._parseDiscoIdentities(query); + var features = self._parseDiscoFeatures(query); + var data_forms = self._parseDiscoDataForms(query); // Process the CAPS var caps = self.process(identities, features, data_forms); @@ -309,8 +611,9 @@ var Caps = (function () { var xid = Common.bareXID(from); // This comes from a private groupchat chat? - if(Utils.isPrivate(xid)) + if(Utils.isPrivate(xid)) { xid = from; + } hash = hex_md5(xid); @@ -326,96 +629,20 @@ var Caps = (function () { }); // Paths - var path = $('#' + hash); - var roster_path = $('#roster .buddy.' + hash); - var roster_jingle_path = roster_path.find('.buddy-infos .call-jingle'); - - var message_area = path.find('.message-area'); - var style = path.find('.chat-tools-style'); - var jingle_audio = path.find('.tools-jingle-audio'); - var roster_jingle_audio = roster_jingle_path.find('a.audio'); - var jingle_video = path.find('.tools-jingle-video'); - var roster_jingle_video = roster_jingle_path.find('a.video'); - var roster_jingle_separator = roster_jingle_path.find('span.separator'); - var file = path.find('.chat-tools-file'); + var path_sel = $('#' + hash); + var roster_sel = $('#roster .buddy.' + hash); + var message_area_sel = path_sel.find('.message-area'); + var style_sel = path_sel.find('.chat-tools-style'); + var file_sel = path_sel.find('.chat-tools-file'); - // Apply xHTML-IM - if(NS_XHTML_IM in features) { - style.show(); - } else { - // Remove the tooltip elements - style.hide(); - style.find('.bubble-style').remove(); - - // Reset the markers - message_area.removeAttr('style') - .removeAttr('data-font') - .removeAttr('data-fontsize') - .removeAttr('data-color') - .removeAttr('data-bold') - .removeAttr('data-italic') - .removeAttr('data-underline'); - } - - // Apply Jingle - var jingle_local_supported = JSJAC_JINGLE_AVAILABLE; - var jingle_audio_xid = self.getFeatureResource(xid, NS_JINGLE_APPS_RTP_AUDIO); - var jingle_video_xid = self.getFeatureResource(xid, NS_JINGLE_APPS_RTP_VIDEO); - - if(jingle_audio_xid && jingle_local_supported) { - jingle_audio.show(); - roster_jingle_audio.show(); - } else { - jingle_audio.hide(); - roster_jingle_audio.hide(); - } - - if(jingle_video_xid && jingle_local_supported) { - jingle_video.show(); - roster_jingle_video.show(); - } else { - jingle_video.hide(); - roster_jingle_video.hide(); - } - - if(jingle_audio_xid && jingle_video_xid && jingle_local_supported) { - roster_jingle_separator.show(); - } else { - roster_jingle_separator.hide(); - } - - if((jingle_audio_xid || jingle_video_xid) && jingle_local_supported) { - roster_jingle_path.show(); - } else { - roster_jingle_path.hide(); - } - - // Apply Out of Band Data - var iq_oob_xid = self.getFeatureResource(xid, NS_IQOOB); - - if(iq_oob_xid || NS_XOOB in features) { - file.show(); - - // Set a marker - file.attr( - 'data-oob', - iq_oob_xid ? 'iq' : 'x' - ); - } else { - // Remove the tooltip elements - file.hide(); - file.find('.bubble-style').remove(); - - // Reset the marker - file.removeAttr('data-oob'); - } - - // Apply receipts - if(NS_URN_RECEIPTS in features) { - message_area.attr('data-receipts', 'true'); - } else { - message_area.removeAttr('data-receipts'); - } + // Apply Features + self._applyDiscoXHTMLIM(xid, features, style_sel, message_area_sel); + self._applyDiscoJingle(xid, path_sel, roster_sel); + self._applyDiscoOOB(xid, features, file_sel); + self._applyDiscoReceipts(xid, features, message_area_sel); + self._applyDiscoCorrection(xid, features, path_sel); + self._applyDiscoMarkers(xid, features, path_sel); + self._applyDiscoAttention(xid, features, path_sel); } catch(e) { Console.error('Caps.displayDiscoInfos', e); } @@ -426,39 +653,39 @@ var Caps = (function () { /** * Generates the CAPS hash * @public - * @param {object} cIdentities - * @param {object} cFeatures - * @param {object} cDataForms + * @param {object} identities + * @param {object} features + * @param {object} dataforms * @return {string} */ - self.process = function(cIdentities, cFeatures, cDataForms) { + self.process = function(identities, features, dataforms) { try { // Initialize - var cString = ''; + var caps_str = ''; // Sort the arrays - cIdentities = cIdentities.sort(); - cFeatures = cFeatures.sort(); - cDataForms = cDataForms.sort(); + identities = identities.sort(); + features = features.sort(); + dataforms = dataforms.sort(); // Process the sorted identity string - for(var a in cIdentities) { - cString += cIdentities[a] + '<'; + for(var a in identities) { + caps_str += identities[a] + '<'; } // Process the sorted feature string - for(var b in cFeatures) { - cString += cFeatures[b] + '<'; + for(var b in features) { + caps_str += features[b] + '<'; } // Process the sorted data-form string - for(var c in cDataForms) { - cString += cDataForms[c] + '<'; + for(var c in dataforms) { + caps_str += dataforms[c] + '<'; } // Process the SHA-1 hash - var cHash = b64_sha1(cString); + var cHash = b64_sha1(caps_str); return cHash; } catch(e) { @@ -477,7 +704,12 @@ var Caps = (function () { try { return self.process( - ['client/web//Jappix'], + [ + self.disco_infos.identity.category + '/' + + self.disco_infos.identity.type + '//' + + self.disco_infos.identity.name + ], + self.myDiscoInfos(), [] ); @@ -507,8 +739,14 @@ var Caps = (function () { var max_priority = null; var cur_xid_full, cur_presence_sel, cur_caps, cur_features, cur_priority; - for(var cur_resource in Presence.resources(xid)) { - cur_xid_full = xid + '/' + cur_resource; + var resources_obj = Presence.resources(xid); + var fn_parse_resource = function(cur_resource) { + cur_xid_full = xid; + + if(cur_resource) { + cur_xid_full += '/' + cur_resource; + } + cur_presence_sel = $(Presence.readStanza(cur_xid_full)); cur_priority = parseInt((cur_presence_sel.find('priority').text() || 0), 10); @@ -523,6 +761,14 @@ var Caps = (function () { selected_xid = cur_xid_full; } } + }; + + if(resources_obj.bare === 1) { + fn_parse_resource(null); + } + + for(var cur_resource in resources_obj.list) { + fn_parse_resource(cur_resource); } } catch(e) { Console.error('Caps.getFeatureResource', e); @@ -538,4 +784,4 @@ var Caps = (function () { */ return self; -})(); \ No newline at end of file +})(); diff --git a/source/app/javascripts/carbons.js b/source/app/javascripts/carbons.js index 7742883..96e64f4 100644 --- a/source/app/javascripts/carbons.js +++ b/source/app/javascripts/carbons.js @@ -22,7 +22,7 @@ var Carbons = (function () { /** * Configures Message Carbons options - * @public + * @private * @param {string} type * @return {undefined} */ @@ -50,7 +50,7 @@ var Carbons = (function () { /** * Configures Message Carbons options - * @public + * @private * @param {object} iq * @param {string} type * @return {undefined} @@ -193,6 +193,9 @@ var Carbons = (function () { } else { Console.debug('Got a sent message from another resource to: ' + (to || 'none') + ', was ignored because body empty'); } + + // Handle chat markers change + Markers.handleCarbonChange(forwarded_message); } else { Console.debug('Got a sent message from another resource to: ' + (to || 'none') + ', was ignored because chat not open'); } diff --git a/source/app/javascripts/chat.js b/source/app/javascripts/chat.js index 4b064dd..31882a1 100644 --- a/source/app/javascripts/chat.js +++ b/source/app/javascripts/chat.js @@ -20,6 +20,289 @@ var Chat = (function () { var self = {}; + /** + * Apply generate events + * @private + * @param {string} path + * @param {string} id + * @param {string} xid + * @return {undefined} + */ + self._generateEvents = function(path, id, xid) { + + try { + // Click event: chat cleaner + $(path + 'tools-clear').click(function() { + self.clean(id); + }); + + // Click event: call (audio) + $(path + 'tools-jingle-audio').click(function() { + Jingle.start(xid, 'audio'); + }); + + // Click event: call (video) + $(path + 'tools-jingle-video').click(function() { + Jingle.start(xid, 'video'); + }); + + // Click event: user-infos + $(path + 'tools-infos').click(function() { + UserInfos.open(xid); + }); + } catch(e) { + Console.error('Chat._generateEvents', e); + } + + }; + + + /** + * Apply generate events + * @private + * @param {object} input_sel + * @param {string} xid + * @param {string} hash + * @return {undefined} + */ + self._createEvents = function(input_sel, xid, hash) { + + try { + self._createEventsInput(input_sel, hash); + self._createEventsKey(input_sel, xid, hash); + self._createEventsMouse(xid, hash); + } catch(e) { + Console.error('Chat._createEvents', e); + } + + }; + + + /** + * Apply generate events (input) + * @private + * @param {object} input_sel + * @param {string} hash + * @return {undefined} + */ + self._createEventsInput = function(input_sel, hash) { + + try { + input_sel.focus(function() { + // Clean notifications for this chat + Interface.chanCleanNotify(hash); + + // Store focus on this chat! + Interface.chat_focus_hash = hash; + }); + + input_sel.blur(function() { + // Reset storage about focus on this chat! + if(Interface.chat_focus_hash == hash) { + Interface.chat_focus_hash = null; + } + }); + } catch(e) { + Console.error('Chat._createEventsInput', e); + } + + }; + + + /** + * Apply generate events (key) + * @private + * @param {object} input_sel + * @param {string} xid + * @param {string} hash + * @return {undefined} + */ + self._createEventsKey = function(input_sel, xid, hash) { + + try { + input_sel.keydown(function(e) { + if(e.keyCode == 13) { + // Enter key + if(e.shiftKey || e.ctrlKey) { + // Add a new line + input_sel.val(input_sel.val() + '\n'); + } else { + if(Correction.isIn(xid) === true) { + var corrected_value = input_sel.val().trim(); + + if(corrected_value) { + // Send the corrected message + Correction.send(xid, 'chat', corrected_value); + } + + Correction.leave(xid); + } else { + // Send the message + Message.send(hash, 'chat'); + } + + // Reset the composing database entry + DataStore.setDB(Connection.desktop_hash, 'chatstate', xid, 'off'); + } + + return false; + } else if(e.keyCode == 8) { + // Leave correction mode? (another way, by flushing input value progressively) + if(Correction.isIn(xid) === true && !input_sel.val()) { + Correction.leave(xid); + } + } + }); + + input_sel.keyup(function(e) { + if(e.keyCode == 27) { + // Escape key + input_sel.val(''); + + // Leave correction mode? (simple escape way) + if(Correction.isIn(xid) === true) { + Correction.leave(xid); + } + } else { + Correction.detect(xid, input_sel); + } + }); + } catch(e) { + Console.error('Chat._createEventsKey', e); + } + + }; + + + /** + * Apply generate events (mouse) + * @private + * @param {string} xid + * @param {string} hash + * @return {undefined} + */ + self._createEventsMouse = function(xid, hash) { + + try { + // Scroll in chat content + $('#page-engine #' + hash + ' .content').scroll(function() { + var self = this; + + if(Features.enabledMAM() && !(xid in MAM.map_pending)) { + var has_state = xid in MAM.map_states; + var rsm_count = has_state ? MAM.map_states[xid].rsm.count : 1; + var rsm_before = has_state ? MAM.map_states[xid].rsm.first : ''; + + // Request more archives? + if(rsm_count > 0 && $(this).scrollTop() < MAM.SCROLL_THRESHOLD) { + var was_scroll_top = $(self).scrollTop() <= 32; + var wait_mam = $('#' + hash).find('.wait-mam'); + wait_mam.show(); + + MAM.getArchives({ + 'with': xid + }, { + 'max': MAM.REQ_MAX, + 'before': rsm_before + }, function() { + var wait_mam_height = was_scroll_top ? 0 : wait_mam.height(); + wait_mam.hide(); + + // Restore scroll? + if($(self).scrollTop() < MAM.SCROLL_THRESHOLD) { + var sel_mam_chunk = $(self).find('.mam-chunk:first'); + + var cont_padding_top = parseInt($(self).css('padding-top').replace(/[^-\d\.]/g, '')); + var cont_one_group_margin_bottom = parseInt(sel_mam_chunk.find('.one-group:last').css('margin-bottom').replace(/[^-\d\.]/g, '')); + var cont_mam_chunk_height = sel_mam_chunk.height(); + + $(self).scrollTop(wait_mam_height + cont_padding_top + cont_one_group_margin_bottom + cont_mam_chunk_height); + } + }); + } + } + }); + } catch(e) { + Console.error('Chat._createEventsMouse', e); + } + + }; + + + /** + * Apply generate events + * @private + * @param {string} type + * @param {string} id + * @return {object} + */ + self._generateChatCode = function(type, id) { + + var code_args = {}; + + try { + // Groupchat special code + if(type == 'groupchat') { + code_args.attributes = ' data-type="groupchat" data-correction="true"'; + code_args.avatar = ''; + code_args.name = '

' + Common._e("Subject") + ' ' + Common._e("no subject defined for this room.") + '

'; + code_args.code = '
' + + '

' + Common._e("Moderators") + '

' + + '

' + Common._e("Participants") + '

' + + '

' + Common._e("Visitors") + '

' + + '

' + Common._e("Others") + '

'; + code_args.link = ''; + code_args.style = ''; + + // Is this a gateway? + if(xid.match(/%/)) { + code_args.disabled = ''; + } else { + code_args.disabled = ' disabled=""'; + } + } else { + code_args.mam = '
'; + code_args.attributes = ' data-type="chat"'; + code_args.avatar = '
'; + code_args.name = '

'; + code_args.code = '
' + code_args.mam + '
'; + code_args.link = '' + + '' + + ''; + code_args.style = ' style="display: none;"'; + code_args.disabled = ''; + } + + // Not a groupchat private chat, we can use the buddy add icon + if((type == 'chat') || (type == 'groupchat')) { + var title; + + if(type == 'chat') { + title = Common._e("Add this contact to your friends"); + } else { + title = Common._e("Add this groupchat to your favorites"); + } + + code_args.link += ''; + } + + // IE DOM parsing bug fix + code_args.style_picker = '
' + + '' + + '
'; + + if((BrowserDetect.browser == 'Explorer') && (BrowserDetect.version < 9)) { + code_args.style_picker = ''; + } + } catch(e) { + Console.error('Chat._generateChatCode', e); + } finally { + return code_args; + } + + }; + + /** * Correctly opens a new chat * @public @@ -34,38 +317,40 @@ var Chat = (function () { try { // No XID? - if(!xid) + if(!xid) { return false; + } // We generate some stuffs var hash = hex_md5(xid); var name; // Gets the name of the user/title of the room - if(title) + if(title) { name = title; - - else { + } else { // Private groupchat chat - if(type == 'private') + if(type == 'private') { name = Common.thisResource(xid); + } // XMPP-ID - else if(xid.indexOf('@') != -1) + else if(xid.indexOf('@') != -1) { name = Name.getBuddy(xid); + } // Gateway - else + else { name = xid; + } } // If the target div does not exist if(!Common.exists('#' + hash)) { // We check the type of the chat to open - if((type == 'chat') || (type == 'private')) + if((type == 'chat') || (type == 'private')) { self.create(hash, xid, name, type); - - else if(type == 'groupchat') { + } else if(type == 'groupchat') { // Try to read the room stored configuration if(!Utils.isAnonymous() && (!nickname || !password || !title)) { // Catch the room data @@ -115,71 +400,21 @@ var Chat = (function () { var escaped_xid = escape(xid); // Special code - var specialAttributes, specialAvatar, specialName, specialCode, specialLink, specialDisabled, specialStyle, specialMAM; - - // Groupchat special code - if(type == 'groupchat') { - specialAttributes = ' data-type="groupchat"'; - specialAvatar = ''; - specialName = '

' + Common._e("Subject") + ' ' + Common._e("no subject defined for this room.") + '

'; - specialCode = '

' + Common._e("Moderators") + '

' + Common._e("Participants") + '

' + Common._e("Visitors") + '

' + Common._e("Others") + '

'; - specialLink = ''; - specialStyle = ''; - - // Is this a gateway? - if(xid.match(/%/)) - specialDisabled = ''; - else - specialDisabled = ' disabled=""'; - } - - // Chat (or other things?!) special code - else { - specialMAM = '
'; - specialAttributes = ' data-type="chat"'; - specialAvatar = '
'; - specialName = '

'; - specialCode = '
' + specialMAM + '
'; - specialLink = '' + - '' + - ''; - specialStyle = ' style="display: none;"'; - specialDisabled = ''; - } - - // Not a groupchat private chat, we can use the buddy add icon - if((type == 'chat') || (type == 'groupchat')) { - var addTitle; - - if(type == 'chat') - addTitle = Common._e("Add this contact to your friends"); - else - addTitle = Common._e("Add this groupchat to your favorites"); - - specialLink += ''; - } - - // IE DOM parsing bug fix - var specialStylePicker = '
' + - '' + - '
'; - - if((BrowserDetect.browser == 'Explorer') && (BrowserDetect.version < 9)) - specialStylePicker = ''; - + var chat_args = self._generateChatCode(type, id); + // Append the chat HTML code $('#page-engine').append( - '
' + + '
' + '
' + - specialAvatar + + chat_args.avatar + '
' + '

' + nick.htmlEnc() + '

' + - specialName + + chat_args.name + '
' + '
' + - specialCode + + chat_args.code + '
' + '' + - specialStylePicker + + chat_args.style_picker + '
' + '' + @@ -199,35 +434,17 @@ var Chat = (function () { '' + - specialLink + + chat_args.link + '
' + '
' + - '' + + '' + '
' + '
' + '
' ); - // Click event: chat cleaner - $(path + 'tools-clear').click(function() { - self.clean(id); - }); - - // Click event: call (audio) - $(path + 'tools-jingle-audio').click(function() { - Jingle.start(xid, 'audio'); - }); - - // Click event: call (video) - $(path + 'tools-jingle-video').click(function() { - Jingle.start(xid, 'video'); - }); - - // Click event: user-infos - $(path + 'tools-infos').click(function() { - UserInfos.open(xid); - }); + self._generateEvents(path, id, xid); } catch(e) { Console.error('Chat.generate', e); } @@ -251,26 +468,32 @@ var Chat = (function () { var chat_switch = '#page-switch .'; // Special code - var specialClass = ' unavailable'; + var special_class = ' unavailable'; var show_close = true; // Groupchat if(type == 'groupchat') { - specialClass = ' groupchat-default'; + special_class = ' groupchat-default'; - if(Utils.isAnonymous() && (xid == Common.generateXID(ANONYMOUS_ROOM, 'groupchat'))) + if(Utils.isAnonymous() && (xid == Common.generateXID(ANONYMOUS_ROOM, 'groupchat'))) { show_close = false; + } } // Generate the HTML code var html = '
' + - '
' + - - '
' + nick.htmlEnc() + '
'; + '
' + + + '
' + nick.htmlEnc() + '
'; // Show the close button if not MUC and not anonymous - if(show_close) - html += '
x
'; + if(show_close) { + html += '
' + + 'x' + + '
'; + } // Close the HTML html += '
'; @@ -353,7 +576,7 @@ var Chat = (function () { // Create the chat switcher self.generateSwitch(type, hash, xid, nick); - // If the user is not in our roster + // Is this a chat? if(type == 'chat') { // MAM? Get archives from there! if(Features.enabledMAM()) { @@ -373,32 +596,43 @@ var Chat = (function () { var friend_hash = hex_md5(xid); // Add chat history HTML - $('#' + hash + ' .content').append(chat_history); + var path_sel = $('#' + hash); + + path_sel.find('.content').append(chat_history); // Filter old groups & messages - $('#' + hash + ' .one-group[data-type="user-message"]').addClass('from-history').attr('data-type', 'old-message'); - $('#' + hash + ' .user-message').removeClass('user-message').addClass('old-message'); + var one_group_sel = path_sel.find('.one-group'); + one_group_sel.filter('[data-type="user-message"]').addClass('from-history').attr('data-type', 'old-message'); + path_sel.find('.user-message').removeClass('user-message').addClass('old-message'); // Regenerate user names - $('#' + hash + ' .one-group.' + my_hash + ' b.name').text(Name.getBuddy(Common.getXID())); - $('#' + hash + ' .one-group.' + friend_hash + ' b.name').text(Name.getBuddy(xid)); + one_group_sel.filter('.' + my_hash + ' b.name').text( + Name.getBuddy(Common.getXID()) + ); + + one_group_sel.filter('.' + friend_hash + ' b.name').text( + Name.getBuddy(xid) + ); // Regenerate group dates - $('#' + hash + ' .one-group').each(function() { - var current_stamp = parseInt($(this).attr('data-stamp')); + one_group_sel.each(function() { + var current_stamp = parseInt($(this).attr('data-stamp'), 10); $(this).find('span.date').text(DateUtils.relative(current_stamp)); }); // Regenerate avatars - if(Common.exists('#' + hash + ' .one-group.' + my_hash + ' .avatar-container')) + if(Common.exists('#' + hash + ' .one-group.' + my_hash + ' .avatar-container')) { Avatar.get(Common.getXID(), 'cache', 'true', 'forget'); - if(Common.exists('#' + hash + ' .one-group.' + friend_hash + ' .avatar-container')) + } + + if(Common.exists('#' + hash + ' .one-group.' + friend_hash + ' .avatar-container')) { Avatar.get(xid, 'cache', 'true', 'forget'); + } } } // Add button - if(!Roster.isFriend(xid)) + if(!Roster.isFriend(xid)) { $('#' + hash + ' .tools-add').click(function() { // Hide the icon (to tell the user all is okay) $(this).hide(); @@ -406,6 +640,7 @@ var Chat = (function () { // Send the subscribe request Roster.addThisContact(xid, nick); }).show(); + } } // We catch the user's informations (like this avatar, vcard, and so on...) @@ -415,81 +650,12 @@ var Chat = (function () { Tooltip.icons(xid, hash); // The event handlers - var inputDetect = $('#page-engine #' + hash + ' .message-area'); + var input_sel = $('#page-engine #' + hash + ' .message-area'); + self._createEvents(input_sel, xid, hash); - inputDetect.focus(function() { - // Clean notifications for this chat - Interface.chanCleanNotify(hash); - - // Store focus on this chat! - Interface.chat_focus_hash = hash; - }); - - inputDetect.blur(function() { - // Reset storage about focus on this chat! - if(Interface.chat_focus_hash == hash) - Interface.chat_focus_hash = null; - }); - - inputDetect.keypress(function(e) { - // Enter key - if(e.keyCode == 13) { - // Add a new line - if(e.shiftKey || e.ctrlKey) { - inputDetect.val(inputDetect.val() + '\n'); - } else { - // Send the message - Message.send(hash, 'chat'); - - // Reset the composing database entry - DataStore.setDB(Connection.desktop_hash, 'chatstate', xid, 'off'); - } - - return false; - } - }); - - // Scroll in chat content - $('#page-engine #' + hash + ' .content').scroll(function() { - var self = this; - - if(Features.enabledMAM() && !(xid in MAM.map_pending)) { - var has_state = xid in MAM.map_states; - var rsm_count = has_state ? MAM.map_states[xid].rsm.count : 1; - var rsm_before = has_state ? MAM.map_states[xid].rsm.first : ''; - - // Request more archives? - if(rsm_count > 0 && $(this).scrollTop() < MAM.SCROLL_THRESHOLD) { - var was_scroll_top = $(self).scrollTop() <= 32; - var wait_mam = $('#' + hash).find('.wait-mam'); - wait_mam.show(); - - MAM.getArchives({ - 'with': xid - }, { - 'max': MAM.REQ_MAX, - 'before': rsm_before - }, function() { - var wait_mam_height = was_scroll_top ? 0 : wait_mam.height(); - wait_mam.hide(); - - // Restore scroll? - if($(self).scrollTop() < MAM.SCROLL_THRESHOLD) { - var sel_mam_chunk = $(self).find('.mam-chunk:first'); - - var cont_padding_top = parseInt($(self).css('padding-top').replace(/[^-\d\.]/g, '')); - var cont_one_group_margin_bottom = parseInt(sel_mam_chunk.find('.one-group:last').css('margin-bottom').replace(/[^-\d\.]/g, '')); - var cont_mam_chunk_height = sel_mam_chunk.height(); - - $(self).scrollTop(wait_mam_height + cont_padding_top + cont_one_group_margin_bottom + cont_mam_chunk_height); - } - }); - } - } - }); - - // Chatstate events - ChatState.events(inputDetect, xid, hash, 'chat'); + // Input events + ChatState.events(input_sel, xid, hash, 'chat'); + Markers.events(input_sel, xid, hash, 'chat'); } catch(e) { Console.error('Chat.create', e); } diff --git a/source/app/javascripts/chatstate.js b/source/app/javascripts/chatstate.js index ad55101..6459103 100644 --- a/source/app/javascripts/chatstate.js +++ b/source/app/javascripts/chatstate.js @@ -36,8 +36,9 @@ var ChatState = (function () { // If the friend client supports chatstates and is online if((user_type == 'groupchat') || ((user_type == 'chat') && $('#' + hash + ' .message-area').attr('data-chatstates') && !Common.exists('#page-switch .' + hash + ' .unavailable'))) { // Already sent? - if(DataStore.getDB(Connection.desktop_hash, 'currentchatstate', xid) == state) + if(DataStore.getDB(Connection.desktop_hash, 'currentchatstate', xid) == state) { return; + } // Write the state DataStore.setDB(Connection.desktop_hash, 'currentchatstate', xid, state); @@ -48,7 +49,9 @@ var ChatState = (function () { aMsg.setType(user_type); // Append the chatstate node - aMsg.appendNode(state, {'xmlns': NS_CHATSTATES}); + aMsg.appendNode(state, { + 'xmlns': NS_CHATSTATES + }); // Send this! con.send(aMsg); @@ -76,8 +79,9 @@ var ChatState = (function () { self.reset(hash, type); // "gone" state not allowed - if(state != 'gone') + if(state != 'gone') { $('#page-engine .page-engine-chan .user.' + hash).addClass(state); + } } // Chat @@ -125,7 +129,9 @@ var ChatState = (function () { $('#' + hash + ' .chatstate').remove(); // We create the chatstate - $('#' + hash + ' .content').after('
' + text + '
'); + $('#' + hash + ' .content').after( + '
' + text + '
' + ); } } catch(e) { Console.error('ChatState.display', e); @@ -147,10 +153,11 @@ var ChatState = (function () { // Define the selector var selector; - if(type == 'groupchat') + if(type == 'groupchat') { selector = $('#page-engine .page-engine-chan .user.' + hash); - else + } else { selector = $('#page-switch .' + hash + ' .name'); + } // Reset! selector.removeClass('active composing paused inactive gone'); @@ -202,30 +209,33 @@ var ChatState = (function () { target.focus(function() { // Not needed - if(target.is(':disabled')) + if(target.is(':disabled')) { return; + } // Something was written, user started writing again - if($(this).val()) + if($(this).val()) { self.send('composing', xid, hash); + } // Chat only: Nothing in the input, user is active - else if(type == 'chat') + else if(type == 'chat') { self.send('active', xid, hash); + } }); target.blur(function() { // Not needed - if(target.is(':disabled')) + if(target.is(':disabled')) { return; - - // Something was written, user paused - if($(this).val()) - self.send('paused', xid, hash); + } - // Chat only: Nothing in the input, user is inactive - else if(type == 'chat') + // Something was written, user paused + if($(this).val()) { + self.send('paused', xid, hash); + } else if(type == 'chat') { self.send('inactive', xid, hash); + } }); } catch(e) { Console.error('ChatState.events', e); diff --git a/source/app/javascripts/common.js b/source/app/javascripts/common.js index c6047a9..2b340a5 100644 --- a/source/app/javascripts/common.js +++ b/source/app/javascripts/common.js @@ -20,6 +20,10 @@ var Common = (function () { var self = {}; + /* Constants */ + self.R_DOMAIN_NAME = /^(([a-zA-Z0-9-\.]+)\.)?[a-zA-Z0-9][a-zA-Z0-9-]{1,61}[a-zA-Z0-9]\.[a-zA-Z]{2,}$/i; + + /** * Checks if an element exists in the DOM * @public @@ -109,6 +113,29 @@ var Common = (function () { }; + /** + * Matches a domain name + * @public + * @param {string} xid + * @return {boolean} + */ + self.isDomain = function(xid) { + + is_domain = false; + + try { + if(xid.match(self.R_DOMAIN_NAME)) { + is_domain = true; + } + } catch(e) { + Console.error('Common.isDomain', e); + } finally { + return is_domain; + } + + }; + + /** * Generates the good XID * @public @@ -120,20 +147,21 @@ var Common = (function () { try { // XID needs to be transformed - // .. and made lowercase (uncertain though this is the right place...) xid = xid.toLowerCase(); - if(xid && (xid.indexOf('@') == -1)) { - // Groupchat - if(type == 'groupchat') + if(xid && (xid.indexOf('@') === -1)) { + // Groupchat XID + if(type == 'groupchat') { return xid + '@' + HOST_MUC; + } - // One-to-one chat - if(xid.indexOf('.') == -1) - return xid + '@' + HOST_MAIN; + // Gateway XID + if(self.isDomain(xid) === true) { + return xid; + } - // It might be a gateway? - return xid; + // User XID + return xid + '@' + HOST_MAIN; } // Nothing special (yet bare XID) @@ -190,14 +218,16 @@ var Common = (function () { self.strAfterLast = function(given_char, str) { try { - if(!given_char || !str) + if(!given_char || !str) { return ''; + } var char_index = str.lastIndexOf(given_char); var str_return = str; - if(char_index >= 0) + if(char_index >= 0) { str_return = str.substr(char_index + 1); + } return str_return; } catch(e) { @@ -223,10 +253,11 @@ var Common = (function () { // We split if necessary the string if(index !== -1) { - if(i === 0) + if(i === 0) { toStr = toStr.substr(0, index); - else + } else { toStr = toStr.substr(index + 1); + } } // We return the value @@ -309,8 +340,9 @@ var Common = (function () { // Spec: http://tools.ietf.org/html/rfc6122#appendix-A try { - if(!node) + if(!node) { return node; + } // Remove prohibited chars var prohibited_chars = ['"', '&', '\'', '/', ':', '<', '>', '@']; @@ -347,6 +379,40 @@ var Common = (function () { }; + /** + * Escapes quotes in a string + * @public + * @param {string} str + * @return {string} + */ + self.escapeQuotes = function(str) { + + try { + return escape(self.encodeQuotes(str)); + } catch(e) { + Console.error('Common.escapeQuotes', e); + } + + }; + + + /** + * Unescapes quotes in a string + * @public + * @param {string} str + * @return {string} + */ + self.unescapeQuotes = function(str) { + + try { + return unescape(str); + } catch(e) { + Console.error('Common.unescapeQuotes', e); + } + + }; + + /** * Gets the bare XID from a XID * @public @@ -360,8 +426,8 @@ var Common = (function () { xid = self.cutResource(xid); // Launch nodeprep - if(xid.indexOf('@') != -1) { - xid = self.nodeprep(self.getXIDNick(xid)) + '@' + self.getXIDHost(xid); + if(xid.indexOf('@') !== -1) { + xid = self.nodeprep(self.getXIDNick(xid, true)) + '@' + self.getXIDHost(xid); } return xid; @@ -386,8 +452,9 @@ var Common = (function () { var resource = self.thisResource(xid); // Any resource? - if(resource) + if(resource) { full += '/' + resource; + } return full; } catch(e) { @@ -401,14 +468,18 @@ var Common = (function () { * Gets the nick from a XID * @public * @param {string} aXID + * @param {boolean} raw_explode * @return {string} */ - self.getXIDNick = function(aXID) { + self.getXIDNick = function(aXID, raw_explode) { try { - // Gateway nick? - if(aXID.match(/\\40/)) - return self.explodeThis('\\40', aXID, 0); + if(raw_explode !== true) { + // Gateway nick? + if(aXID.match(/\\40/)) { + return self.explodeThis('\\40', aXID, 0); + } + } return self.explodeThis('@', aXID, 0); } catch(e) { @@ -549,7 +620,7 @@ var Common = (function () { is_gateway = true; try { - if(xid.indexOf('@') != -1) { + if(xid.indexOf('@') !== -1) { is_gateway = false; } } catch(e) { @@ -618,12 +689,14 @@ var Common = (function () { try { // Negative number (without first 0) - if(i > -10 && i < 0) + if(i > -10 && i < 0) { return '-0' + (i * -1); + } // Positive number (without first 0) - if(i < 10 && i >= 0) + if(i < 10 && i >= 0) { return '0' + i; + } // All is okay return i; @@ -643,23 +716,31 @@ var Common = (function () { */ self.escapeRegex = function(query) { - if (query instanceof Array) { - var result = new Array(query.length); - for(i=0; i' + - Common._e("You have been registered, here is your XMPP address:") + ' ' + username.htmlEnc() + '@' + domain.htmlEnc() + ' - ' + Common._e("Login") + '' + + Common._e("You have been registered, here is your XMPP address:") + + ' ' + username.htmlEnc() + '@' + domain.htmlEnc() + ' - ' + + '' + Common._e("Login") + '' + '
' ); @@ -179,102 +380,9 @@ var Connection = (function () { }); if((REGISTER_API == 'on') && (domain == HOST_MAIN) && captcha) { - // Show the waiting image - Interface.showGeneralWait(); - - // Change the page title - Interface.title('wait'); - - // Send request - $.post('./server/register.php', {username: username, domain: domain, password: pass, captcha: captcha}, function(data) { - // Error registering - Interface.removeGeneralWait(); - Interface.title('home'); - - // In all case, update CAPTCHA - $('#home img.captcha_img').attr('src', './server/captcha.php?id=' + genID()); - $('#home input.captcha').val(''); - - // Registration okay - if($(data).find('query status').text() == '1') { - self.handleRegistered(); - } else { - // Show error message - var error_message = ''; - - switch($(data).find('query message').text()) { - case 'CAPTCHA Not Matching': - error_message = Common._e("The security code you entered is invalid. Please retry with another one."); - - $('#home input.captcha').focus(); - - break; - - case 'Username Unavailable': - error_message = Common._e("The username you picked is not available. Please try another one."); - - $('#home input.nick').focus(); - - break; - - default: - error_message = Common._e("There was an error registering your account. Please retry."); - - break; - } - - if(error_message) - Errors.show('', error_message, ''); - } - }); + self._doRegisterAPI(username, domain, pass, captcha); } else { - try { - oArgs = {}; - - if(Common.hasWebSocket()) { - // WebSocket supported & configured - con = new JSJaCWebSocketConnection({ - httpbase: HOST_WEBSOCKET - }); - } else { - var httpbase = (HOST_BOSH_MAIN || HOST_BOSH); - - // Check BOSH origin - BOSH_SAME_ORIGIN = Origin.isSame(httpbase); - - // We create the new http-binding connection - con = new JSJaCHttpBindingConnection({ - httpbase: httpbase - }); - } - - // We setup the connection ! - con.registerHandler('onconnect', self.handleRegistered); - con.registerHandler('onerror', Errors.handle); - - // We retrieve what the user typed in the register inputs - oArgs = {}; - oArgs.domain = $.trim(domain); - oArgs.username = $.trim(username); - oArgs.resource = JAPPIX_RESOURCE + ' Register (' + (new Date()).getTime() + ')'; - oArgs.pass = pass; - oArgs.register = true; - oArgs.secure = true; - oArgs.xmllang = XML_LANG; - - con.connect(oArgs); - - // Show the waiting image - Interface.showGeneralWait(); - - // Change the page title - Interface.title('wait'); - } - - catch(e) { - // Logs errors - Console.error('doRegister', e); - } + self._doRegisterInBand(username, domain, pass); } } catch(e) { Console.error('Connection.doRegister', e); @@ -295,30 +403,28 @@ var Connection = (function () { try { Console.info('Trying to login anonymously...'); - var aPath = '#home .anonymouser '; - var room = $(aPath + '.room').val(); - var nick = $(aPath + '.nick').val(); + var path_sel = $('#home .anonymouser'); + var room = path_sel.find('.room').val(); + var nick = path_sel.find('.nick').val(); - // If the form is correctly completed + // Form correctly completed? if(room && nick) { // We remove the not completed class to avoid problems $('#home .anonymouser input').removeClass('please-complete'); // Redirect the user to the anonymous room window.location.href = JAPPIX_LOCATION + '?r=' + room + '&n=' + nick; - } - - // We check if the form is entirely completed - else { - $(aPath + 'input[type="text"]').each(function() { - var select = $(this); + } else { + path_sel.find('input[type="text"]').each(function() { + var this_sel = $(this); - if(!select.val()) + if(!this_sel.val()) { $(document).oneTime(10, function() { - select.addClass('please-complete').focus(); + this_sel.addClass('please-complete').focus(); }); - else - select.removeClass('please-complete'); + } else { + this_sel.removeClass('please-complete'); + } }); } } catch(e) { @@ -355,7 +461,7 @@ var Connection = (function () { Interface.removeGeneralWait(); // Init Jingle - Jingle.init(); + Call.init(); } catch(e) { Console.error('Connection.handleConnected', e); } @@ -374,8 +480,9 @@ var Connection = (function () { // Not resumed? if(!self.resume) { // Remember the session? - if(DataStore.getDB(self.desktop_hash, 'remember', 'session')) + if(DataStore.getDB(self.desktop_hash, 'remember', 'session')) { DataStore.setPersistent('global', 'session', 1, self.current_session); + } // We show the chatting app. Talk.create(); @@ -431,24 +538,32 @@ var Connection = (function () { * Setups the normal connection * @public * @param {object} con - * @param {object} oExtend + * @param {object} extend_obj * @return {undefined} */ - self.setupCon = function(con, oExtend) { + self.setupCon = function(con, extend_obj) { try { - // Setup connection handlers - con.registerHandler('message', Message.handle); - con.registerHandler('presence', Presence.handle); - con.registerHandler('iq', IQ.handle); - con.registerHandler('onconnect', self.handleConnected); - con.registerHandler('onerror', Errors.handle); - con.registerHandler('ondisconnect', self.handleDisconnected); - + var connection_handlers = { + 'message': Message.handle, + 'presence': Presence.handle, + 'iq': IQ.handle, + 'onconnect': self.handleConnected, + 'onerror': Errors.handle, + 'ondisconnect': self.handleDisconnected + }; + + for(var cur_handler in connection_handlers) { + con.registerHandler( + cur_handler, + connection_handlers[cur_handler] + ); + } + // Extended handlers - oExtend = oExtend || {}; + extend_obj = extend_obj || {}; - jQuery.each(oExtend, function(keywd,funct) { + jQuery.each(extend_obj, function(keywd,funct) { con.registerHandler(keywd, funct); }); } catch(e) { @@ -559,51 +674,25 @@ var Connection = (function () { // Create the HTML code var html = '
' + - '
' + - Common._e("Due to a network issue, you were disconnected. What do you want to do now?"); + '
' + + Common._e("Due to a network issue, you were disconnected. What do you want to do now?"); // Can we cancel reconnection? - if(mode == 'normal') + if(mode == 'normal') { html += '' + Common._e("Cancel") + ''; + } html += '' + Common._e("Reconnect") + '' + - '
'; + '
'; // Append the code $('body').append(html); - // Click events - if(mode == 'normal') - $('#reconnect a.finish.cancel').click(function() { - return self.cancelReconnect(); - }); + // Attach events + self._eventsReconnect(mode); - $('#reconnect a.finish.reconnect').click(function() { - return self.acceptReconnect(mode); - }); - - // Try to reconnect automatically after a while - if(self.reconnect_try < 5) - self.reconnect_timer = 5 + (5 * self.reconnect_try); - else - self.reconnect_timer = 120; - - // Change the try number - self.reconnect_try++; - - // Fire the event! - $('#reconnect a.finish.reconnect').everyTime('1s', function() { - // We can reconnect! - if(self.reconnect_timer === 0) - return self.acceptReconnect(mode); - - // Button text - if(self.reconnect_timer <= 10) - $(this).text(Common._e("Reconnect") + ' (' + self.reconnect_timer + ')'); - - // Remove 1 second - self.reconnect_timer--; - }); + // Schedule next reconnect + self._scheduleReconnect(mode); // Page title Interface.updateTitle(); @@ -634,9 +723,11 @@ var Connection = (function () { // Reset some various stuffs var groupchats = '#page-engine .page-engine-chan[data-type="groupchat"]'; - $(groupchats + ' .list .role').hide(); - $(groupchats + ' .one-group, ' + groupchats + ' .list .user').remove(); - $(groupchats).attr('data-initial', 'false'); + var groupchats_sel = $(groupchats); + + groupchats_sel.find('.list .role').hide(); + groupchats_sel.find('.one-group, ' + groupchats + ' .list .user').remove(); + groupchats_sel.attr('data-initial', 'false'); // Stop the timer $('#reconnect a.finish.reconnect').stopTime(); @@ -645,10 +736,11 @@ var Connection = (function () { $('#reconnect').remove(); // Try to login again - if(mode == 'normal') + if(mode == 'normal') { self.loginFromSession(Common.XMLFromString(self.current_session)); - else if(mode == 'anonymous') + } else if(mode == 'anonymous') { Anonymous.login(HOST_ANONYMOUS); + } } catch(e) { Console.error('Connection.acceptReconnect', e); } finally { @@ -817,7 +909,14 @@ var Connection = (function () { try { // Generate a session XML to be stored - session_xml = 'true' + lServer.htmlEnc() + '' + lNick.htmlEnc() + '' + lResource.htmlEnc() + '' + lPass.htmlEnc() + '' + lPriority.htmlEnc() + ''; + session_xml = '' + + 'true' + + '' + lServer.htmlEnc() + '' + + '' + lNick.htmlEnc() + '' + + '' + lResource.htmlEnc() + '' + + '' + lPass.htmlEnc() + '' + + '' + lPriority.htmlEnc() + '' + + ''; // Save the session parameters (for reconnect if network issue) self.current_session = session_xml; @@ -848,8 +947,9 @@ var Connection = (function () { $(window).bind('beforeunload', Connection.terminate); // Nothing to do when anonymous! - if(Utils.isAnonymous()) + if(Utils.isAnonymous()) { return; + } // Connection params submitted in URL? if(XMPPLinks.links_var.u && XMPPLinks.links_var.q) { @@ -865,7 +965,15 @@ var Connection = (function () { // Must store session? if(XMPPLinks.links_var.h && (XMPPLinks.links_var.h === '1')) { // Store session - var session_xml = self.storeSession(login_nick, login_server, login_pwd, login_resource, login_priority, true); + var session_xml = self.storeSession( + login_nick, + login_server, + login_pwd, + login_resource, + login_priority, + true + ); + DataStore.setPersistent('global', 'session', 1, session_xml); // Redirect to a clean URL @@ -900,10 +1008,7 @@ var Connection = (function () { self.loginFromSession(session); Console.info('Saved session found, resuming it...'); - } - - // Not connected, maybe a XMPP link is submitted? - else if((parent.location.hash != '#OK') && XMPPLinks.links_var.x) { + } else if((parent.location.hash != '#OK') && XMPPLinks.links_var.x) { Home.change('loginer'); Console.info('A XMPP link is set, switch to login page.'); diff --git a/source/app/javascripts/constants.js b/source/app/javascripts/constants.js index 1e55ac5..be9241e 100644 --- a/source/app/javascripts/constants.js +++ b/source/app/javascripts/constants.js @@ -11,103 +11,109 @@ Authors: Stefan Strigler, Valérian Saliou, Kloadut, Maranda */ // XMPP XMLNS attributes -var NS_PROTOCOL = 'http://jabber.org/protocol/'; -var NS_FEATURES = 'http://jabber.org/features/'; -var NS_CLIENT = 'jabber:client'; -var NS_IQ = 'jabber:iq:'; -var NS_X = 'jabber:x:'; -var NS_IETF = 'urn:ietf:params:xml:ns:'; -var NS_IETF_XMPP = NS_IETF + 'xmpp-'; -var NS_XMPP = 'urn:xmpp:'; +var NS_PROTOCOL = 'http://jabber.org/protocol/'; +var NS_FEATURES = 'http://jabber.org/features/'; +var NS_CLIENT = 'jabber:client'; +var NS_IQ = 'jabber:iq:'; +var NS_X = 'jabber:x:'; +var NS_IETF = 'urn:ietf:params:xml:ns:'; +var NS_IETF_XMPP = NS_IETF + 'xmpp-'; +var NS_XMPP = 'urn:xmpp:'; -var NS_STORAGE = 'storage:'; -var NS_BOOKMARKS = NS_STORAGE + 'bookmarks'; -var NS_ROSTERNOTES = NS_STORAGE + 'rosternotes'; +var NS_STORAGE = 'storage:'; +var NS_BOOKMARKS = NS_STORAGE + 'bookmarks'; +var NS_ROSTERNOTES = NS_STORAGE + 'rosternotes'; -var NS_JAPPIX = 'jappix:'; -var NS_INBOX = NS_JAPPIX + 'inbox'; -var NS_OPTIONS = NS_JAPPIX + 'options'; +var NS_JAPPIX = 'jappix:'; +var NS_INBOX = NS_JAPPIX + 'inbox'; +var NS_OPTIONS = NS_JAPPIX + 'options'; -var NS_DISCO_ITEMS = NS_PROTOCOL + 'disco#items'; -var NS_DISCO_INFO = NS_PROTOCOL + 'disco#info'; -var NS_VCARD = 'vcard-temp'; -var NS_VCARD_P = NS_VCARD + ':x:update'; -var NS_IETF_VCARD4 = NS_IETF + 'vcard-4.0'; -var NS_XMPP_VCARD4 = NS_XMPP + 'vcard4'; -var NS_URN_ADATA = NS_XMPP + 'avatar:data'; -var NS_URN_AMETA = NS_XMPP + 'avatar:metadata'; -var NS_AUTH = NS_IQ + 'auth'; -var NS_AUTH_ERROR = NS_IQ + 'auth:error'; -var NS_REGISTER = NS_IQ + 'register'; -var NS_SEARCH = NS_IQ + 'search'; -var NS_ROSTER = NS_IQ + 'roster'; -var NS_PRIVACY = NS_IQ + 'privacy'; -var NS_PRIVATE = NS_IQ + 'private'; -var NS_VERSION = NS_IQ + 'version'; -var NS_TIME = NS_IQ + 'time'; -var NS_LAST = NS_IQ + 'last'; -var NS_IQDATA = NS_IQ + 'data'; -var NS_XDATA = NS_X + 'data'; -var NS_IQOOB = NS_IQ + 'oob'; -var NS_XOOB = NS_X + 'oob'; -var NS_DELAY = NS_X + 'delay'; -var NS_EXPIRE = NS_X + 'expire'; -var NS_EVENT = NS_X + 'event'; -var NS_XCONFERENCE = NS_X + 'conference'; -var NS_STATS = NS_PROTOCOL + 'stats'; -var NS_MUC = NS_PROTOCOL + 'muc'; -var NS_MUC_USER = NS_MUC + '#user'; -var NS_MUC_ADMIN = NS_MUC + '#admin'; -var NS_MUC_OWNER = NS_MUC + '#owner'; -var NS_MUC_CONFIG = NS_MUC + '#roomconfig'; -var NS_PUBSUB = NS_PROTOCOL + 'pubsub'; -var NS_PUBSUB_EVENT = NS_PUBSUB + '#event'; -var NS_PUBSUB_OWNER = NS_PUBSUB + '#owner'; -var NS_PUBSUB_NMI = NS_PUBSUB + '#node-meta-info'; -var NS_PUBSUB_NC = NS_PUBSUB + '#node_config'; -var NS_PUBSUB_CN = NS_PUBSUB + '#config-node'; -var NS_PUBSUB_RI = NS_PUBSUB + '#retrieve-items'; -var NS_COMMANDS = NS_PROTOCOL + 'commands'; -var NS_BOSH = NS_PROTOCOL + 'httpbind'; +var NS_DISCO_ITEMS = NS_PROTOCOL + 'disco#items'; +var NS_DISCO_INFO = NS_PROTOCOL + 'disco#info'; +var NS_VCARD = 'vcard-temp'; +var NS_VCARD_P = NS_VCARD + ':x:update'; +var NS_IETF_VCARD4 = NS_IETF + 'vcard-4.0'; +var NS_XMPP_VCARD4 = NS_XMPP + 'vcard4'; +var NS_URN_ADATA = NS_XMPP + 'avatar:data'; +var NS_URN_AMETA = NS_XMPP + 'avatar:metadata'; +var NS_AUTH = NS_IQ + 'auth'; +var NS_AUTH_ERROR = NS_IQ + 'auth:error'; +var NS_REGISTER = NS_IQ + 'register'; +var NS_SEARCH = NS_IQ + 'search'; +var NS_ROSTER = NS_IQ + 'roster'; +var NS_PRIVACY = NS_IQ + 'privacy'; +var NS_PRIVATE = NS_IQ + 'private'; +var NS_VERSION = NS_IQ + 'version'; +var NS_TIME = NS_IQ + 'time'; +var NS_LAST = NS_IQ + 'last'; +var NS_IQDATA = NS_IQ + 'data'; +var NS_XDATA = NS_X + 'data'; +var NS_IQOOB = NS_IQ + 'oob'; +var NS_XOOB = NS_X + 'oob'; +var NS_DELAY = NS_X + 'delay'; +var NS_EXPIRE = NS_X + 'expire'; +var NS_EVENT = NS_X + 'event'; +var NS_XCONFERENCE = NS_X + 'conference'; +var NS_STATS = NS_PROTOCOL + 'stats'; +var NS_MUC = NS_PROTOCOL + 'muc'; +var NS_MUC_USER = NS_MUC + '#user'; +var NS_MUC_ADMIN = NS_MUC + '#admin'; +var NS_MUC_OWNER = NS_MUC + '#owner'; +var NS_MUC_CONFIG = NS_MUC + '#roomconfig'; +var NS_PUBSUB = NS_PROTOCOL + 'pubsub'; +var NS_PUBSUB_EVENT = NS_PUBSUB + '#event'; +var NS_PUBSUB_OWNER = NS_PUBSUB + '#owner'; +var NS_PUBSUB_NMI = NS_PUBSUB + '#node-meta-info'; +var NS_PUBSUB_NC = NS_PUBSUB + '#node_config'; +var NS_PUBSUB_CN = NS_PUBSUB + '#config-node'; +var NS_PUBSUB_RI = NS_PUBSUB + '#retrieve-items'; +var NS_COMMANDS = NS_PROTOCOL + 'commands'; +var NS_BOSH = NS_PROTOCOL + 'httpbind'; var NS_STREAM = 'http://etherx.jabber.org/streams'; -var NS_URN_TIME = NS_XMPP + 'time'; -var NS_URN_PING = NS_XMPP + 'ping'; -var NS_URN_MBLOG = NS_XMPP + 'microblog:0'; -var NS_URN_INBOX = NS_XMPP + 'inbox'; -var NS_URN_FORWARD = NS_XMPP + 'forward:0'; -var NS_URN_MAM = NS_XMPP + 'mam:tmp'; -var NS_URN_DELAY = NS_XMPP + 'delay'; -var NS_URN_RECEIPTS = NS_XMPP + 'receipts'; -var NS_URN_CARBONS = NS_XMPP + 'carbons:2'; -var NS_RSM = NS_PROTOCOL + 'rsm'; -var NS_IPV6 = 'ipv6'; -var NS_XHTML = 'http://www.w3.org/1999/xhtml'; -var NS_XHTML_IM = NS_PROTOCOL + 'xhtml-im'; -var NS_CHATSTATES = NS_PROTOCOL + 'chatstates'; -var NS_HTTP_AUTH = NS_PROTOCOL + 'http-auth'; -var NS_ROSTERX = NS_PROTOCOL + 'rosterx'; -var NS_MOOD = NS_PROTOCOL + 'mood'; -var NS_ACTIVITY = NS_PROTOCOL + 'activity'; -var NS_TUNE = NS_PROTOCOL + 'tune'; -var NS_GEOLOC = NS_PROTOCOL + 'geoloc'; -var NS_NICK = NS_PROTOCOL + 'nick'; -var NS_NOTIFY = '+notify'; -var NS_CAPS = NS_PROTOCOL + 'caps'; -var NS_ATOM = 'http://www.w3.org/2005/Atom'; +var NS_URN_TIME = NS_XMPP + 'time'; +var NS_URN_PING = NS_XMPP + 'ping'; +var NS_URN_MBLOG = NS_XMPP + 'microblog:0'; +var NS_URN_INBOX = NS_XMPP + 'inbox'; +var NS_URN_FORWARD = NS_XMPP + 'forward:0'; +var NS_URN_MAM = NS_XMPP + 'mam:tmp'; +var NS_URN_DELAY = NS_XMPP + 'delay'; +var NS_URN_RECEIPTS = NS_XMPP + 'receipts'; +var NS_URN_CARBONS = NS_XMPP + 'carbons:2'; +var NS_URN_CORRECT = NS_XMPP + 'message-correct:0'; +var NS_URN_IDLE = NS_XMPP + 'idle:1'; +var NS_URN_REACH = NS_XMPP + 'reach:0'; +var NS_URN_MARKERS = NS_XMPP + 'chat-markers:0'; +var NS_URN_ATTENTION = NS_XMPP + 'attention:0'; +var NS_URN_HINTS = NS_XMPP + 'hints'; +var NS_RSM = NS_PROTOCOL + 'rsm'; +var NS_IPV6 = 'ipv6'; +var NS_XHTML = 'http://www.w3.org/1999/xhtml'; +var NS_XHTML_IM = NS_PROTOCOL + 'xhtml-im'; +var NS_CHATSTATES = NS_PROTOCOL + 'chatstates'; +var NS_HTTP_AUTH = NS_PROTOCOL + 'http-auth'; +var NS_ROSTERX = NS_PROTOCOL + 'rosterx'; +var NS_MOOD = NS_PROTOCOL + 'mood'; +var NS_ACTIVITY = NS_PROTOCOL + 'activity'; +var NS_TUNE = NS_PROTOCOL + 'tune'; +var NS_GEOLOC = NS_PROTOCOL + 'geoloc'; +var NS_NICK = NS_PROTOCOL + 'nick'; +var NS_NOTIFY = '+notify'; +var NS_CAPS = NS_PROTOCOL + 'caps'; +var NS_ATOM = 'http://www.w3.org/2005/Atom'; -var NS_STANZAS = NS_IETF_XMPP + 'stanzas'; -var NS_STREAMS = NS_IETF_XMPP + 'streams'; +var NS_STANZAS = NS_IETF_XMPP + 'stanzas'; +var NS_STREAMS = NS_IETF_XMPP + 'streams'; -var NS_TLS = NS_IETF_XMPP + 'tls'; -var NS_SASL = NS_IETF_XMPP + 'sasl'; -var NS_SESSION = NS_IETF_XMPP + 'session'; -var NS_BIND = NS_IETF_XMPP + 'bind'; +var NS_TLS = NS_IETF_XMPP + 'tls'; +var NS_SASL = NS_IETF_XMPP + 'sasl'; +var NS_SESSION = NS_IETF_XMPP + 'session'; +var NS_BIND = NS_IETF_XMPP + 'bind'; -var NS_FEATURE_IQAUTH = NS_FEATURES + 'iq-auth'; +var NS_FEATURE_IQAUTH = NS_FEATURES + 'iq-auth'; var NS_FEATURE_IQREGISTER = NS_FEATURES + 'iq-register'; -var NS_FEATURE_COMPRESS = NS_FEATURES + 'compress'; +var NS_FEATURE_COMPRESS = NS_FEATURES + 'compress'; -var NS_COMPRESS = NS_PROTOCOL + 'compress'; +var NS_COMPRESS = NS_PROTOCOL + 'compress'; var NS_METRONOME_MAM_PURGE = 'http://metronome.im/protocol/mam-purge'; diff --git a/source/app/javascripts/correction.js b/source/app/javascripts/correction.js new file mode 100644 index 0000000..1bfa930 --- /dev/null +++ b/source/app/javascripts/correction.js @@ -0,0 +1,509 @@ +/* + +Jappix - An open social platform +Implementation of XEP-0308: Last Message Correction + +------------------------------------------------- + +License: AGPL +Author: Valérian Saliou + +*/ + +// Bundle +var Correction = (function () { + + /** + * Alias of this + * @private + */ + var self = {}; + + + /** + * @private + * @param {string} xid + * @return {boolean} + */ + self._hasSupport = function(xid) { + + var support = false; + + try { + if($('#' + hex_md5(xid) + '[data-correction="true"]').size()) { + support = true; + } + } catch(e) { + Console.error('Correction._hasSupport', e); + } finally { + return support; + } + + }; + + + /** + * @private + * @param {string} xid + * @return {string} + */ + self._getLastID = function(xid) { + + var last_id = null; + + try { + if(self._hasSupport(xid) === true) { + // Check last message from ourselves + last_id = $('#' + hex_md5(xid) + ' .content .one-line.user-message[data-mode="me"]:last').attr('data-id') || null; + } + } catch(e) { + Console.error('Correction._getLastID', e); + } finally { + return last_id; + } + + }; + + + /** + * @private + * @param {string} xid + * @return {string} + */ + self._getCurrentID = function(xid) { + + var current_id = null; + + try { + if(self._hasSupport(xid) === true) { + // Check the ID of the message being edited (if any) + current_id = $('#' + hex_md5(xid) + ' .message-area').attr('data-correction-current') || null; + } + } catch(e) { + Console.error('Correction._getCurrentID', e); + } finally { + return current_id; + } + + }; + + + /** + * @private + * @param {string} xid + * @return {object} + */ + self._getLastMessage = function(xid) { + + var last_message_val = null; + var last_message_sel = null; + + try { + if(self._hasSupport(xid) === true) { + // Check last message from ourselves + last_message_sel = $('#' + hex_md5(xid) + ' .content .one-line.user-message[data-mode="me"]:last'); + last_message_val = last_message_sel.find('.message-content').text() || null; + + if(last_message_val === null) { + last_message_sel = null; + } + } + } catch(e) { + Console.error('Correction._getLastMessage', e); + } finally { + return { + 'value': last_message_val, + 'selector': last_message_sel + }; + } + + }; + + + /** + * @private + * @param {string} xid + * @param {object} message_sel + * @return {undefined} + */ + self._bindInterface = function(xid, message_sel) { + + try { + // Add message area elements + var text_sel = $('#' + hex_md5(xid) + ' .text'); + + text_sel.addClass('correction-active'); + text_sel.prepend( + '
' + + '' + Common._e("Editing") + '' + + '' + Common._e("Cancel") + '' + + '
' + ); + + // Add message correction marker + message_sel.addClass('correction-active'); + message_sel.find('.correction-label').remove(); + message_sel.find('.correction-edit').hide(); + + message_sel.append( + '' + + Common._e("Being edited") + + '' + ); + + // Bind click events + text_sel.find('.correction-cancel').click(function() { + self.leave(xid); + return false; + }); + } catch(e) { + Console.error('Correction._bindInterface', e); + } + + }; + + + /** + * @private + * @param {string} xid + * @param {object} message_sel + * @return {undefined} + */ + self._unbindInterface = function(xid, message_sel) { + + try { + // Remove message area elements + var text_sel = $('#' + hex_md5(xid) + ' .text'); + text_sel.removeClass('correction-active'); + text_sel.find('.correction-toolbox, .correction-label').remove(); + + if(message_sel.size()) { + message_sel.find('.correction-edit').css('display', ''); + + // Remove message correction marker + message_sel.removeClass('correction-active'); + message_sel.find('.correction-label').remove(); + } + } catch(e) { + Console.error('Correction._unbindInterface', e); + } + + }; + + + /** + * @private + * @param {string} xid + * @param {string} full_xid + * @param {string} type + * @param {string} message_id + * @param {string} message_body + * @return {string} + */ + self._sendStanza = function(xid, full_xid, type, message_id, message_body) { + + var args = { + 'id': null, + 'xhtml': false, + 'message': null + }; + + try { + var hash = hex_md5(xid); + var id = genID(); + args.id = id; + + // Initialize message stanza + var message = new JSJaCMessage(); + args.message = message; + + message.setType(type); + message.setTo(full_xid); + message.setID(id); + + // Generates the correct message depending of the choosen style + var generate_message = Message.generate(message, message_body, hash); + args.xhtml = (generate_message === 'XHTML'); + + // Receipt request + var receipt_request = Receipts.request(hash); + + if(receipt_request) { + message.appendNode('request', {'xmlns': NS_URN_RECEIPTS}); + } + + // Chatstate + message.appendNode('active', {'xmlns': NS_CHATSTATES}); + + if(message_id !== null) { + message.appendNode('replace', { + 'xmlns': NS_URN_CORRECT, + 'id': message_id + }); + } + + con.send(message, Errors.handleReply); + } catch(e) { + Console.error('Correction._sendStanza', e); + } finally { + return args; + } + + }; + + + /** + * Detects correction mode request (in input) + * @public + * @param {string} xid + * @param {object} input_sel + * @return {undefined} + */ + self.detect = function(xid, input_sel) { + + try { + // Other keys + if(input_sel.val().match(/^\/correct/) && self.isIn(xid) === false) { + // Enter correction mode? + self.enter(xid); + } + } catch(e) { + Console.error('Correction.detect', e); + } + + }; + + + /** + * Enter correction mode (for last message) + * @public + * @param {string} xid + * @return {undefined} + */ + self.enter = function(xid) { + + try { + Console.debug('Correction.enter', 'Requested to enter the correction mode with: ' + xid); + + if(self._hasSupport(xid) === true && self.isIn(xid) === false) { + var last_message = self._getLastMessage(xid); + + if(last_message.value && last_message.selector) { + Console.info('Correction.enter', 'Valid last message found for correction mode with: ' + xid); + + var message_area_sel = $('#' + hex_md5(xid) + ' .message-area'); + message_area_sel.val(last_message.value); + + self._bindInterface( + xid, + last_message.selector + ); + + // Focus hack (to get cursor at the end of textarea) + message_area_sel.oneTime(10, function() { + message_area_sel[0].select(); + message_area_sel[0].selectionStart = message_area_sel[0].selectionEnd; + }); + } + } + } catch(e) { + Console.error('Correction.enter', e); + } + + }; + + + /** + * Leave correction mode + * @public + * @param {string} xid + * @return {undefined} + */ + self.leave = function(xid) { + + try { + if(self.isIn(xid) === true) { + var base_sel = $('#' + hex_md5(xid)); + var active_message_sel = base_sel.find('.content .one-line.user-message.correction-active'); + + self._unbindInterface(xid, active_message_sel); + + var message_area_sel = base_sel.find('.message-area'); + message_area_sel.val(''); + message_area_sel.focus(); + } + } catch(e) { + Console.error('Correction.leave', e); + } + + }; + + + /** + * Send corrected message + * @public + * @param {string} xid + * @param {string} type + * @param {string} replacement + * @return {undefined} + */ + self.send = function(xid, type, replacement) { + + try { + if(self._hasSupport(xid) === true) { + if(self._getLastMessage(xid).value != replacement) { + var own_xid = Common.getXID(); + var hash = hex_md5(xid); + var replace_id = self._getLastID(xid); + + Console.info('Correction.send', 'Sending replacement message for: ' + xid + ' "' + replacement + '" with ID: ' + (replace_id || 'none')); + + // Send the stanza itself + var full_xid = Presence.highestPriority(xid) || xid; + var stanza_args = self._sendStanza( + xid, + full_xid, + type, + replace_id, + replacement + ); + + // Update DOM (for chat only) + if(type == 'chat') { + // Filter the xHTML message (for us!) + var replacement_formatted = replacement; + + if(stanza_args.xhtml) { + replacement_formatted = Filter.xhtml(stanza_args.message.getNode()); + } + + // Remove old message + old_message_sel = $('#' + hash + ' .content .one-line.user-message[data-mode="me"]').filter(function() { + return ($(this).attr('data-id') + '') === (replace_id + ''); + }).filter(':last'); + + var edit_count = old_message_sel.attr('data-edit-count') || 0; + edit_count = isNaN(edit_count) ? 0 : parseInt(edit_count, 10); + + if(type == 'chat') { + old_message_sel.remove(); + } + + // Display edited message + Message.display( + 'chat', + own_xid, + hash, + Name.getBuddy(own_xid).htmlEnc(), + replacement_formatted, + DateUtils.getCompleteTime(), + DateUtils.getTimeStamp(), + 'user-message', + !stanza_args.xhtml, + '', + 'me', + stanza_args.id, + undefined, + undefined, + true, + (edit_count + 1) + ); + } + } + } + } catch(e) { + Console.error('Correction.send', e); + } + + }; + + + /** + * Catches a replace message + * @public + * @param {object} message + * @param {string} hash + * @param {string} type + * @return {object} + */ + self.catch = function(message, hash, type) { + + var edit_results = { + 'has_replace': false, + 'is_edited': false, + 'count': 0, + 'next_count': 0 + }; + + try { + var replace_node = message.getChild('replace', NS_URN_CORRECT); + + if(replace_node) { + edit_results.has_replace = true; + var message_edit_id = $(replace_node).attr('id'); + + if(typeof message_edit_id != 'undefined') { + var message_edit_sel = $('#' + hash + ' .one-line.user-message').filter(function() { + var this_sel = $(this); + var is_valid_mode = true; + + if(type == 'chat') { + is_valid_mode = true ? this_sel.attr('data-mode') == 'him' : false; + } + + return is_valid_mode && ((this_sel.attr('data-id') + '') === (message_edit_id + '')); + }).filter(':last'); + + if(message_edit_sel.size()) { + edit_results.count = message_edit_sel.attr('data-edit-count') || 0; + edit_results.count = isNaN(edit_results.count) ? 0 : parseInt(edit_results.count, 10); + edit_results.next_count = edit_results.count + 1; + edit_results.is_edited = true; + + // Empty group? + var message_edit_group_sel = message_edit_sel.parents('.one-group'); + + if(message_edit_group_sel.find('.one-line').size() <= 1) { + message_edit_group_sel.remove(); + } else { + message_edit_sel.remove(); + } + } + } + } + } catch(e) { + Console.error('Correction.catch', e); + } finally { + return edit_results; + } + + }; + + + /** + * Returns whether we are in correction mode or not + * @public + * @param {string} xid + * @return {boolean} + */ + self.isIn = function(xid) { + + var is_in = false; + + try { + is_in = $('#' + hex_md5(xid) + ' .text').hasClass('correction-active'); + } catch(e) { + Console.error('Correction.isIn', e); + } finally { + return is_in; + } + + }; + + + /** + * Return class scope + */ + return self; + +})(); diff --git a/source/app/javascripts/dataform.js b/source/app/javascripts/dataform.js index cd5ceed..62304a0 100644 --- a/source/app/javascripts/dataform.js +++ b/source/app/javascripts/dataform.js @@ -1,1160 +1,1200 @@ -/* - -Jappix - An open social platform -These are the dataform JS scripts for Jappix - -------------------------------------------------- - -License: AGPL -Authors: Valérian Saliou, Maranda - -*/ - -// Bundle -var DataForm = (function () { - - /** - * Alias of this - * @private - */ - var self = {}; - - - /** - * Gets the defined dataform elements - * @public - * @param {string} host - * @param {string} type - * @param {string} node - * @param {string} action - * @param {string} target - * @return {boolean} - */ - self.go = function(host, type, node, action, target) { - - try { - // Clean the current session - self.clean(target); - - // We tell the user that a search has been launched - $('#' + target + ' .wait').show(); - - // If we have enough data - if(host && type) { - // Generate a session ID - var sessionID = Math.round(100000.5 + (((900000.49999) - (100000.5)) * Math.random())); - var id = target + '-' + sessionID + '-' + genID(); - $('.' + target + '-results').attr('data-session', target + '-' + sessionID); - - // We request the service item - var iq = new JSJaCIQ(); - iq.setID(id); - iq.setTo(host); - iq.setType('get'); - - // MUC admin query - if(type == 'muc') { - iq.setQuery(NS_MUC_OWNER); - con.send(iq, self.handleMUC); - } - - // Browse query - else if(type == 'browse') { - var iqQuery = iq.setQuery(NS_DISCO_ITEMS); - - if(node) - iqQuery.setAttribute('node', node); - - con.send(iq, self.handleBrowse); - } - - // Command - else if(type == 'command') { - var items; - - if(node) - items = iq.appendNode('command', {'node': node, 'xmlns': NS_COMMANDS}); - - else { - items = iq.setQuery(NS_DISCO_ITEMS); - items.setAttribute('node', NS_COMMANDS); - } - - if(action && node) { - iq.setType('set'); - items.setAttribute('action', action); - } - - con.send(iq, self.handleCommand); - } - - // Search query - else if(type == 'search') { - iq.setQuery(NS_SEARCH); - con.send(iq, self.handleSearch); - } - - // Subscribe query - else if(type == 'subscribe') { - iq.setQuery(NS_REGISTER); - con.send(iq, self.handleSubscribe); - } - - // Join - else if(type == 'join') { - if(target == 'discovery') - Discovery.close(); - - Chat.checkCreate(host, 'groupchat'); - } - } - } catch(e) { - Console.error('DataForm.go', e); - } finally { - return false; - } - - }; - - - /** - * Sends a given dataform - * @public - * @param {string} type - * @param {string} action - * @param {string} x_type - * @param {string} id - * @param {string} xid - * @param {string} node - * @param {string} sessionid - * @param {string} target - * @return {boolean} - */ - self.send = function(type, action, x_type, id, xid, node, sessionid, target) { - - try { - // Path - var pathID = '#' + target + ' .results[data-session="' + id + '"]'; - - // New IQ - var iq = new JSJaCIQ(); - iq.setTo(xid); - iq.setType('set'); - - // Set the correct query - var query; - - if(type == 'subscribe') - iqQuery = iq.setQuery(NS_REGISTER); - else if(type == 'search') - iqQuery = iq.setQuery(NS_SEARCH); - else if(type == 'command') - iqQuery = iq.appendNode('command', {'xmlns': NS_COMMANDS, 'node': node, 'sessionid': sessionid, 'action': action}); - else if(type == 'x') - iqQuery = iq.setQuery(NS_MUC_OWNER); - - // Build the XML document - if(action != 'cancel') { - // No X node - if(Common.exists('input.register-special') && (type == 'subscribe')) { - $('input.register-special').each(function() { - var iName = $(this).attr('name'); - var iValue = $(this).val(); - - iqQuery.appendChild(iq.buildNode(iName, {'xmlns': NS_REGISTER}, iValue)); - }); - } - - // Can create the X node - else { - var iqX = iqQuery.appendChild(iq.buildNode('x', {'xmlns': NS_XDATA, 'type': x_type})); - - // Each input - $(pathID + ' .oneresult input, ' + pathID + ' .oneresult textarea, ' + pathID + ' .oneresult select').each(function() { - // Get the current input value - var iVar = $(this).attr('name'); - var iType = $(this).attr('data-type'); - var iValue = $(this).val(); - - // Build a new field node - var field = iqX.appendChild(iq.buildNode('field', {'var': iVar, 'type': iType, 'xmlns': NS_XDATA})); - - // Boolean input? - if(iType == 'boolean') { - if($(this).filter(':checked').size()) - iValue = '1'; - else - iValue = '0'; - } - - // JID-multi input? - if(iType == 'jid-multi') { - // Values array - var xid_arr = [iValue]; - var xid_check = []; - - // Try to split it - if(iValue.indexOf(',') != -1) - xid_arr = iValue.split(','); - - // Append each value to the XML document - for(var i in xid_arr) { - // Get the current value - xid_current = $.trim(xid_arr[i]); - - // No current value? - if(!xid_current) - continue; - - // Add the current value - if(!Utils.existArrayValue(xid_check, xid_current)) { - xid_check.push(xid_current); - field.appendChild(iq.buildNode('value', {'xmlns': NS_XDATA}, xid_current)); - } - } - } - - // List-multi selector? - else if(iType == 'list-multi') { - // Any value? - if(iValue && iValue.length) { - for(var j in iValue) { - field.appendChild(iq.buildNode('value', {'xmlns': NS_XDATA}, iValue[j])); - } - } - } - - // Other inputs? - else - field.appendChild(iq.buildNode('value', {'xmlns': NS_XDATA}, iValue)); - }); - } - } - - // Clean the current session - self.clean(target); - - // Show the waiting item - $('#' + target + ' .wait').show(); - - // Change the ID of the current discovered item - var iqID = target + '-' + genID(); - $('#' + target + ' .' + target + '-results').attr('data-session', iqID); - iq.setID(iqID); - - // Send the IQ - if(type == 'subscribe') - con.send(iq, self.handleSubscribe); - else if(type == 'search') - con.send(iq, self.handleSearch); - else if(type == 'command') - con.send(iq, self.handleCommand); - else - con.send(iq); - } catch(e) { - Console.error('DataForm.send', e); - } finally { - return false; - } - - }; - - - /** - * Displays the good dataform buttons - * @public - * @param {string} type - * @param {string} action - * @param {string} id - * @param {string} xid - * @param {string} node - * @param {string} sessionid - * @param {string} target - * @param {string} pathID - * @return {undefined} - */ - self.buttons = function(type, action, id, xid, node, sessionid, target, pathID) { - - try { - // No need to use buttons? - if(type == 'muc') - return; - - // Override the "undefined" output - if(!id) - id = ''; - if(!xid) - xid = ''; - if(!node) - node = ''; - if(!sessionid) - sessionid = ''; - - // We generate the buttons code - var buttonsCode = '
'; - - if(action == 'submit') { - if((target == 'adhoc') && (type == 'command')) { - buttonsCode += '' + Common._e("Submit") + ''; - - // When keyup on one text input - $(pathID + ' input').keyup(function(e) { - if(e.keyCode == 13) { - self.send(type, 'execute', 'submit', id, xid, node, sessionid, target); - - return false; - } - }); - } - - else { - buttonsCode += '' + Common._e("Submit") + ''; - - // When keyup on one text input - $(pathID + ' input').keyup(function(e) { - if(e.keyCode == 13) { - self.send(type, 'submit', 'submit', id, xid, node, sessionid, target); - - return false; - } - }); - } - } - - if((action == 'submit') && (type != 'subscribe') && (type != 'search')) - buttonsCode += '' + Common._e("Cancel") + ''; - - if(((action == 'back') || (type == 'subscribe') || (type == 'search')) && (target == 'discovery')) - buttonsCode += '' + Common._e("Close") + ''; - - if((action == 'back') && ((target == 'welcome') || (target == 'directory'))) - buttonsCode += '' + Common._e("Previous") + ''; - - if((action == 'back') && (target == 'adhoc')) - buttonsCode += '' + Common._e("Previous") + ''; - - buttonsCode += '
'; - - // We display the buttons code - $(pathID).append(buttonsCode); - - // If no submit link, lock the form - if(!Common.exists(pathID + ' a.submit')) - $(pathID + ' input, ' + pathID + ' textarea').attr('readonly', true); - } catch(e) { - Console.error('DataForm.buttons', e); - } - - }; - - - /** - * Handles the MUC dataform - * @public - * @param {object} iq - * @return {undefined} - */ - self.handleMUC = function(iq) { - - try { - Errors.handleReply(iq); - self.handleContent(iq, 'muc'); - } catch(e) { - Console.error('DataForm.handleMUC', e); - } - - }; - - - /** - * Handles the browse dataform - * @public - * @param {object} iq - * @return {undefined} - */ - self.handleBrowse = function(iq) { - - try { - Errors.handleReply(iq); - self.handleContent(iq, 'browse'); - } catch(e) { - Console.error('DataForm.handleBrowse', e); - } - - }; - - - /** - * Handles the command dataform - * @public - * @param {object} iq - * @return {undefined} - */ - self.handleCommand = function(iq) { - - try { - Errors.handleReply(iq); - self.handleContent(iq, 'command'); - } catch(e) { - Console.error('DataForm.handleCommand', e); - } - - }; - - - /** - * Handles the subscribe dataform - * @public - * @param {object} iq - * @return {undefined} - */ - self.handleSubscribe = function(iq) { - - try { - Errors.handleReply(iq); - self.handleContent(iq, 'subscribe'); - } catch(e) { - Console.error('DataForm.handleSubscribe', e); - } - - }; - - - /** - * Handles the search dataform - * @public - * @param {object} iq - * @return {undefined} - */ - self.handleSearch = function(iq) { - - try { - Errors.handleReply(iq); - self.handleContent(iq, 'search'); - } catch(e) { - Console.error('DataForm.handleSearch', e); - } - - }; - - - /** - * Handles the dataform content - * @public - * @param {object} iq - * @param {string} type - * @return {undefined} - */ - self.handleContent = function(iq, type) { - - try { - // Get the ID - var sID = iq.getID(); - - // Get the target - var splitted = sID.split('-'); - var target = splitted[0]; - var sessionID = target + '-' + splitted[1]; - var from = Common.fullXID(Common.getStanzaFrom(iq)); - var pathID = '#' + target + ' .results[data-session="' + sessionID + '"]'; - - // If an error occured - if(!iq || (iq.getType() != 'result')) - self.noResult(pathID); - - // If we got something okay - else { - var handleXML = iq.getNode(); - - if(type == 'browse') { - if($(handleXML).find('item').attr('jid')) { - // Get the query node - var queryNode = $(handleXML).find('query').attr('node'); - - $(handleXML).find('item').each(function() { - // We parse the received xml - var itemHost = $(this).attr('jid'); - var itemNode = $(this).attr('node'); - var itemName = $(this).attr('name'); - var itemHash = hex_md5(itemHost); - - // Node - if(itemNode) - $(pathID).append( - '
' + - '
' + itemNode.htmlEnc() + '
' + - '
' - ); - - // Item - else if(queryNode && itemName) - $(pathID).append( - '
' + - '
' + itemName.htmlEnc() + '
' + - '
' - ); - - // Item with children - else { - // We display the waiting element - $(pathID + ' .disco-wait .disco-category-title').after( - '
' + - '
' + - '
' + itemHost + '
' + - '
' + Common._e("Requesting this service...") + '
' + - '
' - ); - - // We display the category - $('#' + target + ' .disco-wait').show(); - - // We ask the server what's the service type - self.getType(itemHost, itemNode, sessionID); - } - }); - } - - // Else, there are no items for this query - else - self.noResult(pathID); - } - - else if((type == 'muc') || (type == 'search') || (type == 'subscribe') || ((type == 'command') && $(handleXML).find('command').attr('xmlns'))) { - // Get some values - var xCommand = $(handleXML).find('command'); - var bNode = xCommand.attr('node'); - var bSession = xCommand.attr('sessionid'); - var bStatus = xCommand.attr('status'); - var xRegister = $(handleXML).find('query[xmlns="' + NS_REGISTER + '"]').text(); - var xElement = $(handleXML).find('x'); - - // Search done - if((xElement.attr('type') == 'result') && (type == 'search')) { - var bPath = pathID; - - // Display the result - $(handleXML).find('item').each(function() { - // Have some "flexibility" for what regards field names, it would be better to return the whole original DF - // layout, but on a large amount of result which have many fields, there's a very high chance the browser can - // choke on old systems or new ones even. - - // Search for useful fields, return first result. This is rather hacky, but jQuery is horrible when it comes to - // matching st. using patterns. (TODO: Improve and return the full DF layout without choking the browser) - var bName; - var bCountry; - var doneName, doneCountry; - - $.each($(this).find('field'), function(i, item) - { - var $item = $(item); - if ($(item).attr('var').match(/^(fn|name|[^n][^i][^c][^k]name)$/gi) && doneName !== true) { - bName = $item.children('value:first').text(); - doneName = true; - } else if ($(item).attr('var').match(/^(ctry|country.*)$/gi) && doneCountry !== true) { - bCountry = $item.children('value:first').text(); - doneCountry = true; - } - }); - - var bXID = $(this).find('field[var="jid"] value:first').text(); - var dName = bName; - - // Override "undefined" value - if(!bXID) - bXID = ''; - if(!bName) - bName = Common._e("Unknown name"); - if(!bCountry) - bCountry = Common._e("Unknown country"); - - // User hash - var bHash = hex_md5(bXID); - - // HTML code - var bHTML = '
' + - '
' + - '' + - '
' + - '
' + bName + '
' + - '
' + bCountry + '
' + - '
' + bXID + '
' + - '
'; - - // The buddy is not in our buddy list? - if(!Common.exists('#roster .buddy[data-xid="' + escape(bXID) + '"]')) - bHTML += '' + Common._e("Add") + ''; - - // Chat button, if not in welcome/directory mode - if(target == 'discovery') - bHTML += '' + Common._e("Chat") + ''; - - // Profile button, if not in discovery mode - else - bHTML += '' + Common._e("Profile") + ''; - - // Close the HTML element - bHTML += '
'; - - $(bPath).append(bHTML); - - // Click events - $(bPath + ' .' + bHash + ' a').click(function() { - // Buddy add - if($(this).is('.one-add')) { - $(this).hide(); - - Roster.addThisContact(bXID, dName); - } - - // Buddy chat - if($(this).is('.one-chat')) { - if(target == 'discovery') - Discovery.close(); - - Chat.checkCreate(bXID, 'chat', '', '', dName); - } - - // Buddy profile - if($(this).is('.one-profile')) - UserInfos.open(bXID); - - return false; - }); - - // Get the user's avatar - if(bXID) - Avatar.get(bXID, 'cache', 'true', 'forget'); - }); - - // No result? - if(!$(handleXML).find('item').size()) - self.noResult(pathID); - - // Previous button - self.buttons(type, 'back', sessionID, from, bNode, bSession, target, pathID); - } - - // Command to complete - else if(xElement.attr('xmlns') || ((type == 'subscribe') && xRegister)) { - // We display the elements - self.fill(handleXML, sessionID); - - // We display the buttons - if(bStatus != 'completed') - self.buttons(type, 'submit', sessionID, from, bNode, bSession, target, pathID); - else - self.buttons(type, 'back', sessionID, from, bNode, bSession, target, pathID); - } - - // Command completed or subscription done - else if(((bStatus == 'completed') && (type == 'command')) || (!xRegister && (type == 'subscribe'))) { - // Display the good text - var cNote = $(xCommand).find('note'); - - // Any note? - if(cNote.size()) { - cNote.each(function() { - $(pathID).append( - '
' + $(this).text().htmlEnc() + '
' - ); - }); - } - - // Default text - else - $(pathID).append('
' + Common._e("Your form has been sent.") + '
'); - - // Display the back button - self.buttons(type, 'back', sessionID, from, '', '', target, pathID); - - // Add the gateway to our roster if subscribed - if(type == 'subscribe') - Roster.addThisContact(from); - } - - // Command canceled - else if((bStatus == 'canceled') && (type == 'command')) { - if(target == 'discovery') - Discovery.start(); - else if(target == 'adhoc') - dataForm(from, 'command', '', '', 'adhoc'); - } - - // No items for this query - else - self.noResult(pathID); - } - - else if(type == 'command') { - if($(handleXML).find('item').attr('jid')) { - // We display the elements - $(handleXML).find('item').each(function() { - // We parse the received xml - var itemHost = $(this).attr('jid'); - var itemNode = $(this).attr('node'); - var itemName = $(this).attr('name'); - var itemHash = hex_md5(itemHost); - - // We display the waiting element - $(pathID).prepend( - '
' + - '
' + itemName + '
' + - '
»
' + - '
' - ); - }); - } - - // Else, there are no items for this query - else - self.noResult(pathID); - } - } - - // Focus on the first input - $(document).oneTime(10, function() { - $(pathID + ' input:visible:first').focus(); - }); - - // Hide the wait icon - $('#' + target + ' .wait').hide(); - } catch(e) { - Console.error('DataForm.handleContent', e); - } - - }; - - - /** - * Fills the dataform elements - * @public - * @param {type} xml - * @param {type} id - * @return {boolean} - */ - self.fill = function(xml, id) { - - /* REF: http://xmpp.org/extensions/xep-0004.html */ - - try { - // Initialize new vars - var target = id.split('-')[0]; - var pathID = '#' + target + ' .results[data-session="' + id + '"]'; - var selector, is_dataform; - - // Is it a dataform? - if($(xml).find('x[xmlns="' + NS_XDATA + '"]').size()) - is_dataform = true; - else - is_dataform = false; - - // Determines the good selector to use - if(is_dataform) - selector = $(xml).find('x[xmlns="' + NS_XDATA + '"]'); - else - selector = $(xml); - - // Form title - selector.find('title').each(function() { - $(pathID).append( - '
' + $(this).text().htmlEnc() + '
' - ); - }); - - // Form instructions - selector.find('instructions').each(function() { - $(pathID).append( - '
' + $(this).text().htmlEnc() + '
' - ); - }); - - // Register? - if(!is_dataform) { - // Items to detect - var reg_names = [Common._e("Nickname"), Common._e("Name"), Common._e("Password"), Common._e("E-mail")]; - var reg_ids = ['username', 'name', 'password', 'email']; - - // Append these inputs - $.each(reg_names, function(a) { - selector.find(reg_ids[a]).each(function() { - $(pathID).append( - '
' + - '' + - '' + - '
' - ); - }); - }); - - return false; - } - - // Dataform? - selector.find('field').each(function() { - // We parse the received xml - var type = $(this).attr('type'); - var label = $(this).attr('label'); - var field = $(this).attr('var'); - var value = $(this).find('value:first').text(); - var required = ''; - - // No value? - if(!field) - return; - - // Required input? - if($(this).find('required').size()) - required = ' required=""'; - - // Compatibility fix - if(!label) - label = field; - - if(!type) - type = ''; - - // Generate some values - var input; - var hideThis = ''; - - // Fixed field - if(type == 'fixed') - $(pathID).append('
' + value.htmlEnc() + '
'); - - else { - // Hidden field - if(type == 'hidden') { - hideThis = ' style="display: none;"'; - input = ''; - } - - // Boolean field - else if(type == 'boolean') { - var checked; - - if(value == '1') - checked = 'checked'; - else - checked = ''; - - input = ''; - } - - // List-single/list-multi field - else if((type == 'list-single') || (type == 'list-multi')) { - var multiple = ''; - - // Multiple options? - if(type == 'list-multi') - multiple = ' multiple=""'; - - // Append the select field - input = ''; - } - - // Text-multi field - else if(type == 'text-multi') - input = ''; - - // JID-multi field - else if(type == 'jid-multi') { - // Put the XID into an array - var xid_arr = []; - - $(this).find('value').each(function() { - var cValue = $(this).text(); - - if(!Utils.existArrayValue(xid_arr, cValue)) - xid_arr.push(cValue); - }); - - // Sort the array - xid_arr.sort(); - - // Create the input - var xid_value = ''; - - if(xid_arr.length) { - for(var i in xid_arr) { - // Any pre-value - if(xid_value) - xid_value += ', '; - - // Add the current XID - xid_value += xid_arr[i]; - } - } - - input = ''; - } - - // Other stuffs that are similar - else { - // Text-single field - var iType = 'text'; - - // Text-private field - if(type == 'text-private') - iType = 'password'; - - // JID-single field - else if(type == 'jid-single') - iType = 'email'; - - input = ''; - } - - // Append the HTML markup for this field - $(pathID).append( - '
' + - '' + - input + - '
' - ); - } - }); - } catch(e) { - Console.error('DataForm.fill', e); - } finally { - return false; - } - - }; - - - /** - * Gets the dataform type - * @public - * @param {string} host - * @param {string} node - * @param {string} id - * @return {undefined} - */ - self.getType = function(host, node, id) { - - try { - var iq = new JSJaCIQ(); - iq.setID(id + '-' + genID()); - iq.setTo(host); - iq.setType('get'); - - var iqQuery = iq.setQuery(NS_DISCO_INFO); - - if(node) { - iqQuery.setAttribute('node', node); - } - - con.send(iq, self.handleThisBrowse); - } catch(e) { - Console.error('DataForm.getType', e); - } - - }; - - - /** - * Handles the browse stanza - * @public - * @param {object} iq - * @return {undefined} - */ - self.handleThisBrowse = function(iq) { - - /* REF: http://xmpp.org/registrar/disco-categories.html */ - - try { - var id = iq.getID(); - var splitted = id.split('-'); - var target = splitted[0]; - var sessionID = target + '-' + splitted[1]; - var from = Common.fullXID(Common.getStanzaFrom(iq)); - var hash = hex_md5(from); - var handleXML = iq.getQuery(); - var pathID = '#' + target + ' .results[data-session="' + sessionID + '"]'; - - // We first remove the waiting element - $(pathID + ' .disco-wait .' + hash).remove(); - - if($(handleXML).find('identity').attr('type')) { - var category = $(handleXML).find('identity').attr('category'); - var type = $(handleXML).find('identity').attr('type'); - var named = $(handleXML).find('identity').attr('name'); - - if(named) - gName = named; - else - gName = ''; - - var one, two, three, four, five; - - // Get the features that this entity supports - var findFeature = $(handleXML).find('feature'); - - for(var i in findFeature) { - var current = findFeature.eq(i).attr('var'); - - switch(current) { - case NS_SEARCH: - one = 1; - break; - - case NS_MUC: - two = 1; - break; - - case NS_REGISTER: - three = 1; - break; - - case NS_COMMANDS: - four = 1; - break; - - case NS_DISCO_ITEMS: - five = 1; - break; - - default: - break; - } - } - - var buttons = Array(one, two, three, four, five); - - // We define the toolbox links depending on the supported features - var tools = ''; - var aTools = Array('search', 'join', 'subscribe', 'command', 'browse'); - var bTools = Array(Common._e("Search"), Common._e("Join"), Common._e("Subscribe"), Common._e("Command"), Common._e("Browse")); - - for(var b in buttons) { - if(buttons[b]) { - tools += ''; - } - } - - // As defined in the ref, we detect the type of each category to put an icon - switch(category) { - case 'account': - case 'auth': - case 'automation': - case 'client': - case 'collaboration': - case 'component': - case 'conference': - case 'directory': - case 'gateway': - case 'headline': - case 'hierarchy': - case 'proxy': - case 'pubsub': - case 'server': - case 'store': - break; - - default: - category = 'others'; - } - - // We display the item we found - $(pathID + ' .disco-' + category + ' .disco-category-title').after( - '
' + - '
' + - '
' + from + '
' + - '
' + gName + '
' + - '
' + tools + '
' + - '
' - ); - - // We display the category - $(pathID + ' .disco-' + category).show(); - } - - else { - $(pathID + ' .disco-others .disco-category-title').after( - '
' + - '
' + - '
' + from + '
' + - '
' + Common._e("Service offline or broken") + '
' + - '
' - ); - - // We display the category - $(pathID + ' .disco-others').show(); - } - - // We hide the waiting stuffs if there's no remaining loading items - if(!$(pathID + ' .disco-wait .' + target + '-oneresult').size()) { - $(pathID + ' .disco-wait, #' + target + ' .wait').hide(); - } - } catch(e) { - Console.error('DataForm.handleThisBrowse', e); - } - - }; - - - /** - * Cleans the current data-form popup - * @public - * @param {string} target - * @return {undefined} - */ - self.clean = function(target) { - - try { - if(target == 'discovery') { - Discovery.clean(); - } else { - $('#' + target + ' div.results').empty(); - } - } catch(e) { - Console.error('DataForm.clean', e); - } - - }; - - - /** - * Displays the no result indicator - * @public - * @param {string} path - * @return {undefined} - */ - self.noResult = function(path) { - - try { - $(path).prepend('

' + Common._e("Sorry, but the entity didn't return any result!") + '

'); - } catch(e) { - Console.error('DataForm.noResult', e); - } - - }; - - - /** - * Return class scope - */ - return self; - +/* + +Jappix - An open social platform +These are the dataform JS scripts for Jappix + +------------------------------------------------- + +License: AGPL +Authors: Valérian Saliou, Maranda + +*/ + +// Bundle +var DataForm = (function () { + + /** + * Alias of this + * @private + */ + var self = {}; + + + /** + * Gets the defined dataform elements + * @public + * @param {string} host + * @param {string} type + * @param {string} node + * @param {string} action + * @param {string} target + * @return {boolean} + */ + self.go = function(host, type, node, action, target) { + + try { + // Clean the current session + self.clean(target); + + // We tell the user that a search has been launched + $('#' + target + ' .wait').show(); + + // If we have enough data + if(host && type) { + // Generate a session ID + var sessionID = Math.round(100000.5 + (((900000.49999) - (100000.5)) * Math.random())); + var id = target + '-' + sessionID + '-' + genID(); + $('.' + target + '-results').attr('data-session', target + '-' + sessionID); + + // We request the service item + var iq = new JSJaCIQ(); + iq.setID(id); + iq.setTo(host); + iq.setType('get'); + + // MUC admin query + if(type == 'muc') { + iq.setQuery(NS_MUC_OWNER); + con.send(iq, self.handleMUC); + } + + // Browse query + else if(type == 'browse') { + var iqQuery = iq.setQuery(NS_DISCO_ITEMS); + + if(node) { + iqQuery.setAttribute('node', node); + } + + con.send(iq, self.handleBrowse); + } + + // Command + else if(type == 'command') { + var items; + + if(node) { + items = iq.appendNode('command', {'node': node, 'xmlns': NS_COMMANDS}); + } + + else { + items = iq.setQuery(NS_DISCO_ITEMS); + items.setAttribute('node', NS_COMMANDS); + } + + if(action && node) { + iq.setType('set'); + items.setAttribute('action', action); + } + + con.send(iq, self.handleCommand); + } + + // Search query + else if(type == 'search') { + iq.setQuery(NS_SEARCH); + con.send(iq, self.handleSearch); + } + + // Subscribe query + else if(type == 'subscribe') { + iq.setQuery(NS_REGISTER); + con.send(iq, self.handleSubscribe); + } + + // Join + else if(type == 'join') { + if(target == 'discovery') { + Discovery.close(); + } + + Chat.checkCreate(host, 'groupchat'); + } + } + } catch(e) { + Console.error('DataForm.go', e); + } finally { + return false; + } + + }; + + + /** + * Sends a given dataform + * @public + * @param {string} type + * @param {string} action + * @param {string} x_type + * @param {string} id + * @param {string} xid + * @param {string} node + * @param {string} sessionid + * @param {string} target + * @return {boolean} + */ + self.send = function(type, action, x_type, id, xid, node, sessionid, target) { + + try { + // Path + var pathID = '#' + target + ' .results[data-session="' + id + '"]'; + + // New IQ + var iq = new JSJaCIQ(); + iq.setTo(xid); + iq.setType('set'); + + // Set the correct query + var query; + + if(type == 'subscribe') { + iqQuery = iq.setQuery(NS_REGISTER); + } else if(type == 'search') { + iqQuery = iq.setQuery(NS_SEARCH); + } else if(type == 'command') { + iqQuery = iq.appendNode('command', {'xmlns': NS_COMMANDS, 'node': node, 'sessionid': sessionid, 'action': action}); + } else if(type == 'x') { + iqQuery = iq.setQuery(NS_MUC_OWNER); + } + + // Build the XML document + if(action != 'cancel') { + // No X node + if(Common.exists('input.register-special') && (type == 'subscribe')) { + $('input.register-special').each(function() { + var iName = $(this).attr('name'); + var iValue = $(this).val(); + + iqQuery.appendChild(iq.buildNode(iName, {'xmlns': NS_REGISTER}, iValue)); + }); + } + + // Can create the X node + else { + var iqX = iqQuery.appendChild(iq.buildNode('x', {'xmlns': NS_XDATA, 'type': x_type})); + + // Each input + $(pathID + ' .oneresult input, ' + pathID + ' .oneresult textarea, ' + pathID + ' .oneresult select').each(function() { + // Get the current input value + var iVar = $(this).attr('name'); + var iType = $(this).attr('data-type'); + var iValue = $(this).val(); + + // Build a new field node + var field = iqX.appendChild(iq.buildNode('field', {'var': iVar, 'type': iType, 'xmlns': NS_XDATA})); + + // Boolean input? + if(iType == 'boolean') { + if($(this).filter(':checked').size()) { + iValue = '1'; + } else { + iValue = '0'; + } + } + + // JID-multi input? + if(iType == 'jid-multi') { + // Values array + var xid_arr = [iValue]; + var xid_check = []; + + // Try to split it + if(iValue.indexOf(',') != -1) { + xid_arr = iValue.split(','); + } + + // Append each value to the XML document + for(var i in xid_arr) { + // Get the current value + xid_current = $.trim(xid_arr[i]); + + // No current value? + if(!xid_current) { + continue; + } + + // Add the current value + if(!Utils.existArrayValue(xid_check, xid_current)) { + xid_check.push(xid_current); + field.appendChild(iq.buildNode('value', {'xmlns': NS_XDATA}, xid_current)); + } + } + } + + // List-multi selector? + else if(iType == 'list-multi') { + // Any value? + if(iValue && iValue.length) { + for(var j in iValue) { + field.appendChild(iq.buildNode('value', {'xmlns': NS_XDATA}, iValue[j])); + } + } + } + + // Other inputs? + else { + field.appendChild(iq.buildNode('value', {'xmlns': NS_XDATA}, iValue)); + } + }); + } + } + + // Clean the current session + self.clean(target); + + // Show the waiting item + $('#' + target + ' .wait').show(); + + // Change the ID of the current discovered item + var iqID = target + '-' + genID(); + $('#' + target + ' .' + target + '-results').attr('data-session', iqID); + iq.setID(iqID); + + // Send the IQ + if(type == 'subscribe') { + con.send(iq, self.handleSubscribe); + } else if(type == 'search') { + con.send(iq, self.handleSearch); + } else if(type == 'command') { + con.send(iq, self.handleCommand); + } else { + con.send(iq); + } + } catch(e) { + Console.error('DataForm.send', e); + } finally { + return false; + } + + }; + + + /** + * Displays the good dataform buttons + * @public + * @param {string} type + * @param {string} action + * @param {string} id + * @param {string} xid + * @param {string} node + * @param {string} sessionid + * @param {string} target + * @param {string} pathID + * @return {undefined} + */ + self.buttons = function(type, action, id, xid, node, sessionid, target, pathID) { + + try { + // No need to use buttons? + if(type == 'muc') { + return; + } + + // Override the "undefined" output + if(!id) + id = ''; + if(!xid) + xid = ''; + if(!node) + node = ''; + if(!sessionid) + sessionid = ''; + + // We generate the buttons code + var buttonsCode = '
'; + + if(action == 'submit') { + if((target == 'adhoc') && (type == 'command')) { + buttonsCode += '' + Common._e("Submit") + ''; + + // When keyup on one text input + $(pathID + ' input').keyup(function(e) { + if(e.keyCode == 13) { + self.send(type, 'execute', 'submit', id, xid, node, sessionid, target); + + return false; + } + }); + } else { + buttonsCode += '' + Common._e("Submit") + ''; + + // When keyup on one text input + $(pathID + ' input').keyup(function(e) { + if(e.keyCode == 13) { + self.send(type, 'submit', 'submit', id, xid, node, sessionid, target); + + return false; + } + }); + } + } + + if((action == 'submit') && (type != 'subscribe') && (type != 'search')) { + buttonsCode += '' + Common._e("Cancel") + ''; + } + + if(((action == 'back') || (type == 'subscribe') || (type == 'search')) && (target == 'discovery')) { + buttonsCode += '' + Common._e("Close") + ''; + } + + if((action == 'back') && ((target == 'welcome') || (target == 'directory'))) { + buttonsCode += '' + Common._e("Previous") + ''; + } + + if((action == 'back') && (target == 'adhoc')) { + buttonsCode += '' + Common._e("Previous") + ''; + } + + buttonsCode += '
'; + + // We display the buttons code + $(pathID).append(buttonsCode); + + // If no submit link, lock the form + if(!Common.exists(pathID + ' a.submit')) { + $(pathID + ' input, ' + pathID + ' textarea').attr('readonly', true); + } + } catch(e) { + Console.error('DataForm.buttons', e); + } + + }; + + + /** + * Handles the MUC dataform + * @public + * @param {object} iq + * @return {undefined} + */ + self.handleMUC = function(iq) { + + try { + Errors.handleReply(iq); + self.handleContent(iq, 'muc'); + } catch(e) { + Console.error('DataForm.handleMUC', e); + } + + }; + + + /** + * Handles the browse dataform + * @public + * @param {object} iq + * @return {undefined} + */ + self.handleBrowse = function(iq) { + + try { + Errors.handleReply(iq); + self.handleContent(iq, 'browse'); + } catch(e) { + Console.error('DataForm.handleBrowse', e); + } + + }; + + + /** + * Handles the command dataform + * @public + * @param {object} iq + * @return {undefined} + */ + self.handleCommand = function(iq) { + + try { + Errors.handleReply(iq); + self.handleContent(iq, 'command'); + } catch(e) { + Console.error('DataForm.handleCommand', e); + } + + }; + + + /** + * Handles the subscribe dataform + * @public + * @param {object} iq + * @return {undefined} + */ + self.handleSubscribe = function(iq) { + + try { + Errors.handleReply(iq); + self.handleContent(iq, 'subscribe'); + } catch(e) { + Console.error('DataForm.handleSubscribe', e); + } + + }; + + + /** + * Handles the search dataform + * @public + * @param {object} iq + * @return {undefined} + */ + self.handleSearch = function(iq) { + + try { + Errors.handleReply(iq); + self.handleContent(iq, 'search'); + } catch(e) { + Console.error('DataForm.handleSearch', e); + } + + }; + + + /** + * Handles the dataform content + * @public + * @param {object} iq + * @param {string} type + * @return {undefined} + */ + self.handleContent = function(iq, type) { + + try { + // Get the ID + var sID = iq.getID(); + + // Get the target + var splitted = sID.split('-'); + var target = splitted[0]; + var sessionID = target + '-' + splitted[1]; + var from = Common.fullXID(Common.getStanzaFrom(iq)); + var pathID = '#' + target + ' .results[data-session="' + sessionID + '"]'; + + // If an error occured + if(!iq || (iq.getType() != 'result')) { + self.noResult(pathID); + } + + // If we got something okay + else { + var handleXML = iq.getNode(); + + if(type == 'browse') { + if($(handleXML).find('item').attr('jid')) { + // Get the query node + var queryNode = $(handleXML).find('query').attr('node'); + + $(handleXML).find('item').each(function() { + // We parse the received xml + var itemHost = $(this).attr('jid'); + var itemNode = $(this).attr('node'); + var itemName = $(this).attr('name'); + var itemHash = hex_md5(itemHost); + + // Node + if(itemNode) { + $(pathID).append( + '
' + + '
' + itemNode.htmlEnc() + '
' + + '
' + ); + } + + // Item + else if(queryNode && itemName) { + $(pathID).append( + '
' + + '
' + itemName.htmlEnc() + '
' + + '
' + ); + } + + // Item with children + else { + // We display the waiting element + $(pathID + ' .disco-wait .disco-category-title').after( + '
' + + '
' + + '
' + itemHost + '
' + + '
' + Common._e("Requesting this service...") + '
' + + '
' + ); + + // We display the category + $('#' + target + ' .disco-wait').show(); + + // We ask the server what's the service type + self.getType(itemHost, itemNode, sessionID); + } + }); + } + + // Else, there are no items for this query + else + self.noResult(pathID); + } + + else if((type == 'muc') || (type == 'search') || (type == 'subscribe') || ((type == 'command') && $(handleXML).find('command').attr('xmlns'))) { + // Get some values + var xCommand = $(handleXML).find('command'); + var bNode = xCommand.attr('node'); + var bSession = xCommand.attr('sessionid'); + var bStatus = xCommand.attr('status'); + var xRegister = $(handleXML).find('query[xmlns="' + NS_REGISTER + '"]').text(); + var xElement = $(handleXML).find('x'); + + // Search done + if((xElement.attr('type') == 'result') && (type == 'search')) { + var bPath = pathID; + + // Display the result + $(handleXML).find('item').each(function() { + // Have some "flexibility" for what regards field names, it would be better to return the whole original DF + // layout, but on a large amount of result which have many fields, there's a very high chance the browser can + // choke on old systems or new ones even. + + // Search for useful fields, return first result. This is rather hacky, but jQuery is horrible when it comes to + // matching st. using patterns. (TODO: Improve and return the full DF layout without choking the browser) + var bName; + var bCountry; + var doneName, doneCountry; + + $.each($(this).find('field'), function(i, item) { + var $item = $(item); + + if($(item).attr('var').match(/^(fn|name|[^n][^i][^c][^k]name)$/gi) && doneName !== true) { + bName = $item.children('value:first').text(); + doneName = true; + } else if($(item).attr('var').match(/^(ctry|country.*)$/gi) && doneCountry !== true) { + bCountry = $item.children('value:first').text(); + doneCountry = true; + } + }); + + var bXID = $(this).find('field[var="jid"] value:first').text(); + var dName = bName; + + // Override "undefined" value + if(!bXID) + bXID = ''; + if(!bName) + bName = Common._e("Unknown name"); + if(!bCountry) + bCountry = Common._e("Unknown country"); + + // User hash + var bHash = hex_md5(bXID); + + // HTML code + var bHTML = '
' + + '
' + + '' + + '
' + + '
' + bName + '
' + + '
' + bCountry + '
' + + '
' + bXID + '
' + + '
'; + + // The buddy is not in our buddy list? + if(!Common.exists('#roster .buddy[data-xid="' + escape(bXID) + '"]')) { + bHTML += '' + Common._e("Add") + ''; + } + + // Chat button, if not in welcome/directory mode + if(target == 'discovery') { + bHTML += '' + Common._e("Chat") + ''; + } + + // Profile button, if not in discovery mode + else { + bHTML += '' + Common._e("Profile") + ''; + } + + // Close the HTML element + bHTML += '
'; + + $(bPath).append(bHTML); + + // Click events + $(bPath + ' .' + bHash + ' a').click(function() { + // Buddy add + if($(this).is('.one-add')) { + $(this).hide(); + + Roster.addThisContact(bXID, dName); + } + + // Buddy chat + if($(this).is('.one-chat')) { + if(target == 'discovery') + Discovery.close(); + + Chat.checkCreate(bXID, 'chat', '', '', dName); + } + + // Buddy profile + if($(this).is('.one-profile')) { + UserInfos.open(bXID); + } + + return false; + }); + + // Get the user's avatar + if(bXID) { + Avatar.get(bXID, 'cache', 'true', 'forget'); + } + }); + + // No result? + if(!$(handleXML).find('item').size()) + self.noResult(pathID); + + // Previous button + self.buttons(type, 'back', sessionID, from, bNode, bSession, target, pathID); + } + + // Command to complete + else if(xElement.attr('xmlns') || ((type == 'subscribe') && xRegister)) { + // We display the elements + self.fill(handleXML, sessionID); + + // We display the buttons + if(bStatus != 'completed') { + self.buttons(type, 'submit', sessionID, from, bNode, bSession, target, pathID); + } else { + self.buttons(type, 'back', sessionID, from, bNode, bSession, target, pathID); + } + } + + // Command completed or subscription done + else if(((bStatus == 'completed') && (type == 'command')) || (!xRegister && (type == 'subscribe'))) { + // Display the good text + var cNote = $(xCommand).find('note'); + + // Any note? + if(cNote.size()) { + cNote.each(function() { + $(pathID).append( + '
' + $(this).text().htmlEnc() + '
' + ); + }); + } + + // Default text + else { + $(pathID).append('
' + Common._e("Your form has been sent.") + '
'); + } + + // Display the back button + self.buttons(type, 'back', sessionID, from, '', '', target, pathID); + + // Add the gateway to our roster if subscribed + if(type == 'subscribe') { + Roster.addThisContact(from); + } + } + + // Command canceled + else if((bStatus == 'canceled') && (type == 'command')) { + if(target == 'discovery') { + Discovery.start(); + } else if(target == 'adhoc') { + dataForm(from, 'command', '', '', 'adhoc'); + } + } + + // No items for this query + else + self.noResult(pathID); + } + + else if(type == 'command') { + if($(handleXML).find('item').attr('jid')) { + // We display the elements + $(handleXML).find('item').each(function() { + // We parse the received xml + var itemHost = $(this).attr('jid'); + var itemNode = $(this).attr('node'); + var itemName = $(this).attr('name'); + var itemHash = hex_md5(itemHost); + + // We display the waiting element + $(pathID).prepend( + '
' + + '
' + itemName + '
' + + '
»
' + + '
' + ); + }); + } + + // Else, there are no items for this query + else { + self.noResult(pathID); + } + } + } + + // Focus on the first input + $(document).oneTime(10, function() { + $(pathID + ' input:visible:first').focus(); + }); + + // Hide the wait icon + $('#' + target + ' .wait').hide(); + } catch(e) { + Console.error('DataForm.handleContent', e); + } + + }; + + + /** + * Fills the dataform elements + * @public + * @param {type} xml + * @param {type} id + * @return {boolean} + */ + self.fill = function(xml, id) { + + /* REF: http://xmpp.org/extensions/xep-0004.html */ + + try { + // Initialize new vars + var target = id.split('-')[0]; + var pathID = '#' + target + ' .results[data-session="' + id + '"]'; + var selector, is_dataform; + + // Is it a dataform? + if($(xml).find('x[xmlns="' + NS_XDATA + '"]').size()) { + is_dataform = true; + } else { + is_dataform = false; + } + + // Determines the good selector to use + if(is_dataform) { + selector = $(xml).find('x[xmlns="' + NS_XDATA + '"]'); + } else { + selector = $(xml); + } + + // Form title + selector.find('title').each(function() { + $(pathID).append( + '
' + $(this).text().htmlEnc() + '
' + ); + }); + + // Form instructions + selector.find('instructions').each(function() { + $(pathID).append( + '
' + $(this).text().htmlEnc() + '
' + ); + }); + + // Register? + if(!is_dataform) { + // Items to detect + var reg_names = [Common._e("Nickname"), Common._e("Name"), Common._e("Password"), Common._e("E-mail")]; + var reg_ids = ['username', 'name', 'password', 'email']; + + // Append these inputs + $.each(reg_names, function(a) { + selector.find(reg_ids[a]).each(function() { + $(pathID).append( + '
' + + '' + + '' + + '
' + ); + }); + }); + + return false; + } + + // Dataform? + selector.find('field').each(function() { + // We parse the received xml + var type = $(this).attr('type'); + var label = $(this).attr('label'); + var field = $(this).attr('var'); + var value = $(this).find('value:first').text(); + var required = ''; + + // No value? + if(!field) { + return; + } + + // Required input? + if($(this).find('required').size()) { + required = ' required=""'; + } + + // Compatibility fix + if(!label) { + label = field; + } + + if(!type) { + type = ''; + } + + // Generate some values + var input; + var hideThis = ''; + + // Fixed field + if(type == 'fixed') { + $(pathID).append('
' + value.htmlEnc() + '
'); + } else { + // Hidden field + if(type == 'hidden') { + hideThis = ' style="display: none;"'; + input = ''; + } + + // Boolean field + else if(type == 'boolean') { + var checked; + + if(value == '1') + checked = 'checked'; + else + checked = ''; + + input = ''; + } + + // List-single/list-multi field + else if((type == 'list-single') || (type == 'list-multi')) { + var multiple = ''; + + // Multiple options? + if(type == 'list-multi') { + multiple = ' multiple=""'; + } + + // Append the select field + input = ''; + } + + // Text-multi field + else if(type == 'text-multi') { + input = ''; + } + + // JID-multi field + else if(type == 'jid-multi') { + // Put the XID into an array + var xid_arr = []; + + $(this).find('value').each(function() { + var cValue = $(this).text(); + + if(!Utils.existArrayValue(xid_arr, cValue)) { + xid_arr.push(cValue); + } + }); + + // Sort the array + xid_arr.sort(); + + // Create the input + var xid_value = ''; + + if(xid_arr.length) { + for(var i in xid_arr) { + // Any pre-value + if(xid_value) { + xid_value += ', '; + } + + // Add the current XID + xid_value += xid_arr[i]; + } + } + + input = ''; + } + + // Other stuffs that are similar + else { + // Text-single field + var iType = 'text'; + + // Text-private field + if(type == 'text-private') { + iType = 'password'; + } + + // JID-single field + else if(type == 'jid-single') { + iType = 'email'; + } + + input = ''; + } + + // Append the HTML markup for this field + $(pathID).append( + '
' + + '' + + input + + '
' + ); + } + }); + } catch(e) { + Console.error('DataForm.fill', e); + } finally { + return false; + } + + }; + + + /** + * Gets the dataform type + * @public + * @param {string} host + * @param {string} node + * @param {string} id + * @return {undefined} + */ + self.getType = function(host, node, id) { + + try { + var iq = new JSJaCIQ(); + iq.setID(id + '-' + genID()); + iq.setTo(host); + iq.setType('get'); + + var iqQuery = iq.setQuery(NS_DISCO_INFO); + + if(node) { + iqQuery.setAttribute('node', node); + } + + con.send(iq, self.handleThisBrowse); + } catch(e) { + Console.error('DataForm.getType', e); + } + + }; + + + /** + * Handles the browse stanza + * @public + * @param {object} iq + * @return {undefined} + */ + self.handleThisBrowse = function(iq) { + + /* REF: http://xmpp.org/registrar/disco-categories.html */ + + try { + var id = iq.getID(); + var splitted = id.split('-'); + var target = splitted[0]; + var sessionID = target + '-' + splitted[1]; + var from = Common.fullXID(Common.getStanzaFrom(iq)); + var hash = hex_md5(from); + var handleXML = iq.getQuery(); + var pathID = '#' + target + ' .results[data-session="' + sessionID + '"]'; + + // We first remove the waiting element + $(pathID + ' .disco-wait .' + hash).remove(); + + if($(handleXML).find('identity').attr('type')) { + var category = $(handleXML).find('identity').attr('category'); + var type = $(handleXML).find('identity').attr('type'); + var named = $(handleXML).find('identity').attr('name'); + + if(named) { + gName = named; + } else { + gName = ''; + } + + var one, two, three, four, five; + + // Get the features that this entity supports + var findFeature = $(handleXML).find('feature'); + + for(var i in findFeature) { + var current = findFeature.eq(i).attr('var'); + + switch(current) { + case NS_SEARCH: + one = 1; + break; + + case NS_MUC: + two = 1; + break; + + case NS_REGISTER: + three = 1; + break; + + case NS_COMMANDS: + four = 1; + break; + + case NS_DISCO_ITEMS: + five = 1; + break; + + default: + break; + } + } + + var buttons = Array(one, two, three, four, five); + + // We define the toolbox links depending on the supported features + var tools = ''; + var aTools = Array('search', 'join', 'subscribe', 'command', 'browse'); + var bTools = Array(Common._e("Search"), Common._e("Join"), Common._e("Subscribe"), Common._e("Command"), Common._e("Browse")); + + for(var b in buttons) { + if(buttons[b]) { + tools += ''; + } + } + + // As defined in the ref, we detect the type of each category to put an icon + switch(category) { + case 'account': + case 'auth': + case 'automation': + case 'client': + case 'collaboration': + case 'component': + case 'conference': + case 'directory': + case 'gateway': + case 'headline': + case 'hierarchy': + case 'proxy': + case 'pubsub': + case 'server': + case 'store': + break; + + default: + category = 'others'; + } + + // We display the item we found + $(pathID + ' .disco-' + category + ' .disco-category-title').after( + '
' + + '
' + + '
' + from + '
' + + '
' + gName + '
' + + '
' + tools + '
' + + '
' + ); + + // We display the category + $(pathID + ' .disco-' + category).show(); + } + + else { + $(pathID + ' .disco-others .disco-category-title').after( + '
' + + '
' + + '
' + from + '
' + + '
' + Common._e("Service offline or broken") + '
' + + '
' + ); + + // We display the category + $(pathID + ' .disco-others').show(); + } + + // We hide the waiting stuffs if there's no remaining loading items + if(!$(pathID + ' .disco-wait .' + target + '-oneresult').size()) { + $(pathID + ' .disco-wait, #' + target + ' .wait').hide(); + } + } catch(e) { + Console.error('DataForm.handleThisBrowse', e); + } + + }; + + + /** + * Cleans the current data-form popup + * @public + * @param {string} target + * @return {undefined} + */ + self.clean = function(target) { + + try { + if(target == 'discovery') { + Discovery.clean(); + } else { + $('#' + target + ' div.results').empty(); + } + } catch(e) { + Console.error('DataForm.clean', e); + } + + }; + + + /** + * Displays the no result indicator + * @public + * @param {string} path + * @return {undefined} + */ + self.noResult = function(path) { + + try { + $(path).prepend('

' + Common._e("Sorry, but the entity didn't return any result!") + '

'); + } catch(e) { + Console.error('DataForm.noResult', e); + } + + }; + + + /** + * Return class scope + */ + return self; + })(); \ No newline at end of file diff --git a/source/app/javascripts/datastore.js b/source/app/javascripts/datastore.js index 5e8f927..c933d8b 100644 --- a/source/app/javascripts/datastore.js +++ b/source/app/javascripts/datastore.js @@ -39,8 +39,9 @@ var DataStore = (function () { this.key = function(key) { if(legacy) { - if(key >= this.length) + if(key >= this.length) { return null; + } var c = 0; @@ -56,8 +57,9 @@ var DataStore = (function () { this.getItem = function(key) { if(legacy) { - if(storage_emulated[key] !== undefined) + if(storage_emulated[key] !== undefined) { return storage_emulated[key]; + } return null; } else { @@ -67,8 +69,9 @@ var DataStore = (function () { this.setItem = function(key, data) { if(legacy) { - if(!(key in storage_emulated)) + if(!(key in storage_emulated)) { this.length++; + } storage_emulated[key] = (data + ''); } else { @@ -472,8 +475,9 @@ var DataStore = (function () { self.resetPersistent(); // Restaure the stored session entry - if(session) + if(session) { self.setPersistent('global', 'session', 1, session); + } Console.info('Persistent database flushed.'); diff --git a/source/app/javascripts/date.js b/source/app/javascripts/date.js index 289eb13..71b63c7 100644 --- a/source/app/javascripts/date.js +++ b/source/app/javascripts/date.js @@ -84,8 +84,9 @@ var DateUtils = (function () { try { // Last activity not yet initialized? - if(self.last_activity === 0) + if(self.last_activity === 0) { return 0; + } return self.getTimeStamp() - self.last_activity; } catch(e) { @@ -95,6 +96,27 @@ var DateUtils = (function () { }; + /** + * Gets the last user activity as a date + * @public + * @return {string} + */ + self.getLastActivityDate = function() { + + try { + var last_activity = self.last_activity || self.getTimeStamp(); + + var last_date = new Date(); + last_date.setTime(last_activity * 1000); + + return self.getDatetime(last_date, 'utc'); + } catch(e) { + Console.error('DateUtils.getLastActivityDate', e); + } + + }; + + /** * Gets the last user available presence in seconds * @public @@ -104,8 +126,9 @@ var DateUtils = (function () { try { // Last presence stamp not yet initialized? - if(self.presence_last_activity === 0) + if(self.presence_last_activity === 0) { return 0; + } return self.getTimeStamp() - self.presence_last_activity; } catch(e) { @@ -115,6 +138,56 @@ var DateUtils = (function () { }; + /** + * Generates a normalized datetime + * @public + * @param {Date} date + * @param {string} location + * @return {string} + */ + self.getDatetime = function(date, location) { + + /* FROM : http://trac.jwchat.org/jsjac/browser/branches/jsjac_1.0/jsextras.js?rev=221 */ + + var year, month, day, hours, minutes, seconds; + var date_string = null; + + try { + if(location == 'utc') { + // UTC date + year = date.getUTCFullYear(); + month = date.getUTCMonth(); + day = date.getUTCDate(); + hours = date.getUTCHours(); + minutes = date.getUTCMinutes(); + seconds = date.getUTCSeconds(); + } else { + // Local date + year = date.getFullYear(); + month = date.getMonth(); + day = date.getDate(); + hours = date.getHours(); + minutes = date.getMinutes(); + seconds = date.getSeconds(); + } + + // Generates the date string + date_string = year + '-'; + date_string += Common.padZero(month + 1) + '-'; + date_string += Common.padZero(day) + 'T'; + date_string += Common.padZero(hours) + ':'; + date_string += Common.padZero(minutes) + ':'; + date_string += Common.padZero(seconds) + 'Z'; + + // Returns the date string + return date_string; + } catch(e) { + Console.error('DateUtils.getDatetime', e); + } + + }; + + /** * Generates the time for XMPP * @public @@ -123,43 +196,11 @@ var DateUtils = (function () { */ self.getXMPPTime = function(location) { - /* FROM : http://trac.jwchat.org/jsjac/browser/branches/jsjac_1.0/jsextras.js?rev=221 */ - try { - // Initialize - var jInit = new Date(); - var year, month, day, hours, minutes, seconds; - - // Gets the UTC date - if(location == 'utc') { - year = jInit.getUTCFullYear(); - month = jInit.getUTCMonth(); - day = jInit.getUTCDate(); - hours = jInit.getUTCHours(); - minutes = jInit.getUTCMinutes(); - seconds = jInit.getUTCSeconds(); - } - - // Gets the local date - else { - year = jInit.getFullYear(); - month = jInit.getMonth(); - day = jInit.getDate(); - hours = jInit.getHours(); - minutes = jInit.getMinutes(); - seconds = jInit.getSeconds(); - } - - // Generates the date string - var jDate = year + '-'; - jDate += Common.padZero(month + 1) + '-'; - jDate += Common.padZero(day) + 'T'; - jDate += Common.padZero(hours) + ':'; - jDate += Common.padZero(minutes) + ':'; - jDate += Common.padZero(seconds) + 'Z'; - - // Returns the date string - return jDate; + return self.getDatetime( + (new Date()), + location + ); } catch(e) { Console.error('DateUtils.getXMPPTime', e); } @@ -176,6 +217,7 @@ var DateUtils = (function () { try { var init = new Date(); + var time = Common.padZero(init.getHours()) + ':'; time += Common.padZero(init.getMinutes()) + ':'; time += Common.padZero(init.getSeconds()); @@ -332,20 +374,24 @@ var DateUtils = (function () { var days = Math.round((current_stamp - old_stamp) / 86400000); // Invalid date? - if(isNaN(old_stamp) || isNaN(days)) + if(isNaN(old_stamp) || isNaN(days)) { return self.getCompleteTime(); + } // Is it today? - if(current_day == old_day) + if(current_day == old_day) { return old_time; + } // It is yesterday? - if(days <= 1) + if(days <= 1) { return Common._e("Yesterday") + ' - ' + old_time; + } // Is it less than a week ago? - if(days <= 7) + if(days <= 7) { return Common.printf(Common._e("%s days ago"), days) + ' - ' + old_time; + } // Another longer period return old_date.toLocaleDateString() + ' - ' + old_time; @@ -371,13 +417,12 @@ var DateUtils = (function () { // Read the delay d_delay = jQuery(node).find('delay[xmlns="' + NS_URN_DELAY + '"]:first').attr('stamp'); - // New delay (valid XEP) - if(d_delay) + // Get delay + if(d_delay) { + // New delay (valid XEP) delay = d_delay; - - // Old delay (obsolete XEP!) - else { - // Try to read the old-school delay + } else { + // Old delay (obsolete XEP!) var x_delay = jQuery(node).find('x[xmlns="' + NS_DELAY + '"]:first').attr('stamp'); if(x_delay) diff --git a/source/app/javascripts/datejs.js b/source/app/javascripts/datejs.js index 77f4986..2d52e9a 100644 --- a/source/app/javascripts/datejs.js +++ b/source/app/javascripts/datejs.js @@ -1,10 +1,10 @@ -/** - * Version: 1.0 Alpha-1 - * Build Date: 13-Nov-2007 - * Copyright (c) 2006-2007, Coolite Inc. (http://www.coolite.com/). All rights reserved. - * License: Licensed under The MIT License. See license.txt and http://www.datejs.com/license/. - * Website: http://www.datejs.com/ or http://www.coolite.com/datejs/ - */ +/** + * Version: 1.0 Alpha-1 + * Build Date: 13-Nov-2007 + * Copyright (c) 2006-2007, Coolite Inc. (http://www.coolite.com/). All rights reserved. + * License: Licensed under The MIT License. See license.txt and http://www.datejs.com/license/. + * Website: http://www.datejs.com/ or http://www.coolite.com/datejs/ + */ Date.CultureInfo={name:"en-US",englishName:"English (United States)",nativeName:"English (United States)",dayNames:["Sunday","Monday","Tuesday","Wednesday","Thursday","Friday","Saturday"],abbreviatedDayNames:["Sun","Mon","Tue","Wed","Thu","Fri","Sat"],shortestDayNames:["Su","Mo","Tu","We","Th","Fr","Sa"],firstLetterDayNames:["S","M","T","W","T","F","S"],monthNames:["January","February","March","April","May","June","July","August","September","October","November","December"],abbreviatedMonthNames:["Jan","Feb","Mar","Apr","May","Jun","Jul","Aug","Sep","Oct","Nov","Dec"],amDesignator:"AM",pmDesignator:"PM",firstDayOfWeek:0,twoDigitYearMax:2029,dateElementOrder:"mdy",formatPatterns:{shortDate:"M/d/yyyy",longDate:"dddd, MMMM dd, yyyy",shortTime:"h:mm tt",longTime:"h:mm:ss tt",fullDateTime:"dddd, MMMM dd, yyyy h:mm:ss tt",sortableDateTime:"yyyy-MM-ddTHH:mm:ss",universalSortableDateTime:"yyyy-MM-dd HH:mm:ssZ",rfc1123:"ddd, dd MMM yyyy HH:mm:ss GMT",monthDay:"MMMM dd",yearMonth:"MMMM, yyyy"},regexPatterns:{jan:/^jan(uary)?/i,feb:/^feb(ruary)?/i,mar:/^mar(ch)?/i,apr:/^apr(il)?/i,may:/^may/i,jun:/^jun(e)?/i,jul:/^jul(y)?/i,aug:/^aug(ust)?/i,sep:/^sep(t(ember)?)?/i,oct:/^oct(ober)?/i,nov:/^nov(ember)?/i,dec:/^dec(ember)?/i,sun:/^su(n(day)?)?/i,mon:/^mo(n(day)?)?/i,tue:/^tu(e(s(day)?)?)?/i,wed:/^we(d(nesday)?)?/i,thu:/^th(u(r(s(day)?)?)?)?/i,fri:/^fr(i(day)?)?/i,sat:/^sa(t(urday)?)?/i,future:/^next/i,past:/^last|past|prev(ious)?/i,add:/^(\+|after|from)/i,subtract:/^(\-|before|ago)/i,yesterday:/^yesterday/i,today:/^t(oday)?/i,tomorrow:/^tomorrow/i,now:/^n(ow)?/i,millisecond:/^ms|milli(second)?s?/i,second:/^sec(ond)?s?/i,minute:/^min(ute)?s?/i,hour:/^h(ou)?rs?/i,week:/^w(ee)?k/i,month:/^m(o(nth)?s?)?/i,day:/^d(ays?)?/i,year:/^y((ea)?rs?)?/i,shortMeridian:/^(a|p)/i,longMeridian:/^(a\.?m?\.?|p\.?m?\.?)/i,timezone:/^((e(s|d)t|c(s|d)t|m(s|d)t|p(s|d)t)|((gmt)?\s*(\+|\-)\s*\d\d\d\d?)|gmt)/i,ordinalSuffix:/^\s*(st|nd|rd|th)/i,timeContext:/^\s*(\:|a|p)/i},abbreviatedTimeZoneStandard:{GMT:"-000",EST:"-0400",CST:"-0500",MST:"-0600",PST:"-0700"},abbreviatedTimeZoneDST:{GMT:"-000",EDT:"-0500",CDT:"-0600",MDT:"-0700",PDT:"-0800"}}; Date.getMonthNumberFromName=function(name){var n=Date.CultureInfo.monthNames,m=Date.CultureInfo.abbreviatedMonthNames,s=name.toLowerCase();for(var i=0;i' + Common._e("Remove") + ''); + button_sel.filter('.add').replaceWith( + '' + Common._e("Remove") + '' + ); // Click event - $(button + '.remove').click(function() { - return self.removeThis(roomXID, roomName); + button_sel.filter('.remove').click(function() { + return self.removeThis(room_xid, room_name); }); // Hide the add button in the (opened?) groupchat - $('#' + hex_md5(roomXID) + ' .tools-add').hide(); + $('#' + hex_md5(room_xid) + ' .tools-add').hide(); // Add the database entry - self.display(roomXID, Common.explodeThis(' (', roomName, 0), Name.getNick(), '0', ''); + self.display( + room_xid, + Common.explodeThis(' (', room_name, 0), Name.getNick(), '0', '' + ); // Publish the favorites self.publish(); @@ -220,29 +230,29 @@ var Favorites = (function () { /** * Removes a room from the favorites * @public - * @param {string} roomXID - * @param {string} roomName + * @param {string} room_xid + * @param {string} room_name * @return {boolean} */ - self.removeThis = function(roomXID, roomName) { + self.removeThis = function(room_xid, room_name) { try { // Button path - var button = '#favorites .fsearch-results div[data-xid="' + escape(roomXID) + '"] a.one-button'; + var button_sel = $('#favorites .fsearch-results div[data-xid="' + escape(room_xid) + '"] a.one-button'); // Add a remove button instead of the add one - $(button + '.remove').replaceWith('' + Common._e("Add") + ''); + button_sel.filter('.remove').replaceWith('' + Common._e("Add") + ''); // Click event - $(button + '.add').click(function() { - return self.addThis(roomXID, roomName); + button_sel.filter('.add').click(function() { + return self.addThis(room_xid, room_name); }); // Show the add button in the (opened?) groupchat - $('#' + hex_md5(roomXID) + ' .tools-add').show(); + $('#' + hex_md5(room_xid) + ' .tools-add').show(); // Remove the favorite - self.remove(roomXID, true); + self.remove(room_xid, true); // Publish the favorites self.publish(); @@ -264,31 +274,34 @@ var Favorites = (function () { try { // Path to favorites - var favorites = '#favorites .'; + var favorites_sel = $('#favorites'); // Reset the favorites self.reset(); // Show the edit/remove button, hide the others - $(favorites + 'fedit-terminate').hide(); - $(favorites + 'fedit-edit').show(); - $(favorites + 'fedit-remove').show(); + favorites_sel.find('.fedit-terminate').hide(); + favorites_sel.find('.fedit-edit').show(); + favorites_sel.find('.fedit-remove').show(); // We retrieve the values - var xid = $(favorites + 'fedit-head-select').val(); - var data = Common.XMLFromString(DataStore.getDB(Connection.desktop_hash, 'favorites', xid)); + var xid = favorites_sel.find('.fedit-head-select').val(); + var data_sel = $(Common.XMLFromString( + DataStore.getDB(Connection.desktop_hash, 'favorites', xid) + )); // If this is not the default room if(xid != 'none') { // We apply the values - $(favorites + 'fedit-title').val($(data).find('name').text()); - $(favorites + 'fedit-nick').val($(data).find('nick').text()); - $(favorites + 'fedit-chan').val(Common.getXIDNick(xid)); - $(favorites + 'fedit-server').val(Common.getXIDHost(xid)); - $(favorites + 'fedit-password').val($(data).find('password').text()); + favorites_sel.find('.fedit-title').val(data_sel.find('name').text()); + favorites_sel.find('.fedit-nick').val(data_sel.find('nick').text()); + favorites_sel.find('.fedit-chan').val(Common.getXIDNick(xid)); + favorites_sel.find('.fedit-server').val(Common.getXIDHost(xid)); + favorites_sel.find('.fedit-password').val(data_sel.find('password').text()); - if($(data).find('autojoin').text() == 'true') - $(favorites + 'fedit-autojoin').attr('checked', true); + if(data_sel.find('autojoin').text() == 'true') { + favorites_sel.find('.fedit-autojoin').attr('checked', true); + } } } catch(e) { Console.error('Favorites.edit', e); @@ -307,52 +320,50 @@ var Favorites = (function () { try { // Path to favorites - var favorites = '#favorites '; + var favorites_sel = $('#favorites'); // We get the values of the current edited groupchat - var old_xid = $(favorites + '.fedit-head-select').val(); + var old_xid = favorites_sel.find('.fedit-head-select').val(); - var title = $(favorites + '.fedit-title').val(); - var nick = $(favorites + '.fedit-nick').val(); - var room = $(favorites + '.fedit-chan').val(); - var server = $(favorites + '.fedit-server').val(); + var title = favorites_sel.find('.fedit-title').val(); + var nick = favorites_sel.find('.fedit-nick').val(); + var room = favorites_sel.find('.fedit-chan').val(); + var server = favorites_sel.find('.fedit-server').val(); var xid = room + '@' + server; - var password = $(favorites + '.fedit-password').val(); + var password = favorites_sel.find('.fedit-password').val(); var autojoin = 'false'; - if($(favorites + '.fedit-autojoin').filter(':checked').size()) + if(favorites_sel.find('.fedit-autojoin').filter(':checked').size()) { autojoin = 'true'; + } // We check the missing values and send this if okay if((type == 'add') || (type == 'edit')) { if(title && nick && room && server) { // Remove the edited room - if(type == 'edit') + if(type == 'edit') { self.remove(old_xid, true); + } // Display the favorites self.display(xid, title, nick, autojoin, password); // Reset the inputs self.reset(); - } - - else { - $(favorites + 'input[required]').each(function() { + } else { + favorites_sel.find('input[required]').each(function() { var select = $(this); - if(!select.val()) + if(!select.val()) { $(document).oneTime(10, function() { select.addClass('please-complete').focus(); }); - else + } else { select.removeClass('please-complete'); + } }); } - } - - // Must remove a favorite? - else if(type == 'remove') { + } else if(type == 'remove') { self.remove(old_xid, true); // Reset the inputs @@ -409,7 +420,9 @@ var Favorites = (function () { iq.setType('set'); var query = iq.setQuery(NS_PRIVATE); - var storage = query.appendChild(iq.buildNode('storage', {'xmlns': NS_BOOKMARKS})); + var storage = query.appendChild(iq.buildNode('storage', { + 'xmlns': NS_BOOKMARKS + })); // We generate the XML var db_regex = new RegExp(('^' + Connection.desktop_hash + '_') + 'favorites_(.+)'); @@ -420,19 +433,35 @@ var Favorites = (function () { // If the pointer is on a stored favorite if(current.match(db_regex)) { - var data = Common.XMLFromString(DataStore.storageDB.getItem(current)); - var xid = $(data).find('xid').text(); - var rName = $(data).find('name').text(); - var nick = $(data).find('nick').text(); - var password = $(data).find('password').text(); - var autojoin = $(data).find('autojoin').text(); + var data_sel = $(Common.XMLFromString( + DataStore.storageDB.getItem(current) + )); + + var xid = data_sel.find('xid').text(); + var rName = data_sel.find('name').text(); + var nick = data_sel.find('nick').text(); + var password = data_sel.find('password').text(); + var autojoin = data_sel.find('autojoin').text(); // We create the node for this groupchat - var item = storage.appendChild(iq.buildNode('conference', {'name': rName, 'jid': xid, 'autojoin': autojoin, xmlns: NS_BOOKMARKS})); - item.appendChild(iq.buildNode('nick', {xmlns: NS_BOOKMARKS}, nick)); + var item = storage.appendChild( + iq.buildNode('conference', { + 'name': rName, + 'jid': xid, + 'autojoin': autojoin, + xmlns: NS_BOOKMARKS + }) + ); - if(password) - item.appendChild(iq.buildNode('password', {xmlns: NS_BOOKMARKS}, password)); + item.appendChild(iq.buildNode('nick', { + xmlns: NS_BOOKMARKS + }, nick)); + + if(password) { + item.appendChild(iq.buildNode('password', { + xmlns: NS_BOOKMARKS + }, password)); + } Console.info('Bookmark sent: ' + xid); } @@ -454,17 +483,17 @@ var Favorites = (function () { self.getGCList = function() { try { - var path = '#favorites .'; - var gcServer = $('.fsearch-head-server').val(); + var path_sel = $('#favorites'); + var groupchat_server = $('.fsearch-head-server').val(); // We reset some things - $(path + 'fsearch-oneresult').remove(); - $(path + 'fsearch-noresults').hide(); - $(path + 'wait').show(); + path_sel.find('.fsearch-oneresult').remove(); + path_sel.find('.fsearch-noresults').hide(); + path_sel.find('.wait').show(); var iq = new JSJaCIQ(); iq.setType('get'); - iq.setTo(gcServer); + iq.setTo(groupchat_server); iq.setQuery(NS_DISCO_ITEMS); @@ -485,13 +514,13 @@ var Favorites = (function () { self.handleGCList = function(iq) { try { - var path = '#favorites .'; + var path_sel = $('#favorites'); var from = Common.fullXID(Common.getStanzaFrom(iq)); if(!iq || (iq.getType() != 'result')) { Board.openThisError(3); - $(path + 'wait').hide(); + path_sel.find('.wait').hide(); Console.error('Error while retrieving the rooms: ' + from); } @@ -504,24 +533,37 @@ var Favorites = (function () { var html = ''; $(handleXML).find('item').each(function() { - var roomXID = $(this).attr('jid'); - var roomName = $(this).attr('name'); + var this_sel = $(this); + + var room_xid = this_sel.attr('jid'); + var room_name = this_sel.attr('name'); - if(roomXID && roomName) { + if(room_xid && room_name) { // Escaped values - var escaped_xid = Utils.encodeOnclick(roomXID); - var escaped_name = Utils.encodeOnclick(roomName); + var escaped_xid = Utils.encodeOnclick(room_xid); + var escaped_name = Utils.encodeOnclick(room_name); // Initialize the room HTML - html += '
' + - '
' + roomName.htmlEnc() + '
' + + html += '
' + + '
' + room_name.htmlEnc() + '
' + '' + Common._e("Join") + ''; // This room is yet a favorite - if(DataStore.existDB('favorites', roomXID)) - html += '' + Common._e("Remove") + ''; - else - html += '' + Common._e("Add") + ''; + if(DataStore.existDB(Connection.desktop_hash, 'favorites', room_xid)) { + html += '' + + Common._e("Remove") + + ''; + } else { + html += '' + + Common._e("Add") + + ''; + } // Close the room HTML html += '
'; @@ -529,16 +571,15 @@ var Favorites = (function () { }); // Append this code to the popup - $(path + 'fsearch-results').append(html); + path_sel.find('.fsearch-results').append(html); + } else { + path_sel.find('.fsearch-noresults').show(); } - else - $(path + 'fsearch-noresults').show(); - Console.info('Rooms retrieved: ' + from); } - $(path + 'wait').hide(); + path_sel.find('.wait').hide(); } catch(e) { Console.error('Favorites.handleGCList', e); } @@ -556,7 +597,14 @@ var Favorites = (function () { try { self.quit(); - Chat.checkCreate(room, 'groupchat', '', '', Common.getXIDNick(room)); + + Chat.checkCreate( + room, + 'groupchat', + '', + '', + Common.getXIDNick(room) + ); } catch(e) { Console.error('Favorites.join', e); } finally { @@ -589,7 +637,14 @@ var Favorites = (function () { $('#roster .gc-join-first-option, #favorites .fedit-head-select-first-option').after(html); // We store the informations - var value = '' + xid.htmlEnc() + '' + name.htmlEnc() + '' + nick.htmlEnc() + '' + autojoin.htmlEnc() + '' + password.htmlEnc() + ''; + var value = '' + + '' + xid.htmlEnc() + '' + + '' + name.htmlEnc() + '' + + '' + nick.htmlEnc() + '' + + '' + autojoin.htmlEnc() + '' + + '' + password.htmlEnc() + '' + + ''; + DataStore.setDB(Connection.desktop_hash, 'favorites', xid, value); } catch(e) { Console.error('Favorites.display', e); @@ -621,13 +676,20 @@ var Favorites = (function () { var data = Common.XMLFromString(DataStore.storageDB.getItem(current)); // Add the current favorite to the HTML code - html += ''; + html += ''; } } // Generate specific HTML code - var favorites_bubble = '' + html; - var favorites_popup = '' + html; + var favorites_bubble = '' + html; + + var favorites_popup = '' + html; // Append the HTML code $('#roster .buddy-conf-groupchat-select').html(favorites_bubble); @@ -647,29 +709,31 @@ var Favorites = (function () { self.instance = function() { try { - var path = '#favorites .'; + var favorites_sel = $('#favorites'); // Keyboard events - $(path + 'fsearch-head-server').keyup(function(e) { + favorites_sel.find('.fsearch-head-server').keyup(function(e) { if(e.keyCode == 13) { + var this_sel = $(this); + // No value? - if(!$(this).val()) - $(this).val(HOST_MUC); + if(!this_sel.val()) { + this_sel.val(HOST_MUC); + } // Get the list self.getGCList(); } }); - $(path + 'fedit-line input').keyup(function(e) { + favorites_sel.find('.fedit-line input').keyup(function(e) { if(e.keyCode == 13) { // Edit a favorite - if($(path + 'fedit-edit').is(':visible')) + if(favorites_sel.find('.fedit-edit').is(':visible')) { self.terminateThis('edit'); - - // Add a favorite - else + } else { self.terminateThis('add'); + } } }); @@ -677,33 +741,33 @@ var Favorites = (function () { $('.fedit-head-select').change(self.edit); // Click events - $(path + 'room-switcher').click(function() { - $(path + 'favorites-content').hide(); + favorites_sel.find('.room-switcher').click(function() { + favorites_sel.find('.favorites-content').hide(); self.reset(); }); - $(path + 'room-list').click(function() { - $(path + 'favorites-edit').show(); + favorites_sel.find('.room-list').click(function() { + favorites_sel.find('.favorites-edit').show(); }); - $(path + 'room-search').click(function() { - $(path + 'favorites-search').show(); + favorites_sel.find('.room-search').click(function() { + favorites_sel.find('.favorites-search').show(); self.getGCList(); }); - $(path + 'fedit-add').click(function() { + favorites_sel.find('.fedit-add').click(function() { return self.terminateThis('add'); }); - $(path + 'fedit-edit').click(function() { + favorites_sel.find('.fedit-edit').click(function() { return self.terminateThis('edit'); }); - $(path + 'fedit-remove').click(function() { + favorites_sel.find('.fedit-remove').click(function() { return self.terminateThis('remove'); }); - $(path + 'bottom .finish').click(function() { + favorites_sel.find('.bottom .finish').click(function() { return self.quit(); }); } catch(e) { diff --git a/source/app/javascripts/features.js b/source/app/javascripts/features.js index e24bb5d..fb0455b 100644 --- a/source/app/javascripts/features.js +++ b/source/app/javascripts/features.js @@ -103,7 +103,7 @@ var Features = (function () { }; // Markers - var namespaces = [NS_PUBSUB, NS_PUBSUB_CN, NS_URN_MAM, NS_COMMANDS, NS_URN_CARBONS]; + var namespaces = [NS_PUBSUB, NS_PUBSUB_CN, NS_URN_MAM, NS_COMMANDS, NS_URN_CARBONS, NS_URN_CORRECT]; var identity = selector.find('identity'); @@ -218,6 +218,11 @@ var Features = (function () { if(self.enabledMAMPurge()) { $(path + 'mam-purge-hidable').show(); } + + // Message correction features + if(self.enabledCorrection()) { + $(path + 'correction-hidable').show(); + } // Commands features if(self.enabledCommands()) { @@ -385,6 +390,22 @@ var Features = (function () { }; + /** + * Returns the XMPP server correction support + * @public + * @return {boolean} + */ + self.enabledCorrection = function() { + + try { + return self.isEnabled(NS_URN_CORRECT); + } catch(e) { + Console.error('Features.enabledCorrection', e); + } + + }; + + /** * Normalizes the XMPP server name * @private diff --git a/source/app/javascripts/filter.js b/source/app/javascripts/filter.js index b5adba0..a06e254 100644 --- a/source/app/javascripts/filter.js +++ b/source/app/javascripts/filter.js @@ -20,6 +20,300 @@ var Filter = (function () { var self = {}; + /* Constants */ + self.message_regex = { + 'commands': { + 'me': /((^)|((.+)(>)))(\/me )([^<]+)/ + }, + + 'emotes': { + 'angry': [ + /(:-?@)($|\s|<)/gi, + '$2' + ], + + 'bat': [ + /(:-?\[)($|\s|<)/gi, + '$2' + ], + + 'beer': [ + /(\(B\))($|\s|<)/g, + '$2' + ], + + 'biggrin': [ + /((:-?D)|(XD))($|\s|<)/gi, + '$4' + ], + + 'blush': [ + /(:-?\$)($|\s|<)/gi, + '$2' + ], + + 'boy': [ + /(\(Z\))($|\s|<)/g, + '$2' + ], + + 'brflower': [ + /(\(W\))($|\s|<)/g, + '$2' + ], + + 'brheart': [ + /((<\/3)|(\(U\)))($|\s|<)/g, + '$4' + ], + + 'coffee': [ + /(\(C\))($|\s|<)/g, + '$2' + ], + + 'coolglasses': [ + /((8-\))|(\(H\)))($|\s|<)/g, + '$4' + ], + + 'cry': [ + /(:'-?\()($|\s|<)/gi, + '$2' + ], + + 'cuffs': [ + /(\(%\))($|\s|<)/g, + '$2' + ], + + 'devil': [ + /(\]:-?>)($|\s|<)/gi, + '$2' + ], + + 'drink': [ + /(\(D\))($|\s|<)/g, + '$2' + ], + + 'flower': [ + /(@}->--)($|\s|<)/gi, + '$2' + ], + + 'frowning': [ + /((:-?\/)|(:-?S))($|\s|<)/gi, + '$4' + ], + + 'girl': [ + /(\(X\))($|\s|<)/g, + '$2' + ], + + 'heart': [ + /((<3)|(\(L\)))($|\s|<)/g, + '$4' + ], + + 'hugleft': [ + /(\(}\))($|\s|<)/g, + '$2' + ], + + 'hugright': [ + /(\({\))($|\s|<)/g, + '$2' + ], + + 'kis': [ + /(:-?{})($|\s|<)/gi, + '$2' + ], + + 'lamp': [ + /(\(I\))($|\s|<)/g, + '$2' + ], + + 'lion': [ + /(:-?3)($|\s|<)/gi, + '$2' + ], + + 'mail': [ + /(\(E\))($|\s|<)/g, + '$2' + ], + + 'moon': [ + /(\(S\))($|\s|<)/g, + '$2' + ], + + 'music': [ + /(\(8\))($|\s|<)/g, + '$2' + ], + + 'oh': [ + /((=-?O)|(:-?O))($|\s|<)/gi, + '$4' + ], + + 'phone': [ + /(\(T\))($|\s|<)/g, + '$2' + ], + + 'photo': [ + /(\(P\))($|\s|<)/g, + '$2' + ], + + 'puke': [ + /(:-?!)($|\s|<)/gi, + '$2' + ], + + 'pussy': [ + /(\(@\))($|\s|<)/g, + '$2' + ], + + 'rainbow': [ + /(\(R\))($|\s|<)/g, + '$2' + ], + + 'smile': [ + /(:-?\))($|\s|<)/gi, + '$2' + ], + + 'star': [ + /(\(\*\))($|\s|<)/g, + '$2' + ], + + 'stare': [ + /(:-?\|)($|\s|<)/gi, + '$2' + ], + + 'thumbdown': [ + /(\(N\))($|\s|<)/g, + '$2' + ], + + 'thumbup': [ + /(\(Y\))($|\s|<)/g, + '$2' + ], + + 'tongue': [ + /(:-?P)($|\s|<)/gi, + '$2' + ], + + 'unhappy': [ + /(:-?\()($|\s|<)/gi, + '$2' + ], + + 'wink': [ + /(;-?\))($|\s|<)/gi, + '$2' + ] + + }, + + 'formatting': { + 'bold': [ + /(^|\s|>|\()((\*)([^<>'"\*]+)(\*))($|\s|<|\))/gi, + '$1$2$6' + ], + + 'italic': [ + /(^|\s|>|\()((\/)([^<>'"\/]+)(\/))($|\s|<|\))/gi, + '$1$2$6' + ], + + 'underline': [ + /(^|\s|>|\()((_)([^<>'"_]+)(_))($|\s|<|\))/gi, + '$1$2$6' + ] + } + + }; + + self.xhtml_allow = { + 'elements': [ + 'a', + 'abbr', + 'acronym', + 'address', + 'blockquote', + 'body', + 'br', + 'cite', + 'code', + 'dd', + 'dfn', + 'div', + 'dt', + 'em', + 'h1', + 'h2', + 'h3', + 'h4', + 'h5', + 'h6', + 'head', + 'html', + 'kbd', + 'li', + 'ol', + 'p', + 'pre', + 'q', + 'samp', + 'span', + 'strong', + 'title', + 'ul', + 'var' + ], + + 'attributes': [ + 'accesskey', + 'alt', + 'charset', + 'cite', + 'class', + 'height', + 'href', + 'hreflang', + 'id', + 'longdesc', + 'profile', + 'rel', + 'rev', + 'src', + 'style', + 'tabindex', + 'title', + 'type', + 'uri', + 'version', + 'width', + 'xml:lang', + 'xmlns' + ] + }; + + /** * Generates a given emoticon HTML code * @public @@ -42,83 +336,64 @@ var Filter = (function () { /** * Filters a given message * @public - * @param {string} neutralMessage + * @param {string} message * @param {string} nick * @param {string} html_escape * @return {string} */ - self.message = function(neutralMessage, nick, html_escape) { + self.message = function(message, nick, html_escape) { try { - var filteredMessage = neutralMessage; + var filtered = message; // We encode the HTML special chars - if(html_escape) - filteredMessage = filteredMessage.htmlEnc(); - - // /me command - filteredMessage = filteredMessage.replace(/((^)|((.+)(>)))(\/me )([^<]+)/, nick + ' $7') - - // We replace the smilies text into images - .replace(/(:-?@)($|\s|<)/gi, self.emoteImage('angry', '$1', '$2')) - .replace(/(:-?\[)($|\s|<)/gi, self.emoteImage('bat', '$1', '$2')) - .replace(/(\(B\))($|\s|<)/g, self.emoteImage('beer', '$1', '$2')) - .replace(/((:-?D)|(XD))($|\s|<)/gi, self.emoteImage('biggrin', '$1', '$4')) - .replace(/(:-?\$)($|\s|<)/gi, self.emoteImage('blush', '$1', '$2')) - .replace(/(\(Z\))($|\s|<)/g, self.emoteImage('boy', '$1', '$2')) - .replace(/(\(W\))($|\s|<)/g, self.emoteImage('brflower', '$1', '$2')) - .replace(/((<\/3)|(\(U\)))($|\s|<)/g, self.emoteImage('brheart', '$1', '$4')) - .replace(/(\(C\))($|\s|<)/g, self.emoteImage('coffee', '$1', '$2')) - .replace(/((8-\))|(\(H\)))($|\s|<)/g, self.emoteImage('coolglasses', '$1', '$4')) - .replace(/(:'-?\()($|\s|<)/gi, self.emoteImage('cry', '$1', '$2')) - .replace(/(\(%\))($|\s|<)/g, self.emoteImage('cuffs', '$1', '$2')) - .replace(/(\]:-?>)($|\s|<)/gi, self.emoteImage('devil', '$1', '$2')) - .replace(/(\(D\))($|\s|<)/g, self.emoteImage('drink', '$1', '$2')) - .replace(/(@}->--)($|\s|<)/gi, self.emoteImage('flower', '$1', '$2')) - .replace(/((:-?\/)|(:-?S))($|\s|<)/gi, self.emoteImage('frowning', '$1', '$4')) - .replace(/(\(X\))($|\s|<)/g, self.emoteImage('girl', '$1', '$2')) - .replace(/((<3)|(\(L\)))($|\s|<)/g, self.emoteImage('heart', '$1', '$4')) - .replace(/(\(}\))($|\s|<)/g, self.emoteImage('hugleft', '$1', '$2')) - .replace(/(\({\))($|\s|<)/g, self.emoteImage('hugright', '$1', '$2')) - .replace(/(:-?{})($|\s|<)/gi, self.emoteImage('kiss', '$1', '$2')) - .replace(/(\(I\))($|\s|<)/g, self.emoteImage('lamp', '$1', '$2')) - .replace(/(:-?3)($|\s|<)/gi, self.emoteImage('lion', '$1', '$2')) - .replace(/(\(E\))($|\s|<)/g, self.emoteImage('mail', '$1', '$2')) - .replace(/(\(S\))($|\s|<)/g, self.emoteImage('moon', '$1', '$2')) - .replace(/(\(8\))($|\s|<)/g, self.emoteImage('music', '$1', '$2')) - .replace(/((=-?O)|(:-?O))($|\s|<)/gi, self.emoteImage('oh', '$1', '$4')) - .replace(/(\(T\))($|\s|<)/g, self.emoteImage('phone', '$1', '$2')) - .replace(/(\(P\))($|\s|<)/g, self.emoteImage('photo', '$1', '$2')) - .replace(/(:-?!)($|\s|<)/gi, self.emoteImage('puke', '$1', '$2')) - .replace(/(\(@\))($|\s|<)/g, self.emoteImage('pussy', '$1', '$2')) - .replace(/(\(R\))($|\s|<)/g, self.emoteImage('rainbow', '$1', '$2')) - .replace(/(:-?\))($|\s|<)/gi, self.emoteImage('smile', '$1', '$2')) - .replace(/(\(\*\))($|\s|<)/g, self.emoteImage('star', '$1', '$2')) - .replace(/(:-?\|)($|\s|<)/gi, self.emoteImage('stare', '$1', '$2')) - .replace(/(\(N\))($|\s|<)/g, self.emoteImage('thumbdown', '$1', '$2')) - .replace(/(\(Y\))($|\s|<)/g, self.emoteImage('thumbup', '$1', '$2')) - .replace(/(:-?P)($|\s|<)/gi, self.emoteImage('tongue', '$1', '$2')) - .replace(/(:-?\()($|\s|<)/gi, self.emoteImage('unhappy', '$1', '$2')) - .replace(/(;-?\))($|\s|<)/gi, self.emoteImage('wink', '$1', '$2')) - - // Text in bold - .replace(/(^|\s|>|\()((\*)([^<>'"\*]+)(\*))($|\s|<|\))/gi, '$1$2$6') - - // Italic text - .replace(/(^|\s|>|\()((\/)([^<>'"\/]+)(\/))($|\s|<|\))/gi, '$1$2$6') - - // Underlined text - .replace(/(^|\s|>|\()((_)([^<>'"_]+)(_))($|\s|<|\))/gi, '$1$2$6'); - - // Add the links if(html_escape) { - filteredMessage = Links.apply(filteredMessage, 'desktop'); + filtered = filtered.htmlEnc(); } - // Filter integratebox links - filteredMessage = IntegrateBox.filter(filteredMessage); + // Security: don't filter huge messages (avoids crash attacks) + if(filtered.length < 10000) { + // /me command + filtered = filtered.replace(self.message_regex.commands.me, nick + ' $7'); + + // We replace the smilies text into images + var cur_emote; + + for(var cur_emote_name in self.message_regex.emotes) { + cur_emote = self.message_regex.emotes[cur_emote_name]; + + filtered = filtered.replace( + cur_emote[0], + self.emoteImage( + cur_emote_name, + '$1', + cur_emote[1] + ) + ); + } + + // Text formatting + var cur_formatting; + + for(var cur_formatting_name in self.message_regex.formatting) { + cur_formatting = self.message_regex.formatting[cur_formatting_name]; + + filtered = filtered.replace( + cur_formatting[0], + cur_formatting[1] + ); + } + + // Add the links + if(html_escape) { + filtered = Links.apply(filtered, 'desktop'); + } + + // Filter integratebox links + filtered = IntegrateBox.filter(filtered); + } - return filteredMessage; + return filtered; } catch(e) { Console.error('Filter.message', e); } @@ -135,85 +410,23 @@ var Filter = (function () { self.xhtml = function(code) { try { - // Allowed elements array - var elements = new Array( - 'a', - 'abbr', - 'acronym', - 'address', - 'blockquote', - 'body', - 'br', - 'cite', - 'code', - 'dd', - 'dfn', - 'div', - 'dt', - 'em', - 'h1', - 'h2', - 'h3', - 'h4', - 'h5', - 'h6', - 'head', - 'html', - 'kbd', - 'li', - 'ol', - 'p', - 'pre', - 'q', - 'samp', - 'span', - 'strong', - 'title', - 'ul', - 'var' - ); + var code_sel = $(code); - // Allowed attributes array - var attributes = new Array( - 'accesskey', - 'alt', - 'charset', - 'cite', - 'class', - 'height', - 'href', - 'hreflang', - 'id', - 'longdesc', - 'profile', - 'rel', - 'rev', - 'src', - 'style', - 'tabindex', - 'title', - 'type', - 'uri', - 'version', - 'width', - 'xml:lang', - 'xmlns' - ); - // Check if Filter for XHTML-IM images is enabled if(DataStore.getDB(Connection.desktop_hash, 'options', 'no-xhtml-images') != '1') { - elements.push("img"); + self.xhtml_allow.elements.push("img"); } // Remove forbidden elements - $(code).find('html body *').each(function() { + code_sel.find('html body *').each(function() { // This element is not authorized - if(!Utils.existArrayValue(elements, (this).nodeName.toLowerCase())) + if(!Utils.existArrayValue(self.xhtml_allow.elements, (this).nodeName.toLowerCase())) { $(this).remove(); + } }); // Remove forbidden attributes - $(code).find('html body *').each(function() { + code_sel.find('html body *').each(function() { // Put a pointer on this element (jQuery way & normal way) var cSelector = $(this); var cElement = (this); @@ -226,15 +439,17 @@ var Filter = (function () { var cVal = cAttr.value; // This attribute is not authorized, or contains JS code - if(!Utils.existArrayValue(attributes, cName.toLowerCase()) || ((cVal.toLowerCase()).match(/(^|"|')javascript:/))) + if(!Utils.existArrayValue(self.xhtml_allow.attributes, cName.toLowerCase()) || + ((cVal.toLowerCase()).match(/(^|"|')javascript:/))) { cSelector.removeAttr(cName); + } }); }); // Filter some other elements - $(code).find('a').attr('target', '_blank'); + code_sel.find('a').attr('target', '_blank'); - return $(code).find('html body').html(); + return code_sel.find('html body').html(); } catch(e) { Console.error('Filter.xhtml', e); } diff --git a/source/app/javascripts/groupchat.js b/source/app/javascripts/groupchat.js index 1a5b778..ccf9a0e 100644 --- a/source/app/javascripts/groupchat.js +++ b/source/app/javascripts/groupchat.js @@ -24,6 +24,196 @@ var Groupchat = (function () { var JOIN_SUGGEST = []; + /** + * Apply generate events + * @private + * @param {object} input_sel + * @param {string} hash + * @param {string} room + * @return {undefined} + */ + self._createEvents = function(input_sel, hash, room) { + + try { + self._createEventsInput(input_sel, hash); + self._createEventsKey(input_sel, hash, room); + } catch(e) { + Console.error('Groupchat._createEvents', e); + } + + }; + + + /** + * Apply generate events (input) + * @private + * @param {object} input_sel + * @param {string} hash + * @return {undefined} + */ + self._createEventsInput = function(input_sel, hash) { + + try { + // Focus event + input_sel.focus(function() { + // Clean notifications for this chat + Interface.chanCleanNotify(hash); + + // Store focus on this chat! + Interface.chat_focus_hash = hash; + }); + + // Blur event + input_sel.blur(function() { + // Reset storage about focus on this chat! + if(Interface.chat_focus_hash == hash) { + Interface.chat_focus_hash = null; + } + + // Reset autocompletion + Autocompletion.reset(hash); + }); + } catch(e) { + Console.error('Groupchat._createEventsInput', e); + } + + }; + + + /** + * Apply generate events (key) + * @private + * @param {object} input_sel + * @param {string} hash + * @param {string} room + * @return {undefined} + */ + self._createEventsKey = function(input_sel, hash, room) { + + try { + // Lock to the input + input_sel.keydown(function(e) { + // Enter key + if(e.keyCode == 13) { + // If shift key (without any others modifiers) was pressed, add a new line + if(e.shiftKey && !e.ctrlKey && !e.altKey && !e.metaKey) { + input_sel.val(input_sel.val() + '\n'); + } else { + if(Correction.isIn(room) === true) { + var corrected_value = input_sel.val().trim(); + + if(corrected_value) { + // Send the corrected message + Correction.send(room, 'groupchat', corrected_value); + } + + Correction.leave(room); + } else { + // Send the message + Message.send(hash, 'groupchat'); + + // Reset the composing database entry + DataStore.setDB(Connection.desktop_hash, 'chatstate', room, 'off'); + } + } + + return false; + } + + // Remove chars (leave correction) + else if(e.keyCode == 8) { + // Leave correction mode? (another way, by flushing input value progressively) + if(Correction.isIn(room) === true && !input_sel.val()) { + Correction.leave(room); + } + } + + // Tabulation key (without any modifiers) + else if(!e.shiftKey && !e.ctrlKey && !e.altKey && !e.metaKey && e.keyCode == 9) { + Autocompletion.create(hash); + + return false; + } + + // Reset the autocompleter + else { + Autocompletion.reset(hash); + } + }); + + input_sel.keyup(function(e) { + if(e.keyCode == 27) { + // Escape key + input_sel.val(''); + + // Leave correction mode? (simple escape way) + if(Correction.isIn(room) === true) { + Correction.leave(room); + } + } else { + Correction.detect(room, input_sel); + } + }); + } catch(e) { + Console.error('Groupchat._createEventsKey', e); + } + + }; + + + /** + * Apply suggest check events + * @private + * @return {undefined} + */ + self._suggestCheckEvents = function() { + + try { + // Click events + $('#suggest .content a.one').click(function() { + var this_sel = $(this); + + // Add/remove the active class + this_sel.toggleClass('active'); + + // We require at least one room to be chosen + if(Common.exists('#suggest .content a.one.active')) { + $('#suggest a.next').removeClass('disabled'); + } else { + $('#suggest a.next').addClass('disabled'); + } + + return false; + }); + + $('#suggest a.next').click(function() { + var this_sel = $(this); + + // Disabled? + if(this_sel.hasClass('disabled')) { + return false; + } + + // Store groupchats to join? + if(this_sel.is('.continue')) { + $('#suggest .content a.one.active').each(function() { + JOIN_SUGGEST.push(this_sel.attr('data-xid')); + }); + } + + // Switch to talk UI + $('#suggest').remove(); + Connection.triggerConnected(); + + return false; + }); + } catch(e) { + Console.error('Groupchat._suggestCheckEvents', e); + } + + }; + + /** * Displays the MUC admin elements * @public @@ -37,16 +227,19 @@ var Groupchat = (function () { try { // We must be in the "login" mode - if(Utils.isAnonymous()) + if(Utils.isAnonymous()) { return; + } // We check if the user is a room owner or administrator to give him privileges - if(affiliation == 'owner' || affiliation == 'admin') + if(affiliation == 'owner' || affiliation == 'admin') { $('#' + id + ' .tools-mucadmin').show(); + } // We check if the room hasn't been yet created - if(statuscode == 201) + if(statuscode == 201) { Board.openThisInfo(4); + } // We add the click event $('#' + id + ' .tools-mucadmin').click(function() { @@ -80,14 +273,16 @@ var Groupchat = (function () { // No nickname? if(!nickname) { // Get some values - if(!Utils.isAnonymous()) + if(!Utils.isAnonymous()) { nickname = Name.getNick(); - else + } else { nickname = ANONYMOUS_NICK; + } // If the nickname could not be retrieved, ask it - if(!nickname) + if(!nickname) { self.generateMUCAsk('nickname', room, hash, nickname, password); + } } // Got our nickname? @@ -126,10 +321,12 @@ var Groupchat = (function () { var room = Common.bareXID(from); var nickname = Common.thisResource(from); var hash = hex_md5(room); + var id = presence.getID(); // No ID: must fix M-Link bug - if(presence.getID() === null) { - presence.setID(1); + if(id === null) { + id = 1; + presence.setID(id); } Console.info('First MUC presence: ' + from); @@ -139,11 +336,17 @@ var Groupchat = (function () { // Define some stuffs var muc_user = $(xml).find('x[xmlns="' + NS_MUC_USER + '"]'); var affiliation = muc_user.find('item').attr('affiliation'); - var statuscode = parseInt(muc_user.find('status').attr('code')); + var statuscode = parseInt(muc_user.find('status').attr('code')); // Handle my presence Presence.handle(presence); + // Configure the new room + if(affiliation == 'owner' || affiliation == 'admin') { + console.debug('presence', presence.xml()); + self._initialConfiguration(id, room); + } + // Check if I am a room owner self.openAdmin(affiliation, hash, room, statuscode); @@ -279,65 +482,16 @@ var Groupchat = (function () { }); // Must show the add button? - if(!DataStore.existDB('favorites', room)) + if(!DataStore.existDB(Connection.desktop_hash, 'favorites', room)) { $('#' + hash + ' .tools-add').show(); + } // The event handlers - var inputDetect = $('#' + hash + ' .message-area'); - - // Focus event - inputDetect.focus(function() { - // Clean notifications for this chat - Interface.chanCleanNotify(hash); - - // Store focus on this chat! - Interface.chat_focus_hash = hash; - }); - - // Blur event - inputDetect.blur(function() { - // Reset storage about focus on this chat! - if(Interface.chat_focus_hash == hash) - Interface.chat_focus_hash = null; - - // Reset autocompletion - Autocompletion.reset(hash); - }); - - // Lock to the input - inputDetect.keydown(function(e) { - // Enter key - if(e.keyCode == 13) { - // If shift key (without any others modifiers) was pressed, add a new line - if(e.shiftKey && !e.ctrlKey && !e.altKey && !e.metaKey) - inputDetect.val(inputDetect.val() + '\n'); - - // Send the message - else { - Message.send(hash, 'groupchat'); - - // Reset the composing database entry - DataStore.setDB(Connection.desktop_hash, 'chatstate', room, 'off'); - } - - return false; - } - - // Tabulation key (without any modifiers) - else if(!e.shiftKey && !e.ctrlKey && !e.altKey && !e.metaKey && e.keyCode == 9) { - Autocompletion.create(hash); - - return false; - } - - // Reset the autocompleter - else { - Autocompletion.reset(hash); - } - }); + var input_sel = $('#' + hash + ' .message-area'); + self._createEvents(input_sel, hash, room); // Chatstate events - ChatState.events(inputDetect, room, hash, 'groupchat'); + ChatState.events(input_sel, room, hash, 'groupchat'); // Get the current muc informations and content self.getMUC(room, nickname, password); @@ -361,23 +515,26 @@ var Groupchat = (function () { var new_arr = []; // Try to split it - if(GROUPCHATS_JOIN.indexOf(',') != -1) + if(GROUPCHATS_JOIN.indexOf(',') != -1) { muc_arr = GROUPCHATS_JOIN.split(','); + } for(var i in muc_arr) { // Get the current value var muc_current = $.trim(muc_arr[i]); // No current value? - if(!muc_current) + if(!muc_current) { continue; + } // Filter the current value muc_current = Common.generateXID(muc_current, 'groupchat'); // Add the current value - if(!Utils.existArrayValue(new_arr, muc_current)) + if(!Utils.existArrayValue(new_arr, muc_current)) { new_arr.push(muc_current); + } } return new_arr; @@ -397,8 +554,9 @@ var Groupchat = (function () { try { // Nothing to join? - if(!JOIN_SUGGEST) + if(!JOIN_SUGGEST) { return; + } // Join the chats if(JOIN_SUGGEST.length) { @@ -425,8 +583,9 @@ var Groupchat = (function () { // Must suggest the user? if((GROUPCHATS_SUGGEST == 'on') && groupchat_arr.length) { - if(Common.exists('#suggest')) + if(Common.exists('#suggest')) { return; + } // Create HTML code var html = '
'; @@ -452,39 +611,8 @@ var Groupchat = (function () { // Append HTML code $('body').append(html); - // Click events - $('#suggest .content a.one').click(function() { - // Add/remove the active class - $(this).toggleClass('active'); - - // We require at least one room to be chosen - if(Common.exists('#suggest .content a.one.active')) - $('#suggest a.next').removeClass('disabled'); - else - $('#suggest a.next').addClass('disabled'); - - return false; - }); - - $('#suggest a.next').click(function() { - // Disabled? - if($(this).hasClass('disabled')) { - return false; - } - - // Store groupchats to join? - if($(this).is('.continue')) { - $('#suggest .content a.one.active').each(function() { - JOIN_SUGGEST.push($(this).attr('data-xid')); - }); - } - - // Switch to talk UI - $('#suggest').remove(); - Connection.triggerConnected(); - - return false; - }); + // Attach events + self._suggestCheckEvents(); } else { JOIN_SUGGEST = groupchat_arr; @@ -512,7 +640,7 @@ var Groupchat = (function () { if(!ban_xid) { Board.openThisInfo(6); - Console.warning('Could not ban user with XID: ' + ban_xid + ' from room: ' + room_xid); + Console.warn('Could not ban user with XID: ' + ban_xid + ' from room: ' + room_xid); } else { // We generate the ban IQ var iq = new JSJaCIQ(); @@ -520,10 +648,16 @@ var Groupchat = (function () { iq.setType('set'); var iqQuery = iq.setQuery(NS_MUC_ADMIN); - var item = iqQuery.appendChild(iq.buildNode('item', {'affiliation': 'outcast', 'jid': ban_xid, 'xmlns': NS_MUC_ADMIN})); + var item = iqQuery.appendChild(iq.buildNode('item', { + 'affiliation': 'outcast', + 'jid': ban_xid, + 'xmlns': NS_MUC_ADMIN + })); if(reason) { - item.appendChild(iq.buildNode('reason', {'xmlns': NS_MUC_ADMIN}, reason)); + item.appendChild(iq.buildNode('reason', { + 'xmlns': NS_MUC_ADMIN + }, reason)); } con.send(iq, Errors.handleReply); @@ -561,10 +695,16 @@ var Groupchat = (function () { iq.setType('set'); var iqQuery = iq.setQuery(NS_MUC_ADMIN); - var item = iqQuery.appendChild(iq.buildNode('item', {'nick': nick, 'role': 'none', 'xmlns': NS_MUC_ADMIN})); + var item = iqQuery.appendChild(iq.buildNode('item', { + 'nick': nick, + 'role': 'none', + 'xmlns': NS_MUC_ADMIN + })); if(reason) { - item.appendChild(iq.buildNode('reason', {'xmlns': NS_MUC_ADMIN}, reason)); + item.appendChild(iq.buildNode('reason', { + 'xmlns': NS_MUC_ADMIN + }, reason)); } con.send(iq, Errors.handleReply); @@ -670,6 +810,50 @@ var Groupchat = (function () { } }; + + /** + * Sends initial configuration of the room + * @private + * @param {string} pid + * @param {string} xid + * @return {undefined} + */ + self._initialConfiguration = function(pid, xid) { + + try { + var iq = new JSJaCIQ(); + + iq.setTo(xid); + iq.setType('set'); + iq.setID('first-muc-config-' + pid); + + var iqQuery = iq.setQuery(NS_MUC_OWNER); + + // Configure room with nil(null) fields + var iqX = iqQuery.appendChild(iq.buildNode('x', { + 'xmlns': NS_XDATA, + 'type': 'submit' + })); + + // Build a new field node + var iqField = iqX.appendChild(iq.buildNode('field', { + 'var': 'FORM_TYPE', + 'type': 'hidden', + 'xmlns': NS_XDATA + })); + + iqField.appendChild(iq.buildNode('value', { + 'xmlns': NS_XDATA + }, NS_MUC_CONFIG)); + + con.send(iq); + + Console.info('Groupchat._initialConfiguration', 'Sent initial room configuration: ' + xid); + } catch(e) { + Console.error('Groupchat._initialConfiguration', e); + } + }; + /** @@ -677,4 +861,4 @@ var Groupchat = (function () { */ return self; -})(); \ No newline at end of file +})(); diff --git a/source/app/javascripts/home.js b/source/app/javascripts/home.js index a733181..b66f29c 100644 --- a/source/app/javascripts/home.js +++ b/source/app/javascripts/home.js @@ -20,6 +20,119 @@ var Home = (function () { var self = {}; + /** + * Apply change events + * @private + * @param {object} current_sel + * @param {string} div + * @return {undefined} + */ + self._eventsChange = function(current_sel, div) { + + try { + // Create the attached events + switch(div) { + // Login tool + case 'loginer': + current_sel.find('a.to-anonymous').click(function() { + return self.change('anonymouser'); + }); + + current_sel.find('a.advanced').click(self.showAdvanced); + current_sel.find('form').submit(self.loginForm); + + break; + + // Anonymous login tool + case 'anonymouser': + current_sel.find('a.to-home').click(function() { + return self.change('loginer'); + }); + + current_sel.find('form').submit(Connection.doAnonymous); + + // Keyup event on anonymous join's room input + current_sel.find('input.room').keyup(function() { + var value = $(this).val(); + var report_sel = current_sel.find('.report'); + var span_sel = current_sel.find('span'); + + if(!value) { + report_sel.hide(); + span_sel.text(''); + } else { + report_sel.show(); + span_sel.text(JAPPIX_LOCATION + '?r=' + value); + } + }); + + break; + + // Register tool + case 'registerer': + // Server input change + $('#home input.server').keyup(function(e) { + if($.trim($(this).val()) == HOST_MAIN) { + $('#home .captcha_grp').show(); + $('#home input.captcha').removeAttr('disabled'); + } else { + $('#home .captcha_grp').hide(); + $('#home input.captcha').attr('disabled', true); + } + }); + + // Register input placeholder + // FIXME: breaks IE compatibility + //$('#home input[placeholder]').placeholder(); + + // Register form submit + current_sel.find('form').submit(self.registerForm); + + break; + } + } catch(e) { + Console.error('Home._eventsChange', e); + } + + }; + + + /** + * Create obsolete form + * @private + * @param {string} home + * @param {string} locale + * @return {undefined} + */ + self._obsolete = function(home, locale) { + + try { + // Add the code + $(locale).after( + '
' + + '

' + Common._e("Your browser is out of date!") + '

' + + + '' + + '' + + '' + + '' + + '' + + '
' + ); + + // Display it later + $(home + '.obsolete').oneTime('1s', function() { + $(this).slideDown(); + }); + + Console.warn('Jappix does not support this browser!'); + } catch(e) { + Console.error('Home._obsolete', e); + } + + }; + + /** * Allows the user to switch the difference home page elements * @public @@ -76,7 +189,7 @@ var Home = (function () { '' + Common._e("Required") + '' + '' + - '@' + + '@' + '' + '' + '' + @@ -112,8 +225,9 @@ var Home = (function () { disable_form = Utils.disableInput(ANONYMOUS, 'off'); code = '

' + Common.printf(Common._e("Enter the groupchat you want to join and the nick you want to have. You can also go back to the %s."), '' + Common._e("login page") + '') + '

'; - if(LEGAL) + if(LEGAL) { code += '

' + Common.printf(Common._e("By using our service, you accept %s."), '' + Common._e("our terms of use") + '') + '

'; + } code += '
' + '
' + @@ -139,27 +253,30 @@ var Home = (function () { case 'registerer': disable_form = Utils.disableInput(REGISTRATION, 'off'); - if(!disable_form) + if(!disable_form) { lock_host = Utils.disableInput(LOCK_HOST, 'on'); + } code = '

' + Common._e("Register a new XMPP account to join your friends on your own social cloud. That's simple!") + '

'; - if(LEGAL) + if(LEGAL) { code += '

' + Common.printf(Common._e("By using our service, you accept %s."), '' + Common._e("our terms of use") + '') + '

'; + } code += '' + '
' + '' + Common._e("Required") + '' + '' + - '@' + + '@' + '' + ''; - if(REGISTER_API == 'on') + if(REGISTER_API == 'on') { code += '
' + '' + '
'; + } code += '
' + @@ -170,78 +287,22 @@ var Home = (function () { } // Form disabled? - if(disable_form) + if(disable_form) { code += '
' + Common._e("This tool has been disabled!") + '
'; + } // Create this HTML code if(code && !Common.exists(current)) { - // Append it! - $(right + '.homediv.default').after('
' + code + '
'); + $(right + '.homediv.default').after( + '
' + code + '
' + ); - // Create the attached events - switch(div) { - // Login tool - case 'loginer': - $(current + ' a.to-anonymous').click(function() { - return self.change('anonymouser'); - }); - - $(current + ' a.advanced').click(self.showAdvanced); - $(current + ' form').submit(self.loginForm); - - break; - - // Anonymous login tool - case 'anonymouser': - $(current + ' a.to-home').click(function() { - return self.change('loginer'); - }); - - $(current + ' form').submit(Connection.doAnonymous); - - // Keyup event on anonymous join's room input - $(current + ' input.room').keyup(function() { - var value = $(this).val(); - var report = current + ' .report'; - var span = report + ' span'; - - if(!value) { - $(report).hide(); - $(span).text(''); - } - - else { - $(report).show(); - $(span).text(JAPPIX_LOCATION + '?r=' + value); - } - }); - - break; - - // Register tool - case 'registerer': - // Server input change - $('#home input.server').keyup(function(e) { - if($.trim($(this).val()) == HOST_MAIN) { - $('#home .captcha_grp').show(); - $('#home input.captcha').removeAttr('disabled'); - } else { - $('#home .captcha_grp').hide(); - $('#home input.captcha').attr('disabled', true); - } - }); - - // Register input placeholder - // FIXME: breaks IE compatibility - //$('#home input[placeholder]').placeholder(); - - // Register form submit - $(current + ' form').submit(self.registerForm); - - break; - } + self._eventsChange( + $(current), + div + ); } // We focus on the first input @@ -288,13 +349,14 @@ var Home = (function () { try { // We get the values - var lPath = '#home .loginer '; - var lServer = $(lPath + '.server').val(); - var lNick = Common.nodeprep($(lPath + '.nick').val()); - var lPass = $(lPath + '.password').val(); - var lResource = $(lPath + '.resource').val(); - var lPriority = $(lPath + '.priority').val(); - var lRemember = $(lPath + '.remember').filter(':checked').size(); + var path_sel = $('#home .loginer'); + + var lServer = path_sel.find('.server').val(); + var lNick = Common.nodeprep(path_sel.find('.nick').val()); + var lPass = path_sel.find('.password').val(); + var lResource = path_sel.find('.resource').val(); + var lPriority = path_sel.find('.priority').val(); + var lRemember = path_sel.find('.remember').filter(':checked').size(); // Enough values? if(lServer && lNick && lPass && lResource && lPriority) { @@ -303,12 +365,13 @@ var Home = (function () { $(lPath + 'input[type="text"], ' + lPath + 'input[type="password"]').each(function() { var select = $(this); - if(!select.val()) + if(!select.val()) { $(document).oneTime(10, function() { select.addClass('please-complete').focus(); }); - else + } else { select.removeClass('please-complete'); + } }); } } catch(e) { @@ -328,17 +391,18 @@ var Home = (function () { self.registerForm = function() { try { - var rPath = '#home .registerer '; + var path = '#home .registerer'; + var path_sel = $(path); // Remove the success info - $(rPath + '.success').remove(); + path_sel.find('.success').remove(); // Get the values - var username = Common.nodeprep($(rPath + '.nick').val()); - var domain = $(rPath + '.server').val(); - var pass = $(rPath + '.password').val(); - var spass = $(rPath + '.spassword').val(); - var captcha = $(rPath + '.captcha').val(); + var username = Common.nodeprep(path_sel.find('.nick').val()); + var domain = path_sel.find('.server').val(); + var pass = path_sel.find('.password').val(); + var spass = path_sel.find('.spassword').val(); + var captcha = path_sel.find('.captcha').val(); // Enough values? if(domain && username && pass && spass && (pass == spass) && !((REGISTER_API == 'on') && (domain == HOST_MAIN) && !captcha)) { @@ -351,15 +415,16 @@ var Home = (function () { // Something is missing? else { - $(rPath + 'input[type="text"], ' + rPath + 'input[type="password"]').each(function() { + $(path + ' input[type="text"], ' + path + ' input[type="password"]').each(function() { var select = $(this); - if(!select.val() || (select.is('#spassword') && pass && (pass != spass))) + if(!select.val() || (select.is('#spassword') && pass && (pass != spass))) { $(document).oneTime(10, function() { select.addClass('please-complete').focus(); }); - else + } else { select.removeClass('please-complete'); + } }); } } catch(e) { @@ -394,12 +459,14 @@ var Home = (function () { // Allows the user to switch the home page $(button).click(function() { // Login button - if($(this).is('.login')) + if($(this).is('.login')) { return self.change('loginer'); + } // Register button - else + else { return self.change('registerer'); + } }); // Allows the user to view the corporation & about infobox @@ -430,31 +497,14 @@ var Home = (function () { // Disables the browser HTTP-requests stopper $(document).keydown(function(e) { - if((e.keyCode == 27) && !System.isDeveloper()) + if((e.keyCode == 27) && !System.isDeveloper()) { return false; + } }); // Warns for an obsolete browser if(Utils.isObsolete()) { - // Add the code - $(locale).after( - '
' + - '

' + Common._e("Your browser is out of date!") + '

' + - - '' + - '' + - '' + - '' + - '' + - '
' - ); - - // Display it later - $(home + '.obsolete').oneTime('1s', function() { - $(this).slideDown(); - }); - - Console.warn('Jappix does not support this browser!'); + self._obsolete(); } Console.log('Welcome to Jappix! Happy coding in developer mode!'); @@ -473,4 +523,4 @@ var Home = (function () { })(); -Home.launch(); \ No newline at end of file +Home.launch(); diff --git a/source/app/javascripts/httpauth.js b/source/app/javascripts/httpauth.js index 070181c..6d2b764 100644 --- a/source/app/javascripts/httpauth.js +++ b/source/app/javascripts/httpauth.js @@ -34,8 +34,6 @@ var HTTPAuth = (function () { try { // We add the login wait div Interface.showGeneralWait(); - - oArgs = {}; if(Common.hasWebSocket()) { // WebSocket supported & configured @@ -67,15 +65,6 @@ var HTTPAuth = (function () { // Generate a priority lPriority = lPriority ? lPriority : 10; - // We retrieve what the user typed in the login inputs - oArgs = {}; - oArgs.domain = $.trim(lServer); - oArgs.username = $.trim(lNick); - oArgs.resource = random_resource; - oArgs.pass = lPass; - oArgs.secure = true; - oArgs.xmllang = XML_LANG; - // Store the resource (for reconnection) DataStore.setDB(Connection.desktop_hash, 'session', 'resource', random_resource); @@ -89,7 +78,14 @@ var HTTPAuth = (function () { DataStore.setDB(Connection.desktop_hash, 'priority', 1, 10); // We connect ! - con.connect(oArgs); + con.connect({ + 'domain': $.trim(lServer), + 'username': $.trim(lNick), + 'resource': random_resource, + 'pass': lPass, + 'secure': true, + 'xmllang': XML_LANG + }); // Change the page title Interface.title('wait'); diff --git a/source/app/javascripts/httpreply.js b/source/app/javascripts/httpreply.js index 9f079ad..6b839bd 100644 --- a/source/app/javascripts/httpreply.js +++ b/source/app/javascripts/httpreply.js @@ -45,11 +45,19 @@ var HTTPReply = (function () { // If "no" if(value == 'no') { aMsg.setType('error'); - aMsg.appendNode('error', {'code': '401', 'type': 'auth'}); + aMsg.appendNode('error', { + 'code': '401', + 'type': 'auth' + }); } // We set the confirm node - aMsg.appendNode('confirm', {'xmlns': xmlns, 'url': url, 'id': id, 'method': method}); + aMsg.appendNode('confirm', { + 'xmlns': xmlns, + 'url': url, + 'id': id, + 'method': method + }); // We send the message con.send(aMsg, Errors.handleReply); diff --git a/source/app/javascripts/inbox.js b/source/app/javascripts/inbox.js index cd99ff4..437bdc5 100644 --- a/source/app/javascripts/inbox.js +++ b/source/app/javascripts/inbox.js @@ -176,17 +176,16 @@ var Inbox = (function () { var value = $(Common.XMLFromString(DataStore.storageDB.getItem(current))); // Create the storage node - storage.appendChild(iq.buildNode('message', { - 'id': value.find('id').text().revertHtmlEnc(), - 'from': value.find('from').text().revertHtmlEnc(), - 'subject': value.find('subject').text().revertHtmlEnc(), - 'status': value.find('status').text().revertHtmlEnc(), - 'date': value.find('date').text().revertHtmlEnc(), - 'xmlns': NS_INBOX - }, - - value.find('content').text().revertHtmlEnc() - )); + storage.appendChild( + iq.buildNode('message', { + 'id': value.find('id').text().revertHtmlEnc(), + 'from': value.find('from').text().revertHtmlEnc(), + 'subject': value.find('subject').text().revertHtmlEnc(), + 'status': value.find('status').text().revertHtmlEnc(), + 'date': value.find('date').text().revertHtmlEnc(), + 'xmlns': NS_INBOX + }, value.find('content').text().revertHtmlEnc() + )); } } @@ -207,18 +206,18 @@ var Inbox = (function () { try { // Init - var mPath = '#inbox .'; + var inbox_sel = $('#inbox'); // Reset the previous buddy search Search.resetBuddy('#inbox .inbox-new-to'); // We switch the divs - $(mPath + 'inbox-results, #inbox .a-new-message, #inbox .a-delete-messages').hide(); - $(mPath + 'inbox-new, #inbox .a-show-messages').show(); + inbox_sel.find('.inbox-results, .a-new-message, .a-delete-messages').hide(); + inbox_sel.find('.inbox-new, .a-show-messages').show(); // We focus on the first input $(document).oneTime(10, function() { - $(mPath + 'inbox-new-to-input').focus(); + inbox_sel.find('.inbox-new-to-input').focus(); }); // We reset some stuffs @@ -241,16 +240,16 @@ var Inbox = (function () { try { // Init - var mPath = '#inbox .'; + var inbox_sel = $('#inbox'); // We reset the forms - $(mPath + 'inbox-new-block:not(form) input, ' + mPath + 'inbox-new textarea').val('').removeClass('please-complete'); - $(mPath + 'inbox-new-file a').remove(); - $(mPath + 'inbox-new-file input').show(); + inbox_sel.find('.inbox-new-block:not(form) input, .inbox-new textarea').val('').removeClass('please-complete'); + inbox_sel.find('.inbox-new-file a').remove(); + inbox_sel.find('.inbox-new-file input').show(); // We close an eventual opened message - $(mPath + 'message-content').remove(); - $(mPath + 'one-message').removeClass('message-reading'); + inbox_sel.find('.message-content').remove(); + inbox_sel.find('.one-message').removeClass('message-reading'); } catch(e) { Console.error('Inbox.cleanNewMessage', e); } @@ -270,26 +269,26 @@ var Inbox = (function () { try { // We send the message - var mess = new JSJaCMessage(); + var message = new JSJaCMessage(); // Main attributes - mess.setTo(to); - mess.setSubject(subject); - mess.setType('normal'); + message.setTo(to); + message.setSubject(subject); + message.setType('normal'); // Any file to attach? var attached = '#inbox .inbox-new-file a.file'; if(Common.exists(attached)) { body += '\n' + - '\n' + - $(attached).attr('data-attachedtitle') + ' - ' + $(attached).attr('data-attachedhref'); + '\n' + + $(attached).attr('data-attachedtitle') + ' - ' + $(attached).attr('data-attachedhref'); } // Set body - mess.setBody(body); + message.setBody(body); - con.send(mess, Errors.handleReply); + con.send(message, Errors.handleReply); } catch(e) { Console.error('Inbox.sendMessage', e); } @@ -306,10 +305,10 @@ var Inbox = (function () { try { // We get some informations - var mPath = '#inbox '; - var to = $(mPath + '.inbox-new-to-input').val(); - var body = $(mPath + '.inbox-new-textarea').val(); - var subject = $(mPath + '.inbox-new-subject-input').val(); + var inbox_sel = $('#inbox'); + var to = inbox_sel.find('.inbox-new-to-input').val(); + var body = inbox_sel.find('.inbox-new-textarea').val(); + var subject = inbox_sel.find('.inbox-new-subject-input').val(); if(to && body && subject) { // New array of XID @@ -345,15 +344,15 @@ var Inbox = (function () { } else { - $(mPath + 'input[type="text"], ' + mPath + 'textarea').each(function() { - var current = this; + inbox_sel.find('input[type="text"], textarea').each(function() { + var this_sel = $(this); - if(!$(current).val()) { + if(!this_sel.val()) { $(document).oneTime(10, function() { - $(current).addClass('please-complete').focus(); + this_sel.addClass('please-complete').focus(); }); } else { - $(current).removeClass('please-complete'); + this_sel.removeClass('please-complete'); } }); } @@ -374,17 +373,16 @@ var Inbox = (function () { self.showMessage = function() { try { - // Init - var mPath = '#inbox .'; + var inbox_sel = $('#inbox'); // We switch the divs - $(mPath + 'inbox-new').hide(); - $(mPath + 'inbox-results').show(); + inbox_sel.find('.inbox-new').hide(); + inbox_sel.find('.inbox-results').show(); // We show a new link in the menu - $(mPath + 'a-show-messages').hide(); - $(mPath + 'a-delete-messages').show(); - $(mPath + 'a-new-message').show(); + inbox_sel.find('.a-show-messages').hide(); + inbox_sel.find('.a-delete-messages').show(); + inbox_sel.find('.a-new-message').show(); // We reset some stuffs self.cleanNewMessage(); @@ -484,10 +482,14 @@ var Inbox = (function () { try { // Initialize the XML data - var xml = '' + id.htmlEnc().htmlEnc() + '' + date.htmlEnc().htmlEnc() + '' + from.htmlEnc().htmlEnc() + '' + subject.htmlEnc().htmlEnc() + '' + status.htmlEnc().htmlEnc() + '' + content.htmlEnc().htmlEnc() + ''; - - // End the XML data - xml += ''; + var xml = '' + + '' + id.htmlEnc().htmlEnc() + '' + + '' + date.htmlEnc().htmlEnc() + '' + + '' + from.htmlEnc().htmlEnc() + '' + + '' + subject.htmlEnc().htmlEnc() + '' + + '' + status.htmlEnc().htmlEnc() + '' + + '' + content.htmlEnc().htmlEnc() + '' + + ''; // Store this message! DataStore.setDB(Connection.desktop_hash, 'inbox', id, xml); @@ -504,7 +506,7 @@ var Inbox = (function () { * @param {string} id * @return {boolean} */ - self.deleteMessage = function() { + self.deleteMessage = function(id) { try { // Remove the message from the inbox @@ -794,13 +796,13 @@ var Inbox = (function () { // Display the current message self.displayMessage( - value.find('from').text().revertHtmlEnc(), - value.find('subject').text().revertHtmlEnc(), - value.find('content').text().revertHtmlEnc(), - value.find('status').text().revertHtmlEnc(), - value.find('id').text().revertHtmlEnc(), - value.find('date').text().revertHtmlEnc() - ); + value.find('from').text().revertHtmlEnc(), + value.find('subject').text().revertHtmlEnc(), + value.find('content').text().revertHtmlEnc(), + value.find('status').text().revertHtmlEnc(), + value.find('id').text().revertHtmlEnc(), + value.find('date').text().revertHtmlEnc() + ); } } @@ -854,16 +856,30 @@ var Inbox = (function () { // Hide the attach link, show the unattach one $('#inbox .inbox-new-file input').hide(); - $('#inbox .inbox-new-file').append('' + fName.htmlEnc() + '' + Common._e("Remove") + ''); + $('#inbox .inbox-new-file').append( + '' + + fName.htmlEnc() + + '' + + + '' + + Common._e("Remove") + + '' + ); // Set values to the file link - $('#inbox .inbox-new-file a.file').attr('data-attachedtitle', fName) - .attr('data-attachedhref', fURL); + var inbox_file_sel = $('#inbox .inbox-new-file a.file'); + + inbox_file_sel.attr('data-attachedtitle', fName); + inbox_file_sel.attr('data-attachedhref', fURL); // Click events - $('#inbox .inbox-new-file a.remove').click(function() { - $('#inbox .inbox-new-file a').remove(); - $('#inbox .inbox-new-file input').show(); + var inbox_new_file_sel = $('#inbox .inbox-new-file'); + + inbox_new_file_sel.find('a.remove').click(function() { + inbox_new_file_sel.find('a').remove(); + inbox_new_file_sel.find('input').show(); return false; }); @@ -899,10 +915,11 @@ var Inbox = (function () { // Send the message when enter pressend $(inbox + 'inbox-new input').keyup(function(e) { if(e.keyCode == 13) { - if(Common.exists(dHovered)) + if(Common.exists(dHovered)) { Search.addBuddy(destination, $(dHovered).attr('data-xid')); - else + } else { self.checkMessage(); + } } }); @@ -948,7 +965,7 @@ var Inbox = (function () { // File upload var attach_options = { - dataType: 'xml', + dataType: 'xml', beforeSubmit: self.waitAttach, success: self.handleAttach }; diff --git a/source/app/javascripts/integratebox.js b/source/app/javascripts/integratebox.js index 36ad941..5eb38ac 100644 --- a/source/app/javascripts/integratebox.js +++ b/source/app/javascripts/integratebox.js @@ -161,8 +161,9 @@ var IntegrateBox = (function () { self.close(); // Media integration not wanted? - if(DataStore.getDB(Connection.desktop_hash, 'options', 'integratemedias') == '0') + if(DataStore.getDB(Connection.desktop_hash, 'options', 'integratemedias') == '0') { return true; + } // Apply the HTML code var dom_code = self.code(service, url); diff --git a/source/app/javascripts/interface.js b/source/app/javascripts/interface.js index dd7197d..7e62a69 100644 --- a/source/app/javascripts/interface.js +++ b/source/app/javascripts/interface.js @@ -74,8 +74,9 @@ var Interface = (function () { try { // Item exists? - if(Common.exists('#general-wait')) + if(Common.exists('#general-wait')) { return false; + } // Generate the HTML code var html = @@ -178,8 +179,9 @@ var Interface = (function () { var more_content = '#page-switch .more-content'; // Yet displayed? - if(Common.exists(more_content)) + if(Common.exists(more_content)) { return Bubble.close(); + } // Add the bubble Bubble.show(more_content); @@ -397,32 +399,48 @@ var Interface = (function () { var date = DateUtils.getXMPPTime('local'); var type = $('#' + hash).attr('data-type'); var direction = $('html').attr('dir') || 'ltr'; + + var content_sel = $(content); // Filter the content smileys - $(content).find('img.emoticon').each(function() { + content_sel.find('img.emoticon').each(function() { $(this).replaceWith($(this).attr('alt')); }); // Remove the useless attributes - $(content).removeAttr('data-type').removeAttr('data-stamp'); + content_sel.removeAttr('data-type').removeAttr('data-stamp'); // Remove the content avatars - $(content).find('.avatar-container').remove(); + content_sel.find('.avatar-container').remove(); + + // Remove the content info + content_sel.find('.correction-edit, .message-marker, .corrected-info, .correction-label').remove(); // Remove the content click events - $(content).find('a').removeAttr('onclick'); + content_sel.find('a').removeAttr('onclick'); // Extract the content HTML code - content = $(content).parent().html(); + content = content_sel.parent().html(); // No avatar? - if(!avatar || !avatar.match(/data:/)) + if(!avatar || !avatar.match(/data:/)) { avatar = 'none'; + } // POST the values to the server - $.post('./server/generate-chat.php', { 'content': content, 'xid': xid, 'nick': nick, 'avatar': avatar, 'date': date, 'type': type, 'direction': direction }, function(data) { + $.post('./server/generate-chat.php', { + 'content': content, + 'xid': xid, + 'nick': nick, + 'avatar': avatar, + 'date': date, + 'type': type, + 'direction': direction + }, function(data) { // Handled! - $(path + 'tooltip-waitlog').replaceWith('' + Common._e("Download file!") + ''); + $(path + 'tooltip-waitlog').replaceWith( + '' + Common._e("Download file!") + '' + ); }); } catch(e) { Console.error('Interface.generateChatLog', e); @@ -433,6 +451,31 @@ var Interface = (function () { }; + /** + * Returns whether chan has focus or not + * @public + * @param {string} hash + * @return {boolean} + */ + self.hasChanFocus = function(hash) { + + var has_focus = true; + + try { + if(!$('#page-switch .' + hash).hasClass('activechan') || + !Common.isFocused() || + (self.chat_focus_hash != hash)) { + has_focus = false; + } + } catch(e) { + Console.error('Interface.hasChanFocus', e); + } finally { + return has_focus; + } + + }; + + /** * Notifies the user from a new incoming message * @public @@ -449,7 +492,7 @@ var Interface = (function () { var active = $(tested).hasClass('activechan'); // We notify the user if he has not the focus on the chat - if(!active || !Common.isFocused() || (self.chat_focus_hash != hash)) { + if(self.hasChanFocus(hash) === false) { if(!active) { if(type == 'personal') { $(tested + ', ' + chat_switch + 'more-button').addClass('chan-newmessage'); @@ -509,10 +552,11 @@ var Interface = (function () { try { // Any pending events? - if(Common.exists('.one-counter[data-counter]')) + if(Common.exists('.one-counter[data-counter]')) { self.title('new'); - else + } else { self.title('talk'); + } } catch(e) { Console.error('Interface.updateTitle', e); } @@ -534,8 +578,9 @@ var Interface = (function () { $(chat_switch + hash).removeClass('chan-newmessage chan-unread'); // We reset the global notifications if no more unread messages - if(!$(chat_switch + 'chans .chan-newmessage').size()) + if(!$(chat_switch + 'chans .chan-newmessage').size()) { $(chat_switch + 'more-button').removeClass('chan-newmessage'); + } // We reset the chat counter $('#' + hash).removeAttr('data-counter'); @@ -598,8 +643,9 @@ var Interface = (function () { // We show all the groups $('#roster .one-group').show(); - if(Search.search_filtered) + if(Search.search_filtered) { Search.funnelFilterBuddy(); + } // Store this in the options if((from == 'roster') && Options.loaded()) { @@ -687,6 +733,39 @@ var Interface = (function () { $(document).ready(function() { // Focus on the first visible input $(window).focus(self.inputFocus); + + // Re-focus to visible chat/groupchat input if typing when input blurred + $(document).keypress(function(evt) { + try { + // Don't trigger if not connected or popup opened + if(Common.isConnected() && !Common.exists('div.lock')) { + // Cannot work if an input/textarea is already focused or chat is not opened + var target_input_sel = $('.page-engine-chan .message-area:visible'); + + if(!target_input_sel.size() || $('input, textarea').is(':focus')) { + return; + } + + // Get key value + var key_value = $.trim(String.fromCharCode(evt.which)); + + // Re-focus on opened chat? + if(key_value) { + // Get input values + value_input = target_input_sel.val(); + + // Append pressed key value + target_input_sel.val(value_input + key_value); + target_input_sel.focus(); + + // Put cursor at the end of input + target_input_sel[0].selectionStart = target_input_sel[0].selectionEnd = value_input.length + 1; + } + } + } catch(e) { + Console.error('Interface.launch[autofocus]', e); + } + }); }); } catch(e) { Console.error('Interface.launch', e); diff --git a/source/app/javascripts/iq.js b/source/app/javascripts/iq.js index 2a3f35b..a7e5d53 100644 --- a/source/app/javascripts/iq.js +++ b/source/app/javascripts/iq.js @@ -20,6 +20,352 @@ var IQ = (function () { var self = {}; + /** + * Handles OOB request + * @private + * @param {string} iqType + * @param {string} iqID + * @param {object} iqNode + * @return {undefined} + */ + self._handleOOBRequest = function(iqFrom, iqID, iqNode) { + + try { + /* REF: http://xmpp.org/extensions/xep-0066.html */ + + OOB.handle(iqFrom, iqID, 'iq', iqNode); + + Console.log('Received IQ OOB request: ' + iqFrom); + } catch(e) { + Console.error('IQ._handleOOBRequest', e); + } + + }; + + + /** + * Handles OOB reply + * @private + * @param {object} iqResponse + * @param {string} iqFrom + * @param {string} iqType + * @param {string} iqID + * @param {object} iqNode + * @return {undefined} + */ + self._handleOOBReply = function(iqResponse, iqFrom, iqType, iqID, iqNode) { + + try { + // Get the values + var oob_url = DataStore.getDB(Connection.desktop_hash, 'send/url', iqID); + var oob_desc = DataStore.getDB(Connection.desktop_hash, 'send/desc', iqID); + var notif_id = hex_md5(oob_url + oob_desc + iqType + iqFrom + iqID); + + if($(iqNode).find('error').size()) { + // Error? + if($(iqNode).find('error not-acceptable').size()) { + // Rejected? + Notification.create('send_reject', iqFrom, [iqFrom, oob_url, 'iq', iqID, iqNode], oob_desc, notif_id); + } else { + // Failed? + Notification.create('send_fail', iqFrom, [iqFrom, oob_url, 'iq', iqID, iqNode], oob_desc, notif_id); + } + + // Remove the file + $.get(oob_url + '&action=remove'); + } else if(iqType == 'result') { + // Success? + Notification.create('send_accept', iqFrom, [iqFrom, oob_url, 'iq', iqID, iqNode], oob_desc, notif_id); + } + } catch(e) { + Console.error('IQ._handleOOBReply', e); + } + + }; + + + /** + * Handles Software Version + * @private + * @param {object} iqResponse + * @param {string} iqFrom + * @return {undefined} + */ + self._handleSoftwareVersion = function(iqResponse, iqFrom) { + + try { + /* REF: http://xmpp.org/extensions/xep-0092.html */ + + iqQuery = iqResponse.setQuery(NS_VERSION); + + iqQuery.appendChild(iqResponse.buildNode('name', {'xmlns': NS_VERSION}, Caps.disco_infos.identity.name)); + iqQuery.appendChild(iqResponse.buildNode('version', {'xmlns': NS_VERSION}, JAPPIX_VERSION)); + iqQuery.appendChild(iqResponse.buildNode('os', {'xmlns': NS_VERSION}, BrowserDetect.OS)); + + con.send(iqResponse); + + Console.log('Received software version query: ' + iqFrom); + } catch(e) { + Console.error('IQ._handleSoftwareVersion', e); + } + + }; + + + /** + * Handles Last Activity + * @private + * @param {object} iqResponse + * @param {string} iqFrom + * @return {undefined} + */ + self._handleLastActivity = function(iqResponse, iqFrom) { + + try { + /* REF: http://xmpp.org/extensions/xep-0012.html */ + + iqQuery = iqResponse.setQuery(NS_LAST); + iqQuery.setAttribute('seconds', DateUtils.getLastActivity()); + + con.send(iqResponse); + + Console.log('Received last activity query: ' + iqFrom); + } catch(e) { + Console.error('IQ._handleLastActivity', e); + } + + }; + + + /** + * Handles Privacy Lists + * @private + * @param {object} iqResponse + * @param {string} iqFrom + * @param {string} iqQuery + * @return {undefined} + */ + self._handlePrivacyLists = function(iqResponse, iqFrom, iqQuery) { + + try { + // REF : http://xmpp.org/extensions/xep-0016.html + + // Roster push + con.send(iqResponse); + + // Get the lists + $(iqQuery).find('list').each(function() { + Privacy.get($(this).attr('name')); + }); + + Console.log('Received privacy lists push: ' + iqFrom); + } catch(e) { + Console.error('IQ._handlePrivacyLists', e); + } + + }; + + + /** + * Handles Roster Push + * @private + * @param {object} iqResponse + * @param {string} iqFrom + * @param {string} iqQuery + * @return {undefined} + */ + self._handleRosterPush = function(iqResponse, iqFrom, iqQuery) { + + try { + // REF : http://xmpp.org/extensions/xep-0092.html + + // Roster push + con.send(iqResponse); + + // Get the values + $(iqQuery).find('item').each(function() { + Roster.parse($(this), 'presence'); + }); + + Console.log('Received roster push: ' + iqFrom); + } catch(e) { + Console.error('IQ._handleRosterPush', e); + } + + }; + + + /** + * Handles Roster Item Exchange + * @private + * @param {object} iqNode + * @param {string} iqFrom + * @return {undefined} + */ + self._handleRosterItemExchange = function(iqNode, iqFrom) { + + try { + // Open a new notification + Notification.create('rosterx', iqFrom, [iqNode], ''); + + Console.log('Roster Item Exchange from: ' + iqFrom); + } catch(e) { + Console.error('IQ._handleRosterItemExchange', e); + } + + }; + + + /** + * Handles Disco Info + * @private + * @param {object} iqResponse + * @param {string} iqFrom + * @return {undefined} + */ + self._handleDiscoInfo = function(iqResponse, iqFrom) { + + try { + /* REF: http://xmpp.org/extensions/xep-0030.html */ + + iqQuery = iqResponse.setQuery(NS_DISCO_INFO); + + // We set the name of the client + iqQuery.appendChild(iqResponse.buildNode('identity', { + 'category': Caps.disco_infos.identity.category, + 'type': Caps.disco_infos.identity.type, + 'name': Caps.disco_infos.identity.name, + 'xmlns': NS_DISCO_INFO + })); + + // We set all the supported features + var disco_infos = Caps.myDiscoInfos(); + + $.each(disco_infos, function(i, disco_info) { + iqQuery.appendChild(iqResponse.buildNode('feature', {'var': disco_info, 'xmlns': NS_DISCO_INFO})); + }); + + con.send(iqResponse); + + Console.log('Received disco#infos query: ' + iqFrom); + } catch(e) { + Console.error('IQ._handleDiscoInfo', e); + } + + }; + + + /** + * Handles User Time + * @private + * @param {object} iqResponse + * @param {string} iqFrom + * @return {undefined} + */ + self._handleUserTime = function(iqResponse, iqFrom) { + + try { + /* REF: http://xmpp.org/extensions/xep-0202.html */ + + var iqTime = iqResponse.appendNode('time', { + 'xmlns': NS_URN_TIME + }); + + iqTime.appendChild(iqResponse.buildNode('tzo', { + 'xmlns': NS_URN_TIME + }, DateUtils.getTZO())); + + iqTime.appendChild(iqResponse.buildNode('utc', { + 'xmlns': NS_URN_TIME + }, DateUtils.getXMPPTime('utc'))); + + con.send(iqResponse); + + Console.log('Received local time query: ' + iqFrom); + } catch(e) { + Console.error('IQ._handleUserTime', e); + } + + }; + + + /** + * Handles Ping + * @private + * @param {object} iqResponse + * @param {string} iqFrom + * @return {undefined} + */ + self._handlePing = function(iqResponse, iqFrom) { + + try { + /* REF: http://xmpp.org/extensions/xep-0199.html */ + + con.send(iqResponse); + + Console.log('Received a ping: ' + iqFrom); + } catch(e) { + Console.error('IQ._handlePing', e); + } + + }; + + + /** + * Handles Jingle + * @private + * @param {string} iqFrom + * @return {undefined} + */ + self._handleJingle = function(iqFrom) { + + try { + /* REF: http://xmpp.org/extensions/xep-0166.html */ + + // Handled via JSJaCJingle.route() (see above) + + Console.log('Received a Jingle packet: ' + iqFrom); + } catch(e) { + Console.error('IQ._handleJingle', e); + } + + }; + + + /** + * Raises a not implemented error + * @private + * @param {object} iqResponse + * @param {object} iqNode + * @param {string} iqFrom + * @return {undefined} + */ + self._raiseNotImplemented = function(iqResponse, iqNode, iqFrom) { + + try { + // Change IQ type + iqResponse.setType('error'); + + // Append stanza content + for(var c = 0; c < iqNode.childNodes.length; c++) { + iqResponse.getNode().appendChild(iqNode.childNodes.item(c).cloneNode(true)); + } + + // Append error content + var iqError = iqResponse.appendNode('error', {'xmlns': NS_CLIENT, 'code': '501', 'type': 'cancel'}); + iqError.appendChild(iqResponse.buildNode('feature-not-implemented', {'xmlns': NS_STANZAS})); + iqError.appendChild(iqResponse.buildNode('text', {'xmlns': NS_STANZAS}, Common._e("The feature requested is not implemented by the recipient or server and therefore cannot be processed."))); + + con.send(iqResponse); + + Console.log('Received an unsupported IQ query from: ' + iqFrom); + } catch(e) { + Console.error('IQ._raiseNotImplemented', e); + } + + }; + + /** * Handles an incoming IQ packet * @public @@ -37,9 +383,6 @@ var IQ = (function () { var iqQuery = iq.getQuery(); var iqType = iq.getType(); - // Handle Jingle packet? - JSJaCJingle_route(iq); - // Build the response var iqResponse = new JSJaCIQ(); @@ -49,178 +392,62 @@ var IQ = (function () { // OOB request if((iqQueryXMLNS == NS_IQOOB) && (iqType == 'set')) { - /* REF: http://xmpp.org/extensions/xep-0066.html */ - - OOB.handle(iqFrom, iqID, 'iq', iqNode); - - Console.log('Received IQ OOB request: ' + iqFrom); + self._handleOOBRequest(iqFrom, iqID, iqNode); } // OOB reply else if(DataStore.getDB(Connection.desktop_hash, 'send/url', iqID)) { - // Get the values - var oob_url = DataStore.getDB(Connection.desktop_hash, 'send/url', iqID); - var oob_desc = DataStore.getDB(Connection.desktop_hash, 'send/desc', iqID); - var notif_id = hex_md5(oob_url + oob_desc + iqType + iqFrom + iqID); - - if($(iqNode).find('error').size()) { - // Error? - if($(iqNode).find('error not-acceptable').size()) { - // Rejected? - Notification.create('send_reject', iqFrom, [iqFrom, oob_url, 'iq', iqID, iqNode], oob_desc, notif_id); - } else { - // Failed? - Notification.create('send_fail', iqFrom, [iqFrom, oob_url, 'iq', iqID, iqNode], oob_desc, notif_id); - } - - // Remove the file - $.get(oob_url + '&action=remove'); - } else if(iqType == 'result') { - // Success? - Notification.create('send_accept', iqFrom, [iqFrom, oob_url, 'iq', iqID, iqNode], oob_desc, notif_id); - } + self._handleOOBReply(iqResponse, iqFrom, iqType, iqID, iqNode); } // Software version query else if((iqQueryXMLNS == NS_VERSION) && (iqType == 'get')) { - /* REF: http://xmpp.org/extensions/xep-0092.html */ - - iqQuery = iqResponse.setQuery(NS_VERSION); - - iqQuery.appendChild(iqResponse.buildNode('name', {'xmlns': NS_VERSION}, 'Jappix')); - iqQuery.appendChild(iqResponse.buildNode('version', {'xmlns': NS_VERSION}, JAPPIX_VERSION)); - iqQuery.appendChild(iqResponse.buildNode('os', {'xmlns': NS_VERSION}, BrowserDetect.OS)); - - con.send(iqResponse); - - Console.log('Received software version query: ' + iqFrom); + self._handleSoftwareVersion(iqResponse, iqFrom); } // Last activity query else if((iqQueryXMLNS == NS_LAST) && (iqType == 'get')) { - /* REF: http://xmpp.org/extensions/xep-0012.html */ - - iqQuery = iqResponse.setQuery(NS_LAST); - iqQuery.setAttribute('seconds', DateUtils.getLastActivity()); - - con.send(iqResponse); - - Console.log('Received last activity query: ' + iqFrom); + self._handleLastActivity(iqResponse, iqFrom); } // Privacy lists push else if((iqQueryXMLNS == NS_PRIVACY) && (iqType == 'set') && Common.isSafeStanza(iq)) { - // REF : http://xmpp.org/extensions/xep-0016.html - - // Roster push - con.send(iqResponse); - - // Get the lists - $(iqQuery).find('list').each(function() { - Privacy.get($(this).attr('name')); - }); - - Console.log('Received privacy lists push: ' + iqFrom); + self._handlePrivacyLists(iqResponse, iqFrom, iqQuery); } // Roster push else if((iqQueryXMLNS == NS_ROSTER) && (iqType == 'set') && Common.isSafeStanza(iq)) { - // REF : http://xmpp.org/extensions/xep-0092.html - - // Roster push - con.send(iqResponse); - - // Get the values - $(iqQuery).find('item').each(function() { - Roster.parse($(this), 'presence'); - }); - - Console.log('Received roster push: ' + iqFrom); + self._handleRosterPush(iqResponse, iqFrom, iqQuery); } // Roster Item Exchange query else if($(iqNode).find('x[xmlns="' + NS_ROSTERX + '"]').size()) { - // Open a new notification - Notification.create('rosterx', iqFrom, [iqNode], ''); - - Console.log('Roster Item Exchange from: ' + iqFrom); + self._handleRosterItemExchange(iqNode, iqFrom); } // Disco info query else if((iqQueryXMLNS == NS_DISCO_INFO) && (iqType == 'get')) { - /* REF: http://xmpp.org/extensions/xep-0030.html */ - - iqQuery = iqResponse.setQuery(NS_DISCO_INFO); - - // We set the name of the client - iqQuery.appendChild(iqResponse.buildNode('identity', { - 'category': 'client', - 'type': 'web', - 'name': 'Jappix', - 'xmlns': NS_DISCO_INFO - })); - - // We set all the supported features - var disco_infos = Caps.myDiscoInfos(); - - $.each(disco_infos, function(i, disco_info) { - iqQuery.appendChild(iqResponse.buildNode('feature', {'var': disco_info, 'xmlns': NS_DISCO_INFO})); - }); - - con.send(iqResponse); - - Console.log('Received disco#infos query: ' + iqFrom); + self._handleDiscoInfo(iqResponse, iqFrom); } // User time query else if($(iqNode).find('time').size() && (iqType == 'get')) { - /* REF: http://xmpp.org/extensions/xep-0202.html */ - - var iqTime = iqResponse.appendNode('time', {'xmlns': NS_URN_TIME}); - iqTime.appendChild(iqResponse.buildNode('tzo', {'xmlns': NS_URN_TIME}, DateUtils.getTZO())); - iqTime.appendChild(iqResponse.buildNode('utc', {'xmlns': NS_URN_TIME}, DateUtils.getXMPPTime('utc'))); - - con.send(iqResponse); - - Console.log('Received local time query: ' + iqFrom); + self._handleUserTime(iqResponse, iqFrom); } // Ping else if($(iqNode).find('ping').size() && (iqType == 'get')) { - /* REF: http://xmpp.org/extensions/xep-0199.html */ - - con.send(iqResponse); - - Console.log('Received a ping: ' + iqFrom); + self._handlePing(iqResponse, iqFrom); } // Jingle else if($(iqNode).find('jingle').size()) { - /* REF: http://xmpp.org/extensions/xep-0166.html */ - - // Handled via JSJaCJingle_route() (see above) - - Console.log('Received a Jingle packet: ' + iqFrom); + self._handleJingle(iqFrom); } // Not implemented else if(!$(iqNode).find('error').size() && ((iqType == 'get') || (iqType == 'set'))) { - // Change IQ type - iqResponse.setType('error'); - - // Append stanza content - for(var c = 0; c < iqNode.childNodes.length; c++) { - iqResponse.getNode().appendChild(iqNode.childNodes.item(c).cloneNode(true)); - } - - // Append error content - var iqError = iqResponse.appendNode('error', {'xmlns': NS_CLIENT, 'code': '501', 'type': 'cancel'}); - iqError.appendChild(iqResponse.buildNode('feature-not-implemented', {'xmlns': NS_STANZAS})); - iqError.appendChild(iqResponse.buildNode('text', {'xmlns': NS_STANZAS}, Common._e("The feature requested is not implemented by the recipient or server and therefore cannot be processed."))); - - con.send(iqResponse); - - Console.log('Received an unsupported IQ query from: ' + iqFrom); + self._raiseNotImplemented(iqResponse, iqNode, iqFrom); } } catch(e) { Console.error('IQ.handle', e); diff --git a/source/app/javascripts/jingle.js b/source/app/javascripts/jingle.js index 32f122a..653bd54 100644 --- a/source/app/javascripts/jingle.js +++ b/source/app/javascripts/jingle.js @@ -21,67 +21,11 @@ var Jingle = (function() { /* Variables */ - self._jingle_current = null; - self._start_stamp = 0; + self._session = null; self._call_ender = null; self._bypass_termination_notify = false; - /** - * Provides an adapter to the JSJaCJingle console implementation which is different - * @private - * @return {object} - */ - self._consoleAdapter = (function() { - - /** - * Alias of this - * @private - */ - var _console = {}; - - - /** - * Console logging interface (adapted) - * @public - * @param {string} message - * @param {number} loglevel - * @return {undefined} - */ - _console.log = function(message, loglevel) { - - try { - if(!message) { - throw 'No message passed to console adapter!'; - } - - switch(loglevel) { - case 0: - Console.warn(message); break; - case 1: - Console.error(message); break; - case 2: - Console.info(message); break; - case 4: - Console.debug(message); break; - default: - Console.log(message); - } - } catch(e) { - Console.error('Jingle._consoleAdapter.log', e); - } - - }; - - - /** - * Return sub-class scope - */ - return _console; - - })(); - - /** * Opens the Jingle interface (depending on the state) * @public @@ -90,13 +34,13 @@ var Jingle = (function() { self.open = function() { try { - var jingle_tool_sel = $('#top-content .tools.jingle'); + var call_tool_sel = $('#top-content .tools.call'); - if(jingle_tool_sel.is('.active')) { - Console.info('Opened Jingle notification drawer'); - } else if(jingle_tool_sel.is('.streaming.video')) { + if(call_tool_sel.is('.active')) { + Console.info('Opened call notification drawer'); + } else if(call_tool_sel.is('.streaming.video')) { // Videobox? - self.showInterface(); + self._show_interface(); Console.info('Opened Jingle videobox'); } else { @@ -127,19 +71,7 @@ var Jingle = (function() { try { // Network configuration - var stun = {}; - var turn = {}; - - if(HOST_STUN) { - stun[HOST_STUN] = {}; - } - - if(HOST_TURN) { - turn[HOST_TURN] = { - 'username': HOST_TURN_USERNAME, - 'credential': HOST_TURN_PASSWORD - }; - } + var ice_servers = Call.generate_ice_servers(); // Jingle arguments args = { @@ -149,14 +81,15 @@ var Jingle = (function() { media: media, local_view: local_view, remote_view: remote_view, - stun: stun, - turn: turn, - //resolution: 'hd', -> this can cause some lags - debug: self._consoleAdapter, + stun: ice_servers.stun, + turn: ice_servers.turn, + resolution: 'md', + debug: Call._consoleAdapter, // Custom handlers (optional) session_initiate_pending: function(jingle) { - self.notify( + Call.notify( + JSJAC_JINGLE_SESSION_SINGLE, Common.bareXID(jingle.get_to()), 'initiating', jingle.get_media() @@ -167,14 +100,15 @@ var Jingle = (function() { session_initiate_success: function(jingle, stanza) { // Already in a call? - if(self.in_call() && !self.is_same_sid(jingle)) { + if(Call.is_ongoing() && !self.is_same_sid(jingle)) { jingle.terminate(JSJAC_JINGLE_REASON_BUSY); - Console.warn('session_initiate_success', 'Dropped incoming call (already in a call)'); + Console.warn('Jingle._args > session_initiate_success', 'Dropped incoming call (already in a call)'); } else { // Incoming call? if(jingle.is_responder()) { - self.notify( + Call.notify( + JSJAC_JINGLE_SESSION_SINGLE, Common.bareXID(jingle.get_to()), ('call_' + jingle.get_media()), jingle.get_media() @@ -187,7 +121,8 @@ var Jingle = (function() { jingle.info(JSJAC_JINGLE_SESSION_INFO_RINGING); }, 250); } else { - self.notify( + Call.notify( + JSJAC_JINGLE_SESSION_SINGLE, Common.bareXID(jingle.get_to()), 'waiting', jingle.get_media() @@ -204,7 +139,8 @@ var Jingle = (function() { session_initiate_error: function(jingle, stanza) { self._reset(); - self.notify( + Call.notify( + JSJAC_JINGLE_SESSION_SINGLE, Common.bareXID(jingle.get_to()), 'error', jingle.get_media() @@ -218,7 +154,8 @@ var Jingle = (function() { }, session_accept_pending: function(jingle) { - self.notify( + Call.notify( + JSJAC_JINGLE_SESSION_SINGLE, Common.bareXID(jingle.get_to()), 'waiting', jingle.get_media() @@ -228,12 +165,12 @@ var Jingle = (function() { }, session_accept_success: function(jingle, stanza) { - self.unnotify(); + Call._unnotify(); // Start call! Go Go Go! - self._startSession(jingle.get_media()); - self.showInterface(); - self._startCounter(); + Call.start_session(jingle.get_media()); + self._show_interface(); + Call.start_counter(); Console.log('Jingle._args', 'session_accept_success'); }, @@ -241,7 +178,8 @@ var Jingle = (function() { session_accept_error: function(jingle, stanza) { self._reset(); - self.notify( + Call.notify( + JSJAC_JINGLE_SESSION_SINGLE, Common.bareXID(jingle.get_to()), 'declined', jingle.get_media() @@ -254,6 +192,10 @@ var Jingle = (function() { Console.log('Jingle._args', 'session_accept_request'); }, + session_info_pending: function(jingle) { + Console.log('Jingle._args', 'session_info_pending'); + }, + session_info_success: function(jingle, stanza) { Console.log('Jingle._args', 'session_info_success'); }, @@ -263,12 +205,13 @@ var Jingle = (function() { }, session_info_request: function(jingle, stanza) { - var info_name = jingle.util_stanza_session_info(stanza); + var info_name = jingle.utils.stanza_session_info(stanza); switch(info_name) { // Ringing? case JSJAC_JINGLE_SESSION_INFO_RINGING: - self.notify( + Call.notify( + JSJAC_JINGLE_SESSION_SINGLE, Common.bareXID(jingle.get_to()), 'ringing', jingle.get_media() @@ -282,7 +225,8 @@ var Jingle = (function() { session_terminate_pending: function(jingle) { self._reset(); - self.notify( + Call.notify( + JSJAC_JINGLE_SESSION_SINGLE, Common.bareXID(jingle.get_to()), 'ending', jingle.get_media() @@ -293,20 +237,21 @@ var Jingle = (function() { session_terminate_success: function(jingle, stanza) { // Ensure we this is the same call session ID (SID) - if(self._jingle_current.get_sid() == jingle.get_sid()) { + if(self._session.get_sid() == jingle.get_sid()) { if(self._bypass_termination_notify !== true) { self._reset(); - self.notify( + Call.notify( + JSJAC_JINGLE_SESSION_SINGLE, Common.bareXID(jingle.get_to()), (self._call_ender === 'remote' ? 'remote_ended' : 'local_ended'), jingle.get_media() ); } - Console.debug('Stopped current Jingle call'); + Console.debug('Jingle._args > session_terminate_success', 'Stopped current Jingle call'); } else { - Console.warn('session_terminate_success', 'Dropped stanza with unmatching SID'); + Console.warn('Jingle._args > session_terminate_success', 'Dropped stanza with unmatching SID'); } Console.log('Jingle._args', 'session_terminate_success'); @@ -314,20 +259,21 @@ var Jingle = (function() { session_terminate_error: function(jingle, stanza) { // Ensure we this is the same call session ID (SID) - if(self._jingle_current.get_sid() == jingle.get_sid()) { + if(self._session.get_sid() == jingle.get_sid()) { if(self._bypass_termination_notify !== true) { self._reset(); - self.notify( + Call.notify( + JSJAC_JINGLE_SESSION_SINGLE, Common.bareXID(jingle.get_to()), 'error', jingle.get_media() ); } - Console.warn('Stopped current Jingle call, but with brute force!'); + Console.warn('Jingle._args > session_terminate_error', 'Stopped current Jingle call, but with brute force!'); } else { - Console.warn('session_terminate_error', 'Dropped stanza with unmatching SID'); + Console.warn('Jingle._args > session_terminate_error', 'Dropped stanza with unmatching SID'); } Console.log('Jingle._args', 'session_terminate_error'); @@ -335,7 +281,7 @@ var Jingle = (function() { session_terminate_request: function(jingle, stanza) { var notify_type; - var reason = jingle.util_stanza_terminate_reason(stanza); + var reason = jingle.utils.stanza_terminate_reason(stanza); // The remote wants to end call self._call_ender = 'remote'; @@ -360,7 +306,8 @@ var Jingle = (function() { self._bypass_termination_notify = true; } - self.notify( + Call.notify( + JSJAC_JINGLE_SESSION_SINGLE, Common.bareXID(jingle.get_to()), notify_type, jingle.get_media() @@ -413,7 +360,7 @@ var Jingle = (function() { // Create interface for video containers $('body').addClass('in_jingle_call'); - var jingle_sel = self.createInterface(bare_xid, mode); + var jingle_sel = self._create_interface(bare_xid, mode); // Filter media var media = null; @@ -427,24 +374,24 @@ var Jingle = (function() { // Start the Jingle negotiation var args = self._args( - con, - full_xid, - bare_hash, - media, - jingle_sel.find('.local_video video')[0], - jingle_sel.find('.remote_video video')[0] - ); + con, + full_xid, + bare_hash, + media, + jingle_sel.find('.local_video video')[0], + jingle_sel.find('.remote_video video')[0] + ); - self._jingle_current = new JSJaCJingle(args); + self._session = new JSJaCJingle.session(JSJAC_JINGLE_SESSION_SINGLE, args); self._call_ender = null; self._bypass_termination_notify = false; if(is_callee) { - self._jingle_current.handle(stanza); + self._session.handle(stanza); Console.debug('Receive call form: ' + full_xid); } else { - self._jingle_current.initiate(); + self._session.initiate(); Console.debug('Emit call to: ' + full_xid); } @@ -459,145 +406,6 @@ var Jingle = (function() { }; - /** - * Processes the Jingle elements size - * @private - * @param {object} screen - * @param {object} video - * @return {object} - */ - self._processSize = function(screen, video) { - - try { - if(!(typeof screen === 'object' && typeof video === 'object')) { - throw 'Invalid object passed, aborting!'; - } - - // Get the intrinsic size of the video - var video_w = video.videoWidth; - var video_h = video.videoHeight; - - // Get the screen size of the video - var screen_w = screen.width(); - var screen_h = screen.height(); - - // Process resize ratios (2 cases) - var r_1 = screen_h / video_h; - var r_2 = screen_w / video_w; - - // Process resized video sizes - var video_w_1 = video_w * r_1; - var video_h_1 = video_h * r_1; - - var video_w_2 = video_w * r_2; - var video_h_2 = video_h * r_2; - - // DOM view modifiers - var dom_width = 'auto'; - var dom_height = 'auto'; - var dom_left = 0; - var dom_top = 0; - - // Landscape/Portrait/Equal container? - if(video_w > video_h || (video_h == video_w && screen_w < screen_h)) { - // Not sufficient? - if(video_w_1 < screen_w) { - dom_width = screen_w + 'px'; - dom_top = -1 * (video_h_2 - screen_h) / 2; - } else { - dom_height = screen_h + 'px'; - dom_left = -1 * (video_w_1 - screen_w) / 2; - } - } else if(video_h > video_w || (video_h == video_w && screen_w > screen_h)) { - // Not sufficient? - if(video_h_1 < screen_h) { - dom_height = screen_h + 'px'; - dom_left = -1 * (video_w_1 - screen_w) / 2; - } else { - dom_width = screen_w + 'px'; - dom_top = -1 * (video_h_2 - screen_h) / 2; - } - } else if(screen_w == screen_h) { - dom_width = screen_w + 'px'; - dom_height = screen_h + 'px'; - } - - return { - width : dom_width, - height : dom_height, - left : dom_left, - top : dom_top - }; - } catch(e) { - Console.error('Jingle._processSize', e); - } - - }; - - - /** - * Adapts the local Jingle view - * @private - * @return {undefined} - */ - self._adaptLocal = function() { - - try { - var local_sel = $('#jingle .local_video'); - var local_video_sel = local_sel.find('video'); - - // Process new sizes - var sizes = self._processSize( - local_sel, - local_video_sel[0] - ); - - // Apply new sizes - local_video_sel.css({ - 'height': sizes.height, - 'width': sizes.width, - 'margin-top': sizes.top, - 'margin-left': sizes.left - }); - } catch(e) { - Console.error('Jingle._adaptLocal', e); - } - - }; - - - /** - * Adapts the remote Jingle view - * @private - * @return {undefined} - */ - self._adaptRemote = function() { - - try { - var videobox_sel = $('#jingle .videobox'); - var remote_sel = videobox_sel.find('.remote_video'); - var remote_video_sel = remote_sel.find('video'); - - // Process new sizes - var sizes = self._processSize( - remote_sel, - remote_video_sel[0] - ); - - // Apply new sizes - remote_video_sel.css({ - 'height': sizes.height, - 'width': sizes.width, - 'margin-top': sizes.top, - 'margin-left': sizes.left - }); - } catch(e) { - Console.error('Jingle._adaptRemote', e); - } - - }; - - /** * Adapts the Jingle view to the window size * @private @@ -607,8 +415,13 @@ var Jingle = (function() { try { if(self.in_call() && Common.exists('#jingle')) { - self._adaptLocal(); - self._adaptRemote(); + Call.adapt_local( + $('#jingle .local_video') + ); + + Call.adapt_remote( + $('#jingle .videobox') + ); } } catch(e) { Console.error('Jingle._adapt', e); @@ -617,68 +430,6 @@ var Jingle = (function() { }; - /** - * Initializes Jingle router - * @public - * @return {undefined} - */ - self.init = function() { - - try { - JSJaCJingle_listen({ - connection: con, - debug: self._consoleAdapter, - // TODO: seems like it fucks up the calls! - //fallback: './server/jingle.php', - - initiate: function(stanza) { - try { - // Already in a call? - if(self.in_call()) { - // Try to restore SID there - var stanza_id = stanza.getID(); - var sid = null; - - if(stanza_id) { - var stanza_id_split = stanza_id.split('_'); - sid = stanza_id_split[1]; - } - - // Build a temporary Jingle session - var jingle_close = new JSJaCJingle({ - to: stanza.getFrom(), - debug: JSJAC_JINGLE_STORE_DEBUG - }); - - if(sid) { - jingle_close._set_sid(sid); - } - - jingle_close.terminate(JSJAC_JINGLE_REASON_BUSY); - - Console.warn('session_initiate_success', 'Dropped incoming call because already in a call.'); - - return; - } - - var xid = Common.fullXID(Common.getStanzaFrom(stanza)); - - Console.info('Incoming call from: ' + xid); - - // Session values - self.receive(xid, stanza); - } catch(e) { - Console.error('Jingle.init[initiate]', e); - } - } - }); - } catch(e) { - Console.error('Jingle.init', e); - } - - }; - - /** * Receive a Jingle call * @public @@ -689,7 +440,7 @@ var Jingle = (function() { self.receive = function(xid, stanza) { try { - if(!self.in_call()) { + if(!Call.is_ongoing()) { self._new(xid, null, true, stanza); } } catch(e) { @@ -711,7 +462,7 @@ var Jingle = (function() { self.start = function(xid, mode) { try { - if(!self.in_call()) { + if(!Call.is_ongoing()) { self._new(xid, mode); } } catch(e) { @@ -732,9 +483,9 @@ var Jingle = (function() { try { // Trash interface - self._stopCounter(); - self._stopSession(); - self.destroyInterface(); + Call.stop_counter(); + Call.stop_session(); + self._destroy_interface(); $('body').removeClass('in_jingle_call'); // Hack: stop audio in case it is still ringing @@ -761,9 +512,9 @@ var Jingle = (function() { self._reset(); // Stop Jingle session - if(self._jingle_current !== null) { + if(self._session !== null) { self._call_ender = 'local'; - self._jingle_current.terminate(); + self._session.terminate(); Console.debug('Stopping current Jingle call...'); } else { @@ -786,18 +537,10 @@ var Jingle = (function() { self.mute = function() { try { - if(self._jingle_current) { - var jingle_controls = $('#jingle .videobox .topbar .controls a'); - - // Toggle interface buttons - jingle_controls.filter('.mute').hide(); - jingle_controls.filter('.unmute').show(); - - // Actually mute audio stream - if(self._jingle_current.get_mute(JSJAC_JINGLE_MEDIA_AUDIO) === false) { - self._jingle_current.mute(JSJAC_JINGLE_MEDIA_AUDIO); - } - } + Call.mute( + self._session, + $('#jingle .videobox .topbar .controls a') + ); } catch(e) { Console.error('Jingle.mute', e); } @@ -813,16 +556,10 @@ var Jingle = (function() { self.unmute = function() { try { - if(self._jingle_current) { - var jingle_controls = $('#jingle .videobox .topbar .controls a'); - - jingle_controls.filter('.unmute').hide(); - jingle_controls.filter('.mute').show(); - - if(self._jingle_current.get_mute(JSJAC_JINGLE_MEDIA_AUDIO) === true) { - self._jingle_current.unmute(JSJAC_JINGLE_MEDIA_AUDIO); - } - } + Call.unmute( + self._session, + $('#jingle .videobox .topbar .controls a') + ); } catch(e) { Console.error('Jingle.mute', e); } @@ -840,12 +577,12 @@ var Jingle = (function() { in_call = false; try { - if(self._jingle_current && - (self._jingle_current.get_status() === JSJAC_JINGLE_STATUS_INITIATING || - self._jingle_current.get_status() === JSJAC_JINGLE_STATUS_INITIATED || - self._jingle_current.get_status() === JSJAC_JINGLE_STATUS_ACCEPTING || - self._jingle_current.get_status() === JSJAC_JINGLE_STATUS_ACCEPTED || - self._jingle_current.get_status() === JSJAC_JINGLE_STATUS_TERMINATING)) { + if(self._session && + (self._session.get_status() === JSJAC_JINGLE_STATUS_INITIATING || + self._session.get_status() === JSJAC_JINGLE_STATUS_INITIATED || + self._session.get_status() === JSJAC_JINGLE_STATUS_ACCEPTING || + self._session.get_status() === JSJAC_JINGLE_STATUS_ACCEPTED || + self._session.get_status() === JSJAC_JINGLE_STATUS_TERMINATING)) { in_call = true; } } catch(e) { @@ -865,17 +602,10 @@ var Jingle = (function() { */ self.is_same_sid = function(jingle) { - is_same = false; - try { - if(jingle && self._jingle_current && - jingle.get_sid() === self._jingle_current.get_sid()) { - is_same = true; - } + return Call.is_same_sid(self._session, jingle); } catch(e) { Console.error('Jingle.is_same_sid', e); - } finally { - return is_same; } }; @@ -888,16 +618,10 @@ var Jingle = (function() { */ self.is_audio = function() { - audio = false; - try { - if(self._jingle_current && self._jingle_current.get_media() === JSJAC_JINGLE_MEDIA_AUDIO) { - audio = true; - } + return Call.is_audio(self._session); } catch(e) { Console.error('Jingle.is_audio', e); - } finally { - return audio; } }; @@ -910,16 +634,10 @@ var Jingle = (function() { */ self.is_video = function() { - video = false; - try { - if(self._jingle_current && self._jingle_current.get_media() === JSJAC_JINGLE_MEDIA_VIDEO) { - video = true; - } + return Call.is_video(self._session); } catch(e) { Console.error('Jingle.is_video', e); - } finally { - return video; } }; @@ -942,7 +660,7 @@ var Jingle = (function() { 'text': Common._e("Accept"), 'color': 'green', 'cb': function(xid, mode) { - self._jingle_current.accept(); + self._session.accept(); Audio.stop('incoming-call'); } }, @@ -951,7 +669,7 @@ var Jingle = (function() { 'text': Common._e("Decline"), 'color': 'red', 'cb': function(xid, mode) { - self._jingle_current.terminate(JSJAC_JINGLE_REASON_DECLINE); + self._session.terminate(JSJAC_JINGLE_REASON_DECLINE); Audio.stop('incoming-call'); } } @@ -966,7 +684,7 @@ var Jingle = (function() { 'text': Common._e("Accept"), 'color': 'green', 'cb': function(xid, mode) { - self._jingle_current.accept(); + self._session.accept(); Audio.stop('incoming-call'); } }, @@ -975,7 +693,7 @@ var Jingle = (function() { 'text': Common._e("Decline"), 'color': 'red', 'cb': function(xid, mode) { - self._jingle_current.terminate(JSJAC_JINGLE_REASON_DECLINE); + self._session.terminate(JSJAC_JINGLE_REASON_DECLINE); Audio.stop('incoming-call'); } } @@ -990,7 +708,7 @@ var Jingle = (function() { 'text': Common._e("Cancel"), 'color': 'red', 'cb': function(xid, mode) { - self._jingle_current.terminate(JSJAC_JINGLE_REASON_CANCEL); + self._session.terminate(JSJAC_JINGLE_REASON_CANCEL); } } } @@ -1004,7 +722,7 @@ var Jingle = (function() { 'text': Common._e("Cancel"), 'color': 'red', 'cb': function(xid, mode) { - self._jingle_current.terminate(JSJAC_JINGLE_REASON_CANCEL); + self._session.terminate(JSJAC_JINGLE_REASON_CANCEL); } } } @@ -1018,7 +736,7 @@ var Jingle = (function() { 'text': Common._e("Cancel"), 'color': 'red', 'cb': function(xid, mode) { - self._jingle_current.terminate(JSJAC_JINGLE_REASON_CANCEL); + self._session.terminate(JSJAC_JINGLE_REASON_CANCEL); } } } @@ -1060,7 +778,7 @@ var Jingle = (function() { 'text': Common._e("Cancel"), 'color': 'red', 'cb': function(xid, mode) { - self._jingle_current.terminate(JSJAC_JINGLE_REASON_CANCEL); + self._session.terminate(JSJAC_JINGLE_REASON_CANCEL); } } } @@ -1157,263 +875,14 @@ var Jingle = (function() { }; - /** - * Notify for something related to Jingle - * @public - * @param {string} xid - * @param {string} type - * @param {string} mode - * @return {boolean} - */ - self.notify = function(xid, type, mode) { - - try { - var map = self._notify_map(); - - if(!(type in map)) { - throw 'Notification type not recognized!'; - } - - var jingle_tools_all_sel = $('#top-content .tools-all:has(.tools.jingle)'); - var jingle_tool_sel = jingle_tools_all_sel.find('.tools.jingle'); - var jingle_content_sel = jingle_tools_all_sel.find('.jingle-content'); - var jingle_subitem_sel = jingle_content_sel.find('.tools-content-subitem'); - - var buttons_html = ''; - var i = 0; - - if(typeof map[type].buttons === 'object') { - $.each(map[type].buttons, function(button, attrs) { - buttons_html += '' + attrs.text + ''; - }); - } - - // Append notification to DOM - jingle_subitem_sel.html( - '
' + - '
' + - '
' + - '' + - '
' + - - '' + - '
' + - - '
' + - '' + Name.getBuddy(xid).htmlEnc() + '' + - '' + map[type].text + '' + - - '
' + buttons_html + '
' + - '
' + - '
' - ); - - // Apply user avatar - Avatar.get(xid, 'cache', 'true', 'forget'); - - // Apply button events - if(typeof map[type].buttons === 'object') { - $.each(map[type].buttons, function(button, attrs) { - jingle_tools_all_sel.find('a.reply-button[data-action="' + button + '"]').click(function() { - try { - // Remove notification - self.unnotify(xid); - - // Execute callback, if any - if(typeof attrs.cb === 'function') { - attrs.cb(xid, mode); - } - - Console.info('Closed Jingle notification drawer'); - } catch(e) { - Console.error('Jingle.notify[async]', e); - } finally { - return false; - } - }); - }); - } - - // Enable notification box! - jingle_tool_sel.addClass('active'); - - // Open notification box! - jingle_content_sel.show(); - } catch(e) { - Console.error('Jingle.notify', e); - } finally { - return false; - } - - }; - - - /** - * Remove notification - * @public - * @return {boolean} - */ - self.unnotify = function() { - - try { - // Selectors - var jingle_tools_all_sel = $('#top-content .tools-all:has(.tools.jingle)'); - var jingle_tool_sel = jingle_tools_all_sel.find('.tools.jingle'); - var jingle_content_sel = jingle_tools_all_sel.find('.jingle-content'); - var jingle_subitem_sel = jingle_content_sel.find('.tools-content-subitem'); - - // Close & disable notification box - jingle_content_sel.hide(); - jingle_subitem_sel.empty(); - jingle_tool_sel.removeClass('active'); - - // Stop all sounds - Audio.stop('incoming-call'); - Audio.stop('outgoing-call'); - } catch(e) { - Console.error('Jingle.unnotify', e); - } finally { - return false; - } - - }; - - - /** - * Set the Jingle session as started - * @private - * @param {string} mode - * @return {boolean} - */ - self._startSession = function(mode) { - - try { - if(!(mode in JSJAC_JINGLE_MEDIAS)) { - throw 'Unknown mode: ' + (mode || 'none'); - } - - var jingle_tool_sel = $('#top-content .tools.jingle'); - - jingle_tool_sel.removeClass('audio video active'); - jingle_tool_sel.addClass('streaming').addClass(mode); - - Console.info('Jingle session successfully started, mode: ' + (mode || 'none')); - } catch(e) { - Console.error('Jingle._startSession', e); - } finally { - return false; - } - - }; - - - /** - * Set the Jingle session as stopped - * @private - * @param {string} mode - * @return {boolean} - */ - self._stopSession = function() { - - try { - $('#top-content .tools.jingle').removeClass('audio video active streaming'); - - Console.info('Jingle session successfully stopped'); - } catch(e) { - Console.error('Jingle._stopSession', e); - } finally { - return false; - } - - }; - - - /** - * Start call elpsed time counter - * @private - * @return {boolean} - */ - self._startCounter = function() { - - try { - // Initialize counter - self._stopCounter(); - self._start_stamp = DateUtils.getTimeStamp(); - self._fireClock(); - - // Fire it every second - $('#top-content .tools.jingle .counter').everyTime('1s', self._fireClock); - - Console.info('Jingle counter started'); - } catch(e) { - Console.error('Jingle._startCounter', e); - } finally { - return false; - } - - }; - - - /** - * Stop call elpsed time counter - * @private - * @return {boolean} - */ - self._stopCounter = function() { - - try { - // Reset stamp storage - self._start_stamp = 0; - - // Reset counter - var counter_sel = $('#top-content .tools.jingle .counter'); - var default_count = counter_sel.attr('data-default'); - - counter_sel.stopTime(); - $('#top-content .tools.jingle .counter, #jingle .videobox .topbar .elapsed').text(default_count); - - Console.info('Jingle counter stopped'); - } catch(e) { - Console.error('Jingle._stopCounter', e); - } finally { - return false; - } - - }; - - - /** - * Fires the counter clock (once more) - * @private - * @return {undefined} - */ - self._fireClock = function() { - - try { - // Process updated time - var count = DateUtils.difference(DateUtils.getTimeStamp(), self._start_stamp); - - if(count.getHours()) { - count = count.toString('H:mm:ss'); - } else { - count = count.toString('mm:ss'); - } - - // Display updated counter - $('#top-content .tools.jingle .counter, #jingle .videobox .topbar .elapsed').text(count); - } catch(e) { - Console.error('Jingle._fireClock', e); - } - - }; - - /** * Create the Jingle interface - * @public + * @private + * @param {string} room + * @param {string} mode * @return {object} */ - self.createInterface = function(xid, mode) { + self._create_interface = function(xid, mode) { try { // Jingle interface already exists? @@ -1423,8 +892,8 @@ var Jingle = (function() { // Create DOM $('body').append( - '
' + - '
' + + '
' + + '
' + '
' + '
' + '
' + @@ -1438,15 +907,15 @@ var Jingle = (function() { '
' + '' + '
00:00:00
' + '
' + - '' + + '' + '
' + '
' + @@ -1455,21 +924,21 @@ var Jingle = (function() { '
' + '
' + - '' + + '' + '
' + - '
' + + '
' + '
' + '
' ); // Apply events - self._eventsInterface(); + self._events_interface(); // Apply user avatar Avatar.get(xid, 'cache', 'true', 'forget'); } catch(e) { - Console.error('Jingle.createInterface', e); + Console.error('Jingle._create_interface', e); } finally { return $('#jingle'); } @@ -1479,20 +948,17 @@ var Jingle = (function() { /** * Destroy the Jingle interface - * @public + * @private * @return {undefined} */ - self.destroyInterface = function() { + self._destroy_interface = function() { try { - var jingle_sel = $('#jingle'); - - jingle_sel.stopTime(); - jingle_sel.find('*').stopTime(); - - jingle_sel.remove(); + Call.destroy_interface( + $('#jingle') + ); } catch(e) { - Console.error('Jingle.destroyInterface', e); + Console.error('Jingle._destroy_interface', e); } }; @@ -1500,20 +966,21 @@ var Jingle = (function() { /** * Show the Jingle interface - * @public + * @private * @return {boolean} */ - self.showInterface = function() { + self._show_interface = function() { try { - if(self.in_call() && self.is_video()) { - $('#jingle:hidden').show(); - - // Launch back some events - $('#jingle .videobox').mousemove(); + if(self.is_video()) { + Call.show_interface( + self, + $('#jingle'), + $('#jingle .videobox') + ); } } catch(e) { - Console.error('Jingle.showInterface', e); + Console.error('Jingle._show_interface', e); } finally { return false; } @@ -1523,18 +990,18 @@ var Jingle = (function() { /** * Hide the Jingle interface - * @public + * @private * @return {boolean} */ - self.hideInterface = function() { + self._hide_interface = function() { try { - $('#jingle:visible').hide(); - - // Reset some events - $('#jingle .videobox .topbar').stopTime().hide(); + Call.hide_interface( + $('#jingle'), + $('#jingle .videobox') + ); } catch(e) { - Console.error('Jingle.hideInterface', e); + Console.error('Jingle._hide_interface', e); } finally { return false; } @@ -1545,68 +1012,21 @@ var Jingle = (function() { /** * Attaches interface events * @private - * @return {undefined} + * @return {boolean} */ - self._eventsInterface = function() { + self._events_interface = function() { try { - var jingle_sel = $('#jingle'); - - jingle_sel.everyTime(50, function() { - self._adapt(); - }); - - // Close interface on click on semi-transparent background - jingle_sel.click(function(evt) { - try { - // Click on lock background? - if($(evt.target).is('.lock')) { - return self.hideInterface(); - } - } catch(e) { - Console.error('Jingle._eventsInterface[async]', e); - } - }); - - // Click on a control or action button - jingle_sel.find('.topbar').find('.controls a, .actions a').click(function() { - try { - switch($(this).data('type')) { - case 'close': - self.hideInterface(); break; - case 'stop': - self.stop(); break; - case 'mute': - self.mute(); break; - case 'unmute': - self.unmute(); break; - } - } catch(e) { - Console.error('Jingle._eventsInterface[async]', e); - } finally { - return false; - } - }); - - // Auto Hide/Show interface topbar - jingle_sel.find('.videobox').mousemove(function() { - try { - var topbar_sel = $(this).find('.topbar'); - - if(topbar_sel.is(':hidden')) { - topbar_sel.stop(true).fadeIn(250); - } - - topbar_sel.stopTime(); - topbar_sel.oneTime('5s', function() { - topbar_sel.stop(true).fadeOut(250); - }); - } catch(e) { - Console.error('Jingle._eventsInterface[async]', e); - } - }); + // Apply events + Call.events_interface( + self, + $('#jingle'), + $('#jingle .videobox') + ); } catch(e) { - Console.error('Popup._eventsInterface', e); + Console.error('Jingle._events_interface', e); + } finally { + return false; } }; diff --git a/source/app/javascripts/jquery.timers.js b/source/app/javascripts/jquery.timers.js index 7c5a309..dfadb43 100644 --- a/source/app/javascripts/jquery.timers.js +++ b/source/app/javascripts/jquery.timers.js @@ -1,138 +1,138 @@ -/** - * jQuery.timers - Timer abstractions for jQuery - * Written by Blair Mitchelmore (blair DOT mitchelmore AT gmail DOT com) - * Licensed under the WTFPL (http://sam.zoy.org/wtfpl/). - * Date: 2009/10/16 - * - * @author Blair Mitchelmore - * @version 1.2 - * - **/ - -jQuery.fn.extend({ - everyTime: function(interval, label, fn, times) { - return this.each(function() { - jQuery.timer.add(this, interval, label, fn, times); - }); - }, - oneTime: function(interval, label, fn) { - return this.each(function() { - jQuery.timer.add(this, interval, label, fn, 1); - }); - }, - stopTime: function(label, fn) { - return this.each(function() { - jQuery.timer.remove(this, label, fn); - }); - } -}); - -jQuery.extend({ - timer: { - global: [], - guid: 1, - dataKey: "jQuery.timer", - regex: /^([0-9]+(?:\.[0-9]*)?)\s*(.*s)?$/, - powers: { - // Yeah this is major overkill... - 'ms': 1, - 'cs': 10, - 'ds': 100, - 's': 1000, - 'das': 10000, - 'hs': 100000, - 'ks': 1000000 - }, - timeParse: function(value) { - if (value == undefined || value == null) - return null; - var result = this.regex.exec(jQuery.trim(value.toString())); - if (result[2]) { - var num = parseFloat(result[1]); - var mult = this.powers[result[2]] || 1; - return num * mult; - } else { - return value; - } - }, - add: function(element, interval, label, fn, times) { - var counter = 0; - - if (jQuery.isFunction(label)) { - if (!times) - times = fn; - fn = label; - label = interval; - } - - interval = jQuery.timer.timeParse(interval); - - if (typeof interval != 'number' || isNaN(interval) || interval < 0) - return; - - if (typeof times != 'number' || isNaN(times) || times < 0) - times = 0; - - times = times || 0; - - var timers = jQuery.data(element, this.dataKey) || jQuery.data(element, this.dataKey, {}); - - if (!timers[label]) - timers[label] = {}; - - fn.timerID = fn.timerID || this.guid++; - - var handler = function() { - if ((++counter > times && times !== 0) || fn.call(element, counter) === false) - jQuery.timer.remove(element, label, fn); - }; - - handler.timerID = fn.timerID; - - if (!timers[label][fn.timerID]) - timers[label][fn.timerID] = window.setInterval(handler,interval); - - this.global.push( element ); - - }, - remove: function(element, label, fn) { - var timers = jQuery.data(element, this.dataKey), ret; - - if ( timers ) { - - if (!label) { - for ( label in timers ) - this.remove(element, label, fn); - } else if ( timers[label] ) { - if ( fn ) { - if ( fn.timerID ) { - window.clearInterval(timers[label][fn.timerID]); - delete timers[label][fn.timerID]; - } - } else { - for ( var fn in timers[label] ) { - window.clearInterval(timers[label][fn]); - delete timers[label][fn]; - } - } - - for ( ret in timers[label] ) break; - if ( !ret ) { - ret = null; - delete timers[label]; - } - } - - for ( ret in timers ) break; - if ( !ret ) - jQuery.removeData(element, this.dataKey); - } - } - } -}); - -jQuery(window).bind("unload", function() { - jQuery.each(jQuery.timer.global, function(index, item) { - jQuery.timer.remove(item); - }); -}); +/** + * jQuery.timers - Timer abstractions for jQuery + * Written by Blair Mitchelmore (blair DOT mitchelmore AT gmail DOT com) + * Licensed under the WTFPL (http://sam.zoy.org/wtfpl/). + * Date: 2009/10/16 + * + * @author Blair Mitchelmore + * @version 1.2 + * + **/ + +jQuery.fn.extend({ + everyTime: function(interval, label, fn, times) { + return this.each(function() { + jQuery.timer.add(this, interval, label, fn, times); + }); + }, + oneTime: function(interval, label, fn) { + return this.each(function() { + jQuery.timer.add(this, interval, label, fn, 1); + }); + }, + stopTime: function(label, fn) { + return this.each(function() { + jQuery.timer.remove(this, label, fn); + }); + } +}); + +jQuery.extend({ + timer: { + global: [], + guid: 1, + dataKey: "jQuery.timer", + regex: /^([0-9]+(?:\.[0-9]*)?)\s*(.*s)?$/, + powers: { + // Yeah this is major overkill... + 'ms': 1, + 'cs': 10, + 'ds': 100, + 's': 1000, + 'das': 10000, + 'hs': 100000, + 'ks': 1000000 + }, + timeParse: function(value) { + if (value == undefined || value == null) + return null; + var result = this.regex.exec(jQuery.trim(value.toString())); + if (result[2]) { + var num = parseFloat(result[1]); + var mult = this.powers[result[2]] || 1; + return num * mult; + } else { + return value; + } + }, + add: function(element, interval, label, fn, times) { + var counter = 0; + + if (jQuery.isFunction(label)) { + if (!times) + times = fn; + fn = label; + label = interval; + } + + interval = jQuery.timer.timeParse(interval); + + if (typeof interval != 'number' || isNaN(interval) || interval < 0) + return; + + if (typeof times != 'number' || isNaN(times) || times < 0) + times = 0; + + times = times || 0; + + var timers = jQuery.data(element, this.dataKey) || jQuery.data(element, this.dataKey, {}); + + if (!timers[label]) + timers[label] = {}; + + fn.timerID = fn.timerID || this.guid++; + + var handler = function() { + if ((++counter > times && times !== 0) || fn.call(element, counter) === false) + jQuery.timer.remove(element, label, fn); + }; + + handler.timerID = fn.timerID; + + if (!timers[label][fn.timerID]) + timers[label][fn.timerID] = window.setInterval(handler,interval); + + this.global.push( element ); + + }, + remove: function(element, label, fn) { + var timers = jQuery.data(element, this.dataKey), ret; + + if ( timers ) { + + if (!label) { + for ( label in timers ) + this.remove(element, label, fn); + } else if ( timers[label] ) { + if ( fn ) { + if ( fn.timerID ) { + window.clearInterval(timers[label][fn.timerID]); + delete timers[label][fn.timerID]; + } + } else { + for ( var fn in timers[label] ) { + window.clearInterval(timers[label][fn]); + delete timers[label][fn]; + } + } + + for ( ret in timers[label] ) break; + if ( !ret ) { + ret = null; + delete timers[label]; + } + } + + for ( ret in timers ) break; + if ( !ret ) + jQuery.removeData(element, this.dataKey); + } + } + } +}); + +jQuery(window).bind("unload", function() { + jQuery.each(jQuery.timer.global, function(index, item) { + jQuery.timer.remove(item); + }); +}); diff --git a/source/app/javascripts/jsjac.jingle.js b/source/app/javascripts/jsjac.jingle.js index 776d34a..780ffa1 100644 --- a/source/app/javascripts/jsjac.jingle.js +++ b/source/app/javascripts/jsjac.jingle.js @@ -1,15 +1,1785 @@ /** + * jsjac-jingle [uncompressed] * @fileoverview JSJaC Jingle library, implementation of XEP-0166. - * Written originally for Uno.im service requirements * - * @version v0.7 (dev) + * @version 0.7.0 + * @date 2014-06-24 + * @author Valérian Saliou https://valeriansaliou.name/ + * @license MPL 2.0 + * + * @url https://github.com/valeriansaliou/jsjac-jingle + * @repository git+https://github.com/valeriansaliou/jsjac-jingle.git + * @depends https://github.com/sstrigler/JSJaC + */ + +// Underscore.js 1.6.0 +// http://underscorejs.org +// (c) 2009-2014 Jeremy Ashkenas, DocumentCloud and Investigative Reporters & Editors +// Underscore may be freely distributed under the MIT license. + +(function() { + + // Baseline setup + // -------------- + + // Establish the root object, `window` in the browser, or `exports` on the server. + var root = this; + + // Save the previous value of the `_` variable. + var previousUnderscore = root._; + + // Establish the object that gets returned to break out of a loop iteration. + var breaker = {}; + + // Save bytes in the minified (but not gzipped) version: + var ArrayProto = Array.prototype, ObjProto = Object.prototype, FuncProto = Function.prototype; + + // Create quick reference variables for speed access to core prototypes. + var + push = ArrayProto.push, + slice = ArrayProto.slice, + concat = ArrayProto.concat, + toString = ObjProto.toString, + hasOwnProperty = ObjProto.hasOwnProperty; + + // All **ECMAScript 5** native function implementations that we hope to use + // are declared here. + var + nativeForEach = ArrayProto.forEach, + nativeMap = ArrayProto.map, + nativeReduce = ArrayProto.reduce, + nativeReduceRight = ArrayProto.reduceRight, + nativeFilter = ArrayProto.filter, + nativeEvery = ArrayProto.every, + nativeSome = ArrayProto.some, + nativeIndexOf = ArrayProto.indexOf, + nativeLastIndexOf = ArrayProto.lastIndexOf, + nativeIsArray = Array.isArray, + nativeKeys = Object.keys, + nativeBind = FuncProto.bind; + + // Create a safe reference to the Underscore object for use below. + var _ = function(obj) { + if (obj instanceof _) return obj; + if (!(this instanceof _)) return new _(obj); + this._wrapped = obj; + }; + + // Export the Underscore object for **Node.js**, with + // backwards-compatibility for the old `require()` API. If we're in + // the browser, add `_` as a global object via a string identifier, + // for Closure Compiler "advanced" mode. + if (typeof exports !== 'undefined') { + if (typeof module !== 'undefined' && module.exports) { + exports = module.exports = _; + } + exports._ = _; + } else { + root._ = _; + } + + // Current version. + _.VERSION = '1.6.0'; + + // Collection Functions + // -------------------- + + // The cornerstone, an `each` implementation, aka `forEach`. + // Handles objects with the built-in `forEach`, arrays, and raw objects. + // Delegates to **ECMAScript 5**'s native `forEach` if available. + var each = _.each = _.forEach = function(obj, iterator, context) { + if (obj == null) return obj; + if (nativeForEach && obj.forEach === nativeForEach) { + obj.forEach(iterator, context); + } else if (obj.length === +obj.length) { + for (var i = 0, length = obj.length; i < length; i++) { + if (iterator.call(context, obj[i], i, obj) === breaker) return; + } + } else { + var keys = _.keys(obj); + for (var i = 0, length = keys.length; i < length; i++) { + if (iterator.call(context, obj[keys[i]], keys[i], obj) === breaker) return; + } + } + return obj; + }; + + // Return the results of applying the iterator to each element. + // Delegates to **ECMAScript 5**'s native `map` if available. + _.map = _.collect = function(obj, iterator, context) { + var results = []; + if (obj == null) return results; + if (nativeMap && obj.map === nativeMap) return obj.map(iterator, context); + each(obj, function(value, index, list) { + results.push(iterator.call(context, value, index, list)); + }); + return results; + }; + + var reduceError = 'Reduce of empty array with no initial value'; + + // **Reduce** builds up a single result from a list of values, aka `inject`, + // or `foldl`. Delegates to **ECMAScript 5**'s native `reduce` if available. + _.reduce = _.foldl = _.inject = function(obj, iterator, memo, context) { + var initial = arguments.length > 2; + if (obj == null) obj = []; + if (nativeReduce && obj.reduce === nativeReduce) { + if (context) iterator = _.bind(iterator, context); + return initial ? obj.reduce(iterator, memo) : obj.reduce(iterator); + } + each(obj, function(value, index, list) { + if (!initial) { + memo = value; + initial = true; + } else { + memo = iterator.call(context, memo, value, index, list); + } + }); + if (!initial) throw new TypeError(reduceError); + return memo; + }; + + // The right-associative version of reduce, also known as `foldr`. + // Delegates to **ECMAScript 5**'s native `reduceRight` if available. + _.reduceRight = _.foldr = function(obj, iterator, memo, context) { + var initial = arguments.length > 2; + if (obj == null) obj = []; + if (nativeReduceRight && obj.reduceRight === nativeReduceRight) { + if (context) iterator = _.bind(iterator, context); + return initial ? obj.reduceRight(iterator, memo) : obj.reduceRight(iterator); + } + var length = obj.length; + if (length !== +length) { + var keys = _.keys(obj); + length = keys.length; + } + each(obj, function(value, index, list) { + index = keys ? keys[--length] : --length; + if (!initial) { + memo = obj[index]; + initial = true; + } else { + memo = iterator.call(context, memo, obj[index], index, list); + } + }); + if (!initial) throw new TypeError(reduceError); + return memo; + }; + + // Return the first value which passes a truth test. Aliased as `detect`. + _.find = _.detect = function(obj, predicate, context) { + var result; + any(obj, function(value, index, list) { + if (predicate.call(context, value, index, list)) { + result = value; + return true; + } + }); + return result; + }; + + // Return all the elements that pass a truth test. + // Delegates to **ECMAScript 5**'s native `filter` if available. + // Aliased as `select`. + _.filter = _.select = function(obj, predicate, context) { + var results = []; + if (obj == null) return results; + if (nativeFilter && obj.filter === nativeFilter) return obj.filter(predicate, context); + each(obj, function(value, index, list) { + if (predicate.call(context, value, index, list)) results.push(value); + }); + return results; + }; + + // Return all the elements for which a truth test fails. + _.reject = function(obj, predicate, context) { + return _.filter(obj, function(value, index, list) { + return !predicate.call(context, value, index, list); + }, context); + }; + + // Determine whether all of the elements match a truth test. + // Delegates to **ECMAScript 5**'s native `every` if available. + // Aliased as `all`. + _.every = _.all = function(obj, predicate, context) { + predicate || (predicate = _.identity); + var result = true; + if (obj == null) return result; + if (nativeEvery && obj.every === nativeEvery) return obj.every(predicate, context); + each(obj, function(value, index, list) { + if (!(result = result && predicate.call(context, value, index, list))) return breaker; + }); + return !!result; + }; + + // Determine if at least one element in the object matches a truth test. + // Delegates to **ECMAScript 5**'s native `some` if available. + // Aliased as `any`. + var any = _.some = _.any = function(obj, predicate, context) { + predicate || (predicate = _.identity); + var result = false; + if (obj == null) return result; + if (nativeSome && obj.some === nativeSome) return obj.some(predicate, context); + each(obj, function(value, index, list) { + if (result || (result = predicate.call(context, value, index, list))) return breaker; + }); + return !!result; + }; + + // Determine if the array or object contains a given value (using `===`). + // Aliased as `include`. + _.contains = _.include = function(obj, target) { + if (obj == null) return false; + if (nativeIndexOf && obj.indexOf === nativeIndexOf) return obj.indexOf(target) != -1; + return any(obj, function(value) { + return value === target; + }); + }; + + // Invoke a method (with arguments) on every item in a collection. + _.invoke = function(obj, method) { + var args = slice.call(arguments, 2); + var isFunc = _.isFunction(method); + return _.map(obj, function(value) { + return (isFunc ? method : value[method]).apply(value, args); + }); + }; + + // Convenience version of a common use case of `map`: fetching a property. + _.pluck = function(obj, key) { + return _.map(obj, _.property(key)); + }; + + // Convenience version of a common use case of `filter`: selecting only objects + // containing specific `key:value` pairs. + _.where = function(obj, attrs) { + return _.filter(obj, _.matches(attrs)); + }; + + // Convenience version of a common use case of `find`: getting the first object + // containing specific `key:value` pairs. + _.findWhere = function(obj, attrs) { + return _.find(obj, _.matches(attrs)); + }; + + // Return the maximum element or (element-based computation). + // Can't optimize arrays of integers longer than 65,535 elements. + // See [WebKit Bug 80797](https://bugs.webkit.org/show_bug.cgi?id=80797) + _.max = function(obj, iterator, context) { + if (!iterator && _.isArray(obj) && obj[0] === +obj[0] && obj.length < 65535) { + return Math.max.apply(Math, obj); + } + var result = -Infinity, lastComputed = -Infinity; + each(obj, function(value, index, list) { + var computed = iterator ? iterator.call(context, value, index, list) : value; + if (computed > lastComputed) { + result = value; + lastComputed = computed; + } + }); + return result; + }; + + // Return the minimum element (or element-based computation). + _.min = function(obj, iterator, context) { + if (!iterator && _.isArray(obj) && obj[0] === +obj[0] && obj.length < 65535) { + return Math.min.apply(Math, obj); + } + var result = Infinity, lastComputed = Infinity; + each(obj, function(value, index, list) { + var computed = iterator ? iterator.call(context, value, index, list) : value; + if (computed < lastComputed) { + result = value; + lastComputed = computed; + } + }); + return result; + }; + + // Shuffle an array, using the modern version of the + // [Fisher-Yates shuffle](http://en.wikipedia.org/wiki/Fisher–Yates_shuffle). + _.shuffle = function(obj) { + var rand; + var index = 0; + var shuffled = []; + each(obj, function(value) { + rand = _.random(index++); + shuffled[index - 1] = shuffled[rand]; + shuffled[rand] = value; + }); + return shuffled; + }; + + // Sample **n** random values from a collection. + // If **n** is not specified, returns a single random element. + // The internal `guard` argument allows it to work with `map`. + _.sample = function(obj, n, guard) { + if (n == null || guard) { + if (obj.length !== +obj.length) obj = _.values(obj); + return obj[_.random(obj.length - 1)]; + } + return _.shuffle(obj).slice(0, Math.max(0, n)); + }; + + // An internal function to generate lookup iterators. + var lookupIterator = function(value) { + if (value == null) return _.identity; + if (_.isFunction(value)) return value; + return _.property(value); + }; + + // Sort the object's values by a criterion produced by an iterator. + _.sortBy = function(obj, iterator, context) { + iterator = lookupIterator(iterator); + return _.pluck(_.map(obj, function(value, index, list) { + return { + value: value, + index: index, + criteria: iterator.call(context, value, index, list) + }; + }).sort(function(left, right) { + var a = left.criteria; + var b = right.criteria; + if (a !== b) { + if (a > b || a === void 0) return 1; + if (a < b || b === void 0) return -1; + } + return left.index - right.index; + }), 'value'); + }; + + // An internal function used for aggregate "group by" operations. + var group = function(behavior) { + return function(obj, iterator, context) { + var result = {}; + iterator = lookupIterator(iterator); + each(obj, function(value, index) { + var key = iterator.call(context, value, index, obj); + behavior(result, key, value); + }); + return result; + }; + }; + + // Groups the object's values by a criterion. Pass either a string attribute + // to group by, or a function that returns the criterion. + _.groupBy = group(function(result, key, value) { + _.has(result, key) ? result[key].push(value) : result[key] = [value]; + }); + + // Indexes the object's values by a criterion, similar to `groupBy`, but for + // when you know that your index values will be unique. + _.indexBy = group(function(result, key, value) { + result[key] = value; + }); + + // Counts instances of an object that group by a certain criterion. Pass + // either a string attribute to count by, or a function that returns the + // criterion. + _.countBy = group(function(result, key) { + _.has(result, key) ? result[key]++ : result[key] = 1; + }); + + // Use a comparator function to figure out the smallest index at which + // an object should be inserted so as to maintain order. Uses binary search. + _.sortedIndex = function(array, obj, iterator, context) { + iterator = lookupIterator(iterator); + var value = iterator.call(context, obj); + var low = 0, high = array.length; + while (low < high) { + var mid = (low + high) >>> 1; + iterator.call(context, array[mid]) < value ? low = mid + 1 : high = mid; + } + return low; + }; + + // Safely create a real, live array from anything iterable. + _.toArray = function(obj) { + if (!obj) return []; + if (_.isArray(obj)) return slice.call(obj); + if (obj.length === +obj.length) return _.map(obj, _.identity); + return _.values(obj); + }; + + // Return the number of elements in an object. + _.size = function(obj) { + if (obj == null) return 0; + return (obj.length === +obj.length) ? obj.length : _.keys(obj).length; + }; + + // Array Functions + // --------------- + + // Get the first element of an array. Passing **n** will return the first N + // values in the array. Aliased as `head` and `take`. The **guard** check + // allows it to work with `_.map`. + _.first = _.head = _.take = function(array, n, guard) { + if (array == null) return void 0; + if ((n == null) || guard) return array[0]; + if (n < 0) return []; + return slice.call(array, 0, n); + }; + + // Returns everything but the last entry of the array. Especially useful on + // the arguments object. Passing **n** will return all the values in + // the array, excluding the last N. The **guard** check allows it to work with + // `_.map`. + _.initial = function(array, n, guard) { + return slice.call(array, 0, array.length - ((n == null) || guard ? 1 : n)); + }; + + // Get the last element of an array. Passing **n** will return the last N + // values in the array. The **guard** check allows it to work with `_.map`. + _.last = function(array, n, guard) { + if (array == null) return void 0; + if ((n == null) || guard) return array[array.length - 1]; + return slice.call(array, Math.max(array.length - n, 0)); + }; + + // Returns everything but the first entry of the array. Aliased as `tail` and `drop`. + // Especially useful on the arguments object. Passing an **n** will return + // the rest N values in the array. The **guard** + // check allows it to work with `_.map`. + _.rest = _.tail = _.drop = function(array, n, guard) { + return slice.call(array, (n == null) || guard ? 1 : n); + }; + + // Trim out all falsy values from an array. + _.compact = function(array) { + return _.filter(array, _.identity); + }; + + // Internal implementation of a recursive `flatten` function. + var flatten = function(input, shallow, output) { + if (shallow && _.every(input, _.isArray)) { + return concat.apply(output, input); + } + each(input, function(value) { + if (_.isArray(value) || _.isArguments(value)) { + shallow ? push.apply(output, value) : flatten(value, shallow, output); + } else { + output.push(value); + } + }); + return output; + }; + + // Flatten out an array, either recursively (by default), or just one level. + _.flatten = function(array, shallow) { + return flatten(array, shallow, []); + }; + + // Return a version of the array that does not contain the specified value(s). + _.without = function(array) { + return _.difference(array, slice.call(arguments, 1)); + }; + + // Split an array into two arrays: one whose elements all satisfy the given + // predicate, and one whose elements all do not satisfy the predicate. + _.partition = function(array, predicate) { + var pass = [], fail = []; + each(array, function(elem) { + (predicate(elem) ? pass : fail).push(elem); + }); + return [pass, fail]; + }; + + // Produce a duplicate-free version of the array. If the array has already + // been sorted, you have the option of using a faster algorithm. + // Aliased as `unique`. + _.uniq = _.unique = function(array, isSorted, iterator, context) { + if (_.isFunction(isSorted)) { + context = iterator; + iterator = isSorted; + isSorted = false; + } + var initial = iterator ? _.map(array, iterator, context) : array; + var results = []; + var seen = []; + each(initial, function(value, index) { + if (isSorted ? (!index || seen[seen.length - 1] !== value) : !_.contains(seen, value)) { + seen.push(value); + results.push(array[index]); + } + }); + return results; + }; + + // Produce an array that contains the union: each distinct element from all of + // the passed-in arrays. + _.union = function() { + return _.uniq(_.flatten(arguments, true)); + }; + + // Produce an array that contains every item shared between all the + // passed-in arrays. + _.intersection = function(array) { + var rest = slice.call(arguments, 1); + return _.filter(_.uniq(array), function(item) { + return _.every(rest, function(other) { + return _.contains(other, item); + }); + }); + }; + + // Take the difference between one array and a number of other arrays. + // Only the elements present in just the first array will remain. + _.difference = function(array) { + var rest = concat.apply(ArrayProto, slice.call(arguments, 1)); + return _.filter(array, function(value){ return !_.contains(rest, value); }); + }; + + // Zip together multiple lists into a single array -- elements that share + // an index go together. + _.zip = function() { + var length = _.max(_.pluck(arguments, 'length').concat(0)); + var results = new Array(length); + for (var i = 0; i < length; i++) { + results[i] = _.pluck(arguments, '' + i); + } + return results; + }; + + // Converts lists into objects. Pass either a single array of `[key, value]` + // pairs, or two parallel arrays of the same length -- one of keys, and one of + // the corresponding values. + _.object = function(list, values) { + if (list == null) return {}; + var result = {}; + for (var i = 0, length = list.length; i < length; i++) { + if (values) { + result[list[i]] = values[i]; + } else { + result[list[i][0]] = list[i][1]; + } + } + return result; + }; + + // If the browser doesn't supply us with indexOf (I'm looking at you, **MSIE**), + // we need this function. Return the position of the first occurrence of an + // item in an array, or -1 if the item is not included in the array. + // Delegates to **ECMAScript 5**'s native `indexOf` if available. + // If the array is large and already in sort order, pass `true` + // for **isSorted** to use binary search. + _.indexOf = function(array, item, isSorted) { + if (array == null) return -1; + var i = 0, length = array.length; + if (isSorted) { + if (typeof isSorted == 'number') { + i = (isSorted < 0 ? Math.max(0, length + isSorted) : isSorted); + } else { + i = _.sortedIndex(array, item); + return array[i] === item ? i : -1; + } + } + if (nativeIndexOf && array.indexOf === nativeIndexOf) return array.indexOf(item, isSorted); + for (; i < length; i++) if (array[i] === item) return i; + return -1; + }; + + // Delegates to **ECMAScript 5**'s native `lastIndexOf` if available. + _.lastIndexOf = function(array, item, from) { + if (array == null) return -1; + var hasIndex = from != null; + if (nativeLastIndexOf && array.lastIndexOf === nativeLastIndexOf) { + return hasIndex ? array.lastIndexOf(item, from) : array.lastIndexOf(item); + } + var i = (hasIndex ? from : array.length); + while (i--) if (array[i] === item) return i; + return -1; + }; + + // Generate an integer Array containing an arithmetic progression. A port of + // the native Python `range()` function. See + // [the Python documentation](http://docs.python.org/library/functions.html#range). + _.range = function(start, stop, step) { + if (arguments.length <= 1) { + stop = start || 0; + start = 0; + } + step = arguments[2] || 1; + + var length = Math.max(Math.ceil((stop - start) / step), 0); + var idx = 0; + var range = new Array(length); + + while(idx < length) { + range[idx++] = start; + start += step; + } + + return range; + }; + + // Function (ahem) Functions + // ------------------ + + // Reusable constructor function for prototype setting. + var ctor = function(){}; + + // Create a function bound to a given object (assigning `this`, and arguments, + // optionally). Delegates to **ECMAScript 5**'s native `Function.bind` if + // available. + _.bind = function(func, context) { + var args, bound; + if (nativeBind && func.bind === nativeBind) return nativeBind.apply(func, slice.call(arguments, 1)); + if (!_.isFunction(func)) throw new TypeError; + args = slice.call(arguments, 2); + return bound = function() { + if (!(this instanceof bound)) return func.apply(context, args.concat(slice.call(arguments))); + ctor.prototype = func.prototype; + var self = new ctor; + ctor.prototype = null; + var result = func.apply(self, args.concat(slice.call(arguments))); + if (Object(result) === result) return result; + return self; + }; + }; + + // Partially apply a function by creating a version that has had some of its + // arguments pre-filled, without changing its dynamic `this` context. _ acts + // as a placeholder, allowing any combination of arguments to be pre-filled. + _.partial = function(func) { + var boundArgs = slice.call(arguments, 1); + return function() { + var position = 0; + var args = boundArgs.slice(); + for (var i = 0, length = args.length; i < length; i++) { + if (args[i] === _) args[i] = arguments[position++]; + } + while (position < arguments.length) args.push(arguments[position++]); + return func.apply(this, args); + }; + }; + + // Bind a number of an object's methods to that object. Remaining arguments + // are the method names to be bound. Useful for ensuring that all callbacks + // defined on an object belong to it. + _.bindAll = function(obj) { + var funcs = slice.call(arguments, 1); + if (funcs.length === 0) throw new Error('bindAll must be passed function names'); + each(funcs, function(f) { obj[f] = _.bind(obj[f], obj); }); + return obj; + }; + + // Memoize an expensive function by storing its results. + _.memoize = function(func, hasher) { + var memo = {}; + hasher || (hasher = _.identity); + return function() { + var key = hasher.apply(this, arguments); + return _.has(memo, key) ? memo[key] : (memo[key] = func.apply(this, arguments)); + }; + }; + + // Delays a function for the given number of milliseconds, and then calls + // it with the arguments supplied. + _.delay = function(func, wait) { + var args = slice.call(arguments, 2); + return setTimeout(function(){ return func.apply(null, args); }, wait); + }; + + // Defers a function, scheduling it to run after the current call stack has + // cleared. + _.defer = function(func) { + return _.delay.apply(_, [func, 1].concat(slice.call(arguments, 1))); + }; + + // Returns a function, that, when invoked, will only be triggered at most once + // during a given window of time. Normally, the throttled function will run + // as much as it can, without ever going more than once per `wait` duration; + // but if you'd like to disable the execution on the leading edge, pass + // `{leading: false}`. To disable execution on the trailing edge, ditto. + _.throttle = function(func, wait, options) { + var context, args, result; + var timeout = null; + var previous = 0; + options || (options = {}); + var later = function() { + previous = options.leading === false ? 0 : _.now(); + timeout = null; + result = func.apply(context, args); + context = args = null; + }; + return function() { + var now = _.now(); + if (!previous && options.leading === false) previous = now; + var remaining = wait - (now - previous); + context = this; + args = arguments; + if (remaining <= 0) { + clearTimeout(timeout); + timeout = null; + previous = now; + result = func.apply(context, args); + context = args = null; + } else if (!timeout && options.trailing !== false) { + timeout = setTimeout(later, remaining); + } + return result; + }; + }; + + // Returns a function, that, as long as it continues to be invoked, will not + // be triggered. The function will be called after it stops being called for + // N milliseconds. If `immediate` is passed, trigger the function on the + // leading edge, instead of the trailing. + _.debounce = function(func, wait, immediate) { + var timeout, args, context, timestamp, result; + + var later = function() { + var last = _.now() - timestamp; + if (last < wait) { + timeout = setTimeout(later, wait - last); + } else { + timeout = null; + if (!immediate) { + result = func.apply(context, args); + context = args = null; + } + } + }; + + return function() { + context = this; + args = arguments; + timestamp = _.now(); + var callNow = immediate && !timeout; + if (!timeout) { + timeout = setTimeout(later, wait); + } + if (callNow) { + result = func.apply(context, args); + context = args = null; + } + + return result; + }; + }; + + // Returns a function that will be executed at most one time, no matter how + // often you call it. Useful for lazy initialization. + _.once = function(func) { + var ran = false, memo; + return function() { + if (ran) return memo; + ran = true; + memo = func.apply(this, arguments); + func = null; + return memo; + }; + }; + + // Returns the first function passed as an argument to the second, + // allowing you to adjust arguments, run code before and after, and + // conditionally execute the original function. + _.wrap = function(func, wrapper) { + return _.partial(wrapper, func); + }; + + // Returns a function that is the composition of a list of functions, each + // consuming the return value of the function that follows. + _.compose = function() { + var funcs = arguments; + return function() { + var args = arguments; + for (var i = funcs.length - 1; i >= 0; i--) { + args = [funcs[i].apply(this, args)]; + } + return args[0]; + }; + }; + + // Returns a function that will only be executed after being called N times. + _.after = function(times, func) { + return function() { + if (--times < 1) { + return func.apply(this, arguments); + } + }; + }; + + // Object Functions + // ---------------- + + // Retrieve the names of an object's properties. + // Delegates to **ECMAScript 5**'s native `Object.keys` + _.keys = function(obj) { + if (!_.isObject(obj)) return []; + if (nativeKeys) return nativeKeys(obj); + var keys = []; + for (var key in obj) if (_.has(obj, key)) keys.push(key); + return keys; + }; + + // Retrieve the values of an object's properties. + _.values = function(obj) { + var keys = _.keys(obj); + var length = keys.length; + var values = new Array(length); + for (var i = 0; i < length; i++) { + values[i] = obj[keys[i]]; + } + return values; + }; + + // Convert an object into a list of `[key, value]` pairs. + _.pairs = function(obj) { + var keys = _.keys(obj); + var length = keys.length; + var pairs = new Array(length); + for (var i = 0; i < length; i++) { + pairs[i] = [keys[i], obj[keys[i]]]; + } + return pairs; + }; + + // Invert the keys and values of an object. The values must be serializable. + _.invert = function(obj) { + var result = {}; + var keys = _.keys(obj); + for (var i = 0, length = keys.length; i < length; i++) { + result[obj[keys[i]]] = keys[i]; + } + return result; + }; + + // Return a sorted list of the function names available on the object. + // Aliased as `methods` + _.functions = _.methods = function(obj) { + var names = []; + for (var key in obj) { + if (_.isFunction(obj[key])) names.push(key); + } + return names.sort(); + }; + + // Extend a given object with all the properties in passed-in object(s). + _.extend = function(obj) { + each(slice.call(arguments, 1), function(source) { + if (source) { + for (var prop in source) { + obj[prop] = source[prop]; + } + } + }); + return obj; + }; + + // Return a copy of the object only containing the whitelisted properties. + _.pick = function(obj) { + var copy = {}; + var keys = concat.apply(ArrayProto, slice.call(arguments, 1)); + each(keys, function(key) { + if (key in obj) copy[key] = obj[key]; + }); + return copy; + }; + + // Return a copy of the object without the blacklisted properties. + _.omit = function(obj) { + var copy = {}; + var keys = concat.apply(ArrayProto, slice.call(arguments, 1)); + for (var key in obj) { + if (!_.contains(keys, key)) copy[key] = obj[key]; + } + return copy; + }; + + // Fill in a given object with default properties. + _.defaults = function(obj) { + each(slice.call(arguments, 1), function(source) { + if (source) { + for (var prop in source) { + if (obj[prop] === void 0) obj[prop] = source[prop]; + } + } + }); + return obj; + }; + + // Create a (shallow-cloned) duplicate of an object. + _.clone = function(obj) { + if (!_.isObject(obj)) return obj; + return _.isArray(obj) ? obj.slice() : _.extend({}, obj); + }; + + // Invokes interceptor with the obj, and then returns obj. + // The primary purpose of this method is to "tap into" a method chain, in + // order to perform operations on intermediate results within the chain. + _.tap = function(obj, interceptor) { + interceptor(obj); + return obj; + }; + + // Internal recursive comparison function for `isEqual`. + var eq = function(a, b, aStack, bStack) { + // Identical objects are equal. `0 === -0`, but they aren't identical. + // See the [Harmony `egal` proposal](http://wiki.ecmascript.org/doku.php?id=harmony:egal). + if (a === b) return a !== 0 || 1 / a == 1 / b; + // A strict comparison is necessary because `null == undefined`. + if (a == null || b == null) return a === b; + // Unwrap any wrapped objects. + if (a instanceof _) a = a._wrapped; + if (b instanceof _) b = b._wrapped; + // Compare `[[Class]]` names. + var className = toString.call(a); + if (className != toString.call(b)) return false; + switch (className) { + // Strings, numbers, dates, and booleans are compared by value. + case '[object String]': + // Primitives and their corresponding object wrappers are equivalent; thus, `"5"` is + // equivalent to `new String("5")`. + return a == String(b); + case '[object Number]': + // `NaN`s are equivalent, but non-reflexive. An `egal` comparison is performed for + // other numeric values. + return a != +a ? b != +b : (a == 0 ? 1 / a == 1 / b : a == +b); + case '[object Date]': + case '[object Boolean]': + // Coerce dates and booleans to numeric primitive values. Dates are compared by their + // millisecond representations. Note that invalid dates with millisecond representations + // of `NaN` are not equivalent. + return +a == +b; + // RegExps are compared by their source patterns and flags. + case '[object RegExp]': + return a.source == b.source && + a.global == b.global && + a.multiline == b.multiline && + a.ignoreCase == b.ignoreCase; + } + if (typeof a != 'object' || typeof b != 'object') return false; + // Assume equality for cyclic structures. The algorithm for detecting cyclic + // structures is adapted from ES 5.1 section 15.12.3, abstract operation `JO`. + var length = aStack.length; + while (length--) { + // Linear search. Performance is inversely proportional to the number of + // unique nested structures. + if (aStack[length] == a) return bStack[length] == b; + } + // Objects with different constructors are not equivalent, but `Object`s + // from different frames are. + var aCtor = a.constructor, bCtor = b.constructor; + if (aCtor !== bCtor && !(_.isFunction(aCtor) && (aCtor instanceof aCtor) && + _.isFunction(bCtor) && (bCtor instanceof bCtor)) + && ('constructor' in a && 'constructor' in b)) { + return false; + } + // Add the first object to the stack of traversed objects. + aStack.push(a); + bStack.push(b); + var size = 0, result = true; + // Recursively compare objects and arrays. + if (className == '[object Array]') { + // Compare array lengths to determine if a deep comparison is necessary. + size = a.length; + result = size == b.length; + if (result) { + // Deep compare the contents, ignoring non-numeric properties. + while (size--) { + if (!(result = eq(a[size], b[size], aStack, bStack))) break; + } + } + } else { + // Deep compare objects. + for (var key in a) { + if (_.has(a, key)) { + // Count the expected number of properties. + size++; + // Deep compare each member. + if (!(result = _.has(b, key) && eq(a[key], b[key], aStack, bStack))) break; + } + } + // Ensure that both objects contain the same number of properties. + if (result) { + for (key in b) { + if (_.has(b, key) && !(size--)) break; + } + result = !size; + } + } + // Remove the first object from the stack of traversed objects. + aStack.pop(); + bStack.pop(); + return result; + }; + + // Perform a deep comparison to check if two objects are equal. + _.isEqual = function(a, b) { + return eq(a, b, [], []); + }; + + // Is a given array, string, or object empty? + // An "empty" object has no enumerable own-properties. + _.isEmpty = function(obj) { + if (obj == null) return true; + if (_.isArray(obj) || _.isString(obj)) return obj.length === 0; + for (var key in obj) if (_.has(obj, key)) return false; + return true; + }; + + // Is a given value a DOM element? + _.isElement = function(obj) { + return !!(obj && obj.nodeType === 1); + }; + + // Is a given value an array? + // Delegates to ECMA5's native Array.isArray + _.isArray = nativeIsArray || function(obj) { + return toString.call(obj) == '[object Array]'; + }; + + // Is a given variable an object? + _.isObject = function(obj) { + return obj === Object(obj); + }; + + // Add some isType methods: isArguments, isFunction, isString, isNumber, isDate, isRegExp. + each(['Arguments', 'Function', 'String', 'Number', 'Date', 'RegExp'], function(name) { + _['is' + name] = function(obj) { + return toString.call(obj) == '[object ' + name + ']'; + }; + }); + + // Define a fallback version of the method in browsers (ahem, IE), where + // there isn't any inspectable "Arguments" type. + if (!_.isArguments(arguments)) { + _.isArguments = function(obj) { + return !!(obj && _.has(obj, 'callee')); + }; + } + + // Optimize `isFunction` if appropriate. + if (typeof (/./) !== 'function') { + _.isFunction = function(obj) { + return typeof obj === 'function'; + }; + } + + // Is a given object a finite number? + _.isFinite = function(obj) { + return isFinite(obj) && !isNaN(parseFloat(obj)); + }; + + // Is the given value `NaN`? (NaN is the only number which does not equal itself). + _.isNaN = function(obj) { + return _.isNumber(obj) && obj != +obj; + }; + + // Is a given value a boolean? + _.isBoolean = function(obj) { + return obj === true || obj === false || toString.call(obj) == '[object Boolean]'; + }; + + // Is a given value equal to null? + _.isNull = function(obj) { + return obj === null; + }; + + // Is a given variable undefined? + _.isUndefined = function(obj) { + return obj === void 0; + }; + + // Shortcut function for checking if an object has a given property directly + // on itself (in other words, not on a prototype). + _.has = function(obj, key) { + return hasOwnProperty.call(obj, key); + }; + + // Utility Functions + // ----------------- + + // Run Underscore.js in *noConflict* mode, returning the `_` variable to its + // previous owner. Returns a reference to the Underscore object. + _.noConflict = function() { + root._ = previousUnderscore; + return this; + }; + + // Keep the identity function around for default iterators. + _.identity = function(value) { + return value; + }; + + _.constant = function(value) { + return function () { + return value; + }; + }; + + _.property = function(key) { + return function(obj) { + return obj[key]; + }; + }; + + // Returns a predicate for checking whether an object has a given set of `key:value` pairs. + _.matches = function(attrs) { + return function(obj) { + if (obj === attrs) return true; //avoid comparing an object to itself. + for (var key in attrs) { + if (attrs[key] !== obj[key]) + return false; + } + return true; + } + }; + + // Run a function **n** times. + _.times = function(n, iterator, context) { + var accum = Array(Math.max(0, n)); + for (var i = 0; i < n; i++) accum[i] = iterator.call(context, i); + return accum; + }; + + // Return a random integer between min and max (inclusive). + _.random = function(min, max) { + if (max == null) { + max = min; + min = 0; + } + return min + Math.floor(Math.random() * (max - min + 1)); + }; + + // A (possibly faster) way to get the current timestamp as an integer. + _.now = Date.now || function() { return new Date().getTime(); }; + + // List of HTML entities for escaping. + var entityMap = { + escape: { + '&': '&', + '<': '<', + '>': '>', + '"': '"', + "'": ''' + } + }; + entityMap.unescape = _.invert(entityMap.escape); + + // Regexes containing the keys and values listed immediately above. + var entityRegexes = { + escape: new RegExp('[' + _.keys(entityMap.escape).join('') + ']', 'g'), + unescape: new RegExp('(' + _.keys(entityMap.unescape).join('|') + ')', 'g') + }; + + // Functions for escaping and unescaping strings to/from HTML interpolation. + _.each(['escape', 'unescape'], function(method) { + _[method] = function(string) { + if (string == null) return ''; + return ('' + string).replace(entityRegexes[method], function(match) { + return entityMap[method][match]; + }); + }; + }); + + // If the value of the named `property` is a function then invoke it with the + // `object` as context; otherwise, return it. + _.result = function(object, property) { + if (object == null) return void 0; + var value = object[property]; + return _.isFunction(value) ? value.call(object) : value; + }; + + // Add your own custom functions to the Underscore object. + _.mixin = function(obj) { + each(_.functions(obj), function(name) { + var func = _[name] = obj[name]; + _.prototype[name] = function() { + var args = [this._wrapped]; + push.apply(args, arguments); + return result.call(this, func.apply(_, args)); + }; + }); + }; + + // Generate a unique integer id (unique within the entire client session). + // Useful for temporary DOM ids. + var idCounter = 0; + _.uniqueId = function(prefix) { + var id = ++idCounter + ''; + return prefix ? prefix + id : id; + }; + + // By default, Underscore uses ERB-style template delimiters, change the + // following template settings to use alternative delimiters. + _.templateSettings = { + evaluate : /<%([\s\S]+?)%>/g, + interpolate : /<%=([\s\S]+?)%>/g, + escape : /<%-([\s\S]+?)%>/g + }; + + // When customizing `templateSettings`, if you don't want to define an + // interpolation, evaluation or escaping regex, we need one that is + // guaranteed not to match. + var noMatch = /(.)^/; + + // Certain characters need to be escaped so that they can be put into a + // string literal. + var escapes = { + "'": "'", + '\\': '\\', + '\r': 'r', + '\n': 'n', + '\t': 't', + '\u2028': 'u2028', + '\u2029': 'u2029' + }; + + var escaper = /\\|'|\r|\n|\t|\u2028|\u2029/g; + + // JavaScript micro-templating, similar to John Resig's implementation. + // Underscore templating handles arbitrary delimiters, preserves whitespace, + // and correctly escapes quotes within interpolated code. + _.template = function(text, data, settings) { + var render; + settings = _.defaults({}, settings, _.templateSettings); + + // Combine delimiters into one regular expression via alternation. + var matcher = new RegExp([ + (settings.escape || noMatch).source, + (settings.interpolate || noMatch).source, + (settings.evaluate || noMatch).source + ].join('|') + '|$', 'g'); + + // Compile the template source, escaping string literals appropriately. + var index = 0; + var source = "__p+='"; + text.replace(matcher, function(match, escape, interpolate, evaluate, offset) { + source += text.slice(index, offset) + .replace(escaper, function(match) { return '\\' + escapes[match]; }); + + if (escape) { + source += "'+\n((__t=(" + escape + "))==null?'':_.escape(__t))+\n'"; + } + if (interpolate) { + source += "'+\n((__t=(" + interpolate + "))==null?'':__t)+\n'"; + } + if (evaluate) { + source += "';\n" + evaluate + "\n__p+='"; + } + index = offset + match.length; + return match; + }); + source += "';\n"; + + // If a variable is not specified, place data values in local scope. + if (!settings.variable) source = 'with(obj||{}){\n' + source + '}\n'; + + source = "var __t,__p='',__j=Array.prototype.join," + + "print=function(){__p+=__j.call(arguments,'');};\n" + + source + "return __p;\n"; + + try { + render = new Function(settings.variable || 'obj', '_', source); + } catch (e) { + e.source = source; + throw e; + } + + if (data) return render(data, _); + var template = function(data) { + return render.call(this, data, _); + }; + + // Provide the compiled function source as a convenience for precompilation. + template.source = 'function(' + (settings.variable || 'obj') + '){\n' + source + '}'; + + return template; + }; + + // Add a "chain" function, which will delegate to the wrapper. + _.chain = function(obj) { + return _(obj).chain(); + }; + + // OOP + // --------------- + // If Underscore is called as a function, it returns a wrapped object that + // can be used OO-style. This wrapper holds altered versions of all the + // underscore functions. Wrapped objects may be chained. + + // Helper function to continue chaining intermediate results. + var result = function(obj) { + return this._chain ? _(obj).chain() : obj; + }; + + // Add all of the Underscore functions to the wrapper object. + _.mixin(_); + + // Add all mutator Array functions to the wrapper. + each(['pop', 'push', 'reverse', 'shift', 'sort', 'splice', 'unshift'], function(name) { + var method = ArrayProto[name]; + _.prototype[name] = function() { + var obj = this._wrapped; + method.apply(obj, arguments); + if ((name == 'shift' || name == 'splice') && obj.length === 0) delete obj[0]; + return result.call(this, obj); + }; + }); + + // Add all accessor Array functions to the wrapper. + each(['concat', 'join', 'slice'], function(name) { + var method = ArrayProto[name]; + _.prototype[name] = function() { + return result.call(this, method.apply(this._wrapped, arguments)); + }; + }); + + _.extend(_.prototype, { + + // Start chaining a wrapped Underscore object. + chain: function() { + this._chain = true; + return this; + }, + + // Extracts the result from a wrapped and chained object. + value: function() { + return this._wrapped; + } + + }); + + // AMD registration happens at the end for compatibility with AMD loaders + // that may not enforce next-turn semantics on modules. Even though general + // practice for AMD registration is to be anonymous, underscore registers + // as a named module because, like jQuery, it is a base library that is + // popular enough to be bundled in a third party lib, but not be part of + // an AMD load request. Those cases could generate an error when an + // anonymous define() is called outside of a loader request. + if (typeof define === 'function' && define.amd) { + define('underscore', [], function() { + return _; + }); + } +}).call(this); + +/* +Ring.js + +Copyright (c) 2013, Nicolas Vanhoren + +Released under the MIT license + +Permission is hereby granted, free of charge, to any person obtaining a copy of +this software and associated documentation files (the "Software"), to deal in +the Software without restriction, including without limitation the rights to use, +copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the +Software, and to permit persons to whom the Software is furnished to do so, +subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS +FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR +COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN +AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION +WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. +*/ + +(function() { +/* jshint es3: true, proto: true */ +"use strict"; + +if (typeof(exports) !== "undefined") { // nodejs + var underscore = require("underscore"); + underscore.extend(exports, declare(underscore)); +} else if (typeof(define) !== "undefined") { // amd + define(["underscore"], declare); +} else { // define global variable + window.ring = declare(_); +} + + +function declare(_) { + var ring = {}; + + function RingObject() {} + /** + ring.Object + + The base class of all other classes. It doesn't have much uses except + testing testing if an object uses the Ring.js class system using + ring.instance(x, ring.Object) + */ + ring.Object = RingObject; + _.extend(ring.Object, { + __mro__: [ring.Object], + __properties__: {__ringConstructor__: function() {}}, + __classId__: 1, + __parents__: [], + __classIndex__: {"1": ring.Object} + }); + _.extend(ring.Object.prototype, { + __ringConstructor__: ring.Object.__properties__.__ringConstructor__ + }); + + // utility function to have Object.create on all browsers + var objectCreate = function(o) { + function CreatedObject(){} + CreatedObject.prototype = o; + var tmp = new CreatedObject(); + tmp.__proto__ = o; + return tmp; + }; + ring.__objectCreate = objectCreate; + + var classCounter = 3; + var fnTest = /xyz/.test(function(){xyz();}) ? /\$super\b/ : /.*/; + + /** + ring.create([parents,] properties) + + Creates a new class and returns it. + + properties is a dictionary of the methods and attributes that should + be added to the new class' prototype. + + parents is a list of the classes this new class should extend. If not + specified or an empty list is specified this class will inherit from one + class: ring.Object. + */ + ring.create = function() { + // arguments parsing + var args = _.toArray(arguments); + args.reverse(); + var props = args[0]; + var parents = args.length >= 2 ? args[1] : []; + if (! (parents instanceof Array)) + parents = [parents]; + _.each(parents, function(el) { + toRingClass(el); + }); + if (parents.length === 0) + parents = [ring.Object]; + // constructor handling + var cons = props.constructor !== Object ? props.constructor : undefined; + props = _.clone(props); + delete props.constructor; + if (cons) + props.__ringConstructor__ = cons; + else { //retro compatibility + cons = props.init; + delete props.init; + if (cons) + props.__ringConstructor__ = cons; + } + // create real class + var claz = function Instance() { + this.$super = null; + this.__ringConstructor__.apply(this, arguments); + }; + claz.__properties__ = props; + // mro creation + var toMerge = _.pluck(parents, "__mro__"); + toMerge = toMerge.concat([parents]); + var __mro__ = [claz].concat(mergeMro(toMerge)); + //generate prototype + var prototype = Object.prototype; + _.each(_.clone(__mro__).reverse(), function(claz) { + var current = objectCreate(prototype); + _.extend(current, claz.__properties__); + _.each(_.keys(current), function(key) { + var p = current[key]; + if (typeof p !== "function" || ! fnTest.test(p) || + (key !== "__ringConstructor__" && claz.__ringConvertedObject__)) + return; + current[key] = (function(name, fct, supProto) { + return function() { + var tmp = this.$super; + this.$super = supProto[name]; + try { + return fct.apply(this, arguments); + } finally { + this.$super = tmp; + } + }; + })(key, p, prototype); + }); + current.constructor = claz; + prototype = current; + }); + // remaining operations + var id = classCounter++; + claz.__mro__ = __mro__; + claz.__parents__ = parents; + claz.prototype = prototype; + claz.__classId__ = id; + // construct classes index + claz.__classIndex__ = {}; + _.each(claz.__mro__, function(c) { + claz.__classIndex__[c.__classId__] = c; + }); + // class init + if (claz.prototype.classInit) { + claz.__classInit__ = claz.prototype.classInit; + delete claz.prototype.classInit; + } + _.each(_.chain(claz.__mro__).clone().reverse().value(), function(c) { + if (c.__classInit__) { + var ret = c.__classInit__(claz.prototype); + if (ret !== undefined) + claz.prototype = ret; + } + }); + + return claz; + }; + + var mergeMro = function(toMerge) { + /* jshint loopfunc:true */ + // C3 merge() implementation + var __mro__ = []; + var current = _.clone(toMerge); + while (true) { + var found = false; + for (var i=0; i < current.length; i++) { + if (current[i].length === 0) + continue; + var currentClass = current[i][0]; + var isInTail = _.find(current, function(lst) { + return _.contains(_.rest(lst), currentClass); + }); + if (! isInTail) { + found = true; + __mro__.push(currentClass); + current = _.map(current, function(lst) { + if (_.head(lst) === currentClass) + return _.rest(lst); + else + return lst; + }); + break; + } + } + if (found) + continue; + if (_.all(current, function(i) { return i.length === 0; })) + return __mro__; + throw new ring.ValueError("Cannot create a consistent method resolution order (MRO)"); + } + }; + + /** + Convert an existing class to be used with the ring.js class system. + */ + var toRingClass = function(claz) { + if (claz.__classId__) + return; + var proto = ! Object.getOwnPropertyNames ? claz.prototype : (function() { + var keys = {}; + (function crawl(p) { + if (p === Object.prototype) + return; + _.extend(keys, _.chain(Object.getOwnPropertyNames(p)) + .map(function(el) {return [el, true];}) + .object().value()); + crawl(Object.getPrototypeOf(p)); + })(claz.prototype); + return _.object(_.map(_.keys(keys), function(k) {return [k, claz.prototype[k]];})); + })(); + proto = _.chain(proto).map(function(v, k) { return [k, v]; }) + .filter(function(el) {return el[0] !== "constructor" && el[0] !== "__proto__";}) + .object().value(); + var id = classCounter++; + _.extend(claz, { + __mro__: [claz, ring.Object], + __properties__: _.extend({}, proto, { + __ringConstructor__: function() { + this.$super.apply(this, arguments); + var tmp = this.$super; + this.$super = null; + try { + claz.apply(this, arguments); + } finally { + this.$super = tmp; + } + } + }), + __classId__: id, + __parents__: [ring.Object], + __classIndex__: {"1": ring.Object}, + __ringConvertedObject__: true + }); + claz.__classIndex__[id] = claz; + }; + + /** + ring.instance(obj, type) + + Returns true if obj is an instance of type or an instance of a sub-class of type. + + It is necessary to use this method instead of instanceof when using the Ring.js class + system because instanceof will not be able to detect sub-classes. + + If used with obj or type that do not use the Ring.js class system this method will + use instanceof instead. So it should be safe to replace all usages of instanceof + by ring.instance() in any program, whether or not it uses Ring.js. + + Additionaly this method allows to test the type of simple JavaScript types like strings. + To do so, pass a string instead of a type as second argument. Examples: + + ring.instance("", "string") // returns true + ring.instance(function() {}, "function") // returns true + ring.instance({}, "object") // returns true + ring.instance(1, "number") // returns true + */ + ring.instance = function(obj, type) { + if (obj !== null && typeof(obj) === "object" && obj.constructor && obj.constructor.__classIndex__ && + typeof(type) === "function" && typeof(type.__classId__) === "number") { + return obj.constructor.__classIndex__[type.__classId__] !== undefined; + } + if (typeof(type) === "string") + return typeof(obj) === type; + return obj instanceof type; + }; + + /** + A class to easily create new classes representing exceptions. This class is special + because it is a sub-class of the standard Error class of JavaScript. Examples: + + ring.instance(e, Error) + + e instanceof Error + + This two expressions will always be true if e is an instance of ring.Error or any + sub-class of ring.Error. + + */ + ring.Error = ring.create({ + /** + The name attribute is used in the default implementation of the toString() method + of the standard JavaScript Error class. According to the standard, all sub-classes + of Error should define a new name. + */ + name: "ring.Error", + /** + A default message to use in instances of this class if there is no arguments given + to the constructor. + */ + defaultMessage: "", + /** + Constructor arguments: + + message: The message to put in the instance. If there is no message specified, the + message will be this.defaultMessage. + */ + constructor: function(message) { + this.message = message || this.defaultMessage; + }, + classInit: function(prototype) { + // some black magic to reconstitute a complete prototype chain + // with Error at the end + var protos = []; + var gather = function(proto) { + if (! proto) + return; + protos.push(proto); + gather(proto.__proto__); + }; + gather(prototype); + var current = new Error(); + _.each(_.clone(protos).reverse(), function(proto) { + var tmp = objectCreate(current); + // using _.each to avoid traversing prototypes + _.each(proto, function(v, k) { + if (k !== "__proto__") + tmp[k] = v; + }); + current = tmp; + }); + return current; + } + }); + + /** + A type of exception to inform that a method received an argument with an incorrect value. + */ + ring.ValueError = ring.create([ring.Error], { + name: "ring.ValueError" + }); + + /** + This method allows to find the super of a method when that method has been re-defined + in a child class. + + Contrary to this.$super(), this function allows to find a super method in another method + than the re-defining one. Example: + + var A = ring.create({ + fctA: function() {...}; + }); + + var B = ring.create([A], { + fctA: function() {...}; + fctB: function() { + ring.getSuper(B, this, "fctA")(); // here we call the original fctA() method + // as it was defined in the A class + }; + }); + + This method is much slower than this.$super(), so this.$super() should always be + preferred when it is possible to use it. + + Arguments: + + * currentClass: The current class. It is necessary to specify it for this function + to work properly. + * obj: The current object (this in most cases). + * attributeName: The name of the desired attribute as it appeared in the base class. + + Returns the attribute as it was defined in the base class. If that attribute is a function, + it will be binded to obj. + */ + ring.getSuper = function(currentClass, obj, attributeName) { + var pos; + var __mro__ = obj.constructor.__mro__; + for (var i = 0; i < __mro__.length; i++) { + if (__mro__[i] === currentClass) { + pos = i; + break; + } + } + if (pos === undefined) + throw new ring.ValueError("Class not found in instance's method resolution order."); + var find = function(proto, counter) { + if (counter === 0) + return proto; + return find(proto.__proto__, counter - 1); + }; + var proto = find(obj.constructor.prototype, pos + 1); + var att; + if (attributeName !== "constructor" && attributeName !== "init") // retro compatibility + att = proto[attributeName]; + else + att = proto.__ringConstructor__; + if (ring.instance(att, "function")) + return _.bind(att, obj); + else + return att; + }; + + return ring; +} +})(); + +/** + * @fileoverview JSJaC Jingle library - Header + * * @url https://github.com/valeriansaliou/jsjac-jingle * @depends https://github.com/sstrigler/JSJaC - * @author Valérian Saliou http://valeriansaliou.name/ + * @author Valérian Saliou https://valeriansaliou.name/ * @license Mozilla Public License v2.0 (MPL v2.0) */ +/** @module jsjac-jingle/header */ + + /** * Implements: * @@ -36,27 +1806,78 @@ * 4.snd Local user sends a session-terminate type='set' * 4.hdl Remote user sends back a type='result' to '4.snd' stanza (ack) */ +/** + * @fileoverview JSJaC Jingle library - Constants map + * + * @url https://github.com/valeriansaliou/jsjac-jingle + * @depends https://github.com/sstrigler/JSJaC + * @author Valérian Saliou https://valeriansaliou.name/ + * @license Mozilla Public License v2.0 (MPL v2.0) + */ + + +/** @module jsjac-jingle/constants */ /** * JINGLE WEBRTC */ +/** + * @constant + * @global + * @type {Function} + * @readonly + * @default + * @public + */ var WEBRTC_GET_MEDIA = ( navigator.webkitGetUserMedia || navigator.mozGetUserMedia || navigator.msGetUserMedia || navigator.getUserMedia ); +/** + * @constant + * @global + * @type {Function} + * @readonly + * @default + * @public + */ var WEBRTC_PEER_CONNECTION = ( window.webkitRTCPeerConnection || window.mozRTCPeerConnection || window.RTCPeerConnection ); +/** + * @constant + * @global + * @type {Function} + * @readonly + * @default + * @public + */ var WEBRTC_SESSION_DESCRIPTION = ( window.mozRTCSessionDescription || window.RTCSessionDescription ); +/** + * @constant + * @global + * @type {Function} + * @readonly + * @default + * @public + */ var WEBRTC_ICE_CANDIDATE = ( window.mozRTCIceCandidate || window.RTCIceCandidate ); +/** + * @constant + * @global + * @type {Object} + * @readonly + * @default + * @public + */ var WEBRTC_CONFIGURATION = { peer_connection : { config : { @@ -87,12 +1908,54 @@ var WEBRTC_CONFIGURATION = { } }; +/** + * @constant + * @global + * @type {String} + * @readonly + * @default + * @public + */ var WEBRTC_SDP_LINE_BREAK = '\r\n'; + +/** + * @constant + * @global + * @type {String} + * @readonly + * @default + * @public + */ var WEBRTC_SDP_TYPE_OFFER = 'offer'; + +/** + * @constant + * @global + * @type {String} + * @readonly + * @default + * @public + */ var WEBRTC_SDP_TYPE_ANSWER = 'answer'; -var R_WEBRTC_SDP_ICE_CANDIDATE = /^a=candidate:(\w{1,32}) (\d{1,5}) (udp|tcp) (\d{1,10}) ([a-zA-Z0-9:\.]{1,45}) (\d{1,5}) (typ) (host|srflx|prflx|relay)( (raddr) ([a-zA-Z0-9:\.]{1,45}) (rport) (\d{1,5}))?( (generation) (\d))?/i; +/** + * @constant + * @global + * @type {RegExp} + * @readonly + * @default + * @public + */ +var R_WEBRTC_SDP_CANDIDATE = /^a=candidate:(\w{1,32}) (\d{1,5}) (udp|tcp) (\d{1,10}) ([a-zA-Z0-9:\.]{1,45}) (\d{1,5}) (typ) (host|srflx|prflx|relay)( (raddr) ([a-zA-Z0-9:\.]{1,45}) (rport) (\d{1,5}))?( (generation) (\d))?/i; +/** + * @constant + * @global + * @type {Object} + * @readonly + * @default + * @public + */ var R_WEBRTC_SDP_ICE_PAYLOAD = { rtpmap : /^a=rtpmap:(\d+) (([^\s\/]+)\/(\d+)(\/([^\s\/]+))?)?/i, fmtp : /^a=fmtp:(\d+) (.+)/i, @@ -103,7 +1966,8 @@ var R_WEBRTC_SDP_ICE_PAYLOAD = { ufrag : /^a=ice-ufrag:(\S+)/i, ptime : /^a=ptime:(\d+)/i, maxptime : /^a=maxptime:(\d+)/i, - ssrc : /^a=ssrc:(\d+) (\w+)(:(\S+))?( (\S+))?/i, + ssrc : /^a=ssrc:(\d+) (\w+)(:(.+))?/i, + ssrc_group : /^a=ssrc-group:(\S+) ([\d ]+)/i, rtcp_mux : /^a=rtcp-mux/i, crypto : /^a=crypto:(\d{1,9}) (\S+) (\S+)( (\S+))?/i, zrtp_hash : /^a=zrtp-hash:(\S+) (\S+)/i, @@ -114,41 +1978,401 @@ var R_WEBRTC_SDP_ICE_PAYLOAD = { media : /^m=(audio|video|application|data) /i }; +/** + * @constant + * @global + * @type {Object} + * @readonly + * @default + * @public + */ +var R_NETWORK_PROTOCOLS = { + stun: /^stun:/i +}; + +/** + * @constant + * @global + * @type {Object} + * @readonly + * @default + * @public + */ +var R_NETWORK_IP = { + all: { + v4: /((25[0-5]|(2[0-4]|1{0,1}[0-9]){0,1}[0-9])\.){3,3}(25[0-5]|(2[0-4]|1{0,1}[0-9]){0,1}[0-9])/, + v6: /^((?=.*::)(?!.*::.+::)(::)?([\dA-F]{1,4}:(:|\b)|){5}|([\dA-F]{1,4}:){6})((([\dA-F]{1,4}((?!\3)::|:\b|$))|(?!\2\3)){2}|(((2[0-4]|1\d|[1-9])?\d|25[0-5])\.?\b){4})$/i + }, + + lan: { + v4: /(^127\.0\.0\.1)|(^10\.)|(^172\.1[6-9]\.)|(^172\.2[0-9]\.)|(^172\.3[0-1]\.)|(^192\.168\.)/, + v6: /((::1)|(^fe80::))(.+)?/i + } +}; + +/** + * @constant + * @global + * @type {RegExp} + * @readonly + * @default + * @public + */ +var R_JSJAC_JINGLE_SERVICE_URI = /^(\w+):([^:\?]+)(?::(\d+))?(?:\?transport=(\w+))?/i; /** * JINGLE NAMESPACES */ +/** + * @constant + * @global + * @type {String} + * @readonly + * @default + * @public + */ var NS_JINGLE = 'urn:xmpp:jingle:1'; + +/** + * @constant + * @global + * @type {String} + * @readonly + * @default + * @public + */ var NS_JINGLE_ERRORS = 'urn:xmpp:jingle:errors:1'; +/** + * @constant + * @global + * @type {String} + * @readonly + * @default + * @public + */ var NS_JINGLE_APPS_RTP = 'urn:xmpp:jingle:apps:rtp:1'; + +/** + * @constant + * @global + * @type {String} + * @readonly + * @default + * @public + */ var NS_JINGLE_APPS_RTP_INFO = 'urn:xmpp:jingle:apps:rtp:info:1'; + +/** + * @constant + * @global + * @type {String} + * @readonly + * @default + * @public + */ var NS_JINGLE_APPS_RTP_AUDIO = 'urn:xmpp:jingle:apps:rtp:audio'; + +/** + * @constant + * @global + * @type {String} + * @readonly + * @default + * @public + */ var NS_JINGLE_APPS_RTP_VIDEO = 'urn:xmpp:jingle:apps:rtp:video'; + +/** + * @constant + * @global + * @type {String} + * @readonly + * @default + * @public + */ var NS_JINGLE_APPS_RTP_RTP_HDREXT = 'urn:xmpp:jingle:apps:rtp:rtp-hdrext:0'; + +/** + * @constant + * @global + * @type {String} + * @readonly + * @default + * @public + */ var NS_JINGLE_APPS_RTP_RTCP_FB = 'urn:xmpp:jingle:apps:rtp:rtcp-fb:0'; + +/** + * @constant + * @global + * @type {String} + * @readonly + * @default + * @public + */ var NS_JINGLE_APPS_RTP_ZRTP = 'urn:xmpp:jingle:apps:rtp:zrtp:1'; + +/** + * @constant + * @global + * @type {String} + * @readonly + * @default + * @public + */ var NS_JINGLE_APPS_RTP_SSMA = 'urn:xmpp:jingle:apps:rtp:ssma:0'; + +/** + * @constant + * @global + * @type {String} + * @readonly + * @default + * @public + */ var NS_JINGLE_APPS_STUB = 'urn:xmpp:jingle:apps:stub:0'; + +/** + * @constant + * @global + * @type {String} + * @readonly + * @default + * @public + */ var NS_JINGLE_APPS_DTLS = 'urn:xmpp:tmp:jingle:apps:dtls:0'; + +/** + * @constant + * @global + * @type {String} + * @readonly + * @default + * @public + */ var NS_JINGLE_APPS_GROUPING = 'urn:xmpp:jingle:apps:grouping:0'; +/** + * @constant + * @global + * @type {String} + * @readonly + * @default + * @public + */ +var NS_JINGLE_TRANSPORTS_RAWUDP = 'urn:xmpp:jingle:transports:raw-udp:1'; + +/** + * @constant + * @global + * @type {String} + * @readonly + * @default + * @public + */ var NS_JINGLE_TRANSPORTS_ICEUDP = 'urn:xmpp:jingle:transports:ice-udp:1'; + +/** + * @constant + * @global + * @type {String} + * @readonly + * @default + * @public + */ var NS_JINGLE_TRANSPORTS_STUB = 'urn:xmpp:jingle:transports:stub:0'; +/** + * @constant + * @global + * @type {String} + * @readonly + * @default + * @public + */ var NS_JINGLE_SECURITY_STUB = 'urn:xmpp:jingle:security:stub:0'; +/** + * @constant + * @global + * @type {String} + * @readonly + * @default + * @public + */ +var NS_JABBER_JINGLENODES = 'http://jabber.org/protocol/jinglenodes'; + +/** + * @constant + * @global + * @type {String} + * @readonly + * @default + * @public + */ +var NS_JABBER_JINGLENODES_CHANNEL = 'http://jabber.org/protocol/jinglenodes#channel'; + +/** + * @constant + * @global + * @type {String} + * @readonly + * @default + * @public + */ +var NS_JABBER_MUC = 'http://jabber.org/protocol/muc'; + +/** + * @constant + * @global + * @type {String} + * @readonly + * @default + * @public + */ +var NS_JABBER_MUC_OWNER = 'http://jabber.org/protocol/muc#owner'; + +/** + * @constant + * @global + * @type {String} + * @readonly + * @default + * @public + */ +var NS_JABBER_MUC_ROOMCONFIG = 'http://jabber.org/protocol/muc#roomconfig'; + +/** + * @constant + * @global + * @type {String} + * @readonly + * @default + * @public + */ +var NS_JABBER_MUC_USER = 'http://jabber.org/protocol/muc#user'; + +/** + * @constant + * @global + * @type {String} + * @readonly + * @default + * @public + */ +var NS_JABBER_CONFERENCE = 'jabber:x:conference'; + +/** + * @constant + * @global + * @type {String} + * @readonly + * @default + * @public + */ +var NS_JABBER_DATA = 'jabber:x:data'; + +/** + * @constant + * @global + * @type {String} + * @readonly + * @default + * @public + */ +var NS_MUJI = 'urn:xmpp:muji:tmp'; + +/** + * @constant + * @global + * @type {String} + * @readonly + * @default + * @public + */ +var NS_MUJI_INVITE = 'urn:xmpp:muji:invite:tmp'; + +/** + * @constant + * @global + * @type {String} + * @readonly + * @default + * @public + */ var NS_EXTDISCO = 'urn:xmpp:extdisco:1'; +/** + * @constant + * @global + * @type {String} + * @readonly + * @default + * @public + */ +var NS_IETF_XMPP_STANZAS = 'urn:ietf:params:xml:ns:xmpp-stanzas'; + +/** + * @constant + * @global + * @type {String} + * @readonly + * @default + * @public + */ var NS_IETF_RFC_3264 = 'urn:ietf:rfc:3264'; + +/** + * @constant + * @global + * @type {String} + * @readonly + * @default + * @public + */ var NS_IETF_RFC_5576 = 'urn:ietf:rfc:5576'; + +/** + * @constant + * @global + * @type {String} + * @readonly + * @default + * @public + */ var NS_IETF_RFC_5888 = 'urn:ietf:rfc:5888'; +/** + * @constant + * @global + * @type {RegExp} + * @readonly + * @default + * @public + */ var R_NS_JINGLE_APP = /^urn:xmpp:jingle:app:(\w+)(:(\w+))?(:(\d+))?$/; + +/** + * @constant + * @global + * @type {RegExp} + * @readonly + * @default + * @public + */ var R_NS_JINGLE_TRANSPORT = /^urn:xmpp:jingle:transport:(\w+)$/; +/** + * @constant + * @global + * @type {Array} + * @readonly + * @default + * @public + */ var MAP_DISCO_JINGLE = [ /* http://xmpp.org/extensions/xep-0166.html#support */ /* http://xmpp.org/extensions/xep-0167.html#support */ @@ -157,6 +2381,9 @@ var MAP_DISCO_JINGLE = [ NS_JINGLE_APPS_RTP_AUDIO, NS_JINGLE_APPS_RTP_VIDEO, + /* http://xmpp.org/extensions/xep-0177.html */ + NS_JINGLE_TRANSPORTS_RAWUDP, + /* http://xmpp.org/extensions/xep-0176.html#support */ NS_JINGLE_TRANSPORTS_ICEUDP, NS_IETF_RFC_3264, @@ -179,115 +2406,1585 @@ var MAP_DISCO_JINGLE = [ /* http://xmpp.org/extensions/xep-0262.html */ NS_JINGLE_APPS_RTP_ZRTP, + /* http://xmpp.org/extensions/xep-0278.html */ + NS_JABBER_JINGLENODES, + /* http://xmpp.org/extensions/xep-0215.html */ NS_EXTDISCO ]; +/** + * @constant + * @global + * @type {Array} + * @readonly + * @default + * @public + */ +var MAP_DISCO_MUJI = [ + /* http://xmpp.org/extensions/xep-0272.html */ + NS_MUJI, + + /* http://xmpp.org/extensions/xep-0272.html#inviting */ + NS_MUJI_INVITE, + + /* http://xmpp.org/extensions/xep-0249.html */ + NS_JABBER_CONFERENCE +]; + + /** * JSJAC JINGLE CONSTANTS */ +/** + * @constant + * @global + * @type {Boolean} + * @readonly + * @default + * @public + */ var JSJAC_JINGLE_AVAILABLE = WEBRTC_GET_MEDIA ? true : false; +/** + * @constant + * @global + * @type {String} + * @readonly + * @default + * @public + */ +var JSJAC_JINGLE_SESSION_SINGLE = 'single'; + +/** + * @constant + * @global + * @type {String} + * @readonly + * @default + * @public + */ +var JSJAC_JINGLE_SESSION_MUJI = 'muji'; + +/** + * @constant + * @global + * @type {Number} + * @readonly + * @default + * @public + */ var JSJAC_JINGLE_PEER_TIMEOUT_DEFAULT = 15; + +/** + * @constant + * @global + * @type {Number} + * @readonly + * @default + * @public + */ var JSJAC_JINGLE_PEER_TIMEOUT_DISCONNECT = 5; + +/** + * @constant + * @global + * @type {Number} + * @readonly + * @default + * @public + */ +var JSJAC_JINGLE_MEDIA_READYSTATE_UNINITIALIZED = 0; + +/** + * @constant + * @global + * @type {Number} + * @readonly + * @default + * @public + */ +var JSJAC_JINGLE_MEDIA_READYSTATE_LOADING = 1; + +/** + * @constant + * @global + * @type {Number} + * @readonly + * @default + * @public + */ +var JSJAC_JINGLE_MEDIA_READYSTATE_LOADED = 2; + +/** + * @constant + * @global + * @type {Number} + * @readonly + * @default + * @public + */ +var JSJAC_JINGLE_MEDIA_READYSTATE_INTERACTIVE = 3; + +/** + * @constant + * @global + * @type {Number} + * @readonly + * @default + * @public + */ +var JSJAC_JINGLE_MEDIA_READYSTATE_COMPLETED = 4; + +/** + * @constant + * @global + * @type {Number} + * @readonly + * @default + * @public + */ var JSJAC_JINGLE_STANZA_TIMEOUT = 10; + +/** + * @constant + * @global + * @type {String} + * @readonly + * @default + * @public + */ var JSJAC_JINGLE_STANZA_ID_PRE = 'jj'; +/** + * @constant + * @global + * @type {String} + * @readonly + * @default + * @public + */ var JSJAC_JINGLE_NETWORK = '0'; + +/** + * @constant + * @global + * @type {String} + * @readonly + * @default + * @public + */ var JSJAC_JINGLE_GENERATION = '0'; +/** + * @constant + * @global + * @type {String} + * @readonly + * @default + * @public + */ +var JSJAC_JINGLE_DIRECTION_LOCAL = 'local'; + +/** + * @constant + * @global + * @type {String} + * @readonly + * @default + * @public + */ +var JSJAC_JINGLE_DIRECTION_REMOTE = 'remote'; + +/** + * @constant + * @global + * @type {String} + * @readonly + * @default + * @public + */ var JSJAC_JINGLE_BROWSER_FIREFOX = 'Firefox'; + +/** + * @constant + * @global + * @type {String} + * @readonly + * @default + * @public + */ var JSJAC_JINGLE_BROWSER_CHROME = 'Chrome'; + +/** + * @constant + * @global + * @type {String} + * @readonly + * @default + * @public + */ var JSJAC_JINGLE_BROWSER_SAFARI = 'Safari'; + +/** + * @constant + * @global + * @type {String} + * @readonly + * @default + * @public + */ var JSJAC_JINGLE_BROWSER_OPERA = 'Opera'; + +/** + * @constant + * @global + * @type {String} + * @readonly + * @default + * @public + */ var JSJAC_JINGLE_BROWSER_IE = 'IE'; +/** + * @constant + * @global + * @type {Object} + * @readonly + * @default + * @public + */ var JSJAC_JINGLE_SENDERS_BOTH = { jingle: 'both', sdp: 'sendrecv' }; + +/** + * @constant + * @global + * @type {Object} + * @readonly + * @default + * @public + */ var JSJAC_JINGLE_SENDERS_INITIATOR = { jingle: 'initiator', sdp: 'sendonly' }; + +/** + * @constant + * @global + * @type {Object} + * @readonly + * @default + * @public + */ var JSJAC_JINGLE_SENDERS_NONE = { jingle: 'none', sdp: 'inactive' }; + +/** + * @constant + * @global + * @type {Object} + * @readonly + * @default + * @public + */ var JSJAC_JINGLE_SENDERS_RESPONDER = { jingle: 'responder', sdp: 'recvonly' }; +/** + * @constant + * @global + * @type {String} + * @readonly + * @default + * @public + */ var JSJAC_JINGLE_CREATOR_INITIATOR = 'initiator'; + +/** + * @constant + * @global + * @type {String} + * @readonly + * @default + * @public + */ var JSJAC_JINGLE_CREATOR_RESPONDER = 'responder'; +/** + * @constant + * @global + * @type {String} + * @readonly + * @default + * @public + */ var JSJAC_JINGLE_STATUS_INACTIVE = 'inactive'; + +/** + * @constant + * @global + * @type {String} + * @readonly + * @default + * @public + */ var JSJAC_JINGLE_STATUS_INITIATING = 'initiating'; + +/** + * @constant + * @global + * @type {String} + * @readonly + * @default + * @public + */ var JSJAC_JINGLE_STATUS_INITIATED = 'initiated'; + +/** + * @constant + * @global + * @type {String} + * @readonly + * @default + * @public + */ var JSJAC_JINGLE_STATUS_ACCEPTING = 'accepting'; + +/** + * @constant + * @global + * @type {String} + * @readonly + * @default + * @public + */ var JSJAC_JINGLE_STATUS_ACCEPTED = 'accepted'; + +/** + * @constant + * @global + * @type {String} + * @readonly + * @default + * @public + */ var JSJAC_JINGLE_STATUS_TERMINATING = 'terminating'; + +/** + * @constant + * @global + * @type {String} + * @readonly + * @default + * @public + */ var JSJAC_JINGLE_STATUS_TERMINATED = 'terminated'; +/** + * @constant + * @global + * @type {String} + * @readonly + * @default + * @public + */ var JSJAC_JINGLE_ACTION_CONTENT_ACCEPT = 'content-accept'; + +/** + * @constant + * @global + * @type {String} + * @readonly + * @default + * @public + */ var JSJAC_JINGLE_ACTION_CONTENT_ADD = 'content-add'; + +/** + * @constant + * @global + * @type {String} + * @readonly + * @default + * @public + */ var JSJAC_JINGLE_ACTION_CONTENT_MODIFY = 'content-modify'; + +/** + * @constant + * @global + * @type {String} + * @readonly + * @default + * @public + */ var JSJAC_JINGLE_ACTION_CONTENT_REJECT = 'content-reject'; + +/** + * @constant + * @global + * @type {String} + * @readonly + * @default + * @public + */ var JSJAC_JINGLE_ACTION_CONTENT_REMOVE = 'content-remove'; + +/** + * @constant + * @global + * @type {String} + * @readonly + * @default + * @public + */ var JSJAC_JINGLE_ACTION_DESCRIPTION_INFO = 'description-info'; + +/** + * @constant + * @global + * @type {String} + * @readonly + * @default + * @public + */ var JSJAC_JINGLE_ACTION_SECURITY_INFO = 'security-info'; + +/** + * @constant + * @global + * @type {String} + * @readonly + * @default + * @public + */ var JSJAC_JINGLE_ACTION_SESSION_ACCEPT = 'session-accept'; + +/** + * @constant + * @global + * @type {String} + * @readonly + * @default + * @public + */ var JSJAC_JINGLE_ACTION_SESSION_INFO = 'session-info'; + +/** + * @constant + * @global + * @type {String} + * @readonly + * @default + * @public + */ var JSJAC_JINGLE_ACTION_SESSION_INITIATE = 'session-initiate'; + +/** + * @constant + * @global + * @type {String} + * @readonly + * @default + * @public + */ var JSJAC_JINGLE_ACTION_SESSION_TERMINATE = 'session-terminate'; + +/** + * @constant + * @global + * @type {String} + * @readonly + * @default + * @public + */ var JSJAC_JINGLE_ACTION_TRANSPORT_ACCEPT = 'transport-accept'; + +/** + * @constant + * @global + * @type {String} + * @readonly + * @default + * @public + */ var JSJAC_JINGLE_ACTION_TRANSPORT_INFO = 'transport-info'; + +/** + * @constant + * @global + * @type {String} + * @readonly + * @default + * @public + */ var JSJAC_JINGLE_ACTION_TRANSPORT_REJECT = 'transport-reject'; + +/** + * @constant + * @global + * @type {String} + * @readonly + * @default + * @public + */ var JSJAC_JINGLE_ACTION_TRANSPORT_REPLACE = 'transport-replace'; +/** + * @constant + * @global + * @type {Object} + * @readonly + * @default + * @public + */ var JSJAC_JINGLE_ERROR_OUT_OF_ORDER = { jingle: 'out-of-order', xmpp: 'unexpected-request', type: 'wait' }; + +/** + * @constant + * @global + * @type {Object} + * @readonly + * @default + * @public + */ var JSJAC_JINGLE_ERROR_TIE_BREAK = { jingle: 'tie-break', xmpp: 'conflict', type: 'cancel' }; + +/** + * @constant + * @global + * @type {Object} + * @readonly + * @default + * @public + */ var JSJAC_JINGLE_ERROR_UNKNOWN_SESSION = { jingle: 'unknown-session', xmpp: 'item-not-found', type: 'cancel' }; + +/** + * @constant + * @global + * @type {Object} + * @readonly + * @default + * @public + */ var JSJAC_JINGLE_ERROR_UNSUPPORTED_INFO = { jingle: 'unsupported-info', xmpp: 'feature-not-implemented', type: 'modify' }; + +/** + * @constant + * @global + * @type {Object} + * @readonly + * @default + * @public + */ var JSJAC_JINGLE_ERROR_SECURITY_REQUIRED = { jingle: 'security-required', xmpp: 'not-acceptable', type: 'cancel' }; +/** + * @constant + * @global + * @type {Object} + * @readonly + * @default + * @public + */ var XMPP_ERROR_UNEXPECTED_REQUEST = { xmpp: 'unexpected-request', type: 'wait' }; + +/** + * @constant + * @global + * @type {Object} + * @readonly + * @default + * @public + */ var XMPP_ERROR_CONFLICT = { xmpp: 'conflict', type: 'cancel' }; + +/** + * @constant + * @global + * @type {Object} + * @readonly + * @default + * @public + */ var XMPP_ERROR_ITEM_NOT_FOUND = { xmpp: 'item-not-found', type: 'cancel' }; + +/** + * @constant + * @global + * @type {Object} + * @readonly + * @default + * @public + */ var XMPP_ERROR_NOT_ACCEPTABLE = { xmpp: 'not-acceptable', type: 'modify' }; + +/** + * @constant + * @global + * @type {Object} + * @readonly + * @default + * @public + */ +var XMPP_ERROR_NOT_AUTHORIZED = { xmpp: 'not-authorized', type: 'auth' }; + +/** + * @constant + * @global + * @type {Object} + * @readonly + * @default + * @public + */ var XMPP_ERROR_FEATURE_NOT_IMPLEMENTED = { xmpp: 'feature-not-implemented', type: 'cancel' }; + +/** + * @constant + * @global + * @type {Object} + * @readonly + * @default + * @public + */ var XMPP_ERROR_SERVICE_UNAVAILABLE = { xmpp: 'service-unavailable', type: 'cancel' }; + +/** + * @constant + * @global + * @type {Object} + * @readonly + * @default + * @public + */ var XMPP_ERROR_REDIRECT = { xmpp: 'redirect', type: 'modify' }; + +/** + * @constant + * @global + * @type {Object} + * @readonly + * @default + * @public + */ var XMPP_ERROR_RESOURCE_CONSTRAINT = { xmpp: 'resource-constraint', type: 'wait' }; + +/** + * @constant + * @global + * @type {Object} + * @readonly + * @default + * @public + */ var XMPP_ERROR_BAD_REQUEST = { xmpp: 'bad-request', type: 'cancel' }; + +/** + * @constant + * @global + * @type {String} + * @readonly + * @default + * @public + */ var JSJAC_JINGLE_REASON_ALTERNATIVE_SESSION = 'alternative-session'; + +/** + * @constant + * @global + * @type {String} + * @readonly + * @default + * @public + */ var JSJAC_JINGLE_REASON_BUSY = 'busy'; + +/** + * @constant + * @global + * @type {String} + * @readonly + * @default + * @public + */ var JSJAC_JINGLE_REASON_CANCEL = 'cancel'; + +/** + * @constant + * @global + * @type {String} + * @readonly + * @default + * @public + */ var JSJAC_JINGLE_REASON_CONNECTIVITY_ERROR = 'connectivity-error'; + +/** + * @constant + * @global + * @type {String} + * @readonly + * @default + * @public + */ var JSJAC_JINGLE_REASON_DECLINE = 'decline'; + +/** + * @constant + * @global + * @type {String} + * @readonly + * @default + * @public + */ var JSJAC_JINGLE_REASON_EXPIRED = 'expired'; + +/** + * @constant + * @global + * @type {String} + * @readonly + * @default + * @public + */ var JSJAC_JINGLE_REASON_FAILED_APPLICATION = 'failed-application'; + +/** + * @constant + * @global + * @type {String} + * @readonly + * @default + * @public + */ var JSJAC_JINGLE_REASON_FAILED_TRANSPORT = 'failed-transport'; + +/** + * @constant + * @global + * @type {String} + * @readonly + * @default + * @public + */ var JSJAC_JINGLE_REASON_GENERAL_ERROR = 'general-error'; + +/** + * @constant + * @global + * @type {String} + * @readonly + * @default + * @public + */ var JSJAC_JINGLE_REASON_GONE = 'gone'; + +/** + * @constant + * @global + * @type {String} + * @readonly + * @default + * @public + */ var JSJAC_JINGLE_REASON_INCOMPATIBLE_PARAMETERS = 'incompatible-parameters'; + +/** + * @constant + * @global + * @type {String} + * @readonly + * @default + * @public + */ var JSJAC_JINGLE_REASON_MEDIA_ERROR = 'media-error'; + +/** + * @constant + * @global + * @type {String} + * @readonly + * @default + * @public + */ var JSJAC_JINGLE_REASON_SECURITY_ERROR = 'security-error'; + +/** + * @constant + * @global + * @type {String} + * @readonly + * @default + * @public + */ var JSJAC_JINGLE_REASON_SUCCESS = 'success'; + +/** + * @constant + * @global + * @type {String} + * @readonly + * @default + * @public + */ var JSJAC_JINGLE_REASON_TIMEOUT = 'timeout'; + +/** + * @constant + * @global + * @type {String} + * @readonly + * @default + * @public + */ var JSJAC_JINGLE_REASON_UNSUPPORTED_APPLICATIONS = 'unsupported-applications'; + +/** + * @constant + * @global + * @type {String} + * @readonly + * @default + * @public + */ var JSJAC_JINGLE_REASON_UNSUPPORTED_TRANSPORTS = 'unsupported-transports'; +/** + * @constant + * @global + * @type {String} + * @readonly + * @default + * @public + */ var JSJAC_JINGLE_SESSION_INFO_ACTIVE = 'active'; + +/** + * @constant + * @global + * @type {String} + * @readonly + * @default + * @public + */ var JSJAC_JINGLE_SESSION_INFO_HOLD = 'hold'; + +/** + * @constant + * @global + * @type {String} + * @readonly + * @default + * @public + */ var JSJAC_JINGLE_SESSION_INFO_MUTE = 'mute'; + +/** + * @constant + * @global + * @type {String} + * @readonly + * @default + * @public + */ var JSJAC_JINGLE_SESSION_INFO_RINGING = 'ringing'; + +/** + * @constant + * @global + * @type {String} + * @readonly + * @default + * @public + */ var JSJAC_JINGLE_SESSION_INFO_UNHOLD = 'unhold'; + +/** + * @constant + * @global + * @type {String} + * @readonly + * @default + * @public + */ var JSJAC_JINGLE_SESSION_INFO_UNMUTE = 'unmute'; +/** + * @constant + * @global + * @type {String} + * @readonly + * @default + * @public + */ var JSJAC_JINGLE_MEDIA_AUDIO = 'audio'; + +/** + * @constant + * @global + * @type {String} + * @readonly + * @default + * @public + */ var JSJAC_JINGLE_MEDIA_VIDEO = 'video'; +/** + * @constant + * @global + * @type {String} + * @readonly + * @default + * @public + */ var JSJAC_JINGLE_VIDEO_SOURCE_CAMERA = 'camera'; + +/** + * @constant + * @global + * @type {String} + * @readonly + * @default + * @public + */ var JSJAC_JINGLE_VIDEO_SOURCE_SCREEN = 'screen'; -var JSJAC_JINGLE_STANZA_TYPE_ALL = 'all'; -var JSJAC_JINGLE_STANZA_TYPE_RESULT = 'result'; -var JSJAC_JINGLE_STANZA_TYPE_SET = 'set'; -var JSJAC_JINGLE_STANZA_TYPE_GET = 'get'; +/** + * @constant + * @global + * @type {String} + * @readonly + * @default + * @public + */ +var JSJAC_JINGLE_STANZA_IQ = 'iq'; + +/** + * @constant + * @global + * @type {String} + * @readonly + * @default + * @public + */ +var JSJAC_JINGLE_STANZA_MESSAGE = 'message'; + +/** + * @constant + * @global + * @type {String} + * @readonly + * @default + * @public + */ +var JSJAC_JINGLE_STANZA_PRESENCE = 'presence'; + +/** + * @constant + * @global + * @type {String} + * @readonly + * @default + * @public + */ +var JSJAC_JINGLE_MESSAGE_TYPE_ALL = 'all'; + +/** + * @constant + * @global + * @type {String} + * @readonly + * @default + * @public + */ +var JSJAC_JINGLE_MESSAGE_TYPE_NORMAL = 'normal'; + +/** + * @constant + * @global + * @type {String} + * @readonly + * @default + * @public + */ +var JSJAC_JINGLE_MESSAGE_TYPE_CHAT = 'chat'; + +/** + * @constant + * @global + * @type {String} + * @readonly + * @default + * @public + */ +var JSJAC_JINGLE_MESSAGE_TYPE_HEADLINE = 'headline'; + +/** + * @constant + * @global + * @type {String} + * @readonly + * @default + * @public + */ +var JSJAC_JINGLE_MESSAGE_TYPE_GROUPCHAT = 'groupchat'; + +/** + * @constant + * @global + * @type {String} + * @readonly + * @default + * @public + */ +var JSJAC_JINGLE_MESSAGE_TYPE_ERROR = 'error'; + +/** + * @constant + * @global + * @type {String} + * @readonly + * @default + * @public + */ +var JSJAC_JINGLE_PRESENCE_TYPE_ALL = 'all'; + +/** + * @constant + * @global + * @type {String} + * @readonly + * @default + * @public + */ +var JSJAC_JINGLE_PRESENCE_TYPE_AVAILABLE = 'available'; + +/** + * @constant + * @global + * @type {String} + * @readonly + * @default + * @public + */ +var JSJAC_JINGLE_PRESENCE_TYPE_UNAVAILABLE = 'unavailable'; + +/** + * @constant + * @global + * @type {String} + * @readonly + * @default + * @public + */ +var JSJAC_JINGLE_PRESENCE_TYPE_ERROR = 'error'; + +/** + * @constant + * @global + * @type {String} + * @readonly + * @default + * @public + */ +var JSJAC_JINGLE_IQ_TYPE_ALL = 'all'; + +/** + * @constant + * @global + * @type {String} + * @readonly + * @default + * @public + */ +var JSJAC_JINGLE_IQ_TYPE_RESULT = 'result'; + +/** + * @constant + * @global + * @type {String} + * @readonly + * @default + * @public + */ +var JSJAC_JINGLE_IQ_TYPE_SET = 'set'; + +/** + * @constant + * @global + * @type {String} + * @readonly + * @default + * @public + */ +var JSJAC_JINGLE_IQ_TYPE_GET = 'get'; + +/** + * @constant + * @global + * @type {String} + * @readonly + * @default + * @public + */ +var JSJAC_JINGLE_IQ_TYPE_ERROR = 'error'; + +/** + * @constant + * @global + * @type {String} + * @readonly + * @default + * @public + */ +var JSJAC_JINGLE_SDP_CANDIDATE_TYPE_HOST = 'host'; + +/** + * @constant + * @global + * @type {String} + * @readonly + * @default + * @public + */ +var JSJAC_JINGLE_SDP_CANDIDATE_TYPE_SRFLX = 'srflx'; + +/** + * @constant + * @global + * @type {String} + * @readonly + * @default + * @public + */ +var JSJAC_JINGLE_SDP_CANDIDATE_TYPE_PRFLX = 'prflx'; + +/** + * @constant + * @global + * @type {String} + * @readonly + * @default + * @public + */ +var JSJAC_JINGLE_SDP_CANDIDATE_TYPE_RELAY = 'relay'; + +/** + * @constant + * @global + * @type {String} + * @readonly + * @default + * @public + */ +var JSJAC_JINGLE_SDP_CANDIDATE_METHOD_ICE = 'ice'; + +/** + * @constant + * @global + * @type {String} + * @readonly + * @default + * @public + */ +var JSJAC_JINGLE_SDP_CANDIDATE_METHOD_RAW = 'raw'; + +/** + * @constant + * @global + * @type {Array} + * @readonly + * @default + * @public + */ +var JSJAC_JINGLE_SDP_CANDIDATE_MAP_ICEUDP = [ + { n: 'component', r: 1 }, + { n: 'foundation', r: 1 }, + { n: 'generation', r: 1 }, + { n: 'id', r: 1 }, + { n: 'ip', r: 1 }, + { n: 'network', r: 1 }, + { n: 'port', r: 1 }, + { n: 'priority', r: 1 }, + { n: 'protocol', r: 1 }, + { n: 'rel-addr', r: 0 }, + { n: 'rel-port', r: 0 }, + { n: 'type', r: 1 } +]; + +/** + * @constant + * @global + * @type {Array} + * @readonly + * @default + * @public + */ +var JSJAC_JINGLE_SDP_CANDIDATE_MAP_RAWUDP = [ + { n: 'component', r: 1 }, + { n: 'generation', r: 1 }, + { n: 'id', r: 1 }, + { n: 'ip', r: 1 }, + { n: 'port', r: 1 }, + { n: 'type', r: 1 } +]; + +/** + * @constant + * @global + * @type {String} + * @readonly + * @default + * @public + */ +var JSJAC_JINGLE_SDP_CANDIDATE_SCOPE_LOCAL = 'IN'; + +/** + * @constant + * @global + * @type {String} + * @readonly + * @default + * @public + */ +var JSJAC_JINGLE_SDP_CANDIDATE_SCOPE_REMOTE = 'IN'; + +/** + * @constant + * @global + * @type {String} + * @readonly + * @default + * @public + */ +var JSJAC_JINGLE_SDP_CANDIDATE_IPVERSION_V4 = 'IP4'; + +/** + * @constant + * @global + * @type {String} + * @readonly + * @default + * @public + */ +var JSJAC_JINGLE_SDP_CANDIDATE_IPVERSION_V6 = 'IP6'; + +/** + * @constant + * @global + * @type {String} + * @readonly + * @default + * @public + */ +var JSJAC_JINGLE_SDP_CANDIDATE_PROTOCOL_TCP = 'tcp'; + +/** + * @constant + * @global + * @type {String} + * @readonly + * @default + * @public + */ +var JSJAC_JINGLE_SDP_CANDIDATE_PROTOCOL_UDP = 'udp'; + +/** + * @constant + * @global + * @type {String} + * @readonly + * @default + * @public + */ +var JSJAC_JINGLE_SDP_CANDIDATE_IP_V4 = '0.0.0.0'; + +/** + * @constant + * @global + * @type {String} + * @readonly + * @default + * @public + */ +var JSJAC_JINGLE_SDP_CANDIDATE_IP_V6 = '::'; + +/** + * @constant + * @global + * @type {String} + * @readonly + * @default + * @public + */ +var JSJAC_JINGLE_SDP_CANDIDATE_IP_DEFAULT = JSJAC_JINGLE_SDP_CANDIDATE_IP_V4; + +/** + * @constant + * @global + * @type {String} + * @readonly + * @default + * @public + */ +var JSJAC_JINGLE_SDP_CANDIDATE_PORT_DEFAULT = '1'; + +/** + * @constant + * @global + * @type {String} + * @readonly + * @default + * @public + */ +var JSJAC_JINGLE_SDP_CANDIDATE_SCOPE_DEFAULT = JSJAC_JINGLE_SDP_CANDIDATE_SCOPE_REMOTE; + +/** + * @constant + * @global + * @type {String} + * @readonly + * @default + * @public + */ +var JSJAC_JINGLE_SDP_CANDIDATE_IPVERSION_DEFAULT = JSJAC_JINGLE_SDP_CANDIDATE_IPVERSION_V4; + +/** + * @constant + * @global + * @type {String} + * @readonly + * @default + * @public + */ +var JSJAC_JINGLE_SDP_CANDIDATE_PROTOCOL_DEFAULT = JSJAC_JINGLE_SDP_CANDIDATE_PROTOCOL_UDP; + +/** + * @constant + * @global + * @type {String} + * @readonly + * @default + * @public + */ +var JSJAC_JINGLE_SDP_CANDIDATE_PRIORITY_DEFAULT = '1'; + + + +/** + * JSJAC JINGLE MUJI CONSTANTS + */ + +/** + * @constant + * @global + * @type {String} + * @readonly + * @default + * @public + */ +var JSJAC_JINGLE_MUJI_ACTION_PREPARE = 'prepare'; + +/** + * @constant + * @global + * @type {String} + * @readonly + * @default + * @public + */ +var JSJAC_JINGLE_MUJI_ACTION_INITIATE = 'initiate'; + +/** + * @constant + * @global + * @type {String} + * @readonly + * @default + * @public + */ +var JSJAC_JINGLE_MUJI_ACTION_LEAVE = 'leave'; + +/** + * @constant + * @global + * @type {String} + * @readonly + * @default + * @public + */ +var JSJAC_JINGLE_MUJI_STATUS_INACTIVE = 'inactive'; + +/** + * @constant + * @global + * @type {String} + * @readonly + * @default + * @public + */ +var JSJAC_JINGLE_MUJI_STATUS_PREPARING = 'preparing'; + +/** + * @constant + * @global + * @type {String} + * @readonly + * @default + * @public + */ +var JSJAC_JINGLE_MUJI_STATUS_PREPARED = 'prepared'; + +/** + * @constant + * @global + * @type {String} + * @readonly + * @default + * @public + */ +var JSJAC_JINGLE_MUJI_STATUS_INITIATING = 'initiating'; + +/** + * @constant + * @global + * @type {String} + * @readonly + * @default + * @public + */ +var JSJAC_JINGLE_MUJI_STATUS_INITIATED = 'initiated'; + +/** + * @constant + * @global + * @type {String} + * @readonly + * @default + * @public + */ +var JSJAC_JINGLE_MUJI_STATUS_LEAVING = 'leaving'; + +/** + * @constant + * @global + * @type {String} + * @readonly + * @default + * @public + */ +var JSJAC_JINGLE_MUJI_STATUS_LEFT = 'left'; + +/** + * @constant + * @global + * @type {Number} + * @readonly + * @default + * @public + */ +var JSJAC_JINGLE_MUJI_INITIATE_WAIT = 2; + +/** + * @constant + * @global + * @type {Number} + * @readonly + * @default + * @public + */ +var JSJAC_JINGLE_MUJI_LEAVE_WAIT = 1; + +/** + * @constant + * @global + * @type {Number} + * @readonly + * @default + * @public + */ +var JSJAC_JINGLE_MUJI_PARTICIPANT_ACCEPT_WAIT = 0.250; + +/** + * @constant + * @global + * @type {String} + * @readonly + * @default + * @public + */ +var JSJAC_JINGLE_MUJI_HANDLER_GET_USER_MEDIA = 'get_user_media'; + +/** + * @constant + * @global + * @type {String} + * @readonly + * @default + * @public + */ +var JSJAC_JINGLE_MUJI_MUC_AFFILIATION_ADMIN = 'admin'; + +/** + * @constant + * @global + * @type {String} + * @readonly + * @default + * @public + */ +var JSJAC_JINGLE_MUJI_MUC_AFFILIATION_MEMBER = 'member'; + +/** + * @constant + * @global + * @type {String} + * @readonly + * @default + * @public + */ +var JSJAC_JINGLE_MUJI_MUC_AFFILIATION_NONE = 'none'; + +/** + * @constant + * @global + * @type {String} + * @readonly + * @default + * @public + */ +var JSJAC_JINGLE_MUJI_MUC_AFFILIATION_OUTCAST = 'outcast'; + +/** + * @constant + * @global + * @type {String} + * @readonly + * @default + * @public + */ +var JSJAC_JINGLE_MUJI_MUC_AFFILIATION_OWNER = 'owner'; + +/** + * @constant + * @global + * @type {String} + * @readonly + * @default + * @public + */ +var JSJAC_JINGLE_MUJI_MUC_OWNER_SUBMIT = 'submit'; + +/** + * @constant + * @global + * @type {String} + * @readonly + * @default + * @public + */ +var JSJAC_JINGLE_MUJI_MUC_CONFIG_SECRET = 'muc#roomconfig_roomsecret'; @@ -295,3747 +3992,894 @@ var JSJAC_JINGLE_STANZA_TYPE_GET = 'get'; * JSJSAC JINGLE CONSTANTS MAPPING */ -var JSJAC_JINGLE_BROWSERS = {}; -JSJAC_JINGLE_BROWSERS[JSJAC_JINGLE_BROWSER_FIREFOX] = 1; -JSJAC_JINGLE_BROWSERS[JSJAC_JINGLE_BROWSER_CHROME] = 1; -JSJAC_JINGLE_BROWSERS[JSJAC_JINGLE_BROWSER_SAFARI] = 1; -JSJAC_JINGLE_BROWSERS[JSJAC_JINGLE_BROWSER_OPERA] = 1; -JSJAC_JINGLE_BROWSERS[JSJAC_JINGLE_BROWSER_IE] = 1; +/** + * @constant + * @global + * @type {Object} + * @readonly + * @default + * @public + */ +var JSJAC_JINGLE_SDP_CANDIDATE_TYPES = {}; +JSJAC_JINGLE_SDP_CANDIDATE_TYPES[JSJAC_JINGLE_SDP_CANDIDATE_TYPE_HOST] = JSJAC_JINGLE_SDP_CANDIDATE_METHOD_ICE; +JSJAC_JINGLE_SDP_CANDIDATE_TYPES[JSJAC_JINGLE_SDP_CANDIDATE_TYPE_SRFLX] = JSJAC_JINGLE_SDP_CANDIDATE_METHOD_ICE; +JSJAC_JINGLE_SDP_CANDIDATE_TYPES[JSJAC_JINGLE_SDP_CANDIDATE_TYPE_PRFLX] = JSJAC_JINGLE_SDP_CANDIDATE_METHOD_ICE; +JSJAC_JINGLE_SDP_CANDIDATE_TYPES[JSJAC_JINGLE_SDP_CANDIDATE_TYPE_RELAY] = JSJAC_JINGLE_SDP_CANDIDATE_METHOD_RAW; -var JSJAC_JINGLE_SENDERS = {}; -JSJAC_JINGLE_SENDERS[JSJAC_JINGLE_SENDERS_BOTH.jingle] = JSJAC_JINGLE_SENDERS_BOTH.sdp; -JSJAC_JINGLE_SENDERS[JSJAC_JINGLE_SENDERS_INITIATOR.jingle] = JSJAC_JINGLE_SENDERS_INITIATOR.sdp; -JSJAC_JINGLE_SENDERS[JSJAC_JINGLE_SENDERS_NONE.jingle] = JSJAC_JINGLE_SENDERS_NONE.sdp; -JSJAC_JINGLE_SENDERS[JSJAC_JINGLE_SENDERS_RESPONDER.jingle] = JSJAC_JINGLE_SENDERS_RESPONDER.sdp; +/** + * @constant + * @global + * @type {Object} + * @readonly + * @default + * @public + */ +var JSJAC_JINGLE_BROWSERS = {}; +JSJAC_JINGLE_BROWSERS[JSJAC_JINGLE_BROWSER_FIREFOX] = 1; +JSJAC_JINGLE_BROWSERS[JSJAC_JINGLE_BROWSER_CHROME] = 1; +JSJAC_JINGLE_BROWSERS[JSJAC_JINGLE_BROWSER_SAFARI] = 1; +JSJAC_JINGLE_BROWSERS[JSJAC_JINGLE_BROWSER_OPERA] = 1; +JSJAC_JINGLE_BROWSERS[JSJAC_JINGLE_BROWSER_IE] = 1; -var JSJAC_JINGLE_CREATORS = {}; -JSJAC_JINGLE_CREATORS[JSJAC_JINGLE_CREATOR_INITIATOR] = 1; -JSJAC_JINGLE_CREATORS[JSJAC_JINGLE_CREATOR_RESPONDER] = 1; +/** + * @constant + * @global + * @type {Object} + * @readonly + * @default + * @public + */ +var JSJAC_JINGLE_SENDERS = {}; +JSJAC_JINGLE_SENDERS[JSJAC_JINGLE_SENDERS_BOTH.jingle] = JSJAC_JINGLE_SENDERS_BOTH.sdp; +JSJAC_JINGLE_SENDERS[JSJAC_JINGLE_SENDERS_INITIATOR.jingle] = JSJAC_JINGLE_SENDERS_INITIATOR.sdp; +JSJAC_JINGLE_SENDERS[JSJAC_JINGLE_SENDERS_NONE.jingle] = JSJAC_JINGLE_SENDERS_NONE.sdp; +JSJAC_JINGLE_SENDERS[JSJAC_JINGLE_SENDERS_RESPONDER.jingle] = JSJAC_JINGLE_SENDERS_RESPONDER.sdp; -var JSJAC_JINGLE_STATUSES = {}; -JSJAC_JINGLE_STATUSES[JSJAC_JINGLE_STATUS_INACTIVE] = 1; -JSJAC_JINGLE_STATUSES[JSJAC_JINGLE_STATUS_INITIATING] = 1; -JSJAC_JINGLE_STATUSES[JSJAC_JINGLE_STATUS_INITIATED] = 1; -JSJAC_JINGLE_STATUSES[JSJAC_JINGLE_STATUS_ACCEPTING] = 1; -JSJAC_JINGLE_STATUSES[JSJAC_JINGLE_STATUS_ACCEPTED] = 1; -JSJAC_JINGLE_STATUSES[JSJAC_JINGLE_STATUS_TERMINATING] = 1; -JSJAC_JINGLE_STATUSES[JSJAC_JINGLE_STATUS_TERMINATED] = 1; +/** + * @constant + * @global + * @type {Object} + * @readonly + * @default + * @public + */ +var JSJAC_JINGLE_CREATORS = {}; +JSJAC_JINGLE_CREATORS[JSJAC_JINGLE_CREATOR_INITIATOR] = 1; +JSJAC_JINGLE_CREATORS[JSJAC_JINGLE_CREATOR_RESPONDER] = 1; -var JSJAC_JINGLE_ACTIONS = {}; -JSJAC_JINGLE_ACTIONS[JSJAC_JINGLE_ACTION_CONTENT_ACCEPT] = 1; -JSJAC_JINGLE_ACTIONS[JSJAC_JINGLE_ACTION_CONTENT_ADD] = 1; -JSJAC_JINGLE_ACTIONS[JSJAC_JINGLE_ACTION_CONTENT_MODIFY] = 1; -JSJAC_JINGLE_ACTIONS[JSJAC_JINGLE_ACTION_CONTENT_REJECT] = 1; -JSJAC_JINGLE_ACTIONS[JSJAC_JINGLE_ACTION_CONTENT_REMOVE] = 1; -JSJAC_JINGLE_ACTIONS[JSJAC_JINGLE_ACTION_DESCRIPTION_INFO] = 1; -JSJAC_JINGLE_ACTIONS[JSJAC_JINGLE_ACTION_SECURITY_INFO] = 1; -JSJAC_JINGLE_ACTIONS[JSJAC_JINGLE_ACTION_SESSION_ACCEPT] = 1; -JSJAC_JINGLE_ACTIONS[JSJAC_JINGLE_ACTION_SESSION_INFO] = 1; -JSJAC_JINGLE_ACTIONS[JSJAC_JINGLE_ACTION_SESSION_INITIATE] = 1; -JSJAC_JINGLE_ACTIONS[JSJAC_JINGLE_ACTION_SESSION_TERMINATE] = 1; -JSJAC_JINGLE_ACTIONS[JSJAC_JINGLE_ACTION_TRANSPORT_ACCEPT] = 1; -JSJAC_JINGLE_ACTIONS[JSJAC_JINGLE_ACTION_TRANSPORT_INFO] = 1; -JSJAC_JINGLE_ACTIONS[JSJAC_JINGLE_ACTION_TRANSPORT_REJECT] = 1; -JSJAC_JINGLE_ACTIONS[JSJAC_JINGLE_ACTION_TRANSPORT_REPLACE] = 1; +/** + * @constant + * @global + * @type {Object} + * @readonly + * @default + * @public + */ +var JSJAC_JINGLE_STATUSES = {}; +JSJAC_JINGLE_STATUSES[JSJAC_JINGLE_STATUS_INACTIVE] = 1; +JSJAC_JINGLE_STATUSES[JSJAC_JINGLE_STATUS_INITIATING] = 1; +JSJAC_JINGLE_STATUSES[JSJAC_JINGLE_STATUS_INITIATED] = 1; +JSJAC_JINGLE_STATUSES[JSJAC_JINGLE_STATUS_ACCEPTING] = 1; +JSJAC_JINGLE_STATUSES[JSJAC_JINGLE_STATUS_ACCEPTED] = 1; +JSJAC_JINGLE_STATUSES[JSJAC_JINGLE_STATUS_TERMINATING] = 1; +JSJAC_JINGLE_STATUSES[JSJAC_JINGLE_STATUS_TERMINATED] = 1; -var JSJAC_JINGLE_ERRORS = {}; -JSJAC_JINGLE_ERRORS[JSJAC_JINGLE_ERROR_OUT_OF_ORDER.jingle] = 1; -JSJAC_JINGLE_ERRORS[JSJAC_JINGLE_ERROR_TIE_BREAK.jingle] = 1; -JSJAC_JINGLE_ERRORS[JSJAC_JINGLE_ERROR_UNKNOWN_SESSION.jingle] = 1; -JSJAC_JINGLE_ERRORS[JSJAC_JINGLE_ERROR_UNSUPPORTED_INFO.jingle] = 1; -JSJAC_JINGLE_ERRORS[JSJAC_JINGLE_ERROR_SECURITY_REQUIRED.jingle] = 1; +/** + * @constant + * @global + * @type {Object} + * @readonly + * @default + * @public + */ +var JSJAC_JINGLE_ACTIONS = {}; +JSJAC_JINGLE_ACTIONS[JSJAC_JINGLE_ACTION_CONTENT_ACCEPT] = 1; +JSJAC_JINGLE_ACTIONS[JSJAC_JINGLE_ACTION_CONTENT_ADD] = 1; +JSJAC_JINGLE_ACTIONS[JSJAC_JINGLE_ACTION_CONTENT_MODIFY] = 1; +JSJAC_JINGLE_ACTIONS[JSJAC_JINGLE_ACTION_CONTENT_REJECT] = 1; +JSJAC_JINGLE_ACTIONS[JSJAC_JINGLE_ACTION_CONTENT_REMOVE] = 1; +JSJAC_JINGLE_ACTIONS[JSJAC_JINGLE_ACTION_DESCRIPTION_INFO] = 1; +JSJAC_JINGLE_ACTIONS[JSJAC_JINGLE_ACTION_SECURITY_INFO] = 1; +JSJAC_JINGLE_ACTIONS[JSJAC_JINGLE_ACTION_SESSION_ACCEPT] = 1; +JSJAC_JINGLE_ACTIONS[JSJAC_JINGLE_ACTION_SESSION_INFO] = 1; +JSJAC_JINGLE_ACTIONS[JSJAC_JINGLE_ACTION_SESSION_INITIATE] = 1; +JSJAC_JINGLE_ACTIONS[JSJAC_JINGLE_ACTION_SESSION_TERMINATE] = 1; +JSJAC_JINGLE_ACTIONS[JSJAC_JINGLE_ACTION_TRANSPORT_ACCEPT] = 1; +JSJAC_JINGLE_ACTIONS[JSJAC_JINGLE_ACTION_TRANSPORT_INFO] = 1; +JSJAC_JINGLE_ACTIONS[JSJAC_JINGLE_ACTION_TRANSPORT_REJECT] = 1; +JSJAC_JINGLE_ACTIONS[JSJAC_JINGLE_ACTION_TRANSPORT_REPLACE] = 1; -var XMPP_ERRORS = {}; -XMPP_ERRORS[XMPP_ERROR_UNEXPECTED_REQUEST.xmpp] = 1; -XMPP_ERRORS[XMPP_ERROR_CONFLICT.xmpp] = 1; -XMPP_ERRORS[XMPP_ERROR_ITEM_NOT_FOUND.xmpp] = 1; -XMPP_ERRORS[XMPP_ERROR_NOT_ACCEPTABLE.xmpp] = 1; -XMPP_ERRORS[XMPP_ERROR_FEATURE_NOT_IMPLEMENTED.xmpp] = 1; -XMPP_ERRORS[XMPP_ERROR_SERVICE_UNAVAILABLE.xmpp] = 1; -XMPP_ERRORS[XMPP_ERROR_REDIRECT.xmpp] = 1; -XMPP_ERRORS[XMPP_ERROR_RESOURCE_CONSTRAINT.xmpp] = 1; -XMPP_ERRORS[XMPP_ERROR_BAD_REQUEST.xmpp] = 1; +/** + * @constant + * @global + * @type {Object} + * @readonly + * @default + * @public + */ +var JSJAC_JINGLE_ERRORS = {}; +JSJAC_JINGLE_ERRORS[JSJAC_JINGLE_ERROR_OUT_OF_ORDER.jingle] = 1; +JSJAC_JINGLE_ERRORS[JSJAC_JINGLE_ERROR_TIE_BREAK.jingle] = 1; +JSJAC_JINGLE_ERRORS[JSJAC_JINGLE_ERROR_UNKNOWN_SESSION.jingle] = 1; +JSJAC_JINGLE_ERRORS[JSJAC_JINGLE_ERROR_UNSUPPORTED_INFO.jingle] = 1; +JSJAC_JINGLE_ERRORS[JSJAC_JINGLE_ERROR_SECURITY_REQUIRED.jingle] = 1; -var JSJAC_JINGLE_REASONS = {}; -JSJAC_JINGLE_REASONS[JSJAC_JINGLE_REASON_ALTERNATIVE_SESSION] = 1; -JSJAC_JINGLE_REASONS[JSJAC_JINGLE_REASON_BUSY] = 1; -JSJAC_JINGLE_REASONS[JSJAC_JINGLE_REASON_CANCEL] = 1; -JSJAC_JINGLE_REASONS[JSJAC_JINGLE_REASON_CONNECTIVITY_ERROR] = 1; -JSJAC_JINGLE_REASONS[JSJAC_JINGLE_REASON_DECLINE] = 1; -JSJAC_JINGLE_REASONS[JSJAC_JINGLE_REASON_EXPIRED] = 1; -JSJAC_JINGLE_REASONS[JSJAC_JINGLE_REASON_FAILED_APPLICATION] = 1; -JSJAC_JINGLE_REASONS[JSJAC_JINGLE_REASON_FAILED_TRANSPORT] = 1; -JSJAC_JINGLE_REASONS[JSJAC_JINGLE_REASON_GENERAL_ERROR] = 1; -JSJAC_JINGLE_REASONS[JSJAC_JINGLE_REASON_GONE] = 1; -JSJAC_JINGLE_REASONS[JSJAC_JINGLE_REASON_INCOMPATIBLE_PARAMETERS] = 1; -JSJAC_JINGLE_REASONS[JSJAC_JINGLE_REASON_MEDIA_ERROR] = 1; -JSJAC_JINGLE_REASONS[JSJAC_JINGLE_REASON_SECURITY_ERROR] = 1; -JSJAC_JINGLE_REASONS[JSJAC_JINGLE_REASON_SUCCESS] = 1; -JSJAC_JINGLE_REASONS[JSJAC_JINGLE_REASON_TIMEOUT] = 1; -JSJAC_JINGLE_REASONS[JSJAC_JINGLE_REASON_UNSUPPORTED_APPLICATIONS] = 1; -JSJAC_JINGLE_REASONS[JSJAC_JINGLE_REASON_UNSUPPORTED_TRANSPORTS] = 1; +/** + * @constant + * @global + * @type {Object} + * @readonly + * @default + * @public + */ +var XMPP_ERRORS = {}; +XMPP_ERRORS[XMPP_ERROR_UNEXPECTED_REQUEST.xmpp] = 1; +XMPP_ERRORS[XMPP_ERROR_CONFLICT.xmpp] = 1; +XMPP_ERRORS[XMPP_ERROR_ITEM_NOT_FOUND.xmpp] = 1; +XMPP_ERRORS[XMPP_ERROR_NOT_ACCEPTABLE.xmpp] = 1; +XMPP_ERRORS[XMPP_ERROR_FEATURE_NOT_IMPLEMENTED.xmpp] = 1; +XMPP_ERRORS[XMPP_ERROR_SERVICE_UNAVAILABLE.xmpp] = 1; +XMPP_ERRORS[XMPP_ERROR_REDIRECT.xmpp] = 1; +XMPP_ERRORS[XMPP_ERROR_RESOURCE_CONSTRAINT.xmpp] = 1; +XMPP_ERRORS[XMPP_ERROR_BAD_REQUEST.xmpp] = 1; -var JSJAC_JINGLE_SESSION_INFOS = {}; -JSJAC_JINGLE_SESSION_INFOS[JSJAC_JINGLE_SESSION_INFO_ACTIVE] = 1; -JSJAC_JINGLE_SESSION_INFOS[JSJAC_JINGLE_SESSION_INFO_HOLD] = 1; -JSJAC_JINGLE_SESSION_INFOS[JSJAC_JINGLE_SESSION_INFO_MUTE] = 1; -JSJAC_JINGLE_SESSION_INFOS[JSJAC_JINGLE_SESSION_INFO_RINGING] = 1; -JSJAC_JINGLE_SESSION_INFOS[JSJAC_JINGLE_SESSION_INFO_UNHOLD] = 1; -JSJAC_JINGLE_SESSION_INFOS[JSJAC_JINGLE_SESSION_INFO_UNMUTE] = 1; +/** + * @constant + * @global + * @type {Object} + * @readonly + * @default + * @public + */ +var JSJAC_JINGLE_REASONS = {}; +JSJAC_JINGLE_REASONS[JSJAC_JINGLE_REASON_ALTERNATIVE_SESSION] = 1; +JSJAC_JINGLE_REASONS[JSJAC_JINGLE_REASON_BUSY] = 1; +JSJAC_JINGLE_REASONS[JSJAC_JINGLE_REASON_CANCEL] = 1; +JSJAC_JINGLE_REASONS[JSJAC_JINGLE_REASON_CONNECTIVITY_ERROR] = 1; +JSJAC_JINGLE_REASONS[JSJAC_JINGLE_REASON_DECLINE] = 1; +JSJAC_JINGLE_REASONS[JSJAC_JINGLE_REASON_EXPIRED] = 1; +JSJAC_JINGLE_REASONS[JSJAC_JINGLE_REASON_FAILED_APPLICATION] = 1; +JSJAC_JINGLE_REASONS[JSJAC_JINGLE_REASON_FAILED_TRANSPORT] = 1; +JSJAC_JINGLE_REASONS[JSJAC_JINGLE_REASON_GENERAL_ERROR] = 1; +JSJAC_JINGLE_REASONS[JSJAC_JINGLE_REASON_GONE] = 1; +JSJAC_JINGLE_REASONS[JSJAC_JINGLE_REASON_INCOMPATIBLE_PARAMETERS] = 1; +JSJAC_JINGLE_REASONS[JSJAC_JINGLE_REASON_MEDIA_ERROR] = 1; +JSJAC_JINGLE_REASONS[JSJAC_JINGLE_REASON_SECURITY_ERROR] = 1; +JSJAC_JINGLE_REASONS[JSJAC_JINGLE_REASON_SUCCESS] = 1; +JSJAC_JINGLE_REASONS[JSJAC_JINGLE_REASON_TIMEOUT] = 1; +JSJAC_JINGLE_REASONS[JSJAC_JINGLE_REASON_UNSUPPORTED_APPLICATIONS] = 1; +JSJAC_JINGLE_REASONS[JSJAC_JINGLE_REASON_UNSUPPORTED_TRANSPORTS] = 1; -var JSJAC_JINGLE_MEDIAS = {}; -JSJAC_JINGLE_MEDIAS[JSJAC_JINGLE_MEDIA_AUDIO] = { label: '0' }; -JSJAC_JINGLE_MEDIAS[JSJAC_JINGLE_MEDIA_VIDEO] = { label: '1' }; +/** + * @constant + * @global + * @type {Object} + * @readonly + * @default + * @public + */ +var JSJAC_JINGLE_SESSION_INFOS = {}; +JSJAC_JINGLE_SESSION_INFOS[JSJAC_JINGLE_SESSION_INFO_ACTIVE] = 1; +JSJAC_JINGLE_SESSION_INFOS[JSJAC_JINGLE_SESSION_INFO_HOLD] = 1; +JSJAC_JINGLE_SESSION_INFOS[JSJAC_JINGLE_SESSION_INFO_MUTE] = 1; +JSJAC_JINGLE_SESSION_INFOS[JSJAC_JINGLE_SESSION_INFO_RINGING] = 1; +JSJAC_JINGLE_SESSION_INFOS[JSJAC_JINGLE_SESSION_INFO_UNHOLD] = 1; +JSJAC_JINGLE_SESSION_INFOS[JSJAC_JINGLE_SESSION_INFO_UNMUTE] = 1; -var JSJAC_JINGLE_VIDEO_SOURCES = {}; -JSJAC_JINGLE_VIDEO_SOURCES[JSJAC_JINGLE_VIDEO_SOURCE_CAMERA] = 1; -JSJAC_JINGLE_VIDEO_SOURCES[JSJAC_JINGLE_VIDEO_SOURCE_SCREEN] = 1; +/** + * @constant + * @global + * @type {Object} + * @readonly + * @default + * @public + */ +var JSJAC_JINGLE_MEDIAS = {}; +JSJAC_JINGLE_MEDIAS[JSJAC_JINGLE_MEDIA_AUDIO] = { label: '0' }; +JSJAC_JINGLE_MEDIAS[JSJAC_JINGLE_MEDIA_VIDEO] = { label: '1' }; -var JSJAC_JINGLE_STANZAS = {}; -JSJAC_JINGLE_STANZAS[JSJAC_JINGLE_STANZA_TYPE_ALL] = 1; -JSJAC_JINGLE_STANZAS[JSJAC_JINGLE_STANZA_TYPE_RESULT] = 1; -JSJAC_JINGLE_STANZAS[JSJAC_JINGLE_STANZA_TYPE_SET] = 1; -JSJAC_JINGLE_STANZAS[JSJAC_JINGLE_STANZA_TYPE_GET] = 1; +/** + * @constant + * @global + * @type {Object} + * @readonly + * @default + * @public + */ +var JSJAC_JINGLE_VIDEO_SOURCES = {}; +JSJAC_JINGLE_VIDEO_SOURCES[JSJAC_JINGLE_VIDEO_SOURCE_CAMERA] = 1; +JSJAC_JINGLE_VIDEO_SOURCES[JSJAC_JINGLE_VIDEO_SOURCE_SCREEN] = 1; + +/** + * @constant + * @global + * @type {Object} + * @readonly + * @default + * @public + */ +var JSJAC_JINGLE_MESSAGE_TYPES = {}; +JSJAC_JINGLE_MESSAGE_TYPES[JSJAC_JINGLE_MESSAGE_TYPE_ALL] = 1; +JSJAC_JINGLE_MESSAGE_TYPES[JSJAC_JINGLE_MESSAGE_TYPE_NORMAL] = 1; +JSJAC_JINGLE_MESSAGE_TYPES[JSJAC_JINGLE_MESSAGE_TYPE_CHAT] = 1; +JSJAC_JINGLE_MESSAGE_TYPES[JSJAC_JINGLE_MESSAGE_TYPE_HEADLINE] = 1; +JSJAC_JINGLE_MESSAGE_TYPES[JSJAC_JINGLE_MESSAGE_TYPE_GROUPCHAT] = 1; +JSJAC_JINGLE_MESSAGE_TYPES[JSJAC_JINGLE_MESSAGE_TYPE_ERROR] = 1; + +/** + * @constant + * @global + * @type {Object} + * @readonly + * @default + * @public + */ +var JSJAC_JINGLE_PRESENCE_TYPES = {}; +JSJAC_JINGLE_PRESENCE_TYPES[JSJAC_JINGLE_PRESENCE_TYPE_ALL] = 1; +JSJAC_JINGLE_PRESENCE_TYPES[JSJAC_JINGLE_PRESENCE_TYPE_AVAILABLE] = 1; +JSJAC_JINGLE_PRESENCE_TYPES[JSJAC_JINGLE_PRESENCE_TYPE_UNAVAILABLE] = 1; +JSJAC_JINGLE_PRESENCE_TYPES[JSJAC_JINGLE_PRESENCE_TYPE_ERROR] = 1; + +/** + * @constant + * @global + * @type {Object} + * @readonly + * @default + * @public + */ +var JSJAC_JINGLE_IQ_TYPES = {}; +JSJAC_JINGLE_IQ_TYPES[JSJAC_JINGLE_IQ_TYPE_ALL] = 1; +JSJAC_JINGLE_IQ_TYPES[JSJAC_JINGLE_IQ_TYPE_RESULT] = 1; +JSJAC_JINGLE_IQ_TYPES[JSJAC_JINGLE_IQ_TYPE_SET] = 1; +JSJAC_JINGLE_IQ_TYPES[JSJAC_JINGLE_IQ_TYPE_GET] = 1; +JSJAC_JINGLE_IQ_TYPES[JSJAC_JINGLE_IQ_TYPE_ERROR] = 1; /** - * JSJAC JINGLE STORAGE - */ - -var JSJAC_JINGLE_STORE_CONNECTION = null; -var JSJAC_JINGLE_STORE_SESSIONS = {}; -var JSJAC_JINGLE_STORE_INITIATE = function(stanza) {}; - -var JSJAC_JINGLE_STORE_DEBUG = { - log : function() {} -}; - -var JSJAC_JINGLE_STORE_EXTDISCO = { - stun : {}, - turn : {} -}; - -var JSJAC_JINGLE_STORE_FALLBACK = { - stun : {}, - turn : {} -}; - -var JSJAC_JINGLE_STORE_DEFER = { - deferred : false, - count : 0, - fn : [] -}; - -var R_JSJAC_JINGLE_SERVICE_URI = /^(\w+):([^:\?]+)(?::(\d+))?(?:\?transport=(\w+))?/i; - - - -/** - * JSJSAC JINGLE METHODS + * JSJAC JINGLE MUJI CONSTANTS MAPPING */ /** - * Creates a new XMPP Jingle session. - * @class Somewhat abstract base class for XMPP Jingle sessions. Contains all - * of the code in common for all Jingle sessions - * @constructor - * @param {Object} args Jingle session arguments. - * @param {function} args.session_initiate_pending The initiate pending custom handler. - * @param {function} args.session_initiate_success The initiate success custom handler. - * @param {function} args.session_initiate_error The initiate error custom handler. - * @param {function} args.session_initiate_request The initiate request custom handler. - * @param {function} args.session_accept_pending The accept pending custom handler. - * @param {function} args.session_accept_success The accept success custom handler. - * @param {function} args.session_accept_error The accept error custom handler. - * @param {function} args.session_accept_request The accept request custom handler. - * @param {function} args.session_info_success The info success custom handler. - * @param {function} args.session_info_error The info error custom handler. - * @param {function} args.session_info_request The info request custom handler. - * @param {function} args.session_terminate_pending The terminate pending custom handler. - * @param {function} args.session_terminate_success The terminate success custom handler. - * @param {function} args.session_terminate_error The terminate error custom handler. - * @param {function} args.session_terminate_request The terminate request custom handler. - * @param {DOM} args.local_view The path to the local stream view element. - * @param {DOM} args.remote_view The path to the remote stream view element. - * @param {string} args.to The full JID to start the Jingle session with. - * @param {string} args.media The media type to be used in the Jingle session. - * @param {string} args.resolution The resolution to be used for video in the Jingle session. - * @param {string} args.bandwidth The bandwidth to be limited for video in the Jingle session. - * @param {string} args.fps The framerate to be used for video in the Jingle session. - * @param {object} args.stun A list of STUN servers to use (override the default one) - * @param {object} args.turn A list of TURN servers to use - * @param {object} args.sdp_trace Log SDP trace in console (requires a debug interface) - * @param {JSJaCDebugger} args.debug A reference to a debugger implementing the JSJaCDebugger interface. + * @constant + * @global + * @type {Object} + * @readonly + * @default + * @public */ -function JSJaCJingle(args) { - var self = this; +var JSJAC_JINGLE_MUJI_ACTIONS = {}; +JSJAC_JINGLE_MUJI_ACTIONS[JSJAC_JINGLE_MUJI_ACTION_PREPARE] = 1; +JSJAC_JINGLE_MUJI_ACTIONS[JSJAC_JINGLE_MUJI_ACTION_INITIATE] = 1; +JSJAC_JINGLE_MUJI_ACTIONS[JSJAC_JINGLE_MUJI_ACTION_LEAVE] = 1; - if(args && args.session_initiate_pending) +/** + * @constant + * @global + * @type {Object} + * @readonly + * @default + * @public + */ +var JSJAC_JINGLE_MUJI_STATUS = {}; +JSJAC_JINGLE_MUJI_STATUS[JSJAC_JINGLE_MUJI_STATUS_INACTIVE] = 1; +JSJAC_JINGLE_MUJI_STATUS[JSJAC_JINGLE_MUJI_STATUS_PREPARING] = 1; +JSJAC_JINGLE_MUJI_STATUS[JSJAC_JINGLE_MUJI_STATUS_PREPARED] = 1; +JSJAC_JINGLE_MUJI_STATUS[JSJAC_JINGLE_MUJI_STATUS_INITIATING] = 1; +JSJAC_JINGLE_MUJI_STATUS[JSJAC_JINGLE_MUJI_STATUS_INITIATED] = 1; +JSJAC_JINGLE_MUJI_STATUS[JSJAC_JINGLE_MUJI_STATUS_LEAVING] = 1; +JSJAC_JINGLE_MUJI_STATUS[JSJAC_JINGLE_MUJI_STATUS_LEFT] = 1; +/** + * @fileoverview JSJaC Jingle library - Storage layer + * + * @url https://github.com/valeriansaliou/jsjac-jingle + * @depends https://github.com/sstrigler/JSJaC + * @author Valérian Saliou https://valeriansaliou.name/ + * @license Mozilla private License v2.0 (MPL v2.0) + */ + + +/** @module jsjac-jingle/storage */ +/** @exports JSJaCJingleStorage */ + + +/** + * Storage layer wrapper. + * @instance + * @requires nicolas-van/ring.js + * @requires jsjac-jingle/constants + * @see {@link http://ringjs.neoname.eu/|Ring.js} + * @see {@link http://stefan-strigler.de/jsjac-1.3.4/doc/|JSJaC Documentation} + */ +var JSJaCJingleStorage = new (ring.create( + /** @lends JSJaCJingleStorage.prototype */ + { /** - * @private + * Constructor */ - self._session_initiate_pending = args.session_initiate_pending; - - if(args && args.session_initiate_success) - /** - * @private - */ - self._session_initiate_success = args.session_initiate_success; - - if(args && args.session_initiate_error) - /** - * @private - */ - self._session_initiate_error = args.session_initiate_error; - - if(args && args.session_initiate_request) - /** - * @private - */ - self._session_initiate_request = args.session_initiate_request; - - if(args && args.session_accept_pending) - /** - * @private - */ - self._session_accept_pending = args.session_accept_pending; - - if(args && args.session_accept_success) - /** - * @private - */ - self._session_accept_success = args.session_accept_success; - - if(args && args.session_accept_error) - /** - * @private - */ - self._session_accept_error = args.session_accept_error; - - if(args && args.session_accept_request) - /** - * @private - */ - self._session_accept_request = args.session_accept_request; - - if(args && args.session_info_success) - /** - * @private - */ - self._session_info_success = args.session_info_success; - - if(args && args.session_info_error) - /** - * @private - */ - self._session_info_error = args.session_info_error; - - if(args && args.session_info_request) - /** - * @private - */ - self._session_info_request = args.session_info_request; - - if(args && args.session_terminate_pending) - /** - * @private - */ - self._session_terminate_pending = args.session_terminate_pending; - - if(args && args.session_terminate_success) - /** - * @private - */ - self._session_terminate_success = args.session_terminate_success; - - if(args && args.session_terminate_error) - /** - * @private - */ - self._session_terminate_error = args.session_terminate_error; - - if(args && args.session_terminate_request) - /** - * @private - */ - self._session_terminate_request = args.session_terminate_request; - - if(args && args.to) - /** - * @private - */ - self._to = args.to; - - if(args && args.media) - /** - * @private - */ - self._media = args.media; - - if(args && args.video_source) - /** - * @private - */ - self._video_source = args.video_source; - - if(args && args.resolution) - /** - * @private - */ - self._resolution = args.resolution; - - if(args && args.bandwidth) - /** - * @private - */ - self._bandwidth = args.bandwidth; - - if(args && args.fps) - /** - * @private - */ - self._fps = args.fps; - - if(args && args.local_view) - /** - * @private - */ - self._local_view = [args.local_view]; - - if(args && args.remote_view) - /** - * @private - */ - self._remote_view = [args.remote_view]; - - if(args && args.stun) { - /** - * @private - */ - self._stun = args.stun; - } else { - self._stun = {}; - } - - if(args && args.turn) { - /** - * @private - */ - self._turn = args.turn; - } else { - self._turn = {}; - } - - if(args && args.sdp_trace) - /** - * @private - */ - self._sdp_trace = args.sdp_trace; - - if(args && args.debug && args.debug.log) { + constructor: function() { /** - * Reference to debugger interface - * (needs to implement method log) - * @type JSJaCDebugger + * JSJAC JINGLE STORAGE */ - self._debug = args.debug; - } else { - self._debug = JSJAC_JINGLE_STORE_DEBUG; - } - /** - * @private - */ - self._local_stream = null; - - /** - * @private - */ - self._remote_stream = null; - - /** - * @private - */ - self._content_local = {}; - - /** - * @private - */ - self._content_remote = {}; - - /** - * @private - */ - self._payloads_local = []; - - /** - * @private - */ - self._group_local = {}; - - /** - * @private - */ - self._candidates_local = {}; - - /** - * @private - */ - self._candidates_queue_local = {}; - - /** - * @private - */ - self._payloads_remote = {}; - - /** - * @private - */ - self._group_remote = {}; - - /** - * @private - */ - self._candidates_remote = {}; - - /** - * @private - */ - self._candidates_queue_remote = {}; - - /** - * @private - */ - self._initiator = ''; - - /** - * @private - */ - self._responder = ''; - - /** - * @private - */ - self._mute = {}; - - /** - * @private - */ - self._lock = false; - - /** - * @private - */ - self._media_busy = false; - - /** - * @private - */ - self._sid = ''; - - /** - * @private - */ - self._name = {}; - - /** - * @private - */ - self._senders = {}; - - /** - * @private - */ - self._creator = {}; - - /** - * @private - */ - self._status = JSJAC_JINGLE_STATUS_INACTIVE; - - /** - * @private - */ - self._reason = JSJAC_JINGLE_REASON_CANCEL; - - /** - * @private - */ - self._handlers = {}; - - /** - * @private - */ - self._peer_connection = null; - - /** - * @private - */ - self._id = 0; - - /** - * @private - */ - self._sent_id = {}; - - /** - * @private - */ - self._received_id = {}; - - - - /** - * Initiates a new Jingle session. - */ - self.initiate = function() { - self.get_debug().log('[JSJaCJingle] initiate', 4); - - try { - // Locked? - if(self.get_lock()) { - self.get_debug().log('[JSJaCJingle] initiate > Cannot initiate, resource locked. Please open another session or check WebRTC support.', 0); - return; - } - - // Defer? - if(JSJaCJingle_defer(function() { self.initiate(); })) { - self.get_debug().log('[JSJaCJingle] initiate > Deferred (waiting for the library components to be initiated).', 0); - return; - } - - // Slot unavailable? - if(self.get_status() != JSJAC_JINGLE_STATUS_INACTIVE) { - self.get_debug().log('[JSJaCJingle] initiate > Cannot initiate, resource not inactive (status: ' + self.get_status() + ').', 0); - return; - } - - self.get_debug().log('[JSJaCJingle] initiate > New Jingle session with media: ' + self.get_media(), 2); - - // Common vars - var i, cur_name; - - // Trigger init pending custom callback - (self._get_session_initiate_pending())(self); - - // Change session status - self._set_status(JSJAC_JINGLE_STATUS_INITIATING); - - // Set session values - self._set_sid(self.util_generate_sid()); - self._set_initiator(self.util_connection_jid()); - self._set_responder(self.get_to()); - - for(i in self.get_media_all()) { - cur_name = self._util_name_generate( - self.get_media_all()[i] - ); - - self._set_name(cur_name); - - self._set_senders( - cur_name, - JSJAC_JINGLE_SENDERS_BOTH.jingle - ); - - self._set_creator( - cur_name, - JSJAC_JINGLE_CREATOR_INITIATOR - ); - } - - // Register session to common router - JSJaCJingle_add(self.get_sid(), self); - - // Initialize WebRTC - self._peer_get_user_media(function() { - self._peer_connection_create(function() { - self.get_debug().log('[JSJaCJingle] initiate > Ready to begin Jingle negotiation.', 2); - - self.send(JSJAC_JINGLE_STANZA_TYPE_SET, { action: JSJAC_JINGLE_ACTION_SESSION_INITIATE }); - }); - }); - } catch(e) { - self.get_debug().log('[JSJaCJingle] initiate > ' + e, 1); - } - }; - - /** - * Accepts the Jingle session. - */ - self.accept = function() { - self.get_debug().log('[JSJaCJingle] accept', 4); - - try { - // Locked? - if(self.get_lock()) { - self.get_debug().log('[JSJaCJingle] accept > Cannot accept, resource locked. Please open another session or check WebRTC support.', 0); - return; - } - - // Defer? - if(JSJaCJingle_defer(function() { self.accept(); })) { - self.get_debug().log('[JSJaCJingle] accept > Deferred (waiting for the library components to be initiated).', 0); - return; - } - - // Slot unavailable? - if(self.get_status() != JSJAC_JINGLE_STATUS_INITIATED) { - self.get_debug().log('[JSJaCJingle] accept > Cannot accept, resource not initiated (status: ' + self.get_status() + ').', 0); - return; - } - - self.get_debug().log('[JSJaCJingle] accept > New Jingle session with media: ' + self.get_media(), 2); - - // Trigger accept pending custom callback - (self._get_session_accept_pending())(self); - - // Change session status - self._set_status(JSJAC_JINGLE_STATUS_ACCEPTING); - - // Initialize WebRTC - self._peer_get_user_media(function() { - self._peer_connection_create(function() { - self.get_debug().log('[JSJaCJingle] accept > Ready to complete Jingle negotiation.', 2); - - // Process accept actions - self.send(JSJAC_JINGLE_STANZA_TYPE_SET, { action: JSJAC_JINGLE_ACTION_SESSION_ACCEPT }); - }); - }); - } catch(e) { - self.get_debug().log('[JSJaCJingle] accept > ' + e, 1); - } - }; - - /** - * Sends a Jingle session info. - */ - self.info = function(name, args) { - self.get_debug().log('[JSJaCJingle] info', 4); - - try { - // Locked? - if(self.get_lock()) { - self.get_debug().log('[JSJaCJingle] info > Cannot accept, resource locked. Please open another session or check WebRTC support.', 0); - return; - } - - // Defer? - if(JSJaCJingle_defer(function() { self.info(name, args); })) { - self.get_debug().log('[JSJaCJingle] info > Deferred (waiting for the library components to be initiated).', 0); - return; - } - - // Slot unavailable? - if(!(self.get_status() == JSJAC_JINGLE_STATUS_INITIATED || self.get_status() == JSJAC_JINGLE_STATUS_ACCEPTING || self.get_status() == JSJAC_JINGLE_STATUS_ACCEPTED)) { - self.get_debug().log('[JSJaCJingle] info > Cannot send info, resource not active (status: ' + self.get_status() + ').', 0); - return; - } - - // Assert - if(typeof args !== 'object') args = {}; - - // Build final args parameter - args.action = JSJAC_JINGLE_ACTION_SESSION_INFO; - if(name) args.info = name; - - self.send(JSJAC_JINGLE_STANZA_TYPE_SET, args); - } catch(e) { - self.get_debug().log('[JSJaCJingle] info > ' + e, 1); - } - }; - - /** - * Terminates the Jingle session. - */ - self.terminate = function(reason) { - self.get_debug().log('[JSJaCJingle] terminate', 4); - - try { - // Locked? - if(self.get_lock()) { - self.get_debug().log('[JSJaCJingle] terminate > Cannot terminate, resource locked. Please open another session or check WebRTC support.', 0); - return; - } - - // Defer? - if(JSJaCJingle_defer(function() { self.terminate(reason); })) { - self.get_debug().log('[JSJaCJingle] terminate > Deferred (waiting for the library components to be initiated).', 0); - return; - } - - // Slot unavailable? - if(self.get_status() == JSJAC_JINGLE_STATUS_TERMINATED) { - self.get_debug().log('[JSJaCJingle] terminate > Cannot terminate, resource already terminated (status: ' + self.get_status() + ').', 0); - return; - } - - // Change session status - self._set_status(JSJAC_JINGLE_STATUS_TERMINATING); - - // Trigger terminate pending custom callback - (self._get_session_terminate_pending())(self); - - // Process terminate actions - self.send(JSJAC_JINGLE_STANZA_TYPE_SET, { action: JSJAC_JINGLE_ACTION_SESSION_TERMINATE, reason: reason }); - } catch(e) { - self.get_debug().log('[JSJaCJingle] terminate > ' + e, 1); - } - }; - - /** - * Sends a given Jingle stanza packet - */ - self.send = function(type, args) { - self.get_debug().log('[JSJaCJingle] send', 4); - - try { - // Locked? - if(self.get_lock()) { - self.get_debug().log('[JSJaCJingle] send > Cannot send, resource locked. Please open another session or check WebRTC support.', 0); - return; - } - - // Defer? - if(JSJaCJingle_defer(function() { self.send(type, args); })) { - self.get_debug().log('[JSJaCJingle] send > Deferred (waiting for the library components to be initiated).', 0); - return; - } - - // Assert - if(typeof args !== 'object') args = {}; - - // Build stanza - var stanza = new JSJaCIQ(); - stanza.setTo(self.get_to()); - - if(type) stanza.setType(type); - - if(!args.id) args.id = self._get_id_new(); - stanza.setID(args.id); - - if(type == JSJAC_JINGLE_STANZA_TYPE_SET) { - if(!(args.action && args.action in JSJAC_JINGLE_ACTIONS)) { - self.get_debug().log('[JSJaCJingle] send > Stanza action unknown: ' + (args.action || 'undefined'), 1); - return; - } - - self._set_sent_id(args.id); - - // Submit to registered handler - switch(args.action) { - case JSJAC_JINGLE_ACTION_CONTENT_ACCEPT: - self.send_content_accept(stanza); break; - - case JSJAC_JINGLE_ACTION_CONTENT_ADD: - self.send_content_add(stanza); break; - - case JSJAC_JINGLE_ACTION_CONTENT_MODIFY: - self.send_content_modify(stanza); break; - - case JSJAC_JINGLE_ACTION_CONTENT_REJECT: - self.send_content_reject(stanza); break; - - case JSJAC_JINGLE_ACTION_CONTENT_REMOVE: - self.send_content_remove(stanza); break; - - case JSJAC_JINGLE_ACTION_DESCRIPTION_INFO: - self.send_description_info(stanza); break; - - case JSJAC_JINGLE_ACTION_SECURITY_INFO: - self.send_security_info(stanza); break; - - case JSJAC_JINGLE_ACTION_SESSION_ACCEPT: - self.send_session_accept(stanza, args); break; - - case JSJAC_JINGLE_ACTION_SESSION_INFO: - self.send_session_info(stanza, args); break; - - case JSJAC_JINGLE_ACTION_SESSION_INITIATE: - self.send_session_initiate(stanza, args); break; - - case JSJAC_JINGLE_ACTION_SESSION_TERMINATE: - self.send_session_terminate(stanza, args); break; - - case JSJAC_JINGLE_ACTION_TRANSPORT_ACCEPT: - self.send_transport_accept(stanza); break; - - case JSJAC_JINGLE_ACTION_TRANSPORT_INFO: - self.send_transport_info(stanza, args); break; - - case JSJAC_JINGLE_ACTION_TRANSPORT_REJECT: - self.send_transport_reject(stanza); break; - - case JSJAC_JINGLE_ACTION_TRANSPORT_REPLACE: - self.send_transport_replace(stanza); break; - - default: - self.get_debug().log('[JSJaCJingle] send > Unexpected error.', 1); - - return false; - } - } else if(type != JSJAC_JINGLE_STANZA_TYPE_RESULT) { - self.get_debug().log('[JSJaCJingle] send > Stanza type must either be set or result.', 1); - - return false; - } - - JSJAC_JINGLE_STORE_CONNECTION.send(stanza); - - return true; - } catch(e) { - self.get_debug().log('[JSJaCJingle] send > ' + e, 1); - } - - return false; - }; - - /** - * Handles a given Jingle stanza response - */ - self.handle = function(stanza) { - self.get_debug().log('[JSJaCJingle] handle', 4); - - try { - // Locked? - if(self.get_lock()) { - self.get_debug().log('[JSJaCJingle] handle > Cannot handle, resource locked. Please open another session or check WebRTC support.', 0); - return; - } - - // Defer? - if(JSJaCJingle_defer(function() { self.handle(stanza); })) { - self.get_debug().log('[JSJaCJingle] handle > Deferred (waiting for the library components to be initiated).', 0); - return; - } - - var id = stanza.getID(); - var type = stanza.getType(); - - if(id && type == JSJAC_JINGLE_STANZA_TYPE_RESULT) self._set_received_id(id); - - // Submit to custom handler - if(typeof self._get_handlers(type, id) == 'function') { - self.get_debug().log('[JSJaCJingle] handle > Submitted to custom handler.', 2); - - (self._get_handlers(type, id))(stanza); - self.unregister_handler(type, id); - - return; - } - - var jingle = self.util_stanza_jingle(stanza); - - // Don't handle non-Jingle stanzas there... - if(!jingle) return; - - var action = self.util_stanza_get_attribute(jingle, 'action'); - - // Don't handle action-less Jingle stanzas there... - if(!action) return; - - // Submit to registered handler - switch(action) { - case JSJAC_JINGLE_ACTION_CONTENT_ACCEPT: - self.handle_content_accept(stanza); break; - - case JSJAC_JINGLE_ACTION_CONTENT_ADD: - self.handle_content_add(stanza); break; - - case JSJAC_JINGLE_ACTION_CONTENT_MODIFY: - self.handle_content_modify(stanza); break; - - case JSJAC_JINGLE_ACTION_CONTENT_REJECT: - self.handle_content_reject(stanza); break; - - case JSJAC_JINGLE_ACTION_CONTENT_REMOVE: - self.handle_content_remove(stanza); break; - - case JSJAC_JINGLE_ACTION_DESCRIPTION_INFO: - self.handle_description_info(stanza); break; - - case JSJAC_JINGLE_ACTION_SECURITY_INFO: - self.handle_security_info(stanza); break; - - case JSJAC_JINGLE_ACTION_SESSION_ACCEPT: - self.handle_session_accept(stanza); break; - - case JSJAC_JINGLE_ACTION_SESSION_INFO: - self.handle_session_info(stanza); break; - - case JSJAC_JINGLE_ACTION_SESSION_INITIATE: - self.handle_session_initiate(stanza); break; - - case JSJAC_JINGLE_ACTION_SESSION_TERMINATE: - self.handle_session_terminate(stanza); break; - - case JSJAC_JINGLE_ACTION_TRANSPORT_ACCEPT: - self.handle_transport_accept(stanza); break; - - case JSJAC_JINGLE_ACTION_TRANSPORT_INFO: - self.handle_transport_info(stanza); break; - - case JSJAC_JINGLE_ACTION_TRANSPORT_REJECT: - self.handle_transport_reject(stanza); break; - - case JSJAC_JINGLE_ACTION_TRANSPORT_REPLACE: - self.handle_transport_replace(stanza); break; - } - } catch(e) { - self.get_debug().log('[JSJaCJingle] handle > ' + e, 1); - } - }; - - /** - * Mutes a Jingle session (local) - */ - self.mute = function(name) { - self.get_debug().log('[JSJaCJingle] mute', 4); - - try { - // Locked? - if(self.get_lock()) { - self.get_debug().log('[JSJaCJingle] mute > Cannot mute, resource locked. Please open another session or check WebRTC support.', 0); - return; - } - - // Defer? - if(JSJaCJingle_defer(function() { self.mute(name); })) { - self.get_debug().log('[JSJaCJingle] mute > Deferred (waiting for the library components to be initiated).', 0); - return; - } - - // Already muted? - if(self.get_mute(name)) { - self.get_debug().log('[JSJaCJingle] mute > Resource already muted.', 0); - return; - } - - self._peer_sound(false); - self._set_mute(name, true); - - self.send(JSJAC_JINGLE_STANZA_TYPE_SET, { action: JSJAC_JINGLE_ACTION_SESSION_INFO, info: JSJAC_JINGLE_SESSION_INFO_MUTE, name: name }); - } catch(e) { - self.get_debug().log('[JSJaCJingle] mute > ' + e, 1); - } - }; - - /** - * Unmutes a Jingle session (local) - */ - self.unmute = function(name) { - self.get_debug().log('[JSJaCJingle] unmute', 4); - - try { - // Locked? - if(self.get_lock()) { - self.get_debug().log('[JSJaCJingle] unmute > Cannot unmute, resource locked. Please open another session or check WebRTC support.', 0); - return; - } - - // Defer? - if(JSJaCJingle_defer(function() { self.unmute(name); })) { - self.get_debug().log('[JSJaCJingle] unmute > Deferred (waiting for the library components to be initiated).', 0); - return; - } - - // Already unmute? - if(!self.get_mute(name)) { - self.get_debug().log('[JSJaCJingle] unmute > Resource already unmuted.', 0); - return; - } - - self._peer_sound(true); - self._set_mute(name, false); - - self.send(JSJAC_JINGLE_STANZA_TYPE_SET, { action: JSJAC_JINGLE_ACTION_SESSION_INFO, info: JSJAC_JINGLE_SESSION_INFO_UNMUTE, name: name }); - } catch(e) { - self.get_debug().log('[JSJaCJingle] unmute > ' + e, 1); - } - }; - - /** - * Toggles media type in a Jingle session - */ - self.media = function(media) { - /* DEV: don't expect this to work as of now! */ - - self.get_debug().log('[JSJaCJingle] media', 4); - - try { - // Locked? - if(self.get_lock()) { - self.get_debug().log('[JSJaCJingle] media > Cannot change media, resource locked. Please open another session or check WebRTC support.', 0); - return; - } - - // Defer? - if(JSJaCJingle_defer(function() { self.media(media); })) { - self.get_debug().log('[JSJaCJingle] media > Deferred (waiting for the library components to be initiated).', 0); - return; - } - - // Toggle media? - if(!media) - media = (self.get_media() == JSJAC_JINGLE_MEDIA_VIDEO) ? JSJAC_JINGLE_MEDIA_AUDIO : JSJAC_JINGLE_MEDIA_VIDEO; - - // Media unknown? - if(!(media in JSJAC_JINGLE_MEDIAS)) { - self.get_debug().log('[JSJaCJingle] media > No media provided or media unsupported (media: ' + media + ').', 0); - return; - } - - // Already using provided media? - if(self.get_media() == media) { - self.get_debug().log('[JSJaCJingle] media > Resource already using this media (media: ' + media + ').', 0); - return; - } - - // Switch locked for now? (another one is being processed) - if(self.get_media_busy()) { - self.get_debug().log('[JSJaCJingle] media > Resource already busy switching media (busy: ' + self.get_media() + ', media: ' + media + ').', 0); - return; - } - - self.get_debug().log('[JSJaCJingle] media > Changing media to: ' + media + '...', 2); - - // Store new media - self._set_media(media); - self._set_media_busy(true); - - // Toggle video mode (add/remove) - if(media == JSJAC_JINGLE_MEDIA_VIDEO) { - // TODO: the flow is something like that... - /*self._peer_get_user_media(function() { - self._peer_connection_create(function() { - self.get_debug().log('[JSJaCJingle] media > Ready to change media (to: ' + media + ').', 2); - - // 'content-add' >> video - // TODO: restart video stream configuration - - // WARNING: only change get user media, DO NOT TOUCH THE STREAM THING (don't stop active stream as it's flowing!!) - - self.send(JSJAC_JINGLE_STANZA_TYPE_SET, { action: JSJAC_JINGLE_ACTION_CONTENT_ADD, name: JSJAC_JINGLE_MEDIA_VIDEO }); - }) - });*/ - } else { - // TODO: the flow is something like that... - /*self._peer_get_user_media(function() { - self._peer_connection_create(function() { - self.get_debug().log('[JSJaCJingle] media > Ready to change media (to: ' + media + ').', 2); - - // 'content-remove' >> video - // TODO: remove video stream configuration - - // WARNING: only change get user media, DO NOT TOUCH THE STREAM THING (don't stop active stream as it's flowing!!) - // here, only stop the video stream, do not touch the audio stream - - self.send(JSJAC_JINGLE_STANZA_TYPE_SET, { action: JSJAC_JINGLE_ACTION_CONTENT_REMOVE, name: JSJAC_JINGLE_MEDIA_VIDEO }); - }) - });*/ - } - } catch(e) { - self.get_debug().log('[JSJaCJingle] media > ' + e, 1); - } - }; - - /** - * Registers a given handler on a given Jingle stanza - */ - self.register_handler = function(type, id, fn) { - self.get_debug().log('[JSJaCJingle] register_handler', 4); - - try { - type = type || JSJAC_JINGLE_STANZA_TYPE_ALL; - - if(typeof fn !== 'function') { - self.get_debug().log('[JSJaCJingle] register_handler > fn parameter not passed or not a function!', 1); - return false; - } - - if(id) { - self._set_handlers(type, id, fn); - - self.get_debug().log('[JSJaCJingle] register_handler > Registered handler for id: ' + id + ' and type: ' + type, 3); - return true; - } else { - self.get_debug().log('[JSJaCJingle] register_handler > Could not register handler (no ID).', 1); - return false; - } - } catch(e) { - self.get_debug().log('[JSJaCJingle] register_handler > ' + e, 1); - } - - return false; - }; - - /** - * Unregisters the given handler on a given Jingle stanza - */ - self.unregister_handler = function(type, id) { - self.get_debug().log('[JSJaCJingle] unregister_handler', 4); - - try { - type = type || JSJAC_JINGLE_STANZA_TYPE_ALL; - - if(type in self._handlers && id in self._handlers[type]) { - delete self._handlers[type][id]; - - self.get_debug().log('[JSJaCJingle] unregister_handler > Unregistered handler for id: ' + id + ' and type: ' + type, 3); - return true; - } else { - self.get_debug().log('[JSJaCJingle] unregister_handler > Could not unregister handler with id: ' + id + ' and type: ' + type + ' (not found).', 2); - return false; - } - } catch(e) { - self.get_debug().log('[JSJaCJingle] unregister_handler > ' + e, 1); - } - - return false; - }; - - /** - * Registers a view element - */ - self.register_view = function(type, view) { - self.get_debug().log('[JSJaCJingle] register_view', 4); - - try { - // Get view functions - var fn = self._util_map_register_view(type); - - if(fn.type == type) { - var i; - - // Check view is not already registered - for(i in (fn.view.get)()) { - if((fn.view.get)()[i] == view) { - self.get_debug().log('[JSJaCJingle] register_view > Could not register view of type: ' + type + ' (already registered).', 2); - return true; - } - } - - // Proceeds registration - (fn.view.set)(view); - - self._util_peer_stream_attach( - [view], - (fn.stream.get)(), - fn.mute - ); - - self.get_debug().log('[JSJaCJingle] register_view > Registered view of type: ' + type, 3); - - return true; - } else { - self.get_debug().log('[JSJaCJingle] register_view > Could not register view of type: ' + type + ' (type unknown).', 1); - return false; - } - } catch(e) { - self.get_debug().log('[JSJaCJingle] register_view > ' + e, 1); - } - - return false; - }; - - /** - * Unregisters a view element - */ - self.unregister_view = function(type, view) { - self.get_debug().log('[JSJaCJingle] unregister_view', 4); - - try { - // Get view functions - var fn = self._util_map_unregister_view(type); - - if(fn.type == type) { - var i; - - // Check view is registered - for(i in (fn.view.get)()) { - if((fn.view.get)()[i] == view) { - // Proceeds un-registration - self._util_peer_stream_detach( - [view] - ); - - self.util_array_remove_value( - (fn.view.get)(), - view - ); - - self.get_debug().log('[JSJaCJingle] unregister_view > Unregistered view of type: ' + type, 3); - return true; - } - } - - self.get_debug().log('[JSJaCJingle] unregister_view > Could not unregister view of type: ' + type + ' (not found).', 2); - return true; - } else { - self.get_debug().log('[JSJaCJingle] unregister_view > Could not unregister view of type: ' + type + ' (type unknown).', 1); - return false; - } - } catch(e) { - self.get_debug().log('[JSJaCJingle] unregister_view > ' + e, 1); - } - - return false; - }; - - - - /** - * JSJSAC JINGLE SENDERS - */ - - /** - * Sends the Jingle content accept - */ - self.send_content_accept = function(stanza) { - self.get_debug().log('[JSJaCJingle] send_content_accept', 4); - - try { - // TODO: remove from remote 'content-add' queue - // TODO: reprocess content_local/content_remote - - // Not implemented for now - self.get_debug().log('[JSJaCJingle] send_content_accept > Feature not implemented!', 0); - } catch(e) { - self.get_debug().log('[JSJaCJingle] send_content_accept > ' + e, 1); - } - }; - - /** - * Sends the Jingle content add - */ - self.send_content_add = function(stanza) { - self.get_debug().log('[JSJaCJingle] send_content_add', 4); - - try { - // TODO: push to local 'content-add' queue - - // Not implemented for now - self.get_debug().log('[JSJaCJingle] send_content_add > Feature not implemented!', 0); - } catch(e) { - self.get_debug().log('[JSJaCJingle] send_content_add > ' + e, 1); - } - }; - - /** - * Sends the Jingle content modify - */ - self.send_content_modify = function(stanza) { - self.get_debug().log('[JSJaCJingle] send_content_modify', 4); - - try { - // TODO: push to local 'content-modify' queue - - // Not implemented for now - self.get_debug().log('[JSJaCJingle] send_content_modify > Feature not implemented!', 0); - } catch(e) { - self.get_debug().log('[JSJaCJingle] send_content_modify > ' + e, 1); - } - }; - - /** - * Sends the Jingle content reject - */ - self.send_content_reject = function(stanza) { - self.get_debug().log('[JSJaCJingle] send_content_reject', 4); - - try { - // TODO: remove from remote 'content-add' queue - - // Not implemented for now - self.get_debug().log('[JSJaCJingle] send_content_reject > Feature not implemented!', 0); - } catch(e) { - self.get_debug().log('[JSJaCJingle] send_content_reject > ' + e, 1); - } - }; - - /** - * Sends the Jingle content remove - */ - self.send_content_remove = function(stanza) { - self.get_debug().log('[JSJaCJingle] send_content_remove', 4); - - try { - // TODO: add to local 'content-remove' queue - - // Not implemented for now - self.get_debug().log('[JSJaCJingle] send_content_remove > Feature not implemented!', 0); - } catch(e) { - self.get_debug().log('[JSJaCJingle] send_content_remove > ' + e, 1); - } - }; - - /** - * Sends the Jingle description info - */ - self.send_description_info = function(stanza) { - self.get_debug().log('[JSJaCJingle] send_description_info', 4); - - try { - // Not implemented for now - self.get_debug().log('[JSJaCJingle] send_description_info > Feature not implemented!', 0); - } catch(e) { - self.get_debug().log('[JSJaCJingle] send_description_info > ' + e, 1); - } - }; - - /** - * Sends the Jingle security info - */ - self.send_security_info = function(stanza) { - self.get_debug().log('[JSJaCJingle] send_security_info', 4); - - try { - // Not implemented for now - self.get_debug().log('[JSJaCJingle] send_security_info > Feature not implemented!', 0); - } catch(e) { - self.get_debug().log('[JSJaCJingle] send_security_info > ' + e, 1); - } - }; - - /** - * Sends the Jingle session accept - */ - self.send_session_accept = function(stanza, args) { - self.get_debug().log('[JSJaCJingle] send_session_accept', 4); - - try { - if(self.get_status() != JSJAC_JINGLE_STATUS_ACCEPTING) { - self.get_debug().log('[JSJaCJingle] send_session_accept > Cannot send accept stanza, resource not accepting (status: ' + self.get_status() + ').', 0); - self.send_error(stanza, JSJAC_JINGLE_ERROR_OUT_OF_ORDER); - return; - } - - if(!args) { - self.get_debug().log('[JSJaCJingle] send_session_accept > Argument not provided.', 1); - return; - } - - // Build Jingle stanza - var jingle = self._util_stanza_generate_jingle(stanza, { - 'action' : JSJAC_JINGLE_ACTION_SESSION_ACCEPT, - 'responder' : self.get_responder() - }); - - self._util_stanza_generate_content_local(stanza, jingle); - self._util_stanza_generate_group_local(stanza, jingle); - - // Schedule success - self.register_handler(JSJAC_JINGLE_STANZA_TYPE_RESULT, args.id, function(stanza) { - (self._get_session_accept_success())(self, stanza); - self.handle_session_accept_success(stanza); - }); - - // Schedule error timeout - self.util_stanza_timeout(JSJAC_JINGLE_STANZA_TYPE_RESULT, args.id, { - external: self._get_session_accept_error(), - internal: self.handle_session_accept_error - }); - - self.get_debug().log('[JSJaCJingle] send_session_accept > Sent.', 4); - } catch(e) { - self.get_debug().log('[JSJaCJingle] send_session_accept > ' + e, 1); - } - }; - - /** - * Sends the Jingle session info - */ - self.send_session_info = function(stanza, args) { - self.get_debug().log('[JSJaCJingle] send_session_info', 4); - - try { - if(!args) { - self.get_debug().log('[JSJaCJingle] send_session_info > Argument not provided.', 1); - return; - } - - // Filter info - args.info = args.info || JSJAC_JINGLE_SESSION_INFO_ACTIVE; - - // Build Jingle stanza - var jingle = self._util_stanza_generate_jingle(stanza, { - 'action' : JSJAC_JINGLE_ACTION_SESSION_INFO, - 'initiator' : self.get_initiator() - }); - - self._util_stanza_generate_session_info(stanza, jingle, args); - - // Schedule success - self.register_handler(JSJAC_JINGLE_STANZA_TYPE_RESULT, args.id, function(stanza) { - (self._get_session_info_success())(self, stanza); - self.handle_session_info_success(stanza); - }); - - // Schedule error timeout - self.util_stanza_timeout(JSJAC_JINGLE_STANZA_TYPE_RESULT, args.id, { - external: self._get_session_info_error(), - internal: self.handle_session_info_error - }); - - self.get_debug().log('[JSJaCJingle] send_session_info > Sent (name: ' + args.info + ').', 2); - } catch(e) { - self.get_debug().log('[JSJaCJingle] send_session_info > ' + e, 1); - } - }; - - /** - * Sends the Jingle session initiate - */ - self.send_session_initiate = function(stanza, args) { - self.get_debug().log('[JSJaCJingle] send_session_initiate', 4); - - try { - if(self.get_status() != JSJAC_JINGLE_STATUS_INITIATING) { - self.get_debug().log('[JSJaCJingle] send_session_initiate > Cannot send initiate stanza, resource not initiating (status: ' + self.get_status() + ').', 0); - return; - } - - if(!args) { - self.get_debug().log('[JSJaCJingle] send_session_initiate > Argument not provided.', 1); - return; - } - - // Build Jingle stanza - var jingle = self._util_stanza_generate_jingle(stanza, { - 'action' : JSJAC_JINGLE_ACTION_SESSION_INITIATE, - 'initiator' : self.get_initiator() - }); - - self._util_stanza_generate_content_local(stanza, jingle); - self._util_stanza_generate_group_local(stanza, jingle); - - // Schedule success - self.register_handler(JSJAC_JINGLE_STANZA_TYPE_RESULT, args.id, function(stanza) { - (self._get_session_initiate_success())(self, stanza); - self.handle_session_initiate_success(stanza); - }); - - // Schedule error timeout - self.util_stanza_timeout(JSJAC_JINGLE_STANZA_TYPE_RESULT, args.id, { - external: self._get_session_initiate_error(), - internal: self.handle_session_initiate_error - }); - - self.get_debug().log('[JSJaCJingle] send_session_initiate > Sent.', 2); - } catch(e) { - self.get_debug().log('[JSJaCJingle] send_session_initiate > ' + e, 1); - } - }; - - /** - * Sends the Jingle session terminate - */ - self.send_session_terminate = function(stanza, args) { - self.get_debug().log('[JSJaCJingle] send_session_terminate', 4); - - try { - if(self.get_status() != JSJAC_JINGLE_STATUS_TERMINATING) { - self.get_debug().log('[JSJaCJingle] send_session_terminate > Cannot send terminate stanza, resource not terminating (status: ' + self.get_status() + ').', 0); - return; - } - - if(!args) { - self.get_debug().log('[JSJaCJingle] send_session_terminate > Argument not provided.', 1); - return; - } - - // Filter reason - args.reason = args.reason || JSJAC_JINGLE_REASON_SUCCESS; - - // Store terminate reason - self._set_reason(args.reason); - - // Build terminate stanza - var jingle = self._util_stanza_generate_jingle(stanza, { - 'action': JSJAC_JINGLE_ACTION_SESSION_TERMINATE - }); - - var jingle_reason = jingle.appendChild(stanza.buildNode('reason', {'xmlns': NS_JINGLE})); - jingle_reason.appendChild(stanza.buildNode(args.reason, {'xmlns': NS_JINGLE})); - - // Schedule success - self.register_handler(JSJAC_JINGLE_STANZA_TYPE_RESULT, args.id, function(stanza) { - (self._get_session_terminate_success())(self, stanza); - self.handle_session_terminate_success(stanza); - }); - - // Schedule error timeout - self.util_stanza_timeout(JSJAC_JINGLE_STANZA_TYPE_RESULT, args.id, { - external: self._get_session_terminate_error(), - internal: self.handle_session_terminate_error - }); - - self.get_debug().log('[JSJaCJingle] send_session_terminate > Sent (reason: ' + args.reason + ').', 2); - } catch(e) { - self.get_debug().log('[JSJaCJingle] send_session_terminate > ' + e, 1); - } - }; - - /** - * Sends the Jingle transport accept - */ - self.send_transport_accept = function(stanza) { - self.get_debug().log('[JSJaCJingle] send_transport_accept', 4); - - try { - // Not implemented for now - self.get_debug().log('[JSJaCJingle] send_transport_accept > Feature not implemented!', 0); - } catch(e) { - self.get_debug().log('[JSJaCJingle] send_transport_accept > ' + e, 1); - } - }; - - /** - * Sends the Jingle transport info - */ - self.send_transport_info = function(stanza, args) { - self.get_debug().log('[JSJaCJingle] send_transport_info', 4); - - try { - if(self.get_status() != JSJAC_JINGLE_STATUS_INITIATED && self.get_status() != JSJAC_JINGLE_STATUS_ACCEPTING && self.get_status() != JSJAC_JINGLE_STATUS_ACCEPTED) { - self.get_debug().log('[JSJaCJingle] send_transport_info > Cannot send transport info, resource not initiated, nor accepting, nor accepted (status: ' + self.get_status() + ').', 0); - return; - } - - if(!args) { - self.get_debug().log('[JSJaCJingle] send_transport_info > Argument not provided.', 1); - return; - } - - if(self.util_object_length(self._get_candidates_queue_local()) === 0) { - self.get_debug().log('[JSJaCJingle] send_transport_info > No local candidate in queue.', 1); - return; - } - - // Build Jingle stanza - var jingle = self._util_stanza_generate_jingle(stanza, { - 'action' : JSJAC_JINGLE_ACTION_TRANSPORT_INFO, - 'initiator' : self.get_initiator() - }); - - // Build queue content - var cur_name; - var content_queue_local = {}; - - for(cur_name in self.get_name()) { - content_queue_local[cur_name] = self._util_generate_content( - self.get_creator(cur_name), - cur_name, - self.get_senders(cur_name), - self._get_payloads_local(cur_name), - self._get_candidates_queue_local(cur_name) - ); - } - - self._util_stanza_generate_content_local(stanza, jingle, content_queue_local); - self._util_stanza_generate_group_local(stanza, jingle); - - // Schedule success - self.register_handler(JSJAC_JINGLE_STANZA_TYPE_RESULT, args.id, function(stanza) { - self.handle_transport_info_success(stanza); - }); - - // Schedule error timeout - self.util_stanza_timeout(JSJAC_JINGLE_STANZA_TYPE_RESULT, args.id, { - internal: self.handle_transport_info_error - }); - - self.get_debug().log('[JSJaCJingle] send_transport_info > Sent.', 2); - } catch(e) { - self.get_debug().log('[JSJaCJingle] send_transport_info > ' + e, 1); - } - }; - - /** - * Sends the Jingle transport reject - */ - self.send_transport_reject = function(stanza) { - self.get_debug().log('[JSJaCJingle] send_transport_reject', 4); - - try { - // Not implemented for now - self.get_debug().log('[JSJaCJingle] send_transport_reject > Feature not implemented!', 0); - } catch(e) { - self.get_debug().log('[JSJaCJingle] send_transport_reject > ' + e, 1); - } - }; - - /** - * Sends the Jingle transport replace - */ - self.send_transport_replace = function(stanza) { - self.get_debug().log('[JSJaCJingle] send_transport_replace', 4); - - try { - // Not implemented for now - self.get_debug().log('[JSJaCJingle] send_transport_replace > Feature not implemented!', 0); - } catch(e) { - self.get_debug().log('[JSJaCJingle] send_transport_replace > ' + e, 1); - } - }; - - /** - * Sends the Jingle transport replace - */ - self.send_error = function(stanza, error) { - self.get_debug().log('[JSJaCJingle] send_error', 4); - - try { - // Assert - if(!('type' in error)) { - self.get_debug().log('[JSJaCJingle] send_error > Type unknown.', 1); - return; - } - - if('jingle' in error && !(error.jingle in JSJAC_JINGLE_ERRORS)) { - self.get_debug().log('[JSJaCJingle] send_error > Jingle condition unknown (' + error.jingle + ').', 1); - return; - } - - if('xmpp' in error && !(error.xmpp in XMPP_ERRORS)) { - self.get_debug().log('[JSJaCJingle] send_error > XMPP condition unknown (' + error.xmpp + ').', 1); - return; - } - - var stanza_error = new JSJaCIQ(); - - stanza_error.setType('error'); - stanza_error.setID(stanza.getID()); - stanza_error.setTo(self.get_to()); - - var error_node = stanza_error.getNode().appendChild(stanza_error.buildNode('error', {'xmlns': NS_CLIENT, 'type': error.type})); - - if('xmpp' in error) error_node.appendChild(stanza_error.buildNode(error.xmpp, { 'xmlns': NS_STANZAS })); - if('jingle' in error) error_node.appendChild(stanza_error.buildNode(error.jingle, { 'xmlns': NS_JINGLE_ERRORS })); - - JSJAC_JINGLE_STORE_CONNECTION.send(stanza_error); - - self.get_debug().log('[JSJaCJingle] send_error > Sent: ' + (error.jingle || error.xmpp), 2); - } catch(e) { - self.get_debug().log('[JSJaCJingle] send_error > ' + e, 1); - } - }; - - - - /** - * JSJSAC JINGLE HANDLERS - */ - - /** - * Handles the Jingle content accept - * @param {JSJaCPacket} stanza Jingle handled stanza - */ - self.handle_content_accept = function(stanza) { - self.get_debug().log('[JSJaCJingle] handle_content_accept', 4); - - try { - // TODO: start to flow accepted stream - // TODO: remove accepted content from local 'content-add' queue - // TODO: reprocess content_local/content_remote - - // Not implemented for now - self.send_error(stanza, XMPP_ERROR_FEATURE_NOT_IMPLEMENTED); - } catch(e) { - self.get_debug().log('[JSJaCJingle] handle_content_accept > ' + e, 1); - } - }; - - /** - * Handles the Jingle content add - * @param {JSJaCPacket} stanza Jingle handled stanza - */ - self.handle_content_add = function(stanza) { - self.get_debug().log('[JSJaCJingle] handle_content_add', 4); - - try { - // TODO: request the user to start this content (need a custom handler) - // on accept: send content-accept - // TODO: push to remote 'content-add' queue - // TODO: reprocess content_local/content_remote - - // Not implemented for now - self.send_error(stanza, XMPP_ERROR_FEATURE_NOT_IMPLEMENTED); - } catch(e) { - self.get_debug().log('[JSJaCJingle] handle_content_add > ' + e, 1); - } - }; - - /** - * Handles the Jingle content modify - * @param {JSJaCPacket} stanza Jingle handled stanza - */ - self.handle_content_modify = function(stanza) { - self.get_debug().log('[JSJaCJingle] handle_content_modify', 4); - - try { - // TODO: change 'senders' value (direction of the stream) - // if(send:from_me): notify the user that media is requested - // if(unacceptable): terminate the session - // if(accepted): change local/remote SDP - // TODO: reprocess content_local/content_remote - - // Not implemented for now - self.send_error(stanza, XMPP_ERROR_FEATURE_NOT_IMPLEMENTED); - } catch(e) { - self.get_debug().log('[JSJaCJingle] handle_content_modify > ' + e, 1); - } - }; - - /** - * Handles the Jingle content reject - * @param {JSJaCPacket} stanza Jingle handled stanza - */ - self.handle_content_reject = function(stanza) { - self.get_debug().log('[JSJaCJingle] handle_content_reject', 4); - - try { - // TODO: remove rejected content from local 'content-add' queue - - // Not implemented for now - self.send_error(stanza, XMPP_ERROR_FEATURE_NOT_IMPLEMENTED); - } catch(e) { - self.get_debug().log('[JSJaCJingle] handle_content_reject > ' + e, 1); - } - }; - - /** - * Handles the Jingle content remove - * @param {JSJaCPacket} stanza Jingle handled stanza - */ - self.handle_content_remove = function(stanza) { - self.get_debug().log('[JSJaCJingle] handle_content_remove', 4); - - try { - // TODO: stop flowing removed stream - // TODO: reprocess content_local/content_remote - - // Not implemented for now - self.send_error(stanza, XMPP_ERROR_FEATURE_NOT_IMPLEMENTED); - } catch(e) { - self.get_debug().log('[JSJaCJingle] handle_content_remove > ' + e, 1); - } - }; - - /** - * Handles the Jingle description info - * @param {JSJaCPacket} stanza Jingle handled stanza - */ - self.handle_description_info = function(stanza) { - self.get_debug().log('[JSJaCJingle] handle_description_info', 4); - - try { - // Not implemented for now - self.send_error(stanza, XMPP_ERROR_FEATURE_NOT_IMPLEMENTED); - } catch(e) { - self.get_debug().log('[JSJaCJingle] handle_description_info > ' + e, 1); - } - }; - - /** - * Handles the Jingle security info - * @param {JSJaCPacket} stanza Jingle handled stanza - */ - self.handle_security_info = function(stanza) { - self.get_debug().log('[JSJaCJingle] handle_security_info', 4); - - try { - // Not implemented for now - self.send_error(stanza, XMPP_ERROR_FEATURE_NOT_IMPLEMENTED); - } catch(e) { - self.get_debug().log('[JSJaCJingle] handle_security_info > ' + e, 1); - } - }; - - /** - * Handles the Jingle session accept - * @param {JSJaCPacket} stanza Jingle handled stanza - */ - self.handle_session_accept = function(stanza) { - self.get_debug().log('[JSJaCJingle] handle_session_accept', 4); - - try { - // Security preconditions - if(!self.util_stanza_safe(stanza)) { - self.get_debug().log('[JSJaCJingle] handle_session_accept > Dropped unsafe stanza.', 0); - - self.send_error(stanza, JSJAC_JINGLE_ERROR_UNKNOWN_SESSION); - return; - } - - // Can now safely dispatch the stanza - switch(stanza.getType()) { - case JSJAC_JINGLE_STANZA_TYPE_RESULT: - (self._get_session_accept_success())(self, stanza); - self.handle_session_accept_success(stanza); - - break; - - case 'error': - (self._get_session_accept_error())(self, stanza); - self.handle_session_accept_error(stanza); - - break; - - case JSJAC_JINGLE_STANZA_TYPE_SET: - // External handler must be set before internal one here... - (self._get_session_accept_request())(self, stanza); - self.handle_session_accept_request(stanza); - - break; - - default: - self.send_error(stanza, XMPP_ERROR_FEATURE_NOT_IMPLEMENTED); - } - } catch(e) { - self.get_debug().log('[JSJaCJingle] handle_session_accept > ' + e, 1); - } - }; - - /** - * Handles the Jingle session accept success - * @param {JSJaCPacket} stanza Jingle handled stanza - */ - self.handle_session_accept_success = function(stanza) { - self.get_debug().log('[JSJaCJingle] handle_session_accept_success', 4); - - try { - // Change session status - self._set_status(JSJAC_JINGLE_STATUS_ACCEPTED); - } catch(e) { - self.get_debug().log('[JSJaCJingle] handle_session_accept_success > ' + e, 1); - } - }; - - /** - * Handles the Jingle session accept error - * @param {JSJaCPacket} stanza Jingle handled stanza - */ - self.handle_session_accept_error = function(stanza) { - self.get_debug().log('[JSJaCJingle] handle_session_accept_error', 4); - - try { - // Terminate the session (timeout) - self.terminate(JSJAC_JINGLE_REASON_TIMEOUT); - } catch(e) { - self.get_debug().log('[JSJaCJingle] handle_session_accept_error > ' + e, 1); - } - }; - - /** - * Handles the Jingle session accept request - * @param {JSJaCPacket} stanza Jingle handled stanza - */ - self.handle_session_accept_request = function(stanza) { - self.get_debug().log('[JSJaCJingle] handle_session_accept_request', 4); - - try { - // Slot unavailable? - if(self.get_status() != JSJAC_JINGLE_STATUS_INITIATED) { - self.get_debug().log('[JSJaCJingle] handle_session_accept_request > Cannot handle, resource already accepted (status: ' + self.get_status() + ').', 0); - self.send_error(stanza, JSJAC_JINGLE_ERROR_OUT_OF_ORDER); - return; - } - - // Common vars - var i, cur_candidate_obj; - - // Change session status - self._set_status(JSJAC_JINGLE_STATUS_ACCEPTING); - - var rd_sid = self.util_stanza_sid(stanza); - - // Request is valid? - if(rd_sid && self.is_initiator() && self._util_stanza_parse_content(stanza)) { - // Handle additional data (optional) - self._util_stanza_parse_group(stanza); - - // Generate and store content data - self._util_build_content_remote(); - - // Trigger accept success callback - (self._get_session_accept_success())(self, stanza); - self.handle_session_accept_success(stanza); - - var sdp_remote = self._util_sdp_generate( - WEBRTC_SDP_TYPE_ANSWER, - self._get_group_remote(), - self._get_payloads_remote(), - self._get_candidates_queue_remote() - ); - - if(self.get_sdp_trace()) self.get_debug().log('[JSJaCJingle] SDP (remote)' + '\n\n' + sdp_remote.description.sdp, 4); - - // Remote description - self._get_peer_connection().setRemoteDescription( - (new WEBRTC_SESSION_DESCRIPTION(sdp_remote.description)), - - function() { - // Success (descriptions are compatible) - }, - - function(e) { - if(self.get_sdp_trace()) self.get_debug().log('[JSJaCJingle] SDP (remote:error)' + '\n\n' + (e.message || e.name || 'Unknown error'), 4); - - // Error (descriptions are incompatible) - self.terminate(JSJAC_JINGLE_REASON_INCOMPATIBLE_PARAMETERS); - } - ); - - // ICE candidates - for(i in sdp_remote.candidates) { - cur_candidate_obj = sdp_remote.candidates[i]; - - self._get_peer_connection().addIceCandidate( - new WEBRTC_ICE_CANDIDATE({ - sdpMLineIndex : cur_candidate_obj.id, - candidate : cur_candidate_obj.candidate - }) - ); - } - - // Empty the unapplied candidates queue - self._set_candidates_queue_remote(null); - - // Success reply - self.send(JSJAC_JINGLE_STANZA_TYPE_RESULT, { id: stanza.getID() }); - } else { - // Trigger accept error callback - (self._get_session_accept_error())(self, stanza); - self.handle_session_accept_error(stanza); - - // Send error reply - self.send_error(stanza, XMPP_ERROR_BAD_REQUEST); - - self.get_debug().log('[JSJaCJingle] handle_session_accept_request > Error.', 1); - } - } catch(e) { - self.get_debug().log('[JSJaCJingle] handle_session_accept_request > ' + e, 1); - } - }; - - /** - * Handles the Jingle session info - * @param {JSJaCPacket} stanza Jingle handled stanza - */ - self.handle_session_info = function(stanza) { - self.get_debug().log('[JSJaCJingle] handle_session_info', 4); - - try { - // Security preconditions - if(!self.util_stanza_safe(stanza)) { - self.get_debug().log('[JSJaCJingle] handle_session_info > Dropped unsafe stanza.', 0); - - self.send_error(stanza, JSJAC_JINGLE_ERROR_UNKNOWN_SESSION); - return; - } - - // Can now safely dispatch the stanza - switch(stanza.getType()) { - case JSJAC_JINGLE_STANZA_TYPE_RESULT: - (self._get_session_info_success())(self, stanza); - self.handle_session_info_success(stanza); - - break; - - case 'error': - (self._get_session_info_error())(self, stanza); - self.handle_session_info_error(stanza); - - break; - - case JSJAC_JINGLE_STANZA_TYPE_SET: - (self._get_session_info_request())(self, stanza); - self.handle_session_info_request(stanza); - - break; - - default: - self.send_error(stanza, XMPP_ERROR_FEATURE_NOT_IMPLEMENTED); - } - } catch(e) { - self.get_debug().log('[JSJaCJingle] handle_session_info > ' + e, 1); - } - }; - - /** - * Handles the Jingle session info success - * @param {JSJaCPacket} stanza Jingle handled stanza - */ - self.handle_session_info_success = function(stanza) { - self.get_debug().log('[JSJaCJingle] handle_session_info_success', 4); - }; - - /** - * Handles the Jingle session info error - * @param {JSJaCPacket} stanza Jingle handled stanza - */ - self.handle_session_info_error = function(stanza) { - self.get_debug().log('[JSJaCJingle] handle_session_info_error', 4); - }; - - /** - * Handles the Jingle session info request - * @param {JSJaCPacket} stanza Jingle handled stanza - */ - self.handle_session_info_request = function(stanza) { - self.get_debug().log('[JSJaCJingle] handle_session_info_request', 4); - - try { - // Parse stanza - var info_name = self.util_stanza_session_info(stanza); - var info_result = false; - - switch(info_name) { - case JSJAC_JINGLE_SESSION_INFO_ACTIVE: - case JSJAC_JINGLE_SESSION_INFO_RINGING: - case JSJAC_JINGLE_SESSION_INFO_MUTE: - case JSJAC_JINGLE_SESSION_INFO_UNMUTE: - info_result = true; break; - } - - if(info_result) { - self.get_debug().log('[JSJaCJingle] handle_session_info_request > (name: ' + (info_name || 'undefined') + ').', 3); - - // Process info actions - self.send(JSJAC_JINGLE_STANZA_TYPE_RESULT, { id: stanza.getID() }); - - // Trigger info success custom callback - (self._get_session_info_success())(self, stanza); - self.handle_session_info_success(stanza); - } else { - self.get_debug().log('[JSJaCJingle] handle_session_info_request > Error (name: ' + (info_name || 'undefined') + ').', 1); - - // Send error reply - self.send_error(stanza, XMPP_ERROR_FEATURE_NOT_IMPLEMENTED); - - // Trigger info error custom callback - (self._get_session_info_error())(self, stanza); - self.handle_session_info_error(stanza); - } - } catch(e) { - self.get_debug().log('[JSJaCJingle] handle_session_info_request > ' + e, 1); - } - }; - - /** - * Handles the Jingle session initiate - * @param {JSJaCPacket} stanza Jingle handled stanza - */ - self.handle_session_initiate = function(stanza) { - self.get_debug().log('[JSJaCJingle] handle_session_initiate', 4); - - try { - switch(stanza.getType()) { - case JSJAC_JINGLE_STANZA_TYPE_RESULT: - (self._get_session_initiate_success())(self, stanza); - self.handle_session_initiate_success(stanza); - - break; - - case 'error': - (self._get_session_initiate_error())(self, stanza); - self.handle_session_initiate_error(stanza); - - break; - - case JSJAC_JINGLE_STANZA_TYPE_SET: - (self._get_session_initiate_request())(self, stanza); - self.handle_session_initiate_request(stanza); - - break; - - default: - self.send_error(stanza, XMPP_ERROR_FEATURE_NOT_IMPLEMENTED); - } - } catch(e) { - self.get_debug().log('[JSJaCJingle] handle_session_initiate > ' + e, 1); - } - }; - - /** - * Handles the Jingle session initiate success - * @param {JSJaCPacket} stanza Jingle handled stanza - */ - self.handle_session_initiate_success = function(stanza) { - self.get_debug().log('[JSJaCJingle] handle_session_initiate_success', 4); - - try { - // Change session status - self._set_status(JSJAC_JINGLE_STATUS_INITIATED); - } catch(e) { - self.get_debug().log('[JSJaCJingle] handle_session_initiate_success > ' + e, 1); - } - }; - - /** - * Handles the Jingle session initiate error - * @param {JSJaCPacket} stanza Jingle handled stanza - */ - self.handle_session_initiate_error = function(stanza) { - self.get_debug().log('[JSJaCJingle] handle_session_initiate_error', 4); - - try { - // Change session status - self._set_status(JSJAC_JINGLE_STATUS_INACTIVE); - - // Stop WebRTC - self._peer_stop(); - - // Lock session (cannot be used later) - self._set_lock(true); - } catch(e) { - self.get_debug().log('[JSJaCJingle] handle_session_initiate_error > ' + e, 1); - } - }; - - /** - * Handles the Jingle session initiate request - * @param {JSJaCPacket} stanza Jingle handled stanza - */ - self.handle_session_initiate_request = function(stanza) { - self.get_debug().log('[JSJaCJingle] handle_session_initiate_request', 4); - - try { - // Slot unavailable? - if(self.get_status() != JSJAC_JINGLE_STATUS_INACTIVE) { - self.get_debug().log('[JSJaCJingle] handle_session_initiate_request > Cannot handle, resource already initiated (status: ' + self.get_status() + ').', 0); - self.send_error(stanza, JSJAC_JINGLE_ERROR_OUT_OF_ORDER); - return; - } - - // Change session status - self._set_status(JSJAC_JINGLE_STATUS_INITIATING); - - // Common vars - var rd_from = self.util_stanza_from(stanza); - var rd_sid = self.util_stanza_sid(stanza); - - // Request is valid? - if(rd_sid && self._util_stanza_parse_content(stanza)) { - // Handle additional data (optional) - self._util_stanza_parse_group(stanza); - - // Set session values - self._set_sid(rd_sid); - self._set_to(rd_from); - self._set_initiator(rd_from); - self._set_responder(self.util_connection_jid()); - - // Register session to common router - JSJaCJingle_add(rd_sid, self); - - // Generate and store content data - self._util_build_content_remote(); - - // Video or audio-only session? - if(JSJAC_JINGLE_MEDIA_VIDEO in self._get_content_remote()) { - self._set_media(JSJAC_JINGLE_MEDIA_VIDEO); - } else if(JSJAC_JINGLE_MEDIA_AUDIO in self._get_content_remote()) { - self._set_media(JSJAC_JINGLE_MEDIA_AUDIO); - } else { - // Session initiation not done - (self._get_session_initiate_error())(self, stanza); - self.handle_session_initiate_error(stanza); - - // Error (no media is supported) - self.terminate(JSJAC_JINGLE_REASON_UNSUPPORTED_APPLICATIONS); - - self.get_debug().log('[JSJaCJingle] handle_session_initiate_request > Error (unsupported media).', 1); - return; - } - - // Session initiate done - (self._get_session_initiate_success())(self, stanza); - self.handle_session_initiate_success(stanza); - - self.send(JSJAC_JINGLE_STANZA_TYPE_RESULT, { id: stanza.getID() }); - } else { - // Session initiation not done - (self._get_session_initiate_error())(self, stanza); - self.handle_session_initiate_error(stanza); - - // Send error reply - self.send_error(stanza, XMPP_ERROR_BAD_REQUEST); - - self.get_debug().log('[JSJaCJingle] handle_session_initiate_request > Error (bad request).', 1); - } - } catch(e) { - self.get_debug().log('[JSJaCJingle] handle_session_initiate_request > ' + e, 1); - } - }; - - /** - * Handles the Jingle session terminate - * @param {JSJaCPacket} stanza Jingle handled stanza - */ - self.handle_session_terminate = function(stanza) { - self.get_debug().log('[JSJaCJingle] handle_session_terminate', 4); - - try { - var type = stanza.getType(); - - // Security preconditions - if(!self.util_stanza_safe(stanza)) { - self.get_debug().log('[JSJaCJingle] handle_session_terminate > Dropped unsafe stanza.', 0); - - self.send_error(stanza, JSJAC_JINGLE_ERROR_UNKNOWN_SESSION); - return; - } - - // Can now safely dispatch the stanza - switch(stanza.getType()) { - case JSJAC_JINGLE_STANZA_TYPE_RESULT: - (self._get_session_terminate_success())(self, stanza); - self.handle_session_terminate_success(stanza); - - break; - - case 'error': - (self._get_session_terminate_error())(self, stanza); - self.handle_session_terminate_error(stanza); - - break; - - case JSJAC_JINGLE_STANZA_TYPE_SET: - (self._get_session_terminate_request())(self, stanza); - self.handle_session_terminate_request(stanza); - - break; - - default: - self.send_error(stanza, XMPP_ERROR_FEATURE_NOT_IMPLEMENTED); - } - } catch(e) { - self.get_debug().log('[JSJaCJingle] handle_session_terminate > ' + e, 1); - } - }; - - /** - * Handles the Jingle session terminate success - * @param {JSJaCPacket} stanza Jingle handled stanza - */ - self.handle_session_terminate_success = function(stanza) { - self.get_debug().log('[JSJaCJingle] handle_session_terminate_success', 4); - - try { - // Change session status - self._set_status(JSJAC_JINGLE_STATUS_TERMINATED); - - // Stop WebRTC - self._peer_stop(); - } catch(e) { - self.get_debug().log('[JSJaCJingle] handle_session_terminate_success > ' + e, 1); - } - }; - - /** - * Handles the Jingle session terminate error - * @param {JSJaCPacket} stanza Jingle handled stanza - */ - self.handle_session_terminate_error = function(stanza) { - self.get_debug().log('[JSJaCJingle] handle_session_terminate_error', 4); - - try { - // Change session status - self._set_status(JSJAC_JINGLE_STATUS_TERMINATED); - - // Stop WebRTC - self._peer_stop(); - - // Lock session (cannot be used later) - self._set_lock(true); - - self.get_debug().log('[JSJaCJingle] handle_session_terminate_error > Forced session termination locally.', 0); - } catch(e) { - self.get_debug().log('[JSJaCJingle] handle_session_terminate_error > ' + e, 1); - } - }; - - /** - * Handles the Jingle session terminate request - * @param {JSJaCPacket} stanza Jingle handled stanza - */ - self.handle_session_terminate_request = function(stanza) { - self.get_debug().log('[JSJaCJingle] handle_session_terminate_request', 4); - - try { - // Slot unavailable? - if(self.get_status() == JSJAC_JINGLE_STATUS_INACTIVE || self.get_status() == JSJAC_JINGLE_STATUS_TERMINATED) { - self.get_debug().log('[JSJaCJingle] handle_session_terminate_request > Cannot handle, resource not active (status: ' + self.get_status() + ').', 0); - self.send_error(stanza, JSJAC_JINGLE_ERROR_OUT_OF_ORDER); - return; - } - - // Change session status - self._set_status(JSJAC_JINGLE_STATUS_TERMINATING); - - // Store termination reason - self._set_reason(self.util_stanza_terminate_reason(stanza)); - - // Trigger terminate success callbacks - (self._get_session_terminate_success())(self, stanza); - self.handle_session_terminate_success(stanza); - - // Process terminate actions - self.send(JSJAC_JINGLE_STANZA_TYPE_RESULT, { id: stanza.getID() }); - - self.get_debug().log('[JSJaCJingle] handle_session_terminate_request > (reason: ' + self.get_reason() + ').', 3); - } catch(e) { - self.get_debug().log('[JSJaCJingle] handle_session_terminate_request > ' + e, 1); - } - }; - - /** - * Handles the Jingle transport accept - * @param {JSJaCPacket} stanza Jingle handled stanza - */ - self.handle_transport_accept = function(stanza) { - self.get_debug().log('[JSJaCJingle] handle_transport_accept', 4); - - try { - // Not implemented for now - self.send_error(stanza, XMPP_ERROR_FEATURE_NOT_IMPLEMENTED); - } catch(e) { - self.get_debug().log('[JSJaCJingle] handle_content_accept > ' + e, 1); - } - }; - - /** - * Handles the Jingle transport info - * @param {JSJaCPacket} stanza Jingle handled stanza - */ - self.handle_transport_info = function(stanza) { - self.get_debug().log('[JSJaCJingle] handle_transport_info', 4); - - try { - // Slot unavailable? - if(self.get_status() != JSJAC_JINGLE_STATUS_INITIATED && self.get_status() != JSJAC_JINGLE_STATUS_ACCEPTING && self.get_status() != JSJAC_JINGLE_STATUS_ACCEPTED) { - self.get_debug().log('[JSJaCJingle] handle_transport_info > Cannot handle, resource not initiated, nor accepting, nor accepted (status: ' + self.get_status() + ').', 0); - self.send_error(stanza, JSJAC_JINGLE_ERROR_OUT_OF_ORDER); - return; - } - - // Common vars - var i, cur_candidate_obj; - - // Parse the incoming transport - var rd_sid = self.util_stanza_sid(stanza); - - // Request is valid? - if(rd_sid && self._util_stanza_parse_content(stanza)) { - // Handle additional data (optional) - // Still unsure if it is relevant to parse groups there... (are they allowed in such stanza?) - //self._util_stanza_parse_group(stanza); - - // Re-generate and store new content data - self._util_build_content_remote(); - - var sdp_candidates_remote = self._util_sdp_generate_candidates( - self._get_candidates_queue_remote() - ); - - // ICE candidates - for(i in sdp_candidates_remote) { - cur_candidate_obj = sdp_candidates_remote[i]; - - self._get_peer_connection().addIceCandidate( - new WEBRTC_ICE_CANDIDATE({ - sdpMLineIndex : cur_candidate_obj.id, - candidate : cur_candidate_obj.candidate - }) - ); - } - - // Empty the unapplied candidates queue - self._set_candidates_queue_remote(null); - - // Success reply - self.send(JSJAC_JINGLE_STANZA_TYPE_RESULT, { id: stanza.getID() }); - } else { - // Send error reply - self.send_error(stanza, XMPP_ERROR_BAD_REQUEST); - - self.get_debug().log('[JSJaCJingle] handle_transport_info > Error.', 1); - } - } catch(e) { - self.get_debug().log('[JSJaCJingle] handle_transport_info > ' + e, 1); - } - }; - - /** - * Handles the Jingle transport info success - * @param {JSJaCPacket} stanza Jingle handled stanza - */ - self.handle_transport_info_success = function(stanza) { - self.get_debug().log('[JSJaCJingle] handle_transport_info_success', 4); - }; - - /** - * Handles the Jingle transport info error - * @param {JSJaCPacket} stanza Jingle handled stanza - */ - self.handle_transport_info_error = function(stanza) { - self.get_debug().log('[JSJaCJingle] handle_transport_info_error', 4); - }; - - /** - * Handles the Jingle transport reject - * @param {JSJaCPacket} stanza Jingle handled stanza - */ - self.handle_transport_reject = function(stanza) { - self.get_debug().log('[JSJaCJingle] handle_transport_reject', 4); - - try { - // Not implemented for now - self.send_error(stanza, XMPP_ERROR_FEATURE_NOT_IMPLEMENTED); - } catch(e) { - self.get_debug().log('[JSJaCJingle] handle_transport_reject > ' + e, 1); - } - }; - - /** - * Handles the Jingle transport replace - * @param {JSJaCPacket} stanza Jingle handled stanza - */ - self.handle_transport_replace = function(stanza) { - self.get_debug().log('[JSJaCJingle] handle_transport_replace', 4); - - try { - // Not implemented for now - self.send_error(stanza, XMPP_ERROR_FEATURE_NOT_IMPLEMENTED); - } catch(e) { - self.get_debug().log('[JSJaCJingle] handle_transport_replace > ' + e, 1); - } - }; - - - - /** - * JSJSAC JINGLE GETTERS - */ - - /** - * @private - */ - self._get_session_initiate_pending = function() { - if(typeof self._session_initiate_pending == 'function') - return self._session_initiate_pending; - - return function() {}; - }; - - /** - * @private - */ - self._get_session_initiate_success = function() { - if(typeof self._session_initiate_success == 'function') - return self._session_initiate_success; - - return function(stanza) {}; - }; - - /** - * @private - */ - self._get_session_initiate_error = function() { - if(typeof self._session_initiate_error == 'function') - return self._session_initiate_error; - - return function(stanza) {}; - }; - - /** - * @private - */ - self._get_session_initiate_request = function() { - if(typeof self._session_initiate_request == 'function') - return self._session_initiate_request; - - return function(stanza) {}; - }; - - /** - * @private - */ - self._get_session_accept_pending = function() { - if(typeof self._session_accept_pending == 'function') - return self._session_accept_pending; - - return function() {}; - }; - - /** - * @private - */ - self._get_session_accept_success = function() { - if(typeof self._session_accept_success == 'function') - return self._session_accept_success; - - return function(stanza) {}; - }; - - /** - * @private - */ - self._get_session_accept_error = function() { - if(typeof self._session_accept_error == 'function') - return self._session_accept_error; - - return function(stanza) {}; - }; - - /** - * @private - */ - self._get_session_accept_request = function() { - if(typeof self._session_accept_request == 'function') - return self._session_accept_request; - - return function(stanza) {}; - }; - - /** - * @private - */ - self._get_session_info_success = function() { - if(typeof self._session_info_success == 'function') - return self._session_info_success; - - return function(stanza) {}; - }; - - /** - * @private - */ - self._get_session_info_error = function() { - if(typeof self._session_info_error == 'function') - return self._session_info_error; - - return function(stanza) {}; - }; - - /** - * @private - */ - self._get_session_info_request = function() { - if(typeof self._session_info_request == 'function') - return self._session_info_request; - - return function(stanza) {}; - }; - - /** - * @private - */ - self._get_session_terminate_pending = function() { - if(typeof self._session_terminate_pending == 'function') - return self._session_terminate_pending; - - return function() {}; - }; - - /** - * @private - */ - self._get_session_terminate_success = function() { - if(typeof self._session_terminate_success == 'function') - return self._session_terminate_success; - - return function(stanza) {}; - }; - - /** - * @private - */ - self._get_session_terminate_error = function() { - if(typeof self._session_terminate_error == 'function') - return self._session_terminate_error; - - return function(stanza) {}; - }; - - /** - * @private - */ - self._get_session_terminate_request = function() { - if(typeof self._session_terminate_request == 'function') - return self._session_terminate_request; - - return function(stanza) {}; - }; - - /** - * @private - */ - self._get_local_stream = function() { - return self._local_stream; - }; - - /** - * @private - */ - self._get_remote_stream = function() { - return self._remote_stream; - }; - - /** - * @private - */ - self._get_payloads_local = function(name) { - if(name) - return (name in self._payloads_local) ? self._payloads_local[name] : {}; - - return self._payloads_local; - }; - - /** - * @private - */ - self._get_group_local = function(semantics) { - if(semantics) - return (semantics in self._group_local) ? self._group_local[semantics] : {}; - - return self._group_local; - }; - - /** - * @private - */ - self._get_candidates_local = function(name) { - if(name) - return (name in self._candidates_local) ? self._candidates_local[name] : {}; - - return self._candidates_local; - }; - - /** - * @private - */ - self._get_candidates_queue_local = function(name) { - if(name) - return (name in self._candidates_queue_local) ? self._candidates_queue_local[name] : {}; - - return self._candidates_queue_local; - }; - - /** - * @private - */ - self._get_payloads_remote = function(name) { - if(name) - return (name in self._payloads_remote) ? self._payloads_remote[name] : {}; - - return self._payloads_remote; - }; - - /** - * @private - */ - self._get_group_remote = function(semantics) { - if(semantics) - return (semantics in self._group_remote) ? self._group_remote[semantics] : {}; - - return self._group_remote; - }; - - /** - * @private - */ - self._get_candidates_remote = function(name) { - if(name) - return (name in self._candidates_remote) ? self._candidates_remote[name] : []; - - return self._candidates_remote; - }; - - /** - * @private - */ - self._get_candidates_queue_remote = function(name) { - if(name) - return (name in self._candidates_queue_remote) ? self._candidates_queue_remote[name] : {}; - - return self._candidates_queue_remote; - }; - - /** - * @private - */ - self._get_content_local = function(name) { - if(name) - return (name in self._content_local) ? self._content_local[name] : {}; - - return self._content_local; - }; - - /** - * @private - */ - self._get_content_remote = function(name) { - if(name) - return (name in self._content_remote) ? self._content_remote[name] : {}; - - return self._content_remote; - }; - - /** - * @private - */ - self._get_handlers = function(type, id) { - type = type || JSJAC_JINGLE_STANZA_TYPE_ALL; - - if(id) { - if(type != JSJAC_JINGLE_STANZA_TYPE_ALL && type in self._handlers && typeof self._handlers[type][id] == 'function') - return self._handlers[type][id]; - - if(JSJAC_JINGLE_STANZA_TYPE_ALL in self._handlers && typeof self._handlers[JSJAC_JINGLE_STANZA_TYPE_ALL][id] == 'function') - return self._handlers[type][id]; - } - - return null; - }; - - /** - * @private - */ - self._get_peer_connection = function() { - return self._peer_connection; - }; - - /** - * @private - */ - self._get_id = function() { - return self._id; - }; - - /** - * @private - */ - self._get_id_pre = function() { - return JSJAC_JINGLE_STANZA_ID_PRE + '_' + (self.get_sid() || '0') + '_'; - }; - - /** - * @private - */ - self._get_id_new = function() { - var trans_id = self._get_id() + 1; - self._set_id(trans_id); - - return self._get_id_pre() + trans_id; - }; - - /** - * @private - */ - self._get_sent_id = function() { - return self._sent_id; - }; - - /** - * @private - */ - self._get_received_id = function() { - return self._received_id; - }; - - /** - * Gets the mute state - * @return mute value - * @type boolean - */ - self.get_mute = function(name) { - if(!name) name = '*'; - - return (name in self._mute) ? self._mute[name] : false; - }; - - /** - * Gets the lock value - * @return lock value - * @type boolean - */ - self.get_lock = function() { - return self._lock || !JSJAC_JINGLE_AVAILABLE; - }; - - /** - * Gets the media busy value - * @return media busy value - * @type boolean - */ - self.get_media_busy = function() { - return self._media_busy; - }; - - /** - * Gets the sid value - * @return sid value - * @type string - */ - self.get_sid = function() { - return self._sid; - }; - - /** - * Gets the status value - * @return status value - * @type string - */ - self.get_status = function() { - return self._status; - }; - - /** - * Gets the reason value - * @return reason value - * @type string - */ - self.get_reason = function() { - return self._reason; - }; - - /** - * Gets the to value - * @return to value - * @type string - */ - self.get_to = function() { - return self._to; - }; - - /** - * Gets the media value - * @return media value - * @type string - */ - self.get_media = function() { - return (self._media && self._media in JSJAC_JINGLE_MEDIAS) ? self._media : JSJAC_JINGLE_MEDIA_VIDEO; - }; - - /** - * Gets a list of medias in use - * @return media list - * @type object - */ - self.get_media_all = function() { - if(self.get_media() == JSJAC_JINGLE_MEDIA_AUDIO) - return [JSJAC_JINGLE_MEDIA_AUDIO]; - - return [JSJAC_JINGLE_MEDIA_AUDIO, JSJAC_JINGLE_MEDIA_VIDEO]; - }; - - /** - * Gets the video source value - * @return video source value - * @type string - */ - self.get_video_source = function() { - return (self._video_source && self._video_source in JSJAC_JINGLE_VIDEO_SOURCES) ? self._video_source : JSJAC_JINGLE_VIDEO_SOURCE_CAMERA; - }; - - /** - * Gets the resolution value - * @return resolution value - * @type string - */ - self.get_resolution = function() { - return self._resolution ? (self._resolution).toString() : null; - }; - - /** - * Gets the bandwidth value - * @return bandwidth value - * @type string - */ - self.get_bandwidth = function() { - return self._bandwidth ? (self._bandwidth).toString() : null; - }; - - /** - * Gets the fps value - * @return fps value - * @type string - */ - self.get_fps = function() { - return self._fps ? (self._fps).toString() : null; - }; - - /** - * Gets the name value - * @return name value - * @type string - */ - self.get_name = function(name) { - if(name) - return name in self._name; - - return self._name; - }; - - /** - * Gets the senders value - * @return senders value - * @type string - */ - self.get_senders = function(name) { - if(name) - return (name in self._senders) ? self._senders[name] : null; - - return self._senders; - }; - - /** - * Gets the creator value - * @return creator value - * @type string - */ - self.get_creator = function(name) { - if(name) - return (name in self._creator) ? self._creator[name] : null; - - return self._creator; - }; - - /** - * Gets the creator value (for this) - * @return creator value - * @type string - */ - self.get_creator_this = function(name) { - return self.get_responder() == self.get_to() ? JSJAC_JINGLE_CREATOR_INITIATOR : JSJAC_JINGLE_CREATOR_RESPONDER; - }; - - /** - * Gets the initiator value - * @return initiator value - * @type string - */ - self.get_initiator = function() { - return self._initiator; - }; - - /** - * Gets the response value - * @return response value - * @type string - */ - self.get_responder = function() { - return self._responder; - }; - - /** - * Gets the local_view value - * @return local_view value - * @type DOM - */ - self.get_local_view = function() { - return (typeof self._local_view == 'object') ? self._local_view : []; - }; - - /** - * Gets the remote_view value - * @return remote_view value - * @type DOM - */ - self.get_remote_view = function() { - return (typeof self._remote_view == 'object') ? self._remote_view : []; - }; - - /** - * Gets the STUN servers - * @return STUN servers - * @type object - */ - self.get_stun = function() { - return (typeof self._stun == 'object') ? self._stun : {}; - }; - - /** - * Gets the TURN servers - * @return TURN servers - * @type object - */ - self.get_turn = function() { - return (typeof self._turn == 'object') ? self._turn : {}; - }; - - /** - * Gets the SDP trace value - * @return SDP trace value - * @type JSJaCsdp_traceger - */ - self.get_sdp_trace = function() { - return (self._sdp_trace === true); - }; - - /** - * Gets the debug value - * @return debug value - * @type JSJaCDebugger - */ - self.get_debug = function() { - return self._debug; - }; - - - - /** - * JSJSAC JINGLE SETTERS - */ - - /** - * @private - */ - self._set_session_initiate_pending = function(session_initiate_pending) { - self._session_initiate_pending = session_initiate_pending; - }; - - /** - * @private - */ - self._set_initiate_success = function(initiate_success) { - self._session_initiate_success = initiate_success; - }; - - /** - * @private - */ - self._set_initiate_error = function(initiate_error) { - self._session_initiate_error = initiate_error; - }; - - /** - * @private - */ - self._set_initiate_request = function(initiate_request) { - self._session_initiate_request = initiate_request; - }; - - /** - * @private - */ - self._set_accept_pending = function(accept_pending) { - self._session_accept_pending = accept_pending; - }; - - /** - * @private - */ - self._set_accept_success = function(accept_success) { - self._session_accept_success = accept_success; - }; - - /** - * @private - */ - self._set_accept_error = function(accept_error) { - self._session_accept_error = accept_error; - }; - - /** - * @private - */ - self._set_accept_request = function(accept_request) { - self._session_accept_request = accept_request; - }; - - /** - * @private - */ - self._set_info_success = function(info_success) { - self._session_info_success = info_success; - }; - - /** - * @private - */ - self._set_info_error = function(info_error) { - self._session_info_error = info_error; - }; - - /** - * @private - */ - self._set_info_request = function(info_request) { - self._session_info_request = info_request; - }; - - /** - * @private - */ - self._set_terminate_pending = function(terminate_pending) { - self._session_terminate_pending = terminate_pending; - }; - - /** - * @private - */ - self._set_terminate_success = function(terminate_success) { - self._session_terminate_success = terminate_success; - }; - - /** - * @private - */ - self._set_terminate_error = function(terminate_error) { - self._session_terminate_error = terminate_error; - }; - - /** - * @private - */ - self._set_terminate_request = function(terminate_request) { - self._session_terminate_request = terminate_request; - }; - - /** - * @private - */ - self._set_local_stream = function(local_stream) { - try { - if(!local_stream && self._local_stream) { - (self._local_stream).stop(); - - self._util_peer_stream_detach( - self.get_local_view() - ); - } - - self._local_stream = local_stream; - - if(local_stream) { - self._util_peer_stream_attach( - self.get_local_view(), - self._get_local_stream(), - true - ); - } else { - self._util_peer_stream_detach( - self.get_local_view() - ); - } - } catch(e) { - self.get_debug().log('[JSJaCJingle] _set_local_stream > ' + e, 1); - } - }; - - /** - * @private - */ - self._set_remote_stream = function(remote_stream) { - try { - if(!remote_stream && self._remote_stream) { - self._util_peer_stream_detach( - self.get_remote_view() - ); - } - - self._remote_stream = remote_stream; - - if(remote_stream) { - self._util_peer_stream_attach( - self.get_remote_view(), - self._get_remote_stream(), - false - ); - } else { - self._util_peer_stream_detach( - self.get_remote_view() - ); - } - } catch(e) { - self.get_debug().log('[JSJaCJingle] _set_remote_stream > ' + e, 1); - } - }; - - /** - * @private - */ - self._set_local_view = function(local_view) { - if(typeof self._local_view !== 'object') - self._local_view = []; - - self._local_view.push(local_view); - }; - - /** - * @private - */ - self._set_remote_view = function(remote_view) { - if(typeof self._remote_view !== 'object') - self._remote_view = []; - - self._remote_view.push(remote_view); - }; - - /** - * @private - */ - self._set_payloads_local = function(name, payload_data) { - self._payloads_local[name] = payload_data; - }; - - /** - * @private - */ - self._set_group_local = function(semantics, group_data) { - self._group_local[semantics] = group_data; - }; - - /** - * @private - */ - self._set_candidates_local = function(name, candidate_data) { - if(!(name in self._candidates_local)) self._candidates_local[name] = []; - - (self._candidates_local[name]).push(candidate_data); - }; - - /** - * @private - */ - self._set_candidates_queue_local = function(name, candidate_data) { - try { - if(name === null) { - self._candidates_queue_local = {}; - } else { - if(!(name in self._candidates_queue_local)) self._candidates_queue_local[name] = []; - - (self._candidates_queue_local[name]).push(candidate_data); - } - } catch(e) { - self.get_debug().log('[JSJaCJingle] _set_candidates_queue_local > ' + e, 1); - } - }; - - /** - * @private - */ - self._set_payloads_remote = function(name, payload_data) { - self._payloads_remote[name] = payload_data; - }; - - /** - * @private - */ - self._set_payloads_remote_add = function(name, payload_data) { - try { - if(!(name in self._payloads_remote)) { - self._set_payloads_remote(name, payload_data); - } else { - var key; - var payloads_store = self._payloads_remote[name].descriptions.payload; - var payloads_add = payload_data.descriptions.payload; - - for(key in payloads_add) { - if(!(key in payloads_store)) - payloads_store[key] = payloads_add[key]; - } - } - } catch(e) { - self.get_debug().log('[JSJaCJingle] _set_payloads_remote_add > ' + e, 1); - } - }; - - /** - * @private - */ - self._set_group_remote = function(semantics, group_data) { - self._group_remote[semantics] = group_data; - }; - - /** - * @private - */ - self._set_candidates_remote = function(name, candidate_data) { - self._candidates_remote[name] = candidate_data; - }; - - /** - * @private - */ - self._set_candidates_queue_remote = function(name, candidate_data) { - if(name === null) - self._candidates_queue_remote = {}; - else - self._candidates_queue_remote[name] = (candidate_data); - }; - - /** - * @private - */ - self._set_candidates_remote_add = function(name, candidate_data) { - try { - if(!name) return; - - if(!(name in self._candidates_remote)) - self._set_candidates_remote(name, []); - - var c, i; - var candidate_ids = []; - - for(c in self._get_candidates_remote(name)) - candidate_ids.push(self._get_candidates_remote(name)[c].id); - - for(i in candidate_data) { - if((candidate_data[i].id).indexOf(candidate_ids) !== -1) - self._get_candidates_remote(name).push(candidate_data[i]); - } - } catch(e) { - self.get_debug().log('[JSJaCJingle] _set_candidates_remote_add > ' + e, 1); - } - }; - - /** - * @private - */ - self._set_content_local = function(name, content_local) { - self._content_local[name] = content_local; - }; - - /** - * @private - */ - self._set_content_remote = function(name, content_remote) { - self._content_remote[name] = content_remote; - }; - - /** - * @private - */ - self._set_handlers = function(type, id, handler) { - if(!(type in self._handlers)) self._handlers[type] = {}; - - self._handlers[type][id] = handler; - }; - - /** - * @private - */ - self._set_peer_connection = function(peer_connection) { - self._peer_connection = peer_connection; - }; - - /** - * @private - */ - self._set_id = function(id) { - self._id = id; - }; - - /** - * @private - */ - self._set_sent_id = function(sent_id) { - self._sent_id[sent_id] = 1; - }; - - /** - * @private - */ - self._set_received_id = function(received_id) { - self._received_id[received_id] = 1; - }; - - /** - * @private - */ - self._set_mute = function(name, mute) { - if(!name || name == '*') { - self._mute = {}; - name = '*'; - } - - self._mute[name] = mute; - }; - - /** - * @private - */ - self._set_lock = function(lock) { - self._lock = lock; - }; - - /** - * Gets the media busy value - * @return media busy value - * @type boolean - */ - self._set_media_busy = function(busy) { - self._media_busy = busy; - }; - - /** - * @private - */ - self._set_sid = function(sid) { - self._sid = sid; - }; - - /** - * @private - */ - self._set_status = function(status) { - self._status = status; - }; - - /** - * @private - */ - self._set_reason = function(reason) { - self._reason = reason || JSJAC_JINGLE_REASON_CANCEL; - }; - - /** - * @private - */ - self._set_to = function(to) { - self._to = to; - }; - - /** - * @private - */ - self._set_media = function(media) { - self._media = media; - }; - - /** - * @private - */ - self._set_video_source = function() { - self._video_source = video_source; - }; - - /** - * @private - */ - self._set_resolution = function(resolution) { - self._resolution = resolution; - }; - - /** - * @private - */ - self._set_bandwidth = function(bandwidth) { - self._bandwidth = bandwidth; - }; - - /** - * @private - */ - self._set_fps = function(fps) { - self._fps = fps; - }; - - /** - * @private - */ - self._set_name = function(name) { - self._name[name] = 1; - }; - - /** - * @private - */ - self._set_senders = function(name, senders) { - if(!(senders in JSJAC_JINGLE_SENDERS)) senders = JSJAC_JINGLE_SENDERS_BOTH.jingle; - - self._senders[name] = senders; - }; - - /** - * @private - */ - self._set_creator = function(name, creator) { - if(!(creator in JSJAC_JINGLE_CREATORS)) creator = JSJAC_JINGLE_CREATOR_INITIATOR; - - self._creator[name] = creator; - }; - - /** - * @private - */ - self._set_initiator = function(initiator) { - self._initiator = initiator; - }; - - /** - * @private - */ - self._set_responder = function(responder) { - self._responder = responder; - }; - - /** - * @private - */ - self._set_stun = function(stun_host, stun_data) { - self._stun[stun_server] = stun_data; - }; - - /** - * @private - */ - self._set_turn = function(turn_host, turn_data) { - self._turn[turn_server] = turn_data; - }; - - /** - * @private - */ - self._set_sdp_trace = function(sdp_trace) { - self._sdp_trace = sdp_trace; - }; - - /** - * @private - */ - self._set_debug = function(debug) { - self._debug = debug; - }; - - - - /** - * JSJSAC JINGLE SHORTCUTS - */ - - /** - * Am I responder? - * @return Receiver state - * @type boolean - */ - self.is_responder = function() { - return self.util_negotiation_status() == JSJAC_JINGLE_SENDERS_RESPONDER.jingle; - }; - - /** - * Am I initiator? - * @return Initiator state - * @type boolean - */ - self.is_initiator = function() { - return self.util_negotiation_status() == JSJAC_JINGLE_SENDERS_INITIATOR.jingle; - }; - - - - /** - * JSJSAC JINGLE UTILITIES - */ - - /** - * Removes a given array value - * @return new array - * @type object - */ - self.util_array_remove_value = function(array, value) { - try { - var i; - - for(i = 0; i < array.length; i++) { - if(array[i] === value) { - array.splice(i, 1); i--; - } - } - } catch(e) { - self.get_debug().log('[JSJaCJingle] util_array_remove_value > ' + e, 1); - } - - return array; - }; - - /** - * Returns whether an object is empty or not - * @return Empty value - * @type boolean - */ - self.util_object_length = function(object) { - var key; - var l = 0; - - try { - for(key in object) { - if(object.hasOwnProperty(key)) l++; - } - } catch(e) { - self.get_debug().log('[JSJaCJingle] util_object_length > ' + e, 1); - } - - return l; - }; - - /** - * Collects given objects - * @return Empty value - * @type object - */ - self.util_object_collect = function() { - var i, p; - - var collect_obj = {}; - var len = arguments.length; - - for(i = 0; i < len; i++) { - for(p in arguments[i]) { - if(arguments[i].hasOwnProperty(p)) - collect_obj[p] = arguments[i][p]; - } - } - - return collect_obj; - }; - - /** - * Clones a given object - * @return Cloned object - * @type object - */ - self.util_object_clone = function(object) { - try { - var copy, i, attr; - - // Assert - if(object === null || typeof object !== 'object') return object; - - // Handle Date - if(object instanceof Date) { - copy = new Date(); - copy.setTime(object.getTime()); - - return copy; - } - - // Handle Array - if(object instanceof Array) { - copy = []; - - for(i = 0, len = object.length; i < len; i++) - copy[i] = self.util_object_clone(object[i]); - - return copy; - } - - // Handle Object - if(object instanceof Object) { - copy = {}; - - for(attr in object) { - if(object.hasOwnProperty(attr)) - copy[attr] = self.util_object_clone(object[attr]); - } - - return copy; - } - } catch(e) { - self.get_debug().log('[JSJaCJingle] util_object_clone > ' + e, 1); - } - - self.get_debug().log('[JSJaCJingle] util_object_clone > Cannot clone this object.', 1); - }; - - /** - * Gets the browser info - * @return browser info - * @type object - */ - self._util_browser = function() { - var browser_info = { - name : 'Generic' - }; - - try { - var user_agent, detect_arr, cur_browser; - - detect_arr = { - 'firefox' : JSJAC_JINGLE_BROWSER_FIREFOX, - 'chrome' : JSJAC_JINGLE_BROWSER_CHROME, - 'safari' : JSJAC_JINGLE_BROWSER_SAFARI, - 'opera' : JSJAC_JINGLE_BROWSER_OPERA, - 'msie' : JSJAC_JINGLE_BROWSER_IE + /** + * @member {JSJaCConnection} + * @default + * @private + */ + this._connection = null; + + /** + * @type {Object} + * @default + * @private + */ + this._sessions = {}; + this._sessions[JSJAC_JINGLE_SESSION_SINGLE] = {}; + this._sessions[JSJAC_JINGLE_SESSION_MUJI] = {}; + + /** + * @type {Function} + * @default + * @private + */ + this._single_initiate = undefined; + + /** + * @type {Function} + * @default + * @private + */ + this._muji_invite = undefined; + + /** + * @type {Object} + * @default + * @private + */ + this._debug = { + log : function() {} }; - user_agent = navigator.userAgent.toLowerCase(); + /** + * @type {Object} + * @default + * @private + */ + this._extdisco = { + stun : [], + turn : [] + }; - for(cur_browser in detect_arr) { - if(user_agent.indexOf(cur_browser) > -1) { - browser_info.name = detect_arr[cur_browser]; - break; + /** + * @type {Object} + * @default + * @private + */ + this._fallback = { + stun : [], + turn : [] + }; + + /** + * @type {Object} + * @default + * @private + */ + this._relaynodes = { + stun : [] + }; + + /** + * @type {Object} + * @default + * @private + */ + this._defer = { + deferred : false, + count : 0, + fn : [] + }; + }, + + + + /** + * JSJSAC JINGLE GETTERS + */ + + /** + * Gets the connection object + * @public + * @returns {JSJaCConnection} Connection + */ + get_connection: function() { + return this._connection; + }, + + /** + * Gets the sessions storage + * @public + * @returns {Object} Sessions + */ + get_sessions: function() { + return this._sessions; + }, + + /** + * Gets the Single initiate function + * @public + * @returns {Function} Single initiate + */ + get_single_initiate: function() { + if(typeof this._single_initiate == 'function') + return this._single_initiate; + + return function(stanza) {}; + }, + + /** + * Gets the Single initiate raw value + * @public + * @returns {Function} Single initiate raw value + */ + get_single_initiate_raw: function() { + return this._single_initiate; + }, + + /** + * Gets the Muji invite function + * @public + * @returns {Function} Muji invite + */ + get_muji_invite: function() { + if(typeof this._muji_invite == 'function') + return this._muji_invite; + + return function(stanza) {}; + }, + + /** + * Gets the Muji invite raw value + * @public + * @returns {Function} Muji invite raw value + */ + get_muji_invite_raw: function() { + return this._muji_invite; + }, + + /** + * Gets the debug interface + * @public + * @returns {Object} Debug + */ + get_debug: function() { + return this._debug; + }, + + /** + * Gets the extdisco storage + * @public + * @returns {Object} Extdisco + */ + get_extdisco: function() { + return this._extdisco; + }, + + /** + * Gets the fallback storage + * @public + * @returns {Object} Fallback + */ + get_fallback: function() { + return this._fallback; + }, + + /** + * Gets the relay nodes storage + * @public + * @returns {Object} Relay nodes + */ + get_relaynodes: function() { + return this._relaynodes; + }, + + /** + * Gets the defer storage + * @public + * @returns {Object} Defer + */ + get_defer: function() { + return this._defer; + }, + + + + /** + * JSJSAC JINGLE SETTERS + */ + + /** + * Sets the connection object + * @public + * @param {JSJaCConnection} Connection + */ + set_connection: function(connection) { + this._connection = connection; + }, + + /** + * Sets the sessions storage + * @public + * @param {Object} sessions + */ + set_sessions: function(sessions) { + this._sessions = sessions; + }, + + /** + * Sets the Single initiate function + * @public + * @param {Function} Single initiate + */ + set_single_initiate: function(single_initiate) { + this._single_initiate = single_initiate; + }, + + /** + * Sets the Muji invite function + * @public + * @param {Function} Muji invite + */ + set_muji_invite: function(muji_invite) { + this._muji_invite = muji_invite; + }, + + /** + * Sets the debug interface + * @public + * @param {Object} Debug + */ + set_debug: function(debug) { + this._debug = debug; + }, + + /** + * Sets the extdisco storage + * @public + * @param {Object} Extdisco + */ + set_extdisco: function(extdisco) { + this._extdisco = extdisco; + }, + + /** + * Sets the fallback storage + * @public + * @param {Object} Fallback + */ + set_fallback: function(fallback) { + this._fallback = fallback; + }, + + /** + * Sets the relay nodes storage + * @public + * @param {Object} Relay nodes + */ + set_relaynodes: function(relaynodes) { + this._relaynodes = relaynodes; + }, + + /** + * Sets the defer storage + * @public + * @param {Object} Defer + */ + set_defer: function(defer) { + this._defer = defer; + }, + } +))(); +/** + * @fileoverview JSJaC Jingle library - Utilities + * + * @url https://github.com/valeriansaliou/jsjac-jingle + * @depends https://github.com/sstrigler/JSJaC + * @author Valérian Saliou https://valeriansaliou.name/ + * @license Mozilla Public License v2.0 (MPL v2.0) + */ + + +/** @module jsjac-jingle/utils */ +/** @exports JSJaCJingleUtils */ + + +/** + * Utilities class. + * @class + * @classdesc Utilities class. + * @param {JSJaCJingleSingle|JSJaCJingleMuji} parent Parent class. + * @requires nicolas-van/ring.js + * @requires sstrigler/JSJaC + * @see {@link http://ringjs.neoname.eu/|Ring.js} + * @see {@link http://stefan-strigler.de/jsjac-1.3.4/doc/|JSJaC Documentation} + */ +var JSJaCJingleUtils = ring.create( + /** @lends JSJaCJingleUtils.prototype */ + { + /** + * Constructor + */ + constructor: function(parent) { + /** + * @constant + * @member {JSJaCJingleSingle|JSJaCJingleMuji} + * @readonly + * @default + * @public + */ + this.parent = parent; + }, + + /** + * Removes a given array value + * @public + * @param {Array} array + * @param {*} value + * @returns {Array} New array + */ + array_remove_value: function(array, value) { + try { + var i; + + for(i = 0; i < array.length; i++) { + if(array[i] === value) { + array.splice(i, 1); i--; + } + } + } catch(e) { + this.parent.get_debug().log('[JSJaCJingle:utils] array_remove_value > ' + e, 1); + } + + return array; + }, + + /** + * Returns whether an object is empty or not + * @public + * @param {Object} object + * @returns {Number} Object length + */ + object_length: function(object) { + var key; + var l = 0; + + try { + for(key in object) { + if(object.hasOwnProperty(key)) l++; + } + } catch(e) { + this.parent.get_debug().log('[JSJaCJingle:utils] object_length > ' + e, 1); + } + + return l; + }, + + /** + * Returns whether given objects are equal or not + * @public + * @param {...Object} arguments - Objects to be compared + * @returns {Boolean} Equality + */ + object_equal: function() { + var equal = true, + last_value = null; + + if(arguments.length >= 1) { + for(var i = 0; i < arguments.length; i++) { + if(i > 0) { + equal = (JSON.stringify(last_value) === JSON.stringify(arguments[i])); + if(equal !== true) break; + } + + last_value = arguments[i]; + } + } else { + equal = false; + } + + return equal; + }, + + /** + * Collects given objects + * @public + * @param {...Object} arguments - Objects to be collected + * @returns {Object} Collected object + */ + object_collect: function() { + var i, p; + + var collect_obj = {}; + + for(i = 0; i < arguments.length; i++) { + for(p in arguments[i]) { + if(arguments[i].hasOwnProperty(p)) + collect_obj[p] = arguments[i][p]; } } - } catch(e) { - self.get_debug().log('[JSJaCJingle] _util_browser > ' + e, 1); - } - return browser_info; - }; + return collect_obj; + }, - /** - * Gets the ICE config - * @return ICE config - * @type object - */ - self._util_config_ice = function() { - try { - // Collect data (user + server) - var stun_config = self.util_object_collect( - self.get_stun(), - JSJAC_JINGLE_STORE_EXTDISCO.stun, - JSJAC_JINGLE_STORE_FALLBACK.stun - ); + /** + * Collects given arrays + * @public + * @param {...Array} arguments - Arrays to be collected + * @returns {Array} Collected array + */ + array_collect: function() { + var i, j, p, + cur_arr; - var turn_config = self.util_object_collect( - self.get_turn(), - JSJAC_JINGLE_STORE_EXTDISCO.turn, - JSJAC_JINGLE_STORE_FALLBACK.turn - ); + var collect_arr = []; - // Can proceed? - if(stun_config && self.util_object_length(stun_config) || - turn_config && self.util_object_length(turn_config) ) { - var config = { - iceServers : [] + for(i = 0; i < arguments.length; i++) { + cur_arr = arguments[i]; + + loop_arr: for(j = 0; j < cur_arr.length; j++) { + // Ensure uniqueness of object + for(p in collect_arr) { + if(this.object_equal(cur_arr[j], collect_arr[p])) continue loop_arr; + } + + collect_arr.push(cur_arr[j]); + } + } + + return collect_arr; + }, + + /** + * Clones a given object + * @public + * @param {Object} object + * @returns {Date|Array|Object} Cloned object + */ + object_clone: function(object) { + try { + var copy, i, attr; + + // Assert + if(object === null || typeof object !== 'object') return object; + + // Handle Date + if(object instanceof Date) { + copy = new Date(); + copy.setTime(object.getTime()); + + return copy; + } + + // Handle Array + if(object instanceof Array) { + copy = []; + + for(i = 0, len = object.length; i < len; i++) + copy[i] = this.object_clone(object[i]); + + return copy; + } + + // Handle Object + if(object instanceof Object) { + copy = {}; + + for(attr in object) { + if(object.hasOwnProperty(attr)) + copy[attr] = this.object_clone(object[attr]); + } + + return copy; + } + } catch(e) { + this.parent.get_debug().log('[JSJaCJingle:utils] object_clone > ' + e, 1); + } + + this.parent.get_debug().log('[JSJaCJingle:utils] object_clone > Cannot clone this object.', 1); + }, + + /** + * Gets the browser info + * @public + * @returns {Object} Browser info + */ + browser: function() { + var browser_info = { + name : 'Generic' + }; + + try { + var user_agent, detect_arr, cur_browser; + + detect_arr = { + 'firefox' : JSJAC_JINGLE_BROWSER_FIREFOX, + 'chrome' : JSJAC_JINGLE_BROWSER_CHROME, + 'safari' : JSJAC_JINGLE_BROWSER_SAFARI, + 'opera' : JSJAC_JINGLE_BROWSER_OPERA, + 'msie' : JSJAC_JINGLE_BROWSER_IE }; - // STUN servers - var cur_stun_host, cur_stun_obj, cur_stun_config; + user_agent = navigator.userAgent.toLowerCase(); - for(cur_stun_host in stun_config) { - if(cur_stun_host) { - cur_stun_obj = stun_config[cur_stun_host]; + for(cur_browser in detect_arr) { + if(user_agent.indexOf(cur_browser) > -1) { + browser_info.name = detect_arr[cur_browser]; + break; + } + } + } catch(e) { + this.parent.get_debug().log('[JSJaCJingle:utils] browser > ' + e, 1); + } + + return browser_info; + }, + + /** + * Gets the ICE config + * @public + * @returns {Object} ICE config + */ + config_ice: function() { + try { + // Collect data (user + server) + var stun_config = this.array_collect( + this.parent.get_stun(), + JSJaCJingleStorage.get_extdisco().stun, + JSJaCJingleStorage.get_relaynodes().stun, + JSJaCJingleStorage.get_fallback().stun + ); + + var turn_config = this.array_collect( + this.parent.get_turn(), + JSJaCJingleStorage.get_extdisco().turn, + JSJaCJingleStorage.get_fallback().turn + ); + + // Can proceed? + if(stun_config.length || turn_config.length) { + var config = { + iceServers : [] + }; + + // STUN servers + var i, cur_stun_obj, cur_stun_config; + + for(i in stun_config) { + cur_stun_obj = stun_config[i]; cur_stun_config = {}; - cur_stun_config.url = 'stun:' + cur_stun_host; + cur_stun_config.url = 'stun:' + cur_stun_obj.host; if(cur_stun_obj.port) cur_stun_config.url += ':' + cur_stun_obj.port; - if(cur_stun_obj.transport && self._util_browser().name != JSJAC_JINGLE_BROWSER_FIREFOX) + if(cur_stun_obj.transport && this.browser().name != JSJAC_JINGLE_BROWSER_FIREFOX) cur_stun_config.url += '?transport=' + cur_stun_obj.transport; (config.iceServers).push(cur_stun_config); } - } - // TURN servers - var cur_turn_host, cur_turn_obj, cur_turn_config; + // TURN servers + var j, cur_turn_obj, cur_turn_config; - for(cur_turn_host in turn_config) { - if(cur_turn_host) { - cur_turn_obj = turn_config[cur_turn_host]; + for(j in turn_config) { + cur_turn_obj = turn_config[j]; cur_turn_config = {}; - cur_turn_config.url = 'turn:' + cur_turn_host; + cur_turn_config.url = 'turn:' + cur_turn_obj.host; if(cur_turn_obj.port) cur_turn_config.url += ':' + cur_turn_obj.port; @@ -4051,2609 +4895,7639 @@ function JSJaCJingle(args) { (config.iceServers).push(cur_turn_config); } - } - // Check we have at least a STUN server (if user can traverse NAT) + // Check we have at least a STUN server (if user can traverse NAT) + var k; + var has_stun = false; + + for(k in config.iceServers) { + if((config.iceServers[k].url).match(R_NETWORK_PROTOCOLS.stun)) { + has_stun = true; break; + } + } + + if(!has_stun) { + (config.iceServers).push({ + url: (WEBRTC_CONFIGURATION.peer_connection.config.iceServers)[0].url + }); + } + + return config; + } + } catch(e) { + this.parent.get_debug().log('[JSJaCJingle:utils] config_ice > ' + e, 1); + } + + return WEBRTC_CONFIGURATION.peer_connection.config; + }, + + /** + * Gets the node value from a stanza element + * @public + * @param {DOM} stanza + * @returns {String|Object} Node value + */ + stanza_get_value: function(stanza) { + try { + return stanza.firstChild.nodeValue || null; + } catch(e) { + try { + return (stanza[0]).firstChild.nodeValue || null; + } catch(_e) { + this.parent.get_debug().log('[JSJaCJingle:utils] stanza_get_value > ' + _e, 1); + } + } + + return null; + }, + + /** + * Gets the attribute value from a stanza element + * @public + * @param {DOM} stanza + * @param {String} name + * @returns {String|Object} Attribute value + */ + stanza_get_attribute: function(stanza, name) { + if(!name) return null; + + try { + return stanza.getAttribute(name) || null; + } catch(e) { + try { + return (stanza[0]).getAttribute(name) || null; + } catch(_e) { + this.parent.get_debug().log('[JSJaCJingle:utils] stanza_get_attribute > ' + _e, 1); + } + } + + return null; + }, + + /** + * Sets the attribute value to a stanza element + * @public + * @param {DOM} stanza + * @param {String} name + * @param {*} value + */ + stanza_set_attribute: function(stanza, name, value) { + if(!(name && value && stanza)) return; + + try { + stanza.setAttribute(name, value); + } catch(e) { + try { + (stanza[0]).setAttribute(name, value); + } catch(_e) { + this.parent.get_debug().log('[JSJaCJingle:utils] stanza_set_attribute > ' + _e, 1); + } + } + }, + + /** + * Gets the Jingle node from a stanza + * @public + * @param {DOM} stanza + * @param {String} name + * @param {String} [ns] + * @returns {DOM} Selected DOM elements + */ + stanza_get_element: function(stanza, name, ns) { + var matches_result = []; + + // Assert + if(!stanza) return matches_result; + if(stanza.length) stanza = stanza[0]; + + ns = (ns || '*'); + + try { var i; - var has_stun = false; - for(i in config.iceServers) { - if((config.iceServers[i].url).match(/^stun:/i)) { - has_stun = true; break; + // Get only in lower level (not all sub-levels) + var matches = stanza.getElementsByTagNameNS(ns, name); + + if(matches && matches.length) { + for(i = 0; i < matches.length; i++) { + if(matches[i] && matches[i].parentNode == stanza) + matches_result.push(matches[i]); } } - if(!has_stun) { - (config.iceServers).push({ - url: (WEBRTC_CONFIGURATION.peer_connection.config.iceServers)[0].url - }); - } - - return config; + return matches_result; + } catch(e) { + this.parent.get_debug().log('[JSJaCJingle:utils] stanza_get_element > ' + e, 1); } - } catch(e) { - self.get_debug().log('[JSJaCJingle] _util_config_ice > ' + e, 1); - } - return WEBRTC_CONFIGURATION.peer_connection.config; - }; + return matches_result; + }, + + /** + * Gets the error node from a stanza + * @private + * @param {JSJaCPacket} stanza + * @param {Object} [error_match_obj] + * @returns {Boolean} Password invalid state + */ + stanza_get_error: function(stanza, error_match_obj) { + var matches_result = []; - /** - * Gets the node value from a stanza element - * @return Node value - * @type string - */ - self.util_stanza_get_value = function(stanza) { - try { - return stanza.firstChild.nodeValue || null; - } catch(e) { try { - return (stanza[0]).firstChild.nodeValue || null; - } catch(_e) { - self.get_debug().log('[JSJaCJingle] util_stanza_get_value > ' + _e, 1); - } - } + var i, + error_child, cur_error_child; + + error_child = stanza.getChild('error', NS_CLIENT); - return null; - }; + if(error_child && error_child.length) { + for(i = 0; i < error_child.length; i++) { + cur_error_child = error_child[i]; - /** - * Gets the attribute value from a stanza element - * @return Attribute value - * @type string - */ - self.util_stanza_get_attribute = function(stanza, name) { - if(!name) return null; - - try { - return stanza.getAttribute(name) || null; - } catch(e) { - try { - return (stanza[0]).getAttribute(name) || null; - } catch(_e) { - self.get_debug().log('[JSJaCJingle] util_stanza_get_attribute > ' + _e, 1); - } - } - - return null; - }; - - /** - * Sets the attribute value to a stanza element - */ - self.util_stanza_set_attribute = function(stanza, name, value) { - if(!(name && value && stanza)) return; - - try { - stanza.setAttribute(name, value); - } catch(e) { - try { - (stanza[0]).setAttribute(name, value); - } catch(_e) { - self.get_debug().log('[JSJaCJingle] util_stanza_set_attribute > ' + _e, 1); - } - } - }; - - /** - * Gets the Jingle node from a stanza - * @return Jingle node - * @type DOM - */ - self.util_stanza_get_element = function(stanza, name, ns) { - // Assert - if(!stanza) return []; - if(stanza.length) stanza = stanza[0]; - - try { - // Get only in lower level (not all sub-levels) - var matches = stanza.getElementsByTagNameNS(ns, name); - - if(matches[0] && matches[0].parentNode == stanza) return matches; - - return []; - } catch(e) { - self.get_debug().log('[JSJaCJingle] util_stanza_get_element > ' + e, 1); - } - - return []; - }; - - /** - * Gets the Jingle node from a stanza - * @return Jingle node - * @type DOM - */ - self.util_stanza_jingle = function(stanza) { - try { - return stanza.getChild('jingle', NS_JINGLE); - } catch(e) { - self.get_debug().log('[JSJaCJingle] util_stanza_jingle > ' + e, 1); - } - - return null; - }; - - /** - * Gets the from value from a stanza - * @return from value - * @type string - */ - self.util_stanza_from = function(stanza) { - try { - return stanza.getFrom() || null; - } catch(e) { - self.get_debug().log('[JSJaCJingle] util_stanza_from > ' + e, 1); - } - - return null; - }; - - /** - * Gets the SID value from a stanza - * @return SID value - * @type string - */ - self.util_stanza_sid = function(stanza) { - try { - return self.util_stanza_get_attribute( - self.util_stanza_jingle(stanza), - 'sid' - ); - } catch(e) { - self.get_debug().log('[JSJaCJingle] util_stanza_sid > ' + e, 1); - } - }; - - /** - * Checks if a stanza is safe (known SID + sender) - * @return safety state - * @type boolean - */ - self.util_stanza_safe = function(stanza) { - try { - return !((stanza.getType() == JSJAC_JINGLE_STANZA_TYPE_SET && self.util_stanza_sid(stanza) != self.get_sid()) || self.util_stanza_from(stanza) != self.get_to()); - } catch(e) { - self.get_debug().log('[JSJaCJingle] util_stanza_safe > ' + e, 1); - } - - return false; - }; - - /** - * Gets a stanza terminate reason - * @return reason code - * @type string - */ - self.util_stanza_terminate_reason = function(stanza) { - try { - var jingle = self.util_stanza_jingle(stanza); - - if(jingle) { - var reason = self.util_stanza_get_element(jingle, 'reason', NS_JINGLE); - - if(reason.length) { - var cur_reason; - - for(cur_reason in JSJAC_JINGLE_REASONS) { - if(self.util_stanza_get_element(reason[0], cur_reason, NS_JINGLE).length) - return cur_reason; - } - } - } - } catch(e) { - self.get_debug().log('[JSJaCJingle] util_stanza_terminate_reason > ' + e, 1); - } - - return null; - }; - - /** - * Gets a stanza session info - * @return info code - * @type string - */ - self.util_stanza_session_info = function(stanza) { - try { - var jingle = self.util_stanza_jingle(stanza); - - if(jingle) { - var cur_info; - - for(cur_info in JSJAC_JINGLE_SESSION_INFOS) { - if(self.util_stanza_get_element(jingle, cur_info, NS_JINGLE_APPS_RTP_INFO).length) - return cur_info; - } - } - } catch(e) { - self.get_debug().log('[JSJaCJingle] util_stanza_session_info > ' + e, 1); - } - - return null; - }; - - /** - * Set a timeout limit to a stanza - */ - self.util_stanza_timeout = function(t_type, t_id, handlers) { - try { - t_type = t_type || JSJAC_JINGLE_STANZA_TYPE_ALL; - - var t_sid = self.get_sid(); - var t_status = self.get_status(); - - self.get_debug().log('[JSJaCJingle] util_stanza_timeout > Registered (id: ' + t_id + ', status: ' + t_status + ').', 4); - - setTimeout(function() { - self.get_debug().log('[JSJaCJingle] util_stanza_timeout > Cheking (id: ' + t_id + ', status: ' + t_status + '-' + self.get_status() + ').', 4); - - // State did not change? - if(self.get_sid() == t_sid && self.get_status() == t_status && !(t_id in self._get_received_id())) { - self.get_debug().log('[JSJaCJingle] util_stanza_timeout > Stanza timeout.', 2); - - self.unregister_handler(t_type, t_id); - - if(handlers.external) (handlers.external)(self); - if(handlers.internal) (handlers.internal)(); - } else { - self.get_debug().log('[JSJaCJingle] util_stanza_timeout > Stanza successful.', 4); - } - }, (JSJAC_JINGLE_STANZA_TIMEOUT * 1000)); - } catch(e) { - self.get_debug().log('[JSJaCJingle] util_stanza_timeout > ' + e, 1); - } - }; - - /** - * @private - */ - self._util_stanza_parse_node = function(parent, name, ns, obj, attrs, value) { - try { - var i, j, - error, child, child_arr; - var children = self.util_stanza_get_element(parent, name, ns); - - if(children.length) { - for(i = 0; i < children.length; i++) { - // Initialize - error = 0; - child = children[i]; - child_arr = {}; - - // Parse attributes - for(j in attrs) { - child_arr[attrs[j].n] = self.util_stanza_get_attribute(child, attrs[j].n); - - if(attrs[j].r && !child_arr[attrs[j].n]) { - error++; break; + if(typeof error_match_obj == 'object') { + if(cur_error_child.getAttribute('type') === error_match_obj.type && + cur_error_child.getChild(error_match_obj.xmpp, NS_IETF_XMPP_STANZAS)) { + matches_result.push(cur_error_child); + } + } else { + matches_result.push(cur_error_child); } } - - // Parse value - if(value) { - child_arr[value.n] = self.util_stanza_get_value(child); - if(value.r && !child_arr[value.n]) error++; - } - - if(error !== 0) continue; - - // Push current children - obj.push(child_arr); } + } catch(e) { + this.get_debug().log('[JSJaCJingle:utils] stanza_get_error > ' + e, 1); } - } catch(e) { - self.get_debug().log('[JSJaCJingle] _util_stanza_parse_node > ' + e, 1); - } - }; - /** - * @private - */ - self._util_stanza_parse_content = function(stanza) { - try { - var i, - jingle, content, cur_content, - content_creator, content_name, content_senders, - cur_candidates; + return matches_result; + }, - // Parse initiate stanza - jingle = self.util_stanza_jingle(stanza); + /** + * Gets the Jingle node from a stanza + * @public + * @param {JSJaCPacket} stanza + * @returns {DOM|Object} Jingle node + */ + stanza_jingle: function(stanza) { + try { + return stanza.getChild('jingle', this.parent.get_namespace()); + } catch(e) { + this.parent.get_debug().log('[JSJaCJingle:utils] stanza_jingle > ' + e, 1); + } - if(jingle) { - // Childs - content = self.util_stanza_get_element(jingle, 'content', NS_JINGLE); + return null; + }, - if(content && content.length) { - for(i = 0; i < content.length; i++) { - cur_content = content[i]; + /** + * Gets the Jingle Muji node from a stanza + * @public + * @param {JSJaCPacket} stanza + * @returns {DOM|Object} Jingle node + */ + stanza_muji: function(stanza) { + try { + return stanza.getChild('muji', NS_MUJI); + } catch(e) { + this.parent.get_debug().log('[JSJaCJingle:utils] stanza_muji > ' + e, 1); + } - // Attrs (avoids senders & creators to be changed later in the flow) - content_name = self.util_stanza_get_attribute(cur_content, 'name'); - content_senders = self.get_senders(content_name) || self.util_stanza_get_attribute(cur_content, 'senders'); - content_creator = self.get_creator(content_name) || self.util_stanza_get_attribute(cur_content, 'creator'); + return null; + }, - self._set_name(content_name); - self._set_senders(content_name, content_senders); - self._set_creator(content_name, content_creator); + /** + * Gets the from value from a stanza + * @public + * @param {JSJaCPacket} stanza + * @returns {String|Object} From value + */ + stanza_from: function(stanza) { + try { + return stanza.getFrom() || null; + } catch(e) { + this.parent.get_debug().log('[JSJaCJingle:utils] stanza_from > ' + e, 1); + } - // Payloads (non-destructive setters / cumulative) - self._set_payloads_remote_add( - content_name, - self._util_stanza_parse_payload(cur_content) - ); + return null; + }, - // Candidates (enqueue them for ICE processing, too) - cur_candidate = self._util_stanza_parse_candidate(cur_content); + /** + * Extracts username from stanza + * @public + * @param {JSJaCPacket} stanza + * @returns {String|Object} Username + */ + stanza_username: function(stanza) { + try { + return this.extract_username(stanza.getFrom()); + } catch(e) { + this.parent.get_debug().log('[JSJaCJingle:utils] stanza_username > ' + e, 1); + } - self._set_candidates_remote_add( - content_name, - cur_candidate - ); + return null; + }, - self._set_candidates_queue_remote( - content_name, - cur_candidate - ); + /** + * Gets the SID value from a stanza + * @public + * @param {JSJaCPacket} stanza + * @returns {String|Object} SID value + */ + stanza_sid: function(stanza) { + try { + return this.stanza_get_attribute( + this.stanza_jingle(stanza), + 'sid' + ); + } catch(e) { + this.parent.get_debug().log('[JSJaCJingle:utils] stanza_sid > ' + e, 1); + } + }, + + /** + * Checks if a stanza is safe (known SID + sender) + * @public + * @param {JSJaCPacket} stanza + * @returns {Boolean} Safety state + */ + stanza_safe: function(stanza) { + try { + return !((stanza.getType() == JSJAC_JINGLE_IQ_TYPE_SET && this.stanza_sid(stanza) != this.parent.get_sid()) || this.stanza_from(stanza) != this.parent.get_to()); + } catch(e) { + this.parent.get_debug().log('[JSJaCJingle:utils] stanza_safe > ' + e, 1); + } + + return false; + }, + + /** + * Gets a stanza terminate reason + * @public + * @param {JSJaCPacket} stanza + * @returns {String|Object} Reason code + */ + stanza_terminate_reason: function(stanza) { + try { + var jingle = this.stanza_jingle(stanza); + + if(jingle) { + var reason = this.stanza_get_element(jingle, 'reason', this.parent.get_namespace()); + + if(reason.length) { + var cur_reason; + + for(cur_reason in JSJAC_JINGLE_REASONS) { + if(this.stanza_get_element(reason[0], cur_reason, this.parent.get_namespace()).length) + return cur_reason; + } } - - return true; } + } catch(e) { + this.parent.get_debug().log('[JSJaCJingle:utils] stanza_terminate_reason > ' + e, 1); } - } catch(e) { - self.get_debug().log('[JSJaCJingle] _util_stanza_parse_content > ' + e, 1); - } - return false; - }; + return null; + }, - /** - * @private - */ - self._util_stanza_parse_group = function(stanza) { - try { - var i, j, - jingle, - group, cur_group, - content, cur_content, group_content_names; + /** + * Gets a stanza session info + * @public + * @param {JSJaCPacket} stanza + * @returns {String|Object} Info code + */ + stanza_session_info: function(stanza) { + try { + var jingle = this.stanza_jingle(stanza); - // Parse initiate stanza - jingle = self.util_stanza_jingle(stanza); + if(jingle) { + var cur_info; - if(jingle) { - // Childs - group = self.util_stanza_get_element(jingle, 'group', NS_JINGLE_APPS_GROUPING); + for(cur_info in JSJAC_JINGLE_SESSION_INFOS) { + if(this.stanza_get_element(jingle, cur_info, NS_JINGLE_APPS_RTP_INFO).length) + return cur_info; + } + } + } catch(e) { + this.parent.get_debug().log('[JSJaCJingle:utils] stanza_session_info > ' + e, 1); + } - if(group && group.length) { - for(i = 0; i < group.length; i++) { - cur_group = group[i]; - group_content_names = []; + return null; + }, - // Attrs - group_semantics = self.util_stanza_get_attribute(cur_group, 'semantics'); + /** + * Set a timeout limit to a stanza + * @public + * @param {String} t_type + * @param {String} t_id + * @param {Object} [handlers] + */ + stanza_timeout: function(t_node, t_type, t_id, handlers) { + try { + var t_sid = this.parent.get_sid(); + var t_status = this.parent.get_status(); - // Contents - content = self.util_stanza_get_element(cur_group, 'content', NS_JINGLE_APPS_GROUPING); + this.parent.get_debug().log('[JSJaCJingle:utils] stanza_timeout > Registered (node: ' + t_node + ', type: ' + t_type + ', id: ' + t_id + ', status: ' + t_status + ').', 4); - for(j = 0; j < content.length; j++) { - cur_content = content[j]; + var _this = this; - // Content attrs - group_content_names.push( - self.util_stanza_get_attribute(cur_content, 'name') - ); + setTimeout(function() { + _this.parent.get_debug().log('[JSJaCJingle:utils] stanza_timeout > Cheking (node: ' + t_node + ', type: ' + t_type + ', id: ' + t_id + ', status: ' + t_status + '-' + _this.parent.get_status() + ').', 4); + + // State did not change? + if(_this.parent.get_sid() == t_sid && _this.parent.get_status() == t_status && !(t_id in _this.parent.get_received_id())) { + _this.parent.get_debug().log('[JSJaCJingle:utils] stanza_timeout > Stanza timeout.', 2); + + _this.parent.unregister_handler(t_node, t_type, t_id); + + if(typeof handlers == 'object') { + if(handlers.external) (handlers.external)(_this); + if(handlers.internal) (handlers.internal)(); + } + } else { + _this.parent.get_debug().log('[JSJaCJingle:utils] stanza_timeout > Stanza successful.', 4); + } + }, (JSJAC_JINGLE_STANZA_TIMEOUT * 1000)); + } catch(e) { + this.parent.get_debug().log('[JSJaCJingle:utils] stanza_timeout > ' + e, 1); + } + }, + + /** + * Parses stanza node + * @public + * @param {DOM} parent + * @param {String} name + * @param {String} ns + * @param {Object} obj + * @param {Array} attrs + * @param {Object} [value] + */ + stanza_parse_node: function(parent, name, ns, obj, attrs, value) { + try { + var i, j, + error, child, child_arr; + var children = this.stanza_get_element(parent, name, ns); + + if(children.length) { + for(i = 0; i < children.length; i++) { + // Initialize + error = 0; + child = children[i]; + child_arr = {}; + + // Parse attributes + for(j in attrs) { + child_arr[attrs[j].n] = this.stanza_get_attribute(child, attrs[j].n); + + if(attrs[j].r && !child_arr[attrs[j].n]) { + error++; break; + } } - // Payloads (non-destructive setters / cumulative) - self._set_group_remote( - group_semantics, - group_content_names - ); - } - } - } - - return true; - } catch(e) { - self.get_debug().log('[JSJaCJingle] _util_stanza_parse_group > ' + e, 1); - } - - return false; - }; - - /** - * @private - */ - self._util_stanza_parse_payload = function(stanza_content) { - var payload_obj = { - descriptions : {}, - transports : {} - }; - - try { - // Common vars - var j, error, - cur_payload, cur_payload_arr, cur_payload_id; - - // Common functions - var init_content = function() { - var ic_key; - var ic_arr = { - 'attrs' : {}, - 'rtcp-fb' : [], - 'bandwidth' : [], - 'payload' : {}, - 'rtp-hdrext' : [], - 'rtcp-mux' : 0, - - 'encryption' : { - 'attrs' : {}, - 'crypto' : [], - 'zrtp-hash' : [] - } - }; - - for(ic_key in ic_arr) - if(!(ic_key in payload_obj.descriptions)) payload_obj.descriptions[ic_key] = ic_arr[ic_key]; - }; - - var init_payload = function(id) { - var ip_key; - var ip_arr = { - 'attrs' : {}, - 'parameter' : [], - 'rtcp-fb' : [], - 'rtcp-fb-trr-int' : [] - }; - - if(!(id in payload_obj.descriptions.payload)) payload_obj.descriptions.payload[id] = {}; - - for(ip_key in ip_arr) - if(!(ip_key in payload_obj.descriptions.payload[id])) payload_obj.descriptions.payload[id][ip_key] = ip_arr[ip_key]; - }; - - // Parse session description - var description = self.util_stanza_get_element(stanza_content, 'description', NS_JINGLE_APPS_RTP); - - if(description.length) { - description = description[0]; - - var cd_media = self.util_stanza_get_attribute(description, 'media'); - var cd_ssrc = self.util_stanza_get_attribute(description, 'ssrc'); - - if(!cd_media) - self.get_debug().log('[JSJaCJingle] util_stanza_parse_payload > No media attribute to ' + cc_name + ' stanza.', 1); - - // Initialize current description - init_content(); - - payload_obj.descriptions.attrs.media = cd_media; - payload_obj.descriptions.attrs.ssrc = cd_ssrc; - - // Loop on multiple payloads - var payload = self.util_stanza_get_element(description, 'payload-type', NS_JINGLE_APPS_RTP); - - if(payload.length) { - for(j = 0; j < payload.length; j++) { - error = 0; - cur_payload = payload[j]; - cur_payload_arr = {}; - - cur_payload_arr.channels = self.util_stanza_get_attribute(cur_payload, 'channels'); - cur_payload_arr.clockrate = self.util_stanza_get_attribute(cur_payload, 'clockrate'); - cur_payload_arr.id = self.util_stanza_get_attribute(cur_payload, 'id') || error++; - cur_payload_arr.name = self.util_stanza_get_attribute(cur_payload, 'name'); - - payload_obj.descriptions.attrs.ptime = self.util_stanza_get_attribute(cur_payload, 'ptime'); - payload_obj.descriptions.attrs.maxptime = self.util_stanza_get_attribute(cur_payload, 'maxptime'); + // Parse value + if(value) { + child_arr[value.n] = this.stanza_get_value(child); + if(value.r && !child_arr[value.n]) error++; + } if(error !== 0) continue; - // Initialize current payload - cur_payload_id = cur_payload_arr.id; - init_payload(cur_payload_id); - - // Push current payload - payload_obj.descriptions.payload[cur_payload_id].attrs = cur_payload_arr; - - // Loop on multiple parameters - self._util_stanza_parse_node( - cur_payload, - 'parameter', - NS_JINGLE_APPS_RTP, - payload_obj.descriptions.payload[cur_payload_id].parameter, - [ { n: 'name', r: 1 }, { n: 'value', r: 0 } ] - ); - - // Loop on multiple RTCP-FB - self._util_stanza_parse_node( - cur_payload, - 'rtcp-fb', - NS_JINGLE_APPS_RTP_RTCP_FB, - payload_obj.descriptions.payload[cur_payload_id]['rtcp-fb'], - [ { n: 'type', r: 1 }, { n: 'subtype', r: 0 } ] - ); - - // Loop on multiple RTCP-FB-TRR-INT - self._util_stanza_parse_node( - cur_payload, - 'rtcp-fb-trr-int', - NS_JINGLE_APPS_RTP_RTCP_FB, - payload_obj.descriptions.payload[cur_payload_id]['rtcp-fb-trr-int'], - [ { n: 'value', r: 1 } ] - ); + // Push current children + obj.push(child_arr); } } + } catch(e) { + this.parent.get_debug().log('[JSJaCJingle:utils] stanza_parse_node > ' + e, 1); + } + }, - // Parse the encryption element - var encryption = self.util_stanza_get_element(description, 'encryption', NS_JINGLE_APPS_RTP); + /** + * Parses stanza content + * @public + * @param {JSJaCPacket} stanza + * @returns {Boolean} Success + */ + stanza_parse_content: function(stanza) { + try { + var i, + jingle, namespace, content, cur_content, + content_creator, content_name, content_senders, + cur_candidates; - if(encryption.length) { - encryption = encryption[0]; + // Parse initiate stanza + switch(stanza.name) { + case JSJAC_JINGLE_STANZA_IQ: + // Jingle elements are encapsulated into IQs + jingle = this.stanza_jingle(stanza); break; - payload_obj.descriptions.encryption.attrs.required = self.util_stanza_get_attribute(encryption, 'required') || '0'; + case JSJAC_JINGLE_STANZA_PRESENCE: + // Muji elements are encapsulated into Presences + jingle = this.stanza_muji(stanza); break; - // Loop on multiple cryptos - self._util_stanza_parse_node( - encryption, - 'crypto', - NS_JINGLE_APPS_RTP, - payload_obj.descriptions.encryption.crypto, - [ { n: 'crypto-suite', r: 1 }, { n: 'key-params', r: 1 }, { n: 'session-params', r: 0 }, { n: 'tag', r: 1 } ] - ); - - // Loop on multiple zrtp-hash - self._util_stanza_parse_node( - encryption, - 'zrtp-hash', - NS_JINGLE_APPS_RTP_ZRTP, - payload_obj.descriptions.encryption['zrtp-hash'], - [ { n: 'version', r: 1 } ], - { n: 'value', r: 1 } - ); + default: + throw 'Stanza is not Jingle, nor Muji.'; } - // Loop on common RTCP-FB - self._util_stanza_parse_node( - description, - 'rtcp-fb', - NS_JINGLE_APPS_RTP_RTCP_FB, - payload_obj.descriptions['rtcp-fb'], - [ { n: 'type', r: 1 }, { n: 'subtype', r: 0 } ] - ); + if(jingle) { + // Childs + content = this.stanza_get_element(jingle, 'content', this.parent.get_namespace()); - // Loop on bandwidth - self._util_stanza_parse_node( - description, - 'bandwidth', - NS_JINGLE_APPS_RTP, - payload_obj.descriptions.bandwidth, - [ { n: 'type', r: 1 } ], - { n: 'value', r: 1 } - ); + if(content && content.length) { + for(i = 0; i < content.length; i++) { + cur_content = content[i]; - // Parse the RTP-HDREXT element - self._util_stanza_parse_node( - description, - 'rtp-hdrext', - NS_JINGLE_APPS_RTP_RTP_HDREXT, - payload_obj.descriptions['rtp-hdrext'], - [ { n: 'id', r: 1 }, { n: 'uri', r: 1 }, { n: 'senders', r: 0 } ] - ); + // Attrs (avoids senders & creators to be changed later in the flow) + content_name = this.stanza_get_attribute(cur_content, 'name'); + content_senders = this.parent.get_senders(content_name) || this.stanza_get_attribute(cur_content, 'senders'); + content_creator = this.parent.get_creator(content_name) || this.stanza_get_attribute(cur_content, 'creator'); - // Parse the RTCP-MUX element - var rtcp_mux = self.util_stanza_get_element(description, 'rtcp-mux', NS_JINGLE_APPS_RTP); + this.parent._set_name(content_name); + this.parent._set_senders(content_name, content_senders); + this.parent._set_creator(content_name, content_creator); - if(rtcp_mux.length) { - payload_obj.descriptions['rtcp-mux'] = 1; - } - } - - // Parse transport (need to get 'ufrag' and 'pwd' there) - var transport = self.util_stanza_get_element(stanza_content, 'transport', NS_JINGLE_TRANSPORTS_ICEUDP); - - if(transport.length) { - payload_obj.transports.pwd = self.util_stanza_get_attribute(transport, 'pwd'); - payload_obj.transports.ufrag = self.util_stanza_get_attribute(transport, 'ufrag'); - - var fingerprint = self.util_stanza_get_element(transport, 'fingerprint', NS_JINGLE_APPS_DTLS); - - if(fingerprint.length) { - payload_obj.transports.fingerprint = {}; - payload_obj.transports.fingerprint.setup = self.util_stanza_get_attribute(fingerprint, 'setup'); - payload_obj.transports.fingerprint.hash = self.util_stanza_get_attribute(fingerprint, 'hash'); - payload_obj.transports.fingerprint.value = self.util_stanza_get_value(fingerprint); - } - } - } catch(e) { - self.get_debug().log('[JSJaCJingle] _util_stanza_parse_payload > ' + e, 1); - } - - return payload_obj; - }; - - /** - * @private - */ - self._util_stanza_parse_candidate = function(stanza_content) { - var candidate_arr = []; - - try { - // Common vars - var i, - transport, candidate, - cur_candidate, cur_candidate_obj; - - // Parse transport candidates - transport = self.util_stanza_get_element(stanza_content, 'transport', NS_JINGLE_TRANSPORTS_ICEUDP); - - if(transport.length) { - self._util_stanza_parse_node( - transport, - 'candidate', - NS_JINGLE_TRANSPORTS_ICEUDP, - candidate_arr, - - [ - { n: 'component', r: 1 }, - { n: 'foundation', r: 1 }, - { n: 'generation', r: 1 }, - { n: 'id', r: 1 }, - { n: 'ip', r: 1 }, - { n: 'network', r: 1 }, - { n: 'port', r: 1 }, - { n: 'priority', r: 1 }, - { n: 'protocol', r: 1 }, - { n: 'rel-addr', r: 0 }, - { n: 'rel-port', r: 0 }, - { n: 'type', r: 1 } - ] - ); - } - } catch(e) { - self.get_debug().log('[JSJaCJingle] _util_stanza_parse_candidate > ' + e, 1); - } - - return candidate_arr; - }; - - /* - * @private - */ - self._util_stanza_build_node = function(doc, parent, children, name, ns, value) { - var node = null; - - try { - var i, child, attr; - - if(children && children.length) { - for(i in children) { - child = children[i]; - - if(!child) continue; - - node = parent.appendChild(doc.buildNode( - name, - { 'xmlns': ns }, - (value && child[value]) ? child[value] : null - )); - - for(attr in child) - if(attr != value) self.util_stanza_set_attribute(node, attr, child[attr]); - } - } - } catch(e) { - self.get_debug().log('[JSJaCJingle] _util_stanza_build_node > name: ' + name + ' > ' + e, 1); - } - - return node; - }; - - /** - * @private - */ - self._util_stanza_generate_jingle = function(stanza, attrs) { - var jingle = null; - - try { - var cur_attr; - - jingle = stanza.getNode().appendChild(stanza.buildNode('jingle', { 'xmlns': NS_JINGLE })); - - if(!attrs.sid) attrs.sid = self.get_sid(); - - for(cur_attr in attrs) self.util_stanza_set_attribute(jingle, cur_attr, attrs[cur_attr]); - } catch(e) { - self.get_debug().log('[JSJaCJingle] _util_stanza_generate_jingle > ' + e, 1); - } - - return jingle; - }; - - /** - * @private - */ - self._util_stanza_generate_session_info = function(stanza, jingle, args) { - try { - var info = jingle.appendChild(stanza.buildNode(args.info, { 'xmlns': NS_JINGLE_APPS_RTP_INFO })); - - // Info attributes - switch(args.info) { - case JSJAC_JINGLE_SESSION_INFO_MUTE: - case JSJAC_JINGLE_SESSION_INFO_UNMUTE: - self.util_stanza_set_attribute(info, 'creator', self.get_creator_this()); - self.util_stanza_set_attribute(info, 'name', args.name); - - break; - } - } catch(e) { - self.get_debug().log('[JSJaCJingle] _util_stanza_generate_session_info > ' + e, 1); - } - }; - - /** - * @private - */ - self._util_stanza_generate_content_local = function(stanza, jingle, override_content) { - try { - var cur_media; - var content_local = override_content ? override_content : self._get_content_local(); - - for(cur_media in content_local) { - var cur_content = content_local[cur_media]; - - var content = jingle.appendChild(stanza.buildNode('content', { 'xmlns': NS_JINGLE })); - - self.util_stanza_set_attribute(content, 'creator', cur_content.creator); - self.util_stanza_set_attribute(content, 'name', cur_content.name); - self.util_stanza_set_attribute(content, 'senders', cur_content.senders); - - // Build description (if action type allows that element) - if(self.util_stanza_get_attribute(jingle, 'action') != JSJAC_JINGLE_ACTION_TRANSPORT_INFO) { - var cs_description = cur_content.description; - var cs_d_attrs = cs_description.attrs; - var cs_d_rtcp_fb = cs_description['rtcp-fb']; - var cs_d_bandwidth = cs_description.bandwidth; - var cs_d_payload = cs_description.payload; - var cs_d_encryption = cs_description.encryption; - var cs_d_rtp_hdrext = cs_description['rtp-hdrext']; - var cs_d_rtcp_mux = cs_description['rtcp-mux']; - - var description = self._util_stanza_build_node( - stanza, content, - [cs_d_attrs], - 'description', - NS_JINGLE_APPS_RTP - ); - - // Payload-type - if(cs_d_payload) { - var i, cs_d_p, payload_type; - - for(i in cs_d_payload) { - cs_d_p = cs_d_payload[i]; - - payload_type = self._util_stanza_build_node( - stanza, - description, - [cs_d_p.attrs], - 'payload-type', - NS_JINGLE_APPS_RTP - ); - - // Parameter - self._util_stanza_build_node( - stanza, - payload_type, - cs_d_p.parameter, - 'parameter', - NS_JINGLE_APPS_RTP + // Payloads (non-destructive setters / cumulative) + this.parent._set_payloads_remote_add( + content_name, + this.stanza_parse_payload(cur_content) ); - // RTCP-FB (sub) - self._util_stanza_build_node( - stanza, - payload_type, - cs_d_p['rtcp-fb'], - 'rtcp-fb', - NS_JINGLE_APPS_RTP_RTCP_FB + // Candidates (enqueue them for ICE processing, too) + cur_candidate = this.stanza_parse_candidate(cur_content); + + this.parent._set_candidates_remote_add( + content_name, + cur_candidate ); - // RTCP-FB-TRR-INT - self._util_stanza_build_node( - stanza, - payload_type, - cs_d_p['rtcp-fb-trr-int'], - 'rtcp-fb-trr-int', - NS_JINGLE_APPS_RTP_RTCP_FB + this.parent._set_candidates_queue_remote( + content_name, + cur_candidate ); } - // Encryption - if(cs_d_encryption && - (cs_d_encryption.crypto && cs_d_encryption.crypto.length || - cs_d_encryption['zrtp-hash'] && cs_d_encryption['zrtp-hash'].length)) { - var encryption = description.appendChild(stanza.buildNode('encryption', { 'xmlns': NS_JINGLE_APPS_RTP })); + return true; + } + } + } catch(e) { + this.parent.get_debug().log('[JSJaCJingle:utils] stanza_parse_content > ' + e, 1); + } - self.util_stanza_set_attribute(encryption, 'required', (cs_d_encryption.attrs.required || '0')); + return false; + }, - // Crypto - self._util_stanza_build_node( - stanza, - encryption, - cs_d_encryption.crypto, - 'crypto', - NS_JINGLE_APPS_RTP - ); + /** + * Parses stanza group + * @public + * @param {JSJaCPacket} stanza + * @returns {Boolean} Success + */ + stanza_parse_group: function(stanza) { + try { + var i, j, + jingle, + group, cur_group, + content, cur_content, group_content_names; - // ZRTP-HASH - self._util_stanza_build_node( - stanza, - encryption, - cs_d_encryption['zrtp-hash'], - 'zrtp-hash', - NS_JINGLE_APPS_RTP_ZRTP, - 'value' + // Parse initiate stanza + jingle = this.stanza_jingle(stanza); + + if(jingle) { + // Childs + group = this.stanza_get_element(jingle, 'group', NS_JINGLE_APPS_GROUPING); + + if(group && group.length) { + for(i = 0; i < group.length; i++) { + cur_group = group[i]; + group_content_names = []; + + // Attrs + group_semantics = this.stanza_get_attribute(cur_group, 'semantics'); + + // Contents + content = this.stanza_get_element(cur_group, 'content', NS_JINGLE_APPS_GROUPING); + + for(j = 0; j < content.length; j++) { + cur_content = content[j]; + + // Content attrs + group_content_names.push( + this.stanza_get_attribute(cur_content, 'name') + ); + } + + // Payloads (non-destructive setters / cumulative) + this.parent._set_group_remote( + group_semantics, + group_content_names ); } - - // RTCP-FB (common) - self._util_stanza_build_node( - stanza, - description, - cs_d_rtcp_fb, - 'rtcp-fb', - NS_JINGLE_APPS_RTP_RTCP_FB - ); - - // Bandwidth - self._util_stanza_build_node( - stanza, - description, - cs_d_bandwidth, - 'bandwidth', - NS_JINGLE_APPS_RTP, - 'value' - ); - - // RTP-HDREXT - self._util_stanza_build_node( - stanza, - description, - cs_d_rtp_hdrext, - 'rtp-hdrext', - NS_JINGLE_APPS_RTP_RTP_HDREXT - ); - - // RTCP-MUX - if(cs_d_rtcp_mux) - description.appendChild(stanza.buildNode('rtcp-mux', { 'xmlns': NS_JINGLE_APPS_RTP })); } } - // Build transport - var cs_transport = cur_content.transport; - - var transport = self._util_stanza_build_node( - stanza, - content, - [cs_transport.attrs], - 'transport', - NS_JINGLE_TRANSPORTS_ICEUDP - ); - - // Fingerprint - self._util_stanza_build_node( - stanza, - transport, - [cs_transport.fingerprint], - 'fingerprint', - NS_JINGLE_APPS_DTLS, - 'value' - ); - - // Candidates - self._util_stanza_build_node( - stanza, - transport, - cs_transport.candidate, - 'candidate', - NS_JINGLE_TRANSPORTS_ICEUDP - ); - } - } catch(e) { - self.get_debug().log('[JSJaCJingle] _util_stanza_generate_content_local > ' + e, 1); - } - }; - - /** - * @private - */ - self._util_stanza_generate_group_local = function(stanza, jingle) { - try { - var i, - cur_semantics, cur_group, cur_group_name, - group; - - var group_local = self._get_group_local(); - - for(cur_semantics in group_local) { - cur_group = group_local[cur_semantics]; - - group = jingle.appendChild(stanza.buildNode('group', { - 'xmlns': NS_JINGLE_APPS_GROUPING, - 'semantics': cur_semantics - })); - - for(i in cur_group) { - cur_group_name = cur_group[i]; - - group.appendChild(stanza.buildNode('content', { - 'xmlns': NS_JINGLE_APPS_GROUPING, - 'name': cur_group_name - })); - } - } - } catch(e) { - self.get_debug().log('[JSJaCJingle] _util_stanza_generate_group_local > ' + e, 1); - } - }; - - /** - * @private - */ - self._util_generate_content = function(creator, name, senders, payloads, transports) { - var content_obj = {}; - - try { - // Generation process - content_obj.creator = creator; - content_obj.name = name; - content_obj.senders = senders; - content_obj.description = {}; - content_obj.transport = {}; - - // Generate description - var i; - var description_cpy = self.util_object_clone(payloads.descriptions); - var description_ptime = description_cpy.attrs.ptime; - var description_maxptime = description_cpy.attrs.maxptime; - - if(description_ptime) delete description_cpy.attrs.ptime; - if(description_maxptime) delete description_cpy.attrs.maxptime; - - for(i in description_cpy.payload) { - if(!('attrs' in description_cpy.payload[i])) - description_cpy.payload[i].attrs = {}; - - description_cpy.payload[i].attrs.ptime = description_ptime; - description_cpy.payload[i].attrs.maxptime = description_maxptime; + return true; + } catch(e) { + this.parent.get_debug().log('[JSJaCJingle:utils] stanza_parse_group > ' + e, 1); } - content_obj.description = description_cpy; - - // Generate transport - content_obj.transport.candidate = transports; - content_obj.transport.attrs = {}; - content_obj.transport.attrs.pwd = payloads.transports ? payloads.transports.pwd : null; - content_obj.transport.attrs.ufrag = payloads.transports ? payloads.transports.ufrag : null; - - if(payloads.transports && payloads.transports.fingerprint) - content_obj.transport.fingerprint = payloads.transports.fingerprint; - } catch(e) { - self.get_debug().log('[JSJaCJingle] _util_generate_content > ' + e, 1); - } - - return content_obj; - }; - - /** - * @private - */ - self._util_build_content_local = function() { - try { - var cur_name; - - for(cur_name in self.get_name()) { - self._set_content_local( - cur_name, - - self._util_generate_content( - JSJAC_JINGLE_SENDERS_INITIATOR.jingle, - cur_name, - self.get_senders(cur_name), - self._get_payloads_local(cur_name), - self._get_candidates_local(cur_name) - ) - ); - } - } catch(e) { - self.get_debug().log('[JSJaCJingle] _util_build_content_local > ' + e, 1); - } - }; - - /** - * @private - */ - self._util_build_content_remote = function() { - try { - var cur_name; - - for(cur_name in self.get_name()) { - self._set_content_remote( - cur_name, - - self._util_generate_content( - self.get_creator(cur_name), - cur_name, - self.get_senders(cur_name), - self._get_payloads_remote(cur_name), - self._get_candidates_remote(cur_name) - ) - ); - } - } catch(e) { - self.get_debug().log('[JSJaCJingle] _util_build_content_remote > ' + e, 1); - } - }; - - /** - * @private - */ - self._util_name_generate = function(media) { - var name = null; - - try { - var i, cur_name; - - var content_all = [ - self._get_content_remote(), - self._get_content_local() - ]; - - for(i in content_all) { - for(cur_name in content_all[i]) { - try { - if(content_all[i][cur_name].description.attrs.media == media) { - name = cur_name; break; - } - } catch(e) {} - } - - if(name) break; - } - - if(!name) name = media; - } catch(e) { - self.get_debug().log('[JSJaCJingle] _util_name_generate > ' + e, 1); - } - - return name; - }; - - /** - * @private - */ - self._util_media_generate = function(name) { - var cur_media; - var media = null; - - try { - if(typeof name == 'number') { - for(cur_media in JSJAC_JINGLE_MEDIAS) { - if(name == parseInt(JSJAC_JINGLE_MEDIAS[cur_media].label, 10)) { - media = cur_media; break; - } - } - } else { - for(cur_media in JSJAC_JINGLE_MEDIAS) { - if(name == self._util_name_generate(cur_media)) { - media = cur_media; break; - } - } - } - - if(!media) media = name; - } catch(e) { - self.get_debug().log('[JSJaCJingle] _util_media_generate > ' + e, 1); - } - - return media; - }; - - /** - * @private - */ - self._util_sdp_generate = function(type, group, payloads, candidates) { - try { - var sdp_obj = {}; - - sdp_obj.candidates = self._util_sdp_generate_candidates(candidates); - sdp_obj.description = self._util_sdp_generate_description(type, group, payloads, sdp_obj.candidates); - - return sdp_obj; - } catch(e) { - self.get_debug().log('[JSJaCJingle] _util_sdp_generate > ' + e, 1); - } - - return {}; - }; - - /** - * @private - */ - self._util_sdp_generate_candidates = function(candidates) { - var candidates_arr = []; - - try { - // Parse candidates - var i, - cur_media, cur_name, cur_c_name, cur_candidate, cur_label, cur_id, cur_candidate_str; - - for(cur_name in candidates) { - cur_c_name = candidates[cur_name]; - cur_media = self._util_media_generate(cur_name); - - for(i in cur_c_name) { - cur_candidate = cur_c_name[i]; - - cur_label = JSJAC_JINGLE_MEDIAS[cur_media].label; - cur_id = cur_label; - cur_candidate_str = ''; - - cur_candidate_str += 'a=candidate:'; - cur_candidate_str += cur_candidate.foundation; - cur_candidate_str += ' '; - cur_candidate_str += cur_candidate.component; - cur_candidate_str += ' '; - cur_candidate_str += cur_candidate.protocol; - cur_candidate_str += ' '; - cur_candidate_str += cur_candidate.priority; - cur_candidate_str += ' '; - cur_candidate_str += cur_candidate.ip; - cur_candidate_str += ' '; - cur_candidate_str += cur_candidate.port; - cur_candidate_str += ' '; - cur_candidate_str += 'typ'; - cur_candidate_str += ' '; - cur_candidate_str += cur_candidate.type; - - if(cur_candidate['rel-addr'] && cur_candidate['rel-port']) { - cur_candidate_str += ' '; - cur_candidate_str += 'raddr'; - cur_candidate_str += ' '; - cur_candidate_str += cur_candidate['rel-addr']; - cur_candidate_str += ' '; - cur_candidate_str += 'rport'; - cur_candidate_str += ' '; - cur_candidate_str += cur_candidate['rel-port']; - } - - if(cur_candidate.generation) { - cur_candidate_str += ' '; - cur_candidate_str += 'generation'; - cur_candidate_str += ' '; - cur_candidate_str += cur_candidate.generation; - } - - cur_candidate_str += WEBRTC_SDP_LINE_BREAK; - - candidates_arr.push({ - label : cur_label, - id : cur_id, - candidate : cur_candidate_str - }); - } - } - } catch(e) { - self.get_debug().log('[JSJaCJingle] _util_sdp_generate_candidates > ' + e, 1); - } - - return candidates_arr; - }; - - /** - * @private - */ - self._util_sdp_generate_description = function(type, group, payloads, sdp_candidates) { - var payloads_obj = {}; - - try { - var payloads_str = ''; - - // Common vars - var i, c, j, k, l, m, n, o, p, q, r, s, t, - cur_name, cur_name_obj, - cur_media, cur_senders, - cur_group_semantics, cur_group_names, cur_group_name, - cur_transports_obj, cur_description_obj, - cur_d_pwd, cur_d_ufrag, cur_d_fingerprint, - cur_d_attrs, cur_d_rtcp_fb, cur_d_bandwidth, cur_d_encryption, cur_d_ssrc, - cur_d_ssrc_obj, cur_d_rtcp_fb_obj, - cur_d_payload, cur_d_payload_obj, cur_d_payload_obj_attrs, cur_d_payload_obj_id, - cur_d_payload_obj_parameter, cur_d_payload_obj_parameter_obj, cur_d_payload_obj_parameter_str, - cur_d_payload_obj_rtcp_fb, cur_d_payload_obj_rtcp_fb_obj, - cur_d_payload_obj_rtcp_fb_ttr_int, cur_d_payload_obj_rtcp_fb_ttr_int_obj, - cur_d_crypto_obj, cur_d_zrtp_hash_obj, - cur_d_rtp_hdrext, cur_d_rtp_hdrext_obj, - cur_d_rtcp_mux; - - // Payloads headers - payloads_str += self._util_sdp_generate_protocol_version(); - payloads_str += WEBRTC_SDP_LINE_BREAK; - payloads_str += self._util_sdp_generate_origin(); - payloads_str += WEBRTC_SDP_LINE_BREAK; - payloads_str += self._util_sdp_generate_session_name(); - payloads_str += WEBRTC_SDP_LINE_BREAK; - payloads_str += self._util_sdp_generate_timing(); - payloads_str += WEBRTC_SDP_LINE_BREAK; - - // Add groups - for(cur_group_semantics in group) { - cur_group_names = group[cur_group_semantics]; - - payloads_str += 'a=group:' + cur_group_semantics; - - for(t in cur_group_names) { - cur_group_name = cur_group_names[t]; - payloads_str += ' ' + cur_group_name; - } - - payloads_str += WEBRTC_SDP_LINE_BREAK; - } - - // Add media groups - for(cur_name in payloads) { - cur_name_obj = payloads[cur_name]; - cur_senders = self.get_senders(cur_name); - cur_media = self.get_name(cur_name) ? self._util_media_generate(cur_name) : null; - - // No media? - if(!cur_media) continue; - - // Transports - cur_transports_obj = cur_name_obj.transports || {}; - cur_d_pwd = cur_transports_obj.pwd; - cur_d_ufrag = cur_transports_obj.ufrag; - cur_d_fingerprint = cur_transports_obj.fingerprint; - - // Descriptions - cur_description_obj = cur_name_obj.descriptions; - cur_d_attrs = cur_description_obj.attrs; - cur_d_rtcp_fb = cur_description_obj['rtcp-fb']; - cur_d_bandwidth = cur_description_obj.bandwidth; - cur_d_payload = cur_description_obj.payload; - cur_d_encryption = cur_description_obj.encryption; - cur_d_ssrc = cur_description_obj.ssrc; - cur_d_rtp_hdrext = cur_description_obj['rtp-hdrext']; - cur_d_rtcp_mux = cur_description_obj['rtcp-mux']; - - // Current media - payloads_str += self._util_sdp_generate_description_media(cur_media, cur_d_encryption, cur_d_fingerprint, cur_d_payload); - payloads_str += WEBRTC_SDP_LINE_BREAK; - - payloads_str += 'c=IN IP4 0.0.0.0'; - payloads_str += WEBRTC_SDP_LINE_BREAK; - payloads_str += 'a=rtcp:1 IN IP4 0.0.0.0'; - payloads_str += WEBRTC_SDP_LINE_BREAK; - - if(cur_d_ufrag) payloads_str += 'a=ice-ufrag:' + cur_d_ufrag + WEBRTC_SDP_LINE_BREAK; - if(cur_d_pwd) payloads_str += 'a=ice-pwd:' + cur_d_pwd + WEBRTC_SDP_LINE_BREAK; - - // Fingerprint - if(cur_d_fingerprint) { - if(cur_d_fingerprint.hash && cur_d_fingerprint.value) { - payloads_str += 'a=fingerprint:' + cur_d_fingerprint.hash + ' ' + cur_d_fingerprint.value; - payloads_str += WEBRTC_SDP_LINE_BREAK; - } - - if(cur_d_fingerprint.setup) { - payloads_str += 'a=setup:' + cur_d_fingerprint.setup; - payloads_str += WEBRTC_SDP_LINE_BREAK; - } - } - - // RTP-HDREXT - if(cur_d_rtp_hdrext && cur_d_rtp_hdrext.length) { - for(i in cur_d_rtp_hdrext) { - cur_d_rtp_hdrext_obj = cur_d_rtp_hdrext[i]; - - payloads_str += 'a=extmap:' + cur_d_rtp_hdrext_obj.id; - - if(cur_d_rtp_hdrext_obj.senders) - payloads_str += '/' + cur_d_rtp_hdrext_obj.senders; - - payloads_str += ' ' + cur_d_rtp_hdrext_obj.uri; - payloads_str += WEBRTC_SDP_LINE_BREAK; - } - } - - // Senders - if(cur_senders) { - payloads_str += 'a=' + JSJAC_JINGLE_SENDERS[cur_senders]; - payloads_str += WEBRTC_SDP_LINE_BREAK; - } - - // Name - if(cur_media && JSJAC_JINGLE_MEDIAS[cur_media]) { - payloads_str += 'a=mid:' + (JSJAC_JINGLE_MEDIAS[cur_media]).label; - payloads_str += WEBRTC_SDP_LINE_BREAK; - } - - // RTCP-MUX - // WARNING: no spec! - // See: http://code.google.com/p/libjingle/issues/detail?id=309 - // http://mail.jabber.org/pipermail/jingle/2011-December/001761.html - if(cur_d_rtcp_mux) { - payloads_str += 'a=rtcp-mux'; - payloads_str += WEBRTC_SDP_LINE_BREAK; - } - - // 'encryption' - if(cur_d_encryption) { - // 'crypto' - for(j in cur_d_encryption.crypto) { - cur_d_crypto_obj = cur_d_encryption.crypto[j]; - - payloads_str += 'a=crypto:' + - cur_d_crypto_obj.tag + ' ' + - cur_d_crypto_obj['crypto-suite'] + ' ' + - cur_d_crypto_obj['key-params'] + - (cur_d_crypto_obj['session-params'] ? (' ' + cur_d_crypto_obj['session-params']) : ''); - - payloads_str += WEBRTC_SDP_LINE_BREAK; - } - - // 'zrtp-hash' - for(p in cur_d_encryption['zrtp-hash']) { - cur_d_zrtp_hash_obj = cur_d_encryption['zrtp-hash'][p]; - - payloads_str += 'a=zrtp-hash:' + - cur_d_zrtp_hash_obj.version + ' ' + - cur_d_zrtp_hash_obj.value; - - payloads_str += WEBRTC_SDP_LINE_BREAK; - } - } - - // 'rtcp-fb' (common) - for(n in cur_d_rtcp_fb) { - cur_d_rtcp_fb_obj = cur_d_rtcp_fb[n]; - - payloads_str += 'a=rtcp-fb:*'; - payloads_str += ' ' + cur_d_rtcp_fb_obj.type; - - if(cur_d_rtcp_fb_obj.subtype) - payloads_str += ' ' + cur_d_rtcp_fb_obj.subtype; - - payloads_str += WEBRTC_SDP_LINE_BREAK; - } - - // 'bandwidth' (common) - for(q in cur_d_bandwidth) { - cur_d_bandwidth_obj = cur_d_bandwidth[q]; - - payloads_str += 'b=' + cur_d_bandwidth_obj.type; - payloads_str += ':' + cur_d_bandwidth_obj.value; - payloads_str += WEBRTC_SDP_LINE_BREAK; - } - - // 'payload-type' - for(k in cur_d_payload) { - cur_d_payload_obj = cur_d_payload[k]; - cur_d_payload_obj_attrs = cur_d_payload_obj.attrs; - cur_d_payload_obj_parameter = cur_d_payload_obj.parameter; - cur_d_payload_obj_rtcp_fb = cur_d_payload_obj['rtcp-fb']; - cur_d_payload_obj_rtcp_fb_ttr_int = cur_d_payload_obj['rtcp-fb-trr-int']; - - cur_d_payload_obj_id = cur_d_payload_obj_attrs.id; - - payloads_str += 'a=rtpmap:' + cur_d_payload_obj_id; - - // 'rtpmap' - if(cur_d_payload_obj_attrs.name) { - payloads_str += ' ' + cur_d_payload_obj_attrs.name; - - if(cur_d_payload_obj_attrs.clockrate) { - payloads_str += '/' + cur_d_payload_obj_attrs.clockrate; - - if(cur_d_payload_obj_attrs.channels) - payloads_str += '/' + cur_d_payload_obj_attrs.channels; - } - } - - payloads_str += WEBRTC_SDP_LINE_BREAK; - - // 'parameter' - if(cur_d_payload_obj_parameter.length) { - payloads_str += 'a=fmtp:' + cur_d_payload_obj_id + ' '; - cur_d_payload_obj_parameter_str = ''; - - for(o in cur_d_payload_obj_parameter) { - cur_d_payload_obj_parameter_obj = cur_d_payload_obj_parameter[o]; - - if(cur_d_payload_obj_parameter_str) cur_d_payload_obj_parameter_str += ';'; - - cur_d_payload_obj_parameter_str += cur_d_payload_obj_parameter_obj.name; - - if(cur_d_payload_obj_parameter_obj.value !== null) { - cur_d_payload_obj_parameter_str += '='; - cur_d_payload_obj_parameter_str += cur_d_payload_obj_parameter_obj.value; - } - } - - payloads_str += cur_d_payload_obj_parameter_str; - payloads_str += WEBRTC_SDP_LINE_BREAK; - } - - // 'rtcp-fb' (sub) - for(l in cur_d_payload_obj_rtcp_fb) { - cur_d_payload_obj_rtcp_fb_obj = cur_d_payload_obj_rtcp_fb[l]; - - payloads_str += 'a=rtcp-fb:' + cur_d_payload_obj_id; - payloads_str += ' ' + cur_d_payload_obj_rtcp_fb_obj.type; - - if(cur_d_payload_obj_rtcp_fb_obj.subtype) - payloads_str += ' ' + cur_d_payload_obj_rtcp_fb_obj.subtype; - - payloads_str += WEBRTC_SDP_LINE_BREAK; - } - - // 'rtcp-fb-ttr-int' - for(m in cur_d_payload_obj_rtcp_fb_ttr_int) { - cur_d_payload_obj_rtcp_fb_ttr_int_obj = cur_d_payload_obj_rtcp_fb_ttr_int[m]; - - payloads_str += 'a=rtcp-fb:' + cur_d_payload_obj_id; - payloads_str += ' ' + 'trr-int'; - payloads_str += ' ' + cur_d_payload_obj_rtcp_fb_ttr_int_obj.value; - payloads_str += WEBRTC_SDP_LINE_BREAK; - } - } - - if(cur_d_attrs.ptime) payloads_str += 'a=ptime:' + cur_d_attrs.ptime + WEBRTC_SDP_LINE_BREAK; - if(cur_d_attrs.maxptime) payloads_str += 'a=maxptime:' + cur_d_attrs.maxptime + WEBRTC_SDP_LINE_BREAK; - - // 'ssrc' (not used in Jingle ATM) - for(r in cur_d_ssrc) { - for(s in cur_d_ssrc[r]) { - cur_d_ssrc_obj = cur_d_ssrc[r][s]; - - payloads_str += 'a=ssrc'; - payloads_str += ':' + cur_d_ssrc_obj.id; - payloads_str += ' ' + cur_d_ssrc_obj.attribute; - - if(cur_d_ssrc_obj.value) - payloads_str += ':' + cur_d_ssrc_obj.value; - - if(cur_d_ssrc_obj.data) - payloads_str += ' ' + cur_d_ssrc_obj.data; - - payloads_str += WEBRTC_SDP_LINE_BREAK; - } - } - - // Candidates (some browsers require them there, too) - if(typeof sdp_candidates == 'object') { - for(c in sdp_candidates) { - if((sdp_candidates[c]).label == JSJAC_JINGLE_MEDIAS[cur_media].label) - payloads_str += (sdp_candidates[c]).candidate; - } - } - } - - // Push to object - payloads_obj.type = type; - payloads_obj.sdp = payloads_str; - } catch(e) { - self.get_debug().log('[JSJaCJingle] _util_sdp_generate_description > ' + e, 1); - } - - return payloads_obj; - }; - - /** - * @private - */ - self._util_sdp_generate_protocol_version = function() { - return 'v=0'; - }; - - /** - * @private - */ - self._util_sdp_generate_origin = function() { - var sdp_origin = ''; - - try { - // Values - var jid = new JSJaCJID(self.get_initiator()); - - var username = jid.getNode() ? jid.getNode() : '-'; - var session_id = '1'; - var session_version = '1'; - var nettype = 'IN'; - var addrtype = 'IP4'; - var unicast_address = jid.getDomain() ? jid.getDomain() : '127.0.0.1'; - - // Line content - sdp_origin += 'o='; - sdp_origin += username + ' '; - sdp_origin += session_id + ' '; - sdp_origin += session_version + ' '; - sdp_origin += nettype + ' '; - sdp_origin += addrtype + ' '; - sdp_origin += unicast_address; - } catch(e) { - self.get_debug().log('[JSJaCJingle] _util_sdp_generate_origin > ' + e, 1); - } - - return sdp_origin; - }; - - /** - * @private - */ - self._util_sdp_generate_session_name = function() { - return 's=' + (self.get_sid() || '-'); - }; - - /** - * @private - */ - self._util_sdp_generate_timing = function() { - return 't=0 0'; - }; - - /** - * @private - */ - self._util_sdp_generate_description_media = function(media, crypto, fingerprint, payload) { - var sdp_media = ''; - - try { - var i; - var type_ids = []; - - sdp_media += 'm=' + media + ' 1 '; - - // Protocol - if((crypto && crypto.length) || (fingerprint && fingerprint.hash && fingerprint.value)) - sdp_media += 'RTP/SAVPF'; - else - sdp_media += 'RTP/AVPF'; - - // Payload type IDs - for(i in payload) type_ids.push(payload[i].attrs.id); - - sdp_media += ' ' + type_ids.join(' '); - } catch(e) { - self.get_debug().log('[JSJaCJingle] _util_sdp_generate_description_media > ' + e, 1); - } - - return sdp_media; - }; - - /** - * Generates a random SID value - * @return SID value - * @type string - */ - self.util_generate_sid = function() { - return cnonce(16); - }; - - /** - * Generates a random ID value - * @return ID value - * @type string - */ - self.util_generate_id = function() { - return cnonce(10); - }; - - /** - * Generates the constraints object - * @return constraints object - * @type object - */ - self.util_generate_constraints = function() { - var constraints = { - audio : false, - video : false - }; - - try { - // Medias? - constraints.audio = true; - constraints.video = (self.get_media() == JSJAC_JINGLE_MEDIA_VIDEO); - - // Video configuration - if(constraints.video === true) { - // Resolution? - switch(self.get_resolution()) { - // 16:9 - case '720': - case 'hd': - constraints.video = { - mandatory : { - minWidth : 1280, - minHeight : 720, - minAspectRatio : 1.77 - } - }; - break; - - case '360': - case 'md': - constraints.video = { - mandatory : { - minWidth : 640, - minHeight : 360, - minAspectRatio : 1.77 - } - }; - break; - - case '180': - case 'sd': - constraints.video = { - mandatory : { - minWidth : 320, - minHeight : 180, - minAspectRatio : 1.77 - } - }; - break; - - // 4:3 - case '960': - constraints.video = { - mandatory : { - minWidth : 960, - minHeight : 720 - } - }; - break; - - case '640': - case 'vga': - constraints.video = { - mandatory : { - maxWidth : 640, - maxHeight : 480 - } - }; - break; - - case '320': - constraints.video = { - mandatory : { - maxWidth : 320, - maxHeight : 240 - } - }; - break; - } - - // Bandwidth? - if(self.get_bandwidth()) - constraints.video.optional = [{ bandwidth: self.get_bandwidth() }]; - - // FPS? - if(self.get_fps()) - constraints.video.mandatory.minFrameRate = self.get_fps(); - - // Custom video source? (screenshare) - if(self.get_media() == JSJAC_JINGLE_MEDIA_VIDEO && - self.get_video_source() != JSJAC_JINGLE_VIDEO_SOURCE_CAMERA ) { - if(document.location.protocol !== 'https:') - self.get_debug().log('[JSJaCJingle] util_generate_constraints > HTTPS might be required to share screen, otherwise you may get a permission denied error.', 0); - - // Unsupported browser? (for that feature) - if(self._util_browser().name != JSJAC_JINGLE_BROWSER_CHROME) { - self.get_debug().log('[JSJaCJingle] util_generate_constraints > Video source not supported by ' + self._util_browser().name + ' (source: ' + self.get_video_source() + ').', 1); - - self.terminate(JSJAC_JINGLE_REASON_MEDIA_ERROR); - return; - } - - constraints.audio = false; - constraints.video.mandatory = { - 'chromeMediaSource': self.get_video_source() + return false; + }, + + /** + * Parses stanza payload + * @public + * @param {DOM} stanza_content + * @returns {Object} Payload object + */ + stanza_parse_payload: function(stanza_content) { + var payload_obj = { + descriptions : {}, + transports : {} + }; + + try { + // Common vars + var j, k, l, error, + cur_ssrc, cur_ssrc_id, + cur_ssrc_group, cur_ssrc_group_semantics, + cur_payload, cur_payload_arr, cur_payload_id; + + // Common functions + var init_content = function() { + var ic_key; + var ic_arr = { + 'attrs' : {}, + 'rtcp-fb' : [], + 'bandwidth' : [], + 'payload' : {}, + 'rtp-hdrext' : [], + 'rtcp-mux' : 0, + + 'encryption' : { + 'attrs' : {}, + 'crypto' : [], + 'zrtp-hash' : [] + }, + + 'ssrc': {}, + 'ssrc-group': {} }; - } - } - } catch(e) { - self.get_debug().log('[JSJaCJingle] util_generate_constraints > ' + e, 1); - } - return constraints; - }; + for(ic_key in ic_arr) + if(!(ic_key in payload_obj.descriptions)) payload_obj.descriptions[ic_key] = ic_arr[ic_key]; + }; - /** - * Returns our negotiation status - * @return Negotiation status - * @type string - */ - self.util_negotiation_status = function() { - return (self.get_initiator() == self.util_connection_jid()) ? JSJAC_JINGLE_SENDERS_INITIATOR.jingle : JSJAC_JINGLE_SENDERS_RESPONDER.jingle; - }; - - /** - * Get my connection JID - * @return JID value - * @type string - */ - self.util_connection_jid = function() { - return JSJAC_JINGLE_STORE_CONNECTION.username + '@' + - JSJAC_JINGLE_STORE_CONNECTION.domain + '/' + - JSJAC_JINGLE_STORE_CONNECTION.resource; - }; - - /** - * @private - */ - self._util_map_register_view = function(type) { - var fn = { - type : null, - mute : false, - - view : { - get : null, - set : null - }, - - stream : { - get : null, - set : null - } - }; - - try { - switch(type) { - case 'local': - fn.type = type; - fn.mute = true; - fn.view.get = self.get_local_view; - fn.view.set = self._set_local_view; - fn.stream.get = self._get_local_stream; - fn.stream.set = self._set_local_stream; - break; - - case 'remote': - fn.type = type; - fn.view.get = self.get_remote_view; - fn.view.set = self._set_remote_view; - fn.stream.get = self._get_remote_stream; - fn.stream.set = self._set_remote_stream; - break; - } - } catch(e) { - self.get_debug().log('[JSJaCJingle] _util_map_register_view > ' + e, 1); - } - - return fn; - }; - - /** - * @private - */ - self._util_map_unregister_view = function(type) { - return self._util_map_register_view(type); - }; - - /** - * @private - */ - self._util_peer_stream_attach = function(element, stream, mute) { - try { - var i; - var stream_src = stream ? URL.createObjectURL(stream) : ''; - - for(i in element) { - element[i].src = stream_src; - - if(navigator.mozGetUserMedia) - element[i].play(); - else - element[i].autoplay = true; - - if(typeof mute == 'boolean') element[i].muted = mute; - } - } catch(e) { - self.get_debug().log('[JSJaCJingle] _util_peer_stream_attach > ' + e, 1); - } - }; - - /** - * @private - */ - self._util_peer_stream_detach = function(element) { - try { - var i; - - for(i in element) { - element[i].pause(); - element[i].src = ''; - } - } catch(e) { - self.get_debug().log('[JSJaCJingle] _util_peer_stream_detach > ' + e, 1); - } - }; - - /** - * @private - */ - self._util_sdp_parse_payload = function(sdp_payload) { - var payload = {}; - - try { - if(!sdp_payload || sdp_payload.indexOf('\n') == -1) return payload; - - // Common vars - var lines = sdp_payload.split('\n'); - var cur_name = null; - var cur_media = null; - - var common_transports = { - 'fingerprint' : {}, - 'pwd' : null, - 'ufrag' : null - }; - - var error, i, j, - cur_line, - cur_fmtp, cur_fmtp_id, cur_fmtp_values, cur_fmtp_attrs, cur_fmtp_key, cur_fmtp_value, - cur_rtpmap, cur_rtcp_fb, cur_rtcp_fb_trr_int, - cur_crypto, cur_zrtp_hash, cur_fingerprint, cur_ssrc, cur_extmap, - cur_rtpmap_id, cur_rtcp_fb_id, cur_bandwidth, - m_rtpmap, m_fmtp, m_rtcp_fb, m_rtcp_fb_trr_int, m_crypto, m_zrtp_hash, - m_fingerprint, m_pwd, m_ufrag, m_ptime, m_maxptime, m_bandwidth, m_media, m_candidate, - cur_check_name, cur_transport_sub; - - // Common functions - var init_content = function(name) { - if(!(name in payload)) payload[name] = {}; - }; - - var init_descriptions = function(name, sub, sub_default) { - init_content(name); - - if(!('descriptions' in payload[name])) payload[name].descriptions = {}; - if(!(sub in payload[name].descriptions)) payload[name].descriptions[sub] = sub_default; - }; - - var init_transports = function(name, sub, sub_default) { - init_content(name); - - if(!('transports' in payload[name])) payload[name].transports = {}; - if(!(sub in payload[name].transports)) payload[name].transports[sub] = sub_default; - }; - - var init_ssrc = function(name, id) { - init_descriptions(name, 'ssrc', {}); - - if(!(id in payload[name].descriptions.ssrc)) - payload[name].descriptions.ssrc[id] = []; - }; - - var init_payload = function(name, id) { - init_descriptions(name, 'payload', {}); - - if(!(id in payload[name].descriptions.payload)) { - payload[name].descriptions.payload[id] = { + var init_payload = function(id) { + var ip_key; + var ip_arr = { 'attrs' : {}, 'parameter' : [], 'rtcp-fb' : [], 'rtcp-fb-trr-int' : [] }; - } - }; - var init_encryption = function(name) { - init_descriptions(name, 'encryption', { - 'attrs' : { - 'required' : '1' - }, + if(!(id in payload_obj.descriptions.payload)) payload_obj.descriptions.payload[id] = {}; - 'crypto' : [], - 'zrtp-hash' : [] - }); - }; + for(ip_key in ip_arr) + if(!(ip_key in payload_obj.descriptions.payload[id])) payload_obj.descriptions.payload[id][ip_key] = ip_arr[ip_key]; + }; - for(i in lines) { - cur_line = lines[i]; + var init_ssrc_group_semantics = function(semantics) { + if(typeof payload_obj.descriptions['ssrc-group'][semantics] != 'object') + payload_obj.descriptions['ssrc-group'][semantics] = []; + }; - m_media = (R_WEBRTC_SDP_ICE_PAYLOAD.media).exec(cur_line); + // Parse session description + var description = this.stanza_get_element(stanza_content, 'description', NS_JINGLE_APPS_RTP); - // 'audio/video' line? - if(m_media) { - cur_media = m_media[1]; - cur_name = self._util_name_generate(cur_media); + if(description.length) { + description = description[0]; - // Push it to parent array - init_descriptions(cur_name, 'attrs', {}); - payload[cur_name].descriptions.attrs.media = cur_media; + var cd_media = this.stanza_get_attribute(description, 'media'); + var cd_ssrc = this.stanza_get_attribute(description, 'ssrc'); - continue; - } + if(!cd_media) + this.parent.get_debug().log('[JSJaCJingle:utils] stanza_parse_payload > No media attribute to ' + cc_name + ' stanza.', 1); - m_bandwidth = (R_WEBRTC_SDP_ICE_PAYLOAD.bandwidth).exec(cur_line); + // Initialize current description + init_content(); - // 'bandwidth' line? - if(m_bandwidth) { - // Populate current object - error = 0; - cur_bandwidth = {}; + payload_obj.descriptions.attrs.media = cd_media; + payload_obj.descriptions.attrs.ssrc = cd_ssrc; - cur_bandwidth.type = m_bandwidth[1] || error++; - cur_bandwidth.value = m_bandwidth[2] || error++; + // Loop on multiple payloads + var payload = this.stanza_get_element(description, 'payload-type', NS_JINGLE_APPS_RTP); - // Incomplete? - if(error !== 0) continue; + if(payload.length) { + for(j = 0; j < payload.length; j++) { + error = 0; + cur_payload = payload[j]; + cur_payload_arr = {}; - // Push it to parent array - init_descriptions(cur_name, 'bandwidth', []); - payload[cur_name].descriptions.bandwidth.push(cur_bandwidth); + cur_payload_arr.channels = this.stanza_get_attribute(cur_payload, 'channels'); + cur_payload_arr.clockrate = this.stanza_get_attribute(cur_payload, 'clockrate'); + cur_payload_arr.id = this.stanza_get_attribute(cur_payload, 'id') || error++; + cur_payload_arr.name = this.stanza_get_attribute(cur_payload, 'name'); - continue; - } + payload_obj.descriptions.attrs.ptime = this.stanza_get_attribute(cur_payload, 'ptime'); + payload_obj.descriptions.attrs.maxptime = this.stanza_get_attribute(cur_payload, 'maxptime'); - m_rtpmap = (R_WEBRTC_SDP_ICE_PAYLOAD.rtpmap).exec(cur_line); - - // 'rtpmap' line? - if(m_rtpmap) { - // Populate current object - error = 0; - cur_rtpmap = {}; - - cur_rtpmap.channels = m_rtpmap[6]; - cur_rtpmap.clockrate = m_rtpmap[4]; - cur_rtpmap.id = m_rtpmap[1] || error++; - cur_rtpmap.name = m_rtpmap[3]; - - // Incomplete? - if(error !== 0) continue; - - cur_rtpmap_id = cur_rtpmap.id; - - // Push it to parent array - init_payload(cur_name, cur_rtpmap_id); - payload[cur_name].descriptions.payload[cur_rtpmap_id].attrs = cur_rtpmap; - - continue; - } - - m_fmtp = (R_WEBRTC_SDP_ICE_PAYLOAD.fmtp).exec(cur_line); - - // 'fmtp' line? - if(m_fmtp) { - cur_fmtp_id = m_fmtp[1]; - - if(cur_fmtp_id) { - cur_fmtp_values = m_fmtp[2] ? (m_fmtp[2]).split(';') : []; - - for(j in cur_fmtp_values) { - // Parse current attribute - if(cur_fmtp_values[j].indexOf('=') !== -1) { - cur_fmtp_attrs = cur_fmtp_values[j].split('='); - cur_fmtp_key = cur_fmtp_attrs[0]; - cur_fmtp_value = cur_fmtp_attrs[1]; - - while(cur_fmtp_key.length && !cur_fmtp_key[0]) - cur_fmtp_key = cur_fmtp_key.substring(1); - } else { - cur_fmtp_key = cur_fmtp_values[j]; - cur_fmtp_value = null; - } - - // Populate current object - error = 0; - cur_fmtp = {}; - - cur_fmtp.name = cur_fmtp_key || error++; - cur_fmtp.value = cur_fmtp_value; - - // Incomplete? if(error !== 0) continue; - // Push it to parent array - init_payload(cur_name, cur_fmtp_id); - payload[cur_name].descriptions.payload[cur_fmtp_id].parameter.push(cur_fmtp); + // Initialize current payload + cur_payload_id = cur_payload_arr.id; + init_payload(cur_payload_id); + + // Push current payload + payload_obj.descriptions.payload[cur_payload_id].attrs = cur_payload_arr; + + // Loop on multiple parameters + this.stanza_parse_node( + cur_payload, + 'parameter', + NS_JINGLE_APPS_RTP, + payload_obj.descriptions.payload[cur_payload_id].parameter, + [ { n: 'name', r: 1 }, { n: 'value', r: 0 } ] + ); + + // Loop on multiple RTCP-FB + this.stanza_parse_node( + cur_payload, + 'rtcp-fb', + NS_JINGLE_APPS_RTP_RTCP_FB, + payload_obj.descriptions.payload[cur_payload_id]['rtcp-fb'], + [ { n: 'type', r: 1 }, { n: 'subtype', r: 0 } ] + ); + + // Loop on multiple RTCP-FB-TRR-INT + this.stanza_parse_node( + cur_payload, + 'rtcp-fb-trr-int', + NS_JINGLE_APPS_RTP_RTCP_FB, + payload_obj.descriptions.payload[cur_payload_id]['rtcp-fb-trr-int'], + [ { n: 'value', r: 1 } ] + ); } } - continue; - } + // Parse the encryption element + var encryption = this.stanza_get_element(description, 'encryption', NS_JINGLE_APPS_RTP); - m_rtcp_fb = (R_WEBRTC_SDP_ICE_PAYLOAD.rtcp_fb).exec(cur_line); + if(encryption.length) { + encryption = encryption[0]; - // 'rtcp-fb' line? - if(m_rtcp_fb) { - // Populate current object - error = 0; - cur_rtcp_fb = {}; + payload_obj.descriptions.encryption.attrs.required = this.stanza_get_attribute(encryption, 'required') || '0'; - cur_rtcp_fb.id = m_rtcp_fb[1] || error++; - cur_rtcp_fb.type = m_rtcp_fb[2]; - cur_rtcp_fb.subtype = m_rtcp_fb[4]; + // Loop on multiple cryptos + this.stanza_parse_node( + encryption, + 'crypto', + NS_JINGLE_APPS_RTP, + payload_obj.descriptions.encryption.crypto, + [ { n: 'crypto-suite', r: 1 }, { n: 'key-params', r: 1 }, { n: 'session-params', r: 0 }, { n: 'tag', r: 1 } ] + ); - // Incomplete? - if(error !== 0) continue; - - cur_rtcp_fb_id = cur_rtcp_fb.id; - - // Push it to parent array - if(cur_rtcp_fb_id == '*') { - init_descriptions(cur_name, 'rtcp-fb', []); - (payload[cur_name].descriptions['rtcp-fb']).push(cur_rtcp_fb); - } else { - init_payload(cur_name, cur_rtcp_fb_id); - (payload[cur_name].descriptions.payload[cur_rtcp_fb_id]['rtcp-fb']).push(cur_rtcp_fb); + // Loop on multiple zrtp-hash + this.stanza_parse_node( + encryption, + 'zrtp-hash', + NS_JINGLE_APPS_RTP_ZRTP, + payload_obj.descriptions.encryption['zrtp-hash'], + [ { n: 'version', r: 1 } ], + { n: 'value', r: 1 } + ); } - continue; - } + // Parse the SSRC-GROUP elements + var ssrc_group = this.stanza_get_element(description, 'ssrc-group', NS_JINGLE_APPS_RTP_SSMA); - m_rtcp_fb_trr_int = (R_WEBRTC_SDP_ICE_PAYLOAD.rtcp_fb_trr_int).exec(cur_line); + if(ssrc_group && ssrc_group.length) { + for(k = 0; k < ssrc_group.length; k++) { + cur_ssrc_group = ssrc_group[k]; + cur_ssrc_group_semantics = this.stanza_get_attribute(cur_ssrc_group, 'semantics') || null; - // 'rtcp-fb-trr-int' line? - if(m_rtcp_fb_trr_int) { - // Populate current object - error = 0; - cur_rtcp_fb_trr_int = {}; + if(cur_ssrc_group_semantics !== null) { + cur_ssrc_group_semantics_obj = { + 'sources': [] + }; - cur_rtcp_fb_trr_int.id = m_rtcp_fb_trr_int[1] || error++; - cur_rtcp_fb_trr_int.value = m_rtcp_fb_trr_int[2] || error++; + init_ssrc_group_semantics(cur_ssrc_group_semantics); - // Incomplete? - if(error !== 0) continue; + this.stanza_parse_node( + cur_ssrc_group, + 'source', + NS_JINGLE_APPS_RTP_SSMA, + cur_ssrc_group_semantics_obj.sources, + [ { n: 'ssrc', r: 1 } ] + ); - cur_rtcp_fb_trr_int_id = cur_rtcp_fb_trr_int.id; - - // Push it to parent array - init_payload(cur_name, cur_rtcp_fb_trr_int_id); - (payload[cur_name].descriptions.payload[cur_rtcp_fb_trr_int_id]['rtcp-fb-trr-int']).push(cur_rtcp_fb_trr_int); - - continue; - } - - m_crypto = (R_WEBRTC_SDP_ICE_PAYLOAD.crypto).exec(cur_line); - - // 'crypto' line? - if(m_crypto) { - // Populate current object - error = 0; - cur_crypto = {}; - - cur_crypto['crypto-suite'] = m_crypto[2] || error++; - cur_crypto['key-params'] = m_crypto[3] || error++; - cur_crypto['session-params'] = m_crypto[5]; - cur_crypto.tag = m_crypto[1] || error++; - - // Incomplete? - if(error !== 0) continue; - - // Push it to parent array - init_encryption(cur_name); - (payload[cur_name].descriptions.encryption.crypto).push(cur_crypto); - - continue; - } - - m_zrtp_hash = (R_WEBRTC_SDP_ICE_PAYLOAD.zrtp_hash).exec(cur_line); - - // 'zrtp-hash' line? - if(m_zrtp_hash) { - // Populate current object - error = 0; - cur_zrtp_hash = {}; - - cur_zrtp_hash.version = m_zrtp_hash[1] || error++; - cur_zrtp_hash.value = m_zrtp_hash[2] || error++; - - // Incomplete? - if(error !== 0) continue; - - // Push it to parent array - init_encryption(cur_name); - (payload[cur_name].descriptions.encryption['zrtp-hash']).push(cur_zrtp_hash); - - continue; - } - - m_ptime = (R_WEBRTC_SDP_ICE_PAYLOAD.ptime).exec(cur_line); - - // 'ptime' line? - if(m_ptime) { - // Push it to parent array - init_descriptions(cur_name, 'attrs', {}); - payload[cur_name].descriptions.attrs.ptime = m_ptime[1]; - - continue; - } - - m_maxptime = (R_WEBRTC_SDP_ICE_PAYLOAD.maxptime).exec(cur_line); - - // 'maxptime' line? - if(m_maxptime) { - // Push it to parent array - init_descriptions(cur_name, 'attrs', {}); - payload[cur_name].descriptions.attrs.maxptime = m_maxptime[1]; - - continue; - } - - m_ssrc = (R_WEBRTC_SDP_ICE_PAYLOAD.ssrc).exec(cur_line); - - // 'ssrc' line? - if(m_ssrc) { - // Populate current object - error = 0; - cur_ssrc = {}; - - cur_ssrc.id = m_ssrc[1] || error++; - cur_ssrc.attribute = m_ssrc[2] || error++; - cur_ssrc.value = m_ssrc[4]; - cur_ssrc.data = m_ssrc[6]; - - // Incomplete? - if(error !== 0) continue; - - // Push it to parent array (not used in Jingle ATM) - init_ssrc(cur_name, cur_ssrc.id); - (payload[cur_name].descriptions.ssrc[cur_ssrc.id]).push(cur_ssrc); - - // Push it to parent array (common attr required for Jingle) - init_descriptions(cur_name, 'attrs', {}); - payload[cur_name].descriptions.attrs.ssrc = m_ssrc[1]; - - continue; - } - - m_rtcp_mux = (R_WEBRTC_SDP_ICE_PAYLOAD.rtcp_mux).exec(cur_line); - - // 'rtcp-mux' line? - if(m_rtcp_mux) { - // Push it to parent array - init_descriptions(cur_name, 'rtcp-mux', 1); - - continue; - } - - m_extmap = (R_WEBRTC_SDP_ICE_PAYLOAD.extmap).exec(cur_line); - - // 'extmap' line? - if(m_extmap) { - // Populate current object - error = 0; - cur_extmap = {}; - - cur_extmap.id = m_extmap[1] || error++; - cur_extmap.uri = m_extmap[4] || error++; - cur_extmap.senders = m_extmap[3]; - - // Incomplete? - if(error !== 0) continue; - - // Push it to parent array - init_descriptions(cur_name, 'rtp-hdrext', []); - (payload[cur_name].descriptions['rtp-hdrext']).push(cur_extmap); - - continue; - } - - m_fingerprint = (R_WEBRTC_SDP_ICE_PAYLOAD.fingerprint).exec(cur_line); - - // 'fingerprint' line? - if(m_fingerprint) { - // Populate current object - error = 0; - cur_fingerprint = common_transports.fingerprint || {}; - - cur_fingerprint.hash = m_fingerprint[1] || error++; - cur_fingerprint.value = m_fingerprint[2] || error++; - - // Incomplete? - if(error !== 0) continue; - - // Push it to parent array - init_transports(cur_name, 'fingerprint', cur_fingerprint); - common_transports.fingerprint = cur_fingerprint; - - continue; - } - - m_setup = (R_WEBRTC_SDP_ICE_PAYLOAD.setup).exec(cur_line); - - // 'setup' line? - if(m_setup) { - // Populate current object - cur_fingerprint = common_transports.fingerprint || {}; - cur_fingerprint.setup = m_setup[1]; - - // Push it to parent array - if(cur_fingerprint.setup) { - // Map it to fingerprint as XML-wise it is related - init_transports(cur_name, 'fingerprint', cur_fingerprint); - common_transports.fingerprint = cur_fingerprint; + payload_obj.descriptions['ssrc-group'][cur_ssrc_group_semantics].push(cur_ssrc_group_semantics_obj); + } + } } - continue; + // Parse the SSRC (source) elements + var ssrc = this.stanza_get_element(description, 'source', NS_JINGLE_APPS_RTP_SSMA); + + if(ssrc && ssrc.length) { + for(l = 0; l < ssrc.length; l++) { + cur_ssrc = ssrc[l]; + cur_ssrc_id = this.stanza_get_attribute(cur_ssrc, 'ssrc') || null; + + if(cur_ssrc_id !== null) { + payload_obj.descriptions.ssrc[cur_ssrc_id] = []; + + this.stanza_parse_node( + cur_ssrc, + 'parameter', + NS_JINGLE_APPS_RTP_SSMA, + payload_obj.descriptions.ssrc[cur_ssrc_id], + [ { n: 'name', r: 1 }, { n: 'value', r: 0 } ] + ); + } + } + } + + // Loop on common RTCP-FB + this.stanza_parse_node( + description, + 'rtcp-fb', + NS_JINGLE_APPS_RTP_RTCP_FB, + payload_obj.descriptions['rtcp-fb'], + [ { n: 'type', r: 1 }, { n: 'subtype', r: 0 } ] + ); + + // Loop on bandwidth + this.stanza_parse_node( + description, + 'bandwidth', + NS_JINGLE_APPS_RTP, + payload_obj.descriptions.bandwidth, + [ { n: 'type', r: 1 } ], + { n: 'value', r: 1 } + ); + + // Parse the RTP-HDREXT element + this.stanza_parse_node( + description, + 'rtp-hdrext', + NS_JINGLE_APPS_RTP_RTP_HDREXT, + payload_obj.descriptions['rtp-hdrext'], + [ { n: 'id', r: 1 }, { n: 'uri', r: 1 }, { n: 'senders', r: 0 } ] + ); + + // Parse the RTCP-MUX element + var rtcp_mux = this.stanza_get_element(description, 'rtcp-mux', NS_JINGLE_APPS_RTP); + + if(rtcp_mux.length) { + payload_obj.descriptions['rtcp-mux'] = 1; + } } - m_pwd = (R_WEBRTC_SDP_ICE_PAYLOAD.pwd).exec(cur_line); + // Parse transport (need to get 'ufrag' and 'pwd' there) + var transport = this.stanza_get_element(stanza_content, 'transport', NS_JINGLE_TRANSPORTS_ICEUDP); - // 'pwd' line? - if(m_pwd) { - init_transports(cur_name, 'pwd', m_pwd[1]); + if(transport.length) { + payload_obj.transports.pwd = this.stanza_get_attribute(transport, 'pwd'); + payload_obj.transports.ufrag = this.stanza_get_attribute(transport, 'ufrag'); - if(!common_transports.pwd) - common_transports.pwd = m_pwd[1]; + var fingerprint = this.stanza_get_element(transport, 'fingerprint', NS_JINGLE_APPS_DTLS); - continue; - } - - m_ufrag = (R_WEBRTC_SDP_ICE_PAYLOAD.ufrag).exec(cur_line); - - // 'ufrag' line? - if(m_ufrag) { - init_transports(cur_name, 'ufrag', m_ufrag[1]); - - if(!common_transports.ufrag) - common_transports.ufrag = m_ufrag[1]; - - continue; - } - - // 'candidate' line? (shouldn't be there) - m_candidate = R_WEBRTC_SDP_ICE_CANDIDATE.exec(cur_line); - - if(m_candidate) { - self._util_sdp_parse_candidate_store({ - media : cur_media, - candidate : cur_line - }); - - continue; + if(fingerprint.length) { + payload_obj.transports.fingerprint = {}; + payload_obj.transports.fingerprint.setup = this.stanza_get_attribute(fingerprint, 'setup'); + payload_obj.transports.fingerprint.hash = this.stanza_get_attribute(fingerprint, 'hash'); + payload_obj.transports.fingerprint.value = this.stanza_get_value(fingerprint); + } } + } catch(e) { + this.parent.get_debug().log('[JSJaCJingle:utils] stanza_parse_payload > ' + e, 1); } - // Filter medias - for(cur_check_name in payload) { - // Undesired media? - if(!self.get_name()[cur_check_name]) { - delete payload[cur_check_name]; continue; - } + return payload_obj; + }, - // Validate transports - if(typeof payload[cur_check_name].transports !== 'object') - payload[cur_check_name].transports = {}; + /** + * Parses stanza candidate + * @public + * @param {Array} Candidates array + */ + stanza_parse_candidate: function(stanza_content) { + var candidate_arr = []; - for(cur_transport_sub in common_transports) { - if(!payload[cur_check_name].transports[cur_transport_sub]) - payload[cur_check_name].transports[cur_transport_sub] = common_transports[cur_transport_sub]; - } + try { + var _this = this; + + var fn_parse_transport = function(namespace, parse_obj) { + var transport = _this.stanza_get_element(stanza_content, 'transport', namespace); + + if(transport.length) { + _this.stanza_parse_node( + transport, + 'candidate', + namespace, + candidate_arr, + parse_obj + ); + } + }; + + // Parse ICE-UDP transport candidates + fn_parse_transport( + NS_JINGLE_TRANSPORTS_ICEUDP, + JSJAC_JINGLE_SDP_CANDIDATE_MAP_ICEUDP + ); + + // Parse RAW-UDP transport candidates + fn_parse_transport( + NS_JINGLE_TRANSPORTS_RAWUDP, + JSJAC_JINGLE_SDP_CANDIDATE_MAP_RAWUDP + ); + } catch(e) { + this.parent.get_debug().log('[JSJaCJingle:utils] stanza_parse_candidate > ' + e, 1); } - } catch(e) { - self.get_debug().log('[JSJaCJingle] _util_sdp_parse_payload > ' + e, 1); - } - return payload; - }; + return candidate_arr; + }, - /** - * @private - */ - self._util_sdp_parse_group = function(sdp_payload) { - var group = {}; + /* + * Builds stanza node + * @param {JSJaCPacket} doc + * @param {DOM} parent + * @param {Array} children + * @param {String} name + * @param {String} ns + * @param {String} [value] + * @returns {DOM} Built node + */ + stanza_build_node: function(doc, parent, children, name, ns, value) { + var node = null; - try { - if(!sdp_payload || sdp_payload.indexOf('\n') == -1) return group; + try { + var i, child, attr; - // Common vars - var lines = sdp_payload.split('\n'); - var i, cur_line, - m_group; + if(children && children.length) { + for(i in children) { + child = children[i]; - var init_group = function(semantics) { - if(!(semantics in group)) group[semantics] = []; + if(!child) continue; + + node = parent.appendChild(doc.buildNode( + name, + { 'xmlns': ns }, + (value && child[value]) ? child[value] : null + )); + + for(attr in child) + if(attr != value) this.stanza_set_attribute(node, attr, child[attr]); + } + } + } catch(e) { + this.parent.get_debug().log('[JSJaCJingle:utils] stanza_build_node > name: ' + name + ' > ' + e, 1); + } + + return node; + }, + + /** + * Generates stanza Jingle node + * @public + * @param {JSJaCPacket} stanza + * @param {Object} attrs + * @returns {DOM} Jingle node + */ + stanza_generate_jingle: function(stanza, attrs) { + var jingle = null; + + try { + var cur_attr; + + jingle = stanza.getNode().appendChild(stanza.buildNode('jingle', { 'xmlns': this.parent.get_namespace() })); + + if(!attrs.sid) attrs.sid = this.parent.get_sid(); + + for(cur_attr in attrs) this.stanza_set_attribute(jingle, cur_attr, attrs[cur_attr]); + } catch(e) { + this.parent.get_debug().log('[JSJaCJingle:utils] stanza_generate_jingle > ' + e, 1); + } + + return jingle; + }, + + /** + * Generates stanza Muji node + * @public + * @param {JSJaCPacket} stanza + * @returns {DOM} Muji node + */ + stanza_generate_muji: function(stanza) { + var muji = null; + + try { + muji = stanza.getNode().appendChild(stanza.buildNode('muji', { 'xmlns': NS_MUJI })); + } catch(e) { + this.parent.get_debug().log('[JSJaCJingle:utils] stanza_generate_muji > ' + e, 1); + } + + return muji; + }, + + /** + * Generates stanza session info + * @public + * @param {JSJaCPacket} stanza + * @param {DOM} jingle + * @param {Object} args + */ + stanza_generate_session_info: function(stanza, jingle, args) { + try { + var info = jingle.appendChild(stanza.buildNode(args.info, { 'xmlns': NS_JINGLE_APPS_RTP_INFO })); + + // Info attributes + switch(args.info) { + case JSJAC_JINGLE_SESSION_INFO_MUTE: + case JSJAC_JINGLE_SESSION_INFO_UNMUTE: + this.stanza_set_attribute(info, 'creator', this.parent.get_creator_this()); + this.stanza_set_attribute(info, 'name', args.name); + + break; + } + } catch(e) { + this.parent.get_debug().log('[JSJaCJingle:utils] stanza_generate_session_info > ' + e, 1); + } + }, + + /** + * Generates stanza local content + * @public + * @param {JSJaCPacket} stanza + * @param {DOM} jingle + * @param {Boolean} has_transport + * @param {Object} [override_content] + */ + stanza_generate_content_local: function(stanza, jingle, has_transport, override_content) { + try { + var cur_media; + var content_local = override_content ? override_content : this.parent.get_content_local(); + + var _this = this; + + var fn_build_transport = function(content, transport_obj, namespace) { + var transport = _this.stanza_build_node( + stanza, + content, + [transport_obj.attrs], + 'transport', + namespace + ); + + // Fingerprint + _this.stanza_build_node( + stanza, + transport, + [transport_obj.fingerprint], + 'fingerprint', + NS_JINGLE_APPS_DTLS, + 'value' + ); + + // Candidates + _this.stanza_build_node( + stanza, + transport, + transport_obj.candidate, + 'candidate', + namespace + ); + }; + + for(cur_media in content_local) { + var cur_content = content_local[cur_media]; + + var content = jingle.appendChild(stanza.buildNode('content', { 'xmlns': this.parent.get_namespace() })); + + this.stanza_set_attribute(content, 'creator', cur_content.creator); + this.stanza_set_attribute(content, 'name', cur_content.name); + this.stanza_set_attribute(content, 'senders', cur_content.senders); + + // Build description (if action type allows that element) + if(this.stanza_get_attribute(jingle, 'action') != JSJAC_JINGLE_ACTION_TRANSPORT_INFO) { + var cs_description = cur_content.description; + var cs_d_attrs = cs_description.attrs; + var cs_d_rtcp_fb = cs_description['rtcp-fb']; + var cs_d_bandwidth = cs_description.bandwidth; + var cs_d_payload = cs_description.payload; + var cs_d_encryption = cs_description.encryption; + var cs_d_ssrc = cs_description.ssrc; + var cs_d_ssrc_group = cs_description['ssrc-group']; + var cs_d_rtp_hdrext = cs_description['rtp-hdrext']; + var cs_d_rtcp_mux = cs_description['rtcp-mux']; + + var description = this.stanza_build_node( + stanza, content, + [cs_d_attrs], + 'description', + NS_JINGLE_APPS_RTP + ); + + // Payload-type + if(cs_d_payload) { + var i, j, + cur_ssrc_id, cur_cs_d_ssrc_group_semantics, + cs_d_p, payload_type; + + for(i in cs_d_payload) { + cs_d_p = cs_d_payload[i]; + + payload_type = this.stanza_build_node( + stanza, + description, + [cs_d_p.attrs], + 'payload-type', + NS_JINGLE_APPS_RTP + ); + + // Parameter + this.stanza_build_node( + stanza, + payload_type, + cs_d_p.parameter, + 'parameter', + NS_JINGLE_APPS_RTP + ); + + // RTCP-FB (sub) + this.stanza_build_node( + stanza, + payload_type, + cs_d_p['rtcp-fb'], + 'rtcp-fb', + NS_JINGLE_APPS_RTP_RTCP_FB + ); + + // RTCP-FB-TRR-INT + this.stanza_build_node( + stanza, + payload_type, + cs_d_p['rtcp-fb-trr-int'], + 'rtcp-fb-trr-int', + NS_JINGLE_APPS_RTP_RTCP_FB + ); + } + + // SSRC-GROUP + if(cs_d_ssrc_group) { + for(cur_cs_d_ssrc_group_semantics in cs_d_ssrc_group) { + for(j in cs_d_ssrc_group[cur_cs_d_ssrc_group_semantics]) { + var ssrc_group = description.appendChild(stanza.buildNode('ssrc-group', { + 'semantics': cur_cs_d_ssrc_group_semantics, + 'xmlns': NS_JINGLE_APPS_RTP_SSMA + })); + + this.stanza_build_node( + stanza, + ssrc_group, + cs_d_ssrc_group[cur_cs_d_ssrc_group_semantics][j].sources, + 'source', + NS_JINGLE_APPS_RTP_SSMA + ); + } + } + } + + // SSRC + if(cs_d_ssrc) { + for(cur_ssrc_id in cs_d_ssrc) { + var ssrc = description.appendChild(stanza.buildNode('source', { + 'ssrc': cur_ssrc_id, + 'xmlns': NS_JINGLE_APPS_RTP_SSMA + })); + + this.stanza_build_node( + stanza, + ssrc, + cs_d_ssrc[cur_ssrc_id], + 'parameter', + NS_JINGLE_APPS_RTP_SSMA + ); + } + } + + // Encryption? + if(has_transport === true) { + if(cs_d_encryption && + (cs_d_encryption.crypto && cs_d_encryption.crypto.length || + cs_d_encryption['zrtp-hash'] && cs_d_encryption['zrtp-hash'].length)) { + var encryption = description.appendChild(stanza.buildNode('encryption', { 'xmlns': NS_JINGLE_APPS_RTP })); + + this.stanza_set_attribute(encryption, 'required', (cs_d_encryption.attrs.required || '0')); + + // Crypto + this.stanza_build_node( + stanza, + encryption, + cs_d_encryption.crypto, + 'crypto', + NS_JINGLE_APPS_RTP + ); + + // ZRTP-HASH + this.stanza_build_node( + stanza, + encryption, + cs_d_encryption['zrtp-hash'], + 'zrtp-hash', + NS_JINGLE_APPS_RTP_ZRTP, + 'value' + ); + } + } + + // RTCP-FB (common) + this.stanza_build_node( + stanza, + description, + cs_d_rtcp_fb, + 'rtcp-fb', + NS_JINGLE_APPS_RTP_RTCP_FB + ); + + // Bandwidth + this.stanza_build_node( + stanza, + description, + cs_d_bandwidth, + 'bandwidth', + NS_JINGLE_APPS_RTP, + 'value' + ); + + // RTP-HDREXT + this.stanza_build_node( + stanza, + description, + cs_d_rtp_hdrext, + 'rtp-hdrext', + NS_JINGLE_APPS_RTP_RTP_HDREXT + ); + + // RTCP-MUX + if(cs_d_rtcp_mux) + description.appendChild(stanza.buildNode('rtcp-mux', { 'xmlns': NS_JINGLE_APPS_RTP })); + } + } + + // Build transport? + if(has_transport === true) { + var cs_transport = this.generate_transport(cur_content.transport); + + // Transport candidates: ICE-UDP + if((cs_transport.ice.candidate).length > 0) { + fn_build_transport( + content, + cs_transport.ice, + NS_JINGLE_TRANSPORTS_ICEUDP + ); + } + + // Transport candidates: RAW-UDP + if((cs_transport.raw.candidate).length > 0) { + fn_build_transport( + content, + cs_transport.raw, + NS_JINGLE_TRANSPORTS_RAWUDP + ); + } + } + } + } catch(e) { + this.parent.get_debug().log('[JSJaCJingle:utils] stanza_generate_content_local > ' + e, 1); + } + }, + + /** + * Generates stanza local group + * @public + * @param {JSJaCPacket} stanza + * @param {DOM} jingle + */ + stanza_generate_group_local: function(stanza, jingle) { + try { + var i, + cur_semantics, cur_group, cur_group_name, + group; + + var group_local = this.parent.get_group_local(); + + for(cur_semantics in group_local) { + cur_group = group_local[cur_semantics]; + + group = jingle.appendChild(stanza.buildNode('group', { + 'xmlns': NS_JINGLE_APPS_GROUPING, + 'semantics': cur_semantics + })); + + for(i in cur_group) { + cur_group_name = cur_group[i]; + + group.appendChild(stanza.buildNode('content', { + 'xmlns': NS_JINGLE_APPS_GROUPING, + 'name': cur_group_name + })); + } + } + } catch(e) { + this.parent.get_debug().log('[JSJaCJingle:utils] stanza_generate_group_local > ' + e, 1); + } + }, + + /** + * Generates content + * @public + * @param {String} creator + * @param {String} name + * @param {Object} senders + * @param {Object} payloads + * @param {Object} transports + * @returns {Object} Content object + */ + generate_content: function(creator, name, senders, payloads, transports) { + var content_obj = {}; + + try { + // Generation process + content_obj.creator = creator; + content_obj.name = name; + content_obj.senders = senders; + content_obj.description = {}; + content_obj.transport = {}; + + // Generate description + var i; + var description_cpy = this.object_clone(payloads.descriptions); + var description_ptime = description_cpy.attrs.ptime; + var description_maxptime = description_cpy.attrs.maxptime; + + if(description_ptime) delete description_cpy.attrs.ptime; + if(description_maxptime) delete description_cpy.attrs.maxptime; + + for(i in description_cpy.payload) { + if(!('attrs' in description_cpy.payload[i])) + description_cpy.payload[i].attrs = {}; + + description_cpy.payload[i].attrs.ptime = description_ptime; + description_cpy.payload[i].attrs.maxptime = description_maxptime; + } + + content_obj.description = description_cpy; + + // Generate transport + content_obj.transport.candidate = transports; + content_obj.transport.attrs = {}; + content_obj.transport.attrs.pwd = payloads.transports ? payloads.transports.pwd : null; + content_obj.transport.attrs.ufrag = payloads.transports ? payloads.transports.ufrag : null; + + if(payloads.transports && payloads.transports.fingerprint) + content_obj.transport.fingerprint = payloads.transports.fingerprint; + } catch(e) { + this.parent.get_debug().log('[JSJaCJingle:utils] generate_content > ' + e, 1); + } + + return content_obj; + }, + + /** + * Generates transport + * @public + * @param {Object} transport_init_obj + * @returns {Object} Transport object + */ + generate_transport: function(transport_init_obj) { + var transport_obj = { + 'ice': {}, + 'raw': {} }; - for(i in lines) { - cur_line = lines[i]; + try { + var i, j, k, + cur_attr, + cur_candidate, cur_transport; - // 'group' line? - m_group = (R_WEBRTC_SDP_ICE_PAYLOAD.group).exec(cur_line); + // Reduce RAW-UDP map object for simpler search + var rawudp_map = {}; + for(i in JSJAC_JINGLE_SDP_CANDIDATE_MAP_RAWUDP) { + rawudp_map[JSJAC_JINGLE_SDP_CANDIDATE_MAP_RAWUDP[i].n] = 1; + } - if(m_group) { - if(m_group[1] && m_group[2]) { - init_group(m_group[1]); + var fn_init_obj = function(transport_sub_obj) { + transport_sub_obj.attrs = transport_init_obj.attrs; + transport_sub_obj.fingerprint = transport_init_obj.fingerprint; + transport_sub_obj.candidate = []; + }; - group[m_group[1]] = (m_group[2].indexOf(' ') === -1 ? [m_group[2]] : m_group[2].split(' ')); + for(j in transport_obj) + fn_init_obj(transport_obj[j]); + + // Nest candidates in their category + for(k = 0; k < (transport_init_obj.candidate).length; k++) { + cur_candidate = this.object_clone(transport_init_obj.candidate[k]); + + if(cur_candidate.type in JSJAC_JINGLE_SDP_CANDIDATE_TYPES) { + // Remove attributes that are not required by RAW-UDP (XEP-0177 compliance) + if(JSJAC_JINGLE_SDP_CANDIDATE_TYPES[cur_candidate.type] === JSJAC_JINGLE_SDP_CANDIDATE_METHOD_RAW) { + for(cur_attr in cur_candidate) { + if(typeof rawudp_map[cur_attr] == 'undefined') + delete cur_candidate[cur_attr]; + } + } + + cur_transport = transport_obj[JSJAC_JINGLE_SDP_CANDIDATE_TYPES[cur_candidate.type]]; + cur_transport.candidate.push(cur_candidate); + } + } + } catch(e) { + this.parent.get_debug().log('[JSJaCJingle:utils] generate_transport > ' + e, 1); + } + + return transport_obj; + }, + + /** + * Builds local content + * @public + */ + build_content_local: function() { + try { + var cur_name; + + for(cur_name in this.parent.get_name()) { + this.parent._set_content_local( + cur_name, + + this.generate_content( + JSJAC_JINGLE_SENDERS_INITIATOR.jingle, + cur_name, + this.parent.get_senders(cur_name), + this.parent.get_payloads_local(cur_name), + this.parent.get_candidates_local(cur_name) + ) + ); + } + } catch(e) { + this.parent.get_debug().log('[JSJaCJingle:utils] build_content_local > ' + e, 1); + } + }, + + /** + * Builds remote content + * @public + */ + build_content_remote: function() { + try { + var cur_name; + + for(cur_name in this.parent.get_name()) { + this.parent._set_content_remote( + cur_name, + + this.generate_content( + this.parent.get_creator(cur_name), + cur_name, + this.parent.get_senders(cur_name), + this.parent.get_payloads_remote(cur_name), + this.parent.get_candidates_remote(cur_name) + ) + ); + } + } catch(e) { + this.parent.get_debug().log('[JSJaCJingle:utils] build_content_remote > ' + e, 1); + } + }, + + /** + * Generates media name + * @public + * @param {String} media + * @returns {String} Media name + */ + name_generate: function(media) { + var name = null; + + try { + var i, cur_name; + + var content_all = []; + + // Push remote contents + var cur_participant, participants, + content_remote = {}; + + if(typeof this.parent.get_content_remote == 'function') + content_remote = this.parent.get_content_remote(); + + for(cur_participant in content_remote) { + content_all.push( + content_remote[cur_participant] + ); + } + + // Push local content + content_all.push( + this.parent.get_content_local() + ); + + for(i in content_all) { + for(cur_name in content_all[i]) { + try { + if(content_all[i][cur_name].description.attrs.media === media) { + name = cur_name; break; + } + } catch(e) {} } - continue; - } - } - } catch(e) { - self.get_debug().log('[JSJaCJingle] _util_sdp_parse_group > ' + e, 1); - } - - return group; - }; - - /** - * @private - */ - self._util_sdp_resolution_payload = function(payload) { - try { - if(!payload || typeof payload !== 'object') return {}; - - // No video? - if(self.get_media_all().indexOf(JSJAC_JINGLE_MEDIA_VIDEO) === -1) return payload; - - var i, j, k, cur_media; - var cur_payload, res_arr, constraints; - var res_height = null; - var res_width = null; - - // Try local view? (more reliable) - for(i in self.get_local_view()) { - if(typeof self.get_local_view()[i].videoWidth == 'number' && - typeof self.get_local_view()[i].videoHeight == 'number' ) { - res_height = self.get_local_view()[i].videoHeight; - res_width = self.get_local_view()[i].videoWidth; - - if(res_height && res_width) break; - } - } - - // Try media constraints? (less reliable) - if(!res_height || !res_width) { - self.get_debug().log('[JSJaCJingle] _util_sdp_resolution_payload > Could not get local video resolution, falling back on constraints (local video may not be ready).', 0); - - constraints = self.util_generate_constraints(); - - // Still nothing?! - if(typeof constraints.video !== 'object' || - typeof constraints.video.mandatory !== 'object' || - typeof constraints.video.mandatory.minWidth !== 'number' || - typeof constraints.video.mandatory.minHeight !== 'number' ) { - self.get_debug().log('[JSJaCJingle] _util_sdp_resolution_payload > Could not get local video resolution (not sending it).', 1); - return payload; + if(name) break; } - res_height = constraints.video.mandatory.minHeight; - res_width = constraints.video.mandatory.minWidth; + if(!name) name = media; + } catch(e) { + this.parent.get_debug().log('[JSJaCJingle:utils] name_generate > ' + e, 1); } - // Constraints to be used - res_arr = [ - { - name : 'height', - value : res_height + return name; + }, + + /** + * Generates media + * @public + * @param {String} name + * @returns {String} Media + */ + media_generate: function(name) { + var cur_media; + var media = null; + + try { + if(typeof name == 'number') { + for(cur_media in JSJAC_JINGLE_MEDIAS) { + if(name == parseInt(JSJAC_JINGLE_MEDIAS[cur_media].label, 10)) { + media = cur_media; break; + } + } + } else { + for(cur_media in JSJAC_JINGLE_MEDIAS) { + if(name == this.name_generate(cur_media)) { + media = cur_media; break; + } + } + } + + if(!media) media = name; + } catch(e) { + this.parent.get_debug().log('[JSJaCJingle:utils] media_generate > ' + e, 1); + } + + return media; + }, + + /** + * Generates a MD5 hash from the given value + * @public + * @param {String} value + * @returns {String} MD5 hash value + */ + generate_hash_md5: function(value) { + return hex_md5(value); + }, + + /** + * Generates a random value + * @public + * @param {Number} i + * @returns {String} Random value + */ + generate_random: function(i) { + return JSJaCUtils.cnonce(i); + }, + + /** + * Generates a random SID value + * @public + * @returns {String} SID value + */ + generate_sid: function() { + return this.generate_random(16); + }, + + /** + * Generates a random IID value + * @public + * @returns {String} IID value + */ + generate_iid: function() { + return this.generate_random(24); + }, + + /** + * Generates a random password value + * @public + * @returns {String} Password value + */ + generate_password: function() { + return this.generate_random(64); + }, + + /** + * Generates a random ID value + * @public + * @returns {String} ID value + */ + generate_id: function() { + return this.generate_random(10); + }, + + /** + * Generates the constraints object + * @public + * @returns {Object} constraints object + */ + generate_constraints: function() { + var constraints = { + audio : false, + video : false + }; + + try { + // Medias? + constraints.audio = true; + constraints.video = (this.parent.get_media() == JSJAC_JINGLE_MEDIA_VIDEO); + + // Video configuration + if(constraints.video === true) { + // Resolution? + switch(this.parent.get_resolution()) { + // 16:9 + case '720': + case 'hd': + constraints.video = { + mandatory : { + minWidth : 1280, + minHeight : 720, + minAspectRatio : 1.77 + } + }; + break; + + case '360': + case 'md': + constraints.video = { + mandatory : { + minWidth : 640, + minHeight : 360, + minAspectRatio : 1.77 + } + }; + break; + + case '180': + case 'sd': + constraints.video = { + mandatory : { + minWidth : 320, + minHeight : 180, + minAspectRatio : 1.77 + } + }; + break; + + // 4:3 + case '960': + constraints.video = { + mandatory : { + minWidth : 960, + minHeight : 720 + } + }; + break; + + case '640': + case 'vga': + constraints.video = { + mandatory : { + maxWidth : 640, + maxHeight : 480 + } + }; + break; + + case '320': + constraints.video = { + mandatory : { + maxWidth : 320, + maxHeight : 240 + } + }; + break; + } + + // Bandwidth? + if(this.parent.get_bandwidth()) + constraints.video.optional = [{ bandwidth: this.parent.get_bandwidth() }]; + + // FPS? + if(this.parent.get_fps()) + constraints.video.mandatory.minFrameRate = this.parent.get_fps(); + + // Custom video source? (screenshare) + if(this.parent.get_media() == JSJAC_JINGLE_MEDIA_VIDEO && + this.parent.get_video_source() != JSJAC_JINGLE_VIDEO_SOURCE_CAMERA ) { + if(document.location.protocol !== 'https:') + this.parent.get_debug().log('[JSJaCJingle:utils] generate_constraints > HTTPS might be required to share screen, otherwise you may get a permission denied error.', 0); + + // Unsupported browser? (for that feature) + if(this.browser().name != JSJAC_JINGLE_BROWSER_CHROME) { + this.parent.get_debug().log('[JSJaCJingle:utils] generate_constraints > Video source not supported by ' + this.browser().name + ' (source: ' + this.parent.get_video_source() + ').', 1); + + this.parent.terminate(JSJAC_JINGLE_REASON_MEDIA_ERROR); + return; + } + + constraints.audio = false; + constraints.video.mandatory = { + 'chromeMediaSource': this.parent.get_video_source() + }; + } + } + } catch(e) { + this.parent.get_debug().log('[JSJaCJingle:utils] generate_constraints > ' + e, 1); + } + + return constraints; + }, + + /** + * Returns whether SDP credentials are common or not (fingerprint & so) + * @public + * @param {Array} payloads + * @returns {Boolean} Credientials same state + */ + is_sdp_common_credentials: function(payloads) { + var is_same = true; + + try { + var i, + prev_credentials, cur_credentials; + + for(i in payloads) { + cur_credentials = payloads[i].transports; + + if(typeof prev_credentials == 'object') { + if((prev_credentials.ufrag !== cur_credentials.ufrag) || + (prev_credentials.pwd !== cur_credentials.pwd) || + this.object_equal(prev_credentials.fingerprint, cur_credentials.fingerprint) + ) { + is_same = false; + break; + } + } + + prev_credentials = cur_credentials; + } + } catch(e) { + this.parent.get_debug().log('[JSJaCJingle:utils] is_sdp_common_credentials > ' + e, 1); + } + + return is_same; + }, + + /** + * Returns number of candidates in candidates object + * @public + * @param {Object} candidates_obj + * @returns {Number} Number of candidates + */ + count_candidates: function(candidates_obj) { + var count_candidates = 0; + + try { + var i; + + for(i in candidates_obj) { + count_candidates += (typeof candidates_obj[i] == 'object') ? candidates_obj[i].length : 0; + } + } catch(e) { + this.parent.get_debug().log('[JSJaCJingle:utils] count_candidates > ' + e, 1); + } finally { + return count_candidates; + } + + }, + + /** + * Extracts network main details + * @public + * @param {String} media + * @param {Array} candidates + * @returns {Object} Network details + */ + network_extract_main: function(media, candidates) { + var network_obj = { + 'ip': JSJAC_JINGLE_SDP_CANDIDATE_IP_DEFAULT, + 'port': JSJAC_JINGLE_SDP_CANDIDATE_PORT_DEFAULT, + 'scope': JSJAC_JINGLE_SDP_CANDIDATE_SCOPE_DEFAULT, + 'protocol': JSJAC_JINGLE_SDP_CANDIDATE_IPVERSION_DEFAULT + }; + + var local_obj, remote_obj; + + try { + var i, + cur_candidate, cur_candidate_parse; + + var fn_proceed_parse = function(type, candidate_eval) { + var r_lan, protocol; + + var parse_obj = { + 'ip': candidate_eval.ip, + 'port': candidate_eval.port + }; + + if(candidate_eval.ip.match(R_NETWORK_IP.all.v4)) { + r_lan = R_NETWORK_IP.lan.v4; + parse_obj.protocol = JSJAC_JINGLE_SDP_CANDIDATE_IPVERSION_V4; + } else if(candidate_eval.ip.match(R_NETWORK_IP.all.v6)) { + r_lan = R_NETWORK_IP.lan.v6; + parse_obj.protocol = JSJAC_JINGLE_SDP_CANDIDATE_IPVERSION_V6; + } else { + return; + } + + if((type === JSJAC_JINGLE_SDP_CANDIDATE_TYPE_HOST) && + candidate_eval.ip.match(r_lan)) { + // Local + parse_obj.scope = JSJAC_JINGLE_SDP_CANDIDATE_SCOPE_LOCAL; + } else if(type === JSJAC_JINGLE_SDP_CANDIDATE_TYPE_SRFLX) { + // Remote + parse_obj.scope = JSJAC_JINGLE_SDP_CANDIDATE_SCOPE_REMOTE; + } else { + return; + } + + return parse_obj; + }; + + for(i in candidates) { + cur_candidate = candidates[i]; + + if(cur_candidate.id == media || cur_candidate.label == media) { + cur_candidate_parse = this.parent.sdp._parse_candidate(cur_candidate.candidate); + + if(cur_candidate_parse.type === JSJAC_JINGLE_SDP_CANDIDATE_TYPE_HOST) { + // Only proceed if no local network yet + if(typeof local_obj == 'undefined') { + local_obj = fn_proceed_parse(JSJAC_JINGLE_SDP_CANDIDATE_TYPE_HOST, cur_candidate_parse); + } + } else if(cur_candidate_parse.type === JSJAC_JINGLE_SDP_CANDIDATE_TYPE_SRFLX) { + // Only proceed if no remote network yet + if(typeof remote_obj == 'undefined') { + remote_obj = fn_proceed_parse(JSJAC_JINGLE_SDP_CANDIDATE_TYPE_SRFLX, cur_candidate_parse); + } + } + } + } + + if(typeof remote_obj != 'undefined') { + network_obj = remote_obj; + } else if(typeof local_obj != 'undefined') { + network_obj = local_obj; + } + } catch(e) { + this.parent.get_debug().log('[JSJaCJingle:utils] network_extract_main > ' + e, 1); + } + + return network_obj; + }, + + /** + * Extracts username from full JID + * @public + * @param {String} full_jid + * @returns {String|Object} Username + */ + extract_username: function(full_jid) { + try { + return (new JSJaCJID(full_jid)).getResource(); + } catch(e) { + this.parent.get_debug().log('[JSJaCJingle:utils] extract_username > ' + e, 1); + } + + return null; + }, + + /** + * Returns our negotiation status + * @public + * @returns {String} Negotiation status + */ + negotiation_status: function() { + return (this.parent.get_initiator() == this.connection_jid()) ? JSJAC_JINGLE_SENDERS_INITIATOR.jingle : JSJAC_JINGLE_SENDERS_RESPONDER.jingle; + }, + + /** + * Get my connection JID + * @public + * @returns {String} JID value + */ + connection_jid: function() { + return this.parent.get_connection().username + '@' + + this.parent.get_connection().domain + '/' + + this.parent.get_connection().resource; + }, + + /** + * Get my connection username + * @public + * @returns {String} Username value + */ + connection_username: function() { + return this.parent.get_connection().username; + }, + + /** + * Get my connection domain + * @public + * @returns {String} Domain value + */ + connection_domain: function() { + return this.parent.get_connection().domain; + }, + + /** + * Get my connection resource + * @public + * @returns {String} Resource value + */ + connection_resource: function() { + return this.parent.get_connection().resource; + }, + + /** + * Registers a view to map + * @public + * @param {String} type + * @returns {Object} View register functions map + */ + map_register_view: function(type) { + var fn = { + type : null, + mute : false, + + view : { + get : null, + set : null }, - { - name : 'width', - value : res_width + stream : { + get : null, + set : null } - ]; + }; - for(cur_media in payload) { - if(cur_media != JSJAC_JINGLE_MEDIA_VIDEO) continue; + try { + switch(type) { + case JSJAC_JINGLE_DIRECTION_LOCAL: + fn.type = type; + fn.mute = true; + fn.view.get = this.parent.get_local_view; + fn.view.set = this.parent._set_local_view; + fn.stream.get = this.parent.get_local_stream; + fn.stream.set = this.parent._set_local_stream; + break; - cur_payload = payload[cur_media].descriptions.payload; - - for(j in cur_payload) { - if(typeof cur_payload[j].parameter !== 'object') cur_payload[j].parameter = []; - - for(k in res_arr) - (cur_payload[j].parameter).push(res_arr[k]); + case JSJAC_JINGLE_DIRECTION_REMOTE: + fn.type = type; + fn.view.get = this.parent.get_remote_view; + fn.view.set = this.parent._set_remote_view; + fn.stream.get = this.parent.get_remote_stream; + fn.stream.set = this.parent._set_remote_stream; + break; } + } catch(e) { + this.parent.get_debug().log('[JSJaCJingle:utils] map_register_view > ' + e, 1); } - self.get_debug().log('[JSJaCJingle] _util_sdp_resolution_payload > Got local video resolution (' + res_width + 'x' + res_height + ').', 2); - } catch(e) { - self.get_debug().log('[JSJaCJingle] _util_sdp_resolution_payload > ' + e, 1); - } + return fn; + }, - return payload; - }; + /** + * Unregister a view from map + * @public + * @param {String} type + * @returns {Object} View unregister functions map + */ + map_unregister_view: function(type) { + return this.map_register_view(type); + }, + } +); +/** + * @fileoverview JSJaC Jingle library - SDP tools + * + * @url https://github.com/valeriansaliou/jsjac-jingle + * @depends https://github.com/sstrigler/JSJaC + * @author Valérian Saliou https://valeriansaliou.name/ + * @license Mozilla Public License v2.0 (MPL v2.0) + */ - /** - * @private - */ - self._util_sdp_parse_candidate = function(sdp_candidate) { - var candidate = {}; - try { - if(!sdp_candidate) return candidate; +/** @module jsjac-jingle/sdp */ +/** @exports JSJaCJingleSDP */ - var error = 0; - var matches = R_WEBRTC_SDP_ICE_CANDIDATE.exec(sdp_candidate); - // Matches! - if(matches) { - candidate.component = matches[2] || error++; - candidate.foundation = matches[1] || error++; - candidate.generation = matches[16] || JSJAC_JINGLE_GENERATION; - candidate.id = self.util_generate_id(); - candidate.ip = matches[5] || error++; - candidate.network = JSJAC_JINGLE_NETWORK; - candidate.port = matches[6] || error++; - candidate.priority = matches[4] || error++; - candidate.protocol = matches[3] || error++; - candidate['rel-addr'] = matches[11]; - candidate['rel-port'] = matches[13]; - candidate.type = matches[8] || error++; +/** + * SDP helpers class. + * @class + * @classdesc SDP helpers class. + * @requires nicolas-van/ring.js + * @requires sstrigler/JSJaC + * @see {@link http://ringjs.neoname.eu/|Ring.js} + * @param {JSJaCJingleSingle|JSJaCJingleMuji} parent Parent class. + */ +var JSJaCJingleSDP = ring.create( + /** @lends JSJaCJingleSDP.prototype */ + { + /** + * Constructor + */ + constructor: function(parent) { + /** + * @constant + * @member {JSJaCJingleSingle|JSJaCJingleMuji} + * @readonly + * @default + * @public + */ + this.parent = parent; + }, + + + /** + * Parses SDP payload + * @private + * @param {String} sdp_payload + * @returns {Object} Parsed payload object + */ + _parse_payload: function(sdp_payload) { + var payload = {}; + + try { + if(!sdp_payload || sdp_payload.indexOf('\n') == -1) return payload; + + // Common vars + var lines = sdp_payload.split('\n'); + var cur_name = null; + var cur_media = null; + + var common_transports = { + 'fingerprint' : {}, + 'pwd' : null, + 'ufrag' : null + }; + + var error, i, j, k, + cur_line, + cur_fmtp, cur_fmtp_id, cur_fmtp_values, cur_fmtp_attrs, cur_fmtp_key, cur_fmtp_value, + cur_rtpmap, cur_rtcp_fb, cur_rtcp_fb_trr_int, + cur_crypto, cur_zrtp_hash, cur_fingerprint, cur_ssrc, + cur_ssrc_group, cur_ssrc_group_semantics, cur_ssrc_group_ids, cur_ssrc_group_id, + cur_extmap, cur_rtpmap_id, cur_rtcp_fb_id, cur_bandwidth, + m_rtpmap, m_fmtp, m_rtcp_fb, m_rtcp_fb_trr_int, m_crypto, m_zrtp_hash, + m_fingerprint, m_pwd, m_ufrag, m_ptime, m_maxptime, m_bandwidth, m_media, m_candidate, + cur_check_name, cur_transport_sub; + + // Common functions + var init_content = function(name) { + if(!(name in payload)) payload[name] = {}; + }; + + var init_descriptions = function(name, sub, sub_default) { + init_content(name); + + if(!('descriptions' in payload[name])) payload[name].descriptions = {}; + if(!(sub in payload[name].descriptions)) payload[name].descriptions[sub] = sub_default; + }; + + var init_transports = function(name, sub, sub_default) { + init_content(name); + + if(!('transports' in payload[name])) payload[name].transports = {}; + if(!(sub in payload[name].transports)) payload[name].transports[sub] = sub_default; + }; + + var init_ssrc = function(name, id) { + init_descriptions(name, 'ssrc', {}); + + if(!(id in payload[name].descriptions.ssrc)) + payload[name].descriptions.ssrc[id] = []; + }; + + var init_ssrc_group = function(name, semantics) { + init_descriptions(name, 'ssrc-group', {}); + + if(!(semantics in payload[name].descriptions['ssrc-group'])) + payload[name].descriptions['ssrc-group'][semantics] = []; + }; + + var init_payload = function(name, id) { + init_descriptions(name, 'payload', {}); + + if(!(id in payload[name].descriptions.payload)) { + payload[name].descriptions.payload[id] = { + 'attrs' : {}, + 'parameter' : [], + 'rtcp-fb' : [], + 'rtcp-fb-trr-int' : [] + }; + } + }; + + var init_encryption = function(name) { + init_descriptions(name, 'encryption', { + 'attrs' : { + 'required' : '1' + }, + + 'crypto' : [], + 'zrtp-hash' : [] + }); + }; + + for(i in lines) { + cur_line = lines[i]; + + m_media = (R_WEBRTC_SDP_ICE_PAYLOAD.media).exec(cur_line); + + // 'audio/video' line? + if(m_media) { + cur_media = m_media[1]; + cur_name = this.parent.utils.name_generate(cur_media); + + // Push it to parent array + init_descriptions(cur_name, 'attrs', {}); + payload[cur_name].descriptions.attrs.media = cur_media; + + continue; + } + + m_bandwidth = (R_WEBRTC_SDP_ICE_PAYLOAD.bandwidth).exec(cur_line); + + // 'bandwidth' line? + if(m_bandwidth) { + // Populate current object + error = 0; + cur_bandwidth = {}; + + cur_bandwidth.type = m_bandwidth[1] || error++; + cur_bandwidth.value = m_bandwidth[2] || error++; + + // Incomplete? + if(error !== 0) continue; + + // Push it to parent array + init_descriptions(cur_name, 'bandwidth', []); + payload[cur_name].descriptions.bandwidth.push(cur_bandwidth); + + continue; + } + + m_rtpmap = (R_WEBRTC_SDP_ICE_PAYLOAD.rtpmap).exec(cur_line); + + // 'rtpmap' line? + if(m_rtpmap) { + // Populate current object + error = 0; + cur_rtpmap = {}; + + cur_rtpmap.channels = m_rtpmap[6]; + cur_rtpmap.clockrate = m_rtpmap[4]; + cur_rtpmap.id = m_rtpmap[1] || error++; + cur_rtpmap.name = m_rtpmap[3]; + + // Incomplete? + if(error !== 0) continue; + + cur_rtpmap_id = cur_rtpmap.id; + + // Push it to parent array + init_payload(cur_name, cur_rtpmap_id); + payload[cur_name].descriptions.payload[cur_rtpmap_id].attrs = cur_rtpmap; + + continue; + } + + m_fmtp = (R_WEBRTC_SDP_ICE_PAYLOAD.fmtp).exec(cur_line); + + // 'fmtp' line? + if(m_fmtp) { + cur_fmtp_id = m_fmtp[1]; + + if(cur_fmtp_id) { + cur_fmtp_values = m_fmtp[2] ? (m_fmtp[2]).split(';') : []; + + for(j in cur_fmtp_values) { + // Parse current attribute + if(cur_fmtp_values[j].indexOf('=') !== -1) { + cur_fmtp_attrs = cur_fmtp_values[j].split('='); + cur_fmtp_key = cur_fmtp_attrs[0]; + cur_fmtp_value = cur_fmtp_attrs[1]; + + while(cur_fmtp_key.length && !cur_fmtp_key[0]) + cur_fmtp_key = cur_fmtp_key.substring(1); + } else { + cur_fmtp_key = cur_fmtp_values[j]; + cur_fmtp_value = null; + } + + // Populate current object + error = 0; + cur_fmtp = {}; + + cur_fmtp.name = cur_fmtp_key || error++; + cur_fmtp.value = cur_fmtp_value; + + // Incomplete? + if(error !== 0) continue; + + // Push it to parent array + init_payload(cur_name, cur_fmtp_id); + payload[cur_name].descriptions.payload[cur_fmtp_id].parameter.push(cur_fmtp); + } + } + + continue; + } + + m_rtcp_fb = (R_WEBRTC_SDP_ICE_PAYLOAD.rtcp_fb).exec(cur_line); + + // 'rtcp-fb' line? + if(m_rtcp_fb) { + // Populate current object + error = 0; + cur_rtcp_fb = {}; + + cur_rtcp_fb.id = m_rtcp_fb[1] || error++; + cur_rtcp_fb.type = m_rtcp_fb[2]; + cur_rtcp_fb.subtype = m_rtcp_fb[4]; + + // Incomplete? + if(error !== 0) continue; + + cur_rtcp_fb_id = cur_rtcp_fb.id; + + // Push it to parent array + if(cur_rtcp_fb_id == '*') { + init_descriptions(cur_name, 'rtcp-fb', []); + (payload[cur_name].descriptions['rtcp-fb']).push(cur_rtcp_fb); + } else { + init_payload(cur_name, cur_rtcp_fb_id); + (payload[cur_name].descriptions.payload[cur_rtcp_fb_id]['rtcp-fb']).push(cur_rtcp_fb); + } + + continue; + } + + m_rtcp_fb_trr_int = (R_WEBRTC_SDP_ICE_PAYLOAD.rtcp_fb_trr_int).exec(cur_line); + + // 'rtcp-fb-trr-int' line? + if(m_rtcp_fb_trr_int) { + // Populate current object + error = 0; + cur_rtcp_fb_trr_int = {}; + + cur_rtcp_fb_trr_int.id = m_rtcp_fb_trr_int[1] || error++; + cur_rtcp_fb_trr_int.value = m_rtcp_fb_trr_int[2] || error++; + + // Incomplete? + if(error !== 0) continue; + + cur_rtcp_fb_trr_int_id = cur_rtcp_fb_trr_int.id; + + // Push it to parent array + init_payload(cur_name, cur_rtcp_fb_trr_int_id); + (payload[cur_name].descriptions.payload[cur_rtcp_fb_trr_int_id]['rtcp-fb-trr-int']).push(cur_rtcp_fb_trr_int); + + continue; + } + + m_crypto = (R_WEBRTC_SDP_ICE_PAYLOAD.crypto).exec(cur_line); + + // 'crypto' line? + if(m_crypto) { + // Populate current object + error = 0; + cur_crypto = {}; + + cur_crypto['crypto-suite'] = m_crypto[2] || error++; + cur_crypto['key-params'] = m_crypto[3] || error++; + cur_crypto['session-params'] = m_crypto[5]; + cur_crypto.tag = m_crypto[1] || error++; + + // Incomplete? + if(error !== 0) continue; + + // Push it to parent array + init_encryption(cur_name); + (payload[cur_name].descriptions.encryption.crypto).push(cur_crypto); + + continue; + } + + m_zrtp_hash = (R_WEBRTC_SDP_ICE_PAYLOAD.zrtp_hash).exec(cur_line); + + // 'zrtp-hash' line? + if(m_zrtp_hash) { + // Populate current object + error = 0; + cur_zrtp_hash = {}; + + cur_zrtp_hash.version = m_zrtp_hash[1] || error++; + cur_zrtp_hash.value = m_zrtp_hash[2] || error++; + + // Incomplete? + if(error !== 0) continue; + + // Push it to parent array + init_encryption(cur_name); + (payload[cur_name].descriptions.encryption['zrtp-hash']).push(cur_zrtp_hash); + + continue; + } + + m_ptime = (R_WEBRTC_SDP_ICE_PAYLOAD.ptime).exec(cur_line); + + // 'ptime' line? + if(m_ptime) { + // Push it to parent array + init_descriptions(cur_name, 'attrs', {}); + payload[cur_name].descriptions.attrs.ptime = m_ptime[1]; + + continue; + } + + m_maxptime = (R_WEBRTC_SDP_ICE_PAYLOAD.maxptime).exec(cur_line); + + // 'maxptime' line? + if(m_maxptime) { + // Push it to parent array + init_descriptions(cur_name, 'attrs', {}); + payload[cur_name].descriptions.attrs.maxptime = m_maxptime[1]; + + continue; + } + + m_ssrc = (R_WEBRTC_SDP_ICE_PAYLOAD.ssrc).exec(cur_line); + + // 'ssrc' line? + if(m_ssrc) { + // Populate current object + error = 0; + cur_ssrc = {}; + + cur_ssrc_id = m_ssrc[1] || error++; + cur_ssrc.name = m_ssrc[2] || error++; + cur_ssrc.value = m_ssrc[4]; + + // Incomplete? + if(error !== 0) continue; + + // Push it to storage array + init_ssrc(cur_name, cur_ssrc_id); + (payload[cur_name].descriptions.ssrc[cur_ssrc_id]).push(cur_ssrc); + + // Push it to parent array (common attr required for Jingle) + init_descriptions(cur_name, 'attrs', {}); + payload[cur_name].descriptions.attrs.ssrc = cur_ssrc_id; + + continue; + } + + m_ssrc_group = (R_WEBRTC_SDP_ICE_PAYLOAD.ssrc_group).exec(cur_line); + + // 'ssrc-group' line? + if(m_ssrc_group) { + // Populate current object + error = 0; + cur_ssrc_group = {}; + + cur_ssrc_group_semantics = m_ssrc_group[1] || error++; + cur_ssrc_group_ids = m_ssrc_group[2] || error++; + + // Explode sources into a list + cur_ssrc_group.sources = []; + cur_ssrc_group_ids = cur_ssrc_group_ids.trim(); + + if(cur_ssrc_group_ids) { + cur_ssrc_group_ids = cur_ssrc_group_ids.split(' '); + + for(k in cur_ssrc_group_ids) { + cur_ssrc_group_id = cur_ssrc_group_ids[k].trim(); + + if(cur_ssrc_group_id) { + cur_ssrc_group.sources.push({ + 'ssrc': cur_ssrc_group_id + }); + } + } + } + + if(cur_ssrc_group.sources.length === 0) error++; + + // Incomplete? + if(error !== 0) continue; + + // Push it to storage array + init_ssrc_group(cur_name, cur_ssrc_group_semantics); + (payload[cur_name].descriptions['ssrc-group'][cur_ssrc_group_semantics]).push(cur_ssrc_group); + + continue; + } + + m_rtcp_mux = (R_WEBRTC_SDP_ICE_PAYLOAD.rtcp_mux).exec(cur_line); + + // 'rtcp-mux' line? + if(m_rtcp_mux) { + // Push it to parent array + init_descriptions(cur_name, 'rtcp-mux', 1); + + continue; + } + + m_extmap = (R_WEBRTC_SDP_ICE_PAYLOAD.extmap).exec(cur_line); + + // 'extmap' line? + if(m_extmap) { + // Populate current object + error = 0; + cur_extmap = {}; + + cur_extmap.id = m_extmap[1] || error++; + cur_extmap.uri = m_extmap[4] || error++; + cur_extmap.senders = m_extmap[3]; + + // Incomplete? + if(error !== 0) continue; + + // Push it to parent array + init_descriptions(cur_name, 'rtp-hdrext', []); + (payload[cur_name].descriptions['rtp-hdrext']).push(cur_extmap); + + continue; + } + + m_fingerprint = (R_WEBRTC_SDP_ICE_PAYLOAD.fingerprint).exec(cur_line); + + // 'fingerprint' line? + if(m_fingerprint) { + // Populate current object + error = 0; + cur_fingerprint = common_transports.fingerprint || {}; + + cur_fingerprint.hash = m_fingerprint[1] || error++; + cur_fingerprint.value = m_fingerprint[2] || error++; + + // Incomplete? + if(error !== 0) continue; + + // Push it to parent array + init_transports(cur_name, 'fingerprint', cur_fingerprint); + common_transports.fingerprint = cur_fingerprint; + + continue; + } + + m_setup = (R_WEBRTC_SDP_ICE_PAYLOAD.setup).exec(cur_line); + + // 'setup' line? + if(m_setup) { + // Populate current object + cur_fingerprint = common_transports.fingerprint || {}; + cur_fingerprint.setup = m_setup[1]; + + // Push it to parent array + if(cur_fingerprint.setup) { + // Map it to fingerprint as XML-wise it is related + init_transports(cur_name, 'fingerprint', cur_fingerprint); + common_transports.fingerprint = cur_fingerprint; + } + + continue; + } + + m_pwd = (R_WEBRTC_SDP_ICE_PAYLOAD.pwd).exec(cur_line); + + // 'pwd' line? + if(m_pwd) { + init_transports(cur_name, 'pwd', m_pwd[1]); + + if(!common_transports.pwd) + common_transports.pwd = m_pwd[1]; + + continue; + } + + m_ufrag = (R_WEBRTC_SDP_ICE_PAYLOAD.ufrag).exec(cur_line); + + // 'ufrag' line? + if(m_ufrag) { + init_transports(cur_name, 'ufrag', m_ufrag[1]); + + if(!common_transports.ufrag) + common_transports.ufrag = m_ufrag[1]; + + continue; + } + + // 'candidate' line? (shouldn't be there) + m_candidate = R_WEBRTC_SDP_CANDIDATE.exec(cur_line); + + if(m_candidate) { + this._parse_candidate_store({ + media : cur_media, + candidate : cur_line + }); + + continue; + } + } + + // Filter medias + for(cur_check_name in payload) { + // Undesired media? + if(!this.parent.get_name()[cur_check_name]) { + delete payload[cur_check_name]; continue; + } + + // Validate transports + if(typeof payload[cur_check_name].transports !== 'object') + payload[cur_check_name].transports = {}; + + for(cur_transport_sub in common_transports) { + if(!payload[cur_check_name].transports[cur_transport_sub]) + payload[cur_check_name].transports[cur_transport_sub] = common_transports[cur_transport_sub]; + } + } + } catch(e) { + this.parent.get_debug().log('[JSJaCJingle:sdp] _parse_payload > ' + e, 1); } - // Incomplete? - if(error !== 0) return {}; - } catch(e) { - self.get_debug().log('[JSJaCJingle] _util_sdp_parse_candidate > ' + e, 1); - } + return payload; + }, - return candidate; - }; + /** + * Parses SDP group + * @private + * @param {String} sdp_payload + * @returns {Object} Parsed group object + */ + _parse_group: function(sdp_payload) { + var group = {}; - /** - * @private - */ - self._util_sdp_parse_candidate_store = function(sdp_candidate) { - // Store received candidate - var candidate_media = sdp_candidate.media; - var candidate_data = sdp_candidate.candidate; + try { + if(!sdp_payload || sdp_payload.indexOf('\n') == -1) return group; - // Convert SDP raw data to an object - var candidate_obj = self._util_sdp_parse_candidate(candidate_data); + // Common vars + var lines = sdp_payload.split('\n'); + var i, cur_line, + m_group; - self._set_candidates_local( - self._util_name_generate( - candidate_media - ), + var init_group = function(semantics) { + if(!(semantics in group)) group[semantics] = []; + }; - candidate_obj - ); + for(i in lines) { + cur_line = lines[i]; - // Enqueue candidate - self._set_candidates_queue_local( - self._util_name_generate( - candidate_media - ), + // 'group' line? + m_group = (R_WEBRTC_SDP_ICE_PAYLOAD.group).exec(cur_line); - candidate_obj - ); - }; + if(m_group) { + if(m_group[1] && m_group[2]) { + init_group(m_group[1]); + group[m_group[1]] = (m_group[2].indexOf(' ') === -1 ? [m_group[2]] : m_group[2].split(' ')); + } - - /** - * JSJSAC JINGLE PEER API - */ - - /** - * @private - */ - self._peer_connection_create = function(sdp_message_callback) { - self.get_debug().log('[JSJaCJingle] _peer_connection_create', 4); - - try { - // Log STUN servers in use - var i; - var ice_config = self._util_config_ice(); - - if(typeof ice_config.iceServers == 'object') { - for(i = 0; i < (ice_config.iceServers).length; i++) - self.get_debug().log('[JSJaCJingle] _peer_connection_create > Using ICE server at: ' + ice_config.iceServers[i].url + ' (' + (i + 1) + ').', 2); - } else { - self.get_debug().log('[JSJaCJingle] _peer_connection_create > No ICE server configured. Network may not work properly.', 0); + continue; + } + } + } catch(e) { + this.parent.get_debug().log('[JSJaCJingle:sdp] _parse_group > ' + e, 1); } - // Create the RTCPeerConnection object - self._set_peer_connection( - new WEBRTC_PEER_CONNECTION( - ice_config, - WEBRTC_CONFIGURATION.peer_connection.constraints - ) + return group; + }, + + /** + * Update video resolution in payload + * @private + * @param {Object} payload + * @returns {Object} Updated payload + */ + _resolution_payload: function(payload) { + try { + if(!payload || typeof payload !== 'object') return {}; + + // No video? + if(this.parent.get_media_all().indexOf(JSJAC_JINGLE_MEDIA_VIDEO) === -1) return payload; + + var i, j, k, cur_media; + var cur_payload, res_arr, constraints; + var res_height = null; + var res_width = null; + + // Try local view? (more reliable) + for(i in this.parent.get_local_view()) { + if(typeof this.parent.get_local_view()[i].videoWidth == 'number' && + typeof this.parent.get_local_view()[i].videoHeight == 'number' ) { + res_height = this.parent.get_local_view()[i].videoHeight; + res_width = this.parent.get_local_view()[i].videoWidth; + + if(res_height && res_width) break; + } + } + + // Try media constraints? (less reliable) + if(!res_height || !res_width) { + this.parent.get_debug().log('[JSJaCJingle:sdp] _resolution_payload > Could not get local video resolution, falling back on constraints (local video may not be ready).', 0); + + constraints = this.parent.utils.generate_constraints(); + + // Still nothing?! + if(typeof constraints.video !== 'object' || + typeof constraints.video.mandatory !== 'object' || + typeof constraints.video.mandatory.minWidth !== 'number' || + typeof constraints.video.mandatory.minHeight !== 'number' ) { + this.parent.get_debug().log('[JSJaCJingle:sdp] _resolution_payload > Could not get local video resolution (not sending it).', 1); + return payload; + } + + res_height = constraints.video.mandatory.minHeight; + res_width = constraints.video.mandatory.minWidth; + } + + // Constraints to be used + res_arr = [ + { + name : 'height', + value : res_height + }, + + { + name : 'width', + value : res_width + } + ]; + + for(cur_media in payload) { + if(cur_media != JSJAC_JINGLE_MEDIA_VIDEO) continue; + + cur_payload = payload[cur_media].descriptions.payload; + + for(j in cur_payload) { + if(typeof cur_payload[j].parameter !== 'object') cur_payload[j].parameter = []; + + for(k in res_arr) + (cur_payload[j].parameter).push(res_arr[k]); + } + } + + this.parent.get_debug().log('[JSJaCJingle:sdp] _resolution_payload > Got local video resolution (' + res_width + 'x' + res_height + ').', 2); + } catch(e) { + this.parent.get_debug().log('[JSJaCJingle:sdp] _resolution_payload > ' + e, 1); + } + + return payload; + }, + + /** + * Parses SDP candidate + * @private + * @param {String} sdp_candidate + * @returns {Object} Parsed candidates object + */ + _parse_candidate: function(sdp_candidate) { + var candidate = {}; + + try { + if(!sdp_candidate) return candidate; + + var error = 0; + var matches = R_WEBRTC_SDP_CANDIDATE.exec(sdp_candidate); + + // Matches! + if(matches) { + candidate.component = matches[2] || error++; + candidate.foundation = matches[1] || error++; + candidate.generation = matches[16] || JSJAC_JINGLE_GENERATION; + candidate.id = this.parent.utils.generate_id(); + candidate.ip = matches[5] || error++; + candidate.network = JSJAC_JINGLE_NETWORK; + candidate.port = matches[6] || error++; + candidate.priority = matches[4] || error++; + candidate.protocol = matches[3] || error++; + candidate['rel-addr'] = matches[11]; + candidate['rel-port'] = matches[13]; + candidate.type = matches[8] || error++; + } + + // Incomplete? + if(error !== 0) return {}; + } catch(e) { + this.parent.get_debug().log('[JSJaCJingle:sdp] _parse_candidate > ' + e, 1); + } + + return candidate; + }, + + /** + * Parses SDP candidate & store it + * @private + * @param {Object} sdp_candidate + */ + _parse_candidate_store: function(sdp_candidate) { + // Store received candidate + var candidate_media = sdp_candidate.media; + var candidate_data = sdp_candidate.candidate; + + // Convert SDP raw data to an object + var candidate_obj = this._parse_candidate(candidate_data); + + this.parent._set_candidates_local( + this.parent.utils.name_generate(candidate_media), + candidate_obj ); - // Event: onicecandidate - self._get_peer_connection().onicecandidate = function(e) { - if(e.candidate) { - self._util_sdp_parse_candidate_store({ - media : (isNaN(e.candidate.sdpMid) ? e.candidate.sdpMid : self._util_media_generate(parseInt(e.candidate.sdpMid, 10))), - candidate : e.candidate.candidate - }); + // Enqueue candidate + this.parent._set_candidates_queue_local( + this.parent.utils.name_generate(candidate_media), + candidate_obj + ); + }, + + /** + * Parses SDP candidate & store it from data + * @private + * @param {Object} data + */ + _parse_candidate_store_store_data: function(data) { + this._parse_candidate_store({ + media : (isNaN(data.candidate.sdpMid) ? data.candidate.sdpMid + : this.parent.utils.media_generate(parseInt(data.candidate.sdpMid, 10))), + candidate : data.candidate.candidate + }); + }, + + /** + * Generates SDP description + * @private + * @param {String} type + * @param {Object} group + * @param {Object} payloads + * @param {Object} candidates + * @returns {Object} SDP object + */ + _generate: function(type, group, payloads, candidates) { + try { + var sdp_obj = {}; + + sdp_obj.candidates = this._generate_candidates(candidates); + sdp_obj.description = this._generate_description(type, group, payloads, sdp_obj.candidates); + + return sdp_obj; + } catch(e) { + this.parent.get_debug().log('[JSJaCJingle:sdp] _generate > ' + e, 1); + } + + return {}; + }, + + /** + * Generate SDP candidates + * @private + * @param {Object} candidates + * @returns {Array} SDP candidates array + */ + _generate_candidates: function(candidates) { + var candidates_arr = []; + + try { + // Parse candidates + var i, + cur_media, cur_name, cur_c_name, cur_candidate, cur_label, cur_id, cur_candidate_str; + + for(cur_name in candidates) { + cur_c_name = candidates[cur_name]; + cur_media = this.parent.utils.media_generate(cur_name); + + for(i in cur_c_name) { + cur_candidate = cur_c_name[i]; + + cur_label = JSJAC_JINGLE_MEDIAS[cur_media].label; + cur_id = cur_label; + cur_candidate_str = ''; + + cur_candidate_str += 'a=candidate:'; + cur_candidate_str += (cur_candidate.foundation || cur_candidate.id); + cur_candidate_str += ' '; + cur_candidate_str += cur_candidate.component; + cur_candidate_str += ' '; + cur_candidate_str += cur_candidate.protocol || JSJAC_JINGLE_SDP_CANDIDATE_PROTOCOL_DEFAULT; + cur_candidate_str += ' '; + cur_candidate_str += cur_candidate.priority || JSJAC_JINGLE_SDP_CANDIDATE_PRIORITY_DEFAULT; + cur_candidate_str += ' '; + cur_candidate_str += cur_candidate.ip; + cur_candidate_str += ' '; + cur_candidate_str += cur_candidate.port; + + if(cur_candidate.type) { + cur_candidate_str += ' '; + cur_candidate_str += 'typ'; + cur_candidate_str += ' '; + cur_candidate_str += cur_candidate.type; + } + + if(cur_candidate['rel-addr'] && cur_candidate['rel-port']) { + cur_candidate_str += ' '; + cur_candidate_str += 'raddr'; + cur_candidate_str += ' '; + cur_candidate_str += cur_candidate['rel-addr']; + cur_candidate_str += ' '; + cur_candidate_str += 'rport'; + cur_candidate_str += ' '; + cur_candidate_str += cur_candidate['rel-port']; + } + + if(cur_candidate.generation) { + cur_candidate_str += ' '; + cur_candidate_str += 'generation'; + cur_candidate_str += ' '; + cur_candidate_str += cur_candidate.generation; + } + + cur_candidate_str += WEBRTC_SDP_LINE_BREAK; + + candidates_arr.push({ + label : cur_label, + id : cur_id, + candidate : cur_candidate_str + }); + } + } + } catch(e) { + this.parent.get_debug().log('[JSJaCJingle:sdp] _generate_candidates > ' + e, 1); + } + + return candidates_arr; + }, + + /** + * Generates SDP description + * @private + * @param {String} type + * @param {Object} group + * @param {Object} payloads + * @param {Object} sdp_candidates + * @returns {Object} SDP description payloads + */ + _generate_description: function(type, group, payloads, sdp_candidates) { + var payloads_obj = {}; + + try { + var payloads_str = ''; + var is_common_credentials = this.parent.utils.is_sdp_common_credentials(payloads); + + // Common vars + var i, c, j, k, l, m, n, o, p, q, r, s, t, u, + cur_name, cur_name_first, cur_name_obj, + cur_media, cur_senders, + cur_group_semantics, cur_group_names, cur_group_name, + cur_network_obj, cur_transports_obj, cur_transports_obj_first, cur_description_obj, + cur_d_pwd, cur_d_ufrag, cur_d_fingerprint, + cur_d_attrs, cur_d_rtcp_fb, cur_d_bandwidth, cur_d_encryption, + cur_d_ssrc, cur_d_ssrc_id, cur_d_ssrc_obj, cur_d_ssrc_group, cur_d_ssrc_group_semantics, cur_d_ssrc_group_obj, + cur_d_rtcp_fb_obj, + cur_d_payload, cur_d_payload_obj, cur_d_payload_obj_attrs, cur_d_payload_obj_id, + cur_d_payload_obj_parameter, cur_d_payload_obj_parameter_obj, cur_d_payload_obj_parameter_str, + cur_d_payload_obj_rtcp_fb, cur_d_payload_obj_rtcp_fb_obj, + cur_d_payload_obj_rtcp_fb_ttr_int, cur_d_payload_obj_rtcp_fb_ttr_int_obj, + cur_d_crypto_obj, cur_d_zrtp_hash_obj, + cur_d_rtp_hdrext, cur_d_rtp_hdrext_obj, + cur_d_rtcp_mux; + + // Payloads headers + payloads_str += this._generate_protocol_version(); + payloads_str += WEBRTC_SDP_LINE_BREAK; + payloads_str += this._generate_origin(); + payloads_str += WEBRTC_SDP_LINE_BREAK; + payloads_str += this._generate_session_name(); + payloads_str += WEBRTC_SDP_LINE_BREAK; + payloads_str += this._generate_timing(); + payloads_str += WEBRTC_SDP_LINE_BREAK; + + // Add groups + for(cur_group_semantics in group) { + cur_group_names = group[cur_group_semantics]; + + payloads_str += 'a=group:' + cur_group_semantics; + + for(s in cur_group_names) { + cur_group_name = cur_group_names[s]; + payloads_str += ' ' + cur_group_name; + } + + payloads_str += WEBRTC_SDP_LINE_BREAK; + } + + // Common credentials? + if(is_common_credentials === true) { + for(cur_name_first in payloads) { + cur_transports_obj_first = payloads[cur_name_first].transports || {}; + + payloads_str += this._generate_credentials( + cur_transports_obj_first.ufrag, + cur_transports_obj_first.pwd, + cur_transports_obj_first.fingerprint + ); + + break; + } + } + + // Add media groups + for(cur_name in payloads) { + cur_name_obj = payloads[cur_name]; + cur_senders = this.parent.get_senders(cur_name); + cur_media = this.parent.get_name(cur_name) ? this.parent.utils.media_generate(cur_name) : null; + + // No media? + if(!cur_media) continue; + + // Network + cur_network_obj = this.parent.utils.network_extract_main(cur_name, sdp_candidates); + + // Transports + cur_transports_obj = cur_name_obj.transports || {}; + cur_d_pwd = cur_transports_obj.pwd; + cur_d_ufrag = cur_transports_obj.ufrag; + cur_d_fingerprint = cur_transports_obj.fingerprint; + + // Descriptions + cur_description_obj = cur_name_obj.descriptions; + cur_d_attrs = cur_description_obj.attrs; + cur_d_rtcp_fb = cur_description_obj['rtcp-fb']; + cur_d_bandwidth = cur_description_obj.bandwidth; + cur_d_payload = cur_description_obj.payload; + cur_d_encryption = cur_description_obj.encryption; + cur_d_ssrc = cur_description_obj.ssrc; + cur_d_ssrc_group = cur_description_obj['ssrc-group']; + cur_d_rtp_hdrext = cur_description_obj['rtp-hdrext']; + cur_d_rtcp_mux = cur_description_obj['rtcp-mux']; + + // Current media + payloads_str += this._generate_description_media( + cur_media, + cur_network_obj.port, + cur_d_encryption, + cur_d_fingerprint, + cur_d_payload + ); + payloads_str += WEBRTC_SDP_LINE_BREAK; + + payloads_str += 'c=' + + cur_network_obj.scope + ' ' + + cur_network_obj.protocol + ' ' + + cur_network_obj.ip; + payloads_str += WEBRTC_SDP_LINE_BREAK; + + payloads_str += 'a=rtcp:' + + cur_network_obj.port + ' ' + + cur_network_obj.scope + ' ' + + cur_network_obj.protocol + ' ' + + cur_network_obj.ip; + payloads_str += WEBRTC_SDP_LINE_BREAK; + + // Specific credentials? + if(is_common_credentials === false) { + payloads_str += this._generate_credentials( + cur_d_ufrag, + cur_d_pwd, + cur_d_fingerprint + ); + } + + // Fingerprint + if(cur_d_fingerprint && cur_d_fingerprint.setup) { + payloads_str += 'a=setup:' + cur_d_fingerprint.setup; + payloads_str += WEBRTC_SDP_LINE_BREAK; + } + + // RTP-HDREXT + if(cur_d_rtp_hdrext && cur_d_rtp_hdrext.length) { + for(i in cur_d_rtp_hdrext) { + cur_d_rtp_hdrext_obj = cur_d_rtp_hdrext[i]; + + payloads_str += 'a=extmap:' + cur_d_rtp_hdrext_obj.id; + + if(cur_d_rtp_hdrext_obj.senders) + payloads_str += '/' + cur_d_rtp_hdrext_obj.senders; + + payloads_str += ' ' + cur_d_rtp_hdrext_obj.uri; + payloads_str += WEBRTC_SDP_LINE_BREAK; + } + } + + // Senders + if(cur_senders) { + payloads_str += 'a=' + JSJAC_JINGLE_SENDERS[cur_senders]; + payloads_str += WEBRTC_SDP_LINE_BREAK; + } + + // Name + if(cur_media && JSJAC_JINGLE_MEDIAS[cur_media]) { + payloads_str += 'a=mid:' + (JSJAC_JINGLE_MEDIAS[cur_media]).label; + payloads_str += WEBRTC_SDP_LINE_BREAK; + } + + // RTCP-MUX + // WARNING: no spec! + // See: http://code.google.com/p/libjingle/issues/detail?id=309 + // http://mail.jabber.org/pipermail/jingle/2011-December/001761.html + if(cur_d_rtcp_mux) { + payloads_str += 'a=rtcp-mux'; + payloads_str += WEBRTC_SDP_LINE_BREAK; + } + + // 'encryption' + if(cur_d_encryption) { + // 'crypto' + for(j in cur_d_encryption.crypto) { + cur_d_crypto_obj = cur_d_encryption.crypto[j]; + + payloads_str += 'a=crypto:' + + cur_d_crypto_obj.tag + ' ' + + cur_d_crypto_obj['crypto-suite'] + ' ' + + cur_d_crypto_obj['key-params'] + + (cur_d_crypto_obj['session-params'] ? (' ' + cur_d_crypto_obj['session-params']) : ''); + + payloads_str += WEBRTC_SDP_LINE_BREAK; + } + + // 'zrtp-hash' + for(p in cur_d_encryption['zrtp-hash']) { + cur_d_zrtp_hash_obj = cur_d_encryption['zrtp-hash'][p]; + + payloads_str += 'a=zrtp-hash:' + + cur_d_zrtp_hash_obj.version + ' ' + + cur_d_zrtp_hash_obj.value; + + payloads_str += WEBRTC_SDP_LINE_BREAK; + } + } + + // 'rtcp-fb' (common) + for(n in cur_d_rtcp_fb) { + cur_d_rtcp_fb_obj = cur_d_rtcp_fb[n]; + + payloads_str += 'a=rtcp-fb:*'; + payloads_str += ' ' + cur_d_rtcp_fb_obj.type; + + if(cur_d_rtcp_fb_obj.subtype) + payloads_str += ' ' + cur_d_rtcp_fb_obj.subtype; + + payloads_str += WEBRTC_SDP_LINE_BREAK; + } + + // 'bandwidth' (common) + for(q in cur_d_bandwidth) { + cur_d_bandwidth_obj = cur_d_bandwidth[q]; + + payloads_str += 'b=' + cur_d_bandwidth_obj.type; + payloads_str += ':' + cur_d_bandwidth_obj.value; + payloads_str += WEBRTC_SDP_LINE_BREAK; + } + + // 'payload-type' + for(k in cur_d_payload) { + cur_d_payload_obj = cur_d_payload[k]; + cur_d_payload_obj_attrs = cur_d_payload_obj.attrs; + cur_d_payload_obj_parameter = cur_d_payload_obj.parameter; + cur_d_payload_obj_rtcp_fb = cur_d_payload_obj['rtcp-fb']; + cur_d_payload_obj_rtcp_fb_ttr_int = cur_d_payload_obj['rtcp-fb-trr-int']; + + cur_d_payload_obj_id = cur_d_payload_obj_attrs.id; + + payloads_str += 'a=rtpmap:' + cur_d_payload_obj_id; + + // 'rtpmap' + if(cur_d_payload_obj_attrs.name) { + payloads_str += ' ' + cur_d_payload_obj_attrs.name; + + if(cur_d_payload_obj_attrs.clockrate) { + payloads_str += '/' + cur_d_payload_obj_attrs.clockrate; + + if(cur_d_payload_obj_attrs.channels) + payloads_str += '/' + cur_d_payload_obj_attrs.channels; + } + } + + payloads_str += WEBRTC_SDP_LINE_BREAK; + + // 'parameter' + if(cur_d_payload_obj_parameter.length) { + payloads_str += 'a=fmtp:' + cur_d_payload_obj_id + ' '; + cur_d_payload_obj_parameter_str = ''; + + for(o in cur_d_payload_obj_parameter) { + cur_d_payload_obj_parameter_obj = cur_d_payload_obj_parameter[o]; + + if(cur_d_payload_obj_parameter_str) cur_d_payload_obj_parameter_str += ';'; + + cur_d_payload_obj_parameter_str += cur_d_payload_obj_parameter_obj.name; + + if(cur_d_payload_obj_parameter_obj.value !== null) { + cur_d_payload_obj_parameter_str += '='; + cur_d_payload_obj_parameter_str += cur_d_payload_obj_parameter_obj.value; + } + } + + payloads_str += cur_d_payload_obj_parameter_str; + payloads_str += WEBRTC_SDP_LINE_BREAK; + } + + // 'rtcp-fb' (sub) + for(l in cur_d_payload_obj_rtcp_fb) { + cur_d_payload_obj_rtcp_fb_obj = cur_d_payload_obj_rtcp_fb[l]; + + payloads_str += 'a=rtcp-fb:' + cur_d_payload_obj_id; + payloads_str += ' ' + cur_d_payload_obj_rtcp_fb_obj.type; + + if(cur_d_payload_obj_rtcp_fb_obj.subtype) + payloads_str += ' ' + cur_d_payload_obj_rtcp_fb_obj.subtype; + + payloads_str += WEBRTC_SDP_LINE_BREAK; + } + + // 'rtcp-fb-ttr-int' + for(m in cur_d_payload_obj_rtcp_fb_ttr_int) { + cur_d_payload_obj_rtcp_fb_ttr_int_obj = cur_d_payload_obj_rtcp_fb_ttr_int[m]; + + payloads_str += 'a=rtcp-fb:' + cur_d_payload_obj_id; + payloads_str += ' ' + 'trr-int'; + payloads_str += ' ' + cur_d_payload_obj_rtcp_fb_ttr_int_obj.value; + payloads_str += WEBRTC_SDP_LINE_BREAK; + } + } + + if(cur_d_attrs.ptime) payloads_str += 'a=ptime:' + cur_d_attrs.ptime + WEBRTC_SDP_LINE_BREAK; + if(cur_d_attrs.maxptime) payloads_str += 'a=maxptime:' + cur_d_attrs.maxptime + WEBRTC_SDP_LINE_BREAK; + + // 'ssrc-group' + for(cur_d_ssrc_group_semantics in cur_d_ssrc_group) { + for(t in cur_d_ssrc_group[cur_d_ssrc_group_semantics]) { + cur_d_ssrc_group_obj = cur_d_ssrc_group[cur_d_ssrc_group_semantics][t]; + + payloads_str += 'a=ssrc-group'; + payloads_str += ':' + cur_d_ssrc_group_semantics; + + for(u in cur_d_ssrc_group_obj.sources) { + payloads_str += ' ' + cur_d_ssrc_group_obj.sources[u].ssrc; + } + + payloads_str += WEBRTC_SDP_LINE_BREAK; + } + } + + // 'ssrc' + for(cur_d_ssrc_id in cur_d_ssrc) { + for(r in cur_d_ssrc[cur_d_ssrc_id]) { + cur_d_ssrc_obj = cur_d_ssrc[cur_d_ssrc_id][r]; + + payloads_str += 'a=ssrc'; + payloads_str += ':' + cur_d_ssrc_id; + payloads_str += ' ' + cur_d_ssrc_obj.name; + + if(cur_d_ssrc_obj.value) + payloads_str += ':' + cur_d_ssrc_obj.value; + + payloads_str += WEBRTC_SDP_LINE_BREAK; + } + } + + // Candidates (some browsers require them there, too) + if(typeof sdp_candidates == 'object') { + for(c in sdp_candidates) { + if((sdp_candidates[c]).label == JSJAC_JINGLE_MEDIAS[cur_media].label) + payloads_str += (sdp_candidates[c]).candidate; + } + } + } + + // Push to object + payloads_obj.type = type; + payloads_obj.sdp = payloads_str; + } catch(e) { + this.parent.get_debug().log('[JSJaCJingle:sdp] _generate_description > ' + e, 1); + } + + return payloads_obj; + }, + + /** + * Generates SDP protocol version + * @private + * @returns {String} SDP protocol version raw text + */ + _generate_protocol_version: function() { + return 'v=0'; + }, + + /** + * Generates SDP origin + * @private + * @returns {String} SDP origin raw text + */ + _generate_origin: function() { + var sdp_origin = ''; + + try { + // Values + var jid = new JSJaCJID(this.parent.get_initiator()); + + var username = jid.getNode() ? jid.getNode() : '-'; + var session_id = '1'; + var session_version = '1'; + var nettype = JSJAC_JINGLE_SDP_CANDIDATE_SCOPE_DEFAULT; + var addrtype = JSJAC_JINGLE_SDP_CANDIDATE_IPVERSION_DEFAULT; + var unicast_address = JSJAC_JINGLE_SDP_CANDIDATE_IP_DEFAULT; + + // Line content + sdp_origin += 'o='; + sdp_origin += username + ' '; + sdp_origin += session_id + ' '; + sdp_origin += session_version + ' '; + sdp_origin += nettype + ' '; + sdp_origin += addrtype + ' '; + sdp_origin += unicast_address; + } catch(e) { + this.parent.get_debug().log('[JSJaCJingle:sdp] _generate_origin > ' + e, 1); + } + + return sdp_origin; + }, + + /** + * Generates SDP session name + * @private + * @returns {String} SDP session name raw text + */ + _generate_session_name: function() { + return 's=' + (this.parent.get_sid() || '-'); + }, + + /** + * Generates SDP timing + * @private + * @returns {String} SDP timing raw text + */ + _generate_timing: function() { + return 't=0 0'; + }, + + /** + * Generates SDP credentials + * @private + * @param {String} ufrag + * @param {String} pwd + * @param {Object} fingerprint + * @returns {String} SDP credentials raw text + */ + _generate_credentials: function(ufrag, pwd, fingerprint) { + var sdp = ''; + + // ICE credentials + if(ufrag) sdp += 'a=ice-ufrag:' + ufrag + WEBRTC_SDP_LINE_BREAK; + if(pwd) sdp += 'a=ice-pwd:' + pwd + WEBRTC_SDP_LINE_BREAK; + + // Fingerprint + if(fingerprint) { + if(fingerprint.hash && fingerprint.value) { + sdp += 'a=fingerprint:' + fingerprint.hash + ' ' + fingerprint.value; + sdp += WEBRTC_SDP_LINE_BREAK; + } + } + + return sdp; + }, + + /** + * Generates SDP media description + * @private + * @param {String} media + * @param {String} port + * @param {String} crypto + * @param {Object} fingerprint + * @param {Array} payload + * @returns {String} SDP media raw text + */ + _generate_description_media: function(media, port, crypto, fingerprint, payload) { + var sdp_media = ''; + + try { + var i; + var type_ids = []; + + sdp_media += 'm=' + media + ' ' + port + ' '; + + // Protocol + if((crypto && crypto.length) || (fingerprint && fingerprint.hash && fingerprint.value)) + sdp_media += 'RTP/SAVPF'; + else + sdp_media += 'RTP/AVPF'; + + // Payload type IDs + for(i in payload) type_ids.push(payload[i].attrs.id); + + sdp_media += ' ' + type_ids.join(' '); + } catch(e) { + this.parent.get_debug().log('[JSJaCJingle:sdp] _generate_description_media > ' + e, 1); + } + + return sdp_media; + }, + } +); +/** + * @fileoverview JSJaC Jingle library - Base call lib + * + * @url https://github.com/valeriansaliou/jsjac-jingle + * @depends https://github.com/sstrigler/JSJaC + * @author Valérian Saliou https://valeriansaliou.name/ + * @license Mozilla Public License v2.0 (MPL v2.0) + */ + + +/** @module jsjac-jingle/base */ +/** @exports __JSJaCJingleBase */ + + +/** + * Abstract base class for XMPP Jingle sessions. + * @abstract + * @class + * @classdesc Abstract base class for XMPP Jingle sessions. + * @requires nicolas-van/ring.js + * @requires sstrigler/JSJaC + * @requires jsjac-jingle/utils + * @requires jsjac-jingle/sdp + * @see {@link http://ringjs.neoname.eu/|Ring.js} + * @see {@link http://stefan-strigler.de/jsjac-1.3.4/doc/|JSJaC Documentation} + * @param {Object} [args] - Jingle session arguments. + * @property {DOM} [args.local_view] - The path to the local stream view element. + * @property {Boolean} [args.local_stream_readonly] - Whether the local stream is read-only or not. + * @property {String} [args.to] - The full JID to start the Jingle session with. + * @property {String} [args.connection] - The connection to be attached to. + * @property {String} [args.media] - The media type to be used in the Jingle session. + * @property {String} [args.resolution] - The resolution to be used for video in the Jingle session. + * @property {String} [args.bandwidth] - The bandwidth to be limited for video in the Jingle session. + * @property {String} [args.fps] - The framerate to be used for video in the Jingle session. + * @property {Array} [args.stun] - A list of STUN servers to use (override the default one). + * @property {Array} [args.turn] - A list of TURN servers to use. + * @property {Boolean} [args.sdp_trace] - Log SDP trace in console (requires a debug interface). + * @property {Boolean} [args.net_trace] - Log network packet trace in console (requires a debug interface). + * @property {JSJaCDebugger} [args.debug] - A reference to a debugger implementing the JSJaCDebugger interface. + */ +var __JSJaCJingleBase = ring.create( + /** @lends __JSJaCJingleBase.prototype */ + { + /** + * Constructor + */ + constructor: function(args) { + /** + * @constant + * @member {JSJaCJingleUtils} + * @readonly + * @default + * @public + */ + this.utils = new JSJaCJingleUtils(this); + + /** + * @constant + * @member {JSJaCJingleSDP} + * @readonly + * @default + * @public + */ + this.sdp = new JSJaCJingleSDP(this); + + if(args && args.to) + /** + * @constant + * @member {String} + * @default + * @private + */ + this._to = args.to; + + if(args && args.connection) { + /** + * @constant + * @member {JSJaCConnection} + * @default + * @private + */ + this._connection = args.connection; + } else { + /** + * @constant + * @member {JSJaCConnection} + * @default + * @private + */ + this._connection = JSJaCJingleStorage.get_connection(); + } + + if(args && args.media) + /** + * @member {String} + * @default + * @private + */ + this._media = args.media; + + if(args && args.video_source) + /** + * @member {String} + * @default + * @private + */ + this._video_source = args.video_source; + + if(args && args.resolution) + /** + * @member {String} + * @default + * @private + */ + this._resolution = args.resolution; + + if(args && args.bandwidth) + /** + * @member {Number} + * @default + * @private + */ + this._bandwidth = args.bandwidth; + + if(args && args.fps) + /** + * @member {Number} + * @default + * @private + */ + this._fps = args.fps; + + if(args && args.local_view) { + if(args.local_view instanceof Array) { + /** + * @member {DOM} + * @default + * @private + */ + this._local_view = args.local_view; + } else { + /** + * @member {DOM} + * @default + * @private + */ + this._local_view = [args.local_view]; + } + } + + if(args && args.local_stream_readonly) { + /** + * @constant + * @member {Boolean} + * @default + * @private + */ + this._local_stream_readonly = args.local_stream_readonly; + } else { + this._local_stream_readonly = false; + } + + if(args && args.stun) { + /** + * @constant + * @member {Array} + * @default + * @private + */ + this._stun = args.stun; + } else { + this._stun = []; + } + + if(args && args.turn) { + /** + * @constant + * @member {Array} + * @default + * @private + */ + this._turn = args.turn; + } else { + this._turn = []; + } + + if(args && args.sdp_trace) + /** + * @member {Boolean} + * @default + * @private + */ + this._sdp_trace = args.sdp_trace; + + if(args && args.net_trace) + /** + * @member {Boolean} + * @default + * @private + */ + this._net_trace = args.net_trace; + + if(args && args.debug && args.debug.log) { + /** + * @member {JSJaCDebugger} + * @default + * @private + */ + this._debug = args.debug; + } else { + /** + * @member {Function} + * @default + * @private + */ + this._debug = JSJaCJingleStorage.get_debug(); + } + + /** + * @member {String} + * @default + * @private + */ + this._initiator = ''; + + /** + * @member {String} + * @default + * @private + */ + this._responder = ''; + + /** + * @member {Object} + * @default + * @private + */ + this._creator = {}; + + /** + * @member {Object} + * @default + * @private + */ + this._senders = {}; + + /** + * @member {Object} + * @default + * @private + */ + this._local_stream = null; + + /** + * @member {Object} + * @default + * @private + */ + this._content_local = []; + + /** + * @member {Object} + * @default + * @private + */ + this._peer_connection = {}; + + /** + * @member {Number} + * @default + * @private + */ + this._id = 0; + + /** + * @member {Object} + * @default + * @private + */ + this._sent_id = {}; + + /** + * @member {Object} + * @default + * @private + */ + this._received_id = {}; + + /** + * @member {Array} + * @default + * @private + */ + this._payloads_local = []; + + /** + * @member {Object} + * @default + * @private + */ + this._group_local = {}; + + /** + * @member {Object} + * @default + * @private + */ + this._candidates_local = {}; + + /** + * @member {Object} + * @default + * @private + */ + this._candidates_queue_local = {}; + + /** + * @member {Object} + * @default + * @private + */ + this._registered_handlers = {}; + + /** + * @member {Object} + * @default + * @private + */ + this._deferred_handlers = {}; + + /** + * @member {Object} + * @default + * @private + */ + this._mute = {}; + + /** + * @member {Boolean} + * @default + * @private + */ + this._lock = false; + + /** + * @member {Boolean} + * @default + * @private + */ + this._media_busy = false; + + /** + * @member {String} + * @default + * @private + */ + this._sid = ''; + + /** + * @member {Object} + * @default + * @private + */ + this._name = {}; + }, + + + + /** + * JSJSAC JINGLE REGISTERS + */ + + /** + * Registers a given handler on a given Jingle stanza + * @public + * @param {String} node + * @param {String} type + * @param {String} id + * @param {Function} fn + * @returns {Boolean} Success + */ + register_handler: function(node, type, id, fn) { + this.get_debug().log('[JSJaCJingle:base] register_handler', 4); + + try { + if(typeof fn !== 'function') { + this.get_debug().log('[JSJaCJingle:base] register_handler > fn parameter not passed or not a function!', 1); + return false; + } + + if(id) { + this._set_registered_handlers(node, type, id, fn); + + this.get_debug().log('[JSJaCJingle:base] register_handler > Registered handler for node: ' + node + ', id: ' + id + ' and type: ' + type, 3); + return true; + } else { + this.get_debug().log('[JSJaCJingle:base] register_handler > Could not register handler (no ID).', 1); + return false; + } + } catch(e) { + this.get_debug().log('[JSJaCJingle:base] register_handler > ' + e, 1); + } + + return false; + }, + + /** + * Unregisters the given handler on a given Jingle stanza + * @public + * @param {String} node + * @param {String} type + * @param {String} id + * @returns {Boolean} Success + */ + unregister_handler: function(node, type, id) { + this.get_debug().log('[JSJaCJingle:base] unregister_handler', 4); + + try { + if(this.get_registered_handlers(node, type, id).length >= 1) { + this._set_registered_handlers(node, type, id, null); + + this.get_debug().log('[JSJaCJingle:base] unregister_handler > Unregistered handler for node: ' + node + ', id: ' + id + ' and type: ' + type, 3); + return true; + } else { + this.get_debug().log('[JSJaCJingle:base] unregister_handler > Could not unregister handler with node: ' + node + ', id: ' + id + ' and type: ' + type + ' (not found).', 2); + return false; + } + } catch(e) { + this.get_debug().log('[JSJaCJingle:base] unregister_handler > ' + e, 1); + } + + return false; + }, + + /** + * Defers a given handler + * @public + * @param {String} ns + * @param {Function} fn + * @returns {Boolean} Success + */ + defer_handler: function(ns, fn) { + this.get_debug().log('[JSJaCJingle:base] defer_handler', 4); + + try { + if(typeof fn !== 'function') { + this.get_debug().log('[JSJaCJingle:base] defer_handler > fn parameter not passed or not a function!', 1); + return false; + } + + this._set_deferred_handlers(ns, fn); + + this.get_debug().log('[JSJaCJingle:base] defer_handler > Deferred handler for namespace: ' + ns, 3); + return true; + } catch(e) { + this.get_debug().log('[JSJaCJingle:base] defer_handler > ' + e, 1); + } + + return false; + }, + + /** + * Undefers the given handler + * @public + * @param {String} ns + * @returns {Boolean} Success + */ + undefer_handler: function(ns) { + this.get_debug().log('[JSJaCJingle:base] undefer_handler', 4); + + try { + if(ns in this._deferred_handlers) { + this._set_deferred_handlers(ns, null); + + this.get_debug().log('[JSJaCJingle:base] undefer_handler > Undeferred handler for namespace: ' + ns, 3); + return true; + } else { + this.get_debug().log('[JSJaCJingle:base] undefer_handler > Could not undefer handler with namespace: ' + ns + ' (not found).', 2); + return false; + } + } catch(e) { + this.get_debug().log('[JSJaCJingle:base] undefer_handler > ' + e, 1); + } + + return false; + }, + + /** + * Registers a view element + * @public + * @param {String} tyoe + * @param {DOM} view + * @returns {Boolean} Success + */ + register_view: function(type, view) { + this.get_debug().log('[JSJaCJingle:base] register_view', 4); + + try { + // Get view functions + var fn = this.utils.map_register_view(type); + + if(fn.type == type) { + var i; + + // Check view is not already registered + for(i in (fn.view.get)()) { + if((fn.view.get)()[i] == view) { + this.get_debug().log('[JSJaCJingle:base] register_view > Could not register view of type: ' + type + ' (already registered).', 2); + return true; + } + } + + // Proceeds registration + (fn.view.set)(view); + + this.utils._peer_stream_attach( + [view], + (fn.stream.get)(), + fn.mute + ); + + this.get_debug().log('[JSJaCJingle:base] register_view > Registered view of type: ' + type, 3); + + return true; + } else { + this.get_debug().log('[JSJaCJingle:base] register_view > Could not register view of type: ' + type + ' (type unknown).', 1); + return false; + } + } catch(e) { + this.get_debug().log('[JSJaCJingle:base] register_view > ' + e, 1); + } + + return false; + }, + + /** + * Unregisters a view element + * @public + * @param {String} type + * @param {DOM} view + * @returns {Boolean} Success + */ + unregister_view: function(type, view) { + this.get_debug().log('[JSJaCJingle:base] unregister_view', 4); + + try { + // Get view functions + var fn = this.utils.map_unregister_view(type); + + if(fn.type == type) { + var i; + + // Check view is registered + for(i in (fn.view.get)()) { + if((fn.view.get)()[i] == view) { + // Proceeds un-registration + this.utils._peer_stream_detach( + [view] + ); + + this.utils.array_remove_value( + (fn.view.get)(), + view + ); + + this.get_debug().log('[JSJaCJingle:base] unregister_view > Unregistered view of type: ' + type, 3); + return true; + } + } + + this.get_debug().log('[JSJaCJingle:base] unregister_view > Could not unregister view of type: ' + type + ' (not found).', 2); + return true; + } else { + this.get_debug().log('[JSJaCJingle:base] unregister_view > Could not unregister view of type: ' + type + ' (type unknown).', 1); + return false; + } + } catch(e) { + this.get_debug().log('[JSJaCJingle:base] unregister_view > ' + e, 1); + } + + return false; + }, + + + + /** + * JSJSAC JINGLE PEER TOOLS + */ + + /** + * Creates a new peer connection + * @private + * @param {Function} sdp_message_callback + */ + _peer_connection_create: function(sdp_message_callback) { + this.get_debug().log('[JSJaCJingle:base] _peer_connection_create', 4); + + try { + // Create peer connection instance + this._peer_connection_create_instance(); + + // Event callbacks + this._peer_connection_callbacks(sdp_message_callback); + + // Add local stream + this._peer_connection_create_local_stream(); + + // Create offer/answer + this._peer_connection_create_dispatch(sdp_message_callback); + } catch(e) { + this.get_debug().log('[JSJaCJingle:base] _peer_connection_create > ' + e, 1); + } + }, + + /** + * Creates peer connection local stream + * @private + */ + _peer_connection_create_local_stream: function() { + this.get_debug().log('[JSJaCJingle:base] _peer_connection_create_local_stream', 4); + + try { + this.get_peer_connection().addStream( + this.get_local_stream() + ); + } catch(e) { + this.get_debug().log('[JSJaCJingle:base] _peer_connection_create_local_stream > ' + e, 1); + } + }, + + /** + * Requests the user media (audio/video) + * @private + * @param {Function} callback + */ + _peer_get_user_media: function(callback) { + this.get_debug().log('[JSJaCJingle:base] _peer_get_user_media', 4); + + try { + if(this.get_local_stream() === null) { + this.get_debug().log('[JSJaCJingle:base] _peer_get_user_media > Getting user media...', 2); + + (WEBRTC_GET_MEDIA.bind(navigator))( + this.utils.generate_constraints(), + this._peer_got_user_media_success.bind(this, callback), + this._peer_got_user_media_error.bind(this) + ); + } else { + this.get_debug().log('[JSJaCJingle:base] _peer_get_user_media > User media already acquired.', 2); + + callback(); + } + } catch(e) { + this.get_debug().log('[JSJaCJingle:base] _peer_get_user_media > ' + e, 1); + } + }, + + /** + * Triggers the media obtained success event + * @private + * @param {Function} callback + * @param {Object} stream + */ + _peer_got_user_media_success: function(callback, stream) { + this.get_debug().log('[JSJaCJingle:base] _peer_got_user_media_success', 4); + + try { + this.get_debug().log('[JSJaCJingle:base] _peer_got_user_media_success > Got user media.', 2); + + this._set_local_stream(stream); + + if(callback && typeof callback == 'function') { + if((this.get_media() == JSJAC_JINGLE_MEDIA_VIDEO) && this.get_local_view().length) { + var _this = this; + + var fn_loaded = function() { + _this.get_debug().log('[JSJaCJingle:base] _peer_got_user_media_success > Local video loaded.', 2); + + this.removeEventListener('loadeddata', fn_loaded, false); + callback(); + }; + + if(_this.get_local_view()[0].readyState >= JSJAC_JINGLE_MEDIA_READYSTATE_LOADED) { + fn_loaded(); + } else { + this.get_debug().log('[JSJaCJingle:base] _peer_got_user_media_success > Waiting for local video to be loaded...', 2); + + _this.get_local_view()[0].addEventListener('loadeddata', fn_loaded, false); + } + } else { + callback(); + } + } + } catch(e) { + this.get_debug().log('[JSJaCJingle:base] _peer_got_user_media_success > ' + e, 1); + } + }, + + /** + * Triggers the SDP description retrieval success event + * @private + * @param {Object} sdp_local + * @param {Function} [sdp_message_callback] + */ + _peer_got_description: function(sdp_local, sdp_message_callback) { + this.get_debug().log('[JSJaCJingle:base] _peer_got_description', 4); + + try { + this.get_debug().log('[JSJaCJingle:base] _peer_got_description > Got local description.', 2); + + if(this.get_sdp_trace()) this.get_debug().log('[JSJaCJingle:base] _peer_got_description > SDP (local:raw)' + '\n\n' + sdp_local.sdp, 4); + + // Convert SDP raw data to an object + var cur_name; + var payload_parsed = this.sdp._parse_payload(sdp_local.sdp); + this.sdp._resolution_payload(payload_parsed); + + for(cur_name in payload_parsed) { + this._set_payloads_local( + cur_name, + payload_parsed[cur_name] + ); + } + + var cur_semantics; + var group_parsed = this.sdp._parse_group(sdp_local.sdp); + + for(cur_semantics in group_parsed) { + this._set_group_local( + cur_semantics, + group_parsed[cur_semantics] + ); + } + + // Filter our local description (remove unused medias) + var sdp_local_desc = this.sdp._generate_description( + sdp_local.type, + this.get_group_local(), + this.get_payloads_local(), + + this.sdp._generate_candidates( + this.get_candidates_local() + ) + ); + + if(this.get_sdp_trace()) this.get_debug().log('[JSJaCJingle:base] _peer_got_description > SDP (local:gen)' + '\n\n' + sdp_local_desc.sdp, 4); + + var _this = this; + + this.get_peer_connection().setLocalDescription( + (new WEBRTC_SESSION_DESCRIPTION(sdp_local_desc)), + + function() { + // Success (descriptions are compatible) + }, + + function(e) { + if(_this.get_sdp_trace()) _this.get_debug().log('[JSJaCJingle:base] _peer_got_description > SDP (local:error)' + '\n\n' + (e.message || e.name || 'Unknown error'), 4); + + // Error (descriptions are incompatible) + } + ); + + // Need to wait for local candidates? + if(typeof sdp_message_callback == 'function') { + this.get_debug().log('[JSJaCJingle:base] _peer_got_description > Executing SDP message callback.', 2); + + /* @function */ + sdp_message_callback(); + } else if(this.utils.count_candidates(this._shortcut_local_user_candidates()) === 0) { + this.get_debug().log('[JSJaCJingle:base] _peer_got_description > Waiting for local candidates...', 2); + } + } catch(e) { + this.get_debug().log('[JSJaCJingle:base] _peer_got_description > ' + e, 1); + } + }, + + /** + * Triggers the SDP description not retrieved error event + * @private + */ + _peer_fail_description: function() { + this.get_debug().log('[JSJaCJingle:base] _peer_fail_description', 4); + + try { + this.get_debug().log('[JSJaCJingle:base] _peer_fail_description > Could not get local description!', 1); + } catch(e) { + this.get_debug().log('[JSJaCJingle:base] _peer_fail_description > ' + e, 1); + } + }, + + /** + * Enables/disables the local stream sound + * @private + * @param {Boolean} enable + */ + _peer_sound: function(enable) { + this.get_debug().log('[JSJaCJingle:base] _peer_sound', 4); + + try { + this.get_debug().log('[JSJaCJingle:base] _peer_sound > Enable: ' + enable, 2); + + var i; + var audio_tracks = this.get_local_stream().getAudioTracks(); + + for(i = 0; i < audio_tracks.length; i++) + audio_tracks[i].enabled = enable; + } catch(e) { + this.get_debug().log('[JSJaCJingle:base] _peer_sound > ' + e, 1); + } + }, + + /** + * Attaches given stream to given DOM element + * @private + * @param {DOM} element + * @param {Object} stream + * @param {Boolean} mute + */ + _peer_stream_attach: function(element, stream, mute) { + try { + var i; + var stream_src = stream ? URL.createObjectURL(stream) : ''; + + for(i in element) { + element[i].src = stream_src; + + if(navigator.mozGetUserMedia) + element[i].play(); + else + element[i].autoplay = true; + + if(typeof mute == 'boolean') element[i].muted = mute; + } + } catch(e) { + this.get_debug().log('[JSJaCJingle:base] _peer_stream_attach > ' + e, 1); + } + }, + + /** + * Detaches stream from given DOM element + * @private + * @param {DOM} element + */ + _peer_stream_detach: function(element) { + try { + var i; + + for(i in element) { + element[i].pause(); + element[i].src = ''; + } + } catch(e) { + this.get_debug().log('[JSJaCJingle:base] _peer_stream_detach > ' + e, 1); + } + }, + + + + /** + * JSJSAC JINGLE STATES + */ + + /** + * Am I responder? + * @public + * @returns {Boolean} Receiver state + */ + is_responder: function() { + return this.utils.negotiation_status() == JSJAC_JINGLE_SENDERS_RESPONDER.jingle; + }, + + /** + * Am I initiator? + * @public + * @returns {Boolean} Initiator state + */ + is_initiator: function() { + return this.utils.negotiation_status() == JSJAC_JINGLE_SENDERS_INITIATOR.jingle; + }, + + + + /** + * JSJSAC JINGLE SHORTCUTS + */ + + /** + * Gets function handler for given member + * @private + * @param {Function|Object} member + * @returns {Function} Handler + */ + _shortcut_get_handler: function(member) { + if(typeof member == 'function') + return member; + + return function() {}; + }, + + + + /** + * JSJSAC JINGLE GETTERS + */ + + /** + * Gets the namespace + * @public + * @returns {String} Namespace value + */ + get_namespace: function() { + return this._namespace; + }, + + /** + * Gets the local payloads + * @public + * @param {String} [name] + * @returns {Object} Local payloads object + */ + get_payloads_local: function(name) { + if(name) + return (name in this._payloads_local) ? this._payloads_local[name] : {}; + + return this._payloads_local; + }, + + /** + * Gets the local group + * @public + * @param {String} [semantics] + * @returns {Object} Local group object + */ + get_group_local: function(semantics) { + if(semantics) + return (semantics in this._group_local) ? this._group_local[semantics] : {}; + + return this._group_local; + }, + + /** + * Gets the local candidates + * @public + * @param {String} [name] + * @returns {Object} Local candidates object + */ + get_candidates_local: function(name) { + if(name) + return (name in this._candidates_local) ? this._candidates_local[name] : {}; + + return this._candidates_local; + }, + + /** + * Gets the local candidates queue + * @public + * @param {String} [name] + * @returns {Object} Local candidates queue object + */ + get_candidates_queue_local: function(name) { + if(name) + return (name in this._candidates_queue_local) ? this._candidates_queue_local[name] : {}; + + return this._candidates_queue_local; + }, + + /** + * Gets the local content + * @public + * @param {String} [name] + * @returns {Object} Local content object + */ + get_content_local: function(name) { + if(name) + return (name in this._content_local) ? this._content_local[name] : {}; + + return this._content_local; + }, + + /** + * Gets the peer connection + * @public + * @returns {Object} Peer connection + */ + get_peer_connection: function() { + return this._peer_connection; + }, + + /** + * Gets the ID + * @public + * @returns {Number} ID value + */ + get_id: function() { + return this._id; + }, + + /** + * Gets the new ID + * @public + * @returns {String} New ID value + */ + get_id_new: function() { + var trans_id = this.get_id() + 1; + this._set_id(trans_id); + + return this.get_id_pre() + trans_id; + }, + + /** + * Gets the sent IDs + * @public + * @returns {Object} Sent IDs object + */ + get_sent_id: function() { + return this._sent_id; + }, + + /** + * Gets the received IDs + * @public + * @returns {Object} Received IDs object + */ + get_received_id: function() { + return this._received_id; + }, + + /** + * Gets the registered stanza handler + * @public + * @param {String} node + * @param {String} type + * @param {String} id + * @returns {Array} Stanza handler + */ + get_registered_handlers: function(node, type, id) { + if(id && node in this._registered_handlers && + type in this._registered_handlers[node] && + typeof this._registered_handlers[node][type][id] == 'object') + return this._registered_handlers[node][type][id]; + + return []; + }, + + /** + * Gets the deferred stanza handler + * @public + * @param {String} ns + * @returns {Array} Stanza handler + */ + get_deferred_handlers: function(ns) { + return this._deferred_handlers[ns] || []; + }, + + /** + * Gets the mute state + * @public + * @param {String} [name] + * @returns {Boolean} Mute value + */ + get_mute: function(name) { + if(!name) name = '*'; + + return (name in this._mute) ? this._mute[name] : false; + }, + + /** + * Gets the lock value + * @public + * @returns {Boolean} Lock value + */ + get_lock: function() { + return this._lock || !JSJAC_JINGLE_AVAILABLE; + }, + + /** + * Gets the media busy value + * @public + * @returns {Boolean} Media busy value + */ + get_media_busy: function() { + return this._media_busy; + }, + + /** + * Gets the sid value + * @public + * @returns {String} SID value + */ + get_sid: function() { + return this._sid; + }, + + /** + * Gets the status value + * @public + * @returns {String} Status value + */ + get_status: function() { + return this._status; + }, + + /** + * Gets the connection value + * @public + * @returns {JSJaCConnection} Connection value + */ + get_connection: function() { + return this._connection; + }, + + /** + * Gets the to value + * @public + * @returns {String} To value + */ + get_to: function() { + return this._to; + }, + + /** + * Gets the initiator value + * @public + * @returns {String} Initiator value + */ + get_initiator: function() { + return this._initiator; + }, + + /** + * Gets the responder value + * @public + * @returns {String} Responder value + */ + get_responder: function() { + return this._responder; + }, + + /** + * Gets the creator value + * @public + * @param {String} [name] + * @returns {String|Object} Creator value + */ + get_creator: function(name) { + if(name) + return (name in this._creator) ? this._creator[name] : null; + + return this._creator; + }, + + /** + * Gets the creator value (for this) + * @public + * @param {String} name + * @returns {String} Creator value + */ + get_creator_this: function(name) { + return this.get_responder() == this.get_to() ? JSJAC_JINGLE_CREATOR_INITIATOR : JSJAC_JINGLE_CREATOR_RESPONDER; + }, + + /** + * Gets the senders value + * @public + * @param {String} [name] + * @returns {String} Senders value + */ + get_senders: function(name) { + if(name) + return (name in this._senders) ? this._senders[name] : null; + + return this._senders; + }, + + /** + * Gets the media value + * @public + * @returns {String} Media value + */ + get_media: function() { + return (this._media && this._media in JSJAC_JINGLE_MEDIAS) ? this._media : JSJAC_JINGLE_MEDIA_VIDEO; + }, + + /** + * Gets a list of medias in use + * @public + * @returns {Object} Media list + */ + get_media_all: function() { + if(this.get_media() == JSJAC_JINGLE_MEDIA_AUDIO) + return [JSJAC_JINGLE_MEDIA_AUDIO]; + + return [JSJAC_JINGLE_MEDIA_AUDIO, JSJAC_JINGLE_MEDIA_VIDEO]; + }, + + /** + * Gets the video source value + * @public + * @returns {String} Video source value + */ + get_video_source: function() { + return (this._video_source && this._video_source in JSJAC_JINGLE_VIDEO_SOURCES) ? this._video_source : JSJAC_JINGLE_VIDEO_SOURCE_CAMERA; + }, + + /** + * Gets the resolution value + * @public + * @returns {String} Resolution value + */ + get_resolution: function() { + return this._resolution ? (this._resolution).toString() : null; + }, + + /** + * Gets the bandwidth value + * @public + * @returns {String} Bandwidth value + */ + get_bandwidth: function() { + return this._bandwidth ? (this._bandwidth).toString() : null; + }, + + /** + * Gets the FPS value + * @public + * @returns {String} FPS value + */ + get_fps: function() { + return this._fps ? (this._fps).toString() : null; + }, + + /** + * Gets the name value + * @public + * @param {String} [name] + * @returns {String} Name value + */ + get_name: function(name) { + if(name) + return name in this._name; + + return this._name; + }, + + /** + * Gets the local stream + * @public + * @returns {Object} Local stream instance + */ + get_local_stream: function() { + return this._local_stream; + }, + + /** + * Gets the local stream read-only state + * @public + * @returns {Boolean} Read-only state + */ + get_local_stream_readonly: function() { + return this._local_stream_readonly; + }, + + /** + * Gets the local view value + * @public + * @returns {DOM} Local view + */ + get_local_view: function() { + return (typeof this._local_view == 'object') ? this._local_view : []; + }, + + /** + * Gets the STUN servers + * @public + * @returns {Array} STUN servers + */ + get_stun: function() { + return (typeof this._stun == 'object') ? this._stun : []; + }, + + /** + * Gets the TURN servers + * @public + * @returns {Array} TURN servers + */ + get_turn: function() { + return (typeof this._turn == 'object') ? this._turn : []; + }, + + /** + * Gets the SDP trace value + * @public + * @returns {Boolean} SDP trace value + */ + get_sdp_trace: function() { + return (this._sdp_trace === true); + }, + + /** + * Gets the network packet trace value + * @public + * @returns {Boolean} Network packet trace value + */ + get_net_trace: function() { + return (this._net_trace === true); + }, + + /** + * Gets the debug value + * @public + * @returns {JSJaCDebugger} Debug value + */ + get_debug: function() { + return this._debug; + }, + + + + /** + * JSJSAC JINGLE SETTERS + */ + + /** + * Sets the namespace + * @private + * @param {String} Namespace value + */ + _set_namespace: function(namespace) { + this._namespace = namespace; + }, + + /** + * Sets the local stream + * @private + * @param {Object} local_stream + */ + _set_local_stream: function(local_stream) { + try { + if(this.get_local_stream_readonly() === true) { + this.get_debug().log('[JSJaCJingle:base] _set_local_stream > Local stream is read-only, not setting it.', 0); return; + } + + if(!local_stream && this._local_stream) { + (this._local_stream).stop(); + + this._peer_stream_detach( + this.get_local_view() + ); + } + + this._set_local_stream_raw(local_stream); + + if(local_stream) { + this._peer_stream_attach( + this.get_local_view(), + this.get_local_stream(), + true + ); + } else { + this._peer_stream_detach( + this.get_local_view() + ); + } + } catch(e) { + this.get_debug().log('[JSJaCJingle:base] _set_local_stream > ' + e, 1); + } + }, + + /** + * Sets the local stream raw object (no further processing) + * @private + * @param {Object} local_stream + */ + _set_local_stream_raw: function(local_stream) { + this._local_stream = local_stream; + }, + + /** + * Sets the local stream read-only state + * @private + * @param {Boolean} local_stream_readonly + */ + _set_local_stream_readonly: function(local_stream_readonly) { + this._local_stream_readonly = local_stream_readonly; + }, + + /** + * Sets the local view + * @private + * @param {DOM} local_view + */ + _set_local_view: function(local_view) { + if(typeof this._local_view !== 'object') + this._local_view = []; + + this._local_view.push(local_view); + }, + + /** + * Sets the local payload + * @private + * @param {String} name + * @param {Object} payload_data + */ + _set_payloads_local: function(name, payload_data) { + this._payloads_local[name] = payload_data; + }, + + /** + * Sets the local group + * @private + * @param {String} name + * @param {Object} group_data + */ + _set_group_local: function(semantics, group_data) { + this._group_local[semantics] = group_data; + }, + + /** + * Sets the local candidates + * @private + * @param {String} name + * @param {Object} candidate_data + */ + _set_candidates_local: function(name, candidate_data) { + if(!(name in this._candidates_local)) this._candidates_local[name] = []; + + (this._candidates_local[name]).push(candidate_data); + }, + + /** + * Sets the local candidates queue + * @private + * @param {String} name + * @param {Object} candidate_data + */ + _set_candidates_queue_local: function(name, candidate_data) { + try { + if(name === null) { + this._candidates_queue_local = {}; + } else { + if(!(name in this._candidates_queue_local)) this._candidates_queue_local[name] = []; + + (this._candidates_queue_local[name]).push(candidate_data); + } + } catch(e) { + this.get_debug().log('[JSJaCJingle:base] _set_candidates_queue_local > ' + e, 1); + } + }, + + /** + * Sets the local content + * @private + * @param {String} name + * @param {Object} content_local + */ + _set_content_local: function(name, content_local) { + this._content_local[name] = content_local; + }, + + /** + * Sets the peer connection + * @private + * @param {Object} peer_connection + */ + _set_peer_connection: function(peer_connection) { + this._peer_connection = peer_connection; + }, + + /** + * Sets the ID + * @private + * @param {String|Number} id + */ + _set_id: function(id) { + this._id = id; + }, + + /** + * Sets the sent ID + * @private + * @param {String|Number} sent_id + */ + _set_sent_id: function(sent_id) { + this._sent_id[sent_id] = 1; + }, + + /** + * Sets the last received ID + * @private + * @param {String|Number} received_id + */ + _set_received_id: function(received_id) { + this._received_id[received_id] = 1; + }, + + /** + * Sets the registered stanza handlers + * @private + * @param {String} node + * @param {String} type + * @param {String|Number} id + * @param {Function} handler + */ + _set_registered_handlers: function(node, type, id, handler) { + if(!(node in this._registered_handlers)) this._registered_handlers[node] = {}; + if(!(type in this._registered_handlers[node])) this._registered_handlers[node][type] = {}; + + if(handler === null) { + if(id in this._registered_handlers[node][type]) + delete this._registered_handlers[node][type][id]; + } else { + if(typeof this._registered_handlers[node][type][id] != 'object') + this._registered_handlers[node][type][id] = []; + + this._registered_handlers[node][type][id].push(handler); + } + }, + + /** + * Sets the deferred stanza handlers + * @private + * @param {String} ns + * @param {Function|Object} handler + */ + _set_deferred_handlers: function(ns, handler) { + if(!(ns in this._deferred_handlers)) this._deferred_handlers[ns] = []; + + if(handler === null) + delete this._deferred_handlers[ns]; + else + this._deferred_handlers[ns].push(handler); + }, + + /** + * Sets the mute state + * @private + * @param {String} [name] + * @param {String} mute + */ + _set_mute: function(name, mute) { + if(!name || name == '*') { + this._mute = {}; + name = '*'; + } + + this._mute[name] = mute; + }, + + /** + * Sets the lock value + * @private + * @param {Boolean} lock + */ + _set_lock: function(lock) { + this._lock = lock; + }, + + /** + * Gets the media busy value + * @private + * @param {Boolean} busy + */ + _set_media_busy: function(busy) { + this._media_busy = busy; + }, + + /** + * Sets the session ID + * @private + * @param {String} sid + */ + _set_sid: function(sid) { + this._sid = sid; + }, + + /** + * Sets the session status + * @private + * @param {String} status + */ + _set_status: function(status) { + this._status = status; + }, + + /** + * Sets the session to value + * @private + * @param {String} to + */ + _set_to: function(to) { + this._to = to; + }, + + /** + * Sets the session connection value + * @private + * @param {JSJaCConnection} connection + */ + _set_connection: function(connection) { + this._connection = connection; + }, + + /** + * Sets the session initiator + * @private + * @param {String} initiator + */ + _set_initiator: function(initiator) { + this._initiator = initiator; + }, + + /** + * Sets the session responder + * @private + * @param {String} responder + */ + _set_responder: function(responder) { + this._responder = responder; + }, + + /** + * Sets the session creator + * @private + * @param {String} name + * @param {String} creator + */ + _set_creator: function(name, creator) { + if(!(creator in JSJAC_JINGLE_CREATORS)) creator = JSJAC_JINGLE_CREATOR_INITIATOR; + + this._creator[name] = creator; + }, + + /** + * Sets the session senders + * @private + * @param {String} name + * @param {String} senders + */ + _set_senders: function(name, senders) { + if(!(senders in JSJAC_JINGLE_SENDERS)) senders = JSJAC_JINGLE_SENDERS_BOTH.jingle; + + this._senders[name] = senders; + }, + + /** + * Sets the session media + * @private + * @param {String} media + */ + _set_media: function(media) { + this._media = media; + }, + + /** + * Sets the video source + * @private + */ + _set_video_source: function() { + this._video_source = video_source; + }, + + /** + * Sets the video resolution + * @private + * @param {String} resolution + */ + _set_resolution: function(resolution) { + this._resolution = resolution; + }, + + /** + * Sets the video bandwidth + * @private + * @param {Number} bandwidth + */ + _set_bandwidth: function(bandwidth) { + this._bandwidth = bandwidth; + }, + + /** + * Sets the video FPS + * @private + * @param {Number} fps + */ + _set_fps: function(fps) { + this._fps = fps; + }, + + /** + * Sets the source name + * @private + * @param {String} name + */ + _set_name: function(name) { + this._name[name] = 1; + }, + + /** + * Sets the STUN server address + * @private + * @param {String} stun_host + * @param {Object} stun_data + */ + _set_stun: function(stun_host, stun_data) { + this._stun.push( + this.utils.object_collect( + { 'host': stun_host }, + stun_data + ) + ); + }, + + /** + * Sets the TURN server address + * @private + * @param {String} turn_host + * @param {Object} turn_data + */ + _set_turn: function(turn_host, turn_data) { + this._turn.push( + this.utils.object_collect( + { 'host': turn_host }, + turn_data + ) + ); + }, + + /** + * Enables/disables SDP traces + * @public + * @param {Boolean} sdp_trace + */ + set_sdp_trace: function(sdp_trace) { + this._sdp_trace = sdp_trace; + }, + + /** + * Enables/disables network traces + * @public + * @param {Boolean} net_trace + */ + set_net_trace: function(net_trace) { + this._net_trace = net_trace; + }, + + /** + * Sets the debugging wrapper + * @public + * @param {JSJaCDebugger} debug + */ + set_debug: function(debug) { + this._debug = debug; + }, + } +); +/** + * @fileoverview JSJaC Jingle library - Single (one-to-one) call lib + * + * @url https://github.com/valeriansaliou/jsjac-jingle + * @depends https://github.com/sstrigler/JSJaC + * @author Valérian Saliou https://valeriansaliou.name/ + * @license Mozilla Public License v2.0 (MPL v2.0) + */ + + +/** @module jsjac-jingle/single */ +/** @exports JSJaCJingleSingle */ + + +/** + * Creates a new XMPP Jingle session. + * @class + * @classdesc Creates a new XMPP Jingle session. + * @augments __JSJaCJingleBase + * @requires nicolas-van/ring.js + * @requires sstrigler/JSJaC + * @requires jsjac-jingle/main + * @requires jsjac-jingle/base + * @see {@link http://xmpp.org/extensions/xep-0166.html|XEP-0166: Jingle} + * @see {@link http://ringjs.neoname.eu/|Ring.js} + * @see {@link http://stefan-strigler.de/jsjac-1.3.4/doc/|JSJaC Documentation} + * @param {Object} [args] - Jingle session arguments. + * @property {*} [args.*] - Herits of JSJaCJingle() baseclass prototype. + * @property {Function} [args.session_initiate_pending] - The initiate pending custom handler. + * @property {Function} [args.session_initiate_success] - The initiate success custom handler. + * @property {Function} [args.session_initiate_error] - The initiate error custom handler. + * @property {Function} [args.session_initiate_request] - The initiate request custom handler. + * @property {Function} [args.session_accept_pending] - The accept pending custom handler. + * @property {Function} [args.session_accept_success] - The accept success custom handler. + * @property {Function} [args.session_accept_error] - The accept error custom handler. + * @property {Function} [args.session_accept_request] - The accept request custom handler. + * @property {Function} [args.session_info_pending] - The info request custom handler. + * @property {Function} [args.session_info_success] - The info success custom handler. + * @property {Function} [args.session_info_error] - The info error custom handler. + * @property {Function} [args.session_info_request] - The info request custom handler. + * @property {Function} [args.session_terminate_pending] - The terminate pending custom handler. + * @property {Function} [args.session_terminate_success] - The terminate success custom handler. + * @property {Function} [args.session_terminate_error] - The terminate error custom handler. + * @property {Function} [args.session_terminate_request] - The terminate request custom handler. + * @property {DOM} [args.remote_view] - The path to the remote stream view element. + */ +var JSJaCJingleSingle = ring.create([__JSJaCJingleBase], + /** @lends JSJaCJingleSingle.prototype */ + { + /** + * Constructor + */ + constructor: function(args) { + this.$super(args); + + if(args && args.session_initiate_pending) + /** + * @member {Function} + * @default + * @private + */ + this._session_initiate_pending = args.session_initiate_pending; + + if(args && args.session_initiate_success) + /** + * @member {Function} + * @default + * @private + */ + this._session_initiate_success = args.session_initiate_success; + + if(args && args.session_initiate_error) + /** + * @member {Function} + * @default + * @private + */ + this._session_initiate_error = args.session_initiate_error; + + if(args && args.session_initiate_request) + /** + * @member {Function} + * @default + * @private + */ + this._session_initiate_request = args.session_initiate_request; + + if(args && args.session_accept_pending) + /** + * @member {Function} + * @default + * @private + */ + this._session_accept_pending = args.session_accept_pending; + + if(args && args.session_accept_success) + /** + * @member {Function} + * @default + * @private + */ + this._session_accept_success = args.session_accept_success; + + if(args && args.session_accept_error) + /** + * @member {Function} + * @default + * @private + */ + this._session_accept_error = args.session_accept_error; + + if(args && args.session_accept_request) + /** + * @member {Function} + * @default + * @private + */ + this._session_accept_request = args.session_accept_request; + + if(args && args.session_info_pending) + /** + * @member {Function} + * @default + * @private + */ + this._session_info_pending = args.session_info_pending; + + if(args && args.session_info_success) + /** + * @member {Function} + * @default + * @private + */ + this._session_info_success = args.session_info_success; + + if(args && args.session_info_error) + /** + * @member {Function} + * @default + * @private + */ + this._session_info_error = args.session_info_error; + + if(args && args.session_info_request) + /** + * @member {Function} + * @default + * @private + */ + this._session_info_request = args.session_info_request; + + if(args && args.session_terminate_pending) + /** + * @member {Function} + * @default + * @private + */ + this._session_terminate_pending = args.session_terminate_pending; + + if(args && args.session_terminate_success) + /** + * @member {Function} + * @default + * @private + */ + this._session_terminate_success = args.session_terminate_success; + + if(args && args.session_terminate_error) + /** + * @member {Function} + * @default + * @private + */ + this._session_terminate_error = args.session_terminate_error; + + if(args && args.session_terminate_request) + /** + * @member {Function} + * @default + * @private + */ + this._session_terminate_request = args.session_terminate_request; + + if(args && args.remote_view) + /** + * @member {Object} + * @default + * @private + */ + this._remote_view = [args.remote_view]; + + /** + * @member {Object} + * @default + * @private + */ + this._remote_stream = {}; + + /** + * @member {Object} + * @default + * @private + */ + this._content_remote = {}; + + /** + * @member {Object} + * @default + * @private + */ + this._payloads_remote = {}; + + /** + * @member {Object} + * @default + * @private + */ + this._group_remote = {}; + + /** + * @member {Object} + * @default + * @private + */ + this._candidates_remote = {}; + + /** + * @member {Object} + * @default + * @private + */ + this._candidates_queue_remote = {}; + + /** + * @constant + * @member {String} + * @default + * @private + */ + this._status = JSJAC_JINGLE_STATUS_INACTIVE; + + /** + * @constant + * @member {String} + * @default + * @private + */ + this._reason = JSJAC_JINGLE_REASON_CANCEL; + + /** + * @constant + * @member {String} + * @default + * @private + */ + this._namespace = NS_JINGLE; + }, + + + /** + * Initiates a new Jingle session. + * @public + * @fires JSJaCJingleSingle#get_session_initiate_pending + */ + initiate: function() { + this.get_debug().log('[JSJaCJingle:single] initiate', 4); + + try { + // Locked? + if(this.get_lock()) { + this.get_debug().log('[JSJaCJingle:single] initiate > Cannot initiate, resource locked. Please open another session or check WebRTC support.', 0); + return; + } + + // Defer? + var _this = this; + + if(JSJaCJingle._defer(function() { _this.initiate(); })) { + this.get_debug().log('[JSJaCJingle:single] initiate > Deferred (waiting for the library components to be initiated).', 0); + return; + } + + // Slot unavailable? + if(this.get_status() !== JSJAC_JINGLE_STATUS_INACTIVE) { + this.get_debug().log('[JSJaCJingle:single] initiate > Cannot initiate, resource not inactive (status: ' + this.get_status() + ').', 0); + return; + } + + this.get_debug().log('[JSJaCJingle:single] initiate > New Jingle Single session with media: ' + this.get_media(), 2); + + // Common vars + var i, cur_name; + + // Trigger init pending custom callback + /* @function */ + (this.get_session_initiate_pending())(this); + + // Change session status + this._set_status(JSJAC_JINGLE_STATUS_INITIATING); + + // Set session values + this._set_sid(this.utils.generate_sid()); + this._set_initiator(this.utils.connection_jid()); + this._set_responder(this.get_to()); + + for(i in this.get_media_all()) { + cur_name = this.utils.name_generate( + this.get_media_all()[i] + ); + + this._set_name(cur_name); + + this._set_senders( + cur_name, + JSJAC_JINGLE_SENDERS_BOTH.jingle + ); + + this._set_creator( + cur_name, + JSJAC_JINGLE_CREATOR_INITIATOR + ); + } + + // Register session to common router + JSJaCJingle._add(JSJAC_JINGLE_SESSION_SINGLE, this.get_sid(), this); + + // Initialize WebRTC + this._peer_get_user_media(function() { + _this._peer_connection_create( + function() { + _this.get_debug().log('[JSJaCJingle:single] initiate > Ready to begin Jingle negotiation.', 2); + + _this.send(JSJAC_JINGLE_IQ_TYPE_SET, { action: JSJAC_JINGLE_ACTION_SESSION_INITIATE }); + } + ); + }); + } catch(e) { + this.get_debug().log('[JSJaCJingle:single] initiate > ' + e, 1); + } + }, + + /** + * Accepts the Jingle session. + * @public + * @fires JSJaCJingleSingle#get_session_accept_pending + */ + accept: function() { + this.get_debug().log('[JSJaCJingle:single] accept', 4); + + try { + // Locked? + if(this.get_lock()) { + this.get_debug().log('[JSJaCJingle:single] accept > Cannot accept, resource locked. Please open another session or check WebRTC support.', 0); + return; + } + + // Defer? + var _this = this; + + if(JSJaCJingle._defer(function() { _this.accept(); })) { + this.get_debug().log('[JSJaCJingle:single] accept > Deferred (waiting for the library components to be initiated).', 0); + return; + } + + // Slot unavailable? + if(this.get_status() !== JSJAC_JINGLE_STATUS_INITIATED) { + this.get_debug().log('[JSJaCJingle:single] accept > Cannot accept, resource not initiated (status: ' + this.get_status() + ').', 0); + return; + } + + this.get_debug().log('[JSJaCJingle:single] accept > New Jingle session with media: ' + this.get_media(), 2); + + // Trigger accept pending custom callback + /* @function */ + (this.get_session_accept_pending())(this); + + // Change session status + this._set_status(JSJAC_JINGLE_STATUS_ACCEPTING); + + // Initialize WebRTC + this._peer_get_user_media(function() { + _this._peer_connection_create( + function() { + _this.get_debug().log('[JSJaCJingle:single] accept > Ready to complete Jingle negotiation.', 2); + + // Process accept actions + _this.send(JSJAC_JINGLE_IQ_TYPE_SET, { action: JSJAC_JINGLE_ACTION_SESSION_ACCEPT }); + } + ); + }); + } catch(e) { + this.get_debug().log('[JSJaCJingle:single] accept > ' + e, 1); + } + }, + + /** + * Sends a Jingle session info. + * @public + * @param {String} name + * @param {Object} [args] + */ + info: function(name, args) { + this.get_debug().log('[JSJaCJingle:single] info', 4); + + try { + // Locked? + if(this.get_lock()) { + this.get_debug().log('[JSJaCJingle:single] info > Cannot accept, resource locked. Please open another session or check WebRTC support.', 0); + return; + } + + // Defer? + var _this = this; + + if(JSJaCJingle._defer(function() { _this.info(name, args); })) { + this.get_debug().log('[JSJaCJingle:single] info > Deferred (waiting for the library components to be initiated).', 0); + return; + } + + // Slot unavailable? + if(!(this.get_status() === JSJAC_JINGLE_STATUS_INITIATED || + this.get_status() === JSJAC_JINGLE_STATUS_ACCEPTING || + this.get_status() === JSJAC_JINGLE_STATUS_ACCEPTED)) { + this.get_debug().log('[JSJaCJingle:single] info > Cannot send info, resource not active (status: ' + this.get_status() + ').', 0); + return; + } + + // Trigger info pending custom callback + /* @function */ + (this.get_session_info_pending())(this); + + if(typeof args !== 'object') args = {}; + + // Build final args parameter + args.action = JSJAC_JINGLE_ACTION_SESSION_INFO; + if(name) args.info = name; + + this.send(JSJAC_JINGLE_IQ_TYPE_SET, args); + } catch(e) { + this.get_debug().log('[JSJaCJingle:single] info > ' + e, 1); + } + }, + + /** + * Terminates the Jingle session. + * @public + * @fires JSJaCJingleSingle#get_session_terminate_pending + * @param {String} reason + */ + terminate: function(reason) { + this.get_debug().log('[JSJaCJingle:single] terminate', 4); + + try { + // Locked? + if(this.get_lock()) { + this.get_debug().log('[JSJaCJingle:single] terminate > Cannot terminate, resource locked. Please open another session or check WebRTC support.', 0); + return; + } + + // Defer? + var _this = this; + + if(JSJaCJingle._defer(function() { _this.terminate(reason); })) { + this.get_debug().log('[JSJaCJingle:single] terminate > Deferred (waiting for the library components to be initiated).', 0); + return; + } + + // Slot unavailable? + if(this.get_status() === JSJAC_JINGLE_STATUS_TERMINATED) { + this.get_debug().log('[JSJaCJingle:single] terminate > Cannot terminate, resource already terminated (status: ' + this.get_status() + ').', 0); + return; + } + + // Change session status + this._set_status(JSJAC_JINGLE_STATUS_TERMINATING); + + // Trigger terminate pending custom callback + /* @function */ + (this.get_session_terminate_pending())(this); + + // Process terminate actions + this.send(JSJAC_JINGLE_IQ_TYPE_SET, { action: JSJAC_JINGLE_ACTION_SESSION_TERMINATE, reason: reason }); + } catch(e) { + this.get_debug().log('[JSJaCJingle:single] terminate > ' + e, 1); + } + }, + + /** + * Aborts the Jingle session. + * @public + * @param {Boolean} [set_lock] + */ + abort: function(set_lock) { + this.get_debug().log('[JSJaCJingle:single] abort', 4); + + try { + // Change session status + this._set_status(JSJAC_JINGLE_STATUS_TERMINATED); + + // Stop WebRTC + this._peer_stop(); + + // Lock session? (cannot be used later) + if(set_lock === true) this._set_lock(true); + } catch(e) { + this.get_debug().log('[JSJaCJingle:single] abort > ' + e, 1); + } + }, + + /** + * Sends a given Jingle stanza packet + * @public + * @param {String} type + * @param {Object} [args] + * @returns {Boolean} Success + */ + send: function(type, args) { + this.get_debug().log('[JSJaCJingle:single] send', 4); + + try { + // Locked? + if(this.get_lock()) { + this.get_debug().log('[JSJaCJingle:single] send > Cannot send, resource locked. Please open another session or check WebRTC support.', 0); + return false; + } + + // Defer? + var _this = this; + + if(JSJaCJingle._defer(function() { _this.send(type, args); })) { + this.get_debug().log('[JSJaCJingle:single] send > Deferred (waiting for the library components to be initiated).', 0); + return false; + } + + // Assert + if(typeof args !== 'object') args = {}; + + // Build stanza + var stanza = new JSJaCIQ(); + stanza.setTo(this.get_to()); + + if(type) stanza.setType(type); + + if(!args.id) args.id = this.get_id_new(); + stanza.setID(args.id); + + if(type == JSJAC_JINGLE_IQ_TYPE_SET) { + if(!(args.action && args.action in JSJAC_JINGLE_ACTIONS)) { + this.get_debug().log('[JSJaCJingle:single] send > Stanza action unknown: ' + (args.action || 'undefined'), 1); + return false; + } + + // Submit to registered handler + switch(args.action) { + case JSJAC_JINGLE_ACTION_CONTENT_ACCEPT: + this._send_content_accept(stanza); break; + + case JSJAC_JINGLE_ACTION_CONTENT_ADD: + this._send_content_add(stanza); break; + + case JSJAC_JINGLE_ACTION_CONTENT_MODIFY: + this._send_content_modify(stanza); break; + + case JSJAC_JINGLE_ACTION_CONTENT_REJECT: + this._send_content_reject(stanza); break; + + case JSJAC_JINGLE_ACTION_CONTENT_REMOVE: + this._send_content_remove(stanza); break; + + case JSJAC_JINGLE_ACTION_DESCRIPTION_INFO: + this._send_description_info(stanza); break; + + case JSJAC_JINGLE_ACTION_SECURITY_INFO: + this._send_security_info(stanza); break; + + case JSJAC_JINGLE_ACTION_SESSION_ACCEPT: + this._send_session_accept(stanza, args); break; + + case JSJAC_JINGLE_ACTION_SESSION_INFO: + this._send_session_info(stanza, args); break; + + case JSJAC_JINGLE_ACTION_SESSION_INITIATE: + this._send_session_initiate(stanza, args); break; + + case JSJAC_JINGLE_ACTION_SESSION_TERMINATE: + this._send_session_terminate(stanza, args); break; + + case JSJAC_JINGLE_ACTION_TRANSPORT_ACCEPT: + this._send_transport_accept(stanza); break; + + case JSJAC_JINGLE_ACTION_TRANSPORT_INFO: + this._send_transport_info(stanza, args); break; + + case JSJAC_JINGLE_ACTION_TRANSPORT_REJECT: + this._send_transport_reject(stanza); break; + + case JSJAC_JINGLE_ACTION_TRANSPORT_REPLACE: + this._send_transport_replace(stanza); break; + + default: + this.get_debug().log('[JSJaCJingle:single] send > Unexpected error.', 1); + + return false; + } + } else if(type != JSJAC_JINGLE_IQ_TYPE_RESULT) { + this.get_debug().log('[JSJaCJingle:single] send > Stanza type must either be set or result.', 1); + + return false; + } + + this._set_sent_id(args.id); + + this.get_connection().send(stanza); + + if(this.get_net_trace()) this.get_debug().log('[JSJaCJingle:single] send > Outgoing packet sent' + '\n\n' + stanza.xml()); + + return true; + } catch(e) { + this.get_debug().log('[JSJaCJingle:single] send > ' + e, 1); + } + + return false; + }, + + /** + * Handles a given Jingle stanza response + * @private + * @fires JSJaCJingleSingle#_handle_content_accept + * @fires JSJaCJingleSingle#_handle_content_add + * @fires JSJaCJingleSingle#_handle_content_modify + * @fires JSJaCJingleSingle#_handle_content_reject + * @fires JSJaCJingleSingle#_handle_content_remove + * @fires JSJaCJingleSingle#_handle_description_info + * @fires JSJaCJingleSingle#_handle_security_info + * @fires JSJaCJingleSingle#_handle_session_accept + * @fires JSJaCJingleSingle#_handle_session_info + * @fires JSJaCJingleSingle#_handle_session_initiate + * @fires JSJaCJingleSingle#_handle_session_terminate + * @fires JSJaCJingleSingle#_handle_transport_accept + * @fires JSJaCJingleSingle#_handle_transport_info + * @fires JSJaCJingleSingle#_handle_transport_reject + * @fires JSJaCJingleSingle#_handle_transport_replace + * @param {JSJaCPacket} stanza + */ + handle: function(stanza) { + this.get_debug().log('[JSJaCJingle:single] handle', 4); + + try { + if(this.get_net_trace()) this.get_debug().log('[JSJaCJingle:single] handle > Incoming packet received' + '\n\n' + stanza.xml()); + + // Locked? + if(this.get_lock()) { + this.get_debug().log('[JSJaCJingle:single] handle > Cannot handle, resource locked. Please open another session or check WebRTC support.', 0); + return; + } + + // Defer? + var _this = this; + + if(JSJaCJingle._defer(function() { _this.handle(stanza); })) { + this.get_debug().log('[JSJaCJingle:single] handle > Deferred (waiting for the library components to be initiated).', 0); + return; + } + + var id = stanza.getID(); + var type = stanza.getType(); + + if(id && type == JSJAC_JINGLE_IQ_TYPE_RESULT) this._set_received_id(id); + + // Submit to custom handler + var i, handlers = this.get_registered_handlers(JSJAC_JINGLE_STANZA_IQ, type, id); + + if(typeof handlers == 'object' && handlers.length) { + this.get_debug().log('[JSJaCJingle:single] handle > Submitted to custom registered handlers.', 2); + + for(i in handlers) { + /* @function */ + handlers[i](stanza); + } + + this.unregister_handler(JSJAC_JINGLE_STANZA_IQ, type, id); + + return; + } + + var jingle = this.utils.stanza_jingle(stanza); + + // Don't handle non-Jingle stanzas there... + if(!jingle) return; + + var action = this.utils.stanza_get_attribute(jingle, 'action'); + + // Don't handle action-less Jingle stanzas there... + if(!action) return; + + // Submit to registered handler + switch(action) { + case JSJAC_JINGLE_ACTION_CONTENT_ACCEPT: + this._handle_content_accept(stanza); break; + + case JSJAC_JINGLE_ACTION_CONTENT_ADD: + this._handle_content_add(stanza); break; + + case JSJAC_JINGLE_ACTION_CONTENT_MODIFY: + this._handle_content_modify(stanza); break; + + case JSJAC_JINGLE_ACTION_CONTENT_REJECT: + this._handle_content_reject(stanza); break; + + case JSJAC_JINGLE_ACTION_CONTENT_REMOVE: + this._handle_content_remove(stanza); break; + + case JSJAC_JINGLE_ACTION_DESCRIPTION_INFO: + this._handle_description_info(stanza); break; + + case JSJAC_JINGLE_ACTION_SECURITY_INFO: + this._handle_security_info(stanza); break; + + case JSJAC_JINGLE_ACTION_SESSION_ACCEPT: + this._handle_session_accept(stanza); break; + + case JSJAC_JINGLE_ACTION_SESSION_INFO: + this._handle_session_info(stanza); break; + + case JSJAC_JINGLE_ACTION_SESSION_INITIATE: + this._handle_session_initiate(stanza); break; + + case JSJAC_JINGLE_ACTION_SESSION_TERMINATE: + this._handle_session_terminate(stanza); break; + + case JSJAC_JINGLE_ACTION_TRANSPORT_ACCEPT: + this._handle_transport_accept(stanza); break; + + case JSJAC_JINGLE_ACTION_TRANSPORT_INFO: + this._handle_transport_info(stanza); break; + + case JSJAC_JINGLE_ACTION_TRANSPORT_REJECT: + this._handle_transport_reject(stanza); break; + + case JSJAC_JINGLE_ACTION_TRANSPORT_REPLACE: + this._handle_transport_replace(stanza); break; + } + } catch(e) { + this.get_debug().log('[JSJaCJingle:single] handle > ' + e, 1); + } + }, + + /** + * Mutes a Jingle session (local) + * @public + * @param {String} name + */ + mute: function(name) { + this.get_debug().log('[JSJaCJingle:single] mute', 4); + + try { + // Locked? + if(this.get_lock()) { + this.get_debug().log('[JSJaCJingle:single] mute > Cannot mute, resource locked. Please open another session or check WebRTC support.', 0); + return; + } + + // Defer? + var _this = this; + + if(JSJaCJingle._defer(function() { _this.mute(name); })) { + this.get_debug().log('[JSJaCJingle:single] mute > Deferred (waiting for the library components to be initiated).', 0); + return; + } + + // Already muted? + if(this.get_mute(name) === true) { + this.get_debug().log('[JSJaCJingle:single] mute > Resource already muted.', 0); + return; + } + + this._peer_sound(false); + this._set_mute(name, true); + + this.send(JSJAC_JINGLE_IQ_TYPE_SET, { action: JSJAC_JINGLE_ACTION_SESSION_INFO, info: JSJAC_JINGLE_SESSION_INFO_MUTE, name: name }); + } catch(e) { + this.get_debug().log('[JSJaCJingle:single] mute > ' + e, 1); + } + }, + + /** + * Unmutes a Jingle session (local) + * @public + * @param {String} name + */ + unmute: function(name) { + this.get_debug().log('[JSJaCJingle:single] unmute', 4); + + try { + // Locked? + if(this.get_lock()) { + this.get_debug().log('[JSJaCJingle:single] unmute > Cannot unmute, resource locked. Please open another session or check WebRTC support.', 0); + return; + } + + // Defer? + var _this = this; + + if(JSJaCJingle._defer(function() { _this.unmute(name); })) { + this.get_debug().log('[JSJaCJingle:single] unmute > Deferred (waiting for the library components to be initiated).', 0); + return; + } + + // Already unmute? + if(this.get_mute(name) === false) { + this.get_debug().log('[JSJaCJingle:single] unmute > Resource already unmuted.', 0); + return; + } + + this._peer_sound(true); + this._set_mute(name, false); + + this.send(JSJAC_JINGLE_IQ_TYPE_SET, { action: JSJAC_JINGLE_ACTION_SESSION_INFO, info: JSJAC_JINGLE_SESSION_INFO_UNMUTE, name: name }); + } catch(e) { + this.get_debug().log('[JSJaCJingle:single] unmute > ' + e, 1); + } + }, + + /** + * Toggles media type in a Jingle session + * @todo Code media() (Single version) + * @public + * @param {String} [media] + */ + media: function(media) { + /* DEV: don't expect this to work as of now! */ + /* MEDIA() - SINGLE VERSION */ + + this.get_debug().log('[JSJaCJingle:single] media', 4); + + try { + // Locked? + if(this.get_lock()) { + this.get_debug().log('[JSJaCJingle:single] media > Cannot change media, resource locked. Please open another session or check WebRTC support.', 0); + return; + } + + // Defer? + var _this = this; + + if(JSJaCJingle._defer(function() { _this.media(media); })) { + this.get_debug().log('[JSJaCJingle:single] media > Deferred (waiting for the library components to be initiated).', 0); + return; + } + + // Toggle media? + if(!media) + media = (this.get_media() == JSJAC_JINGLE_MEDIA_VIDEO) ? JSJAC_JINGLE_MEDIA_AUDIO : JSJAC_JINGLE_MEDIA_VIDEO; + + // Media unknown? + if(!(media in JSJAC_JINGLE_MEDIAS)) { + this.get_debug().log('[JSJaCJingle:single] media > No media provided or media unsupported (media: ' + media + ').', 0); + return; + } + + // Already using provided media? + if(this.get_media() == media) { + this.get_debug().log('[JSJaCJingle:single] media > Resource already using this media (media: ' + media + ').', 0); + return; + } + + // Switch locked for now? (another one is being processed) + if(this.get_media_busy()) { + this.get_debug().log('[JSJaCJingle:single] media > Resource already busy switching media (busy: ' + this.get_media() + ', media: ' + media + ').', 0); + return; + } + + this.get_debug().log('[JSJaCJingle:single] media > Changing media to: ' + media + '...', 2); + + // Store new media + this._set_media(media); + this._set_media_busy(true); + + // Toggle video mode (add/remove) + if(media == JSJAC_JINGLE_MEDIA_VIDEO) { + /* @todo the flow is something like that... */ + /*this._peer_get_user_media(function() { + this._peer_connection_create( + function() { + this.get_debug().log('[JSJaCJingle:muji] media > Ready to change media (to: ' + media + ').', 2); + + // 'content-add' >> video + // @todo restart video stream configuration + + // WARNING: only change get user media, DO NOT TOUCH THE STREAM THING (don't stop active stream as it's flowing!!) + + this.send(JSJAC_JINGLE_IQ_TYPE_SET, { action: JSJAC_JINGLE_ACTION_CONTENT_ADD, name: JSJAC_JINGLE_MEDIA_VIDEO }); + } + ) + });*/ + } else { + /* @todo the flow is something like that... */ + /*this._peer_get_user_media(function() { + this._peer_connection_create( + function() { + this.get_debug().log('[JSJaCJingle:muji] media > Ready to change media (to: ' + media + ').', 2); + + // 'content-remove' >> video + // @todo remove video stream configuration + + // WARNING: only change get user media, DO NOT TOUCH THE STREAM THING (don't stop active stream as it's flowing!!) + // here, only stop the video stream, do not touch the audio stream + + this.send(JSJAC_JINGLE_IQ_TYPE_SET, { action: JSJAC_JINGLE_ACTION_CONTENT_REMOVE, name: JSJAC_JINGLE_MEDIA_VIDEO }); + } + ) + });*/ + } + } catch(e) { + this.get_debug().log('[JSJaCJingle:single] media > ' + e, 1); + } + }, + + + /** + * JSJSAC JINGLE SENDERS + */ + + /** + * Sends the Jingle content accept + * @private + * @param {JSJaCPacket} stanza + */ + _send_content_accept: function(stanza) { + this.get_debug().log('[JSJaCJingle:single] _send_content_accept', 4); + + try { + /* @todo remove from remote 'content-add' queue */ + /* @todo reprocess content_local/content_remote */ + + // Not implemented for now + this.get_debug().log('[JSJaCJingle:single] _send_content_accept > Feature not implemented!', 0); + } catch(e) { + this.get_debug().log('[JSJaCJingle:single] _send_content_accept > ' + e, 1); + } + }, + + /** + * Sends the Jingle content add + * @private + * @param {JSJaCPacket} stanza + */ + _send_content_add: function(stanza) { + this.get_debug().log('[JSJaCJingle:single] _send_content_add', 4); + + try { + /* @todo push to local 'content-add' queue */ + + // Not implemented for now + this.get_debug().log('[JSJaCJingle:single] _send_content_add > Feature not implemented!', 0); + } catch(e) { + this.get_debug().log('[JSJaCJingle:single] _send_content_add > ' + e, 1); + } + }, + + /** + * Sends the Jingle content modify + * @private + * @param {JSJaCPacket} stanza + */ + _send_content_modify: function(stanza) { + this.get_debug().log('[JSJaCJingle:single] _send_content_modify', 4); + + try { + /* @todo push to local 'content-modify' queue */ + + // Not implemented for now + this.get_debug().log('[JSJaCJingle:single] _send_content_modify > Feature not implemented!', 0); + } catch(e) { + this.get_debug().log('[JSJaCJingle:single] _send_content_modify > ' + e, 1); + } + }, + + /** + * Sends the Jingle content reject + * @private + * @param {JSJaCPacket} stanza + */ + _send_content_reject: function(stanza) { + this.get_debug().log('[JSJaCJingle:single] _send_content_reject', 4); + + try { + /* @todo remove from remote 'content-add' queue */ + + // Not implemented for now + this.get_debug().log('[JSJaCJingle:single] _send_content_reject > Feature not implemented!', 0); + } catch(e) { + this.get_debug().log('[JSJaCJingle:single] _send_content_reject > ' + e, 1); + } + }, + + /** + * Sends the Jingle content remove + * @private + * @param {JSJaCPacket} stanza + */ + _send_content_remove: function(stanza) { + this.get_debug().log('[JSJaCJingle:single] _send_content_remove', 4); + + try { + /* @todo add to local 'content-remove' queue */ + + // Not implemented for now + this.get_debug().log('[JSJaCJingle:single] _send_content_remove > Feature not implemented!', 0); + } catch(e) { + this.get_debug().log('[JSJaCJingle:single] _send_content_remove > ' + e, 1); + } + }, + + /** + * Sends the Jingle description info + * @private + * @param {JSJaCPacket} stanza + */ + _send_description_info: function(stanza) { + this.get_debug().log('[JSJaCJingle:single] _send_description_info', 4); + + try { + // Not implemented for now + this.get_debug().log('[JSJaCJingle:single] _send_description_info > Feature not implemented!', 0); + } catch(e) { + this.get_debug().log('[JSJaCJingle:single] _send_description_info > ' + e, 1); + } + }, + + /** + * Sends the Jingle security info + * @private + * @param {JSJaCPacket} stanza + */ + _send_security_info: function(stanza) { + this.get_debug().log('[JSJaCJingle:single] _send_security_info', 4); + + try { + // Not implemented for now + this.get_debug().log('[JSJaCJingle:single] _send_security_info > Feature not implemented!', 0); + } catch(e) { + this.get_debug().log('[JSJaCJingle:single] _send_security_info > ' + e, 1); + } + }, + + /** + * Sends the Jingle session accept + * @private + * @fires JSJaCJingleSingle#_handle_session_accept_success + * @fires JSJaCJingleSingle#_handle_session_accept_error + * @fires JSJaCJingleSingle#get_session_accept_success + * @fires JSJaCJingleSingle#get_session_accept_error + * @param {JSJaCPacket} stanza + * @param {Object} args + */ + _send_session_accept: function(stanza, args) { + this.get_debug().log('[JSJaCJingle:single] _send_session_accept', 4); + + try { + if(this.get_status() !== JSJAC_JINGLE_STATUS_ACCEPTING) { + this.get_debug().log('[JSJaCJingle:single] _send_session_accept > Cannot send accept stanza, resource not accepting (status: ' + this.get_status() + ').', 0); + this._send_error(stanza, JSJAC_JINGLE_ERROR_OUT_OF_ORDER); + return; + } + + if(!args) { + this.get_debug().log('[JSJaCJingle:single] _send_session_accept > Arguments not provided.', 1); + return; + } + + // Build Jingle stanza + var jingle = this.utils.stanza_generate_jingle(stanza, { + 'action' : JSJAC_JINGLE_ACTION_SESSION_ACCEPT, + 'responder' : this.get_responder() + }); + + this.utils.stanza_generate_content_local(stanza, jingle, true); + this.utils.stanza_generate_group_local(stanza, jingle); + + // Schedule success + var _this = this; + + this.register_handler(JSJAC_JINGLE_STANZA_IQ, JSJAC_JINGLE_IQ_TYPE_RESULT, args.id, function(stanza) { + /* @function */ + (_this.get_session_accept_success())(_this, stanza); + _this._handle_session_accept_success(stanza); + }); + + // Schedule error timeout + this.utils.stanza_timeout(JSJAC_JINGLE_STANZA_IQ, JSJAC_JINGLE_IQ_TYPE_RESULT, args.id, { + /* @function */ + external: this.get_session_accept_error().bind(this), + internal: this._handle_session_accept_error.bind(this) + }); + + this.get_debug().log('[JSJaCJingle:single] _send_session_accept > Sent.', 4); + } catch(e) { + this.get_debug().log('[JSJaCJingle:single] _send_session_accept > ' + e, 1); + } + }, + + /** + * Sends the Jingle session info + * @private + * @fires JSJaCJingleSingle#_handle_session_info_success + * @fires JSJaCJingleSingle#_handle_session_info_error + * @param {JSJaCPacket} stanza + * @param {Object} args + */ + _send_session_info: function(stanza, args) { + this.get_debug().log('[JSJaCJingle:single] _send_session_info', 4); + + try { + if(!args) { + this.get_debug().log('[JSJaCJingle:single] _send_session_info > Arguments not provided.', 1); + return; + } + + // Filter info + args.info = args.info || JSJAC_JINGLE_SESSION_INFO_ACTIVE; + + // Build Jingle stanza + var jingle = this.utils.stanza_generate_jingle(stanza, { + 'action' : JSJAC_JINGLE_ACTION_SESSION_INFO, + 'initiator' : this.get_initiator() + }); + + this.utils.stanza_generate_session_info(stanza, jingle, args); + + // Schedule success + var _this = this; + + this.register_handler(JSJAC_JINGLE_STANZA_IQ, JSJAC_JINGLE_IQ_TYPE_RESULT, args.id, function(stanza) { + /* @function */ + (_this.get_session_info_success())(this, stanza); + _this._handle_session_info_success(stanza); + }); + + // Schedule error timeout + this.utils.stanza_timeout(JSJAC_JINGLE_STANZA_IQ, JSJAC_JINGLE_IQ_TYPE_RESULT, args.id, { + /* @function */ + external: this.get_session_info_error().bind(this), + internal: this._handle_session_info_error.bind(this) + }); + + this.get_debug().log('[JSJaCJingle:single] _send_session_info > Sent (name: ' + args.info + ').', 2); + } catch(e) { + this.get_debug().log('[JSJaCJingle:single] _send_session_info > ' + e, 1); + } + }, + + /** + * Sends the Jingle session initiate + * @private + * @fires JSJaCJingleSingle#_handle_initiate_info_success + * @fires JSJaCJingleSingle#_handle_initiate_info_error + * @fires JSJaCJingleSingle#get_session_initiate_success + * @fires JSJaCJingleSingle#get_session_initiate_error + * @param {JSJaCPacket} stanza + * @param {Object} args + */ + _send_session_initiate: function(stanza, args) { + this.get_debug().log('[JSJaCJingle:single] _send_session_initiate', 4); + + try { + if(this.get_status() !== JSJAC_JINGLE_STATUS_INITIATING) { + this.get_debug().log('[JSJaCJingle:single] _send_session_initiate > Cannot send initiate stanza, resource not initiating (status: ' + this.get_status() + ').', 0); + return; + } + + if(!args) { + this.get_debug().log('[JSJaCJingle:single] _send_session_initiate > Arguments not provided.', 1); + return; + } + + // Build Jingle stanza + var jingle = this.utils.stanza_generate_jingle(stanza, { + 'action' : JSJAC_JINGLE_ACTION_SESSION_INITIATE, + 'initiator' : this.get_initiator() + }); + + this.utils.stanza_generate_content_local(stanza, jingle, true); + this.utils.stanza_generate_group_local(stanza, jingle); + + // Schedule success + var _this = this; + + this.register_handler(JSJAC_JINGLE_STANZA_IQ, JSJAC_JINGLE_IQ_TYPE_RESULT, args.id, function(stanza) { + /* @function */ + (_this.get_session_initiate_success())(_this, stanza); + _this._handle_session_initiate_success(stanza); + }); + + // Schedule error timeout + this.utils.stanza_timeout(JSJAC_JINGLE_STANZA_IQ, JSJAC_JINGLE_IQ_TYPE_RESULT, args.id, { + /* @function */ + external: this.get_session_initiate_error().bind(this), + internal: this._handle_session_initiate_error.bind(this) + }); + + this.get_debug().log('[JSJaCJingle:single] _send_session_initiate > Sent.', 2); + } catch(e) { + this.get_debug().log('[JSJaCJingle:single] _send_session_initiate > ' + e, 1); + } + }, + + /** + * Sends the Jingle session terminate + * @private + * @fires JSJaCJingleSingle#_handle_session_terminate_success + * @fires JSJaCJingleSingle#_handle_session_terminate_error + * @fires JSJaCJingleSingle#get_session_terminate_success + * @fires JSJaCJingleSingle#get_session_terminate_error + * @param {JSJaCPacket} stanza + * @param {Object} args + */ + _send_session_terminate: function(stanza, args) { + this.get_debug().log('[JSJaCJingle:single] _send_session_terminate', 4); + + try { + if(this.get_status() !== JSJAC_JINGLE_STATUS_TERMINATING) { + this.get_debug().log('[JSJaCJingle:single] _send_session_terminate > Cannot send terminate stanza, resource not terminating (status: ' + this.get_status() + ').', 0); + return; + } + + if(!args) { + this.get_debug().log('[JSJaCJingle:single] _send_session_terminate > Arguments not provided.', 1); + return; + } + + // Filter reason + args.reason = args.reason || JSJAC_JINGLE_REASON_SUCCESS; + + // Store terminate reason + this._set_reason(args.reason); + + // Build terminate stanza + var jingle = this.utils.stanza_generate_jingle(stanza, { + 'action': JSJAC_JINGLE_ACTION_SESSION_TERMINATE + }); + + var jingle_reason = jingle.appendChild(stanza.buildNode('reason', {'xmlns': this.get_namespace()})); + jingle_reason.appendChild(stanza.buildNode(args.reason, {'xmlns': this.get_namespace()})); + + // Schedule success + var _this = this; + + this.register_handler(JSJAC_JINGLE_STANZA_IQ, JSJAC_JINGLE_IQ_TYPE_RESULT, args.id, function(stanza) { + /* @function */ + (_this.get_session_terminate_success())(_this, stanza); + _this._handle_session_terminate_success(stanza); + }); + + // Schedule error timeout + this.utils.stanza_timeout(JSJAC_JINGLE_STANZA_IQ, JSJAC_JINGLE_IQ_TYPE_RESULT, args.id, { + /* @function */ + external: this.get_session_terminate_error().bind(this), + internal: this._handle_session_terminate_error.bind(this) + }); + + this.get_debug().log('[JSJaCJingle:single] _send_session_terminate > Sent (reason: ' + args.reason + ').', 2); + } catch(e) { + this.get_debug().log('[JSJaCJingle:single] _send_session_terminate > ' + e, 1); + } + }, + + /** + * Sends the Jingle transport accept + * @private + * @param {JSJaCPacket} stanza + */ + _send_transport_accept: function(stanza) { + this.get_debug().log('[JSJaCJingle:single] _send_transport_accept', 4); + + try { + // Not implemented for now + this.get_debug().log('[JSJaCJingle:single] _send_transport_accept > Feature not implemented!', 0); + } catch(e) { + this.get_debug().log('[JSJaCJingle:single] _send_transport_accept > ' + e, 1); + } + }, + + /** + * Sends the Jingle transport info + * @private + * @fires JSJaCJingleSingle#_handle_transport_info_success + * @fires JSJaCJingleSingle#_handle_transport_info_error + * @param {JSJaCPacket} stanza + * @param {Object} args + */ + _send_transport_info: function(stanza, args) { + this.get_debug().log('[JSJaCJingle:single] _send_transport_info', 4); + + try { + if(this.get_status() !== JSJAC_JINGLE_STATUS_INITIATED && + this.get_status() !== JSJAC_JINGLE_STATUS_ACCEPTING && + this.get_status() !== JSJAC_JINGLE_STATUS_ACCEPTED) { + this.get_debug().log('[JSJaCJingle:single] _send_transport_info > Cannot send transport info, resource not initiated, nor accepting, nor accepted (status: ' + this.get_status() + ').', 0); + return; + } + + if(!args) { + this.get_debug().log('[JSJaCJingle:single] _send_transport_info > Arguments not provided.', 1); + return; + } + + if(this.utils.object_length(this.get_candidates_queue_local()) === 0) { + this.get_debug().log('[JSJaCJingle:single] _send_transport_info > No local candidate in queue.', 1); + return; + } + + // Build Jingle stanza + var jingle = this.utils.stanza_generate_jingle(stanza, { + 'action' : JSJAC_JINGLE_ACTION_TRANSPORT_INFO, + 'initiator' : this.get_initiator() + }); + + // Build queue content + var cur_name; + var content_queue_local = {}; + + for(cur_name in this.get_name()) { + content_queue_local[cur_name] = this.utils.generate_content( + this.get_creator(cur_name), + cur_name, + this.get_senders(cur_name), + this.get_payloads_local(cur_name), + this.get_candidates_queue_local(cur_name) + ); + } + + this.utils.stanza_generate_content_local(stanza, jingle, true, content_queue_local); + this.utils.stanza_generate_group_local(stanza, jingle); + + // Schedule success + var _this = this; + + this.register_handler(JSJAC_JINGLE_STANZA_IQ, JSJAC_JINGLE_IQ_TYPE_RESULT, args.id, function(stanza) { + _this._handle_transport_info_success(stanza); + }); + + // Schedule error timeout + this.utils.stanza_timeout(JSJAC_JINGLE_STANZA_IQ, JSJAC_JINGLE_IQ_TYPE_RESULT, args.id, { + internal: this._handle_transport_info_error.bind(this) + }); + + this.get_debug().log('[JSJaCJingle:single] _send_transport_info > Sent.', 2); + } catch(e) { + this.get_debug().log('[JSJaCJingle:single] _send_transport_info > ' + e, 1); + } + }, + + /** + * Sends the Jingle transport reject + * @private + * @param {JSJaCPacket} stanza + */ + _send_transport_reject: function(stanza) { + this.get_debug().log('[JSJaCJingle:single] _send_transport_reject', 4); + + try { + // Not implemented for now + this.get_debug().log('[JSJaCJingle:single] _send_transport_reject > Feature not implemented!', 0); + } catch(e) { + this.get_debug().log('[JSJaCJingle:single] _send_transport_reject > ' + e, 1); + } + }, + + /** + * Sends the Jingle transport replace + * @private + * @param {JSJaCPacket} stanza + */ + _send_transport_replace: function(stanza) { + this.get_debug().log('[JSJaCJingle:single] _send_transport_replace', 4); + + try { + // Not implemented for now + this.get_debug().log('[JSJaCJingle:single] _send_transport_replace > Feature not implemented!', 0); + } catch(e) { + this.get_debug().log('[JSJaCJingle:single] _send_transport_replace > ' + e, 1); + } + }, + + /** + * Sends the Jingle transport replace + * @private + * @param {JSJaCPacket} stanza + * @param {Object} error + */ + _send_error: function(stanza, error) { + this.get_debug().log('[JSJaCJingle:single] _send_error', 4); + + try { + // Assert + if(!('type' in error)) { + this.get_debug().log('[JSJaCJingle:single] _send_error > Type unknown.', 1); + return; + } + + if('jingle' in error && !(error.jingle in JSJAC_JINGLE_ERRORS)) { + this.get_debug().log('[JSJaCJingle:single] _send_error > Jingle condition unknown (' + error.jingle + ').', 1); + return; + } + + if('xmpp' in error && !(error.xmpp in XMPP_ERRORS)) { + this.get_debug().log('[JSJaCJingle:single] _send_error > XMPP condition unknown (' + error.xmpp + ').', 1); + return; + } + + var stanza_error = new JSJaCIQ(); + + stanza_error.setType('error'); + stanza_error.setID(stanza.getID()); + stanza_error.setTo(this.get_to()); + + var error_node = stanza_error.getNode().appendChild(stanza_error.buildNode('error', {'xmlns': NS_CLIENT, 'type': error.type})); + + if('xmpp' in error) error_node.appendChild(stanza_error.buildNode(error.xmpp, { 'xmlns': NS_STANZAS })); + if('jingle' in error) error_node.appendChild(stanza_error.buildNode(error.jingle, { 'xmlns': NS_JINGLE_ERRORS })); + + this.get_connection().send(stanza_error); + + this.get_debug().log('[JSJaCJingle:single] _send_error > Sent: ' + (error.jingle || error.xmpp), 2); + } catch(e) { + this.get_debug().log('[JSJaCJingle:single] _send_error > ' + e, 1); + } + }, + + + + /** + * JSJSAC JINGLE HANDLERS + */ + + /** + * Handles the Jingle content accept + * @private + * @event JSJaCJingleSingle#_handle_content_accept + * @param {JSJaCPacket} stanza + */ + _handle_content_accept: function(stanza) { + this.get_debug().log('[JSJaCJingle:single] _handle_content_accept', 4); + + try { + /* @todo start to flow accepted stream */ + /* @todo remove accepted content from local 'content-add' queue */ + /* @todo reprocess content_local/content_remote */ + + // Not implemented for now + this._send_error(stanza, XMPP_ERROR_FEATURE_NOT_IMPLEMENTED); + } catch(e) { + this.get_debug().log('[JSJaCJingle:single] _handle_content_accept > ' + e, 1); + } + }, + + /** + * Handles the Jingle content add + * @private + * @event JSJaCJingleSingle#_handle_content_add + * @param {JSJaCPacket} stanza + */ + _handle_content_add: function(stanza) { + this.get_debug().log('[JSJaCJingle:single] _handle_content_add', 4); + + try { + /* @todo request the user to start this content (need a custom handler) + * on accept: send content-accept */ + /* @todo push to remote 'content-add' queue */ + /* @todo reprocess content_local/content_remote */ + + // Not implemented for now + this._send_error(stanza, XMPP_ERROR_FEATURE_NOT_IMPLEMENTED); + } catch(e) { + this.get_debug().log('[JSJaCJingle:single] _handle_content_add > ' + e, 1); + } + }, + + /** + * Handles the Jingle content modify + * @private + * @event JSJaCJingleSingle#_handle_content_modify + * @param {JSJaCPacket} stanza + */ + _handle_content_modify: function(stanza) { + this.get_debug().log('[JSJaCJingle:single] _handle_content_modify', 4); + + try { + /* @todo change 'senders' value (direction of the stream) + * if(send:from_me): notify the user that media is requested + * if(unacceptable): terminate the session + * if(accepted): change local/remote SDP */ + /* @todo reprocess content_local/content_remote */ + + // Not implemented for now + this._send_error(stanza, XMPP_ERROR_FEATURE_NOT_IMPLEMENTED); + } catch(e) { + this.get_debug().log('[JSJaCJingle:single] _handle_content_modify > ' + e, 1); + } + }, + + /** + * Handles the Jingle content reject + * @private + * @event JSJaCJingleSingle#_handle_content_reject + * @param {JSJaCPacket} stanza + */ + _handle_content_reject: function(stanza) { + this.get_debug().log('[JSJaCJingle:single] _handle_content_reject', 4); + + try { + /* @todo remove rejected content from local 'content-add' queue */ + + // Not implemented for now + this._send_error(stanza, XMPP_ERROR_FEATURE_NOT_IMPLEMENTED); + } catch(e) { + this.get_debug().log('[JSJaCJingle:single] _handle_content_reject > ' + e, 1); + } + }, + + /** + * Handles the Jingle content remove + * @private + * @event JSJaCJingleSingle#_handle_content_remove + * @param {JSJaCPacket} stanza + */ + _handle_content_remove: function(stanza) { + this.get_debug().log('[JSJaCJingle:single] _handle_content_remove', 4); + + try { + /* @todo stop flowing removed stream */ + /* @todo reprocess content_local/content_remote */ + + // Not implemented for now + this._send_error(stanza, XMPP_ERROR_FEATURE_NOT_IMPLEMENTED); + } catch(e) { + this.get_debug().log('[JSJaCJingle:single] _handle_content_remove > ' + e, 1); + } + }, + + /** + * Handles the Jingle description info + * @private + * @event JSJaCJingleSingle#_handle_description_info + * @param {JSJaCPacket} stanza + */ + _handle_description_info: function(stanza) { + this.get_debug().log('[JSJaCJingle:single] _handle_description_info', 4); + + try { + // Not implemented for now + this._send_error(stanza, XMPP_ERROR_FEATURE_NOT_IMPLEMENTED); + } catch(e) { + this.get_debug().log('[JSJaCJingle:single] _handle_description_info > ' + e, 1); + } + }, + + /** + * Handles the Jingle security info + * @private + * @event JSJaCJingleSingle#_handle_security_info + * @param {JSJaCPacket} stanza + */ + _handle_security_info: function(stanza) { + this.get_debug().log('[JSJaCJingle:single] _handle_security_info', 4); + + try { + // Not implemented for now + this._send_error(stanza, XMPP_ERROR_FEATURE_NOT_IMPLEMENTED); + } catch(e) { + this.get_debug().log('[JSJaCJingle:single] _handle_security_info > ' + e, 1); + } + }, + + /** + * Handles the Jingle session accept + * @private + * @event JSJaCJingleSingle#_handle_session_accept + * @fires JSJaCJingleSingle#_handle_session_accept_success + * @fires JSJaCJingleSingle#_handle_session_accept_error + * @fires JSJaCJingleSingle#get_session_accept_success + * @fires JSJaCJingleSingle#get_session_accept_error + * @fires JSJaCJingleSingle#get_session_accept_request + * @param {JSJaCPacket} stanza + */ + _handle_session_accept: function(stanza) { + this.get_debug().log('[JSJaCJingle:single] _handle_session_accept', 4); + + try { + // Security preconditions + if(!this.utils.stanza_safe(stanza)) { + this.get_debug().log('[JSJaCJingle:single] _handle_session_accept > Dropped unsafe stanza.', 0); + + this._send_error(stanza, JSJAC_JINGLE_ERROR_UNKNOWN_SESSION); + return; + } + + // Can now safely dispatch the stanza + switch(stanza.getType()) { + case JSJAC_JINGLE_IQ_TYPE_RESULT: + /* @function */ + (this.get_session_accept_success())(this, stanza); + this._handle_session_accept_success(stanza); + + break; + + case 'error': + /* @function */ + (this.get_session_accept_error())(this, stanza); + this._handle_session_accept_error(stanza); + + break; + + case JSJAC_JINGLE_IQ_TYPE_SET: + // External handler must be set before internal one here... + /* @function */ + (this.get_session_accept_request())(this, stanza); + this._handle_session_accept_request(stanza); + + break; + + default: + this._send_error(stanza, XMPP_ERROR_FEATURE_NOT_IMPLEMENTED); + } + } catch(e) { + this.get_debug().log('[JSJaCJingle:single] _handle_session_accept > ' + e, 1); + } + }, + + /** + * Handles the Jingle session accept success + * @private + * @event JSJaCJingleSingle#_handle_session_accept_success + * @param {JSJaCPacket} stanza + */ + _handle_session_accept_success: function(stanza) { + this.get_debug().log('[JSJaCJingle:single] _handle_session_accept_success', 4); + + try { + // Change session status + this._set_status(JSJAC_JINGLE_STATUS_ACCEPTED); + } catch(e) { + this.get_debug().log('[JSJaCJingle:single] _handle_session_accept_success > ' + e, 1); + } + }, + + /** + * Handles the Jingle session accept error + * @private + * @event JSJaCJingleSingle#_handle_session_accept_error + * @param {JSJaCPacket} stanza + */ + _handle_session_accept_error: function(stanza) { + this.get_debug().log('[JSJaCJingle:single] _handle_session_accept_error', 4); + + try { + // Terminate the session (timeout) + this.terminate(JSJAC_JINGLE_REASON_TIMEOUT); + } catch(e) { + this.get_debug().log('[JSJaCJingle:single] _handle_session_accept_error > ' + e, 1); + } + }, + + /** + * Handles the Jingle session accept request + * @private + * @event JSJaCJingleSingle#_handle_session_accept_request + * @fires JSJaCJingleSingle#_handle_session_accept_success + * @fires JSJaCJingleSingle#_handle_session_accept_error + * @fires JSJaCJingleSingle#get_session_accept_success + * @fires JSJaCJingleSingle#get_session_accept_error + * @param {JSJaCPacket} stanza + */ + _handle_session_accept_request: function(stanza) { + this.get_debug().log('[JSJaCJingle:single] _handle_session_accept_request', 4); + + try { + // Slot unavailable? + if(this.get_status() !== JSJAC_JINGLE_STATUS_INITIATED) { + this.get_debug().log('[JSJaCJingle:single] _handle_session_accept_request > Cannot handle, resource already accepted (status: ' + this.get_status() + ').', 0); + this._send_error(stanza, JSJAC_JINGLE_ERROR_OUT_OF_ORDER); + return; + } + + // Common vars + var i, cur_candidate_obj; + + // Change session status + this._set_status(JSJAC_JINGLE_STATUS_ACCEPTING); + + var rd_sid = this.utils.stanza_sid(stanza); + + // Request is valid? + if(rd_sid && this.is_initiator() && this.utils.stanza_parse_content(stanza)) { + // Handle additional data (optional) + this.utils.stanza_parse_group(stanza); + + // Generate and store content data + this.utils.build_content_remote(); + + // Trigger accept success callback + /* @function */ + (this.get_session_accept_success())(this, stanza); + this._handle_session_accept_success(stanza); + + var sdp_remote = this.sdp._generate( + WEBRTC_SDP_TYPE_ANSWER, + this.get_group_remote(), + this.get_payloads_remote(), + this.get_candidates_queue_remote() + ); + + if(this.get_sdp_trace()) this.get_debug().log('[JSJaCJingle:single] SDP (remote)' + '\n\n' + sdp_remote.description.sdp, 4); + + // Remote description + var _this = this; + + this.get_peer_connection().setRemoteDescription( + (new WEBRTC_SESSION_DESCRIPTION(sdp_remote.description)), + + function() { + // Success (descriptions are compatible) + }, + + function(e) { + if(_this.get_sdp_trace()) _this.get_debug().log('[JSJaCJingle:single] SDP (remote:error)' + '\n\n' + (e.message || e.name || 'Unknown error'), 4); + + // Error (descriptions are incompatible) + _this.terminate(JSJAC_JINGLE_REASON_INCOMPATIBLE_PARAMETERS); + } + ); + + // ICE candidates + for(i in sdp_remote.candidates) { + cur_candidate_obj = sdp_remote.candidates[i]; + + this.get_peer_connection().addIceCandidate( + new WEBRTC_ICE_CANDIDATE({ + sdpMLineIndex : cur_candidate_obj.id, + candidate : cur_candidate_obj.candidate + }) + ); + } + + // Empty the unapplied candidates queue + this._set_candidates_queue_remote(null); + + // Success reply + this.send(JSJAC_JINGLE_IQ_TYPE_RESULT, { id: stanza.getID() }); + } else { + // Trigger accept error callback + /* @function */ + (this.get_session_accept_error())(this, stanza); + this._handle_session_accept_error(stanza); + + // Send error reply + this._send_error(stanza, XMPP_ERROR_BAD_REQUEST); + + this.get_debug().log('[JSJaCJingle:single] _handle_session_accept_request > Error.', 1); + } + } catch(e) { + this.get_debug().log('[JSJaCJingle:single] _handle_session_accept_request > ' + e, 1); + } + }, + + /** + * Handles the Jingle session info + * @private + * @event JSJaCJingleSingle#_handle_session_info + * @fires JSJaCJingleSingle#_handle_session_info_success + * @fires JSJaCJingleSingle#_handle_session_info_error + * @fires JSJaCJingleSingle#_handle_session_info_request + * @fires JSJaCJingleSingle#get_session_info_success + * @fires JSJaCJingleSingle#get_session_info_error + * @fires JSJaCJingleSingle#get_session_info_request + * @param {JSJaCPacket} stanza + */ + _handle_session_info: function(stanza) { + this.get_debug().log('[JSJaCJingle:single] _handle_session_info', 4); + + try { + // Security preconditions + if(!this.utils.stanza_safe(stanza)) { + this.get_debug().log('[JSJaCJingle:single] _handle_session_info > Dropped unsafe stanza.', 0); + + this._send_error(stanza, JSJAC_JINGLE_ERROR_UNKNOWN_SESSION); + return; + } + + // Can now safely dispatch the stanza + switch(stanza.getType()) { + case JSJAC_JINGLE_IQ_TYPE_RESULT: + /* @function */ + (this.get_session_info_success())(this, stanza); + this._handle_session_info_success(stanza); + + break; + + case 'error': + /* @function */ + (this.get_session_info_error())(this, stanza); + this._handle_session_info_error(stanza); + + break; + + case JSJAC_JINGLE_IQ_TYPE_SET: + /* @function */ + (this.get_session_info_request())(this, stanza); + this._handle_session_info_request(stanza); + + break; + + default: + this._send_error(stanza, XMPP_ERROR_FEATURE_NOT_IMPLEMENTED); + } + } catch(e) { + this.get_debug().log('[JSJaCJingle:single] _handle_session_info > ' + e, 1); + } + }, + + /** + * Handles the Jingle session info success + * @private + * @event JSJaCJingleSingle#_handle_session_info_success + * @param {JSJaCPacket} stanza + */ + _handle_session_info_success: function(stanza) { + this.get_debug().log('[JSJaCJingle:single] _handle_session_info_success', 4); + }, + + /** + * Handles the Jingle session info error + * @private + * @event JSJaCJingleSingle#_handle_session_info_error + * @param {JSJaCPacket} stanza + */ + _handle_session_info_error: function(stanza) { + this.get_debug().log('[JSJaCJingle:single] _handle_session_info_error', 4); + }, + + /** + * Handles the Jingle session info request + * @private + * @event JSJaCJingleSingle#_handle_session_info_request + * @fires JSJaCJingleSingle#_handle_session_info_success + * @fires JSJaCJingleSingle#_handle_session_info_error + * @fires JSJaCJingleSingle#get_session_info_success + * @fires JSJaCJingleSingle#get_session_info_error + * @param {JSJaCPacket} stanza + */ + _handle_session_info_request: function(stanza) { + this.get_debug().log('[JSJaCJingle:single] _handle_session_info_request', 4); + + try { + // Parse stanza + var info_name = this.utils.stanza_session_info(stanza); + var info_result = false; + + switch(info_name) { + case JSJAC_JINGLE_SESSION_INFO_ACTIVE: + case JSJAC_JINGLE_SESSION_INFO_RINGING: + case JSJAC_JINGLE_SESSION_INFO_MUTE: + case JSJAC_JINGLE_SESSION_INFO_UNMUTE: + info_result = true; break; + } + + if(info_result) { + this.get_debug().log('[JSJaCJingle:single] _handle_session_info_request > (name: ' + (info_name || 'undefined') + ').', 3); + + // Process info actions + this.send(JSJAC_JINGLE_IQ_TYPE_RESULT, { id: stanza.getID() }); + + // Trigger info success custom callback + /* @function */ + (this.get_session_info_success())(this, stanza); + this._handle_session_info_success(stanza); + } else { + this.get_debug().log('[JSJaCJingle:single] _handle_session_info_request > Error (name: ' + (info_name || 'undefined') + ').', 1); + + // Send error reply + this._send_error(stanza, XMPP_ERROR_FEATURE_NOT_IMPLEMENTED); + + // Trigger info error custom callback + /* @function */ + (this.get_session_info_error())(this, stanza); + this._handle_session_info_error(stanza); + } + } catch(e) { + this.get_debug().log('[JSJaCJingle:single] _handle_session_info_request > ' + e, 1); + } + }, + + /** + * Handles the Jingle session initiate + * @private + * @event JSJaCJingleSingle#_handle_session_initiate + * @fires JSJaCJingleSingle#_handle_session_initiate_success + * @fires JSJaCJingleSingle#_handle_session_initiate_error + * @fires JSJaCJingleSingle#_handle_session_initiate_request + * @fires JSJaCJingleSingle#get_session_initiate_success + * @fires JSJaCJingleSingle#get_session_initiate_error + * @fires JSJaCJingleSingle#get_session_initiate_request + * @param {JSJaCPacket} stanza + */ + _handle_session_initiate: function(stanza) { + this.get_debug().log('[JSJaCJingle:single] _handle_session_initiate', 4); + + try { + switch(stanza.getType()) { + case JSJAC_JINGLE_IQ_TYPE_RESULT: + /* @function */ + (this.get_session_initiate_success())(this, stanza); + this._handle_session_initiate_success(stanza); + + break; + + case 'error': + /* @function */ + (this.get_session_initiate_error())(this, stanza); + this._handle_session_initiate_error(stanza); + + break; + + case JSJAC_JINGLE_IQ_TYPE_SET: + /* @function */ + (this.get_session_initiate_request())(this, stanza); + this._handle_session_initiate_request(stanza); + + break; + + default: + this._send_error(stanza, XMPP_ERROR_FEATURE_NOT_IMPLEMENTED); + } + } catch(e) { + this.get_debug().log('[JSJaCJingle:single] _handle_session_initiate > ' + e, 1); + } + }, + + /** + * Handles the Jingle session initiate success + * @private + * @event JSJaCJingleSingle#_handle_session_initiate_success + * @param {JSJaCPacket} stanza + */ + _handle_session_initiate_success: function(stanza) { + this.get_debug().log('[JSJaCJingle:single] _handle_session_initiate_success', 4); + + try { + // Change session status + this._set_status(JSJAC_JINGLE_STATUS_INITIATED); + } catch(e) { + this.get_debug().log('[JSJaCJingle:single] _handle_session_initiate_success > ' + e, 1); + } + }, + + /** + * Handles the Jingle session initiate error + * @private + * @event JSJaCJingleSingle#_handle_session_initiate_error + * @param {JSJaCPacket} stanza + */ + _handle_session_initiate_error: function(stanza) { + this.get_debug().log('[JSJaCJingle:single] _handle_session_initiate_error', 4); + + try { + // Change session status + this._set_status(JSJAC_JINGLE_STATUS_INACTIVE); + + // Stop WebRTC + this._peer_stop(); + + // Lock session (cannot be used later) + this._set_lock(true); + } catch(e) { + this.get_debug().log('[JSJaCJingle:single] _handle_session_initiate_error > ' + e, 1); + } + }, + + /** + * Handles the Jingle session initiate request + * @private + * @event JSJaCJingleSingle#_handle_session_initiate_request + * @fires JSJaCJingleSingle#_handle_session_initiate_success + * @fires JSJaCJingleSingle#_handle_session_initiate_error + * @fires JSJaCJingleSingle#get_session_initiate_success + * @fires JSJaCJingleSingle#get_session_initiate_error + * @param {JSJaCPacket} stanza + */ + _handle_session_initiate_request: function(stanza) { + this.get_debug().log('[JSJaCJingle:single] _handle_session_initiate_request', 4); + + try { + // Slot unavailable? + if(this.get_status() !== JSJAC_JINGLE_STATUS_INACTIVE) { + this.get_debug().log('[JSJaCJingle:single] _handle_session_initiate_request > Cannot handle, resource already initiated (status: ' + this.get_status() + ').', 0); + this._send_error(stanza, JSJAC_JINGLE_ERROR_OUT_OF_ORDER); + return; + } + + // Change session status + this._set_status(JSJAC_JINGLE_STATUS_INITIATING); + + // Common vars + var rd_from = this.utils.stanza_from(stanza); + var rd_sid = this.utils.stanza_sid(stanza); + + // Request is valid? + if(rd_sid && this.utils.stanza_parse_content(stanza)) { + // Handle additional data (optional) + this.utils.stanza_parse_group(stanza); + + // Set session values + this._set_sid(rd_sid); + this._set_to(rd_from); + this._set_initiator(rd_from); + this._set_responder(this.utils.connection_jid()); + + // Register session to common router + JSJaCJingle._add(JSJAC_JINGLE_SESSION_SINGLE, rd_sid, this); + + // Generate and store content data + this.utils.build_content_remote(); + + // Video or audio-only session? + if(JSJAC_JINGLE_MEDIA_VIDEO in this.get_content_remote()) { + this._set_media(JSJAC_JINGLE_MEDIA_VIDEO); + } else if(JSJAC_JINGLE_MEDIA_AUDIO in this.get_content_remote()) { + this._set_media(JSJAC_JINGLE_MEDIA_AUDIO); + } else { + // Session initiation not done + /* @function */ + (this.get_session_initiate_error())(this, stanza); + this._handle_session_initiate_error(stanza); + + // Error (no media is supported) + this.terminate(JSJAC_JINGLE_REASON_UNSUPPORTED_APPLICATIONS); + + this.get_debug().log('[JSJaCJingle:single] _handle_session_initiate_request > Error (unsupported media).', 1); + return; + } + + // Session initiate done + /* @function */ + (this.get_session_initiate_success())(this, stanza); + this._handle_session_initiate_success(stanza); + + this.send(JSJAC_JINGLE_IQ_TYPE_RESULT, { id: stanza.getID() }); + } else { + // Session initiation not done + /* @function */ + (this.get_session_initiate_error())(this, stanza); + this._handle_session_initiate_error(stanza); + + // Send error reply + this._send_error(stanza, XMPP_ERROR_BAD_REQUEST); + + this.get_debug().log('[JSJaCJingle:single] _handle_session_initiate_request > Error (bad request).', 1); + } + } catch(e) { + this.get_debug().log('[JSJaCJingle:single] _handle_session_initiate_request > ' + e, 1); + } + }, + + /** + * Handles the Jingle session terminate + * @private + * @event JSJaCJingleSingle#_handle_session_terminate + * @fires JSJaCJingleSingle#_handle_session_terminate_success + * @fires JSJaCJingleSingle#_handle_session_terminate_error + * @fires JSJaCJingleSingle#_handle_session_terminate_request + * @fires JSJaCJingleSingle#get_session_terminate_success + * @fires JSJaCJingleSingle#get_session_terminate_error + * @fires JSJaCJingleSingle#get_session_terminate_request + * @param {JSJaCPacket} stanza + */ + _handle_session_terminate: function(stanza) { + this.get_debug().log('[JSJaCJingle:single] _handle_session_terminate', 4); + + try { + var type = stanza.getType(); + + // Security preconditions + if(!this.utils.stanza_safe(stanza)) { + this.get_debug().log('[JSJaCJingle:single] _handle_session_terminate > Dropped unsafe stanza.', 0); + + this._send_error(stanza, JSJAC_JINGLE_ERROR_UNKNOWN_SESSION); + return; + } + + // Can now safely dispatch the stanza + switch(stanza.getType()) { + case JSJAC_JINGLE_IQ_TYPE_RESULT: + /* @function */ + (this.get_session_terminate_success())(this, stanza); + this._handle_session_terminate_success(stanza); + + break; + + case 'error': + /* @function */ + (this.get_session_terminate_error())(this, stanza); + this._handle_session_terminate_error(stanza); + + break; + + case JSJAC_JINGLE_IQ_TYPE_SET: + /* @function */ + (this.get_session_terminate_request())(this, stanza); + this._handle_session_terminate_request(stanza); + + break; + + default: + this._send_error(stanza, XMPP_ERROR_FEATURE_NOT_IMPLEMENTED); + } + } catch(e) { + this.get_debug().log('[JSJaCJingle:single] _handle_session_terminate > ' + e, 1); + } + }, + + /** + * Handles the Jingle session terminate success + * @private + * @event JSJaCJingleSingle#_handle_session_terminate_success + * @param {JSJaCPacket} stanza + */ + _handle_session_terminate_success: function(stanza) { + this.get_debug().log('[JSJaCJingle:single] _handle_session_terminate_success', 4); + + try { + this.abort(); + } catch(e) { + this.get_debug().log('[JSJaCJingle:single] _handle_session_terminate_success > ' + e, 1); + } + }, + + /** + * Handles the Jingle session terminate error + * @private + * @event JSJaCJingleSingle#_handle_session_terminate_error + * @param {JSJaCPacket} stanza + */ + _handle_session_terminate_error: function(stanza) { + this.get_debug().log('[JSJaCJingle:single] _handle_session_terminate_error', 4); + + try { + this.abort(true); + + this.get_debug().log('[JSJaCJingle:single] _handle_session_terminate_error > Forced session termination locally.', 0); + } catch(e) { + this.get_debug().log('[JSJaCJingle:single] _handle_session_terminate_error > ' + e, 1); + } + }, + + /** + * Handles the Jingle session terminate request + * @private + * @event JSJaCJingleSingle#_handle_session_terminate_request + * @fires JSJaCJingleSingle#_handle_session_terminate_success + * @fires JSJaCJingleSingle#get_session_terminate_success + * @param {JSJaCPacket} stanza + */ + _handle_session_terminate_request: function(stanza) { + this.get_debug().log('[JSJaCJingle:single] _handle_session_terminate_request', 4); + + try { + // Slot unavailable? + if(this.get_status() === JSJAC_JINGLE_STATUS_INACTIVE || + this.get_status() === JSJAC_JINGLE_STATUS_TERMINATED) { + this.get_debug().log('[JSJaCJingle:single] _handle_session_terminate_request > Cannot handle, resource not active (status: ' + this.get_status() + ').', 0); + this._send_error(stanza, JSJAC_JINGLE_ERROR_OUT_OF_ORDER); + return; + } + + // Change session status + this._set_status(JSJAC_JINGLE_STATUS_TERMINATING); + + // Store termination reason + this._set_reason(this.utils.stanza_terminate_reason(stanza)); + + // Trigger terminate success callbacks + /* @function */ + (this.get_session_terminate_success())(this, stanza); + this._handle_session_terminate_success(stanza); + + // Process terminate actions + this.send(JSJAC_JINGLE_IQ_TYPE_RESULT, { id: stanza.getID() }); + + this.get_debug().log('[JSJaCJingle:single] _handle_session_terminate_request > (reason: ' + this.get_reason() + ').', 3); + } catch(e) { + this.get_debug().log('[JSJaCJingle:single] _handle_session_terminate_request > ' + e, 1); + } + }, + + /** + * Handles the Jingle transport accept + * @private + * @event JSJaCJingleSingle#_handle_transport_accept + * @param {JSJaCPacket} stanza + */ + _handle_transport_accept: function(stanza) { + this.get_debug().log('[JSJaCJingle:single] _handle_transport_accept', 4); + + try { + // Not implemented for now + this._send_error(stanza, XMPP_ERROR_FEATURE_NOT_IMPLEMENTED); + } catch(e) { + this.get_debug().log('[JSJaCJingle:single] _handle_content_accept > ' + e, 1); + } + }, + + /** + * Handles the Jingle transport info + * @private + * @event JSJaCJingleSingle#_handle_transport_info + * @param {JSJaCPacket} stanza + */ + _handle_transport_info: function(stanza) { + this.get_debug().log('[JSJaCJingle:single] _handle_transport_info', 4); + + try { + // Slot unavailable? + if(this.get_status() !== JSJAC_JINGLE_STATUS_INITIATED && + this.get_status() !== JSJAC_JINGLE_STATUS_ACCEPTING && + this.get_status() !== JSJAC_JINGLE_STATUS_ACCEPTED) { + this.get_debug().log('[JSJaCJingle:single] _handle_transport_info > Cannot handle, resource not initiated, nor accepting, nor accepted (status: ' + this.get_status() + ').', 0); + this._send_error(stanza, JSJAC_JINGLE_ERROR_OUT_OF_ORDER); + return; + } + + // Common vars + var i, cur_candidate_obj; + + // Parse the incoming transport + var rd_sid = this.utils.stanza_sid(stanza); + + // Request is valid? + if(rd_sid && this.utils.stanza_parse_content(stanza)) { + // Handle additional data (optional) + // Still unsure if it is relevant to parse groups there... (are they allowed in such stanza?) + //this.utils.stanza_parse_group(stanza); + + // Re-generate and store new content data + this.utils.build_content_remote(); + + var sdp_candidates_remote = this.sdp._generate_candidates( + this.get_candidates_queue_remote() + ); + + // ICE candidates + for(i in sdp_candidates_remote) { + cur_candidate_obj = sdp_candidates_remote[i]; + + this.get_peer_connection().addIceCandidate( + new WEBRTC_ICE_CANDIDATE({ + sdpMLineIndex : cur_candidate_obj.id, + candidate : cur_candidate_obj.candidate + }) + ); + } + + // Empty the unapplied candidates queue + this._set_candidates_queue_remote(null); + + // Success reply + this.send(JSJAC_JINGLE_IQ_TYPE_RESULT, { id: stanza.getID() }); + } else { + // Send error reply + this._send_error(stanza, XMPP_ERROR_BAD_REQUEST); + + this.get_debug().log('[JSJaCJingle:single] _handle_transport_info > Error.', 1); + } + } catch(e) { + this.get_debug().log('[JSJaCJingle:single] _handle_transport_info > ' + e, 1); + } + }, + + /** + * Handles the Jingle transport info success + * @private + * @event JSJaCJingleSingle#_handle_transport_info_success + * @param {JSJaCPacket} stanza + */ + _handle_transport_info_success: function(stanza) { + this.get_debug().log('[JSJaCJingle:single] _handle_transport_info_success', 4); + }, + + /** + * Handles the Jingle transport info error + * @private + * @event JSJaCJingleSingle#_handle_transport_info_error + * @param {JSJaCPacket} stanza + */ + _handle_transport_info_error: function(stanza) { + this.get_debug().log('[JSJaCJingle:single] _handle_transport_info_error', 4); + }, + + /** + * Handles the Jingle transport reject + * @private + * @event JSJaCJingleSingle#_handle_transport_reject + * @param {JSJaCPacket} stanza + */ + _handle_transport_reject: function(stanza) { + this.get_debug().log('[JSJaCJingle:single] _handle_transport_reject', 4); + + try { + // Not implemented for now + this._send_error(stanza, XMPP_ERROR_FEATURE_NOT_IMPLEMENTED); + } catch(e) { + this.get_debug().log('[JSJaCJingle:single] _handle_transport_reject > ' + e, 1); + } + }, + + /** + * Handles the Jingle transport replace + * @private + * @event JSJaCJingleSingle#_handle_transport_replace + * @param {JSJaCPacket} stanza + */ + _handle_transport_replace: function(stanza) { + this.get_debug().log('[JSJaCJingle:single] _handle_transport_replace', 4); + + try { + // Not implemented for now + this._send_error(stanza, XMPP_ERROR_FEATURE_NOT_IMPLEMENTED); + } catch(e) { + this.get_debug().log('[JSJaCJingle:single] _handle_transport_replace > ' + e, 1); + } + }, + + + + /** + * JSJSAC JINGLE PEER TOOLS + */ + + /** + * Creates peer connection instance + * @private + */ + _peer_connection_create_instance: function() { + this.get_debug().log('[JSJaCJingle:single] _peer_connection_create_instance', 4); + + try { + // Log STUN servers in use + var i; + var ice_config = this.utils.config_ice(); + + if(typeof ice_config.iceServers == 'object') { + for(i = 0; i < (ice_config.iceServers).length; i++) + this.get_debug().log('[JSJaCJingle:single] _peer_connection_create_instance > Using ICE server at: ' + ice_config.iceServers[i].url + ' (' + (i + 1) + ').', 2); + } else { + this.get_debug().log('[JSJaCJingle:single] _peer_connection_create_instance > No ICE server configured. Network may not work properly.', 0); + } + + // Create the RTCPeerConnection object + this._set_peer_connection( + new WEBRTC_PEER_CONNECTION( + ice_config, + WEBRTC_CONFIGURATION.peer_connection.constraints + ) + ); + } catch(e) { + this.get_debug().log('[JSJaCJingle:single] _peer_connection_create_instance > ' + e, 1); + } + }, + + /** + * Attaches peer connection callbacks + * @private + * @fires JSJaCJingleSingle#_peer_connection_callback_onicecandidate + * @fires JSJaCJingleSingle#_peer_connection_callback_oniceconnectionstatechange + * @fires JSJaCJingleSingle#_peer_connection_callback_onaddstream + * @fires JSJaCJingleSingle#_peer_connection_callback_onremovestream + * @param {Function} sdp_message_callback + */ + _peer_connection_callbacks: function(sdp_message_callback) { + this.get_debug().log('[JSJaCJingle:single] _peer_connection_callbacks', 4); + + try { + var _this = this; + + /** + * Listens for incoming ICE candidates + * @event JSJaCJingleSingle#_peer_connection_callback_onicecandidate + * @type {Function} + */ + this.get_peer_connection().onicecandidate = function(data) { + _this._peer_connection_callback_onicecandidate.bind(this)(_this, sdp_message_callback, data); + }; + + /** + * Listens for ICE connection state change + * @event JSJaCJingleSingle#_peer_connection_callback_oniceconnectionstatechange + * @type {Function} + */ + this.get_peer_connection().oniceconnectionstatechange = function(data) { + _this._peer_connection_callback_oniceconnectionstatechange.bind(this)(_this, data); + }; + + /** + * Listens for stream add + * @event JSJaCJingleSingle#_peer_connection_callback_onaddstream + * @type {Function} + */ + this.get_peer_connection().onaddstream = function(data) { + _this._peer_connection_callback_onaddstream.bind(this)(_this, data); + }; + + /** + * Listens for stream remove + * @event JSJaCJingleSingle#_peer_connection_callback_onremovestream + * @type {Function} + */ + this.get_peer_connection().onremovestream = function(data) { + _this._peer_connection_callback_onremovestream.bind(this)(_this, data); + }; + } catch(e) { + this.get_debug().log('[JSJaCJingle:single] _peer_connection_callbacks > ' + e, 1); + } + }, + + /** + * Generates peer connection callback for 'onicecandidate' + * @private + * @callback + * @param {JSJaCJingleSingle} _this + * @param {Function} sdp_message_callback + * @param {Object} data + */ + _peer_connection_callback_onicecandidate: function(_this, sdp_message_callback, data) { + _this.get_debug().log('[JSJaCJingle:single] _peer_connection_callback_onicecandidate', 4); + + try { + if(data.candidate) { + _this.sdp._parse_candidate_store_store_data(data); } else { // Build or re-build content (local) - self._util_build_content_local(); + _this.utils.build_content_local(); // In which action stanza should candidates be sent? - if((self.is_initiator() && self.get_status() == JSJAC_JINGLE_STATUS_INITIATING) || - (self.is_responder() && self.get_status() == JSJAC_JINGLE_STATUS_ACCEPTING)) { - self.get_debug().log('[JSJaCJingle] _peer_connection_create > onicecandidate > Got initial candidates.', 2); + if((_this.is_initiator() && _this.get_status() === JSJAC_JINGLE_STATUS_INITIATING) || + (_this.is_responder() && _this.get_status() === JSJAC_JINGLE_STATUS_ACCEPTING)) { + _this.get_debug().log('[JSJaCJingle:single] _peer_connection_callback_onicecandidate > Got initial candidates.', 2); // Execute what's next (initiate/accept session) sdp_message_callback(); } else { - self.get_debug().log('[JSJaCJingle] _peer_connection_create > onicecandidate > Got more candidates (on the go).', 2); + _this.get_debug().log('[JSJaCJingle:single] _peer_connection_callback_onicecandidate > Got more candidates (on the go).', 2); // Send unsent candidates - var candidates_queue_local = self._get_candidates_queue_local(); + var candidates_queue_local = _this.get_candidates_queue_local(); - if(self.util_object_length(candidates_queue_local) > 0) - self.send(JSJAC_JINGLE_STANZA_TYPE_SET, { action: JSJAC_JINGLE_ACTION_TRANSPORT_INFO, candidates: candidates_queue_local }); + if(_this.utils.object_length(candidates_queue_local) > 0) + _this.send(JSJAC_JINGLE_IQ_TYPE_SET, { action: JSJAC_JINGLE_ACTION_TRANSPORT_INFO, candidates: candidates_queue_local }); } // Empty the unsent candidates queue - self._set_candidates_queue_local(null); + _this._set_candidates_queue_local(null); } - }; + } catch(e) { + _this.get_debug().log('[JSJaCJingle:single] _peer_connection_callback_onicecandidate > ' + e, 1); + } + }, - // Event: oniceconnectionstatechange - self._get_peer_connection().oniceconnectionstatechange = function(e) { - self.get_debug().log('[JSJaCJingle] _peer_connection_create > oniceconnectionstatechange', 2); + /** + * Generates peer connection callback for 'oniceconnectionstatechange' + * @private + * @callback + * @param {JSJaCJingleSingle} _this + * @param {Object} data + */ + _peer_connection_callback_oniceconnectionstatechange: function(_this, data) { + _this.get_debug().log('[JSJaCJingle:single] _peer_connection_callback_oniceconnectionstatechange', 4); + try { // Connection errors? switch(this.iceConnectionState) { case 'disconnected': - self._peer_timeout(this.iceConnectionState, { + _this._peer_timeout(this.iceConnectionState, { timer : JSJAC_JINGLE_PEER_TIMEOUT_DISCONNECT, reason : JSJAC_JINGLE_REASON_CONNECTIVITY_ERROR }); break; case 'checking': - self._peer_timeout(this.iceConnectionState); break; + _this._peer_timeout(this.iceConnectionState); break; } - self.get_debug().log('[JSJaCJingle] _peer_connection_create > oniceconnectionstatechange > (state: ' + this.iceConnectionState + ').', 2); - }; + _this.get_debug().log('[JSJaCJingle:single] _peer_connection_callback_oniceconnectionstatechange > (state: ' + this.iceConnectionState + ').', 2); + } catch(e) { + _this.get_debug().log('[JSJaCJingle:single] _peer_connection_callback_oniceconnectionstatechange > ' + e, 1); + } + }, - // Event: onaddstream - self._get_peer_connection().onaddstream = function(e) { - if (!e) return; + /** + * Generates peer connection callback for 'onaddstream' + * @private + * @callback + * @param {JSJaCJingleSingle} _this + * @param {Object} data + */ + _peer_connection_callback_onaddstream: function(_this, data) { + _this.get_debug().log('[JSJaCJingle:single] _peer_connection_callback_onaddstream', 4); - self.get_debug().log('[JSJaCJingle] _peer_connection_create > onaddstream', 2); + try { + if(!data) { + _this.get_debug().log('[JSJaCJingle:single] _peer_connection_callback_onaddstream > No data passed, dropped.', 2); return; + } // Attach remote stream to DOM view - self._set_remote_stream(e.stream); - }; + _this._set_remote_stream(data.stream); + } catch(e) { + _this.get_debug().log('[JSJaCJingle:single] _peer_connection_callback_onaddstream > ' + e, 1); + } + }, - // Event: onremovestream - self._get_peer_connection().onremovestream = function(e) { - self.get_debug().log('[JSJaCJingle] _peer_connection_create > onremovestream', 2); + /** + * Generates peer connection callback for 'onremovestream' + * @private + * @callback + * @param {JSJaCJingleSingle} _this + * @param {Object} data + */ + _peer_connection_callback_onremovestream: function(_this, data) { + _this.get_debug().log('[JSJaCJingle:single] _peer_connection_callback_onremovestream', 4); + try { // Detach remote stream from DOM view - self._set_remote_stream(null); - }; + _this._set_remote_stream(null); + } catch(e) { + _this.get_debug().log('[JSJaCJingle:single] _peer_connection_callback_onremovestream > ' + e, 1); + } + }, - // Add local stream - self._get_peer_connection().addStream(self._get_local_stream()); + /** + * Dispatches peer connection to correct creator (offer/answer) + * @private + * @param {Function} [sdp_message_callback] - Not used there + */ + _peer_connection_create_dispatch: function(sdp_message_callback) { + this.get_debug().log('[JSJaCJingle:single] _peer_connection_create_dispatch', 4); - // Create offer - self.get_debug().log('[JSJaCJingle] _peer_connection_create > Getting local description...', 2); + try { + if(this.is_initiator()) + this._peer_connection_create_offer(); + else + this._peer_connection_create_answer(); + } catch(e) { + this.get_debug().log('[JSJaCJingle:single] _peer_connection_create_dispatch > ' + e, 1); + } + }, + + /** + * Creates peer connection offer + * @private + * @param {Function} [sdp_message_callback] - Not used there + */ + _peer_connection_create_offer: function(sdp_message_callback) { + this.get_debug().log('[JSJaCJingle:single] _peer_connection_create_offer', 4); + + try { + // Create offer + this.get_debug().log('[JSJaCJingle:single] _peer_connection_create_offer > Getting local description...', 2); - if(self.is_initiator()) { // Local description - self._get_peer_connection().createOffer(self._peer_got_description, self._peer_fail_description, WEBRTC_CONFIGURATION.create_offer); + var _this = this; - // Then, wait for responder to send back its remote description - } else { - // Apply SDP data - sdp_remote = self._util_sdp_generate( - WEBRTC_SDP_TYPE_OFFER, - self._get_group_remote(), - self._get_payloads_remote(), - self._get_candidates_queue_remote() + this.get_peer_connection().createOffer( + function(sdp_local) { + _this._peer_got_description(sdp_local); + }.bind(this), + + this._peer_fail_description.bind(this), + WEBRTC_CONFIGURATION.create_offer ); - if(self.get_sdp_trace()) self.get_debug().log('[JSJaCJingle] SDP (remote)' + '\n\n' + sdp_remote.description.sdp, 4); + // Then, wait for responder to send back its remote description + } catch(e) { + this.get_debug().log('[JSJaCJingle:single] _peer_connection_create_offer > ' + e, 1); + } + }, + + /** + * Creates peer connection answer + * @private + */ + _peer_connection_create_answer: function() { + this.get_debug().log('[JSJaCJingle:single] _peer_connection_create_answer', 4); + + try { + // Create offer + this.get_debug().log('[JSJaCJingle:single] _peer_connection_create_answer > Getting local description...', 2); + + // Apply SDP data + sdp_remote = this.sdp._generate( + WEBRTC_SDP_TYPE_OFFER, + this.get_group_remote(), + this.get_payloads_remote(), + this.get_candidates_queue_remote() + ); + + if(this.get_sdp_trace()) this.get_debug().log('[JSJaCJingle:single] _peer_connection_create_answer > SDP (remote)' + '\n\n' + sdp_remote.description.sdp, 4); // Remote description - self._get_peer_connection().setRemoteDescription( + var _this = this; + + this.get_peer_connection().setRemoteDescription( (new WEBRTC_SESSION_DESCRIPTION(sdp_remote.description)), function() { @@ -6661,15 +12535,22 @@ function JSJaCJingle(args) { }, function(e) { - if(self.get_sdp_trace()) self.get_debug().log('[JSJaCJingle] SDP (remote:error)' + '\n\n' + (e.message || e.name || 'Unknown error'), 4); + if(_this.get_sdp_trace()) _this.get_debug().log('[JSJaCJingle:single] _peer_connection_create_answer > SDP (remote:error)' + '\n\n' + (e.message || e.name || 'Unknown error'), 4); // Error (descriptions are incompatible) - self.terminate(JSJAC_JINGLE_REASON_INCOMPATIBLE_PARAMETERS); + _this.terminate(JSJAC_JINGLE_REASON_INCOMPATIBLE_PARAMETERS); } ); // Local description - self._get_peer_connection().createAnswer(self._peer_got_description, self._peer_fail_description, WEBRTC_CONFIGURATION.create_answer); + this.get_peer_connection().createAnswer( + function(sdp_local) { + _this._peer_got_description(sdp_local); + }.bind(this), + + this._peer_fail_description.bind(this), + WEBRTC_CONFIGURATION.create_answer + ); // ICE candidates var c; @@ -6678,7 +12559,7 @@ function JSJaCJingle(args) { for(c in sdp_remote.candidates) { cur_candidate_obj = sdp_remote.candidates[c]; - self._get_peer_connection().addIceCandidate( + this.get_peer_connection().addIceCandidate( new WEBRTC_ICE_CANDIDATE({ sdpMLineIndex : cur_candidate_obj.id, candidate : cur_candidate_obj.candidate @@ -6687,561 +12568,4914 @@ function JSJaCJingle(args) { } // Empty the unapplied candidates queue - self._set_candidates_queue_remote(null); + this._set_candidates_queue_remote(null); + } catch(e) { + this.get_debug().log('[JSJaCJingle:single] _peer_connection_create_answer > ' + e, 1); } - } catch(e) { - self.get_debug().log('[JSJaCJingle] _peer_connection_create > ' + e, 1); - } - }; + }, - /** - * @private - */ - self._peer_get_user_media = function(callback) { - self.get_debug().log('[JSJaCJingle] _peer_get_user_media', 4); + /** + * Triggers the media not obtained error event + * @private + * @fires JSJaCJingleSingle#get_session_initiate_error + * @param {Object} error + */ + _peer_got_user_media_error: function(error) { + this.get_debug().log('[JSJaCJingle:single] _peer_got_user_media_error', 4); - try { - self.get_debug().log('[JSJaCJingle] _peer_get_user_media > Getting user media...', 2); - - (WEBRTC_GET_MEDIA.bind(navigator))( - self.util_generate_constraints(), - self._peer_got_user_media_success.bind(this, callback), - self._peer_got_user_media_error.bind(this) - ); - } catch(e) { - self.get_debug().log('[JSJaCJingle] _peer_get_user_media > ' + e, 1); - } - }; - - /** - * @private - */ - self._peer_got_user_media_success = function(callback, stream) { - self.get_debug().log('[JSJaCJingle] _peer_got_user_media_success', 4); - - try { - self.get_debug().log('[JSJaCJingle] _peer_got_user_media_success > Got user media.', 2); - - self._set_local_stream(stream); - - if(callback && typeof callback == 'function') { - if((self.get_media() == JSJAC_JINGLE_MEDIA_VIDEO) && self.get_local_view().length) { - self.get_debug().log('[JSJaCJingle] _peer_got_user_media_success > Waiting for local video to be loaded...', 2); - - var fn_loaded = function() { - self.get_debug().log('[JSJaCJingle] _peer_got_user_media_success > Local video loaded.', 2); - - this.removeEventListener('loadeddata', fn_loaded, false); - callback(); - }; - - self.get_local_view()[0].addEventListener('loadeddata', fn_loaded, false); - } else { - callback(); - } - } - } catch(e) { - self.get_debug().log('[JSJaCJingle] _peer_got_user_media_success > ' + e, 1); - } - }; - - /** - * @private - */ - self._peer_got_user_media_error = function(error) { - self.get_debug().log('[JSJaCJingle] _peer_got_user_media_error', 4); - - try { - (self._get_session_initiate_error())(self); - - // Not needed in case we are the responder (breaks termination) - if(self.is_initiator()) self.handle_session_initiate_error(); - - // Not needed in case we are the initiator (no packet sent, ever) - if(self.is_responder()) self.terminate(JSJAC_JINGLE_REASON_MEDIA_ERROR); - - self.get_debug().log('[JSJaCJingle] _peer_got_user_media_error > Failed (' + (error.PERMISSION_DENIED ? 'permission denied' : 'unknown' ) + ').', 1); - } catch(e) { - self.get_debug().log('[JSJaCJingle] _peer_got_user_media_error > ' + e, 1); - } - }; - - /** - * @private - */ - self._peer_got_description = function(sdp_local) { - self.get_debug().log('[JSJaCJingle] _peer_got_description', 4); - - try { - self.get_debug().log('[JSJaCJingle] _peer_got_description > Got local description.', 2); - - if(self.get_sdp_trace()) self.get_debug().log('[JSJaCJingle] SDP (local:raw)' + '\n\n' + sdp_local.sdp, 4); - - // Convert SDP raw data to an object - var cur_name; - var payload_parsed = self._util_sdp_parse_payload(sdp_local.sdp); - self._util_sdp_resolution_payload(payload_parsed); - - for(cur_name in payload_parsed) { - self._set_payloads_local( - cur_name, - payload_parsed[cur_name] - ); - } - - var cur_semantics; - var group_parsed = self._util_sdp_parse_group(sdp_local.sdp); - - for(cur_semantics in group_parsed) { - self._set_group_local( - cur_semantics, - group_parsed[cur_semantics] - ); - } - - // Filter our local description (remove unused medias) - var sdp_local_desc = self._util_sdp_generate_description( - sdp_local.type, - self._get_group_local(), - self._get_payloads_local(), - - self._util_sdp_generate_candidates( - self._get_candidates_local() - ) - ); - - if(self.get_sdp_trace()) self.get_debug().log('[JSJaCJingle] SDP (local:gen)' + '\n\n' + sdp_local_desc.sdp, 4); - - self._get_peer_connection().setLocalDescription( - (new WEBRTC_SESSION_DESCRIPTION(sdp_local_desc)), - - function() { - // Success (descriptions are compatible) - }, - - function(e) { - if(self.get_sdp_trace()) self.get_debug().log('[JSJaCJingle] SDP (local:error)' + '\n\n' + (e.message || e.name || 'Unknown error'), 4); - - // Error (descriptions are incompatible) - } - ); - - self.get_debug().log('[JSJaCJingle] _peer_got_description > Waiting for local candidates...', 2); - } catch(e) { - self.get_debug().log('[JSJaCJingle] _peer_got_description > ' + e, 1); - } - }; - - /** - * @private - */ - self._peer_fail_description = function() { - self.get_debug().log('[JSJaCJingle] _peer_fail_description', 4); - - try { - self.get_debug().log('[JSJaCJingle] _peer_fail_description > Could not get local description!', 1); - } catch(e) { - self.get_debug().log('[JSJaCJingle] _peer_fail_description > ' + e, 1); - } - }; - - /** - * @private - */ - self._peer_sound = function(enable) { - self.get_debug().log('[JSJaCJingle] _peer_sound', 4); - - try { - self.get_debug().log('[JSJaCJingle] _peer_sound > Enable: ' + enable + ' (current: ' + self.get_mute(JSJAC_JINGLE_MEDIA_AUDIO) + ').', 2); - - var i; - var audio_tracks = self._get_local_stream().getAudioTracks(); - - for(i = 0; i < audio_tracks.length; i++) - audio_tracks[i].enabled = enable; - } catch(e) { - self.get_debug().log('[JSJaCJingle] _peer_sound > ' + e, 1); - } - }; - - /** - * Set a timeout limit to peer connection - */ - self._peer_timeout = function(state, args) { - try { - // Assert - if(typeof args !== 'object') args = {}; - - var t_sid = self.get_sid(); - - setTimeout(function() { - // State did not change? - if(self.get_sid() == t_sid && self._get_peer_connection().iceConnectionState == state) { - self.get_debug().log('[JSJaCJingle] util_stanza_timeout > Peer timeout.', 2); - - // Error (transports are incompatible) - self.terminate(args.reason || JSJAC_JINGLE_REASON_FAILED_TRANSPORT); - } - }, ((args.timer || JSJAC_JINGLE_PEER_TIMEOUT_DEFAULT) * 1000)); - } catch(e) { - self.get_debug().log('[JSJaCJingle] _peer_timeout > ' + e, 1); - } - }; - - /** - * @private - */ - self._peer_stop = function() { - self.get_debug().log('[JSJaCJingle] _peer_stop', 4); - - // Detach media streams from DOM view - self._set_local_stream(null); - self._set_remote_stream(null); - - // Close the media stream - if(self._get_peer_connection()) - self._get_peer_connection().close(); - - // Remove this session from router - JSJaCJingle_remove(self.get_sid()); - }; -} - - - -/** - * Listens for Jingle events - */ -function JSJaCJingle_listen(args) { - try { - if(args && args.connection) - JSJAC_JINGLE_STORE_CONNECTION = args.connection; - - if(args && args.initiate) - JSJAC_JINGLE_STORE_INITIATE = args.initiate; - - if(args && args.debug) - JSJAC_JINGLE_STORE_DEBUG = args.debug; - - // Incoming IQs handler - JSJAC_JINGLE_STORE_CONNECTION.registerHandler('iq', JSJaCJingle_route); - - JSJAC_JINGLE_STORE_DEBUG.log('[JSJaCJingle] lib:listen > Listening.', 2); - - // Discover available network services - if(!args || args.extdisco !== false) - JSJaCJingle_extdisco(); - if(args.fallback && typeof args.fallback === 'string') - JSJaCJingle_fallback(args.fallback); - } catch(e) { - JSJAC_JINGLE_STORE_DEBUG.log('[JSJaCJingle] lib:listen > ' + e, 1); - } -} - -/** - * Routes Jingle stanzas - */ -function JSJaCJingle_route(stanza) { - try { - var action = null; - var sid = null; - - // Route the incoming stanza - var jingle = stanza.getChild('jingle', NS_JINGLE); - - if(jingle) { - sid = jingle.getAttribute('sid'); - action = jingle.getAttribute('action'); - } else { - var stanza_id = stanza.getID(); - - if(stanza_id) { - var is_jingle = stanza_id.indexOf(JSJAC_JINGLE_STANZA_ID_PRE + '_') !== -1; - - if(is_jingle) { - var stanza_id_split = stanza_id.split('_'); - sid = stanza_id_split[1]; - } - } - } - - // WebRTC not available ATM? - if(jingle && !JSJAC_JINGLE_AVAILABLE) { - JSJAC_JINGLE_STORE_DEBUG.log('[JSJaCJingle] lib:route > Dropped Jingle packet (WebRTC not available).', 0); - - (new JSJaCJingle({ to: stanza.getFrom() })).send_error(stanza, XMPP_ERROR_SERVICE_UNAVAILABLE); - } else { - // New session? Or registered one? - var session_route = JSJaCJingle_read(sid); - - if(action == JSJAC_JINGLE_ACTION_SESSION_INITIATE && session_route === null) { - JSJAC_JINGLE_STORE_DEBUG.log('[JSJaCJingle] lib:route > New Jingle session (sid: ' + sid + ').', 2); - - JSJAC_JINGLE_STORE_INITIATE(stanza); - } else if(sid) { - if(session_route !== null) { - JSJAC_JINGLE_STORE_DEBUG.log('[JSJaCJingle] lib:route > Routed to Jingle session (sid: ' + sid + ').', 2); - - session_route.handle(stanza); - } else if(stanza.getType() == JSJAC_JINGLE_STANZA_TYPE_SET && stanza.getFrom()) { - JSJAC_JINGLE_STORE_DEBUG.log('[JSJaCJingle] lib:route > Unknown Jingle session (sid: ' + sid + ').', 0); - - (new JSJaCJingle({ to: stanza.getFrom() })).send_error(stanza, JSJAC_JINGLE_ERROR_UNKNOWN_SESSION); - } - } - } - } catch(e) { - JSJAC_JINGLE_STORE_DEBUG.log('[JSJaCJingle] lib:route > ' + e, 1); - } -} - -/** - * Adds a new Jingle session - */ -function JSJaCJingle_add(sid, obj) { - JSJAC_JINGLE_STORE_SESSIONS[sid] = obj; -} - -/** - * Reads a new Jingle session - * @return Session - * @type object - */ -function JSJaCJingle_read(sid) { - return (sid in JSJAC_JINGLE_STORE_SESSIONS) ? JSJAC_JINGLE_STORE_SESSIONS[sid] : null; -} - -/** - * Removes a new Jingle session - */ -function JSJaCJingle_remove(sid) { - delete JSJAC_JINGLE_STORE_SESSIONS[sid]; -} - -/** - * Defer given task/execute deferred tasks - */ -function JSJaCJingle_defer(arg) { - try { - if(typeof arg == 'function') { - // Deferring? - if(JSJAC_JINGLE_STORE_DEFER.deferred) { - (JSJAC_JINGLE_STORE_DEFER.fn).push(arg); - - JSJAC_JINGLE_STORE_DEBUG.log('[JSJaCJingle] lib:defer > Registered a function to be executed once ready.', 2); - } - - return JSJAC_JINGLE_STORE_DEFER.deferred; - } else if(!arg || typeof arg == 'boolean') { - JSJAC_JINGLE_STORE_DEFER.deferred = (arg === true); - - if(JSJAC_JINGLE_STORE_DEFER.deferred === false) { - // Execute deferred tasks? - if((--JSJAC_JINGLE_STORE_DEFER.count) <= 0) { - JSJAC_JINGLE_STORE_DEFER.count = 0; - - JSJAC_JINGLE_STORE_DEBUG.log('[JSJaCJingle] lib:defer > Executing ' + JSJAC_JINGLE_STORE_DEFER.fn.length + ' deferred functions...', 2); - - while(JSJAC_JINGLE_STORE_DEFER.fn.length) - ((JSJAC_JINGLE_STORE_DEFER.fn).shift())(); - - JSJAC_JINGLE_STORE_DEBUG.log('[JSJaCJingle] lib:defer > Done executing deferred functions.', 2); - } - } else { - ++JSJAC_JINGLE_STORE_DEFER.count; - } - } - } catch(e) { - JSJAC_JINGLE_STORE_DEBUG.log('[JSJaCJingle] lib:defer > ' + e, 1); - } -} - -/** - * Maps the Jingle disco features - * @return Feature namespaces - * @type array - */ -function JSJaCJingle_disco() { - return JSJAC_JINGLE_AVAILABLE ? MAP_DISCO_JINGLE : []; -} - -/** - * Query the server for external services - */ -function JSJaCJingle_extdisco() { - JSJAC_JINGLE_STORE_DEBUG.log('[JSJaCJingle] lib:extdisco > Discovering available services...', 2); - - try { - // Pending state (defer other requests) - JSJaCJingle_defer(true); - - // Build request - var request = new JSJaCIQ(); - - request.setTo(JSJAC_JINGLE_STORE_CONNECTION.domain); - request.setType(JSJAC_JINGLE_STANZA_TYPE_GET); - - request.getNode().appendChild(request.buildNode('services', { 'xmlns': NS_EXTDISCO })); - - JSJAC_JINGLE_STORE_CONNECTION.send(request, function(response) { try { - // Parse response - if(response.getType() == JSJAC_JINGLE_STANZA_TYPE_RESULT) { - var i, - service_arr, cur_service, - cur_host, cur_password, cur_port, cur_transport, cur_type, cur_username; + /* @function */ + (this.get_session_initiate_error())(this); - var services = response.getChild('services', NS_EXTDISCO); + // Not needed in case we are the responder (breaks termination) + if(this.is_initiator()) this._handle_session_initiate_error(); - if(services) { - service_arr = services.getElementsByTagNameNS(NS_EXTDISCO, 'service'); + // Not needed in case we are the initiator (no packet sent, ever) + if(this.is_responder()) this.terminate(JSJAC_JINGLE_REASON_MEDIA_ERROR); - for(i = 0; i < service_arr.length; i++) { - cur_service = service_arr[i]; + this.get_debug().log('[JSJaCJingle:single] _peer_got_user_media_error > Failed (' + (error.PERMISSION_DENIED ? 'permission denied' : 'unknown' ) + ').', 1); + } catch(e) { + this.get_debug().log('[JSJaCJingle:single] _peer_got_user_media_error > ' + e, 1); + } + }, - cur_host = cur_service.getAttribute('host') || null; - cur_port = cur_service.getAttribute('port') || null; - cur_transport = cur_service.getAttribute('transport') || null; - cur_type = cur_service.getAttribute('type') || null; + /** + * Set a timeout limit to peer connection + * @private + * @param {String} state + * @param {Object} [args] + */ + _peer_timeout: function(state, args) { + try { + // Assert + if(typeof args !== 'object') args = {}; - cur_username = cur_service.getAttribute('username') || null; - cur_password = cur_service.getAttribute('password') || null; + var t_sid = this.get_sid(); - if(!cur_host || !cur_type) continue; + var _this = this; - if(!(cur_type in JSJAC_JINGLE_STORE_EXTDISCO)) { - JSJAC_JINGLE_STORE_DEBUG.log('[JSJaCJingle] lib:extdisco > handle > Service skipped (type: ' + cur_type + ', host: ' + cur_host + ', port: ' + cur_port + ', transport: ' + cur_transport + ').', 4); - continue; - } + setTimeout(function() { + // State did not change? + if(_this.get_sid() == t_sid && _this.get_peer_connection().iceConnectionState == state) { + _this.get_debug().log('[JSJaCJingle:single] _peer_timeout > Peer timeout.', 2); - JSJAC_JINGLE_STORE_EXTDISCO[cur_type][cur_host] = { - 'port' : cur_port, - 'transport' : cur_transport, - 'type' : cur_type - }; - - if(cur_type == 'turn') { - JSJAC_JINGLE_STORE_EXTDISCO[cur_type][cur_host].username = cur_username; - JSJAC_JINGLE_STORE_EXTDISCO[cur_type][cur_host].password = cur_password; - } - - JSJAC_JINGLE_STORE_DEBUG.log('[JSJaCJingle] lib:extdisco > handle > Service stored (type: ' + cur_type + ', host: ' + cur_host + ', port: ' + cur_port + ', transport: ' + cur_transport + ').', 4); - } + // Error (transports are incompatible) + _this.terminate(args.reason || JSJAC_JINGLE_REASON_FAILED_TRANSPORT); } + }, ((args.timer || JSJAC_JINGLE_PEER_TIMEOUT_DEFAULT) * 1000)); + } catch(e) { + this.get_debug().log('[JSJaCJingle:single] _peer_timeout > ' + e, 1); + } + }, - JSJAC_JINGLE_STORE_DEBUG.log('[JSJaCJingle] lib:extdisco > handle > Discovered available services.', 2); + /** + * Stops ongoing peer connections + * @private + */ + _peer_stop: function() { + this.get_debug().log('[JSJaCJingle:single] _peer_stop', 4); + + // Detach media streams from DOM view + this._set_local_stream(null); + this._set_remote_stream(null); + + // Close the media stream + if(this.get_peer_connection() && + (typeof this.get_peer_connection().close == 'function')) + this.get_peer_connection().close(); + + // Remove this session from router + JSJaCJingle._remove(JSJAC_JINGLE_SESSION_SINGLE, this.get_sid()); + }, + + + + /** + * JSJSAC JINGLE SHORTCUTS + */ + + /** + * Returns local user candidates + * @private + * @returns {Object} Candidates + */ + _shortcut_local_user_candidates: function() { + return this.get_candidates_local(); + }, + + + + /** + * JSJSAC JINGLE GETTERS + */ + + /** + * Gets the session initiate pending callback function + * @public + * @event JSJaCJingleSingle#get_session_initiate_pending + * @returns {Function} Callback function + */ + get_session_initiate_pending: function() { + return this._shortcut_get_handler( + this._session_initiate_pending + ); + }, + + /** + * Gets the session initiate success callback function + * @public + * @event JSJaCJingleSingle#get_session_initiate_success + * @returns {Function} Callback function + */ + get_session_initiate_success: function() { + return this._shortcut_get_handler( + this._session_initiate_success + ); + }, + + /** + * Gets the session initiate error callback function + * @public + * @event JSJaCJingleSingle#get_session_initiate_error + * @returns {Function} Callback function + */ + get_session_initiate_error: function() { + return this._shortcut_get_handler( + this._session_initiate_error + ); + }, + + /** + * Gets the session initiate request callback function + * @public + * @event JSJaCJingleSingle#get_session_initiate_request + * @returns {Function} Callback function + */ + get_session_initiate_request: function() { + return this._shortcut_get_handler( + this._session_initiate_request + ); + }, + + /** + * Gets the session accept pending callback function + * @public + * @event JSJaCJingleSingle#get_session_accept_pending + * @returns {Function} Callback function + */ + get_session_accept_pending: function() { + return this._shortcut_get_handler( + this._session_accept_pending + ); + }, + + /** + * Gets the session accept success callback function + * @public + * @event JSJaCJingleSingle#get_session_accept_success + * @returns {Function} Callback function + */ + get_session_accept_success: function() { + return this._shortcut_get_handler( + this._session_accept_success + ); + }, + + /** + * Gets the session accept error callback function + * @public + * @event JSJaCJingleSingle#get_session_accept_error + * @returns {Function} Callback function + */ + get_session_accept_error: function() { + return this._shortcut_get_handler( + this._session_accept_error + ); + }, + + /** + * Gets the session accept request callback function + * @public + * @event JSJaCJingleSingle#get_session_accept_request + * @returns {Function} Callback function + */ + get_session_accept_request: function() { + return this._shortcut_get_handler( + this._session_accept_request + ); + }, + + /** + * Gets the session info pending callback function + * @public + * @event JSJaCJingleSingle#get_session_info_pending + * @returns {Function} Callback function + */ + get_session_info_pending: function() { + return this._shortcut_get_handler( + this._session_info_pending + ); + }, + + /** + * Gets the session info success callback function + * @public + * @event JSJaCJingleSingle#get_session_info_success + * @returns {Function} Callback function + */ + get_session_info_success: function() { + return this._shortcut_get_handler( + this._session_info_success + ); + }, + + /** + * Gets the session info error callback function + * @public + * @event JSJaCJingleSingle#get_session_info_error + * @returns {Function} Callback function + */ + get_session_info_error: function() { + return this._shortcut_get_handler( + this._session_info_error + ); + }, + + /** + * Gets the session info request callback function + * @public + * @event JSJaCJingleSingle#get_session_info_request + * @returns {Function} Callback function + */ + get_session_info_request: function() { + return this._shortcut_get_handler( + this._session_info_request + ); + }, + + /** + * Gets the session terminate pending callback function + * @public + * @event JSJaCJingleSingle#get_session_terminate_pending + * @returns {Function} Callback function + */ + get_session_terminate_pending: function() { + return this._shortcut_get_handler( + this._session_terminate_pending + ); + }, + + /** + * Gets the session terminate success callback function + * @public + * @event JSJaCJingleSingle#get_session_terminate_success + * @returns {Function} Callback function + */ + get_session_terminate_success: function() { + return this._shortcut_get_handler( + this._session_terminate_success + ); + }, + + /** + * Gets the session terminate error callback function + * @public + * @event JSJaCJingleSingle#get_session_terminate_error + * @returns {Function} Callback function + */ + get_session_terminate_error: function() { + return this._shortcut_get_handler( + this._session_terminate_error + ); + }, + + /** + * Gets the session terminate request callback function + * @public + * @event JSJaCJingleSingle#get_session_terminate_request + * @returns {Function} Callback function + */ + get_session_terminate_request: function() { + return this._shortcut_get_handler( + this._session_terminate_request + ); + }, + + /** + * Gets the prepended ID + * @public + * @returns {String} Prepended ID value + */ + get_id_pre: function() { + return JSJAC_JINGLE_STANZA_ID_PRE + '_' + (this.get_sid() || '0') + '_'; + }, + + /** + * Gets the reason value + * @public + * @returns {String} Reason value + */ + get_reason: function() { + return this._reason; + }, + + /** + * Gets the remote_view value + * @public + * @returns {DOM} Remote view + */ + get_remote_view: function() { + return this._remote_view; + }, + + /** + * Gets the remote stream + * @public + * @returns {Object} Remote stream instance + */ + get_remote_stream: function() { + return this._remote_stream; + }, + + /** + * Gets the remote content + * @public + * @param {String} [name] + * @returns {Object} Remote content object + */ + get_content_remote: function(name) { + if(name) + return (name in this._content_remote) ? this._content_remote[name] : {}; + + return this._content_remote; + }, + + /** + * Gets the remote payloads + * @public + * @param {String} [name] + * @returns {Object} Remote payloads object + */ + get_payloads_remote: function(name) { + if(name) + return (name in this._payloads_remote) ? this._payloads_remote[name] : {}; + + return this._payloads_remote; + }, + + /** + * Gets the remote group + * @public + * @param {String} [semantics] + * @returns {Object} Remote group object + */ + get_group_remote: function(semantics) { + if(semantics) + return (semantics in this._group_remote) ? this._group_remote[semantics] : {}; + + return this._group_remote; + }, + + /** + * Gets the remote candidates + * @public + * @param {String} [name] + * @returns {Object} Remote candidates object + */ + get_candidates_remote: function(name) { + if(name) + return (name in this._candidates_remote) ? this._candidates_remote[name] : []; + + return this._candidates_remote; + }, + + /** + * Gets the remote candidates queue + * @public + * @param {String} [name] + * @returns {Object} Remote candidates queue object + */ + get_candidates_queue_remote: function(name) { + if(name) + return (name in this._candidates_queue_remote) ? this._candidates_queue_remote[name] : {}; + + return this._candidates_queue_remote; + }, + + + + /** + * JSJSAC JINGLE SETTERS + */ + + /** + * Sets the session initiate pending callback function + * @private + * @param {Function} session_initiate_pending + */ + _set_session_initiate_pending: function(session_initiate_pending) { + this._session_initiate_pending = session_initiate_pending; + }, + + /** + * Sets the session initiate success callback function + * @private + * @param {Function} initiate_success + */ + _set_session_initiate_success: function(initiate_success) { + this._session_initiate_success = initiate_success; + }, + + /** + * Sets the session initiate error callback function + * @private + * @param {Function} initiate_error + */ + _set_session_initiate_error: function(initiate_error) { + this._session_initiate_error = initiate_error; + }, + + /** + * Sets the session initiate request callback function + * @private + * @param {Function} initiate_request + */ + _set_session_initiate_request: function(initiate_request) { + this._session_initiate_request = initiate_request; + }, + + /** + * Sets the session accept pending callback function + * @private + * @param {Function} accept_pending + */ + _set_session_accept_pending: function(accept_pending) { + this._session_accept_pending = accept_pending; + }, + + /** + * Sets the session accept success callback function + * @private + * @param {Function} accept_success + */ + _set_session_accept_success: function(accept_success) { + this._session_accept_success = accept_success; + }, + + /** + * Sets the session accept error callback function + * @private + * @param {Function} accept_error + */ + _set_session_accept_error: function(accept_error) { + this._session_accept_error = accept_error; + }, + + /** + * Sets the session accept request callback function + * @private + * @param {Function} accept_request + */ + _set_session_accept_request: function(accept_request) { + this._session_accept_request = accept_request; + }, + + /** + * Sets the session info pending callback function + * @private + * @param {Function} info_pending + */ + _set_session_info_pending: function(info_pending) { + this._session_info_pending = info_pending; + }, + + /** + * Sets the session info success callback function + * @private + * @param {Function} info_success + */ + _set_session_info_success: function(info_success) { + this._session_info_success = info_success; + }, + + /** + * Sets the session info error callback function + * @private + * @param {Function} info_error + */ + _set_session_info_error: function(info_error) { + this._session_info_error = info_error; + }, + + /** + * Sets the session info request callback function + * @private + * @param {Function} info_request + */ + _set_session_info_request: function(info_request) { + this._session_info_request = info_request; + }, + + /** + * Sets the session terminate pending callback function + * @private + * @param {Function} terminate_pending + */ + _set_session_terminate_pending: function(terminate_pending) { + this._session_terminate_pending = terminate_pending; + }, + + /** + * Sets the session terminate success callback function + * @private + * @param {Function} terminate_success + */ + _set_session_terminate_success: function(terminate_success) { + this._session_terminate_success = terminate_success; + }, + + /** + * Sets the session terminate error callback function + * @private + * @param {Function} terminate_error + */ + _set_session_terminate_error: function(terminate_error) { + this._session_terminate_error = terminate_error; + }, + + /** + * Sets the session terminate request callback function + * @private + * @param {Function} terminate_request + */ + _set_session_terminate_request: function(terminate_request) { + this._session_terminate_request = terminate_request; + }, + + /** + * Sets the termination reason + * @private + * @param {String} reason + */ + _set_reason: function(reason) { + this._reason = reason || JSJAC_JINGLE_REASON_CANCEL; + }, + + /** + * Sets the remote stream + * @private + * @param {DOM} [remote_stream] + */ + _set_remote_stream: function(remote_stream) { + try { + if(!remote_stream && this._remote_stream !== null) { + this._peer_stream_detach( + this.get_remote_view() + ); + } + + if(remote_stream) { + this._remote_stream = remote_stream; + + this._peer_stream_attach( + this.get_remote_view(), + this.get_remote_stream(), + false + ); } else { - JSJAC_JINGLE_STORE_DEBUG.log('[JSJaCJingle] lib:extdisco > handle > Could not discover services (server might not support XEP-0215).', 0); + this._remote_stream = null; + + this._peer_stream_detach( + this.get_remote_view() + ); } } catch(e) { - JSJAC_JINGLE_STORE_DEBUG.log('[JSJaCJingle] lib:extdisco > handle > ' + e, 1); + this.get_debug().log('[JSJaCJingle:single] _set_remote_stream > ' + e, 1); } + }, - JSJAC_JINGLE_STORE_DEBUG.log('[JSJaCJingle] lib:extdisco > Ready.', 2); + /** + * Sets the remote view + * @private + * @param {DOM} [remote_view] + */ + _set_remote_view: function(remote_view) { + if(typeof this._remote_view !== 'object') + this._remote_view = []; - // Execute deferred requests - JSJaCJingle_defer(false); - }); - } catch(e) { - JSJAC_JINGLE_STORE_DEBUG.log('[JSJaCJingle] lib:extdisco > ' + e, 1); - - // Execute deferred requests - JSJaCJingle_defer(false); + this._remote_view.push(remote_view); + }, + + /** + * Sets the remote content + * @private + * @param {String} name + * @param {Object} content_remote + */ + _set_content_remote: function(name, content_remote) { + this._content_remote[name] = content_remote; + }, + + /** + * Sets the remote payloads + * @private + * @param {String} name + * @param {Object} payload_data + */ + _set_payloads_remote: function(name, payload_data) { + this._payloads_remote[name] = payload_data; + }, + + /** + * Adds a remote payload + * @private + * @param {String} name + * @param {Object} payload_data + */ + _set_payloads_remote_add: function(name, payload_data) { + try { + if(!(name in this._payloads_remote)) { + this._set_payloads_remote(name, payload_data); + } else { + var key; + var payloads_store = this._payloads_remote[name].descriptions.payload; + var payloads_add = payload_data.descriptions.payload; + + for(key in payloads_add) { + if(!(key in payloads_store)) + payloads_store[key] = payloads_add[key]; + } + } + } catch(e) { + this.get_debug().log('[JSJaCJingle:single] _set_payloads_remote_add > ' + e, 1); + } + }, + + /** + * Sets the remote group + * @private + * @param {String} semantics + * @param {Object} group_data + */ + _set_group_remote: function(semantics, group_data) { + this._group_remote[semantics] = group_data; + }, + + /** + * Sets the remote candidates + * @private + * @param {String} name + * @param {Object} candidate_data + */ + _set_candidates_remote: function(name, candidate_data) { + this._candidates_remote[name] = candidate_data; + }, + + /** + * Sets the session initiate pending callback function + * @private + * @param {String} name + * @param {Object} candidate_data + */ + _set_candidates_queue_remote: function(name, candidate_data) { + if(name === null) + this._candidates_queue_remote = {}; + else + this._candidates_queue_remote[name] = (candidate_data); + }, + + /** + * Adds a remote candidate + * @private + * @param {String} name + * @param {Object} candidate_data + */ + _set_candidates_remote_add: function(name, candidate_data) { + try { + if(!name) return; + + if(!(name in this._candidates_remote)) + this._set_candidates_remote(name, []); + + var c, i; + var candidate_ids = []; + + for(c in this.get_candidates_remote(name)) + candidate_ids.push(this.get_candidates_remote(name)[c].id); + + for(i in candidate_data) { + if((candidate_data[i].id).indexOf(candidate_ids) !== -1) + this.get_candidates_remote(name).push(candidate_data[i]); + } + } catch(e) { + this.get_debug().log('[JSJaCJingle:single] _set_candidates_remote_add > ' + e, 1); + } + }, } -} +); +/** + * @fileoverview JSJaC Jingle library - Multi-user call lib + * + * @url https://github.com/valeriansaliou/jsjac-jingle + * @depends https://github.com/sstrigler/JSJaC + * @author Valérian Saliou https://valeriansaliou.name/ + * @license Mozilla Public License v2.0 (MPL v2.0) + */ + + +/** @module jsjac-jingle/muji */ +/** @exports JSJaCJingleMuji */ + /** - * Query some external APIs for fallback STUN/TURN (must be configured) + * Creates a new XMPP Jingle Muji session. + * @class + * @classdesc Creates a new XMPP Jingle Muji session. + * @augments __JSJaCJingleBase + * @requires nicolas-van/ring.js + * @requires sstrigler/JSJaC + * @requires jsjac-jingle/main + * @requires jsjac-jingle/base + * @see {@link http://xmpp.org/extensions/xep-0272.html|XEP-0272: Multiparty Jingle (Muji)} + * @see {@link http://ringjs.neoname.eu/|Ring.js} + * @see {@link http://stefan-strigler.de/jsjac-1.3.4/doc/|JSJaC Documentation} + * @param {Object} [args] - Muji session arguments. + * @property {*} [args.*] - Herits of JSJaCJingle() baseclass prototype. + * @property {String} [args.username] - The username when joining room. + * @property {String} [args.password] - The room password. + * @property {Boolean} [args.password_protect] - Automatically password-protect the MUC if first joiner. + * @property {Function} [args.room_message_in] - The incoming message custom handler. + * @property {Function} [args.room_message_out] - The outgoing message custom handler. + * @property {Function} [args.room_presence_in] - The incoming presence custom handler. + * @property {Function} [args.room_presence_out] - The outgoing presence custom handler. + * @property {Function} [args.session_prepare_pending] - The session prepare pending custom handler. + * @property {Function} [args.session_prepare_success] - The session prepare success custom handler. + * @property {Function} [args.session_prepare_error] - The session prepare error custom handler. + * @property {Function} [args.session_initiate_pending] - The session initiate pending custom handler. + * @property {Function} [args.session_initiate_success] - The session initiate success custom handler. + * @property {Function} [args.session_initiate_error] - The session initiate error custom handler. + * @property {Function} [args.session_leave_pending] - The session leave pending custom handler. + * @property {Function} [args.session_leave_success] - The session leave success custom handler. + * @property {Function} [args.session_leave_error] - The session leave error custom handler. + * @property {Function} [args.participant_prepare] - The participant prepare custom handler. + * @property {Function} [args.participant_initiate] - The participant initiate custom handler. + * @property {Function} [args.participant_leave] - The participant session leave custom handler. + * @property {Function} [args.participant_session_initiate_pending] - The participant session initiate pending custom handler. + * @property {Function} [args.participant_session_initiate_success] - The participant session initiate success custom handler. + * @property {Function} [args.participant_session_initiate_error] - The participant session initiate error custom handler. + * @property {Function} [args.participant_session_initiate_request] - The participant session initiate request custom handler. + * @property {Function} [args.participant_session_accept_pending] - The participant session accept pending custom handler. + * @property {Function} [args.participant_session_accept_success] - The participant session accept success custom handler. + * @property {Function} [args.participant_session_accept_error] - The participant session accept error custom handler. + * @property {Function} [args.participant_session_accept_request] - The participant session accept request custom handler. + * @property {Function} [args.participant_session_info_pending] - The participant session info request custom handler. + * @property {Function} [args.participant_session_info_success] - The participant session info success custom handler. + * @property {Function} [args.participant_session_info_error] - The participant session info error custom handler. + * @property {Function} [args.participant_session_info_request] - The participant session info request custom handler. + * @property {Function} [args.participant_session_terminate_pending] - The participant session terminate pending custom handler. + * @property {Function} [args.participant_session_terminate_success] - The participant session terminate success custom handler. + * @property {Function} [args.participant_session_terminate_error] - The participant session terminate error custom handler. + * @property {Function} [args.participant_session_terminate_request] - The participant session terminate request custom handler. + * @property {Function} [args.add_remote_view] - The remote view media add (audio/video) custom handler. + * @property {Function} [args.remove_remote_view] - The remote view media removal (audio/video) custom handler. */ -function JSJaCJingle_fallback(fallback_url) { - JSJAC_JINGLE_STORE_DEBUG.log('[JSJaCJingle] lib:fallback > Discovering fallback services...', 2); +var JSJaCJingleMuji = ring.create([__JSJaCJingleBase], + /** @lends JSJaCJingleMuji.prototype */ + { + /** + * Constructor + */ + constructor: function(args) { + this.$super(args); - try { - // Pending state (defer other requests) - JSJaCJingle_defer(true); + if(args && args.room_message_in) + /** + * @member {Function} + * @default + * @private + */ + this._room_message_in = args.room_message_in; - // Generate fallback API URL - fallback_url += '?username=' + - encodeURIComponent(JSJAC_JINGLE_STORE_CONNECTION.username + '@' + JSJAC_JINGLE_STORE_CONNECTION.domain); + if(args && args.room_message_out) + /** + * @member {Function} + * @default + * @private + */ + this._room_message_out = args.room_message_out; - // Proceed request - var xhr = new XMLHttpRequest(); - xhr.open('GET', fallback_url, true); + if(args && args.room_presence_in) + /** + * @member {Function} + * @default + * @private + */ + this._room_presence_in = args.room_presence_in; - xhr.onreadystatechange = function() { - if(xhr.readyState === 4) { - // Success? - if(xhr.status === 200) { - var data = JSON.parse(xhr.responseText); + if(args && args.room_presence_out) + /** + * @member {Function} + * @default + * @private + */ + this._room_presence_out = args.room_presence_out; - var cur_parse, - i, cur_url, - cur_type, cur_host, cur_port, cur_transport, - cur_username, cur_password; + if(args && args.session_prepare_pending) + /** + * @member {Function} + * @default + * @private + */ + this._session_prepare_pending = args.session_prepare_pending; - if(data.uris && data.uris.length) { - JSJAC_JINGLE_STORE_DEBUG.log('[JSJaCJingle] lib:fallback > handle > Parsing ' + data.uris.length + ' URIs...', 2); + if(args && args.session_prepare_success) + /** + * @member {Function} + * @default + * @private + */ + this._session_prepare_success = args.session_prepare_success; - for(i in data.uris) { - cur_url = data.uris[i]; + if(args && args.session_prepare_error) + /** + * @member {Function} + * @default + * @private + */ + this._session_prepare_error = args.session_prepare_error; - if(cur_url) { - // Parse current URL - cur_parse = R_JSJAC_JINGLE_SERVICE_URI.exec(cur_url); + if(args && args.session_initiate_pending) + /** + * @member {Function} + * @default + * @private + */ + this._session_initiate_pending = args.session_initiate_pending; - if(cur_parse) { - cur_type = cur_parse[1] || null; - cur_host = cur_parse[2] || null; - cur_port = cur_parse[3] || null; - cur_transport = cur_parse[4] || null; + if(args && args.session_initiate_success) + /** + * @member {Function} + * @default + * @private + */ + this._session_initiate_success = args.session_initiate_success; - cur_username = data.username || null; - cur_password = data.password || null; + if(args && args.session_initiate_error) + /** + * @member {Function} + * @default + * @private + */ + this._session_initiate_error = args.session_initiate_error; + + if(args && args.session_leave_pending) + /** + * @member {Function} + * @default + * @private + */ + this._session_leave_pending = args.session_leave_pending; + + if(args && args.session_leave_success) + /** + * @member {Function} + * @default + * @private + */ + this._session_leave_success = args.session_leave_success; + + if(args && args.session_leave_error) + /** + * @member {Function} + * @default + * @private + */ + this._session_leave_error = args.session_leave_error; + + if(args && args.participant_prepare) + /** + * @member {Function} + * @default + * @private + */ + this._participant_prepare = args.participant_prepare; + + if(args && args.participant_initiate) + /** + * @member {Function} + * @default + * @private + */ + this._participant_initiate = args.participant_initiate; + + if(args && args.participant_leave) + /** + * @member {Function} + * @default + * @private + */ + this._participant_leave = args.participant_leave; + + if(args && args.participant_session_initiate_pending) + /** + * @member {Function} + * @default + * @private + */ + this._participant_session_initiate_pending = args.participant_session_initiate_pending; + + if(args && args.participant_session_initiate_success) + /** + * @member {Function} + * @default + * @private + */ + this._participant_session_initiate_success = args.participant_session_initiate_success; + + if(args && args.participant_session_initiate_error) + /** + * @member {Function} + * @default + * @private + */ + this._participant_session_initiate_error = args.participant_session_initiate_error; + + if(args && args.participant_session_initiate_request) + /** + * @member {Function} + * @default + * @private + */ + this._participant_session_initiate_request = args.participant_session_initiate_request; + + if(args && args.participant_session_accept_pending) + /** + * @member {Function} + * @default + * @private + */ + this._participant_session_accept_pending = args.participant_session_accept_pending; + + if(args && args.participant_session_accept_success) + /** + * @member {Function} + * @default + * @private + */ + this._participant_session_accept_success = args.participant_session_accept_success; + + if(args && args.participant_session_accept_error) + /** + * @member {Function} + * @default + * @private + */ + this._participant_session_accept_error = args.participant_session_accept_error; + + if(args && args.participant_session_accept_request) + /** + * @member {Function} + * @default + * @private + */ + this._participant_session_accept_request = args.participant_session_accept_request; + + if(args && args.participant_session_info_pending) + /** + * @member {Function} + * @default + * @private + */ + this._participant_session_info_pending = args.participant_session_info_pending; + + if(args && args.participant_session_info_success) + /** + * @member {Function} + * @default + * @private + */ + this._participant_session_info_success = args.participant_session_info_success; + + if(args && args.participant_session_info_error) + /** + * @member {Function} + * @default + * @private + */ + this._participant_session_info_error = args.participant_session_info_error; + + if(args && args.participant_session_info_request) + /** + * @member {Function} + * @default + * @private + */ + this._participant_session_info_request = args.participant_session_info_request; + + if(args && args.participant_session_terminate_pending) + /** + * @member {Function} + * @default + * @private + */ + this._participant_session_terminate_pending = args.participant_session_terminate_pending; + + if(args && args.participant_session_terminate_success) + /** + * @member {Function} + * @default + * @private + */ + this._participant_session_terminate_success = args.participant_session_terminate_success; + + if(args && args.participant_session_terminate_error) + /** + * @member {Function} + * @default + * @private + */ + this._participant_session_terminate_error = args.participant_session_terminate_error; + + if(args && args.participant_session_terminate_request) + /** + * @member {Function} + * @default + * @private + */ + this._participant_session_terminate_request = args.participant_session_terminate_request; + + if(args && args.add_remote_view) + /** + * @member {Function} + * @default + * @private + */ + this._add_remote_view = args.add_remote_view; + + if(args && args.remove_remote_view) + /** + * @member {Function} + * @default + * @private + */ + this._remove_remote_view = args.remove_remote_view; + + if(args && args.username) { + /** + * @member {String} + * @default + * @private + */ + this._username = args.username; + } else { + /** + * @member {String} + * @default + * @private + */ + this._username = this.utils.connection_username(); + } + + if(args && args.password) + /** + * @member {String} + * @default + * @private + */ + this._password = args.password; + + if(args && args.password_protect) + /** + * @member {Boolean} + * @default + * @private + */ + this._password_protect = args.password_protect; + + /** + * @member {Object} + * @default + * @private + */ + this._participants = {}; + + /** + * @member {String} + * @default + * @private + */ + this._iid = ''; + + /** + * @member {Boolean} + * @default + * @private + */ + this._is_room_owner = false; + + /** + * @constant + * @member {String} + * @default + * @private + */ + this._status = JSJAC_JINGLE_MUJI_STATUS_INACTIVE; + + /** + * @constant + * @member {String} + * @default + * @private + */ + this._namespace = NS_MUJI; + }, + + + /** + * Initiates a new Muji session. + * @public + * @fires JSJaCJingleMuji#get_session_initiate_pending + */ + join: function() { + this.get_debug().log('[JSJaCJingle:muji] join', 4); + + try { + // Locked? + if(this.get_lock()) { + this.get_debug().log('[JSJaCJingle:muji] join > Cannot join, resource locked. Please open another session or check WebRTC support.', 0); + return; + } + + // Defer? + var _this = this; + + if(JSJaCJingle._defer(function() { _this.join(); })) { + this.get_debug().log('[JSJaCJingle:muji] join > Deferred (waiting for the library components to be initiated).', 0); + return; + } + + // Slot unavailable? + if(this.get_status() !== JSJAC_JINGLE_STATUS_INACTIVE) { + this.get_debug().log('[JSJaCJingle:muji] join > Cannot join, resource not inactive (status: ' + this.get_status() + ').', 0); + return; + } + + this.get_debug().log('[JSJaCJingle:muji] join > New Jingle Muji session with media: ' + this.get_media(), 2); + + // Common vars + var i, cur_name; + + // Trigger session prepare pending custom callback + /* @function */ + (this.get_session_prepare_pending())(this); + + // Change session status + this._set_status(JSJAC_JINGLE_MUJI_STATUS_PREPARING); + + // Set session values + this._set_iid(this.utils.generate_iid()); + this._set_sid( + this.utils.generate_hash_md5(this.get_to()) + ); + + this._set_initiator(this.get_to()); + this._set_responder(this.utils.connection_jid()); + + for(i in this.get_media_all()) { + cur_name = this.utils.name_generate( + this.get_media_all()[i] + ); + + this._set_name(cur_name); + + this._set_senders( + cur_name, + JSJAC_JINGLE_SENDERS_BOTH.jingle + ); + + this._set_creator( + cur_name, + JSJAC_JINGLE_CREATOR_INITIATOR + ); + } + + // Register session to common router + JSJaCJingle._add(JSJAC_JINGLE_SESSION_MUJI, this.get_to(), this); + + // Send initial join presence + this.send_presence({ action: JSJAC_JINGLE_MUJI_ACTION_PREPARE }); + } catch(e) { + this.get_debug().log('[JSJaCJingle:muji] join > ' + e, 1); + } + }, + + + /** + * Leaves current Muji session. + * @public + * @fires JSJaCJingleMuji#get_session_leave_pending + */ + leave: function() { + this.get_debug().log('[JSJaCJingle:muji] leave', 4); + + try { + // Locked? + if(this.get_lock()) { + this.get_debug().log('[JSJaCJingle:muji] leave > Cannot leave, resource locked. Please open another session or check WebRTC support.', 0); + return; + } + + // Defer? + var _this = this; + + if(JSJaCJingle._defer(function() { _this.leave(); })) { + this.get_debug().log('[JSJaCJingle:muji] leave > Deferred (waiting for the library components to be initiated).', 0); + return; + } + + // Slot unavailable? + if(this.get_status() === JSJAC_JINGLE_MUJI_STATUS_LEFT) { + this.get_debug().log('[JSJaCJingle:muji] leave > Cannot terminate, resource already terminated (status: ' + this.get_status() + ').', 0); + return; + } + + // Change session status + this._set_status(JSJAC_JINGLE_MUJI_STATUS_LEAVING); + + // Trigger session leave pending custom callback + /* @function */ + (this.get_session_leave_pending())(this); + + // Leave the room (after properly terminating participant sessions) + this._terminate_participant_sessions(true, function() { + _this.send_presence({ action: JSJAC_JINGLE_MUJI_ACTION_LEAVE }); + }); + } catch(e) { + this.get_debug().log('[JSJaCJingle:muji] leave > ' + e, 1); + } + }, + + /** + * Aborts current Muji session. + * @public + * @param {Boolean} [set_lock] + */ + abort: function(set_lock) { + this.get_debug().log('[JSJaCJingle:muji] abort', 4); + + try { + // Change session status + this._set_status(JSJAC_JINGLE_MUJI_STATUS_LEFT); + + // Stop WebRTC + this._peer_stop(); + + // Flush all participant content + this._set_participants(null); + + // Lock session? (cannot be used later) + if(set_lock === true) this._set_lock(true); + } catch(e) { + this.get_debug().log('[JSJaCJingle:muji] abort > ' + e, 1); + } + }, + + /** + * Invites people to current Muji session + * @public + * @param {String|Array} jid + * @param {String} [reason] + */ + invite: function(jid, reason) { + this.get_debug().log('[JSJaCJingle:muji] invite', 4); + + try { + // Locked? + if(this.get_lock()) { + this.get_debug().log('[JSJaCJingle:muji] invite > Cannot invite, resource locked. Please open another session or check WebRTC support.', 0); + return; + } + + // Defer? + var _this = this; + + if(JSJaCJingle._defer(function() { _this.invite(jid); })) { + this.get_debug().log('[JSJaCJingle:muji] invite > Deferred (waiting for the library components to be initiated).', 0); + return; + } + + if(!jid) { + this.get_debug().log('[JSJaCJingle:muji] invite > JID parameter not provided or blank.', 0); + return; + } + + var i; + jid_arr = (jid instanceof Array) ? jid : [jid]; + + for(i = 0; i < jid_arr.length; i++) this._send_invite(jid_arr[i], reason); + } catch(e) { + this.get_debug().log('[JSJaCJingle:muji] invite > ' + e, 1); + } + }, + + /** + * Mutes a Muji session (local) + * @public + * @param {String} name + */ + mute: function(name) { + this.get_debug().log('[JSJaCJingle:muji] mute', 4); + + try { + // Locked? + if(this.get_lock()) { + this.get_debug().log('[JSJaCJingle:muji] mute > Cannot mute, resource locked. Please open another session or check WebRTC support.', 0); + return; + } + + // Defer? + var _this = this; + + if(JSJaCJingle._defer(function() { _this.mute(name); })) { + this.get_debug().log('[JSJaCJingle:muji] mute > Deferred (waiting for the library components to be initiated).', 0); + return; + } + + // Already muted? + if(this.get_mute(name) === true) { + this.get_debug().log('[JSJaCJingle:muji] mute > Resource already muted.', 0); + return; + } + + this._peer_sound(false); + this._set_mute(name, true); + + // Mute all participants + this._toggle_participants_mute(name, JSJAC_JINGLE_SESSION_INFO_MUTE); + } catch(e) { + this.get_debug().log('[JSJaCJingle:muji] mute > ' + e, 1); + } + }, + + /** + * Unmutes a Muji session (local) + * @public + * @param {String} name + */ + unmute: function(name) { + this.get_debug().log('[JSJaCJingle:muji] unmute', 4); + + try { + // Locked? + if(this.get_lock()) { + this.get_debug().log('[JSJaCJingle:muji] unmute > Cannot unmute, resource locked. Please open another session or check WebRTC support.', 0); + return; + } + + // Defer? + var _this = this; + + if(JSJaCJingle._defer(function() { _this.unmute(name); })) { + this.get_debug().log('[JSJaCJingle:muji] unmute > Deferred (waiting for the library components to be initiated).', 0); + return; + } + + // Already unmute? + if(this.get_mute(name) === false) { + this.get_debug().log('[JSJaCJingle:muji] unmute > Resource already unmuted.', 0); + return; + } + + this._peer_sound(true); + this._set_mute(name, false); + + // Unmute all participants + this._toggle_participants_mute(name, JSJAC_JINGLE_SESSION_INFO_UNMUTE); + } catch(e) { + this.get_debug().log('[JSJaCJingle:muji] unmute > ' + e, 1); + } + }, + + /** + * Toggles media type in a Muji session (local) + * @todo Code media() (Muji version) + * @public + * @param {String} [media] + */ + media: function(media) { + /* DEV: don't expect this to work as of now! */ + /* MEDIA() - MUJI VERSION */ + + this.get_debug().log('[JSJaCJingle:muji] media', 4); + + try { + // Locked? + if(this.get_lock()) { + this.get_debug().log('[JSJaCJingle:muji] media > Cannot change media, resource locked. Please open another session or check WebRTC support.', 0); + return; + } + + // Defer? + var _this = this; + + if(JSJaCJingle._defer(function() { _this.media(media); })) { + this.get_debug().log('[JSJaCJingle:muji] media > Deferred (waiting for the library components to be initiated).', 0); + return; + } + + // Toggle media? + if(!media) + media = (this.get_media() == JSJAC_JINGLE_MEDIA_VIDEO) ? JSJAC_JINGLE_MEDIA_AUDIO : JSJAC_JINGLE_MEDIA_VIDEO; + + // Media unknown? + if(!(media in JSJAC_JINGLE_MEDIAS)) { + this.get_debug().log('[JSJaCJingle:muji] media > No media provided or media unsupported (media: ' + media + ').', 0); + return; + } + + // Already using provided media? + if(this.get_media() == media) { + this.get_debug().log('[JSJaCJingle:muji] media > Resource already using this media (media: ' + media + ').', 0); + return; + } + + // Switch locked for now? (another one is being processed) + if(this.get_media_busy()) { + this.get_debug().log('[JSJaCJingle:muji] media > Resource already busy switching media (busy: ' + this.get_media() + ', media: ' + media + ').', 0); + return; + } + + this.get_debug().log('[JSJaCJingle:muji] media > Changing media to: ' + media + '...', 2); + + // Store new media + this._set_media(media); + this._set_media_busy(true); + + // Toggle video mode (add/remove) + if(media == JSJAC_JINGLE_MEDIA_VIDEO) { + /* @todo the flow is something like that... */ + /*this._peer_get_user_media(function() { + this._peer_connection_create( + function() { + this.get_debug().log('[JSJaCJingle:muji] media > Ready to change media (to: ' + media + ').', 2); + + // 'content-add' >> video + // @todo restart video stream configuration + + // WARNING: only change get user media, DO NOT TOUCH THE STREAM THING (don't stop active stream as it's flowing!!) + + this.send(JSJAC_JINGLE_IQ_TYPE_SET, { action: JSJAC_JINGLE_ACTION_CONTENT_ADD, name: JSJAC_JINGLE_MEDIA_VIDEO }); + } + ) + });*/ + } else { + /* @todo the flow is something like that... */ + /*this._peer_get_user_media(function() { + this._peer_connection_create( + function() { + this.get_debug().log('[JSJaCJingle:muji] media > Ready to change media (to: ' + media + ').', 2); + + // 'content-remove' >> video + // @todo remove video stream configuration + + // WARNING: only change get user media, DO NOT TOUCH THE STREAM THING (don't stop active stream as it's flowing!!) + // here, only stop the video stream, do not touch the audio stream + + this.send(JSJAC_JINGLE_IQ_TYPE_SET, { action: JSJAC_JINGLE_ACTION_CONTENT_REMOVE, name: JSJAC_JINGLE_MEDIA_VIDEO }); + } + ) + });*/ + } + + /* @todo loop on participant sessions and toggle medias individually */ + } catch(e) { + this.get_debug().log('[JSJaCJingle:muji] media > ' + e, 1); + } + }, + + /** + * Sends a given Muji presence stanza + * @public + * @fires JSJaCJingleMuji#get_room_presence_out + * @param {Object} [args] + * @returns {Boolean} Success + */ + send_presence: function(args) { + this.get_debug().log('[JSJaCJingle:muji] send_presence', 4); + + try { + // Locked? + if(this.get_lock()) { + this.get_debug().log('[JSJaCJingle:muji] send_presence > Cannot send, resource locked. Please open another session or check WebRTC support.', 0); + return false; + } + + // Defer? + var _this = this; + + if(JSJaCJingle._defer(function() { _this.send_presence(args); })) { + this.get_debug().log('[JSJaCJingle:muji] send_presence > Deferred (waiting for the library components to be initiated).', 0); + return false; + } + + if(typeof args !== 'object') args = {}; + + // Build stanza + var stanza = new JSJaCPresence(); + stanza.setTo(this.get_muc_to()); + + if(!args.id) args.id = this.get_id_new(); + stanza.setID(args.id); + + // Submit to registered handler + switch(args.action) { + case JSJAC_JINGLE_MUJI_ACTION_PREPARE: + this._send_session_prepare(stanza, args); break; + + case JSJAC_JINGLE_MUJI_ACTION_INITIATE: + this._send_session_initiate(stanza, args); break; + + case JSJAC_JINGLE_MUJI_ACTION_LEAVE: + this._send_session_leave(stanza, args); break; + + default: + this.get_debug().log('[JSJaCJingle:muji] send_presence > Unexpected error.', 1); + + return false; + } + + this._set_sent_id(args.id); + + this.get_connection().send(stanza); + + if(this.get_net_trace()) this.get_debug().log('[JSJaCJingle:muji] send_presence > Outgoing packet sent' + '\n\n' + stanza.xml()); + + // Trigger custom callback + /* @function */ + (this.get_room_presence_out())(this, stanza); + + return true; + } catch(e) { + this.get_debug().log('[JSJaCJingle:muji] send_presence > ' + e, 1); + } + + return false; + }, + + /** + * Sends a given Muji message stanza + * @public + * @fires JSJaCJingleMuji#get_room_message_out + * @param {String} body + * @returns {Boolean} Success + */ + send_message: function(body) { + this.get_debug().log('[JSJaCJingle:muji] send_message', 4); + + try { + // Missing args? + if(!body) { + this.get_debug().log('[JSJaCJingle:muji] send_message > Message body missing.', 0); + return false; + } + + // Locked? + if(this.get_lock()) { + this.get_debug().log('[JSJaCJingle:muji] send_message > Cannot send, resource locked. Please open another session or check WebRTC support.', 0); + return false; + } + + // Defer? + var _this = this; + + if(JSJaCJingle._defer(function() { _this.send_message(body); })) { + this.get_debug().log('[JSJaCJingle:muji] send_message > Deferred (waiting for the library components to be initiated).', 0); + return false; + } + + // Build stanza + var stanza = new JSJaCMessage(); + + stanza.setTo(this.get_to()); + stanza.setType(JSJAC_JINGLE_MESSAGE_TYPE_GROUPCHAT); + stanza.setBody(body); + + this.get_connection().send(stanza); + + if(this.get_net_trace()) this.get_debug().log('[JSJaCJingle:muji] send_message > Outgoing packet sent' + '\n\n' + stanza.xml()); + + // Trigger custom callback + /* @function */ + (this.get_room_message_out())(this, stanza); + + return true; + } catch(e) { + this.get_debug().log('[JSJaCJingle:muji] send_message > ' + e, 1); + } + + return false; + }, + + /** + * Handles a Muji presence stanza + * @public + * @fires JSJaCJingleMuji#_handle_participant_prepare + * @fires JSJaCJingleMuji#_handle_participant_initiate + * @fires JSJaCJingleMuji#_handle_participant_leave + * @fires JSJaCJingleMuji#get_room_presence_in + * @fires JSJaCJingleMuji#get_participant_prepare + * @fires JSJaCJingleMuji#get_participant_initiate + * @fires JSJaCJingleMuji#get_participant_leave + * @param {JSJaCPacket} stanza + */ + handle_presence: function(stanza) { + this.get_debug().log('[JSJaCJingle:muji] handle_presence', 4); + + try { + if(this.get_net_trace()) this.get_debug().log('[JSJaCJingle:muji] handle_presence > Incoming packet received' + '\n\n' + stanza.xml()); + + // Locked? + if(this.get_lock()) { + this.get_debug().log('[JSJaCJingle:muji] handle_presence > Cannot handle, resource locked. Please open another session or check WebRTC support.', 0); + return; + } + + // Defer? + var _this = this; + + if(JSJaCJingle._defer(function() { _this.handle_presence(stanza); })) { + this.get_debug().log('[JSJaCJingle:muji] handle_presence > Deferred (waiting for the library components to be initiated).', 0); + return; + } + + // Trigger custom callback + /* @function */ + (this.get_room_presence_in())(this, stanza); + + var id = stanza.getID(); + var type = (stanza.getType() || JSJAC_JINGLE_PRESENCE_TYPE_AVAILABLE); + + if(id) this._set_received_id(id); + + // Submit to custom handler (only for local user packets) + var i, handlers, is_stanza_from_local; + + handlers = this.get_registered_handlers(JSJAC_JINGLE_STANZA_PRESENCE, type, id); + is_stanza_from_local = this.is_stanza_from_local(stanza); + + if(typeof handlers == 'object' && handlers.length && is_stanza_from_local === true) { + this.get_debug().log('[JSJaCJingle:muji] handle_presence > Submitted to custom registered handlers.', 2); + + for(i in handlers) { + /* @function */ + handlers[i](stanza); + } + + this.unregister_handler(JSJAC_JINGLE_STANZA_PRESENCE, type, id); + + return; + } + + // Local stanza? + if(is_stanza_from_local === true) { + if(stanza.getType() === JSJAC_JINGLE_PRESENCE_TYPE_UNAVAILABLE) { + this.get_debug().log('[JSJaCJingle:muji] handle_presence > Conference room going offline, forcing termination...', 1); + + // Change session status + this._set_status(JSJAC_JINGLE_MUJI_STATUS_LEAVING); + + this._terminate_participant_sessions(); + + // Trigger leave error handlers + /* @function */ + this.get_session_leave_error()(this, stanza); + this._handle_session_leave_error(stanza); + } else { + this.get_debug().log('[JSJaCJingle:muji] handle_presence > Dropped local stanza.', 1); + } + } else { + // Defer if user media not ready yet + this._defer_participant_handlers(function(is_deferred) { + // Remote stanza handlers + if(stanza.getType() === JSJAC_JINGLE_PRESENCE_TYPE_UNAVAILABLE) { + _this._handle_participant_leave(stanza, is_deferred); + + /* @function */ + _this.get_participant_leave()(stanza); + } else { + var muji = _this.utils.stanza_muji(stanza); + + // Don't handle non-Muji stanzas there... + if(!muji) return; + + // Submit to registered handler + var username = _this.utils.stanza_username(stanza); + var status = _this._shortcut_participant_status(username); + + var fn_log_drop = function() { + _this.get_debug().log('[JSJaCJingle:muji] handle_presence > Dropped out-of-order participant stanza with status: ' + status, 1); + }; + + if(_this._stanza_has_preparing(muji)) { + if(!status || status === JSJAC_JINGLE_MUJI_STATUS_INACTIVE) { + _this._handle_participant_prepare(stanza, is_deferred); + + /* @function */ + _this.get_participant_prepare()(_this, stanza); + } else { + fn_log_drop(); + } + } else if(_this._stanza_has_content(muji)) { + if(!status || status === JSJAC_JINGLE_MUJI_STATUS_INACTIVE || status === JSJAC_JINGLE_MUJI_STATUS_PREPARED) { + _this._handle_participant_initiate(stanza, is_deferred); + + /* @function */ + _this.get_participant_initiate()(_this, stanza); + } else { + fn_log_drop(); + } + } else if(_this.is_stanza_from_participant(stanza)) { + if(!status || status === JSJAC_JINGLE_MUJI_STATUS_INACTIVE || status === JSJAC_JINGLE_MUJI_STATUS_INITIATED) { + _this._handle_participant_leave(stanza, is_deferred); + + /* @function */ + _this.get_participant_leave()(_this, stanza); + } else { + fn_log_drop(); + } + } + } + }); + } + } catch(e) { + this.get_debug().log('[JSJaCJingle:muji] handle_presence > ' + e, 1); + } + }, + + /** + * Handles a Muji message stanza + * @public + * @fires JSJaCJingleMuji#get_room_message_in + * @param {JSJaCPacket} stanza + */ + handle_message: function(stanza) { + this.get_debug().log('[JSJaCJingle:muji] handle_message', 4); + + try { + var stanza_type = stanza.getType(); + + if(stanza_type != JSJAC_JINGLE_MESSAGE_TYPE_GROUPCHAT) { + this.get_debug().log('[JSJaCJingle:muji] handle_message > Dropped invalid stanza type: ' + stanza_type, 0); + return; + } + + if(this.get_net_trace()) this.get_debug().log('[JSJaCJingle:muji] handle_message > Incoming packet received' + '\n\n' + stanza.xml()); + + // Locked? + if(this.get_lock()) { + this.get_debug().log('[JSJaCJingle:muji] handle_message > Cannot handle, resource locked. Please open another session or check WebRTC support.', 0); + return; + } + + // Trigger custom callback + /* @function */ + (this.get_room_message_in())(this, stanza); + } catch(e) { + this.get_debug().log('[JSJaCJingle:muji] handle_message > ' + e, 1); + } + }, + + + + /** + * JSJSAC JINGLE MUJI SENDERS + */ + + /** + * Sends the invite message. + * @private + * @param {String} jid + */ + _send_invite: function(jid, reason) { + this.get_debug().log('[JSJaCJingle:muji] _send_invite', 4); + + try { + var cur_participant, participants, + stanza, x_invite; + + stanza = new JSJaCMessage(); + stanza.setTo(jid); + + x_invite = stanza.buildNode('x', { + 'jid': this.get_to(), + 'xmlns': NS_JABBER_CONFERENCE + }); + + if(reason) + x_invite.setAttribute('reason', reason); + if(this.get_password()) + x_invite.setAttribute('password', this.get_password()); + + stanza.getNode().appendChild(x_invite); + + stanza.appendNode('x', { + 'media': this.get_media(), + 'xmlns': NS_MUJI_INVITE + }); + + this.get_connection().send(stanza); + + if(this.get_net_trace()) this.get_debug().log('[JSJaCJingle:muji] _send_invite > Outgoing packet sent' + '\n\n' + stanza.xml()); + + // Trigger custom callback + /* @function */ + (this.get_room_message_out())(this, stanza); + } catch(e) { + this.get_debug().log('[JSJaCJingle:muji] _send_invite > ' + e, 1); + } + }, + + /** + * Sends the session prepare event. + * @private + * @fires JSJaCJingleMuji#_handle_session_prepare_success + * @fires JSJaCJingleMuji#_handle_session_prepare_error + * @fires JSJaCJingleMuji#get_session_prepare_success + * @fires JSJaCJingleMuji#get_session_prepare_error + * @fires JSJaCJingleMuji#get_session_prepare_pending + * @param {JSJaCPacket} stanza + * @param {Object} args + */ + _send_session_prepare: function(stanza, args) { + this.get_debug().log('[JSJaCJingle:muji] _send_session_prepare', 4); + + try { + if(this.get_status() !== JSJAC_JINGLE_MUJI_STATUS_PREPARING) { + this.get_debug().log('[JSJaCJingle:muji] _send_session_prepare > Cannot send prepare stanza, resource already prepared (status: ' + this.get_status() + ').', 0); + return; + } + + if(!args) { + this.get_debug().log('[JSJaCJingle:muji] _send_session_prepare > Arguments not provided.', 1); + return; + } + + // Build Muji stanza + var muji = this.utils.stanza_generate_muji(stanza); + muji.appendChild(stanza.buildNode('preparing', { 'xmlns': NS_MUJI })); + + // Password-protected room? + if(this.get_password()) { + var x_muc = stanza.getNode().appendChild(stanza.buildNode('x', { 'xmlns': NS_JABBER_MUC })); + + x_muc.appendChild( + stanza.buildNode('password', { 'xmlns': NS_JABBER_MUC }, this.get_password()) + ); + } + + // Schedule success + var _this = this; + + this.register_handler(JSJAC_JINGLE_STANZA_PRESENCE, JSJAC_JINGLE_PRESENCE_TYPE_AVAILABLE, args.id, function(stanza) { + /* @function */ + (_this.get_session_prepare_success())(_this, stanza); + _this._handle_session_prepare_success(stanza); + }); + + this.register_handler(JSJAC_JINGLE_STANZA_PRESENCE, JSJAC_JINGLE_PRESENCE_TYPE_ERROR, args.id, function(stanza) { + /* @function */ + (_this.get_session_prepare_error())(_this, stanza); + _this._handle_session_prepare_error(stanza); + }); + + // Schedule timeout + this.utils.stanza_timeout(JSJAC_JINGLE_STANZA_PRESENCE, JSJAC_JINGLE_PRESENCE_TYPE_AVAILABLE, args.id, { + /* @function */ + external: this.get_session_prepare_error().bind(this), + internal: this._handle_session_prepare_error.bind(this) + }); + this.utils.stanza_timeout(JSJAC_JINGLE_STANZA_PRESENCE, JSJAC_JINGLE_PRESENCE_TYPE_ERROR, args.id); + + this.get_debug().log('[JSJaCJingle:muji] _send_session_prepare > Sent.', 2); + } catch(e) { + this.get_debug().log('[JSJaCJingle:muji] _send_session_prepare > ' + e, 1); + } + }, + + /** + * Sends the session initiate event. + * @private + * @fires JSJaCJingleMuji#_handle_session_initiate_success + * @fires JSJaCJingleMuji#_handle_session_initiate_error + * @fires JSJaCJingleMuji#get_session_initiate_success + * @fires JSJaCJingleMuji#get_session_initiate_error + * @param {JSJaCPacket} stanza + * @param {Object} args + */ + _send_session_initiate: function(stanza, args) { + this.get_debug().log('[JSJaCJingle:muji] _send_session_initiate', 4); + + try { + if(this.get_status() !== JSJAC_JINGLE_MUJI_STATUS_INITIATING) { + this.get_debug().log('[JSJaCJingle:muji] _send_session_initiate > Cannot send initiate stanza, resource already initiated (status: ' + this.get_status() + ').', 0); + return; + } + + if(!args) { + this.get_debug().log('[JSJaCJingle:muji] _send_session_initiate > Arguments not provided.', 1); + return; + } + + // Build Muji stanza + var muji = this.utils.stanza_generate_muji(stanza); + + this.utils.stanza_generate_content_local(stanza, muji, false); + this.utils.stanza_generate_group_local(stanza, muji); + + // Schedule success + var _this = this; + + this.register_handler(JSJAC_JINGLE_STANZA_PRESENCE, JSJAC_JINGLE_PRESENCE_TYPE_AVAILABLE, args.id, function(stanza) { + /* @function */ + (_this.get_session_initiate_success())(_this, stanza); + _this._handle_session_initiate_success(stanza); + }); + + this.register_handler(JSJAC_JINGLE_STANZA_PRESENCE, JSJAC_JINGLE_PRESENCE_TYPE_ERROR, args.id, function(stanza) { + /* @function */ + (_this.get_session_initiate_error())(_this, stanza); + _this._handle_session_initiate_error(stanza); + }); + + // Schedule timeout + this.utils.stanza_timeout(JSJAC_JINGLE_STANZA_PRESENCE, JSJAC_JINGLE_PRESENCE_TYPE_AVAILABLE, args.id, { + /* @function */ + external: this.get_session_initiate_error().bind(this), + internal: this._handle_session_initiate_error.bind(this) + }); + this.utils.stanza_timeout(JSJAC_JINGLE_STANZA_PRESENCE, JSJAC_JINGLE_PRESENCE_TYPE_ERROR, args.id); + + this.get_debug().log('[JSJaCJingle:muji] _send_session_initiate > Sent.', 2); + } catch(e) { + this.get_debug().log('[JSJaCJingle:muji] _send_session_initiate > ' + e, 1); + } + }, + + /** + * Sends the session leave event. + * @private + * @fires JSJaCJingleMuji#_handle_session_leave_success + * @fires JSJaCJingleMuji#_handle_session_leave_error + * @fires JSJaCJingleMuji#get_session_leave_success + * @fires JSJaCJingleMuji#get_session_leave_error + * @param {JSJaCPacket} stanza + * @param {Object} args + */ + _send_session_leave: function(stanza, args) { + this.get_debug().log('[JSJaCJingle:muji] _send_session_leave', 4); + + try { + if(this.get_status() !== JSJAC_JINGLE_MUJI_STATUS_LEAVING) { + this.get_debug().log('[JSJaCJingle:muji] _send_session_leave > Cannot send leave stanza, resource already left (status: ' + this.get_status() + ').', 0); + return; + } + + if(!args) { + this.get_debug().log('[JSJaCJingle:muji] _send_session_leave > Arguments not provided.', 1); + return; + } + + stanza.setType(JSJAC_JINGLE_PRESENCE_TYPE_UNAVAILABLE); + + // Schedule success + var _this = this; + + this.register_handler(JSJAC_JINGLE_STANZA_PRESENCE, JSJAC_JINGLE_PRESENCE_TYPE_UNAVAILABLE, args.id, function(stanza) { + /* @function */ + (_this.get_session_leave_success())(_this, stanza); + _this._handle_session_leave_success(stanza); + }); + + this.register_handler(JSJAC_JINGLE_STANZA_PRESENCE, JSJAC_JINGLE_PRESENCE_TYPE_ERROR, args.id, function(stanza) { + /* @function */ + (_this.get_session_leave_error())(_this, stanza); + _this._handle_session_leave_error(stanza); + }); + + // Schedule timeout + this.utils.stanza_timeout(JSJAC_JINGLE_STANZA_PRESENCE, JSJAC_JINGLE_PRESENCE_TYPE_UNAVAILABLE, args.id, { + /* @function */ + external: this.get_session_leave_error().bind(this), + internal: this._handle_session_leave_error.bind(this) + }); + this.utils.stanza_timeout(JSJAC_JINGLE_STANZA_PRESENCE, JSJAC_JINGLE_PRESENCE_TYPE_ERROR, args.id); + + this.get_debug().log('[JSJaCJingle:muji] _send_session_leave > Sent.', 2); + } catch(e) { + this.get_debug().log('[JSJaCJingle:muji] _send_session_leave > ' + e, 1); + } + }, + + + + /** + * JSJSAC JINGLE MUJI HANDLERS + */ + + /** + * Handles the Jingle session prepare success + * @private + * @event JSJaCJingleMuji#_handle_session_prepare_success + * @param {JSJaCPacket} stanza + */ + _handle_session_prepare_success: function(stanza) { + this.get_debug().log('[JSJaCJingle:muji] _handle_session_prepare_success', 4); + + try { + if(this.get_status() !== JSJAC_JINGLE_MUJI_STATUS_PREPARING) { + this.get_debug().log('[JSJaCJingle:muji] _handle_session_prepare_success > Cannot handle prepare success stanza, resource already prepared (status: ' + this.get_status() + ').', 0); + return; + } + + var username = this.utils.stanza_username(stanza); + + if(!username) { + throw 'No username provided, not accepting session prepare stanza.'; + } + + if(this._stanza_has_room_owner(stanza)) { + this.get_debug().log('[JSJaCJingle:muji] _handle_session_prepare_success > Current MUC affiliation is owner.', 2); + + this._set_is_room_owner(true); + } + + if(this._stanza_has_password_invalid(stanza)) { + // Password protected room? + this.get_debug().log('[JSJaCJingle:muji] _handle_session_prepare_success > Password-protected room, aborting.', 1); + + /* @function */ + (this.get_session_leave_success())(this, stanza); + this._handle_session_leave_success(stanza); + } else if(this._stanza_has_username_conflict(stanza)) { + // Username conflict + var alt_username = (this.get_username() + this.utils.generate_random(4)); + + this.get_debug().log('[JSJaCJingle:muji] _handle_session_prepare_success > Conflicting username, changing it to: ' + alt_username, 2); + + this._set_username(alt_username); + this.send_presence({ action: JSJAC_JINGLE_MUJI_ACTION_PREPARE }); + } else { + // Change session status + this._set_status(JSJAC_JINGLE_MUJI_STATUS_PREPARED); + + // Initialize WebRTC + var _this = this; + + this._peer_get_user_media(function() { + _this._peer_connection_create(function() { + _this.get_debug().log('[JSJaCJingle:muji] _handle_session_prepare_success > Ready to begin Muji initiation.', 2); + + // Trigger session initiate pending custom callback + /* @function */ + (_this.get_session_initiate_pending())(_this); + + // Build content (local) + _this.utils.build_content_local(); + + // Change session status + _this._set_status(JSJAC_JINGLE_MUJI_STATUS_INITIATING); + + _this.send_presence({ action: JSJAC_JINGLE_MUJI_ACTION_INITIATE }); + }); + }); + } + } catch(e) { + this.get_debug().log('[JSJaCJingle:muji] _handle_session_prepare_success > ' + e, 1); + } + }, + + /** + * Handles the Jingle session prepare error + * @private + * @event JSJaCJingleMuji#_handle_session_prepare_error + * @param {JSJaCPacket} stanza + */ + _handle_session_prepare_error: function(stanza) { + this.get_debug().log('[JSJaCJingle:muji] _handle_session_prepare_error', 4); + + try { + if(this.get_status() !== JSJAC_JINGLE_MUJI_STATUS_PREPARING) { + this.get_debug().log('[JSJaCJingle:muji] _handle_session_prepare_error > Cannot handle prepare error stanza, resource already prepared (status: ' + this.get_status() + ').', 0); + return; + } + + this.leave(); + } catch(e) { + this.get_debug().log('[JSJaCJingle:muji] _handle_session_prepare_error > ' + e, 1); + } + }, + + /** + * Handles the Jingle session initiate success + * @private + * @event JSJaCJingleMuji#_handle_session_initiate_success + * @param {JSJaCPacket} stanza + */ + _handle_session_initiate_success: function(stanza) { + this.get_debug().log('[JSJaCJingle:muji] _handle_session_initiate_success', 4); + + try { + if(this.get_status() !== JSJAC_JINGLE_MUJI_STATUS_INITIATING) { + this.get_debug().log('[JSJaCJingle:muji] _handle_session_initiate_success > Cannot handle initiate success stanza, resource already initiated (status: ' + this.get_status() + ').', 0); + return; + } + + // Change session status + this._set_status(JSJAC_JINGLE_MUJI_STATUS_INITIATED); + + // Undefer pending participant handlers + this._undefer_participant_handlers(); + + // Autoconfigure room password if new MUC + if(this.get_is_room_owner() === true && + this.get_password_protect() === true && + this.utils.object_length(this.get_participants()) === 0) { + this._autoconfigure_room_password(); + } + } catch(e) { + this.get_debug().log('[JSJaCJingle:muji] _handle_session_initiate_success > ' + e, 1); + } + }, + + /** + * Handles the Jingle session initiate error + * @private + * @event JSJaCJingleMuji#_handle_session_initiate_error + * @param {JSJaCPacket} stanza + */ + _handle_session_initiate_error: function(stanza) { + this.get_debug().log('[JSJaCJingle:muji] _handle_session_initiate_error', 4); + + try { + if(this.get_status() !== JSJAC_JINGLE_MUJI_STATUS_INITIATING) { + this.get_debug().log('[JSJaCJingle:muji] _handle_session_initiate_error > Cannot handle initiate error stanza, resource already initiated (status: ' + this.get_status() + ').', 0); + return; + } + + this.leave(); + } catch(e) { + this.get_debug().log('[JSJaCJingle:muji] _handle_session_initiate_error > ' + e, 1); + } + }, + + /** + * Handles the Jingle session leave success + * @private + * @event JSJaCJingleMuji#_handle_session_leave_success + * @param {JSJaCPacket} stanza + */ + _handle_session_leave_success: function(stanza) { + this.get_debug().log('[JSJaCJingle:muji] _handle_session_leave_success', 4); + + try { + if(this.get_status() !== JSJAC_JINGLE_MUJI_STATUS_LEAVING) { + this.get_debug().log('[JSJaCJingle:muji] _handle_session_leave_success > Cannot handle leave success stanza, resource already left (status: ' + this.get_status() + ').', 0); + return; + } + + this.abort(); + } catch(e) { + this.get_debug().log('[JSJaCJingle:muji] _handle_session_leave_success > ' + e, 1); + } + }, + + /** + * Handles the Jingle session leave error + * @private + * @event JSJaCJingleMuji#_handle_session_leave_error + * @param {JSJaCPacket} stanza + */ + _handle_session_leave_error: function(stanza) { + this.get_debug().log('[JSJaCJingle:muji] _handle_session_leave_error', 4); + + try { + if(this.get_status() !== JSJAC_JINGLE_MUJI_STATUS_LEAVING) { + this.get_debug().log('[JSJaCJingle:muji] _handle_session_leave_success > Cannot handle leave error stanza, resource already left (status: ' + this.get_status() + ').', 0); + return; + } + + this.abort(true); + + this.get_debug().log('[JSJaCJingle:muji] _handle_session_leave_error > Forced session exit locally.', 0); + } catch(e) { + this.get_debug().log('[JSJaCJingle:muji] _handle_session_leave_error > ' + e, 1); + } + }, + + /** + * Handles the participant prepare event. + * @private + * @event JSJaCJingleMuji#_handle_participant_prepare + * @param {JSJaCPacket} stanza + * @param {Boolean} [is_deferred] + */ + _handle_participant_prepare: function(stanza, is_deferred) { + this.get_debug().log('[JSJaCJingle:muji] _handle_participant_prepare', 4); + + try { + var username = this.utils.stanza_username(stanza); + + if(!username) { + throw 'No username provided, not accepting participant prepare stanza.'; + } + + // Local slot unavailable? + if(this.get_status() === JSJAC_JINGLE_MUJI_STATUS_INACTIVE || + this.get_status() === JSJAC_JINGLE_MUJI_STATUS_LEAVING || + this.get_status() === JSJAC_JINGLE_MUJI_STATUS_LEFT) { + this.get_debug().log('[JSJaCJingle:muji] _handle_participant_prepare > [' + username + '] > Cannot handle, resource not available (status: ' + this.get_status() + ').', 0); + return; + } + + // Remote slot unavailable? + var status = this._shortcut_participant_status(username); + + if(status !== JSJAC_JINGLE_MUJI_STATUS_INACTIVE) { + this.get_debug().log('[JSJaCJingle:muji] _handle_participant_prepare > [' + username + '] > Cannot handle prepare stanza, participant already prepared (status: ' + status + ').', 0); + return; + } + + this._set_participants(username, { + status: JSJAC_JINGLE_MUJI_STATUS_PREPARED, + view: this._shortcut_participant_view(username) + }); + } catch(e) { + this.get_debug().log('[JSJaCJingle:muji] _handle_participant_prepare > ' + e, 1); + } + }, + + /** + * Handles the participant initiate event. + * @private + * @event JSJaCJingleMuji#_handle_participant_initiate + * @param {JSJaCPacket} stanza + * @param {Boolean} [is_deferred] + */ + _handle_participant_initiate: function(stanza, is_deferred) { + this.get_debug().log('[JSJaCJingle:muji] _handle_participant_initiate', 4); + + try { + var username = this.utils.stanza_username(stanza); + + if(!username) { + throw 'No username provided, not accepting participant initiate stanza.'; + } + + // Local slot unavailable? + if(this.get_status() === JSJAC_JINGLE_MUJI_STATUS_INACTIVE || + this.get_status() === JSJAC_JINGLE_MUJI_STATUS_LEAVING || + this.get_status() === JSJAC_JINGLE_MUJI_STATUS_LEFT) { + this.get_debug().log('[JSJaCJingle:muji] _handle_participant_initiate > [' + username + '] > Cannot handle, resource not available (status: ' + this.get_status() + ').', 0); + return; + } + + // Remote slot unavailable? + var status = this._shortcut_participant_status(username); + + if(status !== JSJAC_JINGLE_MUJI_STATUS_INACTIVE && + status !== JSJAC_JINGLE_MUJI_STATUS_PREPARED) { + this.get_debug().log('[JSJaCJingle:muji] _handle_participant_initiate > [' + username + '] > Cannot handle initiate stanza, participant already initiated (status: ' + status + ').', 0); + return; + } + + // Need to initiate? (participant was here before we joined) + /* @see {@link http://xmpp.org/extensions/xep-0272.html#joining|XEP-0272: Multiparty Jingle (Muji) - Joining a Conference} */ + if(is_deferred === true) { + this.get_debug().log('[JSJaCJingle:muji] _handle_participant_initiate > [' + username + '] Initiating participant Jingle session...', 2); + + // Create Jingle session + this._create_participant_session(username).initiate(); + } else { + this.get_debug().log('[JSJaCJingle:muji] _handle_participant_initiate > [' + username + '] Waiting for participant Jingle initiation request...', 2); + + this._set_participants(username, { + status: JSJAC_JINGLE_MUJI_STATUS_INITIATED, + view: this._shortcut_participant_view(username) + }); + } + } catch(e) { + this.get_debug().log('[JSJaCJingle:muji] _handle_participant_initiate > ' + e, 1); + } + }, + + /** + * Handles the participant leave event. + * @private + * @event JSJaCJingleMuji#_handle_participant_leave + * @param {JSJaCPacket} stanza + * @param {Boolean} [is_deferred] + */ + _handle_participant_leave: function(stanza, is_deferred) { + this.get_debug().log('[JSJaCJingle:muji] _handle_participant_leave', 4); + + try { + var username = this.utils.stanza_username(stanza); + + if(!username) { + throw 'No username provided, not accepting participant leave stanza.'; + } + + // Local slot unavailable? + if(this.get_status() === JSJAC_JINGLE_MUJI_STATUS_INACTIVE || + this.get_status() === JSJAC_JINGLE_MUJI_STATUS_LEAVING || + this.get_status() === JSJAC_JINGLE_MUJI_STATUS_LEFT) { + this.get_debug().log('[JSJaCJingle:muji] _handle_participant_leave > [' + username + '] > Cannot handle, resource not available (status: ' + this.get_status() + ').', 0); + return; + } + + // Remote slot unavailable? + var status = this._shortcut_participant_status(username); + + if(status !== JSJAC_JINGLE_MUJI_STATUS_PREPARED && + status !== JSJAC_JINGLE_MUJI_STATUS_INITIATED) { + this.get_debug().log('[JSJaCJingle:muji] _handle_participant_leave > [' + username + '] > Cannot handle leave stanza, participant already left or inactive (status: ' + status + ').', 0); + return; + } + + // Remove participant session + var session = (this.get_participants(username) || {}).session; + + if(session && session.get_status() !== JSJAC_JINGLE_STATUS_TERMINATED) + session.abort(true); + + this._set_participants(username, null); + this.get_remove_remote_view()(this, username); + } catch(e) { + this.get_debug().log('[JSJaCJingle:muji] _handle_participant_leave > ' + e, 1); + } + }, + + + + /** + * JSJSAC JINGLE SESSION HANDLERS + */ + + /** + * Handles the Jingle session prepare success + * @private + * @event JSJaCJingleMuji#_handle_participant_session_initiate_pending + * @param {JSJaCJingleSingle} session + */ + _handle_participant_session_initiate_pending: function(session) { + this.get_debug().log('[JSJaCJingle:muji] _handle_participant_session_initiate_pending', 4); + + try { + /* @function */ + (this.get_participant_session_initiate_pending())(this, session); + } catch(e) { + this.get_debug().log('[JSJaCJingle:muji] _handle_participant_session_initiate_pending > ' + e, 1); + } + }, + + /** + * Handles the Jingle session prepare success + * @private + * @event JSJaCJingleMuji#_handle_participant_session_initiate_success + * @param {JSJaCJingleSingle} session + * @param {JSJaCPacket} stanza + */ + _handle_participant_session_initiate_success: function(session, stanza) { + this.get_debug().log('[JSJaCJingle:muji] _handle_participant_session_initiate_success', 4); + + try { + /* @function */ + (this.get_participant_session_initiate_success())(this, session, stanza); + + // Mute participant? + var cur_media_name; + + for(cur_media_name in this._mute) { + if(this.get_mute(cur_media_name) === true) { + this._toggle_participants_mute( + cur_media_name, + JSJAC_JINGLE_SESSION_INFO_MUTE, + username + ); + } + } + + // Auto-accept incoming sessions + if(session.is_responder()) { + // Accept after a while + setTimeout(function() { + session.accept(); + }, (JSJAC_JINGLE_MUJI_PARTICIPANT_ACCEPT_WAIT * 1000)); + } + } catch(e) { + this.get_debug().log('[JSJaCJingle:muji] _handle_participant_session_initiate_success > ' + e, 1); + } + }, + + /** + * Handles the Jingle session prepare success + * @private + * @event JSJaCJingleMuji#_handle_participant_session_initiate_error + * @param {JSJaCJingleSingle} session + * @param {JSJaCPacket} stanza + */ + _handle_participant_session_initiate_error: function(session, stanza) { + this.get_debug().log('[JSJaCJingle:muji] _handle_participant_session_initiate_error', 4); + + try { + /* @function */ + (this.get_participant_session_initiate_error())(this, session, stanza); + } catch(e) { + this.get_debug().log('[JSJaCJingle:muji] _handle_participant_session_initiate_error > ' + e, 1); + } + }, + + /** + * Handles the Jingle session prepare success + * @private + * @event JSJaCJingleMuji#_handle_participant_session_initiate_request + * @param {JSJaCJingleSingle} session + * @param {JSJaCPacket} stanza + */ + _handle_participant_session_initiate_request: function(session, stanza) { + this.get_debug().log('[JSJaCJingle:muji] _handle_participant_session_initiate_request', 4); + + try { + /* @function */ + (this.get_participant_session_initiate_request())(this, session, stanza); + } catch(e) { + this.get_debug().log('[JSJaCJingle:muji] _handle_participant_session_initiate_request > ' + e, 1); + } + }, + + /** + * Handles the Jingle session prepare success + * @private + * @event JSJaCJingleMuji#_handle_participant_session_accept_pending + * @param {JSJaCJingleSingle} session + */ + _handle_participant_session_accept_pending: function(session) { + this.get_debug().log('[JSJaCJingle:muji] _handle_participant_session_accept_pending', 4); + + try { + /* @function */ + (this.get_participant_session_accept_pending())(this, session); + } catch(e) { + this.get_debug().log('[JSJaCJingle:muji] _handle_participant_session_accept_pending > ' + e, 1); + } + }, + + /** + * Handles the Jingle session prepare success + * @private + * @event JSJaCJingleMuji#_handle_participant_session_accept_success + * @param {JSJaCJingleSingle} session + * @param {JSJaCPacket} stanza + */ + _handle_participant_session_accept_success: function(session, stanza) { + this.get_debug().log('[JSJaCJingle:muji] _handle_participant_session_accept_success', 4); + + try { + /* @function */ + (this.get_participant_session_accept_success())(this, session, stanza); + } catch(e) { + this.get_debug().log('[JSJaCJingle:muji] _handle_participant_session_accept_success > ' + e, 1); + } + }, + + /** + * Handles the Jingle session prepare success + * @private + * @event JSJaCJingleMuji#_handle_participant_session_accept_error + * @param {JSJaCJingleSingle} session + * @param {JSJaCPacket} stanza + */ + _handle_participant_session_accept_error: function(session, stanza) { + this.get_debug().log('[JSJaCJingle:muji] _handle_participant_session_accept_error', 4); + + try { + /* @function */ + (this.get_participant_session_accept_error())(this, session, stanza); + } catch(e) { + this.get_debug().log('[JSJaCJingle:muji] _handle_participant_session_accept_error > ' + e, 1); + } + }, + + /** + * Handles the Jingle session prepare success + * @private + * @event JSJaCJingleMuji#_handle_participant_session_accept_request + * @param {JSJaCJingleSingle} session + * @param {JSJaCPacket} stanza + */ + _handle_participant_session_accept_request: function(session, stanza) { + this.get_debug().log('[JSJaCJingle:muji] _handle_participant_session_accept_request', 4); + + try { + /* @function */ + (this.get_participant_session_accept_request())(this, session, stanza); + } catch(e) { + this.get_debug().log('[JSJaCJingle:muji] _handle_participant_session_accept_request > ' + e, 1); + } + }, + + /** + * Handles the Jingle session prepare success + * @private + * @event JSJaCJingleMuji#_handle_participant_session_info_pending + * @param {JSJaCJingleSingle} session + */ + _handle_participant_session_info_pending: function(session) { + this.get_debug().log('[JSJaCJingle:muji] _handle_participant_session_info_pending', 4); + + try { + /* @function */ + (this.get_participant_session_info_pending())(this, session); + } catch(e) { + this.get_debug().log('[JSJaCJingle:muji] _handle_participant_session_info_pending > ' + e, 1); + } + }, + + /** + * Handles the Jingle session prepare success + * @private + * @event JSJaCJingleMuji#_handle_participant_session_info_success + * @param {JSJaCJingleSingle} session + * @param {JSJaCPacket} stanza + */ + _handle_participant_session_info_success: function(session, stanza) { + this.get_debug().log('[JSJaCJingle:muji] _handle_participant_session_info_success', 4); + + try { + /* @function */ + (this.get_participant_session_info_success())(this, session, stanza); + } catch(e) { + this.get_debug().log('[JSJaCJingle:muji] _handle_participant_session_info_success > ' + e, 1); + } + }, + + /** + * Handles the Jingle session prepare success + * @private + * @event JSJaCJingleMuji#_handle_participant_session_info_error + * @param {JSJaCJingleSingle} session + * @param {JSJaCPacket} stanza + */ + _handle_participant_session_info_error: function(session, stanza) { + this.get_debug().log('[JSJaCJingle:muji] _handle_participant_session_info_error', 4); + + try { + /* @function */ + (this.get_participant_session_info_error())(this, session, stanza); + } catch(e) { + this.get_debug().log('[JSJaCJingle:muji] _handle_participant_session_info_error > ' + e, 1); + } + }, + + /** + * Handles the Jingle session prepare success + * @private + * @event JSJaCJingleMuji#_handle_participant_session_info_request + * @param {JSJaCJingleSingle} session + * @param {JSJaCPacket} stanza + */ + _handle_participant_session_info_request: function(session, stanza) { + this.get_debug().log('[JSJaCJingle:muji] _handle_participant_session_info_request', 4); + + try { + /* @function */ + (this.get_participant_session_info_request())(this, session, stanza); + } catch(e) { + this.get_debug().log('[JSJaCJingle:muji] _handle_participant_session_info_request > ' + e, 1); + } + }, + + /** + * Handles the Jingle session prepare success + * @private + * @event JSJaCJingleMuji#_handle_participant_session_terminate_pending + * @param {JSJaCJingleSingle} session + */ + _handle_participant_session_terminate_pending: function(session) { + this.get_debug().log('[JSJaCJingle:muji] _handle_participant_session_terminate_pending', 4); + + try { + /* @function */ + (this.get_participant_session_terminate_pending())(this, session); + } catch(e) { + this.get_debug().log('[JSJaCJingle:muji] _handle_participant_session_terminate_pending > ' + e, 1); + } + }, + + /** + * Handles the Jingle session prepare success + * @private + * @event JSJaCJingleMuji#_handle_participant_session_terminate_success + * @param {JSJaCJingleSingle} session + * @param {JSJaCPacket} stanza + */ + _handle_participant_session_terminate_success: function(session, stanza) { + this.get_debug().log('[JSJaCJingle:muji] _handle_participant_session_terminate_success', 4); + + try { + /* @function */ + (this.get_participant_session_terminate_success())(this, session, stanza); + } catch(e) { + this.get_debug().log('[JSJaCJingle:muji] _handle_participant_session_terminate_success > ' + e, 1); + } + }, + + /** + * Handles the Jingle session prepare success + * @private + * @event JSJaCJingleMuji#_handle_participant_session_terminate_error + * @param {JSJaCJingleSingle} session + * @param {JSJaCPacket} stanza + */ + _handle_participant_session_terminate_error: function(session, stanza) { + this.get_debug().log('[JSJaCJingle:muji] _handle_participant_session_terminate_error', 4); + + try { + /* @function */ + (this.get_participant_session_terminate_error())(this, session, stanza); + } catch(e) { + this.get_debug().log('[JSJaCJingle:muji] _handle_participant_session_terminate_error > ' + e, 1); + } + }, + + /** + * Handles the Jingle session prepare success + * @private + * @event JSJaCJingleMuji#_handle_participant_session_terminate_request + * @param {JSJaCJingleSingle} session + * @param {JSJaCPacket} stanza + */ + _handle_participant_session_terminate_request: function(session, stanza) { + this.get_debug().log('[JSJaCJingle:muji] _handle_participant_session_terminate_request', 4); + + try { + /* @function */ + (this.get_participant_session_terminate_request())(this, session, stanza); + } catch(e) { + this.get_debug().log('[JSJaCJingle:muji] _handle_participant_session_terminate_request > ' + e, 1); + } + }, + + + + /** + * JSJSAC JINGLE STANZA PARSERS + */ + + /** + * Returns whether user is preparing or not + * @private + * @param {DOM} muji + * @returns {Boolean} Preparing state + */ + _stanza_has_preparing: function(muji) { + return this.utils.stanza_get_element(muji, 'preparing', NS_MUJI).length && true; + }, + + /** + * Returns whether user has content or not + * @private + * @param {DOM} muji + * @returns {Boolean} Content state + */ + _stanza_has_content: function(muji) { + return this.utils.stanza_get_element(muji, 'content', NS_MUJI).length && true; + }, + + /** + * Returns whether stanza has the room owner code or not + * @private + * @param {JSJaCPacket} stanza + * @returns {Boolean} Room owner state + */ + _stanza_has_room_owner: function(stanza) { + var is_room_owner = false; + + try { + var i, items, + x_muc_user = stanza.getChild('x', NS_JABBER_MUC_USER); + + if(x_muc_user) { + items = this.utils.stanza_get_element(x_muc_user, 'item', NS_JABBER_MUC_USER); + + for(i = 0; i < items.length; i++) { + if(items[i].getAttribute('affiliation') === JSJAC_JINGLE_MUJI_MUC_AFFILIATION_OWNER) { + is_room_owner = true; break; + } + } + } + } catch(e) { + this.get_debug().log('[JSJaCJingle:muji] _stanza_has_room_owner > ' + e, 1); + } finally { + return is_room_owner; + } + }, + + /** + * Returns whether stanza is a password invalid or not + * @private + * @param {JSJaCPacket} stanza + * @returns {Boolean} Password invalid state + */ + _stanza_has_password_invalid: function(stanza) { + return (this.utils.stanza_get_error(stanza, XMPP_ERROR_NOT_AUTHORIZED).length >= 1) && true; + }, + + /** + * Returns whether stanza is an username conflict or not + * @private + * @param {JSJaCPacket} stanza + * @returns {Boolean} Local user state + */ + _stanza_has_username_conflict: function(stanza) { + return (this.utils.stanza_get_error(stanza, XMPP_ERROR_CONFLICT).length >= 1) && true; + }, + + + + /** + * JSJSAC JINGLE PEER TOOLS + */ + + /** + * Creates peer connection instance + * @private + */ + _peer_connection_create_instance: function() { + this.get_debug().log('[JSJaCJingle:muji] _peer_connection_create_instance', 4); + + try { + // Create the RTCPeerConnection object + this._set_peer_connection( + new WEBRTC_PEER_CONNECTION( + null, + WEBRTC_CONFIGURATION.peer_connection.constraints + ) + ); + } catch(e) { + this.get_debug().log('[JSJaCJingle:muji] _peer_connection_create_instance > ' + e, 1); + } + }, + + /** + * Attaches peer connection callbacks (not used) + * @private + * @param {Function} [sdp_message_callback] + */ + _peer_connection_callbacks: function(sdp_message_callback) { + this.get_debug().log('[JSJaCJingle:muji] _peer_connection_callbacks', 4); + + try { + // Not used for Muji + } catch(e) { + this.get_debug().log('[JSJaCJingle:muji] _peer_connection_callbacks > ' + e, 1); + } + }, + + /** + * Dispatches peer connection to correct creator (offer/answer) + * @private + * @param {Function} [sdp_message_callback] + */ + _peer_connection_create_dispatch: function(sdp_message_callback) { + this.get_debug().log('[JSJaCJingle:muji] _peer_connection_create_dispatch', 4); + + try { + this._peer_connection_create_offer(sdp_message_callback); + } catch(e) { + this.get_debug().log('[JSJaCJingle:muji] _peer_connection_create_dispatch > ' + e, 1); + } + }, + + /** + * Creates peer connection offer + * @private + * @param {Function} [sdp_message_callback] + */ + _peer_connection_create_offer: function(sdp_message_callback) { + this.get_debug().log('[JSJaCJingle:muji] _peer_connection_create_offer', 4); + + try { + // Create offer + this.get_debug().log('[JSJaCJingle:muji] _peer_connection_create_offer > Getting local description...', 2); + + // Local description + this.get_peer_connection().createOffer( + function(sdp_local) { + this._peer_got_description(sdp_local, sdp_message_callback); + }.bind(this), + + this._peer_fail_description.bind(this), + WEBRTC_CONFIGURATION.create_offer + ); + } catch(e) { + this.get_debug().log('[JSJaCJingle:muji] _peer_connection_create_offer > ' + e, 1); + } + }, + + /** + * Triggers the media not obtained error event + * @private + * @fires JSJaCJingleMuji#get_session_initiate_error + * @param {Object} error + */ + _peer_got_user_media_error: function(error) { + this.get_debug().log('[JSJaCJingle:muji] _peer_got_user_media_error', 4); + + try { + /* @function */ + (this.get_session_initiate_error())(this); + this.handle_session_initiate_error(); + + this.get_debug().log('[JSJaCJingle:muji] _peer_got_user_media_error > Failed (' + (error.PERMISSION_DENIED ? 'permission denied' : 'unknown' ) + ').', 1); + } catch(e) { + this.get_debug().log('[JSJaCJingle:muji] _peer_got_user_media_error > ' + e, 1); + } + }, + + /** + * Set a timeout limit to peer connection + * @private + * @param {String} state + * @param {Object} [args] + */ + _peer_timeout: function(state, args) { + try { + // Assert + if(typeof args !== 'object') args = {}; + + var t_iid = this.get_iid(); + + var _this = this; + + setTimeout(function() { + try { + // State did not change? + if(_this.get_iid() == t_iid && _this.get_peer_connection().iceConnectionState == state) { + _this.get_debug().log('[JSJaCJingle:muji] _peer_timeout > Peer timeout.', 2); + } + } catch(e) { + _this.get_debug().log('[JSJaCJingle:muji] _peer_timeout > ' + e, 1); + } + }, ((args.timer || JSJAC_JINGLE_PEER_TIMEOUT_DEFAULT) * 1000)); + } catch(e) { + this.get_debug().log('[JSJaCJingle:muji] _peer_timeout > ' + e, 1); + } + }, + + /** + * Stops ongoing peer connections + * @private + */ + _peer_stop: function() { + this.get_debug().log('[JSJaCJingle:muji] _peer_stop', 4); + + // Detach media streams from DOM view + this._set_local_stream(null); + + // Close the media stream + if(this.get_peer_connection() && + (typeof this.get_peer_connection().close == 'function')) + this.get_peer_connection().close(); + + // Remove this session from router + JSJaCJingle._remove(JSJAC_JINGLE_SESSION_SINGLE, this.get_sid()); + }, + + + + /** + * JSJSAC JINGLE STATES + */ + + /** + * Is user media ready? + * @public + * @returns {Boolean} Ready state + */ + is_ready_user_media: function() { + return (this.get_local_stream() !== null) && true; + }, + + /** + * Is this stanza from a participant? + * @public + * @param {JSJaCPacket} stanza + * @returns {Boolean} Participant state + */ + is_stanza_from_participant: function(stanza) { + var username = this.utils.stanza_username(stanza); + return (this.get_participants(username) in JSJAC_JINGLE_MUJI_STATUS) && true; + }, + + /** + * Is this stanza from local user? + * @public + * @param {JSJaCPacket} stanza + * @returns {Boolean} Local user state + */ + is_stanza_from_local: function(stanza) { + return this.utils.stanza_username(stanza) === this.get_username(); + }, + + + + /** + * JSJSAC JINGLE SHORTCUTS + */ + + /** + * Returns participant status (even if inexistant) + * @private + * @param {String} username + * @returns {String} Status + */ + _shortcut_participant_status: function(username) { + return ((this.get_participants(username) || {}).status || JSJAC_JINGLE_MUJI_STATUS_INACTIVE); + }, + + /** + * Returns local user candidates + * @private + * @returns {Object} Candidates + */ + _shortcut_local_user_candidates: function() { + return this.get_candidates_local(); + }, + + /** + * Gets participant view (or create it) + * @private + * @param {String} username + * @returns {Object} View + */ + _shortcut_participant_view: function(username) { + if((this.get_participants(username) || {}).view) + return this.get_participants(username).view; + + return this.get_add_remote_view()(this, username, this.get_media()); + }, + + + + /** + * JSJSAC JINGLE VARIOUS TOOLS + */ + + /** + * Terminate participant sessions + * @private + * @param {Boolean} [send_terminate] + * @param {Function} [leave_callback] + */ + _terminate_participant_sessions: function(send_terminate, leave_callback) { + try { + // Terminate each session + var cur_username, cur_participant, + participants = this.get_participants(); + + for(cur_username in participants) { + cur_participant = participants[cur_username]; + + if(typeof cur_participant.session != 'undefined') { + if(send_terminate === true) + cur_participant.session.terminate(); + + this.get_remove_remote_view()(this, cur_username); + } + } + + // Execute callback after a while + var _this = this; + + if(typeof leave_callback == 'function') { + setTimeout(function() { + try { + leave_callback(); + } catch(e) { + _this.get_debug().log('[JSJaCJingle:muji] _terminate_participant_sessions > ' + e, 1); + } + }, (JSJAC_JINGLE_MUJI_LEAVE_WAIT * 1000)); + } + } catch(e) { + this.get_debug().log('[JSJaCJingle:muji] _terminate_participant_sessions > ' + e, 1); + } + }, + + /** + * Mutes/unmutes all or given participant(s) + * @private + * @param {String} media_name + * @param {String} mute_action + * @param {String} [username] + */ + _toggle_participants_mute: function(media_name, mute_action, username) { + try { + var i, cur_participant; + var participants = {}; + + // One specific or all? + if(username) + participants[username] = this.get_participants(username); + else + participants = this.get_participants(); + + for(i in participants) { + cur_participant = participants[i]; + + if(cur_participant.session.get_status() === JSJAC_JINGLE_STATUS_ACCEPTED) { + switch(mute_action) { + case JSJAC_JINGLE_SESSION_INFO_MUTE: + cur_participant.session.mute(media_name); break; + + case JSJAC_JINGLE_SESSION_INFO_UNMUTE: + cur_participant.session.unmute(media_name); break; + } + } + } + } catch(e) { + this.get_debug().log('[JSJaCJingle:muji] _toggle_participants_mute > ' + e, 1); + } + }, + + /** + * Defers given participant handler (or executes it) + * @private + * @param {Function} fn + * @returns {Boolean} Defer status + */ + _defer_participant_handlers: function(fn) { + var is_deferred = false; + + try { + var _this = this; + + if(this.get_status() !== JSJAC_JINGLE_MUJI_STATUS_INITIATED && + this.get_status() !== JSJAC_JINGLE_MUJI_STATUS_LEAVING && + this.get_status() !== JSJAC_JINGLE_MUJI_STATUS_LEFT + ) { + this.defer_handler(JSJAC_JINGLE_MUJI_HANDLER_GET_USER_MEDIA, function() { + fn.bind(_this)(true); + }); + + is_deferred = true; + + this.get_debug().log('[JSJaCJingle:muji] _defer_participant_handlers > Deferred participant handler (waiting for user media).', 0); + } else { + fn(false); + } + } catch(e) { + this.get_debug().log('[JSJaCJingle:muji] _defer_participant_handlers > ' + e, 1); + } finally { + return is_deferred; + } + }, + + /** + * Undefers participant handlers + * @private + */ + _undefer_participant_handlers: function() { + try { + // Undefer pending handlers + var i, handlers; + handlers = this.get_deferred_handlers(JSJAC_JINGLE_MUJI_HANDLER_GET_USER_MEDIA); + + if(typeof handlers == 'object' && handlers.length) { + this.get_debug().log('[JSJaCJingle:muji] _undefer_participant_handlers > Submitted to deferred handlers.', 2); + + for(i = 0; i < handlers.length; i++) { + /* @function */ + handlers[i](); + } + + this.undefer_handler(JSJAC_JINGLE_MUJI_HANDLER_GET_USER_MEDIA); + } + } catch(e) { + this.get_debug().log('[JSJaCJingle:muji] _undefer_participant_handlers > ' + e, 1); + } + }, + + /** + * Creates participant Jingle session + * @private + * @param {String} username + * @returns {JSJaCJingleSingle|Object} Jingle session instance + */ + _create_participant_session: function(username) { + var session = null; + + try { + // Create Jingle session + var session_args = this._generate_participant_session_args(username); + + session = new JSJaCJingleSingle(session_args); + + this._set_participants(username, { + status: JSJAC_JINGLE_MUJI_STATUS_INITIATED, + session: session, + view: session_args.remote_view + }); + + // Configure Jingle session + this.get_participants(username).session._set_local_stream_raw( + this.get_local_stream() + ); + } catch(e) { + this.get_debug().log('[JSJaCJingle:muji] _create_participant_session > ' + e, 1); + } finally { + return session; + } + }, + + /** + * Generates participant Jingle session arguments + * @private + * @param {String} username + * @returns {Object} Jingle session arguments + */ + _generate_participant_session_args: function(username) { + args = {}; + + try { + // Main values + args.connection = this.get_connection(); + args.to = this.get_to() + '/' + username; + args.local_view = this.get_local_view(); + args.remote_view = this._shortcut_participant_view(username); + args.local_stream_readonly = true; + + // Propagate values + args.media = this.get_media(); + args.video_source = this.get_video_source(); + args.resolution = this.get_resolution(); + args.bandwidth = this.get_bandwidth(); + args.fps = this.get_fps(); + args.stun = this.get_stun(); + args.turn = this.get_turn(); + args.sdp_trace = this.get_sdp_trace(); + args.net_trace = this.get_net_trace(); + args.debug = this.get_debug(); + + // Handlers + args.session_initiate_pending = this._handle_participant_session_initiate_pending.bind(this); + args.session_initiate_success = this._handle_participant_session_initiate_success.bind(this); + args.session_initiate_error = this._handle_participant_session_initiate_error.bind(this); + args.session_initiate_request = this._handle_participant_session_initiate_request.bind(this); + + args.session_accept_pending = this._handle_participant_session_accept_pending.bind(this); + args.session_accept_success = this._handle_participant_session_accept_success.bind(this); + args.session_accept_error = this._handle_participant_session_accept_error.bind(this); + args.session_accept_request = this._handle_participant_session_accept_request.bind(this); + + args.session_info_pending = this._handle_participant_session_info_pending.bind(this); + args.session_info_success = this._handle_participant_session_info_success.bind(this); + args.session_info_error = this._handle_participant_session_info_error.bind(this); + args.session_info_request = this._handle_participant_session_info_request.bind(this); + + args.session_terminate_pending = this._handle_participant_session_terminate_pending.bind(this); + args.session_terminate_success = this._handle_participant_session_terminate_success.bind(this); + args.session_terminate_error = this._handle_participant_session_terminate_error.bind(this); + args.session_terminate_request = this._handle_participant_session_terminate_request.bind(this); + } catch(e) { + this.get_debug().log('[JSJaCJingle:muji] _generate_participant_session_args > ' + e, 1); + } finally { + return args; + } + }, + + /** + * Autoconfigures MUC room password + * @private + */ + _autoconfigure_room_password: function() { + try { + // Build stanza + stanza = new JSJaCIQ(); + + stanza.setTo(this.get_to()); + stanza.setType(JSJAC_JINGLE_IQ_TYPE_GET); + + stanza.setQuery(NS_JABBER_MUC_OWNER); + + var _this = this; + + this.get_connection().send(stanza, function(_stanza) { + if(_this.get_net_trace()) _this.get_debug().log('[JSJaCJingle:muji] _autoconfigure_room_password > Incoming packet received' + '\n\n' + _stanza.xml()); + + if(_stanza.getType() === JSJAC_JINGLE_IQ_TYPE_ERROR) + _this.get_debug().log('[JSJaCJingle:muji] _autoconfigure_room_password > Could not get room configuration.', 1); + else + _this._receive_autoconfigure_room_password(_stanza); + }); + + if(this.get_net_trace()) this.get_debug().log('[JSJaCJingle:muji] _autoconfigure_room_password > Outgoing packet sent' + '\n\n' + stanza.xml()); + + this.get_debug().log('[JSJaCJingle:muji] _autoconfigure_room_password > Getting room configuration...', 4); + } catch(e) { + this.get_debug().log('[JSJaCJingle:muji] _autoconfigure_room_password > ' + e, 1); + } + }, + + /** + * Receives MUC room password configuration + * @private + * @param {JSJaCPacket} stanza + */ + _receive_autoconfigure_room_password: function(stanza) { + try { + var parse_obj = this._parse_autoconfigure_room_password(stanza); + + this._set_password(parse_obj.password); + + if(parse_obj.password != parse_obj.old_password) { + this._send_autoconfigure_room_password(stanza, parse_obj); + } else { + this.get_debug().log('[JSJaCJingle:muji] _parse_autoconfigure_room_password > Room password already configured (password: ' + parse_obj.password + ').', 2); + } + } catch(e) { + this.get_debug().log('[JSJaCJingle:muji] _receive_autoconfigure_room_password > ' + e, 1); + } + }, + + /** + * Parses MUC room password configuration + * @private + * @param {JSJaCPacket} stanza + * @returns {Object} Parse results + */ + _parse_autoconfigure_room_password: function(stanza) { + var i, + x_data_sel, field_item_sel, password_field_sel, password_value_sel, + old_password, password; + + try { + // Get stanza items + query_sel = stanza.getQuery(NS_JABBER_MUC_OWNER); + + if(!query_sel) throw 'No query element received.'; + + x_data_sel = this.utils.stanza_get_element(query_sel, 'x', NS_JABBER_DATA); + if(!x_data_sel || x_data_sel.length === 0) throw 'No X data element received.'; + + x_data_sel = x_data_sel[0]; + + field_item_sel = this.utils.stanza_get_element(x_data_sel, 'field', NS_JABBER_DATA); + if(!field_item_sel || field_item_sel.length === 0) throw 'No field element received.'; + + for(i = 0; i < field_item_sel.length; i++) { + if(field_item_sel[i].getAttribute('var') === JSJAC_JINGLE_MUJI_MUC_CONFIG_SECRET) { + password_field_sel = field_item_sel[i]; break; + } + } + + if(password_field_sel === undefined) throw 'No password field element received.'; + + password_value_sel = this.utils.stanza_get_element(password_field_sel, 'value', NS_JABBER_DATA); + if(!password_value_sel || password_value_sel.length === 0) throw 'No password field value element received.'; + + password_value_sel = password_value_sel[0]; + + // Get old password + old_password = password_value_sel.nodeValue; + + // Apply password? + if(this.get_password() && old_password != this.get_password()) { + password = this.get_password(); + } else if(old_password) { + password = old_password; + } else { + password = this.utils.generate_password(); + } + } catch(e) { + this.get_debug().log('[JSJaCJingle:muji] _parse_autoconfigure_room_password > ' + e, 1); + } finally { + return { + password : password, + old_password : old_password, + x_data_sel : x_data_sel, + field_item_sel : field_item_sel, + password_field_sel : password_field_sel, + password_value_sel : password_value_sel, + }; + } + }, + + /** + * Receives MUC room password configuration + * @private + * @param {JSJaCPacket} stanza + * @param {Object} parse_obj + */ + _send_autoconfigure_room_password: function(stanza, parse_obj) { + try { + // Change stanza headers + stanza.setID(this.get_id_new()); + stanza.setType(JSJAC_JINGLE_IQ_TYPE_SET); + stanza.setTo(stanza.getFrom()); + stanza.setFrom(null); + + // Change stanza items + parse_obj.x_data_sel.setAttribute('type', JSJAC_JINGLE_MUJI_MUC_OWNER_SUBMIT); + + parse_obj.password_value_sel.parentNode.removeChild(parse_obj.password_value_sel); + parse_obj.password_field_sel.appendChild( + stanza.buildNode('value', { 'xmlns': NS_JABBER_DATA }, parse_obj.password) + ); + + var _this = this; + + this.get_connection().send(stanza, function(_stanza) { + if(_this.get_net_trace()) _this.get_debug().log('[JSJaCJingle:muji] _send_autoconfigure_room_password > Incoming packet received' + '\n\n' + _stanza.xml()); + + if(_stanza.getType() === JSJAC_JINGLE_IQ_TYPE_ERROR) { + _this._set_password(undefined); + + _this.get_debug().log('[JSJaCJingle:muji] _send_autoconfigure_room_password > Could not autoconfigure room password.', 1); + } else { + _this.get_debug().log('[JSJaCJingle:muji] _send_autoconfigure_room_password > Successfully autoconfigured room password.', 2); + } + }); + + this.get_debug().log('[JSJaCJingle:muji] _send_autoconfigure_room_password > Autoconfiguring room password (password: ' + parse_obj.password + ')...', 4); + + if(this.get_net_trace()) this.get_debug().log('[JSJaCJingle:muji] _send_autoconfigure_room_password > Outgoing packet sent' + '\n\n' + stanza.xml()); + } catch(e) { + this.get_debug().log('[JSJaCJingle:muji] _send_autoconfigure_room_password > ' + e, 1); + } + }, + + + + /** + * JSJSAC JINGLE MUJI GETTERS + */ + + /** + * Gets the participants object + * @public + * @param {String} username + * @returns {Object} Participants object + */ + get_participants: function(username) { + if(username) + return this._participants[username]; + + return this._participants; + }, + + /** + * Gets the creator value + * @public + * @returns {String} Creator value + */ + get_creator: function() { + return this.get_to(); + }, + + /** + * Gets the incoming message callback function + * @public + * @event JSJaCJingleMuji#get_room_message_in + * @returns {Function} Incoming message callback function + */ + get_room_message_in: function() { + return this._shortcut_get_handler( + this._room_message_in + ); + }, + + /** + * Gets the outgoing message callback function + * @public + * @event JSJaCJingleMuji#get_room_message_out + * @returns {Function} Outgoing message callback function + */ + get_room_message_out: function() { + return this._shortcut_get_handler( + this._room_message_out + ); + }, + + /** + * Gets the incoming presence callback function + * @public + * @event JSJaCJingleMuji#get_room_presence_in + * @returns {Function} Incoming presence callback function + */ + get_room_presence_in: function() { + return this._shortcut_get_handler( + this._room_presence_in + ); + }, + + /** + * Gets the outgoing presence callback function + * @public + * @event JSJaCJingleMuji#get_room_presence_out + * @returns {Function} Outgoing presence callback function + */ + get_room_presence_out: function() { + return this._shortcut_get_handler( + this._room_presence_out + ); + }, + + /** + * Gets the session prepare pending callback function + * @public + * @event JSJaCJingleMuji#get_session_prepare_pending + * @returns {Function} Session prepare pending callback function + */ + get_session_prepare_pending: function() { + return this._shortcut_get_handler( + this._session_prepare_pending + ); + }, + + /** + * Gets the session prepare success callback function + * @public + * @event JSJaCJingleMuji#get_session_prepare_success + * @returns {Function} Session prepare success callback function + */ + get_session_prepare_success: function() { + return this._shortcut_get_handler( + this._session_prepare_success + ); + }, + + /** + * Gets the session prepare error callback function + * @public + * @event JSJaCJingleMuji#get_session_prepare_error + * @returns {Function} Session prepare error callback function + */ + get_session_prepare_error: function() { + return this._shortcut_get_handler( + this._session_prepare_error + ); + }, + + /** + * Gets the session initiate pending callback function + * @public + * @event JSJaCJingleMuji#get_session_initiate_pending + * @returns {Function} Session initiate pending callback function + */ + get_session_initiate_pending: function() { + return this._shortcut_get_handler( + this._session_initiate_pending + ); + }, + + /** + * Gets the session initiate success callback function + * @public + * @event JSJaCJingleMuji#get_session_initiate_success + * @returns {Function} Session initiate success callback function + */ + get_session_initiate_success: function() { + return this._shortcut_get_handler( + this._session_initiate_success + ); + }, + + /** + * Gets the session initiate error callback function + * @public + * @event JSJaCJingleMuji#get_session_initiate_error + * @returns {Function} Session initiate error callback function + */ + get_session_initiate_error: function() { + return this._shortcut_get_handler( + this._session_initiate_error + ); + }, + + /** + * Gets the session leave pending callback function + * @public + * @event JSJaCJingleMuji#get_session_leave_pending + * @returns {Function} Session leave pending callback function + */ + get_session_leave_pending: function() { + return this._shortcut_get_handler( + this._session_leave_pending + ); + }, + + /** + * Gets the session leave success callback function + * @public + * @event JSJaCJingleMuji#get_session_leave_success + * @returns {Function} Session leave success callback function + */ + get_session_leave_success: function() { + return this._shortcut_get_handler( + this._session_leave_success + ); + }, + + /** + * Gets the session leave error callback function + * @public + * @event JSJaCJingleMuji#get_session_leave_error + * @returns {Function} Session leave error callback function + */ + get_session_leave_error: function() { + return this._shortcut_get_handler( + this._session_leave_error + ); + }, + + /** + * Gets the participant prepare callback function + * @public + * @event JSJaCJingleMuji#get_participant_prepare + * @returns {Function} Participant prepare callback function + */ + get_participant_prepare: function() { + return this._shortcut_get_handler( + this._participant_prepare + ); + }, + + /** + * Gets the participant initiate callback function + * @public + * @event JSJaCJingleMuji#get_participant_initiate + * @returns {Function} Participant initiate callback function + */ + get_participant_initiate: function() { + return this._shortcut_get_handler( + this._participant_initiate + ); + }, + + /** + * Gets the participant leave callback function + * @public + * @event JSJaCJingleMuji#get_participant_leave + * @returns {Function} Participant leave callback function + */ + get_participant_leave: function() { + return this._shortcut_get_handler( + this._participant_leave + ); + }, + + /** + * Gets the participant session initiate pending callback function + * @public + * @event JSJaCJingleMuji#get_participant_session_initiate_pending + * @returns {Function} Participant session initiate pending callback function + */ + get_participant_session_initiate_pending: function() { + return this._shortcut_get_handler( + this._participant_session_initiate_pending + ); + }, + + /** + * Gets the participant session initiate success callback function + * @public + * @event JSJaCJingleMuji#get_participant_session_initiate_success + * @returns {Function} Participant session initiate success callback function + */ + get_participant_session_initiate_success: function() { + return this._shortcut_get_handler( + this._participant_session_initiate_success + ); + }, + + /** + * Gets the participant session initiate error callback function + * @public + * @event JSJaCJingleMuji#get_participant_session_initiate_error + * @returns {Function} Participant session initiate error callback function + */ + get_participant_session_initiate_error: function() { + return this._shortcut_get_handler( + this._participant_session_initiate_error + ); + }, + + /** + * Gets the participant session initiate request callback function + * @public + * @event JSJaCJingleMuji#get_participant_session_initiate_request + * @returns {Function} Participant session initiate request callback function + */ + get_participant_session_initiate_request: function() { + return this._shortcut_get_handler( + this._participant_session_initiate_request + ); + }, + + /** + * Gets the participant session accept pending callback function + * @public + * @event JSJaCJingleMuji#get_participant_session_accept_pending + * @returns {Function} Participant session accept pending callback function + */ + get_participant_session_accept_pending: function() { + return this._shortcut_get_handler( + this._participant_session_accept_pending + ); + }, + + /** + * Gets the participant session accept success callback function + * @public + * @event JSJaCJingleMuji#get_participant_session_accept_success + * @returns {Function} Participant session accept success callback function + */ + get_participant_session_accept_success: function() { + return this._shortcut_get_handler( + this._participant_session_accept_success + ); + }, + + /** + * Gets the participant session accept error callback function + * @public + * @event JSJaCJingleMuji#get_participant_session_accept_error + * @returns {Function} Participant session accept error callback function + */ + get_participant_session_accept_error: function() { + return this._shortcut_get_handler( + this._participant_session_accept_error + ); + }, + + /** + * Gets the participant session accept request callback function + * @public + * @event JSJaCJingleMuji#get_participant_session_accept_request + * @returns {Function} Participant session accept request callback function + */ + get_participant_session_accept_request: function() { + return this._shortcut_get_handler( + this._participant_session_accept_request + ); + }, + + /** + * Gets the participant session info pending callback function + * @public + * @event JSJaCJingleMuji#get_participant_session_info_pending + * @returns {Function} Participant session info pending callback function + */ + get_participant_session_info_pending: function() { + return this._shortcut_get_handler( + this._participant_session_info_pending + ); + }, + + /** + * Gets the participant session info success callback function + * @public + * @event JSJaCJingleMuji#get_participant_session_info_success + * @returns {Function} Participant session info success callback function + */ + get_participant_session_info_success: function() { + return this._shortcut_get_handler( + this._participant_session_info_success + ); + }, + + /** + * Gets the participant session info error callback function + * @public + * @event JSJaCJingleMuji#get_participant_session_info_error + * @returns {Function} Participant session info error callback function + */ + get_participant_session_info_error: function() { + return this._shortcut_get_handler( + this._participant_session_info_error + ); + }, + + /** + * Gets the participant session info request callback function + * @public + * @event JSJaCJingleMuji#get_participant_session_info_request + * @returns {Function} Participant session info request callback function + */ + get_participant_session_info_request: function() { + return this._shortcut_get_handler( + this._participant_session_info_request + ); + }, + + /** + * Gets the participant session terminate pending callback function + * @public + * @event JSJaCJingleMuji#get_participant_session_terminate_pending + * @returns {Function} Participant session terminate pending callback function + */ + get_participant_session_terminate_pending: function() { + return this._shortcut_get_handler( + this._participant_session_terminate_pending + ); + }, + + /** + * Gets the participant session terminate success callback function + * @public + * @event JSJaCJingleMuji#get_participant_session_terminate_success + * @returns {Function} Participant session terminate success callback function + */ + get_participant_session_terminate_success: function() { + return this._shortcut_get_handler( + this._participant_session_terminate_success + ); + }, + + /** + * Gets the participant session terminate error callback function + * @public + * @event JSJaCJingleMuji#get_participant_session_terminate_error + * @returns {Function} Participant session terminate error callback function + */ + get_participant_session_terminate_error: function() { + return this._shortcut_get_handler( + this._participant_session_terminate_error + ); + }, + + /** + * Gets the participant session terminate request callback function + * @public + * @event JSJaCJingleMuji#get_participant_session_terminate_request + * @returns {Function} Participant session terminate request callback function + */ + get_participant_session_terminate_request: function() { + return this._shortcut_get_handler( + this._participant_session_terminate_request + ); + }, + + /** + * Gets the remote view add callback function + * @public + * @event JSJaCJingleMuji#get_add_remote_view + * @returns {Function} Remote view add callback function + */ + get_add_remote_view: function() { + return this._shortcut_get_handler( + this._add_remote_view + ); + }, + + /** + * Gets the remote view removal callback function + * @public + * @event JSJaCJingleMuji#get_remove_remote_view + * @returns {Function} Remote view removal callback function + */ + get_remove_remote_view: function() { + return this._shortcut_get_handler( + this._remove_remote_view + ); + }, + + /** + * Gets the local username + * @public + * @returns {String} Local username + */ + get_username: function() { + return this._username; + }, + + /** + * Gets the room password + * @public + * @returns {String} Room password + */ + get_password: function() { + return this._password; + }, + + /** + * Gets the password protect state + * @public + * @returns {Boolean} Password protect state + */ + get_password_protect: function() { + return this._password_protect; + }, + + /** + * Gets the MUC to value + * @public + * @returns {String} To value for MUC + */ + get_muc_to: function() { + return (this.get_to() + '/' + this.get_username()); + }, + + /** + * Gets the prepended ID + * @public + * @returns {String} Prepended ID value + */ + get_id_pre: function() { + return JSJAC_JINGLE_STANZA_ID_PRE + '_' + (this.get_sid() || '0') + '_' + this.get_username() + '_'; + }, + + /** + * Gets the instance ID + * @public + * @returns {String} IID value + */ + get_iid: function() { + return this._iid; + }, + + /** + * Gets the room owner state + * @public + * @returns {Boolean} Room owner state + */ + get_is_room_owner: function() { + return this._is_room_owner; + }, + + + + /** + * JSJSAC JINGLE MUJI SETTERS + */ + + /** + * Sets the room message in callback function + * @private + * @param {Function} room_message_in + */ + _set_room_message_in: function(room_message_in) { + this._room_message_in = room_message_in; + }, + + /** + * Sets the room message out callback function + * @private + * @param {Function} room_message_out + */ + _set_room_message_out: function(room_message_out) { + this._room_message_out = room_message_out; + }, + + /** + * Sets the room presence in callback function + * @private + * @param {Function} room_presence_in + */ + _set_room_presence_in: function(room_presence_in) { + this._room_presence_in = room_presence_in; + }, + + /** + * Sets the room presence out callback function + * @private + * @param {Function} room_presence_out + */ + _set_room_presence_out: function(room_presence_out) { + this._room_presence_out = room_presence_out; + }, + + /** + * Sets the session prepare pending callback function + * @private + * @param {Function} session_prepare_pending + */ + _set_session_prepare_pending: function(session_prepare_pending) { + this._session_prepare_pending = session_prepare_pending; + }, + + /** + * Sets the session prepare success callback function + * @private + * @param {Function} session_prepare_success + */ + _set_session_prepare_success: function(session_prepare_success) { + this._session_prepare_success = session_prepare_success; + }, + + /** + * Sets the session prepare error callback function + * @private + * @param {Function} session_prepare_error + */ + _set_session_prepare_error: function(session_prepare_error) { + this._session_prepare_error = session_prepare_error; + }, + + /** + * Sets the session initiate pending callback function + * @private + * @param {Function} session_initiate_pending + */ + _set_session_initiate_pending: function(session_initiate_pending) { + this._session_initiate_pending = session_initiate_pending; + }, + + /** + * Sets the session initiate success callback function + * @private + * @param {Function} session_initiate_success + */ + _set_session_initiate_success: function(session_initiate_success) { + this._session_initiate_success = session_initiate_success; + }, + + /** + * Sets the session initiate error callback function + * @private + * @param {Function} session_initiate_error + */ + _set_session_initiate_error: function(session_initiate_error) { + this._session_initiate_error = session_initiate_error; + }, + + /** + * Sets the session leave pending callback function + * @private + * @param {Function} session_leave_pending + */ + _set_session_leave_pending: function(session_leave_pending) { + this._session_leave_pending = session_leave_pending; + }, + + /** + * Sets the session leave success callback function + * @private + * @param {Function} session_leave_success + */ + _set_session_leave_success: function(session_leave_success) { + this._session_leave_success = session_leave_success; + }, + + /** + * Sets the session leave error callback function + * @private + * @param {Function} session_leave_error + */ + _set_session_leave_error: function(session_leave_error) { + this._session_leave_error = session_leave_error; + }, + + /** + * Sets the participant prepare callback function + * @private + * @param {Function} participant_prepare + */ + _set_participant_prepare: function(participant_prepare) { + this._participant_prepare = participant_prepare; + }, + + /** + * Sets the participant initiate callback function + * @private + * @param {Function} participant_initiate + */ + _set_participant_initiate: function(participant_initiate) { + this._participant_initiate = participant_initiate; + }, + + /** + * Sets the participant leave callback function + * @private + * @param {Function} participant_leave + */ + _set_participant_leave: function(participant_leave) { + this._participant_leave = participant_leave; + }, + + /** + * Sets the participant session initiate pending callback function + * @private + * @param {Function} participant_session_initiate_pending + */ + _set_participant_session_initiate_pending: function(participant_session_initiate_pending) { + this._participant_session_initiate_pending = participant_session_initiate_pending; + }, + + /** + * Sets the participant session initiate success callback function + * @private + * @param {Function} participant_session_initiate_success + */ + _set_participant_session_initiate_success: function(participant_session_initiate_success) { + this._participant_session_initiate_success = participant_session_initiate_success; + }, + + /** + * Sets the participant session initiate error callback function + * @private + * @param {Function} participant_session_initiate_error + */ + _set_participant_session_initiate_error: function(participant_session_initiate_error) { + this._participant_session_initiate_error = participant_session_initiate_error; + }, + + /** + * Sets the participant session initiate request callback function + * @private + * @param {Function} participant_session_initiate_request + */ + _set_participant_session_initiate_request: function(participant_session_initiate_request) { + this._participant_session_initiate_request = participant_session_initiate_request; + }, + + /** + * Sets the participant session accept pending callback function + * @private + * @param {Function} participant_session_accept_pending + */ + _set_participant_session_accept_pending: function(participant_session_accept_pending) { + this._participant_session_accept_pending = participant_session_accept_pending; + }, + + /** + * Sets the participant session accept success callback function + * @private + * @param {Function} participant_session_accept_success + */ + _set_participant_session_accept_success: function(participant_session_accept_success) { + this._participant_session_accept_success = participant_session_accept_success; + }, + + /** + * Sets the participant session accept error callback function + * @private + * @param {Function} participant_session_accept_error + */ + _set_participant_session_accept_error: function(participant_session_accept_error) { + this._participant_session_accept_error = participant_session_accept_error; + }, + + /** + * Sets the participant session accept request callback function + * @private + * @param {Function} participant_session_accept_request + */ + _set_participant_session_accept_request: function(participant_session_accept_request) { + this._participant_session_accept_request = participant_session_accept_request; + }, + + /** + * Sets the participant session info pending callback function + * @private + * @param {Function} participant_session_info_pending + */ + _set_participant_session_info_pending: function(participant_session_info_pending) { + this._participant_session_info_pending = participant_session_info_pending; + }, + + /** + * Sets the participant session info success callback function + * @private + * @param {Function} participant_session_info_success + */ + _set_participant_session_info_success: function(participant_session_info_success) { + this._participant_session_info_success = participant_session_info_success; + }, + + /** + * Sets the participant session info error callback function + * @private + * @param {Function} participant_session_info_error + */ + _set_participant_session_info_error: function(participant_session_info_error) { + this._participant_session_info_error = participant_session_info_error; + }, + + /** + * Sets the participant session info request callback function + * @private + * @param {Function} participant_session_info_request + */ + _set_participant_session_info_request: function(participant_session_info_request) { + this._participant_session_info_request = participant_session_info_request; + }, + + /** + * Sets the participant session terminate pending callback function + * @private + * @param {Function} participant_session_terminate_pending + */ + _set_participant_session_terminate_pending: function(participant_session_terminate_pending) { + this._participant_session_terminate_pending = participant_session_terminate_pending; + }, + + /** + * Sets the participant session terminate success callback function + * @private + * @param {Function} participant_session_terminate_success + */ + _set_participant_session_terminate_success: function(participant_session_terminate_success) { + this._participant_session_terminate_success = participant_session_terminate_success; + }, + + /** + * Sets the participant session terminate error callback function + * @private + * @param {Function} participant_session_terminate_error + */ + _set_participant_session_terminate_error: function(participant_session_terminate_error) { + this._participant_session_terminate_error = participant_session_terminate_error; + }, + + /** + * Sets the participant session terminate request callback function + * @private + * @param {Function} participant_session_terminate_request + */ + _set_participant_session_terminate_request: function(participant_session_terminate_request) { + this._participant_session_terminate_request = participant_session_terminate_request; + }, + + /** + * Sets the add remote view callback function + * @private + * @param {Function} add_remote_view + */ + _set_add_remote_view: function(add_remote_view) { + this._add_remote_view = add_remote_view; + }, + + /** + * Sets the remove remote view pending callback function + * @private + * @param {Function} remove_remote_view + */ + _set_remove_remote_view: function(remove_remote_view) { + this._remove_remote_view = remove_remote_view; + }, + + /** + * Sets the participants object + * @private + * @param {String} username + * @param {Object} data_obj + */ + _set_participants: function(username, data_obj) { + if(username === null) { + this._participants = {}; + } else if(data_obj === null) { + if(username in this._participants) + delete this._participants[username]; + } else if(username) { + this._participants[username] = data_obj; + } + }, + + /** + * Sets the local username + * @private + * @param {String} username + */ + _set_username: function(username) { + this._username = username; + }, + + /** + * Sets the room password + * @private + * @param {String} password + */ + _set_password: function(password) { + this._password = password; + }, + + /** + * Sets the password protect state + * @private + * @param {Boolean} password_protect + */ + _set_password_protect: function(password_protect) { + this._password_protect = password_protect; + }, + + /** + * Sets the instance ID + * @private + * @param {String} iid + */ + _set_iid: function(iid) { + this._iid = iid; + }, + + /** + * Sets the room owner state + * @private + * @param {Boolean} is_room_owner + */ + _set_is_room_owner: function(is_room_owner) { + this._is_room_owner = is_room_owner; + }, + } +); +/** + * @fileoverview JSJaC Jingle library - Initialization components + * + * @url https://github.com/valeriansaliou/jsjac-jingle + * @depends https://github.com/sstrigler/JSJaC + * @author Valérian Saliou https://valeriansaliou.name/ + * @license Mozilla Public License v2.0 (MPL v2.0) + */ + + +/** @module jsjac-jingle/init */ +/** @exports JSJaCJingleInit */ + + +/** + * Library initialization class. + * @class + * @classdesc Library initialization class. + * @requires nicolas-van/ring.js + * @requires jsjac-jingle/main + * @see {@link http://ringjs.neoname.eu/|Ring.js} + * @see {@link http://stefan-strigler.de/jsjac-1.3.4/doc/|JSJaC Documentation} + */ +var JSJaCJingleInit = new (ring.create( + /** @lends JSJaCJingleInit.prototype */ + { + /** + * Query the server for external services + * @private + */ + _extdisco: function() { + JSJaCJingleStorage.get_debug().log('[JSJaCJingle:init] _extdisco > Discovering available services...', 2); + + try { + // Pending state (defer other requests) + JSJaCJingle._defer(true); + + // Build request + var request = new JSJaCIQ(); + + request.setTo(JSJaCJingleStorage.get_connection().domain); + request.setType(JSJAC_JINGLE_IQ_TYPE_GET); + + request.getNode().appendChild(request.buildNode('services', { 'xmlns': NS_EXTDISCO })); + + JSJaCJingleStorage.get_connection().send(request, function(response) { + try { + // Parse response + if(response.getType() == JSJAC_JINGLE_IQ_TYPE_RESULT) { + var i, + service_arr, cur_service, + cur_host, cur_password, cur_port, cur_transport, cur_type, cur_username, + store_obj; + + var services = response.getChild('services', NS_EXTDISCO); + + if(services) { + service_arr = services.getElementsByTagNameNS(NS_EXTDISCO, 'service'); + + for(i = 0; i < service_arr.length; i++) { + cur_service = service_arr[i]; + + cur_host = cur_service.getAttribute('host') || null; + cur_port = cur_service.getAttribute('port') || null; + cur_transport = cur_service.getAttribute('transport') || null; + cur_type = cur_service.getAttribute('type') || null; + + cur_username = cur_service.getAttribute('username') || null; + cur_password = cur_service.getAttribute('password') || null; if(!cur_host || !cur_type) continue; - if(!(cur_type in JSJAC_JINGLE_STORE_FALLBACK)) { - JSJAC_JINGLE_STORE_DEBUG.log('[JSJaCJingle] lib:fallback > handle > Service skipped (type: ' + cur_type + ', host: ' + cur_host + ', port: ' + cur_port + ', transport: ' + cur_transport + ').', 4); + if(!(cur_type in JSJaCJingleStorage.get_extdisco())) { + JSJaCJingleStorage.get_debug().log('[JSJaCJingle:init] _extdisco > handle > Service skipped (type: ' + cur_type + ', host: ' + cur_host + ', port: ' + cur_port + ', transport: ' + cur_transport + ').', 4); continue; } - JSJAC_JINGLE_STORE_FALLBACK[cur_type][cur_host] = { + store_obj = { + 'host' : cur_host, 'port' : cur_port, 'transport' : cur_transport, 'type' : cur_type }; if(cur_type == 'turn') { - JSJAC_JINGLE_STORE_FALLBACK[cur_type][cur_host].username = cur_username; - JSJAC_JINGLE_STORE_FALLBACK[cur_type][cur_host].password = cur_password; + store_obj.username = cur_username; + store_obj.password = cur_password; } - JSJAC_JINGLE_STORE_DEBUG.log('[JSJaCJingle] lib:fallback > handle > Fallback service stored (type: ' + cur_type + ', host: ' + cur_host + ', port: ' + cur_port + ', transport: ' + cur_transport + ').', 4); - } else { - JSJAC_JINGLE_STORE_DEBUG.log('[JSJaCJingle] lib:fallback > handle > Fallback service not stored, weird URI (' + cur_url + ').', 0); + JSJaCJingleStorage.get_extdisco()[cur_type].push(store_obj); + + JSJaCJingleStorage.get_debug().log('[JSJaCJingle:init] _extdisco > handle > Service stored (type: ' + cur_type + ', host: ' + cur_host + ', port: ' + cur_port + ', transport: ' + cur_transport + ').', 4); } } - } - JSJAC_JINGLE_STORE_DEBUG.log('[JSJaCJingle] lib:fallback > handle > Finished parsing URIs.', 2); - } else { - JSJAC_JINGLE_STORE_DEBUG.log('[JSJaCJingle] lib:fallback > handle > No URI to parse.', 2); + JSJaCJingleStorage.get_debug().log('[JSJaCJingle:init] _extdisco > handle > Discovered available services.', 2); + } else { + JSJaCJingleStorage.get_debug().log('[JSJaCJingle:init] _extdisco > handle > Could not discover services (server might not support XEP-0215).', 0); + } + } catch(e) { + JSJaCJingleStorage.get_debug().log('[JSJaCJingle:init] _extdisco > handle > ' + e, 1); } - JSJAC_JINGLE_STORE_DEBUG.log('[JSJaCJingle] lib:fallback > handle > Discovered fallback services.', 2); - } else { - JSJAC_JINGLE_STORE_DEBUG.log('[JSJaCJingle] lib:fallback > handle > Could not discover fallback services (API malfunction).', 0); + JSJaCJingleStorage.get_debug().log('[JSJaCJingle:init] _extdisco > Ready.', 2); + + // Execute deferred requests + JSJaCJingle._defer(false); + }); + } catch(e) { + JSJaCJingleStorage.get_debug().log('[JSJaCJingle:init] _extdisco > ' + e, 1); + + // Execute deferred requests + JSJaCJingle._defer(false); + } + }, + + /** + * Query the server for Jingle Relay Nodes services + * @private + */ + _relaynodes: function() { + JSJaCJingleStorage.get_debug().log('[JSJaCJingle:init] _relaynodes > Discovering available Jingle Relay Nodes services...', 2); + + try { + // Pending state (defer other requests) + JSJaCJingle._defer(true); + + // Build request + var request = new JSJaCIQ(); + + request.setTo(JSJaCJingleStorage.get_connection().domain); + request.setType(JSJAC_JINGLE_IQ_TYPE_GET); + + request.getNode().appendChild(request.buildNode('services', { 'xmlns': NS_JABBER_JINGLENODES })); + + JSJaCJingleStorage.get_connection().send(request, function(response) { + try { + // Parse response + if(response.getType() == JSJAC_JINGLE_IQ_TYPE_RESULT) { + var i, + stun_arr, cur_stun, + cur_policy, cur_address, cur_protocol; + + var services = response.getChild('services', NS_JABBER_JINGLENODES); + + if(services) { + // Parse STUN servers + stun_arr = services.getElementsByTagNameNS(NS_JABBER_JINGLENODES, 'stun'); + + for(i = 0; i < stun_arr.length; i++) { + cur_stun = stun_arr[i]; + + cur_policy = cur_stun.getAttribute('policy') || null; + cur_address = cur_stun.getAttribute('address') || null; + cur_port = cur_stun.getAttribute('port') || null; + cur_protocol = cur_stun.getAttribute('protocol') || null; + + if(!cur_address || !cur_protocol || !cur_policy || (cur_policy && cur_policy != 'public')) continue; + + JSJaCJingleStorage.get_relaynodes().stun.push({ + 'host' : cur_address, + 'port' : cur_port, + 'transport' : cur_protocol, + 'type' : 'stun' + }); + + JSJaCJingleStorage.get_debug().log('[JSJaCJingle:init] _relaynodes > handle > STUN service stored (address: ' + cur_address + ', port: ' + cur_port + ', policy: ' + cur_policy + ', protocol: ' + cur_protocol + ').', 4); + } + } + + JSJaCJingleStorage.get_debug().log('[JSJaCJingle:init] _relaynodes > handle > Discovered available Jingle Relay Nodes services.', 2); + } else { + JSJaCJingleStorage.get_debug().log('[JSJaCJingle:init] _relaynodes > handle > Could not discover Jingle Relay Nodes services (server might not support XEP-0278).', 0); + } + } catch(e) { + JSJaCJingleStorage.get_debug().log('[JSJaCJingle:init] _relaynodes > handle > ' + e, 1); + } + + JSJaCJingleStorage.get_debug().log('[JSJaCJingle:init] _relaynodes > Ready.', 2); + + // Execute deferred requests + JSJaCJingle._defer(false); + }); + } catch(e) { + JSJaCJingleStorage.get_debug().log('[JSJaCJingle:init] _relaynodes > ' + e, 1); + + // Execute deferred requests + JSJaCJingle._defer(false); + } + }, + + /** + * Query some external APIs for fallback STUN/TURN (must be configured) + * @private + * @param {String} fallback_url + */ + _fallback: function(fallback_url) { + JSJaCJingleStorage.get_debug().log('[JSJaCJingle:init] _fallback > Discovering fallback services...', 2); + + try { + // Pending state (defer other requests) + JSJaCJingle._defer(true); + + // Generate fallback API URL + fallback_url += '?username=' + + encodeURIComponent(JSJaCJingleStorage.get_connection().username + '@' + JSJaCJingleStorage.get_connection().domain); + + // Proceed request + var xhr = new XMLHttpRequest(); + xhr.open('GET', fallback_url, true); + + xhr.onreadystatechange = function() { + if(xhr.readyState === 4) { + // Success? + if(xhr.status === 200) { + var data = JSON.parse(xhr.responseText); + + var cur_parse, + i, cur_url, + cur_type, cur_host, cur_port, cur_transport, + cur_username, cur_password, + store_obj; + + if(data.uris && data.uris.length) { + JSJaCJingleStorage.get_debug().log('[JSJaCJingle:init] _fallback > handle > Parsing ' + data.uris.length + ' URIs...', 2); + + for(i in data.uris) { + cur_url = data.uris[i]; + + if(cur_url) { + // Parse current URL + cur_parse = R_JSJAC_JINGLE_SERVICE_URI.exec(cur_url); + + if(cur_parse) { + cur_type = cur_parse[1] || null; + cur_host = cur_parse[2] || null; + cur_port = cur_parse[3] || null; + cur_transport = cur_parse[4] || null; + + cur_username = data.username || null; + cur_password = data.password || null; + + if(!cur_host || !cur_type) continue; + + if(!(cur_type in JSJaCJingleStorage.get_fallback())) { + JSJaCJingleStorage.get_debug().log('[JSJaCJingle:init] _fallback > handle > Service skipped (type: ' + cur_type + ', host: ' + cur_host + ', port: ' + cur_port + ', transport: ' + cur_transport + ').', 4); + continue; + } + + store_obj = { + 'host' : cur_host, + 'port' : cur_port, + 'transport' : cur_transport, + 'type' : cur_type + }; + + if(cur_type == 'turn') { + store_obj.username = cur_username; + store_obj.password = cur_password; + } + + JSJaCJingleStorage.get_fallback()[cur_type].push(store_obj); + + JSJaCJingleStorage.get_debug().log('[JSJaCJingle:init] _fallback > handle > Fallback service stored (type: ' + cur_type + ', host: ' + cur_host + ', port: ' + cur_port + ', transport: ' + cur_transport + ').', 4); + } else { + JSJaCJingleStorage.get_debug().log('[JSJaCJingle:init] _fallback > handle > Fallback service not stored, weird URI (' + cur_url + ').', 0); + } + } + } + + JSJaCJingleStorage.get_debug().log('[JSJaCJingle:init] _fallback > handle > Finished parsing URIs.', 2); + } else { + JSJaCJingleStorage.get_debug().log('[JSJaCJingle:init] _fallback > handle > No URI to parse.', 2); + } + + JSJaCJingleStorage.get_debug().log('[JSJaCJingle:init] _fallback > handle > Discovered fallback services.', 2); + } else { + JSJaCJingleStorage.get_debug().log('[JSJaCJingle:init] _fallback > handle > Could not discover fallback services (API malfunction).', 0); + } + + JSJaCJingleStorage.get_debug().log('[JSJaCJingle:init] _fallback > Ready.', 2); + + // Execute deferred requests + JSJaCJingle._defer(false); + } + }; + + xhr.send(); + } catch(e) { + JSJaCJingleStorage.get_debug().log('[JSJaCJingle:init] _fallback > ' + e, 1); + } + }, + } +))(); +/** + * @fileoverview JSJaC Jingle library - Common components + * + * @url https://github.com/valeriansaliou/jsjac-jingle + * @depends https://github.com/sstrigler/JSJaC + * @author Valérian Saliou https://valeriansaliou.name/ + * @license Mozilla Public License v2.0 (MPL v2.0) + */ + + +/** @module jsjac-jingle/main */ +/** @exports JSJaCJingle */ + + +/** + * Library main class. + * @instance + * @requires nicolas-van/ring.js + * @requires sstrigler/JSJaC + * @requires jsjac-jingle/init + * @requires jsjac-jingle/single + * @requires jsjac-jingle/muji + * @see {@link http://ringjs.neoname.eu/|Ring.js} + * @see {@link http://stefan-strigler.de/jsjac-1.3.4/doc/|JSJaC Documentation} + */ +var JSJaCJingle = new (ring.create( + /** @lends JSJaCJingle.prototype */ + { + /** + * Starts a new Jingle session + * @public + * @param {String} type + * @param {Object} [args] + * @returns {JSJaCJingleSingle|JSJaCJingleMuji} JSJaCJingle session instance + */ + session: function(type, args) { + var jingle; + + try { + switch(type) { + case JSJAC_JINGLE_SESSION_SINGLE: + jingle = new JSJaCJingleSingle(args); + break; + + case JSJAC_JINGLE_SESSION_MUJI: + jingle = new JSJaCJingleMuji(args); + break; + + default: + throw ('Unknown session type: ' + type); + } + } catch(e) { + JSJaCJingleStorage.get_debug().log('[JSJaCJingle:main] session > ' + e, 1); + } finally { + return jingle; + } + }, + + /** + * Listens for Jingle events + * @public + * @param {Object} [args] + * @property {JSJaCConnection} [args.connection] - The connection to be attached to. + * @property {Function} [args.single_initiate] - The Jingle session initiate request custom handler. + * @property {Function} [args.muji_invite] - The Muji session invite message custom handler. + * @property {JSJaCDebugger} [args.debug] - A reference to a debugger implementing the JSJaCDebugger interface. + * @property {Boolean} [args.extdisco] - Whether or not to discover external services as per XEP-0215. + * @property {Boolean} [args.relaynodes] - Whether or not to discover relay nodes as per XEP-0278. + * @property {Boolean} [args.fallback] - Whether or not to request STUN/TURN from a fallback URL. + * @see {@link https://github.com/valeriansaliou/jsjac-jingle/blob/master/examples/fallback.json|Fallback JSON Sample} - Fallback URL format. + */ + listen: function(args) { + try { + // Apply arguments + if(args && args.connection) + JSJaCJingleStorage.set_connection(args.connection); + if(args && args.single_initiate) + JSJaCJingleStorage.set_single_initiate(args.single_initiate); + if(args && args.muji_invite) + JSJaCJingleStorage.set_muji_invite(args.muji_invite); + if(args && args.debug) + JSJaCJingleStorage.set_debug(args.debug); + + // Incoming IQs handler + var cur_type, route_map = {}; + route_map[JSJAC_JINGLE_STANZA_IQ] = this._route_iq; + route_map[JSJAC_JINGLE_STANZA_MESSAGE] = this._route_message; + route_map[JSJAC_JINGLE_STANZA_PRESENCE] = this._route_presence; + + for(cur_type in route_map) { + JSJaCJingleStorage.get_connection().registerHandler( + cur_type, + route_map[cur_type].bind(this) + ); } - JSJAC_JINGLE_STORE_DEBUG.log('[JSJaCJingle] lib:fallback > Ready.', 2); + JSJaCJingleStorage.get_debug().log('[JSJaCJingle:main] listen > Listening.', 2); - // Execute deferred requests - JSJaCJingle_defer(false); + // Discover available network services + if(!args || args.extdisco !== false) + JSJaCJingleInit._extdisco(); + if(!args || args.relaynodes !== false) + JSJaCJingleInit._relaynodes(); + if(args.fallback && typeof args.fallback === 'string') + JSJaCJingleInit._fallback(args.fallback); + } catch(e) { + JSJaCJingleStorage.get_debug().log('[JSJaCJingle:main] listen > ' + e, 1); } - }; + }, - xhr.send(); - } catch(e) { - JSJAC_JINGLE_STORE_DEBUG.log('[JSJaCJingle] lib:fallback > ' + e, 1); + /** + * Maps the Jingle disco features + * @public + * @returns {Array} Feature namespaces + */ + disco: function() { + // Check for listen status + var has_muji = (typeof JSJaCJingleStorage.get_muji_invite_raw() == 'function' && true); + var has_jingle = ((has_muji || (typeof JSJaCJingleStorage.get_single_initiate_raw() == 'function')) && true); + + if(JSJAC_JINGLE_AVAILABLE && has_jingle) { + if(has_muji) { + return MAP_DISCO_JINGLE.concat(MAP_DISCO_MUJI); + } else { + return MAP_DISCO_JINGLE; + } + } + + return []; + }, + + /** + * Routes Jingle IQ stanzas + * @private + * @param {JSJaCPacket} stanza + */ + _route_iq: function(stanza) { + try { + var from = stanza.getFrom(); + + if(from) { + var jid_obj = new JSJaCJID(from); + var from_bare = (jid_obj.getNode() + '@' + jid_obj.getDomain()); + + // Single or Muji? + var is_muji = (this._read(JSJAC_JINGLE_SESSION_MUJI, from_bare) !== null); + var is_single = !is_muji; + + var action = null; + var sid = null; + var session_route = null; + + // Route the incoming stanza + var jingle = stanza.getChild('jingle', NS_JINGLE); + + if(jingle) { + sid = jingle.getAttribute('sid'); + action = jingle.getAttribute('action'); + } else { + var stanza_id = stanza.getID(); + + if(stanza_id) { + var is_jingle = stanza_id.indexOf(JSJAC_JINGLE_STANZA_ID_PRE + '_') !== -1; + + if(is_jingle) { + var stanza_id_split = stanza_id.split('_'); + sid = stanza_id_split[1]; + } + } + } + + // WebRTC not available ATM? + if(jingle && !JSJAC_JINGLE_AVAILABLE) { + JSJaCJingleStorage.get_debug().log('[JSJaCJingle:main] _route_iq > Dropped Jingle packet (WebRTC not available).', 0); + + (new JSJaCJingleSingle({ to: from })).send_error(stanza, XMPP_ERROR_SERVICE_UNAVAILABLE); + } else if(is_muji) { + var username, participant; + + username = jid_obj.getResource(); + session_route = this._read(JSJAC_JINGLE_SESSION_MUJI, from_bare); + participant = session_route.get_participants(username); + + // Muji: new session? Or registered one? + if(participant && participant.session && + (participant.session instanceof JSJaCJingleSingle)) { + // Route to Single session + var session_route_single = this._read( + JSJAC_JINGLE_SESSION_SINGLE, + participant.session.get_sid() + ); + + if(session_route_single !== null) { + JSJaCJingleStorage.get_debug().log('[JSJaCJingle:main] _route_iq > [' + username + '] > Routed to Muji participant session (sid: ' + sid + ').', 2); + + session_route_single.handle(stanza); + } else if(stanza.getType() == JSJAC_JINGLE_IQ_TYPE_SET && from) { + JSJaCJingleStorage.get_debug().log('[JSJaCJingle:main] _route_iq > Unknown Muji participant session route (sid: ' + sid + ').', 0); + + (new JSJaCJingleSingle({ to: from })).send_error(stanza, JSJAC_JINGLE_ERROR_UNKNOWN_SESSION); + } + } else if(sid) { + if(action == JSJAC_JINGLE_ACTION_SESSION_INITIATE) { + JSJaCJingleStorage.get_debug().log('[JSJaCJingle:main] _route_iq > [' + username + '] > New Muji participant session (sid: ' + sid + ').', 2); + + session_route._create_participant_session(username).handle(stanza); + } else if(stanza.getType() == JSJAC_JINGLE_IQ_TYPE_SET && from) { + JSJaCJingleStorage.get_debug().log('[JSJaCJingle:main] _route_iq > Unknown Muji participant session (sid: ' + sid + ').', 0); + + (new JSJaCJingleSingle({ to: from })).send_error(stanza, JSJAC_JINGLE_ERROR_UNKNOWN_SESSION); + } + } + } else if(is_single) { + // Single: new session? Or registered one? + session_route = this._read(JSJAC_JINGLE_SESSION_SINGLE, sid); + + if(action == JSJAC_JINGLE_ACTION_SESSION_INITIATE && session_route === null) { + JSJaCJingleStorage.get_debug().log('[JSJaCJingle:main] _route_iq > New Jingle session (sid: ' + sid + ').', 2); + + JSJaCJingleStorage.get_single_initiate()(stanza); + } else if(sid) { + if(session_route !== null) { + JSJaCJingleStorage.get_debug().log('[JSJaCJingle:main] _route_iq > Routed to Jingle session (sid: ' + sid + ').', 2); + + session_route.handle(stanza); + } else if(stanza.getType() == JSJAC_JINGLE_IQ_TYPE_SET && from) { + JSJaCJingleStorage.get_debug().log('[JSJaCJingle:main] _route_iq > Unknown Jingle session (sid: ' + sid + ').', 0); + + (new JSJaCJingleSingle({ to: from })).send_error(stanza, JSJAC_JINGLE_ERROR_UNKNOWN_SESSION); + } + } + } else { + JSJaCJingleStorage.get_debug().log('[JSJaCJingle:main] _route_iq > No route to session, not Jingle nor Muji (sid: ' + sid + ').', 0); + } + } + } catch(e) { + JSJaCJingleStorage.get_debug().log('[JSJaCJingle:main] _route_iq > ' + e, 1); + } + }, + + /** + * Routes Jingle message stanzas + * @private + * @param {JSJaCPacket} stanza + */ + _route_message: function(stanza) { + try { + // Muji? + var from = stanza.getFrom(); + + if(from) { + var jid = new JSJaCJID(from); + var room = jid.getNode() + '@' + jid.getDomain(); + + var session_route = this._read(JSJAC_JINGLE_SESSION_MUJI, room); + + var x_conference = stanza.getChild('x', NS_JABBER_CONFERENCE); + var x_invite = stanza.getChild('x', NS_MUJI_INVITE); + + var is_invite = (x_conference && x_invite && true); + + if(is_invite === true) { + if(session_route === null) { + JSJaCJingleStorage.get_debug().log('[JSJaCJingle:main] _route_message > Muji invite received (room: ' + room + ').', 2); + + // Read invite data + var err = 0; + var args = { + from : (from || err++), + jid : (x_conference.getAttribute('jid') || err++), + password : (x_conference.getAttribute('password') || null), + reason : (x_conference.getAttribute('reason') || null), + media : (x_invite.getAttribute('media') || err++) + }; + + if(err === 0) { + JSJaCJingleStorage.get_muji_invite()(stanza, args); + } else { + JSJaCJingleStorage.get_debug().log('[JSJaCJingle:main] _route_message > Dropped invite because incomplete (room: ' + room + ').', 0); + } + } else { + JSJaCJingleStorage.get_debug().log('[JSJaCJingle:main] _route_message > Dropped invite because Muji already joined (room: ' + room + ').', 0); + } + } else { + if(session_route !== null) { + JSJaCJingleStorage.get_debug().log('[JSJaCJingle:main] _route_message > Routed to Jingle session (room: ' + room + ').', 2); + + session_route.handle_message(stanza); + } + } + } + } catch(e) { + JSJaCJingleStorage.get_debug().log('[JSJaCJingle:main] _route_message > ' + e, 1); + } + }, + + /** + * Routes Jingle presence stanzas + * @private + * @param {JSJaCPacket} stanza + */ + _route_presence: function(stanza) { + try { + // Muji? + var from = stanza.getFrom(); + + if(from) { + var jid = new JSJaCJID(from); + var room = jid.getNode() + '@' + jid.getDomain(); + + var session_route = this._read(JSJAC_JINGLE_SESSION_MUJI, room); + + if(session_route !== null) { + JSJaCJingleStorage.get_debug().log('[JSJaCJingle:main] _route_presence > Routed to Jingle session (room: ' + room + ').', 2); + + session_route.handle_presence(stanza); + } + } + } catch(e) { + JSJaCJingleStorage.get_debug().log('[JSJaCJingle:main] _route_presence > ' + e, 1); + } + }, + + /** + * Adds a new Jingle session + * @private + * @param {String} type + * @param {String} sid + * @param {Object} obj + */ + _add: function(type, sid, obj) { + JSJaCJingleStorage.get_sessions()[type][sid] = obj; + }, + + /** + * Reads a new Jingle session + * @private + * @param {String} type + * @param {String} sid + * @returns {Object} Session + */ + _read: function(type, sid) { + return (sid in JSJaCJingleStorage.get_sessions()[type]) ? JSJaCJingleStorage.get_sessions()[type][sid] : null; + }, + + /** + * Removes a new Jingle session + * @private + * @param {String} type + * @param {String} sid + */ + _remove: function(type, sid) { + delete JSJaCJingleStorage.get_sessions()[type][sid]; + }, + + /** + * Defer given task/execute deferred tasks + * @private + * @param {(Function|Boolean)} arg + */ + _defer: function(arg) { + try { + if(typeof arg == 'function') { + // Deferring? + if(JSJaCJingleStorage.get_defer().deferred) { + (JSJaCJingleStorage.get_defer().fn).push(arg); + + JSJaCJingleStorage.get_debug().log('[JSJaCJingle:main] defer > Registered a function to be executed once ready.', 2); + } + + return JSJaCJingleStorage.get_defer().deferred; + } else if(!arg || typeof arg == 'boolean') { + JSJaCJingleStorage.get_defer().deferred = (arg === true); + + if(JSJaCJingleStorage.get_defer().deferred === false) { + // Execute deferred tasks? + if((--JSJaCJingleStorage.get_defer().count) <= 0) { + JSJaCJingleStorage.get_defer().count = 0; + + JSJaCJingleStorage.get_debug().log('[JSJaCJingle:main] defer > Executing ' + JSJaCJingleStorage.get_defer().fn.length + ' deferred functions...', 2); + + while(JSJaCJingleStorage.get_defer().fn.length) + ((JSJaCJingleStorage.get_defer().fn).shift())(); + + JSJaCJingleStorage.get_debug().log('[JSJaCJingle:main] defer > Done executing deferred functions.', 2); + } + } else { + ++JSJaCJingleStorage.get_defer().count; + } + } + } catch(e) { + JSJaCJingleStorage.get_debug().log('[JSJaCJingle:main] defer > ' + e, 1); + } + }, } -} +))(); \ No newline at end of file diff --git a/source/app/javascripts/jsjac.js b/source/app/javascripts/jsjac.js index 285b040..97b4d21 100644 --- a/source/app/javascripts/jsjac.js +++ b/source/app/javascripts/jsjac.js @@ -14,11 +14,11 @@ Authors: Stefan Strigler, Valérian Saliou, Zash, Maranda * @fileoverview Magic dependency loading. Taken from script.aculo.us * and modified to break it. * @author Stefan Strigler steve@zeank.in-berlin.de - * @version $Revision$ + * @version 1.3 */ var JSJaC = { - Version: '$Rev$', + Version: '1.3', bind: function(fn, obj, optArg) { return function(arg) { return fn.apply(obj, [arg, optArg]); @@ -26,9 +26,6 @@ var JSJaC = { } }; -if (typeof JSJaCConnection == 'undefined') - JSJaC.load(); - /* Copyright 2006 Erik Arvidsson @@ -52,7 +49,7 @@ if (typeof JSJaCConnection == 'undefined') * this code is taken from * http://webfx.eae.net/dhtml/xmlextras/xmlextras.html * @author Stefan Strigler steve@zeank.in-berlin.de - * @version $Revision$ + * @version 1.3 */ /** @@ -282,7 +279,7 @@ if (window.XMLSerializer && /** * @fileoverview Collection of functions to make live easier * @author Stefan Strigler - * @version $Revision$ + * @version 1.3 */ /** @@ -1381,7 +1378,7 @@ JSJaCJSON.parse = function (str) { * @fileoverview This file contains all things that make life easier when * dealing with JIDs * @author Stefan Strigler - * @version $Revision$ + * @version 1.3 */ /** @@ -1714,7 +1711,7 @@ var JSJaCBuilder = { /** * @fileoverview Contains all Jabber/XMPP packet related classes. * @author Stefan Strigler steve@zeank.in-berlin.de - * @version $Revision$ + * @version 1.3 */ var JSJACPACKET_USE_XMLNS = true; @@ -2567,7 +2564,7 @@ function JSJaCKeys(func,oDbg) { * @fileoverview Contains all things in common for all subtypes of connections * supported. * @author Stefan Strigler steve@zeank.in-berlin.de - * @version $Revision$ + * @version 1.3 */ /** @@ -3601,10 +3598,6 @@ JSJaCConnection.prototype._handlePID = function(packet) { if (!packet.getID()) return false; - if (packet.pType() != 'iq' || - (packet.getType() != 'error' && packet.getType() != 'result')) - return false; - var jid = packet.getFrom() || this.jid; if (packet.getFrom() == this.domain) @@ -3723,7 +3716,7 @@ JSJaCConnection.prototype._parseStreamFeatures = function(doc) { // Get legacy session capability if available this.legacy_sessions=null; - if (doc.getElementsByTagName("session")[0]) { + if (doc.getElementsByTagName("session")) { this.legacy_sessions=true; } @@ -3955,7 +3948,7 @@ JSJaCConnection.prototype._setStatus = function(status) { /** * @fileoverview All stuff related to HTTP Binding * @author Stefan Strigler steve@zeank.in-berlin.de - * @version $Revision$ + * @version 1.3 */ /** @@ -4865,7 +4858,26 @@ JSJaCWebSocketConnection.prototype._parseXml = function(s) { this.oDbg.log('Parsing: ' + s, 4); try { doc = XmlDocument.create('stream', NS_STREAM); - if(s.indexOf('') { + // Consider session as closed + this.oDbg.log("session terminated", 1); + + clearTimeout(this._timeout); // remove timer + clearInterval(this._interval); + clearInterval(this._inQto); + + try { + DataStore.removeDB(MINI_HASH, 'jsjac', 'state'); + } catch (e) {} + + this._connected = false; + this._handleEvent('onerror',JSJaCError('503','cancel','session-terminate')); + + this.oDbg.log("Disconnected.",1); + this._handleEvent('ondisconnect'); + + return null; + } else if(s.indexOf('" + s + ""); return doc.documentElement.firstChild; @@ -5018,3 +5030,61 @@ JSJaCWebSocketConnection.prototype._sendRaw = function(xml, cb, arg) { return true; }; +/*exported JSJaCUtils */ + +/** + * Various utilities put together so that they don't pollute global + * name space. + * @namespace + */ +var JSJaCUtils = { + /** + * XOR two strings of equal length. + * @param {string} s1 first string to XOR. + * @param {string} s2 second string to XOR. + * @return {string} s1 ^ s2. + */ + xor: function(s1, s2) { + /*jshint bitwise: false */ + if(!s1) { + return s2; + } + if(!s2) { + return s1; + } + + var result = ''; + for(var i = 0; i < s1.length; i++) { + result += String.fromCharCode(s1.charCodeAt(i) ^ s2.charCodeAt(i)); + } + return result; + }, + + /** + * Create nonce value of given size. + * @param {int} size size of the nonce that should be generated. + * @return {string} generated nonce. + */ + cnonce: function(size) { + var tab = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789"; + var cnonce = ''; + for (var i = 0; i < size; i++) { + cnonce += tab.charAt(Math.round(Math.random(new Date().getTime()) * (tab.length - 1))); + } + return cnonce; + }, + + /** + * Current timestamp. + * @return Seconds since 1.1.1970. + * @type int + */ + now: function() { + if (Date.now && typeof Date.now == 'function') { + return Date.now(); + } else { + return new Date().getTime(); + } + } + +}; diff --git a/source/app/javascripts/jxhr.js b/source/app/javascripts/jxhr.js index 48d4789..5566a0a 100644 --- a/source/app/javascripts/jxhr.js +++ b/source/app/javascripts/jxhr.js @@ -1,116 +1,116 @@ -// jXHR.js (JSON-P XHR) -// v0.1 (c) Kyle Simpson -// License: MIT -// modified by gueron Jonathan to work with strophe lib -// for http://www.iadvize.com - -(function(global){ - var SETTIMEOUT = global.setTimeout, // for better compression - doc = global.document, - callback_counter = 0; - - global.jXHR = function() { - var script_url, - script_loaded, - jsonp_callback, - scriptElem, - publicAPI = null; - - function removeScript() { try { scriptElem.parentNode.removeChild(scriptElem); } catch (err) { } } - - function reset() { - script_loaded = false; - script_url = ""; - removeScript(); - scriptElem = null; - fireReadyStateChange(0); - } - - function ThrowError(msg) { - try { - publicAPI.onerror.call(publicAPI,msg,script_url); - } catch (err) { - //throw new Error(msg); - } - } - - function handleScriptLoad() { - if ((this.readyState && this.readyState!=="complete" && this.readyState!=="loaded") || script_loaded) { return; } - this.onload = this.onreadystatechange = null; // prevent memory leak - script_loaded = true; - if (publicAPI.readyState !== 4) ThrowError("handleScriptLoad: Script failed to load ["+script_url+"]."); - removeScript(); - } - - function parseXMLString(xmlStr) { - var xmlDoc = null; - if(window.DOMParser) { - var parser = new DOMParser(); - xmlDoc = parser.parseFromString(xmlStr,"text/xml"); - } - else { - xmlDoc = new ActiveXObject("Microsoft.XMLDOM"); - xmlDoc.async="false"; - xmlDoc.loadXML(xmlStr); - } - return xmlDoc; - } - - function fireReadyStateChange(rs,args) { - - args = args || []; - publicAPI.readyState = rs; - if (rs == 4) { - publicAPI.responseText = args[0].reply; - publicAPI.responseXML = parseXMLString(args[0].reply); - } - if (typeof publicAPI.onreadystatechange === "function") publicAPI.onreadystatechange.apply(publicAPI,args); - } - - publicAPI = { - onerror:null, - onreadystatechange:null, - readyState:0, - status:200, - responseBody: null, - responseText: null, - responseXML: null, - open:function(method,url){ - reset(); - var internal_callback = "cb"+(callback_counter++); - (function(icb){ - global.jXHR[icb] = function() { - try { fireReadyStateChange.call(publicAPI,4,arguments); } - catch(err) { - publicAPI.readyState = -1; - ThrowError("Script failed to run ["+script_url+"]."); - } - global.jXHR[icb] = null; - }; - })(internal_callback); - script_url = url + '?callback=?jXHR&data='; - script_url = script_url.replace(/=\?jXHR/,"=jXHR."+internal_callback); - fireReadyStateChange(1); - }, - send:function(data){ - script_url = script_url + encodeURIComponent(data); - SETTIMEOUT(function(){ - scriptElem = doc.createElement("script"); - scriptElem.setAttribute("type","text/javascript"); - scriptElem.onload = scriptElem.onreadystatechange = function(){handleScriptLoad.call(scriptElem);}; - scriptElem.setAttribute("src",script_url); - doc.getElementsByTagName("head")[0].appendChild(scriptElem); - },0); - fireReadyStateChange(2); - }, - abort:function(){}, - setRequestHeader:function(){}, // noop - getResponseHeader:function(){return "";}, // basically noop - getAllResponseHeaders:function(){return [];} // ditto - }; - - reset(); - - return publicAPI; - }; -})(window); +// jXHR.js (JSON-P XHR) +// v0.1 (c) Kyle Simpson +// License: MIT +// modified by gueron Jonathan to work with strophe lib +// for http://www.iadvize.com + +(function(global){ + var SETTIMEOUT = global.setTimeout, // for better compression + doc = global.document, + callback_counter = 0; + + global.jXHR = function() { + var script_url, + script_loaded, + jsonp_callback, + scriptElem, + publicAPI = null; + + function removeScript() { try { scriptElem.parentNode.removeChild(scriptElem); } catch (err) { } } + + function reset() { + script_loaded = false; + script_url = ""; + removeScript(); + scriptElem = null; + fireReadyStateChange(0); + } + + function ThrowError(msg) { + try { + publicAPI.onerror.call(publicAPI,msg,script_url); + } catch (err) { + //throw new Error(msg); + } + } + + function handleScriptLoad() { + if ((this.readyState && this.readyState!=="complete" && this.readyState!=="loaded") || script_loaded) { return; } + this.onload = this.onreadystatechange = null; // prevent memory leak + script_loaded = true; + if (publicAPI.readyState !== 4) ThrowError("handleScriptLoad: Script failed to load ["+script_url+"]."); + removeScript(); + } + + function parseXMLString(xmlStr) { + var xmlDoc = null; + if(window.DOMParser) { + var parser = new DOMParser(); + xmlDoc = parser.parseFromString(xmlStr,"text/xml"); + } + else { + xmlDoc = new ActiveXObject("Microsoft.XMLDOM"); + xmlDoc.async="false"; + xmlDoc.loadXML(xmlStr); + } + return xmlDoc; + } + + function fireReadyStateChange(rs,args) { + + args = args || []; + publicAPI.readyState = rs; + if (rs == 4) { + publicAPI.responseText = args[0].reply; + publicAPI.responseXML = parseXMLString(args[0].reply); + } + if (typeof publicAPI.onreadystatechange === "function") publicAPI.onreadystatechange.apply(publicAPI,args); + } + + publicAPI = { + onerror:null, + onreadystatechange:null, + readyState:0, + status:200, + responseBody: null, + responseText: null, + responseXML: null, + open:function(method,url){ + reset(); + var internal_callback = "cb"+(callback_counter++); + (function(icb){ + global.jXHR[icb] = function() { + try { fireReadyStateChange.call(publicAPI,4,arguments); } + catch(err) { + publicAPI.readyState = -1; + ThrowError("Script failed to run ["+script_url+"]."); + } + global.jXHR[icb] = null; + }; + })(internal_callback); + script_url = url + '?callback=?jXHR&data='; + script_url = script_url.replace(/=\?jXHR/,"=jXHR."+internal_callback); + fireReadyStateChange(1); + }, + send:function(data){ + script_url = script_url + encodeURIComponent(data); + SETTIMEOUT(function(){ + scriptElem = doc.createElement("script"); + scriptElem.setAttribute("type","text/javascript"); + scriptElem.onload = scriptElem.onreadystatechange = function(){handleScriptLoad.call(scriptElem);}; + scriptElem.setAttribute("src",script_url); + doc.getElementsByTagName("head")[0].appendChild(scriptElem); + },0); + fireReadyStateChange(2); + }, + abort:function(){}, + setRequestHeader:function(){}, // noop + getResponseHeader:function(){return "";}, // basically noop + getAllResponseHeaders:function(){return [];} // ditto + }; + + reset(); + + return publicAPI; + }; +})(window); diff --git a/source/app/javascripts/links.js b/source/app/javascripts/links.js index 8876467..6107e44 100644 --- a/source/app/javascripts/links.js +++ b/source/app/javascripts/links.js @@ -34,22 +34,30 @@ var Links = (function () { var target; // Links style - if(!style) + if(!style) { style = ''; - else + } else { style = ' style="' + style + '"'; + } // Open in new tabs - if(mode != 'xhtml-im') + if(mode != 'xhtml-im') { target = ' target="_blank"'; - else + } else { target = ''; + } // XMPP address - string = string.replace(/(\s|
|^)(([a-zA-Z0-9\._-]+)@([a-zA-Z0-9\.\/_-]+))(,|\s|$)/gi, '$1$2$5'); + string = string.replace( + /(\s|
|^)(([a-zA-Z0-9\._-]+)@([a-zA-Z0-9\.\/_-]+))(,|\s|$)/gi, + '$1$2$5' + ); // Simple link - string = string.replace(/(\s|
|^|\()((https?|ftp|file|xmpp|irc|mailto|vnc|webcal|ssh|ldap|smb|magnet|spotify)(:)([^<>'"\s\)]+))/gim, '$1$2'); + string = string.replace( + /(\s|
|^|\()((https?|ftp|file|xmpp|irc|mailto|vnc|webcal|ssh|ldap|smb|magnet|spotify)(:)([^<>'"\s\)]+))/gim, + '$1$2' + ); return string; } catch(e) { diff --git a/source/app/javascripts/mam.js b/source/app/javascripts/mam.js index 6485679..0921d23 100644 --- a/source/app/javascripts/mam.js +++ b/source/app/javascripts/mam.js @@ -35,6 +35,7 @@ var MAM = (function () { self.map_reqs = {}; self.map_pending = {}; self.map_states = {}; + self.map_messages = {}; self.msg_queue = {}; @@ -311,11 +312,16 @@ var MAM = (function () { if(c_message[0]) { // Re-build a proper JSJaC message stanza var message = JSJaCPacket.wrapNode(c_message[0]); + var message_node = message.getNode(); // Check message type var type = message.getType() || 'chat'; if(type == 'chat') { + // Display function + var c_display_fn; + var c_display_msg_bool = false; + // Read message data var xid = Common.bareXID(Common.getStanzaFrom(message)); var id = message.getID(); @@ -331,50 +337,100 @@ var MAM = (function () { var hash = hex_md5(xid); var body = message.getBody(); - // Read delay (required since we deal w/ a past message!) - var time, stamp; - var delay = c_delay.attr('stamp'); + // Content message? + if(body) { + // Read delay (required since we deal w/ a past message!) + var time, stamp; + var delay = c_delay.attr('stamp'); - if(delay) { - time = DateUtils.relative(delay); - stamp = DateUtils.extractStamp(Date.jab2date(delay)); - } - - // Last-minute checks before display - if(time && stamp && body) { - var mam_chunk_path = '#' + hash + ' .mam-chunk'; - - // No chat auto-scroll? - var no_scroll = Common.exists(mam_chunk_path); - - // Select the custom target - var c_target_sel = function() { - return $(mam_chunk_path).filter(function() { - return $(this).attr('data-start') <= stamp && $(this).attr('data-end') >= stamp; - }).filter(':first'); - }; - - // Display the message in that target - var c_msg_display = function() { - Message.display(type, from_xid, hash, b_name.htmlEnc(), body, time, stamp, 'old-message', true, null, mode, null, c_target_sel(), no_scroll); - }; - - // Hack: do not display the message in case we would duplicate it w/ current session messages - // only used when initiating a new chat, avoids collisions - if(!(xid in self.map_states) && $('#' + hash).find('.one-line.user-message:last').text() == body) { - return; + if(delay) { + time = DateUtils.relative(delay); + stamp = DateUtils.extractStamp(Date.jab2date(delay)); } + + // Last-minute checks before display + if(time && stamp) { + var mam_chunk_path = '#' + hash + ' .mam-chunk'; + + // Markable message? + var is_markable = Markers.hasRequestMarker(message_node); + + // No chat auto-scroll? + var no_scroll = Common.exists(mam_chunk_path); + + // Select the custom target + var c_target_sel = function() { + return $(mam_chunk_path).filter(function() { + return $(this).attr('data-start') <= stamp && $(this).attr('data-end') >= stamp; + }).filter(':first'); + }; - if(c_target_sel().size()) { // Display the message in that target - c_msg_display(); + c_display_fn = function() { + // Display message + Message.display( + type, + from_xid, + hash, + b_name.htmlEnc(), + body, + time, + stamp, + 'old-message', + true, + null, + mode, + id + '-mam', + c_target_sel(), + no_scroll, + undefined, + undefined, + undefined, + is_markable + ); + + self.map_messages[id] = 1; + }; + + c_display_msg_bool = c_target_sel().size() ? true : false; + + // Hack: do not display the message in case we would duplicate it w/ current session messages + // only used when initiating a new chat, avoids collisions + if(!(xid in self.map_states) && $('#' + hash).find('.one-line.user-message:last').text() == body) { + return; + } + } + } else if(Markers.hasResponseMarker(message_node)) { + // Marked message? (by other party) + if(mode == 'him') { + var marked_message_id = Markers.getMessageID(message_node); + + c_display_fn = function() { + var is_mam_marker = true; + + Markers.handle( + from_xid, + message_node, + is_mam_marker + ); + }; + + c_display_msg_bool = (self.map_messages[marked_message_id] === 1) ? true : false; + } + } + + // Display message? + if(typeof c_display_fn == 'function') { + if(c_display_msg_bool === true) { + // Display message now + c_display_fn(); } else { // Delay display (we may not have received the MAM reply ATM) if(typeof self.msg_queue[xid] != 'object') { self.msg_queue[xid] = []; } - self.msg_queue[xid].push(c_msg_display); + self.msg_queue[xid].push(c_display_fn); } } } diff --git a/source/app/javascripts/markers.js b/source/app/javascripts/markers.js new file mode 100644 index 0000000..d314f28 --- /dev/null +++ b/source/app/javascripts/markers.js @@ -0,0 +1,428 @@ +/* + +Jappix - An open social platform +Implementation of XEP-0333: Chat Markers + +------------------------------------------------- + +License: AGPL +Author: Valérian Saliou + +*/ + +// Bundle +var Markers = (function () { + + /** + * Alias of this + * @private + */ + var self = {}; + + + /* Constants */ + self.MARK_TYPE_MARKABLE = 'markable'; + self.MARK_TYPE_RECEIVED = 'received'; + self.MARK_TYPE_DISPLAYED = 'displayed'; + self.MARK_TYPE_ACKNOWLEDGED = 'acknowledged'; + + self.MARK_TYPES = {}; + self.MARK_TYPES[self.MARK_TYPE_MARKABLE] = 1; + self.MARK_TYPES[self.MARK_TYPE_RECEIVED] = 1; + self.MARK_TYPES[self.MARK_TYPE_DISPLAYED] = 1; + self.MARK_TYPES[self.MARK_TYPE_ACKNOWLEDGED] = 1; + + + /** + * Returns whether entity supports message markers + * @public + * @param {string} xid + * @return {boolean} + */ + self.hasSupport = function(xid) { + + var has_support = false; + + try { + has_support = true ? $('#' + hex_md5(xid)).attr('data-markers') == 'true' : false; + } catch(e) { + Console.error('Markers.hasSupport', e); + } finally { + return has_support; + } + + }; + + + /** + * Returns whether request message is marked or not + * @public + * @param {object} message + * @return {boolean} + */ + self.hasRequestMarker = function(message) { + + var has_request_marker = false; + + try { + has_request_marker = ($(message).find('markable[xmlns="' + NS_URN_MARKERS + '"]').size() ? true : false); + } catch(e) { + Console.error('Markers.hasRequestMarker', e); + } finally { + return has_request_marker; + } + + }; + + + /** + * Returns whether response message is marked or not + * @public + * @param {object} message + * @return {boolean} + */ + self.hasResponseMarker = function(message) { + + var has_response_marker = false; + + try { + var marker_sel = $(message).find('[xmlns="' + NS_URN_MARKERS + '"]'); + + if(marker_sel.size()) { + var mark_type = marker_sel.prop('tagName').toLowerCase(); + + switch(mark_type) { + case self.MARK_TYPE_RECEIVED: + case self.MARK_TYPE_DISPLAYED: + case self.MARK_TYPE_ACKNOWLEDGED: + has_response_marker = true; + break; + } + } + } catch(e) { + Console.error('Markers.hasResponseMarker', e); + } finally { + return has_response_marker; + } + + }; + + + /** + * Returns the marked message ID + * @public + * @param {object} message + * @return {boolean} + */ + self.getMessageID = function(message) { + + var message_id = null; + + try { + message_id = $(message).find('[xmlns="' + NS_URN_MARKERS + '"]').attr('id'); + } catch(e) { + Console.error('Markers.getMessageID', e); + } finally { + return message_id; + } + + }; + + + /** + * Marks a message + * @public + * @param {object} message + * @return {undefined} + */ + self.mark = function(message) { + + try { + message.appendNode('markable', { + 'xmlns': NS_URN_MARKERS + }); + } catch(e) { + Console.error('Markers.mark', e); + } + + }; + + + /** + * Changes received message status (once received or read) + * @public + * @param {string} mark_type + * @param {object} message_id + * @return {undefined} + */ + self.change = function(to, mark_type, message_id, message_sel) { + + try { + if(!(mark_type in self.MARK_TYPES)) { + throw 'Marker type (' + mark_type + ') not supported, aborting.'; + } + + // Store mark state + message_sel.attr('data-mark', mark_type); + + var message = new JSJaCMessage(); + + message.setType('chat'); + message.setTo(to); + + message.appendNode(mark_type, { + 'xmlns': NS_URN_MARKERS, + 'id': message_id + }); + + con.send(message); + + Console.debug('Markers.change', 'Changed marker to: ' + mark_type + ' for message with ID: ' + message_id + ' from: ' + to); + } catch(e) { + Console.error('Markers.change', e); + } + + }; + + + /** + * Handles marker change coming from Carbons + * @public + * @param {string} message + * @return {undefined} + */ + self.handleCarbonChange = function(message) { + + try { + // Check the marker element is existing + var marker_sel = $(message).find('[xmlns="' + NS_URN_MARKERS + '"]'); + + if(marker_sel.size()) { + var xid = Common.bareXID(message.getTo()); + + var mark_type = marker_sel.prop('tagName').toLowerCase(); + var mark_handle = false; + + // Filter allowed markers + switch(mark_type) { + case self.MARK_TYPE_RECEIVED: + case self.MARK_TYPE_DISPLAYED: + case self.MARK_TYPE_ACKNOWLEDGED: + mark_handle = true; + break; + } + + if(mark_handle === true) { + var mark_message_id = marker_sel.attr('id'); + + var message_sel = $('#' + hex_md5(xid) + ' .content .one-line[data-mode="him"][data-markable="true"]').filter(function() { + return ($(this).attr('data-id') + '') === (mark_message_id + ''); + }).filter(':last'); + + if(!message_sel.size()) { + Console.warn('Markers.handleCarbonChange', 'Unknown message marker to keep in sync with Carbons for: ' + xid); + return false; + } + + // Store mark state + message_sel.attr('data-mark', mark_type); + + Console.debug('Markers.handleCarbonChange', 'Received Carbons chat marker (' + mark_type + ') from another resource for: ' + from); + } + } + } catch(e) { + Console.error('Markers.handleCarbonChange', e); + } + + }; + + + /** + * Handles a marked message + * @public + * @param {string} from + * @param {object} message + * @param {boolean} is_mam_marker + * @return {undefined} + */ + self.handle = function(from, message, is_mam_marker) { + + try { + var xid = Common.bareXID(from); + var marker_sel = $(message).find('[xmlns="' + NS_URN_MARKERS + '"]'); + + if(marker_sel.size()) { + var mark_type = marker_sel.prop('tagName').toLowerCase(); + var mark_message_id = marker_sel.attr('id'); + + if(is_mam_marker === true) { + mark_message_id += '-mam'; + } + + // Filter allowed markers + var mark_valid = false; + + switch(mark_type) { + case self.MARK_TYPE_RECEIVED: + case self.MARK_TYPE_DISPLAYED: + case self.MARK_TYPE_ACKNOWLEDGED: + mark_valid = true; + break; + } + + if(mark_valid === false) { + Console.warn('Markers.handle', 'Dropping unexpected chat marker (' + mark_type + ') from: ' + from); + return false; + } + + // Find marked message target + var message_sel = $('#' + hex_md5(xid) + ' .content .one-line[data-mode="me"]').filter(function() { + return ($(this).attr('data-id') + '') === (mark_message_id + ''); + }).filter(':last'); + + if(!message_sel.size()) { + Console.warn('Markers.handle', 'Dropping chat marker (' + mark_type + ') for inexisting message ID (' + mark_message_id + ') from: ' + from); + return false; + } + + Console.debug('Markers.handle', 'Received chat marker (' + mark_type + ') from: ' + from); + + // Finally display received marker + self._display(xid, message_sel, mark_type); + + return true; + } + + return false; + } catch(e) { + Console.error('Markers.handle', e); + return false; + } + + }; + + + /** + * Adds the markers input events + * @public + * @param {object} target + * @param {string} xid + * @param {string} hash + * @param {string} type + * @return {undefined} + */ + self.events = function(target, xid, hash, type) { + + try { + target.focus(function() { + // Not needed + if(target.is(':disabled')) { + return; + } + + // Send displayed message marker? + if(type == 'chat' && self.hasSupport(xid) === true) { + var last_message = $('#' + hash + ' .content .one-line.user-message[data-markable="true"]:last'); + + if(last_message.attr('data-mark') != self.MARK_TYPE_DISPLAYED) { + var last_message_id = last_message.attr('data-id'); + var full_xid = Presence.highestPriority(xid) || xid; + + if(last_message_id) { + self.change( + full_xid, + self.MARK_TYPE_DISPLAYED, + last_message_id, + last_message + ); + } + } + } + }); + } catch(e) { + Console.error('Markers.events', e); + } + + }; + + + /** + * Displays a marker + * @private + * @param {string} xid + * @param {object} message_sel + * @param {string} mark_type + * @return {boolean} + */ + self._display = function(xid, message_sel, mark_type) { + + try { + // Get marker state translation + var marker_sel = message_sel.find('.message-marker'); + var mark_message = null; + var css_classes = 'talk-images message-marker-read'; + var marker_category = null; + + switch(mark_type) { + case self.MARK_TYPE_RECEIVED: + marker_category = 'delivered'; + + marker_sel.removeClass(css_classes); + marker_sel.text( + Common._e("Delivered") + ); + break; + + case self.MARK_TYPE_DISPLAYED: + case self.MARK_TYPE_ACKNOWLEDGED: + marker_category = 'read'; + + marker_sel.addClass(css_classes); + marker_sel.text( + Common._e("Read") + ); + break; + + default: + return false; + } + + if(marker_category !== null) { + marker_sel.attr('data-category', marker_category); + } + + // Reset sending state + message_sel.removeClass('is-sending'); + + // Toggle marker visibility + message_sel.parents('.content').find('.one-line .message-marker').filter(function() { + var data_category = $(this).attr('data-category'); + + if(data_category != 'delivered' && data_category != 'read') { + return false; + } + + // Leave older "read" checkpoint on screen + if(marker_category == 'delivered') { + return data_category == marker_category; + } + + return true; + }).hide(); + marker_sel.show(); + + return true; + } catch(e) { + Console.error('Markers._display', e); + return false; + } + + }; + + + /** + * Return class scope + */ + return self; + +})(); \ No newline at end of file diff --git a/source/app/javascripts/me.js b/source/app/javascripts/me.js index 59b55cc..7b50e43 100644 --- a/source/app/javascripts/me.js +++ b/source/app/javascripts/me.js @@ -93,12 +93,14 @@ var Me = (function () { self.instance = function() { try { + var me_sel = $('#me'); + // Click events - $('#me .content a.go').click(function() { + me_sel.find('.content a.go').click(function() { self.close(); }); - $('#me .bottom .finish').click(self.close); + me_sel.find('.bottom .finish').click(self.close); } catch(e) { Console.error('Me.instance', e); } diff --git a/source/app/javascripts/message.js b/source/app/javascripts/message.js index 4135763..bc043af 100644 --- a/source/app/javascripts/message.js +++ b/source/app/javascripts/message.js @@ -20,6 +20,1296 @@ var Message = (function () { var self = {}; + /** + * Handles MAM forwared messages + * @private + * @param {object} c_mam + * @return {boolean} + */ + self._handleMAM = function(c_mam) { + + try { + var c_mam_sel = $(c_mam); + var c_mam_delay = c_mam_sel.find('delay[xmlns="' + NS_URN_DELAY + '"]'); + var c_mam_forward = c_mam_sel.find('forwarded[xmlns="' + NS_URN_FORWARD + '"]'); + + if(c_mam_forward.size()) { + MAM.handleMessage(c_mam_forward, c_mam_delay); + } + } catch(e) { + Console.error('Message._handleMAM', e); + } finally { + return false; + } + + }; + + + /** + * Handles chatstate messages + * @private + * @param {string} from + * @param {string} hash + * @param {string} type + * @param {object} node + * @return {undefined} + */ + self._handleChatstate = function(from, hash, type, node) { + + try { + /* REF: http://xmpp.org/extensions/xep-0085.html */ + + var node_sel = $(node); + + // Re-process the hash? + var chatstate_hash = (type == 'groupchat') ? hex_md5(from) : hash; + + // Do something depending of the received state + if(node_sel.find('active').size()) { + ChatState.display('active', chatstate_hash, type); + + // Tell Jappix the entity supports chatstates + $('#' + chatstate_hash + ' .message-area').attr('data-chatstates', 'true'); + + Console.log('Active chatstate received from: ' + from); + } else if(node_sel.find('composing').size()) { + ChatState.display('composing', chatstate_hash, type); + + Console.log('Composing chatstate received from: ' + from); + } else if(node_sel.find('paused').size()) { + ChatState.display('paused', chatstate_hash, type); + + Console.log('Paused chatstate received from: ' + from); + } else if(node_sel.find('inactive').size()){ + ChatState.display('inactive', chatstate_hash, type); + + Console.log('Inactive chatstate received from: ' + from); + } else if(node_sel.find('gone').size()){ + ChatState.display('gone', chatstate_hash, type); + + Console.log('Gone chatstate received from: ' + from); + } + } catch(e) { + Console.error('Message._doThat', e); + } + + }; + + + /** + * Handles Jappix App messages + * @private + * @param {string} xid + * @param {string} body + * @param {object} node + * @return {boolean} + */ + self._handleJappixApp = function(xid, body, node) { + + var is_exit = false; + + try { + var node_sel = $(node); + + // Get notification data + var jappix_app_node = node_sel.find('app[xmlns="jappix:app"]'); + var jappix_app_name = jappix_app_node.find('name'); + + var jappix_app_name_id = jappix_app_name.attr('id'); + var jappix_app_name_value = jappix_app_name.text(); + + // Jappix Me notification? + if(jappix_app_name_id == 'me') { + // Get more notification data + var jappix_app_data = jappix_app_node.find('data[xmlns="jappix:app:me"]'); + var jappix_app_data_action = jappix_app_data.find('action'); + var jappix_app_data_url = jappix_app_data.find('url'); + + var jappix_app_data_action_type = jappix_app_data_action.attr('type'); + var jappix_app_data_action_success = jappix_app_data_action.attr('success'); + var jappix_app_data_action_job = jappix_app_data_action.attr('job'); + var jappix_app_data_url_value = jappix_app_data_url.text(); + + // Validate data + if(jappix_app_data_action_type && jappix_app_data_action_success && jappix_app_data_action_job) { + // Filter success + jappix_app_data_action_success = parseInt(jappix_app_data_action_success) == 1 ? 'success' : 'error'; + + // Generate notification namespace + var jappix_me_notification_ns = jappix_app_name_id + '_' + jappix_app_data_action_type + '_' + jappix_app_data_action_job + '_' + jappix_app_data_action_success; + + // Open a new notification + Notification.create(jappix_me_notification_ns, xid, [jappix_app_name_value, jappix_app_data_url_value], body); + + Console.log('Jappix Me notification from: ' + xid + ' with namespace: ' + jappix_me_notification_ns); + + is_exit = true; + } + } + } catch(e) { + Console.error('Message._handleJappixApp', e); + } finally { + return is_exit; + } + + }; + + + /** + * Handles invite messages + * @private + * @param {string} body + * @param {object} node + * @return {boolean} + */ + self._handleInvite = function(body, node) { + + try { + var node_sel = $(node); + + // We get the needed values + var iFrom = node_sel.find('x[xmlns="' + NS_MUC_USER + '"] invite').attr('from'); + var iRoom = node_sel.find('x[xmlns="' + NS_XCONFERENCE + '"]').attr('jid') || from; + + // We display the notification + Notification.create('invite_room', iFrom, [iRoom], body); + + Console.log('Invite Request from: ' + iFrom + ' to join: ' + iRoom); + } catch(e) { + Console.error('Message._handleInvite', e); + } finally { + return false; + } + + }; + + + /** + * Handles request messages + * @private + * @param {string} xid + * @param {object} message + * @param {string} body + * @return {boolean} + */ + self._handleRequest = function(xid, message, body) { + + try { + // Open a new notification + Notification.create('request', xid, [message], body); + + Console.log('HTTP Request from: ' + xid); + } catch(e) { + Console.error('Message._handleRequest', e); + } finally { + return false; + } + + }; + + + /** + * Handles OOB messages + * @private + * @param {string} from + * @param {string} xid + * @param {string} id + * @param {object} node + * @return {boolean} + */ + self._handleOOB = function(from, xid, id, node) { + + try { + OOB.handle(from, id, 'x', node); + + Console.log('Message OOB request from: ' + xid); + } catch(e) { + Console.error('Message._handleOOB', e); + } finally { + return false; + } + + }; + + + /** + * Handles Roster Item Exchange messages + * @private + * @param {string} xid + * @param {object} message + * @param {string} body + * @return {boolean} + */ + self._handleRosterItemExchange = function(xid, message, body) { + + try { + // Open a new notification + Notification.create('rosterx', xid, [message], body); + + Console.log('Roster Item Exchange from: ' + xid); + } catch(e) { + Console.error('Message._handleRosterItemExchange', e); + } finally { + return false; + } + + }; + + + /** + * Handles attention messages + * @private + * @param {string} xid + * @param {string} body + * @return {boolean} + */ + self._handleAttention = function(xid, body) { + + try { + Attention.receive(xid, body); + } catch(e) { + Console.error('Message._handleAttention', e); + } finally { + return false; + } + + }; + + + /** + * Handles normal messages + * @private + * @param {string} xid + * @param {string} subject + * @param {string} body + * @param {string} delay + * @return {boolean} + */ + self._handleNormal = function(xid, subject, body, delay) { + + try { + var message_date = delay || DateUtils.getXMPPTime('utc'); + var message_id = hex_md5(xid + subject + message_date); + + // Store the received message + Inbox.storeMessage(xid, subject, body, 'unread', message_id, message_date); + + // Display the inbox message + if(Common.exists('#inbox')) { + Inbox.displayMessage(xid, subject, body, 'unread', message_id, message_date); + } + + // Check we have new messages (play a sound if any unread messages) + if(Inbox.checkMessages()) { + Audio.play('notification'); + } + + // Send it to the server + Inbox.store(); + } catch(e) { + Console.error('Message._handleNormal', e); + } finally { + return false; + } + + }; + + + /** + * Handles Pubsub event messages + * @private + * @param {string} xid + * @param {string} hash + * @param {object} message + * @param {object} node + * @return {boolean} + */ + self._handlePubsub = function(xid, hash, message, node) { + + try { + // We get the needed values + var items_sel = $(node).find('event items'); + var node_attr = items_sel.attr('node'); + var text; + + // Turn around the different result cases + if(node_attr) { + switch(node_attr) { + // Mood + case NS_MOOD: + // Retrieve the values + var mood = items_sel.find('mood'); + var value = ''; + text = ''; + + // There's something + if(mood.children().size()) { + value = node.getElementsByTagName('mood').item(0).childNodes.item(0).nodeName || ''; + text = mood.find('text').text(); + } + + // Store the PEP event (and display it) + PEP.store(xid, 'mood', value, text); + + break; + + // Activity + case NS_ACTIVITY: + // Retrieve the values + var activity_sel = items_sel.find('activity'); + text = ''; + + // There's something + if(activity_sel.children().size()) { + value = node.getElementsByTagName('activity').item(0).childNodes.item(0).nodeName || ''; + text = activity_sel.find('text').text(); + } + + // Store the PEP event (and display it) + PEP.store(xid, 'activity', value, text); + + break; + + // Tune + case NS_TUNE: + // Retrieve the values + var tune_sel = items_sel.find('tune'); + var artist = tune_sel.find('artist').text(); + var source = tune_sel.find('source').text(); + var title = tune_sel.find('title').text(); + var uri = tune_sel.find('uri').text(); + + // Store the PEP event (and display it) + PEP.store(xid, 'tune', artist, title, source, uri); + + break; + + // Geolocation + case NS_GEOLOC: + // Retrieve the values + var geoloc_sel = items_sel.find('geoloc'); + var lat = geoloc_sel.find('lat').text(); + var lon = geoloc_sel.find('lon').text(); + + // Any extra-values? + var locality = geoloc_sel.find('locality').text(); + var region = geoloc_sel.find('region').text(); + var country = geoloc_sel.find('country').text(); + var human = PEP.humanPosition(locality, region, country); + + // Store the PEP event (and display it) + PEP.store(xid, 'geoloc', lat, lon, human); + + break; + + // Microblog + case NS_URN_MBLOG: + Microblog.display(message, xid, hash, 'mixed', 'push'); + + break; + + // Inbox + case NS_URN_INBOX: + // Do not handle friend's notifications + if(xid == Common.getXID()) { + Notification.handle(message); + } + + break; + } + } + } catch(e) { + Console.error('Message._handlePubsub', e); + } finally { + return false; + } + + }; + + + /** + * Handles room topic messages + * @private + * @param {string} type + * @param {string} from + * @param {string} hash + * @param {string} subject + * @param {string} resource + * @param {string} time + * @param {string} stamp + * @return {undefined} + */ + self._handleRoomTopic = function(type, from, hash, subject, resource, time, stamp) { + + try { + // Filter the vars + var filter_subject = subject.replace(/\n+/g, ' '); + var filteredSubject = Filter.message(filter_subject, resource, true); + var filteredName = resource.htmlEnc(); + + // Display the new subject at the top + $('#' + hash + ' .top .name .bc-infos .muc-topic').replaceWith( + '' + filteredSubject + '' + ); + + // Display the new subject as a system message + if(resource) { + var topic_body = filteredName + ' ' + Common._e("changed the subject to:") + ' ' + Filter.message(subject, resource, true); + self.display(type, from, hash, filteredName, topic_body, time, stamp, 'system-message', false); + } + } catch(e) { + Console.error('Message._handleRoomTopic', e); + } + + }; + + + /** + * Handles groupchat messages + * @private + * @param {string} from + * @param {string} hash + * @param {string} type + * @param {string} resource + * @param {string} id + * @param {string} body + * @param {string} raw_body + * @param {string} time + * @param {number} stamp + * @param {boolean} html_escape + * @param {string} delay + * @param {object} message_edit + * @param {boolean} is_storable + * @return {undefined} + */ + self._handleGroupchat = function(from, hash, type, resource, id, body, raw_body, time, stamp, html_escape, delay, message_edit, is_storable) { + + try { + /* REF: http://xmpp.org/extensions/xep-0045.html */ + + // Message type + var message_type = 'user-message'; + + if(delay && resource) { + // Old message + message_type = 'old-message'; + } else if(!resource) { + // System message + message_type = 'system-message'; + } + + var nickQuote = ''; + + // If this is not an old message + if(message_type == 'user-message') { + var myNick = Name.getMUCNick(hash); + + // If an user quoted our nick (with some checks) + var regex = new RegExp('((^)|( )|(@))' + Common.escapeRegex(myNick) + '(($)|(:)|(,)|( ))', 'gi'); + + if(body.match(regex) && (myNick != resource) && (message_type == 'user-message')) { + nickQuote = ' my-nick'; + } + + // We notify the user if there's a new personal message + if(nickQuote) { + Interface.messageNotify(hash, 'personal'); + Board.quick(from, 'groupchat', raw_body, resource); + Audio.play('receive-message'); + } + + // We notify the user there's a new unread MUC message + else { + Interface.messageNotify(hash, 'unread'); + + // Play sound to all users in the MUC, except user who sent the message. + if(myNick != resource) { + Audio.play('receive-message'); + } + } + } + + // Display the received message + self.display( + type, + from, + hash, + resource.htmlEnc(), + body, + time, + stamp, + message_type, + html_escape, + nickQuote, + undefined, + id, + undefined, + undefined, + message_edit.is_edited, + message_edit.next_count, + is_storable + ); + } catch(e) { + Console.error('Message._handleGroupchat', e); + } + + }; + + + /** + * Handles chat messages + * @private + * @param {string} from + * @param {string} xid + * @param {string} hash + * @param {string} type + * @param {string} resource + * @param {string} id + * @param {string} body + * @param {string} raw_body + * @param {string} time + * @param {number} stamp + * @param {boolean} html_escape + * @param {object} message_edit + * @param {boolean} is_storable + * @param {boolean} is_markable + * @param {boolean} is_groupchat_user + * @param {object} message + * @return {undefined} + */ + self._handleChat = function(from, xid, hash, type, resource, id, body, raw_body, time, stamp, html_escape, message_edit, is_storable, is_markable, is_groupchat_user, message) { + + try { + // Gets the nickname of the user + var fromName = resource; + var chatType = 'chat'; + + // Must send a receipt notification? + if(Receipts.has(message) && (id !== null)) { + Receipts.sendReceived(type, from, id); + } + + // It does not come from a groupchat user, get the full name + if(!is_groupchat_user) { + fromName = Name.getBuddy(xid); + } else { + chatType = 'private'; + } + + // If the chat isn't yet opened, open it ! + if(!Common.exists('#' + hash)) { + // We create a new chat + Chat.create(hash, xid, fromName, chatType); + + // We tell the user that a new chat has started + Audio.play('new-chat'); + } else { + Audio.play('receive-message'); + } + + // Display the received message + var message_sel = self.display( + type, + xid, + hash, + fromName.htmlEnc(), + body, + time, + stamp, + 'user-message', + html_escape, + '', + 'him', + id, + undefined, + undefined, + message_edit.is_edited, + message_edit.next_count, + is_storable, + is_markable + ); + + // We notify the user + Interface.messageNotify(hash, 'personal'); + Board.quick(xid, 'chat', raw_body, fromName); + + // Mark the message + if(is_markable === true && Markers.hasSupport(xid)) { + var mark_type = Markers.MARK_TYPE_RECEIVED; + + if(Interface.hasChanFocus(hash) === true) { + mark_type = Markers.MARK_TYPE_DISPLAYED; + } + + Markers.change(from, mark_type, id, message_sel); + } + } catch(e) { + Console.error('Message._handleChat', e); + } + + }; + + + /** + * Sends an help message + * @private + * @param {string} type + * @param {string} xid + * @param {string} hash + * @return {undefined} + */ + self._sendHelp = function(type, xid, hash) { + + try { + // Help text + var help_text = '

'; + help_text += '' + Common._e("Available shortcuts:") + ''; + + // Shortcuts array + var shortcuts = []; + + // Common shortcuts + shortcuts.push(Common.printf(Common._e("%s removes the chat logs"), '/clear')); + shortcuts.push(Common.printf(Common._e("%s joins a groupchat"), '/join jid')); + shortcuts.push(Common.printf(Common._e("%s closes the chat"), '/part')); + shortcuts.push(Common.printf(Common._e("%s shows the user profile"), '/whois jid')); + + // Groupchat shortcuts + if(type == 'groupchat') { + shortcuts.push(Common.printf(Common._e("%s sends a message to the room"), '/say message')); + shortcuts.push(Common.printf(Common._e("%s changes your nickname"), '/nick nickname')); + shortcuts.push(Common.printf(Common._e("%s sends a message to someone in the room"), '/msg nickname message')); + shortcuts.push(Common.printf(Common._e("%s changes the room topic"), '/topic subject')); + shortcuts.push(Common.printf(Common._e("%s kicks a user of the room"), '/kick [reason:] nickname')); + shortcuts.push(Common.printf(Common._e("%s bans a user of the room"), '/ban [reason:] nickname')); + shortcuts.push(Common.printf(Common._e("%s invites someone to join the room"), '/invite jid message')); + } + + // Generate the code from the array + shortcuts = shortcuts.sort(); + + for(var s in shortcuts) { + help_text += shortcuts[s] + '
'; + } + + help_text += '

'; + + // Display the message + self.display(type, xid, hash, 'help', help_text, DateUtils.getCompleteTime(), DateUtils.getTimeStamp(), 'system-message', false); + + // Reset chatstate + ChatState.send('active', xid, hash); + } catch(e) { + Console.error('Message._sendHelp', e); + } + + }; + + + /** + * Sends a clear message + * @private + * @param {string} xid + * @param {string} hash + * @return {undefined} + */ + self._sendClear = function(xid, hash) { + + try { + Chat.clean(hex_md5(xid)); + + // Reset chatstate + ChatState.send('active', xid, hash); + } catch(e) { + Console.error('Message._sendClear', e); + } + + }; + + + /** + * Sends a join message + * @private + * @param {string} xid + * @param {string} hash + * @param {string} e_1 + * @param {string} e_2 + * @return {undefined} + */ + self._sendJoin = function(xid, hash, e_1, e_2) { + + try { + // Join + var room_gen = Common.generateXID(e_1, 'groupchat'); + var pass = e_2; + + Chat.checkCreate(room_gen, 'groupchat'); + + // Reset chatstate + ChatState.send('active', xid, hash); + } catch(e) { + Console.error('Message._sendJoin', e); + } + + }; + + + /** + * Sends a part message + * @private + * @param {string} xid + * @param {string} type + * @return {undefined} + */ + self._sendPart = function(xid, type) { + + try { + Interface.quitThisChat(xid, hex_md5(xid), type); + } catch(e) { + Console.error('Message._sendPart', e); + } + + }; + + + /** + * Sends a WHOIS message + * @private + * @param {string} type + * @param {string} xid + * @param {string} hash + * @param {string} e_3 + * @return {undefined} + */ + self._sendWHOIS = function(type, xid, hash, e_3) { + + try { + var whois_xid = RegExp.$3; + + // Groupchat WHOIS + if(type == 'groupchat') { + nXID = Utils.getMUCUserXID(xid, whois_xid); + + if(!nXID) { + Board.openThisInfo(6); + } else { + UserInfos.open(nXID); + } + } + + // Chat or private WHOIS + else { + if(!whois_xid) { + UserInfos.open(xid); + } else { + UserInfos.open(whois_xid); + } + } + + // Reset chatstate + ChatState.send('active', xid, hash); + } catch(e) { + Console.error('Message._sendWHOIS', e); + } + + }; + + + /** + * Sends an attention message + * @private + * @param {string} xid + * @param {string} e_2 + * @return {undefined} + */ + self._sendAttention = function(xid, e_2) { + + try { + Attention.send( + xid, + $.trim(e_2) + ); + } catch(e) { + Console.error('Message._sendAttention', e); + } + + }; + + + /** + * Sends a chat message + * @private + * @param {string} xid + * @param {string} hash + * @param {string} id + * @param {string} body + * @param {object} message_packet + * @return {undefined} + */ + self._sendChat = function(xid, hash, id, body, message_packet) { + + try { + message_packet.setType('chat'); + + // Generates the correct message depending of the choosen style + var genMsg = self.generate(message_packet, body, hash); + var html_escape = (genMsg !== 'XHTML'); + + // Receipt request + var receipt_request = Receipts.request(hash); + + if(receipt_request) { + message_packet.appendNode('request', { + 'xmlns': NS_URN_RECEIPTS + }); + } + + // Chatstate + message_packet.appendNode('active', { + 'xmlns': NS_CHATSTATES + }); + + // Markable message? + var has_markers = Markers.hasSupport(xid); + + if(has_markers === true) { + Markers.mark(message_packet); + } + + // Send it! + con.send(message_packet, Errors.handleReply); + + // Filter the xHTML message (for us!) + if(!html_escape) { + body = Filter.xhtml(message_packet.getNode()); + } + + // Finally we display the message we just sent + var my_xid = Common.getXID(); + + var message_sel = self.display( + 'chat', + my_xid, + hash, + Name.getBuddy(my_xid).htmlEnc(), + body, + DateUtils.getCompleteTime(), + DateUtils.getTimeStamp(), + 'user-message', + html_escape, + '', + 'me', + id + ); + + if(has_markers === true) { + message_sel.addClass('is-sending'); + message_sel.find('.message-marker').text( + Common._e("Sending...") + ).show(); + } + + // Receipt timer + if(receipt_request) { + Receipts.checkReceived(hash, id); + } + } catch(e) { + Console.error('Message._sendChat', e); + } + + }; + + + /** + * Sends a groupchat say message + * @private + * @param {string} hash + * @param {string} body + * @param {object} message_packet + * @return {undefined} + */ + self._sendGroupchatSat = function(hash, body, message_packet) { + + try { + body = body.replace(/^\/say (.+)/, '$1'); + + message_packet.setType('groupchat'); + self.generate(message_packet, body, hash); + + con.send(message_packet, Errors.handleReply); + } catch(e) { + Console.error('Message._sendGroupchatSat', e); + } + + }; + + + /** + * Sends a groupchat nick message + * @private + * @param {string} xid + * @param {string} hash + * @param {string} nick + * @param {string} body + * @return {undefined} + */ + self._sendGroupchatNick = function(xid, hash, e_1, body) { + + try { + var nick = $.trim(e_1); + + // Does not exist yet? + if(nick && !Utils.getMUCUserXID(xid, nick)) { + // Send a new presence + Presence.send( + (xid + '/' + nick), + '', + Presence.getUserShow(), + Presence.getUserStatus(), + '', + false, + false, + Errors.handleReply + ); + + // Change the stored nickname + $('#' + hex_md5(xid)).attr('data-nick', escape(nick)); + + // Reset chatstate + ChatState.send('active', xid, hash); + } + } catch(e) { + Console.error('Message._sendGroupchatNick', e); + } + + }; + + + /** + * Sends a groupchat msg message + * @private + * @param {string} xid + * @param {string} hash + * @param {string} e_1 + * @param {string} e_2 + * @param {object} message_packet + * @return {undefined} + */ + self._sendGroupchatMsg = function(xid, hash, e_1, e_2, message_packet) { + + try { + var msg_nick = e_1; + var msg_body = e_2; + var nick_xid = Utils.getMUCUserXID(xid, msg_nick); + + // We check if the user exists + if(!nick_xid) { + Board.openThisInfo(6); + } else if(msg_body) { + message_packet.setType('chat'); + message_packet.setTo(nick_xid); + self.generate(message_packet, msg_body, hash); + + con.send(message_packet, Errors.handleReply); + } + } catch(e) { + Console.error('Message._sendGroupchatMsg', e); + } + + }; + + + /** + * Sends a groupchat XXX message + * @private + * @param {string} xid + * @param {string} hash + * @param {string} body + * @param {object} message_packet + * @return {undefined} + */ + self._sendGroupchatTopic = function(xid, hash, body, message_packet) { + + try { + var topic = body.replace(/^\/topic (.+)/, '$1'); + + message_packet.setType('groupchat'); + message_packet.setSubject(topic); + + con.send(message_packet, Errors.handleMessage); + + // Reset chatstate + ChatState.send('active', xid, hash); + } catch(e) { + Console.error('Message._sendGroupchatTopic', e); + } + + }; + + + /** + * Sends a groupchat XXX message + * @private + * @param {string} xid + * @param {string} hash + * @param {string} body + * @param {string} e_1 + * @param {string} e_2 + * @return {undefined} + */ + self._sendGroupchatBan = function(xid, hash, body, e_1, e_2) { + + try { + var ban_nick = $.trim(e_1); + var ban_reason = ''; + + // We check if the user exists, if not it may be because a reason is given + // we do not check it at first because the nickname could contain ':' + var ban_xid = Utils.getMUCUserRealXID(xid, ban_nick); + + if(!ban_xid && (body.match(/^\/ban ([^:]+)[:]*(.*)/))) { + ban_reason = $.trim(e_1); + ban_nick = $.trim(e_2); + + if(ban_nick.length === 0) { + ban_nick = ban_reason; + ban_reason = ''; + } + + ban_xid = Utils.getMUCUserXID(xid, ban_nick); + } + + Groupchat.banUser(xid, ban_xid, ban_reason); + + // Reset chatstate + ChatState.send('active', xid, hash); + } catch(e) { + Console.error('Message._sendGroupchatBan', e); + } + + }; + + + /** + * Sends a groupchat XXX message + * @private + * @param {string} xid + * @param {string} hash + * @param {string} body + * @param {string} e_1 + * @param {string} e_2 + * @return {undefined} + */ + self._sendGroupchatKick = function(xid, hash, body, e_1, e_2) { + + try { + var kick_nick = $.trim(e_1); + var kick_reason = ''; + + // We check if the user exists, if not it may be because a reason is given + // we do not check it at first because the nickname could contain ':' + var kick_xid = Utils.getMUCUserRealXID(xid, kick_nick); + + if(!kick_xid && (body.match(/^\/kick ([^:]+)[:]*(.*)/))) { + kick_reason = $.trim(e_1); + kick_nick = $.trim(e_2); + + if(kick_nick.length === 0) { + kick_nick = kick_reason; + kick_reason = ''; + } + + kick_xid = Utils.getMUCUserXID(xid, kick_nick); + } + + Groupchat.kickUser(xid, kick_xid, kick_nick, kick_reason); + + // Reset chatstate + ChatState.send('active', xid, hash); + } catch(e) { + Console.error('Message._sendGroupchatKick', e); + } + + }; + + + /** + * Sends a groupchat XXX message + * @private + * @param {string} xid + * @param {string} hash + * @param {string} e_1 + * @param {string} e_2 + * @param {object} message_packet + * @return {undefined} + */ + self._sendGroupchatInvite = function(xid, hash, e_1, e_2, message_packet) { + + try { + var i_xid = e_1; + var invite_reason = e_2; + + var x = message_packet.appendNode('x', { + 'xmlns': NS_MUC_USER + }); + + var node = x.appendChild(message_packet.buildNode('invite', { + 'to': i_xid, + 'xmlns': NS_MUC_USER + })); + + if(invite_reason) { + node.appendChild(message_packet.buildNode('reason', { + 'xmlns': NS_MUC_USER + }, invite_reason)); + } + + con.send(message_packet, Errors.handleReply); + + // Reset chatstate + ChatState.send('active', xid, hash); + } catch(e) { + Console.error('Message._sendGroupchatInvite', e); + } + + }; + + + /** + * Sends a groupchat XXX message + * @private + * @param {string} xid + * @param {string} hash + * @param {string} type + * @param {string} body + * @param {object} message_packet + * @return {undefined} + */ + self._sendGroupchatMessage = function(xid, hash, type, body, message_packet) { + + try { + message_packet.setType('groupchat'); + + // Chatstate + message_packet.appendNode('active', { + 'xmlns': NS_CHATSTATES + }); + + self.generate(message_packet, body, hash); + + con.send(message_packet, Errors.handleMessage); + + Console.info('Message sent to: ' + xid + ' / ' + type); + } catch(e) { + Console.error('Message._sendGroupchatMessage', e); + } + + }; + + + /** + * Sends a groupchat message + * @private + * @param {string} xid + * @param {string} hash + * @param {string} type + * @param {string} body + * @param {object} message_packet + * @return {undefined} + */ + self._sendGroupchat = function(xid, hash, type, body, message_packet) { + + try { + // /say shortcut + if(body.match(/^\/say (.+)/)) { + self._sendGroupchatSat( + hash, + body, + message_packet + ); + } + + // /nick shortcut + else if(body.match(/^\/nick (.+)/)) { + self._sendGroupchatNick( + xid, + hash, + RegExp.$1, + body + ); + } + + // /msg shortcut + else if(body.match(/^\/msg (\S+)\s+(.+)/)) { + self._sendGroupchatMsg( + xid, + hash, + RegExp.$1, + RegExp.$2, + message_packet + ); + } + + // /topic shortcut + else if(body.match(/^\/topic (.+)/)) { + self._sendGroupchatTopic( + xid, + hash, + body, + message_packet + ); + } + + // /ban shortcut + else if(body.match(/^\/ban (.*)/)) { + self._sendGroupchatBan( + xid, + hash, + body, + RegExp.$1, + RegExp.$2 + ); + } + + // /kick shortcut + else if(body.match(/^\/kick (.*)/)) { + self._sendGroupchatKick( + xid, + hash, + body, + RegExp.$1, + RegExp.$2 + ); + } + + // /invite shortcut + else if(body.match(/^\/invite (\S+)\s*(.*)/)) { + self._sendGroupchatInvite( + xid, + hash, + RegExp.$1, + RegExp.$2, + message_packet + ); + } + + // No shortcut, this is a message + else { + self._sendGroupchatMessage( + xid, + hash, + type, + body, + message_packet + ); + } + } catch(e) { + Console.error('Message._sendGroupchat', e); + } + + }; + + /** * Handles the incoming message packets * @public @@ -30,8 +1320,9 @@ var Message = (function () { try { // Error packet? Stop! - if(Errors.handleReply(message)) + if(Errors.handleReply(message)) { return; + } // Carbon-forwarded message? if(message.getChild('sent', NS_URN_CARBONS)) { @@ -45,15 +1336,7 @@ var Message = (function () { var c_mam = message.getChild('result', NS_URN_MAM); if(c_mam) { - var c_mam_sel = $(c_mam); - var c_mam_delay = c_mam_sel.find('delay[xmlns="' + NS_URN_DELAY + '"]'); - var c_mam_forward = c_mam_sel.find('forwarded[xmlns="' + NS_URN_FORWARD + '"]'); - - if(c_mam_forward.size()) { - MAM.handleMessage(c_mam_forward, c_mam_delay); - } - - return; + return self._handleMAM(c_mam); } // We get the message items @@ -72,11 +1355,16 @@ var Message = (function () { var resource = Common.thisResource(from); var hash = hex_md5(xid); var xHTML = $(node).find('html body').size(); - var GCUser = false; + var is_groupchat_user = false; + + // This message comes from a Muji room (ignore) + if(Muji.is_room(xid)) { + return false; + } // This message comes from a groupchat user if(Utils.isPrivate(xid) && ((type == 'chat') || !type) && resource) { - GCUser = true; + is_groupchat_user = true; xid = from; hash = hex_md5(xid); } @@ -95,295 +1383,61 @@ var Message = (function () { } // Received message - if(Receipts.hasReceived(message)) + if(Receipts.hasReceived(message)) { return Receipts.messageReceived(hash, id); - + } + // Chatstate message - if(node && !delay && ((((type == 'chat') || !type) && !Common.exists('#page-switch .' + hash + ' .unavailable')) || (type == 'groupchat'))) { - /* REF: http://xmpp.org/extensions/xep-0085.html */ - - // Re-process the hash - var chatstate_hash = hash; - - if(type == 'groupchat') - chatstate_hash = hex_md5(from); - - // Do something depending of the received state - if($(node).find('active').size()) { - ChatState.display('active', chatstate_hash, type); - - // Tell Jappix the entity supports chatstates - $('#' + chatstate_hash + ' .message-area').attr('data-chatstates', 'true'); - - Console.log('Active chatstate received from: ' + from); - } - - else if($(node).find('composing').size()) { - ChatState.display('composing', chatstate_hash, type); - - Console.log('Composing chatstate received from: ' + from); - } - - else if($(node).find('paused').size()) { - ChatState.display('paused', chatstate_hash, type); - - Console.log('Paused chatstate received from: ' + from); - } - - else if($(node).find('inactive').size()){ - ChatState.display('inactive', chatstate_hash, type); - - Console.log('Inactive chatstate received from: ' + from); - } - - else if($(node).find('gone').size()){ - ChatState.display('gone', chatstate_hash, type); - - Console.log('Gone chatstate received from: ' + from); - } + if(node && !delay && + ((((type == 'chat') || !type) && !Common.exists('#page-switch .' + hash + ' .unavailable')) || (type == 'groupchat'))) { + self._handleChatstate(from, hash, type, node); } // Jappix App message if(message.getChild('app', 'jappix:app')) { - // Get notification data - var jappix_app_node = $(node).find('app[xmlns="jappix:app"]'); - var jappix_app_name = jappix_app_node.find('name'); - - var jappix_app_name_id = jappix_app_name.attr('id'); - var jappix_app_name_value = jappix_app_name.text(); - - // Jappix Me notification? - if(jappix_app_name_id == 'me') { - // Get more notification data - var jappix_app_data = jappix_app_node.find('data[xmlns="jappix:app:me"]'); - var jappix_app_data_action = jappix_app_data.find('action'); - var jappix_app_data_url = jappix_app_data.find('url'); - - var jappix_app_data_action_type = jappix_app_data_action.attr('type'); - var jappix_app_data_action_success = jappix_app_data_action.attr('success'); - var jappix_app_data_action_job = jappix_app_data_action.attr('job'); - var jappix_app_data_url_value = jappix_app_data_url.text(); - - // Validate data - if(jappix_app_data_action_type && jappix_app_data_action_success && jappix_app_data_action_job) { - // Filter success - jappix_app_data_action_success = parseInt(jappix_app_data_action_success) == 1 ? 'success' : 'error'; - - // Generate notification namespace - var jappix_me_notification_ns = jappix_app_name_id + '_' + jappix_app_data_action_type + '_' + jappix_app_data_action_job + '_' + jappix_app_data_action_success; - - // Open a new notification - Notification.create(jappix_me_notification_ns, xid, [jappix_app_name_value, jappix_app_data_url_value], body); - - Console.log('Jappix Me notification from: ' + xid + ' with namespace: ' + jappix_me_notification_ns); - - return false; - } + if(self._handleJappixApp(xid, body, node) === true) { + return false; } } // Invite message if($(node).find('x[xmlns="' + NS_MUC_USER + '"] invite').size()) { - // We get the needed values - var iFrom = $(node).find('x[xmlns="' + NS_MUC_USER + '"] invite').attr('from'); - var iRoom = $(node).find('x[xmlns="' + NS_XCONFERENCE + '"]').attr('jid'); - - // Old invite method? - if(!iRoom) - iRoom = from; - - // We display the notification - Notification.create('invite_room', iFrom, [iRoom], body); - - Console.log('Invite Request from: ' + iFrom + ' to join: ' + iRoom); - - return false; + return self._handleInvite(body, node); } // Request message if(message.getChild('confirm', NS_HTTP_AUTH)) { - // Open a new notification - Notification.create('request', xid, [message], body); - - Console.log('HTTP Request from: ' + xid); - - return false; + return self._handleRequest(xid, message, body); } // OOB message if(message.getChild('x', NS_XOOB)) { - OOB.handle(from, id, 'x', node); - - Console.log('Message OOB request from: ' + xid); - - return false; + return self._handleOOB(from, xid, id, node); } // Roster Item Exchange message if(message.getChild('x', NS_ROSTERX)) { - // Open a new notification - Notification.create('rosterx', xid, [message], body); - - Console.log('Roster Item Exchange from: ' + xid); - - return false; + return self._handleRosterItemExchange(xid, message, body); + } + + // Attention message + if(message.getChild('attention', NS_URN_ATTENTION)) { + return self._handleAttention(xid, body); } // Normal message if((type == 'normal') && body) { - // Message date - var messageDate = delay; - - // No message date? - if(!messageDate) - messageDate = DateUtils.getXMPPTime('utc'); - - // Message ID - var messageID = hex_md5(xid + subject + messageDate); - - // We store the received message - Inbox.storeMessage(xid, subject, body, 'unread', messageID, messageDate); - - // Display the inbox message - if(Common.exists('#inbox')) - Inbox.displayMessage(xid, subject, body, 'unread', messageID, messageDate); - - // Check we have new messages (play a sound if any unread messages) - if(Inbox.checkMessages()) - Audio.play('notification'); - - // Send it to the server - Inbox.store(); - - return false; + return self._handleNormal(xid, subject, body, delay); } // PubSub event if($(node).find('event').attr('xmlns') == NS_PUBSUB_EVENT) { - // We get the needed values - var iParse = $(node).find('event items'); - var iNode = iParse.attr('node'); - var tText; - - // Turn around the different result cases - if(iNode) { - switch(iNode) { - // Mood - case NS_MOOD: - // Retrieve the values - var iMood = iParse.find('mood'); - var fValue = ''; - tText = ''; - - // There's something - if(iMood.children().size()) { - // Read the value - fValue = node.getElementsByTagName('mood').item(0).childNodes.item(0).nodeName; - - // Read the text - tText = iMood.find('text').text(); - - // Avoid errors - if(!fValue) - fValue = ''; - } - - // Store the PEP event (and display it) - PEP.store(xid, 'mood', fValue, tText); - - break; - - // Activity - case NS_ACTIVITY: - // Retrieve the values - var iActivity = iParse.find('activity'); - var sValue = ''; - tText = ''; - - // There's something - if(iActivity.children().size()) { - // Read the value - fValue = node.getElementsByTagName('activity').item(0).childNodes.item(0).nodeName; - - // Read the text - tText = iActivity.find('text').text(); - - // Avoid errors - if(!fValue) - fValue = ''; - } - - // Store the PEP event (and display it) - PEP.store(xid, 'activity', fValue, tText); - - break; - - // Tune - case NS_TUNE: - // Retrieve the values - var iTune = iParse.find('tune'); - var tArtist = iTune.find('artist').text(); - var tSource = iTune.find('source').text(); - var tTitle = iTune.find('title').text(); - var tURI = iTune.find('uri').text(); - - // Store the PEP event (and display it) - PEP.store(xid, 'tune', tArtist, tTitle, tSource, tURI); - - break; - - // Geolocation - case NS_GEOLOC: - // Retrieve the values - var iGeoloc = iParse.find('geoloc'); - var tLat = iGeoloc.find('lat').text(); - var tLon = iGeoloc.find('lon').text(); - - // Any extra-values? - var tLocality = iGeoloc.find('locality').text(); - var tRegion = iGeoloc.find('region').text(); - var tCountry = iGeoloc.find('country').text(); - var tHuman = PEP.humanPosition(tLocality, tRegion, tCountry); - - // Store the PEP event (and display it) - PEP.store(xid, 'geoloc', tLat, tLon, tHuman); - - break; - - // Microblog - case NS_URN_MBLOG: - Microblog.display(message, xid, hash, 'mixed', 'push'); - - break; - - // Inbox - case NS_URN_INBOX: - // Do not handle friend's notifications - if(xid == Common.getXID()) - Notification.handle(message); - - break; - } - } - - return false; + return self._handlePubsub(xid, hash, message, node); } // If this is a room topic message if(subject && (type == 'groupchat')) { - // Filter the vars - var filter_subject = subject.replace(/\n+/g, ' '); - var filteredSubject = Filter.message(filter_subject, resource, true); - var filteredName = resource.htmlEnc(); - - // Display the new subject at the top - $('#' + hash + ' .top .name .bc-infos .muc-topic').replaceWith('' + filteredSubject + ''); - - // Display the new subject as a system message - if(resource) { - var topic_body = filteredName + ' ' + Common._e("changed the subject to:") + ' ' + Filter.message(subject, resource, true); - self.display(type, from, hash, filteredName, topic_body, time, stamp, 'system-message', false); - } + self._handleRoomTopic(type, from, hash, subject, resource, time, stamp); } // If the message has a content @@ -396,104 +1450,67 @@ var Message = (function () { xHTML = 0; } - //If this is a xHTML message + // If this is a xHTML message if(xHTML) { html_escape = false; // Filter the xHTML message body = Filter.xhtml(node); } + + // Catch message edit (XEP-0308) + var message_edit = Correction.catch(message, hash, type); + + // Storable message? + var is_storable = message.getChild('no-permanent-storage', NS_URN_HINTS) ? false : true; // Groupchat message if(type == 'groupchat') { - /* REF: http://xmpp.org/extensions/xep-0045.html */ - - // We generate the message type and time - var message_type = 'user-message'; - - // This is an old message - if(delay && resource) { - message_type = 'old-message'; - } - - // This is a system message - else if(!resource) { - message_type = 'system-message'; - } - - var nickQuote = ''; - - // If this is not an old message - if(message_type == 'user-message') { - var myNick = Name.getMUCNick(hash); - - // If an user quoted our nick (with some checks) - var regex = new RegExp('((^)|( )|(@))' + Common.escapeRegex(myNick) + '(($)|(:)|(,)|( ))', 'gi'); - - if(body.match(regex) && (myNick != resource) && (message_type == 'user-message')) - nickQuote = ' my-nick'; - - // We notify the user if there's a new personal message - if(nickQuote) { - Interface.messageNotify(hash, 'personal'); - Board.quick(from, 'groupchat', raw_body, resource); - Audio.play('receive-message'); - } - - // We notify the user there's a new unread MUC message - else { - Interface.messageNotify(hash, 'unread'); - - // Play sound to all users in the MUC, except user who sent the message. - if(myNick != resource) { - Audio.play('receive-message'); - } - } - } - - // Display the received message - self.display(type, from, hash, resource.htmlEnc(), body, time, stamp, message_type, html_escape, nickQuote); + self._handleGroupchat( + from, + hash, + type, + resource, + id, + body, + raw_body, + time, + stamp, + html_escape, + delay, + message_edit, + is_storable + ); + } else { + // Markable message? + var is_markable = Markers.hasRequestMarker(node); + + self._handleChat( + from, + xid, + hash, + type, + resource, + id, + body, + raw_body, + time, + stamp, + html_escape, + message_edit, + is_storable, + is_markable, + is_groupchat_user, + message + ); } - - // Chat message - else { - // Gets the nickname of the user - var fromName = resource; - var chatType = 'chat'; - - // Must send a receipt notification? - if(Receipts.has(message) && (id !== null)) - Receipts.sendReceived(type, from, id); - - // It does not come from a groupchat user, get the full name - if(!GCUser) { - fromName = Name.getBuddy(xid); - } else { - chatType = 'private'; - } - - // If the chat isn't yet opened, open it ! - if(!Common.exists('#' + hash)) { - // We create a new chat - Chat.create(hash, xid, fromName, chatType); - - // We tell the user that a new chat has started - Audio.play('new-chat'); - } else { - Audio.play('receive-message'); - } - - // Display the received message - self.display(type, xid, hash, fromName.htmlEnc(), body, time, stamp, 'user-message', html_escape, '', 'him'); - - // We notify the user - Interface.messageNotify(hash, 'personal'); - Board.quick(xid, 'chat', raw_body, fromName); - } - - return false; } - + + // Message marker? + if(Markers.hasResponseMarker(node)) { + return Markers.handle(from, node); + } + return false; } catch(e) { Console.error('Message.handle', e); @@ -519,293 +1536,70 @@ var Message = (function () { var nXID; // If the user didn't entered any message, stop - if(!body || !xid) + if(!body || !xid) { return false; + } // We send the message through the XMPP network - var aMsg = new JSJaCMessage(); - aMsg.setTo(xid); + var message_packet = new JSJaCMessage(); + message_packet.setTo(xid); // Set an ID var id = genID(); - aMsg.setID(id); + message_packet.setID(id); // /help shortcut if(body.match(/^\/help\s*(.*)/)) { - // Help text - var help_text = '

'; - help_text += '' + Common._e("Available shortcuts:") + ''; - - // Shortcuts array - var shortcuts = []; - - // Common shortcuts - shortcuts.push(Common.printf(Common._e("%s removes the chat logs"), '/clear')); - shortcuts.push(Common.printf(Common._e("%s joins a groupchat"), '/join jid')); - shortcuts.push(Common.printf(Common._e("%s closes the chat"), '/part')); - shortcuts.push(Common.printf(Common._e("%s shows the user profile"), '/whois jid')); - - // Groupchat shortcuts - if(type == 'groupchat') { - shortcuts.push(Common.printf(Common._e("%s sends a message to the room"), '/say message')); - shortcuts.push(Common.printf(Common._e("%s changes your nickname"), '/nick nickname')); - shortcuts.push(Common.printf(Common._e("%s sends a message to someone in the room"), '/msg nickname message')); - shortcuts.push(Common.printf(Common._e("%s changes the room topic"), '/topic subject')); - shortcuts.push(Common.printf(Common._e("%s kicks a user of the room"), '/kick [reason:] nickname')); - shortcuts.push(Common.printf(Common._e("%s bans a user of the room"), '/ban [reason:] nickname')); - shortcuts.push(Common.printf(Common._e("%s invites someone to join the room"), '/invite jid message')); - } - - // Generate the code from the array - shortcuts = shortcuts.sort(); - - for(var s in shortcuts) - help_text += shortcuts[s] + '
'; - - help_text += '

'; - - // Display the message - self.display(type, xid, hash, 'help', help_text, DateUtils.getCompleteTime(), DateUtils.getTimeStamp(), 'system-message', false); - - // Reset chatstate - ChatState.send('active', xid, hash); + self._sendHelp(type, xid, hash); } // /clear shortcut else if(body.match(/^\/clear/)) { - Chat.clean(hex_md5(xid)); - - // Reset chatstate - ChatState.send('active', xid, hash); + self._sendClear(xid, hash); } // /join shortcut else if(body.match(/^\/join (\S+)\s*(.*)/)) { - // Join - var room = Common.generateXID(RegExp.$1, 'groupchat'); - var pass = RegExp.$2; - - Chat.checkCreate(room, 'groupchat'); - - // Reset chatstate - ChatState.send('active', xid, hash); + self._sendJoin(xid, hash, RegExp.$1, RegExp.$2); } // /part shortcut - else if(body.match(/^\/part\s*(.*)/) && (!Utils.isAnonymous() || (Utils.isAnonymous() && (xid != Common.generateXID(ANONYMOUS_ROOM, 'groupchat'))))) - Interface.quitThisChat(xid, hex_md5(xid), type); + else if(body.match(/^\/part\s*(.*)/) && + (!Utils.isAnonymous() || (Utils.isAnonymous() && + (xid != Common.generateXID(ANONYMOUS_ROOM, 'groupchat'))))) { + self._sendPart(xid, type); + } // /whois shortcut else if(body.match(/^\/whois(( (\S+))|($))/)) { - var whois_xid = RegExp.$3; - - // Groupchat WHOIS - if(type == 'groupchat') { - nXID = Utils.getMUCUserXID(xid, whois_xid); - - if(!nXID) { - Board.openThisInfo(6); - } else { - UserInfos.open(nXID); - } - } - - // Chat or private WHOIS - else { - if(!whois_xid) { - UserInfos.open(xid); - } else { - UserInfos.open(whois_xid); - } - } - - // Reset chatstate - ChatState.send('active', xid, hash); + self._sendWHOIS(type, xid, hash, RegExp.$3); } - + + // /attention shortcut + else if(body.match(/^\/attention( (.*))?/) && type == 'chat') { + self._sendAttention(xid, RegExp.$2); + } + // Chat message type else if(type == 'chat') { - aMsg.setType('chat'); - - // Generates the correct message depending of the choosen style - var genMsg = self.generate(aMsg, body, hash); - var html_escape = (genMsg !== 'XHTML'); - - // Receipt request - var receipt_request = Receipts.request(hash); - - if(receipt_request) - aMsg.appendNode('request', {'xmlns': NS_URN_RECEIPTS}); - - // Chatstate - aMsg.appendNode('active', {'xmlns': NS_CHATSTATES}); - - // Send it! - con.send(aMsg, Errors.handleReply); - - // Filter the xHTML message (for us!) - if(!html_escape) { - body = Filter.xhtml(aMsg.getNode()); - } - - // Finally we display the message we just sent - var my_xid = Common.getXID(); - - self.display('chat', my_xid, hash, Name.getBuddy(my_xid).htmlEnc(), body, DateUtils.getCompleteTime(), DateUtils.getTimeStamp(), 'user-message', html_escape, '', 'me', id); - - // Receipt timer - if(receipt_request) { - Receipts.checkReceived(hash, id); - } + self._sendChat( + xid, + hash, + id, + body, + message_packet + ); } // Groupchat message type else if(type == 'groupchat') { - // /say shortcut - if(body.match(/^\/say (.+)/)) { - body = body.replace(/^\/say (.+)/, '$1'); - - aMsg.setType('groupchat'); - self.generate(aMsg, body, hash); - - con.send(aMsg, Errors.handleReply); - } - - // /nick shortcut - else if(body.match(/^\/nick (.+)/)) { - var nick = body.replace(/^\/nick (.+)/, '$1'); - - // Does not exist yet? - if(!Utils.getMUCUserXID(xid, nick)) { - // Send a new presence - Presence.send(xid + '/' + nick, '', Presence.getUserShow(), self.getUserStatus(), '', false, false, Errors.handleReply); - - // Change the stored nickname - $('#' + hex_md5(xid)).attr('data-nick', escape(nick)); - - // Reset chatstate - ChatState.send('active', xid, hash); - } - } - - // /msg shortcut - else if(body.match(/^\/msg (\S+)\s+(.+)/)) { - var msg_nick = RegExp.$1; - var msg_body = RegExp.$2; - nXID = Utils.getMUCUserXID(xid, msg_nick); - - // We check if the user exists - if(!nXID) - Board.openThisInfo(6); - - // If the private message is not empty - else if(msg_body) { - aMsg.setType('chat'); - aMsg.setTo(nXID); - self.generate(aMsg, msg_body, hash); - - con.send(aMsg, Errors.handleReply); - } - } - - // /topic shortcut - else if(body.match(/^\/topic (.+)/)) { - var topic = body.replace(/^\/topic (.+)/, '$1'); - - aMsg.setType('groupchat'); - aMsg.setSubject(topic); - - con.send(aMsg, Errors.handleMessage); - - // Reset chatstate - ChatState.send('active', xid, hash); - } - - // /ban shortcut - else if(body.match(/^\/ban (.*)/)) { - var ban_nick = $.trim(RegExp.$1); - var ban_reason = ''; - - // We check if the user exists, if not it may be because a reason is given - // we do not check it at first because the nickname could contain ':' - var ban_xid = Utils.getMUCUserRealXID(xid, ban_nick); - - if(!ban_xid && (body.match(/^\/ban ([^:]+)[:]*(.*)/))) { - ban_reason = $.trim(RegExp.$1); - ban_nick = $.trim(RegExp.$2); - - if(ban_nick.length === 0) { - ban_nick = ban_reason; - ban_reason = ''; - } - - ban_xid = Utils.getMUCUserXID(xid, ban_nick); - } - - Groupchat.banUser(xid, ban_xid, ban_reason); - - // Reset chatstate - ChatState.send('active', xid, hash); - } - - // /kick shortcut - else if(body.match(/^\/kick (.*)/)) { - var kick_nick = $.trim(RegExp.$1); - var kick_reason = ''; - - // We check if the user exists, if not it may be because a reason is given - // we do not check it at first because the nickname could contain ':' - var kick_xid = Utils.getMUCUserRealXID(xid, kick_nick); - - if(!kick_xid && (body.match(/^\/kick ([^:]+)[:]*(.*)/))) { - kick_reason = $.trim(RegExp.$1); - kick_nick = $.trim(RegExp.$2); - - if(kick_nick.length === 0) { - kick_nick = kick_reason; - kick_reason = ''; - } - - kick_xid = Utils.getMUCUserXID(xid, kick_nick); - } - - Groupchat.kickUser(xid, kick_xid, kick_nick, kick_reason); - - // Reset chatstate - ChatState.send('active', xid, hash); - } - - // /invite shortcut - else if(body.match(/^\/invite (\S+)\s*(.*)/)) { - var i_xid = RegExp.$1; - var invite_reason = RegExp.$2; - - var x = aMsg.appendNode('x', {'xmlns': NS_MUC_USER}); - var aNode = x.appendChild(aMsg.buildNode('invite', {'to': i_xid, 'xmlns': NS_MUC_USER})); - - if(invite_reason) { - aNode.appendChild(aMsg.buildNode('reason', {'xmlns': NS_MUC_USER}, invite_reason)); - } - - con.send(aMsg, Errors.handleReply); - - // Reset chatstate - ChatState.send('active', xid, hash); - } - - // No shortcut, this is a message - else { - aMsg.setType('groupchat'); - - // Chatstate - aMsg.appendNode('active', {'xmlns': NS_CHATSTATES}); - - self.generate(aMsg, body, hash); - - con.send(aMsg, Errors.handleMessage); - - Console.info('Message sent to: ' + xid + ' / ' + type); - } + self._sendGroupchat( + xid, + hash, + type, + body, + message_packet + ); } // We reset the message input @@ -849,8 +1643,9 @@ var Message = (function () { // Loop the input values $(checkbox).filter(':checked').each(function() { // If there is a previous element - if(style) + if(style) { style += ' '; + } // Get the current style switch($(this).attr('class')) { @@ -966,16 +1761,16 @@ var Message = (function () { /** * Generates the correct message code * @public - * @param {object} aMsg + * @param {object} message_packet * @param {string} body * @param {string} hash * @return {string} */ - self.generate = function(aMsg, body, hash) { + self.generate = function(message_packet, body, hash) { try { // Create the classical body - aMsg.setBody(body); + message_packet.setBody(body); // Get the style var style = $('#' + hash + ' .message-area').attr('style'); @@ -985,12 +1780,18 @@ var Message = (function () { // Explode the message body new lines (to create one

element by line) var new_lines = new Array(body); - if(body.match(/\n/)) + if(body.match(/\n/)) { new_lines = body.split('\n'); + } // Create the XML elements - var aHtml = aMsg.appendNode('html', {'xmlns': NS_XHTML_IM}); - var aBody = aHtml.appendChild(aMsg.buildNode('body', {'xmlns': NS_XHTML})); + var html_node = message_packet.appendNode('html', { + 'xmlns': NS_XHTML_IM + }); + + var body_node = html_node.appendChild(message_packet.buildNode('body', { + 'xmlns': NS_XHTML + })); // Use the exploded body array to create one element per entry for(var i in new_lines) { @@ -998,8 +1799,9 @@ var Message = (function () { var cLine = new_lines[i]; // Blank line, we put a
- if(cLine.match(/(^)(\s+)($)/) || !cLine) - aBody.appendChild(aMsg.buildNode('br', {'xmlns': NS_XHTML})); + if(cLine.match(/(^)(\s+)($)/) || !cLine) { + body_node.appendChild(message_packet.buildNode('br', {'xmlns': NS_XHTML})); + } // Line with content, we put a

else { @@ -1010,7 +1812,7 @@ var Message = (function () { cLine = Links.apply(cLine, 'xhtml-im', style); // Append the filtered line - $(aBody).append($('

' + cLine + '

')); + $(body_node).append($('

' + cLine + '

')); } } @@ -1042,9 +1844,15 @@ var Message = (function () { * @param {string} id * @param {object} c_target_sel * @param {boolean} no_scroll - * @return {undefined} + * @param {boolean} is_edited + * @param {number} edit_count + * @param {boolean} is_storable + * @param {boolean} is_markable + * @return {object} */ - self.display = function(type, xid, hash, name, body, time, stamp, message_type, html_escape, nick_quote, mode, id, c_target_sel, no_scroll) { + self.display = function(type, xid, hash, name, body, time, stamp, message_type, html_escape, nick_quote, mode, id, c_target_sel, no_scroll, is_edited, edit_count, is_storable, is_markable) { + + var message_sel = null; try { // Target @@ -1052,6 +1860,18 @@ var Message = (function () { c_target_sel = $('#' + hash + ' .content'); } + // Auto-calculate mode for groupchat? + if(type == 'groupchat' && !mode) { + var own_groupchat_nickname = $('#' + hash).attr('data-nick') || ''; + own_groupchat_nickname = unescape(own_groupchat_nickname); + + if(name == own_groupchat_nickname) { + mode = 'me'; + } else { + mode = 'him'; + } + } + // Generate some stuffs var has_avatar = false; var xid_hash = ''; @@ -1079,12 +1899,24 @@ var Message = (function () { if(id) { data_id = ' data-id="' + id + '"'; } + + // Edited state? + var data_edited = ''; + var data_edit_count = ''; + + if(is_edited === true) { + data_edited = ' data-edited="true"'; + data_edit_count = ' data-edit-count="' + (edit_count || 0) + '"'; + } + + // Markable state? + var data_markable = (is_markable === true) ? ' data-markable="true"' : ''; // Filter the message var filteredMessage = Filter.message(body, name, html_escape); // Display the received message in the room - var messageCode = '
'; + var message_code = '
'; // Name color attribute if(type == 'groupchat') { @@ -1121,7 +1953,30 @@ var Message = (function () { filteredMessage = '' + filteredMessage + ''; } - messageCode += filteredMessage + '
'; + message_code += filteredMessage + '
'; + + // Message correction containers + if(message_type == 'user-message') { + // Message edit properties + message_code += '' + Common._e("Edit") + ''; + + if(is_edited === true) { + var edit_text = Common._e("Edited"); + + if(edit_count > 1) { + edit_text = Common._e(Common.printf("Edited (%s)", edit_count)); + } + + message_code += '' + edit_text + ''; + } + } + + // Message marker container + if(type == 'chat') { + message_code += ''; + } + + message_code += '
'; // Must group it? if(!grouped) { @@ -1137,16 +1992,18 @@ var Message = (function () { message_head += '' + time + '' + name + ''; // Generate message code - messageCode = '
' + message_head + messageCode + '
'; + message_code = '
' + message_head + message_code + '
'; } // Write the code in the DOM if(grouped) { - c_target_sel.find('.one-group:last').append(messageCode); + c_target_sel.find('.one-group:last').append(message_code); } else { - c_target_sel.append(messageCode); + c_target_sel.append(message_code); } + message_sel = c_target_sel.find('.one-line:last'); + // Store the last MAM.REQ_MAX message groups if(!Features.enabledMAM() && (type == 'chat') && (message_type == 'user-message')) { // Filter the DOM @@ -1160,9 +2017,13 @@ var Message = (function () { var store_html = $(dom_filter).parent().html(); // Store the data - if(store_html) { + if(store_html && is_storable !== false) { self.storeLocalArchive(hash, store_html); } + + if(is_storable === false) { + Console.info('Message.display', 'Won\'t store message since it\'s labeled as not storable (' + xid + ')'); + } } // Must get the avatar? @@ -1174,8 +2035,22 @@ var Message = (function () { if(can_scroll) { Interface.autoScroll(hash); } + + // Add click events + var xid_to = $('#' + hash).attr('data-xid'); + + if(xid_to) { + xid_to = unescape(xid_to); + + $('#' + hash + ' .content .one-line:last .correction-edit').click(function() { + Correction.enter(xid_to); + return false; + }); + } } catch(e) { Console.error('Message.display', e); + } finally { + return message_sel; } }; @@ -1186,4 +2061,4 @@ var Message = (function () { */ return self; -})(); \ No newline at end of file +})(); diff --git a/source/app/javascripts/microblog.js b/source/app/javascripts/microblog.js index 51dca65..529b715 100644 --- a/source/app/javascripts/microblog.js +++ b/source/app/javascripts/microblog.js @@ -1,1837 +1,1967 @@ -/* - -Jappix - An open social platform -These are the microblog JS scripts for Jappix - -------------------------------------------------- - -License: AGPL -Authors: Valérian Saliou, Maranda - -*/ - -// Bundle -var Microblog = (function () { - - /** - * Alias of this - * @private - */ - var self = {}; - - - /** - * Completes arrays of an entry's attached files - * @public - * @param {string} selector - * @param {object} tFName - * @param {object} tFURL - * @param {object} tFThumb - * @param {object} tFSource - * @param {object} tFLength - * @param {object} tFEComments - * @param {object} tFNComments - * @return {undefined} - */ - self.attached = function(selector, tFName, tFURL, tFThumb, tFSource, tFType, tFLength, tFEComments, tFNComments) { - - try { - if($(selector).attr('title')) - tFName.push($(selector).attr('title')); - else - tFName.push(''); - - if($(selector).attr('href')) - tFURL.push($(selector).attr('href')); - else - tFURL.push(''); - - if($(selector).find('link[rel="self"][title="thumb"]:first').attr('href')) - tFThumb.push($(selector).find('link[rel="self"][title="thumb"]:first').attr('href')); - else - tFThumb.push(''); - - if($(selector).attr('source')) - tFSource.push($(selector).attr('source')); - else - tFSource.push(''); - - if($(selector).attr('type')) - tFType.push($(selector).attr('type')); - else - tFType.push(''); - - if($(selector).attr('length')) - tFLength.push($(selector).attr('length')); - else - tFLength.push(''); - - // Comments? - var comments_href_c = $(selector).find('link[rel="replies"][title="comments_file"]:first').attr('href'); - - if(comments_href_c && comments_href_c.match(/^xmpp:(.+)\?;node=(.+)/)) { - tFEComments.push(RegExp.$1); - tFNComments.push(decodeURIComponent(RegExp.$2)); - } - - else { - tFEComments.push(''); - tFNComments.push(''); - } - } catch(e) { - Console.error('Microblog.attached', e); - } - - }; - - - /** - * Displays a given microblog item - * @public - * @param {object} packet - * @param {string} from - * @param {string} hash - * @param {string} mode - * @param {string} way - * @return {undefined} - */ - self.display = function(packet, from, hash, mode, way) { - - try { - // Get some values - var iParse = $(packet.getNode()).find('items item'); - - iParse.each(function() { - // Initialize - var tContent, tFiltered, tTime, tDate, tStamp, tBody, tName, tID, tHash, tIndividual, tFEClick; - var tHTMLEscape = false; - - // Arrays - var tFName = []; - var tFURL = []; - var tFThumb = []; - var tFSource = []; - var tFType = []; - var tFLength = []; - var tFEComments = []; - var tFNComments = []; - var aFURL = []; - var aFCat = []; - - // Get the values - tDate = $(this).find('published').text(); - tBody = $(this).find('body').text(); - tID = $(this).attr('id'); - tName = Name.getBuddy(from); - tHash = 'update-' + hex_md5(tName + tDate + tID); - - // Read attached files with a thumb (place them at first) - $(this).find('link[rel="enclosure"]:has(link[rel="self"][title="thumb"])').each(function() { - self.attached(this, tFName, tFURL, tFThumb, tFSource, tFType, tFLength, tFEComments, tFNComments); - }); - - // Read attached files without any thumb - $(this).find('link[rel="enclosure"]:not(:has(link[rel="self"][title="thumb"]))').each(function() { - self.attached(this, tFName, tFURL, tFThumb, tFSource, tFType, tFLength, tFEComments, tFNComments); - }); - - // Get the repeat value - var uRepeat = [$(this).find('author name').text(), Common.explodeThis(':', $(this).find('author uri').text(), 1)]; - var uRepeated = false; - - if(!uRepeat[0]) - uRepeat = [Name.getBuddy(from), uRepeat[1]]; - if(!uRepeat[1]) - uRepeat = [uRepeat[0], from]; - - // Repeated? - if(uRepeat[1] != from) - uRepeated = true; - - // Get the comments node - var entityComments, nodeComments; - - // Get the comments - var comments_href = $(this).find('link[title="comments"]:first').attr('href'); - - if(comments_href && comments_href.match(/^xmpp:(.+)\?;node=(.+)/)) { - entityComments = RegExp.$1; - nodeComments = decodeURIComponent(RegExp.$2); - } - - // No comments node? - if(!entityComments || !nodeComments) { - entityComments = ''; - nodeComments = ''; - } - - // Get the stamp & time - if(tDate) { - tStamp = DateUtils.extractStamp(Date.jab2date(tDate)); - tTime = DateUtils.relative(tDate); - } - - else { - tStamp = DateUtils.getTimeStamp(); - tTime = ''; - } - - // Get the item geoloc - var tGeoloc = ''; - var sGeoloc = $(this).find('geoloc:first'); - var gLat = sGeoloc.find('lat').text(); - var gLon = sGeoloc.find('lon').text(); - - if(gLat && gLon) { - tGeoloc += ''; - - // Human-readable name? - var gHuman = PEP.humanPosition( - sGeoloc.find('locality').text(), - sGeoloc.find('region').text(), - sGeoloc.find('country').text() - ); - - if(gHuman) - tGeoloc += gHuman.htmlEnc(); - else - tGeoloc += gLat.htmlEnc() + '; ' + gLon.htmlEnc(); - - tGeoloc += ''; - } - - // Entry content: HTML, parse! - if($(this).find('content[type="html"]').size()) { - // Filter the xHTML message - tContent = Filter.xhtml(this); - tHTMLEscape = false; - } - - // Entry content: Fallback on PLAIN? - if(!tContent) { - tContent = $(this).find('content[type="text"]').text(); - - if(!tContent) { - // Legacy? - tContent = $(this).find('title:not(source > title)').text(); - - // Last chance? - if(!tContent) - tContent = tBody; - } - - // Trim the content - tContent = $.trim(tContent); - tHTMLEscape = true; - } - - // Any content? - if(tContent) { - // Apply links to message body - tFiltered = Filter.message(tContent, tName.htmlEnc(), tHTMLEscape); - - // Display the received message - var html = '
' + - '
' + - '
' + - '' + - '
' + - '
' + - - '
' + - '

'; - - // Is it a repeat? - if(uRepeated) - html += ''; - - html += '' + tName.htmlEnc() + ' ' + tFiltered + '

' + - '

' + tTime + tGeoloc + '

'; - - // Any file to display? - if(tFURL.length) - html += '

'; - - // Generate an array of the files URL - for(var a = 0; a < tFURL.length; a++) { - // Not enough data? - if(!tFURL[a]) - continue; - - // Push the current URL! (YouTube or file) - if(tFURL[a].match(/(\w{3,5})(:)(\S+)((\.youtube\.com\/watch(\?v|\?\S+v|\#\!v|\#\!\S+v)\=)|(youtu\.be\/))([^& ]+)((&\S)|(&\S)|\s|$)/gim)) { - aFURL.push($.trim(RegExp.$8)); - aFCat.push('youtube'); - } - - else if(IntegrateBox.can(Common.strAfterLast('.', tFURL[a]))) { - aFURL.push(tFURL[a]); - aFCat.push(Utils.fileCategory(Common.strAfterLast('.', tFURL[a]))); - } - } - - // Add each file code - for(var f = 0; f < tFURL.length; f++) { - // Not enough data? - if(!tFURL[f]) - continue; - - // Get the file type - var tFLink = tFURL[f]; - var tFExt = Common.strAfterLast('.', tFLink); - var tFCat = Utils.fileCategory(tFExt); - - // Youtube video? - if(tFLink.match(/(\w{3,5})(:)(\S+)((\.youtube\.com\/watch(\?v|\?\S+v|\#\!v|\#\!\S+v)\=)|(youtu\.be\/))([^& ]+)((&\S)|(&\S)|\s|$)/gim)) { - tFLink = $.trim(RegExp.$8); - tFCat = 'youtube'; - } - - // Supported image/video/sound - if(IntegrateBox.can(tFExt) || (tFCat == 'youtube')) - tFEClick = 'onclick="return IntegrateBox.apply(\'' + Utils.encodeOnclick(tFLink) + '\', \'' + Utils.encodeOnclick(tFCat) + '\', \'' + Utils.encodeOnclick(aFURL) + '\', \'' + Utils.encodeOnclick(aFCat) + '\', \'' + Utils.encodeOnclick(tFEComments) + '\', \'' + Utils.encodeOnclick(tFNComments) + '\', \'large\');" '; - else - tFEClick = ''; - - // Any thumbnail? - if(tFThumb[f]) - html += ''; - else - html += '' + tFName[f].htmlEnc() + ''; - } - - if(tFURL.length) - html += '

'; - - // It's my own notice, we can remove it! - if(from == Common.getXID()) - html += ''; - - // Notice from another user - else { - // User profile - html += ''; - - // If PEP is enabled - if(Features.enabledPEP() && tHTMLEscape) - html += ''; - } - - html += '
'; - - // Mixed mode - if((mode == 'mixed') && !Common.exists('.mixed .' + tHash)) { - // Remove the old element - if(way == 'push') - $('#channel .content.mixed .one-update.update_' + hash).remove(); - - // Get the nearest element - var nearest = Search.sortElementByStamp(tStamp, '#channel .mixed .one-update'); - - // Append the content at the right position (date relative) - if(nearest === 0) - $('#channel .content.mixed').append(html); - else - $('#channel .one-update[data-stamp="' + nearest + '"]:first').before(html); - - // Show the new item - if(way == 'push') - $('#channel .content.mixed .one-update.' + tHash).fadeIn('fast'); - else - $('#channel .content.mixed .one-update.' + tHash).show(); - - // Remove the old notices to make the DOM lighter - var oneUpdate = '#channel .content.mixed .one-update'; - - if($(oneUpdate).size() > 80) - $(oneUpdate + ':last').remove(); - - // Click event on avatar/name - $('.mixed .' + tHash + ' .avatar-container, .mixed .' + tHash + ' .body b').click(function() { - self.get(from, hash); - }); - } - - // Individual mode - tIndividual = '#channel .content.individual.microblog-' + hash; - - // Can append individual content? - var can_individual = true; - - if($('#channel .top.individual input[name="comments"]').val() && Common.exists(tIndividual + ' .one-update')) - can_individual = false; - - if(can_individual && Common.exists(tIndividual) && !Common.exists('.individual .' + tHash)) { - if(mode == 'mixed') - $(tIndividual).prepend(html); - else - $(tIndividual + ' a.more').before(html); - - // Show the new item - if(way == 'push') - $('#channel .content.individual .one-update.' + tHash).fadeIn('fast'); - else - $('#channel .content.individual .one-update.' + tHash).show(); - - // Make 'more' link visible - $(tIndividual + ' a.more').css('visibility', 'visible'); - - // Click event on name (if not me!) - if(from != Common.getXID()) - $('.individual .' + tHash + ' .avatar-container, .individual .' + tHash + ' .body b').click(function() { - Chat.checkCreate(from, 'chat'); - }); - } - - // Apply the click event - $('.' + tHash + ' a.repost:not([data-event="true"])').click(function() { - return self.publish(tContent, tFName, tFURL, tFType, tFLength, tFThumb, uRepeat, entityComments, nodeComments, tFEComments, tFNComments); - }) - - .attr('data-event', 'true'); - - // Apply the hover event - if(nodeComments) { - $('.' + mode + ' .' + tHash).hover(function() { - self.showComments($(this), entityComments, nodeComments, tHash); - }, function() { - if($(this).find('div.comments a.one-comment.loading').size()) - $(this).find('div.comments').remove(); - }); - } - } - }); - - // Display the avatar of this buddy - Avatar.get(from, 'cache', 'true', 'forget'); - } catch(e) { - Console.error('Microblog.display', e); - } - - }; - - - /** - * Removes a given microblog item - * @public - * @param {string} id - * @param {string} hash - * @param {string} pserver - * @param {string} cnode - * @return {boolean} - */ - self.remove = function(id, hash, pserver, cnode) { - - /* REF: http://xmpp.org/extensions/xep-0060.html#publisher-delete */ - - try { - // Initialize - var selector = $('.' + hash); - var get_last = false; - - // Get the latest item for the mixed mode - if(Common.exists('#channel .content.mixed .' + hash)) - get_last = true; - - // Remove the item from our DOM - selector.fadeOut('fast', function() { - $(this).remove(); - }); - - // Send the IQ to remove the item (and get eventual error callback) - // Also attempt to remove the comments node. - var retract_iq = new JSJaCIQ(); - retract_iq.setType('set'); - retract_iq.appendNode('pubsub', {'xmlns': NS_PUBSUB}).appendChild(retract_iq.buildNode('retract', {'node': NS_URN_MBLOG, 'xmlns': NS_PUBSUB})).appendChild(retract_iq.buildNode('item', {'id': id, 'xmlns': NS_PUBSUB})); - - var comm_delete_iq; - if (pserver !== '' && cnode !== '') { - comm_delete_iq = new JSJaCIQ(); - comm_delete_iq.setType('set'); - comm_delete_iq.setTo(pserver); - comm_delete_iq.appendNode('pubsub', {'xmlns': 'http://jabber.org/protocol/pubsub#owner'}).appendChild(comm_delete_iq.buildNode('delete', {'node': cnode, 'xmlns': 'http://jabber.org/protocol/pubsub#owner'})); - } - - if(get_last) { - if (comm_delete_iq) { con.send(comm_delete_iq); } - con.send(retract_iq, self.handleRemove); - } else { - if (comm_delete_iq) { con.send(comm_delete_iq); } - con.send(retract_iq, Errors.handleReply); - } - } catch(e) { - Console.error('Microblog.remove', e); - } finally { - return false; - } - - }; - - - /** - * Handles the microblog item removal - * @public - * @param {object} iq - * @return {undefined} - */ - self.handleRemove = function(iq) { - - try { - // Handle the error reply - Errors.handleReply(iq); - - // Get the latest item - self.request(Common.getXID(), '1', false, self.handleUpdateRemove); - } catch(e) { - Console.error('Microblog.handleRemove', e); - } - - }; - - - /** - * Handles the microblog update - * @public - * @param {object} iq - * @return {undefined} - */ - self.handleUpdateRemove = function(iq) { - - try { - // Error? - if(iq.getType() == 'error') - return; - - // Initialize - var xid = Common.bareXID(Common.getStanzaFrom(iq)); - var hash = hex_md5(xid); - - // Display the item! - self.display(iq, xid, hash, 'mixed', 'push'); - } catch(e) { - Console.error('Microblog.handleUpdateRemove', e); - } - - }; - - - /** - * Gets a given microblog comments node - * @public - * @param {string} server - * @param {string} node - * @param {string} id - * @return {boolean} - */ - self.getComments = function(server, node, id) { - - /* REF: http://xmpp.org/extensions/xep-0060.html#subscriber-retrieve-requestall */ - - try { - var iq = new JSJaCIQ(); - iq.setType('get'); - iq.setID('get_' + genID() + '-' + id); - iq.setTo(server); - - var pubsub = iq.appendNode('pubsub', {'xmlns': NS_PUBSUB}); - pubsub.appendChild(iq.buildNode('items', {'node': node, 'xmlns': NS_PUBSUB})); - - con.send(iq, self.handleComments); - } catch(e) { - Console.error('Microblog.getComments', e); - } finally { - return false; - } - - }; - - - /** - * Handles a microblog comments node items - * @public - * @param {object} iq - * @return {undefined} - */ - self.handleComments = function(iq) { - - try { - // Path - var id = Common.explodeThis('-', iq.getID(), 1); - var path = 'div.comments[data-id="' + id + '"] div.comments-content'; - - // Does not exist? - if(!Common.exists(path)) - return false; - - // Any error? - if(Errors.handleReply(iq)) { - $(path).html('
' + Common._e("Could not get the comments!") + '
'); - - return false; - } - - // Initialize - var data = iq.getNode(); - var server = Common.bareXID(Common.getStanzaFrom(iq)); - var node = $(data).find('items:first').attr('node'); - var users_xid = []; - var code = ''; - - // No node? - if(!node) { - node = $(data).find('publish:first').attr('node'); - } - - // Get the parent microblog item - var parent_select = $('#channel .one-update:has(*[data-node="' + node + '"])'); - var parent_data = [parent_select.attr('data-xid'), NS_URN_MBLOG, parent_select.attr('data-id')]; - - // Get the owner XID - var owner_xid = parent_select.attr('data-xid'); - var repeat_xid = parent_select.find('a.repeat').attr('data-xid'); - - // Must we create the complete DOM? - var complete = true; - - if($(path).find('.one-comment.compose').size()) - complete = false; - - // Add the comment tool - if(complete) { - code += - '
' + - '' + - '
'; - } - - // Append the comments - $(data).find('item').each(function() { - // Get comment - var current_id = $(this).attr('id'); - var current_xid = Common.explodeThis(':', $(this).find('author uri').text(), 1); - var current_name = $(this).find('author name').text(); - var current_date = $(this).find('published').text(); - var current_body = $(this).find('content[type="text"]').text(); - var current_bname = Name.getBuddy(current_xid); - - // Legacy? - if(!current_body) - current_body = $(this).find('title:not(source > title)').text(); - - // Yet displayed? (continue the loop) - if($(path).find('.one-comment[data-id="' + current_id + '"]').size()) - return; - - // No XID? - if(!current_xid) { - current_xid = ''; - - if(!current_name) - current_name = Common._e("unknown"); - } - - else if(!current_name || (current_bname != Common.getXIDNick(current_xid))) - current_name = current_bname; - - // Any date? - if(current_date) - current_date = DateUtils.relative(current_date); - else - current_date = DateUtils.getCompleteTime(); - - // Click event - var onclick = 'false'; - - if(current_xid != Common.getXID()) - onclick = 'Chat.checkCreate(\'' + Utils.encodeOnclick(current_xid) + '\', \'chat\')'; - - // If this is my comment, add a marker - var type = 'him'; - var marker = ''; - var remove = ''; - - if(current_xid == Common.getXID()) { - type = 'me'; - marker = '
'; - remove = '' + Common._e("Remove") + ''; - } - - // New comment? - var new_class = ''; - - if(!complete) - new_class = ' new'; - - // Add the comment - if(current_body) { - // Add the XID - if(!Utils.existArrayValue(users_xid, current_xid)) - users_xid.push(current_xid); - - // Add the HTML code - code = '
' + - marker + - - '
' + - '' + - '
' + - - '
' + - '' + current_name.htmlEnc() + '' + - '' + current_date.htmlEnc() + '' + - remove + - - '

' + Filter.message(current_body, current_name, true) + '

' + - '
' + - - '
' + - '
' + code; - } - }); - - // Add the HTML - if(complete) { - $(path).html(code); - - // Focus on the compose input - $(document).oneTime(10, function() { - $(path).find('.one-comment.compose input').focus(); - }); - } - - else { - $(path).find('.one-comment.compose').before(code); - - // Beautiful effect - $(path).find('.one-comment.new').slideDown('fast', function() { - self.adaptComment(id); - }).removeClass('new'); - } - - // Set the good widths - self.adaptComment(id); - - // Get the avatars - for(var a in users_xid) - Avatar.get(users_xid[a], 'cache', 'true', 'forget'); - - // Add the owner XID - if(owner_xid && owner_xid.match('@') && !Utils.existArrayValue(users_xid, owner_xid)) - users_xid.push(owner_xid); - - // Add the repeated from XID - if(repeat_xid && repeat_xid.match('@') && !Utils.existArrayValue(users_xid, repeat_xid)) - users_xid.push(repeat_xid); - - // Remove my own XID - Utils.removeArrayValue(users_xid, Common.getXID()); - - // DOM events - if(complete) { - // Update timer - $(path).everyTime('60s', function() { - self.getComments(server, node, id); - - Console.log('Updating comments node: ' + node + ' on ' + server + '...'); - }); - - // Input key event - $(path).find('.one-comment.compose input').placeholder() - .keyup(function(e) { - if((e.keyCode == 13) && $(this).val()) { - // Send the comment! - self.sendComment($(this).val(), server, node, id, users_xid, parent_data); - - // Reset the input value - $(this).val(''); - - return false; - } - }); - } - } catch(e) { - Console.error('Microblog.handleComments', e); - } - - }; - - - /** - * Shows the microblog comments box - * @public - * @param {string} path - * @param {string} entityComments - * @param {string} nodeComments - * @param {string} tHash - * @return {undefined} - */ - self.showComments = function(path, entityComments, nodeComments, tHash) { - - try { - // Do not display it twice! - if(path.find('div.comments').size()) - return; - - // Generate an unique ID - var idComments = genID(); - - // Create comments container - path.find('div.comments-container').append( - '
' + - '
' + - '' + - '
' - ); - - // Click event - path.find('div.comments a.one-comment').click(function() { - // Set loading info - $(this).parent().html('
' + Common._e("Loading comments...") + '
'); - - // Request comments - self.getComments(entityComments, nodeComments, idComments); - - // Remove the comments from the DOM if click away - if(tHash) { - $('#channel').off('click'); - - $('#channel').on('click', function(evt) { - if(!$(evt.target).parents('.' + tHash).size()) { - $('#channel').off('click'); - $('#channel .one-update div.comments-content').stopTime(); - $('#channel .one-update div.comments').remove(); - } - }); - } - - return false; - }); - } catch(e) { - Console.error('Microblog.showComments', e); - } - - }; - - - /** - * Sends a comment on a given microblog comments node - * @public - * @param {string} value - * @param {string} server - * @param {string} node - * @param {string} id - * @param {object} notifiy_arr - * @param {string} parent_data - * @return {boolean} - */ - self.sendComment = function(value, server, node, id, notifiy_arr, parent_data) { - - /* REF: http://xmpp.org/extensions/xep-0060.html#publisher-publish */ - - try { - // Not enough data? - if(!value || !server || !node) - return false; - - // Get some values - var date = DateUtils.getXMPPTime('utc'); - var hash = hex_md5(value + date); - - // New IQ - var iq = new JSJaCIQ(); - iq.setType('set'); - iq.setTo(server); - iq.setID('set_' + genID() + '-' + id); - - // PubSub main elements - var pubsub = iq.appendNode('pubsub', {'xmlns': NS_PUBSUB}); - var publish = pubsub.appendChild(iq.buildNode('publish', {'node': node, 'xmlns': NS_PUBSUB})); - var item = publish.appendChild(iq.buildNode('item', {'id': hash, 'xmlns': NS_PUBSUB})); - var entry = item.appendChild(iq.buildNode('entry', {'xmlns': NS_ATOM})); - entry.appendChild(iq.buildNode('title', {'xmlns': NS_ATOM})); - - // Author infos - var author = entry.appendChild(iq.buildNode('author', {'xmlns': NS_ATOM})); - author.appendChild(iq.buildNode('name', {'xmlns': NS_ATOM}, Name.get())); - author.appendChild(iq.buildNode('uri', {'xmlns': NS_ATOM}, 'xmpp:' + Common.getXID())); - - // Create the comment - entry.appendChild(iq.buildNode('content', {'type': 'text', 'xmlns': NS_ATOM}, value)); - entry.appendChild(iq.buildNode('published', {'xmlns': NS_ATOM}, date)); - - con.send(iq); - - // Handle this comment! - iq.setFrom(server); - self.handleComments(iq); - - // Notify users - if(notifiy_arr && notifiy_arr.length) { - // XMPP link to the item - var href = 'xmpp:' + server + '?;node=' + encodeURIComponent(node) + ';item=' + encodeURIComponent(hash); - - // Loop! - for(var n in notifiy_arr) { - Notification.send(notifiy_arr[n], 'comment', href, value, parent_data); - } - } - } catch(e) { - Console.error('Microblog.sendComment', e); - } finally { - return false; - } - - }; - - - /** - * Removes a given microblog comment item - * @public - * @param {string} server - * @param {string} node - * @param {string} id - * @return {undefined} - */ - self.removeComment = function(server, node, id) { - - /* REF: http://xmpp.org/extensions/xep-0060.html#publisher-delete */ - - try { - // Remove the item from our DOM - $('.one-comment[data-id="' + id + '"]').slideUp('fast', function() { - // Get the parent ID - var parent_id = $(this).parents('div.comments').attr('data-id'); - - // Remove it! - $(this).remove(); - - // Adapt the width - self.adaptComment(parent_id); - }); - - // Send the IQ to remove the item (and get eventual error callback) - var iq = new JSJaCIQ(); - iq.setType('set'); - iq.setTo(server); - - var pubsub = iq.appendNode('pubsub', {'xmlns': NS_PUBSUB}); - var retract = pubsub.appendChild(iq.buildNode('retract', {'node': node, 'xmlns': NS_PUBSUB})); - retract.appendChild(iq.buildNode('item', {'id': id, 'xmlns': NS_PUBSUB})); - - con.send(iq); - } catch(e) { - Console.error('Microblog.removeComment', e); - } finally { - return false; - } - - }; - - - /** - * Adapts the comment elements width - * @public - * @param {string} id - * @return {undefined} - */ - self.adaptComment = function(id) { - - try { - var selector = $('div.comments[data-id="' + id + '"] div.comments-content'); - var selector_width = selector.width(); - - // Change widths - selector.find('.one-comment.compose input').css('width', selector_width - 60); - selector.find('.one-comment .comment-container').css('width', selector_width - 55); - } catch(e) { - Console.error('Microblog.adaptComment', e); - } - - }; - - - /** - * Handles the microblog of an user - * @public - * @param {object} iq - * @return {undefined} - */ - self.handle = function(iq) { - - try { - // Get the from attribute of this IQ - var from = Common.bareXID(Common.getStanzaFrom(iq)); - - // Define the selector path - var selector = '#channel .top.individual input[name='; - - // Is this request still alive? - if(from == $(selector + 'jid]').val()) { - var hash = hex_md5(from); - - // Update the items counter - var old_count = parseInt($(selector + 'counter]').val()); - $(selector + 'counter]').val(old_count + 20); - - // Display the microblog - self.display(iq, from, hash, 'individual', 'request'); - - // Hide the waiting icon - if(Features.enabledPEP()) - self.wait('sync'); - else - self.wait('unsync'); - - // Hide the 'more items' link? - if($(iq.getNode()).find('item').size() < old_count) - $('#channel .individual a.more').remove(); - - // Get the comments? - var comments_node = $('#channel .top.individual input[name="comments"]').val(); - - if(comments_node && comments_node.match(/^xmpp:(.+)\?;node=(.+);item=(.+)/)) { - // Get the values - var comments_entity = RegExp.$1; - comments_node = decodeURIComponent(RegExp.$2); - - // Selectors - var file_link = $('#channel .individual .one-update p.file a[data-node="' + comments_node + '"]'); - var entry_link = $('#channel .individual .one-update:has(.comments-container[data-node="' + comments_node + '"])'); - - // Is it a microblog entry (or a lonely entry file)? - if(entry_link.size()) { - self.showComments(entry_link, comments_entity, comments_node); - entry_link.find('a.one-comment').click(); - } - - // Is it a file? - else if(file_link.size()) - file_link.click(); - } - } - - Console.info('Microblog got: ' + from); - } catch(e) { - Console.error('Microblog.handle', e); - } - - }; - - - /** - * Handles the microblog of an user (from roster) - * @public - * @param {object} iq - * @return {undefined} - */ - self.handleRoster = function(iq) { - - try { - // Get the from attribute of this IQ - var from = Common.bareXID(Common.getStanzaFrom(iq)); - - // Display the microblog - self.display(iq, from, hex_md5(from), 'mixed', 'push'); - } catch(e) { - Console.error('Microblog.handleRoster', e); - } - - }; - - - /** - * Resets the microblog elements - * @public - * @return {boolean} - */ - self.reset = function() { - - try { - // Reset everything - $('#channel .individual .one-update div.comments-content').stopTime(); - $('#channel .individual').remove(); - $('#channel .mixed').show(); - - // Hide the waiting icon - if(Features.enabledPEP()) { - self.wait('sync'); - } else { - self.wait('unsync'); - } - } catch(e) { - Console.error('Microblog.reset', e); - } finally { - return false; - } - - }; - - - /** - * Gets the user's microblog to check it exists - * @public - * @return {undefined} - */ - self.getInit = function() { - - try { - self.get(Common.getXID(), hex_md5(Common.getXID()), true); - } catch(e) { - Console.error('Microblog.getInit', e); - } - - }; - - - /** - * Handles the user's microblog to create it in case of error - * @public - * @param {object} iq - * @return {undefined} - */ - self.handleInit = function(iq) { - - try { - // Any error? - if((iq.getType() == 'error') && $(iq.getNode()).find('item-not-found').size()) { - // The node may not exist, create it! - Pubsub.setup('', NS_URN_MBLOG, '1', '1000000', '', '', true); - - Console.warn('Error while getting microblog, trying to reconfigure the PubSub node!'); - } - } catch(e) { - Console.error('Microblog.handleInit', e); - } - - }; - - - /** - * Requests an user's microblog - * @public - * @param {type} name - * @return {undefined} - */ - self.request = function(xid, items, get_item, handler) { - - try { - // Ask the server the user's microblog - var iq = new JSJaCIQ(); - iq.setType('get'); - iq.setTo(xid); - - var pubsub = iq.appendNode('pubsub', {'xmlns': NS_PUBSUB}); - var ps_items = pubsub.appendChild(iq.buildNode('items', {'node': NS_URN_MBLOG, 'xmlns': NS_PUBSUB})); - - // Request a particular item? - if(get_item) - ps_items.appendChild(iq.buildNode('item', {'id': get_item, 'xmlns': NS_PUBSUB})); - else - ps_items.setAttribute('max_items', items); - - if(handler) { - con.send(iq, handler); - } else { - con.send(iq, self.handle); - } - } catch(e) { - Console.error('Microblog.request', e); - } finally { - return false; - } - - }; - - - /** - * Gets the microblog of an user - * @public - * @param {string} xid - * @param {string} hash - * @param {boolean} check - * @return {boolean} - */ - self.get = function(xid, hash, check) { - - /* REF: http://xmpp.org/extensions/xep-0060.html#subscriber-retrieve */ - - try { - Console.info('Get the microblog: ' + xid); - - // Fire the wait event - self.wait('fetch'); - - // XMPP URI? - var get_item = ''; - - if(xid.match(/^xmpp:(.+)\?;node=(.+);item=(.+)/)) { - xid = RegExp.$1; - get_item = decodeURIComponent(RegExp.$3); - } - - // No hash? - if(!hash) - hash = hex_md5(xid); - - // Can display the individual channel? - if(!check && !Common.exists('#channel .individual')) { - // Hide the mixed channel - $('#channel .mixed').hide(); - - // Get the channel title depending on the XID - var cTitle; - var cShortcuts = ''; - - if(xid == Common.getXID()) - cTitle = Common._e("Your channel"); - else { - cTitle = Common._e("Channel of") + ' ' + Name.getBuddy(xid).htmlEnc(); - cShortcuts = '
' + - '' + - '' + - '' + - '' + - '
'; - } - - // Create a new individual channel - $('#channel .content.mixed').after( - '' - ) - - .before( - '
' + - '
' + - '' + - '
' + - - '
' + - '

' + cTitle + '

' + - '« ' + Common._e("Previous") + '' + - '
' + - - cShortcuts + - - '' + - '' + - '
' - ); - - // Microblog navigation - $('#channel .content.individual').scroll(function() { - if($('#channel .footer div.fetch').is(':hidden') && $('#channel .individual a.more:visible').size() && $('#channel .content.individual').scrollTop() >= ($('#channel .content.individual')[0].scrollHeight - $('#channel .content.individual').height() - 200)) - $('#channel .individual a.more').click(); - }); - - // Display the user avatar - Avatar.get(xid, 'cache', 'true', 'forget'); - } - - // Get the number of items to retrieve - var items = '0'; - - if(!check) - items = $('#channel .top.individual input[name="counter"]').val(); - - // Request - if(check) - self.request(xid, items, get_item, self.handleInit); - else - self.request(xid, items, get_item, self.handle); - } catch(e) { - Console.error('Microblog.get', e); - } finally { - return false; - } - - }; - - - /** - * Show a given microblog waiting status - * @public - * @param {string} type - * @return {undefined} - */ - self.wait = function(type) { - - try { - // First hide all the infos elements - $('#channel .footer div').hide(); - - // Display the good one - $('#channel .footer div.' + type).show(); - - // Depending on the type, disable/enable certain tools - var selector = $('#channel .top input[name="microblog_body"]'); - - if(type == 'unsync') { - selector.attr('disabled', true); - } else if(type == 'sync') { - $(document).oneTime(10, function() { - selector.removeAttr('disabled').focus(); - }); - } - } catch(e) { - Console.error('Microblog.wait', e); - } - - }; - - - /** - * Gets the microblog configuration - * @public - * @return {undefined} - */ - self.getConfig = function() { - - try { - // Lock the microblog options - $('#persistent, #maxnotices').attr('disabled', true); - - // Get the microblog configuration - var iq = new JSJaCIQ(); - iq.setType('get'); - - var pubsub = iq.appendNode('pubsub', {'xmlns': NS_PUBSUB_OWNER}); - pubsub.appendChild(iq.buildNode('configure', {'node': NS_URN_MBLOG, 'xmlns': NS_PUBSUB_OWNER})); - - con.send(iq, self.handleGetConfig); - } catch(e) { - Console.error('Microblog.getConfig', e); - } - - }; - - - /** - * Handles the microblog configuration - * @public - * @param {object} iq - * @return {undefined} - */ - self.handleGetConfig = function(iq) { - - try { - // Reset the options stuffs - Options.wait('microblog'); - - // Unlock the microblog options - $('#persistent, #maxnotices').removeAttr('disabled'); - - // End if not a result - if(!iq || (iq.getType() != 'result')) - return; - - // Initialize the values - var selector = $(iq.getNode()); - var persistent = '0'; - var maxnotices = '1000000'; - - // Get the values - var xPersistent = selector.find('field[var="pubsub#persist_items"] value:first').text(); - var xMaxnotices = selector.find('field[var="pubsub#max_items"] value:first').text(); - - // Any value? - if(xPersistent) - persistent = xPersistent; - - if(xMaxnotices) - maxnotices = xMaxnotices; - - // Change the maxnotices value - switch(maxnotices) { - case '1': - case '100': - case '1000': - case '10000': - case '100000': - case '1000000': - break; - - default: - maxnotices = '1000000'; - break; - } - - // Apply persistent value - if(persistent == '0') - $('#persistent').attr('checked', false); - else - $('#persistent').attr('checked', true); - - // Apply maxnotices value - $('#maxnotices').val(maxnotices); - } catch(e) { - Console.error('Microblog.handleGetConfig', e); - } - - }; - - - /** - * Handles the user's microblog - * @public - * @param {object} packet - * @return {undefined} - */ - self.handleMine = function(packet) { - - try { - // Reset the entire form - $('#channel .top input[name="microblog_body"]').removeAttr('disabled').val(''); - $('#channel .top input[name="microblog_body"]').placeholder(); - self.unattach(); - - // Check for errors - Errors.handleReply(packet); - } catch(e) { - Console.error('Microblog.handleMy', e); - } - - }; - - - /** - * Performs the microblog sender checks - * @public - * @param {type} name - * @return {boolean} - */ - self.send = function() { - - try { - // Get the values - var selector = $('#channel .top input[name="microblog_body"]'); - var body = $.trim(selector.val()); - - // Sufficient parameters - if(body) { - // Disable & blur our input - selector.attr('disabled', true).blur(); - - // Files array - var fName = []; - var fType = []; - var fLength = []; - var fURL = []; - var fThumb = []; - - // Read the files - $('#attach .one-file').each(function() { - // Push the values! - fName.push($(this).find('a.link').text()); - fType.push($(this).attr('data-type')); - fLength.push($(this).attr('data-length')); - fURL.push($(this).find('a.link').attr('href')); - fThumb.push($(this).attr('data-thumb')); - }); - - // Containing YouTube videos? - var yt_matches = body.match(/(\w{3,5})(:)(\S+)((\.youtube\.com\/watch(\?v|\?\S+v|\#\!v|\#\!\S+v)\=)|(youtu\.be\/))([^& ]+)((&\S)|(&\S)|\s|$)/gim); - - for(var y in yt_matches) { - fName.push(''); - fType.push('text/html'); - fLength.push(''); - fURL.push($.trim(yt_matches[y])); - fThumb.push('https://img.youtube.com/vi/' + $.trim(yt_matches[y].replace(/(\w{3,5})(:)(\S+)((\.youtube\.com\/watch(\?v|\?\S+v|\#\!v|\#\!\S+v)\=)|(youtu\.be\/))([^& ]+)((&\S)|(&\S)|\s|$)/gim, '$8')) + '/0.jpg'); - } - - // Send the message on the XMPP network - self.publish(body, fName, fURL, fType, fLength, fThumb); - } - } catch(e) { - Console.error('Microblog.send', e); - } finally { - return false; - } - - }; - - - /** - * Publishes a given microblog item - * @public - * @param {type} body - * @param {type} attachedname - * @param {type} attachedurl - * @param {type} attachedtype - * @param {type} attachedlength - * @param {type} attachedthumb - * @param {type} repeat - * @param {type} comments_entity - * @param {type} comments_node - * @param {type} comments_entity_file - * @param {type} comments_node_file - * @return {boolean} - */ - self.publish = function(body, attachedname, attachedurl, attachedtype, attachedlength, attachedthumb, repeat, comments_entity, comments_node, comments_entity_file, comments_node_file) { - - /* REF: http://xmpp.org/extensions/xep-0277.html */ - - try { - // Generate some values - var time = DateUtils.getXMPPTime('utc'); - var id = hex_md5(body + time); - var nick = Name.get(); - var xid = Common.getXID(); - - // Define repeat options - var author_nick = nick; - var author_xid = xid; - - if(repeat && repeat.length) { - author_nick = repeat[0]; - author_xid = repeat[1]; - } - - // Define comments options - var node_create = false; - - if(!comments_entity || !comments_node) { - node_create = true; - comments_entity = HOST_PUBSUB; - comments_node = NS_URN_MBLOG + ':comments/' + id; - } - - if(!comments_entity_file) - comments_entity_file = []; - if(!comments_node_file) - comments_node_file = []; - - // Don't create another comments node if only 1 file is attached - if(attachedurl && (attachedurl.length == 1) && (!comments_entity_file[0] || !comments_node_file[0])) { - comments_entity_file = [comments_entity]; - comments_node_file = [comments_node]; - } - - // New IQ - var iq = new JSJaCIQ(); - iq.setType('set'); - iq.setTo(xid); - - // Create the main XML nodes/childs - var pubsub = iq.appendNode('pubsub', {'xmlns': NS_PUBSUB}); - var publish = pubsub.appendChild(iq.buildNode('publish', {'node': NS_URN_MBLOG, 'xmlns': NS_PUBSUB})); - var item = publish.appendChild(iq.buildNode('item', {'id': id, 'xmlns': NS_PUBSUB})); - var entry = item.appendChild(iq.buildNode('entry', {'xmlns': NS_ATOM})); - entry.appendChild(iq.buildNode('title', {'xmlns': NS_ATOM})); - - // Create the XML author childs - var author = entry.appendChild(iq.buildNode('author', {'xmlns': NS_ATOM})); - author.appendChild(iq.buildNode('name', {'xmlns': NS_ATOM}, author_nick)); - author.appendChild(iq.buildNode('uri', {'xmlns': NS_ATOM}, 'xmpp:' + author_xid)); - - // Create the XML entry childs - entry.appendChild(iq.buildNode('content', {'type': 'text', 'xmlns': NS_ATOM}, body)); - entry.appendChild(iq.buildNode('published', {'xmlns': NS_ATOM}, time)); - entry.appendChild(iq.buildNode('updated', {'xmlns': NS_ATOM}, time)); - entry.appendChild(iq.buildNode('link', { - 'rel': 'alternate', - 'href': 'xmpp:' + xid + '?;node=' + encodeURIComponent(NS_URN_MBLOG) + ';item=' + encodeURIComponent(id), - 'xmlns': NS_ATOM - })); - - // Create the attached files nodes - for(var i = 0; i < attachedurl.length; i++) { - // Not enough data? - if(!attachedurl[i]) - continue; - - // Append a new file element - var file = entry.appendChild(iq.buildNode('link', {'xmlns': NS_ATOM, 'rel': 'enclosure', 'href': attachedurl[i]})); - - // Add attributes - if(attachedname[i]) - file.setAttribute('title', attachedname[i]); - if(attachedtype[i]) - file.setAttribute('type', attachedtype[i]); - if(attachedlength[i]) - file.setAttribute('length', attachedlength[i]); - - // Any thumbnail? - if(attachedthumb[i]) - file.appendChild(iq.buildNode('link', {'xmlns': NS_URN_MBLOG, 'rel': 'self', 'title': 'thumb', 'type': attachedtype[i], 'href': attachedthumb[i]})); - - // Any comments node? - if(!comments_entity_file[i] || !comments_node_file[i]) { - // Generate values - comments_entity_file[i] = HOST_PUBSUB; - comments_node_file[i] = NS_URN_MBLOG + ':comments/' + hex_md5(attachedurl[i] + attachedname[i] + attachedtype[i] + attachedlength[i] + time); - - // Create the node - Pubsub.setup(comments_entity_file[i], comments_node_file[i], '1', '1000000', 'open', 'open', true); - } - - file.appendChild(iq.buildNode('link', {'xmlns': NS_URN_MBLOG, 'rel': 'replies', 'title': 'comments_file', 'href': 'xmpp:' + comments_entity_file[i] + '?;node=' + encodeURIComponent(comments_node_file[i])})); - } - - // Create the comments child - entry.appendChild(iq.buildNode('link', {'xmlns': NS_ATOM, 'rel': 'replies', 'title': 'comments', 'href': 'xmpp:' + comments_entity + '?;node=' + encodeURIComponent(comments_node)})); - - // Create the geoloc child - var geoloc_xml = DataStore.getDB(Connection.desktop_hash, 'geolocation', 'now'); - - if(geoloc_xml) { - // Create two position arrays - var geo_names = ['lat', 'lon', 'country', 'countrycode', 'region', 'postalcode', 'locality', 'street', 'building', 'text', 'uri', 'timestamp']; - var geo_values = PEP.parsePosition(Common.XMLFromString(geoloc_xml)); - - // New geoloc child - var geoloc = entry.appendChild(iq.buildNode('geoloc', {'xmlns': NS_GEOLOC})); - - // Append the geoloc content - for(var g = 0; g < geo_names.length; g++) { - if(geo_names[g] && geo_values[g]) - geoloc.appendChild(iq.buildNode(geo_names[g], {'xmlns': NS_GEOLOC}, geo_values[g])); - } - } - - // Send the IQ - con.send(iq, self.handleMine); - - // Create the XML comments PubSub nodes - if(node_create) { - Pubsub.setup(comments_entity, comments_node, '1', '1000000', 'open', 'open', true); - } - } catch(e) { - Console.error('Microblog.publish', e); - } finally { - return false; - } - - }; - - - /** - * Attaches a file to a microblog post - * @public - * @return {undefined} - */ - self.attach = function() { - - try { - // File upload vars - var attach_options = { - dataType: 'xml', - beforeSubmit: self.waitAttach, - success: self.handleAttach - }; - - // Upload form submit event - $('#attach').submit(function() { - if(!Common.exists('#attach .wait') && $('#attach input[type="file"]').val()) - $(this).ajaxSubmit(attach_options); - - return false; - }); - - // Upload input change event - $('#attach input[type="file"]').change(function() { - if(!Common.exists('#attach .wait') && $(this).val()) - $('#attach').ajaxSubmit(attach_options); - - return false; - }); - } catch(e) { - Console.error('Microblog.attach', e); - } - - }; - - - /** - * Unattaches a microblog file - * @public - * @param {string} id - * @return {boolean} - */ - self.unattach = function(id) { - - try { - // Individual removal? - if(id) - $('#attach .one-file[data-id="' + id + '"]').remove(); - else - $('#attach .one-file').remove(); - - // Must enable the popup again? - if(!Common.exists('#attach .one-file')) { - // Restore the bubble class - $('#attach').addClass('bubble'); - - // Enable the bubble click events - if(id) { - $('#attach').hide(); - Bubble.show('#attach'); - } - - else - Bubble.close(); - } - } catch(e) { - Console.error('Microblog.unattach', e); - } finally { - return false; - } - - }; - - - /** - * Wait event for file attaching - * @public - * @return {undefined} - */ - self.waitAttach = function() { - - try { - // Append the wait icon - $('#attach input[type="submit"]').after('
'); - - // Lock the bubble - $('#attach').removeClass('bubble'); - } catch(e) { - Console.error('Microblog.waitAttach', e); - } - - }; - - - /** - * Success event for file attaching - * @public - * @param {string} responseXML - * @return {undefined} - */ - self.handleAttach = function(responseXML) { - - try { - // Data selector - var dData = $(responseXML).find('jappix'); - - // Process the returned data - if(!dData.find('error').size()) { - // Do not allow this bubble to be hidden - $('#attach').removeClass('bubble'); - - // Get the file values - var fName = dData.find('title').text(); - var fType = dData.find('type').text(); - var fLength = dData.find('length').text(); - var fURL = dData.find('href').text(); - var fThumb = dData.find('thumb').text(); - - // Generate a file ID - var fID = hex_md5(fURL); - - // Add this file - $('#attach .attach-subitem').append( - '
' + - '' + - '' + fName.htmlEnc() + '' + - '
' - ); - - // Click event - $('#attach .one-file[data-id="' + fID + '"] a.remove').click(function() { - return self.unattach(fID); - }); - - Console.info('File attached.'); - } - - // Any error? - else { - Board.openThisError(4); - - // Unlock the bubble? - if(!Common.exists('#attach .one-file')) { - $('#attach').addClass('bubble').hide(); - - // Show the bubble again! - Bubble.show('#attach'); - } - - Console.error('Error while attaching the file', dData.find('error').text()); - } - - // Reset the attach bubble - $('#attach input[type="file"]').val(''); - $('#attach .wait').remove(); - - // Focus on the text input - $(document).oneTime(10, function() { - $('#channel .top input[name="microblog_body"]').focus(); - }); - } catch(e) { - Console.error('Microblog.handleAttach', e); - } - - }; - - - /** - * Shows the microblog of an user from his infos - * @public - * @param {string} xid - * @param {string} hash - * @return {undefined} - */ - self.fromInfos = function(xid, hash) { - - try { - // Renitialize the channel - self.reset(); - - // Switch to the channel - Interface.switchChan('channel'); - - // Get the microblog - self.get(xid, hash); - } catch(e) { - Console.error('Microblog.fromInfos', e); - } - - }; - - - /** - * Plugin launcher - * @public - * @return {undefined} - */ - self.instance = function() { - - try { - // Keyboard event - $('#channel .top input[name="microblog_body"]').keyup(function(e) { - // Enter pressed: send the microblog notice - if((e.keyCode == 13) && !Common.exists('#attach .wait')) - return self.send(); - }) - - // Placeholder - .placeholder(); - - // Microblog file attacher - self.attach(); - } catch(e) { - Console.error('Microblog.instance', e); - } - - }; - - - /** - * Return class scope - */ - return self; - +/* + +Jappix - An open social platform +These are the microblog JS scripts for Jappix + +------------------------------------------------- + +License: AGPL +Authors: Valérian Saliou, Maranda + +*/ + +// Bundle +var Microblog = (function () { + + /** + * Alias of this + * @private + */ + var self = {}; + + + /** + * Completes arrays of an entry's attached files + * @public + * @param {string} selector + * @param {object} tFName + * @param {object} tFURL + * @param {object} tFThumb + * @param {object} tFSource + * @param {object} tFLength + * @param {object} tFEComments + * @param {object} tFNComments + * @return {undefined} + */ + self.attached = function(selector, tFName, tFURL, tFThumb, tFSource, tFType, tFLength, tFEComments, tFNComments) { + + try { + tFName.push($(selector).attr('title') || ''); + tFURL.push($(selector).attr('href') || ''); + tFThumb.push($(selector).find('link[rel="self"][title="thumb"]:first').attr('href') || ''); + tFSource.push($(selector).attr('source') || ''); + tFType.push($(selector).attr('type') || ''); + tFLength.push($(selector).attr('length') || ''); + + // Comments? + var comments_href_c = $(selector).find('link[rel="replies"][title="comments_file"]:first').attr('href'); + + if(comments_href_c && comments_href_c.match(/^xmpp:(.+)\?;node=(.+)/)) { + tFEComments.push(RegExp.$1); + tFNComments.push(decodeURIComponent(RegExp.$2)); + } else { + tFEComments.push(''); + tFNComments.push(''); + } + } catch(e) { + Console.error('Microblog.attached', e); + } + + }; + + + /** + * Displays a given microblog item + * @public + * @param {object} packet + * @param {string} from + * @param {string} hash + * @param {string} mode + * @param {string} way + * @return {undefined} + */ + self.display = function(packet, from, hash, mode, way) { + + try { + // Get some values + var iParse = $(packet.getNode()).find('items item'); + + iParse.each(function() { + var this_sel = $(this); + + // Initialize + var tContent, tFiltered, tTime, tDate, tStamp, tBody, tName, tID, tHash, tIndividual, tFEClick; + var tHTMLEscape = false; + + // Arrays + var tFName = []; + var tFURL = []; + var tFThumb = []; + var tFSource = []; + var tFType = []; + var tFLength = []; + var tFEComments = []; + var tFNComments = []; + var aFURL = []; + var aFCat = []; + + // Get the values + tDate = this_sel.find('published').text(); + tBody = this_sel.find('body').text(); + tID = this_sel.attr('id'); + tName = Name.getBuddy(from); + tHash = 'update-' + hex_md5(tName + tDate + tID); + + // Read attached files with a thumb (place them at first) + this_sel.find('link[rel="enclosure"]:has(link[rel="self"][title="thumb"])').each(function() { + self.attached(this, tFName, tFURL, tFThumb, tFSource, tFType, tFLength, tFEComments, tFNComments); + }); + + // Read attached files without any thumb + this_sel.find('link[rel="enclosure"]:not(:has(link[rel="self"][title="thumb"]))').each(function() { + self.attached(this, tFName, tFURL, tFThumb, tFSource, tFType, tFLength, tFEComments, tFNComments); + }); + + // Get the repeat value + var uRepeat = [this_sel.find('author name').text(), Common.explodeThis(':', this_sel.find('author uri').text(), 1)]; + var uRepeated = false; + + if(!uRepeat[0]) + uRepeat = [Name.getBuddy(from), uRepeat[1]]; + if(!uRepeat[1]) + uRepeat = [uRepeat[0], from]; + + // Repeated? + if(uRepeat[1] != from) + uRepeated = true; + + // Get the comments node + var entityComments, nodeComments; + + // Get the comments + var comments_href = this_sel.find('link[title="comments"]:first').attr('href'); + + if(comments_href && comments_href.match(/^xmpp:(.+)\?;node=(.+)/)) { + entityComments = RegExp.$1; + nodeComments = decodeURIComponent(RegExp.$2); + } + + // No comments node? + if(!entityComments || !nodeComments) { + entityComments = ''; + nodeComments = ''; + } + + // Get the stamp & time + if(tDate) { + tStamp = DateUtils.extractStamp(Date.jab2date(tDate)); + tTime = DateUtils.relative(tDate); + } + + else { + tStamp = DateUtils.getTimeStamp(); + tTime = ''; + } + + // Get the item geoloc + var tGeoloc = ''; + var sGeoloc = this_sel.find('geoloc:first'); + var gLat = sGeoloc.find('lat').text(); + var gLon = sGeoloc.find('lon').text(); + + if(gLat && gLon) { + tGeoloc += ''; + + // Human-readable name? + var gHuman = PEP.humanPosition( + sGeoloc.find('locality').text(), + sGeoloc.find('region').text(), + sGeoloc.find('country').text() + ); + + if(gHuman) { + tGeoloc += gHuman.htmlEnc(); + } else { + tGeoloc += gLat.htmlEnc() + '; ' + gLon.htmlEnc(); + } + + tGeoloc += ''; + } + + // Entry content: HTML, parse! + if(this_sel.find('content[type="html"]').size()) { + // Filter the xHTML message + tContent = Filter.xhtml(this); + tHTMLEscape = false; + } + + // Entry content: Fallback on PLAIN? + if(!tContent) { + tContent = this_sel.find('content[type="text"]').text(); + + if(!tContent) { + // Legacy? + tContent = this_sel.find('title:not(source > title)').text(); + + // Last chance? + if(!tContent) { + tContent = tBody; + } + } + + // Trim the content + tContent = $.trim(tContent); + tHTMLEscape = true; + } + + // Any content? + if(tContent) { + // Apply links to message body + tFiltered = Filter.message(tContent, tName.htmlEnc(), tHTMLEscape); + + // Display the received message + var html = '
' + + '
' + + '
' + + '' + + '
' + + '
' + + + '
' + + '

'; + + // Is it a repeat? + if(uRepeated) + html += ''; + + html += '' + tName.htmlEnc() + ' ' + tFiltered + '

' + + '

' + tTime + tGeoloc + '

'; + + // Any file to display? + if(tFURL.length) + html += '

'; + + // Generate an array of the files URL + for(var a = 0; a < tFURL.length; a++) { + // Not enough data? + if(!tFURL[a]) { + continue; + } + + // Push the current URL! (YouTube or file) + if(tFURL[a].match(/(\w{3,5})(:)(\S+)((\.youtube\.com\/watch(\?v|\?\S+v|\#\!v|\#\!\S+v)\=)|(youtu\.be\/))([^& ]+)((&\S)|(&\S)|\s|$)/gim)) { + aFURL.push($.trim(RegExp.$8)); + aFCat.push('youtube'); + } + + else if(IntegrateBox.can(Common.strAfterLast('.', tFURL[a]))) { + aFURL.push(tFURL[a]); + aFCat.push(Utils.fileCategory(Common.strAfterLast('.', tFURL[a]))); + } + } + + // Add each file code + for(var f = 0; f < tFURL.length; f++) { + // Not enough data? + if(!tFURL[f]) { + continue; + } + + // Get the file type + var tFLink = tFURL[f]; + var tFExt = Common.strAfterLast('.', tFLink); + var tFCat = Utils.fileCategory(tFExt); + + // Youtube video? + if(tFLink.match(/(\w{3,5})(:)(\S+)((\.youtube\.com\/watch(\?v|\?\S+v|\#\!v|\#\!\S+v)\=)|(youtu\.be\/))([^& ]+)((&\S)|(&\S)|\s|$)/gim)) { + tFLink = $.trim(RegExp.$8); + tFCat = 'youtube'; + } + + // Supported image/video/sound + if(IntegrateBox.can(tFExt) || (tFCat == 'youtube')) { + tFEClick = 'onclick="return IntegrateBox.apply(\'' + Utils.encodeOnclick(tFLink) + '\', \'' + Utils.encodeOnclick(tFCat) + '\', \'' + Utils.encodeOnclick(aFURL) + '\', \'' + Utils.encodeOnclick(aFCat) + '\', \'' + Utils.encodeOnclick(tFEComments) + '\', \'' + Utils.encodeOnclick(tFNComments) + '\', \'large\');" '; + } else { + tFEClick = ''; + } + + // Any thumbnail? + if(tFThumb[f]) { + html += ''; + } else { + html += '' + tFName[f].htmlEnc() + ''; + } + } + + if(tFURL.length) { + html += '

'; + } + + // It's my own notice, we can remove it! + if(from == Common.getXID()) { + html += ''; + } + + // Notice from another user + else { + // User profile + html += ''; + + // If PEP is enabled + if(Features.enabledPEP() && tHTMLEscape) { + html += ''; + } + } + + html += '
'; + + // Mixed mode + if((mode == 'mixed') && !Common.exists('.mixed .' + tHash)) { + // Remove the old element + if(way == 'push') { + $('#channel .content.mixed .one-update.update_' + hash).remove(); + } + + // Get the nearest element + var nearest = Search.sortElementByStamp(tStamp, '#channel .mixed .one-update'); + + // Append the content at the right position (date relative) + if(nearest === 0) { + $('#channel .content.mixed').append(html); + } else { + $('#channel .one-update[data-stamp="' + nearest + '"]:first').before(html); + } + + // Show the new item + if(way == 'push') { + $('#channel .content.mixed .one-update.' + tHash).fadeIn('fast'); + } else { + $('#channel .content.mixed .one-update.' + tHash).show(); + } + + // Remove the old notices to make the DOM lighter + var oneUpdate = '#channel .content.mixed .one-update'; + + if($(oneUpdate).size() > 80) { + $(oneUpdate + ':last').remove(); + } + + // Click event on avatar/name + $('.mixed .' + tHash + ' .avatar-container, .mixed .' + tHash + ' .body b').click(function() { + self.get(from, hash); + }); + } + + // Individual mode + tIndividual = '#channel .content.individual.microblog-' + hash; + + // Can append individual content? + var can_individual = true; + + if($('#channel .top.individual input[name="comments"]').val() && Common.exists(tIndividual + ' .one-update')) { + can_individual = false; + } + + if(can_individual && Common.exists(tIndividual) && !Common.exists('.individual .' + tHash)) { + if(mode == 'mixed') { + $(tIndividual).prepend(html); + } else { + $(tIndividual + ' a.more').before(html); + } + + // Show the new item + if(way == 'push') { + $('#channel .content.individual .one-update.' + tHash).fadeIn('fast'); + } else { + $('#channel .content.individual .one-update.' + tHash).show(); + } + + // Make 'more' link visible + $(tIndividual + ' a.more').css('visibility', 'visible'); + + // Click event on name (if not me!) + if(from != Common.getXID()) { + $('.individual .' + tHash + ' .avatar-container, .individual .' + tHash + ' .body b').click(function() { + Chat.checkCreate(from, 'chat'); + }); + } + } + + // Apply the click event + $('.' + tHash + ' a.repost:not([data-event="true"])').click(function() { + return self.publish(tContent, tFName, tFURL, tFType, tFLength, tFThumb, uRepeat, entityComments, nodeComments, tFEComments, tFNComments); + }) + + .attr('data-event', 'true'); + + // Apply the hover event + if(nodeComments) { + $('.' + mode + ' .' + tHash).hover(function() { + self.showComments($(this), entityComments, nodeComments, tHash); + }, function() { + if($(this).find('div.comments a.one-comment.loading').size()) { + $(this).find('div.comments').remove(); + } + }); + } + } + }); + + // Display the avatar of this buddy + Avatar.get(from, 'cache', 'true', 'forget'); + } catch(e) { + Console.error('Microblog.display', e); + } + + }; + + + /** + * Removes a given microblog item + * @public + * @param {string} id + * @param {string} hash + * @param {string} pserver + * @param {string} cnode + * @return {boolean} + */ + self.remove = function(id, hash, pserver, cnode) { + + /* REF: http://xmpp.org/extensions/xep-0060.html#publisher-delete */ + + try { + // Initialize + var selector = $('.' + hash); + var get_last = false; + + // Get the latest item for the mixed mode + if(Common.exists('#channel .content.mixed .' + hash)) { + get_last = true; + } + + // Remove the item from our DOM + selector.fadeOut('fast', function() { + $(this).remove(); + }); + + // Send the IQ to remove the item (and get eventual error callback) + // Also attempt to remove the comments node. + var retract_iq = new JSJaCIQ(); + retract_iq.setType('set'); + + retract_iq.appendNode('pubsub', { + 'xmlns': NS_PUBSUB + }).appendChild(retract_iq.buildNode('retract', { + 'node': NS_URN_MBLOG, + 'xmlns': NS_PUBSUB + })).appendChild(retract_iq.buildNode('item', { + 'id': id, + 'xmlns': NS_PUBSUB + })); + + var comm_delete_iq; + if(pserver !== '' && cnode !== '') { + comm_delete_iq = new JSJaCIQ(); + comm_delete_iq.setType('set'); + comm_delete_iq.setTo(pserver); + comm_delete_iq.appendNode('pubsub', { + 'xmlns': 'http://jabber.org/protocol/pubsub#owner' + }).appendChild(comm_delete_iq.buildNode('delete', { + 'node': cnode, + 'xmlns': 'http://jabber.org/protocol/pubsub#owner' + })); + } + + if(get_last) { + if(comm_delete_iq) { + con.send(comm_delete_iq); + } + + con.send(retract_iq, self.handleRemove); + } else { + if(comm_delete_iq) { + con.send(comm_delete_iq); + } + + con.send(retract_iq, Errors.handleReply); + } + } catch(e) { + Console.error('Microblog.remove', e); + } finally { + return false; + } + + }; + + + /** + * Handles the microblog item removal + * @public + * @param {object} iq + * @return {undefined} + */ + self.handleRemove = function(iq) { + + try { + // Handle the error reply + Errors.handleReply(iq); + + // Get the latest item + self.request(Common.getXID(), '1', false, self.handleUpdateRemove); + } catch(e) { + Console.error('Microblog.handleRemove', e); + } + + }; + + + /** + * Handles the microblog update + * @public + * @param {object} iq + * @return {undefined} + */ + self.handleUpdateRemove = function(iq) { + + try { + // Error? + if(iq.getType() == 'error') { + return; + } + + // Initialize + var xid = Common.bareXID(Common.getStanzaFrom(iq)); + var hash = hex_md5(xid); + + // Display the item! + self.display(iq, xid, hash, 'mixed', 'push'); + } catch(e) { + Console.error('Microblog.handleUpdateRemove', e); + } + + }; + + + /** + * Gets a given microblog comments node + * @public + * @param {string} server + * @param {string} node + * @param {string} id + * @return {boolean} + */ + self.getComments = function(server, node, id) { + + /* REF: http://xmpp.org/extensions/xep-0060.html#subscriber-retrieve-requestall */ + + try { + var iq = new JSJaCIQ(); + iq.setType('get'); + iq.setID('get_' + genID() + '-' + id); + iq.setTo(server); + + var pubsub = iq.appendNode('pubsub', { + 'xmlns': NS_PUBSUB + }); + + pubsub.appendChild(iq.buildNode('items', { + 'node': node, + 'xmlns': NS_PUBSUB + })); + + con.send(iq, self.handleComments); + } catch(e) { + Console.error('Microblog.getComments', e); + } finally { + return false; + } + + }; + + + /** + * Handles a microblog comments node items + * @public + * @param {object} iq + * @return {undefined} + */ + self.handleComments = function(iq) { + + try { + // Path + var id = Common.explodeThis('-', iq.getID(), 1); + var path = 'div.comments[data-id="' + id + '"] div.comments-content'; + + // Does not exist? + if(!Common.exists(path)) { + return false; + } + + var path_sel = $(path); + + // Any error? + if(Errors.handleReply(iq)) { + path_sel.html('
' + Common._e("Could not get the comments!") + '
'); + + return false; + } + + // Initialize + var data = iq.getNode(); + var server = Common.bareXID(Common.getStanzaFrom(iq)); + var node = $(data).find('items:first').attr('node'); + var users_xid = []; + var code = ''; + + // No node? + if(!node) { + node = $(data).find('publish:first').attr('node'); + } + + // Get the parent microblog item + var parent_select = $('#channel .one-update:has(*[data-node="' + node + '"])'); + var parent_data = [parent_select.attr('data-xid'), NS_URN_MBLOG, parent_select.attr('data-id')]; + + // Get the owner XID + var owner_xid = parent_select.attr('data-xid'); + var repeat_xid = parent_select.find('a.repeat').attr('data-xid'); + + // Must we create the complete DOM? + var complete = true; + + if(path_sel.find('.one-comment.compose').size()) { + complete = false; + } + + // Add the comment tool + if(complete) { + code += '
' + + '' + + '
'; + } + + // Append the comments + $(data).find('item').each(function() { + var this_sel = $(this); + + // Get comment + var current_id = this_sel.attr('id'); + var current_xid = Common.explodeThis(':', this_sel.find('author uri').text(), 1); + var current_name = this_sel.find('author name').text(); + var current_date = this_sel.find('published').text(); + var current_body = this_sel.find('content[type="text"]').text(); + var current_bname = Name.getBuddy(current_xid); + + // Legacy? + if(!current_body) { + current_body = this_sel.find('title:not(source > title)').text(); + } + + // Yet displayed? (continue the loop) + if(path_sel.find('.one-comment[data-id="' + current_id + '"]').size()) { + return; + } + + // No XID? + if(!current_xid) { + current_xid = ''; + + if(!current_name) { + current_name = Common._e("unknown"); + } + } + + else if(!current_name || (current_bname != Common.getXIDNick(current_xid))) { + current_name = current_bname; + } + + // Any date? + if(current_date) { + current_date = DateUtils.relative(current_date); + } else { + current_date = DateUtils.getCompleteTime(); + } + + // Click event + var onclick = 'false'; + + if(current_xid != Common.getXID()) { + onclick = 'Chat.checkCreate(\'' + Utils.encodeOnclick(current_xid) + '\', \'chat\')'; + } + + // If this is my comment, add a marker + var type = 'him'; + var marker = ''; + var remove = ''; + + if(current_xid == Common.getXID()) { + type = 'me'; + marker = '
'; + remove = '' + Common._e("Remove") + ''; + } + + // New comment? + var new_class = ''; + + if(!complete) { + new_class = ' new'; + } + + // Add the comment + if(current_body) { + // Add the XID + if(!Utils.existArrayValue(users_xid, current_xid)) { + users_xid.push(current_xid); + } + + // Add the HTML code + code = '
' + + marker + + + '
' + + '' + + '
' + + + '
' + + '' + current_name.htmlEnc() + '' + + '' + current_date.htmlEnc() + '' + + remove + + + '

' + Filter.message(current_body, current_name, true) + '

' + + '
' + + + '
' + + '
' + code; + } + }); + + // Add the HTML + if(complete) { + path_sel.html(code); + + // Focus on the compose input + $(document).oneTime(10, function() { + path_sel.find('.one-comment.compose input').focus(); + }); + } + + else { + path_sel.find('.one-comment.compose').before(code); + + // Beautiful effect + path_sel.find('.one-comment.new').slideDown('fast', function() { + self.adaptComment(id); + }).removeClass('new'); + } + + // Set the good widths + self.adaptComment(id); + + // Get the avatars + for(var a in users_xid) { + Avatar.get(users_xid[a], 'cache', 'true', 'forget'); + } + + // Add the owner XID + if(owner_xid && owner_xid.match('@') && !Utils.existArrayValue(users_xid, owner_xid)) { + users_xid.push(owner_xid); + } + + // Add the repeated from XID + if(repeat_xid && repeat_xid.match('@') && !Utils.existArrayValue(users_xid, repeat_xid)) { + users_xid.push(repeat_xid); + } + + // Remove my own XID + Utils.removeArrayValue(users_xid, Common.getXID()); + + // DOM events + if(complete) { + // Update timer + path_sel.everyTime('60s', function() { + self.getComments(server, node, id); + + Console.log('Updating comments node: ' + node + ' on ' + server + '...'); + }); + + // Input key event + var comment_compose_input_sel = path_sel.find('.one-comment.compose input'); + + comment_compose_input_sel.placeholder(); + comment_compose_input_sel.keyup(function(e) { + var this_input_sel = $(this); + + if((e.keyCode == 13) && this_input_sel.val()) { + // Send the comment! + self.sendComment(this_input_sel.val(), server, node, id, users_xid, parent_data); + + // Reset the input value + this_input_sel.val(''); + + return false; + } + }); + } + } catch(e) { + Console.error('Microblog.handleComments', e); + } + + }; + + + /** + * Shows the microblog comments box + * @public + * @param {string} path + * @param {string} entityComments + * @param {string} nodeComments + * @param {string} tHash + * @return {undefined} + */ + self.showComments = function(path, entityComments, nodeComments, tHash) { + + try { + // Do not display it twice! + if(path.find('div.comments').size()) + return; + + // Generate an unique ID + var idComments = genID(); + + // Create comments container + path.find('div.comments-container').append( + '
' + + '
' + + '' + + '
' + ); + + // Click event + path.find('div.comments a.one-comment').click(function() { + // Set loading info + $(this).parent().html('
' + Common._e("Loading comments...") + '
'); + + // Request comments + self.getComments(entityComments, nodeComments, idComments); + + // Remove the comments from the DOM if click away + if(tHash) { + $('#channel').off('click'); + + $('#channel').on('click', function(evt) { + if(!$(evt.target).parents('.' + tHash).size()) { + $('#channel').off('click'); + $('#channel .one-update div.comments-content').stopTime(); + $('#channel .one-update div.comments').remove(); + } + }); + } + + return false; + }); + } catch(e) { + Console.error('Microblog.showComments', e); + } + + }; + + + /** + * Sends a comment on a given microblog comments node + * @public + * @param {string} value + * @param {string} server + * @param {string} node + * @param {string} id + * @param {object} notifiy_arr + * @param {string} parent_data + * @return {boolean} + */ + self.sendComment = function(value, server, node, id, notifiy_arr, parent_data) { + + /* REF: http://xmpp.org/extensions/xep-0060.html#publisher-publish */ + + try { + // Not enough data? + if(!value || !server || !node) { + return false; + } + + // Get some values + var date = DateUtils.getXMPPTime('utc'); + var hash = hex_md5(value + date); + + // New IQ + var iq = new JSJaCIQ(); + iq.setType('set'); + iq.setTo(server); + iq.setID('set_' + genID() + '-' + id); + + // PubSub main elements + var pubsub = iq.appendNode('pubsub', { + 'xmlns': NS_PUBSUB + }); + + var publish = pubsub.appendChild(iq.buildNode('publish', { + 'node': node, + 'xmlns': NS_PUBSUB + })); + + var item = publish.appendChild(iq.buildNode('item', { + 'id': hash, + 'xmlns': NS_PUBSUB + })); + + var entry = item.appendChild(iq.buildNode('entry', { + 'xmlns': NS_ATOM + })); + + entry.appendChild(iq.buildNode('title', { + 'xmlns': NS_ATOM + })); + + // Author infos + var author = entry.appendChild(iq.buildNode('author', { + 'xmlns': NS_ATOM + })); + + author.appendChild(iq.buildNode('name', { + 'xmlns': NS_ATOM + }, Name.get())); + + author.appendChild(iq.buildNode('uri', { + 'xmlns': NS_ATOM + }, 'xmpp:' + Common.getXID())); + + // Create the comment + entry.appendChild(iq.buildNode('content', { + 'type': 'text', + 'xmlns': NS_ATOM + }, value)); + + entry.appendChild(iq.buildNode('published', { + 'xmlns': NS_ATOM + }, date)); + + con.send(iq); + + // Handle this comment! + iq.setFrom(server); + self.handleComments(iq); + + // Notify users + if(notifiy_arr && notifiy_arr.length) { + // XMPP link to the item + var href = 'xmpp:' + server + '?;node=' + encodeURIComponent(node) + ';item=' + encodeURIComponent(hash); + + // Loop! + for(var n in notifiy_arr) { + Notification.send(notifiy_arr[n], 'comment', href, value, parent_data); + } + } + } catch(e) { + Console.error('Microblog.sendComment', e); + } finally { + return false; + } + + }; + + + /** + * Removes a given microblog comment item + * @public + * @param {string} server + * @param {string} node + * @param {string} id + * @return {undefined} + */ + self.removeComment = function(server, node, id) { + + /* REF: http://xmpp.org/extensions/xep-0060.html#publisher-delete */ + + try { + // Remove the item from our DOM + $('.one-comment[data-id="' + id + '"]').slideUp('fast', function() { + var this_sel = $(this); + + // Get the parent ID + var parent_id = this_sel.parents('div.comments').attr('data-id'); + + // Remove it! + this_sel.remove(); + + // Adapt the width + self.adaptComment(parent_id); + }); + + // Send the IQ to remove the item (and get eventual error callback) + var iq = new JSJaCIQ(); + iq.setType('set'); + iq.setTo(server); + + var pubsub = iq.appendNode('pubsub', { + 'xmlns': NS_PUBSUB + }); + + var retract = pubsub.appendChild(iq.buildNode('retract', { + 'node': node, + 'xmlns': NS_PUBSUB + })); + + retract.appendChild(iq.buildNode('item', { + 'id': id, + 'xmlns': NS_PUBSUB + })); + + con.send(iq); + } catch(e) { + Console.error('Microblog.removeComment', e); + } finally { + return false; + } + + }; + + + /** + * Adapts the comment elements width + * @public + * @param {string} id + * @return {undefined} + */ + self.adaptComment = function(id) { + + try { + var selector = $('div.comments[data-id="' + id + '"] div.comments-content'); + var selector_width = selector.width(); + + // Change widths + selector.find('.one-comment.compose input').css('width', selector_width - 60); + selector.find('.one-comment .comment-container').css('width', selector_width - 55); + } catch(e) { + Console.error('Microblog.adaptComment', e); + } + + }; + + + /** + * Handles the microblog of an user + * @public + * @param {object} iq + * @return {undefined} + */ + self.handle = function(iq) { + + try { + // Get the from attribute of this IQ + var from = Common.bareXID(Common.getStanzaFrom(iq)); + + // Define the selector path + var selector = '#channel .top.individual input[name='; + + // Is this request still alive? + if(from == $(selector + 'jid]').val()) { + var hash = hex_md5(from); + + // Update the items counter + var old_count = parseInt($(selector + 'counter]').val()); + $(selector + 'counter]').val(old_count + 20); + + // Display the microblog + self.display(iq, from, hash, 'individual', 'request'); + + // Hide the waiting icon + self.wait( + Features.enabledPEP() ? 'sync' : 'unsync' + ); + + // Hide the 'more items' link? + if($(iq.getNode()).find('item').size() < old_count) + $('#channel .individual a.more').remove(); + + // Get the comments? + var comments_node = $('#channel .top.individual input[name="comments"]').val(); + + if(comments_node && comments_node.match(/^xmpp:(.+)\?;node=(.+);item=(.+)/)) { + // Get the values + var comments_entity = RegExp.$1; + comments_node = decodeURIComponent(RegExp.$2); + + // Selectors + var file_link = $('#channel .individual .one-update p.file a[data-node="' + comments_node + '"]'); + var entry_link = $('#channel .individual .one-update:has(.comments-container[data-node="' + comments_node + '"])'); + + // Is it a microblog entry (or a lonely entry file)? + if(entry_link.size()) { + self.showComments(entry_link, comments_entity, comments_node); + entry_link.find('a.one-comment').click(); + } + + // Is it a file? + else if(file_link.size()) { + file_link.click(); + } + } + } + + Console.info('Microblog got: ' + from); + } catch(e) { + Console.error('Microblog.handle', e); + } + + }; + + + /** + * Handles the microblog of an user (from roster) + * @public + * @param {object} iq + * @return {undefined} + */ + self.handleRoster = function(iq) { + + try { + // Get the from attribute of this IQ + var from = Common.bareXID(Common.getStanzaFrom(iq)); + + // Display the microblog + self.display(iq, from, hex_md5(from), 'mixed', 'push'); + } catch(e) { + Console.error('Microblog.handleRoster', e); + } + + }; + + + /** + * Resets the microblog elements + * @public + * @return {boolean} + */ + self.reset = function() { + + try { + var channel_sel = $('#channel'); + var individual_sel = channel_sel.find('.individual'); + + // Reset everything + individual_sel.find('.one-update div.comments-content').stopTime(); + individual_sel.remove(); + channel_sel.find('.mixed').show(); + + // Hide the waiting icon + self.wait( + Features.enabledPEP() ? 'sync' : 'unsync' + ); + } catch(e) { + Console.error('Microblog.reset', e); + } finally { + return false; + } + + }; + + + /** + * Gets the user's microblog to check it exists + * @public + * @return {undefined} + */ + self.getInit = function() { + + try { + self.get(Common.getXID(), hex_md5(Common.getXID()), true); + } catch(e) { + Console.error('Microblog.getInit', e); + } + + }; + + + /** + * Handles the user's microblog to create it in case of error + * @public + * @param {object} iq + * @return {undefined} + */ + self.handleInit = function(iq) { + + try { + // Any error? + if((iq.getType() == 'error') && $(iq.getNode()).find('item-not-found').size()) { + // The node may not exist, create it! + Pubsub.setup('', NS_URN_MBLOG, '1', '1000000', '', '', true); + + Console.warn('Error while getting microblog, trying to reconfigure the PubSub node!'); + } + } catch(e) { + Console.error('Microblog.handleInit', e); + } + + }; + + + /** + * Requests an user's microblog + * @public + * @param {type} name + * @return {undefined} + */ + self.request = function(xid, items, get_item, handler) { + + try { + // Ask the server the user's microblog + var iq = new JSJaCIQ(); + iq.setType('get'); + iq.setTo(xid); + + var pubsub = iq.appendNode('pubsub', { + 'xmlns': NS_PUBSUB + }); + + var ps_items = pubsub.appendChild(iq.buildNode('items', { + 'node': NS_URN_MBLOG, + 'xmlns': NS_PUBSUB + })); + + // Request a particular item? + if(get_item) { + ps_items.appendChild(iq.buildNode('item', { + 'id': get_item, + 'xmlns': NS_PUBSUB + })); + } else { + ps_items.setAttribute('max_items', items); + } + + if(handler) { + con.send(iq, handler); + } else { + con.send(iq, self.handle); + } + } catch(e) { + Console.error('Microblog.request', e); + } finally { + return false; + } + + }; + + + /** + * Gets the microblog of an user + * @public + * @param {string} xid + * @param {string} hash + * @param {boolean} check + * @return {boolean} + */ + self.get = function(xid, hash, check) { + + /* REF: http://xmpp.org/extensions/xep-0060.html#subscriber-retrieve */ + + try { + Console.info('Get the microblog: ' + xid); + + var channel_sel = $('#channel'); + + // Fire the wait event + self.wait('fetch'); + + // XMPP URI? + var get_item = ''; + + if(xid.match(/^xmpp:(.+)\?;node=(.+);item=(.+)/)) { + xid = RegExp.$1; + get_item = decodeURIComponent(RegExp.$3); + } + + // No hash? + if(!hash) { + hash = hex_md5(xid); + } + + // Can display the individual channel? + if(!check && !Common.exists('#channel .individual')) { + // Hide the mixed channel + channel_sel.find('.mixed').hide(); + + // Get the channel title depending on the XID + var cTitle; + var cShortcuts = ''; + + if(xid == Common.getXID()) { + cTitle = Common._e("Your channel"); + } else { + cTitle = Common._e("Channel of") + ' ' + Name.getBuddy(xid).htmlEnc(); + cShortcuts = '
' + + '' + + '' + + '' + + '' + + '
'; + } + + // Create a new individual channel + channel_sel.find('.content.mixed').after( + '' + ) + + .before( + '
' + + '
' + + '' + + '
' + + + '
' + + '

' + cTitle + '

' + + '« ' + Common._e("Previous") + '' + + '
' + + + cShortcuts + + + '' + + '' + + '
' + ); + + // Microblog navigation + channel_sel.find('.content.individual').scroll(function() { + if(channel_sel.find('.footer div.fetch').is(':hidden') && + channel_sel.find('.individual a.more:visible').size() && + channel_sel.find('.content.individual').scrollTop() >= (channel_sel.find('.content.individual')[0].scrollHeight - channel_sel.find('.content.individual').height() - 200)) { + channel_sel.find('.individual a.more').click(); + } + }); + + // Display the user avatar + Avatar.get(xid, 'cache', 'true', 'forget'); + } + + // Get the number of items to retrieve + var items = !check ? channel_sel.find('.top.individual input[name="counter"]').val() : '0'; + + // Request + self.request( + xid, + items, + get_item, + (check ? self.handleInit : self.handle) + ); + } catch(e) { + Console.error('Microblog.get', e); + } finally { + return false; + } + + }; + + + /** + * Show a given microblog waiting status + * @public + * @param {string} type + * @return {undefined} + */ + self.wait = function(type) { + + try { + // First hide all the infos elements + $('#channel .footer div').hide(); + + // Display the good one + $('#channel .footer div.' + type).show(); + + // Depending on the type, disable/enable certain tools + var selector = $('#channel .top input[name="microblog_body"]'); + + if(type == 'unsync') { + selector.attr('disabled', true); + } else if(type == 'sync') { + $(document).oneTime(10, function() { + selector.removeAttr('disabled').focus(); + }); + } + } catch(e) { + Console.error('Microblog.wait', e); + } + + }; + + + /** + * Gets the microblog configuration + * @public + * @return {undefined} + */ + self.getConfig = function() { + + try { + // Lock the microblog options + $('#persistent, #maxnotices').attr('disabled', true); + + // Get the microblog configuration + var iq = new JSJaCIQ(); + iq.setType('get'); + + var pubsub = iq.appendNode('pubsub', { + 'xmlns': NS_PUBSUB_OWNER + }); + + pubsub.appendChild(iq.buildNode('configure', { + 'node': NS_URN_MBLOG, + 'xmlns': NS_PUBSUB_OWNER + })); + + con.send(iq, self.handleGetConfig); + } catch(e) { + Console.error('Microblog.getConfig', e); + } + + }; + + + /** + * Handles the microblog configuration + * @public + * @param {object} iq + * @return {undefined} + */ + self.handleGetConfig = function(iq) { + + try { + // Reset the options stuffs + Options.wait('microblog'); + + // Unlock the microblog options + $('#persistent, #maxnotices').removeAttr('disabled'); + + // End if not a result + if(!iq || (iq.getType() != 'result')) { + return; + } + + // Initialize the values + var selector = $(iq.getNode()); + var persistent = '0'; + var maxnotices = '1000000'; + + // Get the values + var xPersistent = selector.find('field[var="pubsub#persist_items"] value:first').text(); + var xMaxnotices = selector.find('field[var="pubsub#max_items"] value:first').text(); + + // Any value? + if(xPersistent) { + persistent = xPersistent; + } + + if(xMaxnotices) { + maxnotices = xMaxnotices; + } + + // Change the maxnotices value + switch(maxnotices) { + case '1': + case '100': + case '1000': + case '10000': + case '100000': + case '1000000': + break; + + default: + maxnotices = '1000000'; + break; + } + + // Apply persistent value + $('#persistent').attr( + 'checked', + (persistent == '0' ? false : true) + ); + + // Apply maxnotices value + $('#maxnotices').val(maxnotices); + } catch(e) { + Console.error('Microblog.handleGetConfig', e); + } + + }; + + + /** + * Handles the user's microblog + * @public + * @param {object} packet + * @return {undefined} + */ + self.handleMine = function(packet) { + + try { + var input_body_sel = $('#channel .top input[name="microblog_body"]'); + + // Reset the entire form + input_body_sel.removeAttr('disabled').val(''); + input_body_sel.placeholder(); + + self.unattach(); + + // Check for errors + Errors.handleReply(packet); + } catch(e) { + Console.error('Microblog.handleMy', e); + } + + }; + + + /** + * Performs the microblog sender checks + * @public + * @param {type} name + * @return {boolean} + */ + self.send = function() { + + try { + // Get the values + var selector = $('#channel .top input[name="microblog_body"]'); + var body = $.trim(selector.val()); + + // Sufficient parameters + if(body) { + // Disable & blur our input + selector.attr('disabled', true).blur(); + + // Files array + var fName = []; + var fType = []; + var fLength = []; + var fURL = []; + var fThumb = []; + + // Read the files + $('#attach .one-file').each(function() { + var this_sel = $(this); + + // Push the values! + fName.push(this_sel.find('a.link').text()); + fType.push(this_sel.attr('data-type')); + fLength.push(this_sel.attr('data-length')); + fURL.push(this_sel.find('a.link').attr('href')); + fThumb.push(this_sel.attr('data-thumb')); + }); + + // Containing YouTube videos? + var yt_matches = body.match(/(\w{3,5})(:)(\S+)((\.youtube\.com\/watch(\?v|\?\S+v|\#\!v|\#\!\S+v)\=)|(youtu\.be\/))([^& ]+)((&\S)|(&\S)|\s|$)/gim); + + for(var y in yt_matches) { + fName.push(''); + fType.push('text/html'); + fLength.push(''); + fURL.push($.trim(yt_matches[y])); + fThumb.push('https://img.youtube.com/vi/' + $.trim(yt_matches[y].replace(/(\w{3,5})(:)(\S+)((\.youtube\.com\/watch(\?v|\?\S+v|\#\!v|\#\!\S+v)\=)|(youtu\.be\/))([^& ]+)((&\S)|(&\S)|\s|$)/gim, '$8')) + '/0.jpg'); + } + + // Send the message on the XMPP network + self.publish(body, fName, fURL, fType, fLength, fThumb); + } + } catch(e) { + Console.error('Microblog.send', e); + } finally { + return false; + } + + }; + + + /** + * Publishes a given microblog item + * @public + * @param {type} body + * @param {type} attachedname + * @param {type} attachedurl + * @param {type} attachedtype + * @param {type} attachedlength + * @param {type} attachedthumb + * @param {type} repeat + * @param {type} comments_entity + * @param {type} comments_node + * @param {type} comments_entity_file + * @param {type} comments_node_file + * @return {boolean} + */ + self.publish = function(body, attachedname, attachedurl, attachedtype, attachedlength, attachedthumb, repeat, comments_entity, comments_node, comments_entity_file, comments_node_file) { + + /* REF: http://xmpp.org/extensions/xep-0277.html */ + + try { + // Generate some values + var time = DateUtils.getXMPPTime('utc'); + var id = hex_md5(body + time); + var nick = Name.get(); + var xid = Common.getXID(); + + // Define repeat options + var author_nick = nick; + var author_xid = xid; + + if(repeat && repeat.length) { + author_nick = repeat[0]; + author_xid = repeat[1]; + } + + // Define comments options + var node_create = false; + + if(!comments_entity || !comments_node) { + node_create = true; + comments_entity = HOST_PUBSUB; + comments_node = NS_URN_MBLOG + ':comments/' + id; + } + + if(!comments_entity_file) { + comments_entity_file = []; + } + + if(!comments_node_file) { + comments_node_file = []; + } + + // Don't create another comments node if only 1 file is attached + if(attachedurl && (attachedurl.length == 1) && (!comments_entity_file[0] || !comments_node_file[0])) { + comments_entity_file = [comments_entity]; + comments_node_file = [comments_node]; + } + + // New IQ + var iq = new JSJaCIQ(); + iq.setType('set'); + iq.setTo(xid); + + // Create the main XML nodes/childs + var pubsub = iq.appendNode('pubsub', {'xmlns': NS_PUBSUB}); + var publish = pubsub.appendChild(iq.buildNode('publish', {'node': NS_URN_MBLOG, 'xmlns': NS_PUBSUB})); + var item = publish.appendChild(iq.buildNode('item', {'id': id, 'xmlns': NS_PUBSUB})); + var entry = item.appendChild(iq.buildNode('entry', {'xmlns': NS_ATOM})); + entry.appendChild(iq.buildNode('title', {'xmlns': NS_ATOM})); + + // Create the XML author childs + var author = entry.appendChild(iq.buildNode('author', {'xmlns': NS_ATOM})); + author.appendChild(iq.buildNode('name', {'xmlns': NS_ATOM}, author_nick)); + author.appendChild(iq.buildNode('uri', {'xmlns': NS_ATOM}, 'xmpp:' + author_xid)); + + // Create the XML entry childs + entry.appendChild(iq.buildNode('content', {'type': 'text', 'xmlns': NS_ATOM}, body)); + entry.appendChild(iq.buildNode('published', {'xmlns': NS_ATOM}, time)); + entry.appendChild(iq.buildNode('updated', {'xmlns': NS_ATOM}, time)); + entry.appendChild(iq.buildNode('link', { + 'rel': 'alternate', + 'href': 'xmpp:' + xid + '?;node=' + encodeURIComponent(NS_URN_MBLOG) + ';item=' + encodeURIComponent(id), + 'xmlns': NS_ATOM + })); + + // Create the attached files nodes + for(var i = 0; i < attachedurl.length; i++) { + // Not enough data? + if(!attachedurl[i]) { + continue; + } + + // Append a new file element + var file = entry.appendChild(iq.buildNode('link', {'xmlns': NS_ATOM, 'rel': 'enclosure', 'href': attachedurl[i]})); + + // Add attributes + if(attachedname[i]) + file.setAttribute('title', attachedname[i]); + if(attachedtype[i]) + file.setAttribute('type', attachedtype[i]); + if(attachedlength[i]) + file.setAttribute('length', attachedlength[i]); + + // Any thumbnail? + if(attachedthumb[i]) { + file.appendChild(iq.buildNode('link', {'xmlns': NS_URN_MBLOG, 'rel': 'self', 'title': 'thumb', 'type': attachedtype[i], 'href': attachedthumb[i]})); + } + + // Any comments node? + if(!comments_entity_file[i] || !comments_node_file[i]) { + // Generate values + comments_entity_file[i] = HOST_PUBSUB; + comments_node_file[i] = NS_URN_MBLOG + ':comments/' + hex_md5(attachedurl[i] + attachedname[i] + attachedtype[i] + attachedlength[i] + time); + + // Create the node + Pubsub.setup(comments_entity_file[i], comments_node_file[i], '1', '1000000', 'open', 'open', true); + } + + file.appendChild(iq.buildNode('link', {'xmlns': NS_URN_MBLOG, 'rel': 'replies', 'title': 'comments_file', 'href': 'xmpp:' + comments_entity_file[i] + '?;node=' + encodeURIComponent(comments_node_file[i])})); + } + + // Create the comments child + entry.appendChild(iq.buildNode('link', {'xmlns': NS_ATOM, 'rel': 'replies', 'title': 'comments', 'href': 'xmpp:' + comments_entity + '?;node=' + encodeURIComponent(comments_node)})); + + // Create the geoloc child + var geoloc_xml = DataStore.getDB(Connection.desktop_hash, 'geolocation', 'now'); + + if(geoloc_xml) { + // Create two position arrays + var geo_names = ['lat', 'lon', 'country', 'countrycode', 'region', 'postalcode', 'locality', 'street', 'building', 'text', 'uri', 'timestamp']; + var geo_values = PEP.parsePosition(Common.XMLFromString(geoloc_xml)); + + // New geoloc child + var geoloc = entry.appendChild(iq.buildNode('geoloc', { + 'xmlns': NS_GEOLOC + })); + + // Append the geoloc content + for(var g = 0; g < geo_names.length; g++) { + if(geo_names[g] && geo_values[g]) { + geoloc.appendChild(iq.buildNode(geo_names[g], { + 'xmlns': NS_GEOLOC + }, geo_values[g])); + } + } + } + + // Send the IQ + con.send(iq, self.handleMine); + + // Create the XML comments PubSub nodes + if(node_create) { + Pubsub.setup(comments_entity, comments_node, '1', '1000000', 'open', 'open', true); + } + } catch(e) { + Console.error('Microblog.publish', e); + } finally { + return false; + } + + }; + + + /** + * Attaches a file to a microblog post + * @public + * @return {undefined} + */ + self.attach = function() { + + try { + // File upload vars + var attach_options = { + dataType: 'xml', + beforeSubmit: self.waitAttach, + success: self.handleAttach + }; + + // Upload form submit event + $('#attach').submit(function() { + if(!Common.exists('#attach .wait') && $('#attach input[type="file"]').val()) { + $(this).ajaxSubmit(attach_options); + } + + return false; + }); + + // Upload input change event + $('#attach input[type="file"]').change(function() { + if(!Common.exists('#attach .wait') && $(this).val()) + $('#attach').ajaxSubmit(attach_options); + + return false; + }); + } catch(e) { + Console.error('Microblog.attach', e); + } + + }; + + + /** + * Unattaches a microblog file + * @public + * @param {string} id + * @return {boolean} + */ + self.unattach = function(id) { + + try { + // Individual removal? + if(id) { + $('#attach .one-file[data-id="' + id + '"]').remove(); + } else { + $('#attach .one-file').remove(); + } + + // Must enable the popup again? + if(!Common.exists('#attach .one-file')) { + // Restore the bubble class + $('#attach').addClass('bubble'); + + // Enable the bubble click events + if(id) { + $('#attach').hide(); + Bubble.show('#attach'); + } else { + Bubble.close(); + } + } + } catch(e) { + Console.error('Microblog.unattach', e); + } finally { + return false; + } + + }; + + + /** + * Wait event for file attaching + * @public + * @return {undefined} + */ + self.waitAttach = function() { + + try { + // Append the wait icon + $('#attach input[type="submit"]').after('
'); + + // Lock the bubble + $('#attach').removeClass('bubble'); + } catch(e) { + Console.error('Microblog.waitAttach', e); + } + + }; + + + /** + * Success event for file attaching + * @public + * @param {string} responseXML + * @return {undefined} + */ + self.handleAttach = function(responseXML) { + + try { + // Data selector + var dData = $(responseXML).find('jappix'); + + // Process the returned data + if(!dData.find('error').size()) { + // Do not allow this bubble to be hidden + $('#attach').removeClass('bubble'); + + // Get the file values + var fName = dData.find('title').text(); + var fType = dData.find('type').text(); + var fLength = dData.find('length').text(); + var fURL = dData.find('href').text(); + var fThumb = dData.find('thumb').text(); + + // Generate a file ID + var fID = hex_md5(fURL); + + // Add this file + $('#attach .attach-subitem').append( + '
' + + '' + + '' + fName.htmlEnc() + '' + + '
' + ); + + // Click event + $('#attach .one-file[data-id="' + fID + '"] a.remove').click(function() { + return self.unattach(fID); + }); + + Console.info('File attached.'); + } + + // Any error? + else { + Board.openThisError(4); + + // Unlock the bubble? + if(!Common.exists('#attach .one-file')) { + $('#attach').addClass('bubble').hide(); + + // Show the bubble again! + Bubble.show('#attach'); + } + + Console.error('Error while attaching the file', dData.find('error').text()); + } + + // Reset the attach bubble + $('#attach input[type="file"]').val(''); + $('#attach .wait').remove(); + + // Focus on the text input + $(document).oneTime(10, function() { + $('#channel .top input[name="microblog_body"]').focus(); + }); + } catch(e) { + Console.error('Microblog.handleAttach', e); + } + + }; + + + /** + * Shows the microblog of an user from his infos + * @public + * @param {string} xid + * @param {string} hash + * @return {undefined} + */ + self.fromInfos = function(xid, hash) { + + try { + // Renitialize the channel + self.reset(); + + // Switch to the channel + Interface.switchChan('channel'); + + // Get the microblog + self.get(xid, hash); + } catch(e) { + Console.error('Microblog.fromInfos', e); + } + + }; + + + /** + * Plugin launcher + * @public + * @return {undefined} + */ + self.instance = function() { + + try { + var microblog_body_sel = $('#channel .top input[name="microblog_body"]'); + + // Keyboard event + microblog_body_sel.keyup(function(e) { + // Enter pressed: send the microblog notice + if((e.keyCode == 13) && !Common.exists('#attach .wait')) { + return self.send(); + } + }); + + // Placeholder + microblog_body_sel.placeholder(); + + // Microblog file attacher + self.attach(); + } catch(e) { + Console.error('Microblog.instance', e); + } + + }; + + + /** + * Return class scope + */ + return self; + })(); \ No newline at end of file diff --git a/source/app/javascripts/mini.js b/source/app/javascripts/mini.js index 885557d..7b5eeac 100644 --- a/source/app/javascripts/mini.js +++ b/source/app/javascripts/mini.js @@ -748,7 +748,7 @@ var JappixMini = (function () { var resources_obj = {}; // Is this a groupchat? - if(JappixCommon.exists('#jappix_mini div.jm_conversation[data-type="groupchat"][data-xid="' + JappixCommon.encodeQuotes(xid) + '"]')) { + if(JappixCommon.exists('#jappix_mini div.jm_conversation[data-type="groupchat"][data-xid="' + JappixCommon.escapeQuotes(xid) + '"]')) { xid = from; } @@ -1003,7 +1003,7 @@ var JappixMini = (function () { is_groupchat = true; // Groupchat buddy presence (not me) - if(resource != unescape(jQuery(groupchat_path).attr('data-nick'))) { + if(resource != JappixCommon.unescapeQuotes(jQuery(groupchat_path).attr('data-nick'))) { // Regenerate some stuffs var groupchat = xid; var groupchat_hash = hash; @@ -1059,7 +1059,7 @@ var JappixMini = (function () { // Check against search string var search = jQuery('#jappix_mini div.jm_roster div.jm_search input.jm_searchbox').val(); var regex = new RegExp('((^)|( ))' + JappixCommon.escapeRegex(search), 'gi'); - var nick = unescape(jQuery(friend).data('nick')); + var nick = JappixCommon.unescapeQuotes(jQuery(friend).data('nick')); if(search && !nick.match(regex)) { jQuery(friend).hide(); @@ -1110,7 +1110,7 @@ var JappixMini = (function () { // Is it a valid server presence? var valid = false; - if(!resource || (resource == unescape(jQuery('#jappix_mini #chat-' + hash + '[data-type="groupchat"]').attr('data-nick')))) { + if(!resource || (resource == JappixCommon.unescapeQuotes(jQuery('#jappix_mini #chat-' + hash + '[data-type="groupchat"]').attr('data-nick')))) { valid = true; } @@ -1154,7 +1154,7 @@ var JappixMini = (function () { self.presence('', '', '', '', room + '/' + nickname, '', true, self.handleMUC); // Update the nickname marker - jQuery('#jappix_mini #chat-' + hash).attr('data-nick', escape(nickname)); + jQuery('#jappix_mini #chat-' + hash).attr('data-nick', JappixCommon.escapeQuotes(nickname)); } // Handle normal presence @@ -1258,7 +1258,7 @@ var JappixMini = (function () { // If the roster does not give us any nick the user may have send us a nickname to use with his first message // @see http://xmpp.org/extensions/xep-0172.html - var known_roster_entry = jQuery('#jappix_mini a.jm_friend[data-xid="' + xid + '"]'); + var known_roster_entry = jQuery('#jappix_mini a.jm_friend[data-xid="' + JappixCommon.escapeQuotes(xid) + '"]'); if(known_roster_entry.size() === 0) { var subscription = known_roster_entry.attr('data-sub'); @@ -1952,7 +1952,7 @@ var JappixMini = (function () { jQuery('#jappix_mini div.jm_conversation[data-type="groupchat"]').each(function() { var this_sub_sel = jQuery(this); - pr_xid.push(unescape(this_sub_sel.attr('data-xid')) + '/' + unescape(this_sub_sel.attr('data-nick'))); + pr_xid.push(JappixCommon.unescapeQuotes(this_sub_sel.attr('data-xid')) + '/' + JappixCommon.unescapeQuotes(this_sub_sel.attr('data-nick'))); }); // Loop on XIDs @@ -2032,7 +2032,7 @@ var JappixMini = (function () { var chat_pwd = MINI_SUGGEST_PASSWORDS[i] || ''; chans_html += - '' + + '' + '' + '' + JappixCommon.getXIDNick(chat_room).htmlEnc() + '' + ''; @@ -2064,12 +2064,12 @@ var JappixMini = (function () { if(!chat_nick) { chat_nick = JappixCommon.getXIDNick(chat_xid); } else { - chat_nick = unescape(chat_nick); + chat_nick = JappixCommon.unescapeQuotes(chat_nick); } // Generate HTML for current chat chans_html += - '' + + '' + '' + '' + JappixCommon.getXIDNick(chat_nick).htmlEnc() + '' + ''; @@ -2103,18 +2103,18 @@ var JappixMini = (function () { // Chat? if(this_sub_sel.is('.jm_suggest_chat')) { - var current_chat = unescape(this_sub_sel.attr('data-xid')); + var current_chat = JappixCommon.unescapeQuotes(this_sub_sel.attr('data-xid')); self.chat('chat', current_chat, this_sub_sel.find('span.jm_chan_name').text(), hex_md5(current_chat)); } // Groupchat? else if(this_sub_sel.is('.jm_suggest_groupchat')) { - var current_groupchat = unescape(this_sub_sel.attr('data-xid')); + var current_groupchat = JappixCommon.unescapeQuotes(this_sub_sel.attr('data-xid')); var current_password = this_sub_sel.attr('data-pwd') || null; if(current_password) - current_password = unescape(current_password); + current_password = JappixCommon.unescapeQuotes(current_password); self.chat('groupchat', current_groupchat, this_sub_sel.find('span.jm_chan_name').text(), hex_md5(current_groupchat), current_password); } @@ -2194,7 +2194,7 @@ var JappixMini = (function () { // Filter buddies jQuery('#jappix_mini div.jm_roster div.jm_buddies a.jm_online').each(function() { var this_sub_sel = jQuery(this); - var nick = unescape(this_sub_sel.data('nick')); + var nick = JappixCommon.unescapeQuotes(this_sub_sel.data('nick')); if(nick.match(regex)) { this_sub_sel.show(); @@ -2380,7 +2380,7 @@ var JappixMini = (function () { // Restore chat click events jQuery('#jappix_mini div.jm_conversation').each(function() { var this_sub_sel = jQuery(this); - self.chatEvents(this_sub_sel.attr('data-type'), unescape(this_sub_sel.attr('data-xid')), this_sub_sel.attr('data-hash')); + self.chatEvents(this_sub_sel.attr('data-type'), JappixCommon.unescapeQuotes(this_sub_sel.attr('data-xid')), this_sub_sel.attr('data-hash')); }); // Restore init marker on all groupchats @@ -2447,7 +2447,7 @@ var JappixMini = (function () { // Using a try/catch override IE issues try { var this_sel = jQuery(this); - self.chat('chat', unescape(this_sel.attr('data-xid')), unescape(this_sel.attr('data-nick')), this_sel.attr('data-hash')); + self.chat('chat', JappixCommon.unescapeQuotes(this_sel.attr('data-xid')), JappixCommon.unescapeQuotes(this_sel.attr('data-nick')), this_sel.attr('data-hash')); } catch(e) {} @@ -2866,7 +2866,7 @@ var JappixMini = (function () { } // Create the HTML markup - var html = '
' + + var html = '
' + '
' + '
' + '' + nick + ''; @@ -2952,7 +2952,7 @@ var JappixMini = (function () { // Join the groupchat if(type == 'groupchat') { // Add nickname & init values - jQuery(current).attr('data-nick', escape(nickname)) + jQuery(current).attr('data-nick', JappixCommon.escapeQuotes(nickname)) .attr('data-init', 'false'); // Send the first groupchat presence @@ -3035,7 +3035,7 @@ var JappixMini = (function () { // Quit the groupchat? if(type == 'groupchat') { // Send an unavailable presence - self.presence('unavailable', '', '', '', xid + '/' + unescape(current_sel.attr('data-nick'))); + self.presence('unavailable', '', '', '', xid + '/' + JappixCommon.unescapeQuotes(current_sel.attr('data-nick'))); // Remove this groupchat! self.removeGroupchat(xid); @@ -3221,7 +3221,7 @@ var JappixMini = (function () { try { // Remove the groupchat private chats & the groupchat buddies from the roster - jQuery('#jappix_mini div.jm_conversation[data-origin="' + escape(JappixCommon.cutResource(xid)) + '"], #jappix_mini div.jm_roster div.jm_grouped[data-xid="' + escape(xid) + '"]').remove(); + jQuery('#jappix_mini div.jm_conversation[data-origin="' + JappixCommon.escapeQuotes(JappixCommon.cutResource(xid)) + '"], #jappix_mini div.jm_roster div.jm_grouped[data-xid="' + JappixCommon.escapeQuotes(xid) + '"]').remove(); // Update the presence counter self.updateRoster(); @@ -3282,7 +3282,7 @@ var JappixMini = (function () { if(!chat_nick) { chat_nick = JappixCommon.getXIDNick(chat_xid); } else { - chat_nick = unescape(chat_nick); + chat_nick = JappixCommon.unescapeQuotes(chat_nick); } // Open the current chat @@ -3325,7 +3325,7 @@ var JappixMini = (function () { // Group: start if(c != MINI_ROSTER_NOGROUP) { - buddy_str += '
'; + buddy_str += '
'; buddy_str += '
' + c.htmlEnc() + '
'; } @@ -3392,23 +3392,23 @@ var JappixMini = (function () { // Generate the groupchat group path if(groupchat) { - path = '#jappix_mini div.jm_roster div.jm_grouped_groupchat[data-xid="' + escape(bare_xid) + '"]'; + path = '#jappix_mini div.jm_roster div.jm_grouped_groupchat[data-xid="' + JappixCommon.escapeQuotes(bare_xid) + '"]'; // Must add a groupchat group? if(!JappixCommon.exists(path)) { jQuery('#jappix_mini div.jm_roster div.jm_buddies').append( - '
' + + '
' + '
' + JappixCommon.getXIDNick(groupchat).htmlEnc() + '
' + '
' ); } } else if(group) { - path = '#jappix_mini div.jm_roster div.jm_grouped_roster[data-name="' + escape(group) + '"]'; + path = '#jappix_mini div.jm_roster div.jm_grouped_roster[data-name="' + JappixCommon.escapeQuotes(group) + '"]'; // Must add a roster group? if(!JappixCommon.exists(path)) { jQuery('#jappix_mini div.jm_roster div.jm_buddies').append( - '
' + + '
' + '
' + group.htmlEnc() + '
' + '
' ); @@ -3465,8 +3465,8 @@ var JappixMini = (function () { buddy_str += '' + '' + '
' ); @@ -4185,7 +4185,7 @@ var JappixMini = (function () { // Append final stylesheet HTML for(var u in css_url) { - css_html += ''; + css_html += ''; } jQuery('head').append(css_html); diff --git a/source/app/javascripts/mobile.js b/source/app/javascripts/mobile.js index af7d607..72bff9e 100644 --- a/source/app/javascripts/mobile.js +++ b/source/app/javascripts/mobile.js @@ -47,10 +47,8 @@ var Mobile = (function () { return false; } - } - - // No "@" in the XID, we should add the default domain - else { + } else { + // No "@" in the XID, we should add the default domain username = xid; domain = HOST_MAIN; } @@ -58,8 +56,9 @@ var Mobile = (function () { var pwd = aForm.pwd.value; var reg = false; - if(aForm.reg) + if(aForm.reg) { reg = aForm.reg.checked; + } // Enough parameters if(username && domain && pwd) { @@ -141,6 +140,30 @@ var Mobile = (function () { }; + /** + * Proceeds client initialization + * @public + * @return {undefined} + */ + self.doInitialize = function() { + + try { + if(typeof HTTP_AUTH === 'object' && + HTTP_AUTH.user && HTTP_AUTH.password && HTTP_AUTH.host) { + var form_sel = document.forms['login-form']; + + form_sel.elements.xid.value = (HTTP_AUTH.user + '@' + HTTP_AUTH.host); + form_sel.elements.pwd.value = HTTP_AUTH.password; + + self.doLogin(form_sel); + } + } catch(e) { + Console.error('Mobile.doInitialize', e); + } + + }; + + /** * Shows target element * @public @@ -308,8 +331,9 @@ var Mobile = (function () { var nick = self.getNick(xid, hash); // No nickname? - if(!nick) + if(!nick) { nick = xid; + } // Create the chat if it does not exist self.chat(xid, nick); @@ -340,10 +364,8 @@ var Mobile = (function () { var type = pre.getType(); var show = pre.getShow(); - // Online buddy: show it! + // Online buddy if(!type) { - self.showThis('buddy-' + hash); - // Display the correct presence switch(show) { case 'chat': @@ -366,8 +388,6 @@ var Mobile = (function () { self.displayPresence(hash, 'available'); break; } - } else { - self.hideThis('buddy-' + hash); } } catch(e) { Console.error('Mobile.handlePresence', e); @@ -538,7 +558,9 @@ var Mobile = (function () { return self.sendPresence('', 'available', 1); // Define some pre-vars - var current, xid, nick, oneBuddy, oneID, hash; + var current, xid, nick, oneBuddy, oneID, hash, cur_buddy; + var roster_buddies = []; + var roster = document.getElementById('roster'); // Get roster items @@ -554,18 +576,30 @@ var Mobile = (function () { hash = hex_md5(xid); // No defined nick? - if(!nick) + if(!nick) { nick = self.getDirectNick(xid); - - // Display the values - oneBuddy = document.createElement('a'); - oneID = 'buddy-' + hash; - oneBuddy.setAttribute('href', '#'); - oneBuddy.setAttribute('id', oneID); - oneBuddy.setAttribute('class', 'one-buddy'); - oneBuddy.setAttribute('onclick', 'return Mobile.chat(\'' + self.encodeOnclick(xid) + '\', \'' + self.encodeOnclick(nick) + '\');'); - oneBuddy.innerHTML = nick.htmlEnc(); - roster.appendChild(oneBuddy); + } + + roster_buddies.push({ + 'xid': xid, + 'hash': hash, + 'nick': nick + }); + } + + // Sort the values + self.sortRoster(roster_buddies); + + // Display the values + for(var j = 0; j < roster_buddies.length; j++) { + cur_buddy = roster_buddies[j]; + + self.displayRoster( + roster, + cur_buddy.xid, + cur_buddy.hash, + cur_buddy.nick + ); } // Start handling buddies presence @@ -721,10 +755,11 @@ var Mobile = (function () { // We split if necessary the string if(index !== -1) { - if(i === 0) + if(i === 0) { toStr = toStr.substr(0, index); - else + } else { toStr = toStr.substr(index + 1); + } } // We return the value @@ -831,10 +866,11 @@ var Mobile = (function () { // Display the message html = ''; @@ -849,6 +885,61 @@ var Mobile = (function () { }; + /** + * Displays a roster buddy + * @public + * @param {object} roster + * @param {string} xid + * @param {string} hash + * @param {string} nick + * @return {undefined} + */ + self.displayRoster = function(roster, xid, hash, nick) { + + try { + oneBuddy = document.createElement('a'); + oneID = 'buddy-' + hash; + + oneBuddy.setAttribute('href', '#'); + oneBuddy.setAttribute('id', oneID); + oneBuddy.setAttribute('class', 'one-buddy'); + oneBuddy.setAttribute('onclick', 'return Mobile.chat(\'' + self.encodeOnclick(xid) + '\', \'' + self.encodeOnclick(nick) + '\');'); + oneBuddy.innerHTML = nick.htmlEnc(); + + roster.appendChild(oneBuddy); + } catch(e) { + Console.error('Mobile.displayRoster', e); + } + + }; + + + /** + * Sorts the roster buddies by nickname + * @public + * @param {object} roster_buddies + * @return {object} + */ + self.sortRoster = function(roster_buddies) { + + try { + var one_nick, two_nick; + + roster_buddies.sort(function(one, two) { + one_nick = (one.nick + '').toLowerCase(); + two_nick = (two.nick + '').toLowerCase(); + + return one_nick < two_nick ? -1 : (one_nick > two_nick ? 1 : 0); + }); + } catch(e) { + Console.error('Mobile.sortRoster', e); + } finally { + return roster_buddies; + } + + }; + + /** * Goes back to roster view * @public @@ -885,8 +976,9 @@ var Mobile = (function () { var divs = document.getElementsByTagName('div'); for(var i = 0; i < divs.length; i++) { - if(divs.item(i).getAttribute('class') == 'one-chat') + if(divs.item(i).getAttribute('class') == 'one-chat') { divs.item(i).style.display = 'none'; + } } // Show the chat @@ -941,8 +1033,9 @@ var Mobile = (function () { // If the chat was not yet opened if(!self.exists('chat-' + hash)) { // No nick? - if(!nick) + if(!nick) { nick = self.getNick(xid, hash); + } // Create the chat self.createChat(xid, nick, hash); @@ -990,6 +1083,7 @@ var Mobile = (function () { try { onbeforeunload = self.doLogout; + onload = self.doInitialize; } catch(e) { Console.error('Mobile.launch', e); } diff --git a/source/app/javascripts/mucadmin.js b/source/app/javascripts/mucadmin.js index 2827e17..11b613c 100644 --- a/source/app/javascripts/mucadmin.js +++ b/source/app/javascripts/mucadmin.js @@ -154,6 +154,7 @@ var MUCAdmin = (function () { self.query(xid, 'owner'); self.query(xid, 'admin'); self.query(xid, 'outcast'); + // We query the room to edit DataForm.go(xid, 'muc', '', '', 'mucadmin'); } else if(aff == 'admin') { @@ -434,8 +435,9 @@ var MUCAdmin = (function () { Board.openThisInfo(5); // We remove the user's favorite - if(DataStore.existDB('favorites', room)) + if(DataStore.existDB(Connection.desktop_hash, 'favorites', room)) { Favorites.removeThis(room, Common.explodeThis('@', room, 0)); + } Console.info('MUC admin destroyed: ' + room); } @@ -555,10 +557,13 @@ var MUCAdmin = (function () { try { // Click events $('#mucadmin .bottom .finish').click(function() { - if($(this).is('.cancel')) + if($(this).is('.cancel')) { return self.close(); - if($(this).is('.save')) + } + + if($(this).is('.save')) { return self.save(); + } }); } catch(e) { Console.error('MUCAdmin.instance', e); diff --git a/source/app/javascripts/muji.js b/source/app/javascripts/muji.js new file mode 100644 index 0000000..40c3358 --- /dev/null +++ b/source/app/javascripts/muji.js @@ -0,0 +1,1824 @@ +/* + +Jappix - An open social platform +These are the Muji helpers & launchers + +------------------------------------------------- + +License: AGPL +Author: Valérian Saliou + +*/ + +// Bundle +var Muji = (function() { + + /** + * Alias of this + * @private + */ + var self = {}; + + + /* Variables */ + self._session = null; + self._caller_xid = null; + + + /** + * Opens the Muji interface (depending on the state) + * @public + * @return {boolean} + */ + self.open = function() { + + try { + var call_tool_sel = $('#top-content .tools.call'); + + if(call_tool_sel.is('.active')) { + Console.info('Opened call notification drawer'); + } else if(call_tool_sel.is('.streaming')) { + self._show_interface(); + + Console.info('Opened Muji box'); + } else { + Console.warn('Could not open any Muji tool (race condition on state)'); + } + } catch(e) { + Console.error('Muji.open', e); + } finally { + return false; + } + + }; + + + /** + * Returns the Muji session arguments (used to configure it) + * @private + * @param connection + * @param xid + * @param hash + * @param local_view + * @return {object} + */ + self._args = function(connection, xid, hash, media, local_view) { + + args = {}; + + try { + // Network configuration + var ice_servers = Call.generate_ice_servers(); + + // Muji arguments + args = { + // Configuration (required) + connection: connection, + to: xid, + media: media, + local_view: local_view, + stun: ice_servers.stun, + turn: ice_servers.turn, + resolution: 'sd', + debug: Call._consoleAdapter, + + // Safety options (optional) + password_protect: true, + + // Custom handlers (optional) + room_message_in: function(muji, stanza) { + var from = muji.utils.stanza_from(stanza); + var username = muji.utils.extract_username(from); + var body = stanza.getBody(); + var muji_sel = $('#muji'); + + var mode = (username === $('#muji').attr('data-username')) ? 'me' : 'him'; + + if(username && body && muji_sel.size()) { + var avatar_html = ''; + + if(mode === 'him') { + avatar_html = + '
' + + '' + + '
'; + } + + muji_sel.find('.chatroom .chatroom_view').append( + '
' + + avatar_html + + + '
' + + '' + body.htmlEnc() + '' + + '' + username.htmlEnc() + '' + + '
' + + + '
' + + '
' + ); + + if($('#muji').is(':visible')) { + self._message_scroll(); + } else { + self._message_notify(); + } + + Console.log('Muji._args > room_message_in', 'Displayed Muji message from: ' + username); + } + + Console.log('Muji._args', 'room_message_in'); + }, + + room_message_out: function(muji, stanza) { + Console.log('Muji._args', 'room_message_out'); + }, + + room_presence_in: function(muji, stanza) { + Console.log('Muji._args', 'room_presence_in'); + }, + + room_presence_out: function(muji, stanza) { + Console.log('Muji._args', 'room_presence_out'); + }, + + session_prepare_pending: function(muji, stanza) { + // Temporary username + $('#muji').attr('data-username', muji.get_username()); + + // Notify user about preparing call + Call.notify( + JSJAC_JINGLE_SESSION_MUJI, + muji.get_to(), + 'preparing', + muji.get_media(), + self.get_caller_xid() + ); + + Console.log('Muji._args', 'session_prepare_pending'); + }, + + session_prepare_success: function(muji, stanza) { + // Final username + $('#muji').attr('data-username', muji.get_username()); + + Console.log('Muji._args', 'session_prepare_success'); + }, + + session_prepare_error: function(muji, stanza) { + self._reset(); + + Call.notify( + JSJAC_JINGLE_SESSION_MUJI, + muji.get_to(), + 'error', + muji.get_media(), + self.get_caller_xid() + ); + + Console.log('Muji._args', 'session_prepare_error'); + }, + + session_initiate_pending: function(muji) { + Call.notify( + JSJAC_JINGLE_SESSION_MUJI, + muji.get_to(), + 'waiting', + muji.get_media(), + self.get_caller_xid() + ); + + Console.log('Muji._args', 'session_initiate_pending'); + }, + + session_initiate_success: function(muji, stanza) { + Call._unnotify(); + + // Start call! Go Go Go! + Call.start_session(muji.get_media()); + self._show_interface(); + Call.start_counter(); + + Console.log('Muji._args', 'session_initiate_success'); + }, + + session_initiate_error: function(muji, stanza) { + self._reset(); + + Call.notify( + JSJAC_JINGLE_SESSION_MUJI, + muji.get_to(), + 'error', + muji.get_media(), + self.get_caller_xid() + ); + + Console.log('Muji._args', 'session_initiate_error'); + }, + + session_leave_pending: function(muji) { + self._reset(); + + Call.notify( + JSJAC_JINGLE_SESSION_MUJI, + muji.get_to(), + 'ending', + muji.get_media(), + self.get_caller_xid() + ); + + Console.log('Muji._args', 'session_leave_pending'); + }, + + session_leave_success: function(muji, stanza) { + self._reset(); + + Call.notify( + JSJAC_JINGLE_SESSION_MUJI, + muji.get_to(), + 'ended', + muji.get_media(), + self.get_caller_xid() + ); + + Console.log('Muji._args', 'session_leave_success'); + }, + + session_leave_error: function(muji, stanza) { + self._reset(); + + if(typeof muji.parent != 'undefined') { + muji = muji.parent; + } + + Call.notify( + JSJAC_JINGLE_SESSION_MUJI, + muji.get_to(), + 'ended', + muji.get_media(), + self.get_caller_xid() + ); + + Console.log('Muji._args', 'session_leave_error'); + }, + + participant_prepare: function(muji, stanza) { + Console.log('Muji._args', 'participant_prepare'); + }, + + participant_initiate: function(muji, stanza) { + Console.log('Muji._args', 'participant_initiate'); + }, + + participant_leave: function(muji, stanza) { + Console.log('Muji._args', 'participant_leave'); + }, + + participant_session_initiate_pending: function(muji, session) { + Console.log('Muji._args', 'participant_session_initiate_pending'); + }, + + participant_session_initiate_success: function(muji, session, stanza) { + Console.log('Muji._args', 'participant_session_initiate_success'); + }, + + participant_session_initiate_error: function(muji, session, stanza) { + Console.log('Muji._args', 'participant_session_initiate_error'); + }, + + participant_session_initiate_request: function(muji, session, stanza) { + Console.log('Muji._args', 'participant_session_initiate_request'); + }, + + participant_session_accept_pending: function(muji, session) { + Console.log('Muji._args', 'participant_session_accept_pending'); + }, + + participant_session_accept_success: function(muji, session, stanza) { + Console.log('Muji._args', 'participant_session_accept_success'); + }, + + participant_session_accept_error: function(muji, session, stanza) { + Console.log('Muji._args', 'participant_session_accept_error'); + }, + + participant_session_accept_request: function(muji, session, stanza) { + Console.log('Muji._args', 'participant_session_accept_request'); + }, + + participant_session_info_pending: function(muji, session) { + Console.log('Muji._args', 'participant_session_info_pending'); + }, + + participant_session_info_success: function(muji, session, stanza) { + Console.log('Muji._args', 'participant_session_info_success'); + }, + + participant_session_info_error: function(muji, session, stanza) { + Console.log('Muji._args', 'participant_session_info_error'); + }, + + participant_session_info_request: function(muji, session, stanza) { + Console.log('Muji._args', 'participant_session_info_request'); + }, + + participant_session_terminate_pending: function(muji, session) { + Console.log('Muji._args', 'participant_session_terminate_pending'); + }, + + participant_session_terminate_success: function(muji, session, stanza) { + Console.log('Muji._args', 'participant_session_terminate_success'); + }, + + participant_session_terminate_error: function(muji, session, stanza) { + Console.log('Muji._args', 'participant_session_terminate_error'); + }, + + participant_session_terminate_request: function(muji, session, stanza) { + Console.log('Muji._args', 'participant_session_terminate_request'); + }, + + add_remote_view: function(muji, username, media) { + Console.log('Muji._args', 'add_remote_view'); + + var muji_sel = $('#muji'); + var nobody_sel = muji_sel.find('.empty_message'); + var remote_container_sel = $('#muji .remote_container'); + var remote_video_shaper_sel = remote_container_sel.find('.remote_video_shaper'); + + var view_sel = null; + var container_sel = remote_video_shaper_sel.filter(function() { + return ($(this).attr('data-username') + '') === (username + ''); + }); + + var count_participants = remote_video_shaper_sel.filter(':has(video)').size(); + + // Not already in view? + if(!container_sel.size()) { + // Select first empty view + var first_empty_view_sel = remote_video_shaper_sel.filter(':not(:has(video)):first'); + + if(first_empty_view_sel.size()) { + container_sel = first_empty_view_sel; + + // Remote poster + var remote_poster = './images/placeholders/jingle_video_remote.png'; + + if(media === 'audio') { + remote_poster = './images/placeholders/jingle_audio_remote.png'; + } + + // Append view + view_sel = $(''); + + container_sel.attr('data-username', username); + view_sel.appendTo(container_sel); + + // Append username label + container_sel.append( + '' + username.htmlEnc() + '' + ); + + // Update counter + muji_sel.attr( + 'data-count', + ++count_participants + ); + } else { + // Room is full... + muji_sel.find('.chatroom_participants .participants_full:hidden').show(); + } + } + + nobody_sel.hide(); + Muji._update_count_participants(count_participants); + Muji._update_invite_participants(); + + // IMPORTANT: return view selector + return (view_sel !== null) ? view_sel[0] : view_sel; + }, + + remove_remote_view: function(muji, username) { + Console.log('Muji._args', 'remove_remote_view'); + + var muji_sel = $('#muji'); + var nobody_sel = muji_sel.find('.empty_message'); + var remote_container_sel = $('#muji .remote_container'); + var remote_video_shaper_sel = remote_container_sel.find('.remote_video_shaper'); + + var container_sel = remote_video_shaper_sel.filter(function() { + return ($(this).attr('data-username') + '') === (username + ''); + }); + + var count_participants = remote_video_shaper_sel.filter(':has(video)').size(); + + // Exists in view? + if(container_sel.size()) { + var view_sel = container_sel.find('video'); + + // Remove video + view_sel.stop(true).fadeOut(250, function() { + container_sel.empty(); + + // Update counter + muji_sel.attr( + 'data-count', + --count_participants + ); + + // Nobody left in the room? + if(!remote_video_shaper_sel.find('video').size()) { + nobody_sel.show(); + muji_sel.removeAttr('data-count'); + } + + // Update participants counter + muji_sel.find('.chatroom_participants .participants_full:visible').hide(); + Muji._update_count_participants(count_participants); + Muji._update_invite_participants(); + }); + + // IMPORTANT: return view selector + if(view_sel.size()) { + return view_sel[0]; + } + } + + return null; + } + }; + } catch(e) { + Console.error('Muji._args', e); + } finally { + return args; + } + + }; + + + /** + * Launch a new Muji session with given buddy + * @private + * @param room + * @param mode + * @param args_invite + * @return {boolean} + */ + self._new = function(room, mode, stanza, args_invite) { + + var status = false; + + try { + if(!room) { + throw 'No room to be joined given!'; + } + + var hash = hex_md5(room); + + // Create interface for video containers + $('body').addClass('in_muji_call'); + var muji_sel = self._create_interface(room, mode); + + // Filter media + var media = null; + + switch(mode) { + case 'audio': + media = JSJAC_JINGLE_MEDIA_AUDIO; break; + case 'video': + media = JSJAC_JINGLE_MEDIA_VIDEO; break; + } + + // Start the Jingle negotiation + var args = self._args( + con, + room, + hash, + media, + muji_sel.find('.local_video video')[0] + ); + + if(typeof args_invite == 'object') { + if(args_invite.password) { + args.password = args_invite.password; + } + + args.media = (args_invite.media == JSJAC_JINGLE_MEDIA_VIDEO) ? JSJAC_JINGLE_MEDIA_VIDEO + : JSJAC_JINGLE_MEDIA_AUDIO; + + self._session = new JSJaCJingle.session(JSJAC_JINGLE_SESSION_MUJI, args); + self._caller_xid = Common.bareXID(args_invite.from); + + Console.debug('Receive Muji call: ' + room); + } else { + self._session = new JSJaCJingle.session(JSJAC_JINGLE_SESSION_MUJI, args); + self._caller_xid = Common.getXID(); + + self._session.join(); + + Console.debug('Create Muji call: ' + room); + } + + Console.debug('Join Muji conference: ' + room); + + status = true; + } catch(e) { + Console.error('Muji._new', e); + } finally { + return status; + } + + }; + + + /** + * Updates the participants counter value + * @private + * @param {number} count_participants + * @return {undefined} + */ + self._update_count_participants = function(count_participants) { + + try { + count_participants = (count_participants || 0); + + var participants_counter_sel = $('#muji .chatroom_participants .participants_counter'); + + if(count_participants === 1) { + participants_counter_sel.text( + Common.printf(Common._e("%s participant"), count_participants) + ); + } else { + participants_counter_sel.text( + Common.printf(Common._e("%s participants"), count_participants) + ); + } + } catch(e) { + Console.error('Muji._update_count_participants', e); + } + + }; + + + /** + * Updates the participants invite tool + * @private + * @return {undefined} + */ + self._update_invite_participants = function() { + + try { + var chatroom_participants_sel = $('#muji .chatroom_participants'); + + var participants_invite_sel = chatroom_participants_sel.find('.participants_invite'); + var participants_invite_box_sel = chatroom_participants_sel.find('.participants_invite_box'); + + if(self.is_full()) { + if(participants_invite_box_sel.is(':visible')) { + participants_invite_box_sel.stop(true); + participants_invite_sel.click(); + } + + participants_invite_sel.filter(':visible').hide(); + } else { + participants_invite_sel.filter(':hidden').show(); + } + } catch(e) { + Console.error('Muji._update_invite_participants', e); + } + + }; + + + /** + * Resets the participants invite filter + * @private + * @return {undefined} + */ + self._reset_participants_invite_filter = function() { + + try { + // Selectors + var chatroom_sel = $('#muji .chatroom'); + var invite_form_sel = chatroom_sel.find('form.participants_invite_form'); + var invite_search_sel = chatroom_sel.find('.participants_invite_search'); + + // Apply + invite_form_sel.find('input.invite_xid').val(''); + + invite_search_sel.empty(); + } catch(e) { + Console.error('Muji._reset_participants_invite_filter', e); + } + + }; + + + /** + * Engages the participants invite filter + * @private + * @param {string} value + * @return {undefined} + */ + self._engage_participants_invite_filter = function(value) { + + try { + // Selectors + var chatroom_sel = $('#muji .chatroom'); + var invite_input_sel = chatroom_sel.find('form.participants_invite_form input.invite_xid'); + var invite_search_sel = chatroom_sel.find('.participants_invite_search'); + + // Reset UI + invite_search_sel.empty(); + + // Proceed search + var results_arr = Search.processBuddy(value); + var results_html = ''; + var bold_regex = new RegExp('((^)|( ))' + value, 'gi'); + + // Exclude already selected buddies + var exclude_obj = self._list_participants_invite_list(); + + if(results_arr && results_arr.length) { + var i, j, + cur_xid, cur_full_xid, cur_hash, cur_support, cur_name, cur_title, + cur_name_bolded, cur_support_class; + + for(i = 0; i < results_arr.length; i++) { + // Generate result data + cur_xid = results_arr[i]; + + if(exclude_obj[cur_xid] !== 1) { + cur_hash = hex_md5(cur_xid); + cur_name = Name.getBuddy(cur_xid); + + // Get target's full XID + cur_full_xid = Caps.getFeatureResource(cur_xid, NS_MUJI); + cur_support = null; + + if(cur_full_xid) { + if(Caps.getFeatureResource(cur_xid, NS_JINGLE_APPS_RTP_VIDEO)) { + cur_support = 'video'; + } else { + cur_support = 'audio'; + } + } + + // Generate a hint title & a class + if(cur_support) { + cur_title = Common.printf(Common._e("%s is able to receive group calls."), cur_name); + cur_support_class = 'participant_search_has_' + cur_support; + } else { + cur_title = Common.printf(Common._e("%s may not support group calls."), cur_name); + cur_support_class = 'participant_search_unsupported'; + } + + // Bold matches in name + cur_name_bolded = cur_name.htmlEnc().replace(bold_regex, '$&'); + + // Generate result HTML + results_html += + '
' + + '' + + '' + + '' + + + '' + + '' + cur_name_bolded + '' + + '' + + '' + + ''; + } + } + + // Add to DOM + invite_search_sel.append(results_html); + + var search_one_sel = invite_search_sel.find('a.participant_search_one'); + search_one_sel.filter(':first').addClass('hover'); + + // Apply avatars + for(j = 0; j < results_arr.length; j++) { + Avatar.get(results_arr[j], 'cache', 'true', 'forget'); + } + + // Apply events + search_one_sel.click(function() { + var this_sel = $(this); + + self._add_participants_invite_list( + this_sel.attr('data-xid'), + this_sel.text(), + this_sel.attr('data-support') + ); + + self._reset_participants_invite_filter(); + invite_input_sel.focus(); + }); + + search_one_sel.hover(function() { + search_one_sel.filter('.hover').removeClass('hover'); + $(this).addClass('hover'); + }, function() { + $(this).removeClass('hover'); + }); + } + } catch(e) { + Console.error('Muji._engage_participants_invite_filter', e); + } + + }; + + + /** + * Sends participant actual Muji invite + * @private + * @param {string|object} xid + * @return {undefined} + */ + self._send_participants_invite_list = function(xid) { + + try { + if(self.in_call()) { + self._session.invite(xid); + } + } catch(e) { + Console.error('Muji._send_participants_invite_list', e); + } + + }; + + + /** + * Adds a participant to the invite list + * @private + * @param {string} xid + * @param {string} name + * @param {string} support + * @return {undefined} + */ + self._add_participants_invite_list = function(xid, name, support) { + + try { + // Selectors + var chatroom_sel = $('#muji .chatroom'); + var invite_form_sel = chatroom_sel.find('form.participants_invite_form'); + var invite_list_sel = chatroom_sel.find('.participants_invite_list'); + + var pre_invite_one_sel = invite_list_sel.find('.invite_one').filter(function() { + return (xid === $(this).attr('data-xid')) && true; + }); + + if(pre_invite_one_sel.size()) { + throw 'Already existing for: ' + xid; + } + + var title; + var _class = []; + + switch(support) { + case 'audio': + case 'video': + title = Common.printf(Common._e("%s is able to receive group calls."), name); break; + + default: + title = Common.printf(Common._e("%s may not support group calls."), name); + _class.push('invite_unsupported'); + } + + // Append element + var invite_one_sel = $('' + name.htmlEnc() + ''); + invite_one_sel.appendTo(invite_list_sel); + + // Events + invite_one_sel.find('a.invite_one_remove').click(function() { + self._remove_participants_invite_list(invite_one_sel); + }); + + if(invite_list_sel.find('.invite_one').size() >= 1) { + invite_form_sel.find('.invite_validate').show(); + invite_list_sel.filter(':hidden').show(); + } + } catch(e) { + Console.error('Muji._add_participants_invite_list', e); + } + + }; + + + /** + * Removes a participant from the invite list + * @private + * @param {object} participant_sel + * @return {undefined} + */ + self._remove_participants_invite_list = function(participant_sel) { + + try { + // Selectors + var chatroom_sel = $('#muji .chatroom'); + var invite_form_sel = chatroom_sel.find('form.participants_invite_form'); + var invite_list_sel = chatroom_sel.find('.participants_invite_list'); + + participant_sel.remove(); + + if(invite_list_sel.find('.invite_one').size() === 0) { + invite_form_sel.find('.invite_validate').hide(); + invite_list_sel.filter(':visible').hide(); + } + + invite_form_sel.find('input.invite_xid').focus(); + } catch(e) { + Console.error('Muji._remove_participants_invite_list', e); + } + + }; + + + /** + * Hovers either the next or previous participant + * @private + * @param {string} direction + * @return {undefined} + */ + self._hover_participants_invite_list = function(direction) { + + try { + // Up/down: navigate through results + var chatroom_sel = $('#muji .chatroom'); + var participants_invite_search_sel = chatroom_sel.find('.participants_invite_search'); + var participant_search_one_sel = chatroom_sel.find('.participant_search_one'); + + if(participant_search_one_sel.size()) { + var hover_index = participant_search_one_sel.index($('.hover')); + + // Up (decrement) or down (increment)? + if(direction === 'up') { + hover_index--; + } else { + hover_index++; + } + + if(!hover_index) { + hover_index = 0; + } + + // Nobody before/after? + if(participant_search_one_sel.eq(hover_index).size() === 0) { + if(direction === 'up') { + hover_index = participant_search_one_sel.filter(':last').index(); + } else { + hover_index = 0; + } + } + + // Hover the previous/next user + participant_search_one_sel.removeClass('hover'); + participant_search_one_sel.eq(hover_index).addClass('hover'); + + // Scroll to the hovered user (if out of limits) + participants_invite_search_sel.scrollTo( + participant_search_one_sel.filter('.hover:first'), 0, { margin: true } + ); + } + } catch(e) { + Console.error('Muji._hover_participants_invite_list', e); + } + + }; + + + /** + * Lists the participants in the invite list + * @private + * @return {object} + */ + self._list_participants_invite_list = function() { + + var participants_obj = {}; + + try { + $('#muji .chatroom .participants_invite_list .invite_one').each(function() { + participants_obj[$(this).attr('data-xid')] = 1; + }); + } catch(e) { + Console.error('Muji._list_participants_invite_list', e); + } finally { + return participants_obj; + } + + }; + + + /** + * Adapts the Muji view to the window size + * @private + * @return {undefined} + */ + self._adapt = function() { + + try { + if(self.in_call() && Common.exists('#muji')) { + Call.adapt_local( + $('#muji .local_video') + ); + + Call.adapt_remote( + $('#muji .videoroom') + ); + } + } catch(e) { + Console.error('Muji._adapt', e); + } + + }; + + + /** + * Scrolls down to last received message + * @private + * @return {undefined} + */ + self._message_scroll = function() { + + try { + var chatroom_view_sel = $('#muji .chatroom .chatroom_view'); + + // Scroll down to message + if(chatroom_view_sel.size() && chatroom_view_sel.is(':visible')) { + chatroom_view_sel[0].scrollTop = chatroom_view_sel[0].scrollHeight; + } + } catch(e) { + Console.error('Muji._message_scroll', e); + } + + }; + + + /** + * Notifies that a new message has been received + * @private + * @return {undefined} + */ + self._message_notify = function() { + + try { + // Selectors + var tools_call_sel = $('#top-content .tools.call'); + var notify_sel = tools_call_sel.find('.notify'); + + if(!notify_sel.size()) { + notify_sel = $( + '
0
' + ); + + notify_sel.appendTo(tools_call_sel); + } + + // Count & update + var count_notifications = parseInt((notify_sel.attr('data-counter') || 0), 10); + count_notifications++; + + notify_sel.text(count_notifications); + notify_sel.attr('data-counter', count_notifications); + + // Update general interface + Interface.updateTitle(); + } catch(e) { + Console.error('Muji._message_notify', e); + } + + }; + + + /** + * Removes displayed message notifications + * @private + * @return {undefined} + */ + self._message_unnotify = function() { + + try { + $('#top-content .tools.call .notify').remove(); + + // Update general interface + Interface.updateTitle(); + } catch(e) { + Console.error('Muji._message_unnotify', e); + } + + }; + + + /** + * Receive a Muji call + * @public + * @param {object} args + * @param {object} stanza + * @return {boolean} + */ + self.receive = function(args, stanza) { + + try { + if(!Call.is_ongoing()) { + // Create call session + self._new( + args.jid, + (args.media || JSJAC_JINGLE_MEDIA_VIDEO), + stanza, + args + ); + + // Notify user + Call.notify( + JSJAC_JINGLE_SESSION_MUJI, + args.jid, + ('call_' + (args.media || 'video')), + args.media, + Common.bareXID(args.from) + ); + + Audio.play('incoming-call', true); + } + } catch(e) { + Console.error('Muji.receive', e); + } finally { + return false; + } + + }; + + + /** + * Start a Muji call + * @public + * @param {string} room + * @param {string} mode + * @return {boolean} + */ + self.start = function(room, mode) { + + try { + if(!Call.is_ongoing()) { + self._new(room, mode); + } + } catch(e) { + Console.error('Muji.start', e); + } finally { + return false; + } + + }; + + + /** + * Reset current Muji call + * @public + * @return {boolean} + */ + self._reset = function() { + + try { + // Trash interface + Call.stop_counter(); + Call.stop_session(); + self._destroy_interface(); + $('body').removeClass('in_muji_call'); + + // Clean notifications + self._message_unnotify(); + + // Hack: stop audio in case it is still ringing + Audio.stop('incoming-call'); + Audio.stop('outgoing-call'); + } catch(e) { + Console.error('Muji._reset', e); + } finally { + return false; + } + + }; + + + /** + * Stops current Muji call + * @public + * @return {boolean} + */ + self.stop = function() { + + try { + // Reset interface + self._reset(); + + // Stop Muji session + if(self._session !== null) { + self._session.leave(); + + Console.debug('Stopping current Muji call...'); + } else { + Console.warn('No Muji call to be terminated!'); + } + } catch(e) { + Console.error('Muji.stop', e); + } finally { + return false; + } + + }; + + + /** + * Mutes current Muji call + * @public + * @return {undefined} + */ + self.mute = function() { + + try { + Call.mute( + $('#muji .videoroom .topbar .controls a') + ); + } catch(e) { + Console.error('Muji.mute', e); + } + + }; + + + /** + * Unmutes current Muji call + * @public + * @return {undefined} + */ + self.unmute = function() { + + try { + Call.unmute( + $('#muji .videoroom .topbar .controls a') + ); + } catch(e) { + Console.error('Muji.mute', e); + } + + }; + + + /** + * Checks whether room given is Muji room or not + * @public + * @param {string} room + * @return {boolean} + */ + self.is_room = function(room) { + + is_room = false; + + try { + if(self.in_call() && self._session.get_to()) { + is_room = (room === self._session.get_to()); + } + } catch(e) { + Console.error('Muji.is_room', e); + } finally { + return is_room; + } + + }; + + + /** + * Checks whether room is full or not (over-capacity) + * @public + * @return {boolean} + */ + self.is_full = function() { + + is_full = false; + + try { + if($('#muji .chatroom_participants .participants_full').is(':visible')) { + is_full = true; + } + } catch(e) { + Console.error('Muji.is_full', e); + } finally { + return is_full; + } + + }; + + + /** + * Checks whether user is in call or not + * @public + * @return {boolean} + */ + self.in_call = function() { + + in_call = false; + + try { + if(self._session && + (self._session.get_status() === JSJAC_JINGLE_MUJI_STATUS_PREPARING || + self._session.get_status() === JSJAC_JINGLE_MUJI_STATUS_PREPARED || + self._session.get_status() === JSJAC_JINGLE_MUJI_STATUS_INITIATING || + self._session.get_status() === JSJAC_JINGLE_MUJI_STATUS_INITIATED || + self._session.get_status() === JSJAC_JINGLE_MUJI_STATUS_LEAVING)) { + in_call = true; + } + } catch(e) { + Console.error('Muji.in_call', e); + } finally { + return in_call; + } + + }; + + + /** + * Checks if the given call SID is the same as the current call's one + * @public + * @param {object} + * @return {boolean} + */ + self.is_same_sid = function(muji) { + + try { + return Call.is_same_sid(self._session, muji); + } catch(e) { + Console.error('Muji.is_same_sid', e); + } + + }; + + + /** + * Returns if current Muji call is audio + * @public + * @return {boolean} + */ + self.is_audio = function() { + + try { + return Call.is_audio(self._session); + } catch(e) { + Console.error('Muji.is_audio', e); + } + + }; + + + /** + * Returns if current Muji call is video + * @public + * @return {boolean} + */ + self.is_video = function() { + + try { + return Call.is_video(self._session); + } catch(e) { + Console.error('Muji.is_video', e); + } + + }; + + + /** + * Returns the caller XID + * @public + * @return {string} + */ + self.get_caller_xid = function() { + + try { + return self._caller_xid || Common.getXID(); + } catch(e) { + Console.error('Muji.get_caller_xid', e); + } + + }; + + + /** + * Get the notification map + * @private + * @return {object} + */ + self._notify_map = function() { + + try { + return { + 'call_audio': { + 'text': Common._e("Incoming group call"), + + 'buttons': { + 'accept': { + 'text': Common._e("Accept"), + 'color': 'green', + 'cb': function(xid, mode) { + self._session.join(); + Audio.stop('incoming-call'); + } + }, + + 'decline': { + 'text': Common._e("Decline"), + 'color': 'red', + 'cb': function(xid, mode) { + self._session.abort(); + Audio.stop('incoming-call'); + } + } + } + }, + + 'call_video': { + 'text': Common._e("Incoming group call"), + + 'buttons': { + 'accept': { + 'text': Common._e("Accept"), + 'color': 'green', + 'cb': function(xid, mode) { + self._session.join(); + Audio.stop('incoming-call'); + } + }, + + 'decline': { + 'text': Common._e("Decline"), + 'color': 'red', + 'cb': function(xid, mode) { + self._session.abort(); + Audio.stop('incoming-call'); + } + } + } + }, + + 'preparing': { + 'text': Common._e("Preparing group call..."), + + 'buttons': { + 'cancel': { + 'text': Common._e("Cancel"), + 'color': 'red', + 'cb': function(xid, mode) { + self._session.abort(); + } + } + } + }, + + 'waiting': { + 'text': Common._e("Preparing group call..."), + + 'buttons': { + 'cancel': { + 'text': Common._e("Cancel"), + 'color': 'red', + 'cb': function(xid, mode) { + self._session.leave(); + } + } + } + }, + + 'connecting': { + 'text': Common._e("Connecting to group call..."), + + 'buttons': { + 'cancel': { + 'text': Common._e("Cancel"), + 'color': 'red', + 'cb': function(xid, mode) { + self._session.leave(); + } + } + } + }, + + 'error': { + 'text': Common._e("Group call error"), + + 'buttons': { + 'retry': { + 'text': Common._e("Retry"), + 'color': 'blue', + 'cb': function(xid, mode) { + self.start(xid, mode); + } + }, + + 'cancel': { + 'text': Common._e("Cancel"), + 'color': 'red', + 'cb': function(xid, mode) { + self._reset(); + } + } + } + }, + + 'ending': { + 'text': Common._e("Ending group call...") + }, + + 'ended': { + 'text': Common._e("Group call ended"), + + 'buttons': { + 'okay': { + 'text': Common._e("Okay"), + 'color': 'blue', + 'cb': function(xid, mode) { + self._reset(); + } + } + } + } + }; + } catch(e) { + Console.error('Muji._notify_map', e); + + return {}; + } + + }; + + + /** + * Create the Muji interface + * @public + * @param {string} room + * @param {string} mode + * @return {object} + */ + self._create_interface = function(room, mode) { + + try { + // Jingle interface already exists? + if(Common.exists('#muji')) { + throw 'Muji interface already exist!'; + } + + // Local poster + var local_poster = './images/placeholders/jingle_video_local.png'; + + if(mode === 'audio') { + local_poster = './images/placeholders/jingle_audio_local.png'; + } + + // Create DOM + $('body').append( + '
' + + '
' + + '
' + + '
' + + '' + + + '
00:00:00
' + + + '
' + + '' + + '
' + + '
' + + + '
' + + '' + + '
' + + + '
' + + '
' + + '
' + + '
' + + '
' + + '
' + + '
' + + '
' + + + '
' + + '' + Common._e("Nobody there. Invite some people!") + '' + + '
' + + '
' + + + '
' + + '
' + + '
' + + '' + Common.printf(Common._e("%s participants"), 0) + '' + + '' + Common._e("(full)") + '' + + + '' + + '
' + + + '
' + + '
' + + + '' + + '
' + + '' + + '
' + + + '' + + '' + + '' + + '' + + '' + + + '' + + '
' + + '
' + + + '
' + + + '
' + + '' + + '' + + + '
' + + '' + + '
' + + '
' + + '
' + + '
' + + '
' + ); + + // Apply events + self._events_interface(); + + // Apply user avatar + Avatar.get(xid, 'cache', 'true', 'forget'); + } catch(e) { + Console.error('Muji._create_interface', e); + } finally { + return $('#muji'); + } + + }; + + + /** + * Destroy the Muji interface + * @public + * @return {undefined} + */ + self._destroy_interface = function() { + + try { + Call.destroy_interface( + $('#muji') + ); + } catch(e) { + Console.error('Muji._destroy_interface', e); + } + + }; + + + /** + * Show the Muji interface + * @private + * @return {boolean} + */ + self._show_interface = function() { + + try { + Call.show_interface( + self, + $('#muji'), + $('#muji .videoroom') + ); + + self._message_scroll(); + self._message_unnotify(); + } catch(e) { + Console.error('Muji._show_interface', e); + } finally { + return false; + } + + }; + + + /** + * Hide the Muji interface + * @private + * @return {boolean} + */ + self._hide_interface = function() { + + try { + Call.hide_interface( + $('#muji'), + $('#muji .videoroom') + ); + } catch(e) { + Console.error('Muji._hide_interface', e); + } finally { + return false; + } + + }; + + + /** + * Attaches interface events + * @private + * @return {boolean} + */ + self._events_interface = function() { + + try { + // Common selectors + var muji_chatroom = $('#muji .chatroom'); + var chatroom_form = muji_chatroom.find('form.chatroom_form'); + var chatroom_participants = muji_chatroom.find('.chatroom_participants'); + var participants_invite = chatroom_participants.find('.participants_default_view .participants_invite'); + var participants_invite_box = chatroom_participants.find('.participants_invite_box'); + var participants_invite_list = participants_invite_box.find('.participants_invite_list'); + var participants_invite_form = participants_invite_box.find('.participants_invite_form'); + var participants_invite_input = participants_invite_form.find('input[name="xid"]'); + var participants_invite_validate = participants_invite_form.find('.invite_validate'); + var participants_invite_search = participants_invite_box.find('.participants_invite_search'); + + // Apply events + Call.events_interface( + self, + $('#muji'), + $('#muji .videoroom') + ); + + // People invite event + participants_invite.click(function() { + try { + if(!participants_invite_box.is(':animated')) { + if(participants_invite_box.is(':hidden')) { + participants_invite_box.stop(true).slideDown(250, function() { + participants_invite_input.focus(); + }); + } else { + participants_invite_input.blur(); + participants_invite_box.stop(true).slideUp(250, function() { + // Reset everything + participants_invite_list.empty().hide(); + participants_invite_validate.hide(); + participants_invite_input.val(''); + self._reset_participants_invite_filter(); + }); + } + } + } catch(_e) { + Console.error('Muji._show_interface[event]', _e); + } finally { + return false; + } + }); + + // Invite input key events + participants_invite_input.keydown(function(e) { + try { + if(e.keyCode == 9) { + self._hover_participants_invite_list('down'); + + return false; + } + } catch(_e) { + Console.error('Muji._show_interface[event]', _e); + } + }); + + participants_invite_input.keyup(function(e) { + try { + var this_sel = $(this); + + if(e.keyCode == 27) { + // Escape: close interface + if(!this_sel.val().trim()) { + participants_invite.click(); + } else { + self._reset_participants_invite_filter(); + } + + return false; + } else if(e.keyCode == 9) { + // Tabulate: skip there (see keydown above) + return false; + } else if(e.keyCode == 38 || e.keyCode == 40) { + var direction = (e.keyCode == 38) ? 'up' : 'down'; + self._hover_participants_invite_list(direction); + + return false; + } else { + // Other keys: assume something has been typed + self._engage_participants_invite_filter( + this_sel.val() + ); + } + } catch(_e) { + Console.error('Muji._show_interface[event]', _e); + } + }); + + // Input auto-focus + chatroom_form.click(function() { + chatroom_form.find('input[name="message"]').focus(); + }); + + // Invite form send event + participants_invite_form.submit(function() { + try { + if(participants_invite_search.find('.participant_search_one.hover').size()) { + // Add the hovered user + var participant_search_one_hover_sel = participants_invite_search.find('.participant_search_one.hover:first'); + + if(participant_search_one_hover_sel.size() >= 1) { + participant_search_one_hover_sel.click(); + + return false; + } + } else { + var invite_arr = Object.keys(self._list_participants_invite_list()); + + if(invite_arr && invite_arr.length) { + self._send_participants_invite_list(invite_arr); + } + + participants_invite.click(); + } + } catch(_e) { + Console.error('Muji._show_interface[event]', _e); + } finally { + return false; + } + }); + + // Invite form validate event + participants_invite_validate.find('.invite_go').click(function() { + try { + participants_invite_form.submit(); + } catch(_e) { + Console.error('Muji._show_interface[event]', _e); + } finally { + return false; + } + }); + + // Message send event + chatroom_form.submit(function() { + try { + if(self._session === null) { + throw 'Muji session unavailable'; + } + + var input_sel = $(this).find('input[name="message"]'); + var body = input_sel.val(); + + if(body) { + self._session.send_message(body); + input_sel.val(''); + } + } catch(_e) { + Console.error('Muji._show_interface[event]', _e); + } finally { + return false; + } + }); + } catch(e) { + Console.error('Muji._events_interface', e); + } finally { + return false; + } + + }; + + + /** + * Plugin launcher + * @public + * @return {undefined} + */ + self.launch = function() { + + try { + $(window).resize(self._adapt()); + } catch(e) { + Console.error('Muji.launch', e); + } + + }; + + + /** + * Return class scope + */ + return self; + +})(); + +Muji.launch(); \ No newline at end of file diff --git a/source/app/javascripts/music.js b/source/app/javascripts/music.js index 923ba12..3b0e2d7 100644 --- a/source/app/javascripts/music.js +++ b/source/app/javascripts/music.js @@ -63,34 +63,30 @@ var Music = (function () { if(!Common.exists(path_type)) { var code = '
'; - if(type == 'local') + if(type == 'local') { $(content).prepend(code); - else + } else { $(content).append(code); + } } // Fill the results $(xml).find('track').each(function() { // Parse the XML - var id = $(this).find('id').text(); - var title = $(this).find('name').text(); - var artist = $(this).find('artist').text(); - var source = $(this).find('source').text(); - var duration = $(this).find('duration').text(); - var uri = $(this).find('url').text(); - var mime = $(this).find('type').text(); - - // No ID? - if(!id) - id = hex_md5(uri); - - // No MIME? - if(!mime) - mime = 'audio/ogg'; + var this_sel = $(this); + + var id = this_sel.find('id').text() || hex_md5(uri); + var title = this_sel.find('name').text(); + var artist = this_sel.find('artist').text(); + var source = this_sel.find('source').text(); + var duration = this_sel.find('duration').text(); + var uri = this_sel.find('url').text(); + var mime = this_sel.find('type').text() || 'audio/ogg'; // Local URL? - if(type == 'local') + if(type == 'local') { uri = Utils.generateURL(uri); + } // Append the HTML code $(path_type).append('' + title + ''); @@ -98,8 +94,9 @@ var Music = (function () { // Current playing song? var current_song = $(path_type + ' a[data-id="' + id + '"]'); - if(Common.exists('.music-audio[data-id="' + id + '"]')) + if(Common.exists('.music-audio[data-id="' + id + '"]')) { current_song.addClass('playing'); + } // Click event current_song.click(function() { @@ -117,12 +114,14 @@ var Music = (function () { $(path + 'input').val('').removeAttr('disabled'); // No result - if(!jamendo && !local) + if(!jamendo && !local) { $(path + '.no-results').show(); + } // We must put a separator between the categories - if(jamendo && local) + if(jamendo && local) { $(content + ' .local').addClass('special'); + } } } catch(e) { Console.error('Music.parse', e); @@ -157,7 +156,10 @@ var Music = (function () { }); // Get the local results - $.get('./server/music-search.php', {searchquery: string, location: JAPPIX_LOCATION}, function(data) { + $.get('./server/music-search.php', { + searchquery: string, + location: JAPPIX_LOCATION + }, function(data) { self.parse(data, 'local'); }); } catch(e) { @@ -177,33 +179,35 @@ var Music = (function () { try { // Initialize - var playThis = document.getElementById('top-content').getElementsByTagName('audio')[0]; + var audio_sel = document.getElementById('top-content').getElementsByTagName('audio')[0]; // Nothing to play, exit - if(!playThis) + if(!audio_sel) { return false; + } var stopButton = $('#top-content a.stop'); // User play a song if(action == 'play') { stopButton.show(); - playThis.load(); - playThis.play(); - playThis.addEventListener('ended', function() { + audio_sel.load(); + audio_sel.play(); + + audio_sel.addEventListener('ended', function() { self.action('stop'); }, true); Console.log('Music is now playing.'); - } - - // User stop the song or the song came to its end - else if(action == 'stop') { + } else if(action == 'stop') { + // User stop the song / end of song stopButton.hide(); - playThis.pause(); + audio_sel.pause(); + $('#top-content .music').removeClass('actived'); $('.music-content .list a').removeClass('playing'); $('.music-audio').remove(); + self.publish(); Console.log('Music is now stopped.'); @@ -245,27 +249,24 @@ var Music = (function () { // Enough data? if(title || artist || source || uri) { - // Data array - var nodes = new Array( - 'title', - 'artist', - 'source', - 'length', - 'uri' - ); - - var values = new Array( - title, - artist, - source, - length, - uri - ); + var music_data = { + 'title': title, + 'artist': artist, + 'source': source, + 'length': length, + 'uri': uri + }; // Create the children nodes - for(var i in nodes) { - if(values[i]) { - tune.appendChild(iq.buildNode(nodes[i], {'xmlns': NS_TUNE}, values[i])); + var cur_value; + + for(var cur_name in music_data) { + cur_value = music_data[cur_name]; + + if(cur_value) { + tune.appendChild(iq.buildNode(cur_name, { + 'xmlns': NS_TUNE + }, cur_value)); } } } @@ -298,16 +299,18 @@ var Music = (function () { try { var path = '.music-content '; + var music_audio_sel = $('.music-audio'); // We remove & create a new audio tag - $('.music-audio').remove(); + music_audio_sel.remove(); $(path + '.player').prepend('
'; // Add the HTML code - if(inverse) + if(inverse) { $('.notifications-content .nothing').before(code); - else + } else { $('.notifications-content .empty').after(code); + } // Play a sound to alert the user Audio.play('notification'); @@ -296,8 +297,9 @@ var Notification = (function () { $('.' + id + ' a.yes').click(function() { self.action(type, data, 'yes', id); - if(($(this).attr('href') == '#') && ($(this).attr('target') != '_blank')) + if(($(this).attr('href') == '#') && ($(this).attr('target') != '_blank')) { return false; + } }); // The no click function @@ -334,28 +336,23 @@ var Notification = (function () { try { // We launch a function depending of the type - if((type == 'subscribe') && (value == 'yes')) + if((type == 'subscribe') && (value == 'yes')) { Presence.acceptSubscribe(data[0], data[1]); - - else if((type == 'subscribe') && (value == 'no')) + } else if((type == 'subscribe') && (value == 'no')) { Presence.sendSubscribe(data[0], 'unsubscribed'); - - else if((type == 'invite_room') && (value == 'yes')) + } else if((type == 'invite_room') && (value == 'yes')) { Chat.checkCreate(data[0], 'groupchat'); - - else if(type == 'request') + } else if(type == 'request') { HTTPReply.go(value, data[0]); + } - if((type == 'send') && (value == 'yes')) + if((type == 'send') && (value == 'yes')) { OOB.reply(data[0], data[3], 'accept', data[2], data[4]); - - else if((type == 'send') && (value == 'no')) + } else if((type == 'send') && (value == 'no')) { OOB.reply(data[0], data[3], 'reject', data[2], data[4]); - - else if((type == 'rosterx') && (value == 'yes')) + } else if((type == 'rosterx') && (value == 'yes')) { RosterX.open(data[0]); - - else if((type == 'comment') || (type == 'like') || (type == 'quote') || (type == 'wall') || (type == 'photo') || (type == 'video')) { + } else if((type == 'comment') || (type == 'like') || (type == 'quote') || (type == 'wall') || (type == 'photo') || (type == 'video')) { if(value == 'yes') { // Get the microblog item Microblog.fromInfos(data[2]); @@ -452,25 +449,29 @@ var Notification = (function () { // Should we inverse? var inverse = true; - if(items.size() == 1) + if(items.size() == 1) { inverse = false; + } // Parse notifications items.each(function() { + var this_sel = $(this); + // Parse the current item - var current_item = $(this).attr('id'); - var current_type = $(this).find('link[rel="via"]:first').attr('title'); - var current_href = $(this).find('link[rel="via"]:first').attr('href'); - var current_parent_href = $(this).find('link[rel="related"]:first').attr('href'); - var current_xid = Common.explodeThis(':', $(this).find('author uri').text(), 1); - var current_name = $(this).find('author name').text(); - var current_text = $(this).find('content[type="text"]:first').text(); + var current_item = this_sel.attr('id'); + var current_type = this_sel.find('link[rel="via"]:first').attr('title'); + var current_href = this_sel.find('link[rel="via"]:first').attr('href'); + var current_parent_href = this_sel.find('link[rel="related"]:first').attr('href'); + var current_xid = Common.explodeThis(':', this_sel.find('author uri').text(), 1); + var current_name = this_sel.find('author name').text(); + var current_text = this_sel.find('content[type="text"]:first').text(); var current_bname = Name.getBuddy(current_xid); var current_id = hex_md5(current_type + current_xid + current_href + current_text); // Choose the good name! - if(!current_name || (current_bname != Common.getXIDNick(current_xid))) + if(!current_name || (current_bname != Common.getXIDNick(current_xid))) { current_name = current_bname; + } // Create it! self.create(current_type, current_xid, [current_name, current_href, current_parent_href, current_item], current_text, current_id, inverse); diff --git a/source/app/javascripts/oob.js b/source/app/javascripts/oob.js index 8421ec3..2e7988e 100644 --- a/source/app/javascripts/oob.js +++ b/source/app/javascripts/oob.js @@ -39,8 +39,9 @@ var OOB = (function () { to = Caps.getFeatureResource(to, NS_IQOOB); // IQs cannot be sent to offline users - if(!to) + if(!to) { return; + } // Register the ID DataStore.setDB(Connection.desktop_hash, 'send/url', id, url); @@ -92,19 +93,15 @@ var OOB = (function () { self.handle = function(from, id, type, node) { try { - var xid = ''; - var url = ''; - var desc = ''; + var xid = '', url = '', desc = ''; - // IQ stanza? if(type == 'iq') { + // IQ stanza xid = Common.fullXID(from); url = $(node).find('url').text(); desc = $(node).find('desc').text(); - } - - // Message stanza? - else { + } else { + // Message stanza xid = Common.bareXID(from); url = $(node).find('url').text(); desc = $(node).find('body').text(); @@ -140,8 +137,9 @@ var OOB = (function () { try { // Not IQ type? - if(type != 'iq') + if(type != 'iq') { return; + } // New IQ var aIQ = new JSJaCIQ(); @@ -160,11 +158,17 @@ var OOB = (function () { aIQ.setType('error'); // Append stanza content - for(var i = 0; i < node.childNodes.length; i++) + for(var i = 0; i < node.childNodes.length; i++) { aIQ.getNode().appendChild(node.childNodes.item(i).cloneNode(true)); + } // Append error content - var aError = aIQ.appendNode('error', {'xmlns': NS_CLIENT, 'code': '406', 'type': 'modify'}); + var aError = aIQ.appendNode('error', { + 'xmlns': NS_CLIENT, + 'code': '406', + 'type': 'modify' + }); + aError.appendChild(aIQ.buildNode('not-acceptable', {'xmlns': NS_STANZAS})); Console.info('Rejected file request from: ' + to); @@ -187,11 +191,16 @@ var OOB = (function () { try { // Append the wait icon - $('#page-engine .chat-tools-file:not(.mini) .tooltip-subitem *').hide(); - $('#page-engine .chat-tools-file:not(.mini) .tooltip-subitem').append('
'); + var chat_tools_file_sel = page_engine_sel.find('.chat-tools-file:not(.mini)'); + var subitem_sel = chat_tools_file_sel.find('.tooltip-subitem'); + + subitem_sel.find('*').hide(); + subitem_sel.append( + '
' + ); // Lock the bubble - $('#page-engine .chat-tools-file:not(.mini)').addClass('mini'); + chat_tools_file_sel.addClass('mini'); } catch(e) { Console.error('OOB.waitUpload', e); } @@ -208,6 +217,8 @@ var OOB = (function () { self.handleUpload = function(responseXML) { try { + var page_engine_sel = $('#page-engine'); + // Data selector var dData = $(responseXML).find('jappix'); @@ -220,25 +231,27 @@ var OOB = (function () { var oob_has; // No ID provided? - if(!fID) + if(!fID) { oob_has = ':has(.wait)'; - else + } else { oob_has = ':has(#oob-upload input[value="' + fID + '"])'; + } - var xid = $('#page-engine .page-engine-chan' + oob_has).attr('data-xid'); - var oob_type = $('#page-engine .chat-tools-file' + oob_has).attr('data-oob'); + var xid = page_engine_sel.find('.page-engine-chan' + oob_has).attr('data-xid'); + var oob_type = page_engine_sel.find('.chat-tools-file' + oob_has).attr('data-oob'); // Reset the file send tool - $('#page-engine .chat-tools-file' + oob_has).removeClass('mini'); - $('#page-engine .bubble-file' + oob_has).remove(); + page_engine_sel.find('.chat-tools-file' + oob_has).removeClass('mini'); + page_engine_sel.find('.bubble-file' + oob_has).remove(); // Not available? - if($('#page-engine .chat-tools-file' + oob_has).is(':hidden') && (oob_type == 'iq')) { + if(page_engine_sel.find('.chat-tools-file' + oob_has).is(':hidden') && (oob_type == 'iq')) { Board.openThisError(4); // Remove the file we sent - if(fURL) + if(fURL) { $.get(fURL + '&action=remove'); + } } // Everything okay? @@ -250,10 +263,7 @@ var OOB = (function () { Notification.create('send_pending', xid, [xid, fURL, oob_type, '', ''], fDesc, hex_md5(fURL + fDesc + fID)); Console.info('File request sent.'); - } - - // Upload error? - else { + } else { Board.openThisError(4); Console.error('Error while sending the file', dData.find('error').text()); diff --git a/source/app/javascripts/options.js b/source/app/javascripts/options.js index ee63d2c..e050f78 100644 --- a/source/app/javascripts/options.js +++ b/source/app/javascripts/options.js @@ -275,11 +275,13 @@ var Options = (function () { self.switchTab = function(id) { try { - $('#options .one-lap').hide(); - $('#options #conf' + id).show(); - $('#options .tab a').removeClass('tab-active'); - $('#options .tab a[data-key="' + id + '"]').addClass('tab-active'); - $('#options .sub-ask .sub-ask-close').click(); + var options_sel = $('#options'); + + options_sel.find('.one-lap').hide(); + options_sel.find('#conf' + id).show(); + options_sel.find('.tab a').removeClass('tab-active'); + options_sel.find('.tab a[data-key="' + id + '"]').addClass('tab-active'); + options_sel.find('.sub-ask .sub-ask-close').click(); } catch(e) { Console.error('Options.switchTab', e); } finally { @@ -298,15 +300,16 @@ var Options = (function () { self.wait = function(id) { try { - var sOptions = $('#options .content'); + var options_sel = $('#options'); + var content_sel = options_sel.find('.content'); // Remove the current item class - sOptions.removeClass(id); + content_sel.removeClass(id); // Hide the waiting items if all was received - if(!sOptions.hasClass('microblog') && !sOptions.hasClass('mam')) { - $('#options .wait').hide(); - $('#options .finish:first').removeClass('disabled'); + if(!content_sel.hasClass('microblog') && !content_sel.hasClass('mam')) { + options_sel.find('.wait').hide(); + options_sel.find('.finish:first').removeClass('disabled'); } } catch(e) { Console.error('Options.wait', e); @@ -533,22 +536,26 @@ var Options = (function () { $('.sub-ask-pass input').each(function() { var select = $(this); - if(!select.val()) + if(!select.val()) { $(document).oneTime(10, function() { select.addClass('please-complete').focus(); }); - else + } else { select.removeClass('please-complete'); + } }); - if(password0 != Utils.getPassword()) + if(password0 != Utils.getPassword()) { $(document).oneTime(10, function() { $('#options .old').addClass('please-complete').focus(); }); - if(password1 != password2) + } + + if(password1 != password2) { $(document).oneTime(10, function() { $('#options .new1, #options .new2').addClass('please-complete').focus(); }); + } } } catch(e) { Console.error('Options.sendNewPassword', e); @@ -612,12 +619,13 @@ var Options = (function () { } else { var selector = $('#options .check-mam'); - if(password != Utils.getPassword()) + if(password != Utils.getPassword()) { $(document).oneTime(10, function() { selector.addClass('please-complete').focus(); }); - else + } else { selector.removeClass('please-complete'); + } } } catch(e) { Console.error('Options.purgeMyArchives', e); @@ -659,12 +667,13 @@ var Options = (function () { } else { var selector = $('#options .check-empty'); - if(password != Utils.getPassword()) + if(password != Utils.getPassword()) { $(document).oneTime(10, function() { selector.addClass('please-complete').focus(); }); - else + } else { selector.removeClass('please-complete'); + } } } catch(e) { Console.error('Options.purgeMyMicroblog', e); @@ -726,17 +735,16 @@ var Options = (function () { con.send(iq, self.handleAccDeletion); Console.info('Delete account sent.'); - } - - else { + } else { var selector = $('#options .check-password'); - if(password != Utils.getPassword()) + if(password != Utils.getPassword()) { $(document).oneTime(10, function() { selector.addClass('please-complete').focus(); }); - else + } else { selector.removeClass('please-complete'); + } } } catch(e) { Console.error('Options.deleteMyAccount', e); @@ -781,50 +789,58 @@ var Options = (function () { } // We show the "privacy" form if something is visible into it - if(enabled_mam || enabled_pep) + if(enabled_mam || enabled_pep) { $('#options fieldset.privacy').show(); + } // We get the values of the forms for the sounds - if(DataStore.getDB(Connection.desktop_hash, 'options', 'sounds') == '0') + if(DataStore.getDB(Connection.desktop_hash, 'options', 'sounds') == '0') { $('#sounds').attr('checked', false); - else + } else { $('#sounds').attr('checked', true); + } // We get the values of the forms for the geolocation - if(DataStore.getDB(Connection.desktop_hash, 'options', 'geolocation') == '1') + if(DataStore.getDB(Connection.desktop_hash, 'options', 'geolocation') == '1') { $('#geolocation').attr('checked', true); - else + } else { $('#geolocation').attr('checked', false); + } // We get the values of the forms for the roster show all - if(DataStore.getDB(Connection.desktop_hash, 'options', 'roster-showall') == '1') + if(DataStore.getDB(Connection.desktop_hash, 'options', 'roster-showall') == '1') { $('#showall').attr('checked', true); - else + } else { $('#showall').attr('checked', false); + } // We get the values of the forms for the XHTML-IM images filter - if(DataStore.getDB(Connection.desktop_hash, 'options', 'no-xhtml-images') == '1') + if(DataStore.getDB(Connection.desktop_hash, 'options', 'no-xhtml-images') == '1') { $('#noxhtmlimg').attr('checked', true); - else + } else { $('#noxhtmlimg').attr('checked', false); + } // We get the values of the forms for the integratemedias - if(DataStore.getDB(Connection.desktop_hash, 'options', 'integratemedias') == '0') + if(DataStore.getDB(Connection.desktop_hash, 'options', 'integratemedias') == '0') { $('#integratemedias').attr('checked', false); - else + } else { $('#integratemedias').attr('checked', true); + } // We get the values of the forms for the groupchatpresence - if(DataStore.getDB(Connection.desktop_hash, 'options', 'groupchatpresence') == '0') + if(DataStore.getDB(Connection.desktop_hash, 'options', 'groupchatpresence') == '0') { $('#groupchatpresence').attr('checked', false); - else + } else { $('#groupchatpresence').attr('checked', true); + } // We get the values of the forms for the localarchives - if(DataStore.getDB(Connection.desktop_hash, 'options', 'localarchives') == '0') + if(DataStore.getDB(Connection.desktop_hash, 'options', 'localarchives') == '0') { $('#localarchives').attr('checked', false); - else + } else { $('#localarchives').attr('checked', true); + } } catch(e) { Console.error('Options.load', e); } @@ -939,10 +955,13 @@ var Options = (function () { }); $('#options .bottom .finish').click(function() { - if($(this).is('.save') && !$(this).hasClass('disabled')) + if($(this).is('.save') && !$(this).hasClass('disabled')) { return self.save(); - if($(this).is('.cancel')) + } + + if($(this).is('.cancel')) { return self.close(); + } return false; }); @@ -951,20 +970,24 @@ var Options = (function () { $('#options .sub-ask input').keyup(function(e) { if(e.keyCode == 13) { // Archives purge - if($(this).is('.purge-archives')) + if($(this).is('.purge-archives')) { return self.purgeMyArchives(); + } // Microblog purge - else if($(this).is('.purge-microblog')) + else if($(this).is('.purge-microblog')) { return self.purgeMyMicroblog(); + } // Password change - else if($(this).is('.password-change')) + else if($(this).is('.password-change')) { return self.sendNewPassword(); + } // Account deletion - else if($(this).is('.delete-account')) + else if($(this).is('.delete-account')) { return self.deleteMyAccount(); + } } }); diff --git a/source/app/javascripts/pep.js b/source/app/javascripts/pep.js index e75bde5..bf7c645 100644 --- a/source/app/javascripts/pep.js +++ b/source/app/javascripts/pep.js @@ -20,6 +20,618 @@ var PEP = (function () { var self = {}; + /** + * Generates display object + * @private + * @return {object} + */ + self._generateDisplayObject = function() { + + var display_object = { + 'pep_value': '', + 'pep_text': '', + 'style_value': '', + 'style_text': '', + 'display_text': '', + 'final_link': '', + 'final_uri': '' + }; + + try { + // Nothing to do there + } catch(e) { + Console.error('PEP._generateDisplayObject', e); + } finally { + return display_object; + } + + }; + + + /** + * Abstracts mood and activity display helpers + * @private + * @param {object} node_sel + * @param {function} icon_fn + * @return {object} + */ + self._abstractDisplayMoodActivity = function(node_sel, icon_fn) { + + var display_args = self._generateDisplayObject(); + + try { + if(node_sel) { + display_args.pep_value = node_sel.find('value').text() || 'none'; + display_args.pep_text = node_sel.find('text').text(); + + display_args.style_value = icon_fn(display_args.pep_value); + display_args.style_text = display_args.pep_text ? display_args.pep_text : Common._e("unknown"); + } else { + display_args.style_value = icon_fn('undefined'); + display_args.style_text = Common._e("unknown"); + } + + display_args.display_text = display_args.style_text; + display_args.style_text = display_args.style_text.htmlEnc(); + } catch(e) { + Console.error('PEP._abstractDisplayMoodActivity', e); + } finally { + return display_args; + } + + }; + + + /** + * Displays PEP mood + * @private + * @param {object} node_sel + * @return {object} + */ + self._displayMood = function(node_sel) { + + var mood_args = self._abstractDisplayMoodActivity( + node_sel, + self.moodIcon + ); + + try { + // Nothing to do there + } catch(e) { + Console.error('PEP._displayMood', e); + } finally { + return mood_args; + } + + }; + + + /** + * Displays PEP activity + * @private + * @param {object} node_sel + * @return {object} + */ + self._displayActivity = function(node_sel) { + + var activity_args = self._abstractDisplayMoodActivity( + node_sel, + self.activityIcon + ); + + try { + // Nothing to do there + } catch(e) { + Console.error('PEP._displayActivity', e); + } finally { + return activity_args; + } + + }; + + + /** + * Displays PEP tune + * @private + * @param {object} node_sel + * @return {object} + */ + self._displayTune = function(node_sel) { + + var tune_args = self._generateDisplayObject(); + + try { + tune_args.style_value = 'tune-note'; + + if(node_sel) { + // Parse the tune XML + var tune_artist = node_sel.find('artist').text(); + var tune_title = node_sel.find('title').text(); + var tune_album = node_sel.find('album').text(); + var tune_uri = node_sel.find('uri').text(); + + // Apply the good values + if(!tune_artist && !tune_album && !tune_title) { + tune_args.style_text = Common._e("unknown"); + tune_args.display_text = tune_args.style_text; + } else { + tune_args.final_uri = tune_uri || + 'http://grooveshark.com/search?q=' + encodeURIComponent(tune_artist + ' ' + tune_title + ' ' + tune_album); + + var final_artist = tune_artist || Common._e("unknown"); + var final_title = tune_title || Common._e("unknown"); + var final_album = tune_album || Common._e("unknown"); + + tune_args.final_link = ' href="' + tune_args.final_uri + '" target="_blank"'; + + // Generate the text to be displayed + tune_args.display_text = final_artist + ' - ' + final_title + ' (' + final_album + ')'; + tune_args.style_text = '' + tune_args.display_text + ''; + } + } else { + tune_args.style_text = Common._e("unknown"); + tune_args.display_text = tune_args.style_text; + } + } catch(e) { + Console.error('PEP._displayTune', e); + } finally { + return tune_args; + } + + }; + + + /** + * Displays PEP geolocation + * @private + * @param {object} node_sel + * @return {object} + */ + self._displayGeolocation = function(node_sel) { + + var geolocation_args = self._generateDisplayObject(); + + try { + geolocation_args.style_value = 'location-world'; + + if(node_sel) { + geolocation_args.geoloc_lat = node_sel.find('lat').text(); + geolocation_args.geoloc_lon = node_sel.find('lon').text(); + geolocation_args.geoloc_human = node_sel.find('human').text() || + Common._e("See his/her position on the globe"); + geolocation_args.geoloc_real = geolocation_args.geoloc_human; + + // Text to be displayed + if(geolocation_args.geoloc_lat && geolocation_args.geoloc_lon) { + geolocation_args.final_uri = 'http://maps.google.com/?q=' + Common.encodeQuotes(geolocation_args.geoloc_lat) + ',' + Common.encodeQuotes(geolocation_args.geoloc_lon); + geolocation_args.final_link = ' href="' + geolocation_args.final_uri + '" target="_blank"'; + + geolocation_args.style_text = '' + + geolocation_args.geoloc_human.htmlEnc() + + ''; + geolocation_args.display_text = geolocation_args.geoloc_real || + (geolocation_args.geoloc_lat + '; ' + geolocation_args.geoloc_lon); + } else { + geolocation_args.style_text = Common._e("unknown"); + geolocation_args.display_text = geolocation_args.style_text; + } + } else { + geolocation_args.style_text = Common._e("unknown"); + geolocation_args.display_text = geolocation_args.style_text; + } + } catch(e) { + Console.error('PEP._displayGeolocation', e); + } finally { + return geolocation_args; + } + + }; + + + /** + * Add foreign display object to DOM + * @private + * @param {string} xid + * @param {string} hash + * @param {string} type + * @param {object} display_args + * @return {undefined} + */ + self._appendForeignDisplayObject = function(xid, hash, type, display_args) { + + try { + var this_buddy = '#roster .buddy[data-xid="' + escape(xid) + '"]'; + + if(Common.exists(this_buddy)) { + $(this_buddy + ' .bi-' + type).replaceWith( + '

' + display_args.style_text + '

' + ); + } + + // Apply the text to the buddy chat + if(Common.exists('#' + hash)) { + // Selector + var bc_pep = $('#' + hash + ' .bc-pep'); + + // We remove the old PEP item + bc_pep.find('a.bi-' + type).remove(); + + // If the new PEP item is not null, create a new one + if(display_args.style_text != Common._e("unknown")) { + bc_pep.prepend( + '' + ); + } + + // Process the new status position + Presence.adaptChat(hash); + } + } catch(e) { + Console.error('PEP._appendOwnDisplayObject', e); + } + + }; + + + /** + * Add own display object to DOM + * @private + * @param {string} type + * @param {object} display_args + * @return {undefined} + */ + self._appendOwnDisplayObject = function(type, display_args) { + + try { + // Change the icon/value of the target element + if((type == 'mood') || (type == 'activity')) { + // Change the input value + var display_value = ''; + var display_attribute = display_args.pep_value; + + // Must apply default values? + if(display_args.pep_value == 'none') { + if(type == 'mood') { + display_attribute = 'happy'; + } else { + display_attribute = 'exercising'; + } + } + + // No text? + if(display_args.display_text != Common._e("unknown")) { + display_value = display_args.display_text; + } + + // Store this user event in our database + DataStore.setDB(Connection.desktop_hash, type + '-value', 1, display_attribute); + DataStore.setDB(Connection.desktop_hash, type + '-text', 1, display_value); + + // Apply this PEP event + $('#my-infos .f-' + type + ' a.picker').attr('data-value', display_attribute); + $('#my-infos .f-' + type + ' input').val(display_value); + $('#my-infos .f-' + type + ' input').placeholder(); + } else if((type == 'tune') || (type == 'geoloc')) { + // Reset the values + $('#my-infos .f-others a.' + type).remove(); + + // Not empty? + if(display_args.display_text != Common._e("unknown")) { + // Specific stuffs + var href, title, icon_class; + + if(type == 'tune') { + href = display_args.final_uri; + title = display_args.display_text; + icon_class = 'tune-note'; + } else { + href = 'http://maps.google.com/?q=' + Common.encodeQuotes(display_args.geoloc_lat) + ',' + Common.encodeQuotes(display_args.geoloc_lon); + title = Common._e("Where are you?") + ' (' + display_args.display_text + ')'; + icon_class = 'location-world'; + } + + // Must create the container? + if(!Common.exists('#my-infos .f-others')) { + $('#my-infos .content').append('
'); + } + + // Create the element + $('#my-infos .f-others').prepend( + '' + + '' + + '' + ); + } + + // Empty? + else if(!Common.exists('#my-infos .f-others a.icon')) { + $('#my-infos .f-others').remove(); + } + + // Process the roster height again + Roster.adapt(); + } + } catch(e) { + Console.error('PEP._appendOwnDisplayObject', e); + } + + }; + + + /** + * Generates storage data + * @private + * @param {object} args + * @return {string} + */ + self._generateStore = function(args) { + + var storage_data = ''; + + try { + var cur_value; + + for(var cur_arg in args) { + storage_data += '<' + cur_arg + '>' + + (args[cur_arg] || '').htmlEnc() + + ''; + } + } catch(e) { + Console.error('PEP._generateStore', e); + } finally { + return storage_data; + } + + }; + + + /** + * Proceeds mood picker event callback + * @private + * @param {object} picker_sel + * @return {boolean} + */ + self._callbackMoodPicker = function(picker_sel) { + + try { + // Initialize some vars + var path = '#my-infos .f-mood div.bubble'; + var mood_val = picker_sel.attr('data-value'); + + var moods_obj = { + 'crazy': Common._e("Crazy"), + 'excited': Common._e("Excited"), + 'playful': Common._e("Playful"), + 'happy': Common._e("Happy"), + 'shocked': Common._e("Shocked"), + 'hot': Common._e("Hot"), + 'sad': Common._e("Sad"), + 'amorous': Common._e("Amorous"), + 'confident': Common._e("Confident") + }; + + // Yet displayed? + var can_append = !Common.exists(path); + + // Add this bubble! + Bubble.show(path); + + if(!can_append) { + return false; + } + + // Generate the HTML code + var html = '
'; + + for(var cur_mood_name in moods_obj) { + // Yet in use: no need to display it! + if(cur_mood_name == mood_val) { + continue; + } + + html += ''; + } + + html += '
'; + + // Append the HTML code + $('#my-infos .f-mood').append(html); + + // Click event + $(path + ' a').click(function() { + // Update the mood marker + picker_sel.attr( + 'data-value', + $(this).attr('data-value') + ); + + // Close the bubble + Bubble.close(); + + // Focus on the status input + $(document).oneTime(10, function() { + $('#mood-text').focus(); + }); + + return; + }); + } catch(e) { + Console.error('PEP._callbackMoodPicker', e); + } finally { + return false; + } + + }; + + + /** + * Proceeds activity picker event callback + * @private + * @param {object} picker_sel + * @return {boolean} + */ + self._callbackActivityPicker = function(picker_sel) { + + try { + // Initialize some vars + var path = '#my-infos .f-activity div.bubble'; + var activity_val = picker_sel.attr('data-value'); + + var activities_obj = { + 'doing_chores': Common._e("Chores"), + 'drinking': Common._e("Drinking"), + 'eating': Common._e("Eating"), + 'exercising': Common._e("Exercising"), + 'grooming': Common._e("Grooming"), + 'having_appointment': Common._e("Appointment"), + 'inactive': Common._e("Inactive"), + 'relaxing': Common._e("Relaxing"), + 'talking': Common._e("Talking"), + 'traveling': Common._e("Traveling"), + 'working': Common._e("Working") + }; + + var can_append = !Common.exists(path); + + // Add this bubble! + Bubble.show(path); + + if(!can_append) { + return false; + } + + // Generate the HTML code + var html = '
'; + + for(var cur_activity_name in activities_obj) { + // Yet in use: no need to display it! + if(cur_activity_name == activity_val) { + continue; + } + + html += ''; + } + + html += '
'; + + // Append the HTML code + $('#my-infos .f-activity').append(html); + + // Click event + $(path + ' a').click(function() { + // Update the activity marker + picker_sel.attr('data-value', $(this).attr('data-value')); + + // Close the bubble + Bubble.close(); + + // Focus on the status input + $(document).oneTime(10, function() { + $('#activity-text').focus(); + }); + + return false; + }); + } catch(e) { + Console.error('PEP._callbackActivityPicker', e); + } finally { + return false; + } + + }; + + + /** + * Attaches common text events + * @private + * @param {string} name + * @param {object} element_text_sel + * @param {function} send_fn + * @return {undefined} + */ + self._eventsCommonText = function(name, element_text_sel, send_fn) { + + try { + // Submit events + element_text_sel.placeholder(); + element_text_sel.keyup(function(e) { + if(e.keyCode == 13) { + $(this).blur(); + + return false; + } + }); + + // Input blur handler + element_text_sel.blur(function() { + // Read the parameters + var value = $('#my-infos .f-' + name + ' a.picker').attr('data-value'); + var text = $(this).val(); + + // Must send? + if((value != DataStore.getDB(Connection.desktop_hash, name + '-value', 1)) || (text != DataStore.getDB(Connection.desktop_hash, name + '-text', 1))) { + // Update the local stored values + DataStore.setDB(Connection.desktop_hash, name + '-value', 1, value); + DataStore.setDB(Connection.desktop_hash, name + '-text', 1, text); + + // Send it! + send_fn(value, undefined, text); + } + }); + + // Input focus handler + element_text_sel.focus(function() { + Bubble.close(); + }); + } catch(e) { + Console.error('PEP._eventsCommonText', e); + } + + }; + + + /** + * Attaches mood text events + * @private + * @param {object} mood_text_sel + * @return {undefined} + */ + self._eventsMoodText = function(mood_text_sel) { + + try { + self._eventsCommonText( + 'mood', + mood_text_sel, + self.sendMood + ); + } catch(e) { + Console.error('PEP._eventsMoodText', e); + } + + }; + + + /** + * Attaches activity text events + * @private + * @param {object} activity_text_sel + * @return {undefined} + */ + self._eventsActivityText = function(activity_text_sel) { + + try { + self._eventsCommonText( + 'activity', + activity_text_sel, + self.sendActivity + ); + } catch(e) { + Console.error('PEP._eventsActivityText', e); + } + + }; + + /** * Stores the PEP items * @public @@ -34,28 +646,34 @@ var PEP = (function () { self.store = function(xid, type, value1, value2, value3, value4) { try { - // Handle the correct values - if(!value1) - value1 = ''; - if(!value2) - value2 = ''; - if(!value3) - value3 = ''; - if(!value4) - value4 = ''; - - // If one value if(value1 || value2 || value3 || value4) { - // Define the XML variable var xml = ''; - // Generate the correct XML - if(type == 'tune') - xml += '' + value1.htmlEnc() + '' + value2.htmlEnc() + '' + value3.htmlEnc() + '' + value4.htmlEnc() + ''; - else if(type == 'geoloc') - xml += '' + value1.htmlEnc() + '' + value2.htmlEnc() + '' + value3.htmlEnc() + ''; - else - xml += '' + value1.htmlEnc() + '' + value2.htmlEnc() + ''; + // Generate the subnodes + switch(type) { + case 'tune': + xml += self._generateStore({ + 'artist': value1, + 'title': value2, + 'album': value3, + 'uri': value4 + }); + break; + + case 'geoloc': + xml += self._generateStore({ + 'lat': value1, + 'lon': value2, + 'human': value3 + }); + break; + + default: + xml += self._generateStore({ + 'value': value1, + 'text': value2 + }); + } // End the XML node xml += ''; @@ -86,244 +704,41 @@ var PEP = (function () { try { // Read the target input for values - var value = $(Common.XMLFromString(DataStore.getDB(Connection.desktop_hash, 'pep-' + type, xid))); - var aLink = ''; + var value = $(Common.XMLFromString( + DataStore.getDB(Connection.desktop_hash, 'pep-' + type, xid)) + ); // If the PEP element exists if(type) { // Get the user hash var hash = hex_md5(xid); - - // Initialize - var fText, fValue; - var dText = ''; + var display_args = {}; - // Initialize typed valyes - var tLat, tLon, tHuman, tReal; - var fArtist, fTitle, fAlbum, fURI; - var pepValue, pepText; - // Parse the XML for mood and activity - if((type == 'mood') || (type == 'activity')) { - if(value) { - pepValue = value.find('value').text(); - pepText = value.find('text').text(); - - // No value? - if(!pepValue) - pepValue = 'none'; - - // Apply the good values - if(type == 'mood') - fValue = self.moodIcon(pepValue); - else if(type == 'activity') - fValue = self.activityIcon(pepValue); - if(!pepText) - fText = Common._e("unknown"); - else - fText = pepText; - } - - else { - if(type == 'mood') - fValue = self.moodIcon('undefined'); - else if(type == 'activity') - fValue = self.activityIcon('exercising'); - - fText = Common._e("unknown"); - } - - dText = fText; - fText = fText.htmlEnc(); + switch(type) { + case 'mood': + display_args = self._displayMood(value); + break; + + case 'activity': + display_args = self._displayActivity(value); + break; + + case 'tune': + display_args = self._displayTune(value); + break; + + case 'geoloc': + display_args = self._displayGeolocation(value); + break; } + + // Append foreign PEP user values + self._appendForeignDisplayObject(xid, hash, type, display_args); - else if(type == 'tune') { - fValue = 'tune-note'; - - if(value) { - // Parse the tune XML - var tArtist = value.find('artist').text(); - var tTitle = value.find('title').text(); - var tAlbum = value.find('album').text(); - var tURI = value.find('uri').text(); - - // Apply the good values - if(!tArtist && !tAlbum && !tTitle) { - fText = Common._e("unknown"); - dText = fText; - } - - else { - // URI element - if(!tURI) - fURI = 'http://grooveshark.com/search?q=' + encodeURIComponent(tArtist + ' ' + tTitle + ' ' + tAlbum); - else - fURI = tURI; - - // Artist element - if(!tArtist) - fArtist = Common._e("unknown"); - else - fArtist = tArtist; - - // Title element - if(!tTitle) - fTitle = Common._e("unknown"); - else - fTitle = tTitle; - - // Album element - if(!tAlbum) - fAlbum = Common._e("unknown"); - else - fAlbum = tAlbum; - - // Generate the link to the title - aLink = ' href="' + fURI + '" target="_blank"'; - - // Generate the text to be displayed - dText = fArtist + ' - ' + fTitle + ' (' + fAlbum + ')'; - fText = '' + dText + ''; - } - } - - else { - fText = Common._e("unknown"); - dText = fText; - } - } - - else if(type == 'geoloc') { - fValue = 'location-world'; - - if(value) { - // Parse the geoloc XML - tLat = value.find('lat').text(); - tLon = value.find('lon').text(); - tHuman = value.find('human').text(); - tReal = tHuman; - - // No human location? - if(!tHuman) - tHuman = Common._e("See his/her position on the globe"); - - // Generate the text to be displayed - if(tLat && tLon) { - aLink = ' href="http://maps.google.com/?q=' + Common.encodeQuotes(tLat) + ',' + Common.encodeQuotes(tLon) + '" target="_blank"'; - fText = '' + tHuman.htmlEnc() + ''; - - if(tReal) - dText = tReal; - else - dText = tLat + '; ' + tLon; - } - - else { - fText = Common._e("unknown"); - dText = fText; - } - } - - else { - fText = Common._e("unknown"); - dText = fText; - } - } - - // Apply the text to the buddy infos - var this_buddy = '#roster .buddy[data-xid="' + escape(xid) + '"]'; - - if(Common.exists(this_buddy)) - $(this_buddy + ' .bi-' + type).replaceWith('

' + fText + '

'); - - // Apply the text to the buddy chat - if(Common.exists('#' + hash)) { - // Selector - var bc_pep = $('#' + hash + ' .bc-pep'); - - // We remove the old PEP item - bc_pep.find('a.bi-' + type).remove(); - - // If the new PEP item is not null, create a new one - if(fText != Common._e("unknown")) - bc_pep.prepend( - '' - ); - - // Process the new status position - Presence.adaptChat(hash); - } - - // If this is the PEP values of the logged in user + // PEP values of the logged in user? if(xid == Common.getXID()) { - // Change the icon/value of the target element - if((type == 'mood') || (type == 'activity')) { - // Change the input value - var dVal = ''; - var dAttr = pepValue; - - // Must apply default values? - if(pepValue == 'none') { - if(type == 'mood') - dAttr = 'happy'; - else - dAttr = 'exercising'; - } - - // No text? - if(dText != Common._e("unknown")) - dVal = dText; - - // Store this user event in our database - DataStore.setDB(Connection.desktop_hash, type + '-value', 1, dAttr); - DataStore.setDB(Connection.desktop_hash, type + '-text', 1, dVal); - - // Apply this PEP event - $('#my-infos .f-' + type + ' a.picker').attr('data-value', dAttr); - $('#my-infos .f-' + type + ' input').val(dVal); - $('#my-infos .f-' + type + ' input').placeholder(); - } - - else if((type == 'tune') || (type == 'geoloc')) { - // Reset the values - $('#my-infos .f-others a.' + type).remove(); - - // Not empty? - if(dText != Common._e("unknown")) { - // Specific stuffs - var href, title, icon_class; - - if(type == 'tune') { - href = fURI; - title = dText; - icon_class = 'tune-note'; - } - - else { - href = 'http://maps.google.com/?q=' + Common.encodeQuotes(tLat) + ',' + Common.encodeQuotes(tLon); - title = Common._e("Where are you?") + ' (' + dText + ')'; - icon_class = 'location-world'; - } - - // Must create the container? - if(!Common.exists('#my-infos .f-others')) - $('#my-infos .content').append('
'); - - // Create the element - $('#my-infos .f-others').prepend( - '' + - '' + - '' - ); - } - - // Empty? - else if(!Common.exists('#my-infos .f-others a.icon')) - $('#my-infos .f-others').remove(); - - // Process the roster height again - Roster.adapt(); - } + self._appendOwnDisplayObject(type, display_args); } } } catch(e) { @@ -518,7 +933,7 @@ var PEP = (function () { * @param {string} text * @return {undefined} */ - self.sendMood = function(value, text) { + self.sendMood = function(value, _, text) { /* REF: http://xmpp.org/extensions/xep-0107.html */ @@ -565,19 +980,40 @@ var PEP = (function () { iq.setType('set'); // We create the XML document - var pubsub = iq.appendNode('pubsub', {'xmlns': NS_PUBSUB}); - var publish = pubsub.appendChild(iq.buildNode('publish', {'node': NS_ACTIVITY, 'xmlns': NS_PUBSUB})); - var item = publish.appendChild(iq.buildNode('item', {'xmlns': NS_PUBSUB})); - var activity = item.appendChild(iq.buildNode('activity', {'xmlns': NS_ACTIVITY})); + var pubsub = iq.appendNode('pubsub', { + 'xmlns': NS_PUBSUB + }); + + var publish = pubsub.appendChild(iq.buildNode('publish', { + 'node': NS_ACTIVITY, + 'xmlns': NS_PUBSUB + })); + + var item = publish.appendChild(iq.buildNode('item', { + 'xmlns': NS_PUBSUB + })); + + var activity = item.appendChild(iq.buildNode('activity', { + 'xmlns': NS_ACTIVITY + })); if(main != 'none') { - var mainType = activity.appendChild(iq.buildNode(main, {'xmlns': NS_ACTIVITY})); + var mainType = activity.appendChild(iq.buildNode(main, { + 'xmlns': NS_ACTIVITY + })); // Child nodes - if(sub) - mainType.appendChild(iq.buildNode(sub, {'xmlns': NS_ACTIVITY})); - if(text) - activity.appendChild(iq.buildNode('text', {'xmlns': NS_ACTIVITY}, text)); + if(sub) { + mainType.appendChild(iq.buildNode(sub, { + 'xmlns': NS_ACTIVITY + })); + } + + if(text) { + activity.appendChild(iq.buildNode('text', { + 'xmlns': NS_ACTIVITY + }, text)); + } } // And finally we send the mood that is set @@ -594,49 +1030,83 @@ var PEP = (function () { /** * Sends the user's geographic position * @public - * @param {string} vLat - * @param {string} vLon + * @param {string} lat + * @param {string} lon * @param {string} vAlt - * @param {string} vCountry - * @param {string} vCountrycode - * @param {string} vRegion - * @param {string} vPostalcode - * @param {string} vLocality - * @param {string} vStreet - * @param {string} vBuilding - * @param {string} vText - * @param {string} vURI + * @param {string} country + * @param {string} countrycode + * @param {string} region + * @param {string} postalcode + * @param {string} locality + * @param {string} street + * @param {string} building + * @param {string} text + * @param {string} uri * @return {undefined} */ - self.sendPosition = function(vLat, vLon, vAlt, vCountry, vCountrycode, vRegion, vPostalcode, vLocality, vStreet, vBuilding, vText, vURI) { + self.sendPosition = function(lat, lon, alt, country, countrycode, region, postalcode, locality, street, building, text, uri) { /* REF: http://xmpp.org/extensions/xep-0080.html */ try { - // We propagate the position on pubsub var iq = new JSJaCIQ(); iq.setType('set'); - // We create the XML document - var pubsub = iq.appendNode('pubsub', {'xmlns': NS_PUBSUB}); - var publish = pubsub.appendChild(iq.buildNode('publish', {'node': NS_GEOLOC, 'xmlns': NS_PUBSUB})); - var item = publish.appendChild(iq.buildNode('item', {'xmlns': NS_PUBSUB})); - var geoloc = item.appendChild(iq.buildNode('geoloc', {'xmlns': NS_GEOLOC})); + // Create XML nodes + var pubsub = iq.appendNode('pubsub', { + 'xmlns': NS_PUBSUB + }); + + var publish = pubsub.appendChild(iq.buildNode('publish', { + 'node': NS_GEOLOC, + 'xmlns': NS_PUBSUB + })); + + var item = publish.appendChild(iq.buildNode('item', { + 'xmlns': NS_PUBSUB + })); + + var geoloc = item.appendChild(iq.buildNode('geoloc', { + 'xmlns': NS_GEOLOC + })); - // Create two position arrays - var pos_names = ['lat', 'lon', 'alt', 'country', 'countrycode', 'region', 'postalcode', 'locality', 'street', 'building', 'text', 'uri', 'timestamp']; - var pos_values = [ vLat, vLon, vAlt, vCountry, vCountrycode, vRegion, vPostalcode, vLocality, vStreet, vBuilding, vText, vURI, DateUtils.getXMPPTime('utc')]; - - for(var i = 0; i < pos_names.length; i++) { - if(pos_names[i] && pos_values[i]) - geoloc.appendChild(iq.buildNode(pos_names[i], {'xmlns': NS_GEOLOC}, pos_values[i])); + // Position object + var position_obj = { + 'lat': lat, + 'lon': lon, + 'alt': alt, + 'country': country, + 'countrycode': countrycode, + 'region': region, + 'postalcode': postalcode, + 'locality': locality, + 'street': street, + 'building': building, + 'text': text, + 'uri': uri, + 'timestamp': DateUtils.getXMPPTime('utc'), + 'tzo': DateUtils.getTZO() + }; + + var cur_position_val; + + for(var cur_position_type in position_obj) { + cur_position_val = position_obj[cur_position_type]; + + if(cur_position_val) { + geoloc.appendChild( + iq.buildNode(cur_position_type, { + 'xmlns': NS_GEOLOC + }, cur_position_val) + ); + } } // And finally we send the XML con.send(iq); // For logger - if(vLat && vLon) { + if(lat && lon) { Console.info('Geolocated.'); } else { Console.warn('Not geolocated.'); @@ -660,22 +1130,26 @@ var PEP = (function () { var result = $(data).find('result:first'); // Get latitude and longitude - var lat = result.find('geometry:first location:first lat').text(); - var lng = result.find('geometry:first location:first lng').text(); + var geometry_sel = result.find('geometry:first location:first'); + + var lat = geometry_sel.find('lat').text(); + var lng = geometry_sel.find('lng').text(); + + var addr_comp_sel = result.find('address_component'); var array = [ - lat, - lng, - result.find('address_component:has(type:contains("country")):first long_name').text(), - result.find('address_component:has(type:contains("country")):first short_name').text(), - result.find('address_component:has(type:contains("administrative_area_level_1")):first long_name').text(), - result.find('address_component:has(type:contains("postal_code")):first long_name').text(), - result.find('address_component:has(type:contains("locality")):first long_name').text(), - result.find('address_component:has(type:contains("route")):first long_name').text(), - result.find('address_component:has(type:contains("street_number")):first long_name').text(), - result.find('formatted_address:first').text(), - 'http://maps.google.com/?q=' + Common.encodeQuotes(lat) + ',' + Common.encodeQuotes(lng) - ]; + lat, + lng, + addr_comp_sel.filter(':has(type:contains("country")):first').find('long_name').text(), + addr_comp_sel.filter(':has(type:contains("country")):first').find('short_name').text(), + addr_comp_sel.filter(':has(type:contains("administrative_area_level_1")):first').find('long_name').text(), + addr_comp_sel.filter(':has(type:contains("postal_code")):first').find('long_name').text(), + addr_comp_sel.filter(':has(type:contains("locality")):first').find('long_name').text(), + addr_comp_sel.filter(':has(type:contains("route")):first').find('long_name').text(), + addr_comp_sel.filter(':has(type:contains("street_number")):first').find('long_name').text(), + result.find('formatted_address:first').text(), + 'http://maps.google.com/?q=' + Common.encodeQuotes(lat) + ',' + Common.encodeQuotes(lng) + ]; return array; } catch(e) { @@ -688,41 +1162,42 @@ var PEP = (function () { /** * Converts a position into an human-readable one * @public - * @param {string} tLocality - * @param {string} tRegion - * @param {string} tCountry + * @param {string} locality + * @param {string} region + * @param {string} country * @return {string} */ - self.humanPosition = function(tLocality, tRegion, tCountry) { + self.humanPosition = function(locality, region, country) { + + var human_value = ''; try { - var tHuman = ''; - - // Any locality? - if(tLocality) { - tHuman += tLocality; + if(locality) { + // Any locality + human_value += locality; - if(tRegion) - tHuman += ', ' + tRegion; - if(tCountry) - tHuman += ', ' + tCountry; - } - - // Any region? - else if(tRegion) { - tHuman += tRegion; + if(region) { + human_value += ', ' + region; + } - if(tCountry) - tHuman += ', ' + tCountry; + if(country) { + human_value += ', ' + country; + } + } else if(region) { + // Any region + human_value += region; + + if(country) { + human_value += ', ' + country; + } + } else if(country) { + // Any country + human_value += country; } - - // Any country? - else if(tCountry) - tHuman += tCountry; - - return tHuman; } catch(e) { Console.error('PEP.humanPosition', e); + } finally { + return human_value; } }; @@ -738,38 +1213,43 @@ var PEP = (function () { try { // Convert integers to strings - var vLat = '' + position.coords.latitude; - var vLon = '' + position.coords.longitude; - var vAlt = '' + position.coords.altitude; + var lat = '' + position.coords.latitude; + var lon = '' + position.coords.longitude; + var alt = '' + position.coords.altitude; // Get full position (from Google Maps API) - $.get('./server/geolocation.php', {latitude: vLat, longitude: vLon, language: XML_LANG}, function(data) { - // Parse data! - var results = self.parsePosition(data); - - // Handled! - self.sendPosition( - Utils.isNumber(vLat) ? vLat : null, - Utils.isNumber(vLon) ? vLon : null, - Utils.isNumber(vAlt) ? vAlt : null, - results[2], - results[3], - results[4], - results[5], - results[6], - results[7], - results[8], - results[9], - results[10] - ); - - // Store data - DataStore.setDB(Connection.desktop_hash, 'geolocation', 'now', Common.xmlToString(data)); - - Console.log('Position details got from Google Maps API.'); + $.get('./server/geolocation.php', { + latitude: lat, + longitude: lon, + language: XML_LANG + }, function(data) { + // Still connected? + if(Common.isConnected()) { + var results = self.parsePosition(data); + + self.sendPosition( + (Utils.isNumber(lat) ? lat : null), + (Utils.isNumber(lon) ? lon : null), + (Utils.isNumber(alt) ? alt : null), + results[2], + results[3], + results[4], + results[5], + results[6], + results[7], + results[8], + results[9], + results[10] + ); + + // Store data + DataStore.setDB(Connection.desktop_hash, 'geolocation', 'now', Common.xmlToString(data)); + + Console.log('Position details got from Google Maps API.'); + } }); - Console.log('Position got: latitude > ' + vLat + ' / longitude > ' + vLon + ' / altitude > ' + vAlt); + Console.log('Position got: latitude > ' + lat + ' / longitude > ' + lon + ' / altitude > ' + alt); } catch(e) { Console.error('PEP.getPosition', e); } @@ -786,7 +1266,9 @@ var PEP = (function () { try { // Don't fire it until options & features are not retrieved! - if(!DataStore.getDB(Connection.desktop_hash, 'options', 'geolocation') || (DataStore.getDB(Connection.desktop_hash, 'options', 'geolocation') == '0') || !Features.enabledPEP()) { + if(!DataStore.getDB(Connection.desktop_hash, 'options', 'geolocation') || + (DataStore.getDB(Connection.desktop_hash, 'options', 'geolocation') == '0') || + !Features.enabledPEP()) { return; } @@ -819,8 +1301,14 @@ var PEP = (function () { var iq = new JSJaCIQ(); iq.setType('get'); - var pubsub = iq.appendNode('pubsub', {'xmlns': NS_PUBSUB}); - var ps_items = pubsub.appendChild(iq.buildNode('items', {'node': NS_GEOLOC, 'xmlns': NS_PUBSUB})); + var pubsub = iq.appendNode('pubsub', { + 'xmlns': NS_PUBSUB + }); + + var ps_items = pubsub.appendChild(iq.buildNode('items', { + 'node': NS_GEOLOC, + 'xmlns': NS_PUBSUB + })); ps_items.setAttribute('max_items', '0'); @@ -890,168 +1378,26 @@ var PEP = (function () { // Click event for user mood $('#my-infos .f-mood a.picker').click(function() { - // Initialize some vars - var path = '#my-infos .f-mood div.bubble'; - var mood_id = ['crazy', 'excited', 'playful', 'happy', 'shocked', 'hot', 'sad', 'amorous', 'confident']; - var mood_lang = [Common._e("Crazy"), Common._e("Excited"), Common._e("Playful"), Common._e("Happy"), Common._e("Shocked"), Common._e("Hot"), Common._e("Sad"), Common._e("Amorous"), Common._e("Confident")]; - var mood_val = $('#my-infos .f-mood a.picker').attr('data-value'); - - // Yet displayed? - var can_append = true; - - if(Common.exists(path)) - can_append = false; - - // Add this bubble! - Bubble.show(path); - - if(!can_append) - return false; - - // Generate the HTML code - var html = '
'; - - for(var i in mood_id) { - // Yet in use: no need to display it! - if(mood_id[i] == mood_val) - continue; - - html += ''; - } - - html += '
'; - - // Append the HTML code - $('#my-infos .f-mood').append(html); - - // Click event - $(path + ' a').click(function() { - // Update the mood marker - $('#my-infos .f-mood a.picker').attr('data-value', $(this).attr('data-value')); - - // Close the bubble - Bubble.close(); - - // Focus on the status input - $(document).oneTime(10, function() { - $('#mood-text').focus(); - }); - - return false; - }); - - return false; + return PEP._callbackMoodPicker( + $(this) + ); }); // Click event for user activity $('#my-infos .f-activity a.picker').click(function() { - // Initialize some vars - var path = '#my-infos .f-activity div.bubble'; - var activity_id = ['doing_chores', 'drinking', 'eating', 'exercising', 'grooming', 'having_appointment', 'inactive', 'relaxing', 'talking', 'traveling', 'working']; - var activity_lang = [Common._e("Chores"), Common._e("Drinking"), Common._e("Eating"), Common._e("Exercising"), Common._e("Grooming"), Common._e("Appointment"), Common._e("Inactive"), Common._e("Relaxing"), Common._e("Talking"), Common._e("Traveling"), Common._e("Working")]; - var activity_val = $('#my-infos .f-activity a.picker').attr('data-value'); - - // Yet displayed? - var can_append = true; - - if(Common.exists(path)) - can_append = false; - - // Add this bubble! - Bubble.show(path); - - if(!can_append) - return false; - - // Generate the HTML code - var html = '
'; - - for(var i in activity_id) { - // Yet in use: no need to display it! - if(activity_id[i] == activity_val) - continue; - - html += ''; - } - - html += '
'; - - // Append the HTML code - $('#my-infos .f-activity').append(html); - - // Click event - $(path + ' a').click(function() { - // Update the activity marker - $('#my-infos .f-activity a.picker').attr('data-value', $(this).attr('data-value')); - - // Close the bubble - Bubble.close(); - - // Focus on the status input - $(document).oneTime(10, function() { - $('#activity-text').focus(); - }); - - return false; - }); - - return false; + return PEP._callbackActivityPicker( + $(this) + ); }); - // Submit events for PEP inputs - $('#mood-text, #activity-text').placeholder() - - .keyup(function(e) { - if(e.keyCode == 13) { - $(this).blur(); - - return false; - } - }); - - // Input blur handler - $('#mood-text').blur(function() { - // Read the parameters - var value = $('#my-infos .f-mood a.picker').attr('data-value'); - var text = $(this).val(); - - // Must send the mood? - if((value != DataStore.getDB(Connection.desktop_hash, 'mood-value', 1)) || (text != DataStore.getDB(Connection.desktop_hash, 'mood-text', 1))) { - // Update the local stored values - DataStore.setDB(Connection.desktop_hash, 'mood-value', 1, value); - DataStore.setDB(Connection.desktop_hash, 'mood-text', 1, text); - - // Send it! - self.sendMood(value, text); - } - }) - - // Input focus handler - .focus(function() { - Bubble.close(); - }); - - // Input blur handler - $('#activity-text').blur(function() { - // Read the parameters - var value = $('#my-infos .f-activity a.picker').attr('data-value'); - var text = $(this).val(); - - // Must send the activity? - if((value != DataStore.getDB(Connection.desktop_hash, 'activity-value', 1)) || (text != DataStore.getDB(Connection.desktop_hash, 'activity-text', 1))) { - // Update the local stored values - DataStore.setDB(Connection.desktop_hash, 'activity-value', 1, value); - DataStore.setDB(Connection.desktop_hash, 'activity-text', 1, text); - - // Send it! - self.sendActivity(value, '', text); - } - }) - - // Input focus handler - .focus(function() { - Bubble.close(); - }); + // Attach events + self._eventsMoodText( + $('#mood-text') + ); + + self._eventsActivityText( + $('#activity-text') + ); } catch(e) { Console.error('PEP.instance', e); } diff --git a/source/app/javascripts/presence.js b/source/app/javascripts/presence.js index 52eee9f..1358405 100644 --- a/source/app/javascripts/presence.js +++ b/source/app/javascripts/presence.js @@ -25,6 +25,319 @@ var Presence = (function () { self.auto_idle = false; + /** + * Handles groupchat presence + * @private + * @param {string} from + * @param {string} xid + * @param {string} hash + * @param {string} type + * @param {string} show + * @param {string} status + * @param {string} xid_hash + * @param {string} resource + * @param {object} node_sel + * @param {object} presence + * @param {number} priority + * @param {boolean} has_photo + * @param {string} checksum + * @param {string} caps + * @return {undefined} + */ + self._handleGroupchat = function(from, xid, hash, type, show, status, xid_hash, resource, node_sel, presence, priority, has_photo, checksum, caps) { + + try { + var resources_obj, xml; + + var x_muc = node_sel.find('x[xmlns="' + NS_MUC_USER + '"]:first'); + var item_sel = x_muc.find('item'); + + var affiliation = item_sel.attr('affiliation'); + var role = item_sel.attr('role'); + var reason = item_sel.find('reason').text(); + var iXID = item_sel.attr('jid'); + var iNick = item_sel.attr('nick'); + + var nick = resource; + var message_time = DateUtils.getCompleteTime(); + var not_initial = !Common.exists('#' + xid_hash + '[data-initial="true"]'); + + // Read the status code + var status_code = []; + + x_muc.find('status').each(function() { + status_code.push(parseInt($(this).attr('code'))); + }); + + if(type && (type == 'unavailable')) { + // User quitting + self.displayMUC( + from, + xid_hash, + hash, + type, + show, + status, + affiliation, + role, + reason, + status_code, + iXID, + iNick, + message_time, + nick, + not_initial + ); + + DataStore.removeDB(Connection.desktop_hash, 'presence-stanza', from); + resources_obj = self.removeResource(xid, resource); + } else { + // User joining + + // Fixes M-Link first presence bug (missing ID!) + if(nick == Name.getMUCNick(xid_hash) && + presence.getID() === null && + !Common.exists('#page-engine #' + xid_hash + ' .list .' + hash)) { + Groupchat.handleMUC(presence); + + Console.warn('Passed M-Link MUC first presence handling.'); + } else { + self.displayMUC( + from, + xid_hash, + hash, + type, + show, + status, + affiliation, + role, + reason, + status_code, + iXID, + iNick, + message_time, + nick, + not_initial + ); + + xml = '' + + '' + priority.htmlEnc() + '' + + '' + show.htmlEnc() + '' + + '' + type.htmlEnc() + '' + + '' + status.htmlEnc() + '' + + '' + has_photo.htmlEnc() + '' + + '' + checksum.htmlEnc() + '' + + '' + caps.htmlEnc() + '' + + ''; + + DataStore.setDB(Connection.desktop_hash, 'presence-stanza', from, xml); + resources_obj = self.addResource(xid, resource); + } + } + + // Manage the presence + self.processPriority(from, resource, resources_obj); + self.funnel(from, hash); + } catch(e) { + Console.error('Groupchat._handleGroupchat', e); + } + + }; + + + /** + * Handles user presence + * @private + * @param {string} from + * @param {string} xid + * @param {string} type + * @param {string} show + * @param {string} status + * @param {string} xid_hash + * @param {string} resource + * @param {object} node_sel + * @param {number} priority + * @param {boolean} has_photo + * @param {string} checksum + * @param {string} caps + * @return {undefined} + */ + self._handleUser = function(from, xid, type, show, status, xid_hash, resource, node_sel, priority, has_photo, checksum, caps) { + + try { + var resources_obj, xml; + + // Subscribed/Unsubscribed stanzas + if((type == 'subscribed') || (type == 'unsubscribed')) { + return; + } + + // Subscribe stanza + else if(type == 'subscribe') { + // This is a buddy we can safely authorize, because we added him to our roster + if(Common.exists('#roster .buddy[data-xid="' + escape(xid) + '"]')) { + self.acceptSubscribe(xid); + } + + // We do not know this entity, we'd be better ask the user + else { + // Get the nickname + var nickname = node_sel.find('nick[xmlns="' + NS_NICK + '"]:first').text(); + + // New notification + Notification.create('subscribe', xid, [xid, nickname], status); + } + } + + // Unsubscribe stanza + else if(type == 'unsubscribe') { + Roster.send(xid, 'remove'); + } + + // Other stanzas + else { + // Unavailable/error presence + if(type == 'unavailable') { + DataStore.removeDB(Connection.desktop_hash, 'presence-stanza', from); + resources_obj = self.removeResource(xid, resource); + } else { + xml = '' + + '' + priority.htmlEnc() + '' + + '' + show.htmlEnc() + '' + + '' + type.htmlEnc() + '' + + '' + status.htmlEnc() + '' + + '' + has_photo.htmlEnc() + '' + + '' + checksum.htmlEnc() + '' + + '' + caps.htmlEnc() + '' + + ''; + + DataStore.setDB(Connection.desktop_hash, 'presence-stanza', from, xml); + resources_obj = self.addResource(xid, resource); + } + + // We manage the presence + self.processPriority(xid, resource, resources_obj); + self.funnel(xid, xid_hash); + + // We display the presence in the current chat + if(Common.exists('#' + xid_hash)) { + var dStatus = self.filterStatus(xid, status, false); + + if(dStatus) { + dStatus = ' (' + dStatus + ')'; + } + + // Generate the presence-in-chat code + var dName = Name.getBuddy(from).htmlEnc(); + var dBody = dName + ' (' + from + ') ' + Common._e("is now") + ' ' + self.humanShow(show, type) + dStatus; + + // Check whether it has been previously displayed + var can_display = ($('#' + xid_hash + ' .one-line.system-message:last').html() != dBody); + + if(can_display) { + Message.display( + 'chat', + xid, + xid_hash, + dName, + dBody, + DateUtils.getCompleteTime(), + DateUtils.getTimeStamp(), + 'system-message', + false + ); + } + } + } + + // Get disco#infos for this presence (related to Caps) + Caps.getDiscoInfos(from, caps); + } catch(e) { + Console.error('Groupchat._handleUser', e); + } + + }; + + + /** + * Attaches picker events + * @private + * @param {string} name + * @param {object} element_text_sel + * @param {function} send_fn + * @return {boolean} + */ + self._eventsPicker = function(element_picker_sel) { + + try { + // Disabled? + if(element_picker_sel.hasClass('disabled')) { + return false; + } + + // Initialize some vars + var path = '#my-infos .f-presence div.bubble'; + var show_val = self.getUserShow(); + + var shows_obj = { + 'xa': Common._e("Not available"), + 'away': Common._e("Away"), + 'available': Common._e("Available") + }; + + var can_append = !Common.exists(path); + + // Add this bubble! + Bubble.show(path); + + if(!can_append) { + return false; + } + + // Generate the HTML code + var html = '
'; + + for(var cur_show_name in shows_obj) { + // Yet in use: no need to display it! + if(cur_show_name == show_val) { + continue; + } + + html += ''; + } + + html += '
'; + + // Append the HTML code + $('#my-infos .f-presence').append(html); + + // Click event + $(path + ' a').click(function() { + // Update the presence show marker + $('#my-infos .f-presence a.picker').attr( + 'data-value', + $(this).attr('data-value') + ); + + // Close the bubble + Bubble.close(); + + // Focus on the status input + $(document).oneTime(10, function() { + $('#presence-status').focus(); + }); + + return false; + }); + } catch(e) { + Console.error('Groupchat._eventsPicker', e); + } finally { + return false; + } + + }; + + /** * Sends the user first presence * @public @@ -36,6 +349,8 @@ var Presence = (function () { try { Console.info('First presence sent.'); + var presence_status_sel = $('#presence-status'); + // Jappix is now ready: change the title Interface.title('talk'); @@ -46,21 +361,20 @@ var Presence = (function () { self.first_sent = true; // Try to use the last status message - var status = DataStore.getDB(Connection.desktop_hash, 'options', 'presence-status'); - - if(!status) - status = ''; - + var status = DataStore.getDB(Connection.desktop_hash, 'options', 'presence-status') || ''; + // We tell the world that we are online - if(!is_anonymous) + if(!is_anonymous) { self.send('', '', '', status, checksum); + } // Any status to apply? - if(status) - $('#presence-status').val(status); + if(status) { + presence_status_sel.val(status); + } // Enable the presence picker - $('#presence-status').removeAttr('disabled'); + presence_status_sel.removeAttr('disabled'); $('#my-infos .f-presence a.picker').removeClass('disabled'); // We set the last activity stamp @@ -102,189 +416,87 @@ var Presence = (function () { // We define everything needed here var from = Common.fullXID(Common.getStanzaFrom(presence)); var hash = hex_md5(from); - var node = presence.getNode(); + var node_sel = $(presence.getNode()); var xid = Common.bareXID(from); - var xidHash = hex_md5(xid); + var xid_hash = hex_md5(xid); var resource = Common.thisResource(from); - var resources_obj, xml; // We get the type content - var type = presence.getType(); - if(!type) - type = ''; + var type = presence.getType() || ''; // We get the priority content var priority = presence.getPriority() + ''; - if(!priority || (type == 'error')) + if(!priority || (type == 'error')) { priority = '0'; + } // We get the show content var show = presence.getShow(); - if(!show || (type == 'error')) + if(!show || (type == 'error')) { show = ''; + } // We get the status content var status = presence.getStatus(); - if(!status || (type == 'error')) + if(!status || (type == 'error')) { status = ''; + } // We get the photo content - var photo = $(node).find('x[xmlns="' + NS_VCARD_P + '"]:first photo'); + var photo = node_sel.find('x[xmlns="' + NS_VCARD_P + '"]:first photo'); var checksum = photo.text(); - var hasPhoto = photo.size(); - - if(hasPhoto && (type != 'error')) - hasPhoto = 'true'; - else - hasPhoto = 'false'; + var has_photo = (photo.size() && (type != 'error')) ? 'true' : 'false'; // We get the CAPS content - var caps = $(node).find('c[xmlns="' + NS_CAPS + '"]:first').attr('ver'); - if(!caps || (type == 'error')) + var caps = node_sel.find('c[xmlns="' + NS_CAPS + '"]:first').attr('ver'); + if(!caps || (type == 'error')) { caps = ''; + } // This presence comes from another resource of my account with a difference avatar checksum - if((xid == Common.getXID()) && (hasPhoto == 'true') && (checksum != DataStore.getDB(Connection.desktop_hash, 'checksum', 1))) + if(xid == Common.getXID() && + has_photo == 'true' && + checksum != DataStore.getDB(Connection.desktop_hash, 'checksum', 1)) { Avatar.get(Common.getXID(), 'force', 'true', 'forget'); + } - // This presence comes from a groupchat if(Utils.isPrivate(xid)) { - var x_muc = $(node).find('x[xmlns="' + NS_MUC_USER + '"]:first'); - var item = x_muc.find('item'); - var affiliation = item.attr('affiliation'); - var role = item.attr('role'); - var reason = item.find('reason').text(); - var iXID = item.attr('jid'); - var iNick = item.attr('nick'); - var nick = resource; - var messageTime = DateUtils.getCompleteTime(); - var notInitial = true; - - // Read the status code - var status_code = []; - - x_muc.find('status').each(function() { - status_code.push(parseInt($(this).attr('code'))); - }); - - // If this is an initial presence (when user join the room) - if(Common.exists('#' + xidHash + '[data-initial="true"]')) { - notInitial = false; - } - - // If one user is quitting - if(type && (type == 'unavailable')) { - self.displayMUC(from, xidHash, hash, type, show, status, affiliation, role, reason, status_code, iXID, iNick, messageTime, nick, notInitial); - - DataStore.removeDB(Connection.desktop_hash, 'presence-stanza', from); - resources_obj = self.removeResource(xid, resource); - } - - // If one user is joining - else { - // Fixes M-Link first presence bug (missing ID!) - if((nick == Name.getMUCNick(xidHash)) && (presence.getID() === null) && !Common.exists('#page-engine #' + xidHash + ' .list .' + hash)) { - Groupchat.handleMUC(presence); - - Console.warn('Passed M-Link MUC first presence handling.'); - } - - else { - self.displayMUC(from, xidHash, hash, type, show, status, affiliation, role, reason, status_code, iXID, iNick, messageTime, nick, notInitial); - - xml = '' + priority.htmlEnc() + '' + show.htmlEnc() + '' + type.htmlEnc() + '' + status.htmlEnc() + '' + hasPhoto.htmlEnc() + '' + checksum.htmlEnc() + '' + caps.htmlEnc() + ''; - - DataStore.setDB(Connection.desktop_hash, 'presence-stanza', from, xml); - resources_obj = self.addResource(xid, resource); - } - } - - // Manage the presence - self.processPriority(from, resource, resources_obj); - self.funnel(from, hash); + // Groupchat presence + self._handleGroupchat( + from, + xid, + hash, + type, + show, + status, + xid_hash, + resource, + node_sel, + presence, + priority, + has_photo, + checksum, + caps + ); + } else { + // User or gateway presence + self._handleUser( + from, + xid, + type, + show, + status, + xid_hash, + resource, + node_sel, + priority, + has_photo, + checksum, + caps + ); } - // This presence comes from an user or a gateway - else { - // Subscribed/Unsubscribed stanzas - if((type == 'subscribed') || (type == 'unsubscribed')) - return; - // Subscribe stanza - else if(type == 'subscribe') { - // This is a buddy we can safely authorize, because we added him to our roster - if(Common.exists('#roster .buddy[data-xid="' + escape(xid) + '"]')) - self.acceptSubscribe(xid); - - // We do not know this entity, we'd be better ask the user - else { - // Get the nickname - var nickname = $(node).find('nick[xmlns="' + NS_NICK + '"]:first').text(); - - // New notification - Notification.create('subscribe', xid, [xid, nickname], status); - } - } - - // Unsubscribe stanza - else if(type == 'unsubscribe') { - Roster.send(xid, 'remove'); - } - - // Other stanzas - else { - // Unavailable/error presence - if(type == 'unavailable') { - DataStore.removeDB(Connection.desktop_hash, 'presence-stanza', from); - resources_obj = self.removeResource(xid, resource); - } - - // Other presence (available, subscribe...) - else { - xml = '' + priority.htmlEnc() + '' + show.htmlEnc() + '' + type.htmlEnc() + '' + status.htmlEnc() + '' + hasPhoto.htmlEnc() + '' + checksum.htmlEnc() + '' + caps.htmlEnc() + ''; - - DataStore.setDB(Connection.desktop_hash, 'presence-stanza', from, xml); - resources_obj = self.addResource(xid, resource); - } - - // We manage the presence - self.processPriority(xid, resource, resources_obj); - self.funnel(xid, xidHash); - - // We display the presence in the current chat - if(Common.exists('#' + xidHash)) { - var dStatus = self.filterStatus(xid, status, false); - - if(dStatus) - dStatus = ' (' + dStatus + ')'; - - // Generate the presence-in-chat code - var dName = Name.getBuddy(from).htmlEnc(); - var dBody = dName + ' (' + from + ') ' + Common._e("is now") + ' ' + self.humanShow(show, type) + dStatus; - - // Check whether it has been previously displayed - var can_display = true; - - if($('#' + xidHash + ' .one-line.system-message:last').html() == dBody) - can_display = false; - - if(can_display) - Message.display('chat', xid, xidHash, dName, dBody, DateUtils.getCompleteTime(), DateUtils.getTimeStamp(), 'system-message', false); - } - } - - // Get disco#infos for this presence (related to Caps) - Caps.getDiscoInfos(from, caps); - } - - // For logger - if(!show) { - if(!type) - show = 'available'; - else - show = 'unavailable'; - } - - Console.log('Presence received: ' + show + ', from ' + from); + Console.log('Presence received (type: ' + (type || 'available') + ', show: ' + (show || 'none') + ') from ' + from); } catch(e) { Console.error('Presence.handle', e); } @@ -307,12 +519,12 @@ var Presence = (function () { * @param {string} status_code * @param {string} iXID * @param {string} iNick - * @param {string} messageTime + * @param {string} message_time * @param {string} nick * @param {boolean} initial * @return {undefined} */ - self.displayMUC = function(from, roomHash, hash, type, show, status, affiliation, role, reason, status_code, iXID, iNick, messageTime, nick, initial) { + self.displayMUC = function(from, roomHash, hash, type, show, status, affiliation, role, reason, status_code, iXID, iNick, message_time, nick, initial) { try { // Generate the values @@ -325,14 +537,17 @@ var Presence = (function () { var notify = false; // Reset data? - if(!role) + if(!role) { role = 'participant'; - if(!affiliation) + } + if(!affiliation) { affiliation = 'none'; + } // Must update the role? - if(Common.exists(thisUser) && (($(thisUser).attr('data-role') != role) || ($(thisUser).attr('data-affiliation') != affiliation))) + if(Common.exists(thisUser) && (($(thisUser).attr('data-role') != role) || ($(thisUser).attr('data-affiliation') != affiliation))) { $(thisUser).remove(); + } // Any XID submitted? if(iXID) { @@ -403,18 +618,26 @@ var Presence = (function () { var user_affiliation = Groupchat.affiliationUser(room_xid, nick); if(user_affiliation.name == 'owner') { - hide_btns.push('promote'); - hide_btns.push('demote'); - hide_btns.push('kick'); + hide_btns.push( + 'promote', + 'demote', + 'kick' + ); } else if(user_affiliation.name === 'admin') { - hide_btns.push('promote'); - hide_btns.push('kick'); + hide_btns.push( + 'promote', + 'kick' + ); } else { - hide_btns.push('demote'); + hide_btns.push( + 'demote' + ); } if(Roster.isFriend(iXID)) { - hide_btns.push('add'); + hide_btns.push( + 'add' + ); } // Go Go Go!! @@ -465,10 +688,11 @@ var Presence = (function () { write += Common._e("joined the chat room"); // Any status? - if(status) + if(status) { write += ' (' + Filter.message(status, nick_html, true) + ')'; - else + } else { write += ' (' + Common._e("no status") + ')'; + } } // Enable the private chat input @@ -493,18 +717,21 @@ var Presence = (function () { notify = true; // Kicked? - if(Utils.existArrayValue(status_code, 307)) + if(Utils.existArrayValue(status_code, 307)) { write += Common._e("has been kicked"); + } // Banned? - if(Utils.existArrayValue(status_code, 301)) + if(Utils.existArrayValue(status_code, 301)) { write += Common._e("has been banned"); + } // Any reason? - if(reason) + if(reason) { write += ' (' + Filter.message(reason, nick_html, true) + ')'; - else + } else { write += ' (' + Common._e("no reason") + ')'; + } } // Nickname change? @@ -517,8 +744,9 @@ var Presence = (function () { var new_hash = hex_md5(new_xid); var new_class = 'user ' + new_hash; - if($(thisUser).hasClass('myself')) + if($(thisUser).hasClass('myself')) { new_class += ' myself'; + } // Die the click event $(thisUser).off('click'); @@ -544,10 +772,11 @@ var Presence = (function () { write += Common._e("left the chat room"); // Any status? - if(status) + if(status) { write += ' (' + Filter.message(status, nick_html, true) + ')'; - else + } else { write += ' (' + Common._e("no status") + ')'; + } } // Disable the private chat input @@ -555,8 +784,9 @@ var Presence = (function () { } // Must notify something - if(notify) - Message.display('groupchat', from, roomHash, nick_html, write, messageTime, DateUtils.getTimeStamp(), 'system-message', false); + if(notify) { + Message.display('groupchat', from, roomHash, nick_html, write, message_time, DateUtils.getTimeStamp(), 'system-message', false); + } // Set the good status show icon switch(show) { @@ -577,21 +807,26 @@ var Presence = (function () { var uTitle = nick; // Any XID to add? - if(iXID) + if(iXID) { uTitle += ' (' + iXID + ')'; + } // Any status to add? - if(status) + if(status) { uTitle += ' - ' + status; + } $(thisUser).attr('title', uTitle); // Show or hide the role category, depending of its content $('#' + roomHash + ' .list .role').each(function() { - if($(this).find('.user').size()) - $(this).show(); - else - $(this).hide(); + var this_sel = $(this); + + if(this_sel.find('.user').size()) { + this_sel.show(); + } else { + this_sel.hide(); + } }); } catch(e) { Console.error('Presence.displayMUC', e); @@ -615,9 +850,7 @@ var Presence = (function () { if(!status) { status = ''; - } - - else { + } else { if(cut) { dStatus = Utils.truncate(status, 50); } else { @@ -663,10 +896,11 @@ var Presence = (function () { $(path + ' .name .buddy-presence').replaceWith('

' + value + '

'); // The buddy presence in the buddy infos - if(dStatus) + if(dStatus) { biStatus = dStatus; - else + } else { biStatus = value; + } $(path + ' .bi-status').replaceWith('

' + biStatus + '

'); @@ -676,12 +910,14 @@ var Presence = (function () { buddy.addClass('hidden-buddy'); // No filtering is launched? - if(!Search.search_filtered) + if(!Search.search_filtered) { buddy.hide(); + } // All the buddies are shown? - if(Roster.blist_all) + if(Roster.blist_all) { buddy.show(); + } // Chat stuffs if(Common.exists('#' + hash)) { @@ -701,8 +937,9 @@ var Presence = (function () { buddy.removeClass('hidden-buddy'); // No filtering is launched? - if(!Search.search_filtered) + if(!Search.search_filtered) { buddy.show(); + } // Get the online buddy avatar if not a gateway Avatar.get(xid, 'cache', avatar, checksum); @@ -713,10 +950,11 @@ var Presence = (function () { // We generate a well formed status message if(dStatus) { // No need to write the same status two times - if(dStatus == value) + if(dStatus == value) { dStatus = ''; - else + } else { dStatus = ' (' + dStatus + ')'; + } } // We show the presence value @@ -768,8 +1006,9 @@ var Presence = (function () { // Process the left/right position var presence_h = 12; - if(pep_numb) + if(pep_numb) { presence_h = (pep_numb * 20) + 18; + } // Apply the left/right position var presence_h_tag = ($('html').attr('dir') == 'rtl') ? 'left' : 'right'; @@ -791,13 +1030,11 @@ var Presence = (function () { self.humanShow = function(show, type) { try { - if(type == 'unavailable') + if(type == 'unavailable') { show = Common._e("Unavailable"); - - else if(type == 'error') + } else if(type == 'error') { show = Common._e("Error"); - - else { + } else { switch(show) { case 'chat': show = Common._e("Talkative"); @@ -845,18 +1082,15 @@ var Presence = (function () { self.IA = function(type, show, status, hash, xid, avatar, checksum, caps) { try { - // Is there a status defined? - if(!status) - status = self.humanShow(show, type); + // Any status defined? + status = status || self.humanShow(show, type); - // Then we can handle the events - if(type == 'error') + // Handle events + if(type == 'error') { self.display(Common._e("Error"), 'error', show, status, hash, xid, avatar, checksum, caps); - - else if(type == 'unavailable') + } else if(type == 'unavailable') { self.display(Common._e("Unavailable"), 'unavailable', show, status, hash, xid, avatar, checksum, caps); - - else { + } else { switch(show) { case 'chat': self.display(Common._e("Talkative"), 'available', show, status, hash, xid, avatar, checksum, caps); @@ -952,20 +1186,29 @@ var Presence = (function () { from_highest = null; max_priority = null; - // Groupchat presence? (no priority here) - if(xid.indexOf('/') !== -1) { + // Groupchat or gateway presence? (no priority here) + if(xid.indexOf('/') !== -1 || Common.isGateway(xid)) { from_highest = xid; Console.log('Processed presence for groupchat user: ' + xid); } else { if(!self.highestPriority(xid)) { - from_highest = xid + '/' + resource; + from_highest = xid; + + if(resource) { + from_highest += '/' + resource; + } Console.log('Processed initial presence for regular user: ' + xid + ' (highest priority for: ' + (from_highest || 'none') + ')'); } else { - for(cur_resource in resources_obj) { + var fn_parse_resource = function(cur_resource) { // Read presence data - cur_from = xid + '/' + cur_resource; + cur_from = xid; + + if(cur_resource) { + cur_from += '/' + cur_resource; + } + cur_pr = DataStore.getDB(Connection.desktop_hash, 'presence-stanza', cur_from); if(cur_pr) { @@ -980,16 +1223,27 @@ var Presence = (function () { from_highest = cur_from; } } + }; + + // Parse bare presences (used by gateway contacts, mostly) + if(resources_obj.bare === 1) { + fn_parse_resource(null); + } + + // Parse resources + for(cur_resource in resources_obj.list) { + fn_parse_resource(cur_resource); } Console.log('Processed presence for regular user: ' + xid + ' (highest priority for: ' + (from_highest || 'none') + ')'); } } - if(from_highest) + if(from_highest) { DataStore.setDB(Connection.desktop_hash, 'presence-priority', xid, from_highest); - else + } else { DataStore.removeDB(Connection.desktop_hash, 'presence-priority', xid); + } } catch(e) { Console.error('Presence.processPriority', e); } @@ -1065,7 +1319,11 @@ var Presence = (function () { self.resources = function(xid) { try { - var resources_obj = {}; + var resources_obj = { + 'bare': 0, + 'list': {} + }; + var resources_db = DataStore.getDB(Connection.desktop_hash, 'presence-resources', xid); if(resources_db) { @@ -1094,7 +1352,12 @@ var Presence = (function () { try { resources_obj = self.resources(xid); - resources_obj[resource] = 1; + if(resource) { + resources_obj.list[resource] = 1; + } else { + resources_obj.bare = 1; + } + DataStore.setDB(Connection.desktop_hash, 'presence-resources', xid, $.toJSON(resources_obj)); } catch(e) { Console.error('Presence.addResource', e); @@ -1119,7 +1382,12 @@ var Presence = (function () { try { resources_obj = self.resources(xid); - delete resources_obj[resource]; + if(resource) { + delete resources_obj.list[resource]; + } else { + resources_obj.bare = 0; + } + DataStore.setDB(Connection.desktop_hash, 'presence-resources', xid, $.toJSON(resources_obj)); } catch(e) { Console.error('Presence.removeResource', e); @@ -1141,19 +1409,21 @@ var Presence = (function () { try { // Get the highest priority presence value - var xml = $(self.highestPriorityStanza(xid)); - var type = xml.find('type').text(); - var show = xml.find('show').text(); - var status = xml.find('status').text(); - var avatar = xml.find('avatar').text(); - var checksum = xml.find('checksum').text(); - var caps = xml.find('caps').text(); + var presence_node = $(self.highestPriorityStanza(xid)); + + var type = presence_node.find('type').text(); + var show = presence_node.find('show').text(); + var status = presence_node.find('status').text(); + var avatar = presence_node.find('avatar').text(); + var checksum = presence_node.find('checksum').text(); + var caps = presence_node.find('caps').text(); // Display the presence with that stored value - if(!type && !show) + if(!type && !show) { self.IA('', 'available', status, hash, xid, avatar, checksum, caps); - else + } else { self.IA(type, show, status, hash, xid, avatar, checksum, caps); + } } catch(e) { Console.error('Presence.funnel', e); } @@ -1178,23 +1448,25 @@ var Presence = (function () { try { // Get some stuffs - var priority = DataStore.getDB(Connection.desktop_hash, 'priority', 1); + var priority = DataStore.getDB(Connection.desktop_hash, 'priority', 1) || '1'; - if(!priority) - priority = '1'; - if(!checksum) - checksum = DataStore.getDB(Connection.desktop_hash, 'checksum', 1); - if(show == 'available') + checksum = checksum || DataStore.getDB(Connection.desktop_hash, 'checksum', 1); + + if(show == 'available') { show = ''; - if(type == 'available') + } + + if(type == 'available') { type = ''; + } // New presence var presence = new JSJaCPresence(); // Avoid "null" or "none" if nothing stored - if(!checksum || (checksum == 'none')) + if(!checksum || (checksum == 'none')) { checksum = ''; + } // Presence headers if(to) @@ -1209,55 +1481,100 @@ var Presence = (function () { presence.setPriority(priority); // CAPS (entity capabilities) - presence.appendNode('c', {'xmlns': NS_CAPS, 'hash': 'sha-1', 'node': 'http://jappix.org/', 'ver': Caps.mine()}); + presence.appendNode('c', { + 'xmlns': NS_CAPS, + 'hash': 'sha-1', + 'node': 'https://jappix.org/', + 'ver': Caps.mine() + }); // Nickname var nickname = Name.get(); - if(nickname && !limit_history) - presence.appendNode('nick', {'xmlns': NS_NICK}, nickname); + if(nickname && !limit_history) { + presence.appendNode('nick', { + 'xmlns': NS_NICK + }, nickname); + } // vcard-temp:x:update node - var x = presence.appendNode('x', {'xmlns': NS_VCARD_P}); - x.appendChild(presence.buildNode('photo', {'xmlns': NS_VCARD_P}, checksum)); + var x = presence.appendNode('x', { + 'xmlns': NS_VCARD_P + }); + + x.appendChild(presence.buildNode('photo', { + 'xmlns': NS_VCARD_P + }, checksum)); // MUC X data if(limit_history || password) { - var xMUC = presence.appendNode('x', {'xmlns': NS_MUC}); + var xMUC = presence.appendNode('x', { + 'xmlns': NS_MUC + }); // Max messages age (for MUC) - if(limit_history) - xMUC.appendChild(presence.buildNode('history', {'maxstanzas': 20, 'seconds': 86400, 'xmlns': NS_MUC})); + if(limit_history) { + xMUC.appendChild(presence.buildNode('history', { + 'maxstanzas': 20, + 'seconds': 86400, + 'xmlns': NS_MUC + })); + } // Room password - if(password) - xMUC.appendChild(presence.buildNode('password', {'xmlns': NS_MUC}, password)); + if(password) { + xMUC.appendChild(presence.buildNode('password', { + 'xmlns': NS_MUC + }, password)); + } + } + + // Reachability details + if(type != 'unavailable') { + var reach_regex = new RegExp('[^+0-9]', 'g'); + var reach_phone = DataStore.getDB(Connection.desktop_hash, 'profile', 'phone') || ''; + reach_phone = reach_phone.replace(reach_regex, ''); + + if(reach_phone) { + /* REF: http://www.xmpp.org/extensions/xep-0152.html */ + var reach_node = presence.appendNode(presence.buildNode('reach', { + 'xmlns': NS_URN_REACH + })); + + reach_node.appendChild( + presence.buildNode('addr', { + 'uri': 'tel:' + reach_phone, + 'xmlns': NS_URN_REACH + }) + ); + } } // If away, send a last activity time if((show == 'away') || (show == 'xa')) { /* REF: http://xmpp.org/extensions/xep-0256.html */ - presence.appendNode(presence.buildNode('query', { 'xmlns': NS_LAST, 'seconds': DateUtils.getPresenceLast() })); + + /* REF: http://xmpp.org/extensions/xep-0319.html */ + presence.appendNode(presence.buildNode('idle', { + 'xmlns': NS_URN_IDLE, + 'since': DateUtils.getLastActivityDate() + })); + } else { + DateUtils.presence_last_activity = DateUtils.getTimeStamp(); } - // Else, set a new last activity stamp - else - DateUtils.presence_last_activity = DateUtils.getTimeStamp(); - - // Send the presence packet - if(handle) + // Send presence packet + if(typeof handle == 'function') { con.send(presence, handle); - else + } else { con.send(presence); - - if(!type) - type = 'available'; - - Console.info('Presence sent: ' + type); + } + + Console.info('Presence sent: ' + (type || 'available')); } catch(e) { Console.error('Presence.send', e); } @@ -1280,8 +1597,9 @@ var Presence = (function () { var status = self.getUserStatus(); // Send the presence - if(!Utils.isAnonymous()) + if(!Utils.isAnonymous()) { self.send('', '', show, status, checksum); + } // We set the good icon self.icon(show); @@ -1294,19 +1612,22 @@ var Presence = (function () { $('.page-engine-chan[data-type="groupchat"]').each(function() { var tmp_nick = $(this).attr('data-nick'); - if(!tmp_nick) + if(!tmp_nick) { return; + } var room = unescape($(this).attr('data-xid')); var nick = unescape(tmp_nick); // Must re-initialize? - if(RESUME) + if(Connection.resume) { Groupchat.getMUC(room, nick); + } // Not disabled? - else if(!$(this).find('.message-area').attr('disabled')) + else if(!$(this).find('.message-area').attr('disabled')) { self.send(room + '/' + nick, '', show, status, '', true); + } }); } catch(e) { Console.error('Presence.quickSend', e); @@ -1345,8 +1666,9 @@ var Presence = (function () { var status = ''; // Subscribe request? - if(type == 'subscribe') + if(type == 'subscribe') { status = Common.printf(Common._e("Hi, I am %s, I would like to add you as my friend."), Name.get()); + } self.send(to, type, '', status); } catch(e) { @@ -1395,14 +1717,16 @@ var Presence = (function () { try { // Not connected? - if(!Common.isConnected()) + if(!Common.isConnected()) { return; + } // Stop if an xa presence was set manually var last_presence = self.getUserShow(); - if(!self.auto_idle && ((last_presence == 'away') || (last_presence == 'xa'))) + if(!self.auto_idle && ((last_presence == 'away') || (last_presence == 'xa'))) { return; + } var idle_presence; var activity_limit; @@ -1411,10 +1735,7 @@ var Presence = (function () { if(self.auto_idle && (last_presence == 'away')) { idle_presence = 'xa'; activity_limit = 1200; - } - - // We must set the user to auto-away (10 minutes) - else { + } else { idle_presence = 'away'; activity_limit = 600; } @@ -1425,11 +1746,8 @@ var Presence = (function () { self.auto_idle = true; // Get the old status message - var status = DataStore.getDB(Connection.desktop_hash, 'options', 'presence-status'); - - if(!status) - status = ''; - + var status = DataStore.getDB(Connection.desktop_hash, 'options', 'presence-status') || ''; + // Change the presence input $('#my-infos .f-presence a.picker').attr('data-value', idle_presence); $('#presence-status').val(status); @@ -1456,22 +1774,21 @@ var Presence = (function () { try { // If we were idle, restore our old presence if(self.auto_idle) { + var presence_status_sel = $('#presence-status'); + // Get the values var show = DataStore.getDB(Connection.desktop_hash, 'presence-show', 1); var status = DataStore.getDB(Connection.desktop_hash, 'options', 'presence-status'); // Change the presence input $('#my-infos .f-presence a.picker').attr('data-value', show); - $('#presence-status').val(status); - $('#presence-status').placeholder(); + presence_status_sel.val(status); + presence_status_sel.placeholder(); // Then restore the old presence self.sendActions('', true); - - if(!show) - show = 'available'; - - Console.info('Presence restored: ' + show); + + Console.info('Presence restored: ' + (show || 'available')); } // Apply some values @@ -1568,75 +1885,25 @@ var Presence = (function () { try { // Click event for user presence show $('#my-infos .f-presence a.picker').click(function() { - // Disabled? - if($(this).hasClass('disabled')) - return false; - - // Initialize some vars - var path = '#my-infos .f-presence div.bubble'; - var show_id = ['xa', 'away', 'available']; - var show_lang = [Common._e("Not available"), Common._e("Away"), Common._e("Available")]; - var show_val = self.getUserShow(); - - // Yet displayed? - var can_append = true; - - if(Common.exists(path)) - can_append = false; - - // Add this bubble! - Bubble.show(path); - - if(!can_append) - return false; - - // Generate the HTML code - var html = '
'; - - for(var i in show_id) { - // Yet in use: no need to display it! - if(show_id[i] == show_val) - continue; - - html += ''; - } - - html += '
'; - - // Append the HTML code - $('#my-infos .f-presence').append(html); - - // Click event - $(path + ' a').click(function() { - // Update the presence show marker - $('#my-infos .f-presence a.picker').attr('data-value', $(this).attr('data-value')); - - // Close the bubble - Bubble.close(); - - // Focus on the status input - $(document).oneTime(10, function() { - $('#presence-status').focus(); - }); - - return false; - }); - - return false; + return self._eventsPicker( + $(this) + ); }); // Submit events for user presence status - $('#presence-status').placeholder() + var presence_status_sel = $('#presence-status'); + + presence_status_sel.placeholder(); - .keyup(function(e) { + presence_status_sel.keyup(function(e) { if(e.keyCode == 13) { $(this).blur(); return false; } - }) + }); - .blur(function() { + presence_status_sel.blur(function() { // Read the parameters var show = self.getUserShow(); var status = self.getUserStatus(); @@ -1651,16 +1918,17 @@ var Presence = (function () { DataStore.setDB(Connection.desktop_hash, 'options', 'presence-status', status); // Update the server stored status - if(status != old_status) + if(status != old_status) { Options.store(); + } // Send the presence self.sendActions(); } - }) + }); // Input focus handler - .focus(function() { + presence_status_sel.focus(function() { Bubble.close(); }); } catch(e) { diff --git a/source/app/javascripts/privacy.js b/source/app/javascripts/privacy.js index e932fcb..fbbeb08 100644 --- a/source/app/javascripts/privacy.js +++ b/source/app/javascripts/privacy.js @@ -215,16 +215,18 @@ var Privacy = (function () { // Any block list? if($(iqQuery).find('list[name="block"]').size()) { // Not the default one? - if(!$(iqQuery).find('default[name="block"]').size()) + if(!$(iqQuery).find('default[name="block"]').size()) { self.change('block', 'default'); - else + } else { DataStore.setDB(Connection.desktop_hash, 'privacy-marker', 'default', 'block'); + } // Not the active one? - if(!$(iqQuery).find('active[name="block"]').size()) + if(!$(iqQuery).find('active[name="block"]').size()) { self.change('block', 'active'); - else + } else { DataStore.setDB(Connection.desktop_hash, 'privacy-marker', 'active', 'block'); + } // Get the block list rules self.get('block'); @@ -381,10 +383,11 @@ var Privacy = (function () { } con.send(iq, function(iq) { - if(iq.getType() == 'result') + if(iq.getType() == 'result') { Console.log('Sent privacy list.'); - else + } else { Console.error('Error sending privacy list.'); + } }); Console.log('Sending privacy list: ' + list); @@ -455,8 +458,9 @@ var Privacy = (function () { if(!c_order) c_order = ''; - if(!isNaN(c_order) && parseInt(c_order) > highest_order) + if(!isNaN(c_order) && parseInt(c_order) > highest_order) { highest_order = parseInt(c_order); + } type.push(c_type); value.push(c_value); @@ -464,25 +468,29 @@ var Privacy = (function () { order.push(c_order); // Child elements - if($(this).find('presence-in').size()) + if($(this).find('presence-in').size()) { presence_in.push(true); - else + } else { presence_in.push(false); + } - if($(this).find('presence-out').size()) + if($(this).find('presence-out').size()) { presence_out.push(true); - else + } else { presence_out.push(false); + } - if($(this).find('message').size()) + if($(this).find('message').size()) { msg.push(true); - else + } else { msg.push(false); + } - if($(this).find('iq').size()) + if($(this).find('iq').size()) { iq_p.push(true); - else + } else { iq_p.push(false); + } } }); @@ -508,8 +516,9 @@ var Privacy = (function () { try { // Yet sent? - if(DataStore.getDB(Connection.desktop_hash, 'privacy-marker', status) == list) + if(DataStore.getDB(Connection.desktop_hash, 'privacy-marker', status) == list) { return; + } // Write a marker DataStore.setDB(Connection.desktop_hash, 'privacy-marker', status, list); @@ -523,8 +532,9 @@ var Privacy = (function () { var iqStatus = iqQuery.appendChild(iq.buildNode(status, {'xmlns': NS_PRIVACY})); // Can add a "name" attribute? - if(list) + if(list) { iqStatus.setAttribute('name', list); + } con.send(iq); @@ -596,18 +606,20 @@ var Privacy = (function () { $(data).find('list').each(function() { var list_name = $(this).attr('name'); - if(list_name) + if(list_name) { code += ''; + } }); // Apply HTML code select.html(code); // Not empty? - if(code) + if(code) { select.removeAttr('disabled'); - else + } else { select.attr('disabled', true); + } } catch(e) { Console.error('Privacy.displayLists', e); } finally { @@ -638,8 +650,9 @@ var Privacy = (function () { select.html(''); // No list? - if(!list) + if(!list) { return false; + } // Reset the list status $('#privacy .privacy-active input[type="checkbox"]').removeAttr('checked'); @@ -648,8 +661,9 @@ var Privacy = (function () { var status = ['active', 'default']; for(var s in status) { - if(DataStore.getDB(Connection.desktop_hash, 'privacy-marker', status[s]) == list) + if(DataStore.getDB(Connection.desktop_hash, 'privacy-marker', status[s]) == list) { $('#privacy .privacy-active input[name=' + status[s] + ']').attr('checked', true); + } } // Try to read the stored items @@ -660,10 +674,9 @@ var Privacy = (function () { select.attr('disabled', true); return self.get(list); - } - - else + } else { select.removeAttr('disabled'); + } // Parse the XML data! $(items).find('item').each(function() { @@ -814,8 +827,9 @@ var Privacy = (function () { $(type_check).attr('checked', true); // Can apply a value? - if(value_input) + if(value_input) { $(value_input).val(value); + } // Apply the things to do var privacy_do = '#privacy .privacy-third input[type="checkbox"]'; @@ -924,8 +938,9 @@ var Privacy = (function () { var list = $('#privacy .privacy-head .list-left select').val(); // No value? - if(!list) + if(!list) { return false; + } // Remove it from popup $('#privacy .privacy-head .list-left select option[value="' + list + '"]').remove(); @@ -941,8 +956,9 @@ var Privacy = (function () { var status = ['active', 'default']; for(var s in status) { - if(DataStore.getDB(Connection.desktop_hash, 'privacy-marker', status[s]) == list) + if(DataStore.getDB(Connection.desktop_hash, 'privacy-marker', status[s]) == list) { self.change('', status[s]); + } } // Remove from server @@ -957,8 +973,9 @@ var Privacy = (function () { $('#privacy .privacy-head .list-right input').keyup(function(e) { // Not enter? - if(e.keyCode != 13) + if(e.keyCode != 13) { return; + } // Get list name var list = $('#privacy .privacy-head .list-right input').val(); @@ -1002,21 +1019,22 @@ var Privacy = (function () { // Display the data! self.displayForm( - item.attr('data-type'), - item.attr('data-value'), - item.attr('data-action'), - item.attr('data-order'), - item.attr('data-presence_in'), - item.attr('data-presence_out'), - item.attr('data-message'), - item.attr('data-iq') - ); + item.attr('data-type'), + item.attr('data-value'), + item.attr('data-action'), + item.attr('data-order'), + item.attr('data-presence_in'), + item.attr('data-presence_out'), + item.attr('data-message'), + item.attr('data-iq') + ); }); $('#privacy .privacy-item a.item-add').click(function() { // Cannot add anything? - if(!Common.exists('#privacy .privacy-head .list-left select option:selected')) + if(!Common.exists('#privacy .privacy-head .list-left select option:selected')) { return false; + } // Disable item select $('#privacy .privacy-item select').attr('disabled', true); @@ -1034,8 +1052,9 @@ var Privacy = (function () { $('#privacy .privacy-item a.item-remove').click(function() { // Cannot add anything? - if(!Common.exists('#privacy .privacy-head .list-left select option:selected')) + if(!Common.exists('#privacy .privacy-head .list-left select option:selected')) { return false; + } // Get values var list = $('#privacy .privacy-head .list-left select').val(); @@ -1055,15 +1074,17 @@ var Privacy = (function () { $('#privacy .privacy-head .list-left select option[value="' + list + '"]').remove(); // No more privacy lists? - if(!Common.exists('#privacy .privacy-head .list-left select option')) + if(!Common.exists('#privacy .privacy-head .list-left select option')) { $('#privacy .privacy-head .list-left select').attr('disabled', true); + } // Disable this list before removing it var status = ['active', 'default']; for(var s in status) { - if(DataStore.getDB(Connection.desktop_hash, 'privacy-marker', status[s]) == list) + if(DataStore.getDB(Connection.desktop_hash, 'privacy-marker', status[s]) == list) { self.change('', status[s]); + } } } @@ -1079,14 +1100,16 @@ var Privacy = (function () { $('#privacy .privacy-item a.item-save').click(function() { // Canot push item? - if(Common.exists('#privacy .privacy-form input:disabled')) + if(Common.exists('#privacy .privacy-form input:disabled')) { return false; + } // Get the hash var item_hash = ''; - if(!$('#privacy .privacy-item select').is(':disabled')) + if(!$('#privacy .privacy-item select').is(':disabled')) { item_hash = $('#privacy .privacy-item select option:selected').attr('data-hash'); + } // Read the form var privacy_second = '#privacy .privacy-second'; @@ -1142,17 +1165,17 @@ var Privacy = (function () { // Push item to the server! self.push( - item_list, - [item_type], - [item_value], - [item_action], - [item_order], - [item_prin], - [item_prout], - [item_msg], - [item_iq], - item_hash - ); + item_list, + [item_type], + [item_value], + [item_action], + [item_order], + [item_prin], + [item_prout], + [item_msg], + [item_iq], + item_hash + ); return false; }); @@ -1170,16 +1193,19 @@ var Privacy = (function () { var target = '#privacy .privacy-third input[type="checkbox"]'; // Must tick "everything" checkbox? - if(!$(target).filter(':checked').size()) + if(!$(target).filter(':checked').size()) { $(target + '[name="everything"]').attr('checked', true); + } // Must untick the other checkboxes? - else if($(this).is('[name="everything"]')) + else if($(this).is('[name="everything"]')) { $(target + ':not([name="everything"])').removeAttr('checked'); + } // Must untick "everything" checkbox? - else + else { $(target + '[name="everything"]').removeAttr('checked'); + } }); $('#privacy .privacy-active input[name="order"]').keyup(function() { @@ -1187,14 +1213,16 @@ var Privacy = (function () { var value = $(this).val(); // No value? - if(!value) + if(!value) { return; + } // Not a number? - if(isNaN(value)) + if(isNaN(value)) { value = 1; - else + } else { value = parseInt(value); + } // Negative? if(value < 0) @@ -1206,8 +1234,9 @@ var Privacy = (function () { .blur(function() { // No value? - if(!$(this).val()) + if(!$(this).val()) { $(this).val('1'); + } }); $('#privacy .privacy-active .privacy-active-elements input').change(function() { @@ -1216,14 +1245,16 @@ var Privacy = (function () { var state_name = $(this).attr('name'); // Cannot continue? - if(!list_name || !state_name) + if(!list_name || !state_name) { return; + } // Change the current list status - if($(this).filter(':checked').size()) + if($(this).filter(':checked').size()) { self.change(list_name, state_name); - else + } else { self.change('', state_name); + } }); } catch(e) { Console.error('Privacy.instance', e); diff --git a/source/app/javascripts/receipts.js b/source/app/javascripts/receipts.js index 4218f17..2843669 100644 --- a/source/app/javascripts/receipts.js +++ b/source/app/javascripts/receipts.js @@ -108,8 +108,9 @@ var Receipts = (function () { aMsg.setID(id); // Any type? - if(type) + if(type) { aMsg.setType(type); + } // Append the received node aMsg.appendNode('received', {'xmlns': NS_URN_RECEIPTS, 'id': id}); @@ -147,7 +148,7 @@ var Receipts = (function () { // Remove the group marker if(!group.find('.one-line[data-lost]').size()) { group.find('b.name').removeClass('talk-images') - .removeAttr('title'); + .removeAttr('title'); } } catch(e) { Console.error('Receipts.messageReceived', e); @@ -170,14 +171,19 @@ var Receipts = (function () { try { // Fire a check 10 seconds later $('#' + hash + ' .one-line[data-id="' + id + '"]').oneTime('10s', function() { + var this_sel = $(this); + // Not received? - if($(this).attr('data-received') != 'true') { + if(this_sel.attr('data-received') != 'true') { // Add a "lost" marker - $(this).attr('data-lost', 'true'); + this_sel.attr('data-lost', 'true'); // Add a warn on the buddy-name - $(this).parent().find('b.name').addClass('talk-images') - .attr('title', Common._e("Your friend seems not to have received your message(s)!")); + this_sel.parent().find('b.name').addClass('talk-images') + .attr( + 'title', + Common._e("Your friend seems not to have received your message(s)!") + ); } }); } catch(e) { diff --git a/source/app/javascripts/roster.js b/source/app/javascripts/roster.js index 01242e6..e483e4e 100644 --- a/source/app/javascripts/roster.js +++ b/source/app/javascripts/roster.js @@ -57,12 +57,12 @@ var Roster = (function () { // Parse the roster xml $(iq.getQuery()).find('item').each(function() { // Get user data - var _this = $(this); - var user_xid = _this.attr('jid'); - var user_subscription = _this.attr('subscription'); + var this_sel = $(this); + var user_xid = this_sel.attr('jid'); + var user_subscription = this_sel.attr('subscription'); // Parse roster data & display user - self.parse($(this), 'load'); + self.parse(this_sel, 'load'); // Request user microblog (populates channel) if(user_xid && ((user_subscription == 'both') || (user_subscription == 'to'))) { @@ -106,17 +106,20 @@ var Roster = (function () { current.find('group').each(function() { var group_text = $(this).text(); - if(group_text) + if(group_text) { groups.push(group_text); + } }); // No group? - if(!groups.length) + if(!groups.length) { groups.push(Common._e("Unclassified")); + } // If no name is defined, we get the default nick of the buddy - if(!dName) + if(!dName) { dName = Common.getXIDNick(xid); + } self.display(xid, xidHash, dName, subscription, groups, mode); } catch(e) { @@ -135,23 +138,28 @@ var Roster = (function () { try { $('#roster .one-group').each(function() { + var this_sel = $(this); + // Current values - var check = $(this).find('.buddy').size(); - var hidden = $(this).find('.buddy:not(.hidden-buddy:hidden)').size(); + var check = this_sel.find('.buddy').size(); + var hidden = this_sel.find('.buddy:not(.hidden-buddy:hidden)').size(); // Special case: the filtering tool - if(Search.search_filtered) - hidden = $(this).find('.buddy:visible').size(); + if(Search.search_filtered) { + hidden = this_sel.find('.buddy:visible').size(); + } // If the group is empty - if(!check) - $(this).remove(); + if(!check) { + this_sel.remove(); + } // If the group contains no online buddy (and is not just hidden) - if(!hidden && $(this).find('a.group').hasClass('minus')) - $(this).hide(); - else - $(this).show(); + if(!hidden && this_sel.find('a.group').hasClass('minus')) { + this_sel.hide(); + } else { + this_sel.show(); + } }); } catch(e) { Console.error('Roster.updateGroups', e); @@ -202,8 +210,9 @@ var Roster = (function () { var privacy_class = ''; var privacy_state = Privacy.status('block', dXID); - if(privacy_state == 'deny') + if(privacy_state == 'deny') { privacy_class = ' blocked'; + } // For each group this buddy has $.each(dGroup, function(i, cGroup) { @@ -214,8 +223,9 @@ var Roster = (function () { var groupBuddies = groupContent + ' .group-buddies'; // Is this group blocked? - if((Privacy.status('block', cGroup) == 'deny') && (privacy_state != 'allow')) + if((Privacy.status('block', cGroup) == 'deny') && (privacy_state != 'allow')) { privacy_class = ' blocked'; + } // Group not yet displayed if(!Common.exists(groupContent)) { @@ -272,13 +282,13 @@ var Roster = (function () { html += '
'; // Special gateway code - if(is_gateway) + if(is_gateway) { html += presence_code + name_code; - - else + } else { html += name_code + presence_code; + } html += '
'; @@ -374,8 +384,9 @@ var Roster = (function () { }); // Create a new checked checkbox - if(!group_exists) + if(!group_exists) { $(bm_choose).prepend(''); + } // Check the checkbox $(bm_choose + ' input[data-group="' + escaped_value + '"]').attr('checked', true); @@ -423,8 +434,9 @@ var Roster = (function () { Bubble.close(); // First unregister if gateway - if(Common.isGateway(xid)) + if(Common.isGateway(xid)) { self.unregisterGateway(xid); + } // Then send roster removal query self.send(xid, 'remove'); @@ -462,8 +474,9 @@ var Roster = (function () { // If the pointer is on a stored presence if(current.match(db_regex)) { - if(Common.bareXID(RegExp.$1) == xid) + if(Common.bareXID(RegExp.$1) == xid) { DataStore.storageDB.removeItem(current); + } } } @@ -510,13 +523,15 @@ var Roster = (function () { // Apply the hover event $(bPath).hover(function() { // Another bubble exist - if(Common.exists('#roster .buddy-infos')) + if(Common.exists('#roster .buddy-infos')) { return false; + } $(bPath).oneTime(200, function() { // Another bubble exist - if(Common.exists('#roster .buddy-infos')) + if(Common.exists('#roster .buddy-infos')) { return false; + } // Add this bubble! Bubble.show(iPath); @@ -552,36 +567,45 @@ var Roster = (function () { // Click events $(bPath + ' .bi-view a').click(function() { + var this_sel = $(this); + // Renitialize the buddy infos Bubble.close(); // Profile - if($(this).is('.profile')) + if(this_sel.is('.profile')) { UserInfos.open(xid); + } // Channel - else if($(this).is('.channel')) + else if(this_sel.is('.channel')) { Microblog.fromInfos(xid, hash); + } // Command - else if($(this).is('.commands')) + else if(this_sel.is('.commands')) { AdHoc.retrieve(xid); + } return false; }); // Jingle events $(bPath + ' .bi-jingle a').click(function() { + var this_sel = $(this); + // Renitialize the buddy infos Bubble.close(); // Audio call? - if($(this).is('.audio')) + if(this_sel.is('.audio')) { Jingle.start(xid, 'audio'); + } // Video call? - else if($(this).is('.video')) + else if(this_sel.is('.video')) { Jingle.start(xid, 'video'); + } return false; }); @@ -593,8 +617,9 @@ var Roster = (function () { }); }); }, function() { - if(!Common.exists(iPath + ' .manage-infos')) + if(!Common.exists(iPath + ' .manage-infos')) { Bubble.close(); + } $(bPath).stopTime(); }); @@ -623,8 +648,9 @@ var Roster = (function () { // Get the offset to define var offset = 3; - if(Common.isGateway(xid)) + if(Common.isGateway(xid)) { offset = -8; + } // Process the position var v_position = $(buddy).position().top + offset; @@ -634,10 +660,11 @@ var Roster = (function () { $(buddy_infos).css('top', v_position); // Apply the left/right position - if($('html').attr('dir') == 'rtl') + if($('html').attr('dir') == 'rtl') { $(buddy_infos).css('right', h_position); - else + } else { $(buddy_infos).css('left', h_position); + } } catch(e) { Console.error('Roster.buddyInfosPosition', e); } @@ -659,14 +686,17 @@ var Roster = (function () { // Each checked checkboxes $(path + 'div.bm-choose input[type="checkbox"]').filter(':checked').each(function() { - array.push(unescape($(this).attr('data-group'))); + array.push( + unescape($(this).attr('data-group')) + ); }); // Entered input value (and not yet in the array) var value = $.trim($(path + 'p.bm-group input').val()); - if(value && !Utils.existArrayValue(array, value)) + if(value && !Utils.existArrayValue(array, value)) { array.push(value); + } return array; } catch(e) { @@ -719,10 +749,13 @@ var Roster = (function () { var groups = []; $('#roster .one-group').each(function() { - var current = unescape($(this).attr('data-group')); + var current = unescape( + $(this).attr('data-group') + ); - if((current != Common._e("Unclassified")) && (current != Common._e("Gateways"))) + if((current != Common._e("Unclassified")) && (current != Common._e("Gateways"))) { groups.push(current); + } }); return groups.sort(); @@ -757,8 +790,9 @@ var Roster = (function () { // Get the group privacy state for(var g in groups) { - if((Privacy.status('block', groups[g]) == 'deny') && (privacy_state != 'allow')) + if((Privacy.status('block', groups[g]) == 'deny') && (privacy_state != 'allow')) { privacy_state = 'deny'; + } } // The subscription with this buddy is not full @@ -767,21 +801,24 @@ var Roster = (function () { html += '

'; // Link to allow to see our status - if((subscription == 'to') || (subscription == 'none')) + if((subscription == 'to') || (subscription == 'none')) { authorize_links += '' + Common._e("Authorize") + ''; + } // Link to ask to see his/her status if((subscription == 'from') || (subscription == 'none')) { - if(authorize_links) + if(authorize_links) { authorize_links += ' / '; + } authorize_links += '' + Common._e("Ask for authorization") + ''; } // Link to unblock this buddy if((privacy_state == 'deny') && privacy_active) { - if(authorize_links) + if(authorize_links) { authorize_links += ' / '; + } html += '' + Common._e("Unblock") + ''; } @@ -795,13 +832,15 @@ var Roster = (function () { remove_links = '' + Common._e("Remove") + ''; // This buddy is allowed to see our presence, we can show a "prohibit" link - if((subscription == 'both') || (subscription == 'from')) + if((subscription == 'both') || (subscription == 'from')) { remove_links += ' / ' + Common._e("Prohibit") + ''; + } // Complete the HTML code if((privacy_state != 'deny') && privacy_active) { - if(remove_links) + if(remove_links) { remove_links += ' / '; + } remove_links += '' + Common._e("Block") + ''; } @@ -812,11 +851,12 @@ var Roster = (function () { '

'; // Only show group tool if not a gateway - if(!Common.isGateway(xid)) + if(!Common.isGateway(xid)) { html += '

' + '
' + '
' + '
'; + } // Close the DOM element html += '' + Common._e("Save") + '' + @@ -836,8 +876,9 @@ var Roster = (function () { // Is the current group checked? var checked = ''; - if(Utils.existArrayValue(groups, all_groups_current)) + if(Utils.existArrayValue(groups, all_groups_current)) { checked = ' checked="true"'; + } // Add the current group HTML all_groups_dom += ''; @@ -901,17 +942,20 @@ var Roster = (function () { var item = iqQuery.appendChild(iq.buildNode('item', {'xmlns': NS_ROSTER, 'jid': xid})); // Any subscription? - if(subscription) + if(subscription) { item.setAttribute('subscription', subscription); + } // Any name? - if(name) + if(name) { item.setAttribute('name', name); + } // Any group? if(group && group.length) { - for(var i in group) + for(var i in group) { item.appendChild(iq.buildNode('group', {'xmlns': NS_ROSTER}, group[i])); + } } con.send(iq); @@ -936,8 +980,9 @@ var Roster = (function () { var new_height = $('#left-content').height() - $('#my-infos').height() - 97; // New height too small - if(new_height < 211) + if(new_height < 211) { new_height = 211; + } // Apply the new height $('#roster .content').css('height', new_height); @@ -1034,10 +1079,11 @@ var Roster = (function () { .blur(function() { // Nothing is entered, put the placeholder instead - if(!$.trim($(this).val())) + if(!$.trim($(this).val())) { aFilter.hide(); - else + } else { aFilter.show(); + } }) .keyup(function(e) { @@ -1062,8 +1108,9 @@ var Roster = (function () { // When the user click on the add button, show the contact adding tool $('#roster .foot .add').click(function() { // Yet displayed? - if(Common.exists('#buddy-conf-add')) + if(Common.exists('#buddy-conf-add')) { return Bubble.close(); + } // Add the bubble Bubble.show('#buddy-conf-add'); @@ -1137,25 +1184,28 @@ var Roster = (function () { var gateway = unescape($('.add-contact-gateway').val()); // Generate the XID to add - if((gateway != 'none') && xid) + if((gateway != 'none') && xid) { xid = xid.replace(/@/g, '%') + '@' + gateway; - else + } else { xid = Common.generateXID(xid, 'chat'); + } // Submit the form - if(xid && Common.getXIDNick(xid) && (xid != Common.getXID())) + if(xid && Common.getXIDNick(xid) && (xid != Common.getXID())) { self.addThisContact(xid, name); - else + } else { $(document).oneTime(10, function() { $('.add-contact-jid').addClass('please-complete').focus(); }); - + } + return false; } // Escape : quit - if(e.keyCode == 27) + if(e.keyCode == 27) { Bubble.close(); + } }); // Click event on search link @@ -1175,8 +1225,9 @@ var Roster = (function () { // When the user click on the join button, show the chat joining tool $('#roster .foot .join').click(function() { // Yet displayed? - if(Common.exists('#buddy-conf-join')) + if(Common.exists('#buddy-conf-join')) { return Bubble.close(); + } // Add the bubble Bubble.show('#buddy-conf-join'); @@ -1209,10 +1260,7 @@ var Roster = (function () { // Select something from the search if(Common.exists(dHovered)) { Search.addBuddy(destination, $(dHovered).attr('data-xid')); - } - - // Join something - else { + } else { var xid = $.trim($('.join-jid').val()); var type = $('.buddy-conf-join-select').val(); @@ -1228,14 +1276,10 @@ var Roster = (function () { // Create a new chat Chat.checkCreate(xid, type); - } - - else { + } else { $('.join-jid').addClass('please-complete'); } - } - - else { + } else { $('.join-jid').addClass('please-complete'); } } @@ -1250,8 +1294,9 @@ var Roster = (function () { // Buddy search? else if($('.buddy-conf-join-select').val() == 'chat') { // New buddy search - if((e.keyCode != 40) && (e.keyCode != 38)) + if((e.keyCode != 40) && (e.keyCode != 38)) { Search.createBuddy(destination); + } // Navigating with keyboard in the results Search.arrowsBuddy(e, destination); @@ -1282,8 +1327,9 @@ var Roster = (function () { // When the user click on the groupchat button, show the groupchat menu $('#roster .foot .groupchat').click(function() { // Yet displayed? - if(Common.exists('#buddy-conf-groupchat')) + if(Common.exists('#buddy-conf-groupchat')) { return Bubble.close(); + } // Add the bubble Bubble.show('#buddy-conf-groupchat'); @@ -1334,12 +1380,61 @@ var Roster = (function () { return false; }); + + // When the user click on the muji button, show the muji menu + $('#roster .foot .muji').click(function() { + // Yet displayed? + if(Common.exists('#buddy-conf-muji') || Call.is_ongoing()) { + return Bubble.close(); + } + + // Add the bubble + Bubble.show('#buddy-conf-muji'); + + // Append the content + $('#roster .roster-muji').append( + '
' + + '
' + + + '
' + + '

' + Common._e("Launch a group call") + '

' + + + '

' + + '- ' + Common._e("Audio conference") + '' + + '

' + + + '

' + + '- ' + Common._e("Video conference") + '' + + '

' + + '
' + + '
' + ); + + // When the user wants to launch + $('.buddy-conf-muji-conference').click(function() { + var media = $(this).attr('data-media'); + + var room_name = hex_md5(media + DateUtils.getTimeStamp() + Math.random()); + var room = Common.generateXID(room_name, 'groupchat'); + + if(media && room && room_name) { + Muji.start(room, media); + } + + Bubble.close(); + + return false; + }); + + return false; + }); // When the user click on the more button, show the more menu $('#roster .foot .more').click(function() { // Yet displayed? - if(Common.exists('#buddy-conf-more')) + if(Common.exists('#buddy-conf-more')) { return Bubble.close(); + } // Add the bubble Bubble.show('#buddy-conf-more'); @@ -1410,11 +1505,13 @@ var Roster = (function () { $('.buddy-conf-more-display-available').show(); } - if(Features.enabledCommands()) + if(Features.enabledCommands()) { $('.buddy-conf-more-commands').parent().show(); + } - if(DataStore.getDB(Connection.desktop_hash, 'privacy-marker', 'available')) + if(DataStore.getDB(Connection.desktop_hash, 'privacy-marker', 'available')) { $('.buddy-conf-more-privacy').parent().show(); + } return false; }); diff --git a/source/app/javascripts/rosterx.js b/source/app/javascripts/rosterx.js index adadc2f..5a160b5 100644 --- a/source/app/javascripts/rosterx.js +++ b/source/app/javascripts/rosterx.js @@ -97,34 +97,44 @@ var RosterX = (function () { // Parse data x.find('item').each(function() { + var this_sel = $(this); + // Generate group XML var group = ''; - $(this).find('group').each(function() { - group += '' + $(this).text().htmlEnc() + ''; + this_sel.find('group').each(function() { + group += '' + this_sel.text().htmlEnc() + ''; }); - if(group) + if(group) { group = '' + group + ''; + } // Display it! - self.display($(this).attr('jid'), $(this).attr('name'), group, $(this).attr('action')); + self.display( + this_sel.attr('jid'), + this_sel.attr('name'), + group, + this_sel.attr('action') + ); }); // Click to check/uncheck $('#rosterx .oneresult').click(function(evt) { // No need to apply when click on input - if($(evt.target).is('input[type="checkbox"]')) + if($(evt.target).is('input[type="checkbox"]')) { return; + } // Input selector var checkbox = $(this).find('input[type="checkbox"]'); // Check or uncheck? - if(checkbox.filter(':checked').size()) + if(checkbox.filter(':checked').size()) { checkbox.removeAttr('checked'); - else + } else { checkbox.attr('checked', true); + } }); } catch(e) { Console.error('RosterX.parse', e); @@ -146,16 +156,19 @@ var RosterX = (function () { try { // End if no XID - if(!xid) + if(!xid) { return false; + } // Set up a default action if no one - if(!action || (action != 'modify') || (action != 'delete')) + if(!action || (action != 'modify') || (action != 'delete')) { action = 'add'; + } // Override "undefined" for nickname - if(!nick) + if(!nick) { nick = ''; + } // Display it $('#rosterx .results').append( @@ -178,25 +191,27 @@ var RosterX = (function () { /** * Saves the rosterx settings * @public - * @return {undefined} + * @return {boolean} */ self.save = function() { try { // Send the requests $('#rosterx .results input[type="checkbox"]').filter(':checked').each(function() { + var this_sel = $(this); + // Read the attributes - var nick = $(this).attr('data-name'); - var xid = $(this).attr('data-xid'); - var action = $(this).attr('data-action'); - var group = $(this).attr('data-group'); + var nick = this_sel.attr('data-name'); + var xid = this_sel.attr('data-xid'); + var action = this_sel.attr('data-action'); + var group = this_sel.attr('data-group'); // Parse groups XML var group_arr = []; if(group) { $(group).find('group').each(function() { - group_arr.push($(this).text().revertHtmlEnc()); + group_arr.push(this_sel.text().revertHtmlEnc()); }); } @@ -233,6 +248,8 @@ var RosterX = (function () { self.close(); } catch(e) { Console.error('RosterX.save', e); + } finally { + return false; } }; @@ -248,17 +265,25 @@ var RosterX = (function () { try { // Click events $('#rosterx .bottom .finish').click(function() { - if($(this).is('.save')) + var this_sel = $(this); + + if(this_sel.is('.save')) { return self.save(); - if($(this).is('.cancel')) + } + + if(this_sel.is('.cancel')) { return self.close(); + } }); $('#rosterx .rosterx-head a').click(function() { - if($(this).is('.check')) + var this_sel = $(this); + + if(this_sel.is('.check')) { $('#rosterx .results input[type="checkbox"]').attr('checked', true); - else if($(this).is('.uncheck')) + } else if(this_sel.is('.uncheck')) { $('#rosterx .results input[type="checkbox"]').removeAttr('checked'); + } return false; }); diff --git a/source/app/javascripts/search.js b/source/app/javascripts/search.js index ef2e6f7..e73dcce 100644 --- a/source/app/javascripts/search.js +++ b/source/app/javascripts/search.js @@ -112,8 +112,9 @@ var Search = (function () { // Get the old value (if there's another value) var old = ''; - if(value.match(/(^(.+)(,)(\s)?)(\w+)$/)) + if(value.match(/(^(.+)(,)(\s)?)(\w+)$/)) { old = RegExp.$1; + } // Add the XID to the "to" input and focus on it $(document).oneTime(10, function() { @@ -144,8 +145,9 @@ var Search = (function () { var value = $(destination + ' input').val(); // Separation with a comma? - if(value.match(/^(.+)((,)(\s)?)(\w+)$/)) + if(value.match(/^(.+)((,)(\s)?)(\w+)$/)) { value = RegExp.$5; + } // Get the result array var entered = self.processBuddy(value); @@ -217,8 +219,9 @@ var Search = (function () { var code = evt.keyCode; // Not the key we want here - if((code != 40) && (code != 38)) + if((code != 40) && (code != 38)) { return; + } // Remove the eventual mouse hover marker $(destination + ' ul').removeAttr('mouse-hover'); @@ -234,25 +237,25 @@ var Search = (function () { if(Common.exists(path + '.hovered')) { var index = $(path).attr('data-hovered'); - if(index) + if(index) { i = parseInt(index); + } - if(code == 40) + if(code == 40) { i++; - else + } else { i--; + } + } else if(code == 38) { + i = pSize - 1; } - else if(code == 38) - i = pSize - 1; - // We must not override the maximum i limit - if(i >= pSize) + if(i >= pSize) { i = 0; - - // We must not have negative i - else if(i < 0) + } else if(i < 0) { i = pSize - 1; + } // Modify the list $(path + '.hovered').removeClass('hovered'); @@ -289,11 +292,13 @@ var Search = (function () { // Only show the buddies which match the search if(!Roster.blist_all) { - for(var i in rFilter) + for(var i in rFilter) { $('#roster .buddy[data-xid="' + escape(rFilter[i]) + '"]:not(.hidden-buddy)').show(); + } } else { - for(var j in rFilter) + for(var j in rFilter) { $('#roster .buddy[data-xid="' + escape(rFilter[j]) + '"]').show(); + } } } catch(e) { Console.error('Search.goFilterBuddy', e); @@ -317,8 +322,9 @@ var Search = (function () { $('#roster .buddy').show(); // Only show available buddies - if(!Roster.blist_all) + if(!Roster.blist_all) { $('#roster .buddy.hidden-buddy').hide(); + } // Update the groups Roster.updateGroups(); @@ -349,15 +355,13 @@ var Search = (function () { // Nothing is entered, or escape pressed if(!value || (keycode == 27)) { - if(keycode == 27) + if(keycode == 27) { input.val(''); + } self.resetFilterBuddy(); cancel.hide(); - } - - // Process the filtering - else { + } else { cancel.show(); self.goFilterBuddy(value); } diff --git a/source/app/javascripts/smileys.js b/source/app/javascripts/smileys.js index 50920c9..99fc037 100644 --- a/source/app/javascripts/smileys.js +++ b/source/app/javascripts/smileys.js @@ -20,6 +20,51 @@ var Smileys = (function () { var self = {}; + /* Constants */ + self.emote_list = { + 'biggrin': ':-D', + 'devil': ']:->', + 'coolglasses': '8-)', + 'tongue': ':-P', + 'smile': ':-)', + 'wink': ';-)', + 'blush': ':-$', + 'stare': ':-|', + 'frowning': ':-/', + 'oh': '=-O', + 'unhappy': ':-(', + 'cry': ':\'-(', + 'angry': ':-@', + 'puke': ':-!', + 'hugright': '({)', + 'hugleft': '(})', + 'lion': ':3', + 'pussy': '(@)', + 'bat': ':-[', + 'kiss': ':-{}', + 'heart': '<3', + 'brheart': '--', + 'brflower': '(W)', + 'thumbup': '(Y)', + 'thumbdown': '(N)', + 'lamp': '(I)', + 'coffee': '(C)', + 'drink': '(D)', + 'beer': '(B)', + 'boy': '(Z)', + 'girl': '(X)', + 'photo': '(P)', + 'phone': '(T)', + 'music': '(8)', + 'cuffs': '(%)', + 'mail': '(E)', + 'rainbow': '(R)', + 'star': '(*)', + 'moon': '(S)' + }; + + /** * Generates the correct HTML code for an emoticon insertion tool * @public @@ -43,101 +88,19 @@ var Smileys = (function () { * Emoticon links arrays * @public * @param {string} hash - * @return {object} + * @return {string} */ self.links = function(hash) { try { var links = ''; - - var sArray = [ - ':-D', - ']:->', - '8-)', - ':-P', - ':-)', - ';-)', - ':-$', - ':-|', - ':-/', - '=-O', - ':-(', - ':\'-(', - ':-@', - ':-!', - '({)', - '(})', - ':3', - '(@)', - ':-[', - ':-{}', - '<3', - '--', - '(W)', - '(Y)', - '(N)', - '(I)', - '(C)', - '(D)', - '(B)', - '(Z)', - '(X)', - '(P)', - '(T)', - '(8)', - '(%)', - '(E)', - '(R)', - '(*)', - '(S)' - ]; - - var cArray = [ - 'biggrin', - 'devil', - 'coolglasses', - 'tongue', - 'smile', - 'wink', - 'blush', - 'stare', - 'frowning', - 'oh', - 'unhappy', - 'cry', - 'angry', - 'puke', - 'hugright', - 'hugleft', - 'lion', - 'pussy', - 'bat', - 'kiss', - 'heart', - 'brheart', - 'flower', - 'brflower', - 'thumbup', - 'thumbdown', - 'lamp', - 'coffee', - 'drink', - 'beer', - 'boy', - 'girl', - 'photo', - 'phone', - 'music', - 'cuffs', - 'mail', - 'rainbow', - 'star', - 'moon' - ]; - - for(var i in sArray) { - links += self.emoteLink(sArray[i], cArray[i], hash); + + for(var cur_emote in self.emote_list) { + links += self.emoteLink( + self.emote_list[cur_emote], + cur_emote, + hash + ); } return links; diff --git a/source/app/javascripts/storage.js b/source/app/javascripts/storage.js index 22d2726..9506d5b 100644 --- a/source/app/javascripts/storage.js +++ b/source/app/javascripts/storage.js @@ -70,44 +70,51 @@ var Storage = (function () { // Parse the options xml options.find('option').each(function() { + var this_sel = $(this); + // We retrieve the informations - var type = $(this).attr('type'); - var value = $(this).text(); + var type = this_sel.attr('type'); + var value = this_sel.text(); // We display the storage DataStore.setDB(Connection.desktop_hash, 'options', type, value); // If this is the buddy list show status - if((type == 'roster-showall') && (value == '1')) + if((type == 'roster-showall') && (value == '1')) { Interface.showAllBuddies('storage'); + } }); // Parse the inbox xml inbox.find('message').each(function() { + var this_sel = $(this); + Inbox.storeMessage( - $(this).attr('from'), - $(this).attr('subject'), - $(this).text(), - $(this).attr('status'), - $(this).attr('id'), - $(this).attr('date'), - [ - $(this).attr('file_title'), - $(this).attr('file_href'), - $(this).attr('file_type'), - $(this).attr('file_length') - ] - ); + this_sel.attr('from'), + this_sel.attr('subject'), + this_sel.text(), + this_sel.attr('status'), + this_sel.attr('id'), + this_sel.attr('date'), + [ + this_sel.attr('file_title'), + this_sel.attr('file_href'), + this_sel.attr('file_type'), + this_sel.attr('file_length') + ] + ); }); // Parse the bookmarks xml bookmarks.find('conference').each(function() { + var this_sel = $(this); + // We retrieve the informations - var xid = $(this).attr('jid'); - var name = $(this).attr('name'); - var autojoin = $(this).attr('autojoin'); - var password = $(this).find('password').text(); - var nick = $(this).find('nick').text(); + var xid = this_sel.attr('jid'); + var name = this_sel.attr('name'); + var autojoin = this_sel.attr('autojoin'); + var password = this_sel.find('password').text(); + var nick = this_sel.find('nick').text(); // Filter autojoin (compatibility) autojoin = ((autojoin == 'true') || (autojoin == '1')) ? 'true' : 'false'; @@ -116,13 +123,21 @@ var Storage = (function () { Favorites.display(xid, name, nick, autojoin, password); // Join the chat if autojoin is enabled - if(autojoin == 'true') + if(autojoin == 'true') { Chat.checkCreate(xid, 'groupchat', nick, password, name); + } }); // Parse the roster notes xml rosternotes.find('note').each(function() { - DataStore.setDB(Connection.desktop_hash, 'rosternotes', $(this).attr('jid'), $(this).text()); + var this_sel = $(this); + + DataStore.setDB( + Connection.desktop_hash, + 'rosternotes', + this_sel.attr('jid'), + this_sel.text() + ); }); // Options received diff --git a/source/app/javascripts/system.js b/source/app/javascripts/system.js index 4416581..4397da1 100644 --- a/source/app/javascripts/system.js +++ b/source/app/javascripts/system.js @@ -31,14 +31,18 @@ var System = (function () { var url = window.location.href; // If the URL has variables, remove them - if(url.indexOf('?') != -1) + if(url.indexOf('?') != -1) { url = url.split('?')[0]; - if(url.indexOf('#') != -1) + } + + if(url.indexOf('#') != -1) { url = url.split('#')[0]; + } // No "/" at the end - if(!url.match(/(.+)\/$/)) + if(!url.match(/(.+)\/$/)) { url += '/'; + } return url; } catch(e) { diff --git a/source/app/javascripts/talk.js b/source/app/javascripts/talk.js index 7a6f2e1..8809c80 100644 --- a/source/app/javascripts/talk.js +++ b/source/app/javascripts/talk.js @@ -51,8 +51,9 @@ var Talk = (function () { try { // Talkpage exists? - if(Common.exists('#talk')) + if(Common.exists('#talk')) { return false; + } // Anonymous detector var anonymous = Utils.isAnonymous(); @@ -116,14 +117,14 @@ var Talk = (function () { if(!anonymous) html += '
' + - '
' + + '
' + '' + '00:00:00' + - '' + Common._e("Stop") + '' + + '' + Common._e("Stop") + '' + '' + '
' + - '
' + + '
' + '
' + '
' + @@ -157,6 +158,10 @@ var Talk = (function () { '' + '
' + + '
' + + '' + + '
' + + '
' + '' + '
' + diff --git a/source/app/javascripts/tooltip.js b/source/app/javascripts/tooltip.js index c3bdc39..54a8a4d 100644 --- a/source/app/javascripts/tooltip.js +++ b/source/app/javascripts/tooltip.js @@ -37,8 +37,9 @@ var Tooltip = (function () { var path_bubble = path_tooltip + ' .bubble-' + type; // Yet exists? - if(Common.exists(path_bubble)) + if(Common.exists(path_bubble)) { return false; + } // Generates special tooltip HTML code var title = ''; @@ -211,47 +212,52 @@ var Tooltip = (function () { // Click event on style bubble $(bubble_style).click(function() { // Hide font selector if opened - if($(font_list).is(':visible')) + if($(font_list).is(':visible')) { $(font_current).click(); + } // Hide font-size selector if opened - if($(fontsize_list).is(':visible')) + if($(fontsize_list).is(':visible')) { $(fontsize_current).click(); + } // Hide color selector if opened - if($(color_hex).is(':visible')) + if($(color_hex).is(':visible')) { $(color_more).click(); + } }); // Click event on font picker $(font_current).click(function() { + var this_sel = $(this); + // The clicked color is yet selected - if($(font_list).is(':visible')) - $(this).parent().removeClass('listed'); - else - $(this).parent().addClass('listed'); + if($(font_list).is(':visible')) { + this_sel.parent().removeClass('listed'); + } else { + this_sel.parent().addClass('listed'); + } return false; }); // Click event on a new font in the picker $(font_select).click(function() { + var this_sel = $(this); + // No font selected - if(!$(this).attr('data-value')) { + if(!this_sel.attr('data-value')) { $(font_current).removeAttr('data-font') .removeAttr('data-value') .text(Common._e("None")); $(message_area).removeAttr('data-font'); - } - - // A font is defined - else { - $(font_current).attr('data-font', $(this).attr('data-font')) - .attr('data-value', $(this).attr('data-value')) - .text($(font_list).find('a[data-value="' + $(this).attr('data-value') + '"]').text()); + } else { + $(font_current).attr('data-font', this_sel.attr('data-font')) + .attr('data-value', this_sel.attr('data-value')) + .text($(font_list).find('a[data-value="' + this_sel.attr('data-value') + '"]').text()); - $(message_area).attr('data-font', $(this).attr('data-value')); + $(message_area).attr('data-font', this_sel.attr('data-value')); } return false; @@ -259,28 +265,33 @@ var Tooltip = (function () { // Click event on font-size picker $(fontsize_current).click(function() { + var this_sel = $(this); + // The clicked color is yet selected - if($(fontsize_list).is(':visible')) - $(this).parent().removeClass('listed'); - else - $(this).parent().addClass('listed'); + if($(fontsize_list).is(':visible')) { + this_sel.parent().removeClass('listed'); + } else { + this_sel.parent().addClass('listed'); + } return false; }); // Click event on a new font-size in the picker $(fontsize_select).click(function() { + var this_sel = $(this); + // No font-size selected - if(!$(this).attr('data-value')) { + if(!this_sel.attr('data-value')) { $(fontsize_current).removeAttr('data-value').text(Common._e("16")); $(message_area).removeAttr('data-fontsize'); } // A font-size is defined else { - $(fontsize_current).attr('data-value', $(this).attr('data-value')) - .text($(this).attr('data-value')); - $(message_area).attr('data-fontsize', $(this).attr('data-value')); + $(fontsize_current).attr('data-value', this_sel.attr('data-value')) + .text(this_sel.attr('data-value')); + $(message_area).attr('data-fontsize', this_sel.attr('data-value')); } return false; @@ -288,19 +299,21 @@ var Tooltip = (function () { // Click event on color picker $(colors).click(function() { + var this_sel = $(this); + // Reset the manual picker $(color_hex).find('input').val(''); // The clicked color is yet selected - if($(this).hasClass('selected')) { + if(this_sel.hasClass('selected')) { $(message_area).removeAttr('data-color'); - $(this).removeClass('selected'); + this_sel.removeClass('selected'); } else { - $(message_area).attr('data-color', $(this).attr('data-color')); + $(message_area).attr('data-color', this_sel.attr('data-color')); $(colors).removeClass('selected'); - $(this).addClass('selected'); + this_sel.addClass('selected'); } return false; @@ -308,12 +321,13 @@ var Tooltip = (function () { // Click event on color picker $(color_more).click(function() { + var this_sel = $(this); + // The clicked color is yet selected - if($(color_hex).is(':visible')) - $(this).parent().removeClass('opened'); - - else { - $(this).parent().addClass('opened'); + if($(color_hex).is(':visible')) { + this_sel.parent().removeClass('opened'); + } else { + this_sel.parent().addClass('opened'); // Focus $(document).oneTime(10, function() { @@ -331,6 +345,8 @@ var Tooltip = (function () { // Keyup event on color picker $(color_hex).find('input').keyup(function(e) { + var this_sel = $(this); + // Submit if(e.keyCode == 13) { if($(color_hex).is(':visible')) { @@ -350,32 +366,37 @@ var Tooltip = (function () { $(colors).removeClass('selected'); // Change value - var new_value = $(this).val().replace(/([^a-z0-9]+)/gi, ''); - $(this).val(new_value); + var new_value = this_sel.val().replace(/([^a-z0-9]+)/gi, ''); + this_sel.val(new_value); - if(new_value) + if(new_value) { $(message_area).attr('data-color', new_value); + } // Regenerate style var style = Message.generateStyle(hash); // Any style to apply? - if(style) + if(style) { $(message_area).attr('style', style); - else + } else { $(message_area).removeAttr('style'); + } }).placeholder(); // Change event on text style checkboxes $(style).change(function() { + var this_sel = $(this); + // Get current type - var style_data = 'data-' + $(this).attr('class'); + var style_data = 'data-' + this_sel.attr('class'); // Checked checkbox? - if($(this).filter(':checked').size()) + if(this_sel.filter(':checked').size()) { $(message_area).attr(style_data, true); - else + } else { $(message_area).removeAttr(style_data); + } }); // Update the textarea style when it is changed @@ -383,10 +404,11 @@ var Tooltip = (function () { var style = Message.generateStyle(hash); // Any style to apply? - if(style) + if(style) { $(message_area).attr('style', style); - else + } else { $(message_area).removeAttr('style'); + } // Focus again on the message textarea $(document).oneTime(10, function() { @@ -410,24 +432,31 @@ var Tooltip = (function () { // Upload form submit event $(path_tooltip + ' #oob-upload').submit(function() { - if($(path_tooltip + ' #oob-upload input[type="file"]').val()) - $(this).ajaxSubmit(oob_upload_options); + var this_sel = $(this); + + if($(path_tooltip + ' #oob-upload input[type="file"]').val()) { + this_sel.ajaxSubmit(oob_upload_options); + } return false; }); // Upload input change event $(path_tooltip + ' #oob-upload input[type="file"]').change(function() { - if($(this).val()) + var this_sel = $(this); + + if(this_sel.val()) { $(path_tooltip + ' #oob-upload').ajaxSubmit(oob_upload_options); + } return false; }); // Input click event $(path_tooltip + ' #oob-upload input[type="file"], ' + path_tooltip + ' #oob-upload input[type="submit"]').click(function() { - if(Common.exists(path_tooltip + ' #oob-upload input[type="reset"]')) + if(Common.exists(path_tooltip + ' #oob-upload input[type="reset"]')) { return; + } // Lock the bubble $(path_bubble).addClass('locked'); @@ -569,17 +598,21 @@ var Tooltip = (function () { // Apply the options to the style selector $(bubble_style + ' input[type="checkbox"]').each(function() { + var this_sel = $(this); + // Current input enabled? - if(message_area.attr('data-' + $(this).attr('class'))) - $(this).attr('checked', true); + if(message_area.attr('data-' + this_sel.attr('class'))) { + this_sel.attr('checked', true); + } }); // Apply message color if(color) { - if($(bubble_style + ' a.color[data-color="' + color + '"]').size()) + if($(bubble_style + ' a.color[data-color="' + color + '"]').size()) { $(bubble_style + ' a.color[data-color="' + color + '"]').addClass('selected'); - else + } else { $(bubble_style + ' div.color-hex input.hex-value').val(color); + } } } catch(e) { Console.error('Tooltip.loadStyleSelector', e); diff --git a/source/app/javascripts/userinfos.js b/source/app/javascripts/userinfos.js index c657be9..6e8ae9d 100644 --- a/source/app/javascripts/userinfos.js +++ b/source/app/javascripts/userinfos.js @@ -298,8 +298,9 @@ var UserInfos = (function () { var path = '#userinfos[data-last="' + id + '"]'; // End if session does not exist - if(!Common.exists(path)) + if(!Common.exists(path)) { return; + } if(iq && (iq.getType() == 'result')) { // Get the values @@ -313,8 +314,9 @@ var UserInfos = (function () { seconds = parseInt(seconds); // Active user - if(seconds <= 60) + if(seconds <= 60) { last = Common._e("User currently active"); + } // Inactive user else { @@ -325,12 +327,14 @@ var UserInfos = (function () { var date = date_last.toLocaleString(); // Offline user - if(from.indexOf('/') == -1) + if(from.indexOf('/') == -1) { last = Common.printf(Common._e("Last seen: %s"), date); + } // Online user - else + else { last = Common.printf(Common._e("Inactive since: %s"), date); + } } // Append this text @@ -363,8 +367,9 @@ var UserInfos = (function () { var path = '#userinfos[data-version="' + id + '"]'; // End if session does not exist - if(!Common.exists(path)) + if(!Common.exists(path)) { return; + } // Extract the reply data if(iq && (iq.getType() == 'result')) { @@ -375,14 +380,18 @@ var UserInfos = (function () { var os = $(xml).find('os').text(); // Put the values together - if(name && version) + if(name && version) { name = name + ' ' + version; + } // Display the values - if(name) + if(name) { $(path + ' #BUDDY-CLIENT').text(name); - if(os) + } + + if(os) { $(path + ' #BUDDY-SYSTEM').text(os); + } Console.log('Software version received: ' + Common.fullXID(Common.getStanzaFrom(iq))); } @@ -410,8 +419,9 @@ var UserInfos = (function () { var path = '#userinfos[data-time="' + id + '"]'; // End if session does not exist - if(!Common.exists(path)) + if(!Common.exists(path)) { return; + } if(iq && (iq.getType() == 'result')) { // Get the values @@ -422,8 +432,9 @@ var UserInfos = (function () { // Any UTC? if(utc) { // Add the TZO if there's no one - if(tzo && utc.match(/^(.+)Z$/)) + if(tzo && utc.match(/^(.+)Z$/)) { utc = RegExp.$1 + tzo; + } // Get the local date string var local_string = Date.hrTime(utc); @@ -454,8 +465,9 @@ var UserInfos = (function () { try { var selector = $('#userinfos .content'); - if(!selector.hasClass('vcard') && !selector.hasClass('last') && !selector.hasClass('version') && !selector.hasClass('time')) + if(!selector.hasClass('vcard') && !selector.hasClass('last') && !selector.hasClass('version') && !selector.hasClass('time')) { $('#userinfos .wait').hide(); + } } catch(e) { Console.error('UserInfos.wait', e); } @@ -478,8 +490,9 @@ var UserInfos = (function () { // Necessary to update? var old_value = DataStore.getDB(Connection.desktop_hash, 'rosternotes', xid); - if((old_value == value) || (!old_value && !value)) + if((old_value == value) || (!old_value && !value)) { return false; + } // Update the database DataStore.setDB(Connection.desktop_hash, 'rosternotes', xid, value); @@ -502,8 +515,9 @@ var UserInfos = (function () { var cur_xid = RegExp.$1; var cur_value = DataStore.storageDB.getItem(current); - if(cur_xid && cur_value) + if(cur_xid && cur_value) { storage.appendChild(iq.buildNode('note', {'jid': cur_xid, 'xmlns': NS_ROSTERNOTES}, cur_value)); + } } } @@ -526,10 +540,15 @@ var UserInfos = (function () { self.switchTab = function(id) { try { - $('#userinfos .content .one-lap').hide(); - $('#userinfos .content .info' + id).show(); - $('#userinfos .tab a').removeClass('tab-active'); - $('#userinfos .tab a[data-key="' + id + '"]').addClass('tab-active'); + var userinfos_sel = $('#userinfos'); + var content_sel = userinfos_sel.find('.content'); + var tab_link_sel = userinfos_sel.find('.tab a'); + + content_sel.find('.one-lap').hide(); + content_sel.find('.info' + id).show(); + + tab_link_sel.removeClass('tab-active'); + tab_link_sel.filter('[data-key="' + id + '"]').addClass('tab-active'); } catch(e) { Console.error('UserInfos.switchTab', e); } finally { @@ -582,12 +601,15 @@ var UserInfos = (function () { try { // Click events $('#userinfos .tab a').click(function() { + var this_sel = $(this); + // Yet active? - if($(this).hasClass('tab-active')) + if(this_sel.hasClass('tab-active')) { return false; + } // Switch to the good tab - var key = parseInt($(this).attr('data-key')); + var key = parseInt(this_sel.attr('data-key')); return self.switchTab(key); }); diff --git a/source/app/javascripts/utilities.js b/source/app/javascripts/utilities.js index 2108d08..b4b1aa0 100644 --- a/source/app/javascripts/utilities.js +++ b/source/app/javascripts/utilities.js @@ -52,8 +52,9 @@ var Utils = (function () { try { // HTTPS not allowed - if((HTTPS_STORAGE != 'on') && url.match(/^https(.+)/)) + if((HTTPS_STORAGE != 'on') && url.match(/^https(.+)/)) { url = 'http' + RegExp.$1; + } return url; } catch(e) { @@ -217,28 +218,34 @@ var Utils = (function () { var browser_version = BrowserDetect.version; // No DOM storage - if(!DataStore.hasDB() || !DataStore.hasPersistent()) + if(!DataStore.hasDB() || !DataStore.hasPersistent()) { return true; + } // Obsolete IE - if((browser_name == 'Explorer') && (browser_version < 8)) + if((browser_name == 'Explorer') && (browser_version < 8)) { return true; + } // Obsolete Chrome - if((browser_name == 'Chrome') && (browser_version < 7)) + if((browser_name == 'Chrome') && (browser_version < 7)) { return true; + } // Obsolete Safari - if((browser_name == 'Safari') && (browser_version < 4)) + if((browser_name == 'Safari') && (browser_version < 4)) { return true; + } // Obsolete Firefox - if((browser_name == 'Firefox') && (browser_version < 3.5)) + if((browser_name == 'Firefox') && (browser_version < 3.5)) { return true; + } // Obsolete Opera - if((browser_name == 'Opera') && (browser_version < 9)) + if((browser_name == 'Opera') && (browser_version < 9)) { return true; + } return false; } catch(e) { @@ -585,6 +592,33 @@ var Utils = (function () { }; + /** + * Removes duplicate values from array + * @public + * @param {object} arr + * @return {object} + */ + self.uniqueArrayValues = function(arr) { + + try { + var a = arr.concat(); + + for(var i = 0; i < a.length; ++i) { + for(var j = i + 1; j < a.length; ++j) { + if(a[i] === a[j]) { + a.splice(j--, 1); + } + } + } + + return a; + } catch(e) { + Console.error('Utils.uniqueArrayValues', e); + } + + }; + + /** * Converts a string to an array * @public @@ -603,10 +637,11 @@ var Utils = (function () { var string_split = string.split(','); for(var i in string_split) { - if(string_split[i]) + if(string_split[i]) { array.push(string_split[i]); - else + } else { array.push(''); + } } } @@ -634,8 +669,9 @@ var Utils = (function () { try { // Nothing? - if(!array || !array.length) + if(!array || !array.length) { return 0; + } // Read the index of the value var index = 0; diff --git a/source/app/javascripts/vcard.js b/source/app/javascripts/vcard.js index 128a292..610f132 100644 --- a/source/app/javascripts/vcard.js +++ b/source/app/javascripts/vcard.js @@ -272,7 +272,7 @@ var vCard = (function () { $('#USER-PHOTO-TYPE').val(aType); $('#USER-PHOTO-BINVAL').val(aBinval); - // We display the avatar ! + // We display the avatar! $('#vcard .avatar-container').replaceWith('
'); } @@ -374,9 +374,7 @@ var vCard = (function () { // Send the IQ con.send(iq, self.handleUser); - } - - else { + } else { // Show the wait icon $('#userinfos .wait').show(); @@ -456,12 +454,14 @@ var vCard = (function () { var values_yet = []; $(iqNode).find('vCard').children().each(function() { + var this_sel = $(this); + // Read the current parent node name var tokenname = (this).nodeName.toUpperCase(); // Node with a parent - if($(this).children().size()) { - $(this).children().each(function() { + if(this_sel.children().size()) { + this_sel.children().each(function() { // Get the node values var currentID = tokenname + '-' + (this).nodeName.toUpperCase(); var currentText = $(this).text(); @@ -473,10 +473,11 @@ var vCard = (function () { // Userinfos viewer popup if((type == 'buddy') && currentText) { - if(currentID == 'EMAIL-USERID') + if(currentID == 'EMAIL-USERID') { $(path_userInfos + ' #BUDDY-' + currentID).html('' + currentText.htmlEnc() + ''); - else + } else { $(path_userInfos + ' #BUDDY-' + currentID).text(currentText.htmlEnc()); + } } // Profile editor popup @@ -505,8 +506,9 @@ var vCard = (function () { // URL modification if(tokenname == 'URL') { // No http:// or https:// prefix, we should add it - if(!currentText.match(/^https?:\/\/(.+)/)) + if(!currentText.match(/^https?:\/\/(.+)/)) { currentText = 'http://' + currentText; + } currentText = '' + currentText.htmlEnc() + ''; } @@ -525,8 +527,9 @@ var vCard = (function () { } // Profile editor popup - else if(type == 'user') + else if(type == 'user') { $(path_vcard + ' #USER-' + tokenname).val(currentText); + } // Avoid duplicating the value values_yet.push(tokenname); @@ -556,9 +559,7 @@ var vCard = (function () { aBinval = $('#USER-PHOTO-BINVAL').val(); aType = $('#USER-PHOTO-TYPE').val(); aContainer = path_vcard + ' .avatar-container'; - } - - else { + } else { aBinval = $(iqNode).find('BINVAL:first').text(); aType = $(iqNode).find('TYPE:first').text(); aContainer = path_userInfos + ' .avatar-container'; @@ -578,8 +579,10 @@ var vCard = (function () { $(path_vcard + ' .avatar').remove(); } + var avatar_src = ('data:' + aType + ';base64,' + aBinval); + // We display the avatar we have just received - $(aContainer).replaceWith('
'); + $(aContainer).replaceWith('
'); } else if(type == 'buddy') { @@ -668,10 +671,11 @@ var vCard = (function () { var tagname = Common.explodeThis('-', item_id, 0); var cur_node; - if(node.getElementsByTagName(tagname).length > 0) + if(node.getElementsByTagName(tagname).length > 0) { cur_node = node.getElementsByTagName(tagname).item(0); - else + } else { cur_node = node.appendChild(stanza.buildNode(tagname, {'xmlns': namespace})); + } cur_node.appendChild( stanza.buildNode( @@ -874,18 +878,22 @@ var vCard = (function () { // Keyboard events $('#vcard input[type="text"]').keyup(function(e) { // Enter pressed: send the vCard - if((e.keyCode == 13) && !$('#vcard .finish.save').hasClass('disabled')) + if((e.keyCode == 13) && !$('#vcard .finish.save').hasClass('disabled')) { return self.send(); + } }); // Click events $('#vcard .tab a').click(function() { + var this_sel = $(this); + // Yet active? - if($(this).hasClass('tab-active')) + if(this_sel.hasClass('tab-active')) { return false; + } // Switch to the good tab - var key = parseInt($(this).attr('data-key')); + var key = parseInt(this_sel.attr('data-key')); return self.switchTab(key); }); @@ -895,10 +903,15 @@ var vCard = (function () { }); $('#vcard .bottom .finish').click(function() { - if($(this).is('.cancel')) + var this_sel = $(this); + + if(this_sel.is('.cancel')) { return self.close(); - if($(this).is('.save') && !$(this).hasClass('disabled')) + } + + if(this_sel.is('.save') && !this_sel.hasClass('disabled')) { return self.send(); + } return false; }); @@ -912,16 +925,22 @@ var vCard = (function () { // Avatar upload form submit event $('#vcard-avatar').submit(function() { - if($('#vcard .wait').is(':hidden') && $('#vcard .avatar-info.avatar-wait').is(':hidden') && $('#vcard-avatar input[type="file"]').val()) + if($('#vcard .wait').is(':hidden') && + $('#vcard .avatar-info.avatar-wait').is(':hidden') && + $('#vcard-avatar input[type="file"]').val()) { $(this).ajaxSubmit(avatar_options); + } return false; }); // Avatar upload input change event $('#vcard-avatar input[type="file"]').change(function() { - if($('#vcard .wait').is(':hidden') && $('#vcard .avatar-info.avatar-wait').is(':hidden') && $(this).val()) + if($('#vcard .wait').is(':hidden') && + $('#vcard .avatar-info.avatar-wait').is(':hidden') && + $(this).val()) { $('#vcard-avatar').ajaxSubmit(avatar_options); + } return false; }); diff --git a/source/app/javascripts/welcome.js b/source/app/javascripts/welcome.js index 32fa875..bec3e7b 100644 --- a/source/app/javascripts/welcome.js +++ b/source/app/javascripts/welcome.js @@ -203,6 +203,7 @@ var Welcome = (function () { // Update the "save" button if all is okay if(!Common.exists(tab + '.tab-missing')) { var finish = welcome + '.finish.'; + $(finish + 'save').show(); $(finish + 'next').hide(); } @@ -280,19 +281,22 @@ var Welcome = (function () { $('#welcome a.box').each(function() { var current = '0'; - if($(this).hasClass('enabled')) + if($(this).hasClass('enabled')) { current = '1'; + } array.push(current); }); // If XMPP links is enabled - if(array[2] == '1') + if(array[2] == '1') { Utils.xmppLinksHandler(); + } // If offline buddies showing is enabled - if(array[4] == '1') + if(array[4] == '1') { Interface.showAllBuddies('welcome'); + } // If archiving is supported by the server if(Features.enabledMAM()) { @@ -336,8 +340,9 @@ var Welcome = (function () { var next = 1; var missing = '#welcome .tab a.tab-missing'; - if(Common.exists(missing)) + if(Common.exists(missing)) { next = parseInt($(missing + ':first').attr('data-step')); + } // Switch to the next step self.switchTab(next); @@ -360,26 +365,36 @@ var Welcome = (function () { try { // Click events $('#welcome .tab a').click(function() { + var this_sel = $(this); + // Switch to the good tab - var key = parseInt($(this).attr('data-step')); + var key = parseInt(this_sel.attr('data-step')); return self.switchTab(key); }); $('#welcome a.box:not(.share)').click(function() { - if($(this).hasClass('enabled')) - $(this).removeClass('enabled').attr('title', Common._e("Click to enable")); - else - $(this).addClass('enabled').attr('title', Common._e("Click to disable")); + var this_sel = $(this); + + if(this_sel.hasClass('enabled')) { + this_sel.removeClass('enabled').attr('title', Common._e("Click to enable")); + } else { + this_sel.addClass('enabled').attr('title', Common._e("Click to disable")); + } return false; }); $('#welcome .bottom .finish').click(function() { - if($(this).is('.next')) + var this_sel = $(this); + + if(this_sel.is('.next')) { return self.next(); - if($(this).is('.save')) + } + + if(this_sel.is('.save')) { return self.save(); + } return false; }); diff --git a/source/app/javascripts/xmpplinks.js b/source/app/javascripts/xmpplinks.js index e55a2e0..0df5ef8 100644 --- a/source/app/javascripts/xmpplinks.js +++ b/source/app/javascripts/xmpplinks.js @@ -35,11 +35,9 @@ var XMPPLinks = (function () { link = Common.explodeThis(':', link, 1); // The XMPP URI has no "?" - if(link.indexOf('?') == -1) + if(link.indexOf('?') == -1) { Chat.checkCreate(link, 'chat'); - - // Parse the URI - else { + } else { var xid = Common.explodeThis('?', link, 0); var action = Common.explodeThis('?', link, 1); @@ -88,11 +86,13 @@ var XMPPLinks = (function () { * Gets the links vars (get parameters in URL) */ self.links_var = (function() { + var hash; var vars = []; var hashes = window.location.href.slice(window.location.href.indexOf('?') + 1).split('&'); for(var i = 0; i < hashes.length; i++) { - var hash = hashes[i].split('='); + hash = hashes[i].split('='); + vars.push(hash[0]); vars[hash[0]] = $.trim(decodeURIComponent(hash[1])); } diff --git a/source/app/sounds/catch-attention.mp3 b/source/app/sounds/catch-attention.mp3 new file mode 100644 index 0000000000000000000000000000000000000000..2f48361e580a2a0be6c5f908a24703cbd689aa40 GIT binary patch literal 28252 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 CWJnbN literal 0 HcmV?d00001 diff --git a/source/app/sounds/catch-attention.oga b/source/app/sounds/catch-attention.oga new file mode 100644 index 0000000000000000000000000000000000000000..a77f205e625522b97c900499297008b93a5e30ea GIT binary patch literal 27520 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 literal 0 HcmV?d00001 diff --git a/source/app/stylesheets/call.css b/source/app/stylesheets/call.css new file mode 100644 index 0000000..865bfcd --- /dev/null +++ b/source/app/stylesheets/call.css @@ -0,0 +1,254 @@ +/* + +Jappix - An open social platform +This is the call CSS stylesheet for Jappix + +------------------------------------------------- + +License: AGPL +Author: Valérian Saliou + +*/ + +.videochat_box { + display: none; +} + +.videochat_box .videochat_items { + background: #ededed; + border: 1px solid rgb(0,0,0); + border: 1px solid rgba(0,0,0,0.8); + text-shadow: none; + min-width: 550px; + min-height: 420px; + overflow: hidden; + position: absolute; + left: 100px; + right: 100px; + top: 40px; + bottom: 40px; + -moz-box-shadow: 0 0 12px rgba(0,0,0,0.4); + -webkit-box-shadow: 0 0 12px rgba(0,0,0,0.4); + box-shadow: 0 0 12px rgba(0,0,0,0.4); +} + +.videochat_box .videochat_items .topbar { + background: rgb(0,0,0); + background: rgba(0,0,0,0.5); + border-bottom: 1px solid rgb(0,0,0); + border-bottom: 1px solid rgba(0,0,0,0.15); + color: #ffffff; + text-shadow: 0 1px 1px rgb(0,0,0); + text-shadow: 0 1px 1px rgba(0,0,0,0.5); + height: 40px; + position: absolute; + left: 0; + right: 0; + top: 0; + z-index: 4; + -moz-box-shadow: 0 0 5px rgba(0,0,0,0.25); + -webkit-box-shadow: 0 0 5px rgba(0,0,0,0.25); + box-shadow: 0 0 5px rgba(0,0,0,0.25); +} + +.videochat_box .videochat_items .topbar .controls, +.videochat_box .videochat_items .topbar .elapsed { + float: left; +} + +html[dir="rtl"] .videochat_box .videochat_items .topbar .controls, +html[dir="rtl"] .videochat_box .videochat_items .topbar .elapsed { + float: right; +} + +html[dir="rtl"] .videochat_box .videochat_items .topbar .controls { + margin-left: 0; + margin-right: 50px; +} + +.videochat_box .videochat_items .topbar .controls a { + margin-top: 7px; + float: left; +} + +html[dir="rtl"] .videochat_box .videochat_items .topbar .controls a { + float: right; +} + +.videochat_box .videochat_items .topbar .controls a, +.call-content .call-notify .notification-content .reply-buttons a.reply-button { + border-width: 1px; + border-style: solid; + font-size: 10px; + color: #ffffff; + text-transform: uppercase; + text-decoration: none; + padding: 5px 6px 6px 6px; + -moz-border-radius: 2px; + -webkit-border-radius: 2px; + border-radius: 2px; +} + +.videochat_box .videochat_items .topbar .controls a:active, +.call-content .call-notify .notification-content .reply-buttons a.reply-button:active { + padding-top: 6px; + padding-bottom: 5px; + -webkit-box-shadow: 0 1px 3px rgba(0,0,0,0.25) inset; + -moz-box-shadow: 0 1px 3px rgba(0,0,0,0.25) inset; + box-shadow: 0 1px 3px rgba(0,0,0,0.25) inset; +} + +.videochat_box .videochat_items .topbar .controls a .icon { + width: 14px; + height: 14px; + margin: -1px 7px 0 2px; + float: left; +} + +.videochat_box .videochat_items .topbar .controls a.stop, +.videochat_box .videochat_items .topbar .controls a.leave, +.call-content .call-notify .notification-content .reply-buttons a.reply-button.red { + background: #cc283f; + border-color: #5e121d; +} + +.videochat_box .videochat_items .topbar .controls a.stop:active, +.videochat_box .videochat_items .topbar .controls a.leave:active, +.call-content .call-notify .notification-content .reply-buttons a.reply-button.red:active { + background: #a92134; + border-color: #480e16; +} + +.videochat_box .videochat_items .topbar .controls a.stop .icon, +.videochat_box .videochat_items .topbar .controls a.leave .icon { + background-position: 0 -62px; +} + +.call-content .call-notify .notification-content .reply-buttons a.reply-button.green { + background: #5ea45e; + border-color: #1a2e1a; +} + +.call-content .call-notify .notification-content .reply-buttons a.reply-button.green:active { + background: #549253; + border-color: #0f1a0f; +} + +.videochat_box .videochat_items .topbar .controls a.mute, +.videochat_box .videochat_items .topbar .controls a.unmute, +.call-content .call-notify .notification-content .reply-buttons a.reply-button.blue { + background: #6e8dc5; + border-color: #303d55; +} + +.videochat_box .videochat_items .topbar .controls a.mute, +.videochat_box .videochat_items .topbar .controls a.unmute { + margin-left: 6px; +} + +html[dir="rtl"] .videochat_box .videochat_items .topbar .controls a.mute, +html[dir="rtl"] .videochat_box .videochat_items .topbar .controls a.unmute { + margin-left: 0; + margin-right: 6px; +} + +.videochat_box .videochat_items .topbar .controls a.mute:active, +.videochat_box .videochat_items .topbar .controls a.unmute:active, +.call-content .call-notify .notification-content .reply-buttons a.reply-button.blue:active { + background: #6480b1; + border-color: #222b3b; +} + +.videochat_box .videochat_items .topbar .controls a.mute .icon { + background-position: 0 -81px; +} + +.videochat_box .videochat_items .topbar .controls a.unmute { + display: none; +} + +.videochat_box .videochat_items .topbar .controls a.unmute .icon { + background-position: 0 -100px; +} + +.videochat_box .videochat_items .topbar .elapsed { + background: rgb(0,0,0); + background: rgba(0,0,0,0.1); + border: 1px solid rgb(255,255,255); + border: 1px solid rgba(255,255,255,0.25); + outline: 1px solid rgb(0,0,0); + outline: 1px solid rgba(0,0,0,0.2); + font-size: 11px; + font-weight: bold; + letter-spacing: 2px; + margin: 10px 0 0 46px; + padding: 2px 6px; +} + +html[dir="rtl"] .videochat_box .videochat_items .topbar .elapsed { + margin-left: 0; + margin-right: 46px; +} + +.videochat_box .videochat_items .topbar .actions { + margin: 7px 15px 0 0; + float: right; +} + +html[dir="rtl"] .videochat_box .videochat_items .topbar .actions { + margin-right: 0; + margin-left: 15px; + float: left; +} + +.videochat_box .videochat_items .topbar .actions a { + float: left; +} + +.videochat_box .videochat_items .topbar .actions a.close { + background-position: 0 -44px; + width: 18px; + height: 12px; + margin-top: 6px; +} + +.videochat_box .videochat_items .local_video { + background-position: 0 -56px; + border: 1px solid rgb(0,0,0); + border: 1px solid rgba(0,0,0,0.5); + width: 180px; + height: 101px; + opacity: 0.6; + overflow: hidden; + position: absolute; + left: 18px; + bottom: 18px; + z-index: 3; + -moz-box-shadow: 0 0 8px rgba(0,0,0,0.25); + -webkit-box-shadow: 0 0 8px rgba(0,0,0,0.25); + box-shadow: 0 0 8px rgba(0,0,0,0.25); + -webkit-transition: all 0.4s ease-in-out 0.2s; + -moz-transition: all 0.4s ease-in-out 0.2s; + -o-transition: all 0.4s ease-in-out 0.2s; + transition: all 0.4s ease-in-out 0.2s; +} + +html[dir="rtl"] .videochat_box .videochat_items .local_video { + left: auto; + right: 18px; +} + +.videochat_box .videochat_items .local_video:disabled { + opacity: 0.2 !important; +} + +.videochat_box .videochat_items .local_video:hover { + width: 320px; + height: 180px; + opacity: 1; + cursor: pointer; +} + +.videochat_box .videochat_items .local_video video { + width: 100%; +} \ No newline at end of file diff --git a/source/app/stylesheets/home.css b/source/app/stylesheets/home.css index 7875212..2b40e90 100644 --- a/source/app/stylesheets/home.css +++ b/source/app/stylesheets/home.css @@ -597,7 +597,8 @@ html[dir="rtl"] #home .right .navigation a { background: -webkit-gradient(linear, left top, left bottom, from(#e4eef9), to(#c5e1ff)); background: -webkit-linear-gradient(top, #e4eef9 0%, #c5e1ff 100%); background: -o-linear-gradient(top, #e4eef9 0%, #c5e1ff 100%); - font-size: 13.4px; + font-size: 11px !important; + overflow: hidden; position: absolute; top: 0; bottom: 0; @@ -693,6 +694,22 @@ html[dir="rtl"] #home .right .navigation a { left: 10px; } +#home .friendsview .friends .group.standard br { + display: none; +} + +#home .friendsview .friends .group.standard a { + text-align: center; + text-decoration: underline; + margin-bottom: 1px; + float: left; + width: 50%; +} + +#home .friendsview .friends .group.standard a:nth-child(even) { + float: right; +} + #home .friendsview .friends a.group.refer { width: 81px; padding-left: 10px; diff --git a/source/app/stylesheets/images.css b/source/app/stylesheets/images.css index 604e794..d5a4104 100644 --- a/source/app/stylesheets/images.css +++ b/source/app/stylesheets/images.css @@ -56,8 +56,8 @@ Author: Valérian Saliou background-repeat: no-repeat; } -.jingle-images { - background-image: url(../images/sprites/jingle.png); +.call-images { + background-image: url(../images/sprites/call.png); background-repeat: no-repeat; } diff --git a/source/app/stylesheets/jingle.css b/source/app/stylesheets/jingle.css index 836ffd4..1614317 100644 --- a/source/app/stylesheets/jingle.css +++ b/source/app/stylesheets/jingle.css @@ -1,7 +1,7 @@ /* Jappix - An open social platform -This is the discovery CSS stylesheet for Jappix +This is the Jingle CSS stylesheet for Jappix ------------------------------------------------- @@ -10,47 +10,6 @@ Author: Valérian Saliou */ -#jingle { - display: none; -} - -#jingle .videobox { - background: #ededed; - border: 1px solid rgb(0,0,0); - border: 1px solid rgba(0,0,0,0.8); - text-shadow: none; - min-width: 550px; - min-height: 420px; - overflow: hidden; - position: absolute; - left: 100px; - right: 100px; - top: 40px; - bottom: 40px; - -moz-box-shadow: 0 0 12px rgba(0,0,0,0.4); - -webkit-box-shadow: 0 0 12px rgba(0,0,0,0.4); - box-shadow: 0 0 12px rgba(0,0,0,0.4); -} - -#jingle .videobox .topbar { - background: rgb(0,0,0); - background: rgba(0,0,0,0.5); - border-bottom: 1px solid rgb(0,0,0); - border-bottom: 1px solid rgba(0,0,0,0.15); - color: #ffffff; - text-shadow: 0 1px 1px rgb(0,0,0); - text-shadow: 0 1px 1px rgba(0,0,0,0.5); - height: 40px; - position: absolute; - left: 0; - right: 0; - top: 0; - z-index: 4; - -moz-box-shadow: 0 0 5px rgba(0,0,0,0.25); - -webkit-box-shadow: 0 0 5px rgba(0,0,0,0.25); - box-shadow: 0 0 5px rgba(0,0,0,0.25); -} - #jingle .videobox .topbar .card { margin: 4px 0 0 12px; } @@ -62,17 +21,13 @@ html[dir="rtl"] #jingle .videobox .topbar .card { #jingle .videobox .topbar .card, #jingle .videobox .topbar .card .avatar-container, -#jingle .videobox .topbar .card .identity, -#jingle .videobox .topbar .controls, -#jingle .videobox .topbar .elapsed { +#jingle .videobox .topbar .card .identity { float: left; } html[dir="rtl"] #jingle .videobox .topbar .card, html[dir="rtl"] #jingle .videobox .topbar .card .avatar-container, -html[dir="rtl"] #jingle .videobox .topbar .card .identity, -html[dir="rtl"] #jingle .videobox .topbar .controls, -html[dir="rtl"] #jingle .videobox .topbar .elapsed { +html[dir="rtl"] #jingle .videobox .topbar .card .identity { float: right; } @@ -115,192 +70,8 @@ html[dir="rtl"] #jingle .videobox .topbar .card .identity { margin-left: 50px; } -html[dir="rtl"] #jingle .videobox .topbar .controls { - margin-left: 0; - margin-right: 50px; -} - -#jingle .videobox .topbar .controls a { - margin-top: 7px; - float: left; -} - -html[dir="rtl"] #jingle .videobox .topbar .controls a { - float: right; -} - -#jingle .videobox .topbar .controls a, -.jingle-content .jingle-notify .notification-content .reply-buttons a.reply-button { - border-width: 1px; - border-style: solid; - font-size: 10px; - color: #ffffff; - text-transform: uppercase; - text-decoration: none; - padding: 5px 6px 6px 6px; - -moz-border-radius: 2px; - -webkit-border-radius: 2px; - border-radius: 2px; -} - -#jingle .videobox .topbar .controls a:active, -.jingle-content .jingle-notify .notification-content .reply-buttons a.reply-button:active { - padding-top: 6px; - padding-bottom: 5px; - -webkit-box-shadow: 0 1px 3px rgba(0,0,0,0.25) inset; - -moz-box-shadow: 0 1px 3px rgba(0,0,0,0.25) inset; - box-shadow: 0 1px 3px rgba(0,0,0,0.25) inset; -} - -#jingle .videobox .topbar .controls a .icon { - width: 14px; - height: 14px; - margin: -1px 7px 0 2px; - float: left; -} - -#jingle .videobox .topbar .controls a.stop, -.jingle-content .jingle-notify .notification-content .reply-buttons a.reply-button.red { - background: #cc283f; - border-color: #5e121d; -} - -#jingle .videobox .topbar .controls a.stop:active, -.jingle-content .jingle-notify .notification-content .reply-buttons a.reply-button.red:active { - background: #a92134; - border-color: #480e16; -} - -#jingle .videobox .topbar .controls a.stop .icon { - background-position: 0 -62px; -} - -.jingle-content .jingle-notify .notification-content .reply-buttons a.reply-button.green { - background: #5ea45e; - border-color: #1a2e1a; -} - -.jingle-content .jingle-notify .notification-content .reply-buttons a.reply-button.green:active { - background: #549253; - border-color: #0f1a0f; -} - -#jingle .videobox .topbar .controls a.mute, -#jingle .videobox .topbar .controls a.unmute, -.jingle-content .jingle-notify .notification-content .reply-buttons a.reply-button.blue { - background: #6e8dc5; - border-color: #303d55; -} - -#jingle .videobox .topbar .controls a.mute, -#jingle .videobox .topbar .controls a.unmute { - margin-left: 6px; -} - -html[dir="rtl"] #jingle .videobox .topbar .controls a.mute, -html[dir="rtl"] #jingle .videobox .topbar .controls a.unmute { - margin-left: 0; - margin-right: 6px; -} - -#jingle .videobox .topbar .controls a.mute:active, -#jingle .videobox .topbar .controls a.unmute:active, -.jingle-content .jingle-notify .notification-content .reply-buttons a.reply-button.blue:active { - background: #6480b1; - border-color: #222b3b; -} - -#jingle .videobox .topbar .controls a.mute .icon { - background-position: 0 -81px; -} - -#jingle .videobox .topbar .controls a.unmute { - display: none; -} - -#jingle .videobox .topbar .controls a.unmute .icon { - background-position: 0 -100px; -} - -#jingle .videobox .topbar .elapsed { - background: rgb(0,0,0); - background: rgba(0,0,0,0.1); - border: 1px solid rgb(255,255,255); - border: 1px solid rgba(255,255,255,0.25); - outline: 1px solid rgb(0,0,0); - outline: 1px solid rgba(0,0,0,0.2); - font-size: 11px; - font-weight: bold; - letter-spacing: 2px; - margin: 10px 0 0 46px; - padding: 2px 6px; -} - -html[dir="rtl"] #jingle .videobox .topbar .elapsed { - margin-left: 0; - margin-right: 46px; -} - -#jingle .videobox .topbar .actions { - margin: 7px 15px 0 0; - float: right; -} - -html[dir="rtl"] #jingle .videobox .topbar .actions { - margin-right: 0; - margin-left: 15px; - float: left; -} - -#jingle .videobox .topbar .actions a { - float: left; -} - -#jingle .videobox .topbar .actions a.close { - background-position: 0 -44px; - width: 18px; - height: 12px; - margin-top: 6px; -} - -#jingle .videobox .local_video { - background-position: 0 -56px; - border: 1px solid rgb(0,0,0); - border: 1px solid rgba(0,0,0,0.5); - width: 180px; - height: 101px; - opacity: 0.6; - overflow: hidden; - position: absolute; - left: 18px; - bottom: 18px; - z-index: 3; - -moz-box-shadow: 0 0 8px rgba(0,0,0,0.25); - -webkit-box-shadow: 0 0 8px rgba(0,0,0,0.25); - box-shadow: 0 0 8px rgba(0,0,0,0.25); - -webkit-transition: all 0.4s ease-in-out 0.2s; - -moz-transition: all 0.4s ease-in-out 0.2s; - -o-transition: all 0.4s ease-in-out 0.2s; - transition: all 0.4s ease-in-out 0.2s; -} - -html[dir="rtl"] #jingle .videobox .local_video { - left: auto; - right: 18px; -} - -#jingle .videobox .local_video:disabled { - opacity: 0.2 !important; -} - -#jingle .videobox .local_video:hover { - width: 320px; - height: 180px; - opacity: 1; - cursor: pointer; -} - #jingle .videobox .remote_video { + background-color: #000000; width: 100%; height: 100%; position: absolute; @@ -311,6 +82,11 @@ html[dir="rtl"] #jingle .videobox .local_video { z-index: 1; } +#jingle .videobox .remote_video video { + width: 100%; + height: 100%; +} + #jingle .videobox .branding { background-position: 0 0; width: 39px; diff --git a/source/app/stylesheets/main.css b/source/app/stylesheets/main.css index 0351b2d..eec236a 100644 --- a/source/app/stylesheets/main.css +++ b/source/app/stylesheets/main.css @@ -114,6 +114,19 @@ input[type="radio"] { border: 0 none !important; } +input.input-reset { + background: transparent; + border: 0 none; + margin: 0; + padding: 0; + -moz-border-radius: 0; + -webkit-border-radius: 0; + border-radius: 0; + -moz-box-shadow: none; + -webkit-box-shadow: none; + box-shadow: none; +} + .please-complete, .please-complete:hover, .please-complete:focus { @@ -129,4 +142,5 @@ input[type="radio"] { .clear { clear: both !important; + display: block !important; } diff --git a/source/app/stylesheets/mobile.css b/source/app/stylesheets/mobile.css index dbd3141..0573af9 100644 --- a/source/app/stylesheets/mobile.css +++ b/source/app/stylesheets/mobile.css @@ -143,7 +143,7 @@ a { } #talk a.one-buddy { - display: none; + display: block; background-color: #87a5ab; border-bottom: 1px solid #5b8088; text-shadow: 1px 1px 1px #5b8088; @@ -154,6 +154,7 @@ a { } #talk a.one-buddy:hover { + background-color: #8fb0b7; cursor: pointer; } @@ -248,7 +249,7 @@ a { border-bottom: 1px solid #cbcbcb; top: 23px; left: 0; - bottom: 25px; + bottom: 37px; overflow: auto; text-align: left; } diff --git a/source/app/stylesheets/muji.css b/source/app/stylesheets/muji.css new file mode 100644 index 0000000..a044131 --- /dev/null +++ b/source/app/stylesheets/muji.css @@ -0,0 +1,700 @@ +/* + +Jappix - An open social platform +This is the Muji CSS stylesheet for Jappix + +------------------------------------------------- + +License: AGPL +Author: Valérian Saliou + +*/ + +#muji .videochat_items { + min-width: 600px; + left: 60px; + right: 60px; +} + +#muji .remote_container { + position: absolute; + left: 0; + right: 0; + bottom: 0; + top: 0; +} + +/* Default video */ +#muji .remote_container .remote_video_shaper { + overflow: hidden; + display: none; + float: left; + position: relative; +} + +/* 1 video view */ +#muji[data-count="1"] .remote_container .remote_video_shaper:nth-child(1) { + width: 100%; + height: 100%; + display: block; +} + +/* 2 video views */ +#muji[data-count="2"] .remote_container .remote_video_shaper:nth-child(1), +#muji[data-count="2"] .remote_container .remote_video_shaper:nth-child(2) { + width: 50%; + height: 100%; + display: block; +} + +/* 3 video views */ +#muji[data-count="3"] .remote_container .remote_video_shaper:nth-child(1), +#muji[data-count="3"] .remote_container .remote_video_shaper:nth-child(2), +#muji[data-count="3"] .remote_container .remote_video_shaper:nth-child(3) { + height: 50%; + display: block; +} + +#muji[data-count="3"] .remote_container .remote_video_shaper:nth-child(1) { + width: 100%; + float: none; +} + +#muji[data-count="3"] .remote_container .remote_video_shaper:nth-child(2), +#muji[data-count="3"] .remote_container .remote_video_shaper:nth-child(3) { + width: 50%; +} + +/* 4 video views */ +#muji[data-count="4"] .remote_container .remote_video_shaper:nth-child(1), +#muji[data-count="4"] .remote_container .remote_video_shaper:nth-child(2), +#muji[data-count="4"] .remote_container .remote_video_shaper:nth-child(3), +#muji[data-count="4"] .remote_container .remote_video_shaper:nth-child(4) { + height: 50%; + width: 50%; + display: block; +} + +/* 5 video views */ +#muji[data-count="5"] .remote_container .remote_video_shaper:nth-child(1), +#muji[data-count="5"] .remote_container .remote_video_shaper:nth-child(2), +#muji[data-count="5"] .remote_container .remote_video_shaper:nth-child(3), +#muji[data-count="5"] .remote_container .remote_video_shaper:nth-child(4), +#muji[data-count="5"] .remote_container .remote_video_shaper:nth-child(5) { + height: 50%; + display: block; +} + +#muji[data-count="5"] .remote_container .remote_video_shaper:nth-child(1), +#muji[data-count="5"] .remote_container .remote_video_shaper:nth-child(2) { + width: 50%; +} + +#muji[data-count="5"] .remote_container .remote_video_shaper:nth-child(3), +#muji[data-count="5"] .remote_container .remote_video_shaper:nth-child(4), +#muji[data-count="5"] .remote_container .remote_video_shaper:nth-child(5) { + width: 33.33333333%; +} + +/* 6 video views */ +#muji[data-count="6"] .remote_container .remote_video_shaper:nth-child(1), +#muji[data-count="6"] .remote_container .remote_video_shaper:nth-child(2), +#muji[data-count="6"] .remote_container .remote_video_shaper:nth-child(3), +#muji[data-count="6"] .remote_container .remote_video_shaper:nth-child(4), +#muji[data-count="6"] .remote_container .remote_video_shaper:nth-child(5), +#muji[data-count="6"] .remote_container .remote_video_shaper:nth-child(6) { + height: 50%; + width: 33.33333333%; + display: block; +} + +#muji .remote_container .remote_video_shaper .label_username { + background: rgb(0,0,0); + background: rgba(0,0,0,0.6); + color: #ffffff; + font-size: 0.75em; + padding: 3px 6px 4px 10px; + position: absolute; + top: 7px; + left: 0; + display: none; +} + +html[dir="rtl"] #muji .remote_container .remote_video_shaper .label_username { + padding-right: 10px; + padding-left: 6px; + left: auto; + right: 0; +} + +#muji[data-count="1"] .remote_container .remote_video_shaper .label_username, +#muji[data-count="2"] .remote_container .remote_video_shaper .label_username, +#muji[data-count="3"] .remote_container .remote_video_shaper:nth-child(1) .label_username, +#muji[data-count="4"] .remote_container .remote_video_shaper:nth-child(n+1):nth-child(-n+2) .label_username, +#muji[data-count="5"] .remote_container .remote_video_shaper:nth-child(n+1):nth-child(-n+2) .label_username, +#muji[data-count="6"] .remote_container .remote_video_shaper:nth-child(n+1):nth-child(-n+3) .label_username { + top: 48px; +} + +#muji .remote_container .remote_video_shaper:hover .label_username { + display: block; +} + +#muji .empty_message { + text-align: center; + margin-top: -11px; + position: absolute; + left: 0; + right: 0; + top: 50%; +} + +#muji .empty_message .text { + color: #ffffff; + letter-spacing: 1px; + font-size: 1.2em; +} + +#muji .videoroom, +#muji .chatroom { + position: absolute; + top: 0; + bottom: 0; +} + +#muji .videoroom { + background-color: #000000; + left: 0; + right: 280px; +} + +html[dir="rtl"] #muji .videoroom { + right: 0; + left: 280px; +} + +#muji .videoroom .topbar .controls { + margin-left: 18px; +} + +html[dir="rtl"] #muji .videoroom .topbar .controls { + margin-right: 18px; +} + +#muji .videoroom .topbar .elapsed { + margin-left: 80px; +} + +html[dir="rtl"] #muji .videoroom .topbar .elapsed { + margin-right: 80px; +} + +#muji .chatroom { + background: #ffffff; + width: 280px; + right: 0; +} + +html[dir="rtl"] #muji .chatroom { + right: auto; + left: 0; +} + +#muji .videoroom .local_video { + width: 140px; + height: 78px; +} + +#muji .videoroom .local_video:hover { + width: 220px; + height: 123px; +} + +#muji .chatroom .chatroom_participants { + background: #fcfcfc; + border-bottom: 1px solid #e1e1e1; + position: absolute; + top: 0; + left: 0; + right: 0; + z-index: 1; +} + +#muji .chatroom .chatroom_participants .participants_default_view { + text-align: center; + height: 40px; +} + +#muji .chatroom .chatroom_participants .participants_default_view .participants_counter, +#muji .chatroom .chatroom_participants .participants_default_view .participants_full { + color: #444444; + font-size: 0.9em; + font-weight: bold; + letter-spacing: 1px; + margin-top: 11px; + display: inline-block; +} + +#muji .chatroom .chatroom_participants .participants_default_view .participants_full { + color: #5a6d7f; + font-size: 0.8em; + margin-left: 6px; + display: none; +} + +html[dir="rtl"] #muji .chatroom .chatroom_participants .participants_default_view .participants_full { + margin-left: auto; + margin-right: 6px; +} + +#muji .chatroom .chatroom_participants .participants_default_view .participants_invite { + background-position: 0 -287px; + width: 13px; + height: 13px; + opacity: 0.6; + display: block; + position: absolute; + top: 14px; + right: 16px; +} + +html[dir="rtl"] #muji .chatroom .chatroom_participants .participants_default_view .participants_invite { + right: auto; + left: 16px; +} + +#muji .chatroom .chatroom_participants .participants_default_view .participants_invite:hover, +#muji .chatroom .chatroom_participants .participants_default_view .participants_invite:focus { + opacity: 1; +} + +#muji .chatroom .chatroom_participants .participants_invite_box { + border-top: 1px solid #e1e1e1; + display: none; +} + +#muji .chatroom .chatroom_participants .participants_invite_box .participants_invite_list { + border-bottom: 1px solid #e1e1e1; + padding: 4px 2px 2px 6px; + display: none; +} + +#muji .chatroom .chatroom_participants .participants_invite_box .participants_invite_list .invite_one { + background: #d7e2f4; + border: 1px solid #aab9f4; + font-size: 0.75em; + margin: 0 4px 3px 0; + padding: 2px 4px; + display: inline-block; + -moz-border-radius: 2px; + -webkit-border-radius: 2px; + border-radius: 2px; +} + +#muji .chatroom .chatroom_participants .participants_invite_box .participants_invite_list .invite_one.invite_unsupported { + background: #f49d90; + border-color: #de8780; + color: #95443e; +} + +#muji .chatroom .chatroom_participants .participants_invite_box .participants_invite_list .invite_one .invite_one_remove { + background-position: 0 -300px; + width: 9px; + height: 9px; + margin-left: 3px; + margin-top: 1px; + display: inline-block; +} + +html[dir="rtl"] #muji .chatroom .chatroom_participants .participants_invite_box .participants_invite_list .invite_one .invite_one_remove { + margin-left: auto; + margin-right: 3px; +} + +#muji .chatroom .chatroom_participants .participants_invite_box .participants_invite_list .invite_one.invite_unsupported .invite_one_remove { + background-position: 0 -309px; +} + +#muji .chatroom .chatroom_participants .participants_invite_box .participants_invite_list .invite_one .invite_one_remove:hover, +#muji .chatroom .chatroom_participants .participants_invite_box .participants_invite_list .invite_one .invite_one_remove:focus, +#muji .chatroom .chatroom_participants .participants_invite_box .participants_invite_list .invite_one .invite_one_remove:active { + background-position: 1px -317px; + margin-left: 2px; + margin-right: -1px; + margin-bottom: -1px; + padding: 1px; + -moz-border-radius: 1px; + -webkit-border-radius: 1px; + border-radius: 1px; +} + +#muji .chatroom .chatroom_participants .participants_invite_box .participants_invite_list .invite_one .invite_one_remove:hover, +#muji .chatroom .chatroom_participants .participants_invite_box .participants_invite_list .invite_one .invite_one_remove:focus { + background-color: #8299ad; +} + +#muji .chatroom .chatroom_participants .participants_invite_box .participants_invite_list .invite_one.invite_unsupported .invite_one_remove:hover, +#muji .chatroom .chatroom_participants .participants_invite_box .participants_invite_list .invite_one.invite_unsupported .invite_one_remove:focus { + background-color: #ad625f; +} + +#muji .chatroom .chatroom_participants .participants_invite_box .participants_invite_list .invite_one .invite_one_remove:active { + background-color: #5b6e80; +} + +#muji .chatroom .chatroom_participants .participants_invite_box .participants_invite_list .invite_one.invite_unsupported .invite_one_remove:active { + background-color: #804847; +} + +#muji .chatroom .chatroom_participants .participants_invite_box form.participants_invite_form { + height: 32px; + margin-left: 8px; + position: relative; +} + +html[dir="rtl"] #muji .chatroom .chatroom_participants .participants_invite_box form.participants_invite_form { + margin-left: auto; + margin-left: 0; + margin-right: 8px; +} + +#muji .chatroom .chatroom_participants .participants_invite_box form.participants_invite_form .invite_validate { + display: none; +} + +#muji .chatroom .chatroom_participants .participants_invite_box form.participants_invite_form .invite_validate .invite_go { + background-position: 0 -329px; + width: 14px; + height: 11px; + opacity: 0.6; + display: block; + position: absolute; + top: 11px; + right: 16px; +} + +html[dir="rtl"] #muji .chatroom .chatroom_participants .participants_invite_box form.participants_invite_form .invite_validate .invite_go { + right: auto; + left: 8px; +} + +#muji .chatroom .chatroom_participants .participants_invite_box form.participants_invite_form .invite_validate .invite_go:hover, +#muji .chatroom .chatroom_participants .participants_invite_box form.participants_invite_form .invite_validate .invite_go:active { + opacity: 1; +} + +#muji .chatroom .chatroom_participants .participants_invite_box form.participants_invite_form .invite_validate .invite_separator, +#muji .chatroom form.chatroom_form .message_separator { + background: #e9e9e9; + width: 1px; + position: absolute; + top: 6px; + bottom: 6px; +} + +#muji .chatroom .chatroom_participants .participants_invite_box form.participants_invite_form .invite_validate .invite_separator { + right: 40px; +} + +html[dir="rtl"] #muji .chatroom .chatroom_participants .participants_invite_box form.participants_invite_form .invite_validate .invite_separator { + right: auto; + left: 32px; +} + +#muji .chatroom .chatroom_participants .participants_invite_box form.participants_invite_form .invite_input_container { + position: absolute; + top: 0; + left: 0; + bottom: 0; + right: 41px; +} + +html[dir="rtl"] #muji .chatroom .chatroom_participants .participants_invite_box form.participants_invite_form .invite_input_container { + right: 0; + left: 41px; +} + +#muji .chatroom .chatroom_participants .participants_invite_box form.participants_invite_form input.invite_xid { + font-size: 0.8em; + width: 100%; + height: 100%; +} + +#muji .chatroom .chatroom_participants .participants_invite_box .participants_invite_search { + max-height: 220px; + overflow: auto; +} + +#muji .chatroom .chatroom_participants .participants_invite_box .participants_invite_search .participant_search_one { + border-top: 1px solid #e1e1e1; + height: 28px; + display: block; + position: relative; +} + +#muji .chatroom .chatroom_participants .participants_invite_box .participants_invite_search .participant_search_one.hover, +#muji .chatroom .chatroom_participants .participants_invite_box .participants_invite_search .participant_search_one.focus { + background: #f5f7ff; +} + +#muji .chatroom .chatroom_participants .participants_invite_box .participants_invite_search .participant_search_one.active { + background: #eef1f8; +} + +#muji .chatroom .chatroom_participants .participants_invite_box .participants_invite_search .participant_search_one.participant_search_unsupported { + background: #f6f6f6; + color: #969696; +} + +#muji .chatroom .chatroom_participants .participants_invite_box .participants_invite_search .participant_search_one.participant_search_unsupported.hover, +#muji .chatroom .chatroom_participants .participants_invite_box .participants_invite_search .participant_search_one.participant_search_unsupported.focus { + background: #f1f1f1; +} + +#muji .chatroom .chatroom_participants .participants_invite_box .participants_invite_search .participant_search_one.participant_search_unsupported.active { + background: #efefef; +} + +#muji .chatroom .chatroom_participants .participants_invite_box .participants_invite_search .participant_search_one .avatar-container { + text-align: center; + height: 20px; + width: 20px; + margin: 4px 0 0 6px; + float: left; +} + +html[dir="rtl"] #muji .chatroom .chatroom_participants .participants_invite_box .participants_invite_search .participant_search_one .avatar-container { + float: right; +} + +#muji .chatroom .chatroom_participants .participants_invite_box .participants_invite_search .participant_search_one .avatar-container .avatar { + max-height: 20px; + max-width: 20px; +} + +#muji .chatroom .chatroom_participants .participants_invite_box .participants_invite_search .participant_search_one .details { + position: absolute; + top: 0; + left: 40px; + right: 0; + bottom: 0; +} + +html[dir="rtl"] #muji .chatroom .chatroom_participants .participants_invite_box .participants_invite_search .participant_search_one .details { + right: 40px; + left: 0; +} + +#muji .chatroom .chatroom_participants .participants_invite_box .participants_invite_search .participant_search_one .details .name, +#muji .chatroom .chatroom_participants .participants_invite_box .participants_invite_search .participant_search_one .details .feature { + position: absolute; + display: block; +} + +#muji .chatroom .chatroom_participants .participants_invite_box .participants_invite_search .participant_search_one .details .name { + font-size: 0.8em; + top: 6px; + left: 0; + right: 50px; + bottom: 0; +} + +html[dir="rtl"] #muji .chatroom .chatroom_participants .participants_invite_box .participants_invite_search .participant_search_one .details .name { + right: 0; + left: 50px; +} + +#muji .chatroom .chatroom_participants .participants_invite_box .participants_invite_search .participant_search_one .details .feature { + width: 16px; + height: 16px; + top: 6px; + right: 16px; + opacity: 0.75; + display: none; +} + +html[dir="rtl"] #muji .chatroom .chatroom_participants .participants_invite_box .participants_invite_search .participant_search_one .details .feature { + right: auto; + left: 7px; +} + +#muji .chatroom .chatroom_participants .participants_invite_box .participants_invite_search .participant_search_one.participant_search_has_audio .details .feature, +#muji .chatroom .chatroom_participants .participants_invite_box .participants_invite_search .participant_search_one.participant_search_has_video .details .feature { + display: block; +} + +#muji .chatroom .chatroom_participants .participants_invite_box .participants_invite_search .participant_search_one.participant_search_has_audio .details .feature { + background-position: 0 -340px; +} + +#muji .chatroom .chatroom_participants .participants_invite_box .participants_invite_search .participant_search_one.participant_search_has_video .details .feature { + background-position: 0 -356px; +} + +#muji .chatroom .chatroom_view { + padding: 4px 4px 12px 8px; + overflow: auto; + position: absolute; + top: 41px; + left: 0; + right: 0; + bottom: 41px; +} + +html[dir="rtl"] #muji .chatroom .chatroom_view { + padding-left: 8px; +} + +#muji .chatroom .chatroom_view .room_message { + margin-top: 10px; + position: relative; +} + +#muji .chatroom .chatroom_view .room_message.me .message_content { + float: left; +} + +html[dir="rtl"] #muji .chatroom .chatroom_view .room_message.me .message_content { + float: right; +} + +#muji .chatroom .chatroom_view .room_message.him .message_content { + margin-right: 30px; + float: right; +} + +html[dir="rtl"] #muji .chatroom .chatroom_view .room_message.him .message_content { + margin-right: auto; + margin-left: 30px; + float: left; +} + +#muji .chatroom .chatroom_view .room_message .message_content .message_bubble { + font-size: 0.85em; + padding: 7px 12px 8px 12px; + display: block; + -moz-border-radius: 6px; + -webkit-border-radius: 6px; + border-radius: 6px; +} + +#muji .chatroom .chatroom_view .room_message.me .message_content .message_bubble { + background-color: #8dc2ef; + -moz-border-radius-bottomleft: 0; + -webkit-border-bottom-left-radius: 0; + border-bottom-left-radius: 0; +} + +html[dir="rtl"] #muji .chatroom .chatroom_view .room_message.me .message_content .message_bubble { + -moz-border-radius-bottomleft: 6px; + -webkit-border-bottom-left-radius: 6px; + border-bottom-left-radius: 6px; + -moz-border-radius-bottomright: 0; + -webkit-border-bottom-right-radius: 0; + border-bottom-right-radius: 0; +} + +#muji .chatroom .chatroom_view .room_message.him .message_content .message_bubble { + background-color: #bcdf6a; + -moz-border-radius-bottomright: 0; + -webkit-border-bottom-right-radius: 0; + border-bottom-right-radius: 0; +} + +html[dir="rtl"] #muji .chatroom .chatroom_view .room_message.him .message_content .message_bubble { + -moz-border-radius-bottomright: 6px; + -webkit-border-bottom-right-radius: 6px; + border-bottom-right-radius: 6px; + -moz-border-radius-bottomleft: 0; + -webkit-border-bottom-left-radius: 0; + border-bottom-left-radius: 0; +} + +#muji .chatroom .chatroom_view .room_message .message_content .message_author { + color: #6c6c6c; + font-size: 0.75em; + margin-top: 2px; + display: block; +} + +#muji .chatroom .chatroom_view .room_message.him .message_content .message_author { + text-align: right; +} + +html[dir="rtl"] #muji .chatroom .chatroom_view .room_message.him .message_content .message_author { + text-align: left; +} + +#muji .chatroom .chatroom_view .room_message .message_avatar { + position: absolute; + right: 0; + bottom: 16px; +} + +html[dir="rtl"] #muji .chatroom .chatroom_view .room_message .message_avatar { + right: auto; + left: 0; +} + +#muji .chatroom .chatroom_view .room_message .message_avatar.avatar-container { + text-align: center; + height: 24px; + width: 24px; +} + +#muji .chatroom .chatroom_view .room_message .message_avatar.avatar-container .avatar { + max-height: 24px; + max-width: 24px; +} + +#muji .chatroom form.chatroom_form { + background: #fcfcfc; + border-top: 1px solid #e1e1e1; + height: 40px; + position: absolute; + bottom: 0; + left: 0; + right: 0; +} + +#muji .chatroom form.chatroom_form .message_icon { + background-position: 0 -272px; + width: 17px; + height: 15px; + position: absolute; + top: 13px; + left: 12px; +} + +html[dir="rtl"] #muji .chatroom form.chatroom_form .message_icon { + left: auto; + right: 12px; +} + +#muji .chatroom form.chatroom_form .message_separator { + left: 40px; +} + +html[dir="rtl"] #muji .chatroom form.chatroom_form .message_separator { + left: auto; + right: 40px; +} + +#muji .chatroom form.chatroom_form .message_input_container { + position: absolute; + top: 0; + right: 0; + bottom: 0; + left: 52px; +} + +html[dir="rtl"] #muji .chatroom form.chatroom_form .message_input_container { + left: auto; + right: 52px; +} + +#muji .chatroom form.chatroom_form .message_input_container input.message_input { + font-size: 0.8em; + width: 100%; + height: 100%; +} \ No newline at end of file diff --git a/source/app/stylesheets/others.css b/source/app/stylesheets/others.css index 363ccaa..4483466 100644 --- a/source/app/stylesheets/others.css +++ b/source/app/stylesheets/others.css @@ -108,7 +108,8 @@ html[dir="rtl"] .general-wait-content { .mam-hidable, .commands-hidable, .privacy-hidable, -.xmpplinks-hidable { +.xmpplinks-hidable, +.muji-hidable { display: none; } diff --git a/source/app/stylesheets/pageengine.css b/source/app/stylesheets/pageengine.css index 5720c9c..37e6f7d 100644 --- a/source/app/stylesheets/pageengine.css +++ b/source/app/stylesheets/pageengine.css @@ -327,6 +327,93 @@ html[dir="rtl"] #page-engine .list .user .user-details .avatar-container { margin-bottom: 10px; } +#page-engine .one-line { + position: relative; +} + +#page-engine .one-line.is-sending { + opacity: 0.6; +} + +#page-engine .one-line .correction-edit, +#page-engine .one-line .correction-label, +#page-engine .one-line.user-message[data-edited] .corrected-info, +#page-engine .one-line .message-marker { + font-size: 0.8em; + position: absolute; + right: 0; + top: 0; +} + +html[dir="rtl"] #page-engine .one-line .correction-edit, +html[dir="rtl"] #page-engine .one-line .correction-label, +html[dir="rtl"] #page-engine .one-line.user-message[data-edited] .corrected-info, +html[dir="rtl"] #page-engine .one-line .message-marker { + left: 0; + right: auto; +} + +#page-engine .one-line .correction-edit, +#page-engine .one-line .correction-label { + border: 1px solid #7f7f7f; + color: black; + font-size: 0.8em; + text-decoration: none; + margin-top: -1px; + padding: 2px 5px; + position: absolute; + right: 0; + top: 0; +} + +#page-engine .one-line .correction-edit { + opacity: 0.4; + display: none; +} + +#page-engine .page-engine-chan[data-correction="true"] .one-line .correction-edit:hover, +#page-engine .page-engine-chan[data-correction="true"] .one-line .correction-edit:focus { + opacity: 1; +} + +#page-engine .one-line.user-message[data-edited] .corrected-info, +#page-engine .one-line .message-marker { + color: #969696; + margin-top: 2px; +} + +#page-engine .one-line .message-marker { + display: none; +} + +#page-engine .one-line .message-marker.message-marker-read { + background-position: 0 -2227px; + padding-left: 11px; +} + +#page-engine .page-engine-chan[data-correction="true"] .one-line.user-message[data-mode="me"]:last-child:hover .correction-edit { + display: block; +} + +#page-engine .one-line.correction-active .corrected-info, +#page-engine .one-line.user-message[data-mode="me"]:last-child:hover .corrected-info { + display: none; +} + +#page-engine .one-line.correction-active .message-marker, +#page-engine .one-line.user-message[data-mode="me"]:last-child:hover .message-marker { + display: none !important; +} + +#page-engine .one-line .message-content { + margin-right: 80px; +} + +html[dir="rtl"] #page-engine .one-line .message-content { + margin-right: 0; + margin-left: 80px; +} + #page-engine .one-line, #page-engine .one-group b.name { padding-left: 50px; @@ -339,6 +426,10 @@ html[dir="rtl"] #page-engine .one-group b.name { padding-right: 50px; } +#page-engine .one-line.correction-active { + opacity: 0.5; +} + #page-engine .one-group b.name { display: block; margin-bottom: 4px; @@ -556,7 +647,9 @@ html[dir="rtl"] #page-engine .text .chat-tools-smileys { } body.in_jingle_call #page-engine .text .tools-jingle-video, -body.in_jingle_call #page-engine .text .tools-jingle-audio { +body.in_jingle_call #page-engine .text .tools-jingle-audio, +body.in_muji_call #page-engine .text .tools-jingle-video, +body.in_muji_call #page-engine .text .tools-jingle-audio { opacity: 0.35; cursor: default; } @@ -834,22 +927,67 @@ body.in_jingle_call #page-engine .text .tools-jingle-audio { } #page-engine .text .compose, -#page-engine .muc-ask { +#page-engine .muc-ask, +#page-engine .correction-toolbox { position: absolute; left: 0; } -#page-engine .text .compose { +#page-engine .text .compose, +#page-engine .correction-toolbox { top: 29px; - right: 12px; bottom: 12px; } +#page-engine .text .compose { + right: 12px; +} + html[dir="rtl"] #page-engine .text .compose { right: 0; left: 12px; } +#page-engine .text.correction-active .compose { + left: 120px; +} + +html[dir="rtl"] #page-engine .text.correction-active .compose { + right: 120px; +} + +#page-engine .correction-toolbox { + width: 120px; +} + +html[dir="rtl"] #page-engine .correction-toolbox { + right: 0; + left: auto; +} + +#page-engine .correction-toolbox .correction-editing, +#page-engine .correction-toolbox .correction-cancel { + display: block; + color: black; + font-size: 0.8em; + text-decoration: none; + text-align: center; + margin: 7px 12px; + padding: 2px 5px; +} + +#page-engine .correction-toolbox .correction-editing { + font-weight: bold; + margin-top: 8px; +} + +#page-engine .correction-toolbox .correction-cancel { + background: #d15e6b; + border: 1px solid #cc273f; + color: white; + text-shadow: 0 1px 1px rgba(0,0,0,0.5); +} + #page-engine .muc-ask { right: 0; bottom: 0; @@ -872,6 +1010,21 @@ html[dir="rtl"] #page-engine .text .compose { border-top-left-radius: 0; } +#page-engine .text.correction-active textarea { + -moz-border-radius-bottomleft: 0; + -webkit-border-bottom-left-radius: 0; + border-bottom-left-radius: 0; +} + +html[dir="rtl"] #page-engine .text.correction-active textarea { + -moz-border-radius-bottomleft: 3px; + -webkit-border-bottom-left-radius: 3px; + border-bottom-left-radius: 3px; + -moz-border-radius-bottomright: 0; + -webkit-border-bottom-right-radius: 0; + border-bottom-right-radius: 0; +} + #page-engine .muc-ask { background-color: #e8f1f3; height: 64px; diff --git a/source/app/stylesheets/roster.css b/source/app/stylesheets/roster.css index d107f39..aa2d736 100644 --- a/source/app/stylesheets/roster.css +++ b/source/app/stylesheets/roster.css @@ -356,7 +356,10 @@ html[dir="rtl"] .manage-infos p.bm-group input { background-position: 1px -2047px; } -body.in_jingle_call .call-jingle { +body.in_jingle_call .call-jingle, +body.in_muji_call .call-jingle, +body.in_jingle_call .roster-muji, +body.in_muji_call .roster-muji { opacity: 0.35; } @@ -367,7 +370,10 @@ body.in_jingle_call .call-jingle { display: none; } -body.in_jingle_call .call-jingle a { +body.in_jingle_call .call-jingle a, +body.in_muji_call .call-jingle a, +body.in_jingle_call .roster-muji a, +body.in_muji_call .roster-muji a { cursor: default; } @@ -455,7 +461,7 @@ html[dir="rtl"] #roster .roster-icon { width: 16px; display: block; } - + #roster .add, #page-engine .text .tools-add { background-position: 0 -1047px; @@ -469,7 +475,11 @@ html[dir="rtl"] #roster .roster-icon { #page-switch .groupchat-default { background-position: 0 -1082px; } - + +#roster .muji { + background-position: 0 -2047px; +} + #roster .more { background-position: 0 -1100px; } @@ -520,7 +530,15 @@ html[dir="rtl"] .buddy-conf-subarrow { margin-right: 9px; float: right; } - + +.buddy-conf-muji .buddy-conf-subarrow { + margin-left: 8px; +} + +html[dir="rtl"] .buddy-conf-muji .buddy-conf-subarrow { + margin-right: 8px; +} + .buddy-conf-subitem { background-color: rgb(0,0,0); background-color: rgba(0,0,0,0.8); diff --git a/source/app/stylesheets/tools.css b/source/app/stylesheets/tools.css index 0222c0e..9688fa3 100644 --- a/source/app/stylesheets/tools.css +++ b/source/app/stylesheets/tools.css @@ -61,46 +61,51 @@ html[dir="rtl"] #top-content .tools-all { float: left; } -#top-content .jingle { +#top-content .call { background-position: 7px -2114px; display: none; } -#top-content .jingle.video { +#top-content .call .notify { + margin-left: 0; + left: -2px; +} + +#top-content .call.video { background-position: 7px -2205px; } -#top-content .jingle.active, -#top-content .jingle.streaming { +#top-content .call.active, +#top-content .call.streaming { display: block; } -#top-content .jingle.active { +#top-content .call.active { -webkit-animation: tool_active 1.5s infinite ease-in-out; -moz-animation: tool_active 1.5s infinite ease-in-out; -o-animation: tool_active 1.5s infinite ease-in-out; animation: tool_active 1.5s infinite ease-in-out; } -#top-content .jingle.streaming { +#top-content .call.streaming { padding-left: 30px; } -#top-content .jingle .streaming-items { +#top-content .call .streaming-items { display: none; } -#top-content .jingle.streaming .streaming-items { +#top-content .call.streaming .streaming-items { display: block; } -#top-content .jingle.streaming .streaming-items .counter { +#top-content .call.streaming .streaming-items .counter { font-size: 11px; font-style: italic; font-weight: bold; } -#top-content .jingle.streaming .streaming-items a.stop { +#top-content .call.streaming .streaming-items a.stop { background: #cc283f; border-left: 1px solid #a12032; color: #ffffff; @@ -116,7 +121,7 @@ html[dir="rtl"] #top-content .tools-all { border-bottom-right-radius: 4px; } -#top-content .jingle.streaming .streaming-items a.stop:active { +#top-content .call.streaming .streaming-items a.stop:active { background: #a92134; padding-top: 6px; padding-bottom: 6px; @@ -135,23 +140,27 @@ html[dir="rtl"] #top-content .tools-all { #top-content .notifications:hover, #top-content .music:hover, -#top-content .jingle.streaming.video:hover { +#top-content .call.streaming.video:hover, +.in_muji_call #top-content .call.streaming.audio:hover { cursor: pointer; } #top-content .music:hover, #top-content .notifications:hover, -#top-content .jingle.streaming.video:hover, +#top-content .call.streaming.video:hover, +.in_muji_call #top-content .call.streaming.audio:hover, #top-content .music:focus, #top-content .notifications:focus, -#top-content .jingle.streaming.video:focus { +#top-content .call.streaming.video:focus, +.in_muji_call #top-content .call.streaming.audio:focus { background-color: rgb(232,241,243); background-color: rgba(232,241,243,0.7); } #top-content .music:active, #top-content .notifications:active, -#top-content .jingle.streaming.video:active { +#top-content .call.streaming.video:active, +.in_muji_call #top-content .call.streaming.audio:active { background-color: rgb(232,241,243); background-color: rgba(232,241,243,0.8); } @@ -485,25 +494,25 @@ html[dir="rtl"] .music-content .list { height: 15px; } -.jingle-content { +.call-content { text-shadow: none; width: 230px; right: -102px; } -html[dir="rtl"] .jingle-content { +html[dir="rtl"] .call-content { left: -102px; } -.jingle-content .tools-content-subitem { +.call-content .tools-content-subitem { position: relative; } -.jingle-content .jingle-notify { +.call-content .call-notify { height: 90px; } -.jingle-content .jingle-notify .avatar-pane { +.call-content .call-notify .avatar-pane { width: 100px; position: absolute; left: 0; @@ -517,12 +526,12 @@ html[dir="rtl"] .jingle-content { border-top-left-radius: 4px; } -html[dir="rtl"] .jingle-content .jingle-notify .avatar-pane { +html[dir="rtl"] .call-content .call-notify .avatar-pane { left: auto; right: 0; } -.jingle-content .jingle-notify .avatar-pane .avatar-container { +.call-content .call-notify .avatar-pane .avatar-container { overflow: hidden; position: absolute; left: 0; @@ -531,7 +540,7 @@ html[dir="rtl"] .jingle-content .jingle-notify .avatar-pane { top: 0; } -.jingle-content .jingle-notify .avatar-pane .avatar-container .avatar { +.call-content .call-notify .avatar-pane .avatar-container .avatar { min-height: 100%; max-height: 100%; min-width: 100%; @@ -543,7 +552,7 @@ html[dir="rtl"] .jingle-content .jingle-notify .avatar-pane { border-bottom-left-radius: 5px; } -html[dir="rtl"] .jingle-content .jingle-notify .avatar-pane .avatar-container .avatar { +html[dir="rtl"] .call-content .call-notify .avatar-pane .avatar-container .avatar { -moz-border-radius: 0; -webkit-border-radius: 0; border-radius: 0; @@ -555,45 +564,45 @@ html[dir="rtl"] .jingle-content .jingle-notify .avatar-pane .avatar-container .a border-bottom-right-radius: 5px; } -.jingle-content .jingle-notify .avatar-pane .icon { +.call-content .call-notify .avatar-pane .icon { opacity: 0.75; position: absolute; left: 8px; bottom: 8px; } -.jingle-content .jingle-notify.notify-call_audio .avatar-pane .icon { +.call-content .call-notify.notify-call_audio .avatar-pane .icon { background-position: 0 -120px; width: 33px; height: 33px; } -.jingle-content .jingle-notify.notify-call_video .avatar-pane .icon { +.call-content .call-notify.notify-call_video .avatar-pane .icon { background-position: 0 -154px; width: 33px; height: 22px; } -.jingle-content .jingle-notify.notify-connecting .avatar-pane .icon { +.call-content .call-notify.notify-connecting .avatar-pane .icon { background-position: 0 -175px; width: 33px; height: 32px; } -.jingle-content .jingle-notify.notify-error .avatar-pane .icon { +.call-content .call-notify.notify-error .avatar-pane .icon { background-position: 0 -207px; width: 33px; height: 31px; } -.jingle-content .jingle-notify.notify-local_ended .avatar-pane .icon, -.jingle-content .jingle-notify.notify-remote_ended .avatar-pane .icon { +.call-content .call-notify.notify-local_ended .avatar-pane .icon, +.call-content .call-notify.notify-remote_ended .avatar-pane .icon { background-position: 0 -238px; width: 33px; height: 34px; } -.jingle-content .jingle-notify .notification-content { +.call-content .call-notify .notification-content { color: #ffffff; text-align: left; text-shadow: 0 1px 1px rgb(0,0,0); @@ -606,28 +615,28 @@ html[dir="rtl"] .jingle-content .jingle-notify .avatar-pane .avatar-container .a left: 100px; } -html[dir="rtl"] .jingle-content .jingle-notify .notification-content { +html[dir="rtl"] .call-content .call-notify .notification-content { text-align: right; right: 100px; left: 0; } -.jingle-content .jingle-notify .notification-content .fullname, -.jingle-content .jingle-notify .notification-content .text { +.call-content .call-notify .notification-content .fullname, +.call-content .call-notify .notification-content .text { display: block; } -.jingle-content .jingle-notify .notification-content .fullname { +.call-content .call-notify .notification-content .fullname { font-weight: bold; } -.jingle-content .jingle-notify .notification-content .text { +.call-content .call-notify .notification-content .text { font-size: 12px; text-transform: lowercase; margin-top: 2px; } -.jingle-content .jingle-notify .notification-content .reply-buttons { +.call-content .call-notify .notification-content .reply-buttons { text-align: center; padding-left: 10px; position: absolute; @@ -636,31 +645,31 @@ html[dir="rtl"] .jingle-content .jingle-notify .notification-content { bottom: 20px; } -html[dir="rtl"] .jingle-content .jingle-notify .notification-content .reply-buttons { +html[dir="rtl"] .call-content .call-notify .notification-content .reply-buttons { padding-left: 0; padding-right: 10px; } -.jingle-content .jingle-notify .notification-content .reply-buttons a.reply-button { +.call-content .call-notify .notification-content .reply-buttons a.reply-button { margin-left: 4px; float: left; } -html[dir="rtl"] .jingle-content .jingle-notify .notification-content .reply-buttons a.reply-button { +html[dir="rtl"] .call-content .call-notify .notification-content .reply-buttons a.reply-button { margin-left: 0; margin-right: 4px; float: right; } -.jingle-content .jingle-notify .notification-content .reply-buttons a.reply-button:active { +.call-content .call-notify .notification-content .reply-buttons a.reply-button:active { margin-top: 0; } -.jingle-content .jingle-notify .notification-content .reply-buttons a.reply-button.first { +.call-content .call-notify .notification-content .reply-buttons a.reply-button.first { margin-left: 0; } -html[dir="rtl"] .jingle-content .jingle-notify .notification-content .reply-buttons a.reply-button.first { +html[dir="rtl"] .call-content .call-notify .notification-content .reply-buttons a.reply-button.first { margin-right: 0; } diff --git a/source/dev/images/placeholders/jingle_audio_local.psd b/source/dev/images/placeholders/jingle_audio_local.psd new file mode 100644 index 0000000000000000000000000000000000000000..dd0389b36c4448ea4fcb14d0f856c58b422796e7 GIT binary patch literal 55994 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 literal 0 HcmV?d00001 diff --git a/source/dev/images/placeholders/jingle_video_local.psd b/source/dev/images/placeholders/jingle_video_local.psd index b037339c942727d363c5663dd94065ad845cab0d..ab6683af246a8a07483c80cd2d7cfd62159e5892 100644 GIT binary patch literal 58201 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 FaA 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

h6sZwct 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 diff --git a/source/dev/images/placeholders/jingle_video_remote.psd b/source/dev/images/placeholders/jingle_video_remote.psd new file mode 100644 index 0000000000000000000000000000000000000000..b3d6dfaeac2b950ac700c44c0a76aedaba81b006 GIT binary patch literal 375103 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 diff --git a/source/dev/images/sprites/talk.psd b/source/dev/images/sprites/talk.psd index 0214165fc062d0796ea33ff3f2821b8cc29f5744..06433697c15ff4263381441f921f09a7da34bde6 100644 GIT binary patch delta 6936 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 delta 11644 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 diff --git a/source/i18n/ar/LC_MESSAGES/main.mo b/source/i18n/ar/LC_MESSAGES/main.mo index 5fa92fe552bc01c78069eb2b68ad8117c713f338..b1da7850e2b65c7368589be1cfa4188311da9426 100644 GIT binary patch delta 1530 zcmXZcOGwmF6vy#9E`7cZ6muu_%ZhOq--2sOUl8U;n%M+;i{$f6l$2@fq~tHPrJ@PzlFT1>f8GUsz1+^&2w}L)eS; zsJuV@31??1g~qU1la5LhKy6?-s=#JcCmO7cs04Al-i~_jgq=TUiLzZ zgX}=nOT_F0hpb0%77LxY0I%R&e1>}QqjeH1iNB+s57Os4tVK5-MSa?nsLphw3ie?- z-o^XWH=_*Nm^j0>_3cth9DS&QOHdnGiB-7A#+Oic;i>f<>ZHF>pKMyG6VE{vF0}D7 zRNgh1O?|VGfm*W*BbdNc97KJZJJ^cPQ3Y1Bn+n{7Ds&h%e+t!!^QaT{+xeU5A%2MJ z#Hfv5;X3M@_Y78HU?CR{_o7bLi~70UMQ!8-`tdF5M6NQYa5gG&ku`)Hwl+uOwFjdu{|9Z+mgfBj7R96c8i%63T`pJU(4)Mqng0OaZKcEj delta 1522 zcmXZcOGwmF6vy$arHF6S6wSw^W0uc+4ETU3v-K@l`lsfdb+Aahd$N)M^P zWH#c$(2`IrB(p_{ML}dSZM1@*q6bJyeSiG#>T}P%|NlAnp8KD<({!zA?0JyaA7IR< zEyg4mW9Czg2}i&6D~1rSVkict8WW8XSb?cnjdd7_KHP~DxEYsGdHLhZbHc*Hvuotz#TI&&1f)=~ph1$S*JAc{6H|)9(U93-FFupctP%74#5)Lp`5KpDVExgYYcsYA>KVGk_{Mgb_H2 zv(z_p40@R8W81pBHEY~9r(iB>BOWZp1{+^TeG8AR?@%ZGg}SnTHjd143MZo03s46s z#{}w|N(O38Jr?0948sxB)jYs%e2Pj?#%>C+3RUPNYW^as6IW3u9JcfKFobv-)rmP9 zzry|0H%kn5p?e3(u^Dx;+o+%0Bx)ni(1mYNtz1PFj>>itCt0&FnE4WPVFl{>!#3{5 z5aNFHYH*8zUhvt4QR{uwi!-RZe2wwAg8JCjtuZ-H!c0^r^R3kwO5B2ckERRRvl&3; zo5`X7YT+Ui+K3<3qR*%oe_Kti6NjN%8*g=ED{&E4;UG4mANkKRd-AwvJc>HtAS&+= zs-vTM^goWlG!uM><|T&XAJi3W$amrttS8P!oxB&d;Xzd5VdRHkM(_|WpuUmx0_U5m z#bV-1s18h_KKA8O1}+Bc*o&$Bf2!p^48ti@p?Oq-MO5oHj-L)2iViKzD=KjnddfXz n{qE%4nEal`_O{LwyIVUN+uHM+J3GePlA8kpipO81)WrS=B7dck diff --git a/source/i18n/ar/LC_MESSAGES/main.po b/source/i18n/ar/LC_MESSAGES/main.po index bfb0a35..b1380d3 100644 --- a/source/i18n/ar/LC_MESSAGES/main.po +++ b/source/i18n/ar/LC_MESSAGES/main.po @@ -4,12 +4,12 @@ # License: AGPL # Authors: Valérian Saliou, JanCBorchardt # Translators: -# Valérian Saliou , 2013 +# Valérian Saliou , 2013 msgid "" msgstr "" "Project-Id-Version: Jappix\n" -"PO-Revision-Date: 2014-01-24 15:59+0100\n" -"Last-Translator: Valérian Saliou \n" +"PO-Revision-Date: 2014-05-08 10:20+0100\n" +"Last-Translator: Valérian Saliou \n" "Language-Team: Arabic (http://www.transifex.com/projects/p/jappix/language/" "ar/)\n" "MIME-Version: 1.0\n" @@ -19,7 +19,7 @@ msgstr "" "Plural-Forms: nplurals=6; plural=n==0 ? 0 : n==1 ? 1 : n==2 ? 2 : n%100>=3 " "&& n%100<=10 ? 3 : n%100>=11 && n%100<=99 ? 4 : 5;\n" "POT-Creation-Date: \n" -"X-Generator: Poedit 1.6.3\n" +"X-Generator: Poedit 1.6.5\n" msgid "default:LTR" msgstr "default:RTL" @@ -1376,6 +1376,9 @@ msgstr "" msgid "Compression" msgstr "" +msgid "Cache assets" +msgstr "" + msgid "" "This page helps you specify the default hosts Jappix will connect to. You " "can leave it as it is and continue if you want to use the official service " diff --git a/source/i18n/bg/LC_MESSAGES/main.mo b/source/i18n/bg/LC_MESSAGES/main.mo index 8138f5c89c7c15430e71dc12597b1e8f21793ab2..9697ce69d8f4e6008da03efca01a42ed6764c12b 100644 GIT binary patch delta 11660 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 T!y`U*Q^$lpsXZ?KoAv(y-$Vx0 delta 11838 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 iV{gV6;ThXr_Y\n" +"PO-Revision-Date: 2014-05-08 10:20+0100\n" +"Last-Translator: Valérian Saliou \n" "Language-Team: Bulgarian (http://www.transifex.com/projects/p/jappix/" "language/bg/)\n" "MIME-Version: 1.0\n" @@ -17,7 +17,7 @@ msgstr "" "Language: bg\n" "Plural-Forms: nplurals=2; plural=(n != 1);\n" "POT-Creation-Date: \n" -"X-Generator: Poedit 1.6.3\n" +"X-Generator: Poedit 1.6.5\n" msgid "default:LTR" msgstr "default:LTR" @@ -855,7 +855,7 @@ msgid "Hi there!" msgstr "Ей, ти там, здравей!" msgid "Welcome to %1s, “%2s”." -msgstr "Добре дошли в %1s, “%2s”." +msgstr "" msgid "Login to your existing XMPP account or create a new one for free!" msgstr "" @@ -1212,7 +1212,7 @@ msgid "Friends" msgstr "Приятели" msgid "Welcome to Jappix, your own social cloud!" -msgstr "Добре дошли в Jappix, твоят собствен социален облак!" +msgstr "" msgid "" "Before you start using it, you will have to change some settings, search for " @@ -1444,6 +1444,9 @@ msgstr "Принудително използване на HTTPS" msgid "Compression" msgstr "Компресия" +msgid "Cache assets" +msgstr "" + msgid "" "This page helps you specify the default hosts Jappix will connect to. You " "can leave it as it is and continue if you want to use the official service " diff --git a/source/i18n/cs/LC_MESSAGES/main.mo b/source/i18n/cs/LC_MESSAGES/main.mo index 5098294f40f8be34c8d4124732190889a28a3441..45f63ba1a3d4f4971b70cd705260a064495b934d 100644 GIT binary patch delta 16659 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 KWb3P?mHr2cJuz7T delta 14826 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, 2012 -# moneytoo , 2013 +# moneytoo , 2013-2014 msgid "" msgstr "" "Project-Id-Version: Jappix\n" -"PO-Revision-Date: 2014-01-24 16:00+0100\n" -"Last-Translator: Valérian Saliou \n" +"PO-Revision-Date: 2014-05-08 10:20+0100\n" +"Last-Translator: Valérian Saliou \n" "Language-Team: Czech (http://www.transifex.com/projects/p/jappix/language/" "cs/)\n" "MIME-Version: 1.0\n" @@ -19,7 +19,7 @@ msgstr "" "Language: cs\n" "Plural-Forms: nplurals=3; plural=(n==1) ? 0 : (n>=2 && n<=4) ? 1 : 2;\n" "POT-Creation-Date: \n" -"X-Generator: Poedit 1.6.3\n" +"X-Generator: Poedit 1.6.5\n" msgid "default:LTR" msgstr "default:LTR" @@ -850,7 +850,7 @@ msgid "Hi there!" msgstr "Ahoj!" msgid "Welcome to %1s, “%2s”." -msgstr "Vítejte na %1s, “%2s”." +msgstr "Vítejte na %1s, \"%2s\"." msgid "Login to your existing XMPP account or create a new one for free!" msgstr "" @@ -1210,7 +1210,7 @@ msgid "Friends" msgstr "Kamarádi" msgid "Welcome to Jappix, your own social cloud!" -msgstr "Vítejte v Jappix, vaší vlastní sociální síti!" +msgstr "Vítá Vás Jappix, Váš vlastní sociální cloud!" msgid "" "Before you start using it, you will have to change some settings, search for " @@ -1330,12 +1330,17 @@ msgid "" "Jappix stores persistent data (such as shared files, chat logs, your own " "music and its configuration) into multiple storage folders." msgstr "" +"Jappix ukládá trvalá data (jako jsou sdílené soubory, historie rozhovorů, " +"vaši hudbu a konfiguraci) do několika adresářů." msgid "" "Jappix must be able to write in this folder to create its sub-directories. " "If not, you must set the rights of %1s to %2s or change the folder owner to " "%3s (depending of your configuration)." msgstr "" +"Jappix musí mít právo zapisovat do tohoto adresáře pro vytváření pod-" +"adresářů. Pokud nemá, musíte nastavit práva %1 na %2 nebo změnit vlastníka " +"adresáře na %3s (v závislosti na vaší konfiguraci)." msgid "The folder is writable, you can continue!" msgstr "Do adresáře je povolen zápis, můžete pokračovat!" @@ -1436,6 +1441,9 @@ msgstr "Vynutit HTTPS" msgid "Compression" msgstr "Komprese" +msgid "Cache assets" +msgstr "" + msgid "" "This page helps you specify the default hosts Jappix will connect to. You " "can leave it as it is and continue if you want to use the official service " @@ -1471,7 +1479,7 @@ msgid "BOSH host" msgstr "BOSH host" msgid "WebSocket host" -msgstr "" +msgstr "WebSocket host" msgid "" "You can install some extra softwares on your server, to extend your Jappix " @@ -1763,7 +1771,7 @@ msgid "Total" msgstr "Celkem" msgid "Archives" -msgstr "" +msgstr "Archívy" msgid "Music" msgstr "Hudba" @@ -2322,73 +2330,73 @@ msgid "Content ads key" msgstr "Klíč pro obsahové reklamy" msgid "AdSense client ID" -msgstr "" +msgstr "AdSense ID klienta" msgid "AdSense slot" -msgstr "" +msgstr "AdSense slot" msgid "Stop" -msgstr "" +msgstr "Zastavit" msgid "Mute" -msgstr "" +msgstr "Ztlumit" msgid "Unmute" -msgstr "" +msgstr "Povolit" msgid "Is calling you" -msgstr "" +msgstr "Vám volá" msgid "Initiating call" -msgstr "" +msgstr "Spojování hovoru" msgid "Connecting to call..." -msgstr "" +msgstr "Spojování hovoru..." msgid "Waiting..." -msgstr "" +msgstr "Čekám..." msgid "Ringing..." -msgstr "" +msgstr "Vyzvání..." msgid "Declined the call" -msgstr "" +msgstr "Odmítl hovor" msgid "Call error" -msgstr "" +msgstr "Chyba hovoru" msgid "Ended the call" -msgstr "" +msgstr "Ukončil hovor" msgid "Call ended" -msgstr "" +msgstr "Hovor ukončen" msgid "Call canceled" -msgstr "" +msgstr "Hovor zrušen" msgid "Canceled the call" -msgstr "" +msgstr "Zrušil hovor" msgid "Is already in a call" -msgstr "" +msgstr "Právě hovoří" msgid "Ending call..." -msgstr "" +msgstr "Ukončování hovoru..." msgid "Accept" -msgstr "" +msgstr "Přijmout" msgid "Decline" -msgstr "" +msgstr "Odmítnout" msgid "Okay" -msgstr "" +msgstr "Jasně" msgid "Retry" -msgstr "" +msgstr "Znovu" msgid "Audio Call" -msgstr "" +msgstr "Hlasový hovor" msgid "Video Call" -msgstr "" +msgstr "Video hovor" diff --git a/source/i18n/de/LC_MESSAGES/main.mo b/source/i18n/de/LC_MESSAGES/main.mo index 8fab197b933ba8fc506ef9d64216a430a0445e5a..582aa0252e876253f04cabc36d7499142ec9cbcd 100644 GIT binary patch delta 15089 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%0DF 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