diff --git a/CHANGELOG.md b/CHANGELOG.md index 3b66ac2..f959291 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -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. diff --git a/COMPILING.md b/COMPILING.md index cd93cc1..1990a15 100644 --- a/COMPILING.md +++ b/COMPILING.md @@ -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 !** \ No newline at end of file diff --git a/Makefile b/Makefile index de67da2..a012022 100644 --- a/Makefile +++ b/Makefile @@ -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 diff --git a/client/package-lock.json b/client/package-lock.json index 1e476d9..1ae7dfb 100644 --- a/client/package-lock.json +++ b/client/package-lock.json @@ -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", diff --git a/client/package.json b/client/package.json index d0110a1..556c104 100644 --- a/client/package.json +++ b/client/package.json @@ -38,5 +38,5 @@ "build": "ng build --base-href ./ --configuration production", "start": "ng serve" }, - "version": "6.4.5" + "version": "6.5.0" } diff --git a/client/src/app/components/rdio-scanner/admin/admin.service.ts b/client/src/app/components/rdio-scanner/admin/admin.service.ts index 085700d..d7a1e7b 100644 --- a/client/src/app/components/rdio-scanner/admin/admin.service.ts +++ b/client/src/app/components/rdio-scanner/admin/admin.service.ts @@ -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], diff --git a/client/src/app/components/rdio-scanner/admin/config/options/options.component.html b/client/src/app/components/rdio-scanner/admin/config/options/options.component.html index 6820635..ba44510 100644 --- a/client/src/app/components/rdio-scanner/admin/config/options/options.component.html +++ b/client/src/app/components/rdio-scanner/admin/config/options/options.component.html @@ -45,6 +45,16 @@ +
+

+ Branding Label
+ A simple line of text displayed above the main screen to help identify the Rdio + Scanner instance. +

+ + + +

Dimmer Delay
@@ -86,6 +96,15 @@

+
+

+ Email Support
+ Email address where users can write to to get support. +

+ + + +

Keypad Beep Style
diff --git a/client/src/app/components/rdio-scanner/common.scss b/client/src/app/components/rdio-scanner/common.scss index 1deab8a..c42bfed 100644 --- a/client/src/app/components/rdio-scanner/common.scss +++ b/client/src/app/components/rdio-scanner/common.scss @@ -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); + } } } diff --git a/client/src/app/components/rdio-scanner/main/main.component.html b/client/src/app/components/rdio-scanner/main/main.component.html index 90fcfbd..8d9657e 100644 --- a/client/src/app/components/rdio-scanner/main/main.component.html +++ b/client/src/app/components/rdio-scanner/main/main.component.html @@ -1,4 +1,5 @@

