diff --git a/README.md b/README.md index 968fe0a..664f308 100644 --- a/README.md +++ b/README.md @@ -3,8 +3,29 @@ Breaking Italy Club bot ## Instructions -```bash -git clone https://gogs.davte.it/Davte/bic_bot.git -cd bic_bot -bash ./run_me.sh -``` \ No newline at end of file +* Get a Telegram bot API token ([@botfather](https://t.me/botfather)), allow groups and set the bot [group privacy](https://core.telegram.org/bots#privacy-mode) to off. +* Install bic_bot + ```bash + git clone https://gogs.davte.it/Davte/bic_bot.git + cd bic_bot + bash ./run_me.sh + ``` +* Add the bot to the BIC chat as administrator +* Write `/set_chat` in the chat +* Send a csv file to the bot named "patrons.csv" with patreons emails + ```csv + person@example.com, + someone@gmail.com, + [...] + ``` +* [NOT YET IMPLEMENTED] Using Patreon's API, the patrons list will be automatically downloaded from the server +* Users will be able to link their email to their Telegram account automatically. A confirmation email will help to verify the association + +## Functioning +* Whenever a person joins the chat, the bot will check whether their Telegram account is linked to an email listed in the patrons list. If not, the user will be kicked out. +* Whenever an email is removed from the white list, the linked Telegram account will be kicked out. +* [NOT YET IMPLEMENTED] Any user can automatically link their email to their Telegram account. If their email is white-listed, they can join the chat. + +## Credits +This [davtelepot-based](https://gogs.davte.it/davte/davtelepot) bot has been developed by [@Davte](https://www.davte.it). +No official endorsement has been given by Breaking Italy. \ No newline at end of file diff --git a/bic_bot/authorization.py b/bic_bot/authorization.py index 861281e..767cfae 100644 --- a/bic_bot/authorization.py +++ b/bic_bot/authorization.py @@ -14,10 +14,46 @@ async def elevate_to_admin(bot: davtelepot.bot.Bot, language: str, user_record: return bot.get_message('elevation', 'denied', language=language) +async def set_chat(bot: davtelepot.bot.Bot, update: dict, language: str): + await bot.delete_message(update=update) + if 'chat' in update and 'id' in update['chat']: + chat_id = update['chat']['id'] + if chat_id > 0: + return bot.get_message('elevation', + 'chat_not_set', + language=language) + bot.db['information'].upsert( + dict( + name='bic_chat_id', + value=chat_id + ), + ['name'] + ) + bot.shared_data['bic_chat_id'] = chat_id + await bot.send_message(chat_id=update['from']['id'], + text=bot.get_message('elevation', + 'chat_set', + chat_id=chat_id, + language=language)) + + def init(telegram_bot: davtelepot.bot.Bot): + if 'information' not in telegram_bot.db.tables: + table = telegram_bot.db.create_table('information') + table.create_column('name', telegram_bot.db.types.string(25)) + 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') + if bic_chat_id is not None: + bic_chat_id = int(bic_chat_id['value']) + telegram_bot.shared_data['bic_chat_id'] = bic_chat_id @telegram_bot.command(command='elevate', authorization_level='everybody') async def _elevate_to_admin(bot, language, user_record): - return await elevate_to_admin(bot=bot, language=language, user_record=user_record) \ No newline at end of file + return await elevate_to_admin(bot=bot, language=language, user_record=user_record) + + @telegram_bot.command(command='set_chat', + authorization_level='admin') + async def _set_chat(bot, update, language): + return await set_chat(bot=bot, update=update, language=language) diff --git a/bic_bot/bot.py b/bic_bot/bot.py index b9ec149..78c1851 100644 --- a/bic_bot/bot.py +++ b/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 +from . import authorization, patreon from .messages import language_messages, supported_languages current_path = os.path.dirname( @@ -88,6 +88,7 @@ def run(): davtelepot.authorization.init(telegram_bot=bic_bot) authorization.init(telegram_bot=bic_bot) davtelepot.administration_tools.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) diff --git a/bic_bot/messages.py b/bic_bot/messages.py index 9a708f9..53779e3 100644 --- a/bic_bot/messages.py +++ b/bic_bot/messages.py @@ -1,4 +1,12 @@ elevation_messages = { + 'chat_not_set': { + 'en': "This chat cannot be set as BIC chat! ❌", + 'it': "Questa chat non può essere impostata come chat del BIC ❌", + }, + 'chat_set': { + 'en': "BIC chat ID set ✅\n🏷 Chat ID: {chat_id}", + 'it': "ID della chat del BIC salvato ✅\n🏷 ID chat: {chat_id}", + }, 'denied': { 'en': "You have no right elevate yourself to Founder! 🚫", 'it': "Non hai diritto a ottenere i privilegi di Fondatore! 🚫", @@ -46,6 +54,17 @@ language_messages = { } } +patreon_messages = { + 'join_chat': { + 'en': "Thank you for your Patreon subscription! You may enter ", + 'it': "", + }, + 'list_updated': { + 'en': "Patrons white list updated ✅", + 'it': "Lista dei patron aggiornata ✅", + }, +} + supported_languages = { 'en': { 'flag': '🇬🇧', @@ -55,4 +74,4 @@ supported_languages = { 'flag': '🇮🇹', 'name': 'Italiano' } -} \ No newline at end of file +} diff --git a/bic_bot/patreon.py b/bic_bot/patreon.py new file mode 100644 index 0000000..7cc17bf --- /dev/null +++ b/bic_bot/patreon.py @@ -0,0 +1,69 @@ +import asyncio +import logging +import os + +import davtelepot + +from .messages import patreon_messages + + +def is_patrons_list_file(update: dict): + return (update['document']['mime_type'] == 'text/csv' + and 'patron' in update['document']['file_name']) + + +async def kick_unlisted_patrons(bot: davtelepot.bot.Bot): + for record in bot.db.query("SELECT u.telegram_id telegram_id, p.id patron_id FROM patrons p " + "LEFT JOIN users u ON u.id = p.user_id " + "WHERE p.tier IS NULL AND p.is_in_chat = 1"): + try: + await bot.kickChatMember(chat_id=bot.shared_data['bic_chat_id'], + user_id=record['telegram_id']) + bot.db.query(f"UPDATE patrons SET is_in_chat = 0 WHERE id = {record['patron_id']}") + except Exception as e: + logging.error(e) + + +async def handle_patrons_list_file(bot: davtelepot.bot.Bot, update: dict, language: str): + file_name = davtelepot.utilities.get_secure_key(length=12) + while os.path.isfile(f'{bot.path}/data/{file_name}'): + file_name = davtelepot.utilities.get_secure_key(length=12) + await bot.download_file(file_id=update['document']['file_id'], + file_name=file_name, + path=f'{bot.path}/data') + patrons_list = davtelepot.utilities.csv_read(f'{bot.path}/data/{file_name}') + os.remove(f'{bot.path}/data/{file_name}') + with bot.db as db: # Unique SQL transaction + db.query("UPDATE patrons SET tier = NULL") + for patron in patrons_list: + db['patrons'].upsert( + dict(tier=1, + email=patron['email']), + ['email'] + ) + asyncio.ensure_future(kick_unlisted_patrons(bot=bot)) + return bot.get_message('patreon', 'list_updated', language=language) + + +def init(telegram_bot: davtelepot.bot.Bot): + telegram_bot.messages['patreon'] = patreon_messages + if 'patrons' not in telegram_bot.db.tables: + table = telegram_bot.db.create_table('patrons') + else: + table = telegram_bot.db.get_table('patrons') + if 'email' not in table.columns: + table.create_column('email', telegram_bot.db.types.string(320)) + if 'tier' not in table.columns: + table.create_column('tier', telegram_bot.db.types.integer) + if 'user_id' not in table.columns: + table.create_column('user_id', telegram_bot.db.types.integer) + if 'is_in_chat' not in table.columns: + table.create_column('is_in_chat', telegram_bot.db.types.boolean) + + @telegram_bot.document_handler(condition=is_patrons_list_file, + authorization_level='administrator') + async def _handle_patrons_list_file(bot: davtelepot.bot.Bot, + update: dict, + language: str): + return await handle_patrons_list_file(bot=bot, update=update, + language=language)