PROJET AUTOBLOG


Sam & Max: Python, Django, Git et du cul

Site original : Sam & Max: Python, Django, Git et du cul

⇐ retour index

Multiboards est désormais customisable 22

dimanche 26 juillet 2015 à 21:23

J’avais pensé à un titre du genre: “Vous aussi, créez votre propre Multiboards et devenez riche” mais ça risquait de faire légèrement pompeux. On garde donc un titre simple.

Pour ceux qui ne connaissaient pas, Multiboards est une simple page web qui affiche les flux rss de certains sites sélectionnés par nos soins. Après quelques années de service on s’est aperçu que pas mal de monde l’utilisait, on a même eu des suggestions d’améliorations (merci Nain Genieu) et des demandes de libération du code source.
Bon le code c’est pas pour tout de suite car il est super crade et que Sam acceptera jamais de laisser ça sans passer un bon coup de balais factoriseur XR-2200. Mais en attendant vous pouvez créer vos pages d’agrégation de flux persos et les partager avec le monde entier ou votre voisine de palier (celle qui a d’énormes nibards).

Pour créer son board on va sur http://multiboards.net/build et on ajoute ses flux RSS, on peut déplacer les flux dans la grille à sa convenance s’ils n’ont pas été rentrés dans le bon ordre. Lorsque l’on est satisfait avec sa page, on clique sur le bouton vert et on peut voir le mimi, le rara le miracle apparaître sous nos yeux ébahis !

mb_custo

Pour le moment vu que ça a été fait à l’arrache dans les chiottes de l’aéroport de Manille y a pas bcp de fonctionnalités marrantes mais lorsque le code sera libéré on espère avoir de jolies contribs ^^.

Quelques idées pour le futur:

NB:
Le player a été changé aussi, plus de flash et place au HTML5, un petit player maison hyper simple mais qui fait son job.

On attend vos retours, ça doit foisonner de bugs

Utilisateurs en ligne avec le sorted set de Redis 7

samedi 25 juillet 2015 à 10:48

Beaucoup de gens sous exploitent Redis en se cantonnant à un usage clé/valeur, voire pour les plus aguérris à la liste ou le hash.

Mais derrière sa simplicité d’usage, l’outil possède une grande richesse de fonctionnalités comme le PUB/SUB, l’hyperloglog et les sorted sets.

C’est de ces derniers dont on va parler.

En effet, dernièrement j’ai dû mettre en place un petit compteur des visiteurs en ligne, et c’est typiquement quelque chose qui fusille une base de données car ça fait une écriture ou une lecture à chaque changement de vue.

Redis est idéal pour ça, le truc tenant 100 000 écritures / seconde, et perdre 4 secondes d’historique du compteur en cas de crash est le cadet de mes soucis.

Pour compter les visiteurs, il nous faut non seulement un moyen d’incrémenter le compteur, mais aussi de le décrémenter quand le visiteur n’est plus en ligne. J’utiliserais WAMP, Crossbar pourrait me donner l’information à tout moment via l’API meta, mais sur un site HTTP only il n’y a aucun événement qui nous signale “l’utilisateur n’est plus connecté”.

Le seule moyen de savoir qu’un utilisateur n’est plus connecté est de garder une trace de lui et de la faire expirer quand on n’a pas de nouvelle depuis un temps supérieur à une durée raisonnable (calculée avec précision ) 10 minutes par l’INDMS).

Problème, on peut bien faire expirer des clés avec Redis, mais on ne peut pas lister ces clés (la fonction keys() le fait mais avec des perfs désastreuses et l’auteur du logiciel la conseille uniquement pour regarder l’état de son serveur depuis la console). D’un autre côté on a des listes, mais l’expiration concerne toute la liste, pas juste un élément.

Une manière de contourner le problème est d’utiliser un sorted set, une structure de données Redis qui a les caractéristiques suivantes :

En gros, c’est comme un dictionnaire, mais ordonné, qui ressemble à :

