Queer European MD passionate about IT
Explorar o código

Allow users to link an email to their Telegram account. If their email is listed in the patrons list, send them an invite link

Davte %!s(int64=3) %!d(string=hai) anos
pai
achega
fc0b29709a
Modificáronse 6 ficheiros con 297 adicións e 3 borrados
  1. 4 0
      bic_bot/authorization.py
  2. 3 1
      bic_bot/bot.py
  3. 218 0
      bic_bot/email_verification.py
  4. 66 0
      bic_bot/messages.py
  5. 5 2
      bic_bot/patreon.py
  6. 1 0
      requirements.txt

+ 4 - 0
bic_bot/authorization.py

@@ -40,7 +40,11 @@ async def set_chat(bot: davtelepot.bot.Bot, update: dict, language: str):
 def init(telegram_bot: davtelepot.bot.Bot):
     if 'information' not in telegram_bot.db.tables:
         table = telegram_bot.db.create_table('information')
+    else:
+        table = telegram_bot.db.get_table('information')
+    if 'name' not in table.columns:
         table.create_column('name', telegram_bot.db.types.string(25))
+    if 'value' not in table.columns:
         table.create_column('value', telegram_bot.db.types.string(100))
     telegram_bot.messages['elevation'] = elevation_messages
     bic_chat_id = telegram_bot.db['information'].find_one(name='bic_chat_id')

+ 3 - 1
bic_bot/bot.py

@@ -6,7 +6,7 @@ import davtelepot.bot
 from davtelepot.messages import (default_unknown_command_message as unknown_command_message,
                                  default_authorization_denied_message as authorization_denied_message)
 
