Posting on Twitter/X

Share your advanced PureBasic knowledge/code with the community.
Seymour Clufley
Addict
Addict
Posts: 1265
Joined: Wed Feb 28, 2007 9:13 am
Location: London

Posting on Twitter/X

Post by Seymour Clufley »

This is a small library for using the "free tier" abilities of the Twitter/X API. This includes posting tweets, posting tweets with images, gifs or videos, posting polls, and replying to and quoting tweets. In order to use it, you will need to create an app on Twitter, then get its keys and use them as shown in the demo code below.

Thanks to Infratec for his binary HMAC code.

Note: this requires my MultipartFormDataRequests.pbi.

TwitterX.pbi:

Code: Select all

XIncludeFile "MultipartFormDataRequests.pbi"


Procedure.b R(t.s)
  MessageRequester("Report",t,0)
EndProcedure

Procedure.d Defeat(a.d,b.d)
  
  If a<b
      ProcedureReturn a
  Else
      ProcedureReturn b
  EndIf
  
EndProcedure

Macro EnsureThisNotEnd(etnt,endd)
	
	If Right(etnt,Len(endd)) = endd
		etnt = Left(etnt,Len(etnt)-Len(endd))
	EndIf
	
EndMacro

Procedure.s EnsureEnd(t.s,endd.s)
	
	If endd
		If Right(t,Len(endd)) <> endd
			t = t+endd
		EndIf
	EndIf
	
	ProcedureReturn t
	
EndProcedure

#d1 = "|"




Procedure.s Twitter_FormatURLParameter(t.s)
  t = ReplaceString(t,":","%3A")
  t = ReplaceString(t,"?","%3F")
  t = ReplaceString(t,"=","%3D")
  t = ReplaceString(t,"/","%2F")
  ProcedureReturn t
EndProcedure

Procedure.s Twitter_PrepareText(t.s)
  t = ReplaceString(t,"<br>",Chr(13))
  t = EscapeString(t)
  ProcedureReturn t
EndProcedure

Macro Twitter_TweetURL(tu_at,tu_tid)
  "https://x.com/"+tu_at+"/status/"+tu_tid
EndMacro


