telephony 1.0.3

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (365) hide show
  1. checksums.yaml +15 -0
  2. data/README.md +105 -0
  3. data/Rakefile +39 -0
  4. data/app/assets/fonts/telephony/zest-telephony.eot +0 -0
  5. data/app/assets/fonts/telephony/zest-telephony.svg +73 -0
  6. data/app/assets/fonts/telephony/zest-telephony.ttf +0 -0
  7. data/app/assets/fonts/telephony/zest-telephony.woff +0 -0
  8. data/app/assets/images/telephony/backspace.png +0 -0
  9. data/app/assets/images/telephony/icon-spinner.gif +0 -0
  10. data/app/assets/javascripts/telephony/application.js +12 -0
  11. data/app/assets/javascripts/telephony/collections/agents.js +4 -0
  12. data/app/assets/javascripts/telephony/config.js.erb +11 -0
  13. data/app/assets/javascripts/telephony/models/agent.js +103 -0
  14. data/app/assets/javascripts/telephony/models/conversation.js +226 -0
  15. data/app/assets/javascripts/telephony/models/device.js +30 -0
  16. data/app/assets/javascripts/telephony/models/transfer.js +65 -0
  17. data/app/assets/javascripts/telephony/namespace.js +6 -0
  18. data/app/assets/javascripts/telephony/push.js +80 -0
  19. data/app/assets/javascripts/telephony/vendor/backbone.js +1159 -0
  20. data/app/assets/javascripts/telephony/vendor/chosen.jquery.js +1018 -0
  21. data/app/assets/javascripts/telephony/vendor/jquery.form.js +1117 -0
  22. data/app/assets/javascripts/telephony/vendor/pusher.js +1304 -0
  23. data/app/assets/javascripts/telephony/vendor/test_dependencies.js +18 -0
  24. data/app/assets/javascripts/telephony/vendor/underscore.js +839 -0
  25. data/app/assets/javascripts/telephony/views/agents_view.js +69 -0
  26. data/app/assets/javascripts/telephony/views/application_view.js +78 -0
  27. data/app/assets/javascripts/telephony/views/call_queue_view.js +21 -0
  28. data/app/assets/javascripts/telephony/views/conversation_buttons_view.js +15 -0
  29. data/app/assets/javascripts/telephony/views/conversation_view.js +322 -0
  30. data/app/assets/javascripts/telephony/views/status_view.js +36 -0
  31. data/app/assets/javascripts/telephony/views/transfer_view.js +87 -0
  32. data/app/assets/javascripts/telephony/views/twilio_client_view.js +117 -0
  33. data/app/assets/javascripts/telephony/views/widget_view.js +55 -0
  34. data/app/assets/javascripts/telephony/widget.js +5 -0
  35. data/app/assets/javascripts/templates/telephony/agents_view.jst.ejs +8 -0
  36. data/app/assets/javascripts/templates/telephony/call_queue_view.jst.ejs +2 -0
  37. data/app/assets/javascripts/templates/telephony/conversation_buttons_view.jst.ejs +23 -0
  38. data/app/assets/javascripts/templates/telephony/conversation_view.jst.ejs +10 -0
  39. data/app/assets/javascripts/templates/telephony/status_view.jst.ejs +4 -0
  40. data/app/assets/javascripts/templates/telephony/transfer_view.jst.ejs +38 -0
  41. data/app/assets/javascripts/templates/telephony/twilio_client_view.jst.ejs +10 -0
  42. data/app/assets/stylesheets/telephony/font.css +43 -0
  43. data/app/assets/stylesheets/telephony/reset.css.scss +10 -0
  44. data/app/assets/stylesheets/telephony/widget.css.scss +512 -0
  45. data/app/controllers/telephony/agents_controller.rb +46 -0
  46. data/app/controllers/telephony/application_controller.rb +4 -0
  47. data/app/controllers/telephony/call_centers_controller.rb +9 -0
  48. data/app/controllers/telephony/conversations_controller.rb +74 -0
  49. data/app/controllers/telephony/inbound/conversation_queues_controller.rb +21 -0
  50. data/app/controllers/telephony/playable_listeners_controller.rb +43 -0
  51. data/app/controllers/telephony/providers/twilio/application_controller.rb +26 -0
  52. data/app/controllers/telephony/providers/twilio/calls_controller.rb +130 -0
  53. data/app/controllers/telephony/providers/twilio/inbound_calls_controller.rb +54 -0
  54. data/app/controllers/telephony/providers/twilio/musics_controller.rb +12 -0
  55. data/app/controllers/telephony/providers/twilio/voicemails_controller.rb +29 -0
  56. data/app/controllers/telephony/signals/agents/presences_controller.rb +64 -0
  57. data/app/controllers/telephony/transfers_controller.rb +18 -0
  58. data/app/controllers/telephony/twilio_client_controller.rb +37 -0
  59. data/app/controllers/telephony/voicemails_controller.rb +13 -0
  60. data/app/controllers/telephony/widget_controller.rb +10 -0
  61. data/app/helpers/telephony/application_helper.rb +4 -0
  62. data/app/helpers/telephony/calls_helper.rb +13 -0
  63. data/app/models/telephony/agent.rb +186 -0
  64. data/app/models/telephony/agent_state_machine.rb +54 -0
  65. data/app/models/telephony/base.rb +7 -0
  66. data/app/models/telephony/blacklisted_number.rb +5 -0
  67. data/app/models/telephony/call.rb +94 -0
  68. data/app/models/telephony/call_center.rb +26 -0
  69. data/app/models/telephony/call_state_machine.rb +98 -0
  70. data/app/models/telephony/conversation.rb +273 -0
  71. data/app/models/telephony/conversation_state_machine.rb +109 -0
  72. data/app/models/telephony/conversations_presenter.rb +83 -0
  73. data/app/models/telephony/events.rb +6 -0
  74. data/app/models/telephony/events/answer.rb +6 -0
  75. data/app/models/telephony/events/base.rb +118 -0
  76. data/app/models/telephony/events/busy.rb +6 -0
  77. data/app/models/telephony/events/call_answered.rb +22 -0
  78. data/app/models/telephony/events/call_fail.rb +6 -0
  79. data/app/models/telephony/events/complete_hold.rb +28 -0
  80. data/app/models/telephony/events/complete_one_step_transfer.rb +17 -0
  81. data/app/models/telephony/events/complete_resume.rb +28 -0
  82. data/app/models/telephony/events/complete_two_step_transfer.rb +6 -0
  83. data/app/models/telephony/events/conference.rb +6 -0
  84. data/app/models/telephony/events/connect.rb +22 -0
  85. data/app/models/telephony/events/customer_left_two_step_transfer.rb +6 -0
  86. data/app/models/telephony/events/dial_agent.rb +6 -0
  87. data/app/models/telephony/events/ended.rb +19 -0
  88. data/app/models/telephony/events/enqueue.rb +6 -0
  89. data/app/models/telephony/events/fail_one_step_transfer.rb +9 -0
  90. data/app/models/telephony/events/fail_two_step_transfer.rb +6 -0
  91. data/app/models/telephony/events/initialize_widget.rb +15 -0
  92. data/app/models/telephony/events/initiate_hold.rb +6 -0
  93. data/app/models/telephony/events/initiate_one_step_transfer.rb +6 -0
  94. data/app/models/telephony/events/initiate_resume.rb +6 -0
  95. data/app/models/telephony/events/initiate_two_step_transfer.rb +6 -0
  96. data/app/models/telephony/events/leave_two_step_transfer.rb +17 -0
  97. data/app/models/telephony/events/leave_voicemail.rb +22 -0
  98. data/app/models/telephony/events/no_answer.rb +6 -0
  99. data/app/models/telephony/events/play_closed_greeting.rb +6 -0
  100. data/app/models/telephony/events/play_message.rb +6 -0
  101. data/app/models/telephony/events/reject.rb +6 -0
  102. data/app/models/telephony/events/rona.rb +6 -0
  103. data/app/models/telephony/events/start.rb +17 -0
  104. data/app/models/telephony/events/straight_to_voicemail.rb +6 -0
  105. data/app/models/telephony/events/terminate.rb +6 -0
  106. data/app/models/telephony/events/transfer.rb +35 -0
  107. data/app/models/telephony/inbound_conversation_queue.rb +101 -0
  108. data/app/models/telephony/playable.rb +7 -0
  109. data/app/models/telephony/playable_listener.rb +42 -0
  110. data/app/models/telephony/pusher_event_publisher.rb +26 -0
  111. data/app/models/telephony/recording.rb +4 -0
  112. data/app/models/telephony/voicemail.rb +36 -0
  113. data/app/observers/telephony/agent_observer.rb +8 -0
  114. data/app/observers/telephony/call_observer.rb +15 -0
  115. data/app/observers/telephony/conversation_observer.rb +9 -0
  116. data/app/observers/telephony/event_observer.rb +9 -0
  117. data/app/views/layouts/telephony/application.html.erb +14 -0
  118. data/app/views/telephony/providers/twilio/calls/child_detached.builder +5 -0
  119. data/app/views/telephony/providers/twilio/calls/complete_hold.builder +6 -0
  120. data/app/views/telephony/providers/twilio/calls/connect.builder +13 -0
  121. data/app/views/telephony/providers/twilio/calls/dial.builder +14 -0
  122. data/app/views/telephony/providers/twilio/calls/done.builder +5 -0
  123. data/app/views/telephony/providers/twilio/calls/join_conference.builder +7 -0
  124. data/app/views/telephony/providers/twilio/calls/whisper_tone.builder +3 -0
  125. data/app/views/telephony/providers/twilio/inbound_calls/closed_hours.builder +7 -0
  126. data/app/views/telephony/providers/twilio/inbound_calls/create.builder +8 -0
  127. data/app/views/telephony/providers/twilio/inbound_calls/enqueue.builder +8 -0
  128. data/app/views/telephony/providers/twilio/inbound_calls/reject.builder +5 -0
  129. data/app/views/telephony/providers/twilio/inbound_calls/wait_music.builder +5 -0
  130. data/app/views/telephony/providers/twilio/musics/hold.builder +5 -0
  131. data/app/views/telephony/providers/twilio/voicemails/new.builder +6 -0
  132. data/app/views/telephony/twilio_client/index.html.erb +74 -0
  133. data/app/views/telephony/widget/index.erb +3 -0
  134. data/config/cucumber.yml +8 -0
  135. data/config/database.yml +57 -0
  136. data/config/environment.rb +67 -0
  137. data/config/initializers/pusher.rb +21 -0
  138. data/config/initializers/telephony.rb +44 -0
  139. data/config/routes.rb +94 -0
  140. data/config/wopr.yml.example +38 -0
  141. data/db/migrate/20130806213053_bootstrap_db.rb +105 -0
  142. data/db/migrate/20131009204026_create_telephony_blacklisted_numbers.rb +11 -0
  143. data/lib/agent_generator.rb +24 -0
  144. data/lib/tasks/cucumber.rake +65 -0
  145. data/lib/tasks/jasmine.rake +8 -0
  146. data/lib/tasks/telephony_tasks.rake +4 -0
  147. data/lib/telephony.rb +34 -0
  148. data/lib/telephony/concerns/controllers/twilio_request_verifier.rb +39 -0
  149. data/lib/telephony/conversation_data.rb +67 -0
  150. data/lib/telephony/engine.rb +19 -0
  151. data/lib/telephony/error.rb +8 -0
  152. data/lib/telephony/error/agent_on_a_call.rb +2 -0
  153. data/lib/telephony/error/base.rb +3 -0
  154. data/lib/telephony/error/connection.rb +2 -0
  155. data/lib/telephony/error/not_in_progress.rb +2 -0
  156. data/lib/telephony/error/queue_empty.rb +2 -0
  157. data/lib/telephony/helper.rb +11 -0
  158. data/lib/telephony/jobs/agent_offline.rb +28 -0
  159. data/lib/telephony/jobs/pusher_event.rb +21 -0
  160. data/lib/telephony/providers/twilio_provider.rb +162 -0
  161. data/lib/telephony/version.rb +3 -0
  162. data/spec/controllers/telephony/agents_controller_spec.rb +117 -0
  163. data/spec/controllers/telephony/call_centers_controller_spec.rb +25 -0
  164. data/spec/controllers/telephony/conversations_controller_spec.rb +229 -0
  165. data/spec/controllers/telephony/playable_listeners_controller_spec.rb +138 -0
  166. data/spec/controllers/telephony/providers/twilio/calls_controller_spec.rb +33 -0
  167. data/spec/controllers/telephony/providers/twilio/musics_controller_spec.rb +20 -0
  168. data/spec/controllers/telephony/signals/agents/presences_controller_spec.rb +154 -0
  169. data/spec/controllers/telephony/twilio_client_controller_spec.rb +58 -0
  170. data/spec/controllers/telephony/widget_controller_spec.rb +25 -0
  171. data/spec/dummy/Rakefile +7 -0
  172. data/spec/dummy/app/assets/javascripts/application.js +6 -0
  173. data/spec/dummy/app/assets/javascripts/lib/event_logger.js +82 -0
  174. data/spec/dummy/app/assets/javascripts/vendor/backbone.js +1159 -0
  175. data/spec/dummy/app/assets/javascripts/vendor/chosen.jquery.js +1018 -0
  176. data/spec/dummy/app/assets/javascripts/vendor/jquery.form.js +1117 -0
  177. data/spec/dummy/app/assets/javascripts/vendor/underscore.js +839 -0
  178. data/spec/dummy/app/assets/stylesheets/application.css +23 -0
  179. data/spec/dummy/app/controllers/application_controller.rb +3 -0
  180. data/spec/dummy/app/controllers/widget_host_controller.rb +2 -0
  181. data/spec/dummy/app/helpers/application_helper.rb +2 -0
  182. data/spec/dummy/app/views/layouts/application.html.erb +12 -0
  183. data/spec/dummy/app/views/widget_host/index.html.erb +53 -0
  184. data/spec/dummy/config.ru +4 -0
  185. data/spec/dummy/config/application.rb +61 -0
  186. data/spec/dummy/config/boot.rb +10 -0
  187. data/spec/dummy/config/call_centers.yml +35 -0
  188. data/spec/dummy/config/call_centers.yml.example +35 -0
  189. data/spec/dummy/config/database.yml +56 -0
  190. data/spec/dummy/config/environment.rb +5 -0
  191. data/spec/dummy/config/environments/cucumber.rb +39 -0
  192. data/spec/dummy/config/environments/development.rb +39 -0
  193. data/spec/dummy/config/environments/production.rb +67 -0
  194. data/spec/dummy/config/environments/test.rb +39 -0
  195. data/spec/dummy/config/initializers/backtrace_silencers.rb +7 -0
  196. data/spec/dummy/config/initializers/inflections.rb +15 -0
  197. data/spec/dummy/config/initializers/mime_types.rb +5 -0
  198. data/spec/dummy/config/initializers/secret_token.rb +7 -0
  199. data/spec/dummy/config/initializers/session_store.rb +8 -0
  200. data/spec/dummy/config/initializers/wrap_parameters.rb +14 -0
  201. data/spec/dummy/config/locales/en.yml +5 -0
  202. data/spec/dummy/config/pusher.yml +14 -0
  203. data/spec/dummy/config/pusher.yml.example +14 -0
  204. data/spec/dummy/config/routes.rb +5 -0
  205. data/spec/dummy/config/sms_whitelist.json.example +4 -0
  206. data/spec/dummy/config/twilio.yml +57 -0
  207. data/spec/dummy/config/twilio.yml.example +57 -0
  208. data/spec/dummy/db/schema.rb +122 -0
  209. data/spec/dummy/db/seeds.rb +6 -0
  210. data/spec/dummy/log/production.log +156 -0
  211. data/spec/dummy/log/test.log +13812 -0
  212. data/spec/dummy/public/404.html +26 -0
  213. data/spec/dummy/public/422.html +26 -0
  214. data/spec/dummy/public/500.html +25 -0
  215. data/spec/dummy/public/assets/application-628309db7ce8c0abdd91f8f796881599.js +32 -0
  216. data/spec/dummy/public/assets/application-628309db7ce8c0abdd91f8f796881599.js.gz +0 -0
  217. data/spec/dummy/public/assets/application-6c551430f3bdd32ba57c9c41e5f91846.css +24 -0
  218. data/spec/dummy/public/assets/application-6c551430f3bdd32ba57c9c41e5f91846.css.gz +0 -0
  219. data/spec/dummy/public/assets/telephony/backspace-9f5b195e049ed90eb20ecdda6e264b0a.png +0 -0
  220. data/spec/dummy/public/assets/telephony/icon-spinner-ddbae473a70c5425810aeee31d4a8ad7.gif +0 -0
  221. data/spec/dummy/public/assets/telephony/zest-telephony-0e6b13673634da80de9ae09bcca253e3.eot +0 -0
  222. data/spec/dummy/public/assets/telephony/zest-telephony-37f394757ccb11b978f16d9fd32cb3b5.ttf +0 -0
  223. data/spec/dummy/public/assets/telephony/zest-telephony-e7efdbc60c0a1c8404951bc79c7fb3a8.woff +0 -0
  224. data/spec/dummy/public/assets/telephony/zest-telephony-f34287df626936908408f53a33db9e83.svg +73 -0
  225. data/spec/dummy/public/favicon.ico +0 -0
  226. data/spec/dummy/script/rails +6 -0
  227. data/spec/dummy/tmp/cache/assets/C98/020/sprockets%2Fa83f8254d688334ff179984e423f502f +0 -0
  228. data/spec/dummy/tmp/cache/assets/CA4/550/sprockets%2F407f9191fe573f2435c55c017cb0d022 +0 -0
  229. data/spec/dummy/tmp/cache/assets/CAA/6A0/sprockets%2F3265550e2d2c7f5a864ca85d255504c2 +0 -0
  230. data/spec/dummy/tmp/cache/assets/CB0/F60/sprockets%2F462f9efed31e6710732376ae06c27208 +0 -0
  231. data/spec/dummy/tmp/cache/assets/CB6/E20/sprockets%2Fa7092cd9f16917099d754f1380af13e1 +0 -0
  232. data/spec/dummy/tmp/cache/assets/CC7/9F0/sprockets%2F0517100815cd45c43fec1d8510fcb630 +0 -0
  233. data/spec/dummy/tmp/cache/assets/CCA/BF0/sprockets%2F16ea15e0b5d367c34b812612a4207e3e +0 -0
  234. data/spec/dummy/tmp/cache/assets/CD8/370/sprockets%2F357970feca3ac29060c1e3861e2c0953 +0 -0
  235. data/spec/dummy/tmp/cache/assets/CF1/960/sprockets%2F36ef26985821ec702c74dae4af789903 +0 -0
  236. data/spec/dummy/tmp/cache/assets/D07/F30/sprockets%2Fd12862f20df0c9ef7aa2b4216d247079 +0 -0
  237. data/spec/dummy/tmp/cache/assets/D09/BF0/sprockets%2F5c8595efaa8cc5235a09801c60e25a16 +0 -0
  238. data/spec/dummy/tmp/cache/assets/D0D/250/sprockets%2F5e759d66e374910dae5a51b221e6e3a4 +0 -0
  239. data/spec/dummy/tmp/cache/assets/D0D/E30/sprockets%2Fa282879e0788ba133f5bdc211cf134e8 +0 -0
  240. data/spec/dummy/tmp/cache/assets/D11/8E0/sprockets%2F2a193a8af29963f1113bef05788e02ff +0 -0
  241. data/spec/dummy/tmp/cache/assets/D14/270/sprockets%2F7f95a752910ed33d728532f8fabd340d +0 -0
  242. data/spec/dummy/tmp/cache/assets/D17/BE0/sprockets%2F58749d7aea4ca766c2520801f0f96b9b +0 -0
  243. data/spec/dummy/tmp/cache/assets/D18/270/sprockets%2Fbb2d88813b1795a97e9a7c14100eef77 +0 -0
  244. data/spec/dummy/tmp/cache/assets/D22/310/sprockets%2Fbae9148ce1229214dfbb0c425b0054d0 +0 -0
  245. data/spec/dummy/tmp/cache/assets/D30/EB0/sprockets%2F2c668b88beb4c9805d91ed4587f549e7 +0 -0
  246. data/spec/dummy/tmp/cache/assets/D32/A10/sprockets%2F13fe41fee1fe35b49d145bcc06610705 +0 -0
  247. data/spec/dummy/tmp/cache/assets/D3C/690/sprockets%2F681ba175fdd2da8d590b7e624b6135a6 +0 -0
  248. data/spec/dummy/tmp/cache/assets/D44/BC0/sprockets%2F7214f66dd91fdd51f00c3882293f5eea +0 -0
  249. data/spec/dummy/tmp/cache/assets/D46/FA0/sprockets%2F4f9543cd9e68ba3141fd488afb13681b +0 -0
  250. data/spec/dummy/tmp/cache/assets/D4E/1B0/sprockets%2Ff7cbd26ba1d28d48de824f0e94586655 +0 -0
  251. data/spec/dummy/tmp/cache/assets/D51/9A0/sprockets%2Fb6e4cbcf30f6f611671695f79ea846d5 +0 -0
  252. data/spec/dummy/tmp/cache/assets/D55/920/sprockets%2F0a02242aa4c4febcc796419aba103c69 +0 -0
  253. data/spec/dummy/tmp/cache/assets/D5A/EA0/sprockets%2Fd771ace226fc8215a3572e0aa35bb0d6 +0 -0
  254. data/spec/dummy/tmp/cache/assets/D5F/6F0/sprockets%2F155e7bc44f326371cce0ca8bc5b2414e +0 -0
  255. data/spec/dummy/tmp/cache/assets/D6B/250/sprockets%2Fe4eecce7a3371b11038c92f07cb28e67 +0 -0
  256. data/spec/dummy/tmp/cache/assets/D6B/E70/sprockets%2F103a456ae3692e3cca20b7696fdee8f0 +0 -0
  257. data/spec/dummy/tmp/cache/assets/D7F/AD0/sprockets%2Fb1c02d12303b1fe2f981771cbb0c4cbd +0 -0
  258. data/spec/dummy/tmp/cache/assets/D81/D00/sprockets%2F0c68b6d9dce98681a2a710d9fc3f7e39 +0 -0
  259. data/spec/dummy/tmp/cache/assets/D94/4D0/sprockets%2F4608a5bdf27ddf11bcaed1375911e9a2 +0 -0
  260. data/spec/dummy/tmp/cache/assets/D9A/580/sprockets%2F433005b19aaf7ed5cba9c7f8cd30b775 +0 -0
  261. data/spec/dummy/tmp/cache/assets/DA4/270/sprockets%2F860ab3bd30204e02c0a5d82bd4bbabe1 +0 -0
  262. data/spec/dummy/tmp/cache/assets/DA4/480/sprockets%2F53e39285bb57a2ddb6ff856b2eb491da +0 -0
  263. data/spec/dummy/tmp/cache/assets/DA6/3C0/sprockets%2F587c7e35f47442eadaa530acc5e9f5e6 +0 -0
  264. data/spec/dummy/tmp/cache/assets/DAB/DA0/sprockets%2Fb8fb5e8518ed3d38a7f6a8f18a9208aa +0 -0
  265. data/spec/dummy/tmp/cache/assets/DB2/700/sprockets%2Ffa85cb79b94fce1b8344d983ea863d2e +0 -0
  266. data/spec/dummy/tmp/cache/assets/DCC/4F0/sprockets%2Fbc0f3c61e16afb5a285ba8b39846ed8b +0 -0
  267. data/spec/dummy/tmp/cache/assets/DDC/400/sprockets%2Fcffd775d018f68ce5dba1ee0d951a994 +0 -0
  268. data/spec/dummy/tmp/cache/assets/DDC/BE0/sprockets%2F2c0a4d4675a61b014aedbbabb3f071fc +0 -0
  269. data/spec/dummy/tmp/cache/assets/E04/640/sprockets%2F3c5ddf2bbefd9d70f1b506b519cf856e +0 -0
  270. data/spec/dummy/tmp/cache/assets/E04/7F0/sprockets%2F35f1b2ff729a5ec21f6dc5f7d6faa67b +0 -0
  271. data/spec/dummy/tmp/cache/assets/E04/890/sprockets%2F2f5173deea6c795b8fdde723bb4b63af +0 -0
  272. data/spec/dummy/tmp/cache/assets/E07/230/sprockets%2Febd4f2dd708879d593aff008ccb75acc +0 -0
  273. data/spec/dummy/tmp/cache/assets/E17/9D0/sprockets%2F8db2b79f6c4eabe7a9ebe7a7898ec066 +0 -0
  274. data/spec/dummy/tmp/cache/assets/E31/F10/sprockets%2Fcebacf56667da1cc2961b59fd922eafb +0 -0
  275. data/spec/factories/agent.rb +26 -0
  276. data/spec/factories/calls.rb +106 -0
  277. data/spec/factories/conversations.rb +171 -0
  278. data/spec/factories/events.rb +250 -0
  279. data/spec/factories/playable_listeners.rb +6 -0
  280. data/spec/factories/recordings.rb +6 -0
  281. data/spec/factories/voicemails.rb +8 -0
  282. data/spec/fixtures/vcr_cassettes/Authenticating_an_online_user.yml +90 -0
  283. data/spec/fixtures/vcr_cassettes/Creating_a_conversation/creates_a_new_call.yml +42 -0
  284. data/spec/fixtures/vcr_cassettes/Creating_a_conversation/creates_a_new_conversation.yml +42 -0
  285. data/spec/fixtures/vcr_cassettes/Creating_a_conversation/returns_the_conversation_as_JSON.yml +42 -0
  286. data/spec/fixtures/vcr_cassettes/Pusher_channel_as_JSON.yml +90 -0
  287. data/spec/fixtures/vcr_cassettes/Pusher_to_publish_the_event.yml +32 -0
  288. data/spec/fixtures/vcr_cassettes/Reloading_the_widget_during_a_call/_create/during_a_call/after_a_failed_transfer/pushes_the_event_for_the_active_call.yml +1110 -0
  289. data/spec/fixtures/vcr_cassettes/Telephony_ConversationsController/_create/by_default/creates_a_new_call.yml +45 -0
  290. data/spec/fixtures/vcr_cassettes/Telephony_ConversationsController/_create/by_default/creates_a_new_conversation.yml +43 -0
  291. data/spec/fixtures/vcr_cassettes/Telephony_ConversationsController/_create/by_default/returns_the_conversation_as_JSON.yml +44 -0
  292. data/spec/fixtures/vcr_cassettes/Telephony_Providers_TwilioProvider/_call/allows_the_phone_to_ring_for_60_seconds.yml +42 -0
  293. data/spec/fixtures/vcr_cassettes/Telephony_Providers_TwilioProvider/_call/includes_a_status_change_callback_url.yml +42 -0
  294. data/spec/fixtures/vcr_cassettes/Telephony_Providers_TwilioProvider/_call/places_a_call.yml +42 -0
  295. data/spec/fixtures/vcr_cassettes/Telephony_Providers_TwilioProvider/_dial_into_conference/allows_the_phone_to_ring_for_15_seconds.yml +42 -0
  296. data/spec/fixtures/vcr_cassettes/Telephony_Providers_TwilioProvider/_dial_into_conference/places_a_call_to_redirect_the_participant_to_a_conference.yml +42 -0
  297. data/spec/fixtures/vcr_cassettes/Telephony_Providers_TwilioProvider/_hangup/by_default/hangs_up_the_call.yml +42 -0
  298. data/spec/fixtures/vcr_cassettes/buying_number_for_an_area_code.yml +42 -0
  299. data/spec/fixtures/vcr_cassettes/returns_success.yml +90 -0
  300. data/spec/javascripts/helpers/jasmine-jquery.js +546 -0
  301. data/spec/javascripts/helpers/mock-ajax.js +207 -0
  302. data/spec/javascripts/support/jasmine.yml +80 -0
  303. data/spec/javascripts/telephony/models/agent_spec.js +40 -0
  304. data/spec/javascripts/telephony/models/conversation_spec.js +515 -0
  305. data/spec/javascripts/telephony/models/device_spec.js +80 -0
  306. data/spec/javascripts/telephony/models/transfer_spec.js +22 -0
  307. data/spec/javascripts/telephony/push_spec.js +427 -0
  308. data/spec/javascripts/telephony/views/agents_view_spec.js +101 -0
  309. data/spec/javascripts/telephony/views/application_view_spec.js +74 -0
  310. data/spec/javascripts/telephony/views/conversation_view_spec.js +626 -0
  311. data/spec/javascripts/telephony/views/status_view_spec.js +30 -0
  312. data/spec/javascripts/telephony/views/transfer_view_spec.js +187 -0
  313. data/spec/javascripts/telephony/views/twilio_client_view_spec.js +77 -0
  314. data/spec/javascripts/telephony/views/widget_view_spec.js +20 -0
  315. data/spec/lib/telephony/concerns/controllers/twilio_request_verifier_spec.rb +77 -0
  316. data/spec/lib/telephony/conversation_data_spec.rb +342 -0
  317. data/spec/lib/telephony/helper_spec.rb +24 -0
  318. data/spec/lib/telephony/jobs/agent_offline_spec.rb +37 -0
  319. data/spec/lib/telephony/jobs/pusher_event_spec.rb +39 -0
  320. data/spec/lib/telephony/providers/twilio_provider_spec.rb +439 -0
  321. data/spec/lib/telephony_spec.rb +104 -0
  322. data/spec/models/telephony/agent_spec.rb +479 -0
  323. data/spec/models/telephony/agent_state_machine_spec.rb +131 -0
  324. data/spec/models/telephony/call_center_spec.rb +32 -0
  325. data/spec/models/telephony/call_spec.rb +597 -0
  326. data/spec/models/telephony/call_state_machine_spec.rb +472 -0
  327. data/spec/models/telephony/conversation_spec.rb +751 -0
  328. data/spec/models/telephony/conversation_state_machine_spec.rb +387 -0
  329. data/spec/models/telephony/conversations_presenter_spec.rb +40 -0
  330. data/spec/models/telephony/event_spec.rb +716 -0
  331. data/spec/models/telephony/inbound_conversation_queue_spec.rb +243 -0
  332. data/spec/models/telephony/playable_listener_spec.rb +43 -0
  333. data/spec/models/telephony/pusher_event_publisher_spec.rb +36 -0
  334. data/spec/models/telephony/recording_spec.rb +6 -0
  335. data/spec/models/telephony/voicemail_spec.rb +96 -0
  336. data/spec/observers/telephony/agent_observer_spec.rb +21 -0
  337. data/spec/observers/telephony/event_observer_spec.rb +19 -0
  338. data/spec/requests/create_conversation_spec.rb +39 -0
  339. data/spec/requests/dequeue_call_spec.rb +42 -0
  340. data/spec/requests/list_conversations_spec.rb +33 -0
  341. data/spec/requests/presences_spec.rb +51 -0
  342. data/spec/requests/providers/twilio/calls/child_answered_spec.rb +24 -0
  343. data/spec/requests/providers/twilio/calls/child_detached_spec.rb +249 -0
  344. data/spec/requests/providers/twilio/calls/dial_spec.rb +132 -0
  345. data/spec/requests/providers/twilio/calls/done_spec.rb +69 -0
  346. data/spec/requests/providers/twilio/calls/join_conference_spec.rb +56 -0
  347. data/spec/requests/providers/twilio/calls/leave_queue_spec.rb +44 -0
  348. data/spec/requests/providers/twilio/calls/parent_answered_spec.rb +59 -0
  349. data/spec/requests/providers/twilio/inbound/connect_dequeued_call_spec.rb +124 -0
  350. data/spec/requests/providers/twilio/inbound/enqueue_inbound_call_spec.rb +108 -0
  351. data/spec/requests/providers/twilio/inbound/enqueued_call_wait_music_spec.rb +13 -0
  352. data/spec/requests/providers/twilio/voicemails/leave_voicemail_spec.rb +78 -0
  353. data/spec/requests/search_conversations_spec.rb +101 -0
  354. data/spec/requests/transfer_spec.rb +65 -0
  355. data/spec/requests/voicemail_api_spec.rb +58 -0
  356. data/spec/spec_helper.rb +27 -0
  357. data/spec/support/factory_girl.rb +5 -0
  358. data/spec/support/matchers/be_complete_hold.rb +20 -0
  359. data/spec/support/matchers/be_whisper_tone.rb +35 -0
  360. data/spec/support/matchers/join_conference.rb +23 -0
  361. data/spec/support/nokogiri.rb +1 -0
  362. data/spec/support/pusher_helper.rb +24 -0
  363. data/spec/support/vcr.rb +12 -0
  364. data/spec/support/webmock.rb +1 -0
  365. metadata +692 -0
