Adding initial team support.

This commit is contained in:
Sergiotarxz 2024-01-13 01:17:57 +01:00
parent c38474614d
commit 9279e6388a
14 changed files with 426 additions and 91 deletions

View File

@ -33,6 +33,7 @@ my $build = Module::Build->new(
'Crypt::Bcrypt' => 0, 'Crypt::Bcrypt' => 0,
'DBIx::Class::TimeStamp' => 0, 'DBIx::Class::TimeStamp' => 0,
'DateTime::Format::HTTP' => 0, 'DateTime::Format::HTTP' => 0,
'GIS::Distance' => 0,
}, },
); );
$build->create_build_script; $build->create_build_script;

View File

@ -238,37 +238,46 @@ export default class Conquer {
} }
} }
private getNearbyNodes(): void { private async getNearbyNodes(): Promise<void> {
const urlNodes = new URL('/conquer/node/near', window.location.protocol + '//' + window.location.hostname + ':' + window.location.port) const urlNodes = new URL('/conquer/node/near', window.location.protocol + '//' + window.location.hostname + ':' + window.location.port)
fetch(urlNodes).then(async (response) => { let response;
let responseBody; try {
try { response = await fetch(urlNodes);
responseBody = await response.json(); } catch (error) {
} catch (error) { console.error(error);
console.error('Error parseando json: ' + responseBody); return;
console.error(error); }
return; let responseBody;
try {
responseBody = await response.json();
} catch (error) {
console.error('Error parseando json: ' + responseBody);
console.error(error);
return;
}
if (response.status !== 200) {
console.error(responseBody.error);
return;
}
const serverNodes: Record<string, MapNode> = {};
const nodes = JsonSerializer.deserialize(responseBody, MapNode);
if (!(nodes instanceof Array)) {
console.error('Received null instead of node list.');
return;
}
for (const node of nodes) {
if (!(node instanceof MapNode)) {
console.error('Received node is not a MapNode.');
continue;
} }
if (response.status !== 200) { node.on('update-nodes', async () => {
console.error(responseBody.error); await this.sendCoordinatesToServer();
return; this.getNearbyNodes();
} });
const serverNodes: Record<string, MapNode> = {}; serverNodes[node.getId()] = node;
const nodes = JsonSerializer.deserialize(responseBody, MapNode); }
if (!(nodes instanceof Array)) { this.serverNodes = serverNodes;
console.error('Received null instead of node list.'); this.refreshLayers();
return;
}
for (const node of nodes) {
if (!(node instanceof MapNode)) {
console.error('Received node is not a MapNode.');
continue;
}
serverNodes[node.getId()] = node;
}
this.serverNodes = serverNodes;
this.refreshLayers();
});
} }
private createIntervalPollNearbyNodes(): void { private createIntervalPollNearbyNodes(): void {
@ -284,29 +293,31 @@ export default class Conquer {
}, 40000); }, 40000);
} }
private sendCoordinatesToServer(): void { private async sendCoordinatesToServer(): Promise<void> {
const urlLog = new URL('/conquer/user/coordinates', window.location.protocol + '//' + window.location.hostname + ':' + window.location.port) const urlLog = new URL('/conquer/user/coordinates', window.location.protocol + '//' + window.location.hostname + ':' + window.location.port)
fetch(urlLog, { let res;
method: 'POST', try {
body: JSON.stringify([ res = await fetch(urlLog, {
this.coordinate_1, method: 'POST',
this.coordinate_2, body: JSON.stringify([
]), this.coordinate_1,
}).then(async (res) => { this.coordinate_2,
let responseBody; ])});
try { } catch (error) {
responseBody = await res.json();
} catch(error) {
console.error('Error parseando json: ' + responseBody);
console.error(error);
return;
}
if (res.status !== 200) {
console.error(responseBody.error);
}
}).catch((error) => {
console.error(error) console.error(error)
}); return;
}
let responseBody;
try {
responseBody = await res.json();
} catch(error) {
console.error('Error parseando json: ' + responseBody);
console.error(error);
return;
}
if (res.status !== 200) {
console.error(responseBody.error);
}
} }
private runPreStartState(): void { private runPreStartState(): void {

View File

@ -24,12 +24,22 @@ export default class NodeView extends AbstractTopBarInterface {
super() super()
this.node = node; this.node = node;
} }
public run() { public async run() {
const mainNode = this.getMainNode() const mainNode = this.getMainNode()
this.runCallbacks('update-nodes');
try {
this.node = await this.node.fetch();
} catch (error) {
this.runCallbacks('close');
}
const view = this.getNodeFromTemplateId('conquer-view-node-template') const view = this.getNodeFromTemplateId('conquer-view-node-template')
mainNode.append(view) mainNode.append(view)
this.getNodeNameH2().innerText = this.node.getName(); this.getNodeNameH2().innerText = this.node.getName();
this.getNodeDescriptionParagraph().innerText = this.node.getDescription(); this.getNodeDescriptionParagraph().innerText = this.node.getDescription()
+ "\n"
+ (this.node.isNear()
? 'Estas cerca y puedes interactuar con este sitio.'
: 'Estás demasiado lejos para hacer nada aquí.');
view.classList.remove('conquer-display-none') view.classList.remove('conquer-display-none')
mainNode.classList.remove('conquer-display-none') mainNode.classList.remove('conquer-display-none')
} }

View File

@ -7,6 +7,7 @@ import Fill from 'ol/style/Fill'
import Stroke from 'ol/style/Stroke' import Stroke from 'ol/style/Stroke'
import InterfaceManager from '@burguillosinfo/conquer/interface-manager' import InterfaceManager from '@burguillosinfo/conquer/interface-manager'
import NodeView from '@burguillosinfo/conquer/interface/node-view' import NodeView from '@burguillosinfo/conquer/interface/node-view'
import JsonSerializer from '@burguillosinfo/conquer/serializer';
@JsonObject() @JsonObject()
export default class MapNode { export default class MapNode {
@ -21,14 +22,42 @@ export default class MapNode {
@JsonProperty() private name: string, @JsonProperty() private name: string,
@JsonProperty() private description: string, @JsonProperty() private description: string,
@JsonProperty() private kind: string, @JsonProperty() private kind: string,
@JsonProperty() private is_near: boolean,
) { ) {
} }
public async fetch(): Promise<MapNode> {
const urlNode = new URL('/conquer/node/' + this.uuid, window.location.protocol + '//' + window.location.hostname + ':' + window.location.port)
const response = await fetch(urlNode);
let responseBody;
const errorThrow = new Error('Unable to fetch node updated.');
try {
responseBody = await response.json();
} catch (error) {
console.error('Error parseando json: ' + responseBody);
console.error(error);
throw errorThrow;
}
if (response.status !== 200) {
console.error(responseBody.error);
throw errorThrow;
}
const node = JsonSerializer.deserialize(responseBody, MapNode);
if (!(node instanceof MapNode)) {
console.error('Unexpected JSON value for MapNode.');
throw errorThrow;
}
return node;
}
public click(interfaceManager: InterfaceManager): void { public click(interfaceManager: InterfaceManager): void {
const viewNodeInterface = new NodeView(this); const viewNodeInterface = new NodeView(this);
viewNodeInterface.on('close', () => { viewNodeInterface.on('close', () => {
interfaceManager.remove(viewNodeInterface); interfaceManager.remove(viewNodeInterface);
}); });
viewNodeInterface.on('update-nodes', () => {
this.runCallbacks('update-nodes');
});
interfaceManager.push(viewNodeInterface); interfaceManager.push(viewNodeInterface);
this.runCallbacks('click'); this.runCallbacks('click');
} }
@ -54,6 +83,10 @@ export default class MapNode {
return this.type; return this.type;
} }
public isNear(): boolean {
return this.is_near;
}
public getName(): string { public getName(): string {
return this.name; return this.name;
} }

71
js-src/conquer/team.ts Normal file
View File

@ -0,0 +1,71 @@
import JsonSerializer from '@burguillosinfo/conquer/serializer';
import { JsonObject, JsonProperty } from 'typescript-json-serializer';
@JsonObject()
export default class ConquerTeam {
@JsonProperty()
private kind: string;
@JsonProperty()
private uuid: string;
@JsonProperty()
private name: string;
@JsonProperty()
private description: string;
@JsonProperty()
private points: number;
@JsonProperty()
private color: string;
constructor(uuid: string, name: string, description: string, points: number, color: string) {
this.kind = 'ConquerTeam';
this.uuid = uuid;
this.name = name;
this.description = description;
this.points = points;
this.color = color;
}
public static async getTeam(uuid: string): Promise<ConquerTeam> {
const urlTeam = new URL('/conquer/team/' + uuid, window.location.protocol + '//' + window.location.hostname + ':' + window.location.port)
try {
const response = await fetch(urlTeam)
if (response.status !== 200) {
throw new Error('Invalid response fetching team.')
}
const teamData = await response.json()
let team = JsonSerializer.deserialize(teamData, ConquerTeam);
if (team === undefined) {
team = null;
}
if (!(team instanceof ConquerTeam)) {
throw new Error('Unable to parse team.');
}
return team;
} catch (error) {
console.error(error)
throw new Error('Unable to fetch Team.');
}
}
public static async getSelfTeam(): Promise<ConquerTeam | null> {
const urlTeam = new URL('/conquer/user/team', window.location.protocol + '//' + window.location.hostname + ':' + window.location.port)
try {
const response = await fetch(urlTeam)
if (response.status !== 200) {
throw new Error('Invalid response fetching team.')
}
const teamData = await response.json()
let team = JsonSerializer.deserialize(teamData, ConquerTeam);
if (team === undefined) {
team = null;
}
if (team !== null && !(team instanceof ConquerTeam)) {
throw new Error('Unable to parse team.');
}
return team;
} catch (error) {
console.error(error)
throw new Error('Unable to fetch Team.');
}
}
}

View File

@ -1,3 +1,7 @@
import JsonSerializer from '@burguillosinfo/conquer/serializer';
import { JsonObject, JsonProperty } from 'typescript-json-serializer';
import ConquerTeam from '@burguillosinfo/conquer/team';
export interface UserData { export interface UserData {
is_admin: number is_admin: number
kind: string kind: string
@ -7,29 +11,37 @@ export interface UserData {
uuid: string uuid: string
} }
@JsonObject()
export default class ConquerUser { export default class ConquerUser {
private _isAdmin = false @JsonProperty()
private kind = "ConquerUser" private is_admin: boolean;
private lastActivity: string | null = null @JsonProperty()
private registrationDate: string | null = null private kind: string;
private username: string | null = null @JsonProperty()
private uuid: string | null = null private last_activity: string | null;
@JsonProperty()
private registration_date: string | null;
@JsonProperty()
private username: string;
@JsonProperty()
private uuid: string;
@JsonProperty({name: 'team'})
private team_uuid: string | null;
constructor(data: UserData) { constructor(kind: string, uuid: string, username: string, is_admin = false, registration_date: string | null = null, last_activity: string | null = null) {
this.lastActivity = data.last_activity ?? null; this.kind = kind;
this.registrationDate = data.registration_date ?? null; this.uuid = uuid;
if (this.kind !== data.kind) { this.username = username;
throw new Error(`We cannot instance a user from a kind different to ${this.kind}.`) this.is_admin = is_admin;
} this.registration_date = registration_date;
this._isAdmin = !!data.is_admin || false this.last_activity = last_activity;
this.uuid = data.uuid }
this.username = data.username
if (this.username === null || this.username === undefined) { public async getTeam(): Promise<ConquerTeam | null> {
throw new Error('No username in user instance') if (this.team_uuid === null) {
} return null;
if (this.uuid === null || this.username === undefined) {
throw new Error('No uuid in user instance')
} }
return ConquerTeam.getTeam(this.team_uuid);
} }
public static async getSelfUser(): Promise<ConquerUser | null> { public static async getSelfUser(): Promise<ConquerUser | null> {
@ -40,7 +52,11 @@ export default class ConquerUser {
throw new Error('Invalid response fetching user.') throw new Error('Invalid response fetching user.')
} }
const userData = await response.json() const userData = await response.json()
return new ConquerUser(userData) const user = JsonSerializer.deserialize(userData, ConquerUser);
if (!(user instanceof ConquerUser)) {
throw new Error('Unable to parse user.');
}
return user;
} catch (error) { } catch (error) {
console.error(error) console.error(error)
return null return null
@ -53,6 +69,6 @@ export default class ConquerUser {
return this.username return this.username
} }
public isAdmin(): boolean { public isAdmin(): boolean {
return this._isAdmin return this.is_admin
} }
} }

View File

@ -7,6 +7,7 @@ use Mojo::Base 'Mojolicious', -signatures;
# This method will run once at server start # This method will run once at server start
sub startup ($self) { sub startup ($self) {
my $metrics = BurguillosInfo::Controller::Metrics->new; my $metrics = BurguillosInfo::Controller::Metrics->new;
$self->sessions->default_expiration(0);
$self->hook( $self->hook(
around_dispatch => sub { around_dispatch => sub {
my $next = shift; my $next = shift;
@ -91,9 +92,12 @@ sub startup ($self) {
$r->get('/stats')->to('Metrics#stats'); $r->get('/stats')->to('Metrics#stats');
$r->get('/conquer')->to('Conquer#index'); $r->get('/conquer')->to('Conquer#index');
$r->put('/conquer/user')->to('UserConquer#create'); $r->put('/conquer/user')->to('UserConquer#create');
$r->get('/conquer/user/team')->to('UserConquer#getSelfTeam');
$r->post('/conquer/user/coordinates')->to('UserConquer#setCoordinates'); $r->post('/conquer/user/coordinates')->to('UserConquer#setCoordinates');
$r->get('/conquer/team/<uuid>')->to('ConquerTeam#get');
$r->put('/conquer/node')->to('ConquerNode#create'); $r->put('/conquer/node')->to('ConquerNode#create');
$r->get('/conquer/node/near')->to('ConquerNode#nearbyNodes'); $r->get('/conquer/node/near')->to('ConquerNode#nearbyNodes');
$r->get('/conquer/node/<uuid>')->to('ConquerNode#get');
$r->get('/conquer/user')->to('UserConquer#get_self'); $r->get('/conquer/user')->to('UserConquer#get_self');
$r->post('/conquer/user/login')->to('UserConquer#login'); $r->post('/conquer/user/login')->to('UserConquer#login');
$r->get('/conquer/tile/<zoom>/<x>/<y>.png')->to('ConquerTile#tile'); $r->get('/conquer/tile/<zoom>/<x>/<y>.png')->to('ConquerTile#tile');

View File

@ -9,6 +9,29 @@ use utf8;
use Mojo::Base 'Mojolicious::Controller', '-signatures'; use Mojo::Base 'Mojolicious::Controller', '-signatures';
use UUID::URandom qw/create_uuid_string/; use UUID::URandom qw/create_uuid_string/;
use BurguillosInfo::Schema;
sub get($self) {
my $uuid = $self->param('uuid');
my $user = $self->current_user;
if (!defined $uuid || !$uuid) {
return $self->render(status => 400, json => {
error => 'UUID de nodo invalido.',
});
}
my $schema = BurguillosInfo::Schema->Schema->resultset('ConquerNode');
my @nodes = $schema->search({uuid => $uuid});
if (!scalar @nodes) {
return $self->render(status => 404, json => {
error => 'Nodo no encontrado',
});
}
my $node = $nodes[0];
if (defined $user) {
return $self->render(json => $node->serialize($user));
}
return $self->render(json => $node->serialize());
}
sub create ($self) { sub create ($self) {
my $user = $self->current_user; my $user = $self->current_user;
@ -118,7 +141,7 @@ sub nearbyNodes($self) {
}); });
} }
my @nodes = BurguillosInfo::Schema->Schema->resultset('ConquerNode')->search({}); my @nodes = BurguillosInfo::Schema->Schema->resultset('ConquerNode')->search({});
@nodes = map { $_->serialize } @nodes; @nodes = map { $_->serialize($user) } @nodes;
return $self->render(json => \@nodes); return $self->render(json => \@nodes);
} }

View File

@ -0,0 +1,57 @@
package BurguillosInfo::Controller::ConquerTeam;
use v5.34.1;
use strict;
use warnings;
use utf8;
use Mojo::Base 'Mojolicious::Controller', '-signatures';
use UUID::URandom qw/create_uuid_string/;
use JSON;
use BurguillosInfo::Schema;
sub get($self) {
my $user = $self->current_user;
if (!defined $user) {
return $self->render(status => 401, json => {
error => 'You must be logged to fetch a team.',
});
}
my $uuid = $self->param('uuid');
my $resultset = BurguillosInfo::Schema->Schema->resultset('ConquerTeam');
my @teams = $resultset->search({
'uuid' => $uuid,
});
if (scalar @teams <= 0) {
return $self->render( status => 404, json => {
error => 'This team does not exist.',
});
}
my $team = $teams[0];
return $self->render(json => $team);
}
sub getSelfTeam($self) {
my $user = $self->current_user;
if (!defined $user) {
return $self->render(status => 401, json => {
error => 'You must be logged to fetch your Team.',
});
}
my $resultset = BurguillosInfo::Schema->Schema->resultset('ConquerTeam');
my @teams = $resultset->search({
'players.uuid' => $user->uuid
}, {
join => 'players',
});
if (scalar @teams <= 0) {
return $self->render(json => undef);
}
my $team = $teams[0];
return $self->render(json => $team);
}
1;

View File

@ -28,11 +28,11 @@ sub MIGRATIONS {
path TEXT, path TEXT,
FOREIGN KEY (path) REFERENCES paths(path) FOREIGN KEY (path) REFERENCES paths(path)
)', )',
'ALTER TABLE paths ADD column last_seen TIMESTAMP;', 'ALTER TABLE paths ADD COLUMN last_seen TIMESTAMP;',
'ALTER TABLE paths ALTER COLUMN last_seen SET DEFAULT NOW();', 'ALTER TABLE paths ALTER COLUMN last_seen SET DEFAULT NOW();',
'ALTER TABLE requests ADD PRIMARY KEY (uuid)', 'ALTER TABLE requests ADD PRIMARY KEY (uuid)',
'CREATE INDEX request_extra_index on requests (date, path);', 'CREATE INDEX request_extra_index on requests (date, path);',
'ALTER TABLE requests ADD column referer text;', 'ALTER TABLE requests ADD COLUMN referer text;',
'CREATE INDEX request_referer_index on requests (referer);', 'CREATE INDEX request_referer_index on requests (referer);',
'ALTER TABLE requests ADD COLUMN country TEXT;', 'ALTER TABLE requests ADD COLUMN country TEXT;',
'CREATE INDEX request_country_index on requests (country);', 'CREATE INDEX request_country_index on requests (country);',
@ -71,6 +71,17 @@ sub MIGRATIONS {
'ALTER TABLE conquer_user ALTER COLUMN last_coordinate_1 DROP DEFAULT;', 'ALTER TABLE conquer_user ALTER COLUMN last_coordinate_1 DROP DEFAULT;',
'ALTER TABLE conquer_user ADD COLUMN last_coordinate_2 REAL NOT NULL DEFAULT 0;', 'ALTER TABLE conquer_user ADD COLUMN last_coordinate_2 REAL NOT NULL DEFAULT 0;',
'ALTER TABLE conquer_user ALTER COLUMN last_coordinate_2 DROP DEFAULT;', 'ALTER TABLE conquer_user ALTER COLUMN last_coordinate_2 DROP DEFAULT;',
'CREATE TABLE conquer_teams (
uuid UUID NOT NULL PRIMARY KEY,
name TEXT NOT NULL,
description TEXT NOT NULL DEFAULT \'\',
points INTEGER NOT NULL DEFAULT 0
);',
'ALTER TABLE conquer_user ADD COLUMN team UUID REFERENCES conquer_teams (uuid);',
'ALTER TABLE conquer_node ADD COLUMN team UUID REFERENCES conquer_teams (uuid);',
'ALTER TABLE conquer_teams ADD COLUMN color TEXT NOT NULL DEFAULT \'#000\';',
'ALTER TABLE conquer_teams ALTER COLUMN color SET DEFAULT \'#555\';',
'ALTER TABLE conquer_teams ALTER COLUMN color SET DEFAULT \'#aaa\';',
); );
} }

