Oregano tiene nuevo lider

December 26th, 2011 | by | oregano, programación

Dec
26

Luego ya de muchisimos años (parece eterno) alguien con pilas apareció Marc Lorber, quien va a ser el nuevo lider y main developer de Oregano, proyecto que salio de este grupo alguna vez. Marc estaba completando el port a Gtk3 y al nuevo canvas además de tener pilas para arreglar los bugs históricos que arrastramos hace tiempo :) .

El main git fue movido a https://github.com/marc-lorber/oregano por lo que es la nueva fuente oficial.

Saludos y Feliz 2012 para todos!

No Comments »

Git branch & Subversion detection en el prompt de Bash

September 6th, 2011 | by | gnu+linux

Sep
06

Simple, agregar en el .bashrc lo siguiente :

function parse_git_branch {
  git branch --no-color 2> /dev/null | sed -e '/^[^*]/d' -e 's/* \(.*\)/(\1)/'
}
 
function detect_svn {
  test -d .svn && echo "(svn)"
}
 
function proml {
  local         RED="\[\033[0;31m\]"
  local   LIGHT_RED="\[\033[1;31m\]"
  local      YELLOW="\[\033[0;33m\]"
  local LIGHT_GREEN="\[\033[1;32m\]"
  local       WHITE="\[\033[1;37m\]"
  local  LIGHT_GRAY="\[\033[0;37m\]"
  local LIGHT_PURPLE="\[\033[1;34m\]"
  case $TERM in
    xterm*)
    TITLEBAR='\[\033]0;\u@\h:\w\007\]'
    ;;
    *)
    TITLEBAR=""
    ;;
  esac
 
PS1="${TITLEBAR}\
$LIGHT_PURPLE\w$YELLOW\$(parse_git_branch)\$(detect_svn)\
$LIGHT_GRAY\$ "
PS2='> '
PS4='+ '
}
proml

Y se ve algo como (sin colores, sorry :) :

~/src/some_git_project(master)$
~/src/some_svn_project(svn)$

No Comments »

Filtro de Contenido para T!

May 11th, 2011 | by | programación

May
11

Hoy estuve leyendo este post donde los dueños de Taringa declara que “no podemos investigar el destino final de cada uno de los 20.000 post diarios”. Lejos de ser algo “colectivamente inteligente” como declaración, es una terrible boludez :) . Con aplicar algunos filtros automáticos, permitir que cualquiera de forma rápida y fácil reporte los contenidos ilegales y actuando más o menos rápido en su eliminación uno se cubre el 90% del orto. El otro 10% hay que pelearlo por carta documento porque hay gente muy al pedo ahí afuera, pero ni en pedo llegamos a un juicio oral teniendo un poco de criterio :) .

El problema es que filtrar a mano es caro, y además filtrar hace que nuestros usuarios bajen pues no tiene ya toda la piratería al alcance de la mano ;) . Pero veamos como hacer un filtro para determinar por heurística los posts que posiblemente sean ilegales, de manera de poder revisar a mano solo los que tal ve son malos, y así bajar el trabajo necesario y por ahí bajar el costo y aumentar el profit :) .

No lo voy a explicar muy en detalle, pero básicamente el programa siguiente hace un scraping de la home de T! y agarra los ‘Ultimos posts’. Para cada uno de ellos, lee el contenido y aplica una heurística muy básica para ver si es potencialmente ilegal :

require 'rubygems'
require 'net/http'
require 'hpricot'
 
AGENT = "Mozilla/5.0 (X11; U; Linux x86_64; en-US; rv:1.9.2.10) Gecko/20100915 Ubuntu/10.04 (lucid) Firefox/3.6.10"
 
url = URI.parse("http://www.taringa.net/")
http = Net::HTTP.new("www.taringa.net")
html = http.request_get('/', {'User-Agent' => AGENT})
 
