Compare commits

..

23 Commits

Author SHA1 Message Date
Ching
e2ed14784a feat(dodo): [A] 增加飞书机器人部署逻辑
[A] 增加飞书机器人部署逻辑

Signed-off-by: Ching <loooching@gmail.com>
2022-01-18 23:05:18 +08:00
Ching
2280fd25c1 feat(week_recipe.vue): [M] add console log for debug
[M] add console log for debug

Signed-off-by: Ching <loooching@gmail.com>
2022-01-17 22:29:16 +08:00
Ching
b017b691ee feat(package.json): [M] frontend dependency
[M] frontend dependency

Signed-off-by: Ching <loooching@gmail.com>
2022-01-16 13:49:08 +08:00
Ching
3e206522e8 Merge branch 'develop' of git.tunpok.com:ching/dsite into develop 2022-01-16 13:17:33 +08:00
Ching
4a940491fe feat(frontend folder): [M] change vue version
[M] change vue version

Signed-off-by: Ching <loooching@gmail.com>
2022-01-16 13:11:31 +08:00
ching
13942425aa modify Jenkinsfile 2022-01-16 01:44:35 +08:00
ching
3f7e61d7dc update Jenkinsfile 2022-01-16 01:32:35 +08:00
Ching
e82ac3ae3d feat(Jenkinsfile): [M] modify file name
[M] modify file name

Signed-off-by: Ching <loooching@gmail.com>
2022-01-16 01:11:46 +08:00
Ching
70cccac644 feat(Jenkinsfile): [A] add Jenkinsfile
[A] add Jenkinsfile

Signed-off-by: Ching <loooching@gmail.com>
2022-01-16 01:01:32 +08:00
Ching
95f70c1478 Merge branch 'develop' of github.com:looching/dsite into develop 2022-01-15 13:17:25 +08:00
Ching
23f813f592 feat(a.txt): [A] add test file for testing multi remote
[A] add test file for testing multi remote

Signed-off-by: Ching <loooching@gmail.com>
2022-01-15 13:16:25 +08:00
Ching
047bd00545 feat(dodo.py): [M] 增加上一条和删除上一条嘟嘟的功能
[M] 增加上一条和删除上一条嘟嘟的功能

Signed-off-by: Ching <loooching@gmail.com>
2022-01-07 11:59:11 +08:00
Ching
78ef05b68e feat(dodo.py): [M] add logger; 缓存event_id避免重复发送消息
[M] add logger; 缓存event_id避免重复发送消息

Signed-off-by: Ching <loooching@gmail.com>
2022-01-07 11:22:20 +08:00
Ching
a8fff9c533 feat(dodo.py): [M] 嘟嘟机增加发送到nofan 功能
[M] 嘟嘟机增加发送到nofan 功能

Signed-off-by: Ching <loooching@gmail.com>
2022-01-05 13:44:29 +08:00
Ching
4a6d2f82e0 feat(scripts folder): [A]增加嘟嘟机脚本
[A]增加嘟嘟机脚本

Signed-off-by: Ching <loooching@gmail.com>
2022-01-05 11:28:02 +08:00
Ching
d63f494594 feat(week recipe page): [M] 修改了刷新每周菜单的方式;修复移动版每周菜单有一天为空时页面为空的问题
[M] 修改了刷新每周菜单的方式;修复移动版每周菜单有一天为空时页面为空的问题

Signed-off-by: Ching <loooching@gmail.com>
2021-10-26 23:28:31 +08:00
Ching
973849540a feat(utils lark client): [A] add lark webhook client
[A] add lark webhook client

Signed-off-by: Ching <loooching@gmail.com>
2021-10-26 15:57:06 +08:00
Ching
0228221b91 feat(gitignore): [M] add settings.py to gitignore file
[M] add settings.py to gitignore file

Signed-off-by: Ching <loooching@gmail.com>
2021-10-26 15:53:34 +08:00
Ching
446b1dc2e5 feat(statics file): build
build

Signed-off-by: Ching <loooching@gmail.com>
2021-10-11 23:18:14 +08:00
Ching
e95ca6089d feat(week recipe page): [M] 修改下拉刷新高度
[M] 修改下拉刷新高度

Signed-off-by: Ching <loooching@gmail.com>
2021-10-11 23:16:46 +08:00
Ching
50fb4bada4 Merge branch 'feature/today-tag' into develop 2021-10-10 23:41:29 +08:00
Ching
018ff7c3a0 feat(week recipe page): [A] 增加今日标识
[A] 增加今日标识

