Skip to content
Commits on Source (17)
......@@ -72,3 +72,6 @@ celerybeat-schedule
#transifex
.tx/
#other
.flake8
......@@ -6,11 +6,6 @@ before_script:
- python -V
- pip install wheel tox
test-3.5-core:
image: python:3.5-buster
script:
- tox -e py35-core
test-3.6-core:
image: python:3.6-buster
script:
......@@ -26,11 +21,6 @@ test-3.8-core:
script:
- tox -e py38-core
test-3.5-all:
image: python:3.5-buster
script:
- tox -e py35-all
test-3.6-all:
image: python:3.6-buster
script:
......
# .readthedocs.yml
# Read the Docs configuration file
# See https://docs.readthedocs.io/en/stable/config-file/v2.html for details
# Required
version: 2
# Build documentation in the docs/ directory with Sphinx
sphinx:
configuration: docs/conf.py
# Build documentation with MkDocs
#mkdocs:
# configuration: mkdocs.yml
# Optionally build your docs in additional formats such as PDF and ePub
formats: all
# Optionally set the version of Python and requirements required to build your docs
python:
version: 3.7
install:
- method: pip
path: .
extra_requirements:
- testing
system_packages: true
\ No newline at end of file
# This will make sure the app is always imported when
# Django starts so that shared_task will use this app.
__version__ = '2.6.3'
__version__ = '2.6.4'
NAME = 'Alliance Auth v%s' % __version__
default_app_config = 'allianceauth.apps.AllianceAuthConfig'
......@@ -22,13 +22,6 @@ urlpatterns = [
r'^account/characters/add/$',
views.add_character,
name='add_character'
),
url(
r'^help/$',
login_required(
TemplateView.as_view(template_name='allianceauth/help.html')
),
name='help'
),
),
url(r'^dashboard/$', views.dashboard, name='dashboard'),
]
source diff could not be displayed: it is too large. Options to address this: view the blob.
This diff is collapsed.
source diff could not be displayed: it is too large. Options to address this: view the blob.
......@@ -4,3 +4,8 @@ from allianceauth import urls
urlpatterns = [
url(r'', include(urls)),
]
handler500 = 'allianceauth.views.Generic500Redirect'
handler404 = 'allianceauth.views.Generic404Redirect'
handler403 = 'allianceauth.views.Generic403Redirect'
handler400 = 'allianceauth.views.Generic400Redirect'
......@@ -36,7 +36,7 @@ class DiscordService(ServicesHook):
def sync_nickname(self, user):
logger.debug('Syncing %s nickname for user %s' % (self.name, user))
DiscordTasks.update_nickname.delay(user.pk)
DiscordTasks.update_nickname.apply_async(args=[user.pk], countdown=5)
def update_all_groups(self):
logger.debug('Update all %s groups called' % self.name)
......
......@@ -27,7 +27,12 @@ class DiscordViewsTestCase(WebTest):
self.login()
manager.generate_oauth_redirect_url.return_value = '/example.com/oauth/'
response = self.app.get('/discord/activate/', auto_follow=False)
self.assertRedirects(response, expected_url='/example.com/oauth/', target_status_code=404)
self.assertRedirects(
response,
expected_url="/example.com/oauth/",
target_status_code=404,
fetch_redirect_response=False,
)
@mock.patch(MODULE_PATH + '.tasks.DiscordOAuthManager')
def test_callback(self, manager):
......
......@@ -40,6 +40,11 @@ class MumbleService(ServicesHook):
if MumbleTasks.has_account(user):
MumbleTasks.update_groups.delay(user.pk)
def sync_nickname(self, user):
logger.debug("Updating %s nickname for %s" % (self.name, user))
if MumbleTasks.has_account(user):
MumbleTasks.update_display_name.apply_async(args=[user.pk], countdown=5) # cooldown on this task to ensure DB clean when syncing
def validate_user(self, user):
if MumbleTasks.has_account(user) and not self.service_active_for_user(user):
self.delete_user(user, notify_user=True)
......
# Generated by Django 2.2.9 on 2020-03-16 07:49
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('mumble', '0007_not_null_user'),
]
operations = [
migrations.AddField(
model_name='mumbleuser',
name='display_name',
field=models.CharField(max_length=254, null=True),
)
]
from django.db import migrations, models
from ..auth_hooks import MumbleService
from allianceauth.services.hooks import NameFormatter
def fwd_func(apps, schema_editor):
MumbleUser = apps.get_model("mumble", "MumbleUser")
db_alias = schema_editor.connection.alias
all_users = MumbleUser.objects.using(db_alias).all()
for user in all_users:
display_name = NameFormatter(MumbleService(), user.user).format_name()
user.display_name = display_name
user.save()
def rev_func(apps, schema_editor):
MumbleUser = apps.get_model("mumble", "MumbleUser")
db_alias = schema_editor.connection.alias
all_users = MumbleUser.objects.using(db_alias).all()
for user in all_users:
user.display_name = None
user.save()
class Migration(migrations.Migration):
dependencies = [
('mumble', '0008_mumbleuser_display_name'),
]
operations = [
migrations.RunPython(fwd_func, rev_func),
migrations.AlterField(
model_name='mumbleuser',
name='display_name',
field=models.CharField(max_length=254, unique=True),
preserve_default=False,
),
]
......@@ -15,10 +15,14 @@ class MumbleManager(models.Manager):
HASH_FN = 'bcrypt-sha256'
@staticmethod
def get_username(user):
def get_display_name(user):
from .auth_hooks import MumbleService
return NameFormatter(MumbleService(), user).format_name()
@staticmethod
def get_username(user):
return user.profile.main_character.character_name # main character as the user.username may be incorect
@staticmethod
def sanitise_username(username):
return username.replace(" ", "_")
......@@ -32,20 +36,26 @@ class MumbleManager(models.Manager):
return bcrypt_sha256.encrypt(password.encode('utf-8'))
def create(self, user):
username = self.get_username(user)
logger.debug("Creating mumble user with username {}".format(username))
username_clean = self.sanitise_username(username)
password = self.generate_random_pass()
pwhash = self.gen_pwhash(password)
logger.debug("Proceeding with mumble user creation: clean username {}, pwhash starts with {}".format(
username_clean, pwhash[0:5]))
logger.info("Creating mumble user {}".format(username_clean))
result = super(MumbleManager, self).create(user=user, username=username_clean,
pwhash=pwhash, hashfn=self.HASH_FN)
result.update_groups()
result.credentials.update({'username': result.username, 'password': password})
return result
try:
username = self.get_username(user)
logger.debug("Creating mumble user with username {}".format(username))
username_clean = self.sanitise_username(username)
display_name = self.get_display_name(user)
password = self.generate_random_pass()
pwhash = self.gen_pwhash(password)
logger.debug("Proceeding with mumble user creation: clean username {}, pwhash starts with {}".format(
username_clean, pwhash[0:5]))
logger.info("Creating mumble user {}".format(username_clean))
result = super(MumbleManager, self).create(user=user, username=username_clean,
pwhash=pwhash, hashfn=self.HASH_FN,
display_name=display_name)
result.update_groups()
result.credentials.update({'username': result.username, 'password': password})
return result
except AttributeError: # No Main or similar errors
return False
return False
def user_exists(self, username):
return self.filter(username=username).exists()
......@@ -59,6 +69,8 @@ class MumbleUser(AbstractServiceModel):
objects = MumbleManager()
display_name = models.CharField(max_length=254, unique=True)
def __str__(self):
return self.username
......@@ -91,6 +103,12 @@ class MumbleUser(AbstractServiceModel):
self.save()
return True
def update_display_name(self):
logger.info("Updating mumble user {} display name".format(self.user))
self.display_name = MumbleManager.get_display_name(self.user)
self.save()
return True
class Meta:
permissions = (
("access_mumble", u"Can access the Mumble service"),
......
......@@ -45,9 +45,37 @@ class MumbleTasks:
logger.debug("User %s does not have a mumble account, skipping" % user)
return False
@staticmethod
@shared_task(bind=True, name="mumble.update_display_name", base=QueueOnce)
def update_display_name(self, pk):
user = User.objects.get(pk=pk)
logger.debug("Updating mumble groups for user %s" % user)
if MumbleTasks.has_account(user):
try:
if not user.mumble.update_display_name():
raise Exception("Display Name Sync failed")
logger.debug("Updated user %s mumble display name." % user)
return True
except MumbleUser.DoesNotExist:
logger.info("Mumble display name sync failed for {}, user does not have a mumble account".format(user))
except:
logger.exception("Mumble display name sync failed for %s, retrying in 10 mins" % user)
raise self.retry(countdown=60 * 10)
else:
logger.debug("User %s does not have a mumble account, skipping" % user)
return False
@staticmethod
@shared_task(name="mumble.update_all_groups")
def update_all_groups():
logger.debug("Updating ALL mumble groups")
for mumble_user in MumbleUser.objects.exclude(username__exact=''):
MumbleTasks.update_groups.delay(mumble_user.user.pk)
@staticmethod
@shared_task(name="mumble.update_all_display_names")
def update_all_display_names():
logger.debug("Updating ALL mumble display names")
for mumble_user in MumbleUser.objects.exclude(username__exact=''):
MumbleTasks.update_display_name.delay(mumble_user.user.pk)
......@@ -25,6 +25,9 @@ class MumbleHooksTestCase(TestCase):
def setUp(self):
self.member = 'member_user'
member = AuthUtils.create_member(self.member)
AuthUtils.add_main_character(member, 'auth_member', '12345', corp_id='111', corp_name='Test Corporation',
corp_ticker='TESTR')
member = User.objects.get(pk=member.pk)
MumbleUser.objects.create(user=member)
self.none_user = 'none_user'
none_user = AuthUtils.create_user(self.none_user)
......@@ -122,24 +125,46 @@ class MumbleViewsTestCase(TestCase):
self.member.save()
AuthUtils.add_main_character(self.member, 'auth_member', '12345', corp_id='111', corp_name='Test Corporation',
corp_ticker='TESTR')
self.member = User.objects.get(pk=self.member.pk)
add_permissions()
def login(self):
self.client.force_login(self.member)
def test_activate(self):
def test_activate_update(self):
self.login()
expected_username = '[TESTR]auth_member'
expected_username = 'auth_member'
expected_displayname = '[TESTR]auth_member'
response = self.client.get(urls.reverse('mumble:activate'), follow=False)
self.assertEqual(response.status_code, 200)
self.assertContains(response, expected_username)
# create
mumble_user = MumbleUser.objects.get(user=self.member)
self.assertEqual(mumble_user.username, expected_username)
self.assertTrue(MumbleUser.objects.user_exists(expected_username))
self.assertEqual(str(mumble_user), expected_username)
self.assertEqual(mumble_user.display_name, expected_displayname)
self.assertTrue(mumble_user.pwhash)
self.assertIn('Guest', mumble_user.groups)
self.assertIn('Member', mumble_user.groups)
self.assertIn(',', mumble_user.groups)
# test update
self.member.profile.main_character.character_name = "auth_member_updated"
self.member.profile.main_character.corporation_ticker = "TESTU"
self.member.profile.main_character.save()
mumble_user.update_display_name()
mumble_user = MumbleUser.objects.get(user=self.member)
expected_displayname = '[TESTU]auth_member_updated'
self.assertEqual(mumble_user.username, expected_username)
self.assertTrue(MumbleUser.objects.user_exists(expected_username))
self.assertEqual(str(mumble_user), expected_username)
self.assertEqual(mumble_user.display_name, expected_displayname)
self.assertTrue(mumble_user.pwhash)
self.assertIn('Guest', mumble_user.groups)
self.assertIn('Member', mumble_user.groups)
self.assertIn(',', mumble_user.groups)
def test_deactivate_post(self):
self.login()
MumbleUser.objects.create(user=self.member)
......@@ -171,7 +196,6 @@ class MumbleViewsTestCase(TestCase):
self.assertTemplateUsed(response, 'services/service_credentials.html')
self.assertContains(response, 'auth_member')
class MumbleManagerTestCase(TestCase):
def setUp(self):
from .models import MumbleManager
......
import logging
from django.contrib.auth.models import User, Group, Permission
from django.core.exceptions import ObjectDoesNotExist
from django.db import transaction
from django.db.models.signals import m2m_changed
from django.db.models.signals import pre_delete
......@@ -11,6 +12,7 @@ from .tasks import disable_user
from allianceauth.authentication.models import State, UserProfile
from allianceauth.authentication.signals import state_changed
from allianceauth.eveonline.models import EveCharacter
logger = logging.getLogger(__name__)
......@@ -157,14 +159,45 @@ def disable_services_on_inactive(sender, instance, *args, **kwargs):
@receiver(pre_save, sender=UserProfile)
def disable_services_on_no_main(sender, instance, *args, **kwargs):
if not instance.pk:
def process_main_character_change(sender, instance, *args, **kwargs):
if not instance.pk: # ignore
# new model being created
return
try:
old_instance = UserProfile.objects.get(pk=instance.pk)
if old_instance.main_character and not instance.main_character:
if old_instance.main_character and not instance.main_character: # lost main char disable services
logger.info("Disabling services due to loss of main character for user {0}".format(instance.user))
disable_user(instance.user)
elif old_instance.main_character is not instance.main_character: # swapping/changing main character
logger.info("Updating Names due to change of main character for user {0}".format(instance.user))
for svc in ServicesHook.get_services():
try:
svc.validate_user(instance.user)
svc.sync_nickname(instance.user)
except:
logger.exception('Exception running sync_nickname for services module %s on user %s' % (svc, instance))
except UserProfile.DoesNotExist:
pass
@receiver(pre_save, sender=EveCharacter)
def process_main_character_update(sender, instance, *args, **kwargs):
try:
if instance.userprofile:
old_instance = EveCharacter.objects.get(pk=instance.pk)
if not instance.character_name == old_instance.character_name or \
not instance.corporation_name == old_instance.corporation_name or \
not instance.alliance_name == old_instance.alliance_name:
logger.info("syncing service nickname for user {0}".format(instance.userprofile.user))
for svc in ServicesHook.get_services():
try:
svc.validate_user(instance.userprofile.user)
svc.sync_nickname(instance.userprofile.user)
except:
logger.exception('Exception running sync_nickname for services module %s on user %s' % (svc, instance))
except ObjectDoesNotExist: # not a main char ignore
pass
from allianceauth import NAME
from esi.clients import esi_client_factory
import requests
import logging
import os
import requests
from allianceauth import NAME
from allianceauth.eveonline.providers import provider
logger = logging.getLogger(__name__)
SWAGGER_SPEC_PATH = os.path.join(os.path.dirname(os.path.abspath(__file__)), 'swagger.json')
"""
Swagger Operations:
get_killmails_killmail_id_killmail_hash
"""
class SRPManager:
def __init__(self):
pass
@staticmethod
def get_kill_id(killboard_link):
num_set = '0123456789'
......@@ -34,18 +30,23 @@ class SRPManager:
if result:
killmail_id = result['killmail_id']
killmail_hash = result['zkb']['hash']
c = esi_client_factory(spec_file=SWAGGER_SPEC_PATH)
km = c.Killmails.get_killmails_killmail_id_killmail_hash(killmail_id=killmail_id,
killmail_hash=killmail_hash).result()
c = provider.client
km = c.Killmails.get_killmails_killmail_id_killmail_hash(
killmail_id=killmail_id,
killmail_hash=killmail_hash
).result()
else:
raise ValueError("Invalid Kill ID")
if km:
ship_type = km['victim']['ship_type_id']
logger.debug("Ship type for kill ID %s is %s" % (kill_id, ship_type))
logger.debug(
"Ship type for kill ID %s is %s" % (kill_id, ship_type)
)
ship_value = result['zkb']['totalValue']
logger.debug("Total loss value for kill id %s is %s" % (kill_id, ship_value))
logger.debug(
"Total loss value for kill id %s is %s" % (kill_id, ship_value)
)
victim_id = km['victim']['character_id']
return ship_type, ship_value, victim_id
else:
raise ValueError("Invalid Kill ID or Hash.")
{"consumes":["application/json"],"definitions":{"bad_request":{"description":"Bad request model","properties":{"error":{"description":"Bad request message","type":"string"}},"required":["error"],"title":"Bad request","type":"object"},"error_limited":{"description":"Error limited model","properties":{"error":{"description":"Error limited message","type":"string"}},"required":["error"],"title":"Error limited","type":"object"},"forbidden":{"description":"Forbidden model","properties":{"error":{"description":"Forbidden message","type":"string"},"sso_status":{"description":"status code received from SSO","type":"integer"}},"required":["error"],"title":"Forbidden","type":"object"},"gateway_timeout":{"description":"Gateway timeout model","properties":{"error":{"description":"Gateway timeout message","type":"string"},"timeout":{"description":"number of seconds the request was given","type":"integer"}},"required":["error"],"title":"Gateway timeout","type":"object"},"internal_server_error":{"description":"Internal server error model","properties":{"error":{"description":"Internal server error message","type":"string"}},"required":["error"],"title":"Internal server error","type":"object"},"service_unavailable":{"description":"Service unavailable model","properties":{"error":{"description":"Service unavailable message","type":"string"}},"required":["error"],"title":"Service unavailable","type":"object"},"unauthorized":{"description":"Unauthorized model","properties":{"error":{"description":"Unauthorized message","type":"string"}},"required":["error"],"title":"Unauthorized","type":"object"}},"host":"esi.evetech.net","info":{"description":"An OpenAPI for EVE Online","title":"EVE Swagger Interface","version":"0.8.6"},"parameters":{"Accept-Language":{"default":"en-us","description":"Language to use in the response","enum":["de","en-us","fr","ja","ru","zh"],"in":"header","name":"Accept-Language","type":"string"},"If-None-Match":{"description":"ETag from a previous request. A 304 will be returned if this matches the current ETag","in":"header","name":"If-None-Match","type":"string"},"alliance_id":{"description":"An EVE alliance ID","format":"int32","in":"path","minimum":1,"name":"alliance_id","required":true,"type":"integer"},"character_id":{"description":"An EVE character ID","format":"int32","in":"path","minimum":1,"name":"character_id","required":true,"type":"integer"},"corporation_id":{"description":"An EVE corporation ID","format":"int32","in":"path","minimum":1,"name":"corporation_id","required":true,"type":"integer"},"datasource":{"default":"tranquility","description":"The server name you would like data from","enum":["tranquility","singularity"],"in":"query","name":"datasource","type":"string"},"language":{"default":"en-us","description":"Language to use in the response, takes precedence over Accept-Language","enum":["de","en-us","fr","ja","ru","zh"],"in":"query","name":"language","type":"string"},"page":{"default":1,"description":"Which page of results to return","format":"int32","in":"query","minimum":1,"name":"page","type":"integer"},"token":{"description":"Access token to use if unable to set a header","in":"query","name":"token","type":"string"}},"paths":{"/v1/killmails/{killmail_id}/{killmail_hash}/":{"get":{"description":"Return a single killmail from its ID and hash\n\n---\n\nThis route is cached for up to 1209600 seconds","operationId":"get_killmails_killmail_id_killmail_hash","parameters":[{"$ref":"#/parameters/datasource"},{"$ref":"#/parameters/If-None-Match"},{"description":"The killmail hash for verification","in":"path","name":"killmail_hash","required":true,"type":"string"},{"description":"The killmail ID to be queried","format":"int32","in":"path","name":"killmail_id","required":true,"type":"integer"}],"responses":{"200":{"description":"A killmail","examples":{"application/json":{"attackers":[{"character_id":95810944,"corporation_id":1000179,"damage_done":5745,"faction_id":500003,"final_blow":true,"security_status":-0.3,"ship_type_id":17841,"weapon_type_id":3074}],"killmail_id":56733821,"killmail_time":"2016-10-22T17:13:36Z","solar_system_id":30002976,"victim":{"alliance_id":621338554,"character_id":92796241,"corporation_id":841363671,"damage_taken":5745,"items":[{"flag":20,"item_type_id":5973,"quantity_dropped":1,"singleton":0}],"position":{"x":452186600569.4748,"y":146704961490.90222,"z":109514596532.54477},"ship_type_id":17812}}},"headers":{"Cache-Control":{"description":"The caching mechanism used","type":"string"},"ETag":{"description":"RFC7232 compliant entity tag","type":"string"},"Expires":{"description":"RFC7231 formatted datetime string","type":"string"},"Last-Modified":{"description":"RFC7231 formatted datetime string","type":"string"}},"schema":{"description":"200 ok object","properties":{"attackers":{"description":"attackers array","items":{"description":"attacker object","properties":{"alliance_id":{"description":"alliance_id integer","format":"int32","title":"get_killmails_killmail_id_killmail_hash_alliance_id","type":"integer"},"character_id":{"description":"character_id integer","format":"int32","title":"get_killmails_killmail_id_killmail_hash_character_id","type":"integer"},"corporation_id":{"description":"corporation_id integer","format":"int32","title":"get_killmails_killmail_id_killmail_hash_corporation_id","type":"integer"},"damage_done":{"description":"damage_done integer","format":"int32","title":"get_killmails_killmail_id_killmail_hash_damage_done","type":"integer"},"faction_id":{"description":"faction_id integer","format":"int32","title":"get_killmails_killmail_id_killmail_hash_faction_id","type":"integer"},"final_blow":{"description":"Was the attacker the one to achieve the final blow\n","title":"get_killmails_killmail_id_killmail_hash_final_blow","type":"boolean"},"security_status":{"description":"Security status for the attacker\n","format":"float","title":"get_killmails_killmail_id_killmail_hash_security_status","type":"number"},"ship_type_id":{"description":"What ship was the attacker flying\n","format":"int32","title":"get_killmails_killmail_id_killmail_hash_ship_type_id","type":"integer"},"weapon_type_id":{"description":"What weapon was used by the attacker for the kill\n","format":"int32","title":"get_killmails_killmail_id_killmail_hash_weapon_type_id","type":"integer"}},"required":["security_status","final_blow","damage_done"],"title":"get_killmails_killmail_id_killmail_hash_attacker","type":"object"},"maxItems":10000,"title":"get_killmails_killmail_id_killmail_hash_attackers","type":"array"},"killmail_id":{"description":"ID of the killmail","format":"int32","title":"get_killmails_killmail_id_killmail_hash_killmail_id","type":"integer"},"killmail_time":{"description":"Time that the victim was killed and the killmail generated\n","format":"date-time","title":"get_killmails_killmail_id_killmail_hash_killmail_time","type":"string"},"moon_id":{"description":"Moon if the kill took place at one","format":"int32","title":"get_killmails_killmail_id_killmail_hash_moon_id","type":"integer"},"solar_system_id":{"description":"Solar system that the kill took place in\n","format":"int32","title":"get_killmails_killmail_id_killmail_hash_solar_system_id","type":"integer"},"victim":{"description":"victim object","properties":{"alliance_id":{"description":"alliance_id integer","format":"int32","title":"get_killmails_killmail_id_killmail_hash_victim_alliance_id","type":"integer"},"character_id":{"description":"character_id integer","format":"int32","title":"get_killmails_killmail_id_killmail_hash_victim_character_id","type":"integer"},"corporation_id":{"description":"corporation_id integer","format":"int32","title":"get_killmails_killmail_id_killmail_hash_victim_corporation_id","type":"integer"},"damage_taken":{"description":"How much total damage was taken by the victim\n","format":"int32","title":"get_killmails_killmail_id_killmail_hash_damage_taken","type":"integer"},"faction_id":{"description":"faction_id integer","format":"int32","title":"get_killmails_killmail_id_killmail_hash_victim_faction_id","type":"integer"},"items":{"description":"items array","items":{"description":"item object","properties":{"flag":{"description":"Flag for the location of the item\n","format":"int32","title":"get_killmails_killmail_id_killmail_hash_flag","type":"integer"},"item_type_id":{"description":"item_type_id integer","format":"int32","title":"get_killmails_killmail_id_killmail_hash_item_type_id","type":"integer"},"items":{"description":"items array","items":{"description":"item object","properties":{"flag":{"description":"flag integer","format":"int32","title":"get_killmails_killmail_id_killmail_hash_item_flag","type":"integer"},"item_type_id":{"description":"item_type_id integer","format":"int32","title":"get_killmails_killmail_id_killmail_hash_item_item_type_id","type":"integer"},"quantity_destroyed":{"description":"quantity_destroyed integer","format":"int64","title":"get_killmails_killmail_id_killmail_hash_item_quantity_destroyed","type":"integer"},"quantity_dropped":{"description":"quantity_dropped integer","format":"int64","title":"get_killmails_killmail_id_killmail_hash_item_quantity_dropped","type":"integer"},"singleton":{"description":"singleton integer","format":"int32","title":"get_killmails_killmail_id_killmail_hash_item_singleton","type":"integer"}},"required":["item_type_id","singleton","flag"],"title":"get_killmails_killmail_id_killmail_hash_items_item","type":"object"},"maxItems":10000,"title":"get_killmails_killmail_id_killmail_hash_item_items","type":"array"},"quantity_destroyed":{"description":"How many of the item were destroyed if any\n","format":"int64","title":"get_killmails_killmail_id_killmail_hash_quantity_destroyed","type":"integer"},"quantity_dropped":{"description":"How many of the item were dropped if any\n","format":"int64","title":"get_killmails_killmail_id_killmail_hash_quantity_dropped","type":"integer"},"singleton":{"description":"singleton integer","format":"int32","title":"get_killmails_killmail_id_killmail_hash_singleton","type":"integer"}},"required":["item_type_id","singleton","flag"],"title":"get_killmails_killmail_id_killmail_hash_item","type":"object"},"maxItems":10000,"title":"get_killmails_killmail_id_killmail_hash_items","type":"array"},"position":{"description":"Coordinates of the victim in Cartesian space relative to the Sun\n","properties":{"x":{"description":"x number","format":"double","title":"get_killmails_killmail_id_killmail_hash_x","type":"number"},"y":{"description":"y number","format":"double","title":"get_killmails_killmail_id_killmail_hash_y","type":"number"},"z":{"description":"z number","format":"double","title":"get_killmails_killmail_id_killmail_hash_z","type":"number"}},"required":["x","y","z"],"title":"get_killmails_killmail_id_killmail_hash_position","type":"object"},"ship_type_id":{"description":"The ship that the victim was piloting and was destroyed\n","format":"int32","title":"get_killmails_killmail_id_killmail_hash_victim_ship_type_id","type":"integer"}},"required":["damage_taken","ship_type_id"],"title":"get_killmails_killmail_id_killmail_hash_victim","type":"object"},"war_id":{"description":"War if the killmail is generated in relation to an official war\n","format":"int32","title":"get_killmails_killmail_id_killmail_hash_war_id","type":"integer"}},"required":["killmail_id","killmail_time","victim","attackers","solar_system_id"],"title":"get_killmails_killmail_id_killmail_hash_ok","type":"object"}},"304":{"description":"Not modified","headers":{"Cache-Control":{"description":"The caching mechanism used","type":"string"},"ETag":{"description":"RFC7232 compliant entity tag","type":"string"},"Expires":{"description":"RFC7231 formatted datetime string","type":"string"},"Last-Modified":{"description":"RFC7231 formatted datetime string","type":"string"}}},"400":{"description":"Bad request","examples":{"application/json":{"error":"Bad request message"}},"schema":{"$ref":"#/definitions/bad_request"}},"420":{"description":"Error limited","examples":{"application/json":{"error":"Error limited message"}},"schema":{"$ref":"#/definitions/error_limited"}},"422":{"description":"Invalid killmail_id and/or killmail_hash","examples":{"application/json":{"error":"Unprocessable entity message"}},"schema":{"description":"Unprocessable entity","properties":{"error":{"description":"Unprocessable entity message","title":"get_killmails_killmail_id_killmail_hash_422_unprocessable_entity","type":"string"}},"title":"get_killmails_killmail_id_killmail_hash_unprocessable_entity","type":"object"}},"500":{"description":"Internal server error","examples":{"application/json":{"error":"Internal server error message"}},"schema":{"$ref":"#/definitions/internal_server_error"}},"503":{"description":"Service unavailable","examples":{"application/json":{"error":"Service unavailable message"}},"schema":{"$ref":"#/definitions/service_unavailable"}},"504":{"description":"Gateway timeout","examples":{"application/json":{"error":"Gateway timeout message"}},"schema":{"$ref":"#/definitions/gateway_timeout"}}},"summary":"Get a single killmail","tags":["Killmails"],"x-alternate-versions":["dev","legacy","v1"],"x-cached-seconds":1209600}}},"produces":["application/json"],"schemes":["https"],"securityDefinitions":{"evesso":{"authorizationUrl":"https://login.eveonline.com/v2/oauth/authorize","flow":"implicit","scopes":{},"type":"oauth2"}},"swagger":"2.0"}
\ No newline at end of file