@@ -24,6 +24,7 @@ const FIELD__ALT_TEXT = 'alt_text';
2424const FIELD__ERROR_MESSAGE = 'error_message' ;
2525const FIELD__FOLLOWERS_COUNT = 'followers_count' ;
2626const FIELD__HIDE_STATUS = 'hide_status' ;
27+ const FIELD__ID = 'id' ;
2728const FIELD__IS_REPLY = 'is_reply' ;
2829const FIELD__LIKES = 'likes' ;
2930const FIELD__LINK_ATTACHMENT_URL = 'link_attachment_url' ;
@@ -55,6 +56,7 @@ const PARAMS__FIELDS = 'fields';
5556const PARAMS__HIDE = 'hide' ;
5657const PARAMS__LINK_ATTACHMENT = 'link_attachment' ;
5758const PARAMS__METRIC = 'metric' ;
59+ const PARAMS__Q = 'q' ;
5860const PARAMS__QUOTA_USAGE = 'quota_usage' ;
5961const PARAMS__QUOTE_POST_ID = 'quote_post_id' ;
6062const PARAMS__REDIRECT_URI = 'redirect_uri' ;
@@ -65,6 +67,7 @@ const PARAMS__REPLY_TO_ID = 'reply_to_id';
6567const PARAMS__RESPONSE_TYPE = 'response_type' ;
6668const PARAMS__RETURN_URL = 'return_url' ;
6769const PARAMS__SCOPE = 'scope' ;
70+ const PARAMS__SEARCH_TYPE = 'search_type' ;
6871const 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+
8391const GRAPH_API_BASE_URL = 'https://graph.threads.net/' +
8492 ( GRAPH_API_VERSION ? GRAPH_API_VERSION + '/' : '' ) ;
8593const 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
99109app . 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
664779app . 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+
678822https
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 ) {
0 commit comments