import { v4 as uuidv4 } from "uuid";
import plist from "plist";
import { WFWorkflow, WFWorkflowAction } from "../types.js";
import {
DEFAULT_WORKFLOW_CLIENT_VERSION, DEFAULT_WORKFLOW_CLIENT_RELEASE,
DEFAULT_MIN_CLIENT_VERSION, DEFAULT_MIN_CLIENT_VERSION_STRING,
DEFAULT_ICON_COLOR, DEFAULT_ICON_GLYPH, CONTENT_ITEM_CLASSES,
ACTION_PREFIX, ICON_COLORS, ICON_GLYPHS, COMPARISON_OPERATORS
} from "../constants.js";
export interface ActionInput {
identifier: string;
parameters?: Record<string, unknown>;
uuid?: string;
}
export interface ShortcutInput {
name: string;
actions: ActionInput[];
icon?: { color?: keyof typeof ICON_COLORS | number; glyph?: keyof typeof ICON_GLYPHS | number };
inputTypes?: string[];
outputTypes?: string[];
}
export function createGroupUUID(): string {
return uuidv4().toUpperCase();
}
export function createAction(input: ActionInput): WFWorkflowAction {
const identifier = input.identifier.startsWith(ACTION_PREFIX)
? input.identifier : `${ACTION_PREFIX}${input.identifier}`;
const action: WFWorkflowAction = {
WFWorkflowActionIdentifier: identifier,
WFWorkflowActionParameters: {}
};
if (input.uuid) action.WFWorkflowActionParameters.UUID = input.uuid;
if (input.parameters) {
for (const [key, value] of Object.entries(input.parameters)) {
action.WFWorkflowActionParameters[key] = value;
}
}
return action;
}
export function createTextAction(text: string): WFWorkflowAction {
return createAction({ identifier: "gettext", parameters: { WFTextActionText: text }, uuid: uuidv4().toUpperCase() });
}
export function createAlertAction(title: string, message?: string, showCancel = true): WFWorkflowAction {
const params: Record<string, unknown> = { WFAlertActionTitle: title, WFAlertActionCancelButtonShown: showCancel };
if (message) params.WFAlertActionMessage = message;
return createAction({ identifier: "alert", parameters: params });
}
export function createNotificationAction(body: string, title?: string, playSound = true): WFWorkflowAction {
const params: Record<string, unknown> = { WFNotificationActionBody: body, WFNotificationActionSound: playSound };
if (title) params.WFNotificationActionTitle = title;
return createAction({ identifier: "notification", parameters: params });
}
export function createHTTPAction(
url: string,
method: "GET" | "POST" | "PUT" | "DELETE" | "PATCH" = "GET",
headers?: Record<string, string>,
jsonBody?: Record<string, unknown>
): WFWorkflowAction {
const params: Record<string, unknown> = { WFURL: url, WFHTTPMethod: method };
if (headers && Object.keys(headers).length > 0) {
params.WFHTTPHeaders = {
Value: {
WFDictionaryFieldValueItems: Object.entries(headers).map(([key, value]) => ({
WFItemType: 0,
WFKey: { Value: { string: key }, WFSerializationType: "WFTextTokenString" },
WFValue: { Value: { string: value }, WFSerializationType: "WFTextTokenString" }
}))
},
WFSerializationType: "WFDictionaryFieldValue"
};
}
if (jsonBody && Object.keys(jsonBody).length > 0) {
params.WFHTTPBodyType = "JSON";
params.WFJSONValues = {
Value: {
WFDictionaryFieldValueItems: Object.entries(jsonBody).map(([key, value]) => ({
WFItemType: typeof value === "number" ? 3 : typeof value === "boolean" ? 4 : 0,
WFKey: { Value: { string: key }, WFSerializationType: "WFTextTokenString" },
WFValue: { Value: { string: String(value) }, WFSerializationType: "WFTextTokenString" }
}))
},
WFSerializationType: "WFDictionaryFieldValue"
};
}
return createAction({ identifier: "downloadurl", parameters: params, uuid: uuidv4().toUpperCase() });
}
export function createIfBlock(
condition: keyof typeof COMPARISON_OPERATORS,
compareValue?: string | number
): { if: WFWorkflowAction; otherwise: WFWorkflowAction; endIf: WFWorkflowAction; groupId: string } {
const groupId = createGroupUUID();
const ifParams: Record<string, unknown> = { GroupingIdentifier: groupId, WFControlFlowMode: 0, WFCondition: COMPARISON_OPERATORS[condition] };
if (compareValue !== undefined) {
if (typeof compareValue === "number") ifParams.WFNumberValue = compareValue;
else ifParams.WFConditionalActionString = compareValue;
}
return {
if: createAction({ identifier: "conditional", parameters: ifParams }),
otherwise: createAction({ identifier: "conditional", parameters: { GroupingIdentifier: groupId, WFControlFlowMode: 1 } }),
endIf: createAction({ identifier: "conditional", parameters: { GroupingIdentifier: groupId, WFControlFlowMode: 2 } }),
groupId
};
}
export function createRepeatBlock(count: number): { repeat: WFWorkflowAction; endRepeat: WFWorkflowAction; groupId: string } {
const groupId = createGroupUUID();
return {
repeat: createAction({ identifier: "repeat.count", parameters: { GroupingIdentifier: groupId, WFControlFlowMode: 0, WFRepeatCount: count } }),
endRepeat: createAction({ identifier: "repeat.count", parameters: { GroupingIdentifier: groupId, WFControlFlowMode: 2 } }),
groupId
};
}
export function createRepeatEachBlock(): { repeat: WFWorkflowAction; endRepeat: WFWorkflowAction; groupId: string } {
const groupId = createGroupUUID();
return {
repeat: createAction({ identifier: "repeat.each", parameters: { GroupingIdentifier: groupId, WFControlFlowMode: 0 } }),
endRepeat: createAction({ identifier: "repeat.each", parameters: { GroupingIdentifier: groupId, WFControlFlowMode: 2 } }),
groupId
};
}
export function createMenuBlock(prompt: string, items: string[]): {
menu: WFWorkflowAction; menuItems: WFWorkflowAction[]; endMenu: WFWorkflowAction; groupId: string
} {
const groupId = createGroupUUID();
return {
menu: createAction({ identifier: "choosefrommenu", parameters: { GroupingIdentifier: groupId, WFControlFlowMode: 0, WFMenuPrompt: prompt, WFMenuItems: items } }),
menuItems: items.map(item => createAction({ identifier: "choosefrommenu", parameters: { GroupingIdentifier: groupId, WFControlFlowMode: 1, WFMenuItemTitle: item } })),
endMenu: createAction({ identifier: "choosefrommenu", parameters: { GroupingIdentifier: groupId, WFControlFlowMode: 2 } }),
groupId
};
}
export function createSetVariableAction(name: string): WFWorkflowAction {
return createAction({ identifier: "setvariable", parameters: { WFVariableName: name } });
}
export function createGetVariableAction(name: string): WFWorkflowAction {
return createAction({
identifier: "getvariable",
parameters: { WFVariable: { Value: { Type: "Variable", VariableName: name }, WFSerializationType: "WFTextTokenAttachment" } }
});
}
export function createCommentAction(text: string): WFWorkflowAction {
return createAction({ identifier: "comment", parameters: { WFCommentActionText: text } });
}
export function createDictionaryAction(items: Record<string, unknown>): WFWorkflowAction {
const dictionaryItems = Object.entries(items).map(([key, value]) => {
let itemType = 0;
if (typeof value === "number") itemType = 3;
else if (typeof value === "boolean") itemType = 4;
else if (Array.isArray(value)) itemType = 2;
else if (typeof value === "object" && value !== null) itemType = 1;
return {
WFItemType: itemType,
WFKey: { Value: { string: key }, WFSerializationType: "WFTextTokenString" },
WFValue: { Value: { string: JSON.stringify(value) }, WFSerializationType: "WFTextTokenString" }
};
});
return createAction({
identifier: "dictionary",
parameters: { WFItems: { Value: { WFDictionaryFieldValueItems: dictionaryItems }, WFSerializationType: "WFDictionaryFieldValue" } },
uuid: uuidv4().toUpperCase()
});
}
function resolveIconColor(color?: keyof typeof ICON_COLORS | number): number {
if (color === undefined) return DEFAULT_ICON_COLOR;
if (typeof color === "number") return color;
return ICON_COLORS[color] ?? DEFAULT_ICON_COLOR;
}
function resolveIconGlyph(glyph?: keyof typeof ICON_GLYPHS | number): number {
if (glyph === undefined) return DEFAULT_ICON_GLYPH;
if (typeof glyph === "number") return glyph;
return ICON_GLYPHS[glyph] ?? DEFAULT_ICON_GLYPH;
}
export function buildShortcut(input: ShortcutInput): WFWorkflow {
return {
WFWorkflowClientVersion: DEFAULT_WORKFLOW_CLIENT_VERSION,
WFWorkflowClientRelease: DEFAULT_WORKFLOW_CLIENT_RELEASE,
WFWorkflowMinimumClientVersion: DEFAULT_MIN_CLIENT_VERSION,
WFWorkflowMinimumClientVersionString: DEFAULT_MIN_CLIENT_VERSION_STRING,
WFWorkflowIcon: {
WFWorkflowIconStartColor: resolveIconColor(input.icon?.color),
WFWorkflowIconGlyphNumber: resolveIconGlyph(input.icon?.glyph)
},
WFWorkflowImportQuestions: [],
WFWorkflowTypes: ["Watch", "NCWidget"],
WFWorkflowInputContentItemClasses: input.inputTypes ?? CONTENT_ITEM_CLASSES,
WFWorkflowOutputContentItemClasses: input.outputTypes ?? CONTENT_ITEM_CLASSES,
WFWorkflowActions: input.actions.map(a => createAction(a))
};
}
export function workflowToPlist(workflow: WFWorkflow): string {
return plist.build(workflow as unknown as plist.PlistValue);
}
export function plistToWorkflow(plistString: string): WFWorkflow {
return plist.parse(plistString) as unknown as WFWorkflow;
}
export function generateShortcut(input: ShortcutInput): { workflow: WFWorkflow; plist: string } {
const workflow = buildShortcut(input);
return { workflow, plist: workflowToPlist(workflow) };
}