diff --git a/chatmaild/pyproject.toml b/chatmaild/pyproject.toml index 90c1fe9a..cdaaabd6 100644 --- a/chatmaild/pyproject.toml +++ b/chatmaild/pyproject.toml @@ -10,6 +10,10 @@ dependencies = [ "iniconfig", "deltachat-rpc-server", "deltachat-rpc-client", + "ConfigArgParse", + "deltachat", + "setuptools>=60", + "setuptools-scm>=8", ] [tool.setuptools] @@ -22,6 +26,7 @@ where = ['src'] doveauth = "chatmaild.doveauth:main" filtermail = "chatmaild.filtermail:main" echobot = "chatmaild.echo:main" +greeterbot = "chatmaild.greeterbot:main" chatmail-metrics = "chatmaild.metrics:main" [project.entry-points.pytest11] diff --git a/chatmaild/src/chatmaild/avatar.jpg b/chatmaild/src/chatmaild/avatar.jpg new file mode 100644 index 00000000..00f1abf5 Binary files /dev/null and b/chatmaild/src/chatmaild/avatar.jpg differ diff --git a/chatmaild/src/chatmaild/database.py b/chatmaild/src/chatmaild/database.py index a7197e14..cb7d100a 100644 --- a/chatmaild/src/chatmaild/database.py +++ b/chatmaild/src/chatmaild/database.py @@ -46,11 +46,17 @@ class Connection: ) return result + def get_user_list(self) -> set[str]: + """Get a set of all users.""" + q = "SELECT addr from users" + return set([tup[0] for tup in self._sqlconn.execute(q).fetchall()]) + class Database: - def __init__(self, path: str): + def __init__(self, path: str, read_only=False): self.path = Path(path) - self.ensure_tables() + if not read_only: + self.ensure_tables() def _get_connection( self, write=False, transaction=False, closing=False diff --git a/chatmaild/src/chatmaild/doveauth.py b/chatmaild/src/chatmaild/doveauth.py index b95f2240..c937cbeb 100644 --- a/chatmaild/src/chatmaild/doveauth.py +++ b/chatmaild/src/chatmaild/doveauth.py @@ -46,7 +46,7 @@ def is_allowed_to_create(config: Config, user, cleartext_password) -> bool: len(localpart) > config.username_max_length or len(localpart) < config.username_min_length ): - if localpart != "echo": + if localpart != "echo" and localpart != "hello": logging.warning( "localpart %s has to be between %s and %s chars long", localpart, diff --git a/chatmaild/src/chatmaild/editor.xdc b/chatmaild/src/chatmaild/editor.xdc new file mode 100644 index 00000000..3e935b7a Binary files /dev/null and b/chatmaild/src/chatmaild/editor.xdc differ diff --git a/chatmaild/src/chatmaild/greeterbot.py b/chatmaild/src/chatmaild/greeterbot.py new file mode 100644 index 00000000..6dac6afe --- /dev/null +++ b/chatmaild/src/chatmaild/greeterbot.py @@ -0,0 +1,124 @@ +import time + +import deltachat +from deltachat.tracker import ConfigureFailed +from time import sleep +import tempfile +import os +import configargparse +import pkg_resources +import secrets + +from chatmaild.database import Database +from chatmaild.config import read_config +from chatmaild.newemail import ALPHANUMERIC_PUNCT, CONFIG_PATH + +PASSDB_PATH = "/home/vmail/passdb.sqlite" + + +def setup_account(data_dir: str, debug: bool) -> deltachat.Account: + """Create a deltachat account with a given addr/password combination. + + :param data_dir: the directory where the data(base) is stored. + :param debug: whether to show log messages for the account. + :return: the deltachat account object. + """ + chatmail_config = read_config(CONFIG_PATH) + addr = "hello@" + chatmail_config.mail_domain + + try: + os.mkdir(os.path.join(data_dir, addr)) + except FileExistsError: + pass + db_path = os.path.join(data_dir, addr, "db.sqlite") + + ac = deltachat.Account(db_path) + if debug: + ac.add_account_plugin(deltachat.events.FFIEventLogger(ac)) + + ac.set_config("mvbox_move", "0") + ac.set_config("sentbox_watch", "0") + ac.set_config("bot", "1") + ac.set_config("mdns_enabled", "0") + + if not ac.is_configured(): + cleartext_password = "".join( + secrets.choice(ALPHANUMERIC_PUNCT) + for _ in range(chatmail_config.password_min_length + 3) + ) + ac.set_config("mail_pw", cleartext_password) + ac.set_config("addr", addr) + + configtracker = ac.configure() + try: + configtracker.wait_finish() + except ConfigureFailed as e: + print( + "configuration setup failed for %s with password:\n%s" + % (ac.get_config("addr"), ac.get_config("mail_pw")) + ) + raise + + ac.start_io() + avatar = pkg_resources.resource_filename(__name__, "avatar.jpg") + ac.set_avatar(avatar) + ac.set_config("displayname", "Hello at try.webxdc.org!") + return ac + + +class GreetBot: + def __init__(self, passdb, account): + self.db = Database(passdb, read_only=True) + self.account = account + self.domain = account.get_config("addr").split("@")[1] + with self.db.read_connection() as conn: + self.existing_users = conn.get_user_list() + + def greet_users(self): + with self.db.read_connection() as conn: + users = conn.get_user_list() + new_users = users.difference(self.existing_users) + self.existing_users = users + time.sleep(20) # wait until Delta is configured on the user side + for user in new_users: + for ci_prefix in ["ac1_", "ac2_", "ac3_", "ac4_", "ac5_", "ci-"]: + if user.startswith(ci_prefix): + continue + if user not in [c.addr for c in self.account.get_contacts()]: + print("Inviting", user) + contact = self.account.create_contact(user) + chat = contact.create_chat() + chat.send_text("Welcome to %s! Here you can try out Delta Chat." % (self.domain,)) + chat.send_text("I prepared some webxdc apps for you, if you are interested:") + chat.send_file(pkg_resources.resource_filename(__name__, "editor.xdc")) + chat.send_file(pkg_resources.resource_filename(__name__, "tower-builder.xdc")) + chat.send_text( + "You can send a message to xstore@testrun.org to discover more apps! " + "Some of these games you can also play with friends, directly in the chat." + ) + + +def main(): + args = configargparse.ArgumentParser() + args.add_argument("--db_path", help="location of the Delta Chat database") + args.add_argument("--passdb", default=PASSDB_PATH, help="location of the chatmail passdb") + args.add_argument("--show-ffi", action="store_true", help="print Delta Chat log") + ops = args.parse_args() + + # ensuring account data directory + if ops.db_path is None: + tempdir = tempfile.TemporaryDirectory(prefix="hellobot") + ops.db_path = tempdir.name + elif not os.path.exists(ops.db_path): + os.mkdir(ops.db_path) + + ac = setup_account(ops.db_path, ops.show_ffi) + greeter = GreetBot(ops.passdb, ac) + print("waiting for new chatmail users...") + while 1: + greeter.greet_users() + sleep(5) + + +if __name__ == "__main__": + main() diff --git a/chatmaild/src/chatmaild/greeterbot.service.f b/chatmaild/src/chatmaild/greeterbot.service.f new file mode 100644 index 00000000..a97268f2 --- /dev/null +++ b/chatmaild/src/chatmaild/greeterbot.service.f @@ -0,0 +1,11 @@ +[Unit] +Description=Chatmail greeterbot, a Delta Chat bot to greet new users + +[Service] +ExecStart={execpath} --passdb {passdb_path} --db_path /home/vmail/greeterbot/ --show-ffi +User=vmail +Restart=always +RestartSec=30 + +[Install] +WantedBy=multi-user.target diff --git a/chatmaild/src/chatmaild/tower-builder.xdc b/chatmaild/src/chatmaild/tower-builder.xdc new file mode 100644 index 00000000..02a1130c Binary files /dev/null and b/chatmaild/src/chatmaild/tower-builder.xdc differ diff --git a/cmdeploy/src/cmdeploy/__init__.py b/cmdeploy/src/cmdeploy/__init__.py index c8f61da0..12a1f821 100644 --- a/cmdeploy/src/cmdeploy/__init__.py +++ b/cmdeploy/src/cmdeploy/__init__.py @@ -101,11 +101,13 @@ def _install_remote_venv_with_chatmaild(config) -> None: "doveauth", "filtermail", "echobot", + "greeterbot", ): params = dict( execpath=f"{remote_venv_dir}/bin/{fn}", config_path=remote_chatmail_inipath, remote_venv_dir=remote_venv_dir, + passdb_path="/home/vmail/passdb.sqlite", ) source_path = importlib.resources.files("chatmaild").joinpath(f"{fn}.service.f") content = source_path.read_text().format(**params).encode()