def _clean_failed_garbages(sid):
	garbages = q.get_custom_list(api.FAILED_NODES)

	message = json.dumps(
		{
			'action':api._ACTION_KEYS[2],
			'data':garbages
			
		})

	_broadcast_to_nodes(
		api.EXCHANGE_FOR_ALL, 
		api.encrypt_msg(
			WebServerModel.objects(
			sid=sid).scalar(
			'common_key_public').get(),
			message))

	if WebServerModel.objects(sid=sid).update(pull_all__children=
		map(lambda x:{
				'nid':x
			}, garbages))>1:
		logger.info("cleaned failed nodes")
	else:
		logger.error("cleanning failed or nothing to clean")

	threading.Timer(api.CLEAN_GARBAGE_NODES, 
		_clean_failed_garbages, [sid]).start()		
def _make_log(header, sid, log_txt):
	WebServerModel.objects(sid=sid).update_one(
		push__server_log=
		ServerLogModel(
			by=sid, 
			header=header, 
			log=log_txt)
		)

	logger.info("====================SERVER_LOG=====================")
	for log in WebServerModel.objects(sid=sid).get().server_log:
		logger.info(log.header +" : "+ log.log)
	logger.info("===================================================")
def receive_heartbeats(nid, sid):
	## if client registered
	if WebServerModel.objects(children__nid=nid):
		if _update_last_access_time(nid, sid) == False:
			logger.error(nid+" access_time update failed")
	else:
		logger.warning(nid+" is not registered to server")
def _refresh_common_key(sid):
	s_obj = WebServerModel.objects(sid=sid)

	if s_obj.get().children:
		logger.info("common key timeout")
		new_prk = api.generate_private_key()
		message = {
					"action":api._ACTION_KEYS[3],
					"data":
					{
			    		"common_key_private":new_prk.exportKey().decode('utf-8')
			    	}
				}

		try:
			if _broadcast_to_nodes(api.EXCHANGE_FOR_ALL, 
				api.encrypt_msg(s_obj.scalar('common_key_public').get(), 
				message)) == False:
				logger.error("Message broadcast failed")
				

			#else:
			res = s_obj.update_one(
				common_key_public=api.generate_public_key(new_prk).exportKey().decode('utf-8'),
				common_key_private=message['data']['common_key_private'])
			s_obj.get().save()

			## make log of updated common key
			_make_log(api._ACTION_KEYS[3], sid, "common key updated")

		except:
			logger.error("try again")
		
	
	threading.Timer(api.COMMON_KEY_TIMEOUT, _refresh_common_key, [sid]).start()
def _get_next_worker(sid):
	s_obj = WebServerModel.objects(sid=sid)
	lowest_time = api._get_current_time()

	for child in s_obj.get().children:
		## list of nodes failed once 
		if child.nid in q.get_custom_list(api.FAILED_NODES):
			print "failed nodes"
			print q.get_custom_list(api.FAILED_NODES)
			pass

		elif not 'last_work' in child:
			s_obj.filter(
				children__nid=child.nid).update_one(
				set__children__S__last_work=lowest_time) 	# set as now if new server
			s_obj.get().save()
			next_worker_id = child.nid


		elif child.last_work < lowest_time:
			lowest_time = child.last_work
			next_worker_id = child.nid
	
	try:
		s_obj.filter(
			children__nid=next_worker_id).update_one(
			set__children__S__last_work=api._get_current_time()) 
		s_obj.get().save()

		print "next worker is... {}".format(next_worker_id)
		return next_worker_id

	except NameError:
		logger.error("no workers are available")
		return None
def _handshake_to_new_node(client, sid):
	## add client info into children document 
	_add_children(client, sid)
	s_obj = WebServerModel.objects(sid=sid)
	logger.info("client id:"+client['nid']+" has joined the system")

	message = json.dumps({
	    'action':api._ACTION_KEYS[0], 
	    'data':
	        {
	            'server_id':s_obj.scalar('sid').get(), 
	            'server_public_key':s_obj.scalar('public_key').get(),
	            'common_key_private':s_obj.scalar('common_key_private').get()
	        }
	    })

	encrypted_message = api.encrypt_msg(client['public_key'], message)

	childs = s_obj.get().children
	message = {}

	for child in childs:
	    if not message:
	        message = {
	            'action':api._ACTION_KEYS[1],
	            'data':[
	            {
	                'nid':child.nid,
	                'public_key':child.public_key
	            }]
	        }

	    else:
	        message['data'].append(
	            {
	                'nid':child.nid,
	                'public_key':child.public_key
	            })

	## TODO: set try and except
	retry = 0
	while retry < api.MAX_TRY:
		try:
			proxy = callme.Proxy(
				server_id=client['nid'], 
				amqp_host="localhost", 
				timeout=3)
			proxy.use_server(client['nid']).execute_command(encrypted_message)
			retry = api.MAX_TRY

		except:
			retry+=1
			print "passing master server info failed... {} times".format(retry)

		else:
			## broadcast of a new client to all nodes
			_broadcast_to_nodes(api.EXCHANGE_FOR_ALL, 
			    api.encrypt_msg(s_obj.scalar('common_key_public').get(),
			        json.dumps(message)))
