Skip to content

Commit 8869d3b

Browse files
feat(linkedin): add timeline feed command (#342)
* feat(linkedin): add timeline feed command * test(linkedin): add timeline adapter unit tests Add shape tests and utility function tests for the new timeline command. Include linkedin in the vitest adapter project config. --------- Co-authored-by: jackwener <jakevingoo@gmail.com>
1 parent 7c9caa8 commit 8869d3b

5 files changed

Lines changed: 638 additions & 1 deletion

File tree

docs/adapters/browser/linkedin.md

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,16 +7,22 @@
77
| Command | Description |
88
|---------|-------------|
99
| `opencli linkedin search` | |
10+
| `opencli linkedin timeline` | Read posts from your LinkedIn home feed |
1011

1112
## Usage Examples
1213

1314
```bash
1415
# Quick start
1516
opencli linkedin search --limit 5
1617

18+
# Read your home timeline
19+
opencli linkedin timeline --limit 5
20+
1721
# JSON output
1822
opencli linkedin search -f json
1923

24+
opencli linkedin timeline -f json
25+
2026
# Verbose mode
2127
opencli linkedin search -v
2228
```

docs/adapters/index.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -16,7 +16,7 @@ Run `opencli list` for the live registry.
1616
| **[v2ex](/adapters/browser/v2ex)** | `hot` `latest` `topic` `node` `user` `member` `replies` `nodes` `daily` `me` `notifications` | 🌐 / 🔐 |
1717
| **[bloomberg](/adapters/browser/bloomberg)** | `main` `markets` `economics` `industries` `tech` `politics` `businessweek` `opinions` `feeds` `news` | 🌐 / 🔐 |
1818
| **[weibo](/adapters/browser/weibo)** | `hot` `search` | 🔐 Browser |
19-
| **[linkedin](/adapters/browser/linkedin)** | `search` | 🔐 Browser |
19+
| **[linkedin](/adapters/browser/linkedin)** | `search` `timeline` | 🔐 Browser |
2020
| **[coupang](/adapters/browser/coupang)** | `search` `add-to-cart` | 🔐 Browser |
2121
| **[boss](/adapters/browser/boss)** | `search` `detail` `recommend` `joblist` `greet` `batchgreet` `send` `chatlist` `chatmsg` `invite` `mark` `exchange` `resume` `stats` | 🔐 Browser |
2222
| **[ctrip](/adapters/browser/ctrip)** | `search` | 🔐 Browser |

src/clis/linkedin/timeline.test.ts

Lines changed: 99 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,99 @@
1+
import { describe, expect, it } from 'vitest';
2+
import { getRegistry } from '../../registry.js';
3+
import './timeline.js';
4+
5+
const { parseMetric, buildPostId, mergeTimelinePosts } = await import('./timeline.js').then(
6+
(m) => (m as any).__test__,
7+
);
8+
9+
describe('linkedin timeline adapter', () => {
10+
const command = getRegistry().get('linkedin/timeline');
11+
12+
it('registers the command with correct shape', () => {
13+
expect(command).toBeDefined();
14+
expect(command!.site).toBe('linkedin');
15+
expect(command!.name).toBe('timeline');
16+
expect(command!.domain).toBe('www.linkedin.com');
17+
expect(command!.strategy).toBe('cookie');
18+
expect(command!.browser).toBe(true);
19+
expect(typeof command!.func).toBe('function');
20+
});
21+
22+
it('has limit arg with default 20', () => {
23+
const limitArg = command!.args.find((a) => a.name === 'limit');
24+
expect(limitArg).toBeDefined();
25+
expect(limitArg!.default).toBe(20);
26+
});
27+
28+
it('includes expected columns', () => {
29+
expect(command!.columns).toEqual(
30+
expect.arrayContaining(['author', 'text', 'reactions', 'comments', 'url']),
31+
);
32+
});
33+
});
34+
35+
describe('parseMetric', () => {
36+
it('parses plain numbers', () => {
37+
expect(parseMetric('42')).toBe(42);
38+
expect(parseMetric('1,234')).toBe(1234);
39+
});
40+
41+
it('handles k/m suffixes', () => {
42+
expect(parseMetric('2.5k')).toBe(2500);
43+
expect(parseMetric('1.2M')).toBe(1200000);
44+
});
45+
46+
it('returns 0 for empty/undefined', () => {
47+
expect(parseMetric('')).toBe(0);
48+
expect(parseMetric(undefined)).toBe(0);
49+
expect(parseMetric(null)).toBe(0);
50+
});
51+
});
52+
53+
describe('buildPostId', () => {
54+
it('uses url when present', () => {
55+
expect(buildPostId({ url: 'https://linkedin.com/post/123' })).toBe(
56+
'https://linkedin.com/post/123',
57+
);
58+
});
59+
60+
it('falls back to composite key', () => {
61+
const id = buildPostId({ author: 'Alice', posted_at: '2h', text: 'Hello world' });
62+
expect(id).toBe('Alice::2h::Hello world');
63+
});
64+
});
65+
66+
describe('mergeTimelinePosts', () => {
67+
it('deduplicates by url', () => {
68+
const url = 'https://linkedin.com/post/1';
69+
const a = {
70+
id: url,
71+
author: 'Alice',
72+
author_url: '',
73+
headline: '',
74+
text: 'Hello',
75+
posted_at: '1h',
76+
reactions: 5,
77+
comments: 1,
78+
url,
79+
};
80+
const result = mergeTimelinePosts([a], [a]);
81+
expect(result).toHaveLength(1);
82+
});
83+
84+
it('skips posts without author or text', () => {
85+
const empty = {
86+
id: '2',
87+
author: '',
88+
author_url: '',
89+
headline: '',
90+
text: 'some text',
91+
posted_at: '',
92+
reactions: 0,
93+
comments: 0,
94+
url: '',
95+
};
96+
const result = mergeTimelinePosts([], [empty]);
97+
expect(result).toHaveLength(0);
98+
});
99+
});

0 commit comments

Comments
 (0)