bgneal@423: """
bgneal@423: This module keeps track of who is online. We maintain records for both
bgneal@423: authenticated users ("users") and non-authenticated visitors ("visitors").
bgneal@423: """
bgneal@423: import logging
bgneal@423: 
bgneal@423: from django.conf import settings
bgneal@423: import redis
bgneal@423: 
bgneal@423: # Users and visitors each have 2 sets that we store in a Redis database:
bgneal@423: # a current set and an old set. Whenever a user or visitor is seen, the
bgneal@423: # current set is updated. At some interval, the current set is renamed
bgneal@423: # to the old set, thus destroying it. At any given time, the union of the
bgneal@423: # current and old sets is the "who's online" set.
bgneal@423: 
bgneal@423: # Redis connection and database settings
bgneal@423: 
bgneal@423: HOST = getattr(settings, 'WHOS_ONLINE_REDIS_HOST', 'localhost')
bgneal@423: PORT = getattr(settings, 'WHOS_ONLINE_REDIS_PORT', 6379)
bgneal@423: DB = getattr(settings, 'WHOS_ONLINE_REDIS_DB', 0)
bgneal@423: 
bgneal@423: # Redis key names:
bgneal@423: USER_CURRENT_KEY = "wo_user_current"
bgneal@423: USER_OLD_KEY = "wo_user_old"
bgneal@423: USER_KEYS = [USER_CURRENT_KEY, USER_OLD_KEY]
bgneal@423: 
bgneal@423: VISITOR_CURRENT_KEY = "wo_visitor_current"
bgneal@423: VISITOR_OLD_KEY = "wo_visitor_old"
bgneal@423: VISITOR_KEYS = [VISITOR_CURRENT_KEY, VISITOR_OLD_KEY]
bgneal@423: 
bgneal@423: 
bgneal@423: # Logging: we don't want a Redis malfunction to bring down the site. So we
bgneal@423: # catch all Redis exceptions, log them, and press on.
bgneal@423: logger = logging.getLogger(__name__)
bgneal@423: 
bgneal@423: 
bgneal@423: def _get_connection():
bgneal@423:     """
bgneal@423:     Create and return a Redis connection. Returns None on failure.
bgneal@423:     """
bgneal@423:     try:
bgneal@423:         conn = redis.Redis(host=HOST, port=PORT, db=DB)
bgneal@423:         return conn
bgneal@423:     except redis.RedisError, e:
bgneal@423:         logger.error(e)
bgneal@423: 
bgneal@423:     return None
bgneal@423: 
bgneal@423: 
bgneal@423: def report_user(username):
bgneal@423:     """
bgneal@423:     Call this function when a user has been seen. The username will be added to
bgneal@423:     the current set.
bgneal@423:     """
bgneal@423:     conn = _get_connection()
bgneal@423:     if conn:
bgneal@423:         try:
bgneal@423:             conn.sadd(USER_CURRENT_KEY, username)
bgneal@423:         except redis.RedisError, e:
bgneal@423:             logger.error(e)
bgneal@423: 
bgneal@423: 
bgneal@423: def report_visitor(ip):
bgneal@423:     """
bgneal@423:     Call this function when a visitor has been seen. The IP address will be
bgneal@423:     added the current set.
bgneal@423:     """
bgneal@423:     conn = _get_connection()
bgneal@423:     if conn:
bgneal@423:         try:
bgneal@423:             conn.sadd(VISITOR_CURRENT_KEY, ip)
bgneal@423:         except redis.RedisError, e:
bgneal@423:             logger.error(e)
bgneal@423: 
bgneal@423: 
bgneal@423: def get_users_online():
bgneal@423:     """
bgneal@423:     Returns a set of user names which is the union of the current and old
bgneal@423:     sets.
bgneal@423:     """
bgneal@423:     conn = _get_connection()
bgneal@423:     if conn:
bgneal@423:         try:
bgneal@423:             # Note that keys that do not exist are considered empty sets
bgneal@423:             return conn.sunion(USER_KEYS)
bgneal@423:         except redis.RedisError, e:
bgneal@423:             logger.error(e)
bgneal@423: 
bgneal@423:     return set()
bgneal@423: 
bgneal@423: 
bgneal@423: def get_visitors_online():
bgneal@423:     """
bgneal@423:     Returns a set of visitor IP addresses which is the union of the current
bgneal@423:     and old sets.
bgneal@423:     """
bgneal@423:     conn = _get_connection()
bgneal@423:     if conn:
bgneal@423:         try:
bgneal@423:             # Note that keys that do not exist are considered empty sets
bgneal@423:             return conn.sunion(VISITOR_KEYS)
bgneal@423:         except redis.RedisError, e:
bgneal@423:             logger.error(e)
bgneal@423: 
bgneal@423:     return set()
bgneal@423: 
bgneal@423: 
bgneal@423: def _tick_set(conn, current, old):
bgneal@423:     """
bgneal@423:     This function renames the set named "current" to "old".
bgneal@423:     """
bgneal@423:     # An exception may be raised if the current key doesn't exist; if that
bgneal@423:     # happens we have to delete the old set because no one is online.
bgneal@423:     try:
bgneal@423:         conn.rename(current, old)
bgneal@423:     except redis.ResponseError:
bgneal@423:         try:
bgneal@423:             del conn[old]
bgneal@423:         except redis.RedisError, e:
bgneal@423:             logger.error(e)
bgneal@423:     except redis.RedisError, e:
bgneal@423:         logger.error(e)
bgneal@423: 
bgneal@423: 
bgneal@423: def tick():
bgneal@423:     """
bgneal@423:     Call this function to "age out" the old sets by renaming the current sets
bgneal@423:     to the old.
bgneal@423:     """
bgneal@423:     conn = _get_connection()
bgneal@423:     if conn:
bgneal@423:         _tick_set(conn, USER_CURRENT_KEY, USER_OLD_KEY)
bgneal@423:         _tick_set(conn, VISITOR_CURRENT_KEY, VISITOR_OLD_KEY)