From afadc61d5decc217f2e4a198be8b64b1bec3695a Mon Sep 17 00:00:00 2001 From: vabene1111 Date: Thu, 18 Mar 2021 22:34:53 +0100 Subject: [PATCH] telegram bot --- cookbook/admin.py | 9 +++- cookbook/migrations/0115_telegrambot.py | 31 +++++++++++++ cookbook/models.py | 14 ++++++ cookbook/urls.py | 6 ++- cookbook/views/__init__.py | 2 + cookbook/views/api.py | 2 +- cookbook/views/telegram.py | 62 +++++++++++++++++++++++++ docs/features/telegram_bot.md | 52 +++++++++++++++++++++ 8 files changed, 175 insertions(+), 3 deletions(-) create mode 100644 cookbook/migrations/0115_telegrambot.py create mode 100644 cookbook/views/telegram.py create mode 100644 docs/features/telegram_bot.md diff --git a/cookbook/admin.py b/cookbook/admin.py index 85bfbe01..1df9f1b9 100644 --- a/cookbook/admin.py +++ b/cookbook/admin.py @@ -7,7 +7,7 @@ from .models import (Comment, CookLog, Food, Ingredient, InviteLink, Keyword, RecipeBook, RecipeBookEntry, RecipeImport, ShareLink, ShoppingList, ShoppingListEntry, ShoppingListRecipe, Space, Step, Storage, Sync, SyncLog, Unit, UserPreference, - ViewLog, Supermarket, SupermarketCategory, SupermarketCategoryRelation, ImportLog) + ViewLog, Supermarket, SupermarketCategory, SupermarketCategoryRelation, ImportLog, TelegramBot) class CustomUserAdmin(UserAdmin): @@ -220,3 +220,10 @@ class ImportLogAdmin(admin.ModelAdmin): admin.site.register(ImportLog, ImportLogAdmin) + + +class TelegramBotAdmin(admin.ModelAdmin): + list_display = ('id', 'name', 'created_by',) + + +admin.site.register(TelegramBot, TelegramBotAdmin) diff --git a/cookbook/migrations/0115_telegrambot.py b/cookbook/migrations/0115_telegrambot.py new file mode 100644 index 00000000..7fe0757f --- /dev/null +++ b/cookbook/migrations/0115_telegrambot.py @@ -0,0 +1,31 @@ +# Generated by Django 3.1.7 on 2021-03-18 21:12 + +import cookbook.models +from django.conf import settings +from django.db import migrations, models +import django.db.models.deletion +import uuid + + +class Migration(migrations.Migration): + + dependencies = [ + migrations.swappable_dependency(settings.AUTH_USER_MODEL), + ('cookbook', '0114_importlog'), + ] + + operations = [ + migrations.CreateModel( + name='TelegramBot', + fields=[ + ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), + ('token', models.CharField(max_length=256)), + ('name', models.CharField(blank=True, default='', max_length=128)), + ('chat_id', models.CharField(blank=True, default='', max_length=128)), + ('webhook_token', models.UUIDField(default=uuid.uuid4)), + ('created_by', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to=settings.AUTH_USER_MODEL)), + ('space', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='cookbook.space')), + ], + bases=(models.Model, cookbook.models.PermissionModelMixin), + ), + ] diff --git a/cookbook/models.py b/cookbook/models.py index 6295fb92..d1668bac 100644 --- a/cookbook/models.py +++ b/cookbook/models.py @@ -617,6 +617,20 @@ class InviteLink(models.Model, PermissionModelMixin): return f'{self.uuid}' +class TelegramBot(models.Model, PermissionModelMixin): + token = models.CharField(max_length=256) + name = models.CharField(max_length=128, default='', blank=True) + chat_id = models.CharField(max_length=128, default='', blank=True) + created_by = models.ForeignKey(User, on_delete=models.CASCADE) + webhook_token = models.UUIDField(default=uuid.uuid4) + + objects = ScopedManager(space='space') + space = models.ForeignKey(Space, on_delete=models.CASCADE) + + def __str__(self): + return f"{self.name}" + + class CookLog(models.Model, PermissionModelMixin): recipe = models.ForeignKey(Recipe, on_delete=models.CASCADE) created_by = models.ForeignKey(User, on_delete=models.CASCADE) diff --git a/cookbook/urls.py b/cookbook/urls.py index bf48e125..2816df0a 100644 --- a/cookbook/urls.py +++ b/cookbook/urls.py @@ -11,7 +11,7 @@ from cookbook.helper import dal from .models import (Comment, Food, InviteLink, Keyword, MealPlan, Recipe, RecipeBook, RecipeBookEntry, RecipeImport, ShoppingList, Storage, Sync, SyncLog, get_model_name) -from .views import api, data, delete, edit, import_export, lists, new, views +from .views import api, data, delete, edit, import_export, lists, new, views, telegram router = routers.DefaultRouter() router.register(r'user-name', api.UserNameViewSet, basename='username') @@ -100,6 +100,10 @@ urlpatterns = [ path('dal/food/', dal.IngredientsAutocomplete.as_view(), name='dal_food'), path('dal/unit/', dal.UnitAutocomplete.as_view(), name='dal_unit'), + path('telegram/setup/', telegram.setup_bot, name='telegram_setup'), + path('telegram/remove/', telegram.remove_bot, name='telegram_remove'), + path('telegram/hook//', telegram.hook, name='telegram_hook'), + path('docs/markdown/', views.markdown_info, name='docs_markdown'), path('docs/api/', views.api_info, name='docs_api'), diff --git a/cookbook/views/__init__.py b/cookbook/views/__init__.py index ab37dad9..2eb43fba 100644 --- a/cookbook/views/__init__.py +++ b/cookbook/views/__init__.py @@ -6,6 +6,7 @@ import cookbook.views.import_export import cookbook.views.lists import cookbook.views.new import cookbook.views.views +import cookbook.views.telegram __all__ = [ 'api', @@ -16,4 +17,5 @@ __all__ = [ 'lists', 'new', 'views', + 'telegram', ] diff --git a/cookbook/views/api.py b/cookbook/views/api.py index 2e23c0ca..e8088cdd 100644 --- a/cookbook/views/api.py +++ b/cookbook/views/api.py @@ -28,7 +28,7 @@ from cookbook.helper.permission_helper import (CustomIsAdmin, CustomIsGuest, CustomIsOwner, CustomIsShare, CustomIsShared, CustomIsUser, group_required) -from cookbook.helper.recipe_url_import import get_from_html, get_from_scraper +from cookbook.helper.recipe_url_import import get_from_html, get_from_scraper, find_recipe_json from cookbook.models import (CookLog, Food, Ingredient, Keyword, MealPlan, MealType, Recipe, RecipeBook, ShoppingList, ShoppingListEntry, ShoppingListRecipe, Step, diff --git a/cookbook/views/telegram.py b/cookbook/views/telegram.py new file mode 100644 index 00000000..67c281c8 --- /dev/null +++ b/cookbook/views/telegram.py @@ -0,0 +1,62 @@ +import json + +import requests +from django.db.models import Q +from django.http import JsonResponse +from django.shortcuts import render, get_object_or_404 +from django.views.decorators.csrf import csrf_exempt + +from cookbook.helper.ingredient_parser import parse +from cookbook.helper.permission_helper import group_required +from cookbook.models import TelegramBot, ShoppingList, ShoppingListEntry, Food, Unit + + +@group_required('user') +def setup_bot(request, pk): + bot = get_object_or_404(TelegramBot, pk=pk, space=request.space) + + hook_url = f'{request.build_absolute_uri("/")}telegram/hook/{bot.webhook_token}/' + + create_response = requests.get(f'https://api.telegram.org/bot{bot.token}/setWebhook?url={hook_url}') + info_response = requests.get(f'https://api.telegram.org/bot{bot.token}/getWebhookInfo') + + return JsonResponse({'hook_url': hook_url, 'create_response': json.loads(create_response.content.decode()), 'info_response': json.loads(info_response.content.decode())}, json_dumps_params={'indent': 4}) + + +@group_required('user') +def remove_bot(request, pk): + bot = get_object_or_404(TelegramBot, pk=pk, space=request.space) + + remove_response = requests.get(f'https://api.telegram.org/bot{bot.token}/deleteWebhook') + info_response = requests.get(f'https://api.telegram.org/bot{bot.token}/getWebhookInfo') + + return JsonResponse({'remove_response': json.loads(remove_response.content.decode()), 'info_response': json.loads(info_response.content.decode())}, json_dumps_params={'indent': 4}) + + +@csrf_exempt +def hook(request, token): + print(request.POST, request.body, token) + + tb = get_object_or_404(TelegramBot, webhook_token=token) + + data = json.loads(request.body.decode()) + + if tb.chat_id == '': + tb.chat_id = data['message']['chat']['id'] + tb.save() + + if tb.chat_id == str(data['message']['chat']['id']): + sl = ShoppingList.objects.filter(Q(created_by=tb.created_by)).filter(finished=False, space=tb.space).order_by('-created_at').first() + if sl: + print(f'found shopping list {sl} adding {data["message"]["text"]}') + amount, unit, ingredient, note = parse(data['message']['text']) + f, created = Food.objects.get_or_create(name=ingredient, space=tb.space) + u, created = Unit.objects.get_or_create(name=unit, space=tb.space) + sl.entries.add( + ShoppingListEntry.objects.create( + food=f, unit=u, amount=amount + ) + ) + return JsonResponse({'data': data['message']['text']}) + + return JsonResponse({}) diff --git a/docs/features/telegram_bot.md b/docs/features/telegram_bot.md new file mode 100644 index 00000000..c31f08f3 --- /dev/null +++ b/docs/features/telegram_bot.md @@ -0,0 +1,52 @@ +The telegram bot is meant to simplify certain interactions with Tandoor. +It is currently very basic but might be expanded in the future. + +!!! warning "Experimental" + This feature is considered experimental. You can use it and it should not break anything but you might be + required to update your configuration in the future. + The setup is also definitely not user-friendly, this will likely improve if the feature is well-received/expanded. + +!!! info "Public IP/Domain" + To use the Telegram Bot you will need an installation that is accessible from the outside, otherwise telegram can't send messages. + This could be circumvented using the polling API but this is currently not implemented. + +## Shopping Bot +The shopping bot will add any message you send it to your latest open shopping list. + +To get a shopping bot follow these steps + +1. Create a new Telegram Bot using the [BotFather](https://t.me/botfather) + - If you want to use the bot with multiple persons add the bot to a group and grant it admin privileges +2. Open the Admin Page (click your username, then admin) and select `Telegram Bots` +3. Create a new Bot + - token: the token obtained in step one + - space: your space (usually Default) + - user: to the user the bot is meant for (determines the shopping list used) + - chat id: if you know where messages will be sent from enter the chat ID, otherwise it is set to the first chat the bot received a message from +4. Visit your installation at `recipes.mydomin.tld/telegram/setup/` with botid being the ID of the bot you just created + You should see the following message: + ``` + { + "hook_url": "https://recipes.mydomin.tld/telegram/hook/c0c08de9-5e1e-4480-8312-3e256af61340/", + "create_response": { + "ok": true, + "result": true, + "description": "Webhook was set" + }, + "info_response": { + "ok": true, + "result": { + "url": "recipes.mydomin.tld/telegram/hook/", + "has_custom_certificate": false, + "pending_update_count": 0, + "max_connections": 40, + "ip_address": "46.4.105.116" + } + } + } + ``` + +You should now be able to send messages to the bot and have the entries appear in your latest shopping list. + +### Resetting +To reset a bot open `recipes.mydomin.tld/telegram/remove/` \ No newline at end of file