{
    'cle': 789789
    'cle2': 78999
    'cle3': 6
}

Comme toutes les structures de données un peu exotiques, on se demande à quoi ça sert d’avoir des caratéristiques aussi précises et ce qu’on peut bien en faire.

La réponse est comme d’hab, “plein de choses”, mais nous on va en faire un compteur :

import datetime
 
import redis
 
# connection à redis
con = redis.StrictRedis()
 
# On appelle cette vue avec ajax, ignorant les
# utilisateurs sans JS qui de toute façon sont des 
# anarco communistes qui ne rapportent
# pas d'argent et fument des joins
# Cette partie dépend de votre framework
@ajax('/counter')
def add_user_count(request):
 
    # la clé d'accès à notre sorted set
    counter_key = "users:online"
 
    # un id unique pour notre visiteur. Ca dépend du
    # framework web
    user_id = request.session.id
 
    # Calcul du timestamp de maintenant et d'il y a 10 minutes
    # car ils vont nous servir de score max et min.
    now = (datetime.utcnow() - datetime(1970, 1, 1)).total_seconds()
    ten_minutes_ago = now - (60 * 10)
 
    # On ajoute l'utilisateur dans le sorted set. S'il existait déjà, son
    # ancienne valeur est remplacée, sinon elle est créée. Comme c'est
    # forcément le plus récent timestamp, il est tout en haut du sorted set
    con.zadd(counter_key, now, user_id)
 
    # Le sorted set ressemble donc à ça :
 
    # {
 
    #     "userid": 809809890, # timestamp de dernière requete
    #     "userid2": 809809885, # timestamp plus ancien
    #     "userid3": 809809880 # timestamp encore plus ancien
    #     ...
    # }
 
    # On retire toutes les entrées du sorted set avec un score
    # qui est plus petit que le timestamp d'il y a 10 minutes
    # histoire de pas saturer la mémoire.
    con.zremrangebyscore(online_user_key, 0, ten_minutes_ago)
 
    # Et on récupére le nombre de visiteurs en ligne entre maintenant
    # et 10 minutes.
    visitors = con.zcount(online_user_key, ten_minutes_ago, now)
 
    return visitors

Notez que toutes ces étapes sont très rapides, que ce soit l’insertion, la suppression ou le compte du total grâce aux fantastiques perfs de Redis. J’aime Redis. Je suce la bite de Redis goulument.

La précision n’est pas parfaite, mais compter les utilisateurs est plus un art (divinatoire) qu’une science et avoir un chiffre précis à 2, 3% prêt est suffisant pour nous.

Des fois ce que je trouve sur 0bin fait flipper 24

vendredi 24 juillet 2015 à 09:42

On reçoit maintenant régulièrement des demandes de take down sur 0bin.net, essentiellement pour des trucs de copyright.

Et de temps en temps, on a des trucs vachement plus velus, comme par exemple des clés d’entrée sur des portails pédophiles.

Mais depuis quelques temps, on nous signale des nouveautés : des gens qui ont leur profile complet (genre complet, complet) et qui nous demande de le virer.

Voici un exemple anonymisé d’un truc que j’ai viré cette semaine :

Picture: https://i.imgur.com/sa_gueule.jpg
------------------------------
Understalker:
- Name: prenom nom
- DOB: 01/01/19XX
- Phone #: xxx-xxx-xxxx - Landline, Google Voice
- Phone #: xxx-xxx-xxxx - Landline, Time Warner Cable
- Address: xxx Avenue, New York, NY 10027
- Passwords: passwd1, passwd2, passwd3
------------------------------
Emails:
- prenomgx3@aim.com, Reset: pseudo@yahoo.com
- hnom@yahoo.com
- prenomtruc@mac.com
------------------------------
Skypes:
- skype:prenomnom // Current
- skype:pseudo.2002
- skype:prenom.nom
- skype:pseudo.nom
- skype:prenomsg
- skype:pseudo
------------------------------
Accounts:
- https://prenomstuff.tumblr.com
- https://www.hackforums.net/member.php?action=profile&uid=
- https://www.facebook.com/prenom.nom.9
- https://www.epicnpc.com/members/pseudo
- https://www.whois.com/whois/nomdedomaine.tld
- https://www.whois.com/whois/nomdedomaine.tld
- https://www.whois.com/whois/nomdedomaine.tld
- https://www.whois.com/whois/nomdedomaine.tld
- https://www.whois.com/whois/nomdedomaine.tld
------------------------------
Time Warner Cable Account
- IP: xx.xxx.xxx.xx
- Name: prenom nom
- Address: xxx Avenue, New York, NY 10027
- Phone Number: xxx-xxx-xxxx
- Account Number:
- PIN:
- Modem MAC Address: XX:XX:XX:XX:XX:XX
- SSID: yxyxyxyxyxyx
------------------------------
Relatives:
- Mother: Prenom nom
- Phone #: xxx-xxx-xxxx - Landline, Time Warner Cable
- Address: xxx Avenue, New York, NY 10027
- Previous Phone #: xxx-xxx-xxxx - Landline, Verizon
- Previous Address: Une autre adresse complete
- Previous Address: Une autre adresse complete
- Skype: pseudo
- Skype: pseudo.nom
- Skype: pseudo.nom1
- Facebook: https://www.facebook.com/pseudo.nom.3
- Occupation: COSMETOLOGIST
- License #: Numero de putain licence !, Expir
- Used to own 'nom de son putain de salon de coifure !'

- Father: prenom nom
- Phone #: xxx-xxx-xxxx - Cell, Verizon
- Phone #: xxx-xxx-xxxx - Landline, Time Warner Cable
- Address: xxx Avenue, New York, NY 10027
- Email: pseudo@yahoo.com
- Facebook: https://www.facebook.com/prenom.nom
- Company: Ouai le nom de la société de son père
- Company #: et son siret

- Brother: Prenom nom

- Unknown: Prenom nom
- Phone #: xxx-xxx-xxxx - Landline, Time Warner Cable
- Address: Autre adresse à NY
- Facebook: https://www.facebook.com/prenom.nom

- Unknown: Prenom nom
- Facebook: https://www.facebook.com/prenom.nom

J’ai tout sur ce gars, tout. Où il habite, sa famille, le taff, les contacts, de quoi lire ses mails et resetter tous ses passwords sur tous ses sites. Notez aussi qu’il y a un historique des comptes, et un croisement des identités en ligne. Truc de ouf.

J’ai tout viré, bien sur, mais je suppose qu’il y a en plein qu’on ne me signale pas, et qui sont là, sur le serveur.

Le bon coup de pute du jeudi 7

jeudi 23 juillet 2015 à 09:06

Le bureau est calme. Trop calme. Les projets avancent, vos clients sont heureux et – pire ! – vos collègues sourient.

Il est temps de changer tout ça.

Choisissez une victime qui est partie boire un café sans locker sa session (quitte à faire un exemple, autant choisir un être faible), et créez un petit fichier dans le chemin “~/au/fin/fond/de/ses/reps/de/travail/evil.py”.

Ensuite mettez ça dedans :

import random
import functools
 
# on garde une référence à l'ancienne fonction
# pour éviter une récursion infinie
old_range = range
 
# on lui créé un remplaçant avec les mêmes métatadas
@functools.wraps(range)
def random_range(start, stop=None, step=1):
 
    # on mimic la signature un peu originale de range()
    if not stop:
        stop = start
        start = 0
 
    # et on fait un range avec un tout petit peu de piment
    return old_range(start, stop + random.randint(-1, 1), step)
 
# on range (lol) tout comme si de rien était
__builtins__.range = random_range

Et il n’y a plus qu’à faire pointer la variable d’environnement PYTHONSTARTUP dessus. Par exemple sous Unix, dans son .bashrc:

export PYTHONSTARTUP=~/au/fin/fon/de/ses/reps/de/travail/evil.py

Et maintenant, à chaque fois qu’il ou elle lancera son shell:

>>> range
    <function builtins.range>
 
>>> list(range(10))
    [0, 1, 2, 3, 4, 5, 6, 7, 8, 9]
 
>>> list(range(10))
    [0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10]
 
>>> list(range(10))
    [0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10]
 
>>> list(range(10))
    [0, 1, 2, 3, 4, 5, 6, 7, 8]

*Zoom final sur rire diabolique*

Aux couleurs du site 11

mercredi 22 juillet 2015 à 12:41

On est en train de tweaker un peu multiboards.net Max et moi, et il a voulu ajouter un feature rigolote : les flux sont affichés avec une palette de couleurs proche de celle du site.

Comme récupérer les couleurs d’un site est compliqué, on s’est rabattus sur les couleurs du favicon, qui en théorie doit résumer l’identité d’un site. Ce n’est pas parfait, mais c’est plus simple à implémenter.

Au début, on avait écrit une solution avec Pillow et numpy. Ça marchait très bien, mais des extensions compilées juste pour faire de la déco c’était un peu lourd, surtout si un jour on libère le code source (mais vu comme les sources sont dégueues et qu’on a pas de doc, pour le moment c’est pas gagné).

On a donc changé notre fusil d’épaule, et on a choisit une nouvelle technique : faire la moitié du taff côté client.

Récupérer le favicon côté serveur

On ne peut pas faire des requêtes cross domain en JS, donc on doit toujours récupérer l’image côté serveur. get_favicon_url() parse le site à coup de regex (c’est mal mais ça évite de rajouter BeautifulSoup en dépendance pour une balise) pour récupérer son URL sur la page donnée, ou à défaut, sur la page à la racine du site. On se donne plusieurs essais en cas d’erreurs de réseau :

 
import urllib
import urlparse 
 
def get_favicon_url(url, retry=3, try_home_page=True):
    """ Try to find a favicon url on the given page """
 
    parsed_url = urlparse.urlparse(url)
    url_root = "%s://%s" % (parsed_url.scheme, parsed_url.netloc)
 
    try:
        # try to get it using a regex on the current URL
        html = fetch_url(url, retry=retry)
        html = html.decode('ascii', errors='ignore')
        pattern = r"""
                               href=(?:"|')\s*
                               (?P<favicon>[^\s'"]*favicon.[a-zA-Z]{3,4})
                               \s*(?:"|')
                          """
 
        match = re.search(pattern, html, re.U|re.VERBOSE)
        favicon_url = match.groups()[0]
 
    except IOError:
        # this is a network error so eventually, let it crash
        if not try_home_page:
            raise
        # try with the home page, maybe this one is accessible
        favicon_url = get_favicon_url(url_root, retry=retry,
                                                       try_home_page=False)
    except (IndexError, AttributeError):
        # url is not on this page, try the home page
        if try_home_page:
            return get_favicon_url(url_root, retry=retry, try_home_page=False)
 
        # can't find the favicon url, default to standard url
        favicon_url = '/favicon.ico'
 
    # make sure to have the domain of the original website in the favicon url
    if url_root not in favicon_url:
        favicon_url = "%s/%s" % (url_root, favicon_url.lstrip('/'))
 
    return favicon_url
 
 
def fetch_favicon(url, retry=3):
    """ Returns the bytes of the favicon of this site """
    favicon_url = get_favicon_url(url, retry=retry)
    return fetch_url(favicon_url, retry=retry)

fetch_favicon() télécharge l’image proprement dite et retourne les bits tels quels. Notez que tout ça est synchrone et bloquant, mais heureusement le traffic de multiboards est sans doute trop faible pour poser problème.

Exposer le résultat au client

