Compare commits

...

25 Commits

Author SHA1 Message Date
Ching
306e54bcd5 feat(recipe edit): 修改删除菜谱按钮位置
修改删除菜谱按钮位置

Signed-off-by: Ching <loooching@gmail.com>
2022-02-12 15:52:16 +08:00
Ching
aa2f401636 fix(scripts): fix typo
fix typo

Signed-off-by: Ching <loooching@gmail.com>
2022-02-11 22:25:47 +08:00
Ching
f44ced4946 feat(scripts): 修复可能导致重复发送图片的问题;修改 logger 位置
修复可能导致重复发送图片的问题;修改 logger 位置

Signed-off-by: Ching <loooching@gmail.com>
2022-02-11 22:13:17 +08:00
Ching
1cc841f3cc feat(scripts): 增加 Instagram 同步至 mastodon 脚本
增加 Instagram 同步至 mastodon 脚本

Signed-off-by: Ching <loooching@gmail.com>
2022-02-11 21:47:47 +08:00
Ching
cc0c22c68b Merge branch 'develop' of github.com:looching/dsite into develop 2022-02-11 19:54:28 +08:00
Ching
b4b5a0e68a feat(scripts): 增加 instagram 同步 mastodon 脚本
增加 instagram 同步 mastodon 脚本

Signed-off-by: Ching <loooching@gmail.com>
2022-02-11 19:53:35 +08:00
Ching
e7a579e168 fix(recipe models): 修复通过嘟嘟创建菜谱时没有设置 status 导致创建失败的问题
修复通过嘟嘟创建菜谱时没有设置 status 导致创建失败的问题

Signed-off-by: Ching <loooching@gmail.com>
2022-02-10 22:38:12 +08:00
Ching
b26000205b fix(scripts): 修复 bitwarden 备份数据路径有误的问题
修复 bitwarden 备份数据路径有误的问题

Signed-off-by: Ching <loooching@gmail.com>
2022-02-10 22:35:57 +08:00
Ching
1bea995dc1 feat(scripts): 增加将图片转为灰度图的脚本
增加将图片转为灰度图的脚本

Signed-off-by: Ching <loooching@gmail.com>
2022-02-10 15:30:30 +08:00
Ching
844ce1bfd5 feat(scripts): 增加 dsite 部署脚本;修复嘟嘟机部署提示有误的问题
增加 dsite 部署脚本;修复嘟嘟机部署提示有误的问题

Signed-off-by: Ching <loooching@gmail.com>
2022-02-05 02:23:07 +08:00
Ching
83a3bd193b feat(recipe detail page): 菜谱编辑页增加删除按钮
菜谱编辑页增加删除按钮

Signed-off-by: Ching <loooching@gmail.com>
2022-02-05 01:41:03 +08:00
Ching
abce5b18fd feat(recipe api): 增加删除菜谱接口
增加删除菜谱接口

Signed-off-by: Ching <loooching@gmail.com>
2022-02-05 00:38:30 +08:00
Ching
1277accf5c feat(scripts): 增加bitwarden备份脚本
增加bitwarden备份脚本

Signed-off-by: Ching <loooching@gmail.com>
2022-02-04 16:49:23 +08:00
Ching
9b115417d3 feat(recipe mobile): 修改菜谱后弹出成功提示
修改菜谱后弹出成功提示

Signed-off-by: Ching <loooching@gmail.com>
2022-02-03 01:21:24 +08:00
Ching
219e7be4a6 feat(scripts): 增加重启dodo脚本
增加重启dodo脚本

Signed-off-by: Ching <loooching@gmail.com>
2022-02-03 00:49:49 +08:00
Ching
86fdc14b56 fix(dodo): 修复 dodo 部署成功消息没发送的问题
修复 dodo 部署成功消息没发送的问题

Signed-off-by: Ching <loooching@gmail.com>
2022-01-23 23:38:50 +08:00
Ching
effc93b56d feat(dodo): 增加飞书 dodo 部署逻辑
增加飞书 dodo 部署逻辑

Signed-off-by: Ching <loooching@gmail.com>
2022-01-22 21:34:31 +08:00
Ching
f683341f66 style(*.py): use Black as python code formatter
use Black as python code formatter

Signed-off-by: Ching <loooching@gmail.com>
2022-01-22 18:02:32 +08:00
Ching
beb10e00a9 feat(recipe): 修改菜谱飞书卡片样式
修改菜谱飞书卡片样式

Signed-off-by: Ching <loooching@gmail.com>
2022-01-22 00:01:44 +08:00
Ching
a0fa3ac98c fix(dodo): 修复指令执行后当成嘟嘟发送的问题
修复指令执行后当成嘟嘟发送的问题

Signed-off-by: Ching <loooching@gmail.com>
2022-01-21 14:22:22 +08:00
Ching
e51ab984fd fix(dodo): 修复指令识别有误的问题
修复指令识别有误的问题

Signed-off-by: Ching <loooching@gmail.com>
2022-01-21 00:54:46 +08:00
Ching
f3c309e38c fix(dodo): fix import error
fix import error

Signed-off-by: Ching <loooching@gmail.com>
2022-01-21 00:39:08 +08:00
Ching
c8d5195669 Merge branch 'develop' of git.tunpok.com:ching/dsite into develop 2022-01-21 00:14:39 +08:00
Ching
ea42cef954 feat(dodo): 增加创建菜谱功能
增加创建菜谱功能

Signed-off-by: Ching <loooching@gmail.com>
2022-01-21 00:14:30 +08:00
Ching
33631e2b94 fix(dodo): 修复删除嘟文时报错的问题
修复删除嘟文时报错的问题

Signed-off-by: Ching <loooching@gmail.com>
2022-01-19 09:35:45 +08:00
31 changed files with 1026 additions and 534 deletions

13
.vscode/settings.json vendored
View File

@ -1,2 +1,15 @@
{ {
"python.formatting.provider": "black",
"python.formatting.blackArgs": [
"-S",
"-C",
"-l 120"
],
"editor.formatOnPaste": true,
"editor.formatOnSave": true,
"editor.rulers": [
120
],
"editor.bracketPairColorization.enabled": true,
"editor.guides.bracketPairs": "active"
} }

0
a.txt
View File

View File

@ -2,12 +2,11 @@
from rest_framework import pagination from rest_framework import pagination
from rest_framework.response import Response from rest_framework.response import Response
class PagePaginationWithPageCount(pagination.PageNumberPagination): class PagePaginationWithPageCount(pagination.PageNumberPagination):
page_size_query_param = 'page_size' page_size_query_param = 'page_size'
def get_paginated_response(self, data):
response = super().get_paginated_response(data)
response.data['page_count'] = self.page.paginator.num_pages
return response
def get_paginated_response(self, data):
response = super().get_paginated_response(data)
response.data['page_count'] = self.page.paginator.num_pages
return response

View File

@ -31,3 +31,5 @@ user-agents==2.2.0
wcwidth==0.2.5 wcwidth==0.2.5
zipp==3.5.0 zipp==3.5.0
redis==4.1.0 redis==4.1.0
black
instagram_private_api

View File