doc = Hpricot(html.body)
(doc/"html/body/div[2]/div[4]/div[4]/div/div[2]/ul/li/a").each do |link|
  text = http.request_get(link.attributes['href'], {'User-Agent' => AGENT})
  doc1 = Hpricot(text.body)
  content = (doc1/"html/body/div[2]/div[4]/div[4]/div/div[2]/div[2]").first.to_s
 
  if (content =~ /megaupload/i) || (content =~ /free/i) || (content =~ /mediafire/i) || (content =~ /fileserve/i)
    puts "#{link.inner_html.strip} posible post ilegal"
  else
    if (content =~ /Este post es privado/i)
        puts "#{link.inner_html.strip} es privado. Al menos si es ilegal no lo ve todo el mundo :)"
    else
      puts "#{link.inner_html.strip} parece legal."
    end
  end
end

Con esto podemos saber :

  • Cuantos posts parecen legales
  • Cuantos parecen ilegales
  • Cuantos no son públicos, por lo que podemos revisarlos o no, no es tan terrible

Con un poco de bash sacamos las stats :

gazer@Max:~$ ruby tfilter.rb > post.txt
54 post.txt
gazer@Max:~$ grep 'posible post ilegal' post.txt | wc -l
2

Solo el 3.7% de los posts analizados parecería ser ilegales, por lo que en lugar de tener que revisar 20.000 posts por día solo deberia revisar 740, el número es otro. Si de esos ademas sumás el report del user, seguro no te quedan mucho más y hasta por ahí lo podés moderar mientras desayunas :) .

Obviamente mi filtro se basa solamente en linksharing (que es el 90% del problema de T! y del uso que sus usuarios le dan al site). Seguramente haya servicios de upload que no puse, pero para demostrar que filtrar el contenido es una terrible pelotudez y que Taringa! no lo hace porque no le conviene, alcanza y sobra :) .

Posts analizados

6 Comments »

Procesando archivos en background con Rails

September 8th, 2010 | by | general, programación

Sep
08

Procesar archivos en paneles de administración es un bajón, sobre todo cuando son cada vez más grande y cada vez más. En uno de nuestros proyectos tenemos ya unos cinco archivos CSV diferentes a procesar donde cada uno cumple alguna función específica. Algunos de estos archivos superan los 5 Mb por upload, por lo que se hizo necesario desacoplar el procesamiento del archivo del request que realiza el upload.

Para manejar procesos en background en rails hay muchas opciones buenas, algunas más complejas con colas de trabajo, workers distribuidos y otras cosas que nosotros no necesitamos. Lo importante para nosotros era encontrar alguno que permita :

  • Procesar en background (obvio :D )
  • Poder saber en que estado está el trabajo (% completado)
  • Al menos un proceso en background a la vez (si hay más que espere su turno, los uploads son semanales o mensuales por lo que no hay tanto solapamiento entre tareas como para justificar múltiples workers)

Y nada más. Buscando y buscando caí con BackgroundFu que cumplía con lo necesario.

Procesando los archivos

Como todos los archivos son de texto con algún separador (algunos ‘;’, otros tabs o comas) empezamos por lo básico y crear una clase base que maneje algunos parámetros comunes.

En BackgroundFu uno puede encolar cualquier clase, por lo que nuestro worker es simplemente una clase con un método process (que también es arbitrario el nombre, podría ser cualquier otro).

Como el archivo que sube el usuario está en un directorio temporal, antes de mandar al proceso en background en necesario guardarlo en algún lugar seguro, para poder saber que al momento de procesarlo va a seguir estando, para ello encapsulamos este comportamiento en un método especial para encolar.

Nuestra completa termina siendo algo como (más adelante veremos en detalle algunas cosas) :

class FileWorker
  include BackgroundFu::WorkerMonitoring
 
  attr_reader :total_lines
 
  def self.enqueue(file, *args)
    filename = "#{RAILS_ROOT}/tmp/#{self.name}-#{Time.now.to_i}"
    File.open(filename, "wb") { |f| f.write(file.read) }
    Job.enqueue!(self.name, :'process', filename, *args).id
  end
 
  def process(filename, *args)
    @lines = 0
    @total_lines = `wc -l #{filename}`.to_i
    record_progress(0)
 
    result = real_process(filename, *args)
 
    record_progress(100)
    File.unlink(filename)
 
    result
  end
 
  protected
  def line_processed!
    @lines += 1
 
    update(@lines)
  end
 
  def update(lines)
    record_progress(lines, @total_lines)
  end
