Oltra a felipe con la sua socialbox, A chi non è mai venuto in mente di realizzare un social network ? Sembrerà strano ma lo ammetto: mai, ma mi diletta realizzare il motore che c’è dietro. Anche se ,secondo il mio punto di vista, creare un nuovo social network sarebbe stupido ed inutile. Ciò nonostante le nostre motivazioni possono sembrarci tanto forti da spingerci verso StatusNet ( il vecchio Laconica ) o BuddyPress. Ma se si vuole andare oltre alle semplici 140 lettere? Se si vuole creare Social Network completo? Cosa bisogna fare per realizzare qualche cosa di innovativo?

Tornado Web Server

Tornado Web Server

Per grazia divina, i creatori di FriendFeed han realizzato Tornado. Questo Web Server Open Source fa al caso nostro: Tornado è alla base di Friendfeed ma può essere sfruttato per realizzare qualsiasi altro genere  disito web. Ma a noi interessa creare un Social Network non un semplice blog! Con Tornado possiamo farlo!

Prima di tutto è necessario installare Tornado sulla nostra macchina. Quindi apriamo un terminale e digitiamo i seguenti comandi:

sudo apt-get install python-simplejson python-pycurl
wget http://www.tornadoweb.org/static/tornado-0.2.tar.gz
tar xvzf tornado-0.2.tar.gz
cd tornado-0.2
python setup.py build
sudo python setup.py install

A questo punto tutte le librerie python sono state installate nel sistema e sono pronte per essere utilizzate! Ora passiamo alla parte fondamentale: Cosa deve fare il nostro motore? Cosa l’utente deve essere in grado di fare con il nostro servizio? Il nostro social network come deve essere costituito? L’utente dovrà:

Queste sono le funzionalità minimali e per realizzarle dovremo prima vedere come è strutturato Tornado. Per chi avesse voglia di leggere la documentazione ufficiale in inglese è possibile farlo direttamente dal sito.

Iniziamo: Mettiamo le mani nel cuore del nostro motore creando il file server.py che dovrà essere eseguito per avviare il nostro Social Network. Per prima cosa modifichiamolo inserendo le prime righe:

#!/usr/bin/env python
# -*- coding=utf-8 -*-
import os, sys, re
from sqlite3 import dbapi2 as sqlite
import tornado.auth
import tornado.httpserver
import tornado.ioloop
import tornado.options
import tornado.web
import hashlib
import time
from tornado.options import define, option

Questa parte del codice importa le librerie necessarie per il nostro motore: oltre a tornado Tornado, importerà anche hashlib, time e sqlite in modo da poter utilizzare i database, il tempo e la libreria per calcolare gli hash di alcuni valori (come le password). Adesso impostiamo la porta dove il nostro Web Server dovrà lavorare. Aggiungiamo la seguente riga:

define("port", default=80, help="run on the given port", type=int)

A questo punto non ci resta che scrivere il codice vero e proprio del nostro motore: dovremo scrivere una applicazione tornado, che sotto forma di classe gestirà il social network. È importante far notare che questa classe associa dei gestori ( Handler ) a determinati indirizzi. Questi Handler non sono altro che classi che analizzano le richieste e rielaborano le pagine. Inseriamo nel file server.py il seguente codice:

class Application(tornado.web.Application):
    def __init__(self):
        handlers = [
            (r"/", HomePageHandler),
            (r"/auth/login", LogInHandler),
            (r"/auth/logout", LogOutHandler),
            (r"/auth/join", JoinHandler),
            (r"/write", NewPostHandler),
            (r"/([^/]+)", ReadHandler),
            ]
        settings = dict(
            base_address="http://127.0.0.1/",
            social_name="MySocialNetwork",
            template_path=os.path.join(os.path.dirname(__file__), "templates"),
            static_path=os.path.join(os.path.dirname(__file__), "static"),
            cookie_secret="itsasecret",
            login_url="/auth/login",
        )
        tornado.web.Application.__init__(self, handlers, **settings)

        self.database = sqlite.connect("database.sql")
        self.cursor =  self.database.cursor()

