@deskthing/cli
Version:
An emulator for the DeskThing Server
597 lines (562 loc) • 20.9 kB
text/typescript
import { ServerService } from "../services/serverService";
import { ServerMessageBus } from "./serverMessageBus";
import { Logger } from "../services/logger";
import {
AppSettings,
Client,
LOGGING_LEVELS,
APP_REQUESTS,
DESKTHING_EVENTS,
SongEvent,
AppToDeskThingData,
AUDIO_REQUESTS,
MusicEventPayloads,
DeskThingToAppCore,
ConnectionState,
ClientConnectionMethod,
} from "@deskthing/types";
import { DeskThingConfig } from "../../config/deskthing.config";
import { exec } from "child_process";
import { MusicService } from "../services/musicService"
import { SettingService } from "../services/settingService";
type HandlerFunction<
T extends APP_REQUESTS | SongEvent | "default",
R extends Extract<AppToDeskThingData, { type: T }>["request"] | string = string
> = (
app: string,
appData: Extract<AppToDeskThingData | MusicEventPayloads, { type: T; request?: R }>
) => void;
type TypeHandler = {
[key in APP_REQUESTS | SongEvent | "default"]: RequestHandler<key>;
};
type RequestHandler<T extends APP_REQUESTS | SongEvent | "default"> = {
[key in Extract<AppToDeskThingData | MusicEventPayloads, { type: T }>["request"] | "default"]?: (
app: string,
data: Extract<AppToDeskThingData | MusicEventPayloads, { type: T; request?: key }>
) => void;
};
const serverService = new ServerService();
let Data: {
data: { [key: string]: any };
} = {
data: {},
};
export const getServerData = () => Data;
export const handleDataFromApp = async (
app: string,
appData: any
): Promise<void> => {
if (Object.values(APP_REQUESTS).includes(appData.type as APP_REQUESTS)) {
try {
const handler =
handleData[appData.type as APP_REQUESTS] || handleData["default"];
const requestHandler =
handler[appData.request || "default"] || handler["default"];
if (!requestHandler) {
Logger.warn(
`No handler found for request ${appData.request} in ${appData.type}`
);
handleRequestMissing(app, appData);
return;
}
requestHandler(app, appData as any);
} catch (error) {
Logger.error("Error in handleDataFromApp:", error);
}
} else {
Logger.error(
"Unknown event type:",
appData.type,
" with request ",
appData.request
);
}
};
/**
* Logs a warning message when an app sends an unknown data type or request.
*
* @param {string} app - The name of the app that sent the unknown data.
* @param {FromAppData} appData - The data received from the app.
*/
const handleRequestMissing: HandlerFunction<any, any> = (app: string, appData: any) => {
Logger.warn(
`[handleComs]: App ${app} sent unknown data type: ${appData.type
} and request: ${appData.request}, with payload ${appData.payload
? JSON.stringify(appData.payload).length > 1000
? "[Large Payload]"
: JSON.stringify(appData.payload)
: "undefined"
}`,
app
);
};
const handleRequestSetSettings: HandlerFunction<
APP_REQUESTS.SET,
"settings"
> = async (app, appData) => {
Logger.info("Simulating adding settings");
Logger.debug("Settings being added: ", appData.payload);
const appSettings = appData.payload as AppSettings;
await SettingService.setSettings(appSettings);
};
const handleRequestInitSettings: HandlerFunction<
APP_REQUESTS.SET,
"settings-init"
> = async (app, appData) => {
Logger.info("Simulating initializing settings");
Logger.debug("Settings being initialized: ", appData.payload);
const appSettings = appData.payload as AppSettings;
await SettingService.initSettings(appSettings);
};
const handleRequestSetData: HandlerFunction<APP_REQUESTS.SET, "data"> = async (
app,
appData
) => {
Logger.info("Simulating adding data");
Logger.debug("Data being added: ", Data.data);
Data.data = { ...Data.data, ...appData.payload };
};
/**
* Handles a request to set data for an app.
*
* @param {string} app - The name of the app requesting the data set.
* @param {any} appData - The payload data to be set.
* @returns {Promise<void>} - A Promise that resolves when the data has been set.
*/
const handleRequestSetAppData: HandlerFunction<
APP_REQUESTS.SET,
"appData"
> = async (app: string, appData): Promise<void> => {
if (!appData.payload) return;
const { settings, ...data } = appData.payload;
SettingService.updateSettings(settings)
Data = {
data: { ...Data.data, ...data }
};
};
/**
* Handles a request to open an authentication window.
*
* @param {any} appData - The payload data containing information for the authentication window.
* @returns {Promise<void>} - A Promise that resolves when the authentication window has been opened.
*/
const handleRequestOpen: HandlerFunction<APP_REQUESTS.OPEN> = async (
_app,
appData
) => {
Logger.debug(`[handleOpen]: Opening ${appData.payload}`);
const encodedUrl = encodeURI(appData.payload);
Logger.info(
`[openUrl]: If your browser doesn't automatically open, try manually clicking the url:\n\n${encodedUrl}\n\n`
);
try {
if (process.platform === "win32") {
// For Windows, use the shell option to avoid command line parsing issues
exec(`start "" "${encodedUrl}"`);
} else if (process.platform === "darwin") {
exec(`open '${encodedUrl}'`);
} else {
exec(`xdg-open '${encodedUrl}'`);
}
Logger.debug(`URL opening command executed successfully`);
} catch (error) {
Logger.error(`Error opening URL: ${error.message}`);
}
};
/**
* Handles a request to log data from an app.
*
* @param {string} app - The name of the app that sent the log request.
* @param {FromAppData} appData - The data received from the app, including the log type and payload.
* @returns {void}
*/
const handleRequestLog: HandlerFunction<APP_REQUESTS.LOG> = (
app,
appData
) => {
Logger.clientLog(appData.request as any, typeof appData.payload === "object" ? JSON.stringify(appData.payload) : appData.payload);
};
/**
* Handles a request to add a new key to the key map store.
*
* @param {string} app - The name of the app requesting the key addition.
* @param {any} appData - The payload data containing the key information to be added.
* @returns {Promise<void>} - A Promise that resolves when the key has been added.
*/
const handleRequestKeyAdd: HandlerFunction<APP_REQUESTS.KEY> = async (
app,
appData
): Promise<void> => {
Logger.warn("Key data isn't supported");
Logger.debug("Received", appData.payload);
};
/**
* Handles a request to remove a key from the key map store.
*
* @param {string} app - The name of the app requesting the key removal.
* @param {any} appData - The payload data containing the ID of the key to be removed.
* @returns {Promise<void>} - A Promise that resolves when the key has been removed.
*/
const handleRequestKeyRemove: HandlerFunction<APP_REQUESTS.KEY> = async (
app,
appData
): Promise<void> => {
Logger.warn("Key data isn't supported");
Logger.debug("Received", appData.payload);
};
/**
* Handles a request to trigger a key in the key map store.
*
* @param {string} app - The name of the app requesting the key trigger.
* @param {any} appData - The payload data containing the ID and mode of the key to be triggered.
* @returns {Promise<void>} - A Promise that resolves when the key has been triggered.
*/
const handleRequestKeyTrigger: HandlerFunction<APP_REQUESTS.KEY> = async (
app,
appData
): Promise<void> => {
Logger.warn("Key data isn't supported");
Logger.debug("Received", appData.payload);
};
/**
* Handles a request to run an action in the key map store.
*
* @param {string} app - The name of the app requesting the action run.
* @param {any} appData - The payload data containing the ID of the action to be run.
* @returns {Promise<void>} - A Promise that resolves when the action has been run.
*/
const handleRequestActionRun: HandlerFunction<APP_REQUESTS.ACTION> = async (
app,
appData
): Promise<void> => {
Logger.warn("Action data isn't supported");
Logger.debug("Received", appData.payload);
};
/**
* Handles a request to update the icon of an action in the key map store.
*
* @param {string} app - The name of the app requesting the action icon update.
* @param {any} appData - The payload data containing the ID of the action and the new icon.
* @returns {Promise<void>} - A Promise that resolves when the action icon has been updated.
*/
const handleRequestActionUpdate: HandlerFunction<APP_REQUESTS.ACTION> = async (
app,
appData
): Promise<void> => {
Logger.warn("Action data isn't supported");
Logger.debug("Received", appData.payload);
};
/**
* Handles a request to remove an action from the key map store.
*
* @param {string} app - The name of the app requesting the action removal.
* @param {any} appData - The payload data containing the ID of the action to be removed.
* @returns {Promise<void>} - A Promise that resolves when the action has been removed.
*/
const handleRequestActionRemove: HandlerFunction<APP_REQUESTS.ACTION> = async (
app,
appData
): Promise<void> => {
Logger.warn("Action data isn't supported");
Logger.debug("Received", appData.payload);
};
/**
* Handles a request to add a new action to the key map store.
*
* @param {string} app - The name of the app requesting the action addition.
* @param {any} appData - The payload data containing the details of the action to be added.
* @returns {Promise<void>} - A Promise that resolves when the action has been added.
*/
const handleRequestActionAdd: HandlerFunction<APP_REQUESTS.ACTION> = async (
app,
appData
): Promise<void> => {
Logger.warn("Action data isn't supported");
Logger.debug("Received", appData.payload);
};
/**
* Handles a request to retrieve data for a specific app.
*
* @param {string} app - The name of the app requesting the data.
* @returns {Promise<void>} - A Promise that resolves when the data has been sent to the app.
*/
const handleRequestGetData: HandlerFunction<APP_REQUESTS.GET, 'data'> = async (
app
): Promise<void> => {
Logger.info(`[handleAppData]: App is requesting data`);
Logger.debug(`[handleAppData]: Returning Data:`, Data.data);
ServerMessageBus.notify("app:data", { type: "data", payload: Data.data });
};
/**
* Handles a request to delete data for a specific app.
*
* @param {string} app - The name of the app requesting the settings.
* @returns {Promise<void>} - A Promise that resolves when the settings have been sent to the app.
*/
const handleRequestDelData: HandlerFunction<APP_REQUESTS.DELETE, 'data'> = async (
app,
appData
): Promise<void> => {
Logger.info(
`[handleAppData]: ${app} is deleting data: ${appData.payload.toString()}`
);
if (
!appData.payload ||
(typeof appData.payload !== "string" && !Array.isArray(appData.payload))
) {
Logger.info(
`[handleAppData]: Cannot delete data because ${appData.payload.toString()} is not a string or string[]`
);
return;
}
Data.data = Object.fromEntries(
Object.entries(Data.data).filter(([key]) => !appData.payload.includes(key))
);
};
const handleRequestGetConfig: HandlerFunction<APP_REQUESTS.GET, 'config'> = async (
app
): Promise<void> => {
ServerMessageBus.notify("app:data", { type: "config", payload: {} });
Logger.warn(
`[handleAppData]: ${app} tried accessing "Config" data type which is depreciated and no longer in use!`
);
};
/**
* Handles a request to retrieve the settings for a specific app.
*
* @param {string} app - The name of the app requesting the settings.
* @returns {Promise<void>} - A Promise that resolves when the settings have been sent to the app.
*/
const handleRequestGetSettings: HandlerFunction<APP_REQUESTS.GET, 'settings'> = async (
app
): Promise<void> => {
Logger.info(`[handleAppData]: App is requesting settings`);
const settings = SettingService.getSettings();
Logger.debug(`[handleAppData]: Returning Settings:`, settings);
ServerMessageBus.notify("app:data", {
type: "settings",
payload: settings,
});
};
/**
* Handles a request to delete settings for a specific app.
*
* @param {string} app - The name of the app requesting the settings.
* @returns {Promise<void>} - A Promise that resolves when the settings have been sent to the app.
*/
const handleRequestDelSettings: HandlerFunction<APP_REQUESTS.DELETE, 'settings'> = async (
app,
appData
): Promise<void> => {
Logger.info(
`[handleAppData]: ${app} is deleting settings: ${appData.payload.toString()}`
);
if (
!appData.payload ||
(typeof appData.payload !== "string" && !Array.isArray(appData.payload))
) {
Logger.warn(
`[handleAppData]: Cannot delete settings because ${appData.payload.toString()} is not a string or string[]`
);
return;
}
SettingService.delSettings(Array.isArray(appData.payload) ? appData.payload : [appData.payload]);
};
/**
* Handles a request to retrieve input data for a specific app.
*
* This function sends an IPC message to the renderer process to display a form and request user data. Once the user data is received, it is sent back to the app via a message.
*
* @param {string} app - The name of the app requesting the input data.
* @param {object} appData - Additional data associated with the request.
* @returns {Promise<void>} - A Promise that resolves when the input data has been sent to the app.
*/
const handleRequestGetInput: HandlerFunction<APP_REQUESTS.GET, 'input'> = async (
app,
appData
) => {
const templateData = Object.keys(appData.payload).reduce((acc, key) => {
acc[key] = "arbData";
return acc;
}, {});
Logger.info(`[handleAppData]: App is requesting input`);
Logger.debug(`[handleAppData]: Returning Input:`, templateData);
ServerMessageBus.notify("app:data", { type: "input", payload: templateData });
};
const handleConnectionsRequest: HandlerFunction<APP_REQUESTS.GET, 'connections'> = async (
app
) => {
Logger.info(`[handleAppData]: App is requesting connections`);
const sampleClient: Client = {
clientId: "sample-id",
connected: false,
meta: {},
identifiers: {
adb: {
id: "sample-provider",
capabilities: [],
method: ClientConnectionMethod.Unknown,
providerId: 'sample-provider',
connectionState: ConnectionState.Established,
active: false,
}
},
connectionState: ConnectionState.Disconnected,
timestamp: Date.now(),
currentApp: app,
};
Logger.debug(`[handleAppData]: Returning Connections:`, sampleClient);
ServerMessageBus.notify("app:data", {
type: DESKTHING_EVENTS.CLIENT_STATUS,
request: "connections",
payload: [sampleClient],
} as DeskThingToAppCore);
};
const handleGet: RequestHandler<APP_REQUESTS.GET> = {
data: handleRequestGetData,
config: handleRequestGetConfig,
settings: handleRequestGetSettings,
input: handleRequestGetInput,
connections: handleConnectionsRequest,
};
const handleSet: RequestHandler<APP_REQUESTS.SET> = {
settings: handleRequestSetSettings,
data: handleRequestSetData,
appData: handleRequestSetAppData,
default: handleRequestMissing,
'settings-init': handleRequestInitSettings,
};
const handleDelete: RequestHandler<APP_REQUESTS.DELETE> = {
settings: handleRequestDelSettings,
data: handleRequestDelData,
};
const handleOpen: RequestHandler<APP_REQUESTS.OPEN> = {
default: handleRequestOpen,
};
const handleSendToClient: RequestHandler<APP_REQUESTS.SEND> = {
default: async (app, appData): Promise<void> => {
serverService.sendToClient({
app: appData.payload.app || app,
type: appData.payload.type || "",
payload: appData.payload.payload ?? "",
request: appData.payload.request || "",
clientId: appData.payload.clientId || "",
});
},
};
const handleSendToApp: RequestHandler<APP_REQUESTS.TOAPP> = {
default: async (app, appData): Promise<void> => {
Logger.info("Sent data ", appData.payload, " to other app");
},
};
const handleRequestSongGet: RequestHandler<SongEvent.GET> = {
song: async (app, appData): Promise<void> => {
Logger.info("Sent data ", appData.payload, " to other app");
},
refresh: async (app, appData): Promise<void> => {
Logger.info("Refreshed data ", appData.payload, " to other app");
},
};
const handleRequestSongSet: RequestHandler<SongEvent.SET> = {
[AUDIO_REQUESTS.FAST_FORWARD]: async (app, appData): Promise<void> => {
Logger.info("Unable to fast forward (no audio app in emulator)");
},
[AUDIO_REQUESTS.REWIND]: async (app, appData): Promise<void> => {
Logger.info("Unable to rewind (no audio app in emulator)");
},
[AUDIO_REQUESTS.PLAY]: async (app, appData): Promise<void> => {
Logger.info("Unable to play (no audio app in emulator)");
},
[AUDIO_REQUESTS.PAUSE]: async (app, appData): Promise<void> => {
Logger.info("Unable to pause (no audio app in emulator)");
},
[AUDIO_REQUESTS.STOP]: async (app, appData): Promise<void> => {
Logger.info("Unable to stop (no audio app in emulator)");
},
[AUDIO_REQUESTS.NEXT]: async (app, appData): Promise<void> => {
Logger.info("Unable to next (no audio app in emulator)");
},
[AUDIO_REQUESTS.PREVIOUS]: async (app, appData): Promise<void> => {
Logger.info("Unable to previous (no audio app in emulator)");
},
[AUDIO_REQUESTS.SEEK]: async (app, appData): Promise<void> => {
Logger.info("Unable to seek (no audio app in emulator)");
},
[AUDIO_REQUESTS.LIKE]: async (app, appData): Promise<void> => {
Logger.info("Unable to like (no audio app in emulator)");
},
[AUDIO_REQUESTS.VOLUME]: async (app, appData): Promise<void> => {
Logger.info("Unable to change volume (no audio app in emulator)");
},
[AUDIO_REQUESTS.REPEAT]: async (app, appData): Promise<void> => {
Logger.info("Unable to change repeat (no audio app in emulator)");
},
[AUDIO_REQUESTS.SHUFFLE]: async (app, appData): Promise<void> => {
Logger.info("Unable to change shuffle (no audio app in emulator)");
}
};
const handleLog: RequestHandler<APP_REQUESTS.LOG> = {
[LOGGING_LEVELS.LOG]: handleRequestLog,
[LOGGING_LEVELS.DEBUG]: handleRequestLog,
[LOGGING_LEVELS.ERROR]: handleRequestLog,
[LOGGING_LEVELS.FATAL]: handleRequestLog,
[LOGGING_LEVELS.WARN]: handleRequestLog,
[LOGGING_LEVELS.MESSAGE]: handleRequestLog,
default: handleRequestMissing,
};
const handleKey: RequestHandler<APP_REQUESTS.KEY> = {
add: handleRequestKeyAdd,
remove: handleRequestKeyRemove,
trigger: handleRequestKeyTrigger,
default: handleRequestMissing,
};
const handleAction: RequestHandler<APP_REQUESTS.ACTION> = {
add: handleRequestActionAdd,
remove: handleRequestActionRemove,
update: handleRequestActionUpdate,
run: handleRequestActionRun,
default: handleRequestMissing,
};
const handleDefault: RequestHandler<APP_REQUESTS.DEFAULT> = {
default: handleRequestMissing,
};
const handleSong: RequestHandler<APP_REQUESTS.SONG> = {
'default': (data, payload) => new MusicService().setSong(payload.payload),
'data': (data, payload) => new MusicService().setSong(payload.payload)
}
const handleData: TypeHandler = {
[APP_REQUESTS.GET]: handleGet,
[APP_REQUESTS.SET]: handleSet,
[APP_REQUESTS.DELETE]: handleDelete,
[APP_REQUESTS.OPEN]: handleOpen,
[APP_REQUESTS.SEND]: handleSendToClient,
[APP_REQUESTS.TOAPP]: handleSendToApp,
[APP_REQUESTS.LOG]: handleLog,
[APP_REQUESTS.KEY]: handleKey,
[APP_REQUESTS.ACTION]: handleAction,
default: handleDefault,
step: { default: () => { } },
task: { default: () => { } },
[APP_REQUESTS.SONG]: handleSong,
};
/**
* Handles a request for authentication data from an app.
*
* @deprecated - This function is deprecated and will be removed in a future version.
* @param {string} appName - The name of the app requesting authentication data.
* @param {string[]} scope - The scope of the authentication request (This is also what the user will be prompted with and how it will be saved in the file).
*/
export async function requestUserInput(
appName: string,
scope: any
): Promise<void> {
const scopeData = Object.keys(scope).reduce(
(acc, key) => ({ ...acc, [key]: "placeholder-value" }),
{}
);
ServerMessageBus.notify("app:data", {
type: "input",
payload: { scope: scopeData },
request: "",
});
}