Merge branch 'develop'
This commit is contained in:
commit
55bd35ea49
4
.dockerignore
Normal file
4
.dockerignore
Normal file
@ -0,0 +1,4 @@
|
|||||||
|
.idea
|
||||||
|
.git
|
||||||
|
.gitignore
|
||||||
|
.gitattributes
|
14
Dockerfile
Normal file
14
Dockerfile
Normal file
@ -0,0 +1,14 @@
|
|||||||
|
FROM php:7.1-apache
|
||||||
|
|
||||||
|
ENV APACHE_DOCUMENT_ROOT /var/www/html/public
|
||||||
|
|
||||||
|
RUN apt-get update \
|
||||||
|
&& apt-get install -y libpq-dev \
|
||||||
|
&& docker-php-ext-install pdo_pgsql \
|
||||||
|
&& curl -sS https://getcomposer.org/installer | php \
|
||||||
|
&& mv composer.phar /usr/local/bin/composer \
|
||||||
|
&& sed -ri -e 's!/var/www/html!${APACHE_DOCUMENT_ROOT}!g' /etc/apache2/sites-available/*.conf \
|
||||||
|
&& sed -ri -e 's!/var/www/!${APACHE_DOCUMENT_ROOT}!g' /etc/apache2/apache2.conf /etc/apache2/conf-available/*.conf \
|
||||||
|
&& a2enmod rewrite
|
||||||
|
|
||||||
|
WORKDIR /var/www/html
|
24
app/Events/LinkDiscovered.php
Normal file
24
app/Events/LinkDiscovered.php
Normal file
@ -0,0 +1,24 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
namespace App\Events;
|
||||||
|
|
||||||
|
use Illuminate\Queue\SerializesModels;
|
||||||
|
use Illuminate\Foundation\Events\Dispatchable;
|
||||||
|
use Illuminate\Contracts\Broadcasting\ShouldBroadcast;
|
||||||
|
|
||||||
|
class LinkDiscovered
|
||||||
|
{
|
||||||
|
use Dispatchable, SerializesModels;
|
||||||
|
|
||||||
|
public $url;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Create a new event instance.
|
||||||
|
*
|
||||||
|
* @param string $url
|
||||||
|
*/
|
||||||
|
public function __construct(string $url)
|
||||||
|
{
|
||||||
|
$this->url = $url;
|
||||||
|
}
|
||||||
|
}
|
@ -2,6 +2,7 @@
|
|||||||
|
|
||||||
namespace App\Http\Controllers;
|
namespace App\Http\Controllers;
|
||||||
|
|
||||||
|
use App\Events\LinkDiscovered;
|
||||||
use App\Tag;
|
use App\Tag;
|
||||||
use App\User;
|
use App\User;
|
||||||
use Carbon\Carbon;
|
use Carbon\Carbon;
|
||||||
@ -71,6 +72,10 @@ class EjaculationController extends Controller
|
|||||||
}
|
}
|
||||||
$ejaculation->tags()->sync($tagIds);
|
$ejaculation->tags()->sync($tagIds);
|
||||||
|
|
||||||
|
if (!empty($ejaculation->link)) {
|
||||||
|
event(new LinkDiscovered($ejaculation->link));
|
||||||
|
}
|
||||||
|
|
||||||
return redirect()->route('checkin.show', ['id' => $ejaculation->id])->with('status', 'チェックインしました!');
|
return redirect()->route('checkin.show', ['id' => $ejaculation->id])->with('status', 'チェックインしました!');
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -148,6 +153,10 @@ class EjaculationController extends Controller
|
|||||||
}
|
}
|
||||||
$ejaculation->tags()->sync($tagIds);
|
$ejaculation->tags()->sync($tagIds);
|
||||||
|
|
||||||
|
if (!empty($ejaculation->link)) {
|
||||||
|
event(new LinkDiscovered($ejaculation->link));
|
||||||
|
}
|
||||||
|
|
||||||
return redirect()->route('checkin.show', ['id' => $ejaculation->id])->with('status', 'チェックインを修正しました!');
|
return redirect()->route('checkin.show', ['id' => $ejaculation->id])->with('status', 'チェックインを修正しました!');
|
||||||
}
|
}
|
||||||
|
|
||||||
|
54
app/Listeners/LinkCollector.php
Normal file
54
app/Listeners/LinkCollector.php
Normal file
@ -0,0 +1,54 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
namespace App\Listeners;
|
||||||
|
|
||||||
|
use App\Events\LinkDiscovered;
|
||||||
|
use App\Metadata;
|
||||||
|
use App\MetadataResolver\MetadataResolver;
|
||||||
|
use App\Utilities\Formatter;
|
||||||
|
use Illuminate\Queue\InteractsWithQueue;
|
||||||
|
use Illuminate\Contracts\Queue\ShouldQueue;
|
||||||
|
|
||||||
|
class LinkCollector
|
||||||
|
{
|
||||||
|
/** @var Formatter */
|
||||||
|
private $formatter;
|
||||||
|
/** @var MetadataResolver */
|
||||||
|
private $metadataResolver;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Create the event listener.
|
||||||
|
*
|
||||||
|
* @return void
|
||||||
|
*/
|
||||||
|
public function __construct(Formatter $formatter, MetadataResolver $metadataResolver)
|
||||||
|
{
|
||||||
|
$this->formatter = $formatter;
|
||||||
|
$this->metadataResolver = $metadataResolver;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Handle the event.
|
||||||
|
*
|
||||||
|
* @param LinkDiscovered $event
|
||||||
|
* @return void
|
||||||
|
*/
|
||||||
|
public function handle(LinkDiscovered $event)
|
||||||
|
{
|
||||||
|
// URLの正規化
|
||||||
|
$url = $this->formatter->normalizeUrl($event->url);
|
||||||
|
|
||||||
|
// 無かったら取得
|
||||||
|
// TODO: ある程度古かったら再取得とかありだと思う
|
||||||
|
$metadata = Metadata::find($url);
|
||||||
|
if ($metadata == null) {
|
||||||
|
$resolved = $this->metadataResolver->resolve($url);
|
||||||
|
Metadata::create([
|
||||||
|
'url' => $url,
|
||||||
|
'title' => $resolved->title,
|
||||||
|
'description' => $resolved->description,
|
||||||
|
'image' => $resolved->image
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
15
app/Metadata.php
Normal file
15
app/Metadata.php
Normal file
@ -0,0 +1,15 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
namespace App;
|
||||||
|
|
||||||
|
use Illuminate\Database\Eloquent\Model;
|
||||||
|
|
||||||
|
class Metadata extends Model
|
||||||
|
{
|
||||||
|
public $incrementing = false;
|
||||||
|
protected $primaryKey = 'url';
|
||||||
|
protected $keyType = 'string';
|
||||||
|
|
||||||
|
protected $fillable = ['url', 'title', 'description', 'image'];
|
||||||
|
protected $visible = ['url', 'title', 'description', 'image'];
|
||||||
|
}
|
30
app/MetadataResolver/KomifloResolver.php
Normal file
30
app/MetadataResolver/KomifloResolver.php
Normal file
@ -0,0 +1,30 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
namespace App\MetadataResolver;
|
||||||
|
|
||||||
|
class KomifloResolver implements Resolver
|
||||||
|
{
|
||||||
|
public function resolve(string $url): Metadata
|
||||||
|
{
|
||||||
|
if (preg_match('~komiflo\.com(?:/#!)?/comics/(\\d+)~', $url, $matches) !== 1) {
|
||||||
|
throw new \RuntimeException("Unmatched URL Pattern: $url");
|
||||||
|
}
|
||||||
|
$id = $matches[1];
|
||||||
|
|
||||||
|
$client = new \GuzzleHttp\Client();
|
||||||
|
$res = $client->get('https://api.komiflo.com/content/id/' . $id);
|
||||||
|
if ($res->getStatusCode() === 200) {
|
||||||
|
$json = json_decode($res->getBody()->getContents(), true);
|
||||||
|
$metadata = new Metadata();
|
||||||
|
|
||||||
|
$metadata->title = $json['content']['data']['title'] ?? '';
|
||||||
|
$metadata->description = ($json['content']['attributes']['artists']['children'][0]['data']['name'] ?? '?') .
|
||||||
|
' - ' .
|
||||||
|
($json['content']['parents'][0]['data']['title'] ?? '?');
|
||||||
|
|
||||||
|
return $metadata;
|
||||||
|
} else {
|
||||||
|
throw new \RuntimeException("{$res->getStatusCode()}: $url");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
@ -7,6 +7,7 @@ class MetadataResolver implements Resolver
|
|||||||
public $rules = [
|
public $rules = [
|
||||||
'~(((sp\.)?seiga\.nicovideo\.jp/seiga(/#!)?|nico\.ms))/im~' => NicoSeigaResolver::class,
|
'~(((sp\.)?seiga\.nicovideo\.jp/seiga(/#!)?|nico\.ms))/im~' => NicoSeigaResolver::class,
|
||||||
'~nijie\.info/view\.php~' => NijieResolver::class,
|
'~nijie\.info/view\.php~' => NijieResolver::class,
|
||||||
|
'~komiflo\.com(/#!)?/comics/(\\d+)~' => KomifloResolver::class,
|
||||||
'/.*/' => OGPResolver::class
|
'/.*/' => OGPResolver::class
|
||||||
];
|
];
|
||||||
|
|
||||||
|
@ -13,9 +13,9 @@ class EventServiceProvider extends ServiceProvider
|
|||||||
* @var array
|
* @var array
|
||||||
*/
|
*/
|
||||||
protected $listen = [
|
protected $listen = [
|
||||||
'App\Events\Event' => [
|
'App\Events\LinkDiscovered' => [
|
||||||
'App\Listeners\EventListener',
|
'App\Listeners\LinkCollector'
|
||||||
],
|
]
|
||||||
];
|
];
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
@ -36,4 +36,29 @@ class Formatter
|
|||||||
{
|
{
|
||||||
return $this->linkify->processUrls($text);
|
return $this->linkify->processUrls($text);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* URLを正規化します。
|
||||||
|
* @param string $url URL
|
||||||
|
* @return string 正規化されたURL
|
||||||
|
*/
|
||||||
|
public function normalizeUrl($url)
|
||||||
|
{
|
||||||
|
// Decode
|
||||||
|
$url = urldecode($url);
|
||||||
|
|
||||||
|
// Remove Hashbang
|
||||||
|
$url = preg_replace('~/#!/~u', '/', $url);
|
||||||
|
|
||||||
|
// Sort query parameters
|
||||||
|
$query = parse_url($url, PHP_URL_QUERY);
|
||||||
|
if (!empty($query)) {
|
||||||
|
$url = str_replace_last('?' . $query, '', $url);
|
||||||
|
parse_str($query, $params);
|
||||||
|
ksort($params);
|
||||||
|
$url = $url . '?' . http_build_query($params);
|
||||||
|
}
|
||||||
|
|
||||||
|
return $url;
|
||||||
|
}
|
||||||
}
|
}
|
@ -0,0 +1,36 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
use Illuminate\Support\Facades\Schema;
|
||||||
|
use Illuminate\Database\Schema\Blueprint;
|
||||||
|
use Illuminate\Database\Migrations\Migration;
|
||||||
|
|
||||||
|
class CreateMetadataTable extends Migration
|
||||||
|
{
|
||||||
|
/**
|
||||||
|
* Run the migrations.
|
||||||
|
*
|
||||||
|
* @return void
|
||||||
|
*/
|
||||||
|
public function up()
|
||||||
|
{
|
||||||
|
Schema::create('metadata', function (Blueprint $table) {
|
||||||
|
$table->string('url');
|
||||||
|
$table->string('title');
|
||||||
|
$table->string('description');
|
||||||
|
$table->string('image');
|
||||||
|
$table->timestamps();
|
||||||
|
|
||||||
|
$table->index('url');
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Reverse the migrations.
|
||||||
|
*
|
||||||
|
* @return void
|
||||||
|
*/
|
||||||
|
public function down()
|
||||||
|
{
|
||||||
|
Schema::dropIfExists('metadata');
|
||||||
|
}
|
||||||
|
}
|
38
docker-compose.yml
Normal file
38
docker-compose.yml
Normal file
@ -0,0 +1,38 @@
|
|||||||
|
version: "3"
|
||||||
|
|
||||||
|
services:
|
||||||
|
web:
|
||||||
|
build: .
|
||||||
|
environment:
|
||||||
|
DB_CONNECTION: pgsql
|
||||||
|
DB_HOST: db
|
||||||
|
DB_PORT: 5432
|
||||||
|
DB_DATABASE: tissue
|
||||||
|
DB_USERNAME: tissue
|
||||||
|
DB_PASSWORD: tissue
|
||||||
|
volumes:
|
||||||
|
- .:/var/www/html
|
||||||
|
networks:
|
||||||
|
- backend
|
||||||
|
ports:
|
||||||
|
- 4545:80
|
||||||
|
restart: always
|
||||||
|
depends_on:
|
||||||
|
- db
|
||||||
|
db:
|
||||||
|
image: postgres:10-alpine
|
||||||
|
environment:
|
||||||
|
POSTGRES_DB: tissue
|
||||||
|
POSTGRES_USER: tissue
|
||||||
|
POSTGRES_PASSWORD: tissue
|
||||||
|
volumes:
|
||||||
|
- db:/var/lib/postgresql/data
|
||||||
|
networks:
|
||||||
|
- backend
|
||||||
|
restart: always
|
||||||
|
|
||||||
|
networks:
|
||||||
|
backend:
|
||||||
|
|
||||||
|
volumes:
|
||||||
|
db:
|
@ -26,14 +26,14 @@
|
|||||||
<!-- span -->
|
<!-- span -->
|
||||||
<div class="d-flex justify-content-between">
|
<div class="d-flex justify-content-between">
|
||||||
<h5>{{ $ejaculatedSpan ?? '精通' }} <small class="text-muted">{{ $ejaculation->before_date }}{{ !empty($ejaculation->before_date) ? ' ~ ' : '' }}{{ $ejaculation->ejaculated_date->format('Y/m/d H:i') }}</small></h5>
|
<h5>{{ $ejaculatedSpan ?? '精通' }} <small class="text-muted">{{ $ejaculation->before_date }}{{ !empty($ejaculation->before_date) ? ' ~ ' : '' }}{{ $ejaculation->ejaculated_date->format('Y/m/d H:i') }}</small></h5>
|
||||||
@if ($user->isMe())
|
|
||||||
<div>
|
<div>
|
||||||
<a class="text-secondary timeline-action-item" href="{{ route('checkin', ['link' => $ejaculation->link, 'tags' => $ejaculation->textTags()]) }}"><span class="oi oi-reload" data-toggle="tooltip" data-placement="bottom" title="同じオカズでチェックイン"></span></a>
|
<a class="text-secondary timeline-action-item" href="{{ route('checkin', ['link' => $ejaculation->link, 'tags' => $ejaculation->textTags()]) }}"><span class="oi oi-reload" data-toggle="tooltip" data-placement="bottom" title="同じオカズでチェックイン"></span></a>
|
||||||
|
@if ($user->isMe())
|
||||||
<a class="text-secondary timeline-action-item" href="{{ route('checkin.edit', ['id' => $ejaculation->id]) }}"><span class="oi oi-pencil" data-toggle="tooltip" data-placement="bottom" title="修正"></span></a>
|
<a class="text-secondary timeline-action-item" href="{{ route('checkin.edit', ['id' => $ejaculation->id]) }}"><span class="oi oi-pencil" data-toggle="tooltip" data-placement="bottom" title="修正"></span></a>
|
||||||
<a class="text-secondary timeline-action-item" href="#" data-toggle="modal" data-target="#deleteCheckinModal" data-id="{{ $ejaculation->id }}" data-date="{{ $ejaculation->ejaculated_date }}"><span class="oi oi-trash" data-toggle="tooltip" data-placement="bottom" title="削除"></span></a>
|
<a class="text-secondary timeline-action-item" href="#" data-toggle="modal" data-target="#deleteCheckinModal" data-id="{{ $ejaculation->id }}" data-date="{{ $ejaculation->ejaculated_date }}"><span class="oi oi-trash" data-toggle="tooltip" data-placement="bottom" title="削除"></span></a>
|
||||||
</div>
|
|
||||||
@endif
|
@endif
|
||||||
</div>
|
</div>
|
||||||
|
</div>
|
||||||
<!-- tags -->
|
<!-- tags -->
|
||||||
@if ($ejaculation->is_private || $ejaculation->tags->isNotEmpty())
|
@if ($ejaculation->is_private || $ejaculation->tags->isNotEmpty())
|
||||||
<p class="mb-2">
|
<p class="mb-2">
|
||||||
|
@ -37,6 +37,9 @@
|
|||||||
<a href="{{ route('user.profile', ['id' => $ejaculation->user->name]) }}" class="text-dark"><img src="{{ $ejaculation->user->getProfileImageUrl(30) }}" width="30" height="30" class="rounded d-inline-block align-bottom"> @{{ $ejaculation->user->name }}</a>
|
<a href="{{ route('user.profile', ['id' => $ejaculation->user->name]) }}" class="text-dark"><img src="{{ $ejaculation->user->getProfileImageUrl(30) }}" width="30" height="30" class="rounded d-inline-block align-bottom"> @{{ $ejaculation->user->name }}</a>
|
||||||
<a href="{{ route('checkin.show', ['id' => $ejaculation->id]) }}" class="text-muted"><small>{{ $ejaculation->ejaculated_date->format('Y/m/d H:i') }}</small></a>
|
<a href="{{ route('checkin.show', ['id' => $ejaculation->id]) }}" class="text-muted"><small>{{ $ejaculation->ejaculated_date->format('Y/m/d H:i') }}</small></a>
|
||||||
</h5>
|
</h5>
|
||||||
|
<div>
|
||||||
|
<a class="text-secondary timeline-action-item" href="{{ route('checkin', ['link' => $ejaculation->link, 'tags' => $ejaculation->textTags()]) }}"><span class="oi oi-reload" data-toggle="tooltip" data-placement="bottom" title="同じオカズでチェックイン"></span></a>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<!-- tags -->
|
<!-- tags -->
|
||||||
@if ($ejaculation->tags->isNotEmpty())
|
@if ($ejaculation->tags->isNotEmpty())
|
||||||
|
@ -12,14 +12,14 @@
|
|||||||
<!-- span -->
|
<!-- span -->
|
||||||
<div class="d-flex justify-content-between">
|
<div class="d-flex justify-content-between">
|
||||||
<h5>{{ $ejaculation->ejaculated_span ?? '精通' }} <a href="{{ route('checkin.show', ['id' => $ejaculation->id]) }}" class="text-muted"><small>{{ $ejaculation->before_date }}{{ !empty($ejaculation->before_date) ? ' ~ ' : '' }}{{ $ejaculation->ejaculated_date->format('Y/m/d H:i') }}</small></a></h5>
|
<h5>{{ $ejaculation->ejaculated_span ?? '精通' }} <a href="{{ route('checkin.show', ['id' => $ejaculation->id]) }}" class="text-muted"><small>{{ $ejaculation->before_date }}{{ !empty($ejaculation->before_date) ? ' ~ ' : '' }}{{ $ejaculation->ejaculated_date->format('Y/m/d H:i') }}</small></a></h5>
|
||||||
@if ($user->isMe())
|
|
||||||
<div>
|
<div>
|
||||||
<a class="text-secondary timeline-action-item" href="{{ route('checkin', ['link' => $ejaculation->link, 'tags' => $ejaculation->textTags()]) }}"><span class="oi oi-reload" data-toggle="tooltip" data-placement="bottom" title="同じオカズでチェックイン"></span></a>
|
<a class="text-secondary timeline-action-item" href="{{ route('checkin', ['link' => $ejaculation->link, 'tags' => $ejaculation->textTags()]) }}"><span class="oi oi-reload" data-toggle="tooltip" data-placement="bottom" title="同じオカズでチェックイン"></span></a>
|
||||||
|
@if ($user->isMe())
|
||||||
<a class="text-secondary timeline-action-item" href="{{ route('checkin.edit', ['id' => $ejaculation->id]) }}"><span class="oi oi-pencil" data-toggle="tooltip" data-placement="bottom" title="修正"></span></a>
|
<a class="text-secondary timeline-action-item" href="{{ route('checkin.edit', ['id' => $ejaculation->id]) }}"><span class="oi oi-pencil" data-toggle="tooltip" data-placement="bottom" title="修正"></span></a>
|
||||||
<a class="text-secondary timeline-action-item" href="#" data-toggle="modal" data-target="#deleteCheckinModal" data-id="{{ $ejaculation->id }}" data-date="{{ $ejaculation->ejaculated_date }}"><span class="oi oi-trash" data-toggle="tooltip" data-placement="bottom" title="削除"></span></a>
|
<a class="text-secondary timeline-action-item" href="#" data-toggle="modal" data-target="#deleteCheckinModal" data-id="{{ $ejaculation->id }}" data-date="{{ $ejaculation->ejaculated_date }}"><span class="oi oi-trash" data-toggle="tooltip" data-placement="bottom" title="削除"></span></a>
|
||||||
</div>
|
|
||||||
@endif
|
@endif
|
||||||
</div>
|
</div>
|
||||||
|
</div>
|
||||||
<!-- tags -->
|
<!-- tags -->
|
||||||
@if ($ejaculation->is_private || $ejaculation->tags->isNotEmpty())
|
@if ($ejaculation->is_private || $ejaculation->tags->isNotEmpty())
|
||||||
<p class="mb-2">
|
<p class="mb-2">
|
||||||
|
@ -1,6 +1,7 @@
|
|||||||
<?php
|
<?php
|
||||||
|
|
||||||
use App\MetadataResolver\MetadataResolver;
|
use App\MetadataResolver\MetadataResolver;
|
||||||
|
use App\Utilities\Formatter;
|
||||||
use Illuminate\Http\Request;
|
use Illuminate\Http\Request;
|
||||||
|
|
||||||
/*
|
/*
|
||||||
@ -18,13 +19,23 @@ Route::middleware('auth:api')->get('/user', function (Request $request) {
|
|||||||
return $request->user();
|
return $request->user();
|
||||||
});
|
});
|
||||||
|
|
||||||
Route::get('/checkin/card', function (Request $request, MetadataResolver $resolver) {
|
Route::get('/checkin/card', function (Request $request, MetadataResolver $resolver, Formatter $formatter) {
|
||||||
$request->validate([
|
$request->validate([
|
||||||
'url:required|url'
|
'url:required|url'
|
||||||
]);
|
]);
|
||||||
$url = $request->input('url');
|
$url = $formatter->normalizeUrl($request->input('url'));
|
||||||
|
|
||||||
|
$metadata = App\Metadata::find($url);
|
||||||
|
if ($metadata == null) {
|
||||||
|
$resolved = $resolver->resolve($url);
|
||||||
|
$metadata = App\Metadata::create([
|
||||||
|
'url' => $url,
|
||||||
|
'title' => $resolved->title,
|
||||||
|
'description' => $resolved->description,
|
||||||
|
'image' => $resolved->image
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
|
||||||
$metadata = $resolver->resolve($url);
|
|
||||||
$response = response()->json($metadata);
|
$response = response()->json($metadata);
|
||||||
if (!config('app.debug')) {
|
if (!config('app.debug')) {
|
||||||
$response = $response->setCache(['public' => true, 'max_age' => 86400]);
|
$response = $response->setCache(['public' => true, 'max_age' => 86400]);
|
||||||
|
Loading…
Reference in New Issue
Block a user