Compare commits

...

187 Commits

Author SHA1 Message Date
Easy b8fa724a9a
Update README.md 2024-02-26 21:46:50 +08:00
Easy 4ce68572b9
Update README.md 2023-12-29 12:33:49 +08:00
Easy 16aec6c468
Update README.md 2023-12-29 12:32:56 +08:00
Easy bfadc52fef 更新证书 2023-12-16 13:15:19 +08:00
Easy d7f3279458
Update README.md
remove core image
2023-11-10 18:11:31 +08:00
Easy f5841403c7
Update docker-compose.serverless.yml 2023-11-10 18:08:46 +08:00
Easy be51654558
Update README.md 2023-10-13 10:54:37 +08:00
Easy aa9a2a9dfa
Update README.md 2023-10-13 10:54:12 +08:00
Easy 1a9ad9e351 update 2023-02-02 23:27:03 +08:00
Easy 19011afdd0 更新推送证书 2023-01-15 16:24:03 +08:00
Easy f9779893ec
Update README.md 2022-12-20 08:26:35 +08:00
Easy 09acc7955e
Merge pull request #159 from abgelehnt/main
添加Rust SDK引用
2022-11-23 20:12:34 +08:00
Chi 6053cf624a
添加Rust SDK引用 2022-11-23 19:25:29 +08:00
Easy d15892727a
Update README.md 2022-09-24 11:00:22 +08:00
Easy b5e6605328
隐藏自架版mysql端口,避免和宿主机冲突 2022-09-21 11:25:41 +08:00
Easy 8e5292acfb
Merge pull request #148 from Hext123/main
获取用户信息时增加了simple_token字段,存下来,token过期以后调用 /login/simple_token 重新获得新的认证token
2022-09-14 00:38:19 +08:00
hext ed61f49eca 获取用户信息时增加了simple_token字段,存下来,token过期以后调用 /login/simple_token 重新获得新的认证token 2022-09-14 00:14:19 +08:00
Easy 6927a94860 添加SimpleToken的API实现和文档 2022-09-06 01:02:53 +08:00
Easy e5d715e06d update mipush sdk 2022-07-01 20:26:43 +08:00
Easy 3fb92270f3 添加key_debug demo 文件 2022-06-27 14:47:51 +08:00
Easy 1ca157b32e save 2022-06-27 14:47:34 +08:00
Easy fd901b04c8 添加key_debug demo文件 2022-06-27 14:47:07 +08:00
Easy c706d83e2f 添加用户禁用逻辑 2022-06-27 00:33:35 +08:00
Easy 034e93d3c7 add index 2022-06-27 00:05:17 +08:00
Easy a218d5c68d add mipush key 2022-06-21 22:29:33 +08:00
Easy 416cc716dd
Update README.md
添加浏览器插件链接
2022-06-20 12:37:52 +08:00
Easy d504a91dc4
Create AppKeys.kt 2022-06-11 18:21:05 +08:00
Easy 20697f77dd
Merge pull request #127 from chengyuhui/markdown-fix
Android 修复Markdown渲染错误,去除Android Studio相关文件
2022-06-11 18:19:24 +08:00
Harry Cheng 4673418d95 android: Fix list updating for image and QR code 2022-06-11 13:11:33 +08:00
Harry Cheng c6caf87a0a android: Fix #117 2022-06-11 11:47:57 +08:00
Harry Cheng 2b89ee699e android: Fix debug signing and remove IDE files 2022-06-11 11:47:22 +08:00
EasyChen d7a0c95382 remove subtitle 2022-05-27 19:04:57 +08:00
Easy 2f461cf8c6
Update README.md 2022-05-21 14:38:26 +08:00
Easy b874100b17
Update 安装文档.md 2022-05-21 12:24:22 +08:00
Easy a0d07f4362
Update 安装文档.md 2022-05-21 12:21:40 +08:00
Easy c2283e50f9
Update 安装文档.md 2022-05-21 12:06:43 +08:00
Easy 4c8b70310e
Update 安装文档.md 2022-05-21 00:19:21 +08:00
Easy 57e467b2e3
Update vhost.conf 2022-05-21 00:13:15 +08:00
Easy 6b097973c2
Update Dockerfile 2022-05-21 00:12:22 +08:00
Easy af112c230c
Update README.md 2022-05-12 14:42:13 +08:00
Easy 3c019e61f8
添加社区贡献的后端实现链接 2022-05-02 21:33:39 +08:00
Easy 914bd7ac1a 修改测试固件效果 2022-04-25 22:24:26 +08:00
Easy 3878706c9f change deeresp default text scale 2022-04-22 17:58:45 +08:00
Easy 628b67191d 添加烧录脚本 2022-04-22 14:12:30 +08:00
Easy fe15fa7593 添加测试用代码 2022-04-22 13:57:14 +08:00
Easy 2ea7fa6c22 更新文字说明 2022-04-22 01:59:18 +08:00
Easy ed3c753f6b 更新IOT文件 2022-04-22 01:54:15 +08:00
Easy 38162f0cbe
Merge pull request #106 from Hext123/main
删除多余图标; 优化逻辑; 添加日志
2022-04-20 00:33:45 +08:00
hext 65e2e729fc 删除多余图标; 优化逻辑; 添加日志 2022-04-20 00:27:26 +08:00
Easy 05d051dc8b
Update Helpers.php 2022-04-19 19:53:56 +08:00
Easy 411f768156
add mutable_content flag to ios push 2022-04-19 19:16:36 +08:00
Easy 6d2b30901b
Merge pull request #105 from Hext123/main
新增功能: 桌面小部件; 一键清空全部消息.
2022-04-19 00:38:25 +08:00
hext c3d0cd13f5 新增功能: 桌面小部件, 展示最近消息, 支持收到推送自动刷新;
新增功能: 1分钟内删除两条消息, 会提示是否一键清空全部消息.
2022-04-19 00:35:56 +08:00
Easy a685433be3
Update 调试文档.md 2022-04-14 10:43:56 +08:00
Easy 0808c2257c 修正时间显示(+0前缀) 2022-04-07 23:41:53 +08:00
Easy 4fbf408eae add screen rotation 2022-04-07 19:00:38 +08:00
Easy 91c2d64295 字体配置 2022-04-07 18:15:52 +08:00
Easy 828f6f5454 添加时钟和配置保存 2022-04-07 16:01:31 +08:00
Easy 331daec61a 添加时钟 2022-04-06 13:02:25 +08:00
Easy 082ed2d3d2 添加配网逻辑 2022-04-05 22:08:37 +08:00
Easy c3bb7474bd add esp32 folder 2022-04-05 20:00:26 +08:00
Easy 80c034ef44 添加清除全部消息接口 2022-03-31 00:20:59 +08:00
Easy 722ba0eb9e 添加说明文档 2022-03-30 21:29:39 +08:00
Easy 654dc50155 fix 2022-03-30 18:29:04 +08:00
Easy 55de3ebf71 fix 2022-03-30 18:23:40 +08:00
Easy c1a3b746b7 添加Android自架版文档 2022-03-30 18:19:46 +08:00
Easy 6ed2d8d458
Merge pull request #99 from Hext123/main
修复消息列表UI可能卡死的问题, 优化一些界面和交互
2022-03-30 10:02:51 +08:00
hext 69ec4f3774 mac版本支持右键删除消息
应用内打开网页使用 BetterSafariView
修复markdown中图片出现相对路径时, 界面会卡死的问题
2022-03-30 01:45:02 +08:00
Easy 0ec3bbc7cc add redirect 2022-03-26 21:32:59 +08:00
Easy 3fa3d73859 完善文档标注只支持http 2022-03-26 18:24:38 +08:00
Easy afa5bf35be 完善文档:mqtt测试 2022-03-26 17:43:43 +08:00
Easy f208c5b314 添加iot设备屏幕旋转设置 2022-03-26 10:53:30 +08:00
Easy 981bc6b7f7 添加用MQTTX测试部分内容 2022-03-24 22:44:38 +08:00
Easy b494dec36c 修改默认mqtt服务器地址 2022-03-24 22:31:46 +08:00
Easy 78a91d642b add info 2022-03-24 18:36:57 +08:00
Easy b97cb44a14 mqtt服务器支持ws协议 2022-03-23 21:15:09 +08:00
Easy b39c92c42d 更新docker-compose,修正容器启动顺序依赖 2022-03-23 12:28:05 +08:00
Easy 9afddbfddb 添加 python sdk 链接 2022-03-22 14:49:24 +08:00
Easy 6f1cd94234 添加java sdk 2022-03-22 14:47:10 +08:00
Easy a570beaf3e 添加定期删除message的任务 2022-03-22 14:31:54 +08:00
Easy 967e1d4bcb
Update README.md 2022-03-19 21:15:23 +08:00
Easy f7b574bccd
Merge pull request #92 from alone-wolf/main
android add inner webview, create folder for self-hosted-push-server
2022-03-14 14:35:07 +08:00
alone-wolf 905ff7c8fe android add inner webview, create folder for self-hosted-push-server 2022-03-14 14:27:07 +08:00
Easy 94d15420f7
add c# sdk link 2022-03-13 10:23:46 +08:00
Easy a9ec7adfb0 add cors to message push api 2022-03-07 09:49:04 +08:00
Easy 987da61003 添加重装容器的文档 2022-03-04 15:21:38 +08:00
Easy 6e97c2f51a add link 2022-03-04 14:59:46 +08:00
Easy bf561050b5 更新调试信息 2022-03-04 14:51:14 +08:00
Easy b7c7ea7dd1
Merge pull request #88 from Hext123/main
去掉自己的长按手势弹复制菜单, 使用系统的
2022-03-02 13:12:50 +08:00
hext 3f61db008f 去掉自己的长按手势弹复制菜单, 使用系统的
针对自建 SelfHosted 版创建单独的 Info.plist 文件; SelfHosted 版本去掉 Schemes 配置
设置添加查看官网入口
2022-03-02 13:06:19 +08:00
Easy 8fbedcc5ce add "firebase/php-jwt": "^6.0" 2022-02-28 12:59:57 +08:00
Easy d58f6da266
Merge pull request #84 from Hext123/main
支持微信登录, 修复一些bug:
2022-02-27 23:06:16 +08:00
hext 2136883536 支持微信登录, 修复一些bug:
支持微信登录, 支持同时绑定微信和苹果账号;
解决markdown标题文字显示不全的问题;
markdown中的链接支持内嵌浏览器打开;
增加是否使用内置浏览器的设置项;
改名输入框, 失去焦点的时候也保存;
2022-02-27 23:02:58 +08:00
Easy 2310265c77 添加mqtt的dockerfile 2022-02-23 17:50:40 +08:00
Easy 7677c81ce8 修改readme,添加example目录 2022-02-23 00:41:25 +08:00
Easy 9114bc6879
Merge pull request #76 from 7YHong/main
快应用-增加运行展示gif
2022-02-22 22:44:44 +08:00
7YHong 8304b2a0f9 快应用-增加运行展示gif 2022-02-22 22:42:28 +08:00
Easy 000ca0b73c
Update README.md 2022-02-22 22:07:54 +08:00
Easy b7584dd371
Update README.md 2022-02-22 21:44:04 +08:00
Easy fb7d115c6b
更新子项目进度 2022-02-22 21:43:29 +08:00
Easy ec470e3728
Merge pull request #75 from 7YHong/main
撰写文档
2022-02-22 20:52:10 +08:00
7YHong 7bcea9c899 撰写文档 2022-02-22 20:50:14 +08:00
Easy 4f5441a982
Merge pull request #72 from 7YHong/main
完成微信登录对接,及其他界面上的修正
2022-02-22 09:25:04 +08:00
7YHong 37021ea19b 完成微信登录对接,及其他界面上的修正 2022-02-22 00:55:19 +08:00
Easy b68dc9abcd 更新调试为你挡 2022-02-21 15:02:31 +08:00
Easy e1f33c7a63 add esp32 dev board info 2022-02-20 23:38:59 +08:00
Easy d1f1940bd2 支持批量key推送,更新文档说明 2022-02-19 22:50:47 +08:00
Easy 9f669714b2 修改client name冲突的问题 2022-02-18 23:19:34 +08:00
Easy 6db248e2b8 添加了文件系统初始化的说明,程序兼容ESP32 2022-02-18 19:50:07 +08:00
Easy bb1025e1ef 更新文档细节格式 2022-02-17 18:16:06 +08:00
Easy 5589c3d361 fix 2022-02-17 18:12:26 +08:00
Easy 660e786984 更新readme 2022-02-17 18:11:14 +08:00
Easy 2c8cb03fac 更新微博地址 2022-02-17 18:06:25 +08:00
Easy 825b7893b7 更新演示gif 2022-02-17 17:59:32 +08:00
Easy ec2a968aae 更新DeerESP中文版代码 2022-02-17 17:53:54 +08:00
Easy a3cc2f4fd3 fix typo 2022-02-17 11:59:13 +08:00
Easy ff1dd0c302 添加推送中文问题的说明 2022-02-17 11:57:53 +08:00
Easy c0980b484c 更新图片 2022-02-17 11:52:43 +08:00
Easy 825273b07e 更新issue模板 2022-02-17 11:29:52 +08:00
Easy 7ce01d4a9d 添加issue模板 2022-02-17 11:19:02 +08:00
Easy 3768506949
Update issue templates 2022-02-17 11:09:34 +08:00
Easy 7488c5ae44 添加robo注释 2022-02-17 01:44:59 +08:00
Easy c7da8e5230 fix docker command 2022-02-17 00:52:43 +08:00
Easy 123ff5cce3 添加deeresp相关源码和文档 2022-02-17 00:46:24 +08:00
Easy c19b8996c9 修正图片类型接口不显示的问题 2022-02-16 19:17:50 +08:00
Easy 7e47762ba1 rename 2022-02-16 18:58:09 +08:00
Easy 619bec20ac 添加MQTT服务文档 2022-02-16 18:57:15 +08:00
Easy 630d479826 add mqtt server ( in docker ) 2022-02-16 17:47:54 +08:00
Easy ab15e50950 fix typo 2022-02-16 11:28:32 +08:00
Easy c938f02de1 默认启动页面添加http前缀和局域网ip提示 2022-02-16 11:27:54 +08:00
Easy ff7325ccd3
Merge pull request #62 from Hext123/main
添加自建服务配置, 优化交互, 修复bug.
2022-02-14 02:12:28 +08:00
hext d3050e154f 添加自建服务配置, 优化交互, 修复bug.
修改配置为pushdeer官方配置(包名,团队标识,api域名);
补充新添加的文字的英文翻译 (国际化);
设备和key是空列表时,加一个提示,让用户知道可以新增;
尝试解决列表删除项后UI偶尔没刷新的bug;
首次打开提示注册设备;
首次自动生成一个 key;
消息列表放到第一个位置;
推送测试时自动生成一个key;
后台进入前台后, 刷新本地消息列表;
键盘上方添加完成按钮, 用于收键盘;
发送推送测试后 自动收键盘;
及时清角标, 拯救强迫症;
为了保持一致, 设置页也改为可滑动;
添加SelfHosted配置, 可以直接一套代码跑出两种版本;
支持服务自建;
添加Env统一管理环境变量.
2022-02-14 01:13:26 +08:00
Easy 19d8e259d8 添加单一容器安装说明 2022-02-13 20:55:22 +08:00
Easy 555b1bc989 update readme 2022-02-13 18:00:32 +08:00
Easy a5bb9f6cc8 serverless version 2022-02-13 17:41:01 +08:00
Easy cd30839006 添加自动化测试,将api频率控制加入配置文件 2022-02-13 13:07:59 +08:00
Easy ed7d256527 gorush的redis有多次推送的bug,改用local 2022-02-11 23:40:00 +08:00
Easy 328704491a
Update ios.yml 2022-02-10 22:10:50 +08:00
Easy 0180a67b54
Merge pull request #53 from 7YHong/main
快应用:基本完成API对接
2022-02-09 01:18:45 +08:00
7YHong 03f0e531ad 快应用:基本完成API对接 2022-02-09 00:23:20 +08:00
Easy 4232570aba 推送默认启用redis作为队列实现 2022-02-08 14:13:41 +08:00
Easy 1fd0943f91 Merge branch 'main' of https://github.com/easychen/pushdeer 2022-02-08 14:12:09 +08:00
Easy 68a4f40f5a
Update README.md 2022-02-08 14:08:56 +08:00
Easy 4c15f4226d
Update README.md 2022-02-08 12:19:19 +08:00
Easy 8f76578aa6
Update README.md 2022-02-08 11:47:37 +08:00
Easy 22e0378d83
Create FUNDING.yml 2022-02-08 01:50:54 +08:00
Easy d1d4566152 修正了push不支持Redis队列的Bug,全面使用go rush作为推送中间件 2022-02-08 01:28:22 +08:00
Easy e0b359f2a1 微调参数,支持随机notifyID 2022-02-07 20:05:26 +08:00
Easy ea29e2314b
Merge pull request #50 from SinTod/main
add channel_id & channel_name
2022-02-07 19:29:48 +08:00
SinTod c19d75f2e7 add channel_id & channel_name 2022-02-07 19:16:48 +08:00
Easy 84cb7c8c93
Merge pull request #49 from SinTod/main
add config.yml & modify README.md
2022-02-07 19:04:12 +08:00
SinTod 9104131c6f modify README.md 2022-02-07 19:01:51 +08:00
SinTod 242335a986 add config.yml 2022-02-07 18:55:08 +08:00
Easy 74cbc0e888
Merge pull request #48 from SinTod/main
add gorush-with-mipush
2022-02-07 18:50:59 +08:00
SinTod ea79289d43 add gorush-with-mipush 2022-02-07 18:48:16 +08:00
Easy 16ccf6c247 fix result format 2022-02-06 13:01:56 +08:00
Easy b197b6fb23 Merge branch 'main' of https://github.com/easychen/pushdeer 2022-02-06 12:53:36 +08:00
Easy 782acfa53a 添加微信code2unionid接口,以支持self-hosted登入 2022-02-06 12:53:31 +08:00
Easy 02a81111e2
Rename dockerfile.serverless to Dockerfile.serverless 2022-02-06 10:48:11 +08:00
Easy b790dfd359
Rename dockerfile to Dockerfile 2022-02-06 10:47:37 +08:00
Easy ae6adf3349 添加Android自架版文档设计 2022-02-06 10:46:39 +08:00
Easy 2534c389c3
Update README.md 2022-02-05 11:25:38 +08:00
Easy b325398302
Update README.md 2022-02-04 14:10:35 +08:00
EasyChen d1571502b5 更新文档说明 2022-02-03 12:43:12 +08:00
EasyChen e61aaa01b1 修正层级关系 2022-02-02 22:02:17 +08:00
EasyChen 59649f6b50 add git pull info 2022-02-02 21:58:16 +08:00
EasyChen 354cff4850 change image size 2022-02-02 21:53:11 +08:00
EasyChen 5501101e35 添加图片大小设置 2022-02-02 21:45:54 +08:00
EasyChen 45f1a3f16f readme 2022-02-02 21:37:20 +08:00
EasyChen a5b8fe9b97 更新自架版启动说明 2022-02-02 21:34:59 +08:00
Easy f6b015ce67
Update README.md 2022-02-02 11:54:35 +08:00
EasyChen f257830b49 更新推送ready的配置,为自架客户端做准备 2022-02-02 00:08:15 +08:00
Easy 5467106425
添加sdk网址 2022-02-01 21:28:27 +08:00
EasyChen 451334d428 更新type为image时,通知文案的显示 2022-01-31 16:49:03 +08:00
EasyChen 623321f217 更新插件目录名称,添加zip包 2022-01-31 11:05:05 +08:00
EasyChen 9d7654c110 添加中间件支持post via json 2022-01-30 21:25:08 +08:00
EasyChen 17d0f894f2 添加wordpress评论通知插件 2022-01-30 21:01:47 +08:00
Easy 77d558d5a7
修改 android 发送函数返回的信息格式 2022-01-30 12:15:28 +08:00
Easy 24ee83ea49
Merge pull request #42 from Hext123/main
修复iOS 14闪退的问题, 优化一些问题
2022-01-30 06:25:02 +08:00
hext 11864a1cb3 Merge commit 'aa796f8d0e519a03f5b56c7ea344bbc0d794e92c'
* commit 'aa796f8d0e519a03f5b56c7ea344bbc0d794e92c':
  Update README.md
  Update README.md
  Update README.md
  未注册设备的情况下发送消息,显示注册提示
  Update README.md
  Update README.md
