diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..e2797e5 --- /dev/null +++ b/.gitignore @@ -0,0 +1,25 @@ +# Created by .ignore support plugin (hsz.mobi) +### Example user template template +### Example user template + +# IntelliJ project files +.idea +.idea/ +index1.php +user/ +record/ +temp/ +tmp/ +*.iml +out +gen +/vendor/ +/configs/ +/tests/ +config +*.log +Traits/ +README1.md +conf/user.conf +/conf/user.conf +/log/ diff --git a/CHANGELOG.md b/CHANGELOG.md new file mode 100644 index 0000000..628437f --- /dev/null +++ b/CHANGELOG.md @@ -0,0 +1,13 @@ +# Release Notes +# 本项目Log + +## v0.0.1.190713 alpha (2019-07-13) + +### Added +- Initialization + +### Changed +- Initialization + +### Fixed +- Initialization diff --git a/DOC.md b/DOC.md new file mode 100644 index 0000000..725c5fa --- /dev/null +++ b/DOC.md @@ -0,0 +1,224 @@ + +

+ +

+ + +

+ + +# BiliHelper + +B 站直播实用脚本 + + + +## 功能组件 + +|plugin |version |description | +|--------------------|--------------------|--------------------| +|Daily |19.07.13 |每日背包奖励 | +|GiftSend |19.07.13 |自动清空过期礼物 | +|Heart |19.07.13 |双端直播间心跳 | +|Login |19.07.13 |帐号登录组件 | +|Silver |19.07.13 |自动领宝箱 | +|Task |19.07.13 |每日任务 | +|GiftHeart |19.07.13 |心跳礼物 | +|Silver2Coin |19.07.13 |银瓜子换硬币 | +|MaterialObject |19.07.13 |实物抽奖 | +|GroupSignIn |19.07.13 |应援团签到 | +|Storm |19.07.13 |节奏风暴 | +|Notice |19.07.13 |Server酱 | +|RaffleHandler |19.07.13 |统一活动抽奖 | +|MasterSite |19.07.13 |主站(观看、分享、投币)| +|Guard |19.07.13 |舰长上船亲密度 | + +## 打赏赞助 + +![](https://i.loli.net/2018/04/07/5ac79ff8c2900.png) + + +## 未完成功能 + +|待续 | +|-----------| +|添加多用户 | +|待添加 | + +## 环境依赖 + +|Requirement | +|--------------------| +|PHP >=7.0 | +|php_curl | +|php_sockets | +|php_openssl | +|php_json | +|php_zlib | +|php_mbstring | +|待添加 | + +通常使用 `composer` 工具会自动检测上述依赖问题。 + +## Composer +* 项目 `composer.lock` 基于阿里云Composer镜像生成 ++ 阿里云(推荐) +``` +# 使用帮助 +https://developer.aliyun.com/composer +# 使用命令 +> composer config -g repo.packagist composer https://mirrors.aliyun.com/composer/ +``` +* 腾讯云(备用) +``` +# 使用帮助 +https://mirrors.cloud.tencent.com/composer/ +# 使用命令 +composer config -g repos.packagist composer https://mirrors.cloud.tencent.com/composer/ +``` + +## 打赏赞助 + +![](https://i.loli.net/2019/07/13/5d2963e5cc1eb22973.png) + +> 待添加 + +## 使用指南 + + 1. 下载(克隆)项目代码,初始化项目 +``` +$ git clone https://github.com/lkeme/BiliHelper-personal.git +$ cd BiliHelper/conf +$ cp user.conf.example user.conf +``` + 2. 使用 [composer](https://getcomposer.org/download/) 工具进行安装 +``` +$ composer install +``` + 3. 按照说明修改配置文件 `user.conf`,只需填写帐号密码即可 + 4. 运行测试 +``` +$ php index.php +``` +> 以下是`多开方案`,单个账户可以无视 + 5. 复制一份example配置文件,修改账号密码即可 + ``` + $ php index.php example.conf + ``` + 6. 请保证配置文件存在,否则默认加载`user.conf`配置文件 + +

