Version 6.5.0

This commit is contained in:
Chrystian Huot 2022-06-27 11:19:18 -04:00
parent 05747a0428
commit 0e088ad534
52 changed files with 923 additions and 547 deletions

View file

@ -1,5 +1,18 @@
# Change log
## Version 6.5
- Fixed API looping on malformed or invalid multipart content (issue #181, #212).
- Source code updated to GO 1.18 with `interface{}` replaced by `any`.
- Removed ingest mutex for performance reasons.
- Replaced all `path.Base()` by `filepath.Base()` to fix an issue with audio filenames on Windows.
- New `Branding Label` and `Email Support` options to show on main screen (issue #220).
- New temporary avoid feature (discussion #218).
- Fix remote address regexp (issue #225).
- Add the `ident` to `new listener` log message (discussion #226).
- New populated talkgroups won't be activated on the client if its group (or tag) is turned off (issue #227).
- Removed the duplicated webapp section from the PDF document.
## Version 6.4
- New `-cmd` command line options to allow advanced administrative tasks.

View file

@ -27,13 +27,13 @@ Clone the official repository on your computer and start the build process.
When finished, you will find the precompiled versions for various platforms in the `dist` folder.
rdio-scanner-darwin-amd64-v6.4.5.zip
rdio-scanner-darwin-arm64-v6.4.5.zip
rdio-scanner-freebsd-amd64-v6.4.5.zip
rdio-scanner-linux-386-v6.4.5.zip
rdio-scanner-linux-amd64-v6.4.5.zip
rdio-scanner-linux-arm64-v6.4.5.zip
rdio-scanner-linux-arm-v6.4.5.zip
rdio-scanner-windows-amd64-v6.4.5.zip
rdio-scanner-darwin-amd64-v6.5.0.zip
rdio-scanner-darwin-arm64-v6.5.0.zip
rdio-scanner-freebsd-amd64-v6.5.0.zip
rdio-scanner-linux-386-v6.5.0.zip
rdio-scanner-linux-amd64-v6.5.0.zip
rdio-scanner-linux-arm64-v6.5.0.zip
rdio-scanner-linux-arm-v6.5.0.zip
rdio-scanner-windows-amd64-v6.5.0.zip
**Happy Rdio scanning !**

View file

@ -16,14 +16,14 @@
################################################################################
app := rdio-scanner
date := 2022/06/14
ver := 6.4.5
date := 2022/06/27
ver := 6.5.0
client := $(wildcard client/*.json client/*.ts)
server := $(wildcard server/*.go)
build = @cd server && GOOS=$(1) GOARCH=$(2) go build -o ../dist/$(1)-$(2)/$(3)
pandoc = @test -d dist/$(1)-$(2) || mkdir -p dist/$(1)-$(2) && pandoc -f markdown -o dist/$(1)-$(2)/$(3) --resource-path docs:docs/platforms $(4) docs/webapp.md docs/webapp.md docs/faq.md CHANGELOG.md
pandoc = @test -d dist/$(1)-$(2) || mkdir -p dist/$(1)-$(2) && pandoc -f markdown -o dist/$(1)-$(2)/$(3) --resource-path docs:docs/platforms $(4) docs/webapp.md docs/faq.md CHANGELOG.md
zip = @cd dist/$(1)-$(2) && zip -q ../$(app)-$(1)-$(2)-v$(ver).zip * && cd ..
.PHONY: all clean container dist sed

View file

@ -1,12 +1,12 @@
{
"name": "rdio-scanner",
"version": "6.4.5",
"version": "6.5.0",
"lockfileVersion": 2,
"requires": true,
"packages": {
"": {
"name": "rdio-scanner",
"version": "6.4.5",
"version": "6.5.0",
"license": "LICENSE",
"dependencies": {
"@angular/animations": "^13.3.10",

View file

@ -38,5 +38,5 @@
"build": "ng build --base-href ./ --configuration production",
"start": "ng serve"
},
"version": "6.4.5"
"version": "6.5.0"
}

View file

@ -132,9 +132,11 @@ export interface Options {
afsSystems?: string;
audioConversion?: 0 | 1 | 2 | 3;
autoPopulate?: boolean;
branding?: string;
dimmerDelay?: number;
disableDuplicateDetection?: boolean;
duplicateDetectionTimeFrame?: number;
email?: string;
keypadBeeps?: string;
maxClients?: number;
playbackGoesLive?: boolean;
@ -497,9 +499,11 @@ export class RdioScannerAdminService implements OnDestroy {
afsSystems: [options?.afsSystems, this.validateAfsSystems()],
audioConversion: [options?.audioConversion],
autoPopulate: [options?.autoPopulate],
branding: [options?.branding],
dimmerDelay: [options?.dimmerDelay, [Validators.required, Validators.min(0)]],
disableDuplicateDetection: [options?.disableDuplicateDetection],
duplicateDetectionTimeFrame: [options?.duplicateDetectionTimeFrame, [Validators.required, Validators.min(0)]],
email: [options?.email],
keypadBeeps: [options?.keypadBeeps, Validators.required],
maxClients: [options?.maxClients, [Validators.required, Validators.min(1)]],
playbackGoesLive: [options?.playbackGoesLive],

View file

@ -45,6 +45,16 @@
<mat-slide-toggle color="primary" formControlName="autoPopulate"></mat-slide-toggle>
</div>
</div>
<div class="row">
<p>
<span class="mat-body">Branding Label</span><br>
<span class="mat-caption">A simple line of text displayed above the main screen to help identify the Rdio
Scanner instance.</span>
</p>
<mat-form-field floatLabel="never">
<input type="text" matInput formControlName="branding" placeholder="Branding">
</mat-form-field>
</div>
<div class="row">
<p>
<span class="mat-body">Dimmer Delay</span><br>
@ -86,6 +96,15 @@
</mat-error>
</mat-form-field>
</div>
<div class="row">
<p>
<span class="mat-body">Email Support</span><br>
<span class="mat-caption">Email address where users can write to to get support.</span>
</p>
<mat-form-field floatLabel="never">
<input type="text" matInput formControlName="email" placeholder="Email">
</mat-form-field>
</div>
<div class="row">
<p>
<span class="mat-body">Keypad Beep Style</span><br>

View file

@ -1,6 +1,11 @@
.rdio-button,
.rdio-button-mini {
background: rgb(45, 45, 45);
--def: rgb(45, 45, 45);
--green: rgb(0, 230, 118);
--red: rgb(255, 23, 68);
--yellow: rgb(255, 234, 0);
background: var(--def);
border-style: solid;
border-width: 1px;
border-bottom-color: rgba(0, 0, 0, 0.87);
@ -41,24 +46,71 @@
right: 4px;
top: 4px;
width: 6px;
animation: blink 1s linear infinite both;
}
&.off::after {
background: rgb(255, 23, 68);
box-shadow: 1px 1px 1px rgba(255, 255, 255, 0.7) inset,
0 0 3px 1px rgb(255, 23, 68);
background: var(--red);
box-shadow: 1px 1px 1px rgba(255, 255, 255, 0.7) inset, 0 0 3px 1px var(--red);
}
&.off.blink::after {
animation: blink-off 500ms linear infinite;
}
@keyframes blink-off {
0%, 50% {
background: var(--def);
box-shadow: 1px 1px 1px rgba(255, 255, 255, 0.7) inset, 0 0 0px 0px var(--def);
}
50%, 100% {
background: var(--red);
box-shadow: 1px 1px 1px rgba(255, 255, 255, 0.7) inset, 0 0 3px 1px var(--red);
}
}
&.on::after {
background: rgb(0, 230, 118);;
box-shadow: 1px 1px 1px rgba(255, 255, 255, 0.7) inset,
0 0 3px 1px rgb(0, 230, 118);;
background: var(--green);
box-shadow: 1px 1px 1px rgba(255, 255, 255, 0.7) inset, 0 0 3px 1px var(--green);
}
&.on.blink::after {
animation: blink-on 500ms linear infinite;
}
@keyframes blink-on {
0%, 50% {
background: var(--def);
box-shadow: 1px 1px 1px rgba(255, 255, 255, 0.7) inset, 0 0 0px 0px var(--def);
}
50%, 100% {
background: var(--def);
box-shadow: 1px 1px 1px rgba(255, 255, 255, 0.7) inset, 0 0 3px 1px var(--green);
}
}
&.partial::after {
background: rgb(255, 234, 0);
box-shadow: 1px 1px 1px rgba(255, 255, 255, 0.7) inset,
0 0 3px 1px rgb(255, 234, 0);
background: var(--yellow);
box-shadow: 1px 1px 1px rgba(255, 255, 255, 0.7) inset, 0 0 3px 1px var(--yellow);
}
&.partial.blink::after {
animation: blink-partial 500ms linear infinite;
}
@keyframes blink-partial {
0%, 50% {
background: var(--def);
box-shadow: 1px 1px 1px rgba(255, 255, 255, 0.7) inset, 0 0 0px 0px var(--def);
}
50%, 100% {
background: var(--yellow);
box-shadow: 1px 1px 1px rgba(255, 255, 255, 0.7) inset, 0 0 3px 1px var(--yellow);
}
}
}

View file

@ -1,4 +1,5 @@
<div class="rdio-status">
<div class="branding">{{ branding }}</div>
<div class="led" [ngClass]="ledStyle"></div>
</div>
<div class="rdio-display" [ngClass]="{ idle: !dimmer }" (dblclick)="toggleFullscreen.emit()">
@ -53,6 +54,9 @@
</div>
</div>
<div class="row right small">
<div *ngIf="tempAvoid">
<span class="flag" [ngClass]="{ flaged: avoided || patched }">&#9202; {{ tempAvoid }}M</span>
</div>
<div>
<span class="flag" [ngClass]="{ flaged: avoided }">AVOID</span>
</div>
@ -154,3 +158,8 @@
</button>
</div>
</div>
<div class="rdio-help" *ngIf="email">
<button mat-icon-button (click)="showHelp()">
<mat-icon>help_center</mat-icon>
</button>
</div>

View file

@ -171,11 +171,30 @@
}
.rdio-status {
align-items: center;
display: flex;
flex-direction: row;
min-height: 1.5rem;
.branding {
color: rgb(64, 64, 64);
flex: 1;
font-size: 18px;
font-weight: 900;
letter-spacing: 0.25rem;
line-height: 1;
overflow: hidden;
text-overflow: ellipsis;
text-shadow: -1px -1px 1px rgb(0, 0, 0), 1px 1px 1px rgba(255, 255, 255, 0.5);
text-transform: uppercase;
white-space: nowrap;
}
.led {
background: rgb(80, 80, 80);
display: block;
height: 12px;
margin-left: auto;
margin-left: 24px;
width: 24px;
&.on {
@ -240,3 +259,10 @@
.rdio-status {
margin-bottom: 24px;
}
.rdio-help {
bottom: 24px;
opacity: 0.3;
position: absolute;
right: 24px;
}

View file

@ -20,6 +20,7 @@
import { ChangeDetectorRef, Component, EventEmitter, OnDestroy, OnInit, Output, ViewChild } from '@angular/core';
import { FormBuilder } from '@angular/forms';
import { MatInput } from '@angular/material/input';
import { MatSnackBar } from '@angular/material/snack-bar';
import { Subscription, timer } from 'rxjs';
import packageInfo from '../../../../../package.json';
import {
@ -32,6 +33,7 @@ import {
RdioScannerLivefeedMode,
} from '../rdio-scanner';
import { RdioScannerService } from '../rdio-scanner.service';
import { RdioScannerSupportComponent } from './support/support.component';
const LOCAL_STORAGE_KEY = RdioScannerService.LOCAL_STORAGE_KEY + '-pin';
@ -49,6 +51,8 @@ export class RdioScannerMainComponent implements OnDestroy, OnInit {
avoided = false;
branding = '';
call: RdioScannerCall | undefined;
callError = '0';
callFrequency: string = this.formatFrequency(0);
@ -61,20 +65,21 @@ export class RdioScannerMainComponent implements OnDestroy, OnInit {
callTag = 'Tag';
callTalkgroup = 'Talkgroup';
callTalkgroupId = '0';
/*
* BEGIN OF RED TAPE:
*
* By modifying, deleting or disabling the following lines, you harm
* the open source project and its author. Rdio Scanner represents a lot of
* investment in time, support, testing and hardware.
*
* Be respectful, sponsor the project if you can, use native apps when possible.
*
*/
//
// BEGIN OF RED TAPE:
//
// By modifying, deleting or disabling the following lines, you harm
// the open source project and its author. Rdio Scanner represents a lot of
// investment in time, support, testing and hardware.
//
// Be respectful, sponsor the project if you can, use native apps when possible.
//
callTalkgroupName = `Rdio Scanner v${packageInfo.version}`;
/**
* END OF RED TAPE.
*/
//
// END OF RED TAPE.
//
callTime = 0;
callUnit = '0';
@ -82,6 +87,8 @@ export class RdioScannerMainComponent implements OnDestroy, OnInit {
dimmer = false;
email = '';
holdSys = false;
holdTg = false;
@ -104,6 +111,8 @@ export class RdioScannerMainComponent implements OnDestroy, OnInit {
replayOffset = 0;
replayTimer: Subscription | undefined;
tempAvoid = 0;
timeFormat = 'HH:mm';
get showListenersCount(): boolean {
@ -128,6 +137,7 @@ export class RdioScannerMainComponent implements OnDestroy, OnInit {
constructor(
private rdioScannerService: RdioScannerService,
private matSnackBar: MatSnackBar,
private ngChangeDetectorRef: ChangeDetectorRef,
private ngFormBuilder: FormBuilder,
) { }
@ -145,28 +155,43 @@ export class RdioScannerMainComponent implements OnDestroy, OnInit {
}
avoid(options?: RdioScannerAvoidOptions): void {
const call = this.call || this.callPrevious;
if (this.auth) {
this.authFocus();
} else {
const call = this.call || this.callPrevious;
if (options || call) {
} else if (options || call) {
if (options) {
this.rdioScannerService.avoid(options);
} else if (call) {
const avoided = this.rdioScannerService.isAvoided(call);
const minutes = this.rdioScannerService.isAvoidedTimer(call);
if (call && !this.map[call.system][call.talkgroup]) {
if (!avoided) {
this.rdioScannerService.avoid({ status: false });
} else if (!minutes) {
this.rdioScannerService.avoid({ minutes: 30, status: false });
} else if (minutes === 30) {
this.rdioScannerService.avoid({ minutes: 60, status: false });
} else if (minutes === 60) {
this.rdioScannerService.avoid({ minutes: 120, status: false });
} else {
this.rdioScannerService.avoid({ status: true });
}
}
if (call && this.rdioScannerService.isAvoided(call)) {
this.rdioScannerService.beep(RdioScannerBeepStyle.Activate);
} else {
this.rdioScannerService.beep(RdioScannerBeepStyle.Deactivate);
}
this.updateDimmer();
} else {
this.rdioScannerService.beep(RdioScannerBeepStyle.Denied);
}
this.updateDimmer();
}
}
holdSystem(): void {
@ -286,6 +311,13 @@ export class RdioScannerMainComponent implements OnDestroy, OnInit {
}
}
showHelp(): void {
this.matSnackBar.openFromComponent(RdioScannerSupportComponent, {
data: { email: this.email },
panelClass: 'snackbar-white',
});
}
showSearchPanel(): void {
if (!this.config) {
return;
@ -378,6 +410,10 @@ export class RdioScannerMainComponent implements OnDestroy, OnInit {
if ('config' in event) {
this.config = event.config;
this.branding = this.config?.branding ?? '';
this.email = this.config?.email ?? '';
this.timeFormat = this.config?.time12hFormat ? 'h:mm a' : 'HH:mm';
const password = this.authForm.get('password')?.value;
@ -566,9 +602,12 @@ export class RdioScannerMainComponent implements OnDestroy, OnInit {
const call = this.call || this.callPrevious;
if (call) {
this.tempAvoid = this.rdioScannerService.isAvoidedTimer(call);
if (this.rdioScannerService.isPatched(call)) {
this.avoided = false;
this.patched = true;
} else {
this.avoided = this.rdioScannerService.isAvoided(call);
this.patched = false;

View file

@ -0,0 +1,10 @@
<div>
<ng-container *ngIf="email">
For further assistance please contact <b><a href="mailto:{{ email }}" target="_blank">{{ email }}</a></b>
</ng-container>
<ng-container *ngIf="!email">
For further assistance please go to <b><a href="https://github.com/chuot/rdio-scanner/discussions"
target="_blank">Rdio&nbsp;Scanner&nbsp;discussions</a></b>
</ng-container>
</div>
<div class="countdown">{{ countdown }}</div>

View file

@ -0,0 +1,26 @@
:host {
align-items: center;
display: flex;
flex-direction: column;
> div {
text-align: center;
}
}
a {
color: currentColor;
text-decoration: none;
}
b {
font-weight: 500;
}
.countdown {
bottom: 4px;
color: rgba(0, 0, 0, 0.67);
font-size: 12px;
position: absolute;
right: 4px;
}

View file

@ -0,0 +1,55 @@
/*
* *****************************************************************************
* Copyright (C) 2019-2022 Chrystian Huot <chrystian.huot@saubeo.solutions>
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <http://www.gnu.org/licenses/>
* ****************************************************************************
*/
import { Component, Inject, Optional } from '@angular/core';
import { MAT_SNACK_BAR_DATA, MatSnackBarRef } from '@angular/material/snack-bar';
import { timer } from 'rxjs';
@Component({
selector: 'RdioScannerSupport',
styleUrls: ['./support.component.scss'],
templateUrl: './support.component.html',
})
export class RdioScannerSupportComponent {
countdown: number = 10;
email: string | undefined;
constructor(
@Optional() private matSnackBarRef: MatSnackBarRef<RdioScannerSupportComponent>,
@Inject(MAT_SNACK_BAR_DATA) public data: { email: string },
) {
this.email = data?.email;
this.wait();
}
private wait(): void {
timer(1000).subscribe(() => {
this.countdown--;
if (this.countdown < 1) {
this.matSnackBarRef?.dismiss();
} else {
this.wait();
}
});
}
}

View file

@ -23,7 +23,7 @@ import { timer } from 'rxjs';
@Component({
selector: 'RdioScannerNative',
styleUrls: ['./native.scss'],
styleUrls: ['./native.component.scss'],
templateUrl: './native.component.html',
})
export class RdioScannerNativeComponent implements OnInit {
@ -34,8 +34,7 @@ export class RdioScannerNativeComponent implements OnInit {
isAndroid: boolean = false;
isApple: boolean = false;
constructor(@Optional() private matSnackBarRef: MatSnackBarRef<RdioScannerNativeComponent>) {
}
constructor(@Optional() private matSnackBarRef: MatSnackBarRef<RdioScannerNativeComponent>) { }
ngOnInit(): void {
const ua = navigator.userAgent;

View file

@ -6,6 +6,7 @@ $background-color: rgb(30, 30, 30);
display: block;
height: 100%;
min-width: 320px;
position: relative;
.mat-sidenav {
background-color: inherit;

View file

@ -23,6 +23,7 @@ import { AppSharedModule } from '../../shared/shared.module';
import { RdioScannerComponent } from './rdio-scanner.component';
import { RdioScannerService } from './rdio-scanner.service';
import { RdioScannerMainComponent } from './main/main.component';
import { RdioScannerSupportComponent } from './main/support/support.component';
import { RdioScannerNativeModule } from './native/native.module';
import { RdioScannerSearchComponent } from './search/search.component';
import { RdioScannerSelectComponent } from './select/select.component';
@ -33,9 +34,13 @@ import { RdioScannerSelectComponent } from './select/select.component';
RdioScannerMainComponent,
RdioScannerSearchComponent,
RdioScannerSelectComponent,
RdioScannerSupportComponent,
],
exports: [RdioScannerComponent],
imports: [AppSharedModule, RdioScannerNativeModule],
imports: [
AppSharedModule,
RdioScannerNativeModule,
],
providers: [
RdioScannerService,
{ provide: OverlayContainer, useClass: FullscreenOverlayContainer },

View file

@ -31,6 +31,7 @@ import {
RdioScannerCategoryType,
RdioScannerConfig,
RdioScannerEvent,
RdioScannerLivefeed,
RdioScannerLivefeedMap,
RdioScannerLivefeedMode,
RdioScannerPlaybackList,
@ -57,6 +58,7 @@ enum WebsocketCommand {
LivefeedMap = 'LFM',
Max = 'MAX',
Pin = 'PIN',
Version = 'VER',
}
@Injectable()
@ -100,8 +102,6 @@ export class RdioScannerService implements OnDestroy {
private playbackPending: number | undefined;
private playbackRefreshing = false;
private replayDelay: Subscription | undefined;
private skipDelay: Subscription | undefined;
private websocket: WebSocket | undefined;
@ -122,6 +122,29 @@ export class RdioScannerService implements OnDestroy {
}
avoid(options: RdioScannerAvoidOptions = {}): void {
const clearTimer = (lfm: RdioScannerLivefeed): void => {
lfm.minutes = undefined;
lfm.timer?.unsubscribe();
lfm.timer = undefined;
};
const setTimer = (lfm: RdioScannerLivefeed, minutes: number): void => {
lfm.minutes = minutes;
lfm.timer = timer(minutes * 60 * 1000).subscribe(() => {
lfm.active = true;
lfm.minutes = undefined;
lfm.timer = undefined;
this.rebuildCategories();
this.storeLivefeedMap();
this.event.emit({
categories: this.categories,
map: this.livefeedMap,
});
});
};
if (this.livefeedMapPriorToHoldSystem) {
this.livefeedMapPriorToHoldSystem = undefined;
}
@ -133,37 +156,39 @@ export class RdioScannerService implements OnDestroy {
if (typeof options.all === 'boolean') {
Object.keys(this.livefeedMap).map((sys: string) => +sys).forEach((sys: number) => {
Object.keys(this.livefeedMap[sys]).map((tg: string) => +tg).forEach((tg: number) => {
this.livefeedMap[sys][tg] = typeof options.status === 'boolean' ? options.status : !!options.all;
const lfm = this.livefeedMap[sys][tg];
clearTimer(lfm);
lfm.active = typeof options.status === 'boolean' ? options.status : !!options.all;
});
});
} else if (options.call) {
const sys = options.call.system;
const tg = options.call.talkgroup;
this.livefeedMap[sys][tg] = typeof options.status === 'boolean' ? options.status : !this.livefeedMap[sys][tg];
const lfm = this.livefeedMap[options.call.system][options.call.talkgroup];
clearTimer(lfm);
lfm.active = typeof options.status === 'boolean' ? options.status : !lfm.active;
if (typeof options.minutes === 'number') setTimer(lfm, options.minutes);
} else if (options.system && options.talkgroup) {
const sys = options.system.id;
const tg = options.talkgroup.id;
this.livefeedMap[sys][tg] = typeof options.status === 'boolean' ? options.status : !this.livefeedMap[sys][tg];
const lfm = this.livefeedMap[options.system.id][options.talkgroup.id];
clearTimer(lfm);
lfm.active = typeof options.status === 'boolean' ? options.status : !lfm.active;
if (typeof options.minutes === 'number') setTimer(lfm, options.minutes);
} else if (options.system && !options.talkgroup) {
const sys = options.system.id;
Object.keys(this.livefeedMap[sys]).map((tg: string) => +tg).forEach((tg: number) => {
this.livefeedMap[sys][tg] = typeof options.status === 'boolean' ? options.status : !this.livefeedMap[sys][tg];
const lfm = this.livefeedMap[sys][tg];
clearTimer(lfm);
lfm.active = typeof options.status === 'boolean' ? options.status : !lfm.active;
});
} else {
const call = this.call || this.callPrevious;
if (call) {
const sys = call.system;
const tg = call.talkgroup;
this.livefeedMap[sys][tg] = typeof options.status === 'boolean' ? options.status : !this.livefeedMap[sys][tg];
const lfm = this.livefeedMap[call.system][call.talkgroup];
clearTimer(lfm);
lfm.active = typeof options.status === 'boolean' ? options.status : !lfm.active;
if (typeof options.minutes === 'number') setTimer(lfm, options.minutes);
}
}
@ -243,18 +268,20 @@ export class RdioScannerService implements OnDestroy {
this.livefeedMapPriorToHoldSystem = this.livefeedMap;
this.livefeedMap = Object.keys(this.livefeedMap).map((sys) => +sys).reduce((sysMap, sys) => {
const allOn = Object.keys(this.livefeedMap[sys]).every((tg) => !this.livefeedMap[sys][tg]);
const allOn = Object.keys(this.livefeedMap[sys]).map((tg) => +tg).every((tg) => !this.livefeedMap[sys][tg]);
sysMap[sys] = Object.keys(this.livefeedMap[sys]).map((tg) => +tg).reduce((tgMap, tg) => {
if (sys === call.system) {
tgMap[tg] = allOn || this.livefeedMap[sys][tg];
tgMap[tg].minutes = undefined;
tgMap[tg].timer?.unsubscribe();
tgMap[tg].timer = undefined;
} else {
tgMap[tg] = false;
}
if (sys === call.system)
tgMap[tg].active = allOn || this.livefeedMap[sys][tg].active;
else
tgMap[tg].active = false;
return tgMap;
}, {} as { [key: number]: boolean });
}, {} as { [key: number]: RdioScannerLivefeed });
return sysMap;
}, {} as RdioScannerLivefeedMap);
@ -298,15 +325,17 @@ export class RdioScannerService implements OnDestroy {
this.livefeedMap = Object.keys(this.livefeedMap).map((sys) => +sys).reduce((sysMap, sys) => {
sysMap[sys] = Object.keys(this.livefeedMap[sys]).map((tg) => +tg).reduce((tgMap, tg) => {
if (sys === call.system) {
tgMap[tg] = tg === call.talkgroup;
tgMap[tg].minutes = undefined;
tgMap[tg].timer?.unsubscribe();
tgMap[tg].timer = undefined;
} else {
tgMap[tg] = false;
}
if (sys === call.system)
tgMap[tg].active = tg === call.talkgroup;
else
tgMap[tg].active = false;
return tgMap;
}, {} as { [key: number]: boolean });
}, {} as { [key: number]: RdioScannerLivefeed });
return sysMap;
}, {} as RdioScannerLivefeedMap);
@ -333,12 +362,19 @@ export class RdioScannerService implements OnDestroy {
}
isAvoided(call: RdioScannerCall): boolean {
return !!this.livefeedMap[call.system] && this.livefeedMap[call.system][call.talkgroup] === false;
return !!this.livefeedMap[call.system] && this.livefeedMap[call.system][call.talkgroup]?.active !== true;
}
isAvoidedTimer(call: RdioScannerCall): number {
if (!!this.livefeedMap[call.system] && this.livefeedMap[call.system][call.talkgroup]?.minutes !== undefined) {
return this.livefeedMap[call.system][call.talkgroup]?.minutes || 0;
}
return 0;
}
isPatched(call: RdioScannerCall): boolean {
return this.isAvoided(call) && call.patches.some((tg) => {
return !!this.livefeedMap[call.system] && this.livefeedMap[call.system][tg];
return !!this.livefeedMap[call.system] && this.livefeedMap[call.system][tg]?.active || false;
});
}
@ -543,11 +579,19 @@ export class RdioScannerService implements OnDestroy {
}
startLivefeed(): void {
const lfm = Object.keys(this.livefeedMap).reduce((sysMap: { [key: number]: { [key: number]: boolean } }, sys) => {
sysMap[+sys] = Object.keys(this.livefeedMap[+sys]).reduce((tgMap: { [key: number]: boolean }, tg: string) => {
tgMap[+tg] = this.livefeedMap[+sys][+tg].active;
return tgMap;
}, {});
return sysMap;
}, {});
this.livefeedMode = RdioScannerLivefeedMode.Online;
this.event.emit({ livefeedMode: this.livefeedMode });
this.sendtoWebsocket(WebsocketCommand.LivefeedMap, this.livefeedMap);
this.sendtoWebsocket(WebsocketCommand.LivefeedMap, lfm);
}
stop(options?: { emit?: boolean }): void {
@ -595,6 +639,12 @@ export class RdioScannerService implements OnDestroy {
}
toggleCategory(category: RdioScannerCategory): void {
const clearTimer = (lfm: RdioScannerLivefeed): void => {
lfm.minutes = 0;
lfm.timer?.unsubscribe();
lfm.timer = undefined;
};
if (category) {
if (this.livefeedMapPriorToHoldSystem) {
this.livefeedMapPriorToHoldSystem = undefined;
@ -608,20 +658,22 @@ export class RdioScannerService implements OnDestroy {
this.config?.systems.forEach((sys) => {
sys.talkgroups?.forEach((tg) => {
if (category.type == RdioScannerCategoryType.Group && tg.group === category.label) {
this.livefeedMap[sys.id][tg.id] = status;
const lfm = this.livefeedMap[sys.id][tg.id];
if (category.type == RdioScannerCategoryType.Group && tg.group === category.label) {
clearTimer(lfm);
lfm.active = status;
} else if (category.type == RdioScannerCategoryType.Tag && tg.tag === category.label) {
this.livefeedMap[sys.id][tg.id] = status;
clearTimer(lfm);
lfm.active = status;
}
});
});
this.rebuildCategories();
if (this.call && !this.livefeedMap[this.call.system] &&
this.livefeedMap[this.call.system][this.call.talkgroup]) {
if (this.call && !this.livefeedMap[this.call.system] && this.livefeedMap[this.call.system][this.call.talkgroup]) {
clearTimer(this.livefeedMap[this.call.system][this.call.talkgroup]);
this.skip();
}
@ -691,7 +743,7 @@ export class RdioScannerService implements OnDestroy {
private cleanQueue(): void {
let isActive = (call: RdioScannerCall) => {
let lfm = (sys: number, tg: number) => this.livefeedMap && this.livefeedMap[sys] && this.livefeedMap[sys][tg];
let lfm = (sys: number, tg: number): boolean => this.livefeedMap && this.livefeedMap[sys] && this.livefeedMap[sys][tg]?.active;
let active = lfm(call.system, call.talkgroup);
if (!active && Array.isArray(call.patches)) {
for (let i = 0; i < call.patches.length; i++) {
@ -793,6 +845,7 @@ export class RdioScannerService implements OnDestroy {
this.websocket.onmessage = (ev: MessageEvent) => this.parseWebsocketMessage(ev.data);
}
this.sendtoWebsocket(WebsocketCommand.Version);
this.sendtoWebsocket(WebsocketCommand.Config);
};
}
@ -831,7 +884,9 @@ export class RdioScannerService implements OnDestroy {
const config = message[1];
this.config = {
branding: typeof config.branding === 'string' ? config.branding : '',
dimmerDelay: typeof config.dimmerDelay === 'number' ? config.dimmerDelay : 5000,
email: typeof config.email === 'string' ? config.email : '',
groups: typeof config.groups !== null && typeof config.groups === 'object' ? config.groups : {},
keypadBeeps: config.keypadBeeps !== null && typeof config.keypadBeeps === 'object' ? config.keypadBeeps : {},
playbackGoesLive: typeof config.playbackGoesLive === 'boolean' ? config.playbackGoesLive : false,
@ -897,6 +952,28 @@ export class RdioScannerService implements OnDestroy {
case WebsocketCommand.Pin:
this.event.emit({ auth: true });
break;
case WebsocketCommand.Version:
var data = message[1];
if (data !== null && typeof data === 'object') {
var branding = data['branding'];
var email = data['email'];
if (typeof branding === 'string') {
this.config.branding = branding;
}
if (typeof email === 'string') {
this.config.email = email;
}
if (this.config.branding || this.config.email) {
this.event.emit({ config: this.config });
}
}
break;
}
}
@ -969,11 +1046,11 @@ export class RdioScannerService implements OnDestroy {
this.categories = Object.keys(this.config.groups || []).map((label) => {
const allOff = Object.keys(this.config.groups[label]).map((sys) => +sys)
.every((sys: number) => this.config.groups[label] && this.config.groups[label][sys]
.every((tg) => this.livefeedMap[sys] && !this.livefeedMap[sys][tg]));
.every((tg) => this.livefeedMap[sys] && !this.livefeedMap[sys][tg].active));
const allOn = Object.keys(this.config.groups[label]).map((sys) => +sys)
.every((sys: number) => this.config.groups[label] && this.config.groups[label][sys]
.every((tg) => this.livefeedMap[sys] && this.livefeedMap[sys][tg]));
.every((tg) => this.livefeedMap[sys] && this.livefeedMap[sys][tg].active));
const status = allOff ? RdioScannerCategoryStatus.Off : allOn ? RdioScannerCategoryStatus.On : RdioScannerCategoryStatus.Partial;
@ -984,11 +1061,11 @@ export class RdioScannerService implements OnDestroy {
this.categories = this.categories.concat(Object.keys(this.config.tags || []).map((label) => {
const allOff = Object.keys(this.config.tags[label]).map((sys) => +sys)
.every((sys: number) => this.config.tags[label] && this.config.tags[label][sys]
.every((tg) => this.livefeedMap[sys] && !this.livefeedMap[sys][tg]));
.every((tg) => this.livefeedMap[sys] && !this.livefeedMap[sys][tg].active));
const allOn = Object.keys(this.config.tags[label]).map((sys) => +sys)
.every((sys: number) => this.config.tags[label] && this.config.tags[label][sys]
.every((tg) => this.livefeedMap[sys] && this.livefeedMap[sys][tg]));
.every((tg) => this.livefeedMap[sys] && this.livefeedMap[sys][tg].active));
const status = allOff ? RdioScannerCategoryStatus.Off : allOn ? RdioScannerCategoryStatus.On : RdioScannerCategoryStatus.Partial;
@ -1000,24 +1077,28 @@ export class RdioScannerService implements OnDestroy {
}
private rebuildLivefeedMap(): void {
const livefeedMap = this.config.systems.reduce((sysMap, sys) => {
const lfm = this.config.systems.reduce((sysMap, sys) => {
sysMap[sys.id] = sys.talkgroups.reduce((tgMap, tg) => {
const state = this.livefeedMap && this.livefeedMap[sys.id] && this.livefeedMap[sys.id][tg.id];
const group = this.categories.find((cat) => cat.label === tg.group);
const tag = this.categories.find((cat) => cat.label === tg.tag);
tgMap[tg.id] = typeof state === 'boolean' ? state : true;
tgMap[tg.id] = (this.livefeedMap[sys.id] && this.livefeedMap[sys.id][tg.id])
? this.livefeedMap[sys.id][tg.id]
: {
active: !(group?.status === RdioScannerCategoryStatus.Off || tag?.status === RdioScannerCategoryStatus.Off),
} as RdioScannerLivefeed;
return tgMap;
}, sysMap[sys.id] || {});
}, sysMap[sys.id] || {} as { [key: number]: RdioScannerLivefeed });
return sysMap;
}, {} as RdioScannerLivefeedMap);
if (this.livefeedMapPriorToHoldSystem != null) {
this.livefeedMapPriorToHoldSystem = livefeedMap;
this.livefeedMapPriorToHoldSystem = lfm;
} else if (this.livefeedMapPriorToHoldTalkgroup != null) {
this.livefeedMapPriorToHoldTalkgroup = livefeedMap;
this.livefeedMapPriorToHoldTalkgroup = lfm;
} else {
this.livefeedMap = livefeedMap;
this.livefeedMap = lfm;
}
this.storeLivefeedMap();
@ -1032,16 +1113,17 @@ export class RdioScannerService implements OnDestroy {
}
private restoreLivefeed(): void {
const map = window?.localStorage?.getItem(RdioScannerService.LOCAL_STORAGE_KEY);
if (map) {
try {
this.livefeedMap = JSON.parse(map);
const lfm = JSON.parse(window?.localStorage?.getItem(RdioScannerService.LOCAL_STORAGE_KEY) || "{}");
} catch (err) {
this.livefeedMap = {};
}
}
Object.keys(lfm).forEach((sys) => {
Object.keys(lfm[sys]).forEach((tg) => {
if (!this.livefeedMap[+sys]) this.livefeedMap[+sys] = {};
if (!this.livefeedMap[+sys][+tg]) this.livefeedMap[+sys][+tg] = {} as RdioScannerLivefeed;
this.livefeedMap[+sys][+tg].active = lfm[sys][tg];
});
});
} catch (_) { }
}
private sendtoWebsocket(command: string, payload?: unknown, flags?: string): void {
@ -1061,7 +1143,15 @@ export class RdioScannerService implements OnDestroy {
}
private storeLivefeedMap(): void {
window?.localStorage?.setItem(RdioScannerService.LOCAL_STORAGE_KEY, JSON.stringify(this.livefeedMap));
const lfm = Object.keys(this.livefeedMap).reduce((sysMap: { [key: number]: { [key: number]: boolean } }, sys: string) => {
sysMap[+sys] = Object.keys(this.livefeedMap[+sys]).reduce((tgMap: { [key: number]: boolean }, tg: string) => {
tgMap[+tg] = this.livefeedMap[+sys][+tg].active;
return tgMap;
}, {});
return sysMap;
}, {});
window?.localStorage?.setItem(RdioScannerService.LOCAL_STORAGE_KEY, JSON.stringify(lfm));
}
private transformCall(call: RdioScannerCall): RdioScannerCall {

View file

@ -17,12 +17,15 @@
* ****************************************************************************
*/
import { Subscription } from "rxjs";
export interface RdioScannerAvoidOptions {
all?: boolean;
call?: RdioScannerCall;
minutes?: number;
status?: boolean;
system?: RdioScannerSystem;
talkgroup?: RdioScannerTalkgroup;
status?: boolean;
}
export interface RdioScannerBeep {
@ -90,7 +93,9 @@ export enum RdioScannerCategoryType {
export interface RdioScannerConfig {
afs?: string;
branding?: string;
dimmerDelay: number | false;
email?: string;
groups: { [key: string]: { [key: number]: number[] } };
keypadBeeps: RdioScannerKeypadBeeps | false;
playbackGoesLive: boolean;
@ -127,9 +132,15 @@ export interface RdioScannerKeypadBeeps {
[RdioScannerBeepStyle.Denied]: RdioScannerBeep[];
}
export interface RdioScannerLivefeed {
active: boolean;
minutes: number | undefined;
timer: Subscription | undefined;
}
export interface RdioScannerLivefeedMap {
[key: string]: {
[key: string]: boolean;
[key: number]: {
[key: number]: RdioScannerLivefeed;
};
}

View file

@ -19,7 +19,7 @@
</legend>
<div>
<button *ngFor="let talkgroup of system.talkgroups" class="rdio-button"
[ngClass]="{ off: !(map[system.id] && map[system.id][talkgroup.id]), on: map[system.id] && map[system.id][talkgroup.id] }"
[ngClass]="{ off: !(map[system.id] && map[system.id][talkgroup.id])?.active, on: map[system.id] && map[system.id][talkgroup.id]?.active, blink: map[system.id][talkgroup.id]?.minutes }"
(click)="avoid({ system: system, talkgroup: talkgroup })">
{{ talkgroup.label }}
</button>

View file

@ -42,8 +42,8 @@ ALWAYS DOWNLOAD THE LATEST VERSION OF [RDIO SCANNER](https://github.com/chuot/rd
rdio@macos ~ % mkdir rdio-scanner
rdio@macos ~ % cd rdio-scanner
rdio@macos rdio-scanner % unzip \
> ~/Downloads/rdio-scanner-darwin-arm64-v6.4.5.zip
Archive: /Users/rdio/Downloads/rdio-scanner-darwin-arm64-v6.4.5.zip
> ~/Downloads/rdio-scanner-darwin-arm64-v6.5.0.zip
Archive: /Users/rdio/Downloads/rdio-scanner-darwin-arm64-v6.5.0.zip
inflating: rdio-scanner
inflating: rdio-scanner.pdf
@ -51,11 +51,11 @@ ALWAYS DOWNLOAD THE LATEST VERSION OF [RDIO SCANNER](https://github.com/chuot/rd
rdio@macos rdio-scanner % ./rdio-scanner
Rdio Scanner v6.4.5
Rdio Scanner v6.5.0
----------------------------------
2022/06/14 08:38:06 server started
2022/06/14 08:38:06 main interface at http://macos.local:3000
2022/06/14 08:38:06 admin interface at http://macos.local:3000/admin
2022/06/27 08:38:06 server started
2022/06/27 08:38:06 main interface at http://macos.local:3000
2022/06/27 08:38:06 admin interface at http://macos.local:3000/admin
4. Access the administrative dashboard to finalize the configuration.
@ -77,11 +77,11 @@ Here we want our [Rdio Scanner](https://guthub.com/chuot/rdio-scanner) instance
rdio@macos rdio-scanner % ./rdio-scanner --listen :80
Rdio Scanner v6.4.5
Rdio Scanner v6.5.0
----------------------------------
2022/06/14 08:48:03 server started
2022/06/14 08:48:03 main interface at http://macos.local
2022/06/14 08:48:03 admin interface at http://macos.local/admin
2022/06/27 08:48:03 server started
2022/06/27 08:48:03 main interface at http://macos.local
2022/06/27 08:48:03 admin interface at http://macos.local/admin
## Listening on a SSL port
@ -95,12 +95,12 @@ You can use your own SSL certificates that match your domain name with `-ssl_cer
> -ssl_key_file mykey.key \
> -ssl_listen :443
Rdio Scanner v6.4.5
Rdio Scanner v6.5.0
----------------------------------
2022/06/14 08:50:58 server started
2022/06/14 08:50:58 main interface at http://macos.local
2022/06/14 08:50:58 main interface at https://macos.local
2022/06/14 08:50:58 admin interface at https://macos.local/admin
2022/06/27 08:50:58 server started
2022/06/27 08:50:58 main interface at http://macos.local
2022/06/27 08:50:58 main interface at https://macos.local
2022/06/27 08:50:58 admin interface at https://macos.local/admin
If you don't want to worry about SSL certificates, you can use the built-in Let's Encrypt auto-cert feature. This requires that you have both port 80 (HTTP) and port 443 (HTTPS) open to the world. Also, your domain name should point to your IP address where [Rdio Scanner](https://github.com/chuot/rdio-scanner/) is running. The advantage of this approach is that everything is done automatically, no certificate request, no certificate renewal.
@ -118,7 +118,7 @@ You don't want to have to type everytime a long list of arguments. No problem, y
> -ssl_auto_cert mydomain.com \
> -ssl_listen :443 \
> -config_save
2022/06/14 08:52:24 rdio-scanner.ini file created
2022/06/27 08:52:24 rdio-scanner.ini file created
All of your parameters passed as arguments to [Rdio Scanner](https://github.com/chuot/rdio-scanner) have been saved to an INI file which has the same arguments/values list.
@ -132,12 +132,12 @@ Then simply run [Rdio Scanner](https://github.com/chuot/rdio-scanner) without an
rdio@macos rdio-scanner % ./rdio-scanner
Rdio Scanner v6.4.5
Rdio Scanner v6.5.0
----------------------------------
2022/06/14 08:54:08 server started
2022/06/14 08:54:08 main interface at http://macos.local
2022/06/14 08:54:08 main interface at https://macos.local
2022/06/14 08:54:08 admin interface at https://macos.local/admin
2022/06/27 08:54:08 server started
2022/06/27 08:54:08 main interface at http://macos.local
2022/06/27 08:54:08 main interface at https://macos.local
2022/06/27 08:54:08 admin interface at https://macos.local/admin
## Install Rdio Scanner as a service

View file

@ -42,8 +42,8 @@ ALWAYS DOWNLOAD THE LATEST VERSION OF [RDIO SCANNER](https://github.com/chuot/rd
rdio@pc-freebsd:~ $ mkdir rdio-scanner
rdio@pc-freebsd:~ $ cd rdio-scanner
rdio@pc-freebsd:~/rdio-scanner $ unzip \
> ~/rdio-scanner-freebsd-amd64-v6.4.5.zip
Archive: ../rdio-scanner-freebsd-amd64-v6.4.5.zip
> ~/rdio-scanner-freebsd-amd64-v6.5.0.zip
Archive: ../rdio-scanner-freebsd-amd64-v6.5.0.zip
extracting: rdio-scanner
extracting: rdio-scanner.pdf
@ -51,11 +51,11 @@ ALWAYS DOWNLOAD THE LATEST VERSION OF [RDIO SCANNER](https://github.com/chuot/rd
rdio@pc-freebsd:~/rdio-scanner $ ./rdio-scanner
Rdio Scanner v6.4.5
Rdio Scanner v6.5.0
----------------------------------
2022/06/14 08:16:36 server started
2022/06/14 08:16:36 main interface at http://pc-freebsd:3000
2022/06/14 08:16:36 admin interface at http://pc-freebsd:3000/admin
2022/06/27 08:16:36 server started
2022/06/27 08:16:36 main interface at http://pc-freebsd:3000
2022/06/27 08:16:36 admin interface at http://pc-freebsd:3000/admin
4. Access the administrative dashboard to finalize the configuration.
@ -81,11 +81,11 @@ Here we want our [Rdio Scanner](https://guthub.com/chuot/rdio-scanner) instance
Password:
root@pc-freebsd:/home/rdio/rdio-scanner # ./rdio-scanner -listen :80
Rdio Scanner v6.4.5
Rdio Scanner v6.5.0
----------------------------------
2022/06/14 08:19:38 server started
2022/06/14 08:19:38 main interface at http://pc-freebsd
2022/06/14 08:19:38 admin interface at http://pc-freebsd/admin
2022/06/27 08:19:38 server started
2022/06/27 08:19:38 main interface at http://pc-freebsd
2022/06/27 08:19:38 admin interface at http://pc-freebsd/admin
## Listening on a SSL port
@ -101,12 +101,12 @@ You can use your own SSL certificates that match your domain name with `-ssl_cer
> -ssl_key_file mykey.key \
> -ssl_listen :443
Rdio Scanner v6.4.5
Rdio Scanner v6.5.0
----------------------------------
2022/06/14 10:34:29 server started
2022/06/14 10:34:29 main interface at http://pc-freebsd
2022/06/14 10:34:29 main interface at https://pc-freebsd
2022/06/14 10:34:29 admin interface at https://pc-freebsd/admin
2022/06/27 10:34:29 server started
2022/06/27 10:34:29 main interface at http://pc-freebsd
2022/06/27 10:34:29 main interface at https://pc-freebsd
2022/06/27 10:34:29 admin interface at https://pc-freebsd/admin
If you don't want to worry about SSL certificates, you can use the built-in Let's Encrypt auto-cert feature. This requires that you have both port 80 (HTTP) and port 443 (HTTPS) open to the world. Also, your domain name should point to your IP address where [Rdio Scanner](https://github.com/chuot/rdio-scanner/) is running. The advantage of this approach is that everything is done automatically, no certificate request, no certificate renewal.
@ -126,7 +126,7 @@ You don't want to have to type everytime a long list of arguments. No problem, y
> -ssl_auto_cert mydomain.com \
> -ssl_listen :443 \
> -config_save
2022/06/14 10:37:00 rdio-scanner.ini file created
2022/06/27 10:37:00 rdio-scanner.ini file created
All of your parameters passed as arguments to [Rdio Scanner](https://github.com/chuot/rdio-scanner) have been saved to an INI file which has the same arguments/values list.
@ -142,12 +142,12 @@ Then simply run [Rdio Scanner](https://github.com/chuot/rdio-scanner) without an
Password:
root@pc-freebsd:/home/rdio/rdio-scanner # ./rdio-scanner
Rdio Scanner v6.4.5
Rdio Scanner v6.5.0
----------------------------------
2022/06/14 10:38:28 server started
2022/06/14 10:38:29 main interface at http://pc-freebsd
2022/06/14 10:38:29 main interface at https://pc-freebsd
2022/06/14 10:38:29 admin interface at https://pc-freebsd/admin
2022/06/27 10:38:28 server started
2022/06/27 10:38:29 main interface at http://pc-freebsd
2022/06/27 10:38:29 main interface at https://pc-freebsd
2022/06/27 10:38:29 admin interface at https://pc-freebsd/admin
## Install Rdio Scanner as a service

View file

@ -42,8 +42,8 @@ ALWAYS DOWNLOAD THE LATEST VERSION OF [RDIO SCANNER](https://github.com/chuot/rd
[rdio@pc-linux ~]$ mkdir rdio-scanner
[rdio@pc-linux ~]$ cd rdio-scanner
[rdio@pc-linux rdio-scanner]$ unzip \
> ~/Downloads/rdio-scanner-linux-amd64-v6.4.5.zip
Archive: /home/rdio/Downloads/rdio-scanner-linux-amd64-v6.4.5.zip
> ~/Downloads/rdio-scanner-linux-amd64-v6.5.0.zip
Archive: /home/rdio/Downloads/rdio-scanner-linux-amd64-v6.5.0.zip
inflating: rdio-scanner
inflating: rdio-scanner.pdf
@ -51,11 +51,11 @@ ALWAYS DOWNLOAD THE LATEST VERSION OF [RDIO SCANNER](https://github.com/chuot/rd
[rdio@pc-linux rdio-scanner]$ ./rdio-scanner
Rdio Scanner v6.4.5
Rdio Scanner v6.5.0
----------------------------------
2022/06/14 09:11:48 server started
2022/06/14 09:11:48 main interface at http://pc-linux:3000
2022/06/14 09:11:48 admin interface at http://pc-linux:3000/admin
2022/06/27 09:11:48 server started
2022/06/27 09:11:48 main interface at http://pc-linux:3000
2022/06/27 09:11:48 admin interface at http://pc-linux:3000/admin
4. Access the administrative dashboard to finalize the configuration.
@ -78,11 +78,11 @@ Here we want our [Rdio Scanner](https://guthub.com/chuot/rdio-scanner) instance
[rdio@pc-linux rdio-scanner]$ sudo ./rdio-scanner -listen :80
[sudo] password for rdio:
Rdio Scanner v6.4.5
Rdio Scanner v6.5.0
----------------------------------
2022/06/14 09:14:00 server started
2022/06/14 09:14:00 main interface at http://pc-linux
2022/06/14 09:14:00 admin interface at http://pc-linux/admin
2022/06/27 09:14:00 server started
2022/06/27 09:14:00 main interface at http://pc-linux
2022/06/27 09:14:00 admin interface at http://pc-linux/admin
## Listening on a SSL port
@ -97,12 +97,12 @@ You can use your own SSL certificates that match your domain name with `-ssl_cer
> -ssl_listen :443
[sudo] password for rdio:
Rdio Scanner v6.4.5
Rdio Scanner v6.5.0
----------------------------------
2022/06/14 09:16:47 server started
2022/06/14 09:16:47 main interface at http://pc-linux
2022/06/14 09:16:47 main interface at https://pc-linux
2022/06/14 09:16:47 admin interface at https://pc-linux/admin
2022/06/27 09:16:47 server started
2022/06/27 09:16:47 main interface at http://pc-linux
2022/06/27 09:16:47 main interface at https://pc-linux
2022/06/27 09:16:47 admin interface at https://pc-linux/admin
If you don't want to worry about SSL certificates, you can use the built-in Let's Encrypt auto-cert feature. This requires that you have both port 80 (HTTP) and port 443 (HTTPS) open to the world. Also, your domain name should point to your IP address where [Rdio Scanner](https://github.com/chuot/rdio-scanner/) is running. The advantage of this approach is that everything is done automatically, no certificate request, no certificate renewal.
@ -120,7 +120,7 @@ You don't want to have to type everytime a long list of arguments. No problem, y
> -ssl_auto_cert mydomain.com \
> -ssl_listen :443 \
> -config_save
2022/06/14 09:19:29 rdio-scanner.ini file created
2022/06/27 09:19:29 rdio-scanner.ini file created
All of your parameters passed as arguments to [Rdio Scanner](https://github.com/chuot/rdio-scanner) have been saved to an INI file which has the same arguments/values list.
@ -135,12 +135,12 @@ Then simply run [Rdio Scanner](https://github.com/chuot/rdio-scanner) without an
[rdio@pc-linux rdio-scanner]$ sudo ./rdio-scanner
[sudo] Mot de passe de rdio :
Rdio Scanner v6.4.5
Rdio Scanner v6.5.0
----------------------------------
2022/06/14 09:20:40 server started
2022/06/14 09:20:40 main interface at http://pc-linux
2022/06/14 09:20:40 main interface at https://pc-linux
2022/06/14 09:20:40 admin interface at https://pc-linux/admin
2022/06/27 09:20:40 server started
2022/06/27 09:20:40 main interface at http://pc-linux
2022/06/27 09:20:40 main interface at https://pc-linux
2022/06/27 09:20:40 admin interface at https://pc-linux/admin
## Install Rdio Scanner as a service

View file

@ -44,7 +44,7 @@ ALWAYS DOWNLOAD THE LATEST VERSION OF [RDIO SCANNER](https://github.com/chuot/rd
C:\Users\rdio> cd rdio-scanner
C:\Users\rdio\rdio-scanner> tar -xvf ^
..\downloads\rdio-scanner-windows-amd64-v6.4.5.zip
..\downloads\rdio-scanner-windows-amd64-v6.5.0.zip
x rdio-scanner.exe
x rdio-scanner.pdf
@ -52,11 +52,11 @@ ALWAYS DOWNLOAD THE LATEST VERSION OF [RDIO SCANNER](https://github.com/chuot/rd
C:\Users\rdio\rdio-scanner>rdio-scanner
Rdio Scanner v6.4.5
Rdio Scanner v6.5.0
----------------------------------
2022/06/14 13:48:48 server started
2022/06/14 13:48:48 main interface at http://pc-windows:3000
2022/06/14 13:48:48 admin interface at http://pc-windows:3000/admin
2022/06/27 13:48:48 server started
2022/06/27 13:48:48 main interface at http://pc-windows:3000
2022/06/27 13:48:48 admin interface at http://pc-windows:3000/admin
4. Access the administrative dashboard to finalize the configuration.
@ -78,11 +78,11 @@ Here we want our [Rdio Scanner](https://guthub.com/chuot/rdio-scanner) instance
C:\Users\rdio\rdio-scanner>rdio-scanner -listen :80
Rdio Scanner v6.4.5
Rdio Scanner v6.5.0
----------------------------------
2022/06/14 10:53:31 server started
2022/06/14 10:53:31 main interface at http://pc-windows
2022/06/14 10:53:31 admin interface at http://pc-windows/admin
2022/06/27 10:53:31 server started
2022/06/27 10:53:31 main interface at http://pc-windows
2022/06/27 10:53:31 admin interface at http://pc-windows/admin
## Listening on a SSL port
@ -96,12 +96,12 @@ You can use your own SSL certificates that match your domain name with `-ssl_cer
-ssl_key_file meykey.key ^
-ssl_listen :443
Rdio Scanner v6.4.5
Rdio Scanner v6.5.0
----------------------------------
2022/06/14 11:05:43 server started
2022/06/14 11:05:43 main interface at http://pc-windows
2022/06/14 11:05:43 main interface at https://pc-windows
2022/06/14 11:05:43 admin interface at https://pc-windows/admin
2022/06/27 11:05:43 server started
2022/06/27 11:05:43 main interface at http://pc-windows
2022/06/27 11:05:43 main interface at https://pc-windows
2022/06/27 11:05:43 admin interface at https://pc-windows/admin
If you don't want to worry about SSL certificates, you can use the built-in Let's Encrypt auto-cert feature. This requires that you have both port 80 (HTTP) and port 443 (HTTPS) open to the world. Also, your domain name should point to your IP address where [Rdio Scanner](https://github.com/chuot/rdio-scanner/) is running. The advantage of this approach is that everything is done automatically, no certificate request, no certificate renewal.
@ -119,7 +119,7 @@ You don't want to have to type everytime a long list of arguments. No problem, y
-ssl_auto_cert mydomain.com ^
-ssl_listen :443 ^
-config_save
2022/06/14 11:08:28 rdio-scanner.ini file created
2022/06/27 11:08:28 rdio-scanner.ini file created
All of your parameters passed as arguments to [Rdio Scanner](https://github.com/chuot/rdio-scanner) have been saved to an INI file which has the same arguments/values list.
@ -133,12 +133,12 @@ Then simply run [Rdio Scanner](https://github.com/chuot/rdio-scanner) without an
C:\Users\rdio\rdio-scanner>rdio-scanner
Rdio Scanner v6.4.5
Rdio Scanner v6.5.0
----------------------------------
2022/06/14 11:11:28 server started
2022/06/14 11:11:28 main interface at http://pc-windows
2022/06/14 11:11:28 main interface at https://pc-windows
2022/06/14 11:11:28 admin interface at https://pc-windows/admin
2022/06/27 11:11:28 server started
2022/06/27 11:11:28 main interface at http://pc-windows
2022/06/27 11:11:28 main interface at https://pc-windows
2022/06/27 11:11:28 admin interface at https://pc-windows/admin
\pagebreak{}

View file

@ -51,7 +51,7 @@ The color is _green_ by default, but can be customized by the system or by the t
| ![HOLD TG](./images/webapp-control-holdtg.png?raw=true) | Temporarily maintain the current talkgroup in live feed mode. |
| ![REPLAY LAST](./images/webapp-control-replay.png?raw=true) | Replay the current audio from the beginning or the previous one if there is none active. Press repeatedly to replay items displayed in the history section. For example, press three times to replay the third history item. |
| ![SKIP NEXT](./images/webapp-control-skip.png?raw=true) | Stop the audio currently playing and play the next one in the listening queue. This is useful when playing boring or encrypted sound. |
| ![AVOID](./images/webapp-control-avoid.png?raw=true) | Activate and deactivate the talkgroup from the current or previous audio. |
| ![AVOID](./images/webapp-control-avoid.png?raw=true) | Activate and deactivate the talkgroup from the current or previous audio. Pressing the **AVOID** button multiple times when on the same call will go through **AVOID** -> **AVOID 30M** -> **AVOID 60M** -> **AVOID 120M** where the last ones will be AVOID for a specific number of minutes. |
| ![SEARCH CALL](./images/webapp-control-search.png?raw=true) | Display the archived audio panel. |
| ![PAUSE](./images/webapp-control-pause.png?raw=true) | Stop playing queue audio. Useful if you have to answer the phone without losing what's queued up for playing. |
| ![SELECT TG](./images/webapp-control-select.png?raw=true) | Display the systems/talkgroups selection panel where you decide which audio you want to listen to in _LIVE FEED_ mode, not in playback mode. |
@ -83,9 +83,9 @@ The first section concerns group selection. This section can be disabled from th
This is much like the group section, but for each system. There is also just two states for each button:
| Button | State |
| ------------------------------------------------------- | -------------------------------------------------------------------------------------------------------------------- |
| ------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| ![ON](./images/webapp-select-system-on.png?raw=true) | The talkgroup from this system is active. If you press it while it is active, it will make the talkgroup inactive. |
| ![OFF](./images/webapp-select-system-off.png?raw=true) | The talkgroup from this system is inactive. If you press it while it is inactive, it will make the talkgroup active. |
| ![OFF](./images/webapp-select-system-off.png?raw=true) | The talkgroup from this system is inactive. If you press it while it is inactive, it will make the talkgroup active. Note that if the LED is flashing, it means that the talkgroup is avoided for a limited period. |
| ![ALL OFF](./images/webapp-select-all-off.png?raw=true) | Make inactive every talkgroups from this system. |
| ![ALL ON](./images/webapp-select-all-on.png?raw=true) | Make active every talkgroups from this system. |

View file

@ -25,20 +25,20 @@ import (
)
type Access struct {
Id interface{} `json:"_id"`
Id any `json:"_id"`
Code string `json:"code"`
Expiration interface{} `json:"expiration"`
Expiration any `json:"expiration"`
Ident string `json:"ident"`
Limit interface{} `json:"limit"`
Order interface{} `json:"order"`
Systems interface{} `json:"systems"`
Limit any `json:"limit"`
Order any `json:"order"`
Systems any `json:"systems"`
}
func NewAccess() *Access {
return &Access{Systems: "*"}
}
func (access *Access) FromMap(m map[string]interface{}) *Access {
func (access *Access) FromMap(m map[string]any) *Access {
switch v := m["_id"].(type) {
case float64:
access.Id = uint(v)
@ -72,7 +72,7 @@ func (access *Access) FromMap(m map[string]interface{}) *Access {
}
switch v := m["systems"].(type) {
case []interface{}:
case []any:
if b, err := json.Marshal(v); err == nil {
access.Systems = string(b)
}
@ -86,10 +86,10 @@ func (access *Access) FromMap(m map[string]interface{}) *Access {
func (access *Access) HasAccess(call *Call) bool {
if access.Systems != nil {
switch v := access.Systems.(type) {
case []interface{}:
case []any:
for _, f := range v {
switch v := f.(type) {
case map[string]interface{}:
case map[string]any:
switch id := v["id"].(type) {
case float64:
if id == float64(call.System) {
@ -98,7 +98,7 @@ func (access *Access) HasAccess(call *Call) bool {
if tg == "*" {
return true
}
case []interface{}:
case []any:
for _, f := range tg {
switch tg := f.(type) {
case float64:
@ -166,7 +166,7 @@ func (accesses *Accesses) Add(access *Access) (*Accesses, bool) {
return accesses, added
}
func (accesses *Accesses) FromMap(f []interface{}) *Accesses {
func (accesses *Accesses) FromMap(f []any) *Accesses {
accesses.mutex.Lock()
defer accesses.mutex.Unlock()
@ -174,7 +174,7 @@ func (accesses *Accesses) FromMap(f []interface{}) *Accesses {
for _, r := range f {
switch m := r.(type) {
case map[string]interface{}:
case map[string]any:
access := &Access{}
access.FromMap(m)
accesses.List = append(accesses.List, access)
@ -207,7 +207,7 @@ func (accesses *Accesses) IsRestricted() bool {
func (accesses *Accesses) Read(db *Database) error {
var (
err error
expiration interface{}
expiration any
id sql.NullFloat64
limit sql.NullFloat64
order sql.NullFloat64
@ -261,7 +261,7 @@ func (accesses *Accesses) Read(db *Database) error {
}
if err = json.Unmarshal([]byte(systems), &access.Systems); err != nil {
access.Systems = []interface{}{}
access.Systems = []any{}
}
accesses.List = append(accesses.List, access)
@ -298,7 +298,7 @@ func (accesses *Accesses) Write(db *Database) error {
err error
rows *sql.Rows
rowIds = []uint{}
systems interface{}
systems any
)
accesses.mutex.Lock()

View file

@ -75,7 +75,7 @@ func (admin *Admin) BroadcastConfig() {
}
}
func (admin *Admin) ChangePassword(currentPassword interface{}, newPassword string) error {
func (admin *Admin) ChangePassword(currentPassword any, newPassword string) error {
var (
err error
hash []byte
@ -158,20 +158,20 @@ func (admin *Admin) ConfigHandler(w http.ResponseWriter, r *http.Request) {
admin.SendConfig(w)
case http.MethodPut:
m := map[string]interface{}{}
m := map[string]any{}
err := json.NewDecoder(r.Body).Decode(&m)
if err != nil {
w.WriteHeader(http.StatusBadRequest)
return
}
admin.Controller.IngestLock()
admin.mutex.Lock()
defer admin.mutex.Unlock()
admin.Controller.Dirwatches.Stop()
switch v := m["access"].(type) {
case []interface{}:
case []any:
admin.Controller.Accesses.FromMap(v)
err := admin.Controller.Accesses.Write(admin.Controller.Database)
if err != nil {
@ -185,7 +185,7 @@ func (admin *Admin) ConfigHandler(w http.ResponseWriter, r *http.Request) {
}
switch v := m["apiKeys"].(type) {
case []interface{}:
case []any:
admin.Controller.Apikeys.FromMap(v)
err = admin.Controller.Apikeys.Write(admin.Controller.Database)
if err != nil {
@ -199,7 +199,7 @@ func (admin *Admin) ConfigHandler(w http.ResponseWriter, r *http.Request) {
}
switch v := m["dirWatch"].(type) {
case []interface{}:
case []any:
admin.Controller.Dirwatches.FromMap(v)
err = admin.Controller.Dirwatches.Write(admin.Controller.Database)
if err != nil {
@ -213,7 +213,7 @@ func (admin *Admin) ConfigHandler(w http.ResponseWriter, r *http.Request) {
}
switch v := m["downstreams"].(type) {
case []interface{}:
case []any:
admin.Controller.Downstreams.FromMap(v)
err = admin.Controller.Downstreams.Write(admin.Controller.Database)
if err != nil {
@ -227,7 +227,7 @@ func (admin *Admin) ConfigHandler(w http.ResponseWriter, r *http.Request) {
}
switch v := m["groups"].(type) {
case []interface{}:
case []any:
admin.Controller.Groups.FromMap(v)
err = admin.Controller.Groups.Write(admin.Controller.Database)
if err != nil {
@ -241,7 +241,7 @@ func (admin *Admin) ConfigHandler(w http.ResponseWriter, r *http.Request) {
}
switch v := m["options"].(type) {
case map[string]interface{}:
case map[string]any:
admin.Controller.Options.FromMap(v)
err = admin.Controller.Options.Write(admin.Controller.Database)
if err != nil {
@ -250,7 +250,7 @@ func (admin *Admin) ConfigHandler(w http.ResponseWriter, r *http.Request) {
}
switch v := m["systems"].(type) {
case []interface{}:
case []any:
admin.Controller.Systems.FromMap(v)
err = admin.Controller.Systems.Write(admin.Controller.Database)
if err != nil {
@ -264,7 +264,7 @@ func (admin *Admin) ConfigHandler(w http.ResponseWriter, r *http.Request) {
}
switch v := m["tags"].(type) {
case []interface{}:
case []any:
admin.Controller.Tags.FromMap(v)
err = admin.Controller.Tags.Write(admin.Controller.Database)
if err != nil {
@ -277,9 +277,6 @@ func (admin *Admin) ConfigHandler(w http.ResponseWriter, r *http.Request) {
}
}
admin.mutex.Unlock()
admin.Controller.IngestUnlock()
admin.Controller.EmitConfig()
admin.Controller.Dirwatches.Start(admin.Controller)
@ -297,10 +294,10 @@ func (admin *Admin) GetAuthorization(r *http.Request) string {
return r.Header.Get("Authorization")
}
func (admin *Admin) GetConfig() map[string]interface{} {
systems := []map[string]interface{}{}
func (admin *Admin) GetConfig() map[string]any {
systems := []map[string]any{}
for _, system := range admin.Controller.Systems.List {
systems = append(systems, map[string]interface{}{
systems = append(systems, map[string]any{
"_id": system.RowId,
"autoPopulate": system.AutoPopulate,
"blacklists": system.Blacklists,
@ -313,7 +310,7 @@ func (admin *Admin) GetConfig() map[string]interface{} {
})
}
return map[string]interface{}{
return map[string]any{
"access": admin.Controller.Accesses.List,
"apiKeys": admin.Controller.Apikeys.List,
"dirWatch": admin.Controller.Dirwatches.List,
@ -334,7 +331,7 @@ func (admin *Admin) LogsHandler(w http.ResponseWriter, r *http.Request) {
switch r.Method {
case http.MethodPost:
m := map[string]interface{}{}
m := map[string]any{}
err := json.NewDecoder(r.Body).Decode(&m)
if err != nil {
w.WriteHeader(http.StatusBadRequest)
@ -372,7 +369,7 @@ func (admin *Admin) LogsHandler(w http.ResponseWriter, r *http.Request) {
func (admin *Admin) LoginHandler(w http.ResponseWriter, r *http.Request) {
switch r.Method {
case http.MethodPost:
m := map[string]interface{}{}
m := map[string]any{}
if err := json.NewDecoder(r.Body).Decode(&m); err != nil {
w.WriteHeader(http.StatusBadRequest)
@ -441,7 +438,7 @@ func (admin *Admin) LoginHandler(w http.ResponseWriter, r *http.Request) {
admin.Tokens = append(admin.Tokens[1:], sToken)
}
b, err := json.Marshal(map[string]interface{}{
b, err := json.Marshal(map[string]any{
"passwordNeedChange": true,
"token": sToken,
})
@ -488,7 +485,7 @@ func (admin *Admin) PasswordHandler(w http.ResponseWriter, r *http.Request) {
case http.MethodPost:
var (
b []byte
currentPassword interface{}
currentPassword any
newPassword string
)
@ -502,7 +499,7 @@ func (admin *Admin) PasswordHandler(w http.ResponseWriter, r *http.Request) {
return
}
m := map[string]interface{}{}
m := map[string]any{}
err := json.NewDecoder(r.Body).Decode(&m)
if err != nil {
w.WriteHeader(http.StatusBadRequest)
@ -528,7 +525,7 @@ func (admin *Admin) PasswordHandler(w http.ResponseWriter, r *http.Request) {
return
}
if b, err = json.Marshal(map[string]interface{}{"passwordNeedChange": admin.Controller.Options.adminPasswordNeedChange}); err == nil {
if b, err = json.Marshal(map[string]any{"passwordNeedChange": admin.Controller.Options.adminPasswordNeedChange}); err == nil {
w.Write(b)
} else {
w.WriteHeader(http.StatusExpectationFailed)
@ -540,16 +537,16 @@ func (admin *Admin) PasswordHandler(w http.ResponseWriter, r *http.Request) {
}
func (admin *Admin) SendConfig(w http.ResponseWriter) {
var m map[string]interface{}
var m map[string]any
_, docker := os.LookupEnv("DOCKER")
if docker {
m = map[string]interface{}{
m = map[string]any{
"config": admin.GetConfig(),
"docker": docker,
"passwordNeedChange": admin.Controller.Options.adminPasswordNeedChange,
}
} else {
m = map[string]interface{}{
m = map[string]any{
"config": admin.GetConfig(),
"passwordNeedChange": admin.Controller.Options.adminPasswordNeedChange,
}
@ -611,7 +608,7 @@ func (admin *Admin) UserAddHandler(w http.ResponseWriter, r *http.Request) {
return
}
m := map[string]interface{}{}
m := map[string]any{}
err := json.NewDecoder(r.Body).Decode(&m)
if err != nil {
w.WriteHeader(http.StatusBadRequest)
@ -651,7 +648,7 @@ func (admin *Admin) UserRemoveHandler(w http.ResponseWriter, r *http.Request) {
return
}
m := map[string]interface{}{}
m := map[string]any{}
err := json.NewDecoder(r.Body).Decode(&m)
if err != nil {
w.WriteHeader(http.StatusBadRequest)
@ -690,7 +687,7 @@ func (admin *Admin) ValidateToken(sToken string) bool {
return false
}
token, err := jwt.Parse(sToken, func(token *jwt.Token) (interface{}, error) {
token, err := jwt.Parse(sToken, func(token *jwt.Token) (any, error) {
if _, ok := token.Method.(*jwt.SigningMethodHMAC); !ok {
return nil, fmt.Errorf("unexpected signing method: %v", token.Header["alg"])
}

View file

@ -42,14 +42,12 @@ func (api *Api) CallUploadHandler(w http.ResponseWriter, r *http.Request) {
mediaType, params, err := mime.ParseMediaType(r.Header.Get("Content-Type"))
if err != nil {
w.WriteHeader(http.StatusBadRequest)
w.Write([]byte("Invalid content-type\n"))
api.exitWithError(w, http.StatusBadRequest, "Invalid content-type")
return
}
if !strings.HasPrefix(mediaType, "multipart/") {
w.WriteHeader(http.StatusBadRequest)
w.Write([]byte("Not a multipart content\n"))
api.exitWithError(w, http.StatusBadRequest, "Not a multipart content")
return
}
@ -60,12 +58,14 @@ func (api *Api) CallUploadHandler(w http.ResponseWriter, r *http.Request) {
if err == io.EOF {
break
} else if err != nil {
continue
api.exitWithError(w, http.StatusExpectationFailed, fmt.Sprintf("multipart: %s\n", err.Error()))
return
}
b, err := io.ReadAll(p)
if err != nil {
continue
api.exitWithError(w, http.StatusExpectationFailed, fmt.Sprintf("ioread: %s\n", err.Error()))
return
}
switch p.FormName() {
@ -79,8 +79,7 @@ func (api *Api) CallUploadHandler(w http.ResponseWriter, r *http.Request) {
if ok, err := call.IsValid(); ok {
api.HandleCall(key, call, w)
} else {
w.WriteHeader(http.StatusExpectationFailed)
w.Write([]byte(fmt.Sprintf("Incomplete call data: %s\n", err.Error())))
api.exitWithError(w, http.StatusExpectationFailed, fmt.Sprintf("Incomplete call data: %s\n", err.Error()))
}
default:
@ -121,14 +120,12 @@ func (api *Api) TrunkRecorderCallUploadHandler(w http.ResponseWriter, r *http.Re
mediaType, params, err := mime.ParseMediaType(r.Header.Get("Content-Type"))
if err != nil {
w.WriteHeader(http.StatusBadRequest)
w.Write([]byte("Invalid content-type\n"))
api.exitWithError(w, http.StatusBadRequest, "Invalid content-type")
return
}
if !strings.HasPrefix(mediaType, "multipart/") {
w.WriteHeader(http.StatusBadRequest)
w.Write([]byte("Not a multipart content\n"))
api.exitWithError(w, http.StatusBadRequest, "Not a multipart content")
return
}
@ -141,12 +138,14 @@ func (api *Api) TrunkRecorderCallUploadHandler(w http.ResponseWriter, r *http.Re
if err == io.EOF {
break
} else if err != nil {
continue
api.exitWithError(w, http.StatusExpectationFailed, fmt.Sprintf("multipart: %s", err.Error()))
return
}
b, err := io.ReadAll(p)
if err != nil {
continue
api.exitWithError(w, http.StatusExpectationFailed, fmt.Sprintf("ioread: %s", err.Error()))
return
}
switch p.FormName() {
@ -154,8 +153,7 @@ func (api *Api) TrunkRecorderCallUploadHandler(w http.ResponseWriter, r *http.Re
key = string(b)
case "meta":
if err := ParseTrunkRecorderMeta(call, b); err != nil {
w.WriteHeader(http.StatusExpectationFailed)
w.Write([]byte("Invalid call data\n"))
api.exitWithError(w, http.StatusExpectationFailed, "Invalid call data")
return
}
default:
@ -171,8 +169,7 @@ func (api *Api) TrunkRecorderCallUploadHandler(w http.ResponseWriter, r *http.Re
api.HandleCall(key, call, w)
} else {
w.WriteHeader(http.StatusExpectationFailed)
w.Write([]byte(fmt.Sprintf("Incomplete call data: %s\n", err.Error())))
api.exitWithError(w, http.StatusExpectationFailed, fmt.Sprintf("Incomplete call data: %s\n", err.Error()))
}
default:
@ -180,3 +177,10 @@ func (api *Api) TrunkRecorderCallUploadHandler(w http.ResponseWriter, r *http.Re
w.Write([]byte("Unsupported method\n"))
}
}
func (api *Api) exitWithError(w http.ResponseWriter, status int, message string) {
api.Controller.Logs.LogEvent(LogLevelError, fmt.Sprintf("api: %s", message))
w.WriteHeader(status)
w.Write([]byte(fmt.Sprintf("%s\n", message)))
}

View file

@ -26,15 +26,15 @@ import (
)
type Apikey struct {
Id interface{} `json:"_id"`
Id any `json:"_id"`
Disabled bool `json:"disabled"`
Ident string `json:"ident"`
Key string `json:"key"`
Order interface{} `json:"order"`
Systems interface{} `json:"systems"`
Order any `json:"order"`
Systems any `json:"systems"`
}
func (apikey *Apikey) FromMap(m map[string]interface{}) *Apikey {
func (apikey *Apikey) FromMap(m map[string]any) *Apikey {
switch v := m["_id"].(type) {
case float64:
apikey.Id = uint(v)
@ -61,7 +61,7 @@ func (apikey *Apikey) FromMap(m map[string]interface{}) *Apikey {
}
switch v := m["systems"].(type) {
case []interface{}:
case []any:
if b, err := json.Marshal(v); err == nil {
apikey.Systems = string(b)
}
@ -74,10 +74,10 @@ func (apikey *Apikey) FromMap(m map[string]interface{}) *Apikey {
func (apikey *Apikey) HasAccess(call *Call) bool {
switch v := apikey.Systems.(type) {
case []interface{}:
case []any:
for _, f := range v {
switch v := f.(type) {
case map[string]interface{}:
case map[string]any:
switch id := v["id"].(type) {
case float64:
if id == float64(call.System) {
@ -86,7 +86,7 @@ func (apikey *Apikey) HasAccess(call *Call) bool {
if tg == "*" {
return true
}
case []interface{}:
case []any:
for _, f := range tg {
switch tg := f.(type) {
case float64:
@ -122,7 +122,7 @@ func NewApikeys() *Apikeys {
}
}
func (apikeys *Apikeys) FromMap(f []interface{}) *Apikeys {
func (apikeys *Apikeys) FromMap(f []any) *Apikeys {
apikeys.mutex.Lock()
defer apikeys.mutex.Unlock()
@ -130,7 +130,7 @@ func (apikeys *Apikeys) FromMap(f []interface{}) *Apikeys {
for _, r := range f {
switch m := r.(type) {
case map[string]interface{}:
case map[string]any:
apikey := &Apikey{}
apikey.FromMap(m)
apikeys.List = append(apikeys.List, apikey)
@ -198,7 +198,7 @@ func (apikeys *Apikeys) Read(db *Database) error {
}
if err = json.Unmarshal([]byte(systems), &apikey.Systems); err != nil {
apikey.Systems = []interface{}{}
apikey.Systems = []any{}
}
apikeys.List = append(apikeys.List, apikey)
@ -219,7 +219,7 @@ func (apikeys *Apikeys) Write(db *Database) error {
err error
rows *sql.Rows
rowIds = []uint{}
systems interface{}
systems any
)
apikeys.mutex.Lock()

View file

@ -27,31 +27,31 @@ import (
)
type Call struct {
Id interface{} `json:"id"`
Id any `json:"id"`
Audio []byte `json:"audio"`
AudioName interface{} `json:"audioName"`
AudioType interface{} `json:"audioType"`
AudioName any `json:"audioName"`
AudioType any `json:"audioType"`
DateTime time.Time `json:"dateTime"`
Frequencies interface{} `json:"frequencies"`
Frequency interface{} `json:"frequency"`
Patches interface{} `json:"patches"`
Source interface{} `json:"source"`
Sources interface{} `json:"sources"`
Frequencies any `json:"frequencies"`
Frequency any `json:"frequency"`
Patches any `json:"patches"`
Source any `json:"source"`
Sources any `json:"sources"`
System uint `json:"system"`
Talkgroup uint `json:"talkgroup"`
systemLabel interface{}
talkgroupGroup interface{}
talkgroupLabel interface{}
talkgroupName interface{}
talkgroupTag interface{}
units interface{}
systemLabel any
talkgroupGroup any
talkgroupLabel any
talkgroupName any
talkgroupTag any
units any
}
func NewCall() *Call {
return &Call{
Frequencies: []map[string]interface{}{},
Frequencies: []map[string]any{},
Patches: []uint{},
Sources: []map[string]interface{}{},
Sources: []map[string]any{},
}
}
@ -85,9 +85,9 @@ func (call *Call) MarshalJSON() ([]byte, error) {
audio := fmt.Sprintf("%v", call.Audio)
audio = strings.ReplaceAll(audio, " ", ",")
return json.Marshal(map[string]interface{}{
return json.Marshal(map[string]any{
"id": call.Id,
"audio": map[string]interface{}{
"audio": map[string]any{
"data": json.RawMessage(audio),
"type": "Buffer",
},
@ -144,7 +144,7 @@ func (calls *Calls) GetCall(id uint, db *Database) (*Call, error) {
var (
audioName sql.NullString
audioType sql.NullString
dateTime interface{}
dateTime any
frequency sql.NullFloat64
source sql.NullFloat64
frequencies string
@ -184,13 +184,13 @@ func (calls *Calls) GetCall(id uint, db *Database) (*Call, error) {
if len(frequencies) > 0 {
if err = json.Unmarshal([]byte(frequencies), &call.Frequencies); err != nil {
call.Frequencies = []interface{}{}
call.Frequencies = []any{}
}
}
if len(patches) > 0 {
if err = json.Unmarshal([]byte(patches), &call.Patches); err != nil {
call.Patches = []interface{}{}
call.Patches = []any{}
}
}
@ -200,7 +200,7 @@ func (calls *Calls) GetCall(id uint, db *Database) (*Call, error) {
if len(sources) > 0 {
if err = json.Unmarshal([]byte(sources), &call.Sources); err != nil {
call.Sources = []interface{}{}
call.Sources = []any{}
}
}
@ -224,7 +224,7 @@ func (calls *Calls) Search(searchOptions *CallsSearchOptions, client *Client) (*
)
var (
dateTime interface{}
dateTime any
err error
id sql.NullFloat64
limit uint
@ -252,14 +252,14 @@ func (calls *Calls) Search(searchOptions *CallsSearchOptions, client *Client) (*
if client.Access != nil {
switch v := client.Access.Systems.(type) {
case []interface{}:
case []any:
a := []string{}
for _, scope := range v {
var c string
switch v := scope.(type) {
case map[string]interface{}:
case map[string]any:
switch v["talkgroups"].(type) {
case []interface{}:
case []any:
b := strings.ReplaceAll(fmt.Sprintf("%v", v["talkgroups"]), " ", ", ")
b = strings.ReplaceAll(b, "[", "(")
b = strings.ReplaceAll(b, "]", ")")
@ -441,7 +441,7 @@ func (calls *Calls) WriteCall(call *Call, db *Database) (uint, error) {
}
switch v := call.Frequencies.(type) {
case []map[string]interface{}:
case []map[string]any:
if b, err = json.Marshal(v); err == nil {
frequencies = string(b)
} else {
@ -459,7 +459,7 @@ func (calls *Calls) WriteCall(call *Call, db *Database) (uint, error) {
}
switch v := call.Sources.(type) {
case []map[string]interface{}:
case []map[string]any:
if b, err = json.Marshal(v); err == nil {
sources = string(b)
} else {
@ -479,18 +479,18 @@ func (calls *Calls) WriteCall(call *Call, db *Database) (uint, error) {
}
type CallsSearchOptions struct {
Date interface{} `json:"date,omitempty"`
Group interface{} `json:"group,omitempty"`
Limit interface{} `json:"limit,omitempty"`
Offset interface{} `json:"offset,omitempty"`
Sort interface{} `json:"sort,omitempty"`
System interface{} `json:"system,omitempty"`
Tag interface{} `json:"tag,omitempty"`
Talkgroup interface{} `json:"talkgroup,omitempty"`
Date any `json:"date,omitempty"`
Group any `json:"group,omitempty"`
Limit any `json:"limit,omitempty"`
Offset any `json:"offset,omitempty"`
Sort any `json:"sort,omitempty"`
System any `json:"system,omitempty"`
Tag any `json:"tag,omitempty"`
Talkgroup any `json:"talkgroup,omitempty"`
searchPatchedTalkgroups bool
}
func (searchOptions *CallsSearchOptions) fromMap(m map[string]interface{}) error {
func (searchOptions *CallsSearchOptions) fromMap(m map[string]any) error {
switch v := m["date"].(type) {
case string:
if t, err := time.Parse(time.RFC3339, v); err == nil {

View file

@ -60,16 +60,21 @@ func (client *Client) Init(controller *Controller, request *http.Request, conn *
client.Controller = controller
client.Conn = conn
client.Livefeed = NewLivefeed()
client.Send = make(chan *Message)
client.Send = make(chan *Message, 1024)
client.request = request
go func() {
defer func() {
recover()
controller.Unregister <- client
controller.Logs.LogEvent(LogLevelInfo, fmt.Sprintf("listener disconnected from ip %v", client.GetRemoteAddr()))
if len(client.Access.Ident) > 0 {
controller.Logs.LogEvent(LogLevelInfo, fmt.Sprintf("listener disconnected from ip %s with ident %s", client.GetRemoteAddr(), client.Access.Ident))
} else {
controller.Logs.LogEvent(LogLevelInfo, fmt.Sprintf("listener disconnected from ip %s", client.GetRemoteAddr()))
}
client.Conn.Close()
}()
client.Conn.SetReadDeadline(time.Now().Add(pongWait))
@ -106,12 +111,9 @@ func (client *Client) Init(controller *Controller, request *http.Request, conn *
})
defer func() {
recover()
ticker.Stop()
timer.Stop()
client.Conn.WriteMessage(websocket.CloseMessage, []byte{})
client.Conn.Close()
}()
@ -124,6 +126,16 @@ func (client *Client) Init(controller *Controller, request *http.Request, conn *
if message.Command == MessageCommandConfig {
timer.Stop()
controller.Register <- client
if len(client.Access.Ident) > 0 {
controller.Logs.LogEvent(LogLevelInfo, fmt.Sprintf("new listener from ip %s with ident %s", client.GetRemoteAddr(), client.Access.Ident))
} else {
controller.Logs.LogEvent(LogLevelInfo, fmt.Sprintf("new listener from ip %s", client.GetRemoteAddr()))
}
}
b, err := message.ToJson()
@ -148,10 +160,6 @@ func (client *Client) Init(controller *Controller, request *http.Request, conn *
}
}()
controller.Register <- client
controller.Logs.LogEvent(LogLevelInfo, fmt.Sprintf("new listener from ip %v", client.GetRemoteAddr()))
return nil
}
@ -160,16 +168,14 @@ func (client *Client) GetRemoteAddr() string {
}
func (client *Client) SendConfig(groups *Groups, options *Options, systems *Systems, tags *Tags) {
defer func() {
recover()
}()
client.SystemsMap = systems.GetScopedSystems(client, groups, tags, options.SortTalkgroups)
client.GroupsMap = groups.GetGroupsMap(&client.SystemsMap)
client.TagsMap = tags.GetTagsMap(&client.SystemsMap)
var payload = map[string]interface{}{
var payload = map[string]any{
"branding": options.Branding,
"dimmerDelay": options.DimmerDelay,
"email": options.Email,
"groups": client.GroupsMap,
"keypadBeeps": GetKeypadBeeps(options),
"playbackGoesLive": options.PlaybackGoesLive,
@ -188,10 +194,6 @@ func (client *Client) SendConfig(groups *Groups, options *Options, systems *Syst
}
func (client *Client) SendListenersCount(count int) {
defer func() {
recover()
}()
client.Send <- &Message{
Command: MessagecommandListenersCount,
Payload: count,
@ -209,7 +211,7 @@ func NewClients() *Clients {
func (clients *Clients) AccessCount(client *Client) int {
count := 0
clients.Map.Range(func(k interface{}, _ interface{}) bool {
clients.Map.Range(func(k any, _ any) bool {
switch c := k.(type) {
case *Client:
if c.Access == client.Access {
@ -229,7 +231,7 @@ func (clients *Clients) Add(client *Client) {
func (clients *Clients) Count() int {
count := 0
clients.Map.Range(func(k, v interface{}) bool {
clients.Map.Range(func(k, v any) bool {
count++
return true
@ -239,11 +241,7 @@ func (clients *Clients) Count() int {
}
func (clients *Clients) EmitCall(call *Call, restricted bool) {
defer func() {
recover()
}()
clients.Map.Range(func(k interface{}, _ interface{}) bool {
clients.Map.Range(func(k any, _ any) bool {
switch c := k.(type) {
case *Client:
if (!restricted || c.Access.HasAccess(call)) && c.Livefeed.IsEnabled(call) {
@ -256,13 +254,9 @@ func (clients *Clients) EmitCall(call *Call, restricted bool) {
}
func (clients *Clients) EmitConfig(groups *Groups, options *Options, systems *Systems, tags *Tags, restricted bool) {
defer func() {
recover()
}()
count := clients.Count()
clients.Map.Range(func(k interface{}, _ interface{}) bool {
clients.Map.Range(func(k any, _ any) bool {
switch c := k.(type) {
case *Client:
if restricted {
@ -283,7 +277,7 @@ func (clients *Clients) EmitConfig(groups *Groups, options *Options, systems *Sy
func (clients *Clients) EmitListenersCount() {
count := clients.Count()
clients.Map.Range(func(k interface{}, _ interface{}) bool {
clients.Map.Range(func(k any, _ any) bool {
switch c := k.(type) {
case *Client:
c.SendListenersCount(count)
@ -294,11 +288,5 @@ func (clients *Clients) EmitListenersCount() {
}
func (clients *Clients) Remove(client *Client) {
defer func() {
recover()
}()
clients.Map.Delete(client)
close(client.Send)
}

View file

@ -23,7 +23,7 @@ import (
"io"
"net/http"
"os"
"path"
"path/filepath"
"regexp"
"runtime"
"strconv"
@ -81,10 +81,10 @@ func NewCommand(baseDir string) *Command {
}
return &Command{
app: path.Base(app),
app: filepath.Base(app),
command: COMMAND_HELP,
password: pass,
tokenFile: baseDir + path.Base(app) + ".token",
tokenFile: baseDir + filepath.Base(app) + ".token",
url: COMMAND_DEF_URL,
}
}
@ -224,7 +224,7 @@ func (command *Command) adminPassword() {
command.exitWithError(fmt.Sprintf("Missing %s <password> arguments.", COMMAND_ARG_PASSWORD))
}
if body, err := command.writeBody(map[string]interface{}{"newPassword": command.password}); err == nil {
if body, err := command.writeBody(map[string]any{"newPassword": command.password}); err == nil {
if res, err := command.submit(http.MethodPost, "/api/admin/password", body, true); err == nil {
if res.StatusCode == http.StatusOK {
fmt.Println("New admin password applied")
@ -246,9 +246,9 @@ func (command *Command) configGet() {
if res.StatusCode == http.StatusOK {
if data, err := command.readBody(res.Body); err == nil {
switch v := data.(type) {
case map[string]interface{}:
case map[string]any:
switch v := v["config"].(type) {
case map[string]interface{}:
case map[string]any:
if f, err := os.Create(command.out); err == nil {
j := json.NewEncoder(f)
j.SetIndent("", " ")
@ -276,10 +276,10 @@ func (command *Command) configSet() {
}
if f, err := os.Open(command.in); err == nil {
var d interface{}
var d any
json.NewDecoder(f).Decode(&d)
switch v := d.(type) {
case map[string]interface{}:
case map[string]any:
if body, err := command.writeBody(v); err == nil {
if res, err := command.submit(http.MethodPut, "/api/admin/config", body, true); err == nil {
if res.StatusCode == http.StatusOK {
@ -300,12 +300,12 @@ func (command *Command) configSet() {
}
func (command *Command) login() {
if body, err := command.writeBody(map[string]interface{}{"password": command.password}); err == nil {
if body, err := command.writeBody(map[string]any{"password": command.password}); err == nil {
if res, err := command.submit(http.MethodPost, "/api/admin/login", body, false); err == nil {
if res.StatusCode == http.StatusOK {
if data, err := command.readBody(res.Body); err == nil {
switch v := data.(type) {
case map[string]interface{}:
case map[string]any:
switch v := v["token"].(type) {
case string:
command.token = v
@ -360,7 +360,7 @@ func (command *Command) userAdd() {
command.exitWithError(fmt.Sprintf("Missing %s <code> arguments.", COMMAND_ARG_CODE))
}
u := map[string]interface{}{
u := map[string]any{
"code": command.code,
"ident": command.ident,
"systems": "*",
@ -392,9 +392,9 @@ func (command *Command) userAdd() {
}
}
if len(s) > 0 {
systems := []map[string]interface{}{}
systems := []map[string]any{}
for _, i := range s {
systems = append(systems, map[string]interface{}{"id": i, "talkgroups": "*"})
systems = append(systems, map[string]any{"id": i, "talkgroups": "*"})
}
u["systems"] = systems
} else {
@ -419,7 +419,7 @@ func (command *Command) userRemove() {
command.exitWithError(fmt.Sprintf("Missing %s <ident> arguments.", COMMAND_ARG_IDENT))
}
if body, err := command.writeBody(map[string]interface{}{"ident": command.ident}); err == nil {
if body, err := command.writeBody(map[string]any{"ident": command.ident}); err == nil {
if res, err := command.submit(http.MethodPost, "/api/admin/user-remove", body, true); err == nil {
if res.StatusCode == http.StatusOK {
fmt.Printf("User %s removed.\n", command.ident)
@ -432,12 +432,12 @@ func (command *Command) userRemove() {
}
}
func (c *Command) readBody(body io.ReadCloser) (data interface{}, err error) {
func (c *Command) readBody(body io.ReadCloser) (data any, err error) {
err = json.NewDecoder(body).Decode(&data)
return data, err
}
func (c *Command) writeBody(data interface{}) (body io.Reader, err error) {
func (c *Command) writeBody(data any) (body io.Reader, err error) {
var b []byte
if b, err = json.Marshal(data); err == nil {
body = bytes.NewReader(b)
@ -464,7 +464,7 @@ func (c *Command) submit(method string, url string, body io.Reader, auth bool) (
return res, err
}
func (c *Command) exitWithError(err interface{}) {
func (c *Command) exitWithError(err any) {
fmt.Printf("%v\n", err)
os.Exit(1)
}

View file

@ -67,10 +67,9 @@ func NewController(config *Config) *Controller {
Systems: NewSystems(),
Tags: NewTags(),
Clients: NewClients(),
Register: make(chan *Client),
Unregister: make(chan *Client),
Ingest: make(chan *Call),
ingestMutex: sync.Mutex{},
Register: make(chan *Client, 4096),
Unregister: make(chan *Client, 4096),
Ingest: make(chan *Call, 4096),
}
controller.Admin = NewAdmin(controller)
@ -110,9 +109,6 @@ func (controller *Controller) IngestCall(call *Call) {
talkgroup *Talkgroup
)
controller.IngestLock()
defer controller.IngestUnlock()
logCall := func(call *Call, level string, message string) {
controller.Logs.LogEvent(level, fmt.Sprintf("newcall: system=%v talkgroup=%v file=%v %v", call.System, call.Talkgroup, call.AudioName, message))
}
@ -317,21 +313,13 @@ func (controller *Controller) IngestCall(call *Call) {
}
}
func (controller *Controller) IngestLock() {
controller.ingestMutex.Lock()
}
func (controller *Controller) IngestUnlock() {
controller.ingestMutex.Unlock()
}
func (controller *Controller) LogClientsCount() {
controller.Logs.LogEvent(LogLevelInfo, fmt.Sprintf("listeners count is %v", controller.Clients.Count()))
}
func (controller *Controller) ProcessMessage(client *Client, message *Message) error {
if message.Command == MessageCommandVersion {
client.Send <- &Message{Command: MessageCommandVersion, Payload: Version}
controller.ProcessMessageCommandVersion(client)
} else if controller.Accesses.IsRestricted() && client.Access.Systems == nil && message.Command != MessageCommandPin {
client.Send <- &Message{Command: MessageCommandPin}
@ -394,7 +382,7 @@ func (controller *Controller) ProcessMessageCommandCall(client *Client, message
func (controller *Controller) ProcessMessageCommandListCall(client *Client, message *Message) error {
switch v := message.Payload.(type) {
case map[string]interface{}:
case map[string]any:
searchOptions := CallsSearchOptions{searchPatchedTalkgroups: controller.Options.SearchPatchedTalkgroups}
searchOptions.fromMap(v)
if searchResults, err := controller.Calls.Search(&searchOptions, client); err == nil {
@ -467,6 +455,20 @@ func (controller *Controller) ProcessMessageCommandPin(client *Client, message *
return nil
}
func (controller *Controller) ProcessMessageCommandVersion(client *Client) {
p := map[string]string{"version": Version}
if len(controller.Options.Branding) > 0 {
p["branding"] = controller.Options.Branding
}
if len(controller.Options.Email) > 0 {
p["email"] = controller.Options.Email
}
client.Send <- &Message{Command: MessageCommandVersion, Payload: p}
}
func (controller *Controller) Start() error {
var err error

View file

@ -76,7 +76,7 @@ func NewDatabase(config *Config) *Database {
return database
}
func (db *Database) ParseDateTime(f interface{}) (time.Time, error) {
func (db *Database) ParseDateTime(f any) (time.Time, error) {
switch v := f.(type) {
case []uint8:
return time.Parse(db.DateTimeFormat, string(v))
@ -377,10 +377,10 @@ func (db *Database) migration20211202094819(verbose bool) error {
func (db *Database) migration20220101070000(verbose bool) error {
var (
err error
frequency interface{}
frequency any
id uint
label string
led interface{}
led any
name string
queries []string
rows *sql.Rows

View file

@ -44,25 +44,25 @@ const (
)
type Dirwatch struct {
Id interface{} `json:"_id"`
Delay interface{} `json:"delay"`
Id any `json:"_id"`
Delay any `json:"delay"`
DeleteAfter bool `json:"deleteAfter"`
Directory string `json:"directory"`
Disabled bool `json:"disabled"`
Extension interface{} `json:"extension"`
Frequency interface{} `json:"frequency"`
Mask interface{} `json:"mask"`
Order interface{} `json:"order"`
SystemId interface{} `json:"systemId"`
TalkgroupId interface{} `json:"talkgroupId"`
Kind interface{} `json:"type"`
Extension any `json:"extension"`
Frequency any `json:"frequency"`
Mask any `json:"mask"`
Order any `json:"order"`
SystemId any `json:"systemId"`
TalkgroupId any `json:"talkgroupId"`
Kind any `json:"type"`
UsePolling bool `json:"usePolling"`
controller *Controller
dirs map[string]bool
watcher *fsnotify.Watcher
}
func (dirwatch *Dirwatch) FromMap(m map[string]interface{}) *Dirwatch {
func (dirwatch *Dirwatch) FromMap(m map[string]any) *Dirwatch {
switch v := m["_id"].(type) {
case float64:
dirwatch.Id = uint(v)
@ -164,7 +164,7 @@ func (dirwatch *Dirwatch) ingestDefault(p string) error {
if strings.EqualFold(path.Ext(p), ext) {
call := NewCall()
call.AudioName = path.Base(p)
call.AudioName = filepath.Base(p)
call.AudioType = mime.TypeByExtension(path.Ext(p))
call.Frequency = dirwatch.Frequency
@ -224,7 +224,7 @@ func (dirwatch *Dirwatch) ingestSdrTrunk(p string) error {
call := NewCall()
call.AudioName = path.Base(p)
call.AudioName = filepath.Base(p)
call.AudioType = mime.TypeByExtension(path.Ext(p))
call.Frequency = dirwatch.Frequency
@ -280,7 +280,7 @@ func (dirwatch *Dirwatch) ingestTrunkRecorder(p string) error {
call := NewCall()
call.AudioName = path.Base(audioName)
call.AudioName = filepath.Base(audioName)
call.AudioType = mime.TypeByExtension(path.Ext(audioName))
call.Frequency = dirwatch.Frequency
@ -344,8 +344,8 @@ func (dirwatch *Dirwatch) parseMask(call *Call) {
var (
filename string
mask string
metapos = [][]interface{}{}
metaval = map[string]interface{}{}
metapos = [][]any{}
metaval = map[string]any{}
)
switch v := dirwatch.Mask.(type) {
@ -364,7 +364,7 @@ func (dirwatch *Dirwatch) parseMask(call *Call) {
for _, v := range meta {
if i := strings.Index(mask, v[1]); i != -1 {
metapos = append(metapos, []interface{}{v[0], i})
metapos = append(metapos, []any{v[0], i})
mask = strings.Replace(mask, v[1], fmt.Sprintf("(%v)", v[2]), 1)
}
}
@ -513,8 +513,8 @@ func (dirwatch *Dirwatch) parseMask(call *Call) {
case string:
if i, err := strconv.Atoi(v); err == nil {
switch sources := call.Sources.(type) {
case []map[string]interface{}:
call.Sources = append(sources, map[string]interface{}{"pos": 0, "src": uint(i)})
case []map[string]any:
call.Sources = append(sources, map[string]any{"pos": 0, "src": uint(i)})
}
}
}
@ -549,7 +549,6 @@ func (dirwatch *Dirwatch) Start(controller *Controller) error {
}
go func() {
// var timers = map[string]*time.Timer{}
var timers = sync.Map{}
logError := func(err error) {
@ -567,7 +566,7 @@ func (dirwatch *Dirwatch) Start(controller *Controller) error {
}
defer func() {
timers.Range(func(k interface{}, t interface{}) bool {
timers.Range(func(k any, t any) bool {
switch v := t.(type) {
case *time.Timer:
v.Stop()
@ -688,7 +687,7 @@ func NewDirwatches() *Dirwatches {
}
}
func (dirwatches *Dirwatches) FromMap(f []interface{}) *Dirwatches {
func (dirwatches *Dirwatches) FromMap(f []any) *Dirwatches {
dirwatches.mutex.Lock()
defer dirwatches.mutex.Unlock()
@ -698,7 +697,7 @@ func (dirwatches *Dirwatches) FromMap(f []interface{}) *Dirwatches {
for _, f := range f {
switch v := f.(type) {
case map[string]interface{}:
case map[string]any:
dirwatch := &Dirwatch{}
dirwatch.FromMap(v)
dirwatches.List = append(dirwatches.List, dirwatch)

View file

@ -32,15 +32,15 @@ import (
)
type Downstream struct {
Id interface{} `json:"_id"`
Id any `json:"_id"`
Apikey string `json:"apiKey"`
Disabled bool `json:"disabled"`
Order interface{} `json:"order"`
Systems interface{} `json:"systems"`
Order any `json:"order"`
Systems any `json:"systems"`
Url string `json:"url"`
}
func (downstream *Downstream) FromMap(m map[string]interface{}) *Downstream {
func (downstream *Downstream) FromMap(m map[string]any) *Downstream {
switch v := m["_id"].(type) {
case float64:
downstream.Id = uint(v)
@ -62,7 +62,7 @@ func (downstream *Downstream) FromMap(m map[string]interface{}) *Downstream {
}
switch v := m["systems"].(type) {
case []interface{}:
case []any:
if b, err := json.Marshal(v); err == nil {
downstream.Systems = string(b)
}
@ -84,10 +84,10 @@ func (downstream *Downstream) HasAccess(call *Call) bool {
}
switch v := downstream.Systems.(type) {
case []interface{}:
case []any:
for _, f := range v {
switch v := f.(type) {
case map[string]interface{}:
case map[string]any:
switch id := v["id"].(type) {
case float64:
if id == float64(call.System) {
@ -96,7 +96,7 @@ func (downstream *Downstream) HasAccess(call *Call) bool {
if tg == "*" {
return true
}
case []interface{}:
case []any:
for _, f := range tg {
switch tg := f.(type) {
case float64:
@ -181,7 +181,7 @@ func (downstream *Downstream) Send(call *Call) error {
}
switch v := call.Frequencies.(type) {
case []map[string]interface{}:
case []map[string]any:
if w, err := mw.CreateFormField("frequencies"); err == nil {
if b, err := json.Marshal(v); err == nil {
if _, err = w.Write(b); err != nil {
@ -241,7 +241,7 @@ func (downstream *Downstream) Send(call *Call) error {
}
switch v := call.Sources.(type) {
case []map[string]interface{}:
case []map[string]any:
if w, err := mw.CreateFormField("sources"); err == nil {
if b, err := json.Marshal(v); err == nil {
if _, err = w.Write(b); err != nil {
@ -363,7 +363,7 @@ func NewDownstreams() *Downstreams {
}
}
func (downstreams *Downstreams) FromMap(f []interface{}) *Downstreams {
func (downstreams *Downstreams) FromMap(f []any) *Downstreams {
downstreams.mutex.Lock()
defer downstreams.mutex.Unlock()
@ -371,7 +371,7 @@ func (downstreams *Downstreams) FromMap(f []interface{}) *Downstreams {
for _, r := range f {
switch m := r.(type) {
case map[string]interface{}:
case map[string]any:
downstream := &Downstream{}
downstream.FromMap(m)
downstreams.List = append(downstreams.List, downstream)
@ -423,7 +423,7 @@ func (downstreams *Downstreams) Read(db *Database) error {
}
if err = json.Unmarshal([]byte(systems), &downstream.Systems); err != nil {
downstream.Systems = []interface{}{}
downstream.Systems = []any{}
}
if len(downstream.Url) == 0 {
@ -464,7 +464,7 @@ func (downstreams *Downstreams) Write(db *Database) error {
err error
rows *sql.Rows
rowIds = []uint{}
systems interface{}
systems any
)
downstreams.mutex.Lock()

View file

@ -1,9 +1,9 @@
module rdio-scanner/server
go 1.17
go 1.18
require (
github.com/dhowden/tag v0.0.0-20220530110423-77907a30b7f1
github.com/dhowden/tag v0.0.0-20220618230019-adf36e896086
github.com/fsnotify/fsnotify v1.5.4
github.com/go-sql-driver/mysql v1.6.0
github.com/golang-jwt/jwt/v4 v4.4.1
@ -21,8 +21,8 @@ require (
github.com/remyoudompheng/bigfft v0.0.0-20200410134404-eec4a21b6bb0 // indirect
github.com/stretchr/testify v1.7.0 // indirect
golang.org/x/mod v0.6.0-dev.0.20220419223038-86c51ed26bb4 // indirect
golang.org/x/net v0.0.0-20220607020251-c690dde0001d // indirect
golang.org/x/sys v0.0.0-20220610221304-9f5ed59c137d // indirect
golang.org/x/net v0.0.0-20220617184016-355a448f1bc9 // indirect
golang.org/x/sys v0.0.0-20220615213510-4f61da869c0c // indirect
golang.org/x/text v0.3.7 // indirect
golang.org/x/tools v0.1.11 // indirect
lukechampine.com/uint128 v1.2.0 // indirect

View file

@ -2,6 +2,8 @@ github.com/davecgh/go-spew v1.1.0 h1:ZDRjVQ15GmhC3fiQ8ni8+OwkZQO4DARzQgrnXU1Liz8
github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
github.com/dhowden/tag v0.0.0-20220530110423-77907a30b7f1 h1:jD1A7flCBuwFb5PvK3B2u/jmUUZ2j3n3puR0F7peZWM=
github.com/dhowden/tag v0.0.0-20220530110423-77907a30b7f1/go.mod h1:SniNVYuaD1jmdEEvi+7ywb1QFR7agjeTdGKyFb0p7Rw=
github.com/dhowden/tag v0.0.0-20220618230019-adf36e896086 h1:ORubSQoKnncsBnR4zD9CuYFJCPOCuSNEpWEZrDdBXkc=
github.com/dhowden/tag v0.0.0-20220618230019-adf36e896086/go.mod h1:Z3Lomva4pyMWYezjMAU5QWRh0p1VvO4199OHlFnyKkM=
github.com/dustin/go-humanize v1.0.0 h1:VSnTsYCnlFHaM2/igO1h6X3HA71jcobQuxemgkq4zYo=
github.com/dustin/go-humanize v1.0.0/go.mod h1:HtrtbFcZ19U5GC7JDqmcUSB87Iq5E25KnS6fMYU6eOk=
github.com/fsnotify/fsnotify v1.5.4 h1:jRbGcIw6P2Meqdwuo0H1p6JVLbL5DHKAKlYndzMwVZI=
@ -51,6 +53,8 @@ golang.org/x/net v0.0.0-20211015210444-4f30a5c0130f/go.mod h1:9nx3DQGgdP8bBQD5qx
golang.org/x/net v0.0.0-20211112202133-69e39bad7dc2/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y=
golang.org/x/net v0.0.0-20220607020251-c690dde0001d h1:4SFsTMi4UahlKoloni7L4eYzhFRifURQLw+yv0QDCx8=
golang.org/x/net v0.0.0-20220607020251-c690dde0001d/go.mod h1:XRhObCWvk6IyKnWLug+ECip1KBveYUHfp+8e9klMJ9c=
golang.org/x/net v0.0.0-20220617184016-355a448f1bc9 h1:Yqz/iviulwKwAREEeUd3nbBFn0XuyJqkoft2IlrvOhc=
golang.org/x/net v0.0.0-20220617184016-355a448f1bc9/go.mod h1:XRhObCWvk6IyKnWLug+ECip1KBveYUHfp+8e9klMJ9c=
golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.0.0-20201020160332-67f06af15bc9/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.0.0-20210220032951-036812b2e83c/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
@ -69,6 +73,8 @@ golang.org/x/sys v0.0.0-20220412211240-33da011f77ad/go.mod h1:oPkhp1MJrh7nUepCBc
golang.org/x/sys v0.0.0-20220520151302-bc2c85ada10a/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.0.0-20220610221304-9f5ed59c137d h1:Zu/JngovGLVi6t2J3nmAf3AoTDwuzw85YZ3b9o4yU7s=
golang.org/x/sys v0.0.0-20220610221304-9f5ed59c137d/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.0.0-20220615213510-4f61da869c0c h1:aFV+BgZ4svzjfabn8ERpuB4JI4N6/rdy1iusx77G3oU=
golang.org/x/sys v0.0.0-20220615213510-4f61da869c0c/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo=
golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8=
golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=

View file

@ -24,11 +24,11 @@ import (
)
type Group struct {
Id interface{} `json:"_id"`
Id any `json:"_id"`
Label string `json:"label"`
}
func (group *Group) FromMap(m map[string]interface{}) *Group {
func (group *Group) FromMap(m map[string]any) *Group {
switch v := m["_id"].(type) {
case float64:
group.Id = uint(v)
@ -54,7 +54,7 @@ func NewGroups() *Groups {
}
}
func (groups *Groups) FromMap(f []interface{}) *Groups {
func (groups *Groups) FromMap(f []any) *Groups {
groups.mutex.Lock()
defer groups.mutex.Unlock()
@ -62,7 +62,7 @@ func (groups *Groups) FromMap(f []interface{}) *Groups {
for _, r := range f {
switch m := r.(type) {
case map[string]interface{}:
case map[string]any:
group := &Group{}
group.FromMap(m)
groups.List = append(groups.List, group)
@ -72,7 +72,7 @@ func (groups *Groups) FromMap(f []interface{}) *Groups {
return groups
}
func (groups *Groups) GetGroup(f interface{}) (group *Group, ok bool) {
func (groups *Groups) GetGroup(f any) (group *Group, ok bool) {
groups.mutex.Lock()
defer groups.mutex.Unlock()

View file

@ -32,7 +32,7 @@ func NewLivefeed() *Livefeed {
}
}
func (livefeed *Livefeed) FromMap(f interface{}) *Livefeed {
func (livefeed *Livefeed) FromMap(f any) *Livefeed {
livefeed.mutex.Lock()
defer livefeed.mutex.Unlock()
@ -41,12 +41,12 @@ func (livefeed *Livefeed) FromMap(f interface{}) *Livefeed {
}
switch v := f.(type) {
case map[string]interface{}:
case map[string]any:
for s, n := range v {
if sysId, err := strconv.Atoi(s); err == nil {
sysId := uint(sysId)
switch v := n.(type) {
case map[string]interface{}:
case map[string]any:
for t, b := range v {
switch v := b.(type) {
case bool:

View file

@ -31,7 +31,7 @@ const (
)
type Log struct {
Id interface{} `json:"_id"`
Id any `json:"_id"`
DateTime time.Time `json:"dateTime"`
Level string `json:"level"`
Message string `json:"message"`
@ -99,7 +99,7 @@ func (logs *Logs) Search(searchOptions *LogsSearchOptions, db *Database) (*LogsS
)
var (
dateTime interface{}
dateTime any
err error
id sql.NullFloat64
limit uint
@ -240,14 +240,14 @@ func (logs *Logs) setDatabase(d *Database) {
}
type LogsSearchOptions struct {
Date interface{} `json:"date,omitempty"`
Level interface{} `json:"level,omitempty"`
Limit interface{} `json:"limit,omitempty"`
Offset interface{} `json:"offset,omitempty"`
Sort interface{} `json:"sort,omitempty"`
Date any `json:"date,omitempty"`
Level any `json:"level,omitempty"`
Limit any `json:"limit,omitempty"`
Offset any `json:"offset,omitempty"`
Sort any `json:"sort,omitempty"`
}
func (searchOptions *LogsSearchOptions) FromMap(m map[string]interface{}) error {
func (searchOptions *LogsSearchOptions) FromMap(m map[string]any) error {
switch v := m["date"].(type) {
case string:
if t, err := time.Parse(time.RFC3339, v); err == nil {

View file

@ -229,7 +229,7 @@ func main() {
}
func GetRemoteAddr(r *http.Request) string {
re := regexp.MustCompile(`(\[[^\]]+\]|[^:]+):.*`)
re := regexp.MustCompile(`(.+):.*$`)
for _, addr := range strings.Split(r.Header.Get("X-Forwarded-For"), ",") {
if ip := re.ReplaceAllString(addr, "$1"); len(ip) > 0 {
@ -241,5 +241,5 @@ func GetRemoteAddr(r *http.Request) string {
return ip
}
return "unknown"
return r.RemoteAddr
}

View file

@ -35,15 +35,15 @@ const (
)
type Message struct {
Command interface{}
Payload interface{}
Flag interface{}
Command any
Payload any
Flag any
}
func (message *Message) FromJson(b []byte) error {
var (
err error
f []interface{}
f []any
)
if err = json.Unmarshal(b, &f); err != nil {
@ -68,7 +68,7 @@ func (message *Message) FromJson(b []byte) error {
}
func (message *Message) ToJson() ([]byte, error) {
str := []interface{}{message.Command}
str := []any{message.Command}
if message.Payload != nil && message.Payload != "" {
str = append(str, message.Payload)

View file

@ -28,9 +28,11 @@ type Options struct {
AfsSystems string `json:"afsSystems"`
AudioConversion uint `json:"audioConversion"`
AutoPopulate bool `json:"autoPopulate"`
Branding string `json:"branding"`
DimmerDelay uint `json:"dimmerDelay"`
DisableDuplicateDetection bool `json:"disableDuplicateDetection"`
DuplicateDetectionTimeFrame uint `json:"duplicateDetectionTimeFrame"`
Email string `json:"email"`
KeypadBeeps string `json:"keypadBeeps"`
MaxClients uint `json:"maxClients"`
PlaybackGoesLive bool `json:"playbackGoesLive"`
@ -59,7 +61,7 @@ func NewOptions() *Options {
}
}
func (options *Options) FromMap(m map[string]interface{}) *Options {
func (options *Options) FromMap(m map[string]any) *Options {
options.mutex.Lock()
defer options.mutex.Unlock()
@ -82,6 +84,11 @@ func (options *Options) FromMap(m map[string]interface{}) *Options {
options.AutoPopulate = defaults.options.autoPopulate
}
switch v := m["branding"].(type) {
case string:
options.Branding = v
}
switch v := m["dimmerDelay"].(type) {
case float64:
options.DimmerDelay = uint(v)
@ -112,6 +119,11 @@ func (options *Options) FromMap(m map[string]interface{}) *Options {
options.DuplicateDetectionTimeFrame = defaults.options.duplicateDetectionTimeFrame
}
switch v := m["email"].(type) {
case string:
options.Email = v
}
switch v := m["keypadBeeps"].(type) {
case string:
options.KeypadBeeps = v
@ -221,7 +233,7 @@ func (options *Options) Read(db *Database) error {
err = db.Sql.QueryRow("select `val` from `rdioScannerConfigs` where `key` = 'options'").Scan(&s)
if err == nil {
var m map[string]interface{}
var m map[string]any
if err = json.Unmarshal([]byte(s), &m); err == nil {
switch v := m["afsSystems"].(type) {
@ -239,6 +251,11 @@ func (options *Options) Read(db *Database) error {
options.AutoPopulate = v
}
switch v := m["branding"].(type) {
case string:
options.Branding = v
}
switch v := m["dimmerDelay"].(type) {
case float64:
options.DimmerDelay = uint(v)
@ -254,6 +271,11 @@ func (options *Options) Read(db *Database) error {
options.DuplicateDetectionTimeFrame = uint(v)
}
switch v := m["email"].(type) {
case string:
options.Email = v
}
switch v := m["keypadBeeps"].(type) {
case string:
options.KeypadBeeps = v
@ -350,13 +372,15 @@ func (options *Options) Write(db *Database) error {
db.Sql.Exec("insert into `rdioScannerConfigs` (`key`, `val`) values (?, ?)", "adminPasswordNeedChange", string(b))
}
if b, err = json.Marshal(map[string]interface{}{
if b, err = json.Marshal(map[string]any{
"afsSystems": options.AfsSystems,
"audioConversion": options.AudioConversion,
"autoPopulate": options.AutoPopulate,
"branding": options.Branding,
"dimmerDelay": options.DimmerDelay,
"disableDuplicateDetection": options.DisableDuplicateDetection,
"duplicateDetectionTimeFrame": options.DuplicateDetectionTimeFrame,
"email": options.Email,
"keypadBeeps": options.KeypadBeeps,
"maxClients": options.MaxClients,
"playbackGoesLive": options.PlaybackGoesLive,

View file

@ -101,10 +101,10 @@ func ParseSdrTrunkMeta(call *Call, controller *Controller) error {
s = regexp.MustCompile(`(\[.+\])`).FindStringSubmatch(m.Title())
if len(s) > 1 && len(s[1]) > 0 {
var f interface{}
var f any
if err = json.Unmarshal([]byte(s[1]), &f); err == nil {
switch v := f.(type) {
case []interface{}:
case []any:
patches := []uint{}
for _, patch := range v {
switch v := patch.(type) {
@ -151,15 +151,15 @@ func ParseMultipartContent(call *Call, p *multipart.Part, b []byte) {
}
case "frequencies":
var f interface{}
var f any
if err := json.Unmarshal(b, &f); err == nil {
switch v := f.(type) {
case []interface{}:
var frequencies = []map[string]interface{}{}
case []any:
var frequencies = []map[string]any{}
for _, f := range v {
freq := map[string]interface{}{}
freq := map[string]any{}
switch v := f.(type) {
case map[string]interface{}:
case map[string]any:
switch v := v["errorCount"].(type) {
case float64:
if v >= 0 {
@ -204,12 +204,12 @@ func ParseMultipartContent(call *Call, p *multipart.Part, b []byte) {
case "patches", "patched_talkgroups":
var (
f interface{}
f any
patches = []uint{}
)
if err := json.Unmarshal(b, &f); err == nil {
switch v := f.(type) {
case []interface{}:
case []any:
for _, patch := range v {
switch v := patch.(type) {
case float64:
@ -229,17 +229,17 @@ func ParseMultipartContent(call *Call, p *multipart.Part, b []byte) {
case "sources":
var (
f interface{}
f any
units *Units
)
if err := json.Unmarshal(b, &f); err == nil {
switch v := f.(type) {
case []interface{}:
var sources = []map[string]interface{}{}
case []any:
var sources = []map[string]any{}
for _, f := range v {
src := map[string]interface{}{}
src := map[string]any{}
switch v := f.(type) {
case map[string]interface{}:
case map[string]any:
switch v := v["pos"].(type) {
case float64:
if v >= 0 {
@ -306,7 +306,7 @@ func ParseMultipartContent(call *Call, p *multipart.Part, b []byte) {
}
func ParseTrunkRecorderMeta(call *Call, b []byte) error {
m := map[string]interface{}{}
m := map[string]any{}
if err := json.Unmarshal(b, &m); err != nil {
return err
@ -320,12 +320,12 @@ func ParseTrunkRecorderMeta(call *Call, b []byte) error {
}
switch v := m["freqList"].(type) {
case []interface{}:
freqs := []map[string]interface{}{}
case []any:
freqs := []map[string]any{}
for _, f := range v {
freq := map[string]interface{}{}
freq := map[string]any{}
switch v := f.(type) {
case map[string]interface{}:
case map[string]any:
switch v := v["error_count"].(type) {
case float64:
if v >= 0 {
@ -368,7 +368,7 @@ func ParseTrunkRecorderMeta(call *Call, b []byte) error {
}
switch v := m["patched_talkgroups"].(type) {
case []interface{}:
case []any:
patches := []uint{}
for _, f := range v {
switch v := f.(type) {
@ -384,12 +384,12 @@ func ParseTrunkRecorderMeta(call *Call, b []byte) error {
}
switch v := m["srcList"].(type) {
case []interface{}:
sources := []map[string]interface{}{}
case []any:
sources := []map[string]any{}
for _, f := range v {
source := map[string]interface{}{}
source := map[string]any{}
switch v := f.(type) {
case map[string]interface{}:
case map[string]any:
switch v := v["pos"].(type) {
case float64:
if v >= 0 {

View file

@ -25,7 +25,7 @@ import (
type Scheduler struct {
Controller *Controller
Ticker *time.Ticker
cancel chan interface{}
cancel chan any
mutex sync.Mutex
started bool
}
@ -33,7 +33,7 @@ type Scheduler struct {
func NewScheduler(controller *Controller) *Scheduler {
return &Scheduler{
Controller: controller,
cancel: make(chan interface{}),
cancel: make(chan any),
}
}
@ -42,9 +42,6 @@ func (scheduler *Scheduler) pruneDatabase() error {
return nil
}
scheduler.Controller.IngestLock()
defer scheduler.Controller.IngestUnlock()
scheduler.Controller.Logs.LogEvent(LogLevelInfo, "database pruning")
if err := scheduler.Controller.Calls.Prune(scheduler.Controller.Database, scheduler.Controller.Options.PruneDays); err != nil {

View file

@ -30,9 +30,9 @@ type System struct {
AutoPopulate bool `json:"autoPopulate"`
Blacklists Blacklists `json:"blacklists"`
Label string `json:"label"`
Led interface{} `json:"led"`
Led any `json:"led"`
Order uint `json:"order"`
RowId interface{} `json:"_id"`
RowId any `json:"_id"`
Talkgroups *Talkgroups `json:"talkgroups"`
Units *Units `json:"units"`
}
@ -44,7 +44,7 @@ func NewSystem() *System {
}
}
func (system *System) FromMap(m map[string]interface{}) *System {
func (system *System) FromMap(m map[string]any) *System {
switch v := m["_id"].(type) {
case float64:
system.RowId = uint(v)
@ -81,19 +81,19 @@ func (system *System) FromMap(m map[string]interface{}) *System {
}
switch v := m["talkgroups"].(type) {
case []interface{}:
case []any:
system.Talkgroups.FromMap(v)
}
switch v := m["units"].(type) {
case []interface{}:
case []any:
system.Units.FromMap(v)
}
return system
}
type SystemMap map[string]interface{}
type SystemMap map[string]any
type Systems struct {
List []*System
@ -107,7 +107,7 @@ func NewSystems() *Systems {
}
}
func (systems *Systems) FromMap(f []interface{}) *Systems {
func (systems *Systems) FromMap(f []any) *Systems {
systems.mutex.Lock()
defer systems.mutex.Unlock()
@ -115,7 +115,7 @@ func (systems *Systems) FromMap(f []interface{}) *Systems {
for _, r := range f {
switch m := r.(type) {
case map[string]interface{}:
case map[string]any:
system := NewSystem()
system.FromMap(m)
systems.List = append(systems.List, system)
@ -141,7 +141,7 @@ NextId:
return 0
}
func (systems *Systems) GetSystem(f interface{}) (system *System, ok bool) {
func (systems *Systems) GetSystem(f any) (system *System, ok bool) {
systems.mutex.Lock()
defer systems.mutex.Unlock()
@ -187,10 +187,10 @@ func (systems *Systems) GetScopedSystems(client *Client, groups *Groups, tags *T
}
}
case []interface{}:
case []any:
for _, fSystem := range v {
switch v := fSystem.(type) {
case map[string]interface{}:
case map[string]any:
var (
mSystemId = v["id"]
mTalkgroups = v["talkgroups"]
@ -216,7 +216,7 @@ func (systems *Systems) GetScopedSystems(client *Client, groups *Groups, tags *T
continue
}
case []interface{}:
case []any:
rawSystem := *system
rawSystem.Talkgroups = NewTalkgroups()
for _, fTalkgroupId := range v {

View file

@ -24,11 +24,11 @@ import (
)
type Tag struct {
Id interface{} `json:"_id"`
Id any `json:"_id"`
Label string `json:"label"`
}
func (tag *Tag) FromMap(m map[string]interface{}) *Tag {
func (tag *Tag) FromMap(m map[string]any) *Tag {
switch v := m["_id"].(type) {
case float64:
tag.Id = uint(v)
@ -54,7 +54,7 @@ func NewTags() *Tags {
}
}
func (tags *Tags) FromMap(f []interface{}) *Tags {
func (tags *Tags) FromMap(f []any) *Tags {
tags.mutex.Lock()
defer tags.mutex.Unlock()
@ -62,7 +62,7 @@ func (tags *Tags) FromMap(f []interface{}) *Tags {
for _, r := range f {
switch m := r.(type) {
case map[string]interface{}:
case map[string]any:
tag := &Tag{}
tag.FromMap(m)
tags.List = append(tags.List, tag)
@ -72,7 +72,7 @@ func (tags *Tags) FromMap(f []interface{}) *Tags {
return tags
}
func (tags *Tags) GetTag(f interface{}) (tag *Tag, ok bool) {
func (tags *Tags) GetTag(f any) (tag *Tag, ok bool) {
tags.mutex.Lock()
defer tags.mutex.Unlock()

View file

@ -25,19 +25,19 @@ import (
)
type Talkgroup struct {
Frequency interface{} `json:"frequency"`
Frequency any `json:"frequency"`
group string
GroupId uint `json:"groupId"`
Id uint `json:"id"`
Label string `json:"label"`
Led interface{} `json:"led"`
Led any `json:"led"`
Name string `json:"name"`
Order uint `json:"order"`
TagId uint `json:"tagId"`
tag string
}
func (talkgroup *Talkgroup) FromMap(m map[string]interface{}) *Talkgroup {
func (talkgroup *Talkgroup) FromMap(m map[string]any) *Talkgroup {
switch v := m["id"].(type) {
case float64:
talkgroup.Id = uint(v)
@ -91,7 +91,7 @@ func (talkgroup *Talkgroup) FromMap(m map[string]interface{}) *Talkgroup {
return talkgroup
}
type TalkgroupMap map[string]interface{}
type TalkgroupMap map[string]any
type Talkgroups struct {
List []*Talkgroup
@ -105,7 +105,7 @@ func NewTalkgroups() *Talkgroups {
}
}
func (talkgroups *Talkgroups) FromMap(f []interface{}) *Talkgroups {
func (talkgroups *Talkgroups) FromMap(f []any) *Talkgroups {
talkgroups.mutex.Lock()
defer talkgroups.mutex.Unlock()
@ -113,7 +113,7 @@ func (talkgroups *Talkgroups) FromMap(f []interface{}) *Talkgroups {
for _, r := range f {
switch m := r.(type) {
case map[string]interface{}:
case map[string]any:
talkgroup := &Talkgroup{}
talkgroup.FromMap(m)
talkgroups.List = append(talkgroups.List, talkgroup)
@ -123,7 +123,7 @@ func (talkgroups *Talkgroups) FromMap(f []interface{}) *Talkgroups {
return talkgroups
}
func (talkgroups *Talkgroups) GetTalkgroup(f interface{}) (system *Talkgroup, ok bool) {
func (talkgroups *Talkgroups) GetTalkgroup(f any) (system *Talkgroup, ok bool) {
talkgroups.mutex.Lock()
defer talkgroups.mutex.Unlock()

View file

@ -30,7 +30,7 @@ type Unit struct {
Order uint `json:"order"`
}
func (unit *Unit) FromMap(m map[string]interface{}) *Unit {
func (unit *Unit) FromMap(m map[string]any) *Unit {
switch v := m["id"].(type) {
case float64:
unit.Id = uint(v)
@ -78,7 +78,7 @@ func (units *Units) Add(id uint, label string) (*Units, bool) {
return units, added
}
func (units *Units) FromMap(f []interface{}) *Units {
func (units *Units) FromMap(f []any) *Units {
units.mutex.Lock()
defer units.mutex.Unlock()
@ -86,7 +86,7 @@ func (units *Units) FromMap(f []interface{}) *Units {
for _, r := range f {
switch m := r.(type) {
case map[string]interface{}:
case map[string]any:
unit := &Unit{}
unit.FromMap(m)
units.List = append(units.List, unit)

View file

@ -15,4 +15,4 @@
package main
const Version = "6.4.5"
const Version = "6.5.0"