2022-01-30 01:12:37 +08:00
hext aa348feace 修复iOS 14闪退的问题, 优化一些问题 2022-01-30 01:02:44 +08:00
Easy aa796f8d0e
Update README.md 2022-01-29 14:53:58 +08:00
Easy b9c5b237b0
Update README.md 2022-01-29 14:50:18 +08:00
Easy 32cad861c3
Update README.md 2022-01-29 14:39:51 +08:00
EasyChen 3e0cc65957 未注册设备的情况下发送消息,显示注册提示 2022-01-29 09:34:45 +08:00
Easy ef14fa680a
Update README.md 2022-01-27 18:16:52 +08:00
Easy d5c198eba6
Update README.md 2022-01-27 17:59:07 +08:00
413 changed files with 109717 additions and 1129 deletions

View File

@ -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和轻APPClip是否分别都注册过设备: [是/否]
- 注册设备页面截图
- 应用通知设置页面截图
> 大部分用户在尝试删除重装应用后,可以修复问题,您也可以试试
**其他说明**

5
.github/FUNDING.yml vendored Normal file
View File

@ -0,0 +1,5 @@
# These are supported funding model platforms
github: [easychen]

67
.github/ISSUE_TEMPLATE/bug_report.md vendored Normal file
View File

@ -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和轻APPClip是否分别都注册过设备: [是/否]
- 注册设备页面截图
- 应用通知设置页面截图
> 大部分用户在尝试删除重装应用后,可以修复问题,您也可以试试
**其他说明**

