diff --git a/app/Http/Controllers/HomeController.php b/app/Http/Controllers/HomeController.php
index a5b257d..9fc045b 100644
--- a/app/Http/Controllers/HomeController.php
+++ b/app/Http/Controllers/HomeController.php
@@ -36,6 +36,31 @@ class HomeController extends Controller
$categories = Information::CATEGORIES;
if (Auth::check()) {
+ // チェックイン動向グラフ用のデータ取得
+ $groupByDay = Ejaculation::select(DB::raw(
+ <<<'SQL'
+to_char(ejaculated_date, 'YYYY/MM/DD') AS "date",
+count(*) AS "count"
+SQL
+ ))
+ ->join('users', function ($join) {
+ $join->on('users.id', '=', 'ejaculations.user_id')
+ ->where('users.accept_analytics', true);
+ })
+ ->where('ejaculated_date', '>=', now()->subDays(14))
+ ->groupBy(DB::raw("to_char(ejaculated_date, 'YYYY/MM/DD')"))
+ ->orderBy(DB::raw("to_char(ejaculated_date, 'YYYY/MM/DD')"))
+ ->get()
+ ->mapWithKeys(function ($item) {
+ return [$item['date'] => $item['count']];
+ });
+ $globalEjaculationCounts = [];
+ $day = Carbon::now()->subDays(29);
+ for ($i = 0; $i < 30; $i++) {
+ $globalEjaculationCounts[$day->format('Y/m/d') . ' の総チェックイン数'] = $groupByDay[$day->format('Y/m/d')] ?? 0;
+ $day->addDay();
+ }
+
// お惣菜コーナー用のデータ取得
$publicLinkedEjaculations = Ejaculation::join('users', 'users.id', '=', 'ejaculations.user_id')
->where('users.is_protected', false)
@@ -47,7 +72,7 @@ class HomeController extends Controller
->take(10)
->get();
- return view('home')->with(compact('informations', 'categories', 'publicLinkedEjaculations'));
+ return view('home')->with(compact('informations', 'categories', 'globalEjaculationCounts', 'publicLinkedEjaculations'));
} else {
return view('guest')->with(compact('informations', 'categories'));
}
diff --git a/app/Http/Controllers/SearchController.php b/app/Http/Controllers/SearchController.php
index f0eb1d4..3fa2725 100644
--- a/app/Http/Controllers/SearchController.php
+++ b/app/Http/Controllers/SearchController.php
@@ -21,7 +21,8 @@ class SearchController extends Controller
->where('is_private', false)
->orderBy('ejaculated_date', 'desc')
->with(['user', 'tags'])
- ->paginate(20);
+ ->paginate(20)
+ ->appends($inputs);
return view('search.index')->with(compact('inputs', 'results'));
}
@@ -34,7 +35,8 @@ class SearchController extends Controller
$results = Tag::query()
->where('name', 'like', "%{$inputs['q']}%")
- ->paginate(50);
+ ->paginate(50)
+ ->appends($inputs);
return view('search.relatedTag')->with(compact('inputs', 'results'));
}
diff --git a/app/Http/Controllers/SettingController.php b/app/Http/Controllers/SettingController.php
index 1f812e3..136f055 100644
--- a/app/Http/Controllers/SettingController.php
+++ b/app/Http/Controllers/SettingController.php
@@ -17,9 +17,13 @@ class SettingController extends Controller
{
$inputs = $request->all();
$validator = Validator::make($inputs, [
- 'display_name' => 'required|string|max:20'
+ 'display_name' => 'required|string|max:20',
+ 'bio' => 'nullable|string|max:160',
+ 'url' => 'nullable|url|max:2000'
], [], [
- 'display_name' => '名前'
+ 'display_name' => '名前',
+ 'bio' => '自己紹介',
+ 'url' => 'URL'
]);
if ($validator->fails()) {
@@ -28,6 +32,8 @@ class SettingController extends Controller
$user = Auth::user();
$user->display_name = $inputs['display_name'];
+ $user->bio = $inputs['bio'] ?? '';
+ $user->url = $inputs['url'] ?? '';
$user->save();
return redirect()->route('setting')->with('status', 'プロフィールを更新しました。');
diff --git a/app/Http/Controllers/UserController.php b/app/Http/Controllers/UserController.php
index beb463c..8f6f909 100644
--- a/app/Http/Controllers/UserController.php
+++ b/app/Http/Controllers/UserController.php
@@ -102,7 +102,7 @@ SQL
}
// 月間グラフ用の配列初期化
- $month = Carbon::now()->subMonth(11)->firstOfMonth(); // 直近12ヶ月
+ $month = Carbon::now()->firstOfMonth()->subMonth(11); // 直近12ヶ月
for ($i = 0; $i < 12; $i++) {
$monthlySum[$month->format('Y/m')] = 0;
$month->addMonth();
diff --git a/app/Http/Middleware/RedirectIfAuthenticated.php b/app/Http/Middleware/RedirectIfAuthenticated.php
index e4cec9c..b256b2d 100644
--- a/app/Http/Middleware/RedirectIfAuthenticated.php
+++ b/app/Http/Middleware/RedirectIfAuthenticated.php
@@ -18,7 +18,7 @@ class RedirectIfAuthenticated
public function handle($request, Closure $next, $guard = null)
{
if (Auth::guard($guard)->check()) {
- return redirect('/home');
+ return redirect()->route('home');
}
return $next($request);
diff --git a/app/Http/ViewComposers/ProfileComposer.php b/app/Http/ViewComposers/ProfileStatsComposer.php
similarity index 98%
rename from app/Http/ViewComposers/ProfileComposer.php
rename to app/Http/ViewComposers/ProfileStatsComposer.php
index cbb9a85..6f93ded 100644
--- a/app/Http/ViewComposers/ProfileComposer.php
+++ b/app/Http/ViewComposers/ProfileStatsComposer.php
@@ -7,7 +7,7 @@ use Carbon\Carbon;
use Illuminate\Support\Facades\DB;
use Illuminate\View\View;
-class ProfileComposer
+class ProfileStatsComposer
{
public function __construct()
{
diff --git a/app/MetadataResolver/ActivityPubResolver.php b/app/MetadataResolver/ActivityPubResolver.php
new file mode 100644
index 0000000..981cf9d
--- /dev/null
+++ b/app/MetadataResolver/ActivityPubResolver.php
@@ -0,0 +1,80 @@
+activityClient = new \GuzzleHttp\Client([
+ 'headers' => [
+ 'Accept' => 'application/activity+json, application/ld+json; profile="https://www.w3.org/ns/activitystreams"'
+ ]
+ ]);
+ }
+
+ public function resolve(string $url): Metadata
+ {
+ $res = $this->activityClient->get($url);
+ if ($res->getStatusCode() === 200) {
+ return $this->parse($res->getBody());
+ } else {
+ throw new \RuntimeException("{$res->getStatusCode()}: $url");
+ }
+ }
+
+ public function parse(string $json): Metadata
+ {
+ $activityOrObject = json_decode($json, true);
+ $object = $activityOrObject['object'] ?? $activityOrObject;
+
+ $metadata = new Metadata();
+
+ $metadata->title = isset($object['attributedTo']) ? $this->getTitleFromActor($object['attributedTo']) : '';
+ $metadata->description .= isset($object['summary']) ? $object['summary'] . " | " : '';
+ $metadata->description .= isset($object['content']) ? $this->html2text($object['content']) : '';
+ $metadata->image = $object['attachment'][0]['url'] ?? '';
+
+ return $metadata;
+ }
+
+ private function getTitleFromActor(string $url): string
+ {
+ try {
+ $res = $this->activityClient->get($url);
+ if ($res->getStatusCode() !== 200) {
+ Log::info(self::class . ': Actorの取得に失敗 URL=' . $url);
+ return '';
+ }
+
+ $actor = json_decode($res->getBody(), true);
+ $title = $actor['name'] ?? '';
+ if (isset($actor['preferredUsername'])) {
+ $title .= ' (@' . $actor['preferredUsername'] . '@' . parse_url($actor['id'], PHP_URL_HOST) . ')';
+ }
+
+ return $title;
+ } catch (TransferException $e) {
+ Log::info(self::class . ': Actorの取得に失敗 URL=' . $url);
+ return '';
+ }
+ }
+
+ private function html2text(string $html): string
+ {
+ $html = mb_convert_encoding($html, 'HTML-ENTITIES', 'UTF-8');
+ $html = preg_replace('~
|
]*>~i', "\n", $html); + $dom = new \DOMDocument(); + $dom->loadHTML($html, LIBXML_HTML_NOIMPLIED | LIBXML_HTML_NODEFDTD); + return $dom->textContent; + } +} diff --git a/app/MetadataResolver/MetadataResolver.php b/app/MetadataResolver/MetadataResolver.php index 246a77a..a00625b 100644 --- a/app/MetadataResolver/MetadataResolver.php +++ b/app/MetadataResolver/MetadataResolver.php @@ -2,6 +2,9 @@ namespace App\MetadataResolver; +use GuzzleHttp\Exception\ClientException; +use GuzzleHttp\Exception\ServerException; + class MetadataResolver implements Resolver { public $rules = [ @@ -15,10 +18,20 @@ class MetadataResolver implements Resolver '~www\.pixiv\.net/member_illust\.php\?illust_id=\d+~' => PixivResolver::class, '~fantia\.jp/posts/\d+~' => FantiaResolver::class, '~dmm\.co\.jp/~' => FanzaResolver::class, + '~www\.patreon\.com/~' => PatreonResolver::class, '~www\.deviantart\.com/.*/art/.*~' => DeviantArtResolver::class, - '/.*/' => OGPResolver::class + '~\.syosetu\.com/n\d+[a-z]{2,}~' => NarouResolver::class, ]; + public $mimeTypes = [ + 'application/activity+json' => ActivityPubResolver::class, + 'application/ld+json' => ActivityPubResolver::class, + 'text/html' => OGPResolver::class, + '*/*' => OGPResolver::class + ]; + + public $defaultResolver = OGPResolver::class; + public function resolve(string $url): Metadata { foreach ($this->rules as $pattern => $class) { @@ -29,6 +42,64 @@ class MetadataResolver implements Resolver } } + $result = $this->resolveWithAcceptHeader($url); + if ($result !== null) { + return $result; + } + + if (isset($this->defaultResolver)) { + $resolver = new $this->defaultResolver(); + return $resolver->resolve($url); + } + throw new \UnexpectedValueException('URL not matched.'); } + + public function resolveWithAcceptHeader(string $url): ?Metadata + { + try { + // Rails等はAcceptに */* が入っていると、ブラウザの適当なAcceptヘッダだと判断して全部無視してしまう。 + // c.f. https://github.com/rails/rails/issues/9940 + // そこでここでは */* を「Acceptヘッダを無視してきたレスポンス(よくある)」のハンドラとして扱い、 + // Acceptヘッダには */* を足さないことにする。 + $acceptTypes = array_diff(array_keys($this->mimeTypes), ['*/*']); + + $client = new \GuzzleHttp\Client(); + $res = $client->request('GET', $url, [ + 'headers' => [ + 'Accept' => implode(', ', $acceptTypes) + ] + ]); + + if ($res->getStatusCode() === 200) { + preg_match('/^[^;\s]+/', $res->getHeaderLine('Content-Type'), $matches); + $mimeType = $matches[0]; + + if (isset($this->mimeTypes[$mimeType])) { + $class = $this->mimeTypes[$mimeType]; + $parser = new $class(); + + return $parser->parse($res->getBody()); + } + + if (isset($this->mimeTypes['*/*'])) { + $class = $this->mimeTypes['*/*']; + $parser = new $class(); + + return $parser->parse($res->getBody()); + } + } else { + // code < 400 && code !== 200 => fallback + } + } catch (ClientException $e) { + // 406 Not Acceptable は多分Acceptが原因なので無視してフォールバック + if ($e->getResponse()->getStatusCode() !== 406) { + throw $e; + } + } catch (ServerException $e) { + // 5xx は変なAcceptが原因かもしれない(?)ので無視してフォールバック + } + + return null; + } } diff --git a/app/MetadataResolver/NarouResolver.php b/app/MetadataResolver/NarouResolver.php new file mode 100644 index 0000000..5f14d0a --- /dev/null +++ b/app/MetadataResolver/NarouResolver.php @@ -0,0 +1,46 @@ + 'yes'], '.syosetu.com'); + + $client = new \GuzzleHttp\Client(); + $res = $client->get($url, ['cookies' => $cookieJar]); + if ($res->getStatusCode() === 200) { + $ogpResolver = new OGPResolver(); + $metadata = $ogpResolver->parse($res->getBody()); + $metadata->description = ''; + + $dom = new \DOMDocument(); + @$dom->loadHTML(mb_convert_encoding($res->getBody(), 'HTML-ENTITIES', 'ASCII,JIS,UTF-8,eucJP-win,SJIS-win')); + $xpath = new \DOMXPath($dom); + + $description = []; + + // 作者名 + $writerNodes = $xpath->query('//*[contains(@class, "novel_writername")]'); + if ($writerNodes->length !== 0 && !empty($writerNodes->item(0)->textContent)) { + $description[] = trim($writerNodes->item(0)->textContent); + } + + // あらすじ + $exNodes = $xpath->query('//*[@id="novel_ex"]'); + if ($exNodes->length !== 0 && !empty($exNodes->item(0)->textContent)) { + $summary = trim($exNodes->item(0)->textContent); + $description[] = mb_strimwidth($summary, 0, 101, '…'); // 100 + '…'(1) + } + + $metadata->description = implode(' / ', $description); + + return $metadata; + } else { + throw new \RuntimeException("{$res->getStatusCode()}: $url"); + } + } +} diff --git a/app/MetadataResolver/OGPResolver.php b/app/MetadataResolver/OGPResolver.php index 5afe83c..1cf4c1e 100644 --- a/app/MetadataResolver/OGPResolver.php +++ b/app/MetadataResolver/OGPResolver.php @@ -2,7 +2,7 @@ namespace App\MetadataResolver; -class OGPResolver implements Resolver +class OGPResolver implements Resolver, Parser { public function resolve(string $url): Metadata { @@ -30,7 +30,7 @@ class OGPResolver implements Resolver $metadata->title = $nodes->item(0)->textContent; } } - $metadata->description = $this->findContent($xpath, '//meta[@*="og:description"]', '//meta[@*="twitter:description"]'); + $metadata->description = $this->findContent($xpath, '//meta[@*="og:description"]', '//meta[@*="twitter:description"]', '//meta[@name="description"]'); $metadata->image = $this->findContent($xpath, '//meta[@*="og:image"]', '//meta[@*="twitter:image"]'); return $metadata; diff --git a/app/MetadataResolver/Parser.php b/app/MetadataResolver/Parser.php new file mode 100644 index 0000000..f9effde --- /dev/null +++ b/app/MetadataResolver/Parser.php @@ -0,0 +1,8 @@ +get($url); + if ($res->getStatusCode() === 200) { + $ogpResolver = new OGPResolver(); + $metadata = $ogpResolver->parse($res->getBody()); + + parse_str(parse_url($metadata->image, PHP_URL_QUERY), $temp); + $expires_at_unixtime = $temp["token-time"]; + $expires_at = Carbon::createFromTimestamp($expires_at_unixtime); + + $metadata->expires_at = $expires_at; + + return $metadata; + } else { + throw new \RuntimeException("{$res->getStatusCode()}: $url"); + } + } +} diff --git a/app/MetadataResolver/PixivResolver.php b/app/MetadataResolver/PixivResolver.php index ca5463c..16c89d9 100644 --- a/app/MetadataResolver/PixivResolver.php +++ b/app/MetadataResolver/PixivResolver.php @@ -8,6 +8,7 @@ class PixivResolver implements Resolver * サムネイル画像 URL から最大長辺 1200px の画像 URL に変換する * * @param string $thumbnailUrl サムネイル画像 URL + * * @return string 1200px の画像 URL */ public function thumbnailToMasterUrl(string $thumbnailUrl): string @@ -23,6 +24,7 @@ class PixivResolver implements Resolver * HUGE THANKS TO PIXIV.CAT! * * @param string $pixivUrl i.pximg URL + * * @return string i.pixiv.cat URL */ public function proxize(string $pixivUrl): string @@ -32,62 +34,33 @@ class PixivResolver implements Resolver public function resolve(string $url): Metadata { - preg_match("~illust_id=(\d+)~", parse_url($url)['query'], $match); - $illustId = $match[1]; + parse_str(parse_url($url, PHP_URL_QUERY), $params); + $illustId = $params['illust_id']; - // 漫画ページかつページ数あり - if (strpos(parse_url($url)['query'], 'mode=manga_big') && strpos(parse_url($url)['query'], 'page=')) { - preg_match("~page=(\d+)~", parse_url($url)['query'], $match); - $page = $match[1]; + // 漫画ページ(ページ数はmanga_bigならあるかも) + if ($params['mode'] === 'manga_big' || $params['mode'] === 'manga') { + $page = $params['page'] ?? 0; // 未ログインでは漫画ページを開けないため、URL を作品ページに変換する - $url = str_replace('mode=manga_big', 'mode=medium', $url); + $url = preg_replace('~mode=manga(_big)?~', 'mode=medium', $url); + } - $client = new \GuzzleHttp\Client(); - $res = $client->get($url); - if ($res->getStatusCode() === 200) { - $ogpResolver = new OGPResolver(); - $metadata = $ogpResolver->parse($res->getBody()); + $client = new \GuzzleHttp\Client(); + $res = $client->get($url); + if ($res->getStatusCode() === 200) { + $ogpResolver = new OGPResolver(); + $metadata = $ogpResolver->parse($res->getBody()); - preg_match("~https://i\.pximg\.net/c/128x128/img-master/img/\d{4}/\d{2}/\d{2}/\d{2}/\d{2}/\d{2}/{$illustId}_p0_square1200\.jpg~", $res->getBody(), $match); - $illustThumbnailUrl = $match[0]; + preg_match("~https://i\.pximg\.net/c/128x128/img-master/img/\d{4}/\d{2}/\d{2}/\d{2}/\d{2}/\d{2}/{$illustId}(_p0)?_square1200\.jpg~", $res->getBody(), $match); + $illustThumbnailUrl = $match[0]; - $illustUrl = $this->thumbnailToMasterUrl($illustThumbnailUrl); + $illustUrl = $this->thumbnailToMasterUrl($illustThumbnailUrl); - // 指定ページに変換 - $illustUrl = str_replace('p0_master', "p{$page}_master", $illustUrl); + $metadata->image = $this->proxize($illustUrl); - $metadata->image = $this->proxize($illustUrl); - - return $metadata; - } else { - throw new \RuntimeException("{$res->getStatusCode()}: $url"); - } + return $metadata; } else { - $client = new \GuzzleHttp\Client(); - $res = $client->get($url); - if ($res->getStatusCode() === 200) { - $ogpResolver = new OGPResolver(); - $metadata = $ogpResolver->parse($res->getBody()); - - // OGP がデフォルト画像であるようならなんとかして画像を取得する - if (strpos($metadata->image, 'pixiv_logo.gif') || strpos($metadata->image, 'pictures.jpg')) { - - // 作品ページの場合のみ対応 - if (strpos(parse_url($url)['query'], 'mode=medium')) { - preg_match("~https://i\.pximg\.net/c/128x128/img-master/img/\d{4}/\d{2}/\d{2}/\d{2}/\d{2}/\d{2}/{$illustId}(_p0)?_square1200\.jpg~", $res->getBody(), $match); - $illustThumbnailUrl = $match[0]; - - $illustUrl = $this->thumbnailToMasterUrl($illustThumbnailUrl); - - $metadata->image = $this->proxize($illustUrl); - } - } - - return $metadata; - } else { - throw new \RuntimeException("{$res->getStatusCode()}: $url"); - } + throw new \RuntimeException("{$res->getStatusCode()}: $url"); } } } diff --git a/app/Providers/ViewComposerServiceProvider.php b/app/Providers/ViewComposerServiceProvider.php index bb4f062..70bf1fd 100644 --- a/app/Providers/ViewComposerServiceProvider.php +++ b/app/Providers/ViewComposerServiceProvider.php @@ -2,7 +2,7 @@ namespace App\Providers; -use App\Http\ViewComposers\ProfileComposer; +use App\Http\ViewComposers\ProfileStatsComposer; use Illuminate\Support\Facades\View; use Illuminate\Support\ServiceProvider; @@ -15,7 +15,7 @@ class ViewComposerServiceProvider extends ServiceProvider */ public function boot() { - View::composer('components.profile', ProfileComposer::class); + View::composer('components.profile-stats', ProfileStatsComposer::class); } /** diff --git a/composer.json b/composer.json index 05edd13..be2f2b5 100644 --- a/composer.json +++ b/composer.json @@ -5,7 +5,7 @@ "license": "MIT", "type": "project", "require": { - "php": ">=7.0.0", + "php": ">=7.1.0", "anhskohbo/no-captcha": "^3.0", "doctrine/dbal": "^2.9", "fideloper/proxy": "~3.3", diff --git a/database/migrations/2019_02_06_235832_add_bio_and_url_to_users.php b/database/migrations/2019_02_06_235832_add_bio_and_url_to_users.php new file mode 100644 index 0000000..d8988d4 --- /dev/null +++ b/database/migrations/2019_02_06_235832_add_bio_and_url_to_users.php @@ -0,0 +1,34 @@ +string('bio', 160)->default(''); + $table->text('url')->default(''); + }); + } + + /** + * Reverse the migrations. + * + * @return void + */ + public function down() + { + Schema::table('users', function (Blueprint $table) { + $table->dropColumn('bio'); + $table->dropColumn('url'); + }); + } +} diff --git a/dist/php.d/99-xdebug.ini b/dist/php.d/99-xdebug.ini index a6e8b8c..007daa1 100644 --- a/dist/php.d/99-xdebug.ini +++ b/dist/php.d/99-xdebug.ini @@ -1,5 +1,4 @@ ; Dockerでのデバッグ用設定 zend_extension=xdebug.so xdebug.remote_enable=true -xdebug.remote_autostart=true xdebug.remote_host=host.docker.internal \ No newline at end of file diff --git a/public/css/tissue.css b/public/css/tissue.css index b571c9e..e71b920 100644 --- a/public/css/tissue.css +++ b/public/css/tissue.css @@ -15,6 +15,25 @@ overflow-x: auto; } +.tis-need-agecheck .container { + filter: blur(45px); + pointer-events: none; +} + +.container { + transition: filter .15s liner; +} + +.list-group-item.no-side-border { + border-left: none; + border-right: none; + border-radius: 0; +} + +.list-group-item.border-bottom-only:first-child { + border-top: none; +} + .list-group-item.border-bottom-only { border-left: none; border-right: none; @@ -27,4 +46,15 @@ .timeline-action-item { margin-left: 16px; +} + +.tis-global-count-graph { + height: 90px; + border-bottom: 1px solid rgba(0, 0, 0, .125); +} + +@media (min-width: 992px) { + .tis-sidebar-info { + font-size: small; + } } \ No newline at end of file diff --git a/public/js/tissue.js b/public/js/tissue.js new file mode 100644 index 0000000..dab7c34 --- /dev/null +++ b/public/js/tissue.js @@ -0,0 +1,49 @@ +// app.jsの名はモジュールバンドラーを投入する日まで予約しておく。CSSも同じ。 + +(function ($) { + + $.fn.linkCard = function (options) { + var settings = $.extend({ + endpoint: '/api/checkin/card' + }, options); + + return this.each(function () { + var $this = $(this); + $.ajax({ + url: settings.endpoint, + method: 'get', + type: 'json', + data: { + url: $this.find('a').attr('href') + } + }).then(function (data) { + var $title = $this.find('.card-title'); + var $desc = $this.find('.card-text'); + var $image = $this.find('img'); + + if (data.title === '') { + $title.hide(); + } else { + $title.text(data.title); + } + + if (data.description === '') { + $desc.hide(); + } else { + $desc.text(data.description); + } + + if (data.image === '') { + $image.hide(); + } else { + $image.attr('src', data.image); + } + + if (data.title !== '' || data.description !== '' || data.image !== '') { + $this.removeClass('d-none'); + } + }); + }); + }; + +})(jQuery); \ No newline at end of file diff --git a/resources/views/auth/register.blade.php b/resources/views/auth/register.blade.php index e84e9d4..0b8764d 100644 --- a/resources/views/auth/register.blade.php +++ b/resources/views/auth/register.blade.php @@ -12,6 +12,10 @@
注意! Tissueでは、登録に使用したメールアドレスの Gravatar を使用します。
+他の場所での活動と紐付いてほしくない場合、使用予定のメールアドレスにGravatarが設定されていないかを確認することを推奨します。
+