@@ -24,6 +24,11 @@ import {
2424 updateStatusRefreshToken ,
2525} from '../Data/supabase'
2626
27+ // Twitter Bot Feature Toggles - Modify these to enable/disable functionality
28+ const TWITTER_LISTENING_ENABLED = false // Enables listening and responding to tweet mentions
29+ const TWITTER_SALE_POSTING_ENABLED = false // Enables posting sale tweets
30+ const TWITTER_MINT_POSTING_ENABLED = false // Enables posting mint tweets
31+
2732const TWITTER_MEDIA_BYTE_LIMIT = 5242880
2833// Search rate limit is 60 queries per 15 minutes - shortest interval is 15 secs (though we should keep it a bit longer)
2934const SEARCH_INTERVAL_MS = 20000
@@ -36,36 +41,59 @@ const prod = process.env.ARTBOT_IS_PROD
3641const ARTBOT_TWITTER_HANDLE = 'artbotartbot'
3742const STATUS_TWITTER_HANDLE = 'ArtbotStatus'
3843
44+ /**
45+ * TwitterBot handles Twitter API interactions for posting tweets and listening/responding to mentions
46+ *
47+ * Feature Toggles (constants at top of file):
48+ * - TWITTER_LISTENING_ENABLED: Controls whether the bot listens for and responds to tweets (default: false)
49+ * - TWITTER_SALE_POSTING_ENABLED: Controls whether the bot posts sale tweets (default: false)
50+ * - TWITTER_MINT_POSTING_ENABLED: Controls whether the bot posts mint tweets (default: false)
51+ *
52+ * The bot can be used for posting functionality (sales, mints, etc.) even when listener is disabled
53+ */
3954export class TwitterBot {
40- twitterClient : TwitterApi
55+ twitterClient ? : TwitterApi
4156 twitterStatusAccount ?: TwitterApi
42- lastTweetId : string
57+ lastTweetId ? : string
4358 intervalId ?: NodeJS . Timeout
4459
4560 constructor ( {
4661 appKey,
4762 appSecret,
4863 accessToken,
4964 accessSecret,
50- listener,
5165 } : {
5266 appKey : string
5367 appSecret : string
5468 accessToken : string
5569 accessSecret : string
56- listener ?: boolean
5770 } ) {
5871 this . lastTweetId = ''
59- if ( listener && process . env . TWITTER_ENABLED === 'true' ) {
60- console . log ( 'Starting Twitter listener' )
61- this . startSearchAndReplyRoutine ( )
72+
73+ if ( ! appKey || ! appSecret || ! accessToken || ! accessSecret ) {
74+ console . warn (
75+ 'Twitter credentials are missing - not initializing TwitterBot'
76+ )
77+ return
6278 }
79+
80+ // Initialize Twitter client for posting capabilities
6381 this . twitterClient = new TwitterApi ( {
6482 appKey,
6583 appSecret,
6684 accessToken,
6785 accessSecret,
6886 } )
87+
88+ // Only start listener if enabled via constant
89+ if ( TWITTER_LISTENING_ENABLED ) {
90+ console . log ( 'Starting Twitter listener' )
91+ this . startSearchAndReplyRoutine ( )
92+ } else {
93+ console . log (
94+ 'Twitter listener disabled via TWITTER_LISTENING_ENABLED constant'
95+ )
96+ }
6997 }
7098
7199 async startSearchAndReplyRoutine ( ) {
@@ -101,7 +129,7 @@ export class TwitterBot {
101129
102130 const query = `(to:${ ARTBOT_TWITTER_HANDLE } OR @${ ARTBOT_TWITTER_HANDLE } ) -is:retweet -has:links has:mentions -from:${ STATUS_TWITTER_HANDLE } -from:${ ARTBOT_TWITTER_HANDLE } `
103131 const devQuery = `to:ArtbotTesting from:ArtbotTesting`
104- artbotTweets = await this . twitterClient . v2 . search ( {
132+ artbotTweets = await this . twitterClient ? .v2 . search ( {
105133 query : prod ? query : devQuery ,
106134 since_id : this . lastTweetId ,
107135 } )
@@ -150,6 +178,7 @@ export class TwitterBot {
150178 }
151179 this . updateLastTweetId ( artbotTweets . meta . newest_id )
152180 }
181+
153182 async updateLastTweetId ( tweetId : string ) {
154183 if ( tweetId === this . lastTweetId ) {
155184 return
@@ -221,7 +250,7 @@ export class TwitterBot {
221250 console . log ( `Replying to ${ tweet . id } with ${ artBlocksData . name } ` )
222251 for ( let i = 0 ; i < NUM_RETRIES ; i ++ ) {
223252 try {
224- await this . twitterClient . v2 . reply ( tweetMessage , tweet . id , {
253+ await this . twitterClient ? .v2 . reply ( tweetMessage , tweet . id , {
225254 media : {
226255 media_ids : [ media_id ] ,
227256 } ,
@@ -280,9 +309,12 @@ export class TwitterBot {
280309 console . log ( 'Uploading media to twitter...' , assetUrl )
281310 for ( let i = 0 ; i < NUM_RETRIES ; i ++ ) {
282311 try {
283- const mediaId = await this . twitterClient . v1 . uploadMedia ( buff , {
312+ const mediaId = await this . twitterClient ? .v1 . uploadMedia ( buff , {
284313 mimeType : this . getMimeType ( assetUrl ) ,
285314 } )
315+ if ( ! mediaId ) {
316+ throw new Error ( 'No media id returned' )
317+ }
286318 return mediaId
287319 } catch ( err ) {
288320 console . log ( `Error uploading ${ assetUrl } :` , err )
@@ -303,7 +335,15 @@ export class TwitterBot {
303335 }
304336 }
305337
306- async tweetArtblock ( artBlock : Mint ) {
338+ async _tweetMint ( artBlock : Mint ) {
339+ // Check if Twitter mint posting is enabled
340+ if ( ! TWITTER_MINT_POSTING_ENABLED ) {
341+ console . log (
342+ 'Twitter mint posting disabled via TWITTER_MINT_POSTING_ENABLED constant'
343+ )
344+ return
345+ }
346+
307347 const assetUrl = artBlock . image
308348 if ( ! artBlock . image ) {
309349 console . error ( 'No artblock image defined' , JSON . stringify ( artBlock ) )
@@ -324,7 +364,7 @@ export class TwitterBot {
324364 } . \n\n${ artBlock . artblocksUrl + TWITTER_PROJECTBOT_UTM } `
325365 console . log ( `Tweeting ${ tweetText } ` )
326366
327- const tweetRes = await this . twitterClient . v2 . tweet ( tweetText , {
367+ const tweetRes = await this . twitterClient ? .v2 . tweet ( tweetText , {
328368 text : tweetText ,
329369 media : { media_ids : [ mediaId ] } ,
330370 } )
@@ -334,9 +374,9 @@ export class TwitterBot {
334374 }
335375 }
336376
337- async sendToTwitter ( mint : Mint ) {
377+ async tweetMint ( mint : Mint ) {
338378 try {
339- await this . tweetArtblock ( mint )
379+ await this . _tweetMint ( mint )
340380 } catch ( e ) {
341381 console . error ( 'Error posting to Twitter: ' , e )
342382 }
@@ -361,6 +401,14 @@ export class TwitterBot {
361401 }
362402
363403 async sendStatusMessage ( message : string , replyId ?: string ) {
404+ // Check if Twitter listening is enabled (status messages are mainly for user interactions)
405+ if ( ! TWITTER_LISTENING_ENABLED ) {
406+ console . log (
407+ 'Twitter status messaging disabled via TWITTER_LISTENING_ENABLED constant'
408+ )
409+ return
410+ }
411+
364412 if ( ! this . twitterStatusAccount ) {
365413 await this . signIntoStatusAccount ( )
366414 }
@@ -373,4 +421,94 @@ export class TwitterBot {
373421 }
374422 await this . twitterStatusAccount ?. v2 . tweet ( message )
375423 }
424+
425+ async tweetSale ( saleData : {
426+ tokenName : string
427+ projectName : string
428+ artist : string
429+ salePrice : number
430+ currency : string
431+ buyer : string
432+ seller : string
433+ assetUrl : string
434+ tokenUrl : string
435+ platform ?: string
436+ } ) {
437+ // Check if Twitter sale posting is enabled
438+ if ( ! TWITTER_SALE_POSTING_ENABLED ) {
439+ console . log (
440+ 'Twitter sale posting disabled via TWITTER_SALE_POSTING_ENABLED constant'
441+ )
442+ return
443+ }
444+
445+ try {
446+ console . log ( `Tweeting sale for ${ saleData . tokenName } ` )
447+
448+ // Upload the token image to Twitter
449+ let media_id : string
450+ try {
451+ media_id = await this . uploadMedia ( saleData . assetUrl )
452+ } catch ( error ) {
453+ console . error ( `Error uploading media for sale tweet:` , error )
454+ return
455+ }
456+
457+ // Get ENS names for buyer and seller
458+ const buyerText = await ensOrAddress ( saleData . buyer )
459+ const sellerText = await ensOrAddress ( saleData . seller )
460+
461+ // Format the platform text if provided
462+ const platformText = saleData . platform ? ` on ${ saleData . platform } ` : ''
463+
464+ // Construct the tweet message
465+ const tweetMessage = `🔥 SALE: ${ saleData . tokenName } by ${ saleData . artist }
466+
467+ 💰 ${ saleData . salePrice } ${ saleData . currency } ${ platformText }
468+ 👤 Sold by ${ sellerText }
469+ 🛒 Bought by ${ buyerText }
470+
471+ ${ saleData . tokenUrl + TWITTER_PROJECTBOT_UTM } `
472+
473+ console . log ( `Posting sale tweet: ${ saleData . tokenName } ` )
474+
475+ // Post the tweet with retry logic
476+ for ( let i = 0 ; i < NUM_RETRIES ; i ++ ) {
477+ try {
478+ await this . twitterClient ?. v2 . tweet ( tweetMessage , {
479+ media : {
480+ media_ids : [ media_id ] ,
481+ } ,
482+ } )
483+ console . log ( `Successfully tweeted sale for ${ saleData . tokenName } ` )
484+ return
485+ } catch ( error ) {
486+ if ( error instanceof ApiResponseError && error . rateLimit ) {
487+ console . log (
488+ `Rate limit hit on sale tweet: \nLimit: ${ error . rateLimit . limit } \nRemaining: ${ error . rateLimit . remaining } \nReset: ${ error . rateLimit . reset } `
489+ )
490+ const reset = new Date ( error . rateLimit . reset * 1000 )
491+ const now = new Date ( )
492+ const diff = reset . getTime ( ) - now . getTime ( )
493+ const diffMinutes = Math . ceil ( diff / 60000 )
494+ try {
495+ await this . sendStatusMessage (
496+ `Sale tweet rate limited :( Will retry in ${ diffMinutes } minutes`
497+ )
498+ } catch ( e ) {
499+ console . error ( 'Error sending rate limit status message:' , e )
500+ }
501+ return
502+ } else {
503+ console . log ( `Error posting sale tweet:` , error )
504+ console . log ( `Retrying (attempt ${ i + 1 } of ${ NUM_RETRIES } )...` )
505+ await delay ( RETRY_DELAY_MS )
506+ }
507+ }
508+ }
509+ console . error ( 'Failed to post sale tweet after all retry attempts' )
510+ } catch ( error ) {
511+ console . error ( 'Error in tweetSale:' , error )
512+ }
513+ }
376514}
0 commit comments