9
.gitignore vendored
View File

@ -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
View File

@ -1,62 +1,224 @@
# PushDeer
> ⚠️ 目前官方架设的Android版本因接口权限停止无法使用[详情请点击](https://github.com/easychen/pushdeer/issues/150)
PushDeer开源版可以自行架设的无APP推送服务WIPAPI、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方案
- 轻APPAPP Clip
- 快应用
- 有APP方案
- iOS客户端
- Mac客户端
- Android客户端
- 自制设备方案:
- DeerESPESP8266/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日|iOS1514兼容修复中|第一版完成代码在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|格式,选填|文本=textmarkdown图片=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)

View File

@ -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");
}

54
android/.gitignore vendored
View File

@ -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

View File

@ -1,3 +0,0 @@
# Default ignored files
/shelf/
/workspace.xml

View File

@ -1,6 +0,0 @@
<?xml version="1.0" encoding="UTF-8"?>
<project version="4">
<component name="CompilerConfiguration">
<bytecodeTargetLevel target="11" />
</component>
</project>

View File

@ -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>

View File

@ -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>

View File

@ -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>

View File

@ -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

View File

@ -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'

Binary file not shown.

Binary file not shown.

View File

@ -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"
}
],

View File

@ -123,6 +123,8 @@
</intent-filter>
</receiver>
<activity android:name=".WebViewActivity"/>
</application>

