Разработка:
- БД скрипт с начальными данными
- вывод списка номеров телефонов по группам
This commit is contained in:
Alexander Zhirov 2023-05-31 02:04:03 +03:00
parent ca5259c638
commit 815b5a6b6a
34 changed files with 1229 additions and 49 deletions

19
.gitignore vendored
View File

@ -1 +1,20 @@
/database/volumes-data /database/volumes-data
.dub
docs.json
__dummy.html
docs/
/web
web.so
web.dylib
web.dll
web.a
web.lib
web-test-*
*.exe
*.o
*.obj
*.lst
bin
web.log
settings.conf
.vscode

33
certs/test.local.crt Normal file
View File

@ -0,0 +1,33 @@
-----BEGIN CERTIFICATE-----
MIIFnjCCA4agAwIBAgIBATANBgkqhkiG9w0BAQsFADCBkjELMAkGA1UEBhMCUlUx
DzANBgNVBAgMBlJ1c3NpYTEWMBQGA1UEBwwNS3JpdmVua292c2tvZTEPMA0GA1UE
CgwGWmhpcm92MRIwEAYDVQQLDAlBbGV4YW5kZXIxDzANBgNVBAMMBnpoaXJvdjEk
MCIGCSqGSIb3DQEJARYVYXpoaXJvdjE5OTFAZ21haWwuY29tMB4XDTIzMDQwNTA4
NTYyN1oXDTI0MDQwNDA4NTYyN1owUTELMAkGA1UEBhMCUlUxDzANBgNVBAgMBlJ1
c3NpYTERMA8GA1UEBwwIQmVsZ29yb2QxETAPBgNVBAoMCE1pcmF0b3JnMQswCQYD
VQQLDAJJVDCCAiIwDQYJKoZIhvcNAQEBBQADggIPADCCAgoCggIBALkJl6TwLPax
A35IWIpq8Z8hPbbK9qTOr5EGwzh1/WCfO5bidhGG/UhZV7JA9Slh+w/PbjMq4qyE
fi654ODmi3dcJgMEoWb3RP+CBn/1A3jRBMBTXGyD7aKNgi7vPuJN51wyVUtQ1P6s
Oc4rF/ob/vArXHS0q9sLJoaOr9DOZxCjycMX/k7GaeoEluf7Z/g1kgOU4wqYxeJk
avinh3NQyuixZ0fiPBi5c9WmXLvzP3cb+BM2aybXZlb4bsRQBWsPy0NDVrApkAk7
guTrXPnwaU0y7XfGMr3nFpJddmGBy3T86k9xZQtdWCoDE4ie/GzDBw1837TPUre/
IV4z1hM7/CO1YWjVE/4JCaG+0TBnDNfiHjzPu0jCpfkX+B8YsTBSRHZd9Geyn4Zh
L/Yz3bPnbvWgs9VS/MU0kgNZ+kCvDanuiJhw12WTtbkx0ZCk1YPP77V9zR0Se5w8
O94vaNpJ4Yav/y7bG8kG4uIJR6r/cQbJYgi3SZN2aEVX7F/bjC29Op5abAr6jz7J
eiu4iKcxXOH9KZQeH+Kwkg2KEGMKbcnaIctjtz3A/f/3MUVVgFnvf9/VB0VOEg3x
sEtHc4WMsWDmYS9Kk25rEfkL3eImZSXRkVCMU4AjhHeN/8eUgJuV9kIE0kpzJgUR
8wxqj02MxOqalDY2dpPR3FPpjb+aydDZAgMBAAGjPzA9MAkGA1UdEwQCMAAwCwYD
VR0PBAQDAgXgMCMGA1UdEQQcMBqCCnRlc3QubG9jYWyCDCoudGVzdC5sb2NhbDAN
BgkqhkiG9w0BAQsFAAOCAgEAVhigTzgvhGE5uyJPaBzAFjNzLqgQuEqKkvPFNvdI
/mXoO1IAtJ6GZS+3MlT3jORj7kric3ipaTd7+zdpMVjoFfSCnFoBcd+NKs0lehIW
rJdvDb9WMP2kQJzTSxMRz0qpYGpEWgmDjxPlq8HaLFTIE9XuWwfIk+noY18Ovhcm
TB1zYA7rwZ26Cf9pfugCw167sRzw4KYy9suX57pNrxhwMSOIawmZeou4bkfMk100
u2wSnFeJZW1hn4Cy4n3HsWiclyAv79SrygRNa4ljKNFnoJ1G9UZf+9dnmn8SDQn2
sMytIf+ouwwYPzCrOjH+9NwC5MqtwSx8EcGc8nF/J6ArWc2fCGNdszaa1NyhP2Lz
SYZg4NdxysywpnwKgby3MmIs7yXxIE8cAaSPLBS2PpqLmohur1eqsMj1XBLk5YpO
LOVR6xzcTqeB8Twt6D/rpw61lBg7lDrmrXD+PMi6yANVS6Yq2wgioT7VjT6jTXi+
SPTKA7Cj1R6UPvwC+XUwIzKXNjhjdTagI7/ePV9Ntc113Wkvb+WECyNPmi2Jbp8e
S19BFiBApvynTEuhq/tuMNSvjiQnHr9Vy7orU+HaGNc4L2KlZ6aE/33XAKgrYyPx
nVNBWX+wciBuiVPmAs2vsc6j3mUreZTvUT5G3UiwafxHbVrHO43gB4p1QuDvEvKp
WwU=
-----END CERTIFICATE-----

