Программное добавление твитта с изображением на C#

Программирование

Tagged Under : , ,

Понадобилось коллеге выполнить в рамках одной из задач добавление сообщения в twitter вместе с картинкой. Примеров не нашёл, но я после непродолжительного поиска нашёл данный исходный код.
Добавил сюда, чтобы потом не искать больше.

Простейшая реализация long polling в WCF-сервисах на примере текстовой трансляции

Программирование

Tagged Under : , , , , ,

Собственно, про long polling я узнал от своего коллеги Ильи Кубаря. Long polling весьма простое в реализации решение для осуществления реалтайм обновлений на клиентской стороне, например, в чатах или например в текстовых трансляциях футбольных матчей.
Суть метода состоит в следующем:

  • с клиента посылается на сервер AJAX-запрос;
  • на серверной части если данные обновились, то отправляется ответ, если нет – ответ не отправляется до тех пор пока нет обновлений;
  • на клиенте и на сервере выставляется таймаут.

Long polling по сравнению с polling’ом имеет значительные плюсы:

  • данные действительно обновляются моментально
  • уменьшается кол-во запросов с клиента к серверу (не страшно, что они дольше выполняются)

Итак, рассмотрим в качестве простейшего примера прототип текстовой трансляции совместив на одной странице список сообщений и их добавление (сделаем прототип страницы управления так сказать), а также дополнительно посмотрим как сделать wcf-сервис так сказать RESTful-сервисом.
Создадим WCF Service Application и переименуем дефолтный интерфейс к ILongPollingService со следующим содержанием:

[ServiceContract(SessionMode = SessionMode.NotAllowed)] // сессия нам не нужна
public interface ILongPollingService
{
    [OperationContract]
    [WebGet(ResponseFormat = WebMessageFormat.Json)] //метод доступен при обращении по http
    List<Message> PushData(string msg); //новое сообщение от ведущего трансляции

    [OperationContract]
    [WebGet(ResponseFormat = WebMessageFormat.Json)] //метод доступен при обращении по http
    List<Message> GetData(string ID); // ID - это идентификатор последнего сообщения полученного клиентом
}

Далее опишем класс реализующий интерфейс ILongPollingService:

[AspNetCompatibilityRequirements(RequirementsMode = AspNetCompatibilityRequirementsMode.Allowed)] //нужно дя поддержки REST'а
[ServiceBehavior(ConcurrencyMode = ConcurrencyMode.Single, InstanceContextMode = InstanceContextMode.PerCall)]
public class Service : ILongPollingService
{
    static List<Message> list = new List<Message>(); //здесь храним сообщения трансляции

    DateTime end; // время когда нужно выдать ответ

    public List<Message> PushData(string msg) //метод добавляющий сообщения
    {
        string id = Guid.NewGuid().ToString(); //создаём идентификатор
        list.Add(new Message { ID = id, MessageBody = msg }); // добавляем сообщение в список
        return list;
    }

    public List<Message> GetData(string ID) //метод, возвращающий текст трансляции
    {
        if (list.Count > 0)
            if (list[list.Count - 1].ID == ID) //если у клиента то же сообщение, что и последнее в списке добавленных, ждём
            {
                end = DateTime.Now.AddMinutes(2); //через две минуты отдаём то, что есть, чтобы если что, очередь запросов двигалась
                Wait(ID); // ждём
            }
        return list;
    }

    private void Wait(string ID)
    {
        while (list[list.Count - 1].ID == ID && DateTime.Now < end)
        {
            System.Threading.Thread.Sleep(1);
        };
    }
}

Принцип я думаю понятен: на каждый запрос создаётся отдельный экземпляр сервиса обрабатывающий запрос и отдающий данные. Список сообщений доступен всем экземплярам поэтому все могут отследить изменения.
Ну, и напоследок, класс сообщений выглядит так:

[DataContract]
public class Message
{
    [DataMember]
    public string ID
    {
        get;
        set;
    }

    [DataMember]
    public string MessageBody
    {
        get;
        set;
    }
}

Для поддержки REST нужно внести кое-какие настройки в кофиг, например:

