incidents UI initial #95
46 changed files with 2847 additions and 223 deletions
71
README.md
71
README.md
|
@ -1,10 +1,77 @@
|
|||
# stillbox
|
||||
|
||||
A Golang scanner call server.
|
||||
*"When they say stillbox, you know it's real."*
|
||||
|
||||
**NOTE**
|
||||
A Golang scanner call server and Angular frontend. Basically a rewrite of [rdio-scanner](https://github.com/chuot/rdio-scanner)
|
||||
with a cleaner *backend* (not frontend, so far; I am not a frontend guy and it shows) and a more opinionated featureset.
|
||||
|
||||
Primary differences:
|
||||
|
||||
- [x] Backend written as if Go is actually a typed language
|
||||
|
||||
I never would have started this project if existing projects were this way, as modifying them would have been far easier.
|
||||
|
||||
- [x] No directory watch source, for now
|
||||
|
||||
This is not a feature I need personally, but would be simple to implement as another [source](pkg/sources/).
|
||||
|
||||
- [x] Only supports Postgres DB right now. May add SQLite someday.
|
||||
|
||||
Most all database access is abstracted using a repository architecture. The calls table is also partitioned, with configurable intervals and retention.
|
||||
|
||||
- [x] Filter calls by duration in search
|
||||
|
||||
This feature was a major impetus for even starting the project.
|
||||
|
||||
- [x] Both REST and WebSocket APIs
|
||||
|
||||
REST is used for the admin interface, and WebSockets/protobuf are used for listener apps, including *Calls* (both Flutter and terminal versions).
|
||||
|
||||
- [x] Uses protobuf instead of JSON for the WebSocket API (no slices of integers for call audio!)
|
||||
|
||||
Another thing that originally spawned the project.
|
||||
|
||||
- [x] Talkgroup activity alerting
|
||||
|
||||
We use [go-trending](https://github.com/codesuki/go-trending) to implement this.
|
||||
|
||||
- [x] "Native" flutter client (Calls) for Android/iOS/macOS/Linux/Windows (in progress)
|
||||
|
||||
This client, as of this writing, works for listening linearly only. More functionality to come. Currently available [here](https://github.com/amigan/calls).
|
||||
|
||||
- [x] RadioReference talkgroup import, SDRtrunk talkgroup playlist export
|
||||
|
||||
Just copy and paste from RadioReference. You can also have your talkgroup labels in SDRtrunk!
|
||||
|
||||
- [x] Incidents functionality (group past calls together and retain them)
|
||||
|
||||
Keep track of interesting past calls and link them to your friends. Calls linked to an incident are even swept away before retention pruning is done!
|
||||
|
||||
- [x] No premiumization nags or advertising of any kind
|
||||
|
||||
Another thing that drove me to start this project. It's either open source, or it isn't, folks.
|
||||
|
||||
- [x] 3-clause BSD license
|
||||
|
||||
But that doesn't mean I don't want your improvements so that I may incorporate them!
|
||||
|
||||
## Note
|
||||
|
||||
If this message is still here, the database schema *initial migration* and protobuf definitions are **still subject to change**.
|
||||
|
||||
Once `stillbox` is actually usable (but not necessarily feature-complete), I will remove this note, and start using DB migrations and
|
||||
protobuf best practices (i.e. not changing field numbers).
|
||||
|
||||
## License and Copyright
|
||||
|
||||
© 2024, Daniel Ponte <dan AT dynatron DOT me>
|
||||
|
||||
Licensed under the 3-clause BSD license. See LICENSE for details.
|
||||
|
||||
## Credits
|
||||
|
||||
Thanks to, among others:
|
||||
* rdio-scanner for the original inspiration
|
||||
* [go-trending](https://github.com/codesuki/go-trending) and [go-time-series](https://github.com/codesuki/go-time-series)
|
||||
* [isoweek](https://github.com/snabb/isoweek)
|
||||
* [minimp3](https://github.com/tosone/minimp3)
|
||||
|
|
|
@ -26,11 +26,9 @@ import {
|
|||
ReactiveFormsModule,
|
||||
} from '@angular/forms';
|
||||
import { MatInputModule } from '@angular/material/input';
|
||||
import { MatTimepickerModule } from '@angular/material/timepicker';
|
||||
import { MatDatepickerModule } from '@angular/material/datepicker';
|
||||
import { debounceTime } from 'rxjs/operators';
|
||||
import { ToolbarContextService } from '../navigation/toolbar-context.service';
|
||||
import { MatSelect, MatSelectModule } from '@angular/material/select';
|
||||
import { MatSelectModule } from '@angular/material/select';
|
||||
|
||||
@Pipe({
|
||||
name: 'grabDate',
|
||||
|
@ -55,7 +53,7 @@ export class TimePipe implements PipeTransform {
|
|||
return timestamp.toLocaleTimeString(navigator.language, {
|
||||
hour: '2-digit',
|
||||
minute: '2-digit',
|
||||
hourCycle: 'h24',
|
||||
hourCycle: 'h23',
|
||||
});
|
||||
}
|
||||
}
|
||||
|
@ -137,8 +135,6 @@ const reqPageSize = 200;
|
|||
ReactiveFormsModule,
|
||||
FormsModule,
|
||||
MatInputModule,
|
||||
MatDatepickerModule,
|
||||
MatTimepickerModule,
|
||||
MatCheckboxModule,
|
||||
CommonModule,
|
||||
MatProgressSpinnerModule,
|
||||
|
@ -166,6 +162,7 @@ export class CallsComponent {
|
|||
'duration',
|
||||
];
|
||||
curPage = <PageEvent>{ pageIndex: 0, pageSize: 0 };
|
||||
curLen = 0;
|
||||
currentSet!: CallRecord[];
|
||||
currentServerPage = 0; // page is never 0, forces load
|
||||
isLoading = true;
|
||||
|
@ -198,7 +195,7 @@ export class CallsComponent {
|
|||
|
||||
isAllSelected() {
|
||||
const numSelected = this.selection.selected.length;
|
||||
const numRows = this.curPage.pageSize;
|
||||
const numRows = this.curLen;
|
||||
return numSelected === numRows;
|
||||
}
|
||||
|
||||
|
@ -274,7 +271,9 @@ export class CallsComponent {
|
|||
this.pageWindow = pageStart % reqPageSize;
|
||||
if (serverPage == this.currentServerPage && !force && this.currentSet) {
|
||||
this.callsResult.next(
|
||||
this.callsResult ? this.currentSet.slice(this.pageWindow, this.pageWindow + p.pageSize) : [],
|
||||
this.callsResult
|
||||
? this.currentSet.slice(this.pageWindow, this.pageWindow + p.pageSize)
|
||||
: [],
|
||||
);
|
||||
} else {
|
||||
this.currentServerPage = serverPage;
|
||||
|
@ -332,6 +331,11 @@ export class CallsComponent {
|
|||
);
|
||||
}),
|
||||
);
|
||||
this.subscriptions.add(
|
||||
this.callsResult.subscribe((cr) => {
|
||||
this.curLen = cr.length;
|
||||
}),
|
||||
);
|
||||
}
|
||||
|
||||
resetFilter() {
|
||||
|
|
17
client/stillbox/src/app/incidents.ts
Normal file
17
client/stillbox/src/app/incidents.ts
Normal file
|
@ -0,0 +1,17 @@
|
|||
import { CallRecord } from './calls';
|
||||
|
||||
export interface IncidentCall extends CallRecord {
|
||||
notes: Object | null;
|
||||
}
|
||||
|
||||
export interface IncidentRecord {
|
||||
id: string;
|
||||
name: string | null;
|
||||
description: string | null;
|
||||
startTime: Date | null;
|
||||
endTime: Date | null;
|
||||
location: Object | null;
|
||||
metadata: Object | null;
|
||||
calls: IncidentCall[] | null;
|
||||
callCount: number | null;
|
||||
}
|
|
@ -0,0 +1 @@
|
|||
<p>incident works!</p>
|
|
@ -0,0 +1,22 @@
|
|||
import { ComponentFixture, TestBed } from '@angular/core/testing';
|
||||
|
||||
import { IncidentComponent } from './incident.component';
|
||||
|
||||
describe('IncidentComponent', () => {
|
||||
let component: IncidentComponent;
|
||||
let fixture: ComponentFixture<IncidentComponent>;
|
||||
|
||||
beforeEach(async () => {
|
||||
await TestBed.configureTestingModule({
|
||||
imports: [IncidentComponent],
|
||||
}).compileComponents();
|
||||
|
||||
fixture = TestBed.createComponent(IncidentComponent);
|
||||
component = fixture.componentInstance;
|
||||
fixture.detectChanges();
|
||||
});
|
||||
|
||||
it('should create', () => {
|
||||
expect(component).toBeTruthy();
|
||||
});
|
||||
});
|
|
@ -0,0 +1,9 @@
|
|||
import { Component } from '@angular/core';
|
||||
|
||||
@Component({
|
||||
selector: 'app-incident',
|
||||
imports: [],
|
||||
templateUrl: './incident.component.html',
|
||||
styleUrl: './incident.component.scss',
|
||||
})
|
||||
export class IncidentComponent {}
|
|
@ -1 +1,126 @@
|
|||
<p>incidents works!</p>
|
||||
<div [ngClass]="{ hidden: (tcSvc.filterPanel | async) }" class="toolbar">
|
||||
<form [formGroup]="form">
|
||||
<mat-form-field subscriptSizing="dynamic" class="timeFilterBox">
|
||||
<mat-label>Start</mat-label>
|
||||
<input
|
||||
matInput
|
||||
type="datetime-local"
|
||||
name="start"
|
||||
placeholder="Start date"
|
||||
formControlName="start"
|
||||
/>
|
||||
</mat-form-field>
|
||||
<mat-form-field subscriptSizing="dynamic" class="timeFilterBox">
|
||||
<mat-label>End</mat-label>
|
||||
<input
|
||||
matInput
|
||||
type="datetime-local"
|
||||
name="end"
|
||||
placeholder="End date"
|
||||
formControlName="end"
|
||||
/>
|
||||
</mat-form-field>
|
||||
<mat-form-field class="filterBox" subscriptSizing="dynamic">
|
||||
<mat-label>Filter</mat-label>
|
||||
<input
|
||||
matInput
|
||||
name="filter"
|
||||
type="text"
|
||||
autocomplete="off"
|
||||
formControlName="filter"
|
||||
/>
|
||||
<button
|
||||
class="clearBtn"
|
||||
*ngIf="form.controls['filter'].value"
|
||||
matSuffix
|
||||
mat-icon-button
|
||||
(click)="form.controls['filter'].setValue('')"
|
||||
>
|
||||
<mat-icon>close</mat-icon>
|
||||
</button>
|
||||
</mat-form-field>
|
||||
<div class="toolbarButtons">
|
||||
<button class="sbButton" (click)="resetFilter()">
|
||||
<mat-icon class="material-symbols-outlined">reset_settings</mat-icon>
|
||||
</button>
|
||||
<button class="sbButton" (click)="refresh()">
|
||||
<mat-icon>refresh</mat-icon>
|
||||
</button>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
<div class="tabContainer" *ngIf="!isLoading; else spinner">
|
||||
<table class="incsTable" mat-table [dataSource]="incsResult">
|
||||
<ng-container matColumnDef="select">
|
||||
<th mat-header-cell *matHeaderCellDef>
|
||||
<mat-checkbox
|
||||
(change)="$event ? masterToggle() : null"
|
||||
[checked]="selection.hasValue() && isAllSelected()"
|
||||
[indeterminate]="selection.hasValue() && !isAllSelected()"
|
||||
>
|
||||
</mat-checkbox>
|
||||
</th>
|
||||
<td mat-cell *matCellDef="let row">
|
||||
<mat-checkbox
|
||||
(click)="$event.stopPropagation()"
|
||||
(change)="$event ? selection.toggle(row) : null"
|
||||
[checked]="selection.isSelected(row)"
|
||||
>
|
||||
</mat-checkbox>
|
||||
</td>
|
||||
</ng-container>
|
||||
<ng-container matColumnDef="startTime">
|
||||
<th mat-header-cell *matHeaderCellDef>Start</th>
|
||||
<td mat-cell *matCellDef="let incident">
|
||||
{{ incident.startTime | fmtDate }}
|
||||
</td>
|
||||
</ng-container>
|
||||
<ng-container matColumnDef="endTime">
|
||||
<th mat-header-cell *matHeaderCellDef>End</th>
|
||||
<td [title]="incident.incident_date" mat-cell *matCellDef="let incident">
|
||||
{{ incident.endTime | fmtDate }}
|
||||
</td>
|
||||
</ng-container>
|
||||
<ng-container matColumnDef="name">
|
||||
<th mat-header-cell *matHeaderCellDef>Name</th>
|
||||
<td mat-cell *matCellDef="let incident">
|
||||
{{ incident.name }}
|
||||
</td>
|
||||
</ng-container>
|
||||
<ng-container matColumnDef="numCalls">
|
||||
<th mat-header-cell *matHeaderCellDef>Calls</th>
|
||||
<td mat-cell *matCellDef="let incident" class="callCount">
|
||||
{{ incident.callCount }}
|
||||
</td>
|
||||
</ng-container>
|
||||
<ng-container matColumnDef="edit">
|
||||
<th mat-header-cell *matHeaderCellDef>Edit</th>
|
||||
<td mat-cell *matCellDef="let incident">
|
||||
<a routerLink="/incidents/{{ incident.id }}"
|
||||
><mat-icon>edit</mat-icon>
|
||||
</a>
|
||||
</td>
|
||||
</ng-container>
|
||||
<tr mat-header-row *matHeaderRowDef="columns; sticky: true"></tr>
|
||||
<tr mat-row *matRowDef="let myRowData; columns: columns"></tr>
|
||||
</table>
|
||||
</div>
|
||||
<div class="pagFoot">
|
||||
<mat-paginator
|
||||
#paginator
|
||||
class="paginator"
|
||||
(page)="setPage($event)"
|
||||
[length]="count"
|
||||
showFirstLastButtons="true"
|
||||
[pageSize]="curPage.pageSize"
|
||||
[pageSizeOptions]="pageSizeOptions"
|
||||
[pageIndex]="curPage.pageIndex"
|
||||
aria-label="Select page"
|
||||
>
|
||||
</mat-paginator>
|
||||
</div>
|
||||
<ng-template #spinner>
|
||||
<div class="spinner">
|
||||
<mat-spinner></mat-spinner>
|
||||
</div>
|
||||
</ng-template>
|
||||
|
|
|
@ -0,0 +1,63 @@
|
|||
.timeFilterBox {
|
||||
flex: 0 0 240px;
|
||||
}
|
||||
|
||||
table,
|
||||
.incsTable {
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.callCount {
|
||||
text-align: right;
|
||||
}
|
||||
|
||||
.mat-column-numCalls {
|
||||
width: 50px;
|
||||
}
|
||||
|
||||
.tabContainer {
|
||||
max-height: calc(
|
||||
(
|
||||
100vh - var(--mat-mat-maginator-container-size, 56px) - 2 *
|
||||
(var(--mat-toolbar-standard-height, 64px))
|
||||
) + 7px
|
||||
);
|
||||
overflow: auto;
|
||||
}
|
||||
|
||||
.toolbarButtons button,
|
||||
.toolbarButtons form,
|
||||
.toolbarButtons form button {
|
||||
justify-content: flex-end;
|
||||
align-content: center;
|
||||
}
|
||||
|
||||
tr.mat-mdc-row {
|
||||
height: 2.3rem !important;
|
||||
font-size: 12pt;
|
||||
}
|
||||
|
||||
.mdc-text-field__input::-webkit-calendar-picker-indicator {
|
||||
display: block !important;
|
||||
}
|
||||
|
||||
@media screen and (max-width: 768px) {
|
||||
.tabFootContainer {
|
||||
padding: 0;
|
||||
}
|
||||
}
|
||||
|
||||
.toolbar,
|
||||
.toolbar form {
|
||||
display: flex;
|
||||
flex-flow: row wrap;
|
||||
}
|
||||
|
||||
form {
|
||||
flex: 1 0;
|
||||
display: flex;
|
||||
}
|
||||
|
||||
.filterBox {
|
||||
flex: 1 1 300px;
|
||||
}
|
|
@ -1,9 +1,248 @@
|
|||
import { Component } from '@angular/core';
|
||||
import { Component, Pipe, PipeTransform, ViewChild } from '@angular/core';
|
||||
import { CommonModule, AsyncPipe } from '@angular/common';
|
||||
import { RouterLink } from '@angular/router';
|
||||
import { MatProgressSpinnerModule } from '@angular/material/progress-spinner';
|
||||
import { MatTableModule } from '@angular/material/table';
|
||||
import {
|
||||
MatPaginator,
|
||||
MatPaginatorModule,
|
||||
PageEvent,
|
||||
} from '@angular/material/paginator';
|
||||
import { PrefsService } from '../prefs/prefs.service';
|
||||
import { MatIconModule } from '@angular/material/icon';
|
||||
import { SelectionModel } from '@angular/cdk/collections';
|
||||
import { MatCheckboxModule } from '@angular/material/checkbox';
|
||||
import { BehaviorSubject, Subscription } from 'rxjs';
|
||||
import { switchMap } from 'rxjs/operators';
|
||||
import { IncidentsListParams, IncidentsService } from './incidents.service';
|
||||
import { IncidentRecord } from '../incidents';
|
||||
|
||||
import { TalkgroupService } from '../talkgroups/talkgroups.service';
|
||||
import { MatFormFieldModule } from '@angular/material/form-field';
|
||||
import {
|
||||
FormControl,
|
||||
FormGroup,
|
||||
FormsModule,
|
||||
ReactiveFormsModule,
|
||||
} from '@angular/forms';
|
||||
import { MatInputModule } from '@angular/material/input';
|
||||
import { debounceTime } from 'rxjs/operators';
|
||||
import { ToolbarContextService } from '../navigation/toolbar-context.service';
|
||||
|
||||
@Pipe({
|
||||
name: 'fmtDate',
|
||||
standalone: true,
|
||||
pure: true,
|
||||
})
|
||||
export class DatePipe implements PipeTransform {
|
||||
transform(ts: string, args?: any): string {
|
||||
if (!ts) {
|
||||
return '\u2014';
|
||||
}
|
||||
const timestamp = new Date(ts);
|
||||
return (
|
||||
timestamp.getMonth() +
|
||||
1 +
|
||||
'/' +
|
||||
timestamp.getDate() +
|
||||
' ' +
|
||||
timestamp.toLocaleTimeString(navigator.language, {
|
||||
hour: '2-digit',
|
||||
minute: '2-digit',
|
||||
hourCycle: 'h23',
|
||||
})
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
const reqPageSize = 200;
|
||||
|
||||
@Component({
|
||||
selector: 'app-incidents',
|
||||
imports: [],
|
||||
imports: [
|
||||
MatIconModule,
|
||||
DatePipe,
|
||||
MatPaginatorModule,
|
||||
MatTableModule,
|
||||
AsyncPipe,
|
||||
MatFormFieldModule,
|
||||
ReactiveFormsModule,
|
||||
FormsModule,
|
||||
MatInputModule,
|
||||
MatCheckboxModule,
|
||||
RouterLink,
|
||||
CommonModule,
|
||||
MatProgressSpinnerModule,
|
||||
],
|
||||
templateUrl: './incidents.component.html',
|
||||
styleUrl: './incidents.component.scss',
|
||||
})
|
||||
export class IncidentsComponent {}
|
||||
export class IncidentsComponent {
|
||||
incsResult = new BehaviorSubject(new Array<IncidentRecord>(0));
|
||||
@ViewChild('paginator') paginator!: MatPaginator;
|
||||
count = 0;
|
||||
curLen = 0;
|
||||
page = 0;
|
||||
perPage = 25;
|
||||
pageSizeOptions = [25, 50, 75, 100, 200];
|
||||
columns = ['select', 'startTime', 'endTime', 'name', 'numCalls', 'edit'];
|
||||
curPage = <PageEvent>{ pageIndex: 0, pageSize: 0 };
|
||||
currentSet!: IncidentRecord[];
|
||||
currentServerPage = 0; // page is never 0, forces load
|
||||
isLoading = true;
|
||||
|
||||
selection = new SelectionModel<IncidentRecord>(true, []);
|
||||
|
||||
form = new FormGroup({
|
||||
start: new FormControl(null),
|
||||
end: new FormControl(null),
|
||||
filter: new FormControl(''),
|
||||
});
|
||||
|
||||
subscriptions = new Subscription();
|
||||
pageWindow = 0;
|
||||
fetchIncidents = new BehaviorSubject<IncidentsListParams>(
|
||||
this.buildParams(this.curPage, this.curPage.pageIndex),
|
||||
);
|
||||
|
||||
constructor(
|
||||
private incidentsSvc: IncidentsService,
|
||||
private prefsSvc: PrefsService,
|
||||
public tcSvc: ToolbarContextService,
|
||||
public tgSvc: TalkgroupService,
|
||||
) {
|
||||
this.tcSvc.showFilterButton();
|
||||
}
|
||||
|
||||
isAllSelected() {
|
||||
const numSelected = this.selection.selected.length;
|
||||
const numRows = this.curLen;
|
||||
return numSelected === numRows;
|
||||
}
|
||||
|
||||
buildParams(p: PageEvent, serverPage: number): IncidentsListParams {
|
||||
const par: IncidentsListParams = {
|
||||
start:
|
||||
this.form.controls['start'].value != null
|
||||
? new Date(this.form.controls['start'].value!)
|
||||
: null,
|
||||
page: serverPage,
|
||||
perPage: reqPageSize,
|
||||
end:
|
||||
this.form.controls['end'].value != null
|
||||
? new Date(this.form.controls['end'].value!)
|
||||
: null,
|
||||
dir: 'desc',
|
||||
filter:
|
||||
this.form.controls['filter'].value != ''
|
||||
? this.form.controls['filter'].value
|
||||
: null,
|
||||
};
|
||||
|
||||
return par;
|
||||
}
|
||||
|
||||
masterToggle() {
|
||||
this.isAllSelected()
|
||||
? this.selection.clear()
|
||||
: this.incsResult.value.forEach((row) => this.selection.select(row));
|
||||
}
|
||||
|
||||
lTime(now: Date): string {
|
||||
now.setDate(new Date().getDate() - 7);
|
||||
now.setMinutes(now.getMinutes() - now.getTimezoneOffset());
|
||||
return now.toISOString().slice(0, 16);
|
||||
}
|
||||
|
||||
setPage(p: PageEvent, force?: boolean) {
|
||||
this.selection.clear();
|
||||
this.curPage = p;
|
||||
if (p && p!.pageSize != this.perPage) {
|
||||
this.perPage = p!.pageSize;
|
||||
this.prefsSvc.set('incidentsPerPage', p!.pageSize);
|
||||
}
|
||||
this.getIncidents(p, force);
|
||||
}
|
||||
|
||||
refresh() {
|
||||
this.selection.clear();
|
||||
this.getIncidents(this.curPage, true);
|
||||
}
|
||||
|
||||
getIncidents(p: PageEvent, force?: boolean) {
|
||||
const pageStart = p.pageIndex * p.pageSize;
|
||||
const serverPage = Math.floor(pageStart / reqPageSize) + 1;
|
||||
this.pageWindow = pageStart % reqPageSize;
|
||||
if (serverPage == this.currentServerPage && !force && this.currentSet) {
|
||||
this.incsResult.next(
|
||||
this.incsResult
|
||||
? this.currentSet.slice(this.pageWindow, this.pageWindow + p.pageSize)
|
||||
: [],
|
||||
);
|
||||
} else {
|
||||
this.currentServerPage = serverPage;
|
||||
this.fetchIncidents.next(this.buildParams(p, serverPage));
|
||||
}
|
||||
}
|
||||
|
||||
zeroPage(): PageEvent {
|
||||
return <PageEvent>{
|
||||
pageIndex: 0,
|
||||
pageSize: this.curPage.pageSize,
|
||||
};
|
||||
}
|
||||
|
||||
ngOnDestroy() {
|
||||
this.tcSvc.hideFilterButton();
|
||||
this.subscriptions.unsubscribe();
|
||||
}
|
||||
|
||||
ngOnInit() {
|
||||
this.form.valueChanges.pipe(debounceTime(300)).subscribe(() => {
|
||||
this.currentServerPage = 0;
|
||||
this.setPage(this.zeroPage(), true);
|
||||
});
|
||||
this.subscriptions.add(
|
||||
this.prefsSvc.get('incidentsPerPage').subscribe((cpp) => {
|
||||
if (cpp && cpp != this.perPage) {
|
||||
this.perPage = cpp;
|
||||
|
||||
this.setPage(<PageEvent>{
|
||||
pageIndex: 0,
|
||||
pageSize: cpp,
|
||||
});
|
||||
}
|
||||
}),
|
||||
);
|
||||
this.subscriptions.add(
|
||||
this.fetchIncidents
|
||||
.pipe(
|
||||
switchMap((params) => {
|
||||
return this.incidentsSvc.getIncidents(params);
|
||||
}),
|
||||
)
|
||||
.subscribe((incidents) => {
|
||||
this.isLoading = false;
|
||||
this.count = incidents.count;
|
||||
this.currentSet = incidents.incidents;
|
||||
this.incsResult.next(
|
||||
this.currentSet
|
||||
? this.currentSet.slice(
|
||||
this.pageWindow,
|
||||
this.pageWindow + this.perPage,
|
||||
)
|
||||
: [],
|
||||
);
|
||||
}),
|
||||
);
|
||||
this.subscriptions.add(
|
||||
this.incsResult.subscribe((cr) => {
|
||||
this.curLen = cr.length;
|
||||
}),
|
||||
);
|
||||
}
|
||||
|
||||
resetFilter() {
|
||||
this.form.reset();
|
||||
}
|
||||
}
|
||||
|
|
16
client/stillbox/src/app/incidents/incidents.service.spec.ts
Normal file
16
client/stillbox/src/app/incidents/incidents.service.spec.ts
Normal file
|
@ -0,0 +1,16 @@
|
|||
import { TestBed } from '@angular/core/testing';
|
||||
|
||||
import { IncidentsService } from './incidents.service';
|
||||
|
||||
describe('IncidentsService', () => {
|
||||
let service: IncidentsService;
|
||||
|
||||
beforeEach(() => {
|
||||
TestBed.configureTestingModule({});
|
||||
service = TestBed.inject(IncidentsService);
|
||||
});
|
||||
|
||||
it('should be created', () => {
|
||||
expect(service).toBeTruthy();
|
||||
});
|
||||
});
|
50
client/stillbox/src/app/incidents/incidents.service.ts
Normal file
50
client/stillbox/src/app/incidents/incidents.service.ts
Normal file
|
@ -0,0 +1,50 @@
|
|||
import { HttpClient } from '@angular/common/http';
|
||||
import { Injectable } from '@angular/core';
|
||||
import { IncidentRecord } from '../incidents';
|
||||
import { Observable } from 'rxjs';
|
||||
|
||||
export interface IncidentsListParams {
|
||||
start: Date | null;
|
||||
end: Date | null;
|
||||
filter: string | null;
|
||||
dir: string;
|
||||
page: number;
|
||||
perPage: number;
|
||||
}
|
||||
|
||||
export interface CallIncidentParams {
|
||||
add: string[] | null; // IDs
|
||||
notes: Object | null;
|
||||
remove: string[] | null; // IDs
|
||||
}
|
||||
|
||||
export interface IncidentsPaginated {
|
||||
incidents: IncidentRecord[];
|
||||
count: number;
|
||||
}
|
||||
@Injectable({
|
||||
providedIn: 'root',
|
||||
})
|
||||
export class IncidentsService {
|
||||
constructor(private http: HttpClient) {}
|
||||
|
||||
getIncidents(p: IncidentsListParams): Observable<IncidentsPaginated> {
|
||||
return this.http.post<IncidentsPaginated>('/api/incident/', p);
|
||||
}
|
||||
|
||||
createIncident(inp: IncidentRecord): Observable<IncidentRecord> {
|
||||
return this.http.post<IncidentRecord>('/api/incident/new', inp);
|
||||
}
|
||||
|
||||
addRemoveCalls(id: string, inp: CallIncidentParams): Observable<void> {
|
||||
return this.http.post<void>('/api/incident/' + id + '/calls', inp);
|
||||
}
|
||||
|
||||
deleteIncident(id: string): Observable<void> {
|
||||
return this.http.delete<void>('/api/incident/' + id);
|
||||
}
|
||||
|
||||
updateIncident(id: string, inp: IncidentRecord): Observable<IncidentRecord> {
|
||||
return this.http.patch<IncidentRecord>('/api/incident/' + id, inp);
|
||||
}
|
||||
}
|
|
@ -233,5 +233,6 @@ input {
|
|||
.spinner {
|
||||
display: flex;
|
||||
margin-top: 40px;
|
||||
margin-bottom: 40px;
|
||||
justify-content: center;
|
||||
}
|
||||
|
|
|
@ -1,3 +1,6 @@
|
|||
# this is used to compose URLs, for example in M3U responses.
|
||||
# set it to what users access the instance as.
|
||||
baseURL: "https://stillbox.example.com/"
|
||||
db:
|
||||
connect: 'postgres://postgres:password@localhost:5432/example'
|
||||
partition:
|
||||
|
|
|
@ -1,5 +1,11 @@
|
|||
package common
|
||||
|
||||
import "errors"
|
||||
|
||||
var (
|
||||
ErrPageOutOfRange = errors.New("requested page out of range")
|
||||
)
|
||||
|
||||
type Pagination struct {
|
||||
Page *int `json:"page"`
|
||||
PerPage *int `json:"perPage"`
|
||||
|
|
|
@ -32,7 +32,7 @@ func call(url string, call *calls.Call) error {
|
|||
var buf bytes.Buffer
|
||||
body := multipart.NewWriter(&buf)
|
||||
|
||||
err := forms.Marshal(call, body)
|
||||
err := forms.Marshal(call, body, forms.WithTag("json"))
|
||||
if err != nil {
|
||||
return fmt.Errorf("relay form parse: %w", err)
|
||||
}
|
||||
|
@ -88,6 +88,8 @@ func TestMarshal(t *testing.T) {
|
|||
t.Run(tc.name, func(t *testing.T) {
|
||||
var serr error
|
||||
var called bool
|
||||
|
||||
// setup request handler
|
||||
h := hand(func(w http.ResponseWriter, r *http.Request) {
|
||||
called = true
|
||||
serr = r.ParseMultipartForm(1024 * 1024 * 2)
|
||||
|
@ -112,6 +114,7 @@ func TestMarshal(t *testing.T) {
|
|||
})
|
||||
svr := httptest.NewServer(h)
|
||||
|
||||
// perform the request
|
||||
err := call(svr.URL, &tc.call)
|
||||
assert.True(t, called)
|
||||
assert.NoError(t, err)
|
||||
|
|
22
internal/forms/testdata/uuid1.http
vendored
Normal file
22
internal/forms/testdata/uuid1.http
vendored
Normal file
|
@ -0,0 +1,22 @@
|
|||
POST /api/incident/8ff93b85-c604-11ef-a555-00e04c0122ba/calls HTTP/1.1
|
||||
Connection: Upgrade, HTTP2-Settings
|
||||
Content-Length: 403
|
||||
Host: xenon:3050
|
||||
HTTP2-Settings: AAEAAEAAAAIAAAAAAAMAAAAAAAQBAAAAAAUAAEAAAAYABgAA
|
||||
Upgrade: h2c
|
||||
Content-Type: multipart/form-data; boundary=--sdrtrunk-sdrtrunk-sdrtrunk
|
||||
User-Agent: sdrtrunk
|
||||
|
||||
----sdrtrunk-sdrtrunk-sdrtrunk
|
||||
Content-Disposition: form-data; name="add"
|
||||
|
||||
[f25ef14b-c5f6-11ef-a555-00e04c0122ba,f25ef14b-c5f6-11ef-a555-06e04c0122ba]
|
||||
----sdrtrunk-sdrtrunk-sdrtrunk
|
||||
Content-Disposition: form-data; name="notes"
|
||||
|
||||
{"this":"note"}
|
||||
----sdrtrunk-sdrtrunk-sdrtrunk
|
||||
Content-Disposition: form-data; name="single"
|
||||
|
||||
17cedf8e-c60b-11ef-a555-00e04c0122ba
|
||||
----sdrtrunk-sdrtrunk-sdrtrunk
|
|
@ -251,7 +251,17 @@ func (o *options) unmIterFields(r *http.Request, destStruct reflect.Value) error
|
|||
|
||||
continue
|
||||
}
|
||||
if destFieldType.Kind() == reflect.Ptr {
|
||||
|
||||
if reflect.PointerTo(destFieldType).Implements(textUnmarshaler) {
|
||||
tum := destFieldVal.Addr().Interface().(encoding.TextUnmarshaler)
|
||||
err := tum.UnmarshalText([]byte(ff))
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
continue
|
||||
}
|
||||
for destFieldType.Kind() == reflect.Ptr {
|
||||
destFieldType = destFieldType.Elem()
|
||||
}
|
||||
if reflect.ValueOf(ff).CanConvert(destFieldType) {
|
||||
|
|
|
@ -2,6 +2,7 @@ package forms_test
|
|||
|
||||
import (
|
||||
"bufio"
|
||||
"encoding/json"
|
||||
"errors"
|
||||
"net/http"
|
||||
"os"
|
||||
|
@ -19,6 +20,7 @@ import (
|
|||
"dynatron.me/x/stillbox/pkg/talkgroups/tgstore"
|
||||
"dynatron.me/x/stillbox/pkg/talkgroups/xport"
|
||||
|
||||
"github.com/google/uuid"
|
||||
"github.com/stretchr/testify/assert"
|
||||
"github.com/stretchr/testify/require"
|
||||
)
|
||||
|
@ -33,7 +35,6 @@ type callUploadRequest struct {
|
|||
Key string `form:"key"`
|
||||
Patches []int `form:"patches"`
|
||||
Source int `form:"source"`
|
||||
Sources []int `form:"sources"`
|
||||
System int `form:"system"`
|
||||
SystemLabel string `form:"systemLabel"`
|
||||
Talkgroup int `form:"talkgroup"`
|
||||
|
@ -67,6 +68,14 @@ type ptrTestJT struct {
|
|||
ScoreEnd jsontypes.Time `form:"scoreEnd"`
|
||||
}
|
||||
|
||||
type CallIncidentParams struct {
|
||||
Add jsontypes.UUIDs `json:"add"`
|
||||
Notes json.RawMessage `json:"notes"`
|
||||
|
||||
Remove jsontypes.UUIDs `json:"remove"`
|
||||
Single jsontypes.UUID `json:"single"`
|
||||
}
|
||||
|
||||
var (
|
||||
UrlEncTest = urlEncTest{
|
||||
LookbackDays: 7,
|
||||
|
@ -141,6 +150,15 @@ var (
|
|||
},
|
||||
},
|
||||
}
|
||||
|
||||
Cap1 = CallIncidentParams{
|
||||
Add: jsontypes.UUIDs{
|
||||
jsontypes.UUID(uuid.MustParse("f25ef14b-c5f6-11ef-a555-00e04c0122ba")),
|
||||
jsontypes.UUID(uuid.MustParse("f25ef14b-c5f6-11ef-a555-06e04c0122ba")),
|
||||
},
|
||||
Single: jsontypes.UUID(uuid.MustParse("17cedf8e-c60b-11ef-a555-00e04c0122ba")),
|
||||
Notes: []byte(`{"this":"note"}`),
|
||||
}
|
||||
)
|
||||
|
||||
func makeRequest(fixture string) *http.Request {
|
||||
|
@ -269,6 +287,13 @@ func TestUnmarshal(t *testing.T) {
|
|||
expect: &ExpJob1,
|
||||
opts: []forms.Option{forms.WithAcceptBlank(), forms.WithOmitEmpty()},
|
||||
},
|
||||
{
|
||||
name: "uuid and json raw message",
|
||||
r: makeRequest("uuid1.http"),
|
||||
dest: &CallIncidentParams{},
|
||||
expect: &Cap1,
|
||||
opts: []forms.Option{forms.WithTag("json"), forms.WithAcceptBlank(), forms.WithOmitEmpty()},
|
||||
},
|
||||
}
|
||||
|
||||
for _, tc := range tests {
|
||||
|
|
|
@ -28,6 +28,14 @@ func (t *Time) UnmarshalYAML(n *yaml.Node) error {
|
|||
return nil
|
||||
}
|
||||
|
||||
func TimePtrFromTSTZ(t pgtype.Timestamptz) *Time {
|
||||
if t.Valid {
|
||||
return (*Time)(&t.Time)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (t *Time) PGTypeTSTZ() pgtype.Timestamptz {
|
||||
if t == nil {
|
||||
return pgtype.Timestamptz{Valid: false}
|
||||
|
|
9
internal/jsontypes/location.go
Normal file
9
internal/jsontypes/location.go
Normal file
|
@ -0,0 +1,9 @@
|
|||
package jsontypes
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
)
|
||||
|
||||
type Location struct {
|
||||
json.RawMessage
|
||||
}
|
57
internal/jsontypes/url.go
Normal file
57
internal/jsontypes/url.go
Normal file
|
@ -0,0 +1,57 @@
|
|||
package jsontypes
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"net/url"
|
||||
|
||||
"gopkg.in/yaml.v3"
|
||||
)
|
||||
|
||||
type URL url.URL
|
||||
|
||||
func (u *URL) URL() url.URL {
|
||||
return url.URL(*u)
|
||||
}
|
||||
|
||||
func (u *URL) UnmarshalJSON(b []byte) error {
|
||||
var s string
|
||||
err := json.Unmarshal(b, &s)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
ur, err := url.Parse(s)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
*u = URL(*ur)
|
||||
return nil
|
||||
}
|
||||
|
||||
func (u *URL) UnmarshalYAML(n *yaml.Node) error {
|
||||
var s string
|
||||
|
||||
err := n.Decode(&s)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
ur, err := url.Parse(s)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
*u = URL(*ur)
|
||||
return nil
|
||||
}
|
||||
|
||||
func (u *URL) UnmarshalText(t []byte) error {
|
||||
ur, err := url.Parse(string(t))
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
*u = URL(*ur)
|
||||
return nil
|
||||
}
|
70
internal/jsontypes/uuid.go
Normal file
70
internal/jsontypes/uuid.go
Normal file
|
@ -0,0 +1,70 @@
|
|||
package jsontypes
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
|
||||
"github.com/google/uuid"
|
||||
)
|
||||
|
||||
type UUID uuid.UUID
|
||||
type UUIDs []UUID
|
||||
|
||||
func (u *UUIDs) UUIDs() []uuid.UUID {
|
||||
r := make([]uuid.UUID, 0, len(*u))
|
||||
|
||||
for _, v := range *u {
|
||||
r = append(r, v.UUID())
|
||||
}
|
||||
|
||||
return r
|
||||
}
|
||||
|
||||
func (u *UUIDs) UnmarshalJSON(b []byte) error {
|
||||
var ss []string
|
||||
err := json.Unmarshal(b, &ss)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
usl := make([]UUID, 0, len(ss))
|
||||
for _, s := range ss {
|
||||
uu, err := uuid.Parse(s)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
usl = append(usl, UUID(uu))
|
||||
}
|
||||
|
||||
*u = usl
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (u UUID) UUID() uuid.UUID {
|
||||
return uuid.UUID(u)
|
||||
}
|
||||
|
||||
func (u *UUID) MarshalJSON() ([]byte, error) {
|
||||
return []byte(`"` + u.UUID().String() + `"`), nil
|
||||
}
|
||||
|
||||
func (u *UUID) UnmarshalJSON(b []byte) error {
|
||||
id, err := uuid.Parse(string(b[:]))
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
*u = UUID(id)
|
||||
return nil
|
||||
}
|
||||
|
||||
func (u *UUID) UnmarshalText(t []byte) error {
|
||||
var gu uuid.UUID
|
||||
err := gu.UnmarshalText(t)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
*u = UUID(gu)
|
||||
|
||||
return nil
|
||||
}
|
|
@ -42,26 +42,25 @@ type CallAudio struct {
|
|||
}
|
||||
|
||||
type Call struct {
|
||||
ID uuid.UUID `form:"-"`
|
||||
Audio []byte `form:"audio" filenameField:"AudioName"`
|
||||
AudioName string `form:"audioName"`
|
||||
AudioType string `form:"audioType"`
|
||||
Duration CallDuration `form:"-"`
|
||||
DateTime time.Time `form:"dateTime"`
|
||||
Frequencies []int `form:"frequencies"`
|
||||
Frequency int `form:"frequency"`
|
||||
Patches []int `form:"patches"`
|
||||
Source int `form:"source"`
|
||||
Sources []int `form:"sources"`
|
||||
System int `form:"system"`
|
||||
Submitter *auth.UserID `form:"-"`
|
||||
SystemLabel string `form:"systemLabel"`
|
||||
Talkgroup int `form:"talkgroup"`
|
||||
TalkgroupGroup *string `form:"talkgroupGroup"`
|
||||
TalkgroupLabel *string `form:"talkgroupLabel"`
|
||||
TGAlphaTag *string `form:"talkgroupTag"` // not 1:1
|
||||
ID uuid.UUID `json:"-"`
|
||||
Audio []byte `json:"audio,omitempty" filenameField:"AudioName"`
|
||||
AudioName string `json:"audioName,omitempty"`
|
||||
AudioType string `json:"audioType,omitempty"`
|
||||
Duration CallDuration `json:"-"`
|
||||
DateTime time.Time `json:"dateTime,omitempty"`
|
||||
Frequencies []int `json:"frequencies,omitempty"`
|
||||
Frequency int `json:"frequency,omitempty"`
|
||||
Patches []int `json:"patches,omitempty"`
|
||||
Source int `json:"source,omitempty"`
|
||||
System int `json:"system,omitempty"`
|
||||
Submitter *auth.UserID `json:"-,omitempty"`
|
||||
SystemLabel string `json:"systemLabel,omitempty"`
|
||||
Talkgroup int `json:"talkgroup,omitempty"`
|
||||
TalkgroupGroup *string `json:"talkgroupGroup,omitempty"`
|
||||
TalkgroupLabel *string `json:"talkgroupLabel,omitempty"`
|
||||
TGAlphaTag *string `json:"talkgroupTag,omitempty"`
|
||||
|
||||
shouldStore bool `form:"-"`
|
||||
shouldStore bool `json:"-"`
|
||||
}
|
||||
|
||||
func (c *Call) String() string {
|
||||
|
@ -114,7 +113,6 @@ func (c *Call) ToPB() *pb.Call {
|
|||
Frequency: int64(c.Frequency),
|
||||
Frequencies: toInt64Slice(c.Frequencies),
|
||||
Patches: toInt32Slice(c.Patches),
|
||||
Sources: toInt32Slice(c.Sources),
|
||||
Duration: c.Duration.MsInt32Ptr(),
|
||||
Audio: c.Audio,
|
||||
}
|
||||
|
|
|
@ -26,7 +26,7 @@ type Store interface {
|
|||
type store struct {
|
||||
}
|
||||
|
||||
func New() *store {
|
||||
func NewStore() *store {
|
||||
return new(store)
|
||||
}
|
||||
|
||||
|
@ -41,7 +41,7 @@ func CtxWithStore(ctx context.Context, s Store) context.Context {
|
|||
func FromCtx(ctx context.Context) Store {
|
||||
s, ok := ctx.Value(StoreCtxKey).(Store)
|
||||
if !ok {
|
||||
return New()
|
||||
return NewStore()
|
||||
}
|
||||
|
||||
return s
|
||||
|
@ -103,16 +103,21 @@ func (s *store) Calls(ctx context.Context, p CallsParams) (rows []database.ListC
|
|||
txErr := db.InTx(ctx, func(db database.Store) error {
|
||||
var err error
|
||||
count, err = db.ListCallsCount(ctx, database.ListCallsCountParams{
|
||||
Start: par.Start,
|
||||
End: par.End,
|
||||
TagsAny: par.TagsAny,
|
||||
TagsNot: par.TagsNot,
|
||||
TGFilter: par.TGFilter,
|
||||
Start: par.Start,
|
||||
End: par.End,
|
||||
TagsAny: par.TagsAny,
|
||||
TagsNot: par.TagsNot,
|
||||
TGFilter: par.TGFilter,
|
||||
LongerThan: par.LongerThan,
|
||||
})
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if offset > int32(count) {
|
||||
return common.ErrPageOutOfRange
|
||||
}
|
||||
|
||||
rows, err = db.ListCallsP(ctx, par)
|
||||
return err
|
||||
}, pgx.TxOptions{})
|
||||
|
|
|
@ -16,16 +16,17 @@ type Configuration struct {
|
|||
}
|
||||
|
||||
type Config struct {
|
||||
DB DB `yaml:"db"`
|
||||
CORS CORS `yaml:"cors"`
|
||||
Auth Auth `yaml:"auth"`
|
||||
Alerting Alerting `yaml:"alerting"`
|
||||
Log []Logger `yaml:"log"`
|
||||
Listen string `yaml:"listen"`
|
||||
Public bool `yaml:"public"`
|
||||
RateLimit RateLimit `yaml:"rateLimit"`
|
||||
Notify Notify `yaml:"notify"`
|
||||
Relay []Relay `yaml:"relay"`
|
||||
BaseURL jsontypes.URL `yaml:"baseURL"`
|
||||
DB DB `yaml:"db"`
|
||||
CORS CORS `yaml:"cors"`
|
||||
Auth Auth `yaml:"auth"`
|
||||
Alerting Alerting `yaml:"alerting"`
|
||||
Log []Logger `yaml:"log"`
|
||||
Listen string `yaml:"listen"`
|
||||
Public bool `yaml:"public"`
|
||||
RateLimit RateLimit `yaml:"rateLimit"`
|
||||
Notify Notify `yaml:"notify"`
|
||||
Relay []Relay `yaml:"relay"`
|
||||
}
|
||||
|
||||
type Auth struct {
|
||||
|
|
|
@ -62,7 +62,7 @@ func (c *Configuration) read() error {
|
|||
})
|
||||
|
||||
if err != nil {
|
||||
return fmt.Errorf("unmarshal err: %w", err)
|
||||
return fmt.Errorf("config: %w", err)
|
||||
}
|
||||
|
||||
return nil
|
||||
|
|
|
@ -157,7 +157,21 @@ func (q *Queries) CleanupSweptCalls(ctx context.Context, rangeStart pgtype.Times
|
|||
}
|
||||
|
||||
const getCallAudioByID = `-- name: GetCallAudioByID :one
|
||||
SELECT call_date, audio_name, audio_type, audio_blob FROM calls WHERE id = $1
|
||||
SELECT
|
||||
c.call_date,
|
||||
c.audio_name,
|
||||
c.audio_type,
|
||||
c.audio_blob
|
||||
FROM calls c
|
||||
WHERE c.id = $1
|
||||
UNION
|
||||
SELECT
|
||||
sc.call_date,
|
||||
sc.audio_name,
|
||||
sc.audio_type,
|
||||
sc.audio_blob
|
||||
FROM swept_calls sc
|
||||
WHERE sc.id = $1
|
||||
`
|
||||
|
||||
type GetCallAudioByIDRow struct {
|
||||
|
|
396
pkg/database/incidents.sql.go
Normal file
396
pkg/database/incidents.sql.go
Normal file
|
@ -0,0 +1,396 @@
|
|||
// Code generated by sqlc. DO NOT EDIT.
|
||||
// versions:
|
||||
// sqlc v1.27.0
|
||||
// source: incidents.sql
|
||||
|
||||
package database
|
||||
|
||||
import (
|
||||
"context"
|
||||
|
||||
"dynatron.me/x/stillbox/internal/jsontypes"
|
||||
"github.com/google/uuid"
|
||||
"github.com/jackc/pgx/v5/pgtype"
|
||||
)
|
||||
|
||||
const addToIncident = `-- name: AddToIncident :exec
|
||||
WITH inp AS (
|
||||
SELECT
|
||||
UNNEST($2::UUID[]) id,
|
||||
UNNEST($3::JSONB[]) notes
|
||||
) INSERT INTO incidents_calls(
|
||||
incident_id,
|
||||
call_id,
|
||||
calls_tbl_id,
|
||||
call_date,
|
||||
notes
|
||||
)
|
||||
SELECT
|
||||
$1::UUID,
|
||||
inp.id,
|
||||
inp.id,
|
||||
c.call_date,
|
||||
inp.notes
|
||||
FROM inp
|
||||
JOIN calls c ON c.id = inp.id
|
||||
`
|
||||
|
||||
func (q *Queries) AddToIncident(ctx context.Context, incidentID uuid.UUID, callIds []uuid.UUID, notes [][]byte) error {
|
||||
_, err := q.db.Exec(ctx, addToIncident, incidentID, callIds, notes)
|
||||
return err
|
||||
}
|
||||
|
||||
const createIncident = `-- name: CreateIncident :one
|
||||
INSERT INTO incidents (
|
||||
id,
|
||||
name,
|
||||
description,
|
||||
start_time,
|
||||
end_time,
|
||||
location,
|
||||
metadata
|
||||
) VALUES (
|
||||
$1,
|
||||
$2,
|
||||
$3,
|
||||
$4,
|
||||
$5,
|
||||
$6,
|
||||
$7
|
||||
)
|
||||
RETURNING id, name, description, start_time, end_time, location, metadata
|
||||
`
|
||||
|
||||
type CreateIncidentParams struct {
|
||||
ID uuid.UUID `json:"id"`
|
||||
Name string `json:"name"`
|
||||
Description *string `json:"description"`
|
||||
StartTime pgtype.Timestamptz `json:"start_time"`
|
||||
EndTime pgtype.Timestamptz `json:"end_time"`
|
||||
Location []byte `json:"location"`
|
||||
Metadata jsontypes.Metadata `json:"metadata"`
|
||||
}
|
||||
|
||||
func (q *Queries) CreateIncident(ctx context.Context, arg CreateIncidentParams) (Incident, error) {
|
||||
row := q.db.QueryRow(ctx, createIncident,
|
||||
arg.ID,
|
||||
arg.Name,
|
||||
arg.Description,
|
||||
arg.StartTime,
|
||||
arg.EndTime,
|
||||
arg.Location,
|
||||
arg.Metadata,
|
||||
)
|
||||
var i Incident
|
||||
err := row.Scan(
|
||||
&i.ID,
|
||||
&i.Name,
|
||||
&i.Description,
|
||||
&i.StartTime,
|
||||
&i.EndTime,
|
||||
&i.Location,
|
||||
&i.Metadata,
|
||||
)
|
||||
return i, err
|
||||
}
|
||||
|
||||
const deleteIncident = `-- name: DeleteIncident :exec
|
||||
DELETE FROM incidents CASCADE WHERE id = $1
|
||||
`
|
||||
|
||||
func (q *Queries) DeleteIncident(ctx context.Context, id uuid.UUID) error {
|
||||
_, err := q.db.Exec(ctx, deleteIncident, id)
|
||||
return err
|
||||
}
|
||||
|
||||
const getIncident = `-- name: GetIncident :one
|
||||
SELECT
|
||||
i.id,
|
||||
i.name,
|
||||
i.description,
|
||||
i.start_time,
|
||||
i.end_time,
|
||||
i.location,
|
||||
i.metadata
|
||||
FROM incidents i
|
||||
WHERE i.id = $1
|
||||
`
|
||||
|
||||
func (q *Queries) GetIncident(ctx context.Context, id uuid.UUID) (Incident, error) {
|
||||
row := q.db.QueryRow(ctx, getIncident, id)
|
||||
var i Incident
|
||||
err := row.Scan(
|
||||
&i.ID,
|
||||
&i.Name,
|
||||
&i.Description,
|
||||
&i.StartTime,
|
||||
&i.EndTime,
|
||||
&i.Location,
|
||||
&i.Metadata,
|
||||
)
|
||||
return i, err
|
||||
}
|
||||
|
||||
const getIncidentCalls = `-- name: GetIncidentCalls :many
|
||||
SELECT ic.call_id, ic.call_date, ic.notes, c.submitter, c.system, c.talkgroup, c.audio_name, c.duration, c.audio_type, c.audio_url, c.frequency, c.frequencies, c.patches, c.source, c.transcript
|
||||
FROM incidents_calls ic, LATERAL (
|
||||
SELECT
|
||||
ca.submitter,
|
||||
ca.system,
|
||||
ca.talkgroup,
|
||||
ca.audio_name,
|
||||
ca.duration,
|
||||
ca.audio_type,
|
||||
ca.audio_url,
|
||||
ca.frequency,
|
||||
ca.frequencies,
|
||||
ca.patches,
|
||||
ca.source,
|
||||
ca.transcript
|
||||
FROM calls ca WHERE ca.id = ic.calls_tbl_id AND ca.call_date = ic.call_date
|
||||
UNION
|
||||
SELECT
|
||||
sc.submitter,
|
||||
sc.system,
|
||||
sc.talkgroup,
|
||||
sc.audio_name,
|
||||
sc.duration,
|
||||
sc.audio_type,
|
||||
sc.audio_url,
|
||||
sc.frequency,
|
||||
sc.frequencies,
|
||||
sc.patches,
|
||||
sc.source,
|
||||
sc.transcript
|
||||
FROM swept_calls sc WHERE sc.id = ic.swept_call_id
|
||||
) c
|
||||
WHERE ic.incident_id = $1
|
||||
`
|
||||
|
||||
type GetIncidentCallsRow struct {
|
||||
CallID uuid.UUID `json:"call_id"`
|
||||
CallDate pgtype.Timestamptz `json:"call_date"`
|
||||
Notes []byte `json:"notes"`
|
||||
Submitter *int32 `json:"submitter"`
|
||||
System int `json:"system"`
|
||||
Talkgroup int `json:"talkgroup"`
|
||||
AudioName *string `json:"audio_name"`
|
||||
Duration *int32 `json:"duration"`
|
||||
AudioType *string `json:"audio_type"`
|
||||
AudioUrl *string `json:"audio_url"`
|
||||
Frequency int `json:"frequency"`
|
||||
Frequencies []int `json:"frequencies"`
|
||||
Patches []int `json:"patches"`
|
||||
Source int `json:"source"`
|
||||
Transcript *string `json:"transcript"`
|
||||
}
|
||||
|
||||
func (q *Queries) GetIncidentCalls(ctx context.Context, id uuid.UUID) ([]GetIncidentCallsRow, error) {
|
||||
rows, err := q.db.Query(ctx, getIncidentCalls, id)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
defer rows.Close()
|
||||
var items []GetIncidentCallsRow
|
||||
for rows.Next() {
|
||||
var i GetIncidentCallsRow
|
||||
if err := rows.Scan(
|
||||
&i.CallID,
|
||||
&i.CallDate,
|
||||
&i.Notes,
|
||||
&i.Submitter,
|
||||
&i.System,
|
||||
&i.Talkgroup,
|
||||
&i.AudioName,
|
||||
&i.Duration,
|
||||
&i.AudioType,
|
||||
&i.AudioUrl,
|
||||
&i.Frequency,
|
||||
&i.Frequencies,
|
||||
&i.Patches,
|
||||
&i.Source,
|
||||
&i.Transcript,
|
||||
); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
items = append(items, i)
|
||||
}
|
||||
if err := rows.Err(); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return items, nil
|
||||
}
|
||||
|
||||
const listIncidentsCount = `-- name: ListIncidentsCount :one
|
||||
SELECT COUNT(*)
|
||||
FROM incidents i
|
||||
WHERE
|
||||
CASE WHEN $1::TIMESTAMPTZ IS NOT NULL THEN
|
||||
i.start_time >= $1 ELSE TRUE END AND
|
||||
CASE WHEN $2::TIMESTAMPTZ IS NOT NULL THEN
|
||||
i.start_time <= $2 ELSE TRUE END AND
|
||||
(CASE WHEN $3::TEXT IS NOT NULL THEN (
|
||||
i.name ILIKE '%' || $3 || '%' OR
|
||||
i.description ILIKE '%' || $3 || '%'
|
||||
) ELSE TRUE END)
|
||||
`
|
||||
|
||||
func (q *Queries) ListIncidentsCount(ctx context.Context, start pgtype.Timestamptz, end pgtype.Timestamptz, filter *string) (int64, error) {
|
||||
row := q.db.QueryRow(ctx, listIncidentsCount, start, end, filter)
|
||||
var count int64
|
||||
err := row.Scan(&count)
|
||||
return count, err
|
||||
}
|
||||
|
||||
const listIncidentsP = `-- name: ListIncidentsP :many
|
||||
SELECT
|
||||
i.id,
|
||||
i.name,
|
||||
i.description,
|
||||
i.start_time,
|
||||
i.end_time,
|
||||
i.location,
|
||||
i.metadata,
|
||||
COUNT(ic.incident_id) calls_count
|
||||
FROM incidents i
|
||||
LEFT JOIN incidents_calls ic ON i.id = ic.incident_id
|
||||
WHERE
|
||||
CASE WHEN $1::TIMESTAMPTZ IS NOT NULL THEN
|
||||
i.start_time >= $1 ELSE TRUE END AND
|
||||
CASE WHEN $2::TIMESTAMPTZ IS NOT NULL THEN
|
||||
i.start_time <= $2 ELSE TRUE END AND
|
||||
(CASE WHEN $3::TEXT IS NOT NULL THEN (
|
||||
i.name ILIKE '%' || $3 || '%' OR
|
||||
i.description ILIKE '%' || $3 || '%'
|
||||
) ELSE TRUE END)
|
||||
GROUP BY i.id
|
||||
ORDER BY
|
||||
CASE WHEN $4::TEXT = 'asc' THEN i.start_time END ASC,
|
||||
CASE WHEN $4::TEXT = 'desc' THEN i.start_time END DESC
|
||||
OFFSET $5 ROWS
|
||||
FETCH NEXT $6 ROWS ONLY
|
||||
`
|
||||
|
||||
type ListIncidentsPParams struct {
|
||||
Start pgtype.Timestamptz `json:"start"`
|
||||
End pgtype.Timestamptz `json:"end"`
|
||||
Filter *string `json:"filter"`
|
||||
Direction string `json:"direction"`
|
||||
Offset int32 `json:"offset"`
|
||||
PerPage int32 `json:"per_page"`
|
||||
}
|
||||
|
||||
type ListIncidentsPRow struct {
|
||||
ID uuid.UUID `json:"id"`
|
||||
Name string `json:"name"`
|
||||
Description *string `json:"description"`
|
||||
StartTime pgtype.Timestamptz `json:"start_time"`
|
||||
EndTime pgtype.Timestamptz `json:"end_time"`
|
||||
Location []byte `json:"location"`
|
||||
Metadata jsontypes.Metadata `json:"metadata"`
|
||||
CallsCount int64 `json:"calls_count"`
|
||||
}
|
||||
|
||||
func (q *Queries) ListIncidentsP(ctx context.Context, arg ListIncidentsPParams) ([]ListIncidentsPRow, error) {
|
||||
rows, err := q.db.Query(ctx, listIncidentsP,
|
||||
arg.Start,
|
||||
arg.End,
|
||||
arg.Filter,
|
||||
arg.Direction,
|
||||
arg.Offset,
|
||||
arg.PerPage,
|
||||
)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
defer rows.Close()
|
||||
var items []ListIncidentsPRow
|
||||
for rows.Next() {
|
||||
var i ListIncidentsPRow
|
||||
if err := rows.Scan(
|
||||
&i.ID,
|
||||
&i.Name,
|
||||
&i.Description,
|
||||
&i.StartTime,
|
||||
&i.EndTime,
|
||||
&i.Location,
|
||||
&i.Metadata,
|
||||
&i.CallsCount,
|
||||
); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
items = append(items, i)
|
||||
}
|
||||
if err := rows.Err(); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return items, nil
|
||||
}
|
||||
|
||||
const removeFromIncident = `-- name: RemoveFromIncident :exec
|
||||
DELETE FROM incidents_calls ic
|
||||
WHERE ic.incident_id = $1 AND ic.call_id = ANY($2::UUID[])
|
||||
`
|
||||
|
||||
func (q *Queries) RemoveFromIncident(ctx context.Context, iD uuid.UUID, callIds []uuid.UUID) error {
|
||||
_, err := q.db.Exec(ctx, removeFromIncident, iD, callIds)
|
||||
return err
|
||||
}
|
||||
|
||||
const updateCallIncidentNotes = `-- name: UpdateCallIncidentNotes :exec
|
||||
UPDATE incidents_Calls
|
||||
SET notes = $1
|
||||
WHERE incident_id = $2 AND call_id = $3
|
||||
`
|
||||
|
||||
func (q *Queries) UpdateCallIncidentNotes(ctx context.Context, notes []byte, incidentID uuid.UUID, callID uuid.UUID) error {
|
||||
_, err := q.db.Exec(ctx, updateCallIncidentNotes, notes, incidentID, callID)
|
||||
return err
|
||||
}
|
||||
|
||||
const updateIncident = `-- name: UpdateIncident :one
|
||||
UPDATE incidents
|
||||
SET
|
||||
name = COALESCE($1, name),
|
||||
description = COALESCE($2, description),
|
||||
start_time = COALESCE($3, start_time),
|
||||
end_time = COALESCE($4, end_time),
|
||||
location = COALESCE($5, location),
|
||||
metadata = COALESCE($6, metadata)
|
||||
WHERE
|
||||
id = $7
|
||||
RETURNING id, name, description, start_time, end_time, location, metadata
|
||||
`
|
||||
|
||||
type UpdateIncidentParams struct {
|
||||
Name *string `json:"name"`
|
||||
Description *string `json:"description"`
|
||||
StartTime pgtype.Timestamptz `json:"start_time"`
|
||||
EndTime pgtype.Timestamptz `json:"end_time"`
|
||||
Location []byte `json:"location"`
|
||||
Metadata jsontypes.Metadata `json:"metadata"`
|
||||
ID uuid.UUID `json:"id"`
|
||||
}
|
||||
|
||||
func (q *Queries) UpdateIncident(ctx context.Context, arg UpdateIncidentParams) (Incident, error) {
|
||||
row := q.db.QueryRow(ctx, updateIncident,
|
||||
arg.Name,
|
||||
arg.Description,
|
||||
arg.StartTime,
|
||||
arg.EndTime,
|
||||
arg.Location,
|
||||
arg.Metadata,
|
||||
arg.ID,
|
||||
)
|
||||
var i Incident
|
||||
err := row.Scan(
|
||||
&i.ID,
|
||||
&i.Name,
|
||||
&i.Description,
|
||||
&i.StartTime,
|
||||
&i.EndTime,
|
||||
&i.Location,
|
||||
&i.Metadata,
|
||||
)
|
||||
return i, err
|
||||
}
|
|
@ -181,6 +181,55 @@ func (_c *Store_AddLearnedTalkgroup_Call) RunAndReturn(run func(context.Context,
|
|||
return _c
|
||||
}
|
||||
|
||||
// AddToIncident provides a mock function with given fields: ctx, incidentID, callIds, notes
|
||||
func (_m *Store) AddToIncident(ctx context.Context, incidentID uuid.UUID, callIds []uuid.UUID, notes [][]byte) error {
|
||||
ret := _m.Called(ctx, incidentID, callIds, notes)
|
||||
|
||||
if len(ret) == 0 {
|
||||
panic("no return value specified for AddToIncident")
|
||||
}
|
||||
|
||||
var r0 error
|
||||
if rf, ok := ret.Get(0).(func(context.Context, uuid.UUID, []uuid.UUID, [][]byte) error); ok {
|
||||
r0 = rf(ctx, incidentID, callIds, notes)
|
||||
} else {
|
||||
r0 = ret.Error(0)
|
||||
}
|
||||
|
||||
return r0
|
||||
}
|
||||
|
||||
// Store_AddToIncident_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'AddToIncident'
|
||||
type Store_AddToIncident_Call struct {
|
||||
*mock.Call
|
||||
}
|
||||
|
||||
// AddToIncident is a helper method to define mock.On call
|
||||
// - ctx context.Context
|
||||
// - incidentID uuid.UUID
|
||||
// - callIds []uuid.UUID
|
||||
// - notes [][]byte
|
||||
func (_e *Store_Expecter) AddToIncident(ctx interface{}, incidentID interface{}, callIds interface{}, notes interface{}) *Store_AddToIncident_Call {
|
||||
return &Store_AddToIncident_Call{Call: _e.mock.On("AddToIncident", ctx, incidentID, callIds, notes)}
|
||||
}
|
||||
|
||||
func (_c *Store_AddToIncident_Call) Run(run func(ctx context.Context, incidentID uuid.UUID, callIds []uuid.UUID, notes [][]byte)) *Store_AddToIncident_Call {
|
||||
_c.Call.Run(func(args mock.Arguments) {
|
||||
run(args[0].(context.Context), args[1].(uuid.UUID), args[2].([]uuid.UUID), args[3].([][]byte))
|
||||
})
|
||||
return _c
|
||||
}
|
||||
|
||||
func (_c *Store_AddToIncident_Call) Return(_a0 error) *Store_AddToIncident_Call {
|
||||
_c.Call.Return(_a0)
|
||||
return _c
|
||||
}
|
||||
|
||||
func (_c *Store_AddToIncident_Call) RunAndReturn(run func(context.Context, uuid.UUID, []uuid.UUID, [][]byte) error) *Store_AddToIncident_Call {
|
||||
_c.Call.Return(run)
|
||||
return _c
|
||||
}
|
||||
|
||||
// BulkSetTalkgroupTags provides a mock function with given fields: ctx, tgs, tags
|
||||
func (_m *Store) BulkSetTalkgroupTags(ctx context.Context, tgs database.TGTuples, tags []string) error {
|
||||
ret := _m.Called(ctx, tgs, tags)
|
||||
|
@ -346,6 +395,63 @@ func (_c *Store_CreateAPIKey_Call) RunAndReturn(run func(context.Context, int, p
|
|||
return _c
|
||||
}
|
||||
|
||||
// CreateIncident provides a mock function with given fields: ctx, arg
|
||||
func (_m *Store) CreateIncident(ctx context.Context, arg database.CreateIncidentParams) (database.Incident, error) {
|
||||
ret := _m.Called(ctx, arg)
|
||||
|
||||
if len(ret) == 0 {
|
||||
panic("no return value specified for CreateIncident")
|
||||
}
|
||||
|
||||
var r0 database.Incident
|
||||
var r1 error
|
||||
if rf, ok := ret.Get(0).(func(context.Context, database.CreateIncidentParams) (database.Incident, error)); ok {
|
||||
return rf(ctx, arg)
|
||||
}
|
||||
if rf, ok := ret.Get(0).(func(context.Context, database.CreateIncidentParams) database.Incident); ok {
|
||||
r0 = rf(ctx, arg)
|
||||
} else {
|
||||
r0 = ret.Get(0).(database.Incident)
|
||||
}
|
||||
|
||||
if rf, ok := ret.Get(1).(func(context.Context, database.CreateIncidentParams) error); ok {
|
||||
r1 = rf(ctx, arg)
|
||||
} else {
|
||||
r1 = ret.Error(1)
|
||||
}
|
||||
|
||||
return r0, r1
|
||||
}
|
||||
|
||||
// Store_CreateIncident_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'CreateIncident'
|
||||
type Store_CreateIncident_Call struct {
|
||||
*mock.Call
|
||||
}
|
||||
|
||||
// CreateIncident is a helper method to define mock.On call
|
||||
// - ctx context.Context
|
||||
// - arg database.CreateIncidentParams
|
||||
func (_e *Store_Expecter) CreateIncident(ctx interface{}, arg interface{}) *Store_CreateIncident_Call {
|
||||
return &Store_CreateIncident_Call{Call: _e.mock.On("CreateIncident", ctx, arg)}
|
||||
}
|
||||
|
||||
func (_c *Store_CreateIncident_Call) Run(run func(ctx context.Context, arg database.CreateIncidentParams)) *Store_CreateIncident_Call {
|
||||
_c.Call.Run(func(args mock.Arguments) {
|
||||
run(args[0].(context.Context), args[1].(database.CreateIncidentParams))
|
||||
})
|
||||
return _c
|
||||
}
|
||||
|
||||
func (_c *Store_CreateIncident_Call) Return(_a0 database.Incident, _a1 error) *Store_CreateIncident_Call {
|
||||
_c.Call.Return(_a0, _a1)
|
||||
return _c
|
||||
}
|
||||
|
||||
func (_c *Store_CreateIncident_Call) RunAndReturn(run func(context.Context, database.CreateIncidentParams) (database.Incident, error)) *Store_CreateIncident_Call {
|
||||
_c.Call.Return(run)
|
||||
return _c
|
||||
}
|
||||
|
||||
// CreatePartition provides a mock function with given fields: ctx, parentTable, partitionName, start, end
|
||||
func (_m *Store) CreatePartition(ctx context.Context, parentTable string, partitionName string, start time.Time, end time.Time) error {
|
||||
ret := _m.Called(ctx, parentTable, partitionName, start, end)
|
||||
|
@ -642,6 +748,53 @@ func (_c *Store_DeleteAPIKey_Call) RunAndReturn(run func(context.Context, string
|
|||
return _c
|
||||
}
|
||||
|
||||
// DeleteIncident provides a mock function with given fields: ctx, id
|
||||
func (_m *Store) DeleteIncident(ctx context.Context, id uuid.UUID) error {
|
||||
ret := _m.Called(ctx, id)
|
||||
|
||||
if len(ret) == 0 {
|
||||
panic("no return value specified for DeleteIncident")
|
||||
}
|
||||
|
||||
var r0 error
|
||||
if rf, ok := ret.Get(0).(func(context.Context, uuid.UUID) error); ok {
|
||||
r0 = rf(ctx, id)
|
||||
} else {
|
||||
r0 = ret.Error(0)
|
||||
}
|
||||
|
||||
return r0
|
||||
}
|
||||
|
||||
// Store_DeleteIncident_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'DeleteIncident'
|
||||
type Store_DeleteIncident_Call struct {
|
||||
*mock.Call
|
||||
}
|
||||
|
||||
// DeleteIncident is a helper method to define mock.On call
|
||||
// - ctx context.Context
|
||||
// - id uuid.UUID
|
||||
func (_e *Store_Expecter) DeleteIncident(ctx interface{}, id interface{}) *Store_DeleteIncident_Call {
|
||||
return &Store_DeleteIncident_Call{Call: _e.mock.On("DeleteIncident", ctx, id)}
|
||||
}
|
||||
|
||||
func (_c *Store_DeleteIncident_Call) Run(run func(ctx context.Context, id uuid.UUID)) *Store_DeleteIncident_Call {
|
||||
_c.Call.Run(func(args mock.Arguments) {
|
||||
run(args[0].(context.Context), args[1].(uuid.UUID))
|
||||
})
|
||||
return _c
|
||||
}
|
||||
|
||||
func (_c *Store_DeleteIncident_Call) Return(_a0 error) *Store_DeleteIncident_Call {
|
||||
_c.Call.Return(_a0)
|
||||
return _c
|
||||
}
|
||||
|
||||
func (_c *Store_DeleteIncident_Call) RunAndReturn(run func(context.Context, uuid.UUID) error) *Store_DeleteIncident_Call {
|
||||
_c.Call.Return(run)
|
||||
return _c
|
||||
}
|
||||
|
||||
// DeleteSystem provides a mock function with given fields: ctx, id
|
||||
func (_m *Store) DeleteSystem(ctx context.Context, id int) error {
|
||||
ret := _m.Called(ctx, id)
|
||||
|
@ -1167,6 +1320,122 @@ func (_c *Store_GetDatabaseSize_Call) RunAndReturn(run func(context.Context) (st
|
|||
return _c
|
||||
}
|
||||
|
||||
// GetIncident provides a mock function with given fields: ctx, id
|
||||
func (_m *Store) GetIncident(ctx context.Context, id uuid.UUID) (database.Incident, error) {
|
||||
ret := _m.Called(ctx, id)
|
||||
|
||||
if len(ret) == 0 {
|
||||
panic("no return value specified for GetIncident")
|
||||
}
|
||||
|
||||
var r0 database.Incident
|
||||
var r1 error
|
||||
if rf, ok := ret.Get(0).(func(context.Context, uuid.UUID) (database.Incident, error)); ok {
|
||||
return rf(ctx, id)
|
||||
}
|
||||
if rf, ok := ret.Get(0).(func(context.Context, uuid.UUID) database.Incident); ok {
|
||||
r0 = rf(ctx, id)
|
||||
} else {
|
||||
r0 = ret.Get(0).(database.Incident)
|
||||
}
|
||||
|
||||
if rf, ok := ret.Get(1).(func(context.Context, uuid.UUID) error); ok {
|
||||
r1 = rf(ctx, id)
|
||||
} else {
|
||||
r1 = ret.Error(1)
|
||||
}
|
||||
|
||||
return r0, r1
|
||||
}
|
||||
|
||||
// Store_GetIncident_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'GetIncident'
|
||||
type Store_GetIncident_Call struct {
|
||||
*mock.Call
|
||||
}
|
||||
|
||||
// GetIncident is a helper method to define mock.On call
|
||||
// - ctx context.Context
|
||||
// - id uuid.UUID
|
||||
func (_e *Store_Expecter) GetIncident(ctx interface{}, id interface{}) *Store_GetIncident_Call {
|
||||
return &Store_GetIncident_Call{Call: _e.mock.On("GetIncident", ctx, id)}
|
||||
}
|
||||
|
||||
func (_c *Store_GetIncident_Call) Run(run func(ctx context.Context, id uuid.UUID)) *Store_GetIncident_Call {
|
||||
_c.Call.Run(func(args mock.Arguments) {
|
||||
run(args[0].(context.Context), args[1].(uuid.UUID))
|
||||
})
|
||||
return _c
|
||||
}
|
||||
|
||||
func (_c *Store_GetIncident_Call) Return(_a0 database.Incident, _a1 error) *Store_GetIncident_Call {
|
||||
_c.Call.Return(_a0, _a1)
|
||||
return _c
|
||||
}
|
||||
|
||||
func (_c *Store_GetIncident_Call) RunAndReturn(run func(context.Context, uuid.UUID) (database.Incident, error)) *Store_GetIncident_Call {
|
||||
_c.Call.Return(run)
|
||||
return _c
|
||||
}
|
||||
|
||||
// GetIncidentCalls provides a mock function with given fields: ctx, id
|
||||
func (_m *Store) GetIncidentCalls(ctx context.Context, id uuid.UUID) ([]database.GetIncidentCallsRow, error) {
|
||||
ret := _m.Called(ctx, id)
|
||||
|
||||
if len(ret) == 0 {
|
||||
panic("no return value specified for GetIncidentCalls")
|
||||
}
|
||||
|
||||
var r0 []database.GetIncidentCallsRow
|
||||
var r1 error
|
||||
if rf, ok := ret.Get(0).(func(context.Context, uuid.UUID) ([]database.GetIncidentCallsRow, error)); ok {
|
||||
return rf(ctx, id)
|
||||
}
|
||||
if rf, ok := ret.Get(0).(func(context.Context, uuid.UUID) []database.GetIncidentCallsRow); ok {
|
||||
r0 = rf(ctx, id)
|
||||
} else {
|
||||
if ret.Get(0) != nil {
|
||||
r0 = ret.Get(0).([]database.GetIncidentCallsRow)
|
||||
}
|
||||
}
|
||||
|
||||
if rf, ok := ret.Get(1).(func(context.Context, uuid.UUID) error); ok {
|
||||
r1 = rf(ctx, id)
|
||||
} else {
|
||||
r1 = ret.Error(1)
|
||||
}
|
||||
|
||||
return r0, r1
|
||||
}
|
||||
|
||||
// Store_GetIncidentCalls_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'GetIncidentCalls'
|
||||
type Store_GetIncidentCalls_Call struct {
|
||||
*mock.Call
|
||||
}
|
||||
|
||||
// GetIncidentCalls is a helper method to define mock.On call
|
||||
// - ctx context.Context
|
||||
// - id uuid.UUID
|
||||
func (_e *Store_Expecter) GetIncidentCalls(ctx interface{}, id interface{}) *Store_GetIncidentCalls_Call {
|
||||
return &Store_GetIncidentCalls_Call{Call: _e.mock.On("GetIncidentCalls", ctx, id)}
|
||||
}
|
||||
|
||||
func (_c *Store_GetIncidentCalls_Call) Run(run func(ctx context.Context, id uuid.UUID)) *Store_GetIncidentCalls_Call {
|
||||
_c.Call.Run(func(args mock.Arguments) {
|
||||
run(args[0].(context.Context), args[1].(uuid.UUID))
|
||||
})
|
||||
return _c
|
||||
}
|
||||
|
||||
func (_c *Store_GetIncidentCalls_Call) Return(_a0 []database.GetIncidentCallsRow, _a1 error) *Store_GetIncidentCalls_Call {
|
||||
_c.Call.Return(_a0, _a1)
|
||||
return _c
|
||||
}
|
||||
|
||||
func (_c *Store_GetIncidentCalls_Call) RunAndReturn(run func(context.Context, uuid.UUID) ([]database.GetIncidentCallsRow, error)) *Store_GetIncidentCalls_Call {
|
||||
_c.Call.Return(run)
|
||||
return _c
|
||||
}
|
||||
|
||||
// GetSystemName provides a mock function with given fields: ctx, systemID
|
||||
func (_m *Store) GetSystemName(ctx context.Context, systemID int) (string, error) {
|
||||
ret := _m.Called(ctx, systemID)
|
||||
|
@ -2500,6 +2769,172 @@ func (_c *Store_ListCallsP_Call) RunAndReturn(run func(context.Context, database
|
|||
return _c
|
||||
}
|
||||
|
||||
// ListIncidentsCount provides a mock function with given fields: ctx, start, end, filter
|
||||
func (_m *Store) ListIncidentsCount(ctx context.Context, start pgtype.Timestamptz, end pgtype.Timestamptz, filter *string) (int64, error) {
|
||||
ret := _m.Called(ctx, start, end, filter)
|
||||
|
||||
if len(ret) == 0 {
|
||||
panic("no return value specified for ListIncidentsCount")
|
||||
}
|
||||
|
||||
var r0 int64
|
||||
var r1 error
|
||||
if rf, ok := ret.Get(0).(func(context.Context, pgtype.Timestamptz, pgtype.Timestamptz, *string) (int64, error)); ok {
|
||||
return rf(ctx, start, end, filter)
|
||||
}
|
||||
if rf, ok := ret.Get(0).(func(context.Context, pgtype.Timestamptz, pgtype.Timestamptz, *string) int64); ok {
|
||||
r0 = rf(ctx, start, end, filter)
|
||||
} else {
|
||||
r0 = ret.Get(0).(int64)
|
||||
}
|
||||
|
||||
if rf, ok := ret.Get(1).(func(context.Context, pgtype.Timestamptz, pgtype.Timestamptz, *string) error); ok {
|
||||
r1 = rf(ctx, start, end, filter)
|
||||
} else {
|
||||
r1 = ret.Error(1)
|
||||
}
|
||||
|
||||
return r0, r1
|
||||
}
|
||||
|
||||
// Store_ListIncidentsCount_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'ListIncidentsCount'
|
||||
type Store_ListIncidentsCount_Call struct {
|
||||
*mock.Call
|
||||
}
|
||||
|
||||
// ListIncidentsCount is a helper method to define mock.On call
|
||||
// - ctx context.Context
|
||||
// - start pgtype.Timestamptz
|
||||
// - end pgtype.Timestamptz
|
||||
// - filter *string
|
||||
func (_e *Store_Expecter) ListIncidentsCount(ctx interface{}, start interface{}, end interface{}, filter interface{}) *Store_ListIncidentsCount_Call {
|
||||
return &Store_ListIncidentsCount_Call{Call: _e.mock.On("ListIncidentsCount", ctx, start, end, filter)}
|
||||
}
|
||||
|
||||
func (_c *Store_ListIncidentsCount_Call) Run(run func(ctx context.Context, start pgtype.Timestamptz, end pgtype.Timestamptz, filter *string)) *Store_ListIncidentsCount_Call {
|
||||
_c.Call.Run(func(args mock.Arguments) {
|
||||
run(args[0].(context.Context), args[1].(pgtype.Timestamptz), args[2].(pgtype.Timestamptz), args[3].(*string))
|
||||
})
|
||||
return _c
|
||||
}
|
||||
|
||||
func (_c *Store_ListIncidentsCount_Call) Return(_a0 int64, _a1 error) *Store_ListIncidentsCount_Call {
|
||||
_c.Call.Return(_a0, _a1)
|
||||
return _c
|
||||
}
|
||||
|
||||
func (_c *Store_ListIncidentsCount_Call) RunAndReturn(run func(context.Context, pgtype.Timestamptz, pgtype.Timestamptz, *string) (int64, error)) *Store_ListIncidentsCount_Call {
|
||||
_c.Call.Return(run)
|
||||
return _c
|
||||
}
|
||||
|
||||
// ListIncidentsP provides a mock function with given fields: ctx, arg
|
||||
func (_m *Store) ListIncidentsP(ctx context.Context, arg database.ListIncidentsPParams) ([]database.ListIncidentsPRow, error) {
|
||||
ret := _m.Called(ctx, arg)
|
||||
|
||||
if len(ret) == 0 {
|
||||
panic("no return value specified for ListIncidentsP")
|
||||
}
|
||||
|
||||
var r0 []database.ListIncidentsPRow
|
||||
var r1 error
|
||||
if rf, ok := ret.Get(0).(func(context.Context, database.ListIncidentsPParams) ([]database.ListIncidentsPRow, error)); ok {
|
||||
return rf(ctx, arg)
|
||||
}
|
||||
if rf, ok := ret.Get(0).(func(context.Context, database.ListIncidentsPParams) []database.ListIncidentsPRow); ok {
|
||||
r0 = rf(ctx, arg)
|
||||
} else {
|
||||
if ret.Get(0) != nil {
|
||||
r0 = ret.Get(0).([]database.ListIncidentsPRow)
|
||||
}
|
||||
}
|
||||
|
||||
if rf, ok := ret.Get(1).(func(context.Context, database.ListIncidentsPParams) error); ok {
|
||||
r1 = rf(ctx, arg)
|
||||
} else {
|
||||
r1 = ret.Error(1)
|
||||
}
|
||||
|
||||
return r0, r1
|
||||
}
|
||||
|
||||
// Store_ListIncidentsP_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'ListIncidentsP'
|
||||
type Store_ListIncidentsP_Call struct {
|
||||
*mock.Call
|
||||
}
|
||||
|
||||
// ListIncidentsP is a helper method to define mock.On call
|
||||
// - ctx context.Context
|
||||
// - arg database.ListIncidentsPParams
|
||||
func (_e *Store_Expecter) ListIncidentsP(ctx interface{}, arg interface{}) *Store_ListIncidentsP_Call {
|
||||
return &Store_ListIncidentsP_Call{Call: _e.mock.On("ListIncidentsP", ctx, arg)}
|
||||
}
|
||||
|
||||
func (_c *Store_ListIncidentsP_Call) Run(run func(ctx context.Context, arg database.ListIncidentsPParams)) *Store_ListIncidentsP_Call {
|
||||
_c.Call.Run(func(args mock.Arguments) {
|
||||
run(args[0].(context.Context), args[1].(database.ListIncidentsPParams))
|
||||
})
|
||||
return _c
|
||||
}
|
||||
|
||||
func (_c *Store_ListIncidentsP_Call) Return(_a0 []database.ListIncidentsPRow, _a1 error) *Store_ListIncidentsP_Call {
|
||||
_c.Call.Return(_a0, _a1)
|
||||
return _c
|
||||
}
|
||||
|
||||
func (_c *Store_ListIncidentsP_Call) RunAndReturn(run func(context.Context, database.ListIncidentsPParams) ([]database.ListIncidentsPRow, error)) *Store_ListIncidentsP_Call {
|
||||
_c.Call.Return(run)
|
||||
return _c
|
||||
}
|
||||
|
||||
// RemoveFromIncident provides a mock function with given fields: ctx, iD, callIds
|
||||
func (_m *Store) RemoveFromIncident(ctx context.Context, iD uuid.UUID, callIds []uuid.UUID) error {
|
||||
ret := _m.Called(ctx, iD, callIds)
|
||||
|
||||
if len(ret) == 0 {
|
||||
panic("no return value specified for RemoveFromIncident")
|
||||
}
|
||||
|
||||
var r0 error
|
||||
if rf, ok := ret.Get(0).(func(context.Context, uuid.UUID, []uuid.UUID) error); ok {
|
||||
r0 = rf(ctx, iD, callIds)
|
||||
} else {
|
||||
r0 = ret.Error(0)
|
||||
}
|
||||
|
||||
return r0
|
||||
}
|
||||
|
||||
// Store_RemoveFromIncident_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'RemoveFromIncident'
|
||||
type Store_RemoveFromIncident_Call struct {
|
||||
*mock.Call
|
||||
}
|
||||
|
||||
// RemoveFromIncident is a helper method to define mock.On call
|
||||
// - ctx context.Context
|
||||
// - iD uuid.UUID
|
||||
// - callIds []uuid.UUID
|
||||
func (_e *Store_Expecter) RemoveFromIncident(ctx interface{}, iD interface{}, callIds interface{}) *Store_RemoveFromIncident_Call {
|
||||
return &Store_RemoveFromIncident_Call{Call: _e.mock.On("RemoveFromIncident", ctx, iD, callIds)}
|
||||
}
|
||||
|
||||
func (_c *Store_RemoveFromIncident_Call) Run(run func(ctx context.Context, iD uuid.UUID, callIds []uuid.UUID)) *Store_RemoveFromIncident_Call {
|
||||
_c.Call.Run(func(args mock.Arguments) {
|
||||
run(args[0].(context.Context), args[1].(uuid.UUID), args[2].([]uuid.UUID))
|
||||
})
|
||||
return _c
|
||||
}
|
||||
|
||||
func (_c *Store_RemoveFromIncident_Call) Return(_a0 error) *Store_RemoveFromIncident_Call {
|
||||
_c.Call.Return(_a0)
|
||||
return _c
|
||||
}
|
||||
|
||||
func (_c *Store_RemoveFromIncident_Call) RunAndReturn(run func(context.Context, uuid.UUID, []uuid.UUID) error) *Store_RemoveFromIncident_Call {
|
||||
_c.Call.Return(run)
|
||||
return _c
|
||||
}
|
||||
|
||||
// RestoreTalkgroupVersion provides a mock function with given fields: ctx, versionIds
|
||||
func (_m *Store) RestoreTalkgroupVersion(ctx context.Context, versionIds int) (database.Talkgroup, error) {
|
||||
ret := _m.Called(ctx, versionIds)
|
||||
|
@ -2859,6 +3294,112 @@ func (_c *Store_SweepCalls_Call) RunAndReturn(run func(context.Context, pgtype.T
|
|||
return _c
|
||||
}
|
||||
|
||||
// UpdateCallIncidentNotes provides a mock function with given fields: ctx, notes, incidentID, callID
|
||||
func (_m *Store) UpdateCallIncidentNotes(ctx context.Context, notes []byte, incidentID uuid.UUID, callID uuid.UUID) error {
|
||||
ret := _m.Called(ctx, notes, incidentID, callID)
|
||||
|
||||
if len(ret) == 0 {
|
||||
panic("no return value specified for UpdateCallIncidentNotes")
|
||||
}
|
||||
|
||||
var r0 error
|
||||
if rf, ok := ret.Get(0).(func(context.Context, []byte, uuid.UUID, uuid.UUID) error); ok {
|
||||
r0 = rf(ctx, notes, incidentID, callID)
|
||||
} else {
|
||||
r0 = ret.Error(0)
|
||||
}
|
||||
|
||||
return r0
|
||||
}
|
||||
|
||||
// Store_UpdateCallIncidentNotes_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'UpdateCallIncidentNotes'
|
||||
type Store_UpdateCallIncidentNotes_Call struct {
|
||||
*mock.Call
|
||||
}
|
||||
|
||||
// UpdateCallIncidentNotes is a helper method to define mock.On call
|
||||
// - ctx context.Context
|
||||
// - notes []byte
|
||||
// - incidentID uuid.UUID
|
||||
// - callID uuid.UUID
|
||||
func (_e *Store_Expecter) UpdateCallIncidentNotes(ctx interface{}, notes interface{}, incidentID interface{}, callID interface{}) *Store_UpdateCallIncidentNotes_Call {
|
||||
return &Store_UpdateCallIncidentNotes_Call{Call: _e.mock.On("UpdateCallIncidentNotes", ctx, notes, incidentID, callID)}
|
||||
}
|
||||
|
||||
func (_c *Store_UpdateCallIncidentNotes_Call) Run(run func(ctx context.Context, notes []byte, incidentID uuid.UUID, callID uuid.UUID)) *Store_UpdateCallIncidentNotes_Call {
|
||||
_c.Call.Run(func(args mock.Arguments) {
|
||||
run(args[0].(context.Context), args[1].([]byte), args[2].(uuid.UUID), args[3].(uuid.UUID))
|
||||
})
|
||||
return _c
|
||||
}
|
||||
|
||||
func (_c *Store_UpdateCallIncidentNotes_Call) Return(_a0 error) *Store_UpdateCallIncidentNotes_Call {
|
||||
_c.Call.Return(_a0)
|
||||
return _c
|
||||
}
|
||||
|
||||
func (_c *Store_UpdateCallIncidentNotes_Call) RunAndReturn(run func(context.Context, []byte, uuid.UUID, uuid.UUID) error) *Store_UpdateCallIncidentNotes_Call {
|
||||
_c.Call.Return(run)
|
||||
return _c
|
||||
}
|
||||
|
||||
// UpdateIncident provides a mock function with given fields: ctx, arg
|
||||
func (_m *Store) UpdateIncident(ctx context.Context, arg database.UpdateIncidentParams) (database.Incident, error) {
|
||||
ret := _m.Called(ctx, arg)
|
||||
|
||||
if len(ret) == 0 {
|
||||
panic("no return value specified for UpdateIncident")
|
||||
}
|
||||
|
||||
var r0 database.Incident
|
||||
var r1 error
|
||||
if rf, ok := ret.Get(0).(func(context.Context, database.UpdateIncidentParams) (database.Incident, error)); ok {
|
||||
return rf(ctx, arg)
|
||||
}
|
||||
if rf, ok := ret.Get(0).(func(context.Context, database.UpdateIncidentParams) database.Incident); ok {
|
||||
r0 = rf(ctx, arg)
|
||||
} else {
|
||||
r0 = ret.Get(0).(database.Incident)
|
||||
}
|
||||
|
||||
if rf, ok := ret.Get(1).(func(context.Context, database.UpdateIncidentParams) error); ok {
|
||||
r1 = rf(ctx, arg)
|
||||
} else {
|
||||
r1 = ret.Error(1)
|
||||
}
|
||||
|
||||
return r0, r1
|
||||
}
|
||||
|
||||
// Store_UpdateIncident_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'UpdateIncident'
|
||||
type Store_UpdateIncident_Call struct {
|
||||
*mock.Call
|
||||
}
|
||||
|
||||
// UpdateIncident is a helper method to define mock.On call
|
||||
// - ctx context.Context
|
||||
// - arg database.UpdateIncidentParams
|
||||
func (_e *Store_Expecter) UpdateIncident(ctx interface{}, arg interface{}) *Store_UpdateIncident_Call {
|
||||
return &Store_UpdateIncident_Call{Call: _e.mock.On("UpdateIncident", ctx, arg)}
|
||||
}
|
||||
|
||||
func (_c *Store_UpdateIncident_Call) Run(run func(ctx context.Context, arg database.UpdateIncidentParams)) *Store_UpdateIncident_Call {
|
||||
_c.Call.Run(func(args mock.Arguments) {
|
||||
run(args[0].(context.Context), args[1].(database.UpdateIncidentParams))
|
||||
})
|
||||
return _c
|
||||
}
|
||||
|
||||
func (_c *Store_UpdateIncident_Call) Return(_a0 database.Incident, _a1 error) *Store_UpdateIncident_Call {
|
||||
_c.Call.Return(_a0, _a1)
|
||||
return _c
|
||||
}
|
||||
|
||||
func (_c *Store_UpdateIncident_Call) RunAndReturn(run func(context.Context, database.UpdateIncidentParams) (database.Incident, error)) *Store_UpdateIncident_Call {
|
||||
_c.Call.Return(run)
|
||||
return _c
|
||||
}
|
||||
|
||||
// UpdatePassword provides a mock function with given fields: ctx, username, password
|
||||
func (_m *Store) UpdatePassword(ctx context.Context, username string, password string) error {
|
||||
ret := _m.Called(ctx, username, password)
|
||||
|
|
|
@ -56,13 +56,13 @@ type Call struct {
|
|||
}
|
||||
|
||||
type Incident struct {
|
||||
ID uuid.UUID `json:"id,omitempty"`
|
||||
Name string `json:"name,omitempty"`
|
||||
Description *string `json:"description,omitempty"`
|
||||
StartTime pgtype.Timestamp `json:"start_time,omitempty"`
|
||||
EndTime pgtype.Timestamp `json:"end_time,omitempty"`
|
||||
Location []byte `json:"location,omitempty"`
|
||||
Metadata []byte `json:"metadata,omitempty"`
|
||||
ID uuid.UUID `json:"id,omitempty"`
|
||||
Name string `json:"name,omitempty"`
|
||||
Description *string `json:"description,omitempty"`
|
||||
StartTime pgtype.Timestamptz `json:"start_time,omitempty"`
|
||||
EndTime pgtype.Timestamptz `json:"end_time,omitempty"`
|
||||
Location []byte `json:"location,omitempty"`
|
||||
Metadata jsontypes.Metadata `json:"metadata,omitempty"`
|
||||
}
|
||||
|
||||
type IncidentsCall struct {
|
||||
|
|
|
@ -15,12 +15,15 @@ type Querier interface {
|
|||
AddAlert(ctx context.Context, arg AddAlertParams) error
|
||||
AddCall(ctx context.Context, arg AddCallParams) error
|
||||
AddLearnedTalkgroup(ctx context.Context, arg AddLearnedTalkgroupParams) (Talkgroup, error)
|
||||
AddToIncident(ctx context.Context, incidentID uuid.UUID, callIds []uuid.UUID, notes [][]byte) error
|
||||
// This is used to sweep calls that are part of an incident prior to pruning a partition.
|
||||
CleanupSweptCalls(ctx context.Context, rangeStart pgtype.Timestamptz, rangeEnd pgtype.Timestamptz) (int64, error)
|
||||
CreateAPIKey(ctx context.Context, owner int, expires pgtype.Timestamp, disabled *bool) (ApiKey, error)
|
||||
CreateIncident(ctx context.Context, arg CreateIncidentParams) (Incident, error)
|
||||
CreateSystem(ctx context.Context, iD int, name string) error
|
||||
CreateUser(ctx context.Context, arg CreateUserParams) (User, error)
|
||||
DeleteAPIKey(ctx context.Context, apiKey string) error
|
||||
DeleteIncident(ctx context.Context, id uuid.UUID) error
|
||||
DeleteSystem(ctx context.Context, id int) error
|
||||
DeleteTalkgroup(ctx context.Context, systemID int32, tGID int32) error
|
||||
DeleteUser(ctx context.Context, username string) error
|
||||
|
@ -29,6 +32,8 @@ type Querier interface {
|
|||
GetAppPrefs(ctx context.Context, appName string, uid int) ([]byte, error)
|
||||
GetCallAudioByID(ctx context.Context, id uuid.UUID) (GetCallAudioByIDRow, error)
|
||||
GetDatabaseSize(ctx context.Context) (string, error)
|
||||
GetIncident(ctx context.Context, id uuid.UUID) (Incident, error)
|
||||
GetIncidentCalls(ctx context.Context, id uuid.UUID) ([]GetIncidentCallsRow, error)
|
||||
GetSystemName(ctx context.Context, systemID int) (string, error)
|
||||
GetTalkgroup(ctx context.Context, systemID int32, tGID int32) (GetTalkgroupRow, error)
|
||||
GetTalkgroupIDsByTags(ctx context.Context, anyTags []string, allTags []string, notTags []string) ([]GetTalkgroupIDsByTagsRow, error)
|
||||
|
@ -48,6 +53,9 @@ type Querier interface {
|
|||
GetUsers(ctx context.Context) ([]User, error)
|
||||
ListCallsCount(ctx context.Context, arg ListCallsCountParams) (int64, error)
|
||||
ListCallsP(ctx context.Context, arg ListCallsPParams) ([]ListCallsPRow, error)
|
||||
ListIncidentsCount(ctx context.Context, start pgtype.Timestamptz, end pgtype.Timestamptz, filter *string) (int64, error)
|
||||
ListIncidentsP(ctx context.Context, arg ListIncidentsPParams) ([]ListIncidentsPRow, error)
|
||||
RemoveFromIncident(ctx context.Context, iD uuid.UUID, callIds []uuid.UUID) error
|
||||
RestoreTalkgroupVersion(ctx context.Context, versionIds int) (Talkgroup, error)
|
||||
SetAppPrefs(ctx context.Context, appName string, prefs []byte, uid int) error
|
||||
SetCallTranscript(ctx context.Context, iD uuid.UUID, transcript *string) error
|
||||
|
@ -55,6 +63,8 @@ type Querier interface {
|
|||
StoreDeletedTGVersion(ctx context.Context, systemID *int32, tGID *int32, submitter *int32) error
|
||||
StoreTGVersion(ctx context.Context, arg []StoreTGVersionParams) *StoreTGVersionBatchResults
|
||||
SweepCalls(ctx context.Context, rangeStart pgtype.Timestamptz, rangeEnd pgtype.Timestamptz) (int64, error)
|
||||
UpdateCallIncidentNotes(ctx context.Context, notes []byte, incidentID uuid.UUID, callID uuid.UUID) error
|
||||
UpdateIncident(ctx context.Context, arg UpdateIncidentParams) (Incident, error)
|
||||
UpdatePassword(ctx context.Context, username string, password string) error
|
||||
UpdateTalkgroup(ctx context.Context, arg UpdateTalkgroupParams) (Talkgroup, error)
|
||||
UpsertTalkgroup(ctx context.Context, arg []UpsertTalkgroupParams) *UpsertTalkgroupBatchResults
|
||||
|
|
25
pkg/incidents/incident.go
Normal file
25
pkg/incidents/incident.go
Normal file
|
@ -0,0 +1,25 @@
|
|||
package incidents
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
|
||||
"dynatron.me/x/stillbox/internal/jsontypes"
|
||||
"dynatron.me/x/stillbox/pkg/calls"
|
||||
"github.com/google/uuid"
|
||||
)
|
||||
|
||||
type Incident struct {
|
||||
ID uuid.UUID `json:"id"`
|
||||
Name string `json:"name"`
|
||||
Description *string `json:"description"`
|
||||
StartTime *jsontypes.Time `json:"startTime"`
|
||||
EndTime *jsontypes.Time `json:"endTime"`
|
||||
Location jsontypes.Location `json:"location"`
|
||||
Metadata jsontypes.Metadata `json:"metadata"`
|
||||
Calls []IncidentCall `json:"calls"`
|
||||
}
|
||||
|
||||
type IncidentCall struct {
|
||||
calls.Call
|
||||
Notes json.RawMessage `json:"notes"`
|
||||
}
|
319
pkg/incidents/incstore/store.go
Normal file
319
pkg/incidents/incstore/store.go
Normal file
|
@ -0,0 +1,319 @@
|
|||
package incstore
|
||||
|
||||
import (
|
||||
"context"
|
||||
"time"
|
||||
|
||||
"dynatron.me/x/stillbox/internal/common"
|
||||
"dynatron.me/x/stillbox/internal/jsontypes"
|
||||
"dynatron.me/x/stillbox/pkg/auth"
|
||||
"dynatron.me/x/stillbox/pkg/calls"
|
||||
"dynatron.me/x/stillbox/pkg/database"
|
||||
"dynatron.me/x/stillbox/pkg/incidents"
|
||||
"github.com/google/uuid"
|
||||
"github.com/jackc/pgx/v5"
|
||||
)
|
||||
|
||||
type IncidentsParams struct {
|
||||
common.Pagination
|
||||
Direction *common.SortDirection `json:"dir"`
|
||||
Filter *string `json:"filter"`
|
||||
|
||||
Start *jsontypes.Time `json:"start"`
|
||||
End *jsontypes.Time `json:"end"`
|
||||
}
|
||||
|
||||
type Store interface {
|
||||
// CreateIncident creates an incident.
|
||||
CreateIncident(ctx context.Context, inc incidents.Incident) (*incidents.Incident, error)
|
||||
|
||||
// AddToIncident adds the specified call IDs to an incident.
|
||||
// If not nil, notes must be valid json.
|
||||
AddRemoveIncidentCalls(ctx context.Context, incidentID uuid.UUID, addCallIDs []uuid.UUID, notes []byte, removeCallIDs []uuid.UUID) error
|
||||
|
||||
// UpdateNotes updates the notes for a call-incident mapping.
|
||||
UpdateNotes(ctx context.Context, incidentID uuid.UUID, callID uuid.UUID, notes []byte) error
|
||||
|
||||
// Incidents gets incidents matching parameters and pagination.
|
||||
Incidents(ctx context.Context, p IncidentsParams) (incs []Incident, totalCount int, err error)
|
||||
|
||||
// Incident gets a single incident.
|
||||
Incident(ctx context.Context, id uuid.UUID) (*incidents.Incident, error)
|
||||
|
||||
// UpdateIncident updates an incident.
|
||||
UpdateIncident(ctx context.Context, id uuid.UUID, p UpdateIncidentParams) (*incidents.Incident, error)
|
||||
|
||||
// DeleteIncident deletes an incident.
|
||||
DeleteIncident(ctx context.Context, id uuid.UUID) error
|
||||
}
|
||||
|
||||
type store struct {
|
||||
}
|
||||
|
||||
type storeCtxKey string
|
||||
|
||||
const StoreCtxKey storeCtxKey = "store"
|
||||
|
||||
func CtxWithStore(ctx context.Context, s Store) context.Context {
|
||||
return context.WithValue(ctx, StoreCtxKey, s)
|
||||
}
|
||||
|
||||
func FromCtx(ctx context.Context) Store {
|
||||
s, ok := ctx.Value(StoreCtxKey).(Store)
|
||||
if !ok {
|
||||
return NewStore()
|
||||
}
|
||||
|
||||
return s
|
||||
}
|
||||
|
||||
func NewStore() Store {
|
||||
return &store{}
|
||||
}
|
||||
|
||||
func (s *store) CreateIncident(ctx context.Context, inc incidents.Incident) (*incidents.Incident, error) {
|
||||
db := database.FromCtx(ctx)
|
||||
var dbInc database.Incident
|
||||
|
||||
id := uuid.New()
|
||||
|
||||
txErr := db.InTx(ctx, func(db database.Store) error {
|
||||
var err error
|
||||
dbInc, err = db.CreateIncident(ctx, database.CreateIncidentParams{
|
||||
ID: id,
|
||||
Name: inc.Name,
|
||||
Description: inc.Description,
|
||||
StartTime: inc.StartTime.PGTypeTSTZ(),
|
||||
EndTime: inc.EndTime.PGTypeTSTZ(),
|
||||
Location: inc.Location.RawMessage,
|
||||
Metadata: inc.Metadata,
|
||||
})
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if len(inc.Calls) > 0 {
|
||||
callIDs := make([]uuid.UUID, 0, len(inc.Calls))
|
||||
notes := make([][]byte, 0, len(inc.Calls))
|
||||
hasNote := false
|
||||
for _, c := range inc.Calls {
|
||||
callIDs = append(callIDs, c.ID)
|
||||
if c.Notes != nil {
|
||||
hasNote = true
|
||||
}
|
||||
notes = append(notes, c.Notes)
|
||||
}
|
||||
if !hasNote {
|
||||
notes = nil
|
||||
}
|
||||
|
||||
err = db.AddToIncident(ctx, dbInc.ID, callIDs, notes)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
}, pgx.TxOptions{})
|
||||
if txErr != nil {
|
||||
return nil, txErr
|
||||
}
|
||||
|
||||
inc = fromDBIncident(id, dbInc)
|
||||
|
||||
return &inc, nil
|
||||
}
|
||||
|
||||
func (s *store) AddRemoveIncidentCalls(ctx context.Context, incidentID uuid.UUID, addCallIDs []uuid.UUID, notes []byte, removeCallIDs []uuid.UUID) error {
|
||||
return database.FromCtx(ctx).InTx(ctx, func(db database.Store) error {
|
||||
if len(addCallIDs) > 0 {
|
||||
var noteAr [][]byte
|
||||
if notes != nil {
|
||||
noteAr = make([][]byte, len(addCallIDs))
|
||||
for i := range addCallIDs {
|
||||
noteAr[i] = notes
|
||||
}
|
||||
}
|
||||
|
||||
err := db.AddToIncident(ctx, incidentID, addCallIDs, noteAr)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
if len(removeCallIDs) > 0 {
|
||||
err := db.RemoveFromIncident(ctx, incidentID, removeCallIDs)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
}, pgx.TxOptions{})
|
||||
}
|
||||
|
||||
func (s *store) Incidents(ctx context.Context, p IncidentsParams) (incs []Incident, totalCount int, err error) {
|
||||
db := database.FromCtx(ctx)
|
||||
|
||||
offset, perPage := p.Pagination.OffsetPerPage(100)
|
||||
dbParam := database.ListIncidentsPParams{
|
||||
Start: p.Start.PGTypeTSTZ(),
|
||||
End: p.End.PGTypeTSTZ(),
|
||||
Filter: p.Filter,
|
||||
Direction: p.Direction.DirString(common.DirAsc),
|
||||
Offset: offset,
|
||||
PerPage: perPage,
|
||||
}
|
||||
|
||||
var count int64
|
||||
var rows []database.ListIncidentsPRow
|
||||
txErr := db.InTx(ctx, func(db database.Store) error {
|
||||
var err error
|
||||
count, err = db.ListIncidentsCount(ctx, dbParam.Start, dbParam.End, dbParam.Filter)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if offset > int32(count) {
|
||||
return common.ErrPageOutOfRange
|
||||
}
|
||||
|
||||
rows, err = db.ListIncidentsP(ctx, dbParam)
|
||||
return err
|
||||
}, pgx.TxOptions{})
|
||||
if txErr != nil {
|
||||
return nil, 0, txErr
|
||||
}
|
||||
|
||||
incs = make([]Incident, 0, len(rows))
|
||||
for _, v := range rows {
|
||||
incs = append(incs, fromDBListInPRow(v.ID, v))
|
||||
}
|
||||
|
||||
return incs, int(count), err
|
||||
}
|
||||
|
||||
func fromDBIncident(id uuid.UUID, d database.Incident) incidents.Incident {
|
||||
return incidents.Incident{
|
||||
ID: id,
|
||||
Name: d.Name,
|
||||
Description: d.Description,
|
||||
StartTime: jsontypes.TimePtrFromTSTZ(d.StartTime),
|
||||
EndTime: jsontypes.TimePtrFromTSTZ(d.EndTime),
|
||||
Metadata: d.Metadata,
|
||||
}
|
||||
}
|
||||
|
||||
type Incident struct {
|
||||
incidents.Incident
|
||||
|
||||
CallCount int `json:"callCount"`
|
||||
}
|
||||
|
||||
func fromDBListInPRow(id uuid.UUID, d database.ListIncidentsPRow) Incident {
|
||||
return Incident{
|
||||
Incident: incidents.Incident{
|
||||
ID: id,
|
||||
Name: d.Name,
|
||||
Description: d.Description,
|
||||
StartTime: jsontypes.TimePtrFromTSTZ(d.StartTime),
|
||||
EndTime: jsontypes.TimePtrFromTSTZ(d.EndTime),
|
||||
Metadata: d.Metadata,
|
||||
},
|
||||
CallCount: int(d.CallsCount),
|
||||
}
|
||||
}
|
||||
|
||||
func fromDBCalls(d []database.GetIncidentCallsRow) []incidents.IncidentCall {
|
||||
r := make([]incidents.IncidentCall, 0, len(d))
|
||||
for _, v := range d {
|
||||
dur := calls.CallDuration(time.Duration(common.ZeroIfNil(v.Duration)) * time.Millisecond)
|
||||
sub := common.PtrTo(auth.UserID(common.ZeroIfNil(v.Submitter)))
|
||||
r = append(r, incidents.IncidentCall{
|
||||
Call: calls.Call{
|
||||
ID: v.CallID,
|
||||
AudioName: common.ZeroIfNil(v.AudioName),
|
||||
AudioType: common.ZeroIfNil(v.AudioType),
|
||||
Duration: dur,
|
||||
DateTime: v.CallDate.Time,
|
||||
Frequencies: v.Frequencies,
|
||||
Frequency: v.Frequency,
|
||||
Patches: v.Patches,
|
||||
Source: v.Source,
|
||||
System: v.System,
|
||||
Submitter: sub,
|
||||
Talkgroup: v.Talkgroup,
|
||||
},
|
||||
Notes: v.Notes,
|
||||
})
|
||||
}
|
||||
|
||||
return r
|
||||
}
|
||||
|
||||
func (s *store) Incident(ctx context.Context, id uuid.UUID) (*incidents.Incident, error) {
|
||||
var r incidents.Incident
|
||||
txErr := database.FromCtx(ctx).InTx(ctx, func(db database.Store) error {
|
||||
inc, err := db.GetIncident(ctx, id)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
calls, err := db.GetIncidentCalls(ctx, id)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
r = fromDBIncident(id, inc)
|
||||
r.Calls = fromDBCalls(calls)
|
||||
|
||||
return nil
|
||||
}, pgx.TxOptions{})
|
||||
if txErr != nil {
|
||||
return nil, txErr
|
||||
}
|
||||
|
||||
return &r, nil
|
||||
}
|
||||
|
||||
type UpdateIncidentParams struct {
|
||||
Name *string `json:"name"`
|
||||
Description *string `json:"description"`
|
||||
StartTime *jsontypes.Time `json:"startTime"`
|
||||
EndTime *jsontypes.Time `json:"endTime"`
|
||||
Location []byte `json:"location"`
|
||||
Metadata jsontypes.Metadata `json:"metadata"`
|
||||
}
|
||||
|
||||
func (uip UpdateIncidentParams) toDBUIP(id uuid.UUID) database.UpdateIncidentParams {
|
||||
return database.UpdateIncidentParams{
|
||||
ID: id,
|
||||
Name: uip.Name,
|
||||
Description: uip.Description,
|
||||
StartTime: uip.StartTime.PGTypeTSTZ(),
|
||||
EndTime: uip.EndTime.PGTypeTSTZ(),
|
||||
Location: uip.Location,
|
||||
Metadata: uip.Metadata,
|
||||
}
|
||||
}
|
||||
|
||||
func (s *store) UpdateIncident(ctx context.Context, id uuid.UUID, p UpdateIncidentParams) (*incidents.Incident, error) {
|
||||
db := database.FromCtx(ctx)
|
||||
|
||||
dbInc, err := db.UpdateIncident(ctx, p.toDBUIP(id))
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
inc := fromDBIncident(id, dbInc)
|
||||
|
||||
return &inc, nil
|
||||
}
|
||||
|
||||
func (s *store) DeleteIncident(ctx context.Context, id uuid.UUID) error {
|
||||
return database.FromCtx(ctx).DeleteIncident(ctx, id)
|
||||
}
|
||||
|
||||
func (s *store) UpdateNotes(ctx context.Context, incidentID uuid.UUID, callID uuid.UUID, notes []byte) error {
|
||||
return database.FromCtx(ctx).UpdateCallIncidentNotes(ctx, notes, incidentID, callID)
|
||||
}
|
|
@ -298,9 +298,8 @@ type Call struct {
|
|||
Frequency int64 `protobuf:"varint,8,opt,name=frequency,proto3" json:"frequency,omitempty"`
|
||||
Frequencies []int64 `protobuf:"varint,9,rep,packed,name=frequencies,proto3" json:"frequencies,omitempty"`
|
||||
Patches []int32 `protobuf:"varint,10,rep,packed,name=patches,proto3" json:"patches,omitempty"`
|
||||
Sources []int32 `protobuf:"varint,11,rep,packed,name=sources,proto3" json:"sources,omitempty"`
|
||||
Duration *int32 `protobuf:"varint,12,opt,name=duration,proto3,oneof" json:"duration,omitempty"`
|
||||
Audio []byte `protobuf:"bytes,13,opt,name=audio,proto3" json:"audio,omitempty"`
|
||||
Duration *int32 `protobuf:"varint,11,opt,name=duration,proto3,oneof" json:"duration,omitempty"`
|
||||
Audio []byte `protobuf:"bytes,12,opt,name=audio,proto3" json:"audio,omitempty"`
|
||||
}
|
||||
|
||||
func (x *Call) Reset() {
|
||||
|
@ -405,13 +404,6 @@ func (x *Call) GetPatches() []int32 {
|
|||
return nil
|
||||
}
|
||||
|
||||
func (x *Call) GetSources() []int32 {
|
||||
if x != nil {
|
||||
return x.Sources
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (x *Call) GetDuration() int32 {
|
||||
if x != nil && x.Duration != nil {
|
||||
return *x.Duration
|
||||
|
@ -1195,7 +1187,7 @@ var file_stillbox_proto_rawDesc = []byte{
|
|||
0x6f, 0x75, 0x70, 0x49, 0x6e, 0x66, 0x6f, 0x48, 0x00, 0x52, 0x06, 0x74, 0x67, 0x49, 0x6e, 0x66,
|
||||
0x6f, 0x42, 0x12, 0x0a, 0x10, 0x63, 0x6f, 0x6d, 0x6d, 0x61, 0x6e, 0x64, 0x5f, 0x72, 0x65, 0x73,
|
||||
0x70, 0x6f, 0x6e, 0x73, 0x65, 0x42, 0x0d, 0x0a, 0x0b, 0x5f, 0x63, 0x6f, 0x6d, 0x6d, 0x61, 0x6e,
|
||||
0x64, 0x5f, 0x69, 0x64, 0x22, 0x91, 0x03, 0x0a, 0x04, 0x43, 0x61, 0x6c, 0x6c, 0x12, 0x0e, 0x0a,
|
||||
0x64, 0x5f, 0x69, 0x64, 0x22, 0xf7, 0x02, 0x0a, 0x04, 0x43, 0x61, 0x6c, 0x6c, 0x12, 0x0e, 0x0a,
|
||||
0x02, 0x69, 0x64, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, 0x02, 0x69, 0x64, 0x12, 0x1c, 0x0a,
|
||||
0x09, 0x61, 0x75, 0x64, 0x69, 0x6f, 0x4e, 0x61, 0x6d, 0x65, 0x18, 0x02, 0x20, 0x01, 0x28, 0x09,
|
||||
0x52, 0x09, 0x61, 0x75, 0x64, 0x69, 0x6f, 0x4e, 0x61, 0x6d, 0x65, 0x12, 0x1c, 0x0a, 0x09, 0x61,
|
||||
|
@ -1214,112 +1206,111 @@ var file_stillbox_proto_rawDesc = []byte{
|
|||
0x0a, 0x0b, 0x66, 0x72, 0x65, 0x71, 0x75, 0x65, 0x6e, 0x63, 0x69, 0x65, 0x73, 0x18, 0x09, 0x20,
|
||||
0x03, 0x28, 0x03, 0x52, 0x0b, 0x66, 0x72, 0x65, 0x71, 0x75, 0x65, 0x6e, 0x63, 0x69, 0x65, 0x73,
|
||||
0x12, 0x18, 0x0a, 0x07, 0x70, 0x61, 0x74, 0x63, 0x68, 0x65, 0x73, 0x18, 0x0a, 0x20, 0x03, 0x28,
|
||||
0x05, 0x52, 0x07, 0x70, 0x61, 0x74, 0x63, 0x68, 0x65, 0x73, 0x12, 0x18, 0x0a, 0x07, 0x73, 0x6f,
|
||||
0x75, 0x72, 0x63, 0x65, 0x73, 0x18, 0x0b, 0x20, 0x03, 0x28, 0x05, 0x52, 0x07, 0x73, 0x6f, 0x75,
|
||||
0x72, 0x63, 0x65, 0x73, 0x12, 0x1f, 0x0a, 0x08, 0x64, 0x75, 0x72, 0x61, 0x74, 0x69, 0x6f, 0x6e,
|
||||
0x18, 0x0c, 0x20, 0x01, 0x28, 0x05, 0x48, 0x00, 0x52, 0x08, 0x64, 0x75, 0x72, 0x61, 0x74, 0x69,
|
||||
0x6f, 0x6e, 0x88, 0x01, 0x01, 0x12, 0x14, 0x0a, 0x05, 0x61, 0x75, 0x64, 0x69, 0x6f, 0x18, 0x0d,
|
||||
0x20, 0x01, 0x28, 0x0c, 0x52, 0x05, 0x61, 0x75, 0x64, 0x69, 0x6f, 0x42, 0x0b, 0x0a, 0x09, 0x5f,
|
||||
0x64, 0x75, 0x72, 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x22, 0x3e, 0x0a, 0x05, 0x48, 0x65, 0x6c, 0x6c,
|
||||
0x6f, 0x12, 0x35, 0x0a, 0x0b, 0x73, 0x65, 0x72, 0x76, 0x65, 0x72, 0x5f, 0x69, 0x6e, 0x66, 0x6f,
|
||||
0x18, 0x01, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x14, 0x2e, 0x73, 0x74, 0x69, 0x6c, 0x6c, 0x62, 0x6f,
|
||||
0x78, 0x2e, 0x53, 0x65, 0x72, 0x76, 0x65, 0x72, 0x49, 0x6e, 0x66, 0x6f, 0x52, 0x0a, 0x73, 0x65,
|
||||
0x72, 0x76, 0x65, 0x72, 0x49, 0x6e, 0x66, 0x6f, 0x22, 0x1d, 0x0a, 0x09, 0x55, 0x73, 0x65, 0x72,
|
||||
0x50, 0x6f, 0x70, 0x75, 0x70, 0x12, 0x10, 0x0a, 0x03, 0x6d, 0x73, 0x67, 0x18, 0x01, 0x20, 0x01,
|
||||
0x28, 0x09, 0x52, 0x03, 0x6d, 0x73, 0x67, 0x22, 0x4a, 0x0a, 0x05, 0x45, 0x72, 0x72, 0x6f, 0x72,
|
||||
0x12, 0x14, 0x0a, 0x05, 0x65, 0x72, 0x72, 0x6f, 0x72, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52,
|
||||
0x05, 0x65, 0x72, 0x72, 0x6f, 0x72, 0x12, 0x2b, 0x0a, 0x07, 0x63, 0x6f, 0x6d, 0x6d, 0x61, 0x6e,
|
||||
0x64, 0x18, 0x02, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x11, 0x2e, 0x73, 0x74, 0x69, 0x6c, 0x6c, 0x62,
|
||||
0x6f, 0x78, 0x2e, 0x43, 0x6f, 0x6d, 0x6d, 0x61, 0x6e, 0x64, 0x52, 0x07, 0x63, 0x6f, 0x6d, 0x6d,
|
||||
0x61, 0x6e, 0x64, 0x22, 0x78, 0x0a, 0x0c, 0x4e, 0x6f, 0x74, 0x69, 0x66, 0x69, 0x63, 0x61, 0x74,
|
||||
0x69, 0x6f, 0x6e, 0x12, 0x37, 0x0a, 0x09, 0x64, 0x61, 0x74, 0x65, 0x5f, 0x74, 0x69, 0x6d, 0x65,
|
||||
0x18, 0x01, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x1a, 0x2e, 0x67, 0x6f, 0x6f, 0x67, 0x6c, 0x65, 0x2e,
|
||||
0x70, 0x72, 0x6f, 0x74, 0x6f, 0x62, 0x75, 0x66, 0x2e, 0x54, 0x69, 0x6d, 0x65, 0x73, 0x74, 0x61,
|
||||
0x6d, 0x70, 0x52, 0x08, 0x64, 0x61, 0x74, 0x65, 0x54, 0x69, 0x6d, 0x65, 0x12, 0x10, 0x0a, 0x03,
|
||||
0x6d, 0x73, 0x67, 0x18, 0x02, 0x20, 0x01, 0x28, 0x09, 0x52, 0x03, 0x6d, 0x73, 0x67, 0x12, 0x1d,
|
||||
0x0a, 0x0a, 0x61, 0x63, 0x74, 0x69, 0x6f, 0x6e, 0x5f, 0x75, 0x72, 0x6c, 0x18, 0x03, 0x20, 0x01,
|
||||
0x28, 0x09, 0x52, 0x09, 0x61, 0x63, 0x74, 0x69, 0x6f, 0x6e, 0x55, 0x72, 0x6c, 0x22, 0xed, 0x01,
|
||||
0x0a, 0x07, 0x43, 0x6f, 0x6d, 0x6d, 0x61, 0x6e, 0x64, 0x12, 0x22, 0x0a, 0x0a, 0x63, 0x6f, 0x6d,
|
||||
0x6d, 0x61, 0x6e, 0x64, 0x5f, 0x69, 0x64, 0x18, 0x01, 0x20, 0x01, 0x28, 0x03, 0x48, 0x01, 0x52,
|
||||
0x09, 0x63, 0x6f, 0x6d, 0x6d, 0x61, 0x6e, 0x64, 0x49, 0x64, 0x88, 0x01, 0x01, 0x12, 0x33, 0x0a,
|
||||
0x0c, 0x6c, 0x69, 0x76, 0x65, 0x5f, 0x63, 0x6f, 0x6d, 0x6d, 0x61, 0x6e, 0x64, 0x18, 0x02, 0x20,
|
||||
0x01, 0x28, 0x0b, 0x32, 0x0e, 0x2e, 0x73, 0x74, 0x69, 0x6c, 0x6c, 0x62, 0x6f, 0x78, 0x2e, 0x4c,
|
||||
0x69, 0x76, 0x65, 0x48, 0x00, 0x52, 0x0b, 0x6c, 0x69, 0x76, 0x65, 0x43, 0x6f, 0x6d, 0x6d, 0x61,
|
||||
0x6e, 0x64, 0x12, 0x39, 0x0a, 0x0e, 0x73, 0x65, 0x61, 0x72, 0x63, 0x68, 0x5f, 0x63, 0x6f, 0x6d,
|
||||
0x6d, 0x61, 0x6e, 0x64, 0x18, 0x03, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x10, 0x2e, 0x73, 0x74, 0x69,
|
||||
0x6c, 0x6c, 0x62, 0x6f, 0x78, 0x2e, 0x53, 0x65, 0x61, 0x72, 0x63, 0x68, 0x48, 0x00, 0x52, 0x0d,
|
||||
0x73, 0x65, 0x61, 0x72, 0x63, 0x68, 0x43, 0x6f, 0x6d, 0x6d, 0x61, 0x6e, 0x64, 0x12, 0x34, 0x0a,
|
||||
0x0a, 0x74, 0x67, 0x5f, 0x63, 0x6f, 0x6d, 0x6d, 0x61, 0x6e, 0x64, 0x18, 0x04, 0x20, 0x01, 0x28,
|
||||
0x05, 0x52, 0x07, 0x70, 0x61, 0x74, 0x63, 0x68, 0x65, 0x73, 0x12, 0x1f, 0x0a, 0x08, 0x64, 0x75,
|
||||
0x72, 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x18, 0x0b, 0x20, 0x01, 0x28, 0x05, 0x48, 0x00, 0x52, 0x08,
|
||||
0x64, 0x75, 0x72, 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x88, 0x01, 0x01, 0x12, 0x14, 0x0a, 0x05, 0x61,
|
||||
0x75, 0x64, 0x69, 0x6f, 0x18, 0x0c, 0x20, 0x01, 0x28, 0x0c, 0x52, 0x05, 0x61, 0x75, 0x64, 0x69,
|
||||
0x6f, 0x42, 0x0b, 0x0a, 0x09, 0x5f, 0x64, 0x75, 0x72, 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x22, 0x3e,
|
||||
0x0a, 0x05, 0x48, 0x65, 0x6c, 0x6c, 0x6f, 0x12, 0x35, 0x0a, 0x0b, 0x73, 0x65, 0x72, 0x76, 0x65,
|
||||
0x72, 0x5f, 0x69, 0x6e, 0x66, 0x6f, 0x18, 0x01, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x14, 0x2e, 0x73,
|
||||
0x74, 0x69, 0x6c, 0x6c, 0x62, 0x6f, 0x78, 0x2e, 0x53, 0x65, 0x72, 0x76, 0x65, 0x72, 0x49, 0x6e,
|
||||
0x66, 0x6f, 0x52, 0x0a, 0x73, 0x65, 0x72, 0x76, 0x65, 0x72, 0x49, 0x6e, 0x66, 0x6f, 0x22, 0x1d,
|
||||
0x0a, 0x09, 0x55, 0x73, 0x65, 0x72, 0x50, 0x6f, 0x70, 0x75, 0x70, 0x12, 0x10, 0x0a, 0x03, 0x6d,
|
||||
0x73, 0x67, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, 0x03, 0x6d, 0x73, 0x67, 0x22, 0x4a, 0x0a,
|
||||
0x05, 0x45, 0x72, 0x72, 0x6f, 0x72, 0x12, 0x14, 0x0a, 0x05, 0x65, 0x72, 0x72, 0x6f, 0x72, 0x18,
|
||||
0x01, 0x20, 0x01, 0x28, 0x09, 0x52, 0x05, 0x65, 0x72, 0x72, 0x6f, 0x72, 0x12, 0x2b, 0x0a, 0x07,
|
||||
0x63, 0x6f, 0x6d, 0x6d, 0x61, 0x6e, 0x64, 0x18, 0x02, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x11, 0x2e,
|
||||
0x73, 0x74, 0x69, 0x6c, 0x6c, 0x62, 0x6f, 0x78, 0x2e, 0x43, 0x6f, 0x6d, 0x6d, 0x61, 0x6e, 0x64,
|
||||
0x52, 0x07, 0x63, 0x6f, 0x6d, 0x6d, 0x61, 0x6e, 0x64, 0x22, 0x78, 0x0a, 0x0c, 0x4e, 0x6f, 0x74,
|
||||
0x69, 0x66, 0x69, 0x63, 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x12, 0x37, 0x0a, 0x09, 0x64, 0x61, 0x74,
|
||||
0x65, 0x5f, 0x74, 0x69, 0x6d, 0x65, 0x18, 0x01, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x1a, 0x2e, 0x67,
|
||||
0x6f, 0x6f, 0x67, 0x6c, 0x65, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x62, 0x75, 0x66, 0x2e, 0x54,
|
||||
0x69, 0x6d, 0x65, 0x73, 0x74, 0x61, 0x6d, 0x70, 0x52, 0x08, 0x64, 0x61, 0x74, 0x65, 0x54, 0x69,
|
||||
0x6d, 0x65, 0x12, 0x10, 0x0a, 0x03, 0x6d, 0x73, 0x67, 0x18, 0x02, 0x20, 0x01, 0x28, 0x09, 0x52,
|
||||
0x03, 0x6d, 0x73, 0x67, 0x12, 0x1d, 0x0a, 0x0a, 0x61, 0x63, 0x74, 0x69, 0x6f, 0x6e, 0x5f, 0x75,
|
||||
0x72, 0x6c, 0x18, 0x03, 0x20, 0x01, 0x28, 0x09, 0x52, 0x09, 0x61, 0x63, 0x74, 0x69, 0x6f, 0x6e,
|
||||
0x55, 0x72, 0x6c, 0x22, 0xed, 0x01, 0x0a, 0x07, 0x43, 0x6f, 0x6d, 0x6d, 0x61, 0x6e, 0x64, 0x12,
|
||||
0x22, 0x0a, 0x0a, 0x63, 0x6f, 0x6d, 0x6d, 0x61, 0x6e, 0x64, 0x5f, 0x69, 0x64, 0x18, 0x01, 0x20,
|
||||
0x01, 0x28, 0x03, 0x48, 0x01, 0x52, 0x09, 0x63, 0x6f, 0x6d, 0x6d, 0x61, 0x6e, 0x64, 0x49, 0x64,
|
||||
0x88, 0x01, 0x01, 0x12, 0x33, 0x0a, 0x0c, 0x6c, 0x69, 0x76, 0x65, 0x5f, 0x63, 0x6f, 0x6d, 0x6d,
|
||||
0x61, 0x6e, 0x64, 0x18, 0x02, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x0e, 0x2e, 0x73, 0x74, 0x69, 0x6c,
|
||||
0x6c, 0x62, 0x6f, 0x78, 0x2e, 0x4c, 0x69, 0x76, 0x65, 0x48, 0x00, 0x52, 0x0b, 0x6c, 0x69, 0x76,
|
||||
0x65, 0x43, 0x6f, 0x6d, 0x6d, 0x61, 0x6e, 0x64, 0x12, 0x39, 0x0a, 0x0e, 0x73, 0x65, 0x61, 0x72,
|
||||
0x63, 0x68, 0x5f, 0x63, 0x6f, 0x6d, 0x6d, 0x61, 0x6e, 0x64, 0x18, 0x03, 0x20, 0x01, 0x28, 0x0b,
|
||||
0x32, 0x10, 0x2e, 0x73, 0x74, 0x69, 0x6c, 0x6c, 0x62, 0x6f, 0x78, 0x2e, 0x53, 0x65, 0x61, 0x72,
|
||||
0x63, 0x68, 0x48, 0x00, 0x52, 0x0d, 0x73, 0x65, 0x61, 0x72, 0x63, 0x68, 0x43, 0x6f, 0x6d, 0x6d,
|
||||
0x61, 0x6e, 0x64, 0x12, 0x34, 0x0a, 0x0a, 0x74, 0x67, 0x5f, 0x63, 0x6f, 0x6d, 0x6d, 0x61, 0x6e,
|
||||
0x64, 0x18, 0x04, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x13, 0x2e, 0x73, 0x74, 0x69, 0x6c, 0x6c, 0x62,
|
||||
0x6f, 0x78, 0x2e, 0x54, 0x61, 0x6c, 0x6b, 0x67, 0x72, 0x6f, 0x75, 0x70, 0x48, 0x00, 0x52, 0x09,
|
||||
0x74, 0x67, 0x43, 0x6f, 0x6d, 0x6d, 0x61, 0x6e, 0x64, 0x42, 0x09, 0x0a, 0x07, 0x63, 0x6f, 0x6d,
|
||||
0x6d, 0x61, 0x6e, 0x64, 0x42, 0x0d, 0x0a, 0x0b, 0x5f, 0x63, 0x6f, 0x6d, 0x6d, 0x61, 0x6e, 0x64,
|
||||
0x5f, 0x69, 0x64, 0x22, 0xf2, 0x02, 0x0a, 0x0d, 0x54, 0x61, 0x6c, 0x6b, 0x67, 0x72, 0x6f, 0x75,
|
||||
0x70, 0x49, 0x6e, 0x66, 0x6f, 0x12, 0x23, 0x0a, 0x02, 0x74, 0x67, 0x18, 0x01, 0x20, 0x01, 0x28,
|
||||
0x0b, 0x32, 0x13, 0x2e, 0x73, 0x74, 0x69, 0x6c, 0x6c, 0x62, 0x6f, 0x78, 0x2e, 0x54, 0x61, 0x6c,
|
||||
0x6b, 0x67, 0x72, 0x6f, 0x75, 0x70, 0x48, 0x00, 0x52, 0x09, 0x74, 0x67, 0x43, 0x6f, 0x6d, 0x6d,
|
||||
0x61, 0x6e, 0x64, 0x42, 0x09, 0x0a, 0x07, 0x63, 0x6f, 0x6d, 0x6d, 0x61, 0x6e, 0x64, 0x42, 0x0d,
|
||||
0x0a, 0x0b, 0x5f, 0x63, 0x6f, 0x6d, 0x6d, 0x61, 0x6e, 0x64, 0x5f, 0x69, 0x64, 0x22, 0xf2, 0x02,
|
||||
0x0a, 0x0d, 0x54, 0x61, 0x6c, 0x6b, 0x67, 0x72, 0x6f, 0x75, 0x70, 0x49, 0x6e, 0x66, 0x6f, 0x12,
|
||||
0x23, 0x0a, 0x02, 0x74, 0x67, 0x18, 0x01, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x13, 0x2e, 0x73, 0x74,
|
||||
0x69, 0x6c, 0x6c, 0x62, 0x6f, 0x78, 0x2e, 0x54, 0x61, 0x6c, 0x6b, 0x67, 0x72, 0x6f, 0x75, 0x70,
|
||||
0x52, 0x02, 0x74, 0x67, 0x12, 0x1f, 0x0a, 0x0b, 0x73, 0x79, 0x73, 0x74, 0x65, 0x6d, 0x5f, 0x6e,
|
||||
0x61, 0x6d, 0x65, 0x18, 0x02, 0x20, 0x01, 0x28, 0x09, 0x52, 0x0a, 0x73, 0x79, 0x73, 0x74, 0x65,
|
||||
0x6d, 0x4e, 0x61, 0x6d, 0x65, 0x12, 0x17, 0x0a, 0x04, 0x6e, 0x61, 0x6d, 0x65, 0x18, 0x03, 0x20,
|
||||
0x01, 0x28, 0x09, 0x48, 0x00, 0x52, 0x04, 0x6e, 0x61, 0x6d, 0x65, 0x88, 0x01, 0x01, 0x12, 0x19,
|
||||
0x0a, 0x05, 0x67, 0x72, 0x6f, 0x75, 0x70, 0x18, 0x04, 0x20, 0x01, 0x28, 0x09, 0x48, 0x01, 0x52,
|
||||
0x05, 0x67, 0x72, 0x6f, 0x75, 0x70, 0x88, 0x01, 0x01, 0x12, 0x20, 0x0a, 0x09, 0x61, 0x6c, 0x70,
|
||||
0x68, 0x61, 0x5f, 0x74, 0x61, 0x67, 0x18, 0x05, 0x20, 0x01, 0x28, 0x09, 0x48, 0x02, 0x52, 0x08,
|
||||
0x61, 0x6c, 0x70, 0x68, 0x61, 0x54, 0x61, 0x67, 0x88, 0x01, 0x01, 0x12, 0x21, 0x0a, 0x09, 0x66,
|
||||
0x72, 0x65, 0x71, 0x75, 0x65, 0x6e, 0x63, 0x79, 0x18, 0x06, 0x20, 0x01, 0x28, 0x05, 0x48, 0x03,
|
||||
0x52, 0x09, 0x66, 0x72, 0x65, 0x71, 0x75, 0x65, 0x6e, 0x63, 0x79, 0x88, 0x01, 0x01, 0x12, 0x12,
|
||||
0x0a, 0x04, 0x74, 0x61, 0x67, 0x73, 0x18, 0x07, 0x20, 0x03, 0x28, 0x09, 0x52, 0x04, 0x74, 0x61,
|
||||
0x67, 0x73, 0x12, 0x38, 0x0a, 0x08, 0x6d, 0x65, 0x74, 0x61, 0x64, 0x61, 0x74, 0x61, 0x18, 0x08,
|
||||
0x20, 0x01, 0x28, 0x0b, 0x32, 0x17, 0x2e, 0x67, 0x6f, 0x6f, 0x67, 0x6c, 0x65, 0x2e, 0x70, 0x72,
|
||||
0x6f, 0x74, 0x6f, 0x62, 0x75, 0x66, 0x2e, 0x53, 0x74, 0x72, 0x75, 0x63, 0x74, 0x48, 0x04, 0x52,
|
||||
0x08, 0x6d, 0x65, 0x74, 0x61, 0x64, 0x61, 0x74, 0x61, 0x88, 0x01, 0x01, 0x12, 0x18, 0x0a, 0x07,
|
||||
0x6c, 0x65, 0x61, 0x72, 0x6e, 0x65, 0x64, 0x18, 0x09, 0x20, 0x01, 0x28, 0x08, 0x52, 0x07, 0x6c,
|
||||
0x65, 0x61, 0x72, 0x6e, 0x65, 0x64, 0x42, 0x07, 0x0a, 0x05, 0x5f, 0x6e, 0x61, 0x6d, 0x65, 0x42,
|
||||
0x08, 0x0a, 0x06, 0x5f, 0x67, 0x72, 0x6f, 0x75, 0x70, 0x42, 0x0c, 0x0a, 0x0a, 0x5f, 0x61, 0x6c,
|
||||
0x70, 0x68, 0x61, 0x5f, 0x74, 0x61, 0x67, 0x42, 0x0c, 0x0a, 0x0a, 0x5f, 0x66, 0x72, 0x65, 0x71,
|
||||
0x75, 0x65, 0x6e, 0x63, 0x79, 0x42, 0x0b, 0x0a, 0x09, 0x5f, 0x6d, 0x65, 0x74, 0x61, 0x64, 0x61,
|
||||
0x74, 0x61, 0x22, 0x7a, 0x0a, 0x04, 0x4c, 0x69, 0x76, 0x65, 0x12, 0x2e, 0x0a, 0x05, 0x73, 0x74,
|
||||
0x61, 0x74, 0x65, 0x18, 0x01, 0x20, 0x01, 0x28, 0x0e, 0x32, 0x13, 0x2e, 0x73, 0x74, 0x69, 0x6c,
|
||||
0x6c, 0x62, 0x6f, 0x78, 0x2e, 0x4c, 0x69, 0x76, 0x65, 0x53, 0x74, 0x61, 0x74, 0x65, 0x48, 0x00,
|
||||
0x52, 0x05, 0x73, 0x74, 0x61, 0x74, 0x65, 0x88, 0x01, 0x01, 0x12, 0x2d, 0x0a, 0x06, 0x66, 0x69,
|
||||
0x6c, 0x74, 0x65, 0x72, 0x18, 0x02, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x10, 0x2e, 0x73, 0x74, 0x69,
|
||||
0x6c, 0x6c, 0x62, 0x6f, 0x78, 0x2e, 0x46, 0x69, 0x6c, 0x74, 0x65, 0x72, 0x48, 0x01, 0x52, 0x06,
|
||||
0x66, 0x69, 0x6c, 0x74, 0x65, 0x72, 0x88, 0x01, 0x01, 0x42, 0x08, 0x0a, 0x06, 0x5f, 0x73, 0x74,
|
||||
0x61, 0x74, 0x65, 0x42, 0x09, 0x0a, 0x07, 0x5f, 0x66, 0x69, 0x6c, 0x74, 0x65, 0x72, 0x22, 0x41,
|
||||
0x0a, 0x09, 0x54, 0x61, 0x6c, 0x6b, 0x67, 0x72, 0x6f, 0x75, 0x70, 0x12, 0x16, 0x0a, 0x06, 0x73,
|
||||
0x79, 0x73, 0x74, 0x65, 0x6d, 0x18, 0x01, 0x20, 0x01, 0x28, 0x05, 0x52, 0x06, 0x73, 0x79, 0x73,
|
||||
0x74, 0x65, 0x6d, 0x12, 0x1c, 0x0a, 0x09, 0x74, 0x61, 0x6c, 0x6b, 0x67, 0x72, 0x6f, 0x75, 0x70,
|
||||
0x18, 0x02, 0x20, 0x01, 0x28, 0x05, 0x52, 0x09, 0x74, 0x61, 0x6c, 0x6b, 0x67, 0x72, 0x6f, 0x75,
|
||||
0x70, 0x22, 0x83, 0x02, 0x0a, 0x06, 0x46, 0x69, 0x6c, 0x74, 0x65, 0x72, 0x12, 0x33, 0x0a, 0x0a,
|
||||
0x74, 0x61, 0x6c, 0x6b, 0x67, 0x72, 0x6f, 0x75, 0x70, 0x73, 0x18, 0x01, 0x20, 0x03, 0x28, 0x0b,
|
||||
0x32, 0x13, 0x2e, 0x73, 0x74, 0x69, 0x6c, 0x6c, 0x62, 0x6f, 0x78, 0x2e, 0x54, 0x61, 0x6c, 0x6b,
|
||||
0x67, 0x72, 0x6f, 0x75, 0x70, 0x52, 0x0a, 0x74, 0x61, 0x6c, 0x6b, 0x67, 0x72, 0x6f, 0x75, 0x70,
|
||||
0x73, 0x12, 0x3a, 0x0a, 0x0e, 0x74, 0x61, 0x6c, 0x6b, 0x67, 0x72, 0x6f, 0x75, 0x70, 0x73, 0x5f,
|
||||
0x6e, 0x6f, 0x74, 0x18, 0x02, 0x20, 0x03, 0x28, 0x0b, 0x32, 0x13, 0x2e, 0x73, 0x74, 0x69, 0x6c,
|
||||
0x6c, 0x62, 0x6f, 0x78, 0x2e, 0x54, 0x61, 0x6c, 0x6b, 0x67, 0x72, 0x6f, 0x75, 0x70, 0x52, 0x0d,
|
||||
0x74, 0x61, 0x6c, 0x6b, 0x67, 0x72, 0x6f, 0x75, 0x70, 0x73, 0x4e, 0x6f, 0x74, 0x12, 0x2c, 0x0a,
|
||||
0x12, 0x74, 0x61, 0x6c, 0x6b, 0x67, 0x72, 0x6f, 0x75, 0x70, 0x5f, 0x74, 0x61, 0x67, 0x73, 0x5f,
|
||||
0x61, 0x6c, 0x6c, 0x18, 0x03, 0x20, 0x03, 0x28, 0x09, 0x52, 0x10, 0x74, 0x61, 0x6c, 0x6b, 0x67,
|
||||
0x72, 0x6f, 0x75, 0x70, 0x54, 0x61, 0x67, 0x73, 0x41, 0x6c, 0x6c, 0x12, 0x2c, 0x0a, 0x12, 0x74,
|
||||
0x61, 0x6c, 0x6b, 0x67, 0x72, 0x6f, 0x75, 0x70, 0x5f, 0x74, 0x61, 0x67, 0x73, 0x5f, 0x61, 0x6e,
|
||||
0x79, 0x18, 0x04, 0x20, 0x03, 0x28, 0x09, 0x52, 0x10, 0x74, 0x61, 0x6c, 0x6b, 0x67, 0x72, 0x6f,
|
||||
0x75, 0x70, 0x54, 0x61, 0x67, 0x73, 0x41, 0x6e, 0x79, 0x12, 0x2c, 0x0a, 0x12, 0x74, 0x61, 0x6c,
|
||||
0x6b, 0x67, 0x72, 0x6f, 0x75, 0x70, 0x5f, 0x74, 0x61, 0x67, 0x73, 0x5f, 0x6e, 0x6f, 0x74, 0x18,
|
||||
0x05, 0x20, 0x03, 0x28, 0x09, 0x52, 0x10, 0x74, 0x61, 0x6c, 0x6b, 0x67, 0x72, 0x6f, 0x75, 0x70,
|
||||
0x54, 0x61, 0x67, 0x73, 0x4e, 0x6f, 0x74, 0x22, 0x08, 0x0a, 0x06, 0x53, 0x65, 0x61, 0x72, 0x63,
|
||||
0x68, 0x22, 0x92, 0x01, 0x0a, 0x0a, 0x53, 0x65, 0x72, 0x76, 0x65, 0x72, 0x49, 0x6e, 0x66, 0x6f,
|
||||
0x12, 0x1f, 0x0a, 0x0b, 0x73, 0x65, 0x72, 0x76, 0x65, 0x72, 0x5f, 0x6e, 0x61, 0x6d, 0x65, 0x18,
|
||||
0x01, 0x20, 0x01, 0x28, 0x09, 0x52, 0x0a, 0x73, 0x65, 0x72, 0x76, 0x65, 0x72, 0x4e, 0x61, 0x6d,
|
||||
0x65, 0x12, 0x18, 0x0a, 0x07, 0x76, 0x65, 0x72, 0x73, 0x69, 0x6f, 0x6e, 0x18, 0x02, 0x20, 0x01,
|
||||
0x28, 0x09, 0x52, 0x07, 0x76, 0x65, 0x72, 0x73, 0x69, 0x6f, 0x6e, 0x12, 0x14, 0x0a, 0x05, 0x62,
|
||||
0x75, 0x69, 0x6c, 0x74, 0x18, 0x03, 0x20, 0x01, 0x28, 0x09, 0x52, 0x05, 0x62, 0x75, 0x69, 0x6c,
|
||||
0x74, 0x12, 0x1a, 0x0a, 0x08, 0x70, 0x6c, 0x61, 0x74, 0x66, 0x6f, 0x72, 0x6d, 0x18, 0x04, 0x20,
|
||||
0x01, 0x28, 0x09, 0x52, 0x08, 0x70, 0x6c, 0x61, 0x74, 0x66, 0x6f, 0x72, 0x6d, 0x12, 0x17, 0x0a,
|
||||
0x07, 0x64, 0x62, 0x5f, 0x73, 0x69, 0x7a, 0x65, 0x18, 0x05, 0x20, 0x01, 0x28, 0x09, 0x52, 0x06,
|
||||
0x64, 0x62, 0x53, 0x69, 0x7a, 0x65, 0x2a, 0x37, 0x0a, 0x09, 0x4c, 0x69, 0x76, 0x65, 0x53, 0x74,
|
||||
0x61, 0x74, 0x65, 0x12, 0x0e, 0x0a, 0x0a, 0x4c, 0x53, 0x5f, 0x53, 0x54, 0x4f, 0x50, 0x50, 0x45,
|
||||
0x44, 0x10, 0x00, 0x12, 0x0b, 0x0a, 0x07, 0x4c, 0x53, 0x5f, 0x4c, 0x49, 0x56, 0x45, 0x10, 0x01,
|
||||
0x12, 0x0d, 0x0a, 0x09, 0x4c, 0x53, 0x5f, 0x50, 0x41, 0x55, 0x53, 0x45, 0x44, 0x10, 0x02, 0x42,
|
||||
0x06, 0x5a, 0x04, 0x2e, 0x2f, 0x70, 0x62, 0x62, 0x06, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x33,
|
||||
0x6b, 0x67, 0x72, 0x6f, 0x75, 0x70, 0x52, 0x02, 0x74, 0x67, 0x12, 0x1f, 0x0a, 0x0b, 0x73, 0x79,
|
||||
0x73, 0x74, 0x65, 0x6d, 0x5f, 0x6e, 0x61, 0x6d, 0x65, 0x18, 0x02, 0x20, 0x01, 0x28, 0x09, 0x52,
|
||||
0x0a, 0x73, 0x79, 0x73, 0x74, 0x65, 0x6d, 0x4e, 0x61, 0x6d, 0x65, 0x12, 0x17, 0x0a, 0x04, 0x6e,
|
||||
0x61, 0x6d, 0x65, 0x18, 0x03, 0x20, 0x01, 0x28, 0x09, 0x48, 0x00, 0x52, 0x04, 0x6e, 0x61, 0x6d,
|
||||
0x65, 0x88, 0x01, 0x01, 0x12, 0x19, 0x0a, 0x05, 0x67, 0x72, 0x6f, 0x75, 0x70, 0x18, 0x04, 0x20,
|
||||
0x01, 0x28, 0x09, 0x48, 0x01, 0x52, 0x05, 0x67, 0x72, 0x6f, 0x75, 0x70, 0x88, 0x01, 0x01, 0x12,
|
||||
0x20, 0x0a, 0x09, 0x61, 0x6c, 0x70, 0x68, 0x61, 0x5f, 0x74, 0x61, 0x67, 0x18, 0x05, 0x20, 0x01,
|
||||
0x28, 0x09, 0x48, 0x02, 0x52, 0x08, 0x61, 0x6c, 0x70, 0x68, 0x61, 0x54, 0x61, 0x67, 0x88, 0x01,
|
||||
0x01, 0x12, 0x21, 0x0a, 0x09, 0x66, 0x72, 0x65, 0x71, 0x75, 0x65, 0x6e, 0x63, 0x79, 0x18, 0x06,
|
||||
0x20, 0x01, 0x28, 0x05, 0x48, 0x03, 0x52, 0x09, 0x66, 0x72, 0x65, 0x71, 0x75, 0x65, 0x6e, 0x63,
|
||||
0x79, 0x88, 0x01, 0x01, 0x12, 0x12, 0x0a, 0x04, 0x74, 0x61, 0x67, 0x73, 0x18, 0x07, 0x20, 0x03,
|
||||
0x28, 0x09, 0x52, 0x04, 0x74, 0x61, 0x67, 0x73, 0x12, 0x38, 0x0a, 0x08, 0x6d, 0x65, 0x74, 0x61,
|
||||
0x64, 0x61, 0x74, 0x61, 0x18, 0x08, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x17, 0x2e, 0x67, 0x6f, 0x6f,
|
||||
0x67, 0x6c, 0x65, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x62, 0x75, 0x66, 0x2e, 0x53, 0x74, 0x72,
|
||||
0x75, 0x63, 0x74, 0x48, 0x04, 0x52, 0x08, 0x6d, 0x65, 0x74, 0x61, 0x64, 0x61, 0x74, 0x61, 0x88,
|
||||
0x01, 0x01, 0x12, 0x18, 0x0a, 0x07, 0x6c, 0x65, 0x61, 0x72, 0x6e, 0x65, 0x64, 0x18, 0x09, 0x20,
|
||||
0x01, 0x28, 0x08, 0x52, 0x07, 0x6c, 0x65, 0x61, 0x72, 0x6e, 0x65, 0x64, 0x42, 0x07, 0x0a, 0x05,
|
||||
0x5f, 0x6e, 0x61, 0x6d, 0x65, 0x42, 0x08, 0x0a, 0x06, 0x5f, 0x67, 0x72, 0x6f, 0x75, 0x70, 0x42,
|
||||
0x0c, 0x0a, 0x0a, 0x5f, 0x61, 0x6c, 0x70, 0x68, 0x61, 0x5f, 0x74, 0x61, 0x67, 0x42, 0x0c, 0x0a,
|
||||
0x0a, 0x5f, 0x66, 0x72, 0x65, 0x71, 0x75, 0x65, 0x6e, 0x63, 0x79, 0x42, 0x0b, 0x0a, 0x09, 0x5f,
|
||||
0x6d, 0x65, 0x74, 0x61, 0x64, 0x61, 0x74, 0x61, 0x22, 0x7a, 0x0a, 0x04, 0x4c, 0x69, 0x76, 0x65,
|
||||
0x12, 0x2e, 0x0a, 0x05, 0x73, 0x74, 0x61, 0x74, 0x65, 0x18, 0x01, 0x20, 0x01, 0x28, 0x0e, 0x32,
|
||||
0x13, 0x2e, 0x73, 0x74, 0x69, 0x6c, 0x6c, 0x62, 0x6f, 0x78, 0x2e, 0x4c, 0x69, 0x76, 0x65, 0x53,
|
||||
0x74, 0x61, 0x74, 0x65, 0x48, 0x00, 0x52, 0x05, 0x73, 0x74, 0x61, 0x74, 0x65, 0x88, 0x01, 0x01,
|
||||
0x12, 0x2d, 0x0a, 0x06, 0x66, 0x69, 0x6c, 0x74, 0x65, 0x72, 0x18, 0x02, 0x20, 0x01, 0x28, 0x0b,
|
||||
0x32, 0x10, 0x2e, 0x73, 0x74, 0x69, 0x6c, 0x6c, 0x62, 0x6f, 0x78, 0x2e, 0x46, 0x69, 0x6c, 0x74,
|
||||
0x65, 0x72, 0x48, 0x01, 0x52, 0x06, 0x66, 0x69, 0x6c, 0x74, 0x65, 0x72, 0x88, 0x01, 0x01, 0x42,
|
||||
0x08, 0x0a, 0x06, 0x5f, 0x73, 0x74, 0x61, 0x74, 0x65, 0x42, 0x09, 0x0a, 0x07, 0x5f, 0x66, 0x69,
|
||||
0x6c, 0x74, 0x65, 0x72, 0x22, 0x41, 0x0a, 0x09, 0x54, 0x61, 0x6c, 0x6b, 0x67, 0x72, 0x6f, 0x75,
|
||||
0x70, 0x12, 0x16, 0x0a, 0x06, 0x73, 0x79, 0x73, 0x74, 0x65, 0x6d, 0x18, 0x01, 0x20, 0x01, 0x28,
|
||||
0x05, 0x52, 0x06, 0x73, 0x79, 0x73, 0x74, 0x65, 0x6d, 0x12, 0x1c, 0x0a, 0x09, 0x74, 0x61, 0x6c,
|
||||
0x6b, 0x67, 0x72, 0x6f, 0x75, 0x70, 0x18, 0x02, 0x20, 0x01, 0x28, 0x05, 0x52, 0x09, 0x74, 0x61,
|
||||
0x6c, 0x6b, 0x67, 0x72, 0x6f, 0x75, 0x70, 0x22, 0x83, 0x02, 0x0a, 0x06, 0x46, 0x69, 0x6c, 0x74,
|
||||
0x65, 0x72, 0x12, 0x33, 0x0a, 0x0a, 0x74, 0x61, 0x6c, 0x6b, 0x67, 0x72, 0x6f, 0x75, 0x70, 0x73,
|
||||
0x18, 0x01, 0x20, 0x03, 0x28, 0x0b, 0x32, 0x13, 0x2e, 0x73, 0x74, 0x69, 0x6c, 0x6c, 0x62, 0x6f,
|
||||
0x78, 0x2e, 0x54, 0x61, 0x6c, 0x6b, 0x67, 0x72, 0x6f, 0x75, 0x70, 0x52, 0x0a, 0x74, 0x61, 0x6c,
|
||||
0x6b, 0x67, 0x72, 0x6f, 0x75, 0x70, 0x73, 0x12, 0x3a, 0x0a, 0x0e, 0x74, 0x61, 0x6c, 0x6b, 0x67,
|
||||
0x72, 0x6f, 0x75, 0x70, 0x73, 0x5f, 0x6e, 0x6f, 0x74, 0x18, 0x02, 0x20, 0x03, 0x28, 0x0b, 0x32,
|
||||
0x13, 0x2e, 0x73, 0x74, 0x69, 0x6c, 0x6c, 0x62, 0x6f, 0x78, 0x2e, 0x54, 0x61, 0x6c, 0x6b, 0x67,
|
||||
0x72, 0x6f, 0x75, 0x70, 0x52, 0x0d, 0x74, 0x61, 0x6c, 0x6b, 0x67, 0x72, 0x6f, 0x75, 0x70, 0x73,
|
||||
0x4e, 0x6f, 0x74, 0x12, 0x2c, 0x0a, 0x12, 0x74, 0x61, 0x6c, 0x6b, 0x67, 0x72, 0x6f, 0x75, 0x70,
|
||||
0x5f, 0x74, 0x61, 0x67, 0x73, 0x5f, 0x61, 0x6c, 0x6c, 0x18, 0x03, 0x20, 0x03, 0x28, 0x09, 0x52,
|
||||
0x10, 0x74, 0x61, 0x6c, 0x6b, 0x67, 0x72, 0x6f, 0x75, 0x70, 0x54, 0x61, 0x67, 0x73, 0x41, 0x6c,
|
||||
0x6c, 0x12, 0x2c, 0x0a, 0x12, 0x74, 0x61, 0x6c, 0x6b, 0x67, 0x72, 0x6f, 0x75, 0x70, 0x5f, 0x74,
|
||||
0x61, 0x67, 0x73, 0x5f, 0x61, 0x6e, 0x79, 0x18, 0x04, 0x20, 0x03, 0x28, 0x09, 0x52, 0x10, 0x74,
|
||||
0x61, 0x6c, 0x6b, 0x67, 0x72, 0x6f, 0x75, 0x70, 0x54, 0x61, 0x67, 0x73, 0x41, 0x6e, 0x79, 0x12,
|
||||
0x2c, 0x0a, 0x12, 0x74, 0x61, 0x6c, 0x6b, 0x67, 0x72, 0x6f, 0x75, 0x70, 0x5f, 0x74, 0x61, 0x67,
|
||||
0x73, 0x5f, 0x6e, 0x6f, 0x74, 0x18, 0x05, 0x20, 0x03, 0x28, 0x09, 0x52, 0x10, 0x74, 0x61, 0x6c,
|
||||
0x6b, 0x67, 0x72, 0x6f, 0x75, 0x70, 0x54, 0x61, 0x67, 0x73, 0x4e, 0x6f, 0x74, 0x22, 0x08, 0x0a,
|
||||
0x06, 0x53, 0x65, 0x61, 0x72, 0x63, 0x68, 0x22, 0x92, 0x01, 0x0a, 0x0a, 0x53, 0x65, 0x72, 0x76,
|
||||
0x65, 0x72, 0x49, 0x6e, 0x66, 0x6f, 0x12, 0x1f, 0x0a, 0x0b, 0x73, 0x65, 0x72, 0x76, 0x65, 0x72,
|
||||
0x5f, 0x6e, 0x61, 0x6d, 0x65, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, 0x0a, 0x73, 0x65, 0x72,
|
||||
0x76, 0x65, 0x72, 0x4e, 0x61, 0x6d, 0x65, 0x12, 0x18, 0x0a, 0x07, 0x76, 0x65, 0x72, 0x73, 0x69,
|
||||
0x6f, 0x6e, 0x18, 0x02, 0x20, 0x01, 0x28, 0x09, 0x52, 0x07, 0x76, 0x65, 0x72, 0x73, 0x69, 0x6f,
|
||||
0x6e, 0x12, 0x14, 0x0a, 0x05, 0x62, 0x75, 0x69, 0x6c, 0x74, 0x18, 0x03, 0x20, 0x01, 0x28, 0x09,
|
||||
0x52, 0x05, 0x62, 0x75, 0x69, 0x6c, 0x74, 0x12, 0x1a, 0x0a, 0x08, 0x70, 0x6c, 0x61, 0x74, 0x66,
|
||||
0x6f, 0x72, 0x6d, 0x18, 0x04, 0x20, 0x01, 0x28, 0x09, 0x52, 0x08, 0x70, 0x6c, 0x61, 0x74, 0x66,
|
||||
0x6f, 0x72, 0x6d, 0x12, 0x17, 0x0a, 0x07, 0x64, 0x62, 0x5f, 0x73, 0x69, 0x7a, 0x65, 0x18, 0x05,
|
||||
0x20, 0x01, 0x28, 0x09, 0x52, 0x06, 0x64, 0x62, 0x53, 0x69, 0x7a, 0x65, 0x2a, 0x37, 0x0a, 0x09,
|
||||
0x4c, 0x69, 0x76, 0x65, 0x53, 0x74, 0x61, 0x74, 0x65, 0x12, 0x0e, 0x0a, 0x0a, 0x4c, 0x53, 0x5f,
|
||||
0x53, 0x54, 0x4f, 0x50, 0x50, 0x45, 0x44, 0x10, 0x00, 0x12, 0x0b, 0x0a, 0x07, 0x4c, 0x53, 0x5f,
|
||||
0x4c, 0x49, 0x56, 0x45, 0x10, 0x01, 0x12, 0x0d, 0x0a, 0x09, 0x4c, 0x53, 0x5f, 0x50, 0x41, 0x55,
|
||||
0x53, 0x45, 0x44, 0x10, 0x02, 0x42, 0x06, 0x5a, 0x04, 0x2e, 0x2f, 0x70, 0x62, 0x62, 0x06, 0x70,
|
||||
0x72, 0x6f, 0x74, 0x6f, 0x33,
|
||||
}
|
||||
|
||||
var (
|
||||
|
|
|
@ -34,9 +34,8 @@ message Call {
|
|||
int64 frequency = 8;
|
||||
repeated int64 frequencies = 9;
|
||||
repeated int32 patches = 10;
|
||||
repeated int32 sources = 11;
|
||||
optional int32 duration = 12;
|
||||
bytes audio = 13;
|
||||
optional int32 duration = 11;
|
||||
bytes audio = 12;
|
||||
}
|
||||
|
||||
message Hello {
|
||||
|
|
|
@ -3,12 +3,15 @@ package rest
|
|||
import (
|
||||
"errors"
|
||||
"net/http"
|
||||
"net/url"
|
||||
|
||||
"dynatron.me/x/stillbox/internal/common"
|
||||
"dynatron.me/x/stillbox/pkg/talkgroups/tgstore"
|
||||
|
||||
"github.com/go-chi/chi/v5"
|
||||
"github.com/go-chi/render"
|
||||
"github.com/go-viper/mapstructure/v2"
|
||||
"github.com/google/uuid"
|
||||
"github.com/jackc/pgx/v5"
|
||||
"github.com/rs/zerolog/log"
|
||||
)
|
||||
|
@ -18,10 +21,11 @@ type API interface {
|
|||
}
|
||||
|
||||
type api struct {
|
||||
baseURL url.URL
|
||||
}
|
||||
|
||||
func New() *api {
|
||||
s := new(api)
|
||||
func New(baseURL url.URL) *api {
|
||||
s := &api{baseURL}
|
||||
|
||||
return s
|
||||
}
|
||||
|
@ -32,6 +36,7 @@ func (a *api) Subrouter() http.Handler {
|
|||
r.Mount("/talkgroup", new(talkgroupAPI).Subrouter())
|
||||
r.Mount("/call", new(callsAPI).Subrouter())
|
||||
r.Mount("/user", new(usersAPI).Subrouter())
|
||||
r.Mount("/incident", newIncidentsAPI(&a.baseURL).Subrouter())
|
||||
|
||||
return r
|
||||
}
|
||||
|
@ -124,6 +129,7 @@ var statusMapping = map[error]errResponder{
|
|||
tgstore.ErrReference: constraintErrText,
|
||||
ErrBadUID: unauthErrText,
|
||||
ErrBadAppName: unauthErrText,
|
||||
common.ErrPageOutOfRange: badRequestErrText,
|
||||
}
|
||||
|
||||
func autoError(err error) render.Renderer {
|
||||
|
@ -173,6 +179,21 @@ func decodeParams(d interface{}, r *http.Request) error {
|
|||
return dec.Decode(m)
|
||||
}
|
||||
|
||||
// idOnlyParam checks for a sole URL parameter, id, and writes an errorif this fails.
|
||||
func idOnlyParam(w http.ResponseWriter, r *http.Request) (uuid.UUID, error) {
|
||||
params := struct {
|
||||
ID uuid.UUID `param:"id"`
|
||||
}{}
|
||||
|
||||
err := decodeParams(¶ms, r)
|
||||
if err != nil {
|
||||
wErr(w, r, badRequest(err))
|
||||
return uuid.UUID{}, err
|
||||
}
|
||||
|
||||
return params.ID, nil
|
||||
}
|
||||
|
||||
func respond(w http.ResponseWriter, r *http.Request, v interface{}) {
|
||||
render.DefaultResponder(w, r, v)
|
||||
}
|
||||
|
|
237
pkg/rest/incidents.go
Normal file
237
pkg/rest/incidents.go
Normal file
|
@ -0,0 +1,237 @@
|
|||
package rest
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"net/http"
|
||||
"net/url"
|
||||
|
||||
"dynatron.me/x/stillbox/internal/common"
|
||||
"dynatron.me/x/stillbox/internal/forms"
|
||||
"dynatron.me/x/stillbox/internal/jsontypes"
|
||||
"dynatron.me/x/stillbox/pkg/incidents"
|
||||
"dynatron.me/x/stillbox/pkg/incidents/incstore"
|
||||
"dynatron.me/x/stillbox/pkg/talkgroups/tgstore"
|
||||
|
||||
"github.com/go-chi/chi/v5"
|
||||
"github.com/google/uuid"
|
||||
)
|
||||
|
||||
type incidentsAPI struct {
|
||||
baseURL *url.URL
|
||||
}
|
||||
|
||||
func newIncidentsAPI(baseURL *url.URL) API {
|
||||
return &incidentsAPI{baseURL}
|
||||
}
|
||||
|
||||
func (ia *incidentsAPI) Subrouter() http.Handler {
|
||||
r := chi.NewMux()
|
||||
|
||||
r.Get(`/{id:[a-f0-9-]+}`, ia.getIncident)
|
||||
r.Get(`/{id:[a-f0-9-]+}.m3u`, ia.getCallsM3U)
|
||||
|
||||
r.Post(`/new`, ia.createIncident)
|
||||
r.Post(`/`, ia.listIncidents)
|
||||
r.Post(`/{id:[a-f0-9-]+}/calls`, ia.postCalls)
|
||||
|
||||
r.Patch(`/{id:[a-f0-9-]+}`, ia.updateIncident)
|
||||
|
||||
r.Delete(`/{id:[a-f0-9-]+}`, ia.deleteIncident)
|
||||
|
||||
return r
|
||||
}
|
||||
|
||||
func (ia *incidentsAPI) listIncidents(w http.ResponseWriter, r *http.Request) {
|
||||
ctx := r.Context()
|
||||
incs := incstore.FromCtx(ctx)
|
||||
|
||||
p := incstore.IncidentsParams{}
|
||||
err := forms.Unmarshal(r, &p, forms.WithTag("json"), forms.WithAcceptBlank(), forms.WithOmitEmpty())
|
||||
if err != nil {
|
||||
wErr(w, r, badRequest(err))
|
||||
return
|
||||
}
|
||||
|
||||
res := struct {
|
||||
Incidents []incstore.Incident `json:"incidents"`
|
||||
Count int `json:"count"`
|
||||
}{}
|
||||
|
||||
res.Incidents, res.Count, err = incs.Incidents(ctx, p)
|
||||
if err != nil {
|
||||
wErr(w, r, autoError(err))
|
||||
return
|
||||
}
|
||||
|
||||
respond(w, r, res)
|
||||
}
|
||||
|
||||
func (ia *incidentsAPI) createIncident(w http.ResponseWriter, r *http.Request) {
|
||||
ctx := r.Context()
|
||||
incs := incstore.FromCtx(ctx)
|
||||
|
||||
p := incidents.Incident{}
|
||||
err := forms.Unmarshal(r, &p, forms.WithTag("json"), forms.WithAcceptBlank(), forms.WithOmitEmpty())
|
||||
if err != nil {
|
||||
wErr(w, r, badRequest(err))
|
||||
return
|
||||
}
|
||||
|
||||
inc, err := incs.CreateIncident(ctx, p)
|
||||
if err != nil {
|
||||
wErr(w, r, autoError(err))
|
||||
return
|
||||
}
|
||||
|
||||
respond(w, r, inc)
|
||||
}
|
||||
|
||||
func (ia *incidentsAPI) getIncident(w http.ResponseWriter, r *http.Request) {
|
||||
ctx := r.Context()
|
||||
incs := incstore.FromCtx(ctx)
|
||||
|
||||
id, err := idOnlyParam(w, r)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
|
||||
inc, err := incs.Incident(ctx, id)
|
||||
if err != nil {
|
||||
wErr(w, r, autoError(err))
|
||||
return
|
||||
}
|
||||
|
||||
respond(w, r, inc)
|
||||
}
|
||||
|
||||
func (ia *incidentsAPI) updateIncident(w http.ResponseWriter, r *http.Request) {
|
||||
ctx := r.Context()
|
||||
incs := incstore.FromCtx(ctx)
|
||||
|
||||
id, err := idOnlyParam(w, r)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
|
||||
p := incstore.UpdateIncidentParams{}
|
||||
err = forms.Unmarshal(r, &p, forms.WithTag("json"), forms.WithAcceptBlank(), forms.WithOmitEmpty())
|
||||
if err != nil {
|
||||
wErr(w, r, badRequest(err))
|
||||
return
|
||||
}
|
||||
|
||||
inc, err := incs.UpdateIncident(ctx, id, p)
|
||||
if err != nil {
|
||||
wErr(w, r, autoError(err))
|
||||
return
|
||||
}
|
||||
|
||||
respond(w, r, inc)
|
||||
}
|
||||
|
||||
func (ia *incidentsAPI) deleteIncident(w http.ResponseWriter, r *http.Request) {
|
||||
ctx := r.Context()
|
||||
incs := incstore.FromCtx(ctx)
|
||||
|
||||
urlParams := struct {
|
||||
ID uuid.UUID `param:"id"`
|
||||
}{}
|
||||
|
||||
err := decodeParams(&urlParams, r)
|
||||
if err != nil {
|
||||
wErr(w, r, badRequest(err))
|
||||
return
|
||||
}
|
||||
|
||||
err = incs.DeleteIncident(ctx, urlParams.ID)
|
||||
if err != nil {
|
||||
wErr(w, r, autoError(err))
|
||||
return
|
||||
}
|
||||
|
||||
w.WriteHeader(http.StatusNoContent)
|
||||
}
|
||||
|
||||
type CallIncidentParams struct {
|
||||
Add jsontypes.UUIDs `json:"add"`
|
||||
Notes json.RawMessage `json:"notes"`
|
||||
|
||||
Remove jsontypes.UUIDs `json:"remove"`
|
||||
}
|
||||
|
||||
func (ia *incidentsAPI) postCalls(w http.ResponseWriter, r *http.Request) {
|
||||
ctx := r.Context()
|
||||
incs := incstore.FromCtx(ctx)
|
||||
|
||||
id, err := idOnlyParam(w, r)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
|
||||
p := CallIncidentParams{}
|
||||
err = forms.Unmarshal(r, &p, forms.WithTag("json"), forms.WithAcceptBlank(), forms.WithOmitEmpty())
|
||||
if err != nil {
|
||||
wErr(w, r, badRequest(err))
|
||||
return
|
||||
}
|
||||
|
||||
err = incs.AddRemoveIncidentCalls(ctx, id, p.Add.UUIDs(), p.Notes, p.Remove.UUIDs())
|
||||
if err != nil {
|
||||
wErr(w, r, autoError(err))
|
||||
return
|
||||
}
|
||||
|
||||
w.WriteHeader(http.StatusNoContent)
|
||||
}
|
||||
|
||||
func (ia *incidentsAPI) getCallsM3U(w http.ResponseWriter, r *http.Request) {
|
||||
ctx := r.Context()
|
||||
incs := incstore.FromCtx(ctx)
|
||||
tgst := tgstore.FromCtx(ctx)
|
||||
|
||||
id, err := idOnlyParam(w, r)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
|
||||
inc, err := incs.Incident(ctx, id)
|
||||
if err != nil {
|
||||
wErr(w, r, autoError(err))
|
||||
return
|
||||
}
|
||||
|
||||
var b bytes.Buffer
|
||||
|
||||
callUrl := common.PtrTo(*ia.baseURL)
|
||||
|
||||
b.WriteString("#EXTM3U\n\n")
|
||||
for _, c := range inc.Calls {
|
||||
tg, err := tgst.TG(ctx, c.TalkgroupTuple())
|
||||
if err != nil {
|
||||
wErr(w, r, autoError(err))
|
||||
return
|
||||
}
|
||||
var from string
|
||||
if c.Source != 0 {
|
||||
from = fmt.Sprintf(" from %d", c.Source)
|
||||
}
|
||||
|
||||
callUrl.Path = "/api/call/" + c.ID.String()
|
||||
|
||||
fmt.Fprintf(w, "#EXTINF:%d,%s%s (%s)\n%s\n\n",
|
||||
c.Duration.Seconds(),
|
||||
tg.StringTag(true),
|
||||
from,
|
||||
c.DateTime.Format("15:04 01/02"),
|
||||
callUrl,
|
||||
)
|
||||
}
|
||||
|
||||
// Not a lot of agreement on which MIME type to use for non-HLS m3u,
|
||||
// let's hope this is good enough
|
||||
w.Header().Set("Content-Type", "audio/x-mpegurl")
|
||||
w.WriteHeader(http.StatusOK)
|
||||
_, _ = b.WriteTo(w)
|
||||
}
|
|
@ -8,9 +8,11 @@ import (
|
|||
|
||||
"dynatron.me/x/stillbox/pkg/alerting"
|
||||
"dynatron.me/x/stillbox/pkg/auth"
|
||||
"dynatron.me/x/stillbox/pkg/calls/callstore"
|
||||
"dynatron.me/x/stillbox/pkg/config"
|
||||
"dynatron.me/x/stillbox/pkg/database"
|
||||
"dynatron.me/x/stillbox/pkg/database/partman"
|
||||
"dynatron.me/x/stillbox/pkg/incidents/incstore"
|
||||
"dynatron.me/x/stillbox/pkg/nexus"
|
||||
"dynatron.me/x/stillbox/pkg/notify"
|
||||
"dynatron.me/x/stillbox/pkg/rest"
|
||||
|
@ -28,22 +30,24 @@ import (
|
|||
const shutdownTimeout = 5 * time.Second
|
||||
|
||||
type Server struct {
|
||||
auth *auth.Auth
|
||||
conf *config.Configuration
|
||||
db database.Store
|
||||
r *chi.Mux
|
||||
sources sources.Sources
|
||||
sinks sinks.Sinks
|
||||
relayer *sinks.RelayManager
|
||||
nex *nexus.Nexus
|
||||
logger *Logger
|
||||
alerter alerting.Alerter
|
||||
notifier notify.Notifier
|
||||
hup chan os.Signal
|
||||
tgs tgstore.Store
|
||||
rest rest.API
|
||||
partman partman.PartitionManager
|
||||
users users.Store
|
||||
auth *auth.Auth
|
||||
conf *config.Configuration
|
||||
db database.Store
|
||||
r *chi.Mux
|
||||
sources sources.Sources
|
||||
sinks sinks.Sinks
|
||||
relayer *sinks.RelayManager
|
||||
nex *nexus.Nexus
|
||||
logger *Logger
|
||||
alerter alerting.Alerter
|
||||
notifier notify.Notifier
|
||||
hup chan os.Signal
|
||||
tgs tgstore.Store
|
||||
rest rest.API
|
||||
partman partman.PartitionManager
|
||||
users users.Store
|
||||
calls callstore.Store
|
||||
incidents incstore.Store
|
||||
}
|
||||
|
||||
func New(ctx context.Context, cfg *config.Configuration) (*Server, error) {
|
||||
|
@ -67,21 +71,23 @@ func New(ctx context.Context, cfg *config.Configuration) (*Server, error) {
|
|||
}
|
||||
|
||||
tgCache := tgstore.NewCache()
|
||||
api := rest.New()
|
||||
api := rest.New(cfg.BaseURL.URL())
|
||||
|
||||
srv := &Server{
|
||||
auth: authenticator,
|
||||
conf: cfg,
|
||||
db: db,
|
||||
r: r,
|
||||
nex: nexus.New(),
|
||||
logger: logger,
|
||||
alerter: alerting.New(cfg.Alerting, tgCache, alerting.WithNotifier(notifier)),
|
||||
notifier: notifier,
|
||||
tgs: tgCache,
|
||||
sinks: sinks.NewSinkManager(),
|
||||
rest: api,
|
||||
users: users.NewStore(),
|
||||
auth: authenticator,
|
||||
conf: cfg,
|
||||
db: db,
|
||||
r: r,
|
||||
nex: nexus.New(),
|
||||
logger: logger,
|
||||
alerter: alerting.New(cfg.Alerting, tgCache, alerting.WithNotifier(notifier)),
|
||||
notifier: notifier,
|
||||
tgs: tgCache,
|
||||
sinks: sinks.NewSinkManager(),
|
||||
rest: api,
|
||||
users: users.NewStore(),
|
||||
calls: callstore.NewStore(),
|
||||
incidents: incstore.NewStore(),
|
||||
}
|
||||
|
||||
if cfg.DB.Partition.Enabled {
|
||||
|
@ -129,14 +135,22 @@ func New(ctx context.Context, cfg *config.Configuration) (*Server, error) {
|
|||
return srv, nil
|
||||
}
|
||||
|
||||
func (s *Server) addStoresTo(ctx context.Context) context.Context {
|
||||
ctx = database.CtxWithDB(ctx, s.db)
|
||||
ctx = tgstore.CtxWithStore(ctx, s.tgs)
|
||||
ctx = users.CtxWithStore(ctx, s.users)
|
||||
ctx = callstore.CtxWithStore(ctx, s.calls)
|
||||
ctx = incstore.CtxWithStore(ctx, s.incidents)
|
||||
|
||||
return ctx
|
||||
}
|
||||
|
||||
func (s *Server) Go(ctx context.Context) error {
|
||||
defer database.Close(s.db)
|
||||
|
||||
s.installHupHandler()
|
||||
|
||||
ctx = database.CtxWithDB(ctx, s.db)
|
||||
ctx = tgstore.CtxWithStore(ctx, s.tgs)
|
||||
ctx = users.CtxWithStore(ctx, s.users)
|
||||
ctx = s.addStoresTo(ctx)
|
||||
|
||||
httpSrv := &http.Server{
|
||||
Addr: s.conf.Listen,
|
||||
|
|
|
@ -81,7 +81,7 @@ func (s *Relay) Call(ctx context.Context, call *calls.Call) error {
|
|||
var buf bytes.Buffer
|
||||
body := multipart.NewWriter(&buf)
|
||||
|
||||
err := forms.Marshal(call, body)
|
||||
err := forms.Marshal(call, body, forms.WithTag("json"))
|
||||
if err != nil {
|
||||
return fmt.Errorf("relay form parse: %w", err)
|
||||
}
|
||||
|
|
|
@ -46,7 +46,6 @@ type CallUploadRequest struct {
|
|||
Key string `form:"key"`
|
||||
Patches []int `form:"patches"`
|
||||
Source int `form:"source"`
|
||||
Sources []int `form:"sources"`
|
||||
System int `form:"system"`
|
||||
SystemLabel string `form:"systemLabel"`
|
||||
Talkgroup int `form:"talkgroup"`
|
||||
|
|
50
pkg/store/store.go
Normal file
50
pkg/store/store.go
Normal file
|
@ -0,0 +1,50 @@
|
|||
package store
|
||||
|
||||
import (
|
||||
"context"
|
||||
|
||||
"dynatron.me/x/stillbox/pkg/talkgroups/tgstore"
|
||||
"dynatron.me/x/stillbox/pkg/users"
|
||||
)
|
||||
|
||||
type Store interface {
|
||||
TG() tgstore.Store
|
||||
User() users.Store
|
||||
}
|
||||
|
||||
type store struct {
|
||||
tg tgstore.Store
|
||||
user users.Store
|
||||
}
|
||||
|
||||
func (s *store) TG() tgstore.Store {
|
||||
return s.tg
|
||||
}
|
||||
|
||||
func (s *store) User() users.Store {
|
||||
return s.user
|
||||
}
|
||||
|
||||
func New() Store {
|
||||
return &store{
|
||||
tg: tgstore.NewCache(),
|
||||
user: users.NewStore(),
|
||||
}
|
||||
}
|
||||
|
||||
type storeCtxKey string
|
||||
|
||||
const StoreCtxKey storeCtxKey = "store"
|
||||
|
||||
func CtxWithStore(ctx context.Context, s Store) context.Context {
|
||||
return context.WithValue(ctx, StoreCtxKey, s)
|
||||
}
|
||||
|
||||
func FromCtx(ctx context.Context) Store {
|
||||
s, ok := ctx.Value(StoreCtxKey).(Store)
|
||||
if !ok {
|
||||
return New()
|
||||
}
|
||||
|
||||
return s
|
||||
}
|
|
@ -104,7 +104,7 @@ CREATE TABLE IF NOT EXISTS calls(
|
|||
) PARTITION BY RANGE (call_date);
|
||||
|
||||
CREATE INDEX IF NOT EXISTS calls_transcript_idx ON calls USING GIN (to_tsvector('english', transcript));
|
||||
CREATE INDEX IF NOT EXISTS calls_call_date_tg_idx ON calls(system, talkgroup, call_date);
|
||||
CREATE INDEX IF NOT EXISTS calls_call_date_tg_idx ON calls(call_date, talkgroup, system);
|
||||
|
||||
CREATE TABLE swept_calls (
|
||||
id UUID PRIMARY KEY,
|
||||
|
@ -142,8 +142,8 @@ CREATE TABLE IF NOT EXISTS incidents(
|
|||
id UUID PRIMARY KEY,
|
||||
name TEXT NOT NULL,
|
||||
description TEXT,
|
||||
start_time TIMESTAMP,
|
||||
end_time TIMESTAMP,
|
||||
start_time TIMESTAMPTZ,
|
||||
end_time TIMESTAMPTZ,
|
||||
location JSONB,
|
||||
metadata JSONB
|
||||
);
|
||||
|
|
|
@ -38,7 +38,22 @@ source
|
|||
);
|
||||
|
||||
-- name: GetCallAudioByID :one
|
||||
SELECT call_date, audio_name, audio_type, audio_blob FROM calls WHERE id = @id;
|
||||
SELECT
|
||||
c.call_date,
|
||||
c.audio_name,
|
||||
c.audio_type,
|
||||
c.audio_blob
|
||||
FROM calls c
|
||||
WHERE c.id = @id
|
||||
UNION
|
||||
SELECT
|
||||
sc.call_date,
|
||||
sc.audio_name,
|
||||
sc.audio_type,
|
||||
sc.audio_blob
|
||||
FROM swept_calls sc
|
||||
WHERE sc.id = @id
|
||||
;
|
||||
|
||||
-- name: SetCallTranscript :exec
|
||||
UPDATE calls SET transcript = $2 WHERE id = $1;
|
||||
|
@ -61,6 +76,7 @@ VALUES
|
|||
SELECT pg_size_pretty(pg_database_size(current_database()));
|
||||
|
||||
-- name: SweepCalls :execrows
|
||||
-- This is used to sweep calls that are part of an incident prior to pruning a partition.
|
||||
WITH to_sweep AS (
|
||||
SELECT id, submitter, system, talkgroup, calls.call_date, audio_name, audio_blob, duration, audio_type,
|
||||
audio_url, frequency, frequencies, patches, tg_label, tg_alpha_tag, tg_group, source, transcript
|
||||
|
@ -70,7 +86,6 @@ WITH to_sweep AS (
|
|||
) INSERT INTO swept_calls SELECT * FROM to_sweep;
|
||||
|
||||
-- name: CleanupSweptCalls :execrows
|
||||
-- This is used to sweep calls that are part of an incident prior to pruning a partition.
|
||||
WITH to_sweep AS (
|
||||
SELECT id FROM calls
|
||||
JOIN incidents_calls ic ON ic.call_id = calls.id
|
||||
|
|
157
sql/postgres/queries/incidents.sql
Normal file
157
sql/postgres/queries/incidents.sql
Normal file
|
@ -0,0 +1,157 @@
|
|||
-- name: AddToIncident :exec
|
||||
WITH inp AS (
|
||||
SELECT
|
||||
UNNEST(@call_ids::UUID[]) id,
|
||||
UNNEST(@notes::JSONB[]) notes
|
||||
) INSERT INTO incidents_calls(
|
||||
incident_id,
|
||||
call_id,
|
||||
calls_tbl_id,
|
||||
call_date,
|
||||
notes
|
||||
)
|
||||
SELECT
|
||||
@incident_id::UUID,
|
||||
inp.id,
|
||||
inp.id,
|
||||
c.call_date,
|
||||
inp.notes
|
||||
FROM inp
|
||||
JOIN calls c ON c.id = inp.id
|
||||
;
|
||||
|
||||
-- name: RemoveFromIncident :exec
|
||||
DELETE FROM incidents_calls ic
|
||||
WHERE ic.incident_id = @id AND ic.call_id = ANY(@call_ids::UUID[]);
|
||||
|
||||
-- name: UpdateCallIncidentNotes :exec
|
||||
UPDATE incidents_Calls
|
||||
SET notes = @notes
|
||||
WHERE incident_id = @incident_id AND call_id = @call_id;
|
||||
|
||||
-- name: CreateIncident :one
|
||||
INSERT INTO incidents (
|
||||
id,
|
||||
name,
|
||||
description,
|
||||
start_time,
|
||||
end_time,
|
||||
location,
|
||||
metadata
|
||||
) VALUES (
|
||||
@id,
|
||||
@name,
|
||||
sqlc.narg('description'),
|
||||
sqlc.narg('start_time'),
|
||||
sqlc.narg('end_time'),
|
||||
sqlc.narg('location'),
|
||||
sqlc.narg('metadata')
|
||||
)
|
||||
RETURNING *;
|
||||
|
||||
|
||||
-- name: ListIncidentsP :many
|
||||
SELECT
|
||||
i.id,
|
||||
i.name,
|
||||
i.description,
|
||||
i.start_time,
|
||||
i.end_time,
|
||||
i.location,
|
||||
i.metadata,
|
||||
COUNT(ic.incident_id) calls_count
|
||||
FROM incidents i
|
||||
LEFT JOIN incidents_calls ic ON i.id = ic.incident_id
|
||||
WHERE
|
||||
CASE WHEN sqlc.narg('start')::TIMESTAMPTZ IS NOT NULL THEN
|
||||
i.start_time >= sqlc.narg('start') ELSE TRUE END AND
|
||||
CASE WHEN sqlc.narg('end')::TIMESTAMPTZ IS NOT NULL THEN
|
||||
i.start_time <= sqlc.narg('end') ELSE TRUE END AND
|
||||
(CASE WHEN sqlc.narg('filter')::TEXT IS NOT NULL THEN (
|
||||
i.name ILIKE '%' || @filter || '%' OR
|
||||
i.description ILIKE '%' || @filter || '%'
|
||||
) ELSE TRUE END)
|
||||
GROUP BY i.id
|
||||
ORDER BY
|
||||
CASE WHEN @direction::TEXT = 'asc' THEN i.start_time END ASC,
|
||||
CASE WHEN @direction::TEXT = 'desc' THEN i.start_time END DESC
|
||||
OFFSET sqlc.arg('offset') ROWS
|
||||
FETCH NEXT sqlc.arg('per_page') ROWS ONLY
|
||||
;
|
||||
|
||||
-- name: ListIncidentsCount :one
|
||||
SELECT COUNT(*)
|
||||
FROM incidents i
|
||||
WHERE
|
||||
CASE WHEN sqlc.narg('start')::TIMESTAMPTZ IS NOT NULL THEN
|
||||
i.start_time >= sqlc.narg('start') ELSE TRUE END AND
|
||||
CASE WHEN sqlc.narg('end')::TIMESTAMPTZ IS NOT NULL THEN
|
||||
i.start_time <= sqlc.narg('end') ELSE TRUE END AND
|
||||
(CASE WHEN sqlc.narg('filter')::TEXT IS NOT NULL THEN (
|
||||
i.name ILIKE '%' || @filter || '%' OR
|
||||
i.description ILIKE '%' || @filter || '%'
|
||||
) ELSE TRUE END)
|
||||
;
|
||||
|
||||
-- name: GetIncidentCalls :many
|
||||
SELECT ic.call_id, ic.call_date, ic.notes, c.*
|
||||
FROM incidents_calls ic, LATERAL (
|
||||
SELECT
|
||||
ca.submitter,
|
||||
ca.system,
|
||||
ca.talkgroup,
|
||||
ca.audio_name,
|
||||
ca.duration,
|
||||
ca.audio_type,
|
||||
ca.audio_url,
|
||||
ca.frequency,
|
||||
ca.frequencies,
|
||||
ca.patches,
|
||||
ca.source,
|
||||
ca.transcript
|
||||
FROM calls ca WHERE ca.id = ic.calls_tbl_id AND ca.call_date = ic.call_date
|
||||
UNION
|
||||
SELECT
|
||||
sc.submitter,
|
||||
sc.system,
|
||||
sc.talkgroup,
|
||||
sc.audio_name,
|
||||
sc.duration,
|
||||
sc.audio_type,
|
||||
sc.audio_url,
|
||||
sc.frequency,
|
||||
sc.frequencies,
|
||||
sc.patches,
|
||||
sc.source,
|
||||
sc.transcript
|
||||
FROM swept_calls sc WHERE sc.id = ic.swept_call_id
|
||||
) c
|
||||
WHERE ic.incident_id = @id;
|
||||
|
||||
-- name: GetIncident :one
|
||||
SELECT
|
||||
i.id,
|
||||
i.name,
|
||||
i.description,
|
||||
i.start_time,
|
||||
i.end_time,
|
||||
i.location,
|
||||
i.metadata
|
||||
FROM incidents i
|
||||
WHERE i.id = @id;
|
||||
|
||||
-- name: UpdateIncident :one
|
||||
UPDATE incidents
|
||||
SET
|
||||
name = COALESCE(sqlc.narg('name'), name),
|
||||
description = COALESCE(sqlc.narg('description'), description),
|
||||
start_time = COALESCE(sqlc.narg('start_time'), start_time),
|
||||
end_time = COALESCE(sqlc.narg('end_time'), end_time),
|
||||
location = COALESCE(sqlc.narg('location'), location),
|
||||
metadata = COALESCE(sqlc.narg('metadata'), metadata)
|
||||
WHERE
|
||||
id = @id
|
||||
RETURNING *;
|
||||
|
||||
-- name: DeleteIncident :exec
|
||||
DELETE FROM incidents CASCADE WHERE id = @id;
|
|
@ -37,6 +37,11 @@ sql:
|
|||
import: "dynatron.me/x/stillbox/internal/jsontypes"
|
||||
type: "Metadata"
|
||||
nullable: true
|
||||
- column: "incidents.metadata"
|
||||
go_type:
|
||||
import: "dynatron.me/x/stillbox/internal/jsontypes"
|
||||
type: "Metadata"
|
||||
nullable: true
|
||||
- column: "pg_catalog.pg_tables.tablename"
|
||||
go_type: string
|
||||
nullable: false
|
||||
|
|
Loading…
Reference in a new issue