import crypto from 'crypto' ; import { Buffer } from 'buffer' ; import * as Sentry from '@sentry/node' ; import { NowRequest , NowResponse } from '@vercel/node' ; import { sendMessage } from '../_utils/telegram/bot-methods' ; import messages from '../_utils/messages' ; import getRawBody from '../_utils/http' ; import config from '../_utils/config' ; const { TWITTER_CONSUMER_SECRET , MAIN_GROUP_ID , NODE_ENV , SENTRY_DSN , } = process . env ; interface TweetInterface { in_reply_to_status_id : number ; id_str : string ; text : string ; user : { screen_name : string ; } ; retweeted_status : { text : string ; } ; is_quote_status : boolean ; } Sentry . init ( { dsn : SENTRY_DSN } ) ; const isReply = ( tweet ) : boolean => tweet . in_reply_to_status_id !== null ; const isRt = ( tweet ) : boolean => tweet . retweeted_status !== undefined ; const isQuote = ( tweet ) : boolean => tweet . is_quote_status ; const isNewTweet = ( tweet ) : boolean => ! isReply ( tweet ) && ! isRt ( tweet ) && ! isQuote ( tweet ) ; const isTweetFromUs = ( tweet ) : boolean => tweet . user . screen_name === config . twitterHandle ; const handleTweets = async ( tweets = [ ] ) => { const promises = [ ] ; tweets . forEach ( tweet => { if ( MAIN_GROUP_ID && isTweetFromUs ( tweet ) && isNewTweet ( tweet ) ) { const tweetUrl = `https://twitter.com/${ tweet . user . screen_name }/status/${ tweet . id_str }` ; const promise = sendMessage ( { chatId : Number ( MAIN_GROUP_ID ) , text : messages . newTweet . replace ( '#{tweetText}' , tweet . text ) . replace ( '#{tweetUrl}' , tweetUrl ) , } ) ; promises . push ( promise ) ; } } ) ; await Promise . all ( promises ) ; } ; description see https const validateSignature = ( headers , body ) => { const signatureHeaderName = 'x-twitter-webhooks-signature' ; if ( typeof headers [ signatureHeaderName ] === 'undefined' ) { throw new TypeError ( `validateSignature: header ${ signatureHeaderName } not found` ) ; } const signature = `sha256=${ crypto . createHmac ( 'sha256' , TWITTER_CONSUMER_SECRET ?? '' ) . update ( body ) . digest ( 'base64' ) }` ; return crypto . timingSafeEqual ( Buffer . from ( headers [ signatureHeaderName ] ) , Buffer . from ( signature ) ) ; } ; see https const getChallengeResponse = ( crcToken ) => crypto . createHmac ( 'sha256' , TWITTER_CONSUMER_SECRET ?? '' ) . update ( crcToken ) . digest ( 'base64' ) ; export default async ( request , response ) : Promise < void > => { try { if ( request . method === 'GET' ) { const crcToken = request . query . crc_token . toString ( ) ; if ( crcToken ) { const hash = getChallengeResponse ( crcToken ) ; response . status ( 200 ) . json ( { response_token : `sha256=${ hash }` , } ) ; } else { response . status ( 400 ) . send ( 'crc_token missing from request' ) ; } } else if ( request . method === 'POST' ) { const rawBody = await getRawBody ( request ) ; if ( validateSignature ( request . headers , rawBody ) ) { const newTweets = request . body . tweet_create_events ; await handleTweets ( newTweets ) ; response . status ( 200 ) . send ( 'ok' ) ; } else { response . status ( 400 ) . send ( 'signature is not valid' ) ; } } else { response . status ( 401 ) . send ( 'Error: Unauthorized' ) ; } } catch ( error ) { if ( NODE_ENV === 'development' ) { console . error ( error ) ; } else { Sentry . captureException ( error ) ; } response . status ( 400 ) . send ( 'not ok' ) ; } } ;