-
Notifications
You must be signed in to change notification settings - Fork 9
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
Merge pull request #1142 from shikorism/feature/stats-api
User stats API
- Loading branch information
Showing
8 changed files
with
634 additions
and
0 deletions.
There are no files selected for viewing
53 changes: 53 additions & 0 deletions
53
app/Http/Controllers/Api/V1/UserStats/DailyCheckinSummary.php
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,53 @@ | ||
<?php | ||
declare(strict_types=1); | ||
|
||
namespace App\Http\Controllers\Api\V1\UserStats; | ||
|
||
use App\Http\Controllers\Controller; | ||
use App\Queries\EjaculationCountByDay; | ||
use App\User; | ||
use Carbon\CarbonImmutable; | ||
use Illuminate\Http\Request; | ||
use Symfony\Component\HttpKernel\Exception\AccessDeniedHttpException; | ||
|
||
class DailyCheckinSummary extends Controller | ||
{ | ||
public function __invoke(Request $request, User $user) | ||
{ | ||
if (!$user->isMe() && $user->is_protected) { | ||
throw new AccessDeniedHttpException('このユーザはチェックイン履歴を公開していません'); | ||
} | ||
|
||
$validated = $request->validate([ | ||
'since' => 'nullable|date_format:Y-m-d|after_or_equal:2000-01-01|before_or_equal:2099-12-31', | ||
'until' => 'nullable|date_format:Y-m-d|after_or_equal:2000-01-01|before_or_equal:2099-12-31', | ||
]); | ||
|
||
if (!empty($validated['since']) && !empty($validated['until'])) { | ||
$since = CarbonImmutable::createFromFormat('Y-m-d', $validated['since'])->startOfDay(); | ||
$until = CarbonImmutable::createFromFormat('Y-m-d', $validated['until'])->startOfDay()->addDay(); | ||
if ($until->isBefore($since)) { | ||
[$since, $until] = [$until, $since]; | ||
} | ||
} elseif (!empty($validated['since'])) { | ||
$since = CarbonImmutable::createFromFormat('Y-m-d', $validated['since'])->startOfDay(); | ||
$until = null; | ||
} elseif (!empty($validated['until'])) { | ||
$since = null; | ||
$until = CarbonImmutable::createFromFormat('Y-m-d', $validated['until'])->startOfDay()->addDay(); | ||
} else { | ||
$since = null; | ||
$until = null; | ||
} | ||
|
||
$countByDay = (new EjaculationCountByDay($user))->query(); | ||
if ($since !== null) { | ||
$countByDay = $countByDay->where('ejaculated_date', '>=', $since); | ||
} | ||
if ($until !== null) { | ||
$countByDay = $countByDay->where('ejaculated_date', '<', $until); | ||
} | ||
|
||
return response()->json($countByDay->get()); | ||
} | ||
} |
76 changes: 76 additions & 0 deletions
76
app/Http/Controllers/Api/V1/UserStats/HourlyCheckinSummary.php
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,76 @@ | ||
<?php | ||
declare(strict_types=1); | ||
|
||
namespace App\Http\Controllers\Api\V1\UserStats; | ||
|
||
use App\Ejaculation; | ||
use App\Http\Controllers\Controller; | ||
use App\User; | ||
use Carbon\CarbonImmutable; | ||
use Illuminate\Http\Request; | ||
use Illuminate\Support\Facades\DB; | ||
use Symfony\Component\HttpKernel\Exception\AccessDeniedHttpException; | ||
|
||
class HourlyCheckinSummary extends Controller | ||
{ | ||
public function __invoke(Request $request, User $user) | ||
{ | ||
if (!$user->isMe() && $user->is_protected) { | ||
throw new AccessDeniedHttpException('このユーザはチェックイン履歴を公開していません'); | ||
} | ||
|
||
$validated = $request->validate([ | ||
'since' => 'nullable|date_format:Y-m-d|after_or_equal:2000-01-01|before_or_equal:2099-12-31', | ||
'until' => 'nullable|date_format:Y-m-d|after_or_equal:2000-01-01|before_or_equal:2099-12-31', | ||
]); | ||
|
||
if (!empty($validated['since']) && !empty($validated['until'])) { | ||
$since = CarbonImmutable::createFromFormat('Y-m-d', $validated['since'])->startOfDay(); | ||
$until = CarbonImmutable::createFromFormat('Y-m-d', $validated['until'])->startOfDay()->addDay(); | ||
if ($until->isBefore($since)) { | ||
[$since, $until] = [$until, $since]; | ||
} | ||
} elseif (!empty($validated['since'])) { | ||
$since = CarbonImmutable::createFromFormat('Y-m-d', $validated['since'])->startOfDay(); | ||
$until = null; | ||
} elseif (!empty($validated['until'])) { | ||
$since = null; | ||
$until = CarbonImmutable::createFromFormat('Y-m-d', $validated['until'])->startOfDay()->addDay(); | ||
} else { | ||
$since = null; | ||
$until = null; | ||
} | ||
|
||
$dateCondition = []; | ||
if ($since !== null) { | ||
$dateCondition[] = ['ejaculated_date', '>=', $since]; | ||
} | ||
if ($until !== null) { | ||
$dateCondition[] = ['ejaculated_date', '<', $until]; | ||
} | ||
|
||
$groupByHour = Ejaculation::select(DB::raw( | ||
<<<'SQL' | ||
to_char(ejaculated_date, 'HH24') AS "hour", | ||
count(*) AS "count" | ||
SQL | ||
)) | ||
->where('user_id', $user->id) | ||
->where($dateCondition) | ||
->groupBy(DB::raw("to_char(ejaculated_date, 'HH24')")) | ||
->orderBy(DB::raw('1')) | ||
->get(); | ||
|
||
$results = []; | ||
for ($hour = 0; $hour < 24; $hour++) { | ||
if (!empty($groupByHour) && (int)($groupByHour->first()->hour) === $hour) { | ||
$data = $groupByHour->shift(); | ||
$results[] = ['hour' => $hour, 'count' => $data->count]; | ||
} else { | ||
$results[] = ['hour' => $hour, 'count' => 0]; | ||
} | ||
} | ||
|
||
return response()->json($results); | ||
} | ||
} |
52 changes: 52 additions & 0 deletions
52
app/Http/Controllers/Api/V1/UserStats/MostlyUsedCheckinTags.php
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,52 @@ | ||
<?php | ||
declare(strict_types=1); | ||
|
||
namespace App\Http\Controllers\Api\V1\UserStats; | ||
|
||
use App\Http\Controllers\Controller; | ||
use App\Queries\CountUsedTags; | ||
use App\User; | ||
use Carbon\CarbonImmutable; | ||
use Illuminate\Http\Request; | ||
use Illuminate\Support\Facades\Auth; | ||
use Symfony\Component\HttpKernel\Exception\AccessDeniedHttpException; | ||
|
||
class MostlyUsedCheckinTags extends Controller | ||
{ | ||
public function __invoke(Request $request, User $user) | ||
{ | ||
if (!$user->isMe() && $user->is_protected) { | ||
throw new AccessDeniedHttpException('このユーザはチェックイン履歴を公開していません'); | ||
} | ||
|
||
$validated = $request->validate([ | ||
'since' => 'nullable|date_format:Y-m-d|after_or_equal:2000-01-01|before_or_equal:2099-12-31', | ||
'until' => 'nullable|date_format:Y-m-d|after_or_equal:2000-01-01|before_or_equal:2099-12-31', | ||
]); | ||
|
||
if (!empty($validated['since']) && !empty($validated['until'])) { | ||
$since = CarbonImmutable::createFromFormat('Y-m-d', $validated['since'])->startOfDay(); | ||
$until = CarbonImmutable::createFromFormat('Y-m-d', $validated['until'])->startOfDay()->addDay(); | ||
if ($until->isBefore($since)) { | ||
[$since, $until] = [$until, $since]; | ||
} | ||
} elseif (!empty($validated['since'])) { | ||
$since = CarbonImmutable::createFromFormat('Y-m-d', $validated['since'])->startOfDay(); | ||
$until = null; | ||
} elseif (!empty($validated['until'])) { | ||
$since = null; | ||
$until = CarbonImmutable::createFromFormat('Y-m-d', $validated['until'])->startOfDay()->addDay(); | ||
} else { | ||
$since = null; | ||
$until = null; | ||
} | ||
|
||
$result = (new CountUsedTags(Auth::user(), $user)) | ||
->since($since) | ||
->until($until) | ||
->setIncludesMetadata($request->boolean('includesMetadata')) | ||
->query(); | ||
|
||
return response()->json($result); | ||
} | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,74 @@ | ||
<?php | ||
declare(strict_types=1); | ||
|
||
namespace App\Http\Controllers\Api\V1\UserStats; | ||
|
||
use App\Http\Controllers\Controller; | ||
use App\User; | ||
use Carbon\CarbonImmutable; | ||
use Carbon\CarbonInterface; | ||
use Illuminate\Http\Request; | ||
use Illuminate\Support\Facades\Auth; | ||
use Illuminate\Support\Facades\DB; | ||
use Symfony\Component\HttpKernel\Exception\AccessDeniedHttpException; | ||
|
||
class MostlyUsedLinks extends Controller | ||
{ | ||
public function __invoke(Request $request, User $user) | ||
{ | ||
if (!$user->isMe() && $user->is_protected) { | ||
throw new AccessDeniedHttpException('このユーザはチェックイン履歴を公開していません'); | ||
} | ||
|
||
$validated = $request->validate([ | ||
'since' => 'nullable|date_format:Y-m-d|after_or_equal:2000-01-01|before_or_equal:2099-12-31', | ||
'until' => 'nullable|date_format:Y-m-d|after_or_equal:2000-01-01|before_or_equal:2099-12-31', | ||
]); | ||
|
||
if (!empty($validated['since']) && !empty($validated['until'])) { | ||
$since = CarbonImmutable::createFromFormat('Y-m-d', $validated['since'])->startOfDay(); | ||
$until = CarbonImmutable::createFromFormat('Y-m-d', $validated['until'])->startOfDay()->addDay(); | ||
if ($until->isBefore($since)) { | ||
[$since, $until] = [$until, $since]; | ||
} | ||
|
||
if ($until->diffInYears($since) >= 1) { | ||
$until = $since->addYear(); | ||
} | ||
} elseif (!empty($validated['since'])) { | ||
$since = CarbonImmutable::createFromFormat('Y-m-d', $validated['since'])->startOfDay(); | ||
$until = $since->addYear(); | ||
} elseif (!empty($validated['until'])) { | ||
$until = CarbonImmutable::createFromFormat('Y-m-d', $validated['until'])->startOfDay()->addDay(); | ||
$since = $until->subYear(); | ||
} else { | ||
$since = CarbonImmutable::now()->startOfDay()->firstOfYear(); | ||
$until = CarbonImmutable::now()->startOfDay()->firstOfYear()->addYear(); | ||
} | ||
|
||
return response()->json($this->countMostFrequentlyUsedOkazu($user, $since, $until)); | ||
} | ||
|
||
private function countMostFrequentlyUsedOkazu(User $user, CarbonInterface $dateSince = null, CarbonInterface $dateUntil = null) | ||
{ | ||
$sql = <<<SQL | ||
SELECT normalized_link as link, count(*) as count | ||
FROM ejaculations e | ||
WHERE user_id = ? AND is_private IN (?, ?) AND ejaculated_date >= ? AND ejaculated_date < ? AND normalized_link <> '' | ||
GROUP BY normalized_link HAVING count(*) >= 2 | ||
ORDER BY count DESC, normalized_link | ||
LIMIT 10 | ||
SQL; | ||
|
||
if ($dateSince === null) { | ||
$dateSince = CarbonImmutable::minValue(); | ||
} | ||
if ($dateUntil === null) { | ||
$dateUntil = now()->addMonth()->startOfMonth(); | ||
} | ||
|
||
return DB::select(DB::raw($sql), [ | ||
$user->id, false, Auth::check() && $user->id === Auth::id(), $dateSince, $dateUntil | ||
]); | ||
} | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,107 @@ | ||
<?php | ||
declare(strict_types=1); | ||
|
||
namespace App\Queries; | ||
|
||
use App\User; | ||
use Carbon\Carbon; | ||
use Carbon\CarbonInterface; | ||
use Illuminate\Support\Facades\DB; | ||
|
||
class CountUsedTags | ||
{ | ||
private ?CarbonInterface $since = null; | ||
private ?CarbonInterface $until = null; | ||
private bool $includesMetadata = false; | ||
|
||
public function __construct(private ?User $operator, private User $user) | ||
{ | ||
} | ||
|
||
public function since(?CarbonInterface $since): self | ||
{ | ||
$this->since = $since; | ||
|
||
return $this; | ||
} | ||
|
||
public function until(?CarbonInterface $until): self | ||
{ | ||
$this->until = $until; | ||
|
||
return $this; | ||
} | ||
|
||
public function setIncludesMetadata(bool $includesMetadata): self | ||
{ | ||
$this->includesMetadata = $includesMetadata; | ||
|
||
return $this; | ||
} | ||
|
||
public function query() | ||
{ | ||
if ($this->includesMetadata) { | ||
return $this->queryToEjaculationsAndMetadata(); | ||
} else { | ||
return $this->queryToEjaculations(); | ||
} | ||
} | ||
|
||
private function queryToEjaculations() | ||
{ | ||
$dateCondition = [ | ||
['ejaculated_date', '<', $this->until ?: now()->addMonth()->startOfMonth()], | ||
]; | ||
if ($this->since !== null) { | ||
$dateCondition[] = ['ejaculated_date', '>=', $this->since]; | ||
} | ||
|
||
$query = DB::table('ejaculations') | ||
->join('ejaculation_tag', 'ejaculations.id', '=', 'ejaculation_tag.ejaculation_id') | ||
->join('tags', 'ejaculation_tag.tag_id', '=', 'tags.id') | ||
->selectRaw('tags.name, count(*) as count') | ||
->where('ejaculations.user_id', $this->user->id) | ||
->where($dateCondition); | ||
if ($this->operator === null || $this->user->id !== $this->operator->id) { | ||
$query = $query->where('ejaculations.is_private', false); | ||
} | ||
|
||
return $query->groupBy('tags.name') | ||
->orderBy('count', 'desc') | ||
->limit(10) | ||
->get(); | ||
} | ||
|
||
private function queryToEjaculationsAndMetadata() | ||
{ | ||
$sql = <<<SQL | ||
SELECT tg.name, count(*) count | ||
FROM ( | ||
SELECT DISTINCT ej.id ej_id, tg.id tg_id | ||
FROM ejaculations ej | ||
INNER JOIN (SELECT id FROM ejaculations WHERE user_id = ? AND is_private IN (?, ?) AND ejaculated_date >= ? AND ejaculated_date < ?) ej2 ON ej.id = ej2.id | ||
INNER JOIN ejaculation_tag et ON ej.id = et.ejaculation_id | ||
INNER JOIN tags tg ON et.tag_id = tg.id | ||
UNION | ||
SELECT DISTINCT ej.id ej_id, tg.id tg_id | ||
FROM ejaculations ej | ||
INNER JOIN (SELECT id FROM ejaculations WHERE user_id = ? AND is_private IN (?, ?) AND ejaculated_date >= ? AND ejaculated_date < ?) ej2 ON ej.id = ej2.id | ||
INNER JOIN metadata_tag mt ON ej.link = mt.metadata_url | ||
INNER JOIN tags tg ON mt.tag_id = tg.id | ||
) ej_with_tag_id | ||
INNER JOIN tags tg ON ej_with_tag_id.tg_id = tg.id | ||
GROUP BY tg.name | ||
ORDER BY count DESC | ||
LIMIT 10 | ||
SQL; | ||
|
||
$dateSince = $this->since ?: Carbon::minValue(); | ||
$dateUntil = $this->until ?: now()->addMonth()->startOfMonth(); | ||
|
||
return DB::select(DB::raw($sql), [ | ||
$this->user->id, false, $this->operator !== null && $this->user->id === $this->operator->id, $dateSince, $dateUntil, | ||
$this->user->id, false, $this->operator !== null && $this->user->id === $this->operator->id, $dateSince, $dateUntil | ||
]); | ||
} | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,28 @@ | ||
<?php | ||
declare(strict_types=1); | ||
|
||
namespace App\Queries; | ||
|
||
use App\Ejaculation; | ||
use App\User; | ||
use Illuminate\Support\Facades\DB; | ||
|
||
class EjaculationCountByDay | ||
{ | ||
public function __construct(private User $user) | ||
{ | ||
} | ||
|
||
public function query() | ||
{ | ||
return Ejaculation::select(DB::raw( | ||
<<<'SQL' | ||
to_char(ejaculated_date, 'YYYY-MM-DD') AS "date", | ||
count(*) AS "count" | ||
SQL | ||
)) | ||
->where('user_id', $this->user->id) | ||
->groupBy(DB::raw("to_char(ejaculated_date, 'YYYY-MM-DD')")) | ||
->orderBy(DB::raw("to_char(ejaculated_date, 'YYYY-MM-DD')")); | ||
} | ||
} |
Oops, something went wrong.