-from . import authorization, patreon
+from . import authorization, email_verification, patreon
 from .messages import language_messages, supported_languages
 
 current_path = os.path.dirname(
@@ -88,9 +88,11 @@ def run():
     davtelepot.authorization.init(telegram_bot=bic_bot)
     authorization.init(telegram_bot=bic_bot)
     davtelepot.administration_tools.init(telegram_bot=bic_bot)
+    email_verification.init(telegram_bot=bic_bot)
     patreon.init(telegram_bot=bic_bot)
     davtelepot.languages.init(telegram_bot=bic_bot,
                               language_messages=language_messages,
                               supported_languages=supported_languages)
+    davtelepot.helper.init(telegram_bot=bic_bot)
     exit_code = bic_bot.run()
     sys.exit(exit_code)

+ 218 - 0
bic_bot/email_verification.py

@@ -0,0 +1,218 @@
+import asyncio
+import datetime
+import logging
+import os
+import re
+import string
+from email.mime.text import MIMEText
+
+import aiosmtplib
+import davtelepot
+
+email_regex = re.compile(r'[A-z\-_0-9]{1,65}@[A-z\-_0-9]{1,320}\.[A-z]{2,24}', flags=re.I)
+validity_timedelta = datetime.timedelta(minutes=15)
+
+current_path = os.path.dirname(
+            os.path.abspath(
+                __file__
+            )
+        )
+
+
+def append_to_passwords_file(line_to_append):
+    with open(f'{current_path}/data/passwords.py', 'a') as passwords_file:
+        passwords_file.write(line_to_append)
+
+
+try:
+    from .data.passwords import mail_server_address
+except ImportError:
+    mail_server_address = input("Enter the mail server address:\n\t\t")
+    append_to_passwords_file(f'mail_server_address = "{mail_server_address}"\n')
+
+try:
+    from .data.passwords import mail_username
+except ImportError:
+    mail_username = input("Enter the mail server username:\n\t\t")
+    append_to_passwords_file(f'mail_username = "{mail_username}"\n')
+
+try:
+    from .data.passwords import mail_password
+except ImportError:
+    mail_password = input("Enter the mail server password:\n\t\t")
+    append_to_passwords_file(f'mail_password = "{mail_password}"\n')
+
+
+async def invite_new_patrons(bot: davtelepot.bot.Bot):
+    invite_link = await get_invite_link(bot=bot)
+    for record in bot.db.query("SELECT u.*, p.id patron_id, c.id confirm_id FROM patrons p "
+                               "LEFT JOIN users u ON u.id = p.user_id "
+                               "LEFT JOIN confirmation_codes c ON c.user_id = u.id "
+                               "WHERE p.tier AND (p.is_in_chat IS NULL OR p.is_in_chat = 0) AND NOT c.notified"):
+        try:
+            await bot.send_message(
+                chat_id=record['telegram_id'],
+                text=bot.get_message('patreon', 'confirmation_email', 'notification',
+                                     invite_link=invite_link,
+                                     user_record=record)
+            )
+            bot.db['confirmation_codes'].update(
+                dict(id=record['confirm_id'], notified=True),
+                ['id']
+            )
+        except Exception as e:
+            logging.error(e)
+
+
+async def send_confirmation_email(recipient_email: str,
+                                  message_text: str,
+                                  message_subject: str = None):
+    message = MIMEText(message_text, 'html')
+    message['To'] = recipient_email
+    message['From'] = mail_username
+    message['Subject'] = message_subject
+    await aiosmtplib.send(message, hostname=mail_server_address,
+                          port=465, use_tls=True,
+                          sender=mail_username,
+                          username=mail_username, password=mail_password)
+
+
+async def link_email(bot: davtelepot.bot.Bot, update: dict, user_record: dict, language: str):
+    patron_record = bot.db['patrons'].find_one(user_id=user_record['id'],
+                                               order_by=['-id'])
+    if patron_record:  # If the user is already verified, send invite link
+        return await verify(bot=bot, update=update, user_record=user_record, language=language)
+    email_address = email_regex.search(update['text'].lower())
+    telegram_account = davtelepot.utilities.get_user(record=user_record)
+    confirmation_code = davtelepot.utilities.get_secure_key(
+        allowed_chars=(string.ascii_uppercase + string.ascii_lowercase + string.digits),
+        length=12
+    )
+    if email_address is None:
+        return bot.get_message('patreon', 'confirmation_email', 'invalid_email_address')
+    email_address = email_address.group()
+    bot.db['confirmation_codes'].upsert(
+        dict(
+            user_id=user_record['id'],
+            email=email_address,
+            code=confirmation_code,
+            expiry=datetime.datetime.now() + validity_timedelta,
+            times_tried=0,
+            notified=0
+        ),
+        ['email']
+    )
+    message_text = bot.get_message('patreon', 'confirmation_email', 'text',
+                                   confirmation_link=f"https://t.me/{bot.name}?start=00verify_{confirmation_code}",
+                                   confirmation_code=confirmation_code,
+                                   telegram_account=telegram_account,
+                                   bot=bot,
+                                   language=language)
+    message_subject = bot.get_message('patreon', 'confirmation_email', 'subject',
+                                      language=language)
+    await send_confirmation_email(recipient_email=email_address, message_text=message_text,
+                                  message_subject=message_subject)
+    return bot.get_message('patreon', 'confirmation_email', 'sent',
+                           language=language)
+
+
+async def verify(bot: davtelepot.bot.Bot, update: dict, user_record: dict, language: str):
+    if not ('chat' in update and update['chat']['id'] > 0):  # Ignore public messages
+        return
+    patron_record = bot.db['patrons'].find_one(user_id=user_record['id'],
+                                               order_by=['-id'])
+    text = update['text']
+    confirmation_code = re.findall(r'(?:00verif.{1,3}_)?([A-z0-9]{12})', text)
+    confirmation_record = bot.db['confirmation_codes'].find_one(user_id=user_record['id'],
+                                                                order_by=['-id'])
+    invite_link = None
+    if patron_record is not None and patron_record['tier']:
+        invite_link = await bot.shared_data['get_invite_link'](bot=bot)
+        message_fields = ['patreon', 'confirmation_email', 'send_invite_link']
+    elif patron_record is not None:
+        message_fields = ['patreon', 'confirmation_email', 'wait_for_invite_link']
+    elif (confirmation_record
+            and davtelepot.utilities.str_to_datetime(confirmation_record['expiry'])
+            < datetime.datetime.now()) or (confirmation_record and confirmation_record['times_tried'] > 2):
+        message_fields = ['patreon', 'confirmation_email', 'expired_code']
+    elif confirmation_code and confirmation_record is not None and confirmation_code[0] == confirmation_record['code']:
+        bot.db['patrons'].upsert(
+            dict(
+                email=confirmation_record['email'],
+                user_id=user_record['id']
+            ),
+            'email'
+        )
+        message_fields = ['patreon', 'confirmation_email', 'wait_for_invite_link']
+    else:
+        message_fields = ['patreon', 'confirmation_email', 'confirmation_failed']
+        confirmation_record['times_tried'] += 1
+        bot.db['confirmation_codes'].update(
+            confirmation_record,
+            ['id']
+        )
+    return bot.get_message(*message_fields, invite_link=invite_link, language=language)
+
+
+async def change_invite_link(bot: davtelepot.bot.Bot, old_link: str, sleep_time: int = 0):
+    await asyncio.sleep(sleep_time)
+    if old_link == bot.db['information'].find_one(name='invite_link')['value']:
+        await bot.exportChatInviteLink(chat_id=bot.shared_data['bic_chat_id'])
+
+
+async def get_invite_link(bot: davtelepot.bot.Bot):
+    invite_link_expiry = bot.db['information'].find_one(name='invite_link_expiry')
+    if (invite_link_expiry is None
+            or davtelepot.utilities.str_to_datetime(invite_link_expiry['value'])
+            <= datetime.datetime.now()):
+        invite_link = await bot.exportChatInviteLink(chat_id=bot.shared_data['bic_chat_id'])
+        bot.db['information'].upsert(
+            dict(name='invite_link_expiry',
+                 value=datetime.datetime.now() + datetime.timedelta(hours=1),
+                 ),
+            ['name']
+        )
+        bot.db['information'].upsert(
+            dict(name='invite_link',
+                 value=invite_link),
+            ['name']
+        )
+    invite_link = bot.db['information'].find_one(name='invite_link')['value']
+    # Inactivate the invite link after 60 minutes
+    asyncio.ensure_future(change_invite_link(bot=bot, old_link=invite_link, sleep_time=3600))
+    return invite_link
+
+
+def init(telegram_bot: davtelepot.bot.Bot):
+    telegram_bot.shared_data['get_invite_link'] = get_invite_link
+    asyncio.ensure_future(get_invite_link(bot=telegram_bot))
+    asyncio.ensure_future(invite_new_patrons(bot=telegram_bot))
+    if 'confirmation_codes' not in telegram_bot.db.tables:
+        table = telegram_bot.db.create_table('confirmation_codes')
+    else:
+        table = telegram_bot.db.get_table('confirmation_codes')
+    if 'user_id' not in table.columns:
+        table.create_column('user_id', telegram_bot.db.types.integer)
+    if 'email' not in table.columns:
+        table.create_column('email', telegram_bot.db.types.string(320))
+    if 'code' not in table.columns:
+        table.create_column('code', telegram_bot.db.types.string(12))
+    if 'times_tried' not in table.columns:
+        table.create_column('times_tried', telegram_bot.db.types.integer)
+    if 'expiry' not in table.columns:
+        table.create_column('expiry', telegram_bot.db.types.datetime)
+    if 'notified' not in table.columns:
+        table.create_column('notified', telegram_bot.db.types.boolean)
+
+    @telegram_bot.command('/email',
+                          authorization_level='everybody')
+    async def _link_email(bot, update, user_record, language):
+        return await link_email(bot=bot, update=update,
+                                user_record=user_record, language=language)
+
+    @telegram_bot.command('/verify',
+                          aliases=['verifica', '/verifica', '00verifica', '00verify'],
+                          authorization_level='everybody')
+    async def _verify(bot, update, user_record, language):
+        return await verify(bot=bot, update=update,
+                            user_record=user_record, language=language)

+ 66 - 0
bic_bot/messages.py

@@ -55,6 +55,72 @@ language_messages = {
 }
 
 patreon_messages = {
+    'confirmation_email': {
+        'confirmation_failed': {
+            'en': "Wrong confirmation code. Check your email and try again: /verify",
+            'it': "Codice di verifica errato. Controlla la mail e prova di nuovo: /verifica",
+        },
+        'expired_code': {
+            'en': "The code has expired. Please try again: /email",
+            'it': "Codice di verifica scaduto. Ottienine un altro: /email",
+        },
+        'invalid_email_address': {
+            'en': "The email you provided is invalid. Please try again.",
+            'it': "La mail inserita non è valida. Per favore riprova.",
+        },
+        'notification': {
+            'en': "🔔 Your status as patron has been verified.\n"
+                  "You can now join the chat clicking this temporary link:\n"
+                  "{invite_link}\n\n"
+                  "If the link has expired, click /verify to get a new one.",
+            'it': "🔔 Il tuo ruolo di patron è stato confermato.\n"
+                  "Ora puoi unirti alla chat usando questo link temporaneo:\n"
+                  "{invite_link}\n\n"
+                  "Se il link è scaduto, clicka /verifica per ottenerne uno nuovo.",
+        },
+        'send_invite_link': {
+            'en': "You can join the chat clicking this temporary link:\n"
+                  "{invite_link}\n\n"
+                  "If the link has expired, click /verify to get a new one.",
+            'it': "Puoi unirti alla chat usando questo link temporaneo:\n"
+                  "{invite_link}\n\n"
+                  "Se il link è scaduto, clicka /verifica per ottenerne uno nuovo.",
+        },
+        'sent': {
+            'en': "✉️ Check your email, including the SPAM folder 🗑",
+            'it': "✉️ Controlla la mail, compresa la cartella SPAM 🗑",
+        },
+        'subject': {
+            'en': "Confirm your email",
+            'it': "Conferma la tua email",
+        },
+        'text': {
+            'en': "<h1>Welcome!</h1>\n"
+                  "<p>Click <a href=\"{confirmation_link}\">this link</a> or send <a href=\"https://t.me/{"
+                  "bot.name}\">@{bot.name}</a> <code>/verify {confirmation_code}</code> to link this email address to "
+                  "the telegram account {telegram_account}.</p> <p>As soon as your email will be listed among "
+                  "patrons, you will receive the invite link to the Breaking Italy Club chat! Just wait a few days "
+                  "=)</p>",
+            'it': "<h1>Bevenutə!</h1>\n"
+                  "<p>Clicka su <a href=\"{confirmation_link}\">questo link</a> oppure scrivi <code>/verifica {"
+                  "confirmation_code}</code> a <a href=\"https://t.me/{bot.name}\">@{bot.name}</a> per associare "
+                  "questo indirizzo email all'account telegram {telegram_account}.</p> "
+                  "<p>Appena la tua email sarà inserita nell'elenco dei patron riceverai il link di invito nella chat "
+                  "del Breaking Italy Club! Pazienta qualche giorno =)</p>",
+        },
+        'wait_for_invite_link': {
+            'en': "Your email has been linked with this Telegram account.\n"
+                  "However, it is not included in the patrons list yet.\n"
+                  "Please wait a few days, I will notify you when you can join the chat. 🔔\n"
+                  "When you are listed among patrons, you can get a temporary invite link to join the chat at any "
+                  "time: just click /verify",
+            'it': "La tua mail è stata associata a questo account Telegram.\n"
+                  "Tuttavia, ancora non compare nella lista dei patron.\n"
+                  "Pazienta qualche giorno, ti invierò un messaggio non appena potrai unirti alla chat. 🔔\n"
+                  "Quando comparirai nella lista dei patron, potrai ricevere un link temporaneo di invito per "
+                  "aggiungerti alla chat in qualiasi momento: basterà clickare /verifica",
+        },
+    },
     'join_chat': {
         'en': "Thank you for your Patreon subscription! You may enter ",
         'it': "",

+ 5 - 2
bic_bot/patreon.py

@@ -4,6 +4,7 @@ import os
 
 import davtelepot
 
+from .email_verification import invite_new_patrons
 from .messages import patreon_messages
 
 
@@ -42,6 +43,7 @@ async def handle_patrons_list_file(bot: davtelepot.bot.Bot, update: dict, langua
                 ['email']
             )
     asyncio.ensure_future(kick_unlisted_patrons(bot=bot))
+    asyncio.ensure_future(invite_new_patrons(bot=bot))
     return bot.get_message('patreon', 'list_updated', language=language)
 
 
@@ -63,12 +65,13 @@ async def handle_new_members(bot, update):
         return
     for member in update['new_chat_members']:
         user_record = bot.db['users'].find_one(telegram_id=member['id'])
-        patron_record = bot.db['patrons'].find_one(user_id=user_record['id'])
+        patron_record = bot.db['patrons'].find_one(user_id=user_record['id'],
+                                                   order_by=['-id'])
         # If user is not white-listed, kick them
         if patron_record is None or not patron_record['tier']:
             await bot.kickChatMember(chat_id=bot.shared_data['bic_chat_id'],
                                      user_id=user_record['telegram_id'])
-        else:  # Otherwise, take not of their joining
+        else:  # Otherwise, take note of their joining
             bot.db['patrons'].upsert(
                 dict(
                     user_id=user_record['id'],

+ 1 - 0
requirements.txt

@@ -1 +1,2 @@
+aiosmtplib
 davtelepot