@ -37,14 +37,11 @@ INSTALLED_APPS = [
'django.contrib.sessions', 'django.contrib.sessions',
'django.contrib.messages', 'django.contrib.messages',
'django.contrib.staticfiles', 'django.contrib.staticfiles',
# third party # third party
'corsheaders', 'corsheaders',
'rest_framework', 'rest_framework',
'rest_framework.authtoken', 'rest_framework.authtoken',
'django_filters', 'django_filters',
# user apps # user apps
'timer', 'timer',
'recipe', 'recipe',
@ -74,9 +71,9 @@ TEMPLATES = [
'django.template.context_processors.request', 'django.template.context_processors.request',
'django.contrib.auth.context_processors.auth', 'django.contrib.auth.context_processors.auth',
'django.contrib.messages.context_processors.messages', 'django.contrib.messages.context_processors.messages',
], ]
}, },
}, }
] ]
WSGI_APPLICATION = 'dsite.wsgi.application' WSGI_APPLICATION = 'dsite.wsgi.application'
@ -85,30 +82,17 @@ WSGI_APPLICATION = 'dsite.wsgi.application'
# Database # Database
# https://docs.djangoproject.com/en/1.11/ref/settings/#databases # https://docs.djangoproject.com/en/1.11/ref/settings/#databases
DATABASES = { DATABASES = {'default': {'ENGINE': 'django.db.backends.sqlite3', 'NAME': os.path.join(BASE_DIR, 'db.sqlite3')}}
'default': {
'ENGINE': 'django.db.backends.sqlite3',
'NAME': os.path.join(BASE_DIR, 'db.sqlite3'),
}
}
# Password validation # Password validation
# https://docs.djangoproject.com/en/1.11/ref/settings/#auth-password-validators # https://docs.djangoproject.com/en/1.11/ref/settings/#auth-password-validators
AUTH_PASSWORD_VALIDATORS = [ AUTH_PASSWORD_VALIDATORS = [
{ {'NAME': 'django.contrib.auth.password_validation.UserAttributeSimilarityValidator'},
'NAME': 'django.contrib.auth.password_validation.UserAttributeSimilarityValidator', {'NAME': 'django.contrib.auth.password_validation.MinimumLengthValidator'},
}, {'NAME': 'django.contrib.auth.password_validation.CommonPasswordValidator'},
{ {'NAME': 'django.contrib.auth.password_validation.NumericPasswordValidator'},
'NAME': 'django.contrib.auth.password_validation.MinimumLengthValidator',
},
{
'NAME': 'django.contrib.auth.password_validation.CommonPasswordValidator',
},
{
'NAME': 'django.contrib.auth.password_validation.NumericPasswordValidator',
},
] ]
@ -140,11 +124,10 @@ REST_FRAMEWORK = {
# 'DEFAULT_PERMISSION_CLASSES': [ # 'DEFAULT_PERMISSION_CLASSES': [
# 'rest_framework.permissions.IsAuthenticatedOrReadOnly' # 'rest_framework.permissions.IsAuthenticatedOrReadOnly'
# ], # ],
# 'DEFAULT_PAGINATION_CLASS': 'rest_framework.pagination.PageNumberPagination', # 'DEFAULT_PAGINATION_CLASS': 'rest_framework.pagination.PageNumberPagination',
'DEFAULT_PAGINATION_CLASS': 'core.pagination.PagePaginationWithPageCount', 'DEFAULT_PAGINATION_CLASS': 'core.pagination.PagePaginationWithPageCount',
'PAGE_SIZE': 10, 'PAGE_SIZE': 10,
'DEFAULT_FILTER_BACKENDS': ['django_filters.rest_framework.DjangoFilterBackend'] 'DEFAULT_FILTER_BACKENDS': ['django_filters.rest_framework.DjangoFilterBackend'],
} }
# CORS # CORS
@ -153,9 +136,16 @@ REST_FRAMEWORK = {
# 'http://localhost:8080', # 'http://localhost:8080',
# ) # )
CORS_ALLOWED_ORIGINS = [] CORS_ALLOWED_ORIGINS = []
CORS_ALLOW_CREDENTIALS = True # 允许携带cookie CORS_ALLOW_CREDENTIALS = True # 允许携带cookie
# CORS_ALLOW_ALL_ORIGINS = False # If this is used then `CORS_ALLOWED_ORIGINS` will not have any effect # CORS_ALLOW_ALL_ORIGINS = False # If this is used then `CORS_ALLOWED_ORIGINS` will not have any effect
LARK_WEBHOOK_URL = '' LARK_WEBHOOK_URL = ''
LARK_WEBHOOK_SECRET = '' LARK_WEBHOOK_SECRET = ''
MASTODON_SYNCED_IMAGES_LOG = ''
IG_PRIVATE_API_SETTINGS = ''
IG_LOGIN_USERNAME = ''
IG_LOGIN_PASSWORD = ''
MASTODON_NOFAN_ACCESS_TOKEN = ''

View File

@ -1,5 +1,5 @@
<template> <template>
<el-row justify="left"> <el-row justify="left" gutter="10">
<el-col> <el-col>
<el-form :rules="rules" ref="form" :model="form" label-position="left"> <el-form :rules="rules" ref="form" :model="form" label-position="left">
<el-form-item label="名字" prop="name"> <el-form-item label="名字" prop="name">
@ -54,13 +54,33 @@
<el-input type="textarea" v-model="form.note"></el-input> <el-input type="textarea" v-model="form.note"></el-input>
</el-form-item> </el-form-item>
<el-form-item> <el-form-item>
<el-button <el-col :span="8" v-if="recipe_id">
type="primary" <el-button
plain type="danger"
class="summit-recipe" plain
@click="onSubmit(recipe_id)" class="summit-recipe"
>提交</el-button @click="onSubmitDelete(recipe_id)"
> >删除</el-button
>
</el-col>
<el-col :span="16" v-if="recipe_id">
<el-button
type="primary"
plain
class="summit-recipe"
@click="onSubmit(recipe_id)"
>提交</el-button
>
</el-col>
<el-col :span="24" v-else>
<el-button
type="primary"
plain
class="summit-recipe"
@click="onSubmit(recipe_id)"
>提交</el-button
>
</el-col>
</el-form-item> </el-form-item>
</el-form> </el-form>
</el-col> </el-col>
@ -69,6 +89,8 @@
<script> <script>
import axios from 'axios'; import axios from 'axios';
import config from '@/config/index'; import config from '@/config/index';
import { ElMessage } from 'element-plus';
export default { export default {
props: ['recipe_'], props: ['recipe_'],
watch: { watch: {
@ -77,7 +99,7 @@ export default {
this.recipe_id = val.id; this.recipe_id = val.id;
}, },
}, },
data: function() { data: function () {
return { return {
form: { form: {
name: null, name: null,
@ -104,23 +126,41 @@ export default {
if (!recipe_id) { if (!recipe_id) {
axios axios
.post(config.publicPath + '/recipe/recipe/', data) .post(config.publicPath + '/recipe/recipe/', data)
.then(function() { .then(function () {
ElMessage({
message: '创建成功.',
type: 'success',
});
location.reload(); location.reload();
}) })
.catch(function(error) { .catch(function (error) {
console.log(error); console.log(error);
}); });
} else { } else {
axios axios
.put(config.publicPath + '/recipe/recipe/' + recipe_id, data) .put(config.publicPath + '/recipe/recipe/' + recipe_id, data)
.then(function() { .then(function () {
location.reload(); ElMessage({
message: '修改成功.',
type: 'success',
});
}) })
.catch(function(error) { .catch(function (error) {
console.log(error); console.log(error);
}); });
} }
}, },
onSubmitDelete(recipe_id) {
axios
.delete(config.publicPath + '/recipe/recipe/' + recipe_id)
.then(function () {
ElMessage.error('删除成功.');
location.reload();
})
.catch(function (error) {
console.log(error);
});
},
}, },
}; };
</script> </script>
@ -128,5 +168,6 @@ export default {
<style scoped> <style scoped>
.summit-recipe { .summit-recipe {
width: 100%; width: 100%;
margin-bottom: 10px;
} }
</style> </style>

View File

@ -43,23 +43,64 @@
/> />
</van-cell-group> </van-cell-group>
<div class="recipe-create"> <div class="recipe-create">
<van-button <van-row gutter="20">
round <van-col span="8" v-if="recipe_id">
type="primary" <van-button
block class="submit-button"
plain round
hairline type="danger"
:disabled="disable_submit" plain
@click="onSubmit(recipe_id)" hairline
:loading="loading" :disabled="disable_submit"
>提交</van-button @click="onSubmitDelete(recipe_id)"
> :loading="loading"
>删除</van-button
>
</van-col>
<van-col span="16" v-if="recipe_id">
<van-button
class="submit-button"
round
type="primary"
plain
hairline
:disabled="disable_submit"
@click="onSubmit(recipe_id)"
:loading="loading"
>提交</van-button
>
</van-col>
<van-col span="24" v-else>
<van-button
class="submit-button"
round
type="primary"
plain
hairline
:disabled="disable_submit"
@click="onSubmit(recipe_id)"
:loading="loading"
>提交</van-button
>
</van-col>
</van-row>
</div> </div>
</van-form> </van-form>
</template> </template>
<script> <script>
import { Form, Field, CellGroup, Radio, RadioGroup, Rate, Button } from 'vant'; import {
Form,
Field,
CellGroup,
Radio,
RadioGroup,
Rate,
Button,
Toast,
Col,
Row,
} from 'vant';
import axios from 'axios'; import axios from 'axios';
import config from '@/config/index'; import config from '@/config/index';
import router from '@/router/index'; import router from '@/router/index';
@ -81,6 +122,8 @@ export default {
[RadioGroup.name]: RadioGroup, [RadioGroup.name]: RadioGroup,
[Rate.name]: Rate, [Rate.name]: Rate,
[Button.name]: Button, [Button.name]: Button,
[Col.name]: Col,
[Row.name]: Row,
}, },
data() { data() {
return { return {
@ -118,11 +161,30 @@ export default {
(response) => (response, router.push({ name: 'RecipeMobileHome' })) (response) => (response, router.push({ name: 'RecipeMobileHome' }))
); );
} else { } else {
axios axios.put(config.publicPath + '/recipe/recipe/' + recipe_id, data).then(
.put(config.publicPath + '/recipe/recipe/' + recipe_id, data) (Toast.success({
.then((this.loading = false)); message: '修改成功',
forbidClick: true,
duration: 500,
}),
(this.loading = false))
);
} }
}, },
onSubmitDelete(recipe_id) {
if (!this.form.name) {
return;
}
this.loading = true;
axios.delete(config.publicPath + '/recipe/recipe/' + recipe_id).then(
(Toast.success({
message: '删除成功',
forbidClick: true,
duration: 500,
}),
(this.loading = false))
);
},
}, },
}; };
</script> </script>
@ -130,4 +192,7 @@ export default {
.recipe-create { .recipe-create {
margin: 20px 16px; margin: 20px 16px;
} }
.submit-button {
width: 100%;
}
</style> </style>

View File

@ -7,8 +7,7 @@ class Migration(migrations.Migration):
initial = True initial = True
dependencies = [ dependencies = []
]
operations = [ operations = [
migrations.CreateModel( migrations.CreateModel(
@ -21,5 +20,5 @@ class Migration(migrations.Migration):
('rate', models.IntegerField(default=0)), ('rate', models.IntegerField(default=0)),
('difficulty', models.IntegerField(default=0)), ('difficulty', models.IntegerField(default=0)),
], ],
), )
] ]

View File