@@ -0,0 +1,1304 @@
1
+ /*!
2
+ * Pusher JavaScript Library v1.12.5
3
+ * http://pusherapp.com/
4
+ *
5
+ * Copyright 2011, Pusher
6
+ * Released under the MIT licence.
7
+ */
8
+
9
+ ;(function() {
10
+ if (Function.prototype.scopedTo === undefined) {
11
+ Function.prototype.scopedTo = function(context, args) {
12
+ var f = this;
13
+ return function() {
14
+ return f.apply(context, Array.prototype.slice.call(args || [])
15
+ .concat(Array.prototype.slice.call(arguments)));
16
+ };
17
+ };
18
+ }
19
+
20
+ var Pusher = function(app_key, options) {
21
+ this.options = options || {};
22
+ this.key = app_key;
23
+ this.channels = new Pusher.Channels();
24
+ this.global_emitter = new Pusher.EventsDispatcher()
25
+
26
+ var self = this;
27
+
28
+ this.checkAppKey();
29
+
30
+ this.connection = new Pusher.Connection(this.key, this.options);
31
+
32
+ // Setup / teardown connection
33
+ this.connection
34
+ .bind('connected', function() {
35
+ self.subscribeAll();
36
+ })
37
+ .bind('message', function(params) {
38
+ var internal = (params.event.indexOf('pusher_internal:') === 0);
39
+ if (params.channel) {
40
+ var channel;
41
+ if (channel = self.channel(params.channel)) {
42
+ channel.emit(params.event, params.data);
43
+ }
44
+ }
45
+ // Emit globaly [deprecated]
46
+ if (!internal) self.global_emitter.emit(params.event, params.data);
47
+ })
48
+ .bind('disconnected', function() {
49
+ self.channels.disconnect();
50
+ })
51
+ .bind('error', function(err) {
52
+ Pusher.warn('Error', err);
53
+ });
54
+
55
+ Pusher.instances.push(this);
56
+
57
+ if (Pusher.isReady) self.connect();
58
+ };
59
+ Pusher.instances = [];
60
+ Pusher.prototype = {
61
+ channel: function(name) {
62
+ return this.channels.find(name);
63
+ },
64
+
65
+ connect: function() {
66
+ this.connection.connect();
67
+ },
68
+
69
+ disconnect: function() {
70
+ this.connection.disconnect();
71
+ },
72
+
73
+ bind: function(event_name, callback) {
74
+ this.global_emitter.bind(event_name, callback);
75
+ return this;
76
+ },
77
+
78
+ bind_all: function(callback) {
79
+ this.global_emitter.bind_all(callback);
80
+ return this;
81
+ },
82
+
83
+ subscribeAll: function() {
84
+ var channel;
85
+ for (channelName in this.channels.channels) {
86
+ if (this.channels.channels.hasOwnProperty(channelName)) {
87
+ this.subscribe(channelName);
88
+ }
89
+ }
90
+ },
91
+
92
+ subscribe: function(channel_name) {
93
+ var self = this;
94
+ var channel = this.channels.add(channel_name, this);
95
+
96
+ if (this.connection.state === 'connected') {
97
+ channel.authorize(this.connection.socket_id, this.options, function(err, data) {
98
+ if (err) {
99
+ channel.emit('pusher:subscription_error', data);
100
+ } else {
101
+ self.send_event('pusher:subscribe', {
102
+ channel: channel_name,
103
+ auth: data.auth,
104
+ channel_data: data.channel_data
105
+ });
106
+ }
107
+ });
108
+ }
109
+ return channel;
110
+ },
111
+
112
+ unsubscribe: function(channel_name) {
113
+ this.channels.remove(channel_name);
114
+ if (this.connection.state === 'connected') {
115
+ this.send_event('pusher:unsubscribe', {
116
+ channel: channel_name
117
+ });
118
+ }
119
+ },
120
+
121
+ send_event: function(event_name, data, channel) {
122
+ return this.connection.send_event(event_name, data, channel);
123
+ },
124
+
125
+ checkAppKey: function() {
126
+ if(this.key === null || this.key === undefined) {
127
+ Pusher.warn('Warning', 'You must pass your app key when you instantiate Pusher.');
128
+ }
129
+ }
130
+ };
131
+
132
+ Pusher.Util = {
133
+ extend: function extend(target, extensions) {
134
+ for (var property in extensions) {
135
+ if (extensions[property] && extensions[property].constructor &&
136
+ extensions[property].constructor === Object) {
137
+ target[property] = extend(target[property] || {}, extensions[property]);
138
+ } else {
139
+ target[property] = extensions[property];
140
+ }
141
+ }
142
+ return target;
143
+ },
144
+
145
+ stringify: function stringify() {
146
+ var m = ["Pusher"]
147
+ for (var i = 0; i < arguments.length; i++){
148
+ if (typeof arguments[i] === "string") {
149
+ m.push(arguments[i])
150
+ } else {
151
+ if (window['JSON'] == undefined) {
152
+ m.push(arguments[i].toString());
153
+ } else {
154
+ m.push(JSON.stringify(arguments[i]))
155
+ }
156
+ }
157
+ };
158
+ return m.join(" : ")
159
+ },
160
+
161
+ arrayIndexOf: function(array, item) { // MSIE doesn't have array.indexOf
162
+ var nativeIndexOf = Array.prototype.indexOf;
163
+ if (array == null) return -1;
164
+ if (nativeIndexOf && array.indexOf === nativeIndexOf) return array.indexOf(item);
165
+ for (i = 0, l = array.length; i < l; i++) if (array[i] === item) return i;
166
+ return -1;
167
+ }
168
+ };
169
+
170
+ // To receive log output provide a Pusher.log function, for example
171
+ // Pusher.log = function(m){console.log(m)}
172
+ Pusher.debug = function() {
173
+ if (!Pusher.log) return
174
+ Pusher.log(Pusher.Util.stringify.apply(this, arguments))
175
+ }
176
+ Pusher.warn = function() {
177
+ if (window.console && window.console.warn) {
178
+ window.console.warn(Pusher.Util.stringify.apply(this, arguments));
179
+ } else {
180
+ if (!Pusher.log) return
181
+ Pusher.log(Pusher.Util.stringify.apply(this, arguments));
182
+ }
183
+ };
184
+
185
+ // Pusher defaults
186
+ Pusher.VERSION = '1.12.5';
187
+ // WS connection parameters
188
+ Pusher.host = 'ws.pusherapp.com';
189
+ Pusher.ws_port = 80;
190
+ Pusher.wss_port = 443;
191
+ // SockJS fallback parameters
192
+ Pusher.sockjs_host = 'sockjs.pusher.com';
193
+ Pusher.sockjs_http_port = 80
194
+ Pusher.sockjs_https_port = 443
195
+ Pusher.sockjs_path = "/pusher"
196
+ // Other settings
197
+ Pusher.channel_auth_endpoint = '/pusher/auth';
198
+ Pusher.cdn_http = 'http://js.pusher.com/'
199
+ Pusher.cdn_https = 'https://d3dy5gmtp8yhk7.cloudfront.net/'
200
+ Pusher.dependency_suffix = '';
201
+ Pusher.channel_auth_transport = 'ajax';
202
+ Pusher.activity_timeout = 120000;
203
+ Pusher.pong_timeout = 30000;
204
+
205
+ Pusher.isReady = false;
206
+ Pusher.ready = function() {
207
+ Pusher.isReady = true;
208
+ for (var i = 0, l = Pusher.instances.length; i < l; i++) {
209
+ Pusher.instances[i].connect();
210
+ }
211
+ };
212
+
213
+ this.Pusher = Pusher;
214
+ }).call(this);
215
+
216
+ ;(function() {
217
+ /* Abstract event binding
218
+ Example:
219
+
220
+ var MyEventEmitter = function(){};
221
+ MyEventEmitter.prototype = new Pusher.EventsDispatcher;
222
+
223
+ var emitter = new MyEventEmitter();
224
+
225
+ // Bind to single event
226
+ emitter.bind('foo_event', function(data){ alert(data)} );
227
+
228
+ // Bind to all
229
+ emitter.bind_all(function(eventName, data){ alert(data) });
230
+
231
+ --------------------------------------------------------*/
232
+
233
+ function CallbackRegistry() {
234
+ this._callbacks = {};
235
+ };
236
+
237
+ CallbackRegistry.prototype.get = function(eventName) {
238
+ return this._callbacks[this._prefix(eventName)];
239
+ };
240
+
241
+ CallbackRegistry.prototype.add = function(eventName, callback) {
242
+ var prefixedEventName = this._prefix(eventName);
243
+ this._callbacks[prefixedEventName] = this._callbacks[prefixedEventName] || [];
244
+ this._callbacks[prefixedEventName].push(callback);
245
+ };
246
+
247
+ CallbackRegistry.prototype.remove = function(eventName, callback) {
248
+ if(this.get(eventName)) {
249
+ var index = Pusher.Util.arrayIndexOf(this.get(eventName), callback);
250
+ this._callbacks[this._prefix(eventName)].splice(index, 1);
251
+ }
252
+ };
253
+
254
+ CallbackRegistry.prototype._prefix = function(eventName) {
255
+ return "_" + eventName;
256
+ };
257
+
258
+
259
+ function EventsDispatcher(failThrough) {
260
+ this.callbacks = new CallbackRegistry();
261
+ this.global_callbacks = [];
262
+ // Run this function when dispatching an event when no callbacks defined
263
+ this.failThrough = failThrough;
264
+ }
265
+
266
+ EventsDispatcher.prototype.bind = function(eventName, callback) {
267
+ this.callbacks.add(eventName, callback);
268
+ return this;// chainable
269
+ };
270
+
271
+ EventsDispatcher.prototype.unbind = function(eventName, callback) {
272
+ this.callbacks.remove(eventName, callback);
273
+ return this;
274
+ };
275
+
276
+ EventsDispatcher.prototype.emit = function(eventName, data) {
277
+ // Global callbacks
278
+ for (var i = 0; i < this.global_callbacks.length; i++) {
279
+ this.global_callbacks[i](eventName, data);
280
+ }
281
+
282
+ // Event callbacks
283
+ var callbacks = this.callbacks.get(eventName);
284
+ if (callbacks) {
285
+ for (var i = 0; i < callbacks.length; i++) {
286
+ callbacks[i](data);
287
+ }
288
+ } else if (this.failThrough) {
289
+ this.failThrough(eventName, data)
290
+ }
291
+
292
+ return this;
293
+ };
294
+
295
+ EventsDispatcher.prototype.bind_all = function(callback) {
296
+ this.global_callbacks.push(callback);
297
+ return this;
298
+ };
299
+
300
+ this.Pusher.EventsDispatcher = EventsDispatcher;
301
+ }).call(this);
302
+
303
+ ;(function() {
304
+ var Pusher = this.Pusher;
305
+
306
+ /*-----------------------------------------------
307
+ Helpers:
308
+ -----------------------------------------------*/
309
+
310
+ function capitalize(str) {
311
+ return str.substr(0, 1).toUpperCase() + str.substr(1);
312
+ }
313
+
314
+
315
+ function safeCall(method, obj, data) {
316
+ if (obj[method] !== undefined) {
317
+ obj[method](data);
318
+ }
319
+ }
320
+
321
+ /*-----------------------------------------------
322
+ The State Machine
323
+ -----------------------------------------------*/
324
+ function Machine(initialState, transitions, stateActions) {
325
+ Pusher.EventsDispatcher.call(this);
326
+
327
+ this.state = undefined;
328
+ this.errors = [];
329
+
330
+ // functions for each state
331
+ this.stateActions = stateActions;
332
+
333
+ // set up the transitions
334
+ this.transitions = transitions;
335
+
336
+ this.transition(initialState);
337
+ };
338
+
339
+ Machine.prototype.transition = function(nextState, data) {
340
+ var prevState = this.state;
341
+ var stateCallbacks = this.stateActions;
342
+
343
+ if (prevState && (Pusher.Util.arrayIndexOf(this.transitions[prevState], nextState) == -1)) {
344
+ this.emit('invalid_transition_attempt', {
345
+ oldState: prevState,
346
+ newState: nextState
347
+ });
348
+
349
+ throw new Error('Invalid transition [' + prevState + ' to ' + nextState + ']');
350
+ }
351
+
352
+ // exit
353
+ safeCall(prevState + 'Exit', stateCallbacks, data);
354
+
355
+ // tween
356
+ safeCall(prevState + 'To' + capitalize(nextState), stateCallbacks, data);
357
+
358
+ // pre
359
+ safeCall(nextState + 'Pre', stateCallbacks, data);
360
+
361
+ // change state:
362
+ this.state = nextState;
363
+
364
+ // handy to bind to
365
+ this.emit('state_change', {
366
+ oldState: prevState,
367
+ newState: nextState
368
+ });
369
+
370
+ // Post:
371
+ safeCall(nextState + 'Post', stateCallbacks, data);
372
+ };
373
+
374
+ Machine.prototype.is = function(state) {
375
+ return this.state === state;
376
+ };
377
+
378
+ Machine.prototype.isNot = function(state) {
379
+ return this.state !== state;
380
+ };
381
+
382
+ Pusher.Util.extend(Machine.prototype, Pusher.EventsDispatcher.prototype);
383
+
384
+ this.Pusher.Machine = Machine;
385
+ }).call(this);
386
+
387
+ ;(function() {
388
+ /*
389
+ A little bauble to interface with window.navigator.onLine,
390
+ window.ononline and window.onoffline. Easier to mock.
391
+ */
392
+
393
+ var NetInfo = function() {
394
+ var self = this;
395
+ Pusher.EventsDispatcher.call(this);
396
+ // This is okay, as IE doesn't support this stuff anyway.
397
+ if (window.addEventListener !== undefined) {
398
+ window.addEventListener("online", function() {
399
+ self.emit('online', null);
400
+ }, false);
401
+ window.addEventListener("offline", function() {
402
+ self.emit('offline', null);
403
+ }, false);
404
+ }
405
+ };
406
+
407
+ // Offline means definitely offline (no connection to router).
408
+ // Inverse does NOT mean definitely online (only currently supported in Safari
409
+ // and even there only means the device has a connection to the router).
410
+ NetInfo.prototype.isOnLine = function() {
411
+ if (window.navigator.onLine === undefined) {
412
+ return true;
413
+ } else {
414
+ return window.navigator.onLine;
415
+ }
416
+ };
417
+
418
+ Pusher.Util.extend(NetInfo.prototype, Pusher.EventsDispatcher.prototype);
419
+
420
+ this.Pusher.NetInfo = NetInfo;
421
+ }).call(this);
422
+
423
+ ;(function() {
424
+ var Pusher = this.Pusher;
425
+
426
+ var machineTransitions = {
427
+ 'initialized': ['waiting', 'failed'],
428
+ 'waiting': ['connecting', 'permanentlyClosed'],
429
+ 'connecting': ['open', 'permanentlyClosing', 'impermanentlyClosing', 'waiting'],
430
+ 'open': ['connected', 'permanentlyClosing', 'impermanentlyClosing', 'waiting'],
431
+ 'connected': ['permanentlyClosing', 'waiting'],
432
+ 'impermanentlyClosing': ['waiting', 'permanentlyClosing'],
433
+ 'permanentlyClosing': ['permanentlyClosed'],
434
+ 'permanentlyClosed': ['waiting', 'failed'],
435
+ 'failed': ['permanentlyClosed']
436
+ };
437
+
438
+
439
+ var OPEN_TIMEOUT_INCREMENT = 2000;
440
+ var CONNECTED_TIMEOUT_INCREMENT = 2000;
441
+
442
+ var MAX_OPEN_TIMEOUT = 10000;
443
+ var MAX_CONNECTED_TIMEOUT = 10000;
444
+
445
+ function resetConnectionParameters(connection) {
446
+ connection.connectionWait = 0;
447
+
448
+ if (Pusher.TransportType === 'native') {
449
+ connection.openTimeout = 4000;
450
+ } else if (Pusher.TransportType === 'flash') {
451
+ connection.openTimeout = 7000;
452
+ } else { // SockJS
453
+ connection.openTimeout = 6000;
454
+ }
455
+ connection.connectedTimeout = 2000;
456
+ connection.connectionSecure = connection.compulsorySecure;
457
+ connection.failedAttempts = 0;
458
+ }
459
+
460
+ function Connection(key, options) {
461
+ var self = this;
462
+
463
+ Pusher.EventsDispatcher.call(this);
464
+
465
+ this.ping = true
466
+ this.options = Pusher.Util.extend({encrypted: false}, options);
467
+
468
+ this.netInfo = new Pusher.NetInfo();
469
+
470
+ this.netInfo.bind('online', function(){
471
+ if (self._machine.is('waiting')) {
472
+ self._machine.transition('connecting');
473
+ updateState('connecting');
474
+ }
475
+ });
476
+
477
+ this.netInfo.bind('offline', function() {
478
+ if (self._machine.is('connected')) {
479
+ // These are for Chrome 15, which ends up
480
+ // having two sockets hanging around.
481
+ self.socket.onclose = undefined;
482
+ self.socket.onmessage = undefined;
483
+ self.socket.onerror = undefined;
484
+ self.socket.onopen = undefined;
485
+
486
+ self.socket.close();
487
+ self.socket = undefined;
488
+ self._machine.transition('waiting');
489
+ }
490
+ });
491
+
492
+ // define the state machine that runs the connection
493
+ this._machine = new Pusher.Machine('initialized', machineTransitions, {
494
+ initializedPre: function() {
495
+ self.compulsorySecure = self.options.encrypted;
496
+
497
+ self.key = key;
498
+ self.socket = null;
499
+ self.socket_id = null;
500
+
501
+ self.state = 'initialized';
502
+ },
503
+
504
+ waitingPre: function() {
505
+ if (self.netInfo.isOnLine()) {
506
+ if (self.failedAttempts < 2) {
507
+ updateState('connecting');
508
+ } else {
509
+ updateState('unavailable');
510
+ // Delay 10s between connection attempts on entering unavailable
511
+ self.connectionWait = 10000;
512
+ }
513
+
514
+ if (self.connectionWait > 0) {
515
+ self.emit('connecting_in', connectionDelay());
516
+ }
517
+
518
+ self._waitingTimer = setTimeout(function() {
519
+ // Even when unavailable we try connecting (not changing state)
520
+ self._machine.transition('connecting');
521
+ }, connectionDelay());
522
+ } else {
523
+ updateState('unavailable');
524
+ }
525
+ },
526
+
527
+ waitingExit: function() {
528
+ clearTimeout(self._waitingTimer);
529
+ },
530
+
531
+ connectingPre: function() {
532
+ // Case that a user manages to get to the connecting
533
+ // state even when offline.
534
+ if (self.netInfo.isOnLine() === false) {
535
+ self._machine.transition('waiting');
536
+ updateState('unavailable');
537
+
538
+ return;
539
+ }
540
+
541
+ var path = connectPath(self.key);
542
+ if (Pusher.TransportType === 'sockjs') {
543
+ Pusher.debug('Connecting to sockjs', Pusher.sockjs);
544
+ var url = buildSockJSURL(self.connectionSecure);
545
+
546
+ self.ping = false
547
+ self.socket = new SockJS(url);
548
+ self.socket.onopen = function() {
549
+ // SockJS does not yet support custom paths and query params
550
+ self.socket.send(JSON.stringify({path: path}));
551
+ self._machine.transition('open');
552
+ }
553
+ } else {
554
+ var url = connectBaseURL(self.connectionSecure) + path;
555
+ Pusher.debug('Connecting', url);
556
+ self.socket = new Pusher.Transport(url);
557
+ self.socket.onopen = function() {
558
+ self._machine.transition('open');
559
+ }
560
+ }
561
+
562
+ self.socket.onclose = transitionToWaiting;
563
+ self.socket.onerror = ws_onError;
564
+
565
+ // allow time to get ws_onOpen, otherwise close socket and try again
566
+ self._connectingTimer = setTimeout(TransitionToImpermanentlyClosing, self.openTimeout);
567
+ },
568
+
569
+ connectingExit: function() {
570
+ clearTimeout(self._connectingTimer);
571
+ self.socket.onopen = undefined; // unbind to avoid open events that are no longer relevant
572
+ },
573
+
574
+ connectingToWaiting: function() {
575
+ updateConnectionParameters();
576
+
577
+ // FUTURE: update only ssl
578
+ },
579
+
580
+ connectingToImpermanentlyClosing: function() {
581
+ updateConnectionParameters();
582
+
583
+ // FUTURE: update only timeout
584
+ },
585
+
586
+ openPre: function() {
587
+ self.socket.onmessage = ws_onMessageOpen;
588
+ self.socket.onerror = ws_onError;
589
+ self.socket.onclose = transitionToWaiting;
590
+
591
+ // allow time to get connected-to-Pusher message, otherwise close socket, try again
592
+ self._openTimer = setTimeout(TransitionToImpermanentlyClosing, self.connectedTimeout);
593
+ },
594
+
595
+ openExit: function() {
596
+ clearTimeout(self._openTimer);
597
+ self.socket.onmessage = undefined; // unbind to avoid messages that are no longer relevant
598
+ },
599
+
600
+ openToWaiting: function() {
601
+ updateConnectionParameters();
602
+ },
603
+
604
+ openToImpermanentlyClosing: function() {
605
+ updateConnectionParameters();
606
+ },
607
+
608
+ connectedPre: function(socket_id) {
609
+ self.socket_id = socket_id;
610
+
611
+ self.socket.onmessage = ws_onMessageConnected;
612
+ self.socket.onerror = ws_onError;
613
+ self.socket.onclose = transitionToWaiting;
614
+
615
+ resetConnectionParameters(self);
616
+ self.connectedAt = new Date().getTime();
617
+
618
+ resetActivityCheck();
619
+ },
620
+
621
+ connectedPost: function() {
622
+ updateState('connected');
623
+ },
624
+
625
+ connectedExit: function() {
626
+ stopActivityCheck();
627
+ updateState('disconnected');
628
+ },
629
+
630
+ impermanentlyClosingPost: function() {
631
+ if (self.socket) {
632
+ self.socket.onclose = transitionToWaiting;
633
+ self.socket.close();
634
+ }
635
+ },
636
+
637
+ permanentlyClosingPost: function() {
638
+ if (self.socket) {
639
+ self.socket.onclose = function() {
640
+ resetConnectionParameters(self);
641
+ self._machine.transition('permanentlyClosed');
642
+ };
643
+
644
+ self.socket.close();
645
+ } else {
646
+ resetConnectionParameters(self);
647
+ self._machine.transition('permanentlyClosed');
648
+ }
649
+ },
650
+
651
+ failedPre: function() {
652
+ updateState('failed');
653
+ Pusher.debug('WebSockets are not available in this browser.');
654
+ },
655
+
656
+ permanentlyClosedPost: function() {
657
+ updateState('disconnected');
658
+ }
659
+ });
660
+
661
+ /*-----------------------------------------------
662
+ -----------------------------------------------*/
663
+
664
+ function updateConnectionParameters() {
665
+ if (self.openTimeout < MAX_OPEN_TIMEOUT) {
666
+ self.openTimeout += OPEN_TIMEOUT_INCREMENT;
667
+ }
668
+
669
+ if (self.connectedTimeout < MAX_CONNECTED_TIMEOUT) {
670
+ self.connectedTimeout += CONNECTED_TIMEOUT_INCREMENT;
671
+ }
672
+
673
+ // Toggle between ws & wss
674
+ if (self.compulsorySecure !== true) {
675
+ self.connectionSecure = !self.connectionSecure;
676
+ }
677
+
678
+ self.failedAttempts++;
679
+ }
680
+
681
+ function connectBaseURL(isSecure) {
682
+ // Always connect with SSL if the current page served over https
683
+ var ssl = (isSecure || document.location.protocol === 'https:');
684
+ var port = ssl ? Pusher.wss_port : Pusher.ws_port;
685
+ var scheme = ssl ? 'wss://' : 'ws://';
686
+
687
+ return scheme + Pusher.host + ':' + port;
688
+ }
689
+
690
+ function connectPath(key) {
691
+ var flash = (Pusher.TransportType === "flash") ? "true" : "false";
692
+ var path = '/app/' + key + '?protocol=5&client=js'
693
+ + '&version=' + Pusher.VERSION
694
+ + '&flash=' + flash;
695
+ return path;
696
+ }
697
+
698
+ function buildSockJSURL(isSecure) {
699
+ var ssl = (isSecure || document.location.protocol === 'https:');
700
+ var port = ssl ? Pusher.sockjs_https_port : Pusher.sockjs_http_port;
701
+ var scheme = ssl ? 'https://' : 'http://';
702
+
703
+ return scheme + Pusher.sockjs_host + ':' + port + Pusher.sockjs_path;
704
+ }
705
+
706
+ // callback for close and retry. Used on timeouts.
707
+ function TransitionToImpermanentlyClosing() {
708
+ self._machine.transition('impermanentlyClosing');
709
+ }
710
+
711
+ function resetActivityCheck() {
712
+ if (self._activityTimer) { clearTimeout(self._activityTimer); }
713
+ // Send ping after inactivity
714
+ if (self.ping) {
715
+ self._activityTimer = setTimeout(function() {
716
+ self.send_event('pusher:ping', {})
717
+ // Wait for pong response
718
+ self._activityTimer = setTimeout(function() {
719
+ self.socket.close();
720
+ }, (self.options.pong_timeout || Pusher.pong_timeout))
721
+ }, (self.options.activity_timeout || Pusher.activity_timeout))
722
+ }
723
+ }
724
+
725
+ function stopActivityCheck() {
726
+ if (self._activityTimer) { clearTimeout(self._activityTimer); }
727
+ }
728
+
729
+ // Returns the delay before the next connection attempt should be made
730
+ //
731
+ // This function guards against attempting to connect more frequently than
732
+ // once every second
733
+ //
734
+ function connectionDelay() {
735
+ var delay = self.connectionWait;
736
+ if (delay === 0) {
737
+ if (self.connectedAt) {
738
+ var t = 1000;
739
+ var connectedFor = new Date().getTime() - self.connectedAt;
740
+ if (connectedFor < t) {
741
+ delay = t - connectedFor;
742
+ }
743
+ }
744
+ }
745
+ return delay;
746
+ }
747
+
748
+ /*-----------------------------------------------
749
+ WebSocket Callbacks
750
+ -----------------------------------------------*/
751
+
752
+ function handleCloseCode(code, message) {
753
+ // first inform the end-developer of this error
754
+ self.emit('error', {type: 'PusherError', data: {code: code, message: message}});
755
+
756
+ if (code === 4000) {
757
+ // SSL only app
758
+ self.compulsorySecure = true;
759
+ self.connectionSecure = true;
760
+ self.options.encrypted = true;
761
+
762
+ TransitionToImpermanentlyClosing();
763
+ } else if (code < 4100) {
764
+ // Permentently close connection
765
+ self._machine.transition('permanentlyClosing')
766
+ } else if (code < 4200) {
767
+ // Backoff before reconnecting
768
+ self.connectionWait = 1000;
769
+ self._machine.transition('waiting')
770
+ } else if (code < 4300) {
771
+ // Reconnect immediately
772
+ TransitionToImpermanentlyClosing();
773
+ } else {
774
+ // Unknown error
775
+ self._machine.transition('permanentlyClosing')
776
+ }
777
+ }
778
+
779
+ function ws_onMessageOpen(event) {
780
+ var params = parseWebSocketEvent(event);
781
+ if (params !== undefined) {
782
+ if (params.event === 'pusher:connection_established') {
783
+ self._machine.transition('connected', params.data.socket_id);
784
+ } else if (params.event === 'pusher:error') {
785
+ handleCloseCode(params.data.code, params.data.message)
786
+ }
787
+ }
788
+ }
789
+
790
+ function ws_onMessageConnected(event) {
791
+ resetActivityCheck();
792
+
793
+ var params = parseWebSocketEvent(event);
794
+ if (params !== undefined) {
795
+ Pusher.debug('Event recd', params);
796
+
797
+ switch (params.event) {
798
+ case 'pusher:error':
799
+ self.emit('error', {type: 'PusherError', data: params.data});
800
+ break;
801
+ case 'pusher:ping':
802
+ self.send_event('pusher:pong', {})
803
+ break;
804
+ }
805
+
806
+ self.emit('message', params);
807
+ }
808
+ }
809
+
810
+
811
+ /**
812
+ * Parses an event from the WebSocket to get
813
+ * the JSON payload that we require
814
+ *
815
+ * @param {MessageEvent} event The event from the WebSocket.onmessage handler.
816
+ **/
817
+ function parseWebSocketEvent(event) {
818
+ try {
819
+ var params = JSON.parse(event.data);
820
+
821
+ if (typeof params.data === 'string') {
822
+ try {
823
+ params.data = JSON.parse(params.data);
824
+ } catch (e) {
825
+ if (!(e instanceof SyntaxError)) {
826
+ throw e;
827
+ }
828
+ }
829
+ }
830
+
831
+ return params;
832
+ } catch (e) {
833
+ self.emit('error', {type: 'MessageParseError', error: e, data: event.data});
834
+ }
835
+ }
836
+
837
+ function transitionToWaiting() {
838
+ self._machine.transition('waiting');
839
+ }
840
+
841
+ function ws_onError(error) {
842
+ // just emit error to user - socket will already be closed by browser
843
+ self.emit('error', { type: 'WebSocketError', error: error });
844
+ }
845
+
846
+ // Updates the public state information exposed by connection
847
+ //
848
+ // This is distinct from the internal state information used by _machine
849
+ // to manage the connection
850
+ //
851
+ function updateState(newState, data) {
852
+ var prevState = self.state;
853
+ self.state = newState;
854
+
855
+ // Only emit when the state changes
856
+ if (prevState !== newState) {
857
+ Pusher.debug('State changed', prevState + ' -> ' + newState);
858
+ self.emit('state_change', {previous: prevState, current: newState});
859
+ self.emit(newState, data);
860
+ }
861
+ }
862
+ };
863
+
864
+ Connection.prototype.connect = function() {
865
+ // no WebSockets
866
+ if (!this._machine.is('failed') && !Pusher.Transport) {
867
+ this._machine.transition('failed');
868
+ }
869
+ // initial open of connection
870
+ else if(this._machine.is('initialized')) {
871
+ resetConnectionParameters(this);
872
+ this._machine.transition('waiting');
873
+ }
874
+ // user skipping connection wait
875
+ else if (this._machine.is('waiting') && this.netInfo.isOnLine() === true) {
876
+ this._machine.transition('connecting');
877
+ }
878
+ // user re-opening connection after closing it
879
+ else if(this._machine.is("permanentlyClosed")) {
880
+ resetConnectionParameters(this);
881
+ this._machine.transition('waiting');
882
+ }
883
+ };
884
+
885
+ Connection.prototype.send = function(data) {
886
+ if (this._machine.is('connected')) {
887
+ // Workaround for MobileSafari bug (see https://gist.github.com/2052006)
888
+ var self = this;
889
+ setTimeout(function() {
890
+ self.socket.send(data);
891
+ }, 0);
892
+ return true;
893
+ } else {
894
+ return false;
895
+ }
896
+ };
897
+
898
+ Connection.prototype.send_event = function(event_name, data, channel) {
899
+ var payload = {
900
+ event: event_name,
901
+ data: data
902
+ };
903
+ if (channel) payload['channel'] = channel;
904
+
905
+ Pusher.debug('Event sent', payload);
906
+ return this.send(JSON.stringify(payload));
907
+ }
908
+
909
+ Connection.prototype.disconnect = function() {
910
+ if (this._machine.is('permanentlyClosed')) return;
911
+
912
+ if (this._machine.is('waiting') || this._machine.is('failed')) {
913
+ this._machine.transition('permanentlyClosed');
914
+ } else {
915
+ this._machine.transition('permanentlyClosing');
916
+ }
917
+ };
918
+
919
+ Pusher.Util.extend(Connection.prototype, Pusher.EventsDispatcher.prototype);
920
+ this.Pusher.Connection = Connection;
921
+ }).call(this);
922
+
923
+ ;(function() {
924
+ Pusher.Channels = function() {
925
+ this.channels = {};
926
+ };
927
+
928
+ Pusher.Channels.prototype = {
929
+ add: function(channel_name, pusher) {
930
+ var existing_channel = this.find(channel_name);
931
+ if (!existing_channel) {
932
+ var channel = Pusher.Channel.factory(channel_name, pusher);
933
+ this.channels[channel_name] = channel;
934
+ return channel;
935
+ } else {
936
+ return existing_channel;
937
+ }
938
+ },
939
+
940
+ find: function(channel_name) {
941
+ return this.channels[channel_name];
942
+ },
943
+
944
+ remove: function(channel_name) {
945
+ delete this.channels[channel_name];
946
+ },
947
+
948
+ disconnect: function () {
949
+ for(var channel_name in this.channels){
950
+ this.channels[channel_name].disconnect()
951
+ }
952
+ }
953
+ };
954
+
955
+ Pusher.Channel = function(channel_name, pusher) {
956
+ var self = this;
957
+ Pusher.EventsDispatcher.call(this, function(event_name, event_data) {
958
+ Pusher.debug('No callbacks on ' + channel_name + ' for ' + event_name);
959
+ });
960
+
961
+ this.pusher = pusher;
962
+ this.name = channel_name;
963
+ this.subscribed = false;
964
+
965
+ this.bind('pusher_internal:subscription_succeeded', function(data) {
966
+ self.onSubscriptionSucceeded(data);
967
+ });
968
+ };
969
+
970
+ Pusher.Channel.prototype = {
971
+ // inheritable constructor
972
+ init: function() {},
973
+ disconnect: function() {
974
+ this.subscribed = false;
975
+ this.emit("pusher_internal:disconnected");
976
+ },
977
+
978
+ onSubscriptionSucceeded: function(data) {
979
+ this.subscribed = true;
980
+ this.emit('pusher:subscription_succeeded');
981
+ },
982
+
983
+ authorize: function(socketId, options, callback){
984
+ return callback(false, {}); // normal channels don't require auth
985
+ },
986
+
987
+ trigger: function(event, data) {
988
+ return this.pusher.send_event(event, data, this.name);
989
+ }
990
+ };
991
+
992
+ Pusher.Util.extend(Pusher.Channel.prototype, Pusher.EventsDispatcher.prototype);
993
+
994
+ Pusher.Channel.PrivateChannel = {
995
+ authorize: function(socketId, options, callback){
996
+ var self = this;
997
+ var authorizer = new Pusher.Channel.Authorizer(this, Pusher.channel_auth_transport, options);
998
+ return authorizer.authorize(socketId, function(err, authData) {
999
+ if(!err) {
1000
+ self.emit('pusher_internal:authorized', authData);
1001
+ }
1002
+
1003
+ callback(err, authData);
1004
+ });
1005
+ }
1006
+ };
1007
+
1008
+ Pusher.Channel.PresenceChannel = {
1009
+ init: function(){
1010
+ this.members = new Members(this); // leeches off channel events
1011
+ },
1012
+
1013
+ onSubscriptionSucceeded: function(data) {
1014
+ this.subscribed = true;
1015
+ // We override this because we want the Members obj to be responsible for
1016
+ // emitting the pusher:subscription_succeeded. It will do this after it has done its work.
1017
+ }
1018
+ };
1019
+
1020
+ var Members = function(channel) {
1021
+ var self = this;
1022
+
1023
+ var reset = function() {
1024
+ this._members_map = {};
1025
+ this.count = 0;
1026
+ this.me = null;
1027
+ };
1028
+ reset.call(this);
1029
+
1030
+ channel.bind('pusher_internal:authorized', function(authorizedData) {
1031
+ var channelData = JSON.parse(authorizedData.channel_data);
1032
+ channel.bind("pusher_internal:subscription_succeeded", function(subscriptionData) {
1033
+ self._members_map = subscriptionData.presence.hash;
1034
+ self.count = subscriptionData.presence.count;
1035
+ self.me = self.get(channelData.user_id);
1036
+ channel.emit('pusher:subscription_succeeded', self);
1037
+ });
1038
+ });
1039
+
1040
+ channel.bind('pusher_internal:member_added', function(data) {
1041
+ if(self.get(data.user_id) === null) { // only incr if user_id does not already exist
1042
+ self.count++;
1043
+ }
1044
+
1045
+ self._members_map[data.user_id] = data.user_info;
1046
+ channel.emit('pusher:member_added', self.get(data.user_id));
1047
+ });
1048
+
1049
+ channel.bind('pusher_internal:member_removed', function(data) {
1050
+ var member = self.get(data.user_id);
1051
+ if(member) {
1052
+ delete self._members_map[data.user_id];
1053
+ self.count--;
1054
+ channel.emit('pusher:member_removed', member);
1055
+ }
1056
+ });
1057
+
1058
+ channel.bind('pusher_internal:disconnected', function() {
1059
+ reset.call(self);
1060
+ });
1061
+ };
1062
+
1063
+ Members.prototype = {
1064
+ each: function(callback) {
1065
+ for(var i in this._members_map) {
1066
+ callback(this.get(i));
1067
+ }
1068
+ },
1069
+
1070
+ get: function(user_id) {
1071
+ if (this._members_map.hasOwnProperty(user_id)) { // have heard of this user user_id
1072
+ return {
1073
+ id: user_id,
1074
+ info: this._members_map[user_id]
1075
+ }
1076
+ } else { // have never heard of this user
1077
+ return null;
1078
+ }
1079
+ }
1080
+ };
1081
+
1082
+ Pusher.Channel.factory = function(channel_name, pusher){
1083
+ var channel = new Pusher.Channel(channel_name, pusher);
1084
+ if (channel_name.indexOf('private-') === 0) {
1085
+ Pusher.Util.extend(channel, Pusher.Channel.PrivateChannel);
1086
+ } else if (channel_name.indexOf('presence-') === 0) {
1087
+ Pusher.Util.extend(channel, Pusher.Channel.PrivateChannel);
1088
+ Pusher.Util.extend(channel, Pusher.Channel.PresenceChannel);
1089
+ };
1090
+ channel.init();
1091
+ return channel;
1092
+ };
1093
+ }).call(this);
1094
+ ;(function() {
1095
+ Pusher.Channel.Authorizer = function(channel, type, options) {
1096
+ this.channel = channel;
1097
+ this.type = type;
1098
+
1099
+ this.authOptions = (options || {}).auth || {};
1100
+ };
1101
+
1102
+ Pusher.Channel.Authorizer.prototype = {
1103
+ composeQuery: function(socketId) {
1104
+ var query = '&socket_id=' + encodeURIComponent(socketId)
1105
+ + '&channel_name=' + encodeURIComponent(this.channel.name);
1106
+
1107
+ for(var i in this.authOptions.params) {
1108
+ query += "&" + encodeURIComponent(i) + "=" + encodeURIComponent(this.authOptions.params[i]);
1109
+ }
1110
+
1111
+ return query;
1112
+ },
1113
+
1114
+ authorize: function(socketId, callback) {
1115
+ return Pusher.authorizers[this.type].call(this, socketId, callback);
1116
+ }
1117
+ };
1118
+
1119
+
1120
+ Pusher.auth_callbacks = {};
1121
+ Pusher.authorizers = {
1122
+ ajax: function(socketId, callback){
1123
+ var self = this, xhr;
1124
+
1125
+ if (Pusher.XHR) {
1126
+ xhr = new Pusher.XHR();
1127
+ } else {
1128
+ xhr = (window.XMLHttpRequest ? new window.XMLHttpRequest() : new ActiveXObject("Microsoft.XMLHTTP"));
1129
+ }
1130
+
1131
+ xhr.open("POST", Pusher.channel_auth_endpoint, true);
1132
+
1133
+ // add request headers
1134
+ xhr.setRequestHeader("Content-Type", "application/x-www-form-urlencoded")
1135
+ for(var headerName in this.authOptions.headers) {
1136
+ xhr.setRequestHeader(headerName, this.authOptions.headers[headerName]);
1137
+ }
1138
+
1139
+ xhr.onreadystatechange = function() {
1140
+ if (xhr.readyState == 4) {
1141
+ if (xhr.status == 200) {
1142
+ var data, parsed = false;
1143
+
1144
+ try {
1145
+ data = JSON.parse(xhr.responseText);
1146
+ parsed = true;
1147
+ } catch (e) {
1148
+ callback(true, 'JSON returned from webapp was invalid, yet status code was 200. Data was: ' + xhr.responseText);
1149
+ }
1150
+
1151
+ if (parsed) { // prevents double execution.
1152
+ callback(false, data);
1153
+ }
1154
+ } else {
1155
+ Pusher.warn("Couldn't get auth info from your webapp", xhr.status);
1156
+ callback(true, xhr.status);
1157
+ }
1158
+ }
1159
+ };
1160
+
1161
+ xhr.send(this.composeQuery(socketId));
1162
+ return xhr;
1163
+ },
1164
+
1165
+ jsonp: function(socketId, callback){
1166
+ if(this.authOptions.headers !== undefined) {
1167
+ Pusher.warn("Warn", "To send headers with the auth request, you must use AJAX, rather than JSONP.");
1168
+ }
1169
+
1170
+ var script = document.createElement("script");
1171
+ // Hacked wrapper.
1172
+ Pusher.auth_callbacks[this.channel.name] = function(data) {
1173
+ callback(false, data);
1174
+ };
1175
+
1176
+ var callback_name = "Pusher.auth_callbacks['" + this.channel.name + "']";
1177
+ script.src = Pusher.channel_auth_endpoint
1178
+ + '?callback='
1179
+ + encodeURIComponent(callback_name)
1180
+ + this.composeQuery(socketId);
1181
+
1182
+ var head = document.getElementsByTagName("head")[0] || document.documentElement;
1183
+ head.insertBefore( script, head.firstChild );
1184
+ }
1185
+ };
1186
+ }).call(this);
1187
+ // _require(dependencies, callback) takes an array of dependency urls and a
1188
+ // callback to call when all the dependecies have finished loading
1189
+ var _require = (function() {
1190
+ function handleScriptLoaded(elem, callback) {
1191
+ if (document.addEventListener) {
1192
+ elem.addEventListener('load', callback, false);
1193
+ } else {
1194
+ elem.attachEvent('onreadystatechange', function () {
1195
+ if (elem.readyState == 'loaded' || elem.readyState == 'complete') {
1196
+ callback();
1197
+ }
1198
+ });
1199
+ }
1200
+ }
1201
+
1202
+ function addScript(src, callback) {
1203
+ var head = document.getElementsByTagName('head')[0];
1204
+ var script = document.createElement('script');
1205
+ script.setAttribute('src', src);
1206
+ script.setAttribute("type","text/javascript");
1207
+ script.setAttribute('async', true);
1208
+
1209
+ handleScriptLoaded(script, function() {
1210
+ callback();
1211
+ });
1212
+
1213
+ head.appendChild(script);
1214
+ }
1215
+
1216
+ return function(deps, callback) {
1217
+ var deps_loaded = 0;
1218
+ for (var i = 0; i < deps.length; i++) {
1219
+ addScript(deps[i], function() {
1220
+ if (deps.length == ++deps_loaded) {
1221
+ // This setTimeout is a workaround for an Opera issue
1222
+ setTimeout(callback, 0);
1223
+ }
1224
+ });
1225
+ }
1226
+ }
1227
+ })();
1228
+
1229
+ ;(function() {
1230
+ // Support Firefox versions which prefix WebSocket
1231
+ if (!window['WebSocket'] && window['MozWebSocket']) {
1232
+ window['WebSocket'] = window['MozWebSocket']
1233
+ }
1234
+
1235
+ if (window['WebSocket']) {
1236
+ Pusher.Transport = window['WebSocket'];
1237
+ Pusher.TransportType = 'native';
1238
+ }
1239
+
1240
+ var cdn = (document.location.protocol == 'http:') ? Pusher.cdn_http : Pusher.cdn_https;
1241
+ var root = cdn + Pusher.VERSION;
1242
+ var deps = [];
1243
+
1244
+ if (!window['JSON']) {
1245
+ deps.push(root + '/json2' + Pusher.dependency_suffix + '.js');
1246
+ }
1247
+ if (!window['WebSocket']) {
1248
+ // Try to use web-socket-js (flash WebSocket emulation)
1249
+ window.WEB_SOCKET_DISABLE_AUTO_INITIALIZATION = true;
1250
+ window.WEB_SOCKET_SUPPRESS_CROSS_DOMAIN_SWF_ERROR = true;
1251
+ deps.push(root + '/flashfallback' + Pusher.dependency_suffix + '.js');
1252
+ }
1253
+
1254
+ var initialize = function() {
1255
+ if (window['WebSocket']) {
1256
+ // Initialize function in the case that we have native WebSocket support
1257
+ return function() {
1258
+ Pusher.ready();
1259
+ }
1260
+ } else {
1261
+ // Initialize function for fallback case
1262
+ return function() {
1263
+ if (window['WebSocket']) {
1264
+ // window['WebSocket'] is a flash emulation of WebSocket
1265
+ Pusher.Transport = window['WebSocket'];
1266
+ Pusher.TransportType = 'flash';
1267
+
1268
+ window.WEB_SOCKET_SWF_LOCATION = root + "/WebSocketMain.swf";
1269
+ WebSocket.__addTask(function() {
1270
+ Pusher.ready();
1271
+ })
1272
+ WebSocket.__initialize();
1273
+ } else {
1274
+ // web-socket-js cannot initialize (most likely flash not installed)
1275
+ sockjsPath = root + '/sockjs' + Pusher.dependency_suffix + '.js';
1276
+ _require([sockjsPath], function() {
1277
+ Pusher.Transport = SockJS;
1278
+ Pusher.TransportType = 'sockjs';
1279
+ Pusher.ready();
1280
+ })
1281
+ }
1282
+ }
1283
+ }
1284
+ }();
1285
+
1286
+ // Allows calling a function when the document body is available
1287
+ var ondocumentbody = function(callback) {
1288
+ var load_body = function() {
1289
+ document.body ? callback() : setTimeout(load_body, 0);
1290
+ }
1291
+ load_body();
1292
+ };
1293
+
1294
+ var initializeOnDocumentBody = function() {
1295
+ ondocumentbody(initialize);
1296
+ }
1297
+
1298
+ if (deps.length > 0) {
1299
+ _require(deps, initializeOnDocumentBody);
1300
+ } else {
1301
+ initializeOnDocumentBody();
1302
+ }
1303
+ })();
1304
+