View File

@ -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"

View File

@ -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"
}

View File

@ -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) {

View File

@ -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
}
}
}

View File

@ -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()
// }
//}

View File

@ -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)

View File

@ -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)
}

View File

@ -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)
)

View File

@ -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 &amp; table extensions\n" +
// " * &amp; 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)
)
}

View File

@ -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)
)
}
}
}
}
}
}

View File

@ -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
// )
// }
// }
//}

View File

@ -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()
}
})
}
}

View File

@ -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>

View File

@ -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>

View File

@ -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>

View File

@ -0,0 +1,4 @@
storeFile=/Users/easy/Code/pushdeer-andorid/com.pushdeer.com
storePassword=pushdeer.com
keyAlias=pushdeer
keyPassword=pushdeer.com

View File

@ -11,5 +11,4 @@ rootProject.name = "PushDeer"
include ':app'
include ':common'
include ':compose'
include ':pushdeerclient'
include ':pushdeercommon'

View File

@ -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

View File

@ -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;
}
}

View File

@ -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();
}
/**

View File

@ -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']);
}
}

View File

@ -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
]
);

View File

@ -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]]);
}

View File

@ -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,
];
}

View File

@ -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);
}
}

View File

@ -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());
});
}
}

View File

@ -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",

61
api/composer.lock generated
View File

@ -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"
}

View File

@ -15,7 +15,7 @@ return [
|
*/
'paths' => ['api/*', 'sanctum/csrf-cookie'],
'paths' => ['message/push', 'sanctum/csrf-cookie'],
'allowed_methods' => ['*'],

