Skip to content

Commit 9307f66

Browse files
committed
add oembed, keyword search, and mentions
1 parent 564b542 commit 9307f66

6 files changed

Lines changed: 273 additions & 14 deletions

File tree

src/index.js

Lines changed: 157 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -24,6 +24,7 @@ const FIELD__ALT_TEXT = 'alt_text';
2424
const FIELD__ERROR_MESSAGE = 'error_message';
2525
const FIELD__FOLLOWERS_COUNT = 'followers_count';
2626
const FIELD__HIDE_STATUS = 'hide_status';
27+
const FIELD__ID = 'id';
2728
const FIELD__IS_REPLY = 'is_reply';
2829
const FIELD__LIKES = 'likes';
2930
const FIELD__LINK_ATTACHMENT_URL = 'link_attachment_url';
@@ -55,6 +56,7 @@ const PARAMS__FIELDS = 'fields';
5556
const PARAMS__HIDE = 'hide';
5657
const PARAMS__LINK_ATTACHMENT = 'link_attachment';
5758
const PARAMS__METRIC = 'metric';
59+
const PARAMS__Q = 'q';
5860
const PARAMS__QUOTA_USAGE = 'quota_usage';
5961
const PARAMS__QUOTE_POST_ID = 'quote_post_id';
6062
const PARAMS__REDIRECT_URI = 'redirect_uri';
@@ -65,6 +67,7 @@ const PARAMS__REPLY_TO_ID = 'reply_to_id';
6567
const PARAMS__RESPONSE_TYPE = 'response_type';
6668
const PARAMS__RETURN_URL = 'return_url';
6769
const PARAMS__SCOPE = 'scope';
70+
const PARAMS__SEARCH_TYPE = 'search_type';
6871
const PARAMS__TEXT = 'text';
6972

7073
// Read variables from environment
@@ -78,8 +81,13 @@ const {
7881
GRAPH_API_VERSION,
7982
INITIAL_ACCESS_TOKEN,
8083
INITIAL_USER_ID,
84+
REJECT_UNAUTHORIZED,
8185
} = process.env;
8286

87+
const agent = new https.Agent({
88+
rejectUnauthorized: REJECT_UNAUTHORIZED !== 'false',
89+
});
90+
8391
const GRAPH_API_BASE_URL = 'https://graph.threads.net/' +
8492
(GRAPH_API_VERSION ? GRAPH_API_VERSION + '/' : '');
8593
const AUTHORIZATION_BASE_URL = 'https://www.threads.net';
@@ -93,7 +101,9 @@ const SCOPES = [
93101
'threads_content_publish',
94102
'threads_manage_insights',
95103
'threads_manage_replies',
96-
'threads_read_replies'
104+
'threads_read_replies',
105+
'threads_keyword_search',
106+
'threads_manage_mentions',
97107
];
98108