Questa classe imposta i vari Handler per i vari indirizzi utilizzabili, e setta anche il Database sqlite da utilizzare per salvare i dati del nostro social network. Ho preferito utilizzare Sqlite a MySql per motivi pratici. Nel dizionario delle impostazioni dell’applicazione ho definito alcuni valori importanti, come l’indirizzo da utilizzare, la cartella dei file statici e quella dei templates, il nome del social network, una chiave segreta per i cookies e l’indirozzo per l’autenticazione richiesto da Tornado.  Non ci resta che scrivere le classi degli Handler. Come  nella documentazione di Tornado, BaseHandler ci permette di risparmiare codice definendo alcune caratteristiche della classe, come il database o la funzione che analizza i cookies. Aggiungiamo al file server.py il seguente codice:

class BaseHandler(tornado.web.RequestHandler):
  @property
  def cursor(self):
    return self.application.cursor
  @property
  def database(self):
    return self.application.database
  def get_current_user(self):
    user_cookie = self.get_secure_cookie("AuthKey")
    if not user_cookie: return None
    return user_cookie

class HomePageHandler(BaseHandler):
  def get(self):
    if self.get_argument("msg", None) == "done":
      self.render("done.html", next=self.get_argument("next", "/") )
      return
    users = list()
    usersdata = self.cursor.execute("SELECT * FROM users ORDER BY time DESC LIMIT 20")
    for ( username, email, password, messages, time) in usersdata:
      users.append(username)
    users.reverse()
    messages = dict()
    messagesdata = self.cursor.execute("SELECT * FROM messages ORDER BY time DESC LIMIT 15")
    for ( url, username, msg, mtime) in messagesdata:
      messages[url] = dict()
      messages[url]["html"] = msg.decode("base64")
      messages[url]["time"] = mtime
      messages[url]["username"] = username
    messages_keys = messages.keys()
    messages_keys.sort()
    self.render("home.html", users=users, messages=messages, messages_keys=messages_keys

Analizzando il codice della classe HomeHandler notiamo la funzione get() che viene eseguita quando apriamo l’indirizzo a cui è assegnato l’handler. In questa funzione si leggono dal database gli ultimi 20 utenti iscritti, gli ultimi 15 messaggi scritti e si ottengono le informazioni dell’utente autenticato. La funzione self.render() si occupa di generare la pagina html dinamica, mediante gli input che gli vengono forniti come opzioni. A questo punto inseriamo gli Handler che gestiranno l’autenticazione, la registrazione ed il logout:

class LogInHandler(BaseHandler):
  def get(self):
    self.render("login.html")
  def post(self):
    username = self.get_argument("username")
    passwd = self.get_argument("passwd")
    OSha256 = hashlib.sha256()
    OSha256.update(username)
    OSha256.update(passwd)
    password = OSha256.hexdigest()
    existsuser = False
    usersdata = self.cursor.execute("SELECT * FROM users WHERE password = '%s'" %
    password)
    for ( username, email, password, amessages, atime) in usersdata:
      existuser = True #A quanto pare usare un semplice ciclo if non funziona
    if existsuser:
      import random
      random.seed()
      skey = random.randint(0, len(password)-1)
      OSha256 = hashlib.sha256()
      OSha256.update(username)
      OSha256.update(password)
      OSha256.update(str(skey))
      skey = OSha256.hexdigest()[:26]
      self.cursor.execute("INSERT INTO authkeys ( key, username ) VALUES ('%s', '%s')" %
       ( str(skey), username ) )
      self.set_secure_cookie("AuthKey", str(skey) + "@" + str(username))
      self.redirect(self.get_argument("next", "/"))
    else:
      self.redirect("/") #TODO: errore!

class LogOutHandler(BaseHandler):
  def get(self):
    if self.current_user:
      key = "@".join(self.current_user.split("@")[:-1])
      execthis = self.cursor.execute("""DELETE FROM authkeys WHERE key='%s'""" %
       key)
    self.clear_cookie("AuthKey")
    self.redirect(self.get_argument("next", "/"))

class JoinHandler(BaseHandler):
  def get(self):
    self.render("login.html")
  def post(self):
    now = time.ctime()
    username = self.get_argument("username")
    email = self.get_argument("email")
    passwd = self.get_argument("passwd")
    if not "@" in email and not passwd or not username:
      self.render("login.html")
      return
    OSha256 = hashlib.sha256()
    OSha256.update(username)
    OSha256.update(passwd)
    password = OSha256.hexdigest()
    self.cursor.execute("""INSERT INTO users ( username, email, password, messages, time) VALUES
     ('%s', '%s', '%s', 'hello', '%s')""" % ( username, email, password, now) )
    self.database.commit()
    self.redirect("/?msg=done&next=/auth/login") # Done!

Gli handler LogInHandler , LogOutHandler e JoinHandler lavorano prevalentemente nella tabella users e nella tabella authkeys del database che contengono i dati degli utenti e le chiavi di autenticazione. Infatti quando un utente si registra, si genera l’hash della sua password che viene inserita nel database assieme agli altri dati; mentre quando un utente fa il login si confronta l’hash della password che ha inserito con quella presente nel database: se le due chiavi saranno identiche sarà generato un cookie contenente una chiave segreta. Questa chiave viene salvata anche nella tabella authkeys e il confronto del cookie con il valore nel database permetterà di risalire al nome utente che si è autenticato ed ai suoi dati. Vediamo ora gli handler da aggiungere per creare e leggere nuovi messaggi. Scriviamo:

class NewPostHandler(BaseHandler):
  @tornado.web.authenticated
  def get(self):
    reply = self.get_argument("reply",None)
    if reply:
      messagesdata = self.cursor.execute("SELECT * FROM messages WHERE url = '%s'" % reply)
      username = reply
      for ( url, ausername, msg, mtime) in messagesdata:
        username = ausername
      self.render("write.html", reply=[reply,username] )
    self.render("write.html", reply=None)

  @tornado.web.authenticated
  def post(self):
    now = time.ctime()
    html = self.get_argument("message").encode("base64")
    #ricavo username e numero del messaggio.
    key = "@".join(self.current_user.split("@")[:-1])
    userdata = self.cursor.execute("SELECT * FROM authkeys WHERE key = '%s'" % key)
    username = None
    for (mkey, musername) in userdata:
      username = musername
    if username == None:
      self.redirect("/") # non autenticato :(
      return
    #calcolo un indirizzo più corto
    OSha256 = hashlib.sha256()
    OSha256.update(html)
    shrt = OSha256.hexdigest()[:30]
    self.cursor.execute("""INSERT INTO messages ( url, username, html, time)
    VALUES ('%s', '%s', '%s', '%s')""" % ( shrt, username, html, now ) )
    usersdata = self.cursor.execute("SELECT * FROM users ORDER BY time DESC LIMIT 20")
    messages = []
    for ( username, email, password, amessages, atime) in usersdata:
      messages = amessages.split(",")
    messages.append(shrt)
    messages = ",".join(messages)
    self.cursor.execute("""UPDATE users SET messages = '%s'
    WHERE username = '%s'""" % ( messages, username) )
    self.database.commit()
    self.redirect("/%s" % shrt )

class ReadHandler(BaseHandler):
  def get(self,shrt):
    messages = dict()
    messagesdata = self.cursor.execute("SELECT * FROM messages WHERE url = '%s'" % shrt)
    for ( url, username, msg, mtime) in messagesdata:
      messages[url] = dict()
      messages[url]["html"] = msg.decode("base64")
      messages[url]["time"] = mtime
      messages[url]["username"] = username
    messages_keys = messages.keys()
    messages_keys.sort()
    self.render("message.html", messages=messages, messages_keys=messages_keys)

Quando un utente è autenticato e visita la pagina /write, potrà scrivere un messaggio e inviarlo. Cosa accade? Il messaggio inviato viene ricevuto dal NewPostHandler il quale analizza il messaggio ricevuto e lo inserisce all’interno della tabella dei messaggi. Successivamente aggiunge il messaggio sul profilo dell’utente che lo ha scritto: così facendo dal profilo dell’utente potremo vedere i suoi messaggi. Ogni messaggio ha un suo indirizzo, che corrisponde ad una parte del suo hash: ad ogni messaggio corrisponde un solo hash e quindi anche un solo URL. Quando si visita l’indirizzo del messaggio scritto, ReadHandler riceve l’hash e lo ricerca nel database, ricava tutte le informazioni e rielabora la pagina messages.html in modo da mostrare il contenuto al visitatore.

Il codice del nostro motore va solamente ultimato. Aggiungiamo le seguenti righe di codice al file server.py:

def main():
  if not os.path.isfile("database.sql"):
    print "Generating the database:",
    database = sqlite.connect("database.sql")
    cursor =  database.cursor()
	# Scelto tutto testo per comodita'
    cursor.execute("""CREATE TABLE authkeys ( key TEXT NOT NULL, username TEXT NOT NULL)""")
    cursor.execute("""CREATE TABLE users ( username TEXT NOT NULL, email TEXT NOT NULL,
    password TEXT, messages TEXT, time TEXT)""")
    cursor.execute("""CREATE TABLE messages ( url TEXT NOT NULL, username TEXT NOT NULL,
    html TEXT NOT NULL, time TEXT)""")
    cursor.execute("""INSERT INTO messages ( url, username, html, time) VALUES
    ('%s', '%s', '%s', '%s')""" % ( "hello", "admin", "hello world!".encode("base64"), "ever" ) )
    database.commit()
    database.close()
    print "done"
    sys.exit(0)
  tornado.options.parse_command_line()
  http_server = tornado.httpserver.HTTPServer(Application())
  http_server.listen(options.port)
  tornado.ioloop.IOLoop.instance().start()

Queste righe si occuperanno di generare il database nel caso non esista, e di avviare il nostro Social Network. Al primo avvio, il programma controllerà se esiste o meno il database. Nel caso non esista lo costruirà e terminerà. A questo punto il nostro social network è quasi ultimato, non ci resta altro che scrivere le pagine html dinamiche ed aggiungere un foglio di stile. Per rendere questo post più breve e facile da comprendere prenderò in considerazione solamente il file home.html che viene rielaborato da HomePageHandler.

Quando avevamo definito le impostazioni dell’applicazione avevamo inserito le seguenti righe:

template_path=os.path.join(os.path.dirname(__file__), "templates"),
static_path=os.path.join(os.path.dirname(__file__), "static"),

Ciò significa che le pagine html dinamiche  si troveranno nella cartella templates mentre le pagine statiche nella cartella static. Quindi creiamo queste due cartelle e spostiamoci all’interno della prima per scrivere il file base.html ed home.html. Il file base.html non è altro che la pagina HTML principale. Questa pagina è vuota ed è divisa in dei blocchi. Così facendo non si deve riscrivere più volte lo stesso codice HTML e si riducono le possibilità di avere problemi. Quindi scriviamo allinterno del file base.html il seguente codice:

<html>
  <head>
    <title>{{ handler.settings["social_name"] }}</title>
    <link rel="stylesheet" href="{{ static_url("style.css") }}" type="text/css"/>
        {% block head %}{% end %}
  </head>
  <body>
	  <div id "igloo-body">
      <div id="igloo-header">
	<div style="float:right" align="right">
	  {% if current_user %}
	    <a href="/auth/logout?next={{ url_escape(request.uri) }}">
            {{ _("Log out") }}</a>
        <br><a href="/write">{{ _("New post") }}</a>
        {% else %}
	    {{ _('<a href="%(url)s">Log in</a>') %
            {"url": "/auth/login?next=" + url_escape(request.uri)} }}<br>
	  {% end %}
	</div>
	<h1><a href="/">Siderus Igloo</a></h1>
      </div>
      <div id="igloo-content"><div id="igloo-spaces"></div>
      {% block body %}{% end %}

      </div>
    </div>
    </div>
    <div id="igloo-footer">
    {% block bottom %}{% end %}
    Powered by: <a href="http://www.tornadoweb.org">Tornado</a> -
    <a href="http://www.siderus.org">Siderus</a> -
    <a href="http://www.koalalorenzo.com">koalalorenzo</a> :)
    </div>
  </body>