<system.serviceModel>
    <services>
      <service name="LongPollingService.Service" behaviorConfiguration="LongPollingService.ServiceBehavior">
        <endpoint address="/json" binding="webHttpBinding" contract="LongPollingService.ILongPollingService" behaviorConfiguration="LongPollingServiceJsonBehavior"/>
      </service>
    </services>
    <bindings>
    </bindings>
    <behaviors>
      <endpointBehaviors>
        <behavior name="LongPollingServiceJsonBehavior">
          <webHttp/>
        </behavior>
      </endpointBehaviors>
      <serviceBehaviors>
        <behavior name="LongPollingService.ServiceBehavior">
          <serviceMetadata httpGetEnabled="true"/>
          <serviceDebug includeExceptionDetailInFaults="false"/>
        </behavior>
      </serviceBehaviors>
    </behaviors>
    <serviceHostingEnvironment multipleSiteBindingsEnabled="true" />
</system.serviceModel>

И в разметку LongPollingService.svc добавить Factory:

<%@ ServiceHost Language="C#" Debug="false" Service="LongPollingService.Service" CodeBehind="LongPollingService.svc.cs" Factory="System.ServiceModel.Activation.WebScriptServiceHostFactory" %>

Этих простых шагов достаточно для добавления поддержки REST.

Ну. а код страницы базируется на паре js-функций и очень прост:

<%@ Page Language="C#" AutoEventWireup="true" CodeBehind="Default.aspx.cs" Inherits="LongPollingService.Default" %>

<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd">

<html xmlns="http://www.w3.org/1999/xhtml">
<head runat="server">
    <title></title>
    <script src="jquery-1.6.4.min.js" type="text/javascript"></script>
    <script type="text/javascript">
        var id = '';

        $(document).ready(function () {
            getmsgs();
        });

        function getmsgs() {
            $.ajax({
                async: true,
                url: '/LongPollingService.svc/json/GetData',
                data: 'ID=' + id,
                type: 'GET',
                dataType: 'json',
                timeout: 130000, //ставим времени больше двух минут, а то будут Aborted-ответы, а нам нужны 200
                success: function (data) {
                    var str = '';
                    for (var index = 0; index < data.length; index++) {
                        str += data[index].MessageBody + '<br/>';
                        id = data[index].ID;
                    }
                    $("#msgs").html(str);
                },
                complete: getmsgs
            });
        }

        function send() {
            $.ajax({
                async: true,
                url: '/LongPollingService.svc/json/PushData',
                data: 'msg=' + $("#txt").val(),
                type: 'GET',
                dataType: 'json',
                success: function (data) {
                    var str = '';
                    for (var index = 0; index < data.length; index++) {
                        str += data[index].MessageBody + '<br/>';
                        id = data[index].ID;
                    }
                    $("#msgs").html(str);
                }
            });
        }
    </script>
</head>
<body>
    <form id="form1" runat="server">
    <div>
    <div id="msgs">
    </div>
    <input type="text" id="txt" />
    <input type="button" value="send" onclick="send(); return false;" />
    </div>
    </form>
</body>
</html>

CodeBehind абсолютно пуст, поэтому его не привожу.

Исходный код примера можно скачать.

P.S.: возможно в WCF есть встроенная поддержка long polling, но замарачиваться на этот счёт я не стал.

Создаём короткие ссылки самостоятельно

Программирование

Tagged Under : ,

Короткие ссылки вещь в последнее время необходимая, в особенности, для сервисов, которые дублируют информацию в твиттер, поэтому может так случиться, что потребуется реализовать свой собственный подобный сервис не используя уже существующие bit.ly, goo.gl и т.п..

Я для себя реализовал простейший генератор коротких ссылок, который создаёт ссылку на основе предыдущей. Генератор, собственно, возвращает ссылку без домена, т.е. после получения ссылки придётся его добавить вручную. Вот такой получился статический класс:

public static class LinkCreator
{
    static string alpha = "0123456789abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ";
    static char last = 'Z';
    static char first = '0';

