Compare commits

..

3 commits

8 changed files with 54 additions and 174 deletions

View file

@ -1,23 +0,0 @@
name: CI
on:
pull_request:
jobs:
test:
runs-on: ubuntu-latest
strategy:
matrix:
python-version: ['3.8', '3.11']
steps:
- uses: actions/checkout@v4
- name: Set up Python ${{ matrix.python-version }}
uses: actions/setup-python@v5
with:
python-version: ${{ matrix.python-version }}
- name: Install dependencies
run: |
python -m pip install --upgrade pip
python -m pip install '.[dev]'
- run: tox

View file

@ -103,6 +103,7 @@ run:
``` ```
python3 -m venv venv python3 -m venv venv
. venv/bin/activate . venv/bin/activate
pip install -e .[dev] pip install pytest tox black pytest-xdist pytest-timeout
pip install -e .
tox tox
``` ```

View file

@ -1,6 +1,6 @@
[metadata] [metadata]
name = team_bot name = team_bot
version = 1.1.0 version = 1.0.0
author = missytake author = missytake
author_email = missytake@systemli.org author_email = missytake@systemli.org
description = This bot connects your team to the outside and makes it addressable. description = This bot connects your team to the outside and makes it addressable.
@ -24,15 +24,7 @@ install_requires =
pyinfra pyinfra
pickleDB pickleDB
qrcode qrcode
deltachat>=1.142.7 deltachat>=1.136.2
[options.extras_require]
dev =
pytest
tox
black
pytest-xdist
pytest-timeout
[options.packages.find] [options.packages.find]
where = src where = src

View file

