Merge pull request #347 from shikorism/develop

Release 20200516.1500
This commit is contained in:
shibafu 2020-05-16 15:07:05 +09:00 committed by GitHub
commit 70dc74ecd4
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
9 changed files with 290 additions and 106 deletions

View File

@ -0,0 +1,113 @@
<?php
namespace App\Console\Commands;
use App\Tag;
use Illuminate\Console\Command;
use Illuminate\Support\Facades\DB;
class DedupTags extends Command
{
/**
* The name and signature of the console command.
*
* @var string
*/
protected $signature = 'tissue:tag:dedup {--dry-run}';
/**
* The console command description.
*
* @var string
*/
protected $description = 'Deduplicate tags';
/**
* Create a new command instance.
*
* @return void
*/
public function __construct()
{
parent::__construct();
}
/**
* Execute the console command.
*
* @return mixed
*/
public function handle()
{
if ($this->option('dry-run')) {
$this->warn('dry-runモードで実行します。');
} else {
if (!$this->confirm('dry-runオプションが付いてないけど、本当に実行しますか')) {
return;
}
}
DB::transaction(function () {
$duplicatedTags = DB::table('tags')
->select('name', DB::raw('count(*)'))
->groupBy('name')
->having(DB::raw('count(*)'), '>=', 2)
->get();
$this->info($duplicatedTags->count() . ' duplicated tags found.');
foreach ($duplicatedTags as $tag) {
$this->line('Tag name: ' . $tag->name);
$tagIds = Tag::where('name', $tag->name)->orderBy('id')->pluck('id');
$newId = $tagIds->first();
$dropIds = $tagIds->slice(1);
$this->line(' New ID: ' . $newId);
$this->line(' Drop IDs: ' . $dropIds->implode(', '));
if ($this->option('dry-run')) {
continue;
}
// 同じタグ名でIDが違うものについて、全て統一する
foreach (['ejaculation_tag', 'metadata_tag'] as $table) {
DB::table($table)
->whereIn('tag_id', $dropIds)
->update(['tag_id' => $newId]);
}
DB::table('tags')->whereIn('id', $dropIds)->delete();
// 統一した上で、重複しているレコードを削除する
DB::delete(
<<<SQL
DELETE FROM ejaculation_tag
WHERE id IN (
SELECT id
FROM (
SELECT id, row_number() OVER (PARTITION BY ejaculation_id, tag_id ORDER BY id) AS ord
FROM ejaculation_tag
) t
WHERE ord > 1
)
SQL
);
DB::delete(
<<<SQL
DELETE FROM metadata_tag
WHERE id IN (
SELECT id
FROM (
SELECT id, row_number() OVER (PARTITION BY metadata_url, tag_id ORDER BY id) AS ord
FROM metadata_tag
) t
WHERE ord > 1
)
SQL
);
}
});
$this->info('Done!');
}
}

View File