99109
app.use(express.static('public'));
@@ -166,6 +176,7 @@ app.get('/callback', async (req, res) => {
166176
headers: {
167177
'Content-Type': 'application/x-www-form-urlencoded',
168178
},
179+
httpsAgent: agent,
169180
});
170181
req.session.access_token = response.data.access_token;
171182
res.redirect('/account');
@@ -188,7 +199,7 @@ app.get('/account', loggedInUserChecker, async (req, res) => {
188199

189200
let userDetails = {};
190201
try {
191-
const response = await axios.get(getUserDetailsUrl);
202+
const response = await axios.get(getUserDetailsUrl, { httpsAgent: agent });
192203
userDetails = response.data;
193204

194205
// This value is not currently used but it may come handy in the future
@@ -230,7 +241,7 @@ app.get('/userInsights', loggedInUserChecker, async (req, res) => {
230241

231242
let data = [];
232243
try {
233-
const queryResponse = await axios.get(queryThreadUrl);
244+
const queryResponse = await axios.get(queryThreadUrl, { httpsAgent: agent });
234245
data = queryResponse.data;
235246
} catch (e) {
236247
console.error(e?.response?.data?.error?.message ?? e.message);
@@ -271,7 +282,7 @@ app.get('/publishingLimit', loggedInUserChecker, async (req, res) => {
271282

272283
let data = [];
273284
try {
274-
const queryResponse = await axios.get(publishingLimitUrl);
285+
const queryResponse = await axios.get(publishingLimitUrl, { httpsAgent: agent });
275286
data = queryResponse.data;
276287
} catch (e) {
277288
console.error(e?.response?.data?.error?.message ?? e.message);
@@ -381,7 +392,7 @@ app.post('/upload', upload.array(), async (req, res) => {
381392

382393
const postThreadsUrl = buildGraphAPIURL(`me/threads`, params, req.session.access_token);
383394
try {
384-
const postResponse = await axios.post(postThreadsUrl, {});
395+
const postResponse = await axios.post(postThreadsUrl, {}, { httpsAgent: agent });
385396
const containerId = postResponse.data.id;
386397
res.json({
387398
id: containerId,
@@ -414,7 +425,7 @@ app.get('/container/status/:containerId', loggedInUserChecker, async (req, res)
414425
}, req.session.access_token);
415426

416427
try {
417-
const queryResponse = await axios.get(getContainerStatusUrl);
428+
const queryResponse = await axios.get(getContainerStatusUrl, { httpsAgent: agent });
418429
res.json(queryResponse.data);
419430
} catch (e) {
420431
console.error(e.message);
@@ -432,7 +443,7 @@ app.post('/publish', upload.array(), async (req, res) => {
432443
}, req.session.access_token);
433444

434445
try {
435-
const postResponse = await axios.post(publishThreadsUrl);
446+
const postResponse = await axios.post(publishThreadsUrl, { httpsAgent: agent });
436447
const threadId = postResponse.data.id;
437448
res.json({
438449
id: threadId,
@@ -466,7 +477,7 @@ app.get('/threads/:threadId', loggedInUserChecker, async (req, res) => {
466477
}, req.session.access_token);
467478

468479
try {
469-
const queryResponse = await axios.get(queryThreadUrl);
480+
const queryResponse = await axios.get(queryThreadUrl, { httpsAgent: agent });
470481
data = queryResponse.data;
471482
} catch (e) {
472483
console.error(e?.response?.data?.error?.message ?? e.message);
@@ -506,7 +517,7 @@ app.get('/threads', loggedInUserChecker, async (req, res) => {
506517
const queryThreadsUrl = buildGraphAPIURL(`me/threads`, params, req.session.access_token);
507518

508519
try {
509-
const queryResponse = await axios.get(queryThreadsUrl);
520+
const queryResponse = await axios.get(queryThreadsUrl, { httpsAgent: agent });
510521
threads = queryResponse.data.data;
511522

512523
if (queryResponse.data.paging) {
@@ -557,7 +568,7 @@ app.get('/replies', loggedInUserChecker, async (req, res) => {
557568
const queryRepliesUrl = buildGraphAPIURL(`me/replies`, params, req.session.access_token);
558569

559570
try {
560-
const queryResponse = await axios.get(queryRepliesUrl);
571+
const queryResponse = await axios.get(queryRepliesUrl, { httpsAgent: agent });
561572
threads = queryResponse.data.data;
562573

563574
if (queryResponse.data.paging) {
@@ -602,7 +613,7 @@ app.post('/manage_reply/:replyId', upload.array(), async (req, res) => {
602613
const hideReplyUrl = buildGraphAPIURL(`${replyId}/manage_reply`, {}, req.session.access_token);
603614

604615
try {
605-
response = await axios.post(hideReplyUrl, params);
616+
response = await axios.post(hideReplyUrl, params, { httpsAgent: agent });
606617
}
607618
catch (e) {
608619
console.error(e?.message);
@@ -639,7 +650,7 @@ app.get('/threads/:threadId/insights', loggedInUserChecker, async (req, res) =>
639650

640651
let data = [];
641652
try {
642-
const queryResponse = await axios.get(queryThreadUrl);
653+
const queryResponse = await axios.get(queryThreadUrl, { httpsAgent: agent });
643654
data = queryResponse.data;
644655
} catch (e) {
645656
console.error(e?.response?.data?.error?.message ?? e.message);
@@ -660,6 +671,110 @@ app.get('/threads/:threadId/insights', loggedInUserChecker, async (req, res) =>
660671
});
661672
});
662673

674+
app.get('/mentions', loggedInUserChecker, async (req, res) => {
675+
const { before, after, limit } = req.query;
676+
const params = {
677+
[PARAMS__FIELDS]: [
678+
FIELD__USERNAME,
679+
FIELD__TEXT,
680+
FIELD__MEDIA_TYPE,
681+
FIELD__MEDIA_URL,
682+
FIELD__PERMALINK,
683+
FIELD__TIMESTAMP,
684+
FIELD__REPLY_AUDIENCE,
685+
FIELD__ALT_TEXT,
686+
].join(','),
687+
limit: limit ?? DEFAULT_THREADS_QUERY_LIMIT,
688+
};
689+
if (before) {
690+
params.before = before;
691+
}
692+
if (after) {
693+
params.after = after;
694+
}
695+
696+
const queryMentionsUrl = buildGraphAPIURL(`me/mentions`, params, req.session.access_token);
697+
698+
let threads = [];
699+
let paging = {};
700+
701+
try {
702+
const queryResponse = await axios.get(queryMentionsUrl, { httpsAgent: agent });
703+
threads = queryResponse.data.data;
704+
705+
if (queryResponse.data.paging) {
706+
const { next, previous } = queryResponse.data.paging;
707+
708+
if (next) {
709+
paging.nextUrl = getCursorUrlFromGraphApiPagingUrl(req, next);
710+
}
711+
712+
if (previous) {
713+
paging.previousUrl = getCursorUrlFromGraphApiPagingUrl(req, previous);
714+
}
715+
}
716+
} catch (e) {
717+
console.error(e?.response?.data?.error?.message ?? e.message);
718+
}
719+
720+
res.render('mentions', {
721+
title: 'Mentions',
722+
threads,
723+
paging,
724+
});
725+
});
726+
727+
app.get('/keywordSearch', loggedInUserChecker, async (req, res) => {
728+
const { keyword, searchType } = req.query;
729+
730+
if (!keyword) {
731+
return res.render('keyword_search', {
732+
title: 'Search for Threads',
733+
});
734+
}
735+
736+
const params = {
737+
[PARAMS__Q]: keyword,
738+
[PARAMS__SEARCH_TYPE]: searchType,
739+
[PARAMS__FIELDS]: [
740+
FIELD__USERNAME,
741+
FIELD__ID,
742+
FIELD__TIMESTAMP,
743+
FIELD__MEDIA_TYPE,
744+
FIELD__TEXT,
745+
FIELD__PERMALINK,
746+
FIELD__REPLY_AUDIENCE,
747+
].join(',')
748+
};
749+
750+
const keywordSearchUrl = buildGraphAPIURL(`keyword_search`, params, req.session.access_token);
751+
752+
let threads = [];
753+
let paging = {};
754+
755+
try {
756+
const response = await axios.get(keywordSearchUrl, { httpsAgent: agent });
757+
threads = response.data.data;
758+
759+
if (response.data.paging) {
760+
const { next, previous } = response.data.paging;
761+
762+
if (next) {
763+
paging.nextUrl = getCursorUrlFromGraphApiPagingUrl(req, next);
764+
}
765+
}
766+
} catch (e) {
767+
console.error(e?.response?.data?.error?.message ?? e.message);
768+
}
769+
770+
return res.render('keyword_search', {
771+
title: 'Search for Threads',
772+
threads,
773+
paging,
774+
resultsTitle: `${searchType} results for '${keyword}'`,
775+
});
776+
});
777+
663778
// Logout route to kill the session
664779
app.get('/logout', (req, res) => {
665780
if (req.session) {
@@ -675,6 +790,35 @@ app.get('/logout', (req, res) => {
675790
}
676791
});
677792

793+
app.get('/oEmbed', async (req, res) => {
794+
const { url } = req.query;
795+
if (!url) {
796+
return res.render('oembed', {
797+
title: 'Embed Threads',
798+
});
799+
}
800+
801+
const oEmbedUrl = buildGraphAPIURL(`oembed`, {
802+
url,
803+
}, `TH|${APP_ID}|${API_SECRET}`);
804+
805+
let html = '<p>Unable to embed</p>';
806+
try {
807+
const response = await axios.get(oEmbedUrl, { httpsAgent: agent });
808+
if (response.data?.html) {
809+
html = response.data.html;
810+
}
811+
} catch (e) {
812+
console.error(e?.response?.data?.error?.message ?? e.message);
813+
}
814+
815+
return res.render('oembed', {
816+
title: 'Embed Threads',
817+
html,
818+
url,
819+
});
820+
});
821+
678822
https
679823
.createServer({
680824
key: fs.readFileSync(path.join(__dirname, '../'+ HOST +'-key.pem')),
@@ -817,7 +961,7 @@ async function showReplies(req, res, isTopLevel) {
817961
const queryThreadsUrl = buildGraphAPIURL(`${threadId}/${repliesOrConversation}`, params, req.session.access_token);
818962

819963
try {
820-
const queryResponse = await axios.get(queryThreadsUrl);
964+
const queryResponse = await axios.get(queryThreadsUrl, { httpsAgent: agent });
821965
replies = queryResponse.data.data;
822966

823967
if (queryResponse.data.paging) {

views/account.pug

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -14,5 +14,7 @@ block content
1414
button(onclick="location.href='/upload'") Publish
1515
button(onclick="location.href='/threads'") My Threads
1616
button(onclick="location.href='/replies'") My Replies
17-
button(onclick="location.href='/userInsights'") Insights
17+
button(onclick="location.href='/mentions'") My Mentions
18+
button(onclick="location.href='/keywordSearch'") Search for Threads
19+
button(onclick="location.href='/userInsights'") My Insights
1820
button(onclick="location.href='/publishingLimit'") Publishing Limit

views/index.pug

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,12 @@
11
extends layout.pug
22

33
block content
4+
style(type="text/css").
5+
#oembed {
6+
margin-top: 40px;
7+
}
48
.button-group
59
button(onclick="location.href='/login'") Log In
610
button(onclick="location.href='/logout'") Log Out
11+
12+
button#oembed(onclick="location.href='/oEmbed'") Embed Threads

views/keyword_search.pug

Lines changed: 42 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,42 @@
1+
extends layout_with_account
2+
3+
block content
4+
form(action='/keywordSearch' id='form' method='GET')
5+
textarea(placeholder='Enter a search query' name='keyword' autocomplete='off')
6+
7+
label(for="search-type") Search for top or recent Threads?
8+
select#search-type(name='searchType')
9+
option(value="TOP" selected) Top
10+
option(value="RECENT") Recent
11+
12+
input(type='submit' value='Search')
13+
14+
if threads
15+
h2=resultsTitle
16+
table.threads-list
17+
thead
18+
tr
19+
th Username
20+
th ID
21+
th Created On
22+
th Media Type
23+
th Text
24+
th Permalink
25+
th Reply Audience
26+
tbody
27+
each thread in threads
28+
tr.threads-list-item
29+
td.thread-username=thread.username
30+
td.thread-id
31+
a(href=`/threads/${thread.id}`)=thread.id
32+
td.thread-timestamp=thread.timestamp
33+
td.thread-type=thread.media_type
34+
td.thread-text=thread.text
35+
td.thread-permalink
36+
a(href=thread.permalink target='_blank') View on Threads
37+
td.thread-reply-audience=thread.reply_audience
38+
39+
div.paging
40+
if paging.nextUrl
41+
div.paging-next
42+
a(href=paging.nextUrl) Next

0 commit comments

Comments
 (0)