import pathlib import typing import urllib.parse import aiohttp import quart.flask_patch from quart import ( Blueprint, current_app, render_template, redirect, request, url_for, session as http_session, ) import werkzeug import wtforms from flask_babel import lazy_gettext as _l, gettext from .infra import client, selected_locale, BaseForm 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 def apple_store_badge() -> str: locale = selected_locale() filename = "{}.svg".format(locale) static_path = pathlib.Path(__file__).parent / "static" / "img" / "apple" if (static_path / filename).exists(): return url_for("static", filename="img/apple/{}".format(filename)) return url_for("static", filename="img/apple/en.svg") @bp.context_processor def context() -> typing.Dict[str, typing.Any]: return { "apple_store_badge": apple_store_badge, } @bp.route("/") async def view_old(id_: str) -> werkzeug.Response: return redirect(url_for(".view", id_=id_)) @bp.route("//") async def view(id_: str) -> typing.Union[quart.Response, typing.Tuple[str, int], str]: try: invite = await client.get_public_invite_by_id(id_) except aiohttp.ClientResponseError as exc: if exc.status == 404: # invite expired return await render_template("invite_invalid.html"), 404 raise if invite.reset_localpart is not None: return await render_template( "invite_reset_view.html", invite=invite, invite_id=id_, account_jid="{}@{}".format(invite.reset_localpart, invite.domain) ) play_store_url = ( "https://play.google.com/store/apps/details?" + urllib.parse.urlencode( ( ("id", "org.snikket.android"), ("referrer", invite.xmpp_uri), ("pcampaignid", "pcampaignidMKT-Other-global-all-co-prtnr-py-" "PartBadge-Mar2515-1"), ), ) ) apple_store_url = current_app.config["APPLE_STORE_URL"] body = await render_template( "invite_view.html", invite=invite, play_store_url=play_store_url, apple_store_url=apple_store_url, f_droid_url="market://details?id=org.snikket.android", invite_id=id_, ) return quart.Response( body, headers={ "Link": "<{}>; rel=\"alternate\"".format(invite.xmpp_uri), } ) class RegisterForm(BaseForm): localpart = wtforms.StringField( _l("Username"), ) password = wtforms.PasswordField( _l("Password"), ) password_confirm = wtforms.PasswordField( _l("Confirm password"), validators=[wtforms.validators.InputRequired(), wtforms.validators.EqualTo( "password", _l("The passwords must match.") )] ) action_register = wtforms.SubmitField( _l("Create account") ) @bp.route("//register", methods=["GET", "POST"]) async def register(id_: str) -> typing.Union[str, werkzeug.Response]: try: invite = await client.get_public_invite_by_id(id_) except aiohttp.ClientResponseError as exc: if exc.status == 404: return redirect(url_for(".view", id_=id_)) if invite.reset_localpart is not None: return redirect(url_for(".reset", id_=id_)) form = RegisterForm() if form.validate_on_submit(): # log the user in? show a guide? no idea. try: jid = await client.register_with_token( username=form.localpart.data, password=form.password.data, token=id_, ) except aiohttp.ClientResponseError as exc: if exc.status == 409: form.localpart.errors.append( _l("That username is already taken.") ) elif exc.status == 403: form.localpart.errors.append( _l("Registration was declined for unknown reasons.") ) elif exc.status == 400: form.localpart.errors.append( _l("The username is not valid.") ) elif exc.status == 404: return redirect(url_for(".view", id_=id_)) else: raise else: http_session[INVITE_SESSION_JID] = jid await client.login(jid, form.password.data) return redirect(url_for(".success")) return await render_template( "invite_register.html", invite=invite, form=form, ) class ResetForm(BaseForm): password = wtforms.PasswordField( _l("Password"), ) password_confirm = wtforms.PasswordField( _l("Confirm password"), validators=[wtforms.validators.InputRequired(), wtforms.validators.EqualTo( "password", _l("The passwords must match.") )] ) action_reset = wtforms.SubmitField( _l("Change password") ) @bp.route("//reset", methods=["GET", "POST"]) async def reset(id_: str) -> typing.Union[str, werkzeug.Response]: try: invite = await client.get_public_invite_by_id(id_) except aiohttp.ClientResponseError as exc: if exc.status == 404: return redirect(url_for(".view", id_=id_)) if invite.reset_localpart is None: return redirect(url_for(".register", id_=id_)) form = ResetForm() if form.validate_on_submit(): # log the user in? show a guide? no idea. try: jid = await client.register_with_token( username=invite.reset_localpart, password=form.password.data, token=id_, ) except aiohttp.ClientResponseError as exc: if exc.status == 403: form.localpart.errors.append( _l("Registration was declined for unknown reasons.") ) elif exc.status == 404: return redirect(url_for(".view", id_=id_)) else: raise else: http_session[INVITE_SESSION_JID] = jid return redirect(url_for(".reset_success")) return await render_template( "invite_reset.html", invite=invite, form=form, ) 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, ) @bp.route("/success/reset", methods=["GET", "POST"]) async def reset_success() -> str: return await render_template( "invite_reset_success.html", jid=http_session.get(INVITE_SESSION_JID, ""), ) @bp.route("/-") async def index() -> werkzeug.Response: return redirect(url_for("index"))