Compare commits

...

4 Commits

Author SHA1 Message Date
Matthew Wild
db363367da Support circles with multiple group chats, remove default group chat 2023-11-06 13:52:30 +00:00
Matthew Wild
7ce13b55ac Merge pull request #162 from snikket-im/feature/policies-and-contacts
Add policy URLs and contact addresses for instances in the relevant places
2023-10-25 16:19:44 +01:00
Kim Alvefur
da52771ebe Merge pull request #161 from Zash/fix-logout
Fix revokation of token on logout
2023-10-21 15:57:46 +02:00
Kim Alvefur
e39b6ca8bb Fix revokation of token on logout
In OAuth 2.0, you don't authenticate with the revocation endpoint using
the token you are revoking, but rather the OAuth client credentials.
2023-10-07 17:39:37 +02:00
7 changed files with 275 additions and 71 deletions

View File

@@ -458,6 +458,8 @@ class EditCircleForm(BaseForm):
_l("Add user") _l("Add user")
) )
action_remove_group_chat = wtforms.StringField()
@bp.route("/circle/<id_>", methods=["GET", "POST"]) @bp.route("/circle/<id_>", methods=["GET", "POST"])
@client.require_admin_session() @client.require_admin_session()
@@ -530,6 +532,15 @@ async def edit_circle(id_: str) -> typing.Union[str, werkzeug.Response]:
_("User removed from circle"), _("User removed from circle"),
"success", "success",
) )
elif form.action_remove_group_chat.data:
await client.remove_group_chat(
id_,
form.action_remove_group_chat.data,
)
await flash(
_("Chat removed from circle"),
"success",
)
return redirect(url_for(".edit_circle", id_=id_)) return redirect(url_for(".edit_circle", id_=id_))
@@ -537,6 +548,7 @@ async def edit_circle(id_: str) -> typing.Union[str, werkzeug.Response]:
"admin_edit_circle.html", "admin_edit_circle.html",
target_circle=circle, target_circle=circle,
form=form, form=form,
circle_chats=circle.chats,
circle_members=circle_members, circle_members=circle_members,
invite_form=invite_form, invite_form=invite_form,
) )
@@ -583,6 +595,56 @@ async def delete_circle(id_: str) -> typing.Union[str, werkzeug.Response]:
) )
class AddCircleChatForm(BaseForm):
name = wtforms.StringField(
_l("Group chat name"),
validators=[wtforms.validators.InputRequired()],
)
action_save = wtforms.SubmitField(
_l("Create group chat")
)
@bp.route("/circle/<id_>/add_chat", methods=["GET", "POST"])
@client.require_admin_session()
async def edit_circle_add_chat(
id_: str
) -> typing.Union[str, werkzeug.Response]:
async with client.authenticated_session() as session:
try:
circle = await client.get_group_by_id(
id_,
session=session,
)
except aiohttp.ClientResponseError as exc:
if exc.status == 404:
await flash(
_("No such circle exists"),
"alert",
)
return redirect(url_for(".circles"))
raise
form = AddCircleChatForm()
if form.validate_on_submit():
if form.action_save.data:
await client.add_group_chat(id_, form.name.data)
await flash(
_("New group chat added to circle"),
"success",
)
return redirect(url_for(".edit_circle", id_=id_))
return await render_template(
"admin_create_circle_chat.html",
target_circle=circle,
group_chat_form=form,
)
_CPU_EPOCH = time.process_time() _CPU_EPOCH = time.process_time()
_MONOTONIC_EPOCH = time.monotonic() _MONOTONIC_EPOCH = time.monotonic()

View File