end

Veamos un poco más en detalle algunas partes.

Encolado

  def self.enqueue(file, *args)
    filename = "#{RAILS_ROOT}/tmp/#{self.name}-#{Time.now.to_i}"
    File.open(filename, "wb") { |f| f.write(file.read) }
    Job.enqueue!(self.name, :'process', filename, *args).id
  end

Este método es quien guarda en un lugar seguro el archivo. Utilizando el nombre de la clase actual (recuerden que este método está en una clase base, de donde luego vamos a heredar) y un timestamp por las dudas que se lleguen a hacer 2 uploads (cosa que en la práctica no pasa, pero por las dudas está).

Luego de guardar el archivo se encola el trabajo pasándole el archivo (el path completo) y cualquier otro argumento que se quiera.

Esto hará que luego el daemon de background instancia la clase ‘self.name’ y llame al método process pasandole nuestros parámetros.

Procesamiento

El método genérico de procesamiento hace uso de un método que la clase base no tiene ‘real_process’ que es el método que las clases hijas definarán para realizar el trabajo final :

  def process(filename, *args)
    @lines = 0
    @total_lines = `wc -l #{filename}`.to_i
    record_progress(0)
 
    result = real_process(filename, *args)
 
    record_progress(100)
    File.unlink(filename)
 
    result
  end

En este caso el método se encarga de saber cual es el total de lineas (de una manera tal vez no muy linda, pero que anda :P ) y borrar el archivo al terminar el proceso.

Un worker real

Para que esto tenga sentido, debemos crear un worker que haga realmente algo, entonces por ejemplo podríamos tener un worker que sume puntos a los usuarios desde un archivo :

class UserPointProcessor < FileWorker
  private
  def real_process(file)
    ActiveRecord::Base.transaction do
      FasterCSV.new(File.read(file), :col_sep => "\t").each do |row|
         # Hacer algo con row
 
         line_processed!
      end
    end
 
    "Podemos retornar algun valor o status de exito"
  end
end

De esta forma podemos ahora en algún controller hacer el encolado del trabajo :

  def add_points
    if request.post?
      redirect_to admin_job_path(UserPointProcessor.enqueue(params[:file])) and return
    end
 
    render 'file_upload'
  end

En este caso el template file_upload es un form con un file field para hacer el upload, sin ningún otro campo. La ruta admin_job_path nos la provee directamente BackgroundFu, generándonos también las vistas con AJAX para actualizar el estado, una linda barra de progreso que nos muestra el tiempo estimado para terminar entre otros datos útiles (o no tanto :D ).

No Comments »

Filtrando datos con jQuery

June 25th, 2010 | by | programación

Jun
25

Filtrar elementos dentro de una lista o una tabla para realizar una búsqueda inline con jQuery es bastante simple, solo basta entender un poco donde va cada cosa. La función en cuestión que nos permite hacer esto es filter() que junto con un poco de trabajo para hacer un deep-search dentro del DOM nos da esta funcionalidad.

Por ejemplo si queremos mostrar los elementos de una lista que coinciden con una búsqueda deberíamos hacer algo como :

  v = 'some word';
  /* Ocultamos todos los LI */
  $('#list ul li').hide();
  /* Filtramos y mostramos los que coinciden */
  $('#list ul').contents().filter(function () {
    return innerSearch(this, v);
  }).show();

La función innerSearch la pueden ver en el ejemplo, y básicamente hace un recorrido por el DOM y devolviendo true en aquellos tags donde su contenido de texto contiene el patrón buscado.

Eso nos da muchas posibilidades, ya que por ejemplo en lugar de un simple show se podría aplicar alguna clase CSS para resaltar, modificar el color o lo que se les pueda ocurrir.

Por razones de performance no es muy útil para un DOM muy complejo, pero para el caso donde yo lo necesité funciona aceptablemente bien.

Dejo un ejemplo completo con filtrado en listas y tablas.

2 Comments »