    public static string GenerateUrl(string url)
    {
                if (string.IsNullOrEmpty(url))
                        return first.ToString();
        string shortUrl = "";
        int index = url.IndexOf(last);
        if (index == -1)
        {
            string prefix = url.Substring(0, url.Length - 1);
            char lastfix = Convert.ToChar(url.Substring(url.Length - 1, 1));
            shortUrl = prefix + alpha[alpha.IndexOf(lastfix) + 1];
        }
        else
        {
            if (index == 0)
            {
                if (url == createDupStr(last, url.Length))
                    shortUrl = createDupStr(first, url.Length + 1);
                else
                {
                    shortUrl = url[0] + GenerateUrl(url.Substring(1));
                }
            }
            else
            {
                string substr = url.Substring(index);
                string dupstr = createDupStr(last, url.Length - index);
                if (index == url.Length - 1 || substr == dupstr)
                    shortUrl = GenerateUrl(url.Substring(0, index)) + createDupStr(first, url.Length - index);
                else
                    shortUrl = url.Substring(0, index + 1) + GenerateUrl(url.Substring(index + 1));
            }
        }
        return shortUrl;
    }

    static string createDupStr(char c, int length)
    {
        StringBuilder r = new StringBuilder();
        for (int i = 1; i <= length; i++)
            r.Append(c);
        return r.ToString();
    }
}

Пример использования:

class Program
{
    static void Main(string[] args)
    {
        Console.WriteLine(LinkCreator.GenerateUrl("0"));
        Console.ReadLine();
    }
}

Ссылки генерятся последовательно, т.е. 0, 1, 2, …., a, b, c, …, aa, ab, ac, ….

OAuth + ASP.NET (Часть 5): авторизация через Odnoklassniki по протоколу OAuth 2.0

Программирование, Проекты

Tagged Under : , , , , , ,

Итак, на очереди авторизация через одноклассники: я не поленился и всё-таки зарегистрировал тестовое приложение. Вопреки ожиданиям всё оказалось гораздо проще.
Итак, метод контроллера, перебрасывающий пользователя на одноклассники для авторизации и для получения кода (используется разработанная ранее библиотека для авторизации через OAuth 1.0 и 2.0 AOAuthNET):

public ActionResult odnoklassniki()
{
    OAuth2 odnoklassniki = new OAuth2("id вашего приложения", "секретный ключ приложения", "http://www.odnoklassniki.ru/oauth/authorize", "http://api.odnoklassniki.ru/oauth/token.do", "http://twitter.kosfiz.net/auth/odnoklassniki/");
    odnoklassniki.GetAuthCode(new Dictionary<string, string>() { { "scope", "" } });
    return View();
}

Метод контроллера, получающий токен и после данные о пользователе:

public ActionResult odnoklassniki(string code)
{
    if (!string.IsNullOrEmpty(code))
    {
        OAuth2 odnoklassniki = new OAuth2("id вашего приложения", "секретный ключ приложения", "http://www.odnoklassniki.ru/oauth/authorize", "http://api.odnoklassniki.ru/oauth/token.do", "http://twitter.kosfiz.net/auth/odnoklassniki/");
        odnoklassniki.Code = code;
        OAuth2Token token = odnoklassniki.GetAccessToken(new Dictionary<string, string> { { "client_secret", "секретный ключ приложения" } }, OAuth2.AccessTokenType.JsonDictionary);
        if (token != null)
        {
            if (token.dictionary_token != null)
            {
                Dictionary<string, string> dict = new Dictionary<string, string>();
                dict.Add("client_id", "id приложения");
                dict.Add("application_key", "публичный ключ");
                dict.Add("method", "users.getCurrentUser");
                Response.Write(OAuth2UserData.GetOdnoklassnikiUserData(token.dictionary_token["access_token"], "секретный ключ", dict));
            }
        }
    }
    return View();
}

Для получения данных о пользователе пришлось доработать класс OAuth2UserData и добавить следующие методы:

public static string GetOdnoklassnikiUserData(string access_token, string secret_key, Dictionary<string,string> dict)
{
    string sig = GetSig(GetMD5(access_token + secret_key), dict);
    dict.Add("access_token", access_token);
    dict.Add("sig", sig);
    return GetUserData(ODNOKLASSNIKI_ME_URL, dict);
}

