Я бы хотел рассмотреть, в данном посте, интеграцию Twitter в ваше third-party application с использованием OAuth и написанном на C#. На этот раз, это будет очень технический пост с большим количеством кода, сразу предупреждаю.
OAuth, что да как?!
Сразу оговорюсь, может я не прав с подходом и пониманием ситуации в целом, и я может очень критичен, НО, когда сервис пишет OAuth 2.0, то нормальный человек понимает, это OAuth 2.0, и в принципе в 90% случаев, пошлют вас посетить сайт http://oauth.net, но вы уж простите, там ведь черт ногу сломит, сидеть читать все это.
Twitter + OAuth
На примере Twitter мы имеем место где можно почитать про API и, в том числе, про OAuth - http://dev.twitter.com. Что дальше? Я советую пройти сюда для прочтения третьей части, это даст начальное понимание, в принципе. Дальше время писать код и имплементировать все это дело в приложение/класс/модуль, как вам угодно. И так, моя любимая картинка описывающая весь процесс получения магических токенов.
Заждались?
Я, как вы могли заметить, не больно люблю писать, так что давайте сразу к сути. Безупречность моего кода для меня нормальна, для кого-то может быть, не очень или ужасно. В тот момент времени в C#, я считал что все нормально, в принципе и сейчас так считаю, хотя разбить на несколько модулей это все можно было.
Consumer key & secret
Чтобы мы могли обращаться к Twitter API, нам потребуются Consumer Key и Consumer Secret, взять и зарегистрировать ваше личное приложение можно просто и быстро перейдя по Register an app далее или перейдя в Your apps вы получаете список ваших зарегистрированных приложений, а так же нужные ключи (Consumer Key, Consumer Secret).
Declarations
Перейдем к написанию части отвечающей за формирование подписи и включения всех необходимых частей для получения окончательного набора параметров OAuth пакета.
#region Properties
public enum Method { GET, POST };
#region Private
private const string ConsumerKey = "ВАШ_CONSUMER_KEY";
private const string ConsumerSecret = "ВАШ_CONSUMER_SECRET";
private readonly string Realm = string.Format(CultureInfo.InvariantCulture, "НашеСамоеКлассноеПриложение/{0}",
System.Reflection.Assembly.GetAssembly(typeof(Twitter)).GetName().Version.ToString());
#endregion
#region Public
public string Token { get; set; }
public string TokenSecret { get; set; }
public Dictionary<string, string> Parameters { get; private set; }
public Dictionary<string, string> OAuthParameters { get; private set; }
public Uri RequestUri { get; set; }
public Method RequestMethod { get; set; }
// кто не понимает что это и зачем, это Singleton ну и wikipedia в помощь
private static Twitter instance;
public static Twitter Instance
{
get
{
if (instance == null)
{
instance = new Twitter();
}
return instance;
}
}
#endregion
#endregion
Надеюсь вы все еще тут, т.к. это только начало, сейчас опишем более менее кратко все необходимое для подписи пакетов OAuth.
#region OAuth Helper Methods
// очень важно, по сути ставим стандартный набор oauth_* после чего заполняем если есть public token и secret token и в конце формируем oauth signature (подпись)
public void SetupOAuth()
{
OAuthParameters.Add("oauth_version", "1.0");
OAuthParameters.Add("oauth_nonce", GenerateNonce());
OAuthParameters.Add("oauth_timestamp", GenerateTimeStamp());
OAuthParameters.Add("oauth_signature_method", "HMAC-SHA1");
OAuthParameters.Add("oauth_consumer_key", ConsumerKey);
OAuthParameters.Add("oauth_consumer_secret", ConsumerSecret);
if (!string.IsNullOrEmpty(Token))
{
OAuthParameters.Add("oauth_token", Token);
}
if (!string.IsNullOrEmpty(TokenSecret))
{
OAuthParameters.Add("oauth_token_secret", TokenSecret);
}
var nonSecretParameters = (from p in this.OAuthParameters
where !(p.Key.EndsWith("_secret", StringComparison.OrdinalIgnoreCase) &&
!p.Key.EndsWith("_verifier", StringComparison.OrdinalIgnoreCase))
select p).Union(this.Parameters);
var urlForSigning = RequestUri;
var signatureBaseString = string.Format(CultureInfo.InvariantCulture, "{0}&{1}&{2}",
RequestMethod.ToString(), UrlEncode(NormalizeUrl(urlForSigning)), UrlEncode(nonSecretParameters));
var key = string.Format(CultureInfo.InvariantCulture, "{0}&{1}", UrlEncode(ConsumerSecret), UrlEncode(TokenSecret));
var hmacsha1 = new HMACSHA1(Encoding.ASCII.GetBytes(key));
var signatureBytes = hmacsha1.ComputeHash(Encoding.ASCII.GetBytes(signatureBaseString));
OAuthParameters.Add("oauth_signature", Convert.ToBase64String(signatureBytes));
}
// думаю в объяснения не нуждается, читаем Twitter API
public static string GenerateTimeStamp()
{
var ts = DateTime.UtcNow - new DateTime(1970, 1, 1, 0, 0, 0, 0);
return Convert.ToInt64(ts.TotalSeconds, CultureInfo.CurrentCulture).ToString(CultureInfo.CurrentCulture);
}
// думаю в объяснения не нуждается, читаем Twitter API
public static string GenerateNonce()
{
return new Random().Next(123400, int.MaxValue).ToString("X", CultureInfo.InvariantCulture);
}
// вполне вероятно, зря скопипастил этот кусок, но все же...
public static string NormalizeUrl(Uri url)
{
string normalizedUrl = string.Format(CultureInfo.InvariantCulture, "{0}://{1}", url.Scheme, url.Host);
if (!((url.Scheme == "http" && url.Port == 80) || (url.Scheme == "https" && url.Port == 443)))
{
normalizedUrl += ":" + url.Port;
}
normalizedUrl += url.AbsolutePath;
return normalizedUrl;
}
// параноя взяла свое, а если серьезно, просто не работает Twitter API если использовать родное преобразование
public static string UrlEncode(string value)
{
if (string.IsNullOrEmpty(value))
{
return string.Empty;
}
value = HttpUtility.UrlEncode(value).Replace("+", "%20");
value = Regex.Replace(value, "(%[0-9a-f][0-9a-f])", c => c.Value.ToUpper());
value = value.Replace("(", "%28")
.Replace(")", "%29")
.Replace("$", "%24")
.Replace("!", "%21")
.Replace("*", "%2A")
.Replace("'", "%27")
.Replace("%7E", "~");
return value;
}
// для облегчения жизни, ради этого и живем
private static string UrlEncode(IEnumerable<KeyValuePair<string, string>> parameters)
{
var parameterString = new StringBuilder();
var paramsSorted = from p in parameters
orderby p.Key, p.Value
select p;
foreach (var item in paramsSorted)
{
if (parameterString.Length > 0)
{
parameterString.Append("&");
}
parameterString.Append(
string.Format(
CultureInfo.InvariantCulture,
"{0}={1}",
UrlEncode(item.Key),
UrlEncode(item.Value)));
}
return UrlEncode(parameterString.ToString());
}
// в принципе ничего страшного, сортируем, выбираем только публичные ключи и ставим наш Realm
public string GenerateAuthorizationHeader()
{
var authHeaderBuilder = new StringBuilder();
authHeaderBuilder.AppendFormat("OAuth realm=\"{0}\"", Realm);
var sortedParameters = from p in this.OAuthParameters
where p.Key.StartsWith("oauth_") &&
!p.Key.EndsWith("_secret", StringComparison.OrdinalIgnoreCase) &&
p.Key != "oauth_signature" &&
p.Key != "oauth_verifier" &&
!string.IsNullOrEmpty(p.Value)
orderby p.Key, UrlEncode(p.Value)
select p;
foreach (var item in sortedParameters)
{
authHeaderBuilder.AppendFormat(
",{0}=\"{1}\"",
UrlEncode(item.Key),
UrlEncode(item.Value));
}
authHeaderBuilder.AppendFormat(",oauth_signature=\"{0}\"", UrlEncode(this.OAuthParameters["oauth_signature"]));
return authHeaderBuilder.ToString();
}
#endregion
HTTP Requests
Для отправки и приема пакетов (текста по сути) необходимо все это поднять как можно повыше, т.е. привести к виду вот параметры, вот ответ. Ну что же, давайте постараемся.
#region Requests
// если имеем параметры и запрос get, нужно сформировать строку вида http://host/parts/?query=parameters
private void AddQueryStringParametersToUri()
{
if (RequestMethod == Method.GET)
{
var requestParametersBuilder = new StringBuilder(RequestUri.AbsoluteUri);
requestParametersBuilder.Append(RequestUri.Query.Length == 0 ? "?" : "&");
foreach (KeyValuePair<string, string> item in Parameters)
{
requestParametersBuilder.AppendFormat("{0}={1}&", item.Key, Uri.EscapeDataString(item.Value));
}
if (requestParametersBuilder.Length == 0)
{
return;
}
requestParametersBuilder.Remove(requestParametersBuilder.Length - 1, 1);
RequestUri = new Uri(requestParametersBuilder.ToString());
}
}
// если имеем параметры и запрос POST, нужно записать в request stream все наши передаваемые параметры
private void AddFormFieldValuesToRequest(WebRequest request)
{
if (RequestMethod == Method.POST)
{
request.ContentType = "application/x-www-form-urlencoded";
var requestParametersBuilder = new StringBuilder();
foreach (KeyValuePair<string, string> item in Parameters)
{
requestParametersBuilder.AppendFormat("{0}={1}&", item.Key, Uri.EscapeDataString(item.Value));
}
if (requestParametersBuilder.Length == 0)
{
return;
}
requestParametersBuilder.Remove(requestParametersBuilder.Length - 1, 1);
var formData = Encoding.UTF8.GetBytes(requestParametersBuilder.ToString());
request.ContentLength = formData.Length;
var requestStream = request.GetRequestStream();
requestStream.Write(formData, 0, formData.Length);
requestStream.Close();
}
}
// начинаем постройку нового запроса, указываем какой типа запроса и ссылку на соответствующую API
private void BuildRequest(Method method, string url)
{
RequestUri = new Uri(url);
RequestMethod = method;
Parameters = new Dictionary<string, string>();
OAuthParameters = new Dictionary<string, string>();
if (!string.IsNullOrEmpty(RequestUri.Query))
{
foreach (Match item in Regex.Matches(RequestUri.Query, @"(?<key>[^&?=]+)=(?<value>[^&?=]+)"))
{
Parameters.Add(item.Groups["key"].Value, item.Groups["value"].Value);
}
RequestUri = new Uri(RequestUri.AbsoluteUri.Replace(RequestUri.Query, ""));
}
}
// приступаем к выполнению построенного ранее запроса (RequestUri, RequestMethod, Parameters, OAuthParameters)
public string ExecuteRequest()
{
SetupOAuth(); // заполняем все поля OAuth
AddQueryStringParametersToUri(); // см. выше
var request = (HttpWebRequest)WebRequest.Create(RequestUri);
request.Method = RequestMethod.ToString();
request.UserAgent = Realm;
request.ServicePoint.Expect100Continue = false;
request.Headers.Add("Authorization", GenerateAuthorizationHeader()); // не забываем о полях авторизации на стороне сервера
AddFormFieldValuesToRequest(request);
var response = request.GetResponse(); // выполняем запрос и получаем ответ
var responseReader = new StreamReader(response.GetResponseStream());
var responseData = responseReader.ReadToEnd(); // чтобы вернуть строку, в нашем случае XML Response
response.Close();
responseReader.Close();
responseReader = null;
request = null;
return responseData;
}
// пропускаем постройку и сразу переходим к выполнению запроса, просто такая функция помощник
public string ExecuteRequest(Method method, string url)
{
BuildRequest(method, url);
return ExecuteRequest();
}
#endregion
Twitter's API implementation
Прежде, чем начать написание функционала, требуется представить сущности такие как статус, пользователь, таймлайны и другое, необходимое в вашем конкретном случае. Логично подумав было решено использовать Serializable классы для десериализации полученных XML от Twitter API. Собственно как это дело описано у меня смотрим ниже.
Как сериализировать и десериализировать объекты, приведу две функции:
/// <summary>
/// Deserialize Xml String to CSharp Object
/// </summary>
/// <typeparam name="T">Class of the serializable CSharp Object</typeparam>
/// <param name="Xml">Xml String</param>
/// <returns>Serialized CSharp Object</returns>
public static T XmlDeserialize<T>(string Xml) where T : class
{
var xs = new XmlSerializer(typeof(T));
var reader = new StringReader(Xml);
return (T)xs.Deserialize(reader);
}
/// <summary>
/// Serialize Xml String to CSharp Object
/// </summary>
/// <typeparam name="T">Class of the serializable CSharp Object</typeparam>
/// <param name="Object">serializable CSharp Object</param>
/// <returns>XML String</returns>
public static string XmlSerialize<T>(T Object) where T : class
{
var xs = new XmlSerializer(typeof(T));
var writer = new StringWriter();
xs.Serialize(writer, Object);
return writer.ToString();
}
Дальше объявление сущностей
#region Declarations
[Serializable]
[XmlRoot("user")]
public class TwitterUser
{
[XmlElement("id")]
public long Id;
[XmlElement("name")]
public string Name;
[XmlElement("profile_image_url")]
public string ProfileImageUrl;
[XmlElement("screen_name")]
public string ScreenName;
[XmlElement("url")]
public string Url;
}
[Serializable]
[XmlRoot("status")]
public class TwitterStatus
{
[XmlElement("id")]
public long Id;
[XmlElement("text")]
public string Text;
[XmlElement("source")]
public string Source;
[XmlElement("user")]
public TwitterUser User;
[XmlIgnore()]
private string _CreatedAtOrg;
[XmlElement("created_at")]
public string CreatedAtOrg
{
get { return _CreatedAtOrg; }
set
{
_CreatedAtOrg = value;
CreatedAt = ParseDateTime(_CreatedAtOrg);
}
}
[XmlIgnore()]
public DateTime CreatedAt;
private static DateTime ParseDateTime(string date)
{
// <created_at>Sun Apr 25 11:25:52 +0000 2010</created_at>
try
{
var dateFormat = "ddd MMM dd HH:mm:ss zzzz yyyy";
return DateTime.ParseExact(date, dateFormat, System.Globalization.DateTimeFormatInfo.InvariantInfo);
}
catch
{
return DateTime.Now;
}
}
}
[Serializable]
[XmlRoot("direct_message")]
public class DirectMessage
{
[XmlElement("id")]
public long Id;
[XmlElement("sender_id")]
public long SenderId;
[XmlElement("text")]
public string Text;
[XmlElement("recipient_id")]
public long RecipientId;
[XmlElement("created_at")]
public DateTime CreatedAt;
[XmlElement("sender_screen_name")]
public string SenderScreenName;
[XmlElement("recipient_screen_name")]
public string RecipientScreenName;
[XmlElement("sender")]
public TwitterUser Sender;
[XmlElement("recipient")]
public TwitterUser Recipient;
}
#endregion
#region URL Generator
private const string Host = "https://twitter.com/";
private const string ResponseFormat = ".xml";
private const string ApiAccountVerifyCredentials = "account/verify_credentials";
private const string ApiAccountEndSession = "account/end_session";
private const string ApiStatusesHomeTimeline = "statuses/home_timeline";
private const string ApiStatusesUpdate = "statuses/update";
private const string ApiStatusesDestroy = "statuses/destroy";
private const string ApiDirectMessageNew = "direct_messages/new";
private string Url(string Api)
{
return string.Concat(Host, Api, ResponseFormat);
}
#endregion
#region Methods
private delegate TwitterUser VerifyCredentialsDelegate();
public TwitterUser VerifyCredentials()
{
var response = ExecuteRequest(Method.GET, Url(ApiAccountVerifyCredentials));
return Extensions.Helper.XmlDeserialize<TwitterUser>(response);
}
public delegate void TwitterVerifyCredentialsCallback(TwitterUser user, Exception e);
public void VerifyCredentialsAsync(TwitterVerifyCredentialsCallback callback)
{
(new Thread(new ThreadStart(delegate()
{
try
{
var user = VerifyCredentials();
if (callback != null)
{
callback(user, null);
}
}
catch (Exception e)
{
if (callback != null)
{
callback(null, e);
}
}
}))).Start();
}
public List<TwitterStatus> StatusesHomeTimeline(long? since_id, long? max_id, long? count, long? page)
{
BuildRequest(Method.GET, Url(ApiStatusesHomeTimeline));
if (since_id.HasValue)
{
Parameters.Add("since_id", since_id.Value.ToString());
}
if (max_id.HasValue)
{
Parameters.Add("max_id", max_id.Value.ToString());
}
if (count.HasValue)
{
Parameters.Add("count", count.ToString());
}
if (page.HasValue)
{
Parameters.Add("page", page.ToString());
}
var response = ExecuteRequest();
var xmlDoc = new XmlDocument();
xmlDoc.LoadXml(response);
var statuses = new List<TwitterStatus>();
var xmlstatuses = xmlDoc.DocumentElement.GetElementsByTagName("status");
foreach (XmlElement child in xmlstatuses)
{
statuses.Add(Extensions.Helper.XmlDeserialize<TwitterStatus>(child.OuterXml));
}
return statuses;
}
public delegate void StatusesHomeTimelineCallback(List<TwitterStatus> statuses, Exception e);
public void StatusesHomeTimelineAsync(long? since_id, long? max_id, long? count, long? page, StatusesHomeTimelineCallback callback)
{
(new Thread(new ThreadStart(delegate()
{
try
{
var statuses = StatusesHomeTimeline(since_id, max_id, count, page);
if (callback != null)
{
callback(statuses, null);
}
}
catch (Exception e)
{
if (callback != null)
{
callback(null, e);
}
}
}))).Start();
}
public TwitterStatus StatusesUpdate(string status, long? in_reply_to_status_id)
{
BuildRequest(Method.POST, Url(ApiStatusesUpdate));
Parameters.Add("status", status);
if (in_reply_to_status_id.HasValue)
{
Parameters.Add("in_reply_to_status_id", in_reply_to_status_id.Value.ToString());
}
var response = ExecuteRequest();
return Extensions.Helper.XmlDeserialize<TwitterStatus>(response);
}
public delegate void StatusesUpdateCallback(TwitterStatus status, Exception e);
public void StatusesUpdateAsync(string status, long? in_reply_to_status_id, StatusesUpdateCallback callback)
{
(new Thread(new ThreadStart(delegate()
{
try
{
var _status = StatusesUpdate(status, in_reply_to_status_id);
if (callback != null)
{
callback(_status, null);
}
}
catch (Exception e)
{
if (callback != null)
{
callback(null, e);
}
}
}))).Start();
}
public TwitterStatus StatusesDestroy(long id)
{
BuildRequest(Method.POST, Url(ApiStatusesDestroy));
Parameters.Add("id", id.ToString());
var response = ExecuteRequest();
return Extensions.Helper.XmlDeserialize<TwitterStatus>(response);
}
public delegate void StatusesDestroyCallback(TwitterStatus status, Exception e);
public void StatusesDestroyAsync(long id, StatusesUpdateCallback callback)
{
(new Thread(new ThreadStart(delegate()
{
try
{
var _status = StatusesDestroy(id);
if (callback != null)
{
callback(_status, null);
}
}
catch (Exception e)
{
if (callback != null)
{
callback(null, e);
}
}
}))).Start();
}
public void AccountEndSession()
{
ExecuteRequest(Method.POST, Url(ApiAccountEndSession));
}
public delegate void AccountEndSessionCallback(Exception e);
public void AccountEndSessionAsync(AccountEndSessionCallback callback)
{
(new Thread(new ThreadStart(delegate()
{
try
{
AccountEndSession();
if (callback != null)
{
callback(null);
}
}
catch (Exception e)
{
if (callback != null)
{
callback(e);
}
}
}))).Start();
}
public DirectMessage DirectMessageNew(long userId, string text)
{
BuildRequest(Method.POST, Url(ApiDirectMessageNew));
Parameters.Add("user", userId.ToString());
Parameters.Add("text", text);
var response = ExecuteRequest();
return Extensions.Helper.XmlDeserialize<DirectMessage>(response);
}
public delegate void DirectMessageNewCallback(DirectMessage message, Exception e);
public void DirectMessageNewAsync(long userId, string text, DirectMessageNewCallback callback)
{
(new Thread(new ThreadStart(delegate()
{
try
{
var message = DirectMessageNew(userId, text);
if (callback != null)
{
callback(message, null);
}
}
catch (Exception e)
{
if (callback != null)
{
callback(null, e);
}
}
}))).Start();
}
public static void ParseSource(string source, ref string href, ref string text)
{
try
{
XmlDocument xml = new XmlDocument();
xml.LoadXml(source);
href = xml["a"].Attributes["href"].Value;
text = xml["a"].InnerText;
}
catch
{
text = source;
}
}
public void RequestToken()
{
var response = ExecuteRequest(Method.POST, "https://api.twitter.com/oauth/request_token");
var keys = HttpUtility.ParseQueryString(response);
foreach (var key in keys.AllKeys)
{
if (key == "oauth_token")
{
Token = keys[key];
}
if (key == "oauth_token_secret")
{
TokenSecret = keys[key];
}
}
}
public delegate void RequestTokenCallback(string Token, string TokenSecret, Exception e);
public void RequestTokenAsync(RequestTokenCallback callback)
{
(new Thread(new ThreadStart(delegate()
{
try
{
RequestToken();
if (callback != null)
{
callback(Token, TokenSecret, null);
}
}
catch (Exception e)
{
if (callback != null)
{
callback(string.Empty, string.Empty, e);
}
}
}))).Start();
}
public void AccessToken(string PIN)
{
var response = ExecuteRequest(Method.POST, "https://api.twitter.com/oauth/access_token?oauth_verifier=" + PIN);
var keys = HttpUtility.ParseQueryString(response);
foreach (var key in keys.AllKeys)
{
if (key == "oauth_token")
{
Token = keys[key];
}
if (key == "oauth_token_secret")
{
TokenSecret = keys[key];
}
}
}
public delegate void AccessTokenCallback(string Token, string TokenSecret, Exception e);
public void AccessTokenAsync(string PIN, AccessTokenCallback callback)
{
(new Thread(new ThreadStart(delegate()
{
try
{
AccessToken(PIN);
if (callback != null)
{
callback(Token, TokenSecret, null);
}
}
catch (Exception e)
{
if (callback != null)
{
callback(string.Empty, string.Empty, e);
}
}
}))).Start();
}
#endregion
Все?
Да, все.
- Какие методы и как вызывать? Постфикс Async говорит о том, что данный метод будет вызван в отдельном потоке и требует соответствующего callback'а.
- В какой последовательности вызывать? Вначале поста привел картинку, где вы могли наблюдать схематически понятную цепь событий для авторизации пользователя и получении всех необходимых токенов.
No comments:
Post a Comment