@ -5,15 +5,11 @@ from django.db import migrations, models
class Migration(migrations.Migration): class Migration(migrations.Migration):
dependencies = [ dependencies = [('recipe', '0001_initial')]
('recipe', '0001_initial'),
]
operations = [ operations = [
migrations.AlterField( migrations.AlterField(
model_name='recipe', model_name='recipe', name='recipe_type', field=models.CharField(default='meat', max_length=32)
name='recipe_type',
field=models.CharField(default='meat', max_length=32),
), ),
migrations.CreateModel( migrations.CreateModel(
name='DailyRecipe', name='DailyRecipe',

View File

@ -0,0 +1,18 @@
# Generated by Django 3.2.6 on 2022-02-04 16:13
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('recipe', '0002_auto_20211002_1926'),
]
operations = [
migrations.AddField(
model_name='recipe',
name='status',
field=models.CharField(default='active', max_length=32),
),
]

View File

@ -6,117 +6,195 @@ from utils import const
import utils import utils
class Recipe(models.Model): class Recipe(models.Model):
name = models.CharField(max_length=128) name = models.CharField(max_length=128)
recipe_type = models.CharField(max_length=32, recipe_type = models.CharField(max_length=32, default=const.RECIPE_TYPE_MEAT)
default=const.RECIPE_TYPE_MEAT) status = models.CharField(max_length=32, default=const.RECIPE_STATUS_ACTIVE)
note = models.TextField(null=True) note = models.TextField(null=True)
rate = models.IntegerField(default=0) rate = models.IntegerField(default=0)
difficulty = models.IntegerField(default=0) difficulty = models.IntegerField(default=0)
def serialize(self, verbose=False): def serialize(self, verbose=False):
data = { data = {'id': self.id, 'name': self.name, 'recipe_type': self.recipe_type}
'id': self.id, if verbose:
'name': self.name, data.update({'difficulty': self.difficulty, 'rate': self.rate, 'note': self.note})
'recipe_type': self.recipe_type,
}
if verbose:
data.update({
'difficulty': self.difficulty,
'rate': self.rate,
'note': self.note,
})
return data return data
@classmethod
def create_from_str(cls, content):
content = content.strip()
if not content:
return
name, *data = content.split(' ')
recipe_type = rate = difficulty = None
keys = [recipe_type, rate, difficulty]
for x in range(len(data)):
keys[x] = data[x]
recipe_type, rate, difficulty = keys
if recipe_type == '':
recipe_type = const.RECIPE_TYPE_MEAT
elif recipe_type == '':
recipe_type = const.RECIPE_TYPE_VEGETABLE
elif recipe_type == '':
recipe_type = const.RECIPE_TYPE_SOUP
else:
recipe_type = const.RECIPE_TYPE_MEAT
if rate:
try:
rate = int(rate)
except:
rate = 0
else:
rate = 0
if difficulty:
try:
difficulty = int(difficulty)
except:
difficulty = 0
else:
difficulty = 0
recipe = cls.objects.create(
name=name, recipe_type=recipe_type, rate=rate, difficulty=difficulty, status=const.RECIPE_STATUS_ACTIVE
)
return recipe
@property
def display_recipe_type(self):
if self.recipe_type == const.RECIPE_TYPE_VEGETABLE:
return ''
elif self.recipe_type == const.RECIPE_TYPE_MEAT:
return ''
elif self.recipe_type == const.RECIPE_TYPE_SOUP:
return ''
return ''
@property
def display_rate(self):
return '🍚' * self.rate
@property
def display_difficult(self):
return '⭐️' * self.difficulty
def construct_lart_card(self):
data = {
"config": {"wide_screen_mode": True},
"header": {"title": {"tag": "plain_text", "content": self.name}, "template": "blue"},
"elements": [
{
"tag": "markdown",
"content": "**类型**%s\n**评分**%s\n**难度**%s"
% (self.display_recipe_type, self.display_rate, self.display_difficult),
},
{
"tag": "action",
"actions": [
{
"tag": "button",
"text": {"tag": "plain_text", "content": "查看"},
"multi_url": {
"url": "https://recipe.tunpok.com/recipe/%s" % self.id,
"android_url": "https://recipe.tunpok.com/recipe-mobile/recipe/%s" % self.id,
"ios_url": "https://recipe.tunpok.com/recipe-mobile/recipe/%s" % self.id,
"pc_url": "https://recipe.tunpok.com/recipe/%s" % self.id,
},
"type": "primary",
},
{"tag": "button", "text": {"tag": "plain_text", "content": "删除"}, "type": "danger"},
],
},
],
}
return data
class DailyRecipe(models.Model): class DailyRecipe(models.Model):
recipes = models.ManyToManyField(Recipe) recipes = models.ManyToManyField(Recipe)
date = models.DateField() date = models.DateField()
meal_type = models.CharField( meal_type = models.CharField(max_length=32, default=const.DAILY_RECIPE_MEAL_TYPE_SUPPER)
max_length=32,
default=const.DAILY_RECIPE_MEAL_TYPE_SUPPER,)
def generate_recipe(self, prev_recipes=None, ignore_prev=False): def generate_recipe(self, prev_recipes=None, ignore_prev=False):
if not prev_recipes: if not prev_recipes:
prev_recipes = [] prev_recipes = []
if ignore_prev: if ignore_prev:
prev_recipes = [] prev_recipes = []
recipes = [] recipes = []
retry = 5 retry = 5
# meat # meat
for x in range(0,2): for x in range(0, 2):
while True: while True:
recipe = Recipe.objects.filter( recipe = (
recipe_type=const.RECIPE_TYPE_MEAT, Recipe.objects.filter(recipe_type=const.RECIPE_TYPE_MEAT)
).order_by('?').first() .exclude(status=const.RECIPE_STATUS_DELETED)
if recipe and recipe.id not in recipes and recipe.id not in prev_recipes: .order_by('?')
recipes.append(recipe.id) .first()
break )
else: if recipe and recipe.id not in recipes and recipe.id not in prev_recipes:
retry -= 1 recipes.append(recipe.id)
if retry <= 0: break
retry = 5 else:
break retry -= 1
if retry <= 0:
retry = 5
break
# vegetable # vegetable
for x in range(0, 1): for x in range(0, 1):
while True: while True:
recipe = Recipe.objects.filter( recipe = (
recipe_type=const.RECIPE_TYPE_VEGETABLE, Recipe.objects.filter(recipe_type=const.RECIPE_TYPE_VEGETABLE)
).order_by('?').first() .exclude(status=const.RECIPE_STATUS_DELETED)
if recipe and recipe.id not in recipes and recipe.id not in prev_recipes: .order_by('?')
recipes.append(recipe.id) .first()
break )
else: if recipe and recipe.id not in recipes and recipe.id not in prev_recipes:
retry -= 1 recipes.append(recipe.id)
if retry <= 0: break
retry = 5 else:
break retry -= 1
if retry <= 0:
retry = 5
break
# soup # soup
if random.randint(0,2): if random.randint(0, 2):
recipe = Recipe.objects.filter( recipe = (
recipe_type=const.RECIPE_TYPE_SOUP, Recipe.objects.filter(recipe_type=const.RECIPE_TYPE_SOUP)
).order_by('?').first() .exclude(status=const.RECIPE_STATUS_DELETED)
# if recipe not in recipes and recipe not in prev_recipes: .order_by('?')
if recipe: .first()
recipes.append(recipe.id) )
# if recipe not in recipes and recipe not in prev_recipes:
if recipe:
recipes.append(recipe.id)
self.recipes.set(Recipe.objects.filter(id__in=recipes)) self.recipes.set(Recipe.objects.filter(id__in=recipes))
def serialize(self): def serialize(self):
data = { data = {const.RECIPE_TYPE_MEAT: [], const.RECIPE_TYPE_VEGETABLE: [], const.RECIPE_TYPE_SOUP: []}
const.RECIPE_TYPE_MEAT: [], for key_, value_ in data.items():
const.RECIPE_TYPE_VEGETABLE: [], for recipe in self.recipes.filter(recipe_type=key_).order_by('id'):
const.RECIPE_TYPE_SOUP: [], value_.append(recipe.serialize())
}
for key_, value_ in data.items():
for recipe in self.recipes.filter(
recipe_type=key_).order_by('id'):
value_.append(recipe.serialize())
date = now() date = now()
date = date.replace(year=self.date.year, month=self.date.month, date = date.replace(year=self.date.year, month=self.date.month, day=self.date.day, hour=0, minute=0)
day=self.date.day, hour=0, minute=0) data['date'] = utils.timestamp_of(utils.day_start(date))
data['date'] = utils.timestamp_of(utils.day_start(date)) data['id'] = self.id
data['id'] = self.id return data
return data
@classmethod @classmethod
def get_week_recipe_data(cls): def get_week_recipe_data(cls):
today = localtime() today = localtime()
week_start = (today - datetime.timedelta(days=today.weekday())).date() week_start = (today - datetime.timedelta(days=today.weekday())).date()
week_end = week_start + datetime.timedelta(days=6) week_end = week_start + datetime.timedelta(days=6)
daily_recipes = cls.objects.filter( daily_recipes = cls.objects.filter(date__gte=week_start, date__lte=week_end).order_by('date')
date__gte=week_start, data = [{}] * (7 - len(daily_recipes))
date__lte=week_end,
).order_by('date')
data = [{}] * (7 - len(daily_recipes))
for daily_recipe in daily_recipes: for daily_recipe in daily_recipes:
data.append(daily_recipe.serialize()) data.append(daily_recipe.serialize())
return data return data

View File

@ -4,23 +4,25 @@ from rest_framework import serializers
import recipe.models import recipe.models
class RecipeSerializer(serializers.ModelSerializer): class RecipeSerializer(serializers.ModelSerializer):
id = serializers.IntegerField(read_only=True) id = serializers.IntegerField(read_only=True)
class Meta:
model = recipe.models.Recipe class Meta:
fields = '__all__' model = recipe.models.Recipe
fields = '__all__'
class WeekRecipeSerializer(serializers.ModelSerializer): class WeekRecipeSerializer(serializers.ModelSerializer):
class Meta:
class Meta: model = recipe.models.DailyRecipe
model = recipe.models.DailyRecipe fields = '__all__'
fields = '__all__'
class DailyRecipeSerializer(serializers.ModelSerializer): class DailyRecipeSerializer(serializers.ModelSerializer):
id = serializers.IntegerField(read_only=True) id = serializers.IntegerField(read_only=True)
recipes = RecipeSerializer(many=True) recipes = RecipeSerializer(many=True)
class Meta:
model = recipe.models.DailyRecipe class Meta:
fields = '__all__' model = recipe.models.DailyRecipe
fields = '__all__'

View File

@ -1,4 +1,5 @@
from django.conf.urls import include, url from django.conf.urls import include, url
# from django.core.urlresolvers import reverse # from django.core.urlresolvers import reverse
from django.urls import path from django.urls import path
from rest_framework import routers from rest_framework import routers
@ -11,6 +12,5 @@ urlpatterns = [
url(r'^recipe/(?P<pk>\d+)$', views.RecipeAPI.as_view(), name='recipe-detail'), url(r'^recipe/(?P<pk>\d+)$', views.RecipeAPI.as_view(), name='recipe-detail'),
url(r'^recipe/$', views.RecipeListAPI.as_view()), url(r'^recipe/$', views.RecipeListAPI.as_view()),
url(r'^week-recipe/$', views.WeekRecipeListAPI.as_view()), url(r'^week-recipe/$', views.WeekRecipeListAPI.as_view()),
url(r'^daily-recipe/(?P<pk>\d+)$', views.DailyRecipeAPI.as_view(), url(r'^daily-recipe/(?P<pk>\d+)$', views.DailyRecipeAPI.as_view(), name='dailyrecipe-detail'),
name='dailyrecipe-detail'),
] ]

View File

@ -11,73 +11,70 @@ import recipe.models
import recipe.serializers import recipe.serializers
from utils import const from utils import const
class RecipeAPI(rest_framework.generics.RetrieveUpdateAPIView):
# authentication_classes = (authentication.TokenAuthentication, class RecipeAPI(rest_framework.generics.RetrieveUpdateDestroyAPIView):
# authentication.SessionAuthentication,
# authentication.BasicAuthentication) # authentication_classes = (authentication.TokenAuthentication,
# permission_classes = (permissions.IsAuthenticated,) # authentication.SessionAuthentication,
queryset = recipe.models.Recipe.objects.all() # authentication.BasicAuthentication)
serializer_class = recipe.serializers.RecipeSerializer # permission_classes = (permissions.IsAuthenticated,)
queryset = recipe.models.Recipe.objects.all()
serializer_class = recipe.serializers.RecipeSerializer
def perform_destroy(self, instance):
instance.status = const.RECIPE_STATUS_DELETED
instance.save(update_fields=['status'])
class RecipeListAPI(rest_framework.generics.ListAPIView, class RecipeListAPI(rest_framework.generics.ListAPIView, rest_framework.generics.CreateAPIView):
rest_framework.generics.CreateAPIView):
# authentication_classes = (authentication.TokenAuthentication, # authentication_classes = (authentication.TokenAuthentication,
# authentication.SessionAuthentication, # authentication.SessionAuthentication,
# authentication.BasicAuthentication) # authentication.BasicAuthentication)
# permission_classes = (permissions.IsAuthenticated,) # permission_classes = (permissions.IsAuthenticated,)
queryset = recipe.models.Recipe.objects.all() queryset = recipe.models.Recipe.objects.exclude(status=const.RECIPE_STATUS_DELETED)
serializer_class = recipe.serializers.RecipeSerializer serializer_class = recipe.serializers.RecipeSerializer
filterset_fields = { filterset_fields = {
'recipe_type': const.FILTER_EXACT, 'recipe_type': const.FILTER_EXACT,
'difficulty': const.FILTER_GTE_LTE, 'difficulty': const.FILTER_GTE_LTE,
'rate': const.FILTER_GTE_LTE, 'rate': const.FILTER_GTE_LTE,
} }
class WeekRecipeListAPI(rest_framework.generics.ListAPIView, class WeekRecipeListAPI(rest_framework.generics.ListAPIView, rest_framework.generics.CreateAPIView):
rest_framework.generics.CreateAPIView):
queryset = recipe.models.DailyRecipe.objects.all() queryset = recipe.models.DailyRecipe.objects.all()
serializer_class = recipe.serializers.WeekRecipeSerializer serializer_class = recipe.serializers.WeekRecipeSerializer
def post(self, request, *args, **kwargs): def post(self, request, *args, **kwargs):
# Monday == 0 ... Sunday == 6 # Monday == 0 ... Sunday == 6
today = localtime() today = localtime()
recipes = [] recipes = []
for x in range(0, 7-today.weekday()): for x in range(0, 7 - today.weekday()):
daily_recipe, __ = recipe.models.DailyRecipe.objects.get_or_create( daily_recipe, __ = recipe.models.DailyRecipe.objects.get_or_create(date=today + datetime.timedelta(days=x))
date=today + datetime.timedelta(days=x) daily_recipe.generate_recipe(recipes)
) recipes.extend(daily_recipe.recipes.values_list('id', flat=True))
daily_recipe.generate_recipe(recipes) return Response(recipe.models.DailyRecipe.get_week_recipe_data(), status=status.HTTP_201_CREATED, headers={})
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={})
def get(self, request, *args, **kwargs): def get(self, request, *args, **kwargs):
data = recipe.models.DailyRecipe.get_week_recipe_data() data = recipe.models.DailyRecipe.get_week_recipe_data()
return Response(data) return Response(data)
class DailyRecipeAPI(rest_framework.generics.RetrieveUpdateAPIView): class DailyRecipeAPI(rest_framework.generics.RetrieveUpdateAPIView):
queryset = recipe.models.DailyRecipe.objects.all() queryset = recipe.models.DailyRecipe.objects.all()
serializer_class = recipe.serializers.DailyRecipeSerializer serializer_class = recipe.serializers.DailyRecipeSerializer
def post(self, request, *args, **kwargs): def post(self, request, *args, **kwargs):
daily_recipe = recipe.models.DailyRecipe.objects.get(id=kwargs['pk']) daily_recipe = recipe.models.DailyRecipe.objects.get(id=kwargs['pk'])
daily_recipe.generate_recipe() daily_recipe.generate_recipe()
return Response(daily_recipe.serialize(), status=status.HTTP_201_CREATED, return Response(daily_recipe.serialize(), status=status.HTTP_201_CREATED, headers={})
headers={})
def put(self, request, *args, **kwargs): def put(self, request, *args, **kwargs):
daily_recipe = recipe.models.DailyRecipe.objects.get(id=kwargs['pk']) daily_recipe = recipe.models.DailyRecipe.objects.get(id=kwargs['pk'])
recipes = request.data.get('meat',[]) recipes = request.data.get('meat', [])
recipes.extend(request.data.get('vegetable', [])) recipes.extend(request.data.get('vegetable', []))
recipes.extend(request.data.get('soup', [])) recipes.extend(request.data.get('soup', []))
daily_recipe.recipes.set(recipe.models.Recipe.objects.filter( daily_recipe.recipes.set(recipe.models.Recipe.objects.filter(id__in=recipes))
id__in=recipes)) return Response(daily_recipe.serialize(), status=status.HTTP_201_CREATED, headers={})
return Response(daily_recipe.serialize(), status=status.HTTP_201_CREATED,
headers={})

View File

@ -0,0 +1,83 @@
# coding: utf-8
from datetime import datetime
from dateutil import tz
from email import encoders
from email.header import Header
from email.mime.base import MIMEBase
from email.mime.multipart import MIMEMultipart
from email.mime.text import MIMEText
from email.utils import parseaddr, formataddr
import os, tarfile
import smtplib
to_zone = tz.gettz('Asia/Shanghai')
localtime = datetime.now(tz=to_zone)
def make_targz(output_filename, source_dir):
"""
一次性打包目录为tar.gz
:param output_filename: 压缩文件名
:param source_dir: 需要打包的目录
:return:
"""
try:
with tarfile.open(output_filename, "w:gz") as tar:
tar.add(source_dir, arcname=os.path.basename(source_dir))
return True
except Exception as e:
print(e)
return False
def send_email(file_name):
def _format_addr(s):
name, addr = parseaddr(s)
return formataddr((Header(name, 'utf-8').encode(), addr))
smtp_server = 'smtp.gmail.com'
smtp_port = 587
server = smtplib.SMTP(smtp_server, smtp_port)
server.starttls()
# 剩下的代码和前面的一模一样:
# server.set_debuglevel(1)
from_addr = ''
to_addr = ''
to_zone = tz.gettz('Asia/Shanghai')
localtime = datetime.now(tz=to_zone)
msg = MIMEMultipart()
msg['From'] = _format_addr('Vaultwarden <%s>' % from_addr)
msg['To'] = _format_addr('<%s>' % to_addr)
msg['Subject'] = Header('bitwarden 备份 %s' % localtime.strftime('%Y-%m-%d'), 'utf-8').encode()
# 邮件正文是MIMEText:
msg.attach(MIMEText('backup file attached', 'plain', 'utf-8'))
# 添加附件就是加上一个MIMEBase从本地读取一个图片:
with open(file_name, 'rb') as f:
# 设置附件的MIME和文件名
mime = MIMEBase('tar.gz', 'tar.gz', filename=file_name)
# 加上必要的头信息:
mime.add_header('Content-Disposition', 'attachment', filename=file_name)
mime.add_header('Content-ID', '<0>')
mime.add_header('X-Attachment-Id', '0')
# 把附件的内容读进来:
mime.set_payload(f.read())
# 用Base64编码:
encoders.encode_base64(mime)
# 添加到MIMEMultipart:
msg.attach(mime)
server.login(from_email, password)
server.sendmail(from_addr, [to_addr], msg.as_string())
server.quit()
if __name__ == '__main__':
file_name = '/root/develop/vaultwarden/backup/bitwarden-%s.tar.gz' % localtime.strftime('%Y-%m-%d')
if make_targz(file_name, '/root/develop/vaultwarden/vw-data'):
send_email(file_name)

View File

@ -1,5 +1,14 @@
#!/usr/bin/env python #!/usr/bin/env python
# --coding:utf-8-- # --coding:utf-8--
import os
import sys
os.environ.setdefault("DJANGO_SETTINGS_MODULE", "dsite.settings")
sys.path.insert(0, '../')
sys.path.insert(0, './')
from django.core.wsgi import get_wsgi_application
get_wsgi_application()
from http.server import BaseHTTPRequestHandler, HTTPServer from http.server import BaseHTTPRequestHandler, HTTPServer
import json import json
@ -15,6 +24,9 @@ import time
import subprocess import subprocess
from bs4 import BeautifulSoup from bs4 import BeautifulSoup
import recipe.models
from utils import const
APP_VERIFICATION_TOKEN = 'uKQQiOVMYg2cTgrjkyBmodrHTUaCXzG3' APP_VERIFICATION_TOKEN = 'uKQQiOVMYg2cTgrjkyBmodrHTUaCXzG3'
APP_ID = 'cli_a115fe8b83f9100c' APP_ID = 'cli_a115fe8b83f9100c'
APP_SECRET = 'yuSQenId0VfvwdZ3qL9wMd8FpCMEUL0u' APP_SECRET = 'yuSQenId0VfvwdZ3qL9wMd8FpCMEUL0u'
@ -26,48 +38,46 @@ KEDAI_ID = '107263380636355825'
logging.basicConfig(filename='/root/develop/log/dodo.log', level=logging.INFO) logging.basicConfig(filename='/root/develop/log/dodo.log', level=logging.INFO)
logger = logging.getLogger('/root/develop/log/dodo.log') logger = logging.getLogger('/root/develop/log/dodo.log')
mastodon = Mastodon( mastodon_cli = Mastodon(access_token='Ug_bUMWCk3RLamOnqYIytmeB0nO6aNfjdmf06mAj2bE', api_base_url='https://nofan.xyz')
access_token = 'Ug_bUMWCk3RLamOnqYIytmeB0nO6aNfjdmf06mAj2bE',
api_base_url = 'https://nofan.xyz'
)
pool = redis.ConnectionPool(host='localhost', port=6379, decode_responses=True) pool = redis.ConnectionPool(host='localhost', port=6379, decode_responses=True)
redis_cli = redis.Redis(host='localhost', port=6379, decode_responses=True) redis_cli = redis.Redis(host='localhost', port=6379, decode_responses=True)
class AESCipher(object):
class AESCipher(object):
def __init__(self, key): def __init__(self, key):
self.bs = AES.block_size self.bs = AES.block_size
self.key=hashlib.sha256(AESCipher.str_to_bytes(key)).digest() self.key = hashlib.sha256(AESCipher.str_to_bytes(key)).digest()
@staticmethod @staticmethod
def str_to_bytes(data): def str_to_bytes(data):
u_type = type(b"".decode('utf8')) u_type = type(b"".decode('utf8'))
if isinstance(data, u_type): if isinstance(data, u_type):
return data.encode('utf8') return data.encode('utf8')
return data return data
@staticmethod @staticmethod
def _unpad(s): def _unpad(s):
return s[:-ord(s[len(s) - 1:])] return s[: -ord(s[len(s) - 1 :])]
def decrypt(self, enc): def decrypt(self, enc):
iv = enc[:AES.block_size] iv = enc[: AES.block_size]
cipher = AES.new(self.key, AES.MODE_CBC, iv) cipher = AES.new(self.key, AES.MODE_CBC, iv)
return self._unpad(cipher.decrypt(enc[AES.block_size:])) return self._unpad(cipher.decrypt(enc[AES.block_size :]))
def decrypt_string(self, enc): def decrypt_string(self, enc):
enc = base64.b64decode(enc) enc = base64.b64decode(enc)
return self.decrypt(enc).decode('utf8') return self.decrypt(enc).decode('utf8')
def get_tenant_access_token(): # 获取token def get_tenant_access_token(): # 获取token
token = redis_cli.get('tenant_access_token_%s' % APP_ID) token = redis_cli.get('tenant_access_token_%s' % APP_ID)
if token: if token:
return token return token
url = "https://open.feishu.cn/open-apis/auth/v3/tenant_access_token/internal/" url = "https://open.feishu.cn/open-apis/auth/v3/tenant_access_token/internal/"
headers = { headers = {"Content-Type": "application/json"}
"Content-Type": "application/json" req_body = {"app_id": APP_ID, "app_secret": APP_SECRET}
}
req_body = {
"app_id": APP_ID,
"app_secret": APP_SECRET
}
data = bytes(json.dumps(req_body), encoding='utf8') data = bytes(json.dumps(req_body), encoding='utf8')
req = request.Request(url=url, data=data, headers=headers, method='POST') req = request.Request(url=url, data=data, headers=headers, method='POST')
@ -83,36 +93,35 @@ def get_tenant_access_token(): # 获取token
if code != 0: if code != 0:
logger.error("get tenant_access_token error, code =%s", code) logger.error("get tenant_access_token error, code =%s", code)
return "" return ""
token = redis_cli.set('tenant_access_token_%s' % APP_ID, token = rsp_dict.get("tenant_access_token", "")
rsp_dict.get("tenant_access_token", ""), redis_cli.set('tenant_access_token_%s' % APP_ID, rsp_dict.get("tenant_access_token", ""), ex=60 * 30)
ex=60*30)
return token return token
def get_group_name(chat_id): def get_group_name(chat_id):
group_name = redis_cli.get('group_name_%s' % chat_id) group_name = redis_cli.get('group_name_%s' % chat_id)
if not group_name: if not group_name:
url = "https://open.feishu.cn/open-apis/im/v1/chats/" url = "https://open.feishu.cn/open-apis/im/v1/chats/"
headers = { headers = {"Content-Type": "application/json", "Authorization": "Bearer " + get_tenant_access_token()}
"Content-Type": "application/json", try:
"Authorization": "Bearer " + get_tenant_access_token() resp = requests.get(url + chat_id, headers=headers)
} resp_data = resp.json()
try: code = resp_data.get("code", -1)
resp = requests.get(url+chat_id, headers=headers) if code == 0:
resp_data = resp.json() group_name = resp_data.get('data', {}).get('name')
code = resp_data.get("code", -1) redis_cli.set('group_name_%s' % chat_id, group_name, ex=60 * 60 * 24)
if code == 0: except:
group_name = resp_data.get('data', {}).get('name') # todo: log
redis_cli.set('group_name_%s' % chat_id, return
group_name, return group_name
ex=60*60*24)
except:
# todo: log
return
return group_name
class RequestHandler(BaseHTTPRequestHandler): class RequestHandler(BaseHTTPRequestHandler):
def do_GET(self):
"""Serve a GET request."""
self.response("")
def do_POST(self): def do_POST(self):
# 解析请求 body # 解析请求 body
req_body = self.rfile.read(int(self.headers['content-length'])) req_body = self.rfile.read(int(self.headers['content-length']))
@ -141,12 +150,12 @@ class RequestHandler(BaseHTTPRequestHandler):
event_id = obj.get('header', {}).get('event_id', '') event_id = obj.get('header', {}).get('event_id', '')
# 重复收到的事件不处理 # 重复收到的事件不处理
if event_id and redis_cli.get(event_id): if event_id and redis_cli.get(event_id):
self.response("") self.response("")
return return
event = obj.get("event") event = obj.get("event")
if event.get("message"): if event.get("message"):
self.handle_message(event, event_id) self.handle_message(event, event_id)
return return
return return
def handle_request_url_verify(self, post_obj): def handle_request_url_verify(self, post_obj):
@ -177,57 +186,82 @@ class RequestHandler(BaseHTTPRequestHandler):
text = text.lstrip() text = text.lstrip()
orig_text = text orig_text = text
if ADD_GROUP_NAME: if ADD_GROUP_NAME:
group_name = get_group_name(msg.get("chat_id")) group_name = get_group_name(msg.get("chat_id"))
text = '%s #%s' % (text, group_name) text = '%s #%s' % (text, group_name)
else: else:
open_id = {"open_id": event.get("sender", {}).get( open_id = {"open_id": event.get("sender", {}).get('sender_id', {}).get('open_id')}
'sender_id', {}).get('open_id')}
self.response("") self.response("")
if orig_text.startswith('/'): if orig_text.startswith('/'):
redis_cli.set(event_id, int(time.time()), ex=60*60*7) redis_cli.set(event_id, int(time.time()), ex=60 * 60 * 7)
if orig_text not in ['/last', '/del']: if orig_text not in ['/last', '/del']:
if not orig_text.startswith('/deploy '): flag = False
self.msg_compoment(access_token, open_id, '指令错误') for action_ in ['/deploy ', '/菜谱 ']:
return if orig_text.startswith(action_):
if orig_text == '/last': flag = True
try: break
statuses = mastodon.account_statuses(KEDAI_ID, limit=1) if not flag:
s_text = BeautifulSoup(statuses[0]['content'], 'html.parser') self.msg_compoment(access_token, open_id, '指令错误')
self.msg_compoment(access_token, open_id, return
s_text.get_text('')) if orig_text == '/last':
except Exception as exc: try:
logger.error('operation error: %s', str(exc)) statuses = mastodon_cli.account_statuses(KEDAI_ID, limit=1)
elif orig_text == '/del': s_text = BeautifulSoup(statuses[0]['content'], 'html.parser')
try: self.msg_compoment(access_token, open_id, s_text.get_text(''))
statuses = mastodon.account_statuses(KEDAI_ID, limit=1) except Exception as exc:
Mastodon.status_delete(statuses[0]['id']) logger.error('operation error: %s', str(exc))
s_text = BeautifulSoup(statuses[0]['content'], 'html.parser') elif orig_text == '/del':
self.msg_compoment(access_token, open_id, try:
'已删除: ' + s_text.get_text('')) statuses = mastodon_cli.account_statuses(KEDAI_ID, limit=1)
except Exception as exc: mastodon_cli.status_delete(statuses[0]['id'])
logger.error('operation error: %s', str(exc)) s_text = BeautifulSoup(statuses[0]['content'], 'html.parser')
elif orig_text.startswith('/deploy '): self.msg_compoment(access_token, open_id, '已删除: ' + s_text.get_text(''))
site_ = orig_text.split('/deploy ')[1] except Exception as exc:
if site_ == 'dsite': logger.error('operation error: %s', str(exc))
self.msg_compoment(access_token, open_id, '🚧 %s 开始部署 🚧' % site_) elif orig_text.startswith('/deploy '):
subprocess.call("/root/deploy/dsite_prepare.sh") site_ = orig_text.split('/deploy ')[1]
subprocess.run(["supervisorctl", "restart", "dsite"]) if site_ == 'dsite':
self.msg_compoment(access_token, open_id, '🎉 %s 部署成功 🎉' % site_) self.msg_compoment(access_token, open_id, '🚧 %s 开始部署 🚧' % site_)
else: subprocess.call("/root/deploy/dsite_prepare.sh")
self.msg_compoment(access_token, open_id, '⚠️ %s 不存在 ⚠️' % site_) subprocess.run(["supervisorctl", "restart", "dsite"])
self.msg_compoment(access_token, open_id, '🎉 %s 部署成功 🎉' % site_)
elif site_ == 'dodo':
self.msg_compoment(access_token, open_id, '🚧 %s 开始部署 🚧' % site_)
subprocess.run(["git", "pull"])
self.msg_compoment(access_token, open_id, '🎉 %s 部署成功 🎉' % site_)
subprocess.run(["supervisorctl", "restart", "dodo"])
else:
self.msg_compoment(access_token, open_id, '⚠️ %s 不存在 ⚠️' % site_)
elif orig_text.startswith('/菜谱 '):
content = orig_text.split('/菜谱 ')[1]
recipe_ = recipe.models.Recipe.create_from_str(content)
if recipe_:
self.msg_compoment(
access_token,
open_id,
None,
const.LARK_WEBHOOK_MSG_TYPE_INTERACTIVE,
recipe_.construct_lart_card(),
)
else:
self.msg_compoment(access_token, open_id, '⚠️ 创建失败 ⚠️')
return return
try: try:
toot_resp = mastodon.status_post(text) toot_resp = mastodon_cli.status_post(text)
if toot_resp.get('id'): if toot_resp.get('id'):
self.msg_compoment(access_token, open_id, '📟 dodo 📟') self.msg_compoment(access_token, open_id, '📟 dodo 📟')
redis_cli.set(event_id, int(time.time()), ex=60*60*7) redis_cli.set(event_id, int(time.time()), ex=60 * 60 * 7)
else: else:
self.msg_compoment(access_token, open_id, """⚠️ didi ⚠️ self.msg_compoment(
access_token,
open_id,
"""⚠️ didi ⚠️
%s %s
""" % json.loads(toot_resp)) """
% json.loads(toot_resp),
)
except Exception as exc: except Exception as exc:
logger.error('send toot error: %s', str(exc)) logger.error('send toot error: %s', str(exc))
return return
elif msg_type == "image": elif msg_type == "image":
@ -240,18 +274,17 @@ class RequestHandler(BaseHTTPRequestHandler):
self.end_headers() self.end_headers()
self.wfile.write(body.encode()) self.wfile.write(body.encode())
def send_message(self, token, open_id, text): def send_message(self, token, open_id, text, msg_type=None, content=None):
url = "https://open.feishu.cn/open-apis/message/v4/send/" url = "https://open.feishu.cn/open-apis/message/v4/send/"
headers = { headers = {"Content-Type": "application/json", "Authorization": "Bearer " + token}
"Content-Type": "application/json", if not msg_type:
"Authorization": "Bearer " + token msg_type = const.LARK_WEBHOOK_MSG_TYPE_TEXT
} req_body = {"msg_type": msg_type}
req_body = { if msg_type == const.LARK_WEBHOOK_MSG_TYPE_TEXT:
"msg_type": "text", req_body['content'] = {'text': text}
"content": { elif msg_type == const.LARK_WEBHOOK_MSG_TYPE_INTERACTIVE:
"text": text req_body['card'] = content
}
}
req_body = dict(req_body, **open_id) # 根据open_id判断返回域 req_body = dict(req_body, **open_id) # 根据open_id判断返回域
data = bytes(json.dumps(req_body), encoding='utf8') data = bytes(json.dumps(req_body), encoding='utf8')
@ -266,12 +299,11 @@ class RequestHandler(BaseHTTPRequestHandler):
rsp_dict = json.loads(rsp_body) rsp_dict = json.loads(rsp_body)
code = rsp_dict.get("code", -1) code = rsp_dict.get("code", -1)
if code != 0: if code != 0:
logger.error("send message error, code = %s, msg =%s", logger.error("send message error, code = %s, msg =%s", code, rsp_dict.get("msg", ""))
code,
rsp_dict.get("msg", "")) def msg_compoment(self, token, open_id, text, msg_type=None, content=None):
self.send_message(token, open_id, text, msg_type, content)
def msg_compoment(self, token, open_id, text):
self.send_message(token, open_id, text)
def run(): def run():
port = 5000 port = 5000

5
scripts/dsite_prepare.sh Normal file
View File

@ -0,0 +1,5 @@
#!/bin/bash
/root/.pyenv/versions/py37/bin/pip install -r /root/deploy/dsite/develop_requirements.txt
/root/.pyenv/versions/py37/bin/python /root/deploy/dsite/manage.py collectstatic --noinput
/root/.pyenv/versions/py37/bin/python /root/deploy/dsite/manage.py migrate

50
scripts/grayscale.py Normal file
View File

@ -0,0 +1,50 @@
# -*- coding: utf-8 -*-
import sys
import numpy as np
import cv2 # opencv-python
# 引入Python的可视化工具包 matplotlib
from matplotlib import pyplot as plt
def print_img_info(img):
print("================打印一下图像的属性================")
print("图像对象的类型 {}".format(type(img)))
print(img.shape)
print("图像宽度: {} pixels".format(img.shape[1]))
print("图像高度: {} pixels".format(img.shape[0]))
# GRAYScale 没有第三个维度哦, 所以这样会报错
# print("通道: {}".format(img.shape[2]))
print("图像分辨率: {}".format(img.size))
print("数据类型: {}".format(img.dtype))
if __name__ == '__main__':
# 导入一张图像 模式为彩色图片
img_path = sys.argv[1]
img = cv2.imread(img_path, cv2.IMREAD_COLOR)
# 将色彩空间转变为灰度图并展示
gray = cv2.cvtColor(img, cv2.COLOR_BGR2GRAY)
# 打印图片信息
# print_img_info(gray)
# 打印图片的局部
# print("打印图片局部")
# print(gray[100:105, 100:105])
# plt.imshow(gray)
# 需要添加colormap 颜色映射函数为gray
plt.imshow(gray, cmap="gray")
# 隐藏坐标系
plt.axis('off')
# 展示图片
# plt.show()
# 你也可以保存图片, 填入图片路径就好
new_name = '%s-gray.%s' % (img_path.split('.')[0], img_path.split('.')[-1])
plt.savefig(new_name)
print(new_name)
plt.close()

85
scripts/ins2mastodon.py Executable file
View File

@ -0,0 +1,85 @@
# -*- coding: utf-8 -*-
# scraper instagram posts
import os
import sys
os.environ.setdefault("DJANGO_SETTINGS_MODULE", "dsite.settings")
sys.path.insert(0, '../')
sys.path.insert(0, './')
from django.core.wsgi import get_wsgi_application
get_wsgi_application()
import pickle
from urllib.parse import urlparse
import time
from mastodon import Mastodon
from instagram_private_api import Client
import requests
import logging
from dsite import settings
logging.basicConfig(filename='/root/develop/log/ins2mastodon.log', level=logging.INFO)
logger = logging.getLogger('/root/develop/log/ins2mastodon.log')
mastodon_cli = Mastodon(access_token=settings.MASTODON_NOFAN_ACCESS_TOKEN, api_base_url='https://nofan.xyz')
def send_image_to_mastodon(image_url, text):
resp = requests.get(image_url)
mime_type = 'image/jpeg'
url = urlparse(image_url)
ext = url.path.split('.')[-1]
if ext == 'gif':
mime_type = 'image/gif'
toot_resp = mastodon_cli.media_post(resp.content, mime_type)
if toot_resp.get('id'):
media_ids = [toot_resp['id']]
mastodon_cli.status_post(text, media_ids=media_ids)
logger.info('send %s', text)
image_name = url.path.split('/')[-1]
with open(settings.MASTODON_SYNCED_IMAGES_LOG, 'a') as f:
f.write(image_name + '\n')
# write binary file with api.settings
def writeSettings(user, pwd, settings_file):
api = Client(user, pwd)
with open(settings_file, "wb") as FileObj:
pickle.dump(api.settings, FileObj)
# read binary file to api.settings
def readSettings(settings_file):
cache = None
with open(settings_file, "rb") as FileObj:
cache = pickle.load(FileObj)
return cache
if __name__ == '__main__':
if not os.path.exists(settings.IG_PRIVATE_API_SETTINGS):
writeSettings(settings.IG_LOGIN_USERNAME, settings.IG_LOGIN_PASSWORD, settings.IG_PRIVATE_API_SETTINGS)
cache_settings = readSettings(settings.IG_PRIVATE_API_SETTINGS)
api = Client(settings.IG_LOGIN_USERNAME, settings.IG_LOGIN_PASSWORD, settings=cache_settings)
while True:
results = api.self_feed()
logger.info('getting %s posts', len(results['items']))
for item in results['items']:
text = item['caption']['text']
image_url = item['image_versions2']['candidates'][0]['url']
try:
with open(settings.MASTODON_SYNCED_IMAGES_LOG, 'r') as f:
send_images = f.readlines()
send_images = [x.strip() for x in send_images]
image_name = urlparse(image_url).path.split('/')[-1]
if image_name not in send_images:
send_image_to_mastodon(image_url, text)
except Exception as e:
logger.error(e)
time.sleep(60)

42
scripts/ins_scraper.py Executable file
View File

@ -0,0 +1,42 @@
# -*- coding: utf-8 -*-
# scraper instagram posts
import os
import sys
os.environ.setdefault("DJANGO_SETTINGS_MODULE", "dsite.settings")
sys.path.insert(0, '../')
sys.path.insert(0, './')
from django.core.wsgi import get_wsgi_application
get_wsgi_application()
import instagram_scraper
import time
from dsite import settings
if __name__ == '__main__':
args = {
'login_user': settings.IG_LOGIN_USERNAME,
'login_pass': settings.IG_LOGIN_PASSWORD,
'cookiejar': settings.IG_SCRAPER_COOKIE_FILE,
'latest_stamps': settings.IG_SCRAPER_LAST_STAMP_FILE,
'usernames': settings.IG_SCRAPER_USERNAMES,
'destination': settings.IG_SCRAPER_DESTINATION,
}
scraper = instagram_scraper.InstagramScraper(**args)
scraper.authenticate_with_login()
scraper.save_cookies()
while True:
time.sleep(10)
args_ = {
'cookiejar': settings.IG_SCRAPER_COOKIE_FILE,
'latest_stamps': settings.IG_SCRAPER_LAST_STAMP_FILE,
'usernames': settings.IG_SCRAPER_USERNAMES,
'destination': settings.IG_SCRAPER_DESTINATION,
'template': '{username}-{datetime}-{shortcode}',
}
scraper = instagram_scraper.InstagramScraper(**args_)
scraper.scrape()

5
scripts/restart_dodo.sh Normal file
View File

@ -0,0 +1,5 @@
#!/bin/bash
supervisorctl restart dodo && \
curl -X POST -H "Content-Type: application/json" \
-d '{"msg_type":"text","content":{"text":"🎉 dodo 部署成功 🎉"}}' \
https://open.feishu.cn/open-apis/bot/v2/hook/57cfa603-6154-4055-a739-210028171d10

View File

@ -4,27 +4,23 @@ import timer.models
class OfficeHoursForm(forms.ModelForm): class OfficeHoursForm(forms.ModelForm):
class Meta: class Meta:
model = timer.models.OfficeHours model = timer.models.OfficeHours
fields = ['user', 'begins_at', 'ends_at'] fields = ['user', 'begins_at', 'ends_at']
def __init__(self, *args, **kwargs): def __init__(self, *args, **kwargs):
self.user = kwargs.pop('user') self.user = kwargs.pop('user')
return super().__init__(*args, **kwargs) return super().__init__(*args, **kwargs)
def full_clean(self):
if not self.user.is_authenticated:
raise forms.ValidationError('Invalid User.')
return
def save(self):
begins_at = timer.models.OfficeHours.parse_time_str(self.data.get('begins_at'))
user = self.user
obj = timer.models.OfficeHours.objects.create(
user=user,
begins_at=begins_at,
ends_at=timer.models.OfficeHours.get_ends_at(begins_at))
return obj
def full_clean(self):
if not self.user.is_authenticated:
raise forms.ValidationError('Invalid User.')
return
def save(self):
begins_at = timer.models.OfficeHours.parse_time_str(self.data.get('begins_at'))
user = self.user
obj = timer.models.OfficeHours.objects.create(
user=user, begins_at=begins_at, ends_at=timer.models.OfficeHours.get_ends_at(begins_at)
)
return obj

View File

@ -9,9 +9,7 @@ class Migration(migrations.Migration):
initial = True initial = True
dependencies = [ dependencies = [migrations.swappable_dependency(settings.AUTH_USER_MODEL)]
migrations.swappable_dependency(settings.AUTH_USER_MODEL),
]
operations = [ operations = [
migrations.CreateModel( migrations.CreateModel(
@ -22,5 +20,5 @@ class Migration(migrations.Migration):
('ends_at', models.DateTimeField()), ('ends_at', models.DateTimeField()),
('user', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to=settings.AUTH_USER_MODEL)), ('user', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to=settings.AUTH_USER_MODEL)),
], ],
), )
] ]