+
{{ branding }}
@@ -53,6 +54,9 @@
+
+ ⏲ {{ tempAvoid }}M +
AVOID
@@ -153,4 +157,9 @@ SELECT
TG
+ +
+
\ No newline at end of file diff --git a/client/src/app/components/rdio-scanner/main/main.component.scss b/client/src/app/components/rdio-scanner/main/main.component.scss index fde33ca..4c332cd 100644 --- a/client/src/app/components/rdio-scanner/main/main.component.scss +++ b/client/src/app/components/rdio-scanner/main/main.component.scss @@ -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; +} diff --git a/client/src/app/components/rdio-scanner/main/main.component.ts b/client/src/app/components/rdio-scanner/main/main.component.ts index e5ff8cd..a392de0 100644 --- a/client/src/app/components/rdio-scanner/main/main.component.ts +++ b/client/src/app/components/rdio-scanner/main/main.component.ts @@ -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]) { - this.rdioScannerService.beep(RdioScannerBeepStyle.Activate); - + 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.beep(RdioScannerBeepStyle.Deactivate); + this.rdioScannerService.avoid({ status: true }); } + } + if (call && this.rdioScannerService.isAvoided(call)) { + this.rdioScannerService.beep(RdioScannerBeepStyle.Activate); } else { - this.rdioScannerService.beep(RdioScannerBeepStyle.Denied); + this.rdioScannerService.beep(RdioScannerBeepStyle.Deactivate); } this.updateDimmer(); + + } else { + this.rdioScannerService.beep(RdioScannerBeepStyle.Denied); } + } 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; diff --git a/client/src/app/components/rdio-scanner/main/support/support.component.html b/client/src/app/components/rdio-scanner/main/support/support.component.html new file mode 100644 index 0000000..a8382f2 --- /dev/null +++ b/client/src/app/components/rdio-scanner/main/support/support.component.html @@ -0,0 +1,10 @@ +
+ + For further assistance please contact {{ email }} + + + For further assistance please go to Rdio Scanner discussions + +
+
{{ countdown }}
\ No newline at end of file diff --git a/client/src/app/components/rdio-scanner/main/support/support.component.scss b/client/src/app/components/rdio-scanner/main/support/support.component.scss new file mode 100644 index 0000000..02bb4cf --- /dev/null +++ b/client/src/app/components/rdio-scanner/main/support/support.component.scss @@ -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; +} diff --git a/client/src/app/components/rdio-scanner/main/support/support.component.ts b/client/src/app/components/rdio-scanner/main/support/support.component.ts new file mode 100644 index 0000000..521bcff --- /dev/null +++ b/client/src/app/components/rdio-scanner/main/support/support.component.ts @@ -0,0 +1,55 @@ +/* + * ***************************************************************************** + * Copyright (C) 2019-2022 Chrystian Huot + * + * 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 + * **************************************************************************** + */ + +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, + @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(); + } + }); + } +} \ No newline at end of file diff --git a/client/src/app/components/rdio-scanner/native/native.scss b/client/src/app/components/rdio-scanner/native/native.component.scss similarity index 100% rename from client/src/app/components/rdio-scanner/native/native.scss rename to client/src/app/components/rdio-scanner/native/native.component.scss diff --git a/client/src/app/components/rdio-scanner/native/native.component.ts b/client/src/app/components/rdio-scanner/native/native.component.ts index 43822c8..0d23c7a 100644 --- a/client/src/app/components/rdio-scanner/native/native.component.ts +++ b/client/src/app/components/rdio-scanner/native/native.component.ts @@ -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) { - } + constructor(@Optional() private matSnackBarRef: MatSnackBarRef) { } ngOnInit(): void { const ua = navigator.userAgent; diff --git a/client/src/app/components/rdio-scanner/rdio-scanner.component.scss b/client/src/app/components/rdio-scanner/rdio-scanner.component.scss index 7905593..5c2a949 100644 --- a/client/src/app/components/rdio-scanner/rdio-scanner.component.scss +++ b/client/src/app/components/rdio-scanner/rdio-scanner.component.scss @@ -6,6 +6,7 @@ $background-color: rgb(30, 30, 30); display: block; height: 100%; min-width: 320px; + position: relative; .mat-sidenav { background-color: inherit; diff --git a/client/src/app/components/rdio-scanner/rdio-scanner.module.ts b/client/src/app/components/rdio-scanner/rdio-scanner.module.ts index 3f8cf30..8362e7a 100644 --- a/client/src/app/components/rdio-scanner/rdio-scanner.module.ts +++ b/client/src/app/components/rdio-scanner/rdio-scanner.module.ts @@ -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 }, diff --git a/client/src/app/components/rdio-scanner/rdio-scanner.service.ts b/client/src/app/components/rdio-scanner/rdio-scanner.service.ts index 6487b8c..087b279 100644 --- a/client/src/app/components/rdio-scanner/rdio-scanner.service.ts +++ b/client/src/app/components/rdio-scanner/rdio-scanner.service.ts @@ -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; } } @@ -949,7 +1026,7 @@ export class RdioScannerService implements OnDestroy { } else if (this.playbackRefreshing) { this.stopPlaybackMode(); - + if (this.config.playbackGoesLive) { this.startLivefeed(); } @@ -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); + try { + const lfm = JSON.parse(window?.localStorage?.getItem(RdioScannerService.LOCAL_STORAGE_KEY) || "{}"); - if (map) { - try { - this.livefeedMap = JSON.parse(map); - - } 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 { diff --git a/client/src/app/components/rdio-scanner/rdio-scanner.ts b/client/src/app/components/rdio-scanner/rdio-scanner.ts index 86174e2..48a94d9 100644 --- a/client/src/app/components/rdio-scanner/rdio-scanner.ts +++ b/client/src/app/components/rdio-scanner/rdio-scanner.ts @@ -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; }; } diff --git a/client/src/app/components/rdio-scanner/select/select.component.html b/client/src/app/components/rdio-scanner/select/select.component.html index 2c0ec4f..430bb78 100644 --- a/client/src/app/components/rdio-scanner/select/select.component.html +++ b/client/src/app/components/rdio-scanner/select/select.component.html @@ -19,7 +19,7 @@
diff --git a/docs/platforms/darwin.md b/docs/platforms/darwin.md index 8c27313..5c48a87 100644 --- a/docs/platforms/darwin.md +++ b/docs/platforms/darwin.md @@ -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 diff --git a/docs/platforms/freebsd.md b/docs/platforms/freebsd.md index 11a535e..f091600 100644 --- a/docs/platforms/freebsd.md +++ b/docs/platforms/freebsd.md @@ -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 diff --git a/docs/platforms/linux.md b/docs/platforms/linux.md index 81e326e..95acfe8 100644 --- a/docs/platforms/linux.md +++ b/docs/platforms/linux.md @@ -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 diff --git a/docs/platforms/windows.md b/docs/platforms/windows.md index 78df23c..b7692b8 100644 --- a/docs/platforms/windows.md +++ b/docs/platforms/windows.md @@ -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{} diff --git a/docs/webapp.md b/docs/webapp.md index ee326ab..7d895be 100644 --- a/docs/webapp.md +++ b/docs/webapp.md @@ -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. | @@ -82,12 +82,12 @@ 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. | -| ![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. | +| 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. 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. | ## Search panel diff --git a/server/access.go b/server/access.go index ad772c0..e5077f8 100644 --- a/server/access.go +++ b/server/access.go @@ -25,20 +25,20 @@ import ( ) type Access struct { - Id interface{} `json:"_id"` - Code string `json:"code"` - Expiration interface{} `json:"expiration"` - Ident string `json:"ident"` - Limit interface{} `json:"limit"` - Order interface{} `json:"order"` - Systems interface{} `json:"systems"` + Id any `json:"_id"` + Code string `json:"code"` + Expiration any `json:"expiration"` + Ident string `json:"ident"` + 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() diff --git a/server/admin.go b/server/admin.go index 8ecf748..6941d74 100644 --- a/server/admin.go +++ b/server/admin.go @@ -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"]) } diff --git a/server/api.go b/server/api.go index 8151288..2c343d4 100644 --- a/server/api.go +++ b/server/api.go @@ -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))) +} diff --git a/server/apikey.go b/server/apikey.go index 46be4d3..cb93b09 100644 --- a/server/apikey.go +++ b/server/apikey.go @@ -26,15 +26,15 @@ import ( ) type Apikey struct { - Id interface{} `json:"_id"` - Disabled bool `json:"disabled"` - Ident string `json:"ident"` - Key string `json:"key"` - Order interface{} `json:"order"` - Systems interface{} `json:"systems"` + Id any `json:"_id"` + Disabled bool `json:"disabled"` + Ident string `json:"ident"` + Key string `json:"key"` + 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() diff --git a/server/call.go b/server/call.go index e98f99b..dc8b284 100644 --- a/server/call.go +++ b/server/call.go @@ -27,31 +27,31 @@ import ( ) type Call struct { - Id interface{} `json:"id"` - Audio []byte `json:"audio"` - AudioName interface{} `json:"audioName"` - AudioType interface{} `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"` - System uint `json:"system"` - Talkgroup uint `json:"talkgroup"` - systemLabel interface{} - talkgroupGroup interface{} - talkgroupLabel interface{} - talkgroupName interface{} - talkgroupTag interface{} - units interface{} + Id any `json:"id"` + Audio []byte `json:"audio"` + AudioName any `json:"audioName"` + AudioType any `json:"audioType"` + DateTime time.Time `json:"dateTime"` + 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 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 { diff --git a/server/client.go b/server/client.go index 0db221a..dbda9ab 100644 --- a/server/client.go +++ b/server/client.go @@ -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) } diff --git a/server/command.go b/server/command.go index 4982829..6762ffe 100644 --- a/server/command.go +++ b/server/command.go @@ -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 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 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 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) } diff --git a/server/controller.go b/server/controller.go index c3dd52c..7c2fd9e 100644 --- a/server/controller.go +++ b/server/controller.go @@ -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 diff --git a/server/database.go b/server/database.go index ed284e4..a4da93d 100644 --- a/server/database.go +++ b/server/database.go @@ -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 diff --git a/server/dirwatch.go b/server/dirwatch.go index d386d20..53febbb 100644 --- a/server/dirwatch.go +++ b/server/dirwatch.go @@ -44,25 +44,25 @@ const ( ) type Dirwatch struct { - Id interface{} `json:"_id"` - Delay interface{} `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"` - UsePolling bool `json:"usePolling"` + Id any `json:"_id"` + Delay any `json:"delay"` + DeleteAfter bool `json:"deleteAfter"` + Directory string `json:"directory"` + Disabled bool `json:"disabled"` + 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) diff --git a/server/downstream.go b/server/downstream.go index 58b699c..2700595 100644 --- a/server/downstream.go +++ b/server/downstream.go @@ -32,15 +32,15 @@ import ( ) type Downstream struct { - Id interface{} `json:"_id"` - Apikey string `json:"apiKey"` - Disabled bool `json:"disabled"` - Order interface{} `json:"order"` - Systems interface{} `json:"systems"` - Url string `json:"url"` + Id any `json:"_id"` + Apikey string `json:"apiKey"` + Disabled bool `json:"disabled"` + 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() diff --git a/server/go.mod b/server/go.mod index 68adfdc..6278d88 100644 --- a/server/go.mod +++ b/server/go.mod @@ -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 diff --git a/server/go.sum b/server/go.sum index c780b72..de78b90 100644 --- a/server/go.sum +++ b/server/go.sum @@ -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= diff --git a/server/group.go b/server/group.go index feca449..f6e0c8b 100644 --- a/server/group.go +++ b/server/group.go @@ -24,11 +24,11 @@ import ( ) type Group struct { - Id interface{} `json:"_id"` - Label string `json:"label"` + 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() diff --git a/server/livefeed.go b/server/livefeed.go index 967051a..90e8415 100644 --- a/server/livefeed.go +++ b/server/livefeed.go @@ -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: diff --git a/server/log.go b/server/log.go index d3ffc3f..302c8b9 100644 --- a/server/log.go +++ b/server/log.go @@ -31,10 +31,10 @@ const ( ) type Log struct { - Id interface{} `json:"_id"` - DateTime time.Time `json:"dateTime"` - Level string `json:"level"` - Message string `json:"message"` + Id any `json:"_id"` + DateTime time.Time `json:"dateTime"` + Level string `json:"level"` + Message string `json:"message"` } type Logs struct { @@ -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 { diff --git a/server/main.go b/server/main.go index dc8beee..cb95f89 100644 --- a/server/main.go +++ b/server/main.go @@ -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 } diff --git a/server/message.go b/server/message.go index e97bb33..3e9eaf3 100644 --- a/server/message.go +++ b/server/message.go @@ -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) diff --git a/server/options.go b/server/options.go index c570008..7a854e9 100644 --- a/server/options.go +++ b/server/options.go @@ -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, diff --git a/server/parsers.go b/server/parsers.go index fc53776..129894d 100644 --- a/server/parsers.go +++ b/server/parsers.go @@ -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 { diff --git a/server/scheduler.go b/server/scheduler.go index 43e5ac0..de51f44 100644 --- a/server/scheduler.go +++ b/server/scheduler.go @@ -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 { diff --git a/server/system.go b/server/system.go index a79e44a..d75ff2d 100644 --- a/server/system.go +++ b/server/system.go @@ -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 { diff --git a/server/tag.go b/server/tag.go index 5135e36..7994a2e 100644 --- a/server/tag.go +++ b/server/tag.go @@ -24,11 +24,11 @@ import ( ) type Tag struct { - Id interface{} `json:"_id"` - Label string `json:"label"` + 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() diff --git a/server/talkgroup.go b/server/talkgroup.go index eb6fbb5..06432de 100644 --- a/server/talkgroup.go +++ b/server/talkgroup.go @@ -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"` - Name string `json:"name"` - Order uint `json:"order"` - TagId uint `json:"tagId"` + GroupId uint `json:"groupId"` + Id uint `json:"id"` + Label string `json:"label"` + 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() diff --git a/server/unit.go b/server/unit.go index a64f784..1351761 100644 --- a/server/unit.go +++ b/server/unit.go @@ -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) diff --git a/server/version.go b/server/version.go index 70b2904..f04878d 100644 --- a/server/version.go +++ b/server/version.go @@ -15,4 +15,4 @@ package main -const Version = "6.4.5" +const Version = "6.5.0"