The new game.azelphur.com

I've been told I should blog more, I think this is probably a good thing. So I'm going to write about what I've been working on recently: the new game.azelphur.com.

The current game.azelphur.com has been unmaintained for a while, and was one of the first Django projects I wrote. It really needed some new life and so we decided to rewrite it from scratch. It's also entirely open source and can be viewed on GitHub here

Mezzanine

Mezzanine is a CMS for Django, we decided to switch away from Django-CMS, I wasn't overly a fan of the new Aldryn stuff, and Mezzanine integrates far easier/better with third party applications. Go Mezzainne! Installing Mezzanine is fairly simple, and is documented on their website

Python Social Auth

I went with python-social-auth as I had used its predecessor before (django-social-auth) on the previous website and other social logins would be useful (twitter, facebook, etc). After installing python-social-auth as normal it is fairly easy to get it working with Mezzanine. First thing to understand is that as far as social auth is concerned, registration and logging in are both the same thing. Social auth also supports account association which is really cool, this means that you can have an existing account, and if you login with steam it'll log you into your existing account, rather than creating a new one. So all you need to do is add this snippet to templates/accounts/account_login.html and templates/accounts/account_signup.html

<a href="{% url 'social:begin' 'steam' %}">Login with Steam</a>

Now, whenever a user visits the login or signup page, they'll be able to login or register with steam too. Cool!

Multiple authentication backends

But now of course that's just steam, we wanted multiple backends. We thought about writing code for each individual social auth provider, but we decided this wasn't a good thing. After a little bit of fiddling I came up with a nice for loop.

{% for backend in backends.backends %}
    {% with 'img/social/'|add:backend|add:'-signup.png' as image_url %}
        <a class="pad-right" href="{% url 'social:begin' backend %}" data-tooltip="{% trans 'Signup with' %} {{ backend }}"><img src="{% static image_url %}"></a>
    {% endwith %}
{% endfor %}

This will loop through all python-social-auth backends we have set up on the server, and load a custom image for each backend. Much cooler.

Handling account association

We decided to handle account association properly this time. We'd like to turn our forum into a trading hub, and this means people have to trust each other. A good way to do this is have people link up all their social accounts to establish identity. Thus we really needed a nice way of handling account association and removal. Doing this was extremely simple and required no backend code at all.

{% for backend in backends.associated %}
    {% with 'img/social/'|add:backend.provider|add:'.png' as image_url %}
        <img src="{% static image_url %}">
        <form action="{% url 'social:disconnect' backend.provider %}" method="post">
            <p>{% trans 'You have signed in with' %} {{ backend.provider }}
                {% csrf_token %}
                <button type="submit">{% trans 'Un-link' %} {{ backend.provider }}</button>
            </p>
        </form>
    {% endwith %}
{% empty %}
    <p>{% trans 'You have not yet associated your account with any social networks.' %}</p>
{% endfor %}

{% for backend in backends.not_associated %}
    {% with 'img/social/'|add:backend|add:'.png' as image_url %}
        <img src="{% static image_url %}">
        <p><a href="{% url 'social:begin' backend %}">{% trans 'Sign in with' %} {{ backend }}</a></p>
    {% endwith %}
{% endfor %}

Here we are looping over backends.associated - which contains all the UserSocialAuth objects for that user, and offering them a disconnect option. We then loop over backends.not_associated offering them to sign in with that social account. Simple!

Handling Donations

Handling donations was a very interesting one, as we add perks for users on the game servers. There are lots of django apps out there that accept paypal donations for adding users to groups, but we needed some custom things, like looking up Steam accounts. I opted to write an app for this. We use python-social-auth to get the users steam account, and django-paypal to be notified of when payments arrive.

Building the form

I wanted to add an extra field to the Paypal Payments form, mainly the amount the person would be donating. Subclassing PaypalPaymentsForm makes this fairly easy

class DonateForm(PayPalPaymentsForm):
    amount = forms.ChoiceField(
        choices=[(x[0], str(x[1])+" days") for x in settings.DONATION_AMOUNTS]
    )

