Compare commits
187 Commits
android1.0
...
main
Author | SHA1 | Date |
---|---|---|
Easy | b8fa724a9a | |
Easy | 4ce68572b9 | |
Easy | 16aec6c468 | |
Easy | bfadc52fef | |
Easy | d7f3279458 | |
Easy | f5841403c7 | |
Easy | be51654558 | |
Easy | aa9a2a9dfa | |
Easy | 1a9ad9e351 | |
Easy | 19011afdd0 | |
Easy | f9779893ec | |
Easy | 09acc7955e | |
Chi | 6053cf624a | |
Easy | d15892727a | |
Easy | b5e6605328 | |
Easy | 8e5292acfb | |
hext | ed61f49eca | |
Easy | 6927a94860 | |
Easy | e5d715e06d | |
Easy | 3fb92270f3 | |
Easy | 1ca157b32e | |
Easy | fd901b04c8 | |
Easy | c706d83e2f | |
Easy | 034e93d3c7 | |
Easy | a218d5c68d | |
Easy | 416cc716dd | |
Easy | d504a91dc4 | |
Easy | 20697f77dd | |
Harry Cheng | 4673418d95 | |
Harry Cheng | c6caf87a0a | |
Harry Cheng | 2b89ee699e | |
EasyChen | d7a0c95382 | |
Easy | 2f461cf8c6 | |
Easy | b874100b17 | |
Easy | a0d07f4362 | |
Easy | c2283e50f9 | |
Easy | 4c8b70310e | |
Easy | 57e467b2e3 | |
Easy | 6b097973c2 | |
Easy | af112c230c | |
Easy | 3c019e61f8 | |
Easy | 914bd7ac1a | |
Easy | 3878706c9f | |
Easy | 628b67191d | |
Easy | fe15fa7593 | |
Easy | 2ea7fa6c22 | |
Easy | ed3c753f6b | |
Easy | 38162f0cbe | |
hext | 65e2e729fc | |
Easy | 05d051dc8b | |
Easy | 411f768156 | |
Easy | 6d2b30901b | |
hext | c3d0cd13f5 | |
Easy | a685433be3 | |
Easy | 0808c2257c | |
Easy | 4fbf408eae | |
Easy | 91c2d64295 | |
Easy | 828f6f5454 | |
Easy | 331daec61a | |
Easy | 082ed2d3d2 | |
Easy | c3bb7474bd | |
Easy | 80c034ef44 | |
Easy | 722ba0eb9e | |
Easy | 654dc50155 | |
Easy | 55de3ebf71 | |
Easy | c1a3b746b7 | |
Easy | 6ed2d8d458 | |
hext | 69ec4f3774 | |
Easy | 0ec3bbc7cc | |
Easy | 3fa3d73859 | |
Easy | afa5bf35be | |
Easy | f208c5b314 | |
Easy | 981bc6b7f7 | |
Easy | b494dec36c | |
Easy | 78a91d642b | |
Easy | b97cb44a14 | |
Easy | b39c92c42d | |
Easy | 9afddbfddb | |
Easy | 6f1cd94234 | |
Easy | a570beaf3e | |
Easy | 967e1d4bcb | |
Easy | f7b574bccd | |
alone-wolf | 905ff7c8fe | |
Easy | 94d15420f7 | |
Easy | a9ec7adfb0 | |
Easy | 987da61003 | |
Easy | 6e97c2f51a | |
Easy | bf561050b5 | |
Easy | b7c7ea7dd1 | |
hext | 3f61db008f | |
Easy | 8fbedcc5ce | |
Easy | d58f6da266 | |
hext | 2136883536 | |
Easy | 2310265c77 | |
Easy | 7677c81ce8 | |
Easy | 9114bc6879 | |
7YHong | 8304b2a0f9 | |
Easy | 000ca0b73c | |
Easy | b7584dd371 | |
Easy | fb7d115c6b | |
Easy | ec470e3728 | |
7YHong | 7bcea9c899 | |
Easy | 4f5441a982 | |
7YHong | 37021ea19b | |
Easy | b68dc9abcd | |
Easy | e1f33c7a63 | |
Easy | d1f1940bd2 | |
Easy | 9f669714b2 | |
Easy | 6db248e2b8 | |
Easy | bb1025e1ef | |
Easy | 5589c3d361 | |
Easy | 660e786984 | |
Easy | 2c8cb03fac | |
Easy | 825b7893b7 | |
Easy | ec2a968aae | |
Easy | a3cc2f4fd3 | |
Easy | ff1dd0c302 | |
Easy | c0980b484c | |
Easy | 825273b07e | |
Easy | 7ce01d4a9d | |
Easy | 3768506949 | |
Easy | 7488c5ae44 | |
Easy | c7da8e5230 | |
Easy | 123ff5cce3 | |
Easy | c19b8996c9 | |
Easy | 7e47762ba1 | |
Easy | 619bec20ac | |
Easy | 630d479826 | |
Easy | ab15e50950 | |
Easy | c938f02de1 | |
Easy | ff7325ccd3 | |
hext | d3050e154f | |
Easy | 19d8e259d8 | |
Easy | 555b1bc989 | |
Easy | a5bb9f6cc8 | |
Easy | cd30839006 | |
Easy | ed7d256527 | |
Easy | 328704491a | |
Easy | 0180a67b54 | |
7YHong | 03f0e531ad | |
Easy | 4232570aba | |
Easy | 1fd0943f91 | |
Easy | 68a4f40f5a | |
Easy | 4c15f4226d | |
Easy | 8f76578aa6 | |
Easy | 22e0378d83 | |
Easy | d1d4566152 | |
Easy | e0b359f2a1 | |
Easy | ea29e2314b | |
SinTod | c19d75f2e7 | |
Easy | 84cb7c8c93 | |
SinTod | 9104131c6f | |
SinTod | 242335a986 | |
Easy | 74cbc0e888 | |
SinTod | ea79289d43 | |
Easy | 16ccf6c247 | |
Easy | b197b6fb23 | |
Easy | 782acfa53a | |
Easy | 02a81111e2 | |
Easy | b790dfd359 | |
Easy | ae6adf3349 | |
Easy | 2534c389c3 | |
Easy | b325398302 | |
EasyChen | d1571502b5 | |
EasyChen | e61aaa01b1 | |
EasyChen | 59649f6b50 | |
EasyChen | 354cff4850 | |
EasyChen | 5501101e35 | |
EasyChen | 45f1a3f16f | |
EasyChen | a5b8fe9b97 | |
Easy | f6b015ce67 | |
EasyChen | f257830b49 | |
Easy | 5467106425 | |
EasyChen | 451334d428 | |
EasyChen | 623321f217 | |
EasyChen | 9d7654c110 | |
EasyChen | 17d0f894f2 | |
Easy | 77d558d5a7 | |
Easy | 24ee83ea49 | |
hext | 11864a1cb3 | |
hext | aa348feace | |
Easy | aa796f8d0e | |
Easy | b9c5b237b0 | |
Easy | 32cad861c3 | |
EasyChen | 3e0cc65957 | |
Easy | ef14fa680a | |
Easy | d5c198eba6 |
|
@ -0,0 +1,58 @@
|
|||
**描述你遇到的Bug**
|
||||
|
||||
请用精确的语言描述你的遇到的问题,不要用「不能用」,「报错」等模糊字眼,要提供具体的信息,如报错信息是什么,最好能提供截图
|
||||
|
||||
以下内容不是必填,但越详细越能帮助我们解决问题。
|
||||
|
||||
**重现**
|
||||
|
||||
重现步骤:
|
||||
|
||||
1. Go to '...'
|
||||
2. Click on '....'
|
||||
3. Scroll down to '....'
|
||||
4. See error
|
||||
|
||||
**期望的结果**
|
||||
|
||||
**截图**
|
||||
|
||||
**自架服务器端请填写以下信息**
|
||||
|
||||
按 [调试文档](/doc/调试文档.md) 进行调试的结果:
|
||||
|
||||
**API服务启动输出截图**
|
||||
|
||||
**联通性测试截图**
|
||||
|
||||
**命令行推送返回截图**
|
||||
|
||||
|
||||
**客户端请填写以下信息**
|
||||
|
||||
**环境和版本**
|
||||
- 系统: [如 Mac、iOS、Android、ESP]
|
||||
- 版本: [如自架版1.2,官方版1.2]
|
||||
- 模块:[如客户端、服务器端、Docker-compose、Docker独立镜像]
|
||||
- 版本号 [如 22]
|
||||
|
||||
**手机或电脑的的详细信息**
|
||||
- Device: [e.g. iPhone6]
|
||||
- OS: [e.g. iOS8.1]
|
||||
- Browser [e.g. stock browser, safari]
|
||||
- Version [e.g. 22]
|
||||
|
||||
**收不到消息请填写**
|
||||
- 是否注册设备: [是/否]
|
||||
- 是否注册当前设备: [是/否]( 设备页面有设备名称后显示「当前设备」 )
|
||||
- 是否开通通知权限: [是/否]
|
||||
- 是否开重装过应用: [是/否]
|
||||
- APP和轻APP(Clip)是否分别都注册过设备: [是/否]
|
||||
|
||||
- 注册设备页面截图
|
||||
- 应用通知设置页面截图
|
||||
|
||||
> 大部分用户在尝试删除重装应用后,可以修复问题,您也可以试试
|
||||
|
||||
**其他说明**
|
||||
|
|
@ -0,0 +1,5 @@
|
|||
# These are supported funding model platforms
|
||||
|
||||
github: [easychen]
|
||||
|
||||
|
|
@ -0,0 +1,67 @@
|
|||
---
|
||||
name: Bug报告
|
||||
about: 新建Bug报告
|
||||
title: "[Bug]"
|
||||
labels: ''
|
||||
assignees: ''
|
||||
|
||||
---
|
||||
|
||||
**描述你遇到的Bug**
|
||||
|
||||
请用精确的语言描述你的遇到的问题,不要用「不能用」,「报错」等模糊字眼,要提供具体的信息,如报错信息是什么,最好能提供截图
|
||||
|
||||
以下内容不是必填,但越详细越能帮助我们解决问题。
|
||||
|
||||
**重现**
|
||||
|
||||
重现步骤:
|
||||
|
||||
1. Go to '...'
|
||||
2. Click on '....'
|
||||
3. Scroll down to '....'
|
||||
4. See error
|
||||
|
||||
**期望的结果**
|
||||
|
||||
**截图**
|
||||
|
||||
**自架服务器端请填写以下信息**
|
||||
|
||||
按 [调试文档](/doc/调试文档.md) 进行调试的结果:
|
||||
|
||||
**API服务启动输出截图**
|
||||
|
||||
**联通性测试截图**
|
||||
|
||||
**命令行推送返回截图**
|
||||
|
||||
|
||||
**客户端请填写以下信息**
|
||||
|
||||
**环境和版本**
|
||||
- 系统: [如 Mac、iOS、Android、ESP]
|
||||
- 版本: [如自架版1.2,官方版1.2]
|
||||
- 模块:[如客户端、服务器端、Docker-compose、Docker独立镜像]
|
||||
- 版本号 [如 22]
|
||||
|
||||
**手机或电脑的的详细信息**
|
||||
- Device: [e.g. iPhone6]
|
||||
- OS: [e.g. iOS8.1]
|
||||
- Browser [e.g. stock browser, safari]
|
||||
- Version [e.g. 22]
|
||||
|
||||
**收不到消息请填写**
|
||||
- 是否注册设备: [是/否]
|
||||
- 是否注册当前设备: [是/否]( 设备页面有设备名称后显示「当前设备」 )
|
||||
- 是否开通通知权限: [是/否]
|
||||
- 是否开重装过应用: [是/否]
|
||||
- APP和轻APP(Clip)是否分别都注册过设备: [是/否]
|
||||
|
||||
- 注册设备页面截图
|
||||
- 应用通知设置页面截图
|
||||
|
||||
> 大部分用户在尝试删除重装应用后,可以修复问题,您也可以试试
|
||||
|
||||
**其他说明**
|
||||
|
|
@ -1 +1,8 @@
|
|||
.DS_Store
|
||||
.DS_Store
|
||||
|
||||
android/app_self*
|
||||
android/pushdeerclient
|
||||
android/app/java/com/pushdeer/os/values/App*
|
||||
android/app/build
|
||||
android/app/debug
|
||||
android/app/release
|
||||
|
|
486
README.md
|
@ -1,62 +1,224 @@
|
|||
# PushDeer
|
||||
> ⚠️ 目前,官方架设的Android版本因接口权限停止无法使用,[详情请点击](https://github.com/easychen/pushdeer/issues/150)
|
||||
|
||||
PushDeer开源版,可以自行架设的无APP推送服务(WIP,API、iOS和Mac第一版本完成,Android即将完成,其他部分正在施工🚧)
|
||||
> ⚠️ 自架版服务器端需每年更新推送证书,如果之前架设的服务突然无法收到推送,请尝试拉取部署最新代码,或者[手动更新证书](https://github.com/easychen/pushdeer/tree/main/push)
|
||||
|
||||
[🐙🐱 GitHub仓库](https://github.com/easychen/pushdeer) [🔮 中国大陆镜像仓库](https://gitee.com/easychen/pushdeer)
|
||||
PushDeer是一个可以自行架设的无APP推送服务,同时也为因为某些原因无法使用无APP推送方案的同学提供有APP/自制设备方案。
|
||||
|
||||
[🐙🐱 GitHub仓库](https://github.com/easychen/pushdeer) [🔮 中国大陆镜像仓库@Gitee](https://gitee.com/easychen/pushdeer)
|
||||
|
||||
本项目已经实现的方案/端包括:
|
||||
|
||||
- 无APP方案:
|
||||
- 轻APP(APP Clip)
|
||||
- 快应用
|
||||
- 有APP方案:
|
||||
- iOS客户端
|
||||
- Mac客户端
|
||||
- Android客户端
|
||||
- 自制设备方案:
|
||||
- DeerESP(ESP8266/ESP32)
|
||||
|
||||
![](iot/image/deeresp.gif)
|
||||
|
||||
👉[点此查看如何将PushDeer消息推送到成本不到40元的自制设备上](iot/README.md)
|
||||
|
||||
---
|
||||
|
||||
|登入|设备|Key|消息|设置|
|
||||
|-|-|-|-|-|
|
||||
|![](doc/design_and_resource/登入.png)|![](doc/design_and_resource/设备.png)|![](doc/design_and_resource/key.png)|![](doc/design_and_resource/消息.png)|![](doc/design_and_resource/设置.png)
|
||||
|
||||
[📼 无App推送使用演示视频](https://weibo.com/tv/show/1034:4714616840978534?from=old_pc_videoshow) [📼 项目视频说明](https://www.bilibili.com/video/BV1Ar4y1S7em/) [📼 项目架构和模块说明](https://www.bilibili.com/video/BV1ZS4y1T7Bf/)
|
||||
|
||||
|
||||
|
||||
## 一期功能核心贡献者
|
||||
# 子项目和核心贡献者
|
||||
|
||||
|功能|核心贡献人|预期完成时间|最低版本兼容|本周进度|
|
||||
|-|-|-|-|-|
|
||||
|iOS/MacApp+Clip开发|[Hext123](https://github.com/Hext123)|2022年1月20日|iOS15(14兼容修复中)|第一版完成,代码在iOS目录下|
|
||||
|Android客户端|[WolfHugo](https://github.com/alone-wolf)|2022年2月24日|5.1|界面细节调整中|
|
||||
|快应用|[7YHong](https://github.com/7YHong)|2022年2月27日|-|完成快应用界面展示部分,下周对接API|
|
||||
|API完善和更新|[古俊杰](https://github.com/ilovintit)|配合客户端同步更新|-|添加自动生成swgger文档功能中|
|
||||
|iOS客户端/Mac客户端/轻APP|[Hext123](https://github.com/Hext123)|已完成|iOS14|第一版完成,代码在iOS目录下|
|
||||
|Android客户端|[WolfHugo](https://github.com/alone-wolf)|已完成|5.1|第一版开发完成,release页面可下载。Websocket方案准备中|
|
||||
|快应用|[7YHong](https://github.com/7YHong)|已完成|-|代码在[quickapp目录下](quickapp),可自行上架|
|
||||
|物联网版本DeerESP|[Easy](https://ftqq.com) |已完成|-|代码在[iot目录下](iot),专用设备PCB和外壳设计中|
|
||||
|API|[Easy](https://ftqq.com) [古俊杰](https://github.com/ilovintit)|已完成|-|-|
|
||||
|gorush的mi push版本|[SinTod](https://www.sintod.cn/)|已完成|-|代码在[push/gorush-with-mipush目录下](push/gorush-with-mipush)|
|
||||
|
||||
## 试用
|
||||
|
||||
使用方法:
|
||||
|
||||
# 试用
|
||||
|
||||
![](doc/image/video.gif)
|
||||
|
||||
## 使用官方在线版本
|
||||
|
||||
官方在线版不用自行架设服务器端,只需启动客户端即可
|
||||
|
||||
### iOS14+
|
||||
|
||||
![](doc/image/clipcode.png)
|
||||
|
||||
1. 用苹果系统(iOS 15+)摄像头扫描上边的码
|
||||
1. 通过apple账号登录
|
||||
苹果手机(iOS 14+)用系统摄像头扫描上边的码即可拉起轻应用。亦可在苹果商店搜索「PushDeer」安装。
|
||||
|
||||
> 注意:这里不要安装PushDeer自架版
|
||||
|
||||
### MacOS 11+
|
||||
|
||||
PushDeer有Mac客户端,亦支持推送。可在Mac应用商店中搜索「PushDeer」安装。
|
||||
|
||||
### Android
|
||||
|
||||
快应用尚在开发,可下载并安装Android测试版APP([GitHub](https://github.com/easychen/pushdeer/releases/tag/android1.0alpha)|[Gitee](https://gitee.com/easychen/pushdeer/releases/android1.0alpha))。
|
||||
|
||||
|
||||
### 发送消息
|
||||
|
||||
1. 通过apple账号(或微信账号·仅Android版支持)登录
|
||||
1. 切换到「设备」标签页,点击右上角的加号,注册当前设备
|
||||
1. 切换到「Key」标签页,点击右上角的加号,创建一个Key
|
||||
1. 通过访问后边的URL即可推送内容:https://api2.pushdeer.com/message/push?pushkey=key&text=要发送的内容
|
||||
|
||||
iOS APP和Mac APP可以在苹果商店搜索安装。注意iOS APP、Mac APP和轻应用都被认为是不同的设备,都需要进行注册才能接收推送。
|
||||
> 注意注册设备用到了device token,应用一旦重装,device token会变,所以需要重新注册一次。
|
||||
|
||||
PS:系统设计最低支持版本为iOS14,但目前存在兼容性问题,有iOS14真机和苹果开发者证书的同学可以下载源码并编译(参考[这个文档](ios/PushDeer-iOS/README.md)),将报错信息提交到issue可以加速我们的修复时间。
|
||||
### 发送实例
|
||||
|
||||
发送文字:
|
||||
|
||||
```
|
||||
https://api2.pushdeer.com/message/push?pushkey=key&text=要发送的内容
|
||||
```
|
||||
|
||||
发送图片:
|
||||
|
||||
```
|
||||
https://api2.pushdeer.com/message/push?pushkey=<key>&text=<图片URL>&type=image
|
||||
```
|
||||
|
||||
发送Markdown:
|
||||
|
||||
```
|
||||
https://api2.pushdeer.com/message/push?pushkey=<key>&text=标题&desp=<markdown>&type=markdown
|
||||
```
|
||||
|
||||
在URL中可以用`%0A`换行,当参数中有特殊字符时,需要进行urlencode,因此更建议通过函数或者SDK发送。
|
||||
|
||||
- [Python SDK](https://github.com/gaoliang/pypushdeer) by [Gao Liang](https://github.com/gaoliang)
|
||||
- [Go SDK](https://github.com/Luoxin/go-pushdeer-sdk) by [Luoxin](https://github.com/Luoxin)
|
||||
- [Rust SDK](https://github.com/abgelehnt/rupushdeer) by [Chi](https://github.com/abgelehnt)
|
||||
|
||||
PHP函数:
|
||||
|
||||
```php
|
||||
function pushdeer_send($text, $desp = '', $type='text', $key = '[PUSHKEY]')
|
||||
{
|
||||
$postdata = http_build_query(array( 'text' => $text, 'desp' => $desp, 'type' => $type , 'pushkey' => $key ));
|
||||
$opts = array('http' =>
|
||||
array(
|
||||
'method' => 'POST',
|
||||
'header' => 'Content-type: application/x-www-form-urlencoded',
|
||||
'content' => $postdata));
|
||||
|
||||
$context = stream_context_create($opts);
|
||||
return $result = file_get_contents('https://api2.pushdeer.com/message/push', false, $context);
|
||||
}
|
||||
```
|
||||
|
||||
### 将 PushDeer 接入 ServerChan
|
||||
|
||||
由于 PushDeer 刚开发,很多软件和平台都尚未整合其接口,你可以将 PushDeer 接入Server酱作为通道使用,效果是:
|
||||
|
||||
1. 使用`sendkey`调用Server酱接口
|
||||
2. 在 PushDeer 客户端收到通知
|
||||
|
||||
接入方式如下:
|
||||
|
||||
1. 登入 sct.ftqq.com ,选择「消息通道」页面,选择「推荐通道」中的「PushDeer」
|
||||
1. 将在PushDeer客户端中生成的Key填入即可
|
||||
|
||||
![](doc/image/2022-02-02-21-39-16.png)
|
||||
|
||||
如果你使用的是自架服务器,那么也可以通过「其他通道」中的「自定义」来修改转发格式和文案。具体操作是,在「自定义 WebHook 配置用 json」中填入以下内容:
|
||||
|
||||
```json
|
||||
{
|
||||
"url":"<endpoint>/message/push?pushkey=<pushkey>",
|
||||
"values":[
|
||||
{"type":"markdown"},
|
||||
{"text":"{{title}} "},
|
||||
{"desp":"{{desp}} [查看详情]({{url}})"}
|
||||
]
|
||||
}
|
||||
```
|
||||
|
||||
注意将 `<endpoint>` 换成你自架服务器的地址,将 `{{pushkey}}` 换成你自己的 PushDeer 账号中的 key,保存即可。可点右侧的测试按钮测试效果。
|
||||
|
||||
## 使用自架服务器端和自架版客户端
|
||||
|
||||
### 自架服务器端
|
||||
|
||||
除了使用官方架设的服务器端,你也可以架设自己的服务器端。本仓库支持通过docker部署服务器端。
|
||||
|
||||
如果你没有云服务器,可以看看[腾讯云30~50元首单的特价服务器](https://curl.qcloud.com/VPjlS4gj)
|
||||
|
||||
#### Docker-compose
|
||||
|
||||
首先请确保服务器(假设其IP或域名为$AAA)上支持docker和docker-compose。
|
||||
|
||||
然后运行以下代码:
|
||||
|
||||
```
|
||||
git clone https://github.com/easychen/pushdeer.git
|
||||
cd pushdeer
|
||||
docker-compose -f docker-compose.self-hosted.yml up --build -d
|
||||
```
|
||||
|
||||
如果你的服务器连接GitHub有困难,可以使用Gitee的代码,但需要核对是否为最新版本(有可能没同步)
|
||||
```
|
||||
git clone https://gitee.com/easychen/pushdeer.git
|
||||
cd pushdeer
|
||||
docker-compose -f docker-compose.self-hosted.yml up --build -d
|
||||
```
|
||||
|
||||
> 如提示docker服务未安装/找不到/未启动,可在 docker-compose 前加 sudo 再试
|
||||
|
||||
等待初始化完成后,访问 `$AAA(需替换为服务器端IP或域名):8800`,看到扫码提示和图片则说明容器已经启动。
|
||||
|
||||
> ⚠️ 自架服务器端需每年2月拉取一次更新推送证书
|
||||
|
||||
如果您在部署中遇到问题,可按[调试文档](/doc/调试文档.md)定位并发现错误信息。
|
||||
|
||||
<!--
|
||||
#### 单一容器部署方案
|
||||
|
||||
对于很多不能运行docker-compose的容器环境,可以直接使用 pushdeer 镜像。该镜像中已经包含了 redis 服务,但需要通过环境变量指定数据库等信息:
|
||||
|
||||
```
|
||||
docker run -e DB_DATABASE=* -e DB_HOST=* -e DB_PORT=*28740* -e DB_USERNAME=* -e DB_PASSWORD=* -e DB_TIMEZONE=+08:00 -e WEB_PHP_SOCKET=127.0.0.1:8000 -p 9000:9000 ccr.ccs.tencentyun.com/ftqq/pushdeercore
|
||||
```
|
||||
|
||||
请将上述命令中的`*`替换为对应的数据库信息。
|
||||
-->
|
||||
|
||||
### 使用自架版客户端
|
||||
|
||||
![](doc/image/2022-02-02-21-45-29.png)
|
||||
|
||||
在苹果商店搜索「PushDeer·自架版」安装并启动。
|
||||
|
||||
![](doc/image/2022-02-02-21-14-21.png)
|
||||
|
||||
在启动界面输入 `$AAA(需替换为服务器端IP或域名):8800`,点保存。如果通信顺利,即可顺利完成「通过Apple登录」。如输入错误,可点击「重置API endpoint」重新输入。
|
||||
|
||||
![](doc/image/2022-02-02-21-17-45.png)
|
||||
|
||||
登入成功后,亦可随时在设置页面重置 API endpoint。
|
||||
|
||||
![](doc/image/2022-02-02-21-18-49.png)
|
||||
|
||||
|
||||
## FAQ
|
||||
### 生产环境的配置强化/优化
|
||||
|
||||
> 这个程序有什么用?
|
||||
参见[𐂂安装文档](doc/安装文档.md)
|
||||
|
||||
请参考本页面应用场景一节。
|
||||
|
||||
> 应用闪退怎么办?
|
||||
# 关于轻应用
|
||||
|
||||
iOS14 闪退问题正在定位,可参照上一段文字帮我们加快修复进度。
|
||||
|
||||
> 推送可以支持Mark和图片吗?
|
||||
|
||||
支持,请参考本页API说明一节中 message 部分.
|
||||
|
||||
> PushDeer 提供推送证书吗?
|
||||
|
||||
出于内容和安全考虑,官方APP不提供推送证书。待功能稳定后,我们会单独发布一个自架版APP/Clip并提供独立的推送证书,以免因为证书被滥用而影响到普通用户的使用。
|
||||
|
||||
> 轻APP找不到了怎么办?
|
||||
> 轻应用找不到了怎么办?
|
||||
|
||||
在搜索框搜索pushdeer就能找到。
|
||||
|
||||
|
@ -64,110 +226,12 @@ iOS14 闪退问题正在定位,可参照上一段文字帮我们加快修复
|
|||
|
||||
系统设置里边有一个轻应用管理,在里边可以清理。30天不用会自动清理掉。注意重新安装后设备id会变动,需要再手动注册一遍。
|
||||
|
||||
> 推送返回 code 0 result false ,但收不到推送
|
||||
# 开发说明
|
||||
|
||||
需要先注册当前设备。
|
||||
|
||||
> URL推送的文字如何换行?
|
||||
|
||||
URL中可以使用`%0A`作为换行符,如 `https://api2.pushdeer.com/message/push?pushkey=key&text=line1%0Aline2`
|
||||
|
||||
## 安装文档
|
||||
|
||||
- 𐂂 [点此查看](doc/安装文档.md)
|
||||
视频版设计文档: [📼 项目视频说明](https://www.bilibili.com/video/BV1Ar4y1S7em/) [📼 项目架构和模块说明](https://www.bilibili.com/video/BV1ZS4y1T7Bf/)
|
||||
|
||||
|
||||
## 相关项目
|
||||
|
||||
- [Android客户端临时仓库](https://github.com/alone-wolf/pushdeer_for_android)
|
||||
- [API的Go实现](https://github.com/iepngs/pushdeer-backend-go) by [iepngs](https://github.com/iepngs)
|
||||
- [Go SDK](https://github.com/Luoxin/go-pushdeer-sdk) by [Luoxin](https://github.com/Luoxin)
|
||||
|
||||
|
||||
## 产品定义
|
||||
|
||||
PushDeer的**核心价值**,包括:「易用」、「可控」和「渐进」。
|
||||
|
||||
### 易用
|
||||
|
||||
易用性表现在两个方面:
|
||||
|
||||
1. 易安装:采用无APP方案,直接**去掉安装步骤**
|
||||
1. 易调用:只需输入URL,**无需阅读文档**,就可以发送消息
|
||||
|
||||
### 可控
|
||||
|
||||
1. `Self-hosted`:让有能力和精力的用户可以自行架设,避免因为在线服务下线导致的接口更换风险。
|
||||
1. 非商用免费:不用PushDeer挣钱,就无需支付费用
|
||||
1. 不依赖微信消息接口:不像Server酱那样受腾讯政策影响
|
||||
|
||||
### 渐进
|
||||
|
||||
1. 通过URL即可发送基本的文本消息;通过更多参数,可以对消息的样式等细节进行调整
|
||||
1. 无APP不能实现的功能不能覆盖的机型,后期可以通过APP来补充
|
||||
|
||||
## 商业模式
|
||||
|
||||
PushDeer是一个商业开源项目,采用「开放源码」、「自用免费」、「在线服务收费」的方式进行运作。
|
||||
|
||||
### 具体实现
|
||||
|
||||
PushDeer是一个以盈利为目的的商业项目,品牌和源码所有权都由「方糖君」公司持有,但和纯商业项目不同的地方在于:
|
||||
|
||||
1. 它开放源代码,所有人都可以在非商业前提下按GPLv2授权使用
|
||||
1. 它接受社区贡献代码,作为回报,它会从商业收益中拿出部分来赞助项目贡献人
|
||||
1. 如果商业收益够大,它会尝试雇佣项目贡献人以兼职或者全职的方式为项目工作
|
||||
|
||||
这里边有一些细节:
|
||||
|
||||
1. 为了避免某些个人或者公司使用源码搭建在线竞品服务收费,我们限制了源码不能商用
|
||||
1. 在刚开始的时候,项目并没有商业收入,而却是开发工作量最大的。所以首先我们会承担产品和界面设计、API设计和开发等工作;并通过众筹的方式筹集了一些资金给其他大模块的贡献人
|
||||
|
||||
开放源码形式保证了其他代码贡献人在非商业场景下对源码的可控:
|
||||
|
||||
1. 如果社区和代码贡献人不满意「方糖君」主导的商业化,可以 Fork 一个版本,继续在非商用的前提下自行运营
|
||||
1. 如果「方糖君」之后不再开放源代码,普通用户依然可以按之前的协议使用修改协议前的源码
|
||||
|
||||
## 用户细分
|
||||
|
||||
PushDeer主要面向以下三类用户
|
||||
|
||||
1. 高阶电脑用户
|
||||
1. 开发者
|
||||
1. 公司或自媒体
|
||||
|
||||
### 高阶电脑用户
|
||||
|
||||
具有一定电脑操作技能的高阶用户,比如:
|
||||
|
||||
1. NAS 用户
|
||||
1. 站长
|
||||
1. 电脑技术爱好者
|
||||
|
||||
他们使用PushDeer的场景包括但不限于:
|
||||
|
||||
1. 推送路由器和 NAS 的状态、公网 IP 等信息
|
||||
1. 推送 Wordpress 最新的评论
|
||||
1. 推送加密货币达到特定价格的通知
|
||||
1. 在多台设备上推送文本
|
||||
1. 自动化工具推送定期汇报
|
||||
|
||||
### 开发者
|
||||
|
||||
使用PushDeer的场景包括但不限于:
|
||||
|
||||
1. 推送报错和调试信息
|
||||
1. 推送服务器异常
|
||||
1. 推送定时任务输出
|
||||
1. 在自己的软件发送消息到手机(引导用户填入PushDeer的key)
|
||||
|
||||
### 公司或自媒体
|
||||
|
||||
使用PushDeer的场景包括但不限于:
|
||||
|
||||
1. 面向自己的用户推送通知、内容和营销信息(类似公众号,但不受微信限制)
|
||||
|
||||
## 项目目录说明
|
||||
## 目录说明
|
||||
|
||||
- api: Laravel实现的API接口,[点此查看请求和返回demo](doc/api/PushDeerOS.md)
|
||||
- docker: API实现的docker封装,一键启动,方便使用
|
||||
|
@ -175,6 +239,7 @@ PushDeer主要面向以下三类用户
|
|||
- push: 基于 [gorush](https://github.com/appleboy/gorush) 架设的推送微服务,配置文件开启 async 可以提升发送速度
|
||||
- ios: 用于放置 iOS 源文件,`ios/Prototype_version` 目录是我边学边写的原型验证版本(SwiftUI+Moya+Codable),很多地方需要重写,仅供参考
|
||||
- quickapp: 用于放置快应用源代码
|
||||
- android: 用于放置安卓客户端源代码
|
||||
|
||||
## 开发环境搭建
|
||||
|
||||
|
@ -182,16 +247,12 @@ PushDeer主要面向以下三类用户
|
|||
|
||||
```git clone https://github.com/easychen/pushdeer.git```
|
||||
|
||||
### 配置推送证书
|
||||
|
||||
进入 `push` 目录,修改 `*.yml.sample` 为 `*.yml`。其中iOS应用和Clip使用两个分开的证书进行推送,`ios.yml` 是APP的配置、`clip.yml`是Clip的配置。注意根据开发和产品状态,修改`yml`中的值`production`。
|
||||
|
||||
默认配置中,`c.p12` 是APP的推送证书、`cc.p12`是Clip的推送证书。
|
||||
|
||||
### 启动docker环境
|
||||
|
||||
运行 `docker-compose up -d`,启动API。默认访问地址为`http://127.0.0.1:8800`。可修改`docker-compose.yml`调整端口。
|
||||
|
||||
|
||||
### API 说明
|
||||
|
||||
[在线文档(Swagger)](https://ilovintit.github.io/pushdeer-api-doc/#/)
|
||||
|
@ -328,11 +389,13 @@ API_BASE=http://127.0.0.1:8800
|
|||
|
||||
|参数|说明|备注|
|
||||
|-|-|-|
|
||||
|pushkey|PushKey|
|
||||
|pushkey|PushKey|多个key用`,`隔开,在线版最多10个,自架版默认最多100个|
|
||||
|text|推送消息内容|
|
||||
|desp|消息内容第二部分,选填|
|
||||
|type|格式,选填|文本=text,markdown,图片=image,默认为markdown|
|
||||
|
||||
type 为 image 时,text 中为要发送图片的URL。
|
||||
|
||||
#### 获得当前用户的消息列表
|
||||
|
||||
`POST /message/list`
|
||||
|
@ -352,6 +415,46 @@ API_BASE=http://127.0.0.1:8800
|
|||
|token|认证token|
|
||||
|id|消息ID|
|
||||
|
||||
#### 清除全部消息
|
||||
|
||||
`POST /message/clean`
|
||||
|
||||
|参数|说明|备注|
|
||||
|-|-|-|
|
||||
|token|认证token|
|
||||
|
||||
#### Simple token
|
||||
> 为了方便客户端永久保持登入状态,我们提供了一个永不失效的Token,即 Simple token
|
||||
|
||||
##### 获取 Simple token
|
||||
通过 上文中的「获得当前用户的基本信息」接口(`POST /user/info`) 得到
|
||||
|
||||
##### 通过 Simple token 登入
|
||||
|
||||
`POST /login/simple_token`
|
||||
|
||||
|参数|说明|备注|
|
||||
|-|-|-|
|
||||
|stoken|Simple token|
|
||||
|
||||
登入成功返回认证token。
|
||||
|
||||
##### 重置 Simple token
|
||||
|
||||
`POST /simple_token/regen`
|
||||
|
||||
|参数|说明|备注|
|
||||
|-|-|-|
|
||||
|token|认证token|
|
||||
|
||||
##### 清空 Simple token
|
||||
|
||||
`POST /simple_token/remove`
|
||||
|
||||
|参数|说明|备注|
|
||||
|-|-|-|
|
||||
|token|认证token|
|
||||
|
||||
|
||||
[更详细的请求和返回值可以参考这里](doc/api/PushDeerOS.md)
|
||||
|
||||
|
@ -364,8 +467,105 @@ API_BASE=http://127.0.0.1:8800
|
|||
error:错误信息,无错误时无此字段
|
||||
}
|
||||
```
|
||||
## 授权
|
||||
|
||||
本项目禁止商用(包括但不限于搭建后挂广告或售卖会员、打包后上架商店销售等),对非商业用途采用 GPLV2 授权
|
||||
|
||||
|
||||
# 产品设计文档
|
||||
|
||||
## 产品定义
|
||||
|
||||
PushDeer的**核心价值**,包括:「易用」、「可控」和「渐进」。
|
||||
|
||||
### 易用
|
||||
|
||||
易用性表现在两个方面:
|
||||
|
||||
1. 易安装:采用无APP方案,直接**去掉安装步骤**
|
||||
1. 易调用:只需输入URL,**无需阅读文档**,就可以发送消息
|
||||
|
||||
### 可控
|
||||
|
||||
1. `Self-hosted`:让有能力和精力的用户可以自行架设,避免因为在线服务下线导致的接口更换风险。
|
||||
1. 非商用免费:不用PushDeer挣钱,就无需支付费用
|
||||
1. 不依赖微信消息接口:不像Server酱那样受腾讯政策影响
|
||||
|
||||
### 渐进
|
||||
|
||||
1. 通过URL即可发送基本的文本消息;通过更多参数,可以对消息的样式等细节进行调整
|
||||
1. 无APP不能实现的功能不能覆盖的机型,后期可以通过APP来补充
|
||||
|
||||
## 商业模式
|
||||
|
||||
PushDeer是一个商业开源项目,采用「开放源码」、「自用免费」、「在线服务收费」的方式进行运作。
|
||||
|
||||
### 具体实现
|
||||
|
||||
PushDeer是一个以盈利为目的的商业项目,品牌和源码所有权都由「方糖君」公司持有,但和纯商业项目不同的地方在于:
|
||||
|
||||
1. 它开放源代码,所有人都可以在非商业前提下按GPLv2授权使用
|
||||
1. 它接受社区贡献代码,作为回报,它会从商业收益中拿出部分来赞助项目贡献人
|
||||
1. 如果商业收益够大,它会尝试雇佣项目贡献人以兼职或者全职的方式为项目工作
|
||||
|
||||
这里边有一些细节:
|
||||
|
||||
1. 为了避免某些个人或者公司使用源码搭建在线竞品服务收费,我们限制了源码不能商用
|
||||
1. 在刚开始的时候,项目并没有商业收入,而却是开发工作量最大的。所以首先我们会承担产品和界面设计、API设计和开发等工作;并通过众筹的方式筹集了一些资金给其他大模块的贡献人
|
||||
|
||||
开放源码形式保证了其他代码贡献人在非商业场景下对源码的可控:
|
||||
|
||||
1. 如果社区和代码贡献人不满意「方糖君」主导的商业化,可以 Fork 一个版本,继续在非商用的前提下自行运营
|
||||
1. 如果「方糖君」之后不再开放源代码,普通用户依然可以按之前的协议使用修改协议前的源码
|
||||
|
||||
## 用户细分
|
||||
|
||||
PushDeer主要面向以下三类用户
|
||||
|
||||
1. 高阶电脑用户
|
||||
1. 开发者
|
||||
1. 公司或自媒体
|
||||
|
||||
### 高阶电脑用户
|
||||
|
||||
具有一定电脑操作技能的高阶用户,比如:
|
||||
|
||||
1. NAS 用户
|
||||
1. 站长
|
||||
1. 电脑技术爱好者
|
||||
|
||||
他们使用PushDeer的场景包括但不限于:
|
||||
|
||||
1. 推送路由器和 NAS 的状态、公网 IP 等信息
|
||||
1. 推送 Wordpress 最新的评论
|
||||
1. 推送加密货币达到特定价格的通知
|
||||
1. 在多台设备上推送文本
|
||||
1. 自动化工具推送定期汇报
|
||||
|
||||
### 开发者
|
||||
|
||||
使用PushDeer的场景包括但不限于:
|
||||
|
||||
1. 推送报错和调试信息
|
||||
1. 推送服务器异常
|
||||
1. 推送定时任务输出
|
||||
1. 在自己的软件发送消息到手机(引导用户填入PushDeer的key)
|
||||
|
||||
### 公司或自媒体
|
||||
|
||||
使用PushDeer的场景包括但不限于:
|
||||
|
||||
1. 面向自己的用户推送通知、内容和营销信息(类似公众号,但不受微信限制)
|
||||
|
||||
|
||||
# 授权
|
||||
|
||||
本项目禁止商用(包括但不限于搭建后挂广告或售卖会员、打包后上架商店销售等),在非商用的情况下遵循GPL v2,当两者冲突时,以非商用原则优先。
|
||||
|
||||
|
||||
# 相关项目
|
||||
|
||||
- [C# SDK](https://gitee.com/mrbread/pushdeer_csharp_sdk) by [MrBread](https://gitee.com/mrbread)
|
||||
- [Java SDK](https://gitee.com/mrbread/pushdeer-java-sdk) by [MrBread](https://gitee.com/mrbread)
|
||||
- [Python SDK](https://github.com/gaoliang/pypushdeer) by [Gao Liang](https://github.com/gaoliang)
|
||||
- [API的Go实现](https://github.com/iepngs/pushdeer-backend-go) by [iepngs](https://github.com/iepngs)
|
||||
- [API的Node实现](https://github.com/xkrfer/pushdeer-node) by [DouDou](https://github.com/xkrfer)
|
||||
- [浏览器插件](https://github.com/xkrfer/pushdeer-crx) by [DouDou](https://github.com/xkrfer)
|
||||
- [Rust SDK](https://github.com/abgelehnt/rupushdeer) by [Chi](https://github.com/abgelehnt)
|
||||
|
|
12
RoboFile.php
|
@ -6,14 +6,22 @@
|
|||
*/
|
||||
class RoboFile extends \Robo\Tasks
|
||||
{
|
||||
// define public methods as commands
|
||||
/**
|
||||
* 构建一个独立镜像,只包含api和redis
|
||||
*/
|
||||
public function buildDockerImage()
|
||||
{
|
||||
$tmp_dir = "/tmp/".md5(__DIR__);
|
||||
$this->_copyDir('docker', $tmp_dir.'/app/docker');
|
||||
# $this->taskReplaceInFile($tmp_dir.'/app/docker/web/init.sh')->from('# ./redis-server &')->to('./redis-server &')->run();
|
||||
|
||||
$this->_copyDir('api', $tmp_dir.'/app/docker/web/api');
|
||||
$this->_copyDir('push', $tmp_dir.'/app/docker/web/push');
|
||||
$this->_exec("cd $tmp_dir/app && docker build -f ./docker/web/dockerfile.serverless -t pushdeercore ./docker/web ");
|
||||
// 更新redis为memory
|
||||
$this->taskReplaceInFile($tmp_dir.'/app/docker/web/push/ios.yml')->from('addr: "redis:6379"')->to('addr: "127.0.0.1:6379"')->run();
|
||||
$this->taskReplaceInFile($tmp_dir.'/app/docker/web/push/clip.yml')->from('addr: "redis:6379"')->to('addr: "127.0.0.1:6379"')->run();
|
||||
// $this->_exec("open ".$tmp_dir.'/app/docker/');
|
||||
$this->_exec("cd $tmp_dir/app && docker build -f ./docker/web/Dockerfile.serverless -t pushdeercore ./docker/web ");
|
||||
$this->_exec("docker tag pushdeercore ccr.ccs.tencentyun.com/ftqq/pushdeercore");
|
||||
$this->_exec("docker push ccr.ccs.tencentyun.com/ftqq/pushdeercore");
|
||||
}
|
||||
|
|
|
@ -1,16 +1,42 @@
|
|||
*.iml
|
||||
.gradle
|
||||
/local.properties
|
||||
/.idea/caches
|
||||
/.idea/libraries
|
||||
/.idea/modules.xml
|
||||
/.idea/workspace.xml
|
||||
/.idea/navEditor.xml
|
||||
/.idea/assetWizardSettings.xml
|
||||
.DS_Store
|
||||
/build
|
||||
/captures
|
||||
.externalNativeBuild
|
||||
.cxx
|
||||
# Gradle files
|
||||
.gradle/
|
||||
build/
|
||||
|
||||
# Local configuration file (sdk path, etc)
|
||||
local.properties
|
||||
|
||||
# Log/OS Files
|
||||
*.log
|
||||
|
||||
# Android Studio generated files and folders
|
||||
captures/
|
||||
.externalNativeBuild/
|
||||
.cxx/
|
||||
*.apk
|
||||
output.json
|
||||
|
||||
# IntelliJ
|
||||
*.iml
|
||||
.idea/
|
||||
misc.xml
|
||||
deploymentTargetDropDown.xml
|
||||
render.experimental.xml
|
||||
|
||||
# Keystore files
|
||||
*.jks
|
||||
*.keystore
|
||||
|
||||
# Google Services (e.g. APIs or Firebase)
|
||||
google-services.json
|
||||
|
||||
# Android Profiling
|
||||
*.hprof
|
||||
|
||||
# Misc Files
|
||||
.DS_Store
|
||||
|
||||
# API Keys
|
||||
/app/src/main/java/com/pushdeer/os/AppKeys.kt
|
||||
|
||||
# Signing Keys
|
||||
/key_debug.properties
|
||||
|
|
|
@ -1,3 +0,0 @@
|
|||
# Default ignored files
|
||||
/shelf/
|
||||
/workspace.xml
|
|
@ -1,6 +0,0 @@
|
|||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<project version="4">
|
||||
<component name="CompilerConfiguration">
|
||||
<bytecodeTargetLevel target="11" />
|
||||
</component>
|
||||
</project>
|
|
@ -1,24 +0,0 @@
|
|||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<project version="4">
|
||||
<component name="GradleMigrationSettings" migrationVersion="1" />
|
||||
<component name="GradleSettings">
|
||||
<option name="linkedExternalProjectsSettings">
|
||||
<GradleProjectSettings>
|
||||
<option name="testRunner" value="GRADLE" />
|
||||
<option name="distributionType" value="DEFAULT_WRAPPED" />
|
||||
<option name="externalProjectPath" value="$PROJECT_DIR$" />
|
||||
<option name="modules">
|
||||
<set>
|
||||
<option value="$PROJECT_DIR$" />
|
||||
<option value="$PROJECT_DIR$/app" />
|
||||
<option value="$PROJECT_DIR$/common" />
|
||||
<option value="$PROJECT_DIR$/compose" />
|
||||
<option value="$PROJECT_DIR$/pushdeerclient" />
|
||||
<option value="$PROJECT_DIR$/pushdeercommon" />
|
||||
</set>
|
||||
</option>
|
||||
<option name="resolveModulePerSourceSet" value="false" />
|
||||
</GradleProjectSettings>
|
||||
</option>
|
||||
</component>
|
||||
</project>
|
|
@ -1,20 +0,0 @@
|
|||
<component name="InspectionProjectProfileManager">
|
||||
<profile version="1.0">
|
||||
<option name="myName" value="Project Default" />
|
||||
<inspection_tool class="PreviewAnnotationInFunctionWithParameters" enabled="true" level="ERROR" enabled_by_default="true">
|
||||
<option name="previewFile" value="true" />
|
||||
</inspection_tool>
|
||||
<inspection_tool class="PreviewDimensionRespectsLimit" enabled="true" level="WARNING" enabled_by_default="true">
|
||||
<option name="previewFile" value="true" />
|
||||
</inspection_tool>
|
||||
<inspection_tool class="PreviewMultipleParameterProviders" enabled="true" level="ERROR" enabled_by_default="true">
|
||||
<option name="previewFile" value="true" />
|
||||
</inspection_tool>
|
||||
<inspection_tool class="PreviewMustBeTopLevelFunction" enabled="true" level="ERROR" enabled_by_default="true">
|
||||
<option name="previewFile" value="true" />
|
||||
</inspection_tool>
|
||||
<inspection_tool class="PreviewNeedsComposableAnnotation" enabled="true" level="ERROR" enabled_by_default="true">
|
||||
<option name="previewFile" value="true" />
|
||||
</inspection_tool>
|
||||
</profile>
|
||||
</component>
|
|
@ -1,33 +0,0 @@
|
|||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<project version="4">
|
||||
<component name="DesignSurface">
|
||||
<option name="filePathToZoomLevelMap">
|
||||
<map>
|
||||
<entry key="../../../../layout/compose-model-1640490388500.xml" value="0.33" />
|
||||
<entry key="../../../../layout/compose-model-1640490589000.xml" value="0.36944444444444446" />
|
||||
<entry key="../../../../layout/compose-model-1640569321856.xml" value="1.6638655462184875" />
|
||||
<entry key="../../../../layout/compose-model-1641297602161.xml" value="0.29771959459459457" />
|
||||
<entry key="../../../../layout/compose-model-1641300051131.xml" value="1.0" />
|
||||
<entry key="../../../../layout/compose-model-1641348859824.xml" value="0.1" />
|
||||
<entry key="../../../../layout/compose-model-1641366243757.xml" value="2.0" />
|
||||
<entry key="../../../../layout/compose-model-1641659551303.xml" value="2.0" />
|
||||
<entry key="../../../../layout/compose-model-1641659962289.xml" value="2.0" />
|
||||
<entry key="../../../../layout/compose-model-1641694023752.xml" value="0.1" />
|
||||
<entry key="../../../../layout/compose-model-1642733328920.xml" value="2.0" />
|
||||
<entry key="../../../../layout/compose-model-1642826587452.xml" value="2.0" />
|
||||
<entry key="app/src/main/res/drawable/fragment_qr_scan.xml" value="0.12314814814814815" />
|
||||
<entry key="app/src/main/res/drawable/ic_markdown.xml" value="0.12962962962962962" />
|
||||
<entry key="app/src/main/res/layout/activity_qr_scan.xml" value="0.1" />
|
||||
<entry key="app/src/main/res/mipmap-anydpi-v26/ic_launcher.xml" value="0.1" />
|
||||
<entry key="common/src/main/res/layout/activity_qr_scan.xml" value="0.1" />
|
||||
<entry key="pushdeerclient/src/main/res/drawable-v24/ic_markdown.xml" value="0.11944444444444445" />
|
||||
</map>
|
||||
</option>
|
||||
</component>
|
||||
<component name="ProjectRootManager" version="2" languageLevel="JDK_11" default="true" project-jdk-name="Android Studio default JDK" project-jdk-type="JavaSDK">
|
||||
<output url="file://$PROJECT_DIR$/build/classes" />
|
||||
</component>
|
||||
<component name="ProjectType">
|
||||
<option name="id" value="Android" />
|
||||
</component>
|
||||
</project>
|
|
@ -4,9 +4,9 @@
|
|||
|
||||
* MiPush(状态:已调通、已接入)
|
||||
* miui √
|
||||
* Mokee √
|
||||
* Mokee-81.0 PixelExperience-12 √
|
||||
* WaWei √
|
||||
* 坚果pro3-8.0.1 √ 坚果pro2s-7.2.0.2 x(暂无法成功注册)
|
||||
* 坚果pro3-8.0.1 √ 坚果pro2s-7.2.0.2 x(暂无法稳定成功注册)
|
||||
* 部分原生、类原生 可成功注册,无法收到推送,可能和地区识别有关,待解决
|
||||
|
||||
### TODO
|
||||
|
@ -117,8 +117,22 @@
|
|||
* 适配 PushDeer 的专用推送通道
|
||||
* 增加修改设备/密钥等名称时"清空文本"按钮
|
||||
* 部分英化汉化
|
||||
* 设置界面的Like
|
||||
|
||||
* 2022-01-23
|
||||
* 修复切换界面超量请求的bug
|
||||
|
||||
* 2022-01-24
|
||||
* 重写列表滑动删除逻辑
|
||||
|
||||
* 2022-01-25、26
|
||||
* 适配微信登陆
|
||||
* 适配 PushDeer 的微信登陆及账号合并路由
|
||||
* 增加只有一个确认按钮的 AlertDialog,替换无意义的双按钮弹框
|
||||
* 增加消息列表中测试推送框的收起按钮的旋转动画
|
||||
* 修改设置界面登陆账号绑定指示器UI
|
||||
|
||||
### 感谢
|
||||
|
||||
https://github.com/taoweiji/MixPush
|
||||
https://github.com/taoweiji/MixPush
|
||||
|
||||
|
|
|
@ -4,7 +4,21 @@ plugins {
|
|||
id 'kotlin-kapt'
|
||||
}
|
||||
|
||||
def debugKeyProps = new Properties()
|
||||
def debugKeyPropsFile = rootProject.file('key_debug.properties')
|
||||
if (debugKeyPropsFile.exists()) {
|
||||
debugKeyProps.load(new FileInputStream(debugKeyPropsFile))
|
||||
}
|
||||
|
||||
android {
|
||||
signingConfigs {
|
||||
debug {
|
||||
storeFile file(debugKeyProps['storeFile'])
|
||||
storePassword debugKeyProps['storePassword']
|
||||
keyAlias debugKeyProps['keyAlias']
|
||||
keyPassword debugKeyProps['keyPassword']
|
||||
}
|
||||
}
|
||||
|
||||
compileSdk 31
|
||||
|
||||
|
@ -12,8 +26,8 @@ android {
|
|||
applicationId "com.pushdeer.os"
|
||||
minSdk 22
|
||||
targetSdk 31
|
||||
versionCode 8
|
||||
versionName "1.0-dev-8"
|
||||
versionCode 15
|
||||
versionName "1.0-alpha-5"
|
||||
|
||||
testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner"
|
||||
vectorDrawables {
|
||||
|
@ -59,7 +73,7 @@ dependencies {
|
|||
implementation "androidx.compose.ui:ui-tooling-preview:$compose_version"
|
||||
implementation 'androidx.lifecycle:lifecycle-runtime-ktx:2.4.0'
|
||||
implementation 'androidx.activity:activity-compose:1.4.0'
|
||||
implementation files('libs/MiPush_SDK_Client_4_9_0.jar')
|
||||
implementation files('libs/MiPush_SDK_Client_5_0_5-C_3rd.aar')
|
||||
testImplementation 'junit:junit:4.13.2'
|
||||
androidTestImplementation 'androidx.test.ext:junit:1.1.3'
|
||||
androidTestImplementation 'androidx.test.espresso:espresso-core:3.4.0'
|
||||
|
@ -105,9 +119,8 @@ dependencies {
|
|||
implementation "io.noties.markwon:ext-tables:$markwon_version"
|
||||
implementation "io.noties.markwon:ext-tasklist:$markwon_version"
|
||||
implementation "io.noties.markwon:image-coil:$markwon_version"
|
||||
implementation "io.noties.markwon:linkify:$markwon_version"
|
||||
// implementation "io.noties.markwon:linkify:$markwon_version"
|
||||
implementation "io.noties.markwon:html:$markwon_version"
|
||||
|
||||
implementation "io.coil-kt:coil:1.4.0"
|
||||
|
||||
implementation 'com.github.vishalkumarsinghvi:sign-in-with-apple-button-android:0.6'
|
||||
|
|
|
@ -11,8 +11,8 @@
|
|||
"type": "SINGLE",
|
||||
"filters": [],
|
||||
"attributes": [],
|
||||
"versionCode": 5,
|
||||
"versionName": "1.0-dev-5",
|
||||
"versionCode": 15,
|
||||
"versionName": "1.0-alpha-5",
|
||||
"outputFile": "app-release.apk"
|
||||
}
|
||||
],
|
||||
|
|
|
@ -123,6 +123,8 @@
|
|||
</intent-filter>
|
||||
</receiver>
|
||||
|
||||
<activity android:name=".WebViewActivity"/>
|
||||
|
||||
|
||||
</application>
|
||||
|
||||
|
|
|
@ -1,20 +1,15 @@
|
|||
package com.pushdeer.os
|
||||
|
||||
import android.app.ActivityManager
|
||||
import android.app.Application
|
||||
import android.os.Process
|
||||
import android.util.Log
|
||||
import com.pushdeer.os.data.api.PushDeerApi
|
||||
import com.pushdeer.os.data.database.AppDatabase
|
||||
import com.pushdeer.os.factory.ViewModelFactory
|
||||
import com.pushdeer.os.keeper.RepositoryKeeper
|
||||
import com.pushdeer.os.keeper.StoreKeeper
|
||||
import com.pushdeer.os.util.MiPushUtils
|
||||
import com.pushdeer.os.values.AppKeys
|
||||
import com.tencent.mm.opensdk.openapi.IWXAPI
|
||||
import com.tencent.mm.opensdk.openapi.WXAPIFactory
|
||||
import com.xiaomi.channel.commonutils.logger.LoggerInterface
|
||||
import com.xiaomi.mipush.sdk.Logger
|
||||
import com.xiaomi.mipush.sdk.MiPushClient
|
||||
import retrofit2.Retrofit
|
||||
import retrofit2.converter.gson.GsonConverterFactory
|
||||
import retrofit2.converter.scalars.ScalarsConverterFactory
|
||||
|
@ -23,7 +18,7 @@ class App : Application() {
|
|||
|
||||
val storeKeeper by lazy { StoreKeeper(this) }
|
||||
val database by lazy { AppDatabase.getDatabase(this) }
|
||||
val repositoryKeeper by lazy { RepositoryKeeper(database,storeKeeper.settingStore) }
|
||||
val repositoryKeeper by lazy { RepositoryKeeper(database, storeKeeper.settingStore) }
|
||||
private val pushDeerService: PushDeerApi by lazy {
|
||||
Retrofit.Builder()
|
||||
.baseUrl(PushDeerApi.baseUrl)
|
||||
|
@ -40,59 +35,14 @@ class App : Application() {
|
|||
)
|
||||
}
|
||||
|
||||
val iwxapi:IWXAPI by lazy { WXAPIFactory.createWXAPI(this, AppKeys.WX_Id, true) }
|
||||
val iwxapi: IWXAPI by lazy { WXAPIFactory.createWXAPI(this, AppKeys.WX_Id, true) }
|
||||
|
||||
override fun onCreate() {
|
||||
super.onCreate()
|
||||
//初始化push推送服务
|
||||
if (shouldInit()) {
|
||||
MiPushClient.registerPush(this, AppKeys.MiPush_Id, AppKeys.MiPush_Key)
|
||||
}
|
||||
//打开Log
|
||||
Logger.setLogger(this, object : LoggerInterface {
|
||||
override fun setTag(tag: String) {
|
||||
// ignore
|
||||
}
|
||||
|
||||
override fun log(content: String, t: Throwable) {
|
||||
Log.d(TAG, content, t)
|
||||
Thread{
|
||||
repositoryKeeper.logDogRepository.log(
|
||||
entity = "mipush",
|
||||
level = "e",
|
||||
event = t.message.toString(),
|
||||
log = content
|
||||
)
|
||||
}.start()
|
||||
}
|
||||
|
||||
override fun log(content: String) {
|
||||
Log.d(TAG, content)
|
||||
// Thread{
|
||||
// repositoryKeeper.logDogRepository.log(
|
||||
// entity = "mipush",
|
||||
// level = "d",
|
||||
// event = "",
|
||||
// log = content
|
||||
// )
|
||||
// }.start()
|
||||
|
||||
}
|
||||
})
|
||||
MiPushUtils.autoInit(this,repositoryKeeper)
|
||||
}
|
||||
|
||||
private fun shouldInit(): Boolean {
|
||||
val am = getSystemService(ACTIVITY_SERVICE) as ActivityManager
|
||||
val processInfoList = am.runningAppProcesses
|
||||
val mainProcessName = applicationInfo.processName
|
||||
val myPid = Process.myPid()
|
||||
for (info in processInfoList) {
|
||||
if (info.pid == myPid && mainProcessName == info.processName) {
|
||||
return true
|
||||
}
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
companion object {
|
||||
const val TAG = "TAG"
|
||||
|
|
|
@ -0,0 +1,9 @@
|
|||
package com.pushdeer.os.values
|
||||
|
||||
object AppKeys {
|
||||
|
||||
const val MiPush_Id = "2882303761520124028"
|
||||
const val MiPush_Key = "5512012428028"
|
||||
|
||||
const val WX_Id = "wx3ae07931d0555a24"
|
||||
}
|
|
@ -2,8 +2,8 @@ package com.pushdeer.os
|
|||
|
||||
import android.content.*
|
||||
import android.os.Bundle
|
||||
import android.text.util.Linkify
|
||||
import android.util.Log
|
||||
import android.view.View
|
||||
import androidx.activity.compose.setContent
|
||||
import androidx.activity.result.ActivityResultLauncher
|
||||
import androidx.activity.viewModels
|
||||
|
@ -42,9 +42,8 @@ import com.pushdeer.os.viewmodel.PushDeerViewModel
|
|||
import com.pushdeer.os.viewmodel.UiViewModel
|
||||
import com.pushdeer.os.wxapi.WXEntryActivity
|
||||
import com.tencent.mm.opensdk.constants.ConstantsAPI
|
||||
import io.noties.markwon.Markwon
|
||||
import io.noties.markwon.*
|
||||
import io.noties.markwon.image.coil.CoilImagesPlugin
|
||||
import io.noties.markwon.linkify.LinkifyPlugin
|
||||
import kotlinx.coroutines.CoroutineScope
|
||||
import kotlinx.coroutines.launch
|
||||
import java.util.*
|
||||
|
@ -72,36 +71,48 @@ class MainActivity : AppCompatActivity(), RequestHolder {
|
|||
.build()
|
||||
}
|
||||
override val alert: RequestHolder.AlertRequest by lazy {
|
||||
object : RequestHolder.AlertRequest(resources) {}
|
||||
RequestHolder.AlertRequest(resources)
|
||||
}
|
||||
override val key: RequestHolder.KeyRequest by lazy {
|
||||
object : RequestHolder.KeyRequest(this) {}
|
||||
RequestHolder.KeyRequest(this)
|
||||
}
|
||||
override val device: RequestHolder.DeviceRequest by lazy {
|
||||
object : RequestHolder.DeviceRequest(this) {}
|
||||
RequestHolder.DeviceRequest(this)
|
||||
}
|
||||
override val message: RequestHolder.MessageRequest by lazy {
|
||||
object : RequestHolder.MessageRequest(this) {}
|
||||
RequestHolder.MessageRequest(this)
|
||||
}
|
||||
override val clip: RequestHolder.ClipRequest by lazy {
|
||||
object : RequestHolder.ClipRequest(
|
||||
RequestHolder.ClipRequest(
|
||||
getSystemService(
|
||||
Context.CLIPBOARD_SERVICE
|
||||
) as ClipboardManager
|
||||
) {}
|
||||
)
|
||||
}
|
||||
override val weChatLogin: RequestHolder.WeChatLoginRequest by lazy {
|
||||
object : RequestHolder.WeChatLoginRequest((application as App).iwxapi) {}
|
||||
RequestHolder.WeChatLoginRequest((application as App).iwxapi)
|
||||
}
|
||||
|
||||
override val appleLogin: RequestHolder.AppleLoginRequest by lazy {
|
||||
object : RequestHolder.AppleLoginRequest(supportFragmentManager, this) {}
|
||||
RequestHolder.AppleLoginRequest(supportFragmentManager, this)
|
||||
}
|
||||
|
||||
override val markdown: Markwon by lazy {
|
||||
Markwon.builder(this)
|
||||
.usePlugin(CoilImagesPlugin.create(this, coilImageLoader))
|
||||
.usePlugin(LinkifyPlugin.create(Linkify.WEB_URLS))
|
||||
.usePlugin(object : AbstractMarkwonPlugin() {
|
||||
override fun configureConfiguration(builder: MarkwonConfiguration.Builder) {
|
||||
builder.linkResolver(object : LinkResolverDef() {
|
||||
override fun resolve(view: View, link: String) {
|
||||
if (settingStore.useInnerWebView){
|
||||
WebViewActivity.load(this@MainActivity, link)
|
||||
}else{
|
||||
super.resolve(view, link)
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
})
|
||||
.build()
|
||||
}
|
||||
|
||||
|
@ -111,7 +122,7 @@ class MainActivity : AppCompatActivity(), RequestHolder {
|
|||
override lateinit var qrScanActivityOpener: ActivityResultLauncher<Intent>
|
||||
override lateinit var requestPermissionOpener: ActivityResultLauncher<Array<String>>
|
||||
|
||||
val wxRegReceiver: BroadcastReceiver by lazy {
|
||||
private val wxRegReceiver: BroadcastReceiver by lazy {
|
||||
object : BroadcastReceiver() {
|
||||
override fun onReceive(context: Context?, intent: Intent?) {
|
||||
intent?.let {
|
||||
|
@ -166,7 +177,8 @@ class MainActivity : AppCompatActivity(), RequestHolder {
|
|||
IntentFilter().apply {
|
||||
addAction(ConstantsAPI.ACTION_REFRESH_WXAPP)
|
||||
addAction(WXEntryActivity.ACTION_RETURN_CODE)
|
||||
})
|
||||
}
|
||||
)
|
||||
|
||||
|
||||
NotificationUtil.setupChannel(this)
|
||||
|
@ -185,7 +197,7 @@ class MainActivity : AppCompatActivity(), RequestHolder {
|
|||
Color.Transparent,
|
||||
useDarkIcons
|
||||
)
|
||||
else -> systemUiController.setSystemBarsColor(Color.Transparent, !useDarkIcons)
|
||||
else -> systemUiController.setSystemBarsColor(Color.Transparent, useDarkIcons)
|
||||
}
|
||||
WindowCompat.setDecorFitsSystemWindows(window, true)
|
||||
miPushRepository.regId.observe(this) {
|
||||
|
|
|
@ -0,0 +1,96 @@
|
|||
package com.pushdeer.os
|
||||
|
||||
import android.annotation.SuppressLint
|
||||
import android.app.Activity
|
||||
import android.content.Context
|
||||
import android.content.Intent
|
||||
import android.os.Bundle
|
||||
import android.view.View
|
||||
import android.webkit.WebChromeClient
|
||||
import android.webkit.WebResourceRequest
|
||||
import android.webkit.WebView
|
||||
import android.webkit.WebViewClient
|
||||
import android.widget.ImageButton
|
||||
import android.widget.ProgressBar
|
||||
import android.widget.TextView
|
||||
|
||||
class WebViewActivity : Activity() {
|
||||
|
||||
companion object {
|
||||
const val URL_KEY = "url-to-open"
|
||||
|
||||
fun load(context: Context, url: String) {
|
||||
context.startActivity(Intent(context, WebViewActivity::class.java).apply {
|
||||
putExtra(URL_KEY, url)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
@SuppressLint("SetJavaScriptEnabled")
|
||||
override fun onCreate(savedInstanceState: Bundle?) {
|
||||
super.onCreate(savedInstanceState)
|
||||
setContentView(R.layout.activity_webview)
|
||||
|
||||
val ibClose = findViewById<ImageButton>(R.id.ib_close)
|
||||
val ibBack = findViewById<ImageButton>(R.id.ib_back)
|
||||
|
||||
val webView = findViewById<WebView>(R.id.webview)
|
||||
|
||||
val progressBar = findViewById<ProgressBar>(R.id.progressBar)
|
||||
|
||||
val tvTitle = findViewById<TextView>(R.id.tv_title)
|
||||
|
||||
webView.settings.apply {
|
||||
this.javaScriptEnabled = true
|
||||
// this.
|
||||
}
|
||||
|
||||
ibClose.setOnClickListener {
|
||||
this.finish()
|
||||
}
|
||||
|
||||
ibBack.setOnClickListener {
|
||||
if (webView.canGoBack()) {
|
||||
webView.goBack()
|
||||
} else {
|
||||
this.finish()
|
||||
}
|
||||
}
|
||||
|
||||
webView.webViewClient = object :WebViewClient(){
|
||||
override fun shouldOverrideUrlLoading(
|
||||
view: WebView?,
|
||||
request: WebResourceRequest?
|
||||
): Boolean {
|
||||
return false
|
||||
}
|
||||
}
|
||||
|
||||
webView.webChromeClient = object : WebChromeClient() {
|
||||
override fun onProgressChanged(view: WebView?, newProgress: Int) {
|
||||
super.onProgressChanged(view, newProgress)
|
||||
progressBar.progress = newProgress
|
||||
if (newProgress == 100){
|
||||
progressBar.visibility = View.GONE
|
||||
}else{
|
||||
progressBar.visibility = View.VISIBLE
|
||||
}
|
||||
}
|
||||
|
||||
override fun onReceivedTitle(view: WebView?, title: String?) {
|
||||
super.onReceivedTitle(view, title)
|
||||
title?.let {
|
||||
tvTitle.text = it
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
|
||||
intent.getStringExtra(URL_KEY)?.let {
|
||||
webView.loadUrl(it)
|
||||
tvTitle.text = it
|
||||
}
|
||||
|
||||
}
|
||||
}
|
|
@ -1,66 +0,0 @@
|
|||
//package com.pushdeer.os.activity
|
||||
//
|
||||
//
|
||||
//import android.content.Context
|
||||
//import android.content.Intent
|
||||
//import android.os.Bundle
|
||||
//import android.util.Log
|
||||
//import androidx.appcompat.app.AppCompatActivity
|
||||
//import cn.bingoogolapple.qrcode.core.QRCodeView
|
||||
//import com.pushdeer.os.R
|
||||
//
|
||||
//
|
||||
//class QrScanActivity : AppCompatActivity(), QRCodeView.Delegate {
|
||||
//
|
||||
// private val TAG = "WH_" + javaClass.simpleName
|
||||
// private lateinit var qrCode: QRCodeView
|
||||
//
|
||||
// companion object {
|
||||
// val RequestCode_get_scan_result = 436
|
||||
// val DataKey = "qr_scan_result"
|
||||
//
|
||||
// fun forScanResultIntent(context: Context): Intent {
|
||||
// return Intent(context, QrScanActivity::class.java).apply {
|
||||
// putExtra(DataKey, 1)
|
||||
// }
|
||||
// }
|
||||
// }
|
||||
//
|
||||
// override fun onCreate(savedInstanceState: Bundle?) {
|
||||
// super.onCreate(savedInstanceState)
|
||||
// setContentView(R.layout.activity_qr_scan)
|
||||
// qrCode = findViewById(R.id.qrcode1)
|
||||
// qrCode.setDelegate(this)
|
||||
// }
|
||||
//
|
||||
// override fun onStart() {
|
||||
// super.onStart()
|
||||
// Log.d(TAG, "onStart")
|
||||
// qrCode.startSpotAndShowRect()
|
||||
// }
|
||||
//
|
||||
// override fun onStop() {
|
||||
// Log.d(TAG, "onStop")
|
||||
// qrCode.stopCamera()
|
||||
// super.onStop()
|
||||
// }
|
||||
//
|
||||
// override fun onDestroy() {
|
||||
// qrCode.onDestroy()
|
||||
// super.onDestroy()
|
||||
// }
|
||||
//
|
||||
// override fun onScanQRCodeSuccess(result: String?) {
|
||||
// Log.d(TAG, "onScanQRCodeSuccess: $result")
|
||||
// qrCode.stopCamera()
|
||||
// val intent = Intent()
|
||||
// intent.putExtra(DataKey, result)
|
||||
// setResult(RequestCode_get_scan_result, intent)
|
||||
// finish()
|
||||
// }
|
||||
//
|
||||
// override fun onScanQRCodeOpenCameraError() {
|
||||
// Log.e(TAG, "onScanQRCodeOpenCameraError")
|
||||
// qrCode.startSpotAndShowRect()
|
||||
// }
|
||||
//}
|
|
@ -76,7 +76,7 @@ interface RequestHolder {
|
|||
})
|
||||
}
|
||||
|
||||
abstract class AppleLoginRequest(
|
||||
class AppleLoginRequest(
|
||||
private val fragmentManager: FragmentManager,
|
||||
private val requestHolder: RequestHolder
|
||||
) {
|
||||
|
@ -141,7 +141,7 @@ interface RequestHolder {
|
|||
}
|
||||
}
|
||||
|
||||
abstract class WeChatLoginRequest(val iwxapi: IWXAPI) {
|
||||
class WeChatLoginRequest(val iwxapi: IWXAPI) {
|
||||
val login: () -> Unit = {
|
||||
val req = SendAuth.Req()
|
||||
req.scope = "snsapi_userinfo"
|
||||
|
@ -150,7 +150,7 @@ interface RequestHolder {
|
|||
}
|
||||
}
|
||||
|
||||
abstract class ClipRequest(private val clipboardManager: ClipboardManager) {
|
||||
class ClipRequest(private val clipboardManager: ClipboardManager) {
|
||||
fun copyMessagePlainText(str: String) {
|
||||
clipboardManager.setPrimaryClip(ClipData.newPlainText("pushdeer-copy-plain-text", str))
|
||||
}
|
||||
|
@ -160,7 +160,7 @@ interface RequestHolder {
|
|||
}
|
||||
}
|
||||
|
||||
abstract class AlertRequest(private val resources: Resources) {
|
||||
class AlertRequest(private val resources: Resources) {
|
||||
val show2BtnDialog: MutableState<Boolean> = mutableStateOf(false)
|
||||
val show1BtnDialog: MutableState<Boolean> = mutableStateOf(false)
|
||||
var title: String = ""
|
||||
|
@ -253,7 +253,7 @@ interface RequestHolder {
|
|||
}
|
||||
}
|
||||
|
||||
abstract class KeyRequest(private val requestHolder: RequestHolder) {
|
||||
class KeyRequest(private val requestHolder: RequestHolder) {
|
||||
fun gen() {
|
||||
requestHolder.coroutineScope.launch {
|
||||
requestHolder.pushDeerViewModel.keyGen()
|
||||
|
@ -284,7 +284,7 @@ interface RequestHolder {
|
|||
}
|
||||
}
|
||||
|
||||
abstract class DeviceRequest(private val requestHolder: RequestHolder) {
|
||||
class DeviceRequest(private val requestHolder: RequestHolder) {
|
||||
fun deviceReg(deviceInfo: DeviceInfo) {
|
||||
requestHolder.coroutineScope.launch {
|
||||
requestHolder.pushDeerViewModel.deviceReg(deviceInfo)
|
||||
|
@ -309,7 +309,7 @@ interface RequestHolder {
|
|||
}
|
||||
}
|
||||
|
||||
abstract class MessageRequest(private val requestHolder: RequestHolder) {
|
||||
class MessageRequest(private val requestHolder: RequestHolder) {
|
||||
fun messagePush(text: String, desp: String, type: String, pushkey: String) {
|
||||
requestHolder.coroutineScope.launch {
|
||||
requestHolder.pushDeerViewModel.messagePush(text, desp, type, pushkey)
|
||||
|
|
|
@ -3,10 +3,10 @@ package com.pushdeer.os.store
|
|||
import android.content.Context
|
||||
import com.wh.common.store.Store
|
||||
|
||||
class SettingStore(context:Context) {
|
||||
val store = Store.create(context,"setting")
|
||||
class SettingStore(context: Context) {
|
||||
val store = Store.create(context, "setting")
|
||||
|
||||
var userToken by store.string("user-token","")
|
||||
var userToken by store.string("user-token", "")
|
||||
// var deviceName by store.string("device-name","My Dear Deer")
|
||||
// var useRecv by store.boolean("use-recv",false) // 启用接收
|
||||
// var useSend by store.boolean("use-send",false)
|
||||
|
@ -15,9 +15,15 @@ class SettingStore(context:Context) {
|
|||
// var useSendMissedCall by store.boolean("use-send=missed-call",false)
|
||||
// var useSendSMS by store.boolean("use-send-sms",false)
|
||||
|
||||
var showMessageSender by store.boolean("show-message-sender",true)
|
||||
var thisPushSdk by store.string("this-push-sdk","mi-push")
|
||||
var thisDeviceId by store.string("this-device-id","")
|
||||
var showMessageSender by store.boolean("show-message-sender", true)
|
||||
var thisPushSdk by store.string("this-push-sdk", "mi-push")
|
||||
var thisDeviceId by store.string("this-device-id", "")
|
||||
|
||||
var logLevel by store.string("log-level","i") // i w e - d
|
||||
var logLevel by store.string("log-level", "i") // i w e - d
|
||||
|
||||
var isServerMethodSelected by store.boolean("server-method-selected", false)
|
||||
var isSelfHosted by store.boolean("self-hosted", false)
|
||||
var selfHostedEndpointUrl by store.string("self-hosted-endpoint-url", "http://")
|
||||
|
||||
var useInnerWebView by store.boolean("user-inner-webview",false)
|
||||
}
|
|
@ -136,15 +136,16 @@ fun KeyItem(key: PushKey, requestHolder: RequestHolder) {
|
|||
) {
|
||||
AndroidView(
|
||||
factory = {
|
||||
ImageView(it).apply {
|
||||
this.setImageBitmap(
|
||||
QRCodeGenerator(
|
||||
key.key,
|
||||
400.dp.value.toInt(),
|
||||
400.dp.value.toInt()
|
||||
).qrCode
|
||||
)
|
||||
}
|
||||
ImageView(it)
|
||||
},
|
||||
update = { view ->
|
||||
view.setImageBitmap(
|
||||
QRCodeGenerator(
|
||||
key.key,
|
||||
400.dp.value.toInt(),
|
||||
400.dp.value.toInt()
|
||||
).qrCode
|
||||
)
|
||||
},
|
||||
modifier = Modifier.align(alignment = Alignment.Center)
|
||||
)
|
||||
|
|
|
@ -1,5 +1,6 @@
|
|||
package com.pushdeer.os.ui.compose.componment
|
||||
|
||||
import android.text.TextUtils
|
||||
import android.widget.ImageView
|
||||
import androidx.compose.foundation.Image
|
||||
import androidx.compose.foundation.background
|
||||
|
@ -13,6 +14,7 @@ import androidx.compose.material.Text
|
|||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.ui.Alignment
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.draw.alpha
|
||||
import androidx.compose.ui.draw.clip
|
||||
import androidx.compose.ui.res.painterResource
|
||||
import androidx.compose.ui.text.style.TextOverflow
|
||||
|
@ -29,7 +31,7 @@ import com.pushdeer.os.values.ConstValues
|
|||
|
||||
@ExperimentalMaterialApi
|
||||
@Composable
|
||||
fun PlainTextMessageItem(message: MessageEntity,requestHolder: RequestHolder) {
|
||||
fun PlainTextMessageItem(message: MessageEntity, requestHolder: RequestHolder) {
|
||||
Column(
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
|
@ -62,14 +64,40 @@ fun PlainTextMessageItem(message: MessageEntity,requestHolder: RequestHolder) {
|
|||
}
|
||||
|
||||
CardItemWithContent {
|
||||
Text(
|
||||
text = message.text,
|
||||
overflow = TextOverflow.Visible,
|
||||
lineHeight = 24.sp,
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.padding(16.dp)
|
||||
)
|
||||
Column(modifier = Modifier.fillMaxSize()) {
|
||||
|
||||
if (TextUtils.isEmpty(message.desp)) {
|
||||
Text(
|
||||
text = message.text,
|
||||
overflow = TextOverflow.Visible,
|
||||
fontSize = 20.sp,
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.padding(16.dp)
|
||||
)
|
||||
|
||||
} else {
|
||||
Text(
|
||||
text = message.text,
|
||||
overflow = TextOverflow.Visible,
|
||||
fontSize = 14.sp,
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.padding(start = 16.dp, top = 16.dp, end = 16.dp, bottom = 4.dp)
|
||||
.alpha(0.8F)
|
||||
)
|
||||
Text(
|
||||
text = message.desp,
|
||||
overflow = TextOverflow.Visible,
|
||||
fontSize = 15.sp,
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.padding(start = 16.dp, end = 16.dp, top = 4.dp, bottom = 16.dp)
|
||||
.alpha(0.5F)
|
||||
)
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -109,12 +137,17 @@ fun ImageMessageItem(message: MessageEntity, requestHolder: RequestHolder) {
|
|||
)
|
||||
}
|
||||
Card(modifier = Modifier.fillMaxWidth()) {
|
||||
AndroidView(factory = {
|
||||
ImageView(it).apply {
|
||||
scaleType = ImageView.ScaleType.FIT_CENTER
|
||||
load(message.text, requestHolder.coilImageLoader)
|
||||
}
|
||||
}, modifier = Modifier.fillMaxWidth())
|
||||
AndroidView(
|
||||
factory = {
|
||||
ImageView(it).apply {
|
||||
scaleType = ImageView.ScaleType.FIT_CENTER
|
||||
}
|
||||
},
|
||||
update = { view ->
|
||||
view.load(message.text, requestHolder.coilImageLoader)
|
||||
},
|
||||
modifier = Modifier.fillMaxWidth()
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -122,6 +155,230 @@ fun ImageMessageItem(message: MessageEntity, requestHolder: RequestHolder) {
|
|||
@ExperimentalMaterialApi
|
||||
@Composable
|
||||
fun MarkdownMessageItem(message: MessageEntity, requestHolder: RequestHolder) {
|
||||
|
||||
// val a = "![logo](./art/markwon_logo.png)\n" +
|
||||
// "\n" +
|
||||
// "# Markwon\n" +
|
||||
// "\n" +
|
||||
// "[![Build](https://github.com/noties/Markwon/workflows/Build/badge.svg)](https://github.com/noties/Markwon/actions)\n" +
|
||||
// "\n" +
|
||||
// "**Markwon** is a markdown library for Android. It parses markdown\n" +
|
||||
// "following [commonmark-spec] with the help of amazing [commonmark-java]\n" +
|
||||
// "library and renders result as _Android-native_ Spannables. **No HTML**\n" +
|
||||
// "is involved as an intermediate step. <u>**No WebView** is required</u>.\n" +
|
||||
// "It's extremely fast, feature-rich and extensible.\n" +
|
||||
// "\n" +
|
||||
// "It gives ability to display markdown in all TextView widgets\n" +
|
||||
// "(**TextView**, **Button**, **Switch**, **CheckBox**, etc), **Toasts**\n" +
|
||||
// "and all other places that accept **Spanned content**. Library provides\n" +
|
||||
// "reasonable defaults to display style of a markdown content but also \n" +
|
||||
// "gives all the means to tweak the appearance if desired. All markdown\n" +
|
||||
// "features listed in [commonmark-spec] are supported\n" +
|
||||
// "(including support for **inlined/block HTML code**, **markdown tables**,\n" +
|
||||
// "**images** and **syntax highlight**).\n" +
|
||||
// "\n" +
|
||||
// "`Markwon` comes with a [sample application](./app-sample/). It is a\n" +
|
||||
// "collection of library usages that comes with search and source code for\n" +
|
||||
// "each code sample.\n" +
|
||||
// "\n" +
|
||||
// "Since version **4.2.0** **Markwon** comes with an [editor](./markwon-editor/) to _highlight_ markdown input\n" +
|
||||
// "as user types (for example in **EditText**).\n" +
|
||||
// "\n" +
|
||||
// "[commonmark-spec]: https://spec.commonmark.org/0.28/\n" +
|
||||
// "[commonmark-java]: https://github.com/atlassian/commonmark-java/blob/master/README.md\n" +
|
||||
// "\n" +
|
||||
// "## Installation\n" +
|
||||
// "\n" +
|
||||
// "![stable](https://img.shields.io/maven-central/v/io.noties.markwon/core.svg?label=stable)\n" +
|
||||
// "![snapshot](https://img.shields.io/nexus/s/https/oss.sonatype.org/io.noties.markwon/core.svg?label=snapshot)\n" +
|
||||
// "\n" +
|
||||
// "```kotlin\n" +
|
||||
// "implementation \"io.noties.markwon:core:\"\n" +
|
||||
// "```\n" +
|
||||
// "\n" +
|
||||
// "Full list of available artifacts is present in the [install section](https://noties.github.io/Markwon/docs/v4/install.html)\n" +
|
||||
// "of the [documentation] web-site.\n" +
|
||||
// "\n" +
|
||||
// "Please visit [documentation] web-site for further reference.\n" +
|
||||
// "\n" +
|
||||
// "\n" +
|
||||
// "> You can find previous version of Markwon in [2.x.x](https://github.com/noties/Markwon/tree/2.x.x)\n" +
|
||||
// "and [3.x.x](https://github.com/noties/Markwon/tree/3.x.x) branches\n" +
|
||||
// "\n" +
|
||||
// "## Supported markdown features:\n" +
|
||||
// "* Emphasis (`*`, `_`)\n" +
|
||||
// "* Strong emphasis (`**`, `__`)\n" +
|
||||
// "* Strike-through (`~~`)\n" +
|
||||
// "* Headers (`#{1,6}`)\n" +
|
||||
// "* Links (`[]()` && `[][]`)\n" +
|
||||
// "* Images\n" +
|
||||
// "* Thematic break (`---`, `***`, `___`)\n" +
|
||||
// "* Quotes & nested quotes (`>{1,}`)\n" +
|
||||
// "* Ordered & non-ordered lists & nested ones\n" +
|
||||
// "* Inline code\n" +
|
||||
// "* Code blocks\n" +
|
||||
// "* Tables (*with limitations*)\n" +
|
||||
// "* Syntax highlight\n" +
|
||||
// "* LaTeX formulas\n" +
|
||||
// "* HTML\n" +
|
||||
// " * Emphasis (`<i>`, `<em>`, `<cite>`, `<dfn>`)\n" +
|
||||
// " * Strong emphasis (`<b>`, `<strong>`)\n" +
|
||||
// " * SuperScript (`<sup>`)\n" +
|
||||
// " * SubScript (`<sub>`)\n" +
|
||||
// " * Underline (`<u>`, `ins`)\n" +
|
||||
// " * Strike-through (`<s>`, `<strike>`, `<del>`)\n" +
|
||||
// " * Link (`a`)\n" +
|
||||
// " * Lists (`ul`, `ol`)\n" +
|
||||
// " * Images (`img` will require configured image loader)\n" +
|
||||
// " * Blockquote (`blockquote`)\n" +
|
||||
// " * Heading (`h1`, `h2`, `h3`, `h4`, `h5`, `h6`)\n" +
|
||||
// " * there is support to render any HTML tag\n" +
|
||||
// "* Task lists:\n" +
|
||||
// "- [ ] Not _done_\n" +
|
||||
// " - [X] **Done** with `X`\n" +
|
||||
// " - [x] ~~and~~ **or** small `x`\n" +
|
||||
// "---\n" +
|
||||
// "\n" +
|
||||
// "## Screenshots\n" +
|
||||
// "\n" +
|
||||
// "Taken with default configuration (except for image loading) in [sample app](./app-sample/):\n" +
|
||||
// "\n" +
|
||||
// "<a href=\"./art/mw_light_01.png\"><img src=\"./art/mw_light_01.png\" width=\"30%\" /></a>\n" +
|
||||
// "<a href=\"./art/mw_light_02.png\"><img src=\"./art/mw_light_02.png\" width=\"30%\" /></a>\n" +
|
||||
// "<a href=\"./art/mw_light_03.png\"><img src=\"./art/mw_light_03.png\" width=\"30%\" /></a>\n" +
|
||||
// "<a href=\"./art/mw_dark_01.png\"><img src=\"./art/mw_dark_01.png\" width=\"30%\" /></a>\n" +
|
||||
// "\n" +
|
||||
// "By default configuration uses TextView textColor for styling, so changing textColor changes style\n" +
|
||||
// "\n" +
|
||||
// "---\n" +
|
||||
// "\n" +
|
||||
// "## Documentation\n" +
|
||||
// "\n" +
|
||||
// "Please visit [documentation] web-site for reference\n" +
|
||||
// "\n" +
|
||||
// "[documentation]: https://noties.github.io/Markwon\n" +
|
||||
// "\n" +
|
||||
// "\n" +
|
||||
// "## Consulting\n" +
|
||||
// "Paid consulting is available. Please reach me out at [markwon+consulting[at]noties.io](mailto:markwon+consulting@noties.io)\n" +
|
||||
// "to discuss your idea or a project\n" +
|
||||
// "\n" +
|
||||
// "---\n" +
|
||||
// "\n" +
|
||||
// "# Demo\n" +
|
||||
// "Based on [this cheatsheet][cheatsheet]\n" +
|
||||
// "\n" +
|
||||
// "---\n" +
|
||||
// "\n" +
|
||||
// "## Headers\n" +
|
||||
// "---\n" +
|
||||
// "# Header 1\n" +
|
||||
// "## Header 2\n" +
|
||||
// "### Header 3\n" +
|
||||
// "#### Header 4\n" +
|
||||
// "##### Header 5\n" +
|
||||
// "###### Header 6\n" +
|
||||
// "---\n" +
|
||||
// "\n" +
|
||||
// "## Emphasis\n" +
|
||||
// "\n" +
|
||||
// "Emphasis, aka italics, with *asterisks* or _underscores_.\n" +
|
||||
// "\n" +
|
||||
// "Strong emphasis, aka bold, with **asterisks** or __underscores__.\n" +
|
||||
// "\n" +
|
||||
// "Combined emphasis with **asterisks and _underscores_**.\n" +
|
||||
// "\n" +
|
||||
// "Strikethrough uses two tildes. ~~Scratch this.~~\n" +
|
||||
// "\n" +
|
||||
// "---\n" +
|
||||
// "\n" +
|
||||
// "## Lists\n" +
|
||||
// "1. First ordered list item\n" +
|
||||
// "2. Another item\n" +
|
||||
// " * Unordered sub-list.\n" +
|
||||
// "1. Actual numbers don't matter, just that it's a number\n" +
|
||||
// " 1. Ordered sub-list\n" +
|
||||
// "4. And another item.\n" +
|
||||
// "\n" +
|
||||
// " You can have properly indented paragraphs within list items. Notice the blank line above, and the leading spaces (at least one, but we'll use three here to also align the raw Markdown).\n" +
|
||||
// "\n" +
|
||||
// " To have a line break without a paragraph, you will need to use two trailing spaces.\n" +
|
||||
// " Note that this line is separate, but within the same paragraph.\n" +
|
||||
// " (This is contrary to the typical GFM line break behaviour, where trailing spaces are not required.)\n" +
|
||||
// "\n" +
|
||||
// "* Unordered list can use asterisks\n" +
|
||||
// "- Or minuses\n" +
|
||||
// "+ Or pluses\n" +
|
||||
// "\n" +
|
||||
// "---\n" +
|
||||
// "\n" +
|
||||
// "## Links\n" +
|
||||
// "\n" +
|
||||
// "[I'm an inline-style link](https://www.google.com)\n" +
|
||||
// "\n" +
|
||||
// "[I'm a reference-style link][Arbitrary case-insensitive reference text]\n" +
|
||||
// "\n" +
|
||||
// "[I'm a relative reference to a repository file](../blob/master/LICENSE)\n" +
|
||||
// "\n" +
|
||||
// "[You can use numbers for reference-style link definitions][1]\n" +
|
||||
// "\n" +
|
||||
// "Or leave it empty and use the [link text itself].\n" +
|
||||
// "\n" +
|
||||
// "---\n" +
|
||||
// "\n" +
|
||||
// "## Code\n" +
|
||||
// "\n" +
|
||||
// "Inline `code` has `back-ticks around` it.\n" +
|
||||
// "\n" +
|
||||
// "```javascript\n" +
|
||||
// "var s = \"JavaScript syntax highlighting\";\n" +
|
||||
// "alert(s);\n" +
|
||||
// "```\n" +
|
||||
// "\n" +
|
||||
// "```python\n" +
|
||||
// "s = \"Python syntax highlighting\"\n" +
|
||||
// "print s\n" +
|
||||
// "```\n" +
|
||||
// "\n" +
|
||||
// "```java\n" +
|
||||
// "/**\n" +
|
||||
// " * Helper method to obtain a Parser with registered strike-through & table extensions\n" +
|
||||
// " * & task lists (added in 1.0.1)\n" +
|
||||
// " *\n" +
|
||||
// " * @return a Parser instance that is supported by this library\n" +
|
||||
// " * @since 1.0.0\n" +
|
||||
// " */\n" +
|
||||
// "@NonNull\n" +
|
||||
// "public static Parser createParser() {\n" +
|
||||
// " return new Parser.Builder()\n" +
|
||||
// " .extensions(Arrays.asList(\n" +
|
||||
// " StrikethroughExtension.create(),\n" +
|
||||
// " TablesExtension.create(),\n" +
|
||||
// " TaskListExtension.create()\n" +
|
||||
// " ))\n" +
|
||||
// " .build();\n" +
|
||||
// "}\n" +
|
||||
// "```\n" +
|
||||
// "\n" +
|
||||
// "```xml\n" +
|
||||
// "<ScrollView\n" +
|
||||
// " android:id=\"@+id/scroll_view\"\n" +
|
||||
// " android:layout_width=\"match_parent\"\n" +
|
||||
// " android:layout_height=\"match_parent\"\n" +
|
||||
// " android:layout_marginTop=\"?android:attr/actionBarSize\">\n" +
|
||||
// "\n" +
|
||||
// " <TextView\n" +
|
||||
// " android:id=\"@+id/text\"\n" +
|
||||
// " android:layout_width=\"match_parent\"\n" +
|
||||
// " android:layout_height=\"wrap_content\"\n" +
|
||||
// " android:layout_margin=\"16dip\"\n" +
|
||||
// " android:lineSpacingExtra=\"2dip\"\n" +
|
||||
// " android:textSize=\"16sp\"\n" +
|
||||
// " tools:text=\"yo\\nman\" />\n" +
|
||||
// "\n" +
|
||||
// "</ScrollView>\n" +
|
||||
// "```\n"
|
||||
|
||||
Column(
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
|
@ -135,6 +392,7 @@ fun MarkdownMessageItem(message: MessageEntity, requestHolder: RequestHolder) {
|
|||
Row(
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
// .height(IntrinsicSize.Max)
|
||||
.padding(bottom = 12.dp), verticalAlignment = Alignment.CenterVertically
|
||||
) {
|
||||
Box {
|
||||
|
@ -143,14 +401,6 @@ fun MarkdownMessageItem(message: MessageEntity, requestHolder: RequestHolder) {
|
|||
contentDescription = "",
|
||||
modifier = Modifier.size(40.dp)
|
||||
)
|
||||
// Icon(
|
||||
// painter = painterResource(id = R.drawable.ic_markdown),
|
||||
// contentDescription = "",
|
||||
// tint = MaterialTheme.colors.MBlue,
|
||||
// modifier = Modifier
|
||||
// .size(20.dp)
|
||||
// .align(alignment = Alignment.BottomCenter)
|
||||
// )
|
||||
}
|
||||
|
||||
Text(
|
||||
|
@ -166,22 +416,20 @@ fun MarkdownMessageItem(message: MessageEntity, requestHolder: RequestHolder) {
|
|||
CardItemWithContent {
|
||||
AndroidView(
|
||||
factory = { ctx ->
|
||||
android.widget.TextView(ctx).apply {
|
||||
android.widget.TextView(ctx)
|
||||
},
|
||||
update = { view ->
|
||||
view.apply {
|
||||
this.post {
|
||||
// requestHolder.markdown.configuration().theme().
|
||||
requestHolder.markdown.setMarkdown(this, message.text)
|
||||
requestHolder.markdown.setMarkdown(
|
||||
this,
|
||||
"${message.text}\n\n${message.desp}"
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
}, modifier = Modifier
|
||||
},
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
// .pointerInput(Unit) {
|
||||
// this.detectTapGestures(
|
||||
// onLongPress = {
|
||||
// Log.d("WH_", "MarkdownMessageItem: ")
|
||||
// }
|
||||
// )
|
||||
// }
|
||||
.padding(16.dp)
|
||||
)
|
||||
}
|
||||
|
|
|
@ -5,15 +5,14 @@ import android.net.Uri
|
|||
import android.util.Log
|
||||
import androidx.annotation.DrawableRes
|
||||
import androidx.compose.animation.*
|
||||
import androidx.compose.foundation.BorderStroke
|
||||
import androidx.compose.foundation.Image
|
||||
import androidx.compose.foundation.clickable
|
||||
import androidx.compose.foundation.*
|
||||
import androidx.compose.foundation.layout.*
|
||||
import androidx.compose.foundation.lazy.LazyColumn
|
||||
import androidx.compose.foundation.shape.RoundedCornerShape
|
||||
import androidx.compose.material.Card
|
||||
import androidx.compose.material.ExperimentalMaterialApi
|
||||
import androidx.compose.material.Icon
|
||||
import androidx.compose.material.*
|
||||
import androidx.compose.material.icons.Icons
|
||||
import androidx.compose.material.icons.filled.Check
|
||||
import androidx.compose.material.icons.filled.Close
|
||||
import androidx.compose.runtime.*
|
||||
import androidx.compose.ui.Alignment
|
||||
import androidx.compose.ui.Modifier
|
||||
|
@ -21,11 +20,13 @@ import androidx.compose.ui.graphics.Color
|
|||
import androidx.compose.ui.res.painterResource
|
||||
import androidx.compose.ui.res.stringResource
|
||||
import androidx.compose.ui.unit.dp
|
||||
import androidx.compose.ui.unit.sp
|
||||
import com.pushdeer.os.R
|
||||
import com.pushdeer.os.data.api.data.response.UserInfo
|
||||
import com.pushdeer.os.holder.RequestHolder
|
||||
import com.pushdeer.os.ui.compose.componment.SettingItem
|
||||
import com.pushdeer.os.ui.navigation.Page
|
||||
import com.pushdeer.os.ui.theme.MBlue
|
||||
import com.pushdeer.os.ui.theme.MainBlue
|
||||
import kotlinx.coroutines.delay
|
||||
import kotlinx.coroutines.launch
|
||||
|
@ -191,6 +192,54 @@ fun SettingPage(requestHolder: RequestHolder) {
|
|||
requestHolder.globalNavController.navigate("logdog")
|
||||
}
|
||||
}
|
||||
item {
|
||||
var useInnerWebView by remember {
|
||||
mutableStateOf(requestHolder.settingStore.useInnerWebView)
|
||||
}
|
||||
|
||||
val bgc by animateColorAsState(
|
||||
targetValue = if (useInnerWebView) MaterialTheme.colors.MBlue else Color.Transparent
|
||||
)
|
||||
val bdc by animateColorAsState(targetValue = if (!useInnerWebView) MaterialTheme.colors.MBlue else Color.Transparent)
|
||||
val fgc by animateColorAsState(targetValue = if (useInnerWebView) Color.White else MaterialTheme.colors.MBlue)
|
||||
Row(
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.height(60.dp)
|
||||
.border(1.dp, color = bdc, shape = RoundedCornerShape(10.dp))
|
||||
.clickable {
|
||||
useInnerWebView = !useInnerWebView
|
||||
requestHolder.settingStore.useInnerWebView = useInnerWebView
|
||||
},
|
||||
verticalAlignment = Alignment.CenterVertically
|
||||
) {
|
||||
Row(
|
||||
modifier = Modifier
|
||||
.weight(0.7F)
|
||||
.height(60.dp)
|
||||
.background(color = bgc, shape = RoundedCornerShape(10.dp)),
|
||||
verticalAlignment = Alignment.CenterVertically,
|
||||
horizontalArrangement = Arrangement.Center
|
||||
) {
|
||||
Text(text = "Use Inner WebView", color = fgc,fontSize = 18.sp)
|
||||
AnimatedVisibility(visible = useInnerWebView) {
|
||||
Icon(
|
||||
imageVector = Icons.Default.Check,
|
||||
contentDescription = "",
|
||||
tint = fgc
|
||||
)
|
||||
}
|
||||
}
|
||||
AnimatedVisibility(visible = !useInnerWebView) {
|
||||
Icon(
|
||||
imageVector = Icons.Default.Close,
|
||||
contentDescription = "",
|
||||
tint = fgc,
|
||||
modifier = Modifier.padding(horizontal = 22.dp)
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -0,0 +1,59 @@
|
|||
//package com.pushdeer.os.util
|
||||
//
|
||||
//import android.content.ComponentName
|
||||
//import android.content.Context
|
||||
//import android.content.pm.PackageManager
|
||||
//import com.pushdeer.os.receiver.MessageReceiver
|
||||
//import com.xiaomi.mipush.sdk.MessageHandleService
|
||||
//import com.xiaomi.mipush.sdk.PushMessageHandler
|
||||
//import com.xiaomi.push.service.XMJobService
|
||||
//import com.xiaomi.push.service.XMPushService
|
||||
//import com.xiaomi.push.service.receivers.NetworkStatusReceiver
|
||||
//import com.xiaomi.push.service.receivers.PingReceiver
|
||||
//
|
||||
//object AppComponentUtils {
|
||||
//
|
||||
// fun switchSelfHosted(isSelfHosted: Boolean, context: Context) {
|
||||
// val noneSelfHostedComponentNames = noneSelfHostedComponentNames(context)
|
||||
//// val selfHostedComponentNames = selfHostedComponentNames(context)
|
||||
// if (isSelfHosted) {
|
||||
// disableComponents(noneSelfHostedComponentNames,context.packageManager)
|
||||
// enableComponents(selfHostedComponentNames,context.packageManager)
|
||||
// } else {
|
||||
// disableComponents(selfHostedComponentNames,context.packageManager)
|
||||
// enableComponents(noneSelfHostedComponentNames,context.packageManager)
|
||||
// }
|
||||
// }
|
||||
//
|
||||
// fun noneSelfHostedComponentNames(context: Context): Array<ComponentName> {
|
||||
// return arrayOf(
|
||||
// ComponentName(context,XMPushService::class.java),
|
||||
// ComponentName(context, XMJobService::class.java),
|
||||
// ComponentName(context, PushMessageHandler::class.java),
|
||||
// ComponentName(context, MessageHandleService::class.java),
|
||||
// ComponentName(context, NetworkStatusReceiver::class.java),
|
||||
// ComponentName(context, PingReceiver::class.java),
|
||||
// ComponentName(context, MessageReceiver::class.java),
|
||||
// )
|
||||
// }
|
||||
//
|
||||
// fun enableComponents(componentNames: Array<ComponentName>, pm: PackageManager) {
|
||||
// componentNames.forEach {
|
||||
// pm.setComponentEnabledSetting(
|
||||
// it,
|
||||
// PackageManager.COMPONENT_ENABLED_STATE_ENABLED,
|
||||
// PackageManager.DONT_KILL_APP
|
||||
// )
|
||||
// }
|
||||
// }
|
||||
//
|
||||
// fun disableComponents(componentNames: Array<ComponentName>, pm: PackageManager) {
|
||||
// componentNames.forEach {
|
||||
// pm.setComponentEnabledSetting(
|
||||
// it,
|
||||
// PackageManager.COMPONENT_ENABLED_STATE_DISABLED,
|
||||
// PackageManager.DONT_KILL_APP
|
||||
// )
|
||||
// }
|
||||
// }
|
||||
//}
|
|
@ -0,0 +1,66 @@
|
|||
package com.pushdeer.os.util
|
||||
|
||||
import android.app.ActivityManager
|
||||
import android.app.Application
|
||||
import android.content.Context
|
||||
import android.os.Process
|
||||
import android.util.Log
|
||||
import com.pushdeer.os.App
|
||||
import com.pushdeer.os.keeper.RepositoryKeeper
|
||||
import com.pushdeer.os.values.AppKeys
|
||||
import com.xiaomi.channel.commonutils.logger.LoggerInterface
|
||||
import com.xiaomi.mipush.sdk.Logger
|
||||
import com.xiaomi.mipush.sdk.MiPushClient
|
||||
|
||||
object MiPushUtils {
|
||||
|
||||
private fun shouldInitMiPush(context: Context): Boolean {
|
||||
val am = context.getSystemService(Application.ACTIVITY_SERVICE) as ActivityManager
|
||||
val processInfoList = am.runningAppProcesses
|
||||
val mainProcessName = context.applicationInfo.processName
|
||||
val myPid = Process.myPid()
|
||||
for (info in processInfoList) {
|
||||
if (info.pid == myPid && mainProcessName == info.processName) {
|
||||
return true
|
||||
}
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
fun autoInit(context: Context,repositoryKeeper:RepositoryKeeper){
|
||||
if (shouldInitMiPush(context)){
|
||||
MiPushClient.registerPush(context, AppKeys.MiPush_Id, AppKeys.MiPush_Key)
|
||||
}
|
||||
//打开Log
|
||||
Logger.setLogger(context, object : LoggerInterface {
|
||||
override fun setTag(tag: String) {
|
||||
// ignore
|
||||
}
|
||||
|
||||
override fun log(content: String, t: Throwable) {
|
||||
Log.d(App.TAG, content, t)
|
||||
Thread {
|
||||
repositoryKeeper.logDogRepository.log(
|
||||
entity = "mipush",
|
||||
level = "e",
|
||||
event = t.message.toString(),
|
||||
log = content
|
||||
)
|
||||
}.start()
|
||||
}
|
||||
|
||||
override fun log(content: String) {
|
||||
Log.d(App.TAG, content)
|
||||
// Thread{
|
||||
// repositoryKeeper.logDogRepository.log(
|
||||
// entity = "mipush",
|
||||
// level = "d",
|
||||
// event = "",
|
||||
// log = content
|
||||
// )
|
||||
// }.start()
|
||||
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
|
@ -0,0 +1,11 @@
|
|||
<vector xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
android:width="24dp"
|
||||
android:height="24dp"
|
||||
android:viewportWidth="24"
|
||||
android:viewportHeight="24"
|
||||
android:tint="?attr/colorControlNormal"
|
||||
android:autoMirrored="true">
|
||||
<path
|
||||
android:fillColor="@android:color/white"
|
||||
android:pathData="M20,11H7.83l5.59,-5.59L12,4l-8,8 8,8 1.41,-1.41L7.83,13H20v-2z"/>
|
||||
</vector>
|
|
@ -0,0 +1,10 @@
|
|||
<vector xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
android:width="24dp"
|
||||
android:height="24dp"
|
||||
android:viewportWidth="24"
|
||||
android:viewportHeight="24"
|
||||
android:tint="?attr/colorControlNormal">
|
||||
<path
|
||||
android:fillColor="@android:color/white"
|
||||
android:pathData="M19,6.41L17.59,5 12,10.59 6.41,5 5,6.41 10.59,12 5,17.59 6.41,19 12,13.41 17.59,19 19,17.59 13.41,12z"/>
|
||||
</vector>
|
|
@ -0,0 +1,73 @@
|
|||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<androidx.constraintlayout.widget.ConstraintLayout xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
xmlns:app="http://schemas.android.com/apk/res-auto"
|
||||
xmlns:tools="http://schemas.android.com/tools"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="match_parent">
|
||||
|
||||
<WebView
|
||||
android:id="@+id/webview"
|
||||
android:layout_width="0dp"
|
||||
android:layout_height="0dp"
|
||||
app:layout_constraintBottom_toBottomOf="parent"
|
||||
app:layout_constraintEnd_toEndOf="parent"
|
||||
app:layout_constraintHorizontal_bias="0.0"
|
||||
app:layout_constraintStart_toStartOf="parent"
|
||||
app:layout_constraintTop_toBottomOf="@+id/constraintLayout">
|
||||
|
||||
</WebView>
|
||||
|
||||
<androidx.constraintlayout.widget.ConstraintLayout
|
||||
android:id="@+id/constraintLayout"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
app:layout_constraintEnd_toEndOf="@+id/webview"
|
||||
app:layout_constraintStart_toStartOf="parent"
|
||||
app:layout_constraintTop_toTopOf="parent">
|
||||
|
||||
<ImageButton
|
||||
android:background="@android:color/transparent"
|
||||
android:id="@+id/ib_close"
|
||||
android:layout_width="40dp"
|
||||
android:layout_height="40dp"
|
||||
android:src="@drawable/ic_baseline_close_24"
|
||||
app:layout_constraintStart_toStartOf="parent"
|
||||
app:layout_constraintTop_toTopOf="parent" />
|
||||
|
||||
<ImageButton
|
||||
android:background="@android:color/transparent"
|
||||
android:id="@+id/ib_back"
|
||||
android:layout_width="40dp"
|
||||
android:layout_height="40dp"
|
||||
app:layout_constraintBottom_toBottomOf="parent"
|
||||
app:layout_constraintStart_toEndOf="@+id/ib_close"
|
||||
app:layout_constraintTop_toTopOf="parent"
|
||||
android:src="@drawable/ic_baseline_arrow_back_24" />
|
||||
|
||||
<TextView
|
||||
android:id="@+id/tv_title"
|
||||
android:layout_width="0dp"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginStart="8dp"
|
||||
android:layout_marginEnd="8dp"
|
||||
android:lines="1"
|
||||
android:maxLines="1"
|
||||
android:minLines="1"
|
||||
android:singleLine="true"
|
||||
app:layout_constraintBottom_toBottomOf="parent"
|
||||
app:layout_constraintEnd_toEndOf="parent"
|
||||
app:layout_constraintStart_toEndOf="@+id/ib_back"
|
||||
app:layout_constraintTop_toTopOf="parent" />
|
||||
|
||||
</androidx.constraintlayout.widget.ConstraintLayout>
|
||||
|
||||
<ProgressBar
|
||||
android:id="@+id/progressBar"
|
||||
style="?android:attr/progressBarStyleHorizontal"
|
||||
android:layout_width="0dp"
|
||||
android:layout_height="wrap_content"
|
||||
app:layout_constraintEnd_toEndOf="parent"
|
||||
app:layout_constraintStart_toStartOf="parent"
|
||||
app:layout_constraintTop_toTopOf="@+id/webview" />
|
||||
|
||||
</androidx.constraintlayout.widget.ConstraintLayout>
|
|
@ -0,0 +1,4 @@
|
|||
storeFile=/Users/easy/Code/pushdeer-andorid/com.pushdeer.com
|
||||
storePassword=pushdeer.com
|
||||
keyAlias=pushdeer
|
||||
keyPassword=pushdeer.com
|
|
@ -11,5 +11,4 @@ rootProject.name = "PushDeer"
|
|||
include ':app'
|
||||
include ':common'
|
||||
include ':compose'
|
||||
include ':pushdeerclient'
|
||||
include ':pushdeercommon'
|
||||
|
|
|
@ -3,6 +3,7 @@ APP_ENV=local
|
|||
APP_KEY=
|
||||
APP_DEBUG=true
|
||||
APP_URL=http://localhost
|
||||
APP_LOCALE=zh-cn
|
||||
|
||||
LOG_CHANNEL=stack
|
||||
LOG_DEPRECATIONS_CHANNEL=null
|
||||
|
@ -60,6 +61,11 @@ GO_PUSH_IOS_CLIP_TOPIC=com.pushdeer.app.ios.Clip
|
|||
ANDROID_PACKAGE=com.pushdeer.app.os
|
||||
MIPUSH_SECRET=NONE
|
||||
|
||||
|
||||
WECHAT_APPID=
|
||||
WECHAT_APPSECRET=
|
||||
|
||||
MAX_PUSH_EVERY_USER_PER_MINUTE=60
|
||||
MAX_PUSH_KEY_PER_TIME=100
|
||||
|
||||
# 清理N天前的推送消息,0为不清理
|
||||
MAX_PUSH_EXISTS_DAYS=0
|
||||
|
|
|
@ -0,0 +1,50 @@
|
|||
<?php
|
||||
|
||||
namespace App\Console\Commands;
|
||||
|
||||
use Illuminate\Console\Command;
|
||||
use App\Models\PushDeerMessage;
|
||||
use Carbon\Carbon;
|
||||
|
||||
class CleanOldPush extends Command
|
||||
{
|
||||
/**
|
||||
* The name and signature of the console command.
|
||||
*
|
||||
* @var string
|
||||
*/
|
||||
protected $signature = 'app:cleanpush';
|
||||
|
||||
/**
|
||||
* The console command description.
|
||||
*
|
||||
* @var string
|
||||
*/
|
||||
protected $description = '清除过时的推送内容';
|
||||
|
||||
/**
|
||||
* Create a new command instance.
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
public function __construct()
|
||||
{
|
||||
parent::__construct();
|
||||
}
|
||||
|
||||
/**
|
||||
* Execute the console command.
|
||||
*
|
||||
* @return int
|
||||
*/
|
||||
public function handle()
|
||||
{
|
||||
$items_count = 10000; // 每次最多删除条数
|
||||
$days = intval(env('MAX_PUSH_EXISTS_DAYS'));
|
||||
if ($days > 0) {
|
||||
// echo "进入清理模式";
|
||||
PushDeerMessage::whereDate('created_at', '<', Carbon::now()->subDays($days))->take($items_count)->delete();
|
||||
}
|
||||
return 0;
|
||||
}
|
||||
}
|
|
@ -15,7 +15,8 @@ class Kernel extends ConsoleKernel
|
|||
*/
|
||||
protected function schedule(Schedule $schedule)
|
||||
{
|
||||
// $schedule->command('inspire')->hourly();
|
||||
// $schedule->command('app:cleanpush')->everyMinute();
|
||||
$schedule->command('app:cleanpush')->hourly();
|
||||
}
|
||||
|
||||
/**
|
||||
|
|
|
@ -60,33 +60,72 @@ class PushDeerMessageController extends Controller
|
|||
$validated['type'] = 'markdown';
|
||||
}
|
||||
|
||||
$key = PushDeerKey::where('key', $validated['pushkey'])->get()->first();
|
||||
$result = [];
|
||||
|
||||
$result = false;
|
||||
$keys = explode(",", $validated['pushkey']);
|
||||
// 去掉重复的key
|
||||
$keys = array_unique($keys);
|
||||
|
||||
if ($key) {
|
||||
$readkey = Str::random(32);
|
||||
$the_message = [];
|
||||
$the_message['uid'] = $key->uid;
|
||||
$the_message['text'] = $validated['text'];
|
||||
$the_message['desp'] = $validated['desp'];
|
||||
$the_message['type'] = $validated['type'];
|
||||
$the_message['readkey'] = $readkey;
|
||||
$the_message['pushkey_name'] = $key->name;
|
||||
$pd_message = Message::create($the_message);
|
||||
// 限制key的数量
|
||||
$keys = array_slice($keys, 0, intval(env('MAX_PUSH_KEY_PER_TIME')));
|
||||
|
||||
$devices = PushDeerDevice::where('uid', $key->uid)->get();
|
||||
foreach ($keys as $thekey) {
|
||||
$key = PushDeerKey::where('key', $thekey)->get()->first();
|
||||
$user = PushDeerUser::where('id', $key->uid)->get()->first();
|
||||
if ($user->level < 1) {
|
||||
return send_error('此账号已被停用', ErrorCode('ARGS'));
|
||||
}
|
||||
|
||||
foreach ($devices as $device) {
|
||||
if ($device) {
|
||||
$func_name = $device['type'].'_send';
|
||||
if (function_exists($func_name)) {
|
||||
$result[] = $func_name($device->is_clip, $device->device_id, $validated['text'], '', env('APP_DEBUG'));
|
||||
if ($key) {
|
||||
$readkey = Str::random(32);
|
||||
$the_message = [];
|
||||
$the_message['uid'] = $key->uid;
|
||||
$the_message['text'] = $validated['text'];
|
||||
$the_message['desp'] = $validated['desp'];
|
||||
$the_message['type'] = $validated['type'];
|
||||
$the_message['readkey'] = $readkey;
|
||||
$the_message['pushkey_name'] = $key->name;
|
||||
$pd_message = Message::create($the_message);
|
||||
|
||||
// 因为通知是悬浮显示,所以将URL改为文字提示
|
||||
// 如果需要访问原始内容,使用 $the_message['text']
|
||||
if (strtolower($validated['type'])=='image') {
|
||||
$validated['text'] = '[图片]';
|
||||
}
|
||||
|
||||
$sent = false;
|
||||
// 如果配置MQTT服务
|
||||
// if (strtolower(env('MQTT_ON')) == 'true') {
|
||||
if (env('MQTT_ON') > 0) {
|
||||
// 给 mqtt/send 转发消息
|
||||
$result[] = make_post('http://mqtt/send', [
|
||||
'key' => env('MQTT_API_KEY'),
|
||||
'content' => $the_message['text'],
|
||||
'payload' => json_encode($the_message),
|
||||
'type' => $validated['type'] == 'image' ? 'bg_url' : 'text',
|
||||
'topic' => $thekey,
|
||||
], 3);
|
||||
}
|
||||
|
||||
if ($devices = PushDeerDevice::where('uid', $key->uid)->get()) {
|
||||
foreach ($devices as $device) {
|
||||
if ($device) {
|
||||
$func_name = $device['type'].'_send';
|
||||
if (function_exists($func_name)) {
|
||||
$result[] = $func_name($device->is_clip, $device->device_id, $validated['text'], '', env('APP_DEBUG'));
|
||||
}
|
||||
}
|
||||
}
|
||||
} else {
|
||||
if (!$sent) {
|
||||
return send_error('没有可用的设备,请先注册', ErrorCode('ARGS'));
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
|
||||
return http_result(['result'=>$result]);
|
||||
}
|
||||
|
||||
|
@ -109,4 +148,10 @@ class PushDeerMessageController extends Controller
|
|||
|
||||
return send_error('消息不存在或已删除', ErrorCode('ARGS'));
|
||||
}
|
||||
|
||||
public function clean(Request $request)
|
||||
{
|
||||
PushDeerMessage::where('uid', $_SESSION['uid'])->delete();
|
||||
return http_result(['message'=>'done']);
|
||||
}
|
||||
}
|
||||
|
|
|
@ -39,6 +39,8 @@ class PushDeerUserController extends Controller
|
|||
$the_user['level'] = 1;
|
||||
|
||||
$pd_user = PushDeerUser::create($the_user);
|
||||
$pd_user['simple_token'] = 'SP'.$pd_user['id'].'P'.md5(uniqid(rand(), true));
|
||||
$pd_user->save();
|
||||
}
|
||||
|
||||
// 将数据写到session
|
||||
|
@ -47,6 +49,7 @@ class PushDeerUserController extends Controller
|
|||
$_SESSION['name'] = $pd_user['name'];
|
||||
$_SESSION['email'] = $pd_user['email'];
|
||||
$_SESSION['level'] = $pd_user['level'];
|
||||
$_SESSION['simple_token'] = $pd_user['simple_token'];
|
||||
|
||||
session_regenerate_id(true);
|
||||
$token = session_id();
|
||||
|
@ -56,11 +59,61 @@ class PushDeerUserController extends Controller
|
|||
return send_error('id_token解析错误', ErrorCode('ARGS'));
|
||||
}
|
||||
|
||||
public function wechatLogin(Request $request)
|
||||
public function loginBySimpleToken(Request $request)
|
||||
{
|
||||
$validated = $request->validate(
|
||||
[
|
||||
'code' => 'string',
|
||||
'stoken' => 'required|string',
|
||||
]
|
||||
);
|
||||
|
||||
if (!$pd_user = PushDeerUser::where('simple_token', $validated['stoken'])->get()->first()) {
|
||||
return send_error('stoken无效', ErrorCode('ARGS'));
|
||||
}
|
||||
|
||||
if ($pd_user['level']<1) {
|
||||
return send_error('账号已被禁用', ErrorCode('ARGS'));
|
||||
}
|
||||
|
||||
// 将数据写到session
|
||||
session_start();
|
||||
$_SESSION['uid'] = $pd_user['id'];
|
||||
$_SESSION['name'] = $pd_user['name'];
|
||||
$_SESSION['email'] = $pd_user['email'];
|
||||
$_SESSION['level'] = $pd_user['level'];
|
||||
|
||||
session_regenerate_id(true);
|
||||
$token = session_id();
|
||||
return http_result(['token'=>$token]);
|
||||
}
|
||||
|
||||
public function simpleTokenRegen(Request $request)
|
||||
{
|
||||
// get user by session
|
||||
if (!$pd_user = PushDeerUser::where('id', $_SESSION['uid'])->get()->first()) {
|
||||
return send_error('用户不存在', ErrorCode('ARGS'));
|
||||
}
|
||||
$pd_user['simple_token'] = 'SP'.$pd_user['id'].'P'.md5(uniqid(rand(), true));
|
||||
$pd_user->save();
|
||||
return http_result(['stoken'=>$pd_user['simple_token']]);
|
||||
}
|
||||
|
||||
public function simpleTokenRemove(Request $request)
|
||||
{
|
||||
// get user by session
|
||||
if (!$pd_user = PushDeerUser::where('id', $_SESSION['uid'])->get()->first()) {
|
||||
return send_error('用户不存在', ErrorCode('ARGS'));
|
||||
}
|
||||
$pd_user['simple_token'] = '';
|
||||
$pd_user->save();
|
||||
return http_result(['stoken'=>$pd_user['simple_token']]);
|
||||
}
|
||||
|
||||
public function wecode2unionid(Request $request)
|
||||
{
|
||||
$validated = $request->validate(
|
||||
[
|
||||
'code' => 'required|string',
|
||||
]
|
||||
);
|
||||
|
||||
|
@ -80,6 +133,47 @@ class PushDeerUserController extends Controller
|
|||
return send_error("错误的Code", ErrorCode('REMOTE'));
|
||||
}
|
||||
|
||||
return http_result(['unionid'=>$code_info['unionid']]);
|
||||
}
|
||||
|
||||
return send_error('微信Code错误', ErrorCode('ARGS'));
|
||||
}
|
||||
|
||||
public function wechatLogin(Request $request)
|
||||
{
|
||||
$validated = $request->validate(
|
||||
[
|
||||
'code' => 'required|string',
|
||||
'self_hosted' => 'integer|nullable',
|
||||
]
|
||||
);
|
||||
|
||||
if (isset($validated['code'])) {
|
||||
if (intval(@$validated['self_hosted']) != 1) {
|
||||
// 解码并进行验证
|
||||
$url = "https://api.weixin.qq.com/sns/oauth2/access_token?appid="
|
||||
.urlencode(env("WECHAT_APPID"))
|
||||
."&secret="
|
||||
.urlencode(env("WECHAT_APPSECRET"))
|
||||
."&code="
|
||||
.urlencode($validated['code'])
|
||||
."&grant_type=authorization_code";
|
||||
|
||||
$code_info = json_decode(file_get_contents($url), true);
|
||||
|
||||
if (!$code_info || !isset($code_info['access_token']) || !isset($code_info['unionid'])) {
|
||||
return send_error("错误的Code", ErrorCode('REMOTE'));
|
||||
}
|
||||
} else {
|
||||
$url = "https://api2.pushdeer.com/login/unoinid?code=".urlencode($validated['code']);
|
||||
$ret = json_decode(file_get_contents($url), true);
|
||||
if (!$ret || !isset($ret['content']) || !isset($ret['content']['unionid'])) {
|
||||
return send_error("错误的Code", ErrorCode('REMOTE'));
|
||||
}
|
||||
|
||||
$code_info = ['unionid'=>$ret['content']['unionid']];
|
||||
}
|
||||
|
||||
// 现在拿到unionid了
|
||||
|
||||
$pd_user = PushDeerUser::where('wechat_id', $code_info['unionid'])->get()->first();
|
||||
|
@ -92,6 +186,8 @@ class PushDeerUserController extends Controller
|
|||
$the_user['level'] = 1;
|
||||
|
||||
$pd_user = PushDeerUser::create($the_user);
|
||||
$pd_user['simple_token'] = 'SP'.$pd_user['id'].'P'.md5(uniqid(rand(), true));
|
||||
$pd_user->save();
|
||||
}
|
||||
|
||||
// 将数据写到session
|
||||
|
@ -100,6 +196,7 @@ class PushDeerUserController extends Controller
|
|||
$_SESSION['name'] = $pd_user['name'];
|
||||
$_SESSION['email'] = $pd_user['email'];
|
||||
$_SESSION['level'] = $pd_user['level'];
|
||||
$_SESSION['simple_token'] = $pd_user['simple_token'];
|
||||
|
||||
session_regenerate_id(true);
|
||||
$token = session_id();
|
||||
|
@ -115,7 +212,7 @@ class PushDeerUserController extends Controller
|
|||
{
|
||||
$validated = $request->validate(
|
||||
[
|
||||
'idToken' => 'string',
|
||||
'idToken' => 'required|string',
|
||||
]
|
||||
);
|
||||
|
||||
|
@ -134,6 +231,8 @@ class PushDeerUserController extends Controller
|
|||
$the_user['level'] = 1;
|
||||
|
||||
$pd_user = PushDeerUser::create($the_user);
|
||||
$pd_user['simple_token'] = 'SP'.$pd_user['id'].'P'.md5(uniqid(rand(), true));
|
||||
$pd_user->save();
|
||||
}
|
||||
|
||||
// 将数据写到session
|
||||
|
@ -142,6 +241,7 @@ class PushDeerUserController extends Controller
|
|||
$_SESSION['name'] = $pd_user['name'];
|
||||
$_SESSION['email'] = $pd_user['email'];
|
||||
$_SESSION['level'] = $pd_user['level'];
|
||||
$_SESSION['simple_token'] = $pd_user['simple_token'];
|
||||
|
||||
session_regenerate_id(true);
|
||||
$token = session_id();
|
||||
|
@ -157,8 +257,8 @@ class PushDeerUserController extends Controller
|
|||
{
|
||||
$validated = $request->validate(
|
||||
[
|
||||
'tokenorcode' => 'string|required',
|
||||
'type' => 'string|required', // apple or wechat
|
||||
'tokenorcode' => 'required|string',
|
||||
'type' => 'required|string', // apple or wechat
|
||||
]
|
||||
);
|
||||
|
||||
|
|
|
@ -67,7 +67,7 @@ function android_sender()
|
|||
return $GLOBALS['PD_ANDROID_SENDER'];
|
||||
}
|
||||
|
||||
function android_send($is_clip, $device_token, $text, $desp = '', $dev = true)
|
||||
function android_send_no_queue($is_clip, $device_token, $text, $desp = '', $dev = true)
|
||||
{
|
||||
if (strlen($desp) < 1) {
|
||||
$desp = $text;
|
||||
|
@ -88,7 +88,9 @@ function android_send($is_clip, $device_token, $text, $desp = '', $dev = true)
|
|||
$message1->build();
|
||||
|
||||
$sender = android_sender();
|
||||
return $sender->send($message1, $device_token)->getRaw();
|
||||
// return $sender->send($message1, $device_token)->getRaw();
|
||||
// 返回和 gorush 类似的格式,
|
||||
return json_encode(["counts"=>1,"logs"=>[$sender->send($message1, $device_token)->getRaw()]]);
|
||||
}
|
||||
|
||||
function ios_send($is_clip, $device_token, $text, $desp = '', $dev = true)
|
||||
|
@ -113,6 +115,9 @@ function ios_send($is_clip, $device_token, $text, $desp = '', $dev = true)
|
|||
$topic = intval($is_clip) == 1 ? config('services.go_push.ios_clip_topic') : config('services.go_push.ios_topic');
|
||||
$notification->topic = $topic;
|
||||
$notification->sound = ['volume'=>2.0];
|
||||
$notification->mutable_content = true;
|
||||
$notification->alert= ['title'=>$text, 'body'=>$desp];
|
||||
// 'subtitle'=>'from PushDeer',
|
||||
|
||||
$json = ['notifications'=>[$notification]];
|
||||
$client = new GuzzleHttp\Client();
|
||||
|
@ -123,3 +128,45 @@ function ios_send($is_clip, $device_token, $text, $desp = '', $dev = true)
|
|||
error_log('push error'. $ret);
|
||||
return $ret;
|
||||
}
|
||||
|
||||
function android_send($is_clip, $device_token, $text, $desp = '', $dev = true)
|
||||
{
|
||||
$notification = new stdClass();
|
||||
$notification->tokens = [ $device_token ];
|
||||
$notification->platform = 4;
|
||||
if (strlen($desp) > 1) {
|
||||
$notification->title = $text;
|
||||
$notification->message = $desp;
|
||||
} else {
|
||||
$notification->title = "PushDeer"; // title不能为空
|
||||
$notification->message = $text;
|
||||
}
|
||||
|
||||
$port = config('services.go_push.ios_port');
|
||||
|
||||
$json = ['notifications'=>[$notification]];
|
||||
|
||||
$client = new GuzzleHttp\Client();
|
||||
$response = $client->post('http://'.config('services.go_push.address').':'. $port .'/api/push', [
|
||||
GuzzleHttp\RequestOptions::JSON => $json
|
||||
]);
|
||||
$ret = $response->getBody()->getContents();
|
||||
error_log('push error'. $ret);
|
||||
return $ret;
|
||||
}
|
||||
|
||||
function make_post($url, $data, $timeout = 3, $form=false)
|
||||
{
|
||||
$ch = curl_init();
|
||||
curl_setopt($ch, CURLOPT_URL, $url);
|
||||
curl_setopt($ch, CURLOPT_RETURNTRANSFER, true);
|
||||
@curl_setopt($ch, CURLOPT_FOLLOWLOCATION, true);
|
||||
curl_setopt($ch, CURLOPT_POST, true);
|
||||
curl_setopt($ch, CURLOPT_TIMEOUT, $timeout);
|
||||
if ($form) {
|
||||
$data = http_build_query($data);
|
||||
}
|
||||
curl_setopt($ch, CURLOPT_POSTFIELDS, $data);
|
||||
$response = curl_exec($ch);
|
||||
return json_encode(["counts"=>1,"logs"=>[$response]]);
|
||||
}
|
||||
|
|
|
@ -65,5 +65,6 @@ class Kernel extends HttpKernel
|
|||
'verified' => \Illuminate\Auth\Middleware\EnsureEmailIsVerified::class,
|
||||
'auto.login' => \App\Http\Middleware\TokenLogin::class,
|
||||
'auth.member' => \App\Http\Middleware\EnsureMember::class,
|
||||
'json.request' => \App\Http\Middleware\JsonRequest::class,
|
||||
];
|
||||
}
|
||||
|
|
|
@ -0,0 +1,21 @@
|
|||
<?php
|
||||
|
||||
namespace App\Http\Middleware;
|
||||
|
||||
use Closure;
|
||||
use Illuminate\Http\Request;
|
||||
|
||||
class JsonRequest
|
||||
{
|
||||
public function handle(Request $request, Closure $next)
|
||||
{
|
||||
if (in_array($request->method(), ['POST', 'PUT', 'PATCH'])
|
||||
&& $request->isJson()
|
||||
) {
|
||||
$data = $request->json()->all();
|
||||
$request->request->replace(is_array($data) ? $data : []);
|
||||
}
|
||||
|
||||
return $next($request);
|
||||
}
|
||||
}
|
|
@ -57,7 +57,7 @@ class RouteServiceProvider extends ServiceProvider
|
|||
protected function configureRateLimiting()
|
||||
{
|
||||
RateLimiter::for('api', function (Request $request) {
|
||||
return Limit::perMinute(60)->by(uid() ?: $request->ip());
|
||||
return Limit::perMinute(env('MAX_PUSH_EVERY_USER_PER_MINUTE'))->by(uid() ?: $request->ip());
|
||||
});
|
||||
}
|
||||
}
|
||||
|
|
|
@ -6,6 +6,7 @@
|
|||
"license": "MIT",
|
||||
"require": {
|
||||
"php": "^7.3|^8.0",
|
||||
"firebase/php-jwt": "^6.0",
|
||||
"fruitcake/laravel-cors": "^2.0",
|
||||
"griffinledingham/php-apple-signin": "^1.1",
|
||||
"guzzlehttp/guzzle": "^7.4",
|
||||
|
|
|
@ -4,7 +4,7 @@
|
|||
"Read more about it at https://getcomposer.org/doc/01-basic-usage.md#installing-dependencies",
|
||||
"This file is @generated automatically"
|
||||
],
|
||||
"content-hash": "0f637f60000aed6893b5527dc426032b",
|
||||
"content-hash": "f1c684ce2a67b02da748765a2cc25aeb",
|
||||
"packages": [
|
||||
{
|
||||
"name": "asm89/stack-cors",
|
||||
|
@ -497,6 +497,63 @@
|
|||
],
|
||||
"time": "2020-12-29T14:50:06+00:00"
|
||||
},
|
||||
{
|
||||
"name": "firebase/php-jwt",
|
||||
"version": "v6.0.0",
|
||||
"source": {
|
||||
"type": "git",
|
||||
"url": "https://github.com/firebase/php-jwt.git",
|
||||
"reference": "0541cba75ab108ef901985e68055a92646c73534"
|
||||
},
|
||||
"dist": {
|
||||
"type": "zip",
|
||||
"url": "https://api.github.com/repos/firebase/php-jwt/zipball/0541cba75ab108ef901985e68055a92646c73534",
|
||||
"reference": "0541cba75ab108ef901985e68055a92646c73534",
|
||||
"shasum": ""
|
||||
},
|
||||
"require": {
|
||||
"php": ">=5.3.0"
|
||||
},
|
||||
"require-dev": {
|
||||
"phpunit/phpunit": ">=4.8 <=9"
|
||||
},
|
||||
"suggest": {
|
||||
"paragonie/sodium_compat": "Support EdDSA (Ed25519) signatures when libsodium is not present"
|
||||
},
|
||||
"type": "library",
|
||||
"autoload": {
|
||||
"psr-4": {
|
||||
"Firebase\\JWT\\": "src"
|
||||
}
|
||||
},
|
||||
"notification-url": "https://packagist.org/downloads/",
|
||||
"license": [
|
||||
"BSD-3-Clause"
|
||||
],
|
||||
"authors": [
|
||||
{
|
||||
"name": "Neuman Vong",
|
||||
"email": "neuman+pear@twilio.com",
|
||||
"role": "Developer"
|
||||
},
|
||||
{
|
||||
"name": "Anant Narayanan",
|
||||
"email": "anant@php.net",
|
||||
"role": "Developer"
|
||||
}
|
||||
],
|
||||
"description": "A simple library to encode and decode JSON Web Tokens (JWT) in PHP. Should conform to the current spec.",
|
||||
"homepage": "https://github.com/firebase/php-jwt",
|
||||
"keywords": [
|
||||
"jwt",
|
||||
"php"
|
||||
],
|
||||
"support": {
|
||||
"issues": "https://github.com/firebase/php-jwt/issues",
|
||||
"source": "https://github.com/firebase/php-jwt/tree/v6.0.0"
|
||||
},
|
||||
"time": "2022-01-24T15:18:34+00:00"
|
||||
},
|
||||
{
|
||||
"name": "fruitcake/laravel-cors",
|
||||
"version": "v2.0.5",
|
||||
|
@ -7995,5 +8052,5 @@
|
|||
"php": "^7.3|^8.0"
|
||||
},
|
||||
"platform-dev": [],
|
||||
"plugin-api-version": "2.0.0"
|
||||
"plugin-api-version": "2.1.0"
|
||||
}
|
||||
|
|
|
@ -15,7 +15,7 @@ return [
|
|||
|
|
||||
*/
|
||||
|
||||
'paths' => ['api/*', 'sanctum/csrf-cookie'],
|
||||
'paths' => ['message/push', 'sanctum/csrf-cookie'],
|
||||
|
||||
'allowed_methods' => ['*'],
|
||||
|
||||
|
|
|
@ -0,0 +1,32 @@
|
|||
<?php
|
||||
|
||||
use Illuminate\Database\Migrations\Migration;
|
||||
use Illuminate\Database\Schema\Blueprint;
|
||||
use Illuminate\Support\Facades\Schema;
|
||||
|
||||
class AddIndexToMessages extends Migration
|
||||
{
|
||||
/**
|
||||
* Run the migrations.
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
public function up()
|
||||
{
|
||||
Schema::table('push_deer_messages', function (Blueprint $table) {
|
||||
$table->index(['created_at'], 'created');
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Reverse the migrations.
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
public function down()
|
||||
{
|
||||
Schema::table('push_deer_messages', function (Blueprint $table) {
|
||||
$table->dropIndex('created');
|
||||
});
|
||||
}
|
||||
}
|
|
@ -0,0 +1,32 @@
|
|||
<?php
|
||||
|
||||
use Illuminate\Database\Migrations\Migration;
|
||||
use Illuminate\Database\Schema\Blueprint;
|
||||
use Illuminate\Support\Facades\Schema;
|
||||
|
||||
class AddIndexToMessageTable extends Migration
|
||||
{
|
||||
/**
|
||||
* Run the migrations.
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
public function up()
|
||||
{
|
||||
Schema::table('push_deer_messages', function (Blueprint $table) {
|
||||
$table->index(['uid'], 'uid');
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Reverse the migrations.
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
public function down()
|
||||
{
|
||||
Schema::table('push_deer_messages', function (Blueprint $table) {
|
||||
$table->dropIndex('uid');
|
||||
});
|
||||
}
|
||||
}
|
|
@ -0,0 +1,32 @@
|
|||
<?php
|
||||
|
||||
use Illuminate\Database\Migrations\Migration;
|
||||
use Illuminate\Database\Schema\Blueprint;
|
||||
use Illuminate\Support\Facades\Schema;
|
||||
|
||||
class AddSimpleTokenToUserTable extends Migration
|
||||
{
|
||||
/**
|
||||
* Run the migrations.
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
public function up()
|
||||
{
|
||||
Schema::table('push_deer_users', function (Blueprint $table) {
|
||||
$table->string('simple_token')->nullable();
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Reverse the migrations.
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
public function down()
|
||||
{
|
||||
Schema::table('push_deer_users', function (Blueprint $table) {
|
||||
$table->dropColumn('simple_token');
|
||||
});
|
||||
}
|
||||
}
|
After Width: | Height: | Size: 50 KiB |
|
@ -0,0 +1,22 @@
|
|||
<!DOCTYPE html>
|
||||
<html lang="{{ str_replace('_', '-', app()->getLocale()) }}">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<meta http-equiv="X-UA-Compatible" content="ie=edge">
|
||||
<title>PushDeer</title>
|
||||
<link rel="stylesheet" href="//cdnjs.cloudflare.com/ajax/libs/tailwindcss/2.2.19/tailwind.min.css">
|
||||
</head>
|
||||
<body class="p-5 mx-auto w-3/4">
|
||||
<div>感谢使用自架版PushDeer</div>
|
||||
<div>请用 ios14+ 系统摄像头扫码<br/>然后输入以下URL进行测试<br/><div id="url" class="bg-yellow-600 text-white p-2 my-2 rounded inline-block"></div></div>
|
||||
<img src="/code.png" alt="clip code">
|
||||
|
||||
<script>
|
||||
const hostname = window.location.hostname;
|
||||
let urlinfo = window.origin;
|
||||
if( hostname == 'localhost' || hostname == '127.0.0.1' ) urlinfo = '您使用的是本机专用地址,请使用局域网或者公网地址测试';
|
||||
window.document.querySelector("#url").innerHTML=urlinfo;
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
|
@ -23,14 +23,20 @@ use Illuminate\Support\Facades\Route;
|
|||
// 假登入,用于测试使用
|
||||
Route::any('/login/fake', 'App\Http\Controllers\PushDeerUserController@fakeLogin');
|
||||
|
||||
// 通过 simple_token 登入
|
||||
Route::any('/login/simple_token', 'App\Http\Controllers\PushDeerUserController@loginBySimpleToken');
|
||||
|
||||
// 通过 apple 返回的 idtoken 登入
|
||||
Route::post('/login/idtoken', 'App\Http\Controllers\PushDeerUserController@login');
|
||||
|
||||
// 通过 微信客户端返回的 code 登入
|
||||
Route::post('/login/wecode', 'App\Http\Controllers\PushDeerUserController@wechatLogin');
|
||||
|
||||
// 通过 微信客户端返回的 code 换取 unionid 等信息
|
||||
Route::any('/login/unoinid', 'App\Http\Controllers\PushDeerUserController@wecode2unionid');
|
||||
|
||||
// 推送消息
|
||||
Route::any('/message/push', 'App\Http\Controllers\PushDeerMessageController@push');
|
||||
Route::middleware('json.request')->any('/message/push', 'App\Http\Controllers\PushDeerMessageController@push');
|
||||
|
||||
|
||||
// 自动登入,适用于通过 token 进行操作的接口
|
||||
|
@ -56,10 +62,17 @@ Route::middleware('auto.login')->group(function () {
|
|||
// 删除一个key
|
||||
Route::post('/key/remove', 'App\Http\Controllers\PushDeerKeyController@remove');
|
||||
|
||||
// simple_token
|
||||
Route::post('/simple_token/regen', 'App\Http\Controllers\PushDeerUserController@simpleTokenRegen');
|
||||
|
||||
Route::post('/simple_token/remove', 'App\Http\Controllers\PushDeerUserController@simpleTokenRemove');
|
||||
|
||||
// 消息列表
|
||||
Route::post('/message/list', 'App\Http\Controllers\PushDeerMessageController@list');
|
||||
// 删除消息
|
||||
Route::post('/message/remove', 'App\Http\Controllers\PushDeerMessageController@remove');
|
||||
// 删除全部消息
|
||||
Route::post('/message/clean', 'App\Http\Controllers\PushDeerMessageController@clean');
|
||||
|
||||
|
||||
Route::post('/user/info', 'App\Http\Controllers\PushDeerUserController@info');
|
||||
|
|
|
@ -14,5 +14,5 @@ use Illuminate\Support\Facades\Route;
|
|||
*/
|
||||
|
||||
Route::get('/', function () {
|
||||
return ['PushDeer'=>'On'];
|
||||
return view('pushdeer', []);
|
||||
});
|
||||
|
|
|
@ -0,0 +1,205 @@
|
|||
<?php
|
||||
|
||||
namespace Tests\Feature;
|
||||
|
||||
use Illuminate\Foundation\Testing\RefreshDatabase;
|
||||
use Illuminate\Foundation\Testing\WithFaker;
|
||||
use Illuminate\Testing\Fluent\AssertableJson;
|
||||
use Tests\TestCase;
|
||||
|
||||
class ApiTest extends TestCase
|
||||
{
|
||||
private $token;
|
||||
private static $key;
|
||||
/**
|
||||
* A basic feature test example.
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
public function test_login_fake()
|
||||
{
|
||||
$response = $this->getJson('/login/fake');
|
||||
$response->assertStatus(200)->assertJson(['code'=>0]);
|
||||
$data = json_decode($response->getContent(), true);
|
||||
$this->token = $data['content']['token'];
|
||||
echo __LINE__ . " " . $this->token ."\r\n";
|
||||
// $response->dd();
|
||||
}
|
||||
|
||||
public function test_user_info()
|
||||
{
|
||||
$response = $this->post('/user/info', ['token'=>$this->token]);
|
||||
$response->assertStatus(200)
|
||||
->assertJson(['code'=>0])
|
||||
->assertJsonPath('content.id', 1)
|
||||
->assertJsonPath('content.apple_id', 'theid999');
|
||||
}
|
||||
|
||||
public function test_device_reg()
|
||||
{
|
||||
$response = $this->post('/device/reg', [
|
||||
'token'=>$this->token,
|
||||
'name'=>'Easy的iPad',
|
||||
'device_id'=>'device-token',
|
||||
'is_clip'=>0,
|
||||
]);
|
||||
|
||||
$response->assertStatus(200)
|
||||
->assertJson(['code'=>0])
|
||||
->assertJsonPath('content.devices.0.uid', '1')
|
||||
->assertJsonPath('content.devices.0.device_id', 'device-token');
|
||||
}
|
||||
|
||||
public function test_device_list()
|
||||
{
|
||||
$response = $this->post('/device/list', [
|
||||
'token'=>$this->token,
|
||||
]);
|
||||
|
||||
$response->assertStatus(200)
|
||||
->assertJson(['code'=>0])
|
||||
->assertJsonPath('content.devices.0.uid', '1')
|
||||
->assertJsonPath('content.devices.0.device_id', 'device-token');
|
||||
}
|
||||
|
||||
public function test_device_rename()
|
||||
{
|
||||
$response = $this->post('/device/rename', [
|
||||
'token'=>$this->token,
|
||||
'id' => 1,
|
||||
'name' => 'device-renamed',
|
||||
]);
|
||||
|
||||
// $response->dump();
|
||||
|
||||
$response->assertStatus(200)
|
||||
->assertJson(['code'=>0])
|
||||
->assertJsonPath('content.message', 'done');
|
||||
|
||||
$response = $this->post('/device/list', [
|
||||
'token'=>$this->token,
|
||||
]);
|
||||
|
||||
$response->assertStatus(200)
|
||||
->assertJson(['code'=>0])
|
||||
->assertSee('device-renamed');
|
||||
}
|
||||
|
||||
public function test_key_gen()
|
||||
{
|
||||
$response = $this->post('/key/gen', [
|
||||
'token'=>$this->token,
|
||||
]);
|
||||
|
||||
$response->assertStatus(200)
|
||||
->assertJson(['code'=>0])
|
||||
// ->assertJson('content.keys.0.id', 1)
|
||||
->assertSee('PDU');
|
||||
|
||||
$data = json_decode($response->getContent(), true);
|
||||
self::$key = $data['content']['keys'][0]['key'];
|
||||
}
|
||||
|
||||
public function test_key_rename()
|
||||
{
|
||||
$response = $this->post('/key/rename', [
|
||||
'token'=>$this->token,
|
||||
'id' => 1,
|
||||
'name' => 'key-renamed',
|
||||
]);
|
||||
|
||||
// $response->dump();
|
||||
|
||||
$response->assertStatus(200)
|
||||
->assertJson(['code'=>0])
|
||||
->assertJsonPath('content.message', 'done');
|
||||
|
||||
$response = $this->post('/key/list', [
|
||||
'token'=>$this->token,
|
||||
]);
|
||||
|
||||
$response->assertStatus(200)
|
||||
->assertJson(['code'=>0])
|
||||
->assertSee('key-renamed');
|
||||
}
|
||||
|
||||
public function test_message_push()
|
||||
{
|
||||
$response = $this->post('/message/push', [
|
||||
'pushkey'=>self::$key,
|
||||
'text' => 'tested',
|
||||
'type' => 'text',
|
||||
]);
|
||||
|
||||
$response->assertStatus(200)
|
||||
->assertJson(['code'=>0])
|
||||
->assertSee('success');
|
||||
}
|
||||
|
||||
public function test_message_list()
|
||||
{
|
||||
$response = $this->post('/message/list', [
|
||||
'token'=>$this->token,
|
||||
]);
|
||||
|
||||
$response->assertStatus(200)
|
||||
->assertJson(['code'=>0])
|
||||
->assertSee('tested');
|
||||
}
|
||||
|
||||
public function test_message_remove()
|
||||
{
|
||||
$response = $this->post('/message/remove', [
|
||||
'token'=>$this->token,
|
||||
'id' => 1,
|
||||
]);
|
||||
|
||||
// $response->dump();
|
||||
|
||||
$response->assertStatus(200)
|
||||
->assertJson(['code'=>0])
|
||||
->assertJsonPath('content.message', 'done');
|
||||
}
|
||||
|
||||
public function test_key_regen()
|
||||
{
|
||||
$response = $this->post('/key/regen', [
|
||||
'token'=>$this->token,
|
||||
'id' => 1,
|
||||
]);
|
||||
|
||||
// $response->dump();
|
||||
|
||||
$response->assertStatus(200)
|
||||
->assertJson(['code'=>0])
|
||||
->assertJsonPath('content.message', 'done');
|
||||
}
|
||||
|
||||
public function test_key_remove()
|
||||
{
|
||||
$response = $this->post('/key/remove', [
|
||||
'token'=>$this->token,
|
||||
'id' => 1,
|
||||
]);
|
||||
|
||||
// $response->dump();
|
||||
|
||||
$response->assertStatus(200)
|
||||
->assertJson(['code'=>0])
|
||||
->assertJsonPath('content.message', 'done');
|
||||
}
|
||||
|
||||
public function test_device_remove()
|
||||
{
|
||||
$response = $this->post('/device/remove', [
|
||||
'token'=>$this->token,
|
||||
'id' => 1,
|
||||
]);
|
||||
|
||||
// $response->dump();
|
||||
|
||||
$response->assertStatus(200)
|
||||
->assertJson(['code'=>0])
|
||||
->assertJsonPath('content.message', 'done');
|
||||
}
|
||||
}
|
|
@ -0,0 +1,31 @@
|
|||
# 自架版当前状态
|
||||
|
||||
> 暂停开发,等待新开发者接手
|
||||
|
||||
由于之前负责Android版开发的同学忙于其他事情已退出开发,目前Android版需要新的同学参与,欢迎感兴趣的同学申请 easychen[A.T]gmail.com
|
||||
|
||||
# 问题
|
||||
|
||||
小米推送不再对个人开放,自架系统的推送权限申请很麻烦,而公开secret会对官方应用带来安全风险。
|
||||
|
||||
# 解决思路
|
||||
|
||||
采用websocket/MQTT来实现自架版的推送。
|
||||
|
||||
优点:
|
||||
|
||||
- 不受推送服务商限制,可以按自己需求随意推送
|
||||
|
||||
缺点:
|
||||
|
||||
- 无法实现后台推送,应用必须常驻后台。但考虑到MiPush在非小米系手机上也有同样的问题,也不是不能接受。
|
||||
|
||||
# 实现方案
|
||||
|
||||
优先采用MQTT协议,因为PushDeer自架版Docker镜像本来就已经支持了MQTT Server( 可参考[此文档](https://github.com/easychen/pushdeer/tree/main/iot#%E5%BC%80%E5%90%AFmqtt%E6%9C%8D%E5%8A%A1) 配置),因此只需要在客户端实现即可。
|
||||
|
||||
以下是UI原型:
|
||||
|
||||
![](image/2022-03-30-18-14-05.png)
|
||||
|
||||
Android实现需要将MQTT启动为服务并常驻后台,可在顶栏显示一个常驻图标,点击后进入应用。
|
After Width: | Height: | Size: 166 KiB |
After Width: | Height: | Size: 167 KiB |
After Width: | Height: | Size: 77 KiB |
After Width: | Height: | Size: 90 KiB |
After Width: | Height: | Size: 50 KiB |
After Width: | Height: | Size: 279 KiB |
After Width: | Height: | Size: 1.8 MiB |
13
doc/安装文档.md
|
@ -44,6 +44,8 @@ services:
|
|||
- DB_TIMEZONE=+08:00
|
||||
- DB_USERNAME=pushdeer
|
||||
- APP_DEBUG=false
|
||||
- GO_PUSH_IOS_TOPIC=com.pushdeer.self.ios
|
||||
- GO_PUSH_IOS_CLIP_TOPIC=com.pushdeer.self.ios.Clip
|
||||
```
|
||||
|
||||
### STEP2:配置 SSL 证书
|
||||
|
@ -51,10 +53,15 @@ services:
|
|||
#### 放置证书文件
|
||||
|
||||
申请域名对应的 SSL 证书,获得证书文件( `example.crt` )和私钥文件( `example.key`)。
|
||||
在项目根目录下建立 `ssl` 目录,将证书文件重命名为 `server.crt` 和 私钥文件重命名为 `server.key` 。
|
||||
在[项目根目录](https://github.com/easychen/pushdeer)下建立 `ssl` 目录,将证书文件重命名为 `server.crt` 和 私钥文件重命名为 `server.key` 。
|
||||
|
||||
如果你下载的证书中还包含根证书(root_crt),可将根证书文本追加到 `server.crt` 之后。
|
||||
|
||||
<!-- #### 通过Dockfile将证书复制到镜像内
|
||||
|
||||
[按提示修改Dockerfile](https://github.com/easychen/pushdeer/blob/main/docker/web/Dockerfile#L27)
|
||||
-->
|
||||
|
||||
#### 修改虚拟host配置
|
||||
|
||||
修改 `docker/web/vhost.conf` 中被注释掉的[这三行](https://github.com/easychen/pushdeer/blob/10e4d3bb62d8d66d4739598a8f4af32eda4cceef/docker/web/vhost.conf#L27)。
|
||||
|
@ -80,6 +87,10 @@ services:
|
|||
|
||||
#### iOS 推送
|
||||
|
||||
可直接使用`PushDeer·自架版`客户端。
|
||||
|
||||
如自行编译客户端,需生成APP和Clip的推送证书,并按以下方式配置:
|
||||
|
||||
进入 push 目录,修改 `*.yml.sample` 为 `*.yml`。其中iOS应用和Clip使用两个分开的证书进行推送,`ios.yml` 是APP的配置、`clip.yml` 是Clip的配置。注意根据开发和产品状态,修改`yml`中的值`production`。
|
||||
|
||||
默认配置中,`c.p12` 是APP的推送证书、`cc.p12`是Clip的推送证书。
|
||||
|
|
118
doc/调试文档.md
|
@ -1,9 +1,121 @@
|
|||
# 调试FAQ
|
||||
|
||||
## 重装容器
|
||||
|
||||
启动过的容器会将数据存储在数据目录,如果需要完全重装,需要手动清除 volume 中的数据。
|
||||
|
||||
首先通过 docker ps 查到 pushdeer_app_1 容器对应的 id。
|
||||
然后运行以下命令删除容器并同时删除 volume。
|
||||
|
||||
```bash
|
||||
docker rm -v <id>
|
||||
```
|
||||
|
||||
你也可以通过 docker volume 命令手工删除数据目录。首先通过 `docker volume ls` 查找数据目录:
|
||||
|
||||
```
|
||||
DRIVER VOLUME NAME
|
||||
local pushdeer_mariadb_data
|
||||
```
|
||||
|
||||
然后运行:
|
||||
|
||||
`docker volume inspect pushdeer_mariadb_data` 查看详细信息:
|
||||
|
||||
```json
|
||||
[
|
||||
{
|
||||
"CreatedAt": "2022-03-04T15:18:11+08:00",
|
||||
"Driver": "local",
|
||||
"Labels": null,
|
||||
"Mountpoint": "/var/lib/docker/volumes/pushdeer_mariadb_data/_data",
|
||||
"Name": "pushdeer_mariadb_data",
|
||||
"Options": null,
|
||||
"Scope": "local"
|
||||
}
|
||||
]
|
||||
```
|
||||
|
||||
删除对应数据目录 `/var/lib/docker/volumes/pushdeer_mariadb_data/_data` 。
|
||||
|
||||
|
||||
## 服务启动调试
|
||||
|
||||
首先确定API服务是否正常启动。为了能查看到详细信息,请先将已经启动的服务停止:
|
||||
|
||||
```
|
||||
docker-compose down
|
||||
```
|
||||
|
||||
然后将原来启动命令中的 `-d` 去掉,再次启动。比如使用自架版,则改为:
|
||||
|
||||
```
|
||||
docker-compose -f docker-compose.self-hosted.yml up --build
|
||||
```
|
||||
|
||||
这时候就可以在终端中看到启动的详细信息了。正常情况下,最后显示的信息应该类似:
|
||||
|
||||
```
|
||||
app_1 | [SYSLOG] syslog-ng[87]: syslog-ng starting up; version='3.19.1'
|
||||
app_1 | [Mon Feb 21 06:36:37.921458 2022] [mpm_event:notice] [pid 92:tid 140276671722624] AH00489: Apache/2.4.38 (Debian) OpenSSL/1.1.1d configured -- resuming normal operations
|
||||
app_1 | [Mon Feb 21 06:36:37.922027 2022] [core:notice] [pid 92:tid 140276671722624] AH00094: Command line: 'apache2 -D FOREGROUND -D APACHE_LOCK_DIR'
|
||||
app_1 | [21-Feb-2022 06:36:37] NOTICE: fpm is running, pid 91
|
||||
app_1 | [21-Feb-2022 06:36:37] NOTICE: ready to handle connections
|
||||
app_1 | 2022-02-21 06:36:38,997 INFO success: syslogd entered RUNNING state, process has stayed up for > than 1 seconds (startsecs)
|
||||
app_1 | 2022-02-21 06:36:38,997 INFO success: redis entered RUNNING state, process has stayed up for > than 1 seconds (startsecs)
|
||||
app_1 | 2022-02-21 06:36:38,997 INFO success: push-ios entered RUNNING state, process has stayed up for > than 1 seconds (startsecs)
|
||||
app_1 | 2022-02-21 06:36:38,997 INFO success: push-clip entered RUNNING state, process has stayed up for > than 1 seconds (startsecs)
|
||||
```
|
||||
|
||||
## End point 连通性测试
|
||||
|
||||
如果API服务启动没有报错信息,而客户端依然提示连接API错误,请测试 end point 和API服务器之间的连通性。
|
||||
|
||||
在安装APP的设备上打开浏览器,输入 API end point 地址访问,查看是否可以看到默认的提示信息。
|
||||
|
||||
也可以用 postman 等工具结合[API文档](https://github.com/easychen/pushdeer#api-%E8%AF%B4%E6%98%8E) 进行测试。
|
||||
|
||||
为了方便测试(比如使用模拟登入),可修改你使用的 `docker-compose` 文件,将 `APP_DEBUG` 设置为 `true`。
|
||||
|
||||
|
||||
比如使用自架版,请修改 `docker-compose.self-hosted.yml`:
|
||||
|
||||
```
|
||||
- GO_PUSH_IOS_TOPIC=com.pushdeer.self.ios
|
||||
- GO_PUSH_IOS_CLIP_TOPIC=com.pushdeer.self.ios.Clip
|
||||
- APP_DEBUG=true
|
||||
```
|
||||
|
||||
## 获取报错信息
|
||||
|
||||
如果某些请求返回状态码不是200,为了进一步获取错误信息,在确保`docker-compose` 文件中 `APP_DEBUG` 设置为 `true` 的前提下,通过浏览器(GET请求)或者postman等工具(POST请求)访问对应的接口,即可看到详细的错误信息。
|
||||
|
||||
[postman使用教程](https://blog.51cto.com/u_10698621/3646204)
|
||||
|
||||
## 推送调试
|
||||
|
||||
命令行测试:
|
||||
如果您的API服务架设成功,且除了推送之外的功能均可使用,那么您可以进入到 docker 容器中进一步调试推送服务。
|
||||
|
||||
首先运行
|
||||
|
||||
```bash
|
||||
../docker/web/gorush -ios -m "推送内容" -c "ios.yml" --topic "com.pushdeer.app.ios" -t "DeviceToken"
|
||||
```
|
||||
sudo docker ps
|
||||
```
|
||||
|
||||
查看 app_1 对应的 contianer id。
|
||||
|
||||
然后运行
|
||||
|
||||
```
|
||||
sudo docker exec -it <id> /bin/bash
|
||||
```
|
||||
|
||||
进入容器。
|
||||
|
||||
接着就可以通过命令行进行推送了测试:
|
||||
|
||||
```bash
|
||||
cd /app/push && /data/gorush -ios -m "推送内容" -c "ios.yml" --topic "com.pushdeer.self.ios" -t "DeviceToken" --production
|
||||
```
|
||||
|
||||
其中 DeviceToken 可以通过[设备列表](https://github.com/easychen/pushdeer#%E8%AE%BE%E5%A4%87%E5%88%97%E8%A1%A8) 接口获得。
|
||||
|
|
|
@ -0,0 +1,56 @@
|
|||
version: '2.1'
|
||||
services:
|
||||
mariadb:
|
||||
image: 'mariadb:10.5.8-focal'
|
||||
healthcheck:
|
||||
test: ["CMD", "mysqladmin", "ping", "--silent","--password=$$MYSQL_ROOT_PASSWORD"]
|
||||
timeout: 10s
|
||||
retries: 3
|
||||
volumes:
|
||||
- 'mariadb_data:/var/lib/mysql'
|
||||
environment:
|
||||
- MYSQL_ROOT_PASSWORD=theVeryp@ssw0rd
|
||||
- MYSQL_DATABASE=pushdeer
|
||||
# ports:
|
||||
# - '3306:3306'
|
||||
redis:
|
||||
image: 'bitnami/redis:6.0.16'
|
||||
healthcheck:
|
||||
test: ["CMD", "redis-cli","ping"]
|
||||
environment:
|
||||
- ALLOW_EMPTY_PASSWORD=yes
|
||||
app:
|
||||
#image: 'webdevops/php-apache:8.0-alpine'
|
||||
build: './docker/web/'
|
||||
ports:
|
||||
- '8800:80'
|
||||
volumes:
|
||||
- './:/app'
|
||||
depends_on:
|
||||
mariadb:
|
||||
condition: service_healthy
|
||||
redis:
|
||||
condition: service_healthy
|
||||
environment:
|
||||
- DB_HOST=mariadb
|
||||
- DB_PORT=3306
|
||||
- DB_USERNAME=root
|
||||
- DB_DATABASE=pushdeer
|
||||
- DB_PASSWORD=theVeryp@ssw0rd
|
||||
- GO_PUSH_IOS_TOPIC=com.pushdeer.self.ios
|
||||
- GO_PUSH_IOS_CLIP_TOPIC=com.pushdeer.self.ios.Clip
|
||||
- APP_DEBUG=false
|
||||
- MQTT_API_KEY=9LKo3
|
||||
- MQTT_ON=false
|
||||
# mqtt:
|
||||
# image: 'ccr.ccs.tencentyun.com/ftqq/pushdeeresp'
|
||||
# ports:
|
||||
# - '1883:1883'
|
||||
# environment:
|
||||
# - API_KEY=9LKo3
|
||||
# - MQTT_PORT=1883
|
||||
# - MQTT_USER=easy
|
||||
# - MQTT_PASSWORD=y0urp@ss
|
||||
# - MQTT_BASE_TOPIC=default
|
||||
volumes:
|
||||
mariadb_data:
|
|
@ -0,0 +1,20 @@
|
|||
# 已废弃
|
||||
# version: '2'
|
||||
# services:
|
||||
# app:
|
||||
# image: 'ccr.ccs.tencentyun.com/ftqq/pushdeercore'
|
||||
# ports:
|
||||
# - '9000:9000'
|
||||
# environment:
|
||||
# - DB_HOST=host.docker.internal
|
||||
# - DB_PORT=3306
|
||||
# - DB_USERNAME=root
|
||||
# - DB_DATABASE=pushdeer_local
|
||||
# - DB_PASSWORD=
|
||||
# - DB_TIMEZONE=+08:00
|
||||
# - GO_PUSH_IOS_TOPIC=com.pushdeer.self.ios
|
||||
# - GO_PUSH_IOS_CLIP_TOPIC=com.pushdeer.self.ios.Clip
|
||||
# - APP_DEBUG=false
|
||||
# - WEB_PHP_SOCKET=127.0.0.1:8000
|
||||
# extra_hosts:
|
||||
# - "host.docker.internal:host-gateway"
|
|
@ -1,7 +1,11 @@
|
|||
version: '2'
|
||||
version: '2.1'
|
||||
services:
|
||||
mariadb:
|
||||
image: 'mariadb:10.5.8-focal'
|
||||
healthcheck:
|
||||
test: ["CMD", "mysqladmin", "ping", "--silent","--password=$$MYSQL_ROOT_PASSWORD"]
|
||||
timeout: 10s
|
||||
retries: 3
|
||||
volumes:
|
||||
- 'mariadb_data:/var/lib/mysql'
|
||||
environment:
|
||||
|
@ -11,6 +15,8 @@ services:
|
|||
- '3306:3306'
|
||||
redis:
|
||||
image: 'bitnami/redis:6.0.16'
|
||||
healthcheck:
|
||||
test: ["CMD", "redis-cli","ping"]
|
||||
environment:
|
||||
- ALLOW_EMPTY_PASSWORD=yes
|
||||
app:
|
||||
|
@ -21,8 +27,10 @@ services:
|
|||
volumes:
|
||||
- './:/app'
|
||||
depends_on:
|
||||
- mariadb
|
||||
- redis
|
||||
mariadb:
|
||||
condition: service_healthy
|
||||
redis:
|
||||
condition: service_healthy
|
||||
environment:
|
||||
- DB_HOST=mariadb
|
||||
- DB_PORT=3306
|
||||
|
|
|
@ -0,0 +1,23 @@
|
|||
FROM node:16-alpine3.15
|
||||
|
||||
RUN sed -i 's/dl-cdn.alpinelinux.org/mirrors.aliyun.com/g' /etc/apk/repositories
|
||||
RUN sed -i 's/dl-4.alpinelinux.org/mirrors.aliyun.com/g' /etc/apk/repositories
|
||||
RUN apk --no-cache add mosquitto mosquitto-clients
|
||||
RUN npm install -g forever
|
||||
|
||||
ADD mosquitto.conf /mosquitto.conf
|
||||
RUN /usr/sbin/mosquitto -c /mosquitto.conf -v -d
|
||||
|
||||
# 测试时注释掉下一行
|
||||
COPY api /api
|
||||
|
||||
COPY init.sh /init.sh
|
||||
RUN chmod +x /init.sh
|
||||
EXPOSE 1883
|
||||
EXPOSE 9001
|
||||
EXPOSE 80
|
||||
|
||||
ENTRYPOINT ["/bin/sh", "/init.sh"]
|
||||
|
||||
|
||||
# ENTRYPOINT ["/usr/sbin/mosquitto", "-c", "/mosquitto.conf",""]
|
|
@ -0,0 +1,20 @@
|
|||
FROM node:16-alpine3.15
|
||||
|
||||
RUN apk --no-cache add mosquitto mosquitto-clients
|
||||
RUN npm install -g forever
|
||||
|
||||
ADD mosquitto.conf /mosquitto.conf
|
||||
RUN /usr/sbin/mosquitto -c /mosquitto.conf -d
|
||||
|
||||
# 测试时注释掉下一行
|
||||
# COPY api /api
|
||||
|
||||
COPY init.sh /init.sh
|
||||
RUN chmod +x /init.sh
|
||||
EXPOSE 1883
|
||||
EXPOSE 80
|
||||
|
||||
ENTRYPOINT ["/bin/sh", "/init.sh"]
|
||||
|
||||
|
||||
# ENTRYPOINT ["/usr/sbin/mosquitto", "-c", "/mosquitto.conf",""]
|
|
@ -0,0 +1,16 @@
|
|||
<?php
|
||||
/**
|
||||
* This is project's console commands configuration for Robo task runner.
|
||||
*
|
||||
* @see http://robo.li/
|
||||
*/
|
||||
class RoboFile extends \Robo\Tasks
|
||||
{
|
||||
// define public methods as commands
|
||||
public function buildImage()
|
||||
{
|
||||
$this->_exec("docker build -t pushdeeresp ./ ");
|
||||
$this->_exec("docker tag pushdeeresp ccr.ccs.tencentyun.com/ftqq/pushdeeresp");
|
||||
$this->_exec("docker push ccr.ccs.tencentyun.com/ftqq/pushdeeresp");
|
||||
}
|
||||
}
|
|
@ -0,0 +1 @@
|
|||
node_modules
|
|
@ -0,0 +1,68 @@
|
|||
const express = require('express');
|
||||
const path = require('path');
|
||||
const fs = require('fs');
|
||||
const timeout = require('connect-timeout')
|
||||
const app = express();
|
||||
|
||||
var multer = require('multer');
|
||||
var forms = multer();
|
||||
const bodyParser = require('body-parser')
|
||||
app.use(bodyParser.json());
|
||||
app.use(forms.array());
|
||||
app.use(bodyParser.urlencoded({ extended: true }));
|
||||
|
||||
app.get(`/`, (req, res) => {
|
||||
res.sendFile(path.join(__dirname, 'index.html'));
|
||||
});
|
||||
|
||||
function haltOnTimedout (req, res, next) {
|
||||
if (!req.timedout) next()
|
||||
}
|
||||
|
||||
app.all('/send', timeout('3s'), haltOnTimedout, async (req, res) => {
|
||||
|
||||
console.log( process.env , req.body, req.query );
|
||||
if( process.env.API_KEY )
|
||||
{
|
||||
const in_key = req.body.key||req.query.key || "";
|
||||
if( in_key != process.env.API_KEY ) return res.status(403).send(`api key error `+ in_key );
|
||||
}
|
||||
|
||||
const msg_type = (req.body.type || req.query.type) == "bg_url" ? "bg_url" : "text";
|
||||
let msg_content = req.body.content || req.query.content || "";
|
||||
if( msg_type== 'bg_url' && msg_content.length < 1 ) res.status(500).send('Bg url cannot be empty');
|
||||
if( msg_type== 'bg_url' ) msg_content = msg_content.replace("https://","http://");
|
||||
const msg_topic = req.body.topic || req.query.topic || process.env.MQTT_BASE_TOPIC;
|
||||
|
||||
|
||||
const mqtt = require('async-mqtt') // require mqtt
|
||||
const mqtt_url = 'mqtt://'+process.env.MQTT_USER+':'+ process.env.MQTT_PASSWORD +'@127.0.0.1:'+process.env.MQTT_PORT;
|
||||
console.log( mqtt_url );
|
||||
const client = mqtt.connect('mqtt://127.0.0.1:'+(process.env.MQTT_PORT||'1883'), {
|
||||
clean: true,
|
||||
connectTimeout: 2800,
|
||||
clientId: 'DeerHttpApi',
|
||||
username: process.env.MQTT_USER||"",
|
||||
password: process.env.MQTT_PASSWORD||"",
|
||||
});
|
||||
try {
|
||||
await client.publish(msg_topic+'_'+msg_type,msg_content);
|
||||
} catch (error) {
|
||||
console.log(error)
|
||||
}finally {
|
||||
await client.end();
|
||||
}
|
||||
|
||||
|
||||
return res.status(200).json('done');
|
||||
});
|
||||
|
||||
// Error handler
|
||||
app.use(function (err, req, res, next) {
|
||||
console.error(err);
|
||||
res.status(500).send('Internal Serverless Error');
|
||||
});
|
||||
|
||||
app.listen(80, () => {
|
||||
console.log(`Server start on http://localhost`);
|
||||
});
|
|
@ -0,0 +1,16 @@
|
|||
{
|
||||
"name": "api",
|
||||
"version": "1.0.0",
|
||||
"main": "index.js",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"async-mqtt": "^2.6.2",
|
||||
"body-parser": "^1.19.1",
|
||||
"connect-timeout": "^1.9.0",
|
||||
"express": "^4.17.2",
|
||||
"multer": "^1.4.4"
|
||||
},
|
||||
"devDependencies": {
|
||||
"nodemon": "^2.0.15"
|
||||
}
|
||||
}
|
|
@ -0,0 +1,17 @@
|
|||
version: '2'
|
||||
services:
|
||||
api:
|
||||
build:
|
||||
context: ./
|
||||
dockerfile: ./Dockerfile.dev
|
||||
volumes:
|
||||
- './api:/api'
|
||||
ports:
|
||||
- '80:80'
|
||||
- '1883:1883'
|
||||
environment:
|
||||
- API_KEY=aPiKe1
|
||||
- MQTT_USER=easy
|
||||
- MQTT_PASSWORD=y0urp@ss
|
||||
- MQTT_PORT=1883
|
||||
- MQTT_BASE_TOPIC=default
|
|
@ -0,0 +1,17 @@
|
|||
version: '2'
|
||||
services:
|
||||
api:
|
||||
build:
|
||||
context: ./
|
||||
dockerfile: ./Dockerfile
|
||||
# volumes:
|
||||
# - './api:/api'
|
||||
ports:
|
||||
- '80:80'
|
||||
- '1883:1883'
|
||||
environment:
|
||||
- API_KEY=aPiKe1
|
||||
- MQTT_USER=easy
|
||||
- MQTT_PASSWORD=y0urp@ss
|
||||
- MQTT_PORT=1883
|
||||
- MQTT_BASE_TOPIC=default
|
|
@ -0,0 +1,10 @@
|
|||
#!/bin/sh
|
||||
|
||||
# 生成密码文件
|
||||
echo "$MQTT_USER:$MQTT_PASSWORD" > '/mospass.txt'
|
||||
mosquitto_passwd -U /mospass.txt
|
||||
|
||||
# 启动 express
|
||||
forever start /api/index.js
|
||||
# mosquitto -c /mosquitto.conf -v
|
||||
mosquitto -c /mosquitto.conf
|
|
@ -0,0 +1,6 @@
|
|||
listener 1883
|
||||
listener 9001
|
||||
protocol websockets
|
||||
allow_anonymous false
|
||||
log_dest stdout
|
||||
password_file /mospass.txt
|
|
@ -20,5 +20,12 @@ RUN chmod +x /data/gorush
|
|||
ADD supervisord-ios.conf /opt/docker/etc/supervisor.d/push-ios.conf
|
||||
ADD supervisord-clip.conf /opt/docker/etc/supervisor.d/push-clip.conf
|
||||
|
||||
ADD larave-cron /etc/cron.d
|
||||
RUN chmod +x /etc/cron.d/larave-cron
|
||||
|
||||
# 配置 https
|
||||
# 在本目录下创建ssl目录,放入证书(server.crt,server.key),然后去掉下一行的注释
|
||||
# ADD ssl /app/ssl
|
||||
|
||||
EXPOSE 80
|
||||
|
|
@ -1,13 +1,19 @@
|
|||
FROM webdevops/php-apache:8.0
|
||||
|
||||
RUN apt-get update && apt-get install -y redis-server
|
||||
# 首先配置 vhost
|
||||
COPY vhost.conf /opt/docker/etc/httpd/vhost.conf
|
||||
# COPY web.vhost.conf /opt/docker/etc/httpd/vhost.common.d/
|
||||
|
||||
# 然后运行初始化脚本
|
||||
# https://dockerfile.readthedocs.io/en/latest/content/Customization/provisioning.html
|
||||
COPY ports.conf /etc/apache2/ports.conf
|
||||
COPY application.conf /opt/docker/etc/php/fpm/pool.d/application.conf
|
||||
COPY init.sh /opt/docker/provision/entrypoint.d/
|
||||
#CMD chmod +x /opt/docker/provision/entrypoint.d/init.sh
|
||||
RUN echo "session.save_handler = redis\n" >> /opt/docker/etc/php/php.webdevops.ini
|
||||
RUN echo "session.save_path = 'tcp://127.0.0.1:6379'\n" >> /opt/docker/etc/php/php.webdevops.ini
|
||||
RUN echo "session.gc_maxlifetime = '259200'\n" >> /opt/docker/etc/php/php.webdevops.ini
|
||||
|
||||
# ADD supervisord-proxy.conf /opt/docker/etc/supervisor.d/prism-proxy.conf
|
||||
RUN mkdir /data
|
||||
|
@ -16,10 +22,10 @@ RUN chmod +x /data/gorush
|
|||
|
||||
COPY api /app/api
|
||||
COPY push /app/push
|
||||
# ADD ../../push /app/push
|
||||
|
||||
ADD supervisord-redis.conf /opt/docker/etc/supervisor.d/redis.conf
|
||||
ADD supervisord-ios.conf /opt/docker/etc/supervisor.d/push-ios.conf
|
||||
ADD supervisord-clip.conf /opt/docker/etc/supervisor.d/push-clip.conf
|
||||
|
||||
EXPOSE 80
|
||||
EXPOSE 80 9000
|
||||
|
|
@ -0,0 +1,459 @@
|
|||
; Start a new pool named 'www'.
|
||||
; the variable $pool can be used in any directive and will be replaced by the
|
||||
; pool name ('www' here)
|
||||
[www]
|
||||
|
||||
; Per pool prefix
|
||||
; It only applies on the following directives:
|
||||
; - 'access.log'
|
||||
; - 'slowlog'
|
||||
; - 'listen' (unixsocket)
|
||||
; - 'chroot'
|
||||
; - 'chdir'
|
||||
; - 'php_values'
|
||||
; - 'php_admin_values'
|
||||
; When not set, the global prefix (or NONE) applies instead.
|
||||
; Note: This directive can also be relative to the global prefix.
|
||||
; Default Value: none
|
||||
;prefix = /path/to/pools/$pool
|
||||
|
||||
; Unix user/group of processes
|
||||
; Note: The user is mandatory. If the group is not set, the default user's group
|
||||
; will be used.
|
||||
user = application
|
||||
group = application
|
||||
|
||||
; The address on which to accept FastCGI requests.
|
||||
; Valid syntaxes are:
|
||||
; 'ip.add.re.ss:port' - to listen on a TCP socket to a specific IPv4 address on
|
||||
; a specific port;
|
||||
; '[ip:6:addr:ess]:port' - to listen on a TCP socket to a specific IPv6 address on
|
||||
; a specific port;
|
||||
; 'port' - to listen on a TCP socket to all addresses
|
||||
; (IPv6 and IPv4-mapped) on a specific port;
|
||||
; '/path/to/unix/socket' - to listen on a unix socket.
|
||||
; Note: This value is mandatory.
|
||||
listen = 127.0.0.1:8000
|
||||
|
||||
; Set listen(2) backlog.
|
||||
; Default Value: 511 (-1 on FreeBSD and OpenBSD)
|
||||
;listen.backlog = 511
|
||||
|
||||
; Set permissions for unix socket, if one is used. In Linux, read/write
|
||||
; permissions must be set in order to allow connections from a web server. Many
|
||||
; BSD-derived systems allow connections regardless of permissions. The owner
|
||||
; and group can be specified either by name or by their numeric IDs.
|
||||
; Default Values: user and group are set as the running user
|
||||
; mode is set to 0660
|
||||
;listen.owner = www-data
|
||||
;listen.group = www-data
|
||||
;listen.mode = 0660
|
||||
; When POSIX Access Control Lists are supported you can set them using
|
||||
; these options, value is a comma separated list of user/group names.
|
||||
; When set, listen.owner and listen.group are ignored
|
||||
;listen.acl_users =
|
||||
;listen.acl_groups =
|
||||
|
||||
; List of addresses (IPv4/IPv6) of FastCGI clients which are allowed to connect.
|
||||
; Equivalent to the FCGI_WEB_SERVER_ADDRS environment variable in the original
|
||||
; PHP FCGI (5.2.2+). Makes sense only with a tcp listening socket. Each address
|
||||
; must be separated by a comma. If this value is left blank, connections will be
|
||||
; accepted from any ip address.
|
||||
; Default Value: any
|
||||
;listen.allowed_clients
|
||||
|
||||
; Specify the nice(2) priority to apply to the pool processes (only if set)
|
||||
; The value can vary from -19 (highest priority) to 20 (lower priority)
|
||||
; Note: - It will only work if the FPM master process is launched as root
|
||||
; - The pool processes will inherit the master process priority
|
||||
; unless it specified otherwise
|
||||
; Default Value: no set
|
||||
; process.priority = -19
|
||||
|
||||
; Set the process dumpable flag (PR_SET_DUMPABLE prctl) even if the process user
|
||||
; or group is different than the master process user. It allows to create process
|
||||
; core dump and ptrace the process for the pool user.
|
||||
; Default Value: no
|
||||
; process.dumpable = yes
|
||||
|
||||
; Choose how the process manager will control the number of child processes.
|
||||
; Possible Values:
|
||||
; static - a fixed number (pm.max_children) of child processes;
|
||||
; dynamic - the number of child processes are set dynamically based on the
|
||||
; following directives. With this process management, there will be
|
||||
; always at least 1 children.
|
||||
; pm.max_children - the maximum number of children that can
|
||||
; be alive at the same time.
|
||||
; pm.start_servers - the number of children created on startup.
|
||||
; pm.min_spare_servers - the minimum number of children in 'idle'
|
||||
; state (waiting to process). If the number
|
||||
; of 'idle' processes is less than this
|
||||
; number then some children will be created.
|
||||
; pm.max_spare_servers - the maximum number of children in 'idle'
|
||||
; state (waiting to process). If the number
|
||||
; of 'idle' processes is greater than this
|
||||
; number then some children will be killed.
|
||||
; ondemand - no children are created at startup. Children will be forked when
|
||||
; new requests will connect. The following parameter are used:
|
||||
; pm.max_children - the maximum number of children that
|
||||
; can be alive at the same time.
|
||||
; pm.process_idle_timeout - The number of seconds after which
|
||||
; an idle process will be killed.
|
||||
; Note: This value is mandatory.
|
||||
pm = dynamic
|
||||
|
||||
; The number of child processes to be created when pm is set to 'static' and the
|
||||
; maximum number of child processes when pm is set to 'dynamic' or 'ondemand'.
|
||||
; This value sets the limit on the number of simultaneous requests that will be
|
||||
; served. Equivalent to the ApacheMaxClients directive with mpm_prefork.
|
||||
; Equivalent to the PHP_FCGI_CHILDREN environment variable in the original PHP
|
||||
; CGI. The below defaults are based on a server without much resources. Don't
|
||||
; forget to tweak pm.* to fit your needs.
|
||||
; Note: Used when pm is set to 'static', 'dynamic' or 'ondemand'
|
||||
; Note: This value is mandatory.
|
||||
pm.max_children = 5
|
||||
|
||||
; The number of child processes created on startup.
|
||||
; Note: Used only when pm is set to 'dynamic'
|
||||
; Default Value: (min_spare_servers + max_spare_servers) / 2
|
||||
pm.start_servers = 2
|
||||
|
||||
; The desired minimum number of idle server processes.
|
||||
; Note: Used only when pm is set to 'dynamic'
|
||||
; Note: Mandatory when pm is set to 'dynamic'
|
||||
pm.min_spare_servers = 1
|
||||
|
||||
; The desired maximum number of idle server processes.
|
||||
; Note: Used only when pm is set to 'dynamic'
|
||||
; Note: Mandatory when pm is set to 'dynamic'
|
||||
pm.max_spare_servers = 3
|
||||
|
||||
; The number of seconds after which an idle process will be killed.
|
||||
; Note: Used only when pm is set to 'ondemand'
|
||||
; Default Value: 10s
|
||||
;pm.process_idle_timeout = 10s;
|
||||
|
||||
; The number of requests each child process should execute before respawning.
|
||||
; This can be useful to work around memory leaks in 3rd party libraries. For
|
||||
; endless request processing specify '0'. Equivalent to PHP_FCGI_MAX_REQUESTS.
|
||||
; Default Value: 0
|
||||
;pm.max_requests = 500
|
||||
|
||||
; The URI to view the FPM status page. If this value is not set, no URI will be
|
||||
; recognized as a status page. It shows the following information:
|
||||
; pool - the name of the pool;
|
||||
; process manager - static, dynamic or ondemand;
|
||||
; start time - the date and time FPM has started;
|
||||
; start since - number of seconds since FPM has started;
|
||||
; accepted conn - the number of request accepted by the pool;
|
||||
; listen queue - the number of request in the queue of pending
|
||||
; connections (see backlog in listen(2));
|
||||
; max listen queue - the maximum number of requests in the queue
|
||||
; of pending connections since FPM has started;
|
||||
; listen queue len - the size of the socket queue of pending connections;
|
||||
; idle processes - the number of idle processes;
|
||||
; active processes - the number of active processes;
|
||||
; total processes - the number of idle + active processes;
|
||||
; max active processes - the maximum number of active processes since FPM
|
||||
; has started;
|
||||
; max children reached - number of times, the process limit has been reached,
|
||||
; when pm tries to start more children (works only for
|
||||
; pm 'dynamic' and 'ondemand');
|
||||
; Value are updated in real time.
|
||||
; Example output:
|
||||
; pool: www
|
||||
; process manager: static
|
||||
; start time: 01/Jul/2011:17:53:49 +0200
|
||||
; start since: 62636
|
||||
; accepted conn: 190460
|
||||
; listen queue: 0
|
||||
; max listen queue: 1
|
||||
; listen queue len: 42
|
||||
; idle processes: 4
|
||||
; active processes: 11
|
||||
; total processes: 15
|
||||
; max active processes: 12
|
||||
; max children reached: 0
|
||||
;
|
||||
; By default the status page output is formatted as text/plain. Passing either
|
||||
; 'html', 'xml' or 'json' in the query string will return the corresponding
|
||||
; output syntax. Example:
|
||||
; http://www.foo.bar/status
|
||||
; http://www.foo.bar/status?json
|
||||
; http://www.foo.bar/status?html
|
||||
; http://www.foo.bar/status?xml
|
||||
;
|
||||
; By default the status page only outputs short status. Passing 'full' in the
|
||||
; query string will also return status for each pool process.
|
||||
; Example:
|
||||
; http://www.foo.bar/status?full
|
||||
; http://www.foo.bar/status?json&full
|
||||
; http://www.foo.bar/status?html&full
|
||||
; http://www.foo.bar/status?xml&full
|
||||
; The Full status returns for each process:
|
||||
; pid - the PID of the process;
|
||||
; state - the state of the process (Idle, Running, ...);
|
||||
; start time - the date and time the process has started;
|
||||
; start since - the number of seconds since the process has started;
|
||||
; requests - the number of requests the process has served;
|
||||
; request duration - the duration in µs of the requests;
|
||||
; request method - the request method (GET, POST, ...);
|
||||
; request URI - the request URI with the query string;
|
||||
; content length - the content length of the request (only with POST);
|
||||
; user - the user (PHP_AUTH_USER) (or '-' if not set);
|
||||
; script - the main script called (or '-' if not set);
|
||||
; last request cpu - the %cpu the last request consumed
|
||||
; it's always 0 if the process is not in Idle state
|
||||
; because CPU calculation is done when the request
|
||||
; processing has terminated;
|
||||
; last request memory - the max amount of memory the last request consumed
|
||||
; it's always 0 if the process is not in Idle state
|
||||
; because memory calculation is done when the request
|
||||
; processing has terminated;
|
||||
; If the process is in Idle state, then informations are related to the
|
||||
; last request the process has served. Otherwise informations are related to
|
||||
; the current request being served.
|
||||
; Example output:
|
||||
; ************************
|
||||
; pid: 31330
|
||||
; state: Running
|
||||
; start time: 01/Jul/2011:17:53:49 +0200
|
||||
; start since: 63087
|
||||
; requests: 12808
|
||||
; request duration: 1250261
|
||||
; request method: GET
|
||||
; request URI: /test_mem.php?N=10000
|
||||
; content length: 0
|
||||
; user: -
|
||||
; script: /home/fat/web/docs/php/test_mem.php
|
||||
; last request cpu: 0.00
|
||||
; last request memory: 0
|
||||
;
|
||||
; Note: There is a real-time FPM status monitoring sample web page available
|
||||
; It's available in: /usr/local/share/php/fpm/status.html
|
||||
;
|
||||
; Note: The value must start with a leading slash (/). The value can be
|
||||
; anything, but it may not be a good idea to use the .php extension or it
|
||||
; may conflict with a real PHP file.
|
||||
; Default Value: not set
|
||||
;pm.status_path = /status
|
||||
|
||||
; The address on which to accept FastCGI status request. This creates a new
|
||||
; invisible pool that can handle requests independently. This is useful
|
||||
; if the main pool is busy with long running requests because it is still possible
|
||||
; to get the status before finishing the long running requests.
|
||||
;
|
||||
; Valid syntaxes are:
|
||||
; 'ip.add.re.ss:port' - to listen on a TCP socket to a specific IPv4 address on
|
||||
; a specific port;
|
||||
; '[ip:6:addr:ess]:port' - to listen on a TCP socket to a specific IPv6 address on
|
||||
; a specific port;
|
||||
; 'port' - to listen on a TCP socket to all addresses
|
||||
; (IPv6 and IPv4-mapped) on a specific port;
|
||||
; '/path/to/unix/socket' - to listen on a unix socket.
|
||||
; Default Value: value of the listen option
|
||||
;pm.status_listen = 127.0.0.1:9001
|
||||
|
||||
; The ping URI to call the monitoring page of FPM. If this value is not set, no
|
||||
; URI will be recognized as a ping page. This could be used to test from outside
|
||||
; that FPM is alive and responding, or to
|
||||
; - create a graph of FPM availability (rrd or such);
|
||||
; - remove a server from a group if it is not responding (load balancing);
|
||||
; - trigger alerts for the operating team (24/7).
|
||||
; Note: The value must start with a leading slash (/). The value can be
|
||||
; anything, but it may not be a good idea to use the .php extension or it
|
||||
; may conflict with a real PHP file.
|
||||
; Default Value: not set
|
||||
;ping.path = /ping
|
||||
|
||||
; This directive may be used to customize the response of a ping request. The
|
||||
; response is formatted as text/plain with a 200 response code.
|
||||
; Default Value: pong
|
||||
;ping.response = pong
|
||||
|
||||
; The access log file
|
||||
; Default: not set
|
||||
access.log = /docker.stdout
|
||||
|
||||
; The access log format.
|
||||
; The following syntax is allowed
|
||||
; %%: the '%' character
|
||||
; %C: %CPU used by the request
|
||||
; it can accept the following format:
|
||||
; - %{user}C for user CPU only
|
||||
; - %{system}C for system CPU only
|
||||
; - %{total}C for user + system CPU (default)
|
||||
; %d: time taken to serve the request
|
||||
; it can accept the following format:
|
||||
; - %{seconds}d (default)
|
||||
; - %{milliseconds}d
|
||||
; - %{mili}d
|
||||
; - %{microseconds}d
|
||||
; - %{micro}d
|
||||
; %e: an environment variable (same as $_ENV or $_SERVER)
|
||||
; it must be associated with embraces to specify the name of the env
|
||||
; variable. Some examples:
|
||||
; - server specifics like: %{REQUEST_METHOD}e or %{SERVER_PROTOCOL}e
|
||||
; - HTTP headers like: %{HTTP_HOST}e or %{HTTP_USER_AGENT}e
|
||||
; %f: script filename
|
||||
; %l: content-length of the request (for POST request only)
|
||||
; %m: request method
|
||||
; %M: peak of memory allocated by PHP
|
||||
; it can accept the following format:
|
||||
; - %{bytes}M (default)
|
||||
; - %{kilobytes}M
|
||||
; - %{kilo}M
|
||||
; - %{megabytes}M
|
||||
; - %{mega}M
|
||||
; %n: pool name
|
||||
; %o: output header
|
||||
; it must be associated with embraces to specify the name of the header:
|
||||
; - %{Content-Type}o
|
||||
; - %{X-Powered-By}o
|
||||
; - %{Transfert-Encoding}o
|
||||
; - ....
|
||||
; %p: PID of the child that serviced the request
|
||||
; %P: PID of the parent of the child that serviced the request
|
||||
; %q: the query string
|
||||
; %Q: the '?' character if query string exists
|
||||
; %r: the request URI (without the query string, see %q and %Q)
|
||||
; %R: remote IP address
|
||||
; %s: status (response code)
|
||||
; %t: server time the request was received
|
||||
; it can accept a strftime(3) format:
|
||||
; %d/%b/%Y:%H:%M:%S %z (default)
|
||||
; The strftime(3) format must be encapsuled in a %{<strftime_format>}t tag
|
||||
; e.g. for a ISO8601 formatted timestring, use: %{%Y-%m-%dT%H:%M:%S%z}t
|
||||
; %T: time the log has been written (the request has finished)
|
||||
; it can accept a strftime(3) format:
|
||||
; %d/%b/%Y:%H:%M:%S %z (default)
|
||||
; The strftime(3) format must be encapsuled in a %{<strftime_format>}t tag
|
||||
; e.g. for a ISO8601 formatted timestring, use: %{%Y-%m-%dT%H:%M:%S%z}t
|
||||
; %u: remote user
|
||||
;
|
||||
; Default: "%R - %u %t \"%m %r\" %s"
|
||||
access.format = "[php-fpm:access] %R - %u %t \"%m %r%Q%q\" %s %f %{mili}d %{kilo}M %C%%"
|
||||
|
||||
; The log file for slow requests
|
||||
; Default Value: not set
|
||||
; Note: slowlog is mandatory if request_slowlog_timeout is set
|
||||
slowlog = /docker.stderr
|
||||
|
||||
; The timeout for serving a single request after which a PHP backtrace will be
|
||||
; dumped to the 'slowlog' file. A value of '0s' means 'off'.
|
||||
; Available units: s(econds)(default), m(inutes), h(ours), or d(ays)
|
||||
; Default Value: 0
|
||||
;request_slowlog_timeout = 0
|
||||
|
||||
; Depth of slow log stack trace.
|
||||
; Default Value: 20
|
||||
;request_slowlog_trace_depth = 20
|
||||
|
||||
; The timeout for serving a single request after which the worker process will
|
||||
; be killed. This option should be used when the 'max_execution_time' ini option
|
||||
; does not stop script execution for some reason. A value of '0' means 'off'.
|
||||
; Available units: s(econds)(default), m(inutes), h(ours), or d(ays)
|
||||
; Default Value: 0
|
||||
;request_terminate_timeout = 0
|
||||
|
||||
; The timeout set by 'request_terminate_timeout' ini option is not engaged after
|
||||
; application calls 'fastcgi_finish_request' or when application has finished and
|
||||
; shutdown functions are being called (registered via register_shutdown_function).
|
||||
; This option will enable timeout limit to be applied unconditionally
|
||||
; even in such cases.
|
||||
; Default Value: no
|
||||
;request_terminate_timeout_track_finished = no
|
||||
|
||||
; Set open file descriptor rlimit.
|
||||
; Default Value: system defined value
|
||||
;rlimit_files = 1024
|
||||
|
||||
; Set max core size rlimit.
|
||||
; Possible Values: 'unlimited' or an integer greater or equal to 0
|
||||
; Default Value: system defined value
|
||||
;rlimit_core = 0
|
||||
|
||||
; Chroot to this directory at the start. This value must be defined as an
|
||||
; absolute path. When this value is not set, chroot is not used.
|
||||
; Note: you can prefix with '$prefix' to chroot to the pool prefix or one
|
||||
; of its subdirectories. If the pool prefix is not set, the global prefix
|
||||
; will be used instead.
|
||||
; Note: chrooting is a great security feature and should be used whenever
|
||||
; possible. However, all PHP paths will be relative to the chroot
|
||||
; (error_log, sessions.save_path, ...).
|
||||
; Default Value: not set
|
||||
;chroot =
|
||||
|
||||
; Chdir to this directory at the start.
|
||||
; Note: relative path can be used.
|
||||
; Default Value: current directory or / when chroot
|
||||
;chdir = /var/www
|
||||
|
||||
; Redirect worker stdout and stderr into main error log. If not set, stdout and
|
||||
; stderr will be redirected to /dev/null according to FastCGI specs.
|
||||
; Note: on highloaded environment, this can cause some delay in the page
|
||||
; process time (several ms).
|
||||
; Default Value: no
|
||||
catch_workers_output = yes
|
||||
|
||||
; Decorate worker output with prefix and suffix containing information about
|
||||
; the child that writes to the log and if stdout or stderr is used as well as
|
||||
; log level and time. This options is used only if catch_workers_output is yes.
|
||||
; Settings to "no" will output data as written to the stdout or stderr.
|
||||
; Default value: yes
|
||||
;decorate_workers_output = no
|
||||
|
||||
; Clear environment in FPM workers
|
||||
; Prevents arbitrary environment variables from reaching FPM worker processes
|
||||
; by clearing the environment in workers before env vars specified in this
|
||||
; pool configuration are added.
|
||||
; Setting to "no" will make all environment variables available to PHP code
|
||||
; via getenv(), $_ENV and $_SERVER.
|
||||
; Default Value: yes
|
||||
clear_env = no
|
||||
|
||||
; Limits the extensions of the main script FPM will allow to parse. This can
|
||||
; prevent configuration mistakes on the web server side. You should only limit
|
||||
; FPM to .php extensions to prevent malicious users to use other extensions to
|
||||
; execute php code.
|
||||
; Note: set an empty value to allow all extensions.
|
||||
; Default Value: .php
|
||||
;security.limit_extensions = .php .php3 .php4 .php5 .php7
|
||||
|
||||
; Pass environment variables like LD_LIBRARY_PATH. All $VARIABLEs are taken from
|
||||
; the current environment.
|
||||
; Default Value: clean env
|
||||
;env[HOSTNAME] = $HOSTNAME
|
||||
;env[PATH] = /usr/local/bin:/usr/bin:/bin
|
||||
;env[TMP] = /tmp
|
||||
;env[TMPDIR] = /tmp
|
||||
;env[TEMP] = /tmp
|
||||
|
||||
; Additional php.ini defines, specific to this pool of workers. These settings
|
||||
; overwrite the values previously defined in the php.ini. The directives are the
|
||||
; same as the PHP SAPI:
|
||||
; php_value/php_flag - you can set classic ini defines which can
|
||||
; be overwritten from PHP call 'ini_set'.
|
||||
; php_admin_value/php_admin_flag - these directives won't be overwritten by
|
||||
; PHP call 'ini_set'
|
||||
; For php_*flag, valid values are on, off, 1, 0, true, false, yes or no.
|
||||
|
||||
; Defining 'extension' will load the corresponding shared extension from
|
||||
; extension_dir. Defining 'disable_functions' or 'disable_classes' will not
|
||||
; overwrite previously defined php.ini values, but will append the new value
|
||||
; instead.
|
||||
|
||||
; Note: path INI options can be relative and will be expanded with the prefix
|
||||
; (pool, global or /usr/local)
|
||||
|
||||
; Default Value: nothing is defined by default except the values in php.ini and
|
||||
; specified at startup with the -d argument
|
||||
;php_admin_value[sendmail_path] = /usr/sbin/sendmail -t -i -f www@my.domain.com
|
||||
;php_flag[display_errors] = off
|
||||
php_admin_value[error_log] = /docker.stderr
|
||||
;php_admin_flag[log_errors] = on
|
||||
;php_admin_value[memory_limit] = 32M
|
||||
php_admin_value[log_errors] = on
|
||||
|
||||
; container env settings
|
||||
php_admin_value[sendmail_path] = /usr/sbin/sendmail -t -i
|
|
@ -8,9 +8,3 @@ chmod -R 0777 /app/api/storage
|
|||
|
||||
mkdir -p /app/api/bootstrap/cache/
|
||||
chmod -R 0777 /app/api/bootstrap/cache/
|
||||
|
||||
|
||||
|
||||
# 启动 proxy
|
||||
# 已经设置为 deamon
|
||||
# cd /app/proxy && ./server-linux &
|
||||
|
|
|
@ -0,0 +1 @@
|
|||
* * * * * cd /app/api && php artisan schedule:run >> /dev/null 2>&1
|
|
@ -0,0 +1,16 @@
|
|||
# If you just change the port or add more ports here, you will likely also
|
||||
# have to change the VirtualHost statement in
|
||||
# /etc/apache2/sites-enabled/000-default.conf
|
||||
|
||||
Listen 80
|
||||
Listen 9000
|
||||
|
||||
<IfModule ssl_module>
|
||||
Listen 443
|
||||
</IfModule>
|
||||
|
||||
<IfModule mod_gnutls.c>
|
||||
Listen 443
|
||||
</IfModule>
|
||||
|
||||
# vim: syntax=apache ts=4 sw=4 sts=4 sr noet
|
|
@ -0,0 +1,13 @@
|
|||
[group:redis]
|
||||
programs=redis
|
||||
priority=18
|
||||
|
||||
[program:redis]
|
||||
process_name=%(program_name)s
|
||||
command=/usr/bin/redis-server
|
||||
autostart=true
|
||||
startretries=10
|
||||
autorestart=true
|
||||
priority=1
|
||||
redirect_stderr=true
|
||||
stdout_logfile=/redis.log
|
|
@ -24,6 +24,8 @@
|
|||
Order allow,deny
|
||||
allow from all
|
||||
</Directory>
|
||||
# 配置 https
|
||||
# 去掉下边三行的注释
|
||||
#SSLEngine on
|
||||
#SSLCertificateFile /app/ssl/server.crt
|
||||
#SSLCertificateKeyFile /app/ssl/server.key
|
||||
|
|
|
@ -0,0 +1,20 @@
|
|||
<?php
|
||||
|
||||
// 调用函数
|
||||
function pushdeer_send($text, $desp = '', $type='text', $key = '[PUSHKEY]')
|
||||
{
|
||||
$postdata = http_build_query(array( 'text' => $text, 'desp' => $desp, 'type' => $type , 'pushkey' => $key ));
|
||||
$opts = array('http' =>
|
||||
array(
|
||||
'method' => 'POST',
|
||||
'header' => 'Content-type: application/x-www-form-urlencoded',
|
||||
'content' => $postdata));
|
||||
|
||||
$context = stream_context_create($opts);
|
||||
return $result = file_get_contents('https://api2.pushdeer.com/message/push', false, $context);
|
||||
}
|
||||
|
||||
|
||||
|
||||
// 使用实例
|
||||
print_r(pushdeer_send('服务器又宕机了主人', '', 'text', 'PDU...'));
|
|
@ -0,0 +1,13 @@
|
|||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
|
||||
<plist version="1.0">
|
||||
<dict>
|
||||
<key>NSExtension</key>
|
||||
<dict>
|
||||
<key>NSExtensionPointIdentifier</key>
|
||||
<string>com.apple.usernotifications.service</string>
|
||||
<key>NSExtensionPrincipalClass</key>
|
||||
<string>$(PRODUCT_MODULE_NAME).NotificationService</string>
|
||||
</dict>
|
||||
</dict>
|
||||
</plist>
|
|
@ -0,0 +1,10 @@
|
|||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
|
||||
<plist version="1.0">
|
||||
<dict>
|
||||
<key>com.apple.security.app-sandbox</key>
|
||||
<true/>
|
||||
<key>com.apple.security.network.client</key>
|
||||
<true/>
|
||||
</dict>
|
||||
</plist>
|
|
@ -0,0 +1,40 @@
|
|||
//
|
||||
// NotificationService.swift
|
||||
// Notification
|
||||
//
|
||||
// Created by HEXT on 2022/4/19.
|
||||
//
|
||||
|
||||
import UserNotifications
|
||||
import WidgetKit
|
||||
|
||||
class NotificationService: UNNotificationServiceExtension {
|
||||
|
||||
var contentHandler: ((UNNotificationContent) -> Void)?
|
||||
var bestAttemptContent: UNMutableNotificationContent?
|
||||
|
||||
override func didReceive(_ request: UNNotificationRequest, withContentHandler contentHandler: @escaping (UNNotificationContent) -> Void) {
|
||||
self.contentHandler = contentHandler
|
||||
bestAttemptContent = (request.content.mutableCopy() as? UNMutableNotificationContent)
|
||||
|
||||
NSLog("push-userInfo: %@", bestAttemptContent?.userInfo ?? "")
|
||||
// 刷新所有桌面小部件
|
||||
WidgetCenter.shared.reloadAllTimelines()
|
||||
|
||||
if let bestAttemptContent = bestAttemptContent {
|
||||
// Modify the notification content here...
|
||||
// bestAttemptContent.title = "\(bestAttemptContent.title) [modified]"
|
||||
|
||||
contentHandler(bestAttemptContent)
|
||||
}
|
||||
}
|
||||
|
||||
override func serviceExtensionTimeWillExpire() {
|
||||
// Called just before the extension will be terminated by the system.
|
||||
// Use this as an opportunity to deliver your "best attempt" at modified content, otherwise the original push payload will be used.
|
||||
if let contentHandler = contentHandler, let bestAttemptContent = bestAttemptContent {
|
||||
contentHandler(bestAttemptContent)
|
||||
}
|
||||
}
|
||||
|
||||
}
|
|
@ -1,6 +1,8 @@
|
|||
# Uncomment the next line to define a global platform for your project
|
||||
platform :ios, '14.0'
|
||||
|
||||
load 'remove_unsupported_libraries.rb'
|
||||
|
||||
# Comment the next line if you don't want to use dynamic frameworks
|
||||
use_frameworks!
|
||||
|
||||
|
@ -9,14 +11,15 @@ def commonPods
|
|||
pod 'Moya', '~> 15.0'
|
||||
pod 'SDWebImageSwiftUI', '~> 2.0.2'
|
||||
pod 'KRProgressHUD', '~> 3.4.7'
|
||||
|
||||
# pod 'WoodPeckeriOS', :configurations => ['Debug']
|
||||
pod 'IQKeyboardManagerSwift', '~> 6.5.9'
|
||||
end
|
||||
|
||||
target 'PushDeer' do
|
||||
commonPods
|
||||
# Pods for PushDeer
|
||||
|
||||
# PushDeer 独享的依赖, Clip 不支持
|
||||
pod 'WechatOpenSDK', '~> 1.8.7.1'
|
||||
pod 'WoodPeckeriOS', :configurations => ['Debug']
|
||||
end
|
||||
|
||||
target 'PushDeerClip' do
|
||||
|
@ -24,3 +27,25 @@ target 'PushDeerClip' do
|
|||
# Pods for PushDeerClip
|
||||
|
||||
end
|
||||
|
||||
target 'PushDeerWidgetExtension' do
|
||||
# Pods for PushDeerWidgetExtension
|
||||
pod 'Moya', '~> 15.0'
|
||||
end
|
||||
|
||||
# define unsupported pods
|
||||
def unsupported_pods
|
||||
# macCatalyst 不支持的库
|
||||
['WoodPeckeriOS', 'WechatOpenSDK']
|
||||
end
|
||||
|
||||
def supported_pods
|
||||
# macCatalyst 支持的库
|
||||
['Moya', 'SDWebImageSwiftUI', 'KRProgressHUD', 'IQKeyboardManagerSwift']
|
||||
end
|
||||
|
||||
# install all pods except unsupported ones
|
||||
post_install do |installer|
|
||||
$verbose = false # remove or set to false to avoid printing
|
||||
installer.configure_support_catalyst(supported_pods, unsupported_pods)
|
||||
end
|
||||
|
|
|
@ -1,5 +1,6 @@
|
|||
PODS:
|
||||
- Alamofire (5.5.0)
|
||||
- IQKeyboardManagerSwift (6.5.9)
|
||||
- KRActivityIndicatorView (3.0.7)
|
||||
- KRProgressHUD (3.4.7):
|
||||
- KRActivityIndicatorView (= 3.0.7)
|
||||
|
@ -12,29 +13,40 @@ PODS:
|
|||
- SDWebImage/Core (5.12.1)
|
||||
- SDWebImageSwiftUI (2.0.2):
|
||||
- SDWebImage (~> 5.10)
|
||||
- WechatOpenSDK (1.8.7.1)
|
||||
- WoodPeckeriOS (1.2.93)
|
||||
|
||||
DEPENDENCIES:
|
||||
- IQKeyboardManagerSwift (~> 6.5.9)
|
||||
- KRProgressHUD (~> 3.4.7)
|
||||
- Moya (~> 15.0)
|
||||
- SDWebImageSwiftUI (~> 2.0.2)
|
||||
- WechatOpenSDK (~> 1.8.7.1)
|
||||
- WoodPeckeriOS
|
||||
|
||||
SPEC REPOS:
|
||||
trunk:
|
||||
- Alamofire
|
||||
- IQKeyboardManagerSwift
|
||||
- KRActivityIndicatorView
|
||||
- KRProgressHUD
|
||||
- Moya
|
||||
- SDWebImage
|
||||
- SDWebImageSwiftUI
|
||||
- WechatOpenSDK
|
||||
- WoodPeckeriOS
|
||||
|
||||
SPEC CHECKSUMS:
|
||||
Alamofire: 1c4fb5369c3fe93d2857c780d8bbe09f06f97e7c
|
||||
IQKeyboardManagerSwift: 6e839c575c4aa1078d58a596e41244e77abe918f
|
||||
KRActivityIndicatorView: ad69e89c4ce40c986cf580595be4829dcad0e35a
|
||||
KRProgressHUD: a248f0bc6c9c2aed40a37b76e03ffecc7f85c887
|
||||
Moya: 138f0573e53411fb3dc17016add0b748dfbd78ee
|
||||
SDWebImage: 4dc3e42d9ec0c1028b960a33ac6b637bb432207b
|
||||
SDWebImageSwiftUI: 8a3923c95108312b03a599ec1498754af55a6819
|
||||
WechatOpenSDK: 6a4d1436c15b3b5fe2a0bd383f3046010186da44
|
||||
WoodPeckeriOS: 12ec7f38c695e51cd94a476228888dfe85d9d916
|
||||
|
||||
PODFILE CHECKSUM: 06aae1de50f9c1a188e69787835ec8718dd7d543
|
||||
PODFILE CHECKSUM: 42e3d8abd976589c1043ff9f9e864c275a490160
|
||||
|
||||
COCOAPODS: 1.11.2
|
||||
COCOAPODS: 1.11.3
|
||||
|
|
|
@ -0,0 +1,78 @@
|
|||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<Scheme
|
||||
LastUpgradeVersion = "1320"
|
||||
version = "1.3">
|
||||
<BuildAction
|
||||
parallelizeBuildables = "YES"
|
||||
buildImplicitDependencies = "YES">
|
||||
<BuildActionEntries>
|
||||
<BuildActionEntry
|
||||
buildForTesting = "YES"
|
||||
buildForRunning = "YES"
|
||||
buildForProfiling = "YES"
|
||||
buildForArchiving = "YES"
|
||||
buildForAnalyzing = "YES">
|
||||
<BuildableReference
|
||||
BuildableIdentifier = "primary"
|
||||
BlueprintIdentifier = "5292F4F42776BC7900B9A7BB"
|
||||
BuildableName = "PushDeer.app"
|
||||
BlueprintName = "PushDeer"
|
||||
ReferencedContainer = "container:PushDeer.xcodeproj">
|
||||
</BuildableReference>
|
||||
</BuildActionEntry>
|
||||
</BuildActionEntries>
|
||||
</BuildAction>
|
||||
<TestAction
|
||||
buildConfiguration = "Debug-SelfHosted"
|
||||
selectedDebuggerIdentifier = "Xcode.DebuggerFoundation.Debugger.LLDB"
|
||||
selectedLauncherIdentifier = "Xcode.DebuggerFoundation.Launcher.LLDB"
|
||||
shouldUseLaunchSchemeArgsEnv = "YES">
|
||||
<Testables>
|
||||
</Testables>
|
||||
</TestAction>
|
||||
<LaunchAction
|
||||
buildConfiguration = "Debug-SelfHosted"
|
||||
selectedDebuggerIdentifier = "Xcode.DebuggerFoundation.Debugger.LLDB"
|
||||
selectedLauncherIdentifier = "Xcode.DebuggerFoundation.Launcher.LLDB"
|
||||
launchStyle = "0"
|
||||
useCustomWorkingDirectory = "NO"
|
||||
ignoresPersistentStateOnLaunch = "NO"
|
||||
debugDocumentVersioning = "YES"
|
||||
debugServiceExtension = "internal"
|
||||
allowLocationSimulation = "YES">
|
||||
<BuildableProductRunnable
|
||||
runnableDebuggingMode = "0">
|
||||
<BuildableReference
|
||||
BuildableIdentifier = "primary"
|
||||
BlueprintIdentifier = "5292F4F42776BC7900B9A7BB"
|
||||
BuildableName = "PushDeer.app"
|
||||
BlueprintName = "PushDeer"
|
||||
ReferencedContainer = "container:PushDeer.xcodeproj">
|
||||
</BuildableReference>
|
||||
</BuildableProductRunnable>
|
||||
</LaunchAction>
|
||||
<ProfileAction
|
||||
buildConfiguration = "Release-SelfHosted"
|
||||
shouldUseLaunchSchemeArgsEnv = "YES"
|
||||
savedToolIdentifier = ""
|
||||
useCustomWorkingDirectory = "NO"
|
||||
debugDocumentVersioning = "YES">
|
||||
<BuildableProductRunnable
|
||||
runnableDebuggingMode = "0">
|
||||
<BuildableReference
|
||||
BuildableIdentifier = "primary"
|
||||
BlueprintIdentifier = "5292F4F42776BC7900B9A7BB"
|
||||
BuildableName = "PushDeer.app"
|
||||
BlueprintName = "PushDeer"
|
||||
ReferencedContainer = "container:PushDeer.xcodeproj">
|
||||
</BuildableReference>
|
||||
</BuildableProductRunnable>
|
||||
</ProfileAction>
|
||||
<AnalyzeAction
|
||||
buildConfiguration = "Debug-SelfHosted">
|
||||
</AnalyzeAction>
|
||||
<ArchiveAction
|
||||
buildConfiguration = "Release-SelfHosted"
|
||||
revealArchiveInOrganizer = "YES">
|
||||
</ArchiveAction>
|
||||
</Scheme>
|
|
@ -0,0 +1,78 @@
|
|||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<Scheme
|
||||
LastUpgradeVersion = "1320"
|
||||
version = "1.3">
|
||||
<BuildAction
|
||||
parallelizeBuildables = "YES"
|
||||
buildImplicitDependencies = "YES">
|
||||
<BuildActionEntries>
|
||||
<BuildActionEntry
|
||||
buildForTesting = "YES"
|
||||
buildForRunning = "YES"
|
||||
buildForProfiling = "YES"
|
||||
buildForArchiving = "YES"
|
||||
buildForAnalyzing = "YES">
|
||||
<BuildableReference
|
||||
BuildableIdentifier = "primary"
|
||||
BlueprintIdentifier = "5292F4F42776BC7900B9A7BB"
|
||||
BuildableName = "PushDeer.app"
|
||||
BlueprintName = "PushDeer"
|
||||
ReferencedContainer = "container:PushDeer.xcodeproj">
|
||||
</BuildableReference>
|
||||
</BuildActionEntry>
|
||||
</BuildActionEntries>
|
||||
</BuildAction>
|
||||
<TestAction
|
||||
buildConfiguration = "Debug"
|
||||
selectedDebuggerIdentifier = "Xcode.DebuggerFoundation.Debugger.LLDB"
|
||||
selectedLauncherIdentifier = "Xcode.DebuggerFoundation.Launcher.LLDB"
|
||||
shouldUseLaunchSchemeArgsEnv = "YES">
|
||||
<Testables>
|
||||
</Testables>
|
||||
</TestAction>
|
||||
<LaunchAction
|
||||
buildConfiguration = "Debug"
|
||||
selectedDebuggerIdentifier = "Xcode.DebuggerFoundation.Debugger.LLDB"
|
||||
selectedLauncherIdentifier = "Xcode.DebuggerFoundation.Launcher.LLDB"
|
||||
launchStyle = "0"
|
||||
useCustomWorkingDirectory = "NO"
|
||||
ignoresPersistentStateOnLaunch = "NO"
|
||||
debugDocumentVersioning = "YES"
|
||||
debugServiceExtension = "internal"
|
||||
allowLocationSimulation = "YES">
|
||||
<BuildableProductRunnable
|
||||
runnableDebuggingMode = "0">
|
||||
<BuildableReference
|
||||
BuildableIdentifier = "primary"
|
||||
BlueprintIdentifier = "5292F4F42776BC7900B9A7BB"
|
||||
BuildableName = "PushDeer.app"
|
||||
BlueprintName = "PushDeer"
|
||||
ReferencedContainer = "container:PushDeer.xcodeproj">
|
||||
</BuildableReference>
|
||||
</BuildableProductRunnable>
|
||||
</LaunchAction>
|
||||
<ProfileAction
|
||||
buildConfiguration = "Release"
|
||||
shouldUseLaunchSchemeArgsEnv = "YES"
|
||||
savedToolIdentifier = ""
|
||||
useCustomWorkingDirectory = "NO"
|
||||
debugDocumentVersioning = "YES">
|
||||
<BuildableProductRunnable
|
||||
runnableDebuggingMode = "0">
|
||||
<BuildableReference
|
||||
BuildableIdentifier = "primary"
|
||||
BlueprintIdentifier = "5292F4F42776BC7900B9A7BB"
|
||||
BuildableName = "PushDeer.app"
|
||||
BlueprintName = "PushDeer"
|
||||
ReferencedContainer = "container:PushDeer.xcodeproj">
|
||||
</BuildableReference>
|
||||
</BuildableProductRunnable>
|
||||
</ProfileAction>
|
||||
<AnalyzeAction
|
||||
buildConfiguration = "Debug">
|
||||
</AnalyzeAction>
|
||||
<ArchiveAction
|
||||
buildConfiguration = "Release"
|
||||
revealArchiveInOrganizer = "YES">
|
||||
</ArchiveAction>
|
||||
</Scheme>
|