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,6 +2,7 @@
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'
@ -9,5 +10,3 @@ class PagePaginationWithPageCount(pagination.PageNumberPagination):
response = super().get_paginated_response(data) response = super().get_paginated_response(data)
response.data['page_count'] = self.page.paginator.num_pages response.data['page_count'] = self.page.paginator.num_pages
return response 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
@ -159,3 +142,10 @@ CORS_ALLOW_CREDENTIALS = True # 允许携带cookie
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,6 +54,16 @@
<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-col :span="8" v-if="recipe_id">
<el-button
type="danger"
plain
class="summit-recipe"
@click="onSubmitDelete(recipe_id)"
>删除</el-button
>
</el-col>
<el-col :span="16" v-if="recipe_id">
<el-button <el-button
type="primary" type="primary"
plain plain
@ -61,6 +71,16 @@
@click="onSubmit(recipe_id)" @click="onSubmit(recipe_id)"
>提交</el-button >提交</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: {
@ -105,6 +127,10 @@ export default {
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) {
@ -114,13 +140,27 @@ export default {
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,10 +43,25 @@
/> />
</van-cell-group> </van-cell-group>
<div class="recipe-create"> <div class="recipe-create">
<van-row gutter="20">
<van-col span="8" v-if="recipe_id">
<van-button <van-button
class="submit-button"
round
type="danger"
plain
hairline
:disabled="disable_submit"
@click="onSubmitDelete(recipe_id)"
:loading="loading"
>删除</van-button
>
</van-col>
<van-col span="16" v-if="recipe_id">
<van-button
class="submit-button"
round round
type="primary" type="primary"
block
plain plain
hairline hairline
:disabled="disable_submit" :disabled="disable_submit"
@ -54,12 +69,38 @@
:loading="loading" :loading="loading"
>提交</van-button >提交</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,37 +6,115 @@ 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,
'name': self.name,
'recipe_type': self.recipe_type,
}
if verbose: if verbose:
data.update({ data.update({'difficulty': self.difficulty, 'rate': self.rate, 'note': self.note})
'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:
@ -50,9 +128,12 @@ class DailyRecipe(models.Model):
# 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)
.order_by('?')
.first()
)
if recipe and recipe.id not in recipes and recipe.id not in prev_recipes: if recipe and recipe.id not in recipes and recipe.id not in prev_recipes:
recipes.append(recipe.id) recipes.append(recipe.id)
break break
@ -65,9 +146,12 @@ class DailyRecipe(models.Model):
# 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)
.order_by('?')
.first()
)
if recipe and recipe.id not in recipes and recipe.id not in prev_recipes: if recipe and recipe.id not in recipes and recipe.id not in prev_recipes:
recipes.append(recipe.id) recipes.append(recipe.id)
break break
@ -79,9 +163,12 @@ class DailyRecipe(models.Model):
# 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)
.order_by('?')
.first()
)
# if recipe not in recipes and recipe not in prev_recipes: # if recipe not in recipes and recipe not in prev_recipes:
if recipe: if recipe:
recipes.append(recipe.id) recipes.append(recipe.id)
@ -89,19 +176,13 @@ class DailyRecipe(models.Model):
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: [],
const.RECIPE_TYPE_VEGETABLE: [],
const.RECIPE_TYPE_SOUP: [],
}
for key_, value_ in data.items(): for key_, value_ in data.items():
for recipe in self.recipes.filter( for recipe in self.recipes.filter(recipe_type=key_).order_by('id'):
recipe_type=key_).order_by('id'):
value_.append(recipe.serialize()) 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
@ -111,10 +192,7 @@ class DailyRecipe(models.Model):
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,
date__lte=week_end,
).order_by('date')
data = [{}] * (7 - len(daily_recipes)) data = [{}] * (7 - len(daily_recipes))
for daily_recipe in daily_recipes: for daily_recipe in daily_recipes:

View File