View File

@ -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');
});
}
}

View File

@ -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');
});
}
}

View File

@ -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');
});
}
}

BIN
api/public/code.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 50 KiB

View File

@ -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>

View File

@ -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');

View File

@ -14,5 +14,5 @@ use Illuminate\Support\Facades\Route;
*/
Route::get('/', function () {
return ['PushDeer'=>'On'];
return view('pushdeer', []);
});

View File

@ -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');
}
}

View File

@ -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启动为服务并常驻后台可在顶栏显示一个常驻图标点击后进入应用。

Binary file not shown.

After

Width:  |  Height:  |  Size: 166 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 167 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 77 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 90 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 50 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 279 KiB

BIN
doc/image/video.gif Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.8 MiB

View File

@ -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的推送证书。

View File

@ -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) 接口获得。

View File

@ -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:

View File

@ -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"

View File

@ -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

BIN
docker/.DS_Store vendored

Binary file not shown.

23
docker/mqtt/Dockerfile Normal file
View File

@ -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",""]

View File

@ -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",""]

16
docker/mqtt/RoboFile.php Normal file
View File

@ -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");
}
}

1
docker/mqtt/api/.gitignore vendored Normal file
View File

@ -0,0 +1 @@
node_modules

68
docker/mqtt/api/index.js Normal file
View File