51
certs/test.local.key Normal file
View File

@ -0,0 +1,51 @@
-----BEGIN RSA PRIVATE KEY-----
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
-----END RSA PRIVATE KEY-----

View File

@ -9,9 +9,15 @@ services:
POSTGRES_PASSWORD: asterisk POSTGRES_PASSWORD: asterisk
PGTZ: Europe/Moscow PGTZ: Europe/Moscow
TZ: Europe/Moscow TZ: Europe/Moscow
networks:
- db_net
ports: ports:
- 5432:5432 - 5432:5432
volumes: volumes:
- ./volumes-data:/var/lib/postgresql/data - ./volumes-data:/var/lib/postgresql/data
- /etc/timezone:/etc/timezone:ro - /etc/timezone:/etc/timezone:ro
- /etc/localtime:/etc/localtime:ro - /etc/localtime:/etc/localtime:ro
networks:
db_net:
name: db_net

View File

@ -1,62 +1,62 @@
create table if not exists "groups" ( create table if not exists da_groups (
"name" varchar(20) not null, da_name varchar(20) not null,
"comment" varchar(100) default null, da_comment varchar(100) default null,
constraint groups_pk primary key ("name") constraint da_groups_pk primary key (da_name)
); );
insert into "groups" ("name", "comment") insert into da_groups (da_name, da_comment)
values values
('general', 'Общие контакты'), ('general', 'Общие контакты'),
('work', 'Рабочие контакты'), ('work', 'Рабочие контакты'),
('personal', 'Личные контакты'); ('personal', 'Личные контакты');
create table if not exists lists ( create table if not exists da_lists (
"name" varchar(20) not null, da_name varchar(20) not null,
"comment" varchar(100) default null, da_comment varchar(100) default null,
constraint lists_pk primary key ("name") constraint da_lists_pk primary key (da_name)
); );
insert into lists ("name", "comment") insert into da_lists (da_name, da_comment)
values values
('general', 'Общий список'), ('general', 'Общий'),
('whitelist', 'Белый список'), ('whitelist', 'Белый'),
('blacklist', 'Черный список'); ('blacklist', 'Черный');
create table if not exists numbers ( create table if not exists da_numbers (
"number" varchar(12) not null, da_number varchar(12) not null,
"group" varchar(20) not null default 'general', da_group varchar(20) not null default 'general',
list varchar(20) not null default 'general', da_list varchar(20) not null default 'general',
all_cc int not null default 0, da_all_cc int not null default 0,
white_cc int not null default 0, da_white_cc int not null default 0,
black_cc int not null default 0, da_black_cc int not null default 0,
"comment" varchar(100) default null, da_comment varchar(100) default null,
constraint numbers_pk primary key ("number"), constraint da_numbers_pk primary key (da_number),
foreign key ("group") references "groups" ("name") on delete set null on update cascade, foreign key (da_group) references da_groups (da_name) on delete set null on update cascade,
foreign key (list) references lists ("name") on delete set null on update cascade foreign key (da_list) references da_lists (da_name) on delete set null on update cascade
); );
create table if not exists sms ( create table if not exists da_sms (
id bigserial not null, da_id bigserial not null,
"date" timestamp not null default NOW(), da_date timestamp not null default NOW(),
"to" varchar(12) not null, da_to varchar(12) not null,
"from" varchar(12) not null, da_from varchar(12) not null,
"text" text not null, da_text text not null,
constraint sms_pk primary key (id) constraint da_sms_pk primary key (da_id)
); );
create table if not exists ussd ( create table if not exists da_ussd (
id bigserial not null, da_id bigserial not null,
"date" timestamp not null default NOW(), da_date timestamp not null default NOW(),
"to" varchar(12) not null, da_to varchar(12) not null,
"type" smallint not null, da_type smallint not null,
"text" text not null, da_text text not null,
constraint ussd_pk primary key (id) constraint da_ussd_pk primary key (da_id)
); );
create table if not exists "server" ( create table if not exists da_server (
address varchar(50) not null, da_address varchar(50) not null,
transparent_mode bool not null default false, da_transparent_mode bool not null default false,
internal_number varchar(12) not null, da_internal_number varchar(12) not null,
external_number varchar(12) not null, da_external_number varchar(12) not null,
constraint server_pk primary key (address) constraint da_server_pk primary key (da_address)
); );

