├── .gitignore ├── README.md ├── demo ├── demo.py ├── dev_config │ ├── config.py │ ├── nginx.conf │ ├── run.sh │ └── stop.sh ├── favicon.ico ├── pro_config │ ├── config.py │ ├── nginx.conf │ ├── post-receive │ ├── run.sh │ └── supervisord.conf ├── sandbox_config │ ├── config.py │ └── post-receive ├── static │ ├── css │ │ ├── customer.css │ │ └── style.css │ ├── images │ │ ├── _avatar.png │ │ ├── _off.png │ │ ├── _s.png │ │ └── reset-icon.png │ └── js │ │ ├── chat.js │ │ ├── client.js │ │ ├── customer_chat.js │ │ ├── group_chat.js │ │ ├── lib │ │ ├── console.js │ │ ├── im.js │ │ ├── jquery-1.11.1.js │ │ └── json2.js │ │ ├── recorder.js │ │ ├── room_chat.js │ │ ├── rtc_adapter.js │ │ ├── voip_chat.js │ │ ├── voip_command.js │ │ ├── voip_session.js │ │ └── voip_stream.js └── templates │ ├── chat.html │ ├── customer_chat.html │ ├── customer_index.html │ ├── group_chat.html │ ├── group_index.html │ ├── index.html │ ├── room_chat.html │ ├── room_index.html │ └── voip_chat.html ├── lib ├── byte_order.js ├── im.d.ts ├── im.js └── utf8.js └── package.json /.gitignore: -------------------------------------------------------------------------------- 1 | *~ 2 | .idea 3 | *.pyc 4 | .DS_Store 5 | node_modules 6 | yarn-error.log 7 | yarn.lock 8 | demo/config.py 9 | demo/run.sh 10 | demo/stop.sh -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | #IM JS SDK 2 | 3 | 4 | ## IMService Methods 5 | 6 | - **constructor** 7 | - Initializes the client 8 | 9 | - observer 10 | - handle messages that received from server 11 | - accessToken 12 | - property must be set before start 13 | 14 | - start 15 | - Start im service 16 | 17 | - stop 18 | - Stop im service 19 | 20 | - sendPeerMessage 21 | - Send a message to peer 22 | - **Parameters** 23 | - `msg` (`Object`): message property(sender, receiver, content, msgLocalID) 24 | 25 | - sendGroupMessage 26 | - Send a message to peer 27 | - **Parameters** 28 | - `msg` (`Object`): message property(sender, receiver, content, msgLocalID) 29 | 30 | 31 | ## IMService Observer 32 | - onConnectState 33 | - callback when im service connection state changed 34 | - **Parameters** 35 | - `state`:im service's connect state 36 | 37 | - handlePeerMessage 38 | - callback when im service received a peer message 39 | - **Parameters** 40 | - `msg` (`Object`): message property(sender, receiver, content, timestamp) 41 | 42 | - handleMessageACK 43 | - callback when im service received an ack of message 44 | - **Parameters** 45 | - `msg` (`Object`): message property(sender, receiver, content, timestamp) 46 | 47 | - handleMessageFailure 48 | - callback when im service can't send out the message 49 | - **Parameters** 50 | - `msg` (`Object`): message property(sender, receiver, content, timestamp) 51 | 52 | - handleGroupMessage 53 | - callback when im service received a peer message 54 | - **Parameters** 55 | - `msg` (`Object`): message property(sender, receiver, content, timestamp) 56 | 57 | - handleGroupMessageACK 58 | - callback when im service received an ack of message 59 | - **Parameters** 60 | - `msg` (`Object`): message property(sender, receiver, content, timestamp) 61 | 62 | - handleGroupMessageFailure 63 | - callback when im service can't send out the message 64 | - **Parameters** 65 | - `msg` (`Object`): message property(sender, receiver, content, timestamp) 66 | 67 | 68 | 69 | ##example 70 | 71 | 72 | 73 | 133 | 134 | 135 | 136 | -------------------------------------------------------------------------------- /demo/demo.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | 3 | from flask import request, Blueprint, redirect, url_for 4 | from flask import render_template, send_from_directory 5 | from flask import Flask 6 | from flask import Response 7 | from flask import g 8 | import flask 9 | from functools import wraps 10 | import flask 11 | import random 12 | 13 | 14 | import hashlib 15 | 16 | 17 | import json 18 | import logging 19 | import sys 20 | import os 21 | import redis 22 | import base64 23 | import requests 24 | import config 25 | 26 | 27 | app = Flask(__name__) 28 | app.debug = config.DEBUG 29 | 30 | 31 | def md5(s): 32 | return hashlib.md5(s.encode(encoding='utf8')).hexdigest() 33 | 34 | 35 | def INVALID_PARAM(): 36 | e = {"error":"非法输入"} 37 | return make_response(400, e) 38 | 39 | def LOGIN_FAIL(): 40 | e = {"error":"登陆失败"} 41 | return make_response(400, e) 42 | 43 | def FORBIDDEN(): 44 | e = {"error":"forbidden"} 45 | return make_response(403, e) 46 | 47 | 48 | def make_response(status_code, data = None): 49 | if data: 50 | res = flask.make_response(json.dumps(data), status_code) 51 | res.headers['Content-Type'] = "application/json" 52 | else: 53 | res = flask.make_response("", status_code) 54 | 55 | return res 56 | 57 | error_html = """ 58 | 59 | 60 | Chat Demo 61 | 62 | 63 | 64 | 65 |

error.