Ensuite il faut faire le lien entre le client et le serveur. On fait donc un petit routing (c’est du bottle) qui va retourner tout ça en base64 afin qu’on puisse l’utiliser directement dans un objet Image JS :

 
import base64
from bottle import post, HTTPError
 
@post('/favicon')
def fetch_favicon_base64():
    """ Return the favicon URL from a website """
    try:
        url = request.POST['url']
    except KeyError:
        raise HTTPError(400, "You must pass a site URL")
    try:
        # return favicon as base64 url to be easily included in
        # a img tag
        favicon = fetch_favicon(url)
        return b'data:image/x-icon;base64,' + base64.b64encode(favicon)
    except (IOError):
        raise HTTPError(400, "Unable to find any favicon URL")

Récupérer la palette côté client

Avec un peu d’AJAX sur notre vue, on récupère les bits de l’image, et on fout le tout dans un objet Image qu’on passe à la lib ColorThief. Cette dernière nous renvoie un array avec la palette. On convertit tout ça en code hexadécimal, et on diminue la luminosité de certaines couleurs pour rendre le texte plus lisible.

 $.post('/favicon', {url: url}).done(function(data){
 
        // load an image with the base64 data of the favicon
        var image = new Image;
        image.src = data;
        image.onload = function() {
 
            // ask ColorThief for the main favicon colors
            var colorThief = new ColorThief();
            var bc = colorThief.getPalette(image);
 
            // some tools to convert the RGB array into
            // rgb hex string
            function componentToHex(c) {
                var hex = c.toString(16);
                return hex.length == 1 ? "0" + hex : hex;
            }
 
            function rgbToHex(array) {
                return componentToHex(array[0]) + componentToHex(array[1]) + componentToHex(array[2]);
            }
 
            // make the color brigther
            function increase_brightness(hex, percent){
 
                // convert 3 char codes --> 6, e.g. `E0F` --> `EE00FF`
                if(hex.length == 3){
                    hex = hex.replace(/(.)/g, '$1$1');
                }
 
                var r = parseInt(hex.substr(0, 2), 16),
                    g = parseInt(hex.substr(2, 2), 16),
                    b = parseInt(hex.substr(4, 2), 16);
 
                return '' +
                   ((0|(1<<8) + r + (256 - r) * percent / 100).toString(16)).substr(1) +
                   ((0|(1<<8) + g + (256 - g) * percent / 100).toString(16)).substr(1) +
                   ((0|(1<<8) + b + (256 - b) * percent / 100).toString(16)).substr(1);
            }
 
            /* Define board colors */
            var header =  '#' + rgbToHex(bc[0]);
            var odd = '#' + increase_brightness(rgbToHex(bc[1]), 90);
            var even = '#' + increase_brightness(rgbToHex(bc[2]), 90);
 
});

Est-ce que la feature est utile ? Je ne sais pas, c’est un peu gadget, mais c’était marrant à faire, et c’est vrai qu’on identifie bien chaque site sur la page du coup.

L’idée ici est que le serveur et le client travaillent en équipe, et que grâce au navigateur on a une stack de traitement image/audio/video à portée de code. D’ailleurs on a viré le plugin flash des radios pour le remplacer par du chteumeuleu 5.

On peut imaginer la même chose avec tous les outils côtés en JS, ceci incluant les préprocesseurs et minifieurs JS ou CSS : pas besoin d’installer Node pour faire tourner tout ça, il suffit de déléguer le boulot à un tab du browser. Pour améliorer la stack, on pourrait par ailleurs utiliser du RPC avec WAMP et même prévenir le navigateur quand un fichier est prêt avec PUB/SUB afin qu’il le recharge. Vous avez vu comme j’arrive toujours à placer crossbar ?

J’ai un pote qui va venir dans quelques jours pour creuser cette idée qui permettrait d’avoir en pur Python une sorte de livereload sans extension, sans gros GUI, qui transpile tout et rafraichit tout avec un simple fichier de config. Peut être aurons nous le temps de faire un proto.

C’était l’inspiration du jour, passez une excellente canicule !