Signed-off-by: Ching <loooching@gmail.com>
2021-10-10 23:36:23 +08:00
Ching
c89b052578 fix(week recipe api): [F] 修复刷新每周菜谱中重复的问题
[F] 修复刷新每周菜谱中重复的问题

Signed-off-by: Ching <loooching@gmail.com>
2021-10-09 22:37:21 +08:00
34 changed files with 11354 additions and 9908 deletions

3
.gitignore vendored
View File

@ -6,6 +6,9 @@ __pycache__/
# C extensions
*.so
# settings
dsite/settings.py
# Distribution / packaging
.Python
build/

17
Jenkinsfile vendored Normal file
View File

@ -0,0 +1,17 @@
pipeline {
agent {
docker {
image 'node:6-alpine'
args '-p 4000:4000'
}
}
stages {
stage('Build') {
steps {
sh 'npm install'
}
}
}
}

0
a.txt Normal file
View File

View File

@ -7,8 +7,8 @@ django-cors-headers==3.8.0
django-filter==2.4.0
djangorestframework==3.12.4
importlib-metadata==4.6.4
ipdb==0.13.9
ipython==7.16.1
ipdb
ipython
ipython-genutils==0.2.0
jedi==0.17.2
Markdown==3.3.4
@ -30,3 +30,4 @@ ua-parser==0.10.0
user-agents==2.2.0
wcwidth==0.2.5
zipp==3.5.0
redis==4.1.0

View File

@ -1,45 +0,0 @@
from .settings_default import *
ALLOWED_HOSTS = ['*']
CORS_ALLOW_CREDENTIALS = True # 允许携带cookie
# CORS_ALLOW_ALL_ORIGINS = True # If this is used then `CORS_ALLOWED_ORIGINS` will not have any effect
CORS_ALLOWED_ORIGINS = [
'http://localhost:8080',
'http://127.0.0.1:8080',
'http://192.168.1.101:8080'
]
CORS_ALLOW_METHODS = [
"DELETE",
"GET",
"OPTIONS",
"PATCH",
"POST",
"PUT",
]
CORS_ALLOW_HEADERS = [
"accept",
"accept-encoding",
"authorization",
"content-type",
"dnt",
"origin",
"user-agent",
"x-csrftoken",
"x-requested-with",
]
CORS_ALLOW_HEADERS = ('*')
BASE_DIR = os.path.dirname(os.path.dirname(os.path.abspath(__file__)))
PROJECT_DIR = os.path.dirname(os.path.abspath(__file__))
STATIC_ROOT = os.path.join(BASE_DIR, 'static')
STATICFILES_DIRS = [
os.path.join(BASE_DIR, "frontend/dist/"),
]
MEDIA_URL = '/media/'
MEDIA_ROOT = os.path.join(BASE_DIR, 'media')

View File

