You've already forked snikket-web-portal
@@ -1,3 +1,4 @@
|
||||
import asyncio
|
||||
import dataclasses
|
||||
import enum
|
||||
import functools
|
||||
@@ -494,6 +495,138 @@ class ProsodyClient:
|
||||
)
|
||||
return xmpputil.extract_avatar_data_get_reply(data_resp)
|
||||
|
||||
@autosession
|
||||
async def get_pubsub_node_access_model(
|
||||
self,
|
||||
to: str,
|
||||
node: str,
|
||||
default: str,
|
||||
*,
|
||||
session: aiohttp.ClientSession) -> str:
|
||||
config = xmpputil.extract_pubsub_node_config_get_reply(
|
||||
await self._xml_iq_call(
|
||||
session,
|
||||
xmpputil.make_pubsub_node_config_get_request(
|
||||
to,
|
||||
node,
|
||||
),
|
||||
)
|
||||
)
|
||||
try:
|
||||
return config[xmpputil.FORM_FIELD_PUBSUB_ACCESS_MODEL][0]
|
||||
except (ValueError, KeyError):
|
||||
return default
|
||||
|
||||
@autosession
|
||||
async def set_pubsub_node_access_model(
|
||||
self,
|
||||
to: str,
|
||||
node: str,
|
||||
new_access_model: str,
|
||||
*,
|
||||
ignore_not_found: bool = False,
|
||||
session: aiohttp.ClientSession) -> None:
|
||||
try:
|
||||
xmpputil.extract_iq_reply(await self._xml_iq_call(
|
||||
session,
|
||||
xmpputil.make_pubsub_access_model_put_request(
|
||||
to,
|
||||
node,
|
||||
new_access_model,
|
||||
)
|
||||
))
|
||||
except quart.exceptions.NotFound:
|
||||
if ignore_not_found:
|
||||
return
|
||||
raise
|
||||
|
||||
@autosession
|
||||
async def get_nickname_access_model(
|
||||
self,
|
||||
*,
|
||||
session: aiohttp.ClientSession) -> str:
|
||||
return await self.get_pubsub_node_access_model(
|
||||
self.session_address,
|
||||
xmpputil.NODE_USER_NICKNAME,
|
||||
"open",
|
||||
session=session,
|
||||
)
|
||||
|
||||
@autosession
|
||||
async def set_nickname_access_model(
|
||||
self,
|
||||
new_access_model: str,
|
||||
*,
|
||||
session: aiohttp.ClientSession) -> None:
|
||||
await self.set_pubsub_node_access_model(
|
||||
self.session_address,
|
||||
xmpputil.NODE_USER_NICKNAME,
|
||||
new_access_model,
|
||||
session=session,
|
||||
ignore_not_found=True,
|
||||
)
|
||||
|
||||
@autosession
|
||||
async def get_avatar_access_model(
|
||||
self,
|
||||
*,
|
||||
session: aiohttp.ClientSession) -> str:
|
||||
return await self.get_pubsub_node_access_model(
|
||||
self.session_address,
|
||||
xmpputil.NODE_USER_AVATAR_METADATA,
|
||||
"open",
|
||||
session=session,
|
||||
)
|
||||
|
||||
@autosession
|
||||
async def set_avatar_access_model(
|
||||
self,
|
||||
new_access_model: str,
|
||||
*,
|
||||
session: aiohttp.ClientSession) -> None:
|
||||
await asyncio.gather(
|
||||
self.set_pubsub_node_access_model(
|
||||
self.session_address,
|
||||
xmpputil.NODE_USER_AVATAR_DATA,
|
||||
new_access_model,
|
||||
ignore_not_found=True,
|
||||
session=session,
|
||||
),
|
||||
self.set_pubsub_node_access_model(
|
||||
self.session_address,
|
||||
xmpputil.NODE_USER_AVATAR_METADATA,
|
||||
new_access_model,
|
||||
ignore_not_found=True,
|
||||
session=session,
|
||||
)
|
||||
)
|
||||
|
||||
@autosession
|
||||
async def get_vcard_access_model(
|
||||
self,
|
||||
*,
|
||||
session: aiohttp.ClientSession) -> str:
|
||||
return await self.get_pubsub_node_access_model(
|
||||
self.session_address,
|
||||
xmpputil.NODE_VCARD,
|
||||
"open",
|
||||
session=session,
|
||||
)
|
||||
|
||||
@autosession
|
||||
async def set_vcard_access_model(
|
||||
self,
|
||||
new_access_model: str,
|
||||
*,
|
||||
session: aiohttp.ClientSession) -> None:
|
||||
await self.set_pubsub_node_access_model(
|
||||
self.session_address,
|
||||
xmpputil.NODE_VCARD,
|
||||
new_access_model,
|
||||
session=session,
|
||||
ignore_not_found=True,
|
||||
)
|
||||
|
||||
@autosession
|
||||
async def set_user_avatar(
|
||||
self,
|
||||
|
||||
@@ -389,6 +389,18 @@ div.form.layout-expanded {
|
||||
}
|
||||
}
|
||||
|
||||
.f-ebox > ul {
|
||||
/* radio group */
|
||||
list-style-type: none;
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
|
||||
> li {
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
/* form buttons */
|
||||
|
||||
|
||||
@@ -14,6 +14,12 @@
|
||||
{{ form.avatar.label }}
|
||||
{{ form.avatar }}
|
||||
</div>
|
||||
<h3 class="form-title">{% trans %}Visibility{% endtrans %}</h3>
|
||||
<p class="form-descr weak">{% trans %}This section allows you to control who can see your profile information, like avatar and nickname.{% endtrans %}</p>
|
||||
<div class="f-ebox">
|
||||
{{ form.profile_access_model.label }}
|
||||
{{ form.profile_access_model }}
|
||||
</div>
|
||||
<div class="f-bbox">
|
||||
<a href="{{ url_for('user.index') }}" class="button secondary">{% trans %}Back{% endtrans %}</a><button type="submit" class="primary">{% trans %}Apply{% endtrans %}</button>
|
||||
</div>
|
||||
|
||||
@@ -42,33 +42,49 @@ msgstr "Passwort"
|
||||
msgid "Invalid user name or password."
|
||||
msgstr "Benutzername oder Passwort falsch."
|
||||
|
||||
#: snikket_web/user.py:25
|
||||
#: snikket_web/user.py:26
|
||||
msgid "Current password"
|
||||
msgstr "Aktuelles Passwort"
|
||||
|
||||
#: snikket_web/user.py:30
|
||||
#: snikket_web/user.py:31
|
||||
msgid "New password"
|
||||
msgstr "Neues Passwort"
|
||||
|
||||
#: snikket_web/user.py:35
|
||||
#: snikket_web/user.py:36
|
||||
msgid "Confirm new password"
|
||||
msgstr "Neues Passwort (Bestätigung)"
|
||||
|
||||
#: snikket_web/user.py:39
|
||||
#: snikket_web/user.py:40
|
||||
msgid "The new passwords must match."
|
||||
msgstr "Die neuen Passwörter müssen übereinstimmen."
|
||||
|
||||
#: snikket_web/user.py:50
|
||||
msgid "Nobody"
|
||||
msgstr "Niemand"
|
||||
|
||||
#: snikket_web/user.py:51
|
||||
msgid "Friends only"
|
||||
msgstr "Nur Freunde"
|
||||
|
||||
#: snikket_web/user.py:52
|
||||
msgid "Everyone"
|
||||
msgstr "Jeder"
|
||||
|
||||
#: snikket_web/templates/admin_delete_user.html:12
|
||||
#: snikket_web/templates/admin_delete_user.html:16
|
||||
#: snikket_web/templates/admin_users.html:15 snikket_web/user.py:50
|
||||
#: snikket_web/templates/admin_users.html:15 snikket_web/user.py:58
|
||||
msgid "Display name"
|
||||
msgstr "Anzeigename"
|
||||
|
||||
#: snikket_web/user.py:54
|
||||
#: snikket_web/user.py:62
|
||||
msgid "Avatar"
|
||||
msgstr "Bild"
|
||||
|
||||
#: snikket_web/user.py:78
|
||||
#: snikket_web/user.py:66
|
||||
msgid "Profile visibility"
|
||||
msgstr "Profilsichtbarkeit"
|
||||
|
||||
#: snikket_web/user.py:91
|
||||
msgid "Incorrect password"
|
||||
msgstr "Ungültiges Passwort"
|
||||
|
||||
@@ -107,7 +123,7 @@ msgstr "Einladung anzeigen"
|
||||
|
||||
#: snikket_web/templates/admin_edit_invite.html:39
|
||||
#: snikket_web/templates/user_passwd.html:39
|
||||
#: snikket_web/templates/user_profile.html:18
|
||||
#: snikket_web/templates/user_profile.html:24
|
||||
msgid "Back"
|
||||
msgstr "Zurück"
|
||||
|
||||
@@ -307,7 +323,19 @@ msgstr ""
|
||||
msgid "Profile"
|
||||
msgstr "Profil"
|
||||
|
||||
#: snikket_web/templates/user_profile.html:17
|
||||
msgid "Visibility"
|
||||
msgstr "Sichtbarkeit"
|
||||
|
||||
#: snikket_web/templates/user_profile.html:18
|
||||
msgid ""
|
||||
"This section allows you to control who can see your profile information, "
|
||||
"like avatar and nickname."
|
||||
msgstr ""
|
||||
"Hier kannst du einstellen, wer deine Profilinformationen, wie Bild oder "
|
||||
"Anzeigename einsehen kann."
|
||||
|
||||
#: snikket_web/templates/user_profile.html:24
|
||||
msgid "Apply"
|
||||
msgstr "Übernehmen"
|
||||
|
||||
|
||||
@@ -43,33 +43,49 @@ msgstr "Password"
|
||||
msgid "Invalid user name or password."
|
||||
msgstr "Confirm new password"
|
||||
|
||||
#: snikket_web/user.py:25
|
||||
#: snikket_web/user.py:26
|
||||
msgid "Current password"
|
||||
msgstr "Current password"
|
||||
|
||||
#: snikket_web/user.py:30
|
||||
#: snikket_web/user.py:31
|
||||
msgid "New password"
|
||||
msgstr "New password"
|
||||
|
||||
#: snikket_web/user.py:35
|
||||
#: snikket_web/user.py:36
|
||||
msgid "Confirm new password"
|
||||
msgstr "Confirm new password"
|
||||
|
||||
#: snikket_web/user.py:39
|
||||
#: snikket_web/user.py:40
|
||||
msgid "The new passwords must match."
|
||||
msgstr "The new passwords must match."
|
||||
|
||||
#: snikket_web/user.py:50
|
||||
msgid "Nobody"
|
||||
msgstr ""
|
||||
|
||||
#: snikket_web/user.py:51
|
||||
msgid "Friends only"
|
||||
msgstr ""
|
||||
|
||||
#: snikket_web/user.py:52
|
||||
msgid "Everyone"
|
||||
msgstr ""
|
||||
|
||||
#: snikket_web/templates/admin_delete_user.html:12
|
||||
#: snikket_web/templates/admin_delete_user.html:16
|
||||
#: snikket_web/templates/admin_users.html:15 snikket_web/user.py:50
|
||||
#: snikket_web/templates/admin_users.html:15 snikket_web/user.py:58
|
||||
msgid "Display name"
|
||||
msgstr "Display name"
|
||||
|
||||
#: snikket_web/user.py:54
|
||||
#: snikket_web/user.py:62
|
||||
msgid "Avatar"
|
||||
msgstr "Avatar"
|
||||
|
||||
#: snikket_web/user.py:78
|
||||
#: snikket_web/user.py:66
|
||||
msgid "Profile visibility"
|
||||
msgstr ""
|
||||
|
||||
#: snikket_web/user.py:91
|
||||
msgid "Incorrect password"
|
||||
msgstr "Incorrect password"
|
||||
|
||||
@@ -109,7 +125,7 @@ msgstr ""
|
||||
|
||||
#: snikket_web/templates/admin_edit_invite.html:39
|
||||
#: snikket_web/templates/user_passwd.html:39
|
||||
#: snikket_web/templates/user_profile.html:18
|
||||
#: snikket_web/templates/user_profile.html:24
|
||||
msgid "Back"
|
||||
msgstr "Back"
|
||||
|
||||
@@ -301,10 +317,23 @@ msgstr ""
|
||||
msgid "Profile"
|
||||
msgstr "Profile"
|
||||
|
||||
#: snikket_web/templates/user_profile.html:17
|
||||
msgid "Visibility"
|
||||
msgstr ""
|
||||
|
||||
#: snikket_web/templates/user_profile.html:18
|
||||
msgid ""
|
||||
"This section allows you to control who can see your profile information, "
|
||||
"like avatar and nickname."
|
||||
msgstr ""
|
||||
|
||||
#: snikket_web/templates/user_profile.html:24
|
||||
msgid "Apply"
|
||||
msgstr "Apply"
|
||||
|
||||
#~ msgid "none"
|
||||
#~ msgstr ""
|
||||
|
||||
#~ msgid "Friends"
|
||||
#~ msgstr ""
|
||||
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
import asyncio
|
||||
import typing
|
||||
|
||||
import quart.flask_patch
|
||||
@@ -45,6 +46,13 @@ class LogoutForm(flask_wtf.FlaskForm): # type:ignore
|
||||
pass
|
||||
|
||||
|
||||
_ACCESS_MODEL_CHOICES = [
|
||||
("whitelist", _l("Nobody")),
|
||||
("presence", _l("Friends only")),
|
||||
("open", _l("Everyone")),
|
||||
]
|
||||
|
||||
|
||||
class ProfileForm(flask_wtf.FlaskForm): # type:ignore
|
||||
nickname = wtforms.TextField(
|
||||
_l("Display name"),
|
||||
@@ -54,6 +62,11 @@ class ProfileForm(flask_wtf.FlaskForm): # type:ignore
|
||||
_l("Avatar")
|
||||
)
|
||||
|
||||
profile_access_model = wtforms.RadioField(
|
||||
_l("Profile visibility"),
|
||||
choices=_ACCESS_MODEL_CHOICES,
|
||||
)
|
||||
|
||||
|
||||
@bp.route("/")
|
||||
@client.require_session()
|
||||
@@ -89,7 +102,15 @@ async def profile() -> typing.Union[str, quart.Response]:
|
||||
form = ProfileForm()
|
||||
if request.method != "POST":
|
||||
user_info = await client.get_user_info()
|
||||
try:
|
||||
profile_access_model = await client.get_nickname_access_model()
|
||||
except quart.exceptions.NotFound:
|
||||
# avatar node does not exist yet, default the access model to
|
||||
# presence
|
||||
# that is what will be set if the user now adds a new avatar.
|
||||
profile_access_model = "presence"
|
||||
form.nickname.data = user_info.get("nickname", "")
|
||||
form.profile_access_model.data = profile_access_model
|
||||
|
||||
if form.validate_on_submit():
|
||||
user_info = await client.get_user_info()
|
||||
@@ -103,6 +124,14 @@ async def profile() -> typing.Union[str, quart.Response]:
|
||||
|
||||
if user_info.get("nickname") != form.nickname.data:
|
||||
await client.set_user_nickname(form.nickname.data)
|
||||
|
||||
access_model = form.profile_access_model.data
|
||||
await asyncio.gather(
|
||||
client.set_avatar_access_model(access_model),
|
||||
client.set_vcard_access_model(access_model),
|
||||
client.set_nickname_access_model(access_model),
|
||||
)
|
||||
|
||||
return redirect(url_for(".profile"))
|
||||
|
||||
return await render_template("user_profile.html", form=form)
|
||||
|
||||
@@ -19,9 +19,12 @@ ERROR_CODE_MAP = {
|
||||
}
|
||||
|
||||
NS_PUBSUB = "http://jabber.org/protocol/pubsub"
|
||||
NS_PUBSUB_OWNER = "http://jabber.org/protocol/pubsub#owner"
|
||||
TAG_PUBSUB = "{{{}}}pubsub".format(NS_PUBSUB)
|
||||
TAG_PUBSUB_OWNER = "{{{}}}pubsub".format(NS_PUBSUB_OWNER)
|
||||
TAG_PUBSUB_ITEM = "{{{}}}item".format(NS_PUBSUB)
|
||||
TAG_PUBSUB_ITEMS = "{{{}}}items".format(NS_PUBSUB)
|
||||
TAG_PUBSUB_CONFIGURE = "{{{}}}configure".format(NS_PUBSUB_OWNER)
|
||||
|
||||
NS_USER_NICKNAME = "http://jabber.org/protocol/nick"
|
||||
NODE_USER_NICKNAME = NS_USER_NICKNAME
|
||||
@@ -36,6 +39,16 @@ NODE_USER_AVATAR_DATA = "urn:xmpp:avatar:data"
|
||||
NS_USER_AVATAR_DATA = "urn:xmpp:avatar:data"
|
||||
TAG_USER_AVATAR_DATA = "{{{}}}data".format(NS_USER_AVATAR_DATA)
|
||||
|
||||
NODE_VCARD = "urn:xmpp:vcard4"
|
||||
|
||||
NS_DATA_FORM = "jabber:x:data"
|
||||
TAG_DATA_FORM_X = "{{{}}}x".format(NS_DATA_FORM)
|
||||
TAG_DATA_FORM_FIELD = "{{{}}}field".format(NS_DATA_FORM)
|
||||
TAG_DATA_FORM_VALUE = "{{{}}}value".format(NS_DATA_FORM)
|
||||
|
||||
FORM_NODE_CONFIG = "http://jabber.org/protocol/pubsub#node_config"
|
||||
FORM_FIELD_PUBSUB_ACCESS_MODEL = "pubsub#access_model"
|
||||
|
||||
|
||||
SimpleJID = typing.Tuple[typing.Optional[str], str, typing.Optional[str]]
|
||||
T = typing.TypeVar("T")
|
||||
@@ -211,7 +224,7 @@ def make_avatar_metadata_set_request(
|
||||
def _require_child(t: ET.Element, tag: str) -> ET.Element:
|
||||
el = t.find(tag)
|
||||
if el is None:
|
||||
raise abort(500, "malformed reply")
|
||||
raise abort(500, "malformed reply: missing {}".format(tag))
|
||||
return el
|
||||
|
||||
|
||||
@@ -283,3 +296,73 @@ def extract_avatar_data_get_reply(
|
||||
if data is None or data.text is None:
|
||||
return None
|
||||
return base64.b64decode(data.text)
|
||||
|
||||
|
||||
def make_pubsub_node_config_put_request(
|
||||
to: str, node: str,
|
||||
id_: typing.Optional[str] = None,
|
||||
) -> typing.Tuple[ET.Element, ET.Element]:
|
||||
req = ET.Element("iq", type="set", to=to)
|
||||
q = ET.SubElement(req, "pubsub", xmlns=NS_PUBSUB_OWNER)
|
||||
configure = ET.SubElement(q, "configure", node=node)
|
||||
form = ET.SubElement(configure, "x",
|
||||
xmlns=NS_DATA_FORM,
|
||||
type="submit")
|
||||
form_type = ET.SubElement(form, "field", var="FORM_TYPE", type="hidden")
|
||||
ET.SubElement(form_type, "value").text = FORM_NODE_CONFIG
|
||||
return req, form
|
||||
|
||||
|
||||
def make_pubsub_node_config_get_request(
|
||||
to: str, node: str,
|
||||
) -> ET.Element:
|
||||
req = ET.Element("iq", type="get", to=to)
|
||||
q = ET.SubElement(req, "pubsub", xmlns=NS_PUBSUB_OWNER)
|
||||
ET.SubElement(q, "configure", node=node)
|
||||
return req
|
||||
|
||||
|
||||
def add_form_field(
|
||||
form: ET.Element,
|
||||
var: str,
|
||||
values: typing.Union[str, typing.Collection[str]],
|
||||
type_: typing.Optional[str] = None,
|
||||
) -> ET.Element:
|
||||
if isinstance(values, str):
|
||||
values = [values]
|
||||
field = ET.SubElement(form, "field", var=var)
|
||||
if type_ is not None:
|
||||
field.set("type", type_)
|
||||
for v in values:
|
||||
ET.SubElement(field, "value").text = v
|
||||
return field
|
||||
|
||||
|
||||
def make_pubsub_access_model_put_request(
|
||||
to: str,
|
||||
node: str,
|
||||
new_access_model: str,
|
||||
) -> ET.Element:
|
||||
req, form = make_pubsub_node_config_put_request(to, node)
|
||||
add_form_field(form, FORM_FIELD_PUBSUB_ACCESS_MODEL, new_access_model)
|
||||
return req
|
||||
|
||||
|
||||
def extract_pubsub_node_config_get_reply(
|
||||
iq_tree: ET.Element,
|
||||
) -> typing.Mapping[str, typing.Sequence[str]]:
|
||||
payload = extract_iq_reply(iq_tree)
|
||||
if payload is None:
|
||||
raise ValueError("invalid reply")
|
||||
form = _require_child(_require_child(payload, TAG_PUBSUB_CONFIGURE),
|
||||
TAG_DATA_FORM_X)
|
||||
result: typing.MutableMapping[str, typing.List[str]] = {}
|
||||
for child in form.findall(TAG_DATA_FORM_FIELD):
|
||||
var = child.get("var")
|
||||
if var is None:
|
||||
continue
|
||||
values = [value_tag.text or ""
|
||||
for value_tag in child.findall(TAG_DATA_FORM_VALUE)]
|
||||
result[var] = values
|
||||
|
||||
return result
|
||||
|
||||
Reference in New Issue
Block a user