Skip to content

Commit

Permalink
Added support for shared lists
Browse files Browse the repository at this point in the history
  • Loading branch information
karelklima committed Dec 19, 2024
1 parent b30cef1 commit 3392c3f
Show file tree
Hide file tree
Showing 8 changed files with 283 additions and 2 deletions.
14 changes: 14 additions & 0 deletions src/client.ts
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@ const LOGIN_URL = `${WORKFLOWY_URL}/ajax_login`;
const INITIALIZATION_DATA_URL =
`${WORKFLOWY_URL}/get_initialization_data?client_version=21&client_version_v2=28&no_root_children=1`;
const TREE_DATA_URL = `${WORKFLOWY_URL}/get_tree_data/`;
const SHARED_TREE_DATA_URL = `${WORKFLOWY_URL}/get_tree_data/?share_id=`;
const PUSH_AND_POLL_URL = `${WORKFLOWY_URL}/push_and_poll`;
const CLIENT_VERSION = "21";
const SESSION_COOKIE_NAME = `sessionid`;
Expand Down Expand Up @@ -198,6 +199,19 @@ export class Client {
return data;
}

/**
* Fetches the shared WorkFlowy subdocument
*
* Queries `workflowy.com/get_tree_data/?share_id=` endpoint
* @returns List of all items in WorkFlowy shared document
*/
public async getSharedTreeData(shareId: string): Promise<TreeData> {
const json = await this.#authenticatedFetch(SHARED_TREE_DATA_URL + shareId);
const data = TreeDataSchema.parse(json);
this.#lastTransactionId = data.most_recent_operation_transaction_id;
return data;
}

