diff --git a/snikket_web/invite.py b/snikket_web/invite.py
index 742509e..1ccda08 100644
--- a/snikket_web/invite.py
+++ b/snikket_web/invite.py
@@ -10,13 +10,14 @@ from quart import (
current_app,
render_template,
redirect,
+ request,
url_for,
session as http_session,
)
import wtforms
-from flask_babel import lazy_gettext as _l
+from flask_babel import lazy_gettext as _l, gettext
from .infra import client, selected_locale, BaseForm
@@ -26,6 +27,11 @@ bp = Blueprint("invite", __name__)
INVITE_SESSION_JID = "invite-session-jid"
+MAX_IMPORT_DATA_SIZE = 5*1024*1024 # 5MB
+SUPPORTED_IMPORT_TYPES = ["application/xml", "text/xml"]
+
+EIMPORTTOOBIG = _l("The account data you tried to import is too large to"
+ "upload. Please contact your Snikket operator.")
# https://play.google.com/store/apps/details?id=org.snikket.android&referrer={uri|urlescape}&pcampaignid=pcampaignidMKT-Other-global-all-co-prtnr-py-PartBadge-Mar2515-1
@@ -163,6 +169,7 @@ async def register(id_: str) -> typing.Union[str, quart.Response]:
raise
else:
http_session[INVITE_SESSION_JID] = jid
+ await client.login(jid, form.password.data)
return redirect(url_for(".success"))
return await render_template(
@@ -232,11 +239,55 @@ async def reset(id_: str) -> typing.Union[str, quart.Response]:
)
+class DataImportForm(BaseForm):
+ account_data_file = wtforms.FileField(
+ _l("Account data file")
+ )
+
+ action_import = wtforms.SubmitField(
+ _l("Import data")
+ )
+
+
@bp.route("/success", methods=["GET", "POST"])
+@client.require_session()
async def success() -> str:
+ form = DataImportForm()
+ if form.validate_on_submit():
+ ok = True
+ file_info = (await request.files).get(form.account_data_file.name)
+ if file_info is not None:
+ mimetype = file_info.mimetype
+ data = file_info.stream.read()
+ if len(data) > MAX_IMPORT_DATA_SIZE:
+ form.account_data_file.errors.append(EIMPORTTOOBIG)
+ ok = False
+ elif mimetype not in SUPPORTED_IMPORT_TYPES:
+ form.account_data_file.errors.append(
+ # not breaking the line here to avoid extract
+ # translations failing (defensive)
+ gettext("The account data you tried to import is in an unknown format. Please upload an XML file in XEP-0227 format (provided format: %(mimetype)s).", mimetype=mimetype), # noqa:E501
+ )
+ ok = False
+ elif len(data) > 0:
+ await client.import_account_data(data)
+
+ if ok:
+ # Re-render success page, this time with no import option
+ return await render_template(
+ "invite_success.html",
+ jid=http_session.get(INVITE_SESSION_JID, ""),
+ migration_success=True,
+ )
+
return await render_template(
"invite_success.html",
jid=http_session.get(INVITE_SESSION_JID, ""),
+ migration_success=False,
+ form=form,
+ max_import_size=MAX_IMPORT_DATA_SIZE,
+ import_too_big_warning_header=_l("Error"),
+ import_too_big_warning=EIMPORTTOOBIG,
)
diff --git a/snikket_web/prosodyclient.py b/snikket_web/prosodyclient.py
index ab6346f..f065c25 100644
--- a/snikket_web/prosodyclient.py
+++ b/snikket_web/prosodyclient.py
@@ -296,6 +296,9 @@ class ProsodyClient:
def _public_v1_endpoint(self, subpath: str) -> str:
return "{}/register_api{}".format(self._endpoint_base, subpath)
+ def _xep227_endpoint(self, subpath: str) -> str:
+ return "{}/xep227{}".format(self._endpoint_base, subpath)
+
async def _oauth2_bearer_token(self,
session: aiohttp.ClientSession,
jid: str,
@@ -1121,6 +1124,34 @@ class ProsodyClient:
) as resp:
self._raise_error_from_response(resp)
+ @autosession
+ async def export_account_data(
+ self,
+ *,
+ session: aiohttp.ClientSession,
+ ) -> typing.Optional[str]:
+ async with session.get(
+ self._xep227_endpoint("/export?stores=roster,vcard,pep,pep_data"), # noqa:E501
+ ) as resp:
+ self._raise_error_from_response(resp)
+ if resp.status == 204:
+ return None
+ return await resp.text()
+
+ @autosession
+ async def import_account_data(
+ self,
+ user_xml: str,
+ *,
+ session: aiohttp.ClientSession,
+ ) -> bool:
+ async with session.put(
+ self._xep227_endpoint("/import?stores=roster,vcard,pep,pep_data"), # noqa:E501
+ data=user_xml,
+ ) as resp:
+ self._raise_error_from_response(resp)
+ return True
+
@autosession
async def revoke_token(
self,
diff --git a/snikket_web/static/img/icons.svg b/snikket_web/static/img/icons.svg
index dd6a38d..17ecc6d 100644
--- a/snikket_web/static/img/icons.svg
+++ b/snikket_web/static/img/icons.svg
@@ -42,6 +42,11 @@ licensed under the terms of the Apache 2.0 License -->
+
+
+
+
+
@@ -88,6 +93,21 @@ licensed under the terms of the Apache 2.0 License -->
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/snikket_web/templates/invite_success.html b/snikket_web/templates/invite_success.html
index 00cd482..e169633 100644
--- a/snikket_web/templates/invite_success.html
+++ b/snikket_web/templates/invite_success.html
@@ -1,6 +1,6 @@
{% extends "invite.html" %}
{% set body_id = "invite" %}
-{% from "library.j2" import form_button, clipboard_button %}
+{% from "library.j2" import form_button, clipboard_button, render_errors %}
{% block head_lead %}
{% trans site_name=config["SITE_NAME"] %}Successfully registered on {{ site_name }} | Snikket{% endtrans %}
{%- include "copy-snippet.html" -%}
@@ -16,5 +16,46 @@
{%- endcall -%}
{% trans %}You can now set up your legacy XMPP client with the above address and the password you chose during registration.{% endtrans %}
{% trans login_url=url_for('main.login') %}You can now safely close this page, or log in to the web portal to manage your account.{% endtrans %}
+
+ {% if migration_success %}
+
{% trans %}Import successful{% endtrans %}
+
{% trans %}Congratulations! Your account data has been successfully imported.{% endtrans %}
+ {% endif %}
+
+ {% if form %}
+
{% trans %}Moving to Snikket?{% endtrans %}
+
{% trans %}If you are moving from a different Snikket instance or another XMPP-compatible service, you may optionally import the data (contacts, profile information, etc.) from your previous account. When you have exported the data from your previous account, upload it using the form below.{% endtrans %}