+ + +## 升级指南 + + 1. 进入项目目录 +``` +$ cd BiliHelper-personal +``` + 2. 拉取最新代码 +``` +$ git pull +``` + 3. 更新依赖库 +``` +$ composer install +``` + 4. 如果使用 systemd 等,需要重启服务 +``` +$ systemctl restart bilibili +``` + +## 部署指南 + +如果你将 BiliHelper-personal 部署到线上服务器时,则需要配置一个进程监控器来监测 `php index.php` 命令,在它意外退出时自动重启。 + +通常可以使用以下的方式 + - systemd (推荐) + - Supervisor + - screen (自用) + - nohup + +## systemd 脚本 + +``` +# /usr/lib/systemd/system/bilibili.service + +[Unit] +Description=Bili Helper Manager +Documentation=https://github.com/lkeme/BiliHelper-personal +After=network.target + +[Service] +ExecStart=/usr/bin/php /path/to/your/BiliHelper-personal/index.php +Restart=always + +[Install] +WantedBy=multi-user.target +``` + +## Supervisor 配置 + +``` +[program:bilibili] +process_name=%(program_name)s +command=php /path/to/your/BiliHelper-personal/index.php +autostart=true +autorestart=true +redirect_stderr=true +stdout_logfile=/tmp/bilibili.log +``` + +## 报错通知问题 + +脚本出现 error 级别的报错,会调用通知地址进行提醒,这里推荐两个服务 + +|服务|官网| +|---|---| +|Server酱|https://sc.ftqq.com/| +|TelegramBot|https://core.telegram.org/bots/api| + +示范如下 +``` +# Server酱 +# 自行替换 +APP_CALLBACK="https://sc.ftqq.com/.send?text={message}" + +# TelegramBot +# 自行替换 +APP_CALLBACK="https://api.telegram.org/bot/sendMessage?chat_id=&text={message}" +``` + +`{message}` 部分会自动替换成错误信息,接口采用 get 方式发送 + + +## 直播间 ID 问题 + +`user.conf` 文件中有个 `ROOM_ID` 配置,填写此项可以清空临过期礼物给指定直播间。 + +通常可以在直播间页面的 url 获取到它 +``` +http://live.bilibili.com/9522051 +``` + +所有直播间号码小于 1000 的直播间为短号,该脚本在每次启动会自动修正,无需关心, + +## 相关 + + > 本项目基于[BilibiliHelper](https://github.com/metowolf/BilibiliHelper)项目 + + > 基于父项目的架构开发,在此感谢父项目的开发 + + > 保留父项目没必要修改的信息,另外欢迎重构(Haha) + +## License 许可证 + +BiliHelper is under the MIT license. + +本项目基于 MIT 协议发布,并增加了 SATA 协议。 + +当你使用了使用 SATA 的开源软件或文档的时候,在遵守基础许可证的前提下,你必须马不停蹄地给你所使用的开源项目 “点赞” ,比如在 GitHub 上 star,然后你必须感谢这个帮助了你的开源项目的作者,作者信息可以在许可证头部的版权声明部分找到。 + +本项目的所有代码文件、配置项,除另有说明外,均基于上述介绍的协议发布,具体请看分支下的 LICENSE。 + +此处的文字仅用于说明,条款以 LICENSE 文件中的内容为准。 diff --git a/README.md b/README.md new file mode 100644 index 0000000..c8b5bcf --- /dev/null +++ b/README.md @@ -0,0 +1,24 @@ +# BiliHelper +B 站直播实用脚本 + +## 公告 + +Currently for Personal Edition **0.0.1.190713 alpha** + +## 文档 + +* [使用文档 / DOC.md](./DOC.md) +* [更新日志 / CHANGELOG.md](./CHANGELOG.md) + +## 交流 + +Group: [55308141](https://jq.qq.com/?_wv=1027&k=5AIDaJg) + +## 打赏 + +![](https://i.loli.net/2019/07/13/5d2963e5cc1eb22973.png) + +## 效果 + +![](https://i.loli.net/2019/07/13/5d296961a4bae41364.png) + diff --git a/composer.json b/composer.json new file mode 100644 index 0000000..f8e1f92 --- /dev/null +++ b/composer.json @@ -0,0 +1,31 @@ +{ + "name": "lkeme/bilihelper", + "description": "B 站自动领瓜子、直播助手、直播挂机脚本、主站助手 - PHP 版(Personal)", + "type": "project", + "require": { + "php": ">=7.0.0", + "ext-curl": "*", + "ext-openssl": "*", + "ext-sockets": "*", + "ext-json": "*", + "ext-zlib": "*", + "ext-mbstring": "*", + "vlucas/phpdotenv": "^2.4", + "monolog/monolog": "^1.23", + "bramus/monolog-colored-line-formatter": "^2.0", + "clue/socket-raw": "1.4" + }, + "license": "MIT", + "authors": [ + { + "name": "Lkeme", + "email": "Useri@live.cn", + "homepage": "https://mudew.com" + } + ], + "autoload": { + "psr-4": { + "lkeme\\BiliHelper\\": "src/" + } + } +} diff --git a/composer.lock b/composer.lock new file mode 100644 index 0000000..db5e915 --- /dev/null +++ b/composer.lock @@ -0,0 +1,439 @@ +{ + "_readme": [ + "This file locks the dependencies of your project to a known state", + "Read more about it at https://getcomposer.org/doc/01-basic-usage.md#installing-dependencies", + "This file is @generated automatically" + ], + "content-hash": "6fca2d61232110e041d8cb569c30d02f", + "packages": [ + { + "name": "bramus/ansi-php", + "version": "3.0.2", + "source": { + "type": "git", + "url": "https://github.com/bramus/ansi-php.git", + "reference": "79d30c30651b0c6f23cf85503c779c72ac74ab8a" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/bramus/ansi-php/zipball/79d30c30651b0c6f23cf85503c779c72ac74ab8a", + "reference": "79d30c30651b0c6f23cf85503c779c72ac74ab8a", + "shasum": "", + "mirrors": [ + { + "url": "https://mirrors.aliyun.com/composer/dists/%package%/%reference%.%type%", + "preferred": true + } + ] + }, + "require": { + "php": ">=5.4.0" + }, + "require-dev": { + "phpunit/phpunit": "~4.0" + }, + "type": "library", + "autoload": { + "psr-4": { + "Bramus\\Ansi\\": "src/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Bramus Van Damme", + "email": "bramus@bram.us", + "homepage": "https://www.bram.us/" + } + ], + "description": "ANSI Control Functions and ANSI Control Sequences (Colors, Erasing, etc.) for PHP CLI Apps", + "time": "2019-02-12T15:05:30+00:00" + }, + { + "name": "bramus/monolog-colored-line-formatter", + "version": "2.0.3", + "source": { + "type": "git", + "url": "https://github.com/bramus/monolog-colored-line-formatter.git", + "reference": "6bff15eee00afe2690642535b0f1541f10852c2b" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/bramus/monolog-colored-line-formatter/zipball/6bff15eee00afe2690642535b0f1541f10852c2b", + "reference": "6bff15eee00afe2690642535b0f1541f10852c2b", + "shasum": "", + "mirrors": [ + { + "url": "https://mirrors.aliyun.com/composer/dists/%package%/%reference%.%type%", + "preferred": true + } + ] + }, + "require": { + "bramus/ansi-php": "~3.0", + "php": ">=5.4.0" + }, + "require-dev": { + "monolog/monolog": "~1.0", + "phpunit/phpunit": "~4.0" + }, + "type": "library", + "autoload": { + "psr-4": { + "Bramus\\Monolog\\": "src/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Bramus Van Damme", + "email": "bramus@bram.us", + "homepage": "https://www.bram.us/" + } + ], + "description": "Colored Line Formatter for Monolog", + "time": "2015-01-07T22:12:35+00:00" + }, + { + "name": "clue/socket-raw", + "version": "v1.4.0", + "source": { + "type": "git", + "url": "https://github.com/clue/php-socket-raw.git", + "reference": "2f6654445233407900c9a804490cecd8e4f2ae86" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/clue/php-socket-raw/zipball/2f6654445233407900c9a804490cecd8e4f2ae86", + "reference": "2f6654445233407900c9a804490cecd8e4f2ae86", + "shasum": "", + "mirrors": [ + { + "url": "https://mirrors.aliyun.com/composer/dists/%package%/%reference%.%type%", + "preferred": true + } + ] + }, + "require": { + "ext-sockets": "*", + "php": ">=5.3" + }, + "require-dev": { + "phpunit/phpunit": "^7.0 || ^6.0 || ^5.2 || ^4.8.35" + }, + "type": "library", + "autoload": { + "psr-4": { + "Socket\\Raw\\": "src" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Christian Lück", + "email": "christian@lueck.tv" + } + ], + "description": "Simple and lightweight OOP wrapper for PHP's low level sockets extension (ext-sockets)", + "homepage": "https://github.com/clue/php-socket-raw", + "keywords": [ + "Socket", + "client", + "datagram", + "dgram", + "icmp", + "ipv6", + "server", + "stream", + "tcp", + "udg", + "udp", + "unix" + ], + "time": "2019-01-22T11:08:01+00:00" + }, + { + "name": "monolog/monolog", + "version": "1.24.0", + "source": { + "type": "git", + "url": "https://github.com/Seldaek/monolog.git", + "reference": "bfc9ebb28f97e7a24c45bdc3f0ff482e47bb0266" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/Seldaek/monolog/zipball/bfc9ebb28f97e7a24c45bdc3f0ff482e47bb0266", + "reference": "bfc9ebb28f97e7a24c45bdc3f0ff482e47bb0266", + "shasum": "", + "mirrors": [ + { + "url": "https://mirrors.aliyun.com/composer/dists/%package%/%reference%.%type%", + "preferred": true + } + ] + }, + "require": { + "php": ">=5.3.0", + "psr/log": "~1.0" + }, + "provide": { + "psr/log-implementation": "1.0.0" + }, + "require-dev": { + "aws/aws-sdk-php": "^2.4.9 || ^3.0", + "doctrine/couchdb": "~1.0@dev", + "graylog2/gelf-php": "~1.0", + "jakub-onderka/php-parallel-lint": "0.9", + "php-amqplib/php-amqplib": "~2.4", + "php-console/php-console": "^3.1.3", + "phpunit/phpunit": "~4.5", + "phpunit/phpunit-mock-objects": "2.3.0", + "ruflin/elastica": ">=0.90 <3.0", + "sentry/sentry": "^0.13", + "swiftmailer/swiftmailer": "^5.3|^6.0" + }, + "suggest": { + "aws/aws-sdk-php": "Allow sending log messages to AWS services like DynamoDB", + "doctrine/couchdb": "Allow sending log messages to a CouchDB server", + "ext-amqp": "Allow sending log messages to an AMQP server (1.0+ required)", + "ext-mongo": "Allow sending log messages to a MongoDB server", + "graylog2/gelf-php": "Allow sending log messages to a GrayLog2 server", + "mongodb/mongodb": "Allow sending log messages to a MongoDB server via PHP Driver", + "php-amqplib/php-amqplib": "Allow sending log messages to an AMQP server using php-amqplib", + "php-console/php-console": "Allow sending log messages to Google Chrome", + "rollbar/rollbar": "Allow sending log messages to Rollbar", + "ruflin/elastica": "Allow sending log messages to an Elastic Search server", + "sentry/sentry": "Allow sending log messages to a Sentry server" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "2.0.x-dev" + } + }, + "autoload": { + "psr-4": { + "Monolog\\": "src/Monolog" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Jordi Boggiano", + "email": "j.boggiano@seld.be", + "homepage": "http://seld.be" + } + ], + "description": "Sends your logs to files, sockets, inboxes, databases and various web services", + "homepage": "http://github.com/Seldaek/monolog", + "keywords": [ + "log", + "logging", + "psr-3" + ], + "time": "2018-11-05T09:00:11+00:00" + }, + { + "name": "psr/log", + "version": "1.1.0", + "source": { + "type": "git", + "url": "https://github.com/php-fig/log.git", + "reference": "6c001f1daafa3a3ac1d8ff69ee4db8e799a654dd" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/php-fig/log/zipball/6c001f1daafa3a3ac1d8ff69ee4db8e799a654dd", + "reference": "6c001f1daafa3a3ac1d8ff69ee4db8e799a654dd", + "shasum": "", + "mirrors": [ + { + "url": "https://mirrors.aliyun.com/composer/dists/%package%/%reference%.%type%", + "preferred": true + } + ] + }, + "require": { + "php": ">=5.3.0" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "1.0.x-dev" + } + }, + "autoload": { + "psr-4": { + "Psr\\Log\\": "Psr/Log/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "PHP-FIG", + "homepage": "http://www.php-fig.org/" + } + ], + "description": "Common interface for logging libraries", + "homepage": "https://github.com/php-fig/log", + "keywords": [ + "log", + "psr", + "psr-3" + ], + "time": "2018-11-20T15:27:04+00:00" + }, + { + "name": "symfony/polyfill-ctype", + "version": "v1.11.0", + "source": { + "type": "git", + "url": "https://github.com/symfony/polyfill-ctype.git", + "reference": "82ebae02209c21113908c229e9883c419720738a" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/symfony/polyfill-ctype/zipball/82ebae02209c21113908c229e9883c419720738a", + "reference": "82ebae02209c21113908c229e9883c419720738a", + "shasum": "", + "mirrors": [ + { + "url": "https://mirrors.aliyun.com/composer/dists/%package%/%reference%.%type%", + "preferred": true + } + ] + }, + "require": { + "php": ">=5.3.3" + }, + "suggest": { + "ext-ctype": "For best performance" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "1.11-dev" + } + }, + "autoload": { + "psr-4": { + "Symfony\\Polyfill\\Ctype\\": "" + }, + "files": [ + "bootstrap.php" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Symfony Community", + "homepage": "https://symfony.com/contributors" + }, + { + "name": "Gert de Pagter", + "email": "BackEndTea@gmail.com" + } + ], + "description": "Symfony polyfill for ctype functions", + "homepage": "https://symfony.com", + "keywords": [ + "compatibility", + "ctype", + "polyfill", + "portable" + ], + "time": "2019-02-06T07:57:58+00:00" + }, + { + "name": "vlucas/phpdotenv", + "version": "v2.6.1", + "source": { + "type": "git", + "url": "https://github.com/vlucas/phpdotenv.git", + "reference": "2a7dcf7e3e02dc5e701004e51a6f304b713107d5" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/vlucas/phpdotenv/zipball/2a7dcf7e3e02dc5e701004e51a6f304b713107d5", + "reference": "2a7dcf7e3e02dc5e701004e51a6f304b713107d5", + "shasum": "", + "mirrors": [ + { + "url": "https://mirrors.aliyun.com/composer/dists/%package%/%reference%.%type%", + "preferred": true + } + ] + }, + "require": { + "php": ">=5.3.9", + "symfony/polyfill-ctype": "^1.9" + }, + "require-dev": { + "phpunit/phpunit": "^4.8.35 || ^5.0" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "2.6-dev" + } + }, + "autoload": { + "psr-4": { + "Dotenv\\": "src/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "BSD-3-Clause" + ], + "authors": [ + { + "name": "Vance Lucas", + "email": "vance@vancelucas.com", + "homepage": "http://www.vancelucas.com" + } + ], + "description": "Loads environment variables from `.env` to `getenv()`, `$_ENV` and `$_SERVER` automagically.", + "keywords": [ + "dotenv", + "env", + "environment" + ], + "time": "2019-01-29T11:11:52+00:00" + } + ], + "packages-dev": [], + "aliases": [], + "minimum-stability": "stable", + "stability-flags": [], + "prefer-stable": false, + "prefer-lowest": false, + "platform": { + "php": ">=7.0.0", + "ext-curl": "*", + "ext-openssl": "*", + "ext-sockets": "*", + "ext-json": "*", + "ext-zlib": "*", + "ext-mbstring": "*" + }, + "platform-dev": [] +} diff --git a/conf/user.conf.example b/conf/user.conf.example new file mode 100644 index 0000000..e8b14c8 --- /dev/null +++ b/conf/user.conf.example @@ -0,0 +1,114 @@ +####################### +# 账户设置 # +####################### + +# 帐号|密码 +APP_USER= +APP_PASS= + +# 令牌(自动生成) +ACCESS_TOKEN= +REFRESH_TOKEN= +COOKIE= + +####################### +# 功能设置 # +####################### + +# 推送服务器 +USE_SERVER=true +SERVER_ADDR= +SERVER_KEY= + +# 舰长总督 +USE_GUARD=true + +# 主站助手 +USE_MASTER_SITE=true + +# 活跃弹幕|弹幕房间(为空则随机)|弹幕内容(为空随机,但是URL不能为空) +USE_DANMU=true +DANMU_ROOMID=9522051 +DANMU_CONTENT= +CONTENT_FETCH="https://v1.hitokoto.cn/?encode=text" + +# 视频投币|投币稿件数(每日任务最大5)|自定义稿件ID,空则随机(AV_NUM=1生效) +USE_ADD_COIN=false +ADD_COIN_AV_NUM=1 +ADD_COIN_AV=33492180 + +# SERVER酱,用于推送消息 +USE_SCKEY= + +# 切换HTTPS,为真则使用https协议 +USE_HTTPS=true + +# 是否使用代理(前提保证有效代理) +USE_PROXY=false +PROXY_IP=127.0.0.1 +PROXY_PORT=8888 + +# 直播间ID,用于礼物赠送 +ROOM_ID=9522051 + +# 弹幕监控房间(为空则随机) +SOCKET_ROOM_ID=9522051 + +# 节奏风暴|丢弃率(0-100)|尝试数(范围值) +USE_STORM=true +STORM_DROPRATE=0 +STORM_ATTEMPT=30,50 + +# 统一活动 +USE_ACTIVE=true + +# 银瓜子兑换硬币 +USE_SILVER2COIN=true + +# 实物抽奖 +USE_MO=true + +####################### +# 日志设置 # +####################### + +# 写入日志 +APP_WRITELOG=false + +# 日志路径 +APP_WRITELOGPATH=log + +# 调试模式 +APP_DEBUG=false + +# 用户名,可自定义 +APP_UNAME= + +# 多账号区别输出 +APP_MULTIPLE=false + +# 账号别名,如果为空则默认使用登陆账号作为标示 +APP_USER_IDENTITY= + +# 日志回调地址 +APP_CALLBACK="http://www.example.com/api.send?text={account}[{level}]: {message}" + +# 错误回调级别 +# +# DEBUG 100 +# INFO 200 +# NOTICE 250 +# WARNING 300 +# ERROR 400 +# +APP_CALLBACK_LEVEL=400 + +####################### +# 固定设置 # +####################### + +# 用于打包请求 +ACTIONENTRY=7 + +# 用于发送心跳 +ACTIONHEARTBEAT=2 \ No newline at end of file diff --git a/index.php b/index.php new file mode 100644 index 0000000..2a9b680 --- /dev/null +++ b/index.php @@ -0,0 +1,94 @@ +overload(); + } + Daily::run(); + MasterSite::run(); + Danmu::run(); + GiftSend::run(); + Heart::run(); + Silver::run(); + Task::run(); + Silver2Coin::run(); + GroupSignIn::run(); + Live::run(); + GiftHeart::run(); + Winning::run(); + MaterialObject::run(); + TcpClinet::run(); + Storm::run(); + RaffleHandler::run(); + Guard::run(); + Statistics::run(); + usleep(0.1 * 1000000); + } + } + + protected static function loadConfigFile() + { + $file_path = __DIR__ . '/conf/' . self::$conf_file; + + if (is_file($file_path) && self::$conf_file != 'user.conf') { + $load_files = [ + self::$conf_file, + ]; + } else { + $default_file_path = __DIR__ . '/conf/user.conf'; + if (!is_file($default_file_path)) { + exit('默认加载配置文件不存在,请按照文档添加配置文件!'); + } + + $load_files = [ + 'user.conf', + ]; + } + foreach ($load_files as $load_file) { + self::$dotenv = new Dotenv(__DIR__ . '/conf', $load_file); + self::$dotenv->load(); + } + + // load ACCESS_KEY + Login::run(); + self::$dotenv->overload(); + } + +} + +// LOAD +$conf_file = isset($argv[1]) ? $argv[1] : 'user.conf'; +// RUN +Index::run($conf_file); diff --git a/src/Curl.php b/src/Curl.php new file mode 100644 index 0000000..ee864cd --- /dev/null +++ b/src/Curl.php @@ -0,0 +1,183 @@ + '*/*', + 'Accept-Encoding' => 'gzip', + 'Accept-Language' => 'zh-cn', + 'Connection' => 'keep-alive', + 'Content-Type' => 'application/x-www-form-urlencoded', + 'User-Agent' => 'bili-universal/8230 CFNetwork/975.0.3 Darwin/18.2.0', + // 'Referer' => 'https://live.bilibili.com/', + ); + + private static function getHeaders($headers) + { + return array_map(function ($k, $v) { + return $k . ': ' . $v; + }, array_keys($headers), $headers); + } + + public static function post($url, $payload = null, $headers = null, $timeout = 30) + { + $url = self::http2https($url); + Log::debug($url); + $header = is_null($headers) ? self::getHeaders(self::$headers) : self::getHeaders($headers); + + // 重试次数 + $ret_count = 300; + $waring = 280; + + while ($ret_count) { + // 网络断开判断 延时方便连接网络 + if ($ret_count < $waring) { + Log::warning("正常等待网络连接状态恢复正常..."); + sleep(mt_rand(5, 10)); + } + try { + $curl = curl_init(); + if (!is_null($payload)) { + curl_setopt($curl, CURLOPT_POST, 1); + curl_setopt($curl, CURLOPT_POSTFIELDS, is_array($payload) ? http_build_query($payload) : $payload); + } + curl_setopt($curl, CURLOPT_URL, $url); + curl_setopt($curl, CURLOPT_HTTPHEADER, $header); + curl_setopt($curl, CURLOPT_HEADER, 0); + curl_setopt($curl, CURLOPT_ENCODING, 'gzip'); + curl_setopt($curl, CURLOPT_IPRESOLVE, 1); + curl_setopt($curl, CURLOPT_RETURNTRANSFER, 1); + curl_setopt($curl, CURLOPT_SSL_VERIFYPEER, 0); + // 超时 重要 + curl_setopt($curl, CURLOPT_TIMEOUT, $timeout); + curl_setopt($curl, CURLOPT_CONNECTTIMEOUT, $timeout); + if (($cookie = getenv('COOKIE')) != "") { + curl_setopt($curl, CURLOPT_COOKIE, $cookie); + } + if (getenv('USE_PROXY') == 'true') { + curl_setopt($curl, CURLOPT_PROXY, getenv('PROXY_IP')); + curl_setopt($curl, CURLOPT_PROXYPORT, getenv('PROXY_PORT')); + } + $raw = curl_exec($curl); + + if ($err_no = curl_errno($curl)) { + throw new \Exception(curl_error($curl)); + } + + if ($raw === false || strpos($raw, 'timeout') !== false) { + Log::warning('重试,获取的资源无效!'); + $ret_count--; + continue; + } + + Log::debug($raw); + curl_close($curl); + return $raw; + + } catch (\Exception $e) { + Log::warning("重试,Curl请求出错,{$e->getMessage()}!"); + $ret_count--; + continue; + } + } + exit('重试次数过多,请检查代码,退出!'); + } + + public static function other($url, $payload = null, $headers = null, $cookie = null, $timeout = 30) + { + Log::debug($url); + $header = is_null($headers) ? self::getHeaders(self::$headers) : self::getHeaders($headers); + + // 重试次数 + $ret_count = 30; + while ($ret_count) { + try { + $curl = curl_init(); + if (!is_null($payload)) { + curl_setopt($curl, CURLOPT_POST, 1); + curl_setopt($curl, CURLOPT_POSTFIELDS, is_array($payload) ? http_build_query($payload) : $payload); + } + curl_setopt($curl, CURLOPT_URL, $url); + curl_setopt($curl, CURLOPT_HTTPHEADER, $header); + curl_setopt($curl, CURLOPT_HEADER, 0); + curl_setopt($curl, CURLOPT_ENCODING, 'gzip'); + curl_setopt($curl, CURLOPT_IPRESOLVE, 1); + curl_setopt($curl, CURLOPT_RETURNTRANSFER, 1); + curl_setopt($curl, CURLOPT_SSL_VERIFYPEER, 0); + // 超时 重要 + curl_setopt($curl, CURLOPT_TIMEOUT, $timeout); + curl_setopt($curl, CURLOPT_CONNECTTIMEOUT, $timeout); + // 认证 + if (!is_null($cookie)) { + curl_setopt($curl, CURLOPT_COOKIE, $cookie); + } + if (getenv('USE_PROXY') == 'true') { + curl_setopt($curl, CURLOPT_PROXY, getenv('PROXY_IP')); + curl_setopt($curl, CURLOPT_PROXYPORT, getenv('PROXY_PORT')); + } + $raw = curl_exec($curl); + + if ($err_no = curl_errno($curl)) { + throw new \Exception(curl_error($curl)); + } + + if ($raw === false || strpos($raw, 'timeout') !== false) { + Log::warning('重试,获取的资源无效!'); + $ret_count--; + continue; + } + + Log::debug($raw); + curl_close($curl); + return $raw; + + } catch (\Exception $e) { + Log::warning("重试,Curl请求出错,{$e->getMessage()}!"); + $ret_count--; + continue; + } + } + exit('重试次数过多,请检查代码,退出!'); + } + + + public static function get($url, $payload = null, $headers = null) + { + if (!is_null($payload)) { + $url .= '?' . http_build_query($payload); + } + return self::post($url, null, $headers); + } + + protected static function http2https($url) + { + switch (getenv('USE_HTTPS')) { + case 'false': + if (strpos($url, 'ttps://')) { + $url = str_replace('https://', 'http://', $url); + } + break; + case 'true': + if (strpos($url, 'ttp://')) { + $url = str_replace('http://', 'https://', $url); + } + break; + default: + Log::warning('当前协议设置不正确,请检查配置文件!'); + die(); + break; + } + + return $url; + } +} diff --git a/src/Daily.php b/src/Daily.php new file mode 100644 index 0000000..7887eb9 --- /dev/null +++ b/src/Daily.php @@ -0,0 +1,40 @@ + time()) { + return; + } + self::dailyBag(); + + self::$lock = time() + 3600; + } + + protected static function dailyBag() + { + $payload = []; + $data = Curl::get('https://api.live.bilibili.com/gift/v2/live/receive_daily_bag', Sign::api($payload)); + $data = json_decode($data, true); + + if (isset($data['code']) && $data['code']) { + Log::warning('每日礼包领取失败!', ['msg' => $data['message']]); + } else { + Log::notice('每日礼包领取成功'); + } + } + +} diff --git a/src/Danmu.php b/src/Danmu.php new file mode 100644 index 0000000..cdb5a86 --- /dev/null +++ b/src/Danmu.php @@ -0,0 +1,130 @@ + time() || getenv('USE_DANMU') == 'false') { + return; + } + $room_id = empty(getenv('DANMU_ROOMID')) ? Live::getUserRecommend() : Live::getRealRoomID(getenv('DANMU_ROOMID')); + $msg = empty(getenv('DANMU_CONTENT')) ? self::getMsgInfo() : getenv('DANMU_CONTENT'); + + $info = [ + 'roomid' => $room_id, + 'content' => $msg, + ]; + + if (self::privateSendMsg($info)) { + self::$lock = time() + 3600; + return; + } + + self::$lock = time() + 30; + } + + // 获取随机弹幕 + private static function getMsgInfo() + { + /* + * 整理一部分API,收集于网络,侵权麻烦联系我删除. + * 如果设置项不能用可以选择,只保证代码发布时正常. + * 格式全部为TEXT,请自己测试好了再放进配置文件. + * -7. https://api.lwl12.com/hitokoto/v1?encode=realjso + * -6. https://api.ly522.com/yan.php?format=text + * -5. https://v1.hitokoto.cn/?encode=text + * -4. https://api.jysafe.cn/yy/ + * -3. https://m.mom1.cn/api/ + * -2. https://api.ooopn.com/yan/api.php?type=text + * -1. https://api.imjad.cn/hitokoto/ + * 0. https://www.ly522.com/hitokoto/ + * 1. https://www.tddiao.online/word/ + * 2. https://api.guoch.xyz/ + * 3. http://www.ooomg.cn/dutang + * 4. https://api.gushi.ci/rensheng.txt + * 5. https://api.itswincer.com/hitokoto/v2/ + * 6. http://api.imiliy.cn/get/ + * 7. http://api.dsecret.com/yiyan/ + * 8. https://api.xygeng.cn/dailywd/api/api.php + */ + try { + $url = getenv('CONTENT_FETCH'); + if (empty($url)) { + exit('活跃弹幕配置错误,请检查相应配置'); + } + $data = Curl::get($url); + $punctuations = [',', ',', '。', '!', '.', ';', '——']; + foreach ($punctuations as $punctuation) { + if (strpos($data, $punctuation)) { + $data = explode($punctuation, $data)[0]; + break; + } + } + return $data; + + } catch (\Exception $e) { + return $e; + } + } + + //转换信息 + private static function convertInfo() + { + preg_match('/bili_jct=(.{32})/', getenv('COOKIE'), $token); + $token = isset($token[1]) ? $token[1] : ''; + return $token; + } + + //发送弹幕通用模块 + private static function sendMsg($info) + { + $raw = Curl::get('https://api.live.bilibili.com/room/v1/Room/room_init?id=' . $info['roomid']); + $de_raw = json_decode($raw, true); + + $payload = [ + 'color' => '16777215', + 'fontsize' => 25, + 'mode' => 1, + 'msg' => $info['content'], + 'rnd' => 0, + 'roomid' => $de_raw['data']['room_id'], + 'csrf' => self::convertInfo(), + 'csrf_token' => self::convertInfo(), + ]; + + return Curl::post('https://api.live.bilibili.com/msg/send', Sign::api($payload)); + } + + //使用发送弹幕模块 + private static function privateSendMsg($info) + { + //TODO 暂时性功能 有需求就修改 + $raw = self::sendMsg($info); + $de_raw = json_decode($raw, true); + + if ($de_raw['code'] == 1001) { + Log::warning($de_raw['msg']); + return false; + } + + if (!$de_raw['code']) { + Log::info('弹幕发送成功!'); + return true; + } + + Log::error("弹幕发送失败, {$de_raw['msg']}"); + return false; + } +} \ No newline at end of file diff --git a/src/DataTreating.php b/src/DataTreating.php new file mode 100644 index 0000000..f8d2430 --- /dev/null +++ b/src/DataTreating.php @@ -0,0 +1,49 @@ + $data['room_id'], 'lid' => $data['raffle_id']]; + } catch (\Exception $e) { + return; + } + switch ($data['raffle_type']) { + case "storm": + // 风暴 + Storm::pushToQueue($info); + break; + case "raffle": + // 礼物 + RaffleHandler::pushToQueue($info); + break; + case "guard": + // 舰长 + Guard::pushToQueue($info); + break; + case "small_tv": + // 小电视 + RaffleHandler::pushToQueue($info); + break; + default: + break; + } + } +} diff --git a/src/File.php b/src/File.php new file mode 100644 index 0000000..d553147 --- /dev/null +++ b/src/File.php @@ -0,0 +1,31 @@ + time()) { + return; + } + if (self::giftheart()) { + self::$lock = time() + 60 * 60; + return; + } + self::$lock = time() + 5 * 60; + } + + // GIFT HEART + protected static function giftheart(): bool + { + $payload = [ + 'roomid' => getenv('ROOM_ID'), + ]; + $raw = Curl::get('https://api.live.bilibili.com/gift/v2/live/heart_gift_receive', Sign::api($payload)); + $de_raw = json_decode($raw, true); + + if ($de_raw['code'] == -403) { + Log::info($de_raw['msg']); + $payload = [ + 'ruid' => 17561885, + ]; + Curl::get('https://api.live.bilibili.com/eventRoom/index', Sign::api($payload)); + return true; + } + + if ($de_raw['code'] != 0) { + Log::warning($de_raw['msg']); + return false; + } + + if ($de_raw['data']['heart_status'] == 0) { + Log::info('没有礼物可以领了呢!'); + return true; + } + + if (isset($de_raw['data']['gift_list'])) { + foreach ($de_raw['data']['gift_list'] as $vo) { + Log::info("{$de_raw['msg']},礼物 {$vo['gift_name']} ({$vo['day_num']}/{$vo['day_limit']})"); + } + return false; + } + } + +} \ No newline at end of file diff --git a/src/GiftSend.php b/src/GiftSend.php new file mode 100644 index 0000000..209e5b3 --- /dev/null +++ b/src/GiftSend.php @@ -0,0 +1,109 @@ + $data['message']]); + Log::warning('清空礼物功能禁用!'); + self::$lock = time() + 100000000; + return; + } + + self::$uid = $data['mid']; + + $payload = [ + 'id' => getenv('ROOM_ID'), + ]; + $data = Curl::get('https://api.live.bilibili.com/room/v1/Room/get_info', Sign::api($payload)); + $data = json_decode($data, true); + + if (isset($data['code']) && $data['code']) { + Log::warning('获取主播房间号失败!', ['msg' => $data['message']]); + Log::warning('清空礼物功能禁用!'); + self::$lock = time() + 100000000; + return; + } + + Log::info('直播间信息生成完毕!'); + + self::$ruid = $data['data']['uid']; + self::$roomid = $data['data']['room_id']; + } + + public static function run() + { + if (empty(self::$ruid)) { + self::getRoomInfo(); + } + + if (self::$lock > time()) { + return; + } + + $payload = []; + $data = Curl::get('https://api.live.bilibili.com/gift/v2/gift/bag_list', Sign::api($payload)); + $data = json_decode($data, true); + + if (isset($data['code']) && $data['code']) { + Log::warning('背包查看失败!', ['msg' => $data['message']]); + } + + if (isset($data['data']['list'])) { + foreach ($data['data']['list'] as $vo) { + if ($vo['expire_at'] >= $data['data']['time'] && $vo['expire_at'] <= $data['data']['time'] + 3600) { + self::send($vo); + sleep(3); + } + } + } + + self::$lock = time() + 600; + } + + protected static function send($value) + { + $payload = [ + 'coin_type' => 'silver', + 'gift_id' => $value['gift_id'], + 'ruid' => self::$ruid, + 'uid' => self::$uid, + 'biz_id' => self::$roomid, + 'gift_num' => $value['gift_num'], + 'data_source_id' => '', + 'data_behavior_id' => '', + 'bag_id' => $value['bag_id'] + ]; + + $data = Curl::post('https://api.live.bilibili.com/gift/v2/live/bag_send', Sign::api($payload)); + $data = json_decode($data, true); + + if (isset($data['code']) && $data['code']) { + Log::warning('送礼失败!', ['msg' => $data['message']]); + } else { + Log::notice("成功向 {$payload['biz_id']} 投喂了 {$value['gift_num']} 个{$value['gift_name']}"); + } + } +} diff --git a/src/GroupSignIn.php b/src/GroupSignIn.php new file mode 100644 index 0000000..9d39f04 --- /dev/null +++ b/src/GroupSignIn.php @@ -0,0 +1,74 @@ + time()) { + return; + } + + $groups = self::getGroupList(); + if (empty($groups)) { + self::$lock = time() + 24 * 60 * 60; + return; + } + + foreach ($groups as $group) { + self::signInGroup($group); + } + + self::$lock = time() + 8 * 60 * 60; + } + + //GROUP LIST + protected static function getGroupList(): array + { + $payload = []; + $raw = Curl::get('https://api.vc.bilibili.com/link_group/v1/member/my_groups', Sign::api($payload)); + $de_raw = json_decode($raw, true); + + if (empty($de_raw['data']['list'])) { + Log::notice('你没有需要签到的应援团!'); + return []; + } + return $de_raw['data']['list']; + } + + //SIGN IN + protected static function signInGroup(array $groupInfo): bool + { + $payload = [ + 'group_id' => $groupInfo['group_id'], + 'owner_id' => $groupInfo['owner_uid'], + ]; + $raw = Curl::get('https://api.vc.bilibili.com/link_setting/v1/link_setting/sign_in', Sign::api($payload)); + $de_raw = json_decode($raw, true); + + if ($de_raw['code'] != '0') { + Log::warning('在应援团{' . $groupInfo['group_name'] . '}中签到失败,原因待查'); + // TODO + return false; + } + if ($de_raw['data']['status'] == '0') { + Log::info('在应援团{' . $groupInfo['group_name'] . '}中签到成功,增加{' . $de_raw['data']['add_num'] . '点}亲密度'); + } else { + Log::notice('在应援团{' . $groupInfo['group_name'] . '}中不要重复签到'); + } + + return true; + } +} \ No newline at end of file diff --git a/src/Guard.php b/src/Guard.php new file mode 100644 index 0000000..9beabdf --- /dev/null +++ b/src/Guard.php @@ -0,0 +1,126 @@ + time()) { + return; + } + self::startLottery(); + } + + /** + * 抽奖逻辑 + * @return bool + */ + protected static function startLottery(): bool + { + $max_num = 3; + while ($max_num) { + $guard = array_shift(self::$wait_list); + if (is_null($guard)) { + break; + } + $guard_lid = $guard['lid']; + $guard_rid = $guard['rid']; + Live::goToRoom($guard_rid); + Statistics::addJoinList(self::KEY); + $data = self::lottery($guard_rid, $guard_lid); + if ($data['code'] == 0) { + Statistics::addSuccessList(self::KEY); + Log::notice("房间 {$guard_rid} 编号 {$guard_lid} " . self::KEY . ": {$data['data']['message']}"); + } elseif ($data['code'] == 400 && $data['msg'] == '你已经领取过啦') { + Log::info("房间 {$guard_rid} 编号 {$guard_lid} " . self::KEY . ": {$data['msg']}"); + } else { + Log::warning("房间 {$guard_rid} 编号 {$guard_lid} " . self::KEY . ": {$data['msg']}"); + } + $max_num--; + } + return true; + } + + /** + * 请求抽奖 + * @param $rid + * @param $lid + * @return array + */ + private static function lottery($rid, $lid): array + { + $user_info = User::parseCookies(); + $url = "https://api.live.bilibili.com/lottery/v2/lottery/join"; + $payload = [ + "roomid" => $rid, + "id" => $lid, + "type" => "guard", + "csrf_token" => $user_info['token'] + ]; + $raw = Curl::post($url, Sign::api($payload)); + $de_raw = json_decode($raw, true); + return $de_raw; + } + + /** + * 重复检测 + * @param int $lid + * @return bool + */ + private static function toRepeatLid(int $lid): bool + { + if (in_array($lid, self::$all_list)) { + return true; + } + if (count(self::$all_list) > 2000) { + self::$all_list = []; + } + array_push(self::$all_list, $lid); + + return false; + } + + /** + * 数据推入队列 + * @param array $data + * @return bool + */ + public static function pushToQueue(array $data): bool + { + if (getenv(self::SWITCH) == 'false') { + return false; + } + if (self::toRepeatLid($data['lid'])) { + return false; + } + Statistics::addPushList(self::KEY); + self::$wait_list = array_merge(self::$wait_list, [['rid' => $data['rid'], 'lid' => $data['lid']]]); + $wait_num = count(self::$wait_list); + if ($wait_num > 2) { + Log::info("当前队列中共有 {$wait_num} 个" . self::KEY . "待抽奖"); + } + return true; + } +} \ No newline at end of file diff --git a/src/Heart.php b/src/Heart.php new file mode 100644 index 0000000..b23ccca --- /dev/null +++ b/src/Heart.php @@ -0,0 +1,58 @@ + time()) { + return; + } + + self::pc(); + self::mobile(); + + self::$lock = time() + 300; + } + + protected static function pc() + { + $payload = [ + 'room_id' => getenv('ROOM_ID'), + ]; + $data = Curl::post('https://api.live.bilibili.com/User/userOnlineHeart', Sign::api($payload)); + $data = json_decode($data, true); + + if (isset($data['code']) && $data['code']) { + Log::warning('WEB端 直播间心跳停止惹~', ['msg' => $data['message']]); + } else { + Log::info('WEB端 发送心跳正常!'); + } + } + + protected static function mobile() + { + $payload = [ + 'room_id' => getenv('ROOM_ID'), + ]; + $data = Curl::post('https://api.live.bilibili.com/mobile/userOnlineHeart', Sign::api($payload)); + $data = json_decode($data, true); + + if (isset($data['code']) && $data['code']) { + Log::warning('APP端 直播间心跳停止惹~', ['msg' => $data['message']]); + } else { + Log::info('APP端 发送心跳正常!'); + } + } +} diff --git a/src/Live.php b/src/Live.php new file mode 100644 index 0000000..5a47bef --- /dev/null +++ b/src/Live.php @@ -0,0 +1,158 @@ + $room_id, + ]; + Curl::post('https://api.live.bilibili.com/room/v1/Room/room_entry_action', Sign::api($payload)); + // Log::info('进入直播间[' . $room_id . ']抽奖!'); + return true; + } + + // get Millisecond + public static function getMillisecond() + { + list($t1, $t2) = explode(' ', microtime()); + return (float)sprintf('%.0f', (floatval($t1) + floatval($t2)) * 1000); + } + + // IS SLEEP + public static function isSleep() + { + if (self::$lock > time()) { + return; + } + self::$lock = time() + 5 * 60; + + $hour = date('H'); + if ($hour >= 2 && $hour < 6) { + self::bannedVisit('sleep'); + Log::warning('休眠时间,暂停非必要任务,4小时后自动开启!'); + return; + } + + $payload = []; + $raw = Curl::get('https://api.live.bilibili.com/mobile/freeSilverAward', Sign::api($payload)); + $de_raw = json_decode($raw, true); + if ($de_raw['msg'] == '访问被拒绝') { + self::bannedVisit('ban'); + Log::warning('账号拒绝访问,暂停非必要任务,凌晨自动开启!'); + } + return; + } + + //被封禁访问 + public static function bannedVisit($arg) + { + //获取当前时间 + $block_time = strtotime(date("Y-m-d H:i:s")); + + if ($arg == 'ban') { + $unblock_time = strtotime(date("Y-m-d", strtotime("+1 day", $block_time))); + } elseif ($arg == 'sleep') { + // TODO + $unblock_time = $block_time + 4 * 60 * 60; + } else { + $unblock_time = time(); + } + + $second = time() + ceil($unblock_time - $block_time) + 5 * 60; + $hour = floor(($second - time()) / 60 / 60); + + if ($arg == 'ban') { + // 推送被ban信息 + Notice::run('banned', $hour); + } + + self::$lock = $second; + + Silver::$lock = $second; + MaterialObject::$lock = $second; + Websocket::$lock = $second; + GiftHeart::$lock = $second; + Guard::$lock = $second; + RaffleHandler::$lock = $second; + RaffleHandler::$rw_lock = $second; + + return; + } + +} \ No newline at end of file diff --git a/src/Log.php b/src/Log.php new file mode 100644 index 0000000..4a45410 --- /dev/null +++ b/src/Log.php @@ -0,0 +1,110 @@ +setFormatter(new ColoredLineFormatter()); + $logger->pushHandler($handler); + self::$instance = $logger; + } + + private static function prefix() + { + if (getenv('APP_MULTIPLE') == 'true') { + return '[' . (empty($t = getenv('APP_USER_IDENTITY')) ? getenv('APP_USER') : $t) . ']'; + } + return ''; + } + + private static function writeLog($type, $message) + { + if (getenv('APP_WRITELOG') == 'true') { + $path = './' . getenv("APP_WRITELOGPATH") . '/'; + if (!file_exists($path)) { + mkdir($path); + chmod($path, 0777); + } + $filename = $path . getenv('APP_USER') . ".log"; + $date = date('[Y-m-d H:i:s] '); + $data = $date . ' Log.' . $type . ' ' . $message . PHP_EOL; + file_put_contents($filename, $data, FILE_APPEND); + } + return; + } + + public static function debug($message, array $context = []) + { + self::writeLog('DEBUG', $message); + self::getLogger()->addDebug($message, $context); + } + + public static function info($message, array $context = []) + { + $message = self::prefix() . $message; + self::writeLog('INFO', $message); + self::getLogger()->addInfo($message, $context); + self::callback(Logger::INFO, 'INFO', $message); + } + + public static function notice($message, array $context = []) + { + $message = self::prefix() . $message; + self::writeLog('NOTICE', $message); + self::getLogger()->addNotice($message, $context); + self::callback(Logger::NOTICE, 'NOTICE', $message); + } + + public static function warning($message, array $context = []) + { + $message = self::prefix() . $message; + self::writeLog('WARNING', $message); + self::getLogger()->addWarning($message, $context); + self::callback(Logger::WARNING, 'WARNING', $message); + } + + public static function error($message, array $context = []) + { + $message = self::prefix() . $message; + self::writeLog('ERROR', $message); + self::getLogger()->addError($message, $context); + self::callback(Logger::ERROR, 'ERROR', $message); + } + + public static function callback($levelId, $level, $message) + { + $callback_level = (('APP_CALLBACK_LEVEL') == '') ? (Logger::ERROR) : intval(getenv('APP_CALLBACK_LEVEL')); + if ($levelId >= $callback_level) { + $url = str_replace('{account}', self::prefix(), getenv('APP_CALLBACK')); + $url = str_replace('{level}', $level, $url); + $url = str_replace('{message}', urlencode($message), $url); + Curl::get($url); + } + } +} \ No newline at end of file diff --git a/src/Login.php b/src/Login.php new file mode 100644 index 0000000..a21266c --- /dev/null +++ b/src/Login.php @@ -0,0 +1,204 @@ +overload(); + + Log::info('正在检查令牌合法性...'); + if (!self::info()) { + Log::warning('令牌即将过期'); + Log::info('申请更换令牌中...'); + if (!self::refresh()) { + Log::warning('无效令牌,正在重新申请...'); + self::login(); + } + } + self::$lock = time() + 3600; + } + + public static function check() + { + if (self::$lock > time()) { + return true; + } + self::$lock = time() + 7200; + if (!self::info()) { + Log::warning('令牌即将过期'); + Log::info('申请更换令牌中...'); + if (!self::refresh()) { + Log::warning('无效令牌,正在重新申请...'); + self::login(); + } + return false; + } + return true; + } + + protected static function info() + { + $access_token = getenv('ACCESS_TOKEN'); + $payload = [ + 'access_token' => $access_token, + ]; + $data = Curl::get('https://passport.bilibili.com/api/v2/oauth2/info', Sign::api($payload)); + $data = json_decode($data, true); + if (isset($data['code']) && $data['code']) { + Log::error('检查令牌失败', ['msg' => $data['message']]); + return false; + } + Log::info('令牌有效期: ' . date('Y-m-d H:i:s', $data['ts'] + $data['data']['expires_in'])); + return $data['data']['expires_in'] > 14400; + } + + public static function refresh() + { + $access_token = getenv('ACCESS_TOKEN'); + $refresh_token = getenv('REFRESH_TOKEN'); + $payload = [ + 'access_token' => $access_token, + 'refresh_token' => $refresh_token, + ]; + $data = Curl::post('https://passport.bilibili.com/api/oauth2/refreshToken', Sign::api($payload)); + $data = json_decode($data, true); + if (isset($data['code']) && $data['code']) { + Log::error('重新生成令牌失败', ['msg' => $data['message']]); + return false; + } + Log::info('令牌生成完毕!'); + $access_token = $data['data']['access_token']; + File::writeNewEnvironmentFileWith('ACCESS_TOKEN', $access_token); + Log::info(' > access token: ' . $access_token); + $refresh_token = $data['data']['refresh_token']; + File::writeNewEnvironmentFileWith('REFRESH_TOKEN', $refresh_token); + Log::info(' > refresh token: ' . $refresh_token); + return true; + } + + protected static function login($captcha = '', $headers = []) + { + $user = getenv('APP_USER'); + $pass = getenv('APP_PASS'); + if (empty($user) || empty($pass)) { + Log::error('空白的帐号和口令!'); + die(); + } + + // get PublicKey + Log::info('正在载入安全模块...'); + $payload = []; + $data = Curl::post('https://passport.bilibili.com/api/oauth2/getKey', Sign::api($payload)); + $data = json_decode($data, true); + if (isset($data['code']) && $data['code']) { + Log::error('公钥获取失败', ['msg' => $data['message']]); + die(); + } else { + Log::info('安全模块载入完毕!'); + } + $public_key = $data['data']['key']; + $hash = $data['data']['hash']; + openssl_public_encrypt($hash . $pass, $crypt, $public_key); + for ($i = 0; $i < 30; $i++) { + // login + Log::info('正在获取令牌...'); + $payload = [ + 'subid' => 1, + 'permission' => 'ALL', + 'username' => $user, + 'password' => base64_encode($crypt), + 'captcha' => $captcha, + ]; + $data = Curl::post('https://passport.bilibili.com/api/v2/oauth2/login', Sign::api($payload), $headers); + $data = json_decode($data, true); + if (isset($data['code']) && $data['code'] == -105) { + $captcha_data = static::loginWithCaptcha(); + $captcha = $captcha_data['captcha']; + $headers = $captcha_data['headers']; + continue; + } + break; + } + if (isset($data['code']) && $data['code']) { + Log::error('登录失败', ['msg' => $data['message']]); + die(); + } + self::saveCookie($data); + Log::info('令牌获取成功!'); + $access_token = $data['data']['token_info']['access_token']; + File::writeNewEnvironmentFileWith('ACCESS_TOKEN', $access_token); + Log::info(' > access token: ' . $access_token); + $refresh_token = $data['data']['token_info']['refresh_token']; + File::writeNewEnvironmentFileWith('REFRESH_TOKEN', $refresh_token); + Log::info(' > refresh token: ' . $refresh_token); + + return; + } + + + protected static function loginWithCaptcha() + { + Log::info('登陆需要验证 ,启动验证码登陆!'); + $headers = [ + 'Accept' => 'application/json, text/plain, */*', + 'User-Agent' => 'bili-universal/8230 CFNetwork/975.0.3 Darwin/18.2.0', + 'Host' => 'passport.bilibili.com', + 'Cookie' => 'sid=blhelper' + ]; + $data = Curl::other('https://passport.bilibili.com/captcha', null, $headers); + $data = base64_encode($data); + $captcha = static::ocrCaptcha($data); + return [ + 'captcha' => $captcha, + 'headers' => $headers, + ]; + } + + + private static function ocrCaptcha($captcha_img) + { + $payload = [ + 'image' => (string)$captcha_img + ]; + $headers = [ + 'Content-Type' => 'application/json', + ]; + $data = Curl::other('http://captcha.biem.club:19951/', json_encode($payload), $headers); + $de_raw = json_decode($data, true); + Log::info("验证码识别结果 {$de_raw['message']}"); + + return $de_raw['message']; + } + + private static function saveCookie($data) + { + Log::info('COOKIE获取成功!'); + //临时保存cookie + $temp = ''; + $cookies = $data['data']['cookie_info']['cookies']; + foreach ($cookies as $cookie) { + $temp .= $cookie['name'] . '=' . $cookie['value'] . ';'; + } + File::writeNewEnvironmentFileWith('COOKIE', $temp); + Log::info(' > auth cookie: ' . $temp); + return; + } + +} \ No newline at end of file diff --git a/src/MasterSite.php b/src/MasterSite.php new file mode 100644 index 0000000..ba89a3a --- /dev/null +++ b/src/MasterSite.php @@ -0,0 +1,289 @@ + time() || getenv('USE_MASTER_SITE') == 'false') { + return; + } + if (self::watchAid() && self::shareAid() && self::coinAdd()) { + self::$lock = time() + 24 * 60 * 60; + return; + } + self::$lock = time() + 3600; + } + + // 投币 + private static function reward($aid): bool + { + $user_info = User::parseCookies(); + $url = "https://api.bilibili.com/x/web-interface/coin/add"; + $payload = [ + "aid" => $aid, + "multiply" => "1", + "cross_domain" => "true", + "csrf" => $user_info['token'] + ]; + $headers = [ + 'Host' => "api.bilibili.com", + 'Origin' => "https://www.bilibili.com", + 'Referer' => "https://www.bilibili.com/video/av{$aid}", + 'User-Agent' => "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/69.0.3497.81 Safari/537.36", + ]; + $raw = Curl::post($url, Sign::api($payload), $headers); + $de_raw = json_decode($raw, true); + if ($de_raw['code'] == 0) { + Log::notice("主站任务: av{$aid}投币成功!"); + return true; + } else { + Log::warning("主站任务: av{$aid}投币失败!"); + return false; + } + } + + // 投币日志 + protected static function coinLog(): int + { + $url = "https://api.bilibili.com/x/member/web/coin/log"; + $payload = []; + $raw = Curl::get($url, Sign::api($payload)); + $de_raw = json_decode($raw, true); + + $logs = $de_raw['data']['list']; + $coins = 0; + foreach ($logs as $log) { + $log_ux = strtotime($log['time']); + $log_date = date('Y-m-d', $log_ux); + $now_date = date('Y-m-d'); + if ($log_date != $now_date) { + break; + } + if (strpos($log['reason'], "打赏") !== false) { + switch ($log['delta']) { + case -1: + $coins += 1; + break; + case -2: + $coins += 2; + break; + default: + break; + } + } + } + return $coins; + } + + // 投币操作 + protected static function coinAdd(): bool + { + switch (getenv('USE_ADD_COIN')) { + case 'false': + break; + case 'true': + $av_num = getenv('ADD_COIN_AV_NUM'); + $av_num = (int)$av_num; + if ($av_num == 0) { + Log::warning('当前视频投币设置不正确,请检查配置文件!'); + die(); + } + if ($av_num == 1) { + $aid = !empty(getenv('ADD_COIN_AV')) ? getenv('ADD_COIN_AV') : self::getRandomAid(); + self::reward($aid); + } else { + $coins = $av_num - self::coinLog(); + if ($coins <= 0) { + Log::info('今日投币上限已满!'); + break; + } + $aids = self::getDayRankingAids($av_num); + foreach ($aids as $aid) { + self::reward($aid); + } + } + break; + default: + Log::warning('当前视频投币设置不正确,请检查配置文件!'); + die(); + break; + } + return true; + } + + // 获取随机AID + private static function getRandomAid(): string + { + $page = random_int(1, 100000); + $payload = []; + $url = "https://api.bilibili.com/x/web-interface/newlist?&pn={$page}&ps=1"; + $raw = Curl::get($url, Sign::api($payload)); + $de_raw = json_decode($raw, true); + $aid = $de_raw['data']['archives'][0]['aid']; + + if (is_null($aid) || empty($aid) || $aid == '') { + $aid = random_int(10000000, 30000000); + } + return (string)$aid; + } + + // 日榜AID + private static function getDayRankingAids($num): array + { + // day: 日榜1 三榜3 周榜7 月榜30 + $payload = []; + $aids = []; + $rand_nums = []; + $url = "https://api.bilibili.com/x/web-interface/ranking?rid=0&day=1&type=1&arc_type=0"; + $raw = Curl::get($url, Sign::api($payload)); + $de_raw = json_decode($raw, true); + for ($i = 0; $i < $num; $i++) { + while (true) { + $rand_num = random_int(1, 100); + if (in_array($rand_num, $rand_nums)) { + continue; + } else { + array_push($rand_nums, $rand_num); + break; + } + } + $aid = $de_raw['data']['list'][$rand_nums[$i]]['aid']; + array_push($aids, $aid); + } + + return $aids; + } + + // 分享视频 + private static function shareAid(): bool + { + # aid = 稿件av号 + $url = "https://api.bilibili.com/x/web-interface/share/add"; + $av_info = self::parseAid(); + $user_info = User::parseCookies(); + $payload = [ + 'aid' => $av_info['aid'], + 'jsonp' => "jsonp", + 'csrf' => $user_info['token'], + ]; + $headers = [ + 'Host' => "api.bilibili.com", + 'Origin' => "https://www.bilibili.com", + 'Referer' => "https://www.bilibili.com/video/av{$av_info['aid']}", + 'User-Agent' => "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/69.0.3497.81 Safari/537.36", + ]; + $raw = Curl::post($url, Sign::api($payload), $headers); + $de_raw = json_decode($raw, true); + if ($de_raw['code'] == 0) { + Log::notice("主站任务: av{$av_info['aid']}分享成功!"); + return true; + } else { + Log::warning("主站任务: av{$av_info['aid']}分享失败!"); + return false; + } + } + + // 观看视频 + private static function watchAid(): bool + { + $url = "https://api.bilibili.com/x/report/click/h5"; + $av_info = self::parseAid(); + $user_info = User::parseCookies(); + $payload = [ + 'aid' => $av_info['aid'], + 'cid' => $av_info['cid'], + 'part' => 1, + 'did' => $user_info['sid'], + 'ftime' => time(), + 'jsonp' => "jsonp", + 'lv' => "", + 'mid' => $user_info['uid'], + 'csrf' => $user_info['token'], + 'stime' => time() + ]; + + $headers = [ + 'Host' => "api.bilibili.com", + 'Origin' => "https://www.bilibili.com", + 'Referer' => "https://www.bilibili.com/video/av{$av_info['aid']}", + 'User-Agent' => "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/69.0.3497.81 Safari/537.36", + ]; + + $raw = Curl::post($url, Sign::api($payload), $headers); + $de_raw = json_decode($raw, true); + + if ($de_raw['code'] == 0) { + $url = "https://api.bilibili.com/x/report/web/heartbeat"; + $payload = [ + "aid" => $av_info['aid'], + "cid" => $av_info['cid'], + "mid" => $user_info['uid'], + "csrf" => $user_info['token'], + "jsonp" => "jsonp", + "played_time" => "0", + "realtime" => $av_info['duration'], + "pause" => false, + "dt" => "7", + "play_type" => "1", + 'start_ts' => time() + ]; + $raw = Curl::post($url, Sign::api($payload), $headers); + $de_raw = json_decode($raw, true); + + if ($de_raw['code'] == 0) { + sleep(5); + $payload['played_time'] = $av_info['duration'] - 1; + $payload['play_type'] = 0; + $payload['start_ts'] = time(); + $raw = Curl::post($url, Sign::api($payload), $headers); + $de_raw = json_decode($raw, true); + if ($de_raw['code'] == 0) { + Log::notice("主站任务: av{$av_info['aid']}观看成功!"); + return true; + } + } + } + Log::warning("主站任务: av{$av_info['aid']}观看失败!"); + return false; + } + + // 解析AID到CID + private static function parseAid(): array + { + while (true) { + $aid = self::getRandomAid(); + $url = "https://api.bilibili.com/x/web-interface/view?aid={$aid}"; + $raw = Curl::get($url); + $de_raw = json_decode($raw, true); + if ($de_raw['code'] != 0) { + continue; + } else { + if (!array_key_exists('cid', $de_raw['data'])) { + continue; + } + } + $cid = $de_raw['data']['cid']; + $duration = $de_raw['data']['duration']; + break; + } + + return [ + 'aid' => $aid, + 'cid' => $cid, + 'duration' => $duration + ]; + } + +} \ No newline at end of file diff --git a/src/MaterialObject.php b/src/MaterialObject.php new file mode 100644 index 0000000..8c32906 --- /dev/null +++ b/src/MaterialObject.php @@ -0,0 +1,182 @@ + time()) { + return; + } + // 计算AID TODO 待优化 + self::calculateAid(150, 550); + self::drawLottery(); + + self::$lock = time() + random_int(5, 10) * 60; + } + + /** + * @use 实物抽奖 + * @return bool + */ + protected static function drawLottery(): bool + { + $block_key_list = ['测试', '加密', 'test', 'TEST', '钓鱼', '炸鱼', '调试']; + $flag = 5; + + for ($i = self::$start_aid; $i < self::$end_aid; $i++) { + if (!$flag) { + break; + } + // 在丢弃列表里 跳过 + if (in_array($i, self::$discard_aid_list)) { + continue; + } + + $payload = [ + 'aid' => $i, + ]; + $url = 'https://api.live.bilibili.com/lottery/v1/box/getStatus'; + // 请求 && 解码 + $raw = Curl::get($url, Sign::api($payload)); + $de_raw = json_decode($raw, true); + // -403 没有抽奖 + if ($de_raw['code'] != '0') { + $flag--; + continue; + } + // 如果最后一个结束时间已过 加入丢弃 + $lotterys = $de_raw['data']['typeB']; + $total = count($lotterys); + if ($lotterys[$total - 1]['join_end_time'] < time()) { + array_push(self::$discard_aid_list, $i); + continue; + } + + // 如果存在敏感词 加入丢弃 + $title = $de_raw['data']['title']; + foreach ($block_key_list as $block_key) { + if (strpos($title, $block_key) !== false) { + array_push(self::$discard_aid_list, $i); + continue; + } + } + + $num = 1; + foreach ($lotterys as $lottery) { + $join_end_time = $lottery['join_end_time']; + $join_start_time = $lottery['join_start_time']; + + if ($join_end_time > time() && time() > $join_start_time) { + switch ($lottery['status']) { + case 3: + Log::info("实物[{$i}]抽奖: 当前轮次已经结束!"); + break; + case 1: + Log::info("实物[{$i}]抽奖: 当前轮次已经抽过了!"); + break; + case -1: + Log::info("实物[{$i}]抽奖: 当前轮次暂未开启!"); + break; + case 0: + Log::info("实物[{$i}]抽奖: 当前轮次正在抽奖中!"); + + $payload = [ + 'aid' => $i, + 'number' => $num, + ]; + $raw = Curl::get('https://api.live.bilibili.com/lottery/v1/box/draw', Sign::api($payload)); + $de_raw = json_decode($raw, true); + + if ($de_raw['code'] == 0) { + Log::notice("实物[{$i}]抽奖: 成功!"); + } + $num++; + break; + + default: + Log::info("实物[{$i}]抽奖: 当前轮次状态码[{$lottery['status'] }]未知!"); + break; + } + } + } + } + return true; + } + + /** + * @use 计算 开始结束的AID + * @param $min + * @param $max + * @return bool + */ + private static function calculateAid($min, $max): bool + { + if (self::$end_aid != 0 && self::$start_aid != 0) { + return false; + } + + while (1) { + $middle = round(($min + $max) / 2); + if (self::aidPost($middle)) { + if (self::aidPost($middle + random_int(0, 3))) { + $max = $middle; + } else { + $min = $middle; + } + } else { + $min = $middle; + } + if ($max - $min == 1) { + break; + } + } + + self::$start_aid = $min - random_int(30, 40); + self::$end_aid = $min + random_int(30, 40); + Log::info("实物抽奖起始值[" . self::$start_aid . "],结束值[" . self::$end_aid . "]"); + return true; + } + + /** + * @use Aid 请求 + * @param $aid + * @return bool + */ + private static function aidPost($aid): bool + { + $payload = [ + 'aid' => $aid, + ]; + $raw = Curl::get('https://api.live.bilibili.com/lottery/v1/box/getStatus', Sign::api($payload)); + $de_raw = json_decode($raw, true); + + // 等于0是有抽奖返回false + if ($de_raw['code'] == 0) { + return false; + } + // 没有抽奖 + return true; + } +} \ No newline at end of file diff --git a/src/Notice.php b/src/Notice.php new file mode 100644 index 0000000..b14c63b --- /dev/null +++ b/src/Notice.php @@ -0,0 +1,123 @@ + '活动抽奖结果', + 'content' => '[' . $nowtime . ']' . ' 用户: ' . self::$uname . ' 在活动抽奖中获得: ' . self::$result, + ]; + break; + case 'storm': + $info = [ + 'title' => '节奏风暴中奖结果', + 'content' => '[' . $nowtime . ']' . ' 用户: ' . self::$uname . ' 在节奏风暴抽奖中: ' . self::$result, + ]; + break; + case 'active': + $info = [ + 'title' => '活动中奖结果', + 'content' => '[' . $nowtime . ']' . ' 用户: ' . self::$uname . ' 在活动抽奖中获得: ' . self::$result, + ]; + break; + case 'cookieRefresh': + $info = [ + 'title' => 'Cookie刷新', + 'content' => '[' . $nowtime . ']' . ' 用户: ' . self::$uname . ' 刷新Cookie: ' . self::$result, + ]; + break; + case 'loginInit': + break; + + case 'todaySign': + $info = [ + 'title' => '每日签到', + 'content' => '[' . $nowtime . ']' . ' 用户: ' . self::$uname . ' 签到: ' . self::$result, + ]; + break; + case 'winIng': + $info = [ + 'title' => '实物中奖', + 'content' => '[' . $nowtime . ']' . ' 用户: ' . self::$uname . ' 实物中奖: ' . self::$result, + ]; + break; + case 'banned': + $info = [ + 'title' => '账号封禁', + 'content' => '[' . $nowtime . ']' . ' 用户: ' . self::$uname . ' 账号被封禁: 程序开始睡眠,凌晨自动唤醒,距离唤醒还有' . self::$result . '小时', + ]; + break; + default: + break; + } + self::scSend($info); + + return true; + } + + // 发送信息 + private static function scSend($info) + { + + $postdata = http_build_query( + [ + 'text' => $info['title'], + 'desp' => $info['content'] + ] + ); + + $opts = ['http' => + [ + 'method' => 'POST', + 'header' => 'Content-type: application/x-www-form-urlencoded', + 'content' => $postdata + ] + ]; + try { + $context = stream_context_create($opts); + file_get_contents('https://sc.ftqq.com/' . self::$sckey . '.send', false, $context); + + } catch (\Exception $e) { + Log::warning('Server酱推送信息失败,请检查!'); + } + return; + //return $result = file_get_contents('https://sc.ftqq.com/' . $this->_sckey . '.send', false, $context); + } +} \ No newline at end of file diff --git a/src/RaffleHandler.php b/src/RaffleHandler.php new file mode 100644 index 0000000..26e81c5 --- /dev/null +++ b/src/RaffleHandler.php @@ -0,0 +1,219 @@ + time()) { + return; + } + self::startLottery(); + } + + /** + * 抽奖逻辑 + * @return bool + */ + protected static function startLottery(): bool + { + $max_num = mt_rand(5, 10); + while ($max_num) { + $raffle = array_shift(self::$wait_list); + if (is_null($raffle)) { + break; + } + Live::goToRoom($raffle['room_id']); + Statistics::addJoinList(self::KEY); + self::lottery($raffle); + $max_num--; + } + return true; + } + + + /** + * 检查抽奖列表 + * @param $rid + * @return bool + */ + private static function checkWeb($rid): bool + { + $payload = [ + 'roomid' => $rid + ]; + $url = 'https://api.live.bilibili.com/gift/v3/smalltv/check'; + $raw = Curl::get($url, Sign::api($payload)); + $de_raw = json_decode($raw, true); + + // 计数 && 跳出 + $total = count($de_raw['data']['list']); + if (!$total) { + return false; + } + + for ($i = 0; $i < $total; $i++) { + /** + * raffleId : 88995 + * title : C位光环抽奖 + * type : GIFT_30013 + */ + $data = [ + 'raffle_id' => $de_raw['data']['list'][$i]['raffleId'], + 'title' => $de_raw['data']['list'][$i]['title'], + 'type' => $de_raw['data']['list'][$i]['type'], + 'room_id' => $rid + ]; + if (self::toRepeatLid($data['raffle_id'])) { + continue; + } + Statistics::addPushList(self::KEY); + array_push(self::$wait_list, $data); + } + return true; + } + + + /** + * @use WEB中奖查询 + */ + public static function resultWeb() + { + // 时间锁 + if (self::$rw_lock > time()) { + return; + } + // 如果待查询为空 && 去重 + if (!count(self::$finsh_list)) { + self::$rw_lock = time() + 40; + return; + } + // 查询,每次查询10个 + $flag = 0; + foreach (self::$finsh_list as $winning_web) { + $flag++; + if ($flag > 40) { + break; + } + // 参数 + $payload = [ + 'type' => $winning_web['type'], + 'raffleId' => $winning_web['raffle_id'] + ]; + // Web V3 Notice + $url = 'https://api.live.bilibili.com/gift/v3/smalltv/notice'; + // 请求 && 解码 + $raw = Curl::get($url, Sign::api($payload)); + $de_raw = json_decode($raw, true); + // 判断 + switch ($de_raw['data']['status']) { + case 3: + break; + case 2: + Statistics::addSuccessList(self::KEY); + // 提示信息 + $info = "房间 {$winning_web['room_id']} 编号 {$winning_web['raffle_id']} {$winning_web['title']}: 获得"; + $info .= "{$de_raw['data']['gift_name']}X{$de_raw['data']['gift_num']}"; + Log::notice($info); + // 推送活动抽奖信息 + if ($de_raw['data']['gift_name'] != '辣条' && $de_raw['data']['gift_name'] != '') { + Notice::run('raffle', $info); + } + // 删除查询完成ID + unset(self::$finsh_list[$flag - 1]); + self::$finsh_list = array_values(self::$finsh_list); + break; + default: + break; + } + } + self::$rw_lock = time() + 40; + return; + } + + + /** + * @use 请求抽奖 + * @param array $data + */ + private static function lottery(array $data) + { + $payload = [ + 'raffleId' => $data['raffle_id'], + 'roomid' => $data['room_id'], + ]; + $url = 'https://api.live.bilibili.com/gift/v3/smalltv/join'; + $raw = Curl::get($url, Sign::api($payload)); + $de_raw = json_decode($raw, true); + if (isset($de_raw['code']) && $de_raw['code']) { + Log::notice("房间 {$data['raffle_id']} 编号 {$data['room_id']} " . self::KEY . ": {$de_raw['message']}"); + } else { + Log::notice("房间 {$data['raffle_id']} 编号 {$data['room_id']} " . self::KEY . ": {$de_raw['msg']}"); + array_push(self::$finsh_list, $data); + } + return; + } + + /** + * 重复检测 + * @param int $lid + * @return bool + */ + private static function toRepeatLid(int $lid): bool + { + if (in_array($lid, self::$all_list)) { + return true; + } + if (count(self::$all_list) > 2000) { + self::$all_list = []; + } + array_push(self::$all_list, $lid); + + return false; + } + + /** + * 数据推入队列 + * @param array $data + * @return bool + */ + public static function pushToQueue(array $data): bool + { + if (getenv(self::SWITCH) == 'false') { + return false; + } + + if (Live::fishingDetection($data['rid'])) { + return false; + } + self::checkWeb($data['rid']); + $wait_num = count(self::$wait_list); + if ($wait_num > 2) { + Log::info("当前队列中共有 {$wait_num} 个" . self::KEY . "待抽奖"); + } + return true; + } + +} diff --git a/src/Sign.php b/src/Sign.php new file mode 100644 index 0000000..f70743f --- /dev/null +++ b/src/Sign.php @@ -0,0 +1,47 @@ + getenv('ACCESS_TOKEN'), + 'actionKey' => 'appkey', + 'appkey' => $appkey, + 'build' => '8230', + 'device' => 'phone', + 'mobi_app' => 'iphone', + 'platform' => 'ios', + 'ts' => time(), + ]; + + $payload = array_merge($payload, $default); + if (isset($payload['sign'])) { + unset($payload['sign']); + } + ksort($payload); + $data = http_build_query($payload); + $payload['sign'] = md5($data . $appsecret); + return $payload; + } +} \ No newline at end of file diff --git a/src/Silver.php b/src/Silver.php new file mode 100644 index 0000000..e31cfc4 --- /dev/null +++ b/src/Silver.php @@ -0,0 +1,92 @@ + time()) { + return; + } + + if (!empty(self::$task)) { + self::pushTask(); + } else { + self::pullTask(); + } + } + + protected static function pushTask() + { + $payload = [ + 'time_end' => self::$task['time_end'], + 'time_start' => self::$task['time_start'] + ]; + $data = Curl::get('https://api.live.bilibili.com/mobile/freeSilverAward', Sign::api($payload)); + $data = json_decode($data, true); + + if ($data['code'] == -800) { + self::$lock = time() + 12 * 60 * 60; + Log::warning("领取宝箱失败,{$data['message']}!"); + return; + } + + if ($data['code'] == -903) { + Log::warning("领取宝箱失败,{$data['message']}!"); + self::$task = []; + self::$lock = time() + 60; + return; + } + + if (isset($data['code']) && $data['code']) { + Log::warning("领取宝箱失败,{$data['message']}!"); + self::$lock = time() + 60; + return; + } + + Log::notice("领取宝箱成功,Silver: {$data['data']['silver']}(+{$data['data']['awardSilver']})"); + + self::$task = []; + self::$lock = time() + 10; + } + + protected static function pullTask() + { + $payload = []; + $data = Curl::get('https://api.live.bilibili.com/lottery/v1/SilverBox/getCurrentTask', Sign::api($payload)); + $data = json_decode($data, true); + + if (isset($data['code']) && $data['code'] == -10017) { + Log::notice($data['message']); + self::$lock = time() + 24 * 60 * 60; + return; + } + + if (isset($data['code']) && $data['code']) { + Log::error("check freeSilverCurrentTask failed! Error message: {$data['message']}"); + die(); + } + + Log::info("获得一个宝箱,内含 {$data['data']['silver']} 个瓜子"); + Log::info("开启宝箱需等待 {$data['data']['minute']} 分钟"); + + self::$task = [ + 'time_start' => $data['data']['time_start'], + 'time_end' => $data['data']['time_end'], + ]; + + self::$lock = time() + $data['data']['minute'] * 60 + 5; + } +} diff --git a/src/Silver2Coin.php b/src/Silver2Coin.php new file mode 100644 index 0000000..c18d308 --- /dev/null +++ b/src/Silver2Coin.php @@ -0,0 +1,66 @@ + time() || getenv('USE_SILVER2COIN') == 'false') { + return; + } + if (self::appSilver2coin() && self::pcSilver2coin()) { + self::$lock = time() + 24 * 60 * 60; + return; + } + self::$lock = time() + 3600; + } + + + // APP API + protected static function appSilver2coin(): bool + { + sleep(1); + $payload = []; + $raw = Curl::get('https://api.live.bilibili.com/AppExchange/silver2coin', Sign::api($payload)); + $de_raw = json_decode($raw, true); + + if (!$de_raw['code'] && $de_raw['msg'] == '兑换成功') { + Log::info('[APP]银瓜子兑换硬币: ' . $de_raw['msg']); + } elseif ($de_raw['code'] == 403) { + Log::info('[APP]银瓜子兑换硬币: ' . $de_raw['msg']); + } else { + Log::warning('[APP]银瓜子兑换硬币: ' . $de_raw['msg']); + return false; + } + return true; + } + + // PC API + protected static function pcSilver2coin(): bool + { + sleep(1); + $payload = []; + $url = "https://api.live.bilibili.com/pay/v1/Exchange/silver2coin"; + $url1 = "https://api.live.bilibili.com/exchange/silver2coin"; + + $raw = Curl::get($url, Sign::api($payload)); + $de_raw = json_decode($raw, true); + if ($de_raw['code'] == -403) { + return false; + } + Log::info('[PC]银瓜子兑换硬币: ' . $de_raw['msg']); + // TODO + return true; + } +} \ No newline at end of file diff --git a/src/Socket.php b/src/Socket.php new file mode 100644 index 0000000..2c54c49 --- /dev/null +++ b/src/Socket.php @@ -0,0 +1,193 @@ + time()) { + return; + } + self::$lock = time() + 0.5; + + self::start(); + $message = self::decodeMessage(); + if (!$message) { + unset($message); + self::resetConnection(); + return; + } + $data = DataTreating::socketJsonToArray($message); + if (!$data) { + return; + } + DataTreating::socketArrayToDispose($data); + return; + } + + // KILL + protected static function killConnection() + { + socket_clear_error(self::$socket_connection); + socket_shutdown(self::$socket_connection); + socket_close(self::$socket_connection); + self::$socket_connection = null; + } + + // RECONNECT + protected static function resetConnection() + { + $errorcode = socket_last_error(); + $errormsg = socket_strerror($errorcode); + unset($errormsg); + unset($errorcode); + self::killConnection(); +// Log::warning('SOCKET连接断开,5秒后重新连接...'); +// sleep(5); + self::start(); + return; + } + + // SOCKET READER + protected static function readerSocket(int $length) + { + return socket_read(self::$socket_connection, $length); + } + + // DECODE MESSAGE + protected static function decodeMessage() + { + $res = ''; + $tmp = ''; + while (1) { + while ($out = self::readerSocket(16)) { + $res = unpack('N', $out); + unset($out); + if ($res[1] != 16) { + break; + } + } + if (isset($res[1])) { + $length = $res[1] - 16; + if ($length > 65535) { + continue; + } + if ($length <= 0) { + return false; + } + return self::readerSocket($length); + } + return false; + } + } + + // START + protected static function start() + { + if (is_null(self::$socket_connection)) { + $room_id = empty(getenv('SOCKET_ROOM_ID')) ? Live::getUserRecommend() : Live::getRealRoomID(getenv('SOCKET_ROOM_ID')); + $room_id = intval($room_id); + if ($room_id) { + self::getSocketServer($room_id); + self::connectServer($room_id, self::$socket_ip, self::$socket_port); + } + } + self::sendHeartBeatPkg(); + return; + } + + // SEND HEART + protected static function sendHeartBeatPkg() + { + if (self::$heart_lock > time()) { + return; + } + $action_heart_beat = intval(getenv('ACTIONHEARTBEAT')); + $str = pack('NnnNN', 16, 16, 1, $action_heart_beat, 1); + socket_write(self::$socket_connection, $str, strlen($str)); + Log::info('发送心跳包到弹幕服务器!'); + self::$heart_lock = time() + 30; + return; + } + + // SOCKET CONNECT + protected static function connectServer($room_id, $ip, $port) + { + $falg = 10; + while ($falg) { + try { + $socket = socket_create(AF_INET, SOCK_STREAM, SOL_TCP); + socket_connect($socket, $ip, $port); + $str = self::packMsg($room_id); + socket_write($socket, $str, strlen($str)); + self::$socket_connection = $socket; + // TODO + Log::info("连接到弹幕服务器[{$room_id}]成功!"); + return; + } catch (\Exception $e) { + Log::info("连接到弹幕服务器[{$room_id}]失败!"); + Log::warning($e); + $falg -= 1; + } + } + Log::info("连接弹幕服务器[{$room_id}]错误次数过多,检查网络!"); + exit(); + } + + // PACK DATA + protected static function packMsg($room_id) + { + $action_entry = intval(getenv('ACTIONENTRY')); + $data = sprintf("{\"uid\":%d%08d,\"roomid\":%d}", + random_int(1000000, 2999999), + random_int(0, 99999999), + intval($room_id) + ); + return pack('NnnNN', 16 + strlen($data), 16, 1, $action_entry, 1) . $data; + } + + // GET SERVER + protected static function getSocketServer(int $room_id): bool + { + while (1) { + try { + $payload = [ + 'room_id' => $room_id, + ]; + $data = Curl::get('https://api.live.bilibili.com/room/v1/Danmu/getConf', Sign::api($payload)); + $data = json_decode($data, true); + + // TODO 判断 + if (isset($data['code']) && $data['code']) { + continue; + } + + self::$socket_ip = gethostbyname($data['data']['host']); + self::$socket_port = $data['data']['port']; + + break; + } catch (\Exception $e) { + Log::warning("获取弹幕服务器出错,错误信息[{$e}]!"); + continue; + } + } + return true; + } +} \ No newline at end of file diff --git a/src/Statistics.php b/src/Statistics.php new file mode 100644 index 0000000..5b62f38 --- /dev/null +++ b/src/Statistics.php @@ -0,0 +1,83 @@ + time()) { + return; + } + self::getStormResult(); + + self::$lock = time() + 5 * 60; + } + + + // 添加推送 + public static function addPushList(string $key): bool + { + // 初始化三个必要值 + if (!array_key_exists($key, self::$push_list)) { + self::$push_list[$key] = []; + self::$join_list[$key] = []; + self::$success_list[$key] = []; + } + + array_push(self::$push_list[$key], 1); + return true; + } + + // 添加参与 + public static function addJoinList(string $key): bool + { + array_push(self::$join_list[$key], 1); + return true; + } + + // 添加成功 + public static function addSuccessList(string $key): bool + { + array_push(self::$success_list[$key], 1); + return true; + } + + // 获取结果 + private static function getStormResult(): bool + { + if (empty(self::$push_list)) { + return false; + } else { + Log::info("-----------密----------封---------线------------"); + } + foreach (self::$push_list as $key => $val) { + $title = $key; + $push_num = count(self::$push_list[$key]); + $join_num = count(self::$join_list[$key]); + $success_num = count(self::$success_list[$key]); + $content = "{$title}: #推送 {$push_num} #参与 {$join_num} #成功 {$success_num}"; + Log::notice($content); + } + Log::info("-----------密----------封---------线------------"); + return true; + } +} \ No newline at end of file diff --git a/src/Storm.php b/src/Storm.php new file mode 100644 index 0000000..0caec65 --- /dev/null +++ b/src/Storm.php @@ -0,0 +1,199 @@ + time()) { + return; + } + self::init(); + self::startLottery(); + } + + private static function init() + { + if (!is_null(self::$attempt) && !is_null(self::$drop_rate)) { + return; + } + // TODO 信息不完整, 请检查配置文件 没有严格校验 + self::$drop_rate = getenv('STORM_DROPRATE') !== "" ? (int)getenv('STORM_DROPRATE') : 0; + self::$attempt = getenv('STORM_ATTEMPT') !== "" ? explode(',', getenv('STORM_ATTEMPT')) : [30, 50]; + } + + /** + * 抽奖逻辑 + * @return bool + * @throws \Exception + */ + protected static function startLottery(): bool + { + while (true) { + $storm = array_shift(self::$wait_list); + if (is_null($storm)) { + break; + } + // 丢弃 + if (mt_rand(1, 100) <= (int)self::$drop_rate) { + break; + } + $storm_lid = $storm['lid']; + $storm_rid = $storm['rid']; + Live::goToRoom($storm_rid); + Statistics::addJoinList(self::KEY); + $num = random_int((int)self::$attempt[0], (int)self::$attempt[1]); + for ($i = 1; $i < $num; $i++) { + if (!self::lottery($storm_rid, $storm_lid, $i)) { + break; + } + } + } + return true; + } + + /** + * 格式化日志输出 + * @param $id + * @param $num + * @param $info + * @return string + */ + private static function formatInfo($id, $num, $info): string + { + return "风暴 {$id} 请求 {$num} 状态 {$info}"; + } + + /** + * 请求抽奖 + * @param $rid + * @param $lid + * @param $num + * @return bool + */ + protected static function lottery($rid, $lid, $num): bool + { + $user_info = User::parseCookies(); + $payload = [ + "id" => $lid, + "color" => "16772431", + "roomid" => $rid, + "captcha_token" => "", + "captcha_phrase" => "", + "token" => $user_info['token'], + "csrf_token" => $user_info['token'], + "visit_id" => "", + ]; + $raw = Curl::post('https://api.live.bilibili.com/lottery/v1/Storm/join', Sign::api($payload)); + $de_raw = json_decode($raw, true); + if ($de_raw['code'] == 429 || $de_raw['code'] == -429) { + Log::notice(self::formatInfo($lid, $num, '节奏风暴未实名或异常验证码')); + return false; + } + if (isset($de_raw['data']) && empty($de_raw['data'])) { + Log::notice(self::formatInfo($lid, $num, '节奏风暴在小黑屋')); + return false; + } + if ($de_raw['code'] == 0) { + Statistics::addSuccessList(self::KEY); + Log::notice(self::formatInfo($lid, $num, $de_raw['data']['mobile_content'])); + return false; + } + if ($de_raw['msg'] == '节奏风暴不存在') { + Log::notice(self::formatInfo($lid, $num, '节奏风暴已结束')); + return false; + } + if ($de_raw['msg'] == '已经领取奖励') { + Log::notice(self::formatInfo($lid, $num, '节奏风暴已经领取')); + return false; + } + if ($de_raw['msg'] == '你错过了奖励,下次要更快一点哦~') { + return true; + } + Log::notice(self::formatInfo($lid, $num, $de_raw['msg'])); + return true; + } + + /** + * 检查ID + * @param $room_id + * @return array|bool + */ + protected static function stormCheckId($room_id) + { + $raw = Curl::get('https://api.live.bilibili.com/lottery/v1/Storm/check?roomid=' . $room_id); + $de_raw = json_decode($raw, true); + + if (empty($de_raw['data']) || $de_raw['data']['hasJoin'] != 0) { + return false; + } + return [ + 'id' => $de_raw['data']['id'], + ]; + } + + + /** + * 重复检测 + * @param $lid + * @return bool + */ + private static function toRepeatLid($lid): bool + { + if (in_array($lid, self::$all_list)) { + return true; + } + if (count(self::$all_list) > 2000) { + self::$all_list = []; + } + array_push(self::$all_list, $lid); + + return false; + } + + /** + * 数据推入队列 + * @param array $data + * @return bool + */ + public static function pushToQueue(array $data): bool + { + if (getenv(self::SWITCH) == 'false') { + return false; + } + if (self::toRepeatLid($data['lid'])) { + return false; + } + Statistics::addPushList(self::KEY); + self::$wait_list = array_merge(self::$wait_list, [['rid' => $data['rid'], 'lid' => $data['lid']]]); + $wait_num = count(self::$wait_list); + if ($wait_num > 2) { + Log::info("当前队列中共有 {$wait_num} 个" . self::KEY . "待抽奖"); + } + return true; + } +} diff --git a/src/Task.php b/src/Task.php new file mode 100644 index 0000000..4381c6f --- /dev/null +++ b/src/Task.php @@ -0,0 +1,101 @@ + time()) { + return; + } + + Log::info('正在检查每日任务...'); + + $data = self::check(); + + if (isset($data['data']['double_watch_info'])) { + self::double_watch_info($data['data']['double_watch_info']); + } + if (isset($data['data']['sign_info'])) { + self::sign_info($data['data']['sign_info']); + } + + self::$lock = time() + 3600; + } + + protected static function check() + { + $payload = []; + $data = Curl::get('https://api.live.bilibili.com/i/api/taskInfo', Sign::api($payload)); + $data = json_decode($data, true); + + if (isset($data['code']) && $data['code']) { + Log::warning('每日任务检查失败!', ['msg' => $data['message']]); + } + + return $data; + } + + protected static function sign_info($info) + { + Log::info('检查任务「每日签到」...'); + + if ($info['status'] == 1) { + Log::notice('该任务已完成'); + return; + } + + $payload = []; + $data = Curl::get('https://api.live.bilibili.com/appUser/getSignInfo', Sign::api($payload)); + $data = json_decode($data, true); + + if (isset($data['code']) && $data['code']) { + Log::warning('签到失败', ['msg' => $data['message']]); + } else { + Log::info('签到成功'); + // 推送签到信息 + Notice::run('todaySign', $data['msg']); + } + } + + protected static function double_watch_info($info) + { + Log::info('检查任务「双端观看直播」...'); + + if ($info['status'] == 2) { + Log::notice('已经领取奖励'); + return; + } + + if ($info['mobile_watch'] != 1 || $info['web_watch'] != 1) { + Log::notice('任务未完成,请等待'); + return; + } + + $payload = [ + 'task_id' => 'double_watch_task', + ]; + $data = Curl::post('https://api.live.bilibili.com/activity/v1/task/receive_award', Sign::api($payload)); + $data = json_decode($data, true); + + if (isset($data['code']) && $data['code']) { + Log::warning("「双端观看直播」任务奖励领取失败,{$data['message']}!"); + } else { + Log::info('奖励领取成功!'); + foreach ($info['awards'] as $vo) { + Log::notice(sprintf("获得 %s × %d", $vo['name'], $vo['num'])); + } + } + } +} diff --git a/src/TcpClinet.php b/src/TcpClinet.php new file mode 100644 index 0000000..c62b767 --- /dev/null +++ b/src/TcpClinet.php @@ -0,0 +1,253 @@ + time() || getenv('USE_SERVER') == 'false') { + return; + } + self::init(); + self::heartBeat(); + self::receive(); + } + + + /** + * @desc 初始化 + */ + private static function init() + { + if (empty(getenv('SERVER_ADDR')) || empty(getenv('SERVER_KEY'))) { + exit('推送服务器信息不完整, 请检查配置文件!'); + } + if (!self::$server_addr || !self::$server_key) { + self::$server_addr = getenv('SERVER_ADDR'); + self::$server_key = getenv('SERVER_KEY'); + } + if (!self::$client) { + self::openConnect(); + } + } + + + /** + * @desc 数据封装 + * @param $value + * @param $fmt + * @return string + */ + private static function packMsg($value, $fmt = "N") + { + $head = pack($fmt, strlen($value)); + $data = $head . $value; + return $data; + } + + /** + * @desc 数据解包 + * @param $value + * @param string $fmt + * @return array|false + */ + private static function unPackMsg($value, $fmt = "N") + { + $data = unpack($fmt, $value); + return $data[1]; + } + + /** + * @desc 连接认证 + */ + private static function handShake() + { + self::writer( + json_encode([ + 'code' => 0, + 'type' => 'ask', + 'data' => [ + 'key' => self::$server_key, + ] + ]) + ); + } + + /** + * @desc 心跳 + */ + private static function heartBeat() + { + if (self::$heart_lock <= time()) { + if (self::writer("")) { + // 心跳默认35s 调整数据错开错误 + self::$heart_lock = time() + 20; + } + } + } + + /** + * @desc 读数据 + * @param $length + * @return array|bool|false + */ + private static function reader($length) + { + $data = false; + try { + $data = self::$client->read($length); + if (!$data) { + throw new Exception("Connection failure"); + } + if ($length == 4) $data = self::unPackMsg($data); + } catch (Exception $exception) { + self::reConnect(); + } + return $data; + } + + /** + * @desc 写数据 + * @param $data + * @return bool + */ + private static function writer($data) + { + $status = false; + try { + $data = self::packMsg($data); + $status = self::$client->write($data); + } catch (Exception $exception) { + self::reConnect(); + } + return $status; + } + + + /** + * @desc 打开连接 + */ + private static function openConnect() + { + if (!self::$client) { + try { + $socket = (new Factory())->createClient(self::$server_addr, 40); + self::$client = $socket; + self::handShake(); + Log::info("连接到 {$socket->getPeerName()} 推送服务器"); + } catch (Exception $e) { + Log::error("连接到推送服务器失败, {$e->getMessage()}"); + self::$lock = time() + 60; + } + } + } + + /** + * @desc 重新连接 + */ + private static function reConnect() + { + Log::info('重新连接到推送服务器'); + self::closeConnect(); + self::openConnect(); + } + + /** + * @desc 断开连接 + */ + private static function closeConnect() + { + Log::info('断开推送服务器'); + try { + self::$client->shutdown(); + self::$client->close(); + } catch (Exception $exception) { + // var_dump($exception); + } + self::$client = null; + } + + + /** + * @use 读取数据 + */ + private static function receive() + { + $len_body = self::reader(4); + if (!$len_body) { + // 长度为0 ,空信息 + return; + } + Log::debug("(len=$len_body)"); + $body = self::reader($len_body); + $raw_data = json_decode($body, true); + // 人气值(或者在线人数或者类似)以及心跳 + $data_type = $raw_data['type']; + switch ($data_type) { + case 'raffle': + // 抽奖推送 + // Log::notice($body); + DataTreating::distribute($raw_data['data']); + break; + case 'entered': + // 握手确认 + Log::info("确认到推送服务器 {$raw_data['type']}"); + break; + case 'error': + // 致命错误 + Log::warning($body); + exit("推送服务器发生致命错误 {$raw_data['data']['msg']}"); + break; + case 'heartbeat': + // 服务端心跳推送 + // Log::info("推送服务器心跳推送 {$body}"); + Log::debug("(heartbeat={$raw_data['data']['now']})"); + break; + default: + // 未知信息 + var_dump($raw_data); + Log::info("出现未知信息 {$body}"); + break; + } + } + + + /** + * @use 写入log + * @param $message + */ + private static function writeLog($message) + { + $path = './danmu/'; + if (!file_exists($path)) { + mkdir($path); + chmod($path, 0777); + } + $filename = $path . getenv('APP_USER') . ".log"; + $date = date('[Y-m-d H:i:s] '); + $data = "[{$date}]{$message}" . PHP_EOL; + file_put_contents($filename, $data, FILE_APPEND); + return; + } +} \ No newline at end of file diff --git a/src/User.php b/src/User.php new file mode 100644 index 0000000..2208f93 --- /dev/null +++ b/src/User.php @@ -0,0 +1,84 @@ + Live::getMillisecond(), + ]; + $raw = Curl::get('https://api.live.bilibili.com/User/getUserInfo', Sign::api($payload)); + $de_raw = json_decode($raw, true); + if ($de_raw['msg'] == 'ok') { + if ($de_raw['data']['vip'] || $de_raw['data']['svip']) { + return true; + } + } + return false; + } + + // 用户名写入 + public static function userInfo(): bool + { + $payload = [ + 'ts' => Live::getMillisecond(), + ]; + $raw = Curl::get('https://api.live.bilibili.com/User/getUserInfo', Sign::api($payload)); + $de_raw = json_decode($raw, true); + + if (getenv('APP_UNAME') != "") { + return true; + } + if ($de_raw['msg'] == 'ok') { + File::writeNewEnvironmentFileWith('APP_UNAME', $de_raw['data']['uname']); + return true; + } + return false; + } + + //转换信息 + public static function parseCookies(): array + { + $cookies = getenv('COOKIE'); + preg_match('/bili_jct=(.{32})/', $cookies, $token); + $token = isset($token[1]) ? $token[1] : ''; + preg_match('/DedeUserID=(\d+)/', $cookies, $uid); + $uid = isset($uid[1]) ? $uid[1] : ''; + preg_match('/DedeUserID__ckMd5=(.{16})/', $cookies, $sid); + $sid = isset($sid[1]) ? $sid[1] : ''; + return [ + 'token' => $token, + 'uid' => $uid, + 'sid' => $sid, + ]; + } +} \ No newline at end of file diff --git a/src/Websocket.php b/src/Websocket.php new file mode 100644 index 0000000..2fcae1a --- /dev/null +++ b/src/Websocket.php @@ -0,0 +1,217 @@ + time()) { + return; + } + static::init(); + static::heart(); + static::receive(); + + } + + + /** + * @use 初始化 + */ + protected static function init() + { + if (!static::$websocket) { + $client = new Client( + 'ws://broadcastlv.chat.bilibili.com:2244/sub', + 'http://live.bilibili.com' + ); + static::$websocket = $client; + } + + if (!static::$room_id) { + static::$room_id = empty(getenv('SOCKET_ROOM_ID')) ? Live::getUserRecommend() : Live::getRealRoomID(getenv('SOCKET_ROOM_ID')); + } + return; + } + + /** + * @use 连接 + */ + protected static function connect() + { + Log::info("连接弹幕服务器"); + if (!static::$websocket->connect()) { + Log::error('连接弹幕服务器失败'); + static::$lock = time() + 60; + return; + } + static::$websocket->sendData( + static::packMsg(json_encode([ + 'uid' => 0, + 'roomid' => static::$room_id, + 'protover' => 1, + 'platform' => 'web', + 'clientver' => '1.4.1', + ]), 0x0007) + ); + } + + /** + * @use 断开连接 + */ + protected static function disconnect() + { + Log::info('断开弹幕服务器'); + static::$websocket->disconnect(); + } + + + /** + * @use 读取数据 + */ + protected static function receive() + { + $responses = static::$websocket->receive(); + if (is_array($responses) && !empty($responses)) { + foreach ($responses as $response) { + static::split($response->getPayload()); + } + static::receive(); + } + + } + + /** + * @use 发送心跳 + */ + protected static function heart() + { + if (!static::$websocket->isConnected()) { + static::connect(); + return; + } + if (static::$heart_lock <= time()) { + if (static::$websocket->sendData(static::packMsg('', 0x0002))) { + static::$heart_lock = time() + 30; + } + } + return; + } + + + /** + * @param $id + * @return mixed|string + */ + protected static function type($id) + { + $option = [ + 0x0002 => 'WS_OP_HEARTBEAT', + 0x0003 => 'WS_OP_HEARTBEAT_REPLY', + 0x0005 => 'WS_OP_MESSAGE', + 0x0007 => 'WS_OP_USER_AUTHENTICATION', + 0x0008 => 'WS_OP_CONNECT_SUCCESS', + ]; + return isset($option[$id]) ? $option[$id] : "WS_OP_UNKNOW($id)"; + } + + + /** + * @param $bin + * @throws \Exception + */ + protected static function split($bin) + { + if (strlen($bin)) { + $head = unpack('Npacklen/nheadlen/nver/Nop/Nseq', substr($bin, 0, 16)); + $bin = substr($bin, 16); + + $length = isset($head['packlen']) ? $head['packlen'] : 16; + $type = isset($head['op']) ? $head['op'] : 0x0000; + $body = substr($bin, 0, $length - 16); + + Log::debug(static::type($type) . " (len=$length)"); + + if (($length - 16) > 65535 || ($length - 16) < 0) { + Log::notice("长度{$length}异常,重新连接服务器!"); + if (static::$websocket->isConnected()) { + static::disconnect(); + } + if (!static::$websocket->isConnected()) { + static::connect(); + } + return; + } + + if ($type == 0x0005 || $type == 0x0003) { + if ($head['ver'] == 2) { + $body = gzuncompress($body); + if (substr($body, 0, 1) != '{') { + static::split($bin); + return; + } + } + DataTreating::socketJsonToArray($body); + } + + $bin = substr($bin, $length - 16); + if (strlen($bin)) { + static::split($bin); + } + } + return; + } + + + /** + * @param $value + * @param $option + * @return string + * @throws \Exception + */ + protected static function packMsg($value, $option) + { + $head = pack('NnnNN', 0x10 + strlen($value), 0x10, 0x01, $option, 0x0001); + $str = $head . $value; + static::split($str); + return $str; + } + + /** + * @use 写入log + * @param $message + */ + private static function writeLog($message) + { + $path = './danmu/'; + if (!file_exists($path)) { + mkdir($path); + chmod($path, 0777); + } + $filename = $path . getenv('APP_USER') . ".log"; + $date = date('[Y-m-d H:i:s] '); + $data = "[{$date}]{$message}" . PHP_EOL; + file_put_contents($filename, $data, FILE_APPEND); + return; + } +} \ No newline at end of file diff --git a/src/Winning.php b/src/Winning.php new file mode 100644 index 0000000..dd13788 --- /dev/null +++ b/src/Winning.php @@ -0,0 +1,68 @@ + time()) { + return; + } + self::$lock = time() + 24 * 60 * 60; + + $payload = [ + 'page' => '1', + 'month' => '', + ]; + $raw = Curl::post('https://api.live.bilibili.com/lottery/v1/award/award_list', Sign::api($payload)); + $de_raw = json_decode($raw, true); + $month = $de_raw['data']['month_list'][0]['Ym']; + + // TODO + $payload = [ + 'page' => '1', + 'month' => $month, + ]; + $raw = Curl::post('https://api.live.bilibili.com/lottery/v1/award/award_list', Sign::api($payload)); + $de_raw = json_decode($raw, true); + // 没有记录 + if (empty($de_raw['data']['list'])) { + return; + } + + $init_time = strtotime(date("y-m-d h:i:s")); //当前时间 + foreach ($de_raw['data']['list'] as $gift) { + $next_time = strtotime($gift['create_time']); //礼物时间 + $day = ceil(($init_time - $next_time) / 86400); //60s*60min*24h + + if ($day <= 2 && $gift['update_time'] == '') { + $data_info = '您在: ' . $gift['create_time'] . '抽中[' . $gift['gift_name'] . 'X' . $gift['gift_num'] . ']未查看!'; + Log::notice($data_info); + // TODO 推送 log + } + } + return; + } +} \ No newline at end of file