mirror of
https://github.com/chuot/rdio-scanner.git
synced 2026-08-13 16:33:01 -06:00
Version 3.1
This commit is contained in:
parent
0b653aaba8
commit
db7b464b99
|
|
@ -1,4 +1,4 @@
|
|||
# Rdio Scanner v3.0
|
||||
# Rdio Scanner v3.1
|
||||
|
||||
*Rdio Scanner* is a progressive web interface designed to act like an old school police radio scanner. It integrates all frontend / backend components to manage audio files from different sources.
|
||||
|
||||
|
|
@ -10,11 +10,10 @@ Need help?
|
|||
|
||||
## What's new in this version
|
||||
|
||||
Version 3.0 brings new features:
|
||||
Version 3.1 brings new features:
|
||||
|
||||
* Unit aliases support, display names instead of unit ID
|
||||
* Download calls from the search pane
|
||||
* New configuration options: *allowDownload* and *useGroup*
|
||||
* Code refactoring
|
||||
* Display live feed listeners count on server end
|
||||
|
||||
*Note that you can only update from version 2.0 and above. You have to do a fresh install if your actual version is prior to version 2.0.*
|
||||
|
||||
|
|
|
|||
|
|
@ -1,6 +1,6 @@
|
|||
{
|
||||
"name": "rdio-scanner-client",
|
||||
"version": "3.0.0",
|
||||
"version": "3.1.0",
|
||||
"private": true,
|
||||
"scripts": {
|
||||
"build": "ng build --cross-origin use-credentials --prod $*",
|
||||
|
|
|
|||
|
|
@ -46,7 +46,7 @@ export interface RdioScannerCallsQueryResponse {
|
|||
export class AppRdioScannerCallsQueryService extends Query<RdioScannerCallsQueryResponse> {
|
||||
document = gql`
|
||||
query rdioScannerCalls(
|
||||
$date: Date
|
||||
$date: RdioScannerDate
|
||||
$limit: Int
|
||||
$offset: Int
|
||||
$sort: Int
|
||||
|
|
|
|||
|
|
@ -173,7 +173,7 @@
|
|||
</button>
|
||||
</ng-container>
|
||||
</mat-toolbar>
|
||||
<ng-container *ngIf="config.useGroup">
|
||||
<ng-container *ngIf="config.useGroup && groups.length">
|
||||
<fieldset>
|
||||
<div fxLayout="row wrap" fxLayoutAlign="space-between">
|
||||
<button *ngFor="let group of groups" class="rdio-button" (click)="toggleGroup(group.name)"
|
||||
|
|
|
|||
|
|
@ -344,7 +344,7 @@ export class AppRdioScannerComponent implements OnDestroy, OnInit {
|
|||
this.subscribeSearchForm();
|
||||
this.subscribeSearchPaginator();
|
||||
});
|
||||
});
|
||||
})
|
||||
|
||||
}
|
||||
|
||||
|
|
@ -567,10 +567,6 @@ export class AppRdioScannerComponent implements OnDestroy, OnInit {
|
|||
}
|
||||
|
||||
private buildGroups(): void {
|
||||
if (!this.config.useGroup) {
|
||||
return;
|
||||
}
|
||||
|
||||
const groups: RdioScannerGroup[] = [];
|
||||
|
||||
this.systems.forEach((system: RdioScannerSystem) => {
|
||||
|
|
|
|||
|
|
@ -1,6 +1,6 @@
|
|||
{
|
||||
"name": "rdio-scanner",
|
||||
"version": "3.0.0",
|
||||
"version": "3.1.0",
|
||||
"private": true,
|
||||
"main": "run.js",
|
||||
"scripts": {
|
||||
|
|
|
|||
|
|
@ -1,37 +1,21 @@
|
|||
'use strict';
|
||||
|
||||
require('dotenv').config();
|
||||
|
||||
const defaultConfig = {
|
||||
database: '',
|
||||
dialect: 'sqlite',
|
||||
const config = {
|
||||
database: process.env.DB_NAME,
|
||||
dialect: process.env.DB_DIALECT || 'sqlite',
|
||||
dialectOptions: {
|
||||
timezone: process.env.DB_TZ || 'Etc/GMT0',
|
||||
},
|
||||
host: '',
|
||||
host: process.env.DB_HOST,
|
||||
logging: false,
|
||||
password: '',
|
||||
port: '',
|
||||
storage: 'database.sqlite',
|
||||
username: '',
|
||||
password: process.env.DB_PASS,
|
||||
port: process.env.DB_PORT,
|
||||
storage: process.env.DB_STORAGE || 'database.sqlite',
|
||||
username: process.env.DB_USER,
|
||||
};
|
||||
|
||||
function getConfig() {
|
||||
return {
|
||||
database: process.env.DB_NAME || defaultConfig.database,
|
||||
dialect: process.env.DB_DIALECT || defaultConfig.dialect,
|
||||
dialectOptions: defaultConfig.dialectOptions,
|
||||
host: process.env.DB_HOST || defaultConfig.host,
|
||||
logging: defaultConfig.logging,
|
||||
password: process.env.DB_PASS || defaultConfig.password,
|
||||
port: process.env.DB_PORT || defaultConfig.port,
|
||||
storage: process.env.DB_STORAGE || defaultConfig.storage,
|
||||
username: process.env.DB_USER || defaultConfig.username,
|
||||
};
|
||||
}
|
||||
|
||||
module.exports = {
|
||||
development: getConfig(),
|
||||
production: getConfig(),
|
||||
test: getConfig(),
|
||||
development: config,
|
||||
production: config,
|
||||
test: config,
|
||||
};
|
||||
|
|
|
|||
5
server/config/index.js
Normal file
5
server/config/index.js
Normal file
|
|
@ -0,0 +1,5 @@
|
|||
'use strict';
|
||||
|
||||
const config = require('./config');
|
||||
|
||||
module.exports = config;
|
||||
|
|
@ -1,20 +0,0 @@
|
|||
'use strict';
|
||||
|
||||
module.exports = ({ store }) => {
|
||||
const camelCase = require('camelcase');
|
||||
const fs = require('fs');
|
||||
const path = require('path');
|
||||
const basename = path.basename(__filename);
|
||||
const dataSources = {};
|
||||
|
||||
fs.readdirSync(__dirname)
|
||||
.filter((file) => (file.indexOf('.') !== 0) && (file !== basename) && (file.slice(-3) === '.js'))
|
||||
.map((file) => file.slice(0, -3))
|
||||
.forEach((file) => {
|
||||
const ds = require(`./${file}`);
|
||||
const name = camelCase(file);
|
||||
dataSources[name] = new ds({ store });
|
||||
});
|
||||
|
||||
return () => dataSources;
|
||||
};
|
||||
|
|
@ -1,72 +0,0 @@
|
|||
'use strict';
|
||||
|
||||
const { DataSource } = require('apollo-datasource');
|
||||
const { callReducer } = require('../../helpers/rdio-scanner');
|
||||
|
||||
class RdioScannerSystem extends DataSource {
|
||||
constructor({ store }) {
|
||||
super();
|
||||
|
||||
this.store = store;
|
||||
}
|
||||
|
||||
initialize(config) {
|
||||
this.context = config.context;
|
||||
}
|
||||
|
||||
async getCall(id) {
|
||||
const result = await this.store.rdioScannerCall.findByPk(id);
|
||||
|
||||
return callReducer(result.dataValues);
|
||||
}
|
||||
|
||||
async getCalls({ date, system, talkgroup }, { limit, offset, sort }) {
|
||||
const Op = this.store.Sequelize.Op;
|
||||
|
||||
const attributes = {
|
||||
exclude: ['audio', 'freqList', 'srcList'],
|
||||
};
|
||||
|
||||
const order = [['startTime', typeof sort === 'number' && sort < 0 ? 'DESC' : 'ASC']];
|
||||
|
||||
const where = {};
|
||||
|
||||
if (typeof system === 'number') {
|
||||
where.system = system;
|
||||
}
|
||||
|
||||
if (typeof talkgroup === 'number') {
|
||||
where.talkgroup = talkgroup;
|
||||
}
|
||||
|
||||
const dateStartQuery = await this.store.rdioScannerCall.findOne({ order: [['startTime', 'ASC']], where });
|
||||
|
||||
const dateStart = dateStartQuery && dateStartQuery.get('startTime');
|
||||
|
||||
const dateStopQuery = await this.store.rdioScannerCall.findOne({ order: [['startTime', 'DESC']], where });
|
||||
|
||||
const dateStop = dateStopQuery && dateStopQuery.get('startTime');
|
||||
|
||||
if (typeof date === 'string') {
|
||||
const d = new Date(date);
|
||||
|
||||
where.startTime = {
|
||||
[Op.gte]: new Date(d.getFullYear(), d.getMonth(), d.getDate(), 0, 0, 0),
|
||||
[Op.lte]: new Date(d.getFullYear(), d.getMonth(), d.getDate(), 23, 59, 59),
|
||||
};
|
||||
}
|
||||
|
||||
const count = await this.store.rdioScannerCall.count({ where });
|
||||
|
||||
limit = typeof limit === 'number' ? limit : 100;
|
||||
offset = typeof offset === 'number' ? offset : 0;
|
||||
|
||||
const calls = await this.store.rdioScannerCall.findAll({ attributes, limit, offset, order, where });
|
||||
|
||||
const results = callReducer(calls);
|
||||
|
||||
return { count, dateStart, dateStop, results };
|
||||
}
|
||||
}
|
||||
|
||||
module.exports = RdioScannerSystem;
|
||||
|
|
@ -1,72 +0,0 @@
|
|||
'use strict';
|
||||
|
||||
const { DataSource } = require('apollo-datasource');
|
||||
const { callReducer } = require('../../helpers/rdio-scanner');
|
||||
|
||||
class RdioScannerSystem extends DataSource {
|
||||
constructor({ store }) {
|
||||
super();
|
||||
|
||||
this.store = store;
|
||||
}
|
||||
|
||||
initialize(config) {
|
||||
this.context = config.context;
|
||||
}
|
||||
|
||||
async getCall(id) {
|
||||
const result = await this.store.rdioScannerCall.findByPk(id);
|
||||
|
||||
return callReducer(result.dataValues);
|
||||
}
|
||||
|
||||
async getCalls({ date, system, talkgroup }, { limit, offset, sort }) {
|
||||
const Op = this.store.Sequelize.Op;
|
||||
|
||||
const attributes = {
|
||||
exclude: ['audio', 'freqList', 'srcList'],
|
||||
};
|
||||
|
||||
const order = [['startTime', typeof sort === 'number' && sort < 0 ? 'DESC' : 'ASC']];
|
||||
|
||||
const where = {};
|
||||
|
||||
if (typeof system === 'number') {
|
||||
where.system = system;
|
||||
}
|
||||
|
||||
if (typeof talkgroup === 'number') {
|
||||
where.talkgroup = talkgroup;
|
||||
}
|
||||
|
||||
const dateStartQuery = await this.store.rdioScannerCall.findOne({ order: [['startTime', 'ASC']], where });
|
||||
|
||||
const dateStart = dateStartQuery && dateStartQuery.get('startTime');
|
||||
|
||||
const dateStopQuery = await this.store.rdioScannerCall.findOne({ order: [['startTime', 'DESC']], where });
|
||||
|
||||
const dateStop = dateStopQuery && dateStopQuery.get('startTime');
|
||||
|
||||
if (typeof date === 'string') {
|
||||
const d = new Date(date);
|
||||
|
||||
where.startTime = {
|
||||
[Op.gte]: new Date(d.getFullYear(), d.getMonth(), d.getDate(), 0, 0, 0),
|
||||
[Op.lte]: new Date(d.getFullYear(), d.getMonth(), d.getDate(), 23, 59, 59),
|
||||
};
|
||||
}
|
||||
|
||||
const count = await this.store.rdioScannerCall.count({ where });
|
||||
|
||||
limit = typeof limit === 'number' ? limit : 100;
|
||||
offset = typeof offset === 'number' ? offset : 0;
|
||||
|
||||
const calls = await this.store.rdioScannerCall.findAll({ attributes, limit, offset, order, where });
|
||||
|
||||
const results = callReducer(calls);
|
||||
|
||||
return { count, dateStart, dateStop, results };
|
||||
}
|
||||
}
|
||||
|
||||
module.exports = RdioScannerSystem;
|
||||
|
|
@ -1,25 +0,0 @@
|
|||
'use strict';
|
||||
|
||||
const { DataSource } = require('apollo-datasource');
|
||||
const { systemReducer } = require ('../../helpers/rdio-scanner');
|
||||
|
||||
class RdioScannerSystem extends DataSource {
|
||||
constructor({ store }) {
|
||||
super();
|
||||
|
||||
this.store = store;
|
||||
}
|
||||
|
||||
initialize(config) {
|
||||
this.context = config.context;
|
||||
}
|
||||
|
||||
async getSystems() {
|
||||
const systems = await this.store.rdioScannerSystem.findAll({
|
||||
order: [['system', 'ASC']],
|
||||
});
|
||||
return systemReducer(systems);
|
||||
}
|
||||
}
|
||||
|
||||
module.exports = RdioScannerSystem;
|
||||
|
|
@ -1,63 +0,0 @@
|
|||
'use strict';
|
||||
|
||||
const camelCase = require('camelcase');
|
||||
const fs = require('fs');
|
||||
const { gql } = require('apollo-server-express');
|
||||
const path = require('path');
|
||||
|
||||
function graphql({ pubsub, store }) {
|
||||
|
||||
const dataSources = require('./datasources')({ store });
|
||||
|
||||
const query = readDir('queries', pubsub);
|
||||
const scalar = readDir('scalars', pubsub);
|
||||
const subscription = readDir('subscriptions', pubsub);
|
||||
const type = readDir('types', pubsub);
|
||||
|
||||
const typeDefs = gql([
|
||||
scalar.schemas,
|
||||
'type Query {', query.schemas, '}', '',
|
||||
'type Subscription {', subscription.schemas, '}', '',
|
||||
type.schemas
|
||||
].join('\n'));
|
||||
|
||||
const resolvers = {
|
||||
Query: query.resolvers,
|
||||
Subscription: subscription.resolvers,
|
||||
};
|
||||
|
||||
return {
|
||||
dataSources,
|
||||
resolvers,
|
||||
typeDefs,
|
||||
};
|
||||
}
|
||||
|
||||
function readDir(dir, pubsub) {
|
||||
let schemas = '';
|
||||
const resolvers = {};
|
||||
|
||||
dir = path.resolve(__dirname, dir);
|
||||
|
||||
const filenames = fs.readdirSync(dir);
|
||||
|
||||
filenames.filter((file) => (file.slice(-4) === '.gql'))
|
||||
.forEach((file) => {
|
||||
const schema = fs.readFileSync(path.join(dir, file));
|
||||
|
||||
schemas += `${schema}\n`;
|
||||
})
|
||||
|
||||
filenames.filter((file) => (file.indexOf('.') !== 0) && (file.slice(-3) === '.js'))
|
||||
.map((file) => file.slice(0, -3))
|
||||
.forEach((file) => {
|
||||
const resolver = require(path.join(dir, file))({ pubsub });
|
||||
const name = camelCase(file);
|
||||
|
||||
resolvers[name] = resolver;
|
||||
});
|
||||
|
||||
return { resolvers, schemas };
|
||||
}
|
||||
|
||||
module.exports = graphql;
|
||||
|
|
@ -1 +0,0 @@
|
|||
rdioScannerCall(id: String): RdioScannerCall
|
||||
|
|
@ -1,7 +0,0 @@
|
|||
'use strict';
|
||||
|
||||
module.exports = () => async (_, { id }, { dataSources }) => {
|
||||
const rdioScannerCall = await dataSources.rdioScannerCall.getCall(id);
|
||||
|
||||
return rdioScannerCall;
|
||||
};
|
||||
|
|
@ -1,8 +0,0 @@
|
|||
rdioScannerCalls(
|
||||
date: Date
|
||||
limit: Int
|
||||
offset: Int
|
||||
sort: Int
|
||||
system: Int
|
||||
talkgroup: Int
|
||||
): RdioScannerCallsQueryResponse
|
||||
|
|
@ -1,7 +0,0 @@
|
|||
'use strict';
|
||||
|
||||
module.exports = () => async (_, { date, limit, offset, sort, system, talkgroup }, { dataSources }) => {
|
||||
const { count, dateStart, dateStop, results } = await dataSources.rdioScannerCall.getCalls({ date, system, talkgroup }, { limit, offset, sort });
|
||||
|
||||
return { count, dateStart, dateStop, results };
|
||||
};
|
||||
|
|
@ -1 +0,0 @@
|
|||
rdioScannerConfig: RdioScannerConfig
|
||||
|
|
@ -1,15 +0,0 @@
|
|||
'use strict';
|
||||
|
||||
require('dotenv').config();
|
||||
|
||||
module.exports = () => async () => {
|
||||
let allowDownload = process.env.RDIO_ALLOW_DOWNLOAD || true;
|
||||
|
||||
allowDownload = `${allowDownload}`.toLowerCase() !== 'false';
|
||||
|
||||
let useGroup = process.env.RDIO_USE_GROUP || true;
|
||||
|
||||
useGroup = `${useGroup}`.toLowerCase() !== 'false';
|
||||
|
||||
return { allowDownload, useGroup };
|
||||
};
|
||||
|
|
@ -1 +0,0 @@
|
|||
rdioScannerSystems: [RdioScannerSystem]
|
||||
|
|
@ -1,5 +0,0 @@
|
|||
'use strict';
|
||||
|
||||
module.exports = () => async (_, __, { dataSources }) => {
|
||||
return await dataSources.rdioScannerSystem.getSystems();
|
||||
};
|
||||
|
|
@ -1 +0,0 @@
|
|||
scalar Audio
|
||||
|
|
@ -1 +0,0 @@
|
|||
scalar Date
|
||||
|
|
@ -1 +0,0 @@
|
|||
rdioScannerCall(selection: String): RdioScannerCall
|
||||
|
|
@ -1,33 +0,0 @@
|
|||
'use strict';
|
||||
|
||||
const { withFilter } = require('apollo-server-express');
|
||||
|
||||
module.exports = ({ pubsub }) => ({
|
||||
subscribe: withFilter(
|
||||
() => pubsub.asyncIterator(['rdioScannerCall']),
|
||||
(payload, variables) => {
|
||||
if (variables.selection) {
|
||||
if (payload.rdioScannerCall) {
|
||||
const call = payload.rdioScannerCall;
|
||||
|
||||
let selection;
|
||||
|
||||
try {
|
||||
selection = JSON.parse(variables.selection);
|
||||
|
||||
} catch (err) {
|
||||
return false;
|
||||
}
|
||||
|
||||
return selection[call.system][call.talkgroup];
|
||||
|
||||
} else {
|
||||
return false;
|
||||
}
|
||||
|
||||
} else {
|
||||
return true;
|
||||
}
|
||||
},
|
||||
),
|
||||
});
|
||||
|
|
@ -1 +0,0 @@
|
|||
rdioScannerSystems: [RdioScannerSystem]
|
||||
|
|
@ -1,5 +0,0 @@
|
|||
'use strict';
|
||||
|
||||
module.exports = ({ pubsub }) => ({
|
||||
subscribe: () => pubsub.asyncIterator(['rdioScannerSystems']),
|
||||
});
|
||||
|
|
@ -1,4 +0,0 @@
|
|||
type RdioScannerAlias {
|
||||
name: String
|
||||
uid: Int
|
||||
}
|
||||
|
|
@ -1,7 +0,0 @@
|
|||
type RdioScannerCallFreq {
|
||||
errorCount: Int
|
||||
freq: Int
|
||||
len: Int
|
||||
pos: Float
|
||||
spikeCount: Int
|
||||
}
|
||||
|
|
@ -1,4 +0,0 @@
|
|||
type RdioScannerCallSrc {
|
||||
pos: Float
|
||||
src: Int
|
||||
}
|
||||
|
|
@ -1,14 +0,0 @@
|
|||
type RdioScannerCall {
|
||||
id: String
|
||||
audio: Audio
|
||||
audioName: String
|
||||
audioType: String
|
||||
emergency: Boolean
|
||||
freq: Int
|
||||
freqList: [RdioScannerCallFreq]
|
||||
startTime: Date
|
||||
stopTime: Date
|
||||
srcList: [RdioScannerCallSrc]
|
||||
system: Int
|
||||
talkgroup: Int
|
||||
}
|
||||
|
|
@ -1,6 +0,0 @@
|
|||
type RdioScannerCallsQueryResponse {
|
||||
count: Int
|
||||
dateStart: Date
|
||||
dateStop: Date
|
||||
results: [RdioScannerCall]
|
||||
}
|
||||
|
|
@ -1,4 +0,0 @@
|
|||
type RdioScannerConfig {
|
||||
allowDownload: Boolean
|
||||
useGroup: Boolean
|
||||
}
|
||||
|
|
@ -1,7 +0,0 @@
|
|||
type RdioScannerSystem {
|
||||
aliases: [RdioScannerAlias]
|
||||
id: String
|
||||
name: String
|
||||
system: Int
|
||||
talkgroups: [RdioScannerTalkgroup]
|
||||
}
|
||||
|
|
@ -1,8 +0,0 @@
|
|||
type RdioScannerTalkgroup {
|
||||
alphaTag: String
|
||||
dec: Int
|
||||
description: String
|
||||
group: String
|
||||
mode: String
|
||||
tag: String
|
||||
}
|
||||
|
|
@ -1,121 +0,0 @@
|
|||
'use strict';
|
||||
|
||||
function callReducer(call) {
|
||||
if (Array.isArray(call)) {
|
||||
return call.map((c) => callReducer(c));
|
||||
|
||||
} else {
|
||||
try {
|
||||
call.freqList = JSON.parse(call.freqList);
|
||||
} catch (err) {
|
||||
call.freqList = [];
|
||||
}
|
||||
|
||||
try {
|
||||
call.srcList = JSON.parse(call.srcList);
|
||||
} catch (err) {
|
||||
call.srcList = [];
|
||||
}
|
||||
|
||||
return call;
|
||||
}
|
||||
}
|
||||
|
||||
function getApiKeys() {
|
||||
let apiKeys = ['b29eb8b9-9bcd-4e6e-bb4f-d244ada12736'];
|
||||
|
||||
if (process.env.RDIO_APIKEYS) {
|
||||
try {
|
||||
apiKeys = JSON.parse(process.env.RDIO_APIKEYS);
|
||||
} catch (err) {
|
||||
console.log('ERROR: Invalid JSON format in RDIO_APIKEYS.');
|
||||
}
|
||||
}
|
||||
|
||||
return apiKeys;
|
||||
}
|
||||
|
||||
function pruningScheduler({ store }) {
|
||||
let pruneDays = parseInt(process.env.RDIO_PRUNEDAYS, 10);
|
||||
|
||||
pruneDays = isNaN(pruneDays) ? 7 : pruneDays;
|
||||
|
||||
if (store === null || typeof store !== 'object' || pruneDays < 1) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const Op = store.Sequelize && store.Sequelize.Op || {};
|
||||
|
||||
return setInterval(() => {
|
||||
const now = new Date();
|
||||
|
||||
store.rdioScannerCall.destroy({
|
||||
where: {
|
||||
startTime: {
|
||||
[Op.lt]: new Date(now.getFullYear(), now.getMonth(), now.getDate() - pruneDays),
|
||||
},
|
||||
},
|
||||
});
|
||||
}, 15 * 60 * 1000);
|
||||
}
|
||||
|
||||
function systemReducer(system) {
|
||||
if (Array.isArray(system)) {
|
||||
return system.map((s) => systemReducer(s));
|
||||
|
||||
} else {
|
||||
try {
|
||||
system.aliases = JSON.parse(system.aliases);
|
||||
} catch (err) {
|
||||
system.aliases = [];
|
||||
}
|
||||
|
||||
try {
|
||||
system.talkgroups = JSON.parse(system.talkgroups);
|
||||
} catch (err) {
|
||||
system.talkgroups = [];
|
||||
}
|
||||
|
||||
return system;
|
||||
}
|
||||
}
|
||||
|
||||
function validateApiKey(key, system, talkgroup) {
|
||||
const apiKeys = getApiKeys();
|
||||
|
||||
if (typeof apiKeys === 'string') {
|
||||
return apiKeys === key;
|
||||
}
|
||||
|
||||
if (Array.isArray(apiKeys)) {
|
||||
return apiKeys.some((apiKey) => {
|
||||
if (typeof apiKey === 'string') {
|
||||
return apiKey === key;
|
||||
}
|
||||
|
||||
if (apiKey !== null && typeof apiKey === 'object') {
|
||||
return apiKey.key === key && Array.isArray(apiKey.systems) && apiKey.systems.some((sys) => {
|
||||
if (typeof sys === 'number') {
|
||||
return sys === system;
|
||||
}
|
||||
|
||||
if (sys !== null && typeof sys === 'object') {
|
||||
if (typeof talkgroup === 'undefined') {
|
||||
return sys.system === +system;
|
||||
}
|
||||
|
||||
return sys.system === +system && Array.isArray(sys.talkgroups) && sys.talkgroups.includes(+talkgroup);
|
||||
}
|
||||
|
||||
return false;
|
||||
});
|
||||
}
|
||||
|
||||
return false;
|
||||
});
|
||||
} else {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
module.exports = { callReducer, pruningScheduler, systemReducer, validateApiKey };
|
||||
|
|
@ -2,72 +2,49 @@
|
|||
|
||||
require('dotenv').config();
|
||||
|
||||
const { ApolloServer, PubSub } = require('apollo-server-express');
|
||||
const cors = require('cors');
|
||||
const express = require('express');
|
||||
const fs = require('fs');
|
||||
const helmet = require('helmet');
|
||||
const http = require('http');
|
||||
const path = require('path');
|
||||
|
||||
const RdioScanner = require('./lib/rdio-scanner');
|
||||
|
||||
const env = process.env.NODE_ENV || 'development';
|
||||
const host = process.env.NODE_HOST || '0.0.0.0';
|
||||
const staticDir = path.resolve(process.env.APP_CLIENT_DIR || '../client/dist');
|
||||
const staticFile = process.env.APP_CLIENT_FILE || 'main.html';
|
||||
const port = parseInt(process.env.NODE_PORT, 10) || 3000;
|
||||
|
||||
const store = require('./models');
|
||||
class App {
|
||||
constructor() {
|
||||
this.router = express();
|
||||
this.router.use(express.json());
|
||||
this.router.use(express.urlencoded({ extended: false }));
|
||||
this.router.use(express.static(staticDir));
|
||||
this.router.set(port);
|
||||
|
||||
const pubsub = new PubSub();
|
||||
if (env !== 'development') {
|
||||
this.router.disable('x-powered-by');
|
||||
|
||||
const { dataSources, resolvers, typeDefs } = require('./graphql')({ pubsub, store });
|
||||
this.router.get(/\/|\/index.html/, cors(), (req, res) => {
|
||||
if (fs.existsSync(path.join(staticDir, staticFile))) {
|
||||
return res.sendFile(staticFile, { root: staticDir });
|
||||
|
||||
const context = async ({ connection, req }) => {
|
||||
if (connection) {
|
||||
return connection.context;
|
||||
} else {
|
||||
return res.send('A new build is being prepared. Please check back in a few minutes.');
|
||||
}
|
||||
});
|
||||
|
||||
} else {
|
||||
return {
|
||||
token: req.headers.authorization || '',
|
||||
};
|
||||
this.router.use(helmet());
|
||||
}
|
||||
|
||||
this.httpServer = http.createServer(this.router);
|
||||
this.httpServer.listen(port, host, () => console.log(`Server is running at http://${host}:${port}/`));
|
||||
|
||||
this.rdioScanner = new RdioScanner(this.httpServer, this.router);
|
||||
}
|
||||
}
|
||||
|
||||
const app = express();
|
||||
|
||||
const httpServer = http.createServer(app);
|
||||
|
||||
const apolloServer = new ApolloServer({
|
||||
context,
|
||||
dataSources,
|
||||
resolvers,
|
||||
typeDefs,
|
||||
});
|
||||
|
||||
const { pruningScheduler } = require('./helpers/rdio-scanner');
|
||||
|
||||
const clientRoot = path.join(__dirname, '../client/dist');
|
||||
|
||||
pruningScheduler({ store });
|
||||
|
||||
app.use(express.json());
|
||||
app.use(express.urlencoded({ extended: false }));
|
||||
app.use(express.static(clientRoot));
|
||||
|
||||
app.post('/api/trunk-recorder-alias-upload', require('./routes/trunk-recorder-alias-upload')({ pubsub, store }));
|
||||
app.post('/api/trunk-recorder-call-upload', require('./routes/trunk-recorder-call-upload')({ pubsub, store }));
|
||||
app.post('/api/trunk-recorder-system-upload', require('./routes/trunk-recorder-system-upload')({ pubsub, store }));
|
||||
|
||||
app.set('port', port);
|
||||
|
||||
if (env !== 'development') {
|
||||
app.disable('x-powered-by');
|
||||
|
||||
app.get(/\/|\/index.html/, cors(), require('./routes/main')({ clientRoot }));
|
||||
|
||||
app.use(helmet());
|
||||
}
|
||||
|
||||
apolloServer.applyMiddleware({ app, cors: true });
|
||||
apolloServer.installSubscriptionHandlers(httpServer);
|
||||
|
||||
httpServer.listen(port, host, () => console.log(`Rdio Scanner is running at http://${host}:${port}/`));
|
||||
|
||||
module.exports = app;
|
||||
module.exports = new App();
|
||||
|
|
|
|||
11
server/lib/rdio-scanner/graphql/datasources/get-call.js
Normal file
11
server/lib/rdio-scanner/graphql/datasources/get-call.js
Normal file
|
|
@ -0,0 +1,11 @@
|
|||
'use strict';
|
||||
|
||||
function getCall(models) {
|
||||
return async (id) => {
|
||||
const result = await models.rdioScannerCall.findByPk(id);
|
||||
|
||||
return result.dataValues;
|
||||
};
|
||||
}
|
||||
|
||||
module.exports = getCall;
|
||||
51
server/lib/rdio-scanner/graphql/datasources/get-calls.js
Normal file
51
server/lib/rdio-scanner/graphql/datasources/get-calls.js
Normal file
|
|
@ -0,0 +1,51 @@
|
|||
'use strict';
|
||||
|
||||
const { Op } = require('sequelize');
|
||||
|
||||
function getCalls(models) {
|
||||
return async ({ date, system, talkgroup }, { limit, offset, sort }) => {
|
||||
const attributes = {
|
||||
exclude: ['audio', 'freqList', 'srcList'],
|
||||
};
|
||||
|
||||
const order = [['startTime', typeof sort === 'number' && sort < 0 ? 'DESC' : 'ASC']];
|
||||
|
||||
const where = {};
|
||||
|
||||
if (typeof system === 'number') {
|
||||
where.system = system;
|
||||
}
|
||||
|
||||
if (typeof talkgroup === 'number') {
|
||||
where.talkgroup = talkgroup;
|
||||
}
|
||||
|
||||
const dateStartQuery = await models.rdioScannerCall.findOne({ order: [['startTime', 'ASC']], where });
|
||||
|
||||
const dateStart = dateStartQuery && dateStartQuery.get('startTime');
|
||||
|
||||
const dateStopQuery = await models.rdioScannerCall.findOne({ order: [['startTime', 'DESC']], where });
|
||||
|
||||
const dateStop = dateStopQuery && dateStopQuery.get('startTime');
|
||||
|
||||
if (typeof date === 'string') {
|
||||
const d = new Date(date);
|
||||
|
||||
where.startTime = {
|
||||
[Op.gte]: new Date(d.getFullYear(), d.getMonth(), d.getDate(), 0, 0, 0),
|
||||
[Op.lte]: new Date(d.getFullYear(), d.getMonth(), d.getDate(), 23, 59, 59),
|
||||
};
|
||||
}
|
||||
|
||||
const count = await models.rdioScannerCall.count({ where });
|
||||
|
||||
limit = typeof limit === 'number' ? limit : 100;
|
||||
offset = typeof offset === 'number' ? offset : 0;
|
||||
|
||||
const results = await models.rdioScannerCall.findAll({ attributes, limit, offset, order, where });
|
||||
|
||||
return { count, dateStart, dateStop, results };
|
||||
};
|
||||
}
|
||||
|
||||
module.exports = getCalls;
|
||||
11
server/lib/rdio-scanner/graphql/datasources/get-systems.js
Normal file
11
server/lib/rdio-scanner/graphql/datasources/get-systems.js
Normal file
|
|
@ -0,0 +1,11 @@
|
|||
'use strict';
|
||||
|
||||
function getSystems(models) {
|
||||
return async () => {
|
||||
const systems = await models.rdioScannerSystem.findAll();
|
||||
|
||||
return systems;
|
||||
};
|
||||
}
|
||||
|
||||
module.exports = getSystems;
|
||||
25
server/lib/rdio-scanner/graphql/datasources/index.js
Normal file
25
server/lib/rdio-scanner/graphql/datasources/index.js
Normal file
|
|
@ -0,0 +1,25 @@
|
|||
'use strict';
|
||||
|
||||
const { DataSource } = require('apollo-datasource');
|
||||
|
||||
const getCall = require('./get-call');
|
||||
const getCalls = require('./get-calls');
|
||||
const getSystems = require('./get-systems');
|
||||
|
||||
class DataSources extends DataSource {
|
||||
constructor(models) {
|
||||
super();
|
||||
|
||||
this.rdioScanner = {
|
||||
getCall: getCall(models),
|
||||
getCalls: getCalls(models),
|
||||
getSystems: getSystems(models),
|
||||
};
|
||||
}
|
||||
|
||||
initialize(config) {
|
||||
this.context = config.context;
|
||||
}
|
||||
}
|
||||
|
||||
module.exports = DataSources;
|
||||
66
server/lib/rdio-scanner/graphql/index.js
Normal file
66
server/lib/rdio-scanner/graphql/index.js
Normal file
|
|
@ -0,0 +1,66 @@
|
|||
'use strict';
|
||||
|
||||
const { ApolloServer, gql } = require('apollo-server-express');
|
||||
|
||||
const DataSources = require('./datasources');
|
||||
const Queries = require('./queries');
|
||||
const Scalars = require('./scalars');
|
||||
const Subscriptions = require('./subscriptions');
|
||||
const Types = require('./types');
|
||||
|
||||
class GraphQL {
|
||||
static get DataSources() {
|
||||
return DataSources;
|
||||
}
|
||||
|
||||
static get Queries() {
|
||||
return Queries;
|
||||
}
|
||||
|
||||
static get Scalars() {
|
||||
return Scalars;
|
||||
}
|
||||
|
||||
static get Subscriptions() {
|
||||
return Subscriptions;
|
||||
}
|
||||
|
||||
static get Types() {
|
||||
return Types;
|
||||
}
|
||||
|
||||
constructor(models, pubsub) {
|
||||
const dataSources = () => new DataSources(models);
|
||||
|
||||
const queries = new Queries(models);
|
||||
|
||||
const scalars = new Scalars(models);
|
||||
|
||||
const subscriptions = new Subscriptions(pubsub);
|
||||
|
||||
const types = new Types(models);
|
||||
|
||||
const typeDefs = gql`
|
||||
${scalars.schemas}
|
||||
|
||||
${types.schemas}
|
||||
|
||||
type Query {
|
||||
${queries.schemas}
|
||||
}
|
||||
|
||||
type Subscription {
|
||||
${subscriptions.schemas}
|
||||
}
|
||||
`;
|
||||
|
||||
const resolvers = Object.assign({},
|
||||
{ Query: queries.resolvers },
|
||||
{ Subscription: subscriptions.resolvers },
|
||||
);
|
||||
|
||||
return new ApolloServer({ dataSources, resolvers, typeDefs });
|
||||
}
|
||||
}
|
||||
|
||||
module.exports = GraphQL;
|
||||
17
server/lib/rdio-scanner/graphql/queries/call.js
Normal file
17
server/lib/rdio-scanner/graphql/queries/call.js
Normal file
|
|
@ -0,0 +1,17 @@
|
|||
'use strict';
|
||||
|
||||
const schema = `
|
||||
rdioScannerCall(id: String): RdioScannerCall
|
||||
`;
|
||||
|
||||
function resolver() {
|
||||
return {
|
||||
async rdioScannerCall(_, { id }, { dataSources }) {
|
||||
const rdioScannerCall = await dataSources.rdioScanner.getCall(id);
|
||||
|
||||
return rdioScannerCall;
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
module.exports = { resolver, schema };
|
||||
24
server/lib/rdio-scanner/graphql/queries/calls.js
Normal file
24
server/lib/rdio-scanner/graphql/queries/calls.js
Normal file
|
|
@ -0,0 +1,24 @@
|
|||
'use strict';
|
||||
|
||||
const schema = `
|
||||
rdioScannerCalls(
|
||||
date: RdioScannerDate
|
||||
limit: Int
|
||||
offset: Int
|
||||
sort: Int
|
||||
system: Int
|
||||
talkgroup: Int
|
||||
): RdioScannerCallsQueryResponse
|
||||
`;
|
||||
|
||||
function resolver() {
|
||||
return {
|
||||
async rdioScannerCalls(_, { date, limit, offset, sort, system, talkgroup }, { dataSources }) {
|
||||
const { count, dateStart, dateStop, results } = await dataSources.rdioScanner.getCalls({ date, system, talkgroup }, { limit, offset, sort });
|
||||
|
||||
return { count, dateStart, dateStop, results };
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
module.exports = { resolver, schema };
|
||||
23
server/lib/rdio-scanner/graphql/queries/config.js
Normal file
23
server/lib/rdio-scanner/graphql/queries/config.js
Normal file
|
|
@ -0,0 +1,23 @@
|
|||
'use strict';
|
||||
|
||||
const schema = `
|
||||
rdioScannerConfig: RdioScannerConfig
|
||||
`;
|
||||
|
||||
function resolver() {
|
||||
return {
|
||||
async rdioScannerConfig() {
|
||||
let allowDownload = process.env.RDIO_ALLOW_DOWNLOAD || true;
|
||||
|
||||
allowDownload = `${allowDownload}`.toLowerCase() !== 'false';
|
||||
|
||||
let useGroup = process.env.RDIO_USE_GROUP || true;
|
||||
|
||||
useGroup = `${useGroup}`.toLowerCase() !== 'false';
|
||||
|
||||
return { allowDownload, useGroup };
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
module.exports = { resolver, schema };
|
||||
26
server/lib/rdio-scanner/graphql/queries/index.js
Normal file
26
server/lib/rdio-scanner/graphql/queries/index.js
Normal file
|
|
@ -0,0 +1,26 @@
|
|||
'use strict';
|
||||
|
||||
const call = require('./call');
|
||||
const calls = require('./calls');
|
||||
const config = require('./config');
|
||||
const systems = require('./systems');
|
||||
|
||||
class Queries {
|
||||
constructor(models) {
|
||||
this.schemas = `
|
||||
${call.schema}
|
||||
${calls.schema}
|
||||
${config.schema}
|
||||
${systems.schema}
|
||||
`;
|
||||
|
||||
this.resolvers = Object.assign({},
|
||||
call.resolver(models),
|
||||
calls.resolver(models),
|
||||
config.resolver(models),
|
||||
systems.resolver(models),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
module.exports = Queries;
|
||||
15
server/lib/rdio-scanner/graphql/queries/systems.js
Normal file
15
server/lib/rdio-scanner/graphql/queries/systems.js
Normal file
|
|
@ -0,0 +1,15 @@
|
|||
'use strict';
|
||||
|
||||
const schema = `
|
||||
rdioScannerSystems: [RdioScannerSystem]
|
||||
`;
|
||||
|
||||
function resolver() {
|
||||
return {
|
||||
async rdioScannerSystems(_, __, { dataSources }) {
|
||||
return await dataSources.rdioScanner.getSystems();
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
module.exports = { resolver, schema };
|
||||
7
server/lib/rdio-scanner/graphql/scalars/audio.js
Normal file
7
server/lib/rdio-scanner/graphql/scalars/audio.js
Normal file
|
|
@ -0,0 +1,7 @@
|
|||
'use strict';
|
||||
|
||||
const schema = `
|
||||
scalar RdioScannerAudio
|
||||
`;
|
||||
|
||||
module.exports = { schema };
|
||||
7
server/lib/rdio-scanner/graphql/scalars/date.js
Normal file
7
server/lib/rdio-scanner/graphql/scalars/date.js
Normal file
|
|
@ -0,0 +1,7 @@
|
|||
'use strict';
|
||||
|
||||
const schema = `
|
||||
scalar RdioScannerDate
|
||||
`;
|
||||
|
||||
module.exports = { schema };
|
||||
15
server/lib/rdio-scanner/graphql/scalars/index.js
Normal file
15
server/lib/rdio-scanner/graphql/scalars/index.js
Normal file
|
|
@ -0,0 +1,15 @@
|
|||
'use strict';
|
||||
|
||||
const audio = require('./audio');
|
||||
const date = require('./date');
|
||||
|
||||
class Scalars {
|
||||
constructor() {
|
||||
this.schemas = `
|
||||
${audio.schema}
|
||||
${date.schema}
|
||||
`;
|
||||
}
|
||||
}
|
||||
|
||||
module.exports = Scalars;
|
||||
43
server/lib/rdio-scanner/graphql/subscriptions/call.js
Normal file
43
server/lib/rdio-scanner/graphql/subscriptions/call.js
Normal file
|
|
@ -0,0 +1,43 @@
|
|||
'use strict';
|
||||
|
||||
const { withFilter } = require('apollo-server-express');
|
||||
|
||||
const schema = `
|
||||
rdioScannerCall(selection: String): RdioScannerCall
|
||||
`;
|
||||
|
||||
function resolver(pubsub) {
|
||||
return {
|
||||
rdioScannerCall: {
|
||||
subscribe: withFilter(
|
||||
() => pubsub.asyncIterator(['rdioScannerCall']),
|
||||
(payload, variables) => {
|
||||
if (variables.selection) {
|
||||
if (payload.rdioScannerCall) {
|
||||
const call = payload.rdioScannerCall;
|
||||
|
||||
let selection;
|
||||
|
||||
try {
|
||||
selection = JSON.parse(variables.selection);
|
||||
|
||||
} catch (err) {
|
||||
return false;
|
||||
}
|
||||
|
||||
return selection[call.system][call.talkgroup];
|
||||
|
||||
} else {
|
||||
return false;
|
||||
}
|
||||
|
||||
} else {
|
||||
return true;
|
||||
}
|
||||
},
|
||||
),
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
module.exports = { resolver, schema };
|
||||
20
server/lib/rdio-scanner/graphql/subscriptions/index.js
Normal file
20
server/lib/rdio-scanner/graphql/subscriptions/index.js
Normal file
|
|
@ -0,0 +1,20 @@
|
|||
'use strict';
|
||||
|
||||
const call = require('./call');
|
||||
const systems = require('./systems');
|
||||
|
||||
class Subscriptions {
|
||||
constructor(pubsub) {
|
||||
this.schemas = `
|
||||
${call.schema}
|
||||
${systems.schema}
|
||||
`;
|
||||
|
||||
this.resolvers = Object.assign({},
|
||||
call.resolver(pubsub),
|
||||
systems.resolver(pubsub),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
module.exports = Subscriptions;
|
||||
15
server/lib/rdio-scanner/graphql/subscriptions/systems.js
Normal file
15
server/lib/rdio-scanner/graphql/subscriptions/systems.js
Normal file
|
|
@ -0,0 +1,15 @@
|
|||
'use strict';
|
||||
|
||||
const schema = `
|
||||
rdioScannerSystems: [RdioScannerSystem]
|
||||
`;
|
||||
|
||||
function resolver(pubsub) {
|
||||
return {
|
||||
rdioScannerSystems: {
|
||||
subscribe: () => pubsub.asyncIterator(['rdioScannerSystems']),
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
module.exports = { resolver, schema };
|
||||
10
server/lib/rdio-scanner/graphql/types/alias.js
Normal file
10
server/lib/rdio-scanner/graphql/types/alias.js
Normal file
|
|
@ -0,0 +1,10 @@
|
|||
'use strict';
|
||||
|
||||
const schema = `
|
||||
type RdioScannerAlias {
|
||||
name: String
|
||||
uid: Int
|
||||
}
|
||||
`;
|
||||
|
||||
module.exports = { schema };
|
||||
13
server/lib/rdio-scanner/graphql/types/call-freq.js
Normal file
13
server/lib/rdio-scanner/graphql/types/call-freq.js
Normal file
|
|
@ -0,0 +1,13 @@
|
|||
'use strict';
|
||||
|
||||
const schema = `
|
||||
type RdioScannerCallFreq {
|
||||
errorCount: Int
|
||||
freq: Int
|
||||
len: Int
|
||||
pos: Float
|
||||
spikeCount: Int
|
||||
}
|
||||
`;
|
||||
|
||||
module.exports = { schema };
|
||||
12
server/lib/rdio-scanner/graphql/types/call-query-response.js
Normal file
12
server/lib/rdio-scanner/graphql/types/call-query-response.js
Normal file
|
|
@ -0,0 +1,12 @@
|
|||
'use strict';
|
||||
|
||||
const schema = `
|
||||
type RdioScannerCallsQueryResponse {
|
||||
count: Int
|
||||
dateStart: RdioScannerDate
|
||||
dateStop: RdioScannerDate
|
||||
results: [RdioScannerCall]
|
||||
}
|
||||
`;
|
||||
|
||||
module.exports = { schema };
|
||||
10
server/lib/rdio-scanner/graphql/types/call-src.js
Normal file
10
server/lib/rdio-scanner/graphql/types/call-src.js
Normal file
|
|
@ -0,0 +1,10 @@
|
|||
'use strict';
|
||||
|
||||
const schema = `
|
||||
type RdioScannerCallSrc {
|
||||
pos: Float
|
||||
src: Int
|
||||
}
|
||||
`;
|
||||
|
||||
module.exports = { schema };
|
||||
20
server/lib/rdio-scanner/graphql/types/call.js
Normal file
20
server/lib/rdio-scanner/graphql/types/call.js
Normal file
|
|
@ -0,0 +1,20 @@
|
|||
'use strict';
|
||||
|
||||
const schema = `
|
||||
type RdioScannerCall {
|
||||
id: String
|
||||
audio: RdioScannerAudio
|
||||
audioName: String
|
||||
audioType: String
|
||||
emergency: Boolean
|
||||
freq: Int
|
||||
freqList: [RdioScannerCallFreq]
|
||||
startTime: RdioScannerDate
|
||||
stopTime: RdioScannerDate
|
||||
srcList: [RdioScannerCallSrc]
|
||||
system: Int
|
||||
talkgroup: Int
|
||||
}
|
||||
`;
|
||||
|
||||
module.exports = { schema };
|
||||
10
server/lib/rdio-scanner/graphql/types/config.js
Normal file
10
server/lib/rdio-scanner/graphql/types/config.js
Normal file
|
|
@ -0,0 +1,10 @@
|
|||
'use strict';
|
||||
|
||||
const schema = `
|
||||
type RdioScannerConfig {
|
||||
allowDownload: Boolean
|
||||
useGroup: Boolean
|
||||
}
|
||||
`;
|
||||
|
||||
module.exports = { schema };
|
||||
27
server/lib/rdio-scanner/graphql/types/index.js
Normal file
27
server/lib/rdio-scanner/graphql/types/index.js
Normal file
|
|
@ -0,0 +1,27 @@
|
|||
'use strict';
|
||||
|
||||
const alias = require('./alias');
|
||||
const callFreq = require('./call-freq');
|
||||
const callQueryResponse = require('./call-query-response');
|
||||
const callSrc = require('./call-src');
|
||||
const call = require('./call');
|
||||
const config = require('./config');
|
||||
const system = require('./system');
|
||||
const talkgroup = require('./talkgroup');
|
||||
|
||||
class Types {
|
||||
constructor() {
|
||||
this.schemas = `
|
||||
${alias.schema}
|
||||
${callFreq.schema}
|
||||
${callQueryResponse.schema}
|
||||
${callSrc.schema}
|
||||
${call.schema}
|
||||
${config.schema}
|
||||
${system.schema}
|
||||
${talkgroup.schema}
|
||||
`;
|
||||
}
|
||||
}
|
||||
|
||||
module.exports = Types;
|
||||
13
server/lib/rdio-scanner/graphql/types/system.js
Normal file
13
server/lib/rdio-scanner/graphql/types/system.js
Normal file
|
|
@ -0,0 +1,13 @@
|
|||
'use strict';
|
||||
|
||||
const schema = `
|
||||
type RdioScannerSystem {
|
||||
aliases: [RdioScannerAlias]
|
||||
id: String
|
||||
name: String
|
||||
system: Int
|
||||
talkgroups: [RdioScannerTalkgroup]
|
||||
}
|
||||
`;
|
||||
|
||||
module.exports = { schema };
|
||||
14
server/lib/rdio-scanner/graphql/types/talkgroup.js
Normal file
14
server/lib/rdio-scanner/graphql/types/talkgroup.js
Normal file
|
|
@ -0,0 +1,14 @@
|
|||
'use strict';
|
||||
|
||||
const schema = `
|
||||
type RdioScannerTalkgroup {
|
||||
alphaTag: String
|
||||
dec: Int
|
||||
description: String
|
||||
group: String
|
||||
mode: String
|
||||
tag: String
|
||||
}
|
||||
`;
|
||||
|
||||
module.exports = { schema };
|
||||
17
server/lib/rdio-scanner/helpers/get-api-key.js
Normal file
17
server/lib/rdio-scanner/helpers/get-api-key.js
Normal file
|
|
@ -0,0 +1,17 @@
|
|||
'use strict';
|
||||
|
||||
function getApiKeys() {
|
||||
let apiKeys = ['b29eb8b9-9bcd-4e6e-bb4f-d244ada12736'];
|
||||
|
||||
if (process.env.RDIO_APIKEYS) {
|
||||
try {
|
||||
apiKeys = JSON.parse(process.env.RDIO_APIKEYS);
|
||||
} catch (err) {
|
||||
console.log('ERROR: Invalid JSON format in RDIO_APIKEYS.');
|
||||
}
|
||||
}
|
||||
|
||||
return apiKeys;
|
||||
}
|
||||
|
||||
module.exports = getApiKeys;
|
||||
15
server/lib/rdio-scanner/helpers/parse-date.js
Normal file
15
server/lib/rdio-scanner/helpers/parse-date.js
Normal file
|
|
@ -0,0 +1,15 @@
|
|||
'use strict';
|
||||
|
||||
function parseDate(value) {
|
||||
if (value instanceof Date) {
|
||||
return value;
|
||||
} else if (typeof value === 'number') {
|
||||
const date = new Date(1970, 0, 1);
|
||||
date.setUTCSeconds(value - date.getTimezoneOffset() * 60);
|
||||
return date;
|
||||
} else {
|
||||
return new Date(value);
|
||||
}
|
||||
}
|
||||
|
||||
module.exports = parseDate;
|
||||
27
server/lib/rdio-scanner/helpers/prune-scheduler.js
Normal file
27
server/lib/rdio-scanner/helpers/prune-scheduler.js
Normal file
|
|
@ -0,0 +1,27 @@
|
|||
'use strict';
|
||||
|
||||
const PRUNE_DAYS = parseInt(process.env.RDIO_PRUNEDAYS, 10);
|
||||
|
||||
function pruneScheduler(models) {
|
||||
const pruneDays = isNaN(PRUNE_DAYS) ? 7 : PRUNE_DAYS;
|
||||
|
||||
if (models === null || typeof models !== 'object' || pruneDays < 1) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const Op = (models.Sequelize && models.Sequelize.Op) || {};
|
||||
|
||||
return setInterval(() => {
|
||||
const now = new Date();
|
||||
|
||||
models.rdioScannerCall.destroy({
|
||||
where: {
|
||||
startTime: {
|
||||
[Op.lt]: new Date(now.getFullYear(), now.getMonth(), now.getDate() - pruneDays),
|
||||
},
|
||||
},
|
||||
});
|
||||
}, 15 * 60 * 1000);
|
||||
}
|
||||
|
||||
module.exports = pruneScheduler;
|
||||
54
server/lib/rdio-scanner/helpers/pubsub.js
Normal file
54
server/lib/rdio-scanner/helpers/pubsub.js
Normal file
|
|
@ -0,0 +1,54 @@
|
|||
const EventEmitter = require('events');
|
||||
const { PubSubEngine } = require('apollo-server-express');
|
||||
|
||||
class PubSub extends PubSubEngine {
|
||||
constructor() {
|
||||
super();
|
||||
|
||||
this.eventEmitter = new EventEmitter();
|
||||
|
||||
this.subscriptions = {};
|
||||
|
||||
this.subIdCounter = 0;
|
||||
}
|
||||
|
||||
async publish(triggerName, payload) {
|
||||
this.eventEmitter.emit(triggerName, payload);
|
||||
|
||||
return Promise.resolve();
|
||||
}
|
||||
|
||||
async subscribe(triggerName, onMessage) {
|
||||
this.eventEmitter.addListener(triggerName, onMessage);
|
||||
|
||||
this.subIdCounter = ++this.subIdCounter;
|
||||
|
||||
this.subscriptions[this.subIdCounter] = [triggerName, onMessage];
|
||||
|
||||
this.logLiveFeedListeners(triggerName);
|
||||
|
||||
return this.subIdCounter;
|
||||
}
|
||||
|
||||
async unsubscribe(subId) {
|
||||
const [triggerName, onMessage] = this.subscriptions[subId];
|
||||
|
||||
delete this.subscriptions[subId];
|
||||
|
||||
this.eventEmitter.removeListener(triggerName, onMessage);
|
||||
|
||||
this.logLiveFeedListeners(triggerName);
|
||||
}
|
||||
|
||||
logLiveFeedListeners(triggerName) {
|
||||
const livefeed = 'rdioScannerCall';
|
||||
|
||||
if (triggerName === livefeed) {
|
||||
const count = Object.keys(this.subscriptions).reduce((c, k) => this.subscriptions[k][0] === livefeed ? ++c : c, 0);
|
||||
|
||||
console.log(`Live feed listeners: ${count}`);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
module.exports = PubSub;
|
||||
44
server/lib/rdio-scanner/helpers/validate-api-keys.js
Normal file
44
server/lib/rdio-scanner/helpers/validate-api-keys.js
Normal file
|
|
@ -0,0 +1,44 @@
|
|||
'use strict';
|
||||
|
||||
const getApiKeys = require('./get-api-key');
|
||||
|
||||
function validateApiKey(key, system, talkgroup) {
|
||||
const apiKeys = getApiKeys();
|
||||
|
||||
if (typeof apiKeys === 'string') {
|
||||
return apiKeys === key;
|
||||
}
|
||||
|
||||
if (Array.isArray(apiKeys)) {
|
||||
return apiKeys.some((apiKey) => {
|
||||
if (typeof apiKey === 'string') {
|
||||
return apiKey === key;
|
||||
}
|
||||
|
||||
if (apiKey !== null && typeof apiKey === 'object') {
|
||||
return apiKey.key === key && Array.isArray(apiKey.systems) && apiKey.systems.some((sys) => {
|
||||
if (typeof sys === 'number') {
|
||||
return sys === system;
|
||||
}
|
||||
|
||||
if (sys !== null && typeof sys === 'object') {
|
||||
if (typeof talkgroup === 'undefined') {
|
||||
return sys.system === +system;
|
||||
}
|
||||
|
||||
return sys.system === +system && Array.isArray(sys.talkgroups) && sys.talkgroups.includes(+talkgroup);
|
||||
}
|
||||
|
||||
return false;
|
||||
});
|
||||
}
|
||||
|
||||
return false;
|
||||
});
|
||||
|
||||
} else {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
module.exports = validateApiKey;
|
||||
51
server/lib/rdio-scanner/index.js
Normal file
51
server/lib/rdio-scanner/index.js
Normal file
|
|
@ -0,0 +1,51 @@
|
|||
'use strict';
|
||||
|
||||
const Sequelize = require('sequelize');
|
||||
|
||||
const config = require('../../config');
|
||||
|
||||
const pruneScheduler = require('./helpers/prune-scheduler');
|
||||
const PubSub = require('./helpers/pubsub');
|
||||
|
||||
const GraphQL = require('./graphql');
|
||||
const Models = require('./models');
|
||||
const Routes = require('./routes');
|
||||
|
||||
const env = process.env.NODE_ENV || 'development';
|
||||
|
||||
class RdioScanner {
|
||||
static get GraphQL() {
|
||||
return GraphQL;
|
||||
}
|
||||
|
||||
static get Models() {
|
||||
return Models;
|
||||
}
|
||||
|
||||
static get PubSub() {
|
||||
return PubSub;
|
||||
}
|
||||
|
||||
static get Routes() {
|
||||
return Routes;
|
||||
}
|
||||
|
||||
constructor(httpServer, router, pubsub) {
|
||||
this.pubsub = pubsub || new PubSub();
|
||||
|
||||
this.sequelize = new Sequelize(config[env]);
|
||||
|
||||
this.models = new Models(this.sequelize);
|
||||
|
||||
this.graphQL = new GraphQL(this.models, this.pubsub);
|
||||
|
||||
this.graphQL.applyMiddleware({ app: router, cors: true });
|
||||
this.graphQL.installSubscriptionHandlers(httpServer);
|
||||
|
||||
this.routes = new Routes(this.models, this.pubsub, router);
|
||||
|
||||
pruneScheduler(this.models);
|
||||
}
|
||||
}
|
||||
|
||||
module.exports = RdioScanner;
|
||||
|
|
@ -14,24 +14,25 @@ module.exports = {
|
|||
type: Sequelize.INTEGER,
|
||||
},
|
||||
createdAt: {
|
||||
allowNull: false,
|
||||
type: Sequelize.DATE,
|
||||
allowNull: false,
|
||||
},
|
||||
updatedAt: {
|
||||
allowNull: false,
|
||||
type: Sequelize.DATE,
|
||||
allowNull: false,
|
||||
},
|
||||
name: {
|
||||
allowNull: false,
|
||||
type: Sequelize.STRING,
|
||||
allowNull: false,
|
||||
},
|
||||
system: {
|
||||
allowNull: false,
|
||||
type: Sequelize.INTEGER,
|
||||
allowNull: false,
|
||||
},
|
||||
talkgroups: {
|
||||
allowNull: false,
|
||||
type: Sequelize.JSON,
|
||||
defaultValue: [],
|
||||
allowNull: false,
|
||||
}
|
||||
}, { transaction });
|
||||
|
||||
|
|
@ -8,54 +8,54 @@ module.exports = {
|
|||
try {
|
||||
await queryInterface.createTable('rdioScannerCalls', {
|
||||
id: {
|
||||
allowNull: false,
|
||||
type: Sequelize.INTEGER,
|
||||
autoIncrement: true,
|
||||
primaryKey: true,
|
||||
type: Sequelize.INTEGER,
|
||||
allowNull: false,
|
||||
},
|
||||
createdAt: {
|
||||
allowNull: false,
|
||||
type: Sequelize.DATE,
|
||||
allowNull: false,
|
||||
},
|
||||
updatedAt: {
|
||||
allowNull: false,
|
||||
type: Sequelize.DATE,
|
||||
allowNull: false,
|
||||
},
|
||||
audio: {
|
||||
allowNull: false,
|
||||
type: Sequelize.BLOB('long'),
|
||||
allowNull: false,
|
||||
},
|
||||
emergency: {
|
||||
allowNull: false,
|
||||
type: Sequelize.BOOLEAN,
|
||||
allowNull: false,
|
||||
},
|
||||
freq: {
|
||||
allowNull: false,
|
||||
type: Sequelize.INTEGER,
|
||||
allowNull: false,
|
||||
},
|
||||
freqList: {
|
||||
allowNull: false,
|
||||
type: Sequelize.JSON,
|
||||
allowNull: false,
|
||||
},
|
||||
startTime: {
|
||||
allowNull: false,
|
||||
type: Sequelize.DATE,
|
||||
allowNull: false,
|
||||
},
|
||||
stopTime: {
|
||||
allowNull: false,
|
||||
type: Sequelize.DATE,
|
||||
allowNull: false,
|
||||
},
|
||||
srcList: {
|
||||
allowNull: false,
|
||||
type: Sequelize.JSON,
|
||||
allowNull: false,
|
||||
},
|
||||
system: {
|
||||
allowNull: false,
|
||||
type: Sequelize.INTEGER,
|
||||
allowNull: false,
|
||||
},
|
||||
talkgroup: {
|
||||
allowNull: false,
|
||||
type: Sequelize.INTEGER,
|
||||
allowNull: false,
|
||||
},
|
||||
}, { transaction });
|
||||
|
||||
23
server/lib/rdio-scanner/models/index.js
Normal file
23
server/lib/rdio-scanner/models/index.js
Normal file
|
|
@ -0,0 +1,23 @@
|
|||
'use strict';
|
||||
|
||||
const Sequelize = require('sequelize');
|
||||
|
||||
const rdioScannerCallFactory = require('./rdio-scanner-call');
|
||||
const rdioScannerSystemFactory = require('./rdio-scanner-system');
|
||||
|
||||
class Models {
|
||||
constructor(sequelize) {
|
||||
if (!(sequelize instanceof Sequelize)) {
|
||||
throw new Error('sequelize must be an instance of Sequelize');
|
||||
}
|
||||
|
||||
this.Sequelize = Sequelize;
|
||||
|
||||
this.sequelize = sequelize;
|
||||
|
||||
this.rdioScannerCall = rdioScannerCallFactory(sequelize);
|
||||
this.rdioScannerSystem = rdioScannerSystemFactory(sequelize);
|
||||
}
|
||||
}
|
||||
|
||||
module.exports = Models;
|
||||
84
server/lib/rdio-scanner/models/rdio-scanner-call.js
Normal file
84
server/lib/rdio-scanner/models/rdio-scanner-call.js
Normal file
|
|
@ -0,0 +1,84 @@
|
|||
'use strict';
|
||||
|
||||
const { DataTypes, Model } = require('sequelize');
|
||||
|
||||
class RdioScannerCall extends Model { }
|
||||
|
||||
function rdioScannerCallFactory(sequelize) {
|
||||
RdioScannerCall.init({
|
||||
audio: {
|
||||
type: DataTypes.BLOB('long'),
|
||||
allowNull: false,
|
||||
},
|
||||
emergency: {
|
||||
type: DataTypes.BOOLEAN,
|
||||
defaultValue: false,
|
||||
allowNull: false,
|
||||
},
|
||||
freq: {
|
||||
type: DataTypes.INTEGER,
|
||||
allowNull: false,
|
||||
},
|
||||
freqList: {
|
||||
type: DataTypes.JSON,
|
||||
defaultValue: [],
|
||||
allowNull: false,
|
||||
get() {
|
||||
let freqList = this.getDataValue('freqList');
|
||||
|
||||
if (typeof freqlist === 'string') {
|
||||
try {
|
||||
freqList = JSON.parse(freqList);
|
||||
|
||||
} catch (error) {
|
||||
freqList = [];
|
||||
}
|
||||
}
|
||||
|
||||
return freqList;
|
||||
},
|
||||
},
|
||||
startTime: {
|
||||
type: DataTypes.DATE,
|
||||
allowNull: false,
|
||||
},
|
||||
stopTime: {
|
||||
type: DataTypes.DATE,
|
||||
allowNull: false,
|
||||
},
|
||||
srcList: {
|
||||
type: DataTypes.JSON,
|
||||
defaultValue: [],
|
||||
allowNull: false,
|
||||
get() {
|
||||
let srclist = this.getDataValue('srcList');
|
||||
|
||||
if (typeof srclist === 'string') {
|
||||
try {
|
||||
srclist = JSON.parse(srclist);
|
||||
|
||||
} catch (error) {
|
||||
srclist = [];
|
||||
}
|
||||
}
|
||||
|
||||
return srclist;
|
||||
},
|
||||
},
|
||||
system: {
|
||||
type: DataTypes.INTEGER,
|
||||
allowNull: false,
|
||||
},
|
||||
talkgroup: {
|
||||
type: DataTypes.INTEGER,
|
||||
allowNull: false,
|
||||
},
|
||||
}, {
|
||||
modelName: 'rdioScannerCall',
|
||||
sequelize,
|
||||
});
|
||||
|
||||
return RdioScannerCall;
|
||||
}
|
||||
|
||||
module.exports = rdioScannerCallFactory;
|
||||
44
server/lib/rdio-scanner/models/rdio-scanner-system.js
Normal file
44
server/lib/rdio-scanner/models/rdio-scanner-system.js
Normal file
|
|
@ -0,0 +1,44 @@
|
|||
'use strict';
|
||||
|
||||
const { DataTypes, Model } = require('sequelize');
|
||||
|
||||
class RdioScannerSystem extends Model { }
|
||||
|
||||
function rdioScannerSystemFactory(sequelize) {
|
||||
RdioScannerSystem.init({
|
||||
name: {
|
||||
type: DataTypes.STRING,
|
||||
allowNull: false,
|
||||
},
|
||||
system: {
|
||||
type: DataTypes.INTEGER,
|
||||
allowNull: false,
|
||||
},
|
||||
talkgroups: {
|
||||
type: DataTypes.JSON,
|
||||
defaultValue: [],
|
||||
allowNull: false,
|
||||
get() {
|
||||
let talkgroups = this.getDataValue('talkgroups');
|
||||
|
||||
if (typeof talkgroups === 'string') {
|
||||
try {
|
||||
talkgroups = JSON.parse(talkgroups);
|
||||
|
||||
} catch (error) {
|
||||
talkgroups = [];
|
||||
}
|
||||
}
|
||||
|
||||
return talkgroups;
|
||||
},
|
||||
},
|
||||
}, {
|
||||
modelName: 'rdioScannerSystem',
|
||||
sequelize,
|
||||
});
|
||||
|
||||
return RdioScannerSystem;
|
||||
}
|
||||
|
||||
module.exports = rdioScannerSystemFactory;
|
||||
16
server/lib/rdio-scanner/routes/index.js
Normal file
16
server/lib/rdio-scanner/routes/index.js
Normal file
|
|
@ -0,0 +1,16 @@
|
|||
'use strict';
|
||||
|
||||
const TrunkRecorderCallUpload = require('./trunk-recorder-call-upload');
|
||||
const TrunkRecorderSystemUpload = require('./trunk-recorder-system-upload');
|
||||
|
||||
class Routes {
|
||||
constructor(models, pubsub, router) {
|
||||
this.trunkRecorderCallUpload = new TrunkRecorderCallUpload(models, pubsub);
|
||||
this.trunkRecorderSystemUpload = new TrunkRecorderSystemUpload(models, pubsub);
|
||||
|
||||
router.use(this.trunkRecorderCallUpload.path, this.trunkRecorderCallUpload.middleware);
|
||||
router.use(this.trunkRecorderSystemUpload.path, this.trunkRecorderSystemUpload.middleware);
|
||||
}
|
||||
}
|
||||
|
||||
module.exports = Routes;
|
||||
94
server/lib/rdio-scanner/routes/trunk-recorder-call-upload.js
Normal file
94
server/lib/rdio-scanner/routes/trunk-recorder-call-upload.js
Normal file
|
|
@ -0,0 +1,94 @@
|
|||
'use strict';
|
||||
|
||||
const multer = require('multer');
|
||||
|
||||
const upload = multer({ storage: multer.memoryStorage() });
|
||||
|
||||
const parseDate = require('../helpers/parse-date');
|
||||
const validateApiKey = require('../helpers/validate-api-keys');
|
||||
|
||||
class TrunkRecorderCallUpload {
|
||||
constructor(models, pubsub) {
|
||||
return {
|
||||
path: '/api/trunk-recorder-call-upload',
|
||||
|
||||
middleware: (req, res) => upload.fields([
|
||||
{ name: 'audio', maxCount: 1 },
|
||||
{ name: 'key', maxCount: 1 },
|
||||
{ name: 'meta', maxCount: 1 },
|
||||
{ name: 'system', maxCount: 1 },
|
||||
])(req, res, async (err) => {
|
||||
if (err instanceof multer.MulterError) {
|
||||
res.send(`${err.message}: ${err.field}\n`);
|
||||
|
||||
} else if (err) {
|
||||
res.send(`${err.message}\n`);
|
||||
|
||||
} else {
|
||||
const key = req.body.key;
|
||||
|
||||
let meta = req.files.meta[0].buffer.toString();
|
||||
|
||||
try {
|
||||
meta = JSON.parse(meta);
|
||||
|
||||
} catch (error) {
|
||||
meta = {};
|
||||
}
|
||||
|
||||
if (Array.isArray(meta.freqList)) {
|
||||
meta.freqList = meta.freqList.map((f) => ({
|
||||
errorCount: f.error_count,
|
||||
freq: f.freq,
|
||||
len: f.len,
|
||||
pos: f.pos,
|
||||
spikeCount: f.spike_count,
|
||||
}));
|
||||
|
||||
} else {
|
||||
meta.freqList = [];
|
||||
}
|
||||
|
||||
if (Array.isArray(meta.srcList)) {
|
||||
meta.srcList = meta.srcList.map((s) => ({
|
||||
src: s.src,
|
||||
pos: s.pos,
|
||||
}));
|
||||
|
||||
} else {
|
||||
meta.srcList = [];
|
||||
}
|
||||
|
||||
const data = {
|
||||
audio: req.files.audio[0].buffer,
|
||||
emergency: !!meta.emergency,
|
||||
freq: parseInt(meta.freq, 10),
|
||||
freqList: Array.isArray(meta.freqList) ? meta.freqList : [],
|
||||
startTime: parseDate(meta.start_time),
|
||||
stopTime: parseDate(meta.stop_time),
|
||||
srcList: Array.isArray(meta.srcList) ? meta.srcList : [],
|
||||
system: parseInt(req.body.system, 10),
|
||||
talkgroup: parseInt(meta.talkgroup, 10),
|
||||
};
|
||||
|
||||
if (!validateApiKey(key, data.system, data.talkgroup)) {
|
||||
return res.send(`Api key is invalid for system ${data.system} talkgroup ${data.talkgroup}.\n`);
|
||||
}
|
||||
|
||||
try {
|
||||
const call = await models.rdioScannerCall.create(data);
|
||||
|
||||
pubsub.publish('rdioScannerCall', { rdioScannerCall: call });
|
||||
|
||||
res.send(`Call imported successfully.\n`);
|
||||
|
||||
} catch (err) {
|
||||
res.send(err.message);
|
||||
}
|
||||
}
|
||||
}),
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
module.exports = TrunkRecorderCallUpload;
|
||||
103
server/lib/rdio-scanner/routes/trunk-recorder-system-upload.js
Normal file
103
server/lib/rdio-scanner/routes/trunk-recorder-system-upload.js
Normal file
|
|
@ -0,0 +1,103 @@
|
|||
'use script';
|
||||
|
||||
const multer = require('multer');
|
||||
|
||||
const upload = multer({ storage: multer.memoryStorage() });
|
||||
|
||||
const validateApiKey = require('../helpers/validate-api-keys');
|
||||
|
||||
class TrunkRecorderSystemUpload {
|
||||
constructor(models, pubsub) {
|
||||
return {
|
||||
path: '/api/trunk-recorder-system-upload',
|
||||
|
||||
middleware: (req, res) => upload.fields([
|
||||
{ name: 'csv', maxCount: 1 },
|
||||
{ name: 'key', maxCount: 1 },
|
||||
{ name: 'name', maxCount: 1 },
|
||||
{ name: 'system', maxCount: 1 },
|
||||
])(req, res, async (err) => {
|
||||
if (err instanceof multer.MulterError) {
|
||||
res.send(`${err.message}: ${err.field}\n`);
|
||||
|
||||
} else if (err) {
|
||||
res.send(`${err.message}\n`);
|
||||
|
||||
} else {
|
||||
const key = req.body.key;
|
||||
|
||||
const system = {
|
||||
name: req.body.name,
|
||||
system: parseInt(req.body.system, 10),
|
||||
talkgroups: [],
|
||||
}
|
||||
|
||||
if (!validateApiKey(key, system.system)) {
|
||||
return res.send(`Api key is invalid for system ${system.system}.\n`);
|
||||
}
|
||||
|
||||
const csv = req.files.csv[0].buffer.toString()
|
||||
.split(/\n/)
|
||||
.filter((l) => l.length && !/^\s*#/.test(l));
|
||||
|
||||
for (const line of csv) {
|
||||
const vals = line.split(',');
|
||||
|
||||
const talkgroup = {
|
||||
alphaTag: vals[3],
|
||||
dec: parseInt(vals[0], 10),
|
||||
description: vals[4],
|
||||
group: vals[6],
|
||||
mode: vals[2],
|
||||
tag: vals[5],
|
||||
};
|
||||
|
||||
if (validateApiKey(key, system.system, talkgroup.dec)) {
|
||||
system.talkgroups.push(talkgroup);
|
||||
}
|
||||
}
|
||||
|
||||
const emit = async () => {
|
||||
const systems = await models.rdioScannerSystem.findAll();
|
||||
|
||||
pubsub.publish('rdioScannerSystems', { rdioScannerSystems: systems });
|
||||
};
|
||||
|
||||
if (system.talkgroups.length) {
|
||||
try {
|
||||
const rec = await models.rdioScannerSystem.findOne({ where: { system: system.system } });
|
||||
|
||||
if (rec) {
|
||||
await rec.update(system);
|
||||
|
||||
} else {
|
||||
await models.rdioScannerSystem.create(system);
|
||||
}
|
||||
|
||||
await emit();
|
||||
|
||||
res.send(`System ${system.name} imported successfully.\n`);
|
||||
|
||||
} catch (err) {
|
||||
res.send(err.message);
|
||||
}
|
||||
|
||||
} else {
|
||||
try {
|
||||
await models.rdioScannerSystem.destroy({ where: { system: system.system } })
|
||||
|
||||
await emit();
|
||||
|
||||
res.send(`System ${system.name} removed from configuration.\n`);
|
||||
|
||||
} catch (err) {
|
||||
res.send(`Error while removing system ${system.name}.\n`);
|
||||
}
|
||||
}
|
||||
}
|
||||
}),
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
module.exports = TrunkRecorderSystemUpload;
|
||||
|
|
@ -1,42 +0,0 @@
|
|||
'use strict';
|
||||
|
||||
module.exports = {
|
||||
|
||||
up: async (queryInterface, Sequelize) => {
|
||||
const transaction = await queryInterface.sequelize.transaction();
|
||||
|
||||
try {
|
||||
await queryInterface.addColumn('rdioScannerCalls', 'audioName', Sequelize.STRING, { transaction });
|
||||
|
||||
await queryInterface.addColumn('rdioScannerCalls', 'audioType', Sequelize.STRING, { transaction });
|
||||
|
||||
await queryInterface.addColumn('rdioScannerSystems', 'aliases', Sequelize.JSON, { transaction });
|
||||
|
||||
await transaction.commit();
|
||||
|
||||
} catch (err) {
|
||||
await transaction.rollback();
|
||||
|
||||
throw err;
|
||||
}
|
||||
},
|
||||
|
||||
down: async (queryInterface, Sequelize) => {
|
||||
const transaction = queryInterface.sequelize.transaction();
|
||||
|
||||
try {
|
||||
await transaction.commit();
|
||||
|
||||
await queryInterface.removeColumn('rdioScannerCalls', 'audioName', Sequelize.STRING, { transaction });
|
||||
|
||||
await queryInterface.removeColumn('rdioScannerCalls', 'audioType', Sequelize.STRING, { transaction });
|
||||
|
||||
await queryInterface.removeColumn('rdioScannerSystems', 'aliases', Sequelize.JSON, { transaction });
|
||||
|
||||
} catch (err) {
|
||||
await transaction.rollback();
|
||||
|
||||
throw err;
|
||||
}
|
||||
},
|
||||
};
|
||||
|
|
@ -1,35 +0,0 @@
|
|||
'use strict';
|
||||
|
||||
const camelCase = require('camelcase');
|
||||
const fs = require('fs');
|
||||
const path = require('path');
|
||||
const Sequelize = require('sequelize');
|
||||
|
||||
const basename = path.basename(__filename);
|
||||
|
||||
const env = process.env.NODE_ENV || 'development';
|
||||
|
||||
const config = require('../config/config.js')[env];
|
||||
|
||||
const db = {};
|
||||
|
||||
const sequelize = new Sequelize(config);
|
||||
|
||||
fs.readdirSync(__dirname).filter(file => {
|
||||
return (file.indexOf('.') !== 0) && (file !== basename) && (file.slice(-3) === '.js');
|
||||
}).forEach(file => {
|
||||
const model = sequelize['import'](path.join(__dirname, file));
|
||||
const modelName = camelCase(model.name);
|
||||
db[modelName] = model;
|
||||
});
|
||||
|
||||
Object.keys(db).forEach(modelName => {
|
||||
if (db[modelName].associate) {
|
||||
db[modelName].associate(db);
|
||||
}
|
||||
});
|
||||
|
||||
db.sequelize = sequelize;
|
||||
db.Sequelize = Sequelize;
|
||||
|
||||
module.exports = db;
|
||||
|
|
@ -1,17 +0,0 @@
|
|||
'use strict';
|
||||
|
||||
module.exports = (sequelize, DataTypes) => {
|
||||
return sequelize.define('rdioScannerCall', {
|
||||
audio: DataTypes.BLOB('long'),
|
||||
audioName: DataTypes.STRING,
|
||||
audioType: DataTypes.STRING,
|
||||
emergency: DataTypes.BOOLEAN,
|
||||
freq: DataTypes.INTEGER,
|
||||
freqList: DataTypes.JSON,
|
||||
startTime: DataTypes.DATE,
|
||||
stopTime: DataTypes.DATE,
|
||||
srcList: DataTypes.JSON,
|
||||
system: DataTypes.INTEGER,
|
||||
talkgroup: DataTypes.INTEGER,
|
||||
}, {});
|
||||
};
|
||||
|
|
@ -1,13 +0,0 @@
|
|||
'use strict';
|
||||
|
||||
module.exports = (sequelize, DataTypes) => {
|
||||
return sequelize.define('rdioScannerSystem', {
|
||||
aliases: DataTypes.JSON,
|
||||
name: DataTypes.STRING,
|
||||
system: {
|
||||
type: DataTypes.INTEGER,
|
||||
unique: true,
|
||||
},
|
||||
talkgroups: DataTypes.JSON,
|
||||
}, {});
|
||||
};
|
||||
|
|
@ -1,10 +1,10 @@
|
|||
{
|
||||
"name": "rdio-scanner-server",
|
||||
"version": "3.0.0",
|
||||
"version": "3.1.0",
|
||||
"private": true,
|
||||
"main": "index.js",
|
||||
"scripts": {
|
||||
"migrate": "npx sequelize db:migrate",
|
||||
"migrate": "npx sequelize db:migrate --migrations-path lib/rdio-scanner/migrations",
|
||||
"start": "nodemon index"
|
||||
},
|
||||
"author": "Chrystian Huot",
|
||||
|
|
|
|||
|
|
@ -1,14 +0,0 @@
|
|||
'use strict';
|
||||
|
||||
const fs = require('fs');
|
||||
const path = require('path');
|
||||
|
||||
const mainFile = 'main.html';
|
||||
|
||||
module.exports = ({ clientRoot }) => (req, res) => {
|
||||
if (fs.existsSync(path.join(clientRoot, mainFile))) {
|
||||
return res.sendFile(mainFile, { root: clientRoot });
|
||||
} else {
|
||||
return res.send('A new build of Rdio Scanner is being prepared. Please check back in a few minutes.');
|
||||
}
|
||||
};
|
||||
|
|
@ -1,59 +0,0 @@
|
|||
'use strict';
|
||||
|
||||
const multer = require('multer');
|
||||
const upload = multer({ storage: multer.memoryStorage() });
|
||||
|
||||
const { systemReducer, validateApiKey } = require('../helpers/rdio-scanner');
|
||||
|
||||
module.exports = ({ pubsub, store }) => (req, res) => upload.fields([
|
||||
{ name: 'csv', maxCount: 1 },
|
||||
{ name: 'key', maxCount: 1 },
|
||||
{ name: 'system', maxCount: 1 },
|
||||
])(req, res, (err) => {
|
||||
if (err instanceof multer.MulterError) {
|
||||
res.send(`${err.message}: ${err.field}\n`);
|
||||
|
||||
} else if (err) {
|
||||
res.send(`${err.message}\n`);
|
||||
|
||||
} else {
|
||||
const key = req.body.key;
|
||||
|
||||
const system = parseInt(req.body.system, 10);
|
||||
|
||||
const aliases = [];
|
||||
|
||||
if (!validateApiKey(key, system)) {
|
||||
return res.send(`Api key is invalid for system ${system}.\n`);
|
||||
}
|
||||
|
||||
const csv = req.files.csv[0].buffer.toString()
|
||||
.split(/\n/)
|
||||
.filter((l) => l.length && !/^\s*#/.test(l));
|
||||
|
||||
for (const line of csv) {
|
||||
const vals = line.split(',');
|
||||
|
||||
aliases.push({
|
||||
name: vals[1],
|
||||
uid: parseInt(vals[0], 10),
|
||||
})
|
||||
}
|
||||
|
||||
store.rdioScannerSystem.findOne({ where: { system } })
|
||||
.then((rdioScannerSystem) => {
|
||||
if (rdioScannerSystem) {
|
||||
rdioScannerSystem.update({ aliases: JSON.stringify(aliases), system }).then(() => {
|
||||
res.send(`System ${rdioScannerSystem.name} updated successfully.\n`);
|
||||
|
||||
store.rdioScannerSystem.findAll({ order: [['system', 'ASC']] }).then((systems) => {
|
||||
pubsub.publish('rdioScannerSystems', { rdioScannerSystems: systemReducer(systems) });
|
||||
})
|
||||
});
|
||||
|
||||
} else {
|
||||
res.send(`Cannot update aliases for system ${rdioScannerSystem.name}.\n`);
|
||||
}
|
||||
});
|
||||
}
|
||||
});
|
||||
|
|
@ -1,89 +0,0 @@
|
|||
'use strict';
|
||||
|
||||
const multer = require('multer');
|
||||
const upload = multer({ storage: multer.memoryStorage() });
|
||||
|
||||
const { callReducer, validateApiKey } = require('../helpers/rdio-scanner');
|
||||
|
||||
module.exports = ({ pubsub, store }) => (req, res) => upload.fields([
|
||||
{ name: 'audio', maxCount: 1 },
|
||||
{ name: 'key', maxCount: 1 },
|
||||
{ name: 'meta', maxCount: 1 },
|
||||
{ name: 'system', maxCount: 1 },
|
||||
])(req, res, (err) => {
|
||||
if (err instanceof multer.MulterError) {
|
||||
res.send(`${err.message}: ${err.field}\n`);
|
||||
|
||||
} else if (err) {
|
||||
res.send(`${err.message}\n`);
|
||||
|
||||
} else {
|
||||
const key = req.body.key;
|
||||
|
||||
let meta = req.files.meta[0].buffer.toString();
|
||||
|
||||
try {
|
||||
meta = JSON.parse(meta);
|
||||
} catch (err) {
|
||||
meta = {};
|
||||
}
|
||||
|
||||
if (Array.isArray(meta.freqList)) {
|
||||
meta.freqList = meta.freqList.map((f) => ({
|
||||
errorCount: f.error_count,
|
||||
freq: f.freq,
|
||||
len: f.len,
|
||||
pos: f.pos,
|
||||
spikeCount: f.spike_count,
|
||||
}));
|
||||
} else {
|
||||
meta.freqList = [];
|
||||
}
|
||||
|
||||
if (Array.isArray(meta.srcList)) {
|
||||
meta.srcList = meta.srcList.map((s) => ({
|
||||
src: s.src,
|
||||
pos: s.pos,
|
||||
}));
|
||||
} else {
|
||||
meta.srcList = [];
|
||||
}
|
||||
|
||||
const data = {
|
||||
audio: req.files.audio[0].buffer,
|
||||
audioName: req.files.audio[0].originalname,
|
||||
audioType: req.files.audio[0].mimetype,
|
||||
emergency: !!meta.emergency,
|
||||
freq: parseInt(meta.freq, 10),
|
||||
freqList: JSON.stringify(Array.isArray(meta.freqList) ? meta.freqList : []),
|
||||
startTime: parseDate(meta.start_time),
|
||||
stopTime: parseDate(meta.stop_time),
|
||||
srcList: JSON.stringify(Array.isArray(meta.srcList) ? meta.srcList : []),
|
||||
system: parseInt(req.body.system, 10),
|
||||
talkgroup: parseInt(meta.talkgroup, 10),
|
||||
};
|
||||
|
||||
if (!validateApiKey(key, data.system, data.talkgroup)) {
|
||||
return res.send(`Api key is invalid for system ${data.system} talkgroup ${data.talkgroup}.\n`);
|
||||
}
|
||||
|
||||
store.rdioScannerCall.create(data)
|
||||
.then((call) => {
|
||||
pubsub.publish('rdioScannerCall', { rdioScannerCall: callReducer(call) });
|
||||
res.send(`Call imported successfully.\n`)
|
||||
})
|
||||
.catch((err) => res.send(err.message));
|
||||
}
|
||||
});
|
||||
|
||||
function parseDate(value) {
|
||||
if (value instanceof Date) {
|
||||
return value;
|
||||
} else if (typeof value === 'number') {
|
||||
const date = new Date(1970, 0, 1);
|
||||
date.setUTCSeconds(value - date.getTimezoneOffset() * 60);
|
||||
return date;
|
||||
} else {
|
||||
return new Date(value);
|
||||
}
|
||||
}
|
||||
|
|
@ -1,88 +0,0 @@
|
|||
'use strict';
|
||||
|
||||
const multer = require('multer');
|
||||
const upload = multer({ storage: multer.memoryStorage() });
|
||||
|
||||
const { systemReducer, validateApiKey } = require('../helpers/rdio-scanner');
|
||||
|
||||
module.exports = ({ pubsub, store }) => (req, res) => upload.fields([
|
||||
{ name: 'csv', maxCount: 1 },
|
||||
{ name: 'key', maxCount: 1 },
|
||||
{ name: 'name', maxCount: 1 },
|
||||
{ name: 'system', maxCount: 1 },
|
||||
])(req, res, (err) => {
|
||||
if (err instanceof multer.MulterError) {
|
||||
res.send(`${err.message}: ${err.field}\n`);
|
||||
|
||||
} else if (err) {
|
||||
res.send(`${err.message}\n`);
|
||||
|
||||
} else {
|
||||
const key = req.body.key;
|
||||
|
||||
const system = {
|
||||
name: req.body.name,
|
||||
system: parseInt(req.body.system, 10),
|
||||
talkgroups: [],
|
||||
}
|
||||
|
||||
if (!validateApiKey(key, system.system)) {
|
||||
return res.send(`Api key is invalid for system ${system.system}.\n`);
|
||||
}
|
||||
|
||||
const csv = req.files.csv[0].buffer.toString()
|
||||
.split(/\n/)
|
||||
.filter((l) => l.length && !/^\s*#/.test(l));
|
||||
|
||||
for (const line of csv) {
|
||||
const vals = line.split(',');
|
||||
|
||||
const talkgroup = {
|
||||
alphaTag: vals[3],
|
||||
dec: parseInt(vals[0], 10),
|
||||
description: vals[4],
|
||||
group: vals[6],
|
||||
mode: vals[2],
|
||||
tag: vals[5],
|
||||
};
|
||||
|
||||
if (validateApiKey(key, system.system, talkgroup.dec)) {
|
||||
system.talkgroups.push(talkgroup);
|
||||
}
|
||||
}
|
||||
|
||||
const emit = () => {
|
||||
store.rdioScannerSystem.findAll({ order: [['system', 'ASC']]}).then((systems) => {
|
||||
pubsub.publish('rdioScannerSystems', { rdioScannerSystems: systemReducer(systems) });
|
||||
})
|
||||
};
|
||||
|
||||
if (system.talkgroups.length) {
|
||||
system.talkgroups = JSON.stringify(system.talkgroups);
|
||||
|
||||
store.rdioScannerSystem.findOne({ where: { system: system.system } })
|
||||
.then((rdioScannerSystem) => {
|
||||
if (rdioScannerSystem) {
|
||||
rdioScannerSystem.update(system).then(() => {
|
||||
emit();
|
||||
res.send(`System ${system.name} updated successfully.\n`);
|
||||
});
|
||||
|
||||
} else {
|
||||
store.rdioScannerSystem.create(system).then(() => {
|
||||
emit();
|
||||
res.send(`System ${system.name} imported successfully.\n`);
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
} else {
|
||||
store.rdioScannerSystem.destroy({ where: { system: system.system } })
|
||||
.then(() => {
|
||||
emit();
|
||||
res.send(`System ${system.name} removed from configuration.\n`);
|
||||
})
|
||||
.catch(() => res.send(`Error while removing system ${system.name}.\n`));
|
||||
}
|
||||
}
|
||||
});
|
||||
Loading…
Reference in a new issue