Version 3.1

This commit is contained in:
Chrystian Huot 2020-03-02 15:05:35 -05:00
parent 0b653aaba8
commit db7b464b99
87 changed files with 1152 additions and 984 deletions

View file

@ -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.*

View file

@ -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 $*",

View file

@ -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

View file

@ -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)"

View file

@ -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) => {

View file

@ -1,6 +1,6 @@
{
"name": "rdio-scanner",
"version": "3.0.0",
"version": "3.1.0",
"private": true,
"main": "run.js",
"scripts": {

View file

@ -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
View file

@ -0,0 +1,5 @@
'use strict';
const config = require('./config');
module.exports = config;

View file

@ -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;
};

View file

@ -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;

View file

@ -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;

View file

@ -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;

View file

@ -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;

View file

@ -1 +0,0 @@
rdioScannerCall(id: String): RdioScannerCall

View file

@ -1,7 +0,0 @@
'use strict';
module.exports = () => async (_, { id }, { dataSources }) => {
const rdioScannerCall = await dataSources.rdioScannerCall.getCall(id);
return rdioScannerCall;
};

View file

@ -1,8 +0,0 @@
rdioScannerCalls(
date: Date
limit: Int
offset: Int
sort: Int
system: Int
talkgroup: Int
): RdioScannerCallsQueryResponse

View file

@ -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 };
};

View file

@ -1 +0,0 @@
rdioScannerConfig: RdioScannerConfig

View file

@ -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 };
};

View file

@ -1 +0,0 @@
rdioScannerSystems: [RdioScannerSystem]

View file

@ -1,5 +0,0 @@
'use strict';
module.exports = () => async (_, __, { dataSources }) => {
return await dataSources.rdioScannerSystem.getSystems();
};

View file

@ -1 +0,0 @@
scalar Audio

View file

@ -1 +0,0 @@
scalar Date

View file

@ -1 +0,0 @@
rdioScannerCall(selection: String): RdioScannerCall

View file

@ -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;
}
},
),
});

View file

@ -1 +0,0 @@
rdioScannerSystems: [RdioScannerSystem]

View file

@ -1,5 +0,0 @@
'use strict';
module.exports = ({ pubsub }) => ({
subscribe: () => pubsub.asyncIterator(['rdioScannerSystems']),
});

View file

@ -1,4 +0,0 @@
type RdioScannerAlias {
name: String
uid: Int
}

View file

@ -1,7 +0,0 @@
type RdioScannerCallFreq {
errorCount: Int
freq: Int
len: Int
pos: Float
spikeCount: Int
}

View file

@ -1,4 +0,0 @@
type RdioScannerCallSrc {
pos: Float
src: Int
}

View file

@ -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
}

View file

@ -1,6 +0,0 @@
type RdioScannerCallsQueryResponse {
count: Int
dateStart: Date
dateStop: Date
results: [RdioScannerCall]
}

View file

@ -1,4 +0,0 @@
type RdioScannerConfig {
allowDownload: Boolean
useGroup: Boolean
}

View file

@ -1,7 +0,0 @@
type RdioScannerSystem {
aliases: [RdioScannerAlias]
id: String
name: String
system: Int
talkgroups: [RdioScannerTalkgroup]
}

View file

@ -1,8 +0,0 @@
type RdioScannerTalkgroup {
alphaTag: String
dec: Int
description: String
group: String
mode: String
tag: String
}

View file

@ -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 };

View file

@ -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();

View 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;

View 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;

View file

@ -0,0 +1,11 @@
'use strict';
function getSystems(models) {
return async () => {
const systems = await models.rdioScannerSystem.findAll();
return systems;
};
}
module.exports = getSystems;

View 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;

View 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;

View 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 };

View 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 };

View 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 };

View 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;

View 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 };

View file

@ -0,0 +1,7 @@
'use strict';
const schema = `
scalar RdioScannerAudio
`;
module.exports = { schema };

View file

@ -0,0 +1,7 @@
'use strict';
const schema = `
scalar RdioScannerDate
`;
module.exports = { schema };

View 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;

View 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 };

View 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;

View file

@ -0,0 +1,15 @@
'use strict';
const schema = `
rdioScannerSystems: [RdioScannerSystem]
`;
function resolver(pubsub) {
return {
rdioScannerSystems: {
subscribe: () => pubsub.asyncIterator(['rdioScannerSystems']),
},
};
}
module.exports = { resolver, schema };

View file

@ -0,0 +1,10 @@
'use strict';
const schema = `
type RdioScannerAlias {
name: String
uid: Int
}
`;
module.exports = { schema };

View file

@ -0,0 +1,13 @@
'use strict';
const schema = `
type RdioScannerCallFreq {
errorCount: Int
freq: Int
len: Int
pos: Float
spikeCount: Int
}
`;
module.exports = { schema };

View file

@ -0,0 +1,12 @@
'use strict';
const schema = `
type RdioScannerCallsQueryResponse {
count: Int
dateStart: RdioScannerDate
dateStop: RdioScannerDate
results: [RdioScannerCall]
}
`;
module.exports = { schema };

View file

@ -0,0 +1,10 @@
'use strict';
const schema = `
type RdioScannerCallSrc {
pos: Float
src: Int
}
`;
module.exports = { schema };

View 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 };

View file

@ -0,0 +1,10 @@
'use strict';
const schema = `
type RdioScannerConfig {
allowDownload: Boolean
useGroup: Boolean
}
`;
module.exports = { schema };

View 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;

View file

@ -0,0 +1,13 @@
'use strict';
const schema = `
type RdioScannerSystem {
aliases: [RdioScannerAlias]
id: String
name: String
system: Int
talkgroups: [RdioScannerTalkgroup]
}
`;
module.exports = { schema };

View 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 };

View 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;

View 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;

View 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;

View 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;

View 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;

View 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;

View file

@ -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 });

View file

@ -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 });

View 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;

View 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;

View 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;

View 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;

View 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;

View 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;

View file

@ -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;
}
},
};

View file

@ -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;

View file

@ -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,
}, {});
};

View file

@ -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,
}, {});
};

View file

@ -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",

View file

@ -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.');
}
};

View file

@ -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`);
}
});
}
});

View file

@ -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);
}
}

View file

@ -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`));
}
}
});