/**
* Applies a list of operations to WorkFlowy document
*
Expand Down
43 changes: 43 additions & 0 deletions src/document.ts
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,7 @@ class Companion {
public readonly client: Client,
public readonly itemMap: Map<string, TreeItemWithChildren>,
public readonly shareMap: Map<string, TreeItemShareInfo>,
public readonly shareIdMap: Map<string, string>,
public readonly expandedProjects: Set<string>,
public readonly initializationData: InitializationData,
) {}
Expand Down Expand Up @@ -78,6 +79,7 @@ export class Document {
client: Client,
data: TreeData,
initializationData: InitializationData,
sharedTrees: Record<string, TreeData> = {},
) {
const itemMap = new Map<string, TreeItemWithChildren>();

Expand Down Expand Up @@ -109,10 +111,42 @@ export class Document {

const expandedProjects = new Set(data.server_expanded_projects_list);

const shareIdMap = new Map<string, string>();

for (const [shareId, sharedTree] of Object.entries(sharedTrees)) {
sharedTree.items.sort((a, b) => Math.sign(a.priority - b.priority));

for (const item of sharedTree.items) {
if (item.parentId !== "None") {
const p = getItem(item.parentId);
p.children.push(item.id);
} else {
shareIdMap.set(shareId, item.id);
}
const t = getItem(item.id);
itemMap.set(item.id, { ...t, ...item });
}

for (
const [id, shareInfo] of Object.entries(sharedTree.shared_projects)
) {
shareMap.set(id, shareInfo);
}

for (const id of sharedTree.server_expanded_projects_list) {
expandedProjects.add(id);
}
}

for (const [id, shareInfo] of shareMap) {
shareIdMap.set(shareInfo.shareId, id);
}

this.#companion = new Companion(
client,
itemMap,
shareMap,
shareIdMap,
expandedProjects,
initializationData,
);
Expand Down Expand Up @@ -176,13 +210,21 @@ export class List {
if (source.isMirrorRoot) {
return this.#companion.itemMap.get(source.originalId!)!;
}
if (source.shareId !== undefined) {
const proxyId = this.#companion.shareIdMap.get(source.shareId);
if (proxyId === undefined) {
throw new Error(`Shared list not found: ${source.shareId}`);
}
return this.#companion.itemMap.get(proxyId!)!;
}
return source;
}

private get shareData(): TreeItemShareInfo {
const id = this.data.id;
if (!this.#companion.shareMap.has(id)) {
this.#companion.shareMap.set(id, {
shareId: "none",
isSharedViaUrl: false,
urlAccessToken: undefined,
urlPermissionLevel: undefined,
Expand Down Expand Up @@ -335,6 +377,7 @@ export class List {
lastModified: this.#companion.getNow(),
originalId: undefined,
isMirrorRoot: false,
shareId: undefined,
children: [],
});

Expand Down
3 changes: 3 additions & 0 deletions src/schema.ts
Original file line number Diff line number Diff line change
Expand Up @@ -38,6 +38,7 @@ const TreeItemShareInfoSchema = z.object({
})),
}).optional(),
}).transform((i) => ({
shareId: i.share_id,
isSharedViaUrl: i.url_shared_info !== undefined,
urlAccessToken: i.url_shared_info?.access_token,
urlPermissionLevel: i.url_shared_info?.permission_level,
Expand All @@ -63,6 +64,7 @@ export const TreeDataSchema = z.object({
isMirrorRoot: z.boolean().optional(),
}).optional(),
}),
as: z.string().optional(),
}).transform((i) => ({
id: i.id,
name: i.nm,
Expand All @@ -73,6 +75,7 @@ export const TreeDataSchema = z.object({
lastModified: i.lm,
originalId: i.metadata?.mirror?.originalId,
isMirrorRoot: i.metadata?.mirror?.isMirrorRoot === true,
shareId: i.as,
})),
),
shared_projects: z.record(TreeItemShareInfoSchema),
Expand Down
46 changes: 44 additions & 2 deletions src/workflowy.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
import { Client } from "./client.ts";
import { Document } from "./document.ts";
import { TreeData } from "./schema.ts";

/**
* The entry point of the library
Expand Down Expand Up @@ -34,11 +35,52 @@ export class WorkFlowy {

/**
* Loads data from WorkFlowy and creates an interactive document out of it
*
* @param includeSharedLists whether to download dependent shared lists
* @returns {Promise<Document>} WorkFlowy outline
*/
public async getDocument(): Promise<Document> {
public async getDocument(includeSharedLists = true): Promise<Document> {
const initializationData = await this.#client.getInitializationData();
const treeData = await this.#client.getTreeData();
return new Document(this.#client, treeData, initializationData);
const sharedTrees: Record<string, TreeData> = includeSharedLists
? await this.getSharedTrees(treeData)
: {};

return new Document(
this.#client,
treeData,
initializationData,
sharedTrees,
);
}

private async getSharedTrees(
treeData: TreeData,
): Promise<Record<string, TreeData>> {
const sharedTrees: Record<string, TreeData> = {};
const queue: string[] = [];
const memo: Record<string, boolean> = {};

const extractSharedItems = (data: TreeData) => {
for (const item of data.items) {
if (item.shareId !== undefined && memo[item.shareId] !== true) {
queue.push(item.shareId);
memo[item.shareId] = true;
}
}
};

extractSharedItems(treeData);

while (queue.length > 0) {
const shareId = queue.pop()!;
const sharedTree = await this.#client.getSharedTreeData(shareId);

extractSharedItems(sharedTree);

sharedTrees[shareId] = sharedTree;
}

return sharedTrees;
}
}
44 changes: 44 additions & 0 deletions tests/mocks/get_tree_data_shared_first.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,44 @@
{
"items": [
{
"id": "aaba6df4-cde1-3322-96cd-957fc76123e8",
"nm": "List shared via URL",
"ct": 45772460,
"pr": 538,
"prnt": null,
"metadata": {},
"lm": 45772507
},
{
"id": "12f62eec-754c-b677-b683-7dd448dc49ac",
"nm": "Normal list",
"ct": 148,
"pr": 200,
"prnt": "aaba6df4-cde1-3322-96cd-957fc76123e8",
"metadata": {},
"lm": 155459
},
{
"id": "68e27b97-4444-77c6-f6e8-22c1e94ab437",
"nm": "",
"ct": 150,
"pr": 300,
"prnt": "aaba6df4-cde1-3322-96cd-957fc76123e8",
"metadata": {},
"lm": 157519,
"as": "NnjJ.lybhWrZBRX"
}
],
"shared_projects": {
"aaba6df4-cde1-3322-96cd-957fc76123e8": {
"share_id": "NnjJ.tUQQvYjgkX",
"url_shared_info": {
"write_permission": false,
"permission_level": 1,
"access_token": "plUzlWcMHcwbR3wZ"
}
}
},
"most_recent_operation_transaction_id": "1160300075",
"server_expanded_projects_list": []
}
36 changes: 36 additions & 0 deletions tests/mocks/get_tree_data_shared_main.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,36 @@
{
"items": [
{
"id": "de843e0e-2d25-a812-1415-6ed31ae618f4",
"nm": "AAA",
"ct": 146,
"pr": 100,
"prnt": null,
"metadata": {},
"lm": 223
},
{
"id": "12f62eec-754c-b677-b683-7dd448dc49ac",
"nm": "ZZZ",
"ct": 148,
"pr": 200,
"prnt": null,
"metadata": {},
"no": "Two Description",
"lm": 155459
},
{
"id": "3e6b63c5-7e40-1e1e-9987-cd7af6e64893",
"nm": "",
"ct": 150,
"pr": 150,
"prnt": null,
"metadata": {},
"lm": 157519,
"as": "NnjJ.tUQQvYjgkX"
}
],
"shared_projects": {},
"most_recent_operation_transaction_id": "1160300075",
"server_expanded_projects_list": []
}
39 changes: 39 additions & 0 deletions tests/mocks/get_tree_data_shared_second.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,39 @@
{
"items": [
{
"id": "8960ce1b-e5b5-4aff-3303-50577f20e76b",
"nm": "List shared via email",
"ct": 45707709,
"pr": 525,
"prnt": null,
"metadata": {},
"lm": 45708060
},
{
"id": "68e27b97-4444-77c6-f6e8-22c1e94ab431",
"nm": "Normal second list",
"ct": 158394,
"pr": 400,
"prnt": "8960ce1b-e5b5-4aff-3303-50577f20e76b",
"metadata": {},
"lm": 158394
}
],
"shared_projects": {
"8960ce1b-e5b5-4aff-3303-50577f20e76b": {
"share_id": "NnjJ.lybhWrZBRX",
"email_shared_info": {
"emails": [
{
"email": "[email protected]",
"access_token": "L2RdOGpOND",
"write_permission": true,
"permission_level": 3
}
]
}
}
},
"most_recent_operation_transaction_id": "1160300075",
"server_expanded_projects_list": []
}
60 changes: 60 additions & 0 deletions tests/shared_test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,60 @@
import mockInitializationData from "./mocks/get_initialization_data.json" with {
type: "json",
};
import mockTreeDataMain from "./mocks/get_tree_data_shared_main.json" with {
type: "json",
};
import mockTreeDataFirst from "./mocks/get_tree_data_shared_first.json" with {
type: "json",
};
import mockTreeDataSecond from "./mocks/get_tree_data_shared_second.json" with {
type: "json",
};

import { assertEquals } from "./test_deps.ts";

import { Document } from "../src/document.ts";
import type { Client } from "../src/client.ts";
import { InitializationDataSchema, TreeDataSchema } from "../src/schema.ts";

const mockClient = () => ({} as unknown as Client);
const mockTree = () => TreeDataSchema.parse(mockTreeDataMain);
const mockInitialization = () =>
InitializationDataSchema.parse(mockInitializationData);
const mockSharedTrees = () => {
const first =
mockTreeDataFirst.shared_projects["aaba6df4-cde1-3322-96cd-957fc76123e8"]
.share_id;
const second =
mockTreeDataSecond.shared_projects["8960ce1b-e5b5-4aff-3303-50577f20e76b"]
.share_id;
return {
[first]: TreeDataSchema.parse(mockTreeDataFirst),
[second]: TreeDataSchema.parse(mockTreeDataSecond),
};
};

const mockDocument = () =>
new Document(
mockClient(),
mockTree(),
mockInitialization(),
mockSharedTrees(),
);

Deno.test("WorkFlowy Shared / X", () => {
const document = mockDocument();

const firstShared = document.items[1];

assertEquals(firstShared.name, "List shared via URL");

assertEquals(firstShared.items.length, 2);
assertEquals(firstShared.items[0].name, "Normal list");

const secondShared = firstShared.items[1];

assertEquals(secondShared.name, "List shared via email");
assertEquals(secondShared.items.length, 1);
assertEquals(secondShared.items[0].name, "Normal second list");
});

0 comments on commit 3392c3f

Please sign in to comment.