(also see my follow-up posts Twitter Timelines with VBA and Using Twitter’s OAuth Tool)
I’ve recently been working on a VBA-based project which incorporates the ability to post status updates to Twitter (i.e., tweeting).
In putting it together, I consulted various code samples, specs, API documentation, blog/forum posts, etc… I’m sharing my efforts here in the hopes of saving anyone else working on a similar project a bit of time and/or frustration. I’ve also added a couple of features that I hadn’t seen elsewhere (e.g., using v1.1. of the Twitter API, including images with tweets, integrating link shortening, etc…), so (hopefully!) I’m adding something new to the tweet-via-VBA body of literature.
A few preliminary notes:
- Everyone has their own way of naming variables/functions/subs, indenting, commenting code, etc… If something I’ve done doesn’t suit you, make whatever changes you see fit.
- I wanted to avoid any third-party, non-MS dlls or add-ins. As a result, I did use the Microsoft Script Control (msscript.ocx), and a couple of .Net libraries (meaning you’ll have to have .Net Framework 1.1 or later installed on your machine). One downside to this is that Microsoft Script Control can only be used on 32-bit versions of MS Office (2007, 2010, and 2013 should all be OK). Either 32-bit or 64-bit Windows is fine, but the code as-is won’t run on 64-bit Office. If you need to modify the code to run on 64-bit Office, I’ve include some links in the ‘Other Resources’ section which should enable you to do so.
- Make sure you have added references to Microsoft WinHTTP Services, Microsoft XML , and Microsoft Script Control to your project (Tools -> Add References in the VBA editor)
- You’ll find some obvious instances of code repetition (for example, the functions get_basestring, get_status_basestring, and get_media_basestring are largely identical). As I was originally working this all out, it was easier for me to focus on the specific task at hand (i.e., plain text tweet, tweet with media, getting Twitter service status info) if I kept each function separate. One could easily refactor the preceding functions into a single function, and there are likely additional opportunities for making the code more efficient/elegant/consistent.
- Tweeting with VBA by Greg Williams is licensed under a Creative Commons Attribution-ShareAlike 3.0 Unported License.
- Code is supplied as-is. All use is at your own risk.
Let’s get started!
WINDOWS API FUNCTIONS AND TYPE DECLARATIONS
First, we’ll need to define a couple of Windows API functions using the Declare keyword. We’ll also create some user-defined data types using the Type statement.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27
|
#If VBA7 Then Public Declare PtrSafe Sub GetSystemTime Lib "Kernel32" (lpSystemTime As SYSTEMTIME) Public Declare PtrSafe Function SystemTimeToFileTime Lib "Kernel32" (lpSystemTime As SYSTEMTIME, lpFileTime As FILETIME) As LongPtr Public Declare PtrSafe Function LocalFileTimeToFileTime Lib "Kernel32" (lpLocalFileTime As FILETIME, lpFileTime As FILETIME) As LongPtr Public Declare PtrSafe Function FileTimeToSystemTime Lib "Kernel32" (lpFileTime As FILETIME, lpSystemTime As SYSTEMTIME) As LongPtr #Else Public Declare Sub GetSystemTime Lib "Kernel32" (lpSystemTime As SYSTEMTIME) Public Declare Function SystemTimeToFileTime Lib "Kernel32" (lpSystemTime As SYSTEMTIME, lpFileTime As FILETIME) As Long Public Declare Function LocalFileTimeToFileTime Lib "Kernel32" (lpLocalFileTime As FILETIME, lpFileTime As FILETIME) As Long Public Declare Function FileTimeToSystemTime Lib "Kernel32" (lpFileTime As FILETIME, lpSystemTime As SYSTEMTIME) As Long #End If Public Type FILETIME dwLowDateTime As Long dwHighDateTime As Long End Type Public Type SYSTEMTIME wYear As Integer wMonth As Integer wDayOfWeek As Integer wDay As Integer wHour As Integer wMinute As Integer wSecond As Integer wMilliseconds As Integer End Type |
The #If VBA7 Then…#Else…#End If syntax, PtrSafe keyword, and LongPtr data type are all used to allow the code to run on 64-bit Office (see this MSDN article for more info). Again, as mentioned above, the code as-is (which makes use of Microsoft Script Control), won’t run on 64-bit Office. But, if you decide to write/use alternatives that don’t need Microsoft Script Control (or in the seemingly unlikely event that Microsoft releases a 64-bit version of msscript.ocx), it’ll be handy to have your API function declarations 64-bit ready.
CONSTANTS
Next, we’ll define some constants. To get the necessary Twitter API keys and tokens, you’ll need to sign up with Twitter as a developer, then register an application.
In this example, I also use Google’s link shortener to shorten any links included with tweets. If you want to do the same, you’ll need to sign up as a Google developer and get an API key.
|
Const cnsAPIMethodP As String = "POST" Const cnsAPIMethodG As String = "GET" Const cnsOauthConsumerKey As String = "yourtwitterconsumerkey" Const cnsOauthConsumerSecret As String = "yourtwitterconsumersecret" Const cnsOauthToken As String = "yourtwitteraccesstoken" Const cnsOauthTokenSecret As String = "yourtwitteraccesstokensecret" Const cnsURLPost As String = "https://api.twitter.com/1.1/statuses/update.json" Const cnsURLMedia As String = "https://api.twitter.com/1.1/statuses/update_with_media.json" Const cnsURLStatus As String = "https://api.twitter.com/1.1/help/configuration.json" Const cnsMethod As String = "HMAC-SHA1" Const cnsOauthVersion As String = "1.0" 'Following only necessary if using Google's goo.gl link shortener API Const cnsGoogleShortenerURL As String = "https://www.googleapis.com/urlshortener/v1/url" Const cnsGoogleShortenerKey As String = "yourgoogleshortenerkeyhere" |
HELPER FUNCTIONS
Next, we’ll look at some of the helper functions we’ll be using when creating/sending our tweets.
The LocalTimeToUTC function converts a date value into UTC format. Thanks to Allen Wyatt for the code.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26
|
Public Function LocalTimeToUTC(dteTime As Date) As Date Dim dteLocalFileTime As FILETIME Dim dteFileTime As FILETIME Dim dteLocalSystemTime As SYSTEMTIME Dim dteSystemTime As SYSTEMTIME dteLocalSystemTime.wYear = CInt(Year(dteTime)) dteLocalSystemTime.wMonth = CInt(Month(dteTime)) dteLocalSystemTime.wDay = CInt(Day(dteTime)) dteLocalSystemTime.wHour = CInt(Hour(dteTime)) dteLocalSystemTime.wMinute = CInt(Minute(dteTime)) dteLocalSystemTime.wSecond = CInt(Second(dteTime)) Call SystemTimeToFileTime(dteLocalSystemTime, _ dteLocalFileTime) Call LocalFileTimeToFileTime(dteLocalFileTime, _ dteFileTime) Call FileTimeToSystemTime(dteFileTime, dteSystemTime) LocalTimeToUTC = CDate(dteSystemTime.wMonth & "/" & _ dteSystemTime.wDay & "/" & _ dteSystemTime.wYear & " " & _ dteSystemTime.wHour & ":" & _ dteSystemTime.wMinute & ":" & _ dteSystemTime.wSecond) End Function |
Twitter uses OAuth authorization to validate all requests. One of the required OAuth parameters is an epoch timestamp (i.e., the number of seconds elapsed since January 1, 1970 00:00:00 GMT). The get_timestamp function provides a timestamp string in the required format.
|
Public Function get_timestamp() As String get_timestamp = DateDiff("s", #1/1/1970#, LocalTimeToUTC(Now)) End Function |
The EncodeBase64 function takes an array of bytes and converts it to a base64-encoded string. It’s used to convert binary data into an ASCII string. I thought this was a pretty slick technique; create an XML document (using the MSXML2.DOMDocument object), create a node and and assign the binary data to it. Then, simply read the same node’s Text property (MSMXL2.DOMDocument will handle the conversion automatically). Thanks to Tim Hastings for the code.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17
|
Public Function EncodeBase64(ByRef arrData() As Byte) As String Dim objXML As MSXML2.DOMDocument Dim objNode As MSXML2.IXMLDOMElement Set objXML = New MSXML2.DOMDocument ' byte array to base64 Set objNode = objXML.createElement("b64") objNode.DataType = "bin.base64" objNode.nodeTypedValue = arrData EncodeBase64 = objNode.Text Set objNode = Nothing Set objXML = Nothing End Function |
The Base64_HMACSHA1 function utilizes a couple of classes from the .Net System namespace to hash a string into a SHA-1 digest and calculate an HMAC authentication code using a secret key. (you can use the VBA CreateObject method to utilize any .Net classes that are ‘explicitly exposed as COM components’) Thanks to HK1 (via StackOverflow) for the code.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21
|
Public Function Base64_HMACSHA1(ByVal sTextToHash As String, ByVal sSharedSecretKey As String) As String Dim asc As Object Dim enc As Object Dim TextToHash() As Byte Dim SharedSecretKey() As Byte Set asc = CreateObject("System.Text.UTF8Encoding") Set enc = CreateObject("System.Security.Cryptography.HMACSHA1") TextToHash = asc.Getbytes_4(sTextToHash) SharedSecretKey = asc.Getbytes_4(sSharedSecretKey) enc.key = SharedSecretKey Dim bytes() As Byte bytes = enc.ComputeHash_2((TextToHash)) Base64_HMACSHA1 = EncodeBase64(bytes) Set asc = Nothing Set enc = Nothing End Function |
The binary_file_to_string function takes a binary file (such as an image file) and converts it to a string. We’ll eventually use this function when we want to include images with our tweets. Nothing too complicated here; read a binary file into a byte array, then use VBA’s built-in StrConv function to convert the byte array to a Unicode string.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22
|
Public Function binary_file_to_string(strFileName) As String Dim bytArr() As Byte Dim strConvData As String Dim intFile As Integer intFile = FreeFile Open strFileName For Binary Access Read As intFile If LOF(intFile) > 0 Then ReDim bytArr(0 To LOF(intFile) - 1) As Byte Get intFile, , bytArr strConvData = StrConv(bytArr, vbUnicode) End If Close intFile binary_file_to_string = strConvData End Function |
Later in our code, we’ll need to generate string values that are unique (or, at least have a very high likelihood of being so). The get_GUID function provides an easy way to generate such a string (note we strip out non-alphaniumeric characters, hence the removal of the GUID’s hyphens and outer braces.) Thanks to Oorang via vbaexpress.com for the code.
|
Public Function get_GUID() As String get_GUID = Replace(Mid$(CreateObject("Scriptlet.TypeLib").Guid, 2, 36), "-", "") End Function |
The strToByteArray function takes a string and converts it to a byte array using VBA’s StrConv function. When including an image with a tweet, we’ll use this function to prepare the body of the HTTP request.
|
Private Function strToByteArray(sText As String) As Byte() strToByteArray = StrConv(sText, vbFromUnicode) End Function |
The UrlEncode function converts non-ASCII characters into a percent-encoded format (e.g., a space becomes ‘%20’). This function creates an MSScriptControl.ScriptControl object, and sets the language to Jscript (the MS implementation of JavaScript). We can then execute JavaScript functions, specifically, the built-in encodeURIComponent function, which will properly URLencode text passed to it.
|
Public Function UrlEncode(str As String) Dim objEnc As MSScriptControl.ScriptControl Set objEnc = New MSScriptControl.ScriptControl objEnc.Language = "JScript" objEnc.AddCode "function encode(str) {return encodeURIComponent(str);}" Dim encoded As String encoded = objEnc.Run("encode", str) UrlEncode = encoded End Function |
TWITTER FUNCTIONS
With our army of helper functions ready to pitch in, it’s time to tweet!
In order to post a status update (i.e., tweet), we will (broadly) need to:
- Calculate a special authentication signature
- Send a properly-constructed http request
Sounds simple enough, but calculating the signature involves a few distinct steps. Let’s expand our to-do list:
- Calculate a special authentication signature
- Create a random text string that is likely to be unique
- Calculate an epoch timestamp
- Create a base string with which to calculate our authentication signature
- Create a special key with which to calculate our authentication signature
- Create authentication signature
- Send a properly constructed http request
We’ll take each section in order.
1. Calculate a special authentication signature
To create a random, unique text string (called a nonce), we’ll use the get_GUID helper function.
|
'create nonce Dim strNonce As String strNonce = get_GUID() |
To calculate an epoch timestamp, we’ll use the get_timestamp helper function.
|
'create timestamp Dim strTimestamp As String strTimestamp = get_timestamp() |
Next, we need to create the base string which we’ll use to calculate our authentication signature. To do so, we’ll use a function called get_basestring, which takes our nonce, timestamp, and our tweet text (hereafter called ‘status’) strings as arguments.
|
'create base string Dim strBase As String strBase = get_basestring(strNonce, strTimestamp, strStatus) |
The get_basestring function simply constructs a base string in accordance with the OAuth spec (or, at least, Twitter’s implementation of it)
|
Public Function get_basestring(strNonce As String, strTimestamp As String, strStatus As String) As String Dim tmpBase As String tmpBase = "oauth_consumer_key=" & cnsOauthConsumerKey tmpBase = tmpBase & "&" & "oauth_nonce=" & strNonce tmpBase = tmpBase & "&" & "oauth_signature_method=" & cnsMethod tmpBase = tmpBase & "&" & "oauth_timestamp=" & strTimestamp tmpBase = tmpBase & "&" & "oauth_token=" & cnsOauthToken tmpBase = tmpBase & "&" & "oauth_version=" & cnsOauthVersion tmpBase = tmpBase & "&" & "status=" & strStatus get_basestring = cnsAPIMethodP & "&" & UrlEncode((cnsURLPost)) & "&" & UrlEncode(tmpBase) End Function |
A properly-constructed base string will look something like this:
POST&https%3A%2F%2Fapi.twitter.com%2F1.1%2Fstatuses%2Fupdate.json&oauth_consumer_key%3Dyourtwitterconsumerkey%26oauth_nonce%3D47f42578fa5fc14027e96eff3b093c04%26oauth_signature_method%3DHMAC-SHA1%26oauth_timestamp%3D1370320828%26oauth_token%3Dyourtwitteraccesstoken%26oauth_version%3D1.0%26status%3DHello%252C%2520world%2521
If you ever have having trouble getting your code to work (and hopefully you won’t), verify that you have generated a properly-formed base string. On your application page in your Twitter developer account, there’s a ‘OAuth Tool’ tab which will let you manually input the various required parameters and show you what Twitter expects as a base string. Make sure your base string matches Twitter’s expected base string exactly.
Now that we have the base string, we need the composite key we’ll use in conjunction with the base string to calculate our authentication signature. The composite key is simply our consumer secret value joined to our access token secret value with the ‘&’ character.
|
'create composite key Dim strKey As String strKey = cnsOauthConsumerSecret & "&" & cnsOauthTokenSecret |
Now that we have our base string and our composite key, we run both through the Base64_HMACSHA1 helper function, then URL encode the result with UrlEncode.
|
'calculate oauth_signature Dim strOauthSig As String strOauthSig = UrlEncode(Base64_HMACSHA1(strBase, strKey)) |
Your signature will look something like: FPMPdE262aH6keADBjV+x8MLkcc=
2. Send a properly constructed http request
Next, we’ll create a WinHttp.WinHttpRequest object to submit our request. We provide the method, destination URL, and set asynchronous mode to False. For text-only tweets, the Content-Type header should be “application/x-www-form-urlencoded”
|
Dim objRest As WinHttp.WinHttpRequest Set objRest = New WinHttp.WinHttpRequest objRest.Open cnsAPIMethodP, cnsURLPost, False objRest.setRequestHeader "Content-Type", "application/x-www-form-urlencoded" |
Next, we need to send the ‘Authorization’ header; like the base string, it includes various OAuth-related values. Twitter also seems to have very specific rules about the format. For example, the OAuth spec (per my reading) seems to state that the space between comma-separated name-value pairs is optional (“Parameters are separated by a “,” character (ASCII code 44) and OPTIONAL linear whitespace per RFC2617.”), but in my testing, Twitter won’t process requests which omit this ‘optional’ whiteaapce. To ensure that the header gets constructed/formatted as Twitter expects it, we’ll use the get_header function.
|
Public Function get_header(strNonce As String, strTimestamp As String, strSignature As String) As String Dim tmpHeader As String tmpHeader = "OAuth oauth_consumer_key=" & Chr(34) & cnsOauthConsumerKey & Chr(34) tmpHeader = tmpHeader & ", oauth_nonce=" & Chr(34) & strNonce & Chr(34) tmpHeader = tmpHeader & ", oauth_signature=" & Chr(34) & strSignature & Chr(34) tmpHeader = tmpHeader & ", oauth_signature_method=" & Chr(34) & cnsMethod & Chr(34) tmpHeader = tmpHeader & ", oauth_timestamp=" & Chr(34) & strTimestamp & Chr(34) tmpHeader = tmpHeader & ", oauth_token=" & Chr(34) & cnsOauthToken & Chr(34) tmpHeader = tmpHeader & ", oauth_version=" & Chr(34) & cnsOauthVersion & Chr(34) get_header = tmpHeader End Function |
Add the properly-constructed Authorization header to the http request.
|
'create authorization header Dim strHeader As String strHeader = get_header(strNonce, strTimestamp, strOauthSig) objRest.setRequestHeader "Authorization", strHeader |
Finally, we provide our tweet text as the body of the POST request, sent as a ‘status=’ name-value pair (for purposes of this example, assume our url-encoded tweet text has been previously stored in the variable strStatus). We then wait for a response from the server before continuing.
|
objRest.send "status=" & strStatus objRest.waitForResponse |
Putting the preceding together gives us the full send_tweet function, which takes the tweet text as a string and returns a boolean indicating whether or not the tweet attempt was successful (via a ‘200’ http status response).
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55
|
Public Function send_tweet(strStatus As String) As Boolean 'URL encode status text strStatus = UrlEncode(strStatus) 'create nonce Dim strNonce As String strNonce = get_GUID() 'create timestamp Dim strTimestamp As String strTimestamp = get_timestamp() 'create basestring Dim strBase As String strBase = get_basestring(strNonce, strTimestamp, strStatus) 'create composite key Dim strKey As String strKey = cnsOauthConsumerSecret & "&" & cnsOauthTokenSecret 'calculate oauth_signature Dim strOauthSig As String strOauthSig = UrlEncode(Base64_HMACSHA1(strBase, strKey)) 'create Authorization header Dim strHeader As String strHeader = get_header(strNonce, strTimestamp, strOauthSig) 'send tweet Dim objRest As WinHttp.WinHttpRequest Set objRest = New WinHttp.WinHttpRequest objRest.Open cnsAPIMethodP, cnsURLPost, False objRest.setRequestHeader "Content-Type", "application/x-www-form-urlencoded" objRest.setRequestHeader "Authorization", strHeader objRest.send "status=" & strStatus objRest.waitForResponse 'Following are useful for debugging/comparing with Twitter OAuth tool; uncomment as needed/desired 'Debug.Print "BASE: " & strBase 'Debug.Print " SIG: " & strOauthSig 'Debug.Print "HEAD:" & strHeader 'Debug.Print objRest.Status & " -- " & objRest.StatusText If objRest.Status = "200" Then send_tweet = True Else send_tweet = False End If Set objRest = Nothing End Function |
Here’s the preceding code in action.

If we want to include an image with our tweet, we need to make a few alterations. While much of the OAuth preparation is the same, the base string for tweets with images gets crafted a bit differently (namely, the status text is not included). Accordingly, we’ll use a new get_media_basestring function.
|
Public Function get_media_basestring(strNonce As String, strTimestamp As String) As String Dim tmpBase As String tmpBase = "oauth_consumer_key=" & cnsOauthConsumerKey tmpBase = tmpBase & "&" & "oauth_nonce=" & strNonce tmpBase = tmpBase & "&" & "oauth_signature_method=" & cnsMethod tmpBase = tmpBase & "&" & "oauth_timestamp=" & strTimestamp tmpBase = tmpBase & "&" & "oauth_token=" & cnsOauthToken tmpBase = tmpBase & "&" & "oauth_version=" & cnsOauthVersion get_media_basestring = cnsAPIMethodP & "&" & UrlEncode((cnsURLMedia)) & "&" & UrlEncode(tmpBase) End Function |
The http request also gets constructed a little differently. While the Authorization header is the same (and can be constructed using the functions previously discussed), we will have to send our request to a different URL (stored in cnsURLMedia), and the Content-Type is set to ‘multipart/form-data’.
Handling this multipart form data is the biggest difference between tweeting plain text and tweeting with an image. Since we’ll be sending multipart data, we first need to provide a ‘delimiter’ (called a boundary) between the various parts (see this StackOverflow thread for some helpful discussions). This boundary must basically be a specific pattern of hyphens followed by a string of characters that does not appear anywhere else in our data. Seems the easiest way to accomplish that is to generate another, random, likely-unique text string using our get_GUID function.
|
Dim strBound As String strBound = get_GUID() Dim objRest As WinHttp.WinHttpRequest Set objRest = New WinHttp.WinHttpRequest objRest.Open cnsAPIMethodP, cnsURLMedia, False objRest.setRequestHeader "Authorization", strHeader objRest.setRequestHeader "Content-Type", "multipart/form-data;boundary=" & strBound |
We then need to construct the body of our POST request. Everything (tweet text, image data, and image metadata) will be included within the request body. To properly construct this portion of the request, we’ll use the get_media_http_body function, which takes our tweet text, boundary ‘delimiter’, and image file path (location and name) as arguments, and outputs a text string containing everything we’ll need to send.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17
|
Public Function get_media_http_body(strStatus As String, strBound As String, strFilePath As String) As String Dim strFileName As String strFileName = Mid(strFilePath, InStrRev(strFilePath, "\") + 1, Len(strFilePath)) strTmp = "--" & strBound & vbCrLf strTmp = strTmp & "Content-Disposition: form-data; name=" & Chr(34) & "status" & Chr(34) & vbCrLf & vbCrLf strTmp = strTmp & strStatus & vbCrLf strTmp = strTmp & "--" & strBound & vbCrLf strTmp = strTmp & "Content-Type: application/octet-stream" & vbCrLf strTmp = strTmp & "Content-Disposition: form-data; name=" & Chr(34) & "media[]" & Chr(34) & "; filename=" & Chr(34) & strFileName & Chr(34) & vbCrLf & vbCrLf strTmp = strTmp & binary_file_to_string(strFilePath) & vbCrLf strTmp = strTmp & "--" & strBound & "--" & vbCrLf get_media_http_body = strTmp End Function |
Output will look something like this (the blank line before the status text the and hyphens around the boundary values are significant!):
–DFA78463375D43848B36F76E30508002
Content-Disposition: form-data; name=”status”
Our lovely state flag.
–DFA78463375D43848B36F76E30508002
Content-Type: application/octet-stream
Content-Disposition: form-data; name=”media[]”; filename=”oregon_flag.png”
[binary image data converted to text goes here]
–DFA78463375D43848B36F76E30508002–
Once we have the body request, we convert it to a byte array using strToByteArray, then send it on its way.
|
objRest.send strToByteArray(strBody) |
Here’s the whole thing put together in the send_tweet_with_media function.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63
|
Public Function send_tweet_with_media(strStatus As String, strFile As String) As Boolean Dim strBound As String strBound = get_GUID() Dim strBody As String strBody = get_media_http_body(strStatus, strBound, strFile) 'create nonce Dim strNonce As String strNonce = get_GUID() 'create timestamp Dim strTimestamp As String strTimestamp = get_timestamp() 'create basestring Dim strBase As String strBase = get_media_basestring(strNonce, strTimestamp) 'create composite key Dim strKey As String strKey = cnsOauthConsumerSecret & "&" & cnsOauthTokenSecret 'calculate oauth_signature Dim strOauthSig As String strOauthSig = UrlEncode2(Base64_HMACSHA1(strBase, strKey)) 'create authorization header Dim strHeader As String strHeader = get_header(strNonce, strTimestamp, strOauthSig) 'send tweet Dim objRest As WinHttp.WinHttpRequest Set objRest = New WinHttp.WinHttpRequest objRest.Open cnsAPIMethodP, cnsURLMedia, False objRest.setRequestHeader "Authorization", strHeader objRest.setRequestHeader "Content-Type", "multipart/form-data;boundary=" & strBound objRest.send strToByteArray(strBody) objRest.waitForResponse 'Twitter limits the number of images you can tweet in any given period of time. When tweeting with an image, the http response will contain 'the following headers which you can examine to see how close you are to reaching your limit. 'X-MediaRateLimit-Limit - Indicates the total pieces of media the current user may upload before the time indicated in X-MediaRateLimit-Reset. 'X-MediaRateLimit-Remaining - The remaining pieces of media the current user may upload before the time indicated in X-MediaRateLimit-Reset. 'X-MediaRateLimit-Reset - A UTC-based timestamp (in seconds) indicating when X-MediaRateLimit-Remaining will reset to the value in X-MediaRateLimit-Limit and the user can resume uploading media. 'so, for example: 'Debug.Print objRest.getResponseHeader("X-MediaRateLimit-Limit") 'Debug.Print objRest.getResponseHeader("X-MediaRateLimit-Remaining") 'Debug.Print objRest.getResponseHeader("X-MediaRateLimit-Reset") If objRest.Status = "200" Then send_tweet_with_media = True Else send_tweet_with_media = False End If Set objRest = Nothing End Function |
Here’s the preceding code in action.

GETTING TWITTER CONFIGURATION INFO
Twitter’s current system status and various limits (e.g., maximum image size, maximum number of characters that will be used in a t.co link wrapper, etc…) are constantly being updated. A full list of these limits/status indicators can be found here.
Querying some of this info before attempting to post your tweet can be very useful (for example, to make sure your image isn’t too large, or to verify your tweet will be 140 characters or less after Twitter wraps your link). Retrieving these various values is very similar to the tweeting functions we’ve reviewed already. There are a few notable differences:
In addition to pointing to using a different URL, the http request to the configuration URL uses GET instead of POST. The previous base string functions both use a constant (set to “POST”) for the request method, so when retrieving status info, make sure you’re calculating the base string using “GET” as the request method (I use a different constant, cnsAPIMethodG).
|
Public Function get_status_basestring(strNonce As String, strTimestamp As String) As String Dim tmpBase As String tmpBase = "oauth_consumer_key=" & cnsOauthConsumerKey tmpBase = tmpBase & "&" & "oauth_nonce=" & strNonce tmpBase = tmpBase & "&" & "oauth_signature_method=" & cnsMethod tmpBase = tmpBase & "&" & "oauth_timestamp=" & strTimestamp tmpBase = tmpBase & "&" & "oauth_token=" & cnsOauthToken tmpBase = tmpBase & "&" & "oauth_version=" & cnsOauthVersion get_status_basestring = cnsAPIMethodG & "&" & UrlEncode((cnsURLStatus)) & "&" & UrlEncode(tmpBase) End Function |
The configuration request doesn’t require anything to be sent in the request body. The server response will be in JSON format. There are several VBA JSON parsing modules out there (see ‘Other Resources’), but again, I’m using the capabilities of the built-in MSScriptcontrol.ScriptControl (which, admittedly, are pretty basic and may not suffice for all scenarios). Basically, we read the JSON response from the http request into a ScriptControl object, then use the Eval method to create an object having properties based on the JSON field names.
(full disclosure: I’m an XML guy, and have only recently started to work with JSON on a somewhat regular basis. There may be infinitely more efficient/robust ways to work with JSON in VBA, but this sufficed for my immediate purposes. Please feel free to offer suggestions for improvement/enhancement!)
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28
|
Dim objRest As WinHttp.WinHttpRequest Set objRest = New WinHttp.WinHttpRequest objRest.Open cnsAPIMethodG, cnsURLStatus, False objRest.setRequestHeader "Content-Type", "application/x-www-form-urlencoded" objRest.setRequestHeader "Authorization", strHeader objRest.send objRest.waitForResponse Dim objJson As MSScriptControl.ScriptControl Set objJson = New MSScriptControl.ScriptControl objJson.Language = "JScript" Dim objResp As Object Set objResp = objJson.Eval("(" & objRest.responseText & ")") If objRest.Status = "200" Then 'use names from https://dev.twitter.com/docs/api/1.1/get/help/configuration 'Get maximum t.co wrapped link length Debug.Print CLng(objResp.short_url_length) 'Get maximum photo file size Debug.Print CLng(objResp.photo_size_limit) 'Etc... Else Debug.Print "Error" End If |
Here’s everything put together in a function called get_max_link_len which returns the current maximum t.co link wrapper length (or, alternatively, -1 in case of an error).
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54
|
Public Function get_max_link_len() As Integer 'create nonce Dim strNonce As String strNonce = get_GUID() 'create timestamp Dim strTimestamp As String strTimestamp = get_timestamp() 'create basestring Dim strBase As String strBase = get_status_basestring(strNonce, strTimestamp) 'create composite key Dim strKey As String strKey = cnsOauthConsumerSecret & "&" & cnsOauthTokenSecret 'calculate oauth_signature Dim strOauthSig As String strOauthSig = UrlEncode(Base64_HMACSHA1(strBase, strKey)) 'create authorization header Dim strHeader As String strHeader = get_header(strNonce, strTimestamp, strOauthSig) 'get status Dim objRest As WinHttp.WinHttpRequest Set objRest = New WinHttp.WinHttpRequest objRest.Open cnsAPIMethodG, cnsURLStatus, False objRest.setRequestHeader "Content-Type", "application/x-www-form-urlencoded" objRest.setRequestHeader "Authorization", strHeader objRest.send objRest.waitForResponse Dim objJson As MSScriptControl.ScriptControl Set objJson = New MSScriptControl.ScriptControl objJson.Language = "JScript" Dim objResp As Object Set objResp = objJson.Eval("(" & objRest.responseText & ")") If objRest.Status = "200" Then get_max_link_len = CLng(objResp.short_url_length) Else get_max_link_len = -1 End If Set objRest = Nothing Set objJson = Nothing Set objResp = Nothing End Function |
SHORTENING URLS
Finally, to conserve precious tweet characters, you may want to shorten your links before including them in your tweet (Twitter will automatically wrap the link itself using the t.co shortener, but what’s displayed in your tweet (and counts toward your 140 character limit) will be the longer URL you provided. For example, if I tweet the status “My new favorite site is http://www.chocolatecoveredeverything.com!”, Twitter will represent the link as <a href=”http://t.co/xxxxxx”>http://www.chocolatecoveredeverything.com</a>; the URL that gets accessed when a user clicks on your tweet gets shortened to a t.co address, but the displayed URL text doesn’t (and counts against your 140 character limit).
So, we can run any links through a link shortener before posting. The get_short_link function can be used on any link you include in your tweet. You’ll have to sign up for an API key for Google’s link shortener service. Also note that the http request body has to be in JSON format.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30
|
Public Function get_short_link(strUrl As String) As String Dim objRest As WinHttp.WinHttpRequest Set objRest = New WinHttp.WinHttpRequest objRest.Open "POST", cnsGoogleShortenerURL & "?key=" & cnsGoogleShortenerKey, False objRest.setRequestHeader "Content-Type", "application/json" objRest.send "{" & Chr(34) & "longUrl" & Chr(34) & ": " & Chr(34) & strUrl & Chr(34) & "}" objRest.waitForResponse Dim objJson As MSScriptControl.ScriptControl Set objJson = New MSScriptControl.ScriptControl objJson.Language = "JScript" Dim objResp As Object Set objResp = objJson.Eval("(" & objRest.responseText & ")") If objRest.Status = "200" Then get_short_link = UrlEncode(CStr(objResp.id)) Else 'if there's an error, just use the original, long URL get_short_link = UrlEncode(strUrl) End If Set objResp = Nothing Set objRest = Nothing Set objJson = Nothing End Function |
OTHER RESOURCES
The following are some resources which I found particularly helpful as I was working on this project. There are also links to some alternate means of parsing JSON, calculating HMACSHA1 signatures, etc…
- Using Excel as Twitter Client with OAuth – Extremely helpful walkthrough/sample code. This was really the starting point for my efforts; I can’t give Adrianus enough credit. Thanks!
- Twitter API v1.1 documentation – A little spartan at times, but better than nothing!
- Twitter API error codes – If your HTTP request is returning anything other than a ‘200’ response, check here to see what the problem might be.
- OAuth protocol – Useful for figuring out how to configure OAuth requests, but it seems as if Twitter’s implementation differs every so often from the spec. When in doubt, go with Twitter’s implementation!
- If you don’t want to use built-in objects like MSScriptControl, here are some links to code you can import/use instead:
- (added Jun. 20, 2013) Also see my follow-up post about pulling user timelines with VBA.
Hope this proves helpful to others. Comments/questions/error reports welcome!
ADDENDA
7/4/2013 – In playing around with some other Twitter APIs (for example, users/lookup.json), I discovered that some APIs send responses using gzip compression. Unfortunately, the WinHttp.WinHttpRequest object won’t automatically decompress gzip responses. Instead, you’ll want to add a reference to ‘Microsoft XML, v6.0’ in the VBA IDE, and instead of using
|
Dim objRest As WinHttp.WinHttpRequest Set objRest = New WinHttp.WinHttpRequest |
you’ll want to use this instead
|
Dim objRest As MSXML2.XMLHTTP60 Set objRest = New MSXML2.XMLHTTP60 |
The XMLHttp object’s methods and properties are largely the same as those of WinHttp.WinHttpRequest. You will, however, have to remove the
objRest.waitForResponse statement, as XMLHttp doesn’t have that method. Instead, you could do something like
|
While objRest.ReadyState <> 4 DoEvents Wend |