Procedure.s DelimitedArrayToJSONStringArray(arr.s,del.s="|")
  #splitter = Chr(34)+","+Chr(34)
  arr = ReplaceString(arr,del,#splitter)
  EnsureThisNotEnd(arr,#splitter)
  arr = Trim(arr,c34)
  arr = "["+c34+arr+c34+"]"
  ProcedureReturn arr
EndProcedure

Procedure StringHMAC_HexStringToBin(*HexString.Ascii, *Destination.Ascii)
  
  Protected HighNibble, LowNibble
  
  
  If *HexString And *Destination
    While *HexString\a
      
      HighNibble = *HexString\a - '0'
      If HighNibble > $f
        HighNibble - $27
      EndIf
      *HexString + 2
      
      LowNibble = *HexString\a - '0'
      If LowNibble > $f
        LowNibble - $27
      EndIf
      *HexString + 2
      
      *Destination\a = HighNibble << 4 | LowNibble
      *Destination + 1
      ;Debug Hex(HighNibble << 4 | LowNibble, #PB_Ascii)
    Wend
  EndIf
  
EndProcedure

Procedure.s StringHMAC(msg$, key$, cipher.i=#PB_Cipher_SHA1, bits.i=256, encode$="Hex")
  
  #IPAD = $36
  #OPAD = $5C
  
  
  Protected.i i, BlockSize, cipherResultSize
  Protected result$, innerHash$
  Protected *tmp, *ptr.Ascii, *innerHash, *outerHash
  
  
  ; adjust the needed values for different ciphers
  BlockSize = 64
  Select cipher
    Case #PB_Cipher_MD5
      cipherResultSize = 16
    Case #PB_Cipher_SHA1
      cipherResultSize = 20
    Case #PB_Cipher_SHA2
      If bits > 256
        BlockSize = 128
      EndIf
      cipherResultSize = bits / 8
    Case #PB_Cipher_SHA3
      Select bits
        Case 224
          BlockSize = 1152 / 8
        Case 256
          BlockSize = 1088 / 8
        Case 384
          BlockSize = 832 / 8
        Case 512
          BlockSize = 576 / 8
      EndSelect
      cipherResultSize = bits / 8
    Default
      ProcedureReturn "cipher not implemented"
  EndSelect
  
  ; special rule if length of the key is larger then the blocksize:
  ; use H(K) instead of K
  If StringByteLength(key$, #PB_Ascii) > BlockSize
    key$ = StringFingerprint(key$, cipher, bits, #PB_Ascii)
    *tmp = AllocateMemory(cipherResultSize)
    If *tmp
      StringHMAC_HexStringToBin(@key$, *tmp)
      key$ = PeekS(*tmp, cipherResultSize, #PB_Ascii)
      FreeMemory(*tmp)
    EndIf
  EndIf
  
  
  *outerHash = AllocateMemory(BlockSize + cipherResultSize)
  If *outerHash
    
    ; K XOR opad
    *ptr = *outerHash
    PokeS(*outerHash, key$, -1, #PB_Ascii|#PB_String_NoZero)
    For i = 0 To BlockSize - 1
      *ptr\a = *ptr\a ! #OPAD
      *ptr + 1
    Next i
      
    *innerHash = AllocateMemory(BlockSize + StringByteLength(msg$, #PB_UTF8))
    If *innerHash
      
      ; K XOR ipad
      *ptr = *innerHash
      PokeS(*innerHash, key$, -1, #PB_Ascii|#PB_String_NoZero)
      For i = 0 To BlockSize - 1
        *ptr\a = *ptr\a ! #IPAD
        *ptr + 1
      Next i
      
      ; (K XOR ipad) + M)
      PokeS(*ptr, msg$, -1, #PB_UTF8|#PB_String_NoZero)
      
      ; H((K XOR ipad) + M))
      innerHash$ = Fingerprint(*innerHash, MemorySize(*innerHash), cipher, bits)
      
      ; (K XOr opad) + H((K XOr ipad) + M)
      StringHMAC_HexStringToBin(@innerHash$, *outerHash + BlockSize)
      
      ; H((K XOR opad) + H((K XOR ipad) + M))
      result$ = Fingerprint(*outerHash, BlockSize + cipherResultSize, cipher, bits)
      
      ; optional result is coded in Base64
      If LCase(encode$) = "base64"
        *tmp = AllocateMemory(cipherResultSize)
        If *tmp
          StringHMAC_HexStringToBin(@result$, *tmp)
          result$ = Base64Encoder(*tmp, MemorySize(*tmp))
          FreeMemory(*tmp)
        EndIf
      EndIf
      
      FreeMemory(*innerHash)
    EndIf
    
    FreeMemory(*outerHash)
  EndIf
  
  ProcedureReturn result$
  
EndProcedure




Structure TwitterCredentials
  ConsumerKey.s
  ConsumerSecret.s
  AccessToken.s
  AccessTokenSecret.s
EndStructure

Procedure.s OAuth(*o.TwitterCredentials,ur.s)
  
  UseMD5Fingerprint() : UseSHA1Fingerprint()
  
  oauthTimestamp.i = Date()
  
  oauthNonce.s = StrD(Date() * Random(999,333),0)
  oauthNonce = StringFingerprint(oauthNonce, #PB_Cipher_MD5)
  
  NewList oauthParameters.s()
  AddElement(oauthParameters()) : oauthParameters() = "oauth_consumer_key=" + *o\ConsumerKey
  AddElement(oauthParameters()) : oauthParameters() = "oauth_nonce=" + oauthNonce
  AddElement(oauthParameters()) : oauthParameters() = "oauth_signature_method=HMAC-SHA1"
  AddElement(oauthParameters()) : oauthParameters() = "oauth_timestamp=" + oauthTimestamp
  AddElement(oauthParameters()) : oauthParameters() = "oauth_token=" + *o\AccessToken
  AddElement(oauthParameters()) : oauthParameters() = "oauth_version=1.0"
  SortList(oauthParameters(),#PB_Sort_Ascending)
  oauthParameterString.s = ""
  ForEach oauthParameters()
    oauthParameterString + oauthParameters()+"&"
  Next
  EnsureThisNotEnd(oauthParameterString,"&")
  oauthParameterString = ReplaceString(oauthParameterString,"&","%26")
  
  oauthBaseString.s = "POST&"
  oauthBaseString + Twitter_FormatURLParameter(ur) + "&"
  oauthBaseString + Twitter_FormatURLParameter(oauthParameterString)
  
  
  oauthSigningKey.s = URLEncoder(*o\ConsumerSecret) + "&" + URLEncoder(*o\AccessTokenSecret)
  
  oauthSignature.s = StringFingerprint(oauthBaseString,#PB_Cipher_SHA1|#PB_Cipher_HMAC,0,#PB_UTF8,oauthSigningKey,#PB_UTF8)
  oauthSignature = StringHMAC(oauthBaseString,oauthSigningKey,#PB_Cipher_SHA1,256,"base64")
  oauthSignature = Twitter_FormatURLParameter(URLEncoder(oauthSignature))
  
  
  authorizationHeader.s = "OAuth "
  authorizationHeader + "oauth_consumer_key="+c34 + *o\ConsumerKey +c34+ ", "
  authorizationHeader + "oauth_nonce="+c34 + oauthNonce + c34+", "
  authorizationHeader + "oauth_signature="+c34 + oauthSignature + c34+", "
  authorizationHeader + "oauth_signature_method="+c34+"HMAC-SHA1" + c34+", "
  authorizationHeader + "oauth_timestamp="+c34+oauthTimestamp +c34+", "
  authorizationHeader + "oauth_token="+c34+*o\AccessToken +c34+", "
  authorizationHeader + "oauth_version="+c34+"1.0"+c34
  
  ProcedureReturn authorizationHeader
  
EndProcedure




Procedure.s DeriveMediaIDFromResponse(ret.s)
  j.i = ParseJSON(#PB_Any,ret)
  
  If j
    ObjectValue = JSONValue(j)
    If ExamineJSONMembers(ObjectValue)
      While NextJSONMember(ObjectValue)
        ;R("JSONMemberKey(ObjectValue): "+JSONMemberKey(ObjectValue))
        If JSONMemberKey(ObjectValue) = "id"
          media_id.s = GetJSONString(JSONMemberValue(ObjectValue))
          Break 1
        EndIf
      Wend
    Else
      R("CAN'T EXAMINE MAIN")
    EndIf
    FreeJSON(j)
  EndIf
  If media_id=""
    R("Couldn't find media_id in Twitter's response!")
  EndIf
  
  ProcedureReturn media_id
EndProcedure

Procedure.s DeriveMediaIDFromResponse_NEW(ret.s)
  j.i = ParseJSON(#PB_Any,ret)
  
  If j
    ObjectValue = JSONValue(j)
    If ExamineJSONMembers(ObjectValue)
      While NextJSONMember(ObjectValue)
        ;R("JSONMemberKey(ObjectValue): "+JSONMemberKey(ObjectValue))
        If JSONMemberKey(ObjectValue) = "data"
          dov = JSONMemberValue(ObjectValue)
          If ExamineJSONMembers(dov)
            While NextJSONMember(dov)
              ;R("JSONMemberKey(dov): "+JSONMemberKey(dov))
              If JSONMemberKey(dov) = "id"
                media_id.s = GetJSONString(JSONMemberValue(dov))
                Break 1
              EndIf
            Wend
          EndIf
        EndIf
      Wend
    Else
      R("CAN'T EXAMINE MAIN")
    EndIf
    FreeJSON(j)
  EndIf
  If media_id=""
    R("Couldn't find media_id in Twitter's response!")
  EndIf
  
  ProcedureReturn media_id
EndProcedure




#DefaultMediaUploadURL = "https://api.x.com/2/media/upload"

Procedure.s Twitter_UploadImageFile(*o.TwitterCredentials,imgfn.s) ; will not work with GIFs
  
  fsz = FileSize(imgfn)
    If fsz = -1
      Debug "Image file does not exist!"
      Debug imgfn
    ProcedureReturn ""
  EndIf

  m.i = MPFR_Create()
  MPFR_AddFile(m,"media",imgfn)
  MPFR_AddTextField(m,"media_category","tweet_image")
  
  NewMap header.s()
  ;ur.s = "https://upload.twitter.com/1.1/media/upload.json"
  ur.s = #DefaultMediaUploadURL;+"?media_category=tweet_image"
  header("Authorization") = OAuth(*o,ur)
  
  MPFR_SendWithCustomHeaders(m,ur,header())
  
  If MPFRequest(m)\status_code=200
    ;R(MPFRequest(m)\response_message)
    ;Debug MPFRequest(m)\response_message
    media_id.s = DeriveMediaIDFromResponse_NEW(MPFRequest(m)\response_message)
  Else
    Debug "Twitter_UploadImageFile STATUS CODE: "+MPFRequest(m)\status_code
    If MPFRequest(m)\error_message : Debug "ERROR MESSAGE: "+MPFRequest(m)\error_message : EndIf
    Debug "Twitter_UploadImageFile RESPONSE: "+MPFRequest(m)\response_message
  EndIf
  
  MPFR_Free(m)
  
  ProcedureReturn media_id

EndProcedure



Procedure.s Twitter_UploadPBImage(*o.TwitterCredentials,img.i)
  
  UseJPEGImageEncoder()
  *mem = EncodeImage(img,#PB_ImagePlugin_JPEG)

  m.i = MPFR_Create()
  MPFR_AddData(m,"media",*mem,MemorySize(*mem),MimeType("jpg"))
  FreeMemory(*mem)
  MPFR_AddTextField(m,"media_category","tweet_image")
  
  NewMap header.s()
  ;ur.s = "https://upload.twitter.com/1.1/media/upload.json"
  ur.s = #DefaultMediaUploadURL
  header("Authorization") = OAuth(*o,ur)
  
  MPFR_SendWithCustomHeaders(m,ur,header())
  
  If MPFRequest(m)\status_code=200
    media_id.s = DeriveMediaIDFromResponse_NEW(MPFRequest(m)\response_message)
  Else
    Debug "Twitter_UploadPBImage STATUS CODE: "+MPFRequest(m)\status_code
    If MPFRequest(m)\error_message : Debug "ERROR MESSAGE: "+MPFRequest(m)\error_message : EndIf
    Debug "Twitter_UploadPBImage RESPONSE: "+MPFRequest(m)\response_message
  EndIf
  
  MPFR_Free(m)
  
  ProcedureReturn media_id

EndProcedure




Procedure.s Twitter_UploadMediaFileChunked(*o.TwitterCredentials,media_fn.s) ; WORKS, uses MPFR library
  Debug "PROC: UploadChunkedMedia"
  
  fsz.d = FileSize(media_fn)
  If fsz = -1
    Debug "Media file does not exist!"
    ProcedureReturn ""
  EndIf
  mime_type.s = MimeType(GetExtensionPart(media_fn))
  
  NewMap custom_header.s()
  
  
  
  
  u.s = #DefaultMediaUploadURL+"/initialize"
  custom_header("Authorization") = OAuth(*o,u)
  ;R(custom_header("Authorization"))
  custom_header("Content-Type") = "application/json"
  
  jt.s = "{"
  jt + c34+"total_bytes"+c34+":"+Str(fsz)+","
  If LCase(GetExtensionPart(media_fn))="srt"
    jt + c34+"media_category"+c34+":"+c34+"subtitles"+c34+","
  EndIf
  jt + c34+"media_type"+c34+":"+c34+mime_type+c34
  jt+"}"
  
  Debug "INIT"
  req.i = HTTPRequest(#PB_HTTP_Post,u,jt,0,custom_header())
  Debug "Twitter_UploadMediaFileChunked init STATUS CODE: "+HTTPInfo(req,#PB_HTTP_StatusCode)
  If HTTPInfo(req,#PB_HTTP_ErrorMessage) : Debug "Twitter_UploadMediaFileChunked init ERROR: "+HTTPInfo(req,#PB_HTTP_ErrorMessage) : EndIf
  Debug "Twitter_UploadMediaFileChunked init RESPONSE: "+HTTPInfo(req,#PB_HTTP_Response)
  
  media_id.s = DeriveMediaIDFromResponse_NEW(HTTPInfo(req,#PB_HTTP_Response))
  ;R("MEDIA ID: *"+media_id+"*")
  FinishHTTP(req)
  If media_id="" : ProcedureReturn "" : EndIf
  Debug "------------------------" : Debug "" : Debug ""
  ;media_id.s=""
  
  
  
  
  u.s = #DefaultMediaUploadURL+"/"+media_id+"/append"
  ClearMap(custom_header())
  custom_header("Authorization") = OAuth(*o,u)
  #ChunkSize = 1024 * 1024 * 3 ; 3mb
  chunks.i = Round(fsz / #ChunkSize,#PB_Round_Up)
  ;R("CHUNKS: "+Str(chunks))
  file_start.i = 0
  f = ReadFile(#PB_Any,media_fn)
  For a = 1 To chunks
    Debug "APPEND #"+Str(a)+"/"+Str(chunks)
    seg = a-1
    
    data_len.i = Defeat(#ChunkSize,fsz-file_start)
    *mem = AllocateMemory(data_len)
    ReadData(f,*mem,data_len)
    
    
    m.i = MPFR_Create()
    ;MPFR_AddTextField(m,"id",media_id)
    MPFR_AddTextField(m,"segment_index",Str(seg))
    
    MPFR_AddData(m,"media",*mem,data_len,mime_type)
    FreeMemory(*mem)
    
    MPFR_SendWithCustomHeaders(m,u,custom_header())
    
    Debug "Twitter_UploadMediaFileChunked append STATUS CODE: "+MPFRequest(m)\status_code
    If MPFRequest(m)\error_message : Debug "Twitter_UploadMediaFileChunked append ERROR: "+MPFRequest(m)\error_message : EndIf
    Debug "Twitter_UploadMediaFileChunked append RESPONSE: "+MPFRequest(m)\response_message
    If MPFRequest(m)\status_code<>200 : Debug "Append failure. Aborting." : CloseFile(f) : MPFR_Free(m) : ProcedureReturn "" : EndIf
    MPFR_Free(m)
    
    
    file_start + #ChunkSize
    Debug "------------------------" : Debug "" : Debug ""
  Next a
  CloseFile(f)
  ;End
  
  
  
  
  Debug "FINALISE"
  ClearMap(custom_header())
  custom_header("Content-Type") = "application/json"
  u.s = #DefaultMediaUploadURL+"/"+media_id+"/finalize"
  custom_header("Authorization") = OAuth(*o,u)
  req.i = HTTPRequest(#PB_HTTP_Post,u,"",0,custom_header())
  Debug "Twitter_UploadMediaFileChunked finalize STATUS CODE: "+HTTPInfo(req,#PB_HTTP_StatusCode)
  If HTTPInfo(req,#PB_HTTP_ErrorMessage) : Debug "Twitter_UploadMediaFileChunked finalize ERROR: "+HTTPInfo(req,#PB_HTTP_ErrorMessage) : EndIf
  Debug "Twitter_UploadMediaFileChunked finalize RESPONSE: "+HTTPInfo(req,#PB_HTTP_Response)
  If HTTPInfo(req,#PB_HTTP_StatusCode)<>"200" : Debug "Finalise failure. Aborting." : FinishHTTP(req) : ProcedureReturn "" : EndIf
  Debug "------------------------"
  FinishHTTP(req)
  
  
  ProcedureReturn media_id
EndProcedure




Procedure.b Twitter_SetMediaMetadata(*o.TwitterCredentials,media_id.s,alt_text.s,allow_download.b)
  If media_id="" ;Or alt_text=""
    ProcedureReturn #False
  EndIf
  
  postFields.s = "{ "
  postFields + c34+"media_id"+c34+": "+c34+media_id+c34
  postFields + ", "+c34+"alt_text"+c34+": {"+c34+"text"+c34+": "+c34+alt_text+c34+" }"
  ;postFields + ", "+c34+"allow_download_status"+c34+": {"+c34+"allow_download"+c34+": "+c34+ByteTruth(allow_download)+c34+" }"
  ;postFields + ", "+c34+"found_media_origin"+c34+": {"+c34+"id"+c34+": "+c34+fmo_id+c34+", "+c34+"provider"+c34+": "+c34+fmo_provider+c34+" }"
  ;postFields + ", "+c34+"upload_source"+c34+": {"+c34+"text"+c34+": "+c34+upload_source+c34+" }"
  postFields + " }"
  ;R(postFields)
  
  
  NewMap header.s()
  ;ur.s = "https://upload.twitter.com/1.1/media/metadata/create.json"
  ur.s = "https://upload.twitter.com/2/media/metadata/create"
  header("Authorization") = OAuth(*o,ur)
  header("Content-Type") = "application/json; charset=UTF-8"
  
  
  req.i = HTTPRequest(#PB_HTTP_Post,ur,postFields,0,header())
  If req
    StatusCode.i = Val(HTTPInfo(req,#PB_HTTP_StatusCode))
    Debug "Twitter_SetMediaMetadata StatusCode: " + Str(StatusCode)
    ;Debug "Response: " + HTTPInfo(req,#PB_HTTP_Response)
    em.s=HTTPInfo(req,#PB_HTTP_ErrorMessage) : If em : Debug "Error: "+em : EndIf
    FinishHTTP(req)
    
    If StatusCode=>200 And StatusCode<=299
      ProcedureReturn #True
    EndIf
  EndIf
EndProcedure



Procedure.i Twitter_SetVideoSubtitles(*o.TwitterCredentials,media_id.s,subtitle_media_id.s,language_code.s,display_name.s)
  ;Debug "PROC: SetVideoSubtitles"
  If media_id="" Or subtitle_media_id=""
    ProcedureReturn #False
  EndIf
  
  postFields.s = "{ "
  postFields + c34+"media_id"+c34+": "+c34+media_id+c34
  postFields + ", "+c34+"media_category"+c34+": "+c34+"TweetVideo"+c34
  postFields + ", "+c34+"subtitle_info"+c34+": { "+c34+"subtitles"+c34+": [{ "+c34+"media_id"+c34+":"+c34+subtitle_media_id+c34+", "+c34+"language_code"+c34+":"+c34+language_code+c34+", "+c34+"display_name"+c34+":"+c34+display_name+c34+" } ] }"
  postFields + " }"
  ;SetClipboardText(postFields)
  ;R(postFields)
  
  
  NewMap header.s()
  ;ur.s = "https://upload.twitter.com/1.1/media/subtitles/create.json"
  ur.s = "https://upload.twitter.com/2/media/subtitles/create.json"
  header("Authorization") = OAuth(*o,ur)
  header("Content-Type") = "application/json; charset=UTF-8"
  
  
  req.i = HTTPRequest(#PB_HTTP_Post,ur,postFields,0,header())
  If req
    StatusCode.i = Val(HTTPInfo(req,#PB_HTTP_StatusCode))
    Debug "Twitter_SetVideoSubtitles StatusCode: " + Str(StatusCode)
    ;Debug "Response: " + HTTPInfo(req,#PB_HTTP_Response)
    em.s=HTTPInfo(req,#PB_HTTP_ErrorMessage) : If em : Debug "Error: "+em : EndIf
    FinishHTTP(req)
    
    If StatusCode=200
      ProcedureReturn #True
    Else
      ProcedureReturn StatusCode
    EndIf
  EndIf
EndProcedure


Procedure.i Twitter_RemoveVideoSubtitles(*o.TwitterCredentials,media_id.s,language_code.s)
  ;Debug "PROC: RemoveVideoSubtitles"
  If media_id="" Or language_code=""
    ProcedureReturn #False
  EndIf
  
  postFields.s = "{ "
  postFields + c34+"media_id"+c34+": "+c34+media_id+c34
  postFields + ", "+c34+"media_category"+c34+": "+c34+"TweetVideo"+c34
  postFields + ", "+c34+"subtitle_info"+c34+": { "+c34+"subtitles"+c34+": [{ "+c34+"language_code"+c34+":"+c34+language_code+c34+" }] }"
  postFields + " }"
  ;R(postFields) : End
  
  
  NewMap header.s()
  ;ur.s = "https://upload.twitter.com/1.1/media/subtitles/delete.json"
  ur.s = "https://upload.twitter.com/2/media/subtitles/delete.json"
  header("Authorization") = OAuth(*o,ur)
  header("Content-Type") = "application/json; charset=UTF-8"
  
  
  req.i = HTTPRequest(#PB_HTTP_Post,ur,postFields,0,header())
  If req
    StatusCode.i = Val(HTTPInfo(req,#PB_HTTP_StatusCode))
    Debug "Twitter_RemoveVideoSubtitles StatusCode: " + Str(StatusCode)
    ;Debug "Response: " + HTTPInfo(req,#PB_HTTP_Response)
    em.s=HTTPInfo(req,#PB_HTTP_ErrorMessage) : If em : Debug "Error: "+em : EndIf
    FinishHTTP(req)
    
    If StatusCode=200
      ProcedureReturn #True
    Else
      ProcedureReturn StatusCode
    EndIf
  EndIf
EndProcedure




Procedure.s Twitter_PostTweet(*o.TwitterCredentials,text.s,media_id_arr.s="",in_reply_to.s="",quoting_tweet_id.s="")
  If text="" And media_id_arr=""
    ProcedureReturn ""
  EndIf
  
  postFields.s = "{ "
  postFields + c34+"text"+c34+": "+c34+Twitter_PrepareText(text)+c34
  If media_id_arr<>""
    media_id_arr = DelimitedArrayToJSONStringArray(media_id_arr,#d1)
    postFields + ", "+c34+"media"+c34+": {"+c34+"media_ids"+c34+": "+media_id_arr+" }"
  EndIf
  If in_reply_to<>""
    postFields + ", "+c34+"reply"+c34+": {"+c34+"in_reply_to_tweet_id"+c34+": "+c34+in_reply_to+c34+" }"
  EndIf
  If quoting_tweet_id<>""
    postFields + ", "+c34+"quote_tweet_id"+c34+": "+c34+quoting_tweet_id+c34
  EndIf
  postFields + " }"
  ;R(postFields)
  
  
  NewMap header.s()
  ur.s = "https://api.twitter.com/2/tweets"
  header("Authorization") = OAuth(*o,ur)
  header("Content-Type") = "application/json"
  
  
  req.i = HTTPRequest(#PB_HTTP_Post,ur,postFields,0,header())
  If req
    If HTTPInfo(req,#PB_HTTP_StatusCode)<>"201"
    Debug "Twitter_PostTweet StatusCode: " + HTTPInfo(req,#PB_HTTP_StatusCode)
    Debug "Twitter_PostTweet Response: " + HTTPInfo(req,#PB_HTTP_Response)
    EndIf
    em.s=HTTPInfo(req,#PB_HTTP_ErrorMessage) : If em : Debug "Twitter_PostTweet Error: "+em : EndIf
    
    *ret = HTTPMemory(req)
    ;Debug "Response size: " + MemorySize(*ret)
    j.i = CatchJSON(#PB_Any,*ret,MemorySize(*ret))
    FinishHTTP(req)
    FreeMemory(*ret)
    ;dat.s = ComposeJSON(j,#PB_JSON_PrettyPrint) : R(dat)
    
    ObjectValue = JSONValue(j)
    If ExamineJSONMembers(ObjectValue)
      While NextJSONMember(ObjectValue)
        If JSONMemberKey(ObjectValue) = "data"
          value = JSONMemberValue(ObjectValue)
          If ExamineJSONMembers(value)
            While NextJSONMember(value)
              If JSONMemberKey(value) = "id"
                id.s = GetJSONString(JSONMemberValue(value))
                Break 2
              EndIf
            Wend
          Else
            R("CAN'T EXAMINE SUB")
          EndIf
        EndIf
      Wend
    Else
      R("CAN'T EXAMINE MAIN")
    EndIf
    FreeJSON(j)
    
    ProcedureReturn id
  EndIf
EndProcedure




Procedure.s Twitter_PostPoll(*o.TwitterCredentials,text.s,duration_minutes.i,option_arr.s,in_reply_to.s="",quoting_tweet_id.s="")
  postFields.s = "{ "
  postFields + c34+"text"+c34+": "+c34+Twitter_PrepareText(text)+c34
  option_arr = DelimitedArrayToJSONStringArray(option_arr)
  postFields + ", "+c34+"poll"+c34+": {"+c34+"options"+c34+": "+option_arr+", "+c34+"duration_minutes"+c34+":"+Str(duration_minutes)+" }"
  If in_reply_to<>""
    postFields + ", "+c34+"reply"+c34+": {"+c34+"in_reply_to_tweet_id"+c34+": "+c34+in_reply_to+c34+" }"
  EndIf
  If quoting_tweet_id<>""
    postFields + ", "+c34+"quote_tweet_id"+c34+": "+c34+quoting_tweet_id+c34
  EndIf
  postFields + " }"
  
  
  NewMap header.s()
  ur.s = "https://api.twitter.com/2/tweets"
  header("Authorization") = OAuth(*o,ur)
  header("Content-Type") = "application/json"
  
  
  req.i = HTTPRequest(#PB_HTTP_Post,ur,postFields,0,header())
  If req
    Debug "Twitter_PostPoll StatusCode: " + HTTPInfo(req,#PB_HTTP_StatusCode)
    Debug "Twitter_PostPoll Response: " + HTTPInfo(req,#PB_HTTP_Response)
    em.s=HTTPInfo(req,#PB_HTTP_ErrorMessage) : If em : Debug "Error: "+em : EndIf
    
    *ret = HTTPMemory(req)
    ;Debug "Response size: " + MemorySize(*ret)
    j.i = CatchJSON(#PB_Any,*ret,MemorySize(*ret))
    FinishHTTP(req)
    FreeMemory(*ret)
    dat.s = ComposeJSON(j,#PB_JSON_PrettyPrint) : R(dat)
    
    ObjectValue = JSONValue(j)
    If ExamineJSONMembers(ObjectValue)
      While NextJSONMember(ObjectValue)
        If JSONMemberKey(ObjectValue) = "data"
          value = JSONMemberValue(ObjectValue)
          If ExamineJSONMembers(value)
            While NextJSONMember(value)
              If JSONMemberKey(value) = "id"
                id.s = GetJSONString(JSONMemberValue(value))
                Break 2
              EndIf
            Wend
          Else
            R("CAN'T EXAMINE SUB")
          EndIf
        EndIf
      Wend
    Else
      R("CAN'T EXAMINE MAIN")
    EndIf
    FreeJSON(j)
    
    ProcedureReturn id
  EndIf
EndProcedure




Procedure.s Twitter_MediaIDFromString(*o.TwitterCredentials,info.s)
  If info=""
    ProcedureReturn ""
  EndIf
  
  img.i = Val(info)
  If IsImage(img)
    ; uploading a PB image
    ProcedureReturn Twitter_UploadPBImage(*o,img)
  Else
    If FileSize(info)>0
      ProcedureReturn Twitter_UploadImageFile(*o,info)
    Else
      ; an already cached online file
      ProcedureReturn info
    EndIf
  EndIf
  
  ProcedureReturn ""
  
EndProcedure


Procedure.s Twitter_PostThread(*o.TwitterCredentials,List item.s(),include_numbers.b=#True)
  
  reply_to.s = ""
  num.i = 0
  items.i = ListSize(item())
  ForEach item()
    num+1
    this_one.s = EnsureEnd(item(),#d1)
    txt.s = StringField(this_one,1,#d1)
    
    media_id_arr.s = ""
    fields = CountString(this_one,#d1)
    For f = 2 To fields
      media_info.s = StringField(this_one,f,#d1)
      media_id.s = Twitter_MediaIDFromString(*o,media_info)
      If media_id<>"" : media_id_arr+media_id+#d1 : EndIf
    Next f
    ;R(Str(num)+" MEDIA ID ARR:"+c13+media_id_arr)
    
    
    If txt="" And media_id_arr="" : R("SKIPPING "+Str(num)) : Continue : EndIf
    
    If include_numbers
      If num=1
        ;txt + " &#x1F9F5;"
        ;txt + " \u1F9F5 "
        txt + " 🧵"
      Else
        txt + " "+Str(num)+"/"+Str(items)
      EndIf
      
    EndIf
    
    id.s = Twitter_PostTweet(*o,txt,media_id_arr,reply_to)
    ;R(Str(num)+c13+txt+c13+c13+c13+id)
    If num=1 : orig_id.s=id : EndIf
    reply_to = id
  Next
  
  ProcedureReturn orig_id
  
EndProcedure




Procedure.b Twitter_Retweet(*o.TwitterCredentials,id.s,tweet_id.s) ; untested; does not work with the free tier
  postFields.s = "{ "+c34+"tweet_id"+c34+": "+c34+tweet_id+c34+" }"
  
  
  NewMap header.s()
  ur.s = "https://api.twitter.com/2/users/:"+id+"/retweets" ; not sure whether the : should be before id
  header("Authorization") = OAuth(*o,ur)
  header("Content-Type") = "application/json"
  
  
  req.i = HTTPRequest(#PB_HTTP_Post,ur,postFields,0,header())
  If req
    Debug "Twitter_Retweet StatusCode: " + HTTPInfo(req,#PB_HTTP_StatusCode)
    Debug "Twitter_Retweet Response: " + HTTPInfo(req,#PB_HTTP_Response)
    em.s=HTTPInfo(req,#PB_HTTP_ErrorMessage) : If em : Debug "Error: "+em : EndIf
    
    *ret = HTTPMemory(req)
    ;Debug "Response size: " + MemorySize(*ret)
    j.i = CatchJSON(#PB_Any,*ret,MemorySize(*ret))
    FinishHTTP(req)
    FreeMemory(*ret)
    dat.s = ComposeJSON(j,#PB_JSON_PrettyPrint) : R(dat)
    
    
    ObjectValue = JSONValue(j)
    If ExamineJSONMembers(ObjectValue)
      While NextJSONMember(ObjectValue)
        If JSONMemberKey(ObjectValue) = "data"
          value = JSONMemberValue(ObjectValue)
          If ExamineJSONMembers(value)
            While NextJSONMember(value)
              If JSONMemberKey(value) = "retweeted"
                status.b = GetJSONBoolean(JSONMemberValue(value))
                Break 2
              EndIf
            Wend
          Else
            R("RETWEET. CAN'T EXAMINE SUB")
          EndIf
        EndIf
      Wend
    Else
      R("RETWEET. CAN'T EXAMINE MAIN")
    EndIf
    FreeJSON(j)
    
    ProcedureReturn status
  EndIf
EndProcedure


Procedure.b Twitter_PinTweet(*o.TwitterCredentials,tweet_id.s) ; untested; does not work with the free tier
  postFields.s = "{ "+c34+"tweet_id"+c34+": "+c34+tweet_id+c34+" }"
  
  
  NewMap header.s()
  ur.s = "https://api.twitter.com/1.1/account/pin_tweet"
  header("Authorization") = OAuth(*o,ur)
  header("Content-Type") = "application/json"
  
  
  req.i = HTTPRequest(#PB_HTTP_Post,ur,postFields,0,header())
  If req
    Debug "Twitter_PinTweet StatusCode: " + HTTPInfo(req,#PB_HTTP_StatusCode)
    Debug "Twitter_PinTweet Response: " + HTTPInfo(req,#PB_HTTP_Response)
    em.s=HTTPInfo(req,#PB_HTTP_ErrorMessage) : If em : Debug "Error: "+em : EndIf
    
    *ret = HTTPMemory(req)
    Debug "Twitter_PinTweet Response size: " + MemorySize(*ret)
    Debug PeekS(*ret)
    j.i = CatchJSON(#PB_Any,*ret,MemorySize(*ret))
    FinishHTTP(req)
    FreeMemory(*ret)
    dat.s = ComposeJSON(j,#PB_JSON_PrettyPrint) : R(dat)
    
    
    ObjectValue = JSONValue(j)
    If ExamineJSONMembers(ObjectValue)
      While NextJSONMember(ObjectValue)
        If JSONMemberKey(ObjectValue) = "data"
          value = JSONMemberValue(ObjectValue)
          If ExamineJSONMembers(value)
            While NextJSONMember(value)
              If JSONMemberKey(value) = "retweeted"
                status.b = GetJSONBoolean(JSONMemberValue(value))
                Break 2
              EndIf
            Wend
          Else
            R("RETWEET. CAN'T EXAMINE SUB")
          EndIf
        EndIf
      Wend
    Else
      R("RETWEET. CAN'T EXAMINE MAIN")
    EndIf
    FreeJSON(j)
    
    ProcedureReturn status
  EndIf
EndProcedure

Usage demo:

Code: Select all

XIncludeFile "TwitterX.pbi"

cred.TwitterCredentials
cred\ConsumerKey = ""
cred\ConsumerSecret = ""
cred\AccessToken = ""
cred\AccessTokenSecret = ""
^ fill in these details!


; post a tweet:
Twitter_PostTweet(@cred,"my test tweet")


; post a tweet then reply to it:
tid.s = Twitter_PostTweet(@cred,"initial tweet")
Twitter_PostTweet(@cred,"reply tweet","",tid)


; post a tweet then quote it in another:
tid.s = Twitter_PostTweet(@cred,"the quoted tweet")
Twitter_PostTweet(@cred,"the quoting tweet","","",tid)


; post a tweet with an image (not a gif):
img_fn.s = "G:\my test image.jpg"
img_id.s = Twitter_UploadImageFile(@cred,img_fn)
Twitter_PostTweet(@cred,"my tweet with image",img_id)


; post a tweet with a PB image:
iw=600 : ih=400
img.i = CreateImage(#PB_Any,iw,ih)
StartDrawing(ImageOutput(img))
For a = 1 To 20
  LineXY(Random(iw),Random(ih),Random(iw),Random(ih),RGB(Random(255),Random(255),Random(255)))
Next a
StopDrawing()
img_id.s = Twitter_UploadPBImage(@cred,img)
Twitter_PostTweet(@cred,"my PB image",img_id)


; post a tweet with an image, video or gif:
gif_fn.s = "G:\my test animation.gif"
gif_id.s = Twitter_UploadMediaFile(@cred,gif_fn)
Twitter_PostTweet(@cred,"my tweet with gif",gif_id)


; post a tweet with a video with subtitles:
video_fn.s = "G:\my video.mp4"
video_id.s = Twitter_UploadMedia(@cred,video_fn)
srt_fn.s = "G:\my subtitles.srt"
subtitle_id.s = Twitter_UploadMedia(@cred,srt_fn)
Twitter_SetVideoSubtitles(@cred,video_id,subtitle_id,"EN","English")
Twitter_PostTweet(@cred,"my subtitled video",video_id)


; post a poll:
Twitter_PostPoll(@cred,"a test poll",60 * 60 * 24,"apple|pear|grape|melon|")


; post a thread:
UsePNGImageDecoder()
UseJPEGImageDecoder()
imgfn.s = OpenFileRequester("Please choose an image to upload", "", "Images (*.jpg;*.png)|*.jpg;*.png", 0)
red_img.i = CreateImage(#PB_Any,600,400,24,#Red)
red_media_id.s = Twitter_UploadPBImage(@cred,red_img)
blue_img.i = CreateImage(#PB_Any,600,400,24,#Blue)
NewList item.s()
AddElement(item()) : item()="my thread listing some fruits"
AddElement(item()) : item()="apple"
AddElement(item()) : item()="pear|"+imgfn
AddElement(item()) : item()="melon|"+red_media_id
AddElement(item()) : item()="grape|"+Str(blue_img)
id.s = Twitter_PostThread(@cred,item(),#True)
ur.s = Twitter_TweetURL(#TwitterAt,id)
RunProgram(ur)
Hopefully this is useful for someone. It certainly will be for me. If you run into any bugs, please let me know.
Last edited by Seymour Clufley on Tue Jun 10, 2025 8:22 pm, edited 18 times in total.
JACK WEBB: "Coding in C is like sculpting a statue using only sandpaper. You can do it, but the result wouldn't be any better. So why bother? Just use the right tools and get the job done."
BarryG
Addict
Addict
Posts: 4155
Joined: Thu Apr 18, 2019 8:17 am

Re: Posting on Twitter/X

Post by BarryG »

Thanks! Looks interesting.
Quin
Addict
Addict
Posts: 1132
Joined: Thu Mar 31, 2022 7:03 pm
Location: Colorado, United States
Contact:

Re: Posting on Twitter/X

Post by Quin »

Nice! It's interesting to see what Twitter's API has become. I wrote a client for it back in 2021 before Elon Musk took over. Needless to say it couldn't exist how it used to if I tried it now :|, waaaaay too expensive. If Buffer can't support it, I can't...
Fred
Administrator
Administrator
Posts: 18162
Joined: Fri May 17, 2002 4:39 pm
Location: France
Contact:

Re: Posting on Twitter/X

Post by Fred »

On a side note, PB now natively supports HMAC: https://www.purebasic.com/documentation ... print.html
Seymour Clufley
Addict
Addict
Posts: 1265
Joined: Wed Feb 28, 2007 9:13 am
Location: London

Re: Posting on Twitter/X

Post by Seymour Clufley »

Fred wrote: Tue Jul 23, 2024 2:46 pm On a side note, PB now natively supports HMAC: https://www.purebasic.com/documentation ... print.html
Yes, but I need binary output, and I don't think PB's own function does that.
JACK WEBB: "Coding in C is like sculpting a statue using only sandpaper. You can do it, but the result wouldn't be any better. So why bother? Just use the right tools and get the job done."
Seymour Clufley
Addict
Addict
Posts: 1265
Joined: Wed Feb 28, 2007 9:13 am
Location: London

Re: Posting on Twitter/X

Post by Seymour Clufley »

I realised that SetMediaMetadata, SetVideoSubtitles and RemoveVideoSubtitles are also available on the free tier, so I have implemented those. The only remaining thing that could be added is the status check during media uploads, but it doesn't seem to be necessary.

To use any more endpoints than this, you would need to pay $100/month for the API's "basic" tier, and I'm not doing that.
JACK WEBB: "Coding in C is like sculpting a statue using only sandpaper. You can do it, but the result wouldn't be any better. So why bother? Just use the right tools and get the job done."
Seymour Clufley
Addict
Addict
Posts: 1265
Joined: Wed Feb 28, 2007 9:13 am
Location: London

Re: Posting on Twitter/X

Post by Seymour Clufley »

I've changed the method of handling OAuth credentials. It should be more convenient now. I've also added the ability to do quote tweets and written a helper procedure for posting threads. Also procedures for pinning and retweeting (although neither of these is possible on the free tier of the API).

That's it for now. If anyone finds any bugs in the library or the demo, please let me know.
JACK WEBB: "Coding in C is like sculpting a statue using only sandpaper. You can do it, but the result wouldn't be any better. So why bother? Just use the right tools and get the job done."
Seymour Clufley
Addict
Addict
Posts: 1265
Joined: Wed Feb 28, 2007 9:13 am
Location: London

Re: Posting on Twitter/X

Post by Seymour Clufley »

Updated to use the v2 API instead of deprecated v1.1 endpoints.
JACK WEBB: "Coding in C is like sculpting a statue using only sandpaper. You can do it, but the result wouldn't be any better. So why bother? Just use the right tools and get the job done."
Quin
Addict
Addict
Posts: 1132
Joined: Thu Mar 31, 2022 7:03 pm
Location: Colorado, United States
Contact:

Re: Posting on Twitter/X

Post by Quin »

Very nice! My first successful piece of software was a Twitter client, so this API still holds a special place in my heart, even after Elon Musk ruined the entire platform. Glad to see someone keeping Twitter API development alive! :)
Seymour Clufley
Addict
Addict
Posts: 1265
Joined: Wed Feb 28, 2007 9:13 am
Location: London

Re: Posting on Twitter/X

Post by Seymour Clufley »

Updated again. For images, use Twitter_UploadImageFile() or Twitter_UploadPBImage(). For videos and gifs, use Twitter_UploadMediaFileChunked(), which can also handle image files and is "preferred" by X to the single-call uploads done by the other two procedures.
JACK WEBB: "Coding in C is like sculpting a statue using only sandpaper. You can do it, but the result wouldn't be any better. So why bother? Just use the right tools and get the job done."
Quin
Addict
Addict
Posts: 1132
Joined: Thu Mar 31, 2022 7:03 pm
Location: Colorado, United States
Contact:

Re: Posting on Twitter/X

Post by Quin »

Every time I see that you're still hacking away on this, it amazes me.
Great stuff, keep up the nice work!
Seymour Clufley
Addict
Addict
Posts: 1265
Joined: Wed Feb 28, 2007 9:13 am
Location: London

Re: Posting on Twitter/X

Post by Seymour Clufley »

Thanks, but hopefully there won't be much need for me to keep hacking at it, once the API is stabilised. There's really not much you can do with the free version of it, which is what I have.

The only future change I'm waiting for is the ability to post articles.
JACK WEBB: "Coding in C is like sculpting a statue using only sandpaper. You can do it, but the result wouldn't be any better. So why bother? Just use the right tools and get the job done."
Post Reply