</html>

Come detto prima, la pagina è divisa in blocchi dinamici, ossia degli spazi vuoti che possono essere riempiti in base alle nostre necessità. Ad esempio il blocco body racchiuderà il corpo della nostra pagina. Per capire meglio ciò osserviamo e scriviamo il seguente codice nel file  home.html :

{% extends "base.html" %}

{% block body %}
  <div id="users">
  Ultimi utenti registrati:
  <ul type="disc">
  {% for user in users %}
    <li>{{ user }}</li>
  {% end %}
  </ul>
  </div>

  <div id="messages">
  {% for shrt in messages_keys %}
     <div class="message">
     {{ messages[shrt]["html"] }}
     <div class="under-message"><a href="/{{ shrt }}">
    {{ messages[shrt]["time"] }} -
    {{  messages[shrt]["username"] }}</a>
    {% if current_user %}<a class="button"
    href="/write?reply={{ shrt }}">reply</a>{% end %}</div>
     </div>
     <br>
  {% end %}
  </div>
{% end %}

Da come si vede dal primo rigo, la pagina base.html viene estesa con i blocchi dinamici della pagina home.html così da generare la HomePage. Quando abbiamo scritto HomePageHandler avevamo inserito la seguente riga:

self.render("home.html", users=users, messages=messages)