33
dub.json Normal file
View File

@ -0,0 +1,33 @@
{
"authors": [
"Alexander Zhirov"
],
"copyright": "Copyright © 2023, Alexander Zhirov",
"dependencies": {
"vibe-d": "~>0.9",
"ldap": "~>0.4",
"singlog": "~>0.3.1",
"arsd-official:postgres": "~>10.9.10",
"readconf": "~>0.3.1"
},
"buildTypes": {
"debug": {
"buildOptions": [
"debugMode",
"debugInfo"
]
},
"release": {
"buildOptions": [
"releaseMode",
"inline",
"optimize"
]
}
},
"description": "Dialplan Asterisk - веб-сервер для управления обработкой вызовов Asterisk",
"license": "proprietary",
"name": "daster",
"targetPath": "bin",
"targetType": "executable"
}

22
dub.selections.json Normal file
View File

@ -0,0 +1,22 @@
{
"fileVersion": 1,
"versions": {
"arsd-official": "10.9.10",
"datefmt": "1.0.4",
"diet-ng": "1.8.1",
"eventcore": "0.9.25",
"ldap": "0.4.0",
"libasync": "0.8.6",
"memutils": "1.0.9",
"mir-linux-kernel": "1.0.1",
"openssl": "3.3.0",
"openssl-static": "1.0.2+3.0.8",
"readconf": "0.3.1",
"silly": "1.1.1",
"singlog": "0.3.1",
"stdx-allocator": "2.77.5",
"taggedalgebraic": "0.11.22",
"vibe-core": "2.2.0",
"vibe-d": "0.9.6"
}
}

4
dub.settings.json Normal file
View File

@ -0,0 +1,4 @@
{
"defaultArchitecture": "x86_64",
"defaultCompiler": "ldc2"
}

BIN
images/favicon.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 11 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 7.0 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 7.0 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.6 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 7.0 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.6 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 6.4 KiB

2
jq/jquery-3.7.0.min.js vendored Normal file

File diff suppressed because one or more lines are too long

7
jq/jquery-ui.min.css vendored Normal file

File diff suppressed because one or more lines are too long

6
jq/jquery-ui.min.js vendored Normal file

File diff suppressed because one or more lines are too long

100
js/message.js Normal file
View File

@ -0,0 +1,100 @@
class Message {
timer;
constructor() {
this.div = $('<div id="div-message"></div>');
this.div.css({
"position": "absolute",
"top": "10px",
"right": "20px",
"z-index": "1000"
});
$('body').append(this.div);
}
success(message, delay = 6000) {
this.print(message, delay, '#52b818', '#bffdc0');
}
warning(message, delay = 6000) {
this.print(message, delay, '#b8ae18', '#f8fdbf');
}
error(message, delay = 6000) {
this.print(message, delay, '#b96161', '#fddede');
}
print = function(message, delay, border, background) {
if (delay < 6000) delay = 6000;
let Timer = function(callback, delay) {
let timerId, start, remaining = delay;
this.pause = function() {
clearTimeout(timerId);
remaining -= new Date() - start;
};
this.resume = function() {
start = new Date();
clearTimeout(timerId);
timerId = setTimeout(callback, remaining);
};
this.dead = function() {
clearTimeout(timerId);
};
this.resume();
}
let newMessage = $(`<div class="message">${message}</div>`);
newMessage.css({
"border": `1px solid ${border}`,
"background-color": `${background}`,
"color": "#333",
"padding": "10px 30px",
"text-align": "center",
"display": "none",
"margin": "10px 0 0 0",
"width": "350px",
"opacity": "1",
"cursor": "pointer"
});
newMessage.hover(function(){
$(this).css({
"opacity": "1"
});
}, function(){
$(this).css({
"opacity": "0.3"
});
});
this.div.append(newMessage);
let opacityTimeout = setTimeout(function() {
newMessage.fadeTo(1000, 0.3);
}, 2500);
newMessage.fadeIn(500).mouseenter(() => {
clearTimeout(opacityTimeout);
this.timer.pause();
}).mouseleave(() => {
this.timer.resume();
}).click(() => {
this.timer.dead();
newMessage.fadeOut(0, function(){
this.remove();
});
});
this.timer = new Timer(() => {
newMessage.fadeOut(500, function(){
this.remove();
});
}, delay);
}
}

90
js/script.js Normal file
View File

@ -0,0 +1,90 @@
$(document).ready(function () {
message = new Message;
// (new divNotFoundNumbers).push("Загрузка...");
$("button").button();
$("#tabs").tabs();
// $("#accordion-numbers").accordion();
// $(".addNumber").click(() => {
// numberAdd()
// });
$("body").fadeTo(500, 1);
// getData("Список номеров успешно загружен");
$(".search").on("input", function () {
// showNumbers(numbers.filter(e => e.id.includes($(this).val())))
}).keydown(function (e) {
// e.key == "Escape" && ($(this).val(""), showNumbers())
});
loadData();
})
async function request(query, type, queryData = {}) {
let response = await fetch('.', {
method: 'POST',
headers: {
'Content-Type': 'application/json;charset=utf-8'
},
body: JSON.stringify({
...queryData,
query: query
})
});
if (!response.ok)
throw new Error(`Произошла неизвестаня ошибка: ${response.status}`);
const data = await response[type]();
return data;
}
function isJSON(str) {
try {
return (JSON.parse(str) && !!str);
} catch (e) {
return false;
}
}
function loadData() {
request('listsgroups', 'json').then(data => {
data.error ? message.error(data.message) : generateListsGroups(data);
}).catch(error => {
message.error(error.message);
});
}
function generateListsGroups(data) {
let numbers = $("#tabs-numbers");
let group = $('<div id="accordion-numbers"></div>');
$(data).each((i, j) => {
group.append(`<h3>${j.comment}</h3><div class="group-content" data-group-name="${j.name}"></div>`);
});
numbers.append(group);
$("#accordion-numbers").accordion({
heightStyle: "content",
create: function( event, ui ) {
generateGroupNumbers(ui.panel);
},
beforeActivate: function( event, ui ) {
generateGroupNumbers(ui.newPanel);
}
});
}
async function generateGroupNumbers(panel) {
request('groupnumbers', 'text', { group: panel.data("group-name") }).then(data => {
if (isJSON(data) && JSON.parse(data).error)
message.error(JSON.parse(data).message);
else {
panel.html(data);
}
}).catch(error => {
message.error(error.message);
});
}

BIN
public/Scada-Regular.ttf Normal file

Binary file not shown.

107
public/style.css Normal file
View File

@ -0,0 +1,107 @@
@font-face {
font-family: Scada;
src: url(Scada-Regular.ttf);
}
body {
display: flex;
align-items: center;
height: 100vh;
overflow: hidden;
flex-direction: column;
color: #333;
margin: 0;
opacity: 0;
font-family: Scada;
}
div.div-header {
display: flex;
align-items: center;
width: 60%;
justify-content: center;
margin-top: 30px
}
/* HEADER */
/* div.div-add {
margin-right: 20px
} */
div.div-search {
display: flex;
height: 100%
}
div.div-user {
flex-grow: 1;
text-align: center;
color: #333;
}
input {
text-align: center;
color: #333;
}
.input-focus {
border: 1px solid#c5c5c5;
}
.input-focus:hover {
border: 1px solid #ccc
}
.input-focus::placeholder {
color: #333
}
.input-focus:focus {
outline: none;
box-shadow: 1px 1px 10px 1px #007fff;
border: 1px solid #003eff;
}
/* BODY */
.content {
width: 60%;
margin-top: 20px
}
.content table {
width: 100%;
border-collapse: collapse;
border-spacing: 0;
table-layout: fixed;
}
div.body-rows {
max-height: 55vh;
overflow-x: auto;
border: 1px solid #c5c5c5;
border-top: 0;
}
th {
color: #333;
height: 50px;
background-color: #f6f6f6;
border: 1px solid #c5c5c5;
font-weight: normal;
}
td {
text-align: center;
}
.body-rows tbody tr:nth-child(even){
background: #fff;
}
tr.row:hover, tr.row:nth-child(even):hover {
background-color: #c5c5c5;
color: #fff;
cursor: pointer;
}

