Sfoglia il codice sorgente

Add the Telegram SeumBookBot!

Basile Bruneau 8 anni fa
parent
commit
431ec42fa8

+ 0 - 0
bot/__init__.py


+ 9 - 0
bot/admin.py

@@ -0,0 +1,9 @@
1
+from django.contrib import admin
2
+
3
+# Register your models here.
4
+from .models import TelegramUser, TelegramUserCheck, TelegramUserChat, TelegramChat
5
+
6
+admin.site.register(TelegramUser)
7
+admin.site.register(TelegramUserCheck)
8
+admin.site.register(TelegramUserChat)
9
+admin.site.register(TelegramChat)

+ 5 - 0
bot/apps.py

@@ -0,0 +1,5 @@
1
+from django.apps import AppConfig
2
+
3
+
4
+class BotConfig(AppConfig):
5
+    name = 'bot'

+ 70 - 0
bot/migrations/0001_initial.py

@@ -0,0 +1,70 @@
1
+# -*- coding: utf-8 -*-
2
+# Generated by Django 1.10 on 2017-05-08 14:02
3
+from __future__ import unicode_literals
4
+
5
+from django.db import migrations, models
6
+import django.db.models.deletion
7
+
8
+
9
+class Migration(migrations.Migration):
10
+
11
+    initial = True
12
+
13
+    dependencies = [
14
+        ('counter', '0008_auto_20170122_1732'),
15
+    ]
16
+
17
+    operations = [
18
+        migrations.CreateModel(
19
+            name='TelegramChat',
20
+            fields=[
21
+                ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
22
+                ('chat_id', models.BigIntegerField(unique=True, verbose_name='telegram_chat_id')),
23
+                ('notify_only_members', models.BooleanField(verbose_name='notify_only_members')),
24
+            ],
25
+            options={
26
+                'verbose_name': 'telegram_chat',
27
+                'verbose_name_plural': 'telegram_chats',
28
+            },
29
+        ),
30
+        migrations.CreateModel(
31
+            name='TelegramUser',
32
+            fields=[
33
+                ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
34
+                ('telegram_user_id', models.BigIntegerField(unique=True, verbose_name='telegram_user_id')),
35
+                ('counter', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='counter.Counter', verbose_name='counter')),
36
+            ],
37
+            options={
38
+                'verbose_name': 'telegram_user',
39
+                'verbose_name_plural': 'telegram_users',
40
+            },
41
+        ),
42
+        migrations.CreateModel(
43
+            name='TelegramUserChat',
44
+            fields=[
45
+                ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
46
+                ('telegram_user_id', models.BigIntegerField(verbose_name='telegram_user_id')),
47
+                ('telegram_chat_id', models.BigIntegerField(verbose_name='telegram_chat_id')),
48
+            ],
49
+            options={
50
+                'verbose_name': 'telegram_user_chat',
51
+                'verbose_name_plural': 'telegram_user_chats',
52
+            },
53
+        ),
54
+        migrations.CreateModel(
55
+            name='TelegramUserCheck',
56
+            fields=[
57
+                ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
58
+                ('telegram_user_id', models.BigIntegerField(verbose_name='telegram_user_id')),
59
+                ('verif_key', models.TextField(unique=True, verbose_name='verify_key')),
60
+            ],
61
+            options={
62
+                'verbose_name': 'telegram_user_check',
63
+                'verbose_name_plural': 'telegram_user_checks',
64
+            },
65
+        ),
66
+        migrations.AlterUniqueTogether(
67
+            name='telegramuserchat',
68
+            unique_together=set([('telegram_user_id', 'telegram_chat_id')]),
69
+        ),
70
+    ]

+ 0 - 0
bot/migrations/__init__.py


+ 65 - 0
bot/models.py