View File

@ -6,22 +6,22 @@ import datetime
class OfficeHours(models.Model): class OfficeHours(models.Model):
begins_at = models.DateTimeField() begins_at = models.DateTimeField()
ends_at = models.DateTimeField() ends_at = models.DateTimeField()
user = models.ForeignKey(User, on_delete=django.db.models.deletion.CASCADE) user = models.ForeignKey(User, on_delete=django.db.models.deletion.CASCADE)
@classmethod @classmethod
def parse_time_str(cls, time_srt): def parse_time_str(cls, time_srt):
return datetime.datetime.strptime(time_srt, '%Y-%m-%d %H:%M') return datetime.datetime.strptime(time_srt, '%Y-%m-%d %H:%M')
@classmethod @classmethod
def get_ends_at(cls, begins_at): def get_ends_at(cls, begins_at):
return begins_at + datetime.timedelta(hours=9.5) return begins_at + datetime.timedelta(hours=9.5)
@property @property
def get_begins_at_str(self): def get_begins_at_str(self):
return localtime(self.begins_at).strftime('%Y-%m-%d %H:%M') return localtime(self.begins_at).strftime('%Y-%m-%d %H:%M')
@property @property
def get_ends_at_str(self): def get_ends_at_str(self):
return localtime(self.ends_at).strftime('%H:%M') return localtime(self.ends_at).strftime('%H:%M')