Getting the users STEAM_ID

We can easily pull this out of social auth. I made a function inside the FormView

def _get_steam(self):
    if self.request.user.is_authenticated():
        try:
            u = self.request.user.social_auth.filter(provider="steam").get()
            return u.uid
        except ObjectDoesNotExist:
            pass

    return ""

And added it to the FormViews context

def get_context_data(self, **kwargs):
    context = super(DonateView, self).get_context_data(**kwargs)
    context['steam'] = self._get_steam()
    return context

We also need to pass the users steam ID through paypal for the IPN you can do this by passing it through PayPals "custom" field, like so.

def get_initial(self):
    """
    Returns the initial data to use for forms on this view.
    """

    steam = self._get_steam()

    domain = get_current_site(self.request).domain

    initial = {
        "business": settings.PAYPAL_RECEIVER_EMAIL,
        "item_name": "Donation",
        "invoice": steam,
        "notify_url": "https://" + domain + reverse('paypal-ipn'),
        "return_url": "https://www.example.com/your-return-location/",
        "cancel_return": "https://www.example.com/your-cancel-location/",
        "custom": steam,  # Custom command to correlate to some function later (optional)
    }

    return initial

Communicating with the game server

In the past, we have done this in various different ways. JSON feeds, VDF feeds and special codes the user types in game, just to name a few. This time I opted for a much safer approach. I took SourceMods standard sql-admins-prefetch, and modified the queries it runs such that they'd look at djangos social auth table, which of course has steam ids in it.

To get a list of users, and their associated account IDs, we run...

SELECT user_id, uid FROM social_auth_usersocialauth

Of course, the game server wants steam ids, rather than account ids. However good news, with a bit of math you can actually convert between the two! We used the lovely CSteamID extension to handle this for us, as I'm lazy. Converting the account id is just one function call.

CSteamIDToSteamID(identity, identity, sizeof(identity));

Next up we get the groups the user is in.

"SELECT auth_user_groups.user_id AS user_id, auth_group.name AS auth_group_name FROM auth_user_groups LEFT JOIN auth_group ON auth_user_groups.group_id = auth_group.id;"

The end result of all this is that SourceMod now pulls Djangos groups. We created a SM_ADMIN group along with another group for Premium users. Now we have Django admins on our game server and can control who has what access from Djangos admin panel. How cool is that?

Mezzanine and reCAPTCHA

One of the problems we had with the old site was spam accounts, we aren't having that any more. reCAPTCHA to the rescue! Adding this to Mezzanine is extremely simple, we used django nocaptcha recaptcha and hooked it into Mezzanine by creating a forms.py file containing the following block of code

from mezzanine.accounts.forms import ProfileForm
from nocaptcha_recaptcha.fields import NoReCaptchaField


class MyProfileForm(ProfileForm):
    captcha = NoReCaptchaField()

and then set ACCOUNTS_PROFILE_FORM_CLASS = "game.forms.MyProfileForm" in settings.py

Mezzanine comments

We wanted to force users to be logged in to comment. This is really easy with Mezzanine, just add COMMENTS_ACCOUNT_REQUIRED = True to your settings.py. The problem with this is that even though users are forced to login to comment, The comments form still requires name and email to be entered. I found this silly since we already have this data. It also had URL which we didn't want either. Luckily Mezzanine allows us to override the comment form just like the login form using the COMMENT_FORM_CLASS setting. We created a custom form class which subclasses mezzanine.generic.forms.ThreadedCommentForm and on init removes name, email and url from self.fields and on clean it adds name, email and url back to cleaned_data, but sets them to empty strings. This satisfies django_comments. You can see the source code for this here we then modified Mezzanines comment template to instead use the users information, rather than the data it gathered from the comments database.

Live server info in the sidebar

Live server info involved an entirely custom application. I used python-valve to pull data from the game servers and output it as a JSON feed and added a context variable. I made this it's own project so it could be included on any site. You can find the source code and installation instructions on GitHub

social