@ -2,55 +2,18 @@
namespace App\Http\Controllers\Api;
use App\Metadata;
use App\MetadataResolver\MetadataResolver;
use App\Tag;
use App\Utilities\Formatter;
use App\Services\MetadataResolveService;
use Illuminate\Http\Request;
class CardController
{
/**
* @var MetadataResolver
*/
private $resolver;
/**
* @var Formatter
*/
private $formatter;
public function __construct(MetadataResolver $resolver, Formatter $formatter)
{
$this->resolver = $resolver;
$this->formatter = $formatter;
}
public function show(Request $request)
public function show(Request $request, MetadataResolveService $service)
{
$request->validate([
'url:required|url'
]);
$url = $this->formatter->normalizeUrl($request->input('url'));
$metadata = Metadata::find($url);
if ($metadata === null || ($metadata->expires_at !== null && $metadata->expires_at < now())) {
$resolved = $this->resolver->resolve($url);
$metadata = Metadata::updateOrCreate(['url' => $url], [
'title' => $resolved->title,
'description' => $resolved->description,
'image' => $resolved->image,
'expires_at' => $resolved->expires_at
]);
$tagIds = [];
foreach ($resolved->normalizedTags() as $tagName) {
$tag = Tag::firstOrCreate(['name' => $tagName]);
$tagIds[] = $tag->id;
}
$metadata->tags()->sync($tagIds);
}
$metadata = $service->execute($request->input('url'));
$metadata->load('tags');
$response = response($metadata);

View File

@ -165,6 +165,7 @@ SQL
}
$ejaculations = $query->orderBy('ejaculated_date', 'desc')
->with('tags')
->withLikes()
->paginate(20);
return view('user.profile')->with(compact('user', 'ejaculations'));

View File

@ -3,32 +3,23 @@
namespace App\Listeners;
use App\Events\LinkDiscovered;
use App\Metadata;
use App\MetadataResolver\MetadataResolver;
use App\Tag;
use App\Utilities\Formatter;
use GuzzleHttp\Exception\TransferException;
use App\Services\MetadataResolveService;
use Illuminate\Contracts\Queue\ShouldQueue;
use Illuminate\Queue\InteractsWithQueue;
use Illuminate\Support\Facades\Log;
class LinkCollector
{
/** @var Formatter */
private $formatter;
/** @var MetadataResolver */
private $metadataResolver;
/** @var MetadataResolveService */
private $metadataResolveService;
/**
* Create the event listener.
*
* @param Formatter $formatter
* @param MetadataResolver $metadataResolver
* @param MetadataResolveService $metadataResolveService
*/
public function __construct(Formatter $formatter, MetadataResolver $metadataResolver)
public function __construct(MetadataResolveService $metadataResolveService)
{
$this->formatter = $formatter;
$this->metadataResolver = $metadataResolver;
$this->metadataResolveService = $metadataResolveService;
}
/**
@ -39,33 +30,11 @@ class LinkCollector
*/
public function handle(LinkDiscovered $event)
{
// URLの正規化
$url = $this->formatter->normalizeUrl($event->url);
// 無かったら取得
// TODO: ある程度古かったら再取得とかありだと思う
$metadata = Metadata::find($url);
if ($metadata == null || ($metadata->expires_at !== null && $metadata->expires_at < now())) {
try {
$resolved = $this->metadataResolver->resolve($url);
$metadata = Metadata::updateOrCreate(['url' => $url], [
'title' => $resolved->title,
'description' => $resolved->description,
'image' => $resolved->image,
'expires_at' => $resolved->expires_at
]);
$tagIds = [];
foreach ($resolved->normalizedTags() as $tagName) {
$tag = Tag::firstOrCreate(['name' => $tagName]);
$tagIds[] = $tag->id;
}
$metadata->tags()->sync($tagIds);
} catch (TransferException $e) {
// 何らかの通信エラーによってメタデータの取得に失敗した時、とりあえずエラーログにURLを残す
Log::error(self::class . ': メタデータの取得に失敗 URL=' . $url);
report($e);
}
try {
$this->metadataResolveService->execute($event->url);
} catch (\Exception $e) {
// 今のところこのイベントは同期実行されるので、上流をクラッシュさせないために雑catchする
report($e);
}
}
}

View File

@ -0,0 +1,58 @@
<?php
namespace App\Services;
use App\Metadata;
use App\MetadataResolver\MetadataResolver;
use App\Tag;
use App\Utilities\Formatter;
use GuzzleHttp\Exception\TransferException;
use Illuminate\Support\Facades\Log;
class MetadataResolveService
{
/** @var MetadataResolver */
private $resolver;
/** @var Formatter */
private $formatter;
public function __construct(MetadataResolver $resolver, Formatter $formatter)
{
$this->resolver = $resolver;
$this->formatter = $formatter;
}
public function execute(string $url): Metadata
{
// URLの正規化
$url = $this->formatter->normalizeUrl($url);
// 無かったら取得
// TODO: ある程度古かったら再取得とかありだと思う
$metadata = Metadata::find($url);
if ($metadata == null || ($metadata->expires_at !== null && $metadata->expires_at < now())) {
try {
$resolved = $this->resolver->resolve($url);
$metadata = Metadata::updateOrCreate(['url' => $url], [
'title' => $resolved->title,
'description' => $resolved->description,
'image' => $resolved->image,
'expires_at' => $resolved->expires_at
]);
$tagIds = [];
foreach ($resolved->normalizedTags() as $tagName) {
$tag = Tag::firstOrCreate(['name' => $tagName]);
$tagIds[] = $tag->id;
}
$metadata->tags()->sync($tagIds);
} catch (TransferException $e) {
// 何らかの通信エラーによってメタデータの取得に失敗した時、とりあえずエラーログにURLを残す
Log::error(self::class . ': メタデータの取得に失敗 URL=' . $url);
throw $e;
}
}
return $metadata;
}
}

View File

@ -0,0 +1,38 @@
<?php
use Illuminate\Database\Migrations\Migration;
use Illuminate\Database\Schema\Blueprint;
use Illuminate\Support\Facades\Schema;
class AddUniqueConstraintToTagRelations extends Migration
{
/**
* Run the migrations.
*
* @return void
*/
public function up()
{
Schema::table('ejaculation_tag', function (Blueprint $table) {
$table->unique(['ejaculation_id', 'tag_id']);
});
Schema::table('metadata_tag', function (Blueprint $table) {
$table->unique(['metadata_url', 'tag_id']);
});
}
/**
* Reverse the migrations.
*
* @return void
*/
public function down()
{
Schema::table('ejaculation_tag', function (Blueprint $table) {
$table->dropUnique(['ejaculation_id', 'tag_id']);
});
Schema::table('metadata_tag', function (Blueprint $table) {
$table->dropUnique(['metadata_url', 'tag_id']);
});
}
}

29
public/maintenance.svg Normal file
View File

@ -0,0 +1,29 @@
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
<!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd">
<svg width="100%" height="100%" viewBox="0 0 500 500" version="1.1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" xml:space="preserve" xmlns:serif="http://www.serif.com/" style="fill-rule:evenodd;clip-rule:evenodd;stroke-linecap:round;stroke-linejoin:round;stroke-miterlimit:1.5;">
<path d="M140,210L140,405C140,413.279 133.279,420 125,420L95,420C86.721,420 80,413.279 80,405L80,210L140,210ZM80,100L80,95C80,86.721 86.721,80 95,80L125,80C133.279,80 140,86.721 140,95L140,100L80,100Z" style="fill:rgb(108,118,125);stroke:rgb(108,118,125);stroke-width:1px;"/>
<path d="M420,210L420,405C420,413.279 413.279,420 405,420L375,420C366.721,420 360,413.279 360,405L360,210L420,210ZM360,100L360,95C360,86.721 366.721,80 375,80L405,80C413.279,80 420,86.721 420,95L420,100L360,100Z" style="fill:rgb(108,118,125);stroke:rgb(108,118,125);stroke-width:1px;"/>
<g transform="matrix(1.033,0,0,1.62604,-11.6416,15.1937)">
<path d="M466.255,72.121C466.255,64.48 456.491,58.277 444.463,58.277L62.104,58.277C50.076,58.277 40.311,64.48 40.311,72.121L40.311,99.81C40.311,107.45 50.076,113.654 62.104,113.654L444.463,113.654C456.491,113.654 466.255,107.45 466.255,99.81L466.255,72.121Z" style="fill:rgb(253,193,7);"/>
<clipPath id="_clip1">
<path d="M466.255,72.121C466.255,64.48 456.491,58.277 444.463,58.277L62.104,58.277C50.076,58.277 40.311,64.48 40.311,72.121L40.311,99.81C40.311,107.45 50.076,113.654 62.104,113.654L444.463,113.654C456.491,113.654 466.255,107.45 466.255,99.81L466.255,72.121Z"/>
</clipPath>
<g clip-path="url(#_clip1)">
<g transform="matrix(0.968055,0,0,0.621753,-75.8552,-10.6967)">
<path d="M180,110.934L140,200L180,200L220,111L180,110.934Z" style="fill:rgb(108,118,125);"/>
</g>
<g transform="matrix(0.968055,0,0,0.621753,1.58913,-10.6967)">
<path d="M180,110.934L140,200L180,200L220,111L180,110.934Z" style="fill:rgb(108,118,125);"/>
</g>
<g transform="matrix(0.968055,0,0,0.621753,79.0335,-10.6967)">
<path d="M180,110.934L140,200L180,200L220,111L180,110.934Z" style="fill:rgb(108,118,125);"/>
</g>
<g transform="matrix(0.968055,0,0,0.621753,156.478,-10.6967)">
<path d="M180,110.934L140,200L180,200L220,111L180,110.934Z" style="fill:rgb(108,118,125);"/>
</g>
<g transform="matrix(0.968055,0,0,0.621753,233.922,-10.6967)">
<path d="M180,110.934L140,200L180,200L220,111L180,110.934Z" style="fill:rgb(108,118,125);"/>
</g>
</g>
</g>
</svg>

After

Width:  |  Height:  |  Size: 2.7 KiB

View File

@ -0,0 +1,11 @@
@extends('layouts.base')
@section('content')
<div class="container text-center">
<img src="{{ asset('maintenance.svg') }}" width="200" height="200" alt="Under maintenance">
<h2>ただいまメンテナンス中です</h2>
<hr>
<p class="mb-1">メンテナンス中はTissueをご利用いただくことができません。終了まで今しばらくお待ちください。</p>
<p>ご不便をおかけしておりますが、ご理解いただきますようお願いいたします。</p>
</div>
@endsection

View File

@ -172,32 +172,34 @@
</div>
</div>
@endauth
@guest
<!-- PC navbar -->
<div class="d-none d-lg-flex navbar-collapse">
<ul class="navbar-nav ml-auto mr-2">
<li class="nav-item">
<a href="{{ route('register') }}" class="nav-link">会員登録</a>
</li>
</ul>
<form class="form-inline">
<a href="{{ route('login') }}" class="btn btn-outline-secondary">ログイン</a>
</form>
</div>
<!-- SP navbar -->
<div class="d-lg-none">
<div class="row mt-2">
<div class="col">
<a class="btn btn-outline-secondary" href="{{ route('register') }}" role="button">会員登録</a>
</div>
<div class="col">
<form class="form-inline">
<a class="btn btn-outline-secondary" href="{{ route('login') }}">ログイン</a>
</form>
@if (!App::isDownForMaintenance())
@guest
<!-- PC navbar -->
<div class="d-none d-lg-flex navbar-collapse">
<ul class="navbar-nav ml-auto mr-2">
<li class="nav-item">
<a href="{{ route('register') }}" class="nav-link">会員登録</a>
</li>
</ul>
<form class="form-inline">
<a href="{{ route('login') }}" class="btn btn-outline-secondary">ログイン</a>
</form>
</div>
<!-- SP navbar -->
<div class="d-lg-none">
<div class="row mt-2">
<div class="col">
<a class="btn btn-outline-secondary" href="{{ route('register') }}" role="button">会員登録</a>
</div>
<div class="col">
<form class="form-inline">
<a class="btn btn-outline-secondary" href="{{ route('login') }}">ログイン</a>
</form>
</div>
</div>
</div>
</div>
@endguest
@endguest
@endif
</div>
</div>
</nav>