Le opzioni passate alla funzione render() le ritroviamo come valori nel file home.html ad esempio:

Da come si può intuitivamente capire i cicli vengono introdotti dai tag {% e chiusi da %} mentre tramite le doppie parentesi graffe {{ e }} vengono sostituite direttamente con un valore passato come opzione. Con un po’ di sana pratica capirete il funzionamento del generatore delle pagine HTML in Tornado. È più difficile spiegarlo che metterlo in pratica! :D Anche per questo ho deciso di tralasciare le altre pagine dinamiche (che ho comunque realizzato ed inserito nel pacchetto con i sorgenti).

A questo punto non ci resta che spostarci nella cartella static ed inserire i file statici. Possiamo inserire l’icona ( favicon ) ed il foglio di stile ( style.css ). Le pagine statiche non vengono rielaborate da Tornado, ma vengono prese in considerazione e sono raggiungibili con l’indirizzo /static/.

Una volta realizzato il vostro foglio di stile, torniamo nella cartella principale e digitiamo da terminale:

sudo python ./server.py

Così facendo il nostro motore costruirà un database, ma dovremo ri-eseguire il comando per avviare il server e poter utilizzare il nostro social network appena creato. Quindi apriamo un browser e visitiamo la pagina all’indirizzo:

http://127.0.0.1/
Congratulazioni! Avete "finito" !

