diff --git a/.gitignore b/.gitignore index ecf19da..36fa4f0 100644 --- a/.gitignore +++ b/.gitignore @@ -5,3 +5,4 @@ __pycache__/ /build/ /venv/ bot.db/ +teams_bot_data/ diff --git a/setup.cfg b/setup.cfg index 8f7d696..aa6c24d 100644 --- a/setup.cfg +++ b/setup.cfg @@ -21,6 +21,7 @@ packages = find: python_requires = >=3.8 install_requires = click + pickleDB qrcode deltachat diff --git a/src/teams_bot/bot.py b/src/teams_bot/bot.py index 099a7ae..9003cea 100644 --- a/src/teams_bot/bot.py +++ b/src/teams_bot/bot.py @@ -1,11 +1,12 @@ import logging from threading import Event +import pickledb import deltachat from deltachat import account_hookimpl from deltachat.capi import lib as dclib -from .commands import help_message, set_display_name, set_avatar, get_crew_id +from .commands import help_message, set_display_name, set_avatar class SetupPlugin: @@ -29,8 +30,10 @@ class SetupPlugin: class RelayPlugin: - def __init__(self, account: deltachat.Account): + def __init__(self, account: deltachat.Account, kvstore: pickledb.PickleDB): self.account = account + self.kvstore = kvstore + self.crew = account.get_chat_by_id(kvstore.get("crew_id")) @account_hookimpl def ac_incoming_message(self, message: deltachat.Message): @@ -47,7 +50,7 @@ class RelayPlugin: """:TODO handle chat name changes""" return - if message.chat.id == get_crew_id(self.account): + if message.chat.id == self.crew.id: if message.text.startswith("/"): logging.debug( "handling command by %s: %s", @@ -64,7 +67,7 @@ class RelayPlugin: quote=message, ) if arguments[0] == "/set_avatar": - result = set_avatar(self.account, message) + result = set_avatar(self.account, message, self.crew) self.reply(message.chat, result, quote=message) else: logging.debug("Ignoring message, just the crew chatting") @@ -119,9 +122,7 @@ class RelayPlugin: def forward_to_relay_group(self, message: deltachat.Message): """forward a request to a relay group; create one if it doesn't exist yet.""" outsider = message.get_sender_contact().addr - crew_members = self.account.get_chat_by_id( - get_crew_id(self.account) - ).get_contacts() + crew_members = self.crew.get_contacts() crew_members.remove(self.account.get_self_contact()) group_name = "[%s] %s" % ( self.account.get_config("addr").split("@")[0], @@ -157,9 +158,7 @@ class RelayPlugin: return False # all relay groups were started by the teamsbot if chat.is_protected(): return False # relay groups don't need to be protected, so they are not - for crew_member in self.account.get_chat_by_id( - get_crew_id(self.account) - ).get_contacts(): + for crew_member in self.crew.get_contacts(): if crew_member not in chat.get_contacts(): return False # all crew members have to be in any relay group return True diff --git a/src/teams_bot/cli.py b/src/teams_bot/cli.py index e2a139a..3e47981 100644 --- a/src/teams_bot/cli.py +++ b/src/teams_bot/cli.py @@ -1,11 +1,13 @@ import logging +import pathlib import sys +import pickledb import click import qrcode import deltachat -from .bot import SetupPlugin, RelayPlugin, get_crew_id +from .bot import SetupPlugin, RelayPlugin def set_log_level(verbose: int, db: str): @@ -37,22 +39,27 @@ def teams_bot(ctx): "--password", type=str, default=None, help="the password of the email account" ) @click.option( - "--db", type=str, default="bot.db/db.sqlite", help="path to the bot's database" + "--db_dir", type=str, default="teams_bot_data", help="path to the bot's database" ) @click.option( "-v", "--verbose", count=True, help="show low level delta chat ffi events" ) @click.pass_context -def init(ctx, email: str, password: str, db: str, verbose: int): +def init(ctx, email: str, password: str, db_dir: str, verbose: int): """Configure bot; create crew; add user to crew by scanning a QR code.""" - set_log_level(verbose, db) + db_dir = pathlib.Path(db_dir) + delta_db = db_dir.joinpath("delta.sqlite") + pickle_path = db_dir.joinpath("pickle.db") + kvstore = pickledb.load(pickle_path, True) - ac = deltachat.Account(db) + set_log_level(verbose, delta_db) + + ac = deltachat.Account(str(delta_db)) ac.run_account(addr=email, password=password, show_ffi=verbose) ac.set_config("mvbox_move", "1") ac.set_config("sentbox_watch", "0") - crew_id_old = get_crew_id(ac) + crew_id_old = kvstore.get("crew_id") chat = ac.create_group_chat( "Team: {}".format(ac.get_config("addr")), contacts=[], verified=True @@ -87,13 +94,20 @@ def init(ctx, email: str, password: str, db: str, verbose: int): if crew_id_old: setupplugin.message_sent.clear() + old_crew = ac.get_chat_by_id(crew_id_old) + old_crew.set_name(f"Old Team: {ac.get_config('addr')}") + new_crew = [contact.addr for contact in chat.get_contacts()] + new_crew_emails = " or ".join(new_crew) + quit_message = f"There is a new Group for the Team now; you can ask {new_crew_emails} to add you to it." + logging.debug( + "Sending quit message to old crew with ID %s: %s", + old_crew.id, + quit_message, + ) try: - new_crew_id = get_crew_id( - ac, setupplugin - ) # notify old crew about who created the new crew - assert ( - new_crew_id == chat.id - ), f"Bot found different 'new crew' than the one we just created; consider deleting {db}" + old_crew.send_text(quit_message) + setupplugin.outgoing_messages += 1 + old_crew.remove_contact(ac.get_self_contact()) setupplugin.message_sent.wait() except ValueError as e: logging.warning("Could not notify the old crew: %s", str(e)) @@ -101,22 +115,31 @@ def init(ctx, email: str, password: str, db: str, verbose: int): sys.stdout.flush() # flush stdout to actually show the messages above ac.shutdown() + kvstore.set("crew_id", chat.id) + logging.info("Successfully changed crew ID to the new group.") + @teams_bot.command() @click.option( - "--db", type=str, default="bot.db/db.sqlite", help="path to the bot's database" + "--db_dir", type=str, default="teams_bot_data", help="path to the bot's database" ) @click.option( "-v", "--verbose", count=True, help="show low level delta chat ffi events" ) @click.pass_context -def run(ctx, db: str, verbose: int): +def run(ctx, db_dir: str, verbose: int): """Run the bot, so it relays messages between the crew and the outside.""" - set_log_level(verbose, db) + db_dir = pathlib.Path(db_dir) + delta_db = db_dir.joinpath("delta.sqlite") + pickle_path = db_dir.joinpath("pickle.db") + kvstore = pickledb.load(pickle_path, True) - ac = deltachat.Account(db) + logging.debug("delta_db: %s", type(delta_db)) + set_log_level(verbose, delta_db) + + ac = deltachat.Account(str(delta_db)) display_name = ac.get_config("displayname") - ac.run_account(account_plugins=[RelayPlugin(ac)], show_ffi=verbose) + ac.run_account(account_plugins=[RelayPlugin(ac, kvstore)], show_ffi=verbose) ac.set_config("displayname", display_name) try: ac.wait_shutdown() diff --git a/src/teams_bot/commands.py b/src/teams_bot/commands.py index b6bb106..668d7ae 100644 --- a/src/teams_bot/commands.py +++ b/src/teams_bot/commands.py @@ -25,7 +25,9 @@ def set_display_name(account: deltachat.Account, display_name: str) -> str: return "Display name changed to " + display_name -def set_avatar(account: deltachat.Account, message: deltachat.Message) -> str: +def set_avatar( + account: deltachat.Account, message: deltachat.Message, crew: deltachat.Chat +) -> str: """Set the avatar of the bot. :return: a success/failure message @@ -34,54 +36,5 @@ def set_avatar(account: deltachat.Account, message: deltachat.Message) -> str: return "Please attach an image so the avatar can be changed." logging.debug("Found file with MIMEtype %s", message.filemime) account.set_avatar(message.filename) - crew = account.get_chat_by_id(get_crew_id(account)) crew.set_profile_image(message.filename) return "Avatar changed to this image." - - -def get_crew_id(ac: deltachat.Account, setupplugin=None) -> int: - """Get the group ID of the crew group if it exists; warn old crews if they might still believe they are the crew. - - :param ac: the account object of the bot. - :param setupplugin: only if this function is run during `teams-bot init`. - :return: the chat ID of the crew group, if there is none, return 0. - """ - crew_id = 0 - for chat in reversed(ac.get_chats()): - if ( - chat.is_protected() - and chat.num_contacts() > 1 - and chat.get_name() == f"Team: {ac.get_config('addr')}" - ): - logging.debug( - "Chat with ID %s and title %s could be a crew", chat.id, chat.get_name() - ) - if crew_id > 0: - old_crew = ac.get_chat_by_id(crew_id) - old_crew.set_name(f"Old Team: {ac.get_config('addr')}") - new_crew = [contact.addr for contact in chat.get_contacts()] - new_crew_emails = " or ".join(new_crew) - quit_message = f"There is a new Group for the Team now; you can ask {new_crew_emails} to add you to it." - logging.debug( - "Sending quit message to old crew with ID %s: %s", - old_crew.id, - quit_message, - ) - old_crew.send_text(quit_message) - if setupplugin: - setupplugin.outgoing_messages += 1 - old_crew.remove_contact(ac.get_self_contact()) - crew_id = chat.id - else: - logging.debug( - "Chat with ID %s and title %s is not a crew.", chat.id, chat.get_name() - ) - if crew_id: - crew_members = [ - contact.addr for contact in ac.get_chat_by_id(crew_id).get_contacts() - ] - crew_emails = " or ".join(crew_members) - logging.debug("The current crew has ID %s and members %s", crew_id, crew_emails) - else: - logging.debug("Currently there is no crew") - return crew_id diff --git a/tests/conftest.py b/tests/conftest.py index 3e7cee4..f9cc887 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -1,4 +1,6 @@ import os + +import pickledb import requests import deltachat @@ -79,14 +81,14 @@ def tmp_file_path(request, tmpdir): @pytest.fixture def relaycrew(crew): - crew.bot.relayplugin = RelayPlugin(crew.bot) + crew.bot.relayplugin = RelayPlugin(crew.bot, crew.kvstore) crew.bot.add_account_plugin(crew.bot.relayplugin) assert not crew.bot.relayplugin.is_relay_group(crew) yield crew @pytest.fixture -def crew(teams_bot, teams_user): +def crew(teams_bot, teams_user, tmpdir): from teams_bot.bot import SetupPlugin crew = teams_bot.create_group_chat( @@ -109,6 +111,8 @@ def crew(teams_bot, teams_user): print("User received message:", last_message) last_message = teams_user.wait_next_incoming_message().text + crew.kvstore = pickledb.load(tmpdir + "pickle.db", True) + crew.kvstore.set("crew_id", crew.id) yield crew diff --git a/tests/test_bot.py b/tests/test_bot.py index db3b021..3b428fc 100644 --- a/tests/test_bot.py +++ b/tests/test_bot.py @@ -1,8 +1,6 @@ import deltachat from deltachat.capi import lib as dclib -from teams_bot.commands import get_crew_id - def test_not_relay_groups(relaycrew, outsider): bot = relaycrew.bot @@ -71,10 +69,7 @@ def test_relay_group_forwarding(relaycrew, outsider): bot_relay_group.get_messages()[0].get_sender_contact() == bot.get_self_contact() ) assert not bot_relay_group.is_protected() - assert ( - bot.get_chat_by_id(get_crew_id(bot)).get_contacts() - == bot_relay_group.get_contacts() - ) + assert relaycrew.get_contacts() == bot_relay_group.get_contacts() assert bot.relayplugin.is_relay_group(bot_relay_group) # send direct reply, should be forwarded diff --git a/tests/test_commands.py b/tests/test_commands.py deleted file mode 100644 index d3f4aa8..0000000 --- a/tests/test_commands.py +++ /dev/null @@ -1,36 +0,0 @@ -from teams_bot.commands import get_crew_id - - -def test_get_crew_id(crew): - """Test if crew is properly found in delta chat database.""" - assert crew.id == get_crew_id(crew.bot) - - -def test_disable_old_crew(crew, outsider): - """Test if crew is properly disabled if someone else creates a new crew on the command line.""" - old_crew_id = get_crew_id(crew.bot) - - # outsider fires up the command line and creates a new crew - new_crew = crew.bot.create_group_chat( - f"Team: {crew.bot.get_config('addr')}", verified=True - ) - assert new_crew.id != old_crew_id - qr = new_crew.get_join_qr() - - # prepare setupplugin for waiting on second group join - crew.bot.setupplugin.member_added.clear() - crew.bot.setupplugin.crew_id = new_crew.id - - # outsider joins new crew - outsider.qr_join_chat(qr) - crew.bot.setupplugin.member_added.wait(timeout=30) - assert len(new_crew.get_contacts()) == 2 - assert new_crew.get_name() == f"Team: {crew.bot.get_config('addr')}" - assert new_crew.is_protected() - assert new_crew.id == get_crew_id(crew.bot, crew.bot.setupplugin) - - # old user receives disable warning - crew.user.wait_next_incoming_message() - quit_message = crew.user.wait_next_incoming_message() - assert "There is a new Group for the Team now" in quit_message.text - assert outsider.get_config("addr") in quit_message.text