@@ -0,0 +1,65 @@
1
+from datetime import datetime
2
+
3
+from django.contrib.auth.models import User
4
+from django.db import models
5
+from django.utils.translation import ugettext_lazy as _, get_language
6
+
7
+import arrow
8
+from babel.dates import format_timedelta
9
+
10
+# Link a SeumBook counter to a Telegram User
11
+class TelegramUser(models.Model):
12
+    counter = models.ForeignKey('counter.Counter', verbose_name=_('counter'))
13
+    telegram_user_id = models.BigIntegerField(_('telegram_user_id'), unique=True)
14
+
15
+    class Meta:
16
+        verbose_name = _('telegram_user')
17
+        verbose_name_plural = _('telegram_users')
18
+
19
+    def __str__(self):
20
+        return _('%(counter)s is %(telegram_user_id)d') % {'counter': self.counter, 'telegram_user_id': self.telegram_user_id}
21
+
22
+# When a user wants to link his SeumBook account to his Telegram account,
23
+# he/she send a message to the bot in a private chat, then the bot answers with
24
+# an URL to the SeumBook website containing a `verif_key`. The user then log in
25
+# on the SeumBook website, and based on the `verif_key` parameter, we find the
26
+# corresponding Telegram User.
27
+# This object remember which Telegram User received which `verif_key`
28
+class TelegramUserCheck(models.Model):
29
+    telegram_user_id = models.BigIntegerField(_('telegram_user_id'))
30
+    verif_key = models.TextField(_('verify_key'), unique=True)
31
+
32
+    class Meta:
33
+        verbose_name = _('telegram_user_check')
34
+        verbose_name_plural = _('telegram_user_checks')
35
+
36
+    def __str__(self):
37
+        return _('%(telegram_user_id)d has verif key %(verif_key)s') % {'telegram_user_id': self.telegram_user_id, 'verif_key': self.verif_key}
38
+
39
+# Memorize which telegram user is in which chat
40
+class TelegramUserChat(models.Model):
41
+    telegram_user_id = models.BigIntegerField(_('telegram_user_id'))
42
+    telegram_chat_id = models.BigIntegerField(_('telegram_chat_id'))
43
+
44
+    class Meta:
45
+        verbose_name = _('telegram_user_chat')
46
+        verbose_name_plural = _('telegram_user_chats')
47
+        unique_together = ('telegram_user_id', 'telegram_chat_id')
48
+
49
+    def __str__(self):
50
+        return _('%(telegram_user_id)d is in the chat %(telegram_chat)d') % {'telegram_user_id': self.telegram_user_id, 'telegram_chat_id': self.telegram_chat_id}
51
+
52
+# Memorize the Telegram chats in which the bot are, and the options of them
53
+class TelegramChat(models.Model):
54
+    chat_id = models.BigIntegerField(_('telegram_chat_id'), unique=True)
55
+    # notify_only_members: True: only when somebody we know is in the chat has the
56
+    # seum, we notify the channel
57
+    # notify_only_members: False: we notify the channel for every new seum
58
+    notify_only_members = models.BooleanField(_('notify_only_members'))
59
+
60
+    class Meta:
61
+        verbose_name = _('telegram_chat')
62
+        verbose_name_plural = _('telegram_chats')
63
+
64
+    def __str__(self):
65
+        return _('%(chat_id)d is a telegram chat, with option notify_only_members to %(notify_only_members)s') % {'chat_id': self.chat_id, 'notify_only_members': self.notify_only_members}

+ 11 - 0
bot/urls.py

@@ -0,0 +1,11 @@
1
+from django.conf.urls import url
2
+from counter.rss import SeumFeed
3
+from django.contrib.auth import views as auth_views
4
+from django.views.generic.base import RedirectView
5
+
6
+from .views import telegram
7
+
8
+urlpatterns = [
9
+    url(r'^webhook/$', telegram.webhook, name='telwebhook'),
10
+    url(r'^link/telegram/(?P<verif_key>.+)/$', telegram.link, name='tellink'),
11
+]

+ 175 - 0
bot/views/telegram.py