private static string GetMD5(string key)
{
    MD5 md5 = MD5.Create();
    byte[] hash = md5.ComputeHash(Encoding.UTF8.GetBytes(key));
    return string.Concat(hash.Select(x => x.ToString("x2")).ToList());
}

Пример авторизации через одноклассники с помощью библиотеки AOAuthNET

Исходный код библиотеки AOAuthNET

OAuth + ASP.NET (Часть 4): авторизация через Windows Live ID по протоколу OAuth 2.0

Программирование, Проекты

Tagged Under : , , , , , ,

В рамках проверки своей библиотеки и её расширения решил заняться интеграцией с Windows Live ID. Совсем недавно live не поддерживал OAuth, но время течёт и всё меняется. Теперь если вы рассчитываете привлечь на сайт аудиторию в том числе и c live-аккаунтами не затрудняя их очередной регистрацией, то возможно использовать OAuth для авторизации пользователя на своём сайте и получения данных о нём.

Ниже я приведу пример как это сделать с помощью библиотеки AOAuthNET для ASP.NET MVC сайта, для классического ASP.NET отличий будет немного.

Итак, метод контроллера, отрабатывающий по нажатию пользователя на кнопке войти

[HttpGet]
public ActionResult live()
{
    OAuth2 live = new OAuth2("id приложения", "секретный ключ", "https://oauth.live.com/authorize", "https://oauth.live.com/token", "на эту страницу будет совершён редирект с передачей кода");
    live.GetAuthCode(new Dictionary<string, string>() { { "display", "page" }, { "scope", "wl.basic" } });
    return View();
}

И теперь, собственно, код отвечающий за получения токена на странице, на которую перекинет пользователя с кодом для получения этого самого токена.

public ActionResult live(string code)
{
    if (!string.IsNullOrEmpty(code))
    {
        OAuth2 live = new OAuth2("id приложения", "секретный ключ", "https://oauth.live.com/authorize", "https://oauth.live.com/token", "на эту страницу будет совершён редирект с передачей кода");
        live.Code = code;
        OAuth2Token token = live.GetAccessToken(new Dictionary<string, string> { { "client_secret", "секретный ключ" }}, OAuth2.AccessTokenType.JsonDictionary);
        if (token != null)
        {
            if (token.dictionary_token != null)
            {
                Response.Write(OAuth2UserData.GetLiveUserData(token.dictionary_token["access_token"]));
            }
        }
    }
    return View();
}

Соответственно, я добавил метод GetLiveUserData, который получает информацию о пользователе (имя, фамилия и т.д.):

public static string GetLiveUserData(string access_token)
{
    return GetUserData(LIVE_ME_URL, new Dictionary<string, string>() { {LIVE_ACCESS_TOKEN, access_token} });
}

Всё просто, о методе GetUserData я писал ранее в первых частях цикла.

Пример работы библиотеки AOAuthNET с windows live id

Библиотека AOAuthNET и исходники

Предложения по расширению возможностей и функционалу приветствуются, так что пишите.

Постинг новостей, сообщений и т.п. в twitter

Программирование

Tagged Under : ,

Весьма популярным в нынешнее время является дублирование сообщений, новостей, статей и т.д. в твиттер. Существует масса библиотек, которые успешно справляются с этой задачей, но я решил написать свою собственную реализацию, чтобы разобраться как это вообще реализуется от начала и до конца.
Итак, нам понадобится добавить веб-приложение к твиттер-аккаунту и получить consumer key, consumer secret, access token и access token secret. Далее я написал следующий класс, у которого всего один публичный статический метод, отправляющий сообщение в твиттер:

public static class TwitterPost
{
/* задаем значения переменным полученные ранее при регистрации веб-приложения*/
    static string consumerkey = "";
    static string access_token = "";
    static string secret_token = "";
    static string consumer_secret = "";

/* энкодим строку, подсмотрено где-то и чуть изменено*/
    static string unreservedChars = "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789-_.~";