@@ -117,12 +117,30 @@ class AdminInviteInfo:
) )
@dataclasses.dataclass(frozen=True)
class AdminGroupChatInfo:
id_: str
jid: str
name: str
@classmethod
def from_api_response(
cls,
data: typing.Mapping[str, typing.Any],
) -> "AdminGroupChatInfo":
return cls(
id_=data["id"],
jid=data["jid"],
name=data["name"],
)
@dataclasses.dataclass(frozen=True) @dataclasses.dataclass(frozen=True)
class AdminGroupInfo: class AdminGroupInfo:
id_: str id_: str
name: str name: str
muc_jid: typing.Optional[str]
members: typing.Collection[str] members: typing.Collection[str]
chats: typing.Collection[AdminGroupChatInfo]
@classmethod @classmethod
def from_api_response( def from_api_response(
@@ -132,8 +150,11 @@ class AdminGroupInfo:
return cls( return cls(
id_=data["id"], id_=data["id"],
name=data["name"], name=data["name"],
muc_jid=data.get("muc_jid") or None,
members=data.get("members", []), members=data.get("members", []),
chats=[
AdminGroupChatInfo.from_api_response(x)
for x in data.get("chats", [])
]
) )
@@ -1032,7 +1053,7 @@ class ProsodyClient:
self, self,
name: str, name: str,
*, *,
create_muc: bool = True, create_muc: bool = False,
session: aiohttp.ClientSession, session: aiohttp.ClientSession,
) -> AdminGroupInfo: ) -> AdminGroupInfo:
payload = { payload = {
@@ -1107,6 +1128,27 @@ class ProsodyClient:
) as resp: ) as resp:
self._raise_error_from_response(resp) self._raise_error_from_response(resp)
@autosession
async def add_group_chat(
self,
id_: str,
name: str,
*,
session: aiohttp.ClientSession,
) -> None:
payload: typing.Dict[str, typing.Any] = {
"name": name,
}
async with session.post(
self._admin_v1_endpoint(
"/groups/{}/chats".format(id_)
),
json=payload,
) as resp:
self._raise_error_from_response(resp)
@autosession @autosession
async def remove_group_member( async def remove_group_member(
self, self,
@@ -1122,6 +1164,21 @@ class ProsodyClient:
) as resp: ) as resp:
self._raise_error_from_response(resp) self._raise_error_from_response(resp)
@autosession
async def remove_group_chat(
self,
group_id: str,
chat_id: str,
*,
session: aiohttp.ClientSession,
) -> None:
async with session.delete(
self._admin_v1_endpoint(
"/groups/{}/chats/{}".format(group_id, chat_id)
),
) as resp:
self._raise_error_from_response(resp)
@autosession @autosession
async def delete_group( async def delete_group(
self, self,
@@ -1162,7 +1219,6 @@ class ProsodyClient:
self._raise_error_from_response(resp) self._raise_error_from_response(resp)
return True return True
@autosession
async def revoke_token( async def revoke_token(
self, self,
*, *,
@@ -1176,7 +1232,8 @@ class ProsodyClient:
async def logout(self) -> None: async def logout(self) -> None:
try: try:
await self.revoke_token() async with self._plain_session as session:
await self.revoke_token(session=session)
except aiohttp.ClientError: except aiohttp.ClientError:
self.logger.warn("failed to revoke token!", self.logger.warn("failed to revoke token!",
exc_info=True) exc_info=True)

View File

@@ -3,7 +3,7 @@
{% block content %} {% block content %}
<h1>{% trans %}Manage circles{% endtrans %}</h1> <h1>{% trans %}Manage circles{% endtrans %}</h1>
<p>{% trans %}<em>Circles</em> aim to help people who are in the same social circle find each other on your service.{% endtrans %}</p> <p>{% trans %}<em>Circles</em> aim to help people who are in the same social circle find each other on your service.{% endtrans %}</p>
<p>{% trans %}Users who are in the same circle will see each other in their contact list. In addition, each circle has a group chat where the circle members are included.{% endtrans %}</p> <p>{% trans %}Users who are in the same circle will see each other in their contact list. In addition, each circle may have group chats where the circle members are included.{% endtrans %}</p>
{%- if circles -%} {%- if circles -%}
<form method="POST" action="{{ url_for(".create_invite") }}"> <form method="POST" action="{{ url_for(".create_invite") }}">
{{- invite_form.csrf_token -}} {{- invite_form.csrf_token -}}

View File

@@ -0,0 +1,5 @@
{% extends "admin_app.html" %}
{% block content %}
<h1>{{ target_circle.name }}</h1>
{%- include "admin_create_circle_group_chat_form.html" -%}
{% endblock %}

View File

@@ -0,0 +1,15 @@
{% from "library.j2" import form_button, render_errors %}
<form method="POST" action="{{ url_for(".edit_circle_add_chat", id_=target_circle.id_) }}">
{{- group_chat_form.csrf_token -}}
<div class="form layout-expanded">
<h2 class="form-title">{% trans %}Create new circle group chat{% endtrans %}</h2>
<p class="form-descr weak">{% trans %}Add a chat to your circle so its members can hold group discussions.{% endtrans %}</p>
<p class="form-descr weak"><strong>{% trans %}Tip:{% endtrans %}</strong> {% trans %}This is only for creating group chats that automatically include <em>all</em> members of the circle. If you want a normal group chat, create it in the Snikket app instead.{% endtrans %}</p>
<div class="f-ebox">
{{ group_chat_form.name.label }}
{{ group_chat_form.name }}
</div>
<div class="f-bbox">
{%- call form_button("add", group_chat_form.action_save, class="primary") %}{% endcall -%}
</div>
</div></form>

View File

@@ -13,13 +13,6 @@
<div class="box hint form layout-expanded"> <div class="box hint form layout-expanded">
<header>{% trans %}This is your main circle{% endtrans %}</header> <header>{% trans %}This is your main circle{% endtrans %}</header>
<p>{% trans %}This circle is managed automatically and cannot be removed or renamed.{% endtrans %}</p> <p>{% trans %}This circle is managed automatically and cannot be removed or renamed.{% endtrans %}</p>
{%- if target_circle.muc_jid -%}
<div><label for="circle-muc-jid">{% trans %}Group chat address{% endtrans %}</label></div>
<div><input type="text" readonly="readonly" id="circle-muc-jid" value="{{ target_circle.muc_jid }}"></div>
{%- call clipboard_button(target_circle.muc_jid, show_label=True) -%}
{%- trans -%}Copy address{%- endtrans -%}
{%- endcall -%}
{%- endif -%}
</div> </div>
{%- else -%} {%- else -%}
<div class="form layout-expanded"> <div class="form layout-expanded">
@@ -28,17 +21,6 @@
{{ form.name.label }} {{ form.name.label }}
{{ form.name }} {{ form.name }}
</div> </div>
<div class="f-ebox">
{%- if target_circle.muc_jid -%}
<label for="circle-muc-jid">{% trans %}Group chat address{% endtrans %}</label>
<input type="text" readonly="readonly" id="circle-muc-jid" value="{{ target_circle.muc_jid }}">
{%- call clipboard_button(target_circle.muc_jid, show_label=True) -%}
{%- trans -%}Copy address{%- endtrans -%}
{%- endcall -%}
{%- else -%}
<p>{% trans %}This circle has no group chat associated.{% endtrans %}<p>
{%- endif -%}
</div>
<div class="f-bbox"> <div class="f-bbox">
{%- call standard_button("back", url_for(".circles"), class="tertiary") -%} {%- call standard_button("back", url_for(".circles"), class="tertiary") -%}
{% trans %}Return to circle list{% endtrans %} {% trans %}Return to circle list{% endtrans %}
@@ -52,7 +34,39 @@
</div> </div>
</div> </div>
{%- endif -%} {%- endif -%}
<h2 id="chats">{% trans %}Group chats{% endtrans %}</h2>
<p>{% trans %}These group chats will be available to all members of the circle.{% endtrans %}</p>
{%- if circle_chats -%}
<div class="el-2 elevated"><table>
<thead>
<th>{% trans %}Name{% endtrans %}</th>
<th>{% trans %}Actions{% endtrans %}</th>
</thead>
<tbody>
{%- for chat in circle_chats -%}
<tr>
<td class="collapsible">{% call value_or_hint(chat.name) %}{% endcall %}</td>
<td class="nowrap">
{%- call custom_form_button("delete", form.action_remove_group_chat.name, chat.id_, class="primary danger", slim=True) -%}
{% trans name=chat.name %}Delete group chat '{{ name }}'{% endtrans %}
{%- endcall -%}
</td>
</tr>
{%- endfor -%}
</tbody>
</table></div>
{%- else -%}
<p>{% trans %}This circle currently has no group chats.{% endtrans %}</p>
{%- endif -%}
{%- call standard_button("add", url_for(".edit_circle_add_chat", id_=target_circle.id_), class="secondary") -%}
{% trans %}Add group chat{% endtrans %}
{%- endcall -%}
<h2 id="members">{% trans %}Circle members{% endtrans %}</h2> <h2 id="members">{% trans %}Circle members{% endtrans %}</h2>
<p>{% trans %}All members of the circle will see each other in their contact list.{% endtrans %}</p>
{%- if circle_members -%} {%- if circle_members -%}
<div class="el-2 elevated"><table> <div class="el-2 elevated"><table>
<thead> <thead>

View File

@@ -8,7 +8,7 @@ msgid ""
msgstr "" msgstr ""
"Project-Id-Version: PROJECT VERSION\n" "Project-Id-Version: PROJECT VERSION\n"
"Report-Msgid-Bugs-To: EMAIL@ADDRESS\n" "Report-Msgid-Bugs-To: EMAIL@ADDRESS\n"
"POT-Creation-Date: 2023-10-25 16:15+0100\n" "POT-Creation-Date: 2023-11-06 13:46+0000\n"
"PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n" "PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n"
"Last-Translator: FULL NAME <EMAIL@ADDRESS>\n" "Last-Translator: FULL NAME <EMAIL@ADDRESS>\n"
"Language-Team: LANGUAGE <LL@li.org>\n" "Language-Team: LANGUAGE <LL@li.org>\n"
@@ -18,13 +18,13 @@ msgstr ""
"Generated-By: Babel 2.13.1\n" "Generated-By: Babel 2.13.1\n"
#: snikket_web/admin.py:69 snikket_web/templates/admin_delete_user.html:10 #: snikket_web/admin.py:69 snikket_web/templates/admin_delete_user.html:10
#: snikket_web/templates/admin_edit_circle.html:59 #: snikket_web/templates/admin_edit_circle.html:73
#: snikket_web/templates/admin_users.html:8 #: snikket_web/templates/admin_users.html:8
msgid "Login name" msgid "Login name"
msgstr "" msgstr ""
#: snikket_web/admin.py:73 snikket_web/templates/admin_delete_user.html:12 #: snikket_web/admin.py:73 snikket_web/templates/admin_delete_user.html:12
#: snikket_web/templates/admin_edit_circle.html:60 #: snikket_web/templates/admin_edit_circle.html:74
#: snikket_web/templates/admin_users.html:9 snikket_web/user.py:63 #: snikket_web/templates/admin_users.html:9 snikket_web/user.py:63
msgid "Display name" msgid "Display name"
msgstr "" msgstr ""
@@ -143,6 +143,7 @@ msgstr ""
#: snikket_web/admin.py:394 snikket_web/admin.py:442 #: snikket_web/admin.py:394 snikket_web/admin.py:442
#: snikket_web/templates/admin_delete_circle.html:10 #: snikket_web/templates/admin_delete_circle.html:10
#: snikket_web/templates/admin_edit_circle.html:44
msgid "Name" msgid "Name"
msgstr "" msgstr ""
@@ -166,47 +167,63 @@ msgstr ""
msgid "Add user" msgid "Add user"
msgstr "" msgstr ""
#: snikket_web/admin.py:474 snikket_web/admin.py:563 #: snikket_web/admin.py:476 snikket_web/admin.py:575 snikket_web/admin.py:623
msgid "No such circle exists" msgid "No such circle exists"
msgstr "" msgstr ""
#: snikket_web/admin.py:511 #: snikket_web/admin.py:513
msgid "Circle data updated" msgid "Circle data updated"
msgstr "" msgstr ""
#: snikket_web/admin.py:521 #: snikket_web/admin.py:523
msgid "User added to circle" msgid "User added to circle"
msgstr "" msgstr ""
#: snikket_web/admin.py:530 #: snikket_web/admin.py:532
msgid "User removed from circle" msgid "User removed from circle"
msgstr "" msgstr ""
#: snikket_web/admin.py:547 #: snikket_web/admin.py:541
msgid "Chat removed from circle"
msgstr ""
#: snikket_web/admin.py:559
msgid "Delete circle permanently" msgid "Delete circle permanently"
msgstr "" msgstr ""
#: snikket_web/admin.py:574 #: snikket_web/admin.py:586
msgid "Circle deleted" msgid "Circle deleted"
msgstr "" msgstr ""
#: snikket_web/admin.py:640 #: snikket_web/admin.py:600
msgid "Group chat name"
msgstr ""
#: snikket_web/admin.py:605
msgid "Create group chat"
msgstr ""
#: snikket_web/admin.py:635
msgid "New group chat added to circle"
msgstr ""
#: snikket_web/admin.py:702
msgid "Message contents" msgid "Message contents"
msgstr "" msgstr ""
#: snikket_web/admin.py:646 #: snikket_web/admin.py:708
msgid "Only send to online users" msgid "Only send to online users"
msgstr "" msgstr ""
#: snikket_web/admin.py:650 #: snikket_web/admin.py:712
msgid "Post to all users" msgid "Post to all users"
msgstr "" msgstr ""
#: snikket_web/admin.py:654 #: snikket_web/admin.py:716
msgid "Send preview to yourself" msgid "Send preview to yourself"
msgstr "" msgstr ""
#: snikket_web/admin.py:676 #: snikket_web/admin.py:738
msgid "Announcement sent!" msgid "Announcement sent!"
msgstr "" msgstr ""
@@ -474,8 +491,8 @@ msgstr ""
#: snikket_web/templates/admin_circles.html:6 #: snikket_web/templates/admin_circles.html:6
msgid "" msgid ""
"Users who are in the same circle will see each other in their contact " "Users who are in the same circle will see each other in their contact "
"list. In addition, each circle has a group chat where the circle members " "list. In addition, each circle may have group chats where the circle "
"are included." "members are included."
msgstr "" msgstr ""
#: snikket_web/templates/admin_circles.html:13 #: snikket_web/templates/admin_circles.html:13
@@ -487,7 +504,8 @@ msgid "Members"
msgstr "" msgstr ""
#: snikket_web/templates/admin_circles.html:15 #: snikket_web/templates/admin_circles.html:15
#: snikket_web/templates/admin_edit_circle.html:61 #: snikket_web/templates/admin_edit_circle.html:45
#: snikket_web/templates/admin_edit_circle.html:75
#: snikket_web/templates/admin_invites.html:24 #: snikket_web/templates/admin_invites.html:24
#: snikket_web/templates/admin_users.html:10 #: snikket_web/templates/admin_users.html:10
msgid "Actions" msgid "Actions"
@@ -523,6 +541,25 @@ msgstr ""
msgid "New circle" msgid "New circle"
msgstr "" msgstr ""
#: snikket_web/templates/admin_create_circle_group_chat_form.html:5
msgid "Create new circle group chat"
msgstr ""
#: snikket_web/templates/admin_create_circle_group_chat_form.html:6
msgid "Add a chat to your circle so its members can hold group discussions."
msgstr ""
#: snikket_web/templates/admin_create_circle_group_chat_form.html:7
msgid "Tip:"
msgstr ""
#: snikket_web/templates/admin_create_circle_group_chat_form.html:7
msgid ""
"This is only for creating group chats that automatically include "
"<em>all</em> members of the circle. If you want a normal group chat, "
"create it in the Snikket app instead."
msgstr ""
#: snikket_web/templates/admin_create_invite.html:3 #: snikket_web/templates/admin_create_invite.html:3
msgid "Create invitation" msgid "Create invitation"
msgstr "" msgstr ""
@@ -565,8 +602,8 @@ msgid "Delete circle %(circle_name)s"
msgstr "" msgstr ""
#: snikket_web/templates/admin_delete_circle.html:6 #: snikket_web/templates/admin_delete_circle.html:6
#: snikket_web/templates/admin_edit_circle.html:48 #: snikket_web/templates/admin_edit_circle.html:30
#: snikket_web/templates/admin_edit_circle.html:51 #: snikket_web/templates/admin_edit_circle.html:33
msgid "Delete circle" msgid "Delete circle"
msgstr "" msgstr ""
@@ -625,69 +662,78 @@ msgstr ""
msgid "This circle is managed automatically and cannot be removed or renamed." msgid "This circle is managed automatically and cannot be removed or renamed."
msgstr "" msgstr ""
#: snikket_web/templates/admin_edit_circle.html:17 #: snikket_web/templates/admin_edit_circle.html:19
#: snikket_web/templates/admin_edit_circle.html:33
msgid "Group chat address"
msgstr ""
#: snikket_web/templates/admin_edit_circle.html:20
#: snikket_web/templates/admin_edit_circle.html:36
#: snikket_web/templates/invite_success.html:15
#: snikket_web/templates/user_home.html:21
msgid "Copy address"
msgstr ""
#: snikket_web/templates/admin_edit_circle.html:26
msgid "Circle information" msgid "Circle information"
msgstr "" msgstr ""
#: snikket_web/templates/admin_edit_circle.html:39 #: snikket_web/templates/admin_edit_circle.html:26
msgid "This circle has no group chat associated."
msgstr ""
#: snikket_web/templates/admin_edit_circle.html:44
msgid "Return to circle list" msgid "Return to circle list"
msgstr "" msgstr ""
#: snikket_web/templates/admin_edit_circle.html:49 #: snikket_web/templates/admin_edit_circle.html:31
msgid "Deleting a circle does not delete any users in the circle." msgid "Deleting a circle does not delete any users in the circle."
msgstr "" msgstr ""
#: snikket_web/templates/admin_edit_circle.html:55 #: snikket_web/templates/admin_edit_circle.html:38
msgid "Group chats"
msgstr ""
#: snikket_web/templates/admin_edit_circle.html:39
msgid "These group chats will be available to all members of the circle."
msgstr ""
#: snikket_web/templates/admin_edit_circle.html:53
#, python-format
msgid "Delete group chat '%(name)s'"
msgstr ""
#: snikket_web/templates/admin_edit_circle.html:61
msgid "This circle currently has no group chats."
msgstr ""
#: snikket_web/templates/admin_edit_circle.html:64
msgid "Add group chat"
msgstr ""
#: snikket_web/templates/admin_edit_circle.html:67
msgid "Circle members" msgid "Circle members"
msgstr "" msgstr ""
#: snikket_web/templates/admin_edit_circle.html:71 #: snikket_web/templates/admin_edit_circle.html:68
msgid "All members of the circle will see each other in their contact list."
msgstr ""
#: snikket_web/templates/admin_edit_circle.html:85
msgid "The user has been deleted from the server." msgid "The user has been deleted from the server."
msgstr "" msgstr ""
#: snikket_web/templates/admin_edit_circle.html:71 #: snikket_web/templates/admin_edit_circle.html:85
#: snikket_web/templates/library.j2:108 #: snikket_web/templates/library.j2:108
msgid "deleted" msgid "deleted"
msgstr "" msgstr ""
#: snikket_web/templates/admin_edit_circle.html:77 #: snikket_web/templates/admin_edit_circle.html:91
#, python-format #, python-format
msgid "Remove user %(username)s from circle" msgid "Remove user %(username)s from circle"
msgstr "" msgstr ""
#: snikket_web/templates/admin_edit_circle.html:85 #: snikket_web/templates/admin_edit_circle.html:99
msgid "This circle currently has no members." msgid "This circle currently has no members."
msgstr "" msgstr ""
#: snikket_web/templates/admin_edit_circle.html:87 #: snikket_web/templates/admin_edit_circle.html:101
msgid "Invite more members" msgid "Invite more members"
msgstr "" msgstr ""
#: snikket_web/templates/admin_edit_circle.html:90 #: snikket_web/templates/admin_edit_circle.html:104
msgid "Add existing user" msgid "Add existing user"
msgstr "" msgstr ""
#: snikket_web/templates/admin_edit_circle.html:101 #: snikket_web/templates/admin_edit_circle.html:115
msgid "All users added" msgid "All users added"
msgstr "" msgstr ""
#: snikket_web/templates/admin_edit_circle.html:102 #: snikket_web/templates/admin_edit_circle.html:116
msgid "All users on this service are already in this circle." msgid "All users on this service are already in this circle."
msgstr "" msgstr ""
@@ -1238,6 +1284,11 @@ msgstr ""
msgid "Your address" msgid "Your address"
msgstr "" msgstr ""
#: snikket_web/templates/invite_success.html:15
#: snikket_web/templates/user_home.html:21
msgid "Copy address"
msgstr ""
#: snikket_web/templates/invite_success.html:17 #: snikket_web/templates/invite_success.html:17
msgid "" msgid ""
"You can now set up your legacy XMPP client with the above address and the" "You can now set up your legacy XMPP client with the above address and the"