View File

@ -5,17 +5,17 @@ import timer.models
class OfficeHoursSerializer(serializers.HyperlinkedModelSerializer): class OfficeHoursSerializer(serializers.HyperlinkedModelSerializer):
class Meta: class Meta:
model = timer.models.OfficeHours model = timer.models.OfficeHours
class UserSerializer(serializers.HyperlinkedModelSerializer): class UserSerializer(serializers.HyperlinkedModelSerializer):
class Meta: class Meta:
model = User model = User
fields = ('url', 'username', 'email', 'groups') fields = ('url', 'username', 'email', 'groups')
class GroupSerializer(serializers.HyperlinkedModelSerializer): class GroupSerializer(serializers.HyperlinkedModelSerializer):
class Meta: class Meta:
model = Group model = Group
fields = ('url', 'name') fields = ('url', 'name')

View File

@ -1,4 +1,5 @@
from django.conf.urls import include, url from django.conf.urls import include, url
# from django.core.urlresolvers import reverse # from django.core.urlresolvers import reverse
from django.urls import path from django.urls import path
from rest_framework import routers from rest_framework import routers

View File

@ -19,59 +19,57 @@ from django.urls import reverse
class OfficeHoursAPI(CreateAPIView): class OfficeHoursAPI(CreateAPIView):
authentication_classes = (authentication.TokenAuthentication, authentication_classes = (
authentication.SessionAuthentication, authentication.TokenAuthentication,
authentication.BasicAuthentication) authentication.SessionAuthentication,
permission_classes = (permissions.IsAuthenticated,) authentication.BasicAuthentication,
)
permission_classes = (permissions.IsAuthenticated,)
queryset = timer.models.OfficeHours.objects.all() queryset = timer.models.OfficeHours.objects.all()
def create(self, request, *args, **kwargs): def create(self, request, *args, **kwargs):
begins_at = request.data.get('begins_at') begins_at = request.data.get('begins_at')
if not begins_at: if not begins_at:
raise raise
try: try:
begins_at = timer.models.OfficeHours.parse_time_str(begins_at) begins_at = timer.models.OfficeHours.parse_time_str(begins_at)
ends_at = timer.models.OfficeHours.get_ends_at(begins_at) ends_at = timer.models.OfficeHours.get_ends_at(begins_at)
oh, __ = timer.models.OfficeHours.objects.get_or_create( oh, __ = timer.models.OfficeHours.objects.get_or_create(
begins_at=begins_at, begins_at=begins_at, ends_at=ends_at, user=request.user
ends_at=ends_at, )
user=request.user) except ValueError:
except ValueError: raise
raise oh.refresh_from_db()
oh.refresh_from_db() resp_data = {'begins_at': oh.get_begins_at_str, 'ends_at': oh.get_ends_at_str}
resp_data = {
'begins_at': oh.get_begins_at_str,
'ends_at': oh.get_ends_at_str,
}
return Response(resp_data, status=status.HTTP_201_CREATED) return Response(resp_data, status=status.HTTP_201_CREATED)
class OfficeHoursFormView(django.views.generic.FormView): class OfficeHoursFormView(django.views.generic.FormView):
template_name = 'index.html' template_name = 'index.html'
form_class = timer.forms.OfficeHoursForm form_class = timer.forms.OfficeHoursForm
def get_form_kwargs(self): def get_form_kwargs(self):
kwargs = super().get_form_kwargs() kwargs = super().get_form_kwargs()
kwargs['user'] = self.request.user kwargs['user'] = self.request.user
return kwargs return kwargs
def post(self, request, *args, **kwargs): def post(self, request, *args, **kwargs):
form = self.form_class(**self.get_form_kwargs()) form = self.form_class(**self.get_form_kwargs())
try: try:
form.is_valid() form.is_valid()
form.save() form.save()
except django.forms.ValidationError as e: except django.forms.ValidationError as e:
return JsonResponse({'error': e.message}, status=status.HTTP_400_BAD_REQUEST) return JsonResponse({'error': e.message}, status=status.HTTP_400_BAD_REQUEST)
return super().post(request, *args, **kwargs) return super().post(request, *args, **kwargs)
def get_success_url(self): def get_success_url(self):
return reverse('office-hours-page') return reverse('office-hours-page')
def get_context_data(self, **kwargs): def get_context_data(self, **kwargs):
context = super().get_context_data(**kwargs) context = super().get_context_data(**kwargs)
today_oh = timer.models.OfficeHours.objects.filter(begins_at__date=localtime().date()).last() today_oh = timer.models.OfficeHours.objects.filter(begins_at__date=localtime().date()).last()
context['today_oh'] = today_oh context['today_oh'] = today_oh
return context return context