@ -4,15 +4,16 @@ 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: class Meta:
model = recipe.models.Recipe model = recipe.models.Recipe
fields = '__all__' 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__'
@ -21,6 +22,7 @@ class WeekRecipeSerializer(serializers.ModelSerializer):
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: class Meta:
model = recipe.models.DailyRecipe model = recipe.models.DailyRecipe
fields = '__all__' 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,7 +11,8 @@ import recipe.models
import recipe.serializers import recipe.serializers
from utils import const from utils import const
class RecipeAPI(rest_framework.generics.RetrieveUpdateAPIView):
class RecipeAPI(rest_framework.generics.RetrieveUpdateDestroyAPIView):
# authentication_classes = (authentication.TokenAuthentication, # authentication_classes = (authentication.TokenAuthentication,
# authentication.SessionAuthentication, # authentication.SessionAuthentication,
@ -20,15 +21,18 @@ class RecipeAPI(rest_framework.generics.RetrieveUpdateAPIView):
queryset = recipe.models.Recipe.objects.all() queryset = recipe.models.Recipe.objects.all()
serializer_class = recipe.serializers.RecipeSerializer 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,
rest_framework.generics.CreateAPIView): class RecipeListAPI(rest_framework.generics.ListAPIView, 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,
@ -37,8 +41,7 @@ class RecipeListAPI(rest_framework.generics.ListAPIView,
} }
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
@ -48,13 +51,10 @@ class WeekRecipeListAPI(rest_framework.generics.ListAPIView,
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) daily_recipe.generate_recipe(recipes)
recipes.extend(daily_recipe.recipes.values_list('id', flat=True)) recipes.extend(daily_recipe.recipes.values_list('id', flat=True))
return Response(recipe.models.DailyRecipe.get_week_recipe_data(), return Response(recipe.models.DailyRecipe.get_week_recipe_data(), status=status.HTTP_201_CREATED, headers={})
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()
@ -69,15 +69,12 @@ class DailyRecipeAPI(rest_framework.generics.RetrieveUpdateAPIView):
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,29 +93,24 @@ 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",
"Authorization": "Bearer " + get_tenant_access_token()
}
try: try:
resp = requests.get(url + chat_id, headers=headers) resp = requests.get(url + chat_id, headers=headers)
resp_data = resp.json() resp_data = resp.json()
code = resp_data.get("code", -1) code = resp_data.get("code", -1)
if code == 0: if code == 0:
group_name = resp_data.get('data', {}).get('name') group_name = resp_data.get('data', {}).get('name')
redis_cli.set('group_name_%s' % chat_id, redis_cli.set('group_name_%s' % chat_id, group_name, ex=60 * 60 * 24)
group_name,
ex=60*60*24)
except: except:
# todo: log # todo: log
return return
@ -113,6 +118,10 @@ def get_group_name(chat_id):
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']))
@ -180,30 +189,32 @@ class RequestHandler(BaseHTTPRequestHandler):
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
for action_ in ['/deploy ', '/菜谱 ']:
if orig_text.startswith(action_):
flag = True
break
if not flag:
self.msg_compoment(access_token, open_id, '指令错误') self.msg_compoment(access_token, open_id, '指令错误')
return return
if orig_text == '/last': if orig_text == '/last':
try: try:
statuses = mastodon.account_statuses(KEDAI_ID, limit=1) statuses = mastodon_cli.account_statuses(KEDAI_ID, limit=1)
s_text = BeautifulSoup(statuses[0]['content'], 'html.parser') s_text = BeautifulSoup(statuses[0]['content'], 'html.parser')
self.msg_compoment(access_token, open_id, self.msg_compoment(access_token, open_id, s_text.get_text(''))
s_text.get_text(''))
except Exception as exc: except Exception as exc:
logger.error('operation error: %s', str(exc)) logger.error('operation error: %s', str(exc))
elif orig_text == '/del': elif orig_text == '/del':
try: try:
statuses = mastodon.account_statuses(KEDAI_ID, limit=1) statuses = mastodon_cli.account_statuses(KEDAI_ID, limit=1)
Mastodon.status_delete(statuses[0]['id']) mastodon_cli.status_delete(statuses[0]['id'])
s_text = BeautifulSoup(statuses[0]['content'], 'html.parser') s_text = BeautifulSoup(statuses[0]['content'], 'html.parser')
self.msg_compoment(access_token, open_id, self.msg_compoment(access_token, open_id, '已删除: ' + s_text.get_text(''))
'已删除: ' + s_text.get_text(''))
except Exception as exc: except Exception as exc:
logger.error('operation error: %s', str(exc)) logger.error('operation error: %s', str(exc))
elif orig_text.startswith('/deploy '): elif orig_text.startswith('/deploy '):
@ -213,19 +224,42 @@ class RequestHandler(BaseHTTPRequestHandler):
subprocess.call("/root/deploy/dsite_prepare.sh") subprocess.call("/root/deploy/dsite_prepare.sh")
subprocess.run(["supervisorctl", "restart", "dsite"]) subprocess.run(["supervisorctl", "restart", "dsite"])
self.msg_compoment(access_token, open_id, '🎉 %s 部署成功 🎉' % site_) 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: else:
self.msg_compoment(access_token, open_id, '⚠️ %s 不存在 ⚠️' % site_) 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))
@ -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

