Ostatnio napisałem jak możemy odebrać pingback z innego serwisu/strony. Teraz pora na to byśmy my poinformowali inną stronę, że linkujemy do niej.

Samo w sobie wysyłanie nie jest skomplikowane, jednak to kiedy wysłać pingback może stanowić nie lada problem. Na przykład dodajemy post, który ma się wyświetlić 30 kwietnia, kiedy więc powinniśmy wysłać pingback?

Przy dodaniu? Nie da rady bo go jeszcze nie ma widocznego, przez co nie ma jak zweryfikować czy pingback pochodzi z naszego źródła.

Przy pierwszym wyświetleniu postu? Możemy niby zastosować async itp., jednak wciąż możemy natrafić na inne problemy.

Ogólnie w takiej sytuacji, najlepszym sposobem było by stworzenie mechanizmu który zachowywał by gdzieś informacje iż powinniśmy danego dnia odpalić pingback. Warto więc o takich problemach pamiętać kiedy się już implementuje wysyłanie pingback.

Ale do rzeczy, by wysłać pingback potrzebujemy tak naprawdę dwóch kroków:

  1. Pobrać wszystkie URL z naszego tekstu i zrobić na nich distinct – po co wywoływać dwa razy tą samą stronę jak mamy dwa linki do niej?
  2. Dla każdego urla:
    1. Pobrać stronę
    2. Sprawdzić header
    3. Wywołać ping

Proste? Tak, tak naprawdę tak :)

Pobranie wszystkich URLi

To można załatwić za pomocą pomniejszej metody i RegEx:

private static readonly Regex UrlsRegex = new Regex(
    @"<a.*?href=[""'](?<url>.*?)[""'].*?>(?<name>.*?)</a>", RegexOptions.IgnoreCase | RegexOptions.Compiled);

private IEnumerable<Uri> get_uris(string content)
{
    var urlsList = new List<Uri>();
    var matches = UrlsRegex
        .Matches(content)
        .Cast<Match>()
        .Select(myMatch => myMatch.Groups["url"].ToString().Trim())
        .Distinct();

    foreach (var url in matches)
    {
        Uri uri;
        if (Uri.TryCreate(url, UriKind.Absolute, out uri))
        {
            urlsList.Add(uri);
        }
    }

    return urlsList;
}

Wysłanie pinga

Tutaj już jest trochę ciężej, po pierwsze nasz kod powinien mieć możliwość bycia przetestowanym i to w łatwy i przyjemny sposób :) Dla tego tutaj użyłem RestSharp – małej podręcznej biblioteki, która umożliwia mi robienie co chcę z requestami.

Do tego, będziemy musieli wysłać dane w odpowiednim formacie. Można by do tego użyć XML-RPC jednak nie znalazłem tam prostego sposobu podania URL do serwisu, dlatego też będziemy potrzebować pomocniczą klasę, która RestSharp nam z serializuje do XMLa.

Cały kod naszego serwisu do wysyłania ping może wyglądać następująco:

public void Send(string content, Uri itemUri)
{
    var uris = get_uris(content);

    foreach (var uri in uris)
    {
        send_ping(itemUri, uri);
    }
}

private void send_ping(Uri itemUri, Uri targetUri)
{
    if (itemUri == null || targetUri == null)
    {
        return;
    }

    try
    {
        var request = new RestRequest(Method.GET);
        // restClient injected into constructor
        _restClient.BaseUrl = targetUri.AbsoluteUri;
        var response = _restClient.Execute(request);

        // only checking headers, we can check <link> too...
        var pingback = response.Headers.Where(x => 
            x.Name.Equals("x-pingback", StringComparison.OrdinalIgnoreCase)
            || x.Name.Equals("pingback", StringComparison.OrdinalIgnoreCase))
            .Select(x => x.Value).FirstOrDefault() as string;

        Uri pingUri;
        if (pingback.IsNotNullOrWhiteSpace() 
            && Uri.TryCreate(pingback, UriKind.Absolute, out pingUri))
        {
            request = new RestRequest(Method.POST);
            _restClient.BaseUrl = pingUri.AbsoluteUri;
            request.RequestFormat = DataFormat.Xml;

            // Im using special class as with anonymous it didnt work :/
            request.AddBody(new RpcMethodCall
            {
                methodCall = new RpcMethodCall.Method
                {
                    methodName = "pingback.ping",
                    @params = new List<RpcMethodCall.Method.Param>
                    {
                        new RpcMethodCall.Method.Param
                        {
                            param = new RpcMethodCall.Method.Param.ParamInternal
                            {
                                value = new RpcMethodCall.Method.Param.ParamInternal.Value
                                {
                                    @string = itemUri.AbsolutePath
                                }
                            }
                        },
                        new RpcMethodCall.Method.Param
                        {
                            param = new RpcMethodCall.Method.Param.ParamInternal
                            {
                                value = new RpcMethodCall.Method.Param.ParamInternal.Value()
                                {
                                    @string = targetUri.AbsolutePath
                                }
                            }
                        },
                    }
                }
            });

            // resp just for trace
            var resp = _restClient.Execute(request);
        }
    }
    catch (Exception ex)
    {
        // do nothing
    }
}

public class RpcMethodCall
{
    public Method methodCall { get; set; }

    public class Method
    {
        public string methodName { get; set; }
        public IList<Param> @params { get; set; }
        public class Param
        {
            public ParamInternal @param { get; set; }
            public class ParamInternal {

                public Value value { get; set; }
                public class Value
                {
                    public string @string { get; set; }
                }
            }
        }
    }
}

Podsumowanie

Czy to działa? Wydaje mi się, że tak :) jak to wyjdzie w życiu, zobaczymy :)

Jeżeli widzicie tutaj gdzieś rażące błędy to proszę, dajcie znać :)