View File

@ -6,113 +6,113 @@ from django.utils import timezone
def timestamp_of(d): def timestamp_of(d):
if hasattr(d, 'isoformat'): if hasattr(d, 'isoformat'):
return calendar.timegm(d.utctimetuple()) return calendar.timegm(d.utctimetuple())
return None return None
def now(): def now():
return timezone.localtime(timezone.now()) return timezone.localtime(timezone.now())
def midnight(): def midnight():
return now().replace(hour=0, minute=0, second=0, microsecond=0) return now().replace(hour=0, minute=0, second=0, microsecond=0)
def is_today(tme): def is_today(tme):
if day_of(tme) != day_of(midnight()): if day_of(tme) != day_of(midnight()):
return False return False
return True return True
def current_time_n(n): def current_time_n(n):
return now() - timedelta(days=n) return now() - timedelta(days=n)
def day_n(n): def day_n(n):
"""Midnight of N days ago.""" """Midnight of N days ago."""
return midnight() - timedelta(days=n) return midnight() - timedelta(days=n)
def day_of(d): def day_of(d):
"""Returns date part.""" """Returns date part."""
return str(d.date()) return str(d.date())
def midnight_of(d): def midnight_of(d):
""" """
Args: Args:
d : datetime object d : datetime object
Return: Return:
Midnight of datetime. Midnight of datetime.
""" """
d = timezone.localtime(d) d = timezone.localtime(d)
return d.replace(hour=0, minute=0, second=0, microsecond=0) return d.replace(hour=0, minute=0, second=0, microsecond=0)
def day_start(date): def day_start(date):
""" 返回 date 这天的开始时间 """返回 date 这天的开始时间
Args: Args:
date: `Datetime` 对象 date: `Datetime` 对象
Returns: Returns:
返回 date 这天的开始时间0 `Datetime` 对象 返回 date 这天的开始时间0 `Datetime` 对象
""" """
return timezone.localtime(date).replace(hour=0, minute=0, second=0, return timezone.localtime(date).replace(hour=0, minute=0, second=0, microsecond=0)
microsecond=0)
def week_start(date): def week_start(date):
""" 返回 date 这天所在周的开始时间 """返回 date 这天所在周的开始时间
Args: Args:
date: `Datetime` 对象 date: `Datetime` 对象
Returns: Returns:
返回 date 这天所在周开始时间周一`Datetime` 对象 返回 date 这天所在周开始时间周一`Datetime` 对象
""" """
return timezone.localtime(date) + dateutil.relativedelta.relativedelta( return timezone.localtime(date) + dateutil.relativedelta.relativedelta(
weekday=dateutil.relativedelta.MO(-1), hour=0, minute=0, second=0, weekday=dateutil.relativedelta.MO(-1), hour=0, minute=0, second=0, microsecond=0
microsecond=0) )
def month_start(d): def month_start(d):
""" 返回 date 这天所在月的开始时间 """返回 date 这天所在月的开始时间
Args: Args:
date: `Datetime` 对象 date: `Datetime` 对象
Returns: Returns:
返回 date 这天所在月的开始时间1`Datetime` 对象 返回 date 这天所在月的开始时间1`Datetime` 对象
""" """
return timezone.localtime(d) + dateutil.relativedelta.relativedelta( return timezone.localtime(d) + dateutil.relativedelta.relativedelta(
day=1, hour=0, minute=0, second=0, microsecond=0) day=1, hour=0, minute=0, second=0, microsecond=0
)
def month_end(d): def month_end(d):
"""返回 date 这天所在月的结束时间,即最后一天的 23:59:59 """返回 date 这天所在月的结束时间,即最后一天的 23:59:59
Args: Args:
data: `Datetime` 对象 data: `Datetime` 对象
Returns: Returns:
返回 date 这天所在月的最后一天的 23:59:59 的时间 返回 date 这天所在月的最后一天的 23:59:59 的时间
""" """
start = month_start(d) start = month_start(d)
month_days = calendar.monthrange(start.year, start.month)[1] month_days = calendar.monthrange(start.year, start.month)[1]
return start + timedelta(days=month_days, seconds=-1) return start + timedelta(days=month_days, seconds=-1)
def year_start(d): def year_start(d):
""" 返回 date 这天所在年的开始时间 """返回 date 这天所在年的开始时间
Args: Args:
date: `Datetime` 对象 date: `Datetime` 对象
Returns:
返回 date 这天所在年的开始时间1 1`Datetime` 对象
"""
return timezone.localtime(d) + dateutil.relativedelta.relativedelta(
month=1, day=1, hour=0, minute=0, second=0, microsecond=0)
Returns:
返回 date 这天所在年的开始时间1 1`Datetime` 对象
"""
return timezone.localtime(d) + dateutil.relativedelta.relativedelta(
month=1, day=1, hour=0, minute=0, second=0, microsecond=0
)