@ -12,7 +12,6 @@ class OfficeHoursForm(forms.ModelForm):
self.user = kwargs.pop('user') self.user = kwargs.pop('user')
return super().__init__(*args, **kwargs) return super().__init__(*args, **kwargs)
def full_clean(self): def full_clean(self):
if not self.user.is_authenticated: if not self.user.is_authenticated:
raise forms.ValidationError('Invalid User.') raise forms.ValidationError('Invalid User.')
@ -22,9 +21,6 @@ class OfficeHoursForm(forms.ModelForm):
begins_at = timer.models.OfficeHours.parse_time_str(self.data.get('begins_at')) begins_at = timer.models.OfficeHours.parse_time_str(self.data.get('begins_at'))
user = self.user user = self.user
obj = timer.models.OfficeHours.objects.create( obj = timer.models.OfficeHours.objects.create(
user=user, user=user, begins_at=begins_at, ends_at=timer.models.OfficeHours.get_ends_at(begins_at)
begins_at=begins_at, )
ends_at=timer.models.OfficeHours.get_ends_at(begins_at))
return obj 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

@ -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,9 +19,11 @@ from django.urls import reverse
class OfficeHoursAPI(CreateAPIView): class OfficeHoursAPI(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 = timer.models.OfficeHours.objects.all() queryset = timer.models.OfficeHours.objects.all()
@ -34,16 +36,12 @@ class OfficeHoursAPI(CreateAPIView):
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 = { resp_data = {'begins_at': oh.get_begins_at_str, 'ends_at': oh.get_ends_at_str}
'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)

View File

@ -59,8 +59,7 @@ def day_start(date):
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):
@ -73,8 +72,8 @@ def week_start(date):
返回 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):
@ -87,7 +86,8 @@ def month_start(d):
返回 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):
@ -114,5 +114,5 @@ def year_start(d):
返回 date 这天所在年的开始时间1 1`Datetime` 对象 返回 date 这天所在年的开始时间1 1`Datetime` 对象
""" """
return timezone.localtime(d) + dateutil.relativedelta.relativedelta( return timezone.localtime(d) + dateutil.relativedelta.relativedelta(
month=1, day=1, hour=0, minute=0, second=0, microsecond=0) month=1, day=1, hour=0, minute=0, second=0, microsecond=0
)

View File

@ -11,14 +11,14 @@ 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")
@ -16,10 +17,5 @@ if __name__ == '__main__':
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

@ -22,15 +22,16 @@ def gen_sign(timestamp, secret):
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