View File

@ -9,6 +9,9 @@ use parent 'DBIx::Class::Core';
use feature 'signatures'; use feature 'signatures';
use JSON;
use GIS::Distance;
__PACKAGE__->table('conquer_node'); __PACKAGE__->table('conquer_node');
__PACKAGE__->load_components("TimeStamp"); __PACKAGE__->load_components("TimeStamp");
@ -40,9 +43,9 @@ __PACKAGE__->add_columns(
} }
); );
sub serialize ($self) { sub serialize ( $self, $player = undef ) {
$self = $self->get_from_storage(); $self = $self->get_from_storage();
return { my $return = {
kind => 'ConquerNode', kind => 'ConquerNode',
uuid => $self->uuid, uuid => $self->uuid,
name => $self->name, name => $self->name,
@ -50,7 +53,31 @@ sub serialize ($self) {
type => $self->type, type => $self->type,
coordinate_1 => $self->coordinate_1, coordinate_1 => $self->coordinate_1,
coordinate_2 => $self->coordinate_2, coordinate_2 => $self->coordinate_2,
is_near => $self->is_near($player),
}; };
return $return;
} }
sub is_near ( $self, $player ) {
if ( !defined $player ) {
return $JSON::false;
}
# Meters
if ($self->get_distance_to_player($player) < 100) {
return $JSON::true;
}
return $JSON::false;
}
sub get_distance_to_player ($self, $player) {
my $longitude_player = $player->last_coordinate_1;
my $latitude_player = $player->last_coordinate_2;
my $longitude_node = $self->coordinate_1;
my $latitude_node = $self->coordinate_2;
my $gis = GIS::Distance->new;
# Setting distance to meters.
my $distance = $gis->distance_metal( $latitude_node, $longitude_node, $latitude_player, $longitude_player) * 1000;
}
__PACKAGE__->set_primary_key('uuid'); __PACKAGE__->set_primary_key('uuid');
1; 1;

View File

@ -0,0 +1,53 @@
package BurguillosInfo::Schema::Result::ConquerTeam;
use v5.36.0;
use strict;
use warnings;
use parent 'DBIx::Class::Core';
use feature 'signatures';
use JSON;
__PACKAGE__->table('conquer_teams');
__PACKAGE__->load_components("TimeStamp");
__PACKAGE__->add_columns(
uuid => {
data_type => 'uuid',
is_nullable => 0,
},
name => {
data_type => 'text',
is_nullable => 0,
},
description => {
data_type => 'text',
is_nullable => 0,
},
points => {
data_type => 'integer',
is_nullable => 0,
},
color => {
data_type => 'text',
is_nullable => 0,
},
);
sub serialize ($self) {
$self = $self->get_from_storage();
return {
kind => 'ConquerTeam',
uuid => $self->uuid,
name => $self->name,
description => $self->description,
points => $self->points,
color => $self->color,
};
}
__PACKAGE__->has_many( players => 'BurguillosInfo::Schema::Result::ConquerUser', 'team');
__PACKAGE__->set_primary_key('uuid');
1;

View File

@ -9,6 +9,8 @@ use parent 'DBIx::Class::Core';
use feature 'signatures'; use feature 'signatures';
use JSON;
__PACKAGE__->table('conquer_user'); __PACKAGE__->table('conquer_user');
__PACKAGE__->load_components("TimeStamp"); __PACKAGE__->load_components("TimeStamp");
@ -17,6 +19,10 @@ __PACKAGE__->add_columns(
data_type => 'uuid', data_type => 'uuid',
is_nullable => 0, is_nullable => 0,
}, },
team => {
data_type => 'uuid',
is_nullable => 1,
},
username => { username => {
data_type => 'text', data_type => 'text',
is_nullable => 0, is_nullable => 0,
@ -52,16 +58,16 @@ __PACKAGE__->add_columns(
}, },
); );
sub coordinates($self, $coordinates = undef) { sub coordinates ( $self, $coordinates = undef ) {
if (defined $coordinates) { if ( defined $coordinates ) {
if (ref $coordinates ne 'ARRAY' || scalar $coordinates->@* != 2) { if ( ref $coordinates ne 'ARRAY' || scalar $coordinates->@* != 2 ) {
die 'The second parameter of this subroutine ' die 'The second parameter of this subroutine '
. 'must be an ARRAYREF of exactly two elements.'; . 'must be an ARRAYREF of exactly two elements.';
} }
$self->last_coordinate_1($coordinates->[0]); $self->last_coordinate_1( $coordinates->[0] );
$self->last_coordinate_2($coordinates->[1]); $self->last_coordinate_2( $coordinates->[1] );
} }
return [$self->last_coordinate_1, $self->last_coordinate_2]; return [ $self->last_coordinate_1, $self->last_coordinate_2 ];
} }
sub serialize_to_owner ($self) { sub serialize_to_owner ($self) {
@ -69,8 +75,9 @@ sub serialize_to_owner ($self) {
return { return {
kind => 'ConquerUser', kind => 'ConquerUser',
uuid => $self->uuid, uuid => $self->uuid,
team => $self->team,
username => $self->username, username => $self->username,
is_admin => $self->is_admin, is_admin => $self->is_admin ? $JSON::true : $JSON::false,
last_activity => $self->last_activity, last_activity => $self->last_activity,
registration_date => $self->registration_date, registration_date => $self->registration_date,
}; };

File diff suppressed because one or more lines are too long