Skip to content

Commit ce484c2

Browse files
authored
feat: add Lobste.rs, Instagram, and Facebook adapters (#199)
* feat(lobsters): add Lobste.rs adapter with hot, newest, active, tag commands Add public API adapter for Lobste.rs (lobste.rs), a developer-focused link aggregation community. All commands use the public JSON API and require no authentication or browser. Commands: - hot: hottest stories - newest: latest stories - active: most active discussions - tag: filter stories by tag (e.g. rust, security, programming) * feat(instagram,facebook): add Instagram and Facebook adapters Instagram (7 commands, browser mode - internal REST API): - profile: user profile info (followers, following, posts, bio) - search: search users - user: recent posts from a user - followers: list user's followers - following: list user's following - saved: saved posts - explore: discover trending posts Facebook (4 commands, browser mode - DOM scraping): - profile: user/page profile info - notifications: recent notifications - feed: news feed posts - search: search people, pages, posts All commands require Chrome to be logged in to the respective site. Instagram uses stable internal API endpoints with cookie auth. Facebook uses DOM scraping via role attributes and semantic selectors.
1 parent 06c902a commit ce484c2

File tree

15 files changed

+622
-0
lines changed

15 files changed

+622
-0
lines changed

src/clis/facebook/feed.yaml

Lines changed: 55 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,55 @@
1+
site: facebook
2+
name: feed
3+
description: Get your Facebook news feed
4+
domain: www.facebook.com
5+
6+
args:
7+
limit:
8+
type: int
9+
default: 10
10+
description: Number of posts
11+
12+
pipeline:
13+
- navigate:
14+
url: https://www.facebook.com/
15+
settleMs: 4000
16+
17+
- evaluate: |
18+
(() => {
19+
const limit = ${{ args.limit }};
20+
const posts = document.querySelectorAll('[role="article"]');
21+
return Array.from(posts)
22+
.filter(el => {
23+
const text = el.textContent.trim();
24+
return text.length > 30 && !text.startsWith('可能认识');
25+
})
26+
.slice(0, limit)
27+
.map((el, i) => {
28+
// Author from header link
29+
const headerLink = el.querySelector('h2 a, h3 a, h4 a, strong a');
30+
const author = headerLink ? headerLink.textContent.trim() : '';
31+
32+
// Post text: grab visible spans, filter noise
33+
const spans = Array.from(el.querySelectorAll('div[dir="auto"]'))
34+
.map(s => s.textContent.trim())
35+
.filter(t => t.length > 10 && t.length < 500);
36+
const content = spans.length > 0 ? spans[0] : '';
37+
38+
// Engagement: find like/comment/share counts
39+
const allText = el.textContent;
40+
const likesMatch = allText.match(/所有心情:([\d,.\s]*[\d万亿KMk]+)/) || allText.match(/All:\s*([\d,.KMk]+)/);
41+
const commentsMatch = allText.match(/([\d,.]+\s*[万亿]?)\s*条评论/) || allText.match(/([\d,.KMk]+)\s*comments?/);
42+
const sharesMatch = allText.match(/([\d,.]+\s*[万亿]?)\s*次分享/) || allText.match(/([\d,.KMk]+)\s*shares?/);
43+
44+
return {
45+
index: i + 1,
46+
author: author.substring(0, 50),
47+
content: content.replace(/\n/g, ' ').substring(0, 120),
48+
likes: likesMatch ? likesMatch[1] : '-',
49+
comments: commentsMatch ? commentsMatch[1] : '-',
50+
shares: sharesMatch ? sharesMatch[1] : '-',
51+
};
52+
});
53+
})()
54+
55+
columns: [index, author, content, likes, comments, shares]
Lines changed: 40 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,40 @@
1+
site: facebook
2+
name: notifications
3+
description: Get recent Facebook notifications
4+
domain: www.facebook.com
5+
6+
args:
7+
limit:
8+
type: int
9+
default: 15
10+
description: Number of notifications
11+
12+
pipeline:
13+
- navigate:
14+
url: https://www.facebook.com/notifications
15+
settleMs: 3000
16+
17+
- evaluate: |
18+
(() => {
19+
const limit = ${{ args.limit }};
20+
const items = document.querySelectorAll('[role="listitem"]');
21+
return Array.from(items)
22+
.filter(el => el.querySelectorAll('a').length > 0)
23+
.slice(0, limit)
24+
.map((el, i) => {
25+
const raw = el.textContent.trim().replace(/\s+/g, ' ');
26+
// Remove leading "未读" and trailing "标记为已读"
27+
const cleaned = raw.replace(/^未读/, '').replace(/标记为已读$/, '').replace(/^Unread/, '').replace(/Mark as read$/, '').trim();
28+
// Try to extract time (last segment like "11小时", "5天", "1周")
29+
const timeMatch = cleaned.match(/(\d+\s*(?:分钟|小时|天|周|个月|minutes?|hours?|days?|weeks?|months?))\s*$/);
30+
const time = timeMatch ? timeMatch[1] : '';
31+
const text = timeMatch ? cleaned.slice(0, -timeMatch[0].length).trim() : cleaned;
32+
return {
33+
index: i + 1,
34+
text: text.substring(0, 150),
35+
time: time || '-',
36+
};
37+
});
38+
})()
39+
40+
columns: [index, text, time]

src/clis/facebook/profile.yaml

Lines changed: 37 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,37 @@
1+
site: facebook
2+
name: profile
3+
description: Get Facebook user/page profile info
4+
domain: www.facebook.com
5+
6+
args:
7+
username:
8+
type: str
9+
required: true
10+
positional: true
11+
description: Facebook username or page name
12+
13+
pipeline:
14+
- navigate:
15+
url: https://www.facebook.com/${{ args.username }}
16+
settleMs: 3000
17+
18+
- evaluate: |
19+
(() => {
20+
const h1 = document.querySelector('h1');
21+
let name = h1 ? h1.textContent.trim() : '';
22+
23+
// Find friends/followers links
24+
const links = Array.from(document.querySelectorAll('a'));
25+
const friendsLink = links.find(a => a.href && a.href.includes('/friends'));
26+
const followersLink = links.find(a => a.href && a.href.includes('/followers'));
27+
28+
return [{
29+
name: name,
30+
username: ${{ args.username | json }},
31+
friends: friendsLink ? friendsLink.textContent.trim() : '-',
32+
followers: followersLink ? followersLink.textContent.trim() : '-',
33+
url: window.location.href,
34+
}];
35+
})()
36+
37+
columns: [name, username, friends, followers, url]

src/clis/facebook/search.yaml

Lines changed: 45 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,45 @@
1+
site: facebook
2+
name: search
3+
description: Search Facebook for people, pages, or posts
4+
domain: www.facebook.com
5+
6+
args:
7+
query:
8+
type: str
9+
required: true
10+
positional: true
11+
description: Search query
12+
limit:
13+
type: int
14+
default: 10
15+
description: Number of results
16+
17+
pipeline:
18+
- navigate:
19+
url: https://www.facebook.com/search/top?q=${{ args.query }}
20+
settleMs: 4000
21+
22+
- evaluate: |
23+
(() => {
24+
const limit = ${{ args.limit }};
25+
// Search results are typically in role="article" or role="listitem"
26+
let items = document.querySelectorAll('[role="article"]');
27+
if (items.length === 0) {
28+
items = document.querySelectorAll('[role="listitem"]');
29+
}
30+
return Array.from(items)
31+
.filter(el => el.textContent.trim().length > 20)
32+
.slice(0, limit)
33+
.map((el, i) => {
34+
const link = el.querySelector('a[href*="facebook.com/"]');
35+
const heading = el.querySelector('h2, h3, h4, strong');
36+
return {
37+
index: i + 1,
38+
title: heading ? heading.textContent.trim().substring(0, 80) : '',
39+
text: el.textContent.trim().replace(/\s+/g, ' ').substring(0, 150),
40+
url: link ? link.href.split('?')[0] : '',
41+
};
42+
});
43+
})()
44+
45+
columns: [index, title, text]

src/clis/instagram/explore.yaml

Lines changed: 43 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,43 @@
1+
site: instagram
2+
name: explore
3+
description: Instagram explore/discover trending posts
4+
domain: www.instagram.com
5+
6+
args:
7+
limit:
8+
type: int
9+
default: 20
10+
description: Number of posts
11+
12+
pipeline:
13+
- navigate: https://www.instagram.com
14+
15+
- evaluate: |
16+
(async () => {
17+
const limit = ${{ args.limit }};
18+
const res = await fetch(
19+
'https://www.instagram.com/api/v1/discover/web/explore_grid/',
20+
{
21+
credentials: 'include',
22+
headers: { 'X-IG-App-ID': '936619743392459' }
23+
}
24+
);
25+
if (!res.ok) throw new Error('HTTP ' + res.status + ' - make sure you are logged in to Instagram');
26+
const data = await res.json();
27+
const posts = [];
28+
for (const sec of (data?.sectional_items || [])) {
29+
for (const m of (sec?.layout_content?.medias || [])) {
30+
const media = m?.media;
31+
if (media) posts.push({
32+
user: media.user?.username || '',
33+
caption: (media.caption?.text || '').replace(/\n/g, ' ').substring(0, 100),
34+
likes: media.like_count ?? 0,
35+
comments: media.comment_count ?? 0,
36+
type: media.media_type === 1 ? 'photo' : media.media_type === 2 ? 'video' : 'carousel',
37+
});
38+
}
39+
}
40+
return posts.slice(0, limit).map((p, i) => ({ rank: i + 1, ...p }));
41+
})()
42+
43+
columns: [rank, user, caption, likes, comments, type]

src/clis/instagram/followers.yaml

Lines changed: 51 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,51 @@
1+
site: instagram
2+
name: followers
3+
description: List followers of an Instagram user
4+
domain: www.instagram.com
5+
6+
args:
7+
username:
8+
type: str
9+
required: true
10+
positional: true
11+
description: Instagram username
12+
limit:
13+
type: int
14+
default: 20
15+
description: Number of followers
16+
17+
pipeline:
18+
- navigate: https://www.instagram.com
19+
20+
- evaluate: |
21+
(async () => {
22+
const username = ${{ args.username | json }};
23+
const limit = ${{ args.limit }};
24+
const headers = { 'X-IG-App-ID': '936619743392459' };
25+
const opts = { credentials: 'include', headers };
26+
27+
const r1 = await fetch(
28+
'https://www.instagram.com/api/v1/users/web_profile_info/?username=' + encodeURIComponent(username),
29+
opts
30+
);
31+
if (!r1.ok) throw new Error('HTTP ' + r1.status + ' - make sure you are logged in to Instagram');
32+
const d1 = await r1.json();
33+
const userId = d1?.data?.user?.id;
34+
if (!userId) throw new Error('User not found: ' + username);
35+
36+
const r2 = await fetch(
37+
'https://www.instagram.com/api/v1/friendships/' + userId + '/followers/?count=' + limit,
38+
opts
39+
);
40+
if (!r2.ok) throw new Error('Failed to fetch followers: HTTP ' + r2.status);
41+
const d2 = await r2.json();
42+
return (d2?.users || []).slice(0, limit).map((u, i) => ({
43+
rank: i + 1,
44+
username: u.username || '',
45+
name: u.full_name || '',
46+
verified: u.is_verified ? 'Yes' : 'No',
47+
private: u.is_private ? 'Yes' : 'No',
48+
}));
49+
})()
50+
51+
columns: [rank, username, name, verified, private]

src/clis/instagram/following.yaml

Lines changed: 51 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,51 @@
1+
site: instagram
2+
name: following
3+
description: List accounts an Instagram user is following
4+
domain: www.instagram.com
5+
6+
args:
7+
username:
8+
type: str
9+
required: true
10+
positional: true
11+
description: Instagram username
12+
limit:
13+
type: int
14+
default: 20
15+
description: Number of accounts
16+
17+
pipeline:
18+
- navigate: https://www.instagram.com
19+
20+
- evaluate: |
21+
(async () => {
22+
const username = ${{ args.username | json }};
23+
const limit = ${{ args.limit }};
24+
const headers = { 'X-IG-App-ID': '936619743392459' };
25+
const opts = { credentials: 'include', headers };
26+
27+
const r1 = await fetch(
28+
'https://www.instagram.com/api/v1/users/web_profile_info/?username=' + encodeURIComponent(username),
29+
opts
30+
);
31+
if (!r1.ok) throw new Error('HTTP ' + r1.status + ' - make sure you are logged in to Instagram');
32+
const d1 = await r1.json();
33+
const userId = d1?.data?.user?.id;
34+
if (!userId) throw new Error('User not found: ' + username);
35+
36+
const r2 = await fetch(
37+
'https://www.instagram.com/api/v1/friendships/' + userId + '/following/?count=' + limit,
38+
opts
39+
);
40+
if (!r2.ok) throw new Error('Failed to fetch following: HTTP ' + r2.status);
41+
const d2 = await r2.json();
42+
return (d2?.users || []).slice(0, limit).map((u, i) => ({
43+
rank: i + 1,
44+
username: u.username || '',
45+
name: u.full_name || '',
46+
verified: u.is_verified ? 'Yes' : 'No',
47+
private: u.is_private ? 'Yes' : 'No',
48+
}));
49+
})()
50+
51+
columns: [rank, username, name, verified, private]

src/clis/instagram/profile.yaml

Lines changed: 42 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,42 @@
1+
site: instagram
2+
name: profile
3+
description: Get Instagram user profile info
4+
domain: www.instagram.com
5+
6+
args:
7+
username:
8+
type: str
9+
required: true
10+
positional: true
11+
description: Instagram username
12+
13+
pipeline:
14+
- navigate: https://www.instagram.com
15+
16+
- evaluate: |
17+
(async () => {
18+
const username = ${{ args.username | json }};
19+
const res = await fetch(
20+
'https://www.instagram.com/api/v1/users/web_profile_info/?username=' + encodeURIComponent(username),
21+
{
22+
credentials: 'include',
23+
headers: { 'X-IG-App-ID': '936619743392459' }
24+
}
25+
);
26+
if (!res.ok) throw new Error('HTTP ' + res.status + ' - make sure you are logged in to Instagram');
27+
const data = await res.json();
28+
const u = data?.data?.user;
29+
if (!u) throw new Error('User not found: ' + username);
30+
return [{
31+
username: u.username,
32+
name: u.full_name || '',
33+
bio: (u.biography || '').replace(/\n/g, ' ').substring(0, 120),
34+
followers: u.edge_followed_by?.count ?? 0,
35+
following: u.edge_follow?.count ?? 0,
36+
posts: u.edge_owner_to_timeline_media?.count ?? 0,
37+
verified: u.is_verified ? 'Yes' : 'No',
38+
url: 'https://www.instagram.com/' + u.username,
39+
}];
40+
})()
41+
42+
columns: [username, name, followers, following, posts, verified, bio]

0 commit comments

Comments
 (0)