Full Code of cookeem/CookIM for AI

master 4898cb9ff875 cached
202 files
2.6 MB
692.2k tokens
609 symbols
1 requests
Download .txt
Showing preview only (2,780K chars total). Download the full file or copy to clipboard to get everything.
Repository: cookeem/CookIM
Branch: master
Commit: 4898cb9ff875
Files: 202
Total size: 2.6 MB

Directory structure:
gitextract_zxrdz1n_/

├── .github/
│   └── workflows/
│       └── scala.yml
├── .gitignore
├── Dockerfile
├── Dockerfile_k8s
├── README.md
├── README_CN.md
├── README_JENKINS.md
├── VERSION
├── build.sbt
├── conf/
│   └── application.conf
├── docker-compose.yml
├── docs/
│   └── doc.md
├── kubernetes/
│   └── cookim.yaml
├── pom.xml
├── project/
│   ├── assembly.sbt
│   ├── build.properties
│   └── plugins.sbt
├── src/
│   └── main/
│       ├── resources/
│       │   └── mykeystore.jks
│       └── scala/
│           └── com/
│               └── cookeem/
│                   └── chat/
│                       ├── CookIM.scala
│                       ├── common/
│                       │   └── CommonUtils.scala
│                       ├── demo/
│                       │   └── TestObj.scala
│                       ├── event/
│                       │   └── ChatEventPackage.scala
│                       ├── jwt/
│                       │   └── JwtOps.scala
│                       ├── mongo/
│                       │   ├── MongoLogic.scala
│                       │   ├── MongoOps.scala
│                       │   └── package.scala
│                       ├── restful/
│                       │   ├── Controller.scala
│                       │   ├── Route.scala
│                       │   └── RouteOps.scala
│                       └── websocket/
│                           ├── ChatSession.scala
│                           ├── ChatSessionActor.scala
│                           ├── NotificationActor.scala
│                           ├── PushSession.scala
│                           ├── PushSessionActor.scala
│                           └── TraitPubSubActor.scala
└── www/
    ├── changeinfo.html
    ├── changepwd.html
    ├── chatlist.html
    ├── chatsession.html
    ├── css/
    │   └── index.css
    ├── error.html
    ├── fonts/
    │   ├── Material_Icon/
    │   │   ├── MaterialIcons-Regular.ijmap
    │   │   ├── codepoints
    │   │   └── material-icons.css
    │   └── fonts.css
    ├── friends.html
    ├── images/
    │   └── cookim.ai
    ├── index.html
    ├── js/
    │   ├── changeinfo.js
    │   ├── changepwd.js
    │   ├── chatlist.js
    │   ├── chatsession.js
    │   ├── error.js
    │   ├── friends.js
    │   ├── index.js
    │   ├── login.js
    │   ├── logout.js
    │   ├── notifications.js
    │   └── register.js
    ├── jslib/
    │   ├── MediaStreamRecorder/
    │   │   └── MediaStreamRecorder.js
    │   ├── angular/
    │   │   ├── LICENSE.md
    │   │   ├── README.md
    │   │   ├── angular-csp.css
    │   │   ├── angular.js
    │   │   ├── angular.min.js.gzip
    │   │   ├── bower.json
    │   │   ├── index.js
    │   │   └── package.json
    │   ├── angular-animate/
    │   │   ├── LICENSE.md
    │   │   ├── README.md
    │   │   ├── angular-animate.js
    │   │   ├── bower.json
    │   │   ├── index.js
    │   │   └── package.json
    │   ├── angular-cookies/
    │   │   ├── LICENSE.md
    │   │   ├── README.md
    │   │   ├── angular-cookies.js
    │   │   ├── bower.json
    │   │   ├── index.js
    │   │   └── package.json
    │   ├── angular-route/
    │   │   ├── LICENSE.md
    │   │   ├── README.md
    │   │   ├── angular-route.js
    │   │   ├── bower.json
    │   │   ├── index.js
    │   │   └── package.json
    │   └── jquery/
    │       ├── AUTHORS.txt
    │       ├── LICENSE.txt
    │       ├── README.md
    │       ├── bower.json
    │       ├── dist/
    │       │   ├── core.js
    │       │   ├── jquery.js
    │       │   └── jquery.slim.js
    │       ├── external/
    │       │   └── sizzle/
    │       │       ├── LICENSE.txt
    │       │       └── dist/
    │       │           └── sizzle.js
    │       ├── package.json
    │       └── src/
    │           ├── .eslintrc
    │           ├── ajax/
    │           │   ├── jsonp.js
    │           │   ├── load.js
    │           │   ├── parseXML.js
    │           │   ├── script.js
    │           │   ├── var/
    │           │   │   ├── location.js
    │           │   │   ├── nonce.js
    │           │   │   └── rquery.js
    │           │   └── xhr.js
    │           ├── ajax.js
    │           ├── attributes/
    │           │   ├── attr.js
    │           │   ├── classes.js
    │           │   ├── prop.js
    │           │   ├── support.js
    │           │   └── val.js
    │           ├── attributes.js
    │           ├── callbacks.js
    │           ├── core/
    │           │   ├── DOMEval.js
    │           │   ├── access.js
    │           │   ├── init.js
    │           │   ├── parseHTML.js
    │           │   ├── ready-no-deferred.js
    │           │   ├── ready.js
    │           │   ├── readyException.js
    │           │   ├── support.js
    │           │   └── var/
    │           │       └── rsingleTag.js
    │           ├── core.js
    │           ├── css/
    │           │   ├── addGetHookIf.js
    │           │   ├── adjustCSS.js
    │           │   ├── curCSS.js
    │           │   ├── hiddenVisibleSelectors.js
    │           │   ├── showHide.js
    │           │   ├── support.js
    │           │   └── var/
    │           │       ├── cssExpand.js
    │           │       ├── getStyles.js
    │           │       ├── isHiddenWithinTree.js
    │           │       ├── rmargin.js
    │           │       ├── rnumnonpx.js
    │           │       └── swap.js
    │           ├── css.js
    │           ├── data/
    │           │   ├── Data.js
    │           │   └── var/
    │           │       ├── acceptData.js
    │           │       ├── dataPriv.js
    │           │       └── dataUser.js
    │           ├── data.js
    │           ├── deferred/
    │           │   └── exceptionHook.js
    │           ├── deferred.js
    │           ├── deprecated.js
    │           ├── dimensions.js
    │           ├── effects/
    │           │   ├── Tween.js
    │           │   └── animatedSelector.js
    │           ├── effects.js
    │           ├── event/
    │           │   ├── ajax.js
    │           │   ├── alias.js
    │           │   ├── focusin.js
    │           │   ├── support.js
    │           │   └── trigger.js
    │           ├── event.js
    │           ├── exports/
    │           │   ├── amd.js
    │           │   └── global.js
    │           ├── jquery.js
    │           ├── manipulation/
    │           │   ├── _evalUrl.js
    │           │   ├── buildFragment.js
    │           │   ├── getAll.js
    │           │   ├── setGlobalEval.js
    │           │   ├── support.js
    │           │   ├── var/
    │           │   │   ├── rcheckableType.js
    │           │   │   ├── rscriptType.js
    │           │   │   └── rtagName.js
    │           │   └── wrapMap.js
    │           ├── manipulation.js
    │           ├── offset.js
    │           ├── queue/
    │           │   └── delay.js
    │           ├── queue.js
    │           ├── selector-native.js
    │           ├── selector-sizzle.js
    │           ├── selector.js
    │           ├── serialize.js
    │           ├── traversing/
    │           │   ├── findFilter.js
    │           │   └── var/
    │           │       ├── dir.js
    │           │       ├── rneedsContext.js
    │           │       └── siblings.js
    │           ├── traversing.js
    │           ├── var/
    │           │   ├── ObjectFunctionString.js
    │           │   ├── arr.js
    │           │   ├── class2type.js
    │           │   ├── concat.js
    │           │   ├── document.js
    │           │   ├── documentElement.js
    │           │   ├── fnToString.js
    │           │   ├── getProto.js
    │           │   ├── hasOwn.js
    │           │   ├── indexOf.js
    │           │   ├── pnum.js
    │           │   ├── push.js
    │           │   ├── rcssNum.js
    │           │   ├── rnotwhite.js
    │           │   ├── slice.js
    │           │   ├── support.js
    │           │   └── toString.js
    │           └── wrap.js
    ├── login.html
    ├── logout.html
    ├── notifications.html
    ├── register.html
    └── websocket.html

================================================
FILE CONTENTS
================================================

================================================
FILE: .github/workflows/scala.yml
================================================
name: Scala CI

on:
  push:
    branches: [ master ]
  pull_request:
    branches: [ master ]

jobs:
  build:

    runs-on: ubuntu-latest

    steps:
    - uses: actions/checkout@v2
    - name: Set up JDK 1.8
      uses: actions/setup-java@v1
      with:
        java-version: 1.8
    - name: Run tests
      run: sbt test


================================================
FILE: .gitignore
================================================
# Created by .ignore support plugin (hsz.mobi)
/www/node_modules/
.DS_Store
/.idea/
/project/project/
/project/target/
/target/
/upload/
/docker
/mongo


================================================
FILE: Dockerfile
================================================
# Notice! Don't use openjdk:latest docker image, it's too big and will build fail in docker:DinD

FROM openjdk:alpine

MAINTAINER cookeem@qq.com

RUN mkdir -p /root/cookim/

ADD target/scala-2.11/CookIM-assembly-0.2.4-SNAPSHOT.jar /root/cookim/cookim.jar
ADD conf /root/cookim/conf
ADD www /root/cookim/www

WORKDIR /root/cookim

RUN echo '#!/bin/ash' >> /root/cookim/run.sh
RUN echo 'java -classpath "/root/cookim/cookim.jar" com.cookeem.chat.CookIM -n -h $(hostname -f) -w $WEB_PORT -a $AKKA_PORT -s $SEED_NODES -m $MONGO_URI' >> /root/cookim/run.sh
RUN chmod a+x /root/cookim/run.sh

CMD [ "/root/cookim/run.sh" ]

# sbt clean assembly
# docker build -t cookeem/cookim .
# docker push cookeem/cookim

================================================
FILE: Dockerfile_k8s
================================================
# Notice! Don't use openjdk:latest docker image, it's too big and will build fail in docker:DinD

FROM k8s-registry:5000/openjdk:alpine

MAINTAINER cookeem@qq.com

RUN mkdir -p /root/cookim/

ADD cookim.jar /root/cookim/
ADD conf /root/cookim/conf
ADD www /root/cookim/www

WORKDIR /root/cookim

RUN echo '#!/bin/ash' >> /root/cookim/run.sh
RUN echo 'java -classpath "/root/cookim/cookim.jar" com.cookeem.chat.CookIM -n -h $(hostname -f) -w $WEB_PORT -a $AKKA_PORT -s $SEED_NODES' >> /root/cookim/run.sh
RUN chmod a+x /root/cookim/run.sh

CMD [ "/root/cookim/run.sh" ]

# sbt clean assembly
# docker build -t cookeem/cookim .


================================================
FILE: README.md
================================================
# CookIM - is a distributed websocket chat applications based on akka

[![Github All Releases](https://img.shields.io/github/downloads/atom/atom/total.svg)](https://github.com/cookeem/CookIM)

- Support private message and group message
- Support chat servers cluster communication
- Now we support send text message, file message and voice message. Thanks for [ft115637850](https://github.com/ft115637850) 's PR for voice message.

![CookIM logo](docs/cookim.png)

- [中文文档](README_CN.md)
- [English document](README.md)

---

- [GitHub project](https://github.com/cookeem/CookIM/)
- [OSChina project](https://git.oschina.net/cookeem/CookIM/)

---

### Category

1. [Demo](#demo)
    1. [Demo on PC](#demo-on-pc)
    1. [Demo on Mobile](#demo-on-mobile)
    1. [Demo link](#demo-link)
1. [Start multiple nodes CookIM in docker compose](#start-multiple-nodes-cookim-in-docker-compose)
    1. [Start docker compose](#start-docker-compose)
    1. [Add nodes in docker compose](#add-nodes-in-docker-compose)
    1. [Debug in docker container](#debug-in-docker-container)
    1. [Stop docker compose](#stop-docker-compose)
1. [How to run](#how-to-run)
    1. [Prerequisites](#prerequisites)
    1. [Clone source code](#clone-source-code)
    1. [Configuration and assembly](#configuration-and-assembly)
    1. [Start CookIM server](#start-cookim-server)
    1. [Open browser and access web port 8080](#open-browser-and-access-web-port-8080)
    1. [Start another CookIM server](#start-another-cookim-server)
    1. [Open browser and access web port 8081](#open-browser-and-access-web-port-8081)
1. [Architecture](#architecture)
    1. [Architecture picture](#architecture-picture)
    1. [akka stream websocket graph](#akka-stream-websocket-graph)
    1. [MongoDB tables specification](#mongodb-tables-specification)
    1. [Websocket message type](#websocket-message-type)
1. [ChangeLog](#ChangeLog)
    1. [0.1.0-SNAPSHOT](#010-snapshot)
    1. [0.2.0-SNAPSHOT](#020-snapshot)
    1. [0.2.4-SNAPSHOT](#024-snapshot)

---
[Category](#category)

###Demo

#### Demo on PC

![screen snapshot](docs/screen.png) 

#### Demo on Mobile

![screen snapshot](docs/screen2.png)


#### Demo link
[https://im.cookeem.com](https://im.cookeem.com)

---
    
### Start multiple nodes CookIM in docker compose

#### Start docker compose

Change into CookIM directory, run command below, start multiple nodes CookIM servers in docker compose mode. This way will start 3 container: mongo, cookim1 and cookim2
```sh
$ git clone https://github.com/cookeem/CookIM.git

$ cd CookIM

$ sudo docker-compose up -d
Creating mongo
Creating cookim1
Creating cookim2
```

After run docker compose, use different browser to access the URLs below to connect to cookim1 and cookim2
> http://localhost:8080
> http://localhost:8081

---

[Category](#category)

#### Add nodes in docker compose

You can add config in ```docker-compose.yml``` (in CookIM directory) to add CookIM server nodes, this example show how to add cookim3 in docker compose: 
```yaml
      cookim3:
        image: cookeem/cookim
        container_name: cookim3
        hostname: cookim3
        environment:
          HOST_NAME: cookim3
          WEB_PORT: 8080
          AKKA_PORT: 2551
          SEED_NODES: cookim1:2551
          MONGO_URI: mongodb://mongo:27017/local
        ports:
        - "8082:8080"
        depends_on:
        - mongo
        - cookim1
```
---

[Category](#category)

#### Debug in docker container

View container ```cookim1``` logs output
```sh
$ sudo docker logs -f cookim1
```

Exec into container ```cookim1``` to debug
```sh
$ sudo docker exec -ti cookim1 bash
```
---

[Category](#category)

#### Stop docker compose
```sh
$ sudo docker-compose stop
$ sudo docker-compose rm
```
---

[Category](#category)

### How to run

#### Prerequisites

* JDK 8+
* Scala 2.11+
* SBT 0.13.15
* MongoDB 2.6 - 3.4

---

[Category](#category)

#### Clone source code
```sh
git clone https://github.com/cookeem/CookIM.git

cd CookIM
```
---

[Category](#category)

#### Configuration and assembly

The configuration file locate at ```conf/application.conf```, please make sure your mongodb uri configuration.
```sh
mongodb {
  dbname = "cookim"
  uri = "mongodb://mongo:27017/local"
}
```

Assembly CookIM project to a fatjar, target jar locate at ```target/scala-2.11/CookIM-assembly-0.2.0-SNAPSHOT.jar```
```sh
sbt clean assembly
```

---

[Category](#category)

#### Start CookIM server

CookIM use MongoDB to store chat messages and users data, make sure you startup MongoDB before you startup CookIM.


There are two ways to start CookIM server: sbt and java

a. sbt debug way:
```sh
$ cd #CookIM directory#

$ sbt "run-main com.cookeem.chat.CookIM -h localhost -w 8080 -a 2551 -s localhost:2551"
```
b. pack and compile fat jar:
```sh
$ sbt assembly
```

c. java production way:
```sh
$ java -classpath "target/scala-2.11/CookIM-assembly-0.2.4-SNAPSHOT.jar" com.cookeem.chat.CookIM -h localhost -w 8080 -a 2551 -s localhost:2551
```

Command above has start a web server listen port 8080 and akka system listen port 2551

Parameters:

 -a <AKKA-PORT> -h <HOST-NAME> [-m <MONGO-URI>] [-n] -s <SEED-NODES> -w
       <WEB-PORT>
 -a,--akka-port <AKKA-PORT>     akka cluster node port
 -h,--host-name <HOST-NAME>     current web service external host name
 -m,--mongo-uri <MONGO-URI>     mongodb connection uri, example:
                                mongodb://localhost:27017/local
 -n,--nat                       is nat network or in docker
 -s,--seed-nodes <SEED-NODES>   akka cluster seed nodes, seperate with
                                comma, example:
                                localhost:2551,localhost:2552
 -w,--web-port <WEB-PORT>       web service port

---

[Category](#category)

#### Open browser and access web port 8080
> http://localhost:8080

---

[Category](#category)

#### Start another CookIM server

Open another terminal, start another CookIM server to test message communication between servers:

a. sbt debug way:
```sh
$ sbt "run-main com.cookeem.chat.CookIM -h localhost -w 8081 -a 2552 -s localhost:2551"
```

b. java production way:
```sh
$ java -classpath "target/scala-2.11/CookIM-assembly-0.2.0-SNAPSHOT.jar" com.cookeem.chat.CookIM -h localhost -w 8081 -a 2552 -s localhost:2551
```

Command above has start a web server listen port 8081 and akka system listen port 2552

---

[Category](#category)

#### Open browser and access web port 8081
> http://localhost:8081

---

[Category](#category)

### Architecture

#### Architecture picture

![Architecture picture](docs/CookIM-Flow.png)

**CookIM server make from 3 parts: **

> 1. akka http: provide web service, browser connect distributed chat servers by websocket

> 2. akka stream: akka http receive websocket message (websocket message include TextMessage and BinaryMessage), then send message to chatService by akka stream way, websocket message include JWT(Javascript web token), if JWT verify failure, chatService stream will return reject message; if JWT verify success, chatService stream will send message to ChatSessionActor

> 3. akka cluster:akka stream send websocket message to akka cluster ChatSessionActor, ChatSessionActor use DistributedPubSub to subscribe and publish message in akka cluster. When user online session, it will subscribe the session; when user send message in session, it will publish message in akka cluster, the actors who subscribe the session will receive the publish message

---

[Category](#category)

#### akka stream websocket graph

![CookIM stream](docs/CookIM-ChatStream.png)

 - When akka http receive messsage from websocket, it will send message to chatService flow, here we use akka stream graph:

> 1. Websocket message body include JWT, flowFromWS use to receive websocket message and decode JWT;

> 2. When JWT verify failure, it will broadcast to filterFailure to filter to fail message; When JWT verify success, it will broadcast to filterSuccess to filter to success message;

> 3. When akka stream created, builder.materializedValue will send message to connectedWs, connectedWs convert message receive to UserOnline message, then send to chatSinkActor finally send to ChatSessionActor; 

> 4. chatActorSink send message to chatSessionActor, when akka stream closed if will send UserOffline message to down stream;

> 5. chatSource receive message back from ChatSessionActor, then send message back to flowAcceptBack;

> 6. flowAcceptBack will let the websocket connection keepAlive;

> 7. flowReject and flowAcceptBack messages finally send to flowBackWs, flowBackWs convert messages to websocket format then send back to users;

---

[Category](#category)

#### MongoDB tables specification

 - users: users table
```
*login (login email)
nickname (nickname)
password (password SHA1)
gender (gender: unknow:0, boy:1, girl:2)
avatar (avatar abs path, example: /upload/avatar/201610/26/xxxx.JPG)
lastLogin (last login timestamp)
loginCount (login counts)
sessionsStatus (user joined sessions status)
    [{sessionid: session id, newCount: unread message count in this session}]
friends (user's friends list: [friends uid])
dateline (register timestamp)
```

 - sessions: sessions table
```
*createuid (creator uid)
*ouid (receiver uid, when session type is private available)
sessionIcon (session icon, when session type is group available)
sessionType (session type: 0:private, 1:group)
publicType (public type: 0:not public, 1:public)
sessionName (session name)
dateline (created timestamp)
usersStatus (users who joined this session status)
    [{uid: uid, online: (true, false)}]
lastMsgid (last message id)
lastUpdate (last update timestamp)
```
 - messages: messages tables
```
*uid (send user uid)
*sessionid (relative session id)
msgType (message type)
content (message content)
fileInfo (file information)
    {
        filePath
        fileName
        fileType
        fileSize
        fileThumb
    }
*dateline (created timestamp)
```

 - onlines: online users table
```
*uid (online user id)
dateline (last update timestamp)
```

 - notifications: receive notifications table
```
noticeType (notification type: "joinFriend", "removeFriend", "inviteSession")
senduid (send user id)
*recvuid (receive user id)
sessionid (relative session id)
isRead (notification is read: 0:not read, 1:already read)
dateline (created timestamp)
```

---

[Category](#category)

#### Websocket message type

There are two websocket channel: ws-push and ws-chat

> ws-push send sessions new message to users, when user not online the session, they still can receive which sessions has new messages

/ws-push channel
```
up message, use to subscribe push message:
{ userToken: "xxx" }

down message:
acceptMsg:     { uid: "xxx", nickname: "xxx", avatar: "xxx", sessionid: "xxx", sessionName: "xxx", sessionIcon: "xxx", msgType: "accept", content: "xxx", dateline: "xxx" }
rejectMsg:     { uid: "", nickname: "", avatar: "", sessionid: "", sessionName: "", sessionIcon: "", msgType: "reject", content: "xxx", dateline: "xxx" }
keepAlive:     { uid: "", nickname: "", avatar: "", sessionid: "", sessionName: "", sessionIcon: "", msgType: "keepalive", content: "", dateline: "xxx" }
textMsg:       { uid: "xxx", nickname: "xxx", avatar: "xxx", sessionid: "xxx", sessionName: "xxx", sessionIcon: "xxx", msgType: "text", content: "xxx", dateline: "xxx" }
fileMsg:       { uid: "xxx", nickname: "xxx", avatar: "xxx", sessionid: "xxx", sessionName: "xxx", sessionIcon: "xxx", msgType: "file", fileName: "xxx", fileType: "xxx", fileid: "xxx", thumbid: "xxx", dateline: "xxx" }
onlineMsg:     { uid: "xxx", nickname: "xxx", avatar: "xxx", sessionid: "xxx", sessionName: "xxx", sessionIcon: "xxx", msgType: "online", content: "xxx", dateline: "xxx" }
offlineMsg:    { uid: "xxx", nickname: "xxx", avatar: "xxx", sessionid: "xxx", sessionName: "xxx", sessionIcon: "xxx", msgType: "offline", content: "xxx", dateline: "xxx" }
joinSessionMsg: { uid: "xxx", nickname: "xxx", avatar: "xxx", sessionid: "xxx", sessionName: "xxx", sessionIcon: "xxx", msgType: "join", content: "xxx", dateline: "xxx" }
leaveSessionMsg:{ uid: "xxx", nickname: "xxx", avatar: "xxx", sessionid: "xxx", sessionName: "xxx", sessionIcon: "xxx", msgType: "leave", content: "xxx", dateline: "xxx" }
noticeMsg:     { uid: "", nickname: "", avatar: "", sessionid: "", sessionName: "xxx", sessionIcon: "xxx", msgType: "system", content: "xxx", dateline: "xxx" }

message push to browser:
pushMsg:       { 
                    uid: "xxx", nickname: "xxx", avatar: "xxx", sessionid: "xxx", sessionName: "xxx", sessionIcon: "xxx", msgType: "xxx", 
                    content: "xxx", fileName: "xxx", fileType: "xxx", fileid: "xxx", thumbid: "xxx",
                    dateline: "xxx" 
               }
```

---

[Category](#category)

> ws-chat is session chat channel, user send and receive session messages in this channel

```
/ws-chat channel
up message: 
onlineMsg:     { userToken: "xxx", sessionToken: "xxx", msgType:"online", content:"" }
textMsg:       { userToken: "xxx", sessionToken: "xxx", msgType:"text", content:"xxx" }
fileMsg:       { userToken: "xxx", sessionToken: "xxx", msgType:"file", fileName:"xxx", fileSize: 999, fileType: "xxx" }<#BinaryInfo#>binary_file_array_buffer

down message:   
rejectMsg:     { uid: "", nickname: "", avatar: "", sessionid: "", sessionName: "", sessionIcon: "", msgType: "reject", content: "xxx", dateline: "xxx" }
keepAlive:     { uid: "", nickname: "", avatar: "", sessionid: "", sessionName: "", sessionIcon: "", msgType: "keepalive", content: "", dateline: "xxx" }
textMsg:       { uid: "xxx", nickname: "xxx", avatar: "xxx", sessionid: "xxx", sessionName: "xxx", sessionIcon: "xxx", msgType: "text", content: "xxx", dateline: "xxx" }
fileMsg:       { uid: "xxx", nickname: "xxx", avatar: "xxx", sessionid: "xxx", sessionName: "xxx", sessionIcon: "xxx", msgType: "file", fileName: "xxx", fileType: "xxx", fileid: "xxx", thumbid: "xxx", dateline: "xxx" }
onlineMsg:     { uid: "xxx", nickname: "xxx", avatar: "xxx", sessionid: "xxx", sessionName: "xxx", sessionIcon: "xxx", msgType: "online", content: "xxx", dateline: "xxx" }
offlineMsg:    { uid: "xxx", nickname: "xxx", avatar: "xxx", sessionid: "xxx", sessionName: "xxx", sessionIcon: "xxx", msgType: "offline", content: "xxx", dateline: "xxx" }
joinSessionMsg:{ uid: "xxx", nickname: "xxx", avatar: "xxx", sessionid: "xxx", sessionName: "xxx", sessionIcon: "xxx", msgType: "join", content: "xxx", dateline: "xxx" }
leaveSessionMsg:{ uid: "xxx", nickname: "xxx", avatar: "xxx", sessionid: "xxx", sessionName: "xxx", sessionIcon: "xxx", msgType: "leave", content: "xxx", dateline: "xxx" }
noticeMsg:     { uid: "", nickname: "", avatar: "", sessionid: "", sessionName: "xxx", sessionIcon: "xxx", msgType: "system", content: "xxx", dateline: "xxx" }

message push to browser:
chatMsg:       { 
                    uid: "xxx", nickname: "xxx", avatar: "xxx", msgType: "xxx", 
                    content: "xxx", fileName: "xxx", fileType: "xxx", fileid: "xxx", thumbid: "xxx",
                    dateline: "xxx" 
               }
```    

---

[Category](#category)


### ChangeLog
#### 0.1.0-SNAPSHOT

---

[Category](#category)

#### 0.2.0-SNAPSHOT

* CookIM now support MongoDB 3.4.4
* Upgrade akka version to 2.5.2
* Update docker-compose startup CookIM cluster readme

---

[Category](#category)

#### 0.2.4-SNAPSHOT

* Now support send voice message, required WebRTC support browser, now Chrome Firefox and the new Safari11 available.
* Configurate mongodb connection params by command line.
* Update docker startup mode.

---

[Category](#category)


================================================
FILE: README_CN.md
================================================
# CookIM - 一个基于akka的分布式websocket聊天程序

- 支持私聊、群聊
- 支持分布式多个服务端通信
- 支持文本消息、文件消息、语音消息(感谢[ft115637850](https://github.com/ft115637850)的PR)

![CookIM logo](docs/cookim.png)

- [中文文档](README_CN.md)
- [English document](README.md)

---

- [GitHub项目地址](https://github.com/cookeem/CookIM/)
- [OSChina项目地址](https://git.oschina.net/cookeem/CookIM/)

---

### 目录

1. [演示](#演示)
    1. [PC演示](#PC演示)
    1. [手机演示](#手机演示)
    1. [演示地址](#演示地址)
1. [以Docker-Compose方式启动CookIM集群](#以docker-compose方式启动cookim集群)
    1. [启动集群](#启动集群)
    1. [增加节点](#增加节点)
    1. [调试容器](#调试容器)
    1. [停止集群](#停止集群)
1. [运行](#运行)
    1. [本地运行需求](#本地运行需求)
    1. [获取源代码](#获取源代码)
    1. [配置与打包](#配置与打包)
    1. [启动CookIM服务](#启动cookim服务)
    1. [打开浏览器,访问以下网址8080](#打开浏览器访问以下网址8080)
    1. [启动另一个CookIM服务](#启动另一个cookim服务)
    1. [打开浏览器,访问以下网址8081](#打开浏览器访问以下网址8081)
1. [架构](#架构)
    1. [整体服务架构](#整体服务架构)
    1. [akka stream websocket graph](#akka-stream-websocket-graph)
    1. [MongoDB数据库说明](#mongodb数据库说明)
    1. [消息类型](#消息类型)
1. [ChangeLog](#ChangeLog)
    1. [0.1.0-SNAPSHOT](#010-snapshot)
    1. [0.2.0-SNAPSHOT](#020-snapshot)
    1. [0.2.4-SNAPSHOT](#024-snapshot)    

---
[返回目录](#目录)

###演示

#### PC演示

![screen snapshot](docs/screen.png) 

#### 手机演示

![screen snapshot](docs/screen2.png)


#### 演示地址
[https://im.cookeem.com](https://im.cookeem.com)

---

### 以Docker-Compose方式启动CookIM集群

#### 启动集群

进入CookIM所在目录,运行以下命令,以docker-compose方式启动CookIM集群,该集群启动了三个容器:mongo、cookim1、cookim2
```sh
$ git clone https://github.com/cookeem/CookIM.git

$ cd CookIM

$ sudo docker-compose up -d
Creating mongo
Creating cookim1
Creating cookim2
```

成功启动集群后,浏览器分别访问以下网址,对应不同的CookIM服务
> http://localhost:8080
> http://localhost:8081

---

[返回目录](#目录)

#### 增加节点

可以通过修改docker-compose.yml文件增加CookIM服务节点,例如增加第三个节点cookim3:

```yaml
      cookim3:
        image: cookeem/cookim
        container_name: cookim3
        hostname: cookim3
        environment:
          HOST_NAME: cookim3
          WEB_PORT: 8080
          AKKA_PORT: 2551
          SEED_NODES: cookim1:2551
          MONGO_URI: mongodb://mongo:27017/local
        ports:
        - "8082:8080"
        depends_on:
        - mongo
        - cookim1
```
---

[返回目录](#目录)

#### 调试容器

查看cookim1容器日志输出
```sh
$ sudo docker logs -f cookim1
```

进入cookim1容器进行调试
```sh
$ sudo docker exec -ti cookim1 bash
```
---

[返回目录](#目录)

#### 停止集群
```sh
$ sudo docker-compose stop
$ sudo docker-compose rm -f
```
---

[返回目录](#目录)

### 运行

#### 本地运行需求

* JDK 8+
* Scala 2.11+
* SBT 0.13.15
* MongoDB 2.6 - 3.4

---

[返回目录](#目录)

#### 获取源代码
```sh
git clone https://github.com/cookeem/CookIM.git

cd CookIM
```
---

[返回目录](#目录)

#### 配置与打包

配置文件位于```conf/application.conf```,请务必配置mongodb的uri配置
```sh
mongodb {
  dbname = "cookim"
  uri = "mongodb://mongo:27017/local"
}
```

对CookIM进行打包fatjar,打包后文件位于```target/scala-2.11/CookIM-assembly-0.2.0-SNAPSHOT.jar```
```sh
sbt clean assembly
```

---

[返回目录](#目录)

#### 启动CookIM服务

CookIM的数据保存在MongoDB中,启动CookIM前务必先启动MongoDB

a. 调试方式启动服务:
```sh
$ sbt "run-main com.cookeem.chat.CookIM -h localhost -w 8080 -a 2551 -s localhost:2551"
```

b. 打包编译:
```sh
$ sbt assembly
```

c. 产品方式启动服务:
```sh
$ java -classpath "target/scala-2.11/CookIM-assembly-0.2.4-SNAPSHOT.jar" com.cookeem.chat.CookIM -h localhost -w 8080 -a 2551 -s localhost:2551
```

以上命令启动了一个监听8080端口的WEB服务,akka system的监听端口为2551

参数说明:

 -a <AKKA-PORT> -h <HOST-NAME> [-m <MONGO-URI>] [-n] -s <SEED-NODES> -w
       <WEB-PORT>
 -a,--akka-port <AKKA-PORT>     akka cluster node port
 -h,--host-name <HOST-NAME>     current web service external host name
 -m,--mongo-uri <MONGO-URI>     mongodb connection uri, example:
                                mongodb://localhost:27017/local
 -n,--nat                       is nat network or in docker
 -s,--seed-nodes <SEED-NODES>   akka cluster seed nodes, seperate with
                                comma, example:
                                localhost:2551,localhost:2552
 -w,--web-port <WEB-PORT>       web service port

---

[返回目录](#目录)

#### 打开浏览器,访问以下网址8080
> http://localhost:8080

---

[返回目录](#目录)

#### 启动另一个CookIM服务

打开另外一个终端,启动另一个CookIM服务,测试服务间的消息通讯功能。

a. 调试方式启动服务:
```sh
$ sbt "run-main com.cookeem.chat.CookIM -h localhost -w 8081 -a 2552 -s localhost:2551"
```

b. 产品方式启动服务:
```sh
$ java -classpath "target/scala-2.11/CookIM-assembly-0.2.0-SNAPSHOT.jar" com.cookeem.chat.CookIM -h localhost -w 8081 -a 2552 -s localhost:2551
```

以上命令启动了一个监听8081端口的WEB服务,akka system的监听端口为2552

---

[返回目录](#目录)

#### 打开浏览器,访问以下网址8081
> http://localhost:8081

该演示启动了两个CookIM服务,访问地址分别为8080端口以及8081端口,用户通过两个浏览器分别访问不同的的CookIM服务,用户在浏览器中通过websocket发送消息到akka集群,akka集群通过分布式的消息订阅与发布,把消息推送到集群中相应的节点,实现消息在不同服务间的分布式通讯。

---

[返回目录](#目录)

### 架构

#### 整体服务架构

![CookIM architecture](docs/CookIM-Flow.png)

**CookIM服务由三部分组成,基础原理如下:**

> 1. akka http:用于提供web服务,浏览器通过websocket连接akka http来访问分布式聊天应用;

> 2. akka stream:akka http在接收websocket发送的消息之后(消息包括文本消息:TextMessage以及二进制文件消息:BinaryMessage),把消息放到chatService流中进行流式处理。websocket消息中包含JWT(Javascript web token),如果JWT校验不通过,chatService流会直接返回reject消息;如果JWT校验通过,chatService流会把消息发送到ChatSessionActor中;

> 3. akka cluster:akka stream把用户消息发送到akka cluster,CookIM使用到akka cluster的DistributedPubSub,当用户进入会话的时候,订阅(Subscribe)对应的会话;当用户向会话发送消息的时候,会把消息发布(Publish)到订阅的actor中,此时,群聊中的用户就可以收到消息。

---

[返回目录](#目录)

#### akka stream websocket graph

![CookIM stream](docs/CookIM-ChatStream.png)

 - akka http在接收到websocket发送的消息之后,会把消息发送到chatService流里边进行处理,这里使用到akka stream graph:

> 1. websocket发送的消息体包含JWT,flowFromWS用于接收websocket消息,并把消息里边的JWT进行解码,验证有效性;

> 2. 对于JWT校验失败的消息,会经过filterFailure进行过滤;对于JWT校验成功的消息,会经过filterSuccess进行过滤;

> 3. builder.materializedValue为akka stream的物化值,在akka stream创建的时候,会自动向connectedWs发送消息,connectedWs把消息转换成UserOnline消息,通过chatSinkActor发送给ChatSessionActor;

> 4. chatActorSink向chatSessionActor发送消息,在akka stream结束的时候,向down stream发送UserOffline消息;

> 5. chatSource用于接收从ChatSessionActor中回送的消息,并且把消息发送给flowAcceptBack;

> 6. flowAcceptBack提供keepAlive,保证连接不中断;

> 7. flowReject和flowAcceptBack的消息最后统一通过flowBackWs处理成websocket形式的Message通过websocket回送给用户;

---

[返回目录](#目录)

#### MongoDB数据库说明

 - users: 用户表
```
*login(登录邮箱)
nickname(昵称)
password(密码SHA1)
gender(性别:未知:0,男生:1,女生:2)
avatar(头像,绝对路径,/upload/avatar/201610/26/xxxx.JPG)
lastLogin(最后登录时间,timstamp)
loginCount(登录次数)
sessionsStatus(用户相关的会话状态列表)
    [{sessionid: 会话id, newCount: 未读的新消息数量}]
friends(用户的好友列表:[好友uuid])
dateline(注册时间,timstamp)
```

 - sessions: 会话表(记录所有群聊私聊的会话信息)
```
*createuid(创建者的uid)
*ouid(接收者的uid,只有当私聊的时候才有效)
sessionIcon(会话的icon,对于群聊有效)
sessionType(会话类型:0:私聊,1:群聊)
publicType(可见类型:0:不公开邀请才能加入,1:公开)
sessionName(群描述)
dateline(创建日期,timestamp)
usersStatus(会话对应的用户uuid数组)
    [{uid: 用户uuid, online: 是否在线(true:在线,false:离线}]
lastMsgid(最新发送的消息id)
lastUpdate(最后更新时间,timstamp)
```
 - messages: 消息表(记录会话中的消息记录)
```
*uid(消息发送者的uid)
*sessionid(所在的会话id)
msgType(消息类型:)
content(消息内容)
fileInfo(文件内容)
    {
        filePath(文件路径)
        fileName(文件名)
        fileType(文件mimetype)
        fileSize(文件大小)
        fileThumb(缩略图)
    }
*dateline(创建日期,timestamp)
```

 - onlines:(在线用户表)
```
*id(唯一标识)
*uid(在线用户uid)
dateline(更新时间戳)
```

 - notifications:(接收通知表)
```
noticeType:通知类型("joinFriend", "removeFriend", "inviteSession")
senduid:操作方uid
*recvuid:接收方uid
sessionid:对应的sessionid
isRead:是否已读(0:未读,1:已读)
dateline(更新时间戳)
```

---

[返回目录](#目录)

#### 消息类型

有两个websocket信道:ws-push和ws-chat

> ws-push向用户下发消息提醒,当用户不在会话中,可以提醒用户有哪些会话有新消息

/ws-push channel
```
上行消息,用于订阅推送消息:
{ userToken: "xxx" }

下行消息:
acceptMsg:     { uid: "xxx", nickname: "xxx", avatar: "xxx", sessionid: "xxx", sessionName: "xxx", sessionIcon: "xxx", msgType: "accept", content: "xxx", dateline: "xxx" }
rejectMsg:     { uid: "", nickname: "", avatar: "", sessionid: "", sessionName: "", sessionIcon: "", msgType: "reject", content: "xxx", dateline: "xxx" }
keepAlive:     { uid: "", nickname: "", avatar: "", sessionid: "", sessionName: "", sessionIcon: "", msgType: "keepalive", content: "", dateline: "xxx" }
textMsg:       { uid: "xxx", nickname: "xxx", avatar: "xxx", sessionid: "xxx", sessionName: "xxx", sessionIcon: "xxx", msgType: "text", content: "xxx", dateline: "xxx" }
fileMsg:       { uid: "xxx", nickname: "xxx", avatar: "xxx", sessionid: "xxx", sessionName: "xxx", sessionIcon: "xxx", msgType: "file", fileName: "xxx", fileType: "xxx", fileid: "xxx", thumbid: "xxx", dateline: "xxx" }
onlineMsg:     { uid: "xxx", nickname: "xxx", avatar: "xxx", sessionid: "xxx", sessionName: "xxx", sessionIcon: "xxx", msgType: "online", content: "xxx", dateline: "xxx" }
offlineMsg:    { uid: "xxx", nickname: "xxx", avatar: "xxx", sessionid: "xxx", sessionName: "xxx", sessionIcon: "xxx", msgType: "offline", content: "xxx", dateline: "xxx" }
joinSessionMsg: { uid: "xxx", nickname: "xxx", avatar: "xxx", sessionid: "xxx", sessionName: "xxx", sessionIcon: "xxx", msgType: "join", content: "xxx", dateline: "xxx" }
leaveSessionMsg:{ uid: "xxx", nickname: "xxx", avatar: "xxx", sessionid: "xxx", sessionName: "xxx", sessionIcon: "xxx", msgType: "leave", content: "xxx", dateline: "xxx" }
noticeMsg:     { uid: "", nickname: "", avatar: "", sessionid: "", sessionName: "xxx", sessionIcon: "xxx", msgType: "system", content: "xxx", dateline: "xxx" }

下行到浏览器消息格式:
pushMsg:       { 
                    uid: "xxx", nickname: "xxx", avatar: "xxx", sessionid: "xxx", sessionName: "xxx", sessionIcon: "xxx", msgType: "xxx", 
                    content: "xxx", fileName: "xxx", fileType: "xxx", fileid: "xxx", thumbid: "xxx",
                    dateline: "xxx" 
               }
```

---

[返回目录](#目录)

> ws-chat为用户在会话中的聊天信道,用户在会话中发送消息以及接收消息用

```
/ws-chat channel
上行消息:
onlineMsg:     { userToken: "xxx", sessionToken: "xxx", msgType:"online", content:"" }
textMsg:       { userToken: "xxx", sessionToken: "xxx", msgType:"text", content:"xxx" }
fileMsg:       { userToken: "xxx", sessionToken: "xxx", msgType:"file", fileName:"xxx", fileSize: 999, fileType: "xxx" }<#BinaryInfo#>binary_file_array_buffer

下行消息:    
rejectMsg:     { uid: "", nickname: "", avatar: "", sessionid: "", sessionName: "", sessionIcon: "", msgType: "reject", content: "xxx", dateline: "xxx" }
keepAlive:     { uid: "", nickname: "", avatar: "", sessionid: "", sessionName: "", sessionIcon: "", msgType: "keepalive", content: "", dateline: "xxx" }
textMsg:       { uid: "xxx", nickname: "xxx", avatar: "xxx", sessionid: "xxx", sessionName: "xxx", sessionIcon: "xxx", msgType: "text", content: "xxx", dateline: "xxx" }
fileMsg:       { uid: "xxx", nickname: "xxx", avatar: "xxx", sessionid: "xxx", sessionName: "xxx", sessionIcon: "xxx", msgType: "file", fileName: "xxx", fileType: "xxx", fileid: "xxx", thumbid: "xxx", dateline: "xxx" }
onlineMsg:     { uid: "xxx", nickname: "xxx", avatar: "xxx", sessionid: "xxx", sessionName: "xxx", sessionIcon: "xxx", msgType: "online", content: "xxx", dateline: "xxx" }
offlineMsg:    { uid: "xxx", nickname: "xxx", avatar: "xxx", sessionid: "xxx", sessionName: "xxx", sessionIcon: "xxx", msgType: "offline", content: "xxx", dateline: "xxx" }
joinSessionMsg:{ uid: "xxx", nickname: "xxx", avatar: "xxx", sessionid: "xxx", sessionName: "xxx", sessionIcon: "xxx", msgType: "join", content: "xxx", dateline: "xxx" }
leaveSessionMsg:{ uid: "xxx", nickname: "xxx", avatar: "xxx", sessionid: "xxx", sessionName: "xxx", sessionIcon: "xxx", msgType: "leave", content: "xxx", dateline: "xxx" }
noticeMsg:     { uid: "", nickname: "", avatar: "", sessionid: "", sessionName: "xxx", sessionIcon: "xxx", msgType: "system", content: "xxx", dateline: "xxx" }

下行到浏览器消息格式:
chatMsg:       { 
                    uid: "xxx", nickname: "xxx", avatar: "xxx", msgType: "xxx", 
                    content: "xxx", fileName: "xxx", fileType: "xxx", fileid: "xxx", thumbid: "xxx",
                    dateline: "xxx" 
               }
```    

---

[返回目录](#目录)

### ChangeLog
#### 0.1.0-SNAPSHOT

---

[返回目录](#目录)

#### 0.2.0-SNAPSHOT

* CookIM支持MongoDB 3.4.4
* 更新akka版本为2.5.2
* 更新容器启动方式,只保留docker-compose方式启动集群

---

[返回目录](#目录)

#### 0.2.4-SNAPSHOT

* 支持发送语音消息,chrome和firefox以及最新的safari11支持
* 支持命令行设置mongodb连接参数设置
* 更新docker启动方式

---

[返回目录](#目录)


================================================
FILE: README_JENKINS.md
================================================
### Jenkins安装相关插件("系统管理" -> "管理插件")

- CloudBees Docker Build and Publish plugin
    > docker插件,在"构建"步骤增加"Docker Build and Publish",把构建结果Build到docker以及push到registry
- CloudBees Docker Custom Build Environment Plugin
    > docker插件,在"构建环境"步骤增加"Build inside a Docker container",在构建环境的时候下载docker客户端,在docker中进行项目构建
- docker-build-step
    > docker插件,在"构建"步骤增加"Execute Docker command",在构建过程中增加docker客户端指令步骤
- GitLab Plugin
    > gitlab插件,在"General"步骤增加"GitLab connection",源码管理可以调用gitlab
- Gitlab Authentication plugin
    > gitlab插件,可以使用gitlab的api token进行授权
- Gitlab Hook Plugin
    > gitlab插件,在"构建触发器"步骤增加"Build when a change is pushed to GitLab. GitLab CI Service URL: http://localhost:8080/project/XXX"
    
    > 当gitlab代码发生提交的时候,通过gitlab hook主动触发构建 
- Kubernetes plugin
    > kubernetes插件,可以在kubernetes中启动相关pod
- Maven Integration plugin
    > Maven插件,可以增加“构建一个Maven项目”
    > 错误修复:
    > 如果安装了“CloudBees Docker Custom Build Environment Plugin”,在进行maven构建的时候,会出现调用dockerhost连接错误的提示:
    ```
    Established TCP socket on dockerhost:57438
    maven33-agent.jar already up to date
    maven33-interceptor.jar already up to date
    maven3-interceptor-commons.jar already up to date
    [cyberoptic-demo-core-messages] $ /etc/alternatives/java_sdk_1.8.0/bin/java -cp /var/lib/jenkins/slave-node/maven33-agent.jar:/opt/apache-maven-3.3.3/boot/plexus-classworlds-2.5.2.jar:/opt/apache-maven-3.3.3/conf/logging jenkins.maven3.agent.Maven33Main /opt/apache-maven-3.3.3 /var/lib/jenkins/slave-node/slave.jar /var/lib/jenkins/slave-node/maven33-interceptor.jar /var/lib/jenkins/slave-node/maven3-interceptor-commons.jar dockerhost:57438
    ```

    > 那是因为jenkins安装了“CloudBees Docker Custom Build Environment Plugin”,并且发现dockerhost这个主机名能够访问,这个时候,就会把本机当成在docker中运行的slave jenkins,并且尝试连接dockerhost来启动maven。

    > 主要是因为使用了中国移动的CMCC或者热点上网,导致DNS劫持。务必修改Mac系统的dockerd的daemon.json设置:
    ```
    {
      "dns": [
        "114.114.114.114"
      ],
      "registry-mirrors" : [
        "http://3d13f480.m.daocloud.io"
      ]
    }
    ```


### Jenkins中GitLab、Docker、Maven基础配置

- GitLab连接设置("系统管理" -> "系统设置" -> "GitLab connections")
    > "Connection name" 设置为 gitlab_cookeem
    
    > "Gitlab host URL" 设置为 http://gitlab
     
    > "Credentials" 需要"Add Credentials","Kind" 选择 "GitLab API token";"API token"对应 Gitlab "User Settings" -> "Account" -> "Private token"
    
    > "Test Connection" 检测GitLab API token能够正常连接

- Docker环境设置("系统管理" -> "Global Tool Configuration" -> "Docker" -> "Docker安装")
    > "新增Docker" 新增一个Docker版本的环境变量
    
    > "Name" 设置为 docker_1.13.1;"自动安装" 选择上
    
    > "新增安装" 选择 "Install latest from docker.io"
    
    > "Docker version" 设置为 1.13.1
    
- Docker Builder环境设置,对应docker-build-step插件("系统管理" -> "系统设置" -> "Docker Builder")
    > "Docker URL" 设置为 tcp://docker:2375
    
    > "Test Connection" 检测连接是否正常
    
- Maven环境设置("系统管理" -> "Global Tool Configuration" -> "Maven" -> "Maven安装")
    > "新增Maven" 新增一个Maven版本的环境变量
    
    > "别名" 设置为 maven3.5.0;"自动安装" 选择上
    
    > "新增安装" 选择 "Install from Apache"
    
    > "Version" 选择 Maven的版本

- JDK环境设置("系统管理" -> "Global Tool Configuration" -> "JDK" -> "JDK安装")
    > "新增JDK" 新增一个JDK版本的环境变量
    
    > "别名" 设置为 jdk8u131;"自动安装" 选择上
    
    > "新增安装" 选择 "从java.sun.com安装"
    
    > "版本" 选择JDK的版本

### Jenkins中新建项目,实现Maven项目通过GitLab进行源码管理和自动打包到Docker

- "新建" -> "构建一个maven项目"

- "General"设置
    > "项目名称" 设置为 CookIM
    
    > "GitLab connection" 选择 gitlab_cookeem(对应"系统管理" -> "系统设置" -> "GitLab connections")

- "源码管理"设置
    > "Git" -> "Repositories" -> "Repository URL" 设置为 http://gitlab/cookeem/CookIM
    
    > "Git" -> "Repositories" -> "Credentials" -> "Add Credentials","Kind" 选择 "Username with password","Username" 设置为 cookeem@qq.com,"Password" 设置为对应GitLab账号密码

- "构建触发器"设置
    > "Build when a change is pushed to GitLab. GitLab CI Service URL: http://localhost:8080/project/CookIM" 该项选择
    
    > "Build when a change is pushed to GitLab." -> "高级" -> "Secret token" -> "Generate" 创建Jenkins token
    
    > 打开GitLab界面,"Projects" -> "cookeem/CookIM" -> "Settings" -> "Integrations","URL" 设置为 http://jenkins:8080/project/CookIM(对应Jenkins的"GitLab CI Service URL"),"Secret Token" 设置为对应Jenkins的"Secret token"。创建WebHook后进行测试,就会触发自动构建

- "构建环境"设置
    > "Add timestamps to the Console Output" 选择上
    
- "Pre Steps"设置
    > "新增构建步骤" -> "Execute shell",执行以下构建脚本
```
printenv
```

- "Build"设置
    > "Goals and options" 设置为 clean install
    > 点击高级
    > "Settings file" 选择 "Settings file in filesystem"
    > "File path" 设置为 /var/jenkins_home/maven/settings.xml (注意,务必设置settings.xml的mirrors设置指向nexus)

- "Post Steps"设置
    > "Add post-build step" -> "Execute shell",执行以下构建脚本
```
# 设置DOCKER_HOME
export MY_DOCKER_HOME=/var/jenkins_home/tools/org.jenkinsci.plugins.docker.commons.tools.DockerTool/docker_1.13.1
export PATH=$PATH:$MY_DOCKER_HOME/bin
export DOCKER_HOST=tcp://ci-docker:2375

# 设置版本信息
export APP_VERSION_NAME=`cat VERSION`

# 把文件复制到项目目录
mv target/cookim-${APP_VERSION_NAME}-allinone.jar cookim.jar

# 构建docker镜像
docker build -t k8s-registry:5000/cookeem/cookim:$APP_VERSION_NAME -f Dockerfile_k8s .

# 把docker镜像推送到k8s-registry:5000
docker push k8s-registry:5000/cookeem/cookim:$APP_VERSION_NAME

# 使用kubectl拉起镜像
kubectl apply -f kubernetes/cookim.yaml
```
    
- "保存"项目

- GitLab中进行push,触发Jenkins进行Maven项目构建,完成构建后,把编译包build成docker镜像,并且把镜像push到docker registry

- 源码的根目录需要创建Dockerfile,用于"CloudBees Docker Build and Publish plugin"进行自动构建docker镜像

- 在jenkins容器中测试CookIM是否启动正常
    ```
        docker exec -ti jenkins bash
        curl docker:8081/user/haijian/ok
        exit
    ```

- 在docker容器中测试CookIM是否启动正常,检测logs中的App Version
    ```
        docker exec -ti docker ash
        docker images
        docker ps
        docker logs CookIM
        exit
    ```

- 关闭服务,注意,如果只是stop再up,docker容器启动会出现异常
    ```
        docker-compose stop && docker-compose rm -f
    ```


================================================
FILE: VERSION
================================================
0.2.3-SNAPSHOT

================================================
FILE: build.sbt
================================================
name := "CookIM"

version := "0.2.4-SNAPSHOT"

scalaVersion := "2.11.8"

scalacOptions := Seq("-unchecked", "-deprecation", "-encoding", "utf8")

libraryDependencies ++= {
  val akkaV = "2.5.2"
  val akkaHttpV = "10.0.7"
  val reactivemongoV = "0.12.3"
  Seq(
    "com.typesafe.akka" %% "akka-actor"  % akkaV,
    // "com.typesafe.akka" %% "akka-remote" % akkaV,
    "com.typesafe.akka" %% "akka-cluster" % akkaV,
    "com.typesafe.akka" %% "akka-cluster-tools" % akkaV,
    "com.typesafe.akka" %% "akka-testkit" % akkaV % Test,
    "com.typesafe.akka" %% "akka-stream" % akkaV,
    "com.typesafe.akka" %% "akka-stream-testkit" % akkaV % Test,
    // "com.typesafe.akka" %% "akka-http-core" % akkaHttpV,
    "com.typesafe.akka" %% "akka-http" % akkaHttpV,
    "com.typesafe.akka" %% "akka-http-testkit" % akkaHttpV % Test,
//     "org.scalactic" %% "scalactic" % "3.0.1",
//     "org.scalatest" %% "scalatest" % "3.0.1" % "test",
    "com.typesafe.play" %% "play-json" % "2.5.15",
    "org.slf4j" % "slf4j-simple" % "1.7.25",
    "com.sksamuel.scrimage" %% "scrimage-core" % "2.1.8",
    "com.sksamuel.scrimage" %% "scrimage-io-extra" % "2.1.8",
    "com.esotericsoftware" % "kryo" % "4.0.0",
    "com.github.romix.akka" %% "akka-kryo-serialization" % "0.5.0",
    "commons-cli" % "commons-cli" % "1.4",
    "io.jsonwebtoken" % "jjwt" % "0.7.0",
    "org.reactivemongo" %% "reactivemongo" % reactivemongoV,
    "org.reactivemongo" %% "reactivemongo-play-json" % reactivemongoV
 )
}

////sbt使用代理
//javaOptions in console ++= Seq(
//  "-Dhttp.proxyHost=cmproxy-sgs.gmcc.net",
//  "-Dhttp.proxyPort=8081"
//)
//javaOptions in run ++= Seq(
//  "-Dhttp.proxyHost=cmproxy-sgs.gmcc.net",
//  "-Dhttp.proxyPort=8081"
//)



================================================
FILE: conf/application.conf
================================================
#mongodb settings
mongodb {
  dbname = "cookim"
  uri = "mongodb://mongo:27017/local"
}
//jwt secret settings
jwt {
  secret = "5d7312635ca0a-d071-454d-be56216c9-8271-4500-9b13-a3e6c850e4-b1de4871a8700132fb96-0655-462a-b7c4-134579e8e06fdf9dbe65-cb5c-42a8-abaf-77ffcf17ec18"
}

//if storeSecret set non-empty, it will use HTTPS
ssl {
  storeSecret = ""
}

#akka http settings, please do not change
akka.http {
  server {
    remote-address-header = on
    raw-request-uri-header = on
    idle-timeout = 60 s
  }
  parsing {
    max-content-length = 8m
  }
}

#akka cluster settings
akka {
  loglevel = "WARNING"
  cluster {
    #seed-nodes = ["akka.tcp://chat-cluster@localhost:2551"]
    #auto-down-unreachable-after = 10s
    metrics.enabled = off
  }
  # remote settings
  remote {
    log-remote-lifecycle-events = off
    netty.tcp {
      # Akka behind NAT or in a Docker container
      #hostname = "localhost"       # external (logical) hostname
      #port = 2551                 # external (logical) port

      #bind-hostname = "127.0.0.1" # internal (bind) hostname
      #bind-port = 0               # internal (bind) port
    }
  }
  # please do not change actor settings
  actor {
    provider = cluster
    serializers {
      #config available serializers
      java = "akka.serialization.JavaSerializer"
      kryo = "com.romix.akka.serialization.kryo.KryoSerializer"
    }
    kryo  { #Kryo settings
      type = "graph"
      idstrategy = "explicit" #it must use explicit
      serializer-pool-size = 16
      buffer-size = 4096
      use-manifests = false
      implicit-registration-logging = true
      kryo-trace = false
      classes = [
        "java.lang.String",
        "scala.Some",
        "scala.None$",
        "akka.util.ByteString$ByteString1C",
        "com.cookeem.chat.event.WsTextDown",
        "com.cookeem.chat.event.WsBinaryDown",
        "com.cookeem.chat.event.ClusterText",
        "com.cookeem.chat.event.ClusterBinary",
        "com.cookeem.chat.event.UserOnline",
        "com.cookeem.chat.event.UserOffline$"
      ]
    }
    serialization-bindings {
      "java.lang.String"=kryo
      "scala.Some"=kryo
      "scala.None$"=kryo
      "akka.util.ByteString$ByteString1C"=kryo
      "com.cookeem.chat.event.WsTextDown"=kryo
      "com.cookeem.chat.event.WsBinaryDown"=kryo
      "com.cookeem.chat.event.ClusterText"=kryo
      "com.cookeem.chat.event.ClusterBinary"=kryo
      "com.cookeem.chat.event.UserOnline"=kryo
      "com.cookeem.chat.event.UserOffline$"=kryo
    }
  }
}

================================================
FILE: docker-compose.yml
================================================
    version: '2'
    services:
      mongo:
        image: mongo:3.4.4
        container_name: mongo
        hostname: mongo
        volumes:
        - ./mongo:/data/db
        ports:
        - "27017:27017"
      cookim1:
        image: cookeem/cookim
        container_name: cookim1
        hostname: cookim1
        environment:
          HOST_NAME: cookim1
          WEB_PORT: 8080
          AKKA_PORT: 2551
          SEED_NODES: cookim1:2551
          MONGO_URI: mongodb://mongo:27017/local
        ports:
        - "8080:8080"
        depends_on:
        - mongo
      cookim2:
        image: cookeem/cookim
        container_name: cookim2
        hostname: cookim2
        environment:
          HOST_NAME: cookim2
          WEB_PORT: 8080
          AKKA_PORT: 2551
          SEED_NODES: cookim1:2551
          MONGO_URI: mongodb://mongo:27017/local
        ports:
        - "8081:8080"
        depends_on:
        - mongo
        - cookim1


================================================
FILE: docs/doc.md
================================================
分别打开不同终端,运行以下命令:

```
sbt "run-main com.cookeem.chat.CookIM -w 8080 -a 2551"

sbt "run-main com.cookeem.chat.CookIM -w 8081 -a 2552"
```

浏览器访问:

```
http://localhost:8080/

http://localhost:8081/
```
---

users: 用户表
===
```
*login(登录邮箱)
nickname(昵称)
password(密码SHA1)
gender(性别:未知:0,男生:1,女生:2)
avatar(头像,绝对路径,/upload/avatar/201610/26/xxxx.JPG)
lastLogin(最后登录时间,timstamp)
loginCount(登录次数)
sessionsStatus(用户相关的会话状态列表:[{sessionid: 会话id, newCount: 未读的新消息数量}])
friends(用户的好友列表:[{uuid: 好友uuid}])
dateline(注册时间,timstamp)
```
sessions: 会话表(记录所有群聊私聊的会话信息)
===
```
*createuid(创建者的uid)
*ouid(接收者的uid,只有当私聊的时候才有效)
sessionIcon(会话的icon,对于群聊有效)
sessionType(会话类型:0:私聊,1:群聊)
publicType(可见类型:0:不公开邀请才能加入,1:公开)
sessionName(群描述)
dateline(创建日期,timestamp)
usersStatus(会话对应的用户uuid数组:[{uid: 用户uuid, online: 是否在线(true:在线,false:离线)}])
lastMsgid(最新发送的消息id)
lastUpdate(最后更新时间,timstamp)
dateline(创建时间,timstamp)
```
messages: 消息表(记录会话中的消息记录)
===
```
*uid(消息发送者的uid)
*sessionid(所在的会话id)
msgType(消息类型:)
content(消息内容)
fileInfo(文件内容)
{
    filePath(文件路径)
    fileName(文件名)
    fileType(文件mimetype)
    fileSize(文件大小)
    fileThumb(缩略图)
}
*dateline(创建日期,timestamp)
```

onlines:(在线用户表)
===
```
*id(唯一标识)
*uid(在线用户uid)
dateline(更新时间戳)
```

notifications:(接收通知表)
===
```
noticeType:通知类型("joinFriend", "removeFriend", "inviteSession")
senduid:操作方uid
*recvuid:接收方uid
sessionid:对应的sessionid
isRead:是否已读(0:未读,1:已读)
dateline(更新时间戳)
```
### 11. 文件支持保存到本地目录,并自动命名文件。并且能够根据客户端发送的文件md5信息与服务端文件md5信息进行比较,建立文件与消息id对应关系

### 20. 支持显示状态: 在线、隐身、离开、忙碌

---
在线、离开、忙碌表示用户的头像状态,可以针对不同的聊天会话设置显示状态
隐身状态为不显示在群聊列表中

### 21. 支持表情

---
支持表情emoji(参见twitter的emoji库)
支持已经梳理好的表情
服务端支持直接解释表情文本为表情

### 22. 支持视频直播


### jwt验证流程

jwt用于保存服务端返回给用户的资源信息,这些资源通过明文传输,但是传输过程不可以篡改。
在jwt里边保存过期日期即可

jwt应该放在request的header中

浏览器(输入login -> 提交username,password)
服务器(验证username,password -> 输出jwt(uid))
浏览器(获取jwt(uid)并保存到程序中,请求uid对应的会话列表界面 -> 提交jwt(uid))
服务器(验证jwt(uid)是否有效 -> 输出jwt(uid)以及session列表信息)
浏览器(显示会话列表页面,点击某个会话 -> 提交jwt(uid)以及sessionid信息)
服务器(验证jwt(uid)是否有效 -> 输出jwt(uid, sessionid)以及session中的消息)
浏览器(展示会话消息查看页面,通过websocket通道提交发送消息 -> 提交jwt(uid, sessionid)以及消息内容)
服务器(通过websocket通道,验证jwt(uid, sessionid)是否有效,如果有效,表示uid有在sessionid中发消息的权限 -> 通过websocket通道发送消息)


# mongodb读写操作
[OK] 用户注册    registerUser
[OK] 用户登录    loginAction
[OK] 用户注销    logoutAction
[OK] 用户修改密码  changePwd
显示个人资料  getUserInfo
[OK] 修改个人资料  updateUserInfo
[OK] 查看会话列表    listSessions
加入群聊会话  joinSession
[OK] 创建群聊会话  createGroupSession
修改群聊信息  updateSessionInfo
离开群聊会话  leaveSession
查看历史消息(分页排序)    listHistoryMessages
创建私聊会话  createPrivateSession
查看群聊私聊资料(显示参与者列表) getSessionInfo


# websocket存在三个channel:
UserTokenChannel:用于从服务端推送UserToken到客户端,UserToken包含如下信息:uid、nickname、avatar,在keepalive中发送UserToken给客户端
SessionTokenChannel:当用户打开某个会话页面的时候,从服务端推送SessionToken到客户端,SessionToken包含如下信息:sessionid,表明用户有权在这个session中发送消息,在keepalive中发送SessionToken
MessageChannel:用于接收用户消息,以及向用户发送消息。当用户向服务端发送消息的时候,必须提供UserToken以及SessionToken,当这两个token验证都通过的情况下,用户可以发送消息,否则拒绝用户发送消息,并回送错误消息给用户。

# MessageChannel消息
```
/ws-push channel
上行:
{ userToken: "xxx" }
下行:
acceptMsg:     { uid: "xxx", nickname: "xxx", avatar: "xxx", sessionid: "xxx", sessionName: "xxx", sessionIcon: "xxx", msgType: "accept", content: "xxx", dateline: "xxx" }
rejectMsg:     { uid: "", nickname: "", avatar: "", sessionid: "", sessionName: "", sessionIcon: "", msgType: "reject", content: "xxx", dateline: "xxx" }
keepAlive:     { uid: "", nickname: "", avatar: "", sessionid: "", sessionName: "", sessionIcon: "", msgType: "keepalive", content: "", dateline: "xxx" }
textMsg:       { uid: "xxx", nickname: "xxx", avatar: "xxx", sessionid: "xxx", sessionName: "xxx", sessionIcon: "xxx", msgType: "text", content: "xxx", dateline: "xxx" }
fileMsg:       { uid: "xxx", nickname: "xxx", avatar: "xxx", sessionid: "xxx", sessionName: "xxx", sessionIcon: "xxx", msgType: "file", filePath: "xxx", fileName: "xxx", fileSize: 999, fileType: "xxx", fileThumb: "xxx", dateline: "xxx" }
onlineMsg:     { uid: "xxx", nickname: "xxx", avatar: "xxx", sessionid: "xxx", sessionName: "xxx", sessionIcon: "xxx", msgType: "online", content: "xxx", dateline: "xxx" }
offlineMsg:    { uid: "xxx", nickname: "xxx", avatar: "xxx", sessionid: "xxx", sessionName: "xxx", sessionIcon: "xxx", msgType: "offline", content: "xxx", dateline: "xxx" }
joinSessionMsg: { uid: "xxx", nickname: "xxx", avatar: "xxx", sessionid: "xxx", sessionName: "xxx", sessionIcon: "xxx", msgType: "join", content: "xxx", dateline: "xxx" }
leaveSessionMsg:{ uid: "xxx", nickname: "xxx", avatar: "xxx", sessionid: "xxx", sessionName: "xxx", sessionIcon: "xxx", msgType: "leave", content: "xxx", dateline: "xxx" }
noticeMsg:     { uid: "", nickname: "", avatar: "", sessionid: "", sessionName: "xxx", sessionIcon: "xxx", msgType: "system", content: "xxx", dateline: "xxx" }
下行用户端:
pushMsg:       { 
                    uid: "xxx", nickname: "xxx", avatar: "xxx", sessionid: "xxx", sessionName: "xxx", sessionIcon: "xxx", msgType: "xxx", 
                    content: "xxx", 
                    fileInfo: { filePath: "xxx", fileName: "xxx", fileSize: 999, fileType: "xxx", fileThumb: "xxx" },
                    dateline: "xxx" 
               }
```
---
```
/ws-chat channel
上行:
onlineMsg:     { userToken: "xxx", sessionToken: "xxx", msgType:"online", content:"" }
textMsg:       { userToken: "xxx", sessionToken: "xxx", msgType:"text", content:"xxx" }
fileMsg:       { userToken: "xxx", sessionToken: "xxx", msgType:"file", fileName:"xxx", fileSize: 999, fileType: "xxx" }<#BinaryInfo#>binary_file_array_buffer
下行:    
rejectMsg:     { uid: "", nickname: "", avatar: "", sessionid: "", sessionName: "", sessionIcon: "", msgType: "reject", content: "xxx", dateline: "xxx" }
keepAlive:     { uid: "", nickname: "", avatar: "", sessionid: "", sessionName: "", sessionIcon: "", msgType: "keepalive", content: "", dateline: "xxx" }
textMsg:       { uid: "xxx", nickname: "xxx", avatar: "xxx", sessionid: "xxx", sessionName: "xxx", sessionIcon: "xxx", msgType: "text", content: "xxx", dateline: "xxx" }
fileMsg:       { uid: "xxx", nickname: "xxx", avatar: "xxx", sessionid: "xxx", sessionName: "xxx", sessionIcon: "xxx", msgType: "file", filePath: "xxx", fileName: "xxx", fileSize: 999, fileType: "xxx", fileThumb: "xxx", dateline: "xxx" }
onlineMsg:     { uid: "xxx", nickname: "xxx", avatar: "xxx", sessionid: "xxx", sessionName: "xxx", sessionIcon: "xxx", msgType: "online", content: "xxx", dateline: "xxx" }
offlineMsg:    { uid: "xxx", nickname: "xxx", avatar: "xxx", sessionid: "xxx", sessionName: "xxx", sessionIcon: "xxx", msgType: "offline", content: "xxx", dateline: "xxx" }
joinSessionMsg:{ uid: "xxx", nickname: "xxx", avatar: "xxx", sessionid: "xxx", sessionName: "xxx", sessionIcon: "xxx", msgType: "join", content: "xxx", dateline: "xxx" }
leaveSessionMsg:{ uid: "xxx", nickname: "xxx", avatar: "xxx", sessionid: "xxx", sessionName: "xxx", sessionIcon: "xxx", msgType: "leave", content: "xxx", dateline: "xxx" }
noticeMsg:     { uid: "", nickname: "", avatar: "", sessionid: "", sessionName: "xxx", sessionIcon: "xxx", msgType: "system", content: "xxx", dateline: "xxx" }
下行用户端:
chatMsg:       { 
                    uid: "xxx", nickname: "xxx", avatar: "xxx", msgType: "xxx", 
                    content: "xxx", 
                    fileInfo: { filePath: "xxx", fileName: "xxx", fileSize: 999, fileType: "xxx", fileThumb: "xxx" },
                    dateline: "xxx" 
               }
```    
---


经常改变的字段:
users.sessionsStatus(用户相关的会话状态列表:[{sessionid: 会话id, newCount: 未读的新消息数量}])
[OK] users.sessionsStatus.sessionid在joinSession的时候增加记录,在leaveSession的时候删除记录
[OK] users.sessionsStatus.newCount,当用户不在线,createMessage的时候+1,listHistoryMessages的时候设置为0

users.lastLogin(最后登录时间,timstamp)
[OK] 在createUserToken的时候更新

users.loginCount(登录次数)
[OK] 在loginAction的时候+1

sessions.usersStatus(会话对应的用户uuid数组:[{uid: 用户uuid, online: 是否在线(true:在线,false:离线)}])
[OK] sessions.usersStatus.uid在joinSession的时候增加记录,在leaveSession的时候删除记录
[OK] sessions.usersStatus.online在用户进入会话的时候userOnlineOffline设置为true,在用户离开会话的时候设置为false
sessions.lastMsgid(最新发送的消息id)
[OK] 在createMessage的时候更新对应的lastmsgid

onlines.uid(在线用户uid)
onlines.dateline(更新时间戳)
[OK] 在createUserToken的时候更新

messages
[OK] 在用户发送消息的时候更新(createMessage)


# 改进需求
1、会话列表页(公开的)(群聊)可以(joinSession);
2、会话列表页(加入的)(群聊)可以(leaveSession);
3、会话列表页(加入的)(私聊)可以(leaveSession),leaveSession会把双方对应的users.sessionsstatus清除;
4、会话列表页(群聊),可以查看会话中的用户,哪些在线,哪些不在线;
5、查看会话中的用户列表界面,可以向某个用户发起会话(不能向自己发起会话),自动创建会话;
6、会话列表页,接口没有显示会话中最后发送的消息 ———— 对于文件、图片消息需要进行翻译;
7、顶部标题栏根据所在页面显示不同标题以及菜单

---

1、消息查看页(群聊),可以查看会话中的用户,哪些在线,哪些不在线;
2、消息查看页(群聊),可以修改群聊资料;
3、消息查看页(群聊),可以(leaveSession)、可以(inviteSession)邀请好友加入会话;
4、消息查看页(私聊),可以加好友或者删除好友;
5、消息查看页(私聊),可以邀请好友加入会话,自动创建新的会话;
6、消息查看页,对于图片消息可以查看图片大图;对于文件消息可以下载文件;
7、消息查看页,可以向某个用户发起会话(不能向自己发起会话),自动创建会话;
8、消息查看页,可以申请加某个用户为好友(不能加自己为好友);

---

1、左侧菜单显示已加入的群聊名称以及新消息数量;
2、主界面可以显示pushMessage的toast通知;
3、主界面右上角菜单可以关闭或者开启pushMessage的toast通知;
4、新建通知页面,以及通知表。通知表用户显示加好友通知,邀请加入会话通知。
——通知类型两类:加好友通知、邀请加入会话通知
5、新建好友列表页面,列表上可以删除好友;


================================================
FILE: kubernetes/cookim.yaml
================================================
---
apiVersion: v1
kind: Service
metadata:
  name: cookim
  labels:
    app: cookim
spec:
  type: NodePort
  selector:
    app: cookim
  ports:
  - name: port-80
    port: 80
    targetPort: 80
    nodePort: 30205

---
kind: Service
apiVersion: v1
metadata:
  name: cookim-headless
  labels:
    app: cookim-headless
spec:
  clusterIP: None
  ports:
  - name: tcp-2551
    protocol: TCP
    port: 2551
  - name: tcp-80
    protocol: TCP
    port: 80
  selector:
    app: cookim

---
apiVersion: policy/v1beta1
kind: PodDisruptionBudget
metadata:
  name: cookim-budget
spec:
  selector:
    matchLabels:
      app: cookim
  minAvailable: 2

---
apiVersion: apps/v1beta1
kind: StatefulSet
metadata:
  name: cookim
spec:
  serviceName: cookim-headless
  replicas: 3
  template:
    metadata:
      labels:
        app: cookim
      annotations:
        pod.alpha.kubernetes.io/initialized: "true"
        scheduler.alpha.kubernetes.io/affinity: >
            {
              "podAntiAffinity": {
                "requiredDuringSchedulingRequiredDuringExecution": [{
                  "labelSelector": {
                    "matchExpressions": [{
                      "key": "app",
                      "operator": "In",
                      "values": ["cookim-headless"]
                    }]
                  },
                  "topologyKey": "kubernetes.io/hostname"
                }]
              }
            }

        # pod.beta.kubernetes.io/init-containers: '[
        #     {
        #         "name": "install",
        #         "image": "k8s-registry:5000/centos:latest",
        #         "command": ["bash", "-c", "
        #           IFS=- read -r -a array <<< $(hostname) \n
        #           ordinal=${array[-1]} \n
        #           echo cookim-$ordinal >> /cookim-data/a.txt \n
        #         "],
        #         "volumeMounts": [
        #             {
        #                 "name": "esgv",
        #                 "mountPath": "/cookim-data"
        #             }
        #         ]
        #     }
        # ]'

    spec:
      volumes:
      - name: localtime
        hostPath:
          path: /etc/localtime
      - name: timezone
        hostPath:
          path: /etc/timezone
      containers:
      - name: cookim
        imagePullPolicy: Always
        image: k8s-registry:5000/cookeem/cookim:0.2.3-SNAPSHOT
        ports:
        - containerPort: 2551
          protocol: TCP
        - containerPort: 80
          protocol: TCP
        env:
        - name: "WEB_PORT"
          value: "80"
        - name: "AKKA_PORT"
          value: "2551"
        - name: "SEED_NODES"
          value: "cookim-0.cookim-headless.default.svc.cluster.local:2551"
        volumeMounts:
        - name: localtime
          mountPath: "/etc/localtime"
          readOnly: true
        - name: timezone
          mountPath: "/etc/timezone"
          readOnly: true
        resources: 
          requests:
            memory: "1Gi"
          limits:
            memory: "2Gi"



================================================
FILE: pom.xml
================================================
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
  <!-- 运行mvn clean install进行项目打包 -->

  <modelVersion>4.0.0</modelVersion>
  
  <name>CookIM</name>
  <groupId>com.cookeem</groupId>
  <artifactId>cookim</artifactId>
  <version>0.2.3-SNAPSHOT</version>


  <properties>
	<scala.version>2.11.8</scala.version>
	<scala.maven.version>2.15.2</scala.maven.version>
  </properties>

  <dependencies>
	<dependency>
		<groupId>org.scala-lang</groupId>
		<artifactId>scala-library</artifactId>
		<version>${scala.version}</version>
	</dependency>
	<dependency>
		<groupId>org.scala-lang</groupId>
		<artifactId>scala-reflect</artifactId>
		<version>${scala.version}</version>
	</dependency>
	<dependency>
		<groupId>org.scala-lang</groupId>
		<artifactId>scala-compiler</artifactId>
		<version>${scala.version}</version>
	</dependency>

	<dependency>
	    <groupId>com.typesafe.akka</groupId>
	    <artifactId>akka-actor_2.11</artifactId>
	    <version>2.5.2</version>
	</dependency>
	<dependency>
	    <groupId>com.typesafe.akka</groupId>
	    <artifactId>akka-cluster_2.11</artifactId>
	    <version>2.5.2</version>
	</dependency>
	<dependency>
	    <groupId>com.typesafe.akka</groupId>
	    <artifactId>akka-cluster-tools_2.11</artifactId>
	    <version>2.5.2</version>
	</dependency>
	<dependency>
	    <groupId>com.typesafe.akka</groupId>
	    <artifactId>akka-testkit_2.11</artifactId>
	    <version>2.5.2</version>
	</dependency>
	<dependency>
	    <groupId>com.typesafe.akka</groupId>
	    <artifactId>akka-stream_2.11</artifactId>
	    <version>2.5.2</version>
	</dependency>
	<dependency>
	    <groupId>com.typesafe.akka</groupId>
	    <artifactId>akka-stream-testkit_2.11</artifactId>
	    <version>2.5.2</version>
	</dependency>
	<dependency>
	    <groupId>com.typesafe.akka</groupId>
	    <artifactId>akka-http_2.11</artifactId>
	    <version>10.0.7</version>
	</dependency>
	<dependency>
	    <groupId>com.typesafe.akka</groupId>
	    <artifactId>akka-http-testkit_2.11</artifactId>
	    <version>10.0.7</version>
	</dependency>
	<dependency>
	    <groupId>com.typesafe.play</groupId>
	    <artifactId>play-json_2.11</artifactId>
	    <version>2.5.15</version>
	</dependency>
	<dependency>
	    <groupId>org.slf4j</groupId>
	    <artifactId>slf4j-simple</artifactId>
	    <version>1.7.25</version>
	</dependency>
	<dependency>
	    <groupId>com.sksamuel.scrimage</groupId>
	    <artifactId>scrimage-core_2.11</artifactId>
	    <version>2.1.8</version>
	</dependency>
	<dependency>
	    <groupId>com.sksamuel.scrimage</groupId>
	    <artifactId>scrimage-io-extra_2.11</artifactId>
	    <version>2.1.8</version>
	</dependency>
	<dependency>
	    <groupId>com.esotericsoftware</groupId>
	    <artifactId>kryo</artifactId>
	    <version>4.0.0</version>
	</dependency>
	<dependency>
	    <groupId>com.github.romix.akka</groupId>
	    <artifactId>akka-kryo-serialization_2.11</artifactId>
	    <version>0.5.0</version>
	</dependency>
	<dependency>
	    <groupId>commons-cli</groupId>
	    <artifactId>commons-cli</artifactId>
	    <version>1.4</version>
	</dependency>
	<dependency>
	    <groupId>io.jsonwebtoken</groupId>
	    <artifactId>jjwt</artifactId>
	    <version>0.7.0</version>
	</dependency>
	<dependency>
	    <groupId>org.reactivemongo</groupId>
	    <artifactId>reactivemongo_2.11</artifactId>
	    <version>0.12.3</version>
	</dependency>
	<dependency>
	    <groupId>org.reactivemongo</groupId>
	    <artifactId>reactivemongo-play-json_2.11</artifactId>
	    <version>0.12.3</version>
	</dependency>

  </dependencies>


<build>

  <plugins>
      <plugin>
          <groupId>org.scala-tools</groupId>
          <artifactId>maven-scala-plugin</artifactId>
          <version>${scala.maven.version}</version>
          <executions>
              <execution>
                  <id>scala-compile-first</id>
                  <goals>
                      <goal>compile</goal>
                  </goals>
                  <configuration>
                      <includes>
                          <include>**/*.scala</include>
                      </includes>
                  </configuration>
              </execution>
              <execution>
                  <id>scala-test-compile</id>
                  <goals>
                      <goal>testCompile</goal>
                  </goals>
              </execution>
          </executions>
      </plugin>
      <!-- 不能使用maven-assembly-plugin打包akka,因为会遗漏reference.conf配置,必须使用maven-shade-plugin打包插件 -->
 	<plugin>
	 <groupId>org.apache.maven.plugins</groupId>
	 <artifactId>maven-shade-plugin</artifactId>
	 <version>1.5</version>
	 <executions>
	  <execution>
	   <phase>package</phase>
	   <goals>
	    <goal>shade</goal>
	   </goals>
	   <configuration>
	    <shadedArtifactAttached>true</shadedArtifactAttached>
	    <shadedClassifierName>allinone</shadedClassifierName>
	    <artifactSet>
	     <includes>
	      <include>*:*</include>
	     </includes>
	    </artifactSet>
	    <transformers>
	      <transformer
	       implementation="org.apache.maven.plugins.shade.resource.AppendingTransformer">
	       <resource>reference.conf</resource>
	      </transformer>
	      <transformer
	       implementation="org.apache.maven.plugins.shade.resource.ManifestResourceTransformer">
	       <manifestEntries>
	        <Main-Class>akka.Main</Main-Class>
	       </manifestEntries>
	      </transformer>
	    </transformers>
	   </configuration>
	  </execution>
	 </executions>
	</plugin>

  </plugins>
</build>  
</project>


================================================
FILE: project/assembly.sbt
================================================
addSbtPlugin("com.eed3si9n" % "sbt-assembly" % "0.14.0")

================================================
FILE: project/build.properties
================================================
sbt.version=0.13.15


================================================
FILE: project/plugins.sbt
================================================
logLevel := Level.Debug


================================================
FILE: src/main/scala/com/cookeem/chat/CookIM.scala
================================================
package com.cookeem.chat

import java.net.InetAddress
import java.security.{KeyStore, SecureRandom}
import javax.net.ssl.{KeyManagerFactory, SSLContext, TrustManagerFactory}

import akka.actor.{ActorSystem, Props}
import akka.http.scaladsl.{ConnectionContext, Http, HttpsConnectionContext}
import akka.stream.ActorMaterializer
import com.cookeem.chat.common.CommonUtils._
import com.cookeem.chat.restful.Route
import com.cookeem.chat.websocket.NotificationActor
import com.typesafe.config.ConfigFactory
import org.apache.commons.cli.{DefaultParser, HelpFormatter, Options, Option => CliOption}
import com.cookeem.chat.common.CommonUtils

/**
  * Created by cookeem on 16/9/25.
  */
object CookIM extends App {
  val serverContext: ConnectionContext = if (CommonUtils.configSslSecret != "") {
    val password = CommonUtils.configSslSecret.toCharArray
    val jks = "/mykeystore.jks"
    val context = SSLContext.getInstance("TLS")
    val ks = KeyStore.getInstance("jks")
    ks.load(getClass.getResourceAsStream(jks), password)
    val keyManagerFactory = KeyManagerFactory.getInstance("SunX509")
    keyManagerFactory.init(ks, password)
    val trustManagerFactory = TrustManagerFactory.getInstance("SunX509")
    trustManagerFactory.init(ks)
    context.init(keyManagerFactory.getKeyManagers, trustManagerFactory.getTrustManagers, new SecureRandom)
    new HttpsConnectionContext(context)
  } else {
    ConnectionContext.noEncryption()
  }

  val options = new Options()
  options.addOption(
    CliOption
      .builder("n")
      .longOpt("nat")
      .desc("is nat network or in docker")
      .hasArg(false)
      .build()
  )
  options.addOption(
    CliOption
      .builder("h")
      .longOpt("host-name")
      .desc("current web service external host name")
      .hasArg()
      .required()
      .argName("HOST-NAME")
      .build()
  )
  options.addOption(
    CliOption
      .builder("w")
      .longOpt("web-port")
      .desc("web service port")
      .hasArg()
      .required()
      .argName("WEB-PORT")
      .build()
  )
  options.addOption(
    CliOption
      .builder("a")
      .longOpt("akka-port")
      .desc("akka cluster node port")
      .hasArg()
      .required()
      .argName("AKKA-PORT")
      .build()
  )
  options.addOption(
    CliOption
      .builder("s")
      .longOpt("seed-nodes")
      .desc("akka cluster seed nodes, seperate with comma, example: localhost:2551,localhost:2552")
      .hasArg()
      .required()
      .argName("SEED-NODES")
      .build()
  )
  options.addOption(
    CliOption
      .builder("m")
      .longOpt("mongo-uri")
      .desc("mongodb connection uri, example: mongodb://localhost:27017/local")
      .hasArg()
      .required(false)
      .argName("MONGO-URI")
      .build()
  )
  try {
    val parser = new DefaultParser()
    val cmd = parser.parse(options, args)
    val nat = cmd.hasOption("n")
    val hostName = cmd.getOptionValue("h")
    val webPort = cmd.getOptionValue("w").toInt
    val akkaPort = cmd.getOptionValue("a").toInt
    val seedNodes = cmd.getOptionValue("s")
    var mongoUri = cmd.getOptionValue("m")
    if (mongoUri != null) {
      configMongoUri = mongoUri
    }
    if (!(webPort > 0 && akkaPort > 0)) {
      throw CustomException("web-port and akka-port should greater than 0")
    } else if (hostName == "" || seedNodes == "") {
      throw CustomException("host-name and seed-nodes should not be empty")
    } else {
      val seedNodesStr = seedNodes.split(",").map(s => s""" "akka.tcp://chat-cluster@$s" """).mkString(",")
      val inetAddress = InetAddress.getLocalHost
      var configCluster = config
        .withFallback(ConfigFactory.parseString(s"akka.cluster.seed-nodes=[$seedNodesStr]"))
      if (!nat) {
        configCluster = configCluster
          .withFallback(ConfigFactory.parseString(s"akka.remote.netty.tcp.hostname=$hostName"))
          .withFallback(ConfigFactory.parseString(s"akka.remote.netty.tcp.port=$akkaPort"))
      } else {
        //very important in docker nat!
        //must set akka.remote.netty.tcp.bind-hostname
        //notice! akka.remote.netty.tcp.bind-port must set to akkaPort!!
        val bindHostName = inetAddress.getHostName
        configCluster = configCluster
          .withFallback(ConfigFactory.parseString(s"akka.remote.netty.tcp.hostname=$hostName"))
          .withFallback(ConfigFactory.parseString(s"akka.remote.netty.tcp.port=0"))
          .withFallback(ConfigFactory.parseString(s"akka.remote.netty.tcp.bind-hostname=$bindHostName"))
          .withFallback(ConfigFactory.parseString(s"akka.remote.netty.tcp.bind-port=$akkaPort"))
      }
      implicit val system = ActorSystem("chat-cluster", configCluster)
      implicit val materializer = ActorMaterializer()
      import system.dispatcher
      implicit val notificationActor = system.actorOf(Props(classOf[NotificationActor]))
      Http().bindAndHandle(Route.logRoute, "0.0.0.0", webPort, connectionContext = serverContext)
      consoleLog("INFO",s"CookIM server started! Access url: https://$hostName:$webPort/")
    }
  } catch {
    case e: Throwable =>
      val formatter = new HelpFormatter()
      consoleLog("ERROR", s"$e")
      formatter.printHelp("Start distributed chat cluster node.\n", options, true)
  }
}


================================================
FILE: src/main/scala/com/cookeem/chat/common/CommonUtils.scala
================================================
package com.cookeem.chat.common

import java.io.File
import java.security.MessageDigest
import java.text.SimpleDateFormat

import com.typesafe.config.ConfigFactory
import org.joda.time.DateTime
import play.api.libs.json.{JsArray, JsNumber, JsString, JsValue}

/**
  * Created by cookeem on 16/9/25.
  */
object CommonUtils {
  val config = ConfigFactory.parseFile(new File("conf/application.conf"))

  val configMongo = config.getConfig("mongodb")
  val configMongoDbname = configMongo.getString("dbname")
  var configMongoUri = configMongo.getString("uri")

  val configJwt = config.getConfig("jwt")
  val configJwtSecret = configJwt.getString("secret")

  val configSsl = config.getConfig("ssl")
  val configSslSecret = configSsl.getString("storeSecret")

  case class CustomException(message: String = "", cause: Throwable = null) extends Exception(message, cause)

  def consoleLog(logType: String, msg: String) = {
    val timeStr = new DateTime().toString("yyyy-MM-dd HH:mm:ss")
    println(s"[$logType] $timeStr: $msg")
  }

  def md5(bytes: Array[Byte]) = {
    MessageDigest.getInstance("MD5").digest(bytes).map("%02x".format(_)).mkString
  }

  def getJsonString(json: JsValue, field: String, default: String = ""): String = {
    val ret = (json \ field).getOrElse(JsString(default)).as[String]
    ret
  }

  def getJsonInt(json: JsValue, field: String, default: Int = 0): Int = {
    val ret = (json \ field).getOrElse(JsNumber(default)).as[Int]
    ret
  }

  def getJsonLong(json: JsValue, field: String, default: Long = 0L): Long = {
    val ret = (json \ field).getOrElse(JsNumber(default)).as[Long]
    ret
  }

  def getJsonDouble(json: JsValue, field: String, default: Double = 0D): Double = {
    val ret = (json \ field).getOrElse(JsNumber(default)).as[Double]
    ret
  }

  def getJsonSeq(json: JsValue, field: String, default: Seq[JsValue] = Seq[JsValue]()): Seq[JsValue] = {
    val ret = (json \ field).getOrElse(JsArray(default)).as[Seq[JsValue]]
    ret
  }

  //从参数Map中获取Int
  def paramsGetInt(params: Map[String, String], key: String, default: Int): Int = {
    var ret = default
    if (params.contains(key)) {
      try {
        ret = params(key).toInt
      } catch {
        case e: Throwable =>
      }
    }
    ret
  }

  //从参数Map中获取String
  def paramsGetString(params: Map[String, String], key: String, default: String): String = {
    var ret = default
    if (params.contains(key)) {
      ret = params(key)
    }
    ret
  }

  def sha1(str: String) = MessageDigest.getInstance("SHA-1").digest(str.getBytes).map("%02x".format(_)).mkString

  def md5(str: String) = MessageDigest.getInstance("MD5").digest(str.getBytes).map("%02x".format(_)).mkString

  def isEmail(email: String): Boolean = {
    """(?=[^\s]+)(?=(\w+)@([\w\.]+))""".r.findFirstIn(email).isDefined
  }

  def timeToStr(time: Long = System.currentTimeMillis()) = {
    val sdf = new SimpleDateFormat("MM-dd HH:mm:ss")
    sdf.format(time)
  }

  def classToMap(c: AnyRef): Map[String, String] = {
    c.getClass.getDeclaredFields.map{ f =>
      f.setAccessible(true)
      f.getName -> f.get(c).toString
    }.toMap
  }

  def trimUtf8(str: String, len: Int) = {
    var i = 0
    var strNew = ""
    str.foreach { ch =>
      if (i < len) {
        strNew = strNew + ch
      }
      var charLen = ch.toString.getBytes.length
      if (charLen > 2) {
        charLen = 2
      }
      i = i + charLen
    }
    strNew
  }

}


================================================
FILE: src/main/scala/com/cookeem/chat/demo/TestObj.scala
================================================
package com.cookeem.chat.demo

import java.util.Date

import io.jsonwebtoken.{Jwts, SignatureAlgorithm}
import io.jsonwebtoken.impl.crypto.MacProvider

import scala.collection.JavaConversions._
/**
  * Created by cookeem on 16/9/26.
  */
object TestObj extends App {
  val key = MacProvider.generateKey()
  val str = "Haijian"
  val map = Map(
    "username" -> "haijian",
    "uid" -> 1234,
    "lat" -> 12.34D,
    "lng" -> 56.78F,
    "long" -> System.currentTimeMillis(),
    "date" -> new Date(),
    "friends" -> Array(1, 2, 3, 4)
  ).asInstanceOf[Map[String, AnyRef]]
  val compactJws = Jwts
    .builder()
    .setExpiration(new Date(System.currentTimeMillis() + 120 * 1000))
    .setSubject(str)
    .setHeaderParams(map)
    .signWith(SignatureAlgorithm.HS512, key)
    .compact()

  println(Jwts.parser().setSigningKey(key).parseClaimsJws(compactJws).getBody.getSubject)
  val header = Jwts.parser().setSigningKey(key).parse(compactJws).getHeader.entrySet().map { t => (t.getKey, t.getValue)}.toMap[String, Any]


  import akka.actor._

  class TestActor extends Actor with ActorLogging {
    def receive = {
      case s: String =>
        println(s"receive $s")
      case _ =>
        log.error("Receive type error!")
    }
  }

  object TestActor {
    def props = Props[TestActor]
  }

  class TestClass {
    var name = ""

    def helloName() = {
      val system: ActorSystem = ActorSystem("system")
      val testActor = system.actorOf(TestActor.props, "test-actor")
      testActor ! name
    }
  }

  val c = new TestClass

  c.name = "haijian"

  c.helloName()
}


================================================
FILE: src/main/scala/com/cookeem/chat/event/ChatEventPackage.scala
================================================
package com.cookeem.chat.event

import akka.actor.ActorRef
import akka.util.ByteString
import com.cookeem.chat.common.CommonUtils._

/**
  * Created by cookeem on 16/11/2.
  */

//akka stream message type
sealed trait WsMessageUp {
  val uid: String
}
case class WsTextUp(uid: String, nickname: String, avatar: String, sessionid: String, sessionName: String, sessionIcon: String, msgType: String, content: String) extends WsMessageUp
case class WsBinaryUp(uid: String, nickname: String, avatar: String, sessionid: String, sessionName: String, sessionIcon: String, msgType: String, bs: ByteString, fileName: String, fileSize: Long, fileType: String) extends WsMessageUp

//akka stream message type
sealed trait WsMessageDown
case class WsTextDown(uid: String, nickname: String, avatar: String, sessionid: String, sessionName: String, sessionIcon: String, msgType: String, content: String, dateline: String = timeToStr(System.currentTimeMillis())) extends WsMessageDown
case class WsBinaryDown(uid: String, nickname: String, avatar: String, sessionid: String, sessionName: String, sessionIcon: String, msgType: String, fileName: String, fileType: String, fileid: String, thumbid: String, dateline: String = timeToStr(System.currentTimeMillis())) extends WsMessageDown

//akka stream message type
case class UserOnline(actor: ActorRef) extends WsMessageDown
case object UserOffline extends WsMessageDown

//akka cluster message type
case class ClusterText(uid: String, nickname: String, avatar: String, sessionid: String, sessionName: String, sessionIcon: String, msgType: String, content: String, dateline: String = timeToStr(System.currentTimeMillis())) extends WsMessageDown
case class ClusterBinary(uid: String, nickname: String, avatar: String, sessionid: String, sessionName: String, sessionIcon: String, msgType: String, fileName: String, fileType: String, fileid: String, thumbid: String, dateline: String = timeToStr(System.currentTimeMillis())) extends WsMessageDown

//client message type
case class ChatMessage(uid: String, nickname: String, avatar: String, msgType: String, content: String, fileName: String, fileType: String, fileid: String, thumbid: String, dateline: String = timeToStr(System.currentTimeMillis())) extends WsMessageDown
case class PushMessage(uid: String, nickname: String, avatar: String, sessionid: String, sessionName: String, sessionIcon: String, msgType: String, content: String, fileName: String, fileType: String, fileid: String, thumbid: String, dateline: String = timeToStr(System.currentTimeMillis())) extends WsMessageDown



================================================
FILE: src/main/scala/com/cookeem/chat/jwt/JwtOps.scala
================================================
package com.cookeem.chat.jwt

import java.util.Date

import com.cookeem.chat.common.CommonUtils.configJwtSecret

import io.jsonwebtoken.{Jwts, SignatureAlgorithm}

import scala.collection.JavaConversions._
/**
  * Created by cookeem on 16/11/3.
  */
object JwtOps {
  val expireMs = 15 * 60 * 1000L

  def encodeJwt(payload: Map[String, Any], expireMs: Long = expireMs): String = {
    try {
      val jwtBuilder = Jwts.builder()
        .setHeaderParams(payload.asInstanceOf[Map[String, AnyRef]])
        .signWith(SignatureAlgorithm.HS512, configJwtSecret)
      if (expireMs > 0) {
        jwtBuilder.setExpiration(new Date(System.currentTimeMillis() + expireMs))
      }
      jwtBuilder.compact()
    } catch {
      case e: Throwable =>
        ""
    }
  }

  def decodeJwt(jwtStr: String): Map[String, Any] = {
    try {
      Jwts.parser().setSigningKey(configJwtSecret).parse(jwtStr).getHeader.entrySet().map { t => (t.getKey, t.getValue)}.toMap[String, Any]
    } catch {
      case e: Throwable =>
        Map[String, Any]()
    }
  }
}


================================================
FILE: src/main/scala/com/cookeem/chat/mongo/MongoLogic.scala
================================================
package com.cookeem.chat.mongo

import java.io.{ByteArrayInputStream, File}
import java.util.Date

import akka.actor.ActorRef
import com.cookeem.chat.common.CommonUtils._
import com.cookeem.chat.event.WsTextDown
import com.cookeem.chat.jwt.JwtOps._
import com.cookeem.chat.mongo.MongoOps._
import com.sksamuel.scrimage.Image
import com.sksamuel.scrimage.nio.PngWriter
import org.apache.commons.io.FileUtils
import play.api.libs.json.JsObject
import reactivemongo.api.collections.bson.BSONCollection
import reactivemongo.bson._
import reactivemongo.play.json.BSONFormats

import scala.concurrent.Future
import scala.util.{Failure, Success}
/**
  * Created by cookeem on 16/10/28.
  */
object MongoLogic {
  val colUsersName = "users"
  val colSessionsName = "sessions"
  val colMessagesName = "messages"
  val colOnlinesName = "onlines"
  val colNotificationsName = "notifications"

  val usersCollection = cookimDB.map(_.collection[BSONCollection](colUsersName))
  val sessionsCollection = cookimDB.map(_.collection[BSONCollection](colSessionsName))
  val messagesCollection = cookimDB.map(_.collection[BSONCollection](colMessagesName))
  val onlinesCollection = cookimDB.map(_.collection[BSONCollection](colOnlinesName))
  val notificationsCollection = cookimDB.map(_.collection[BSONCollection](colNotificationsName))

  implicit def sessionStatusHandler = Macros.handler[SessionStatus]
  implicit def userHandler = Macros.handler[User]
  implicit def userStatusHandler = Macros.handler[UserStatus]
  implicit def sessionHandler = Macros.handler[Session]
  implicit def messageHandler = Macros.handler[Message]
  implicit def onlineHandler = Macros.handler[Online]
  implicit def notificationHandler = Macros.handler[Notification]

  val defaultAvatar = getDefaultAvatar

  //create users collection and index
  def createUsersCollection(): Future[String] = {
    val indexSettings = Array(
      //colName, sort, unique, expire
      ("login", 1, true, 0),
      ("nickname", 1, false, 0)
    )
    createIndex(colUsersName, indexSettings)
  }

  //create sessions collection and index
  def createSessionsCollection(): Future[String] = {
    val indexSettings = Array(
      //colName, sort, unique, expire
      ("createuid", 1, false, 0),
      ("ouid", 1, false, 0),
      ("lastUpdate", -1, false, 0)
    )
    createIndex(colSessionsName, indexSettings)
  }

  //create messages collection and index
  def createMessagesCollection(): Future[String] = {
    val indexSettings = Array(
      //colName, sort, unique, expire
      ("uid", 1, false, 0),
      ("sessionid", 1, false, 0),
      ("dateline", -1, false, 0)
    )
    createIndex(colMessagesName, indexSettings)
  }

  //create onlines collection and index
  def createOnlinesCollection(): Future[String] = {
    val indexSettings = Array(
      //colName, sort, unique, expire
      ("uid", 1, true, 0),
      ("dateline", -1, false, 15 * 60)
    )
    createIndex(colOnlinesName, indexSettings)
  }

  //create notifications collection and index
  def createNotificationsCollection(): Future[String] = {
    val indexSettings = Array(
      //colName, sort, unique, expire
      ("recvuid", 1, false, 0),
      ("dateline", -1, false, 0)
    )
    createIndex(colNotificationsName, indexSettings)
  }

  //register new user
  def registerUser(login: String, nickname: String, password: String, gender: Int): Future[(String, String, String)] = {
    var errmsg = ""
    val token = ""
    if (!isEmail(login)) {
      errmsg = "login must be email"
    } else if (nickname.getBytes.length < 4) {
      errmsg = "nickname must at least 4 charactors"
    } else if (password.length < 6) {
      errmsg = "password must at least 6 charactors"
    } else if (!(gender == 1 || gender == 2)) {
      errmsg = "gender must be boy or girl"
    }
    if (errmsg != "") {
      Future(("", token, errmsg))
    } else {
      for {
        avatar <- defaultAvatar.map { avatarMap =>
          gender match {
            case 1 => avatarMap("boy")
            case 2 => avatarMap("girl")
            case _ => avatarMap("unknown")
          }
        }
        user <- findCollectionOne[User](usersCollection, document("login" -> login))
        (uid, token, errmsg) <- {
          if (user != null) {
            errmsg = "user already exist"
            Future((user._id, token, errmsg))
          } else {
            val newUser = User("", login, nickname, sha1(password), gender, avatar)
            insertCollection[User](usersCollection, newUser).map { case (iuid, ierrmsg) =>
              if (iuid != "") {
                loginUpdate(iuid)
                createUserToken(iuid).map { token => (iuid, token, ierrmsg) }
              } else {
                Future((iuid, token, ierrmsg))
              }
            }.flatMap(f => f)
          }
        }
      } yield {
        (uid, token, errmsg)
      }
    }
  }

  def getUserInfo(uid: String): Future[User] = {
    findCollectionOne[User](usersCollection, document("_id" -> uid))
  }

  //update users info
  def updateUserInfo(uid: String, nickname: String = "", gender: Int = 0, avatarBytes: Array[Byte] = Array[Byte](), avatarFileName: String = "", avatarFileType: String = ""): Future[UpdateResult] = {
    var update = document()
    var sets = document()
    if (nickname.getBytes.length >= 4) {
      sets = sets.merge(document("nickname" -> nickname))
    }
    if (gender == 1 || gender == 2) {
      sets = sets.merge(document("gender" -> gender))
    }
    var avatarId = Future("")
    if (avatarBytes.isEmpty) {
      avatarId = gender match {
        case 1 => defaultAvatar.map(m => m("boy"))
        case 2 => defaultAvatar.map(m => m("girl"))
        case _ => defaultAvatar.map(m => m("unknown"))
      }
    } else {
      avatarId = createThumbId(uid, avatarBytes, avatarFileName, avatarFileType)
    }
    avatarId.map { avatarFileId =>
      sets = sets.merge(document("avatar" -> avatarFileId))
      update = document("$set" -> sets)
      updateCollection(usersCollection, document("_id" -> uid), update)
    }.flatMap(t => t)
  }

  def loginAction(login: String, pwd: String): Future[(String, String)] = {
    for {
      user <- findCollectionOne[User](usersCollection, document("login" -> login))
      (uid, token) <- {
        var uid = ""
        if (user != null) {
          val pwdSha1 = user.password
          if (pwdSha1 != "" && sha1(pwd) == pwdSha1) {
            uid = user._id
            loginUpdate(uid)
          }
        }
        if (uid != "") {
          createUserToken(uid).map { token => (uid, token) }
        } else {
          Future("", "")
        }
      }
    } yield {
      (uid, token)
    }
  }

  def logoutAction(userTokenStr: String): Future[UpdateResult] = {
    val userToken = verifyUserToken(userTokenStr)
    if (userToken.uid != "") {
      removeCollection(onlinesCollection, document("uid" -> userToken.uid))
    } else {
      Future(UpdateResult(n = 0, errmsg = "no privilege to logout"))
    }
  }

  //update user online status
  def updateOnline(uid: String): Future[String] = {
    val selector = document("uid" -> uid)
    for {
      online <- findCollectionOne[Online](onlinesCollection, selector)
      errmsg <- {
        if (online == null) {
          // time expire after 15 minutes
          val onlineNew = Online("", uid, new Date())
          insertCollection[Online](onlinesCollection, onlineNew).map { case (id, errmsg) =>
            errmsg
          }
        } else {
          val update = document("$set" -> document("dateline" -> new Date()))
          updateCollection(onlinesCollection, selector, update).map { ur =>
            ur.errmsg
          }
        }
      }
    } yield {
      errmsg
    }
  }

  //check and change password
  def changePwd(uid: String, oldPwd: String, newPwd: String, renewPwd: String): Future[UpdateResult] = {
    var errmsg = ""
    if (oldPwd.length < 6) {
      errmsg = "old password must at least 6 charactors"
    } else if (newPwd.length < 6) {
      errmsg = "new password must at least 6 charactors"
    } else if (newPwd != renewPwd) {
      errmsg = "new password and repeat password must be same"
    } else if (newPwd == oldPwd) {
      errmsg = "new password and old password can not be same"
    }
    if (errmsg != "") {
      Future(UpdateResult(0, errmsg))
    } else {
      val selector = document("_id" -> uid, "password" -> sha1(oldPwd))
      val update = document(
        "$set" -> document("password" -> sha1(newPwd))
      )
      updateCollection(usersCollection, selector, update).map{ ur =>
        if (ur.n == 0) {
          errmsg = "user not exist or password not match"
          UpdateResult(0, errmsg)
        } else {
          ur
        }
      }
    }
  }

  //when user login, update the loginCount and online info
  def loginUpdate(uid: String): Future[UpdateResult] = {
    for {
      onlineResult <- updateOnline(uid)
      loginResult <- {
        val selector = document("_id" -> uid)
        val update = document(
          "$inc" -> document("loginCount" -> 1)
        )
        updateCollection(usersCollection, selector, update)
      }
    } yield {
      loginResult
    }
  }

  //join new friend
  def joinFriend(uid: String, fuid: String): Future[UpdateResult] = {
    var errmsg = ""
    for {
      user <- findCollectionOne[User](usersCollection, document("_id" -> uid, "friends" -> document("$ne" -> fuid)))
      friend <- findCollectionOne[User](usersCollection, document("_id" -> fuid))
      updateResult <- {
        if (user == null) {
          errmsg = "user not exist or already your friend"
        }
        if (friend == null) {
          errmsg = "user friend not exists"
        }
        var ret = Future(UpdateResult(n = 0, errmsg = errmsg))
        if (errmsg == "") {
          val update = document("$push" -> document("friends" -> fuid))
          ret = for {
            notificationRet <- createNotification("joinFriend", uid, fuid, "")
            updateResult <- updateCollection(usersCollection, document("_id" -> uid), update)
          } yield {
            updateResult
          }
        }
        ret
      }
    } yield {
      updateResult
    }
  }

  //remove friend
  def removeFriend(uid: String, fuid: String): Future[UpdateResult] = {
    var errmsg = ""
    for {
      user <- findCollectionOne[User](usersCollection, document("_id" -> uid, "friends" -> document("$eq" -> fuid)))
      ret <- {
        if (user == null) {
          errmsg = "user not exists or friend not in your friends"
          Future(UpdateResult(n = 0, errmsg = errmsg))
        } else {
          val update = document("$pull" -> document("friends" -> fuid))
          for {
            notificationRet <- createNotification("removeFriend", uid, fuid, "")
            ret <- updateCollection(usersCollection, document("_id" -> uid), update)
          } yield {
            ret
          }
        }
      }
    } yield {
      ret
    }
  }

  def listFriends(uid: String): Future[List[User]] = {
    for {
      user <- findCollectionOne[User](usersCollection, document("_id" -> uid))
      friends <- {
        var friends = Future(List[User]())
        if (user != null) {
          val fuids = user.friends
          val selector = document(
            "_id" -> document(
              "$in" -> fuids
            )
          )
          val sort = document("nickname" -> 1)
          friends = findCollection[User](usersCollection, selector)
        }
        friends
      }
    } yield {
      friends
    }
  }

  //create a new group session
  def createGroupSession(uid: String, sessionName: String, sessionIconBytes: Array[Byte], sessionIconFileName: String, sessionIconFileType: String, publicType: Int)(implicit notificationActor: ActorRef): Future[(String, String)] = {
    var errmsg = ""
    val selector = document("_id" -> uid)
    val sessionType = 1
    for {
      user <- findCollectionOne[User](usersCollection, selector)
      (sessionid, errmsg) <- {
        if (user == null) {
          errmsg = "user not exists"
          Future("", errmsg)
        } else if (sessionName.length < 3) {
          errmsg = "session desc must at least 3 character"
          Future("", errmsg)
        } else if (!(publicType == 0 || publicType == 1)) {
          errmsg = "publicType error"
          Future("", errmsg)
        } else if (sessionIconBytes.isEmpty) {
          errmsg = "please select chat icon"
          Future("", errmsg)
        } else {
          val sessionIconId = createThumbId(uid, sessionIconBytes, sessionIconFileName, sessionIconFileType)
          sessionIconId.map { sessionIconFileId =>
            val newSession = Session("", createuid = uid, ouid = "", sessionName = sessionName, sessionIcon = sessionIconFileId, sessionType = sessionType, publicType = publicType)
            val insRet = insertCollection[Session](sessionsCollection, newSession)
            for {
              (sessionid, errormsg) <- insRet
              retJoin <- {
                var retJoin = Future(UpdateResult(n = 0, errmsg = errormsg))
                if (errormsg == "") {
                  retJoin = joinSession(uid, sessionid)
                }
                retJoin
              }
            } yield {
              retJoin
            }
            insRet
          }.flatMap(t => t)
        }
      }
    } yield {
      (sessionid, errmsg)
    }
  }

  //get edit group session info
  def getEditGroupSessionInfo(uid: String, sessionid: String): Future[Session] = {
    findCollectionOne[Session](sessionsCollection, document("_id" -> sessionid, "createuid" -> uid))
  }

  //invite friend to session
  def inviteFriend(uid: String, fuid: String, sessionid: String)(implicit notificationActor: ActorRef): Future[(String, UpdateResult)] = {
    for {
      user <- findCollectionOne[User](usersCollection, document("_id" -> uid))
      fuser <- findCollectionOne[User](usersCollection, document("_id" -> fuid))
      joinResult <- {
        var errmsg = ""
        if (user == null || fuser == null) {
          errmsg = "user or friend not exist"
        }
        if (errmsg != "") {
          Future(fuser.nickname, UpdateResult(n = 0, errmsg = errmsg))
        } else {
          joinSession(fuid, sessionid).map{ updateResult =>
            if (updateResult.errmsg == "") {
              createNotification("inviteSession", uid, fuid, sessionid)
            }
            (fuser.nickname, updateResult)
          }
        }
      }
    } yield {
      joinResult
    }
  }

  def inviteFriendsToGroupSession(uid: String, fuids: List[String], sessionid: String)(implicit notificationActor: ActorRef): Future[List[(String, UpdateResult)]] = {
    Future.sequence(
      fuids.map { fuid =>
        inviteFriend(uid, fuid, sessionid)
      }
    )
  }

  //edit group session info
  def editGroupSession(uid: String, sessionid: String, sessionName: String, sessionIconBytes: Array[Byte], sessionIconFileName: String, sessionIconFileType: String, publicType: Int): Future[String] = {
    var errmsg = ""
    for {
      user <- findCollectionOne[User](usersCollection, document("_id" -> uid))
      session <- findCollectionOne[Session](sessionsCollection, document("_id" -> sessionid))
      errmsg <- {
        if (user == null || session == null) {
          errmsg = "user or session not exists"
          Future(errmsg)
        } else if (session.createuid != uid) {
          errmsg = "you have no privilege to edit session info"
          Future(errmsg)
        } else if (sessionName.length < 3) {
          errmsg = "session desc must at least 3 character"
          Future(errmsg)
        } else if (!(publicType == 0 || publicType == 1)) {
          errmsg = "publicType error"
          Future(errmsg)
        } else {
          var sessionIconNew = Future(session.sessionIcon)
          if (sessionIconBytes.nonEmpty) {
            sessionIconNew = createThumbId(uid, sessionIconBytes, sessionIconFileName, sessionIconFileType)
          }
          sessionIconNew.map{ sessionIconFileId =>
            val update = document(
              "$set" -> document(
                "sessionName" -> sessionName,
                "sessionIcon" -> sessionIconFileId,
                "publicType" -> publicType
              )
            )
            updateCollection(sessionsCollection, document("_id" -> sessionid), update).map(_.errmsg)
          }.flatMap(t => t)
        }
      }
    } yield {
      errmsg
    }
  }

  //create private session if not exist or get private session
  def createPrivateSession(uid: String, ouid: String)(implicit notificationActor: ActorRef): Future[(String, String)] = {
    for {
      user <- findCollectionOne[User](usersCollection, document("_id" -> uid))
      ouser <- findCollectionOne[User](usersCollection, document("_id" -> ouid))
      (session, errmsgUserNotExist) <- {
        var errmsg = ""
        var ret = Future[(Session, String)](null, errmsg)
        if (user != null && ouser != null) {
          val selector = document(
            "$or" -> array(
              document("createuid" -> uid, "ouid" -> ouid),
              document("createuid" -> ouid, "ouid" -> uid)
            )
          )
          ret = findCollectionOne[Session](sessionsCollection, selector).map {s => (s, "")}
        } else {
          errmsg = "send user or recv user not exist"
          ret = Future(null, errmsg)
        }
        ret
      }
      (sessionid, errmsg) <- {
        var ret = Future("", errmsgUserNotExist)
        if (errmsgUserNotExist == "") {
          if (session != null) {
            ret = Future(session._id, "")
          } else {
            val newSession = Session("", createuid = uid, ouid = ouid, sessionName = "", sessionIcon = "", sessionType = 0, publicType = 0)
            ret = insertCollection[Session](sessionsCollection, newSession)
            for {
              (sessionid, errmsg) <- ret
              uidJoin <- {
                if (sessionid != "") {
                  joinSession(uid, sessionid)
                } else {
                  Future(UpdateResult(0, "sessionid is empty"))
                }
              }
              ouidJoin <- {
                if (sessionid != "") {
                  joinSession(ouid, sessionid)
                } else {
                  Future(UpdateResult(0, "sessionid is empty"))
                }
              }
            } yield {
            }
          }
        }
        ret
      }
    } yield {
      (sessionid, errmsg)
    }
  }

  def getUserInfoByName(nickname: String): Future[List[User]] = {
    for {
      users <- {
        var users = Future(List[User]())
        users = findCollection[User](usersCollection, document("nickname" -> nickname))
        users
      }
    } yield {
      (users)
    }
  }

  //get session info and users who join this session
  def getJoinedUsers(sessionid: String): Future[(Session, List[User])] = {
    for {
      session <- findCollectionOne[Session](sessionsCollection, document("_id" -> sessionid))
      users <- {
        var users = Future(List[User]())
        if (session != null) {
          val uids = session.usersStatus.map(_.uid)
          val selector = document("_id" -> document("$in" -> uids))
          users = findCollection[User](usersCollection, selector)
        }
        users
      }
    } yield {
      (session, users)
    }
  }

  //join new session
  def joinSession(uid: String, sessionid: String)(implicit notificationActor: ActorRef): Future[UpdateResult] = {
    var errmsg = ""
    for {
      user <- findCollectionOne[User](usersCollection, document("_id" -> uid, "sessionsStatus.sessionid" -> document("$ne" -> sessionid)))
      session <- findCollectionOne[Session](sessionsCollection, document("_id" -> sessionid))
      updateResult <- {
        if (user == null) {
          errmsg = "user not exists or already join session"
        }
        if (session == null) {
          errmsg = "session not exists"
        }
        var ret = Future(UpdateResult(n = 0, errmsg = errmsg))
        if (errmsg == "") {
          ret = for {
            ur1 <- {
              val docSessionStatus = document("sessionid" -> sessionid, "newCount" -> 0)
              val update1 = document("$push" -> document("sessionsStatus" -> docSessionStatus))
              updateCollection(usersCollection, document("_id" -> uid), update1)
            }
            ur2 <- {
              val docUserStatus = document("uid" -> uid, "online" -> false)
              val update2 = document("$push" -> document("usersStatus" -> docUserStatus))
              updateCollection(sessionsCollection, document("_id" -> sessionid), update2)
            }
          } yield {
            val nickname = user.nickname
            val avatar = user.avatar
            val sessionName = session.sessionName
            val sessionIcon = session.sessionIcon
            val msgType = "join"
            val content = s"$nickname join session $sessionName"
            val dateline = timeToStr(System.currentTimeMillis())
            notificationActor ! WsTextDown(uid, nickname, avatar, sessionid, sessionName, sessionIcon, msgType, content, dateline)
            ur2
          }
        }
        ret
      }
    } yield {
      updateResult
    }
  }

  //join a group session
  def joinGroupSession(uid: String, sessionid: String)(implicit notificationActor: ActorRef): Future[UpdateResult] = {
    for {
      session <- findCollectionOne[Session](sessionsCollection, document("_id" -> sessionid))
      updateResult <- {
        var errmsg = ""
        if (session == null) {
          errmsg = "session not exist"
        } else {
          if (session.sessionType == 0) {
            errmsg = "not join a group session"
          }
        }
        if (errmsg == "") {
          joinSession(uid, sessionid)
        } else {
          Future(UpdateResult(n = 0, errmsg = errmsg))
        }
      }
    } yield {
      updateResult
    }
  }

  //leave session
  def leaveSession(uid: String, sessionid: String)(implicit notificationActor: ActorRef): Future[UpdateResult] = {
    for {
      user <- findCollectionOne[User](usersCollection, document("_id" -> uid, "sessionsStatus.sessionid" -> sessionid))
      session <- findCollectionOne[Session](sessionsCollection, document("_id" -> sessionid, "usersStatus.uid" -> uid))
      ret <- {
        if (user == null || session == null) {
          val errmsg = "user not exists or not join the session"
          Future(UpdateResult(n = 0, errmsg = errmsg))
        } else {
          for {
            ur1 <- {
              val sessionstatus = user.sessionsStatus.filter(_.sessionid == sessionid).head
              val docSessionStatus = document("sessionid" -> sessionstatus.sessionid, "newCount" -> sessionstatus.newCount)
              val update1 = document("$pull" -> document("sessionsStatus" -> docSessionStatus))
              updateCollection(usersCollection, document("_id" -> uid), update1)
            }
            ur2 <- {
              val userstatus = session.usersStatus.filter(_.uid == uid).head
              val docUserStatus = document("uid" -> userstatus.uid, "online" -> userstatus.online)
              val update2 = document("$pull" -> document("usersStatus" -> docUserStatus))
              updateCollection(sessionsCollection, document("_id" -> sessionid), update2)
            }
          } yield {
            val nickname = user.nickname
            val avatar = user.avatar
            val sessionName = session.sessionName
            val sessionIcon = session.sessionIcon
            val msgType = "leave"
            val content = s"$nickname leave session $sessionName"
            val dateline = timeToStr(System.currentTimeMillis())
            notificationActor ! WsTextDown(uid, nickname, avatar, sessionid, sessionName, sessionIcon, msgType, content, dateline)
            ur2
          }
        }
      }
    } yield {
      ret
    }
  }

  def leaveGroupSession(uid: String, sessionid: String)(implicit notificationActor: ActorRef) = {
    for {
      session <- findCollectionOne[Session](sessionsCollection, document("_id" -> sessionid))
      updateResult <- {
        var errmsg = ""
        if (session == null) {
          errmsg = "session not exist"
        } else {
          if (session.sessionType == 0) {
            errmsg = "not a group session"
          } else if (session.createuid == uid) {
            errmsg = "creator can not leave your own session"
          }
        }
        if (errmsg == "") {
          leaveSession(uid, sessionid)
        } else {
          Future(UpdateResult(n = 0, errmsg = errmsg))
        }
      }
    } yield {
      updateResult
    }
  }

  //list public and joined session
  def listSessions(uid: String, isPublic: Boolean): Future[List[(Session, SessionStatus)]] = {
    for {
      user <- findCollectionOne[User](usersCollection, document("_id" -> uid))
      sessionInfoList <- {
        if (user != null) {
          if (isPublic) {
            val sessionids = user.sessionsStatus.map(_.sessionid)
            var ba = array()
            sessionids.foreach { sessionid =>
              ba = ba.merge(sessionid)
            }
            val selector = document(
              "publicType" -> 1,
              "sessionType" -> 1,
              "_id" -> document(
                "$nin" -> ba
              )
            )
            val sort = document("lastUpdate" -> -1)
            findCollection[Session](sessionsCollection, selector, sort = sort).map { sessions =>
              sessions.map { session =>
                val sessionStatus = user.sessionsStatus.find(_.sessionid == session._id).getOrElse(SessionStatus("", 0))
                (session, sessionStatus)
              }
            }
          } else {
            Future.sequence(
              user.sessionsStatus.map { sessionStatus =>
                findCollectionOne[Session](sessionsCollection, document("_id" -> sessionStatus.sessionid)).map { session =>
                  (session, sessionStatus)
                }
              }
            ).map { sessions => sessions.sortBy{ case (session, sessionStatus) => session.lastUpdate * -1}}
          }
        } else {
          Future(List[(Session, SessionStatus)]())
        }
      }
      sessions <- {
        Future.sequence(
          sessionInfoList.map { case (session, sessionStatus) =>
            getSessionNameIcon(uid, session._id).map { sessionToken =>
              session.sessionName = sessionToken.sessionName
              session.sessionIcon = sessionToken.sessionIcon
              (session, sessionStatus)
            }
          }
        )
      }
    } yield {
      sessions
    }
  }

  def listJoinedSessions(uid: String): Future[List[(Session, SessionStatus)]] = {
    for {
      user <- findCollectionOne[User](usersCollection, document("_id" -> uid))
      sessionInfoList <- {
        if (user != null) {
          Future.sequence(
            user.sessionsStatus.map { sessionStatus =>
              findCollectionOne[Session](sessionsCollection, document("_id" -> sessionStatus.sessionid)).map { session =>
                getSessionNameIcon(uid, session._id).map { sessionToken =>
                  session.sessionName = sessionToken.sessionName
                  session.sessionIcon = sessionToken.sessionIcon
                  (session, sessionStatus)
                }
              }.flatMap(t => t)
            }
          ).map{ sessions =>
            sessions.sortBy{ case (session, sessionStatus) => session.lastUpdate * -1 }
          }
        } else {
          Future(List[(Session, SessionStatus)]())
        }
      }
    } yield {
      sessionInfoList
    }
  }

  def getNewNotificationCount(uid: String): Future[(Int, String)] = {
    for {
      user <- findCollectionOne[User](usersCollection, document("_id" -> uid))
      (rsCount, errmsg) <- {
        if (user != null) {
          countCollection(notificationsCollection, document("recvuid" -> uid, "isRead" -> 0)).map { rsCount =>
            (rsCount, "")
          }
        } else {
          Future(0, "user not exists")
        }
      }
    } yield {
      (rsCount, errmsg)
    }
  }

  //verify user is in session
  def verifySession(senduid: String, sessionid: String): Future[String] = {
    for {
      user <- findCollectionOne[User](usersCollection, document("_id" -> senduid, "sessionsStatus.sessionid" -> sessionid))
      session <- findCollectionOne[Session](sessionsCollection, document("_id" -> sessionid, "usersStatus.uid" -> senduid))
    } yield {
      if (user != null && session != null) {
        ""
      } else {
        "no privilege in this session"
      }
    }
  }

  //create a new message
  def createMessage(uid: String, sessionid: String, msgType: String, content: String = "", fileName: String = "", fileType: String = "", fileid: String = "", thumbid: String = ""): Future[(String, String)] = {
    val message = Message("", uid, sessionid, msgType, content, fileName, fileType, fileid, thumbid)
    for {
      (msgid, errmsg) <- insertCollection[Message](messagesCollection, message)
      session <- {
        if (msgid != "") {
          findCollectionOne[Session](sessionsCollection, document("_id" -> sessionid))
        } else {
          Future(null)
        }
      }
      updateLastMsgId <- {
        if (session != null) {
          val selector = document("_id" -> sessionid)
          val update = document("$set" ->
            document(
              "lastMsgid" -> msgid,
              "lastUpdate" -> System.currentTimeMillis()
            )
          )
          updateCollection(sessionsCollection, selector, update)
        } else {
          Future(UpdateResult(n = 0, errmsg = "nothing to update"))
        }
      }
      updateNewCounts <- {
        if (session != null) {
          Future.sequence(
            //update not online users newCount
            session.usersStatus.filterNot(_.online).map { userstatus =>
              //update userstatus nest array
              val selector = document(
                "_id" -> userstatus.uid,
                "sessionsStatus.sessionid" -> sessionid
              )
              val update = document(
                "$inc" -> document(
                  "sessionsStatus.$.newCount" -> 1
                )
              )
              updateCollection(usersCollection, selector, update)
            }
          )
        } else {
          Future(List[UpdateResult]())
        }
      }
    } yield {
      (msgid, errmsg)
    }
  }

  def createNotification(noticeType: String, senduid: String, recvuid: String, sessionid: String): Future[(String, String)] = {
    var errmsg = ""
    if (senduid == "" || recvuid == "") {
      errmsg = "senduid or recvuid is empty"
    } else if (noticeType != "joinFriend" && noticeType != "removeFriend" && noticeType != "inviteSession") {
      errmsg = "noticeType error"
    } else if (noticeType == "inviteSession" && sessionid == "") {
      errmsg = "inviteSession must provide sessionid"
    }
    if (errmsg != "") {
      Future("", errmsg)
    } else {
      val notificationNew = Notification("", noticeType, senduid, recvuid, sessionid)
      insertCollection[Notification](notificationsCollection, notificationNew)
    }
  }

  def listNotifications(uid: String, page: Int = 10, count: Int = 1) = {
    val selector = document("recvuid" -> uid)
    val sort = document("dateline" -> -1)
    for {
      notifications <- findCollection[Notification](notificationsCollection, selector, sort = sort, page = page, count = count)
      results <- {
        Future.sequence(
          notifications.map { notification =>
            val senduserFuture = findCollectionOne[User](usersCollection, document("_id" -> notification.senduid))
            var sessionFuture: Future[Session] = Future(null)
            if (notification.sessionid != "") {
              sessionFuture = findCollectionOne[Session](sessionsCollection, document("_id" -> notification.sessionid))
            }
            for {
              updateResult <- updateCollection(notificationsCollection, document("_id" -> notification._id), document("$set" -> document("isRead" -> 1)))
              senduser <- senduserFuture
              session <- sessionFuture
            } yield {
              (notification, senduser, session)
            }
          }
        )
      }
    } yield {
      results
    }
  }

  def userOnlineOffline(uid: String, sessionid: String, isOnline: Boolean): Future[UpdateResult] = {
    val selector = document(
      "_id" -> sessionid,
      "usersStatus.uid" -> uid
    )
    val update = document(
      "$set" -> document(
        "usersStatus.$.online" -> isOnline
      )
    )
    updateCollection(sessionsCollection, selector, update)
  }

  def getSessionLastMessage(userTokenStr: String, sessionid: String): Future[(Session, Message, User)] = {
    val UserToken(uid, nickname, avatar) = verifyUserToken(userTokenStr)
    if (uid != "") {
      for {
        session <- findCollectionOne[Session](sessionsCollection, document("_id" -> sessionid))
        message <- {
          if (session != null) {
            findCollectionOne[Message](messagesCollection, document("_id" -> session.lastMsgid))
          } else {
            null
          }
        }
        user <- {
          if (message != null) {
            findCollectionOne[User](usersCollection, document("_id" -> message.uid))
          } else {
            Future(null)
          }
        }
      } yield {
        (session, message, user)
      }
    } else {
      Future(null, null, null)
    }
  }

  //list history messages
  def listHistoryMessages(uid: String, sessionid: String, page: Int = 1, count: Int = 10, sort: BSONDocument): Future[(String, List[(Message, User)])] = {
    for {
      errmsg <- verifySession(uid, sessionid)
      messages <- {
        var messages = Future(List[Message]())
        if (errmsg == "") {
          messages = findCollection[Message](messagesCollection, document("sessionid" -> sessionid), sort = sort, page = page, count = count)
        }
        messages
      }
      updateNewCount <- {
        if (messages.nonEmpty) {
          val selector = document(
            "_id" -> uid,
            "sessionsStatus.sessionid" -> sessionid
          )
          val update = document(
            "$set" -> document(
              "sessionsStatus.$.newCount" -> 0
            )
          )
          updateCollection(usersCollection, selector, update)
        } else {
          Future(UpdateResult(n = 0, errmsg = "nothing to update"))
        }
      }
      listMessageUser <- {
        Future.sequence(
          messages.map { message =>
            findCollectionOne[User](usersCollection, document("_id" -> message.uid)).map { user =>
              (message, user)
            }
          }
        )
      }
    } yield {
      (errmsg, listMessageUser)
    }
  }

  //create user token, include uid, nickname, avatar
  def createUserToken(uid: String): Future[String] = {
    for {
      user <- findCollectionOne[User](usersCollection, document("_id" -> uid))
      onlineUpdate <- {
        if (user != null) {
          updateOnline(uid)
        } else {
          Future("online not update")
        }
      }
      updateLastLogin <- {
        if (user != null) {
          updateCollection(
            usersCollection,
            document("_id" -> uid),
            document("$set" -> document("lastLogin" -> System.currentTimeMillis()))
          )
        } else {
          Future(UpdateResult(n = 0, errmsg = "nothing to update"))
        }
      }
    } yield {
      var token = ""
      if (user != null) {
        val payload = Map[String, Any](
          "uid" -> user._id,
          "nickname" -> user.nickname,
          "avatar" -> user.avatar
        )
        token = encodeJwt(payload)
      }
      token
    }
  }

  def verifyUserToken(token: String): UserToken = {
    var userToken = UserToken("", "", "")
    val mapUserToken = decodeJwt(token)
    if (mapUserToken.contains("uid") && mapUserToken.contains("nickname") && mapUserToken.contains("avatar")) {
      val uid = mapUserToken("uid").asInstanceOf[String]
      val nickname = mapUserToken("nickname").asInstanceOf[String]
      val avatar = mapUserToken("avatar").asInstanceOf[String]
      if (uid != "" && nickname != "" && avatar != "") {
        userToken = UserToken(uid, nickname, avatar)
      }
    }
    userToken
  }

  //create session token, include sessionid
  def createSessionToken(uid: String, sessionid: String): Future[String] = {
    for {
      errmsg <- verifySession(uid, sessionid)
      sessionToken <- {
        if (errmsg == "") {
          findCollectionOne[Session](sessionsCollection, document("_id" -> sessionid)).map { session =>
            if (session != null) {
              SessionToken(sessionid, session.sessionName, session.sessionIcon)
            } else {
              SessionToken("", "", "")
            }
          }
        } else {
          Future(SessionToken("", "", ""))
        }
      }
    } yield {
      var token = ""
      if (sessionToken.sessionid != "") {
        val payload = Map[String, Any](
          "sessionid" -> sessionToken.sessionid,
          "sessionName" -> sessionToken.sessionName,
          "sessionIcon" -> sessionToken.sessionIcon
        )
        token = encodeJwt(payload)
      }
      token
    }
  }

  def verifySessionToken(token: String): SessionToken = {
    var sessionToken = SessionToken("", "", "")
    val mapSessionToken = decodeJwt(token)
    if (mapSessionToken.contains("sessionid")) {
      val sessionid = mapSessionToken("sessionid").asInstanceOf[String]
      val sessionName = mapSessionToken("sessionName").asInstanceOf[String]
      val sessionIcon = mapSessionToken("sessionIcon").asInstanceOf[String]
      if (sessionid != "") {
        sessionToken = SessionToken(sessionid, sessionName, sessionIcon)
      }
    }
    sessionToken
  }

  def verifyUserSessionToken(userTokenStr: String, sessionTokenStr: String): UserSessionInfo = {
    val userToken = verifyUserToken(userTokenStr)
    val sessionToken = verifySessionToken(sessionTokenStr)
    if (userToken.uid != "" && userToken.nickname != "" && userToken.avatar != "" && sessionToken.sessionid != "") {
      UserSessionInfo(userToken.uid, userToken.nickname, userToken.avatar, sessionToken.sessionid, sessionToken.sessionName, sessionToken.sessionIcon)
    } else {
      UserSessionInfo("", "", "", "", "", "")
    }
  }

  def getSessionNameIcon(uid: String, sessionid: String): Future[SessionToken] = {
    for {
      session <- findCollectionOne[Session](sessionsCollection, document("_id" -> sessionid))
      sessionToken <- {
        var futureSessionToken = Future(SessionToken("", "", ""))
        if (session != null) {
          if (session.sessionType == 1) {
            //group session
            futureSessionToken = Future(SessionToken(session._id, session.sessionName, session.sessionIcon))
          } else {
            //private session
            if (session.usersStatus.nonEmpty) {
              val ouid = session.usersStatus.filter(_.uid != uid).map(_.uid).head
              futureSessionToken = findCollectionOne[User](usersCollection, document("_id" -> ouid)).map { ouser =>
                if (ouser != null) {
                  SessionToken(session._id, ouser.nickname, ouser.avatar)
                } else {
                  SessionToken("", "", "")
                }
              }
            }
          }
        }
        futureSessionToken
      }
    } yield {
      sessionToken
    }
  }

  def getSessionHeader(uid: String, sessionid: String): Future[(Session, SessionToken)] = {
    for {
      session <- findCollectionOne[Session](sessionsCollection, document("_id" -> sessionid))
      sessionToken <- getSessionNameIcon(uid, sessionid)
    } yield {
      (session, sessionToken)
    }
  }

  def getSessionMenu(uid: String, sessionid: String): Future[(Session, Boolean, Boolean)] = {
    for {
      session <- findCollectionOne[Session](sessionsCollection, document("_id" -> sessionid))
      user <- findCollectionOne[User](usersCollection, document("_id" -> uid))
    } yield {
      if (session != null && user != null) {
        val joined = session.usersStatus.map(_.uid).contains(uid)
        val editable = session.createuid == uid
        (session, joined, editable)
      } else {
        (null, false, false)
      }
    }
  }

  def getUserMenu(uid: String, ouid: String): Future[(User, Boolean)] = {
    for {
      user <- findCollectionOne[User](usersCollection, document("_id" -> uid))
      ouser <- findCollectionOne[User](usersCollection, document("_id" -> ouid))
    } yield {
      if (user != null && ouser != null) {
        val isFriend = user.friends.contains(ouid)
        (ouser, isFriend)
      } else {
        (null, false)
      }
    }
  }

  def generateNewGroupSession(uid: String, friends: List[String]): Future[(String, List[String])] = {
    val uids = (uid +: friends).take(4)
    for {
      users <- findCollection[User](usersCollection, document("_id" -> document("$in" -> uids)))
    } yield {
      val sessionName = users.map(_.nickname).mkString(", ").take(30)
      val sessionIcons = users.map(_.avatar)
      (sessionName, sessionIcons)
    }
  }

  def writeGridFile(uid: String, bytes: Array[Byte], fileName: String, fileType: String): Future[String] = {
    val metadata = document("uid" -> uid)
    saveGridFile(bytes = bytes, fileName = fileName, contentType = fileType, metaData = metadata).map { case (id, errmsg) =>
      id match {
        case bsid: BSONObjectID => bsid.stringify
        case _ => ""
      }
    }
  }

  def getGridFile(bsid: String): Future[(String, String, Long, BSONDocument, Array[Byte], String)] = {
    readGridFile(bsid)
  }

  def getGridFileMetaData(bsid: String): Future[(BSONValue, String, String, Long, BSONDocument, String)] = {
    getGridFileMetaById(bsid)
  }

  def getDefaultAvatar: Future[Map[String, String]] = {
    for {
      (idBoy, fileNameBoy, fileTypeBoy, fileSizeBoy, fileMetaDataBoy, errmsgBoy) <- getGridFileMeta(document("metadata" -> document("avatar" -> "boy")))
      bsidBoy <- {
        if (fileNameBoy == "") {
          val bytes = FileUtils.readFileToByteArray(new File("www/images/avatar/boy.jpg"))
          saveGridFile(bytes, fileName = "boy.jpg", contentType = "image/jpeg", metaData = document("avatar" -> "boy")).map(_._1)
        } else {
          Future(idBoy)
        }
      }

      (idGirl, fileNameGirl, fileTypeGirl, fileSizeGirl, fileMetaDataGirl, errmsgGirl) <- getGridFileMeta(document("metadata" -> document("avatar" -> "girl")))
      bsidGirl <- {
        if (fileNameGirl == "") {
          val bytes = FileUtils.readFileToByteArray(new File("www/images/avatar/girl.jpg"))
          saveGridFile(bytes, fileName = "girl.jpg", contentType = "image/jpeg", metaData = document("avatar" -> "girl")).map(_._1)
        } else {
          Future(idGirl)
        }
      }

      (idUnknown, fileNameUnknown, fileTypeUnknown, fileSizeUnknown, fileMetaDataUnknown, errmsgUnknown) <- getGridFileMeta(document("metadata" -> document("avatar" -> "unknown")))
      bsidUnknown <- {
        if (fileNameUnknown == "") {
          val bytes = FileUtils.readFileToByteArray(new File("www/images/avatar/unknown.jpg"))
          saveGridFile(bytes, fileName = "unknown.jpg", contentType = "image/jpeg", metaData = document("avatar" -> "unknown")).map(_._1)
        } else {
          Future(idUnknown)
        }
      }
    } yield {
      var idBoyStr = ""
      var idGirlStr = ""
      var idUnknownStr = ""
      bsidBoy match {
        case bsid: BSONObjectID =>
          idBoyStr = bsid.stringify
        case _ =>
      }
      bsidGirl match {
        case bsid: BSONObjectID =>
          idGirlStr = bsid.stringify
        case _ =>
      }
      bsidUnknown match {
        case bsid: BSONObjectID =>
          idUnknownStr = bsid.stringify
        case _ =>
      }
      Map(
        "boy" -> idBoyStr,
        "girl" -> idGirlStr,
        "unknow" -> idUnknownStr
      )
    }
  }

  def createThumbId(uid: String, bytes: Array[Byte], fileName: String, fileType: String): Future[String] = {
    var futureThumbid = Future("")
    try {
      if (fileType == "image/jpeg" || fileType == "image/gif" || fileType == "image/png") {
        //resize image
        implicit val writer = PngWriter.NoCompression
        val bytesImage = Image.fromStream(new ByteArrayInputStream(bytes)).bound(200, 200).bytes
        futureThumbid = writeGridFile(uid, bytesImage, s"$fileName.thumb.png", "image/png")
      }
    } catch { case e: Throwable =>
      consoleLog("ERROR", s"create thumb error: $e")
    }
    futureThumbid
  }
}


================================================
FILE: src/main/scala/com/cookeem/chat/mongo/MongoOps.scala
================================================
package com.cookeem.chat.mongo

import com.cookeem.chat.common.CommonUtils._
import java.util.concurrent.Executors

import play.api.libs.iteratee.{Enumerator, Iteratee}
import reactivemongo.api.collections.bson.BSONCollection
import reactivemongo.api.commands.Command
import reactivemongo.api.commands.Command.CommandWithPackRunner
import reactivemongo.api._
import reactivemongo.api.gridfs.{DefaultFileToSave, GridFS}
import reactivemongo.api.gridfs.Implicits._
import reactivemongo.bson._

import scala.concurrent.{ExecutionContext, ExecutionContextExecutor, Future}
import scala.util.{Failure, Success}

/**
  * Created by cookeem on 16/10/27.
  */
object MongoOps {

  implicit val ec: ExecutionContextExecutor = ExecutionContext.fromExecutor(Executors.newFixedThreadPool(50))

  val dbName = configMongoDbname
  val mongoUri = configMongoUri
  val driver = MongoDriver()
  val parsedUri = MongoConnection.parseURI(mongoUri)
  val connection = parsedUri.map(driver.connection)
  val futureConnection = Future.fromTry(connection)
  val cookimDB = futureConnection.map(_.database(dbName)).flatMap(f => f)

  //create collection and index
  /**
  * @param colName: String, collection name to create
  * @param indexSettings: Array[(indexField: String, sort: Int, unique: Boolean, expireAfterSeconds: Int)], index setting
  * @return Future[errmsg: String], if no error, errmsg is empty
  */
  def createIndex(colName: String, indexSettings: Array[(String, Int, Boolean, Int)]): Future[String] = {
    var errmsg = ""
    var indexSettingDoc = array()
    indexSettings.foreach { case (indexCol, indexMode, unique, expireAfterSeconds) =>
      if (expireAfterSeconds > 0) {
        indexSettingDoc = indexSettingDoc.add(
          document(
            "key" -> document(indexCol -> indexMode),
            "name" -> s"index-$colName-$indexCol",
            "unique" -> unique,
            "expireAfterSeconds" -> expireAfterSeconds
          )
        )
      } else {
        indexSettingDoc = indexSettingDoc.add(
          document(
            "key" -> document(indexCol -> indexMode),
            "name" -> s"index-$colName-$indexCol",
            "unique" -> unique
          )
        )
      }
    }
    val createResult = for {
      db <- cookimDB
      doc <- {
        val runner: CommandWithPackRunner[BSONSerializationPack.type] = Command.run(BSONSerializationPack, FailoverStrategy.default)
        val commandDoc = document(
          "createIndexes" -> colName,
          "indexes" -> indexSettingDoc
        )
        runner(db, runner.rawCommand(commandDoc)).one[BSONDocument](ReadPreference.Primary)
      }
    } yield {
      if (doc.get("errmsg").isDefined) {
        errmsg = doc.getAs[String]("errmsg").getOrElse("")
      } else {
        errmsg = ""
      }
      errmsg
    }
    createResult.recover { case e: Throwable =>
        s"create index error: $e"
    }
  }

  //insert single document into collection
  /**
    * @param futureCollection: Future[BSONCollection], collection to insert
    * @param record: T, record is BaseMongoObj
    * @return Future[(id: String, errmsg: String)], inserted id string and errmsg
    */
  def insertCollection[T <: BaseMongoObj](futureCollection: Future[BSONCollection], record: T)(implicit handler: BSONDocumentReader[T] with BSONDocumentWriter[T] with BSONHandler[BSONDocument, T]): Future[(String, String)] = {
    val recordIns = record
    recordIns._id = BSONObjectID.generate().stringify
    val insertResult = for {
      col <- futureCollection
      wr <- col.insert[T](recordIns)
    } yield {
      var errmsg = ""
      var id = ""
      if (wr.ok) {
        id = recordIns._id
      } else {
        errmsg = s"insert ${record.getClass} record error"
      }
      (id, errmsg)
    }
    insertResult.recover { case e: Throwable =>
      ("", s"insert ${record.getClass} record error: $e")
    }
  }

  def bulkInsertCollection[T <: BaseMongoObj](futureCollection: Future[BSONCollection], records: List[T])(implicit handler: BSONDocumentReader[T] with BSONDocumentWriter[T] with BSONHandler[BSONDocument, T]) = {
    val recordsIns = records.map { record =>
      val recordIns = record
      recordIns._id = BSONObjectID.generate().stringify
      recordIns
    }
    val bulkResult = for {
      col <- futureCollection
      mwr <- {
        val docs = recordsIns.map(implicitly[col.ImplicitlyDocumentProducer](_))
        col.bulkInsert(ordered = false)(docs: _*)
      }
    } yield {
      UpdateResult(n = mwr.n, errmsg = mwr.errmsg.getOrElse(""))
    }
    bulkResult.recover { case e: Throwable =>
      ("", s"bulk insert records error: $e")
    }
  }

  //find in collection can return multiple records
  /**
    * @param futureCollection: Future[BSONCollection], collection to insert
    * @param selector: BSONDocument, filter
    * @param count = -1: Int, return record count
    * @param sort: BSONDocument = document(), sort
    * @return Future[List[T] ], return the record list
    */
  def findCollection[T <: BaseMongoObj](futureCollection: Future[BSONCollection], selector: BSONDocument, count: Int = -1, page: Int = 1, sort: BSONDocument = document())(implicit handler: BSONDocumentReader[T] with BSONDocumentWriter[T] with BSONHandler[BSONDocument, T]): Future[List[T]] = {
    var queryOpts = QueryOpts()
    if (count > 0 && page > 0) {
      queryOpts = QueryOpts(skipN = (page - 1) * count)
    }
    val findResult = for {
      col <- futureCollection
      rs <- col.find(selector).options(queryOpts).sort(sort).cursor[T]().collect(count, Cursor.FailOnError[List[T]]())
    } yield {
      rs
    }
    findResult.recover { case e: Throwable =>
      List[T]()
    }
  }

  //find in collection return one record
  /**
    * @param futureCollection: Future[BSONCollection], collection to insert
    * @param selector: BSONDocument, filter
    * @return Future[T], return the record, if not found return null
    */
  def findCollectionOne[T <: BaseMongoObj](futureCollection: Future[BSONCollection], selector: BSONDocument)(implicit handler: BSONDocumentReader[T] with BSONDocumentWriter[T] with BSONHandler[BSONDocument, T]): Future[T] = {
    val findResult: Future[T] = for {
      col <- futureCollection
      rs <- col.find(selector).cursor[T]().collect(1, Cursor.FailOnError[List[T]]())
    } yield {
      rs.headOption.getOrElse(null.asInstanceOf[T])
    }
    findResult.recover { case e: Throwable =>
      null.asInstanceOf[T]
    }
  }

  //count in collection
  /**
    * @param futureCollection: Future[BSONCollection], collection to count
    * @param selector: BSONDocument, filter
    * @return Future[Int], return record count
    */
  def countCollection(futureCollection: Future[BSONCollection], selector: BSONDocument): Future[Int] = {
    val countResult: Future[Int] = for {
      col <- futureCollection
      rsCount <- col.count(Some(selector))
    } yield {
      rsCount
    }
    countResult.recover { case e: Throwable =>
      0
    }
  }

  //update in collection
  /**
    * @param futureCollection: Future[BSONCollection], collection to update
    * @param selector: BSONDocument, filter
    * @param update: BSONDocument, update info
    * @param multi: Boolean = false, update multi records
    * @return Future[UpdateResult], return the update result
    */
  def updateCollection(futureCollection: Future[BSONCollection], selector: BSONDocument, update: BSONDocument, multi: Boolean = false): Future[UpdateResult] = {
    val updateResult = for {
      col <- futureCollection
      uwr <- col.update(selector, update, multi = multi)
    } yield {
      UpdateResult(
        n = uwr.nModified,
        errmsg = uwr.errmsg.getOrElse("")
      )
    }
    updateResult.recover { case e: Throwable =>
      UpdateResult(
        n = 0,
        errmsg = s"update collection error: $e"
      )
    }
  }

  //remove in collection
  /**
    * @param futureCollection: Future[BSONCollection], collection to update
    * @param selector: BSONDocument, filter
    * @param firstMatchOnly: Boolean = false, only remove fisrt match record
    * @return Future[UpdateResult], return the update result
    */
  def removeCollection(futureCollection: Future[BSONCollection], selector: BSONDocument, firstMatchOnly: Boolean = false): Future[UpdateResult] = {
    val removeResult = for {
      col <- futureCollection
      wr <- col.remove[BSONDocument](selector, firstMatchOnly = firstMatchOnly)
    } yield {
      UpdateResult(
        n = wr.n,
        errmsg = wr.writeErrors.map(_.errmsg).mkString
      )
    }
    removeResult.recover { case e: Throwable =>
      UpdateResult(
        n = 0,
        errmsg = s"remove collection item error: $e"
      )
    }
  }

  //save grid file in mongodb database
  /**
    * @param bytes: Array[Byte], file bytes
    * @param fileName: String, file display name
    * @param contentType: String, content mime type
    * @param metaData: BSONDocument = document(), file metadata
    * @return Future[(BSONValue, errmsg)], return (id, errmsg)
    */
  def saveGridFile(bytes: Array[Byte], fileName: String, contentType: String, metaData: BSONDocument = document()): Future[(BSONValue, String)] = {
    val saveGridFileResult = for {
      db <- cookimDB
      readFile <- {
        val gridfs = GridFS[BSONSerializationPack.type](db)
        val data = Enumerator(bytes)
        val gridfsObj = DefaultFileToSave(filename = Some(fileName), contentType = Some(contentType), metadata = metaData)
        gridfs.saveWithMD5(data, gridfsObj)
      }
    } yield {
      (readFile.id, "")
    }
    saveGridFileResult.recover { case e: Throwable =>
      val errmsg = s"save grid file error: fileName = $fileName, contentType = $contentType, $e"
      (BSONNull, errmsg)
    }
  }

  //read grid file in mongodb database
  /**
    * @param bsid: String, _id
    * @return Future[(String, String, Long, BSONDocument, Array[Byte], String)]
    *         return the grid file info: (fileName, fileType, fileSize, fileMetaData, fileBytes, errmsg)
    */
  def readGridFile(bsid: String): Future[(String, String, Long, BSONDocument, Array[Byte], String)] = {
    BSONObjectID.parse(bsid) match {
      case Success(id) =>
        val readGridFileResult = for {
          db <- cookimDB
          bsonFile <- {
            val gridfs = GridFS[BSONSerializationPack.type](db)
            gridfs.find(document("_id" -> id)).head
          }
          bytes <- {
            val gridfs = GridFS[BSONSerializationPack.type](db)
            val enumerate = gridfs.enumerate(bsonFile)
            val sink = Iteratee.consume[Array[Byte]]()
            enumerate |>>> sink
          }
        } yield {
          (bsonFile.filename.getOrElse(""), bsonFile.contentType.getOrElse(""), bsonFile.length, bsonFile.metadata, bytes, "")
        }
        readGridFileResult.recover { case e: Throwable =>
          val errmsg = s"read grid file error: bsid = $bsid, $e"
          ("", "", 0L, document(), Array[Byte](), errmsg)
        }

      case Failure(e) =>
        val errmsg = s"read grid file error: bsid = $bsid, $e"
        Future("", "", 0L, document(), Array[Byte](), errmsg)
    }
  }

  //get grid file meta data in mongodb database
  /**
    * @param selector: BSONDocument, selector filter
    * @return Future[(BSONValue, String, String, Long, BSONDocument, String)]
    *         return the grid file info: (id, fileName, fileType, fileSize, fileMetaData, errmsg)
    */
  def getGridFileMeta(selector: BSONDocument): Future[(BSONValue, String, String, Long, BSONDocument, String)] = {
    val getGridFileResult = for {
      db <- cookimDB
      bsonFile <- {
        val gridfs = GridFS[BSONSerializationPack.type](db)
        gridfs.find(selector).head
      }
    } yield {
      (bsonFile.id, bsonFile.filename.getOrElse(""), bsonFile.contentType.getOrElse(""), bsonFile.length, bsonFile.metadata, "")
    }
    getGridFileResult.recover { case e: Throwable =>
      val errmsg = s"get grid file meta error: selector = $selector, $e"
      (BSONNull, "", "", 0L, document(), errmsg)
    }
  }

  //get grid file meta data by id in mongodb database
  /**
    * @param bsid: String, _id
    * @return Future[(BSONValue, String, String, Long, BSONDocument, String)]
    *         return the grid file info: (id, fileName, fileType, fileSize, fileMetaData, errmsg)
    */
  def getGridFileMetaById(bsid: String): Future[(BSONValue, String, String, Long, BSONDocument, String)] = {
    BSONObjectID.parse(bsid) match {
      case Success(id) =>
        getGridFileMeta(document("_id" -> id))
      case Failure(e) =>
        val errmsg = s"read grid file meta error: bsid = $bsid, $e"
        Future(BSONNull, "", "", 0L, document(), errmsg)
    }
  }


}


================================================
FILE: src/main/scala/com/cookeem/chat/mongo/package.scala
================================================
package com.cookeem.chat

import java.util.Date

/**
  * Created by cookeem on 16/11/1.
  */
package object mongo {
  //mongoDB schema
  trait BaseMongoObj { var _id: String }
  case class User(var _id: String, login: String, nickname: String, password: String, gender: Int, avatar: String, lastLogin: Long = 0, loginCount: Int = 0, sessionsStatus: List[SessionStatus] = List(), friends: List[String] = List(), dateline: Long = System.currentTimeMillis()) extends BaseMongoObj
  case class SessionStatus(sessionid: String, newCount: Int)
  case class Session(var _id: String, createuid: String, ouid: String, var sessionName: String, var sessionIcon: String, sessionType: Int, publicType: Int, usersStatus: List[UserStatus] = List(), lastMsgid: String = "", lastUpdate: Long = System.currentTimeMillis(), dateline: Long = System.currentTimeMillis()) extends BaseMongoObj
  case class UserStatus(uid: String, online: Boolean)
  case class Message(var _id: String, uid: String, sessionid: String, msgType: String, content: String = "", fileName: String = "", fileType: String = "", fileid: String = "", thumbid: String = "", dateline: Long = System.currentTimeMillis()) extends BaseMongoObj
  case class Online(var _id: String, uid: String, dateline: Date = new Date()) extends BaseMongoObj
  case class Notification(var _id: String, noticeType: String, senduid: String, recvuid: String, sessionid: String, isRead: Int = 0, dateline: Long = System.currentTimeMillis()) extends BaseMongoObj

  //mongoDB update result
  case class UpdateResult(n: Int, errmsg: String)

  //user and session token info
  case class UserToken(uid: String, nickname: String, avatar: String)
  case class SessionToken(sessionid: String, sessionName: String, sessionIcon: String)
  case class UserSessionInfo(uid: String, nickname: String, avatar: String, sessionid: String, sessionName: String, sessionIcon: String)

}


================================================
FILE: src/main/scala/com/cookeem/chat/restful/Controller.scala
================================================
package com.cookeem.chat.restful

import java.io.File

import akka.actor.ActorRef
import com.cookeem.chat.common.CommonUtils._
import com.cookeem.chat.event.ChatMessage
import com.cookeem.chat.mongo.MongoLogic._
import com.sksamuel.scrimage.{Color, Image}
import com.sksamuel.scrimage.nio.PngWriter
import play.api.libs.json._
import reactivemongo.bson._

import scala.concurrent.{ExecutionContext, Future}

/**
  * Created by cookeem on 16/11/2.
  */
object Controller {
  def registerUserCtl(login: String, nickname: String, password: String, repassword: String, gender: Int)(implicit ec: ExecutionContext): Future[JsObject] = {
    if (password != repassword) {
      Future {
        Json.obj(
          "uid" -> "",
          "errmsg" -> s"password and repassword must be same",
          "successmsg" -> "",
          "userToken" -> ""
        )
      }
    } else {
      registerUser(login, nickname, password, gender).map { case (uid, userTokenStr, errmsg) =>
        var successmsg = ""
        if (uid != "") {
          successmsg = "register user success, thank you for join us"
        }
        Json.obj(
          "uid" -> uid,
          "errmsg" -> errmsg,
          "successmsg" -> successmsg,
          "userToken" -> userTokenStr
        )
      }
    }
  }

  def createUserTokenCtl(userTokenStr: String)(implicit ec: ExecutionContext): Future[JsObject] = {
    val userToken = verifyUserToken(userTokenStr)
    if (userToken.uid == "") {
      Future(
        Json.obj(
          "errmsg" -> "no privilege to create user token",
          "uid" -> "",
          "userToken" -> ""
        )
      )
    } else {
      val uid = userToken.uid
      createUserToken(uid).map { newUserTokenStr =>
        if (newUserTokenStr == "") {
          Json.obj(
            "errmsg" -> "no privilege to create user token",
            "uid" -> "",
            "userToken" -> ""
          )
        } else {
          Json.obj(
            "errmsg" -> "",
            "uid" -> uid,
            "userToken" -> newUserTokenStr
          )
        }
      }
    }
  }

  def createSessionTokenCtl(userTokenStr: String, sessionid: String)(implicit ec: ExecutionContext): Future[JsObject] = {
    val userToken = verifyUserToken(userTokenStr)
    if (userToken.uid == "") {
      Future(
        Json.obj(
          "errmsg" -> "no privilege to create session token",
          "sessionToken" -> ""
        )
      )
    } else {
      val uid = userToken.uid
      createSessionToken(uid, sessionid).map { sessionTokenStr =>
        if (sessionTokenStr == "") {
          Json.obj(
            "errmsg" -> "no privilege to create session token",
            "sessionToken" -> ""
          )
        } else {
          Json.obj(
            "errmsg" -> "",
            "sessionToken" -> sessionTokenStr
          )
        }
      }
    }
  }

  def verifyUserTokenCtl(userTokenStr: String)(implicit ec: ExecutionContext): Future[JsObject] = {
    val userToken = verifyUserToken(userTokenStr)
    Future(
      Json.obj(
        "uid" -> userToken.uid,
        "nickname" -> userToken.nickname,
        "avatar" -> userToken.avatar
      )
    )
  }

  def loginCtl(login: String, password: String)(implicit ec: ExecutionContext): Future[JsObject] = {
    var errmsg = ""
    if (password.length < 6) {
      Future {
        Json.obj(
          "uid" -> "",
          "errmsg" -> s"password must at least 6 characters",
          "successmsg" -> "",
          "userToken" -> ""
        )
      }
    } else {
      loginAction(login, password).map { case (uid, userTokenStr) =>
        var successmsg = ""
        if (uid != "") {
          successmsg = "login in success"
        } else {
          errmsg = "user not exist or password not match"
        }
        Json.obj(
          "uid" -> uid,
          "errmsg" -> errmsg,
          "successmsg" -> successmsg,
          "userToken" -> userTokenStr
        )
      }
    }
  }

  def logoutCtl(userTokenStr: String)(implicit ec: ExecutionContext): Future[JsObject] = {
    logoutAction(userTokenStr).map { updateResult =>
      if (updateResult.errmsg != "") {
        Json.obj(
          "errmsg" -> updateResult.errmsg,
          "successmsg" -> ""
        )
      } else {
        Json.obj(
          "errmsg" -> updateResult.errmsg,
          "successmsg" -> "logout success"
        )
      }
    }
  }

  def updateUserInfoCtl(userTokenStr: String, nickname: String = "", gender: Int = 0, avatarBytes: Array[Byte] = Array[Byte](), avatarFileName: String = "", avatarFileType: String = "")(implicit ec: ExecutionContext): Future[JsObject] = {
    val userToken = verifyUserToken(userTokenStr)
    if (userToken.uid == "") {
      Future(
        Json.obj(
          "errmsg" -> "no privilege to update user info",
          "successmsg" -> ""
        )
      )
    } else {
      val uid = userToken.uid
      updateUserInfo(uid, nickname, gender, avatarBytes, avatarFileName, avatarFileType).map { updateResult =>
        if (updateResult.errmsg != "") {
          Json.obj(
            "errmsg" -> updateResult.errmsg,
            "successmsg" -> ""
          )
        } else {
          Json.obj(
            "errmsg" -> "",
            "successmsg" -> "update user info success"
          )
        }
      }
    }
  }

  def changePwdCtl(userTokenStr: String, oldPwd: String, newPwd: String, renewPwd: String)(implicit ec: ExecutionContext): Future[JsObject] = {
    val userToken = verifyUserToken(userTokenStr)
    if (userToken.uid == "") {
      Future(
        Json.obj(
          "errmsg" -> "no privilege to update user info",
          "successmsg" -> ""
        )
      )
    } else {
      val uid = userToken.uid
      changePwd(uid, oldPwd, newPwd, renewPwd).map { updateResult =>
        if (updateResult.errmsg != "") {
          Json.obj(
            "errmsg" -> updateResult.errmsg,
            "successmsg" -> ""
          )
        } else {
          Json.obj(
            "errmsg" -> "",
            "successmsg" -> "change password success"
          )
        }
      }
    }
  }

  def getUserInfoCtl(userTokenStr: String, uid: String)(implicit ec: ExecutionContext): Future[JsObject] = {
    getUserInfo(uid).map { user =>
      if (user == null) {
        Json.obj(
          "errmsg" -> "user not exist",
          "successmsg" -> "",
          "userInfo" -> JsNull
        )
      } else {
        val userToken = verifyUserToken(userTokenStr)
        var login = ""
        if (uid == userToken.uid) {
          login = user.login
        }
        Json.obj(
          "errmsg" -> "",
          "successmsg" -> "get user info success",
          "userInfo" -> Json.obj(
            "uid" -> user._id,
            "nickname" -> user.nickname,
            "avatar" -> user.avatar,
            "gender" -> user.gender,
            "login" -> login,
            "lastLogin" -> timeToStr(user.lastLogin),
            "loginCount" -> user.loginCount
          )
        )
      }
    }
  }

  def createGroupSessionCtl(userTokenStr: String, sessionName: String, sessionIconBytes: Array[Byte], sessionIconFileName: String, sessionIconFileType: String, publicType: Int)(implicit ec: ExecutionContext, notificationActor: ActorRef): Future[JsObject] = {
    val userToken = verifyUserToken(userTokenStr)
    if (userToken.uid == "") {
      Future(
        Json.obj(
          "sessionid" -> "",
          "errmsg" -> "no privilege to create group session",
          "successmsg" -> ""
        )
      )
    } else {
      val uid = userToken.uid
      createGroupSession(uid, sessionName, sessionIconBytes, sessionIconFileName, sessionIconFileType, publicType).map { case (sessionid, errmsg) =>
        if (errmsg != "") {
          Json.obj(
            "sessionid" -> sessionid,
            "errmsg" -> errmsg,
            "successmsg" -> ""
          )
        } else {
          Json.obj(
            "sessionid" -> sessionid,
            "errmsg" -> errmsg,
            "successmsg" -> "create group session success"
          )
        }
      }
    }
  }

  def getEditGroupSessionInfoCtl(userTokenStr: String, sessionid: String)(implicit ec: ExecutionContext): Future[JsObject] = {
    val userToken = verifyUserToken(userTokenStr)
    if (userToken.uid == "") {
      Future(
        Json.obj(
          "errmsg" -> "no privilege to get group session info",
          "session" -> JsNull
        )
      )
    } else {
      val uid = userToken.uid
      getEditGroupSessionInfo(uid, sessionid).map { session =>
        if (session != null) {
          Json.obj(
            "errmsg" -> "",
            "session" -> Json.obj(
              "sessionName" -> session.sessionName,
              "sessionIcon" -> session.sessionIcon,
              "publicType" -> session.publicType
            )
          )
        } else {
          Json.obj(
            "errmsg" -> "no privilege to get group session info",
            "session" -> JsNull
          )
        }
      }
    }
  }

  def editGroupSessionCtl(userTokenStr: String, sessionid: String, sessionName: String, sessionIconBytes: Array[Byte], sessionIconFileName: String, sessionIconFileType: String, publicType: Int)(implicit ec: ExecutionContext): Future[JsObject] = {
    val userToken = verifyUserToken(userTokenStr)
    if (userToken.uid == "") {
      Future(
        Json.obj(
          "errmsg" -> "no privilege to edit group session",
          "successmsg" -> ""
        )
      )
    } else {
      val uid = userToken.uid
      editGroupSession(uid, sessionid, sessionName, sessionIconBytes, sessionIconFileName, sessionIconFileType, publicType).map { errmsg =>
        if (errmsg != "") {
          Json.obj(
            "errmsg" -> errmsg,
            "successmsg" -> ""
          )
        } else {
          Json.obj(
            "errmsg" -> errmsg,
            "successmsg" -> "edit group session success"
          )
        }
      }
    }
  }

  def listSessionsCtl(userTokenStr: String, isPublic: Boolean)(implicit ec: ExecutionContext): Future[JsObject] = {
    val userToken = verifyUserToken(userTokenStr)
    if (userToken.uid == "") {
      Future(
        Json.obj(
          "errmsg" -> "no privilege to list sessions",
          "sessions" -> JsArray()
        )
      )
    } else {
      val uid = userToken.uid
      listSessions(uid, isPublic).map { sessionInfoList =>
        Future.sequence(
          sessionInfoList.map { case (session, sessionStatus) =>
            getSessionLastMessage(userTokenStr, session._id).map { case (sessionLast, messageLast, userLast) =>
              var jsonMessage: JsValue = JsNull
              if (messageLast != null && userLast != null) {
                var content = messageLast.content
                if (messageLast.thumbid != "") {
                  content = "send a [PHOTO]"
                } else if (messageLast.fileid != "") {
                  content = "send a [FILE]"
                }
                jsonMessage = Json.obj(
                  "uid" -> userLast._id,
                  "nickname" -> userLast.nickname,
                  "avatar" -> userLast.avatar,
                  "msgType" -> messageLast.msgType,
                  "content" -> content,
                  "dateline" -> timeToStr(messageLast.dateline)
                )
              }
              Json.obj(
                "sessionid" -> session._id,
                "createuid" -> session.createuid,
                "ouid" -> session.ouid,
                "sessionName" -> session.sessionName.take(30),
                "sessionType" -> session.sessionType,
                "sessionIcon" -> session.sessionIcon,
                "publicType" -> session.publicType,
                "lastUpdate" -> timeToStr(session.lastUpdate),
                "dateline" -> timeToStr(session.dateline),
                "newCount" -> sessionStatus.newCount,
                "message" -> jsonMessage
              )
            }
          }
        )
      }.flatMap(t => t).map { sessions =>
        Json.obj(
          "errmsg" -> "",
          "sessions" -> sessions
        )
      }
    }
  }

  def listJoinedSessionsCtl(userTokenStr: String)(implicit ec: ExecutionContext): Future[JsObject] = {
    val userToken = verifyUserToken(userTokenStr)
    if (userToken.uid == "") {
      Future(
        Json.obj(
          "errmsg" -> "no privilege to list sessions",
          "sessions" -> JsArray()
        )
      )
    } else {
      val uid = userToken.uid
      listJoinedSessions(uid).map { sessionInfoList =>
        val sessions = sessionInfoList.map { case (session, sessionStatus) =>
          Json.obj(
            "sessionid" -> session._id,
            "createuid" -> session.createuid,
            "sessionName" -> trimUtf8(session.sessionName, 24),
            "sessionType" -> session.sessionType,
            "sessionIcon" -> session.sessionIcon,
            "publicType" -> session.publicType,
            "dateline" -> timeToStr(session.dateline),
            "lastUpdate" -> timeToStr(session.lastUpdate),
            "newCount" -> sessionStatus.newCount
          )
        }
        Json.obj(
          "errmsg" -> "",
          "sessions" -> sessions
        )
      }
    }
  }

  def getNewNotificationCountCtl(userTokenStr: String)(implicit ec: ExecutionContext): Future[JsObject] = {
    val userToken = verifyUserToken(userTokenStr)
    if (userToken.uid == "") {
      Future(
        Json.obj(
          "errmsg" -> "no privilege to get new notification count",
          "rsCount" -> 0
        )
      )
    } else {
      val uid = userToken.uid
      getNewNotificationCount(uid).map { case (rsCount, errmsg) =>
        Json.obj(
          "errmsg" -> errmsg,
          "rsCount" -> rsCount
        )
      }
    }
  }

  def listMessagesCtl(userTokenStr: String, sessionid: String, page: Int = 1, count: Int = 10)(implicit ec: ExecutionContext): Future[JsObject] = {
    implicit val chatMessageWrites = Json.writes[ChatMessage]
    val userToken = verifyUserToken(userTokenStr)
    if (userToken.uid == "") {
      Future(
        Json.obj(
          "errmsg" -> "no privilege to list session messages",
          "sessionToken" -> "",
          "messages" -> JsArray()
        )
      )
    } else {
      val uid = userToken.uid
      for {
        sessionTokenStr <- createSessionToken(uid, sessionid)
        ret <- {
          listHistoryMessages(uid, sessionid, page, count, sort = document("dateline" -> -1)).map { case (errmsg, messageUsers) =>
            var token = ""
            if (errmsg == "") {
              token = sessionTokenStr
            }
            Json.obj(
              "errmsg" -> errmsg,
              "sessionToken" -> token,
              "messages" -> messageUsers.reverse.map { case (message, user) =>
                var suid = ""
                var snickname = ""
                var savatar = ""
                if (user != null) {
                  suid = user._id
                  snickname = user.nickname
                  savatar = user.avatar
                }
                val chatMessage = ChatMessage(suid, snickname, savatar, message.msgType, message.content, message.fileName, message.fileType, message.fileid, message.thumbid, timeToStr(message.dateline))
                Json.toJson(chatMessage)
              }
            )
          }
        }
      } yield {
        ret
      }
    }
  }

  def joinGroupSessionCtl(userTokenStr: String, sessionid: String)(implicit ec: ExecutionContext, notificationActor: ActorRef): Future[JsObject] = {
    val userToken = verifyUserToken(userTokenStr)
    if (userToken.uid == "") {
      Future(
        Json.obj(
          "errmsg" -> "no privilege to join session",
          "sessionToken" -> ""
        )
      )
    } else {
      val uid = userToken.uid
      for {
        updateResult <- joinGroupSession(uid, sessionid)
        json <- {
          if (updateResult.errmsg != "") {
            Future(
              Json.obj(
                "errmsg" -> updateResult.errmsg,
                "sessionToken" -> ""
              )
            )
          } else {
            createSessionToken(uid, sessionid).map { sessionTokenStr =>
              Json.obj(
                "errmsg" -> updateResult.errmsg,
                "sessionToken" -> sessionTokenStr
              )
            }
          }
        }
      } yield {
        json
      }
    }
  }

  def leaveGroupSessionCtl(userTokenStr: String, sessionid: String)(implicit ec: ExecutionContext, notificationActor: ActorRef): Future[JsObject] = {
    val userToken = verifyUserToken(userTokenStr)
    if (userToken.uid == "") {
      Future(
        Json.obj(
          "errmsg" -> "no privilege to leave session"
        )
      )
    } else {
      val uid = userToken.uid
      leaveGroupSession(uid, sessionid).map { updateResult =>
        Json.obj(
          "errmsg" -> updateResult.errmsg
        )
      }
    }
  }

  def getUserInfoByNameCtl(userTokenStr: String, nickName: String)(implicit ec: ExecutionContext): Future[JsObject] = {
    getUserInfoByName(nickName).map { users =>
      if (users == null) {
        Json.obj(
          "errmsg" -> "user not exist",
          "successmsg" -> "",
          "userInfo" -> JsNull
        )
      } else {
        Json.obj(
          "errmsg" -> "",
          "successmsg" -> "get user info success",
          "userInfo" -> users.map { user =>
            Json.obj(
            "uid" -> user._id,
            "nickname" -> user.nickname,
            "avatar" -> user.avatar,
            "gender" -> user.gender,
            "dateline" -> timeToStr(user.dateline)
          )
          }
        )
    }
  }
  }

  def getJoinedUsersCtl(userTokenStr: String, sessionid: String)(implicit ec: ExecutionContext): Future[JsObject] = {
    val userToken = verifyUserToken(userTokenStr)
    if (userToken.uid == "") {
      Future(
        Json.obj(
          "errmsg" -> "no privilege to get joined users",
          "onlineUsers" -> JsArray(),
          "offlineUsers" -> JsArray()
        )
      )
    } else {
      val uid = userToken.uid
      getJoinedUsers(sessionid).map { case (session, users) =>
        if (session != null) {
          val onlineUsers = session.usersStatus.filter(_.online).map(_.uid).map { uid =>
            users.find(_._id == uid).orNull
          }.filter(_ != null)
          val offlineUsers = session.usersStatus.filterNot(_.online).map(_.uid).map { uid =>
            users.find(_._id == uid).orNull
          }.filter(_ != null)
          Json.obj(
            "errmsg" -> "",
            "onlineUsers" -> onlineUsers.map { user =>
              Json.obj(
                "uid" -> user._id,
                "nickname" -> user.nickname,
                "avatar" -> user.avatar
              )
            },
            "offlineUsers" -> offlineUsers.map { user =>
              Json.obj(
                "uid" -> user._id,
                "nickname" -> user.nickname,
                "avatar" -> user.avatar
              )
            }
          )
        } else {
          Json.obj(
            "errmsg" -> "session not exist",
            "onlineUsers" -> JsArray(),
            "offlineUsers" -> JsArray()
          )
        }
      }
    }
  }

  def getFriendsCtl(userTokenStr: String)(implicit ec: ExecutionContext) = {
    val userToken = verifyUserToken(userTokenStr)
    if (userToken.uid == "") {
      Future(
        Json.obj(
          "errmsg" -> "no privilege to get friends",
          "friends" -> JsArray()
        )
      )
    } else {
      val uid = userToken.uid
      listFriends(uid).map { users =>
        Json.obj(
          "errmsg" -> "",
          "friends" -> users.map { user =>
            val gender = user.gender match {
              case 1 => "boy"
              case 2 => "girl"
              case _ => "unknown"
            }
            Json.obj(
              "uid" -> user._id,
              "nickname" -> user.nickname,
              "avatar" -> user.avatar,
              "gender" -> gender,
              "dateline" -> timeToStr(user.dateline)
            )
          }
        )
      }
    }
  }

  def inviteFriendsCtl(userTokenStr: String, sessionid: String, friendsStr: String, ouid: String)(implicit ec: ExecutionContext, notificationActor: ActorRef): Future[JsObject] = {
    val userToken = verifyUserToken(userTokenStr)
    if (userToken.uid == "") {
      Future(
        Json.obj(
          "errmsg" -> "no privilege to invite friends",
          "successmsg" -> "",
          "sessionid" -> ""
        )
      )
    } else {
      val uid = userToken.uid
      var friends = List[String]()
      try {
        friends = Json.parse(friendsStr).as[List[String]]
      } catch { case e: Throwable =>
          consoleLog("ERROR", s"friends string parse to json error: $e")
      }
      if (friends.isEmpty) {
        Future(
          Json.obj(
            "errmsg" -> "please select friends to invite",
            "successmsg" -> "",
            "sessionid" -> ""
          )
        )
      } else {
        for {
          (session, joined, editable) <- getSessionMenu(uid, sessionid)
          (sessionidNew, errmsgNew) <- {
            var errmsgNew = ""
            if (session == null) {
              //sessionid not exists
              errmsgNew = "session not exists"
              Future("", errmsgNew)
            } else if (session.publicType == 0) {
              //private session
              if (ouid != "") {
                //private session and ouid not empty
                friends = (ouid +: friends).distinct
                generateNewGroupSession(uid, friends).map { case (sessionName, sessionIcons) =>
                  try {
                    implicit val writer = PngWriter.NoCompression
                    var bgImg = Image.filled(200, 200, Color.White)
                    sessionIcons.map { avatar =>
                      var avatarPath = avatar
                      if (avatar.startsWith("/")) {
                        avatarPath = avatar.drop(1)
                      } else {
                        avatarPath = s"www/$avatar"
                      }
                      Image.fromFile(new File(avatarPath)).cover(90, 90)
                    }.zipWithIndex.foreach { case (avatarImg, i) =>
                      val x = (i % 2) * 100 + 5
                      val y = (i / 2) * 100 + 5
                      bgImg = bgImg.overlay(avatarImg, x, y)
                    }
                    createGroupSession(uid, sessionName = sessionName, sessionIconBytes = bgImg.bytes, sessionIconFileName = s"$uid.thumb.png", sessionIconFileType = "image/png", publicType = 0).map { case (sessionCreated, errmsgCreated) =>
                      //after session created user must join session first
                      joinSession(uid, sessionCreated).map { updateResult =>
                        if (updateResult.errmsg != "") {
                          ("", updateResult.errmsg)
                        } else {
                          (sessionCreated, errmsgCreated)
                        }
                      }
                    }.flatMap(t => t)
                  } catch { case e: Throwable =>
                    errmsgNew = s"create group session icon error: $e"
                    consoleLog("ERROR", errmsgNew)
                    Future("", errmsgNew)
                  }
                }.flatMap(t => t)
              } else {
                //private session but ouid is empty
                errmsgNew = "ouid is empty"
                Future("", errmsgNew)
              }
            } else {
              //group session
              Future(sessionid, errmsgNew)
            }
          }
          json <- {
            if (sessionidNew != "") {
              inviteFriendsToGroupSession(uid, friends, sessionidNew).map { list =>
                val successUsers = list.filter { case (nickname, updateResult) => updateResult.errmsg == "" }
                if (successUsers.isEmpty) {
                  Json.obj(
                    "errmsg" -> "no friends invite to session",
                    "successmsg" -> "",
                    "sessionid" -> ""
                  )
                } else {
                  val successUsersNickname = successUsers.map { case (nickname, updateResult) => nickname}.mkString(", ")
                  Json.obj(
                    "errmsg" -> "",
                    "successmsg" -> s"invite $successUsersNickname success",
                    "sessionid" -> sessionidNew
                  )
                }
              }
            } else {
              Future(
                Json.obj(
                  "errmsg" -> errmsgNew,
                  "successmsg" -> "",
                  "sessionid" -> ""
                )
              )
            }
          }
        } yield {
          json
        }
      }
    }
  }

  def joinFriendCtl(userTokenStr: String, fuid: String)(implicit ec: ExecutionContext): Future[JsObject] = {
    val userToken = verifyUserToken(userTokenStr)
    if (userToken.uid == "") {
      Future(
        Json.obj(
          "errmsg" -> "no privilege to join friends",
          "successmsg" -> ""
        )
      )
    } else {
      val uid = userToken.uid
      joinFriend(uid, fuid).map { updateResult =>
        if (updateResult.errmsg != "") {
          Json.obj(
            "errmsg" -> updateResult.errmsg,
            "successmsg" -> ""
          )
        } else {
          Json.obj(
            "errmsg" -> "",
            "successmsg" -> "join friend success"
          )
        }
      }
    }
  }

  def removeFriendCtl(userTokenStr: String, fuid: String)(implicit ec: ExecutionContext): Future[JsObject] = {
    val userToken = verifyUserToken(userTokenStr)
    if (userToken.uid == "") {
      Future(
        Json.obj(
          "errmsg" -> "no privilege to remove friends",
          "successmsg" -> ""
        )
      )
    } else {
      val uid = userToken.uid
      removeFriend(uid, fuid).map { updateResult =>
        if (updateResult.errmsg != "") {
          Json.obj(
            "errmsg" -> updateResult.errmsg,
            "successmsg" -> ""
          )
        } else {
          Json.obj(
            "errmsg" -> "",
            "successmsg" -> "remove friend success"
          )
        }
      }
    }
  }

  def getPrivateSessionCtl(userTokenStr: String, ouid: String)(implicit ec: ExecutionContext, notificationActor: ActorRef): Future[JsObject] = {
    val userToken = verifyUserToken(userTokenStr)
    if (userToken.uid == "") {
      Future(
        Json.obj(
          "errmsg" -> "no privilege to get private session",
          "sessionid" -> ""
        )
      )
    } else {
      val uid = userToken.uid
      createPrivateSession(uid, ouid).map { case (sessionid, errmsg) =>
        Json.obj(
          "errmsg" -> errmsg,
          "sessionid" -> sessionid
        )
      }
    }
  }

  def getSessionHeaderCtl(userTokenStr: String, sessionid: String)(implicit ec: ExecutionContext): Future[JsObject] = {
    val userToken = verifyUserToken(userTokenStr)
    if (userToken.uid == "") {
      Future(
        Json.obj(
          "errmsg" -> "no privilege to get session header",
          "session" -> JsNull
        )
      )
    } else {
      val uid = userToken.uid
      getSessionHeader(uid, sessionid).map { case (session, sessionToken) =>
        if (session != null && sessionToken.sessionName != "") {
          Json.obj(
            "errmsg" -> "",
            "session" -> Json.obj(
              "sessionid" -> sessionid,
              "sessionName" -> sessionToken.sessionName,
              "sessionIcon" -> sessionToken.sessionIcon,
              "createuid" -> session.createuid,
              "ouid" -> session.ouid
            )
          )
        } else {
          Json.obj(
            "errmsg" -> "no privilege or session not exists",
            "session" -> JsNull
          )
        }
      }
    }
  }

  def getSessionMenuCtl(userTokenStr: String, sessionid: String)(implicit ec: ExecutionContext): Future[JsObject] = {
    val userToken = verifyUserToken(userTokenStr)
    if (userToken.uid == "") {
      Future(
        Json.obj(
          "errmsg" -> "no privilege to get session menu",
          "session" -> JsNull
        )
      )
    } else {
      val uid = userToken.uid
      getSessionMenu(uid, sessionid).map { case (session, joined, editable) =>
        if (session == null) {
          Json.obj(
            "errmsg" -> "no privilege to get session menu",
            "session" -> JsNull
          )
        } else {
          Json.obj(
            "errmsg" -> "",
            "session" -> Json.obj(
              "sessionid" -> session._id,
              "sessionName" -> session.sessionName,
              "sessionIcon" -> session.sessionIcon,
              "createuid" -> session.createuid,
              "ouid" -> session.ouid,
              "joined" -> joined,
              "editable" -> editable
            )
          )
        }
      }
    }
  }

  def getUserMenuCtl(userTokenStr: String, ouid: String)(implicit ec: ExecutionContext): Future[JsObject] = {
    val userToken = verifyUserToken(userTokenStr)
    if (userToken.uid == "") {
      Future(
        Json.obj(
          "errmsg" -> "no privilege to get user menu",
          "user" -> JsNull
        )
      )
    } else {
      val uid = userToken.uid
      getUserMenu(uid, ouid).map { case (ouser, isFriend) =>
        if (ouid == null) {
          Json.obj(
            "errmsg" -> "no privilege to get user menu",
            "user" -> JsNull
          )
        } else {
          Json.obj(
            "errmsg" -> "",
            "user" -> Json.obj(
              "uid" -> ouser._id,
              "nickname" -> ouser.nickname,
              "avatar" -> ouser.avatar,
              "gender" -> ouser.gender,
              "isFriend" -> isFriend
            )
          )
        }
      }
    }
  }

  def listNotificationsCtl(userTokenStr: String, page: Int = 10, count: Int = 1)(implicit ec: ExecutionContext): Future[JsObject] = {
    val userToken = verifyUserToken(userTokenStr)
    if (userToken.uid == "") {
      Future(
        Json.obj(
          "errmsg" -> "no privilege to list notifications",
          "notifications" -> JsArray()
        )
      )
    } else {
      val uid = userToken.uid
      listNotifications(uid, page, count).map { results =>
        val notifications = results.map { case (notification, senduser, session) =>
          var uid = ""
          var nickname = ""
          var avatar = ""
          var sessionid = ""
          var sessionName = ""
          var content = ""
          if (senduser != null) {
            uid = senduser._id
            nickname = senduser.nickname
            avatar = senduser.avatar
          }
          if (session != null) {
            sessionName = session.sessionName
            sessionid = session._id
          }
          if (notification.noticeType == "joinFriend") {
            content = s"$nickname join you as friend"
          } else if (notification.noticeType == "removeFriend") {
            content = s"$nickname remove you from friend"
          } else {
            content = s"$nickname invite you in $sessionName"
          }
          Json.obj(
            "uid" -> uid,
            "nickname" -> nickname,
            "avatar" -> avatar,
            "content" -> content,
            "sessionid" -> sessionid,
            "sessionName" -> sessionName,
            "isRead" -> notification.isRead,
            "dateline" -> timeToStr(notification.dateline)
          )
        }
        Json.obj(
          "errmsg" -> "",
          "notifications" -> notifications
        )
      }
    }
  }

  def getFileMetaCtl(id: String)(implicit ec: ExecutionContext): Future[JsObject] = {
    getGridFileMetaData(id).map { case (fid, fileName, fileType, fileSize, fileMetaData, errmsg) =>
      Json.obj(
        "id" -> id,
        "fileName" -> fileName,
        "fileType" -> fileType,
        "fileSize" -> fileSize,
        "errmsg" -> errmsg
      )
    }
  }

  def getFileCtl(id: String)(implicit ec: ExecutionContext): Future[(String, String, Long, BSONDocument, Array[Byte], String)] = {
    getGridFile(id)
  }


}


================================================
FILE: src/main/scala/com/cookeem/chat/restful/Route.scala
================================================
package com.cookeem.chat.restful


import akka.actor.{ActorRef, ActorSystem}
import akka.http.scaladsl.model.{HttpRequest, StatusCodes}
import akka.http.scaladsl.server.Directives._
import akka.http.scaladsl.server._
import akka.stream.ActorMaterializer
import com.cookeem.chat.restful.RouteOps._
import org.joda.time.DateTime

import scala.concurrent.{ExecutionContext, Future}

/**
  * Created by cookeem on 16/11/2.
  */
object Route {
  def badRequest(request: HttpRequest): StandardRoute = {
    val method = request.method.value.toLowerCase
    val path = request.getUri().path()
    val queryString = request.getUri().rawQueryString().orElse("")
    method match {
      case _ =>
        complete((StatusCodes.NotFound, "404 error, resource not found!"))
    }
  }

  //log duration and request info route
  def logDuration(inner: Route)(implicit ec: ExecutionContext): Route = { ctx =>
    val rejectionHandler = RejectionHandler.default
    val start = System.currentTimeMillis()
    val innerRejectionsHandled = handleRejections(rejectionHandler)(inner)
    mapResponse { resp =>
      val currentTime = new DateTime()
      val currentTimeStr = currentTime.toString("yyyy-MM-dd HH:mm:ss")
      val duration = System.currentTimeMillis() - start
      var remoteAddress = ""
      var userAgent = ""
      var rawUri = ""
      ctx.request.headers.foreach(header => {
        //this setting come from nginx
        if (header.name() == "X-Real-Ip") {
          remoteAddress = header.value()
        }
        if (header.name() == "User-Agent") {
          userAgent = header.value()
        }
        //you must set akka.http.raw-request-uri-header=on config
        if (header.name() == "Raw-Request-URI") {
          rawUri = header.value()
        }
      })
      Future {
        val mapPattern = Seq("chat")
        var isIgnore = false
        mapPattern.foreach(pattern =>
          isIgnore = isIgnore || rawUri.startsWith(s"/$pattern")
        )
        if (!isIgnore) {
          println(s"# $currentTimeStr ${ctx.request.uri} [$remoteAddress] [${ctx.request.method.name}] [${resp.status.value}] [$userAgent] took: ${duration}ms")
        }
      }
      resp
    }(innerRejectionsHandled)(ctx)
  }

  def routeRoot(implicit ec: ExecutionContext, system: ActorSystem, materializer: ActorMaterializer, notificationActor: ActorRef) = {
    routeLogic ~
    extractRequest { request =>
      badRequest(request)
    }
  }

  def logRoute(implicit ec: ExecutionContext, system: ActorSystem, materializer: ActorMaterializer, notificationActor: ActorRef) = logDuration(routeRoot)
}


================================================
FILE: src/main/scala/com/cookeem/chat/restful/RouteOps.scala
================================================
package com.cookeem.chat.restful

import akka.actor.{ActorRef, ActorSystem}
import akka.http.scaladsl.model.headers.RawHeader
import akka.http.scaladsl.model._
import akka.http.scaladsl.server.Directives._
import akka.stream.ActorMaterializer
import akka.util.ByteString
import com.cookeem.chat.common.CommonUtils._
import com.cookeem.chat.mongo.MongoLogic._
import com.cookeem.chat.restful.Controller._
import com.cookeem.chat.websocket.{ChatSession, PushSession}
import play.api.libs.json.Json

import scala.concurrent.{ExecutionContext, Future}
import scala.util.{Failure, Success}

/**
  * Created by cookeem on 16/11/3.
  */
object RouteOps {
  //init create mongodb collection
  createUsersCollection()
  createSessionsCollection()
  createMessagesCollection()
  createOnlinesCollection()
  createNotificationsCollection()

  def routeLogic(implicit ec: ExecutionContext, system: ActorSystem, materializer: ActorMaterializer, notificationActor: ActorRef) = {
    routeWebsocket ~
    routeAsset ~
    routeUserRegister ~
    routeGetUserToken ~
    routeGetSessionToken ~
    routeVerifyUserToken ~
    routeUserLogin ~
    routeUserLogout ~
    routeUserInfoUpdate ~
    routeUserPwdChange ~
    routeGetUserInfo ~
    routeCreateGroupSession ~
    routeGetGroupSessionInfo ~
    routeEditGroupSession ~
    routeListSessions ~
    routeListJoinedSessions ~
    routeGetNewNotificationCount ~
    routeListMessages ~
    routeJoinGroupSession ~
    routeLeaveGroupSession ~
    routeGetJoinedUsers ~
    routeGetUserInfoByName ~
    routeGetFriends ~
    routeInviteFriends ~
    routeJoinFriend ~
    routeRemoveFriend ~
    routeGetPrivateSession ~
    routeGetSessionHeader ~
    routeGetSessionMenu ~
    routeGetUserMenu ~
    routeListNotifications ~
    routeGetFileMeta ~
    routeGetFile
  }

  // mix multiform to Future[Map[String, ByteString]].
  // if part type is file, then part name have prefix "binary!", and ByteString content is {"fileName": "xxx", "fileType": "xxx"} ++ <#HeaderInfo#> ++ file content bytestring
  def multiPartExtract(formData: Multipart.FormData)(implicit ec: ExecutionContext, materializer: ActorMaterializer): Future[Map[String, ByteString]] = {
    formData.parts.map { part =>
      if (part.filename.isDefined) {
        val contentType = part.entity.contentType.value
        val jsonHeaderInfo = Json.obj(
          "fileName" -> part.filename.get,
          "fileType" -> contentType
        )
        val bsHeaderInfo = ByteString(Json.stringify(jsonHeaderInfo) + "<#HeaderInfo#>")
        part.entity.dataBytes.runFold(ByteString.empty)(_ ++ _).map(bs => (s"binary!${part.name}", bsHeaderInfo ++ bs))
      } else {
        part.entity.dataBytes.runFold(ByteString.empty)(_ ++ _).map(bs => (part.name, bs))
      }
    }.mapAsync[(String, ByteString)](6)(t => t).runFold(Map[String, ByteString]())(_ + _)
  }

  def extractHeaderInfo(paramBytes: Map[String, ByteString], key: String): (Array[Byte], String, String) = {
    var bytes = Array[Byte]()
    var fileName = ""
    var fileType = ""
    if (paramBytes.contains(s"binary!$key")) {
      try {
        val bs = paramBytes(s"binary!$key")
        val splitor = "<#HeaderInfo#>"
        val (bsJson, bsBin) = bs.splitAt(bs.indexOfSlice(splitor))
        val jsonStr = bsJson.utf8String
        bytes = bsBin.drop(splitor.length).toArray
        val json = Json.parse(jsonStr)
        fileName = getJsonString(json, "fileName")
        fileType = getJsonString(json, "fileType")
      } catch { case e: Throwable =>
          consoleLog("ERROR", s"extract header info error: key = $key, $e")
      }
    }
    (bytes, fileName, fileType)
  }

  def routeWebsocket(implicit ec: ExecutionContext, system: ActorSystem, materializer: ActorMaterializer) = {
    get {
      //use for chat service
      path("ws-chat") {
        val chatSession = new ChatSession()
        handleWebSocketMessages(chatSession.chatService)
        //use for push service
      } ~ path("ws-push") {
        val pushSession = new PushSession()
        handleWebSocketMessages(pushSession.pushService)
      }
    }
  }

  def routeAsset(implicit ec: ExecutionContext) = {
    get {
      pathSingleSlash {
        redirect("chat/", StatusCodes.PermanentRedirect)
      } ~ path("chat") {
        redirect("chat/", StatusCodes.PermanentRedirect)
      } ~ path("chat" / "") {
        getFromFile("www/index.html")
      } ~ pathPrefix("chat") {
        getFromDirectory("www")
      } ~ path("ping") {
        val headers = List(
          RawHeader("X-MyObject-Id", "myobjid"),
          RawHeader("X-MyObject-Name", "myobjname")
        )
        respondWithHeaders(headers) {
          complete("pong")
        }
      }
    }
  }

  def routeUserRegister(implicit ec: ExecutionContext) = post {
    path("api" / "registerUser") {
      formFieldMap { params =>
        val login = paramsGetString(params, "login", "")
        val nickname = paramsGetString(params, "nickname", "")
        val password = paramsGetString(params, "password", "")
        val repassword = paramsGetString(params, "repassword", "")
        val gender = paramsGetInt(params, "gender", 0)
        complete {
          registerUserCtl(login, nickname, password, repassword, gender) map { json =>
            HttpEntity(ContentTypes.`application/json`, Json.stringify(json))
          }
        }
      }
    }
  }

  def routeGetUserToken(implicit ec: ExecutionContext) = post {
    path("api" / "userToken") {
      formFieldMap { params =>
        val userTokenStr = paramsGetString(params, "userToken", "")
        complete {
          createUserTokenCtl(userTokenStr) map { json =>
            HttpEntity(ContentTypes.`application/json`, Json.stringify(json))
          }
        }
      }
    }
  }

  def routeGetSessionToken(implicit ec: ExecutionContext) = post {
    path("api" / "sessionToken") {
      formFieldMap { params =>
        val userTokenStr = paramsGetString(params, "userToken", "")
        val sessionid = paramsGetString(params, "sessionid", "")
        complete {
          createSessionTokenCtl(userTokenStr, sessionid) map { json =>
            HttpEntity(ContentTypes.`application/json`, Json.stringify(json))
          }
        }
      }
    }
  }

  def routeVerifyUserToken(implicit ec: ExecutionContext) = post {
    path("api" / "verifyUserToken") {
      formFieldMap { params =>
        val userTokenStr = paramsGetString(params, "userToken", "")
        complete {
          verifyUserTokenCtl(userTokenStr) map { json =>
            HttpEntity(ContentTypes.`application/json`, Json.stringify(json))
          }
        }
      }
    }
  }

  def routeUserLogin(implicit ec: ExecutionContext) = post {
    path("api" / "loginUser") {
      formFieldMap { params =>
        val login = paramsGetString(params, "login", "")
        val password = paramsGetString(params, "password", "")
        complete {
          loginCtl(login, password) map { json =>
            HttpEntity(ContentTypes.`application/json`, Json.stringify(json))
          }
        }
      }
    }
  }

  def routeUserLogout(implicit ec: ExecutionContext) = post {
    path("api" / "logoutUser") {
      formFieldMap { params =>
        val userTokenStr = paramsGetString(params, "userToken", "")
        complete {
          logoutCtl(userTokenStr) map { json =>
            HttpEntity(ContentTypes.`application/json`, Json.stringify(json))
          }
        }
      }
    }
  }

  def routeUserInfoUpdate(implicit ec: ExecutionContext, materializer: ActorMaterializer) = post {
    path("api" / "updateUser") {
      entity(as[Multipart.FormData]) { formData =>
        val futureParams: Future[Map[String, ByteString]] = multiPartExtract(formData)
        complete {
          // complete support nest future
          futureParams.map { paramBytes =>
            val params = paramBytes.filterNot { case (k, v) => k.startsWith("binary!")}.map { case (k, v) => (k, v.utf8String)}
            val userTokenStr = paramsGetString(params, "userToken", "")
            val nickname = paramsGetString(params, "nickname", "")
            val gender = paramsGetInt(params, "gender", 0)
            val (avatarBytes, avatarFileName, avatarFileType) = extractHeaderInfo(paramBytes, "avatar")
            updateUserInfoCtl(userTokenStr, nickname, gender, avatarBytes, avatarFileName, avatarFileType).map { json =>
              HttpEntity(ContentTypes.`application/json`, Json.stringify(json))
            }
          }
        }
      }
    }
  }

  def routeUserPwdChange(implicit ec: ExecutionContext) = post {
    path("api" / "changePwd") {
      formFieldMap { params =>
        val userTokenStr = paramsGetString(params, "userToken", "")
        val oldPwd = paramsGetString(params, "oldPwd", "")
        val newPwd = paramsGetString(params, "newPwd", "")
        val renewPwd = paramsGetString(params, "renewPwd", "")
        complete {
          changePwdCtl(userTokenStr, oldPwd, newPwd, renewPwd) map { json =>
            HttpEntity(ContentTypes.`application/json`, Json.stringify(json))
          }
        }
      }
    }
  }

  def routeGetUserInfo(implicit ec: ExecutionContext) = post {
    path("api" / "getUserInfo") {
      formFieldMap { params =>
        val userTokenStr = paramsGetString(params, "userToken", "")
        val uid = paramsGetString(params, "uid", "")
        complete {
          getUserInfoCtl(userTokenStr, uid) map { json =>
            HttpEntity(ContentTypes.`application/json`, Json.stringify(json))
          }
        }
      }
    }
  }

  def routeCreateGroupSession(implicit ec: ExecutionContext, materializer: ActorMaterializer, notificationActor: ActorRef) = post {
    path("api" / "createGroupSession") {
      entity(as[Multipart.FormData]) { formData =>
        val futureParams: Future[Map[String, ByteString]] = multiPartExtract(formData)
        complete {
          // complete support nest future
          futureParams.map { paramBytes =>
            val params = paramBytes.filterNot { case (k, v) => k.startsWith("binary!")}.map { case (k, v) => (k, v.utf8String)}
            val userTokenStr = paramsGetString(params, "userToken", "")
            val publicType = paramsGetInt(params, "publicType", 0)
            val sessionName = paramsGetString(params, "sessionName", "")
            val (sessionIconBytes, sessionIconFileName, sessionIconFileType) = extractHeaderInfo(paramBytes, "sessionIcon")
            createGroupSessionCtl(userTokenStr, sessionName, sessionIconBytes, sessionIconFileName, sessionIconFileType, publicType).map { json =>
              HttpEntity(ContentTypes.`application/json`, Json.stringify(json))
            }
          }
        }
      }
    }
  }

  def routeGetGroupSessionInfo(implicit ec: ExecutionContext) = post {
    path("api" / "getGroupSessionInfo") {
      formFieldMap { params =>
        val userTokenStr = paramsGetString(params, "userToken", "")
        val sessionid = paramsGetString(params, "sessionid", "")
        complete {
          getEditGroupSessionInfoCtl(userTokenStr, sessionid) map { json =>
            HttpEntity(ContentTypes.`application/json`, Json.stringify(json))
          }
        }
      }
    }
  }

  def routeEditGroupSession(implicit ec: ExecutionContext, materializer: ActorMaterializer) = post {
    path("api" / "editGroupSession") {
      entity(as[Multipart.FormData]) { formData =>
        //mix file upload and text formdata to Map[String, String]
        val futureParams: Future[Map[String, ByteString]] = multiPartExtract(formData)
        complete {
          // complete support nest future
          futureParams.map { paramBytes =>
            val params = paramBytes.filterNot { case (k, v) => k.startsWith("binary!")}.map { case (k, v) => (k, v.utf8String)}
            val userTokenStr = paramsGetString(params, "userToken", "")
            val publicType = paramsGetInt(params, "publicType", 0)
            val sessionid = paramsGetString(params, "sessionid", "")
            val sessionName = paramsGetString(params, "sessionName", "")
            val (sessionIconBytes, sessionIconFileName, sessionIconFileType) = extractHeaderInfo(paramBytes, "sessionIcon")
            editGroupSessionCtl(userTokenStr, sessionid, sessionName, sessionIconBytes, sessionIconFileName, sessionIconFileType, publicType).map { json =>
              HttpEntity(ContentTypes.`application/json`, Json.stringify(json))
            }
          }
        }
      }
    }
  }

  def routeListSessions(implicit ec: ExecutionContext) = post {
    path("api" / "listSessions") {
      formFieldMap { params =>
        val userTokenStr = paramsGetString(params, "userToken", "")
        val isPublic = paramsGetInt(params, "isPublic", 0) match {
          case 1 => true
          case _ => false
        }
        complete {
          listSessionsCtl(userTokenStr, isPublic) map { json =>
            HttpEntity(ContentTypes.`application/json`, Json.stringify(json))
          }
        }
      }
    }
  }

  def routeListJoinedSessions(implicit ec: ExecutionContext) = post {
    path("api" / "listJoinedSessions") {
      formFieldMap { params =>
        val userTokenStr = paramsGetString(params, "userToken", "")
        complete {
          listJoinedSessionsCtl(userTokenStr) map { json =>
            HttpEntity(ContentTypes.`application/json`, Json.stringify(json))
          }
        }
      }
    }
  }

  def routeGetNewNotificationCount(implicit ec: ExecutionContext) = post {
    path("api" / "getNewNotificationCount") {
      formFieldMap { params =>
        val userTokenStr = paramsGetString(params, "userToken", "")
        complete {
          getNewNotificationCountCtl(userTokenStr) map { json =>
            HttpEntity(ContentTypes.`application/json`, Json.stringify(json))
          }
        }
      }
    }
  }
  def routeListMessages(implicit ec: ExecutionContext) = post {
    path("api" / "listMessages") {
      formFieldMap { params =>
        val userTokenStr = paramsGetString(params, "userToken", "")
        val sessionid = paramsGetString(params, "sessionid", "")
        val page = paramsGetInt(params, "page", 1)
        val count = paramsGetInt(params, "count", 10)
        complete {
          listMessagesCtl(userTokenStr, sessionid, page, count) map { json =>
            HttpEntity(ContentTypes.`application/json`, Json.stringify(json))
          }
        }
      }
    }
  }

  def routeJoinGroupSession(implicit ec: ExecutionContext, notificationActor: ActorRef) = post {
    path("api" / "joinGroupSession") {
      formFieldMap { params =>
        val userTokenStr = paramsGetString(params, "userToken", "")
        val sessionid = paramsGetString(params, "sessionid", "")
        complete {
          joinGroupSessionCtl(userTokenStr, sessionid) map { json =>
            println(s"notificationActor: $notificationActor")
            HttpEntity(ContentTypes.`application/json`, Json.stringify(json))
          }
        }
      }
    }
  }

  def routeLeaveGroupSession(implicit ec: ExecutionContext, notificationActor: ActorRef) = post {
    path("api" / "leaveGroupSession") {
      formFieldMap { params =>
        val userTokenStr = paramsGetString(params, "userToken", "")
        val sessionid = paramsGetString(params, "sessionid", "")
        complete {
          leaveGroupSessionCtl(userTokenStr, sessionid) map { json =>
            HttpEntity(ContentTypes.`application/json`, Json.stringify(json))
          }
        }
      }
    }
  }

  def routeGetJoinedUsers(implicit ec: ExecutionContext) = post {
    path("api" / "getJoinedUsers") {
      formFieldMap { params =>
        val userTokenStr = paramsGetString(params, "userToken", "")
        val sessionid = paramsGetString(params, "sessionid", "")
        complete {
          getJoinedUsersCtl(userTokenStr, sessionid) map { json =>
            HttpEntity(ContentTypes.`application/json`, Json.stringify(json))
          }
        }
      }
    }
  }

  def routeGetUserInfoByName(implicit ec: ExecutionContext) = post {
    path("api" / "getUserInfoByName") {
      formFieldMap { params =>
        val userTokenStr = paramsGetString(params, "userToken", "")
        val nickName = paramsGetString(params, "nickName", "")
        complete {
          getUserInfoByNameCtl(userTokenStr, nickName) map { json =>
            HttpEntity(ContentTypes.`application/json`, Json.stringify(json))
          }
        }
      }
    }
  }


  def routeGetFriends(implicit ec: ExecutionContext) = post {
    path("api" / "getFriends") {
      formFieldMap { params =>
        val userTokenStr = paramsGetString(params, "userToken", "")
        complete {
          getFriendsCtl(userTokenStr) map { json =>
            HttpEntity(ContentTypes.`application/json`, Json.stringify(json))
          }
        }
      }
    }
  }

  def routeInviteFriends(implicit ec: ExecutionContext, notificationActor: ActorRef) = post {
    path("api" / "inviteFriends") {
      formFieldMap { params =>
        val userTokenStr = paramsGetString(params, "userToken", "")
        val sessionid = paramsGetString(params, "sessionid", "")
        val ouid = paramsGetString(params, "ouid", "")
        val friendsStr = paramsGetString(params, "friends", "")
        complete {
          inviteFriendsCtl(userTokenStr, sessionid, friendsStr, ouid) map { json =>
            HttpEntity(ContentTypes.`application/json`, Json.stringify(json))
          }
        }
      }
    }
  }

  def routeJoinFriend(implicit ec: ExecutionContext) = post {
    path("api" / "joinFriend") {
      formFieldMap { params =>
        val userTokenStr = paramsGetString(params, "userToken", "")
        val fuid = paramsGetString(params, "fuid", "")
        complete {
          joinFriendCtl(userTokenStr, fuid) map { json =>
            HttpEntity(ContentTypes.`application/json`, Json.stringify(json))
          }
        }
      }
    }
  }

  def routeRemoveFriend(implicit ec: ExecutionContext) = post {
    path("api" / "removeFriend") {
      formFieldMap { params =>
        val userTokenStr = paramsGetString(params, "userToken", "")
        val fuid = paramsGetString(params, "fuid", "")
        complete {
          removeFriendCtl(userTokenStr, fuid) map { json =>
            HttpEntity(ContentTypes.`application/json`, Json.stringify(json))
          }
        }
      }
    }
  }

  def routeGetPrivateSession(implicit ec: ExecutionContext, notificationActor: ActorRef) = post {
    path("api" / "getPrivateSession") {
      formFieldMap { params =>
        val userTokenStr = paramsGetString(params, "userToken", "")
        val ouid = paramsGetString(params, "ouid", "")
        complete {
          getPrivateSessionCtl(userTokenStr, ouid) map { json =>
            HttpEntity(ContentTypes.`application/json`, Json.stringify(json))
          }
        }
      }
    }
  }

  def routeGetSessionHeader(implicit ec: ExecutionContext) = post {
    path("api" / "getSessionHeader") {
      formFieldMap { params =>
        val userTokenStr = paramsGetString(params, "userToken", "")
        val sessionid = paramsGetString(params, "sessionid", "")
        complete {
          getSessionHeaderCtl(userTokenStr, sessionid) map { json =>
            HttpEntity(ContentTypes.`application/json`, Json.stringify(json))
          }
        }
      }
    }
  }

  def routeGetSessionMenu(implicit ec: ExecutionContext) = post {
    path("api" / "getSessionMenu") {
      formFieldMap { params =>
        val userTokenStr = paramsGetString(params, "userToken", "")
        val sessionid = paramsGetString(params, "sessionid", "")
        complete {
          getSessionMenuCtl(userTokenStr, sessionid) map { json =>
            HttpEntity(ContentTypes.`application/json`, Json.stringify(json))
          }
        }
      }
    }
  }

  def routeGetUserMenu(implicit ec: ExecutionContext) = post {
    path("api" / "getUserMenu") {
      formFieldMap { params =>
        val userTokenStr = paramsGetString(params, "userToken", "")
        val ouid = paramsGetString(params, "ouid", "")
        complete {
          getUserMenuCtl(userTokenStr, ouid) map { json =>
            HttpEntity(ContentTypes.`application/json`, Json.stringify(json))
          }
        }
      }
    }
  }

  def routeListNotifications(implicit ec: ExecutionContext) = post {
    path("api" / "getNotifications") {
      formFieldMap { params =>
        val userTokenStr = paramsGetString(params, "userToken", "")
        val page = paramsGetInt(params, "page", 1)
        val count = paramsGetInt(params, "count", 30)
        complete {
          listNotificationsCtl(userTokenStr, page, count) map { json =>
            HttpEntity(ContentTypes.`application/json`, Json.stringify(json))
          }
        }
      }
    }
  }

  def routeGetFileMeta(implicit ec: ExecutionContext) = get {
    path("api" / "getFileMeta") {
      parameterMap { params =>
        val id = paramsGetString(params, "id", "")
        complete {
          getFileMetaCtl(id) map { json =>
            HttpEntity(ContentTypes.`application/json`, Json.stringify(json))
          }
        }
      }
    }
  }

  def routeGetFile(implicit ec: ExecutionContext) = get {
    path("api" / "getFile") {
      parameterMap { params =>
        val id = paramsGetString(params, "id", "")
        onComplete(getFileCtl(id)) {
          case Success((fileName, fileType, fileSize, fileMetaData, fileBytes, errmsg)) =>
            withPrecompressedMediaTypeSupport {
              var contentType = ContentTypes.`application/octet-stream`
              withPrecompressedMediaTypeSupport
              var headerDisposition = RawHeader("Content-Disposition", s"""attachment; filename="$fileName"""")
              if (fileType == "image/jpeg") {
                contentType = ContentType(MediaTypes.`image/jpeg`)
              } else if (fileType == "image/png") {
                contentType = ContentType(MediaTypes.`image/png`)
              } else if (fileType == "image/gif") {
                contentType = ContentType(MediaTypes.`image/gif`)
              }
              if (contentType != ContentTypes.`application/octet-stream`) {
                headerDisposition = RawHeader("Content-Disposition", s"""inline; filename="$fileName"""")
              }
              respondWithHeaders(headerDisposition) {
                complete(HttpEntity(contentType, ByteString(fileBytes)))
              }
            }
          case Failure(e) =>
            complete((StatusCodes.InternalServerError, s"An error occurred: $e"))
        }
      }
    }
  }

}


================================================
FILE: src/main/scala/com/cookeem/chat/websocket/ChatSession.scala
================================================
package com.cookeem.chat.websocket

import akka.NotUsed
import akka.actor.{ActorRef, ActorSystem, Props}
import akka.http.scaladsl.model.ws.TextMessage.Strict
import akka.http.scaladsl.model.ws.{BinaryMessage, Message, TextMessage}
import akka.stream._
import akka.stream.scaladsl._
import akka.util.ByteString
import com.cookeem.chat.common.CommonUtils._
import com.cookeem.chat.event._
import com.cookeem.chat.mongo.MongoLogic._
import com.cookeem.chat.mongo._
import play.api.libs.json.Json

import scala.concurrent.{ExecutionContext, Future}
import scala.concurrent.duration._

/**
  * Created by cookeem on 16/9/25.
  */
class ChatSession()(implicit ec: ExecutionContext, actorSystem: ActorSystem, materializer: ActorMaterializer) {
  implicit val chatMessageWrites = Json.writes[ChatMessage]

  val chatSessionActor = actorSystem.actorOf(Props(classOf[ChatSessionActor]))
  consoleLog("INFO", s"create new chatSessionActor: $chatSessionActor")

  val source: Source[WsMessageDown, ActorRef] = Source.actorRef[WsMessageDown](bufferSize = Int.MaxValue, OverflowStrategy.fail)
  def chatService: Flow[Message, Strict, ActorRef] = Flow.fromGraph(GraphDSL.create(source) { implicit builder =>
    chatSource =>
      import GraphDSL.Implicits._

      val flowFromWs: FlowShape[Message, WsMessageUp] = builder.add(
        Flow[Message].collect {
          case tm: TextMessage =>
            tm.textStream.runFold("")(_ + _).map { jsonStr =>
              var userTokenStr = ""
              var sessionTokenStr = ""
              var msgType = ""
              var content = ""
              try {
                val json = Json.parse(jsonStr)
                userTokenStr = getJsonString(json, "userToken")
                sessionTokenStr = getJsonString(json, "sessionToken")
                msgType = getJsonString(json, "msgType")
                content = getJsonString(json, "content")
              } catch { case e: Throwable =>
                consoleLog("ERROR", s"parse websocket text message error: $e")
              }
              val UserSessionInfo(uid, nickname, avatar, sessionid, sessionName, sessionIcon) = verifyUserSessionToken(userTokenStr, sessionTokenStr)
              WsTextUp(uid, nickname, avatar, sessionid, sessionName, sessionIcon, msgType, content)
            }
          case bm: BinaryMessage =>
            bm.dataStream.runFold(ByteString.empty)(_ ++ _).map { bs =>
              val splitor = "<#BinaryInfo#>"
              val (bsJson, bsBin) = bs.splitAt(bs.indexOfSlice(splitor))
              val jsonStr = bsJson.utf8String
              val bsFile = bsBin.drop(splitor.length)
              var userTokenStr = ""
              var sessionTokenStr = ""
              var msgType = ""
              var fileName = ""
              var fileSize = 0L
              var fileType = ""
              try {
                val json = Json.parse(jsonStr)
                userTokenStr = getJsonString(json, "userToken")
                sessionTokenStr = getJsonString(json, "sessionToken")
                msgType = getJsonString(json, "msgType")
                fileName = getJsonString(json, "fileName")
                fileSize = getJsonLong(json, "fileSize")
                fileType = getJsonString(json, "fileType")
              } catch { case e: Throwable =>
                consoleLog("ERROR", s"parse websocket binary message error: $e")
              }
              val UserSessionInfo(uid, nickname, avatar, sessionid, sessionName, sessionIcon) = verifyUserSessionToken(userTokenStr, sessionTokenStr)
              WsBinaryUp(uid, nickname, avatar, sessionid, sessionName, sessionIcon, msgType, bsFile, fileName, fileSize, fileType)
            }
        }.buffer(1024 * 1024, OverflowStrategy.fail).mapAsync(6)(t => t)
      )

      val broadcastWs: UniformFanOutShape[WsMessageUp, WsMessageUp] = builder.add(Broadcast[WsMessageUp](2))

      val filterFailure: FlowShape[WsMessageUp, WsMessageUp] = builder.add(Flow[WsMessageUp].filter(_.uid == ""))
      val flowReject: FlowShape[WsMessageUp, WsTextDown] = builder.add(
        Flow[WsMessageUp].map(_ => WsTextDown("", "", "", "", "", "", "reject", "no privilege to send message"))
      )

      val filterSuccess: FlowShape[WsMessageUp, WsMessageUp] = builder.add(Flow[WsMessageUp].filter(_.uid != ""))

      val flowAccept: FlowShape[WsMessageUp, WsMessageDown] = builder.add(
        Flow[WsMessageUp].collect {
          case WsTextUp(uid, nickname, avatar, sessionid, sessionName, sessionIcon, msgType, content) =>
            Future(
              WsTextDown(uid, nickname, avatar, sessionid, sessionName, sessionIcon, msgType, content)
            )
          case WsBinaryUp(uid, nickname, avatar, sessionid, sessionName, sessionIcon, msgType, bs, fileName, fileSize, fileType) =>
            val bytes = bs.toArray
            writeGridFile(uid, bytes, fileName, fileType).map { fileid =>
              var futureThumbid = createThumbId(uid, bytes, fileName, fileType)
              futureThumbid.map( thumbid => (fileid, thumbid))
            }.flatMap(t => t).map { case (fileid, thumbid) =>
              WsBinaryDown(uid, nickname, avatar, sessionid, sessionName, sessionIcon, msgType, fileName, fileType, fileid, thumbid)
            }
        }.buffer(1024 * 1024, OverflowStrategy.fail).mapAsync(6)(t => t)
      )

      val mergeAccept: UniformFanInShape[WsMessageDown, WsMessageDown] = builder.add(Merge[WsMessageDown](2))

      val connectedWs: Flow[ActorRef, UserOnline, NotUsed] = Flow[ActorRef].map { actor =>
        UserOnline(actor)
      }

      val chatActorSink: Sink[WsMessageDown, NotUsed] = Sink.actorRef[WsMessageDown](chatSessionActor, UserOffline)

      val flowAcceptBack: FlowShape[WsMessageDown, WsMessageDown] = builder.add(
        // websocket default timeout after 60 second, to prevent timeout send keepalive message
        // you can config akka.http.server.idle-timeout to set timeout duration
        Flow[WsMessageDown].keepAlive(50.seconds, () => WsTextDown("", "", "", "", "", "", "keepalive", ""))
      )

      val mergeBackWs: UniformFanInShape[WsMessageDown, WsMessageDown] = builder.add(Merge[WsMessageDown](2))

      val flowBackWs: FlowShape[WsMessageDown, Strict] = builder.add(
        Flow[WsMessageDown].collect {
          case WsTextDown(uid, nickname, avatar, sessionid, sessionName, sessionIcon, msgType, content, dateline) =>
            val chatMessage = ChatMessage(uid, nickname, avatar, msgType, content, fileName = "", fileType = "", fileid = "", thumbid = "", dateline)
            TextMessage(Json.stringify(Json.toJson(chatMessage)))
          case WsBinaryDown(uid, nickname, avatar, sessionid, sessionName, sessionIcon, msgType, fileName, fileType, fileid, thumbid, dateline) =>
            val chatMessage = ChatMessage(uid, nickname, avatar, msgTy
Download .txt
gitextract_zxrdz1n_/

├── .github/
│   └── workflows/
│       └── scala.yml
├── .gitignore
├── Dockerfile
├── Dockerfile_k8s
├── README.md
├── README_CN.md
├── README_JENKINS.md
├── VERSION
├── build.sbt
├── conf/
│   └── application.conf
├── docker-compose.yml
├── docs/
│   └── doc.md
├── kubernetes/
│   └── cookim.yaml
├── pom.xml
├── project/
│   ├── assembly.sbt
│   ├── build.properties
│   └── plugins.sbt
├── src/
│   └── main/
│       ├── resources/
│       │   └── mykeystore.jks
│       └── scala/
│           └── com/
│               └── cookeem/
│                   └── chat/
│                       ├── CookIM.scala
│                       ├── common/
│                       │   └── CommonUtils.scala
│                       ├── demo/
│                       │   └── TestObj.scala
│                       ├── event/
│                       │   └── ChatEventPackage.scala
│                       ├── jwt/
│                       │   └── JwtOps.scala
│                       ├── mongo/
│                       │   ├── MongoLogic.scala
│                       │   ├── MongoOps.scala
│                       │   └── package.scala
│                       ├── restful/
│                       │   ├── Controller.scala
│                       │   ├── Route.scala
│                       │   └── RouteOps.scala
│                       └── websocket/
│                           ├── ChatSession.scala
│                           ├── ChatSessionActor.scala
│                           ├── NotificationActor.scala
│                           ├── PushSession.scala
│                           ├── PushSessionActor.scala
│                           └── TraitPubSubActor.scala
└── www/
    ├── changeinfo.html
    ├── changepwd.html
    ├── chatlist.html
    ├── chatsession.html
    ├── css/
    │   └── index.css
    ├── error.html
    ├── fonts/
    │   ├── Material_Icon/
    │   │   ├── MaterialIcons-Regular.ijmap
    │   │   ├── codepoints
    │   │   └── material-icons.css
    │   └── fonts.css
    ├── friends.html
    ├── images/
    │   └── cookim.ai
    ├── index.html
    ├── js/
    │   ├── changeinfo.js
    │   ├── changepwd.js
    │   ├── chatlist.js
    │   ├── chatsession.js
    │   ├── error.js
    │   ├── friends.js
    │   ├── index.js
    │   ├── login.js
    │   ├── logout.js
    │   ├── notifications.js
    │   └── register.js
    ├── jslib/
    │   ├── MediaStreamRecorder/
    │   │   └── MediaStreamRecorder.js
    │   ├── angular/
    │   │   ├── LICENSE.md
    │   │   ├── README.md
    │   │   ├── angular-csp.css
    │   │   ├── angular.js
    │   │   ├── angular.min.js.gzip
    │   │   ├── bower.json
    │   │   ├── index.js
    │   │   └── package.json
    │   ├── angular-animate/
    │   │   ├── LICENSE.md
    │   │   ├── README.md
    │   │   ├── angular-animate.js
    │   │   ├── bower.json
    │   │   ├── index.js
    │   │   └── package.json
    │   ├── angular-cookies/
    │   │   ├── LICENSE.md
    │   │   ├── README.md
    │   │   ├── angular-cookies.js
    │   │   ├── bower.json
    │   │   ├── index.js
    │   │   └── package.json
    │   ├── angular-route/
    │   │   ├── LICENSE.md
    │   │   ├── README.md
    │   │   ├── angular-route.js
    │   │   ├── bower.json
    │   │   ├── index.js
    │   │   └── package.json
    │   └── jquery/
    │       ├── AUTHORS.txt
    │       ├── LICENSE.txt
    │       ├── README.md
    │       ├── bower.json
    │       ├── dist/
    │       │   ├── core.js
    │       │   ├── jquery.js
    │       │   └── jquery.slim.js
    │       ├── external/
    │       │   └── sizzle/
    │       │       ├── LICENSE.txt
    │       │       └── dist/
    │       │           └── sizzle.js
    │       ├── package.json
    │       └── src/
    │           ├── .eslintrc
    │           ├── ajax/
    │           │   ├── jsonp.js
    │           │   ├── load.js
    │           │   ├── parseXML.js
    │           │   ├── script.js
    │           │   ├── var/
    │           │   │   ├── location.js
    │           │   │   ├── nonce.js
    │           │   │   └── rquery.js
    │           │   └── xhr.js
    │           ├── ajax.js
    │           ├── attributes/
    │           │   ├── attr.js
    │           │   ├── classes.js
    │           │   ├── prop.js
    │           │   ├── support.js
    │           │   └── val.js
    │           ├── attributes.js
    │           ├── callbacks.js
    │           ├── core/
    │           │   ├── DOMEval.js
    │           │   ├── access.js
    │           │   ├── init.js
    │           │   ├── parseHTML.js
    │           │   ├── ready-no-deferred.js
    │           │   ├── ready.js
    │           │   ├── readyException.js
    │           │   ├── support.js
    │           │   └── var/
    │           │       └── rsingleTag.js
    │           ├── core.js
    │           ├── css/
    │           │   ├── addGetHookIf.js
    │           │   ├── adjustCSS.js
    │           │   ├── curCSS.js
    │           │   ├── hiddenVisibleSelectors.js
    │           │   ├── showHide.js
    │           │   ├── support.js
    │           │   └── var/
    │           │       ├── cssExpand.js
    │           │       ├── getStyles.js
    │           │       ├── isHiddenWithinTree.js
    │           │       ├── rmargin.js
    │           │       ├── rnumnonpx.js
    │           │       └── swap.js
    │           ├── css.js
    │           ├── data/
    │           │   ├── Data.js
    │           │   └── var/
    │           │       ├── acceptData.js
    │           │       ├── dataPriv.js
    │           │       └── dataUser.js
    │           ├── data.js
    │           ├── deferred/
    │           │   └── exceptionHook.js
    │           ├── deferred.js
    │           ├── deprecated.js
    │           ├── dimensions.js
    │           ├── effects/
    │           │   ├── Tween.js
    │           │   └── animatedSelector.js
    │           ├── effects.js
    │           ├── event/
    │           │   ├── ajax.js
    │           │   ├── alias.js
    │           │   ├── focusin.js
    │           │   ├── support.js
    │           │   └── trigger.js
    │           ├── event.js
    │           ├── exports/
    │           │   ├── amd.js
    │           │   └── global.js
    │           ├── jquery.js
    │           ├── manipulation/
    │           │   ├── _evalUrl.js
    │           │   ├── buildFragment.js
    │           │   ├── getAll.js
    │           │   ├── setGlobalEval.js
    │           │   ├── support.js
    │           │   ├── var/
    │           │   │   ├── rcheckableType.js
    │           │   │   ├── rscriptType.js
    │           │   │   └── rtagName.js
    │           │   └── wrapMap.js
    │           ├── manipulation.js
    │           ├── offset.js
    │           ├── queue/
    │           │   └── delay.js
    │           ├── queue.js
    │           ├── selector-native.js
    │           ├── selector-sizzle.js
    │           ├── selector.js
    │           ├── serialize.js
    │           ├── traversing/
    │           │   ├── findFilter.js
    │           │   └── var/
    │           │       ├── dir.js
    │           │       ├── rneedsContext.js
    │           │       └── siblings.js
    │           ├── traversing.js
    │           ├── var/
    │           │   ├── ObjectFunctionString.js
    │           │   ├── arr.js
    │           │   ├── class2type.js
    │           │   ├── concat.js
    │           │   ├── document.js
    │           │   ├── documentElement.js
    │           │   ├── fnToString.js
    │           │   ├── getProto.js
    │           │   ├── hasOwn.js
    │           │   ├── indexOf.js
    │           │   ├── pnum.js
    │           │   ├── push.js
    │           │   ├── rcssNum.js
    │           │   ├── rnotwhite.js
    │           │   ├── slice.js
    │           │   ├── support.js
    │           │   └── toString.js
    │           └── wrap.js
    ├── login.html
    ├── logout.html
    ├── notifications.html
    ├── register.html
    └── websocket.html
Download .txt
SYMBOL INDEX (609 symbols across 39 files)

FILE: www/js/chatsession.js
  function onMediaSuccess (line 273) | function onMediaSuccess(stream) {
  function onMediaError (line 297) | function onMediaError(e) {

FILE: www/js/index.js
  function showHideSideBar (line 1034) | function showHideSideBar(isShow) {
  function utf8StringToArrayBuffer (line 1064) | function utf8StringToArrayBuffer(s) {
  function arrayBufferToUtf8String (line 1077) | function arrayBufferToUtf8String(ua) {
  function concatenateBuffers (line 1092) | function concatenateBuffers(buffA, buffB) {

FILE: www/jslib/MediaStreamRecorder/MediaStreamRecorder.js
  function MediaStreamRecorder (line 18) | function MediaStreamRecorder(mediaStream) {
  function MultiStreamRecorder (line 145) | function MultiStreamRecorder(arrayOfMediaStreams) {
  function mergeProps (line 643) | function mergeProps(mergein, mergeto) {
  function dropFirstFrame (line 655) | function dropFirstFrame(arr) {
  function invokeSaveAsDialog (line 667) | function invokeSaveAsDialog(file, fileName) {
  function bytesToSize (line 719) | function bytesToSize(bytes) {
  function isMediaRecorderCompatible (line 735) | function isMediaRecorderCompatible() {
  function MediaRecorderWrapper (line 792) | function MediaRecorderWrapper(mediaStream) {
  function StereoAudioRecorder (line 1109) | function StereoAudioRecorder(mediaStream) {
  function StereoAudioRecorderHelper (line 1165) | function StereoAudioRecorderHelper(mediaStream, root) {
  function WhammyRecorder (line 1429) | function WhammyRecorder(mediaStream) {
  function WhammyRecorderHelper (line 1496) | function WhammyRecorderHelper(mediaStream, root) {
  function GifRecorder (line 1800) | function GifRecorder(mediaStream) {
  function WhammyVideo (line 1962) | function WhammyVideo(duration, quality) {
  function processInWebWorker (line 1999) | function processInWebWorker(_function) {
  function whammyInWebWorker (line 2011) | function whammyInWebWorker(frames) {
  function readAsArrayBuffer (line 2399) | function readAsArrayBuffer() {
  function concatenateBuffers (line 2414) | function concatenateBuffers() {

FILE: www/jslib/angular-animate/angular-animate.js
  function assertArg (line 64) | function assertArg(arg, name, reason) {
  function mergeClasses (line 71) | function mergeClasses(a,b) {
  function packageStyles (line 80) | function packageStyles(options) {
  function pendClasses (line 89) | function pendClasses(classes, fix, isPrefix) {
  function removeFromArray (line 106) | function removeFromArray(arr, val) {
  function stripCommentsFromElement (line 113) | function stripCommentsFromElement(element) {
  function extractElementNode (line 138) | function extractElementNode(element) {
  function $$addClass (line 148) | function $$addClass($$jqLite, element, className) {
  function $$removeClass (line 154) | function $$removeClass($$jqLite, element, className) {
  function applyAnimationClassesFactory (line 160) | function applyAnimationClassesFactory($$jqLite) {
  function prepareAnimationOptions (line 173) | function prepareAnimationOptions(options) {
  function applyAnimationStyles (line 187) | function applyAnimationStyles(element, options) {
  function applyAnimationFromStyles (line 192) | function applyAnimationFromStyles(element, options) {
  function applyAnimationToStyles (line 199) | function applyAnimationToStyles(element, options) {
  function mergeAnimationDetails (line 206) | function mergeAnimationDetails(element, oldAnimation, newAnimation) {
  function resolveElementClasses (line 247) | function resolveElementClasses(existing, toAdd, toRemove) {
  function getDomNode (line 305) | function getDomNode(element) {
  function applyGeneratedPreparationClasses (line 309) | function applyGeneratedPreparationClasses(element, event, options) {
  function clearGeneratedClasses (line 326) | function clearGeneratedClasses(element, options) {
  function blockTransitions (line 337) | function blockTransitions(node, duration) {
  function blockKeyframeAnimations (line 346) | function blockKeyframeAnimations(node, applyBlock) {
  function applyInlineStyle (line 353) | function applyInlineStyle(node, styleTuple) {
  function concatWithSpace (line 359) | function concatWithSpace(a,b) {
  function scheduler (line 368) | function scheduler(tasks) {
  function nextTick (line 398) | function nextTick() {
  function setData (line 505) | function setData(value) {
  function getCssKeyframeDurationStyle (line 751) | function getCssKeyframeDurationStyle(duration) {
  function getCssDelayStyle (line 755) | function getCssDelayStyle(delay, isKeyframeAnimation) {
  function computeCssStyles (line 760) | function computeCssStyles($window, element, properties) {
  function parseMaxTime (line 786) | function parseMaxTime(str) {
  function truthyTimingValue (line 801) | function truthyTimingValue(val) {
  function getCssTransitionDurationStyle (line 805) | function getCssTransitionDurationStyle(duration, applyOnlyDuration) {
  function createLocalCacheLookup (line 816) | function createLocalCacheLookup() {
  function registerRestorableStyles (line 852) | function registerRestorableStyles(backup, node, properties) {
  function gcsHashFn (line 872) | function gcsHashFn(node, extraClasses) {
  function computeCachedCssStyles (line 879) | function computeCachedCssStyles(node, className, cacheKey, properties) {
  function computeCachedCssStaggerStyles (line 895) | function computeCachedCssStaggerStyles(node, className, cacheKey, proper...
  function waitUntilQuiet (line 926) | function waitUntilQuiet(callback) {
  function computeTimings (line 945) | function computeTimings(node, className, cacheKey) {
  function endFn (line 1215) | function endFn() {
  function cancelFn (line 1219) | function cancelFn() {
  function close (line 1223) | function close(rejected) { // jshint ignore:line
  function applyBlocking (line 1282) | function applyBlocking(duration) {
  function closeAndReturnNoopAnimator (line 1292) | function closeAndReturnNoopAnimator() {
  function onAnimationProgress (line 1311) | function onAnimationProgress(event) {
  function start (line 1338) | function start() {
  function isDocumentFragment (line 1524) | function isDocumentFragment(node) {
  function filterCssClasses (line 1555) | function filterCssClasses(classes) {
  function getUniqueValues (line 1560) | function getUniqueValues(a, b) {
  function prepareAnchoredAnimation (line 1568) | function prepareAnchoredAnimation(classes, outAnchor, inAnchor) {
  function prepareFromToAnchorAnimation (line 1695) | function prepareFromToAnchorAnimation(from, to, classes, anchors) {
  function prepareRegularAnimation (line 1748) | function prepareRegularAnimation(animationDetails) {
  function applyOptions (line 1843) | function applyOptions() {
  function close (line 1848) | function close() {
  function onComplete (line 1910) | function onComplete(success) {
  function endAnimations (line 1915) | function endAnimations(cancelled) {
  function executeAnimationFn (line 1924) | function executeAnimationFn(fn, element, event, options, onDone) {
  function groupEventedAnimations (line 1967) | function groupEventedAnimations(element, event, options, animations, fnN...
  function packageAnimations (line 2008) | function packageAnimations(element, event, options, animations, fnName) {
  function lookupAnimations (line 2050) | function lookupAnimations(classes) {
  function endFnFactory (line 2096) | function endFnFactory() {
  function done (line 2105) | function done(status) {
  function prepareAnimation (line 2115) | function prepareAnimation(animationDetails) {
  function makeTruthyCssClassMap (line 2139) | function makeTruthyCssClassMap(classString) {
  function hasMatchingClasses (line 2153) | function hasMatchingClasses(newClassString, currentClassString) {
  function isAllowed (line 2162) | function isAllowed(ruleType, element, currentAnimation, previousAnimatio...
  function hasAnimationClasses (line 2168) | function hasAnimationClasses(animation, and) {
  function postDigestTaskFactory (line 2235) | function postDigestTaskFactory() {
  function normalizeAnimationDetails (line 2295) | function normalizeAnimationDetails(element, animation) {
  function findCallbacks (line 2306) | function findCallbacks(parent, element, event) {
  function filterFromRegistry (line 2325) | function filterFromRegistry(list, matchContainer, matchCallback) {
  function cleanupEventListeners (line 2334) | function cleanupEventListeners(phase, element) {
  function queueAnimation (line 2430) | function queueAnimation(element, event, initialOptions) {
  function closeChildAnimations (line 2710) | function closeChildAnimations(element) {
  function clearElementAnimationState (line 2729) | function clearElementAnimationState(element) {
  function isMatchingElement (line 2735) | function isMatchingElement(nodeOrElmA, nodeOrElmB) {
  function areAnimationsAllowed (line 2746) | function areAnimationsAllowed(element, parentElement, event) {
  function markElementAnimationState (line 2831) | function markElementAnimationState(element, state, details) {
  function setRunner (line 2854) | function setRunner(element, runner) {
  function removeRunner (line 2858) | function removeRunner(element) {
  function getRunner (line 2862) | function getRunner(element) {
  function sortAnimations (line 2872) | function sortAnimations(animations) {
  function getAnchorNodes (line 3072) | function getAnchorNodes(node) {
  function groupAnimations (line 3087) | function groupAnimations(animations) {
  function cssClassesIntersection (line 3170) | function cssClassesIntersection(a,b) {
  function invokeFirstDriver (line 3190) | function invokeFirstDriver(animationDetails) {
  function beforeStart (line 3203) | function beforeStart() {
  function updateAnimationRunners (line 3214) | function updateAnimationRunners(animation, newRunner) {
  function handleDestroyedElement (line 3228) | function handleDestroyedElement() {
  function close (line 3235) | function close(rejected) { // jshint ignore:line

FILE: www/jslib/angular-cookies/angular-cookies.js
  function calcOptions (line 57) | function calcOptions(options) {
  function $$CookieWriter (line 273) | function $$CookieWriter($document, $log, $browser) {

FILE: www/jslib/angular-route/angular-route.js
  function shallowCopy (line 15) | function shallowCopy(src, dst) {
  function $RouteProvider (line 76) | function $RouteProvider() {
  function $RouteParamsProvider (line 773) | function $RouteParamsProvider() {
  function ngViewFactory (line 963) | function ngViewFactory($route, $anchorScroll, $animate) {
  function ngViewFillContentFactory (line 1040) | function ngViewFillContentFactory($compile, $controller, $route) {

FILE: www/jslib/angular/angular.js
  function minErr (line 38) | function minErr(module, ErrorConstructor) {
  function isArrayLike (line 249) | function isArrayLike(obj) {
  function forEach (line 306) | function forEach(obj, iterator, context) {
  function forEachSorted (line 350) | function forEachSorted(obj, iterator, context) {
  function reverseParams (line 364) | function reverseParams(iteratorFn) {
  function nextUid (line 378) | function nextUid() {
  function setHashKey (line 388) | function setHashKey(obj, h) {
  function baseExtend (line 397) | function baseExtend(dst, objs, deep) {
  function extend (line 449) | function extend(dst) {
  function merge (line 472) | function merge(dst) {
  function toInt (line 478) | function toInt(str) {
  function inherit (line 483) | function inherit(parent, extra) {
  function noop (line 503) | function noop() {}
  function identity (line 535) | function identity($) {return $;}
  function valueFn (line 539) | function valueFn(value) {return function valueRef() {return value;};}
  function hasCustomToString (line 541) | function hasCustomToString(obj) {
  function isUndefined (line 558) | function isUndefined(value) {return typeof value === 'undefined';}
  function isDefined (line 573) | function isDefined(value) {return typeof value !== 'undefined';}
  function isObject (line 589) | function isObject(value) {
  function isBlankObject (line 600) | function isBlankObject(value) {
  function isString (line 617) | function isString(value) {return typeof value === 'string';}
  function isNumber (line 638) | function isNumber(value) {return typeof value === 'number';}
  function isDate (line 653) | function isDate(value) {
  function isFunction (line 684) | function isFunction(value) {return typeof value === 'function';}
  function isRegExp (line 694) | function isRegExp(value) {
  function isWindow (line 706) | function isWindow(obj) {
  function isScope (line 711) | function isScope(obj) {
  function isFile (line 716) | function isFile(obj) {
  function isFormData (line 721) | function isFormData(obj) {
  function isBlob (line 726) | function isBlob(obj) {
  function isBoolean (line 731) | function isBoolean(value) {
  function isPromiseLike (line 736) | function isPromiseLike(obj) {
  function isTypedArray (line 742) | function isTypedArray(value) {
  function isArrayBuffer (line 746) | function isArrayBuffer(obj) {
  function isElement (line 776) | function isElement(node) {
  function makeMap (line 786) | function makeMap(str) {
  function nodeName_ (line 795) | function nodeName_(element) {
  function includes (line 799) | function includes(array, obj) {
  function arrayRemove (line 803) | function arrayRemove(array, value) {
  function copy (line 876) | function copy(source, destination) {
  function equals (line 1078) | function equals(o1, o2) {
  function noUnsafeEval (line 1143) | function noUnsafeEval() {
  function concat (line 1208) | function concat(array1, array2, index) {
  function sliceArgs (line 1212) | function sliceArgs(args, startIndex) {
  function bind (line 1236) | function bind(self, fn) {
  function toJsonReplacer (line 1257) | function toJsonReplacer(key, value) {
  function toJson (line 1310) | function toJson(obj, pretty) {
  function fromJson (line 1331) | function fromJson(json) {
  function timezoneToOffset (line 1339) | function timezoneToOffset(timezone, fallback) {
  function addDateMinutes (line 1347) | function addDateMinutes(date, minutes) {
  function convertTimezoneToLocal (line 1354) | function convertTimezoneToLocal(date, timezone, reverse) {
  function startingTag (line 1365) | function startingTag(element) {
  function tryDecodeURIComponent (line 1395) | function tryDecodeURIComponent(value) {
  function parseKeyValue (line 1408) | function parseKeyValue(/**string*/keyValue) {
  function toKeyValue (line 1435) | function toKeyValue(obj) {
  function encodeUriSegment (line 1463) | function encodeUriSegment(val) {
  function encodeUriQuery (line 1482) | function encodeUriQuery(val, pctEncodeSpaces) {
  function getNgAttribute (line 1494) | function getNgAttribute(element, ngAttr) {
  function angularInit (line 1639) | function angularInit(element, bootstrap) {
  function bootstrap (line 1727) | function bootstrap(element, modules, config) {
  function reloadWithDebugInfo (line 1805) | function reloadWithDebugInfo() {
  function getTestability (line 1818) | function getTestability(rootElement) {
  function snake_case (line 1828) | function snake_case(name, separator) {
  function bindJQuery (line 1836) | function bindJQuery() {
  function assertArg (line 1890) | function assertArg(arg, name, reason) {
  function assertArgFn (line 1897) | function assertArgFn(arg, name, acceptArrayAnnotation) {
  function assertNotHasOwnProperty (line 1912) | function assertNotHasOwnProperty(name, context) {
  function getter (line 1926) | function getter(obj, path, bindFnToScope) {
  function getBlockNodes (line 1950) | function getBlockNodes(nodes) {
  function createMap (line 1980) | function createMap() {
  function setupModuleLoader (line 2000) | function setupModuleLoader(window) {
  function shallowCopy (line 2360) | function shallowCopy(src, dst) {
  function serializeObject (line 2382) | function serializeObject(obj) {
  function toDebugString (line 2397) | function toDebugString(obj) {
  function publishExternalAPI (line 2530) | function publishExternalAPI(angular) {
  function jqNextId (line 2810) | function jqNextId() { return ++jqId; }
  function camelCase (line 2823) | function camelCase(name) {
  function jqLiteIsTextNode (line 2851) | function jqLiteIsTextNode(html) {
  function jqLiteAcceptsData (line 2855) | function jqLiteAcceptsData(node) {
  function jqLiteHasData (line 2862) | function jqLiteHasData(node) {
  function jqLiteCleanData (line 2869) | function jqLiteCleanData(nodes) {
  function jqLiteBuildFragment (line 2875) | function jqLiteBuildFragment(html, context) {
  function jqLiteParseHTML (line 2912) | function jqLiteParseHTML(html, context) {
  function jqLiteWrapNode (line 2927) | function jqLiteWrapNode(node, wrapper) {
  function JQLite (line 2946) | function JQLite(element) {
  function jqLiteClone (line 2971) | function jqLiteClone(element) {
  function jqLiteDealoc (line 2975) | function jqLiteDealoc(element, onlyDescendants) {
  function jqLiteOff (line 2986) | function jqLiteOff(element, type, fn, unsupported) {
  function jqLiteRemoveData (line 3024) | function jqLiteRemoveData(element, name) {
  function jqLiteExpandoStore (line 3046) | function jqLiteExpandoStore(element, createIfNecessary) {
  function jqLiteData (line 3059) | function jqLiteData(element, key, value) {
  function jqLiteHasClass (line 3085) | function jqLiteHasClass(element, selector) {
  function jqLiteRemoveClass (line 3091) | function jqLiteRemoveClass(element, cssClasses) {
  function jqLiteAddClass (line 3103) | function jqLiteAddClass(element, cssClasses) {
  function jqLiteAddNodes (line 3120) | function jqLiteAddNodes(root, elements) {
  function jqLiteController (line 3146) | function jqLiteController(element, name) {
  function jqLiteInheritedData (line 3150) | function jqLiteInheritedData(element, name, value) {
  function jqLiteEmpty (line 3170) | function jqLiteEmpty(element) {
  function jqLiteRemove (line 3177) | function jqLiteRemove(element, keepData) {
  function jqLiteDocumentLoaded (line 3184) | function jqLiteDocumentLoaded(action, win) {
  function trigger (line 3204) | function trigger() {
  function getBooleanAttrName (line 3258) | function getBooleanAttrName(element, name) {
  function getAliasedAttrName (line 3266) | function getAliasedAttrName(name) {
  function getText (line 3359) | function getText(element, value) {
  function createEventHandler (line 3444) | function createEventHandler(element, events) {
  function defaultHandlerWrapper (line 3496) | function defaultHandlerWrapper(element, event, handler) {
  function specialMouseHandlerWrapper (line 3500) | function specialMouseHandlerWrapper(target, event, handler) {
  function $$jqLiteProvider (line 3747) | function $$jqLiteProvider() {
  function hashKey (line 3778) | function hashKey(obj, nextUidFn) {
  function HashMap (line 3801) | function HashMap(array, isolatedUid) {
  function stringifyFn (line 3915) | function stringifyFn(fn) {
  function extractArgs (line 3923) | function extractArgs(fn) {
  function anonFn (line 3929) | function anonFn(fn) {
  function annotate (line 3939) | function annotate(fn, strictDi, name) {
  function createInjector (line 4490) | function createInjector(modulesToLoad, strictDi) {
  function $AnchorScrollProvider (line 4759) | function $AnchorScrollProvider() {
  function mergeClasses (line 5026) | function mergeClasses(a,b) {
  function extractElementNode (line 5035) | function extractElementNode(element) {
  function splitClasses (line 5044) | function splitClasses(classes) {
  function prepareAnimateOptions (line 5069) | function prepareAnimateOptions(options) {
  function updateData (line 5114) | function updateData(data, classes, value) {
  function handleCSSClassChanges (line 5129) | function handleCSSClassChanges() {
  function addRemoveClassesPostDigest (line 5158) | function addRemoveClassesPostDigest(element, add, remove) {
  function domInsert (line 5272) | function domInsert(element, parentElement, afterElement) {
  function waitForTick (line 5666) | function waitForTick(fn) {
  function next (line 5701) | function next() {
  function onProgress (line 5725) | function onProgress(response) {
  function AnimateRunner (line 5733) | function AnimateRunner(host) {
  function run (line 5889) | function run() {
  function applyAnimationContents (line 5900) | function applyAnimationContents() {
  function Browser (line 5941) | function Browser(window, document, $log, $sniffer) {
  function $BrowserProvider (line 6272) | function $BrowserProvider() {
  function $CacheFactoryProvider (line 6360) | function $CacheFactoryProvider() {
  function $TemplateCacheProvider (line 6675) | function $TemplateCacheProvider() {
  function UNINITIALIZED_VALUE (line 7629) | function UNINITIALIZED_VALUE() {}
  function $CompileProvider (line 7639) | function $CompileProvider($provide, $$sanitizeUriProvider) {
  function SimpleChange (line 10101) | function SimpleChange(previous, current) {
  function directiveNormalize (line 10113) | function directiveNormalize(name) {
  function nodesetLinkingFn (line 10162) | function nodesetLinkingFn(
  function directiveLinkingFn (line 10169) | function directiveLinkingFn(
  function tokenDifference (line 10177) | function tokenDifference(str1, str2) {
  function removeComments (line 10193) | function removeComments(jqNodes) {
  function identifierForController (line 10214) | function identifierForController(controller, ident) {
  function $ControllerProvider (line 10233) | function $ControllerProvider() {
  function $DocumentProvider (line 10415) | function $DocumentProvider() {
  function $ExceptionHandlerProvider (line 10464) | function $ExceptionHandlerProvider() {
  function serializeValue (line 10510) | function serializeValue(v) {
  function $HttpParamSerializerProvider (line 10518) | function $HttpParamSerializerProvider() {
  function $HttpParamSerializerJQLikeProvider (line 10555) | function $HttpParamSerializerJQLikeProvider() {
  function defaultHttpResponseTransform (line 10627) | function defaultHttpResponseTransform(data, headers) {
  function isJsonLike (line 10643) | function isJsonLike(str) {
  function parseHeaders (line 10654) | function parseHeaders(headers) {
  function headersGetter (line 10690) | function headersGetter(headers) {
  function transformData (line 10720) | function transformData(data, headers, status, fns) {
  function isSuccess (line 10733) | function isSuccess(status) {
  function $HttpProvider (line 10744) | function $HttpProvider() {
  function $xhrFactoryProvider (line 11893) | function $xhrFactoryProvider() {
  function $HttpBackendProvider (line 11918) | function $HttpBackendProvider() {
  function createHttpBackend (line 11924) | function createHttpBackend($browser, createXhr, $browserDefer, callbacks...
  function $InterpolateProvider (line 12131) | function $InterpolateProvider() {
  function $IntervalProvider (line 12480) | function $IntervalProvider() {
  function createCallback (line 12693) | function createCallback(callbackId) {
  function encodePath (line 12782) | function encodePath(path) {
  function parseAbsoluteUrl (line 12793) | function parseAbsoluteUrl(absoluteUrl, locationObj) {
  function parseAppUrl (line 12802) | function parseAppUrl(relativeUrl, locationObj) {
  function startsWith (line 12819) | function startsWith(haystack, needle) {
  function stripBaseUrl (line 12830) | function stripBaseUrl(base, url) {
  function stripHash (line 12837) | function stripHash(url) {
  function trimEmptyHash (line 12842) | function trimEmptyHash(url) {
  function stripFile (line 12847) | function stripFile(url) {
  function serverBase (line 12852) | function serverBase(url) {
  function LocationHtml5Url (line 12866) | function LocationHtml5Url(appBase, appBaseNoFile, basePrefix) {
  function LocationHashbangUrl (line 12945) | function LocationHashbangUrl(appBase, appBaseNoFile, hashPrefix) {
  function LocationHashbangInHtml5Url (line 13057) | function LocationHashbangInHtml5Url(appBase, appBaseNoFile, hashPrefix) {
  function locationGetter (line 13427) | function locationGetter(property) {
  function locationGetterSetter (line 13434) | function locationGetterSetter(property, preprocess) {
  function $LocationProvider (line 13480) | function $LocationProvider() {
  function $LogProvider (line 13814) | function $LogProvider() {
  function ensureSafeMemberName (line 13970) | function ensureSafeMemberName(name, fullExpression) {
  function getStringValue (line 13981) | function getStringValue(name) {
  function ensureSafeObject (line 13999) | function ensureSafeObject(obj, fullExpression) {
  function ensureSafeFunction (line 14030) | function ensureSafeFunction(obj, fullExpression) {
  function ensureSafeAssignContext (line 14044) | function ensureSafeAssignContext(obj, fullExpression) {
  function ifDefined (line 14610) | function ifDefined(v, d) {
  function plusFn (line 14614) | function plusFn(l, r) {
  function isStateless (line 14620) | function isStateless($filter, filterName) {
  function findConstantAndWatchExpressions (line 14625) | function findConstantAndWatchExpressions(ast, $filter) {
  function getInputs (line 14733) | function getInputs(body) {
  function isAssignable (line 14741) | function isAssignable(ast) {
  function assignableAST (line 14745) | function assignableAST(ast) {
  function isLiteral (line 14751) | function isLiteral(ast) {
  function isConstant (line 14759) | function isConstant(ast) {
  function ASTCompiler (line 14763) | function ASTCompiler(astBuilder, $filter) {
  function ASTInterpreter (line 15292) | function ASTInterpreter(astBuilder, $filter) {
  function isPossiblyDangerousMemberName (line 15704) | function isPossiblyDangerousMemberName(name) {
  function getValueOf (line 15710) | function getValueOf(value) {
  function $ParseProvider (line 15765) | function $ParseProvider() {
  function $QProvider (line 16287) | function $QProvider() {
  function $$QProvider (line 16296) | function $$QProvider() {
  function qFactory (line 16312) | function qFactory(nextTick, exceptionHandler) {
  function $$RAFProvider (line 16690) | function $$RAFProvider() { //rAF
  function $RootScopeProvider (line 16787) | function $RootScopeProvider() {
  function $$SanitizeUriProvider (line 18114) | function $$SanitizeUriProvider() {
  function adjustMatcher (line 18205) | function adjustMatcher(matcher) {
  function adjustMatchers (line 18233) | function adjustMatchers(matchers) {
  function $SceDelegateProvider (line 18311) | function $SceDelegateProvider() {
  function $SceProvider (line 18848) | function $SceProvider() {
  function $SnifferProvider (line 19260) | function $SnifferProvider() {
  function $TemplateRequestProvider (line 19348) | function $TemplateRequestProvider() {
  function $$TestabilityProvider (line 19446) | function $$TestabilityProvider() {
  function $TimeoutProvider (line 19561) | function $TimeoutProvider() {
  function urlResolve (line 19712) | function urlResolve(url) {
  function urlIsSameOrigin (line 19746) | function urlIsSameOrigin(requestUrl) {
  function $WindowProvider (line 19793) | function $WindowProvider() {
  function $$CookieReader (line 19806) | function $$CookieReader($document) {
  function $$CookieReaderProvider (line 19848) | function $$CookieReaderProvider() {
  function $FilterProvider (line 19952) | function $FilterProvider($provide) {
  function filterFilter (line 20147) | function filterFilter() {
  function createPredicateFn (line 20185) | function createPredicateFn(expression, comparator, anyPropertyKey, match...
  function deepCompare (line 20222) | function deepCompare(actual, expected, comparator, anyPropertyKey, match...
  function getTypeForFilter (line 20272) | function getTypeForFilter(val) {
  function currencyFilter (line 20333) | function currencyFilter($locale) {
  function numberFilter (line 20407) | function numberFilter($locale) {
  function parse (line 20432) | function parse(numStr) {
  function roundNumber (line 20487) | function roundNumber(parsedNumber, fractionSize, minFrac, maxFrac) {
  function formatNumber (line 20562) | function formatNumber(number, pattern, groupSep, decimalSep, fractionSiz...
  function padNumber (line 20628) | function padNumber(num, digits, trim, negWrap) {
  function dateGetter (line 20647) | function dateGetter(name, size, offset, trim, negWrap) {
  function dateStrGetter (line 20659) | function dateStrGetter(name, shortForm, standAlone) {
  function timeZoneGetter (line 20669) | function timeZoneGetter(date, formats, offset) {
  function getFirstThursdayOfYear (line 20679) | function getFirstThursdayOfYear(year) {
  function getThursdayThisWeek (line 20687) | function getThursdayThisWeek(datetime) {
  function weekGetter (line 20693) | function weekGetter(size) {
  function ampmGetter (line 20705) | function ampmGetter(date, formats) {
  function eraGetter (line 20709) | function eraGetter(date, formats) {
  function longEraGetter (line 20713) | function longEraGetter(date, formats) {
  function dateFilter (line 20849) | function dateFilter($locale) {
  function jsonFilter (line 20956) | function jsonFilter() {
  function limitToFilter (line 21086) | function limitToFilter() {
  function sliceFn (line 21113) | function sliceFn(input, begin, end) {
  function orderByFilter (line 21667) | function orderByFilter($parse) {
  function ngDirective (line 21810) | function ngDirective(directive) {
  function defaultLinkFn (line 22203) | function defaultLinkFn(scope, element, attr) {
  function nullFormRenameControl (line 22305) | function nullFormRenameControl(control, name) {
  function FormController (line 22353) | function FormController(element, attrs, $scope, $animate, $interpolate) {
  function getSetter (line 22827) | function getSetter(expression) {
  function stringBasedInputType (line 23936) | function stringBasedInputType(ctrl) {
  function textInputType (line 23942) | function textInputType(scope, element, attr, ctrl, $sniffer, $browser) {
  function baseInputType (line 23947) | function baseInputType(scope, element, attr, ctrl, $sniffer, $browser) {
  function weekParser (line 24057) | function weekParser(isoWeek, existingDate) {
  function createDateParser (line 24089) | function createDateParser(regexp, mapping) {
  function createDateInputType (line 24139) | function createDateInputType(type, regexp, parseDate, format) {
  function badInputChecker (line 24211) | function badInputChecker(scope, element, attr, ctrl) {
  function numberInputType (line 24222) | function numberInputType(scope, element, attr, ctrl, $sniffer, $browser) {
  function urlInputType (line 24276) | function urlInputType(scope, element, attr, ctrl, $sniffer, $browser) {
  function emailInputType (line 24289) | function emailInputType(scope, element, attr, ctrl, $sniffer, $browser) {
  function radioInputType (line 24302) | function radioInputType(scope, element, attr, ctrl) {
  function parseConstantExpr (line 24324) | function parseConstantExpr($parse, context, name, expression, fallback) {
  function checkboxInputType (line 24337) | function checkboxInputType(scope, element, attr, ctrl, $sniffer, $browse...
  function classDirective (line 24921) | function classDirective(name, selector) {
  function processParseErrors (line 27532) | function processParseErrors() {
  function processSyncValidators (line 27552) | function processSyncValidators() {
  function processAsyncValidators (line 27568) | function processAsyncValidators() {
  function setValidity (line 27594) | function setValidity(name, isValid) {
  function validationDone (line 27600) | function validationDone(allValid) {
  function writeToModelIfNeeded (line 27680) | function writeToModelIfNeeded() {
  function addSetValidityMethod (line 28256) | function addSetValidityMethod(context) {
  function isObjectEmpty (line 28350) | function isObjectEmpty(obj) {
  function parseOptionsExpression (line 28642) | function parseOptionsExpression(optionsExp, selectElement, scope) {
  function ngOptionsPostLink (line 28804) | function ngOptionsPostLink(scope, selectElement, attr, ctrls) {
  function updateElementText (line 29337) | function updateElementText(newText) {
  function ngTranscludeCloneAttachFn (line 30693) | function ngTranscludeCloneAttachFn(clone, transcludedScope) {
  function useFallbackContent (line 30704) | function useFallbackContent() {
  function chromeHack (line 30766) | function chromeHack(optionElement) {
  function selectPreLink (line 31128) | function selectPreLink(scope, element, attr, ctrls) {
  function selectPostLink (line 31192) | function selectPostLink(scope, element, attrs, ctrls) {
  function getDecimals (line 31621) | function getDecimals(n) {
  function getVF (line 31627) | function getVF(n, opt_precision) {

FILE: www/jslib/jquery/dist/core.js
  function isArrayLike (line 463) | function isArrayLike( obj ) {

FILE: www/jslib/jquery/dist/jquery.js
  function DOMEval (line 77) | function DOMEval( code, doc ) {
  function isArrayLike (line 528) | function isArrayLike( obj ) {
  function Sizzle (line 760) | function Sizzle( selector, context, results, seed ) {
  function createCache (line 899) | function createCache() {
  function markFunction (line 917) | function markFunction( fn ) {
  function assert (line 926) | function assert( fn ) {
  function addHandle (line 948) | function addHandle( attrs, handler ) {
  function siblingCheck (line 963) | function siblingCheck( a, b ) {
  function createInputPseudo (line 989) | function createInputPseudo( type ) {
  function createButtonPseudo (line 1000) | function createButtonPseudo( type ) {
  function createDisabledPseudo (line 1011) | function createDisabledPseudo( disabled ) {
  function createPositionalPseudo (line 1039) | function createPositionalPseudo( fn ) {
  function testContext (line 1062) | function testContext( context ) {
  function setFilters (line 2118) | function setFilters() {}
  function toSelector (line 2189) | function toSelector( tokens ) {
  function addCombinator (line 2199) | function addCombinator( matcher, combinator, base ) {
  function elementMatcher (line 2261) | function elementMatcher( matchers ) {
  function multipleContexts (line 2275) | function multipleContexts( selector, contexts, results ) {
  function condense (line 2284) | function condense( unmatched, map, filter, context, xml ) {
  function setMatcher (line 2305) | function setMatcher( preFilter, selector, matcher, postFilter, postFinde...
  function matcherFromTokens (line 2398) | function matcherFromTokens( tokens ) {
  function matcherFromGroupMatchers (line 2456) | function matcherFromGroupMatchers( elementMatchers, setMatchers ) {
  function winnow (line 2798) | function winnow( elements, qualifier, not ) {
  function sibling (line 3094) | function sibling( cur, dir ) {
  function createOptions (line 3170) | function createOptions( options ) {
  function Identity (line 3395) | function Identity( v ) {
  function Thrower (line 3398) | function Thrower( ex ) {
  function adoptValue (line 3402) | function adoptValue( value, resolve, reject ) {
  function resolve (line 3494) | function resolve( depth, deferred, handler, special ) {
  function completed (line 3860) | function completed() {
  function Data (line 3959) | function Data() {
  function dataAttr (line 4128) | function dataAttr( elem, key, data ) {
  function adjustCSS (line 4448) | function adjustCSS( elem, prop, valueParts, tween ) {
  function getDefaultDisplay (line 4513) | function getDefaultDisplay( elem ) {
  function showHide (line 4536) | function showHide( elements, show ) {
  function getAll (line 4637) | function getAll( context, tag ) {
  function setGlobalEval (line 4654) | function setGlobalEval( elems, refElements ) {
  function buildFragment (line 4670) | function buildFragment( elems, context, scripts, selection, ignored ) {
  function returnTrue (line 4793) | function returnTrue() {
  function returnFalse (line 4797) | function returnFalse() {
  function safeActiveElement (line 4803) | function safeActiveElement() {
  function on (line 4809) | function on( elem, types, selector, data, fn, one ) {
  function manipulationTarget (line 5518) | function manipulationTarget( elem, content ) {
  function disableScript (line 5529) | function disableScript( elem ) {
  function restoreScript (line 5533) | function restoreScript( elem ) {
  function cloneCopyEvent (line 5545) | function cloneCopyEvent( src, dest ) {
  function fixInput (line 5580) | function fixInput( src, dest ) {
  function domManip (line 5593) | function domManip( collection, args, callback, ignored ) {
  function remove (line 5683) | function remove( elem, selector, keepData ) {
  function computeStyleTests (line 5976) | function computeStyleTests() {
  function curCSS (line 6050) | function curCSS( elem, name, computed ) {
  function addGetHookIf (line 6097) | function addGetHookIf( conditionFn, hookFn ) {
  function vendorPropName (line 6133) | function vendorPropName( name ) {
  function setPositiveNumber (line 6152) | function setPositiveNumber( elem, value, subtract ) {
  function augmentWidthOrHeight (line 6164) | function augmentWidthOrHeight( elem, name, extra, isBorderBox, styles ) {
  function getWidthOrHeight (line 6208) | function getWidthOrHeight( elem, name, extra ) {
  function Tween (line 6516) | function Tween( elem, options, prop, end, easing ) {
  function raf (line 6639) | function raf() {
  function createFxNow (line 6647) | function createFxNow() {
  function genFx (line 6655) | function genFx( type, includeWidth ) {
  function createTween (line 6675) | function createTween( value, prop, animation ) {
  function defaultPrefilter (line 6689) | function defaultPrefilter( elem, props, opts ) {
  function propFilter (line 6860) | function propFilter( props, specialEasing ) {
  function Animation (line 6897) | function Animation( elem, properties, options ) {
  function getClass (line 7588) | function getClass( elem ) {
  function buildParams (line 8213) | function buildParams( prefix, obj, traditional, add ) {
  function addToPrefiltersOrTransports (line 8359) | function addToPrefiltersOrTransports( structure ) {
  function inspectPrefiltersOrTransports (line 8393) | function inspectPrefiltersOrTransports( structure, options, originalOpti...
  function ajaxExtend (line 8422) | function ajaxExtend( target, src ) {
  function ajaxHandleResponses (line 8442) | function ajaxHandleResponses( s, jqXHR, responses ) {
  function ajaxConvert (line 8500) | function ajaxConvert( s, response, jqXHR, isSuccess ) {
  function done (line 9013) | function done( status, nativeStatusText, responses, headers ) {
  function getWindow (line 9738) | function getWindow( elem ) {

FILE: www/jslib/jquery/dist/jquery.slim.js
  function DOMEval (line 77) | function DOMEval( code, doc ) {
  function isArrayLike (line 528) | function isArrayLike( obj ) {
  function Sizzle (line 760) | function Sizzle( selector, context, results, seed ) {
  function createCache (line 899) | function createCache() {
  function markFunction (line 917) | function markFunction( fn ) {
  function assert (line 926) | function assert( fn ) {
  function addHandle (line 948) | function addHandle( attrs, handler ) {
  function siblingCheck (line 963) | function siblingCheck( a, b ) {
  function createInputPseudo (line 989) | function createInputPseudo( type ) {
  function createButtonPseudo (line 1000) | function createButtonPseudo( type ) {
  function createDisabledPseudo (line 1011) | function createDisabledPseudo( disabled ) {
  function createPositionalPseudo (line 1039) | function createPositionalPseudo( fn ) {
  function testContext (line 1062) | function testContext( context ) {
  function setFilters (line 2118) | function setFilters() {}
  function toSelector (line 2189) | function toSelector( tokens ) {
  function addCombinator (line 2199) | function addCombinator( matcher, combinator, base ) {
  function elementMatcher (line 2261) | function elementMatcher( matchers ) {
  function multipleContexts (line 2275) | function multipleContexts( selector, contexts, results ) {
  function condense (line 2284) | function condense( unmatched, map, filter, context, xml ) {
  function setMatcher (line 2305) | function setMatcher( preFilter, selector, matcher, postFilter, postFinde...
  function matcherFromTokens (line 2398) | function matcherFromTokens( tokens ) {
  function matcherFromGroupMatchers (line 2456) | function matcherFromGroupMatchers( elementMatchers, setMatchers ) {
  function winnow (line 2798) | function winnow( elements, qualifier, not ) {
  function sibling (line 3094) | function sibling( cur, dir ) {
  function createOptions (line 3170) | function createOptions( options ) {
  function Identity (line 3395) | function Identity( v ) {
  function Thrower (line 3398) | function Thrower( ex ) {
  function adoptValue (line 3402) | function adoptValue( value, resolve, reject ) {
  function resolve (line 3494) | function resolve( depth, deferred, handler, special ) {
  function completed (line 3860) | function completed() {
  function Data (line 3959) | function Data() {
  function dataAttr (line 4128) | function dataAttr( elem, key, data ) {
  function adjustCSS (line 4448) | function adjustCSS( elem, prop, valueParts, tween ) {
  function getDefaultDisplay (line 4513) | function getDefaultDisplay( elem ) {
  function showHide (line 4536) | function showHide( elements, show ) {
  function getAll (line 4637) | function getAll( context, tag ) {
  function setGlobalEval (line 4654) | function setGlobalEval( elems, refElements ) {
  function buildFragment (line 4670) | function buildFragment( elems, context, scripts, selection, ignored ) {
  function returnTrue (line 4793) | function returnTrue() {
  function returnFalse (line 4797) | function returnFalse() {
  function safeActiveElement (line 4803) | function safeActiveElement() {
  function on (line 4809) | function on( elem, types, selector, data, fn, one ) {
  function manipulationTarget (line 5518) | function manipulationTarget( elem, content ) {
  function disableScript (line 5529) | function disableScript( elem ) {
  function restoreScript (line 5533) | function restoreScript( elem ) {
  function cloneCopyEvent (line 5545) | function cloneCopyEvent( src, dest ) {
  function fixInput (line 5580) | function fixInput( src, dest ) {
  function domManip (line 5593) | function domManip( collection, args, callback, ignored ) {
  function remove (line 5683) | function remove( elem, selector, keepData ) {
  function computeStyleTests (line 5976) | function computeStyleTests() {
  function curCSS (line 6050) | function curCSS( elem, name, computed ) {
  function addGetHookIf (line 6097) | function addGetHookIf( conditionFn, hookFn ) {
  function vendorPropName (line 6133) | function vendorPropName( name ) {
  function setPositiveNumber (line 6152) | function setPositiveNumber( elem, value, subtract ) {
  function augmentWidthOrHeight (line 6164) | function augmentWidthOrHeight( elem, name, extra, isBorderBox, styles ) {
  function getWidthOrHeight (line 6208) | function getWidthOrHeight( elem, name, extra ) {
  function getClass (line 6807) | function getClass( elem ) {
  function buildParams (line 7404) | function buildParams( prefix, obj, traditional, add ) {
  function getWindow (line 7656) | function getWindow( elem ) {

FILE: www/jslib/jquery/external/sizzle/dist/sizzle.js
  function Sizzle (line 216) | function Sizzle( selector, context, results, seed ) {
  function createCache (line 355) | function createCache() {
  function markFunction (line 373) | function markFunction( fn ) {
  function assert (line 382) | function assert( fn ) {
  function addHandle (line 404) | function addHandle( attrs, handler ) {
  function siblingCheck (line 419) | function siblingCheck( a, b ) {
  function createInputPseudo (line 445) | function createInputPseudo( type ) {
  function createButtonPseudo (line 456) | function createButtonPseudo( type ) {
  function createDisabledPseudo (line 467) | function createDisabledPseudo( disabled ) {
  function createPositionalPseudo (line 495) | function createPositionalPseudo( fn ) {
  function testContext (line 518) | function testContext( context ) {
  function setFilters (line 1574) | function setFilters() {}
  function toSelector (line 1645) | function toSelector( tokens ) {
  function addCombinator (line 1655) | function addCombinator( matcher, combinator, base ) {
  function elementMatcher (line 1717) | function elementMatcher( matchers ) {
  function multipleContexts (line 1731) | function multipleContexts( selector, contexts, results ) {
  function condense (line 1740) | function condense( unmatched, map, filter, context, xml ) {
  function setMatcher (line 1761) | function setMatcher( preFilter, selector, matcher, postFilter, postFinde...
  function matcherFromTokens (line 1854) | function matcherFromTokens( tokens ) {
  function matcherFromGroupMatchers (line 1912) | function matcherFromGroupMatchers( elementMatchers, setMatchers ) {

FILE: www/jslib/jquery/src/ajax.js
  function addToPrefiltersOrTransports (line 55) | function addToPrefiltersOrTransports( structure ) {
  function inspectPrefiltersOrTransports (line 89) | function inspectPrefiltersOrTransports( structure, options, originalOpti...
  function ajaxExtend (line 118) | function ajaxExtend( target, src ) {
  function ajaxHandleResponses (line 138) | function ajaxHandleResponses( s, jqXHR, responses ) {
  function ajaxConvert (line 196) | function ajaxConvert( s, response, jqXHR, isSuccess ) {
  function done (line 709) | function done( status, nativeStatusText, responses, headers ) {

FILE: www/jslib/jquery/src/attributes/classes.js
  function getClass (line 12) | function getClass( elem ) {

FILE: www/jslib/jquery/src/callbacks.js
  function createOptions (line 9) | function createOptions( options ) {

FILE: www/jslib/jquery/src/core.js
  function isArrayLike (line 463) | function isArrayLike( obj ) {

FILE: www/jslib/jquery/src/core/DOMEval.js
  function DOMEval (line 6) | function DOMEval( code, doc ) {

FILE: www/jslib/jquery/src/core/ready-no-deferred.js
  function completed (line 86) | function completed() {

FILE: www/jslib/jquery/src/core/ready.js
  function completed (line 70) | function completed() {

FILE: www/jslib/jquery/src/css.js
  function vendorPropName (line 41) | function vendorPropName( name ) {
  function setPositiveNumber (line 60) | function setPositiveNumber( elem, value, subtract ) {
  function augmentWidthOrHeight (line 72) | function augmentWidthOrHeight( elem, name, extra, isBorderBox, styles ) {
  function getWidthOrHeight (line 116) | function getWidthOrHeight( elem, name, extra ) {

FILE: www/jslib/jquery/src/css/addGetHookIf.js
  function addGetHookIf (line 5) | function addGetHookIf( conditionFn, hookFn ) {

FILE: www/jslib/jquery/src/css/adjustCSS.js
  function adjustCSS (line 8) | function adjustCSS( elem, prop, valueParts, tween ) {

FILE: www/jslib/jquery/src/css/curCSS.js
  function curCSS (line 12) | function curCSS( elem, name, computed ) {

FILE: www/jslib/jquery/src/css/showHide.js
  function getDefaultDisplay (line 11) | function getDefaultDisplay( elem ) {
  function showHide (line 34) | function showHide( elements, show ) {

FILE: www/jslib/jquery/src/css/support.js
  function computeStyleTests (line 14) | function computeStyleTests() {

FILE: www/jslib/jquery/src/data.js
  function dataAttr (line 23) | function dataAttr( elem, key, data ) {

FILE: www/jslib/jquery/src/data/Data.js
  function Data (line 9) | function Data() {

FILE: www/jslib/jquery/src/deferred.js
  function Identity (line 9) | function Identity( v ) {
  function Thrower (line 12) | function Thrower( ex ) {
  function adoptValue (line 16) | function adoptValue( value, resolve, reject ) {
  function resolve (line 108) | function resolve( depth, deferred, handler, special ) {

FILE: www/jslib/jquery/src/effects.js
  function raf (line 30) | function raf() {
  function createFxNow (line 38) | function createFxNow() {
  function genFx (line 46) | function genFx( type, includeWidth ) {
  function createTween (line 66) | function createTween( value, prop, animation ) {
  function defaultPrefilter (line 80) | function defaultPrefilter( elem, props, opts ) {
  function propFilter (line 251) | function propFilter( props, specialEasing ) {
  function Animation (line 288) | function Animation( elem, properties, options ) {

FILE: www/jslib/jquery/src/effects/Tween.js
  function Tween (line 8) | function Tween( elem, options, prop, end, easing ) {

FILE: www/jslib/jquery/src/event.js
  function returnTrue (line 20) | function returnTrue() {
  function returnFalse (line 24) | function returnFalse() {
  function safeActiveElement (line 30) | function safeActiveElement() {
  function on (line 36) | function on( elem, types, selector, data, fn, one ) {

FILE: www/jslib/jquery/src/manipulation.js
  function manipulationTarget (line 50) | function manipulationTarget( elem, content ) {
  function disableScript (line 61) | function disableScript( elem ) {
  function restoreScript (line 65) | function restoreScript( elem ) {
  function cloneCopyEvent (line 77) | function cloneCopyEvent( src, dest ) {
  function fixInput (line 112) | function fixInput( src, dest ) {
  function domManip (line 125) | function domManip( collection, args, callback, ignored ) {
  function remove (line 215) | function remove( elem, selector, keepData ) {

FILE: www/jslib/jquery/src/manipulation/buildFragment.js
  function buildFragment (line 14) | function buildFragment( elems, context, scripts, selection, ignored ) {

FILE: www/jslib/jquery/src/manipulation/getAll.js
  function getAll (line 7) | function getAll( context, tag ) {

FILE: www/jslib/jquery/src/manipulation/setGlobalEval.js
  function setGlobalEval (line 8) | function setGlobalEval( elems, refElements ) {

FILE: www/jslib/jquery/src/offset.js
  function getWindow (line 21) | function getWindow( elem ) {

FILE: www/jslib/jquery/src/selector-native.js
  function sortOrder (line 63) | function sortOrder( a, b ) {
  function uniqueSort (line 106) | function uniqueSort( results ) {
  function escape (line 134) | function escape( sel ) {

FILE: www/jslib/jquery/src/serialize.js
  function buildParams (line 17) | function buildParams( prefix, obj, traditional, add ) {

FILE: www/jslib/jquery/src/traversing.js
  function sibling (line 105) | function sibling( cur, dir ) {

FILE: www/jslib/jquery/src/traversing/findFilter.js
  function winnow (line 13) | function winnow( elements, qualifier, not ) {
Condensed preview — 202 files, each showing path, character count, and a content snippet. Download the .json file or copy for the full structured content (2,939K chars).
[
  {
    "path": ".github/workflows/scala.yml",
    "chars": 323,
    "preview": "name: Scala CI\n\non:\n  push:\n    branches: [ master ]\n  pull_request:\n    branches: [ master ]\n\njobs:\n  build:\n\n    runs-"
  },
  {
    "path": ".gitignore",
    "chars": 152,
    "preview": "# Created by .ignore support plugin (hsz.mobi)\n/www/node_modules/\n.DS_Store\n/.idea/\n/project/project/\n/project/target/\n/"
  },
  {
    "path": "Dockerfile",
    "chars": 702,
    "preview": "# Notice! Don't use openjdk:latest docker image, it's too big and will build fail in docker:DinD\n\nFROM openjdk:alpine\n\nM"
  },
  {
    "path": "Dockerfile_k8s",
    "chars": 626,
    "preview": "# Notice! Don't use openjdk:latest docker image, it's too big and will build fail in docker:DinD\n\nFROM k8s-registry:5000"
  },
  {
    "path": "README.md",
    "chars": 15616,
    "preview": "# CookIM - is a distributed websocket chat applications based on akka\n\n[![Github All Releases](https://img.shields.io/gi"
  },
  {
    "path": "README_CN.md",
    "chars": 12002,
    "preview": "# CookIM - 一个基于akka的分布式websocket聊天程序\n\n- 支持私聊、群聊\n- 支持分布式多个服务端通信\n- 支持文本消息、文件消息、语音消息(感谢[ft115637850](https://github.com/ft1"
  },
  {
    "path": "README_JENKINS.md",
    "chars": 5834,
    "preview": "### Jenkins安装相关插件(\"系统管理\" -> \"管理插件\")\n\n- CloudBees Docker Build and Publish plugin\n    > docker插件,在\"构建\"步骤增加\"Docker Build a"
  },
  {
    "path": "VERSION",
    "chars": 14,
    "preview": "0.2.3-SNAPSHOT"
  },
  {
    "path": "build.sbt",
    "chars": 1714,
    "preview": "name := \"CookIM\"\n\nversion := \"0.2.4-SNAPSHOT\"\n\nscalaVersion := \"2.11.8\"\n\nscalacOptions := Seq(\"-unchecked\", \"-deprecatio"
  },
  {
    "path": "conf/application.conf",
    "chars": 2527,
    "preview": "#mongodb settings\nmongodb {\n  dbname = \"cookim\"\n  uri = \"mongodb://mongo:27017/local\"\n}\n//jwt secret settings\njwt {\n  se"
  },
  {
    "path": "docker-compose.yml",
    "chars": 948,
    "preview": "    version: '2'\n    services:\n      mongo:\n        image: mongo:3.4.4\n        container_name: mongo\n        hostname: m"
  },
  {
    "path": "docs/doc.md",
    "chars": 8967,
    "preview": "分别打开不同终端,运行以下命令:\n\n```\nsbt \"run-main com.cookeem.chat.CookIM -w 8080 -a 2551\"\n\nsbt \"run-main com.cookeem.chat.CookIM -w 8"
  },
  {
    "path": "kubernetes/cookim.yaml",
    "chars": 3012,
    "preview": "---\napiVersion: v1\nkind: Service\nmetadata:\n  name: cookim\n  labels:\n    app: cookim\nspec:\n  type: NodePort\n  selector:\n "
  },
  {
    "path": "pom.xml",
    "chars": 5685,
    "preview": "<?xml version=\"1.0\" encoding=\"UTF-8\"?>\n<project xmlns=\"http://maven.apache.org/POM/4.0.0\" xmlns:xsi=\"http://www.w3.org/2"
  },
  {
    "path": "project/assembly.sbt",
    "chars": 56,
    "preview": "addSbtPlugin(\"com.eed3si9n\" % \"sbt-assembly\" % \"0.14.0\")"
  },
  {
    "path": "project/build.properties",
    "chars": 20,
    "preview": "sbt.version=0.13.15\n"
  },
  {
    "path": "project/plugins.sbt",
    "chars": 24,
    "preview": "logLevel := Level.Debug\n"
  },
  {
    "path": "src/main/scala/com/cookeem/chat/CookIM.scala",
    "chars": 5266,
    "preview": "package com.cookeem.chat\n\nimport java.net.InetAddress\nimport java.security.{KeyStore, SecureRandom}\nimport javax.net.ssl"
  },
  {
    "path": "src/main/scala/com/cookeem/chat/common/CommonUtils.scala",
    "chars": 3435,
    "preview": "package com.cookeem.chat.common\n\nimport java.io.File\nimport java.security.MessageDigest\nimport java.text.SimpleDateForma"
  },
  {
    "path": "src/main/scala/com/cookeem/chat/demo/TestObj.scala",
    "chars": 1586,
    "preview": "package com.cookeem.chat.demo\n\nimport java.util.Date\n\nimport io.jsonwebtoken.{Jwts, SignatureAlgorithm}\nimport io.jsonwe"
  },
  {
    "path": "src/main/scala/com/cookeem/chat/event/ChatEventPackage.scala",
    "chars": 2565,
    "preview": "package com.cookeem.chat.event\n\nimport akka.actor.ActorRef\nimport akka.util.ByteString\nimport com.cookeem.chat.common.Co"
  },
  {
    "path": "src/main/scala/com/cookeem/chat/jwt/JwtOps.scala",
    "chars": 1049,
    "preview": "package com.cookeem.chat.jwt\n\nimport java.util.Date\n\nimport com.cookeem.chat.common.CommonUtils.configJwtSecret\n\nimport "
  },
  {
    "path": "src/main/scala/com/cookeem/chat/mongo/MongoLogic.scala",
    "chars": 44937,
    "preview": "package com.cookeem.chat.mongo\n\nimport java.io.{ByteArrayInputStream, File}\nimport java.util.Date\n\nimport akka.actor.Act"
  },
  {
    "path": "src/main/scala/com/cookeem/chat/mongo/MongoOps.scala",
    "chars": 12739,
    "preview": "package com.cookeem.chat.mongo\n\nimport com.cookeem.chat.common.CommonUtils._\nimport java.util.concurrent.Executors\n\nimpo"
  },
  {
    "path": "src/main/scala/com/cookeem/chat/mongo/package.scala",
    "chars": 1895,
    "preview": "package com.cookeem.chat\n\nimport java.util.Date\n\n/**\n  * Created by cookeem on 16/11/1.\n  */\npackage object mongo {\n  //"
  },
  {
    "path": "src/main/scala/com/cookeem/chat/restful/Controller.scala",
    "chars": 32294,
    "preview": "package com.cookeem.chat.restful\n\nimport java.io.File\n\nimport akka.actor.ActorRef\nimport com.cookeem.chat.common.CommonU"
  },
  {
    "path": "src/main/scala/com/cookeem/chat/restful/Route.scala",
    "chars": 2599,
    "preview": "package com.cookeem.chat.restful\n\n\nimport akka.actor.{ActorRef, ActorSystem}\nimport akka.http.scaladsl.model.{HttpReques"
  },
  {
    "path": "src/main/scala/com/cookeem/chat/restful/RouteOps.scala",
    "chars": 22579,
    "preview": "package com.cookeem.chat.restful\n\nimport akka.actor.{ActorRef, ActorSystem}\nimport akka.http.scaladsl.model.headers.RawH"
  },
  {
    "path": "src/main/scala/com/cookeem/chat/websocket/ChatSession.scala",
    "chars": 7487,
    "preview": "package com.cookeem.chat.websocket\n\nimport akka.NotUsed\nimport akka.actor.{ActorRef, ActorSystem, Props}\nimport akka.htt"
  },
  {
    "path": "src/main/scala/com/cookeem/chat/websocket/ChatSessionActor.scala",
    "chars": 4511,
    "preview": "package com.cookeem.chat.websocket\n\nimport akka.actor.ActorRef\nimport akka.cluster.pubsub.{DistributedPubSub, Distribute"
  },
  {
    "path": "src/main/scala/com/cookeem/chat/websocket/NotificationActor.scala",
    "chars": 870,
    "preview": "package com.cookeem.chat.websocket\n\nimport akka.cluster.pubsub.{DistributedPubSub, DistributedPubSubMediator}\nimport com"
  },
  {
    "path": "src/main/scala/com/cookeem/chat/websocket/PushSession.scala",
    "chars": 4887,
    "preview": "package com.cookeem.chat.websocket\n\nimport akka.NotUsed\nimport akka.actor.{ActorRef, ActorSystem, Props}\nimport akka.htt"
  },
  {
    "path": "src/main/scala/com/cookeem/chat/websocket/PushSessionActor.scala",
    "chars": 4106,
    "preview": "package com.cookeem.chat.websocket\n\nimport akka.actor.ActorRef\nimport akka.cluster.pubsub.{DistributedPubSub, Distribute"
  },
  {
    "path": "src/main/scala/com/cookeem/chat/websocket/TraitPubSubActor.scala",
    "chars": 1406,
    "preview": "package com.cookeem.chat.websocket\n\nimport akka.actor.Actor\nimport akka.cluster.Cluster\nimport akka.cluster.ClusterEvent"
  },
  {
    "path": "www/changeinfo.html",
    "chars": 3091,
    "preview": "<div ng-controller=\"changeInfoAppCtl\">\n    <div ng-if=\"!isLoading\" class=\"row\">\n        <div class=\"card-panel grey ligh"
  },
  {
    "path": "www/changepwd.html",
    "chars": 2170,
    "preview": "<div ng-controller=\"changePwdAppCtl\">\n    <div ng-if=\"!isLoading\" class=\"row\">\n        <div class=\"card-panel grey light"
  },
  {
    "path": "www/chatlist.html",
    "chars": 2422,
    "preview": "<div ng-controller=\"chatListAppCtl\">\n    <div ng-if=\"!isLoading\" class=\"row\">\n        <div ng-if=\"1 == 0\" class=\"search\""
  },
  {
    "path": "www/chatsession.html",
    "chars": 8172,
    "preview": "<div ng-controller=\"chatSessionAppCtl\">\n    <div class=\"row\">\n        <div ng-if=\"1 == 0\" class=\"search\">\n            <d"
  },
  {
    "path": "www/css/index.css",
    "chars": 3337,
    "preview": "body {\n    display: flex;\n    min-height: 100vh;\n    flex-direction: column;\n    font-family: \"Roboto\";\n    font-style: "
  },
  {
    "path": "www/error.html",
    "chars": 1100,
    "preview": "<div ng-controller=\"errorAppCtl\">\n    <div ng-if=\"!isLoading\" class=\"row\">\n        <div class=\"card-panel grey lighten-5"
  },
  {
    "path": "www/fonts/Material_Icon/MaterialIcons-Regular.ijmap",
    "chars": 28416,
    "preview": "{\"icons\":{\"e84d\":{\"name\":\"3d Rotation\"},\"eb3b\":{\"name\":\"Ac Unit\"},\"e190\":{\"name\":\"Access Alarm\"},\"e191\":{\"name\":\"Access "
  },
  {
    "path": "www/fonts/Material_Icon/codepoints",
    "chars": 16289,
    "preview": "3d_rotation e84d\nac_unit eb3b\naccess_alarm e190\naccess_alarms e191\naccess_time e192\naccessibility e84e\naccessible e914\na"
  },
  {
    "path": "www/fonts/Material_Icon/material-icons.css",
    "chars": 970,
    "preview": "@font-face {\n  font-family: 'Material Icons';\n  font-style: normal;\n  font-weight: 400;\n  src: url(MaterialIcons-Regular"
  },
  {
    "path": "www/fonts/fonts.css",
    "chars": 2049,
    "preview": "@font-face {\n    font-family: 'Open Sans';\n    font-style: normal;\n    font-weight: 400;\n    src: url(open-sans-v13-lati"
  },
  {
    "path": "www/friends.html",
    "chars": 2449,
    "preview": "<div ng-controller=\"friendsAppCtl\">\n    <div ng-if=\"!isLoading\" class=\"row\">\n        <div ng-if=\"1 == 0\" class=\"search\">"
  },
  {
    "path": "www/images/cookim.ai",
    "chars": 61752,
    "preview": "%PDF-1.5\r%\r\n1 0 obj\r<</Metadata 2 0 R/OCProperties<</D<</ON[5 0 R 21 0 R]/Order 22 0 R/RBGroups[]>>/OCGs[5 0 R 21 0 R]>>"
  },
  {
    "path": "www/index.html",
    "chars": 23694,
    "preview": "<!DOCTYPE html>\n<html lang=\"en\">\n<head>\n    <meta charset=\"UTF-8\">\n    <title>CookIM</title>\n    <meta http-equiv=\"X-UA-"
  },
  {
    "path": "www/js/changeinfo.js",
    "chars": 3930,
    "preview": "/**\n * Created by cookeem on 16/6/3.\n */\napp.controller('changeInfoAppCtl', function($rootScope, $scope, $cookies, $time"
  },
  {
    "path": "www/js/changepwd.js",
    "chars": 1968,
    "preview": "/**\n * Created by cookeem on 16/6/3.\n */\napp.controller('changePwdAppCtl', function($rootScope, $scope, $cookies, $timeo"
  },
  {
    "path": "www/js/chatlist.js",
    "chars": 2207,
    "preview": "/**\n * Created by cookeem on 16/6/3.\n */\napp.controller('chatListAppCtl', function($rootScope, $scope, $cookies, $timeou"
  },
  {
    "path": "www/js/chatsession.js",
    "chars": 10807,
    "preview": "/**\n * Created by cookeem on 16/6/3.\n */\napp.controller('chatSessionAppCtl', function($rootScope, $scope, $cookies, $tim"
  },
  {
    "path": "www/js/error.js",
    "chars": 820,
    "preview": "/**\n * Created by cookeem on 16/6/2.\n */\napp.controller('errorAppCtl', function($rootScope, $timeout) {\n    $rootScope.s"
  },
  {
    "path": "www/js/friends.js",
    "chars": 2769,
    "preview": "/**\n * Created by cookeem on 16/6/2.\n */\napp.controller('friendsAppCtl', function($rootScope, $scope, $http, $timeout) {"
  },
  {
    "path": "www/js/index.js",
    "chars": 41837,
    "preview": "/**\n * Created by cookeem on 16/9/27.\n */\n\nvar app = angular.module('app', ['ngRoute', 'ngAnimate', 'ngCookies', 'ui.mat"
  },
  {
    "path": "www/js/login.js",
    "chars": 2883,
    "preview": "/**\n * Created by cookeem on 16/6/3.\n */\napp.controller('loginAppCtl', function($rootScope, $scope, $cookies, $timeout, "
  },
  {
    "path": "www/js/logout.js",
    "chars": 1950,
    "preview": "/**\n * Created by cookeem on 16/6/2.\n */\napp.controller('logoutAppCtl', function($rootScope, $scope, $cookies, $http, $t"
  },
  {
    "path": "www/js/notifications.js",
    "chars": 1819,
    "preview": "/**\n * Created by cookeem on 16/6/2.\n */\napp.controller('notificationsAppCtl', function($rootScope, $timeout, $scope, $h"
  },
  {
    "path": "www/js/register.js",
    "chars": 1979,
    "preview": "/**\n * Created by cookeem on 16/6/3.\n */\napp.controller('registerAppCtl', function($rootScope, $scope, $cookies, $timeou"
  },
  {
    "path": "www/jslib/MediaStreamRecorder/MediaStreamRecorder.js",
    "chars": 74464,
    "preview": "'use strict';\n\n// Last time updated: 2017-08-04 12:43:52 PM UTC\n\n// __________________________\n// MediaStreamRecorder v1"
  },
  {
    "path": "www/jslib/angular/LICENSE.md",
    "chars": 1074,
    "preview": "The MIT License (MIT)\n\nCopyright (c) 2016 Angular\n\nPermission is hereby granted, free of charge, to any person obtaining"
  },
  {
    "path": "www/jslib/angular/README.md",
    "chars": 1885,
    "preview": "# packaged angular\n\nThis repo is for distribution on `npm` and `bower`. The source for this module is in the\n[main Angul"
  },
  {
    "path": "www/jslib/angular/angular-csp.css",
    "chars": 343,
    "preview": "/* Include this file in your html if you are using the CSP mode. */\n\n@charset \"UTF-8\";\n\n[ng\\:cloak], [ng-cloak], [data-n"
  },
  {
    "path": "www/jslib/angular/angular.js",
    "chars": 1187291,
    "preview": "/**\n * @license AngularJS v1.5.8\n * (c) 2010-2016 Google, Inc. http://angularjs.org\n * License: MIT\n */\n(function(window"
  },
  {
    "path": "www/jslib/angular/bower.json",
    "chars": 133,
    "preview": "{\n  \"name\": \"angular\",\n  \"version\": \"1.5.8\",\n  \"license\": \"MIT\",\n  \"main\": \"./angular.js\",\n  \"ignore\": [],\n  \"dependenci"
  },
  {
    "path": "www/jslib/angular/index.js",
    "chars": 48,
    "preview": "require('./angular');\nmodule.exports = angular;\n"
  },
  {
    "path": "www/jslib/angular/package.json",
    "chars": 2285,
    "preview": "{\n  \"_args\": [\n    [\n      \"angular@1.5.8\",\n      \"/Volumes/Share/Spark_program/DistributedWebChat/www\"\n    ]\n  ],\n  \"_c"
  },
  {
    "path": "www/jslib/angular-animate/LICENSE.md",
    "chars": 1074,
    "preview": "The MIT License (MIT)\n\nCopyright (c) 2016 Angular\n\nPermission is hereby granted, free of charge, to any person obtaining"
  },
  {
    "path": "www/jslib/angular-animate/README.md",
    "chars": 2045,
    "preview": "# packaged angular-animate\n\nThis repo is for distribution on `npm` and `bower`. The source for this module is in the\n[ma"
  },
  {
    "path": "www/jslib/angular-animate/angular-animate.js",
    "chars": 150567,
    "preview": "/**\n * @license AngularJS v1.5.8\n * (c) 2010-2016 Google, Inc. http://angularjs.org\n * License: MIT\n */\n(function(window"
  },
  {
    "path": "www/jslib/angular-animate/bower.json",
    "chars": 172,
    "preview": "{\n  \"name\": \"angular-animate\",\n  \"version\": \"1.5.8\",\n  \"license\": \"MIT\",\n  \"main\": \"./angular-animate.js\",\n  \"ignore\": ["
  },
  {
    "path": "www/jslib/angular-animate/index.js",
    "chars": 60,
    "preview": "require('./angular-animate');\nmodule.exports = 'ngAnimate';\n"
  },
  {
    "path": "www/jslib/angular-animate/package.json",
    "chars": 2614,
    "preview": "{\n  \"_args\": [\n    [\n      \"angular-animate@1.5.8\",\n      \"/Volumes/Share/Spark_program/DistributedWebChat/www\"\n    ]\n  "
  },
  {
    "path": "www/jslib/angular-cookies/LICENSE.md",
    "chars": 1074,
    "preview": "The MIT License (MIT)\n\nCopyright (c) 2016 Angular\n\nPermission is hereby granted, free of charge, to any person obtaining"
  },
  {
    "path": "www/jslib/angular-cookies/README.md",
    "chars": 2040,
    "preview": "# packaged angular-cookies\n\nThis repo is for distribution on `npm` and `bower`. The source for this module is in the\n[ma"
  },
  {
    "path": "www/jslib/angular-cookies/angular-cookies.js",
    "chars": 9739,
    "preview": "/**\n * @license AngularJS v1.5.8\n * (c) 2010-2016 Google, Inc. http://angularjs.org\n * License: MIT\n */\n(function(window"
  },
  {
    "path": "www/jslib/angular-cookies/bower.json",
    "chars": 172,
    "preview": "{\n  \"name\": \"angular-cookies\",\n  \"version\": \"1.5.8\",\n  \"license\": \"MIT\",\n  \"main\": \"./angular-cookies.js\",\n  \"ignore\": ["
  },
  {
    "path": "www/jslib/angular-cookies/index.js",
    "chars": 60,
    "preview": "require('./angular-cookies');\nmodule.exports = 'ngCookies';\n"
  },
  {
    "path": "www/jslib/angular-cookies/package.json",
    "chars": 2500,
    "preview": "{\n  \"_args\": [\n    [\n      \"angular-cookies@1.5.8\",\n      \"/Volumes/Share/Scala_program/CookIM/www\"\n    ]\n  ],\n  \"_cnpm_"
  },
  {
    "path": "www/jslib/angular-route/LICENSE.md",
    "chars": 1074,
    "preview": "The MIT License (MIT)\n\nCopyright (c) 2016 Angular\n\nPermission is hereby granted, free of charge, to any person obtaining"
  },
  {
    "path": "www/jslib/angular-route/README.md",
    "chars": 2018,
    "preview": "# packaged angular-route\n\nThis repo is for distribution on `npm` and `bower`. The source for this module is in the\n[main"
  },
  {
    "path": "www/jslib/angular-route/angular-route.js",
    "chars": 38825,
    "preview": "/**\n * @license AngularJS v1.5.8\n * (c) 2010-2016 Google, Inc. http://angularjs.org\n * License: MIT\n */\n(function(window"
  },
  {
    "path": "www/jslib/angular-route/bower.json",
    "chars": 168,
    "preview": "{\n  \"name\": \"angular-route\",\n  \"version\": \"1.5.8\",\n  \"license\": \"MIT\",\n  \"main\": \"./angular-route.js\",\n  \"ignore\": [],\n "
  },
  {
    "path": "www/jslib/angular-route/index.js",
    "chars": 56,
    "preview": "require('./angular-route');\nmodule.exports = 'ngRoute';\n"
  },
  {
    "path": "www/jslib/angular-route/package.json",
    "chars": 2491,
    "preview": "{\n  \"_args\": [\n    [\n      \"angular-route@1.5.8\",\n      \"/Volumes/Share/Spark_program/DistributedWebChat/www\"\n    ]\n  ],"
  },
  {
    "path": "www/jslib/jquery/AUTHORS.txt",
    "chars": 10995,
    "preview": "Authors ordered by first contribution.\n\nJohn Resig <jeresig@gmail.com>\nGilles van den Hoven <gilles0181@gmail.com>\nMicha"
  },
  {
    "path": "www/jslib/jquery/LICENSE.txt",
    "chars": 1606,
    "preview": "Copyright jQuery Foundation and other contributors, https://jquery.org/\n\nThis software consists of voluntary contributio"
  },
  {
    "path": "www/jslib/jquery/README.md",
    "chars": 1794,
    "preview": "# jQuery\n\n> jQuery is a fast, small, and feature-rich JavaScript library.\n\nFor information on how to get started and how"
  },
  {
    "path": "www/jslib/jquery/bower.json",
    "chars": 190,
    "preview": "{\n  \"name\": \"jquery\",\n  \"main\": \"dist/jquery.js\",\n  \"license\": \"MIT\",\n  \"ignore\": [\n    \"package.json\"\n  ],\n  \"keywords\""
  },
  {
    "path": "www/jslib/jquery/dist/core.js",
    "chars": 11329,
    "preview": "/* global Symbol */\n// Defining this global in .eslintrc would create a danger of using the global\n// unguarded in anoth"
  },
  {
    "path": "www/jslib/jquery/dist/jquery.js",
    "chars": 263767,
    "preview": "/*eslint-disable no-unused-vars*/\n/*!\n * jQuery JavaScript Library v3.1.0\n * https://jquery.com/\n *\n * Includes Sizzle.j"
  },
  {
    "path": "www/jslib/jquery/dist/jquery.slim.js",
    "chars": 210659,
    "preview": "/*eslint-disable no-unused-vars*/\n/*!\n * jQuery JavaScript Library v3.1.0 -ajax,-ajax/jsonp,-ajax/load,-ajax/parseXML,-a"
  },
  {
    "path": "www/jslib/jquery/external/sizzle/LICENSE.txt",
    "chars": 1606,
    "preview": "Copyright jQuery Foundation and other contributors, https://jquery.org/\n\nThis software consists of voluntary contributio"
  },
  {
    "path": "www/jslib/jquery/external/sizzle/dist/sizzle.js",
    "chars": 63623,
    "preview": "/*!\n * Sizzle CSS Selector Engine v2.3.0\n * https://sizzlejs.com/\n *\n * Copyright jQuery Foundation and other contributo"
  },
  {
    "path": "www/jslib/jquery/package.json",
    "chars": 3929,
    "preview": "{\n  \"_args\": [\n    [\n      \"jquery@3.1.0\",\n      \"/Volumes/Share/Spark_program/DistributedWebChat/www\"\n    ]\n  ],\n  \"_cn"
  },
  {
    "path": "www/jslib/jquery/src/.eslintrc",
    "chars": 360,
    "preview": "{\n\t// Support: IE <=9 only, Android <=4.0 only\n\t// The above browsers are failing a lot of tests in the ES5\n\t// test sui"
  },
  {
    "path": "www/jslib/jquery/src/ajax/jsonp.js",
    "chars": 2728,
    "preview": "define( [\n\t\"../core\",\n\t\"./var/nonce\",\n\t\"./var/rquery\",\n\t\"../ajax\"\n], function( jQuery, nonce, rquery ) {\n\n\"use strict\";\n"
  },
  {
    "path": "www/jslib/jquery/src/ajax/load.js",
    "chars": 1825,
    "preview": "define( [\n\t\"../core\",\n\t\"../core/parseHTML\",\n\t\"../ajax\",\n\t\"../traversing\",\n\t\"../manipulation\",\n\t\"../selector\"\n], function"
  },
  {
    "path": "www/jslib/jquery/src/ajax/parseXML.js",
    "chars": 559,
    "preview": "define( [\n\t\"../core\"\n], function( jQuery ) {\n\n\"use strict\";\n\n// Cross-browser xml parsing\njQuery.parseXML = function( da"
  },
  {
    "path": "www/jslib/jquery/src/ajax/script.js",
    "chars": 1583,
    "preview": "define( [\n\t\"../core\",\n\t\"../var/document\",\n\t\"../ajax\"\n], function( jQuery, document ) {\n\n\"use strict\";\n\n// Prevent auto-e"
  },
  {
    "path": "www/jslib/jquery/src/ajax/var/location.js",
    "chars": 67,
    "preview": "define( function() {\n\t\"use strict\";\n\n\treturn window.location;\n} );\n"
  },
  {
    "path": "www/jslib/jquery/src/ajax/var/nonce.js",
    "chars": 91,
    "preview": "define( [\n\t\"../../core\"\n], function( jQuery ) {\n\t\"use strict\";\n\n\treturn jQuery.now();\n} );\n"
  },
  {
    "path": "www/jslib/jquery/src/ajax/var/rquery.js",
    "chars": 60,
    "preview": "define( function() {\n\t\"use strict\";\n\n\treturn ( /\\?/ );\n} );\n"
  },
  {
    "path": "www/jslib/jquery/src/ajax/xhr.js",
    "chars": 4298,
    "preview": "define( [\n\t\"../core\",\n\t\"../var/support\",\n\t\"../ajax\"\n], function( jQuery, support ) {\n\n\"use strict\";\n\njQuery.ajaxSettings"
  },
  {
    "path": "www/jslib/jquery/src/ajax.js",
    "chars": 22222,
    "preview": "define( [\n\t\"./core\",\n\t\"./var/document\",\n\t\"./var/rnotwhite\",\n\t\"./ajax/var/location\",\n\t\"./ajax/var/nonce\",\n\t\"./ajax/var/rq"
  },
  {
    "path": "www/jslib/jquery/src/attributes/attr.js",
    "chars": 3099,
    "preview": "define( [\n\t\"../core\",\n\t\"../core/access\",\n\t\"./support\",\n\t\"../var/rnotwhite\",\n\t\"../selector\"\n], function( jQuery, access, "
  },
  {
    "path": "www/jslib/jquery/src/attributes/classes.js",
    "chars": 4249,
    "preview": "define( [\n\t\"../core\",\n\t\"../var/rnotwhite\",\n\t\"../data/var/dataPriv\",\n\t\"../core/init\"\n], function( jQuery, rnotwhite, data"
  },
  {
    "path": "www/jslib/jquery/src/attributes/prop.js",
    "chars": 2755,
    "preview": "define( [\n\t\"../core\",\n\t\"../core/access\",\n\t\"./support\",\n\t\"../selector\"\n], function( jQuery, access, support ) {\n\n\"use str"
  },
  {
    "path": "www/jslib/jquery/src/attributes/support.js",
    "chars": 786,
    "preview": "define( [\n\t\"../var/document\",\n\t\"../var/support\"\n], function( document, support ) {\n\n\"use strict\";\n\n( function() {\n\tvar i"
  },
  {
    "path": "www/jslib/jquery/src/attributes/val.js",
    "chars": 4141,
    "preview": "define( [\n\t\"../core\",\n\t\"./support\",\n\t\"../core/init\"\n], function( jQuery, support ) {\n\n\"use strict\";\n\nvar rreturn = /\\r/g"
  },
  {
    "path": "www/jslib/jquery/src/attributes.js",
    "chars": 217,
    "preview": "define( [\n\t\"./core\",\n\t\"./attributes/attr\",\n\t\"./attributes/prop\",\n\t\"./attributes/classes\",\n\t\"./attributes/val\"\n], functio"
  },
  {
    "path": "www/jslib/jquery/src/callbacks.js",
    "chars": 5483,
    "preview": "define( [\n\t\"./core\",\n\t\"./var/rnotwhite\"\n], function( jQuery, rnotwhite ) {\n\n\"use strict\";\n\n// Convert String-formatted o"
  },
  {
    "path": "www/jslib/jquery/src/core/DOMEval.js",
    "chars": 292,
    "preview": "define( [\n\t\"../var/document\"\n], function( document ) {\n\t\"use strict\";\n\n\tfunction DOMEval( code, doc ) {\n\t\tdoc = doc || d"
  },
  {
    "path": "www/jslib/jquery/src/core/access.js",
    "chars": 1234,
    "preview": "define( [\n\t\"../core\"\n], function( jQuery ) {\n\n\"use strict\";\n\n// Multifunctional method to get and set values of a collec"
  },
  {
    "path": "www/jslib/jquery/src/core/init.js",
    "chars": 3316,
    "preview": "// Initialize a jQuery object\ndefine( [\n\t\"../core\",\n\t\"../var/document\",\n\t\"./var/rsingleTag\",\n\t\"../traversing/findFilter\""
  },
  {
    "path": "www/jslib/jquery/src/core/parseHTML.js",
    "chars": 1604,
    "preview": "define( [\n\t\"../core\",\n\t\"../var/document\",\n\t\"./var/rsingleTag\",\n\t\"../manipulation/buildFragment\",\n\n\t// This is the only m"
  },
  {
    "path": "www/jslib/jquery/src/core/ready-no-deferred.js",
    "chars": 2498,
    "preview": "define( [\n\t\"../core\",\n\t\"../var/document\"\n], function( jQuery, document ) {\n\n\"use strict\";\n\nvar readyCallbacks = [],\n\trea"
  },
  {
    "path": "www/jslib/jquery/src/core/ready.js",
    "chars": 2250,
    "preview": "define( [\n\t\"../core\",\n\t\"../var/document\",\n\t\"../core/readyException\",\n\t\"../deferred\"\n], function( jQuery, document ) {\n\n\""
  },
  {
    "path": "www/jslib/jquery/src/core/readyException.js",
    "chars": 168,
    "preview": "define( [\n\t\"../core\"\n], function( jQuery ) {\n\n\"use strict\";\n\njQuery.readyException = function( error ) {\n\twindow.setTime"
  },
  {
    "path": "www/jslib/jquery/src/core/support.js",
    "chars": 631,
    "preview": "define( [\n\t\"../var/document\",\n\t\"../var/support\"\n], function( document, support ) {\n\n\"use strict\";\n\n// Support: Safari 8 "
  },
  {
    "path": "www/jslib/jquery/src/core/var/rsingleTag.js",
    "chars": 148,
    "preview": "define( function() {\n\t\"use strict\";\n\n\t// Match a standalone tag\n\treturn ( /^<([a-z][^\\/\\0>:\\x20\\t\\r\\n\\f]*)[\\x20\\t\\r\\n\\f]"
  },
  {
    "path": "www/jslib/jquery/src/core.js",
    "chars": 11329,
    "preview": "/* global Symbol */\n// Defining this global in .eslintrc would create a danger of using the global\n// unguarded in anoth"
  },
  {
    "path": "www/jslib/jquery/src/css/addGetHookIf.js",
    "chars": 530,
    "preview": "define( function() {\n\n\"use strict\";\n\nfunction addGetHookIf( conditionFn, hookFn ) {\n\n\t// Define the hook, we'll check on"
  },
  {
    "path": "www/jslib/jquery/src/css/adjustCSS.js",
    "chars": 1897,
    "preview": "define( [\n\t\"../core\",\n\t\"../var/rcssNum\"\n], function( jQuery, rcssNum ) {\n\n\"use strict\";\n\nfunction adjustCSS( elem, prop,"
  },
  {
    "path": "www/jslib/jquery/src/css/curCSS.js",
    "chars": 1510,
    "preview": "define( [\n\t\"../core\",\n\t\"./var/rnumnonpx\",\n\t\"./var/rmargin\",\n\t\"./var/getStyles\",\n\t\"./support\",\n\t\"../selector\" // Get jQue"
  },
  {
    "path": "www/jslib/jquery/src/css/hiddenVisibleSelectors.js",
    "chars": 317,
    "preview": "define( [\n\t\"../core\",\n\t\"../selector\"\n], function( jQuery ) {\n\n\"use strict\";\n\njQuery.expr.pseudos.hidden = function( elem"
  },
  {
    "path": "www/jslib/jquery/src/css/showHide.js",
    "chars": 2304,
    "preview": "define( [\n\t\"../core\",\n\t\"../data/var/dataPriv\",\n\t\"../css/var/isHiddenWithinTree\"\n], function( jQuery, dataPriv, isHiddenW"
  },
  {
    "path": "www/jslib/jquery/src/css/support.js",
    "chars": 2453,
    "preview": "define( [\n\t\"../core\",\n\t\"../var/document\",\n\t\"../var/documentElement\",\n\t\"../var/support\"\n], function( jQuery, document, do"
  },
  {
    "path": "www/jslib/jquery/src/css/var/cssExpand.js",
    "chars": 88,
    "preview": "define( function() {\n\t\"use strict\";\n\n\treturn [ \"Top\", \"Right\", \"Bottom\", \"Left\" ];\n} );\n"
  },
  {
    "path": "www/jslib/jquery/src/css/var/getStyles.js",
    "chars": 401,
    "preview": "define( function() {\n\t\"use strict\";\n\n\treturn function( elem ) {\n\n\t\t// Support: IE <=11 only, Firefox <=30 (#15098, #1415"
  },
  {
    "path": "www/jslib/jquery/src/css/var/isHiddenWithinTree.js",
    "chars": 1290,
    "preview": "define( [\n\t\"../../core\",\n\t\"../../selector\"\n\n\t// css is assumed\n], function( jQuery ) {\n\t\"use strict\";\n\n\t// isHiddenWithi"
  },
  {
    "path": "www/jslib/jquery/src/css/var/rmargin.js",
    "chars": 65,
    "preview": "define( function() {\n\t\"use strict\";\n\n\treturn ( /^margin/ );\n} );\n"
  },
  {
    "path": "www/jslib/jquery/src/css/var/rnumnonpx.js",
    "chars": 131,
    "preview": "define( [\n\t\"../../var/pnum\"\n], function( pnum ) {\n\t\"use strict\";\n\n\treturn new RegExp( \"^(\" + pnum + \")(?!px)[a-z%]+$\", \""
  },
  {
    "path": "www/jslib/jquery/src/css/var/swap.js",
    "chars": 520,
    "preview": "define( function() {\n\n\"use strict\";\n\n// A method for quickly swapping in/out CSS properties to get correct calculations."
  },
  {
    "path": "www/jslib/jquery/src/css.js",
    "chars": 11457,
    "preview": "define( [\n\t\"./core\",\n\t\"./var/pnum\",\n\t\"./core/access\",\n\t\"./css/var/rmargin\",\n\t\"./var/document\",\n\t\"./var/rcssNum\",\n\t\"./css"
  },
  {
    "path": "www/jslib/jquery/src/data/Data.js",
    "chars": 3943,
    "preview": "define( [\n\t\"../core\",\n\t\"../var/rnotwhite\",\n\t\"./var/acceptData\"\n], function( jQuery, rnotwhite, acceptData ) {\n\n\"use stri"
  },
  {
    "path": "www/jslib/jquery/src/data/var/acceptData.js",
    "chars": 318,
    "preview": "define( function() {\n\n\"use strict\";\n\n/**\n * Determines whether an object can have data\n */\nreturn function( owner ) {\n\n\t"
  },
  {
    "path": "www/jslib/jquery/src/data/var/dataPriv.js",
    "chars": 84,
    "preview": "define( [\n\t\"../Data\"\n], function( Data ) {\n\t\"use strict\";\n\n\treturn new Data();\n} );\n"
  },
  {
    "path": "www/jslib/jquery/src/data/var/dataUser.js",
    "chars": 84,
    "preview": "define( [\n\t\"../Data\"\n], function( Data ) {\n\t\"use strict\";\n\n\treturn new Data();\n} );\n"
  },
  {
    "path": "www/jslib/jquery/src/data.js",
    "chars": 4171,
    "preview": "define( [\n\t\"./core\",\n\t\"./core/access\",\n\t\"./data/var/dataPriv\",\n\t\"./data/var/dataUser\"\n], function( jQuery, access, dataP"
  },
  {
    "path": "www/jslib/jquery/src/deferred/exceptionHook.js",
    "chars": 640,
    "preview": "define( [\n\t\"../core\",\n\t\"../deferred\"\n], function( jQuery ) {\n\n\"use strict\";\n\n// These usually indicate a programmer mist"
  },
  {
    "path": "www/jslib/jquery/src/deferred.js",
    "chars": 10729,
    "preview": "define( [\n\t\"./core\",\n\t\"./var/slice\",\n\t\"./callbacks\"\n], function( jQuery, slice ) {\n\n\"use strict\";\n\nfunction Identity( v "
  },
  {
    "path": "www/jslib/jquery/src/deprecated.js",
    "chars": 596,
    "preview": "define( [\n\t\"./core\"\n], function( jQuery ) {\n\n\"use strict\";\n\njQuery.fn.extend( {\n\n\tbind: function( types, data, fn ) {\n\t\t"
  },
  {
    "path": "www/jslib/jquery/src/dimensions.js",
    "chars": 1729,
    "preview": "define( [\n\t\"./core\",\n\t\"./core/access\",\n\t\"./css\"\n], function( jQuery, access ) {\n\n\"use strict\";\n\n// Create innerHeight, i"
  },
  {
    "path": "www/jslib/jquery/src/effects/Tween.js",
    "chars": 3252,
    "preview": "define( [\n\t\"../core\",\n\t\"../css\"\n], function( jQuery ) {\n\n\"use strict\";\n\nfunction Tween( elem, options, prop, end, easing"
  },
  {
    "path": "www/jslib/jquery/src/effects/animatedSelector.js",
    "chars": 244,
    "preview": "define( [\n\t\"../core\",\n\t\"../selector\",\n\t\"../effects\"\n], function( jQuery ) {\n\n\"use strict\";\n\njQuery.expr.pseudos.animated"
  },
  {
    "path": "www/jslib/jquery/src/effects.js",
    "chars": 17234,
    "preview": "define( [\n\t\"./core\",\n\t\"./var/document\",\n\t\"./var/rcssNum\",\n\t\"./var/rnotwhite\",\n\t\"./css/var/cssExpand\",\n\t\"./css/var/isHidd"
  },
  {
    "path": "www/jslib/jquery/src/event/ajax.js",
    "chars": 346,
    "preview": "define( [\n\t\"../core\",\n\t\"../event\"\n], function( jQuery ) {\n\n\"use strict\";\n\n// Attach a bunch of functions for handling co"
  },
  {
    "path": "www/jslib/jquery/src/event/alias.js",
    "chars": 649,
    "preview": "define( [\n\t\"../core\",\n\n\t\"../event\",\n\t\"./trigger\"\n], function( jQuery ) {\n\n\"use strict\";\n\njQuery.each( ( \"blur focus focu"
  },
  {
    "path": "www/jslib/jquery/src/event/focusin.js",
    "chars": 1512,
    "preview": "define( [\n\t\"../core\",\n\t\"../data/var/dataPriv\",\n\t\"./support\",\n\n\t\"../event\",\n\t\"./trigger\"\n], function( jQuery, dataPriv, s"
  },
  {
    "path": "www/jslib/jquery/src/event/support.js",
    "chars": 133,
    "preview": "define( [\n\t\"../var/support\"\n], function( support ) {\n\n\"use strict\";\n\nsupport.focusin = \"onfocusin\" in window;\n\nreturn su"
  },
  {
    "path": "www/jslib/jquery/src/event/trigger.js",
    "chars": 5002,
    "preview": "define( [\n\t\"../core\",\n\t\"../var/document\",\n\t\"../data/var/dataPriv\",\n\t\"../data/var/acceptData\",\n\t\"../var/hasOwn\",\n\n\t\"../ev"
  },
  {
    "path": "www/jslib/jquery/src/event.js",
    "chars": 19017,
    "preview": "define( [\n\t\"./core\",\n\t\"./var/document\",\n\t\"./var/documentElement\",\n\t\"./var/rnotwhite\",\n\t\"./var/slice\",\n\t\"./data/var/dataP"
  },
  {
    "path": "www/jslib/jquery/src/exports/amd.js",
    "chars": 1024,
    "preview": "define( [\n\t\"../core\"\n], function( jQuery ) {\n\n\"use strict\";\n\n// Register as a named AMD module, since jQuery can be conc"
  },
  {
    "path": "www/jslib/jquery/src/exports/global.js",
    "chars": 706,
    "preview": "/* ExcludeStart */\n\n// This file is included in a different way from all the others\n// so the \"use strict\" pragma is not"
  },
  {
    "path": "www/jslib/jquery/src/jquery.js",
    "chars": 650,
    "preview": "define( [\n\t\"./core\",\n\t\"./selector\",\n\t\"./traversing\",\n\t\"./callbacks\",\n\t\"./deferred\",\n\t\"./deferred/exceptionHook\",\n\t\"./cor"
  },
  {
    "path": "www/jslib/jquery/src/manipulation/_evalUrl.js",
    "chars": 356,
    "preview": "define( [\n\t\"../ajax\"\n], function( jQuery ) {\n\n\"use strict\";\n\njQuery._evalUrl = function( url ) {\n\treturn jQuery.ajax( {\n"
  },
  {
    "path": "www/jslib/jquery/src/manipulation/buildFragment.js",
    "chars": 2454,
    "preview": "define( [\n\t\"../core\",\n\t\"./var/rtagName\",\n\t\"./var/rscriptType\",\n\t\"./wrapMap\",\n\t\"./getAll\",\n\t\"./setGlobalEval\"\n], function"
  },
  {
    "path": "www/jslib/jquery/src/manipulation/getAll.js",
    "chars": 563,
    "preview": "define( [\n\t\"../core\"\n], function( jQuery ) {\n\n\"use strict\";\n\nfunction getAll( context, tag ) {\n\n\t// Support: IE <=9 - 11"
  },
  {
    "path": "www/jslib/jquery/src/manipulation/setGlobalEval.js",
    "chars": 381,
    "preview": "define( [\n\t\"../data/var/dataPriv\"\n], function( dataPriv ) {\n\n\"use strict\";\n\n// Mark scripts as having already been evalu"
  },
  {
    "path": "www/jslib/jquery/src/manipulation/support.js",
    "chars": 1034,
    "preview": "define( [\n\t\"../var/document\",\n\t\"../var/support\"\n], function( document, support ) {\n\n\"use strict\";\n\n( function() {\n\tvar f"
  },
  {
    "path": "www/jslib/jquery/src/manipulation/var/rcheckableType.js",
    "chars": 79,
    "preview": "define( function() {\n\t\"use strict\";\n\n\treturn ( /^(?:checkbox|radio)$/i );\n} );\n"
  },
  {
    "path": "www/jslib/jquery/src/manipulation/var/rscriptType.js",
    "chars": 83,
    "preview": "define( function() {\n\t\"use strict\";\n\n\treturn ( /^$|\\/(?:java|ecma)script/i );\n} );\n"
  },
  {
    "path": "www/jslib/jquery/src/manipulation/var/rtagName.js",
    "chars": 88,
    "preview": "define( function() {\n\t\"use strict\";\n\n\treturn ( /<([a-z][^\\/\\0>\\x20\\t\\r\\n\\f]+)/i );\n} );\n"
  },
  {
    "path": "www/jslib/jquery/src/manipulation/wrapMap.js",
    "chars": 798,
    "preview": "define( function() {\n\n\"use strict\";\n\n// We have to close these tags to support XHTML (#13200)\nvar wrapMap = {\n\n\t// Suppo"
  },
  {
    "path": "www/jslib/jquery/src/manipulation.js",
    "chars": 12438,
    "preview": "define( [\n\t\"./core\",\n\t\"./var/concat\",\n\t\"./var/push\",\n\t\"./core/access\",\n\t\"./manipulation/var/rcheckableType\",\n\t\"./manipul"
  },
  {
    "path": "www/jslib/jquery/src/offset.js",
    "chars": 6345,
    "preview": "define( [\n\t\"./core\",\n\t\"./core/access\",\n\t\"./var/document\",\n\t\"./var/documentElement\",\n\t\"./css/var/rnumnonpx\",\n\t\"./css/curC"
  },
  {
    "path": "www/jslib/jquery/src/queue/delay.js",
    "chars": 636,
    "preview": "define( [\n\t\"../core\",\n\t\"../queue\",\n\t\"../effects\" // Delay is optional because of this dependency\n], function( jQuery ) {"
  },
  {
    "path": "www/jslib/jquery/src/queue.js",
    "chars": 3092,
    "preview": "define( [\n\t\"./core\",\n\t\"./data/var/dataPriv\",\n\t\"./deferred\",\n\t\"./callbacks\"\n], function( jQuery, dataPriv ) {\n\n\"use stric"
  },
  {
    "path": "www/jslib/jquery/src/selector-native.js",
    "chars": 6394,
    "preview": "define( [\n\t\"./core\",\n\t\"./var/document\",\n\t\"./var/documentElement\",\n\t\"./var/hasOwn\",\n\t\"./var/indexOf\"\n], function( jQuery,"
  },
  {
    "path": "www/jslib/jquery/src/selector-sizzle.js",
    "chars": 411,
    "preview": "define( [\n\t\"./core\",\n\t\"../external/sizzle/dist/sizzle\"\n], function( jQuery, Sizzle ) {\n\n\"use strict\";\n\njQuery.find = Siz"
  },
  {
    "path": "www/jslib/jquery/src/selector.js",
    "chars": 66,
    "preview": "define( [ \"./selector-sizzle\" ], function() {\n\t\"use strict\";\n} );\n"
  },
  {
    "path": "www/jslib/jquery/src/serialize.js",
    "chars": 3141,
    "preview": "define( [\n\t\"./core\",\n\t\"./manipulation/var/rcheckableType\",\n\t\"./core/init\",\n\t\"./traversing\", // filter\n\t\"./attributes/pro"
  },
  {
    "path": "www/jslib/jquery/src/traversing/findFilter.js",
    "chars": 2334,
    "preview": "define( [\n\t\"../core\",\n\t\"../var/indexOf\",\n\t\"./var/rneedsContext\",\n\t\"../selector\"\n], function( jQuery, indexOf, rneedsCont"
  },
  {
    "path": "www/jslib/jquery/src/traversing/var/dir.js",
    "chars": 371,
    "preview": "define( [\n\t\"../../core\"\n], function( jQuery ) {\n\n\"use strict\";\n\nreturn function( elem, dir, until ) {\n\tvar matched = [],"
  },
  {
    "path": "www/jslib/jquery/src/traversing/var/rneedsContext.js",
    "chars": 128,
    "preview": "define( [\n\t\"../../core\",\n\t\"../../selector\"\n], function( jQuery ) {\n\t\"use strict\";\n\n\treturn jQuery.expr.match.needsContex"
  },
  {
    "path": "www/jslib/jquery/src/traversing/var/siblings.js",
    "chars": 218,
    "preview": "define( function() {\n\n\"use strict\";\n\nreturn function( n, elem ) {\n\tvar matched = [];\n\n\tfor ( ; n; n = n.nextSibling ) {\n"
  },
  {
    "path": "www/jslib/jquery/src/traversing.js",
    "chars": 4166,
    "preview": "define( [\n\t\"./core\",\n\t\"./var/indexOf\",\n\t\"./traversing/var/dir\",\n\t\"./traversing/var/siblings\",\n\t\"./traversing/var/rneedsC"
  },
  {
    "path": "www/jslib/jquery/src/var/ObjectFunctionString.js",
    "chars": 110,
    "preview": "define( [\n\t\"./fnToString\"\n], function( fnToString ) {\n\t\"use strict\";\n\n\treturn fnToString.call( Object );\n} );\n"
  },
  {
    "path": "www/jslib/jquery/src/var/arr.js",
    "chars": 54,
    "preview": "define( function() {\n\t\"use strict\";\n\n\treturn [];\n} );\n"
  },
  {
    "path": "www/jslib/jquery/src/var/class2type.js",
    "chars": 82,
    "preview": "define( function() {\n\t\"use strict\";\n\n\t// [[Class]] -> type pairs\n\treturn {};\n} );\n"
  },
  {
    "path": "www/jslib/jquery/src/var/concat.js",
    "chars": 81,
    "preview": "define( [\n\t\"./arr\"\n], function( arr ) {\n\t\"use strict\";\n\n\treturn arr.concat;\n} );\n"
  },
  {
    "path": "www/jslib/jquery/src/var/document.js",
    "chars": 67,
    "preview": "define( function() {\n\t\"use strict\";\n\n\treturn window.document;\n} );\n"
  },
  {
    "path": "www/jslib/jquery/src/var/documentElement.js",
    "chars": 105,
    "preview": "define( [\n\t\"./document\"\n], function( document ) {\n\t\"use strict\";\n\n\treturn document.documentElement;\n} );\n"
  },
  {
    "path": "www/jslib/jquery/src/var/fnToString.js",
    "chars": 92,
    "preview": "define( [\n\t\"./hasOwn\"\n], function( hasOwn ) {\n\t\"use strict\";\n\n\treturn hasOwn.toString;\n} );\n"
  },
  {
    "path": "www/jslib/jquery/src/var/getProto.js",
    "chars": 73,
    "preview": "define( function() {\n\t\"use strict\";\n\n\treturn Object.getPrototypeOf;\n} );\n"
  },
  {
    "path": "www/jslib/jquery/src/var/hasOwn.js",
    "chars": 110,
    "preview": "define( [\n\t\"./class2type\"\n], function( class2type ) {\n\t\"use strict\";\n\n\treturn class2type.hasOwnProperty;\n} );\n"
  },
  {
    "path": "www/jslib/jquery/src/var/indexOf.js",
    "chars": 82,
    "preview": "define( [\n\t\"./arr\"\n], function( arr ) {\n\t\"use strict\";\n\n\treturn arr.indexOf;\n} );\n"
  },
  {
    "path": "www/jslib/jquery/src/var/pnum.js",
    "chars": 100,
    "preview": "define( function() {\n\t\"use strict\";\n\n\treturn ( /[+-]?(?:\\d*\\.|)\\d+(?:[eE][+-]?\\d+|)/ ).source;\n} );\n"
  },
  {
    "path": "www/jslib/jquery/src/var/push.js",
    "chars": 79,
    "preview": "define( [\n\t\"./arr\"\n], function( arr ) {\n\t\"use strict\";\n\n\treturn arr.push;\n} );\n"
  },
  {
    "path": "www/jslib/jquery/src/var/rcssNum.js",
    "chars": 136,
    "preview": "define( [\n\t\"../var/pnum\"\n], function( pnum ) {\n\n\"use strict\";\n\nreturn new RegExp( \"^(?:([+-])=|)(\" + pnum + \")([a-z%]*)$"
  },
  {
    "path": "www/jslib/jquery/src/var/rnotwhite.js",
    "chars": 62,
    "preview": "define( function() {\n\t\"use strict\";\n\n\treturn ( /\\S+/g );\n} );\n"
  },
  {
    "path": "www/jslib/jquery/src/var/slice.js",
    "chars": 80,
    "preview": "define( [\n\t\"./arr\"\n], function( arr ) {\n\t\"use strict\";\n\n\treturn arr.slice;\n} );\n"
  },
  {
    "path": "www/jslib/jquery/src/var/support.js",
    "chars": 117,
    "preview": "define( function() {\n\t\"use strict\";\n\n\t// All support tests are defined in their respective modules.\n\treturn {};\n} );\n"
  },
  {
    "path": "www/jslib/jquery/src/var/toString.js",
    "chars": 104,
    "preview": "define( [\n\t\"./class2type\"\n], function( class2type ) {\n\t\"use strict\";\n\n\treturn class2type.toString;\n} );\n"
  },
  {
    "path": "www/jslib/jquery/src/wrap.js",
    "chars": 1457,
    "preview": "define( [\n\t\"./core\",\n\t\"./core/init\",\n\t\"./manipulation\", // clone\n\t\"./traversing\" // parent, contents\n], function( jQuery"
  },
  {
    "path": "www/login.html",
    "chars": 2350,
    "preview": "<div ng-controller=\"loginAppCtl\">\n    <div ng-if=\"!isLoading\" class=\"row\">\n        <div class=\"card-panel grey lighten-5"
  },
  {
    "path": "www/logout.html",
    "chars": 46,
    "preview": "<div ng-controller=\"logoutAppCtl\">\n    \n</div>"
  },
  {
    "path": "www/notifications.html",
    "chars": 1294,
    "preview": "<div ng-controller=\"notificationsAppCtl\">\n    <div ng-if=\"!isLoading\" class=\"row\">\n        <div ng-if=\"1 == 0\" class=\"se"
  },
  {
    "path": "www/register.html",
    "chars": 3495,
    "preview": "<div ng-controller=\"registerAppCtl\">\n    <div ng-if=\"!isLoading\" class=\"row\">\n        <div class=\"card-panel grey lighte"
  },
  {
    "path": "www/websocket.html",
    "chars": 6394,
    "preview": "<!DOCTYPE html>\n<html>\n<meta charset=\"utf-8\" />\n<title>WebSocket Test</title>\n<!--materialize css-->\n<link href=\"jslib/m"
  }
]

// ... and 2 more files (download for full content)

About this extraction

This page contains the full source code of the cookeem/CookIM GitHub repository, extracted and formatted as plain text for AI agents and large language models (LLMs). The extraction includes 202 files (2.6 MB), approximately 692.2k tokens, and a symbol index with 609 extracted functions, classes, methods, constants, and types. Use this with OpenClaw, Claude, ChatGPT, Cursor, Windsurf, or any other AI tool that accepts text input. You can copy the full output to your clipboard or download it as a .txt file.

Extracted by GitExtract — free GitHub repo to text converter for AI. Built by Nikandr Surkov.

Copied to clipboard!