View File

@ -2,23 +2,23 @@ DAILY_RECIPE_MEAL_TYPE_BREAKFAST = 'breakfast'
DAILY_RECIPE_MEAL_TYPE_LUNCH = 'lunch' DAILY_RECIPE_MEAL_TYPE_LUNCH = 'lunch'
DAILY_RECIPE_MEAL_TYPE_SUPPER = 'supper' DAILY_RECIPE_MEAL_TYPE_SUPPER = 'supper'
DAILY_RECIPE_MEAL_TYPE_CHOICE = [ DAILY_RECIPE_MEAL_TYPE_CHOICE = [
DAILY_RECIPE_MEAL_TYPE_BREAKFAST, DAILY_RECIPE_MEAL_TYPE_BREAKFAST,
DAILY_RECIPE_MEAL_TYPE_LUNCH, DAILY_RECIPE_MEAL_TYPE_LUNCH,
DAILY_RECIPE_MEAL_TYPE_SUPPER, DAILY_RECIPE_MEAL_TYPE_SUPPER,
] ]
RECIPE_TYPE_MEAT = 'meat' RECIPE_TYPE_MEAT = 'meat'
RECIPE_TYPE_VEGETABLE = 'vegetable' RECIPE_TYPE_VEGETABLE = 'vegetable'
RECIPE_TYPE_SOUP = 'soup' RECIPE_TYPE_SOUP = 'soup'
RECIPE_TYPE_CHOICE = [ RECIPE_STATUS_ACTIVE = 'active'
RECIPE_TYPE_MEAT, RECIPE_STATUS_DELETED = 'deleted'
RECIPE_TYPE_VEGETABLE,
RECIPE_TYPE_SOUP, RECIPE_TYPE_CHOICE = [RECIPE_TYPE_MEAT, RECIPE_TYPE_VEGETABLE, RECIPE_TYPE_SOUP]
]
FILTER_EXACT = ['exact'] FILTER_EXACT = ['exact']
FILTER_GTE_LTE = ['exact', 'gte', 'gt', 'lte', 'lt'] FILTER_GTE_LTE = ['exact', 'gte', 'gt', 'lte', 'lt']
LARK_WEBHOOK_MSG_TYPE_TEXT = 'text' LARK_WEBHOOK_MSG_TYPE_TEXT = 'text'
LARK_WEBHOOK_MSG_TYPE_INTERACTIVE = 'interactive'

View File

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

View File

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