@ -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`);
});

View File

@ -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"
}
}

1496
docker/mqtt/api/yarn.lock Normal file

File diff suppressed because it is too large Load Diff

View File

@ -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

View File

@ -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

10
docker/mqtt/init.sh Normal file
View File

@ -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

View File

@ -0,0 +1,6 @@
listener 1883
listener 9001
protocol websockets
allow_anonymous false
log_dest stdout
password_file /mospass.txt

View File

@ -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.crtserver.key然后去掉下一行的注释
# ADD ssl /app/ssl
EXPOSE 80

View File

@ -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

459
docker/web/application.conf Normal file
View File

@ -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

BIN
docker/web/gorush Normal file → Executable file

Binary file not shown.

View File

@ -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 &

1
docker/web/larave-cron Normal file
View File

@ -0,0 +1 @@
* * * * * cd /app/api && php artisan schedule:run >> /dev/null 2>&1

16
docker/web/ports.conf Normal file
View File

@ -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

View File

@ -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

View File

@ -24,6 +24,8 @@
Order allow,deny
allow from all
</Directory>
# 配置 https
# 去掉下边三行的注释
#SSLEngine on
#SSLCertificateFile /app/ssl/server.crt
#SSLCertificateKeyFile /app/ssl/server.key

20
examples/demo.php Normal file
View File

@ -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...'));

View File

@ -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>

View File

@ -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>

View File

@ -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)
}
}
}

View File

@ -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

View File

@ -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

File diff suppressed because it is too large Load Diff

View File

@ -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>

View File

@ -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>

Some files were not shown because too many files have changed in this diff Show More