@@ -0,0 +1,175 @@
1
+from django.contrib.auth.models import User
2
+from django.core.urlresolvers import reverse
3
+from django.shortcuts import render
4
+from django.utils.translation import ugettext as _
5
+from django.views.decorators.csrf import csrf_exempt
6
+from django.http import HttpResponse
7
+from django.contrib.auth.decorators import login_required
8
+from django.db.models.signals import post_save
9
+from django.dispatch import receiver
10
+from django.db.utils import IntegrityError
11
+from django.conf import settings
12
+
13
+import json
14
+import requests
15
+import random
16
+import string
17
+import re
18
+
19
+from counter.models import Counter, Reset
20
+from bot.models import TelegramUser, TelegramUserCheck, TelegramUserChat, TelegramChat
21
+
22
+telegram_ips = ['149.154.167.' + str(i) for i in range(197, 234)]
23
+telegram_url = 'https://api.telegram.org/bot' + settings.BOT_TELEGRAM_KEY + '/'
24
+telegram_bot_id = settings.BOT_TELEGRAM_ID
25
+telegram_bot_name = settings.BOT_TELEGRAM_NAME
26
+
27
+@receiver(post_save, sender=Reset)
28
+def notify_telegram(sender, instance, created, **kwargs):
29
+    if not settings.BOT_TELEGRAM_KEY or not settings.telegram_bot_id or not settings.telegram_bot_name:
30
+        return
31
+    if created:
32
+        chat_ids = [e.chat_id for e in TelegramChat.objects.filter(notify_only_members=False)]
33
+        try:
34
+            telegram_user = TelegramUser.objects.get(counter=instance.counter)
35
+            chats = TelegramUserChat.objects.filter(telegram_user_id=telegram_user.telegram_user_id)
36
+            chat_ids = chat_ids + [e.telegram_chat_id for e in chats]
37
+        except TelegramUser.DoesNotExist:
38
+            do_nothing = True
39
+
40
+        if instance.who is None or instance.who == instance.counter:
41
+            message = str(instance.counter) + ' has le seum: ' + instance.reason
42
+        else:
43
+            message = str(instance.who) + ' put le seum to ' + str(instance.counter) + ': ' + instance.reason
44
+
45
+        for chat_id in set(chat_ids):
46
+            requests.post(telegram_url + 'sendMessage', json={'chat_id': chat_id, 'text': message})
47
+
48
+@login_required
49
+def link(request, verif_key):
50
+    try:
51
+        telegram_user_check = TelegramUserCheck.objects.get(verif_key=verif_key)
52
+        the_counter = Counter.objects.get(user__id=request.user.id)
53
+        TelegramUser.objects.create(counter=the_counter, telegram_user_id=telegram_user_check.telegram_user_id)
54
+        TelegramUserCheck.objects.filter(telegram_user_id=telegram_user_check.telegram_user_id).delete()
55
+        return HttpResponse('Your Telegram account has been linked!')
56
+    except TelegramUserCheck.DoesNotExist:
57
+        return HttpResponse(status=404)
58
+
59
+@csrf_exempt
60
+def webhook(request):
61
+    ip = request.META.get('REMOTE_ADDR')
62
+
63
+    # Uncomment the following two lines, and correctly configure the
64
+    # reverse proxy to enable the security, or everyone will be able
65
+    # to put le seum to everyone
66
+    if not ip in telegram_ips:
67
+        return HttpResponse(status=401)
68
+
69
+    data = json.loads(request.body.decode('utf-8'))
70
+    print(data)
71
+
72
+    # We have different types of messages
73
+    # - a simple text message from a person
74
+    # - the bot joined/left a channel
75
+    # - somebody joined/left a channel
76
+    # The idea is to keep a list of all the telegram users in all channels
77
+    # Then when a new seum is created, we look all the channels in which this user
78
+    # is, and we send a message in those to notify everybody
79
+
80
+    if not 'message' in data or not 'chat' in data['message']:
81
+        return HttpResponse(201) # we should return something correct, or Telegram will try to send us the message again multiple times
82
+
83
+    chat = data['message']['chat']
84
+
85
+    if chat['type'] != 'private':
86
+        if 'new_chat_member' in data['message']:
87
+            user_id = data['message']['new_chat_member']['id']
88
+            if user_id == telegram_bot_id:
89
+                r = requests.get(telegram_url + 'getChatMembersCount?chat_id=' + str(chat['id'])).json()
90
+                if r['result'] < 20: # when there are less than 20 people, we deactivate notify_only_members
91
+                    try:
92
+                        TelegramChat.objects.create(chat_id=chat['id'], notify_only_members=True)
93
+                    except IntegrityError as e:
94
+                        print(e)
95
+                        return HttpResponse('')
96
+                    requests.post(telegram_url + 'sendMessage', json={'chat_id': chat['id'], 'text': 'Hello everyone! Because of Telegram restrictions, I don\'t know who is here :( . Everybody, please say /seumhello, so I can see you\'re here!'})
97
+                else:
98
+                    TelegramChat.objects.create(chat_id=chat['id'], notify_only_members=False)
99
+                    requests.post(telegram_url + 'sendMessage', json={'chat_id': chat['id'], 'text': 'Hello everyone! I will notify you everytime a person in the world has the seum. I you prefer to be notified only when a member of this group has the seum, use the command /notify_every_seum_or_not'})
100
+            else:
101
+                TelegramUserChat.objects.create(telegram_user_id=user_id, telegram_chat_id=chat['id'])
102
+            return HttpResponse('')
103
+
104
+        if 'left_chat_member' in data['message']:
105
+            user_id = data['message']['left_chat_member']['id']
106
+            if user_id == telegram_bot_id:
107
+                TelegramUserChat.objects.filter(telegram_chat_id=chat['id']).delete()
108
+                TelegramChat.objects.filter(chat_id=chat['id']).delete()
109
+            else:
110
+                TelegramUserChat.objects.filter(telegram_user_id=user_id, telegram_chat_id=chat['id']).delete()
111
+            return HttpResponse(200)
112
+
113
+    if not 'message' in data or not 'from' in data['message'] or not 'id' in data['message']['from']:
114
+        return HttpResponse(201)
115
+
116
+    telegram_user_id = data['message']['from']['id']
117
+
118
+    if chat['type'] != 'private':
119
+        # For each message we receive in a non private chat, we save that this user is in this chat
120
+        try:
121
+            TelegramUserChat.objects.create(telegram_user_id=telegram_user_id, telegram_chat_id=chat['id'])
122
+        except:
123
+            do_nothing = True
124
+
125
+    text = data['message']['text']
126
+    if text == '/notify_every_seum_or_not' or text == '/notify_every_seum_or_not@' + telegram_bot_name:
127
+        tchat = TelegramChat.objects.get(chat_id=chat['id'])
128
+        tchat.notify_only_members = not tchat.notify_only_members
129
+        if tchat.notify_only_members:
130
+            requests.post(telegram_url + 'sendMessage', json={'chat_id': chat['id'], 'text': 'Ok, I will notify you only if someone in the group has the seum. But, because of Telegram restrictions, I don\'t know who is here :( . Everybody, please say /seumhello, so I can see you\'re here!'})
131
+        else:
132
+            requests.post(telegram_url + 'sendMessage', json={'chat_id': chat['id'], 'text': 'Ok, so now I will notify you everytime a person in the world has the seum.'})
133
+        tchat.save()
134
+
135
+    try:
136
+        telegram_user = TelegramUser.objects.get(telegram_user_id=telegram_user_id)
137
+        # in that cas we need to parse the message
138
+        # and either create a new seum and reset a counter
139
+        # either like some existing seum
140
+
141
+        if text == '/seumunlink' or text == '/seumunlink@' + telegram_bot_name:
142
+            TelegramUser.objects.filter(telegram_user_id=telegram_user_id).delete()
143
+            requests.post(telegram_url + 'sendMessage', json={'chat_id': chat['id'], 'text': 'Your Telegram account has successfully been unlinked from your SeumBook account', 'reply_to_message_id': data['message']['message_id']})
144
+            return HttpResponse('')
145
+
146
+        if text == '/seumhello' or text == '/seumhello@' + telegram_bot_name:
147
+            requests.post(telegram_url + 'sendMessage', json={'chat_id': chat['id'], 'text': 'Hello ' + telegram_user.counter.name + ' :-)', 'reply_to_message_id': data['message']['message_id']})
148
+            return HttpResponse('')
149
+
150
+        seum_cmd = r"^/seum((@" + telegram_bot_name + ")?) (.+)$"
151
+        if re.match(seum_cmd, text) is not None:
152
+            # it's a /seum cmd
153
+            m = re.sub(seum_cmd, r"\3", text)
154
+            maybe_counter = m.split(' ')[0]
155
+            try:
156
+                yes_counter = Counter.objects.get(trigramme=maybe_counter)
157
+                seum_message = ' '.join(m.split(' ')[1:])
158
+            except Counter.DoesNotExist:
159
+                yes_counter = telegram_user.counter
160
+                seum_message = m
161
+            reset = Reset(counter=yes_counter, who=telegram_user.counter, reason=seum_message)
162
+            reset.save()
163
+    except TelegramUser.DoesNotExist:
164
+        print('in that case we send a link to the user')
165
+        if chat['type'] == 'private' and chat['id'] == telegram_user_id:
166
+            # We are in a private channel, we directly send the link
167
+            verif_key = ''.join(random.choice(string.ascii_uppercase + string.digits) for _ in range(30))
168
+            requests.post(telegram_url + 'sendMessage', json={'chat_id': chat['id'], 'text': 'Open the following URL to link your Telegram account to your SeumBook account: ' + request.build_absolute_uri(reverse('tellink', args=[verif_key]))})
169
+            TelegramUserCheck.objects.create(telegram_user_id=telegram_user_id, verif_key=verif_key)
170
+        else:
171
+            print('bou')
172
+            # We are not in a private channel, so we mention the user to talk with us
173
+            requests.post(telegram_url + 'sendMessage', json={'chat_id': chat['id'], 'text': 'Your Telegram account isn\'t linked to a SeumBook account. Say hello to me in a private chat to link it :-)! https://telegram.me/' + telegram_bot_name + '?start=Hello', 'reply_to_message_id': data['message']['message_id']})
174
+
175
+    return HttpResponse('')

