TT#9402 refresh modules from upstream

* migrate from files to dirs as upstream
* refresh debian/control list of modules
* refresh mod_auth_sql.patch
* update_plugins.sh for automate the process
* keep the list of prosody modules at
  prosody-modules.list file
* keep the upstream mercurial revision at
  prosody-modules.revision file

Change-Id: Id4a0a2c4e88dae64641270ee79c6ee206f5bb695
changes/90/10790/3
Victor Seva 8 years ago
parent 0fbc57ef6e
commit e191475653

15
debian/control vendored

@ -43,12 +43,19 @@ Description: ngcp modules for the prosody Jabber/XMPP server
The following modules are from prosody-modules:
.
* mod_auth_sql.lua: simple SQL Authentication module
* mod_blocking: allows the client to manage a simple
list of blocked JIDs
* mod_carbons: XEP-0280: Message Carbons
* mod_carbons_adhoc
* mod_carbons_copies
* mod_csi: XEP-0352: Client State Indication
* mod_filter_chatstates: will filter Chat State Notifications out
while the session is considered inactive
* mod_limit_auth: lets you put a per-IP limit on the number of
failed authentication attempts
* mod_log_auth: logs IP address in a failed authentication attempt
* mod_websocket.lua: XMPP over websocket
* mod_man: XEP-0313: Message Archive Management
* mod_csi: XEP-0352: Client State Indication
* mod_smacks: XEP-0198: Stream Management
* mod_throttle_presence: cuts down on presence traffic when clients
indicate they are inactive
* mod_filter_chatstates: will filter Chat State Notifications out
while the session is considered inactive
* mod_websocket.lua: XMPP over websocket

@ -0,0 +1 @@
debian/README_mod*

@ -2,8 +2,8 @@ From: Andreas Granig <agranig@sipwise.com>
Date: Wed Aug 7 22:34:34 2013 +0200
Subject: sipwise custom auth
---
--- a/plugins/mod_auth_sql.lua
+++ b/plugins/mod_auth_sql.lua
--- a/plugins/mod_auth_sql/mod_auth_sql.lua
+++ b/plugins/mod_auth_sql/mod_auth_sql.lua
@@ -71,7 +71,7 @@ local function getsql(sql, ...)
end

14
debian/rules vendored

@ -4,3 +4,17 @@
%:
dh $@ --with quilt
override_dh_auto_configure:
while read module ; do \
if [ -f plugins/$$module/README.markdown ] ; then \
cp plugins/$$module/README.markdown debian/README_$$module.markdown; \
fi; \
done < prosody-modules.list; \
override_dh_install:
dh_install
find debian/ngcp-prosody-modules/ -name README.\* -delete
override_dh_auto_clean:
rm -f debian/README_mod_*.*

@ -0,0 +1,40 @@
---
labels:
- 'Type-Auth'
- 'Stage-Stable'
summary: SQL Database authentication module
...
Introduction
============
Allow client authentication to be handled by an SQL database query.
Unlike mod\_storage\_sql (which is supplied with Prosody) this module
allows for custom schemas (though currently it is required to edit the
source).
Configuration
=============
As with all auth modules, there is no need to add this to
modules\_enabled. Simply add in the global section, or for the relevant
hosts:
authentication = "sql"
This module reuses the database configuration of
[mod\_storage\_sql](http://prosody.im/doc/modules/mod_storage_sql) (the
'sql' option), which you can set even if you are not using SQL as
Prosody's primary storage backend.
The query is currently hardcoded in the module, so you will need to edit
the module to change it. The default query is compatible with jabberd2
DB schema.
Compatibility
=============
----- -------
0.8 Works
----- -------