66 | 67 | 68 | """ 69 | 70 | def login(uid, uname, platform_id, device_id): 71 | url = config.IM_URL + "/auth/grant" 72 | obj = {"uid":uid, "user_name":uname} 73 | 74 | logging.debug("platform:%s device:%s", platform_id, device_id) 75 | if platform_id and device_id: 76 | obj['platform_id'] = platform_id 77 | obj['device_id'] = device_id 78 | 79 | secret = md5(config.APP_SECRET) 80 | 81 | basic = base64.b64encode((str(config.APP_ID) + ":" + secret).encode(encoding="utf8")) 82 | basic = basic.decode("utf-8") 83 | headers = {'Content-Type': 'application/json; charset=UTF-8', 84 | 'Authorization': 'Basic ' + basic} 85 | 86 | res = requests.post(url, data=json.dumps(obj), headers=headers) 87 | if res.status_code != 200: 88 | return None 89 | obj = json.loads(res.text) 90 | return obj["data"]["token"] 91 | 92 | 93 | @app.route("/login", methods=["POST"]) 94 | def login_session(): 95 | sender = int(request.form['sender']) if 'sender' in request.form else 0 96 | receiver = int(request.form['receiver']) if 'receiver' in request.form else 0 97 | 98 | if sender == 0 or receiver == 0: 99 | return error_html 100 | 101 | token = login(sender, '', None, None) 102 | if not token: 103 | return error_html 104 | 105 | response = flask.make_response(redirect(url_for('.chat', sender=sender, receiver=receiver))) 106 | response.set_cookie('token', token) 107 | return response 108 | 109 | 110 | @app.route("/chat") 111 | def chat(): 112 | return render_template('chat.html', host=config.HOST) 113 | 114 | @app.route('/') 115 | def index(): 116 | return render_template('index.html') 117 | 118 | 119 | @app.route('/customer/chat') 120 | def customer_chat(): 121 | return render_template('customer_chat.html', host=config.HOST) 122 | 123 | @app.route('/customer/login', methods=["POST"]) 124 | def customer_login(): 125 | sender = int(request.form['sender']) if 'sender' in request.form else 0 126 | receiver = int(request.form['receiver']) if 'receiver' in request.form else 0 127 | 128 | if sender == 0 or receiver == 0: 129 | return error_html 130 | 131 | token = login(sender, '', None, None) 132 | if not token: 133 | return error_html 134 | 135 | response = flask.make_response(redirect(url_for('.customer_chat', sender=sender, receiver=receiver))) 136 | response.set_cookie('token', token) 137 | return response 138 | 139 | @app.route('/customer') 140 | def customer(): 141 | return render_template('customer_index.html') 142 | 143 | 144 | @app.route('/room/chat') 145 | def room_chat(): 146 | return render_template('room_chat.html', host=config.HOST) 147 | 148 | 149 | @app.route('/room/login', methods=["POST"]) 150 | def room_login(): 151 | sender = int(request.form['sender']) if 'sender' in request.form else 0 152 | receiver = int(request.form['receiver']) if 'receiver' in request.form else 0 153 | 154 | if sender == 0 or receiver == 0: 155 | return error_html 156 | 157 | token = login(sender, '', None, None) 158 | if not token: 159 | return error_html 160 | 161 | response = flask.make_response(redirect(url_for('.room_chat', sender=sender, receiver=receiver))) 162 | response.set_cookie('token', token) 163 | return response 164 | 165 | @app.route('/room') 166 | def room_index(): 167 | return render_template('room_index.html') 168 | 169 | 170 | 171 | @app.route('/group/chat') 172 | def group_chat(): 173 | return render_template('group_chat.html', host=config.HOST) 174 | 175 | 176 | @app.route('/group/login', methods=["POST"]) 177 | def group_login(): 178 | sender = int(request.form['sender']) if 'sender' in request.form else 0 179 | receiver = int(request.form['receiver']) if 'receiver' in request.form else 0 180 | 181 | if sender == 0 or receiver == 0: 182 | return error_html 183 | 184 | token = login(sender, '', None, None) 185 | if not token: 186 | return error_html 187 | 188 | response = flask.make_response(redirect(url_for('.group_chat', sender=sender, receiver=receiver))) 189 | response.set_cookie('token', token) 190 | return response 191 | 192 | @app.route('/group') 193 | def group_index(): 194 | return render_template('group_index.html') 195 | 196 | 197 | @app.route('/voip') 198 | def voip_chat(): 199 | sender = int(request.args.get('sender')) if request.args.get('sender') else 0 200 | if not sender: 201 | return error_html 202 | 203 | token = login(sender, '', None, None) 204 | if not token: 205 | return error_html 206 | 207 | response = flask.make_response(render_template('voip_chat.html', host=config.HOST)) 208 | response.set_cookie('token', token) 209 | return response 210 | 211 | @app.route('/favicon.ico') 212 | def favicon(): 213 | return send_from_directory(app.root_path, 214 | 'favicon.ico', mimetype='image/vnd.microsoft.icon') 215 | 216 | 217 | @app.route("/auth/token", methods=["POST"]) 218 | def access_token(): 219 | if not request.data: 220 | return INVALID_PARAM() 221 | 222 | obj = json.loads(request.data) 223 | uid = obj.get("uid", None) 224 | user_name = obj.get("user_name", "") 225 | if not uid: 226 | return INVALID_PARAM() 227 | if int(uid) > 10000: 228 | return INVALID_PARAM() 229 | 230 | logging.debug("obj:%s", obj) 231 | token = login(uid, user_name, obj.get('platform_id'), obj.get('device_id')) 232 | if not token: 233 | return LOGIN_FAIL() 234 | 235 | obj = {"token":token} 236 | return make_response(200, obj) 237 | 238 | 239 | def init_logger(logger): 240 | root = logger 241 | root.setLevel(logging.DEBUG) 242 | 243 | ch = logging.StreamHandler(sys.stdout) 244 | ch.setLevel(logging.DEBUG) 245 | formatter = logging.Formatter('%(filename)s:%(lineno)d - %(levelname)s - %(message)s') 246 | ch.setFormatter(formatter) 247 | root.addHandler(ch) 248 | 249 | log = logging.getLogger('') 250 | init_logger(log) 251 | if __name__ == '__main__': 252 | app.run(host="0.0.0.0", port=5001) 253 | -------------------------------------------------------------------------------- /demo/dev_config/config.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | 3 | DEBUG=True 4 | 5 | IM_URL = "http://192.168.33.1:23002" 6 | 7 | APP_ID = 7 8 | APP_KEY = "sVDIlIiDUm7tWPYWhi6kfNbrqui3ez44" 9 | APP_SECRET = '0WiCxAU1jh76SbgaaFC7qIaBPm2zkyM1' 10 | 11 | HOST = "192.168.1.101" 12 | -------------------------------------------------------------------------------- /demo/dev_config/nginx.conf: -------------------------------------------------------------------------------- 1 | # For more information on configuration, see: 2 | # * Official English Documentation: http://nginx.org/en/docs/ 3 | # * Official Russian Documentation: http://nginx.org/ru/docs/ 4 | 5 | user nginx; 6 | worker_processes 1; 7 | 8 | error_log /var/log/nginx/error.log; 9 | #error_log /var/log/nginx/error.log notice; 10 | #error_log /var/log/nginx/error.log info; 11 | 12 | pid /var/run/nginx.pid; 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | events { 21 | worker_connections 1024; 22 | } 23 | 24 | 25 | http { 26 | include /etc/nginx/mime.types; 27 | default_type application/octet-stream; 28 | 29 | log_format main '$remote_addr - $remote_user [$time_local] "$request" ' 30 | '$status $body_bytes_sent "$http_referer" ' 31 | '"$http_user_agent" "$http_x_forwarded_for"'; 32 | 33 | access_log /var/log/nginx/access.log main; 34 | error_log /var/log/nginx/error.log; 35 | sendfile off; 36 | #tcp_nopush on; 37 | 38 | #keepalive_timeout 0; 39 | keepalive_timeout 65; 40 | 41 | #gzip on; 42 | 43 | 44 | 45 | proxy_redirect off ; 46 | proxy_set_header Host $host:$proxy_port; 47 | proxy_set_header X-Real-IP $remote_addr; 48 | proxy_set_header REMOTE-HOST $remote_addr; 49 | proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; 50 | 51 | proxy_connect_timeout 30; 52 | proxy_send_timeout 30; 53 | proxy_read_timeout 60; 54 | proxy_buffer_size 256k; 55 | proxy_buffers 4 256k; 56 | proxy_busy_buffers_size 256k; 57 | proxy_temp_file_write_size 256k; 58 | proxy_max_temp_file_size 128m; 59 | 60 | proxy_ignore_client_abort on; 61 | 62 | 63 | server { 64 | listen 80; 65 | server_name 192.168.33.10 localhost; 66 | 67 | location = / { 68 | proxy_pass http://127.0.0.1:5001; 69 | } 70 | 71 | location ~ /static/(.+) { 72 | root /vagrant/dev/im_api/; 73 | } 74 | 75 | location = /index.html { 76 | proxy_pass http://127.0.0.1:5001; 77 | } 78 | 79 | location ~ /qrcode/login { 80 | proxy_pass http://127.0.0.1:5002; 81 | } 82 | 83 | location ~ /qrcode/(.+) { 84 | proxy_pass http://127.0.0.1:5001; 85 | } 86 | 87 | 88 | location / { 89 | proxy_pass http://127.0.0.1:5000; 90 | } 91 | } 92 | 93 | # Load config files from the /etc/nginx/conf.d directory 94 | # The default server is in conf.d/default.conf 95 | include /etc/nginx/conf.d/*.conf; 96 | 97 | } 98 | -------------------------------------------------------------------------------- /demo/dev_config/run.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | uwsgi=/usr/local/bin/uwsgi 4 | home=/usr/local 5 | app_dir=/vagrant/production/gobelieve/im_js 6 | 7 | 8 | $uwsgi --uid nobody --gid nobody --chdir $app_dir --http :5001 -M -p 1 -w demo --callable app -t 60 --max-requests 5000 --vacuum --home $home --daemonize /tmp/im_demo.log --pidfile /tmp/im_demo.pid --touch-reload /tmp/im_demo.touch 9 | 10 | 11 | -------------------------------------------------------------------------------- /demo/dev_config/stop.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | uwsgi=/usr/local/bin/uwsgi 3 | 4 | $uwsgi --stop /tmp/im_demo.pid 5 | 6 | -------------------------------------------------------------------------------- /demo/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/GoBelieveIO/im_js/a5ef11a504dd354f4422bd353f20e3157ee72030/demo/favicon.ico -------------------------------------------------------------------------------- /demo/pro_config/config.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | 3 | DEBUG=False 4 | 5 | IM_URL = "http://api.gobelieve.io" 6 | 7 | APP_ID = 7 8 | APP_KEY = "sVDIlIiDUm7tWPYWhi6kfNbrqui3ez44" 9 | APP_SECRET = '0WiCxAU1jh76SbgaaFC7qIaBPm2zkyM1' 10 | 11 | HOST = "imnode2.gobelieve.io" 12 | 13 | -------------------------------------------------------------------------------- /demo/pro_config/nginx.conf: -------------------------------------------------------------------------------- 1 | server { 2 | listen 80; 3 | server_name demo.im.gobelieve.io; 4 | 5 | access_log /var/log/nginx/demo.im.gobelieve.io.access.log; 6 | error_log /var/log/nginx/demo.im.gobelieve.io.error.log; 7 | 8 | location / { 9 | include uwsgi_params; 10 | uwsgi_pass 127.0.0.1:6402; 11 | } 12 | } -------------------------------------------------------------------------------- /demo/pro_config/post-receive: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | project="im_js" 3 | app_dir="/data/wwwroot/${project}" 4 | log_dir="/data/logs/${project}" 5 | image="python_dev" 6 | 7 | cd ${app_dir} || exit 8 | unset GIT_DIR 9 | git pull 10 | 11 | cp -f pro_config/config.py ./config.py 12 | 13 | mkdir -p ${log_dir} 14 | 15 | docker restart ${project} || docker run -d --restart=always -e "PYTHONDONTWRITEBYTECODE=1" --name=${project} --net=host -v ${log_dir}:/logs -v ${app_dir}/pro_config/supervisord.conf:/etc/supervisord.conf -v ${app_dir}:/app ${image} /usr/local/python/bin/supervisord 16 | 17 | -------------------------------------------------------------------------------- /demo/pro_config/run.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | project="im_js" 3 | 4 | app_dir="/data/wwwroot/${project}" 5 | log_dir="/data/logs/${project}" 6 | image="python_dev" 7 | 8 | cp -f pro_config/config.py ./config.py 9 | 10 | mkdir -p ${log_dir} 11 | 12 | docker restart ${project} || docker run -d --restart=always --name=${project} --net=host -v ${log_dir}:/logs -v ${app_dir}/pro_config/supervisord.conf:/etc/supervisord.conf -v ${app_dir}:/app ${image} /usr/local/python/bin/supervisord 13 | -------------------------------------------------------------------------------- /demo/pro_config/supervisord.conf: -------------------------------------------------------------------------------- 1 | [supervisord] 2 | nodaemon=true 3 | logfile=/logs/supervisord.log 4 | logfile_backups = 1 5 | 6 | 7 | [program:demo] 8 | command = /usr/local/python/bin/uwsgi -H /usr/local/python -s 127.0.0.1:6402 -w demo:app --gevent 1000 -p 2 -t 300 --harakiri-verbose -M -b 32768 9 | directory = /app 10 | stdout_logfile = /logs/demo.log 11 | autostart = true 12 | autorestart = true 13 | stopsignal = QUIT 14 | redirect_stderr = true 15 | 16 | -------------------------------------------------------------------------------- /demo/sandbox_config/config.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | 3 | DEBUG=False 4 | 5 | IM_URL = "http://127.0.0.1:23002" 6 | 7 | APP_ID = 7 8 | APP_KEY = "sVDIlIiDUm7tWPYWhi6kfNbrqui3ez44" 9 | APP_SECRET = '0WiCxAU1jh76SbgaaFC7qIaBPm2zkyM1' 10 | 11 | HOST = "sandbox.imnode.gobelieve.io" 12 | 13 | -------------------------------------------------------------------------------- /demo/sandbox_config/post-receive: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | project="im_js" 3 | app_dir="/data/wwwroot/${project}" 4 | log_dir="/data/logs/${project}" 5 | image="python_dev" 6 | 7 | cd ${app_dir} || exit 8 | unset GIT_DIR 9 | git pull 10 | 11 | cp -f sandbox_config/config.py ./config.py 12 | 13 | mkdir -p ${log_dir} 14 | 15 | docker restart ${project} || docker run -d --restart=always -e "PYTHONDONTWRITEBYTECODE=1" --name=${project} --net=host -v ${log_dir}:/logs -v ${app_dir}/pro_config/supervisord.conf:/etc/supervisord.conf -v ${app_dir}:/app ${image} /usr/local/python/bin/supervisord 16 | 17 | -------------------------------------------------------------------------------- /demo/static/css/customer.css: -------------------------------------------------------------------------------- 1 | body { 2 | background-color: #ddd; 3 | font-size: 1em; } 4 | body ::-webkit-scrollbar-thumb { 5 | background-color: rgba(0, 0, 0, 0.2); } 6 | body ::-webkit-scrollbar-track { 7 | background: rgba(255, 255, 255, 0.08); } 8 | body ::-webkit-scrollbar { 9 | width: 6px; } 10 | 11 | li { 12 | list-style: none; } 13 | 14 | ul, ol, li { 15 | padding: 0; 16 | margin: 0; } 17 | 18 | .hide { 19 | display: none !important; } 20 | 21 | .app-wrap { 22 | width: 100%; 23 | height: 100%; } 24 | .app-wrap::before { 25 | position: absolute; 26 | z-index: 1; 27 | background-color: #009688; 28 | width: 100%; 29 | height: 127px; 30 | content: ''; 31 | -webkit-animation: bar-slide 0.2s cubic-bezier(0.1, 0.82, 0.25, 1); 32 | animation: bar-slide 0.2s cubic-bezier(0.1, 0.82, 0.25, 1); 33 | top: 0; 34 | left: 0; } 35 | 36 | .app { 37 | box-shadow: 0 1px 1px 0 rgba(0, 0, 0, 0.06), 0 2px 5px 0 rgba(0, 0, 0, 0.2); 38 | height: calc(100% - 38px); 39 | position: relative; 40 | margin: 0 auto; 41 | top: 19px; 42 | z-index: 2; 43 | border-radius: 3px; 44 | overflow: hidden; 45 | -webkit-transform-origin: center; 46 | -ms-transform-origin: center; 47 | transform-origin: center; 48 | -webkit-animation: app-enter .3s; 49 | animation: app-enter .3s; 50 | background-position: top left; 51 | background-repeat: repeat-x; 52 | background-color: #f7f7f7; } 53 | 54 | .avatar { 55 | width: 64px; 56 | height: 64px; 57 | border-radius: 5px; } 58 | 59 | .pane { 60 | position: absolute; 61 | top: 0; 62 | left: 50%; 63 | height: 100%; 64 | width: 30%; 65 | -webkit-transform: translateZ(0); 66 | transform: translateZ(0); 67 | display: -webkit-box; 68 | display: -webkit-flex; 69 | display: -ms-flexbox; 70 | display: flex; 71 | -webkit-box-orient: vertical; 72 | -webkit-box-direction: normal; 73 | -webkit-flex-direction: column; 74 | -ms-flex-direction: column; 75 | flex-direction: column; 76 | overflow: hidden; } 77 | .pane .pane-header { 78 | background-color: #eee; 79 | -webkit-box-flex: 0; 80 | -webkit-flex: none; 81 | -ms-flex: none; 82 | flex: none; 83 | height: 59px; 84 | width: 100%; 85 | box-sizing: border-box; 86 | padding: 10px; } 87 | 88 | .pane-list .pane-header { 89 | height: 100px; 90 | position: relative; 91 | padding-left: 100px; } 92 | .pane-list .pane-header .avatar { 93 | position: absolute; 94 | left: 18px; 95 | top: 18px; 96 | border-radius: 10px; } 97 | .pane-list .pane-header .name { 98 | line-height: 89px; } 99 | 100 | .pane-list-header { 101 | -webkit-flex: none; 102 | -ms-flex: none; 103 | flex: none; } 104 | 105 | .pane-chat { 106 | z-index: 5; 107 | background-color: #e0dad6; 108 | background-size: cover; 109 | background-position: center center; } 110 | 111 | .main .intro { 112 | height: 100%; 113 | padding-top: 100px; 114 | text-align: center; } 115 | .main .pane-chat-header { 116 | height: 100px; 117 | position: relative; 118 | padding-left: 100px; 119 | z-index: 103; 120 | padding-right: 15px; 121 | display: -webkit-box; 122 | display: -webkit-flex; 123 | display: -ms-flexbox; 124 | display: flex; 125 | box-sizing: border-box; 126 | border-left: 1px solid rgba(0, 0, 0, 0.08); 127 | border-right: 1px solid rgba(0, 0, 0, 0.08); } 128 | .main .pane-chat-header:after { 129 | position: absolute; 130 | content: ''; 131 | width: 100%; 132 | height: 1px; 133 | left: 0; 134 | bottom: -1px; 135 | background-color: rgba(255, 255, 255, 0.14); 136 | border-bottom: 1px solid rgba(0, 0, 0, 0.08); } 137 | .main .pane-chat-header .avatar { 138 | position: absolute; 139 | left: 18px; 140 | top: 18px; 141 | border-radius: 10px; } 142 | .main .pane-chat-header .name { 143 | line-height: 89px; } 144 | 145 | .chat-list { 146 | display: -webkit-box; 147 | display: -webkit-flex; 148 | display: -ms-flexbox; 149 | display: flex; 150 | -webkit-box-orient: vertical; 151 | -webkit-box-direction: normal; 152 | -webkit-flex-direction: column; 153 | -ms-flex-direction: column; 154 | flex-direction: column; 155 | overflow-x: hidden; 156 | overflow-y: scroll; 157 | position: relative; 158 | z-index: 100; 159 | padding: 17px 9% 8px; 160 | transform: translateZ(0); 161 | -webkit-box-flex: 1; 162 | -webkit-flex-grow: 1; 163 | -ms-flex-positive: 1; 164 | flex-grow: 1; 165 | -moz-user-select: text; 166 | -webkit-user-select: all; 167 | -webkit-transform: translateZ(0); 168 | border-right: 1px solid #efefef; } 169 | .chat-list ul { 170 | -webkit-box-flex: 0; 171 | -webkit-flex: 0 0 auto; 172 | -ms-flex: 0 0 auto; 173 | flex: 0 0 auto; } 174 | .chat-list .chat-item { 175 | margin-bottom: 9px; } 176 | .chat-list .chat-item:after { 177 | content: ''; 178 | display: table; 179 | clear: both; } 180 | .chat-list .ack, .chat-list .rack { 181 | position: absolute; 182 | width: 15px; 183 | height: 15px; 184 | right: 10px; 185 | bottom: 0; 186 | background: url(../images/_s.png) no-repeat; 187 | background-size: 10px; 188 | opacity: .8; } 189 | .chat-list .ack.rack, .chat-list .rack.rack { 190 | right: 3px; } 191 | .chat-list .message { 192 | -webkit-user-select: none; 193 | -moz-user-select: none; 194 | -ms-user-select: none; 195 | user-select: none; } 196 | .chat-list .message .bubble { 197 | padding: 8px 7px 8px 9px; 198 | display: inline-block; } 199 | .chat-list .message .bubble .image-thumb-body { 200 | -webkit-box-flex: 0; 201 | -webkit-flex: none; 202 | -ms-flex: none; 203 | flex: none; 204 | -webkit-transition: -webkit-filter .16s linear; 205 | max-width: 300px; } 206 | .chat-list .message .bubble p.pre { 207 | padding: 0; 208 | margin: 0; 209 | line-height: 1.2; } 210 | .chat-list .message .bubble p.pre:after { 211 | display: inline; 212 | content: " \00a0\00a0\00a0\00a0\00a0\00a0\00a0\00a0\00a0\00a0\00a0\00a0\00a\00a0\00a0\00a"; } 213 | .chat-list .message .bubble .time { 214 | position: absolute; 215 | right: 28px; 216 | bottom: 4px; 217 | font-size: .75em; 218 | color: #888; } 219 | .chat-list .message-in, .chat-list .message-out { 220 | border-radius: 7.5px; 221 | position: relative; 222 | max-width: 336px; 223 | box-shadow: 0 1px 0.5px rgba(0, 0, 0, 0.13); 224 | -webkit-transition: 0.2s cubic-bezier(0.31, 0.34, 0.3, 0.99); 225 | transition: 0.2s cubic-bezier(0.31, 0.34, 0.3, 0.99); } 226 | .chat-list .message-in { 227 | background-color: #fff; 228 | float: left; 229 | -webkit-transform-origin: bottom left; 230 | -ms-transform-origin: bottom left; 231 | transform-origin: bottom left; } 232 | .chat-list .message-in:before { 233 | content: ''; 234 | position: absolute; 235 | left: -11px; 236 | bottom: 3px; 237 | width: 12px; 238 | height: 19px; 239 | background-position: 50% 50%; 240 | background-repeat: no-repeat; 241 | background-size: contain; 242 | background-image: url(data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABgAAAAmCAYAAADeB1slAAAAGXRFWHRTb2Z0d2FyZQBBZG9iZSBJbWFnZVJlYWR5ccllPAAAAZZJREFUeNq0l79Kw1AUh3tjSTRa0CWD+GcQBHUsDj6ALpLJuZBBcMjq6ib4CA4OggpuGXwIH0CIwS7aQRyl4JCmaY6/QCIxxqZN7vngkBBy73eTc3PviSCiRooQoiELSjpWGsywCDB4jVUAdG5Bi1sw4Bbw5iAIgnlWgaIoa9yCTe4cbGc/ip+QRRRFz5QgXYB+ViiD9FeE0R/mjVKfAILH7BNIFaCPXcohVYDRP7AJ0P6ACpAiQNsW4pVFgHYCcU//IENwQWOoJUCbcyqhkgD3ziJuaAKmFiRz/YkmZGJBssZcIkKaglIBru+NRqMrHH2qQKEA50vo9AxHj2oiikpHXHN/bRo1+CNIqrIvRJOldIRk4Pv+MRauD1k7UFGShWmauud5Zr/fv0M+PqvmoGwWxUlpWpa12O12jyC7DsPwTeo0zcsQc47jbPV6vRMIb1FkvYz7NkSNH5A4fzOxtNPptGzb3jEMY0PX9XVN05ZVVV3F+b7MHU2kQoTabrd15O6dpS5KGQ6Hp6wC13UXBNdPYMq3AAMA932Yhlf0jSsAAAAASUVORK5CYII=); } 243 | .chat-list .message-out { 244 | background-color: #dcf8c6; 245 | float: right; 246 | -webkit-transform-origin: bottom right; 247 | -ms-transform-origin: bottom right; 248 | transform-origin: bottom right; 249 | border-radius: 7.5px; 250 | position: relative; 251 | max-width: 336px; 252 | box-shadow: 0 1px 0.5px rgba(0, 0, 0, 0.13); 253 | -webkit-transition: 0.2s cubic-bezier(0.31, 0.34, 0.3, 0.99); 254 | transition: 0.2s cubic-bezier(0.31, 0.34, 0.3, 0.99); } 255 | .chat-list .message-out:before { 256 | content: ''; 257 | position: absolute; 258 | right: -11px; 259 | bottom: 3px; 260 | width: 12px; 261 | height: 19px; 262 | background-position: 50% 50%; 263 | background-repeat: no-repeat; 264 | background-size: contain; 265 | background-image: url(data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABgAAAAmCAYAAADeB1slAAAAGXRFWHRTb2Z0d2FyZQBBZG9iZSBJbWFnZVJlYWR5ccllPAAAA3NpVFh0WE1MOmNvbS5hZG9iZS54bXAAAAAAADw/eHBhY2tldCBiZWdpbj0i77u/IiBpZD0iVzVNME1wQ2VoaUh6cmVTek5UY3prYzlkIj8+IDx4OnhtcG1ldGEgeG1sbnM6eD0iYWRvYmU6bnM6bWV0YS8iIHg6eG1wdGs9IkFkb2JlIFhNUCBDb3JlIDUuNS1jMDE0IDc5LjE1MTQ4MSwgMjAxMy8wMy8xMy0xMjowOToxNSAgICAgICAgIj4gPHJkZjpSREYgeG1sbnM6cmRmPSJodHRwOi8vd3d3LnczLm9yZy8xOTk5LzAyLzIyLXJkZi1zeW50YXgtbnMjIj4gPHJkZjpEZXNjcmlwdGlvbiByZGY6YWJvdXQ9IiIgeG1sbnM6eG1wTU09Imh0dHA6Ly9ucy5hZG9iZS5jb20veGFwLzEuMC9tbS8iIHhtbG5zOnN0UmVmPSJodHRwOi8vbnMuYWRvYmUuY29tL3hhcC8xLjAvc1R5cGUvUmVzb3VyY2VSZWYjIiB4bWxuczp4bXA9Imh0dHA6Ly9ucy5hZG9iZS5jb20veGFwLzEuMC8iIHhtcE1NOk9yaWdpbmFsRG9jdW1lbnRJRD0ieG1wLmRpZDo0YTJkYTc4Ni0yMmE3LTQzODktOTFjNy03Y2M1ZDgzZGRmNWIiIHhtcE1NOkRvY3VtZW50SUQ9InhtcC5kaWQ6NTE2OTAxNkQxNTA5MTFFNDkwQjVDMTY4RUYwRURBRTYiIHhtcE1NOkluc3RhbmNlSUQ9InhtcC5paWQ6NTE2OTAxNkMxNTA5MTFFNDkwQjVDMTY4RUYwRURBRTYiIHhtcDpDcmVhdG9yVG9vbD0iQWRvYmUgUGhvdG9zaG9wIENDIChNYWNpbnRvc2gpIj4gPHhtcE1NOkRlcml2ZWRGcm9tIHN0UmVmOmluc3RhbmNlSUQ9InhtcC5paWQ6MGJjOTEyYjktODYzMC00ZWI5LTlhYmQtZDdjNmE0YmRiYmQ5IiBzdFJlZjpkb2N1bWVudElEPSJ4bXAuZGlkOjRhMmRhNzg2LTIyYTctNDM4OS05MWM3LTdjYzVkODNkZGY1YiIvPiA8L3JkZjpEZXNjcmlwdGlvbj4gPC9yZGY6UkRGPiA8L3g6eG1wbWV0YT4gPD94cGFja2V0IGVuZD0iciI/PjKJFeEAAAJKSURBVHjaYrz36dz/f//+fVYRMJEHst8zkAiU+Iywiv///x9MMzHQGIAtYGRkZKO1BezySrIsNLMABNx8HHloasH9Ow9/AiOMkWYWKKrIs4NSFM0sAMYBJ019oKGtIkPTOBCXElOhRb6AG8jFzaVBUwu4uTmtgBQLzSxgY2czqOsqFaWZBUDA7Oxp5w5MSUy0soBBVEwkgtrBhGIBBye7w+pd8/SomR/Qg4NJU1etHBRctLIAlFyD95xdZ0ctX2CLUEY5BZnp/XNaeGllAQMLK4uaq4/jJGqkKJwGcHFxxl96eriWUkvwaubh5W648vxIJSWWENQIjPSW669OzDxx+AwHTSwAAXYOthRTS8Ojnz5+NiQ1dRHtdWYWZiM+ft6Tt96emvT+7QdJqlsABazAFJYjIMR/98/vP9O/f/9hCkrW+DQwUloPA1uFV///+7/k6ZMX0x30fD9SvWXHxMSkDQy+dmkZiV3YihiqFc1MzEzaoHqLJhb8/fP3yc2rdwpAIYZRKpBt6N9/74DJdsu9W/fXZsWWHn/98u03oPB3iiz4/ev3g8+fvux/8ujZjpqCtsNXLlz/AhT+CcR/sLkerwXAVPAXaODdr1+/n3z/9v2xQ3uPH2kq634GlPoFxX+AKfAfrv4BigVfPn/d8efPn+c/vv98+vXLt0fPn764N7F91vWzJy6AvPwbauBfkCtJTdYsQFfe0ZO2TQCyP0G9CcfUaKuyPH38fBaQ/gw07DtNmo55iZVLoBFFmx4OMCV8BLr+L60sAAgwAKLk6X4B6GN3AAAAAElFTkSuQmCC); } 266 | 267 | 268 | .pane-chat-footer { 269 | background-color: rgba(255, 255, 255, 0.6); 270 | z-index: 102; 271 | position: relative; 272 | transform: translateZ(0); 273 | -webkit-box-flex: 0; 274 | -webkit-flex: none; 275 | -ms-flex: none; 276 | flex: none; 277 | width: 100%; 278 | box-sizing: border-box; 279 | padding: 10px; } 280 | .pane-chat-footer .chat-input { 281 | border-radius: 5px; 282 | background-clip: padding-box; 283 | background-color: #fff; 284 | width: 100%; 285 | border: 0; 286 | outline: none; 287 | padding-left: 8px; 288 | padding-top: 12px; 289 | font-size: 16px; 290 | padding-bottom: 0; 291 | height: 40px; } 292 | 293 | .intro { 294 | background-color: #fff; 295 | font-size: 32px; 296 | position: absolute; 297 | z-index: 10000; 298 | left: 0; 299 | top: 0; 300 | width: 100%; 301 | height: 100%; } 302 | 303 | 304 | 305 | @media screen and (min-width: 1320px) { 306 | .app, .app.three { 307 | width: 1212px; } } 308 | .pane-list { 309 | left: 0; } 310 | 311 | .pane-chat { 312 | left: 30%; 313 | width: 70%; } 314 | 315 | @media screen and (min-width: 1200px) { 316 | .app { 317 | width: 1118px; } } 318 | .btn-download { 319 | position: absolute; 320 | right: 50px; 321 | top: 35px; 322 | padding: 10px 30px; 323 | text-decoration: none; 324 | background-color: #fff; 325 | cursor: pointer; 326 | color: #078496; 327 | border-radius: 5px; 328 | z-index: 10002; } 329 | 330 | .btn-download:hover { 331 | background-color: #eee; 332 | text-decoration: none; } 333 | 334 | /*# sourceMappingURL=style.css.map */ 335 | -------------------------------------------------------------------------------- /demo/static/css/style.css: -------------------------------------------------------------------------------- 1 | body { 2 | padding: 0; 3 | margin: 0; 4 | } 5 | 6 | body, #entry { 7 | background: #c5e8d6; 8 | color: #000000; 9 | } 10 | 11 | body, table { 12 | font-family: DejaVu Sans Mono, fixed; 13 | font-size: 14pt; 14 | line-height: 150%; 15 | } 16 | 17 | #loginView { 18 | width: 100%; 19 | font-size: 13pt; 20 | overflow: hidden; 21 | } 22 | 23 | #loginTitle { 24 | width: 100%; 25 | margin-top: 150px; 26 | font-size: 50pt; 27 | text-align: center; 28 | } 29 | 30 | #loginView input[type = "text"] { 31 | height: 30px; 32 | width: 270px; 33 | font-size: inherit; 34 | margin-left: 15px; 35 | } 36 | 37 | #loginView input[type = "button"] { 38 | height: 30px; 39 | width: 90px; 40 | font-size: inherit; 41 | } 42 | 43 | #loginView table { 44 | width: 100%; 45 | height: 100%; 46 | margin-top: 10%; 47 | } 48 | 49 | #loginView table tr { 50 | text-align: center; 51 | height: 50%; 52 | } 53 | 54 | #loginView table tr td { 55 | padding-top: 25px; 56 | } 57 | 58 | #loginError { 59 | text-align: center; 60 | font-size: 16pt; 61 | color: #ff0000; 62 | margin-top: 15px; 63 | } 64 | 65 | #chatHistory { 66 | padding-bottom: 5.1em; 67 | } 68 | 69 | #toolbar { 70 | position: fixed; 71 | width: 100%; 72 | bottom: 0; 73 | background: #007077; 74 | } 75 | 76 | #toolbar ul { 77 | margin: 0; 78 | padding: 5px 0 0 6px; 79 | height: 35px; 80 | list-style: none; 81 | } 82 | 83 | #toolbar li { 84 | display: block; 85 | float: left; 86 | margin: 0 2em 0 0; 87 | } 88 | 89 | #toolbar select { 90 | width: 100px; 91 | height: 30px; 92 | font-size: inherit; 93 | } 94 | 95 | #entry { 96 | width: 100%; 97 | font-size: inherit; 98 | padding: 1em; 99 | margin: 0; 100 | border-width: 0; 101 | outline-width: 0; 102 | clear: both; 103 | } 104 | 105 | .message { 106 | margin: 0.1em 0; 107 | } 108 | 109 | .message td { 110 | vertical-align: top; 111 | } 112 | 113 | .nick { 114 | font-weight: bold; 115 | padding: 0 1em 0 0.5em; 116 | } -------------------------------------------------------------------------------- /demo/static/images/_avatar.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/GoBelieveIO/im_js/a5ef11a504dd354f4422bd353f20e3157ee72030/demo/static/images/_avatar.png -------------------------------------------------------------------------------- /demo/static/images/_off.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/GoBelieveIO/im_js/a5ef11a504dd354f4422bd353f20e3157ee72030/demo/static/images/_off.png -------------------------------------------------------------------------------- /demo/static/images/_s.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/GoBelieveIO/im_js/a5ef11a504dd354f4422bd353f20e3157ee72030/demo/static/images/_s.png -------------------------------------------------------------------------------- /demo/static/images/reset-icon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/GoBelieveIO/im_js/a5ef11a504dd354f4422bd353f20e3157ee72030/demo/static/images/reset-icon.png -------------------------------------------------------------------------------- /demo/static/js/chat.js: -------------------------------------------------------------------------------- 1 | var username; 2 | var receiver; 3 | var users; 4 | var base = 1000; 5 | var msgLocalID=0; 6 | var increase = 25; 7 | 8 | util = { 9 | urlRE: /https?:\/\/([-\w\.]+)+(:\d+)?(\/([^\s]*(\?\S+)?)?)?/g, 10 | // html sanitizer 11 | toStaticHTML: function (inputHtml) { 12 | inputHtml = inputHtml.toString(); 13 | return inputHtml.replace(/&/g, "&").replace(//g, ">"); 14 | }, 15 | //pads n with zeros on the left, 16 | //digits is minimum length of output 17 | //zeroPad(3, 5); returns "005" 18 | //zeroPad(2, 500); returns "500" 19 | zeroPad: function (digits, n) { 20 | n = n.toString(); 21 | while (n.length < digits) 22 | n = '0' + n; 23 | return n; 24 | }, 25 | //it is almost 8 o'clock PM here 26 | //timeString(new Date); returns "19:49" 27 | timeString: function (date) { 28 | var minutes = date.getMinutes().toString(); 29 | var hours = date.getHours().toString(); 30 | return this.zeroPad(2, hours) + ":" + this.zeroPad(2, minutes); 31 | }, 32 | 33 | //does the argument only contain whitespace? 34 | isBlank: function (text) { 35 | var blank = /^\s*$/; 36 | return (text.match(blank) !== null); 37 | }, 38 | 39 | 40 | getURLParameter: function(name, search) { 41 | search = search || location.search 42 | var param = search.match( 43 | RegExp(name + '=' + '(.+?)(&|$)')) 44 | return param ? decodeURIComponent(param[1]) : null 45 | }, 46 | 47 | getCookie: function(c_name) { 48 | if (document.cookie.length>0) { 49 | c_start=document.cookie.indexOf(c_name + "=") 50 | if (c_start!=-1) { 51 | c_start=c_start + c_name.length+1 52 | c_end=document.cookie.indexOf(";",c_start) 53 | if (c_end==-1) c_end=document.cookie.length 54 | return unescape(document.cookie.substring(c_start,c_end)) 55 | } 56 | } 57 | return "" 58 | }, 59 | 60 | }; 61 | 62 | //always view the most recent message when it is added 63 | function scrollDown(base) { 64 | window.scrollTo(0, base); 65 | $("#entry").focus(); 66 | } 67 | 68 | // add message on board 69 | function addMessage(from, target, text, time) { 70 | var name = (target == '*' ? 'all' : target); 71 | if (text === null) return; 72 | if (time == null) { 73 | // if the time is null or undefined, use the current time. 74 | time = new Date(); 75 | } else if ((time instanceof Date) === false) { 76 | // if it's a timestamp, interpret it 77 | time = new Date(time); 78 | } 79 | //every message you see is actually a table with 3 cols: 80 | // the time, 81 | // the person who caused the event, 82 | // and the content 83 | var messageElement = $(document.createElement("table")); 84 | messageElement.addClass("message"); 85 | // sanitize 86 | text = util.toStaticHTML(text); 87 | var content = '' + ' ' + util.timeString(time) + '' + ' ' + util.toStaticHTML(from) + ' says to ' + name + ': ' + '' + ' ' + text + '' + ''; 88 | messageElement.html(content); 89 | //the log is the stream that we view 90 | $("#chatHistory").append(messageElement); 91 | base += increase; 92 | scrollDown(base); 93 | } 94 | 95 | // show tip 96 | function tip(type, name) { 97 | var tip, title; 98 | switch (type) { 99 | case 'online': 100 | tip = name + ' is online now.'; 101 | title = 'Online Notify'; 102 | break; 103 | case 'offline': 104 | tip = name + ' is offline now.'; 105 | title = 'Offline Notify'; 106 | break; 107 | case 'message': 108 | tip = name + ' is saying now.'; 109 | title = 'Message Notify'; 110 | break; 111 | } 112 | var pop = new Pop(title, tip); 113 | } 114 | 115 | // init user list 116 | function initUserList(data) { 117 | users = data.users; 118 | for (var i = 0; i < users.length; i++) { 119 | var slElement = $(document.createElement("option")); 120 | slElement.attr("value", users[i]); 121 | slElement.text(users[i]); 122 | $("#usersList").append(slElement); 123 | } 124 | } 125 | 126 | // add user in user list 127 | function addUser(user) { 128 | var slElement = $(document.createElement("option")); 129 | slElement.attr("value", user); 130 | slElement.text(user); 131 | $("#usersList").append(slElement); 132 | } 133 | 134 | // remove user from user list 135 | function removeUser(user) { 136 | $("#usersList option").each( 137 | function () { 138 | if ($(this).val() === user) $(this).remove(); 139 | }); 140 | } 141 | 142 | // set your name 143 | function setName() { 144 | $("#name").text(username); 145 | } 146 | 147 | // show error 148 | function showError(content) { 149 | $("#loginError").text(content); 150 | $("#loginError").show(); 151 | } 152 | 153 | // show chat panel 154 | function showChat() { 155 | $("#toolbar").show(); 156 | $("entry").focus(); 157 | scrollDown(base); 158 | } 159 | 160 | 161 | 162 | $(document).ready(function () { 163 | observer = { 164 | handlePeerMessage: function (msg) { 165 | //console.log("msg sender:", msg.sender, " receiver:", msg.receiver, " content:", msg.content, " timestamp:", msg.timestamp); 166 | addMessage(msg.sender, msg.receiver, msg.content); 167 | $("#chatHistory").show(); 168 | //if (msg.sender !== username) 169 | // tip('message', msg.sender); 170 | }, 171 | handleMessageACK: function(msg) { 172 | //console.log("message ack local id:", msgLocalID, " receiver:", receiver) 173 | }, 174 | handleMessageFailure: function(msg) { 175 | //console.log("message fail local id:", msgLocalID, " receiver:", receiver) 176 | }, 177 | onConnectState: function(state) { 178 | if (state == IMService.STATE_CONNECTED) { 179 | console.log("im connected"); 180 | } else if (state == IMService.STATE_CONNECTING) { 181 | console.log("im connecting"); 182 | } else if (state == IMService.STATE_CONNECTFAIL) { 183 | console.log("im connect fail"); 184 | } else if (state == IMService.STATE_UNCONNECTED) { 185 | console.log("im unconnected"); 186 | } 187 | } 188 | }; 189 | 190 | var im = new IMService(); 191 | im.host = host; 192 | im.observer = observer; 193 | 194 | var r = util.getURLParameter('receiver', location.search); 195 | if (r) { 196 | receiver = r; 197 | } else { 198 | receiver = 0; 199 | } 200 | console.log("receiver:" + receiver) 201 | 202 | r = util.getURLParameter('sender', location.search); 203 | if (r) { 204 | sender = r; 205 | } else { 206 | sender = 0; 207 | } 208 | username = sender; 209 | console.log("sender:" + sender) 210 | 211 | 212 | addUser(receiver); 213 | token = util.getCookie("token"); 214 | console.log("token:" + token) 215 | im.accessToken = token 216 | im.start(); 217 | 218 | 219 | setName(); 220 | showChat(); 221 | //deal with chat mode. 222 | $("#entry").keypress(function (e) { 223 | var target = $("#usersList").val(); 224 | if (e.keyCode != 13 /* Return */) return; 225 | var msg = $("#entry").val().replace("\n", ""); 226 | if (!util.isBlank(msg)) { 227 | var now = new Date().getTime() / 1000; 228 | var obj = {"text": msg}; 229 | var textMsg = JSON.stringify(obj); 230 | var message = {sender:username, receiver: target, content: textMsg, timestamp:now, msgLocalID:msgLocalID++}; 231 | if (im.connectState == IMService.STATE_CONNECTED) { 232 | im.sendPeerMessage(message); 233 | $("#entry").val(""); // clear the entry field. 234 | if (target != '*' && target != username) { 235 | addMessage(username, target, msg); 236 | $("#chatHistory").show(); 237 | } 238 | } 239 | } 240 | }); 241 | }); 242 | -------------------------------------------------------------------------------- /demo/static/js/client.js: -------------------------------------------------------------------------------- 1 | var username; 2 | var receiver; 3 | var users; 4 | var base = 1000; 5 | var msgLocalID=0; 6 | var increase = 25; 7 | 8 | util = { 9 | urlRE: /https?:\/\/([-\w\.]+)+(:\d+)?(\/([^\s]*(\?\S+)?)?)?/g, 10 | // html sanitizer 11 | toStaticHTML: function (inputHtml) { 12 | inputHtml = inputHtml.toString(); 13 | return inputHtml.replace(/&/g, "&").replace(//g, ">"); 14 | }, 15 | //pads n with zeros on the left, 16 | //digits is minimum length of output 17 | //zeroPad(3, 5); returns "005" 18 | //zeroPad(2, 500); returns "500" 19 | zeroPad: function (digits, n) { 20 | n = n.toString(); 21 | while (n.length < digits) 22 | n = '0' + n; 23 | return n; 24 | }, 25 | //it is almost 8 o'clock PM here 26 | //timeString(new Date); returns "19:49" 27 | timeString: function (date) { 28 | var minutes = date.getMinutes().toString(); 29 | var hours = date.getHours().toString(); 30 | return this.zeroPad(2, hours) + ":" + this.zeroPad(2, minutes); 31 | }, 32 | 33 | //does the argument only contain whitespace? 34 | isBlank: function (text) { 35 | var blank = /^\s*$/; 36 | return (text.match(blank) !== null); 37 | } 38 | }; 39 | 40 | //always view the most recent message when it is added 41 | function scrollDown(base) { 42 | window.scrollTo(0, base); 43 | $("#entry").focus(); 44 | } 45 | 46 | // add message on board 47 | function addMessage(from, target, text, time) { 48 | var name = (target == '*' ? 'all' : target); 49 | if (text === null) return; 50 | if (time == null) { 51 | // if the time is null or undefined, use the current time. 52 | time = new Date(); 53 | } else if ((time instanceof Date) === false) { 54 | // if it's a timestamp, interpret it 55 | time = new Date(time); 56 | } 57 | //every message you see is actually a table with 3 cols: 58 | // the time, 59 | // the person who caused the event, 60 | // and the content 61 | var messageElement = $(document.createElement("table")); 62 | messageElement.addClass("message"); 63 | // sanitize 64 | text = util.toStaticHTML(text); 65 | var content = '' + ' ' + util.timeString(time) + '' + ' ' + util.toStaticHTML(from) + ' says to ' + name + ': ' + '' + ' ' + text + '' + ''; 66 | messageElement.html(content); 67 | //the log is the stream that we view 68 | $("#chatHistory").append(messageElement); 69 | base += increase; 70 | scrollDown(base); 71 | } 72 | 73 | // show tip 74 | function tip(type, name) { 75 | var tip, title; 76 | switch (type) { 77 | case 'online': 78 | tip = name + ' is online now.'; 79 | title = 'Online Notify'; 80 | break; 81 | case 'offline': 82 | tip = name + ' is offline now.'; 83 | title = 'Offline Notify'; 84 | break; 85 | case 'message': 86 | tip = name + ' is saying now.'; 87 | title = 'Message Notify'; 88 | break; 89 | } 90 | var pop = new Pop(title, tip); 91 | } 92 | 93 | // init user list 94 | function initUserList(data) { 95 | users = data.users; 96 | for (var i = 0; i < users.length; i++) { 97 | var slElement = $(document.createElement("option")); 98 | slElement.attr("value", users[i]); 99 | slElement.text(users[i]); 100 | $("#usersList").append(slElement); 101 | } 102 | } 103 | 104 | // add user in user list 105 | function addUser(user) { 106 | var slElement = $(document.createElement("option")); 107 | slElement.attr("value", user); 108 | slElement.text(user); 109 | $("#usersList").append(slElement); 110 | } 111 | 112 | // remove user from user list 113 | function removeUser(user) { 114 | $("#usersList option").each( 115 | function () { 116 | if ($(this).val() === user) $(this).remove(); 117 | }); 118 | } 119 | 120 | // set your name 121 | function setName() { 122 | $("#name").text(username); 123 | } 124 | 125 | // show error 126 | function showError(content) { 127 | $("#loginError").text(content); 128 | $("#loginError").show(); 129 | } 130 | 131 | // show login panel 132 | function showLogin() { 133 | $("#loginView").show(); 134 | $("#chatHistory").hide(); 135 | $("#toolbar").hide(); 136 | $("#loginError").hide(); 137 | $("#loginUser").focus(); 138 | } 139 | 140 | // show chat panel 141 | function showChat() { 142 | $("#loginView").hide(); 143 | $("#loginError").hide(); 144 | $("#toolbar").show(); 145 | $("entry").focus(); 146 | scrollDown(base); 147 | } 148 | 149 | $(document).ready(function () { 150 | //when first time into chat room. 151 | showLogin(); 152 | 153 | observer = { 154 | handlePeerMessage: function (msg) { 155 | //console.log("msg sender:", msg.sender, " receiver:", msg.receiver, " content:", msg.content, " timestamp:", msg.timestamp); 156 | addMessage(msg.sender, msg.receiver, msg.content); 157 | $("#chatHistory").show(); 158 | //if (msg.sender !== username) 159 | // tip('message', msg.sender); 160 | }, 161 | handleMessageACK: function(msg) { 162 | //console.log("message ack local id:", msgLocalID, " receiver:", receiver) 163 | }, 164 | handleMessageFailure: function(msg) { 165 | //console.log("message fail local id:", msgLocalID, " receiver:", receiver) 166 | }, 167 | 168 | handleRTMessage: function(msg) { 169 | console.log("rt message sender:", msg.sender, " receiver", msg.receiver, " content:", msg.content); 170 | }, 171 | 172 | onConnectState: function(state) { 173 | if (state == IMService.STATE_CONNECTED) { 174 | //console.log("im connected"); 175 | // 连接成功 176 | setName(); 177 | showChat(); 178 | } else if (state == IMService.STATE_CONNECTING) { 179 | //console.log("im connecting"); 180 | } else if (state == IMService.STATE_CONNECTFAIL) { 181 | //console.log("im connect fail"); 182 | } else if (state == IMService.STATE_UNCONNECTED) { 183 | //console.log("im unconnected"); 184 | //showLogin(); 185 | } 186 | } 187 | }; 188 | 189 | var im = new IMService(observer); 190 | im.host = host 191 | //deal with login button click. 192 | $("#login").click(function () { 193 | username = parseInt($("#loginUser").val()); 194 | 195 | $.ajax({ 196 | url: "auth/token", 197 | dataType: 'json', 198 | type: 'POST', 199 | contentType: "application/json", 200 | data:JSON.stringify({uid:username}), 201 | success: function(result, status, xhr) { 202 | if (status == "success") { 203 | console.log("login success:", result.token); 204 | receiver = parseInt($("#receiver").val()); 205 | addUser(receiver); 206 | im.accessToken = result.token; 207 | im.start(); 208 | } else { 209 | console.log("login error status:", status); 210 | alert("login fail"); 211 | } 212 | }, 213 | error : function(xhr, err) { 214 | console.log("login err:", err, xhr.status); 215 | alert("login fail"); 216 | } 217 | }); 218 | }); 219 | 220 | //deal with chat mode. 221 | $("#entry").keypress(function (e) { 222 | var target = parseInt($("#usersList").val()); 223 | if (e.keyCode != 13 /* Return */) return; 224 | var msg = $("#entry").val().replace("\n", ""); 225 | if (!util.isBlank(msg)) { 226 | 227 | var message = {sender:username, receiver: target, content: msg, msgLocalID:msgLocalID++}; 228 | if (im.connectState == IMService.STATE_CONNECTED) { 229 | im.sendPeerMessage(message); 230 | 231 | $("#entry").val(""); // clear the entry field. 232 | if (target != '*' && target != username) { 233 | addMessage(username, target, msg); 234 | $("#chatHistory").show(); 235 | } 236 | } 237 | } 238 | }); 239 | }); 240 | -------------------------------------------------------------------------------- /demo/static/js/customer_chat.js: -------------------------------------------------------------------------------- 1 | var username; 2 | var receiver; 3 | var users; 4 | var base = 1000; 5 | var msgLocalID=0; 6 | var increase = 25; 7 | 8 | var appID = 7; 9 | var uid = 0; 10 | 11 | var receiver = 0; 12 | 13 | //客服appid 14 | var receiverAppID = 17; 15 | 16 | 17 | util = { 18 | urlRE: /https?:\/\/([-\w\.]+)+(:\d+)?(\/([^\s]*(\?\S+)?)?)?/g, 19 | // html sanitizer 20 | toStaticHTML: function (inputHtml) { 21 | inputHtml = inputHtml.toString(); 22 | return inputHtml.replace(/&/g, "&").replace(//g, ">"); 23 | }, 24 | //pads n with zeros on the left, 25 | //digits is minimum length of output 26 | //zeroPad(3, 5); returns "005" 27 | //zeroPad(2, 500); returns "500" 28 | zeroPad: function (digits, n) { 29 | n = n.toString(); 30 | while (n.length < digits) 31 | n = '0' + n; 32 | return n; 33 | }, 34 | //it is almost 8 o'clock PM here 35 | //timeString(new Date); returns "19:49" 36 | timeString: function (date) { 37 | var minutes = date.getMinutes().toString(); 38 | var hours = date.getHours().toString(); 39 | return this.zeroPad(2, hours) + ":" + this.zeroPad(2, minutes); 40 | }, 41 | 42 | //does the argument only contain whitespace? 43 | isBlank: function (text) { 44 | var blank = /^\s*$/; 45 | return (text.match(blank) !== null); 46 | }, 47 | 48 | 49 | getURLParameter: function(name, search) { 50 | search = search || location.search 51 | var param = search.match( 52 | RegExp(name + '=' + '(.+?)(&|$)')) 53 | return param ? decodeURIComponent(param[1]) : null 54 | }, 55 | 56 | getCookie: function(c_name) { 57 | if (document.cookie.length>0) { 58 | c_start=document.cookie.indexOf(c_name + "=") 59 | if (c_start!=-1) { 60 | c_start=c_start + c_name.length+1 61 | c_end=document.cookie.indexOf(";",c_start) 62 | if (c_end==-1) c_end=document.cookie.length 63 | return unescape(document.cookie.substring(c_start,c_end)) 64 | } 65 | } 66 | return "" 67 | }, 68 | 69 | }; 70 | 71 | //always view the most recent message when it is added 72 | function scrollDown(base) { 73 | window.scrollTo(0, base); 74 | $("#entry").focus(); 75 | } 76 | 77 | // add message on board 78 | function addMessage(from, target, text, time) { 79 | var name = (target == '*' ? 'all' : target); 80 | if (text === null) return; 81 | if (time == null) { 82 | // if the time is null or undefined, use the current time. 83 | time = new Date(); 84 | } else if ((time instanceof Date) === false) { 85 | // if it's a timestamp, interpret it 86 | time = new Date(time); 87 | } 88 | //every message you see is actually a table with 3 cols: 89 | // the time, 90 | // the person who caused the event, 91 | // and the content 92 | var messageElement = $(document.createElement("table")); 93 | messageElement.addClass("message"); 94 | // sanitize 95 | text = util.toStaticHTML(text); 96 | var content = '' + ' ' + util.timeString(time) + '' + ' ' + util.toStaticHTML(from) + ' says to ' + name + ': ' + '' + ' ' + text + '' + ''; 97 | messageElement.html(content); 98 | //the log is the stream that we view 99 | $("#chatHistory").append(messageElement); 100 | base += increase; 101 | scrollDown(base); 102 | } 103 | 104 | // show tip 105 | function tip(type, name) { 106 | var tip, title; 107 | switch (type) { 108 | case 'online': 109 | tip = name + ' is online now.'; 110 | title = 'Online Notify'; 111 | break; 112 | case 'offline': 113 | tip = name + ' is offline now.'; 114 | title = 'Offline Notify'; 115 | break; 116 | case 'message': 117 | tip = name + ' is saying now.'; 118 | title = 'Message Notify'; 119 | break; 120 | } 121 | var pop = new Pop(title, tip); 122 | } 123 | 124 | // init user list 125 | function initUserList(data) { 126 | users = data.users; 127 | for (var i = 0; i < users.length; i++) { 128 | var slElement = $(document.createElement("option")); 129 | slElement.attr("value", users[i]); 130 | slElement.text(users[i]); 131 | $("#usersList").append(slElement); 132 | } 133 | } 134 | 135 | // add user in user list 136 | function addUser(user) { 137 | var slElement = $(document.createElement("option")); 138 | slElement.attr("value", user); 139 | slElement.text(user); 140 | $("#usersList").append(slElement); 141 | } 142 | 143 | // remove user from user list 144 | function removeUser(user) { 145 | $("#usersList option").each( 146 | function () { 147 | if ($(this).val() === user) $(this).remove(); 148 | }); 149 | } 150 | 151 | // set your name 152 | function setName() { 153 | $("#name").text(username); 154 | } 155 | 156 | // show error 157 | function showError(content) { 158 | $("#loginError").text(content); 159 | $("#loginError").show(); 160 | } 161 | 162 | // show chat panel 163 | function showChat() { 164 | $("#toolbar").show(); 165 | $("entry").focus(); 166 | scrollDown(base); 167 | } 168 | 169 | 170 | 171 | $(document).ready(function () { 172 | observer = { 173 | handleCustomerMessage: function(msg) { 174 | addMessage(msg.sender, msg.receiver, msg.content); 175 | $("#chatHistory").show(); 176 | }, 177 | handleCustomerMessageACK: function(msg) { 178 | console.log("handleCustomerMessageACK..."); 179 | }, 180 | handleCustomerMessageFailure: function(msg) { 181 | console.log("handleCustomerMessageFailure..."); 182 | }, 183 | onConnectState: function(state) { 184 | if (state == IMService.STATE_CONNECTED) { 185 | console.log("im connected"); 186 | } else if (state == IMService.STATE_CONNECTING) { 187 | console.log("im connecting"); 188 | } else if (state == IMService.STATE_CONNECTFAIL) { 189 | console.log("im connect fail"); 190 | } else if (state == IMService.STATE_UNCONNECTED) { 191 | console.log("im unconnected"); 192 | } 193 | } 194 | }; 195 | 196 | var im = new IMService(); 197 | im.host = host; 198 | im.observer = observer; 199 | im.customerMessageObserver = observer; 200 | 201 | var r = util.getURLParameter('receiver', location.search); 202 | if (r) { 203 | receiver = r; 204 | } else { 205 | receiver = 0; 206 | } 207 | console.log("receiver:" + receiver) 208 | 209 | r = util.getURLParameter('sender', location.search); 210 | if (r) { 211 | sender = r; 212 | } else { 213 | sender = 0; 214 | } 215 | username = sender; 216 | uid = sender; 217 | console.log("sender:" + sender) 218 | 219 | 220 | addUser(receiver); 221 | token = util.getCookie("token"); 222 | console.log("token:" + token) 223 | im.accessToken = token 224 | im.start(); 225 | 226 | 227 | setName(); 228 | showChat(); 229 | //deal with chat mode. 230 | $("#entry").keypress(function (e) { 231 | var target = $("#usersList").val(); 232 | if (e.keyCode != 13 /* Return */) return; 233 | var msg = $("#entry").val().replace("\n", ""); 234 | if (!util.isBlank(msg)) { 235 | var now = new Date().getTime() / 1000; 236 | var obj = { 237 | text:msg, 238 | name:"test", 239 | "app_name":"demo", 240 | "store_id":7, 241 | "store_name":"五湖四海", 242 | }; 243 | var textMsg = JSON.stringify(obj); 244 | 245 | var message = { 246 | sender:uid, 247 | senderAppID:appID, 248 | receiver:receiver, 249 | receiverAppID:receiverAppID, 250 | content: textMsg, 251 | contentObj: obj, 252 | msgLocalID:msgLocalID++ 253 | }; 254 | message.outgoing = true; 255 | message.timestamp = now; 256 | 257 | if (im.connectState == IMService.STATE_CONNECTED) { 258 | im.sendCustomerMessage(message); 259 | $("#entry").val(""); // clear the entry field. 260 | if (target != '*' && target != username) { 261 | addMessage(username, target, msg); 262 | $("#chatHistory").show(); 263 | } 264 | } 265 | } 266 | }); 267 | }); 268 | -------------------------------------------------------------------------------- /demo/static/js/group_chat.js: -------------------------------------------------------------------------------- 1 | var username; 2 | var uid; 3 | var receiver; 4 | var users; 5 | var base = 1000; 6 | var msgLocalID=0; 7 | var increase = 25; 8 | 9 | util = { 10 | urlRE: /https?:\/\/([-\w\.]+)+(:\d+)?(\/([^\s]*(\?\S+)?)?)?/g, 11 | // html sanitizer 12 | toStaticHTML: function (inputHtml) { 13 | inputHtml = inputHtml.toString(); 14 | return inputHtml.replace(/&/g, "&").replace(//g, ">"); 15 | }, 16 | //pads n with zeros on the left, 17 | //digits is minimum length of output 18 | //zeroPad(3, 5); returns "005" 19 | //zeroPad(2, 500); returns "500" 20 | zeroPad: function (digits, n) { 21 | n = n.toString(); 22 | while (n.length < digits) 23 | n = '0' + n; 24 | return n; 25 | }, 26 | //it is almost 8 o'clock PM here 27 | //timeString(new Date); returns "19:49" 28 | timeString: function (date) { 29 | var minutes = date.getMinutes().toString(); 30 | var hours = date.getHours().toString(); 31 | return this.zeroPad(2, hours) + ":" + this.zeroPad(2, minutes); 32 | }, 33 | 34 | //does the argument only contain whitespace? 35 | isBlank: function (text) { 36 | var blank = /^\s*$/; 37 | return (text.match(blank) !== null); 38 | }, 39 | 40 | 41 | getURLParameter: function(name, search) { 42 | search = search || location.search 43 | var param = search.match( 44 | RegExp(name + '=' + '(.+?)(&|$)')) 45 | return param ? decodeURIComponent(param[1]) : null 46 | }, 47 | 48 | getCookie: function(c_name) { 49 | if (document.cookie.length>0) { 50 | c_start=document.cookie.indexOf(c_name + "=") 51 | if (c_start!=-1) { 52 | c_start=c_start + c_name.length+1 53 | c_end=document.cookie.indexOf(";",c_start) 54 | if (c_end==-1) c_end=document.cookie.length 55 | return unescape(document.cookie.substring(c_start,c_end)) 56 | } 57 | } 58 | return "" 59 | }, 60 | 61 | }; 62 | 63 | //always view the most recent message when it is added 64 | function scrollDown(base) { 65 | window.scrollTo(0, base); 66 | $("#entry").focus(); 67 | } 68 | 69 | // add message on board 70 | function addMessage(from, target, text, time) { 71 | var name = (target == '*' ? 'all' : target); 72 | if (text === null) return; 73 | if (time == null) { 74 | // if the time is null or undefined, use the current time. 75 | time = new Date(); 76 | } else if ((time instanceof Date) === false) { 77 | // if it's a timestamp, interpret it 78 | time = new Date(time); 79 | } 80 | //every message you see is actually a table with 3 cols: 81 | // the time, 82 | // the person who caused the event, 83 | // and the content 84 | var messageElement = $(document.createElement("table")); 85 | messageElement.addClass("message"); 86 | // sanitize 87 | text = util.toStaticHTML(text); 88 | var content = '' + ' ' + util.timeString(time) + '' + ' ' + util.toStaticHTML(from) + ' says to ' + name + ': ' + '' + ' ' + text + '' + ''; 89 | messageElement.html(content); 90 | //the log is the stream that we view 91 | $("#chatHistory").append(messageElement); 92 | base += increase; 93 | scrollDown(base); 94 | } 95 | 96 | // show tip 97 | function tip(type, name) { 98 | var tip, title; 99 | switch (type) { 100 | case 'online': 101 | tip = name + ' is online now.'; 102 | title = 'Online Notify'; 103 | break; 104 | case 'offline': 105 | tip = name + ' is offline now.'; 106 | title = 'Offline Notify'; 107 | break; 108 | case 'message': 109 | tip = name + ' is saying now.'; 110 | title = 'Message Notify'; 111 | break; 112 | } 113 | var pop = new Pop(title, tip); 114 | } 115 | 116 | // init user list 117 | function initUserList(data) { 118 | users = data.users; 119 | for (var i = 0; i < users.length; i++) { 120 | var slElement = $(document.createElement("option")); 121 | slElement.attr("value", users[i]); 122 | slElement.text(users[i]); 123 | $("#usersList").append(slElement); 124 | } 125 | } 126 | 127 | // add user in user list 128 | function addUser(user) { 129 | var slElement = $(document.createElement("option")); 130 | slElement.attr("value", user); 131 | slElement.text(user); 132 | $("#usersList").append(slElement); 133 | } 134 | 135 | // remove user from user list 136 | function removeUser(user) { 137 | $("#usersList option").each( 138 | function () { 139 | if ($(this).val() === user) $(this).remove(); 140 | }); 141 | } 142 | 143 | // set your name 144 | function setName() { 145 | $("#name").text(username); 146 | } 147 | 148 | // show error 149 | function showError(content) { 150 | $("#loginError").text(content); 151 | $("#loginError").show(); 152 | } 153 | 154 | // show chat panel 155 | function showChat() { 156 | $("#toolbar").show(); 157 | $("entry").focus(); 158 | scrollDown(base); 159 | } 160 | 161 | 162 | 163 | $(document).ready(function () { 164 | observer = { 165 | handleGroupMessage: function (msg) { 166 | console.log("msg sender:", msg.sender, " receiver:", msg.receiver, " content:", msg.content, " timestamp:", msg.timestamp); 167 | addMessage(msg.sender, msg.receiver, msg.content); 168 | $("#chatHistory").show(); 169 | }, 170 | handleGroupMessageACK: function(msg) { 171 | console.log("message ack local id:", msg.msgLocalID, " receiver:", msg.receiver) 172 | }, 173 | handleGroupMessageFailure: function(msgLocalID, receiver) { 174 | console.log("message fail local id:", msg.msgLocalID, " receiver:", msg.receiver) 175 | }, 176 | 177 | //创建群,加入群,离开群的通知消息 178 | handleGroupNotification: function(groupNotification) { 179 | console.log("group notification:", groupNotification); 180 | }, 181 | 182 | onConnectState: function(state) { 183 | if (state == IMService.STATE_CONNECTED) { 184 | console.log("im connected"); 185 | } else if (state == IMService.STATE_CONNECTING) { 186 | console.log("im connecting"); 187 | } else if (state == IMService.STATE_CONNECTFAIL) { 188 | console.log("im connect fail"); 189 | } else if (state == IMService.STATE_UNCONNECTED) { 190 | console.log("im unconnected"); 191 | } 192 | } 193 | }; 194 | 195 | var im = new IMService(); 196 | im.host = host; 197 | im.observer = observer; 198 | 199 | var r = util.getURLParameter('receiver', location.search); 200 | if (r) { 201 | receiver = parseInt(r); 202 | } else { 203 | receiver = 0; 204 | } 205 | console.log("receiver:" + receiver) 206 | 207 | r = util.getURLParameter('sender', location.search); 208 | if (r) { 209 | sender = parseInt(r); 210 | } else { 211 | sender = 0; 212 | } 213 | username = sender; 214 | uid = sender; 215 | console.log("sender:" + sender) 216 | 217 | 218 | addUser(receiver); 219 | token = util.getCookie("token"); 220 | console.log("token:" + token) 221 | im.accessToken = token 222 | im.start(); 223 | 224 | setName(); 225 | showChat(); 226 | //deal with chat mode. 227 | $("#entry").keypress(function (e) { 228 | if (e.keyCode != 13 /* Return */) return; 229 | var msg = $("#entry").val().replace("\n", ""); 230 | if (!util.isBlank(msg)) { 231 | var obj = {"text": msg}; 232 | var textMsg = JSON.stringify(obj); 233 | var message = {sender:uid, receiver: receiver, content: textMsg, msgLocalID:msgLocalID++}; 234 | if (im.connectState == IMService.STATE_CONNECTED) { 235 | im.sendGroupMessage(message); 236 | $("#entry").val(""); // clear the entry field. 237 | addMessage(username, receiver, msg); 238 | $("#chatHistory").show(); 239 | } 240 | } 241 | }); 242 | }); 243 | -------------------------------------------------------------------------------- /demo/static/js/lib/console.js: -------------------------------------------------------------------------------- 1 | // Avoid `console` errors in browsers that lack a console. 2 | (function() { 3 | var method; 4 | var noop = function () {}; 5 | var methods = [ 6 | 'assert', 'clear', 'count', 'debug', 'dir', 'dirxml', 'error', 7 | 'exception', 'group', 'groupCollapsed', 'groupEnd', 'info', 'log', 8 | 'markTimeline', 'profile', 'profileEnd', 'table', 'time', 'timeEnd', 9 | 'timeline', 'timelineEnd', 'timeStamp', 'trace', 'warn' 10 | ]; 11 | var length = methods.length; 12 | var console = (window.console = window.console || {}); 13 | 14 | while (length--) { 15 | method = methods[length]; 16 | 17 | // Only stub undefined methods. 18 | if (!console[method]) { 19 | console[method] = noop; 20 | } 21 | } 22 | }()); -------------------------------------------------------------------------------- /demo/static/js/lib/json2.js: -------------------------------------------------------------------------------- 1 | /* 2 | json2.js 3 | 2014-02-04 4 | 5 | Public Domain. 6 | 7 | NO WARRANTY EXPRESSED OR IMPLIED. USE AT YOUR OWN RISK. 8 | 9 | See http://www.JSON.org/js.html 10 | 11 | 12 | This code should be minified before deployment. 13 | See http://javascript.crockford.com/jsmin.html 14 | 15 | USE YOUR OWN COPY. IT IS EXTREMELY UNWISE TO LOAD CODE FROM SERVERS YOU DO 16 | NOT CONTROL. 17 | 18 | 19 | This file creates a global JSON object containing two methods: stringify 20 | and parse. 21 | 22 | JSON.stringify(value, replacer, space) 23 | value any JavaScript value, usually an object or array. 24 | 25 | replacer an optional parameter that determines how object 26 | values are stringified for objects. It can be a 27 | function or an array of strings. 28 | 29 | space an optional parameter that specifies the indentation 30 | of nested structures. If it is omitted, the text will 31 | be packed without extra whitespace. If it is a number, 32 | it will specify the number of spaces to indent at each 33 | level. If it is a string (such as '\t' or ' '), 34 | it contains the characters used to indent at each level. 35 | 36 | This method produces a JSON text from a JavaScript value. 37 | 38 | When an object value is found, if the object contains a toJSON 39 | method, its toJSON method will be called and the result will be 40 | stringified. A toJSON method does not serialize: it returns the 41 | value represented by the name/value pair that should be serialized, 42 | or undefined if nothing should be serialized. The toJSON method 43 | will be passed the key associated with the value, and this will be 44 | bound to the value 45 | 46 | For example, this would serialize Dates as ISO strings. 47 | 48 | Date.prototype.toJSON = function (key) { 49 | function f(n) { 50 | // Format integers to have at least two digits. 51 | return n < 10 ? '0' + n : n; 52 | } 53 | 54 | return this.getUTCFullYear() + '-' + 55 | f(this.getUTCMonth() + 1) + '-' + 56 | f(this.getUTCDate()) + 'T' + 57 | f(this.getUTCHours()) + ':' + 58 | f(this.getUTCMinutes()) + ':' + 59 | f(this.getUTCSeconds()) + 'Z'; 60 | }; 61 | 62 | You can provide an optional replacer method. It will be passed the 63 | key and value of each member, with this bound to the containing 64 | object. The value that is returned from your method will be 65 | serialized. If your method returns undefined, then the member will 66 | be excluded from the serialization. 67 | 68 | If the replacer parameter is an array of strings, then it will be 69 | used to select the members to be serialized. It filters the results 70 | such that only members with keys listed in the replacer array are 71 | stringified. 72 | 73 | Values that do not have JSON representations, such as undefined or 74 | functions, will not be serialized. Such values in objects will be 75 | dropped; in arrays they will be replaced with null. You can use 76 | a replacer function to replace those with JSON values. 77 | JSON.stringify(undefined) returns undefined. 78 | 79 | The optional space parameter produces a stringification of the 80 | value that is filled with line breaks and indentation to make it 81 | easier to read. 82 | 83 | If the space parameter is a non-empty string, then that string will 84 | be used for indentation. If the space parameter is a number, then 85 | the indentation will be that many spaces. 86 | 87 | Example: 88 | 89 | text = JSON.stringify(['e', {pluribus: 'unum'}]); 90 | // text is '["e",{"pluribus":"unum"}]' 91 | 92 | 93 | text = JSON.stringify(['e', {pluribus: 'unum'}], null, '\t'); 94 | // text is '[\n\t"e",\n\t{\n\t\t"pluribus": "unum"\n\t}\n]' 95 | 96 | text = JSON.stringify([new Date()], function (key, value) { 97 | return this[key] instanceof Date ? 98 | 'Date(' + this[key] + ')' : value; 99 | }); 100 | // text is '["Date(---current time---)"]' 101 | 102 | 103 | JSON.parse(text, reviver) 104 | This method parses a JSON text to produce an object or array. 105 | It can throw a SyntaxError exception. 106 | 107 | The optional reviver parameter is a function that can filter and 108 | transform the results. It receives each of the keys and values, 109 | and its return value is used instead of the original value. 110 | If it returns what it received, then the structure is not modified. 111 | If it returns undefined then the member is deleted. 112 | 113 | Example: 114 | 115 | // Parse the text. Values that look like ISO date strings will 116 | // be converted to Date objects. 117 | 118 | myData = JSON.parse(text, function (key, value) { 119 | var a; 120 | if (typeof value === 'string') { 121 | a = 122 | /^(\d{4})-(\d{2})-(\d{2})T(\d{2}):(\d{2}):(\d{2}(?:\.\d*)?)Z$/.exec(value); 123 | if (a) { 124 | return new Date(Date.UTC(+a[1], +a[2] - 1, +a[3], +a[4], 125 | +a[5], +a[6])); 126 | } 127 | } 128 | return value; 129 | }); 130 | 131 | myData = JSON.parse('["Date(09/09/2001)"]', function (key, value) { 132 | var d; 133 | if (typeof value === 'string' && 134 | value.slice(0, 5) === 'Date(' && 135 | value.slice(-1) === ')') { 136 | d = new Date(value.slice(5, -1)); 137 | if (d) { 138 | return d; 139 | } 140 | } 141 | return value; 142 | }); 143 | 144 | 145 | This is a reference implementation. You are free to copy, modify, or 146 | redistribute. 147 | */ 148 | 149 | /*jslint evil: true, regexp: true */ 150 | 151 | /*members "", "\b", "\t", "\n", "\f", "\r", "\"", JSON, "\\", apply, 152 | call, charCodeAt, getUTCDate, getUTCFullYear, getUTCHours, 153 | getUTCMinutes, getUTCMonth, getUTCSeconds, hasOwnProperty, join, 154 | lastIndex, length, parse, prototype, push, replace, slice, stringify, 155 | test, toJSON, toString, valueOf 156 | */ 157 | 158 | 159 | // Create a JSON object only if one does not already exist. We create the 160 | // methods in a closure to avoid creating global variables. 161 | 162 | if (typeof JSON !== 'object') { 163 | JSON = {}; 164 | } 165 | 166 | (function () { 167 | 'use strict'; 168 | 169 | function f(n) { 170 | // Format integers to have at least two digits. 171 | return n < 10 ? '0' + n : n; 172 | } 173 | 174 | if (typeof Date.prototype.toJSON !== 'function') { 175 | 176 | Date.prototype.toJSON = function () { 177 | 178 | return isFinite(this.valueOf()) 179 | ? this.getUTCFullYear() + '-' + 180 | f(this.getUTCMonth() + 1) + '-' + 181 | f(this.getUTCDate()) + 'T' + 182 | f(this.getUTCHours()) + ':' + 183 | f(this.getUTCMinutes()) + ':' + 184 | f(this.getUTCSeconds()) + 'Z' 185 | : null; 186 | }; 187 | 188 | String.prototype.toJSON = 189 | Number.prototype.toJSON = 190 | Boolean.prototype.toJSON = function () { 191 | return this.valueOf(); 192 | }; 193 | } 194 | 195 | var cx, 196 | escapable, 197 | gap, 198 | indent, 199 | meta, 200 | rep; 201 | 202 | 203 | function quote(string) { 204 | 205 | // If the string contains no control characters, no quote characters, and no 206 | // backslash characters, then we can safely slap some quotes around it. 207 | // Otherwise we must also replace the offending characters with safe escape 208 | // sequences. 209 | 210 | escapable.lastIndex = 0; 211 | return escapable.test(string) ? '"' + string.replace(escapable, function (a) { 212 | var c = meta[a]; 213 | return typeof c === 'string' 214 | ? c 215 | : '\\u' + ('0000' + a.charCodeAt(0).toString(16)).slice(-4); 216 | }) + '"' : '"' + string + '"'; 217 | } 218 | 219 | 220 | function str(key, holder) { 221 | 222 | // Produce a string from holder[key]. 223 | 224 | var i, // The loop counter. 225 | k, // The member key. 226 | v, // The member value. 227 | length, 228 | mind = gap, 229 | partial, 230 | value = holder[key]; 231 | 232 | // If the value has a toJSON method, call it to obtain a replacement value. 233 | 234 | if (value && typeof value === 'object' && 235 | typeof value.toJSON === 'function') { 236 | value = value.toJSON(key); 237 | } 238 | 239 | // If we were called with a replacer function, then call the replacer to 240 | // obtain a replacement value. 241 | 242 | if (typeof rep === 'function') { 243 | value = rep.call(holder, key, value); 244 | } 245 | 246 | // What happens next depends on the value's type. 247 | 248 | switch (typeof value) { 249 | case 'string': 250 | return quote(value); 251 | 252 | case 'number': 253 | 254 | // JSON numbers must be finite. Encode non-finite numbers as null. 255 | 256 | return isFinite(value) ? String(value) : 'null'; 257 | 258 | case 'boolean': 259 | case 'null': 260 | 261 | // If the value is a boolean or null, convert it to a string. Note: 262 | // typeof null does not produce 'null'. The case is included here in 263 | // the remote chance that this gets fixed someday. 264 | 265 | return String(value); 266 | 267 | // If the type is 'object', we might be dealing with an object or an array or 268 | // null. 269 | 270 | case 'object': 271 | 272 | // Due to a specification blunder in ECMAScript, typeof null is 'object', 273 | // so watch out for that case. 274 | 275 | if (!value) { 276 | return 'null'; 277 | } 278 | 279 | // Make an array to hold the partial results of stringifying this object value. 280 | 281 | gap += indent; 282 | partial = []; 283 | 284 | // Is the value an array? 285 | 286 | if (Object.prototype.toString.apply(value) === '[object Array]') { 287 | 288 | // The value is an array. Stringify every element. Use null as a placeholder 289 | // for non-JSON values. 290 | 291 | length = value.length; 292 | for (i = 0; i < length; i += 1) { 293 | partial[i] = str(i, value) || 'null'; 294 | } 295 | 296 | // Join all of the elements together, separated with commas, and wrap them in 297 | // brackets. 298 | 299 | v = partial.length === 0 300 | ? '[]' 301 | : gap 302 | ? '[\n' + gap + partial.join(',\n' + gap) + '\n' + mind + ']' 303 | : '[' + partial.join(',') + ']'; 304 | gap = mind; 305 | return v; 306 | } 307 | 308 | // If the replacer is an array, use it to select the members to be stringified. 309 | 310 | if (rep && typeof rep === 'object') { 311 | length = rep.length; 312 | for (i = 0; i < length; i += 1) { 313 | if (typeof rep[i] === 'string') { 314 | k = rep[i]; 315 | v = str(k, value); 316 | if (v) { 317 | partial.push(quote(k) + (gap ? ': ' : ':') + v); 318 | } 319 | } 320 | } 321 | } else { 322 | 323 | // Otherwise, iterate through all of the keys in the object. 324 | 325 | for (k in value) { 326 | if (Object.prototype.hasOwnProperty.call(value, k)) { 327 | v = str(k, value); 328 | if (v) { 329 | partial.push(quote(k) + (gap ? ': ' : ':') + v); 330 | } 331 | } 332 | } 333 | } 334 | 335 | // Join all of the member texts together, separated with commas, 336 | // and wrap them in braces. 337 | 338 | v = partial.length === 0 339 | ? '{}' 340 | : gap 341 | ? '{\n' + gap + partial.join(',\n' + gap) + '\n' + mind + '}' 342 | : '{' + partial.join(',') + '}'; 343 | gap = mind; 344 | return v; 345 | } 346 | } 347 | 348 | // If the JSON object does not yet have a stringify method, give it one. 349 | 350 | if (typeof JSON.stringify !== 'function') { 351 | escapable = /[\\\"\x00-\x1f\x7f-\x9f\u00ad\u0600-\u0604\u070f\u17b4\u17b5\u200c-\u200f\u2028-\u202f\u2060-\u206f\ufeff\ufff0-\uffff]/g; 352 | meta = { // table of character substitutions 353 | '\b': '\\b', 354 | '\t': '\\t', 355 | '\n': '\\n', 356 | '\f': '\\f', 357 | '\r': '\\r', 358 | '"' : '\\"', 359 | '\\': '\\\\' 360 | }; 361 | JSON.stringify = function (value, replacer, space) { 362 | 363 | // The stringify method takes a value and an optional replacer, and an optional 364 | // space parameter, and returns a JSON text. The replacer can be a function 365 | // that can replace values, or an array of strings that will select the keys. 366 | // A default replacer method can be provided. Use of the space parameter can 367 | // produce text that is more easily readable. 368 | 369 | var i; 370 | gap = ''; 371 | indent = ''; 372 | 373 | // If the space parameter is a number, make an indent string containing that 374 | // many spaces. 375 | 376 | if (typeof space === 'number') { 377 | for (i = 0; i < space; i += 1) { 378 | indent += ' '; 379 | } 380 | 381 | // If the space parameter is a string, it will be used as the indent string. 382 | 383 | } else if (typeof space === 'string') { 384 | indent = space; 385 | } 386 | 387 | // If there is a replacer, it must be a function or an array. 388 | // Otherwise, throw an error. 389 | 390 | rep = replacer; 391 | if (replacer && typeof replacer !== 'function' && 392 | (typeof replacer !== 'object' || 393 | typeof replacer.length !== 'number')) { 394 | throw new Error('JSON.stringify'); 395 | } 396 | 397 | // Make a fake root object containing our value under the key of ''. 398 | // Return the result of stringifying the value. 399 | 400 | return str('', {'': value}); 401 | }; 402 | } 403 | 404 | 405 | // If the JSON object does not yet have a parse method, give it one. 406 | 407 | if (typeof JSON.parse !== 'function') { 408 | cx = /[\u0000\u00ad\u0600-\u0604\u070f\u17b4\u17b5\u200c-\u200f\u2028-\u202f\u2060-\u206f\ufeff\ufff0-\uffff]/g; 409 | JSON.parse = function (text, reviver) { 410 | 411 | // The parse method takes a text and an optional reviver function, and returns 412 | // a JavaScript value if the text is a valid JSON text. 413 | 414 | var j; 415 | 416 | function walk(holder, key) { 417 | 418 | // The walk method is used to recursively walk the resulting structure so 419 | // that modifications can be made. 420 | 421 | var k, v, value = holder[key]; 422 | if (value && typeof value === 'object') { 423 | for (k in value) { 424 | if (Object.prototype.hasOwnProperty.call(value, k)) { 425 | v = walk(value, k); 426 | if (v !== undefined) { 427 | value[k] = v; 428 | } else { 429 | delete value[k]; 430 | } 431 | } 432 | } 433 | } 434 | return reviver.call(holder, key, value); 435 | } 436 | 437 | 438 | // Parsing happens in four stages. In the first stage, we replace certain 439 | // Unicode characters with escape sequences. JavaScript handles many characters 440 | // incorrectly, either silently deleting them, or treating them as line endings. 441 | 442 | text = String(text); 443 | cx.lastIndex = 0; 444 | if (cx.test(text)) { 445 | text = text.replace(cx, function (a) { 446 | return '\\u' + 447 | ('0000' + a.charCodeAt(0).toString(16)).slice(-4); 448 | }); 449 | } 450 | 451 | // In the second stage, we run the text against regular expressions that look 452 | // for non-JSON patterns. We are especially concerned with '()' and 'new' 453 | // because they can cause invocation, and '=' because it can cause mutation. 454 | // But just to be safe, we want to reject all unexpected forms. 455 | 456 | // We split the second stage into 4 regexp operations in order to work around 457 | // crippling inefficiencies in IE's and Safari's regexp engines. First we 458 | // replace the JSON backslash pairs with '@' (a non-JSON character). Second, we 459 | // replace all simple value tokens with ']' characters. Third, we delete all 460 | // open brackets that follow a colon or comma or that begin the text. Finally, 461 | // we look to see that the remaining characters are only whitespace or ']' or 462 | // ',' or ':' or '{' or '}'. If that is so, then the text is safe for eval. 463 | 464 | if (/^[\],:{}\s]*$/ 465 | .test(text.replace(/\\(?:["\\\/bfnrt]|u[0-9a-fA-F]{4})/g, '@') 466 | .replace(/"[^"\\\n\r]*"|true|false|null|-?\d+(?:\.\d*)?(?:[eE][+\-]?\d+)?/g, ']') 467 | .replace(/(?:^|:|,)(?:\s*\[)+/g, ''))) { 468 | 469 | // In the third stage we use the eval function to compile the text into a 470 | // JavaScript structure. The '{' operator is subject to a syntactic ambiguity 471 | // in JavaScript: it can begin a block or an object literal. We wrap the text 472 | // in parens to eliminate the ambiguity. 473 | 474 | j = eval('(' + text + ')'); 475 | 476 | // In the optional fourth stage, we recursively walk the new structure, passing 477 | // each name/value pair to a reviver function for possible transformation. 478 | 479 | return typeof reviver === 'function' 480 | ? walk({'': j}, '') 481 | : j; 482 | } 483 | 484 | // If the text is not JSON parseable, then a SyntaxError is thrown. 485 | 486 | throw new SyntaxError('JSON.parse'); 487 | }; 488 | } 489 | }()); 490 | -------------------------------------------------------------------------------- /demo/static/js/recorder.js: -------------------------------------------------------------------------------- 1 | 2 | 3 | var mediaSource = new MediaSource(); 4 | mediaSource.addEventListener('sourceopen', handleSourceOpen, false); 5 | var mediaRecorder; 6 | var recordedBlobs; 7 | var sourceBuffer; 8 | 9 | 10 | 11 | // window.isSecureContext could be used for Chrome 12 | var isSecureOrigin = location.protocol === 'https:' || 13 | location.hostname === 'localhost'; 14 | if (!isSecureOrigin) { 15 | alert('getUserMedia() must be run from a secure origin: HTTPS or localhost.' + 16 | '\n\nChanging protocol to HTTPS'); 17 | location.protocol = 'HTTPS'; 18 | } 19 | 20 | function download() { 21 | var blob = new Blob(recordedBlobs, {type: 'video/webm'}); 22 | var url = window.URL.createObjectURL(blob); 23 | var a = document.createElement('a'); 24 | a.style.display = 'none'; 25 | a.href = url; 26 | a.download = 'test.webm'; 27 | document.body.appendChild(a); 28 | a.click(); 29 | setTimeout(function() { 30 | document.body.removeChild(a); 31 | window.URL.revokeObjectURL(url); 32 | }, 100); 33 | } 34 | 35 | function handleSourceOpen(event) { 36 | console.log('MediaSource opened'); 37 | sourceBuffer = mediaSource.addSourceBuffer('video/webm; codecs="vp8"'); 38 | console.log('Source buffer: ', sourceBuffer); 39 | } 40 | 41 | 42 | function handleDataAvailable(event) { 43 | if (event.data && event.data.size > 0) { 44 | recordedBlobs.push(event.data); 45 | } 46 | } 47 | 48 | function handleStop(event) { 49 | console.log('Recorder stopped: ', event); 50 | } 51 | 52 | 53 | function startRecording() { 54 | recordedBlobs = []; 55 | var options = {mimeType: 'video/webm;codecs=vp9'}; 56 | if (!MediaRecorder.isTypeSupported(options.mimeType)) { 57 | console.log(options.mimeType + ' is not Supported'); 58 | options = {mimeType: 'video/webm;codecs=vp8'}; 59 | if (!MediaRecorder.isTypeSupported(options.mimeType)) { 60 | console.log(options.mimeType + ' is not Supported'); 61 | options = {mimeType: 'video/webm'}; 62 | if (!MediaRecorder.isTypeSupported(options.mimeType)) { 63 | console.log(options.mimeType + ' is not Supported'); 64 | options = {mimeType: ''}; 65 | } 66 | } 67 | } 68 | try { 69 | mediaRecorder = new MediaRecorder(window.stream, options); 70 | } catch (e) { 71 | console.error('Exception while creating MediaRecorder: ' + e); 72 | alert('Exception while creating MediaRecorder: ' 73 | + e + '. mimeType: ' + options.mimeType); 74 | return; 75 | } 76 | console.log('Created MediaRecorder', mediaRecorder, 'with options', options); 77 | mediaRecorder.onstop = handleStop; 78 | mediaRecorder.ondataavailable = handleDataAvailable; 79 | mediaRecorder.start(10); // collect 10ms of data 80 | console.log('MediaRecorder started', mediaRecorder); 81 | } 82 | 83 | function stopRecording() { 84 | mediaRecorder.stop(); 85 | console.log('Recorded Blobs: ', recordedBlobs); 86 | } 87 | -------------------------------------------------------------------------------- /demo/static/js/room_chat.js: -------------------------------------------------------------------------------- 1 | var username; 2 | var receiver; 3 | var users; 4 | var base = 1000; 5 | var msgLocalID=0; 6 | var increase = 25; 7 | 8 | 9 | util = { 10 | urlRE: /https?:\/\/([-\w\.]+)+(:\d+)?(\/([^\s]*(\?\S+)?)?)?/g, 11 | // html sanitizer 12 | toStaticHTML: function (inputHtml) { 13 | inputHtml = inputHtml.toString(); 14 | return inputHtml.replace(/&/g, "&").replace(//g, ">"); 15 | }, 16 | //pads n with zeros on the left, 17 | //digits is minimum length of output 18 | //zeroPad(3, 5); returns "005" 19 | //zeroPad(2, 500); returns "500" 20 | zeroPad: function (digits, n) { 21 | n = n.toString(); 22 | while (n.length < digits) 23 | n = '0' + n; 24 | return n; 25 | }, 26 | //it is almost 8 o'clock PM here 27 | //timeString(new Date); returns "19:49" 28 | timeString: function (date) { 29 | var minutes = date.getMinutes().toString(); 30 | var hours = date.getHours().toString(); 31 | return this.zeroPad(2, hours) + ":" + this.zeroPad(2, minutes); 32 | }, 33 | 34 | //does the argument only contain whitespace? 35 | isBlank: function (text) { 36 | var blank = /^\s*$/; 37 | return (text.match(blank) !== null); 38 | }, 39 | 40 | 41 | getURLParameter: function(name, search) { 42 | search = search || location.search 43 | var param = search.match( 44 | RegExp(name + '=' + '(.+?)(&|$)')) 45 | return param ? decodeURIComponent(param[1]) : null 46 | }, 47 | 48 | getCookie: function(c_name) { 49 | if (document.cookie.length>0) { 50 | c_start=document.cookie.indexOf(c_name + "=") 51 | if (c_start!=-1) { 52 | c_start=c_start + c_name.length+1 53 | c_end=document.cookie.indexOf(";",c_start) 54 | if (c_end==-1) c_end=document.cookie.length 55 | return unescape(document.cookie.substring(c_start,c_end)) 56 | } 57 | } 58 | return "" 59 | }, 60 | 61 | }; 62 | 63 | //always view the most recent message when it is added 64 | function scrollDown(base) { 65 | window.scrollTo(0, base); 66 | $("#entry").focus(); 67 | } 68 | 69 | // add message on board 70 | function addMessage(from, target, text, time) { 71 | var name = (target == '*' ? 'all' : target); 72 | if (text === null) return; 73 | if (time == null) { 74 | // if the time is null or undefined, use the current time. 75 | time = new Date(); 76 | } else if ((time instanceof Date) === false) { 77 | // if it's a timestamp, interpret it 78 | time = new Date(time); 79 | } 80 | //every message you see is actually a table with 3 cols: 81 | // the time, 82 | // the person who caused the event, 83 | // and the content 84 | var messageElement = $(document.createElement("table")); 85 | messageElement.addClass("message"); 86 | // sanitize 87 | text = util.toStaticHTML(text); 88 | var content = '' + ' ' + util.timeString(time) + '' + ' ' + util.toStaticHTML(from) + ' says to ' + name + ': ' + '' + ' ' + text + '' + ''; 89 | messageElement.html(content); 90 | //the log is the stream that we view 91 | $("#chatHistory").append(messageElement); 92 | base += increase; 93 | scrollDown(base); 94 | } 95 | 96 | // show tip 97 | function tip(type, name) { 98 | var tip, title; 99 | switch (type) { 100 | case 'online': 101 | tip = name + ' is online now.'; 102 | title = 'Online Notify'; 103 | break; 104 | case 'offline': 105 | tip = name + ' is offline now.'; 106 | title = 'Offline Notify'; 107 | break; 108 | case 'message': 109 | tip = name + ' is saying now.'; 110 | title = 'Message Notify'; 111 | break; 112 | } 113 | var pop = new Pop(title, tip); 114 | } 115 | 116 | // set your name 117 | function setName(name) { 118 | $("#name").text(name); 119 | } 120 | 121 | // show error 122 | function showError(content) { 123 | $("#loginError").text(content); 124 | $("#loginError").show(); 125 | } 126 | 127 | // show chat panel 128 | function showChat() { 129 | $("#toolbar").show(); 130 | $("entry").focus(); 131 | scrollDown(base); 132 | } 133 | 134 | $(document).ready(function () { 135 | observer = { 136 | handleRoomMessage: function (msg) { 137 | console.log("msg sender:", msg.sender, " room id:", msg.receiver, " content:", msg.content, " timestamp:", msg.timestamp); 138 | try { 139 | msg.contentObj = JSON.parse(msg.content) 140 | if (msg.contentObj) { 141 | addMessage(msg.sender, msg.receiver, msg.contentObj.text); 142 | } else { 143 | addMessage(msg.sender, msg.receiver, msg.content); 144 | } 145 | } catch (e) { 146 | console.log("json parse exception:", e); 147 | addMessage(msg.sender, msg.receiver, msg.content); 148 | return 149 | } 150 | }, 151 | onConnectState: function(state) { 152 | if (state == IMService.STATE_CONNECTED) { 153 | //console.log("im connected"); 154 | // 连接成功 155 | //showChat(); 156 | } else if (state == IMService.STATE_CONNECTING) { 157 | console.log("im connecting"); 158 | } else if (state == IMService.STATE_CONNECTFAIL) { 159 | console.log("im connect fail"); 160 | } else if (state == IMService.STATE_UNCONNECTED) { 161 | console.log("im unconnected"); 162 | } 163 | } 164 | }; 165 | 166 | var im = new IMService(); 167 | im.host = host; 168 | im.observer = observer; 169 | 170 | var r = util.getURLParameter('receiver', location.search); 171 | if (r) { 172 | roomID = parseInt(r); 173 | } else { 174 | roomID = 0; 175 | } 176 | console.log("roomID:" + roomID) 177 | 178 | r = util.getURLParameter('sender', location.search); 179 | if (r) { 180 | sender = parseInt(r); 181 | } else { 182 | sender = 0; 183 | } 184 | username = sender; 185 | setName(sender); 186 | console.log("sender:" + sender) 187 | 188 | token = util.getCookie("token"); 189 | console.log("token:" + token) 190 | im.accessToken = token 191 | im.start(); 192 | 193 | im.enterRoom(roomID); 194 | 195 | //deal with chat mode. 196 | $("#entry").keypress(function (e) { 197 | if (e.keyCode != 13 /* Return */) return; 198 | var msg = $("#entry").val().replace("\n", ""); 199 | if (!util.isBlank(msg)) { 200 | var obj = {"text": msg}; 201 | var textMsg = JSON.stringify(obj); 202 | //receiver表示roomid 203 | var message = {sender:sender, receiver:roomID, content: textMsg}; 204 | if (im.connectState == IMService.STATE_CONNECTED) { 205 | im.sendRoomMessage(message); 206 | $("#entry").val(""); // clear the entry field. 207 | addMessage(sender, roomID, msg); 208 | } 209 | } 210 | }); 211 | }); 212 | 213 | -------------------------------------------------------------------------------- /demo/static/js/rtc_adapter.js: -------------------------------------------------------------------------------- 1 | var RTCPeerConnection = null; 2 | var getUserMedia = null; 3 | var attachMediaStream = null; 4 | var reattachMediaStream = null; 5 | var webrtcDetectedBrowser = null; 6 | 7 | function trace(text) 8 | { 9 | if (text[text.length - 1] == '\n') text = text.substring(0, text.length - 1); 10 | console.log((performance.now() / 1000).toFixed(3) + ": " + text); 11 | } 12 | 13 | if (navigator.mozGetUserMedia) 14 | { 15 | console.log("This appears to be Firefox"); 16 | 17 | webrtcDetectedBrowser = "firefox"; 18 | 19 | RTCPeerConnection = mozRTCPeerConnection; 20 | RTCSessionDescription = mozRTCSessionDescription; 21 | RTCIceCandidate = mozRTCIceCandidate; 22 | getUserMedia = navigator.mozGetUserMedia.bind(navigator); 23 | 24 | attachMediaStream = 25 | function(element, stream) 26 | { 27 | console.log("Attaching media stream"); 28 | element.mozSrcObject = stream; 29 | element.play(); 30 | }; 31 | 32 | reattachMediaStream = 33 | function(to, from) 34 | { 35 | console.log("Reattaching media stream"); 36 | to.mozSrcObject = from.mozSrcObject; 37 | to.play(); 38 | }; 39 | 40 | MediaStream.prototype.getVideoTracks = 41 | function() 42 | { 43 | return []; 44 | }; 45 | 46 | MediaStream.prototype.getAudioTracks = 47 | function() 48 | { 49 | return []; 50 | }; 51 | } 52 | else if (navigator.webkitGetUserMedia) 53 | { 54 | console.log("This appears to be Chrome"); 55 | 56 | webrtcDetectedBrowser = "chrome"; 57 | 58 | RTCPeerConnection = webkitRTCPeerConnection; 59 | getUserMedia = navigator.webkitGetUserMedia.bind(navigator); 60 | attachMediaStream = 61 | function(element, stream) 62 | { 63 | element.src = webkitURL.createObjectURL(stream); 64 | }; 65 | 66 | reattachMediaStream = 67 | function(to, from) 68 | { 69 | to.src = from.src; 70 | }; 71 | 72 | if (!webkitMediaStream.prototype.getVideoTracks) 73 | { 74 | webkitMediaStream.prototype.getVideoTracks = 75 | function() 76 | { 77 | return this.videoTracks; 78 | }; 79 | webkitMediaStream.prototype.getAudioTracks = 80 | function() 81 | { 82 | return this.audioTracks; 83 | }; 84 | } 85 | 86 | if (!webkitRTCPeerConnection.prototype.getLocalStreams) 87 | { 88 | webkitRTCPeerConnection.prototype.getLocalStreams = 89 | function() 90 | { 91 | return this.localStreams; 92 | }; 93 | webkitRTCPeerConnection.prototype.getRemoteStreams = 94 | function() 95 | { 96 | return this.remoteStreams; 97 | }; 98 | } 99 | } 100 | else 101 | { 102 | console.log("Browser does not appear to be WebRTC-capable"); 103 | } 104 | -------------------------------------------------------------------------------- /demo/static/js/voip_chat.js: -------------------------------------------------------------------------------- 1 | 2 | var peer;//对方id 3 | var uid;//当前用户id 4 | 5 | var cameraView; 6 | var remoteView; 7 | 8 | var im; 9 | var voipSession = null; 10 | 11 | 12 | var observer = { 13 | 14 | handleRTMessage:function(msg) { 15 | console.log("rt message...:", msg.content); 16 | 17 | if (msg.receiver != uid) { 18 | return; 19 | } 20 | 21 | var rtObj = JSON.parse(msg.content); 22 | if (rtObj.p2p) { 23 | if (voipSession) { 24 | voipSession.handleP2PMessage(rtObj.p2p); 25 | } 26 | } else if (rtObj.voip) { 27 | var obj = rtObj.voip; 28 | 29 | var cmd = new VOIPCommand(obj); 30 | cmd.fromData(obj); 31 | if (cmd.cmd == VOIPCommand.VOIP_COMMAND_DIAL || 32 | cmd.cmd == VOIPCommand.VOIP_COMMAND_DIAL_VIDEO) { 33 | if (!voipSession) { 34 | voipSession = new VOIPSession(im); 35 | 36 | voipSession.onConnected = onConnected; 37 | voipSession.onRemoteHangUp = onRemoteHangUp; 38 | 39 | voipSession.uid = uid; 40 | voipSession.peer = msg.sender; 41 | voipSession.isCaller = false; 42 | voipSession.token = token; 43 | voipSession.channelID = cmd.channelID; 44 | voipSession.cameraView = cameraView; 45 | voipSession.remoteView = remoteView; 46 | 47 | document.getElementById('accept').style.display = 'inline'; 48 | document.getElementById('refuse').style.display = 'inline'; 49 | document.getElementById('dial').style.display = 'none'; 50 | } 51 | } 52 | if (voipSession) { 53 | voipSession.handleVOIPMessage(obj, msg.sender); 54 | } 55 | } 56 | }, 57 | 58 | onConnectState: function(state) { 59 | if (state == IMService.STATE_CONNECTED) { 60 | console.log("im connected"); 61 | } else if (state == IMService.STATE_CONNECTING) { 62 | console.log("im connecting"); 63 | } else if (state == IMService.STATE_CONNECTFAIL) { 64 | console.log("im connect fail"); 65 | } else if (state == IMService.STATE_UNCONNECTED) { 66 | console.log("im unconnected"); 67 | } 68 | } 69 | }; 70 | 71 | 72 | util = { 73 | urlRE: /https?:\/\/([-\w\.]+)+(:\d+)?(\/([^\s]*(\?\S+)?)?)?/g, 74 | // html sanitizer 75 | toStaticHTML: function (inputHtml) { 76 | inputHtml = inputHtml.toString(); 77 | return inputHtml.replace(/&/g, "&").replace(//g, ">"); 78 | }, 79 | //pads n with zeros on the left, 80 | //digits is minimum length of output 81 | //zeroPad(3, 5); returns "005" 82 | //zeroPad(2, 500); returns "500" 83 | zeroPad: function (digits, n) { 84 | n = n.toString(); 85 | while (n.length < digits) 86 | n = '0' + n; 87 | return n; 88 | }, 89 | //it is almost 8 o'clock PM here 90 | //timeString(new Date); returns "19:49" 91 | timeString: function (date) { 92 | var minutes = date.getMinutes().toString(); 93 | var hours = date.getHours().toString(); 94 | return this.zeroPad(2, hours) + ":" + this.zeroPad(2, minutes); 95 | }, 96 | 97 | //does the argument only contain whitespace? 98 | isBlank: function (text) { 99 | var blank = /^\s*$/; 100 | return (text.match(blank) !== null); 101 | }, 102 | 103 | 104 | getURLParameter: function(name, search) { 105 | search = search || location.search 106 | var param = search.match( 107 | RegExp(name + '=' + '(.+?)(&|$)')) 108 | return param ? decodeURIComponent(param[1]) : null 109 | }, 110 | 111 | getCookie: function(c_name) { 112 | if (document.cookie.length>0) { 113 | c_start=document.cookie.indexOf(c_name + "=") 114 | if (c_start!=-1) { 115 | c_start=c_start + c_name.length+1 116 | c_end=document.cookie.indexOf(";",c_start) 117 | if (c_end==-1) c_end=document.cookie.length 118 | return unescape(document.cookie.substring(c_start,c_end)) 119 | } 120 | } 121 | return "" 122 | }, 123 | 124 | }; 125 | 126 | 127 | $(document).ready(function () { 128 | var r = util.getURLParameter('receiver', location.search); 129 | if (r) { 130 | peer = parseInt(r); 131 | } else { 132 | peer = 0; 133 | } 134 | console.log("peer:" + peer) 135 | 136 | r = util.getURLParameter('sender', location.search); 137 | if (r) { 138 | sender = parseInt(r); 139 | } else { 140 | sender = 0; 141 | } 142 | uid = sender; 143 | console.log("uid:" + sender) 144 | 145 | token = util.getCookie("token"); 146 | console.log("token:" + token) 147 | 148 | im = new IMService(); 149 | im.host = host 150 | 151 | im.accessToken = token 152 | im.start(); 153 | 154 | im.observer = observer; 155 | im.voipObserver = observer; 156 | 157 | cameraView = document.getElementById('camera'); 158 | remoteView = document.getElementById('remote'); 159 | cameraView.muted = true; 160 | }); 161 | 162 | function onConnected() { 163 | console.log("on connected"); 164 | VOIPSession.prototype.onConnected.call(this); 165 | 166 | document.getElementById('accept').style.display = 'none'; 167 | document.getElementById('refuse').style.display = 'none'; 168 | document.getElementById('dial').style.display = 'none'; 169 | document.getElementById('hangup').style.display = 'inline'; 170 | } 171 | 172 | function onRemoteHangUp() { 173 | VOIPSession.prototype.onRemoteHangUp.call(this); 174 | document.getElementById('accept').style.display = 'none'; 175 | document.getElementById('refuse').style.display = 'none'; 176 | document.getElementById('dial').style.display = 'inline'; 177 | document.getElementById('hangup').style.display = 'none'; 178 | 179 | voipSession = null; 180 | } 181 | 182 | //呼叫对方 183 | function onDialClick() { 184 | console.log("on dial click"); 185 | 186 | voipSession = new VOIPSession(im); 187 | 188 | voipSession.onConnected = onConnected; 189 | 190 | voipSession.onRemoteHangUp = onRemoteHangUp; 191 | 192 | voipSession.uid = sender; 193 | voipSession.peer = peer; 194 | voipSession.isCaller = true; 195 | voipSession.token = token; 196 | 197 | 198 | var timeInMs = Date.now(); 199 | voipSession.channelID = "" + timeInMs; 200 | 201 | voipSession.cameraView = cameraView; 202 | voipSession.remoteView = remoteView; 203 | voipSession.dial(); 204 | } 205 | 206 | function onAccept() { 207 | console.log("on accept"); 208 | voipSession.accept(); 209 | } 210 | 211 | function onRefuse() { 212 | console.log("on refuse"); 213 | voipSession.refuse(); 214 | } 215 | 216 | function onHangUp() { 217 | console.log("on hangup"); 218 | voipSession.hangUp(); 219 | voipSession = null; 220 | document.getElementById('accept').style.display = 'none'; 221 | document.getElementById('refuse').style.display = 'none'; 222 | document.getElementById('dial').style.display = 'inline'; 223 | document.getElementById('hangup').style.display = 'none'; 224 | } 225 | -------------------------------------------------------------------------------- /demo/static/js/voip_command.js: -------------------------------------------------------------------------------- 1 | function VOIPCommand() { 2 | this.cmd = 0; 3 | this.channelID = ""; 4 | } 5 | 6 | VOIPCommand.prototype.fromData = function(obj) { 7 | this.cmd = obj.command; 8 | this.channelID = obj.channel_id; 9 | }; 10 | 11 | VOIPCommand.prototype.toData = function() { 12 | var obj = { 13 | command:this.cmd, 14 | channel_id:this.channelID 15 | }; 16 | return obj; 17 | }; 18 | 19 | //语音通话 20 | VOIPCommand.VOIP_COMMAND_DIAL = 1; 21 | VOIPCommand.VOIP_COMMAND_ACCEPT = 2; 22 | VOIPCommand.VOIP_COMMAND_CONNECTED = 3; 23 | VOIPCommand.VOIP_COMMAND_REFUSE = 4; 24 | VOIPCommand.VOIP_COMMAND_REFUSED = 5; 25 | VOIPCommand.VOIP_COMMAND_HANG_UP = 6; 26 | VOIPCommand.VOIP_COMMAND_RESET = 7; 27 | VOIPCommand.VOIP_COMMAND_TALKING = 8; 28 | VOIPCommand.VOIP_COMMAND_DIAL_VIDEO = 9; 29 | VOIPCommand.VOIP_COMMAND_PING = 10; 30 | 31 | 32 | -------------------------------------------------------------------------------- /demo/static/js/voip_session.js: -------------------------------------------------------------------------------- 1 | function VOIPSession(im) { 2 | VOIPStream.call(this); 3 | this.im = im; 4 | this.state = VOIPSession.VOIP_ACCEPTING; 5 | 6 | this.dialTimer = 0; 7 | this.pingTimer = 0; 8 | this.transferTimer = 0; 9 | } 10 | 11 | VOIPSession.VOIP_LISTENING = 1; 12 | VOIPSession.VOIP_DIALING = 2;//呼叫对方 13 | VOIPSession.VOIP_CONNECTED = 3;//通话连接成功 14 | VOIPSession.VOIP_ACCEPTING = 4;//询问用户是否接听来电 15 | VOIPSession.VOIP_ACCEPTED = 5;//用户接听来电 16 | VOIPSession.VOIP_REFUSING = 6;//来电被拒 17 | VOIPSession.VOIP_REFUSED = 7;//(来/去)电已被拒 18 | VOIPSession.VOIP_HANGED_UP = 8;//通话被挂断 19 | VOIPSession.VOIP_SHUTDOWN = 9;//对方正在通话中连接被终止 20 | 21 | 22 | function getNow() { 23 | var d = new Date(); 24 | return d.getTime()/1000; 25 | } 26 | 27 | //ECMAScript 5 Object.create(o) 28 | function object(o) { 29 | var F = function () {}; 30 | F.prototype = o; 31 | return new F(); 32 | } 33 | 34 | VOIPSession.prototype = object(VOIPStream.prototype); 35 | VOIPSession.prototype.constructor = VOIPSession; 36 | 37 | VOIPSession.prototype.dial = function() { 38 | this.state = VOIPSession.VOIP_DIALING; 39 | 40 | this.dialBeginTimestamp = getNow(); 41 | this.sendDial(); 42 | 43 | var self = this; 44 | this.dialTimer = setInterval(function() { 45 | self.sendDial(); 46 | var now = getNow(); 47 | if (now - self.dialBeginTimestamp >= 60) { 48 | self.onDialTimeout(); 49 | self.clearDialTimer(); 50 | } 51 | }, 1000); 52 | 53 | console.log("dial timer:", this.dialTimer); 54 | }; 55 | 56 | 57 | VOIPSession.prototype.sendDial = function() { 58 | console.log("send dial..."); 59 | var command = new VOIPCommand(); 60 | command.cmd = VOIPCommand.VOIP_COMMAND_DIAL_VIDEO; 61 | command.channelID = this.channelID; 62 | 63 | this.sendCommand(command); 64 | }; 65 | 66 | VOIPSession.prototype.ping = function() { 67 | var self = this; 68 | this.pingTimer = setInterval(function() { 69 | self.sendPing(); 70 | }, 1000); 71 | this.sendPing(); 72 | } 73 | 74 | VOIPSession.prototype.sendPing = function() { 75 | var command = new VOIPCommand(); 76 | command.cmd = VOIPCommand.VOIP_COMMAND_PING; 77 | command.channelID = this.channelID; 78 | this.sendCommand(command); 79 | }; 80 | 81 | VOIPSession.prototype.accept = function() { 82 | this.state = VOIPSession.VOIP_ACCEPTED; 83 | this.sendDialAccept(); 84 | }; 85 | 86 | 87 | VOIPSession.prototype.refuse = function() { 88 | this.state = VOIPSession.VOIP_REFUSED; 89 | this.sendDialRefuse(); 90 | }; 91 | 92 | 93 | //清空所有的定时器 94 | VOIPSession.prototype.close = function() { 95 | if (this.pingTimer) { 96 | clearInterval(this.pingTimer); 97 | this.pingTimer = 0; 98 | } 99 | if (this.dialTimer) { 100 | clearInterval(this.dialTimer); 101 | this.dialTimer = 0; 102 | } 103 | } 104 | 105 | VOIPSession.prototype.hangUp = function() { 106 | if (this.state == VOIPSession.VOIP_DIALING) { 107 | this.clearDialTimer(); 108 | this.sendHangUp(); 109 | this.state = VOIPSession.VOIP_HANG_UP; 110 | } else if (this.state == VOIPSession.VOIP_CONNECTED) { 111 | this.sendHangUp(); 112 | this.state = VOIPSession.VOIP_HANG_UP; 113 | 114 | this.stopStream(function() { 115 | console.log("on stream closed"); 116 | }); 117 | 118 | this.close(); 119 | 120 | } else { 121 | console.log("invalid voip state:" + this.state); 122 | } 123 | }; 124 | 125 | VOIPSession.prototype.sendCommand = function(command) { 126 | var msg = {}; 127 | msg.sender = this.uid; 128 | msg.receiver = this.peer; 129 | msg.content = JSON.stringify({voip:command.toData()}); 130 | var r = this.im.sendRTMessage(msg); 131 | return r; 132 | }; 133 | 134 | VOIPSession.prototype.sendTalking = function(receiver) { 135 | var command = new VOIPCommand(); 136 | command.cmd = VOIPCommand.VOIP_COMMAND_TALKING; 137 | command.channelID = this.channelID; 138 | var msg = {}; 139 | msg.sender = this.uid; 140 | msg.receiver = receiver; 141 | msg.content = JSON.stringify({voip:command.toData()}); 142 | this.im.sendRTMessage(msg); 143 | }; 144 | 145 | 146 | VOIPSession.prototype.sendDialAccept = function(recveiver) { 147 | var command = new VOIPCommand(); 148 | command.cmd = VOIPCommand.VOIP_COMMAND_ACCEPT; 149 | command.channelID = this.channelID; 150 | this.sendCommand(command); 151 | }; 152 | 153 | 154 | VOIPSession.prototype.sendDialRefuse = function(receiver) { 155 | var command = new VOIPCommand(); 156 | command.cmd = VOIPCommand.VOIP_COMMAND_REFUSE; 157 | command.channelID = this.channelID; 158 | this.sendCommand(command); 159 | }; 160 | 161 | 162 | VOIPSession.prototype.sendConnected = function(receiver) { 163 | var command = new VOIPCommand(); 164 | command.cmd = VOIPCommand.VOIP_COMMAND_CONNECTED; 165 | command.channelID = this.channelID; 166 | this.sendCommand(command); 167 | }; 168 | 169 | VOIPSession.prototype.sendRefused = function(receiver) { 170 | var command = new VOIPCommand(); 171 | command.cmd = VOIPCommand.VOIP_COMMAND_REFUSED; 172 | command.channelID = this.channelID; 173 | this.sendCommand(command); 174 | }; 175 | 176 | VOIPSession.prototype.sendHangUp = function() { 177 | var command = new VOIPCommand(); 178 | command.cmd = VOIPCommand.VOIP_COMMAND_HANG_UP; 179 | command.channelID = this.channelID; 180 | this.sendCommand(command); 181 | }; 182 | 183 | VOIPSession.prototype.clearDialTimer = function() { 184 | console.log("clear dial timer....:", this.dialTimer); 185 | if (this.dialTimer > 0) { 186 | console.log("clear dial timer"); 187 | clearInterval(this.dialTimer); 188 | this.dialTimer = 0; 189 | } 190 | }; 191 | 192 | VOIPSession.prototype.handleVOIPMessage = function(obj, sender) { 193 | 194 | console.log("handle voip message..."); 195 | if (sender != this.peer) { 196 | this.sendTalking(sender); 197 | return; 198 | } 199 | 200 | var command = new VOIPCommand(); 201 | command.fromData(obj); 202 | if (this.state == VOIPSession.VOIP_DIALING) { 203 | if (command.cmd == VOIPCommand.VOIP_COMMAND_ACCEPT) { 204 | this.sendConnected(); 205 | this.clearDialTimer(); 206 | this.onConnected(); 207 | this.state = VOIPSession.VOIP_CONNECTED; 208 | } else if (command.cmd == VOIPCommand.VOIP_COMMAND_REFUSE) { 209 | this.sendRefused(); 210 | this.clearDialTimer(); 211 | this.onRemoteRefuse(); 212 | this.state = VOIPSession.VOIP_REFUSED; 213 | } else if (command.cmd == VOIPCommand.VOIP_COMMAND_TALKING) { 214 | this.clearDialTimer(); 215 | this.onTalking(); 216 | this.state = VOIPSession.VOIP_SHUTDOWN; 217 | } 218 | } else if (this.state == VOIPSession.VOIP_ACCEPTING) { 219 | if (command.cmd == VOIPCommand.VOIP_COMMAND_HANG_UP) { 220 | this.onRemoteHangUp(); 221 | this.state = VOIPSession.VOIP_HANGED_UP; 222 | } 223 | } else if (this.state == VOIPSession.VOIP_ACCEPTED) { 224 | if (command.cmd == VOIPCommand.VOIP_COMMAND_CONNECTED) { 225 | this.onConnected(); 226 | this.state = VOIPSession.VOIP_CONNECTED; 227 | } else if (command.cmd == VOIPCommand.VOIP_COMMAND_DIAL|| 228 | command.cmd == VOIPCommand.VOIP_COMMAND_DIAL_VIDEO) { 229 | this.sendDialAccept(); 230 | } 231 | } else if (this.state == VOIPSession.VOIP_CONNECTED) { 232 | if (command.cmd == VOIPCommand.VOIP_COMMAND_HANG_UP) { 233 | this.onRemoteHangUp(); 234 | this.state = VOIPSession.VOIP_HANGED_UP; 235 | } 236 | } 237 | }; 238 | 239 | 240 | VOIPSession.prototype.onRemoteRefuse = function() { 241 | console.log("on refuse"); 242 | }; 243 | 244 | VOIPSession.prototype.onRemoteHangUp = function() { 245 | console.log("on remote hangup"); 246 | if (this.state == VOIPSession.VOIP_CONNECTED) { 247 | this.stopStream(function() { 248 | console.log("on stream closed"); 249 | }); 250 | } 251 | 252 | this.close(); 253 | }; 254 | 255 | VOIPSession.prototype.onTalking = function() { 256 | console.log("on talking"); 257 | }; 258 | 259 | VOIPSession.prototype.onDialTimeout = function() { 260 | console.log("dial timeout"); 261 | }; 262 | 263 | VOIPSession.prototype.onConnected = function() { 264 | console.log("on connected"); 265 | this.startStream(); 266 | this.ping(); 267 | }; 268 | 269 | 270 | -------------------------------------------------------------------------------- /demo/static/js/voip_stream.js: -------------------------------------------------------------------------------- 1 | 2 | var configuration = { 3 | 'iceServers': [{ 4 | 'url': 'stun:stun.counterpath.net:3478' 5 | }, {"url":"turn:turn.gobelieve.io:3478?transport=udp"}] 6 | }; 7 | 8 | 9 | 10 | function VOIPStream() { 11 | this.pc = null; 12 | this.mediaStream = null; 13 | 14 | this.isCaller = false; 15 | this.uid = 0; 16 | this.peer = 0; 17 | 18 | this.cameraView = null; 19 | this.remoteView = null; 20 | 21 | this.logError = this.logError.bind(this); 22 | this.localDescCreated = this.localDescCreated.bind(this); 23 | } 24 | 25 | 26 | VOIPStream.prototype.startStream = function() { 27 | console.log("stream stream..."); 28 | var kRTCICECandidateTypeKey = "type"; 29 | var kRTCICECandidateTypeValue = "candidate"; 30 | var kRTCICECandidateMidKey = "id"; 31 | var kRTCICECandidateMLineIndexKey = "label"; 32 | var kRTCICECandidateSdpKey = "candidate"; 33 | var kRTCICECandidatesTypeKey = "candidates"; 34 | 35 | var turnServer = configuration.iceServers[1]; 36 | turnServer.username = "7_" + uid; 37 | turnServer.credential = token; 38 | 39 | var pc = new RTCPeerConnection(configuration); 40 | 41 | var uid = this.uid; 42 | var peer = this.peer; 43 | // send any ice candidates to the other peer 44 | pc.onicecandidate = function (evt) { 45 | if (evt.candidate) { 46 | var candidate = {}; 47 | console.log("origin candidate:" + JSON.stringify(evt.candidate)); 48 | candidate[kRTCICECandidateTypeKey] = kRTCICECandidateTypeValue; 49 | 50 | candidate[kRTCICECandidateMLineIndexKey] = evt.candidate.sdpMLineIndex; 51 | candidate[kRTCICECandidateMidKey] = evt.candidate.sdpMid; 52 | candidate[kRTCICECandidateSdpKey] = evt.candidate.candidate; 53 | 54 | var content = JSON.stringify({p2p:candidate}); 55 | console.log("candidate:" + content); 56 | 57 | msg = {sender:uid, receiver:peer, content:content}; 58 | im.sendRTMessage(msg); 59 | } 60 | }; 61 | 62 | // let the 'negotiationneeded' event trigger offer generation 63 | pc.onnegotiationneeded = function () { 64 | 65 | } 66 | 67 | var self = this; 68 | // once remote stream arrives, show it in the remote video element 69 | pc.onaddstream = function (evt) { 70 | self.remoteView.src = URL.createObjectURL(evt.stream); 71 | }; 72 | 73 | // get a local stream, show it in a self-view and add it to be sent 74 | navigator.getUserMedia({ 75 | 'audio': true, 76 | 'video': {width:640, height:480} 77 | }, function(stream) { 78 | console.log("got media stream:" + stream); 79 | self.mediaStream = stream; 80 | self.cameraView.src = URL.createObjectURL(stream); 81 | pc.addStream(stream); 82 | 83 | if (self.isCaller) { 84 | console.log("create offer..."); 85 | pc.createOffer(self.localDescCreated, self.logError); 86 | } 87 | }, this.logError); 88 | 89 | this.pc = pc; 90 | } 91 | 92 | 93 | VOIPStream.prototype.stopStream = function() { 94 | 95 | if (this.mediaStream) { 96 | this.mediaStream.getAudioTracks()[0].stop(); 97 | this.mediaStream.getVideoTracks()[0].stop(); 98 | this.mediaStream = null; 99 | } 100 | if (this.pc) { 101 | this.pc.close(); 102 | this.pc = null; 103 | } 104 | } 105 | 106 | 107 | 108 | VOIPStream.prototype.localDescCreated = function(desc) { 109 | console.log("set local description"); 110 | 111 | var pc = this.pc; 112 | var peer = this.peer; 113 | var uid = this.uid; 114 | pc.setLocalDescription(desc, function () { 115 | var obj = {}; 116 | obj.sdp = pc.localDescription.sdp; 117 | obj.type = pc.localDescription.type; 118 | console.log("local desc:" + pc.localDescription) 119 | console.log("local desc:" + JSON.stringify(obj)); 120 | 121 | var msg = {}; 122 | msg.sender = uid; 123 | msg.receiver = peer; 124 | msg.content = JSON.stringify({p2p:obj}); 125 | var r = im.sendRTMessage(msg); 126 | console.log("send rt message:" + r); 127 | 128 | }, this.logError); 129 | } 130 | 131 | VOIPStream.prototype.logError = function(error) { 132 | console.log(error.name + ': ' + error.message); 133 | } 134 | 135 | 136 | VOIPStream.prototype.handleP2PMessage = function(obj) { 137 | if (!this.pc) { 138 | console.log("peer connection is null"); 139 | return; 140 | } 141 | 142 | var pc = this.pc; 143 | if (obj.type == "candidate") { 144 | var m = {"sdpMid":obj.id, "sdpMLineIndex":obj.label, "candidate":obj.candidate} 145 | pc.addIceCandidate(new RTCIceCandidate(m)); 146 | } else if (obj.type == "remove-candidates") { 147 | 148 | } else if (obj.type == "offer") { 149 | var sd = new RTCSessionDescription(obj); 150 | console.log("set remote offer description") 151 | var self = this; 152 | 153 | pc.setRemoteDescription(sd, function() { 154 | pc.createAnswer(self.localDescCreated, self.logError); 155 | }); 156 | } else if (obj.type == "answer") { 157 | var sd = new RTCSessionDescription(obj); 158 | console.log("set remote answer description"); 159 | pc.setRemoteDescription(sd, function() { 160 | 161 | }); 162 | } 163 | } 164 | 165 | -------------------------------------------------------------------------------- /demo/templates/chat.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | example 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 18 | 19 | 20 | 21 | 22 |
23 |
24 |
25 |
26 | 39 | 40 |
41 |
42 | 43 | 44 | 45 | -------------------------------------------------------------------------------- /demo/templates/customer_chat.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | example 8 | 9 | 10 | 11 | 12 | 13 | 14 | 28 | 29 | 30 |
31 |
32 |
33 |
34 | 47 | 48 |
49 |
50 | 51 | 52 | 53 | 54 | 55 | 56 | 57 | 60 | 61 | 62 | 63 | 64 | -------------------------------------------------------------------------------- /demo/templates/customer_index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | Chat Demo 7 | 8 | 9 | 10 | 27 | 28 | 29 | 30 | 31 |
32 | 33 | 34 | 35 | 36 | 37 | 38 | 39 | 40 | 41 | 42 | 43 | 44 | 45 | 46 | 47 |
48 | 49 | 50 | -------------------------------------------------------------------------------- /demo/templates/group_chat.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | example 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 18 | 19 | 20 | 21 | 22 |
23 |
24 |
25 |
26 | 39 | 40 |
41 |
42 | 43 | 44 | 45 | -------------------------------------------------------------------------------- /demo/templates/group_index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | Group Demo 7 | 8 | 9 | 10 | 27 | 28 | 29 | 30 | 31 |
32 | 33 | 34 | 35 | 36 | 37 | 38 | 39 | 40 | 41 | 42 | 43 | 44 | 45 | 46 | 47 |
48 | 49 | 50 | -------------------------------------------------------------------------------- /demo/templates/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | Chat Demo 7 | 8 | 9 | 10 | 27 | 28 | 29 | 30 | 31 |
32 | 33 | 34 | 35 | 36 | 37 | 38 | 39 | 40 | 41 | 42 | 43 | 44 | 45 | 46 | 47 |
48 | 49 | 50 | -------------------------------------------------------------------------------- /demo/templates/room_chat.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | example 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 18 | 19 | 20 | 21 | 22 |
23 |
24 |
25 |
26 | 33 | 34 |
35 |
36 | 37 | 38 | 39 | -------------------------------------------------------------------------------- /demo/templates/room_index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | Chat Demo 7 | 8 | 9 | 10 | 27 | 28 | 29 | 30 | 31 |
32 | 33 | 34 | 35 | 36 | 37 | 38 | 39 | 40 | 41 | 42 | 43 | 44 | 45 | 46 | 47 |
48 | 49 | 50 | -------------------------------------------------------------------------------- /demo/templates/voip_chat.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | example 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | 38 | 39 | -------------------------------------------------------------------------------- /lib/byte_order.js: -------------------------------------------------------------------------------- 1 | /* Copyright 2010 Membase, Inc. 2 | * 3 | * Licensed under the Apache License, Version 2.0 (the "License"); 4 | * you may not use this file except in compliance with the License. 5 | * You may obtain a copy of the License at 6 | * 7 | * http://www.apache.org/licenses/LICENSE-2.0 8 | * 9 | * Unless required by applicable law or agreed to in writing, software 10 | * distributed under the License is distributed on an "AS IS" BASIS, 11 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | * See the License for the specific language governing permissions and 13 | * limitations under the License. 14 | */ 15 | 16 | 'use strict'; 17 | 18 | var bigInt = require("big-integer"); 19 | 20 | /*jshint node:true*/ 21 | 22 | 23 | /** 24 | * Convert a 16-bit quantity (short integer) from host byte order to network byte order (Little-Endian to Big-Endian). 25 | * 26 | * @param {Array|Buffer} b Array of octets or a nodejs Buffer 27 | * @param {number} i Zero-based index at which to write into b 28 | * @param {number} v Value to convert 29 | */ 30 | exports.htons = function(b, i, v) { 31 | b[i] = (0xff & (v >> 8)); 32 | b[i + 1] = (0xff & (v)); 33 | }; 34 | 35 | 36 | /** 37 | * Convert a 16-bit quantity (short integer) from network byte order to host byte order (Big-Endian to Little-Endian). 38 | * 39 | * @param {Array|Buffer} b Array of octets or a nodejs Buffer to read value from 40 | * @param {number} i Zero-based index at which to read from b 41 | * @returns {number} 42 | */ 43 | exports.ntohs = function(b, i) { 44 | return ((0xff & b[i]) << 8) | 45 | ((0xff & b[i + 1])); 46 | }; 47 | 48 | 49 | /** 50 | * Convert a 16-bit quantity (short integer) from network byte order to host byte order (Big-Endian to Little-Endian). 51 | * 52 | * @param {string} s String to read value from 53 | * @param {number} i Zero-based index at which to read from s 54 | * @returns {number} 55 | */ 56 | exports.ntohsStr = function(s, i) { 57 | return ((0xff & s.charCodeAt(i)) << 8) | 58 | ((0xff & s.charCodeAt(i + 1))); 59 | }; 60 | 61 | 62 | /** 63 | * Convert a 32-bit quantity (long integer) from host byte order to network byte order (Little-Endian to Big-Endian). 64 | * 65 | * @param {Array|Buffer} b Array of octets or a nodejs Buffer 66 | * @param {number} i Zero-based index at which to write into b 67 | * @param {number} v Value to convert 68 | */ 69 | exports.htonl = function(b, i, v) { 70 | b[i] = (0xff & (v >> 24)); 71 | b[i + 1] = (0xff & (v >> 16)); 72 | b[i + 2] = (0xff & (v >> 8)); 73 | b[i + 3] = (0xff & (v)); 74 | }; 75 | 76 | 77 | /** 78 | * Convert a 32-bit quantity (long integer) from network byte order to host byte order (Big-Endian to Little-Endian). 79 | * 80 | * @param {Array|Buffer} b Array of octets or a nodejs Buffer to read value from 81 | * @param {number} i Zero-based index at which to read from b 82 | * @returns {number} 83 | */ 84 | exports.ntohl = function(b, i) { 85 | return ((0xff & b[i]) << 24) | 86 | ((0xff & b[i + 1]) << 16) | 87 | ((0xff & b[i + 2]) << 8) | 88 | ((0xff & b[i + 3])); 89 | }; 90 | 91 | 92 | /** 93 | * Convert a 32-bit quantity (long integer) from network byte order to host byte order (Big-Endian to Little-Endian). 94 | * 95 | * @param {string} s String to read value from 96 | * @param {number} i Zero-based index at which to read from s 97 | * @returns {number} 98 | */ 99 | exports.ntohlStr = function(s, i) { 100 | return ((0xff & s.charCodeAt(i)) << 24) | 101 | ((0xff & s.charCodeAt(i + 1)) << 16) | 102 | ((0xff & s.charCodeAt(i + 2)) << 8) | 103 | ((0xff & s.charCodeAt(i + 3))); 104 | }; 105 | 106 | //max int64 2^53 107 | exports.hton64 = function(b, i, v) { 108 | var h32; 109 | var l32; 110 | if (typeof(v) == 'string') { 111 | var largeNumber = bigInt(v); 112 | l32 = largeNumber.and(0xffffffff); 113 | h32 = largeNumber.shiftRight(32); 114 | } else { 115 | h32 = Math.floor(v/4294967296); 116 | l32 = v - h32*4294967296; 117 | } 118 | 119 | b[i] = 0xff & (h32 >> 24); 120 | b[i+1] = 0xff & (h32 >> 16); 121 | b[i+2] = 0xff & (h32 >> 8); 122 | b[i+3] = 0xff & h32; 123 | b[i+4] = 0xff & (l32 >> 24); 124 | b[i+5] = 0xff & (l32 >> 16); 125 | b[i+6] = 0xff & (l32 >> 8); 126 | b[i+7] = 0xff & l32; 127 | } 128 | 129 | 130 | 131 | exports.ntoh64 = function(b, i) { 132 | var MAX_INT = 9007199254740992; 133 | 134 | var low32 = ((0xff & b[i+4]) * (1<< 24)) + (((0xff & b[i+5]) << 16)| 135 | ((0xff & b[i+6]) << 8) | 136 | ((0xff & b[i+7]))); 137 | var high32 = ((0xff & b[i]) << 24) | 138 | ((0xff & b[i+1]) << 16)| 139 | ((0xff & b[i+2]) << 8)| 140 | (0xff & b[i+3]); 141 | 142 | var high = bigInt(high32); 143 | 144 | 145 | var v = high.shiftLeft(32).add(low32); 146 | 147 | if (v.gt(-MAX_INT) && v.lesser(MAX_INT)) { 148 | return v.toJSNumber(); 149 | } 150 | 151 | return v.toString(); 152 | } 153 | -------------------------------------------------------------------------------- /lib/im.d.ts: -------------------------------------------------------------------------------- 1 | export interface RTMessage { 2 | sender:number; 3 | receiver:number; 4 | content:string; 5 | } 6 | 7 | export interface IMMessage { 8 | sender:number; 9 | receiver:number; 10 | timestamp:number; 11 | content:string; 12 | isSelf?:boolean; 13 | } 14 | 15 | export interface CustomerMessage extends IMMessage { 16 | senderAppID:number; 17 | receiverAppID:number; 18 | } 19 | 20 | export interface PeerMessageObserver { 21 | handlePeerMessage(msg:any); 22 | handlePeerMessageACK?(msg); 23 | handlePeerMessageFailure?(msg); 24 | } 25 | 26 | export interface GroupMessageObserver { 27 | handleGroupMessage(msg); 28 | handleGroupMessageACK?(msg); 29 | handleGroupMessageFailure?(msg); 30 | handleGroupNotification?(msg); 31 | } 32 | 33 | export interface CustomerMessageObserver { 34 | handleCustomerMessage(msg); 35 | handleCustomerMessageACK?(msg); 36 | handleCustomerMessageFailure?(msg); 37 | } 38 | 39 | export interface RTMessageObserver { 40 | handleRTMessage(msg); 41 | } 42 | 43 | export interface SystemMessageObserver { 44 | handleSystemMessage(msg); 45 | } 46 | 47 | export interface RoomMessageObserver { 48 | handleRoomMessage(msg); 49 | } 50 | 51 | export interface Observer { 52 | onConnectState?(state:number); 53 | handleNotification?(msg); 54 | saveSyncKey?(syncKey); 55 | } 56 | 57 | export default class IMService { 58 | accessToken:string; 59 | protocol:string; 60 | host:string; 61 | port:number; 62 | syncKey:number; 63 | deviceID:string; 64 | platformID:number; 65 | peerMessageObserver?:PeerMessageObserver; 66 | groupMessageObserver?:GroupMessageObserver; 67 | customerMessageObserver?:CustomerMessageObserver; 68 | rtMessageObserver?:RTMessageObserver; 69 | systemMessageObserver?:SystemMessageObserver; 70 | roomMessageObserver?:RoomMessageObserver; 71 | observer?:Observer; 72 | 73 | connectState:number; 74 | sendPeerMessage(IMMessage); 75 | sendGroupMessage(IMMessage); 76 | sendRTMessage(RTMessage); 77 | sendCustomerMessage(msg:CustomerMessage); 78 | enterRoom(roomID:number); 79 | leaveRoom(roomID:number); 80 | handleConnectivityChange(reach:string); 81 | enterBackground(); 82 | enterForeground(); 83 | start(); 84 | stop(); 85 | } 86 | 87 | export const STATE_UNCONNECTED = 0; 88 | export const STATE_CONNECTING = 1; 89 | export const STATE_CONNECTED = 2; 90 | export const STATE_CONNECTFAIL = 3; 91 | export const STATE_AUTHENTICATION_FAIL = 4; -------------------------------------------------------------------------------- /lib/im.js: -------------------------------------------------------------------------------- 1 | var order = require('./byte_order'); 2 | var utf8 = require("./utf8"); 3 | 4 | var hton64 = order.hton64; 5 | var ntoh64 = order.ntoh64; 6 | var htonl = order.htonl; 7 | var ntohl = order.ntohl; 8 | 9 | 10 | 11 | function IMService() { 12 | this.host = "imnode2.gobelieve.io"; 13 | if (global.location && 'https:' === location.protocol) { 14 | this.port = 14891; 15 | } else { 16 | this.port = 13891; 17 | } 18 | this.protocol = undefined; 19 | 20 | this.accessToken = ""; 21 | this.syncKey = 0; 22 | this.groupSyncKeys = {}; 23 | 24 | this.peerMessageObserver = null; 25 | this.groupMessageObserver = null; 26 | this.customerMessageObserver = null; 27 | this.rtMessageObserver = null; 28 | this.systemMessageObserver = null; 29 | this.roomMessageObserver = null; 30 | this.observer = null; 31 | 32 | this.socket = null; 33 | this.connectFailCount = 0; 34 | this.connectState = IMService.STATE_UNCONNECTED; 35 | this.seq = 0; 36 | this.stopped = true; 37 | this.suspended = true; 38 | this.reachable = true; 39 | this.isBackground = false; 40 | 41 | this.roomID = 0; 42 | //sending message 43 | this.messages = {}; 44 | this.groupMessages = {}; 45 | this.customerMessages = {}; 46 | this.isSyncing = false; 47 | this.syncTimestamp = 0; 48 | this.pendingSyncKey = 0; 49 | 50 | this.pingTimer = null; 51 | this.pingTimestamp = 0; 52 | this.ping = this.ping.bind(this); 53 | this.platformID = IMService.PLATFORM_WEB; 54 | this.deviceID = IMService.guid(); 55 | } 56 | 57 | IMService.HEADSIZE = 12; 58 | IMService.VERSION = 1; 59 | 60 | IMService.STATE_UNCONNECTED = 0; 61 | IMService.STATE_CONNECTING = 1; 62 | IMService.STATE_CONNECTED = 2; 63 | IMService.STATE_CONNECTFAIL = 3; 64 | IMService.STATE_AUTHENTICATION_FAIL = 4; 65 | 66 | IMService.ACK_TIMEOUT = 5;//ack 超时5s,主动断开socket 67 | 68 | IMService.MSG_AUTH_STATUS = 3; 69 | IMService.MSG_IM = 4; 70 | IMService.MSG_ACK = 5; 71 | IMService.MSG_RST = 6; 72 | IMService.MSG_GROUP_NOTIFICATION = 7; 73 | IMService.MSG_GROUP_IM = 8; 74 | IMService.MSG_PEER_ACK = 9; 75 | IMService.MSG_PING = 13; 76 | IMService.MSG_PONG = 14; 77 | IMService.MSG_AUTH_TOKEN = 15; 78 | IMService.MSG_RT = 17; 79 | IMService.MSG_ENTER_ROOM = 18; 80 | IMService.MSG_LEAVE_ROOM = 19; 81 | IMService.MSG_ROOM_IM = 20; 82 | IMService.MSG_SYSTEM = 21; 83 | 84 | IMService.MSG_CUSTOMER_ = 24; 85 | IMService.MSG_CUSTOMER_SUPPORT_ = 25; 86 | 87 | IMService.MSG_SYNC = 26; 88 | IMService.MSG_SYNC_BEGIN = 27; 89 | IMService.MSG_SYNC_END = 28; 90 | IMService.MSG_SYNC_NOTIFY = 29; 91 | IMService.MSG_SYNC_GROUP = 30; 92 | IMService.MSG_SYNC_GROUP_BEGIN = 31; 93 | IMService.MSG_SYNC_GROUP_END = 32; 94 | IMService.MSG_SYNC_GROUP_NOTIFY = 33; 95 | IMService.MSG_SYNC_KEY = 34; 96 | IMService.MSG_GROUP_SYNC_KEY = 35; 97 | 98 | IMService.MSG_NOTIFICATION = 36; 99 | IMService.MSG_METADATA = 37; 100 | 101 | IMService.MSG_CUSTOMER = 64; 102 | 103 | 104 | //消息标志 105 | IMService.MESSAGE_FLAG_TEXT = 1; 106 | IMService.MESSAGE_FLAG_UNPERSISTENT = 2; 107 | IMService.MESSAGE_FLAG_SELF = 8; 108 | IMService.MESSAGE_FLAG_PUSH = 16; 109 | IMService.MESSAGE_FLAG_SUPER_GROUP = 32; 110 | 111 | IMService.PLATFORM_IOS = 1; 112 | IMService.PLATFORM_ANDROID = 2; 113 | IMService.PLATFORM_WEB = 3; 114 | IMService.PLATFORM_WIN = 4; 115 | IMService.PLATFORM_OSX = 5; 116 | 117 | IMService.HEARTBEAT = 60*3; 118 | 119 | 120 | 121 | IMService.prototype.handleConnectivityChange = function(reach) { 122 | console.log("connectivity changed:" + reach); 123 | this.reachable = reach && (reach.toLowerCase() == 'wifi' || reach.toLowerCase() == 'cell'); 124 | 125 | console.log("reachable:", reach, this.reachable); 126 | if (this.reachable) { 127 | if (!this.stopped && !this.isBackground) { 128 | console.log("reconnect im service"); 129 | this.suspend(); 130 | this.resume(); 131 | } 132 | } else if (!this.reachable) { 133 | this.suspend(); 134 | } 135 | } 136 | 137 | 138 | IMService.prototype.enterBackground = function() { 139 | this.isBackground = true; 140 | if (!this.stopped) { 141 | this.suspend(); 142 | } 143 | } 144 | 145 | IMService.prototype.enterForeground = function() { 146 | this.isBackground = false; 147 | if (!this.stopped) { 148 | this.resume(); 149 | } 150 | } 151 | 152 | IMService.prototype.suspend = function() { 153 | if (this.suspended) { 154 | return; 155 | } 156 | console.log("suspend im service"); 157 | this.suspended = true; 158 | 159 | console.log("close socket"); 160 | var sock = this.socket; 161 | if (sock) { 162 | //trigger socket onClose event 163 | sock.close(); 164 | } 165 | clearInterval(this.pingTimer); 166 | this.pingTimer = null; 167 | } 168 | 169 | IMService.prototype.resume = function() { 170 | if (!this.suspended) { 171 | return; 172 | } 173 | console.log("resume im service"); 174 | this.suspended = false; 175 | 176 | this.connect(); 177 | this.pingTimer = setInterval(this.ping, IMService.HEARTBEAT*1000); 178 | } 179 | 180 | IMService.prototype.start = function () { 181 | if (!this.stopped) { 182 | console.log("im service already be started"); 183 | return; 184 | } 185 | console.log("start im service"); 186 | this.stopped = false; 187 | this.resume(); 188 | }; 189 | 190 | IMService.prototype.stop = function () { 191 | if (this.stopped) { 192 | console.log("im service already be stopped"); 193 | return; 194 | } 195 | console.log("stop im service"); 196 | this.stopped = true; 197 | this.suspend(); 198 | }; 199 | 200 | IMService.prototype.callStateObserver = function () { 201 | if (this.observer && "onConnectState" in this.observer) { 202 | this.observer.onConnectState(this.connectState) 203 | } 204 | }; 205 | 206 | IMService.prototype.connect = function () { 207 | if (this.stopped || this.suspended) { 208 | console.log("im service stopped||suspended"); 209 | return; 210 | } 211 | if (this.socket != null) { 212 | console.log("socket is't null") 213 | return; 214 | } 215 | 216 | this.connectState = IMService.STATE_CONNECTING; 217 | this.callStateObserver(); 218 | 219 | var protocol; 220 | if (this.protocol) { 221 | protocol = this.protocol; 222 | } else { 223 | protocol = ('https:' === location.protocol) ? "wss://" : "ws://"; 224 | } 225 | var url = protocol + this.host + ":" + this.port.toString() + "/ws" ; 226 | console.log("connect protocol:", protocol, " host:", this.host, " port:", this.port); 227 | 228 | var BrowserWebSocket = global.WebSocket || global.MozWebSocket; 229 | this.socket = new BrowserWebSocket(url); 230 | this.socket.binaryType = 'arraybuffer'; 231 | 232 | var self = this; 233 | this.socket.onopen = function(evt) { 234 | self.onOpen(); 235 | } 236 | 237 | this.socket.onerror = function(event) { 238 | self.onError(event); 239 | }; 240 | }; 241 | 242 | IMService.prototype.ping = function () { 243 | if (this.connectState != IMService.STATE_CONNECTED) { 244 | return false; 245 | } 246 | console.log("ping..."); 247 | this.sendPing() 248 | if (this.pingTimestamp == 0) { 249 | this.pingTimestamp = Math.floor(new Date().getTime()/1000); 250 | } 251 | 252 | var self = this; 253 | setTimeout(function() { 254 | var now = Math.floor(new Date().getTime()/1000); 255 | if (self.pingTimestamp > 0 && now - self.pingTimestamp >= 3) { 256 | console.log("ping timeout"); 257 | if (self.connectState == IMService.STATE_CONNECTED) { 258 | //trigger close event 259 | self.socket.close(); 260 | self.socket = null; 261 | } 262 | } 263 | }, 3100); 264 | } 265 | 266 | IMService.prototype.onOpen = function () { 267 | console.log("socket opened"); 268 | var self = this; 269 | this.socket.onmessage = function(message) { 270 | self.onMessage(message.data) 271 | }; 272 | this.socket.onclose = function(e) { 273 | console.log("socket disconnect:", e); 274 | self.onClose(); 275 | }; 276 | 277 | this.seq = 0; 278 | this.connectState = IMService.STATE_CONNECTED; 279 | this.metaMessage = undefined; 280 | 281 | this.sendAuth(); 282 | if (this.roomID > 0) { 283 | this.sendEnterRoom(this.roomID); 284 | } 285 | this.sendSync(this.syncKey); 286 | this.isSyncing = true; 287 | var now = new Date().getTime() / 1000; 288 | this.syncTimestamp = now; 289 | this.pendingSyncKey = 0; 290 | 291 | for (var groupID in this.groupSyncKeys) { 292 | var s = this.groupSyncKeys[groupID]; 293 | this.sendGroupSync(groupID, s); 294 | } 295 | 296 | this.callStateObserver(); 297 | }; 298 | 299 | IMService.prototype.handleACK = function(msg) { 300 | var ack = msg.ack; 301 | if (ack in this.messages) { 302 | var m = this.messages[ack]; 303 | delete(m.__sendTimestamp__); 304 | if (this.peerMessageObserver && "handlePeerMessageACK" in this.peerMessageObserver){ 305 | this.peerMessageObserver.handlePeerMessageACK(m); 306 | } 307 | delete this.messages[ack]; 308 | } 309 | if (ack in this.customerMessages) { 310 | var m = this.customerMessages[ack]; 311 | delete(m.__sendTimestamp__); 312 | if (this.customerMessageObserver && "handleCustomerMessageACK" in this.customerMessageObserver){ 313 | this.customerMessageObserver.handleCustomerMessageACK(m); 314 | } 315 | delete this.customerMessages[ack]; 316 | } 317 | 318 | var groupMessage; 319 | for (ack in this.groupMessages) { 320 | var m = this.groupMessages[ack]; 321 | delete(m.__sendTimestamp__); 322 | if (this.groupMessageObserver && "handleGroupMessageACK" in this.groupMessageObserver){ 323 | this.groupMessageObserver.handleGroupMessageACK(m); 324 | } 325 | groupMessage = m; 326 | delete this.groupMessages[ack]; 327 | } 328 | 329 | 330 | var metaMessage = this.metaMessage; 331 | var metadata; 332 | this.metaMessage = undefined; 333 | 334 | if (metaMessage && metaMessage.seq + 1 == msg.seq) { 335 | metadata = metaMessage; 336 | 337 | if (metadata.prevSyncKey == 0 || metadata.syncKey == 0) { 338 | return; 339 | } 340 | 341 | var newSyncKey = metadata.syncKey; 342 | 343 | if (msg.flag & IMService.MESSAGE_FLAG_SUPER_GROUP) { 344 | if (!groupMessage) { 345 | return; 346 | } 347 | var groupID = groupMessage.receiver; 348 | var groupSyncKey = 0; 349 | if (groupID in this.groupSyncKeys) { 350 | groupSyncKey = this.groupSyncKeys[groupID]; 351 | } 352 | 353 | if (metadata.prevSyncKey == groupSyncKey && newSyncKey != groupSyncKey) { 354 | this.groupSyncKeys[groupID] = newSyncKey; 355 | this.sendGroupSyncKey(groupID, newSyncKey); 356 | if (this.observer && 357 | "saveSuperGroupSyncKey" in this.observer) { 358 | this.observer.saveSuperGroupSyncKey(groupID, newSyncKey); 359 | } 360 | } 361 | } else { 362 | if (this.syncKey == metadata.prevSyncKey && newSyncKey != this.syncKey) { 363 | this.syncKey = newSyncKey; 364 | this.sendSyncKey(this.syncKey); 365 | 366 | if (this.observer && 367 | "saveSyncKey" in this.observer){ 368 | this.observer.saveSyncKey(this.syncKey); 369 | } 370 | } 371 | } 372 | } 373 | }; 374 | 375 | IMService.prototype.handleMessage = function(msg) { 376 | var seq = msg.seq; 377 | var cmd = msg.cmd; 378 | 379 | console.log("handle message:", msg); 380 | //处理服务器推到客户端的消息 381 | if (msg.flag & IMService.MESSAGE_FLAG_PUSH) { 382 | var metaMessage = this.metaMessage; 383 | this.metaMessage = undefined; 384 | var metadata; 385 | if (metaMessage && metaMessage.seq + 1 == msg.seq) { 386 | metadata = metaMessage; 387 | } else { 388 | return; 389 | } 390 | 391 | if (metadata.prevSyncKey == 0 || metadata.syncKey == 0) { 392 | return; 393 | } 394 | 395 | //校验metadata中的synckey是否连续 396 | if (msg.flag & IMService.MESSAGE_FLAG_SUPER_GROUP) { 397 | var groupID; 398 | if (cmd == IMService.MSG_GROUP_IM) { 399 | groupID = msg.receiver; 400 | } else { 401 | return; 402 | } 403 | 404 | var groupSyncKey = 0; 405 | if (groupID in this.groupSyncKeys) { 406 | groupSyncKey = this.groupSyncKeys[groupID]; 407 | } 408 | 409 | if (metadata.prevSyncKey != groupSyncKey) { 410 | console.log("sync key is not sequence:", metadata.prevSyncKey, "----", groupSyncKey); 411 | return; 412 | } 413 | } else { 414 | if (metadata.prevSyncKey != this.syncKey) { 415 | console.log("sync key is not sequence:", metadata.prevSyncKey, "----", this.syncKey); 416 | return; 417 | } 418 | } 419 | } 420 | 421 | if (cmd == IMService.MSG_IM) { 422 | console.log("im message sender:" + msg.sender +" receiver:" + msg.receiver + "content:" + msg.content); 423 | 424 | if (this.peerMessageObserver) { 425 | this.peerMessageObserver.handlePeerMessage(msg); 426 | } 427 | 428 | this.sendACK(seq); 429 | } else if (cmd == IMService.MSG_GROUP_IM) { 430 | console.log("im message sender:" + msg.sender +" receiver:" + msg.receiver + "content:" + msg.content); 431 | 432 | if (this.groupMessageObserver) { 433 | this.groupMessageObserver.handleGroupMessage(msg); 434 | } 435 | 436 | this.sendACK(seq); 437 | } else if (cmd == IMService.MSG_GROUP_NOTIFICATION) { 438 | if (this.groupMessageObserver && "handleGroupNotification" in this.groupMessageObserver) { 439 | this.groupMessageObserver.handleGroupNotification(msg.content); 440 | } 441 | this.sendACK(seq); 442 | } else if (cmd == IMService.MSG_CUSTOMER) { 443 | console.log("customer message sender appid:" + msg.senderAppID + 444 | " sender id:" + msg.sender + " receiver appid:" + msg.receiverAppID + " receiver id:" + 445 | msg.receiver + "content:" + msg.content); 446 | 447 | if (this.customerMessageObserver && "handleCustomerMessage" in this.customerMessageObserver) { 448 | this.customerMessageObserver.handleCustomerMessage(msg); 449 | } 450 | 451 | this.sendACK(seq); 452 | } else if (cmd == IMService.MSG_RT) { 453 | console.log("rt message sender:" + msg.sender +" receiver:" + msg.receiver + "content:" + msg.content); 454 | 455 | if (this.rtMessageObserver) { 456 | this.rtMessageObserver.handleRTMessage(msg); 457 | } 458 | } else if (cmd == IMService.MSG_ROOM_IM) { 459 | 460 | console.log("room message sender:" + msg.sender +" receiver:" + msg.receiver + "content:" + msg.content); 461 | 462 | if (this.roomMessageObserver) { 463 | this.roomMessageObserver.handleRoomMessage(msg); 464 | } 465 | } else if (cmd == IMService.MSG_AUTH_STATUS) { 466 | var status = msg.status; 467 | console.log("auth status:" + status); 468 | if (status != 0) { 469 | this.connectState = IMService.STATE_AUTHENTICATION_FAIL; 470 | this.callStateObserver(); 471 | } else { 472 | this.connectFailCount = 0; 473 | } 474 | } else if (cmd == IMService.MSG_ACK) { 475 | this.handleACK(msg); 476 | } else if (cmd == IMService.MSG_SYNC_NOTIFY) { 477 | var newSyncKey = msg.syncKey 478 | console.log("sync notify:" + newSyncKey); 479 | 480 | var now = new Date().getTime() / 1000; 481 | var isSyncing = this.isSyncing && (now - this.syncTimestamp < 4); 482 | if (!isSyncing && this.syncKey < newSyncKey) { 483 | this.sendSync(this.syncKey); 484 | this.isSyncing = true; 485 | this.syncTimestamp = now; 486 | this.pendingSyncKey = 0; 487 | } else if (newSyncKey > this.pendingSyncKey) { 488 | this.pendingSyncKey = newSyncKey; 489 | } 490 | } else if (cmd == IMService.MSG_SYNC_BEGIN) { 491 | var newSyncKey = msg.syncKey; 492 | console.log("sync begin:" + newSyncKey); 493 | 494 | } else if (cmd == IMService.MSG_SYNC_END) { 495 | var newSyncKey = msg.syncKey; 496 | 497 | console.log("sync end:" + newSyncKey); 498 | if (newSyncKey != this.syncKey) { 499 | this.syncKey = newSyncKey; 500 | this.sendSyncKey(this.syncKey); 501 | 502 | if (this.observer && 503 | "saveSyncKey" in this.observer){ 504 | this.observer.saveSyncKey(this.syncKey); 505 | } 506 | } 507 | 508 | if (this.observer && 509 | "handleSyncEnd" in this.observer) { 510 | this.observer.handleSyncEnd(this.syncKey); 511 | } 512 | 513 | var now = new Date().getTime() / 1000; 514 | this.isSyncing = false; 515 | if (this.pendingSyncKey > this.syncKey) { 516 | this.sendSync(this.syncKey); 517 | this.isSyncing = true; 518 | this.syncTimestamp = now; 519 | this.pendingSyncKey = 0; 520 | } 521 | } else if (cmd == IMService.MSG_SYNC_GROUP_NOTIFY) { 522 | var groupID = msg.groupID; 523 | var newSyncKey = msg.syncKey; 524 | console.log("sync group notify:" + groupID + 525 | " sync key: " + newSyncKey); 526 | 527 | var groupSyncKey = 0; 528 | if (groupID in this.groupSyncKeys) { 529 | groupSyncKey = this.groupSyncKeys[groupID]; 530 | } 531 | if (newSyncKey > groupSyncKey) { 532 | this.sendGroupSync(groupID, this.groupSyncKeys[groupID]); 533 | } 534 | } else if (cmd == IMService.MSG_SYNC_GROUP_BEGIN) { 535 | var groupID = msg.groupID; 536 | var newSyncKey = msg.syncKey; 537 | console.log("sync group begin:" + groupID + 538 | " sync key: " + newSyncKey); 539 | 540 | } else if (cmd == IMService.MSG_SYNC_GROUP_END) { 541 | var groupID = msg.groupID; 542 | var newSyncKey = msg.syncKey; 543 | console.log("sync group end:" + groupID + 544 | " sync key: " + newSyncKey); 545 | 546 | var groupSyncKey = 0; 547 | if (groupID in this.groupSyncKeys) { 548 | groupSyncKey = this.groupSyncKeys[groupID]; 549 | } 550 | if (newSyncKey != groupSyncKey) { 551 | this.groupSyncKeys[groupID] = newSyncKey; 552 | this.sendGroupSyncKey(groupID, newSyncKey); 553 | if (this.observer && 554 | "saveSuperGroupSyncKey" in this.observer) { 555 | this.observer.saveSuperGroupSyncKey(groupID, newSyncKey); 556 | } 557 | } 558 | } else if (cmd == IMService.MSG_SYSTEM) { 559 | var content = msg.content; 560 | if (this.systemMessageObserver) { 561 | this.systemMessageObserver.handleSystemMessage(content); 562 | } 563 | } else if (cmd == IMService.MSG_NOTIFICATION) { 564 | var content = msg.content; 565 | if (this.observer && 566 | "handleNotification" in this.observer) { 567 | this.observer.handleNotification(content); 568 | } 569 | } else if (cmd == IMService.MSG_METADATA) { 570 | this.metaMessage = msg; 571 | } else if (cmd == IMService.MSG_PONG) { 572 | console.log("pong"); 573 | this.pingTimestamp = 0; 574 | } else { 575 | console.log("message command:" + cmd); 576 | } 577 | 578 | if (msg.flag & IMService.MESSAGE_FLAG_PUSH) { 579 | var newSyncKey = metadata.syncKey; 580 | if (msg.flag & IMService.MESSAGE_FLAG_SUPER_GROUP) { 581 | var groupID; 582 | if (cmd == IMService.MSG_GROUP_IM) { 583 | groupID = msg.receiver; 584 | } else { 585 | return; 586 | } 587 | 588 | this.groupSyncKeys[groupID] = newSyncKey; 589 | this.sendGroupSyncKey(groupID, newSyncKey); 590 | if (this.observer && 591 | "saveSuperGroupSyncKey" in this.observer) { 592 | this.observer.saveSuperGroupSyncKey(groupID, newSyncKey); 593 | } 594 | } else { 595 | this.syncKey = newSyncKey; 596 | this.sendSyncKey(this.syncKey); 597 | 598 | if (this.observer && 599 | "saveSyncKey" in this.observer){ 600 | this.observer.saveSyncKey(this.syncKey); 601 | } 602 | } 603 | } 604 | }; 605 | 606 | IMService.prototype.onMessage = function (data) { 607 | var buf; 608 | if (global.ArrayBuffer && data instanceof ArrayBuffer) { 609 | buf = new Uint8Array(data); 610 | } else { 611 | console.log("invalid data type:" + typeof data); 612 | return; 613 | } 614 | 615 | var len = ntohl(buf, 0); 616 | var seq = ntohl(buf, 4); 617 | var cmd = buf[8]; 618 | var flag = buf[10]; 619 | var isSelf = !!(flag & IMService.MESSAGE_FLAG_SELF); 620 | 621 | if (len + IMService.HEADSIZE < buf.length) { 622 | console.log("invalid data length:" + buf.length + " " + len+IMService.HEADSIZE); 623 | return; 624 | } 625 | 626 | var pos = IMService.HEADSIZE; 627 | 628 | var msg = {} 629 | msg.seq = seq; 630 | msg.cmd = cmd; 631 | msg.flag = flag; 632 | if (cmd == IMService.MSG_IM) { 633 | msg.isSelf = isSelf; 634 | 635 | msg.sender = ntoh64(buf, pos); 636 | pos += 8; 637 | 638 | msg.receiver = ntoh64(buf, pos); 639 | pos += 8; 640 | 641 | msg.timestamp = ntohl(buf, pos); 642 | pos += 4; 643 | 644 | //msgid 645 | pos += 4; 646 | 647 | msg.content = utf8.decodeUTF8(buf.subarray(IMService.HEADSIZE + 24, IMService.HEADSIZE + len)); 648 | } else if (cmd == IMService.MSG_GROUP_IM) { 649 | msg.isSelf = isSelf; 650 | 651 | msg.sender = ntoh64(buf, pos); 652 | pos += 8; 653 | 654 | msg.receiver = ntoh64(buf, pos); 655 | pos += 8; 656 | 657 | msg.timestamp = ntohl(buf, pos); 658 | pos += 4; 659 | 660 | //msgid 661 | pos += 4; 662 | 663 | msg.content = utf8.decodeUTF8(buf.subarray(IMService.HEADSIZE + 24, IMService.HEADSIZE + len)); 664 | } else if (cmd == IMService.MSG_GROUP_NOTIFICATION) { 665 | msg.content = utf8.decodeUTF8(buf.subarray(IMService.HEADSIZE, IMService.HEADSIZE + len)); 666 | } else if (cmd == IMService.MSG_CUSTOMER) { 667 | msg.isSelf = isSelf; 668 | 669 | msg.senderAppID = ntoh64(buf, pos); 670 | pos += 8; 671 | 672 | msg.sender = ntoh64(buf, pos); 673 | pos += 8; 674 | 675 | msg.receiverAppID = ntoh64(buf, pos); 676 | pos += 8; 677 | 678 | msg.receiver = ntoh64(buf, pos); 679 | pos += 8; 680 | 681 | msg.timestamp = ntohl(buf, pos); 682 | pos += 4; 683 | 684 | msg.content = utf8.decodeUTF8(buf.slice(IMService.HEADSIZE + 36, IMService.HEADSIZE + len)); 685 | 686 | } else if (cmd == IMService.MSG_RT) { 687 | msg.sender = ntoh64(buf, pos); 688 | pos += 8; 689 | 690 | msg.receiver = ntoh64(buf, pos); 691 | pos += 8; 692 | 693 | msg.content = utf8.decodeUTF8(buf.subarray(IMService.HEADSIZE + 16, IMService.HEADSIZE + len)); 694 | } else if (cmd == IMService.MSG_ROOM_IM) { 695 | msg.sender = ntoh64(buf, pos); 696 | pos += 8; 697 | 698 | msg.receiver = ntoh64(buf, pos); 699 | pos += 8; 700 | 701 | msg.content = utf8.decodeUTF8(buf.subarray(IMService.HEADSIZE + 16, IMService.HEADSIZE + len)); 702 | } else if (cmd == IMService.MSG_AUTH_STATUS) { 703 | msg.status = ntohl(buf, pos); 704 | } else if (cmd == IMService.MSG_ACK) { 705 | msg.ack = ntohl(buf, pos); 706 | } else if (cmd == IMService.MSG_SYNC_NOTIFY) { 707 | msg.syncKey = ntoh64(buf, pos); 708 | pos += 8; 709 | } else if (cmd == IMService.MSG_SYNC_BEGIN) { 710 | msg.syncKey = ntoh64(buf, pos); 711 | pos += 8; 712 | } else if (cmd == IMService.MSG_SYNC_END) { 713 | msg.syncKey = ntoh64(buf, pos); 714 | pos += 8; 715 | } else if (cmd == IMService.MSG_SYNC_GROUP_NOTIFY) { 716 | msg.groupID = ntoh64(buf, pos) 717 | pos += 8; 718 | msg.syncKey = ntoh64(buf, pos); 719 | pos += 8; 720 | } else if (cmd == IMService.MSG_SYNC_GROUP_BEGIN) { 721 | msg.groupID = ntoh64(buf, pos); 722 | pos += 8; 723 | msg.syncKey = ntoh64(buf, pos); 724 | pos += 8; 725 | } else if (cmd == IMService.MSG_SYNC_GROUP_END) { 726 | msg.groupID = ntoh64(buf, pos); 727 | pos += 8; 728 | msg.syncKey = ntoh64(buf, pos); 729 | pos += 8; 730 | } else if (cmd == IMService.MSG_SYSTEM) { 731 | msg.content = utf8.decodeUTF8(buf.subarray(IMService.HEADSIZE, IMService.HEADSIZE + len)); 732 | } else if (cmd == IMService.MSG_NOTIFICATION) { 733 | msg.content = utf8.decodeUTF8(buf.subarray(IMService.HEADSIZE, IMService.HEADSIZE + len)); 734 | } else if (cmd == IMService.MSG_METADATA) { 735 | msg.syncKey = ntoh64(buf, pos) 736 | pos += 8; 737 | msg.prevSyncKey = ntoh64(buf, pos); 738 | pos += 8; 739 | } else { 740 | console.log("message command:" + cmd); 741 | } 742 | 743 | this.handleMessage(msg); 744 | }; 745 | 746 | IMService.prototype.onError = function (err) { 747 | console.log("socket err:" + err); 748 | this.socket.close(); 749 | this.socket = null; 750 | this.connectFailCount++; 751 | this.connectState = IMService.STATE_CONNECTFAIL; 752 | this.callStateObserver(); 753 | 754 | var self = this; 755 | var f = function() { 756 | self.connect() 757 | }; 758 | 759 | var timeout = this.connectFailCount*1000; 760 | if (this.connetFailCount > 60) { 761 | timeout = 60*1000; 762 | } 763 | setTimeout(f, timeout); 764 | }; 765 | 766 | IMService.prototype.onClose = function() { 767 | console.log("on socket close"); 768 | this.socket = null; 769 | this.connectState = IMService.STATE_UNCONNECTED; 770 | this.callStateObserver(); 771 | 772 | if (this.metaMessage) { 773 | console.log("socket closed, meta message:", this.metaMessage); 774 | this.metaMessage = undefined; 775 | } 776 | 777 | for (let seq in this.messages) { 778 | let msg = this.messages[seq]; 779 | delete(msg.__sendTimestamp__); 780 | if (this.peerMessageObserver && "handlePeerMessageFailure" in this.peerMessageObserver){ 781 | this.peerMessageObserver.handlePeerMessageFailure(msg); 782 | } 783 | } 784 | this.messages = {}; 785 | 786 | for (let seq in this.groupMessages) { 787 | let msg = this.groupMessages[seq]; 788 | delete(msg.__sendTimestamp__); 789 | if (this.groupMessageObserver && "handleGroupMessageFailure" in this.groupMessageObserver){ 790 | this.groupMessageObserver.handleGroupMessageFailure(msg); 791 | } 792 | } 793 | this.groupMessages = {}; 794 | 795 | for (let seq in this.customerMessages) { 796 | let msg = this.customerMessages[seq]; 797 | delete(msg.__sendTimestamp__); 798 | if (this.customerMessageObserver && "handleCustomerMessageFailure" in this.customerMessageObserver){ 799 | this.customerMessageObserver.handleCustomerMessageFailure(msg); 800 | } 801 | } 802 | this.customerMessages = {}; 803 | 804 | var self = this; 805 | var f = function() { 806 | self.connect(); 807 | }; 808 | 809 | var timeout = this.connectFailCount*1000; 810 | if (this.connetFailCount > 60) { 811 | timeout = 60*1000; 812 | } else if (this.connectFailCount == 0) { 813 | timeout = 400 814 | } 815 | setTimeout(f, timeout); 816 | }; 817 | 818 | IMService.prototype.sendSync = function(syncKey) { 819 | var buf = new Uint8Array(8); 820 | hton64(buf, 0, syncKey); 821 | this.send(IMService.MSG_SYNC, buf); 822 | } 823 | 824 | 825 | IMService.prototype.sendSyncKey = function(syncKey) { 826 | var buf = new Uint8Array(8); 827 | hton64(buf, 0, syncKey); 828 | this.send(IMService.MSG_SYNC_KEY, buf); 829 | } 830 | 831 | IMService.prototype.sendGroupSync = function(groupID, syncKey) { 832 | var buf = new Uint8Array(16); 833 | hton64(buf, 0, groupID); 834 | hton64(buf, 8, syncKey); 835 | this.send(IMService.MSG_SYNC_GROUP, buf); 836 | } 837 | 838 | 839 | IMService.prototype.sendGroupSyncKey = function(groupID, syncKey) { 840 | var buf = new Uint8Array(16); 841 | hton64(buf, 0, groupID); 842 | hton64(buf, 8, syncKey); 843 | this.send(IMService.MSG_GROUP_SYNC_KEY, buf); 844 | } 845 | 846 | IMService.prototype.sendACK = function(ack) { 847 | var buf = new Uint8Array(4); 848 | htonl(buf, 0, ack); 849 | this.send(IMService.MSG_ACK, buf); 850 | } 851 | 852 | IMService.prototype.sendAuth = function() { 853 | var buf = new Uint8Array(1024); 854 | var pos = 0; 855 | var len = 0; 856 | 857 | buf[pos] = this.platformID; 858 | pos++; 859 | 860 | var accessToken = utf8.encodeUTF8(this.accessToken); 861 | len = accessToken.length; 862 | buf[pos] = len; 863 | pos++; 864 | buf.set(accessToken, pos); 865 | pos += len; 866 | 867 | var deviceId = utf8.encodeUTF8(this.deviceID); 868 | len = deviceId.length; 869 | buf[pos] = len; 870 | pos++; 871 | buf.set(deviceId, pos); 872 | pos += len; 873 | 874 | var body = buf.subarray(0, pos); 875 | 876 | this.send(IMService.MSG_AUTH_TOKEN, body); 877 | } 878 | 879 | IMService.prototype.sendPing = function() { 880 | var body = new Uint8Array(0); 881 | this.send(IMService.MSG_PING, body); 882 | } 883 | 884 | 885 | //typeof body == uint8array 886 | IMService.prototype.send = function (cmd, body, nonpersistent) { 887 | if (this.socket == null) { 888 | return false; 889 | } 890 | if (this.connectState != IMService.STATE_CONNECTED) { 891 | return false; 892 | } 893 | 894 | this.seq++; 895 | 896 | var buf = new Uint8Array(IMService.HEADSIZE+body.length); 897 | 898 | var pos = 0; 899 | htonl(buf, pos, body.length); 900 | pos += 4; 901 | 902 | htonl(buf, pos, this.seq); 903 | pos += 4; 904 | 905 | buf[pos] = cmd; 906 | pos++; 907 | buf[pos] = IMService.VERSION; 908 | pos++; 909 | 910 | if (nonpersistent) { 911 | buf[pos] = IMService.MESSAGE_FLAG_UNPERSISTENT; 912 | } else { 913 | buf[pos] = 0; 914 | } 915 | pos++; 916 | 917 | buf[pos] = 0; 918 | pos++; 919 | 920 | buf.set(body, pos); 921 | pos += body.length; 922 | 923 | this.socket.send(buf.buffer); 924 | return true 925 | }; 926 | 927 | 928 | 929 | IMService.prototype.addSuperGroupSyncKey = function(groupID, syncKey) { 930 | this.groupSyncKeys[groupID] = syncKey; 931 | }; 932 | 933 | IMService.prototype.removeSuperGroupSyncKey = function(groupID) { 934 | delete this.groupSyncKeys[groupID]; 935 | }; 936 | 937 | IMService.prototype.clearSuperGroupSyncKey = function() { 938 | this.groupSyncKeys = {}; 939 | }; 940 | 941 | IMService.prototype.sendPeerMessage = function (msg) { 942 | if (this.connectState != IMService.STATE_CONNECTED) { 943 | return false; 944 | } 945 | 946 | var content = utf8.encodeUTF8(msg.content) 947 | var len = content.length; 948 | var buf = new Uint8Array(24+len); 949 | var pos = 0; 950 | var ts = msg.timestamp || 0; 951 | var msgId = msg.msgLocalID || 0; 952 | 953 | hton64(buf, pos, msg.sender); 954 | pos += 8; 955 | hton64(buf, pos, msg.receiver); 956 | pos += 8; 957 | htonl(buf, pos, ts); 958 | pos += 4; 959 | htonl(buf, pos, msgId); 960 | pos += 4; 961 | 962 | buf.set(content, pos); 963 | pos += len; 964 | 965 | var r = this.send(IMService.MSG_IM, buf); 966 | if (!r) { 967 | return false; 968 | } 969 | 970 | var now = new Date().getTime() / 1000; 971 | msg.__sendTimestamp__ = now; 972 | this.messages[this.seq] = msg; 973 | 974 | var self = this; 975 | var t = IMService.ACK_TIMEOUT*1000+100; 976 | setTimeout(function() { 977 | self.checkAckTimeout(); 978 | }, t); 979 | return true; 980 | }; 981 | 982 | 983 | IMService.prototype.sendGroupMessage = function (msg) { 984 | if (this.connectState != IMService.STATE_CONNECTED) { 985 | return false; 986 | } 987 | 988 | var content = utf8.encodeUTF8(msg.content); 989 | var len = content.length; 990 | var buf = new Uint8Array(24+len); 991 | var pos = 0; 992 | var ts = msg.timestamp || 0; 993 | var msgId = msg.msgLocalID || 0; 994 | 995 | hton64(buf, pos, msg.sender); 996 | pos += 8; 997 | hton64(buf, pos, msg.receiver); 998 | pos += 8; 999 | htonl(buf, pos, ts); 1000 | pos += 4; 1001 | htonl(buf, pos, msgId); 1002 | pos += 4; 1003 | 1004 | len = buf.set(content, pos); 1005 | pos += len; 1006 | 1007 | 1008 | var r = this.send(IMService.MSG_GROUP_IM, buf); 1009 | if (!r) { 1010 | return false; 1011 | } 1012 | 1013 | var now = new Date().getTime() / 1000; 1014 | msg.__sendTimestamp__ = now; 1015 | this.groupMessages[this.seq] = msg; 1016 | 1017 | var self = this; 1018 | var t = IMService.ACK_TIMEOUT*1000+100; 1019 | setTimeout(function() { 1020 | self.checkAckTimeout(); 1021 | }, t); 1022 | 1023 | return true; 1024 | }; 1025 | 1026 | 1027 | 1028 | IMService.prototype.sendRTMessage = function (msg) { 1029 | if (this.connectState != IMService.STATE_CONNECTED) { 1030 | return false; 1031 | } 1032 | console.log("send rt message:" + msg.sender + " receiver:" + msg.receiver + "content:" + msg.content); 1033 | var content = utf8.encodeUTF8(msg.content); 1034 | var len = content.length; 1035 | var buf = new Uint8Array(16+len); 1036 | var pos = 0; 1037 | 1038 | hton64(buf, pos, msg.sender); 1039 | pos += 8; 1040 | hton64(buf, pos, msg.receiver); 1041 | pos += 8; 1042 | 1043 | len = buf.set(content, pos); 1044 | pos += len; 1045 | 1046 | var r = this.send(IMService.MSG_RT, buf); 1047 | if (!r) { 1048 | return false; 1049 | } 1050 | return true; 1051 | }; 1052 | 1053 | IMService.prototype.sendEnterRoom = function(roomID) { 1054 | var buf = new Uint8Array(8); 1055 | hton64(buf, 0, roomID); 1056 | this.send(IMService.MSG_ENTER_ROOM, buf); 1057 | } 1058 | 1059 | IMService.prototype.sendLeaveRoom = function(roomID) { 1060 | var buf = new Uint8Array(8); 1061 | hton64(buf, 0, roomID); 1062 | this.send(IMService.MSG_LEAVE_ROOM, buf); 1063 | } 1064 | 1065 | IMService.prototype.enterRoom = function(roomID) { 1066 | if (roomID == 0) { 1067 | return; 1068 | } 1069 | this.roomID = roomID; 1070 | this.sendEnterRoom(this.roomID); 1071 | } 1072 | 1073 | IMService.prototype.leaveRoom = function(roomID) { 1074 | if (roomID != self.roomID || roomID == 0) { 1075 | return; 1076 | } 1077 | 1078 | this.sendLeaveRoom(this.roomID); 1079 | this.roomID = 0; 1080 | } 1081 | 1082 | IMService.prototype.sendRoomMessage = function (msg) { 1083 | if (this.connectState != IMService.STATE_CONNECTED) { 1084 | return false; 1085 | } 1086 | var content = utf8.encodeUTF8(msg.content); 1087 | var len = content.length; 1088 | var buf = new Uint8Array(16+len); 1089 | var pos = 0; 1090 | 1091 | hton64(buf, pos, msg.sender); 1092 | pos += 8; 1093 | hton64(buf, pos, msg.receiver); 1094 | pos += 8; 1095 | 1096 | len = buf.set(content, pos); 1097 | pos += len; 1098 | 1099 | var r = this.send(IMService.MSG_ROOM_IM, buf); 1100 | if (!r) { 1101 | return false; 1102 | } 1103 | return true; 1104 | } 1105 | 1106 | IMService.prototype.writeCustomerMessage = function(msg) { 1107 | var content = utf8.encodeUTF8(msg.content); 1108 | var len = content.length; 1109 | var buf = new Uint8Array(36+len); 1110 | var pos = 0; 1111 | 1112 | hton64(buf, pos, msg.senderAppID); 1113 | pos += 8; 1114 | hton64(buf, pos, msg.sender); 1115 | pos += 8; 1116 | hton64(buf, pos, msg.receiverAppID); 1117 | pos += 8; 1118 | hton64(buf, pos, msg.receiver); 1119 | pos += 8; 1120 | var ts = msg.timestamp || 0; 1121 | htonl(buf, pos, ts); 1122 | pos += 4; 1123 | buf.set(content, pos); 1124 | pos += len; 1125 | return buf 1126 | }; 1127 | 1128 | 1129 | IMService.prototype.sendCustomerMessage = function (msg) { 1130 | if (this.connectState != IMService.STATE_CONNECTED) { 1131 | return false; 1132 | } 1133 | 1134 | var buf = this.writeCustomerMessage(msg); 1135 | var r = this.send(IMService.MSG_CUSTOMER, buf); 1136 | if (!r) { 1137 | return false; 1138 | } 1139 | 1140 | var now = new Date().getTime() / 1000; 1141 | msg.__sendTimestamp__ = now; 1142 | this.customerMessages[this.seq] = msg; 1143 | 1144 | var self = this; 1145 | var t = IMService.ACK_TIMEOUT*1000+100; 1146 | setTimeout(function() { 1147 | self.checkAckTimeout(); 1148 | }, t); 1149 | 1150 | return true; 1151 | }; 1152 | 1153 | //检查是否有消息ack超时 1154 | IMService.prototype.checkAckTimeout = function() { 1155 | var now = new Date().getTime() / 1000; 1156 | var isTimeout = false; 1157 | var ack; 1158 | for (ack in this.messages) { 1159 | var msg = this.messages[ack]; 1160 | if (now - msg.__sendTimestamp__ >= IMService.ACK_TIMEOUT) { 1161 | isTimeout = true; 1162 | } 1163 | } 1164 | for (ack in this.groupMessages) { 1165 | var msg = this.groupMessages[ack]; 1166 | if (now - msg.__sendTimestamp__ >= IMService.ACK_TIMEOUT) { 1167 | isTimeout = true; 1168 | } 1169 | } 1170 | for (ack in this.customerMessages) { 1171 | var msg = this.customerMessages[ack]; 1172 | if (now - msg.__sendTimestamp__ >= IMService.ACK_TIMEOUT) { 1173 | isTimeout = true; 1174 | } 1175 | } 1176 | 1177 | if (isTimeout) { 1178 | console.log("ack timeout, close socket"); 1179 | this.onClose(); 1180 | } 1181 | }; 1182 | 1183 | 1184 | IMService.guid = function () { 1185 | function s4() { 1186 | return Math.floor((1 + Math.random()) * 0x10000) 1187 | .toString(16) 1188 | .substring(1); 1189 | } 1190 | return s4() + s4() + '-' + s4() + '-' + s4() + '-' + 1191 | s4() + '-' + s4() + s4() + s4(); 1192 | } 1193 | 1194 | module.exports = IMService; 1195 | -------------------------------------------------------------------------------- /lib/utf8.js: -------------------------------------------------------------------------------- 1 | // This is free and unencumbered software released into the public domain. 2 | 3 | // Marshals a string to an Uint8Array. 4 | exports.encodeUTF8 = function(s) { 5 | var i = 0, bytes = new Uint8Array(s.length * 4); 6 | for (var ci = 0; ci != s.length; ci++) { 7 | var c = s.charCodeAt(ci); 8 | if (c < 128) { 9 | bytes[i++] = c; 10 | continue; 11 | } 12 | if (c < 2048) { 13 | bytes[i++] = c >> 6 | 192; 14 | } else { 15 | if (c > 0xd7ff && c < 0xdc00) { 16 | if (++ci >= s.length) 17 | throw new Error('UTF-8 encode: incomplete surrogate pair'); 18 | var c2 = s.charCodeAt(ci); 19 | if (c2 < 0xdc00 || c2 > 0xdfff) 20 | throw new Error('UTF-8 encode: second surrogate character 0x' + c2.toString(16) + ' at index ' + ci + ' out of range'); 21 | c = 0x10000 + ((c & 0x03ff) << 10) + (c2 & 0x03ff); 22 | bytes[i++] = c >> 18 | 240; 23 | bytes[i++] = c >> 12 & 63 | 128; 24 | } else bytes[i++] = c >> 12 | 224; 25 | bytes[i++] = c >> 6 & 63 | 128; 26 | } 27 | bytes[i++] = c & 63 | 128; 28 | } 29 | return bytes.subarray(0, i); 30 | } 31 | 32 | // Unmarshals a string from an Uint8Array. 33 | exports.decodeUTF8 = function(bytes) { 34 | var i = 0, s = ''; 35 | while (i < bytes.length) { 36 | var c = bytes[i++]; 37 | if (c > 127) { 38 | if (c > 191 && c < 224) { 39 | if (i >= bytes.length) 40 | throw new Error('UTF-8 decode: incomplete 2-byte sequence'); 41 | c = (c & 31) << 6 | bytes[i++] & 63; 42 | } else if (c > 223 && c < 240) { 43 | if (i + 1 >= bytes.length) 44 | throw new Error('UTF-8 decode: incomplete 3-byte sequence'); 45 | c = (c & 15) << 12 | (bytes[i++] & 63) << 6 | bytes[i++] & 63; 46 | } else if (c > 239 && c < 248) { 47 | if (i + 2 >= bytes.length) 48 | throw new Error('UTF-8 decode: incomplete 4-byte sequence'); 49 | c = (c & 7) << 18 | (bytes[i++] & 63) << 12 | (bytes[i++] & 63) << 6 | bytes[i++] & 63; 50 | } else throw new Error('UTF-8 decode: unknown multibyte start 0x' + c.toString(16) + ' at index ' + (i - 1)); 51 | } 52 | if (c <= 0xffff) s += String.fromCharCode(c); 53 | else if (c <= 0x10ffff) { 54 | c -= 0x10000; 55 | s += String.fromCharCode(c >> 10 | 0xd800) 56 | s += String.fromCharCode(c & 0x3FF | 0xdc00) 57 | } else throw new Error('UTF-8 decode: code point 0x' + c.toString(16) + ' exceeds UTF-16 reach'); 58 | } 59 | return s; 60 | } 61 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "gobelieve", 3 | "version": "1.0.0", 4 | "description": "gobelieve sdk", 5 | "main": "lib/im.js", 6 | "scripts": { 7 | "build": "browserify -s IMService . > demo/static/js/lib/im.js", 8 | "watch": "watchify --debug -s IMService . -o demo/static/js/lib/im.js" 9 | }, 10 | "dependencies": { 11 | "big-integer": "^1.6.42", 12 | "browserify": "^17.0.0", 13 | "watchify": "^4.0.0" 14 | } 15 | } 16 | --------------------------------------------------------------------------------