Skip to content

Commit

Permalink
Merge pull request #1142 from shikorism/feature/stats-api
Browse files Browse the repository at this point in the history
User stats API
  • Loading branch information
shibafu528 authored Dec 25, 2023
2 parents b86a8e9 + 9a8a582 commit 74816b6
Show file tree
Hide file tree
Showing 8 changed files with 634 additions and 0 deletions.
53 changes: 53 additions & 0 deletions app/Http/Controllers/Api/V1/UserStats/DailyCheckinSummary.php
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 app/Http/Controllers/Api/V1/UserStats/HourlyCheckinSummary.php
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 app/Http/Controllers/Api/V1/UserStats/MostlyUsedCheckinTags.php
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);
}
}
74 changes: 74 additions & 0 deletions app/Http/Controllers/Api/V1/UserStats/MostlyUsedLinks.php
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
]);
}
}
107 changes: 107 additions & 0 deletions app/Queries/CountUsedTags.php
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
]);
}
}
28 changes: 28 additions & 0 deletions app/Queries/EjaculationCountByDay.php
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')"));
}
}
Loading

0 comments on commit 74816b6

Please sign in to comment.