Compare commits

...

47 Commits
V1.0 ... master

Author SHA1 Message Date
cd70b8ea8a 更新 'README.md'
Signed-off-by: heibai2006 <heibai2006@noreply.localhost>
2023-05-14 07:42:54 +00:00
xpnas
42f9461643 update Telegram.Bot to 18.0.0 2022-11-01 21:56:28 +08:00
xpnas
a373cac904
Merge pull request #8 from xpnas/dependabot/nuget/Inotify/Microsoft.AspNetCore.Authentication.JwtBearer-3.1.18
Bump Microsoft.AspNetCore.Authentication.JwtBearer from 3.1.12 to 3.1.18 in /Inotify
2022-11-01 20:31:58 +08:00
xpnas
65921743eb
Update README.md 2022-10-13 11:15:30 +08:00
xpnas
e4970bf07c
Update package.json
remove element-plus
2022-10-13 11:10:44 +08:00
xpnas
b8afd8021b
Update package.json 2022-02-07 16:10:32 +08:00
xpnas
cc784cdd38 fix bug 2022-01-08 01:24:39 +08:00
xpnas
ffa9e77c81 del build.yml 2022-01-06 22:29:08 +08:00
xpnas
c9b06ee5a4 for sonar 2022-01-05 20:42:35 +08:00
xpnas
aa30040f8c Merge branch 'master' of github.com:xpnas/inotify 2022-01-05 20:41:53 +08:00
xpnas
3a1d222fc2 for sonar 2022-01-05 20:41:37 +08:00
xpnas
b247f04a03
Update build.yml 2022-01-05 20:35:51 +08:00
xpnas
0f1e9acea3
Create build.yml 2022-01-05 13:53:37 +08:00
dependabot[bot]
acecc333b8
Bump Microsoft.AspNetCore.Authentication.JwtBearer in /Inotify
Bumps [Microsoft.AspNetCore.Authentication.JwtBearer](https://github.com/aspnet/AspNetCore) from 3.1.12 to 3.1.18.
- [Release notes](https://github.com/aspnet/AspNetCore/releases)
- [Commits](https://github.com/aspnet/AspNetCore/compare/v3.1.12...v3.1.18)

---
updated-dependencies:
- dependency-name: Microsoft.AspNetCore.Authentication.JwtBearer
  dependency-type: direct:production
...

Signed-off-by: dependabot[bot] <support@github.com>
2021-10-21 07:09:21 +00:00
xpnas
cc86def7e7
Update README.md 2021-10-17 02:57:32 +08:00
xpnas
b07efef9c0
Update README.md 2021-05-19 21:12:01 +08:00
xpnas
418dab6097 TAG2.3 2021-05-19 21:06:57 +08:00
xpnas
9d63518106 添加BARK授权信息初始化 2021-05-19 21:01:56 +08:00
xpnas
4b15dfaa0f 修改workflow 2021-05-12 19:34:39 +08:00
xpnas
31d177ca4b 增加dev分支 2021-05-12 19:33:36 +08:00
xpnas
7eb71ff504
Merge pull request #4 from cxfksword/master
修正data参数拼写错误
2021-05-07 19:20:03 +08:00
cxfksword
e07a873bc2
Fix typo 2021-05-07 12:09:40 +08:00
xpnas
71b617e754 修复table systeminfo not found错误 2021-04-13 12:36:48 +08:00
xpnas
47b43d4013
Update README.md 2021-04-08 22:30:31 +08:00
xpnas
9031f3fafa 修改打包脚本 2021-04-08 22:28:29 +08:00
xpnas
5cd022770e 增加BARK声音、保存、自动复制支持 2021-04-08 22:17:40 +08:00
xpnas
77048a07a9
Update README.md 2021-04-06 21:11:49 +08:00
xpnas
a99d5a2705 完成BARK支持 2021-04-06 20:05:06 +08:00
xpnas
ec70089d77 增加消息通道独立推送 2021-04-05 17:06:21 +08:00
xpnas
fa1ad54afb 修正版本信息 2021-04-05 13:18:02 +08:00
xpnas
974d0e8543 增加钉钉与飞信群机器人推送支持 2021-04-05 13:14:50 +08:00
xpnas
e96c85cd55 兼容V1版本消息接口 2021-04-04 00:08:05 +08:00
xpnas
6612a662cb 修复V1直接升级到V2.0.0.2数据库兼容 2021-04-03 23:58:50 +08:00
xpnas
939f64549c 添加BARK支持 2021-04-03 23:26:09 +08:00
xpnas
d4b1f9faf2 Merge branch 'master' of github.com:xpnas/inotify 2021-04-02 01:14:42 +08:00
xpnas
bb7e25729f 增加版本数据库兼容
增加多通道消息发送
BARK部分代码
2021-04-02 01:14:09 +08:00
xpnas
0d95547798
Update README.md 2021-03-28 23:09:17 +08:00
xpnas
1e1cdfa6c2 添加自定义Get和POST 2021-03-28 23:07:13 +08:00
xpnas
7275e8810e 添加自定义Get和Post支持 2021-03-28 23:05:43 +08:00
xpnas
303530589c 修改README 2021-03-28 09:56:59 +08:00
xpnas
f413ea34ec 修改README 2021-03-27 20:55:30 +08:00
xpnas
44e74f4818 修复打包错误 2021-03-27 20:39:54 +08:00
xpnas
07a6d28e2e Merge branch 'master' of github.com:xpnas/inotify 2021-03-27 20:25:26 +08:00
xpnas
6e862ac968 启用CDN 2021-03-27 20:25:13 +08:00
xpnas
a46949a181
Update README.md 2021-03-27 01:44:34 +08:00
xpnas
8b117c6798
Update README.md 2021-03-27 01:41:28 +08:00
xpnas
ed5975df7d 修改Docker脚本 2021-03-27 01:39:14 +08:00
83 changed files with 2907 additions and 756 deletions

View File

@ -14,10 +14,6 @@ jobs:
name: Check out the repo
uses: actions/checkout@v2
-
name: Set up QEMU
uses: docker/setup-qemu-action@v1
- name: Cache node modules NPM
uses: actions/cache@v2
env:
@ -60,4 +56,4 @@ jobs:
tags: ${{ secrets.DOCKERHUB_TAG }}:master
- name: 'Report Suecss'
run: curl ${{ secrets.INOTIFY }}/Inotify/dockerBuildComplated!
run: curl ${{ secrets.INOTIFY }}/Inotify/masterIsOk!

59
.github/workflows/docker_dev.yml vendored Normal file
View File

@ -0,0 +1,59 @@
name: docker_dev
on:
push:
branches: [ dev ]
pull_request:
branches: [ dev ]
jobs:
build:
runs-on: ubuntu-latest
steps:
-
name: Check out the repo
uses: actions/checkout@v2
- name: Cache node modules NPM
uses: actions/cache@v2
env:
cache-name: cache-node-modules-NPM
with:
path: ~/.npm
key: ${{ runner.os }}-build-${{ env.cache-name }}-${{ hashFiles('./Inotify.Vue/package.json') }}
restore-keys: ${{ runner.os }}-build-${{ env.cache-name }}-${{ hashFiles('./Inotify.Vue/package.json') }}
- name: Cache node modules
uses: actions/cache@v2
env:
cache-name: cache-node-modules
with:
path: ./Inotify.Vue/node_modules
key: ${{ runner.os }}-build-${{ env.cache-name }}-${{ hashFiles('./Inotify.Vue/package.json') }}
restore-keys: ${{ runner.os }}-build-${{ env.cache-name }}-${{ hashFiles('./Inotify.Vue/package.json') }}
- name: InstallNode and BuildVue
run: |
cd ./Inotify.Vue
npm install
npm run build:prod
-
name: Login to DockerHub
uses: docker/login-action@v1
with:
username: ${{ secrets.DOCKERHUB_USERNAME }}
password: ${{ secrets.DOCKERHUB_TOKEN }}
-
name: Docker Build & Push to Docker Hub For Service
uses: docker/build-push-action@v2
with:
context: .
file: ./Inotify/Dockerfile
platforms: linux/amd64
push: true
tags: ${{ secrets.DOCKERHUB_TAG }}:dev
- name: 'Report Suecss'
run: curl ${{ secrets.INOTIFY }}/Inotify/DevIsOk!

View File

@ -12,10 +12,6 @@ jobs:
name: Check out the repo
uses: actions/checkout@v2
-
name: Set up QEMU
uses: docker/setup-qemu-action@v1
- name: Cache node modules NPM
uses: actions/cache@v2
env:
@ -58,4 +54,4 @@ jobs:
tags: ${{ secrets.DOCKERHUB_TAG }}:latest
- name: 'Report Suecss'
run: curl ${{ secrets.INOTIFY }}/Inotify/dockerBuildComplated!
run: curl ${{ secrets.INOTIFY }}/Inotify/latestIsOK!

View File

@ -1,5 +1,5 @@
{
"name": "Inotify-vue",
"name": "inotify",
"version": "0.0.1",
"description": "WebControl for Inotify",
"author": "<mrdemonson@gmail.com>",
@ -17,13 +17,13 @@
"axios": "0.18.1",
"core-js": "3.6.5",
"echarts": "^4.9.0",
"element-plus": "^1.0.2-beta.32",
"element-ui": "2.13.2",
"js-cookie": "2.2.0",
"moment": "^2.29.1",
"normalize.css": "7.0.0",
"nprogress": "0.2.0",
"path-to-regexp": "2.4.0",
"qrcodejs": "^1.0.0",
"vue": "2.6.10",
"vue-router": "3.0.6",
"vuex": "3.1.0"

Binary file not shown.

Before

Width:  |  Height:  |  Size: 66 KiB

After

Width:  |  Height:  |  Size: 1.1 KiB

View File

@ -1,17 +1,23 @@
<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8">
<meta http-equiv="X-UA-Compatible" content="IE=edge,chrome=1">
<meta name="viewport" content="width=device-width, initial-scale=1, maximum-scale=1, user-scalable=no">
<link rel="icon" href="<%= BASE_URL %>favicon.ico">
<title><%= webpackConfig.name %></title>
</head>
<body>
<noscript>
<strong>We're sorry but <%= webpackConfig.name %> doesn't work properly without JavaScript enabled. Please enable it to continue.</strong>
</noscript>
<div id="app"></div>
<!-- built files will be auto injected -->
</body>
</html>
<head>
<meta charset="utf-8">
<meta http-equiv="X-UA-Compatible" content="IE=edge,chrome=1">
<meta name="viewport" content="width=device-width, initial-scale=1, maximum-scale=1, user-scalable=no">
<link rel="icon" href="<%= BASE_URL %>favicon.ico">
<% for (var i in htmlWebpackPlugin.options.cdn && htmlWebpackPlugin.options.cdn.css) { %>
<link href="<%= htmlWebpackPlugin.options.cdn.css[i] %>" rel="stylesheet">
<% } %>
<title>
<%= webpackConfig.name %>
</title>
</head>
<body>
<% for (var i in htmlWebpackPlugin.options.cdn && htmlWebpackPlugin.options.cdn.js) { %>
<script src="<%= htmlWebpackPlugin.options.cdn.js[i] %>"></script>
<% } %>
<div id="app"></div>
</body>
</html>

View File

@ -1,8 +1,8 @@
import Vue from 'vue'
import 'normalize.css/normalize.css' // A modern alternative to CSS resets
// import 'normalize.css/normalize.css' // A modern alternative to CSS resets
import ElementUI from 'element-ui'
import 'element-ui/lib/theme-chalk/index.css'
// import 'element-ui/lib/theme-chalk/index.css'
import locale from 'element-ui/lib/locale/lang/zh-CN' // lang i18n
import '@/styles/index.scss' // global css
@ -13,25 +13,7 @@ import router from './router'
import '@/icons' // icon
import '@/permission' // permission control
/**
* If you don't want to use mock-server
* you want to use MockJs for mock api
* you can execute: mockXHR()
*
* Currently MockJs will be used in the production environment,
* please remove it before going online ! ! !
*/
// if (process.env.NODE_ENV === 'production') {
// const { mockXHR } = require('../mock')
// mockXHR()
// }
// set ElementUI lang to EN
Vue.use(ElementUI, { locale, size: 'small' })
// 如果想要中文版 element-ui按如下方式声明
// Vue.use(ElementUI)
Vue.config.productionTip = false
new Vue({

View File

@ -1,228 +1,45 @@
<template>
<div class="wscn-http404-container">
<div class="wscn-http404">
<div class="pic-404">
<img class="pic-404__parent" src="@/assets/404_images/404.png" alt="404">
<img class="pic-404__child left" src="@/assets/404_images/404_cloud.png" alt="404">
<img class="pic-404__child mid" src="@/assets/404_images/404_cloud.png" alt="404">
<img class="pic-404__child right" src="@/assets/404_images/404_cloud.png" alt="404">
</div>
<div class="bullshit">
<div class="bullshit__oops">OOPS!</div>
<div class="bullshit__info">All rights reserved
<a style="color:#20a0ff" href="https://wallstreetcn.com" target="_blank">wallstreetcn</a>
</div>
<div class="bullshit__headline">{{ message }}</div>
<div class="bullshit__info">Please check that the URL you entered is correct, or click the button below to return to the homepage.</div>
<a href="" class="bullshit__return-home">Back to home</a>
</div>
</div>
<div class="circle">
<div><span class="block"></span><span>404</span><span class="block"></span></div>
</div>
</template>
<script>
export default {
name: 'Page404',
computed: {
message() {
return 'The webmaster said that you can not enter this page...'
}
}
},
},
}
</script>
<style lang="scss" scoped>
.wscn-http404-container{
transform: translate(-50%,-50%);
<style>
.circle {
width: 200px;
height: 200px;
border-radius: 200px;
border: 15px solid #b22727;
}
.circle > div {
color: #b22727;
font: bolder 50px arial;
transform: matrix(0.642788, -0.766044, 0.766044, 0.642788, 0, 95);
-ms-transform: matrix(0.642788, -0.766044, 0.766044, 0.642788, 0, 95);
-moz-transform: matrix(0.642788, -0.766044, 0.766044, 0.642788, 0, 95);
-o-transform: matrix(0.642788, -0.766044, 0.766044, 0.642788, 0, 95);
position: absolute;
top: 40%;
left: 50%;
top: 0;
left: 0;
}
.wscn-http404 {
position: relative;
width: 1200px;
padding: 0 50px;
overflow: hidden;
.pic-404 {
position: relative;
float: left;
width: 600px;
overflow: hidden;
&__parent {
width: 100%;
}
&__child {
position: absolute;
&.left {
width: 80px;
top: 17px;
left: 220px;
opacity: 0;
animation-name: cloudLeft;
animation-duration: 2s;
animation-timing-function: linear;
animation-fill-mode: forwards;
animation-delay: 1s;
}
&.mid {
width: 46px;
top: 10px;
left: 420px;
opacity: 0;
animation-name: cloudMid;
animation-duration: 2s;
animation-timing-function: linear;
animation-fill-mode: forwards;
animation-delay: 1.2s;
}
&.right {
width: 62px;
top: 100px;
left: 500px;
opacity: 0;
animation-name: cloudRight;
animation-duration: 2s;
animation-timing-function: linear;
animation-fill-mode: forwards;
animation-delay: 1s;
}
@keyframes cloudLeft {
0% {
top: 17px;
left: 220px;
opacity: 0;
}
20% {
top: 33px;
left: 188px;
opacity: 1;
}
80% {
top: 81px;
left: 92px;
opacity: 1;
}
100% {
top: 97px;
left: 60px;
opacity: 0;
}
}
@keyframes cloudMid {
0% {
top: 10px;
left: 420px;
opacity: 0;
}
20% {
top: 40px;
left: 360px;
opacity: 1;
}
70% {
top: 130px;
left: 180px;
opacity: 1;
}
100% {
top: 160px;
left: 120px;
opacity: 0;
}
}
@keyframes cloudRight {
0% {
top: 100px;
left: 500px;
opacity: 0;
}
20% {
top: 120px;
left: 460px;
opacity: 1;
}
80% {
top: 180px;
left: 340px;
opacity: 1;
}
100% {
top: 200px;
left: 300px;
opacity: 0;
}
}
}
}
.bullshit {
position: relative;
float: left;
width: 300px;
padding: 30px 0;
overflow: hidden;
&__oops {
font-size: 32px;
font-weight: bold;
line-height: 40px;
color: #1482f0;
opacity: 0;
margin-bottom: 20px;
animation-name: slideUp;
animation-duration: 0.5s;
animation-fill-mode: forwards;
}
&__headline {
font-size: 20px;
line-height: 24px;
color: #222;
font-weight: bold;
opacity: 0;
margin-bottom: 10px;
animation-name: slideUp;
animation-duration: 0.5s;
animation-delay: 0.1s;
animation-fill-mode: forwards;
}
&__info {
font-size: 13px;
line-height: 21px;
color: grey;
opacity: 0;
margin-bottom: 30px;
animation-name: slideUp;
animation-duration: 0.5s;
animation-delay: 0.2s;
animation-fill-mode: forwards;
}
&__return-home {
display: block;
float: left;
width: 110px;
height: 36px;
background: #1482f0;
border-radius: 100px;
text-align: center;
color: #ffffff;
opacity: 0;
font-size: 14px;
line-height: 36px;
cursor: pointer;
animation-name: slideUp;
animation-duration: 0.5s;
animation-delay: 0.3s;
animation-fill-mode: forwards;
}
@keyframes slideUp {
0% {
transform: translateY(60px);
opacity: 0;
}
100% {
transform: translateY(0);
opacity: 1;
}
}
}
.block {
width: 60px;
display: inline-block;
height: 15px;
background-color: #b22727;
margin: 12px 10px;
}
</style>
</style>

View File

@ -1,10 +1,10 @@
<template>
<div class='app-container'>
<el-dialog :title='title' :visible.sync='dialogVisible' :append-to-body='true'>
<el-form ref="authform" :model="authform" label-width="100px" label-position='right' :rules="authformrules">
<el-form ref="authform" :model="authform" label-width="120px" label-position='right' :rules="authformrules">
<el-form-item label='通道类型'>
<el-select :disabled='isModify' value-key="key" v-model='selectTemplate' placeholder='请选择' @change="selectTemplateChange">
<el-option v-for="item in sendTemplates" :key="item.key" :label="item.name" :value="item">
<el-select :disabled='isModify' value-key="type" v-model='selectTemplate' placeholder='请选择' @change="selectTemplateChange">
<el-option v-for="item in sendTemplates" :key="item.type" :label="item.typeName" :value="item">
</el-option>
</el-select>
</el-form-item>
@ -12,7 +12,13 @@
<el-input v-model="selectTemplate.name"></el-input>
</el-form-item>
<el-form-item v-for="(item) in selectTemplate.values" :label="item.description" :key="item.name" required>
<el-input v-model="item.value" :placeholder="item.default"></el-input>
<el-input v-model="item.value" :placeholder="item.default" :readonly="item.readonly"></el-input>
</el-form-item>
<el-form-item label="绑定地址" v-show="isBark">
<el-input v-model="selectTemplate.barkUrl" readonly></el-input>
</el-form-item>
<el-form-item label="二维码地址" v-show="isBark">
<div id="bark"></div>
</el-form-item>
</el-form>
@ -32,7 +38,7 @@
</el-table-column>
<el-table-column label='类型' width='110' align='center'>
<template slot-scope='scope'>
<span>{{ scope.row.type }}</span>
<span>{{ scope.row.typeName }}</span>
</template>
</el-table-column>
<el-table-column label='名称' width='110' align='center'>
@ -40,9 +46,9 @@
<span>{{ scope.row.name }}</span>
</template>
</el-table-column>
<el-table-column label='配置信息' align='center'>>
<el-table-column label='调用接口' align='center'>
<template slot-scope='scope'>
{{ scope.row.authData }}
<el-link type="primary" target="_blank"> <a :href="scope.row.url" target="_blank" class="buttonText">{{scope.row.url}}</a></el-link>
</template>
</el-table-column>
<el-table-column align='center' prop='created_at' label='编辑' width='200'>
@ -71,6 +77,7 @@ import {
getSendTemplates,
addAuthInfo,
deepClone,
getSendKey
} from '@/api/setting'
@ -102,7 +109,8 @@ export default {
isModify: false,
authform: {},
title: "设置",
sendKey: "",
isBark: false
}
},
created() {
@ -113,18 +121,44 @@ export default {
this.listLoading = true
getSendAuths().then((response) => {
this.sendAuthinfos = response.data
let origin = window.document.location.origin;
for (var sendinfo of this.sendAuthinfos) {
sendinfo.url = origin + '/' + sendinfo.key + ".send/{title}/{data}"
}
this.listLoading = false
})
getSendTemplates().then((response) => {
if (response.code == 200) {
this.sendTemplates = response.data;
}
})
getSendKey().then(response => {
if (response.code == 200) {
this.sendKey = response.data;
}
})
},
selectTemplateChange(selectTemplate) {
this.selectTemplate = deepClone(selectTemplate);
this.selectTemplate = deepClone(selectTemplate)
this.isBark = false;
if (this.selectTemplate.warning) {
this.$message({
message: this.selectTemplate.warning,
type: 'warning'
})
if (this.selectTemplate.typeName == 'Bark') {
this.isBark = true;
this.selectTemplate.values = [];
let origin = window.document.location.origin;
this.selectTemplate.barkUrl = origin + '?act=' + this.sendKey;
document.getElementById("bark").innerHTML = '';
new QRCode(document.getElementById('bark'), {
text: this.selectTemplate.barkUrl,
width: 250,
height: 250,
});
}
}
},
submitForm(formName) {
this.$refs[formName].validate((valid) => {
@ -171,6 +205,7 @@ export default {
});
},
addAuth() {
this.isBark = false;
this.title = '新增设置'
this.dialogVisible = true
this.isModify = false
@ -180,9 +215,21 @@ export default {
modifyAuth(index, row) {
this.title = '修改设置'
this.isModify = true;
this.isBark = false;
this.selectTemplate = deepClone(row);
this.dialogVisible = true;
if (this.selectTemplate.typeName == "Bark") {
this.isBark = true;
let origin = window.document.location.origin;
var devieItem = this.selectTemplate.values.find(item => {
return item.name == "DeviceKey"
})
var sendUrlItem = this.selectTemplate.values.find(item => {
return item.name == "SendUrl"
});
sendUrlItem.value = origin + "?act=" + this.sendKey + "/" + devieItem.value + "/{title}/{data}"
}
this.dialogVisible = true;
},
deleteAuth(index, row) {
deleteAuthInfo(row.sendAuthId).then((response) => {
@ -209,28 +256,32 @@ export default {
this.$message.error(state ? '激活失败' : '注销失败');
}
})
},
getBarkUrl() {
}
}
}
</script>
<style lang="scss">
<style lang="scss">
@media screen and (min-width:800px) {
.el-dialog__wrapper .el-dialog {
width: 800px !important;
}
.el-dialog__wrapper .el-dialog .el-dialog__body {
overflow: auto
}
}
@media screen and (max-width: 800px) {
@media screen and (max-width: 800px) {
.el-dialog__wrapper .el-dialog {
width: 99% !important;
}
.el-dialog__wrapper .el-dialog .el-dialog__body {
overflow: auto
}
}
</style>

View File

@ -20,17 +20,27 @@
<el-divider content-position="left">重置授权</el-divider>
<el-form ref="resetform" :model="keyForm" label-width="20%">
<el-form-item label="当前SendKey">
<el-form-item label="当前Token">
<el-input v-model="keyForm.sendKey" :readonly="true"></el-input>
</el-form-item>
<el-form-item label="快捷地址(标题)">
<el-form-item label="调用接口(标题)">
<el-input type="textarea" v-model="keyForm.sendUrlTitle" :readonly="true"></el-input>
</el-form-item>
<el-form-item label="快捷地址(完整)">
<el-form-item label="调用接口(完整)">
<el-input type="textarea" v-model="keyForm.sendUrl" :readonly="true"></el-input>
</el-form-item>
<el-form-item>
<el-button type="primary" @click="onReSendKey('resetform')">重置SendKey</el-button>
<el-button type="primary" @click="onReSendKey('resetform')">重置Token</el-button>
</el-form-item>
</el-form>
<el-divider content-position="left">BARK授权</el-divider>
<el-form ref="resetform" :model="keyForm" label-width="20%">
<el-form-item label="绑定地址">
<el-input v-model="keyForm.barkUrl" :readonly="true"></el-input>
</el-form-item>
<el-form-item>
<div id="qrcode"> </div>
</el-form-item>
</el-form>
</el-card>
@ -90,15 +100,16 @@ export default {
getSendKey().then((response) => {
this.keyForm.sendKey = response.data;
this.messageForm.sendKey = response.data;
let wPath = window.document.location.href;
let pathName = this.$route.path;
let pos = wPath.indexOf(pathName);
let localhostPath = wPath.substring(0, pos);
localhostPath = localhostPath.replace("#", "");
this.keyForm.sendUrlTitle = localhostPath + 'api/'+ this.keyForm.sendKey+'.send' + "/{title}"
this.keyForm.sendUrl = localhostPath + 'api/'+ this.keyForm.sendKey+'.send' + "/{title}/{data}"
// this.keyForm.sendUrl = localhostPath + 'api/send?token=' + this.keyForm.sendKey + "&title={title}&data={data}"
let origin = window.document.location.origin;
this.keyForm.sendUrlTitle = origin + '/' + this.keyForm.sendKey + '.send' + "/{title}"
this.keyForm.sendUrl = origin + '/' + this.keyForm.sendKey + '.send' + "/{title}/{data}"
this.keyForm.barkUrl = origin + '?act=' + this.keyForm.sendKey
this.listLoading = false;
new QRCode(document.getElementById("qrcode"), {
text: this.keyForm.barkUrl,
width: 150,
height: 150,
});
});
},
onMessage(fromname) {

View File

@ -36,6 +36,17 @@
<el-switch active-color='#13ce66' v-model='settingForm.githubEnable' inactive-color='#ff4949'></el-switch>
</template>
</el-form-item>
<el-divider content-position="left">BARK授权</el-divider>
<el-form-item label="KeyID">
<el-input v-model="settingForm.barkKeyId" placeholder="KeyID">></el-input>
</el-form-item>
<el-form-item label="TempID">
<el-input v-model="settingForm.barkTeamId" placeholder="TempID"></el-input>
</el-form-item>
<el-form-item label="P8Key">
<el-input type="textarea" v-model="settingForm.barkPrivateKey" placeholder="P8Key"></el-input>
</el-form-item>
<el-form-item>
<el-button type="primary" @click="submitForm('settingForm')">确认修改</el-button>
</el-form-item>

View File

@ -6,24 +6,49 @@ function resolve(dir) {
return path.join(__dirname, dir)
}
const name = defaultSettings.title || 'Inotify' // page title
const name = defaultSettings.title || 'Inotify'
const port = process.env.port || process.env.npm_config_port || 9000
// If your port is set to 80,
// use administrator privileges to execute the command line.
// For example, Mac: sudo npm run
// You can change the port by the following methods:
// port = 9528 npm run dev OR npm run dev --port = 9528
const port = process.env.port || process.env.npm_config_port || 9528 // dev port
const axiosV = require('axios/package.json').version
const echartsV = require('echarts/package.json').version
const elementV = require('element-ui/package.json').version
const jscookieV = require('js-cookie/package.json').version
const normalizeV = require('normalize.css/package.json').version
const vueV = require('vue/package.json').version
const routerV = require('vue-router/package.json').version
const vuexV = require('vuex/package.json').version
const cookieV = require('js-cookie/package.json').version
const nprogressV = require('nprogress/package.json').version
const momentV = require('moment/package.json').version
const cdn = {
externals: {
axios: 'axios',
echarts: 'echarts',
'element-ui': 'ELEMENT',
moment: 'moment',
locale: 'locale',
vue: 'Vue',
vuex: 'Vuex',
'vue-router': 'VueRouter'
},
css: [`https://cdn.bootcdn.net/ajax/libs/element-ui/${elementV}/theme-chalk/index.css`, `https://cdn.bootcdn.net/ajax/libs/nprogress/${nprogressV}/nprogress.min.css`, `https://lib.baomitu.com/normalize/${normalizeV}/normalize.css`],
js: [
`https://cdn.bootcdn.net/ajax/libs/vue/${vueV}/vue.min.js`,
`https://cdn.bootcdn.net/ajax/libs/vuex/${vuexV}/vuex.min.js`,
`https://cdn.bootcdn.net/ajax/libs/js-cookie/${jscookieV}/js.cookie.js`,
`https://cdn.bootcdn.net/ajax/libs/element-ui/${elementV}/index.js`,
`https://cdn.bootcdn.net/ajax/libs/axios/${axiosV}/axios.min.js`,
`https://cdn.bootcdn.net/ajax/libs/moment.js/2.29.1/moment.min.js`,
`https://cdn.bootcdn.net/ajax/libs/echarts/4.9.0-rc.1/echarts.min.js`,
`https://cdn.bootcdn.net/ajax/libs/vue-router/${routerV}/vue-router.min.js`,
`https://cdn.bootcdn.net/ajax/libs/element-ui/${elementV}/locale/zh-CN.js`,
`https://cdn.bootcdn.net/ajax/libs/js-cookie/${cookieV}/js.cookie.min.js`,
`https://cdn.bootcdn.net/ajax/libs/nprogress/${nprogressV}/nprogress.min.js`,
`https://cdn.bootcdn.net/ajax/libs/qrcodejs/1.0.0/qrcode.js`
]
}
// All configuration item explanations can be find in https://cli.vuejs.org/config/
module.exports = {
/**
* You will need to set publicPath if you plan to deploy your site under a sub path,
* for example GitHub Pages. If you plan to deploy your site to https://foo.github.io/bar/,
* then publicPath should be set to "/bar/".
* In most cases please use '/' !!!
* Detail: https://cli.vuejs.org/config/#publicpath
*/
publicPath: '/',
outputDir: '../Inotify/wwwroot',
assetsDir: 'static',
@ -39,9 +64,6 @@ module.exports = {
before: require('./mock/mock-server.js')
},
configureWebpack: {
// provide the app's title in webpack's name field, so that
// it can be accessed in index.html to inject the correct title.
name: name,
resolve: {
alias: {
'@': resolve('src')
@ -49,19 +71,23 @@ module.exports = {
},
devtool: 'source-map',
performance: {
hints: 'warning', // 枚举
hints: 'error', // 性能提示中抛出错误
hints: false, // 关闭性能提示
maxAssetSize: 200000, // 整数类型(以字节为单位)
maxEntrypointSize: 400000, // 整数类型(以字节为单位)
hints: 'warning',
hints: 'error',
hints: false,
maxAssetSize: 200000,
maxEntrypointSize: 400000,
assetFilter: function(assetFilename) {
// 提供资源文件名的断言函数
return assetFilename.endsWith('.css') || assetFilename.endsWith('.js')
}
}
},
externals: cdn.externals
},
chainWebpack(config) {
// it can improve the speed of the first screen, it is recommended to turn on preload
config.plugin('html').tap(args => {
args[0].cdn = cdn
return args
})
config.plugin('preload').tap(() => [
{
rel: 'preload',
@ -72,10 +98,7 @@ module.exports = {
}
])
// when there are many pages, it will cause too many meaningless requests
config.plugins.delete('prefetch')
// set svg-sprite-loader
config.module
.rule('svg')
.exclude.add(resolve('src/icons'))

View File

@ -17,3 +17,9 @@ dotnet_diagnostic.CS8602.severity = none
# CS8600: 将 null 文本或可能的 null 值转换为不可为 null 类型。
dotnet_diagnostic.CS8600.severity = none
# IDE0037: 使用推断的成员名称
dotnet_diagnostic.IDE0037.severity = none
# IDE0008: 使用显式类型
dotnet_diagnostic.IDE0008.severity = none

View File

@ -1,19 +1,13 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Security.Cryptography;
using System.Text;
using System.Threading.Tasks;
namespace Inotify.Common
{
public static class Extensions
{
/// <summary>
/// MD5加密字符串32位大写
/// </summary>
/// <param name="source">源字符串</param>
/// <returns>加密后的字符串</returns>
private static int rep = 0;
public static string ToMd5(this string source)
{
MD5CryptoServiceProvider md5 = new MD5CryptoServiceProvider();
@ -22,5 +16,68 @@ namespace Inotify.Common
return result.Replace("-", "");
}
public static string UrlEncode(this string str)
{
string urlStr = System.Web.HttpUtility.UrlEncode(str);
return urlStr;
}
public static string Base64Encode(this string source)
{
if (string.IsNullOrEmpty(source))
{
return "";
}
byte[] bytes = (Encoding.UTF8.GetBytes(source));
return Convert.ToBase64String(bytes);
}
public static string Base64Decode(this string source)
{
if (string.IsNullOrEmpty(source))
{
return "";
}
var bytes = Convert.FromBase64String(source);
return System.Text.Encoding.Default.GetString(bytes);
}
public static string GenerateCheckCode(this int codeCount)
{
string str = string.Empty;
long num2 = DateTime.Now.Ticks + rep;
rep++;
Random random = new Random(((int)(((ulong)num2) & 0xffffffffL)) | ((int)(num2 >> rep)));
for (int i = 0; i < codeCount; i++)
{
char ch;
int num = random.Next();
if ((num % 2) == 0)
{
ch = (char)(0x30 + ((ushort)(num % 10)));
}
else
{
ch = (char)(0x41 + ((ushort)(num % 0x1a)));
}
str += ch.ToString();
}
return str;
}
public static long ToUTC(this DateTime time)
{
TimeSpan ts = time - new DateTime(1970, 1, 1, 0, 0, 0, 0, DateTimeKind.Utc);
return Convert.ToInt64(ts.TotalMilliseconds);
}
public static long ToUnix(this DateTime time)
{
var expiration = time.ToUniversalTime() - new DateTime(1970, 1, 1, 0, 0, 0, 0, DateTimeKind.Utc);
return (long)expiration.TotalSeconds;
}
}
}

115
Inotify/Common/XmlHelper.cs Normal file
View File

@ -0,0 +1,115 @@
using System;
using System.Collections.Generic;
using System.IO;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
using System.Xml;
using System.Xml.Serialization;
namespace Inotify.Common
{
public static class XmlHelper
{
private static void XmlSerializeInternal(Stream stream, object o, Encoding encoding)
{
if (o == null)
throw new ArgumentNullException("o");
if (encoding == null)
throw new ArgumentNullException("encoding");
XmlSerializer serializer = new XmlSerializer(o.GetType());
XmlWriterSettings settings = new XmlWriterSettings();
settings.Indent = true;
settings.NewLineChars = "\r\n";
settings.Encoding = encoding;
settings.IndentChars = " ";
using (XmlWriter writer = XmlWriter.Create(stream, settings))
{
serializer.Serialize(writer, o);
writer.Close();
}
}
/// <summary>
/// 将一个对象序列化为XML字符串
/// </summary>
/// <param name="o">要序列化的对象</param>
/// <param name="encoding">编码方式</param>
/// <returns>序列化产生的XML字符串</returns>
public static string XmlSerialize(object o, Encoding encoding)
{
using (MemoryStream stream = new MemoryStream())
{
XmlSerializeInternal(stream, o, encoding);
stream.Position = 0;
using (StreamReader reader = new StreamReader(stream, encoding))
{
return reader.ReadToEnd();
}
}
}
/// <summary>
/// 将一个对象按XML序列化的方式写入到一个文件
/// </summary>
/// <param name="o">要序列化的对象</param>
/// <param name="path">保存文件路径</param>
/// <param name="encoding">编码方式</param>
public static void XmlSerializeToFile(object o, string path, Encoding encoding)
{
if (string.IsNullOrEmpty(path))
throw new ArgumentNullException("path");
using (FileStream file = new FileStream(path, FileMode.Create, FileAccess.Write))
{
XmlSerializeInternal(file, o, encoding);
}
}
/// <summary>
/// 从XML字符串中反序列化对象
/// </summary>
/// <typeparam name="T">结果对象类型</typeparam>
/// <param name="s">包含对象的XML字符串</param>
/// <param name="encoding">编码方式</param>
/// <returns>反序列化得到的对象</returns>
public static T XmlDeserialize<T>(string s, Encoding encoding)
{
if (string.IsNullOrEmpty(s))
throw new ArgumentNullException("s");
if (encoding == null)
throw new ArgumentNullException("encoding");
XmlSerializer mySerializer = new XmlSerializer(typeof(T));
using (MemoryStream ms = new MemoryStream(encoding.GetBytes(s)))
{
using (StreamReader sr = new StreamReader(ms, encoding))
{
return (T)mySerializer.Deserialize(sr);
}
}
}
/// <summary>
/// 读入一个文件并按XML的方式反序列化对象。
/// </summary>
/// <typeparam name="T">结果对象类型</typeparam>
/// <param name="path">文件路径</param>
/// <param name="encoding">编码方式</param>
/// <returns>反序列化得到的对象</returns>
public static T XmlDeserializeFromFile<T>(string path, Encoding encoding)
{
if (string.IsNullOrEmpty(path))
throw new ArgumentNullException("path");
if (encoding == null)
throw new ArgumentNullException("encoding");
string xml = File.ReadAllText(path, encoding);
return XmlDeserialize<T>(xml, encoding);
}
}
}

View File

@ -0,0 +1,133 @@
using Inotify.Common;
using Inotify.Data;
using Inotify.Data.Models;
using Inotify.Sends;
using Inotify.Sends.Products;
using Microsoft.AspNetCore.Mvc;
using Newtonsoft.Json;
using System;
using System.Linq;
using System.Runtime.InteropServices;
namespace Inotify.Controllers
{
[ApiController]
[Route("/")]
public class BarkControllor : BaseController
{
[HttpGet, Route("Ping")]
public JsonResult Ping()
{
return Me("pong");
}
[HttpGet, Route("Info")]
public JsonResult Info()
{
var dateTime = System.IO.File.GetLastWriteTime(GetType().Assembly.Location);
var devices = DBManager.Instance.DBase.Query<SendAuthInfo>().Count();
return Json(new
{
version = "v2.0.1",
build = dateTime.ToString("yyyy-MM-dd HH:mm:ss"),
arch = RuntimeInformation.OSDescription,
commit = System.Reflection.Assembly.GetExecutingAssembly().GetName().Version.ToString(),
devices
});
}
[HttpGet, Route("Healthz")]
public string Healthz()
{
return "ok";
}
[HttpGet, Route("Register")]
public JsonResult Register(string? act, string? key, string? devicetoken, string? device_key)
{
return !string.IsNullOrEmpty(device_key) ? Register(device_key) : Register(act, key, devicetoken);
}
[HttpPost, Route("Register")]
public JsonResult Register(string? act, string? device_key, string? device_token)
{
if (string.IsNullOrEmpty(act))
{
return Fail(400, "request bind failed : act is empty");
}
if (string.IsNullOrEmpty(device_token))
{
return Fail(400, "request bind failed : device_token is empty");
}
var userInfo = DBManager.Instance.DBase.Query<SendUserInfo>().FirstOrDefault(e => e.Token == act);
if (userInfo == null)
{
return Fail(400, "request bind failed : act is not registered");
}
else
{
BarkAuth barkAuth = null;
SendAuthInfo barkSendAuthInfo = null;
var barkTemplateAttribute = typeof(BarkSendTemplate).GetCustomAttributes(typeof(SendMethodKeyAttribute), false).OfType<SendMethodKeyAttribute>().First();
if (!string.IsNullOrEmpty(device_key))
{
barkSendAuthInfo = DBManager.Instance.DBase.Query<SendAuthInfo>().FirstOrDefault(e => e.Key == device_key);
if (barkSendAuthInfo != null)
{
barkAuth = JsonConvert.DeserializeObject<BarkAuth>(barkSendAuthInfo.AuthData);
barkAuth.DeviceToken = device_token;
barkSendAuthInfo.AuthData = JsonConvert.SerializeObject(barkAuth);
barkSendAuthInfo.ModifyTime = DateTime.Now;
DBManager.Instance.DBase.Update(barkSendAuthInfo);
}
}
if (barkSendAuthInfo == null)
{
if(string.IsNullOrEmpty(device_key))
device_key = Guid.NewGuid().ToString("N").ToUpper();
barkAuth = new BarkAuth() { DeviceKey = device_key, DeviceToken = device_token, IsArchive = "1", AutoMaticallyCopy = "1", Sound = "1107" };
barkSendAuthInfo = new SendAuthInfo()
{
Name = barkTemplateAttribute.Name,
SendMethodTemplate = barkTemplateAttribute.Key,
Key = device_key,
AuthData = JsonConvert.SerializeObject(barkAuth),
UserId = userInfo.Id,
CreateTime = DateTime.Now,
ModifyTime = DateTime.Now,
Active = true,
};
DBManager.Instance.DBase.Insert(barkSendAuthInfo);
}
return Json(new
{
key = device_key,
device_key = device_key,
device_token = device_token
});
}
}
[HttpGet, Route("RegisterCheck")]
public JsonResult Register(string device_key)
{
if (string.IsNullOrEmpty(device_key))
{
return Fail(400, "device key is empty");
}
if (!DBManager.Instance.DBase.Query<SendAuthInfo>().Any(e => e.Key == device_key))
{
return Fail(400, "device not registered");
}
return OK();
}
}
}

View File

@ -1,9 +1,11 @@
using Microsoft.AspNetCore.Mvc;
using Inotify.Common;
using Microsoft.AspNetCore.Http;
using Microsoft.AspNetCore.Mvc;
using System;
using System.Collections.Generic;
using System.IO;
using System.Linq;
using System.Security.Claims;
using System.Threading.Tasks;
using System.Xml;
namespace Inotify.Controllers
{
@ -64,30 +66,54 @@ namespace Inotify.Controllers
}
}
protected JsonResult OK(object? obj = null)
protected JsonResult OK()
{
return new JsonResult(new
return Json(new
{
code = 200,
data = obj ?? "sucess"
message = "sucess",
timestamp = DateTime.Now.ToUnix()
});
}
protected JsonResult OK(object obj)
{
return Json(new
{
code = 200,
message = "sucess",
data = obj ?? "",
timestamp = DateTime.Now.ToUnix()
});
}
protected JsonResult Me(string message)
{
return Json(new
{
code = 200,
message = message,
timestamp = DateTime.Now.ToUnix()
});
}
protected JsonResult Fail()
{
return new JsonResult(new
return Json(new
{
code = 404,
data = "fail"
message = "failed",
timestamp = DateTime.Now.ToUnix()
});
}
protected JsonResult Fail(int code)
{
return new JsonResult(new
return Json(new
{
code,
data = "fail"
code = code,
message = "failed",
timestamp = DateTime.Now.ToUnix()
});
}
@ -95,12 +121,48 @@ namespace Inotify.Controllers
{
return new JsonResult(new
{
code,
message
code = code,
message = message,
timestamp = DateTime.Now.ToUnix()
});
}
protected JsonResult Json(object obj)
{
return new JsonResult(obj);
}
protected string GetPostParams(HttpContext context)
{
string param = string.Empty;
if (context.Request.Method.ToLower().Equals("post"))
{
param += "[post]";
foreach (var key in context.Request.Form.Keys.ToList())
{
param += key + ":" + context.Request.Form[key].ToString();
}
}
else if (context.Request.Method.ToLower().Equals("get"))
{
param += "[get]" + context.Request.QueryString.Value;
}
else
{
param += "[" + context.Request.Method + "]";
}
return param;
}
protected string GetPostXML()
{
Stream reqStream = Request.Body;
using (StreamReader reader = new StreamReader(reqStream))
{
return reader.ReadToEnd();
}
}
}
}

View File

@ -5,22 +5,16 @@ using Inotify.Sends;
using Inotify.ThridOauth.Common;
using Inotify.ThridOauth.IService;
using Microsoft.AspNetCore.Authentication;
using Microsoft.AspNetCore.Authentication.Cookies;
using Microsoft.AspNetCore.Authorization;
using Microsoft.AspNetCore.Hosting;
using Microsoft.AspNetCore.Mvc;
using Microsoft.Extensions.Caching.Memory;
using Microsoft.Extensions.Configuration;
using Microsoft.Extensions.Hosting;
using Microsoft.IdentityModel.Tokens;
using Newtonsoft.Json.Linq;
using NPoco;
using System;
using System.IdentityModel.Tokens.Jwt;
using System.Linq;
using System.Security.Claims;
using System.Text;
using System.Threading.Tasks;
namespace Inotify.Controllers
{
@ -47,7 +41,10 @@ namespace Inotify.Controllers
if (userInfo != null)
{
if (!userInfo.Active)
{
return Fail(401, "用户被禁用");
}
if (userInfo.Password == password.ToMd5())
{
var token = GenToken(username);
@ -97,13 +94,19 @@ namespace Inotify.Controllers
string? avtar = null;
string email = "";
if (res.Result.TryGetValue("login", out JToken? jToken))
{
githubUserName = jToken.ToString();
}
if (res.Result.TryGetValue("avatar_url", out jToken))
{
avtar = jToken.ToString();
}
if (res.Result.TryGetValue("email", out jToken))
{
email = jToken.ToString();
}
if (githubUserName != null && avtar != null)
{
@ -114,7 +117,9 @@ namespace Inotify.Controllers
user.Avatar = avtar;
DBManager.Instance.DBase.Update(user);
if (!user.Active)
{
return Fail(401, "用户被禁用");
}
}
else
{

View File

@ -0,0 +1,66 @@
using Inotify.Data;
using Inotify.Sends;
using Microsoft.AspNetCore.Mvc;
namespace Inotify.Controllers
{
[ApiController]
[Route("api")]
public class SendController : BaseController
{
[HttpGet, Route("send")]
public JsonResult Send(string? token, string? title, string? data)
{
if (DBManager.Instance.IsToken(token, out bool hasActive))
{
if (!hasActive)
{
return Fail(400, "you have no tunnel is acitve");
}
var message = new SendMessage()
{
Token = token,
Title = title,
Data = data
};
if (SendTaskManager.Instance.SendMessage(message))
{
return OK();
}
}
else
{
var key = token;
if (DBManager.Instance.IsSendKey(key, out bool isActive, out token))
{
if (!isActive)
{
return Fail(400, $"device:{key} tunnel is not acitve");
}
var message = new SendMessage()
{
Token = token,
Title = title,
Data = data,
Key = key,
};
if (SendTaskManager.Instance.SendMessage(message))
{
return OK();
}
}
else
{
return Fail(400, $"device:{key} is not registered");
}
}
return Fail(400, $"token:{token} is not registered");
}
}
}

View File

@ -1,31 +0,0 @@
using Inotify.Data;
using Inotify.Sends;
using Microsoft.AspNetCore.Mvc;
using Microsoft.Extensions.Caching.Memory;
using System;
namespace Inotify.Controllers
{
[ApiController]
[Route("api")]
public class SendController : BaseController
{
[HttpGet, Route("send")]
public JsonResult Send(string token, string title, string? data)
{
if (DBManager.Instance.IsSendKey(token))
{
var message = new SendMessage()
{
Token = token,
Title = title,
Data = data,
};
if (SendTaskManager.Instance.SendMessage(message))
return OK();
}
return Fail();
}
}
}

View File

@ -1,23 +1,18 @@
using Inotify.Data;
using Inotify.Data.Models;
using Inotify.Sends;
using Microsoft.AspNetCore.Authentication.Cookies;
using Inotify.Sends.Products;
using Microsoft.AspNetCore.Authorization;
using Microsoft.AspNetCore.Mvc;
using Newtonsoft.Json;
using System;
using System.Collections.Generic;
using System.Linq;
using System.Security.Claims;
using System.Text.Json;
using System.Text.Json.Serialization;
using System.Threading.Tasks;
namespace Inotify.Controllers
{
[ApiController]
[Route("api/setting")]
public class SettingControlor : BaseController
public class SettingController : BaseController
{
[HttpGet, Authorize(Policys.SystemOrUsers)]
public JsonResult Index()
@ -38,6 +33,7 @@ namespace Inotify.Controllers
var userInfo = DBManager.Instance.GetUser(UserName);
if (userInfo != null)
{
var barkTemplateAttribute = typeof(BarkSendTemplate).GetCustomAttributes(typeof(SendMethodKeyAttribute), false).OfType<SendMethodKeyAttribute>().First();
var sendAuthInfos = DBManager.Instance.DBase.Query<SendAuthInfo>().Where(e => e.UserId == userInfo.Id).ToArray();
var userSendTemplates = new List<InputTemeplate>();
foreach (var sendAuthInfo in sendAuthInfos)
@ -45,12 +41,17 @@ namespace Inotify.Controllers
var sendTemplate = SendTaskManager.Instance.GetInputTemplate(sendAuthInfo.SendMethodTemplate);
if (sendTemplate != null)
{
sendTemplate.Key = sendAuthInfo.Key;
sendTemplate.SendAuthId = sendAuthInfo.Id;
sendTemplate.Name = sendAuthInfo.Name;
sendTemplate.AuthData = sendAuthInfo.AuthData;
sendTemplate.SendAuthId = sendAuthInfo.Id;
sendTemplate.IsActive = sendAuthInfo.Id == userInfo.SendAuthId;
sendTemplate.IsActive = sendAuthInfo.Active;
sendTemplate.AuthToTemplate(sendAuthInfo.AuthData);
userSendTemplates.Add(sendTemplate);
var bark = sendTemplate.Values.FirstOrDefault(e => e.Name == nameof(BarkSendTemplate.Auth.SendUrl));
if(bark!=null) bark .Value = "";
}
}
@ -68,9 +69,9 @@ namespace Inotify.Controllers
var authInfo = DBManager.Instance.DBase.Query<SendAuthInfo>().FirstOrDefault(e => e.Id == sendAuthId && e.UserId == userInfo.Id);
if (authInfo != null)
{
userInfo.SendAuthId = state ? sendAuthId : -1;
DBManager.Instance.DBase.Update(userInfo);
return OK(userInfo);
authInfo.Active = state;
DBManager.Instance.DBase.Update(authInfo);
return OK(authInfo);
}
}
return Fail();
@ -96,20 +97,30 @@ namespace Inotify.Controllers
public JsonResult AddSendAuth(InputTemeplate inputTemeplate)
{
var userInfo = DBManager.Instance.GetUser(UserName);
if (userInfo != null && inputTemeplate.Key != null && inputTemeplate.Name != null)
if (userInfo != null && inputTemeplate.Type != null && inputTemeplate.Name != null)
{
var authInfo = inputTemeplate.TemplateToAuth();
var sendAuth = new SendAuthInfo()
var barkKey = typeof(BarkSendTemplate).GetCustomAttributes(typeof(SendMethodKeyAttribute), false).OfType<SendMethodKeyAttribute>().First().Key;
if (barkKey == inputTemeplate.Type)
{
UserId = userInfo.Id,
SendMethodTemplate = inputTemeplate.Key,
AuthData = authInfo,
Name = inputTemeplate.Name,
CreateTime = DateTime.Now,
ModifyTime = DateTime.Now,
};
DBManager.Instance.DBase.Insert(sendAuth);
return OK(sendAuth);
return Fail(406, "BARK通道勿手动添加请使用APP添加BARK地址绑定");
}
else
{
var authInfo = inputTemeplate.TemplateToAuth();
var sendAuth = new SendAuthInfo()
{
UserId = userInfo.Id,
SendMethodTemplate = inputTemeplate.Type,
AuthData = authInfo,
Name = inputTemeplate.Name,
Key = Guid.NewGuid().ToString("N").ToUpper(),
CreateTime = DateTime.Now,
ModifyTime = DateTime.Now,
};
DBManager.Instance.DBase.Insert(sendAuth);
return OK(sendAuth);
}
}
return Fail();
}
@ -120,6 +131,7 @@ namespace Inotify.Controllers
var userInfo = DBManager.Instance.GetUser(UserName);
if (userInfo != null)
{
var barkTemplateAttribute = typeof(BarkSendTemplate).GetCustomAttributes(typeof(SendMethodKeyAttribute), false).OfType<SendMethodKeyAttribute>().First();
var oldSendInfo = DBManager.Instance.DBase.Query<SendAuthInfo>().FirstOrDefault(e => e.Id == inputTemeplate.SendAuthId);
if (oldSendInfo != null && inputTemeplate.Name != null)
{
@ -138,7 +150,10 @@ namespace Inotify.Controllers
{
var userInfo = DBManager.Instance.GetUser(UserName);
if (userInfo != null)
{
return OK(userInfo.Token);
}
return Fail();
}

View File

@ -4,17 +4,13 @@ using Inotify.Data.Models.System;
using Inotify.Sends;
using Microsoft.AspNetCore.Authorization;
using Microsoft.AspNetCore.Mvc;
using NPoco;
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
namespace Inotify.Controllers
{
[ApiController]
[Route("api/settingsys")]
public class SetttingSysControlor : BaseController
public class SetttingSysController : BaseController
{
[HttpGet, Route("GetGlobal"), Authorize(Policys.Systems)]
public IActionResult GetGlobal()
@ -30,6 +26,9 @@ namespace Inotify.Controllers
githubClientID = SendCacheStore.GetSystemValue("githubClientID"),
githubClientSecret = SendCacheStore.GetSystemValue("githubClientSecret"),
githubEnable = githubEnable != "" && bool.Parse(githubEnable),
barkKeyId = SendCacheStore.GetSystemValue("barkKeyId"),
barkTeamId = SendCacheStore.GetSystemValue("barkTeamId"),
barkPrivateKey = SendCacheStore.GetSystemValue("barkPrivateKey"),
});
}
@ -41,7 +40,10 @@ namespace Inotify.Controllers
string? proxyenable,
string? githubClientID,
string? githubClientSecret,
string? githubEnable)
string? githubEnable,
string? barkKeyId,
string? barkTeamId,
string? barkPrivateKey)
{
SendCacheStore.SetSystemValue("sendthread", sendthread);
SendCacheStore.SetSystemValue("administrators", administrators);
@ -50,6 +52,9 @@ namespace Inotify.Controllers
SendCacheStore.SetSystemValue("githubClientID", githubClientID);
SendCacheStore.SetSystemValue("githubClientSecret", githubClientSecret);
SendCacheStore.SetSystemValue("githubEnable", githubEnable);
SendCacheStore.SetSystemValue("barkKeyId", barkKeyId);
SendCacheStore.SetSystemValue("barkTeamId", barkTeamId);
SendCacheStore.SetSystemValue("barkPrivateKey", barkPrivateKey);
SendTaskManager.Instance.Stop();
SendTaskManager.Instance.Run();
@ -102,16 +107,20 @@ namespace Inotify.Controllers
public IActionResult GetUsers(string? query, int page, int pageSize)
{
if (query == null)
{
return OK(DBManager.Instance.DBase.Query<SendUserInfo>().ToPage(page, pageSize));
else return OK(DBManager.Instance.DBase.Query<SendUserInfo>().Where(e => e.UserName.Contains(query) || e.Email.Contains(query)).ToPage(page, pageSize));
}
else
{
return OK(DBManager.Instance.DBase.Query<SendUserInfo>().Where(e => e.UserName.Contains(query) || e.Email.Contains(query)).ToPage(page, pageSize));
}
}
[HttpGet, Route("GetSendInfos"), Authorize(Policys.Systems)]
public IActionResult GetSendInfos(string? start, string? end)
{
var templates = SendTaskManager.Instance.GetInputTemeplates();
var sendInfos = DBManager.Instance.DBase.Fetch<SendInfo>();
var sendInfos = DBManager.Instance.DBase.Fetch<SendInfo>().Where(e=>!string.IsNullOrEmpty( e.TemplateID)).ToList();
var sendInfoQuerys = sendInfos.Where(e => int.Parse(e.Date) >= int.Parse(start) && int.Parse(e.Date) <= int.Parse(end)).ToList();
var sendInfoGroups = sendInfoQuerys.GroupBy(e => e.Date).Select(e => new { date = e.Key, count = e.Sum(item => item.Count) }).ToList();
var sendTypeInfoGroups = sendInfoQuerys.GroupBy(e => e.TemplateID).Select(e => new { date = e.Key, count = e.Sum(item => item.Count) }).ToList();

View File

@ -0,0 +1,544 @@
using Inotify.Common;
using Microsoft.AspNetCore.Authorization;
using Microsoft.AspNetCore.Mvc;
using System;
using System.Collections;
using System.Collections.Generic;
using System.IO;
using System.Linq;
using System.Net;
using System.Security.Cryptography;
using System.Text;
using System.Threading.Tasks;
using System.Xml;
using System.Xml.Serialization;
namespace Inotify.Controllers
{
[ApiController]
[Route("api/weixin")]
public class WeiXinCallBackController : BaseController
{
[HttpGet]
public string Get(string msg_signature, string timestamp, string nonce, string echostr)
{
var replyEchoStr = string.Empty;
var WXBizMsgCrypt = new WXBizMsgCrypt("", "", "");
var result = WXBizMsgCrypt.VerifyURL(msg_signature, timestamp, nonce, echostr, ref replyEchoStr);
if (result == 0)
{
return replyEchoStr;
}
return result.ToString();
}
[HttpPost]
public JsonResult Post(string? msg_signature, string? timestamp, string? nonce)
{
try
{
var reqStream = Request.Body;
string postData = "";
using (StreamReader reader = new StreamReader(reqStream))
{
postData = reader.ReadToEnd();
}
var msg = string.Empty;
var WXBizMsgCrypt = new WXBizMsgCrypt("", "", "");
var result = WXBizMsgCrypt.DecryptMsg(msg_signature, timestamp, nonce, postData, ref msg);
if (result == 0)
{
var serializer = new XmlSerializer(typeof(xml));
}
}
catch
{
}
return OK();
}
}
public class xml
{
public string ToUserName { get; set; }
public string FromUserName { get; set; }
public string CreateTime { get; set; }
public string MsgType { get; set; }
public string Content { get; set; }
public string MsgId { get; set; }
public string AgentID { get; set; }
}
class Cryptography
{
public static UInt32 HostToNetworkOrder(UInt32 inval)
{
UInt32 outval = 0;
for (int i = 0; i < 4; i++)
outval = (outval << 8) + ((inval >> (i * 8)) & 255);
return outval;
}
public static Int32 HostToNetworkOrder(Int32 inval)
{
Int32 outval = 0;
for (int i = 0; i < 4; i++)
outval = (outval << 8) + ((inval >> (i * 8)) & 255);
return outval;
}
/// <summary>
/// 解密方法
/// </summary>
/// <param name="Input">密文</param>
/// <param name="EncodingAESKey"></param>
/// <returns></returns>
///
public static string AES_decrypt(String Input, string EncodingAESKey, ref string corpid)
{
byte[] Key;
Key = Convert.FromBase64String(EncodingAESKey + "=");
byte[] Iv = new byte[16];
Array.Copy(Key, Iv, 16);
byte[] btmpMsg = AES_decrypt(Input, Iv, Key);
int len = BitConverter.ToInt32(btmpMsg, 16);
len = IPAddress.NetworkToHostOrder(len);
byte[] bMsg = new byte[len];
byte[] bCorpid = new byte[btmpMsg.Length - 20 - len];
Array.Copy(btmpMsg, 20, bMsg, 0, len);
Array.Copy(btmpMsg, 20 + len, bCorpid, 0, btmpMsg.Length - 20 - len);
string oriMsg = Encoding.UTF8.GetString(bMsg);
corpid = Encoding.UTF8.GetString(bCorpid);
return oriMsg;
}
public static String AES_encrypt(String Input, string EncodingAESKey, string corpid)
{
byte[] Key;
Key = Convert.FromBase64String(EncodingAESKey + "=");
byte[] Iv = new byte[16];
Array.Copy(Key, Iv, 16);
string Randcode = CreateRandCode(16);
byte[] bRand = Encoding.UTF8.GetBytes(Randcode);
byte[] bCorpid = Encoding.UTF8.GetBytes(corpid);
byte[] btmpMsg = Encoding.UTF8.GetBytes(Input);
byte[] bMsgLen = BitConverter.GetBytes(HostToNetworkOrder(btmpMsg.Length));
byte[] bMsg = new byte[bRand.Length + bMsgLen.Length + bCorpid.Length + btmpMsg.Length];
Array.Copy(bRand, bMsg, bRand.Length);
Array.Copy(bMsgLen, 0, bMsg, bRand.Length, bMsgLen.Length);
Array.Copy(btmpMsg, 0, bMsg, bRand.Length + bMsgLen.Length, btmpMsg.Length);
Array.Copy(bCorpid, 0, bMsg, bRand.Length + bMsgLen.Length + btmpMsg.Length, bCorpid.Length);
return AES_encrypt(bMsg, Iv, Key);
}
private static string CreateRandCode(int codeLen)
{
string codeSerial = "2,3,4,5,6,7,a,c,d,e,f,h,i,j,k,m,n,p,r,s,t,A,C,D,E,F,G,H,J,K,M,N,P,Q,R,S,U,V,W,X,Y,Z";
if (codeLen == 0)
{
codeLen = 16;
}
string[] arr = codeSerial.Split(',');
string code = "";
int randValue = -1;
Random rand = new Random(unchecked((int)DateTime.Now.Ticks));
for (int i = 0; i < codeLen; i++)
{
randValue = rand.Next(0, arr.Length - 1);
code += arr[randValue];
}
return code;
}
private static String AES_encrypt(String Input, byte[] Iv, byte[] Key)
{
var aes = new RijndaelManaged();
//秘钥的大小,以位为单位
aes.KeySize = 256;
//支持的块大小
aes.BlockSize = 128;
//填充模式
aes.Padding = PaddingMode.PKCS7;
aes.Mode = CipherMode.CBC;
aes.Key = Key;
aes.IV = Iv;
var encrypt = aes.CreateEncryptor(aes.Key, aes.IV);
byte[] xBuff = null;
using (var ms = new MemoryStream())
{
using (var cs = new CryptoStream(ms, encrypt, CryptoStreamMode.Write))
{
byte[] xXml = Encoding.UTF8.GetBytes(Input);
cs.Write(xXml, 0, xXml.Length);
}
xBuff = ms.ToArray();
}
String Output = Convert.ToBase64String(xBuff);
return Output;
}
private static String AES_encrypt(byte[] Input, byte[] Iv, byte[] Key)
{
var aes = new RijndaelManaged();
//秘钥的大小,以位为单位
aes.KeySize = 256;
//支持的块大小
aes.BlockSize = 128;
//填充模式
//aes.Padding = PaddingMode.PKCS7;
aes.Padding = PaddingMode.None;
aes.Mode = CipherMode.CBC;
aes.Key = Key;
aes.IV = Iv;
var encrypt = aes.CreateEncryptor(aes.Key, aes.IV);
byte[] xBuff = null;
#region PKCS7补位
byte[] msg = new byte[Input.Length + 32 - Input.Length % 32];
Array.Copy(Input, msg, Input.Length);
byte[] pad = KCS7Encoder(Input.Length);
Array.Copy(pad, 0, msg, Input.Length, pad.Length);
#endregion
#region
//ICryptoTransform transform = aes.CreateEncryptor();
//byte[] xBuff = transform.TransformFinalBlock(msg, 0, msg.Length);
#endregion
using (var ms = new MemoryStream())
{
using (var cs = new CryptoStream(ms, encrypt, CryptoStreamMode.Write))
{
cs.Write(msg, 0, msg.Length);
}
xBuff = ms.ToArray();
}
String Output = Convert.ToBase64String(xBuff);
return Output;
}
private static byte[] KCS7Encoder(int text_length)
{
int block_size = 32;
// 计算需要填充的位数
int amount_to_pad = block_size - (text_length % block_size);
if (amount_to_pad == 0)
{
amount_to_pad = block_size;
}
// 获得补位所用的字符
char pad_chr = chr(amount_to_pad);
string tmp = "";
for (int index = 0; index < amount_to_pad; index++)
{
tmp += pad_chr;
}
return Encoding.UTF8.GetBytes(tmp);
}
/**
* ASCII码对应的字符
*
* @param a
* @return
*/
static char chr(int a)
{
byte target = (byte)(a & 0xFF);
return (char)target;
}
private static byte[] AES_decrypt(String Input, byte[] Iv, byte[] Key)
{
RijndaelManaged aes = new RijndaelManaged();
aes.KeySize = 256;
aes.BlockSize = 128;
aes.Mode = CipherMode.CBC;
aes.Padding = PaddingMode.None;
aes.Key = Key;
aes.IV = Iv;
var decrypt = aes.CreateDecryptor(aes.Key, aes.IV);
byte[] xBuff = null;
using (var ms = new MemoryStream())
{
using (var cs = new CryptoStream(ms, decrypt, CryptoStreamMode.Write))
{
byte[] xXml = Convert.FromBase64String(Input);
byte[] msg = new byte[xXml.Length + 32 - xXml.Length % 32];
Array.Copy(xXml, msg, xXml.Length);
cs.Write(xXml, 0, xXml.Length);
}
xBuff = decode2(ms.ToArray());
}
return xBuff;
}
private static byte[] decode2(byte[] decrypted)
{
int pad = (int)decrypted[decrypted.Length - 1];
if (pad < 1 || pad > 32)
{
pad = 0;
}
byte[] res = new byte[decrypted.Length - pad];
Array.Copy(decrypted, 0, res, 0, decrypted.Length - pad);
return res;
}
}
class WXBizMsgCrypt
{
string m_sToken;
string m_sEncodingAESKey;
string m_sReceiveId;
enum WXBizMsgCryptErrorCode
{
WXBizMsgCrypt_OK = 0,
WXBizMsgCrypt_ValidateSignature_Error = -40001,
WXBizMsgCrypt_ParseXml_Error = -40002,
WXBizMsgCrypt_ComputeSignature_Error = -40003,
WXBizMsgCrypt_IllegalAesKey = -40004,
WXBizMsgCrypt_ValidateCorpid_Error = -40005,
WXBizMsgCrypt_EncryptAES_Error = -40006,
WXBizMsgCrypt_DecryptAES_Error = -40007,
WXBizMsgCrypt_IllegalBuffer = -40008,
WXBizMsgCrypt_EncodeBase64_Error = -40009,
WXBizMsgCrypt_DecodeBase64_Error = -40010
};
//构造函数
// @param sToken: 企业微信后台开发者设置的Token
// @param sEncodingAESKey: 企业微信后台开发者设置的EncodingAESKey
// @param sReceiveId: 不同场景含义不同,详见文档说明
public WXBizMsgCrypt(string sToken, string sEncodingAESKey, string sReceiveId)
{
m_sToken = sToken;
m_sReceiveId = sReceiveId;
m_sEncodingAESKey = sEncodingAESKey;
}
//验证URL
// @param sMsgSignature: 签名串对应URL参数的msg_signature
// @param sTimeStamp: 时间戳对应URL参数的timestamp
// @param sNonce: 随机串对应URL参数的nonce
// @param sEchoStr: 随机串对应URL参数的echostr
// @param sReplyEchoStr: 解密之后的echostr当return返回0时有效
// @return成功0失败返回对应的错误码
public int VerifyURL(string sMsgSignature, string sTimeStamp, string sNonce, string sEchoStr, ref string sReplyEchoStr)
{
int ret = 0;
if (m_sEncodingAESKey.Length != 43)
{
return (int)WXBizMsgCryptErrorCode.WXBizMsgCrypt_IllegalAesKey;
}
ret = VerifySignature(m_sToken, sTimeStamp, sNonce, sEchoStr, sMsgSignature);
if (0 != ret)
{
return ret;
}
sReplyEchoStr = "";
string cpid = "";
try
{
sReplyEchoStr = Cryptography.AES_decrypt(sEchoStr, m_sEncodingAESKey, ref cpid); //m_sReceiveId);
}
catch (Exception)
{
sReplyEchoStr = "";
return (int)WXBizMsgCryptErrorCode.WXBizMsgCrypt_DecryptAES_Error;
}
if (cpid != m_sReceiveId)
{
sReplyEchoStr = "";
return (int)WXBizMsgCryptErrorCode.WXBizMsgCrypt_ValidateCorpid_Error;
}
return 0;
}
// 检验消息的真实性,并且获取解密后的明文
// @param sMsgSignature: 签名串对应URL参数的msg_signature
// @param sTimeStamp: 时间戳对应URL参数的timestamp
// @param sNonce: 随机串对应URL参数的nonce
// @param sPostData: 密文对应POST请求的数据
// @param sMsg: 解密后的原文当return返回0时有效
// @return: 成功0失败返回对应的错误码
public int DecryptMsg(string sMsgSignature, string sTimeStamp, string sNonce, string sPostData, ref string sMsg)
{
if (m_sEncodingAESKey.Length != 43)
{
return (int)WXBizMsgCryptErrorCode.WXBizMsgCrypt_IllegalAesKey;
}
XmlDocument doc = new XmlDocument();
XmlNode root;
string sEncryptMsg;
try
{
doc.LoadXml(sPostData);
root = doc.FirstChild;
sEncryptMsg = root["Encrypt"].InnerText;
}
catch (Exception)
{
return (int)WXBizMsgCryptErrorCode.WXBizMsgCrypt_ParseXml_Error;
}
//verify signature
int ret = 0;
ret = VerifySignature(m_sToken, sTimeStamp, sNonce, sEncryptMsg, sMsgSignature);
if (ret != 0)
return ret;
//decrypt
string cpid = "";
try
{
sMsg = Cryptography.AES_decrypt(sEncryptMsg, m_sEncodingAESKey, ref cpid);
}
catch (FormatException)
{
sMsg = "";
return (int)WXBizMsgCryptErrorCode.WXBizMsgCrypt_DecodeBase64_Error;
}
catch (Exception)
{
sMsg = "";
return (int)WXBizMsgCryptErrorCode.WXBizMsgCrypt_DecryptAES_Error;
}
if (cpid != m_sReceiveId)
return (int)WXBizMsgCryptErrorCode.WXBizMsgCrypt_ValidateCorpid_Error;
return 0;
}
//将企业号回复用户的消息加密打包
// @param sReplyMsg: 企业号待回复用户的消息xml格式的字符串
// @param sTimeStamp: 时间戳可以自己生成也可以用URL参数的timestamp
// @param sNonce: 随机串可以自己生成也可以用URL参数的nonce
// @param sEncryptMsg: 加密后的可以直接回复用户的密文包括msg_signature, timestamp, nonce, encrypt的xml格式的字符串,
// 当return返回0时有效
// return成功0失败返回对应的错误码
public int EncryptMsg(string sReplyMsg, string sTimeStamp, string sNonce, ref string sEncryptMsg)
{
if (m_sEncodingAESKey.Length != 43)
{
return (int)WXBizMsgCryptErrorCode.WXBizMsgCrypt_IllegalAesKey;
}
string raw = "";
try
{
raw = Cryptography.AES_encrypt(sReplyMsg, m_sEncodingAESKey, m_sReceiveId);
}
catch (Exception)
{
return (int)WXBizMsgCryptErrorCode.WXBizMsgCrypt_EncryptAES_Error;
}
string MsgSigature = "";
int ret = 0;
ret = GenarateSinature(m_sToken, sTimeStamp, sNonce, raw, ref MsgSigature);
if (0 != ret)
return ret;
sEncryptMsg = "";
string EncryptLabelHead = "<Encrypt><![CDATA[";
string EncryptLabelTail = "]]></Encrypt>";
string MsgSigLabelHead = "<MsgSignature><![CDATA[";
string MsgSigLabelTail = "]]></MsgSignature>";
string TimeStampLabelHead = "<TimeStamp><![CDATA[";
string TimeStampLabelTail = "]]></TimeStamp>";
string NonceLabelHead = "<Nonce><![CDATA[";
string NonceLabelTail = "]]></Nonce>";
sEncryptMsg = sEncryptMsg + "<xml>" + EncryptLabelHead + raw + EncryptLabelTail;
sEncryptMsg = sEncryptMsg + MsgSigLabelHead + MsgSigature + MsgSigLabelTail;
sEncryptMsg = sEncryptMsg + TimeStampLabelHead + sTimeStamp + TimeStampLabelTail;
sEncryptMsg = sEncryptMsg + NonceLabelHead + sNonce + NonceLabelTail;
sEncryptMsg += "</xml>";
return 0;
}
public class DictionarySort : System.Collections.IComparer
{
public int Compare(object oLeft, object oRight)
{
string sLeft = oLeft as string;
string sRight = oRight as string;
int iLeftLength = sLeft.Length;
int iRightLength = sRight.Length;
int index = 0;
while (index < iLeftLength && index < iRightLength)
{
if (sLeft[index] < sRight[index])
return -1;
else if (sLeft[index] > sRight[index])
return 1;
else
index++;
}
return iLeftLength - iRightLength;
}
}
//Verify Signature
private static int VerifySignature(string sToken, string sTimeStamp, string sNonce, string sMsgEncrypt, string sSigture)
{
string hash = "";
int ret = 0;
ret = GenarateSinature(sToken, sTimeStamp, sNonce, sMsgEncrypt, ref hash);
if (ret != 0)
return ret;
if (hash == sSigture)
return 0;
else
{
return (int)WXBizMsgCryptErrorCode.WXBizMsgCrypt_ValidateSignature_Error;
}
}
public static int GenarateSinature(string sToken, string sTimeStamp, string sNonce, string sMsgEncrypt, ref string sMsgSignature)
{
ArrayList AL = new ArrayList();
AL.Add(sToken);
AL.Add(sTimeStamp);
AL.Add(sNonce);
AL.Add(sMsgEncrypt);
AL.Sort(new DictionarySort());
string raw = "";
for (int i = 0; i < AL.Count; ++i)
{
raw += AL[i];
}
SHA1 sha;
ASCIIEncoding enc;
string hash = "";
try
{
sha = new SHA1CryptoServiceProvider();
enc = new ASCIIEncoding();
byte[] dataToHash = enc.GetBytes(raw);
byte[] dataHashed = sha.ComputeHash(dataToHash);
hash = BitConverter.ToString(dataHashed).Replace("-", "");
hash = hash.ToLower();
}
catch (Exception)
{
return (int)WXBizMsgCryptErrorCode.WXBizMsgCrypt_ComputeSignature_Error;
}
sMsgSignature = hash;
return 0;
}
}
}

View File

@ -1,23 +1,21 @@
using Inotify.Common;
using Inotify.Data.Models;
using Inotify.Data.Models;
using Inotify.Data.Models.System;
using Inotify.Sends.Products;
using Microsoft.Data.Sqlite;
using Microsoft.IdentityModel;
using Newtonsoft.Json;
using NPoco;
using NPoco.Migrations;
using NPoco.Migrations.CurrentVersion;
using System;
using System.Data;
using System.IO;
using System.Net.Mail;
using System.Runtime.InteropServices;
using System.Security.Cryptography;
namespace Inotify.Data
{
public class DBManager
{
private readonly string MigrationName = "inotify";
private readonly SqliteConnection m_dbConnection;
private readonly Migrator m_migrator;
@ -32,7 +30,7 @@ namespace Inotify.Data
public JwtInfo JWT
{
get { return m_JWT; }
get => m_JWT;
set
{
m_JWT = value;
@ -40,6 +38,8 @@ namespace Inotify.Data
}
}
public readonly string Inotify_Data;
public Database DBase { get; private set; }
private static DBManager? m_Instance;
@ -48,7 +48,11 @@ namespace Inotify.Data
{
get
{
if (m_Instance == null) m_Instance = new DBManager();
if (m_Instance == null)
{
m_Instance = new DBManager();
}
return m_Instance;
}
}
@ -58,13 +62,24 @@ namespace Inotify.Data
if (RuntimeInformation.IsOSPlatform(OSPlatform.Linux))
{
Inotify_Data = Path.Combine(Directory.GetCurrentDirectory(), m_dataPath);
if (!Directory.Exists(Inotify_Data))
{
Directory.CreateDirectory(Inotify_Data);
}
m_jwtPath = Path.Combine(Directory.GetCurrentDirectory(), "/" + m_dataPath + "/jwt.json");
m_dbPath = Path.Combine(Directory.GetCurrentDirectory(), "/" + m_dataPath + "/data.db");
}
if (RuntimeInformation.IsOSPlatform(OSPlatform.Windows))
{
var dataPath = Path.Combine(Directory.GetCurrentDirectory(), m_dataPath);
if (!Directory.Exists(dataPath)) Directory.CreateDirectory(dataPath);
Inotify_Data = Path.Combine(Directory.GetCurrentDirectory(), m_dataPath);
if (!Directory.Exists(Inotify_Data))
{
Directory.CreateDirectory(Inotify_Data);
}
m_jwtPath = Path.Combine(Directory.GetCurrentDirectory(), m_dataPath + "/jwt.json");
m_dbPath = Path.Combine(Directory.GetCurrentDirectory(), m_dataPath + "/data.db");
@ -95,16 +110,48 @@ namespace Inotify.Data
m_dbConnection = new SqliteConnection(string.Format("Data Source={0}", m_dbPath));
if (m_dbConnection.State == ConnectionState.Closed)
m_dbConnection.Open();
DBase = new NPoco.Database(m_dbConnection, DatabaseType.SQLite);
if (m_dbConnection.State == ConnectionState.Closed)
{
m_dbConnection.Open();
}
DBase = new Database(m_dbConnection, DatabaseType.SQLite)
{
KeepConnectionAlive = true
};
m_migrator = new Migrator(DBase);
}
public bool IsSendKey(string token)
public bool IsToken(string token, out bool hasActive)
{
return DBase.Query<SendUserInfo>().Any(e => e.Token == token);
hasActive = false;
var userInfo = DBase.Query<SendUserInfo>().FirstOrDefault(e => e.Token == token);
if (userInfo != null)
{
hasActive = DBase.Query<SendAuthInfo>().Any(e => e.UserId == userInfo.Id && e.Active);
return true;
}
return false;
}
public bool IsSendKey(string key, out bool isActive, out string token)
{
isActive = false;
token = null;
var sendAuthInfo = DBase.Query<SendAuthInfo>().FirstOrDefault(e => e.Key == key);
if (sendAuthInfo != null)
{
var userInfo = DBase.Query<SendUserInfo>().FirstOrDefault(e => e.Id == sendAuthInfo.UserId);
if (userInfo != null&& userInfo.Token!=null)
{
token = userInfo.Token;
}
isActive = sendAuthInfo.Active;
return true;
}
return false;
}
public bool IsUser(string userName)
@ -114,59 +161,66 @@ namespace Inotify.Data
public SendUserInfo GetUser(string userName)
{
return DBase.Query<SendUserInfo>().First(e => e.UserName == userName);
return DBase.Query<SendUserInfo>().FirstOrDefault(e => e.UserName == userName);
}
public string GetAuth(string token, out string guid)
public string GetSendAuthInfo(string key, out string guid)
{
guid = string.Empty;
var upToekn = token.ToUpper();
var userInfo = DBManager.Instance.DBase.Query<SendUserInfo>().FirstOrDefault(e => e.Token == upToekn && e.Active);
if (userInfo == null) return null;
var authInfo = DBManager.Instance.DBase.Query<SendAuthInfo>().FirstOrDefault(e => e.Id == userInfo.SendAuthId && e.UserId == userInfo.Id);
guid = null;
var authInfo = DBManager.Instance.DBase.Query<SendAuthInfo>().FirstOrDefault(e => e.Key== key.ToUpper());
if (authInfo == null)
{
return null;
}
guid = authInfo.SendMethodTemplate;
return authInfo.AuthData;
}
public void GetSendAuthInfos(string token, string key, out SendAuthInfo[] sendAuthInfos)
{
sendAuthInfos = null;
var upToekn = token.ToUpper();
var userInfo = DBManager.Instance.DBase.Query<SendUserInfo>().FirstOrDefault(e => e.Token == upToekn && e.Active);
if (userInfo != null)
{
if (string.IsNullOrEmpty(key))
{
sendAuthInfos = DBManager.Instance.DBase.Query<SendAuthInfo>().Where(e => e.UserId == userInfo.Id && e.Active).ToArray();
}
else
{
sendAuthInfos = DBManager.Instance.DBase.Query<SendAuthInfo>().Where(e => e.UserId == userInfo.Id && e.Active && e.Key == key).ToArray();
}
}
}
public void Run()
{
if (!m_migrator.TableExists<SendInfo>())
m_migrator.CreateTable<SendInfo>(true).Execute();
var codeVersion = System.Reflection.Assembly.GetExecutingAssembly().GetName().Version;
var versionProvider = new DatabaseCurrentVersionProvider(DBase);
if (!m_migrator.TableExists<SystemInfo>())
{
m_migrator.CreateTable<SystemInfo>(true).Execute();
DBase.Insert(new SystemInfo()
{
key = "administrators",
Value = "admin"
});
var migrationBuilder = new MigrationBuilder(MigrationName, DBase);
migrationBuilder.Append(new Version(codeVersion.ToString()), new LatestMigration());
migrationBuilder.Execute();
versionProvider.SetMigrationVersion(MigrationName, new Version(codeVersion.ToString()));
}
if (!m_migrator.TableExists<SendUserInfo>())
else
{
m_migrator.CreateTable<SendUserInfo>(true).Execute();
SendUserInfo userInfo = new SendUserInfo()
if (versionProvider.GetMigrationVersion(MigrationName).ToString() == "0.0")
{
Token = "112D77BAD9704FFEAECD716B5678DFBE".ToUpper(),
UserName = "admin",
Email = "admin@qq.com",
CreateTime = DateTime.Now,
Active = true,
Password = "123456".ToMd5()
};
DBase.Insert(userInfo);
}
versionProvider.SetMigrationVersion(MigrationName, new Version(1, 0, 0, 0));
}
if (!m_migrator.TableExists<SendAuthInfo>())
{
m_migrator.CreateTable<SendAuthInfo>(true).Execute();
var builder = new MigrationBuilder(MigrationName, DBase);
builder.Append(new Version(2, 0, 0, 0), new V2UpdateMigration());
builder.Append(new Version(2, 0, 0, 1), new V2001UpdateMigration());
builder.Append(new Version(2, 0, 0, 4), new V2004UpdateMigration());
builder.Execute();
}
}
}

View File

@ -0,0 +1,136 @@
using Inotify.Common;
using Inotify.Data.Models;
using NPoco.Migrations;
using System;
namespace Inotify.Data
{
public class EmptyMigration : Migration, IMigration
{
protected override void execute()
{
}
}
public class LatestMigration : Migration, IMigration
{
protected override void execute()
{
if (!Migrator.TableExists<SystemInfo>())
{
Migrator.CreateTable<SystemInfo>(true).Execute();
Migrator.Database.Insert(new SystemInfo()
{
key = "administrators",
Value = "admin"
});
Migrator.Database.Insert(new SystemInfo()
{
key = "barkKeyId",
Value = "TEg0VDlWNVU0Ug==".Base64Decode(),
});
Migrator.Database.Insert(new SystemInfo()
{
key = "barkTeamId",
Value = "NVU4TEJSWEczQQ==".Base64Decode(),
});
Migrator.Database.Insert(new SystemInfo()
{
key = "barkPrivateKey",
Value = "LS0tLS1CRUdJTiBQUklWQVRFIEtFWS0tLS0tCk1JR1RBZ0VBTUJNR0J5cUdTTTQ5QWdFR0NDcUdTTTQ5QXdFSEJIa3dkd0lCQVFRZzR2dEMzZzVMNUhnS0dKMitUMWVBMHRPaXZSRXZFQVkyZytqdVJYSmtZTDJnQ2dZSUtvWkl6ajBEQVFlaFJBTkNBQVNtT3MzSmtTeW9HRVdac1VHeEZzLzRwdzFySWxTVjJJQzE5TTh1M0c1a3EzNnVwT3d5RldqOUdpM0VqYzlkM3NDNytTSFJxWHJFQUpvdzgvN3RScFYrCi0tLS0tRU5EIFBSSVZBVEUgS0VZLS0tLS0=".Base64Decode()
});
}
if (!Migrator.TableExists<SendInfo>())
{
Migrator.CreateTable<SendInfo>(true).Execute();
}
if (!Migrator.TableExists<SendUserInfo>())
{
Migrator.CreateTable<SendUserInfo>(true).Execute();
SendUserInfo userInfo = new SendUserInfo()
{
Token = Guid.NewGuid().ToString("N").ToUpper(),
UserName = "admin",
Email = "admin@qq.com",
CreateTime = DateTime.Now,
Active = true,
Password = "123456".ToMd5()
};
Migrator.Database.Insert(userInfo);
}
if (!Migrator.TableExists<SendAuthInfo>())
{
Migrator.CreateTable<SendAuthInfo>(true).Execute();
}
}
}
public class V2UpdateMigration : Migration, IMigration
{
protected override void execute()
{
//V2版本允许多通道,激活标记放入SendAuthInfo表中增加Active列同时更新原有用户的激活通道
Migrator.AlterTable<SendAuthInfo>().AddColumn(e => e.Active).Execute();
Migrator.AlterTable<SendAuthInfo>().AddColumn(e => e.Key).Execute();
Migrator.Database.UpdateMany<SendAuthInfo>().OnlyFields(e => e.Active).Execute(new SendAuthInfo() { Active = false });
var activeUsers = Migrator.Database.Query<SendUserInfo>().ToList();
activeUsers.ForEach(user =>
{
var sendUserInfo = Migrator.Database.Query<SendAuthInfo>().FirstOrDefault(e => e.Id == user.SendAuthId);
if (sendUserInfo != null)
{
sendUserInfo.Active = true;
Migrator.Database.Update(sendUserInfo, e => e.Active);
}
});
}
}
public class V2001UpdateMigration : Migration, IMigration
{
protected override void execute()
{
var sendAuthInfos = Migrator.Database.Query<SendAuthInfo>().ToList();
sendAuthInfos.ForEach(sendAuthInfo =>
{
sendAuthInfo.AuthData = sendAuthInfo.AuthDataSave;
Migrator.Database.Update(sendAuthInfo);
});
Migrator.Database.Insert(new SystemInfo()
{
key = "barkKeyId",
Value = "TEg0VDlWNVU0Ug==".Base64Decode(),
});
Migrator.Database.Insert(new SystemInfo()
{
key = "barkTeamId",
Value = "NVU4TEJSWEczQQ==".Base64Decode(),
});
Migrator.Database.Insert(new SystemInfo()
{
key = "barkPrivateKey",
Value = "LS0tLS1CRUdJTiBQUklWQVRFIEtFWS0tLS0tCk1JR1RBZ0VBTUJNR0J5cUdTTTQ5QWdFR0NDcUdTTTQ5QXdFSEJIa3dkd0lCQVFRZzR2dEMzZzVMNUhnS0dKMitUMWVBMHRPaXZSRXZFQVkyZytqdVJYSmtZTDJnQ2dZSUtvWkl6ajBEQVFlaFJBTkNBQVNtT3MzSmtTeW9HRVdac1VHeEZzLzRwdzFySWxTVjJJQzE5TTh1M0c1a3EzNnVwT3d5RldqOUdpM0VqYzlkM3NDNytTSFJxWHJFQUpvdzgvN3RScFYrCi0tLS0tRU5EIFBSSVZBVEUgS0VZLS0tLS0=".Base64Decode()
});
}
}
public class V2004UpdateMigration : Migration, IMigration
{
protected override void execute()
{
var sendAuthInfos = Migrator.Database.Query<SendAuthInfo>().ToList();
sendAuthInfos.ForEach(sendAuthInfo =>
{
if (string.IsNullOrEmpty(sendAuthInfo.Key))
sendAuthInfo.Key = Guid.NewGuid().ToString("N").ToUpper();
Migrator.Database.Update(sendAuthInfo);
});
}
}
}

View File

@ -1,5 +1,5 @@
using System;
using Inotify.Common;
using System;
namespace Inotify.Data.Models
{
[NPoco.TableName("sendAuthInfo")]
@ -19,12 +19,26 @@ namespace Inotify.Data.Models
public string SendMethodTemplate { get; set; }
[NPoco.Column("authData")]
public string AuthData { get; set; }
public string AuthDataSave { get; set; }
[NPoco.Ignore]
public string AuthData
{
get => AuthDataSave.Base64Decode();
set => AuthDataSave = value.Base64Encode();
}
[NPoco.Column("modifyTime")]
public DateTime ModifyTime { get; set; }
[NPoco.Column("createTime")]
public DateTime CreateTime { get; set; }
[NPoco.Column("active")]
public bool Active { get; set; }
[NPoco.Column("key")]
public string Key { get; set; }
}
}

View File

@ -1,9 +1,4 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Threading.Tasks;
namespace Inotify.Data.Models
namespace Inotify.Data.Models
{
[NPoco.TableName("sendInfo")]
[NPoco.PrimaryKey(new string[] { "templateID", "date" }, AutoIncrement = false)]

View File

@ -1,9 +1,4 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Threading.Tasks;
namespace Inotify.Data.Models
namespace Inotify.Data.Models
{
[NPoco.TableName("userInfo")]
[NPoco.PrimaryKey("id")]

View File

@ -1,9 +1,4 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Threading.Tasks;
namespace Inotify.Data.Models.System
namespace Inotify.Data.Models.System
{
public class JwtInfo
{

View File

@ -1,9 +1,4 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Threading.Tasks;
namespace Inotify.Data.Models
namespace Inotify.Data.Models
{
[NPoco.TableName("systemInfo")]

View File

@ -6,28 +6,35 @@
<DockerDefaultTargetOS>Linux</DockerDefaultTargetOS>
<StartupObject>Inotify.Program</StartupObject>
<Nullable>enable</Nullable>
<AssemblyVersion>2.3.0.0</AssemblyVersion>
<FileVersion>2.3.0.0</FileVersion>
</PropertyGroup>
<PropertyGroup Condition="'$(Configuration)|$(Platform)'=='Debug|AnyCPU'">
<WarningLevel>3</WarningLevel>
</PropertyGroup>
<ItemGroup>
<None Include="..\.editorconfig" Link=".editorconfig" />
</ItemGroup>
<ItemGroup>
<PackageReference Include="FluentEmail.Core" Version="3.0.0" />
<PackageReference Include="FluentEmail.Liquid" Version="3.0.0" />
<PackageReference Include="FluentEmail.Smtp" Version="3.0.0" />
<PackageReference Include="Microsoft.AspNetCore.Authentication.JwtBearer" Version="3.1.12" />
<PackageReference Include="Microsoft.AspNetCore.Authentication.JwtBearer" Version="3.1.18" />
<PackageReference Include="Microsoft.Data.Sqlite" Version="5.0.3" />
<PackageReference Include="Microsoft.Extensions.Caching.Memory" Version="5.0.0" />
<PackageReference Include="Microsoft.VisualStudio.Azure.Containers.Tools.Targets" Version="1.10.9" />
<PackageReference Include="Newtonsoft.Json" Version="12.0.3" />
<PackageReference Include="NPoco" Version="5.1.2" />
<PackageReference Include="NPoco.Migrations" Version="0.3.2" />
<PackageReference Include="Portable.BouncyCastle" Version="1.8.10" />
<PackageReference Include="System.Runtime.Caching" Version="5.0.0" />
<PackageReference Include="Telegram.Bot" Version="15.7.1" />
<PackageReference Include="Telegram.Bot" Version="18.0.0" />
</ItemGroup>
<ItemGroup>
<None Update="inotify_data\AuthKey_LH4T9V5U4R_5U8LBRXG3A.p8">
<CopyToOutputDirectory>Always</CopyToOutputDirectory>
</None>
</ItemGroup>
<ProjectExtensions><VisualStudio><UserProperties properties_4launchsettings_1json__JsonSchema="" /></VisualStudio></ProjectExtensions>
</Project>

View File

@ -5,7 +5,7 @@ VisualStudioVersion = 16.0.30804.86
MinimumVisualStudioVersion = 10.0.40219.1
Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Solution Items", "Solution Items", "{80E8FF2E-B685-44E6-82A8-FDCA48EDAD47}"
ProjectSection(SolutionItems) = preProject
..\.editorconfig = ..\.editorconfig
.editorconfig = .editorconfig
EndProjectSection
EndProject
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Inotify", "Inotify.csproj", "{9AF94AE0-E8C6-414C-A0AC-EDFB301CA05E}"

View File

@ -1,16 +1,7 @@
using Inotify.Data;
using Inotify.Sends;
using Microsoft.AspNetCore.Hosting;
using Microsoft.Extensions.Configuration;
using Microsoft.Extensions.Hosting;
using Microsoft.Extensions.Logging;
using Newtonsoft.Json;
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text.RegularExpressions;
using System.Threading.Tasks;
namespace Inotify
{

View File

@ -0,0 +1,125 @@
using CorePush.Apple;
using Newtonsoft.Json;
using System;
using System.Net.Http;
namespace Inotify.Sends.Products
{
public class BarkAuth
{
[InputTypeAttribte(1, "Sound", "声音", "1107")]
public string Sound { get; set; }
[InputTypeAttribte(2, "IsArchive", "自动保存", "1或0")]
public string IsArchive { get; set; }
[InputTypeAttribte(3, "AutoMaticallyCopy", "自动复制", "1或0")]
public string AutoMaticallyCopy { get; set; }
[InputTypeAttribte(4, "DeviceKey", "DeviceKey", "DeviceKey", true, true)]
public string DeviceKey { get; set; }
[InputTypeAttribte(5, "DeviceToken", "DeviceToken", "DeviceToken", true, true)]
public string DeviceToken { get; set; }
[InputTypeAttribte(6, "SendUrl", "SendUrl", "SendUrl", true, true)]
public string SendUrl { get; set; }
}
[SendMethodKey("3B6DE04D-A9EF-4C91-A151-60B7425C5AB2", "Bark", Order = 2999, Waring = "BARK通道勿手动添加请使用APP添加BARK地址绑定")]
public class BarkSendTemplate : SendTemplate<BarkAuth>
{
private static ApnSender apnSender;
public override bool SendMessage(SendMessage message)
{
if (apnSender == null)
{
var keyID = SendCacheStore.GetSystemValue("barkKeyId");
var teamID = SendCacheStore.GetSystemValue("barkTeamId");
var privateKey = SendCacheStore.GetSystemValue("barkPrivateKey");
var privateKeyContent = privateKey.Split('\n')[1];
var apnSettings = new ApnSettings()
{
TeamId = teamID,
AppBundleIdentifier = "me.fin.bark",
P8PrivateKey = privateKeyContent,
ServerType = ApnServerType.Production,
P8PrivateKeyId = keyID,
};
apnSender = new ApnSender(apnSettings, new HttpClient());
}
var payload = new AppleNotification(
Guid.NewGuid(),
message.Data,
message.Title)
{
IsArchive = Auth.IsArchive,
AutoMaticallyCopy=Auth.AutoMaticallyCopy,
};
payload.Aps.Sound = Auth.Sound;
var response = apnSender.Send(payload, Auth.DeviceToken);
if (response.IsSuccess)
return true;
return false;
}
}
public class AppleNotification
{
public class ApsPayload
{
public class Alert
{
[JsonProperty("title")]
public string Title { get; set; }
[JsonProperty("body")]
public string Body { get; set; }
}
[JsonProperty("sound")]
public string Sound { get; set; }
[JsonProperty("alert")]
public Alert AlertBody { get; set; }
[JsonProperty("apns-push-type")]
public string PushType { get; set; } = "alert";
}
public AppleNotification(Guid id, string message, string title = "")
{
Id = id;
Aps = new ApsPayload
{
AlertBody = new ApsPayload.Alert
{
Title = title,
Body = message
}
};
}
[JsonProperty("aps")]
public ApsPayload Aps { get; set; }
[JsonProperty("id")]
public Guid Id { get; set; }
[JsonProperty("isarchive")]
public string IsArchive { get; set; }
[JsonProperty("automaticallycopy")]
public string AutoMaticallyCopy { get; set; }
}
}

View File

@ -0,0 +1,88 @@
using Inotify.Common;
using Newtonsoft.Json;
using System;
using System.IO;
using System.Net;
using System.Security.Cryptography;
using System.Text;
using System.Web;
namespace Inotify.Sends.Products
{
public class DingtalkAuth
{
[InputTypeAttribte(0, "WebHook", "WebHook", "https://oapi.dingtalk.com/robot/send?access_token=xxxxx")]
public string WebHook { get; set; }
[InputTypeAttribte(0, "Secret", "签名校验", "SEC77xxxx")]
public string Secret { get; set; }
}
[SendMethodKey("048297D4-D975-48F6-9A91-8B4EF75805C1", "钉钉群机器人", Order = 21)]
public class DingtalkSendTemplate : SendTemplate<DingtalkAuth>
{
public override bool SendMessage(SendMessage message)
{
var bodyObject = new
{
markdown = new
{
title = $"{message.Title}",
text = $"#### {message.Title}\n{message.Data}",
},
msgtype = "markdown",
};
var timestamp = DateTime.UtcNow.ToUTC();
var sign = GetHmac(timestamp, Auth.Secret);
var url = $"{Auth.WebHook}&timestamp={timestamp}&sign={sign}";
var webRequest = WebRequest.Create(url);
var bytes = Encoding.UTF8.GetBytes(JsonConvert.SerializeObject(bodyObject));
webRequest.Method = "POST";
webRequest.ContentType = "application/json;charset=utf-8";
webRequest.ContentLength = 0;
using (var postStream = webRequest.GetRequestStream())
{
var requestStream = webRequest.GetRequestStream();
webRequest.ContentLength = bytes.Length;
requestStream.Write(bytes, 0, bytes.Length);
}
try
{
var response = webRequest.GetResponse();
using (Stream stream = response.GetResponseStream())
{
using StreamReader reader = new StreamReader(stream, Encoding.UTF8);
var resuleJson = reader.ReadToEnd();
if (resuleJson.Contains("errcode"))
{
return false;
}
}
return true;
}
catch
{
return false;
}
}
private string GetHmac(long timestamp, string secret)
{
var stringToSign = $"{timestamp}\n{secret}";
using var hmacsha256 = new HMACSHA256(Encoding.UTF8.GetBytes(secret));
byte[] hashmessage = hmacsha256.ComputeHash(Encoding.UTF8.GetBytes(stringToSign));
return HttpUtility.UrlEncode(Convert.ToBase64String(hashmessage), Encoding.UTF8);
}
}
}

View File

@ -1,11 +1,7 @@
using FluentEmail.Core;
using FluentEmail.Liquid;
using FluentEmail.Smtp;
using System;
using System.Collections.Generic;
using System.Net;
using System.Net.Mail;
using System.Text;
namespace Inotify.Sends.Products
{
@ -38,8 +34,6 @@ namespace Inotify.Sends.Products
[SendMethodKey("EA2B43F7-956C-4C01-B583-0C943ABB36C3", "邮件推送", Order = 1)]
public class EmailSendTemplate : SendTemplate<EmailAuth>
{
public override EmailAuth Auth { get; set; }
public override bool SendMessage(SendMessage message)
{
var smtpSender = new SmtpSender(new SmtpClient()
@ -52,8 +46,7 @@ namespace Inotify.Sends.Products
Credentials = new NetworkCredential(Auth.From, Auth.Password),
});
var email =
Email.From(Auth.From, Auth.FromName)
var email = Email.From(Auth.From, Auth.FromName)
.Subject(message.Title)
.Body(message.Data ?? "")
.To(Auth.To);

View File

@ -0,0 +1,96 @@
using Inotify.Common;
using Newtonsoft.Json;
using System;
using System.IO;
using System.Net;
using System.Security.Cryptography;
using System.Text;
namespace Inotify.Sends.Products
{
public class FeishuAuth
{
[InputTypeAttribte(0, "WebHook", "WebHook", "https://open.feishu.cn/open-apis/bot/v2/hook/5d7b917e-bfb8-4c7e-ba8c-337xxxx")]
public string WebHook { get; set; }
[InputTypeAttribte(0, "Secret", "签名校验", "VcgAbeuZOhTZPSP0zxxxx")]
public string Secret { get; set; }
}
[SendMethodKey("C01A08B4-3A71-452B-9D4B-D8EC7EF1D68F", "飞书群机器人", Order = 22)]
public class FeishuASendTemplate : SendTemplate<FeishuAuth>
{
public override bool SendMessage(SendMessage message)
{
var timestamp = DateTime.Now.ToUnix() - 10;
var sign = GetHmac(timestamp, Auth.Secret);
var bodyObject = new
{
content = new
{
text = $"{message.Title}\n{message.Data}",
},
msg_type = "text",
sign = sign,
timestamp = timestamp,
};
Console.WriteLine(bodyObject);
var webRequest = WebRequest.Create(Auth.WebHook);
var bytes = Encoding.UTF8.GetBytes(JsonConvert.SerializeObject(bodyObject));
webRequest.Method = "POST";
webRequest.ContentType = "application/json";
webRequest.ContentLength = 0;
using (var postStream = webRequest.GetRequestStream())
{
var requestStream = webRequest.GetRequestStream();
webRequest.ContentLength = bytes.Length;
requestStream.Write(bytes, 0, bytes.Length);
}
try
{
var response = webRequest.GetResponse();
using (Stream stream = response.GetResponseStream())
{
using StreamReader reader = new StreamReader(stream, Encoding.UTF8);
var resuleJson = reader.ReadToEnd();
if (resuleJson.Contains("code"))
{
return false;
}
}
return true;
}
catch
{
return false;
}
}
private string GetHmac(long timestamp, string secret)
{
var stringToSign = $"{timestamp}\n{secret}";
using var hmacsha256 = new HMACSHA256Final(Encoding.UTF8.GetBytes(stringToSign));
return Convert.ToBase64String(hmacsha256.GetHashFinal());
}
}
public class HMACSHA256Final : HMACSHA256
{
public HMACSHA256Final(byte[] bytes) : base(bytes)
{
}
public byte[] GetHashFinal()
{
return base.HashFinal();
}
}
}

View File

@ -0,0 +1,37 @@
using System.Net;
namespace Inotify.Sends.Products
{
public class HttpGetAuth
{
[InputTypeAttribte(0, "URL", "请求地址", "https://api.day.app/token/{title}/{data}")]
public string URL { get; set; }
}
[SendMethodKey("ADB11045-F2C8-457E-BF7E-1698AD37ED53", "自定义GET", Order = 4)]
public class HttpGetTemplate : SendTemplate<HttpGetAuth>
{
public override bool SendMessage(SendMessage message)
{
var url = Auth.URL.Replace("{title}", message.Title).Replace("{data}", message.Data);
var webRequest = WebRequest.Create(url);
if (webRequest != null)
{
try
{
webRequest.GetResponse().GetResponseStream();
return true;
}
catch
{
return false;
}
}
return false;
}
}
}

View File

@ -0,0 +1,74 @@
using System.Net;
using System.Text;
namespace Inotify.Sends.Products
{
public class HttpPostAuth
{
[InputTypeAttribte(0, "URL", "请求地址", "https://api.day.app/token/{title}/{data}")]
public string URL { get; set; }
[InputTypeAttribte(1, "Encoding", "Encoding", "utf-8")]
public string Encoding { get; set; }
[InputTypeAttribte(1, "ContentType", "ContentType", "application/json")]
public string ContentType { get; set; }
[InputTypeAttribte(2, "Data", "POST参数", @"{""msgid"":""123456"",""title"":""{title}"",""data"":""{data}""}")]
public string Data { get; set; }
}
[SendMethodKey("A3C1E614-717E-4CF1-BA9B-7242717FC037", "自定义POST", Order = 5)]
public class HttpPostTemplate : SendTemplate<HttpPostAuth>
{
public override bool SendMessage(SendMessage message)
{
if (Auth.Data == null)
{
Auth.Data = "";
}
if (string.IsNullOrEmpty(Auth.ContentType))
{
Auth.ContentType = "application/json";
}
if (string.IsNullOrEmpty(Auth.Encoding))
{
Auth.Encoding = "utf-8";
}
var url = Auth.URL.Replace("{title}", message.Title).Replace("{data}", message.Data);
var webRequest = WebRequest.Create(url);
if (webRequest != null)
{
var data = Auth.Data.Replace("{title}", message.Title).Replace("{data}", message.Data);
var bytes = Encoding.GetEncoding(Auth.Encoding).GetBytes(data);
webRequest.Method = "POST";
webRequest.ContentType = Auth.ContentType;
webRequest.ContentLength = bytes.Length;
var requestStream = webRequest.GetRequestStream();
requestStream.Write(bytes, 0, bytes.Length);
requestStream.Close();
try
{
webRequest.GetResponse().GetResponseStream();
return true;
}
catch
{
return false;
}
}
return false;
}
}
}

View File

@ -1,10 +1,6 @@

using System;
using System.Collections.Generic;
using System.IO;
using System.Linq;
using System.Net;
using System.Threading.Tasks;
using System.Net.Http;
using Telegram.Bot;
using Telegram.Bot.Types.InputFiles;
@ -26,15 +22,20 @@ namespace Inotify.Sends.Products
public class TelegramBotSendTemplate : SendTemplate<TelegramBotAuth>
{
public override TelegramBotAuth Auth { get; set; }
public override bool SendMessage(SendMessage message)
{
var proxy = GetProxy();
var client = proxy == null ? new TelegramBotClient(Auth.BotToken) : new TelegramBotClient(Auth.BotToken, proxy);
var proxyHttpClientHandler = new HttpClientHandler
{
Proxy = proxy,
UseProxy = true,
};
var httpClient = new HttpClient(proxyHttpClientHandler);
var client = proxy == null ? new TelegramBotClient(Auth.BotToken) : new TelegramBotClient(Auth.BotToken, httpClient);
var content = string.IsNullOrEmpty(message.Title) ? message.Title : message.Title + "\n" + message.Data;
var isIMG = !String.IsNullOrEmpty(message.Title) && IsUrl(message.Title) && IsImage(message.Title) && String.IsNullOrEmpty(message.Data);
var isIMG = !string.IsNullOrEmpty(message.Title) && IsUrl(message.Title) && IsImage(message.Title) && string.IsNullOrEmpty(message.Data);
if (isIMG)
{
client.SendPhotoAsync(Auth.Chat_id, new InputOnlineFile(new Uri(message.Title)));

View File

@ -1,9 +1,6 @@
using Inotify.Common;
using Inotify.Sends;
using Newtonsoft.Json;
using Newtonsoft.Json;
using Newtonsoft.Json.Linq;
using System;
using System.Collections.Generic;
using System.IO;
using System.Net;
using System.Text;
@ -30,64 +27,34 @@ namespace Inotify.Sends.Products
[SendMethodKey("409A30D5-ABE8-4A28-BADD-D04B9908D763", "企业微信", Order = 0)]
public class WeixiSendTemplate : SendTemplate<WeixiAuth>
{
public override WeixiAuth Auth { get; set; }
public override bool SendMessage(SendMessage message)
{
if (Auth == null) return false;
var token = GetAccessToken();
if (token == null) return false;
return PostMail(token, message.Title, message.Data);
}
/// 获取AccessToken
/// </summary>
/// <returns></returns>
private string GetAccessToken()
{
var key = Auth.Corpid + Auth.AgentID + Auth.Corpsecret;
var toekn = SendCacheStore.Get(key);
if (toekn == null)
if (Auth == null)
{
var url = string.Format(@"https://qyapi.weixin.qq.com/cgi-bin/gettoken?corpid={0}&corpsecret={1}", Auth.Corpid, Auth.Corpsecret);
WebRequest request = WebRequest.Create(url);
request.Credentials = CredentialCache.DefaultCredentials;
using WebResponse response = request.GetResponse();
using Stream streamResponse = response.GetResponseStream();
StreamReader reader = new StreamReader(streamResponse);
string responseFromServer = reader.ReadToEnd();
if (!string.IsNullOrEmpty(responseFromServer))
{
if (JsonConvert.DeserializeObject(responseFromServer) is JObject res)
{
if (res.TryGetValue("access_token", out JToken? jtoken))
{
toekn = jtoken.ToString();
}
}
}
reader.Close();
return false;
}
if (toekn != null)
SendCacheStore.Set(key, toekn, DateTimeOffset.Now.AddHours(2));
var token = CreateAcessToken();
if (token == null)
{
return false;
}
return toekn;
return CreatePush(token, message.Title, message.Data);
}
private bool PostMail(string accessToken, string title, string? data)
private bool CreatePush(string accessToken, string title, string? data)
{
var uri = "https://qyapi.weixin.qq.com/cgi-bin/message/send?access_token=" + accessToken;
var content = string.IsNullOrEmpty(data) ? title : title + "\n" + data;
var isImage = !String.IsNullOrEmpty(title) && IsUrl(title) && IsImage(title) && String.IsNullOrEmpty(data);
var isImage = !string.IsNullOrEmpty(title) && IsUrl(title) && IsImage(title) && string.IsNullOrEmpty(data);
var imageData = isImage ? GetImage(title) : null;
string mediaId = null;
string mediaId = string.Empty;
if (imageData != null)
mediaId = UpLoadIMage(accessToken, imageData);
{
mediaId = CreateImage(accessToken, imageData);
}
//创建请求
WebRequest myWebRequest = WebRequest.Create(uri);
@ -148,7 +115,42 @@ namespace Inotify.Sends.Products
return true;
}
private string UpLoadIMage(string accessToken, byte[] bytes)
private string CreateAcessToken()
{
var key = Auth.Corpid + Auth.AgentID + Auth.Corpsecret;
var toekn = SendCacheStore.Get(key);
if (toekn == null)
{
var url = string.Format(@"https://qyapi.weixin.qq.com/cgi-bin/gettoken?corpid={0}&corpsecret={1}", Auth.Corpid, Auth.Corpsecret);
WebRequest request = WebRequest.Create(url);
request.Credentials = CredentialCache.DefaultCredentials;
using WebResponse response = request.GetResponse();
using Stream streamResponse = response.GetResponseStream();
StreamReader reader = new StreamReader(streamResponse);
string responseFromServer = reader.ReadToEnd();
if (!string.IsNullOrEmpty(responseFromServer))
{
if (JsonConvert.DeserializeObject(responseFromServer) is JObject res)
{
if (res.TryGetValue("access_token", out JToken? jtoken))
{
toekn = jtoken.ToString();
}
}
}
reader.Close();
}
if (toekn != null)
{
SendCacheStore.Set(key, toekn, DateTimeOffset.Now.AddHours(2));
}
return toekn;
}
private string CreateImage(string accessToken, byte[] bytes)
{
try
{
@ -171,17 +173,21 @@ namespace Inotify.Sends.Products
postStream.Write(endBoundaryBytes, 0, endBoundaryBytes.Length);
postStream.Close();
HttpWebResponse response = request.GetResponse() as HttpWebResponse;
Stream instream = response.GetResponseStream();
StreamReader sr = new StreamReader(instream, Encoding.UTF8);
string content = sr.ReadToEnd();
var result = JObject.Parse(content);
return result.Value<string>("media_id").ToString();
if (request.GetResponse() is HttpWebResponse response)
{
Stream instream = response.GetResponseStream();
StreamReader sr = new StreamReader(instream, Encoding.UTF8);
string content = sr.ReadToEnd();
var result = JObject.Parse(content);
return result.Value<string>("media_id").ToString();
}
}
catch
{
return null;
}
return null;
}
}
}

View File

@ -1,13 +1,9 @@
using Inotify;
using Inotify.Data;
using Inotify.Data;
using Inotify.Data.Models;
using Inotify.Sends;
using System;
using System.Collections.Generic;
using System.Linq;
using System.Runtime.Caching;
using System.Text;
using System.Web;
namespace Inotify.Sends
{
@ -27,7 +23,10 @@ namespace Inotify.Sends
{
object obj = m_cache.Get(key);
if (obj != null && obj is string)
{
return obj as string;
}
return null;
}
@ -40,7 +39,10 @@ namespace Inotify.Sends
public static string GetSystemValue(string key)
{
if (m_systemInfos.ContainsKey(key))
{
return m_systemInfos[key];
}
return "";
}

View File

@ -1,13 +1,25 @@
using System;
using System.Collections.Generic;
using System.Text;
namespace Inotify.Sends
namespace Inotify.Sends
{
public class SendMessage
{
public SendMessage()
{
}
public SendMessage(SendMessage sendMessage)
{
Token = sendMessage.Token;
Title = sendMessage.Title;
Data = sendMessage.Data;
Key = sendMessage.Key;
}
public string Token;
public string Title;
public string? Data;
public string? Key;
}
}

View File

@ -1,30 +1,27 @@
using Inotify;
using Inotify.Data;
using Inotify.Data;
using Inotify.Data.Models;
using Inotify.Sends;
using Newtonsoft.Json;
using System;
using System.Collections.Concurrent;
using System.Collections.Generic;
using System.IO;
using System.Linq;
using System.Reflection;
using System.Text;
using System.Threading;
namespace Inotify.Sends
{
public class SendTaskManager
{
private static SendTaskManager m_Instance;
public static SendTaskManager Instance
{
get
{
if (m_Instance == null) m_Instance = new SendTaskManager();
if (m_Instance == null)
{
m_Instance = new SendTaskManager();
}
return m_Instance;
}
}
@ -39,7 +36,6 @@ namespace Inotify.Sends
private readonly Dictionary<string, Type> m_sendMethodTemplateTypes;
private SendTaskManager()
{
m_sendMessages = new BlockingCollection<SendMessage>();
@ -50,7 +46,7 @@ namespace Inotify.Sends
var sendMethodTemplates = Assembly.GetExecutingAssembly()
.GetTypes()
.Where(e => e.GetCustomAttribute<SendMethodKeyAttribute>() != null)
.OrderBy(e => e.GetCustomAttribute<SendMethodKeyAttribute>().Order)
.OrderBy(e => e.GetCustomAttribute<SendMethodKeyAttribute>().Order.ToString())
.ToList();
sendMethodTemplates.ForEach(sendMethodTemplate =>
@ -72,7 +68,6 @@ namespace Inotify.Sends
m_analyseThread.Start();
}
public EventHandler<SendMessage> OnMessageAdd;
public EventHandler<SendMessage> OnSendSucessed;
@ -109,7 +104,9 @@ namespace Inotify.Sends
public bool SendMessage(SendMessage message)
{
if (m_sendMessages.Count > 10000)
{
return false;
}
m_sendMessages.Add(message);
@ -127,8 +124,10 @@ namespace Inotify.Sends
var getTemeplateMethod = sendMethodTemplateType.GetMethod("GetTemeplate");
if (getTemeplateMethod != null)
{
if (getTemeplateMethod.Invoke(obj, null) is InputTemeplate temeplate && temeplate.Key != null)
sendTemplates.Add(temeplate.Key, temeplate);
if (getTemeplateMethod.Invoke(obj, null) is InputTemeplate temeplate && temeplate.Type != null)
{
sendTemplates.Add(temeplate.Type, temeplate);
}
}
}
return sendTemplates;
@ -154,42 +153,50 @@ namespace Inotify.Sends
try
{
var message = m_sendMessages.Take();
var authData = DBManager.Instance.GetAuth(message.Token, out string temeplateId);
if (temeplateId != null && authData != null)
DBManager.Instance.GetSendAuthInfos(message.Token, message.Key, out SendAuthInfo[] sendAuthInfos);
foreach (var authInfo in sendAuthInfos)
{
if (m_sendMethodTemplateTypes.ContainsKey(temeplateId))
var authData = authInfo.AuthData;
var temeplateId = authInfo.SendMethodTemplate;
if (temeplateId != null && authData != null)
{
var sendMethodTemplateActor = Activator.CreateInstance(m_sendMethodTemplateTypes[temeplateId]);
if (sendMethodTemplateActor != null)
if (m_sendMethodTemplateTypes.ContainsKey(temeplateId))
{
var sendMethodType = sendMethodTemplateActor.GetType();
var compositonMethod = sendMethodType.GetMethod("Composition");
if (compositonMethod != null)
var sendMethodTemplateActor = Activator.CreateInstance(m_sendMethodTemplateTypes[temeplateId]);
if (sendMethodTemplateActor != null)
{
compositonMethod.Invoke(sendMethodTemplateActor, new object[] { authData });
}
var sendMessageMethod = sendMethodType.GetMethod("SendMessage");
if (sendMessageMethod != null)
{
var result = sendMessageMethod.Invoke(sendMethodTemplateActor, new object[] { message });
if (result != null)
var sendMethodType = sendMethodTemplateActor.GetType();
var compositonMethod = sendMethodType.GetMethod("Composition");
if (compositonMethod != null)
{
m_analyseMessages.Add(message);
if ((bool)result)
{
OnSendSucessed?.Invoke(this, message);
continue;
}
compositonMethod.Invoke(sendMethodTemplateActor, new object[] { authData });
}
var sendMessageMethod = sendMethodType.GetMethod("SendMessage");
if (sendMessageMethod != null)
{
var result = sendMessageMethod.Invoke(sendMethodTemplateActor, new object[] { message });
if (result != null)
{
var logMessage = new SendMessage(message);
logMessage.Key = authInfo.Key;
m_analyseMessages.Add(logMessage);
if ((bool)result)
{
OnSendSucessed?.Invoke(this, logMessage);
continue;
}
else
{
OnSendFailed?.Invoke(this, logMessage);
}
}
}
}
}
}
}
OnSendFailed?.Invoke(this, message);
}
catch (ThreadInterruptedException)
{
@ -208,7 +215,7 @@ namespace Inotify.Sends
{
var message = m_analyseMessages.Take();
var date = DateTime.Now.ToString("yyyyMMdd");
var authData = DBManager.Instance.GetAuth(message.Token, out string temeplateId);
var authData = DBManager.Instance.GetSendAuthInfo(message.Key, out string temeplateId);
if (temeplateId != null)
{

View File

@ -1,14 +1,10 @@
using Inotify.Sends;
using Newtonsoft.Json;
using Newtonsoft.Json;
using Newtonsoft.Json.Linq;
using System;
using System.Collections.Generic;
using System.IO;
using System.Linq;
using System.Net;
using System.Net.Mail;
using System.Reflection;
using System.Text;
using System.Text.RegularExpressions;
namespace Inotify.Sends
@ -31,30 +27,47 @@ namespace Inotify.Sends
public int Order;
public SendMethodKeyAttribute(string key, string name, bool open = true)
public string Waring;
public SendMethodKeyAttribute(string key, string name, bool open = true, string waring = "")
{
Key = key;
Name = name;
Open = open;
Waring = waring;
}
}
public class InputTypeValue
{
public InputTypeValue()
{
Show = true;
Readonly = false;
}
public string? Name { get; set; }
public string? Description { get; set; }
public string? Default { get; set; }
public InputType? Type { get; set; }
public int? Order { get; set; }
public string? Value { get; set; }
public string? Warning { get; set; }
public bool Show { get; set; }
public bool Readonly { get; set; }
}
public class InputTemeplate
{
public string? Key { get; set; }
public string? Type { get; set; }
public string? TypeName { get; set; }
public string? Key { get; set; }
public string? Name { get; set; }
public bool IsActive { get; set; }
public string Warning { get; set; }
public string? AuthData { get; set; }
public int? SendAuthId { get; set; }
public List<InputTypeValue>? Values { get; set; }
@ -70,8 +83,13 @@ namespace Inotify.Sends
if (item.Name != null && item.Value != null)
{
if (item.Type == InputType.CHECK)
{
jObject.Add(item.Name, item.Value.ToLower() == "true");
else jObject.Add(item.Name, item.Value);
}
else
{
jObject.Add(item.Name, item.Value);
}
}
}
}
@ -109,7 +127,8 @@ namespace Inotify.Sends
{
public InputTypeValue InputTypeData { get; set; }
private InputTypeAttribte(int order, string name, string description, string defaultValue, InputType type)
private InputTypeAttribte(int order, string name, string description, string defaultValue, InputType type, bool show = true, bool readOnly = false)
{
InputTypeData = new InputTypeValue
{
@ -117,18 +136,20 @@ namespace Inotify.Sends
Description = description,
Default = defaultValue,
Order = order,
Type = type
Type = type,
Show = show,
Readonly = readOnly
};
}
public InputTypeAttribte(int order, string name, string description, string defaultValue)
: this(order, name, description, defaultValue, InputType.TEXT)
public InputTypeAttribte(int order, string name, string description, string defaultValue, bool show = true, bool readOnly = false)
: this(order, name, description, defaultValue, InputType.TEXT, show, readOnly)
{
}
public InputTypeAttribte(int order, string name, string description, bool defaultValue)
: this(order, name, description, "", InputType.CHECK)
public InputTypeAttribte(int order, string name, string description, bool defaultValue, bool show = true, bool readOnly = false)
: this(order, name, description, "", InputType.CHECK, show, readOnly)
{
InputTypeData.Default = defaultValue ? "是" : "否";
}
@ -137,8 +158,7 @@ namespace Inotify.Sends
public abstract class SendTemplate<T>
{
public abstract T Auth { get; set; }
public T Auth { get; set; }
public void Composition(string authInfo)
{
@ -154,15 +174,17 @@ namespace Inotify.Sends
.Select(e => e.InputTypeData)
.ToList();
var sendMethodKeyAttribute = this.GetType().GetCustomAttribute<SendMethodKeyAttribute>();
var sendMethodKeyAttribute = GetType().GetCustomAttribute<SendMethodKeyAttribute>();
if (sendMethodKeyAttribute != null)
{
return new InputTemeplate()
{
Key = "",
Name = sendMethodKeyAttribute.Name,
Type = sendMethodKeyAttribute.Name,
Key = sendMethodKeyAttribute.Key,
Type = sendMethodKeyAttribute.Key,
TypeName = sendMethodKeyAttribute.Name,
Warning = sendMethodKeyAttribute.Waring,
Values = values
};
}
@ -170,10 +192,8 @@ namespace Inotify.Sends
return null;
}
public virtual bool SendMessage(SendMessage message)
{
return false;
}
public abstract bool SendMessage(SendMessage message);
protected WebProxy GetProxy()
{
@ -183,7 +203,7 @@ namespace Inotify.Sends
if (proxyurl != null)
{
WebProxy proxy = new WebProxy
WebProxy proxy = new WebProxy()
{
Address = new Uri(proxyurl)
};
@ -217,8 +237,11 @@ namespace Inotify.Sends
{
if (IsUrl(url) && IsImage(url))
{
WebClient mywebclient = new WebClient();
return mywebclient.DownloadData(url);
using (WebClient mywebclient = new WebClient())
{
return mywebclient.DownloadData(url);
}
}
return null;
}

View File

@ -2,24 +2,21 @@ using Inotify.Controllers;
using Inotify.Data;
using Inotify.Sends;
using Inotify.ThridOauth;
using Microsoft.AspNetCore.Authentication.Cookies;
using Microsoft.AspNetCore.Authentication.JwtBearer;
using Microsoft.AspNetCore.Builder;
using Microsoft.AspNetCore.Hosting;
using Microsoft.AspNetCore.Http;
using Microsoft.AspNetCore.Rewrite;
using Microsoft.AspNetCore.Server.Kestrel.Core;
using Microsoft.Extensions.Configuration;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Hosting;
using Microsoft.IdentityModel.Tokens;
using Newtonsoft.Json;
using NPoco;
using System;
using System.Net;
using System.Net.Http;
using System.Runtime.InteropServices;
using System.Text;
using System.Text.Encodings.Web;
using System.Text.RegularExpressions;
using System.Text.Unicode;
using System.Threading.Tasks;
@ -56,7 +53,7 @@ namespace Inotify
{
OnAuthenticationFailed = context =>
{
var payload = JsonConvert.SerializeObject(new { message = "ÈÏ֤ʧ°Ü", code = 403 });
var payload = JsonConvert.SerializeObject(new { message = "认证失败", code = 403 });
context.Response.ContentType = "application/json";
context.Response.StatusCode = StatusCodes.Status200OK;
context.Response.WriteAsync(payload);
@ -65,7 +62,7 @@ namespace Inotify
},
OnForbidden = context =>
{
var payload = JsonConvert.SerializeObject(new { message = "δ¾­ÊÚȨ", code = 405 });
var payload = JsonConvert.SerializeObject(new { message = "未经授权", code = 405 });
context.Response.ContentType = "application/json";
context.Response.StatusCode = StatusCodes.Status200OK;
context.Response.WriteAsync(payload);
@ -92,25 +89,82 @@ namespace Inotify
{
options.JsonSerializerOptions.Encoder = JavaScriptEncoder.Create(UnicodeRanges.All);
});
services.Configure<KestrelServerOptions>(x => x.AllowSynchronousIO = true)
.Configure<IISServerOptions>(x => x.AllowSynchronousIO = true);
}
public void Configure(IApplicationBuilder app, IWebHostEnvironment env)
{
#if !DEBUG
app.UseStaticFiles();
app.UseFileServer();
#endif
if (env.IsDevelopment())
{
app.UseDeveloperExceptionPage();
}
app.Use(next => context =>
{
context.Request.EnableBuffering();
return next(context);
});
var options = new RewriteOptions();
options.AddRewrite(@"api/(.*).send/(.*)/(.*)", "api/send?token=$1&title=$2&data=$3", true);
options.AddRewrite(@"api/(.*).send/(.*)", "api/send?token=$1&title=$2", true);
options.Add(rewriteContext =>
{
Match match;
if (rewriteContext.HttpContext.Request.Path == "/")
{
var queryValue = rewriteContext.HttpContext.Request.QueryString.Value;
match = Regex.Match(queryValue, @"^\?act=(.*)/(.*)/(.*)/(.*)$");
if (match.Success)
{
var groups = match.Groups;
rewriteContext.HttpContext.Request.Path = @"/api/send";
rewriteContext.HttpContext.Request.QueryString = new QueryString($"?token={groups[2]}&title={groups[3]}&data={groups[4]}");
}
else
{
match = Regex.Match(queryValue, @"^\?act=(.*)/(.*)/(.*)$");
if (match.Success)
{
var groups = match.Groups;
rewriteContext.HttpContext.Request.Path = @"/api/send";
rewriteContext.HttpContext.Request.QueryString = new QueryString($"?token={groups[2]}&title={groups[3]}");
}
else
{
match = Regex.Match(queryValue, @"^\?act=(.*)/(.*)$");
if (match.Success)
{
var groups = match.Groups;
rewriteContext.HttpContext.Request.Path = @"/api/send";
rewriteContext.HttpContext.Request.QueryString = new QueryString($"?token={groups[2]}");
}
else if (rewriteContext.HttpContext.Request.QueryString.Value.StartsWith("?"))
{
var groups = match.Groups;
rewriteContext.HttpContext.Request.Path = @"/info";
rewriteContext.HttpContext.Request.QueryString = new QueryString();
}
}
}
}
rewriteContext.Result = RuleResult.ContinueRules;
});
options.AddRewrite(@"^api/(.*).send/(.*)/(.*)", "api/send?token=$1&title=$2&data=$3", true);
options.AddRewrite(@"^api/(.*).send/(.*)", "api/send?token=$1&title=$2", true);
options.AddRewrite(@"^(.*).send/(.*)/(.*)", "api/send?token=$1&title=$2&data=$3", true);
options.AddRewrite(@"^(.*).send/(.*)", "api/send?token=$1&title=$2", true);
app.UseRewriter(options);
app.UseRouting();
app.UseStaticFiles();
app.UseFileServer();
app.UseAuthentication();
app.UseAuthorization();
app.UseEndpoints(endpoints =>
@ -119,4 +173,4 @@ namespace Inotify
});
}
}
}
}

View File

@ -1,10 +1,6 @@
using Microsoft.AspNetCore.Hosting;
using Microsoft.Extensions.Hosting;
using System;
using System.Collections.Generic;
using System.Linq;
using System.Threading;
using System.Threading.Tasks;
namespace Inotify
{
@ -27,7 +23,9 @@ namespace Inotify
public static StartUpManager Load()
{
if (_appManager == null)
{
_appManager = new StartUpManager();
}
return _appManager;
}
@ -35,10 +33,14 @@ namespace Inotify
public void Start(string[] args)
{
if (_running)
{
return;
}
if (_tokenSource != null && _tokenSource.IsCancellationRequested)
{
return;
}
_tokenSource = new CancellationTokenSource();
_tokenSource.Token.ThrowIfCancellationRequested();
@ -56,7 +58,9 @@ namespace Inotify
public void Stop()
{
if (!_running)
{
return;
}
_tokenSource.Cancel();
_running = false;

View File

@ -1,5 +1,4 @@
using Inotify.ThridOauth.Common;
using Newtonsoft.Json.Linq;
using Newtonsoft.Json.Linq;

View File

@ -1,5 +1,4 @@
using Inotify.ThridOauth.Common;
using Newtonsoft.Json;
using Newtonsoft.Json;
using Newtonsoft.Json.Linq;

View File

@ -1,8 +1,4 @@
using Inotify.ThridOauth.Entity;
namespace Inotify.ThridOauth.Entity
namespace Inotify.ThridOauth.Entity
{
public class CredentialSetting
{

View File

@ -1,7 +1,3 @@
using Inotify.ThridOauth.Entity;
namespace Inotify.ThridOauth.Entity
{
public class FaceBookCredential : CredentialSetting

View File

@ -1,7 +1,3 @@
using Inotify.ThridOauth.Entity;
namespace Inotify.ThridOauth.Entity
{
public class GitHubCredential : CredentialSetting

View File

@ -1,7 +1,3 @@
using Inotify.ThridOauth.Entity;
namespace Inotify.ThridOauth.Entity
{
public class QQCredential : CredentialSetting

View File

@ -1,7 +1,3 @@
using Inotify.ThridOauth.Entity;
namespace Inotify.ThridOauth.Entity
{
public class WechatCredential : CredentialSetting

View File

@ -1,7 +1,3 @@
using Inotify.ThridOauth.Entity;
namespace Inotify.ThridOauth.Entity
{
public class WeiBoCredential : CredentialSetting

View File

@ -1,8 +1,4 @@
using Inotify.ThridOauth.IService;
namespace Inotify.ThridOauth.IService
namespace Inotify.ThridOauth.IService
{
public interface IFacebookLogin : ILogin
{

View File

@ -1,7 +1,3 @@
using Inotify.ThridOauth.IService;
namespace Inotify.ThridOauth.IService
{
public interface IGitHubLogin : ILogin

View File

@ -1,5 +1,4 @@
using Inotify.ThridOauth.Common;
using Inotify.ThridOauth.IService;

View File

@ -1,8 +1,4 @@
using Inotify.ThridOauth.IService;
namespace Inotify.ThridOauth.IService
namespace Inotify.ThridOauth.IService
{
public interface IQqLogin : ILogin
{

View File

@ -1,8 +1,4 @@
using Inotify.ThridOauth.IService;
namespace Inotify.ThridOauth.IService
namespace Inotify.ThridOauth.IService
{
public interface ISinaLogin : ILogin
{

View File

@ -1,8 +1,4 @@
using Inotify.ThridOauth.IService;
namespace Inotify.ThridOauth.IService
namespace Inotify.ThridOauth.IService
{
public interface IWeChatLogin : ILogin
{

View File

@ -1,7 +1,6 @@
using Inotify.ThridOauth.Common;
using Inotify.ThridOauth.Entity;
using Inotify.ThridOauth.IService;
using Inotify.ThridOauth.Service;
using Microsoft.AspNetCore.Http;
using Microsoft.Extensions.Options;
using Newtonsoft.Json.Linq;
@ -47,7 +46,10 @@ namespace Inotify.ThridOauth.Service
var token = GetAccessToken(code, ref errMsg);
if (!string.IsNullOrEmpty(errMsg))
{
return new AuthorizeResult { Code = Code.UserInfoErrorMsg, Error = errMsg };
}
var accessToken = token.Value<string>("access_token");
var user = UserInfo(accessToken, ref errMsg);

View File

@ -2,14 +2,10 @@ using Inotify.Sends;
using Inotify.ThridOauth.Common;
using Inotify.ThridOauth.Entity;
using Inotify.ThridOauth.IService;
using Inotify.ThridOauth.Service;
using Microsoft.AspNetCore.Http;
using Microsoft.Extensions.Options;
using Newtonsoft.Json.Linq;
using System;
using System.Collections.Generic;
using System.Linq;
using System.Net;
using System.Net.Http;
@ -26,7 +22,7 @@ namespace Inotify.ThridOauth.Service
private readonly string _authorizeUrl;
public GitHubLogin(IHttpContextAccessor contextAccessor, IOptions<GitHubCredential> options) : base(
public GitHubLogin(IHttpContextAccessor contextAccessor) : base(
contextAccessor)
{
Credential = new CredentialSetting()
@ -56,11 +52,14 @@ namespace Inotify.ThridOauth.Service
var token = GetAccessToken(code, ref errorMsg);
if (!string.IsNullOrEmpty(errorMsg))
{
return new AuthorizeResult
{
Code = Code.UserInfoErrorMsg,
Error = errorMsg
};
}
var accessToken = token.Value<string>("access_token");
var user = UserInfo(accessToken, ref errorMsg);
@ -140,11 +139,17 @@ namespace Inotify.ThridOauth.Service
{
startindex = sourse.IndexOf(startstr, StringComparison.Ordinal);
if (startindex == -1)
{
return result;
}
string tmpstr = sourse[(startindex + startstr.Length)..];
endindex = tmpstr.IndexOf(endstr, StringComparison.Ordinal);
if (endindex == -1)
{
return result;
}
result = tmpstr.Remove(endindex);
}
catch (Exception ex)

View File

@ -1,6 +1,5 @@
using Inotify.Sends;
using Inotify.ThridOauth.Entity;
using Inotify.ThridOauth.Service;
using Microsoft.AspNetCore.Http;
using System;
using System.Net;

View File

@ -1,7 +1,6 @@
using Inotify.ThridOauth.Common;
using Inotify.ThridOauth.Entity;
using Inotify.ThridOauth.IService;
using Inotify.ThridOauth.Service;
using Microsoft.AspNetCore.Http;
using Microsoft.Extensions.Options;
using Newtonsoft.Json.Linq;
@ -58,7 +57,10 @@ namespace Inotify.ThridOauth.Service
var token = GetAccessToken(code, ref errorMsg);
if (!string.IsNullOrEmpty(errorMsg))
{
return new AuthorizeResult { Code = Code.UserInfoErrorMsg, Error = errorMsg };
}
var accessToken = token["access_token"];
var user = UserInfo(accessToken, ref errorMsg);

View File

@ -1,7 +1,6 @@
using Inotify.ThridOauth.Common;
using Inotify.ThridOauth.Entity;
using Inotify.ThridOauth.IService;
using Inotify.ThridOauth.Service;
using Microsoft.AspNetCore.Http;
using Microsoft.Extensions.Options;
using Newtonsoft.Json.Linq;
@ -55,7 +54,11 @@ namespace Inotify.ThridOauth.Service
var token = GetAccessToken(code, ref errorMsg);
if (!string.IsNullOrEmpty(errorMsg)) return new AuthorizeResult { Code = Code.UserInfoErrorMsg, Error = errorMsg };
if (!string.IsNullOrEmpty(errorMsg))
{
return new AuthorizeResult { Code = Code.UserInfoErrorMsg, Error = errorMsg };
}
var accessToken = token.Value<string>("access_token");
var uid = token.Value<string>("openid");

View File

@ -1,7 +1,6 @@
using Inotify.ThridOauth.Common;
using Inotify.ThridOauth.Entity;
using Inotify.ThridOauth.IService;
using Inotify.ThridOauth.Service;
using Microsoft.AspNetCore.Http;
using Microsoft.Extensions.Options;
using Newtonsoft.Json.Linq;
@ -53,7 +52,10 @@ namespace Inotify.ThridOauth.Service
var token = GetAccessToken(code, ref errorMsg);
if (!string.IsNullOrEmpty(errorMsg))
{
return new AuthorizeResult { Code = Code.UserInfoErrorMsg, Error = errorMsg };
}
var accessToken = token.Value<string>("access_token");
var uid = token.Value<string>("uid");

View File

@ -1,5 +1,4 @@
using Inotify.ThridOauth;
using Inotify.ThridOauth.Entity;
using Inotify.ThridOauth.Entity;
using Inotify.ThridOauth.IService;
using Inotify.ThridOauth.Service;
using Microsoft.Extensions.DependencyInjection;
@ -16,7 +15,11 @@ namespace Inotify.ThridOauth
public static IServiceCollection AddWeChatLogin(this IServiceCollection services,
Action<WechatCredential> credential)
{
if (services == null) throw new ArgumentNullException(nameof(services));
if (services == null)
{
throw new ArgumentNullException(nameof(services));
}
services.Configure(credential);
services.AddScoped<IWeChatLogin, WeChatLogin>();
return services;
@ -25,7 +28,11 @@ namespace Inotify.ThridOauth
public static IServiceCollection AddQqLogin(this IServiceCollection services,
Action<QQCredential> credential)
{
if (services == null) throw new ArgumentNullException(nameof(services));
if (services == null)
{
throw new ArgumentNullException(nameof(services));
}
services.Configure(credential);
services.AddScoped<IQqLogin, QqLogin>();
return services;
@ -34,7 +41,11 @@ namespace Inotify.ThridOauth
public static IServiceCollection AddSinaLogin(this IServiceCollection services,
Action<WeiBoCredential> credential)
{
if (services == null) throw new ArgumentNullException(nameof(services));
if (services == null)
{
throw new ArgumentNullException(nameof(services));
}
services.Configure(credential);
services.AddScoped<ISinaLogin, WeiBoLogin>();
return services;
@ -43,7 +54,11 @@ namespace Inotify.ThridOauth
public static IServiceCollection AddFackbookLogin(this IServiceCollection services,
Action<FaceBookCredential> credential)
{
if (services == null) throw new ArgumentNullException(nameof(services));
if (services == null)
{
throw new ArgumentNullException(nameof(services));
}
services.Configure(credential);
services.AddScoped<IFacebookLogin, FacebookLogin>();
return services;
@ -52,7 +67,11 @@ namespace Inotify.ThridOauth
public static IServiceCollection AddGitHubLogin(this IServiceCollection services,
Action<GitHubCredential> credential)
{
if (services == null) throw new ArgumentNullException(nameof(services));
if (services == null)
{
throw new ArgumentNullException(nameof(services));
}
services.Configure(credential);
services.AddScoped<IGitHubLogin, GitHubLogin>();
return services;

View File

@ -0,0 +1,172 @@
using CorePush.Interfaces;
using CorePush.Utils;
using System;
using System.Collections.Concurrent;
using System.Collections.Generic;
using System.Linq;
using System.Net.Http;
using System.Security.Cryptography;
using System.Text;
using System.Threading;
using System.Threading.Tasks;
namespace CorePush.Apple
{
/// <summary>
/// HTTP2 Apple Push Notification sender
/// </summary>
public class ApnSender : IApnSender
{
private static readonly ConcurrentDictionary<string, Tuple<string, DateTime>> tokens = new ConcurrentDictionary<string, Tuple<string, DateTime>>();
private static readonly Dictionary<ApnServerType, string> servers = new Dictionary<ApnServerType, string>
{
{ApnServerType.Development, "https://api.development.push.apple.com:443" },
{ApnServerType.Production, "https://api.push.apple.com:443" }
};
private const string apnidHeader = "apns-id";
private const int tokenExpiresMinutes = 50;
private readonly ApnSettings settings;
private readonly HttpClient http;
/// <summary>
/// Apple push notification sender constructor
/// </summary>
/// <param name="settings">Apple Push Notification settings</param>
public ApnSender(ApnSettings settings, HttpClient http)
{
this.settings = settings ?? throw new ArgumentNullException(nameof(settings));
this.http = http ?? throw new ArgumentNullException(nameof(http));
}
/// <summary>
/// Serialize and send notification to APN. Please see how your message should be formatted here:
/// https://developer.apple.com/library/archive/documentation/NetworkingInternet/Conceptual/RemoteNotificationsPG/CreatingtheNotificationPayload.html#//apple_ref/doc/uid/TP40008194-CH10-SW1
/// Payload will be serialized using Newtonsoft.Json package.
/// !IMPORTANT: If you send many messages at once, make sure to retry those calls. Apple typically doesn't like
/// to receive too many requests and may ocasionally respond with HTTP 429. Just try/catch this call and retry as needed.
/// </summary>
/// <exception cref="HttpRequestException">Throws exception when not successful</exception>
///
public ApnsResponse Send(object notification,
string deviceToken,
string apnsId = null,
int apnsExpiration = 0,
int apnsPriority = 10,
bool isBackground = false,
CancellationToken cancellationToken = default)
{
var task= SendAsync(notification, deviceToken, apnsId, apnsExpiration, apnsPriority, isBackground, cancellationToken);
task.Wait();
return task.Result;
}
public async Task<ApnsResponse> SendAsync(
object notification,
string deviceToken,
string apnsId = null,
int apnsExpiration = 0,
int apnsPriority = 10,
bool isBackground = false,
CancellationToken cancellationToken = default)
{
var path = $"/3/device/{deviceToken}";
var json = JsonHelper.Serialize(notification);
var request = new HttpRequestMessage(HttpMethod.Post, new Uri(servers[settings.ServerType] + path))
{
Version = new Version(2, 0),
Content = new StringContent(json)
};
request.Headers.Authorization = new System.Net.Http.Headers.AuthenticationHeaderValue("bearer", GetJwtToken());
request.Headers.TryAddWithoutValidation(":method", "POST");
request.Headers.TryAddWithoutValidation(":path", path);
request.Headers.Add("apns-topic", settings.AppBundleIdentifier);
request.Headers.Add("apns-expiration", apnsExpiration.ToString());
request.Headers.Add("apns-priority", apnsPriority.ToString());
request.Headers.Add("apns-push-type", isBackground ? "background" : "alert"); // for iOS 13 required
if (!string.IsNullOrWhiteSpace(apnsId))
{
request.Headers.Add(apnidHeader, apnsId);
}
using (var response = await http.SendAsync(request, cancellationToken))
{
var succeed = response.IsSuccessStatusCode;
var content = await response.Content.ReadAsStringAsync();
var error = JsonHelper.Deserialize<ApnsError>(content);
return new ApnsResponse
{
IsSuccess = succeed,
Error = error
};
}
}
private string GetJwtToken()
{
var (token, date) = tokens.GetOrAdd(settings.AppBundleIdentifier, _ => new Tuple<string, DateTime>(CreateJwtToken(), DateTime.UtcNow));
if (date < DateTime.UtcNow.AddMinutes(-tokenExpiresMinutes))
{
tokens.TryRemove(settings.AppBundleIdentifier, out _);
return GetJwtToken();
}
return token;
}
private string CreateJwtToken()
{
var header = JsonHelper.Serialize(new { alg = "ES256", kid = CleanP8Key(settings.P8PrivateKeyId) });
var payload = JsonHelper.Serialize(new { iss = settings.TeamId, iat = ToEpoch(DateTime.UtcNow) });
var headerBase64 = Convert.ToBase64String(Encoding.UTF8.GetBytes(header));
var payloadBasae64 = Convert.ToBase64String(Encoding.UTF8.GetBytes(payload));
var unsignedJwtData = $"{headerBase64}.{payloadBasae64}";
var unsignedJwtBytes = Encoding.UTF8.GetBytes(unsignedJwtData);
using (var dsa = AppleCryptoHelper.GetEllipticCurveAlgorithm(CleanP8Key(settings.P8PrivateKey)))
{
var signature = dsa.SignData(unsignedJwtBytes, 0, unsignedJwtBytes.Length, HashAlgorithmName.SHA256);
return $"{unsignedJwtData}.{Convert.ToBase64String(signature)}";
}
}
private static int ToEpoch(DateTime time)
{
var span = DateTime.UtcNow - new DateTime(1970, 1, 1);
return Convert.ToInt32(span.TotalSeconds);
}
private static string CleanP8Key(string p8Key)
{
// If we have an empty p8Key, then don't bother doing any tasks.
if (string.IsNullOrEmpty(p8Key))
{
return p8Key;
}
var lines = p8Key.Split(new [] { '\n' }).ToList();
if (0 != lines.Count && lines[0].StartsWith("-----BEGIN PRIVATE KEY-----"))
{
lines.RemoveAt(0);
}
if (0 != lines.Count && lines[lines.Count - 1].StartsWith("-----END PRIVATE KEY-----"))
{
lines.RemoveAt(lines.Count - 1);
}
var result = string.Join(string.Empty, lines);
return result;
}
}
}

View File

@ -0,0 +1,8 @@
namespace CorePush.Apple
{
public enum ApnServerType
{
Development,
Production
}
}

View File

@ -0,0 +1,30 @@
namespace CorePush.Apple
{
public class ApnSettings
{
/// <summary>
/// p8 certificate string
/// </summary>
public string P8PrivateKey { get; set; }
/// <summary>
/// 10 digit p8 certificate id. Usually a part of a downloadable certificate filename
/// </summary>
public string P8PrivateKeyId { get; set; }
/// <summary>
/// Apple 10 digit team id
/// </summary>
public string TeamId { get; set; }
/// <summary>
/// App slug / bundle name
/// </summary>
public string AppBundleIdentifier { get; set; }
/// <summary>
/// Development or Production server
/// </summary>
public ApnServerType ServerType { get; set; }
}
}

View File

@ -0,0 +1,50 @@
namespace CorePush.Apple
{
public class ApnsResponse
{
public bool IsSuccess { get; set; }
public ApnsError Error { get; set; }
}
public class ApnsError
{
public ReasonEnum Reason {get; set;}
public long? Timestamp {get; set; }
}
/// <summary>
/// https://developer.apple.com/library/archive/documentation/NetworkingInternet/Conceptual/RemoteNotificationsPG/CommunicatingwithAPNs.html#//apple_ref/doc/uid/TP40008194-CH11-SW15
/// </summary>
public enum ReasonEnum
{
BadCollapseId,
BadDeviceToken,
BadExpirationDate,
BadMessageId,
BadPriority,
BadTopic,
DeviceTokenNotForTopic,
DuplicateHeaders,
IdleTimeout,
MissingDeviceToken,
MissingTopic,
PayloadEmpty,
TopicDisallowed,
BadCertificate,
BadCertificateEnvironment,
ExpiredProviderToken,
Forbidden,
InvalidProviderToken,
MissingProviderToken,
BadPath,
MethodNotAllowed,
Unregistered,
PayloadTooLarge,
TooManyProviderTokenUpdates,
TooManyRequests,
InternalServerError,
ServiceUnavailable,
Shutdown,
}
}

View File

@ -0,0 +1,37 @@
using Newtonsoft.Json;
using System.Collections.Generic;
namespace CorePush.Google
{
public class FcmResponse
{
[JsonProperty("multicast_id")]
public string MulticastId { get; set; }
[JsonProperty("canonical_ids")]
public int CanonicalIds { get; set; }
/// <summary>
/// Success count
/// </summary>
public int Success { get; set; }
/// <summary>
/// Failure count
/// </summary>
public int Failure { get; set; }
/// <summary>
/// Results
/// </summary>
public List<FcmResult> Results { get; set; }
/// <summary>
/// Returns value indicating notification sent success or failure
/// </summary>
public bool IsSuccess()
{
return Success > 0 && Failure == 0;
}
}
}

View File

@ -0,0 +1,15 @@
using Newtonsoft.Json;
namespace CorePush.Google
{
public class FcmResult
{
[JsonProperty("message_id")]
public string MessageId { get; set; }
[JsonProperty("registration_id")]
public string RegistrationId { get; set; }
public string Error { get; set; }
}
}

View File

@ -0,0 +1,81 @@
using CorePush.Interfaces;
using CorePush.Utils;
using Newtonsoft.Json.Linq;
using System.Net.Http;
using System.Text;
using System.Threading.Tasks;
using System.Threading;
namespace CorePush.Google
{
/// <summary>
/// Firebase message sender
/// </summary>
public class FcmSender : IFcmSender
{
private readonly string fcmUrl = "https://fcm.googleapis.com/fcm/send";
private readonly FcmSettings settings;
private readonly HttpClient http;
public FcmSender(FcmSettings settings, HttpClient http)
{
this.settings = settings;
this.http = http;
}
/// <summary>
/// Send firebase notification.
/// Please check out payload formats:
/// https://firebase.google.com/docs/cloud-messaging/concept-options#notifications
/// The SendAsync method will add/replace "to" value with deviceId
/// </summary>
/// <param name="deviceId">Device token (will add `to` to the payload)</param>
/// <param name="payload">Notification payload that will be serialized using Newtonsoft.Json package</param>
/// <cref="HttpRequestException">Throws exception when not successful</exception>
public Task<FcmResponse> SendAsync(string deviceId, object payload, CancellationToken cancellationToken = default)
{
var jsonObject = JObject.FromObject(payload);
jsonObject.Remove("to");
jsonObject.Add("to", JToken.FromObject(deviceId));
return SendAsync(jsonObject, cancellationToken);
}
/// <summary>
/// Send firebase notification.
/// Please check out payload formats:
/// https://firebase.google.com/docs/cloud-messaging/concept-options#notifications
/// The SendAsync method will add/replace "to" value with deviceId
/// </summary>
/// <param name="payload">Notification payload that will be serialized using Newtonsoft.Json package</param>
/// <exception cref="HttpRequestException">Throws exception when not successful</exception>
public async Task<FcmResponse> SendAsync(object payload, CancellationToken cancellationToken = default)
{
var serialized = JsonHelper.Serialize(payload);
using (var httpRequest = new HttpRequestMessage(HttpMethod.Post, fcmUrl))
{
httpRequest.Headers.Add("Authorization", $"key = {settings.ServerKey}");
if (!string.IsNullOrEmpty(settings.SenderId))
{
httpRequest.Headers.Add("Sender", $"id = {settings.SenderId}");
}
httpRequest.Content = new StringContent(serialized, Encoding.UTF8, "application/json");
using (var response = await http.SendAsync(httpRequest, cancellationToken))
{
var responseString = await response.Content.ReadAsStringAsync();
if (!response.IsSuccessStatusCode)
{
throw new HttpRequestException("Firebase notification error: " + responseString);
}
return JsonHelper.Deserialize<FcmResponse>(responseString);
}
}
}
}
}

View File

@ -0,0 +1,15 @@
namespace CorePush.Google
{
public class FcmSettings
{
/// <summary>
/// FCM Sender ID
/// </summary>
public string SenderId { get; set; }
/// <summary>
/// FCM Server Key
/// </summary>
public string ServerKey { get; set; }
}
}

View File

@ -0,0 +1,18 @@
using CorePush.Apple;
using System.Threading;
using System.Threading.Tasks;
namespace CorePush.Interfaces
{
public interface IApnSender
{
Task<ApnsResponse> SendAsync(
object notification,
string deviceToken,
string apnsId = null,
int apnsExpiration = 0,
int apnsPriority = 10,
bool isBackground = false,
CancellationToken cancellationToken = default);
}
}

View File

@ -0,0 +1,12 @@
using CorePush.Google;
using System.Threading;
using System.Threading.Tasks;
namespace CorePush.Interfaces
{
public interface IFcmSender
{
Task<FcmResponse> SendAsync(string deviceId, object payload, CancellationToken cancellationToken = default);
Task<FcmResponse> SendAsync(object payload, CancellationToken cancellationToken = default);
}
}

View File

@ -0,0 +1,28 @@

using System;
using System.Security.Cryptography;
using Org.BouncyCastle.Crypto.Parameters;
using Org.BouncyCastle.Security;
namespace CorePush.Utils
{
public static class AppleCryptoHelper
{
public static ECDsa GetEllipticCurveAlgorithm(string privateKey)
{
var keyParams = (ECPrivateKeyParameters) PrivateKeyFactory.CreateKey(Convert.FromBase64String(privateKey));
var q = keyParams.Parameters.G.Multiply(keyParams.D).Normalize();
return ECDsa.Create(new ECParameters
{
Curve = ECCurve.CreateFromValue(keyParams.PublicKeyParamSet.Id),
D = keyParams.D.ToByteArrayUnsigned(),
Q =
{
X = q.XCoord.GetEncoded(),
Y = q.YCoord.GetEncoded()
}
});
}
}
}

View File

@ -0,0 +1,24 @@
using Newtonsoft.Json;
using Newtonsoft.Json.Serialization;
namespace CorePush.Utils
{
public static class JsonHelper
{
private static readonly JsonSerializerSettings settings = new JsonSerializerSettings
{
ContractResolver = new CamelCasePropertyNamesContractResolver(),
NullValueHandling = NullValueHandling.Ignore,
};
public static string Serialize(object obj)
{
return JsonConvert.SerializeObject(obj, settings);
}
public static TObject Deserialize<TObject>(string json)
{
return JsonConvert.DeserializeObject<TObject>(json, settings);
}
}
}

View File

@ -1,6 +1,6 @@
# inotify
[![docker](https://github.com/xpnas/inotify/actions/workflows/docker.yml/badge.svg)](https://github.com/xpnas/inotify/actions/workflows/docker.yml)
[![docker](https://github.com/xpnas/inotify/actions/workflows/docker.yml/badge.svg?branch=master)](https://github.com/xpnas/inotify/actions/workflows/docker.yml)
一个简易的消息通知系统,支持企业微信、电报机器人、邮件推送
@ -20,33 +20,47 @@
- [x] 企业微信应用消息
- [x] 电报机器人消息
- [x] SMTP邮箱消息
- [ ] 钉钉群机器人
- [ ] 飞书群机器人
- [ ] 自定义
- [x] BARK
- [x] 钉钉群机器人
- [x] 飞书群机器人
- [x] 自定义
## 使用方法
1. Docker安装
```
docker run --name=inotify -d -p 8000:80 -v inotify_data:/inotify_data --restart=always xpnas/inotify:master
```
1. Docker安装
* 发布版
```
docker run --name=inotify -d -p 8000:80 -v inotify_data:/inotify_data --restart=always xpnas/inotify:latest
```
* 开发版
```
docker run --name=inotify -d -p 8000:80 -v inotify_data:/inotify_data --restart=always xpnas/inotify:master
```
2. 配置Nginx代理
```
server
{
```
server
{
location / { proxy_pass http://127.0.0.1:8000; }
}
```
3. 默认用户名admin密码123456
}
```
3. 进入`Github/Settings/Developer settings/OAuth Apps`创建应用
* 记录`Client ID`,创建`Client secrets`
* `Authorization callback URL`回调地址填写https://{您的域名}/api/oauth/githubLogin
4. 使用`默认用户名admin密码123456`登陆后台/全局参数,修改Github登陆的`应用ID`、`应用密钥`并启动登陆
5. 建议将`管理权限`的用户名设置成自己的github用户名再使用Github登陆后在用户管理页面`删除默认账号admin`
## BARK设置
1. 本项目依据Bark-Server接口规范实现了内置BARK服务端
2. 复制或扫码`消息验证\BARK授权`中的地址填入BARK应用的服务器地址中,如`https://inotify.cf?act=6D474C0DB1474F19BD8F7342D570C0FC`
3. BARK的APP会自动在本系统注册数据记录将直接出现在`消息通道`
## 系统截图
![](../master/public/A.png)
![](../public/A.png)
![](../master/public/B.png)
![](../public/B.png)
![](../master/public/C.png)
![](../public/C.png)
![](../master/public/D.png)
![](../public/D.png)

Binary file not shown.

Before

Width:  |  Height:  |  Size: 86 KiB

After

Width:  |  Height:  |  Size: 104 KiB

25
sonar.sln Normal file
View File

@ -0,0 +1,25 @@

Microsoft Visual Studio Solution File, Format Version 12.00
# Visual Studio Version 16
VisualStudioVersion = 16.0.31129.286
MinimumVisualStudioVersion = 10.0.40219.1
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Inotify", "Inotify\Inotify.csproj", "{EEDAC4E1-2C4F-4400-B2AE-E7027EE83C83}"
EndProject
Global
GlobalSection(SolutionConfigurationPlatforms) = preSolution
Debug|Any CPU = Debug|Any CPU
Release|Any CPU = Release|Any CPU
EndGlobalSection
GlobalSection(ProjectConfigurationPlatforms) = postSolution
{EEDAC4E1-2C4F-4400-B2AE-E7027EE83C83}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{EEDAC4E1-2C4F-4400-B2AE-E7027EE83C83}.Debug|Any CPU.Build.0 = Debug|Any CPU
{EEDAC4E1-2C4F-4400-B2AE-E7027EE83C83}.Release|Any CPU.ActiveCfg = Release|Any CPU
{EEDAC4E1-2C4F-4400-B2AE-E7027EE83C83}.Release|Any CPU.Build.0 = Release|Any CPU
EndGlobalSection
GlobalSection(SolutionProperties) = preSolution
HideSolutionNode = FALSE
EndGlobalSection
GlobalSection(ExtensibilityGlobals) = postSolution
SolutionGuid = {E659C816-7213-4399-9AB3-0E575CAD9C97}
EndGlobalSection
EndGlobal