- Arguments are now grouped in a new `PageData` NamedTuple, making it simpler to use, and easier to extend. - Page index is automatically calculated, and can be checked in callback to know which page to show. - Form send by calling entity is automatically parsed, using original form to instantiate the right field types. - A new `DataForm.get_field` method use Generic type as second argument to retrieve the right field when getting a `submit` form. - Updated all classes registering Ad-Hoc commands to use the new system and simplify the code.
301 lines
12 KiB
Python
301 lines
12 KiB
Python
#!/usr/bin/env python3
|
|
|
|
# Libervia plugin to handle web frontend registration links with Ad-Hoc Commands
|
|
# Copyright (C) 2009-2023 Jérôme Poisson (goffi@goffi.org)
|
|
|
|
# This program is free software: you can redistribute it and/or modify
|
|
# it under the terms of the GNU Affero General Public License as published by
|
|
# the Free Software Foundation, either version 3 of the License, or
|
|
# (at your option) any later version.
|
|
|
|
# This program is distributed in the hope that it will be useful,
|
|
# but WITHOUT ANY WARRANTY; without even the implied warranty of
|
|
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
|
# GNU Affero General Public License for more details.
|
|
|
|
# You should have received a copy of the GNU Affero General Public License
|
|
# along with this program. If not, see <http://www.gnu.org/licenses/>.
|
|
|
|
|
|
import time
|
|
from typing import cast
|
|
from uuid import uuid4
|
|
|
|
from twisted.internet import defer
|
|
|
|
from libervia.backend.core import exceptions
|
|
from libervia.backend.core.constants import Const as C
|
|
from libervia.backend.core.core_types import SatXMPPEntity
|
|
from libervia.backend.core.i18n import D_, _
|
|
from libervia.backend.core.log import getLogger
|
|
from libervia.backend.memory import persistent
|
|
from libervia.backend.plugins.plugin_xep_0004 import DataForm, ListSingle, Option, Text
|
|
from libervia.backend.plugins.plugin_xep_0050 import (
|
|
XEP_0050,
|
|
AdHocCallbackData,
|
|
AdHocCommand,
|
|
AdHocError,
|
|
Error,
|
|
Note,
|
|
PageData,
|
|
Status,
|
|
)
|
|
from libervia.backend.tools.common import data_format, date_utils
|
|
|
|
log = getLogger(__name__)
|
|
|
|
|
|
PLUGIN_INFO = {
|
|
C.PI_NAME: "Ad-Hoc Commands - Registration",
|
|
C.PI_IMPORT_NAME: "AD_HOC_REGISTRATION",
|
|
C.PI_TYPE: "Misc",
|
|
C.PI_PROTOCOLS: [],
|
|
C.PI_DEPENDENCIES: ["XEP-0050"],
|
|
C.PI_MAIN: "AdHocRegistration",
|
|
C.PI_HANDLER: "no",
|
|
C.PI_DESCRIPTION: _("""Add registration link handling Ad-Hoc commands"""),
|
|
}
|
|
|
|
|
|
class AdHocRegistration:
|
|
def __init__(self, host):
|
|
log.info(f"plugin {PLUGIN_INFO[C.PI_NAME]!r} initialization")
|
|
self.host = host
|
|
self._c = cast(XEP_0050, host.plugins["XEP-0050"])
|
|
self.ad_hoc_registration_data = persistent.LazyPersistentBinaryDict(
|
|
"registration_links"
|
|
)
|
|
host.bridge.add_method(
|
|
"registration_link_get",
|
|
".plugin",
|
|
in_sign="s",
|
|
out_sign="s",
|
|
method=self._get,
|
|
async_=True,
|
|
)
|
|
|
|
def _get(self, registration_link_id: str) -> defer.Deferred[str]:
|
|
d = defer.ensureDeferred(self.get(registration_link_id))
|
|
d.addCallback(data_format.serialise)
|
|
return d
|
|
|
|
async def get(self, registration_link_id: str) -> dict:
|
|
"""Retrieve registration link from its ID
|
|
|
|
@param registration_link_id: registration link data
|
|
@return: registration link data
|
|
@raise exceptions.NotFound: not registration link found with this ID.
|
|
"""
|
|
link_data = await self.ad_hoc_registration_data.get(registration_link_id)
|
|
if not link_data:
|
|
raise exceptions.NotFound
|
|
expiration_timestamp = link_data.get("expiration_timestamp")
|
|
if expiration_timestamp is not None and expiration_timestamp < time.time():
|
|
log.info(f"Deleting expiration link {registration_link_id}.")
|
|
await self.ad_hoc_registration_data.adel(registration_link_id)
|
|
raise exceptions.NotFound
|
|
return link_data
|
|
|
|
async def profile_connected(self, client):
|
|
if client.is_admin:
|
|
self._c.register_ad_hoc_command(
|
|
client,
|
|
AdHocCommand(
|
|
callback=self.create_registration_link,
|
|
label=D_("Create Registration Link"),
|
|
node="https://libervia.org/registration/create",
|
|
form=DataForm(
|
|
title=D_("Create Registration Link"),
|
|
fields=[
|
|
Text(var="recipient", label=D_("Recipient Name")),
|
|
Text(
|
|
var="expiration_time_pattern",
|
|
label=D_("Expiration Date"),
|
|
desc=D_(
|
|
"Set the expiry duration for this link. Use the"
|
|
" Libervia Time Pattern (e.g., '1 week'). The link"
|
|
" will expire after this period."
|
|
),
|
|
value="1 week",
|
|
required=True,
|
|
),
|
|
Text(
|
|
var="registration_limit",
|
|
label=D_("Maximum registrations limit"),
|
|
desc=D_(
|
|
"How many accounts can be registered using this link."
|
|
" Set to 0 for unlimited registrations."
|
|
),
|
|
value="1",
|
|
required=True,
|
|
),
|
|
Text(
|
|
type="text-multi",
|
|
var="welcome_message",
|
|
label=D_("Welcome Message"),
|
|
),
|
|
Text(
|
|
type="text-multi",
|
|
var="extra_details",
|
|
label=D_("Additional Details"),
|
|
),
|
|
],
|
|
),
|
|
),
|
|
)
|
|
self._c.register_ad_hoc_command(
|
|
client,
|
|
AdHocCommand(
|
|
callback=self.list_registration_links,
|
|
label=D_("List Registration Links"),
|
|
node="https://libervia.org/registration/list",
|
|
),
|
|
)
|
|
self._c.register_ad_hoc_command(
|
|
client,
|
|
AdHocCommand(
|
|
callback=self.delete_registration_link,
|
|
label=D_("Delete Registration Link"),
|
|
node="https://libervia.org/registration/delete",
|
|
),
|
|
)
|
|
|
|
async def create_registration_link(
|
|
self, client: SatXMPPEntity, page_data: PageData
|
|
) -> AdHocCallbackData:
|
|
"""Ad-hoc command used to create a registration link.
|
|
|
|
This method presents a form to the user for creating a registration link,
|
|
and processes the submitted form to generate and store the link with its
|
|
associated data.
|
|
|
|
@param client: The XMPP client instance.
|
|
@param page_data: Ad-Hoc page data.
|
|
@return: Command results.
|
|
|
|
@raise AdHocError: If the payload is invalid or internal error occurs.
|
|
"""
|
|
form = page_data.form
|
|
try:
|
|
recipient = form.get_field("recipient", Text).value
|
|
expiration_time_pattern = form.get_field(
|
|
"expiration_time_pattern", Text
|
|
).value
|
|
if not expiration_time_pattern:
|
|
raise KeyError
|
|
if expiration_time_pattern == "never":
|
|
expiration_timestamp = None
|
|
else:
|
|
expiration_timestamp = date_utils.date_parse_ext(
|
|
expiration_time_pattern, default_tz=date_utils.TZ_LOCAL
|
|
)
|
|
registration_limit = form.get_field("registration_limit", Text).value
|
|
if registration_limit is None:
|
|
raise KeyError
|
|
registration_limit = int(registration_limit)
|
|
|
|
welcome_message = form.get_field("welcome_message", Text).value or ""
|
|
extra_details = form.get_field("extra_details", Text).value or ""
|
|
|
|
link_id = str(uuid4())
|
|
|
|
link_data = {
|
|
"recipient": recipient,
|
|
"registration_limit": registration_limit,
|
|
"welcome_message": welcome_message,
|
|
"extra_details": extra_details,
|
|
"creator_jid": page_data.requestor.full(),
|
|
"use_count": 0,
|
|
"creation_timestamp": int(time.time()),
|
|
}
|
|
if expiration_timestamp is not None:
|
|
link_data["expiration_timestamp"] = expiration_timestamp
|
|
|
|
await self.ad_hoc_registration_data.aset(link_id, link_data)
|
|
|
|
notes = [
|
|
Note(
|
|
text=D_("Registration link created successfully: {link_id}").format(
|
|
link_id=link_id
|
|
),
|
|
)
|
|
]
|
|
except (KeyError, ValueError):
|
|
raise AdHocError(Error.BAD_PAYLOAD)
|
|
|
|
return AdHocCallbackData(status=Status.COMPLETED, notes=notes)
|
|
|
|
async def list_registration_links(
|
|
self, client: SatXMPPEntity, page_data: PageData
|
|
) -> AdHocCallbackData:
|
|
"""Ad-hoc command used to list all registration links.
|
|
|
|
This method retrieves all the registration links and presents them to the user.
|
|
|
|
@param client: The XMPP client instance.
|
|
@param page_data: Ad-Hoc page data.
|
|
@return: Command results.
|
|
|
|
@raise AdHocError: If internal error occurs.
|
|
"""
|
|
all_links = await self.ad_hoc_registration_data.all()
|
|
|
|
form = DataForm(type="result", title=D_("Registered Links"))
|
|
for link_id, link_data in all_links.items():
|
|
form.fields.append(
|
|
Text(
|
|
type="text-multi",
|
|
var=f"link_{link_id}",
|
|
label=D_("Link ID: {link_id}").format(link_id=link_id),
|
|
value=str(link_data),
|
|
)
|
|
)
|
|
|
|
return AdHocCallbackData(form=form, status=Status.COMPLETED)
|
|
|
|
async def delete_registration_link(
|
|
self, client: SatXMPPEntity, page_data: PageData
|
|
) -> AdHocCallbackData:
|
|
"""Ad-hoc command used to delete a registration link.
|
|
|
|
This method presents a form to the user for selecting a registration link to
|
|
delete, and processes the submitted form to delete the selected link.
|
|
|
|
@param client: The XMPP client instance.
|
|
@param page_data: Ad-Hoc page data.
|
|
@return: Command results.
|
|
|
|
@raise AdHocError: If the payload is invalid or internal error occurs.
|
|
"""
|
|
if page_data.idx == 0:
|
|
all_links = await self.ad_hoc_registration_data.all()
|
|
form = DataForm(
|
|
title=D_("Delete Registration Link"),
|
|
)
|
|
|
|
link_options = [Option(value=link_id, label=link_id) for link_id in all_links]
|
|
form.fields.append(
|
|
ListSingle(
|
|
var="link_to_delete",
|
|
label=D_("Select Link to Delete"),
|
|
options=link_options,
|
|
required=True,
|
|
)
|
|
)
|
|
return AdHocCallbackData(form=form, status=Status.EXECUTING)
|
|
|
|
else:
|
|
link_to_delete = page_data.form.get_field("link_to_delete", ListSingle).value
|
|
if not link_to_delete:
|
|
raise AdHocError(Error.BAD_PAYLOAD)
|
|
await self.ad_hoc_registration_data.adel(link_to_delete)
|
|
return AdHocCallbackData(
|
|
status=Status.COMPLETED,
|
|
notes=[
|
|
Note(
|
|
text=D_(
|
|
"Registration link {link_id} deleted successfully."
|
|
).format(link_id=link_to_delete),
|
|
)
|
|
],
|
|
)
|