Skip to content

Instantly share code, notes, and snippets.

@EelcoKoster
Last active September 12, 2022 07:51
Show Gist options
  • Save EelcoKoster/326aa7d1b1338806b058ddcc404622b6 to your computer and use it in GitHub Desktop.
Save EelcoKoster/326aa7d1b1338806b058ddcc404622b6 to your computer and use it in GitHub Desktop.
Simple class for sending and searching tweets on Twitter using Single-user OAuth.
using System;
using System.Collections.Generic;
using System.Linq;
using System.Net.Http;
using System.Security.Cryptography;
using System.Text;
using System.Threading.Tasks;
namespace TwitterSearch
{
/// <summary>
/// Simple class for sending and searching tweets on Twitter using Single-user OAuth.
/// https://dev.twitter.com/oauth/overview/single-user
///
/// Get your access keys by creating an app at apps.twitter.com then visiting the
/// "Keys and Access Tokens" section for your app. They can be found under the
/// "Your Access Token" heading.
///
/// Credits to Danny Tuppeny for the initial version:
/// https://blog.dantup.com/2016/07/simplest-csharp-code-to-post-a-tweet-using-oauth/
/// </summary>
class TwitterApi
{
const string TwitterApiBaseUrl = "https://api.twitter.com/1.1/";
readonly string consumerKey, consumerKeySecret, accessToken, accessTokenSecret;
readonly HMACSHA1 sigHasher;
readonly DateTime epochUtc = new DateTime(1970, 1, 1, 0, 0, 0, DateTimeKind.Utc);
/// <summary>
/// Creates an object for sending tweets to Twitter using Single-user OAuth.
///
/// Get your access keys by creating an app at apps.twitter.com then visiting the
/// "Keys and Access Tokens" section for your app. They can be found under the
/// "Your Access Token" heading.
/// </summary>
public TwitterApi(string consumerKey, string consumerKeySecret, string accessToken, string accessTokenSecret)
{
this.consumerKey = consumerKey;
this.consumerKeySecret = consumerKeySecret;
this.accessToken = accessToken;
this.accessTokenSecret = accessTokenSecret;
sigHasher = new HMACSHA1(new ASCIIEncoding().GetBytes(string.Format("{0}&{1}", consumerKeySecret, accessTokenSecret)));
}
/// <summary>
/// Sends a tweet with the supplied text and returns the response from the Twitter API.
/// </summary>
public Task<string> Tweet(string text)
{
var data = new Dictionary<string, string> {
{ "status", text },
{ "trim_user", "1" }
};
return SendRequest("statuses/update.json", HttpMethod.POST, data);
}
/// <summary>
/// Search for tweets using the Twitter API.
/// </summary>
public Task<string> Search(string search)
{
return SendRequest(string.Format("search/tweets.json?q={0}", search), HttpMethod.GET);
}
/// <summary>
/// Retrieve user settings from the Twitter API.
/// </summary>
public Task<string> Settings()
{
return SendRequest("account/settings.json", HttpMethod.GET);
}
Task<string> SendRequest(string url, HttpMethod httpMethod, Dictionary<string, string> data = null)
{
var fullUrl = TwitterApiBaseUrl + url;
Random rand = new Random();
// Timestamps are in seconds since 1/1/1970.
var timestamp = (int)((DateTime.UtcNow - epochUtc).TotalSeconds);
if (data == null) data = new Dictionary<string, string>();
// Add all the OAuth headers and querystring parameters, we'll need to use when constructing the hash.
var query = url.Split('?');
if (query.Count() > 1)
{
var pairs = query[1].Split('&');
foreach (var pair in pairs)
{
var keyvalue = pair.Split('=');
data.Add(keyvalue[0], keyvalue[1]);
}
}
data.Add("oauth_consumer_key", consumerKey);
data.Add("oauth_signature_method", "HMAC-SHA1");
data.Add("oauth_timestamp", timestamp.ToString());
data.Add("oauth_nonce", rand.Next(10000000, 999999999).ToString());
data.Add("oauth_token", accessToken);
data.Add("oauth_version", "1.0");
// Generate the OAuth signature and add it to our payload.
data.Add("oauth_signature", GenerateSignature(fullUrl, data, httpMethod));
// Build the OAuth HTTP Header from the data.
string oAuthHeader = GenerateOAuthHeader(data);
switch(httpMethod){
case HttpMethod.GET:
return SendRequest(fullUrl, oAuthHeader, null, httpMethod);
case HttpMethod.POST:
var formData = new FormUrlEncodedContent(data.Where(kvp => !kvp.Key.StartsWith("oauth_")));
return SendRequest(fullUrl, oAuthHeader, formData, httpMethod);
default: return null;
}
}
/// <summary>
/// Generate an OAuth signature from OAuth header values.
/// </summary>
string GenerateSignature(string url, Dictionary<string, string> data, HttpMethod httpMethod)
{
var sigString = string.Join(
"&",
data
.Union(data)
.Select(kvp => string.Format("{0}={1}", Uri.EscapeDataString(kvp.Key), Uri.EscapeDataString(kvp.Value)))
.OrderBy(s => s)
);
string urlWithoutParameters = url.Split('?')[0];
var fullSigData = string.Format(
"{0}&{1}&{2}",
httpMethod.ToString(),
Uri.EscapeDataString(urlWithoutParameters),
Uri.EscapeDataString(sigString.ToString())
);
return Convert.ToBase64String(sigHasher.ComputeHash(new ASCIIEncoding().GetBytes(fullSigData.ToString())));
}
/// <summary>
/// Generate the raw OAuth HTML header from the values (including signature).
/// </summary>
string GenerateOAuthHeader(Dictionary<string, string> data)
{
return "OAuth " + string.Join(
",",
data
.Where(kvp => kvp.Key.StartsWith("oauth_"))
.Select(kvp => string.Format("{0}=\"{1}\"", Uri.EscapeDataString(kvp.Key), Uri.EscapeDataString(kvp.Value)))
.OrderBy(s => s)
);
}
/// <summary>
/// Send HTTP Request and return the response.
/// </summary>
async Task<string> SendRequest(string fullUrl, string oAuthHeader, FormUrlEncodedContent formData, HttpMethod httpMethod)
{
using (var http = new HttpClient())
{
http.DefaultRequestHeaders.Add("Authorization", oAuthHeader);
HttpResponseMessage httpResp = null;
switch (httpMethod) {
case HttpMethod.GET: httpResp = await http.GetAsync(fullUrl);
break;
case HttpMethod.POST: httpResp = await http.PostAsync(fullUrl, formData);
break;
}
var respBody = await httpResp.Content.ReadAsStringAsync();
return respBody;
}
}
}
public enum HttpMethod
{
POST,
GET
}
}
@vishal119
Copy link

