Skip to content

Commit

Permalink
Merge pull request #165 from snikket-im/feature/multiple-circle-mucs
Browse files Browse the repository at this point in the history
Support circles with multiple group chats, remove default group chat
  • Loading branch information
mwild1 authored Nov 6, 2023
2 parents 7ce13b5 + db36336 commit bd66600
Show file tree
Hide file tree
Showing 7 changed files with 269 additions and 65 deletions.
62 changes: 62 additions & 0 deletions snikket_web/admin.py
Original file line number Diff line number Diff line change
Expand Up @@ -458,6 +458,8 @@ class EditCircleForm(BaseForm):
_l("Add user")
)

action_remove_group_chat = wtforms.StringField()


@bp.route("/circle/<id_>", methods=["GET", "POST"])
@client.require_admin_session()
Expand Down Expand Up @@ -530,13 +532,23 @@ async def edit_circle(id_: str) -> typing.Union[str, werkzeug.Response]:
_("User removed from circle"),
"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 await render_template(
"admin_edit_circle.html",
target_circle=circle,
form=form,
circle_chats=circle.chats,
circle_members=circle_members,
invite_form=invite_form,
)
Expand Down Expand Up @@ -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()
_MONOTONIC_EPOCH = time.monotonic()

Expand Down
63 changes: 60 additions & 3 deletions snikket_web/prosodyclient.py
Original file line number Diff line number Diff line change
Expand Up @@ -117,12 +117,30 @@ def from_api_response(
)


@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)
class AdminGroupInfo:
id_: str
name: str
muc_jid: typing.Optional[str]
members: typing.Collection[str]
chats: typing.Collection[AdminGroupChatInfo]

@classmethod
def from_api_response(
Expand All @@ -132,8 +150,11 @@ def from_api_response(
return cls(
id_=data["id"],
name=data["name"],
muc_jid=data.get("muc_jid") or None,
members=data.get("members", []),
chats=[
AdminGroupChatInfo.from_api_response(x)
for x in data.get("chats", [])
]
)


Expand Down Expand Up @@ -1032,7 +1053,7 @@ async def create_group(
self,
name: str,
*,
create_muc: bool = True,
create_muc: bool = False,
session: aiohttp.ClientSession,
) -> AdminGroupInfo:
payload = {
Expand Down Expand Up @@ -1107,6 +1128,27 @@ async def add_group_member(
) as 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
async def remove_group_member(
self,
Expand All @@ -1122,6 +1164,21 @@ async def remove_group_member(
) as 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
async def delete_group(
self,
Expand Down
2 changes: 1 addition & 1 deletion snikket_web/templates/admin_circles.html
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@
{% block content %}
<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 %}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 -%}
<form method="POST" action="{{ url_for(".create_invite") }}">
{{- invite_form.csrf_token -}}
Expand Down
5 changes: 5 additions & 0 deletions snikket_web/templates/admin_create_circle_chat.html
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
{% extends "admin_app.html" %}
{% block content %}
<h1>{{ target_circle.name }}</h1>
{%- include "admin_create_circle_group_chat_form.html" -%}
{% endblock %}
15 changes: 15 additions & 0 deletions snikket_web/templates/admin_create_circle_group_chat_form.html
Original file line number Diff line number Diff line change
@@ -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>
50 changes: 32 additions & 18 deletions snikket_web/templates/admin_edit_circle.html
Original file line number Diff line number Diff line change
Expand Up @@ -13,13 +13,6 @@ <h1>{% trans circle_name=(target_circle | circle_name) %}Edit circle {{ circle_n
<div class="box hint form layout-expanded">
<header>{% trans %}This is your main circle{% endtrans %}</header>
<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>
{%- else -%}
<div class="form layout-expanded">
Expand All @@ -28,17 +21,6 @@ <h2 class="form-title">{% trans %}Circle information{% endtrans %}</h2>
{{ form.name.label }}
{{ form.name }}
</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">
{%- call standard_button("back", url_for(".circles"), class="tertiary") -%}
{% trans %}Return to circle list{% endtrans %}
Expand All @@ -52,7 +34,39 @@ <h3 class="form-title">{% trans %}Delete circle{% endtrans %}</h3>
</div>
</div>
{%- 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>
<p>{% trans %}All members of the circle will see each other in their contact list.{% endtrans %}</p>

{%- if circle_members -%}
<div class="el-2 elevated"><table>
<thead>
Expand Down
Loading

0 comments on commit bd66600

Please sign in to comment.