    static string UrlEncode(string value)
    {
        StringBuilder result = new StringBuilder();

        foreach (char symbol in value)
        {
            if (unreservedChars.IndexOf(symbol) != -1)
            {
                result.Append(symbol);
            }
            else
            {
                if (symbol == ' ')
                    result.Append("%20");
                else
                    result.Append(HttpUtility.UrlEncode(symbol.ToString(), Encoding.UTF8).ToUpper());
            }
        }

        return result.ToString();
    }
/*получаем подпись запроса*/
    static string getHMACSHA1(string key, string sourceStr)
    {
        UTF8Encoding encoding = new UTF8Encoding();
        string token_secret = secret_token;

        HMACSHA1 hmacsha1 = new HMACSHA1(encoding.GetBytes(HttpUtility.UrlEncode(key) + "&" + HttpUtility.UrlEncode(token_secret)));
        byte[] encoded = hmacsha1.ComputeHash(encoding.GetBytes(sourceStr));
        return Convert.ToBase64String(encoded);
    }

    static string getNonce()
    {
        Random rnd = new Random();
        int nonce = rnd.Next(1, Int32.MaxValue);
        return nonce.ToString();
    }

/* получаем дельту времени между сервером и твиттером, необходимо, иначе будет ругаться твиттер 401 ошибкой */
    static string getTimestamp()
    {
        int delta = GetTimestampDelta();
        return (((TimeSpan)(DateTime.Now - new DateTime(1970, 1, 1, 0, 0, 0))).TotalSeconds + delta).ToString();
    }

/* генерим базовую строку запроса на основе словаря параметров, метода и адреса */
    static string getBasesign(string method, string baseUrl, Dictionary<string, string> items)
    {
        StringBuilder sb = new StringBuilder();
        foreach (KeyValuePair<string, string> item in items.OrderBy(x => x.Key))
            if (sb.Length > 0)
                sb.AppendFormat("%26{0}%3D{1}", UrlEncode(item.Key), UrlEncode(item.Value));
            else sb.AppendFormat("{0}%3D{1}", UrlEncode(item.Key), UrlEncode(item.Value));
        return string.Format("{2}&{0}&{1}", UrlEncode(baseUrl), sb.ToString(), method);
    }

/* на выходе получим значение заголовка запроса Authorization, надо, конечно, все контстантные строки запрятать в строку форматирования */
    static string getAuthorization(string oauth_nonce, string oauth_signature_method, string oauth_timestamp, string oauth_signature, string oauth_version)
    {
        return string.Format("OAuth {0}="{1}", {2}="{3}", {4}="{5}", {6}="{7}",  {12}="{13}", {8}="{9}", {10}="{11}"", "oauth_nonce", oauth_nonce, "oauth_signature_method", oauth_signature_method, "oauth_timestamp", oauth_timestamp, "oauth_consumer_key", consumerkey, "oauth_token", access_token, "oauth_signature", oauth_signature, "oauth_version", oauth_version);
    }

/* получаем timestamp твиттера и вычисляем разницу с нашим и сохраняем в Application, чтобы потом не запрашивать */
    private static int GetTimestampDelta()
    {
        int delta = 0;
        if (HttpContext.Current.Application["twitterTimeDelta"] == null)
        {
            string toUrl = "http://api.twitter.com/1/help/test.json";
            string method = "GET";
            HttpWebRequest req = (HttpWebRequest)WebRequest.Create(toUrl);
            req.Method = method;
            HttpWebResponse resp = (HttpWebResponse)req.GetResponse();
            delta = int.Parse(resp.Headers["X-Transaction"].Split('-')[0]);
            int serverTimestamp = (int)((TimeSpan)(Convert.ToDateTime(resp.Headers["Date"]) - new DateTime(1970, 1, 1, 0, 0, 0))).TotalSeconds;
            delta = delta - serverTimestamp;
            HttpContext.Current.Application["twitterTimeDelta"] = delta;
        }
        else
            delta = (int)HttpContext.Current.Application["twitterTimeDelta"];
        return delta;
    }

/* отправляем твитт */
    public static void UpdateStatus(string text)
    {
        string toUrl = "http://api.twitter.com/1/statuses/update.json";
        string method = "POST";
        string timestamp = getTimestamp().Split(',')[0];
        string nonce = getNonce();
        Dictionary<string, string> items = new Dictionary<string, string>();
        items.Add("oauth_consumer_key", consumerkey);
        items.Add("oauth_nonce", nonce);
        items.Add("oauth_version", "1.0");
        items.Add("oauth_signature_method", "HMAC-SHA1");
        items.Add("oauth_timestamp", timestamp);
        items.Add("oauth_token", access_token);
        items.Add("status", UrlEncode(text));

        string basesign = getBasesign(method,
                                        toUrl,
                                        items);
        string sign = getHMACSHA1(consumer_secret,
                        basesign);

        HttpWebRequest req = (HttpWebRequest)WebRequest.Create(toUrl);
        req.Method = method;
        string auth = getAuthorization(nonce, "HMAC-SHA1", timestamp, UrlEncode(sign), "1.0");
        req.Headers.Add("Authorization", auth);
        byte[] data = Encoding.UTF8.GetBytes("status=" + UrlEncode(text));
        req.ContentLength = data.Length;
        req.ContentType = "application/x-www-form-urlencoded";
        req.ServicePoint.Expect100Continue = false;
        req.GetRequestStream().Write(data, 0, data.Length);
/*на всякий случай ловим исключение, вдруг ошибка 401*/
        try
        {
            HttpWebResponse resp = (HttpWebResponse)req.GetResponse();
        }
        catch (Exception)
        {
        }
    }
}