I am trying to use the get method , by replacing my keys . I am getting 403 , any idea to this??

@EelcoKoster
Copy link
Author

EelcoKoster commented Sep 9, 2022

Do you have a code example?
403 means an authentication error, so maybe your keys are incorrect? Did you get your access keys by creating an app at apps.twitter.com?
Did you give the app "Read and write" permissions?

I just created a .net6 console app with my keys and the code works (on my machine :-))
Example:

var twitterApi = new TwitterSearch.TwitterApi(apikey, apikeySecret, accesstoken, accesstokenSecret);

// Search for tweets with term "Github"
var result = await twitterApi.Search("Github");
Console.WriteLine(result);

// Create a tweet
await twitterApi.Tweet("Test - automated tweet.");

// Retrieve the settings of logged in user
var settings = await twitterApi.Settings();
Console.WriteLine(settings);

@EelcoKoster
Copy link
Author

Hi vishal119, I removed your latest comment because it contained keys en secrets you should not put openly on the internet.

I did try your keys... And it came back with the following message: {"errors":[{"message":"You currently have Essential access which includes access to Twitter API v2 endpoints only. If you need access to this endpoint, you'll need to apply for Elevated access via the Developer Portal. You can learn more here: https://developer.twitter.com/en/docs/twitter-api/getting-started/about-twitter-api#v2-access-leve","code":453}]}

The code (6 years old) uses Twitter API v1.1. But your keys only have Essential access which only have access to v2? You have to convert your account to Elevated access. It is free, but I don't now how it works... (my account was created long ago and automatically got Elevated access).

Hope this helps!

@vishal119
Copy link

Holy Moly.. that was the reason . Thanks a lot for it. I know that i should not put it openly but no other go was there . Yes the elevated access was the reason . Now its working fine. Thanks a ton.

@EelcoKoster
Copy link
Author

EelcoKoster commented Sep 9, 2022 via email

@vishal119
Copy link

Hey , i noticed that when i added more than a word , its giving error . However in single word its working fine.

// Search for tweets with term "Github"
var result = await twitterApi.Search("Github"); // This one works fine
Console.WriteLine(result);

// Search for tweets with term "New Moon"
var result = await twitterApi.Search("New Moon"); // this give auth issue , that you dont have access
Console.WriteLine(result);

Any ideas to this ??

@EelcoKoster
Copy link
Author

I removed line 64: search = Uri.EscapeDataString(search);
The escaping of the search query was handled different when the authentication token was created.
Removing the EscapeDataString solves this problem.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment