Add some basic subscriptions statistics

Move the statistics to a specific Library
Show some subscriptions charts in the admin panel
Inject the browser user agent to the database if available
Split statistics in different view (day/week/month)
Install cron scripts
Update the dependencies
This commit is contained in:
Timothée Jaussoin 2021-09-23 17:25:09 +02:00
parent e832eae21e
commit 4f11deeaf9
26 changed files with 601 additions and 7 deletions

View file

@ -28,6 +28,7 @@ package-common:
# General
cp README.md $(OUTPUT_DIR)/flexisip-account-manager/
cp -R httpd/ $(OUTPUT_DIR)/flexisip-account-manager/
cp -R cron/ $(OUTPUT_DIR)/flexisip-account-manager/
cp flexisip-account-manager.spec $(OUTPUT_DIR)/rpmbuild/SPECS/
tar cvf flexisip-account-manager.tar.gz -C $(OUTPUT_DIR) flexisip-account-manager

6
cron/flexiapi.debian Normal file
View file

@ -0,0 +1,6 @@
#!/bin/sh
cd /opt/belledonne-communications/share/flexisip-account-manager/flexiapi/
sudo -su www-data && php artisan digest:expired-nonces-clear 60
sudo -su www-data && php artisan accounts:clear-accounts-tombstones 7 --apply
sudo -su www-data && php artisan accounts:clear-unconfirmed 30 --apply

6
cron/flexiapi.redhat Normal file
View file

@ -0,0 +1,6 @@
#!/bin/sh
cd /opt/belledonne-communications/share/flexisip-account-manager/flexiapi/
scl enable rh-php73 "php artisan digest:expired-nonces-clear 60"
scl enable rh-php73 "php artisan accounts:clear-accounts-tombstones 7 --apply"
scl enable rh-php73 "php artisan accounts:clear-unconfirmed 30 --apply"

View file

@ -58,6 +58,12 @@ class Utils
return mt_rand(1000, 9999);
}
public static function percent($value, $max)
{
if ($max == 0) $max = 1;
return round(($value*100)/$max, 2);
}
public static function markdownDocumentationView($view)
{
$environment = Environment::createCommonMarkEnvironment();

View file

@ -91,7 +91,7 @@ class RegisterController extends Controller
$account->domain = config('app.sip_domain');
$account->ip_address = $request->ip();
$account->creation_time = Carbon::now();
$account->user_agent = config('app.name');
$account->user_agent = $request->header('User-Agent') ?? config('app.name');
$account->save();
$account->confirmation_key = Str::random($this->emailCodeSize);
@ -143,7 +143,7 @@ class RegisterController extends Controller
$account->domain = config('app.sip_domain');
$account->ip_address = $request->ip();
$account->creation_time = Carbon::now();
$account->user_agent = config('app.name');
$account->user_agent = $request->header('User-Agent') ?? config('app.name');
$account->save();
$alias = new Alias;

View file

@ -0,0 +1,53 @@
<?php
namespace App\Http\Controllers\Admin;
use App\Http\Controllers\Controller;
use Illuminate\Http\Request;
use App\Libraries\StatisticsCruncher;
class StatisticsController extends Controller
{
public function showDay(Request $request)
{
$day = StatisticsCruncher::day();
$maxDay = 0;
foreach ($day as $hour) {
if ($maxDay < $hour['all']) $maxDay = $hour['all'];
}
return view('admin.statistics.show_day', [
'day' => $day,
'max_day' => $maxDay,
]);
}
public function showWeek(Request $request)
{
$week = StatisticsCruncher::week();
$maxWeek = 0;
foreach ($week as $day) {
if ($maxWeek < $day['all']) $maxWeek = $day['all'];
}
return view('admin.statistics.show_week', [
'week' => $week,
'max_week' => $maxWeek,
]);
}
public function showMonth(Request $request)
{
$month = StatisticsCruncher::month();
$maxMonth = 0;
foreach ($month as $day) {
if ($maxMonth < $day['all']) $maxMonth = $day['all'];
}
return view('admin.statistics.show_month', [
'month' => $month,
'max_month' => $maxMonth,
]);
}
}

View file

@ -124,7 +124,7 @@ class AccountController extends Controller
$account->domain = $request->has('domain') && config('app.everyone_is_admin')
? $request->get('domain')
: config('app.sip_domain');
$account->user_agent = config('app.name');
$account->user_agent = $request->header('User-Agent') ?? config('app.name');
if (!$request->has('activated') || !(bool)$request->get('activated')) {
$account->confirmation_key = Str::random(WebAuthenticateController::$emailCodeSize);

View file

@ -0,0 +1,43 @@
<?php
/*
Flexisip Account Manager is a set of tools to manage SIP accounts.
Copyright (C) 2021 Belledonne Communications SARL, All rights reserved.
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/>.
*/
namespace App\Http\Controllers\Api;
use App\Http\Controllers\Controller;
use Illuminate\Http\Request;
use App\Libraries\StatisticsCruncher;
class StatisticController extends Controller
{
public function month(Request $request)
{
return StatisticsCruncher::month();
}
public function week(Request $request)
{
return StatisticsCruncher::week();
}
public function day(Request $request)
{
return StatisticsCruncher::day();
}
}

View file

@ -0,0 +1,197 @@
<?php
/*
Flexisip Account Manager is a set of tools to manage SIP accounts.
Copyright (C) 2020 Belledonne Communications SARL, All rights reserved.
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/>.
*/
namespace App\Libraries;
use Illuminate\Support\Facades\DB;
use Carbon\Carbon;
use Carbon\CarbonPeriod;
use Carbon\CarbonInterval;
use App\Account;
class StatisticsCruncher
{
public static function month()
{
$data = self::getAccountFrom(Carbon::now()->subMonth())
->get(array(
DB::raw("date_format(creation_time,'%Y-%m-%d') as moment"),
DB::raw('COUNT(*) as "count"')
))->each->setAppends([])->pluck('count', 'moment');
$dataAliases = self::getAccountFrom(Carbon::now()->subMonth())
->whereIn('id', function($query) {
$query->select('account_id')
->from('aliases');
})
->get(array(
DB::raw("date_format(creation_time,'%Y-%m-%d') as moment"),
DB::raw('COUNT(*) as "count"')
))->each->setAppends([])->pluck('count', 'moment');
$dataActivated = self::getAccountFrom(Carbon::now()->subMonth())
->where('activated', true)
->get(array(
DB::raw("date_format(creation_time,'%Y-%m-%d') as moment"),
DB::raw('COUNT(*) as "count"')
))->each->setAppends([])->pluck('count', 'moment');
$dataAliasesActivated = self::getAccountFrom(Carbon::now()->subMonth())
->where('activated', true)
->whereIn('id', function($query) {
$query->select('account_id')
->from('aliases');
})
->get(array(
DB::raw("date_format(creation_time,'%Y-%m-%d') as moment"),
DB::raw('COUNT(*) as "count"')
))->each->setAppends([])->pluck('count', 'moment');
return self::compileStatistics(
collect(CarbonPeriod::create(Carbon::now()->subMonth(), Carbon::now()))->map->format('Y-m-d'),
$data,
$dataAliases,
$dataActivated,
$dataAliasesActivated
);
}
public static function week()
{
$data = self::getAccountFrom(Carbon::now()->subWeek())
->get(array(
DB::raw("date_format(creation_time,'%Y-%m-%d') as moment"),
DB::raw('COUNT(*) as "count"')
))->each->setAppends([])->pluck('count', 'moment');
$dataAliases = self::getAccountFrom(Carbon::now()->subWeek())
->whereIn('id', function($query) {
$query->select('account_id')
->from('aliases');
})
->get(array(
DB::raw("date_format(creation_time,'%Y-%m-%d') as moment"),
DB::raw('COUNT(*) as "count"')
))->each->setAppends([])->pluck('count', 'moment');
$dataActivated = self::getAccountFrom(Carbon::now()->subWeek())
->where('activated', true)
->get(array(
DB::raw("date_format(creation_time,'%Y-%m-%d') as moment"),
DB::raw('COUNT(*) as "count"')
))->each->setAppends([])->pluck('count', 'moment');
$dataAliasesActivated = self::getAccountFrom(Carbon::now()->subWeek())
->where('activated', true)
->whereIn('id', function($query) {
$query->select('account_id')
->from('aliases');
})
->get(array(
DB::raw("date_format(creation_time,'%Y-%m-%d') as moment"),
DB::raw('COUNT(*) as "count"')
))->each->setAppends([])->pluck('count', 'moment');
return self::compileStatistics(
collect(CarbonPeriod::create(Carbon::now()->subWeek(), Carbon::now()))->map->format('Y-m-d'),
$data,
$dataAliases,
$dataActivated,
$dataAliasesActivated
);
}
public static function day()
{
$data = self::getAccountFrom(Carbon::now()->subDay())
->get(array(
DB::raw("date_format(creation_time,'%Y-%m-%d %H') as moment"),
DB::raw('COUNT(*) as "count"')
))->each->setAppends([])->pluck('count', 'moment');
$dataAliases = self::getAccountFrom(Carbon::now()->subDay())
->whereIn('id', function($query) {
$query->select('account_id')
->from('aliases');
})
->get(array(
DB::raw("date_format(creation_time,'%Y-%m-%d %H') as moment"),
DB::raw('COUNT(*) as "count"')
))->each->setAppends([])->pluck('count', 'moment');
$dataActivated = self::getAccountFrom(Carbon::now()->subDay())
->where('activated', true)
->get(array(
DB::raw("date_format(creation_time,'%Y-%m-%d %H') as moment"),
DB::raw('COUNT(*) as "count"')
))->each->setAppends([])->pluck('count', 'moment');
$dataAliasesActivated = self::getAccountFrom(Carbon::now()->subDay())
->where('activated', true)
->whereIn('id', function($query) {
$query->select('account_id')
->from('aliases');
})
->get(array(
DB::raw("date_format(creation_time,'%Y-%m-%d %H') as moment"),
DB::raw('COUNT(*) as "count"')
))->each->setAppends([])->pluck('count', 'moment');
return self::compileStatistics(
collect(CarbonInterval::hour()->toPeriod(Carbon::now()->subDay(), Carbon::now()))->map->format('Y-m-d H'),
$data,
$dataAliases,
$dataActivated,
$dataAliasesActivated
);
}
private static function getAccountFrom($date)
{
return Account::where('creation_time', '>=', $date)
->groupBy('moment')
->orderBy('moment', 'DESC')
->setEagerLoads([]);
}
private static function compileStatistics($period, $data, $dataAliases, $dataActivated, $dataAliasesActivated)
{
$stats = [];
foreach ($period as $moment) {
$all = $data[$moment] ?? 0;
$aliases = $dataAliases[$moment] ?? 0;
$activated = $dataActivated[$moment] ?? 0;
$activatedAliases = $dataAliasesActivated[$moment] ?? 0;
$stats[$moment] = [
'all' => $all,
'phone' => $aliases,
'email' => $all - $aliases,
'activated_phone' => $activatedAliases,
'activated_email' => $activated - $activatedAliases
];
}
return $stats;
}
}

Binary file not shown.

View file

@ -257,9 +257,9 @@ return [
'Storage' => Illuminate\Support\Facades\Storage::class,
'Str' => Illuminate\Support\Str::class,
'URL' => Illuminate\Support\Facades\URL::class,
'Utils' => App\Helpers\Utils::class,
'Validator' => Illuminate\Support\Facades\Validator::class,
'View' => Illuminate\Support\Facades\View::class,
],
];

View file

@ -1,4 +1,21 @@
<?php
/*
Flexisip Account Manager is a set of tools to manage SIP accounts.
Copyright (C) 2020 Belledonne Communications SARL, All rights reserved.
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/>.
*/
use Illuminate\Database\Migrations\Migration;
use Illuminate\Database\Schema\Blueprint;

View file

@ -1,4 +1,21 @@
<?php
/*
Flexisip Account Manager is a set of tools to manage SIP accounts.
Copyright (C) 2020 Belledonne Communications SARL, All rights reserved.
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/>.
*/
use Illuminate\Database\Migrations\Migration;
use Illuminate\Database\Schema\Blueprint;

77
flexiapi/public/css/charts.css vendored Normal file
View file

@ -0,0 +1,77 @@
.legend {
margin-bottom: 1rem;
}
.legend div:before {
content: '';
display: inline-block;
width: 1rem;
height: 1rem;
vertical-align: middle;
background-color: gray;
margin: 0 0.5rem;
opacity: 0.5;
}
.columns {
display: flex;
height: 600px;
align-items: stretch;
border: 1px solid #CCC;
margin-bottom: 10rem;
}
.columns .column {
flex: 1;
display: flex;
flex-direction: column;
align-items: stretch;
justify-content: flex-end;
position: relative;
}
.columns .column .bar {
border-right: 1px solid white;
font-size: 0.5rem;
text-align: center;
position: relative;
opacity: 0.5;
}
.columns .column::after {
display: block;
content: attr(data-value);
position: absolute;
top: calc(100% + 1rem);
writing-mode: vertical-rl;
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
}
.columns .column .bar.activated,
.legend div.activated:before {
opacity: 1;
}
.columns .column .bar.first,
.legend .first:before {
background-color: salmon;
}
.columns .column .bar.second,
.legend .second:before {
background-color: seagreen;
}
.columns .column .bar::after {
display: block;
content: attr(data-value);
color: white;
position: absolute;
font-size: 0.75rem;
line-height: 1rem;
left: 0rem;
top: calc(50% - 0.5rem);
width: 100%;
}

View file

@ -84,6 +84,14 @@ Finally the account page allows you to provision the account, using a QR Code or
The provisioning link can be generated and refreshed from this page as well.
### Create and edit an account
Administrators can create and edit accounts directly from the admin panel.
### Delete an account
The deletion of an account is definitive, all the database related data (password, aliases…) will be destroyed after the deletion.
The deletion of an account is definitive, all the database related data (password, aliases…) will be destroyed after the deletion.
## Statistics
The statistics panel show registrations statistics based on their type (mobile and email based registration) and their activations states.

View file

@ -54,6 +54,12 @@
</div>
<p class="mb-1">Manage the Flexisip accounts</p>
</a>
<a href="{{ route('admin.statistics.show.day') }}" class="list-group-item list-group-item-action">
<div class="d-flex w-100 justify-content-between">
<h5 class="mb-1">Statistics</h5>
</div>
<p class="mb-1">Show some registration statistics</p>
</a>
</div>
<h5>API Key</h5>

View file

@ -0,0 +1,12 @@
<div class="bar first" style="flex-basis: {{ Utils::percent($slice['phone'] - $slice['activated_phone'], $max) }}%"
data-value="{{ $slice['phone'] - $slice['activated_phone'] }}"
title="Unactivated phone: {{ $slice['phone'] - $slice['activated_phone'] }}"></div>
<div class="bar first activated" style="flex-basis: {{ Utils::percent($slice['activated_phone'], $max) }}%"
data-value="{{ $slice['activated_phone'] }}"
title="Activated phone: {{ $slice['activated_phone'] }}"></div>
<div class="bar second" style="flex-basis: {{ Utils::percent($slice['email'] - $slice['activated_email'], $max) }}%"
data-value="{{ $slice['email'] - $slice['activated_email'] }}"
title="Unactivated email: {{ $slice['email'] - $slice['activated_email'] }}"></div>
<div class="bar second activated" style="flex-basis: {{ Utils::percent($slice['activated_email'], $max) }}%"
data-value="{{ $slice['activated_email'] }}"
title="Activated email: {{ $slice['activated_email'] }}"></div>

View file

@ -0,0 +1,6 @@
<div class="legend">
<div class="first">Unactivated phones</div>
<div class="first activated">Activated phones</div>
<div class="second">Unactivated emails</div>
<div class="second activated">Activated emails</div>
</div>

View file

@ -0,0 +1,37 @@
@extends('layouts.account')
@section('breadcrumb')
<li class="breadcrumb-item active" aria-current="page">
Statistics
</li>
@endsection
@section('content')
<ul class="nav justify-content-center">
<li class="nav-item">
<a class="nav-link disabled" href="#">Day</a>
</li>
<li class="nav-item">
<a class="nav-link" href="{{ route('admin.statistics.show.week') }}">Week</a>
</li>
<li class="nav-item">
<a class="nav-link" href="{{ route('admin.statistics.show.month') }}">Month</a>
</li>
</ul>
<h2>Statistics</h2>
@include('admin.statistics.parts.legend')
<h3>Day</h3>
<div class="columns">
@foreach ($day as $key => $hour)
<div class="column" data-value="{{ substr($key, -2, 2) }}:00">
@include('admin.statistics.parts.columns', ['slice' => $hour, 'max' => $max_day])
</div>
@endforeach
</div>
@endsection

View file

@ -0,0 +1,37 @@
@extends('layouts.account')
@section('breadcrumb')
<li class="breadcrumb-item active" aria-current="page">
Statistics
</li>
@endsection
@section('content')
<ul class="nav justify-content-center">
<li class="nav-item">
<a class="nav-link" href="{{ route('admin.statistics.show.day') }}">Day</a>
</li>
<li class="nav-item">
<a class="nav-link" href="{{ route('admin.statistics.show.week') }}">Week</a>
</li>
<li class="nav-item">
<a class="nav-link disabled" href="#">Month</a>
</li>
</ul>
<h2>Statistics</h2>
@include('admin.statistics.parts.legend')
<h3>Month</h3>
<div class="columns">
@foreach ($month as $key => $day)
<div class="column" data-value="{{ $key }}">
@include('admin.statistics.parts.columns', ['slice' => $day, 'max' => $max_month])
</div>
@endforeach
</div>
@endsection

View file

@ -0,0 +1,37 @@
@extends('layouts.account')
@section('breadcrumb')
<li class="breadcrumb-item active" aria-current="page">
Statistics
</li>
@endsection
@section('content')
<ul class="nav justify-content-center">
<li class="nav-item">
<a class="nav-link" href="{{ route('admin.statistics.show.day') }}">Day</a>
</li>
<li class="nav-item">
<a class="nav-link disabled" href="#">Week</a>
</li>
<li class="nav-item">
<a class="nav-link" href="{{ route('admin.statistics.show.month') }}">Month</a>
</li>
</ul>
<h2>Statistics</h2>
@include('admin.statistics.parts.legend')
<h3>Week</h3>
<div class="columns">
@foreach ($week as $key => $day)
<div class="column" data-value="{{ $key }}">
@include('admin.statistics.parts.columns', ['slice' => $day, 'max' => $max_week])
</div>
@endforeach
</div>
@endsection

View file

@ -186,6 +186,17 @@ Activate an account.
#### `GET /accounts/{id}/deactivate`
Deactivate an account.
### Statistics
#### `GET /statistics/day`
Retrieve registrations statistics for 24 hours.
#### `GET /statistics/week`
Retrieve registrations statistics for a week.
#### `GET /statistics/month`
Retrieve registrations statistics for a month.
# Provisioning
When an account is having an available `confirmation_key` it can be provisioned using the two following URL.

View file

@ -13,6 +13,7 @@
@else
<link rel="stylesheet" type="text/css" href="{{ asset('css/style.css') }}" >
@endif
<link rel="stylesheet" type="text/css" href="{{ asset('css/charts.css') }}" >
@endif
</head>
<body>

View file

@ -33,6 +33,10 @@ Route::post('accounts/{sip}/activate/email', 'Api\AccountController@activateEmai
Route::post('accounts/{sip}/activate/phone', 'Api\AccountController@activatePhone');
Route::group(['middleware' => ['auth.digest_or_key']], function () {
Route::get('statistic/month', 'Api\StatisticController@month');
Route::get('statistic/week', 'Api\StatisticController@week');
Route::get('statistic/day', 'Api\StatisticController@day');
Route::get('accounts/me', 'Api\AccountController@show');
Route::delete('accounts/me', 'Api\AccountController@delete');

View file

@ -71,6 +71,10 @@ Route::group(['middleware' => 'auth'], function () {
Route::group(['middleware' => 'auth.admin'], function () {
Route::post('admin/api_key', 'Admin\AccountController@generateApiKey')->name('admin.api_key.generate');
Route::get('admin/statistics/day', 'Admin\StatisticsController@showDay')->name('admin.statistics.show.day');
Route::get('admin/statistics/week', 'Admin\StatisticsController@showWeek')->name('admin.statistics.show.week');
Route::get('admin/statistics/month', 'Admin\StatisticsController@showMonth')->name('admin.statistics.show.month');
Route::get('admin/accounts/{account}/show', 'Admin\AccountController@show')->name('admin.account.show');
Route::get('admin/accounts/{account}/activate', 'Admin\AccountController@activate')->name('admin.account.activate');

View file

@ -8,7 +8,7 @@
#%define _datadir %{_datarootdir}
#%define _docdir %{_datadir}/doc
%define build_number 105
%define build_number 106
%define var_dir /var/opt/belledonne-communications
%define opt_dir /opt/belledonne-communications/share/flexisip-account-manager
@ -64,12 +64,16 @@ cp README* "$RPM_BUILD_ROOT%{opt_dir}/"
mkdir -p "$RPM_BUILD_ROOT/etc/flexisip-account-manager"
cp -R conf/* "$RPM_BUILD_ROOT/etc/flexisip-account-manager/"
mkdir -p $RPM_BUILD_ROOT/etc/cron.daily
%if %{with deb}
mkdir -p $RPM_BUILD_ROOT/etc/apache2/conf-available
cp httpd/flexisip-account-manager.conf "$RPM_BUILD_ROOT/etc/apache2/conf-available/"
cp cron/flexiapi.debian "$RPM_BUILD_ROOT/etc/cron.daily/"
%else
mkdir -p $RPM_BUILD_ROOT/opt/rh/httpd24/root/etc/httpd/conf.d
cp httpd/flexisip-account-manager.conf "$RPM_BUILD_ROOT/opt/rh/httpd24/root/etc/httpd/conf.d/"
cp cron/flexiapi.redhat "$RPM_BUILD_ROOT/etc/cron.daily/"
%endif
# POST INSTALLATION
@ -168,15 +172,19 @@ fi
%if %{with deb}
%config(noreplace) /etc/apache2/conf-available/flexisip-account-manager.conf
%config(noreplace) /etc/cron.daily/flexiapi.debian
%else
%config(noreplace) /opt/rh/httpd24/root/etc/httpd/conf.d/flexisip-account-manager.conf
%config(noreplace) /etc/cron.daily/flexiapi.redhat
%endif
%clean
rm -rf $RPM_BUILD_ROOT
%changelog
* Tue Jan 5 2020 Timothée Jaussoin <timothee.jaussoin@belledonne-communications.com>
* Tue Sep 28 2021 Timothée Jaussoin <timothee.jaussoin@belledonne-communications.com>
- Install cron scripts
* Sun Jan 5 2020 Timothée Jaussoin <timothee.jaussoin@belledonne-communications.com>
- Import and configure the new API package
* Thu Jul 4 2019 Sylvain Berfini <sylvain.berfini@belledonne-communications.com>
- New files layout