+ 1 - 0
requirements.txt

@@ -6,3 +6,4 @@ django-debug-toolbar==1.6
6 6
 django-graphos-3
7 7
 pandas==0.19
8 8
 django_extensions==1.7.8
9
+requests==2.13.0

+ 4 - 0
seum/settings.py.default

@@ -124,6 +124,10 @@ EMAIL_HOST_PASSWORD = ''
124 124
 EMAIL_USE_TLS = False
125 125
 DEFAULT_FROM_EMAIL = 'SeumMan <seum@merigoux.ovh>'
126 126
 
127
+# Telegram Bot
128
+# BOT_TELEGRAM_KEY = 'telegram-bot-key'
129
+# BOT_TELEGRAM_ID = 1234
130
+# BOT_TELEGRAM_NAME = 'telegramBot'
127 131
 
128 132
 #Production settings
129 133
 SECURE_CONTENT_TYPE_NOSNIFF = True

+ 1 - 0
seum/urls.py

@@ -20,6 +20,7 @@ from django.contrib import admin
20 20
 from django.views.generic.base import RedirectView
21 21
 
22 22
 urlpatterns = [
23
+    url(r'^bot/', include('bot.urls')),
23 24
     url(r'^i18n/', include('django.conf.urls.i18n'), name='set_language'),]
24 25
 
25 26
 urlpatterns += i18n_patterns(