18
settings.conf.sample Normal file
View File

@ -0,0 +1,18 @@
[web-host]
title => "Управление диалпланом"
addresses => 127.0.0.1
http => 8080
https => 443
cert => certs/test.local.crt
key => certs/test.local.key
data => ./ ; Путь к каталогу, в котором расположены каталоги public, js, images
loglevel => 0 ; 0 - debug, 1 - crit, 2 - err, 3 - warn, 4 - notice, 5 - info, 6 - alert
logoutput => 1, 4 ; 1 - syslog, 2 - stout, 4 - file => example: 1,2 or 1,2,4
logfile => /var/log/jaster.log ; if log-output set with 4
[daster-db]
host => 127.0.0.1
port => 5432
dbname => daster
user => daster
password => daster

405
source/daster.d Normal file
View File

@ -0,0 +1,405 @@
module daster;
import vibe.vibe;
import singlog;
import readconf;
import core.stdc.stdlib : exit, EXIT_SUCCESS, EXIT_FAILURE;
import std.stdio: writefln;
import std.getopt;
import std.file;
import std.path;
import std.algorithm;
import std.array;
import verinfo;
import pgdb;
import structures;
import requests.listsgroups;
import requests.groupnumbers;
static ServerInfo serverInfo;
private void showVersion() {
writefln("daster версия %s, собрано %s", getDasterVersion(), __DATE__);
}
int main(string[] args) {
log.level(log.INFORMATION);
log.output(log.SYSLOG);
bool flagVersion;
string flagSettings;
try {
auto opt = getopt(
args,
config.bundling,
config.caseSensitive,
"settings|s",
"<файл> Путь к файлу конфигурации settings.conf",
&flagSettings,
"version|v",
"Текущая версия программы",
&flagVersion
);
if (opt.helpWanted) {
showVersion();
defaultGetoptPrinter(
"Использование: daster [ОПЦИИ]...\n",
opt.options
);
return EXIT_SUCCESS;
}
if (flagVersion) {
showVersion();
return EXIT_SUCCESS;
}
} catch (GetOptException e) {
showVersion();
log.c(e.msg);
log.i("Попробуйте 'daster -h' для дополнительной информации");
log.i("https://git.zhirov.kz/alexander/daster");
return EXIT_FAILURE;
}
if (!flagSettings.length)
flagSettings = "./settings.conf";
if (!flagSettings.exists) {
log.c("Файл конфигурации не найден: " ~ flagSettings);
exit(1);
}
rc.read(flagSettings);
rcAsteriskDB();
auto webHost = rcWebHost();
serverInfo = ServerInfo(webHost.title);
if (webHost.loglevel != -1) log.level(webHost.loglevel);
if (webHost.logoutput) log.output(webHost.logoutput);
if (webHost.logfile.length) log.file(webHost.logfile);
auto router = new URLRouter;
router.post("/", &postReq);
router.get("/", &getReq);
router.get("*", serveStaticFiles(buildPath(webHost.data, "public")));
router.get("*", serveStaticFiles(buildPath(webHost.data, "images")));
router.get("*", serveStaticFiles(buildPath(webHost.data, "js")));
router.get("*", serveStaticFiles(buildPath(webHost.data, "jq")));
auto memorySessionStore = new MemorySessionStore;
auto settingsHTTP = new HTTPServerSettings;
auto settingsHTTPS = new HTTPServerSettings;
if (webHost.http) {
settingsHTTP.sessionStore = memorySessionStore;
settingsHTTP.port = webHost.http;
settingsHTTP.bindAddresses = ["::1"] ~ webHost.addresses;
}
if (webHost.https) {
settingsHTTPS.sessionStore = memorySessionStore;
settingsHTTPS.port = webHost.https;
settingsHTTPS.bindAddresses = ["::1"] ~ webHost.addresses;
settingsHTTPS.tlsContext = createTLSContext(TLSContextKind.server);
settingsHTTPS.tlsContext.useCertificateChainFile(webHost.cert);
settingsHTTPS.tlsContext.usePrivateKeyFile(webHost.key);
}
startWebServer(webHost, settingsHTTP, settingsHTTPS, router);
return EXIT_SUCCESS;
}
void startWebServer(WebHost wh, HTTPServerSettings http, HTTPServerSettings https, URLRouter router)
{
if (wh.http && wh.https) {
auto listenerHTTP = listenHTTP(http, router);
auto listenerHTTPS = listenHTTP(https, router);
scope (exit) { listenerHTTP.stopListening(); }
scope (exit) { listenerHTTPS.stopListening(); }
runApplication();
} else if (wh.http) {
auto listenerHTTP = listenHTTP(http, router);
scope (exit) { listenerHTTP.stopListening(); }
runApplication();
} else if (wh.https) {
auto listenerHTTPS = listenHTTP(https, router);
scope (exit) { listenerHTTPS.stopListening(); }
runApplication();
}
}
void getReq(HTTPServerRequest req, HTTPServerResponse res) {
// if (req.session) {
// auto user = req.session.get!UserData("userData");
// if (user.loggedIn) {
// renderMainPage(req, res);
// return;
// }
// }
// render!("index.dt", serverInfo)(res);
renderMainPage(req, res);
}
void renderMainPage(HTTPServerRequest req, HTTPServerResponse res) {
render!("index.dt", serverInfo)(res);
}
void postReq(HTTPServerRequest req, HTTPServerResponse res) {
if (req.method != HTTPMethod.POST) return;
auto jsr = req.json;
string query = jsr["query"].get!string;
if (query.empty) return;
// if (query != "authorization" && !checkAuth(req)) {
// res.send(
// true,
// "Сессия не существует. Перезагрузите страницу"
// );
// return;
// }
switch (query) {
case "listsgroups":
listsgroups(req, res);
break;
case "groupnumbers":
groupnumbers(req, res);
break;
// case "authorization":
// authorization(req, res);
// break;
// case "logout":
// logout(req, res);
// break;
// case "numbers":
// numbers(req, res);
// break;
// case "add":
// addNumber(req, res);
// break;
// case "write":
// writeNumber(req, res);
// break;
// case "edit":
// editNumber(req, res);
// break;
// case "update":
// updateNumber(req, res);
// break;
// case "remove":
// removeNumber(req, res);
// break;
default:
res.redirect("/");
}
}
void rcAsteriskDB() {
ConfigSection asteriskDB;
try {
asteriskDB = rc[]["daster-db"];
} catch (Exception e) {
log.c("В конфигурационном файле не верны настройки daster-db");
exit(1);
}
if (asteriskDB["host"].empty) {
log.c("В конфигурационном файле не верны настройки daster-db.host");
exit(1);
}
if (asteriskDB["port"].empty) {
log.c("В конфигурационном файле не верны настройки daster-db.port");
exit(1);
}
if (asteriskDB["dbname"].empty) {
log.c("В конфигурационном файле не верны настройки daster-db.dbname");
exit(1);
}
if (asteriskDB["user"].empty) {
log.c("В конфигурационном файле не верны настройки daster-db.user");
exit(1);
}
try {
pgsql(
"hostaddr=" ~ asteriskDB["host"] ~
" port=" ~ asteriskDB["port"] ~
" dbname=" ~ asteriskDB["dbname"] ~
" user=" ~ asteriskDB["user"] ~
" password=" ~ asteriskDB["password"]
);
} catch (Exception e) {
log.c(e);
exit(1);
}
}
WebHost rcWebHost() {
WebHost wh;
ConfigSection webHost;
try {
webHost = rc[]["web-host"];
} catch (Exception e) {
log.c("В конфигурационном файле не верны настройки web-host");
exit(1);
}
if (webHost["addresses"].empty) {
log.c("В конфигурационном файле не верны настройки web-host.addresses");
exit(1);
}
try {
wh.addresses = webHost["addresses"].to!string.split(',').map!(a => a.strip).array;
} catch (Exception e) {
log.c("В конфигурационном файле не верны настройки web-host.addresses");
exit(1);
}
if (webHost["http"].empty && webHost["https"].empty) {
log.c("В конфигурационном файле не верны настройки web-host.[http|https]:
должен быть указан хотя бы один протокол");
exit(1);
}
if (!webHost["http"].empty) {
try {
wh.http = webHost["http"].to!ushort;
} catch (Exception e) {
log.c("В конфигурационном файле не верны настройки web-host.http");
exit(1);
}
}
if (!webHost["https"].empty) {
try {
wh.https = webHost["https"].to!ushort;
} catch (Exception e) {
log.c("В конфигурационном файле не верны настройки web-host.https");
exit(1);
}
}
if (!webHost["https"].empty) {
if (webHost["cert"].empty || webHost["key"].empty) {
log.c("В конфигурационном файле не верны настройки web-host.[cert|key]:
необходимо указать сертификат и ключ для использования https протокола"
);
exit(1);
}
}
try {
wh.cert = webHost["cert"];
} catch (Exception e) {
log.c("В конфигурационном файле не верны настройки web-host.cert");
exit(1);
}
try {
wh.key = webHost["key"];
} catch (Exception e) {
log.c("В конфигурационном файле не верны настройки web-host.key");
exit(1);
}
if (!webHost["data"].empty) {
string data;
try {
data = webHost["data"];
} catch (Exception e) {
log.c("В конфигурационном файле не верны настройки web-host.data");
exit(1);
}
if (data.exists) {
if (!buildPath(data, "public").exists) {
log.c("В конфигурационном файле не верны настройки web-host.data - каталог не существует: public");
exit(1);
}
if (!buildPath(data, "js").exists) {
log.c("В конфигурационном файле не верны настройки web-host.data - каталог не существует: js");
exit(1);
}
if (!buildPath(data, "images").exists) {
log.c("В конфигурационном файле не верны настройки web-host.data - каталог не существует: images");
exit(1);
}
} else {
log.c("В конфигурационном файле не верны настройки web-host.data - каталог не существует: " ~ data);
exit(1);
}
wh.data = data;
}
if (!webHost["title"].empty) {
try {
wh.title = webHost["title"];
} catch (Exception e) {
log.w("Заголовок не был установлен - web-host.title");
}
}
if (!webHost["loglevel"].empty) {
try {
wh.loglevel = webHost["loglevel"].to!int;
if (wh.loglevel < 0 || wh.loglevel > 6)
throw new Exception("несуществующий уровень");
} catch (Exception e) {
log.c("В конфигурационном файле не верны настройки web-host.loglevel: " ~ e.msg);
exit(1);
}
}
if (!webHost["logoutput"].empty) {
try {
wh.logoutput = webHost["logoutput"]
.to!string.split(',')
.map!((a) {
auto flag = a.strip.to!int;
if ([1, 2, 4].canFind(flag))
return flag;
log.c("В конфигурационном файле не верны настройки web-host.logoutput");
exit(1);
})
.fold!((a, b) => a | b);
} catch (Exception e) {
log.c("В конфигурационном файле не верны настройки web-host.logoutput");
exit(1);
}
if (wh.logoutput & 4) {
if (webHost["logfile"].empty) {
log.c("В конфигурационном файле не верны настройки web-host.logfile");
exit(1);
}
try {
wh.logfile = webHost["logfile"];
} catch (Exception e) {
log.c("В конфигурационном файле не верны настройки web-host.logfile");
exit(1);
}
}
}
return wh;
}

67
source/data.d Normal file
View File

@ -0,0 +1,67 @@
module data;
import pgdb;
import singlog;
import structures;
import std.conv;
GroupDB[] getListGroups() {
GroupDB[] groups;
try {
auto queryResult = pgsql.sql(
"select distinct
n.da_group,
g.da_comment
from da_numbers n
left join da_groups g ON g.da_name = n.da_group"
);
foreach (row; queryResult) {
GroupDB data;
data.name = row["da_group"];
data.comment = row["da_comment"];
groups ~= data;
}
} catch (Exception e) {
log.e("Не удалось выполнить запрос к БД. " ~ e.msg);
}
return groups;
}
NumberDB[] getListNumbers(string group) {
NumberDB[] numbers;
try {
auto queryResult = pgsql.sql(
"select
dan.da_number,
dal.da_comment da_list,
dan.da_all_cc,
dan.da_white_cc,
dan.da_black_cc,
dan.da_comment
from da_numbers dan
left join da_lists dal on dal.da_name = dan.da_list
where da_group = ?",
group
);
foreach (row; queryResult) {
NumberDB data;
data.number = row["da_number"];
data.list = row["da_list"];
data.all_cc = row["da_all_cc"].to!int;
data.white_cc = row["da_white_cc"].to!int;
data.black_cc = row["da_black_cc"].to!int;
data.comment = row["da_comment"];
numbers ~= data;
}
} catch (Exception e) {
log.e("Не удалось выполнить запрос к БД. " ~ e.msg);
}
return numbers;
}

38
source/pgdb.d Normal file
View File

@ -0,0 +1,38 @@
module pgdb;
import arsd.postgres;
import std.stdio;
alias pgsql = PG.getConnection;
class PG : PostgreSql {
private:
static PG _pgsql;
this(string config) {
super(config);
}
public:
@property static PG getConnection(string config = null) {
if (this._pgsql is null) {
try {
this._pgsql = new PG(config);
} catch (Exception e) {
throw new Exception(e.msg);
}
}
return this._pgsql;
}
PostgresResult sql(T...)(string query, T t) {
return cast(PostgresResult)this.query(query, t);
}
void commit() {
this.query("COMMIT");
}
void rollback() {
this.query("ROLLBACK");
}
}

View File

@ -0,0 +1,12 @@
module requests.groupnumbers;
import vibe.vibe;
import response;
import data;
import singlog;
void groupnumbers(HTTPServerRequest req, HTTPServerResponse res) {
auto jsr = req.json;
auto listNumbers = getListNumbers(jsr["group"].get!string);
render!("group-numbers-list.dt", listNumbers)(res);
}

View File

@ -0,0 +1,11 @@
module requests.listsgroups;
import vibe.vibe;
import response;
import data;
void listsgroups(HTTPServerRequest req, HTTPServerResponse res) {
// auto jsr = req.json;
res.writeJsonBody(getListGroups().serializeToJson());
}

15
source/response.d Normal file
View File

@ -0,0 +1,15 @@
module response;
import vibe.vibe;
struct Res {
bool error;
string message;
string data;
}
void send(HTTPServerResponse res, bool error = false, string message = "", string data = "") {
res.writeJsonBody(Res(
error, message, data
).serializeToJson());
}

33
source/structures.d Normal file
View File

@ -0,0 +1,33 @@
module structures;
struct ServerInfo {
string name;
}
struct WebHost {
string[] addresses;
ushort http = 0;
ushort https = 0;
string cert;
string key;
string data;
string title;
int loglevel = -1;
int logoutput = 0;
string logfile;
}
struct GroupDB {
string name;
string comment;
}
struct NumberDB {
string number;
string group;
string list;
int all_cc;
int white_cc;
int black_cc;
string comment;
}

37
source/verinfo.d Normal file
View File

@ -0,0 +1,37 @@
module verinfo;
import std.algorithm: startsWith;
import version_;
import std.array : split, join;
string getDasterVersion() {
auto verstr = dasterVersion;
if (verstr.startsWith("v"))
verstr = verstr[1 .. $];
auto parts = verstr.split("-");
if (parts.length >= 3) {
if (parts[$-1].length == 8 && parts[$-1][1 .. $].isHexNumber() && parts[$-2].isNumber())
verstr = parts[0 .. $-2].join("-") ~ "+" ~ parts[$-2 .. $].join("-");
}
return verstr;
}
private bool isHexNumber(string str) {
foreach (ch; str)
switch (ch) {
case '0': .. case '9': break;
case 'a': .. case 'f': break;
case 'A': .. case 'F': break;
default: return false;
}
return true;
}
private bool isNumber(string str) {
foreach (ch; str)
switch (ch) {
case '0': .. case '9': break;
default: return false;
}
return true;
}

3
source/version_.d Normal file
View File

@ -0,0 +1,3 @@
module version_;
enum dasterVersion = "v0.0.1";

View File

@ -0,0 +1,22 @@
- import structures;
table
thead.head
tr
th Номер
th Список
th Общие звонки
th Белые звонки
th Черные звонки
th Комментарий
div.body-rows
table
tbody.body
- foreach (number; listNumbers)
tr.row
td #{number.number}
td #{number.list}
td #{number.all_cc}
td #{number.white_cc}
td #{number.black_cc}
td #{number.comment}

39
views/index.dt Normal file
View File

@ -0,0 +1,39 @@
doctype html
head
title #{serverInfo.name}
link(rel='icon', type='image/png', sizes='128x128', href='favicon.png')
link(rel='stylesheet', type='text/css', href='jquery-ui.min.css')
link(rel='stylesheet', type='text/css', href='style.css')
script(src='jquery-3.7.0.min.js')
script(src='jquery-ui.min.js')
script(src='message.js')
script(src='script.js')
body
div.div-header
// div.div-add
// button.addNumber Добавить номер
div.div-search
input.input-focus.search(name='search', type='text', value='', placeholder='Найти номер')
// div.div-user Вы вошли как #{user.name}
div.div-user Вы вошли как Александр
div.div-button
button Выход
// button(onclick='logout()') Выход
div.content
div#tabs
ul
li
a(href='#tabs-numbers') Номера телефонов
li
a(href='#tabs-sms') SMS
li
a(href='#tabs-ussd') USSD
li
a(href='#tabs-server') Сервер
div#tabs-numbers
div#tabs-sms
p Список SMS сообщений
div#tabs-ussd
p Список результатов USSD запросов
div#tabs-server
p Информация о сервере