miércoles, 30 de marzo de 2011

Publicando contenido de nuestro sitio en Facebook

Resulta muy divertido publicar en Facebook pero más aún si se hace a través del código.

Recientemente recibimos una solicitud, de esas que a veces no queremos oír, de publicar parte del contenido que los usuarios subían a nuestro sitio, a la página oficial del sitio en Facebook. Aunque parecía algo disparatado al inicio, tenía su encanto, así es que aquí les comento la solución que encontramos.

Pues bien, lo primero que hicimos fue crear una página en Facebook lo cual resultó sumamente sencillo, como era de esperar. Luego creamos una aplicación en Facebook y por último vino el problema de verdad, ver cómo publicar en esa página, el contenido subido por los usuarios a nuestro sitio a través de la aplicación, es en este punto donde aparece el Graph API.

También el API resultó sencillo de usar, está basado en REST y bastante bien documentado, lo otro que necesitamos es una biblioteca que nos ayude, en nuestro caso usamos el Facebook C# SDK.

Veamos un poco la arquitectura que seguimos para este problema. Cuando un usuario publica un contenido en nuestro sitio y decide que desea también publicarlo en Facebook, no se publica de manera automática (sería un costo en tiempo que puede llegar a ser perjudicial), lo que hacemos es enviar un mensaje a una cola con los datos del contenido que deseamos publicar. Luego, existe un proceso en background que es el encargado de tomar estos datos y hacer la publicación en Facebook.

Es en este último punto donde aparece un problema, el contenido no se va a publicar en el muro del usuario sino en la página de nuestro sitio en Facebook y la idea es que parezca que la propia página es quien publica el contenido. Pueden ver el resultado final en la siguiente imagen o con más detalle en la página Chil Social Network en Facebook.
ChilSocialNetwork 
Para que parezca que es la propia página la que publica el contenido necesitamos de alguna manera obtener el access_token de esa página, lo cual está descrito en la documentación de Facebook y más concretamente en el apartado “Page Login”.

La aplicación que procesa los mensajes de la cola en background es una aplicación desktop. El procesamiento de los mensajes se ejecuta en un hilo aparte. Se usa un hilo para evitar conflictos con la interfaz de usuario, pero esto genera otros “agradables” dolores de cabeza. En el siguiente fragmento se muestra cómo se inicializa el procesador de la cola de mensajes:

   1: public void InitProcessor()
   2: {
   3:     var queueName = ConfigurationManager.AppSettings["FacebookQueue"];
   4:     messageQueue = new MessageQueue(queueName);
   5:     messageQueue.MessageReadPropertyFilter.SetAll();
   6:     messageQueue.ReceiveCompleted += new ReceiveCompletedEventHandler(messageQueueReceiveCompleted);
   7:     messageQueue.BeginReceive();
   8: }

Cada vez que se recibe un nuevo mensaje se ejecuta el método messageQueueReceiveCompleted. Si a la aplicación no se le han suministrado las credenciales necesarias para acceder a Facebook, se muestra la interfaz de autenticación de Facebook y espera a que se haga un login en Facebook. En condiciones normales esto no debe suceder porque lo primero que debemos hacer al echar a andar la aplicación es autenticar al usuario dueño de la página en Facebook (esto lo hace un administrador), pero los errores pueden ocurrir y más vale estar atentos, veamos cómo está codificado el método:


   1: private void messageQueueReceiveCompleted(object sender, ReceiveCompletedEventArgs e)
   2: {
   3:     try
   4:     {
   5:         IFacebookMessageProcessor processor = ServiceLocator.GetInstance<IFacebookMessageProcessor>(e.Message.Label);
   6:         if (processor != null)
   7:         {
   8:             var result = processor.ProcessMessage(e.Message, facebookCLientProvider.GetFacebookClient());
   9:             if (!string.IsNullOrEmpty(result))
  10:             {
  11:                 resultWriter.Write(result);
  12:             }
  13:         }
  14:     }
  15:     catch { }
  16:     messageQueue.BeginReceive();
  17: }

Este método básicamente lo que hace es buscar alguna implementación de IFacebookMessageProcessor (la cual veremos más adelante) adecuada para el mensaje recibido, mandar a procesar el mensaje, por ejemplo publicar los datos de un documento en el muro de la página y por último escribir el resultado en algún sitio a través de la interface IResultWriter.

La interface IResultWriter, a través del método Write, es la encargada de mostrar el resultado de la operación, bien puede ser en un fichero, en una base de datos, o simplemente en pantalla como es nuestro caso. Veamos la implementación:


   1: public void Write(string result)
   2: {
   3:     if (!string.IsNullOrEmpty(result))
   4:     {
   5:         uiDelegate d = text => listBox.Items.Add(text);
   6:         listBox.BeginInvoke(d, result);
   7:     }
   8: }

En este caso como es un hilo el que manda a hacer la operación, para poder mostrar los resultados en un componente de la interfaz de usuario es necesario hacer uso del método BeginInvoke de cada componente.

La interface IFacebookMessageProcessor solo posee un método como se muestra en esta figura:

IFacebookMessageProcessor

El método ProcessMessage recibe 2 parámetros uno con el mensaje y otro con una instancia de FacebookClient que necesitamos para publicar en el muro. Para publicar documentos en el muro se usa el siguiente método definido en DocProcessor.


   1: public string ProcessMessage(Message m, FacebookClient facebookClient)
   2: {
   3:     m.Formatter = new XmlMessageFormatter(new Type[] { typeof(int) });
   4:     var id = (int)m.Body;
   5:  
   6:     var doc = docService.GetById(id);
   7:     if (doc != null)
   8:     {
   9:         dynamic parameters = new ExpandoObject();
  10:         parameters.message = "Nuevo Documento en Chil";
  11:         parameters.link = "http://www.chil.es/document/" + doc.Slug + "/" + doc.Id;
  12:         if (doc.PreviewId != null)
  13:             parameters.picture = "http://www.chil.es/document-preview/" + doc.Id + "?width=88&height=125";
  14:         else
  15:             parameters.picture = "http://www.chil.es/images/default-img-docs-88.png";
  16:         parameters.name = doc.Title;
  17:         parameters.caption = doc.Authors;
  18:         parameters.description = doc.Summary;
  19:  
  20:         dynamic result = facebookClient.Post(string.Format("{0}/feed", Enviroment.PageId), parameters);
  21:         return "DocId: " + id + ", FbId: " + result.id;
  22:     }
  23:  
  24:     return null;
  25: }

Aquí solo aclarar que en Enviroment.PageId tenemos el Id de la página en la que queremos publicar contenidos.

Por último (y fue lo más costoso), solo nos falta ver cómo obtener instancias de FacebookClient. Como expliqué anteriormente el FacebookClient se debe obtener al arrancar la aplicación, pero para que no se pierdan mensajes en caso de que el administrador olvide autenticarse o porque expiró el access_token, siempre se verifica que contamos con un FacebookClient válido, en caso contrario se muestra la pantalla de autenticación de Facebook y no se procesa el próximo mensaje hasta que no se cuente con un FacebookClient válido.


   1: public FacebookClient GetFacebookClient()
   2: {
   3:     lock (lockObject)
   4:     {
   5:         if (!isOAuthResultValid) 
   6:         {
   7:             browserDelegate d = () => ShowFacebookLoginPage();
   8:             browser.BeginInvoke(d);
   9:             while (!isOAuthResultValid)
  10:             {
  11:                 Application.DoEvents();
  12:             }
  13:         }
  14:     
  15:         if (facebookClient == null)
  16:         {
  17:             FacebookClient fc = new FacebookClient(oAuthResult.AccessToken);
  18:             dynamic result = fc.Get("me/accounts");
  19:             facebookClient = new FacebookClient(result.data[0].access_token);
  20:         }
  21:         
  22:         return facebookClient;
  23:     }
  24: }

El método GetFacebookClient de la interface IFacebookClientProvider es el encargado de mantener y fabricar, en caso de que sea necesario, una instancia de FacebookClient.

Nótese como el hilo que llega a este método bloquea a los demás, con esto aseguramos solo un FacebookClient para toda la aplicación. Primero se verifica si tenemos un access_token válido, si no es así, se muestra la pantalla de autenticación de Facebook. Una vez que el administrador esté autenticado se pregunta por el access_token que necesitamos para publicar en el muro de la página, recordemos que necesitamos el access_token propio de la página y no del administrador que se autenticó. De las cuentas que me devuelve Facebook, me interesa la primera, ahí están los datos que necesito para publicar en mi página (en caso de que se tengan más páginas se deberá establecer el número de la cuenta que se quiere coger en algún tipo de configuración o algo por el estilo) y que parezca que la propia página fue la que generó el post.

La implementación del método ShowFacebookLoginPage es muy sencilla, esto se consigue a través de un componente WebBrowser y se puede definir de la siguiente forma:


   1: private void browser_Navigated(object sender, WebBrowserNavigatedEventArgs e)
   2: {
   3:     if (FacebookOAuthResult.TryParse(e.Url, out oAuthResult))
   4:     {
   5:         if (oAuthResult.IsSuccess)
   6:         {
   7:             browser.Visible = false;
   8:         }
   9:         else
  10:         {
  11:             MessageBox.Show(oAuthResult.ErrorDescription);
  12:         }
  13:     }            
  14: }
  15: private void ShowFacebookLoginPage()
  16: {
  17:     var oauth = new FacebookOAuthClient
  18:     {
  19:         ClientId = Chil.Facebook.Publisher.Enviroment.AppId
  20:     };
  21:  
  22:     var paramaters = new Dictionary<string, object>
  23:                         {
  24:                             { "display", "popup" },
  25:                             { "response_type", "token" }
  26:                         };
  27:     
  28:     var loginUri = oauth.GetLoginUrl(paramaters);
  29:     browser.Navigate(loginUri); 
  30: }

Ojalá lo explicado pueda servir de algo, no es muy complicado de implementar y da muchas satisfacciones, ahora solo queda disfrutar.

No hay comentarios:

Publicar un comentario