Используем так:

TwitterPost.UpdateStatus(text);

Впринципе оказалось несложно, проблему вызвала только необходимость синхронизации с твиттером timestamp’а и то, только потому что я не думал, что это может повлиять на работу.

Длину сообщения надо регулировать самостоятельно, поскольку твиттер не будет его обрезать, оно просто не будет проходить.

Пример работы тут, а вот результат

Проверка на NULL в LINQ2SQL

Программирование

Tagged Under : , ,

Собственно, коллега по работе столкнулся с тем, что конструкция в linq запросе вида:

where c != value

давала неверные результаты в том случае, когда value = null, а сам построенный sql-запрос был в результате вида:

c != @VALUE
......................
@VALUE = NULL

что приводило к некорректным результатам.

В итоге воспользовались этим решением после чего условие в where стало таким:

where Equals(c, value)

а запрос стал корректно строиться: в случае, если value = null подставлялось корректное условие IS NOT NULL.

Работаем с DBF-файлами

Программирование

Tagged Under : , ,

Понадобилось недавно перегнать содержимое базы, сохранённое в dbf-файлах то ли парадокса то ли ещё чего в MS SQL.
Всё бы ничего, дело решается с помощью OleDb провайдера, но интересно то, что в качестве базы понимается папка содержащая файлы dbf, а сами dbf-файлы воспринимаются как таблицы в привычном понимании.
Строка соединения выглядит так:

<connectionStrings>
    <add name="kladrdbf" connectionString="Provider=Microsoft.Jet.OLEDB.4.0;Data Source=наша папка с файлами;Extended Properties=DBASE IV;Persist Security Info=False;"
     providerName="System.Data.OleDb" />    
</connectionStrings>

Ну, а текст команды так:

OleDbCommand comm = new OleDbCommand("select * from file.dbf", conn); //где conn - наш экземпляр OleDbConnection

IPGeoBase: определяем город посетителя по IP

Программирование

Tagged Under : , , ,

Определение местонахождения посетителя сайта в последнее время считается стандартом, особенно на тех сайтах где это необходимо — по этому поводу написано уже много. Когда мне понадобилось реализовать подобную вещь у меня встал выбор между российским сервисом — IpGeoBase и широкоизвестным maximind.com.

Поработал я в итоге с обоими, но сейчас напишу о российском сервисе. IpGeoBase очень удобен в использовании, постоянно обновляется, судя по новостям на сайте и к тому же можно отправить свои замечания относительно неправильно определённого местоположения + необязательно хранить базу где-то у себя, можно воспользоваться xml-сервисом. Для сих нужд я и написал небольшую библиотечку.
Читать дальше »

ASP.NET MVC + LINQ2SQL: древовидные комментарии

Программирование

Tagged Under : , , ,

Не так давно задумал реализовать один небольшой проект и входе реализации столкнулся с необходимостью реализовать комментирование. Есстественно задумал древовидные комментарии, но даже и не думал, что всё так просто.
Читать дальше »