[
  {
    "path": ".eslintignore",
    "content": "/http/res/*\nconfig.js"
  },
  {
    "path": ".eslintrc",
    "content": "{\n  \"env\": {\n    \"es6\": true,\n    \"browser\": false,\n    \"node\": true\n  },\n  \"parser\": \"babel-eslint\",\n  \"parserOptions\": {\n    \"sourceType\": \"module\"\n  },\n  \"plugins\": [\n    \"babel\"\n  ],\n  \"extends\": [\n    \"standard\"\n  ]\n}"
  },
  {
    "path": ".gitattributes",
    "content": "# Auto detect text files and perform LF normalization\n* text=auto\n\n# Custom for Visual Studio\n*.cs     diff=csharp\n\n# Standard to msysgit\n*.doc\t diff=astextplain\n*.DOC\t diff=astextplain\n*.docx diff=astextplain\n*.DOCX diff=astextplain\n*.dot  diff=astextplain\n*.DOT  diff=astextplain\n*.pdf  diff=astextplain\n*.PDF\t diff=astextplain\n*.rtf\t diff=astextplain\n*.RTF\t diff=astextplain\n"
  },
  {
    "path": ".gitignore",
    "content": ".vs\n.idea\n.vscode\n*.csv\n\n# Logs\nlogs\n*.log\n\n# Runtime data\npids\n*.pid\n*.seed\n\n# Directory for instrumented libs generated by jscoverage/JSCover\nlib-cov\n\n# Coverage directory used by tools like istanbul\ncoverage\n\n# Grunt intermediate storage (http://gruntjs.com/creating-plugins#storing-task-files)\n.grunt\n\n# Compiled binary addons (http://nodejs.org/api/addons.html)\nbuild/Release\n\n# Dependency directory\n# Commenting this out is preferred by some people, see\n# https://www.npmjs.org/doc/misc/npm-faq.html#should-i-check-my-node_modules-folder-into-git-\nnode_modules\n\n# Users Environment Variables\n.lock-wscript\n\n# =========================\n# Operating System Files\n# =========================\n\n# OSX\n# =========================\n\n.DS_Store\n.AppleDouble\n.LSOverride\n\n# Thumbnails\n._*\n\n# Files that might appear on external disk\n.Spotlight-V100\n.Trashes\n\n# Directories potentially created on remote AFP share\n.AppleDB\n.AppleDesktop\nNetwork Trash Folder\nTemporary Items\n.apdisk\n\n# Windows\n# =========================\n\n# Windows image file caches\nThumbs.db\nehthumbs.db\n\n# Folder config file\nDesktop.ini\n\n# Recycle Bin used on file shares\n$RECYCLE.BIN/\n\n# Windows Installer files\n*.cab\n*.msi\n*.msm\n*.msp\n\n# Windows shortcuts\n*.lnk\n\n# Visual Studio Code\ntypings\n"
  },
  {
    "path": "Dockerfile",
    "content": "FROM node:latest\nMAINTAINER zsx <zsx@zsxsoft.com>\nENV APP /usr/src/app\n\n## ----------------------------\n##       MariaDB Start\n## ----------------------------\n## Add MariaDB PPK\nRUN apt-key adv --recv-keys --keyserver hkp://keyserver.ubuntu.com:80 0xcbcb082a1bb943db && \\\n    echo 'deb http://mirrors.syringanetworks.net/mariadb/repo/10.1/ubuntu trusty main' >> /etc/apt/sources.list && \\\n    echo 'deb-src http://mirrors.syringanetworks.net/mariadb/repo/10.1/ubuntu trusty main' >> /etc/apt/sources.list  && \\\n    apt-get update && \\\n    DEBIAN_FRONTEND=noninteractive apt-get install -y mariadb-server pwgen && \\\n    rm -rf /var/lib/mysql/* && \\\n    sed -i -r 's/bind-address.*$/bind-address = 0.0.0.0/' /etc/mysql/my.cnf && \\\n    apt-get install -y memcached && \\\n    mkdir -p ${APP}\n\n\nWORKDIR ${APP}/\nADD ./ ./\nADD ./docker/ /docker\nRUN chmod +x /docker/*.sh && \\\n    npm install && \\\n    npm cache clean && apt-get clean && rm -rf /var/lib/apt/lists/*\n\nVOLUME  [\"/etc/mysql\", \"/var/lib/mysql\"]\nEXPOSE 3306 11211 3000\nCMD [\"/docker/run.sh\"]\n"
  },
  {
    "path": "README.md",
    "content": "danmu-server\n================\n[![David deps](https://david-dm.org/zsxsoft/danmu-server.svg)](https://david-dm.org/zsxsoft/danmu-server)\n\n弹幕服务器，其客户端项目见[danmu-client](https://github.com/zsxsoft/danmu-client)。\n\n**欲使用此项目，客户端需要使用对应的版本。[已发布的服务端](https://github.com/zsxsoft/danmu-server/releases)均已写明对应的客户端版本号，开发分支内的服务端版本仅对应开发分支的客户端。**\n\n## 功能特色\n- 跨平台；\n- 房间功能；\n- 后台管理；\n- 弹幕记录与搜索（需要开启数据库）；\n- 黑名单功能；\n- 关键词替换、拦截功能；\n- 弹幕记录；\n- 扩展；\n   - 新浪微博登录扩展（需要开启缓存）；\n   - 自动封禁功能扩展（需要开启缓存）；\n   - 审核扩展；\n   - 直播拉取扩展\n- 删除单条弹幕功能；\n- 易于部署，简单高效。\n\n## 后台截图\n![后台截图](http://zsxsoft.github.io/danmu-server/management.png)\n\n## 一些警告\n\n稳定版请于[Release](https://github.com/zsxsoft/danmu-server/releases)手动下载。\n\n\n## 部署方式\n\n### 检查环境\n\n#### Nodejs\n\nNodejs >= 6\n\n#### 数据库\n 如使用``csv``，可无视此节。\n\n 默认使用``MySQL``数据库。如需使用，需检查[MariaDB](https://mariadb.org/)或[MySQL](https://www.mysql.com/)的安装状态。支持``5.0+``。安装完成后，请创建相应的数据库。\n\n 如使用``MongoDB``数据库，请检查[MongoDB](https://www.mongodb.org/)的安装状态。然后需要在安装完成后执行：``npm install mongodb``。\n\n#### 缓存\n\n 如不使用新浪微博与自动封禁功能，可无视此节。\n\n 默认使用``memcached``。如需使用，请检查[Memcached(Linux)](http://memcached.org/)的安装状态。``Windows``用户请自行查找适合的``Memcached``版本。\n\n 如果要用[阿里云开放缓存服务OCS](http://www.aliyun.com/product/ocs/)，需要在安装完成后执行：``npm install aliyun-sdk``。\n\n### 直接安装\n 1. 配置MariaDB，创建数据库等，不需要创建数据表。\n 2. 修改``config.js``，使其参数与环境相符。\n 3. 切换到命令行或终端，``cd``到程序所在目录执行``npm install``，安装程序依赖库。\n 4. 现在，你可以直接``npm start``启动。\n\n### Docker安装\n\n__Dockerfile可能年久失修，建议自己用``alpine``封装一个。__\n直接用``Docker``安装的话，镜像内是含``MariaDB``的。\n 1. [安装Docker](http://yeasy.gitbooks.io/docker_practice/content/install/index.html)。\n 2. ``config.js``调整配置。\n 3. ``docker build -t=\"zsxsoft/danmu-server:\" . && docker run -t -i -p 3000:3000 \"zsxsoft/danmu-server\"``\n\n## 升级\n### 1.0.6 -> 1.1.0\n* 在每个房间内增加cdn: false配置\n### 1.0.5 -> 1.0.6\n* 在每个房间内增加hostname配置，类型为数组，用于将房间与域名绑定\n\n## 网页接口\n\n### GET /\n可以直接发布最简单的弹幕。\n\n### GET /advanced\n可以发布高级弹幕（需要密码）\n\n### GET /manage\n可以进行后台管理\n\n### GET /realtime\n可以实时接收弹幕并直接删除或封禁（需要密码）\n\n## 配置说明\n\n以下标有``*``的配置项，运行时不可在后台修改。\n\n```javascript\n\t\"rooms\": {\n\t\t\"房间1\": {\n\t\t\t* \"hostname\": [\"test.zsxsoft.com\", \"localhost\", \"127.0.0.1\"],\n\t\t\t* \"cdn\": 是否使用CDN或反向代理（用于获取正确的IP）,\n\t\t\t* \"display\": \"房间显示名\",\n\t\t\t* \"table\": \"对应MySQL的数据表、MongoDB的集合\",\n\t\t\t\"connectpassword\": \"客户端连接密码\",\n\t\t\t\"managepassword\": \"管理密码\",\n\t\t\t\"advancedpassword\": \"高级弹幕密码\",\n\t\t\t\"keyword\": {\n\t\t\t\t\"block\": /强制屏蔽关键词，正则格式。/\n\t\t\t\t\"replacement\": /替换关键词，正则格式/,\n\t\t\t\t\"ignore\": /忽略词，正则格式/\n\t\t\t},\n\t\t\t\"blockusers\": [\n\t\t\t\t\"默认封禁用户列表\"\n\t\t\t],\n\t\t\t\"maxlength\": 弹幕堆积队列最大长度,\n\t\t\t\"textlength\": 每条弹幕最大长度,\n\t\t\t* \"image\": {\n\t\t\t\t* \"regex\": /图片弹幕解析正则，正则格式，不要修改/ig,\n\t\t\t\t* \"lifetime\": 每个图片给每条弹幕增加的存货时间\n\t\t\t},\n\t\t\t\"permissions\": { // 普通用户允许的弹幕权限\n\t\t\t\t\"send\": 弹幕开关；关闭后无论普通用户还是高级权限都完全禁止弹幕。,\n\t\t\t\t\"style\": 弹幕样式开关,\n\t\t\t\t\"color\": 颜色开关,\n\t\t\t\t\"textStyle\": CSS开关,\n\t\t\t\t\"height\": 高度开关,\n\t\t\t\t\"lifeTime\": 显示时间开关,\n\t\t\t}\n\t\t},\n\t\t\"房间ID2\": {\n\t\t\t// 同上\n\t\t}\n\t},\n\t* \"database\": { // 数据库\n\t\t* \"type\": \"数据库类型（mysql / mongo / csv / none）\",\n\t\t* \"server\": \" 数据库地址（mysql / mongo）\",\n\t\t* \"username\": \"数据库用户名（mysql / mongo）\",\n\t\t* \"password\": \"数据库密码（mysql / mongo）\",\n\t\t* \"port\": \"数据库端口（mysql / mongo）\",\n\t\t* \"db\": \"数据库（mysql / mongo）\",\n\t\t* \"retry\": 24小时允许断线重连最大次数，超过则自动退出程序。24小时以第一次断线时间计。（mysql）,\n\t\t* \"timeout\": 数据库重连延时及Ping（mysql）,\n\t\t* \"savedir\": \"指定文件保存位置（csv）\",\n\t},\n\t\"websocket\": {\n\t\t\"interval\": 弹幕发送间隔\n\t\t\"singlesize\": 每次弹幕发送数量\n\t},\n\t* \"http\": {\n\t\t* \"port\": 服务器HTTP端口,\n\t\t* \"headers\": {}, // HTTP头\n\t\t* \"sessionKey\": \"随便写点，防冲突的\"\n\t},\n\t* \"cache\": {\n\t\t* \"type\": \"缓存类型（memcached / aliyun）\",\n\t\t* \"host\": \"缓存服务器地址，可用socket\",\n\t\t* \"auth\": 打开身份验证,\n\t\t* \"authUser\": 身份验证账号,\n\t\t* \"authPassword\": 身份验证密码,\n\t},\n\t\"ext\": {\n\t\t// 扩展\n\t}\n}\n```\n\n## 扩展\n### 新浪微博登录\n```javascript\n\"weibo\": { // 新浪微博扩展\n\t\"clientID\": '', // App ID\n\t\"clientSecret\": '', // App Secret\n\t\"callbackURL\": 'http://test.zsxsoft.com:3000/auth/sina/callback', // 这里填写的是 网站地址/auth/sina/callback\n\t\"requireState\": true // 是否打开CSRF防御\n}\n```\n\n### 自动封禁\n```javascript\n\"autoban\": { // 自动封号扩展\n\t\"block\": 3, // 被拦截超过一定数字自动封号\n}\n```\n\n### 全局审核\n```javascript\n\"audit\": { // 审核扩展\n}\n```\n\n### 直播同步\n\n此扩展基于[danmu](https://github.com/littlecodersh/danmu)项目开发，需要安装Python 2.7+ 或 Python 3.5+。在启用前，你首先需要\n```bash\npip install danmu\n```\n\n才可打开。\n\n```javascript\n\"livesync\": { // 新浪微博扩展\n\t\"房间名\": {\n\t\t\"liveUrl\": '', // 直播网站地址\n\t}\n}\n```\n## 常见问题\n### 数据库相关\n``{ [Error: Connection lost: The server closed the connection.] fatal: true, code: 'PROTOCOL_CONNECTION_LOST' }``\n\n请把MySQL的``wait_timeout``设置得大一些。\n\n## 搭配项目\n\n- [danmu-client](https://github.com/zsxsoft/danmu-client)\n\n## 流程图\n\n![流程图](http://zsxsoft.github.io/danmu-server/route.png)\n\n## 协议\nThe MIT License (MIT)\n\n## 博文\n[弹幕服务器及搭配之透明弹幕客户端研究结题报告](http://blog.zsxsoft.com/post/15)\n\n[弹幕服务器及搭配之透明弹幕客户端研究中期报告](http://blog.zsxsoft.com/post/14)\n\n[弹幕服务器及搭配之透明弹幕客户端研究开题报告](http://blog.zsxsoft.com/post/13)\n\n## 开发者\nzsx - https://www.zsxsoft.com / 博客 - https://blog.zsxsoft.com\n"
  },
  {
    "path": "app.js",
    "content": "const os = require('os')\nconst async = require('async')\nconst fs = require('fs')\nconst path = require('path')\nconst configEvent = require('./src/interfaces/Config')\nconst log = require('./src/utilities/log')\nconst packageJson = require('./package.json')\n\nlet config = require('./config')\n\nglobal.version = packageJson.version\nglobal.Promise = require('bluebird')\n\n{\n  log.log(`弹幕服务器版本：${global.version}`)\n  log.log(`环境：${os.platform()}(${os.release()}) ${os.arch()} with ${parseInt(os.totalmem() / 1024 / 1024)}MB`)\n\n  let dbPos = config.database\n  if (process.env.MYSQL_PORT_3306_TCP_PORT) { // 检测DaoCloud的MySQL服务\n    dbPos.type = 'mysql'\n    dbPos.server = process.env.MYSQL_PORT_3306_TCP_ADDR\n    dbPos.username = process.env.MYSQL_USERNAME\n    dbPos.password = process.env.MYSQL_PASSWORD\n    dbPos.port = process.env.MYSQL_PORT_3306_TCP_PORT\n    dbPos.db = process.env.MYSQL_INSTANCE_NAME\n    console.log('检测到配置在环境变量内的MySQL，自动使用之。')\n  } else if (dbPos.type === 'mongo' && process.env['27017/tcp']) { // MongoDB服务\n    dbPos.type = 'mongo'\n    dbPos.server = process.env['27017/tcp'].split(':')[0].trim() // tcp://xx.xx.xx.xx:27017\n    dbPos.port = process.env['27017/tcp'].split(':')[1].trim() // tcp://xx.xx.xx.xx:27017\n    dbPos.username = process.env.USERNAME\n    dbPos.password = process.env.PASSWORD\n    dbPos.db = process.env.INSTANCE_NAME\n    console.log('检测到配置在环境变量内的MongoDB，自动使用之。')\n  }\n\n  // 加载模块\n  async.map(['extensions', 'libraries/cache', 'libraries/transfer', 'libraries/database', 'libraries/http', 'libraries/socket'], (mdl, callback) => {\n    require(`./src/${mdl}`).init(callback)\n  }, err => {\n    if (err) throw err\n    fs.readdir(path.join(__dirname, './src/controllers'), (err, files) => {\n      if (err) throw err\n      files.forEach((filename) => require(path.join(__dirname, './src/controllers', filename)))\n    })\n    configEvent.updated.emit()\n    log.log('服务器初始化完成')\n  })\n}\n"
  },
  {
    "path": "config.js",
    "content": "module.exports = {\n\t\"rooms\": {\n\t\t\"default\": {\n\t\t\t\"hostname\": [\"test.zsxsoft.com\", \"danmu.zsxsoft.com\"], \n\t\t\t\"cdn\": false,\n\t\t\t\"display\": \"默认\",\n\t\t\t\"table\": \"room_default\", // 数据表\n\t\t\t\"connectpassword\": \"123456\", // 客户端连接密码\n\t\t\t\"managepassword\": \"123456\", // 管理密码\n\t\t\t\"advancedpassword\": \"123456\", // 高级弹幕密码\n\t\t\t\"keyword\": {\n\t\t\t\t\"block\": /宣你|阳痿|臭脚|难听|难看|法克|木耳|纵欲|dick|爽|下流|非礼|煞笔|傻比|沙比|蠢|丁丁|抠脚|奸|粑|EXO|TFBoys|王源|王俊凯|易烊千玺|吴亦凡|鹿晗|Love|土狗|我爱|爱你|我喜欢|喜欢你|嫁|在一起|娶|啪啪啪|性交|(大|肉|小|贫|丰|巨|胸|乳)(胸|罩|乳|房|棒)|fuck|bitch|傻|残|垃圾|(大|小)便|屎|滚|逼|屄|叼|屌|草泥马|陪侍|女友|阴茎|睾丸|附睾|阴囊|前列腺|精液|尿道|精囊|阴蒂|阴道|阴唇|子宫|输卵|卵巢|(前|后)庭|(推广|群发|广告|解密|赌博|包青天|阿凡提|发贴|顶贴|(针孔|隐形|隐蔽)摄像|干扰|顶帖|发帖|消声|遥控|解码|窃听|身份证生成|拦截|复制|监听|定位|消声|作弊|扩散|侦探|追杀)(机|器|软件|设备|系统)|(求|换|有偿|买|卖|出售)(肾|器官|眼角膜|血)|肾源|(假|毕业)(证|文凭|发票|币)|(手榴|人|麻醉|霰)弹|治疗(肿瘤|乙肝|性病|红斑狼疮)|重亚硒酸钠|(粘氯|原砷)酸|麻醉乙醚|原藜芦碱A|永伏虫|蝇毒|罂粟|银氰化钾|氯胺酮|因毒(硫磷|磷)|异氰酸(甲酯|苯酯)|异硫氰酸烯丙酯|乙酰(亚砷酸铜|替硫脲)|乙烯甲醇|乙酸(亚铊|铊|三乙基锡|三甲基锡|甲氧基乙基汞|汞)|乙硼烷|乙醇腈|乙撑亚胺|乙撑氯醇|伊皮恩|海洛因|一氧(化汞|化二氟)|一氯(乙醛|丙酮)|氧氯化磷|氧化(亚铊|铊|汞|二丁基锡)|烟碱|亚硝酰乙氧|亚硝酸乙酯|亚硒酸氢钠|亚硒酸钠|亚硒酸镁|亚硒酸二钠|亚硒酸|亚砷酸(钠|钾|酐)|冰毒|摇头丸|预测答案|考前预测|押题|代写论文|(提供|司考|级|传送|考中|短信)答案|(待|代|带|替|助)考|(包|顺利|保)过|考后付款|作弊|考前密卷|漏题|中特|一肖|报码|(合|香港)彩|彩宝|3D轮盘|liuhecai|一码|(皇家|俄罗斯)轮盘|赌具|特码|盗取?(号|qq|密码)|嗑药|帮招人|社会混|拜大哥|电警棒|帮人怀孕|切腹|电鸡|手枪|炸弹|走私|陪聊|h(图|漫|网)|开苞|找(男|女)|(口|足|胸|乳)(淫|交|推)|后入式|卖身|一夜|(男|女)奴|双(筒|桶)|看JJ|(做|坐)台|厕奴|骚女|嫩逼|一夜激情|乱伦|泡友|富(姐|婆)|(足|群|茹)交|阴户|性(服务|伴侣|伙伴|交)|(有|无)码|包养|(犬|兽|幼)交|根浴|援交|性(虐|爱|息)|刻章|昏药|性奴|透视眼(睛|镜)|拍肩神|(失忆|催情|迷(幻|昏|奸)?|安定)(药|片|香)|香港生子|土炮|胎盘|手机魔卡|容弹量|枪模|铅弹|汽(枪|狗|走表器)|气枪|气狗|伟哥|纽扣摄像机|免电灯|麻醉药|康生丹|警徽|记号扑克|激光(汽|气)|红床|狗友|电子狗导航手机|弹(种|夹)|(追|讨)债|避孕|办理(证件|文凭)|斑蝥|暗访包|BB(枪|弹)|雷管|弓弩|(电|长)狗|导爆索|爆炸物|爆破|左棍|婊子|换妻|成人片|淫(靡|水|兽)|阴(毛|蒂|道|唇)|小穴|缩阴|少妇自拍|(三级|色情|激情|黄色|小)(片|电影|视频|交友|电话)|肉棒|(情|奸)杀|裸照|乱伦|口交|禁(网|片)|春宫图|SM用品|自动群发|私家侦探服务|生意宝|商务(快车|短信)|慧聪|供应发票|发票代开|短信群发|短信猫|点金商务|士的宁|士的年|六合(采|彩)|乐透码|彩票|百乐二呓|百家乐|黄页|出租|求购|留学咨询|外挂|网络(兼职|赚钱)|(证件|婚庆|翻译|搬家|追债|债务)公司|手机(游戏|窃听|监听|铃声|图片)|三唑仑|彩(信|铃|票)|显示屏|投影仪|虚拟主机|(域名|专业)注册|营销|性病|不孕不育|乳腺病|尖锐湿疣|皮肤病|减肥|瘦|3P|人兽|代孕|打炮|找小姐|刻章|乱伦|中出|楼凤|卖淫|荡妇|群交|幼女|18禁|伦理电影|(催情|蒙汗|蒙汉|春)药|情趣用品|成人.+?(电影|用品)|激情(视频|电影|影院)|爽片|美女|交友|怀孕|裸聊|制服诱惑|丝袜|长腿|寂寞女子|双色球|福彩|体彩|6合彩|时时彩|双色球|咨询热线|股票|荐股|开股|私服|枪|警棒|警服|麻醉|诚招加盟|诚信经营|杀手|(游戏|金)币|群发|加盟|名表|特卖|分销|残党|共惨党|共匪|赤匪|裆中央|北京当局|中宣|真理部|十八大|18大|太子|上海帮|团派|九常委|九长老|政治局常委内幕|锦涛|hujin|家宝|影帝|wenjiabao|wjb|近平|xijinping|xjp|假庆淋|jiaqinglin|李月月鳥|李鹏|回良玉|汪洋|岐山|王山支山|wangqishan张高丽|俞正声|徐才厚|郭伯雄|梁光烈|孟建柱|戴秉国|马凯|计划|韩正|章沁生|陈世炬|泽民|贼民|先皇|太上皇|蛤蟆|驾崩|jiangzemin|jzm|邓小平|庆红|罗干|likeqiang|zhouyongkang|lichangchun|wubangguo|heguoqiang|老人政治|老人干政|陈光诚事件|自由光诚|陈光诚.*使馆|使馆.*陈光诚|光诚.*沂南|沂南.*光诚|要有光.*要有诚|要有诚.*要有光|马驰.*新加坡|职称英语.*答案|答案.*职称英语|公务员.*答案|答案.*公务员|重庆|熙来|薄督|不厚|薄瓜瓜|谷开来|海伍德|尼尔伍德|heywood|neil.*wood|wood.*neil|天线宝宝.*康师傅|康师傅.*天线宝宝|天线宝宝.*方便面|方便面.*天线宝宝|政变|暴动|枪声|戒严|3\\.19|北京事件|北京.*出事了|出事了.*北京|北京怎么了|叶城.*砍杀|砍杀.*叶城|弟弟.*睡|睡.*弟弟|山水文园|跑官|移动.*十年兴衰|十年兴衰.*移动|叶兵|张斌|陈瑞卿|高念书|华如秀|鲁向东|曲乃杰|孙静晔|涂志森|于剑鸣|张晓明|赵志强|郑建源|丘小雄|公诉|右派|增城|莫日根|内蒙古.*抗议|抗议.*内蒙古|西乌旗|天府|人民公园|埃及|突尼斯|茉莉|jasmine.*revolution|revolution.*jasmine|moli|集会|公开信|六四|六 四|六\\.四|64|天安门|八九|平反64|六月四日|5月35日|5月35号|89动乱|64memo|tiananmen|8964|天安.*事件|事件.*天安|1989.*天安門|天安門.*1989|开枪|广场|1989年|198964﻿|89.*学生动乱|学生动乱.*89|89.*学生运动|学生运动.*89|64.*学生运动|学生运动.*64|64.*镇压|镇压.*64|64.*真相|真相.*64|学潮|罢课|民运|学运|学联|学自联|高自联|工自联|坦克人|挡坦克|tankman|木犀地|维园晚会|blood is on the square|耀邦|紫阳|改革.*历程|历程.*改革|国家的囚徒|prisoner of the state|民联|民阵|中国民主党|中国民主正义党|中国民主运动|世纪中国基金会|姜维平|艾未未|艾末末|路青|发课|余杰|辛子陵|茅于轼|铁流|liu.*xiaobo|xiaobo.*liu|刘霞|我没有敌人|我的最后陈述|零八.*宪章|宪章.*零八|08.*宪章|宪章.*08|八宪章|8宪章|零八.*县长|县长.*零八|08县长|淋巴县长|谭作人|高智晟|冯正虎|丁子霖|唯色|焦国标|何清涟|方励之|严家其|柴玲|乌尔凯西|封从德|炳章|苏绍智|陈一谘|韩东方|辛灏年|曹长青|陈破空|盘古乐队|盛雪|伍凡|魏京生|司徒华|黎安友|张宏堡|地下教会|冤民大同盟|达赖|藏独|freetibet|雪山狮子|西藏流亡政府|青天白日旗|民进党|洪哲胜|独立台湾会|台湾政论区|台湾自由联盟|台湾建国运动组织|台湾.*独立联盟|独立联盟.*台湾|新疆.*独立|独立.*新疆|东土耳其斯坦|east.*turkistan|世维会|迪里夏提|明报|纽约时报|美国之音|自由亚洲电台|记者无疆界|维基解密.*中国|中国.*维基解密|世界经济导报|中国数字时代|蟹农场|中国.*禁闻|禁闻.*中国|阅后即焚|阿波罗网|阿波罗新闻|大参考|bignews|多维|看中国|博讯|boxun|peacehall|hrichina|独立中文笔会|华夏文摘|开放杂志|大家论坛|华夏论坛|中国|坛|木子论坛|争鸣论坛|大中华论坛|反腐败论坛|新观察论坛|新华通论坛|正义党论坛|热站政论网|华通时事论坛|华语世界论坛|华岳时事论坛|两岸三地论坛|南大自由论坛|人民之声论坛|万维读者论坛|你说我说论坛|东西南北论坛|东南西北论谈|知情者|红太阳的陨落|和谐拯救危机|血房|一个孤僻的人|河殇|天葬|黄祸|我的奋斗|历史的伤口|改革年代政治斗争|改革年代的政治斗争|关键时刻|超越红墙|梦萦未名湖|一寸山河一寸血|北国之春|北京之春|中国之春|东方红时空|婴儿汤|代开.*发票|发票.*代开|钓鱼岛|女保镖|chinese people eating babies|洗脑|网特|内斗|党魁|文字狱|一党专政|一党独裁|新闻封锁|^freechina|反社会|维权人士|维权律师|异见人士|异议人士|高瞻|地下刊物|tits|boobs|色情|花花公子|中功|法轮|falun|明慧|minghui|退党|三退|九评|nine commentaries|洪吟|神韵艺术|神韵晚会|人民报|renminbao|纪元|^dajiyuan|epochtimes|新唐人|ntdtv|ndtv|新生网|^xinsheng|正见网|zhengjian|追查国际|真善忍|法会|正念|经文|天灭|天怒|迫害|酷刑|邪恶|讲真相|马三家|善恶有报|活摘器官|群体灭绝|防火长城|great.*firewall|firewall.*great|gfw.*什么|什么.*gfw|国家防火墙|翻墙|代理|方滨兴|vpn.*免费|免费.*vpn|vpn.*下载|下载.*vpn|vpn.*世纪|世纪.*vpn|hotspot.*shield|shield.*hotspot|goagent|ultrasurf|动态网|花园网|纳米比亚|freegate|自由门|自由門|无界|無界|动网通|dongtaiwang/ig,\n\t\t\t\t// 强制屏蔽关键词\n\t\t\t\t\"replacement\": /父|母|夫|妻|女儿|儿子|孙子|孙女|女婿|娘|爹|爸|妈|爷|奶|哥|弟|兄|姐|妹|鸡|鸭|狗|猪|gay|mother|mom|father|dad|sister|brother|son|daughter|dog|pig/ig,\n\t\t\t\t// 替换关键词\n\t\t\t\t\"ignore\": /\\~|\\!|\\@|\\#|\\$|\\%|\\^|\\&|\\*|\\(|\\)|_|\\||\\+|\\-|\\=|\\{|\\}|\\[|\\]|\\;|\\'|\\:|\\\"|\\<|\\>|\\?|\\/|\\.|\\,|\\！|\\＃|\\￥|\\…|\\（|\\）|\\—|\\、|\\【|\\】|\\｛|\\｝|\\；|\\：|\\‘|\\’|\\“|\\”|\\《|\\》|\\＼|\\，|\\。|\\、|\\？|\\ |\\　/ig\n\t\t\t\t\t// 忽略词\n\t\t\t},\n\t\t\t\"blockusers\": [ // 封禁用户\n\t\t\t\t\"test\"\n\t\t\t],\n\t\t\t\"maxlength\": 100, // 队列最大长度\n\t\t\t\"textlength\": 1000, // 弹幕最大长度\n\t\t\t\"image\": {\n\t\t\t\t\"regex\": /\\[IMG WIDTH=(\\d+)\\](.+?)\\[\\/IMG\\]/ig, // 图片弹幕\n\t\t\t\t\"lifetime\": 300 // 每个图片给每条弹幕增加的时间\n\t\t\t},\n\t\t\t\"permissions\": { // 普通用户允许的弹幕权限\n\t\t\t\t\"send\": true, // 弹幕开关；关闭后无论普通用户还是高级权限都完全禁止弹幕。\n\t\t\t\t\"style\": false, // 弹幕样式开关\n\t\t\t\t\"color\": false, // 颜色开关\n\t\t\t\t\"textStyle\": false, // CSS开关\n\t\t\t\t\"height\": false, // 高度开关\n\t\t\t\t\"lifeTime\": false, // 显示时间开关\n\t\t\t\t\"sourceCode\": false, // 自定义高级JavaScript弹幕开关\n\t\t\t}\n\t\t},\n\t\t\"unlimited\": {\n\t\t\t\"hostname\": [\"127.0.0.1\", \"localhost\"],\n\t\t\t\"cdn\": false,\n\t\t\t\"display\": \"无限房间\",\n\t\t\t\"table\": \"room_unlimited\", // 数据表\n\t\t\t\"connectpassword\": \"\", // 客户端连接密码\n\t\t\t\"managepassword\": \"\", // 管理密码\n\t\t\t\"advancedpassword\": \"\", // 高级弹幕密码\n\t\t\t\"keyword\": {\n\t\t\t\t\"block\": /^$/,\n\t\t\t\t// 强制屏蔽关键词\n\t\t\t\t\"replacement\": /^$/,\n\t\t\t\t// 替换关键词\n\t\t\t\t\"ignore\": /^$/\n\t\t\t\t\t// 忽略词\n\t\t\t},\n\t\t\t\"blockusers\": [ // 封禁用户\n\t\t\t],\n\t\t\t\"maxlength\": 1000, // 队列最大长度\n\t\t\t\"textlength\": 10000, // 弹幕最大长度\n\t\t\t\"image\": {\n\t\t\t\t\"regex\": /\\[IMG WIDTH=(\\d+)\\](.+?)\\[\\/IMG\\]/ig, // 图片弹幕\n\t\t\t\t\"lifetime\": 300 // 每个图片给每条弹幕增加的时间\n\t\t\t},\n\t\t\t\"permissions\": { // 普通用户允许的弹幕权限\n\t\t\t\t\"send\": true, // 弹幕开关；关闭后无论普通用户还是高级权限都完全禁止弹幕。\n\t\t\t\t\"style\": true, // 弹幕样式开关\n\t\t\t\t\"color\": true, // 颜色开关\n\t\t\t\t\"textStyle\": true, // CSS开关\n\t\t\t\t\"height\": true, // 高度开关\n\t\t\t\t\"lifeTime\": true, // 显示时间开关\n\t\t\t\t\"sourceCode\": true, // 自定义高级JavaScript弹幕开关\n\t\t\t}\n\t\t}\n\t},\n\t\"database\": {\n\t\t\"type\": \"csv\", // 数据库类型（mysql / mongo / csv / none）\n\t\t\"server\": \"127.0.0.1\", // 数据库地址（mysql / mongo）\n\t\t\"username\": \"root\", // 数据库用户名（mysql / mongo）\n\t\t\"password\": \"123456\", // 数据库密码（mysql / mongo）\n\t\t\"port\": \"3306\", // 数据库端口（mysql / mongo）\n\t\t\"db\": \"danmu\", // 数据库（mysql / mongo）\n\t\t\"retry\": 10, // 24小时允许断线重连最大次数，超过则自动退出程序。24小时以第一次断线时间计。（mysql）\n\t\t\"timeout\": 1000, // 数据库重连延时及Ping（mysql）\n\t\t\"savedir\": \"./\", // 指定文件保存位置（csv）\n\t},\n\t\"websocket\": {\n\t\t\"interval\": 10, // 弹幕发送间隔\n\t\t\"singlesize\": 5 // 每次弹幕发送数量\n\t},\n\t\"http\": {\n\t\t\"port\": 3000, // 服务器端口\n\t\t\"headers\": { // HTTP头\n\t\t\t//\"Access-Control-Allow-Origin\": \"*\",\n\t\t\t//\"Access-Control-Allow-Methods\": \"POST\"\n\t\t},\n\t\t\"sessionKey\": \"hey\"\n\t},\n\t\"cache\": {\n\t\t\"type\": \"none\", // 缓存类型，支持memcached和aliyun。后者需要npm install aliyun-sdk\n\t\t\"host\": \"127.0.0.1:11211\", // 缓存服务器地址，可用socket\n\t\t\"auth\": false, // 是否打开身份验证\n\t\t\"authUser\": \"\", // 身份验证账号\n\t\t\"authPassword\": \"\" // 身份验证密码\n\t},\n\t\"ext\": {\n\t\t/*\n\t\t\"weibo\": { // 新浪微博扩展\n\t\t\t\"clientID\": '', // App ID\n\t\t\t\"clientSecret\": '', // App Secret\n\t\t\t\"callbackURL\": 'http://test.zsxsoft.com:3000/auth/sina/callback', // 这里填写的是 网站地址/auth/sina/callback\n\t\t\t\"requireState\": true // 是否打开CSRF防御\n\t\t},*/\n\t\t\"autoban\": { // 自动封号扩展\n\t\t\t\"block\": 3, // 被拦截超过一定数字自动封号\n\t\t}, \n\t\t/*\"audit\": { // 审核扩展\n\t\t\t\n\t\t},*/\n\t\t\"livesync\": {\n\t\t\t\"unlimited\": { // 房间名\n\t\t\t\t\"liveUrl\": \"http://live.bilibili.com/3\" // 直播地址\n\t\t\t}\n\t\t}\n\t\t\n\t}\n};"
  },
  {
    "path": "docker/create_db.sh",
    "content": "#!/bin/bash\n\nif [[ $# -eq 0 ]]; then\n\techo \"Usage: $0 <db_name>\"\n\texit 1\nfi\n\necho \"=> Creating database $1\"\nRET=1\nwhile [[ RET -ne 0 ]]; do\n\tsleep 5\n\tmysql -uroot -e \"CREATE DATABASE $1\"\n\tRET=$?\ndone\n\necho \"=> Done!\"\n"
  },
  {
    "path": "docker/create_mysql_admin_user.sh",
    "content": "#!/bin/bash\n\n\n\necho \"=> Creating database danmu in MySQL\"\n/docker/create_db.sh danmu\n\n\nPASS=${MYSQL_PASS:-$(pwgen -s 12 1)}\n_word=$( [ ${MYSQL_PASS} ] && echo \"preset\" || echo \"random\" )\necho \"=> Creating MySQL admin user with ${_word} password\"\n\nmysql -uroot -e \"CREATE USER 'admin'@'%' IDENTIFIED BY '$PASS'\"\nmysql -uroot -e \"GRANT ALL PRIVILEGES ON *.* TO 'admin'@'%' WITH GRANT OPTION\"\n\n\necho \"=> Done!\"\n\necho \"========================================================================\"\necho \"You can now connect to this MySQL Server using:\"\necho \"\"\necho \"    mysql -uadmin -p$PASS -h<host> -P<port>\"\necho \"\"\necho \"Please remember to change the above password as soon as possible!\"\necho \"MySQL user 'root' has no password but only allows local connections\"\necho \"========================================================================\"\n\n"
  },
  {
    "path": "docker/run.sh",
    "content": "#!/bin/bash\n\n## Part MySQL\nif [ ! -n \"$MYSQL_PORT_3306_TCP_PORT\" ]; then\n    VOLUME_HOME=\"/var/lib/mysql\"\n    MYSQL_INSTALLED=\"no\"\n    if [[ ! -d $VOLUME_HOME/mysql ]]; then\n        echo \"=> An empty or uninitialized MySQL volume is detected in $VOLUME_HOME\"\n        echo \"=> Installing MySQL ...\"\n        mysql_install_db > /dev/null 2>&1\n        echo \"=> Done!\"  \n    else\n        MYSQL_INSTALLED=\"yes\"\n    fi\n    echo \"=> Starting MySQL ...\"\n    /usr/bin/mysqld_safe > /dev/null 2>&1 &\n    MYSQL_STATE=1\n    while [[ RET -ne 0 ]]; do\n        echo \"=> Waiting for confirmation of MySQL service startup\"\n        sleep 5\n        mysql -uroot -e \"status\" > /dev/null 2>&1\n        MYSQL_STATE=$?\n    done\n    if [ \"$MYSQL_INSTALLED\" = no ]; then\n        /docker/create_mysql_admin_user.sh\n    fi\nfi\n## Part Memcached\nUSERNOTEXISTSRET=true\ngetent passwd memcached >/dev/null 2>&1 && USERNOTEXISTSRET=false\nif $USERNOTEXISTSRET; then\n    useradd memcached -s /nologin\nfi\necho \"=> Starting memcached ...\"\nmemcached -u memcached &\n\n## Part Main\necho \"=> Starting service ...\"\nnpm start\n\n"
  },
  {
    "path": "jsconfig.json",
    "content": "{\n\t// See http://go.microsoft.com/fwlink/?LinkId=759670\n\t// for the documentation about the jsconfig.json format\n\t\"compilerOptions\": {\n\t\t\"target\": \"es6\"\n\t},\n\t\"exclude\": [\n\t\t\"node_modules\",\n\t\t\"bower_components\",\n\t\t\"jspm_packages\",\n\t\t\"tmp\",\n\t\t\"temp\", \n        \"lib/http/res\"\n\t]\n}\n"
  },
  {
    "path": "package.json",
    "content": "{\n  \"name\": \"danmu-server\",\n  \"version\": \"1.1.0\",\n  \"license\": \"MIT\",\n  \"scripts\": {\n    \"start\": \"node app.js\"\n  },\n  \"dependencies\": {\n    \"angular\": \"^1.4.6\",\n    \"angular-ui-bootstrap\": \"^0.14.3\",\n    \"async\": \"^2.5.0\",\n    \"bluebird\": \"^3.5.0\",\n    \"body-parser\": \"^1.17.2\",\n    \"bootstrap\": \"^3.3.7\",\n    \"cookie-parser\": \"^1.4.3\",\n    \"ejs\": \"^2.5.7\",\n    \"errorhandler\": \"^1.5.0\",\n    \"express\": \"^4.15.4\",\n    \"express-session\": \"^1.15.5\",\n    \"jquery\": \"^3.2.1\",\n    \"memcached\": \"^2.2.2\",\n    \"morgan\": \"^1.8.2\",\n    \"mysql\": \"^2.14.1\",\n    \"passport\": \"^0.4.0\",\n    \"passport-sina\": \"git+https://github.com/zsxtoys/passport-sina-fork.git\",\n    \"ramda\": \"^0.24.1\",\n    \"socket.io\": \"^2.0.3\"\n  },\n  \"devDependencies\": {\n    \"babel-eslint\": \"^7.2.3\",\n    \"eslint\": \"^4.5.0\",\n    \"eslint-config-standard\": \"^10.2.1\",\n    \"eslint-plugin-babel\": \"^4.1.2\",\n    \"eslint-plugin-import\": \"^2.7.0\",\n    \"eslint-plugin-node\": \"^5.1.1\",\n    \"eslint-plugin-promise\": \"^3.5.0\",\n    \"eslint-plugin-standard\": \"^3.0.1\"\n  },\n  \"description\": \"danmu-server\",\n  \"main\": \"app.js\",\n  \"repository\": {\n    \"type\": \"git\",\n    \"url\": \"git+https://github.com/zsxsoft/danmu-server.git\"\n  },\n  \"keywords\": [\n    \"danmu\",\n    \"danmaku\",\n    \"弹幕\"\n  ],\n  \"author\": \"zsx <zsx@zsxsoft.com>\",\n  \"bugs\": {\n    \"url\": \"https://github.com/zsxsoft/danmu-server/issues\"\n  },\n  \"homepage\": \"https://github.com/zsxsoft/danmu-server#readme\",\n  \"engines\": {\n    \"node\": \">=5.5\"\n  },\n  \"babel\": {\n    \"presets\": [\n      \"es2015\",\n      \"stage-0\"\n    ]\n  }\n}\n"
  },
  {
    "path": "src/controllers/DanmuController.js",
    "content": "const filter = require('../utilities/filter')\nconst log = require('../utilities/log')\nconst danmuEvent = require('../interfaces/Danmu')\nconst configEvent = require('../interfaces/Config')\n\nconst permissions = ['color', 'style', 'height', 'lifeTime', 'textStyle', 'sourceCode'] // 为了不foreach\nlet config = require('../../config')\n\ndanmuEvent.addSingle.listen((data, inputs = {}, extra = {\n  password: '',\n  isAdvanced: false\n}) => {\n  return new Promise((resolve, reject) => {\n    const room = data.room\n    const roomConfig = config.rooms[room]\n    const realFilter = filter(room)\n    if (!roomConfig.permissions.send) {\n      return reject(new Error('弹幕暂时被关闭'))\n    }\n    if (extra.isAdvanced) {\n      if (extra.password !== roomConfig.advancedpassword) {\n        return reject(new Error('高级弹幕密码错误！'))\n      }\n    }\n    if (!extra.isAdvanced && data.text.length > roomConfig.textlength) {\n      return reject(new Error(`弹幕长度大于${roomConfig.textlength}个字，可能影响弹幕观感，请删减。`))\n    }\n    if (realFilter.checkUserIsBlocked(data.hash) || !realFilter.validateText(data.text)) {\n      log.log(`拦截 ${data.hash} - ${data.text}`)\n      danmuEvent.ban.emit(data)\n      return reject(new Error('发送失败！\\n请检查你发送的弹幕有无关键词，或确认自己未被封禁。'))\n    }\n\n    permissions.forEach((val) => {\n      if (extra.isAdvanced || roomConfig.permissions[val]) {\n        data[val] = inputs[val] || ''\n      }\n    })\n    resolve(true)\n    danmuEvent.get.emit(data)\n  })\n})\n\n/**\n * 删除一条弹幕\n */\ndanmuEvent.removeSingle.listen((data, blockUser = false) => {\n  let deleteObject = {}\n  deleteObject[data.room] = {\n    ids: [data.id],\n    hashs: [data.hash]\n  }\n  danmuEvent.removing.emit(deleteObject)\n  if (blockUser) configEvent.blockUser.emit(data.room, data.hash)\n  log.log(`删除弹幕 ${data.id} 成功`)\n})\n"
  },
  {
    "path": "src/controllers/UserController.js",
    "content": "const configEvent = require('../interfaces/Config')\nconst log = require('../utilities/log')\nlet config = require('../../config')\n\nconfigEvent.blockUser.listen((room, username) => {\n  config.rooms[room].blockusers.push(username)\n  configEvent.updated.emit()\n  log.log(`封禁用户${username}成功`)\n})\n\nconfigEvent.unblockUser.listen((room, username) => {\n  return new Promise((resolve, reject) => {\n    let indexOf = config.rooms[room].blockusers.indexOf(username)\n    if (indexOf >= 0) {\n      config.rooms[room].blockusers.splice(indexOf, 1)\n      configEvent.updated.emit()\n      log.log(`已从黑名单移除用户${username}`)\n      resolve()\n    } else {\n      log.log(`黑名单中未搜索到${username}`)\n      reject(new Error(username))\n    }\n  })\n})\n"
  },
  {
    "path": "src/extensions/audit/Audit.js",
    "content": "const Base = require('../../interfaces/Base')\nconst className = 'audit'\nconst _ = require('ramda')\nconst register = _.curry(Base.register)(className)\nconst passed = register('passed')\nclass Audit extends Base {\n  static get className () {\n    return className\n  }\n  static get passed () {\n    return passed\n  }\n}\nmodule.exports = Audit\n"
  },
  {
    "path": "src/extensions/audit/audit.html",
    "content": "<!DOCTYPE html>\n<html lang=\"zh-Hans-cn\" ng-app=\"danmu.audit\" ng-controller=\"MainCtrl\">\n\n<head>\n    <meta charset=\"utf-8\">\n    <meta http-equiv=\"X-UA-Compatible\" content=\"IE=edge\">\n    <meta name=\"viewport\" content=\"width=device-width, initial-scale=1\">\n    <link href=\"http://apps.bdimg.com/libs/bootstrap/3.3.0/css/bootstrap-theme.min.css\" rel=\"stylesheet\">\n    <link href=\"http://apps.bdimg.com/libs/bootstrap/3.3.0/css/bootstrap.min.css\" rel=\"stylesheet\">\n    <link href=\"/manage.css\" rel=\"stylesheet\">\n    <script src=\"http://apps.bdimg.com/libs/angular.js/1.4.6/angular.min.js\"></script>\n    <script src=\"http://cdn.staticfile.org/angular-ui-bootstrap/0.14.3/ui-bootstrap-tpls.min.js\"></script>\n    <title>弹幕审核</title>\n</head>\n\n<body>\n    <nav class=\"navbar navbar-inverse navbar-fixed-top\" role=\"navigation\">\n        <div class=\"container-fluid\">\n            <div class=\"navbar-header\"><a class=\"navbar-brand\" href=\"#\">弹幕审核</a></div>\n        </div>\n    </nav>\n    <div class=\"container-fluid\">\n        <div class=\"row\">\n            <uib-alert type=\"danger\" ng-show=\"haveError\">错误{{err.code}}：{{err.desc}}；建议刷新页面。</uib-alert>\n            <div class=\"col-sm-12 col-md-12 main\">\n                <uib-accordion close-others=\"false\">\n                    <uib-accordion-group heading=\"管理信息\" is-open=\"true\">\n                        <div class=\"container\">\n                            <uib-alert type=\"danger\" ng-show=\"!isLogin\">务必先填入房间信息再进行管理。</uib-alert>\n                            <uib-alert type=\"success\" ng-show=\"isLogin\">你选择了{{room}}房间</uib-alert>\n                            <div class=\"list-group\" ng-show=\"!isLogin\">\n                                <a href=\"#\" class=\"list-group-item\" ng-class=\"{active: room == roomS.id}\" ng-repeat=\"roomS in roomList\" ng-click=\"initRoom(roomS.id)\">{{roomS.display}} ({{roomS.id}})</a>\n                            </div>\n                            <form ng-submit=\"enterRoom(password)\" ng-show=\"!isLogin\">\n                                <div class=\"input-group\">\n                                    <input type=\"password\" class=\"form-control\" placeholder=\"房间密码\" ng-model=\"password\">\n                                    <span class=\"input-group-btn\">\n                                        <button class=\"btn btn-success\" type=\"submit\">确认</button>\n                                    </span>\n                                </div>\n                            </form>\n                        </div>\n                    </uib-accordion-group>\n                    <uib-accordion-group heading=\"弹幕审核\" is-open=\"true\">\n                        <div class=\"container\">\n                            <div ng-repeat=\"danmu in danmus track by danmu.socketId\" style=\"margin-top: 5px; margin-right: 5px; display: inline-block\">\n                                <div class=\"btn-group\" uib-dropdown>\n                                    <button id=\"split-button-{{$index}}\" type=\"button\" class=\"btn btn-success\" ng-click=\"passDanmu($index, danmu.id, '')\">{{danmu.text}}</button>\n                                    <button type=\"button\" class=\"btn btn-danger\" ng-click=\"deleteDanmu($index, danmu.id, '')\">删</button>\n                                    <button type=\"button\" class=\"btn btn-primary\" uib-dropdown-toggle>\n                                <span class=\"caret\"></span>\n                                <span class=\"sr-only\">Split button!</span>\n                              </button>\n                                    <ul uib-dropdown-menu role=\"menu\" aria-labelledby=\"split-button-{{$index}}\">\n                                        <li role=\"menuitem\" class=\"disabled\"><a href=\"#\">{{danmu.hash}}</a></li>\n                                        <li class=\"divider\"></li>\n                                        <li role=\"menuitem\"><a href=\"#\" ng-click=\"deleteDanmu($index, danmu.id, danmu.hash)\">封</a></li>\n                                    </ul>\n                                </div>\n                            </div>\n                        </div>\n                    </uib-accordion-group>\n                </uib-accordion>\n            </div>\n        </div>\n    </div>\n    <script src=\"/socket.io/socket.io.js\"></script>\n\n    <script>\nvar realtime = (function () {\nvar realtime = angular.module(\"danmu.audit\", [\n\"ui.bootstrap\",\n\"auditControllers\"\n]);\nvar auditControllers = angular.module('auditControllers', []);\nvar registerInit = []; // 用于初始化回调\nvar socket = null;\n\n// alternatively, register the interceptor via an anonymous factory\n\nrealtime.config(['$httpProvider',\nfunction ($httpProvider) {\n$httpProvider.interceptors.push(function ($q, $rootScope) {\nreturn {\n'responseError': function (response) {\n$rootScope.err.code = response.status;\n$rootScope.err.desc = response.data.error;\n$rootScope.haveError = true;\nreturn $q.reject(response);\n},\n'response': function (response) {\n$rootScope.err.code = response.status;\n$rootScope.err.desc = \"\";\n$rootScope.haveError = false;\nreturn response;\n}\n};\n});\n}\n]);\n\nauditControllers.controller(\"MainCtrl\",\nfunction ($scope, $http, $rootScope) {\n$scope.accordion = {\nopenInfo: true,\n};\n$scope.isLogin = false;\n$scope.connectToServer = false;\n$scope.room = \"\";\n$scope.password = \"\";\n$scope.danmus = [];\n$scope.config = null;\n$rootScope.haveError = false;\n$rootScope.err = {\ncode: 200,\ndesc: \"\"\n};\n\n$scope.initRoom = function (room) {\n$scope.room = room;\n};\n$scope.enterRoom = function (password) {\n$scope.password = password;\nfor (var object in registerInit) registerInit[object].call();\n$scope.isLogin = true;\n$scope.accordion.openInfo = false;\n};\n$scope.buildParam = function (object) {\nobject.room = $scope.room;\nobject.password = $scope.password;\nreturn object;\n};\n$scope.passDanmu = function ($index, id, blockHash) {\n                socket.emit(\"auditPass\", {\n                    room: $scope.room, \n                    id: id \n                });\n                $scope.danmus.splice($index, 1);\n};\n$scope.deleteDanmu = function ($index, id, blockHash) {\nsocket.emit(\"auditFail\", {\n                    room: $scope.room, \n                    id: id, \n                    hash: blockHash, \n                });\n                $scope.danmus.splice($index, 1);\n};\n\n$http.post(\"/manage/room/get/\", $scope.buildParam({})).success(function (data, status, headers, config) {\n$scope.roomList = data;\n});\nregisterInit.push(function () {\n$http.post(\"/manage/config/password/get/\", $scope.buildParam({})).success(function (data, status, headers, config) {\n$scope.config = data;\nsocket = io(location.origin);\n                    socket.on(\"connect\", function() {\n                        socket.emit(\"auditLogin\", {\n                            password: $scope.config.connectpassword,\n                            room: $scope.room\n                        });\n                    });\nsocket.on(\"auditConnected\", function () {\n                        $scope.danmus = [];\n                        $scope.$apply();\n});\nsocket.on(\"auditDanmu\", function (data) {\n                        for (key in data) {\n                            var value = data[key];\n                            value.id = key;\n                            value.socketId = value.id + \"-\" + socket.id;\n$scope.danmus.push(value);\n                        }\n$scope.$apply();\n});\n});\n});\n}\n);\n\nreturn realtime;\n})();\n    </script>\n</body>\n\n</html>"
  },
  {
    "path": "src/extensions/audit/index.js",
    "content": "// / <reference path=\"../../../typings/main.d.ts\" />\n\n'use strict'\nconst fs = require('fs')\nconst path = require('path')\nconst socketEvent = require('../../interfaces/Socket')\nconst httpEvent = require('../../interfaces/Http')\nconst configEvent = require('../../interfaces/Config')\nconst danmuEvent = require('../../interfaces/Danmu')\nconst auditEvent = require('./Audit')\nconst log = require('../../utilities/log')\nconst config = require('../../../config')\nlet danmuQueue = {}\nlet danmuId = 0\nlet io = null\n\nfunction Audit () {\n  socketEvent.created.listen(socketObject => {\n    io = socketObject\n    io.on('connection', socket => {\n      socket.on('auditLogin', data => {\n        let room = data.room\n        if (!config.rooms[room]) return socket.emit('auditInit', 'Room Not Found')\n        let managePassword = config.rooms[room].managepassword\n        if (managePassword !== data.password) return socket.emit('auditInit', 'Password Error')\n        socket.join(`auditRoom${room}`)\n        socket.emit('auditConnected')\n        log.log(`审核页面${socket.id}已连接${room}`)\n\n        {\n          let danmuObject = {}\n          danmuQueue[room].forEach((value, key) => {\n            danmuObject[key] = value\n          })\n          socket.emit('auditDanmu', danmuObject)\n        }\n      })\n      socket.on('auditPass', data => {\n        log.log(`通过${data.room}(id = ${data.id})`)\n        auditEvent.passed.emit(danmuQueue[data.room].get(parseInt(data.id)))\n        danmuQueue[data.room].delete(data.id)\n      })\n      socket.on('auditFail', data => {\n        log.log(`否决${data.room}(id = ${data.id})`)\n        danmuQueue[data.room].delete(data.id)\n        if (data.hash !== '') {\n          config.rooms[data.room].blockusers.push(data.hash)\n          log.log(`封禁用户${data.hash}成功`)\n          configEvent.updated.emit()\n        }\n      })\n    })\n  })\n  httpEvent.beforeRoute.listen(app => {\n    let danmuKeys = Object.keys(config.rooms)\n\n    app.get('/audit', (req, res, next) => fs.readFile(path.join(__dirname, './audit.html'), (err, data) => {\n      if (err) throw err\n      res.end(data)\n    }))\n\n    // Remove all listeners to gotDanmu and bind to a new listener.\n    const danmuEvents = danmuEvent.get.listeners()\n    danmuEvent.get.removeAllListeners()\n    danmuEvents.forEach(event => auditEvent.passed.listen(event))\n\n    danmuKeys.forEach(room => {\n      danmuQueue[room] = new Map()\n    })\n    danmuEvent.get.listen(data => {\n      let room = data.room\n      danmuQueue[room].set(++danmuId, data)\n      io.to(`auditRoom${room}`).emit('auditDanmu', { [danmuId]: data }) // 懒得再去写队列\n      log.log(`${data.room}得到待审核弹幕（${data.hash}） - ${danmuId}：${data.text}`)\n    })\n  })\n};\nmodule.exports = Audit\n"
  },
  {
    "path": "src/extensions/autoban/index.js",
    "content": "// / <reference path=\"../../../typings/main.d.ts\" />\n\n'use strict'\nconst configEvent = require('../../interfaces/Config')\nconst danmuEvent = require('../../interfaces/Danmu')\nconst filter = require('../../utilities/filter')\nconst log = require('../../utilities/log')\nconst cache = require('../../libraries/cache')\n\nlet config = require('../../../config')\n\nmodule.exports = function () {\n// 弹幕被拦截达到一定次数后封号\n  danmuEvent.ban.listen(danmuData => {\n    process.nextTick(_ => {\n      if (filter(danmuData.room).checkUserIsBlocked(danmuData.hash)) return\n      cache.cache().get('block_' + danmuData.hash, (err, data) => {\n        if (err !== null && typeof err !== 'undefined') {\n          log.log('封禁用户查询失败')\n          console.log(err)\n          data = 0\n        } else if (typeof data === 'undefined') {\n          data = 0\n        } else {\n          data = parseInt(data)\n          data++\n        }\n        log.log('自动封号检测' + danmuData.hash + '次数为' + data)\n\n        if (data >= config.ext.autoban.block) { // 开始封号\n          config.rooms[danmuData.room].blockusers.push(danmuData.hash)\n          log.log('自动封号' + danmuData.hash)\n          configEvent.updated.emit()\n        } else {\n          cache.cache().set('block_' + danmuData.hash, data, 60 * 60 * 3, () => {})\n        }\n      })\n    })\n  })\n}\n"
  },
  {
    "path": "src/extensions/index.js",
    "content": "// / <reference path=\"../../typings/main.d.ts\" />\n'use strict'\nconst path = require('path')\nconst log = require('../utilities/log')\nlet config = require('../../config')\n\nmodule.exports.init = function (callback) {\n  Object.keys(config.ext).map(name => {\n    log.log('加载扩展组件：' + name)\n    require(path.join(__dirname, './', name))()\n  })\n  log.log('扩展组件加载完成')\n  callback(null)\n}\n"
  },
  {
    "path": "src/extensions/livesync/get.py",
    "content": "import time, sys, json\nfrom danmu import DanMuClient\n\ndef out(p):\n    sys.stdout.write(json.dumps(p) + \"\\n\")\n    sys.stdout.flush()\n\ndmc = DanMuClient(sys.argv[1])\nif not dmc.isValid(): print('Url invalid')\n\n@dmc.danmu\ndef danmu_fn(msg):\n    out({'type': 'danmu', 'data': msg})\n\n@dmc.gift\ndef gift_fn(msg):\n    out({'type': 'gift', 'data': msg})\n\n@dmc.other\ndef other_fn(msg):\n    out({'type': 'other', 'data': msg})\n\ndmc.start(blockThread = True)"
  },
  {
    "path": "src/extensions/livesync/index.js",
    "content": "'use strict'\nconst cp = require('child_process')\nconst path = require('path')\nconst danmuEvent = require('../../interfaces/Danmu')\nconst log = require('../../utilities/log')\nlet config = require('../../../config')\n\nconst tryCatch = (fn, e) => {\n  try {\n    return fn()\n  } catch (err) {\n    return e(err)\n  }\n}\n\nmodule.exports = function () {\n  Object.keys(config.ext.livesync).forEach(room => {\n    const liveConfig = config.ext.livesync[room]\n    const ls = cp.spawn('python', [path.join(__dirname, '/get.py'), liveConfig.liveUrl], ['ignore', 'pipe', 'pipe'])\n    ls.stdout.on('data', stdout => {\n      const splitted = stdout.toString().split('\\n')\n      splitted.forEach(data => {\n        if (data.trim() === '') return\n        tryCatch(() => {\n          const ret = JSON.parse(data)\n          let content = ''\n          switch (ret.type) {\n            case 'danmu':\n              content = `${ret.data.Content}`\n              break\n            case 'gift':\n              log.log(ret.data.NickName + ' 送了一份礼物')\n              return\n            case 'other':\n              log.log('弹幕直播信息：' + data)\n              return\n          }\n          danmuEvent.addSingle.wait({\n            hash: ret.data.NickName,\n            room,\n            text: content,\n            ip: '127.0.0.1',\n            ua: 'liveSync',\n            style: '',\n            textStyle: '',\n            lifeTime: '',\n            color: '',\n            height: '',\n            sourceCode: ''\n          })\n        }, (e) => {\n          log.log('解析弹幕失败：' + e.toString())\n        })\n      })\n    })\n  })\n}\n"
  },
  {
    "path": "src/extensions/weibo/index.js",
    "content": "// / <reference path=\"../../../../typings/main.d.ts\" />\n\n'use strict'\nconst Passport = require('passport')\nconst PassportSina = require('passport-sina')\nconst session = require('express-session')\nconst cookieParser = require('cookie-parser')\nconst async = require('async')\nconst fs = require('fs')\nconst path = require('path')\nconst httpEvent = require('../../interfaces/Http')\nconst danmuEvent = require('../../interfaces/Danmu')\nconst log = require('../../utilities/log')\nconst utilities = require('../../utilities')\nconst cache = require('../../libraries/cache')\n\nlet config = require('../../../config')\n\n/*\npassport.serializeUser(function (user, callback) {\n    callback(null, user);\n});\n\npassport.deserializeUser(function (obj, callback) {\n    callback(null, obj);\n});\n*/\nPassport.use(new PassportSina(config.ext.weibo,\n  function (accessToken, refreshToken, profile, callback) {\n    process.nextTick(function () {\n      return callback(null, {\n        accessToken: accessToken,\n        profile: profile\n      })\n    })\n  }))\n\nmodule.exports = function () {\n  httpEvent.beforeRoute.listen(app => {\n    app.use(session({\n      secret: config.http.sessionKey,\n      resave: false,\n      saveUninitialized: true,\n      cookie: {\n        maxAge: 24 * 60 * 60 * 1000 // 1 day\n      }\n    }))\n    app.use(Passport.initialize())\n    app.use(cookieParser())\n    app.get('/auth/sina', Passport.authenticate('sina', {\n      session: false\n    }))\n    app.get('/auth/sina/callback', (req, res, next) => {\n      Passport.authenticate('sina', {\n        session: false\n      }, function (err, data) {\n        if (err !== null) {\n          console.log(err)\n          res.redirect('/')\n          return\n        }\n        if (data === false) {\n          console.log(arguments)\n          res.type('html')\n          res.end(\"<meta charset='utf-8'><script>alert('系统错误，请重新登录，抱歉');location.href='/';</script>\")\n          return\n        }\n        let hash = utilities.getHash(data.profile.id, data.profile.name, data.profile.created_at)\n        cache.cache().set('weibo_' + hash, JSON.stringify({\n          accessToken: data.accessToken,\n          name: data.profile.name,\n          id: data.profile.id,\n          nick: data.profile.screen_name\n        }), 24 * 60 * 60, (err, data) => {\n          err // eslint-disable-line no-unused-expressions\n          // Do nothing\n          // eat it\n        })\n        res.cookie('weibo', hash, {\n          maxAge: 24 * 60 * 60 * 1000\n        })\n        log.log('用户' + data.profile.id + '(' + data.profile.name + ')登录(' + hash + ')')\n        res.redirect('/')\n      })(req, res, next)\n    })\n    app.use(function (req, res, next) {\n      // 这里用来给req添加函数\n      req.getSina = function (callback) {\n        if (typeof req.cookies.weibo !== 'undefined') {\n          cache.cache().get('weibo_' + req.cookies.weibo, function (err, data) {\n            if (err) {\n              callback(err, false)\n            } else if (typeof data === 'undefined') {\n              callback(null, false)\n            } else {\n              callback(null, JSON.parse(data))\n            }\n          })\n        } else {\n          callback(null, false)\n        }\n      }\n      next()\n    })\n\n    // 未登录时直接跳转到新浪微博\n    app.get('/', (req, res, next) => {\n      async.waterfall([\n        function (callback) {\n          req.getSina(callback)\n        },\n        function (data, callback) {\n          if (data === false) {\n            fs.readFile(path.join(__dirname, './login.html'), function (err, data) {\n              err // eslint-disable-line no-unused-expressions\n              res.end(data)\n            })\n          } else {\n            next()\n          }\n        }\n      ])\n    })\n\n    app.post('/post', (req, res, next) => {\n      req.getSina((err, data) => {\n        if (err || data === false) {\n          res.end('你还没有用微博登录！请刷新页面后重试！')\n        } else {\n          next()\n        }\n      })\n    })\n\n    danmuEvent.httpReceived.listen((req, res, danmuData) => {\n      return new Promise((resolve, reject) => {\n        req.getSina((err, data) => {\n          if (data && !err) {\n            danmuData.hash = data.nick\n            return resolve(true)\n          }\n          reject(err)\n        })\n      })\n    })\n\n    danmuEvent.transfer.listen(data => {\n      data.data.forEach(item => `${item.text}=@${item.hash}: ${item.text}`)\n    })\n  })\n}\n"
  },
  {
    "path": "src/extensions/weibo/login.html",
    "content": "<!DOCTYPE html>\n<html lang=\"zh-cn\">\n\n<head>\n    <meta charset=\"utf-8\">\n    <meta http-equiv=\"X-UA-Compatible\" content=\"IE=edge\">\n    <meta name=\"viewport\" content=\"width=device-width, initial-scale=1\">\n    <title>新浪微博登录</title>\n    <link href=\"http://apps.bdimg.com/libs/bootstrap/3.3.0/css/bootstrap-theme.min.css\" rel=\"stylesheet\">\n    <link href=\"http://apps.bdimg.com/libs/bootstrap/3.3.0/css/bootstrap.min.css\" rel=\"stylesheet\">\n    <style type=\"text/css\">\n    body {\n        padding-top: 50px;\n    }\n    \n    .starter-template {\n        padding: 40px 15px;\n        text-align: center;\n    }\n    </style>\n</head>\n\n<body>\n    <nav class=\"navbar navbar-inverse navbar-fixed-top\" role=\"navigation\">\n        <div class=\"container-fluid\">\n            <div class=\"navbar-header\">\n                <a class=\"navbar-brand\" href=\"#\">弹幕</a>\n            </div>\n            <div id=\"navbar\" class=\"navbar-collapse collapse\">\n                <ul class=\"nav navbar-nav navbar-right\"></ul>\n            </div>\n        </div>\n    </nav>\n    <div class=\"container\">\n        <div class=\"starter-template\">\n            <div class=\"row\">\n                <p>\n                    <a href=\"/auth/sina/\"><img src=\"http://www.sinaimg.cn/blog/developer/wiki/240.png\" alt=\"新浪微博登录\" /></a>\n                </p>\n            </div>\n            <div class=\"row\">\n                <p>你需要先使用新浪微博账号登录才可以发送弹幕。</p>\n                <p>程序<span style=\"color:red\">无法</span>获知你的密码，也不会主动发送微博。</p>\n                <p>你可以随时在微博管理中心 -&gt; 我的应用处取消授权。</p>\n            </div>\n            <div class=\"row\">\n                <p>\n                    <br/>\n                </p>\n                <p>Powered by zsx</p>\n                <p>若弹幕无法发送，则请刷新。</p>\n                <p>在弹幕上表白、使用不文明语言有被封号的可能。</p>\n            </div>\n        </div>\n    </div>\n</body>\n\n</html>\n"
  },
  {
    "path": "src/interfaces/Base.js",
    "content": "const events = require('events')\nconst eventEmitter = new events.EventEmitter()\nconst callbackObject = {}\nclass Base {\n  static get className () {\n    return 'base'\n  }\n  static register (className, fieldName) {\n    const eventName = `${className}/${fieldName}`\n    return {\n      eventName,\n      once: eventEmitter.once.bind(eventEmitter, eventName),\n      listen: eventEmitter.on.bind(eventEmitter, eventName),\n      emit: eventEmitter.emit.bind(eventEmitter, eventName),\n      listeners: () => eventEmitter.listeners(eventName),\n      removeAllListeners: () => eventEmitter.removeAllListeners(eventName)\n    }\n  }\n  static registerCallbackable (className, fieldName) {\n    const eventName = `${className}/${fieldName}`\n    if (callbackObject[eventName]) return callbackObject[eventName]\n    let callback = () => new Promise((resolve, reject) => resolve(true))\n    callbackObject[eventName] = {\n      listen: fun => {\n        callback = fun\n      },\n      wait: function (...args) {\n        return callback(...args) // eslint-disable-line standard/no-callback-literal\n      }\n    }\n    return callbackObject[eventName]\n  }\n}\nmodule.exports = Base\n"
  },
  {
    "path": "src/interfaces/Config.js",
    "content": "const Base = require('./Base')\nconst className = 'config'\nconst updated = Base.register(className, 'updated')\nconst blockUser = Base.register(className, 'blockUser')\nconst unblockUser = Base.register(className, 'unblockUser')\nclass Config extends Base {\n  static get className () {\n    return className\n  }\n  static get blockUser () {\n    return blockUser\n  }\n  static get updated () {\n    return updated\n  }\n  static get unblockUser () {\n    return unblockUser\n  }\n}\nmodule.exports = Config\n"
  },
  {
    "path": "src/interfaces/Danmu.js",
    "content": "const Base = require('./Base')\nconst className = 'danmu'\nconst addSingle = Base.registerCallbackable(className, 'addSingle')\nconst ban = Base.register(className, 'ban')\nconst transfer = Base.register(className, 'transfer')\nconst removeSingle = Base.register(className, 'removeSingle')\nconst removing = Base.register(className, 'removing')\nconst get = Base.register(className, 'get')\nconst httpReceived = Base.registerCallbackable(className, 'httpReceived')\nconst search = Base.registerCallbackable(className, 'search')\nclass Danmu extends Base {\n  static get className () {\n    return className\n  }\n  /**\n   * 新增一条未经处理的弹幕\n   */\n  static get addSingle () {\n    return addSingle\n  }\n  /**\n   * 封禁弹幕\n   **/\n  static get ban () {\n    return ban\n  }\n  /**\n   * 收到用户刚刚发送的弹幕（通过HTTP）\n   **/\n  static get httpReceived () {\n    return httpReceived\n  }\n  /**\n   * 得到格式化后的弹幕\n   **/\n  static get get () {\n    return get\n  }\n  /**\n   * 删除单条弹幕事件\n   * @param {object} data\n   * @param {boolean?} blockUser false\n   **/\n  static get removeSingle () {\n    return removeSingle\n  }\n  /**\n   * 正在删除事件（即准备删除但还未动手）\n   **/\n  static get removing () {\n    return removing\n  }\n  /**\n   * 搜索弹幕\n   **/\n  static get search () {\n    return search\n  }\n  /**\n   * 传输弹幕到客户端\n   **/\n  static get transfer () {\n    return transfer\n  }\n}\nmodule.exports = Danmu\n"
  },
  {
    "path": "src/interfaces/Http.js",
    "content": "const Base = require('./Base')\nconst className = 'http'\nconst _ = require('ramda')\nconst register = _.curry(Base.register)(className)\nconst created = register('created')\nconst beforeRoute = register('beforeRoute')\nclass Http extends Base {\n  static get className () {\n    return className\n  }\n  static get created () {\n    return created\n  }\n  static get beforeRoute () {\n    return beforeRoute\n  }\n}\nmodule.exports = Http\n"
  },
  {
    "path": "src/interfaces/Socket.js",
    "content": "const Base = require('./Base')\nconst className = 'socket'\nconst _ = require('ramda')\nconst register = _.curry(Base.register)(className)\nconst created = register('created')\nclass Socket extends Base {\n  static get className () {\n    return className\n  }\n  static get created () {\n    return created\n  }\n}\nmodule.exports = Socket\n"
  },
  {
    "path": "src/libraries/cache/index.js",
    "content": "'use strict'\nlet cache = null\nconst config = require('../../../config')\nconst memoryCacheMap = new Map()\n\nmodule.exports = {\n  init: function (callback) {\n    switch (config.cache.type) {\n      case 'memcached':\n        cache = new (require('memcached'))(config.cache.host)\n        break\n      case 'aliyun':\n        const ALY = require('aliyun-sdk')\n        const PORT = config.cache.host.split(':')[1]\n        const HOST = config.cache.host.split(':')[0]\n        cache = ALY.MEMCACHED.createClient(PORT, HOST, {\n          username: config.cache.authUser,\n          password: config.cache.authPassword\n        })\n        break\n      default:\n        cache = {\n          get: (name, callback) => {\n            callback(null, memoryCacheMap.get(name))\n          },\n          set: (name, value, date, callback) => {\n            memoryCacheMap.set(name, value)\n            callback(null)\n          }\n        }\n    }\n    callback(null)\n  }\n}\nmodule.exports.cache = () => {\n  return cache\n}\n"
  },
  {
    "path": "src/libraries/database/csv.js",
    "content": "const fs = require('fs')\nconst path = require('path')\nconst danmuEvent = require('../../interfaces/Danmu')\nconst log = require('../../utilities/log')\nconst config = require('../../../config')\n\nfunction formatContent (content) {\n  return '\"' + content.toString().replace(/\"/g, '\"\"') + '\"'\n}\nmodule.exports = {}\nmodule.exports.init = function (callback) {\n  let savePath = path.resolve(config.database.savedir)\n  log.log('保存位置：' + savePath)\n  callback(null)\n  danmuEvent.get.listen(data => {\n    let joinArray = []\n    joinArray.push(formatContent(Math.round(new Date().getTime() / 1000)))\n    joinArray.push(formatContent(data.hash))\n    joinArray.push(formatContent(data.ip))\n    joinArray.push(formatContent(data.ua))\n    joinArray.push(formatContent(data.text))\n    joinArray.push('\\r\\n')\n    fs.appendFile(path.resolve(savePath, data.room + '.csv'), joinArray.join(','), err => {\n      if (err) log.log(err)\n    })\n  })\n\n  danmuEvent.search.listen((data) => new Promise((resolve, reject) => {\n    resolve('[{\"user\": \"ERROR\", \"text\": \"Not yet supported\", \"publish\": \"\"}]')\n  }))\n}\n"
  },
  {
    "path": "src/libraries/database/index.js",
    "content": "const config = require('../../../config')\n\nmodule.exports = {\n  init: function (callback) {\n    require('./' + config.database.type + '.js').init(function () {\n      callback.apply(this, arguments)\n    })\n  }\n}\n"
  },
  {
    "path": "src/libraries/database/mongo.js",
    "content": "const mongodb = require('mongodb')\nconst danmuEvent = require('../../interfaces/Danmu')\nconst log = require('../../utilities/log')\nconst config = require('../../../config')\nlet db = null\n\nconst server = new mongodb.Server(config.database.server, config.database.port, {\n  auto_reconnect: true\n})\nconst getConnection = function (callback) {\n  db = new mongodb.Db(config.database.db, server, {\n    w: 1\n  })\n  db.open(err => {\n    if (err !== null) {\n      log.log('数据库连接错误')\n      throw err\n    }\n\n    if (config.database.username !== '') {\n      db.authenticate(config.database.username, config.database.password, function (err, result) {\n        if (err !== null) {\n          log.log('数据库验证错误')\n          throw err\n        }\n        callback.apply(callback, arguments)\n      })\n    }\n\n    callback.apply(callback, arguments)\n    log.log('数据库连接成功')\n  })\n  // callback(null);\n}\n\nmodule.exports = {\n  init: function (callback) {\n    getConnection(callback)\n\n    danmuEvent.get.listen(data => {\n      let room = data.room\n      db.collection(config.rooms[room].table).insert({\n        user: data.hash,\n        text: data.text,\n        publish: Math.round(new Date().getTime() / 1000),\n        ip: data.ip,\n        ua: data.ua\n      }, (err, results) => {\n        if (err !== null) {\n          log.log('数据库写入出错')\n          console.log(err)\n        }\n      })\n    })\n\n    danmuEvent.search.listen((data) => new Promise((resolve, reject) => {\n      let room = data.room\n      db.collection(config.rooms[room].table).find({\n        text: {\n          $regex: '.*?' + pregQuote(data.key) + '.*?'\n        }\n      }, null, null).toArray(function (err, results) {\n        if (err === null) {\n          results.map(function (object) {\n            object.id = object._id\n          })\n          resolve(JSON.stringify(results))\n        } else {\n          log.log('数据库搜索出错')\n          console.error(err)\n          reject(err)\n        }\n      })\n    }))\n  }\n}\n\nfunction pregQuote (str, delimiter) {\n  //  discuss at: http://phpjs.org/functions/preg_quote/\n  // original by: booeyOH\n  // improved by: Ates Goral (http://magnetiq.com)\n  // improved by: Kevin van Zonneveld (http://kevin.vanzonneveld.net)\n  // improved by: Brett Zamir (http://brett-zamir.me)\n  // bugfixed by: Onno Marsman\n  //   example 1: preg_quote(\"$40\");\n  //   returns 1: '\\\\$40'\n  //   example 2: preg_quote(\"*RRRING* Hello?\");\n  //   returns 2: '\\\\*RRRING\\\\* Hello\\\\?'\n  //   example 3: preg_quote(\"\\\\.+*?[^]$(){}=!<>|:\");\n  //   returns 3: '\\\\\\\\\\\\.\\\\+\\\\*\\\\?\\\\[\\\\^\\\\]\\\\$\\\\(\\\\)\\\\{\\\\}\\\\=\\\\!\\\\<\\\\>\\\\|\\\\:'\n\n  return String(str)\n    .replace(new RegExp('[.\\\\\\\\+*?\\\\[\\\\^\\\\]$(){}=!<>|:\\\\' + (delimiter || '') + '-]', 'g'), '\\\\$&')\n}\n"
  },
  {
    "path": "src/libraries/database/mysql.js",
    "content": "const SECONDS_IN_DAY = 24 * 60 * 60 * 1000\nconst mysql = require('mysql')\nconst async = require('async')\nconst danmuEvent = require('../../interfaces/Danmu')\nconst log = require('../../utilities/log')\nconst config = require('../../../config')\n\nlet pool = null\nlet connection = null\nlet errorCounter = 0\nlet firstErrorTime = new Date()\n\nconst createTableSql = [\n  'CREATE TABLE IF NOT EXISTS `%table%` (',\n  'danmu_id int(11) NOT NULL AUTO_INCREMENT,',\n  \"danmu_user varchar(255) NOT NULL DEFAULT '',\",\n  'danmu_text text NOT NULL,',\n  \"danmu_publish int(11) NOT NULL DEFAULT '0',\",\n  \"danmu_ip varchar(255) NOT NULL DEFAULT '',\",\n  'danmu_useragent text NOT NULL,',\n  'PRIMARY KEY (danmu_id),',\n  'KEY danmu_TPISC (danmu_publish)',\n  ') ENGINE=MyISAM DEFAULT CHARSET=utf8 AUTO_INCREMENT=1;'\n].join('\\n')\n\nconst createDatabase = function (callbackOrig) {\n  const asyncList = Object.keys(config.rooms)\n  async.each(asyncList, (room, callback) => {\n    connection.query('SELECT MAX(danmu_id) FROM `' + config.rooms[room].table + '`', function (err, rows) {\n      if (err !== null) {\n        log.log('Creating Table...')\n        connection.query(createTableSql.replace(/%table%/g, config.rooms[room].table), function (err, rows) {\n          callback(err)\n        })\n      } else {\n        callback(null)\n      }\n    })\n  }, function (err) {\n    callbackOrig(err)\n  })\n}\n\nconst dbErrorHandler = function (err) {\n  if (err !== null) {\n    if (err.errno !== 'ECONNRESET') { // 部分MySQL会自动超时，此时要重连但不计errorCounter\n      if (errorCounter === 0 || new Date() - firstErrorTime >= SECONDS_IN_DAY) {\n        firstErrorTime = new Date()\n        errorCounter = 0\n      }\n      errorCounter++\n      console.log(err)\n      log.log('数据库第' + errorCounter + '次连接出错。')\n      if (connection) {\n        connection.release()\n      }\n      getConnection()\n    }\n    if (errorCounter >= config.database.retry) {\n      log.log('数据库连接错误次数超过上限，程序退出。')\n      throw err\n    }\n  }\n}\n\nconst getConnection = function (callback) {\n  let called = false\n  pool.getConnection((err, privateConnection) => {\n    connection = privateConnection\n    if (err) {\n      dbErrorHandler(err)\n      if (!called && callback) {\n        callback(err)\n        called = true\n      }\n    } else {\n      connection.on('error', dbErrorHandler)\n      log.log('数据库连接正常')\n      createDatabase(err => {\n        if (!called && callback) {\n          callback(err)\n          called = true\n        }\n      })\n    }\n  })\n}\n\nmodule.exports = {\n  init: function (callback) {\n    pool = mysql.createPool({\n      host: config.database.server,\n      user: config.database.username,\n      password: config.database.password,\n      port: config.database.port,\n      database: config.database.db,\n      acquireTimeout: config.database.timeout,\n      connectionLimit: 1\n      // debug: true\n    })\n    getConnection(callback)\n\n    let keepAlive = function () {\n      if (!connection) return\n      connection.ping()\n    }\n\n    danmuEvent.get.listen(data => {\n      let room = data.room\n      connection.query('INSERT INTO `%table%` (danmu_user, danmu_text, danmu_publish, danmu_ip, danmu_useragent) VALUES (?, ?, ?, ?, ?)'.replace('%table%', config.rooms[room].table), [\n        data.hash, data.text, Math.round(new Date().getTime() / 1000), data.ip, data.ua\n      ], function (err, rows) {\n        if (err !== null) {\n          log.log('数据库写入出错')\n          console.log(err)\n        }\n      })\n    })\n\n    danmuEvent.search.listen((data) => new Promise((resolve, reject) => {\n      let room = data.room\n      connection.query('SELECT * from `%table%` where `danmu_text` LIKE ? LIMIT 20'.replace('%table%', config.rooms[room].table), [\n        '%' + data.key + '%'\n      ], function (err, rows) {\n        if (err === null) {\n          let ret = []\n          ret = JSON.stringify(rows).replace(/\"danmu_/g, '\"')\n          resolve(ret)\n        } else {\n          log.log('数据库搜索出错')\n          console.log(err)\n          reject(err)\n        }\n      })\n    }))\n\n    getConnection()\n    setInterval(keepAlive, config.database.timeout)\n    //        connection.on(\"error\", dbErrorHandler);\n    //       connectDataBase(callback);\n  }\n}\n"
  },
  {
    "path": "src/libraries/database/none.js",
    "content": "const log = require('../../utilities/log')\n\nmodule.exports = {\n  init: function (callback) {\n    log.log('无数据库')\n    callback(null)\n  }\n}\n"
  },
  {
    "path": "src/libraries/http/index.js",
    "content": "const fs = require('fs')\nconst express = require('express')\nconst errorHandler = require('errorhandler')\nconst path = require('path')\nconst app = express()\nconst bodyParser = require('body-parser')\nconst httpEvent = require('../../interfaces/Http')\nconst log = require('../../utilities/log')\nlet config = require('../../../config')\n\nmodule.exports = {\n  init: function (callback) {\n    app\n      .engine('.html', require('ejs').__express)\n    // .use(logger('dev'))\n      .use(bodyParser.json())\n      .use(bodyParser.urlencoded({\n        extended: true\n      }))\n      .use(errorHandler())\n      .set('view engine', 'html')\n      .set('views', path.join(__dirname, './view/'))\n      .use('/static/bootstrap', express.static(path.join(__dirname, '../../../node_modules/bootstrap/dist/')))\n      .use('/static/jquery', express.static(path.join(__dirname, '../../../node_modules/jquery/dist/')))\n      .use('/static/angular', express.static(path.join(__dirname, '../../../node_modules/angular/')))\n      .use('/static/angular-ui-bootstrap', express.static(path.join(__dirname,  '../../../node_modules/angular-ui-bootstrap/')))\n      .use(express.static(path.join(__dirname, './res/')))\n    httpEvent.beforeRoute.emit(app)\n\n    // 处理路由\n    fs.readdir(path.join(__dirname, './route'), (err, files) => {\n      if (err) {\n        console.error(err)\n        return\n      }\n      files.forEach((filename) => {\n        require(path.join(__dirname, './route', filename))(app)\n      })\n    })\n\n    let server = app.listen(config.http.port, () => {\n      log.log(`服务器于http://127.0.0.1:${config.http.port}/成功创建`)\n      httpEvent.created.emit(server)\n      callback(null)\n    })\n  }\n}\n"
  },
  {
    "path": "src/libraries/http/res/manage.css",
    "content": "/*\n * Base structure\n */\n\n/* Move down content because we have a fixed navbar that is 50px tall */\nbody {\n  padding-top: 50px;\n}\n\n\n/*\n * Global add-ons\n */\n\n.sub-header {\n  padding-bottom: 10px;\n  border-bottom: 1px solid #eee;\n}\n\n/*\n * Top navigation\n * Hide default border to remove 1px line.\n */\n.navbar-fixed-top {\n  border: 0;\n}\n\n/*\n * Sidebar\n */\n\n/* Hide for mobile, show later */\n.sidebar {\n  display: none;\n}\n@media (min-width: 768px) {\n  .sidebar {\n    position: fixed;\n    top: 51px;\n    bottom: 0;\n    left: 0;\n    z-index: 1000;\n    display: block;\n    padding: 20px;\n    overflow-x: hidden;\n    overflow-y: auto; /* Scrollable contents if viewport is shorter than content. */\n    background-color: #f5f5f5;\n    border-right: 1px solid #eee;\n  }\n}\n\n/* Sidebar navigation */\n.nav-sidebar {\n  margin-right: -21px; /* 20px padding + 1px border */\n  margin-bottom: 20px;\n  margin-left: -20px;\n}\n.nav-sidebar > li > a {\n  padding-right: 20px;\n  padding-left: 20px;\n}\n.nav-sidebar > .active > a,\n.nav-sidebar > .active > a:hover,\n.nav-sidebar > .active > a:focus {\n  color: #fff;\n  background-color: #428bca;\n}\n\n\n/*\n * Main content\n */\n\n.main {\n  padding: 20px;\n}\n@media (min-width: 768px) {\n  .main {\n    padding-right: 40px;\n    padding-left: 40px;\n  }\n}\n.main .page-header {\n  margin-top: 0;\n}\n\n\n/*\n * Placeholder dashboard ideas\n */\n\n.placeholders {\n  margin-bottom: 30px;\n  text-align: center;\n}\n.placeholders h4 {\n  margin-bottom: 0;\n}\n.placeholder {\n  margin-bottom: 20px;\n}\n.placeholder img {\n  display: inline-block;\n  border-radius: 50%;\n}"
  },
  {
    "path": "src/libraries/http/res/manage.js",
    "content": "var manage = (function () { // eslint-disable-line\n  var manage = angular.module('danmu.manage', [ // eslint-disable-line\n    'ui.bootstrap',\n    'manageControllers'\n  ])\n  var manageControllers = angular.module('manageControllers', []) // eslint-disable-line\n  var registerInit = [] // 用于初始化回调\n\n  // alternatively, register the interceptor via an anonymous factory\n\n  manage.config(['$httpProvider',\n    function ($httpProvider) {\n      $httpProvider.interceptors.push(function ($q, $rootScope) {\n        return {\n          'responseError': function (response) {\n            $rootScope.err.code = response.status\n            $rootScope.err.desc = response.data.error\n            $rootScope.haveError = true\n            return $q.reject(response)\n          },\n          'response': function (response) {\n            $rootScope.err.code = response.status\n            $rootScope.err.desc = ''\n            $rootScope.haveError = false\n            return response\n          }\n        }\n      })\n    }\n  ])\n\n  manageControllers.controller('MainCtrl',\n    function ($scope, $http, $rootScope) {\n      $scope.accordion = {\n        closeOther: false,\n        openInfo: true,\n        openDanmu: false,\n        openBlock: true,\n        openConfig: false,\n        openPermissions: true,\n        openPassword: false,\n        disableInfo: false\n      }\n      $scope.isLogin = false\n      $scope.room = ''\n      $scope.password = ''\n      $rootScope.haveError = false\n      $rootScope.err = {\n        code: 200,\n        desc: ''\n      }\n\n      $scope.initRoom = function (room) {\n        $scope.room = room\n      }\n      $scope.enterRoom = function (password) {\n        $scope.password = password\n        for (var object in registerInit) registerInit[object].call()\n        $scope.isLogin = true\n        $scope.accordion.openInfo = false\n      }\n      $scope.buildParam = function (object) {\n        object.room = $scope.room\n        object.password = $scope.password\n        return object\n      }\n      $http.post('/manage/room/get/', $scope.buildParam({})).success(function (data, status, headers, config) {\n        $scope.roomList = data\n      })\n    }\n  )\n  manageControllers.controller('DanmuCtrl',\n    function ($scope, $http) {\n      $scope.danmu = {}\n      $scope.danmu.searchKey = ''\n      $scope.danmu.doSearch = function () {\n        $http.post('/manage/search', $scope.buildParam({\n          key: $scope.danmu.searchKey\n        })).success(function (data, status, headers, config) {\n          $scope.danmu.result = data\n        })\n      }\n    }\n  )\n  manageControllers.controller('BlockCtrl',\n    function ($scope, $http) {\n      $scope.block = {}\n      $scope.block.textUser = ''\n      $scope.block.doAdd = function () {\n        $http.post('/manage/block/add', $scope.buildParam({\n          user: $scope.block.textUser\n        })).success(function (data, status, headers, config) {\n          $scope.block.result.push($scope.block.textUser)\n          $scope.block.textUser = ''\n        })\n      }\n      $scope.block.checkKeyDown = function (e) {\n        if (e.keyCode === 13) this.doAdd()\n      }\n      $scope.block.doRemove = function (user) {\n        $http.post('/manage/block/remove', $scope.buildParam({\n          user: user\n        })).success(function (data, status, headers, config) {\n          $scope.block.result.splice($scope.block.result.indexOf(user), 1)\n        })\n      }\n      registerInit.push(function () {\n        $http.post('/manage/block/get/', $scope.buildParam({})).success(function (data, status, headers, config) {\n          $scope.block.result = data\n        })\n      })\n    }\n  )\n  manageControllers.controller('ConfigCtrl',\n    function ($scope, $http) {\n      $scope.config = {}\n      $scope.config.realConfig = {}\n      $scope.config.realConfig.replaceKeyword = ''\n      $scope.config.realConfig.blockKeyword = ''\n      $scope.config.realConfig.ignoreKeyword = ''\n      $scope.config.realConfig.socketinterval = 0\n      $scope.config.realConfig.socketsingle = 0\n      $scope.config.realConfig.maxlength = 0\n      $scope.config.realConfig.textlength = 0\n\n      $scope.config.submitConfig = function () {\n        try {\n          $http.post('/manage/config/set/', $scope.buildParam($scope.config.realConfig)).success(function (data, status, headers, config) {\n            $scope.config.realConfig = data\n          })\n        } catch (e) {\n          window.alert('正则检测出错！\\n\\n' + e.toString())\n        }\n      }\n      registerInit.push(function () {\n        $http.post('/manage/config/get/', $scope.buildParam({})).success(function (data, status, headers, config) {\n          $scope.config.realConfig = data\n        })\n      })\n    }\n  )\n  manageControllers.controller('PermissionCtrl',\n    function ($scope, $http) {\n      $scope.config = {}\n      $scope.$makeClass = function (configName) {\n        return {\n          'active': $scope.config[configName],\n          'btn-danger': !$scope.config[configName],\n          'btn-success': $scope.config[configName]\n        }\n      }\n      $scope.$getStateText = function (configName) {\n        return $scope.config[configName] ? '开' : '关'\n      }\n      $scope.$setState = function (configName) {\n        $scope.config[configName] = !$scope.config[configName]\n      }\n\n      $scope.$submitPermissions = function () {\n        $http.post('/manage/config/permissions/set/', $scope.buildParam($scope.config)).success(function (data, status, headers, config) {\n          $scope.config = data\n        })\n      }\n\n      registerInit.push(function () {\n        $http.post('/manage/config/permissions/get/', $scope.buildParam({})).success(function (data, status, headers, config) {\n          $scope.config = data\n        })\n      })\n    }\n  )\n  manageControllers.controller('PasswordCtrl',\n    function ($scope, $http) {\n      $scope.config = {\n        advancedpassword: '',\n        managepassword: '',\n        connectpassword: ''\n      }\n      $scope.$submitPassword = function (passwordState) {\n        var modal = passwordState + 'password'\n        if (!$scope.config[modal] || $scope.config[modal] === '') return\n        if (window.confirm('确定要更新密码（类型：' + modal + '）？\\n\\n更新连接密码后，已经连接的客户端不受影响，新客户端将使用新密码；\\n更新管理密码后，必须刷新页面才可以继续使用。')) {\n          $http.post('/manage/config/password/set/', $scope.buildParam({\n            type: modal,\n            newPassword: $scope.config[modal]\n          })).success(function (data, status, headers, config) {\n            if (modal === 'managepassword') {\n              window.location.reload()\n            }\n            $scope.config[modal] = ''\n          })\n        }\n      }\n    }\n  )\n  return manage\n})()\n"
  },
  {
    "path": "src/libraries/http/res/realtime.js",
    "content": "// / <reference path=\"../../../typings/main.d.ts\" />\nvar realtime = (function () { // eslint-disable-line\n  var realtime = angular.module('danmu.realtime', [ // eslint-disable-line\n    'ui.bootstrap',\n    'realtimeControllers'\n  ])\n  var realtimeControllers = angular.module('realtimeControllers', []) // eslint-disable-line\n  var registerInit = [] // 用于初始化回调\n  var socket = null\n\n  // alternatively, register the interceptor via an anonymous factory\n\n  realtime.config(['$httpProvider',\n    function ($httpProvider) {\n      $httpProvider.interceptors.push(function ($q, $rootScope) {\n        return {\n          'responseError': function (response) {\n            $rootScope.err.code = response.status\n            $rootScope.err.desc = response.data.error\n            $rootScope.haveError = true\n            return $q.reject(response)\n          },\n          'response': function (response) {\n            $rootScope.err.code = response.status\n            $rootScope.err.desc = ''\n            $rootScope.haveError = false\n            return response\n          }\n        }\n      })\n    }\n  ])\n\n  realtimeControllers.controller('MainCtrl',\n    function ($scope, $http, $rootScope) {\n      $scope.accordion = {\n        openInfo: true\n      }\n      $scope.isLogin = false\n      $scope.connectToServer = false\n      $scope.room = ''\n      $scope.password = ''\n      $scope.danmus = []\n      $scope.config = null\n      $rootScope.haveError = false\n      $rootScope.err = {\n        code: 200,\n        desc: ''\n      }\n\n      $scope.initRoom = function (room) {\n        $scope.room = room\n      }\n      $scope.enterRoom = function (password) {\n        $scope.password = password\n        for (var object in registerInit) registerInit[object].call()\n        $scope.isLogin = true\n        $scope.accordion.openInfo = false\n      }\n      $scope.buildParam = function (object) {\n        object.room = $scope.room\n        object.password = $scope.password\n        return object\n      }\n      $scope.deleteDanmu = function ($index, id, blockHash) {\n        $http.post('/manage/danmu/delete/', $scope.buildParam({\n          id: id,\n          hash: blockHash\n        })).success(function (data, status, headers, config) {\n          if ($scope.danmus[$index]) {\n            if ($scope.danmus[$index].id === id) {\n              $scope.danmus[$index].lifeTime = 0\n            }\n          }\n        })\n      }\n\n      $http.post('/manage/room/get/', $scope.buildParam({})).success(function (data, status, headers, config) {\n        $scope.roomList = data\n      })\n      registerInit.push(function () {\n        $http.post('/manage/config/password/get/', $scope.buildParam({})).success(function (data, status, headers, config) {\n          $scope.config = data\n          socket = window.io(window.location.origin)\n          socket.emit('password', {\n            password: $scope.config.connectpassword,\n            room: $scope.room,\n            info: {\n              version: window.serverVersion\n            }\n          })\n          socket.on('connected', function () {\n            $scope.connectToServer = true\n          })\n          socket.on('danmu', function (data) {\n            data.data.forEach(function (value) {\n              value.socketId = value.id + '-' + socket.id\n              value.lifeTime = parseInt(value.lifeTime)\n              $scope.danmus.push(value)\n            })\n            $scope.$apply()\n          })\n          setInterval(function () {\n            var isRemoved = false\n            $scope.danmus.forEach(function (value, key) {\n              value.lifeTime -= 60\n              if (value.lifeTime <= 0) {\n                $scope.danmus.splice(key, 1)\n                isRemoved = true\n              }\n            })\n            if (isRemoved) {\n              $scope.$apply()\n            }\n          }, 1000) // auto remove 60fps\n        })\n      })\n    }\n  )\n\n  return realtime\n})()\n"
  },
  {
    "path": "src/libraries/http/route/index.js",
    "content": "const config = require('../../../../config')\n\nmodule.exports = function (app) {\n// Initialize Hostname Map\n  const hostnameMap = new Map()\n  Object.keys(config.rooms).forEach(room => config.rooms[room].hostname.forEach(value => hostnameMap.set(value, room)))\n\n  function getRoom (hostname) {\n    return (hostnameMap.has(hostname)) ? hostnameMap.get(hostname) : null\n  }\n\n  function renderIndex (advanced, room) {\n    const permission = config.rooms[room].permissions\n    return {\n      config,\n      advanced,\n      room,\n      permission\n    }\n  }\n\n  app.route('/*').all((req, res, next) => {\n    res.append('Server', 'zsx\\'s Danmu Server')\n    req.room = getRoom(req.hostname)\n    for (let item in config.http.headers) {\n      res.append(item, config.http.headers[item])\n    }\n    if (req.room === null) {\n      res.status(403)\n      res.end('403 Forbidden')\n    }\n    next()\n  })\n\n  app.get('/', (req, res) => {\n    res.render('index', renderIndex(false, req.room))\n  })\n\n  app.get('/advanced', (req, res) => {\n    res.render('index', renderIndex(true, req.room))\n  })\n\n  app.post((req, res, next) => {\n    res.header('Content-Type', 'text/html; charset=utf-8')\n    next()\n  })\n}\n"
  },
  {
    "path": "src/libraries/http/route/manage.js",
    "content": "const config = require('../../../../config')\n\nmodule.exports = function (app) {\n  app.get('/manage', function (req, res) {\n    res.render('manage', {\n      config\n    })\n  })\n\n  // 总身份验证\n  app.post('/manage/*', (req, res, next) => {\n    if (/room\\/get/.test(req.url)) {\n      // 如果是房间下发则不验证身份\n      return next()\n    }\n    let room = req.body.room\n    if (!config.rooms[room]) {\n      res.status(404)\n      return res.end('{\"error\": \"房间错误\"}')\n    }\n    if (config.rooms[room].managepassword !== req.body.password) {\n      res.status(403)\n      return res.end('{\"error\": \"密码错误\"}')\n    }\n    return next()\n  })\n}\n"
  },
  {
    "path": "src/libraries/http/route/manageBlock.js",
    "content": "const configEvent = require('../../../interfaces/Config')\nconst log = require('../../../utilities/log')\nconst config = require('../../../../config')\n\nmodule.exports = function (app) {\n  app.post('/manage/block/add/', (req, res) => {\n    let room = req.body.room\n    configEvent.blockUser.emit(room, req.body.user)\n    res.end('{\"error\": \"封禁用户成功\"}')\n  })\n\n  app.post('/manage/block/get/', (req, res) => {\n    let room = req.body.room\n    log.log('请求被封禁用户成功')\n    res.end(JSON.stringify(config.rooms[room].blockusers))\n  })\n\n  app.post('/manage/block/remove/', (req, res) => {\n    let room = req.body.room\n    configEvent.unblockUser.emit(room, req.body.user)\n    res.end('{\"error\": \"移除封禁成功\"}')\n  })\n}\n"
  },
  {
    "path": "src/libraries/http/route/manageConfig.js",
    "content": "const configEvent = require('../../../interfaces/Config')\nconst utilities = require('../../../utilities')\nconst log = require('../../../utilities/log')\nconst config = require('../../../../config')\n\nmodule.exports = function (app) {\n  app.post('/manage/config/set/', (req, res) => {\n    const room = req.body.room || ''\n    config.rooms[room].keyword.replacement = new RegExp(req.body.replaceKeyword, 'ig')\n    config.rooms[room].keyword.block = new RegExp(req.body.blockKeyword, 'ig')\n    config.rooms[room].keyword.ignore = new RegExp(req.body.ignoreKeyword, 'ig')\n    config.rooms[room].maxlength = req.body.maxlength\n    config.rooms[room].textlength = req.body.textlength\n    config.rooms[room].openstate = req.body.openstate\n    config.websocket.interval = req.body.socketinterval\n    config.websocket.singlesize = req.body.socketsingle\n\n    const newConfig = JSON.stringify(utilities.buildConfigToArray(room))\n    log.log('收到配置信息：' + newConfig)\n    configEvent.updated.emit()\n    return res.end(newConfig)\n  })\n\n  app.post('/manage/config/permissions/set/', (req, res) => {\n    const room = req.body.room || ''\n    Object.keys(config.rooms[room].permissions).map(item => {\n      config.rooms[room].permissions[item] = !!req.body[item]\n    })\n    let newConfig = JSON.stringify(config.rooms[room].permissions)\n    log.log('收到权限配置信息：' + newConfig)\n    configEvent.updated.emit()\n    return res.end(newConfig)\n  })\n\n  app.post('/manage/config/password/set/', (req, res) => {\n    const type = req.body.type || ''\n    const password = req.body.newPassword || ''\n    const room = req.body.room || ''\n\n    if (config.rooms[room][type]) {\n      config.rooms[room][type] = password\n      log.log('房间（' + room + '）的' + type + '已更新为' + password)\n    }\n    configEvent.updated.emit()\n    return res.end()\n  })\n\n  app.post('/manage/config/permissions/get/', (req, res) => {\n    const room = req.body.room || ''\n    log.log('已将权限配置向管理页面下发')\n    return res.end(JSON.stringify(config.rooms[room].permissions))\n  })\n\n  app.post('/manage/config/get/', (req, res) => {\n    const room = req.body.room || ''\n    log.log('已将配置向管理页面下发')\n    return res.end(JSON.stringify(utilities.buildConfigToArray(room)))\n  })\n\n  app.post('/manage/config/password/get/', (req, res) => {\n    const room = req.body.room || ''\n    log.log('已将密码向管理页面下发')\n    return res.end(JSON.stringify({\n      connectpassword: config.rooms[room].connectpassword\n    }))\n  })\n}\n"
  },
  {
    "path": "src/libraries/http/route/manageDanmu.js",
    "content": "const danmuEvent = require('../../../interfaces/Danmu')\n\nmodule.exports = function (app) {\n  app.post('/manage/danmu/delete/', (req, res) => {\n    const data = {}\n    data.hash = req.body.hash || ''\n    data.id = req.body.id || 0\n    data.room = req.body.room || ''\n\n    if (data.id === 0) {\n      res.end({})\n      return\n    }\n\n    danmuEvent.removeSingle.emit(data, data.hash !== '')\n    return res.end('{\"error\": \"删除弹幕成功\"}')\n  })\n}\n"
  },
  {
    "path": "src/libraries/http/route/manageRoom.js",
    "content": "const log = require('../../../utilities/log')\nconst config = require('../../../../config')\n\nmodule.exports = function (app) {\n  app.post('/manage/room/get/', (req, res) => {\n    const ret = []\n    Object.keys(config.rooms).map(room => {\n      ret.push({\n        id: room,\n        display: config.rooms[room].display\n      })\n    })\n    log.log('已把房间信息向管理页面下发')\n    return res.end(JSON.stringify(ret))\n  })\n}\n"
  },
  {
    "path": "src/libraries/http/route/manageSearch.js",
    "content": "const log = require('../../../utilities/log')\nconst danmuEvent = require('../../../interfaces/Danmu')\n\nmodule.exports = function (app) {\n  app.post('/manage/search', function (req, res) {\n    const room = req.body.room\n\n    log.log('尝试搜索' + req.body.key)\n    danmuEvent.search.wait({\n      key: req.body.key,\n      room\n    }).then(data => {\n      log.log('搜索' + req.body.key + '成功')\n      res.end(data)\n    }).catch(data => {\n      res.end(`[{\"user\": \"ERROR\", \"text\": \"${JSON.parse(data.toString())}\", \"publish\": \"\"}]`)\n    })\n  })\n}\n"
  },
  {
    "path": "src/libraries/http/route/post.js",
    "content": "const utilities = require('../../../utilities')\nconst danmuEvent = require('../../../interfaces/Danmu')\nconst config = require('../../../../config')\n\nmodule.exports = function (app) {\n  app.post('/post', (req, res) => {\n    const room = req.room\n    const roomConfig = config.rooms[room]\n    const ip = roomConfig.cdn ? req.ip : (req.get('X-Real-IP') || req.get('X-Forwarded-For') || req.ip)\n    const hash = utilities.getHash(ip, req.headers['user-agent'], req.body.hash)\n\n    const danmuData = {\n      hash,\n      room,\n      text: req.body.text,\n      ip,\n      ua: req.headers['user-agent'],\n      style: '',\n      textStyle: '',\n      lifeTime: '',\n      color: '',\n      height: '',\n      sourceCode: ''\n    }\n\n    if (req.body.text === '') {\n      return res.end('弹幕不能为空')\n    }\n\n    danmuEvent.httpReceived.wait(req, res, danmuData)\n      .then(() => danmuEvent.addSingle.wait(danmuData, req.body, {\n        password: req.body.password,\n        isAdvanced: req.body.type === 'advanced'\n      }))\n      .then(() => res.end('发送成功！'))\n      .catch(e => res.end(e.toString()))\n  })\n}\n"
  },
  {
    "path": "src/libraries/http/route/realtime.js",
    "content": "const config = require('../../../../config')\n\nmodule.exports = function (app) {\n  app.get('/realtime', (req, res) => {\n    res.render('realtime', {\n      config,\n      version: global.version\n    })\n  })\n}\n"
  },
  {
    "path": "src/libraries/http/view/index.html",
    "content": "<!DOCTYPE html>\n<!--懒得写CSS和一堆JS了，麻烦~-->\n<html lang=\"zh-cmn-CN\">\n\n<head>\n    <meta charset=\"utf-8\">\n    <meta http-equiv=\"X-UA-Compatible\" content=\"IE=edge\">\n    <meta name=\"viewport\" content=\"width=device-width, initial-scale=1\">\n    <title>发送弹幕</title>\n    <link href=\"/static/bootstrap/css/bootstrap-theme.min.css\" rel=\"stylesheet\">\n    <link href=\"/static/bootstrap/css/bootstrap.min.css\" rel=\"stylesheet\">\n    <script src=\"/static/jquery/jquery.min.js\"></script>\n    <script src=\"/static/bootstrap/js/bootstrap.min.js\"></script>\n    <style type=\"text/css\">\n    body {\n        padding-top: 50px;\n    }\n\n    .starter-template {\n        padding: 40px 15px;\n        text-align: center;\n    }\n\n    .danmu-advanced {\n        display: <%=advanced ? \"block\": \"none\"%>;\n    }\n\n    </style>\n</head>\n\n<body>\n    <nav class=\"navbar navbar-inverse navbar-fixed-top\" role=\"navigation\">\n        <div class=\"container-fluid\">\n            <div class=\"navbar-header\">\n                <a class=\"navbar-brand\" href=\"#\">弹幕</a>\n            </div>\n            <div id=\"navbar\" class=\"navbar-collapse collapse\">\n                <ul class=\"nav navbar-nav navbar-right\"></ul>\n            </div>\n        </div>\n    </nav>\n    <div class=\"container\">\n        <div class=\"starter-template\">\n            <form id=\"form-ret\">\n                <div class=\"row\">\n                    <p class=\"text-success\">发送你的弹幕吧！</p>\n                </div>\n                <div class=\"row\">\n                    <textarea class=\"form-control\" name=\"text\" row=\"4\" id=\"txt-barrage\"></textarea>\n                </div>\n                <p>&nbsp;</p>\n                <div class=\"row danmu-advanced danmu-style\">\n                    <div class=\"form-group\">\n                        <label class=\"col-sm-4 control-label\">特效</label>\n                        <div class=\"col-sm-8\">\n                            <label class=\"radio-inline\">\n                                <input type=\"radio\" name=\"style\" value=\"scroll\" checked>滚动</label>\n                            <label class=\"radio-inline\">\n                                <input type=\"radio\" name=\"style\" value=\"reversescroll\">反向滚动</label>\n                            <label class=\"radio-inline\">\n                                <input type=\"radio\" name=\"style\" value=\"staticup\">固定最上</label>\n                            <label class=\"radio-inline\">\n                                <input type=\"radio\" name=\"style\" value=\"staticdown\">固定最下</label>\n                            <label class=\"radio-inline danmu-sourceCode\">\n                                <input type=\"radio\" name=\"style\" value=\"custom\">自定义</label>\n                        </div>\n                    </div>\n                    <p>&nbsp;</p>\n                </div>\n                <div class=\"row danmu-advanced danmu-color\">\n                    <div class=\"form-group\">\n                        <label class=\"col-sm-4 control-label\">颜色(16进制)</label>\n                        <div class=\"col-sm-8\">\n                            <input type=\"color\" name=\"color\" class=\"form-control\" name=\"txt-color\" value=\"#ffffff\" />\n                        </div>\n                    </div>\n                    <p>&nbsp;</p>\n                </div>\n                <div class=\"row danmu-advanced danmu-textStyle\">\n                    <div class=\"form-group\">\n                        <label class=\"col-sm-4 control-label\">文字CSS</label>\n                        <div class=\"col-sm-8\">\n                            <input type=\"text\" name=\"textStyle\" class=\"form-control\" name=\"txt-style\" value=\"normal bold 3em 微软雅黑\" />\n                        </div>\n                    </div>\n                    <p>&nbsp;</p>\n                </div>\n                <div class=\"row danmu-advanced danmu-height\">\n                    <div class=\"form-group\">\n                        <label class=\"col-sm-4 control-label\">弹幕高度</label>\n                        <div class=\"col-sm-8\">\n                            <input type=\"text\" name=\"height\" class=\"form-control\" name=\"txt-height\" value=\"30\" />\n                        </div>\n                    </div>\n                    <p>&nbsp;</p>\n                </div>\n                <div class=\"row danmu-advanced danmu-lifeTime\">\n                    <div class=\"form-group\">\n                        <label class=\"col-sm-4 control-label\">显示时间</label>\n                        <div class=\"col-sm-8\">\n                            <input type=\"text\" name=\"lifeTime\" class=\"form-control\" name=\"txt-time\" value=\"240\" />\n                        </div>\n                    </div>\n                    <p>&nbsp;</p>\n                </div>\n                <div class=\"row danmu-advanced\">\n                    <div class=\"form-group\">\n                        <label class=\"col-sm-4 control-label\">高级弹幕密码</label>\n                        <div class=\"col-sm-8\">\n                            <input type=\"password\" name=\"password\" class=\"form-control\" name=\"txt-password\" value=\"\" />\n                        </div>\n                    </div>\n                    <p>&nbsp;</p>\n                </div>\n                <div class=\"row\">\n                    <button class=\"form-control btn-success\" id=\"btn-send\">喷射</button>\n                </div>\n                <div class=\"row\">\n                    <div class=\"checkbox\">\n                        <label>\n                            <input type=\"checkbox\" id=\"saveContent\">保留发送内容</label>\n                    </div>\n                </div>\n                <input type=\"hidden\" name=\"type\" value=\"<%=advanced?'advanced':''%>\" />\n                <div class=\"danmu-custom danmu-sourceCode hide\">\n                <div class=\"row\">\n                    <p><label>自定义弹幕</label></p>\n                </div>\n                <div class=\"row\">\n                    <textarea class=\"form-control\" name=\"sourceCode\" row=\"4\" id=\"txt-custom\" placeholder=\"Enter JavaScript Here..\"></textarea>\n                </div>\n                </div>\n                <div class=\"row\">\n                    <p>\n                        <br/>\n                    </p>\n                    <p>Powered by zsx</p>\n                    <p>若弹幕无法发送，则请刷新。</p>\n                    <p>在弹幕上表白、使用不文明语言有被封号的可能。</p>\n                </div>\n            </form>\n        </div>\n    </div>\n    <script>\n    $(function() {\n        var roomsConfig = <%-JSON.stringify(permission)%>;\n        var room = \"<%=room%>\";\n        var danmuHash = null;\n\n        calculateHash();\n        initializeRoomSelector();\n        bindSendButton();\n\n        function calculateHash() {\n            function b2h(s) {\n                var i, l, n, o = '';\n                s += '';\n                for (i = 0, l = s.length; i < l; i++) {\n                    n = s.charCodeAt(i).toString(16);\n                    o += n.length < 2 ? '0' + n : n;\n                }\n                return o;\n            }\n\n            // In some modes, localStorage cannot be used.\n            try {\n                danmuHash = localStorage.getItem(\"danmuHash\");\n            } catch (e) {\n                danmuHash = \"CANNOT_GET_HASH\";\n                // eat it\n            }\n\n            if (danmuHash === null) {\n                var canvas = document.createElement('canvas');\n                var ctx = canvas.getContext('2d');\n                ctx.textBaseline = \"top\";\n                ctx.font = \"14px '微软雅黑'\";\n                ctx.fillStyle = \"#0281cd\";\n                ctx.fillText(\"zsx's Danmu Server\\n\\n弹幕验证帆布指纹\", 5, 5);\n                ctx.fillText(\"http://www.zsxsoft.com/\", 5, 30);\n                var b64 = b2h(atob(canvas.toDataURL().replace(\"data:image/png;base64,\", \"\")).slice(-32, -24));\n                try {\n                    localStorage.danmuHash = b64;\n                } catch (e) {\n                    // Still do nothing\n                }\n            }\n        }\n\n        function initializeRoomSelector() {\n\n            function initializePermissions(room) {\n                for (var item in roomsConfig) {\n                    if (roomsConfig[item]) {\n                        $(\".danmu-\" + item).show();\n                    }\n                }\n            }\n            initializePermissions(room);\n\n            $(\"input[name='style']\").change(function(e) {\n                if ($(this).val() == \"custom\") {\n                    $(\".danmu-custom\").removeClass(\"hide\");\n                } else {\n                    $(\".danmu-custom\").addClass(\"hide\");\n                }\n            });\n\n        }\n\n        function bindSendButton() {\n\n            $(\"#btn-send\").click(function() {\n                var ret = {};\n                var object = $(\"#form-ret\").serializeArray();\n                for (var i = 0; i < object.length; i++) {\n                    ret[object[i].name] = object[i].value;\n                }\n                ret.hash = danmuHash;\n                $.post(\"/post\", ret).done(function(data) {\n                    alert(data);\n                    if (!$(\"#saveContent\")[0].checked) $(\"#txt-barrage\").val(\"\").focus();\n                    $(\"#btn-send\").removeAttr('disabled');\n                });\n                $(\"#btn-send\").attr(\"disabled\", \"disabled\");\n            });\n        }\n    });\n    </script>\n</body>\n\n</html>\n"
  },
  {
    "path": "src/libraries/http/view/manage.html",
    "content": "<!DOCTYPE html>\n<html lang=\"zh-cmn-CN\" ng-app=\"danmu.manage\" ng-controller=\"MainCtrl\">\n\n<head>\n    <meta charset=\"utf-8\">\n    <meta http-equiv=\"X-UA-Compatible\" content=\"IE=edge\">\n    <meta name=\"viewport\" content=\"width=device-width, initial-scale=1\">\n    <link href=\"/static/bootstrap/css/bootstrap-theme.min.css\" rel=\"stylesheet\">\n    <link href=\"/static/bootstrap/css/bootstrap.min.css\" rel=\"stylesheet\">\n    <script src=\"/static/angular/angular.min.js\"></script>\n    <script src=\"/static/angular-ui-bootstrap/ui-bootstrap-tpls.min.js\"></script>\n    <link href=\"/manage.css\" rel=\"stylesheet\">\n    <title>系统管理</title>\n</head>\n\n<body>\n    <nav class=\"navbar navbar-inverse navbar-fixed-top\" role=\"navigation\">\n        <div class=\"container-fluid\">\n            <div class=\"navbar-header\"><a class=\"navbar-brand\" href=\"#\">系统管理</a></div>\n        </div>\n    </nav>\n    <div class=\"container-fluid\">\n        <div class=\"row\">\n            <uib-alert type=\"danger\" ng-show=\"haveError\">错误{{err.code}}：{{err.desc}}；建议刷新页面。</uib-alert>\n            <div class=\"col-sm-12 col-md-12 main\">\n                <uib-accordion close-others=\"accordion.closeOther\">\n                    <uib-accordion-group heading=\"管理信息\" is-open=\"accordion.openInfo\" is-disabled=\"accordion.disableInfo\">\n                        <div class=\"container\">\n                            <uib-alert type=\"danger\" ng-show=\"!isLogin\">务必先填入房间信息再进行管理。</uib-alert>\n                            <uib-alert type=\"success\" ng-show=\"isLogin\">你选择了{{room}}房间</uib-alert>\n                            <div class=\"list-group\" ng-show=\"!isLogin\">\n                                <a href=\"#\" class=\"list-group-item\" ng-class=\"{active: room == roomS.id}\" ng-repeat=\"roomS in roomList\" ng-click=\"initRoom(roomS.id)\">{{roomS.display}} ({{roomS.id}})</a>\n                            </div>\n                            <form ng-submit=\"enterRoom(password)\" ng-show=\"!isLogin\">\n                                <div class=\"input-group\">\n                                    <input type=\"password\" class=\"form-control\" placeholder=\"房间密码\" ng-model=\"password\">\n                                    <span class=\"input-group-btn\">\n                            <button class=\"btn btn-success\" type=\"submit\">确认</button>\n                        </span>\n                                </div>\n                            </form>\n                        </div>\n                    </uib-accordion-group>\n                    <uib-accordion-group heading=\"弹幕搜索\" is-open=\"accordion.openDanmu\">\n                        <div ng-controller=\"DanmuCtrl\" class=\"container\">\n                            <form ng-submit=\"danmu.doSearch()\">\n                                <div class=\"input-group\">\n                                    <input type=\"text\" class=\"form-control\" placeholder=\"搜索弹幕\" ng-model=\"danmu.searchKey\">\n                                    <span class=\"input-group-btn\">\n                            <button class=\"btn btn-success\" type=\"submit\">搜索</button>\n                        </span>\n                                </div>\n                            </form>\n                            <div class=\"row\">\n                                <p>\n                                    <br/>\n                                </p>\n                                <table class=\"table table-hover\">\n                                    <tr>\n                                        <th>ID</th>\n                                        <th>用户</th>\n                                        <th>弹幕</th>\n                                    </tr>\n                                    <tr ng-repeat=\"item in danmu.result\">\n                                        <td>{{item.id}}</td>\n                                        <td>{{item.user}}</td>\n                                        <td>{{item.text}}</td>\n                                    </tr>\n                                </table>\n                            </div>\n                        </div>\n                    </uib-accordion-group>\n                    <uib-accordion-group heading=\"黑名单管理\" is-open=\"accordion.openBlock\">\n                        <div ng-controller=\"BlockCtrl\" class=\"container\">\n                            <form ng-submit=\"block.doAdd()\">\n                                <div class=\"input-group\">\n                                    <input type=\"text\" class=\"form-control\" placeholder=\"添加用户\" ng-model=\"block.textUser\">\n                                    <span class=\"input-group-btn\">\n                                    <button class=\"btn btn-success\" type=\"submit\">添加</button>\n                                    </span>\n                                </div>\n                            </form>\n                            <div class=\"row\">\n                                <p>\n                                    <br/>\n                                </p>\n                                <button type=\"button\" class=\"btn btn-primary btn-sm\" ng-repeat=\"item in block.result track by $index\" style=\"margin-right: 5px\" ng-click=\"block.doRemove(item)\" ng-value=\"item\">{{item}}</button>\n                            </div>\n                        </div>\n                    </uib-accordion-group>\n                    <uib-accordion-group heading=\"权限设置\" is-open=\"accordion.openPermissions\">\n                        <div ng-controller=\"PermissionCtrl\" class=\"container\">\n                            <form class=\"form-horizontal\" ng-submit=\"$submitPermissions()\">\n                                <div class=\"form-group\">\n                                    <label for=\"permissionSend\" class=\"col-sm-2 control-label\">弹幕接收开关</label>\n                                    <div class=\"col-sm-10\">\n                                        <button type=\"button\" class=\"btn\" ng-class=\"$makeClass('send')\" id=\"permissionSend\" ng-click=\"$setState('send')\">当前状态：{{$getStateText('send')}}</button> <small>关闭后不再接收任何新弹幕</small>\n                                    </div>\n                                </div>\n                                <div class=\"form-group\">\n                                    <label for=\"permissionStyle\" class=\"col-sm-2 control-label\">弹幕样式开关</label>\n                                    <div class=\"col-sm-10\">\n                                        <button type=\"button\" class=\"btn\" ng-class=\"$makeClass('style')\" id=\"permissionStyle\" ng-click=\"$setState('style')\">当前状态：{{$getStateText('style')}}</button>\n                                    </div>\n                                </div>\n                                <div class=\"form-group\">\n                                    <label for=\"permissionColor\" class=\"col-sm-2 control-label\">颜色设置开关</label>\n                                    <div class=\"col-sm-10\">\n                                        <button type=\"button\" class=\"btn\" ng-class=\"$makeClass('color')\" id=\"permissionColor\" ng-click=\"$setState('color')\">当前状态：{{$getStateText('color')}}</button>\n                                    </div>\n                                </div>\n                                <div class=\"form-group\">\n                                    <label for=\"permissionTextStyle\" class=\"col-sm-2 control-label\">文字样式开关</label>\n                                    <div class=\"col-sm-10\">\n                                        <button type=\"button\" class=\"btn\" ng-class=\"$makeClass('textStyle')\" id=\"permissionTextStyle\" ng-click=\"$setState('textStyle')\">当前状态：{{$getStateText('textStyle')}}</button>\n                                    </div>\n                                </div>\n                                <div class=\"form-group\">\n                                    <label for=\"permissionHeight\" class=\"col-sm-2 control-label\">高度设置开关</label>\n                                    <div class=\"col-sm-10\">\n                                        <button type=\"button\" class=\"btn\" ng-class=\"$makeClass('height')\" id=\"permissionHeight\" ng-click=\"$setState('height')\">当前状态：{{$getStateText('height')}}</button>\n                                    </div>\n                                </div>\n                                <div class=\"form-group\">\n                                    <label for=\"permissionLifeTime\" class=\"col-sm-2 control-label\">时间设置开关</label>\n                                    <div class=\"col-sm-10\">\n                                        <button type=\"button\" class=\"btn\" ng-class=\"$makeClass('lifeTime')\" id=\"permissionLifeTime\" ng-click=\"$setState('lifeTime')\">当前状态：{{$getStateText('lifeTime')}}</button>\n                                    </div>\n                                </div>\n                                <div class=\"form-group\">\n                                    <label for=\"permissionSourceCode\" class=\"col-sm-2 control-label\">自定义弹幕开关</label>\n                                    <div class=\"col-sm-10\">\n                                        <button type=\"button\" class=\"btn\" ng-class=\"$makeClass('sourceCode')\" id=\"permissionSourceCode\" ng-click=\"$setState('sourceCode')\">当前状态：{{$getStateText('sourceCode')}}</button> <small>此权限要求弹幕样式开关一并打开才可使用。非常危险，可能导致客户端出现莫名错误，对非信任用户不要打开！</small>\n                                    </div>\n                                </div>\n                                <div class=\"row\">\n                                    <button class=\"col-sm-offset-5 btn btn-success\" type=\"submit\">更新权限</button>\n                                </div>\n                            </form>\n                        </div>\n                    </uib-accordion-group>\n                    <uib-accordion-group heading=\"参数设置\" is-open=\"accordion.openConfig\">\n                        <div ng-controller=\"ConfigCtrl\" class=\"container\">\n                            <form class=\"form-horizontal\" ng-submit=\"config.submitConfig()\">\n                                <div class=\"form-group\">\n                                    <label for=\"configReplaceKeyword\" class=\"col-sm-2 control-label\">替换关键词</label>\n                                    <div class=\"col-sm-10\">\n                                        <input type=\"text\" class=\"form-control\" id=\"configReplaceKeyword\" placeholder=\"\" ng-model=\"config.realConfig.replaceKeyword\">\n                                    </div>\n                                </div>\n                                <div class=\"form-group\">\n                                    <label for=\"configBlockKeyword\" class=\"col-sm-2 control-label\">拦截关键词</label>\n                                    <div class=\"col-sm-10\">\n                                        <input type=\"text\" class=\"form-control\" id=\"configBlockKeyword\" placeholder=\"\" ng-model=\"config.realConfig.blockKeyword\">\n                                    </div>\n                                </div>\n                                <div class=\"form-group\">\n                                    <label for=\"configIgnoreKeyword\" class=\"col-sm-2 control-label\">判断拦截时忽略关键词</label>\n                                    <div class=\"col-sm-10\">\n                                        <input type=\"text\" class=\"form-control\" id=\"configIgnoreKeyword\" placeholder=\"\" ng-model=\"config.realConfig.ignoreKeyword\">\n                                    </div>\n                                </div>\n                                <div class=\"form-group\">\n                                    <label for=\"configSocketInterval\" class=\"col-sm-2 control-label\">弹幕向客户端发送间隔</label>\n                                    <div class=\"col-sm-10\">\n                                        <input type=\"number\" class=\"form-control\" id=\"configSocketInterval\" placeholder=\"\" ng-model=\"config.realConfig.socketinterval\">\n                                    </div>\n                                </div>\n                                <div class=\"form-group\">\n                                    <label for=\"configSocketSingle\" class=\"col-sm-2 control-label\">每次弹幕发送数量</label>\n                                    <div class=\"col-sm-10\">\n                                        <input type=\"number\" class=\"form-control\" id=\"configSocketSingle\" placeholder=\"\" ng-model=\"config.realConfig.socketsingle\">\n                                    </div>\n                                </div>\n                                <div class=\"form-group\">\n                                    <label for=\"configRoomLength\" class=\"col-sm-2 control-label\">弹幕堆积队列最大长度</label>\n                                    <div class=\"col-sm-10\">\n                                        <input type=\"number\" class=\"form-control\" id=\"configRoomLength\" placeholder=\"\" ng-model=\"config.realConfig.maxlength\">\n                                    </div>\n                                </div>\n                                <div class=\"form-group\">\n                                    <label for=\"configTextLength\" class=\"col-sm-2 control-label\">单条弹幕最大长度</label>\n                                    <div class=\"col-sm-10\">\n                                        <input type=\"number\" class=\"form-control\" id=\"configTextLength\" placeholder=\"\" ng-model=\"config.realConfig.textlength\">\n                                    </div>\n                                </div>\n                                <div class=\"row\">\n                                    <button class=\"col-sm-offset-5 btn btn-success\" type=\"submit\">更新配置</button>\n                                </div>\n                            </form>\n                        </div>\n                    </uib-accordion-group>\n                    <uib-accordion-group heading=\"密码设置\" is-open=\"accordion.openPassword\">\n                        <div ng-controller=\"PasswordCtrl\" class=\"container\">\n                            <form class=\"form-horizontal\" ng-submit=\"$submitPassword('advanced')\">\n                                <div class=\"form-group\">\n                                    <label for=\"passwordAdvanced\" class=\"col-sm-2 control-label\">高级弹幕发送密码</label>\n                                    <div class=\"col-sm-8\"><input type=\"password\" class=\"form-control\" id=\"passwordAdvanced\" placeholder=\"\" ng-model=\"config.advancedpassword\"></div>\n                                    <div class=\"col-sm-1\"><button class=\"col-sm-offset-5 btn btn-success\" type=\"submit\">更新密码</button></div>\n                                </div>\n                            </form>\n                            <form class=\"form-horizontal\" ng-submit=\"$submitPassword('manage')\">\n                                <div class=\"form-group\">\n                                    <label for=\"passwordManage\" class=\"col-sm-2 control-label\">后台管理密码</label>\n                                    <div class=\"col-sm-8\"><input type=\"password\" class=\"form-control\" id=\"passwordManage\" placeholder=\"\" ng-model=\"config.managepassword\"></div>\n                                    <div class=\"col-sm-1\"><button class=\"col-sm-offset-5 btn btn-success\" type=\"submit\">更新密码</button></div>\n                                </div>\n                            </form>\n                            <form class=\"form-horizontal\" ng-submit=\"$submitPassword('connect')\">\n                                <div class=\"form-group\">\n                                    <label for=\"passwordConnect\" class=\"col-sm-2 control-label\">客户端连接密码</label>\n                                    <div class=\"col-sm-8\"><input type=\"password\" class=\"form-control\" id=\"passwordConnect\" placeholder=\"\" ng-model=\"config.connectpassword\"></div>\n                                    <div class=\"col-sm-1\"><button class=\"col-sm-offset-5 btn btn-success\" type=\"submit\">更新密码</button></div>\n                                </div>\n                            </form>\n                        </div>\n                    </uib-accordion-group>\n                </uib-accordion>\n            </div>\n        </div>\n    </div>\n    <script src=\"/manage.js\"></script>\n</body>\n\n</html>\n"
  },
  {
    "path": "src/libraries/http/view/realtime.html",
    "content": "<!DOCTYPE html>\n<html lang=\"zh-cmn-CN\" ng-app=\"danmu.realtime\" ng-controller=\"MainCtrl\">\n\n<head>\n    <meta charset=\"utf-8\">\n    <meta http-equiv=\"X-UA-Compatible\" content=\"IE=edge\">\n    <meta name=\"viewport\" content=\"width=device-width, initial-scale=1\">\n    <link href=\"/static/bootstrap/css/bootstrap-theme.min.css\" rel=\"stylesheet\">\n    <link href=\"/static/bootstrap/css/bootstrap.min.css\" rel=\"stylesheet\">\n    <script src=\"/static/angular/angular.min.js\"></script>\n    <script src=\"/static/angular-ui-bootstrap/ui-bootstrap-tpls.js\"></script>\n    <link href=\"/manage.css\" rel=\"stylesheet\">\n    <title>实时弹幕</title>\n</head>\n\n<body>\n    <nav class=\"navbar navbar-inverse navbar-fixed-top\" role=\"navigation\">\n        <div class=\"container-fluid\">\n            <div class=\"navbar-header\"><a class=\"navbar-brand\" href=\"#\">实时弹幕</a></div>\n        </div>\n    </nav>\n    <div class=\"container-fluid\">\n        <div class=\"row\">\n            <uib-alert type=\"danger\" ng-show=\"haveError\">错误{{err.code}}：{{err.desc}}；建议刷新页面。</uib-alert>\n            <div class=\"col-sm-12 col-md-12 main\">\n                <uib-accordion close-others=\"false\">\n                    <uib-accordion-group heading=\"管理信息\" is-open=\"true\">\n                        <div class=\"container\">\n                            <uib-alert type=\"danger\" ng-show=\"!isLogin\">务必先填入房间信息再进行管理。</uib-alert>\n                            <uib-alert type=\"success\" ng-show=\"isLogin\">你选择了{{room}}房间</uib-alert>\n                            <div class=\"list-group\" ng-show=\"!isLogin\">\n                                <a href=\"#\" class=\"list-group-item\" ng-class=\"{active: room == roomS.id}\" ng-repeat=\"roomS in roomList\" ng-click=\"initRoom(roomS.id)\">{{roomS.display}} ({{roomS.id}})</a>\n                            </div>\n                            <form ng-submit=\"enterRoom(password)\" ng-show=\"!isLogin\">\n                                <div class=\"input-group\">\n                                    <input type=\"password\" class=\"form-control\" placeholder=\"房间密码\" ng-model=\"password\">\n                                    <span class=\"input-group-btn\">\n                                        <button class=\"btn btn-success\" type=\"submit\">确认</button>\n                                    </span>\n                                </div>\n                            </form>\n                        </div>\n                    </uib-accordion-group>\n                    <uib-accordion-group heading=\"实时弹幕\" is-open=\"true\">\n                        <div class=\"container\">\n                            <div ng-repeat=\"danmu in danmus track by danmu.socketId\" style=\"margin-top: 5px; margin-right: 5px; display: inline-block\">\n                            <div class=\"btn-group\" uib-dropdown>\n                              <button id=\"split-button-{{$index}}\" type=\"button\" class=\"btn btn-primary\" ng-click=\"deleteDanmu($index, danmu.id, '')\">{{danmu.text}}</button>\n                              <button type=\"button\" class=\"btn btn-primary\" uib-dropdown-toggle>\n                                <span class=\"caret\"></span>\n                                <span class=\"sr-only\">Split button!</span>\n                              </button>\n                              <ul uib-dropdown-menu role=\"menu\" aria-labelledby=\"split-button-{{$index}}\">\n                                <li role=\"menuitem\" class=\"disabled\"><a href=\"#\">{{danmu.hash}}</a></li>\n                                <li class=\"divider\"></li>\n                                <li role=\"menuitem\"><a href=\"#\" ng-click=\"deleteDanmu($index, danmu.id, '')\">删</a></li>\n                                <li role=\"menuitem\"><a href=\"#\" ng-click=\"deleteDanmu($index, danmu.id, danmu.hash)\">封</a></li>\n                              </ul>\n                            </div>\n                            </div>\n                        </div>\n                    </uib-accordion-group>\n                </uib-accordion>\n            </div>\n        </div>\n    </div>\n    <script>\n    var serverVersion = \"<%=version%>\";\n    </script>\n    <script src=\"realtime.js\"></script>\n    <script src=\"/socket.io/socket.io.js\"></script>\n</body>\n\n</html>\n"
  },
  {
    "path": "src/libraries/socket/index.js",
    "content": "const httpEvent = require('../../interfaces/Http')\nconst socketEvent = require('../../interfaces/Socket')\nconst danmuEvent = require('../../interfaces/Danmu')\nconst log = require('../../utilities/log')\nconst config = require('../../../config')\nlet inProcessRandomNumber = Math.random()\nlet io = null\nmodule.exports = {\n  init: function (callback) {\n    // 删除弹幕\n    danmuEvent.removing.listen(data => {\n      Object.keys(data).forEach(room => {\n        io.to(room).emit('delete', data[room])\n      })\n    })\n    // 推送弹幕\n    danmuEvent.transfer.listen(data => {\n      io.to(data.room).emit('danmu', data)\n    })\n\n    // 当服务器创建后，绑定WebSocket\n    httpEvent.created.listen(app => {\n      io = require('socket.io')(app)\n\n      io.on('connection', socket => {\n        // 向客户端推送密码请求\n        socket.emit('init', 'Require Password.')\n        socket.on('password', data => {\n          let room = data.room\n          if (!config.rooms[room]) {\n            socket.emit('init', 'Room Not Found')\n            log.log(`${socket.id}试图加入未定义房间`)\n            return false\n          }\n          if (data.password !== config.rooms[room].connectpassword) {\n            socket.emit('init', 'Password error')\n            return false\n          }\n          if (!data.info) {\n            log.log('该版本弹幕客户端过老，请更新弹幕客户端。')\n            return false\n          }\n          log.log(`客户端 ${socket.id}（${data.info.version}） in ${socket.conn.remoteAddress} 连接于 ${room}`)\n          socket.join(room)\n          socket.emit('connected', {\n            version, // eslint-disable-line\n            randomNumber: inProcessRandomNumber // 用于给客户端检测服务器是重启还是断线\n          })\n        })\n      })\n      socketEvent.created.emit(io)\n    })\n\n    callback(null)\n  }\n}\n"
  },
  {
    "path": "src/libraries/transfer/index.js",
    "content": "const configEvent = require('../../interfaces/Config')\nconst danmuEvent = require('../../interfaces/Danmu')\nconst filter = require('../../utilities/filter')\nconst log = require('../../utilities/log')\nconst utilities = require('../../utilities')\n\nlet danmuQueue = {}\nlet danmuKeys = []\nconst config = require('../../../config')\n\nlet danmuId = 0\n\nmodule.exports = {\n  init: function (callback) {\n    callback(null)\n  }\n}\n\n// 更新配置\nconfigEvent.updated.listen(data => {\n  clearAllTimeval()\n  initDanmuQueue()\n  startAllTimeval()\n})\n\n// 待推送弹幕\ndanmuEvent.get.listen(data => {\n// 过老弹幕没有意义，直接从队列头出队列\n  while (danmuQueue[data.room].queue.length > config.rooms[data.room].maxlength) {\n    danmuQueue[data.room].queue.shift()\n  }\n  if (data.lifeTime === '') {\n    data.lifeTime = utilities.parseLifeTime(data)\n  }\n  log.log(`房间${data.room}得到弹幕（${data.hash}）：${data.text}`)\n  danmuQueue[data.room].queue.push(data)\n})\n\nlet initDanmuQueue = function () {\n  danmuQueue = {}\n  danmuKeys = Object.keys(config.rooms)\n  danmuKeys.forEach(room => {\n    danmuQueue[room] = {\n      queue: [],\n      timeval: null\n    }\n  }\n  )\n}\n\nlet startAllTimeval = function () {\n  danmuKeys.forEach(room => {\n    if (config.rooms[room].permissions.send) {\n      danmuQueue[room].timeval = initTimeval(room)\n      log.log(`创建(${room})定时器 - ${config.websocket.interval} ms.`)\n    } else {\n      log.log(`${room} 房间弹幕已关闭，不创建定时器。`)\n    }\n  })\n}\n\nlet clearAllTimeval = function () {\n  danmuKeys.forEach(room => {\n    log.log(`清理(${room})定时器`)\n    clearInterval(danmuQueue[room].timeval)\n  })\n}\n\nlet initTimeval = function (room) {\n  return setInterval(() => {\n    // 定时推送\n    let ret = []\n    if (danmuQueue[room].queue.length === 0) return\n    while (ret.length < config.websocket.singlesize && danmuQueue[room].queue.length > 0) {\n      let object = danmuQueue[room].queue.pop()\n      // 只在传输时才需要进行替换\n      object.text = filter(room).replaceKeyword(object.text)\n      object.id = ++danmuId\n      ret.push(object)\n    }\n    log.log(`推送${ret.length}条弹幕到${room}，剩余${danmuQueue[room].queue.length}条。`)\n\n    danmuEvent.transfer.emit({\n      room: room,\n      data: ret\n    })\n  }, config.websocket.interval)\n}\n"
  },
  {
    "path": "src/utilities/filter.js",
    "content": "const configEvent = require('../interfaces/Config')\nconst _ = require('ramda')\nconst config = require('../../config')\n\nconst cachedFilters = {}\n/**\n * 检测用户是否被封禁\n */\nconst checkUserIsBlocked = _.curry((blockUsers, hash) => {\n  return (blockUsers.indexOf(hash)) > -1\n})\n/**\n * 检测文字是否和谐\n */\nconst validateText = _.curry((ignoreRegEx, checkRegEx, str) => {\n  checkRegEx.lastIndex = 0\n  const testStr = str.replace(ignoreRegEx, '')\n  return !checkRegEx.test(testStr)\n})\n/**\n * 替换关键字\n */\nconst replaceKeyword = _.curry((regex, str) => str.replace(regex, '***'))\n\nfunction initialize (roomName, forceUpdate) {\n  if (cachedFilters[roomName] && !forceUpdate) {\n    return cachedFilters[roomName]\n  }\n  const room = config.rooms[roomName]\n  if (typeof room.keyword.block === 'string') {\n    console.error('请升级你的配置，将所有字符串类型关键词转换为正则类型关键词。')\n    throw new Error('Init RegExp Error')\n  }\n  const ret = {\n    checkUserIsBlocked: checkUserIsBlocked(room.blockusers),\n    validateText: validateText(room.keyword.ignore)(room.keyword.block),\n    replaceKeyword: replaceKeyword(room.keyword.replacement)\n  }\n  cachedFilters[roomName] = null // Release Memory\n  cachedFilters[roomName] = ret\n  return ret\n};\n\n// 正则缓存更新\nconfigEvent.updated.listen(() => {\n  Object.keys(config.rooms).forEach(room => initialize(room, true))\n})\nmodule.exports = initialize\n"
  },
  {
    "path": "src/utilities/index.js",
    "content": "'use strict'\nconst crypto = require('crypto')\nconst config = require('../../config')\n/**\n * 生成MD5\n */\nconst md5 = text => crypto.createHash('md5').update(text).digest('hex')\n/**\n * 计算Hash\n */\nconst getHash = (ip, userAgent, hashCode) => md5(`IP=${ip}\\nUA=${userAgent}\\nHC=${hashCode}`)\n/**\n * 获取一个可读的当前时间\n */\nconst getTime = () => {\n  const d = new Date()\n  return d.getFullYear() + '-' + (d.getMonth() + 1) + '-' + d.getDate() + ' ' + d.getHours() + ':' + d.getMinutes() + ':' + d.getSeconds() + '.' + d.getMilliseconds()\n}\n/**\n * 把配置格式化为数组\n */\nconst buildConfigToArray = room => {\n  return {\n    replaceKeyword: config.rooms[room].keyword.replacement.source,\n    blockKeyword: config.rooms[room].keyword.block.source,\n    ignoreKeyword: config.rooms[room].keyword.ignore.source,\n    maxlength: config.rooms[room].maxlength,\n    textlength: config.rooms[room].textlength,\n    socketinterval: config.websocket.interval,\n    socketsingle: config.websocket.singlesize\n  }\n}\n/**\n * 计算弹幕生存时间\n */\nconst parseLifeTime = data => {\n  const imageMatches = data.text.match(config.rooms[data.room].image.regex)\n  const imageLength = imageMatches === null ? 0 : imageMatches.length\n  return (Math.trunc(data.text.length / 10)) * 240 + config.rooms[data.room].image.lifetime * imageLength\n}\n// 全局工具\nmodule.exports = {\n  md5,\n  getHash,\n  getTime,\n  buildConfigToArray,\n  parseLifeTime\n}\n"
  },
  {
    "path": "src/utilities/log.js",
    "content": "const utilities = require('./')\nmodule.exports = {\n  log: function (text) {\n    console.log('[' + utilities.getTime() + '] ' + text)\n  }\n}\n"
  }
]