def _create_usersession(uname, sid):
	sk = api._generate_session_key()

	mUserSession = UserSession(
			session_id=sk,
			username = uname
		)
	pk = WebServerModel.objects(sid=sid).get().public_key
	print pk
	return api.encrypt_msg(pk, sk)
def _add_children(client, sid):
	s_obj = WebServerModel.objects(sid=sid)
	children = ChildrenModel(
		nid=client['nid'], 
    	created_at=client['created_at'], 
    	public_key=client['public_key'],
    	last_access=client['created_at'])
    	#last_work=api._get_current_time())
	s_obj.update_one(push__children=children)

	## make log of a new node joined
	_make_log(api._ACTION_KEYS[1], sid, client['nid'])
def _update_last_access_time(nid, sid):
	collection = WebServerModel._get_collection()
	result = collection.update_one(
			{
				"_id":sid,
				"children._id":nid
			},
			{
				"$set":
				{
					"children.$.last_access":api._get_current_time()
				}
				
			})
	return True if result.modified_count == 1 and result.matched_count == 1 else False
def _send_add_comment_call(**data):
	if data['nid'] == None:
		return False

	retry = 0
	prm = None

	while retry < api.MAX_TRY:
		try:
			# 	do not retry pushing node id inth the queue over as it failed first time
			if retry == 0:
				prm = _coordinate_acc_to_res(
									sid=data['sid'],
		    						nid=data['nid'])

			proxy = callme.Proxy(
				server_id=data['nid'], 
				amqp_host="localhost", 
				timeout=3)

			for c in WebServerModel.objects(sid=data['sid']).get().children:
			 	if c.nid == data['nid']:
					response = proxy.use_server(c.nid).execute_command(
						api.encrypt_msg(c.public_key, {
							'action':api._ACTION_KEYS[5],
							## access to shared resource has been permitted
							'permission':prm,
							'sid':data['sid'],
							'content':
							{
								'by':data['by'],
								'session_id':data['session_id'],
								'comment':data['comment'],
								'created_at':data['created_at']
							}
						}))
					return response
			## given node is not matching with any of registered node
			retry+=1
			print "trying {}".format(retry)
		except:
			retry+=1
			print "node {} is not responding.. trying {} times...".format(data['nid'], retry)

	# finally
	return False
def _inspect_inactive_children(sid):
	#print "inspection...{}".format(sid)
	s_obj = WebServerModel.objects(sid=sid)

	children = s_obj.get().children

	if children:
		for child in children:
			#logger.info(child.last_access)
			#logger.info(api._get_current_time())

			if api._get_unix_from_datetime(
				api._get_current_time())-api._get_unix_from_datetime(
				child.last_access)>api.TIME_DETERMINE_INACTIVE:

				logger.info(child.nid+" -> user logged out")
				## pull inactive user
				s_obj.update_one(pull__children__nid=child.nid)
				s_obj.get().save()

				## notify all clients about inactive user
				message = json.dumps(
					{
			        'action':api._ACTION_KEYS[2],
			        'data':{
			            "nid":child.nid,
			            }
			        })

				## remove lock on shared resource if the inactive child is holding it
				if q.get(api.RES_HOLDER) == child.nid:
					logger.info("logging out user had locks")
					_coordinate_acc_to_res(sid, child.nid, 
						api._ACTION_KEYS[6])

				_broadcast_to_nodes(api.EXCHANGE_FOR_ALL, 
					api.encrypt_msg(s_obj.scalar('common_key_public').get(),
						message))

				_make_log(api._ACTION_KEYS[2], sid, child.nid)

				## TODO: try deamon thread
	threading.Timer(api.INSPECTION_TIME_INTERVAL, _inspect_inactive_children, [sid]).start()
