@lhremote/core 0.12.0 → 0.13.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/index.d.ts +1 -1
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +3 -3
- package/dist/index.js.map +1 -1
- package/dist/operations/comment-on-post.d.ts +7 -0
- package/dist/operations/comment-on-post.d.ts.map +1 -1
- package/dist/operations/comment-on-post.js +26 -14
- package/dist/operations/comment-on-post.js.map +1 -1
- package/dist/operations/dismiss-feed-post.d.ts +28 -0
- package/dist/operations/dismiss-feed-post.d.ts.map +1 -0
- package/dist/operations/dismiss-feed-post.js +115 -0
- package/dist/operations/dismiss-feed-post.js.map +1 -0
- package/dist/operations/dismiss-feed-post.test.d.ts +2 -0
- package/dist/operations/dismiss-feed-post.test.d.ts.map +1 -0
- package/dist/operations/dismiss-feed-post.test.js +137 -0
- package/dist/operations/dismiss-feed-post.test.js.map +1 -0
- package/dist/operations/hide-feed-author.d.ts +32 -0
- package/dist/operations/hide-feed-author.d.ts.map +1 -0
- package/dist/operations/hide-feed-author.js +116 -0
- package/dist/operations/hide-feed-author.js.map +1 -0
- package/dist/operations/hide-feed-author.test.d.ts +2 -0
- package/dist/operations/hide-feed-author.test.d.ts.map +1 -0
- package/dist/operations/hide-feed-author.test.js +160 -0
- package/dist/operations/hide-feed-author.test.js.map +1 -0
- package/dist/operations/index.d.ts +3 -0
- package/dist/operations/index.d.ts.map +1 -1
- package/dist/operations/index.js +3 -0
- package/dist/operations/index.js.map +1 -1
- package/dist/operations/react-to-post.d.ts +9 -0
- package/dist/operations/react-to-post.d.ts.map +1 -1
- package/dist/operations/react-to-post.js +15 -4
- package/dist/operations/react-to-post.js.map +1 -1
- package/dist/operations/react-to-post.test.js +49 -0
- package/dist/operations/react-to-post.test.js.map +1 -1
- package/dist/operations/unfollow-from-feed.d.ts +30 -0
- package/dist/operations/unfollow-from-feed.d.ts.map +1 -0
- package/dist/operations/unfollow-from-feed.js +114 -0
- package/dist/operations/unfollow-from-feed.js.map +1 -0
- package/dist/operations/unfollow-from-feed.test.d.ts +2 -0
- package/dist/operations/unfollow-from-feed.test.d.ts.map +1 -0
- package/dist/operations/unfollow-from-feed.test.js +141 -0
- package/dist/operations/unfollow-from-feed.test.js.map +1 -0
- package/package.json +1 -1
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
import type { HumanizedMouse } from "../linkedin/humanized-mouse.js";
|
|
2
|
+
import type { ConnectionOptions } from "./types.js";
|
|
3
|
+
export interface UnfollowFromFeedInput extends ConnectionOptions {
|
|
4
|
+
/** Zero-based index of the post in the visible LinkedIn feed. */
|
|
5
|
+
readonly feedIndex: number;
|
|
6
|
+
/** Optional humanized mouse for natural cursor movement and clicks. */
|
|
7
|
+
readonly mouse?: HumanizedMouse | null | undefined;
|
|
8
|
+
/** When true, locate the menu item but do not click it. */
|
|
9
|
+
readonly dryRun?: boolean | undefined;
|
|
10
|
+
}
|
|
11
|
+
export interface UnfollowFromFeedOutput {
|
|
12
|
+
readonly success: true;
|
|
13
|
+
readonly feedIndex: number;
|
|
14
|
+
/** The name extracted from the "Unfollow {Name}" menu item. */
|
|
15
|
+
readonly unfollowedName: string;
|
|
16
|
+
readonly dryRun: boolean;
|
|
17
|
+
}
|
|
18
|
+
/**
|
|
19
|
+
* Unfollow the author of a LinkedIn post via its feed three-dot menu.
|
|
20
|
+
*
|
|
21
|
+
* Navigates to the LinkedIn home feed, opens the three-dot menu of the
|
|
22
|
+
* post at the given `feedIndex`, and clicks the "Unfollow {Name}" menu
|
|
23
|
+
* item. The unfollowed person's name is extracted from the menu item text.
|
|
24
|
+
*
|
|
25
|
+
* @param input - Feed index and CDP connection parameters.
|
|
26
|
+
* @returns Confirmation including the unfollowed person's name.
|
|
27
|
+
* @throws If the three-dot menu does not contain an "Unfollow" item.
|
|
28
|
+
*/
|
|
29
|
+
export declare function unfollowFromFeed(input: UnfollowFromFeedInput): Promise<UnfollowFromFeedOutput>;
|
|
30
|
+
//# sourceMappingURL=unfollow-from-feed.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"unfollow-from-feed.d.ts","sourceRoot":"","sources":["../../src/operations/unfollow-from-feed.ts"],"names":[],"mappings":"AAOA,OAAO,KAAK,EAAE,cAAc,EAAE,MAAM,gCAAgC,CAAC;AAErE,OAAO,KAAK,EAAE,iBAAiB,EAAE,MAAM,YAAY,CAAC;AAQpD,MAAM,WAAW,qBAAsB,SAAQ,iBAAiB;IAC9D,iEAAiE;IACjE,QAAQ,CAAC,SAAS,EAAE,MAAM,CAAC;IAC3B,uEAAuE;IACvE,QAAQ,CAAC,KAAK,CAAC,EAAE,cAAc,GAAG,IAAI,GAAG,SAAS,CAAC;IACnD,2DAA2D;IAC3D,QAAQ,CAAC,MAAM,CAAC,EAAE,OAAO,GAAG,SAAS,CAAC;CACvC;AAED,MAAM,WAAW,sBAAsB;IACrC,QAAQ,CAAC,OAAO,EAAE,IAAI,CAAC;IACvB,QAAQ,CAAC,SAAS,EAAE,MAAM,CAAC;IAC3B,+DAA+D;IAC/D,QAAQ,CAAC,cAAc,EAAE,MAAM,CAAC;IAChC,QAAQ,CAAC,MAAM,EAAE,OAAO,CAAC;CAC1B;AAED;;;;;;;;;;GAUG;AACH,wBAAsB,gBAAgB,CACpC,KAAK,EAAE,qBAAqB,GAC3B,OAAO,CAAC,sBAAsB,CAAC,CAqHjC"}
|
|
@@ -0,0 +1,114 @@
|
|
|
1
|
+
// SPDX-License-Identifier: AGPL-3.0-only
|
|
2
|
+
// Copyright (C) 2026 Oleksii PELYKH
|
|
3
|
+
import { resolveInstancePort } from "../cdp/index.js";
|
|
4
|
+
import { CDPClient } from "../cdp/client.js";
|
|
5
|
+
import { discoverTargets } from "../cdp/discovery.js";
|
|
6
|
+
import { humanizedScrollToByIndex, retryInteraction } from "../linkedin/dom-automation.js";
|
|
7
|
+
import { gaussianDelay, maybeHesitate } from "../utils/delay.js";
|
|
8
|
+
import { navigateAwayIf } from "./navigate-away.js";
|
|
9
|
+
import { waitForFeedLoad } from "./get-feed.js";
|
|
10
|
+
/** CSS selector for feed post menu buttons. */
|
|
11
|
+
const FEED_MENU_BUTTON_SELECTOR = '[data-testid="mainFeed"] div[role="listitem"] button[aria-label^="Open control menu for post"]';
|
|
12
|
+
/**
|
|
13
|
+
* Unfollow the author of a LinkedIn post via its feed three-dot menu.
|
|
14
|
+
*
|
|
15
|
+
* Navigates to the LinkedIn home feed, opens the three-dot menu of the
|
|
16
|
+
* post at the given `feedIndex`, and clicks the "Unfollow {Name}" menu
|
|
17
|
+
* item. The unfollowed person's name is extracted from the menu item text.
|
|
18
|
+
*
|
|
19
|
+
* @param input - Feed index and CDP connection parameters.
|
|
20
|
+
* @returns Confirmation including the unfollowed person's name.
|
|
21
|
+
* @throws If the three-dot menu does not contain an "Unfollow" item.
|
|
22
|
+
*/
|
|
23
|
+
export async function unfollowFromFeed(input) {
|
|
24
|
+
const cdpPort = await resolveInstancePort(input.cdpPort, input.cdpHost);
|
|
25
|
+
const cdpHost = input.cdpHost ?? "127.0.0.1";
|
|
26
|
+
const allowRemote = input.allowRemote ?? false;
|
|
27
|
+
// Enforce loopback guard
|
|
28
|
+
if (!allowRemote && cdpHost !== "127.0.0.1" && cdpHost !== "localhost") {
|
|
29
|
+
throw new Error(`Non-loopback CDP host "${cdpHost}" requires --allow-remote. ` +
|
|
30
|
+
"This is a security measure to prevent remote code execution.");
|
|
31
|
+
}
|
|
32
|
+
const targets = await discoverTargets(cdpPort, cdpHost);
|
|
33
|
+
const linkedInTarget = targets.find((t) => t.type === "page" && t.url?.includes("linkedin.com"));
|
|
34
|
+
if (!linkedInTarget) {
|
|
35
|
+
throw new Error("No LinkedIn page found in LinkedHelper. " +
|
|
36
|
+
"Ensure LinkedHelper is running with an active LinkedIn session.");
|
|
37
|
+
}
|
|
38
|
+
const client = new CDPClient(cdpPort, { host: cdpHost, allowRemote });
|
|
39
|
+
await client.connect(linkedInTarget.id);
|
|
40
|
+
try {
|
|
41
|
+
const mouse = input.mouse;
|
|
42
|
+
const dryRun = input.dryRun ?? false;
|
|
43
|
+
const feedIndex = input.feedIndex;
|
|
44
|
+
// Navigate to the feed (force fresh load if already there)
|
|
45
|
+
await navigateAwayIf(client, "/feed");
|
|
46
|
+
await client.navigate("https://www.linkedin.com/feed/");
|
|
47
|
+
await waitForFeedLoad(client);
|
|
48
|
+
await maybeHesitate();
|
|
49
|
+
// Scroll the menu button into view by index and click it, retrying if
|
|
50
|
+
// the menu does not open on the first attempt.
|
|
51
|
+
const unfollowedName = await retryInteraction(async () => {
|
|
52
|
+
await humanizedScrollToByIndex(client, FEED_MENU_BUTTON_SELECTOR, feedIndex, mouse);
|
|
53
|
+
const clicked = await client.evaluate(`(() => {
|
|
54
|
+
const btns = document.querySelectorAll(
|
|
55
|
+
${JSON.stringify(FEED_MENU_BUTTON_SELECTOR)}
|
|
56
|
+
);
|
|
57
|
+
const btn = btns[${feedIndex}];
|
|
58
|
+
if (!btn) return false;
|
|
59
|
+
btn.click();
|
|
60
|
+
return true;
|
|
61
|
+
})()`);
|
|
62
|
+
if (!clicked) {
|
|
63
|
+
throw new Error("No feed post menu button found. " +
|
|
64
|
+
"Ensure the feed index points to a visible feed post.");
|
|
65
|
+
}
|
|
66
|
+
await gaussianDelay(700, 100, 500, 900);
|
|
67
|
+
// Find and click the "Unfollow {Name}" menu item, extracting
|
|
68
|
+
// the name from the text.
|
|
69
|
+
const name = await client.evaluate(`(() => {
|
|
70
|
+
const dryRun = ${dryRun};
|
|
71
|
+
for (const el of document.querySelectorAll('[role="menuitem"]')) {
|
|
72
|
+
const text = el.textContent?.trim() ?? '';
|
|
73
|
+
if (text.startsWith('Unfollow ')) {
|
|
74
|
+
if (!dryRun) el.click();
|
|
75
|
+
return text.slice('Unfollow '.length);
|
|
76
|
+
}
|
|
77
|
+
}
|
|
78
|
+
return null;
|
|
79
|
+
})()`);
|
|
80
|
+
if (!name) {
|
|
81
|
+
// Dismiss any open menu before retrying
|
|
82
|
+
await client.evaluate(`(() => {
|
|
83
|
+
document.dispatchEvent(
|
|
84
|
+
new KeyboardEvent('keydown', { key: 'Escape', bubbles: true })
|
|
85
|
+
);
|
|
86
|
+
})()`);
|
|
87
|
+
await gaussianDelay(300, 75, 200, 500);
|
|
88
|
+
throw new Error('No "Unfollow" item found in the post control menu. ' +
|
|
89
|
+
"The post author may already be unfollowed, or the post " +
|
|
90
|
+
"may not support this action.");
|
|
91
|
+
}
|
|
92
|
+
return name;
|
|
93
|
+
}, 3);
|
|
94
|
+
if (dryRun) {
|
|
95
|
+
await client.evaluate(`(() => {
|
|
96
|
+
document.dispatchEvent(new KeyboardEvent('keydown', { key: 'Escape', bubbles: true }));
|
|
97
|
+
})()`);
|
|
98
|
+
await gaussianDelay(300, 75, 200, 500);
|
|
99
|
+
}
|
|
100
|
+
// Let the UI settle after clicking Unfollow
|
|
101
|
+
await gaussianDelay(550, 75, 400, 700);
|
|
102
|
+
await gaussianDelay(1_500, 500, 700, 3_500); // Post-action dwell
|
|
103
|
+
return {
|
|
104
|
+
success: true,
|
|
105
|
+
feedIndex: input.feedIndex,
|
|
106
|
+
unfollowedName,
|
|
107
|
+
dryRun,
|
|
108
|
+
};
|
|
109
|
+
}
|
|
110
|
+
finally {
|
|
111
|
+
client.disconnect();
|
|
112
|
+
}
|
|
113
|
+
}
|
|
114
|
+
//# sourceMappingURL=unfollow-from-feed.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"unfollow-from-feed.js","sourceRoot":"","sources":["../../src/operations/unfollow-from-feed.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"unfollow-from-feed.test.d.ts","sourceRoot":"","sources":["../../src/operations/unfollow-from-feed.test.ts"],"names":[],"mappings":""}
|
|
@@ -0,0 +1,141 @@
|
|
|
1
|
+
// SPDX-License-Identifier: AGPL-3.0-only
|
|
2
|
+
// Copyright (C) 2026 Oleksii PELYKH
|
|
3
|
+
import { afterEach, beforeEach, describe, expect, it, vi } from "vitest";
|
|
4
|
+
vi.mock("../cdp/client.js", () => ({
|
|
5
|
+
CDPClient: vi.fn(),
|
|
6
|
+
}));
|
|
7
|
+
vi.mock("../cdp/discovery.js", () => ({
|
|
8
|
+
discoverTargets: vi.fn(),
|
|
9
|
+
}));
|
|
10
|
+
vi.mock("../linkedin/dom-automation.js", () => ({
|
|
11
|
+
humanizedScrollToByIndex: vi.fn().mockResolvedValue(undefined),
|
|
12
|
+
retryInteraction: vi.fn().mockImplementation((fn) => fn()),
|
|
13
|
+
}));
|
|
14
|
+
vi.mock("../utils/delay.js", () => ({
|
|
15
|
+
delay: vi.fn().mockResolvedValue(undefined),
|
|
16
|
+
gaussianDelay: vi.fn().mockResolvedValue(undefined),
|
|
17
|
+
maybeHesitate: vi.fn().mockResolvedValue(undefined),
|
|
18
|
+
}));
|
|
19
|
+
vi.mock("./navigate-away.js", () => ({
|
|
20
|
+
navigateAwayIf: vi.fn().mockResolvedValue(undefined),
|
|
21
|
+
}));
|
|
22
|
+
vi.mock("./get-feed.js", () => ({
|
|
23
|
+
waitForFeedLoad: vi.fn().mockResolvedValue(undefined),
|
|
24
|
+
}));
|
|
25
|
+
import { CDPClient } from "../cdp/client.js";
|
|
26
|
+
import { discoverTargets } from "../cdp/discovery.js";
|
|
27
|
+
import { humanizedScrollToByIndex, retryInteraction } from "../linkedin/dom-automation.js";
|
|
28
|
+
import { unfollowFromFeed } from "./unfollow-from-feed.js";
|
|
29
|
+
const mockClient = {
|
|
30
|
+
connect: vi.fn().mockResolvedValue(undefined),
|
|
31
|
+
navigate: vi.fn().mockResolvedValue(undefined),
|
|
32
|
+
evaluate: vi.fn().mockResolvedValue(null),
|
|
33
|
+
disconnect: vi.fn(),
|
|
34
|
+
};
|
|
35
|
+
function setupMocks(unfollowName = "John Doe") {
|
|
36
|
+
vi.mocked(CDPClient).mockImplementation(function () {
|
|
37
|
+
return mockClient;
|
|
38
|
+
});
|
|
39
|
+
vi.mocked(discoverTargets).mockResolvedValue([
|
|
40
|
+
{ id: "target-1", type: "page", title: "LinkedIn", url: "https://www.linkedin.com/feed/", description: "", devtoolsFrontendUrl: "" },
|
|
41
|
+
]);
|
|
42
|
+
vi.mocked(humanizedScrollToByIndex).mockResolvedValue(undefined);
|
|
43
|
+
// First evaluate: click menu button by index → returns true
|
|
44
|
+
// Second evaluate: click "Unfollow {Name}" menu item → returns name
|
|
45
|
+
mockClient.evaluate
|
|
46
|
+
.mockResolvedValueOnce(true) // menu button clicked
|
|
47
|
+
.mockResolvedValueOnce(unfollowName); // unfollow name from menu item
|
|
48
|
+
}
|
|
49
|
+
describe("unfollowFromFeed", () => {
|
|
50
|
+
beforeEach(() => {
|
|
51
|
+
vi.clearAllMocks();
|
|
52
|
+
});
|
|
53
|
+
afterEach(() => {
|
|
54
|
+
vi.restoreAllMocks();
|
|
55
|
+
});
|
|
56
|
+
it("throws on non-loopback host without allowRemote", async () => {
|
|
57
|
+
await expect(unfollowFromFeed({
|
|
58
|
+
feedIndex: 0,
|
|
59
|
+
cdpPort: 9222,
|
|
60
|
+
cdpHost: "192.168.1.100",
|
|
61
|
+
})).rejects.toThrow("requires --allow-remote");
|
|
62
|
+
});
|
|
63
|
+
it("allows non-loopback host with allowRemote", async () => {
|
|
64
|
+
setupMocks();
|
|
65
|
+
const result = await unfollowFromFeed({
|
|
66
|
+
feedIndex: 0,
|
|
67
|
+
cdpPort: 9222,
|
|
68
|
+
cdpHost: "192.168.1.100",
|
|
69
|
+
allowRemote: true,
|
|
70
|
+
});
|
|
71
|
+
expect(result.success).toBe(true);
|
|
72
|
+
});
|
|
73
|
+
it("throws when no LinkedIn page is found", async () => {
|
|
74
|
+
vi.mocked(discoverTargets).mockResolvedValue([
|
|
75
|
+
{ id: "target-1", type: "page", title: "Example", url: "https://example.com", description: "", devtoolsFrontendUrl: "" },
|
|
76
|
+
]);
|
|
77
|
+
await expect(unfollowFromFeed({
|
|
78
|
+
feedIndex: 0,
|
|
79
|
+
cdpPort: 9222,
|
|
80
|
+
})).rejects.toThrow("No LinkedIn page found");
|
|
81
|
+
});
|
|
82
|
+
it("navigates to the feed", async () => {
|
|
83
|
+
setupMocks();
|
|
84
|
+
await unfollowFromFeed({
|
|
85
|
+
feedIndex: 0,
|
|
86
|
+
cdpPort: 9222,
|
|
87
|
+
});
|
|
88
|
+
expect(mockClient.navigate).toHaveBeenCalledWith("https://www.linkedin.com/feed/");
|
|
89
|
+
});
|
|
90
|
+
it("returns success with unfollowed name", async () => {
|
|
91
|
+
setupMocks("Jane Smith");
|
|
92
|
+
const result = await unfollowFromFeed({
|
|
93
|
+
feedIndex: 0,
|
|
94
|
+
cdpPort: 9222,
|
|
95
|
+
});
|
|
96
|
+
expect(result).toEqual({
|
|
97
|
+
success: true,
|
|
98
|
+
feedIndex: 0,
|
|
99
|
+
unfollowedName: "Jane Smith",
|
|
100
|
+
dryRun: false,
|
|
101
|
+
});
|
|
102
|
+
});
|
|
103
|
+
it("throws when no Unfollow menu item is found", async () => {
|
|
104
|
+
setupMocks(null);
|
|
105
|
+
// Third evaluate is the Escape dismiss
|
|
106
|
+
mockClient.evaluate.mockResolvedValueOnce(undefined);
|
|
107
|
+
await expect(unfollowFromFeed({
|
|
108
|
+
feedIndex: 0,
|
|
109
|
+
cdpPort: 9222,
|
|
110
|
+
})).rejects.toThrow('No "Unfollow" item found');
|
|
111
|
+
});
|
|
112
|
+
it("wraps menu interaction in retryInteraction", async () => {
|
|
113
|
+
setupMocks();
|
|
114
|
+
await unfollowFromFeed({
|
|
115
|
+
feedIndex: 0,
|
|
116
|
+
cdpPort: 9222,
|
|
117
|
+
});
|
|
118
|
+
expect(retryInteraction).toHaveBeenCalledWith(expect.any(Function), 3);
|
|
119
|
+
});
|
|
120
|
+
it("scrolls menu button into view and clicks by index", async () => {
|
|
121
|
+
setupMocks();
|
|
122
|
+
await unfollowFromFeed({
|
|
123
|
+
feedIndex: 0,
|
|
124
|
+
cdpPort: 9222,
|
|
125
|
+
});
|
|
126
|
+
expect(humanizedScrollToByIndex).toHaveBeenCalledWith(mockClient, '[data-testid="mainFeed"] div[role="listitem"] button[aria-label^="Open control menu for post"]', 0, undefined);
|
|
127
|
+
// Menu button is clicked via evaluate (by index), not humanizedClick
|
|
128
|
+
expect(mockClient.evaluate).toHaveBeenCalled();
|
|
129
|
+
});
|
|
130
|
+
it("disconnects the client even on error", async () => {
|
|
131
|
+
setupMocks(null);
|
|
132
|
+
// Third evaluate is the Escape dismiss
|
|
133
|
+
mockClient.evaluate.mockResolvedValueOnce(undefined);
|
|
134
|
+
await unfollowFromFeed({
|
|
135
|
+
feedIndex: 0,
|
|
136
|
+
cdpPort: 9222,
|
|
137
|
+
}).catch(() => { });
|
|
138
|
+
expect(mockClient.disconnect).toHaveBeenCalled();
|
|
139
|
+
});
|
|
140
|
+
});
|
|
141
|
+
//# sourceMappingURL=unfollow-from-feed.test.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"unfollow-from-feed.test.js","sourceRoot":"","sources":["../../src/operations/unfollow-from-feed.test.ts"],"names":[],"mappings":"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"}
|