Files
companion-module-toggl-track/src/main.ts

410 lines
11 KiB
TypeScript

// toggltrack module
// Peter Daniel, Matthias Kesler
import { InstanceBase, runEntrypoint, InstanceStatus, SomeCompanionConfigField } from '@companion-module/base'
import { GetConfigFields, type ModuleConfig } from './config.js'
import UpdateActions from './actions.js'
import UpdatePresets from './presets.js'
import UpdateVariableDefinitions from './variables.js'
import UpgradeScripts from './upgrades.js'
import { UpdateFeedbacks } from './feedbacks.js'
import { Toggl, ITimeEntry, IWorkspaceProject, IClient } from 'toggl-track'
import { togglGetWorkspaces } from './toggl-extend.js'
import { timecodeSince } from './utils.js'
export class TogglTrack extends InstanceBase<ModuleConfig> {
config!: ModuleConfig // Setup in init()
toggl?: Toggl
workspaceId?: number // current active workspace id
workspaceName: string = '' // name of workspace
projects?: { id: number; label: string; clientID?: number }[]
clients?: { id: number; label: string }[]
currentEntry?: ITimeEntry
intervalId?: NodeJS.Timeout
currentTimerUpdaterIntervalId?: NodeJS.Timeout
constructor(internal: unknown) {
super(internal)
}
getConfigFields(): SomeCompanionConfigField[] {
return GetConfigFields()
}
async destroy(): Promise<void> {
this.log('info', 'destroy ' + this.id)
if (this.config.startTimerPoller) {
this.stopTimeEntryPoller()
}
clearInterval(this.currentTimerUpdaterIntervalId)
}
async init(config: ModuleConfig): Promise<void> {
this.log('info', '--- init toggltrack ' + this.id + ' ---')
this.config = config
this.updateVariableDefinitions()
this.updatePresets()
await this.initToggleConnection()
await this.loadStaticData()
this.updateActions()
this.updateFeedbacks()
if (this.toggl && this.workspaceId) {
this.updateStatus(InstanceStatus.Ok)
}
await this.getCurrentTimer()
if (this.config.startTimerPoller) {
this.startTimeEntryPoller()
}
}
async configUpdated(config: ModuleConfig): Promise<void> {
this.log('debug', 'config updated')
const apiTokenChanged: boolean = this.config.apiToken != config.apiToken
const workSpaceDefaultChanged: boolean = this.config.workspaceName != config.workspaceName
const timeEntryPollerChanged: boolean = this.config.startTimerPoller != config.startTimerPoller
this.config = config
if (apiTokenChanged) {
this.log('debug', 'api token changed. init new toggle connection')
this.toggl = undefined
await this.initToggleConnection()
await this.loadStaticData()
} else if (workSpaceDefaultChanged) {
this.log('debug', 'workspace default changed. reload workspaces')
await this.loadStaticData()
}
if (timeEntryPollerChanged) {
if (this.config.startTimerPoller) {
this.startTimeEntryPoller()
} else {
this.stopTimeEntryPoller()
}
}
this.updateActions()
//this.updateVariables()
if (this.toggl && this.workspaceId) {
this.updateStatus(InstanceStatus.Ok)
}
}
updateVariables(): void {
this.log('error', 'updateVariables not implemented')
//throw new Error('Method not implemented.')
}
updateActions(): void {
UpdateActions(this)
}
updateFeedbacks(): void {
UpdateFeedbacks(this)
}
updatePresets(): void {
UpdatePresets(this)
}
updateVariableDefinitions(): void {
UpdateVariableDefinitions(this)
}
async initToggleConnection(): Promise<void> {
if (this.config.apiToken && this.config.apiToken.length > 0) {
this.toggl = new Toggl({
auth: {
token: this.config.apiToken,
},
})
const resp = await this.toggl.me.logged()
if (resp !== '') {
this.log('warn', 'error during token check: ' + resp)
this.toggl = undefined
this.updateStatus(InstanceStatus.AuthenticationFailure, resp)
return
}
}
}
private startTimeEntryPoller(): void {
this.log('info', 'Starting TimeEntry-Poller')
// fetch current timer every 30 seconds
this.intervalId = setInterval(() => {
// this harms the linter (handle unawaited promise in an non-async context)
void (async () => {
await this.getCurrentTimer()
})()
}, this.config.timerPollerInterval * 1000)
}
private stopTimeEntryPoller(): void {
this.log('info', 'Stopping TimeEntry-Poller')
clearInterval(this.intervalId)
}
/**
* Set variables to this time entry
* @param entry running entry or undefined
*/
private setCurrentlyRunningTimeEntry(entry: ITimeEntry | undefined): void {
this.currentEntry = entry
if (entry) {
const project = this.projects?.find((p) => p.id == entry.project_id)
this.setVariableValues({
timerId: entry.id,
timerDescription: entry.description,
timerDuration: timecodeSince(new Date(entry.start)),
timerProject: project?.label,
timerProjectID: entry.project_id,
timerClient: this.clients?.find((c) => c.id == project?.clientID)?.label ?? 'None',
timerClientID: project?.clientID,
})
// in case there is on update thread running clear it
clearInterval(this.currentTimerUpdaterIntervalId)
// Update timerDuration once per second
this.currentTimerUpdaterIntervalId = setInterval(() => {
// this harms the linter (handle unawaited promise in an non-async context)
void (async () => {
this.setVariableValues({
timerDuration: timecodeSince(new Date(entry.start)),
})
})()
}, 1000) // update every second
} else {
clearInterval(this.currentTimerUpdaterIntervalId)
this.setVariableValues({
timerId: undefined,
timerDescription: undefined,
timerDuration: undefined,
timerProject: undefined,
timerProjectID: undefined,
timerClient: undefined,
timerClientID: undefined,
})
}
this.checkFeedbacks('ProjectRunningState', 'ClientRunningState')
}
async getCurrentTimer(): Promise<number | null> {
this.log('debug', 'function: getCurrentTimer')
if (!this.toggl) {
this.log('warn', 'Not authorized')
return null
}
const entry: ITimeEntry = await this.toggl.timeEntry.current()
this.log('debug', 'response for timer id ' + JSON.stringify(entry))
if (entry) {
this.log('info', 'Current timer id: ' + entry.id)
this.setCurrentlyRunningTimeEntry(entry)
return entry.id
} else {
this.log('info', 'No current timer')
this.setCurrentlyRunningTimeEntry(undefined)
return null
}
}
async loadStaticData(): Promise<void> {
if (!this.toggl) {
this.log('warn', 'loadStaticData: toggle connection not set up')
return
}
await this.getWorkspace()
await this.getProjects()
await this.getClients()
}
private async getWorkspace(): Promise<void> {
this.log('debug', 'function: getWorkspace')
if (!this.toggl) {
this.log('warn', 'Not authorized')
return
}
// reset
this.workspaceId = undefined
this.setVariableValues({
workspace: undefined,
})
const workspaces = await togglGetWorkspaces(this.toggl)
this.log('info', 'Found ' + workspaces.length + ' workspace')
for (const ws of workspaces) {
if (this.config.workspaceName == '' || this.config.workspaceName == ws.name) {
// take the first or matching one and continue
this.workspaceId = ws.id
this.workspaceName = ws.name
break
}
// workspaceName does not match => continue with next
continue
}
if (this.workspaceId == undefined) {
// no workspace found
this.log('debug', 'workspace not found. Response: ' + JSON.stringify(workspaces))
this.updateStatus(InstanceStatus.BadConfig, 'Available Workspaces: ' + workspaces.map((ws) => ws.name).join(','))
return
}
this.log('info', 'Workspace: ' + this.workspaceId + ' - ' + this.workspaceName)
this.setVariableValues({
workspace: this.workspaceName,
})
await this.getProjects()
await this.getClients()
}
async getProjects(): Promise<void> {
this.log('debug', 'function: getProjects ' + this.workspaceId)
if (!this.workspaceId) {
this.log('warn', 'workspaceId undefined')
return
}
const projects: IWorkspaceProject[] = await this.toggl!.projects.list(this.workspaceId)
if (typeof projects === 'string' || projects.length == 0) {
this.log('debug', 'No projects found')
this.projects = undefined
this.log('debug', 'projects response' + JSON.stringify(projects))
return
}
this.projects = projects
.filter((p) => p.active)
.map((p) => {
return {
id: p.id,
label: p.name,
clientID: p.client_id,
}
})
.sort((a, b) => {
const fa = a.label.toLowerCase()
const fb = b.label.toLowerCase()
if (fa < fb) {
return -1
}
if (fa > fb) {
return 1
}
return 0
})
this.log('debug', 'Projects: ' + JSON.stringify(this.projects))
}
private async getClients(): Promise<void> {
this.log('debug', 'function: getClients ' + this.workspaceId)
if (!this.workspaceId) {
this.log('warn', 'workspaceId undefined')
return
}
const clients: IClient[] = await this.toggl!.me.clients()
if (typeof clients === 'string' || clients.length == 0) {
this.log('debug', 'No clients found')
this.clients = undefined
this.log('debug', 'clients response' + JSON.stringify(clients))
return
}
this.clients = clients
.filter((c) => c.wid == this.workspaceId)
.map((c) => {
return {
id: c.id,
label: c.name,
}
})
.sort((a, b) => {
const fa = a.label.toLowerCase()
const fb = b.label.toLowerCase()
if (fa < fb) {
return -1
}
if (fa > fb) {
return 1
}
return 0
})
this.log('debug', 'Clients: ' + JSON.stringify(this.clients))
}
async startTimer(project: number, description: string): Promise<void> {
if (!this.toggl || !this.workspaceId) {
this.log('error', 'toggle not initialized. Do not start time')
return
}
const currentId = await this.getCurrentTimer()
let newEntry: ITimeEntry
if (currentId === null || this.config.alwaysStart === true) {
// there is no running time entry or alwaysStart is true
newEntry = await this.toggl.timeEntry.create(this.workspaceId, {
description: description,
workspace_id: this.workspaceId,
created_with: 'companion',
start: new Date().toISOString(),
duration: -1,
project_id: project != 0 ? project : undefined,
})
this.log('info', 'New timer started ' + newEntry.id + ' ' + newEntry.description)
this.setCurrentlyRunningTimeEntry(newEntry)
} else {
this.log('info', 'A timer is already running ' + currentId + ' not starting a new one!')
}
}
async stopTimer(): Promise<void> {
this.log('debug', 'function: stopTimer')
if (!this.toggl || !this.workspaceId) {
this.log('error', 'toggle not initialized. Do not start time')
return
}
const currentId = await this.getCurrentTimer()
this.log('info', 'Trying to stop current timer id: ' + currentId)
if (currentId !== null) {
const updated: ITimeEntry = await this.toggl.timeEntry.stop(currentId, this.workspaceId)
this.log('info', 'Stopped ' + updated.id + ', duration ' + updated.duration)
this.setCurrentlyRunningTimeEntry(undefined)
this.setVariableValues({
lastTimerDuration: updated.duration,
})
} else {
this.log('warn', 'No running timer to stop or running timer id unknown')
}
}
}
runEntrypoint(TogglTrack, UpgradeScripts)