@ -11,7 +11,6 @@ from .commands import (
crew_help, crew_help,
set_display_name, set_display_name,
set_avatar, set_avatar,
generate_invite,
start_chat, start_chat,
outside_help, outside_help,
set_outside_help, set_outside_help,
@ -66,14 +65,15 @@ class RelayPlugin:
relay_group = self.get_relay_group(message.chat.id) relay_group = self.get_relay_group(message.chat.id)
relay_group.send_text(f"Sending Message failed:\n\n{error}") relay_group.send_text(f"Sending Message failed:\n\n{error}")
@account_hookimpl
def ac_member_removed(self, chat, contact, actor, message):
if chat == self.crew:
self.offboard(contact)
@account_hookimpl @account_hookimpl
def ac_incoming_message(self, message: deltachat.Message): def ac_incoming_message(self, message: deltachat.Message):
"""This method is called on every incoming message and decides what to do with it.""" """This method is called on every incoming message and decides what to do with it."""
logging.info(
"New message from %s in chat %s: %s",
message.get_sender_contact().addr,
message.chat.get_name(),
message.text,
)
if message.is_system_message(): if message.is_system_message():
if message.chat.id == self.crew.id: if message.chat.id == self.crew.id:
@ -114,9 +114,6 @@ class RelayPlugin:
if arguments[0] == "/set_avatar": if arguments[0] == "/set_avatar":
result = set_avatar(self.account, message, self.crew) result = set_avatar(self.account, message, self.crew)
self.reply(message.chat, result, quote=message) self.reply(message.chat, result, quote=message)
if arguments[0] == "/generate-invite":
text = generate_invite(self.account)
self.reply(message.chat, text, quote=message)
if arguments[0] == "/start_chat": if arguments[0] == "/start_chat":
outside_chat, result = start_chat( outside_chat, result = start_chat(
self.account, self.account,
@ -156,7 +153,6 @@ class RelayPlugin:
else: else:
logging.debug("Ignoring message, just the crew chatting") logging.debug("Ignoring message, just the crew chatting")
else: else:
self.mark_last_messages_read(message.chat)
logging.debug("Ignoring message, just the crew chatting") logging.debug("Ignoring message, just the crew chatting")
else: else:
@ -240,10 +236,21 @@ class RelayPlugin:
def is_relay_group(self, chat: deltachat.Chat) -> bool: def is_relay_group(self, chat: deltachat.Chat) -> bool:
"""Check whether a chat is a relay group.""" """Check whether a chat is a relay group."""
for mapping in self.kvstore.get("relays"): if not chat.get_name().startswith(
if mapping[1] == chat.id: "[%s] " % (self.account.get_config("addr").split("@")[0],)
return True ):
return False return False # all relay groups' names begin with a [tag] with the localpart of the team-bot's address
if (
chat.get_messages()[0].get_sender_contact()
!= self.account.get_self_contact()
):
return False # all relay groups were started by the team-bot
if chat.is_protected():
return False # relay groups don't need to be protected, so they are not
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
def get_outside_chat(self, relay_group_id: int) -> deltachat.Chat: def get_outside_chat(self, relay_group_id: int) -> deltachat.Chat:
"""Get the corresponding outside chat for the ID of a relay group. """Get the corresponding outside chat for the ID of a relay group.
@ -268,22 +275,3 @@ class RelayPlugin:
if mapping[0] == outside_id: if mapping[0] == outside_id:
return self.account.get_chat_by_id(mapping[1]) return self.account.get_chat_by_id(mapping[1])
return None return None
def offboard(self, ex_admin: deltachat.Contact) -> None:
"""Remove a former crew member from all relay groups they are part of.
:param ex_admin: a contact which just got removed from the crew.
"""
for mapping in self.kvstore.get("relays"):
relay_group = self.account.get_chat_by_id(mapping[1])
if ex_admin in relay_group.get_contacts():
relay_group.remove_contact(ex_admin)
def mark_last_messages_read(self, relay_group: deltachat.Chat) -> None:
"""Mark the last incoming messages as read for a corresponding relay group.
:param relay_group: the relay group in which the messages which should marked read were forwarded.
"""
outside_chat = self.get_outside_chat(relay_group.id)
for msg in outside_chat.get_messages():
msg.mark_seen()

View file

@ -15,7 +15,6 @@ def crew_help() -> str:
Start a chat:\t/start_chat alice@example.org,bob@example.org Chat_Title Hello friends! Start a chat:\t/start_chat alice@example.org,bob@example.org Chat_Title Hello friends!
Change the bot's name:\t/set_name Name Change the bot's name:\t/set_name Name
Change the bot's avatar:\t/set_avatar <attach image> Change the bot's avatar:\t/set_avatar <attach image>
Generate invite link:\t\t/generate-invite
Show this help text:\t\t/help Show this help text:\t\t/help
Change the help message for outsiders:\t/set_outside_help Hello outsider Change the help message for outsiders:\t/set_outside_help Hello outsider
""" """
@ -64,14 +63,6 @@ def set_avatar(
return "Avatar changed to this image." return "Avatar changed to this image."
def generate_invite(account: deltachat.Account) -> str:
"""Return a https://i.delta.chat invite link for chatting with the bot.
:return: the invite link, e.g.: https://i.delta.chat
"""
return account.get_setup_contact_qr()
def start_chat( def start_chat(
ac: deltachat.Account, ac: deltachat.Account,
command: deltachat.Message, command: deltachat.Message,

View file

@ -6,7 +6,9 @@ from pyinfra import host
from pyinfra.facts.systemd import SystemdStatus from pyinfra.facts.systemd import SystemdStatus
def deploy_team_bot(unix_user: str, bot_email: str, bot_passwd: str, dbdir: str = None): def deploy_team_bot(
unix_user: str, bot_email: str, bot_passwd: str, dbdir: str = None
):
"""Deploy TeamsBot to a UNIX user, with specified credentials """Deploy TeamsBot to a UNIX user, with specified credentials
:param unix_user: the existing UNIX user of the bot :param unix_user: the existing UNIX user of the bot

View file

@ -105,7 +105,6 @@ def crew(team_bot, team_user, tmpdir) -> deltachat.Chat:
# wait until old user is properly added to crew # wait until old user is properly added to crew
team_user._evtracker.wait_securejoin_joiner_progress(1000) team_user._evtracker.wait_securejoin_joiner_progress(1000)
team_user._evtracker.wait_next_incoming_message() # member added message
crew.kvstore = pickledb.load(tmpdir + "pickle.db", True) crew.kvstore = pickledb.load(tmpdir + "pickle.db", True)
crew.kvstore.set("crew_id", crew.id) crew.kvstore.set("crew_id", crew.id)

View file

@ -6,10 +6,10 @@ import pytest
from deltachat.capi import lib as dclib from deltachat.capi import lib as dclib
TIMEOUT = 40 TIMEOUT = 20
def get_user_crew(crewuser: deltachat.Account, id=11) -> deltachat.Chat: def get_user_crew(crewuser: deltachat.Account) -> deltachat.Chat:
"""Get the Team chat from the team member's point of view. """Get the Team chat from the team member's point of view.
:param crewuser: the account object of the team member :param crewuser: the account object of the team member
@ -17,79 +17,52 @@ def get_user_crew(crewuser: deltachat.Account, id=11) -> deltachat.Chat:
""" """
for chat in crewuser.get_chats(): for chat in crewuser.get_chats():
print(chat.id, chat.get_name()) print(chat.id, chat.get_name())
user_crew = crewuser.get_chat_by_id(id) user_crew = crewuser.get_chat_by_id(11)
assert user_crew.get_name().startswith("Team") assert user_crew.get_name().startswith("Team")
return user_crew return user_crew
@pytest.mark.timeout(TIMEOUT) @pytest.mark.timeout(TIMEOUT * 3)
def test_not_relay_groups(relaycrew, outsider, lp): def test_not_relay_groups(relaycrew, outsider, lp):
bot = relaycrew.bot bot = relaycrew.bot
user = relaycrew.user user = relaycrew.user
def find_msg(ac, text): lp.sec("bot <-> outsider 1:1 chat")
for chat in ac.get_chats():
for msg in chat.get_messages():
if msg.text == text:
return msg
text = "outsider -> bot 1:1 chat"
lp.sec(text)
outsider_botcontact = outsider.create_contact(bot.get_config("addr")) outsider_botcontact = outsider.create_contact(bot.get_config("addr"))
outsider_outside_chat = outsider.create_chat(outsider_botcontact) outsider_outside_chat = outsider.create_chat(outsider_botcontact)
outsider_outside_chat.send_text(text) outsider_outside_chat.send_text("test 1:1 message to bot")
lp.sec("receiving message from outsider in 1:1 chat")
bot_message_from_outsider = bot._evtracker.wait_next_incoming_message() bot_message_from_outsider = bot._evtracker.wait_next_incoming_message()
bot_outside_chat = bot_message_from_outsider.chat bot_outside_chat = bot_message_from_outsider.chat
assert bot_message_from_outsider.text == text
assert not bot.relayplugin.is_relay_group(bot_outside_chat) assert not bot.relayplugin.is_relay_group(bot_outside_chat)
lp.sec("leave relay group with user") lp.sec("bot <-> outsider group chat")
relayed_msg = find_msg(user, text)
if not relayed_msg:
relayed_msg = user._evtracker.wait_next_incoming_message()
relayed_msg.chat.remove_contact(user.get_config("addr"))
leave_msg = bot._evtracker.wait_next_incoming_message()
assert bot.relayplugin.is_relay_group(leave_msg.chat)
text = "outsider -> bot group chat"
lp.sec(text)
outsider_bot_group = outsider.create_group_chat( outsider_bot_group = outsider.create_group_chat(
"test with outsider", contacts=[outsider_botcontact] "test with outsider", contacts=[outsider_botcontact]
) )
outsider_bot_group.send_text(text) outsider_bot_group.send_text("test message to outsider group")
lp.sec("receiving message from outsider in group chat")
bot_message_from_outsider = bot._evtracker.wait_next_incoming_message() bot_message_from_outsider = bot._evtracker.wait_next_incoming_message()
assert bot_message_from_outsider.text == text
assert not bot.relayplugin.is_relay_group(bot_message_from_outsider.chat) assert not bot.relayplugin.is_relay_group(bot_message_from_outsider.chat)
text = "user -> bot 1:1 chat" lp.sec("bot <-> user 1:1 chat")
lp.sec(text)
user_botcontact = user.create_contact(bot.get_config("addr")) user_botcontact = user.create_contact(bot.get_config("addr"))
user_to_bot = user.create_chat(user_botcontact) user_to_bot = user.create_chat(user_botcontact)
user_to_bot.send_text(text) user_to_bot.send_text("test message to bot")
lp.sec("receiving message from user in 1:1 chat")
# somehow the message doesn't trigger DC_EVENT_INCOMING_MSG # somehow the message doesn't trigger DC_EVENT_INCOMING_MSG
# bot._evtracker.wait_next_incoming_message() bot_message_from_user = bot.get_chats()[-3].get_messages()[-1] # bot._evtracker.wait_next_incoming_message()
bot_message_from_user = find_msg(bot, text) while bot_message_from_user.text != "test message to bot":
while not bot_message_from_user: bot_message_from_user = bot.get_chats()[-3].get_messages()[-1] # bot._evtracker.wait_next_incoming_message()
bot_message_from_user = find_msg(bot, text)
time.sleep(1) time.sleep(1)
assert bot_message_from_user.text == text
assert not bot.relayplugin.is_relay_group(bot_message_from_user.chat) assert not bot.relayplugin.is_relay_group(bot_message_from_user.chat)
text = "user -> bot group chat" lp.sec("bot <-> user group chat")
lp.sec(text)
user_group = user.create_group_chat("test with user", contacts=[user_botcontact]) user_group = user.create_group_chat("test with user", contacts=[user_botcontact])
user_group.send_text(text) user_group.send_text("testing message to user group")
lp.sec("receiving message from user in group chat")
bot_message_from_user = bot._evtracker.wait_next_incoming_message() bot_message_from_user = bot._evtracker.wait_next_incoming_message()
assert bot_message_from_user.text == text
assert not bot.relayplugin.is_relay_group(bot_message_from_user.chat) assert not bot.relayplugin.is_relay_group(bot_message_from_user.chat)
@pytest.mark.timeout(TIMEOUT) @pytest.mark.timeout(60)
def test_relay_group_forwarding(relaycrew, outsider): def test_relay_group_forwarding(relaycrew, outsider):
bot = relaycrew.bot bot = relaycrew.bot
user = relaycrew.user user = relaycrew.user
@ -103,9 +76,9 @@ def test_relay_group_forwarding(relaycrew, outsider):
message_from_outsider = bot._evtracker.wait_next_incoming_message() message_from_outsider = bot._evtracker.wait_next_incoming_message()
bot_outside_chat = message_from_outsider.chat bot_outside_chat = message_from_outsider.chat
assert not bot.relayplugin.is_relay_group(bot_outside_chat) assert not bot.relayplugin.is_relay_group(bot_outside_chat)
assert message_from_outsider.is_in_fresh()
# get relay group # get relay group
user._evtracker.wait_next_incoming_message() # group added message
user_forwarded_message_from_outsider = user._evtracker.wait_next_incoming_message() user_forwarded_message_from_outsider = user._evtracker.wait_next_incoming_message()
user_relay_group = user_forwarded_message_from_outsider.create_chat() user_relay_group = user_forwarded_message_from_outsider.create_chat()
user_relay_group.send_text( user_relay_group.send_text(
@ -133,7 +106,6 @@ def test_relay_group_forwarding(relaycrew, outsider):
user._dc_context, user_relay_group.id, user_direct_reply._dc_msg user._dc_context, user_relay_group.id, user_direct_reply._dc_msg
) )
assert sent_id == user_direct_reply.id assert sent_id == user_direct_reply.id
assert message_from_outsider.is_in_seen()
# check that direct reply was forwarded to outsider # check that direct reply was forwarded to outsider
outsider_direct_reply = outsider._evtracker.wait_next_incoming_message() outsider_direct_reply = outsider._evtracker.wait_next_incoming_message()
@ -159,44 +131,6 @@ def test_relay_group_forwarding(relaycrew, outsider):
assert "This is the relay group for" not in msg.text assert "This is the relay group for" not in msg.text
@pytest.mark.timeout(TIMEOUT)
def test_offboarding(team_bot, relaycrew, outsider, team_user):
# outsider sends message, creates relay group
outsider_botcontact = outsider.create_contact(team_bot.get_config("addr"))
outsider_outside_chat = outsider.create_chat(outsider_botcontact)
outsider_outside_chat.send_text("test 1:1 message to bot")
# get relay group
user_relay_group = team_user._evtracker.wait_next_incoming_message().chat
bot_relay_group = team_bot.get_chats()[-1]
# outsider gets added to crew
qr = relaycrew.get_join_qr()
outsider.qr_join_chat(qr)
outsider._evtracker.wait_securejoin_joiner_progress(1000)
# user kicks outsider from crew
user_crew = get_user_crew(team_user)
user_crew.remove_contact(team_user.create_contact(outsider))
team_bot._evtracker.wait_next_incoming_message()
# user leaves crew
user_crew.remove_contact(team_user)
# make sure they are also offboarded from relay group
team_bot._evtracker.wait_next_incoming_message()
team_user._evtracker.wait_next_incoming_message()
team_user._evtracker.wait_next_incoming_message()
team_user._evtracker.wait_next_incoming_message()
for contact in bot_relay_group.get_contacts():
assert team_user.get_config("addr") != contact.addr
# make sure there is no message in relay group that outsider was kicked
for msg in user_relay_group.get_messages():
print(msg.text)
assert outsider.get_config("addr") + " removed by " not in msg.text
@pytest.mark.timeout(TIMEOUT)
def test_default_outside_help(relaycrew, outsider): def test_default_outside_help(relaycrew, outsider):
bot = relaycrew.bot bot = relaycrew.bot
user = relaycrew.user user = relaycrew.user
@ -215,7 +149,6 @@ def test_default_outside_help(relaycrew, outsider):
assert len(user.get_chats()) == 1 assert len(user.get_chats()) == 1
@pytest.mark.timeout(TIMEOUT)
def test_empty_outside_help(relaycrew, outsider): def test_empty_outside_help(relaycrew, outsider):
bot = relaycrew.bot bot = relaycrew.bot
user = relaycrew.user user = relaycrew.user
@ -235,13 +168,13 @@ def test_empty_outside_help(relaycrew, outsider):
outsider_outside_chat.send_text("/help") outsider_outside_chat.send_text("/help")
# get forwarded /help message # get forwarded /help message
user._evtracker.wait_next_incoming_message() # group added message
user._evtracker.wait_next_incoming_message() # "Removed help message for outsiders" user._evtracker.wait_next_incoming_message() # "Removed help message for outsiders"
user._evtracker.wait_next_incoming_message() # explanation message user._evtracker.wait_next_incoming_message() # explanation message
user_forwarded_message_from_outsider = user._evtracker.wait_next_incoming_message() user_forwarded_message_from_outsider = user._evtracker.wait_next_incoming_message()
assert user_forwarded_message_from_outsider.text == "/help" assert user_forwarded_message_from_outsider.text == "/help"
@pytest.mark.timeout(TIMEOUT)
def test_changed_outside_help(relaycrew, outsider): def test_changed_outside_help(relaycrew, outsider):
bot = relaycrew.bot bot = relaycrew.bot
user = relaycrew.user user = relaycrew.user
@ -270,7 +203,6 @@ def test_changed_outside_help(relaycrew, outsider):
assert len(user.get_chats()) == 1 assert len(user.get_chats()) == 1
@pytest.mark.timeout(TIMEOUT)
def test_change_avatar(relaycrew): def test_change_avatar(relaycrew):
bot = relaycrew.bot bot = relaycrew.bot
user = relaycrew.user user = relaycrew.user
@ -295,6 +227,7 @@ def test_change_avatar(relaycrew):
sent_id = dclib.dc_send_msg(user._dc_context, user_crew.id, msg._dc_msg) sent_id = dclib.dc_send_msg(user._dc_context, user_crew.id, msg._dc_msg)
assert sent_id == msg.id assert sent_id == msg.id
user._evtracker.wait_next_incoming_message() # chat successfully created
group_avatar_changed_msg = user._evtracker.wait_next_incoming_message() group_avatar_changed_msg = user._evtracker.wait_next_incoming_message()
assert "Group image changed" in group_avatar_changed_msg.text assert "Group image changed" in group_avatar_changed_msg.text
assert user_crew.get_profile_image() assert user_crew.get_profile_image()
@ -304,7 +237,6 @@ def test_change_avatar(relaycrew):
assert botcontact.get_profile_image() assert botcontact.get_profile_image()
@pytest.mark.timeout(TIMEOUT * 2)
def test_forward_sending_errors_to_relay_group(relaycrew): def test_forward_sending_errors_to_relay_group(relaycrew):
usercrew = relaycrew.user.get_chats()[-1] usercrew = relaycrew.user.get_chats()[-1]
usercrew.send_text("/start_chat alice@example.org This_Message_will_fail test") usercrew.send_text("/start_chat alice@example.org This_Message_will_fail test")
@ -338,19 +270,17 @@ def test_forward_sending_errors_to_relay_group(relaycrew):
) )
@pytest.mark.timeout(TIMEOUT) @pytest.mark.timeout(TIMEOUT * 2)
def test_public_invite(relaycrew, outsider): def test_public_invite(relaycrew, outsider):
crew = get_user_crew(relaycrew.user) crew = get_user_crew(relaycrew.user)
crew.send_text("/generate-invite") crew.send_text("/generate-invite")
result = relaycrew.user._evtracker.wait_next_incoming_message() result = relaycrew.user._evtracker.wait_next_incoming_message()
# assert result.filename assert result.filename
# assert result.text.startswith("https://i.delta.chat") assert result.text.startswith("https://i.delta.chat")
# qr = result.filename qr = result.filename
# invite = "OPENPGP4FPR:" + result.text[22::] chat = outsider.qr_setup_contact(qr)
chat = outsider.qr_setup_contact(result.text)
outsider._evtracker.wait_securejoin_joiner_progress(1000)
while not chat.is_protected(): while not chat.is_protected():
print(chat.get_messages()[-1].text) print(chat.get_messages()[:-1].text)
time.sleep(1) time.sleep(1)