diff --git a/composer.json b/composer.json index c18ae597..e2e8140a 100644 --- a/composer.json +++ b/composer.json @@ -27,7 +27,7 @@ "php": "^8.1", "laravel/framework": "^10.0", "maatwebsite/excel": "^3.1.45", - "eveseat/eseye": "^3.2", + "eveseat/eseye": "^3.2.1", "eveseat/services": "^5.1", "guzzlehttp/guzzle": "^7.0", "doctrine/dbal": "^3.0", diff --git a/src/Bus/Corporation.php b/src/Bus/Corporation.php index 8a9f3c4f..2cecd8a1 100644 --- a/src/Bus/Corporation.php +++ b/src/Bus/Corporation.php @@ -48,6 +48,7 @@ use Seat\Eveapi\Jobs\Corporation\Starbases; use Seat\Eveapi\Jobs\Corporation\Structures; use Seat\Eveapi\Jobs\Corporation\Titles; +use Seat\Eveapi\Jobs\CorporationProjects\Projects; use Seat\Eveapi\Jobs\Industry\Corporation\Jobs; use Seat\Eveapi\Jobs\Industry\Corporation\Mining\Extractions; use Seat\Eveapi\Jobs\Industry\Corporation\Mining\ObserverDetails; @@ -205,5 +206,8 @@ protected function addAuthenticatedJobs() $this->addAuthenticatedJob(new ContainerLogs($this->corporation_id, $this->token)); $this->addAuthenticatedJob(new Locations($this->corporation_id, $this->token)); $this->addAuthenticatedJob(new Names($this->corporation_id, $this->token)); + + // projects + $this->addAuthenticatedJob(new Projects($this->corporation_id, $this->token)); } } diff --git a/src/Config/eveapi.scopes.php b/src/Config/eveapi.scopes.php index ba59775e..f7b2f637 100644 --- a/src/Config/eveapi.scopes.php +++ b/src/Config/eveapi.scopes.php @@ -86,4 +86,5 @@ 'esi-universe.read_structures.v1', 'esi-wallet.read_character_wallet.v1', 'esi-wallet.read_corporation_wallets.v1', + 'esi-corporations.read_projects.v1', ]; diff --git a/src/Jobs/CorporationProjects/Contributors.php b/src/Jobs/CorporationProjects/Contributors.php new file mode 100644 index 00000000..22911078 --- /dev/null +++ b/src/Jobs/CorporationProjects/Contributors.php @@ -0,0 +1,147 @@ +project_id = $project_id; + + parent::__construct($corporation_id, $token); + } + + /** + * Execute the job. + * + * @return void + * + * @throws \Throwable + */ + public function handle() + { + parent::handle(); + + $this->query_string['limit'] = '100'; + + $before = '0'; + + $proj = CorporationProject::findOrFail($this->project_id); + + $contriblist = collect(); + + while (true) { + + $this->query_string['before'] = $before; + + $response = $this->retrieve([ + 'corporation_id' => $this->getCorporationId(), + 'project_id' => $this->project_id, + ]); + + $contribs = $response->getBody(); + if (isset($contribs->cursor) && isset($contribs->cursor->before)) { + $before = $contribs->cursor->before; + } else { + break; // empty cursor + } + if (isset($contribs->contributors)) { + $contriblist = $contriblist->concat($contribs->contributors); + } else { + // We have reached the end of the dataset + break; + } + } + + // Clear out list if necessary. Disabled under assumption contributors cant leave list + // CorporationProjectContributor::where('project_id', $this->project_id)->delete(); + + $rows = $contriblist->map(function ($item) use ($proj) { + // handle object or array item + $id = is_object($item) ? ($item->id ?? null) : ($item['id'] ?? null); + $contributed = is_object($item) ? ($item->contributed ?? 0) : ($item['contributed'] ?? 0); + + return [ + 'project_id' => $proj->id, + 'character_id' => $id, + 'contributed' => $contributed, + ]; + })->toArray(); + + // Perform upsert: unique by project_id + character_id, update contributed on conflict + // Timestamps are auto updated + CorporationProjectContributor::upsert( + $rows, + uniqueBy: ['project_id', 'character_id'], + update: ['contributed'] + ); + + } +} diff --git a/src/Jobs/CorporationProjects/Details.php b/src/Jobs/CorporationProjects/Details.php new file mode 100644 index 00000000..02844c03 --- /dev/null +++ b/src/Jobs/CorporationProjects/Details.php @@ -0,0 +1,142 @@ +project_id = $project_id; + + parent::__construct($corporation_id, $token); + } + + /** + * Execute the job. + * + * @return void + * + * @throws \Throwable + */ + public function handle() + { + parent::handle(); + + $cid = $this->getCorporationId(); // Extract it here early as we use it a log + + $response = $this->retrieve([ + 'project_id' => $this->project_id, + 'corporation_id' => $cid, + ]); + + $details = $response->getBody(); + + $lm = Carbon::parse($details->last_modified); + + // Weird early projects, bad data + $thresholdDate = Carbon::parse('2025-01-01'); + if ($lm->isBefore($thresholdDate)){ + logger()->warning('early project detected', ['body' => $details]); // These may need investigation by CCP if requried. + + return; + } + + $proj = CorporationProject::firstOrNew([ + 'id' => $this->project_id, + 'corporation_id' => $cid, + ]); + + ProjectsMapping::make($proj, $details, [ + 'last_modified' => function () use ($lm) { + return $lm->format('Y-m-d H:i:s'); + }, + 'corporation_id' => function () use ($cid) { + return $cid; + }, + 'created' => function () use ($details) { + return Carbon::parse($details->details->created)->format('Y-m-d H:i:s'); + }, + 'finished' => function () use ($details) { + if (! isset($details->details->finished)){ + return; + } + + return Carbon::parse($details->details->finished)->format('Y-m-d H:i:s'); + }, + 'expires' => function () use ($details) { + if (! isset($details->details->expires)){ + return; + } + + return Carbon::parse($details->details->expires)->format('Y-m-d H:i:s'); + }, + 'configuration' => function () use ($details) { + return json_encode($details->configuration); + }, + ])->save(); + + } +} diff --git a/src/Jobs/CorporationProjects/Projects.php b/src/Jobs/CorporationProjects/Projects.php new file mode 100644 index 00000000..f8c78e0e --- /dev/null +++ b/src/Jobs/CorporationProjects/Projects.php @@ -0,0 +1,152 @@ +query_string['limit'] = '100'; + $this->query_string['state'] = 'All'; + + $before = '0'; + + $this->project_jobs = collect(); + + while (true) { + + // TODO - proper cursor based caching, not just grab it all every time. + $this->query_string['before'] = $before; + + $response = $this->retrieve([ + 'corporation_id' => $this->getCorporationId(), + ]); + + $projects = $response->getBody(); + if (isset($projects->cursor) && isset($projects->cursor->before)) { + $before = $projects->cursor->before; + } else { + break; // empty cursor + } + if (isset($projects->projects)) { + foreach ($projects->projects as $project) { + $lm = Carbon::parse($project->last_modified); + // Weird early projects + $thresholdDate = Carbon::parse('2025-01-01'); + if ($lm->isBefore($thresholdDate)){ + logger()->warning('early project detected', ['project' => $project]); // // These may need investigation by CCP if requried. + continue; + } + + $proj = CorporationProject::firstOrNew([ + 'id' => $project->id, + ]); + + ProjectsMapping::make($proj, $project, [ + 'last_modified' => function () use ($lm) { + return $lm->format('Y-m-d H:i:s'); + }, + 'corporation_id' => function () { + return $this->getCorporationId(); + }, + ])->save(); + + $this->project_jobs->add(new Details($this->getCorporationId(), $this->getToken(), $proj->id)); + $this->project_jobs->add(new Contributors($this->getCorporationId(), $this->getToken(), $proj->id)); + } + } else { + // We have reached the end of the dataset + break; + } + } + + if ($this->project_jobs->isNotEmpty()) { + if($this->batchId) { + $this->batch()->add($this->project_jobs->toArray()); + } else { + Bus::batch($this->project_jobs->toArray()) + ->name(sprintf('Projects: %s', $this->token->character->name ?? $this->token->character_id)) + ->onQueue($this->job->getQueue()) + ->dispatch(); + } + } + + } +} diff --git a/src/Mapping/CorporationProjects/ProjectsMapping.php b/src/Mapping/CorporationProjects/ProjectsMapping.php new file mode 100644 index 00000000..6d916eca --- /dev/null +++ b/src/Mapping/CorporationProjects/ProjectsMapping.php @@ -0,0 +1,59 @@ + 'id', + 'corporation_id' => 'corporation_id', + 'last_modified' => 'last_modified', + 'name' => 'name', + 'progress_current' => 'progress.current', + 'progress_desired' => 'progress.desired', + 'reward_initial' => 'reward.initial', + 'reward_remaining' => 'reward.remaining', + 'state' => 'state', + 'configuration' => 'configuration', + 'contribution_participation_limit' => 'contribution.participation_limit', + 'contribution_reward' => 'contribution.reward_per_contribution', + 'contribution_submission_limit' => 'contribution.submission_limit', + 'contribution_submission_multiplier' => 'contribution.submission_multiplier', + 'creator_id' => 'creator.id', + 'career' => 'details.career', + 'created' => 'details.created', + 'description' => 'details.description', + 'expires' => 'details.expires', + 'finished' => 'details.finished', + ]; +} diff --git a/src/Models/CorporationProjects/CorporationProject.php b/src/Models/CorporationProjects/CorporationProject.php new file mode 100644 index 00000000..7a22a0c2 --- /dev/null +++ b/src/Models/CorporationProjects/CorporationProject.php @@ -0,0 +1,58 @@ +hasMany(CorporationProjectContributor::class, 'project_id', 'id'); + } + + public function creator() + { + return $this->hasOne(UniverseName::class, 'entity_id', 'creator_id') + ->withDefault([ + 'category' => 'character', + 'name' => trans('web::seat.unknown'), + ]); + } +} diff --git a/src/Models/CorporationProjects/CorporationProjectContributor.php b/src/Models/CorporationProjects/CorporationProjectContributor.php new file mode 100644 index 00000000..28424a36 --- /dev/null +++ b/src/Models/CorporationProjects/CorporationProjectContributor.php @@ -0,0 +1,55 @@ + 'integer', + ]; + + public function project() + { + return $this->belongsTo(CorporationProject::class, 'project_id', 'id'); + } + + public function character() + { + return $this->belongsTo(CharacterInfo::class, 'character_id', 'character_id'); + } +} diff --git a/src/database/migrations/2025_08_17_141928_create_corporation_projects_table.php b/src/database/migrations/2025_08_17_141928_create_corporation_projects_table.php new file mode 100644 index 00000000..a25d218d --- /dev/null +++ b/src/database/migrations/2025_08_17_141928_create_corporation_projects_table.php @@ -0,0 +1,78 @@ +uuid('id')->primary(); + $table->bigInteger('corporation_id')->index(); // Index because the datatable will filter on it. + $table->dateTime('last_modified'); + $table->string('name', length:61); + $table->bigInteger('progress_current'); + $table->bigInteger('progress_desired'); + $table->double('reward_initial')->nullable(); + $table->double('reward_remaining')->nullable(); + $table->enum('state', ['Unspecified', 'Active', 'Closed', 'Completed', 'Expired', 'Deleted']); + + // The below only come from the details endpoint (which can also update some of the above) + $table->enum('career', ['Unspecified', 'Explorer', 'Industrialist', 'Enforcer', 'Soldier of Fortune'])->nullable(); + $table->dateTime('created')->nullable(); + $table->string('description', length:1001)->nullable(); + $table->dateTime('expires')->nullable(); + $table->dateTime('finished')->nullable(); + $table->bigInteger('creator_id')->nullable(); + + $table->bigInteger('contribution_participation_limit')->nullable(); + $table->double('contribution_reward')->nullable(); + $table->bigInteger('contribution_submission_limit')->nullable(); + $table->double('contribution_submission_multiplier')->nullable(); + + $table->json('configuration')->nullable(); + + $table->timestamps(); + }); + } + + /** + * Reverse the migrations. + * + * @return void + */ + public function down() + { + Schema::drop('corporation_projects'); + } +} diff --git a/src/database/migrations/2025_08_21_075552_create_corporation_project_contributors_table.php b/src/database/migrations/2025_08_21_075552_create_corporation_project_contributors_table.php new file mode 100644 index 00000000..e2e710c1 --- /dev/null +++ b/src/database/migrations/2025_08_21_075552_create_corporation_project_contributors_table.php @@ -0,0 +1,57 @@ +bigIncrements('id'); + $table->foreignUuid('project_id')->constrained(table: 'corporation_projects')->cascadeOnDelete()->index(); + $table->unsignedBigInteger('character_id')->index(); + $table->bigInteger('contributed'); + + $table->unique(['project_id', 'character_id']); + $table->timestamps(); + }); + } + + /** + * Reverse the migrations. + * + * @return void + */ + public function down() + { + Schema::drop('corporation_project_contributors'); + } +}