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 |舰长上船亲密度 |
+
+## 打赏赞助
+
+
+
+
+## 未完成功能
+
+|待续 |
+|-----------|
+|添加多用户 |
+|待添加 |
+
+## 环境依赖
+
+|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/
+```
+
+## 打赏赞助
+
+
+
+> 待添加
+
+## 使用指南
+
+ 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)
+
+## 打赏
+
+
+
+## 效果
+
+
+
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