From 3ea7848ca507c409be4354dd340c59b43da7eb01 Mon Sep 17 00:00:00 2001 From: Antony Kimpton Date: Tue, 19 Jun 2018 04:22:13 +0100 Subject: [PATCH 1/4] Clean the folder ready for Django2 --- api/__init__.py | 0 api/apps.py | 7 - api/migrations/0001_initial_squash.py | 178 ------------------------ api/migrations/__init__.py | 0 api/models/__init__.py | 6 - api/models/character.py | 30 ---- api/models/characterhistory.py | 9 -- api/models/charactermanager.py | 7 - api/models/clan.py | 14 -- api/models/clanmanager.py | 30 ---- api/models/server.py | 15 -- api/models/servermanager.py | 17 --- api/models/serversyncdata.py | 7 - api/plugins/__init__.py | 1 - api/plugins/ginfo/__init__.py | 3 - api/plugins/ginfo/firebase_pushid.py | 65 --------- api/plugins/ginfo/ginfo.py | 153 --------------------- api/plugins/ginfo/ginfocharacter.py | 12 -- api/serializers.py | 85 ------------ api/sync/__init__.py | 3 - api/sync/ginfo.py | 13 -- api/sync/serverdata.py | 188 -------------------------- api/tasks.py | 21 --- api/urls.py | 20 --- api/views/__init__.py | 11 -- api/views/base.py | 21 --- api/views/character.py | 28 ---- api/views/characterhistory.py | 28 ---- api/views/characters.py | 23 ---- api/views/clan.py | 29 ---- api/views/clancharacters.py | 28 ---- api/views/clans.py | 26 ---- api/views/server.py | 35 ----- api/views/servers.py | 25 ---- api/views/sync.py | 25 ---- api/views/widgets/__init__.py | 3 - api/views/widgets/activeclans.py | 26 ---- api/views/widgets/serverinfo.py | 16 --- manage.py | 25 ---- requirements.txt | 13 -- runtime.txt | 1 - serverthrallapi/__init__.py | 2 - serverthrallapi/celery.py | 9 -- serverthrallapi/settings.py | 98 -------------- serverthrallapi/urls.py | 7 - serverthrallapi/wsgi.py | 16 --- tox.ini | 12 -- 47 files changed, 1391 deletions(-) delete mode 100644 api/__init__.py delete mode 100644 api/apps.py delete mode 100644 api/migrations/0001_initial_squash.py delete mode 100644 api/migrations/__init__.py delete mode 100644 api/models/__init__.py delete mode 100644 api/models/character.py delete mode 100644 api/models/characterhistory.py delete mode 100644 api/models/charactermanager.py delete mode 100644 api/models/clan.py delete mode 100644 api/models/clanmanager.py delete mode 100644 api/models/server.py delete mode 100644 api/models/servermanager.py delete mode 100644 api/models/serversyncdata.py delete mode 100644 api/plugins/__init__.py delete mode 100644 api/plugins/ginfo/__init__.py delete mode 100644 api/plugins/ginfo/firebase_pushid.py delete mode 100644 api/plugins/ginfo/ginfo.py delete mode 100644 api/plugins/ginfo/ginfocharacter.py delete mode 100644 api/serializers.py delete mode 100644 api/sync/__init__.py delete mode 100644 api/sync/ginfo.py delete mode 100644 api/sync/serverdata.py delete mode 100644 api/tasks.py delete mode 100644 api/urls.py delete mode 100644 api/views/__init__.py delete mode 100644 api/views/base.py delete mode 100644 api/views/character.py delete mode 100644 api/views/characterhistory.py delete mode 100644 api/views/characters.py delete mode 100644 api/views/clan.py delete mode 100644 api/views/clancharacters.py delete mode 100644 api/views/clans.py delete mode 100644 api/views/server.py delete mode 100644 api/views/servers.py delete mode 100644 api/views/sync.py delete mode 100644 api/views/widgets/__init__.py delete mode 100644 api/views/widgets/activeclans.py delete mode 100644 api/views/widgets/serverinfo.py delete mode 100755 manage.py delete mode 100644 requirements.txt delete mode 100644 runtime.txt delete mode 100644 serverthrallapi/__init__.py delete mode 100644 serverthrallapi/celery.py delete mode 100644 serverthrallapi/settings.py delete mode 100644 serverthrallapi/urls.py delete mode 100644 serverthrallapi/wsgi.py delete mode 100644 tox.ini diff --git a/api/__init__.py b/api/__init__.py deleted file mode 100644 index e69de29..0000000 diff --git a/api/apps.py b/api/apps.py deleted file mode 100644 index 5c27fd5..0000000 --- a/api/apps.py +++ /dev/null @@ -1,7 +0,0 @@ -from __future__ import unicode_literals - -from django.apps import AppConfig - - -class ApiConfig(AppConfig): - name = 'api' diff --git a/api/migrations/0001_initial_squash.py b/api/migrations/0001_initial_squash.py deleted file mode 100644 index fb8ee02..0000000 --- a/api/migrations/0001_initial_squash.py +++ /dev/null @@ -1,178 +0,0 @@ -# -*- coding: utf-8 -*- -# Generated by Django 1.11.2 on 2018-05-27 23:31 -from __future__ import unicode_literals - -from django.db import migrations, models -import django.db.models.deletion - - -# Functions from the following migrations need manual copying. -# Move them and any dependencies into this file, then update the -# RunPython operations to refer to the local versions: -# api.migrations.0002_add_test_data - -class Migration(migrations.Migration): - - replaces = [ - (b'api', '0001_initial'), - (b'api', '0002_add_test_data'), - (b'api', '0003_remove_server_public_secret'), - (b'api', '0004_characterhistory'), - (b'api', '0005_ginfocharacter'), - (b'api', '0006_clan'), - (b'api', 'add_clan_id'), - (b'api', '0001_server_ip_address'), - (b'api', '0002_clean_data_schema'), - (b'api', '0003_add_server_fields'), - (b'api', '0004_server_version'), - (b'api', '0005_make_location_nullable') - ] - - initial = True - - dependencies = [ - ] - - operations = [ - migrations.CreateModel( - name='Character', - fields=[ - ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), - ('name', models.TextField()), - ('level', models.IntegerField()), - ('is_online', models.BooleanField()), - ('steam_id', models.TextField()), - ('conan_id', models.TextField()), - ('last_online', models.DateTimeField(null=True)), - ('last_killed_by', models.TextField(null=True)), - ('created', models.DateTimeField(auto_now_add=True)), - ('x', models.FloatField()), - ('y', models.FloatField()), - ('z', models.FloatField()), - ], - ), - migrations.CreateModel( - name='Server', - fields=[ - ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), - ('name', models.TextField()), - ('public_secret', models.UUIDField()), - ('private_secret', models.UUIDField()), - ('last_sync', models.DateTimeField(null=True)), - ], - ), - migrations.AddField( - model_name='character', - name='server', - field=models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='api.Server'), - ), - migrations.RemoveField( - model_name='server', - name='public_secret', - ), - migrations.CreateModel( - name='CharacterHistory', - fields=[ - ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), - ('created', models.DateTimeField(auto_now_add=True)), - ('x', models.FloatField()), - ('y', models.FloatField()), - ('z', models.FloatField()), - ('character', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='history', to='api.Character')), - ], - ), - migrations.CreateModel( - name='GinfoCharacter', - fields=[ - ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), - ('ginfo_marker_uid', models.TextField()), - ('character', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='ginfo_character', related_query_name='ginfo_character', to='api.Character')), - ], - ), - migrations.CreateModel( - name='Clan', - fields=[ - ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), - ('name', models.TextField()), - ('conan_id', models.TextField()), - ('conan_owner_id', models.TextField()), - ('motd', models.TextField()), - ('created', models.DateTimeField(auto_now_add=True)), - ('server', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='api.Server')), - ], - ), - migrations.AddField( - model_name='character', - name='conan_clan_id', - field=models.TextField(null=True), - ), - migrations.AlterField( - model_name='character', - name='server', - field=models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='characters', to='api.Server'), - ), - migrations.AddField( - model_name='server', - name='ip_address', - field=models.TextField(default=b''), - ), - migrations.CreateModel( - name='ServerSyncData', - fields=[ - ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), - ('data', models.TextField()), - ('created', models.DateTimeField(auto_now_add=True)), - ('server', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='+', to='api.Server')), - ], - ), - migrations.AddField( - model_name='character', - name='clan', - field=models.ForeignKey(null=True, on_delete=django.db.models.deletion.CASCADE, related_name='members', to='api.Clan'), - ), - migrations.AddField( - model_name='clan', - name='owner', - field=models.ForeignKey(null=True, on_delete=django.db.models.deletion.DO_NOTHING, related_name='owner', to='api.Character'), - ), - migrations.AlterField( - model_name='clan', - name='server', - field=models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='clans', to='api.Server'), - ), - migrations.AddField( - model_name='server', - name='max_players', - field=models.IntegerField(null=True), - ), - migrations.AddField( - model_name='server', - name='query_port', - field=models.TextField(null=True), - ), - migrations.AddField( - model_name='server', - name='tick_rate', - field=models.IntegerField(null=True), - ), - migrations.AddField( - model_name='server', - name='version', - field=models.TextField(null=True), - ), - migrations.AlterField( - model_name='character', - name='x', - field=models.FloatField(null=True), - ), - migrations.AlterField( - model_name='character', - name='y', - field=models.FloatField(null=True), - ), - migrations.AlterField( - model_name='character', - name='z', - field=models.FloatField(null=True), - ), - ] diff --git a/api/migrations/__init__.py b/api/migrations/__init__.py deleted file mode 100644 index e69de29..0000000 diff --git a/api/models/__init__.py b/api/models/__init__.py deleted file mode 100644 index 23f62a0..0000000 --- a/api/models/__init__.py +++ /dev/null @@ -1,6 +0,0 @@ -# flake8: noqa -from .character import Character -from .characterhistory import CharacterHistory -from .server import Server -from .clan import Clan -from .serversyncdata import ServerSyncData diff --git a/api/models/character.py b/api/models/character.py deleted file mode 100644 index b33b2b2..0000000 --- a/api/models/character.py +++ /dev/null @@ -1,30 +0,0 @@ -from django.db import models -from .charactermanager import CharacterManager - - -class Character(models.Model): - name = models.TextField() - level = models.IntegerField() - is_online = models.BooleanField() - steam_id = models.TextField() - conan_id = models.TextField() - conan_clan_id = models.TextField(null=True) - last_online = models.DateTimeField(null=True) - last_killed_by = models.TextField(null=True) - created = models.DateTimeField(auto_now_add=True) - clan = models.ForeignKey('api.Clan', related_name='members', null=True) - server = models.ForeignKey('api.Server', related_name='characters') - x = models.FloatField(null=True) - y = models.FloatField(null=True) - z = models.FloatField(null=True) - - objects = CharacterManager.as_manager() - - def generate_history(self, created): - from .characterhistory import CharacterHistory - return CharacterHistory( - character=self, - created=created, - x=self.x, - y=self.y, - z=self.z) diff --git a/api/models/characterhistory.py b/api/models/characterhistory.py deleted file mode 100644 index f01f1a4..0000000 --- a/api/models/characterhistory.py +++ /dev/null @@ -1,9 +0,0 @@ -from django.db import models - - -class CharacterHistory(models.Model): - created = models.DateTimeField(auto_now_add=True) - character = models.ForeignKey('api.Character', related_name='history') - x = models.FloatField() - y = models.FloatField() - z = models.FloatField() diff --git a/api/models/charactermanager.py b/api/models/charactermanager.py deleted file mode 100644 index 1a6fb57..0000000 --- a/api/models/charactermanager.py +++ /dev/null @@ -1,7 +0,0 @@ -from django.db import models -from datetime import datetime, timedelta - -class CharacterManager(models.QuerySet): - - def active_only(self): - return self.filter(last_online__gt=datetime.now() - timedelta(days=7)) diff --git a/api/models/clan.py b/api/models/clan.py deleted file mode 100644 index 1fb596c..0000000 --- a/api/models/clan.py +++ /dev/null @@ -1,14 +0,0 @@ -from django.db import models -from .clanmanager import ClanManager - - -class Clan(models.Model): - name = models.TextField() - conan_id = models.TextField() - conan_owner_id = models.TextField() - motd = models.TextField() - created = models.DateTimeField(auto_now_add=True) - owner = models.ForeignKey('api.Character', related_name='owner', null=True, on_delete=models.DO_NOTHING) - server = models.ForeignKey('api.Server', related_name='clans') - - objects = ClanManager.as_manager() diff --git a/api/models/clanmanager.py b/api/models/clanmanager.py deleted file mode 100644 index 4990c2e..0000000 --- a/api/models/clanmanager.py +++ /dev/null @@ -1,30 +0,0 @@ -from django.db import models -from django.db.models import Count, Max, OuterRef, Subquery -from datetime import datetime, timedelta - - -class ClanManager(models.QuerySet): - - def with_character_count(self): - return self.annotate(character_count=Count('members')) - - def with_last_logged_in(self): - return self.annotate(last_logged_in=Max('members__last_online')) - - def with_active_count(self): - from api.models import Character - - active_query = (Character.objects - .active_only() - .filter(clan=OuterRef('id')) - .values('clan_id') - .order_by() - .annotate(count=Count('*')) - .values('count')[:1]) - - return self.annotate(active_count=Subquery( - active_query, output_field=models.IntegerField())) - - def active_only(self): - return (self.with_last_logged_in().filter( - last_logged_in__gt=datetime.now() - timedelta(days=7))) diff --git a/api/models/server.py b/api/models/server.py deleted file mode 100644 index 952770a..0000000 --- a/api/models/server.py +++ /dev/null @@ -1,15 +0,0 @@ -from django.db import models -from .servermanager import ServerManager - - -class Server(models.Model): - name = models.TextField() - ip_address = models.TextField(default='') - version = models.TextField(null=True) - query_port = models.TextField(null=True) - max_players = models.IntegerField(null=True) - tick_rate = models.IntegerField(null=True) - private_secret = models.UUIDField() - last_sync = models.DateTimeField(null=True) - - objects = ServerManager.as_manager() diff --git a/api/models/servermanager.py b/api/models/servermanager.py deleted file mode 100644 index af4fe97..0000000 --- a/api/models/servermanager.py +++ /dev/null @@ -1,17 +0,0 @@ -from django.db import models -from django.utils import timezone -from datetime import timedelta -from django.db.models import Count - - -class ServerManager(models.QuerySet): - - def only_active(self): - active_threshold = timezone.now() - timedelta(days=2) - - return self.filter( - last_sync__gt=active_threshold, - last_sync__isnull=False) - - def with_character_count(self): - return self.annotate(character_count=Count('characters')) diff --git a/api/models/serversyncdata.py b/api/models/serversyncdata.py deleted file mode 100644 index 6f62eb2..0000000 --- a/api/models/serversyncdata.py +++ /dev/null @@ -1,7 +0,0 @@ -from django.db import models - - -class ServerSyncData(models.Model): - server = models.ForeignKey('api.Server', related_name='+') - data = models.TextField() - created = models.DateTimeField(auto_now_add=True) diff --git a/api/plugins/__init__.py b/api/plugins/__init__.py deleted file mode 100644 index 9c0fa90..0000000 --- a/api/plugins/__init__.py +++ /dev/null @@ -1 +0,0 @@ -# flake8: noqa diff --git a/api/plugins/ginfo/__init__.py b/api/plugins/ginfo/__init__.py deleted file mode 100644 index dad7e36..0000000 --- a/api/plugins/ginfo/__init__.py +++ /dev/null @@ -1,3 +0,0 @@ -# flake8: noqa -from .ginfo import GinfoPlugin -from .ginfocharacter import GinfoCharacter diff --git a/api/plugins/ginfo/firebase_pushid.py b/api/plugins/ginfo/firebase_pushid.py deleted file mode 100644 index b4ba738..0000000 --- a/api/plugins/ginfo/firebase_pushid.py +++ /dev/null @@ -1,65 +0,0 @@ -# This is taken from -# https://gist.github.com/risent/4cab3878d995bec7d1c2 -# which is a Python port of the official JS code by Firebase -# https://gist.github.com/mikelehen/3596a30bd69384624c11 -import random -import time -from exceptions import ValueError - - -""" - Generates PushID for Firebase -""" -class PushID(object): - # Modeled after base64 web-safe chars, but ordered by ASCII. - PUSH_CHARS = ('-0123456789' - 'ABCDEFGHIJKLMNOPQRSTUVWXYZ' - '_abcdefghijklmnopqrstuvwxyz') - - def __init__(self): - - # Timestamp of last push, used to prevent local collisions if you - # pushtwice in one ms. - self.lastPushTime = 0 - - # We generate 72-bits of randomness which get turned into 12 - # characters and appended to the timestamp to prevent - # collisions with other clients. We store the last characters - # we generated because in the event of a collision, we'll use - # those same characters except "incremented" by one. - self.lastRandChars = [0] * 12 - - def next_id(self): - now = int(time.time() * 1000) - duplicateTime = (now == self.lastPushTime) - self.lastPushTime = now - timeStampChars = [''] * 8 - - for i in range(7, -1, -1): - timeStampChars[i] = self.PUSH_CHARS[now % 64] - now = int(now / 64) - - if (now != 0): - raise ValueError('We should have converted the entire timestamp.') - - uid = ''.join(timeStampChars) - - if not duplicateTime: - for i in range(12): - self.lastRandChars[i] = int(random.random() * 64) - else: - # If the timestamp hasn't changed since last push, use the - # same random number, except incremented by 1. - for i in range(11, -1, -1): - if self.lastRandChars[i] == 63: - self.lastRandChars[i] = 0 - else: - break - self.lastRandChars[i] += 1 - - for i in range(12): - uid += self.PUSH_CHARS[self.lastRandChars[i]] - - if len(uid) != 20: - raise ValueError('Length should be 20.') - return uid diff --git a/api/plugins/ginfo/ginfo.py b/api/plugins/ginfo/ginfo.py deleted file mode 100644 index 5c7e650..0000000 --- a/api/plugins/ginfo/ginfo.py +++ /dev/null @@ -1,153 +0,0 @@ -import math - -import requests - -from firebase_pushid import PushID -from ginfocharacter import GinfoCharacter - - -class GinfoPlugin(object): - """ - GinfoPlugin provides methods to post a position to the ginfo firebase database - """ - API_URL = 'https://ginfo-34baa.firebaseio.com' - GAME = 'conanexiles' - MAP = 'conanexiles_desert' - SERVERTHRALL_USER_UID = 'y67Efa3kddSuhb26UoUapIJakMh1' - - def __init__(self): - self.uid_generator = PushID() - - def marker_path(self, group, marker): - """ - Returns the path to which a marker has to be posted in the firebase database - - @param group: UID of the ginfo group for which the marker should be posted - @param marker: UID of the marker that should be posted - """ - return 'games/%s/groups/%s/maps/%s/markers/%s' % (self.GAME, group, self.MAP, marker) - - def access_token_path(self, group, marker): - """ - Returns the path to which the groups access token has to be posted in the firebase database - - @param group: UID of the ginfo group for which the marker should be posted - @param marker: UID of the marker that should be posted - """ - return 'groupAccessTokenVerifier/%s/%s' % (group, marker) - - TRANSFORMATION_KX = 50.60049940473328 - TRANSFORMATION_KY = -50.83608443474857 - TRANSFORMATION_DX = -2644742.2783469525 - TRANSFORMATION_DY = -2647361.614800021 - - def linear_transform(self, x, y): - """ - Applies a linear transform to the point x/y - """ - return ( - x * self.TRANSFORMATION_KX + self.TRANSFORMATION_DX, - y * self.TRANSFORMATION_KY + self.TRANSFORMATION_DY - ) - - def convert_position(self, x, y): - """ - Converts the given position from x/y to lat/lng coordinates - @param x, y: x/y Position of in the game - @return: lat/lng for the same position on the ginfo map - """ - (x, y) = self.linear_transform(x, y) - return SphericalMercator.unproject(x, y) - - DEFAULT_MARKER_COLOR = "#1D8BF1" - DEFAULT_MARKER_TYPE = 1 - DEFAULT_MARKER_SKIN = 0 - - def update_position(self, character, group, access_token): - """ - Posts the position of this character to Firebase - @param character: - The character for which the position should be updated - @param group: - The group in which the marker should be postet - @param access_token: - Access token for the ginfo group - """ - if character.x is None or character.y is None: - return - - (lat, lng) = self.convert_position( - float(character.x), float(character.y)) - - ginfo_character, created = GinfoCharacter.objects.get_or_create( - character_id=character.id) - if created: - ginfo_character.ginfo_marker_uid = self.uid_generator.next_id() - ginfo_character.save() - - data = { - self.marker_path(group, ginfo_character.ginfo_marker_uid): { - "name": character.name, - "updatedAt": { - # This tells firebase to use its internal timestamp - ".sv": "timestamp" - }, - "createdAt": { - # This tells firebase to use its internal timestamp - ".sv": "timestamp" - }, - "color": self.DEFAULT_MARKER_COLOR, - "creatorId": self.SERVERTHRALL_USER_UID, - "position": { - "lat": lat, - "lng": lng - }, - "type": self.DEFAULT_MARKER_TYPE, - "skin": self.DEFAULT_MARKER_SKIN, - }, - self.access_token_path(group, ginfo_character.ginfo_marker_uid): access_token - } - - response = requests.patch( - url=self.API_URL + '/.json', - json=data - ) - try: - json_data = response.json() - if "error" in json_data: - # TODO: Use proper logger - print "Error from Ginfo Firebase: " + json_data["error"] - except: - pass - - -class SphericalMercator(object): - R = 6378137 - MAX_LATITUDE = 85.0511287798 - - @staticmethod - def project(lat, lng): - """ - Converts a lat/lng coordiante to a x/y point - """ - d = math.pi / 180.0 - lat = max( - min(SphericalMercator.MAX_LATITUDE, lat), - - SphericalMercator.MAX_LATITUDE - ) - sin = math.sin(lat * d) - return ( - SphericalMercator.R * lng * d, - SphericalMercator.R * math.log((1 + sin) / (1 - sin)) / 2.0 - ) - - @staticmethod - def unproject(x, y): - """ - Converts a x/y point to a lat/lng coordinate - """ - d = 180.0 / math.pi - return ( - (2.0 * math.atan(math.exp(y / SphericalMercator.R)) - (math.pi / 2.0)) * d, - x * d / SphericalMercator.R - ) diff --git a/api/plugins/ginfo/ginfocharacter.py b/api/plugins/ginfo/ginfocharacter.py deleted file mode 100644 index f593cda..0000000 --- a/api/plugins/ginfo/ginfocharacter.py +++ /dev/null @@ -1,12 +0,0 @@ -from django.db import models - -from api.models import Character - - -class GinfoCharacter(models.Model): - character = models.ForeignKey( - Character, - related_name="ginfo_character", - related_query_name="ginfo_character", - ) - ginfo_marker_uid = models.TextField() diff --git a/api/serializers.py b/api/serializers.py deleted file mode 100644 index 26aeccc..0000000 --- a/api/serializers.py +++ /dev/null @@ -1,85 +0,0 @@ -import calendar -from api.models import Character - -import serpy - - -class EnumField(serpy.Field): - - def to_value(self, v): - return v.name - - -class DatetimeToUnixField(serpy.Field): - - def to_value(self, v): - if v is None: - return None - return calendar.timegm(v.utctimetuple()) - -class CharacterLocationField(serpy.Field): - - getter_takes_serializer = True - - def as_getter(self, serializer_field_name, serializer_cls): - return lambda self, v: {'x': v.x, 'y': v.y, 'z': v.z} - - -class ClanSerializer(serpy.Serializer): - id = serpy.Field() - server_id = serpy.Field() - name = serpy.Field() - motd = serpy.Field() - owner_id = serpy.Field() - owner_name = serpy.MethodField() - character_count = serpy.Field() - created = DatetimeToUnixField() - active_count = serpy.MethodField() - - def get_active_count(self, item): - return item.active_count if item.active_count else 0 - - def get_owner_name(self, item): - return item.owner.name if item.owner is not None else None - - -class CharacterSerializer(serpy.Serializer): - id = serpy.Field() - server_id = serpy.Field() - steam_id = serpy.Field() - clan_id = serpy.Field() - name = serpy.Field() - level = serpy.Field() - is_online = serpy.Field() - clan_name = serpy.MethodField() - created = DatetimeToUnixField() - last_online = DatetimeToUnixField() - last_killed_by = serpy.Field() - - def get_clan_name(self, item): - return item.clan.name if item.clan is not None else None - - -class CharacterHistorySerializer(serpy.Serializer): - character_id = serpy.Field() - created = DatetimeToUnixField() - - -class ServerSerializer(serpy.Serializer): - id = serpy.Field() - name = serpy.Field() - character_count = serpy.Field() - online_count = serpy.MethodField() - ip_address = serpy.Field() - query_port = serpy.Field() - tick_rate = serpy.Field() - max_players = serpy.Field() - version = serpy.Field() - last_sync = DatetimeToUnixField() - - def get_online_count(self, server): - return Character.objects.filter(server=server, is_online=True).count() - - -class ServerAdminSerializer(ServerSerializer): - private_secret = serpy.Field() diff --git a/api/sync/__init__.py b/api/sync/__init__.py deleted file mode 100644 index cd9f9fd..0000000 --- a/api/sync/__init__.py +++ /dev/null @@ -1,3 +0,0 @@ -# flake8: noqa -from .ginfo import sync_ginfo -from .serverdata import sync_server_data diff --git a/api/sync/ginfo.py b/api/sync/ginfo.py deleted file mode 100644 index 3ba8fd0..0000000 --- a/api/sync/ginfo.py +++ /dev/null @@ -1,13 +0,0 @@ -from api.models import Character -from api.plugins.ginfo import GinfoPlugin - - -def sync_ginfo(character_ids, group_uid, access_token): - characters = (Character.objects - .filter(id__in=character_ids) - .order_by('id')) - - plugin = GinfoPlugin() - - for character in characters: - plugin.update_position(character, group_uid, access_token) diff --git a/api/sync/serverdata.py b/api/sync/serverdata.py deleted file mode 100644 index 5841d67..0000000 --- a/api/sync/serverdata.py +++ /dev/null @@ -1,188 +0,0 @@ -from api.models import Clan, Character, CharacterHistory, ServerSyncData -from datetime import datetime -from django.conf import settings -from django.db import transaction -from django.db.models import Q -from django.utils import timezone -import json -import pytz - - -def sync_characters(server, data): - now = timezone.now() - history_buffer = [] - changed_ids = [] - - # Delete removed characters - sent_ids = [c['conan_id'] for c in data] - (Character.objects - .filter(server=server) - .filter(~Q(conan_id__in=sent_ids)) - .delete()) - - character_map = {} - - for sync_data in data: - - character = (Character.objects - .filter(server=server) - .filter(conan_id=sync_data['conan_id']) - .first()) - - if character is None: - character = Character() - - has_moved = ( - (sync_data['x'] == character.x) or - (sync_data['y'] == character.y) or - (sync_data['z'] == character.y)) - - last_online = (datetime - .utcfromtimestamp(sync_data['last_online']) - .replace(tzinfo=pytz.utc)) - - character.server = server - character.clan_id = None - character.conan_id = sync_data['conan_id'] - character.conan_clan_id = sync_data.get('clan_id') - character.name = sync_data['name'] - character.level = sync_data['level'] - character.is_online = sync_data['is_online'] - character.steam_id = sync_data['steam_id'] - character.last_killed_by = sync_data['last_killed_by'] - character.x = sync_data['x'] - character.y = sync_data['y'] - character.z = sync_data['z'] - character.last_online = last_online - character.save() - - character_map[character.conan_id] = character - - if has_moved: - changed_ids.append(character.id) - history_buffer.append(character.generate_history(now)) - - if settings.ST_ENABLE_HISTORY: - CharacterHistory.objects.bulk_create(history_buffer) - - return character_map, changed_ids - - -def sync_clans(server, data): - # Delete clans we werent sent - sent_ids = [c['id'] for c in data] - (Clan.objects - .filter(server=server) - .filter(~Q(conan_id__in=sent_ids)) - .delete()) - - clan_map = {} - - for sync_data in data: - clan = (Clan.objects - .filter( - server=server, - conan_id=sync_data['id']) - .first()) - - if clan is None: - clan = Clan() - - clan.server = server - clan.owner_id = None - clan.conan_id = sync_data['id'] - clan.conan_owner_id = sync_data['owner_id'] - clan.name = sync_data['name'] - clan.motd = sync_data['motd'] - clan.save() - - clan_map[clan.conan_id] = clan - - return clan_map - - -def fix_primary_keys(character_map, clan_map): - for character in character_map.values(): - if character.conan_clan_id in clan_map: - character.clan = clan_map[character.conan_clan_id] - character.save() - - for clan in clan_map.values(): - if clan.conan_owner_id in character_map: - clan.owner = character_map[clan.conan_owner_id] - clan.save() - - -def get_int(data, key, default=None): - if key not in data: - return default - try: - return int(data[key]) - except ValueError: - return default - except TypeError: - return default - - -def remove_outer_quotes(value): - if value.startswith('"'): - value = value[1:] - if value.endswith('"'): - value = value[:len(value) - 1] - return value - - -def sync_server_data(sync_data_id, request_get_params): - sync_data = (ServerSyncData.objects - .filter(id=sync_data_id) - .select_related('server') - .first()) - - if sync_data is None: - print 'Dropping sync request because data is invalid' - return - - sync_data.delete() - server = sync_data.server - - if server.last_sync and server.last_sync >= sync_data.created: - print "Dropping stale sync request" - return - - data = json.loads(sync_data.data) - changed_character_ids = [] - - with transaction.atomic(): - if 'version' in data: - server.version = data['version'] - - if 'server' in data: - server.name = remove_outer_quotes(data['server'].get('name', '')) - server.ip_address = data['server'].get('ip_address', '') - server.query_port = get_int(data['server'], 'query_port') - server.max_players = get_int(data['server'], 'max_players') - server.tick_rate = get_int(data['server'], 'tick_rate') - - if 'characters' in data: - character_map, changed_character_ids = sync_characters(server, data['characters']) - - if 'clans' in data: - clan_map = sync_clans(server, data['clans']) - - fix_primary_keys(character_map, clan_map) - - server.last_sync = timezone.now() - server.save() - - has_ginfo = ( - 'ginfo_group_uid' in request_get_params and - 'ginfo_access_token' in request_get_params) - - if has_ginfo: - from api.tasks import sync_ginfo_task - sync_ginfo_task.delay(changed_character_ids, - request_get_params['ginfo_group_uid'], - request_get_params['ginfo_access_token']) - - from api.tasks import delete_old_history_task - delete_old_history_task.delay() diff --git a/api/tasks.py b/api/tasks.py deleted file mode 100644 index 0152f37..0000000 --- a/api/tasks.py +++ /dev/null @@ -1,21 +0,0 @@ -from .sync import sync_server_data, sync_ginfo -from api.models import CharacterHistory -from datetime import timedelta -from django.utils import timezone -from serverthrallapi.celery import app - - -@app.task() -def sync_ginfo_task(character_ids, group_uid, access_token): - sync_ginfo(character_ids, group_uid, access_token) - - -@app.task() -def sync_server_data_task(sync_data_id, request_get_params): - sync_server_data(sync_data_id, request_get_params) - - -@app.task() -def delete_old_history_task(): - history_threshold = timezone.now() - timedelta(days=5) - CharacterHistory.objects.filter(created__lt=history_threshold).delete() diff --git a/api/urls.py b/api/urls.py deleted file mode 100644 index 46efc39..0000000 --- a/api/urls.py +++ /dev/null @@ -1,20 +0,0 @@ -from django.conf.urls import url - -from .views import (CharacterHistoryView, CharactersView, CharacterView, - ClansView, ClanView, ServersView, ServerView, - SyncCharactersView, ClanCharactersView, ActiveClansView, - ServerInfoView) - -urlpatterns = [ - url(r'^$', ServersView.as_view()), - url(r'^widgets/serverinfo$', ServerInfoView.as_view()), - url(r'^(?P\d+)$', ServerView.as_view()), - url(r'^(?P\d+)/sync/characters$', SyncCharactersView.as_view()), - url(r'^(?P\d+)/clans$', ClansView.as_view()), - url(r'^(?P\d+)/clans/(?P\d+)$', ClanView.as_view()), - url(r'^(?P\d+)/clans/(?P\d+)/characters$', ClanCharactersView.as_view()), - url(r'^(?P\d+)/characters$', CharactersView.as_view()), - url(r'^(?P\d+)/characters/(?P\d+)$', CharacterView.as_view()), - url(r'^(?P\d+)/characters/(?P\d+)/history$', CharacterHistoryView.as_view()), - url(r'^(?P\d+)/widgets/activeclans$', ActiveClansView.as_view()), -] diff --git a/api/views/__init__.py b/api/views/__init__.py deleted file mode 100644 index dc11f7e..0000000 --- a/api/views/__init__.py +++ /dev/null @@ -1,11 +0,0 @@ -# flake8: noqa -from .character import CharacterView -from .characterhistory import CharacterHistoryView -from .characters import CharactersView -from .clan import ClanView -from .clans import ClansView -from .server import ServerView -from .servers import ServersView -from .sync import SyncCharactersView -from .clancharacters import ClanCharactersView -from .widgets import * diff --git a/api/views/base.py b/api/views/base.py deleted file mode 100644 index 7b8d487..0000000 --- a/api/views/base.py +++ /dev/null @@ -1,21 +0,0 @@ -from django.views import View - -from api.models import Server - - -class BaseView(View): - - def get_servers(self): - return (Server.objects - .with_character_count()) - - def get_server_public(self, request, server_id): - return (self.get_servers() - .filter(id=server_id) - .first()) - - def get_server_private(self, request, server_id): - return (self.get_servers() - .filter(id=server_id) - .filter(private_secret=request.GET.get('private_secret', None)) - .first()) diff --git a/api/views/character.py b/api/views/character.py deleted file mode 100644 index 5e46b91..0000000 --- a/api/views/character.py +++ /dev/null @@ -1,28 +0,0 @@ -from django.http import HttpResponse, JsonResponse - -from api.models import Character -from api.serializers import CharacterSerializer - -from .base import BaseView - - -class CharacterView(BaseView): - - def get(self, request, server_id, character_id): - server = self.get_server_public(request, server_id) - - if server is None: - return HttpResponse('server does not exist', status=400) - - character = (Character.objects - .filter( - server_id=server.id, - id=character_id) - .select_related('clan') - .first()) - - if character is None: - return HttpResponse('character does not exist', status=400) - - serialized = CharacterSerializer(character).data - return JsonResponse(serialized, status=200) diff --git a/api/views/characterhistory.py b/api/views/characterhistory.py deleted file mode 100644 index 1043497..0000000 --- a/api/views/characterhistory.py +++ /dev/null @@ -1,28 +0,0 @@ -from django.http import HttpResponse, JsonResponse - -from api.models import Character, CharacterHistory -from api.serializers import CharacterHistorySerializer - -from .base import BaseView - - -class CharacterHistoryView(BaseView): - - def get(self, request, server_id, character_id): - server = self.get_server_public(request, server_id) - - if server is None: - return HttpResponse('server does not exist', status=400) - - character = (Character.objects - .filter( - server_id=server.id, - id=character_id) - .first()) - - if character is None: - return HttpResponse('character does not exist', status=400) - - histories = CharacterHistory.objects.filter(character_id=character.id) - serialized = CharacterHistorySerializer(histories, many=True).data - return JsonResponse({'history': serialized}, status=200) diff --git a/api/views/characters.py b/api/views/characters.py deleted file mode 100644 index 7e8d1e2..0000000 --- a/api/views/characters.py +++ /dev/null @@ -1,23 +0,0 @@ -from django.http import HttpResponse, JsonResponse - -from api.models import Character -from api.serializers import CharacterSerializer - -from .base import BaseView - - -class CharactersView(BaseView): - - def get(self, request, server_id): - server = self.get_server_public(request, server_id) - - if server is None: - return HttpResponse('server does not exist', status=400) - - characters = (Character.objects - .filter(server_id=server.id) - .select_related('clan') - .all()) - - serialized = CharacterSerializer(characters, many=True).data - return JsonResponse(serialized, status=200, safe=False) diff --git a/api/views/clan.py b/api/views/clan.py deleted file mode 100644 index 3c8e7ac..0000000 --- a/api/views/clan.py +++ /dev/null @@ -1,29 +0,0 @@ -from django.http import HttpResponse, JsonResponse - -from api.models import Clan -from api.serializers import ClanSerializer - -from .base import BaseView - - -class ClanView(BaseView): - - def get(self, request, server_id, clan_id): - server = self.get_server_public(request, server_id) - - if server is None: - return HttpResponse('server does not exist', status=400) - - clan = (Clan.objects - .filter( - server_id=server.id, - id=clan_id) - .with_character_count() - .with_active_count() - .first()) - - if clan is None: - return HttpResponse('clan does not exist', status=400) - - serialized = ClanSerializer(clan).data - return JsonResponse(serialized, status=200) diff --git a/api/views/clancharacters.py b/api/views/clancharacters.py deleted file mode 100644 index d6794dc..0000000 --- a/api/views/clancharacters.py +++ /dev/null @@ -1,28 +0,0 @@ -from django.http import HttpResponse, JsonResponse - -from api.models import Character, Clan -from api.serializers import CharacterSerializer - -from .base import BaseView - - -class ClanCharactersView(BaseView): - - def get(self, request, server_id, clan_id): - server = self.get_server_public(request, server_id) - - if server is None: - return HttpResponse('server does not exist', status=400) - - clan = Clan.objects.filter(server_id=server.id, id=clan_id).first() - - if clan is None: - return HttpResponse('clan does not exist', status=400) - - characters = (Character.objects - .filter(server_id=server.id, clan_id=clan_id) - .select_related('clan') - .all()) - - serialized = CharacterSerializer(characters, many=True).data - return JsonResponse(serialized, status=200, safe=False) diff --git a/api/views/clans.py b/api/views/clans.py deleted file mode 100644 index 6b19a8a..0000000 --- a/api/views/clans.py +++ /dev/null @@ -1,26 +0,0 @@ -from django.http import HttpResponse, JsonResponse - -from api.models import Clan -from api.serializers import ClanSerializer - -from .base import BaseView - - -class ClansView(BaseView): - - def get(self, request, server_id): - server = self.get_server_public(request, server_id) - - if server is None: - return HttpResponse('server does not exist', status=400) - - clans = (Clan.objects - .filter(server_id=server.id) - .select_related('owner') - .with_character_count() - .with_active_count() - .filter(active_count__gt=0) - .all()) - - serialized = ClanSerializer(clans, many=True).data - return JsonResponse(serialized, status=200, safe=False) diff --git a/api/views/server.py b/api/views/server.py deleted file mode 100644 index a878170..0000000 --- a/api/views/server.py +++ /dev/null @@ -1,35 +0,0 @@ -from django.http import HttpResponse, JsonResponse - -from api.serializers import ServerSerializer, ServerAdminSerializer - -from .base import BaseView - - -class ServerView(BaseView): - - def get(self, request, server_id): - server = self.get_server_public(request, server_id) - - if server is None: - return HttpResponse('server does not exist', status=404) - - serialized = ServerSerializer(server).data - return JsonResponse(serialized, status=200) - - def post(self, request, server_id): - if 'private_secret' not in request.GET: - return HttpResponse('missing required param private_secret') - - data = request.GET - server = self.get_server_private(request, server_id) - - if server is None: - return HttpResponse('server does not exist', status=404) - - if 'name' in data: - server.name = data['name'] - - server.save() - - serialized = ServerAdminSerializer(server).data - return JsonResponse(serialized, status=200) diff --git a/api/views/servers.py b/api/views/servers.py deleted file mode 100644 index 35a0d91..0000000 --- a/api/views/servers.py +++ /dev/null @@ -1,25 +0,0 @@ -from uuid import uuid1 - -from django.http import JsonResponse - -from api.models import Server -from api.serializers import ServerAdminSerializer, ServerSerializer - -from .base import BaseView - - -class ServersView(BaseView): - - def get(self, request): - servers = self.get_servers().only_active() - serialized = ServerSerializer(servers, many=True).data - return JsonResponse({'items': serialized}) - - def post(self, request): - server = Server() - server.private_secret = uuid1() - server.save() - - server = self.get_server_public(request, server.id) - serialized = ServerAdminSerializer(server).data - return JsonResponse(serialized, status=200) diff --git a/api/views/sync.py b/api/views/sync.py deleted file mode 100644 index aa8dd0b..0000000 --- a/api/views/sync.py +++ /dev/null @@ -1,25 +0,0 @@ -from django.http import HttpResponse - -from api.tasks import sync_server_data_task -from api.models import ServerSyncData - -from .base import BaseView - - -class SyncCharactersView(BaseView): - - def post(self, request, server_id): - if 'private_secret' not in request.GET: - return HttpResponse('missing required param private_secret', status=400) - - server = self.get_server_private(request, server_id) - - if server is None: - return HttpResponse('server does not exist', status=404) - - sync_data = ServerSyncData.objects.create( - server=server, - data=request.body) - - sync_server_data_task.delay(sync_data.id, request.GET) - return HttpResponse(status=200) diff --git a/api/views/widgets/__init__.py b/api/views/widgets/__init__.py deleted file mode 100644 index 272c7cd..0000000 --- a/api/views/widgets/__init__.py +++ /dev/null @@ -1,3 +0,0 @@ -# flake8: noqa -from .activeclans import ActiveClansView -from .serverinfo import ServerInfoView diff --git a/api/views/widgets/activeclans.py b/api/views/widgets/activeclans.py deleted file mode 100644 index 8039a0d..0000000 --- a/api/views/widgets/activeclans.py +++ /dev/null @@ -1,26 +0,0 @@ -from django.http import HttpResponse, JsonResponse - -from api.models import Clan -from api.serializers import ClanSerializer - -from ..base import BaseView - - -class ActiveClansView(BaseView): - - def get(self, request, server_id): - server = self.get_server_public(request, server_id) - - if server is None: - return HttpResponse('server does not exist', status=400) - - clans = (Clan.objects - .filter(server_id=server.id) - .select_related('owner') - .with_character_count() - .with_active_count() - .active_only() - .order_by('-active_count')) - - serialized = ClanSerializer(clans, many=True).data - return JsonResponse(serialized, status=200, safe=False) diff --git a/api/views/widgets/serverinfo.py b/api/views/widgets/serverinfo.py deleted file mode 100644 index c8270b4..0000000 --- a/api/views/widgets/serverinfo.py +++ /dev/null @@ -1,16 +0,0 @@ -from django.http import JsonResponse - -from api.models import Server, Character, Clan - -from ..base import BaseView - - -class ServerInfoView(BaseView): - - def get(self, request): - data = { - 'server_count': Server.objects.count(), - 'character_count': Character.objects.count(), - 'clan_count': Clan.objects.count() - } - return JsonResponse(data, status=200, safe=False) diff --git a/manage.py b/manage.py deleted file mode 100755 index fc791ff..0000000 --- a/manage.py +++ /dev/null @@ -1,25 +0,0 @@ -#!/usr/bin/env python -# flake8: noqa -import os -import sys - -sys.dont_write_bytecode = True - -if __name__ == "__main__": - os.environ.setdefault("DJANGO_SETTINGS_MODULE", "serverthrallapi.settings") - try: - from django.core.management import execute_from_command_line - except ImportError: - # The above import may fail for some other reason. Ensure that the - # issue is really that Django is missing to avoid masking other - # exceptions on Python 2. - try: - import django - except ImportError: - raise ImportError( - "Couldn't import Django. Are you sure it's installed and " - "available on your PYTHONPATH environment variable? Did you " - "forget to activate a virtual environment?" - ) - raise - execute_from_command_line(sys.argv) diff --git a/requirements.txt b/requirements.txt deleted file mode 100644 index ac1b583..0000000 --- a/requirements.txt +++ /dev/null @@ -1,13 +0,0 @@ -celery==4.1.0 -dj-database-url==0.4.1 -django-celery-beat==1.0.1 -django-cors-headers==2.1.0 -django==1.11.2 -flake8==3.3.0 -gunicorn==19.6.0 -isort==4.2.5 -newrelic==2.88.1.73 -psycopg2==2.6.2 -pytz==2017.2 -requests==2.18.1 -serpy==0.1.1 diff --git a/runtime.txt b/runtime.txt deleted file mode 100644 index ba85ab9..0000000 --- a/runtime.txt +++ /dev/null @@ -1 +0,0 @@ -python-2.7.13 \ No newline at end of file diff --git a/serverthrallapi/__init__.py b/serverthrallapi/__init__.py deleted file mode 100644 index 7f13f4f..0000000 --- a/serverthrallapi/__init__.py +++ /dev/null @@ -1,2 +0,0 @@ -# flake8: noqa -from .celery import app as celery_app \ No newline at end of file diff --git a/serverthrallapi/celery.py b/serverthrallapi/celery.py deleted file mode 100644 index f6b5cdf..0000000 --- a/serverthrallapi/celery.py +++ /dev/null @@ -1,9 +0,0 @@ -from __future__ import absolute_import, unicode_literals -import os -from celery import Celery - -os.environ.setdefault('DJANGO_SETTINGS_MODULE', 'serverthrallapi.settings') - -app = Celery('proj') -app.config_from_object('django.conf:settings', namespace='CELERY') -app.autodiscover_tasks() diff --git a/serverthrallapi/settings.py b/serverthrallapi/settings.py deleted file mode 100644 index 095bd87..0000000 --- a/serverthrallapi/settings.py +++ /dev/null @@ -1,98 +0,0 @@ -import os -import json -import dj_database_url - -ENVIRONMENT = os.environ.get('ENVIRONMENT', 'DEVELOPMENT') - -# Build paths inside the project like this: os.path.join(BASE_DIR, ...) -BASE_DIR = os.path.dirname(os.path.dirname(os.path.abspath(__file__))) - -# SECURITY WARNING: keep the secret key used in production secret! -SECRET_KEY = '_@#qy@r25)+ae#_=%87!b2ad1q)e-7+zd#9ty&!0n7%u=6lh)g' - -# SECURITY WARNING: don't run with debug turned on in production! -DEBUG = ENVIRONMENT == 'DEVELOPMENT' - -ALLOWED_HOSTS = ['*'] - -# Application definition -INSTALLED_APPS = [ - 'django.contrib.admin', - 'django.contrib.auth', - 'django.contrib.contenttypes', - 'django.contrib.sessions', - 'django.contrib.messages', - 'django_celery_beat', - 'corsheaders', - 'api' -] - -CORS_ORIGIN_ALLOW_ALL = True - -MIDDLEWARE = [ - 'django.middleware.security.SecurityMiddleware', - 'django.contrib.sessions.middleware.SessionMiddleware', - 'corsheaders.middleware.CorsMiddleware', - 'django.middleware.common.CommonMiddleware', - 'django.contrib.auth.middleware.AuthenticationMiddleware', - 'django.contrib.messages.middleware.MessageMiddleware', -] - -ROOT_URLCONF = 'serverthrallapi.urls' - -TEMPLATES = [ - { - 'BACKEND': 'django.template.backends.django.DjangoTemplates', - 'DIRS': [], - 'APP_DIRS': True, - 'OPTIONS': { - 'context_processors': [ - 'django.template.context_processors.debug', - 'django.template.context_processors.request', - 'django.contrib.auth.context_processors.auth', - 'django.contrib.messages.context_processors.messages', - ], - }, - }, -] - -WSGI_APPLICATION = 'serverthrallapi.wsgi.application' - - -# Database -# https://docs.djangoproject.com/en/1.11/ref/settings/#databases -DATABASES = { - 'default': { - 'ENGINE': 'django.db.backends.sqlite3', - 'NAME': os.path.join(BASE_DIR, 'db.sqlite3'), - } -} - -# Update database configuration with $DATABASE_URL. -db_from_env = dj_database_url.config(conn_max_age=500) -DATABASES['default'].update(db_from_env) - -# Password validation -# https://docs.djangoproject.com/en/1.11/ref/settings/#auth-password-validators -AUTH_PASSWORD_VALIDATORS = [ - {'NAME': 'django.contrib.auth.password_validation.UserAttributeSimilarityValidator', }, - {'NAME': 'django.contrib.auth.password_validation.MinimumLengthValidator', }, - {'NAME': 'django.contrib.auth.password_validation.CommonPasswordValidator', }, - {'NAME': 'django.contrib.auth.password_validation.NumericPasswordValidator', }, -] - -# Internationalization -# https://docs.djangoproject.com/en/1.11/topics/i18n/ -LANGUAGE_CODE = 'en-us' -TIME_ZONE = 'UTC' -USE_I18N = True -USE_L10N = True -USE_TZ = True - -# Celery -CELERY_TASK_ALWAYS_EAGER = DEBUG -CELERY_TASK_SERIALIZER = 'json' -CELERY_BROKER_URL = os.environ.get('RABBITMQ_BIGWIG_URL') - - -ST_ENABLE_HISTORY = os.environ.get('ST_ENABLE_HISTORY', 'false').lower().startswith('true') diff --git a/serverthrallapi/urls.py b/serverthrallapi/urls.py deleted file mode 100644 index 3220c19..0000000 --- a/serverthrallapi/urls.py +++ /dev/null @@ -1,7 +0,0 @@ -from django.conf.urls import include, url -from django.contrib import admin - -urlpatterns = [ - url(r'^admin/', admin.site.urls), - url(r'^api/', include('api.urls')), -] diff --git a/serverthrallapi/wsgi.py b/serverthrallapi/wsgi.py deleted file mode 100644 index ebb70bf..0000000 --- a/serverthrallapi/wsgi.py +++ /dev/null @@ -1,16 +0,0 @@ -""" -WSGI config for serverthrallapi project. - -It exposes the WSGI callable as a module-level variable named ``application``. - -For more information on this file, see -https://docs.djangoproject.com/en/1.11/howto/deployment/wsgi/ -""" - -import os - -from django.core.wsgi import get_wsgi_application - -os.environ.setdefault("DJANGO_SETTINGS_MODULE", "serverthrallapi.settings") - -application = get_wsgi_application() diff --git a/tox.ini b/tox.ini deleted file mode 100644 index e6ad30e..0000000 --- a/tox.ini +++ /dev/null @@ -1,12 +0,0 @@ -[flake8] -ignore = E221,E128,E122,E302,D100,D101,D102,D103,E241,D200,D208 -exclude = .git,virtualenv -max-complexity = 14 -max-line-length = 140 - -[pytest] -ignore = .git,__pycache__,.cache,virtualenv - -[isort] -line_length=140 -multi_line_output=4 From 8691b49a3f84d7e2684bb1427ce8778d0fe31063 Mon Sep 17 00:00:00 2001 From: Antony Kimpton Date: Tue, 19 Jun 2018 04:32:31 +0100 Subject: [PATCH 2/4] Just generated the api app - currently just a shell --- api/__init__.py | 0 api/admin.py | 3 + api/apps.py | 5 ++ api/migrations/__init__.py | 0 api/models.py | 3 + api/tests.py | 3 + api/views.py | 3 + manage.py | 15 +++++ serverthrallapi/__init__.py | 0 serverthrallapi/settings.py | 120 ++++++++++++++++++++++++++++++++++++ serverthrallapi/urls.py | 21 +++++++ serverthrallapi/wsgi.py | 16 +++++ 12 files changed, 189 insertions(+) create mode 100644 api/__init__.py create mode 100644 api/admin.py create mode 100644 api/apps.py create mode 100644 api/migrations/__init__.py create mode 100644 api/models.py create mode 100644 api/tests.py create mode 100644 api/views.py create mode 100644 manage.py create mode 100644 serverthrallapi/__init__.py create mode 100644 serverthrallapi/settings.py create mode 100644 serverthrallapi/urls.py create mode 100644 serverthrallapi/wsgi.py diff --git a/api/__init__.py b/api/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/api/admin.py b/api/admin.py new file mode 100644 index 0000000..8c38f3f --- /dev/null +++ b/api/admin.py @@ -0,0 +1,3 @@ +from django.contrib import admin + +# Register your models here. diff --git a/api/apps.py b/api/apps.py new file mode 100644 index 0000000..d87006d --- /dev/null +++ b/api/apps.py @@ -0,0 +1,5 @@ +from django.apps import AppConfig + + +class ApiConfig(AppConfig): + name = 'api' diff --git a/api/migrations/__init__.py b/api/migrations/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/api/models.py b/api/models.py new file mode 100644 index 0000000..71a8362 --- /dev/null +++ b/api/models.py @@ -0,0 +1,3 @@ +from django.db import models + +# Create your models here. diff --git a/api/tests.py b/api/tests.py new file mode 100644 index 0000000..7ce503c --- /dev/null +++ b/api/tests.py @@ -0,0 +1,3 @@ +from django.test import TestCase + +# Create your tests here. diff --git a/api/views.py b/api/views.py new file mode 100644 index 0000000..91ea44a --- /dev/null +++ b/api/views.py @@ -0,0 +1,3 @@ +from django.shortcuts import render + +# Create your views here. diff --git a/manage.py b/manage.py new file mode 100644 index 0000000..2a6ff7d --- /dev/null +++ b/manage.py @@ -0,0 +1,15 @@ +#!/usr/bin/env python +import os +import sys + +if __name__ == "__main__": + os.environ.setdefault("DJANGO_SETTINGS_MODULE", "serverthrallapi.settings") + try: + from django.core.management import execute_from_command_line + except ImportError as exc: + raise ImportError( + "Couldn't import Django. Are you sure it's installed and " + "available on your PYTHONPATH environment variable? Did you " + "forget to activate a virtual environment?" + ) from exc + execute_from_command_line(sys.argv) diff --git a/serverthrallapi/__init__.py b/serverthrallapi/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/serverthrallapi/settings.py b/serverthrallapi/settings.py new file mode 100644 index 0000000..a1f9a44 --- /dev/null +++ b/serverthrallapi/settings.py @@ -0,0 +1,120 @@ +""" +Django settings for serverthrallapi project. + +Generated by 'django-admin startproject' using Django 2.0.6. + +For more information on this file, see +https://docs.djangoproject.com/en/2.0/topics/settings/ + +For the full list of settings and their values, see +https://docs.djangoproject.com/en/2.0/ref/settings/ +""" + +import os + +# Build paths inside the project like this: os.path.join(BASE_DIR, ...) +BASE_DIR = os.path.dirname(os.path.dirname(os.path.abspath(__file__))) + + +# Quick-start development settings - unsuitable for production +# See https://docs.djangoproject.com/en/2.0/howto/deployment/checklist/ + +# SECURITY WARNING: keep the secret key used in production secret! +SECRET_KEY = 'ey55!hebc6#y$mv+m8cm2*^lf21z1j1nsxhoiaw$pkfk5lg^+5' + +# SECURITY WARNING: don't run with debug turned on in production! +DEBUG = True + +ALLOWED_HOSTS = [] + + +# Application definition + +INSTALLED_APPS = [ + 'django.contrib.admin', + 'django.contrib.auth', + 'django.contrib.contenttypes', + 'django.contrib.sessions', + 'django.contrib.messages', + 'django.contrib.staticfiles', +] + +MIDDLEWARE = [ + 'django.middleware.security.SecurityMiddleware', + 'django.contrib.sessions.middleware.SessionMiddleware', + 'django.middleware.common.CommonMiddleware', + 'django.middleware.csrf.CsrfViewMiddleware', + 'django.contrib.auth.middleware.AuthenticationMiddleware', + 'django.contrib.messages.middleware.MessageMiddleware', + 'django.middleware.clickjacking.XFrameOptionsMiddleware', +] + +ROOT_URLCONF = 'serverthrallapi.urls' + +TEMPLATES = [ + { + 'BACKEND': 'django.template.backends.django.DjangoTemplates', + 'DIRS': [], + 'APP_DIRS': True, + 'OPTIONS': { + 'context_processors': [ + 'django.template.context_processors.debug', + 'django.template.context_processors.request', + 'django.contrib.auth.context_processors.auth', + 'django.contrib.messages.context_processors.messages', + ], + }, + }, +] + +WSGI_APPLICATION = 'serverthrallapi.wsgi.application' + + +# Database +# https://docs.djangoproject.com/en/2.0/ref/settings/#databases + +DATABASES = { + 'default': { + 'ENGINE': 'django.db.backends.sqlite3', + 'NAME': os.path.join(BASE_DIR, 'db.sqlite3'), + } +} + + +# Password validation +# https://docs.djangoproject.com/en/2.0/ref/settings/#auth-password-validators + +AUTH_PASSWORD_VALIDATORS = [ + { + 'NAME': 'django.contrib.auth.password_validation.UserAttributeSimilarityValidator', + }, + { + 'NAME': 'django.contrib.auth.password_validation.MinimumLengthValidator', + }, + { + 'NAME': 'django.contrib.auth.password_validation.CommonPasswordValidator', + }, + { + 'NAME': 'django.contrib.auth.password_validation.NumericPasswordValidator', + }, +] + + +# Internationalization +# https://docs.djangoproject.com/en/2.0/topics/i18n/ + +LANGUAGE_CODE = 'en-us' + +TIME_ZONE = 'UTC' + +USE_I18N = True + +USE_L10N = True + +USE_TZ = True + + +# Static files (CSS, JavaScript, Images) +# https://docs.djangoproject.com/en/2.0/howto/static-files/ + +STATIC_URL = '/static/' diff --git a/serverthrallapi/urls.py b/serverthrallapi/urls.py new file mode 100644 index 0000000..5477080 --- /dev/null +++ b/serverthrallapi/urls.py @@ -0,0 +1,21 @@ +"""serverthrallapi URL Configuration + +The `urlpatterns` list routes URLs to views. For more information please see: + https://docs.djangoproject.com/en/2.0/topics/http/urls/ +Examples: +Function views + 1. Add an import: from my_app import views + 2. Add a URL to urlpatterns: path('', views.home, name='home') +Class-based views + 1. Add an import: from other_app.views import Home + 2. Add a URL to urlpatterns: path('', Home.as_view(), name='home') +Including another URLconf + 1. Import the include() function: from django.urls import include, path + 2. Add a URL to urlpatterns: path('blog/', include('blog.urls')) +""" +from django.contrib import admin +from django.urls import path + +urlpatterns = [ + path('admin/', admin.site.urls), +] diff --git a/serverthrallapi/wsgi.py b/serverthrallapi/wsgi.py new file mode 100644 index 0000000..21aa3e6 --- /dev/null +++ b/serverthrallapi/wsgi.py @@ -0,0 +1,16 @@ +""" +WSGI config for serverthrallapi project. + +It exposes the WSGI callable as a module-level variable named ``application``. + +For more information on this file, see +https://docs.djangoproject.com/en/2.0/howto/deployment/wsgi/ +""" + +import os + +from django.core.wsgi import get_wsgi_application + +os.environ.setdefault("DJANGO_SETTINGS_MODULE", "serverthrallapi.settings") + +application = get_wsgi_application() From 102d3f30c6331020ab21d1a3e9a2a767e4170df3 Mon Sep 17 00:00:00 2001 From: A-Kimpton Date: Tue, 19 Jun 2018 04:43:52 +0100 Subject: [PATCH 3/4] Test commit --- serverthrallapi/urls.py | 15 --------------- 1 file changed, 15 deletions(-) diff --git a/serverthrallapi/urls.py b/serverthrallapi/urls.py index 5477080..dfc7362 100644 --- a/serverthrallapi/urls.py +++ b/serverthrallapi/urls.py @@ -1,18 +1,3 @@ -"""serverthrallapi URL Configuration - -The `urlpatterns` list routes URLs to views. For more information please see: - https://docs.djangoproject.com/en/2.0/topics/http/urls/ -Examples: -Function views - 1. Add an import: from my_app import views - 2. Add a URL to urlpatterns: path('', views.home, name='home') -Class-based views - 1. Add an import: from other_app.views import Home - 2. Add a URL to urlpatterns: path('', Home.as_view(), name='home') -Including another URLconf - 1. Import the include() function: from django.urls import include, path - 2. Add a URL to urlpatterns: path('blog/', include('blog.urls')) -""" from django.contrib import admin from django.urls import path From 4ddecaaaff24b374e3fca17cc0974faa07a7bd29 Mon Sep 17 00:00:00 2001 From: A-Kimpton Date: Tue, 19 Jun 2018 05:59:58 +0100 Subject: [PATCH 4/4] Migrate 90% of the code Still got some work todo with celery, too tired to debug why it cant see the module! --- api/migrations/0001_initial.py | 102 +++++++++++++++ api/models.py | 3 - api/models/__init__.py | 5 + api/models/character.py | 30 +++++ api/models/characterhistory.py | 9 ++ api/models/charactermanager.py | 7 + api/models/clan.py | 14 ++ api/models/clanmanager.py | 30 +++++ api/models/server.py | 15 +++ api/models/servermanager.py | 17 +++ api/models/serversyncdata.py | 7 + api/plugins/__init__.py | 1 + api/plugins/ginfo/__init__.py | 3 + api/plugins/ginfo/firebase_pushid.py | 65 +++++++++ api/plugins/ginfo/ginfo.py | 153 ++++++++++++++++++++++ api/plugins/ginfo/ginfocharacter.py | 13 ++ api/serializers.py | 85 ++++++++++++ api/sync/__init__.py | 3 + api/sync/ginfo.py | 13 ++ api/sync/serverdata.py | 188 +++++++++++++++++++++++++++ api/tasks.py | 21 +++ api/urls.py | 20 +++ api/views.py | 3 - api/views/__init__.py | 10 ++ api/views/base.py | 21 +++ api/views/character.py | 28 ++++ api/views/characterhistory.py | 28 ++++ api/views/characters.py | 23 ++++ api/views/clan.py | 29 +++++ api/views/clancharacters.py | 28 ++++ api/views/clans.py | 26 ++++ api/views/server.py | 35 +++++ api/views/servers.py | 25 ++++ api/views/sync.py | 25 ++++ api/views/widgets/__init__.py | 3 + api/views/widgets/activeclans.py | 26 ++++ api/views/widgets/serverinfo.py | 16 +++ serverthrallapi/__init__.py | 1 + serverthrallapi/celery.py | 9 ++ serverthrallapi/settings.py | 14 ++ serverthrallapi/urls.py | 3 +- 41 files changed, 1150 insertions(+), 7 deletions(-) create mode 100644 api/migrations/0001_initial.py delete mode 100644 api/models.py create mode 100644 api/models/__init__.py create mode 100644 api/models/character.py create mode 100644 api/models/characterhistory.py create mode 100644 api/models/charactermanager.py create mode 100644 api/models/clan.py create mode 100644 api/models/clanmanager.py create mode 100644 api/models/server.py create mode 100644 api/models/servermanager.py create mode 100644 api/models/serversyncdata.py create mode 100644 api/plugins/__init__.py create mode 100644 api/plugins/ginfo/__init__.py create mode 100644 api/plugins/ginfo/firebase_pushid.py create mode 100644 api/plugins/ginfo/ginfo.py create mode 100644 api/plugins/ginfo/ginfocharacter.py create mode 100644 api/serializers.py create mode 100644 api/sync/__init__.py create mode 100644 api/sync/ginfo.py create mode 100644 api/sync/serverdata.py create mode 100644 api/tasks.py create mode 100644 api/urls.py delete mode 100644 api/views.py create mode 100644 api/views/__init__.py create mode 100644 api/views/base.py create mode 100644 api/views/character.py create mode 100644 api/views/characterhistory.py create mode 100644 api/views/characters.py create mode 100644 api/views/clan.py create mode 100644 api/views/clancharacters.py create mode 100644 api/views/clans.py create mode 100644 api/views/server.py create mode 100644 api/views/servers.py create mode 100644 api/views/sync.py create mode 100644 api/views/widgets/__init__.py create mode 100644 api/views/widgets/activeclans.py create mode 100644 api/views/widgets/serverinfo.py create mode 100644 serverthrallapi/celery.py diff --git a/api/migrations/0001_initial.py b/api/migrations/0001_initial.py new file mode 100644 index 0000000..ca451f9 --- /dev/null +++ b/api/migrations/0001_initial.py @@ -0,0 +1,102 @@ +# Generated by Django 2.0.6 on 2018-06-19 04:52 + +from django.db import migrations, models +import django.db.models.deletion + + +class Migration(migrations.Migration): + + initial = True + + dependencies = [ + ] + + operations = [ + migrations.CreateModel( + name='Character', + fields=[ + ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), + ('name', models.TextField()), + ('level', models.IntegerField()), + ('is_online', models.BooleanField()), + ('steam_id', models.TextField()), + ('conan_id', models.TextField()), + ('conan_clan_id', models.TextField(null=True)), + ('last_online', models.DateTimeField(null=True)), + ('last_killed_by', models.TextField(null=True)), + ('created', models.DateTimeField(auto_now_add=True)), + ('x', models.FloatField(null=True)), + ('y', models.FloatField(null=True)), + ('z', models.FloatField(null=True)), + ], + ), + migrations.CreateModel( + name='CharacterHistory', + fields=[ + ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), + ('created', models.DateTimeField(auto_now_add=True)), + ('x', models.FloatField()), + ('y', models.FloatField()), + ('z', models.FloatField()), + ('character', models.ForeignKey(on_delete=django.db.models.deletion.DO_NOTHING, related_name='history', to='api.Character')), + ], + ), + migrations.CreateModel( + name='Clan', + fields=[ + ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), + ('name', models.TextField()), + ('conan_id', models.TextField()), + ('conan_owner_id', models.TextField()), + ('motd', models.TextField()), + ('created', models.DateTimeField(auto_now_add=True)), + ('owner', models.ForeignKey(null=True, on_delete=django.db.models.deletion.DO_NOTHING, related_name='owner', to='api.Character')), + ], + ), + migrations.CreateModel( + name='GinfoCharacter', + fields=[ + ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), + ('ginfo_marker_uid', models.TextField()), + ('character', models.ForeignKey(on_delete=django.db.models.deletion.DO_NOTHING, related_name='ginfo_character', related_query_name='ginfo_character', to='api.Character')), + ], + ), + migrations.CreateModel( + name='Server', + fields=[ + ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), + ('name', models.TextField()), + ('ip_address', models.TextField(default='')), + ('version', models.TextField(null=True)), + ('query_port', models.TextField(null=True)), + ('max_players', models.IntegerField(null=True)), + ('tick_rate', models.IntegerField(null=True)), + ('private_secret', models.UUIDField()), + ('last_sync', models.DateTimeField(null=True)), + ], + ), + migrations.CreateModel( + name='ServerSyncData', + fields=[ + ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), + ('data', models.TextField()), + ('created', models.DateTimeField(auto_now_add=True)), + ('server', models.ForeignKey(on_delete=django.db.models.deletion.DO_NOTHING, related_name='+', to='api.Server')), + ], + ), + migrations.AddField( + model_name='clan', + name='server', + field=models.ForeignKey(on_delete=django.db.models.deletion.DO_NOTHING, related_name='clans', to='api.Server'), + ), + migrations.AddField( + model_name='character', + name='clan', + field=models.ForeignKey(null=True, on_delete=django.db.models.deletion.DO_NOTHING, related_name='members', to='api.Clan'), + ), + migrations.AddField( + model_name='character', + name='server', + field=models.ForeignKey(on_delete=django.db.models.deletion.DO_NOTHING, related_name='characters', to='api.Server'), + ), + ] diff --git a/api/models.py b/api/models.py deleted file mode 100644 index 71a8362..0000000 --- a/api/models.py +++ /dev/null @@ -1,3 +0,0 @@ -from django.db import models - -# Create your models here. diff --git a/api/models/__init__.py b/api/models/__init__.py new file mode 100644 index 0000000..7b0eb64 --- /dev/null +++ b/api/models/__init__.py @@ -0,0 +1,5 @@ +from .character import Character +from .characterhistory import CharacterHistory +from .server import Server +from .clan import Clan +from .serversyncdata import ServerSyncData diff --git a/api/models/character.py b/api/models/character.py new file mode 100644 index 0000000..67c0bc2 --- /dev/null +++ b/api/models/character.py @@ -0,0 +1,30 @@ +from django.db import models +from .charactermanager import CharacterManager + + +class Character(models.Model): + name = models.TextField() + level = models.IntegerField() + is_online = models.BooleanField() + steam_id = models.TextField() + conan_id = models.TextField() + conan_clan_id = models.TextField(null=True) + last_online = models.DateTimeField(null=True) + last_killed_by = models.TextField(null=True) + created = models.DateTimeField(auto_now_add=True) + clan = models.ForeignKey('api.Clan', related_name='members', null=True, on_delete=models.DO_NOTHING) + server = models.ForeignKey('api.Server', related_name='characters', on_delete=models.DO_NOTHING) + x = models.FloatField(null=True) + y = models.FloatField(null=True) + z = models.FloatField(null=True) + + objects = CharacterManager.as_manager() + + def generate_history(self, created): + from .characterhistory import CharacterHistory + return CharacterHistory( + character=self, + created=created, + x=self.x, + y=self.y, + z=self.z) diff --git a/api/models/characterhistory.py b/api/models/characterhistory.py new file mode 100644 index 0000000..3e3c6e9 --- /dev/null +++ b/api/models/characterhistory.py @@ -0,0 +1,9 @@ +from django.db import models + + +class CharacterHistory(models.Model): + created = models.DateTimeField(auto_now_add=True) + character = models.ForeignKey('api.Character', related_name='history', on_delete=models.DO_NOTHING) + x = models.FloatField() + y = models.FloatField() + z = models.FloatField() diff --git a/api/models/charactermanager.py b/api/models/charactermanager.py new file mode 100644 index 0000000..1a6fb57 --- /dev/null +++ b/api/models/charactermanager.py @@ -0,0 +1,7 @@ +from django.db import models +from datetime import datetime, timedelta + +class CharacterManager(models.QuerySet): + + def active_only(self): + return self.filter(last_online__gt=datetime.now() - timedelta(days=7)) diff --git a/api/models/clan.py b/api/models/clan.py new file mode 100644 index 0000000..51d4639 --- /dev/null +++ b/api/models/clan.py @@ -0,0 +1,14 @@ +from django.db import models +from .clanmanager import ClanManager + + +class Clan(models.Model): + name = models.TextField() + conan_id = models.TextField() + conan_owner_id = models.TextField() + motd = models.TextField() + created = models.DateTimeField(auto_now_add=True) + owner = models.ForeignKey('api.Character', related_name='owner', null=True, on_delete=models.DO_NOTHING) + server = models.ForeignKey('api.Server', related_name='clans', on_delete=models.DO_NOTHING) + + objects = ClanManager.as_manager() diff --git a/api/models/clanmanager.py b/api/models/clanmanager.py new file mode 100644 index 0000000..4990c2e --- /dev/null +++ b/api/models/clanmanager.py @@ -0,0 +1,30 @@ +from django.db import models +from django.db.models import Count, Max, OuterRef, Subquery +from datetime import datetime, timedelta + + +class ClanManager(models.QuerySet): + + def with_character_count(self): + return self.annotate(character_count=Count('members')) + + def with_last_logged_in(self): + return self.annotate(last_logged_in=Max('members__last_online')) + + def with_active_count(self): + from api.models import Character + + active_query = (Character.objects + .active_only() + .filter(clan=OuterRef('id')) + .values('clan_id') + .order_by() + .annotate(count=Count('*')) + .values('count')[:1]) + + return self.annotate(active_count=Subquery( + active_query, output_field=models.IntegerField())) + + def active_only(self): + return (self.with_last_logged_in().filter( + last_logged_in__gt=datetime.now() - timedelta(days=7))) diff --git a/api/models/server.py b/api/models/server.py new file mode 100644 index 0000000..952770a --- /dev/null +++ b/api/models/server.py @@ -0,0 +1,15 @@ +from django.db import models +from .servermanager import ServerManager + + +class Server(models.Model): + name = models.TextField() + ip_address = models.TextField(default='') + version = models.TextField(null=True) + query_port = models.TextField(null=True) + max_players = models.IntegerField(null=True) + tick_rate = models.IntegerField(null=True) + private_secret = models.UUIDField() + last_sync = models.DateTimeField(null=True) + + objects = ServerManager.as_manager() diff --git a/api/models/servermanager.py b/api/models/servermanager.py new file mode 100644 index 0000000..af4fe97 --- /dev/null +++ b/api/models/servermanager.py @@ -0,0 +1,17 @@ +from django.db import models +from django.utils import timezone +from datetime import timedelta +from django.db.models import Count + + +class ServerManager(models.QuerySet): + + def only_active(self): + active_threshold = timezone.now() - timedelta(days=2) + + return self.filter( + last_sync__gt=active_threshold, + last_sync__isnull=False) + + def with_character_count(self): + return self.annotate(character_count=Count('characters')) diff --git a/api/models/serversyncdata.py b/api/models/serversyncdata.py new file mode 100644 index 0000000..1643b0d --- /dev/null +++ b/api/models/serversyncdata.py @@ -0,0 +1,7 @@ +from django.db import models + + +class ServerSyncData(models.Model): + server = models.ForeignKey('api.Server', related_name='+', on_delete=models.DO_NOTHING) + data = models.TextField() + created = models.DateTimeField(auto_now_add=True) diff --git a/api/plugins/__init__.py b/api/plugins/__init__.py new file mode 100644 index 0000000..9c0fa90 --- /dev/null +++ b/api/plugins/__init__.py @@ -0,0 +1 @@ +# flake8: noqa diff --git a/api/plugins/ginfo/__init__.py b/api/plugins/ginfo/__init__.py new file mode 100644 index 0000000..dad7e36 --- /dev/null +++ b/api/plugins/ginfo/__init__.py @@ -0,0 +1,3 @@ +# flake8: noqa +from .ginfo import GinfoPlugin +from .ginfocharacter import GinfoCharacter diff --git a/api/plugins/ginfo/firebase_pushid.py b/api/plugins/ginfo/firebase_pushid.py new file mode 100644 index 0000000..7e4f0a6 --- /dev/null +++ b/api/plugins/ginfo/firebase_pushid.py @@ -0,0 +1,65 @@ +# This is taken from +# https://gist.github.com/risent/4cab3878d995bec7d1c2 +# which is a Python port of the official JS code by Firebase +# https://gist.github.com/mikelehen/3596a30bd69384624c11 +import random +import time +#from exceptions import ValueError + + +""" + Generates PushID for Firebase +""" +class PushID(object): + # Modeled after base64 web-safe chars, but ordered by ASCII. + PUSH_CHARS = ('-0123456789' + 'ABCDEFGHIJKLMNOPQRSTUVWXYZ' + '_abcdefghijklmnopqrstuvwxyz') + + def __init__(self): + + # Timestamp of last push, used to prevent local collisions if you + # pushtwice in one ms. + self.lastPushTime = 0 + + # We generate 72-bits of randomness which get turned into 12 + # characters and appended to the timestamp to prevent + # collisions with other clients. We store the last characters + # we generated because in the event of a collision, we'll use + # those same characters except "incremented" by one. + self.lastRandChars = [0] * 12 + + def next_id(self): + now = int(time.time() * 1000) + duplicateTime = (now == self.lastPushTime) + self.lastPushTime = now + timeStampChars = [''] * 8 + + for i in range(7, -1, -1): + timeStampChars[i] = self.PUSH_CHARS[now % 64] + now = int(now / 64) + + if (now != 0): + raise ValueError('We should have converted the entire timestamp.') + + uid = ''.join(timeStampChars) + + if not duplicateTime: + for i in range(12): + self.lastRandChars[i] = int(random.random() * 64) + else: + # If the timestamp hasn't changed since last push, use the + # same random number, except incremented by 1. + for i in range(11, -1, -1): + if self.lastRandChars[i] == 63: + self.lastRandChars[i] = 0 + else: + break + self.lastRandChars[i] += 1 + + for i in range(12): + uid += self.PUSH_CHARS[self.lastRandChars[i]] + + if len(uid) != 20: + raise ValueError('Length should be 20.') + return uid diff --git a/api/plugins/ginfo/ginfo.py b/api/plugins/ginfo/ginfo.py new file mode 100644 index 0000000..c074e9d --- /dev/null +++ b/api/plugins/ginfo/ginfo.py @@ -0,0 +1,153 @@ +import math + +import requests + +from .firebase_pushid import PushID +from .ginfocharacter import GinfoCharacter + + +class GinfoPlugin(object): + """ + GinfoPlugin provides methods to post a position to the ginfo firebase database + """ + API_URL = 'https://ginfo-34baa.firebaseio.com' + GAME = 'conanexiles' + MAP = 'conanexiles_desert' + SERVERTHRALL_USER_UID = 'y67Efa3kddSuhb26UoUapIJakMh1' + + def __init__(self): + self.uid_generator = PushID() + + def marker_path(self, group, marker): + """ + Returns the path to which a marker has to be posted in the firebase database + + @param group: UID of the ginfo group for which the marker should be posted + @param marker: UID of the marker that should be posted + """ + return 'games/%s/groups/%s/maps/%s/markers/%s' % (self.GAME, group, self.MAP, marker) + + def access_token_path(self, group, marker): + """ + Returns the path to which the groups access token has to be posted in the firebase database + + @param group: UID of the ginfo group for which the marker should be posted + @param marker: UID of the marker that should be posted + """ + return 'groupAccessTokenVerifier/%s/%s' % (group, marker) + + TRANSFORMATION_KX = 50.60049940473328 + TRANSFORMATION_KY = -50.83608443474857 + TRANSFORMATION_DX = -2644742.2783469525 + TRANSFORMATION_DY = -2647361.614800021 + + def linear_transform(self, x, y): + """ + Applies a linear transform to the point x/y + """ + return ( + x * self.TRANSFORMATION_KX + self.TRANSFORMATION_DX, + y * self.TRANSFORMATION_KY + self.TRANSFORMATION_DY + ) + + def convert_position(self, x, y): + """ + Converts the given position from x/y to lat/lng coordinates + @param x, y: x/y Position of in the game + @return: lat/lng for the same position on the ginfo map + """ + (x, y) = self.linear_transform(x, y) + return SphericalMercator.unproject(x, y) + + DEFAULT_MARKER_COLOR = "#1D8BF1" + DEFAULT_MARKER_TYPE = 1 + DEFAULT_MARKER_SKIN = 0 + + def update_position(self, character, group, access_token): + """ + Posts the position of this character to Firebase + @param character: + The character for which the position should be updated + @param group: + The group in which the marker should be postet + @param access_token: + Access token for the ginfo group + """ + if character.x is None or character.y is None: + return + + (lat, lng) = self.convert_position( + float(character.x), float(character.y)) + + ginfo_character, created = GinfoCharacter.objects.get_or_create( + character_id=character.id) + if created: + ginfo_character.ginfo_marker_uid = self.uid_generator.next_id() + ginfo_character.save() + + data = { + self.marker_path(group, ginfo_character.ginfo_marker_uid): { + "name": character.name, + "updatedAt": { + # This tells firebase to use its internal timestamp + ".sv": "timestamp" + }, + "createdAt": { + # This tells firebase to use its internal timestamp + ".sv": "timestamp" + }, + "color": self.DEFAULT_MARKER_COLOR, + "creatorId": self.SERVERTHRALL_USER_UID, + "position": { + "lat": lat, + "lng": lng + }, + "type": self.DEFAULT_MARKER_TYPE, + "skin": self.DEFAULT_MARKER_SKIN, + }, + self.access_token_path(group, ginfo_character.ginfo_marker_uid): access_token + } + + response = requests.patch( + url=self.API_URL + '/.json', + json=data + ) + try: + json_data = response.json() + if "error" in json_data: + # TODO: Use proper logger + print("Error from Ginfo Firebase: " + json_data["error"]) + except: + pass + + +class SphericalMercator(object): + R = 6378137 + MAX_LATITUDE = 85.0511287798 + + @staticmethod + def project(lat, lng): + """ + Converts a lat/lng coordiante to a x/y point + """ + d = math.pi / 180.0 + lat = max( + min(SphericalMercator.MAX_LATITUDE, lat), - + SphericalMercator.MAX_LATITUDE + ) + sin = math.sin(lat * d) + return ( + SphericalMercator.R * lng * d, + SphericalMercator.R * math.log((1 + sin) / (1 - sin)) / 2.0 + ) + + @staticmethod + def unproject(x, y): + """ + Converts a x/y point to a lat/lng coordinate + """ + d = 180.0 / math.pi + return ( + (2.0 * math.atan(math.exp(y / SphericalMercator.R)) - (math.pi / 2.0)) * d, + x * d / SphericalMercator.R + ) diff --git a/api/plugins/ginfo/ginfocharacter.py b/api/plugins/ginfo/ginfocharacter.py new file mode 100644 index 0000000..06fb318 --- /dev/null +++ b/api/plugins/ginfo/ginfocharacter.py @@ -0,0 +1,13 @@ +from django.db import models + +from api.models import Character + + +class GinfoCharacter(models.Model): + character = models.ForeignKey( + Character, + related_name="ginfo_character", + related_query_name="ginfo_character", + on_delete=models.DO_NOTHING + ) + ginfo_marker_uid = models.TextField() diff --git a/api/serializers.py b/api/serializers.py new file mode 100644 index 0000000..26aeccc --- /dev/null +++ b/api/serializers.py @@ -0,0 +1,85 @@ +import calendar +from api.models import Character + +import serpy + + +class EnumField(serpy.Field): + + def to_value(self, v): + return v.name + + +class DatetimeToUnixField(serpy.Field): + + def to_value(self, v): + if v is None: + return None + return calendar.timegm(v.utctimetuple()) + +class CharacterLocationField(serpy.Field): + + getter_takes_serializer = True + + def as_getter(self, serializer_field_name, serializer_cls): + return lambda self, v: {'x': v.x, 'y': v.y, 'z': v.z} + + +class ClanSerializer(serpy.Serializer): + id = serpy.Field() + server_id = serpy.Field() + name = serpy.Field() + motd = serpy.Field() + owner_id = serpy.Field() + owner_name = serpy.MethodField() + character_count = serpy.Field() + created = DatetimeToUnixField() + active_count = serpy.MethodField() + + def get_active_count(self, item): + return item.active_count if item.active_count else 0 + + def get_owner_name(self, item): + return item.owner.name if item.owner is not None else None + + +class CharacterSerializer(serpy.Serializer): + id = serpy.Field() + server_id = serpy.Field() + steam_id = serpy.Field() + clan_id = serpy.Field() + name = serpy.Field() + level = serpy.Field() + is_online = serpy.Field() + clan_name = serpy.MethodField() + created = DatetimeToUnixField() + last_online = DatetimeToUnixField() + last_killed_by = serpy.Field() + + def get_clan_name(self, item): + return item.clan.name if item.clan is not None else None + + +class CharacterHistorySerializer(serpy.Serializer): + character_id = serpy.Field() + created = DatetimeToUnixField() + + +class ServerSerializer(serpy.Serializer): + id = serpy.Field() + name = serpy.Field() + character_count = serpy.Field() + online_count = serpy.MethodField() + ip_address = serpy.Field() + query_port = serpy.Field() + tick_rate = serpy.Field() + max_players = serpy.Field() + version = serpy.Field() + last_sync = DatetimeToUnixField() + + def get_online_count(self, server): + return Character.objects.filter(server=server, is_online=True).count() + + +class ServerAdminSerializer(ServerSerializer): + private_secret = serpy.Field() diff --git a/api/sync/__init__.py b/api/sync/__init__.py new file mode 100644 index 0000000..cd9f9fd --- /dev/null +++ b/api/sync/__init__.py @@ -0,0 +1,3 @@ +# flake8: noqa +from .ginfo import sync_ginfo +from .serverdata import sync_server_data diff --git a/api/sync/ginfo.py b/api/sync/ginfo.py new file mode 100644 index 0000000..3ba8fd0 --- /dev/null +++ b/api/sync/ginfo.py @@ -0,0 +1,13 @@ +from api.models import Character +from api.plugins.ginfo import GinfoPlugin + + +def sync_ginfo(character_ids, group_uid, access_token): + characters = (Character.objects + .filter(id__in=character_ids) + .order_by('id')) + + plugin = GinfoPlugin() + + for character in characters: + plugin.update_position(character, group_uid, access_token) diff --git a/api/sync/serverdata.py b/api/sync/serverdata.py new file mode 100644 index 0000000..03ff398 --- /dev/null +++ b/api/sync/serverdata.py @@ -0,0 +1,188 @@ +from api.models import Clan, Character, CharacterHistory, ServerSyncData +from datetime import datetime +from django.conf import settings +from django.db import transaction +from django.db.models import Q +from django.utils import timezone +import json +import pytz + + +def sync_characters(server, data): + now = timezone.now() + history_buffer = [] + changed_ids = [] + + # Delete removed characters + sent_ids = [c['conan_id'] for c in data] + (Character.objects + .filter(server=server) + .filter(~Q(conan_id__in=sent_ids)) + .delete()) + + character_map = {} + + for sync_data in data: + + character = (Character.objects + .filter(server=server) + .filter(conan_id=sync_data['conan_id']) + .first()) + + if character is None: + character = Character() + + has_moved = ( + (sync_data['x'] == character.x) or + (sync_data['y'] == character.y) or + (sync_data['z'] == character.y)) + + last_online = (datetime + .utcfromtimestamp(sync_data['last_online']) + .replace(tzinfo=pytz.utc)) + + character.server = server + character.clan_id = None + character.conan_id = sync_data['conan_id'] + character.conan_clan_id = sync_data.get('clan_id') + character.name = sync_data['name'] + character.level = sync_data['level'] + character.is_online = sync_data['is_online'] + character.steam_id = sync_data['steam_id'] + character.last_killed_by = sync_data['last_killed_by'] + character.x = sync_data['x'] + character.y = sync_data['y'] + character.z = sync_data['z'] + character.last_online = last_online + character.save() + + character_map[character.conan_id] = character + + if has_moved: + changed_ids.append(character.id) + history_buffer.append(character.generate_history(now)) + + if settings.ST_ENABLE_HISTORY: + CharacterHistory.objects.bulk_create(history_buffer) + + return character_map, changed_ids + + +def sync_clans(server, data): + # Delete clans we werent sent + sent_ids = [c['id'] for c in data] + (Clan.objects + .filter(server=server) + .filter(~Q(conan_id__in=sent_ids)) + .delete()) + + clan_map = {} + + for sync_data in data: + clan = (Clan.objects + .filter( + server=server, + conan_id=sync_data['id']) + .first()) + + if clan is None: + clan = Clan() + + clan.server = server + clan.owner_id = None + clan.conan_id = sync_data['id'] + clan.conan_owner_id = sync_data['owner_id'] + clan.name = sync_data['name'] + clan.motd = sync_data['motd'] + clan.save() + + clan_map[clan.conan_id] = clan + + return clan_map + + +def fix_primary_keys(character_map, clan_map): + for character in character_map.values(): + if character.conan_clan_id in clan_map: + character.clan = clan_map[character.conan_clan_id] + character.save() + + for clan in clan_map.values(): + if clan.conan_owner_id in character_map: + clan.owner = character_map[clan.conan_owner_id] + clan.save() + + +def get_int(data, key, default=None): + if key not in data: + return default + try: + return int(data[key]) + except ValueError: + return default + except TypeError: + return default + + +def remove_outer_quotes(value): + if value.startswith('"'): + value = value[1:] + if value.endswith('"'): + value = value[:len(value) - 1] + return value + + +def sync_server_data(sync_data_id, request_get_params): + sync_data = (ServerSyncData.objects + .filter(id=sync_data_id) + .select_related('server') + .first()) + + if sync_data is None: + print('Dropping sync request because data is invalid') + return + + sync_data.delete() + server = sync_data.server + + if server.last_sync and server.last_sync >= sync_data.created: + print("Dropping stale sync request") + return + + data = json.loads(sync_data.data) + changed_character_ids = [] + + with transaction.atomic(): + if 'version' in data: + server.version = data['version'] + + if 'server' in data: + server.name = remove_outer_quotes(data['server'].get('name', '')) + server.ip_address = data['server'].get('ip_address', '') + server.query_port = get_int(data['server'], 'query_port') + server.max_players = get_int(data['server'], 'max_players') + server.tick_rate = get_int(data['server'], 'tick_rate') + + if 'characters' in data: + character_map, changed_character_ids = sync_characters(server, data['characters']) + + if 'clans' in data: + clan_map = sync_clans(server, data['clans']) + + fix_primary_keys(character_map, clan_map) + + server.last_sync = timezone.now() + server.save() + + has_ginfo = ( + 'ginfo_group_uid' in request_get_params and + 'ginfo_access_token' in request_get_params) + + if has_ginfo: + from api.tasks import sync_ginfo_task + sync_ginfo_task.delay(changed_character_ids, + request_get_params['ginfo_group_uid'], + request_get_params['ginfo_access_token']) + + from api.tasks import delete_old_history_task + delete_old_history_task.delay() diff --git a/api/tasks.py b/api/tasks.py new file mode 100644 index 0000000..0152f37 --- /dev/null +++ b/api/tasks.py @@ -0,0 +1,21 @@ +from .sync import sync_server_data, sync_ginfo +from api.models import CharacterHistory +from datetime import timedelta +from django.utils import timezone +from serverthrallapi.celery import app + + +@app.task() +def sync_ginfo_task(character_ids, group_uid, access_token): + sync_ginfo(character_ids, group_uid, access_token) + + +@app.task() +def sync_server_data_task(sync_data_id, request_get_params): + sync_server_data(sync_data_id, request_get_params) + + +@app.task() +def delete_old_history_task(): + history_threshold = timezone.now() - timedelta(days=5) + CharacterHistory.objects.filter(created__lt=history_threshold).delete() diff --git a/api/urls.py b/api/urls.py new file mode 100644 index 0000000..abfc1bf --- /dev/null +++ b/api/urls.py @@ -0,0 +1,20 @@ +from django.urls import path + +from .views import (CharacterHistoryView, CharactersView, CharacterView, + ClansView, ClanView, ServersView, ServerView, + SyncCharactersView, ClanCharactersView, ActiveClansView, + ServerInfoView) + +urlpatterns = [ + path(r'', ServersView.as_view()), + path(r'widgets/serverinfo/', ServerInfoView.as_view()), + path(r'/', ServerView.as_view()), + path(r'/sync/characters/', SyncCharactersView.as_view()), + path(r'/clans/', ClansView.as_view()), + path(r'/clans//', ClanView.as_view()), + path(r'/clans//characters/', ClanCharactersView.as_view()), + path(r'/characters/', CharactersView.as_view()), + path(r'/characters//', CharacterView.as_view()), + path(r'/characters//history/', CharacterHistoryView.as_view()), + path(r'/widgets/activeclans/', ActiveClansView.as_view()), +] diff --git a/api/views.py b/api/views.py deleted file mode 100644 index 91ea44a..0000000 --- a/api/views.py +++ /dev/null @@ -1,3 +0,0 @@ -from django.shortcuts import render - -# Create your views here. diff --git a/api/views/__init__.py b/api/views/__init__.py new file mode 100644 index 0000000..7574fe6 --- /dev/null +++ b/api/views/__init__.py @@ -0,0 +1,10 @@ +from .character import CharacterView +from .characterhistory import CharacterHistoryView +from .characters import CharactersView +from .clan import ClanView +from .clans import ClansView +from .server import ServerView +from .servers import ServersView +from .sync import SyncCharactersView +from .clancharacters import ClanCharactersView +from .widgets import * diff --git a/api/views/base.py b/api/views/base.py new file mode 100644 index 0000000..7b8d487 --- /dev/null +++ b/api/views/base.py @@ -0,0 +1,21 @@ +from django.views import View + +from api.models import Server + + +class BaseView(View): + + def get_servers(self): + return (Server.objects + .with_character_count()) + + def get_server_public(self, request, server_id): + return (self.get_servers() + .filter(id=server_id) + .first()) + + def get_server_private(self, request, server_id): + return (self.get_servers() + .filter(id=server_id) + .filter(private_secret=request.GET.get('private_secret', None)) + .first()) diff --git a/api/views/character.py b/api/views/character.py new file mode 100644 index 0000000..5e46b91 --- /dev/null +++ b/api/views/character.py @@ -0,0 +1,28 @@ +from django.http import HttpResponse, JsonResponse + +from api.models import Character +from api.serializers import CharacterSerializer + +from .base import BaseView + + +class CharacterView(BaseView): + + def get(self, request, server_id, character_id): + server = self.get_server_public(request, server_id) + + if server is None: + return HttpResponse('server does not exist', status=400) + + character = (Character.objects + .filter( + server_id=server.id, + id=character_id) + .select_related('clan') + .first()) + + if character is None: + return HttpResponse('character does not exist', status=400) + + serialized = CharacterSerializer(character).data + return JsonResponse(serialized, status=200) diff --git a/api/views/characterhistory.py b/api/views/characterhistory.py new file mode 100644 index 0000000..1043497 --- /dev/null +++ b/api/views/characterhistory.py @@ -0,0 +1,28 @@ +from django.http import HttpResponse, JsonResponse + +from api.models import Character, CharacterHistory +from api.serializers import CharacterHistorySerializer + +from .base import BaseView + + +class CharacterHistoryView(BaseView): + + def get(self, request, server_id, character_id): + server = self.get_server_public(request, server_id) + + if server is None: + return HttpResponse('server does not exist', status=400) + + character = (Character.objects + .filter( + server_id=server.id, + id=character_id) + .first()) + + if character is None: + return HttpResponse('character does not exist', status=400) + + histories = CharacterHistory.objects.filter(character_id=character.id) + serialized = CharacterHistorySerializer(histories, many=True).data + return JsonResponse({'history': serialized}, status=200) diff --git a/api/views/characters.py b/api/views/characters.py new file mode 100644 index 0000000..7e8d1e2 --- /dev/null +++ b/api/views/characters.py @@ -0,0 +1,23 @@ +from django.http import HttpResponse, JsonResponse + +from api.models import Character +from api.serializers import CharacterSerializer + +from .base import BaseView + + +class CharactersView(BaseView): + + def get(self, request, server_id): + server = self.get_server_public(request, server_id) + + if server is None: + return HttpResponse('server does not exist', status=400) + + characters = (Character.objects + .filter(server_id=server.id) + .select_related('clan') + .all()) + + serialized = CharacterSerializer(characters, many=True).data + return JsonResponse(serialized, status=200, safe=False) diff --git a/api/views/clan.py b/api/views/clan.py new file mode 100644 index 0000000..3c8e7ac --- /dev/null +++ b/api/views/clan.py @@ -0,0 +1,29 @@ +from django.http import HttpResponse, JsonResponse + +from api.models import Clan +from api.serializers import ClanSerializer + +from .base import BaseView + + +class ClanView(BaseView): + + def get(self, request, server_id, clan_id): + server = self.get_server_public(request, server_id) + + if server is None: + return HttpResponse('server does not exist', status=400) + + clan = (Clan.objects + .filter( + server_id=server.id, + id=clan_id) + .with_character_count() + .with_active_count() + .first()) + + if clan is None: + return HttpResponse('clan does not exist', status=400) + + serialized = ClanSerializer(clan).data + return JsonResponse(serialized, status=200) diff --git a/api/views/clancharacters.py b/api/views/clancharacters.py new file mode 100644 index 0000000..d6794dc --- /dev/null +++ b/api/views/clancharacters.py @@ -0,0 +1,28 @@ +from django.http import HttpResponse, JsonResponse + +from api.models import Character, Clan +from api.serializers import CharacterSerializer + +from .base import BaseView + + +class ClanCharactersView(BaseView): + + def get(self, request, server_id, clan_id): + server = self.get_server_public(request, server_id) + + if server is None: + return HttpResponse('server does not exist', status=400) + + clan = Clan.objects.filter(server_id=server.id, id=clan_id).first() + + if clan is None: + return HttpResponse('clan does not exist', status=400) + + characters = (Character.objects + .filter(server_id=server.id, clan_id=clan_id) + .select_related('clan') + .all()) + + serialized = CharacterSerializer(characters, many=True).data + return JsonResponse(serialized, status=200, safe=False) diff --git a/api/views/clans.py b/api/views/clans.py new file mode 100644 index 0000000..6b19a8a --- /dev/null +++ b/api/views/clans.py @@ -0,0 +1,26 @@ +from django.http import HttpResponse, JsonResponse + +from api.models import Clan +from api.serializers import ClanSerializer + +from .base import BaseView + + +class ClansView(BaseView): + + def get(self, request, server_id): + server = self.get_server_public(request, server_id) + + if server is None: + return HttpResponse('server does not exist', status=400) + + clans = (Clan.objects + .filter(server_id=server.id) + .select_related('owner') + .with_character_count() + .with_active_count() + .filter(active_count__gt=0) + .all()) + + serialized = ClanSerializer(clans, many=True).data + return JsonResponse(serialized, status=200, safe=False) diff --git a/api/views/server.py b/api/views/server.py new file mode 100644 index 0000000..a878170 --- /dev/null +++ b/api/views/server.py @@ -0,0 +1,35 @@ +from django.http import HttpResponse, JsonResponse + +from api.serializers import ServerSerializer, ServerAdminSerializer + +from .base import BaseView + + +class ServerView(BaseView): + + def get(self, request, server_id): + server = self.get_server_public(request, server_id) + + if server is None: + return HttpResponse('server does not exist', status=404) + + serialized = ServerSerializer(server).data + return JsonResponse(serialized, status=200) + + def post(self, request, server_id): + if 'private_secret' not in request.GET: + return HttpResponse('missing required param private_secret') + + data = request.GET + server = self.get_server_private(request, server_id) + + if server is None: + return HttpResponse('server does not exist', status=404) + + if 'name' in data: + server.name = data['name'] + + server.save() + + serialized = ServerAdminSerializer(server).data + return JsonResponse(serialized, status=200) diff --git a/api/views/servers.py b/api/views/servers.py new file mode 100644 index 0000000..35a0d91 --- /dev/null +++ b/api/views/servers.py @@ -0,0 +1,25 @@ +from uuid import uuid1 + +from django.http import JsonResponse + +from api.models import Server +from api.serializers import ServerAdminSerializer, ServerSerializer + +from .base import BaseView + + +class ServersView(BaseView): + + def get(self, request): + servers = self.get_servers().only_active() + serialized = ServerSerializer(servers, many=True).data + return JsonResponse({'items': serialized}) + + def post(self, request): + server = Server() + server.private_secret = uuid1() + server.save() + + server = self.get_server_public(request, server.id) + serialized = ServerAdminSerializer(server).data + return JsonResponse(serialized, status=200) diff --git a/api/views/sync.py b/api/views/sync.py new file mode 100644 index 0000000..aa8dd0b --- /dev/null +++ b/api/views/sync.py @@ -0,0 +1,25 @@ +from django.http import HttpResponse + +from api.tasks import sync_server_data_task +from api.models import ServerSyncData + +from .base import BaseView + + +class SyncCharactersView(BaseView): + + def post(self, request, server_id): + if 'private_secret' not in request.GET: + return HttpResponse('missing required param private_secret', status=400) + + server = self.get_server_private(request, server_id) + + if server is None: + return HttpResponse('server does not exist', status=404) + + sync_data = ServerSyncData.objects.create( + server=server, + data=request.body) + + sync_server_data_task.delay(sync_data.id, request.GET) + return HttpResponse(status=200) diff --git a/api/views/widgets/__init__.py b/api/views/widgets/__init__.py new file mode 100644 index 0000000..272c7cd --- /dev/null +++ b/api/views/widgets/__init__.py @@ -0,0 +1,3 @@ +# flake8: noqa +from .activeclans import ActiveClansView +from .serverinfo import ServerInfoView diff --git a/api/views/widgets/activeclans.py b/api/views/widgets/activeclans.py new file mode 100644 index 0000000..8039a0d --- /dev/null +++ b/api/views/widgets/activeclans.py @@ -0,0 +1,26 @@ +from django.http import HttpResponse, JsonResponse + +from api.models import Clan +from api.serializers import ClanSerializer + +from ..base import BaseView + + +class ActiveClansView(BaseView): + + def get(self, request, server_id): + server = self.get_server_public(request, server_id) + + if server is None: + return HttpResponse('server does not exist', status=400) + + clans = (Clan.objects + .filter(server_id=server.id) + .select_related('owner') + .with_character_count() + .with_active_count() + .active_only() + .order_by('-active_count')) + + serialized = ClanSerializer(clans, many=True).data + return JsonResponse(serialized, status=200, safe=False) diff --git a/api/views/widgets/serverinfo.py b/api/views/widgets/serverinfo.py new file mode 100644 index 0000000..c8270b4 --- /dev/null +++ b/api/views/widgets/serverinfo.py @@ -0,0 +1,16 @@ +from django.http import JsonResponse + +from api.models import Server, Character, Clan + +from ..base import BaseView + + +class ServerInfoView(BaseView): + + def get(self, request): + data = { + 'server_count': Server.objects.count(), + 'character_count': Character.objects.count(), + 'clan_count': Clan.objects.count() + } + return JsonResponse(data, status=200, safe=False) diff --git a/serverthrallapi/__init__.py b/serverthrallapi/__init__.py index e69de29..cf2e85f 100644 --- a/serverthrallapi/__init__.py +++ b/serverthrallapi/__init__.py @@ -0,0 +1 @@ +from .celery import app as celery_app diff --git a/serverthrallapi/celery.py b/serverthrallapi/celery.py new file mode 100644 index 0000000..f6b5cdf --- /dev/null +++ b/serverthrallapi/celery.py @@ -0,0 +1,9 @@ +from __future__ import absolute_import, unicode_literals +import os +from celery import Celery + +os.environ.setdefault('DJANGO_SETTINGS_MODULE', 'serverthrallapi.settings') + +app = Celery('proj') +app.config_from_object('django.conf:settings', namespace='CELERY') +app.autodiscover_tasks() diff --git a/serverthrallapi/settings.py b/serverthrallapi/settings.py index a1f9a44..8b30955 100644 --- a/serverthrallapi/settings.py +++ b/serverthrallapi/settings.py @@ -37,8 +37,13 @@ 'django.contrib.sessions', 'django.contrib.messages', 'django.contrib.staticfiles', + 'django_celery_beat', + 'corsheaders', + 'api', ] +CORS_ORIGIN_ALLOW_ALL = True + MIDDLEWARE = [ 'django.middleware.security.SecurityMiddleware', 'django.contrib.sessions.middleware.SessionMiddleware', @@ -47,6 +52,7 @@ 'django.contrib.auth.middleware.AuthenticationMiddleware', 'django.contrib.messages.middleware.MessageMiddleware', 'django.middleware.clickjacking.XFrameOptionsMiddleware', + 'corsheaders.middleware.CorsMiddleware', ] ROOT_URLCONF = 'serverthrallapi.urls' @@ -118,3 +124,11 @@ # https://docs.djangoproject.com/en/2.0/howto/static-files/ STATIC_URL = '/static/' + +# Celery +CELERY_TASK_ALWAYS_EAGER = DEBUG +CELERY_TASK_SERIALIZER = 'json' +CELERY_BROKER_URL = os.environ.get('RABBITMQ_BIGWIG_URL') + + +ST_ENABLE_HISTORY = os.environ.get('ST_ENABLE_HISTORY', 'false').lower().startswith('true') diff --git a/serverthrallapi/urls.py b/serverthrallapi/urls.py index dfc7362..1e1118c 100644 --- a/serverthrallapi/urls.py +++ b/serverthrallapi/urls.py @@ -1,6 +1,7 @@ from django.contrib import admin -from django.urls import path +from django.urls import include, path urlpatterns = [ path('admin/', admin.site.urls), + path('api/', include('api.urls')), ]