Compare commits
2 Commits
main
...
b0e40e66a7
| Author | SHA1 | Date | |
|---|---|---|---|
| b0e40e66a7 | |||
| 0ab9fd495f |
1
.husky/pre-commit
Normal file
1
.husky/pre-commit
Normal file
@@ -0,0 +1 @@
|
|||||||
|
lint-staged
|
||||||
29
package.json
29
package.json
@@ -1,9 +1,10 @@
|
|||||||
{
|
{
|
||||||
"name": "toggl-track",
|
"name": "toggl-track",
|
||||||
"version": "2.1.0",
|
"version": "2.1.1",
|
||||||
"main": "dist/main.js",
|
"main": "dist/main.js",
|
||||||
"type": "module",
|
"type": "module",
|
||||||
"scripts": {
|
"scripts": {
|
||||||
|
"postinstall": "husky",
|
||||||
"format": "prettier -w .",
|
"format": "prettier -w .",
|
||||||
"package": "yarn run build && companion-module-build",
|
"package": "yarn run build && companion-module-build",
|
||||||
"build": "rimraf dist && yarn run build:main",
|
"build": "rimraf dist && yarn run build:main",
|
||||||
@@ -17,19 +18,27 @@
|
|||||||
"type": "git",
|
"type": "git",
|
||||||
"url": "git+https://github.com/bitfocus/companion-module-toggl-track.git"
|
"url": "git+https://github.com/bitfocus/companion-module-toggl-track.git"
|
||||||
},
|
},
|
||||||
|
"engines": {
|
||||||
|
"node": "^22.14",
|
||||||
|
"yarn": "^4"
|
||||||
|
},
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@companion-module/base": "~1.11",
|
"@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.1.1",
|
"@companion-module/tools": "~2.3.0",
|
||||||
"@types/node": "^22.10.2",
|
"@types/node": "^22.14.1",
|
||||||
"eslint": "^9.17.0",
|
"@types/ws": "^8",
|
||||||
"prettier": "^3.4.2",
|
"eslint": "^9.24.0",
|
||||||
"rimraf": "^5.0.10",
|
"husky": "^9.1.7",
|
||||||
"typescript": "~5.5.4",
|
"lint-staged": "^15.5.1",
|
||||||
"typescript-eslint": "^8.18.1"
|
"prettier": "^3.5.3",
|
||||||
|
"rimraf": "^6.0.1",
|
||||||
|
"typescript": "~5.8.3",
|
||||||
|
"typescript-eslint": "^8.30.1"
|
||||||
},
|
},
|
||||||
"prettier": "@companion-module/tools/.prettierrc.json",
|
"prettier": "@companion-module/tools/.prettierrc.json",
|
||||||
"lint-staged": {
|
"lint-staged": {
|
||||||
|
|||||||
@@ -5,6 +5,9 @@ export interface ModuleConfig {
|
|||||||
workspaceName: string
|
workspaceName: string
|
||||||
alwaysStart: boolean
|
alwaysStart: boolean
|
||||||
startTimerPoller: boolean
|
startTimerPoller: boolean
|
||||||
|
timerPollerInterval: number
|
||||||
|
startWebSocket: boolean
|
||||||
|
websocketSecret: string
|
||||||
}
|
}
|
||||||
|
|
||||||
export function GetConfigFields(): SomeCompanionConfigField[] {
|
export function GetConfigFields(): SomeCompanionConfigField[] {
|
||||||
@@ -36,9 +39,32 @@ export function GetConfigFields(): SomeCompanionConfigField[] {
|
|||||||
{
|
{
|
||||||
type: 'checkbox',
|
type: 'checkbox',
|
||||||
id: 'startTimerPoller',
|
id: 'startTimerPoller',
|
||||||
label: 'Poll for current time entry every 30 seconds',
|
label: 'Poll for current time entry every n seconds',
|
||||||
width: 12,
|
width: 12,
|
||||||
default: false,
|
default: false,
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
type: 'number',
|
||||||
|
id: 'timerPollerInterval',
|
||||||
|
label: 'Poll interval in seconds',
|
||||||
|
width: 12,
|
||||||
|
default: 60,
|
||||||
|
min: 30,
|
||||||
|
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',
|
||||||
|
},
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
|
|||||||
96
src/main.ts
96
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()
|
||||||
@@ -146,13 +165,86 @@ export class TogglTrack extends InstanceBase<ModuleConfig> {
|
|||||||
void (async () => {
|
void (async () => {
|
||||||
await this.getCurrentTimer()
|
await this.getCurrentTimer()
|
||||||
})()
|
})()
|
||||||
}, 30 * 1000)
|
}, this.config.timerPollerInterval * 1000)
|
||||||
}
|
}
|
||||||
private stopTimeEntryPoller(): void {
|
private stopTimeEntryPoller(): void {
|
||||||
this.log('info', 'Stopping TimeEntry-Poller')
|
this.log('info', 'Stopping TimeEntry-Poller')
|
||||||
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'
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user