@ -0,0 +1,54 @@
---
labels:
- 'Stage-Alpha'
summary: 'XEP-0191: Simple Communications Blocking support'
...
Introduction
============
Privacy lists are a widely implemented protocol for instructing your
server on blocking communications with selected users and services.
However experience has shown that the power and flexibility of the
rule-based system that privacy lists allow is very often much more
complex than the user needs, and that in most cases a simple block on
all communications to or from a list of specified JIDs would suffice.
Such a protocol would also allow much simpler user interface design than
the current attempts at full privacy list interfaces.
Details
=======
Simple Communications Blocking was developed to solve the above issues,
and allows the client to manage a simple list of blocked JIDs. This
plugin implements support for that protocol in Prosody, however the
actual blocking is still managed by mod\_privacy, so it is **required**
for that plugin to be loaded (this may change in future).
An XEP-0191 implementation without dependency on mod\_privacy is
available in Prosody 0.10 as [mod\_blocklist][doc:modules:mod_blocklist].
Configuration
=============
Simply ensure that mod\_privacy (or [mod\_privacy\_lists] in 0.10+) and
mod\_blocking are loaded in your modules\_enabled list:
modules_enabled = {
-- ...
"privacy", -- or privacy_lists in Prosody 0.10+
"blocking",
-- ...
Compatibility
=============
------ ---------------------------------------------
0.10 Works but will conflict with mod\_blocklist
0.9 Works
0.8 Works
0.7 Works
0.6 Doesn't work
------ ---------------------------------------------

@ -0,0 +1,49 @@
---
labels:
- 'Stage-Beta'
summary: Message Carbons
...
Introduction
============
This module implements [XEP-0280: Message
Carbons](http://xmpp.org/extensions/xep-0280.html), allowing users to
maintain a shared and synchronized view of all conversations across all
their online clients and devices.
Configuration
=============
As with all modules, you enable it by adding it to the modules\_enabled
list.
modules_enabled = {
...
"carbons";
...
}
The module has no further configuration.
Clients
=======
Clients that support XEP-0280:
- [Gajim](http://gajim.org/) (Desktop)
- [Adium (1.6)](http://adium.im/) (Desktop - OS X)
- [Yaxim](http://yaxim.org/) (Mobile - Android)
- [Conversations](https://play.google.com/store/apps/details?id=eu.siacs.conversations)
(Mobile - Android)
- [poezio](http://poezio.eu/en/) (Console)
Compatibility
=============
------- -----------------------
0.8 Works
0.9 Works
0.10 Included with prosody
trunk Included with prosody
------- -----------------------

@ -1,5 +1,5 @@
-- XEP-0280: Message Carbons implementation for Prosody
-- Copyright (C) 2011 Kim Alvefur
-- Copyright (C) 2011-2016 Kim Alvefur
--
-- This file is MIT/X11 licensed.
@ -33,7 +33,7 @@ local function message_handler(event, c2s)
local orig_from = stanza.attr.from;
local orig_to = stanza.attr.to;
if not(orig_type == "chat" or orig_type == "normal" and stanza:get_child("body")) then
if not(orig_type == "chat" or (orig_type == "normal" and stanza:get_child("body"))) then
return -- Only chat type messages
end
@ -74,7 +74,7 @@ local function message_handler(event, c2s)
elseif stanza:get_child("no-copy", "urn:xmpp:hints") then
module:log("debug", "Message has no-copy hint, ignoring");
return
elseif stanza:get_child("x", "http://jabber.org/protocol/muc#user") then
elseif not c2s and bare_jid == orig_from and stanza:get_child("x", "http://jabber.org/protocol/muc#user") then
module:log("debug", "MUC PM, ignoring");
return
end
@ -122,12 +122,12 @@ local function c2s_message_handler(event)
end
-- Stanzas sent by local clients
module:hook("pre-message/host", c2s_message_handler, 1);
module:hook("pre-message/bare", c2s_message_handler, 1);
module:hook("pre-message/full", c2s_message_handler, 1);
module:hook("pre-message/host", c2s_message_handler, 0.05); -- priority between mod_message (0 in 0.9) and mod_firewall (0.1)
module:hook("pre-message/bare", c2s_message_handler, 0.05);
module:hook("pre-message/full", c2s_message_handler, 0.05);
-- Stanzas to local clients
module:hook("message/bare", message_handler, 1);
module:hook("message/full", message_handler, 1);
module:hook("message/bare", message_handler, 0.05);
module:hook("message/full", message_handler, 0.05);
module:add_feature(xmlns_carbons);
module:add_feature(xmlns_carbons_old);

@ -0,0 +1,32 @@
---
summary: Client State Indication support
...
Introduction
============
This module implements [Client State
Indication](http://xmpp.org/extensions/xep-0352.html), a way for mobile
clients to tell the server that they are sitting in someones pocket and
would rather not get some less urgent things pushed to it.
However this module does not do anything by itself. Deciding what things
are considered "less urgent" is left to other modules.
- [mod\_throttle\_presence](/mod_throttle_presence.html) supresses
presence updates
- [mod\_filter\_chatstates](/mod_filter_chatstates.html) removes chat
states (*Someone is typing...*)
Configuration
=============
There is no configuration for this module, just add it to
modules\_enabled as normal.
Compatibility
=============
----- -------
0.9 Works
----- -------

@ -0,0 +1,26 @@
---
summary: Drop chat states from messages to inactive sessions
...
Introduction
============
Some mobile XMPP client developers consider [Chat State
Notifications](http://xmpp.org/extensions/xep-0085.html) to be a waste
of power and bandwidth, especially when the user is not actively looking
at their device. This module will filter them out while the session is
considered inactive. It depends on [mod\_csi](/mod_csi.html) for
deciding when to begin and end filtering.
Configuration
=============
There is no configuration for this module, just add it to
modules\_enabled as normal.
Compatibility
=============
----- -------
0.9 Works
----- -------

@ -0,0 +1,35 @@
---
summary: Throttle authentication attempts with optional tarpit
...
Introduction
============
This module lets you put a per-IP limit on the number of failed
authentication attempts.
It features an optioanal
[tarpit](https://en.wikipedia.org/wiki/Tarpit_%28networking%29), i.e.
waiting some time before returning an "authentication failed" response.
Configuration
=============
``` {.lua}
modules_enabled = {
-- your other modules
"limit_auth";
}
limit_auth_period = 30 -- over 30 seconds
limit_auth_max = 5 -- tolerate no more than 5 failed attempts
-- Will only work with Prosody trunk:
limit_auth_tarpit_delay = 10 -- delay answer this long
```
Compatibility
=============
Requires 0.9 or later. The tarpit feature requires Prosody trunk.

@ -0,0 +1,49 @@
---
labels:
- 'Stage-Stable'
summary: Log failed authentication attempts with their IP address
...
Introduction
============
Prosody doesn't write IP addresses to its log file by default for
privacy reasons (unless debug logging is enabled).
This module enables logging of the IP address in a failed authentication
attempt so that those trying to break into accounts for example can be
blocked.
fail2ban configuration
======================
fail2ban is a utility for monitoring log files and automatically
blocking "bad" IP addresses at the firewall level.
With this module enabled in Prosody you can use the following example
configuration for fail2ban:
# /etc/fail2ban/filter.d/prosody-auth.conf
# Fail2Ban configuration file for prosody authentication
[Definition]
failregex = Failed authentication attempt \(not-authorized\) for user .* from IP: <HOST>
ignoreregex =
And at the appropriate place (usually the bottom) of
/etc/fail2ban/jail.conf add these lines:
[prosody]
enabled = true
port = 5222
filter = prosody-auth
logpath = /var/log/prosody/prosody*.log
maxretry = 6
Compatibility
-------------
------- --------------
trunk Works
0.9 Works
0.8 Doesn't work
------- --------------

@ -3,7 +3,7 @@ assert(({ all = true, failure = true, success = true })[mode], "Unknown log mode
if mode == "failure" or mode == "all" then
module:hook("authentication-failure", function (event)
module:log("info", "Failed authentication attempt (%s) from IP: %s", event.condition or "unknown-condition", event.session.ip or "?");
module:log("info", "Failed authentication attempt (%s) for user %s from IP: %s", event.condition or "unknown-condition", event.session.username or "?", event.session.ip or "?");
end);
end

@ -0,0 +1,128 @@
---
labels:
- 'Stage-Beta'
summary: 'XEP-0313: Message Archive Management'
...
Introduction
============
Implementation of [XEP-0313: Message Archive Management].
Details
=======
This module will archive all messages that match the simple rules setup
by the user, and allow the user to access this archive.
Usage
=====
First copy the module to the prosody plugins directory.
Then add "mam" to your modules\_enabled list:
``` {.lua}
modules_enabled = {
-- ...
"mam",
-- ...
}
```
Configuration
=============
Option summary
--------------
option type default
------------------------------ ----------------------- -----------
max\_archive\_query\_results number `50`
default\_archive\_policy boolean or `"roster"` `true`
archive\_expires\_after string `"1w"`
archive\_cleanup\_interval number `4*60*60`
Storage backend
---------------
mod\_mam uses the store "archive2"[^1]. See [Prosodys data storage
documentation][doc:storage] for information on how to configure storage.
For example, to use mod\_storage\_sql (requires Prosody 0.10 or later):
``` {.lua}
storage = {
archive2 = "sql";
}
```
If no archive-capable storage backend can be opened then an in-memory
one will be used as fallback.
Query size limits
-----------------
max_archive_query_results = 20;
This is the largest number of messages that are allowed to be retrieved
in one request *page*. A query that does not fit in one page will
include a reference to the next page, letting clients page through the
result set. Setting large number is not recomended, as Prosody will be
blocked while processing the request and will not be able to do anything
else.
Archive expiry
--------------
Messages in the archive will expire after some time, by default one
week. This can be changed by setting `archive_expires_after`:
``` {.lua}
archive_expires_after = "1d" -- one day
archive_expires_after = "1w" -- one week, the default
archive_expires_after = "2m" -- two months
archive_expires_after = "1y" -- one year
archive_expires_after = 60 * 60 -- one hour
archive_expires_after = "never" -- forever
```
The format is an integer number of seconds or a multiple of a period
given by a suffix that can be one of `d` (day), `w` (week), `m` (month)
or `y` (year). No multiplier means seconds.
Message matching policy
-----------------------
The MAM protocol includes a way for clients to control what messages
should be stored. This allows users to enable or disable archiving by
default or for specific contacts.
``` {.lua}
default_archive_policy = true
```
`default_archive_policy =` Meaning
---------------------------- ------------------------------------------------------
`false` Store no messages.
`"roster"` Store messages to/from contacts in the users roster.
`true` Store all messages. This is the default.
Compatibility
=============
------- ---------------
trunk Works
0.10 Works
0.9 Works
0.8 Does not work
------- ---------------
[^1]: Might be changed to "mam" at some point

@ -0,0 +1,76 @@
---
labels:
- 'Stage-Alpha'
summary: 'XEP-0198: Reliability and fast reconnects for XMPP'
...
Introduction
============
By default XMPP is as reliable as your network is. Unfortunately in some
cases that is not very reliable - in some network conditions disconnects
can be frequent and message loss can occur.
To overcome this, XMPP has an optional extension (XEP-0198: Stream
Management) which, when supported by both the client and server, can
allow a client to resume a disconnected session, and prevent message
loss.
Details
=======
When using XEP-0198 both the client and the server keep a queue of the
most recently sent stanzas - this is cleared when the other end
acknowledges they have received the stanzas. If the client disconnects,
instead of marking the user offline the server pretends the client is
still online for a short (configurable) period of time. If the client
reconnects within this period, any stanzas in the queue that the client
did not receive are re-sent.
If the client fails to reconnect before the timeout then it is marked
offline as normal, and any stanzas in the queue are returned to the
sender as a "recipient-unavailable" error.
If you don't want this behaviour on timeout you can use [mod_smacks_offline]
or [mod_smacks_noerror] to customize the behaviour further.
This module also provides some events used by [mod_cloud_notify].
These events are: "smacks-ack-delayed", "smacks-hibernation-start" and
"smacks-hibernation-end". See [mod_cloud_notify] for details on how this
events are used there.
Configuration
=============
Option Default Description
------------------------------ ----------------- -----------------------------------------------------------------------------------------
`smacks_hibernation_time` 300 (5 minutes) The number of seconds a disconnected session should stay alive for (to allow reconnect)
`smacks_enabled_s2s` false Enable Stream Management on server connections? *Experimental*
`smacks_max_unacked_stanzas` 0 How many stanzas to send before requesting acknowledgement
`smacks_max_ack_delay` 60 (1 minute) The number of seconds an ack must be unanswered to trigger an "smacks-ack-delayed" event
Compatibility
=============
----- -----------------------------------
0.10 Works
0.9 Works
0.8 Works, use version [7693724881b3]
----- -----------------------------------
Clients
=======
Clients that support XEP-0198:
- Gajim
- Swift (but not resumption, as of version 2.0 and alphas of 3.0)
- Psi (in an unreleased branch)
- Conversations
- Yaxim
[7693724881b3]: //hg.prosody.im/prosody-modules/raw-file/7693724881b3/mod_smacks/mod_smacks.lua
[mod_smacks_offline]: //modules.prosody.im/mod_smacks_offline
[mod_smacks_noerror]: //modules.prosody.im/mod_smacks_noerror
[mod_cloud_notify]: //modules.prosody.im/mod_cloud_notify

@ -5,6 +5,7 @@
-- Copyright (C) 2012-2015 Kim Alvefur
-- Copyright (C) 2012 Thijs Alkemade
-- Copyright (C) 2014 Florian Zeitz
-- Copyright (C) 2016 Thilo Molitor
--
-- This project is MIT/X11 licensed. Please see the
-- COPYING file in the source package for more information.
@ -33,17 +34,26 @@ local resume_timeout = module:get_option_number("smacks_hibernation_time", 300);
local s2s_smacks = module:get_option_boolean("smacks_enabled_s2s", false);
local s2s_resend = module:get_option_boolean("smacks_s2s_resend", false);
local max_unacked_stanzas = module:get_option_number("smacks_max_unacked_stanzas", 0);
local delayed_ack_timeout = module:get_option_number("smacks_max_ack_delay", 60);
local core_process_stanza = prosody.core_process_stanza;
local sessionmanager = require"core.sessionmanager";
local c2s_sessions = module:shared("/*/c2s/sessions");
local session_registry = {};
local function delayed_ack_function(session)
-- fire event only when configured to do so
if delayed_ack_timeout > 0 and session.awaiting_ack and not session.outgoing_stanza_queue == nil then
session.log("debug", "Firing event 'smacks-ack-delayed', queue = %d", #session.outgoing_stanza_queue);
module:fire_event("smacks-ack-delayed", {origin = session, queue = session.outgoing_stanza_queue});
end
end
local function can_do_smacks(session, advertise_only)
if session.smacks then return false, "unexpected-request", "Stream management is already enabled"; end
local session_type = session.type;
if session_type == "c2s" then
if session.username then
if not(advertise_only) and not(session.resource) then -- Fail unless we're only advertising sm
return false, "unexpected-request", "Client must bind a resource before enabling stream management";
end
@ -85,11 +95,22 @@ local function outgoing_stanza_filter(stanza, session)
session.log("debug", "#queue = %d", #queue);
if session.hibernating then
session.log("debug", "hibernating, stanza queued");
return ""; -- Hack to make session.send() not return nil
return nil;
end
if #queue > max_unacked_stanzas and not session.awaiting_ack then
session.awaiting_ack = true;
return tostring(stanza)..tostring(st.stanza("r", { xmlns = session.smacks }));
if #queue > max_unacked_stanzas and session.awaiting_ack == nil then
session.log("debug", "Queuing <r> (in a moment)");
session.awaiting_ack = false;
session.awaiting_ack_timer = module:add_timer(1e-06, function ()
if not session.awaiting_ack then
session.log("debug", "Sending <r> (before send)");
(session.sends2s or session.send)(st.stanza("r", { xmlns = session.smacks }))
session.log("debug", "Sending <r> (after send)");
session.awaiting_ack = true;
session.delayed_ack_timer = module:add_timer(delayed_ack_timeout, function()
delayed_ack_function(session);
end);
end
end);
end
end
return stanza;
@ -109,7 +130,7 @@ local function wrap_session_out(session, resume)
session.last_acknowledged_stanza = 0;
end
add_filter(session, "stanzas/out", outgoing_stanza_filter, -1000);
add_filter(session, "stanzas/out", outgoing_stanza_filter, -999);
local session_close = session.close;
function session.close(...)
@ -126,7 +147,7 @@ local function wrap_session_in(session, resume)
if not resume then
session.handled_stanza_count = 0;
end
add_filter(session, "stanzas/in", count_incoming_stanzas, 1000);
add_filter(session, "stanzas/in", count_incoming_stanzas, 999);
return session;
end
@ -165,7 +186,7 @@ module:hook_stanza(xmlns_sm3, "enable", function (session, stanza) return handle
module:hook_stanza("http://etherx.jabber.org/streams", "features",
function (session, stanza)
module:add_timer(0, function ()
module:add_timer(1e-6, function ()
if can_do_smacks(session) then
if stanza:get_child("sm", xmlns_sm3) then
session.sends2s(st.stanza("enable", sm3_attr));
@ -210,6 +231,12 @@ module:hook_stanza(xmlns_sm3, "r", function (origin, stanza) return handle_r(ori
function handle_a(origin, stanza)
if not origin.smacks then return; end
origin.awaiting_ack = nil;
if origin.awaiting_ack_timer then
origin.awaiting_ack_timer:stop();
end
if origin.delayed_ack_timer then
origin.delayed_ack_timer:stop();
end
-- Remove handled stanzas from outgoing_stanza_queue
--log("debug", "ACK: h=%s, last=%s", stanza.attr.h or "", origin.last_acknowledged_stanza or "");
local h = tonumber(stanza.attr.h);
@ -272,6 +299,7 @@ module:hook("pre-resource-unbind", function (event)
local hibernate_time = os_time(); -- Track the time we went into hibernation
session.hibernating = hibernate_time;
local resumption_token = session.resumption_token;
module:fire_event("smacks-hibernation-start", {origin = session, queue = session.outgoing_stanza_queue});
timer.add_task(resume_timeout, function ()
session.log("debug", "mod_smacks hibernation timeout reached...");
-- We need to check the current resumption token for this resource
@ -369,6 +397,7 @@ function handle_resume(session, stanza, xmlns_sm)
-- Ok, we need to re-send any stanzas that the client didn't see
-- ...they are what is now left in the outgoing stanza queue
local queue = original_session.outgoing_stanza_queue;
module:fire_event("smacks-hibernation-end", {origin = session, queue = queue});
session.log("debug", "#queue = %d", #queue);
for i=1,#queue do
session.send(queue[i]);
@ -390,10 +419,21 @@ local function handle_read_timeout(event)
local session = event.session;
if session.smacks then
if session.awaiting_ack then
if session.awaiting_ack_timer then
session.awaiting_ack_timer:stop();
end
if session.delayed_ack_timer then
session.delayed_ack_timer:stop();
end
return false; -- Kick the session
end
session.log("debug", "Sending <r> (read timeout)");
session.awaiting_ack = false;
(session.sends2s or session.send)(st.stanza("r", { xmlns = session.smacks }));
session.awaiting_ack = true;
session.delayed_ack_timer = module:add_timer(delayed_ack_timeout, function()
delayed_ack_function(session);
end);
return true;
end
end

@ -0,0 +1,31 @@
---
labels:
- 'Stage-Beta'
summary: Limit presence stanzas to save traffic
...
Introduction
============
For most people 'presence' (status changes) of contacts make up most of
the traffic received by their client. However much of the time it is not
essential to have highly accurate presence information.
This module automatically cuts down on presence traffic when clients
indicate they are inactive (using the [CSI protocol](mod_csi.html)).
This is extremely valuable for mobile clients that wish to save battery
power while in the background.
Configuration
=============
Just load the module (e.g. in modules\_enabled). There are no
configuration options.
Compatibility
=============
----- -------
0.9 Works
----- -------

@ -0,0 +1,13 @@
mod_auth_sql
mod_blocking
mod_carbons
mod_carbons_adhoc
mod_carbons_copies
mod_csi
mod_filter_chatstates
mod_limit_auth
mod_log_auth
mod_mam
mod_smacks
mod_throttle_presence
mod_websocket

@ -0,0 +1 @@
51cf82d36a8a

@ -0,0 +1,44 @@
#!/bin/bash
#
set -e
MOD_SRC=$1
usage() {
echo "$0 modules_src_dir"
printf "\tmodules_src_dir:\tpath to prosody-modules mercurial source\n"
}
refresh_sources() {
echo "refresh sources at $MOD_SRC"
( cd "$MOD_SRC" && hg pull && hg update )
}
copy_modules() {
while read -r dir ; do
if [ -d "$MOD_SRC/$dir" ] ; then
cp -vr "$MOD_SRC/$dir" plugins
else
echo "$MOD_SRC/$dir no longer there"
fi
done < prosody-modules.list
}
get_revision_id() {
(cd "$MOD_SRC/$dir" && hg id -i) > 'prosody-modules.revision'
}
if [ $# -ne 1 ] ; then
echo "wrong number of parameters" >&2
usage
exit 1
fi
if ! [ -d "$MOD_SRC" ] ; then
printf "modules_src_dir[%s] not found\n" "$MOD_SRC"
fi
refresh_sources
copy_modules
get_revision_id
exit 0
Loading…
Cancel
Save