def process_msg_add_request(sid, uname, ssid, comment):
    ## if master fails to find slave node, try MAX_TRY times until it
    ## response 503
    retry = 0
    while retry < api.MAX_TRY:
		try:
			node = _get_next_worker(sid)
			if node == None:
			    break
		    
			s_time = api._get_current_time().isoformat()
			response = _send_add_comment_call(**{
				'nid':node,
				'sid':sid,
				'by':uname,
				'session_id':ssid,
				'comment':comment,
				'created_at':s_time
			})

			if not response:
			    retry+=1
			    q.custom_push(api.FAILED_NODES, node)
			    logger.error("failed... trying another node...")

			## execution succeed
			else:
				return api.decrypt_msg(
					WebServerModel.objects(sid=sid).get().private_key, response)

		except:
			retry+=1
			logger.error("failed... trying another node...")

    return {
    	'errorMsg':"chatting is not available at the moment"
    }
def _get_server_log(sid):
	return WebServerModel.objects(sid=sid).get().server_log
def _find_usersession(secure_id, sid):
	pk = WebServerModel.objects(sid=sid).get().private_key
	s_id = api.decrypt_msg(pk, secure_id)

	return UserSession.objects(sid=sid).find_one({"session_id":s_id})
def _check_availability(sid):
	if WebServerModel.objects(sid=sid).get().children:
		return True
	else:
		return False
#CommentModel.drop_collection()  ## temp
#Sessions.drop_collection()  ## temp
#UserSessions.drop_collection() ## temp
#CommentReplicaModel.drop_collection() ## temp

## allow cross-origin requests
CORS(app)

## create web server
private_key = api.generate_private_key()
common_key_private = api.generate_private_key()

mWebServerModel = WebServerModel(
    public_key=api.generate_public_key(private_key).exportKey().decode('utf-8'),
    sid=str(api.__uuid_generator_4()),
    #session_key='secret',   # temp use instead  api._get_session_key(), 
    private_key=private_key.exportKey().decode('utf-8'),
    common_key_private=common_key_private.exportKey().decode('utf-8'),
    common_key_public=api.generate_public_key(common_key_private).exportKey().decode('utf-8'))
mWebServerModel.save()

logger.info("Master server has been initiated")
logger.info("Server id: "+mWebServerModel.sid)

##  this class manages users' session in the db
app.session_interface = MongoSessionInterface(
    db='chatchat', by=mWebServerModel.sid)

## log
server_api._make_log("server_init", mWebServerModel.sid, "server initiated")
## [END]
def _coordinate_acc_to_res(sid=None, nid=None, action=None):
	## done working message from a node who has access to the shared resource
	if action == api._ACTION_KEYS[6]:
		q.pop()
		q.delete(api.RES_HOLDER)
		_make_log("resource_released", sid, nid)

		## next node in the queue
		nxt = q.bottom()
		if nxt:
			logger.info("next node exists")
			logger.info(nxt)

			#	if next resource accessor is master node himself
			if nxt == sid:
				_upload_user_sessions(sid)
				_coordinate_acc_to_res(sid, sid, 
							api._ACTION_KEYS[6])

			## save as node which holding lock to the resource at present
			q.append(api.RES_HOLDER, nxt)

			for child in WebServerModel.objects(sid=sid).get().children:
				if child.nid == nxt:
					_send_direct_msg(nxt, api.encrypt_msg(child.public_key, {
							'action':api._ACTION_KEYS[8],	# 	action : access permission
							'by':sid
						}))
					break

			_make_log("resource_lock", sid, nxt)
		else:
			logger.info("queue is empty")


		return True

	elif action == None and nid == None:
		if q.length() == 0:
			q.push(sid)
			#q.push(nid) 	# for debugging
			q.append(api.RES_HOLDER, sid)
			_make_log("resource_lock", sid, sid)
			return True	

		else:
			q.push(sid)
			return False

	elif action == None:
		for child in WebServerModel.objects(sid=sid).get().children:
			## node exists
			if child.nid == nid:
				if q.length() == 0:
					q.push(nid)
					#q.push(nid) 	# for debugging
					q.append(api.RES_HOLDER, nid)
					_make_log("resource_lock", sid, nid)
					return True	

				else:
					q.push(nid)
					return False

		## not registered node
		logger.error("not registered node")
		return False