Compare commits
1 Commits
mk/add_web
...
a7821a7cb5
| Author | SHA1 | Date | |
|---|---|---|---|
| a7821a7cb5 |
@@ -25,7 +25,8 @@
|
|||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@companion-module/base": "~1.11.3",
|
"@companion-module/base": "~1.11.3",
|
||||||
"toggl-track": "^0.8.0"
|
"toggl-track": "^0.8.0",
|
||||||
|
"ws": "^8.18.2"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@companion-module/tools": "~2.3.0",
|
"@companion-module/tools": "~2.3.0",
|
||||||
|
|||||||
@@ -6,6 +6,8 @@ export interface ModuleConfig {
|
|||||||
alwaysStart: boolean
|
alwaysStart: boolean
|
||||||
startTimerPoller: boolean
|
startTimerPoller: boolean
|
||||||
timerPollerInterval: number
|
timerPollerInterval: number
|
||||||
|
startWebSocket: boolean
|
||||||
|
websocketSecret: string
|
||||||
}
|
}
|
||||||
|
|
||||||
export function GetConfigFields(): SomeCompanionConfigField[] {
|
export function GetConfigFields(): SomeCompanionConfigField[] {
|
||||||
@@ -50,5 +52,19 @@ export function GetConfigFields(): SomeCompanionConfigField[] {
|
|||||||
min: 30,
|
min: 30,
|
||||||
max: 3600,
|
max: 3600,
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
type: 'checkbox',
|
||||||
|
id: 'startWebSocket',
|
||||||
|
label: 'start Websocket (krombel testing)',
|
||||||
|
width: 12,
|
||||||
|
default: false,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
type: 'textinput',
|
||||||
|
id: 'websocketSecret',
|
||||||
|
label: 'secret to authenticate against Websocket server (krombel testing)',
|
||||||
|
width: 12,
|
||||||
|
default: 'secret',
|
||||||
|
},
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
|
|||||||
94
src/main.ts
94
src/main.ts
@@ -9,8 +9,9 @@ import UpdateVariableDefinitions from './variables.js'
|
|||||||
import UpgradeScripts from './upgrades.js'
|
import UpgradeScripts from './upgrades.js'
|
||||||
import { UpdateFeedbacks } from './feedbacks.js'
|
import { UpdateFeedbacks } from './feedbacks.js'
|
||||||
import { Toggl, ITimeEntry, IWorkspaceProject, IClient } from 'toggl-track'
|
import { Toggl, ITimeEntry, IWorkspaceProject, IClient } from 'toggl-track'
|
||||||
import { togglGetWorkspaces } from './toggl-extend.js'
|
import { isITimeEntryWebhookPayload, togglGetWorkspaces } from './toggl-extend.js'
|
||||||
import { timecodeSince } from './utils.js'
|
import { timecodeSince } from './utils.js'
|
||||||
|
import WebSocket from 'ws'
|
||||||
|
|
||||||
export class TogglTrack extends InstanceBase<ModuleConfig> {
|
export class TogglTrack extends InstanceBase<ModuleConfig> {
|
||||||
config!: ModuleConfig // Setup in init()
|
config!: ModuleConfig // Setup in init()
|
||||||
@@ -25,6 +26,9 @@ export class TogglTrack extends InstanceBase<ModuleConfig> {
|
|||||||
|
|
||||||
intervalId?: NodeJS.Timeout
|
intervalId?: NodeJS.Timeout
|
||||||
currentTimerUpdaterIntervalId?: NodeJS.Timeout
|
currentTimerUpdaterIntervalId?: NodeJS.Timeout
|
||||||
|
wsReconnectTimer?: NodeJS.Timeout
|
||||||
|
|
||||||
|
ws?: WebSocket
|
||||||
|
|
||||||
constructor(internal: unknown) {
|
constructor(internal: unknown) {
|
||||||
super(internal)
|
super(internal)
|
||||||
@@ -40,6 +44,10 @@ export class TogglTrack extends InstanceBase<ModuleConfig> {
|
|||||||
this.stopTimeEntryPoller()
|
this.stopTimeEntryPoller()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (this.config.startWebSocket) {
|
||||||
|
this.stopWebSocket()
|
||||||
|
}
|
||||||
|
|
||||||
clearInterval(this.currentTimerUpdaterIntervalId)
|
clearInterval(this.currentTimerUpdaterIntervalId)
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -66,6 +74,9 @@ export class TogglTrack extends InstanceBase<ModuleConfig> {
|
|||||||
if (this.config.startTimerPoller) {
|
if (this.config.startTimerPoller) {
|
||||||
this.startTimeEntryPoller()
|
this.startTimeEntryPoller()
|
||||||
}
|
}
|
||||||
|
if (this.config.startWebSocket) {
|
||||||
|
this.startWebSocket()
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
async configUpdated(config: ModuleConfig): Promise<void> {
|
async configUpdated(config: ModuleConfig): Promise<void> {
|
||||||
@@ -74,6 +85,7 @@ export class TogglTrack extends InstanceBase<ModuleConfig> {
|
|||||||
const apiTokenChanged: boolean = this.config.apiToken != config.apiToken
|
const apiTokenChanged: boolean = this.config.apiToken != config.apiToken
|
||||||
const workSpaceDefaultChanged: boolean = this.config.workspaceName != config.workspaceName
|
const workSpaceDefaultChanged: boolean = this.config.workspaceName != config.workspaceName
|
||||||
const timeEntryPollerChanged: boolean = this.config.startTimerPoller != config.startTimerPoller
|
const timeEntryPollerChanged: boolean = this.config.startTimerPoller != config.startTimerPoller
|
||||||
|
const webSocketChanged: boolean = this.config.startWebSocket != config.startWebSocket
|
||||||
|
|
||||||
this.config = config
|
this.config = config
|
||||||
|
|
||||||
@@ -94,6 +106,13 @@ export class TogglTrack extends InstanceBase<ModuleConfig> {
|
|||||||
this.stopTimeEntryPoller()
|
this.stopTimeEntryPoller()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
if (webSocketChanged) {
|
||||||
|
if (this.config.startWebSocket) {
|
||||||
|
this.startWebSocket()
|
||||||
|
} else {
|
||||||
|
this.stopWebSocket()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
this.updateActions()
|
this.updateActions()
|
||||||
//this.updateVariables()
|
//this.updateVariables()
|
||||||
@@ -153,6 +172,79 @@ export class TogglTrack extends InstanceBase<ModuleConfig> {
|
|||||||
clearInterval(this.intervalId)
|
clearInterval(this.intervalId)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private startWebSocket(): void {
|
||||||
|
this.log('info', 'start websocket')
|
||||||
|
|
||||||
|
this.updateStatus(InstanceStatus.Connecting)
|
||||||
|
if (this.ws) {
|
||||||
|
this.ws.close(1000)
|
||||||
|
delete this.ws
|
||||||
|
}
|
||||||
|
|
||||||
|
this.ws = new WebSocket('wss://doh.krombel.de/toggl-websocket')
|
||||||
|
this.ws.addEventListener('open', () => {
|
||||||
|
this.updateStatus(InstanceStatus.Ok)
|
||||||
|
this.ws?.send(this.config.websocketSecret)
|
||||||
|
})
|
||||||
|
this.ws.addEventListener('close', (code) => {
|
||||||
|
this.log('debug', `Connection closed with code ${code.code}`)
|
||||||
|
this.updateStatus(InstanceStatus.Disconnected, `WS-Connection closed with code ${code.code}`)
|
||||||
|
this.maybeReconnectWebSocket()
|
||||||
|
})
|
||||||
|
this.ws.addEventListener('message', this.websocketMessageHandler.bind(this))
|
||||||
|
this.ws.addEventListener('error', (data) => {
|
||||||
|
this.log('error', `WebSocket error: ${data.error}`)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
private stopWebSocket(): void {
|
||||||
|
this.log('info', 'stop websocket')
|
||||||
|
|
||||||
|
if (this.wsReconnectTimer) {
|
||||||
|
clearTimeout(this.wsReconnectTimer)
|
||||||
|
this.wsReconnectTimer = undefined
|
||||||
|
}
|
||||||
|
if (this.ws) {
|
||||||
|
this.ws.close(1000)
|
||||||
|
delete this.ws
|
||||||
|
}
|
||||||
|
}
|
||||||
|
private maybeReconnectWebSocket() {
|
||||||
|
if (this.wsReconnectTimer) {
|
||||||
|
clearTimeout(this.wsReconnectTimer)
|
||||||
|
}
|
||||||
|
this.wsReconnectTimer = setTimeout(() => {
|
||||||
|
this.startWebSocket()
|
||||||
|
}, 5000)
|
||||||
|
}
|
||||||
|
|
||||||
|
private websocketMessageHandler(data: WebSocket.MessageEvent) {
|
||||||
|
this.log('info', 'Websocket message received')
|
||||||
|
|
||||||
|
if (typeof data.data === 'string') {
|
||||||
|
this.log('debug', `data: ${data.data}`)
|
||||||
|
const event = JSON.parse(data.data)
|
||||||
|
if (isITimeEntryWebhookPayload(event)) {
|
||||||
|
const entry = event.payload
|
||||||
|
if (entry.stop === '' && event.metadata.action != 'deleted') {
|
||||||
|
this.log('info', `update time entry to ${entry.description}`)
|
||||||
|
this.setCurrentlyRunningTimeEntry(entry)
|
||||||
|
} else if (entry.id == this.currentEntry?.id) {
|
||||||
|
// only unset value if update is for current entry
|
||||||
|
this.log('info', 'stop time entry')
|
||||||
|
this.setCurrentlyRunningTimeEntry(undefined)
|
||||||
|
} else {
|
||||||
|
this.log('warn', `unhandled case for time entry ${JSON.stringify(entry)}`)
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
this.log('warn', `unhandled event ${data.data}`)
|
||||||
|
}
|
||||||
|
|
||||||
|
return
|
||||||
|
}
|
||||||
|
this.log('warn', `unhandled websocket event '${JSON.stringify(data)}'`)
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Set variables to this time entry
|
* Set variables to this time entry
|
||||||
* @param entry running entry or undefined
|
* @param entry running entry or undefined
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
import { Toggl } from 'toggl-track'
|
import { ITimeEntry, Toggl } from 'toggl-track'
|
||||||
|
|
||||||
export interface IWorkspace {
|
export interface IWorkspace {
|
||||||
id: number
|
id: number
|
||||||
@@ -9,3 +9,55 @@ export async function togglGetWorkspaces(toggl: Toggl): Promise<IWorkspace[]> {
|
|||||||
const resp: IWorkspace[] = await toggl.request<IWorkspace[]>('workspaces', {})
|
const resp: IWorkspace[] = await toggl.request<IWorkspace[]>('workspaces', {})
|
||||||
return resp
|
return resp
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export interface IWebhookMetadata {
|
||||||
|
action: 'created' | 'updated' | 'deleted'
|
||||||
|
event_user_id: number
|
||||||
|
entity_id: number
|
||||||
|
model: 'time_entry'
|
||||||
|
workspace_id: number
|
||||||
|
project_id: number
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface ITimeEntryWebhookPayload {
|
||||||
|
event_id: number
|
||||||
|
created_at: string
|
||||||
|
creator_id: number
|
||||||
|
metadata: IWebhookMetadata
|
||||||
|
payload: ITimeEntry
|
||||||
|
subscription_id: number
|
||||||
|
timestamp: string
|
||||||
|
url_callback: string
|
||||||
|
}
|
||||||
|
|
||||||
|
export function isITimeEntryWebhookPayload(event: unknown): event is ITimeEntryWebhookPayload {
|
||||||
|
return (
|
||||||
|
typeof event === 'object' &&
|
||||||
|
event != null &&
|
||||||
|
'metadata' in event &&
|
||||||
|
typeof event.metadata === 'object' &&
|
||||||
|
event.metadata !== null &&
|
||||||
|
'model' in event.metadata &&
|
||||||
|
typeof event.metadata.model === 'string' &&
|
||||||
|
event.metadata.model === 'time_entry' &&
|
||||||
|
'payload' in event &&
|
||||||
|
typeof event.payload === 'object' &&
|
||||||
|
event.payload !== null &&
|
||||||
|
isITimeEntry(event.payload)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
export function isITimeEntry(entry: unknown): entry is ITimeEntry {
|
||||||
|
return (
|
||||||
|
typeof entry === 'object' &&
|
||||||
|
entry != null &&
|
||||||
|
'id' in entry &&
|
||||||
|
typeof entry.id === 'number' &&
|
||||||
|
'description' in entry &&
|
||||||
|
typeof entry.description === 'string' &&
|
||||||
|
'project_id' in entry &&
|
||||||
|
typeof entry.project_id === 'number' &&
|
||||||
|
'start' in entry &&
|
||||||
|
typeof entry.start === 'string'
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|||||||
16
yarn.lock
16
yarn.lock
@@ -3035,6 +3035,7 @@ __metadata:
|
|||||||
toggl-track: "npm:^0.8.0"
|
toggl-track: "npm:^0.8.0"
|
||||||
typescript: "npm:~5.8.3"
|
typescript: "npm:~5.8.3"
|
||||||
typescript-eslint: "npm:^8.30.1"
|
typescript-eslint: "npm:^8.30.1"
|
||||||
|
ws: "npm:^8.18.2"
|
||||||
languageName: unknown
|
languageName: unknown
|
||||||
linkType: soft
|
linkType: soft
|
||||||
|
|
||||||
@@ -3307,6 +3308,21 @@ __metadata:
|
|||||||
languageName: node
|
languageName: node
|
||||||
linkType: hard
|
linkType: hard
|
||||||
|
|
||||||
|
"ws@npm:^8.18.2":
|
||||||
|
version: 8.18.3
|
||||||
|
resolution: "ws@npm:8.18.3"
|
||||||
|
peerDependencies:
|
||||||
|
bufferutil: ^4.0.1
|
||||||
|
utf-8-validate: ">=5.0.2"
|
||||||
|
peerDependenciesMeta:
|
||||||
|
bufferutil:
|
||||||
|
optional: true
|
||||||
|
utf-8-validate:
|
||||||
|
optional: true
|
||||||
|
checksum: 10c0/eac918213de265ef7cb3d4ca348b891a51a520d839aa51cdb8ca93d4fa7ff9f6ccb339ccee89e4075324097f0a55157c89fa3f7147bde9d8d7e90335dc087b53
|
||||||
|
languageName: node
|
||||||
|
linkType: hard
|
||||||
|
|
||||||
"yallist@npm:^5.0.0":
|
"yallist@npm:^5.0.0":
|
||||||
version: 5.0.0
|
version: 5.0.0
|
||||||
resolution: "yallist@npm:5.0.0"
|
resolution: "yallist@npm:5.0.0"
|
||||||
|
|||||||
Reference in New Issue
Block a user