Repository: zsxsoft/danmu-server Branch: master Commit: 1b030926221e Files: 56 Total size: 111.2 KB Directory structure: gitextract_ll54n030/ ├── .eslintignore ├── .eslintrc ├── .gitattributes ├── .gitignore ├── Dockerfile ├── README.md ├── app.js ├── config.js ├── docker/ │ ├── create_db.sh │ ├── create_mysql_admin_user.sh │ └── run.sh ├── jsconfig.json ├── package.json └── src/ ├── controllers/ │ ├── DanmuController.js │ └── UserController.js ├── extensions/ │ ├── audit/ │ │ ├── Audit.js │ │ ├── audit.html │ │ └── index.js │ ├── autoban/ │ │ └── index.js │ ├── index.js │ ├── livesync/ │ │ ├── get.py │ │ └── index.js │ └── weibo/ │ ├── index.js │ └── login.html ├── interfaces/ │ ├── Base.js │ ├── Config.js │ ├── Danmu.js │ ├── Http.js │ └── Socket.js ├── libraries/ │ ├── cache/ │ │ └── index.js │ ├── database/ │ │ ├── csv.js │ │ ├── index.js │ │ ├── mongo.js │ │ ├── mysql.js │ │ └── none.js │ ├── http/ │ │ ├── index.js │ │ ├── res/ │ │ │ ├── manage.css │ │ │ ├── manage.js │ │ │ └── realtime.js │ │ ├── route/ │ │ │ ├── index.js │ │ │ ├── manage.js │ │ │ ├── manageBlock.js │ │ │ ├── manageConfig.js │ │ │ ├── manageDanmu.js │ │ │ ├── manageRoom.js │ │ │ ├── manageSearch.js │ │ │ ├── post.js │ │ │ └── realtime.js │ │ └── view/ │ │ ├── index.html │ │ ├── manage.html │ │ └── realtime.html │ ├── socket/ │ │ └── index.js │ └── transfer/ │ └── index.js └── utilities/ ├── filter.js ├── index.js └── log.js ================================================ FILE CONTENTS ================================================ ================================================ FILE: .eslintignore ================================================ /http/res/* config.js ================================================ FILE: .eslintrc ================================================ { "env": { "es6": true, "browser": false, "node": true }, "parser": "babel-eslint", "parserOptions": { "sourceType": "module" }, "plugins": [ "babel" ], "extends": [ "standard" ] } ================================================ FILE: .gitattributes ================================================ # Auto detect text files and perform LF normalization * text=auto # Custom for Visual Studio *.cs diff=csharp # Standard to msysgit *.doc diff=astextplain *.DOC diff=astextplain *.docx diff=astextplain *.DOCX diff=astextplain *.dot diff=astextplain *.DOT diff=astextplain *.pdf diff=astextplain *.PDF diff=astextplain *.rtf diff=astextplain *.RTF diff=astextplain ================================================ FILE: .gitignore ================================================ .vs .idea .vscode *.csv # Logs logs *.log # Runtime data pids *.pid *.seed # Directory for instrumented libs generated by jscoverage/JSCover lib-cov # Coverage directory used by tools like istanbul coverage # Grunt intermediate storage (http://gruntjs.com/creating-plugins#storing-task-files) .grunt # Compiled binary addons (http://nodejs.org/api/addons.html) build/Release # Dependency directory # Commenting this out is preferred by some people, see # https://www.npmjs.org/doc/misc/npm-faq.html#should-i-check-my-node_modules-folder-into-git- node_modules # Users Environment Variables .lock-wscript # ========================= # Operating System Files # ========================= # OSX # ========================= .DS_Store .AppleDouble .LSOverride # Thumbnails ._* # Files that might appear on external disk .Spotlight-V100 .Trashes # Directories potentially created on remote AFP share .AppleDB .AppleDesktop Network Trash Folder Temporary Items .apdisk # Windows # ========================= # Windows image file caches Thumbs.db ehthumbs.db # Folder config file Desktop.ini # Recycle Bin used on file shares $RECYCLE.BIN/ # Windows Installer files *.cab *.msi *.msm *.msp # Windows shortcuts *.lnk # Visual Studio Code typings ================================================ FILE: Dockerfile ================================================ FROM node:latest MAINTAINER zsx ENV APP /usr/src/app ## ---------------------------- ## MariaDB Start ## ---------------------------- ## Add MariaDB PPK RUN apt-key adv --recv-keys --keyserver hkp://keyserver.ubuntu.com:80 0xcbcb082a1bb943db && \ echo 'deb http://mirrors.syringanetworks.net/mariadb/repo/10.1/ubuntu trusty main' >> /etc/apt/sources.list && \ echo 'deb-src http://mirrors.syringanetworks.net/mariadb/repo/10.1/ubuntu trusty main' >> /etc/apt/sources.list && \ apt-get update && \ DEBIAN_FRONTEND=noninteractive apt-get install -y mariadb-server pwgen && \ rm -rf /var/lib/mysql/* && \ sed -i -r 's/bind-address.*$/bind-address = 0.0.0.0/' /etc/mysql/my.cnf && \ apt-get install -y memcached && \ mkdir -p ${APP} WORKDIR ${APP}/ ADD ./ ./ ADD ./docker/ /docker RUN chmod +x /docker/*.sh && \ npm install && \ npm cache clean && apt-get clean && rm -rf /var/lib/apt/lists/* VOLUME ["/etc/mysql", "/var/lib/mysql"] EXPOSE 3306 11211 3000 CMD ["/docker/run.sh"] ================================================ FILE: README.md ================================================ danmu-server ================ [![David deps](https://david-dm.org/zsxsoft/danmu-server.svg)](https://david-dm.org/zsxsoft/danmu-server) 弹幕服务器,其客户端项目见[danmu-client](https://github.com/zsxsoft/danmu-client)。 **欲使用此项目,客户端需要使用对应的版本。[已发布的服务端](https://github.com/zsxsoft/danmu-server/releases)均已写明对应的客户端版本号,开发分支内的服务端版本仅对应开发分支的客户端。** ## 功能特色 - 跨平台; - 房间功能; - 后台管理; - 弹幕记录与搜索(需要开启数据库); - 黑名单功能; - 关键词替换、拦截功能; - 弹幕记录; - 扩展; - 新浪微博登录扩展(需要开启缓存); - 自动封禁功能扩展(需要开启缓存); - 审核扩展; - 直播拉取扩展 - 删除单条弹幕功能; - 易于部署,简单高效。 ## 后台截图 ![后台截图](http://zsxsoft.github.io/danmu-server/management.png) ## 一些警告 稳定版请于[Release](https://github.com/zsxsoft/danmu-server/releases)手动下载。 ## 部署方式 ### 检查环境 #### Nodejs Nodejs >= 6 #### 数据库 如使用``csv``,可无视此节。 默认使用``MySQL``数据库。如需使用,需检查[MariaDB](https://mariadb.org/)或[MySQL](https://www.mysql.com/)的安装状态。支持``5.0+``。安装完成后,请创建相应的数据库。 如使用``MongoDB``数据库,请检查[MongoDB](https://www.mongodb.org/)的安装状态。然后需要在安装完成后执行:``npm install mongodb``。 #### 缓存 如不使用新浪微博与自动封禁功能,可无视此节。 默认使用``memcached``。如需使用,请检查[Memcached(Linux)](http://memcached.org/)的安装状态。``Windows``用户请自行查找适合的``Memcached``版本。 如果要用[阿里云开放缓存服务OCS](http://www.aliyun.com/product/ocs/),需要在安装完成后执行:``npm install aliyun-sdk``。 ### 直接安装 1. 配置MariaDB,创建数据库等,不需要创建数据表。 2. 修改``config.js``,使其参数与环境相符。 3. 切换到命令行或终端,``cd``到程序所在目录执行``npm install``,安装程序依赖库。 4. 现在,你可以直接``npm start``启动。 ### Docker安装 __Dockerfile可能年久失修,建议自己用``alpine``封装一个。__ 直接用``Docker``安装的话,镜像内是含``MariaDB``的。 1. [安装Docker](http://yeasy.gitbooks.io/docker_practice/content/install/index.html)。 2. ``config.js``调整配置。 3. ``docker build -t="zsxsoft/danmu-server:" . && docker run -t -i -p 3000:3000 "zsxsoft/danmu-server"`` ## 升级 ### 1.0.6 -> 1.1.0 * 在每个房间内增加cdn: false配置 ### 1.0.5 -> 1.0.6 * 在每个房间内增加hostname配置,类型为数组,用于将房间与域名绑定 ## 网页接口 ### GET / 可以直接发布最简单的弹幕。 ### GET /advanced 可以发布高级弹幕(需要密码) ### GET /manage 可以进行后台管理 ### GET /realtime 可以实时接收弹幕并直接删除或封禁(需要密码) ## 配置说明 以下标有``*``的配置项,运行时不可在后台修改。 ```javascript "rooms": { "房间1": { * "hostname": ["test.zsxsoft.com", "localhost", "127.0.0.1"], * "cdn": 是否使用CDN或反向代理(用于获取正确的IP), * "display": "房间显示名", * "table": "对应MySQL的数据表、MongoDB的集合", "connectpassword": "客户端连接密码", "managepassword": "管理密码", "advancedpassword": "高级弹幕密码", "keyword": { "block": /强制屏蔽关键词,正则格式。/ "replacement": /替换关键词,正则格式/, "ignore": /忽略词,正则格式/ }, "blockusers": [ "默认封禁用户列表" ], "maxlength": 弹幕堆积队列最大长度, "textlength": 每条弹幕最大长度, * "image": { * "regex": /图片弹幕解析正则,正则格式,不要修改/ig, * "lifetime": 每个图片给每条弹幕增加的存货时间 }, "permissions": { // 普通用户允许的弹幕权限 "send": 弹幕开关;关闭后无论普通用户还是高级权限都完全禁止弹幕。, "style": 弹幕样式开关, "color": 颜色开关, "textStyle": CSS开关, "height": 高度开关, "lifeTime": 显示时间开关, } }, "房间ID2": { // 同上 } }, * "database": { // 数据库 * "type": "数据库类型(mysql / mongo / csv / none)", * "server": " 数据库地址(mysql / mongo)", * "username": "数据库用户名(mysql / mongo)", * "password": "数据库密码(mysql / mongo)", * "port": "数据库端口(mysql / mongo)", * "db": "数据库(mysql / mongo)", * "retry": 24小时允许断线重连最大次数,超过则自动退出程序。24小时以第一次断线时间计。(mysql), * "timeout": 数据库重连延时及Ping(mysql), * "savedir": "指定文件保存位置(csv)", }, "websocket": { "interval": 弹幕发送间隔 "singlesize": 每次弹幕发送数量 }, * "http": { * "port": 服务器HTTP端口, * "headers": {}, // HTTP头 * "sessionKey": "随便写点,防冲突的" }, * "cache": { * "type": "缓存类型(memcached / aliyun)", * "host": "缓存服务器地址,可用socket", * "auth": 打开身份验证, * "authUser": 身份验证账号, * "authPassword": 身份验证密码, }, "ext": { // 扩展 } } ``` ## 扩展 ### 新浪微博登录 ```javascript "weibo": { // 新浪微博扩展 "clientID": '', // App ID "clientSecret": '', // App Secret "callbackURL": 'http://test.zsxsoft.com:3000/auth/sina/callback', // 这里填写的是 网站地址/auth/sina/callback "requireState": true // 是否打开CSRF防御 } ``` ### 自动封禁 ```javascript "autoban": { // 自动封号扩展 "block": 3, // 被拦截超过一定数字自动封号 } ``` ### 全局审核 ```javascript "audit": { // 审核扩展 } ``` ### 直播同步 此扩展基于[danmu](https://github.com/littlecodersh/danmu)项目开发,需要安装Python 2.7+ 或 Python 3.5+。在启用前,你首先需要 ```bash pip install danmu ``` 才可打开。 ```javascript "livesync": { // 新浪微博扩展 "房间名": { "liveUrl": '', // 直播网站地址 } } ``` ## 常见问题 ### 数据库相关 ``{ [Error: Connection lost: The server closed the connection.] fatal: true, code: 'PROTOCOL_CONNECTION_LOST' }`` 请把MySQL的``wait_timeout``设置得大一些。 ## 搭配项目 - [danmu-client](https://github.com/zsxsoft/danmu-client) ## 流程图 ![流程图](http://zsxsoft.github.io/danmu-server/route.png) ## 协议 The MIT License (MIT) ## 博文 [弹幕服务器及搭配之透明弹幕客户端研究结题报告](http://blog.zsxsoft.com/post/15) [弹幕服务器及搭配之透明弹幕客户端研究中期报告](http://blog.zsxsoft.com/post/14) [弹幕服务器及搭配之透明弹幕客户端研究开题报告](http://blog.zsxsoft.com/post/13) ## 开发者 zsx - https://www.zsxsoft.com / 博客 - https://blog.zsxsoft.com ================================================ FILE: app.js ================================================ const os = require('os') const async = require('async') const fs = require('fs') const path = require('path') const configEvent = require('./src/interfaces/Config') const log = require('./src/utilities/log') const packageJson = require('./package.json') let config = require('./config') global.version = packageJson.version global.Promise = require('bluebird') { log.log(`弹幕服务器版本:${global.version}`) log.log(`环境:${os.platform()}(${os.release()}) ${os.arch()} with ${parseInt(os.totalmem() / 1024 / 1024)}MB`) let dbPos = config.database if (process.env.MYSQL_PORT_3306_TCP_PORT) { // 检测DaoCloud的MySQL服务 dbPos.type = 'mysql' dbPos.server = process.env.MYSQL_PORT_3306_TCP_ADDR dbPos.username = process.env.MYSQL_USERNAME dbPos.password = process.env.MYSQL_PASSWORD dbPos.port = process.env.MYSQL_PORT_3306_TCP_PORT dbPos.db = process.env.MYSQL_INSTANCE_NAME console.log('检测到配置在环境变量内的MySQL,自动使用之。') } else if (dbPos.type === 'mongo' && process.env['27017/tcp']) { // MongoDB服务 dbPos.type = 'mongo' dbPos.server = process.env['27017/tcp'].split(':')[0].trim() // tcp://xx.xx.xx.xx:27017 dbPos.port = process.env['27017/tcp'].split(':')[1].trim() // tcp://xx.xx.xx.xx:27017 dbPos.username = process.env.USERNAME dbPos.password = process.env.PASSWORD dbPos.db = process.env.INSTANCE_NAME console.log('检测到配置在环境变量内的MongoDB,自动使用之。') } // 加载模块 async.map(['extensions', 'libraries/cache', 'libraries/transfer', 'libraries/database', 'libraries/http', 'libraries/socket'], (mdl, callback) => { require(`./src/${mdl}`).init(callback) }, err => { if (err) throw err fs.readdir(path.join(__dirname, './src/controllers'), (err, files) => { if (err) throw err files.forEach((filename) => require(path.join(__dirname, './src/controllers', filename))) }) configEvent.updated.emit() log.log('服务器初始化完成') }) } ================================================ FILE: config.js ================================================ module.exports = { "rooms": { "default": { "hostname": ["test.zsxsoft.com", "danmu.zsxsoft.com"], "cdn": false, "display": "默认", "table": "room_default", // 数据表 "connectpassword": "123456", // 客户端连接密码 "managepassword": "123456", // 管理密码 "advancedpassword": "123456", // 高级弹幕密码 "keyword": { "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, // 强制屏蔽关键词 "replacement": /父|母|夫|妻|女儿|儿子|孙子|孙女|女婿|娘|爹|爸|妈|爷|奶|哥|弟|兄|姐|妹|鸡|鸭|狗|猪|gay|mother|mom|father|dad|sister|brother|son|daughter|dog|pig/ig, // 替换关键词 "ignore": /\~|\!|\@|\#|\$|\%|\^|\&|\*|\(|\)|_|\||\+|\-|\=|\{|\}|\[|\]|\;|\'|\:|\"|\<|\>|\?|\/|\.|\,|\!|\#|\¥|\…|\(|\)|\—|\、|\【|\】|\{|\}|\;|\:|\‘|\’|\“|\”|\《|\》|\\|\,|\。|\、|\?|\ |\ /ig // 忽略词 }, "blockusers": [ // 封禁用户 "test" ], "maxlength": 100, // 队列最大长度 "textlength": 1000, // 弹幕最大长度 "image": { "regex": /\[IMG WIDTH=(\d+)\](.+?)\[\/IMG\]/ig, // 图片弹幕 "lifetime": 300 // 每个图片给每条弹幕增加的时间 }, "permissions": { // 普通用户允许的弹幕权限 "send": true, // 弹幕开关;关闭后无论普通用户还是高级权限都完全禁止弹幕。 "style": false, // 弹幕样式开关 "color": false, // 颜色开关 "textStyle": false, // CSS开关 "height": false, // 高度开关 "lifeTime": false, // 显示时间开关 "sourceCode": false, // 自定义高级JavaScript弹幕开关 } }, "unlimited": { "hostname": ["127.0.0.1", "localhost"], "cdn": false, "display": "无限房间", "table": "room_unlimited", // 数据表 "connectpassword": "", // 客户端连接密码 "managepassword": "", // 管理密码 "advancedpassword": "", // 高级弹幕密码 "keyword": { "block": /^$/, // 强制屏蔽关键词 "replacement": /^$/, // 替换关键词 "ignore": /^$/ // 忽略词 }, "blockusers": [ // 封禁用户 ], "maxlength": 1000, // 队列最大长度 "textlength": 10000, // 弹幕最大长度 "image": { "regex": /\[IMG WIDTH=(\d+)\](.+?)\[\/IMG\]/ig, // 图片弹幕 "lifetime": 300 // 每个图片给每条弹幕增加的时间 }, "permissions": { // 普通用户允许的弹幕权限 "send": true, // 弹幕开关;关闭后无论普通用户还是高级权限都完全禁止弹幕。 "style": true, // 弹幕样式开关 "color": true, // 颜色开关 "textStyle": true, // CSS开关 "height": true, // 高度开关 "lifeTime": true, // 显示时间开关 "sourceCode": true, // 自定义高级JavaScript弹幕开关 } } }, "database": { "type": "csv", // 数据库类型(mysql / mongo / csv / none) "server": "127.0.0.1", // 数据库地址(mysql / mongo) "username": "root", // 数据库用户名(mysql / mongo) "password": "123456", // 数据库密码(mysql / mongo) "port": "3306", // 数据库端口(mysql / mongo) "db": "danmu", // 数据库(mysql / mongo) "retry": 10, // 24小时允许断线重连最大次数,超过则自动退出程序。24小时以第一次断线时间计。(mysql) "timeout": 1000, // 数据库重连延时及Ping(mysql) "savedir": "./", // 指定文件保存位置(csv) }, "websocket": { "interval": 10, // 弹幕发送间隔 "singlesize": 5 // 每次弹幕发送数量 }, "http": { "port": 3000, // 服务器端口 "headers": { // HTTP头 //"Access-Control-Allow-Origin": "*", //"Access-Control-Allow-Methods": "POST" }, "sessionKey": "hey" }, "cache": { "type": "none", // 缓存类型,支持memcached和aliyun。后者需要npm install aliyun-sdk "host": "127.0.0.1:11211", // 缓存服务器地址,可用socket "auth": false, // 是否打开身份验证 "authUser": "", // 身份验证账号 "authPassword": "" // 身份验证密码 }, "ext": { /* "weibo": { // 新浪微博扩展 "clientID": '', // App ID "clientSecret": '', // App Secret "callbackURL": 'http://test.zsxsoft.com:3000/auth/sina/callback', // 这里填写的是 网站地址/auth/sina/callback "requireState": true // 是否打开CSRF防御 },*/ "autoban": { // 自动封号扩展 "block": 3, // 被拦截超过一定数字自动封号 }, /*"audit": { // 审核扩展 },*/ "livesync": { "unlimited": { // 房间名 "liveUrl": "http://live.bilibili.com/3" // 直播地址 } } } }; ================================================ FILE: docker/create_db.sh ================================================ #!/bin/bash if [[ $# -eq 0 ]]; then echo "Usage: $0 " exit 1 fi echo "=> Creating database $1" RET=1 while [[ RET -ne 0 ]]; do sleep 5 mysql -uroot -e "CREATE DATABASE $1" RET=$? done echo "=> Done!" ================================================ FILE: docker/create_mysql_admin_user.sh ================================================ #!/bin/bash echo "=> Creating database danmu in MySQL" /docker/create_db.sh danmu PASS=${MYSQL_PASS:-$(pwgen -s 12 1)} _word=$( [ ${MYSQL_PASS} ] && echo "preset" || echo "random" ) echo "=> Creating MySQL admin user with ${_word} password" mysql -uroot -e "CREATE USER 'admin'@'%' IDENTIFIED BY '$PASS'" mysql -uroot -e "GRANT ALL PRIVILEGES ON *.* TO 'admin'@'%' WITH GRANT OPTION" echo "=> Done!" echo "========================================================================" echo "You can now connect to this MySQL Server using:" echo "" echo " mysql -uadmin -p$PASS -h -P" echo "" echo "Please remember to change the above password as soon as possible!" echo "MySQL user 'root' has no password but only allows local connections" echo "========================================================================" ================================================ FILE: docker/run.sh ================================================ #!/bin/bash ## Part MySQL if [ ! -n "$MYSQL_PORT_3306_TCP_PORT" ]; then VOLUME_HOME="/var/lib/mysql" MYSQL_INSTALLED="no" if [[ ! -d $VOLUME_HOME/mysql ]]; then echo "=> An empty or uninitialized MySQL volume is detected in $VOLUME_HOME" echo "=> Installing MySQL ..." mysql_install_db > /dev/null 2>&1 echo "=> Done!" else MYSQL_INSTALLED="yes" fi echo "=> Starting MySQL ..." /usr/bin/mysqld_safe > /dev/null 2>&1 & MYSQL_STATE=1 while [[ RET -ne 0 ]]; do echo "=> Waiting for confirmation of MySQL service startup" sleep 5 mysql -uroot -e "status" > /dev/null 2>&1 MYSQL_STATE=$? done if [ "$MYSQL_INSTALLED" = no ]; then /docker/create_mysql_admin_user.sh fi fi ## Part Memcached USERNOTEXISTSRET=true getent passwd memcached >/dev/null 2>&1 && USERNOTEXISTSRET=false if $USERNOTEXISTSRET; then useradd memcached -s /nologin fi echo "=> Starting memcached ..." memcached -u memcached & ## Part Main echo "=> Starting service ..." npm start ================================================ FILE: jsconfig.json ================================================ { // See http://go.microsoft.com/fwlink/?LinkId=759670 // for the documentation about the jsconfig.json format "compilerOptions": { "target": "es6" }, "exclude": [ "node_modules", "bower_components", "jspm_packages", "tmp", "temp", "lib/http/res" ] } ================================================ FILE: package.json ================================================ { "name": "danmu-server", "version": "1.1.0", "license": "MIT", "scripts": { "start": "node app.js" }, "dependencies": { "angular": "^1.4.6", "angular-ui-bootstrap": "^0.14.3", "async": "^2.5.0", "bluebird": "^3.5.0", "body-parser": "^1.17.2", "bootstrap": "^3.3.7", "cookie-parser": "^1.4.3", "ejs": "^2.5.7", "errorhandler": "^1.5.0", "express": "^4.15.4", "express-session": "^1.15.5", "jquery": "^3.2.1", "memcached": "^2.2.2", "morgan": "^1.8.2", "mysql": "^2.14.1", "passport": "^0.4.0", "passport-sina": "git+https://github.com/zsxtoys/passport-sina-fork.git", "ramda": "^0.24.1", "socket.io": "^2.0.3" }, "devDependencies": { "babel-eslint": "^7.2.3", "eslint": "^4.5.0", "eslint-config-standard": "^10.2.1", "eslint-plugin-babel": "^4.1.2", "eslint-plugin-import": "^2.7.0", "eslint-plugin-node": "^5.1.1", "eslint-plugin-promise": "^3.5.0", "eslint-plugin-standard": "^3.0.1" }, "description": "danmu-server", "main": "app.js", "repository": { "type": "git", "url": "git+https://github.com/zsxsoft/danmu-server.git" }, "keywords": [ "danmu", "danmaku", "弹幕" ], "author": "zsx ", "bugs": { "url": "https://github.com/zsxsoft/danmu-server/issues" }, "homepage": "https://github.com/zsxsoft/danmu-server#readme", "engines": { "node": ">=5.5" }, "babel": { "presets": [ "es2015", "stage-0" ] } } ================================================ FILE: src/controllers/DanmuController.js ================================================ const filter = require('../utilities/filter') const log = require('../utilities/log') const danmuEvent = require('../interfaces/Danmu') const configEvent = require('../interfaces/Config') const permissions = ['color', 'style', 'height', 'lifeTime', 'textStyle', 'sourceCode'] // 为了不foreach let config = require('../../config') danmuEvent.addSingle.listen((data, inputs = {}, extra = { password: '', isAdvanced: false }) => { return new Promise((resolve, reject) => { const room = data.room const roomConfig = config.rooms[room] const realFilter = filter(room) if (!roomConfig.permissions.send) { return reject(new Error('弹幕暂时被关闭')) } if (extra.isAdvanced) { if (extra.password !== roomConfig.advancedpassword) { return reject(new Error('高级弹幕密码错误!')) } } if (!extra.isAdvanced && data.text.length > roomConfig.textlength) { return reject(new Error(`弹幕长度大于${roomConfig.textlength}个字,可能影响弹幕观感,请删减。`)) } if (realFilter.checkUserIsBlocked(data.hash) || !realFilter.validateText(data.text)) { log.log(`拦截 ${data.hash} - ${data.text}`) danmuEvent.ban.emit(data) return reject(new Error('发送失败!\n请检查你发送的弹幕有无关键词,或确认自己未被封禁。')) } permissions.forEach((val) => { if (extra.isAdvanced || roomConfig.permissions[val]) { data[val] = inputs[val] || '' } }) resolve(true) danmuEvent.get.emit(data) }) }) /** * 删除一条弹幕 */ danmuEvent.removeSingle.listen((data, blockUser = false) => { let deleteObject = {} deleteObject[data.room] = { ids: [data.id], hashs: [data.hash] } danmuEvent.removing.emit(deleteObject) if (blockUser) configEvent.blockUser.emit(data.room, data.hash) log.log(`删除弹幕 ${data.id} 成功`) }) ================================================ FILE: src/controllers/UserController.js ================================================ const configEvent = require('../interfaces/Config') const log = require('../utilities/log') let config = require('../../config') configEvent.blockUser.listen((room, username) => { config.rooms[room].blockusers.push(username) configEvent.updated.emit() log.log(`封禁用户${username}成功`) }) configEvent.unblockUser.listen((room, username) => { return new Promise((resolve, reject) => { let indexOf = config.rooms[room].blockusers.indexOf(username) if (indexOf >= 0) { config.rooms[room].blockusers.splice(indexOf, 1) configEvent.updated.emit() log.log(`已从黑名单移除用户${username}`) resolve() } else { log.log(`黑名单中未搜索到${username}`) reject(new Error(username)) } }) }) ================================================ FILE: src/extensions/audit/Audit.js ================================================ const Base = require('../../interfaces/Base') const className = 'audit' const _ = require('ramda') const register = _.curry(Base.register)(className) const passed = register('passed') class Audit extends Base { static get className () { return className } static get passed () { return passed } } module.exports = Audit ================================================ FILE: src/extensions/audit/audit.html ================================================ 弹幕审核
错误{{err.code}}:{{err.desc}};建议刷新页面。
务必先填入房间信息再进行管理。 你选择了{{room}}房间
================================================ FILE: src/extensions/audit/index.js ================================================ // / 'use strict' const fs = require('fs') const path = require('path') const socketEvent = require('../../interfaces/Socket') const httpEvent = require('../../interfaces/Http') const configEvent = require('../../interfaces/Config') const danmuEvent = require('../../interfaces/Danmu') const auditEvent = require('./Audit') const log = require('../../utilities/log') const config = require('../../../config') let danmuQueue = {} let danmuId = 0 let io = null function Audit () { socketEvent.created.listen(socketObject => { io = socketObject io.on('connection', socket => { socket.on('auditLogin', data => { let room = data.room if (!config.rooms[room]) return socket.emit('auditInit', 'Room Not Found') let managePassword = config.rooms[room].managepassword if (managePassword !== data.password) return socket.emit('auditInit', 'Password Error') socket.join(`auditRoom${room}`) socket.emit('auditConnected') log.log(`审核页面${socket.id}已连接${room}`) { let danmuObject = {} danmuQueue[room].forEach((value, key) => { danmuObject[key] = value }) socket.emit('auditDanmu', danmuObject) } }) socket.on('auditPass', data => { log.log(`通过${data.room}(id = ${data.id})`) auditEvent.passed.emit(danmuQueue[data.room].get(parseInt(data.id))) danmuQueue[data.room].delete(data.id) }) socket.on('auditFail', data => { log.log(`否决${data.room}(id = ${data.id})`) danmuQueue[data.room].delete(data.id) if (data.hash !== '') { config.rooms[data.room].blockusers.push(data.hash) log.log(`封禁用户${data.hash}成功`) configEvent.updated.emit() } }) }) }) httpEvent.beforeRoute.listen(app => { let danmuKeys = Object.keys(config.rooms) app.get('/audit', (req, res, next) => fs.readFile(path.join(__dirname, './audit.html'), (err, data) => { if (err) throw err res.end(data) })) // Remove all listeners to gotDanmu and bind to a new listener. const danmuEvents = danmuEvent.get.listeners() danmuEvent.get.removeAllListeners() danmuEvents.forEach(event => auditEvent.passed.listen(event)) danmuKeys.forEach(room => { danmuQueue[room] = new Map() }) danmuEvent.get.listen(data => { let room = data.room danmuQueue[room].set(++danmuId, data) io.to(`auditRoom${room}`).emit('auditDanmu', { [danmuId]: data }) // 懒得再去写队列 log.log(`${data.room}得到待审核弹幕(${data.hash}) - ${danmuId}:${data.text}`) }) }) }; module.exports = Audit ================================================ FILE: src/extensions/autoban/index.js ================================================ // / 'use strict' const configEvent = require('../../interfaces/Config') const danmuEvent = require('../../interfaces/Danmu') const filter = require('../../utilities/filter') const log = require('../../utilities/log') const cache = require('../../libraries/cache') let config = require('../../../config') module.exports = function () { // 弹幕被拦截达到一定次数后封号 danmuEvent.ban.listen(danmuData => { process.nextTick(_ => { if (filter(danmuData.room).checkUserIsBlocked(danmuData.hash)) return cache.cache().get('block_' + danmuData.hash, (err, data) => { if (err !== null && typeof err !== 'undefined') { log.log('封禁用户查询失败') console.log(err) data = 0 } else if (typeof data === 'undefined') { data = 0 } else { data = parseInt(data) data++ } log.log('自动封号检测' + danmuData.hash + '次数为' + data) if (data >= config.ext.autoban.block) { // 开始封号 config.rooms[danmuData.room].blockusers.push(danmuData.hash) log.log('自动封号' + danmuData.hash) configEvent.updated.emit() } else { cache.cache().set('block_' + danmuData.hash, data, 60 * 60 * 3, () => {}) } }) }) }) } ================================================ FILE: src/extensions/index.js ================================================ // / 'use strict' const path = require('path') const log = require('../utilities/log') let config = require('../../config') module.exports.init = function (callback) { Object.keys(config.ext).map(name => { log.log('加载扩展组件:' + name) require(path.join(__dirname, './', name))() }) log.log('扩展组件加载完成') callback(null) } ================================================ FILE: src/extensions/livesync/get.py ================================================ import time, sys, json from danmu import DanMuClient def out(p): sys.stdout.write(json.dumps(p) + "\n") sys.stdout.flush() dmc = DanMuClient(sys.argv[1]) if not dmc.isValid(): print('Url invalid') @dmc.danmu def danmu_fn(msg): out({'type': 'danmu', 'data': msg}) @dmc.gift def gift_fn(msg): out({'type': 'gift', 'data': msg}) @dmc.other def other_fn(msg): out({'type': 'other', 'data': msg}) dmc.start(blockThread = True) ================================================ FILE: src/extensions/livesync/index.js ================================================ 'use strict' const cp = require('child_process') const path = require('path') const danmuEvent = require('../../interfaces/Danmu') const log = require('../../utilities/log') let config = require('../../../config') const tryCatch = (fn, e) => { try { return fn() } catch (err) { return e(err) } } module.exports = function () { Object.keys(config.ext.livesync).forEach(room => { const liveConfig = config.ext.livesync[room] const ls = cp.spawn('python', [path.join(__dirname, '/get.py'), liveConfig.liveUrl], ['ignore', 'pipe', 'pipe']) ls.stdout.on('data', stdout => { const splitted = stdout.toString().split('\n') splitted.forEach(data => { if (data.trim() === '') return tryCatch(() => { const ret = JSON.parse(data) let content = '' switch (ret.type) { case 'danmu': content = `${ret.data.Content}` break case 'gift': log.log(ret.data.NickName + ' 送了一份礼物') return case 'other': log.log('弹幕直播信息:' + data) return } danmuEvent.addSingle.wait({ hash: ret.data.NickName, room, text: content, ip: '127.0.0.1', ua: 'liveSync', style: '', textStyle: '', lifeTime: '', color: '', height: '', sourceCode: '' }) }, (e) => { log.log('解析弹幕失败:' + e.toString()) }) }) }) }) } ================================================ FILE: src/extensions/weibo/index.js ================================================ // / 'use strict' const Passport = require('passport') const PassportSina = require('passport-sina') const session = require('express-session') const cookieParser = require('cookie-parser') const async = require('async') const fs = require('fs') const path = require('path') const httpEvent = require('../../interfaces/Http') const danmuEvent = require('../../interfaces/Danmu') const log = require('../../utilities/log') const utilities = require('../../utilities') const cache = require('../../libraries/cache') let config = require('../../../config') /* passport.serializeUser(function (user, callback) { callback(null, user); }); passport.deserializeUser(function (obj, callback) { callback(null, obj); }); */ Passport.use(new PassportSina(config.ext.weibo, function (accessToken, refreshToken, profile, callback) { process.nextTick(function () { return callback(null, { accessToken: accessToken, profile: profile }) }) })) module.exports = function () { httpEvent.beforeRoute.listen(app => { app.use(session({ secret: config.http.sessionKey, resave: false, saveUninitialized: true, cookie: { maxAge: 24 * 60 * 60 * 1000 // 1 day } })) app.use(Passport.initialize()) app.use(cookieParser()) app.get('/auth/sina', Passport.authenticate('sina', { session: false })) app.get('/auth/sina/callback', (req, res, next) => { Passport.authenticate('sina', { session: false }, function (err, data) { if (err !== null) { console.log(err) res.redirect('/') return } if (data === false) { console.log(arguments) res.type('html') res.end("") return } let hash = utilities.getHash(data.profile.id, data.profile.name, data.profile.created_at) cache.cache().set('weibo_' + hash, JSON.stringify({ accessToken: data.accessToken, name: data.profile.name, id: data.profile.id, nick: data.profile.screen_name }), 24 * 60 * 60, (err, data) => { err // eslint-disable-line no-unused-expressions // Do nothing // eat it }) res.cookie('weibo', hash, { maxAge: 24 * 60 * 60 * 1000 }) log.log('用户' + data.profile.id + '(' + data.profile.name + ')登录(' + hash + ')') res.redirect('/') })(req, res, next) }) app.use(function (req, res, next) { // 这里用来给req添加函数 req.getSina = function (callback) { if (typeof req.cookies.weibo !== 'undefined') { cache.cache().get('weibo_' + req.cookies.weibo, function (err, data) { if (err) { callback(err, false) } else if (typeof data === 'undefined') { callback(null, false) } else { callback(null, JSON.parse(data)) } }) } else { callback(null, false) } } next() }) // 未登录时直接跳转到新浪微博 app.get('/', (req, res, next) => { async.waterfall([ function (callback) { req.getSina(callback) }, function (data, callback) { if (data === false) { fs.readFile(path.join(__dirname, './login.html'), function (err, data) { err // eslint-disable-line no-unused-expressions res.end(data) }) } else { next() } } ]) }) app.post('/post', (req, res, next) => { req.getSina((err, data) => { if (err || data === false) { res.end('你还没有用微博登录!请刷新页面后重试!') } else { next() } }) }) danmuEvent.httpReceived.listen((req, res, danmuData) => { return new Promise((resolve, reject) => { req.getSina((err, data) => { if (data && !err) { danmuData.hash = data.nick return resolve(true) } reject(err) }) }) }) danmuEvent.transfer.listen(data => { data.data.forEach(item => `${item.text}=@${item.hash}: ${item.text}`) }) }) } ================================================ FILE: src/extensions/weibo/login.html ================================================ 新浪微博登录

新浪微博登录

你需要先使用新浪微博账号登录才可以发送弹幕。

程序无法获知你的密码,也不会主动发送微博。

你可以随时在微博管理中心 -> 我的应用处取消授权。


Powered by zsx

若弹幕无法发送,则请刷新。

在弹幕上表白、使用不文明语言有被封号的可能。

================================================ FILE: src/interfaces/Base.js ================================================ const events = require('events') const eventEmitter = new events.EventEmitter() const callbackObject = {} class Base { static get className () { return 'base' } static register (className, fieldName) { const eventName = `${className}/${fieldName}` return { eventName, once: eventEmitter.once.bind(eventEmitter, eventName), listen: eventEmitter.on.bind(eventEmitter, eventName), emit: eventEmitter.emit.bind(eventEmitter, eventName), listeners: () => eventEmitter.listeners(eventName), removeAllListeners: () => eventEmitter.removeAllListeners(eventName) } } static registerCallbackable (className, fieldName) { const eventName = `${className}/${fieldName}` if (callbackObject[eventName]) return callbackObject[eventName] let callback = () => new Promise((resolve, reject) => resolve(true)) callbackObject[eventName] = { listen: fun => { callback = fun }, wait: function (...args) { return callback(...args) // eslint-disable-line standard/no-callback-literal } } return callbackObject[eventName] } } module.exports = Base ================================================ FILE: src/interfaces/Config.js ================================================ const Base = require('./Base') const className = 'config' const updated = Base.register(className, 'updated') const blockUser = Base.register(className, 'blockUser') const unblockUser = Base.register(className, 'unblockUser') class Config extends Base { static get className () { return className } static get blockUser () { return blockUser } static get updated () { return updated } static get unblockUser () { return unblockUser } } module.exports = Config ================================================ FILE: src/interfaces/Danmu.js ================================================ const Base = require('./Base') const className = 'danmu' const addSingle = Base.registerCallbackable(className, 'addSingle') const ban = Base.register(className, 'ban') const transfer = Base.register(className, 'transfer') const removeSingle = Base.register(className, 'removeSingle') const removing = Base.register(className, 'removing') const get = Base.register(className, 'get') const httpReceived = Base.registerCallbackable(className, 'httpReceived') const search = Base.registerCallbackable(className, 'search') class Danmu extends Base { static get className () { return className } /** * 新增一条未经处理的弹幕 */ static get addSingle () { return addSingle } /** * 封禁弹幕 **/ static get ban () { return ban } /** * 收到用户刚刚发送的弹幕(通过HTTP) **/ static get httpReceived () { return httpReceived } /** * 得到格式化后的弹幕 **/ static get get () { return get } /** * 删除单条弹幕事件 * @param {object} data * @param {boolean?} blockUser false **/ static get removeSingle () { return removeSingle } /** * 正在删除事件(即准备删除但还未动手) **/ static get removing () { return removing } /** * 搜索弹幕 **/ static get search () { return search } /** * 传输弹幕到客户端 **/ static get transfer () { return transfer } } module.exports = Danmu ================================================ FILE: src/interfaces/Http.js ================================================ const Base = require('./Base') const className = 'http' const _ = require('ramda') const register = _.curry(Base.register)(className) const created = register('created') const beforeRoute = register('beforeRoute') class Http extends Base { static get className () { return className } static get created () { return created } static get beforeRoute () { return beforeRoute } } module.exports = Http ================================================ FILE: src/interfaces/Socket.js ================================================ const Base = require('./Base') const className = 'socket' const _ = require('ramda') const register = _.curry(Base.register)(className) const created = register('created') class Socket extends Base { static get className () { return className } static get created () { return created } } module.exports = Socket ================================================ FILE: src/libraries/cache/index.js ================================================ 'use strict' let cache = null const config = require('../../../config') const memoryCacheMap = new Map() module.exports = { init: function (callback) { switch (config.cache.type) { case 'memcached': cache = new (require('memcached'))(config.cache.host) break case 'aliyun': const ALY = require('aliyun-sdk') const PORT = config.cache.host.split(':')[1] const HOST = config.cache.host.split(':')[0] cache = ALY.MEMCACHED.createClient(PORT, HOST, { username: config.cache.authUser, password: config.cache.authPassword }) break default: cache = { get: (name, callback) => { callback(null, memoryCacheMap.get(name)) }, set: (name, value, date, callback) => { memoryCacheMap.set(name, value) callback(null) } } } callback(null) } } module.exports.cache = () => { return cache } ================================================ FILE: src/libraries/database/csv.js ================================================ const fs = require('fs') const path = require('path') const danmuEvent = require('../../interfaces/Danmu') const log = require('../../utilities/log') const config = require('../../../config') function formatContent (content) { return '"' + content.toString().replace(/"/g, '""') + '"' } module.exports = {} module.exports.init = function (callback) { let savePath = path.resolve(config.database.savedir) log.log('保存位置:' + savePath) callback(null) danmuEvent.get.listen(data => { let joinArray = [] joinArray.push(formatContent(Math.round(new Date().getTime() / 1000))) joinArray.push(formatContent(data.hash)) joinArray.push(formatContent(data.ip)) joinArray.push(formatContent(data.ua)) joinArray.push(formatContent(data.text)) joinArray.push('\r\n') fs.appendFile(path.resolve(savePath, data.room + '.csv'), joinArray.join(','), err => { if (err) log.log(err) }) }) danmuEvent.search.listen((data) => new Promise((resolve, reject) => { resolve('[{"user": "ERROR", "text": "Not yet supported", "publish": ""}]') })) } ================================================ FILE: src/libraries/database/index.js ================================================ const config = require('../../../config') module.exports = { init: function (callback) { require('./' + config.database.type + '.js').init(function () { callback.apply(this, arguments) }) } } ================================================ FILE: src/libraries/database/mongo.js ================================================ const mongodb = require('mongodb') const danmuEvent = require('../../interfaces/Danmu') const log = require('../../utilities/log') const config = require('../../../config') let db = null const server = new mongodb.Server(config.database.server, config.database.port, { auto_reconnect: true }) const getConnection = function (callback) { db = new mongodb.Db(config.database.db, server, { w: 1 }) db.open(err => { if (err !== null) { log.log('数据库连接错误') throw err } if (config.database.username !== '') { db.authenticate(config.database.username, config.database.password, function (err, result) { if (err !== null) { log.log('数据库验证错误') throw err } callback.apply(callback, arguments) }) } callback.apply(callback, arguments) log.log('数据库连接成功') }) // callback(null); } module.exports = { init: function (callback) { getConnection(callback) danmuEvent.get.listen(data => { let room = data.room db.collection(config.rooms[room].table).insert({ user: data.hash, text: data.text, publish: Math.round(new Date().getTime() / 1000), ip: data.ip, ua: data.ua }, (err, results) => { if (err !== null) { log.log('数据库写入出错') console.log(err) } }) }) danmuEvent.search.listen((data) => new Promise((resolve, reject) => { let room = data.room db.collection(config.rooms[room].table).find({ text: { $regex: '.*?' + pregQuote(data.key) + '.*?' } }, null, null).toArray(function (err, results) { if (err === null) { results.map(function (object) { object.id = object._id }) resolve(JSON.stringify(results)) } else { log.log('数据库搜索出错') console.error(err) reject(err) } }) })) } } function pregQuote (str, delimiter) { // discuss at: http://phpjs.org/functions/preg_quote/ // original by: booeyOH // improved by: Ates Goral (http://magnetiq.com) // improved by: Kevin van Zonneveld (http://kevin.vanzonneveld.net) // improved by: Brett Zamir (http://brett-zamir.me) // bugfixed by: Onno Marsman // example 1: preg_quote("$40"); // returns 1: '\\$40' // example 2: preg_quote("*RRRING* Hello?"); // returns 2: '\\*RRRING\\* Hello\\?' // example 3: preg_quote("\\.+*?[^]$(){}=!<>|:"); // returns 3: '\\\\\\.\\+\\*\\?\\[\\^\\]\\$\\(\\)\\{\\}\\=\\!\\<\\>\\|\\:' return String(str) .replace(new RegExp('[.\\\\+*?\\[\\^\\]$(){}=!<>|:\\' + (delimiter || '') + '-]', 'g'), '\\$&') } ================================================ FILE: src/libraries/database/mysql.js ================================================ const SECONDS_IN_DAY = 24 * 60 * 60 * 1000 const mysql = require('mysql') const async = require('async') const danmuEvent = require('../../interfaces/Danmu') const log = require('../../utilities/log') const config = require('../../../config') let pool = null let connection = null let errorCounter = 0 let firstErrorTime = new Date() const createTableSql = [ 'CREATE TABLE IF NOT EXISTS `%table%` (', 'danmu_id int(11) NOT NULL AUTO_INCREMENT,', "danmu_user varchar(255) NOT NULL DEFAULT '',", 'danmu_text text NOT NULL,', "danmu_publish int(11) NOT NULL DEFAULT '0',", "danmu_ip varchar(255) NOT NULL DEFAULT '',", 'danmu_useragent text NOT NULL,', 'PRIMARY KEY (danmu_id),', 'KEY danmu_TPISC (danmu_publish)', ') ENGINE=MyISAM DEFAULT CHARSET=utf8 AUTO_INCREMENT=1;' ].join('\n') const createDatabase = function (callbackOrig) { const asyncList = Object.keys(config.rooms) async.each(asyncList, (room, callback) => { connection.query('SELECT MAX(danmu_id) FROM `' + config.rooms[room].table + '`', function (err, rows) { if (err !== null) { log.log('Creating Table...') connection.query(createTableSql.replace(/%table%/g, config.rooms[room].table), function (err, rows) { callback(err) }) } else { callback(null) } }) }, function (err) { callbackOrig(err) }) } const dbErrorHandler = function (err) { if (err !== null) { if (err.errno !== 'ECONNRESET') { // 部分MySQL会自动超时,此时要重连但不计errorCounter if (errorCounter === 0 || new Date() - firstErrorTime >= SECONDS_IN_DAY) { firstErrorTime = new Date() errorCounter = 0 } errorCounter++ console.log(err) log.log('数据库第' + errorCounter + '次连接出错。') if (connection) { connection.release() } getConnection() } if (errorCounter >= config.database.retry) { log.log('数据库连接错误次数超过上限,程序退出。') throw err } } } const getConnection = function (callback) { let called = false pool.getConnection((err, privateConnection) => { connection = privateConnection if (err) { dbErrorHandler(err) if (!called && callback) { callback(err) called = true } } else { connection.on('error', dbErrorHandler) log.log('数据库连接正常') createDatabase(err => { if (!called && callback) { callback(err) called = true } }) } }) } module.exports = { init: function (callback) { pool = mysql.createPool({ host: config.database.server, user: config.database.username, password: config.database.password, port: config.database.port, database: config.database.db, acquireTimeout: config.database.timeout, connectionLimit: 1 // debug: true }) getConnection(callback) let keepAlive = function () { if (!connection) return connection.ping() } danmuEvent.get.listen(data => { let room = data.room connection.query('INSERT INTO `%table%` (danmu_user, danmu_text, danmu_publish, danmu_ip, danmu_useragent) VALUES (?, ?, ?, ?, ?)'.replace('%table%', config.rooms[room].table), [ data.hash, data.text, Math.round(new Date().getTime() / 1000), data.ip, data.ua ], function (err, rows) { if (err !== null) { log.log('数据库写入出错') console.log(err) } }) }) danmuEvent.search.listen((data) => new Promise((resolve, reject) => { let room = data.room connection.query('SELECT * from `%table%` where `danmu_text` LIKE ? LIMIT 20'.replace('%table%', config.rooms[room].table), [ '%' + data.key + '%' ], function (err, rows) { if (err === null) { let ret = [] ret = JSON.stringify(rows).replace(/"danmu_/g, '"') resolve(ret) } else { log.log('数据库搜索出错') console.log(err) reject(err) } }) })) getConnection() setInterval(keepAlive, config.database.timeout) // connection.on("error", dbErrorHandler); // connectDataBase(callback); } } ================================================ FILE: src/libraries/database/none.js ================================================ const log = require('../../utilities/log') module.exports = { init: function (callback) { log.log('无数据库') callback(null) } } ================================================ FILE: src/libraries/http/index.js ================================================ const fs = require('fs') const express = require('express') const errorHandler = require('errorhandler') const path = require('path') const app = express() const bodyParser = require('body-parser') const httpEvent = require('../../interfaces/Http') const log = require('../../utilities/log') let config = require('../../../config') module.exports = { init: function (callback) { app .engine('.html', require('ejs').__express) // .use(logger('dev')) .use(bodyParser.json()) .use(bodyParser.urlencoded({ extended: true })) .use(errorHandler()) .set('view engine', 'html') .set('views', path.join(__dirname, './view/')) .use('/static/bootstrap', express.static(path.join(__dirname, '../../../node_modules/bootstrap/dist/'))) .use('/static/jquery', express.static(path.join(__dirname, '../../../node_modules/jquery/dist/'))) .use('/static/angular', express.static(path.join(__dirname, '../../../node_modules/angular/'))) .use('/static/angular-ui-bootstrap', express.static(path.join(__dirname, '../../../node_modules/angular-ui-bootstrap/'))) .use(express.static(path.join(__dirname, './res/'))) httpEvent.beforeRoute.emit(app) // 处理路由 fs.readdir(path.join(__dirname, './route'), (err, files) => { if (err) { console.error(err) return } files.forEach((filename) => { require(path.join(__dirname, './route', filename))(app) }) }) let server = app.listen(config.http.port, () => { log.log(`服务器于http://127.0.0.1:${config.http.port}/成功创建`) httpEvent.created.emit(server) callback(null) }) } } ================================================ FILE: src/libraries/http/res/manage.css ================================================ /* * Base structure */ /* Move down content because we have a fixed navbar that is 50px tall */ body { padding-top: 50px; } /* * Global add-ons */ .sub-header { padding-bottom: 10px; border-bottom: 1px solid #eee; } /* * Top navigation * Hide default border to remove 1px line. */ .navbar-fixed-top { border: 0; } /* * Sidebar */ /* Hide for mobile, show later */ .sidebar { display: none; } @media (min-width: 768px) { .sidebar { position: fixed; top: 51px; bottom: 0; left: 0; z-index: 1000; display: block; padding: 20px; overflow-x: hidden; overflow-y: auto; /* Scrollable contents if viewport is shorter than content. */ background-color: #f5f5f5; border-right: 1px solid #eee; } } /* Sidebar navigation */ .nav-sidebar { margin-right: -21px; /* 20px padding + 1px border */ margin-bottom: 20px; margin-left: -20px; } .nav-sidebar > li > a { padding-right: 20px; padding-left: 20px; } .nav-sidebar > .active > a, .nav-sidebar > .active > a:hover, .nav-sidebar > .active > a:focus { color: #fff; background-color: #428bca; } /* * Main content */ .main { padding: 20px; } @media (min-width: 768px) { .main { padding-right: 40px; padding-left: 40px; } } .main .page-header { margin-top: 0; } /* * Placeholder dashboard ideas */ .placeholders { margin-bottom: 30px; text-align: center; } .placeholders h4 { margin-bottom: 0; } .placeholder { margin-bottom: 20px; } .placeholder img { display: inline-block; border-radius: 50%; } ================================================ FILE: src/libraries/http/res/manage.js ================================================ var manage = (function () { // eslint-disable-line var manage = angular.module('danmu.manage', [ // eslint-disable-line 'ui.bootstrap', 'manageControllers' ]) var manageControllers = angular.module('manageControllers', []) // eslint-disable-line var registerInit = [] // 用于初始化回调 // alternatively, register the interceptor via an anonymous factory manage.config(['$httpProvider', function ($httpProvider) { $httpProvider.interceptors.push(function ($q, $rootScope) { return { 'responseError': function (response) { $rootScope.err.code = response.status $rootScope.err.desc = response.data.error $rootScope.haveError = true return $q.reject(response) }, 'response': function (response) { $rootScope.err.code = response.status $rootScope.err.desc = '' $rootScope.haveError = false return response } } }) } ]) manageControllers.controller('MainCtrl', function ($scope, $http, $rootScope) { $scope.accordion = { closeOther: false, openInfo: true, openDanmu: false, openBlock: true, openConfig: false, openPermissions: true, openPassword: false, disableInfo: false } $scope.isLogin = false $scope.room = '' $scope.password = '' $rootScope.haveError = false $rootScope.err = { code: 200, desc: '' } $scope.initRoom = function (room) { $scope.room = room } $scope.enterRoom = function (password) { $scope.password = password for (var object in registerInit) registerInit[object].call() $scope.isLogin = true $scope.accordion.openInfo = false } $scope.buildParam = function (object) { object.room = $scope.room object.password = $scope.password return object } $http.post('/manage/room/get/', $scope.buildParam({})).success(function (data, status, headers, config) { $scope.roomList = data }) } ) manageControllers.controller('DanmuCtrl', function ($scope, $http) { $scope.danmu = {} $scope.danmu.searchKey = '' $scope.danmu.doSearch = function () { $http.post('/manage/search', $scope.buildParam({ key: $scope.danmu.searchKey })).success(function (data, status, headers, config) { $scope.danmu.result = data }) } } ) manageControllers.controller('BlockCtrl', function ($scope, $http) { $scope.block = {} $scope.block.textUser = '' $scope.block.doAdd = function () { $http.post('/manage/block/add', $scope.buildParam({ user: $scope.block.textUser })).success(function (data, status, headers, config) { $scope.block.result.push($scope.block.textUser) $scope.block.textUser = '' }) } $scope.block.checkKeyDown = function (e) { if (e.keyCode === 13) this.doAdd() } $scope.block.doRemove = function (user) { $http.post('/manage/block/remove', $scope.buildParam({ user: user })).success(function (data, status, headers, config) { $scope.block.result.splice($scope.block.result.indexOf(user), 1) }) } registerInit.push(function () { $http.post('/manage/block/get/', $scope.buildParam({})).success(function (data, status, headers, config) { $scope.block.result = data }) }) } ) manageControllers.controller('ConfigCtrl', function ($scope, $http) { $scope.config = {} $scope.config.realConfig = {} $scope.config.realConfig.replaceKeyword = '' $scope.config.realConfig.blockKeyword = '' $scope.config.realConfig.ignoreKeyword = '' $scope.config.realConfig.socketinterval = 0 $scope.config.realConfig.socketsingle = 0 $scope.config.realConfig.maxlength = 0 $scope.config.realConfig.textlength = 0 $scope.config.submitConfig = function () { try { $http.post('/manage/config/set/', $scope.buildParam($scope.config.realConfig)).success(function (data, status, headers, config) { $scope.config.realConfig = data }) } catch (e) { window.alert('正则检测出错!\n\n' + e.toString()) } } registerInit.push(function () { $http.post('/manage/config/get/', $scope.buildParam({})).success(function (data, status, headers, config) { $scope.config.realConfig = data }) }) } ) manageControllers.controller('PermissionCtrl', function ($scope, $http) { $scope.config = {} $scope.$makeClass = function (configName) { return { 'active': $scope.config[configName], 'btn-danger': !$scope.config[configName], 'btn-success': $scope.config[configName] } } $scope.$getStateText = function (configName) { return $scope.config[configName] ? '开' : '关' } $scope.$setState = function (configName) { $scope.config[configName] = !$scope.config[configName] } $scope.$submitPermissions = function () { $http.post('/manage/config/permissions/set/', $scope.buildParam($scope.config)).success(function (data, status, headers, config) { $scope.config = data }) } registerInit.push(function () { $http.post('/manage/config/permissions/get/', $scope.buildParam({})).success(function (data, status, headers, config) { $scope.config = data }) }) } ) manageControllers.controller('PasswordCtrl', function ($scope, $http) { $scope.config = { advancedpassword: '', managepassword: '', connectpassword: '' } $scope.$submitPassword = function (passwordState) { var modal = passwordState + 'password' if (!$scope.config[modal] || $scope.config[modal] === '') return if (window.confirm('确定要更新密码(类型:' + modal + ')?\n\n更新连接密码后,已经连接的客户端不受影响,新客户端将使用新密码;\n更新管理密码后,必须刷新页面才可以继续使用。')) { $http.post('/manage/config/password/set/', $scope.buildParam({ type: modal, newPassword: $scope.config[modal] })).success(function (data, status, headers, config) { if (modal === 'managepassword') { window.location.reload() } $scope.config[modal] = '' }) } } } ) return manage })() ================================================ FILE: src/libraries/http/res/realtime.js ================================================ // / var realtime = (function () { // eslint-disable-line var realtime = angular.module('danmu.realtime', [ // eslint-disable-line 'ui.bootstrap', 'realtimeControllers' ]) var realtimeControllers = angular.module('realtimeControllers', []) // eslint-disable-line var registerInit = [] // 用于初始化回调 var socket = null // alternatively, register the interceptor via an anonymous factory realtime.config(['$httpProvider', function ($httpProvider) { $httpProvider.interceptors.push(function ($q, $rootScope) { return { 'responseError': function (response) { $rootScope.err.code = response.status $rootScope.err.desc = response.data.error $rootScope.haveError = true return $q.reject(response) }, 'response': function (response) { $rootScope.err.code = response.status $rootScope.err.desc = '' $rootScope.haveError = false return response } } }) } ]) realtimeControllers.controller('MainCtrl', function ($scope, $http, $rootScope) { $scope.accordion = { openInfo: true } $scope.isLogin = false $scope.connectToServer = false $scope.room = '' $scope.password = '' $scope.danmus = [] $scope.config = null $rootScope.haveError = false $rootScope.err = { code: 200, desc: '' } $scope.initRoom = function (room) { $scope.room = room } $scope.enterRoom = function (password) { $scope.password = password for (var object in registerInit) registerInit[object].call() $scope.isLogin = true $scope.accordion.openInfo = false } $scope.buildParam = function (object) { object.room = $scope.room object.password = $scope.password return object } $scope.deleteDanmu = function ($index, id, blockHash) { $http.post('/manage/danmu/delete/', $scope.buildParam({ id: id, hash: blockHash })).success(function (data, status, headers, config) { if ($scope.danmus[$index]) { if ($scope.danmus[$index].id === id) { $scope.danmus[$index].lifeTime = 0 } } }) } $http.post('/manage/room/get/', $scope.buildParam({})).success(function (data, status, headers, config) { $scope.roomList = data }) registerInit.push(function () { $http.post('/manage/config/password/get/', $scope.buildParam({})).success(function (data, status, headers, config) { $scope.config = data socket = window.io(window.location.origin) socket.emit('password', { password: $scope.config.connectpassword, room: $scope.room, info: { version: window.serverVersion } }) socket.on('connected', function () { $scope.connectToServer = true }) socket.on('danmu', function (data) { data.data.forEach(function (value) { value.socketId = value.id + '-' + socket.id value.lifeTime = parseInt(value.lifeTime) $scope.danmus.push(value) }) $scope.$apply() }) setInterval(function () { var isRemoved = false $scope.danmus.forEach(function (value, key) { value.lifeTime -= 60 if (value.lifeTime <= 0) { $scope.danmus.splice(key, 1) isRemoved = true } }) if (isRemoved) { $scope.$apply() } }, 1000) // auto remove 60fps }) }) } ) return realtime })() ================================================ FILE: src/libraries/http/route/index.js ================================================ const config = require('../../../../config') module.exports = function (app) { // Initialize Hostname Map const hostnameMap = new Map() Object.keys(config.rooms).forEach(room => config.rooms[room].hostname.forEach(value => hostnameMap.set(value, room))) function getRoom (hostname) { return (hostnameMap.has(hostname)) ? hostnameMap.get(hostname) : null } function renderIndex (advanced, room) { const permission = config.rooms[room].permissions return { config, advanced, room, permission } } app.route('/*').all((req, res, next) => { res.append('Server', 'zsx\'s Danmu Server') req.room = getRoom(req.hostname) for (let item in config.http.headers) { res.append(item, config.http.headers[item]) } if (req.room === null) { res.status(403) res.end('403 Forbidden') } next() }) app.get('/', (req, res) => { res.render('index', renderIndex(false, req.room)) }) app.get('/advanced', (req, res) => { res.render('index', renderIndex(true, req.room)) }) app.post((req, res, next) => { res.header('Content-Type', 'text/html; charset=utf-8') next() }) } ================================================ FILE: src/libraries/http/route/manage.js ================================================ const config = require('../../../../config') module.exports = function (app) { app.get('/manage', function (req, res) { res.render('manage', { config }) }) // 总身份验证 app.post('/manage/*', (req, res, next) => { if (/room\/get/.test(req.url)) { // 如果是房间下发则不验证身份 return next() } let room = req.body.room if (!config.rooms[room]) { res.status(404) return res.end('{"error": "房间错误"}') } if (config.rooms[room].managepassword !== req.body.password) { res.status(403) return res.end('{"error": "密码错误"}') } return next() }) } ================================================ FILE: src/libraries/http/route/manageBlock.js ================================================ const configEvent = require('../../../interfaces/Config') const log = require('../../../utilities/log') const config = require('../../../../config') module.exports = function (app) { app.post('/manage/block/add/', (req, res) => { let room = req.body.room configEvent.blockUser.emit(room, req.body.user) res.end('{"error": "封禁用户成功"}') }) app.post('/manage/block/get/', (req, res) => { let room = req.body.room log.log('请求被封禁用户成功') res.end(JSON.stringify(config.rooms[room].blockusers)) }) app.post('/manage/block/remove/', (req, res) => { let room = req.body.room configEvent.unblockUser.emit(room, req.body.user) res.end('{"error": "移除封禁成功"}') }) } ================================================ FILE: src/libraries/http/route/manageConfig.js ================================================ const configEvent = require('../../../interfaces/Config') const utilities = require('../../../utilities') const log = require('../../../utilities/log') const config = require('../../../../config') module.exports = function (app) { app.post('/manage/config/set/', (req, res) => { const room = req.body.room || '' config.rooms[room].keyword.replacement = new RegExp(req.body.replaceKeyword, 'ig') config.rooms[room].keyword.block = new RegExp(req.body.blockKeyword, 'ig') config.rooms[room].keyword.ignore = new RegExp(req.body.ignoreKeyword, 'ig') config.rooms[room].maxlength = req.body.maxlength config.rooms[room].textlength = req.body.textlength config.rooms[room].openstate = req.body.openstate config.websocket.interval = req.body.socketinterval config.websocket.singlesize = req.body.socketsingle const newConfig = JSON.stringify(utilities.buildConfigToArray(room)) log.log('收到配置信息:' + newConfig) configEvent.updated.emit() return res.end(newConfig) }) app.post('/manage/config/permissions/set/', (req, res) => { const room = req.body.room || '' Object.keys(config.rooms[room].permissions).map(item => { config.rooms[room].permissions[item] = !!req.body[item] }) let newConfig = JSON.stringify(config.rooms[room].permissions) log.log('收到权限配置信息:' + newConfig) configEvent.updated.emit() return res.end(newConfig) }) app.post('/manage/config/password/set/', (req, res) => { const type = req.body.type || '' const password = req.body.newPassword || '' const room = req.body.room || '' if (config.rooms[room][type]) { config.rooms[room][type] = password log.log('房间(' + room + ')的' + type + '已更新为' + password) } configEvent.updated.emit() return res.end() }) app.post('/manage/config/permissions/get/', (req, res) => { const room = req.body.room || '' log.log('已将权限配置向管理页面下发') return res.end(JSON.stringify(config.rooms[room].permissions)) }) app.post('/manage/config/get/', (req, res) => { const room = req.body.room || '' log.log('已将配置向管理页面下发') return res.end(JSON.stringify(utilities.buildConfigToArray(room))) }) app.post('/manage/config/password/get/', (req, res) => { const room = req.body.room || '' log.log('已将密码向管理页面下发') return res.end(JSON.stringify({ connectpassword: config.rooms[room].connectpassword })) }) } ================================================ FILE: src/libraries/http/route/manageDanmu.js ================================================ const danmuEvent = require('../../../interfaces/Danmu') module.exports = function (app) { app.post('/manage/danmu/delete/', (req, res) => { const data = {} data.hash = req.body.hash || '' data.id = req.body.id || 0 data.room = req.body.room || '' if (data.id === 0) { res.end({}) return } danmuEvent.removeSingle.emit(data, data.hash !== '') return res.end('{"error": "删除弹幕成功"}') }) } ================================================ FILE: src/libraries/http/route/manageRoom.js ================================================ const log = require('../../../utilities/log') const config = require('../../../../config') module.exports = function (app) { app.post('/manage/room/get/', (req, res) => { const ret = [] Object.keys(config.rooms).map(room => { ret.push({ id: room, display: config.rooms[room].display }) }) log.log('已把房间信息向管理页面下发') return res.end(JSON.stringify(ret)) }) } ================================================ FILE: src/libraries/http/route/manageSearch.js ================================================ const log = require('../../../utilities/log') const danmuEvent = require('../../../interfaces/Danmu') module.exports = function (app) { app.post('/manage/search', function (req, res) { const room = req.body.room log.log('尝试搜索' + req.body.key) danmuEvent.search.wait({ key: req.body.key, room }).then(data => { log.log('搜索' + req.body.key + '成功') res.end(data) }).catch(data => { res.end(`[{"user": "ERROR", "text": "${JSON.parse(data.toString())}", "publish": ""}]`) }) }) } ================================================ FILE: src/libraries/http/route/post.js ================================================ const utilities = require('../../../utilities') const danmuEvent = require('../../../interfaces/Danmu') const config = require('../../../../config') module.exports = function (app) { app.post('/post', (req, res) => { const room = req.room const roomConfig = config.rooms[room] const ip = roomConfig.cdn ? req.ip : (req.get('X-Real-IP') || req.get('X-Forwarded-For') || req.ip) const hash = utilities.getHash(ip, req.headers['user-agent'], req.body.hash) const danmuData = { hash, room, text: req.body.text, ip, ua: req.headers['user-agent'], style: '', textStyle: '', lifeTime: '', color: '', height: '', sourceCode: '' } if (req.body.text === '') { return res.end('弹幕不能为空') } danmuEvent.httpReceived.wait(req, res, danmuData) .then(() => danmuEvent.addSingle.wait(danmuData, req.body, { password: req.body.password, isAdvanced: req.body.type === 'advanced' })) .then(() => res.end('发送成功!')) .catch(e => res.end(e.toString())) }) } ================================================ FILE: src/libraries/http/route/realtime.js ================================================ const config = require('../../../../config') module.exports = function (app) { app.get('/realtime', (req, res) => { res.render('realtime', { config, version: global.version }) }) } ================================================ FILE: src/libraries/http/view/index.html ================================================ 发送弹幕

发送你的弹幕吧!

 

 

 

 

 

 

 


Powered by zsx

若弹幕无法发送,则请刷新。

在弹幕上表白、使用不文明语言有被封号的可能。

================================================ FILE: src/libraries/http/view/manage.html ================================================ 系统管理
错误{{err.code}}:{{err.desc}};建议刷新页面。
务必先填入房间信息再进行管理。 你选择了{{room}}房间


ID 用户 弹幕
{{item.id}} {{item.user}} {{item.text}}


关闭后不再接收任何新弹幕
此权限要求弹幕样式开关一并打开才可使用。非常危险,可能导致客户端出现莫名错误,对非信任用户不要打开!
================================================ FILE: src/libraries/http/view/realtime.html ================================================ 实时弹幕
错误{{err.code}}:{{err.desc}};建议刷新页面。
务必先填入房间信息再进行管理。 你选择了{{room}}房间
================================================ FILE: src/libraries/socket/index.js ================================================ const httpEvent = require('../../interfaces/Http') const socketEvent = require('../../interfaces/Socket') const danmuEvent = require('../../interfaces/Danmu') const log = require('../../utilities/log') const config = require('../../../config') let inProcessRandomNumber = Math.random() let io = null module.exports = { init: function (callback) { // 删除弹幕 danmuEvent.removing.listen(data => { Object.keys(data).forEach(room => { io.to(room).emit('delete', data[room]) }) }) // 推送弹幕 danmuEvent.transfer.listen(data => { io.to(data.room).emit('danmu', data) }) // 当服务器创建后,绑定WebSocket httpEvent.created.listen(app => { io = require('socket.io')(app) io.on('connection', socket => { // 向客户端推送密码请求 socket.emit('init', 'Require Password.') socket.on('password', data => { let room = data.room if (!config.rooms[room]) { socket.emit('init', 'Room Not Found') log.log(`${socket.id}试图加入未定义房间`) return false } if (data.password !== config.rooms[room].connectpassword) { socket.emit('init', 'Password error') return false } if (!data.info) { log.log('该版本弹幕客户端过老,请更新弹幕客户端。') return false } log.log(`客户端 ${socket.id}(${data.info.version}) in ${socket.conn.remoteAddress} 连接于 ${room}`) socket.join(room) socket.emit('connected', { version, // eslint-disable-line randomNumber: inProcessRandomNumber // 用于给客户端检测服务器是重启还是断线 }) }) }) socketEvent.created.emit(io) }) callback(null) } } ================================================ FILE: src/libraries/transfer/index.js ================================================ const configEvent = require('../../interfaces/Config') const danmuEvent = require('../../interfaces/Danmu') const filter = require('../../utilities/filter') const log = require('../../utilities/log') const utilities = require('../../utilities') let danmuQueue = {} let danmuKeys = [] const config = require('../../../config') let danmuId = 0 module.exports = { init: function (callback) { callback(null) } } // 更新配置 configEvent.updated.listen(data => { clearAllTimeval() initDanmuQueue() startAllTimeval() }) // 待推送弹幕 danmuEvent.get.listen(data => { // 过老弹幕没有意义,直接从队列头出队列 while (danmuQueue[data.room].queue.length > config.rooms[data.room].maxlength) { danmuQueue[data.room].queue.shift() } if (data.lifeTime === '') { data.lifeTime = utilities.parseLifeTime(data) } log.log(`房间${data.room}得到弹幕(${data.hash}):${data.text}`) danmuQueue[data.room].queue.push(data) }) let initDanmuQueue = function () { danmuQueue = {} danmuKeys = Object.keys(config.rooms) danmuKeys.forEach(room => { danmuQueue[room] = { queue: [], timeval: null } } ) } let startAllTimeval = function () { danmuKeys.forEach(room => { if (config.rooms[room].permissions.send) { danmuQueue[room].timeval = initTimeval(room) log.log(`创建(${room})定时器 - ${config.websocket.interval} ms.`) } else { log.log(`${room} 房间弹幕已关闭,不创建定时器。`) } }) } let clearAllTimeval = function () { danmuKeys.forEach(room => { log.log(`清理(${room})定时器`) clearInterval(danmuQueue[room].timeval) }) } let initTimeval = function (room) { return setInterval(() => { // 定时推送 let ret = [] if (danmuQueue[room].queue.length === 0) return while (ret.length < config.websocket.singlesize && danmuQueue[room].queue.length > 0) { let object = danmuQueue[room].queue.pop() // 只在传输时才需要进行替换 object.text = filter(room).replaceKeyword(object.text) object.id = ++danmuId ret.push(object) } log.log(`推送${ret.length}条弹幕到${room},剩余${danmuQueue[room].queue.length}条。`) danmuEvent.transfer.emit({ room: room, data: ret }) }, config.websocket.interval) } ================================================ FILE: src/utilities/filter.js ================================================ const configEvent = require('../interfaces/Config') const _ = require('ramda') const config = require('../../config') const cachedFilters = {} /** * 检测用户是否被封禁 */ const checkUserIsBlocked = _.curry((blockUsers, hash) => { return (blockUsers.indexOf(hash)) > -1 }) /** * 检测文字是否和谐 */ const validateText = _.curry((ignoreRegEx, checkRegEx, str) => { checkRegEx.lastIndex = 0 const testStr = str.replace(ignoreRegEx, '') return !checkRegEx.test(testStr) }) /** * 替换关键字 */ const replaceKeyword = _.curry((regex, str) => str.replace(regex, '***')) function initialize (roomName, forceUpdate) { if (cachedFilters[roomName] && !forceUpdate) { return cachedFilters[roomName] } const room = config.rooms[roomName] if (typeof room.keyword.block === 'string') { console.error('请升级你的配置,将所有字符串类型关键词转换为正则类型关键词。') throw new Error('Init RegExp Error') } const ret = { checkUserIsBlocked: checkUserIsBlocked(room.blockusers), validateText: validateText(room.keyword.ignore)(room.keyword.block), replaceKeyword: replaceKeyword(room.keyword.replacement) } cachedFilters[roomName] = null // Release Memory cachedFilters[roomName] = ret return ret }; // 正则缓存更新 configEvent.updated.listen(() => { Object.keys(config.rooms).forEach(room => initialize(room, true)) }) module.exports = initialize ================================================ FILE: src/utilities/index.js ================================================ 'use strict' const crypto = require('crypto') const config = require('../../config') /** * 生成MD5 */ const md5 = text => crypto.createHash('md5').update(text).digest('hex') /** * 计算Hash */ const getHash = (ip, userAgent, hashCode) => md5(`IP=${ip}\nUA=${userAgent}\nHC=${hashCode}`) /** * 获取一个可读的当前时间 */ const getTime = () => { const d = new Date() return d.getFullYear() + '-' + (d.getMonth() + 1) + '-' + d.getDate() + ' ' + d.getHours() + ':' + d.getMinutes() + ':' + d.getSeconds() + '.' + d.getMilliseconds() } /** * 把配置格式化为数组 */ const buildConfigToArray = room => { return { replaceKeyword: config.rooms[room].keyword.replacement.source, blockKeyword: config.rooms[room].keyword.block.source, ignoreKeyword: config.rooms[room].keyword.ignore.source, maxlength: config.rooms[room].maxlength, textlength: config.rooms[room].textlength, socketinterval: config.websocket.interval, socketsingle: config.websocket.singlesize } } /** * 计算弹幕生存时间 */ const parseLifeTime = data => { const imageMatches = data.text.match(config.rooms[data.room].image.regex) const imageLength = imageMatches === null ? 0 : imageMatches.length return (Math.trunc(data.text.length / 10)) * 240 + config.rooms[data.room].image.lifetime * imageLength } // 全局工具 module.exports = { md5, getHash, getTime, buildConfigToArray, parseLifeTime } ================================================ FILE: src/utilities/log.js ================================================ const utilities = require('./') module.exports = { log: function (text) { console.log('[' + utilities.getTime() + '] ' + text) } }