Congratulazioni! Avete "finito" !

Realizzare un Social Network è complicato. C’è tanto che è stato tralasciato, come l’utilizzo delle email, la gestione degli errori, i profili degli utenti e le relazioni tra di loro… Questo articolo affronta solamente una parte del discorso legato allo sviluppo, poiché credo che ci sia molto più lavoro dietro: per non allungare troppo e renderlo noioso, ho deciso di troncare questa guida e ridurre le funzioni che avevo in mente ( like, analisi delle risposte, gestione dei profili degli utenti, email e amicizie etc… etc…) ma ho pensato di rendere disponibile il codice che ho scritto. Grazie a Tornado ed a questa “base” sarà facile implementare/migliorare le funzioni del vostro social network. Come un preparato per torte: basta aggiungere dell’acqua e metterlo nel forno a 150° :) Nel caso utilizzerete questo codice vi chiedo solamente di non modificare il footer contenente un link a questo blog. Spero vi possa tornare utile:

Scarica i sorgenti: http://www.koalalorenzo.com/source/social-igloo.tar.gz

Tutto codice ed il testo in questa pagina è rilasciato sotto licenza GPL v3
Copyright (C) 2010 Lorenzo Setale

Tornado Web Server

View Comments a “Creare un "social network" con Tornado”

  1. Pikadilly |

    Non è mia intenzione creare un social network -già fatico con un paio di forum-, tuttavia trovo questo post molto interessante per chi vuole iniziare questa carriera impegnativa, complicata, assolutamente sconsigliata dall'Associazione Medici Psichiatri italiani. :)

    A parte gli scherzi, Koalalorenzo, complimenti. ;)

  2. Pikadilly |

    Non è mia intenzione creare un social network -già fatico con un paio di forum-, tuttavia trovo questo post molto interessante per chi vuole iniziare questa carriera impegnativa, complicata, assolutamente sconsigliata dall'Associazione Medici Psichiatri italiani. :)

    A parte gli scherzi, Koalalorenzo, complimenti. ;)

  3. koalalorenzo |

    Grazie per il complimento! ;-)

  4. koalalorenzo |

    Grazie per il complimento! ;-)

  5. firstbit |

    Interessante! Effettivamente tornado rende il tutto molto semplice ed immediato, davvero non male!

    Una sola cosa domanda mi sovviene: come si comporta in quanto a prestazioni?

  6. firstbit |

    Interessante! Effettivamente tornado rende il tutto molto semplice ed immediato, davvero non male!

    Una sola cosa domanda mi sovviene: come si comporta in quanto a prestazioni?

  7. koalalorenzo |

    In quanto a prestazioni Tornado super molti altri.
    Oltre ad essere più semplice da gestire, Tornado resiste di più ed analizza velocemente le richieste: http://developers.facebook.com/news.php?blog=1&...

    È perfetto :)

  8. koalalorenzo |

    In quanto a prestazioni Tornado super molti altri.
    Oltre ad essere più semplice da gestire, Tornado resiste di più ed analizza velocemente le richieste: http://developers.facebook.com/news.php?blog=1&...

    È perfetto :)

  9. Jesson |

    good man

Lascia un Commento

blog comments powered by Disqus