Implement a server event queue using beanstalkd
This commit is contained in:
60
api/events.py
Normal file
60
api/events.py
Normal file
@@ -0,0 +1,60 @@
|
||||
from django.conf import settings
|
||||
from minecraft.models import Server
|
||||
from json import dumps, JSONEncoder
|
||||
import beanstalkc
|
||||
from django.contrib.auth.models import User
|
||||
|
||||
class EventEncoder(JSONEncoder):
|
||||
def default(self, obj):
|
||||
if isinstance(obj, Event):
|
||||
return {'type': obj.type, 'payload': obj.data}
|
||||
return super(EventEncoder, self).default(obj)
|
||||
|
||||
|
||||
class Event(object):
|
||||
def __init__(self, type, data):
|
||||
self.type = type
|
||||
self.data = data
|
||||
|
||||
class BroadcastEvent(Event):
|
||||
def __init__(self, message):
|
||||
super(BroadcastEvent, self).__init__(type='broadcast', data={'message':
|
||||
message})
|
||||
|
||||
class PlayerMessageEvent(Event):
|
||||
def __init__(self, user, message):
|
||||
super(PlayerMessageEvent, self).__init__(type='player-message',
|
||||
data={'message': message, 'player': user})
|
||||
|
||||
def server_queue(server, users=[]):
|
||||
queueName = 'caminus-broadcast-%s'%server.id
|
||||
queue = beanstalkc.Connection(host=settings.CAMINUS_BEANSTALKD_HOST,
|
||||
port=settings.CAMINUS_BEANSTALKD_PORT)
|
||||
queue.use(queueName)
|
||||
queue.watch(queueName)
|
||||
if len(users) > 0:
|
||||
for user in users:
|
||||
queue.watch("caminus-user-%s"%user)
|
||||
return queue
|
||||
|
||||
def send_server_event(server, event):
|
||||
if settings.CAMINUS_USE_BEANSTALKD:
|
||||
queue = server_queue(server)
|
||||
json = dumps(event, cls=EventEncoder)
|
||||
queue.put(json)
|
||||
|
||||
def server_broadcast(message, *args):
|
||||
message = message%args
|
||||
for server in Server.objects.all():
|
||||
event = BroadcastEvent(message)
|
||||
send_server_event(server, event)
|
||||
|
||||
def user_message(user, message, *args):
|
||||
player = user.minecraftprofile.mc_username
|
||||
player_message(player, message, *args)
|
||||
|
||||
def player_message(playername, message, *args):
|
||||
message = message%args
|
||||
for server in Server.objects.all():
|
||||
event = PlayerMessageEvent(playername, message)
|
||||
send_server_event(server, event)
|
@@ -9,6 +9,7 @@ from urllib2 import urlopen
|
||||
import json
|
||||
from datetime import datetime
|
||||
from models import cachePlayerList
|
||||
from servers import server_queue, user_queue
|
||||
|
||||
class MOTDHandler(AnonymousBaseHandler):
|
||||
allowed_methods = ('GET',)
|
||||
@@ -76,6 +77,23 @@ class ServerPingHandler(BaseHandler):
|
||||
def read(self, request):
|
||||
return {'identity': request.server}
|
||||
|
||||
class ServerEventHandler(BaseHandler):
|
||||
allowed_methods = ('GET', 'POST')
|
||||
|
||||
def read(self, request):
|
||||
queue = server_queue(request.server)
|
||||
queue.watch('caminus-broadcast-%s'%request.server.id)
|
||||
events = []
|
||||
job = queue.reserve(timeout=30)
|
||||
if job:
|
||||
events.append({'id': job.jid, 'event': json.loads(job.body)})
|
||||
return {'events': events}
|
||||
|
||||
def create(self, request):
|
||||
queue = server_queue(request.server)
|
||||
queue.delete(int(request.POST['job']))
|
||||
return {'result': 'success'}
|
||||
|
||||
class PollHandler(BaseHandler):
|
||||
allowed_methods = ('GET',)
|
||||
|
||||
|
0
api/management/__init__.py
Normal file
0
api/management/__init__.py
Normal file
0
api/management/commands/__init__.py
Normal file
0
api/management/commands/__init__.py
Normal file
36
api/tests.py
36
api/tests.py
@@ -5,6 +5,8 @@ from django.contrib.auth.models import User
|
||||
from minecraft.models import MinecraftProfile, Server, PlayerSession, MOTD, Ban
|
||||
from local.models import Quote
|
||||
import hashlib
|
||||
import events
|
||||
from django.conf import settings
|
||||
|
||||
class ServerPingTest(unittest.TestCase):
|
||||
def setUp(self):
|
||||
@@ -25,6 +27,40 @@ class ServerPingTest(unittest.TestCase):
|
||||
resp = self.client.get('/api/server/whoami', HTTP_AUTHORIZATION='X-Caminus %s'%(self.token))
|
||||
self.assertEqual(resp.status_code, 200)
|
||||
|
||||
if settings.CAMINUS_USE_BEANSTALKD:
|
||||
class ServerEventTest(unittest.TestCase):
|
||||
def setUp(self):
|
||||
self.client = Client()
|
||||
self.user = User.objects.create_user('ValidUsername', 'test@example.com')
|
||||
self.user.minecraftprofile.mc_username = "ValidUsername"
|
||||
self.user.minecraftprofile.save()
|
||||
self.server = Server.objects.create(hostname='localhost', secret='secret')
|
||||
tokenHash = hashlib.sha1()
|
||||
tokenHash.update("%s%s%s"%('localhost', 0, 'secret'))
|
||||
self.token = "%s$%s$%s"%('localhost', 0, tokenHash.hexdigest())
|
||||
|
||||
def tearDown(self):
|
||||
self.user.delete()
|
||||
self.server.delete()
|
||||
|
||||
def testBroadcast(self):
|
||||
events.server_broadcast("Test message")
|
||||
response = json.loads(self.client.get('/api/server/events',
|
||||
HTTP_AUTHORIZATION='X-Caminus %s'%(self.token)).content)
|
||||
self.assertTrue(len(response['events']) > 0)
|
||||
response = json.loads(self.client.post('/api/server/events', {'job':response['events'][0]['id']},
|
||||
HTTP_AUTHORIZATION='X-Caminus %s'%(self.token)).content)
|
||||
self.assertEqual(response['result'], 'success')
|
||||
|
||||
def testUserMessage(self):
|
||||
events.user_message(self.user, "Test user message")
|
||||
response = json.loads(self.client.get('/api/server/events',
|
||||
HTTP_AUTHORIZATION='X-Caminus %s'%(self.token)).content)
|
||||
self.assertTrue(len(response['events']) > 0)
|
||||
response = json.loads(self.client.post('/api/server/events', {'job':response['events'][0]['id']},
|
||||
HTTP_AUTHORIZATION='X-Caminus %s'%(self.token)).content)
|
||||
self.assertEqual(response['result'], 'success')
|
||||
|
||||
class MOTDTest(unittest.TestCase):
|
||||
def setUp(self):
|
||||
self.client = Client()
|
||||
|
@@ -40,6 +40,7 @@ class ServerResource(Resource):
|
||||
urlpatterns = patterns('api',
|
||||
url(r'^motd/(?P<username>.*)$', motdHandler),
|
||||
url(r'^server/whoami$', ServerResource(handlers.ServerPingHandler)),
|
||||
url(r'^server/events$', ServerResource(handlers.ServerEventHandler)),
|
||||
url(r'^server/economy/(?P<playername>.*)$', ServerResource(handlers.EconomyHandler)),
|
||||
url(r'^server/session/(?P<playername>.*)/new$', ServerResource(handlers.NewPlayerSessionHandler)),
|
||||
url(r'^server/session/(?P<playername>.*)/close$', ServerResource(handlers.ClosePlayerSessionHandler)),
|
||||
|
@@ -1,6 +1,7 @@
|
||||
from django.db import models
|
||||
from django.contrib.auth.models import User
|
||||
import api
|
||||
from caminus.api.events import user_message
|
||||
from notification import models as notification
|
||||
from django.core.urlresolvers import reverse
|
||||
|
||||
@@ -57,6 +58,7 @@ class Award(models.Model):
|
||||
super(Award, self).save(*args, **kwargs)
|
||||
api.badge_awarded.send_robust(sender=intern(str(self.badge.slug)), award=self)
|
||||
notification.send([self.user], "badge_awarded", {"award": self, 'notice_description': self.badge, 'notice_url': reverse('user_profile')})
|
||||
user_message(self.user, "You have been awarded the '%s' badge."%self.badge)
|
||||
|
||||
def __unicode__(self):
|
||||
return "%s for %s"%(self.badge.__unicode__(), self.user.__unicode__())
|
||||
|
@@ -9,6 +9,7 @@ from django.template import RequestContext
|
||||
from django.core.urlresolvers import reverse
|
||||
from django.core.exceptions import PermissionDenied
|
||||
from notification import models as notification
|
||||
from api.servers import server_broadcast, user_broadcast
|
||||
|
||||
def index(request):
|
||||
forums = models.Forum.objects.filter(parent=None)
|
||||
@@ -44,6 +45,8 @@ def reply(request, topicID=None):
|
||||
if reply.parent.user != request.user:
|
||||
notification.send([reply.parent.user], "forum_reply", {"reply": reply, 'notice_url': reverse('forums.views.post', kwargs={'id':reply.id}), 'notice_description': reply.topic().title})
|
||||
messages.info(request, "Reply successful")
|
||||
user_message(reply.parent.user, "%s replied to your post in '%s'",
|
||||
request.user, reply.topic().title)
|
||||
return HttpResponseRedirect(reverse('forums.views.post', kwargs={"id":reply.id}))
|
||||
return render_to_response('forums/reply.html', {"parent":parentPost, "form":form}, context_instance = RequestContext(request))
|
||||
|
||||
@@ -81,6 +84,7 @@ def newTopic(request, forumID=None):
|
||||
topic.rootPost = reply
|
||||
topic.save()
|
||||
messages.info(request, "Posting successful")
|
||||
server_broadcast("New forum topic: %s", topic.title)
|
||||
return HttpResponseRedirect(reverse('forums.views.post', kwargs={'id': reply.id}))
|
||||
return render_to_response('forums/newTopic.html', {"forum":parentForum, "replyForm":replyForm, "topicForm": topicForm}, context_instance = RequestContext(request))
|
||||
|
||||
|
@@ -72,7 +72,7 @@ class InviteManageTest(TestCase):
|
||||
self.user.delete()
|
||||
|
||||
def testCreateMaxInvites(self):
|
||||
settings.CAMINUS_MAX_INVITES = 800
|
||||
settings.CAMINUS_MAX_INVITES = 80
|
||||
for i in range(0, settings.CAMINUS_MAX_INVITES*2):
|
||||
self.client.get(reverse('local.views.createInvite'))
|
||||
self.assertEqual(len(self.user.invites.all()),
|
||||
|
@@ -12,6 +12,7 @@ from django.contrib.auth import authenticate, login
|
||||
from django.core.exceptions import ObjectDoesNotExist
|
||||
import forms
|
||||
import models
|
||||
from api.events import user_message
|
||||
from forums.models import Forum
|
||||
from minecraft.forms import ProfileForm
|
||||
from minecraft.models import MinecraftProfile
|
||||
@@ -79,6 +80,7 @@ def register(request):
|
||||
profile.save()
|
||||
user = authenticate(username=userForm.cleaned_data['username'], password=userForm.cleaned_data['password'])
|
||||
notification.send_now([invite.creator], "invite_accepted", {"new_user": user})
|
||||
user_message(invite.creator, "%s has accepted your invite."%(user.username))
|
||||
login(request, user)
|
||||
del request.session['profile-invite']
|
||||
return HttpResponseRedirect(reverse('welcome'))
|
||||
|
@@ -1,7 +1,6 @@
|
||||
from django.db.models.signals import post_syncdb
|
||||
import badges.api
|
||||
import badges.models
|
||||
from local import update_badges
|
||||
from django.contrib.auth.models import User
|
||||
|
||||
def create_playtime_badges(app, created_models, verbosity, **kwargs):
|
||||
|
@@ -3,7 +3,6 @@ from django.contrib.auth.models import User, Group
|
||||
from django.db.models.signals import post_save
|
||||
from django.core.cache import cache
|
||||
from django.core.exceptions import ObjectDoesNotExist
|
||||
from minecraft import update_badges
|
||||
import socket
|
||||
import datetime
|
||||
|
||||
@@ -82,6 +81,7 @@ class PlayerSession(models.Model):
|
||||
def save(self, *args, **kwargs):
|
||||
super(PlayerSession, self).save(*args, **kwargs)
|
||||
if self.end:
|
||||
from minecraft import update_badges
|
||||
update_badges(self.player.user)
|
||||
|
||||
def create_profile(sender, instance, created, **kwargs):
|
||||
|
@@ -8,6 +8,7 @@ from django.core.urlresolvers import reverse
|
||||
from django.contrib.auth.models import User
|
||||
from notification import models as notification
|
||||
from django.contrib import messages
|
||||
from api.events import user_message
|
||||
|
||||
@login_required
|
||||
def create(request):
|
||||
@@ -22,6 +23,8 @@ def create(request):
|
||||
petition.save()
|
||||
adminUsers = User.objects.filter(is_staff=True)
|
||||
notification.send(adminUsers, "petition_opened", {"petition": petition, 'notice_url': reverse('petition.views.view', kwargs={'id':petition.id}),'notice_description': petition.id})
|
||||
for user in adminUsers:
|
||||
user_message(user, "%s has opened a petition."%(request.user))
|
||||
messages.info(request, "Petition created.")
|
||||
return HttpResponseRedirect(reverse('petition.views.view', kwargs={"id":petition.id}))
|
||||
return render_to_response('petition/create.html', {'form':form}, context_instance = RequestContext(request))
|
||||
@@ -54,6 +57,10 @@ def comment(request, id):
|
||||
comment.body = form.cleaned_data['body']
|
||||
comment.petition = petition
|
||||
comment.save()
|
||||
adminUsers = User.objects.filter(is_staff=True)
|
||||
for user in adminUsers:
|
||||
user_message(user, "%s has opened a petition."%(request.user))
|
||||
notification.send(adminUsers, "petition_commented", {"petition": petition, 'notice_url': reverse('petition.views.view', kwargs={'id':petition.id}),'notice_description': petition.id, 'comment': comment})
|
||||
if comment.author != petition.author:
|
||||
notification.send([petition.author], "petition_commented", {"petition": petition, 'notice_url': reverse('petition.views.view', kwargs={'id':petition.id}),'notice_description': petition.id, 'comment': comment})
|
||||
messages.info(request, "Comment added.")
|
||||
@@ -65,6 +72,9 @@ def close(request, id):
|
||||
petition.closed = True
|
||||
petition.save()
|
||||
if petition.author != request.user:
|
||||
user_notification(petition.author, "One of your petitions has been closed.")
|
||||
notification.send([petition.author], "petition_closed", {"petition": petition, 'notice_url': reverse('petition.views.view', kwargs={'id':petition.id}),'notice_description': petition.id})
|
||||
adminUsers = User.objects.filter(is_staff=True)
|
||||
notification.send([adminUsers], "petition_closed", {"petition": petition, 'notice_url': reverse('petition.views.view', kwargs={'id':petition.id}),'notice_description': petition.id})
|
||||
messages.info(request, "Petition closed.")
|
||||
return HttpResponseRedirect(reverse('petition.views.view', kwargs={"id":petition.id}))
|
||||
|
@@ -8,3 +8,4 @@ south==0.7.3
|
||||
django-piston==0.2.2
|
||||
coverage
|
||||
pydot==1.0.28
|
||||
beanstalkc
|
||||
|
@@ -190,6 +190,9 @@ APPVERSION_GIT_REPO = os.path.sep.join((os.path.dirname(__file__), '.git'))
|
||||
CAMINUS_MAX_INVITES = 2
|
||||
|
||||
CAMINUS_NEWS_FORUM_ID = 1
|
||||
CAMINUS_USE_BEANSTALKD = False
|
||||
CAMINUS_BEANSTALKD_HOST = 'localhost'
|
||||
CAMINUS_BEANSTALKD_PORT = 11300
|
||||
|
||||
# Load any site-local overrides, such as camin.us' database settings, etc
|
||||
try:
|
||||
|
Reference in New Issue
Block a user