@ -155,3 +155,7 @@ REST_FRAMEWORK = {
CORS_ALLOWED_ORIGINS = []
CORS_ALLOW_CREDENTIALS = True # 允许携带cookie
# CORS_ALLOW_ALL_ORIGINS = False # If this is used then `CORS_ALLOWED_ORIGINS` will not have any effect
LARK_WEBHOOK_URL = ''
LARK_WEBHOOK_SECRET = ''

View File

@ -1 +1 @@
.summit-recipe[data-v-16b31338]{width:100%}.el-pagination{margin:10px 0 0 0}.el-row{margin-bottom:20px;&:last-child{margin-bottom:0}}.el-col,.grid-content{border-radius:4px}.grid-content{min-height:36px}.row-bg{padding:10px 0;background-color:#f9fafc}.content{padding:20px 10px}.re-generate{margin:20px 0;width:100%}.el-tag#meal a:link,.el-tag#meal a:visited{text-decoration:none}.el-tag#meal a:active,.el-tag#meal a:hover{text-decoration:underline}.el-tag{margin:0 5px 0 0}.el-tag+.el-tag{margin:5px 0 0 0}.recipe-type-tag{margin:0 10px 0 0;padding-bottom:0}.recipe-list{height:50%}body{background-color:#f7f8fa}.recipe-create{margin:20px 16px}.recipe_type,.recipe_type .van-grid-item__content{background-color:#f2f3f5}.daily_recipes{margin-bottom:10px}.daily_recipes:last-child{margin-bottom:40px}.action-button{height:100%;width:50%}.daily_recipe{margin-top:20px}.delete-button{width:100%;height:100%}.delete-icon{font-size:16px;line-height:inherit;padding:0 5px}
.summit-recipe[data-v-16b31338]{width:100%}.el-pagination{margin:10px 0 0 0}.el-row{margin-bottom:20px;&:last-child{margin-bottom:0}}.el-col,.grid-content{border-radius:4px}.grid-content{min-height:36px}.row-bg{padding:10px 0;background-color:#f9fafc}.content{padding:20px 10px}.re-generate{margin:20px 0;width:100%}.el-tag#meal a:link,.el-tag#meal a:visited{text-decoration:none}.el-tag#meal a:active,.el-tag#meal a:hover{text-decoration:underline}.el-tag{margin:0 5px 0 0}.el-tag+.el-tag{margin:5px 0 0 0}.recipe-type-tag{margin:0 10px 0 0;padding-bottom:0}.recipe-list{height:50%}body{background-color:#f7f8fa}.recipe-create{margin:20px 16px}.recipe_type,.recipe_type .van-grid-item__content{background-color:#f2f3f5}.daily_recipes{margin-bottom:10px}.daily_recipes:last-child{margin-bottom:40px}.action-button{height:100%;width:50%}.today-tag{margin-left:10px}.refresh-button{float:right;margin-right:20px}.daily_recipe{margin-top:20px}.delete-button{width:100%;height:100%}.delete-icon{font-size:16px;line-height:inherit;padding:0 5px}

File diff suppressed because one or more lines are too long

View File

@ -1,29 +1 @@
<!DOCTYPE html>
<html lang="">
<head>
<meta charset="utf-8" />
<meta
name="viewport"
content="width=device-width,initial-scale=1.0,maximum-scale=1.0, user-scalable=0"
/>
<link rel="icon" href="/favicon.ico" />
<title>frontend</title>
<link href="/css/app.12f77eb5.css" rel="preload" as="style" />
<link href="/css/chunk-vendors.b9153273.css" rel="preload" as="style" />
<link href="/js/app.4a8d2f87.js" rel="preload" as="script" />
<link href="/js/chunk-vendors.4d54ba89.js" rel="preload" as="script" />
<link href="/css/chunk-vendors.b9153273.css" rel="stylesheet" />
<link href="/css/app.12f77eb5.css" rel="stylesheet" />
</head>
<body>
<noscript
><strong
>We're sorry but frontend doesn't work properly without JavaScript
enabled. Please enable it to continue.</strong
></noscript
>
<div id="app"></div>
<script src="/js/chunk-vendors.4d54ba89.js"></script>
<script src="/js/app.4a8d2f87.js"></script>
</body>
</html>
<!DOCTYPE html><html lang=""><head><meta charset="utf-8"><meta http-equiv="X-UA-Compatible" content="IE=edge"><meta name="viewport" content="width=device-width,initial-scale=1"><link rel="icon" href="/favicon.ico"><title>frontend</title><link href="/css/app.54db996e.css" rel="preload" as="style"><link href="/css/chunk-vendors.920d8cf8.css" rel="preload" as="style"><link href="/js/app.30780f00.js" rel="preload" as="script"><link href="/js/chunk-vendors.6cd9f85d.js" rel="preload" as="script"><link href="/css/chunk-vendors.920d8cf8.css" rel="stylesheet"><link href="/css/app.54db996e.css" rel="stylesheet"></head><body><noscript><strong>We're sorry but frontend doesn't work properly without JavaScript enabled. Please enable it to continue.</strong></noscript><div id="app"></div><script src="/js/chunk-vendors.6cd9f85d.js"></script><script src="/js/app.30780f00.js"></script></body></html>

2
frontend/dist/js/app.30780f00.js vendored Normal file

File diff suppressed because one or more lines are too long

1
frontend/dist/js/app.30780f00.js.map vendored Normal file

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

16285
frontend/package-lock.json generated

File diff suppressed because it is too large Load Diff

View File

@ -14,9 +14,9 @@
"axios": "^0.21.2",
"bootstrap": "^5.1.1",
"core-js": "^3.6.5",
"element-plus": "^1.0.2-beta.28",
"element-plus": "^1.3.0-beta.5",
"vant": "^3.2.4",
"vue": "^3.2.2",
"vue": "^3.1",
"vue-router": "^4.0.11"
},
"devDependencies": {

View File

@ -1,105 +1,124 @@
<template>
<van-pull-refresh
<!-- <van-pull-refresh
v-model="loading"
@refresh="onRefresh"
pulling-text="下拉重新生成每周菜谱"
loosing-text="可以松手了..."
>
<div v-for:="(daily_recipe, recipe_index) in daily_recipes">
<van-cell-group
inset
:title="formatDate(daily_recipe.date)"
class="daily_recipes"
>
<van-swipe-cell>
<van-row>
<van-col span="3" class="recipe_type">
<van-grid :column-num="1">
<van-grid-item text="肉" class="recipe_type"></van-grid-item>
</van-grid>
</van-col>
<van-col span="21">
<van-grid :column-num="3" clickable>
<van-grid-item
v-for:="recipe in daily_recipe.meat"
:text="recipe.name"
:to="{
name: 'RecipeMobileRecipeDetail',
params: { id: recipe.id },
}"
/>
<van-grid-item
v-for="value in 3 - (daily_recipe.meat.length % 3)"
:key="value"
/>
</van-grid>
</van-col>
</van-row>
<van-row>
<van-col span="3" class="recipe_type">
<van-grid :column-num="1">
<van-grid-item text="菜" class="recipe_type"></van-grid-item>
</van-grid>
</van-col>
<van-col span="21">
<van-grid :column-num="3" clickable>
<van-grid-item
v-for:="recipe in daily_recipe.vegetable"
:text="recipe.name"
:to="{
name: 'RecipeMobileRecipeDetail',
params: { id: recipe.id },
}"
/>
<van-grid-item
v-for="value in 3 - (daily_recipe.vegetable.length % 3)"
:key="value"
/>
</van-grid>
</van-col>
</van-row>
<van-row v-if="daily_recipe.soup.length > 0">
<van-col span="3" class="recipe_type">
<van-grid :column-num="1">
<van-grid-item text="汤" class="recipe_type"></van-grid-item>
</van-grid>
</van-col>
<van-col span="21">
<van-grid :column-num="1" clickable>
<van-grid-item
v-for:="recipe in daily_recipe.soup"
:text="recipe.name"
:to="{
name: 'RecipeMobileRecipeDetail',
params: { id: recipe.id },
}"
/>
</van-grid>
</van-col>
</van-row>
<template #right>
<van-button
square
text="编辑"
type="primary"
class="action-button"
:to="{
name: 'RecipeMobileDailyRecipeDetail',
params: { id: daily_recipe.id },
}"
/>
<van-button
square
text="刷新"
type="danger"
class="action-button"
@click="reGenerateRecipe(daily_recipe.id, recipe_index)"
/>
</template>
</van-swipe-cell>
</van-cell-group>
</div>
</van-pull-refresh>
pull-distance="100"
:head-height="100"
> -->
<div v-for:="(daily_recipe, recipe_index) in daily_recipes">
<van-cell-group inset class="daily_recipes">
<template #title>
<span> {{ formatDate(daily_recipe.date) }}</span>
<van-tag
type="danger"
class="today-tag"
v-if="isToday(daily_recipe.date)"
>今天</van-tag
>
</template>
<van-swipe-cell>
<van-row v-if="daily_recipe.meat">
<van-col span="3" class="recipe_type">
<van-grid :column-num="1">
<van-grid-item text="肉" class="recipe_type"></van-grid-item>
</van-grid>
</van-col>
<van-col span="21">
<van-grid :column-num="3" clickable>
<van-grid-item
v-for:="recipe in daily_recipe.meat"
:text="recipe.name"
:to="{
name: 'RecipeMobileRecipeDetail',
params: { id: recipe.id },
}"
/>
<van-grid-item
v-for="value in 3 - (daily_recipe.meat.length % 3)"
:key="value"
/>
</van-grid>
</van-col>
</van-row>
<van-row v-if="daily_recipe.vegetable">
<van-col span="3" class="recipe_type">
<van-grid :column-num="1">
<van-grid-item text="菜" class="recipe_type"></van-grid-item>
</van-grid>
</van-col>
<van-col span="21">
<van-grid :column-num="3" clickable>
<van-grid-item
v-for:="recipe in daily_recipe.vegetable"
:text="recipe.name"
:to="{
name: 'RecipeMobileRecipeDetail',
params: { id: recipe.id },
}"
/>
<van-grid-item
v-for="value in 3 - (daily_recipe.vegetable.length % 3)"
:key="value"
/>
</van-grid>
</van-col>
</van-row>
<van-row
v-if="daily_recipe.soup !== undefined && daily_recipe.soup.length > 0"
>
<van-col span="3" class="recipe_type">
<van-grid :column-num="1">
<van-grid-item text="汤" class="recipe_type"></van-grid-item>
</van-grid>
</van-col>
<van-col span="21">
<van-grid :column-num="1" clickable>
<van-grid-item
v-for:="recipe in daily_recipe.soup"
:text="recipe.name"
:to="{
name: 'RecipeMobileRecipeDetail',
params: { id: recipe.id },
}"
/>
</van-grid>
</van-col>
</van-row>
<template #right>
<van-button
square
text="编辑"
type="primary"
class="action-button"
:to="{
name: 'RecipeMobileDailyRecipeDetail',
params: { id: daily_recipe.id },
}"
/>
<van-button
square
text="刷新"
type="danger"
class="action-button"
@click="reGenerateRecipe(daily_recipe.id, recipe_index)"
/>
</template>
</van-swipe-cell>
</van-cell-group>
</div>
<!-- </van-pull-refresh> -->
<van-sticky :offset-bottom="60" position="bottom" class="refresh-button">
<van-button
type="primary"
color="linear-gradient(to right, #ff6034, #ee0a24)"
hairline
round
@click="onRefresh"
>刷新</van-button
>
</van-sticky>
</template>
<script>
@ -113,6 +132,8 @@ import {
PullRefresh,
Row,
SwipeCell,
Tag,
Sticky,
} from 'vant';
import axios from 'axios';
import config from '@/config/index';
@ -120,6 +141,7 @@ import config from '@/config/index';
export default {
name: 'RecipeMobileWeekRecipe',
components: {
[Tag.name]: Tag,
[Button.name]: Button,
[Cell.name]: Cell,
[CellGroup.name]: CellGroup,
@ -129,6 +151,7 @@ export default {
[PullRefresh.name]: PullRefresh,
[Row.name]: Row,
[SwipeCell.name]: SwipeCell,
[Sticky.name]: Sticky,
},
data() {
return {
@ -140,13 +163,29 @@ export default {
this.getWeekRecipe(false);
},
methods: {
isToday(date) {
const today = new Date();
var date_ = new Date(date * 1000);
if (
date_.getDate() == today.getDate() &&
date_.getMonth() == today.getMonth() &&
date_.getFullYear() == today.getFullYear()
) {
return true;
}
return false;
},
formatDate(date) {
if (date === undefined) {
return;
}
var date_ = new Date(date * 1000);
var days = ['周日', '周一', '周二', '周三', '周四', '周五', '周六'];
return days[date_.getDay()];
var date_str = days[date_.getDay()];
if (this.isToday(date_)) {
date_str += ' 🌟';
}
return date_str;
},
onRefresh() {
this.getWeekRecipe(true);
@ -197,4 +236,11 @@ export default {
height: 100%;
width: 50%;
}
.today-tag {
margin-left: 10px;
}
.refresh-button {
float: right;
margin-right: 20px;
}
</style>

View File

@ -2,12 +2,19 @@
<el-row justify="center">
<el-col>
<el-table border stripe :data="daily_recipes" max-height="500">
<el-table-column
prop="date"
label="日期"
:formatter="formatDate"
width="50px"
></el-table-column>
<el-table-column label="日期">
<template #default="scope">
{{ formatDate(scope.row, scope.column, scope.row.date) }}
<el-tag
v-if:="isToday(scope.row.date)"
:key="scope"
size="small"
type="danger"
effect="dark"
>今天</el-tag
>
</template>
</el-table-column>
<el-table-column prop="meat" label="肉">
<template #default="scope">
<el-tag
@ -87,11 +94,25 @@ export default {
};
},
mounted() {
console.log(config.publicPath);
console.log(process.env);
axios
.get(config.publicPath + '/recipe/week-recipe/')
.then((response) => (this.daily_recipes = response.data));
},
methods: {
isToday(date) {
const today = new Date();
var date_ = new Date(date * 1000);
if (
date_.getDate() == today.getDate() &&
date_.getMonth() == today.getMonth() &&
date_.getFullYear() == today.getFullYear()
) {
return true;
}
return false;
},
reGenerateRecipe() {
axios
.post(config.publicPath + '/recipe/week-recipe/')

View File

@ -1,5 +1,5 @@
import ElementPlus from 'element-plus'
import 'element-plus/lib/theme-chalk/index.css'
import 'element-plus/dist/index.css'
import locale from 'element-plus/lib/locale/lang/zh-cn'
export default (app) => {

File diff suppressed because it is too large Load Diff

View File

@ -51,8 +51,8 @@ class WeekRecipeListAPI(rest_framework.generics.ListAPIView,
daily_recipe, __ = recipe.models.DailyRecipe.objects.get_or_create(
date=today + datetime.timedelta(days=x)
)
daily_recipe.generate_recipe()
recipes.append(daily_recipe.recipes.values_list('id', flat=True))
daily_recipe.generate_recipe(recipes)
recipes.extend(daily_recipe.recipes.values_list('id', flat=True))
return Response(recipe.models.DailyRecipe.get_week_recipe_data(),
status=status.HTTP_201_CREATED, headers={})

285
scripts/dodo.py Normal file
View File

@ -0,0 +1,285 @@
#!/usr/bin/env python
# --coding:utf-8--
from http.server import BaseHTTPRequestHandler, HTTPServer
import json
from urllib import request
import hashlib
import base64
from Crypto.Cipher import AES
from mastodon import Mastodon
import logging
import redis
import requests
import time
import subprocess
from bs4 import BeautifulSoup
APP_VERIFICATION_TOKEN = 'uKQQiOVMYg2cTgrjkyBmodrHTUaCXzG3'
APP_ID = 'cli_a115fe8b83f9100c'
APP_SECRET = 'yuSQenId0VfvwdZ3qL9wMd8FpCMEUL0u'
ENCRYPT_KEY = '4XfjcA5xou3pztBD4g5V7dgHtr0BBYDE'
EVENT_TYPE = ['im.message.receive_v1']
ADD_GROUP_NAME = True
KEDAI_ID = '107263380636355825'
logging.basicConfig(filename='/root/develop/log/dodo.log', level=logging.INFO)
logger = logging.getLogger('/root/develop/log/dodo.log')
mastodon = Mastodon(
access_token = 'Ug_bUMWCk3RLamOnqYIytmeB0nO6aNfjdmf06mAj2bE',
api_base_url = 'https://nofan.xyz'
)
pool = redis.ConnectionPool(host='localhost', port=6379, decode_responses=True)
redis_cli = redis.Redis(host='localhost', port=6379, decode_responses=True)
class AESCipher(object):
def __init__(self, key):
self.bs = AES.block_size
self.key=hashlib.sha256(AESCipher.str_to_bytes(key)).digest()
@staticmethod
def str_to_bytes(data):
u_type = type(b"".decode('utf8'))
if isinstance(data, u_type):
return data.encode('utf8')
return data
@staticmethod
def _unpad(s):
return s[:-ord(s[len(s) - 1:])]
def decrypt(self, enc):
iv = enc[:AES.block_size]
cipher = AES.new(self.key, AES.MODE_CBC, iv)
return self._unpad(cipher.decrypt(enc[AES.block_size:]))
def decrypt_string(self, enc):
enc = base64.b64decode(enc)
return self.decrypt(enc).decode('utf8')
def get_tenant_access_token(): # 获取token
token = redis_cli.get('tenant_access_token_%s' % APP_ID)
if token:
return token
url = "https://open.feishu.cn/open-apis/auth/v3/tenant_access_token/internal/"
headers = {
"Content-Type": "application/json"
}
req_body = {
"app_id": APP_ID,
"app_secret": APP_SECRET
}
data = bytes(json.dumps(req_body), encoding='utf8')
req = request.Request(url=url, data=data, headers=headers, method='POST')
try:
response = request.urlopen(req)
except Exception as e:
logger.error('get tenant token error: %s', e.read().decode())
return ""
rsp_body = response.read().decode('utf-8')
rsp_dict = json.loads(rsp_body)
code = rsp_dict.get("code", -1)
if code != 0:
logger.error("get tenant_access_token error, code =%s", code)
return ""
token = redis_cli.set('tenant_access_token_%s' % APP_ID,
rsp_dict.get("tenant_access_token", ""),
ex=60*30)
return token
def get_group_name(chat_id):
group_name = redis_cli.get('group_name_%s' % chat_id)
if not group_name:
url = "https://open.feishu.cn/open-apis/im/v1/chats/"
headers = {
"Content-Type": "application/json",
"Authorization": "Bearer " + get_tenant_access_token()
}
try:
resp = requests.get(url+chat_id, headers=headers)
resp_data = resp.json()
code = resp_data.get("code", -1)
if code == 0:
group_name = resp_data.get('data', {}).get('name')
redis_cli.set('group_name_%s' % chat_id,
group_name,
ex=60*60*24)
except:
# todo: log
return
return group_name
class RequestHandler(BaseHTTPRequestHandler):
def do_POST(self):
# 解析请求 body
req_body = self.rfile.read(int(self.headers['content-length']))
obj = json.loads(req_body.decode("utf-8"))
cipher = AESCipher(ENCRYPT_KEY)
obj = json.loads(cipher.decrypt_string(obj['encrypt']))
logger.info('lark request body: %s', obj)
# 校验 verification token 是否匹配token 不匹配说明该回调并非来自开发平台
token = obj.get("token", "")
if not token:
token = obj.get('header', {}).get('token', '')
if token != APP_VERIFICATION_TOKEN:
logger.error("verification token not match, token =%s", token)
self.response("")
return
# 根据 type 处理不同类型事件
type = obj.get("type", "")
if not type:
type = obj.get('header', {}).get('event_type')
if "url_verification" == type: # 验证请求 URL 是否有效
self.handle_request_url_verify(obj)
elif type in EVENT_TYPE: # 事件回调
# 获取事件内容和类型,并进行相应处理,此处只关注给机器人推送的消息事件
event_id = obj.get('header', {}).get('event_id', '')
# 重复收到的事件不处理
if event_id and redis_cli.get(event_id):
self.response("")
return
event = obj.get("event")
if event.get("message"):
self.handle_message(event, event_id)
return
return
def handle_request_url_verify(self, post_obj):
# 原样返回 challenge 字段内容
challenge = post_obj.get("challenge", "")
rsp = {'challenge': challenge}
self.response(json.dumps(rsp))
return
def handle_message(self, event, event_id=None):
# 此处只处理 text 类型消息,其他类型消息忽略
msg = event.get('message', {})
msg_type = msg.get("message_type", "")
if msg_type == "text":
# 调用发消息 API 之前,先要获取 API 调用凭证tenant_access_token
access_token = get_tenant_access_token()
if access_token == "":
self.response("")
return
# 机器人回复收到的消息
text = json.loads(msg.get('content')).get('text')
orig_text = text
if msg.get('chat_type') == 'group' and msg.get('mentions'):
open_id = {"open_chat_id": msg.get("chat_id")}
for mention in msg.get('mentions'):
text = text.replace(mention['key'], '')
text = text.lstrip()
orig_text = text
if ADD_GROUP_NAME:
group_name = get_group_name(msg.get("chat_id"))
text = '%s #%s' % (text, group_name)
else:
open_id = {"open_id": event.get("sender", {}).get(
'sender_id', {}).get('open_id')}
self.response("")
if orig_text.startswith('/'):
redis_cli.set(event_id, int(time.time()), ex=60*60*7)
if orig_text not in ['/last', '/del']:
if not orig_text.startswith('/deploy '):
self.msg_compoment(access_token, open_id, '指令错误')
return
if orig_text == '/last':
try:
statuses = mastodon.account_statuses(KEDAI_ID, limit=1)
s_text = BeautifulSoup(statuses[0]['content'], 'html.parser')
self.msg_compoment(access_token, open_id,
s_text.get_text(''))
except Exception as exc:
logger.error('operation error: %s', str(exc))
elif orig_text == '/del':
try:
statuses = mastodon.account_statuses(KEDAI_ID, limit=1)
Mastodon.status_delete(statuses[0]['id'])
s_text = BeautifulSoup(statuses[0]['content'], 'html.parser')
self.msg_compoment(access_token, open_id,
'已删除: ' + s_text.get_text(''))
except Exception as exc:
logger.error('operation error: %s', str(exc))
elif orig_text.startswith('/deploy '):
site_ = orig_text.split('/deploy ')[1]
if site_ == 'dsite':
self.msg_compoment(access_token, open_id, '🚧 %s 开始部署 🚧' % site_)
subprocess.call("/root/deploy/dsite_prepare.sh")
subprocess.run(["supervisorctl", "restart", "dsite"])
self.msg_compoment(access_token, open_id, '🎉 %s 部署成功 🎉' % site_)
else:
self.msg_compoment(access_token, open_id, '⚠️ %s 不存在 ⚠️' % site_)
return
try:
toot_resp = mastodon.status_post(text)
if toot_resp.get('id'):
self.msg_compoment(access_token, open_id, '📟 dodo 📟')
redis_cli.set(event_id, int(time.time()), ex=60*60*7)
else:
self.msg_compoment(access_token, open_id, """⚠️ didi ⚠️
%s
""" % json.loads(toot_resp))
except Exception as exc:
logger.error('send toot error: %s', str(exc))
return
elif msg_type == "image":
self.response("")
return
def response(self, body):
self.send_response(200)
self.send_header('Content-Type', 'application/json')
self.end_headers()
self.wfile.write(body.encode())
def send_message(self, token, open_id, text):
url = "https://open.feishu.cn/open-apis/message/v4/send/"
headers = {
"Content-Type": "application/json",
"Authorization": "Bearer " + token
}
req_body = {
"msg_type": "text",
"content": {
"text": text
}
}
req_body = dict(req_body, **open_id) # 根据open_id判断返回域
data = bytes(json.dumps(req_body), encoding='utf8')
req = request.Request(url=url, data=data, headers=headers, method='POST')
try:
response = request.urlopen(req)
except Exception as e:
logger.error('send message error: %s', e.read().decode())
return
rsp_body = response.read().decode('utf-8')
rsp_dict = json.loads(rsp_body)
code = rsp_dict.get("code", -1)
if code != 0:
logger.error("send message error, code = %s, msg =%s",
code,
rsp_dict.get("msg", ""))
def msg_compoment(self, token, open_id, text):
self.send_message(token, open_id, text)
def run():
port = 5000
server_address = ('', port)
httpd = HTTPServer(server_address, RequestHandler)
logger.info("start...")
httpd.serve_forever()
if __name__ == '__main__':
run()

0
toot/__init__.py Normal file
View File

3
toot/admin.py Normal file
View File

@ -0,0 +1,3 @@
from django.contrib import admin
# Register your models here.

6
toot/apps.py Normal file
View File

@ -0,0 +1,6 @@
from django.apps import AppConfig
class TootConfig(AppConfig):
default_auto_field = 'django.db.models.BigAutoField'
name = 'toot'

View File

1
toot/models.py Normal file
View File

@ -0,0 +1 @@
from django.db import models

3
toot/tests.py Normal file
View File

@ -0,0 +1,3 @@
from django.test import TestCase
# Create your tests here.

3
toot/views.py Normal file
View File

@ -0,0 +1,3 @@
from django.shortcuts import render
# Create your views here.

View File

@ -19,3 +19,6 @@ RECIPE_TYPE_CHOICE = [
FILTER_EXACT = ['exact']
FILTER_GTE_LTE = ['exact', 'gte', 'gt', 'lte', 'lt']
LARK_WEBHOOK_MSG_TYPE_TEXT = 'text'

25
utils/depoly_notify.py Executable file
View File

@ -0,0 +1,25 @@
# -*- coding: UTF-8 -*-
import sys
import os
from imp import reload
sys.path.insert(0, os.path.abspath('..'))
sys.path.append('/Users/ching/develop/dsite')
os.environ.setdefault("DJANGO_SETTINGS_MODULE", "../dsite.settings")
reload(sys)
import utils.lark
import git
import ipdb
if __name__ == '__main__':
# def notify():
repo = git.Repo(search_parent_directories=True)
commit = repo.head.commit
rev, branch = commit.name_rev.split(' ')
msg = 'rev: %s\n\nauther: %s\n\nbranch: %s\n\nmessage: %s' % (
rev,
commit.author.name,
branch,
commit.summary
)
print(utils.lark.request({'text': msg}))

37
utils/lark.py Normal file
View File

@ -0,0 +1,37 @@
# -*- coding: UTF-8 -*-
from dsite import settings
import utils
from utils import const
import hashlib
import base64
import hmac
import json
import requests
from django.utils.timezone import now
def gen_sign(timestamp, secret):
# 拼接timestamp和secret
string_to_sign = '{}\n{}'.format(timestamp, secret)
hmac_code = hmac.new(string_to_sign.encode("utf-8"), digestmod=hashlib.sha256).digest()
# 对结果进行base64处理
sign = base64.b64encode(hmac_code).decode('utf-8')
return sign
def request(content, msg_type=const.LARK_WEBHOOK_MSG_TYPE_TEXT):
""" content: {'text': 'xxxxx}
"""
timestamp = utils.timestamp_of(now())
data = {
"timestamp": timestamp,
"sign": gen_sign(timestamp, settings.LARK_WEBHOOK_SECRET),
"msg_type": msg_type,
"content": content}
resp = requests.post(settings.LARK_WEBHOOK_URL, data=json.dumps(data))
if resp.status_code == 200 and resp.json().get('StatusCode') == 0:
return True
return False