mirror of
https://github.com/ArcaneChat/arcanechat-tui.git
synced 2026-07-03 14:05:26 +02:00
add support for importing/exporting keys and backups (including password-protected backups)
close #61 close #13
This commit is contained in:
@@ -10,6 +10,8 @@
|
||||
- `/mute` and `/unmute` commands to mute/unmute chats
|
||||
- `/topic` command to change chat name
|
||||
- `/clear` command to delete all messages in current chat
|
||||
- support for importing/exporting encryption keys
|
||||
- support for importing/exporting backups (including password-protected backups)
|
||||
|
||||
### Changed
|
||||
|
||||
|
||||
@@ -19,13 +19,13 @@ ncurses Delta Chat client developed in Python with the urwid library.
|
||||
- [X] Ability to send files
|
||||
- [X] Notifications
|
||||
- [X] Read receipts ✓✓
|
||||
- [X] Import/export keys and backups (including password-protected backups)
|
||||
- [X] Emoji support and auto-completion
|
||||
- [X] @Mentions support and auto-completion
|
||||
- [X] Display message replies
|
||||
- [X] Chat operations: delete, pin/unpin, mute/unmute, add/remove members, change chat name, clear chat messages
|
||||
- [ ] Ability to send stickers
|
||||
- [ ] Jump to quoted messages
|
||||
- [ ] Export/import keys and full backups
|
||||
- [ ] Record and send voice messages
|
||||
- [ ] Reply messages
|
||||
- [ ] Delete messages
|
||||
|
||||
@@ -2,13 +2,42 @@ from tempfile import NamedTemporaryFile
|
||||
from typing import List
|
||||
|
||||
from deltachat import Account as _Account
|
||||
from deltachat import Message
|
||||
from deltachat import Message, const
|
||||
from deltachat.capi import ffi, lib
|
||||
from deltachat.cutil import as_dc_charpointer
|
||||
from deltachat.tracker import ImexTracker
|
||||
|
||||
|
||||
class Account(_Account):
|
||||
"""Patched Delta Chat Account"""
|
||||
|
||||
def export_all(self, path: str, passphrase: str = None) -> str:
|
||||
export_files = self._export(path, const.DC_IMEX_EXPORT_BACKUP, passphrase)
|
||||
if len(export_files) != 1:
|
||||
raise RuntimeError("found more than one new file")
|
||||
return export_files[0]
|
||||
|
||||
def import_all(self, path: str, passphrase: str = None) -> None:
|
||||
assert not self.is_configured(), "cannot import into configured account"
|
||||
self._import(path, const.DC_IMEX_IMPORT_BACKUP, passphrase)
|
||||
|
||||
def _import(self, path: str, imex_cmd: int, passphrase: str = None) -> None:
|
||||
with self.temp_plugin(ImexTracker()) as imex_tracker:
|
||||
self.imex(path, imex_cmd, passphrase)
|
||||
imex_tracker.wait_finish()
|
||||
|
||||
def _export(self, path: str, imex_cmd: int, passphrase: str = None) -> list:
|
||||
with self.temp_plugin(ImexTracker()) as imex_tracker:
|
||||
self.imex(path, imex_cmd, passphrase)
|
||||
return imex_tracker.wait_finish()
|
||||
|
||||
def imex(self, path, imex_cmd, passphrase=None):
|
||||
if not passphrase:
|
||||
passphrase = ffi.NULL
|
||||
else:
|
||||
passphrase = as_dc_charpointer(passphrase)
|
||||
lib.dc_imex(self._dc_context, imex_cmd, as_dc_charpointer(path), passphrase)
|
||||
|
||||
def get_fresh_messages_cnt(self) -> int:
|
||||
"""Return the number of fresh messages"""
|
||||
return lib.dc_array_get_cnt(
|
||||
|
||||
@@ -5,7 +5,7 @@ from argparse import ArgumentParser, Namespace
|
||||
|
||||
import deltachat.const
|
||||
from deltachat.events import FFIEventLogger
|
||||
from deltachat.tracker import ConfigureTracker
|
||||
from deltachat.tracker import ConfigureTracker, ImexFailed
|
||||
|
||||
from . import __version__
|
||||
from .account import Account
|
||||
@@ -13,6 +13,7 @@ from .application import Application
|
||||
from .event import AccountPlugin
|
||||
from .oauth2 import get_authz_code, is_oauth2
|
||||
from .util import (
|
||||
abspath,
|
||||
capture_keyboard_interrupt,
|
||||
fail,
|
||||
get_configuration,
|
||||
@@ -96,7 +97,11 @@ def get_parser(cfg: dict) -> ArgumentParser:
|
||||
required=True,
|
||||
)
|
||||
send_parser.add_argument(
|
||||
"-a", metavar="file", dest="filename", help="attach file to the message"
|
||||
"-a",
|
||||
metavar="file",
|
||||
dest="filename",
|
||||
help="attach file to the message",
|
||||
type=abspath,
|
||||
)
|
||||
send_parser.add_argument("text", help="text message to send", nargs="?")
|
||||
send_parser.set_defaults(cmd=send_cmd)
|
||||
@@ -104,6 +109,40 @@ def get_parser(cfg: dict) -> ArgumentParser:
|
||||
list_parser = subparsers.add_parser("list", help="print chat list")
|
||||
list_parser.set_defaults(cmd=list_cmd)
|
||||
|
||||
import_parser = subparsers.add_parser("import", help="import keys or full backup")
|
||||
import_parser.add_argument(
|
||||
"--password",
|
||||
"-p",
|
||||
help="if provided, this passphrase will be used to access the encrypted backup",
|
||||
)
|
||||
import_parser.add_argument(
|
||||
"path",
|
||||
help="path to a backup file or path to a directory containing keys to import",
|
||||
type=abspath,
|
||||
)
|
||||
import_parser.set_defaults(cmd=import_cmd)
|
||||
|
||||
export_parser = subparsers.add_parser("export", help="export full backup or keys")
|
||||
export_parser.add_argument(
|
||||
"--keys-only",
|
||||
"-k",
|
||||
action="store_true",
|
||||
help="export only the public and private keys",
|
||||
)
|
||||
export_parser.add_argument(
|
||||
"--password",
|
||||
"-p",
|
||||
help="if provided, this passphrase will be used to encrypt the exported backup",
|
||||
)
|
||||
export_parser.add_argument(
|
||||
"folder",
|
||||
help="path to the directory where the files should be saved, if not given, current working directory is used",
|
||||
nargs="?",
|
||||
default=os.curdir,
|
||||
type=abspath,
|
||||
)
|
||||
export_parser.set_defaults(cmd=export_cmd)
|
||||
|
||||
return parser
|
||||
|
||||
|
||||
@@ -182,6 +221,42 @@ def list_cmd(args: Namespace) -> None:
|
||||
print(f"#{chat.id} - {chat.get_name()}")
|
||||
|
||||
|
||||
def import_cmd(args: Namespace) -> None:
|
||||
if os.path.isdir(args.path):
|
||||
if args.password:
|
||||
fail("Error: only full backups support password protection")
|
||||
try:
|
||||
args.acct.import_self_keys(args.path)
|
||||
print("Keys imported successfully")
|
||||
except ImexFailed:
|
||||
fail(f"Error: no valid keys found in {args.path!r}")
|
||||
elif os.path.isfile(args.path):
|
||||
if args.acct.is_configured():
|
||||
fail("Error: can't import backup into an already configured account")
|
||||
try:
|
||||
args.acct.import_all(args.path, args.password)
|
||||
print("Backup imported successfully")
|
||||
except ImexFailed:
|
||||
fail(f"Error: invalid password or backup file {args.path!r}")
|
||||
else:
|
||||
fail(f"Error: file doesn't exists {args.path!r}")
|
||||
|
||||
|
||||
def export_cmd(args: Namespace) -> None:
|
||||
try:
|
||||
if args.keys_only:
|
||||
if args.password:
|
||||
fail("Error: only full backups support password protection")
|
||||
paths = args.acct.export_self_keys(args.folder)
|
||||
else:
|
||||
paths = [args.acct.export_all(args.folder, args.password)]
|
||||
print("Exported files:")
|
||||
for path in paths:
|
||||
print(path)
|
||||
except ImexFailed:
|
||||
fail(f"Error: failed to export to {args.folder!r}")
|
||||
|
||||
|
||||
def start_ui(args: Namespace) -> None:
|
||||
plugin = AccountPlugin(args.acct)
|
||||
args.acct.add_account_plugin(plugin)
|
||||
|
||||
@@ -259,3 +259,7 @@ def get_subtitle(chat: Chat) -> str:
|
||||
if count == 1:
|
||||
return "1 member"
|
||||
return f"{count} members"
|
||||
|
||||
|
||||
def abspath(path: str) -> str:
|
||||
return os.path.abspath(os.path.expanduser(path))
|
||||
|
||||
Reference in New Issue
Block a user