Compare commits

..

373 Commits

Author SHA1 Message Date
Irie Aoi
cb218c435d lint 2020-12-11 03:08:20 +09:00
Irie Aoi
db9d93ed90 OGPResolver https 2020-12-11 03:04:30 +09:00
Irie Aoi
69ec50a6c0 XtubeResolverTest 追従 2020-12-11 02:57:52 +09:00
Irie Aoi
f408c23982 XtubeResolver HTMLが壊れているので説明の取得をコメントアウト 2020-12-11 02:57:42 +09:00
Irie Aoi
3d9179268f ToranoanaResolver 追従 2020-12-11 02:52:32 +09:00
Irie Aoi
e1e4ffbdc8 PlurkResolver 追従 2020-12-11 02:49:48 +09:00
Irie Aoi
1febc0866d PixivResolver 追従 2020-12-11 02:49:08 +09:00
Irie Aoi
eacc7bf5f8 NijieResolverTest 追従 2020-12-11 02:40:23 +09:00
Irie Aoi
ed9307c078 KomifloResolverTest 追従 2020-12-11 02:38:33 +09:00
Irie Aoi
330facf974 KomifloResolver タグをsort 2020-12-11 02:38:22 +09:00
Irie Aoi
2778d34711 IwaraResolverTest 追従 2020-12-11 02:34:31 +09:00
Irie Aoi
8d53c0f175 IwaraResolver 説明をtrim 2020-12-11 02:34:22 +09:00
Irie Aoi
c73141c1d3 FC2ContentsResolver OGP以上の情報が取れなくなったため削除 2020-12-11 02:31:55 +09:00
Irie Aoi
a667767858 FanzaResolverTest 追従 2020-12-11 02:18:03 +09:00
Irie Aoi
daa12d93a5 FanzaResolver 動画のタグと説明取得方法を改善 2020-12-11 02:17:47 +09:00
Irie Aoi
4b51f6dfec FanzaResolver PCゲームのタグ取得セレクタを修正 2020-12-11 01:49:24 +09:00
Irie Aoi
5a1560fef9 DLsiteResolverTest 追従 2020-12-11 01:40:39 +09:00
Irie Aoi
3b3fc6c973 DLsiteResolver タグをソート 2020-12-11 01:39:55 +09:00
Irie Aoi
21a7d3f9e7 update fixtures 2020-12-11 01:19:48 +09:00
dependabot[bot]
7bcf48c8bf
Bump nunomaduro/collision from 3.0.1 to 3.1.0 (#565) 2020-12-08 13:46:31 +00:00
dependabot[bot]
8f1a94b863
Bump dot-prop from 4.2.0 to 4.2.1 (#561) 2020-12-08 13:08:25 +00:00
dependabot[bot]
1b54af63ce
Bump facade/ignition from 1.16.3 to 1.16.4 (#567) 2020-12-08 13:04:43 +00:00
dependabot[bot]
3ba946ec11
Bump symfony/dom-crawler from 4.4.11 to 4.4.17 (#556) 2020-12-08 13:02:49 +00:00
dependabot[bot]
32e25a9f7a
Bump laravel/helpers from 1.2.0 to 1.4.0 (#548) 2020-12-07 23:33:42 +00:00
dependabot[bot]
31626f48d9
Bump barryvdh/laravel-ide-helper from 2.8.0 to 2.8.1 (#540) 2020-12-07 23:06:43 +00:00
dependabot[bot]
174d802edf
Bump league/csv from 9.6.0 to 9.6.1 (#541) 2020-12-07 22:51:57 +00:00
shibafu
9c9db69662
Merge pull request #558 from eai04191/fix-cien-resolver
Fix Cien resolver
2020-12-07 08:33:14 +09:00
Irie Aoi
8c5fdfb7f8 未使用のfixtureを削除 2020-12-06 19:37:56 +09:00
Irie Aoi
c616800da1 投稿に使える画像があれば使う 2020-12-06 19:37:56 +09:00
Irie Aoi
6449094b78 JWTがついているときだけexpires_atを付与する 2020-12-06 19:37:56 +09:00
Irie Aoi
072b3a0910 Cien fixture 更新 2020-12-06 19:37:56 +09:00
shibafu
83d22b807f
Merge pull request #559 from shikorism/fix/xdebug-first-aid
CIでカバレッジ取りに使用するXdebugのバージョンを落とす
2020-12-05 10:01:38 +09:00
shibafu
914d92e545 CIでカバレッジ取りに使用するXdebugのバージョンを落とす 2020-12-03 21:20:23 +09:00
shibafu
dcef270790
Merge pull request #554 from shikorism/fix/negative-margin-destroyer
ユーザーページでstatus alertを出した時に余白が無くて不恰好なのを修正
2020-11-21 15:50:23 +09:00
shibafu
5808ac58ee ユーザーページでstatus alertを出した時に余白が無くて不恰好なのを修正 2020-11-18 23:09:21 +09:00
shibafu
b60932b6c5 add config for heroku 2020-11-13 23:39:42 +09:00
shibafu
28520007e4
Merge pull request #553 from shikorism/feature/332-discard-elapsed-time
「前回チェックインからの経過時間を記録しない」チェックインオプションの追加
2020-11-12 00:32:17 +09:00
shibafu
63af49ba70 概況計算でdiscard elapsed timeを考慮 2020-11-08 23:41:51 +09:00
shibafu
3825228344 discard elapsed timeフラグの立ったチェックインの表示対応 2020-11-08 23:07:51 +09:00
shibafu
855011c624 add discard_elapsed_time option 2020-11-08 15:38:54 +09:00
shibafu
bb7b05435e add migration 2020-11-08 01:04:56 +09:00
shibafu
5c26e58c1d
Merge pull request #550 from shikorism/fix/239-actual-ejaculated-span
チェックインスパンの計算手段をWindow関数から相関サブクエリに変更
2020-11-07 17:26:19 +09:00
shibafu
ddb11ee96c
Merge pull request #547 from shikorism/feature/realtime-checkin
日時指定を省略してチェックインする機能
2020-11-07 17:18:09 +09:00
shibafu
38c7755757 チェックインスパンの計算手段をWindow関数から相関サブクエリに変更 2020-11-06 09:12:25 +09:00
shibafu
c0cfbe1bc4 日時指定を省略してチェックインする機能 2020-11-03 14:19:25 +09:00
shibafu
96bf90104e
Merge pull request #545 from shikorism/composer2
thanks prestissimo, goodbye!
2020-10-31 01:26:46 +09:00
shibafu
2bcd050fed thanks prestissimo, goodbye! 2020-10-31 01:06:06 +09:00
shibafu
ea1483a90a
Merge pull request #538 from shikorism/fix/degrade-profile-stats
プロフィールページの概況が消えていたのを修正
2020-10-26 08:58:09 +09:00
shibafu
7e7d0f80c1 プロフィールページの概況が消えていたのを修正 2020-10-26 00:22:06 +09:00
shibafu
32398fea73
Merge pull request #536 from shikorism/fix/degrade-show-checkin
チェックイン個別ページのユーザープロフィールが半分消えているのを修正
2020-10-25 17:30:22 +09:00
shibafu
076e7d5d0d チェックイン個別ページのユーザープロフィールが半分消えているのを修正 2020-10-25 17:22:04 +09:00
dependabot[bot]
41c7679423
Bump symfony/css-selector from 4.4.11 to 4.4.15 (#523) 2020-10-25 07:55:53 +00:00
dependabot[bot]
486e5bad0a
Bump barryvdh/laravel-debugbar from 3.4.1 to 3.5.1 (#516) 2020-10-25 07:47:00 +00:00
dependabot[bot]
a026897986
Bump laravel/framework from 6.18.35 to 6.19.1 (#533) 2020-10-25 07:36:53 +00:00
dependabot[bot]
96658ac34c
Bump http-proxy from 1.18.0 to 1.18.1 (#514) 2020-10-25 07:32:42 +00:00
dependabot[bot]
622964ec01
Bump symfony/http-kernel from 4.4.11 to 4.4.13 (#510) 2020-10-25 07:05:32 +00:00
shibafu
141067beab
Merge pull request #534 from shikorism/feature/refresh-navigations
プロフィールページ内タブのデザイン変更とヘッダーナビゲーションの再編
2020-10-25 16:04:21 +09:00
shibafu
61b83c18a7 ユーザー名のスタイル調整 2020-10-24 18:30:13 +09:00
shibafu
581ae56173 プロフィール内タブのデザイン調整 2020-10-24 15:40:11 +09:00
shibafu
b50a15c109 ヘッダーナビゲーションの内容を再編 2020-10-24 15:07:46 +09:00
shibafu
060bdeef43 ヘッダーメニューに自アカウントのグラフ・オカズへのリンクを追加 2020-10-24 15:00:21 +09:00
shibafu
be098c38cb ヘッダーのドロップダウンメニューの中身を別ファイルに分割 2020-10-24 14:53:12 +09:00
shibafu
d7d2bc2397 プロフィールページのタブ周りのデザインを刷新 2020-10-24 14:32:30 +09:00
shibafu
0f435c09b3
Merge pull request #479 from shikorism/feature/463-update-fixture
Fixture updater
2020-10-24 11:57:12 +09:00
shibafu
16071f7cff
Merge pull request #531 from shikorism/fix/robots-ignore-non-plain-response
robots.txt取得時、text/plain以外の応答は失敗扱いにする
2020-10-21 20:47:19 +09:00
shibafu
a22f41766a remove unused use 2020-10-19 09:18:24 +09:00
shibafu
552ff421dd file_get_contentsを直接使うのをやめよう運動 2020-10-19 09:17:49 +09:00
shibafu
f8952474b5 MetadataResolverのFixtureを更新する仕組みを追加 2020-10-19 09:17:49 +09:00
shibafu
3fd62dcd6f robots.txt取得時、text/plain以外の応答は失敗扱いにする 2020-10-17 16:54:00 +09:00
shibafu
4360144d6f
Merge pull request #530 from shikorism/fix/cien-jwt
Ci-en: JWTから有効期限を取得する
2020-10-17 16:20:18 +09:00
shibafu
e74a9675ce Ci-en: JWTから有効期限を取得する 2020-10-17 14:01:28 +09:00
shibafu
0bc546ac64
Merge pull request #498 from shikorism/fix/card-throttle-in-guest
ゲストアクセス時のレートリミットを厳しめにする
2020-08-25 22:29:34 +09:00
shibafu
95ed292f4f ゲストアクセス時のレートリミットを厳しめにする 2020-08-25 22:07:54 +09:00
shibafu
032533666d
Merge pull request #495 from shikorism/fix/middleware-order-in-stateful-api
Stateful APIのMiddleware順序を調整
2020-08-25 01:48:46 +09:00
shibafu
5fc8a22cca Stateful APIのMiddleware順序を調整 2020-08-23 16:56:35 +09:00
shibafu
3406b326f0
Merge pull request #494 from shikorism/fix/es5-error-workaround
ES5 User error class workaround
2020-08-23 15:40:40 +09:00
shibafu
7cf1a362ac
Merge pull request #493 from shikorism/fix/491-fetch-card-error
カード情報の取得に失敗した時にちゃんと処理を中断させる
2020-08-23 15:18:40 +09:00
shibafu
7708e705cc ES5 User error class workaround 2020-08-23 15:08:13 +09:00
dependabot[bot]
494eb7a17b
Bump @typescript-eslint/parser from 3.1.0 to 3.9.1 (#488) 2020-08-23 04:45:04 +00:00
shibafu
24a74c4c2c カード情報の取得に失敗した時にちゃんと処理を中断させる 2020-08-23 13:43:31 +09:00
shibafu
d1f14ed271
Merge pull request #492 from shikorism/fix/489-fix-copipe
チェックイン編集・流用時のフラグ周りの動作修正
2020-08-23 13:12:44 +09:00
shibafu
16d71a1621 オカズ流用時に非公開フラグを継承 2020-08-23 13:06:11 +09:00
shibafu
c956a96605 チェックイン編集時に非公開フラグが維持されていないバグの修正 2020-08-23 13:02:27 +09:00
dependabot[bot]
f301c56a1a
Bump barryvdh/laravel-debugbar from 3.3.3 to 3.4.1 (#487) 2020-08-23 02:26:13 +00:00
dependabot[bot]
2c2b17653e
Bump barryvdh/laravel-ide-helper from 2.7.0 to 2.8.0 (#486) 2020-08-23 02:21:20 +00:00
dependabot[bot]
569363c72e
Bump laravel/framework from 6.18.26 to 6.18.35 (#482) 2020-08-23 02:18:32 +00:00
dependabot[bot]
3ba3165b93
Bump @types/chart.js from 2.9.23 to 2.9.24 (#483) 2020-08-23 02:17:30 +00:00
dependabot[bot]
083373ad7d
Bump laravel/tinker from 2.4.1 to 2.4.2 (#485) 2020-08-23 02:17:04 +00:00
dependabot[bot]
2b109163fc
Bump anhskohbo/no-captcha from 3.2.0 to 3.2.1 (#484) 2020-08-23 02:16:04 +00:00
dependabot[bot]
beca7fb0c6
Bump stylelint-config-recess-order from 2.0.4 to 2.1.0 (#475) 2020-08-23 01:37:15 +00:00
dependabot[bot]
fa733417dd
Bump stylelint from 9.10.1 to 13.6.1 (#412) 2020-08-23 01:27:31 +00:00
dependabot[bot]
7dca83d12a
Bump mockery/mockery from 1.4.0 to 1.4.2 (#467) 2020-08-22 18:29:27 +00:00
dependabot[bot]
d8c88fab63
Bump symfony/css-selector from 4.4.10 to 4.4.11 (#446) 2020-08-22 18:28:43 +00:00
dependabot[bot]
9bac56f912
Bump symfony/dom-crawler from 4.4.10 to 4.4.11 (#447) 2020-08-22 18:27:21 +00:00
dependabot[bot]
28c0060679
Bump facade/ignition from 1.16.1 to 1.16.3 (#434) 2020-08-22 18:18:24 +00:00
dependabot[bot]
b5af600336
Bump symfony/thanks from 1.2.8 to 1.2.9 (#426) 2020-08-22 18:16:30 +00:00
shibafu
301fc83e7e
Merge pull request #468 from shikorism/feature/per-host-resolve-control
リモートホストごとの同時アクセス制御とメタデータ取得ポリシー制御
2020-08-22 09:37:09 +09:00
shibafu
3bb2b9afe0
Merge pull request #465 from shikorism/feature/metadata-error
メタデータ取得エラーの記録とリトライ制限の適用
2020-08-22 09:34:58 +09:00
shibafu
4e521baf56
Merge pull request #442 from shikorism/feature/300-incoming-webhook
Incoming webhook
2020-08-21 01:11:00 +09:00
shibafu
59e4cb8d91 throttleをルートごとに別々にかける
refs #474
2020-08-20 20:49:01 +09:00
shibafu
d0fcbd79ca fix import 2020-08-20 09:26:02 +09:00
shibafu
c680cd8d8e Merge branch 'develop' into feature/300-incoming-webhook
# Conflicts:
#	package.json
#	webpack.mix.js
#	yarn.lock
2020-08-20 09:24:28 +09:00
shibafu
2d42f48bea
Merge pull request #440 from shikorism/dependabot/npm_and_yarn/date-fns-2.15.0
Bump date-fns from 1.30.1 to 2.15.0
2020-08-20 00:09:48 +09:00
shibafu
a54d57827f fix format pattern 2020-08-19 23:55:26 +09:00
dependabot[bot]
f8a5cc5d54
Bump date-fns from 1.30.1 to 2.15.0
Bumps [date-fns](https://github.com/date-fns/date-fns) from 1.30.1 to 2.15.0.
- [Release notes](https://github.com/date-fns/date-fns/releases)
- [Changelog](https://github.com/date-fns/date-fns/blob/master/CHANGELOG.md)
- [Commits](https://github.com/date-fns/date-fns/compare/v1.30.1...v2.15.0)

Signed-off-by: dependabot[bot] <support@github.com>
2020-08-19 14:53:18 +00:00
shibafu
6f8c81cef2
Merge pull request #473 from shikorism/feature/365-csv-flags
非公開フラグ、センシティブフラグのCSVインポート・エクスポート対応
2020-08-19 23:19:19 +09:00
shibafu
915b575e6e 非公開フラグ、センシティブフラグのCSV入力対応 2020-08-19 21:25:07 +09:00
shibafu
27e9a86be8 add test 2020-08-19 21:24:47 +09:00
shibafu
3015f82611 非公開フラグ、センシティブフラグのCSV出力対応 2020-08-19 09:47:54 +09:00
dependabot[bot]
d894897b82
Bump @types/chart.js from 2.9.22 to 2.9.23 (#461) 2020-08-18 23:45:16 +00:00
shibafu
0ccb04c651
Merge pull request #470 from shikorism/feature/326-react-checkin-form
チェックインフォームをReactで作り替える
2020-08-18 14:07:03 +09:00
shibafu
acc3c05c86 fix lint-staged extension 2020-08-17 20:06:07 +09:00
shibafu
4405a2b526 concat import 2020-08-17 18:23:16 +09:00
shibafu
7d8969e5f1 esModuleInterop 2020-08-17 18:17:38 +09:00
shibafu
8641f26350 fix eslint 2020-08-17 16:19:14 +09:00
shibafu
432c2bf4d5 rm vue 2020-08-17 16:11:31 +09:00
shibafu
228b1cdaaa fix いろいろ 2020-08-17 16:08:35 +09:00
shibafu
f37a9c79f4 update package.json 2020-08-17 16:02:39 +09:00
shibafu
6968ca7333 やるだけ.tsx 2020-08-17 16:00:04 +09:00
shibafu
867cafa26b
Merge pull request #469 from shikorism/fix/466-mobatwi
Twitterのサブドメイン対応用Resolverを追加
2020-08-16 16:17:00 +09:00
shibafu
04d546b1f0 Twitterのサブドメイン対応用Resolverを追加 2020-08-16 14:38:35 +09:00
shibafu
e961a2d4b4 export defaultは微妙 2020-08-13 01:57:09 +09:00
shibafu
064cfff211 wip 2020-08-13 00:38:26 +09:00
shibafu
969ddb94a9 タグの確定時に空白文字を置換する
from 978eccd643
2020-08-12 23:06:17 +09:00
shibafu
ae79f0225e CSSをどうにかするのを諦める 2020-08-12 23:05:38 +09:00
shibafu
196819270b wip 2020-08-12 22:48:23 +09:00
shibafu
45fd630e1a fix test 2020-08-12 22:05:56 +09:00
shibafu
b71b7e5cb2 リモートホストごとの同時アクセス制御とメタデータ取得ポリシー制御 2020-08-11 00:17:10 +09:00
shibafu
f715e7feee add migration and model 2020-08-10 20:38:38 +09:00
shibafu
ca070e773a add test 2020-08-10 15:57:02 +09:00
shibafu
da7be61698 Don't report circuit break exception 2020-08-10 13:40:40 +09:00
shibafu
c372c11867 rm Log 2020-08-10 13:40:03 +09:00
shibafu
578b9934f5 メタデータ取得エラーの記録とリトライ制限の適用 2020-08-10 13:32:47 +09:00
shibafu
4acebcec7e Add metadata error attrs migration 2020-08-10 12:25:00 +09:00
shibafu
0ca16459a4
Merge pull request #464 from shikorism/fix/transact-checkin
EjaculationControllerでトランザクションを使う
2020-08-10 11:48:23 +09:00
shibafu
b99930e145 EjaculationControllerでトランザクションを使う 2020-08-10 00:10:06 +09:00
shibafu
8093b22981
Merge pull request #462 from shikorism/fix/454-ogpresolver-cookie
OGPResolver: リダイレクト時にCookieを維持させる
2020-08-09 12:35:28 +09:00
shibafu
8cde943cf8 つらい 2020-08-09 11:55:57 +09:00
shibafu
1c6959bfcb is_private, is_too_sensitiveにfalseを明示的に指定すると真になってた 2020-08-09 11:06:42 +09:00
shibafu
73c64f0f27 use Validator::validate() 2020-08-09 10:59:44 +09:00
shibafu
7aa11275cc OGPResolver: リダイレクト時にCookieを維持させる 2020-08-08 18:32:38 +09:00
shibafu
e6950a5dfb
Merge pull request #460 from shikorism/replace-jquery-tissue-plugin
Replace jquery tissue plugin
2020-08-08 16:41:48 +09:00
shibafu
134983d13d JSON APIリクエスト時に例外が発生したら、常にJSONでレスポンスする 2020-08-08 15:17:32 +09:00
shibafu
35aa7b3916 Rename ejaculation scope "public" -> "visibleToTimeline" 2020-08-08 14:19:57 +09:00
shibafu
0587a0f1d4 use transaction 2020-08-07 23:22:01 +09:00
shibafu
969bc79cbc 削除確認モーダルのsubmit処理を変更 2020-08-07 22:35:49 +09:00
shibafu
723587b0ac deleteCheckinModalの中身でなるべくjQueryを使わないようにした 2020-08-07 09:50:01 +09:00
shibafu
23189b76e4 deleteCheckinModalをVanilla化 2020-08-07 01:02:36 +09:00
shibafu
8a0a29feef linkCardをVanilla化 2020-08-07 00:27:15 +09:00
hina
2f928a29e1 yarn.lock 2020-08-07 00:09:21 +09:00
Hinaloe
62a3e883e1
Merge branch 'develop' into feature/300-incoming-webhook 2020-08-06 23:28:30 +09:00
dependabot[bot]
65077571e7
Bump eslint from 7.2.0 to 7.6.0 (#456) 2020-08-06 14:24:53 +00:00
shibafu
a4fdd220ba pageSelectorをVanilla化 2020-08-06 23:17:25 +09:00
shibafu
a52211758e
Merge pull request #459 from shikorism/fetch
Fetch APIを使う
2020-08-06 23:14:28 +09:00
shibafu
1482c33448 jQuery.ajax向けのCSRF Token設定を削除 2020-08-06 21:32:13 +09:00
shibafu
7c32ab068d jQuery.ajaxの村を滅ぼした 2020-08-06 21:32:12 +09:00
shibafu
3a1ec763ea fetch: 共通headerが送られてなかった 2020-08-06 21:15:36 +09:00
shibafu
77620c1699 x-csrf-tokenが送られねえ 2020-08-06 09:53:50 +09:00
shibafu
126e44bcd9 今日からjQuery.ajax禁止な 2020-08-06 09:09:53 +09:00
shibafu
422891e237 fetch wrapper 2020-08-06 09:05:31 +09:00
shibafu
081dd8da28
Merge pull request #457 from shikorism/fix/source-map-in-dev
SourceMapが欲しい
2020-08-06 02:02:33 +09:00
shibafu
87c97d27a8 generateForProduction: false 2020-08-06 01:27:47 +09:00
shibafu
8f1a4d3e88 これでいけるじゃん 2020-08-06 01:26:11 +09:00
shibafu
82bb10ae24 SourceMapが欲しい 2020-08-06 01:10:13 +09:00
dependabot[bot]
54e112fa57
Bump elliptic from 6.5.2 to 6.5.3 (#449) 2020-08-01 10:11:59 +00:00
shibafu
a63c39a56f
Merge pull request #452 from shikorism/fix/451-atomic-metadata-resolve
Metadata解決処理をトランザクション内で実行する
2020-08-01 18:48:13 +09:00
shibafu
f8a93fdf45 Metadata解決処理をトランザクション内で実行する 2020-08-01 18:39:43 +09:00
shibafu
978d54cf12
Merge pull request #443 from shikorism/fix/ignore-resolve-self
Tissue内のURLに対するメタデータ取得は拒否する
2020-08-01 18:38:14 +09:00
shibafu
43ed36ccb7
Merge pull request #450 from shikorism/feature/312-tag-normalize
正規化したタグ名で検索
2020-08-01 16:10:14 +09:00
shibafu
18ae64a870 テストしたいのはNFDだった 2020-07-31 23:19:35 +09:00
shibafu
b5901f26bf add test 2020-07-31 23:10:54 +09:00
shibafu
c9efcb538c add extension requirements 2020-07-31 22:21:31 +09:00
shibafu
561c9d028d 検索時にはtags.normalized_nameを使う 2020-07-30 23:12:29 +09:00
shibafu
d18f245129 Docker: use php-intl 2020-07-30 23:04:53 +09:00
shibafu
e2c43fef80 tags.normalized_name 2020-07-30 22:42:10 +09:00
shibafu
c7aa002625 なんもわからんわー 2020-07-30 00:46:33 +09:00
shibafu
66322dfec0 Webhookチェックインをお惣菜コーナーに流す 2020-07-24 17:51:48 +09:00
shibafu
fcdc00f165 Webhookからのチェックインであることを明示 2020-07-24 16:31:43 +09:00
shibafu
e2ba3581f9 add test 2020-07-24 13:56:06 +09:00
shibafu
d58afc0324 quirksはクソ 2020-07-24 12:42:11 +09:00
dependabot[bot]
d69fe6a22a
Bump laravel/framework from 6.18.25 to 6.18.26 (#444)
Bumps [laravel/framework](https://github.com/laravel/framework) from 6.18.25 to 6.18.26.
- [Release notes](https://github.com/laravel/framework/releases)
- [Changelog](https://github.com/laravel/framework/blob/7.x/CHANGELOG-6.x.md)
- [Commits](https://github.com/laravel/framework/compare/v6.18.25...v6.18.26)

Signed-off-by: dependabot[bot] <support@github.com>

Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2020-07-24 12:41:43 +09:00
shibafu
ef18d26b5d add ejaculations.checkin_webhook_id 2020-07-24 12:12:42 +09:00
shibafu
94446e0174 use softdelete 2020-07-24 12:02:27 +09:00
shibafu
d3ecfd1fb8 fix empty check 2020-07-24 12:00:17 +09:00
shibafu
08ab0c6543 URL間違えてんじゃねーか 2020-07-23 23:22:11 +09:00
shibafu
bbbea73a05 Webhook識別名重複チェック 2020-07-23 22:57:21 +09:00
shibafu
059c6d69cf Webhook作成数を制限 2020-07-23 22:36:08 +09:00
shibafu
08e12cd218 impl settings page 2020-07-23 22:26:34 +09:00
shibafu
134a11ad51
Merge pull request #445 from Al-aighumaisa/replace-whitespace-in-tag
タグの確定時に空白文字を置換する
2020-07-23 18:35:36 +09:00
مرزم اليَغميصاء
978eccd643 タグの確定時に空白文字を置換する
Closes #414.
2020-07-23 15:38:55 +09:00
shibafu
0a9920b11c さすがにServiceからHttpExceptionは雑すぎたか 2020-07-23 13:08:20 +09:00
shibafu
16b5fb3533 Tissue内のURLに対するメタデータ取得は拒否する 2020-07-21 23:39:48 +09:00
shibafu
35dea402ab add api document 2020-07-21 23:18:39 +09:00
shibafu
ab960cda7c fix validation 2020-07-21 22:55:11 +09:00
shibafu
e6252c49d1 date/timeをchecked_in_atに統合 2020-07-21 00:29:48 +09:00
shibafu
de07e950f2 add POST /api/webhooks/checkin/{id} 2020-07-19 23:04:10 +09:00
shibafu
5926c6e640 add checkin_webhooks table 2020-07-19 18:39:19 +09:00
shibafu
034a47cd25
Merge pull request #439 from shikorism/fix/robots-txt
update robots.txt
2020-07-18 00:22:46 +09:00
shibafu
45645d9ae5 2020-07-17 22:23:37 +09:00
shibafu
73b63a1f39 update robots.txt 2020-07-17 21:46:25 +09:00
shibafu
c5ab67d547
Merge pull request #436 from shikorism/feature/sentry
Add Sentry integration
2020-07-16 22:23:39 +09:00
shibafu
19d4ba5e40 Add Sentry integration 2020-07-16 22:11:42 +09:00
dependabot[bot]
6c3bcd57c5
Bump symfony/dom-crawler from 4.4.9 to 4.4.10 (#407) 2020-07-14 14:04:22 +00:00
dependabot[bot]
ce0d9e7163
Bump eslint-plugin-prettier from 3.1.3 to 3.1.4 (#424) 2020-07-14 14:03:45 +00:00
dependabot[bot]
26f07e2b54
Bump friendsofphp/php-cs-fixer from 2.16.3 to 2.16.4 (#425) 2020-07-14 14:03:16 +00:00
dependabot[bot]
405a968ad8
Bump laravel/tinker from 2.4.0 to 2.4.1 (#427) 2020-07-14 14:02:52 +00:00
dependabot[bot]
9c6990979a
Bump laravel/framework from 6.18.23 to 6.18.25 (#432) 2020-07-14 14:02:31 +00:00
shibafu
d83516f394
Merge pull request #430 from shikorism/fix/429-fanza-cookie
FANZA: 年齢認証Cookieを送信する
2020-07-09 00:33:09 +09:00
shibafu
b232279c25 FANZA: 年齢認証Cookieを送信する 2020-07-09 00:22:43 +09:00
dependabot[bot]
a04faa7001
Bump laravel/framework from 6.18.20 to 6.18.23 (#422) 2020-07-03 06:39:15 +00:00
dependabot[bot]
831c099247
Bump fideloper/proxy from 4.3.0 to 4.4.0 (#418) 2020-07-03 06:27:08 +00:00
dependabot[bot]
48815eed89
Bump guzzlehttp/guzzle from 6.5.4 to 6.5.5 (#411) 2020-07-03 05:22:51 +00:00
dependabot[bot]
dc65645269
Bump @types/chart.js from 2.9.21 to 2.9.22 (#416) 2020-07-03 05:22:14 +00:00
dependabot[bot]
d9fc2a4fca
Bump phpunit/phpunit from 8.5.6 to 8.5.8 (#419) 2020-07-03 05:21:25 +00:00
shibafu
5215398149
Merge pull request #423 from shikorism/fix/old-narou
古いなろう作品のIDにマッチングするように
2020-07-03 09:22:31 +09:00
Hinaloe
7afcf3f339
古いなろう作品のIDがマッチしていない 2020-07-02 00:20:35 +09:00
dependabot[bot]
261e27a3eb
Bump vue-property-decorator from 8.3.0 to 9.0.0 (#415) 2020-06-21 06:51:53 +00:00
dependabot[bot]
9db8fe78bb
Bump symfony/css-selector from 4.4.9 to 4.4.10 (#404) 2020-06-17 14:16:51 +00:00
dependabot[bot]
688f24193f
Bump symfony/thanks from 1.2.7 to 1.2.8 (#408) 2020-06-17 14:16:07 +00:00
dependabot[bot]
dc055c9017
Bump phpunit/phpunit from 8.5.5 to 8.5.6 (#409) 2020-06-17 14:15:24 +00:00
dependabot[bot]
28e7497e53
Bump laravel/framework from 6.18.19 to 6.18.20 (#410) 2020-06-17 11:31:11 +00:00
dependabot[bot]
06a5bfac6b
Bump laravel/framework from 6.18.18 to 6.18.19 (#403) 2020-06-12 00:27:23 +00:00
dependabot[bot]
9a94f2ce42
Bump stylelint-config-recess-order from 2.0.3 to 2.0.4 (#399) 2020-06-09 00:01:45 +00:00
shibafu
43e22045ae
Merge pull request #396 from shikorism/fix/lint-staged
[fix] lint-staged のglobパターンが誤ってた
2020-06-07 23:20:49 +09:00
shibafu
4fbb047485
Merge pull request #397 from shikorism/lang/auth
auth言語ファイルの追加
2020-06-07 23:20:20 +09:00
hina
c28967a5ae
言語ファイルの追加 2020-06-07 21:27:49 +09:00
hina
a3e687bdb4
[fix] lint-staged のglobパターンが誤ってた 2020-06-07 19:34:27 +09:00
dependabot[bot]
6b15dcaef3
Bump resolve-url-loader from 2.3.2 to 3.1.1 (#372) 2020-06-07 08:40:16 +00:00
dependabot[bot]
f28043e192
Bump sass from 1.25.0 to 1.26.8 (#388) 2020-06-07 08:17:02 +00:00
dependabot[bot]
79b873308a
Merge pull request #394 from shikorism/dependabot/npm_and_yarn/websocket-extensions-0.1.4 2020-06-07 08:07:50 +00:00
dependabot[bot]
ecaa6e8110
Bump symfony/css-selector from 4.4.8 to 4.4.9 (#385) 2020-06-06 14:41:07 +00:00
dependabot[bot]
9fd0f0774d
Bump websocket-extensions from 0.1.3 to 0.1.4
Bumps [websocket-extensions](https://github.com/faye/websocket-extensions-node) from 0.1.3 to 0.1.4.
- [Release notes](https://github.com/faye/websocket-extensions-node/releases)
- [Changelog](https://github.com/faye/websocket-extensions-node/blob/master/CHANGELOG.md)
- [Commits](https://github.com/faye/websocket-extensions-node/compare/0.1.3...0.1.4)

Signed-off-by: dependabot[bot] <support@github.com>
2020-06-06 13:36:06 +00:00
shibafu
d0edea659e
Merge pull request #393 from shikorism/add-eslint
eslintをいれる
2020-06-06 20:21:57 +09:00
hina
b9ed86b69a
@typescript-eslint/no-non-null-assertion を黙らせる 2020-06-06 19:12:00 +09:00
hina
2693d340c6
allowed underscore prefixed unused var 2020-06-06 18:40:49 +09:00
hina
b909035fe8
disable @typescript-eslint/explicit-module-boundary-types 2020-06-06 18:11:49 +09:00
hina
b908351a35
errorをつぶす 2020-06-06 18:03:51 +09:00
hina
edd7a6f4c8
とりあえず一旦これで自動修正 2020-06-06 18:01:56 +09:00
hina
334c810b8d
リーナスも80文字は短いみたいなこといってたし120文字でええやろ 2020-06-06 17:51:17 +09:00
hina
51d9b74697
duplicate 2020-06-06 17:45:26 +09:00
hina
c8c278653a
eslintの仮構成 2020-06-06 17:37:01 +09:00
Hinaloe
d3592f9fea
Merge pull request #391 from shikorism/tsf
全フロントエンドコードのTypeScript化
2020-06-06 17:03:03 +09:00
shibafu
a67a6a8be9 型を付ける音「カタカタ...」 2020-06-06 16:33:49 +09:00
shibafu
57c6eff442
Merge pull request #390 from shikorism/moment-locale
JSバンドルからMomentを除去する
2020-06-06 16:32:48 +09:00
hina
5d16c3b680
moment自体をexternalsで追い出す 2020-06-06 15:38:40 +09:00
hina
aae9e0c502
草をうつす 2020-06-06 07:11:56 +09:00
hina
2a988275f3
Add moment-locales-webpack-plugin 2020-06-06 06:48:00 +09:00
hina
350360e2a9
D3をchartのチャンクに含める 2020-06-06 06:47:59 +09:00
dependabot[bot]
9aa3bb43d3
Merge pull request #383 from shikorism/dependabot/composer/symfony/dom-crawler-4.4.9 2020-06-05 16:04:59 +00:00
dependabot[bot]
d0966e14ea
Bump symfony/dom-crawler from 4.4.8 to 4.4.9
Bumps [symfony/dom-crawler](https://github.com/symfony/dom-crawler) from 4.4.8 to 4.4.9.
- [Release notes](https://github.com/symfony/dom-crawler/releases)
- [Changelog](https://github.com/symfony/dom-crawler/blob/master/CHANGELOG.md)
- [Commits](https://github.com/symfony/dom-crawler/compare/v4.4.8...v4.4.9)

Signed-off-by: dependabot[bot] <support@github.com>
2020-06-05 15:59:15 +00:00
dependabot[bot]
9389593c3a
Merge pull request #381 from shikorism/dependabot/composer/laravel/framework-6.18.18 2020-06-05 15:56:08 +00:00
dependabot[bot]
037cfdfba9
Merge pull request #370 from shikorism/dependabot/npm_and_yarn/bootstrap-4.5.0 2020-06-05 15:53:57 +00:00
dependabot[bot]
c559237ffd
Bump laravel/framework from 6.18.15 to 6.18.18
Bumps [laravel/framework](https://github.com/laravel/framework) from 6.18.15 to 6.18.18.
- [Release notes](https://github.com/laravel/framework/releases)
- [Changelog](https://github.com/laravel/framework/blob/7.x/CHANGELOG-6.x.md)
- [Commits](https://github.com/laravel/framework/compare/v6.18.15...v6.18.18)

Signed-off-by: dependabot[bot] <support@github.com>
2020-06-03 05:12:01 +00:00
dependabot[bot]
6c5332081c
Merge pull request #375 from shikorism/dependabot/composer/symfony/thanks-1.2.7 2020-06-02 14:53:38 +00:00
dependabot[bot]
204a0b2b70
Merge pull request #379 from shikorism/dependabot/composer/guzzlehttp/guzzle-6.5.4 2020-06-02 14:52:32 +00:00
dependabot[bot]
b66896632a
Bump bootstrap from 4.4.1 to 4.5.0
Bumps [bootstrap](https://github.com/twbs/bootstrap) from 4.4.1 to 4.5.0.
- [Release notes](https://github.com/twbs/bootstrap/releases)
- [Commits](https://github.com/twbs/bootstrap/compare/v4.4.1...v4.5.0)

Signed-off-by: dependabot[bot] <support@github.com>
2020-06-02 14:48:12 +00:00
shibafu
4bac437d39
Merge pull request #374 from shikorism/dependabot/npm_and_yarn/types/jquery-3.3.38
Bump @types/jquery from 3.3.31 to 3.3.38
2020-06-02 23:46:04 +09:00
dependabot[bot]
e4e065c614
Bump guzzlehttp/guzzle from 6.5.3 to 6.5.4
Bumps [guzzlehttp/guzzle](https://github.com/guzzle/guzzle) from 6.5.3 to 6.5.4.
- [Release notes](https://github.com/guzzle/guzzle/releases)
- [Changelog](https://github.com/guzzle/guzzle/blob/6.5.4/CHANGELOG.md)
- [Commits](https://github.com/guzzle/guzzle/compare/6.5.3...6.5.4)

Signed-off-by: dependabot[bot] <support@github.com>
2020-06-02 14:17:34 +00:00
dependabot[bot]
5284b93d40
Bump symfony/thanks from 1.2.5 to 1.2.7
Bumps [symfony/thanks](https://github.com/symfony/thanks) from 1.2.5 to 1.2.7.
- [Release notes](https://github.com/symfony/thanks/releases)
- [Commits](https://github.com/symfony/thanks/compare/v1.2.5...v1.2.7)

Signed-off-by: dependabot[bot] <support@github.com>
2020-06-02 14:09:23 +00:00
dependabot[bot]
f6fe04b766
Bump @types/jquery from 3.3.31 to 3.3.38
Bumps [@types/jquery](https://github.com/DefinitelyTyped/DefinitelyTyped/tree/HEAD/types/jquery) from 3.3.31 to 3.3.38.
- [Release notes](https://github.com/DefinitelyTyped/DefinitelyTyped/releases)
- [Commits](https://github.com/DefinitelyTyped/DefinitelyTyped/commits/HEAD/types/jquery)

Signed-off-by: dependabot[bot] <support@github.com>
2020-06-02 14:08:03 +00:00
shibafu
5f7a25e1b8
Merge pull request #368 from shikorism/chore/install-depandabot
dependabotのセットアップ
2020-06-02 23:06:38 +09:00
Hinaloe
b71ee87691
Create dependabot.yml 2020-06-02 22:58:11 +09:00
shibafu
83b0ccf770
Merge pull request #367 from shikorism/feature/user-agent
外部へのHTTPリクエストに独自のUser-Agentを付与する
2020-06-02 22:51:43 +09:00
shibafu
77e83e48d3 yamlはクソ 2020-06-02 22:41:00 +09:00
shibafu
c93106cab9 ふぁっきゅー 2020-06-02 22:40:00 +09:00
shibafu
4c04deb80f 外部へのHTTPリクエストに独自のUser-Agentを付与する 2020-06-02 22:20:26 +09:00
shibafu
da8af95043 管理画面のお知らせのリンクもおかしかった 2020-05-25 00:57:52 +09:00
shibafu
9fcf9cd49e
Merge pull request #362 from shikorism/fix/checkin-user-link
チェックインのユーザーリンクのパラメータ名がおかしいのを修正
2020-05-24 22:43:47 +09:00
shibafu
b5a72e33eb チェックインのユーザーリンクのパラメータ名がおかしいのを修正 2020-05-24 22:37:23 +09:00
shibafu
36c8d84555 オカズページの存在忘れてた 2020-05-24 21:44:18 +09:00
shibafu
ced289b95d
Merge pull request #358 from shikorism/feature/remake-user-page
グラフページの再編
2020-05-24 21:23:23 +09:00
shibafu
40f45da282
Merge pull request #356 from shikorism/feature/302-bookmarklet
ブックマークレット等の案内ページ
2020-05-24 21:23:06 +09:00
shibafu
0218035aeb
Merge pull request #359 from shikorism/fix/ej-source-nonnull
ejaculations.sourceはNULL非許容にする
2020-05-24 21:22:13 +09:00
shibafu
1a3cead932 fix test 2020-05-24 19:50:51 +09:00
shibafu
f3a5644a32 スコープの簡略化 2020-05-24 19:42:16 +09:00
shibafu
e2f01561a8 ejaculations.sourceはNULL非許容にする 2020-05-24 19:39:24 +09:00
shibafu
8fb8677406 プロフィールトップに最近のシコ草を移動 2020-05-24 19:03:15 +09:00
shibafu
8ac65f1cc6 fix params 2020-05-24 18:32:45 +09:00
shibafu
5a3e41c82e fix class 2020-05-24 18:30:48 +09:00
shibafu
c9fd46d408 共通化 2020-05-24 18:01:55 +09:00
shibafu
ea2894cd4b 簡単なバリデーション 2020-05-24 17:49:22 +09:00
shibafu
2f4c61c900 rename view/route 2020-05-24 17:49:17 +09:00
shibafu
019412c72a グラフページを対象期間ごとに掘り下げる形に変更 2020-05-24 17:27:42 +09:00
shibafu
00f75f33cc Update README 2020-05-24 12:02:34 +09:00
shibafu
3a4c734bcf
Merge pull request #357 from shikorism/laravel-6
Laravel 6
2020-05-24 12:00:55 +09:00
shibafu
ee7b7cf274 add parsedown 2020-05-24 00:28:19 +09:00
shibafu
34c4f13376 Laravel 6 2020-05-23 23:09:55 +09:00
shibafu
80590888df QUEUE_CONNECTION -> QUEUE_DRIVER (revert) 2020-05-23 22:31:17 +09:00
shibafu
74fee83f4e 非推奨のヘルパ関数の利用をやめる 2020-05-23 22:26:47 +09:00
shibafu
a48fe596e1 Laravel 5.8 2020-05-23 22:17:07 +09:00
shibafu
cb4dacdaf6 composer update忘れてた 2020-05-23 21:36:22 +09:00
shibafu
a531e5c9bb Laravel 5.7 2020-05-23 21:34:54 +09:00
shibafu
17ee207281 change pagination file name for La5.6 2020-05-23 20:42:00 +09:00
shibafu
015bcaa563 Laravel 5.6 2020-05-23 20:32:24 +09:00
shibafu
36a2ab5fe2 ブックマークレット等の案内ページ 2020-05-23 17:37:53 +09:00
shibafu
3a05d2c9cc
Merge pull request #354 from shikorism/feature/319-csv-import
チェックインデータのインポート
2020-05-23 16:35:31 +09:00
shibafu
948b517c4d add doc 2020-05-23 16:26:07 +09:00
shibafu
cc0e0271b8 add test 2020-05-23 15:56:51 +09:00
shibafu
5af55fa6b4 fix typo 2020-05-23 15:39:28 +09:00
shibafu
bd84effedc インポート可能な件数に上限を設ける 2020-05-23 15:35:36 +09:00
shibafu
c0d62f5112 インポートしたデータを消せるようにする 2020-05-23 02:47:25 +09:00
shibafu
1853c28093
Merge pull request #355 from shikorism/fix/purge-squatters
未来のオカズはお惣菜として出さない
2020-05-22 23:57:34 +09:00
shibafu
77f4bbcfce 未来のオカズはお惣菜として出さない 2020-05-22 23:53:14 +09:00
shibafu
fb5b34b239 strict_types 2020-05-22 23:12:41 +09:00
shibafu
531067fb9c 取り込み件数を出力 2020-05-22 22:11:49 +09:00
shibafu
3a2d0e67aa 半角スペースを含むタグは受け付けない 2020-05-22 00:27:32 +09:00
shibafu
54b6ff2282 インポートバッジ 2020-05-22 00:07:04 +09:00
shibafu
023446e0a8 CSVインポートのチェックインはお惣菜コーナーに表示しない 2020-05-21 23:58:58 +09:00
shibafu
8681c328d0 ejaculated_dateの重複エラーをcatch 2020-05-21 23:13:39 +09:00
shibafu
fa4827f382 インポート処理のコントローラーとか実装 2020-05-21 22:33:27 +09:00
shibafu
15c462449f
Merge pull request #353 from shikorism/fix/csv-export-date-format
CSVエクスポートの日付書式が仕様と違っていた
2020-05-21 22:32:37 +09:00
shibafu
03b05a48cd CSVエクスポートの日付書式が仕様と違っていた 2020-05-21 22:28:34 +09:00
shibafu
1671070d0c Merge branch 'feature/319-csv-export' into develop 2020-05-20 21:56:37 +09:00
shibafu
9f047544b9
Merge pull request #352 from shikorism/feature/319-csv-export
チェックインデータのエクスポート
2020-05-20 21:49:23 +09:00
shibafu
988ad9d992 タグ出力数の上限 2020-05-20 21:42:22 +09:00
shibafu
5668296e7d add 気休め 2020-05-20 21:28:46 +09:00
shibafu
8a764c756c use custom control style 2020-05-20 21:07:11 +09:00
shibafu
cf9df74ed3 Export checkin to CSV 2020-05-20 01:50:34 +09:00
shibafu
e872964144
Merge pull request #344 from shikorism/feature/319-csv-importer
CheckinCsvImporter
2020-05-18 01:05:58 +09:00
shibafu
5db25ce017
Merge pull request #350 from shikorism/fix/unique-metadata-and-tags
metadata.urlをPKに、tags.nameをUNIQUEに設定
2020-05-16 17:07:50 +09:00
shibafu
a58a206b86 metadata.urlをPKに、tags.nameをUNIQUEに設定 2020-05-16 17:01:14 +09:00
shibafu
dcdd33925b
Merge pull request #348 from shikorism/fix/tag-to-metadata-assoc
Tag -> Metadata のリレーション設定
2020-05-16 15:29:39 +09:00
shibafu
552c2d4460 Tag -> Metadata のリレーション設定 2020-05-16 15:25:03 +09:00
shibafu
c11bbbbff7
Merge pull request #331 from shikorism/fix/330-tag-dedup
メタデータのタグ重複対策
2020-05-16 15:00:51 +09:00
shibafu
d33d7b58a4
Merge pull request #346 from shikorism/feature/maintenance-template
メンテナンス用ビューテンプレート
2020-05-16 15:00:42 +09:00
shibafu
2a50ee4f5b メンテナンス中はヘッダーにログイン等を表示しない 2020-05-16 14:47:17 +09:00
shibafu
e9414517b9 メンテナンス用ビューテンプレート 2020-05-16 14:31:05 +09:00
shibafu
ab9ecd6bfa
Merge pull request #345 from shikorism/fix/334-okazu-with-likes
/user/{name}/okazu にもいいねしたユーザーのアイコンを表示する
2020-05-14 01:26:55 +09:00
shibafu
59c6e4a52a /user/{name}/okazu にもいいねしたユーザーのアイコンを表示する 2020-05-14 01:18:14 +09:00
shibafu
0ca7b8b7e1 タグ列の番号飛びを許容 2020-05-14 00:31:03 +09:00
shibafu
882f239d58
Merge pull request #343 from shikorism/feature/integrate-metadata-resolve
メタデータの解決と保存の処理を統一
2020-05-14 00:02:51 +09:00
shibafu
4c3c5f18d2 メタデータの解決と保存の処理を統一 2020-05-10 18:50:03 +09:00
shibafu
1953143ab2
Merge pull request #341 from shikorism/lib/jquery-3.5.1
Bump jquery from 3.5.0 to 3.5.1
2020-05-10 16:31:00 +09:00
shibafu
8c2f7d4212 Bump jquery from 3.5.0 to 3.5.1 2020-05-10 16:19:32 +09:00
shibafu
f2b91b71f8 Merge commit '631d2ea298f62788dd0a14c65f553e0d0ec8322c' into develop 2020-05-09 22:54:38 +09:00
dependabot[bot]
ef6fd71cee
Merge pull request #336 from shikorism/dependabot/npm_and_yarn/jquery-3.5.0 2020-04-30 16:37:39 +00:00
dependabot[bot]
55fba9e2a2
Merge pull request #333 from shikorism/dependabot/npm_and_yarn/acorn-6.4.1 2020-04-30 16:36:35 +00:00
dependabot[bot]
7f4dd703db
Bump jquery from 3.4.1 to 3.5.0
Bumps [jquery](https://github.com/jquery/jquery) from 3.4.1 to 3.5.0.
- [Release notes](https://github.com/jquery/jquery/releases)
- [Commits](https://github.com/jquery/jquery/compare/3.4.1...3.5.0)

Signed-off-by: dependabot[bot] <support@github.com>
2020-04-30 01:40:50 +00:00
dependabot[bot]
49bc254e8b
Bump acorn from 6.4.0 to 6.4.1
Bumps [acorn](https://github.com/acornjs/acorn) from 6.4.0 to 6.4.1.
- [Release notes](https://github.com/acornjs/acorn/releases)
- [Commits](https://github.com/acornjs/acorn/compare/6.4.0...6.4.1)

Signed-off-by: dependabot[bot] <support@github.com>
2020-03-14 03:06:40 +00:00
shibafu
631ae820f3 add unique constraints 2020-02-22 16:46:15 +09:00
shibafu
77d3ebd452 重複タグの削除コマンド 2020-02-22 16:32:49 +09:00
shibafu
631d2ea298 タグを保存前に正規化する 2020-02-22 14:39:25 +09:00
shibafu
cb0af4113d
Merge pull request #328 from shikorism/fix/325-tune-public-tl
トップページのお惣菜表示量をお惣菜コーナー1ページ分にする
2020-02-22 10:10:30 +09:00
shibafu
46ca276eab トップページのお惣菜表示量をお惣菜コーナー1ページ分にする 2020-02-20 22:10:04 +09:00
shibafu
41ce5229c7 これはいわゆるサービスなのではと思った 2020-02-18 02:09:01 +09:00
shibafu
6387d4e853 チェックインデータがCSVで投入されたことを記録できるようにした 2020-02-18 02:03:49 +09:00
shibafu
0a53199399 ネストをちょっと減らした 2020-02-16 23:21:17 +09:00
shibafu
794cdf2be6 argument unpackingの存在を思い出した 2020-02-16 23:17:07 +09:00
shibafu
84b955b195 continue 2はキモい 2020-02-16 23:09:30 +09:00
shibafu
12553321e1
Merge pull request #327 from shikorism/feature/xdebug-cli-wrapper
IDEからのデバッグ実行用ラッパーを追加
2020-02-16 22:51:44 +09:00
shibafu
24a5017334 タグ列のテスト 2020-02-16 22:43:18 +09:00
shibafu
272e7ecc61 エラーメッセージの統一感を高めた 2020-02-16 22:43:18 +09:00
shibafu
22845fe279 オカズリンクの判定テスト 2020-02-16 22:43:18 +09:00
shibafu
45eba30528 文字コード判定が上手く行かないケースがあったので、もう少し粘るようなアルゴリズムにした 2020-02-16 19:52:50 +09:00
shibafu
7259ee3647 ノートの改行コードを正規化 2020-02-16 19:52:50 +09:00
shibafu
3def26ddb9 Fixtureの改行コードをCRLFに変更 2020-02-16 19:52:50 +09:00
shibafu
cef69a1545 ノートの文字数,改行などのチェック 2020-02-16 19:52:49 +09:00
shibafu
ea59bcf150 日付形式と範囲のチェック 2020-02-16 19:52:33 +09:00
shibafu
b29a82435c DataProviderを使ってテストを書いてみる 2020-02-16 19:52:30 +09:00
shibafu
67b697a600 Validatorを使いたくなった
日時のバリデーションが思うように動いていなかったので、カスタムルールを追加
2020-02-16 19:52:23 +09:00
shibafu
64065ce9e6 ざっくりとした処理を書いた 2020-02-16 19:52:19 +09:00
shibafu
8b9abe2d7b IDEからのデバッグ実行用ラッパーを追加 2020-02-16 19:34:45 +09:00
shibafu
a5b9021e95
Merge pull request #324 from shikorism/fix/323-cien-pattern
Ci-enのドメインが変わっていたのでパターンに追加
2020-02-14 01:28:39 +09:00
shibafu
92e1131d20 全年齢向けドメインをパターンに追加 2020-02-13 00:18:58 +09:00
shibafu
af02375475 Ci-enのドメインが変わっていたのでパターンに追加 2020-02-11 17:14:24 +09:00
shibafu
267eb48589
Merge pull request #322 from eai04191/feature/chart-nearest-fix
statsのグラフでカーソルに近い位置のツールチップを表示する
2020-02-11 00:20:03 +09:00
shibafu
38ae540009 文字コード判定と必須列のチェックまで 2020-02-04 01:44:16 +09:00
shibafu
b2eed9a9c5 league/csvとopenpear/stream_filter_mbstringを依存関係に追加 2020-02-04 01:43:58 +09:00
eai04191
e7e4195a10 グラフでカーソルに近い位置のツールチップを表示する 2020-02-03 01:12:52 +09:00
shibafu
7968019e1c
Merge pull request #320 from shikorism/fix/318-profile-image-srcset
プロフィール画像のsrcsetを出力するヘルパーを追加
2020-01-29 22:52:51 +09:00
shibafu
1d0c51c284 rename argument 2020-01-29 08:38:13 +09:00
hina
c9af8130f4
Add test for profileImageSrcSet 2020-01-29 03:41:14 +09:00
shibafu
9431cd5b5d プロフィール画像のsrcsetを出力するヘルパーを追加 2020-01-28 01:38:30 +09:00
284 changed files with 41810 additions and 33233 deletions

View File

@ -24,8 +24,13 @@ commands:
- checkout
- run: sudo apt update
- run: sudo apt install -y libpq-dev
- run: sudo pecl install -f xdebug-2.9.8 && sudo docker-php-ext-enable xdebug
- run: sudo docker-php-ext-install zip
- run: sudo docker-php-ext-install pdo_pgsql
- run:
command: |
curl -sSL "https://nodejs.org/dist/v12.16.3/node-v12.16.3-linux-x64.tar.xz" | sudo tar --strip-components=2 -xJ -C /usr/local/bin/ node-v12.16.3-linux-x64/bin/node
curl https://www.npmjs.com/install.sh | sudo bash
restore_composer:
steps:
- restore_cache:
@ -101,6 +106,12 @@ jobs:
command: yarn run stylelint
when: always
# Run eslint
- run:
name: eslint
command: yarn run eslint
when: always
# Run unit test
- run:
command: |

View File

@ -11,7 +11,7 @@ insert_final_newline = true
[*.md]
trim_trailing_whitespace = false
[*.yml]
[*.{yml,yaml}]
indent_size = 2
[*.json]

View File

@ -2,12 +2,16 @@ APP_NAME=Tissue
APP_ENV=local
APP_KEY=
APP_DEBUG=true
APP_LOG_LEVEL=debug
APP_URL=http://localhost
LOG_CHANNEL=stack
# テストにモックを使用するか falseの場合は実際のHTML等を取得してテストする
TEST_USE_HTTP_MOCK=true
# テスト用のスナップショットを更新する場合はtrueにする (TEST_USE_HTTP_MOCKと重複させないよう注意)
TEST_UPDATE_SNAPSHOT=false
DB_CONNECTION=pgsql
DB_HOST=db
DB_PORT=5432
@ -33,13 +37,23 @@ MAIL_ENCRYPTION=tls
MAIL_FROM_ADDRESS=support@mail.shikorism.net
MAIL_FROM_NAME=Tissue
AWS_ACCESS_KEY_ID=
AWS_SECRET_ACCESS_KEY=
AWS_DEFAULT_REGION=us-east-1
AWS_BUCKET=
SPARKPOST_SECRET=
PUSHER_APP_ID=
PUSHER_APP_KEY=
PUSHER_APP_SECRET=
MIX_PUSHER_APP_KEY="${PUSHER_APP_KEY}"
MIX_PUSHER_APP_CLUSTER="${PUSHER_APP_CLUSTER}"
# (Optional) reCAPTCHA Key
# https://www.google.com/recaptcha
NOCAPTCHA_SECRET=
NOCAPTCHA_SITEKEY=
SENTRY_LARAVEL_DSN=

38
.eslintrc.js vendored Normal file
View File

@ -0,0 +1,38 @@
module.exports = {
env: {
browser: true,
es2020: true,
node: true,
},
extends: [
'eslint:recommended',
'plugin:react/recommended',
'plugin:prettier/recommended',
'plugin:@typescript-eslint/recommended',
'prettier',
'prettier/@typescript-eslint',
'prettier/react',
],
parser: '@typescript-eslint/parser',
parserOptions: {
ecmaVersion: 11,
ecmaFeatures: {
jsx: true,
},
sourceType: 'module',
},
plugins: ['prettier', 'react', '@typescript-eslint', 'jquery'],
rules: {
'@typescript-eslint/explicit-module-boundary-types': 0,
'@typescript-eslint/no-explicit-any': 0,
'@typescript-eslint/no-unused-vars': ['warn', { argsIgnorePattern: '^_' }],
'jquery/no-ajax': 2,
'jquery/no-ajax-events': 2,
'react/prop-types': 0,
},
settings: {
react: {
version: 'detect',
},
},
};

14
.github/dependabot.yml vendored Normal file
View File

@ -0,0 +1,14 @@
version: 2
updates:
# Maintain dependencies for npm
- package-ecosystem: "npm"
directory: "/"
schedule:
interval: "daily"
# Maintain dependencies for Composer
- package-ecosystem: "composer"
directory: "/"
schedule:
interval: "daily"

4
.gitignore vendored
View File

@ -5,6 +5,8 @@
/public/hot
/public/storage
/public/mix-manifest.json
/public/report.html
/public/apidoc.html
/storage/*.key
/vendor
/.idea
@ -15,6 +17,8 @@ Homestead.yaml
npm-debug.log
yarn-error.log
.env
.env.backup
.phpunit.result.cache
*.iml
.php_cs
.php_cs.cache

5
.prettierrc Normal file
View File

@ -0,0 +1,5 @@
{
"arrowParens": "always",
"singleQuote": true,
"printWidth": 120
}

View File

@ -5,12 +5,11 @@ FROM php:7.3-apache
ENV APACHE_DOCUMENT_ROOT /var/www/html/public
RUN apt-get update \
&& apt-get install -y git libpq-dev unzip \
&& docker-php-ext-install pdo_pgsql \
&& apt-get install -y git libpq-dev unzip libicu-dev \
&& docker-php-ext-install pdo_pgsql intl \
&& pecl install xdebug \
&& curl -sS https://getcomposer.org/installer | php \
&& mv composer.phar /usr/local/bin/composer \
&& composer global require hirak/prestissimo \
&& 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

1
Procfile Normal file
View File

@ -0,0 +1 @@
web: vendor/bin/heroku-php-apache2 public/

View File

@ -7,8 +7,8 @@ a.k.a. shikorism.net
## 構成
- Laravel 5.5
- Bootstrap 4.3.1
- Laravel 6
- Bootstrap 4.4.1
## 実行環境

39
app/CheckinWebhook.php Normal file
View File

@ -0,0 +1,39 @@
<?php
namespace App;
use Illuminate\Database\Eloquent\Model;
use Illuminate\Database\Eloquent\SoftDeletes;
use Illuminate\Support\Str;
class CheckinWebhook extends Model
{
use SoftDeletes;
/** @var int ユーザーごとの作成数制限 */
const PER_USER_LIMIT = 10;
public $incrementing = false;
protected $keyType = 'string';
protected $fillable = ['name'];
protected static function boot()
{
parent::boot();
self::creating(function (CheckinWebhook $webhook) {
$webhook->id = Str::random(64);
});
}
public function user()
{
return $this->belongsTo(User::class);
}
public function isAvailable()
{
return $this->user !== null;
}
}

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

@ -0,0 +1,61 @@
<?php
namespace App\Console\Commands;
use App\Tag;
use App\Utilities\Formatter;
use DB;
use Illuminate\Console\Command;
class NormalizeTags extends Command
{
/**
* The name and signature of the console command.
*
* @var string
*/
protected $signature = 'tissue:tag:normalize';
/**
* The console command description.
*
* @var string
*/
protected $description = 'Normalize tags';
private $formatter;
/**
* Create a new command instance.
*
* @return void
*/
public function __construct(Formatter $formatter)
{
parent::__construct();
$this->formatter = $formatter;
}
/**
* Execute the console command.
*
* @return mixed
*/
public function handle()
{
$start = hrtime(true);
DB::transaction(function () {
/** @var Tag $tag */
foreach (Tag::query()->cursor() as $tag) {
$normalizedName = $this->formatter->normalizeTagName($tag->name);
$this->line("{$tag->name} : {$normalizedName}");
$tag->normalized_name = $normalizedName;
$tag->save();
}
});
$elapsed = (hrtime(true) - $start) / 1e+9;
$this->info("Done! ({$elapsed} sec)");
}
}

24
app/ContentProvider.php Normal file
View File

@ -0,0 +1,24 @@
<?php
namespace App;
use Illuminate\Database\Eloquent\Model;
class ContentProvider extends Model
{
public $incrementing = false;
protected $primaryKey = 'host';
protected $keyType = 'string';
protected $fillable = [
'host',
'robots',
'robots_cached_at',
];
protected $dates = [
'created_at',
'updated_at',
'robots_cached_at',
];
}

View File

@ -12,10 +12,15 @@ class Ejaculation extends Model
{
use HasEagerLimit;
const SOURCE_WEB = 'web';
const SOURCE_CSV = 'csv';
const SOURCE_WEBHOOK = 'webhook';
protected $fillable = [
'user_id', 'ejaculated_date',
'note', 'geo_latitude', 'geo_longitude', 'link',
'is_private', 'is_too_sensitive'
'note', 'geo_latitude', 'geo_longitude', 'link', 'source',
'is_private', 'is_too_sensitive', 'discard_elapsed_time',
'checkin_webhook_id'
];
protected $dates = [
@ -44,6 +49,11 @@ class Ejaculation extends Model
return $this->hasMany(Like::class);
}
public function scopeVisibleToTimeline(Builder $query)
{
return $query->whereIn('ejaculations.source', [Ejaculation::SOURCE_WEB, Ejaculation::SOURCE_WEBHOOK]);
}
public function scopeWithLikes(Builder $query)
{
if (Auth::check()) {
@ -91,7 +101,37 @@ class Ejaculation extends Model
return route('checkin', [
'link' => $this->link,
'tags' => $this->textTags(),
'is_private' => $this->is_private,
'is_too_sensitive' => $this->is_too_sensitive,
]);
}
public function ejaculatedSpan(): string
{
if (array_key_exists('ejaculated_span', $this->attributes)) {
if ($this->ejaculated_span === null) {
return '精通';
}
if ($this->discard_elapsed_time) {
return '0日 0時間 0分'; // TODO: 気の効いたフレーズにする
}
return $this->ejaculated_span;
} else {
$previous = Ejaculation::select('ejaculated_date')
->where('user_id', $this->user_id)
->where('ejaculated_date', '<', $this->ejaculated_date)
->orderByDesc('ejaculated_date')
->first();
if ($previous === null) {
return '精通';
}
if ($this->discard_elapsed_time) {
return '0日 0時間 0分';
}
return $this->ejaculated_date->diff($previous->ejaculated_date)->format('%a日 %h時間 %i分');
}
}
}

View File

@ -0,0 +1,30 @@
<?php
namespace App\Exceptions;
use Illuminate\Support\Arr;
use Throwable;
class CsvImportException extends \RuntimeException
{
/** @var string[] */
private $errors;
/**
* CsvImportException constructor.
* @param string[] $errors
*/
public function __construct(...$errors)
{
parent::__construct(Arr::first($errors));
$this->errors = $errors;
}
/**
* @return string[]
*/
public function getErrors(): array
{
return $this->errors;
}
}

View File

@ -4,7 +4,10 @@ namespace App\Exceptions;
use Exception;
use Illuminate\Auth\AuthenticationException;
use Illuminate\Database\Eloquent\ModelNotFoundException;
use Illuminate\Foundation\Exceptions\Handler as ExceptionHandler;
use Illuminate\Http\JsonResponse;
use Symfony\Component\HttpKernel\Exception\NotFoundHttpException;
class Handler extends ExceptionHandler
{
@ -20,18 +23,23 @@ class Handler extends ExceptionHandler
\Illuminate\Database\Eloquent\ModelNotFoundException::class,
\Illuminate\Session\TokenMismatchException::class,
\Illuminate\Validation\ValidationException::class,
\App\MetadataResolver\ResolverCircuitBreakException::class,
];
/**
* Report or log an exception.
*
* This is a great spot to send exceptions to Sentry, Bugsnag, etc.
*
* @param \Exception $exception
* @return void
*
* @throws \Exception
*/
public function report(Exception $exception)
{
if (app()->bound('sentry') && $this->shouldReport($exception)) {
app('sentry')->captureException($exception);
}
parent::report($exception);
}
@ -40,7 +48,9 @@ class Handler extends ExceptionHandler
*
* @param \Illuminate\Http\Request $request
* @param \Exception $exception
* @return \Illuminate\Http\Response
* @return \Symfony\Component\HttpFoundation\Response
*
* @throws \Exception
*/
public function render($request, Exception $exception)
{
@ -62,4 +72,28 @@ class Handler extends ExceptionHandler
return redirect()->guest(route('login'));
}
protected function prepareException(Exception $e)
{
if (!config('app.debug') && $e instanceof ModelNotFoundException) {
return new NotFoundHttpException('Resource not found.', $e);
}
return parent::prepareException($e);
}
protected function prepareJsonResponse($request, Exception $e)
{
$status = $this->isHttpException($e) ? $e->getStatusCode() : 500;
return new JsonResponse(
[
'status' => $status,
'error' => $this->convertExceptionToArray($e),
],
$status,
$this->isHttpException($e) ? $e->getHeaders() : [],
JSON_PRETTY_PRINT | JSON_UNESCAPED_SLASHES
);
}
}

View File

@ -44,12 +44,10 @@ class InfoController extends Controller
return redirect()->route('admin.info.edit', ['info' => $info])->with('status', 'お知らせを更新しました。');
}
public function edit($id)
public function edit(Information $info)
{
$information = Information::findOrFail($id);
return view('admin.info.edit')->with([
'info' => $information,
'info' => $info,
'categories' => Information::CATEGORIES
]);
}

View File

@ -2,55 +2,23 @@
namespace App\Http\Controllers\Api;
use App\Metadata;
use App\MetadataResolver\MetadataResolver;
use App\Tag;
use App\Utilities\Formatter;
use App\MetadataResolver\DeniedHostException;
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->tags as $tagName) {
$tag = Tag::firstOrCreate(['name' => $tagName]);
$tagIds[] = $tag->id;
}
$metadata->tags()->sync($tagIds);
try {
$metadata = $service->execute($request->input('url'));
} catch (DeniedHostException $e) {
abort(403, $e->getMessage());
}
$metadata->load('tags');
$response = response($metadata);

View File

@ -0,0 +1,105 @@
<?php
namespace App\Http\Controllers\Api;
use App\CheckinWebhook;
use App\Ejaculation;
use App\Events\LinkDiscovered;
use App\Http\Controllers\Controller;
use App\Http\Resources\EjaculationResource;
use App\Tag;
use Carbon\Carbon;
use Illuminate\Http\Request;
use Illuminate\Support\Facades\DB;
use Illuminate\Support\Facades\Validator;
use Illuminate\Validation\ValidationException;
class WebhookController extends Controller
{
public function checkin(CheckinWebhook $webhook, Request $request)
{
if (!$webhook->isAvailable()) {
return response()->json([
'status' => 404,
'error' => [
'message' => 'The webhook is unavailable'
]
], 404);
}
$validator = Validator::make($request->all(), [
'checked_in_at' => 'nullable|date|after_or_equal:2000-01-01 00:00:00|before_or_equal:2099-12-31 23:59:59',
'note' => 'nullable|string|max:500',
'link' => 'nullable|url|max:2000',
'tags' => 'nullable|array',
'tags.*' => ['string', 'not_regex:/[\s\r\n]/u', 'max:255'],
'is_private' => 'nullable|boolean',
'is_too_sensitive' => 'nullable|boolean',
'discard_elapsed_time' => 'nullable|boolean',
], [
'tags.*.not_regex' => 'The :attribute cannot contain spaces, tabs and newlines.'
]);
try {
$inputs = $validator->validate();
} catch (ValidationException $e) {
return response()->json([
'status' => 422,
'error' => [
'message' => 'Validation failed',
'violations' => $validator->errors()->all(),
]
], 422);
}
$ejaculatedDate = empty($inputs['checked_in_at']) ? now() : new Carbon($inputs['checked_in_at']);
$ejaculatedDate = $ejaculatedDate->setTimezone(date_default_timezone_get())->startOfMinute();
if (Ejaculation::where(['user_id' => $webhook->user_id, 'ejaculated_date' => $ejaculatedDate])->count()) {
return response()->json([
'status' => 422,
'error' => [
'message' => 'Checkin already exists in this time',
]
], 422);
}
$ejaculation = DB::transaction(function () use ($inputs, $webhook, $ejaculatedDate) {
$ejaculation = Ejaculation::create([
'user_id' => $webhook->user_id,
'ejaculated_date' => $ejaculatedDate,
'note' => $inputs['note'] ?? '',
'link' => $inputs['link'] ?? '',
'source' => Ejaculation::SOURCE_WEBHOOK,
'is_private' => (bool)($inputs['is_private'] ?? false),
'is_too_sensitive' => (bool)($inputs['is_too_sensitive'] ?? false),
'discard_elapsed_time' => (bool)($inputs['discard_elapsed_time'] ?? false),
'checkin_webhook_id' => $webhook->id
]);
$tagIds = [];
if (!empty($inputs['tags'])) {
foreach ($inputs['tags'] as $tag) {
$tag = trim($tag);
if ($tag === '') {
continue;
}
$tag = Tag::firstOrCreate(['name' => $tag]);
$tagIds[] = $tag->id;
}
}
$ejaculation->tags()->sync($tagIds);
return $ejaculation;
});
if (!empty($ejaculation->link)) {
event(new LinkDiscovered($ejaculation->link));
}
return response()->json([
'status' => 200,
'checkin' => new EjaculationResource($ejaculation)
]);
}
}

View File

@ -5,6 +5,7 @@ namespace App\Http\Controllers\Auth;
use App\Http\Controllers\Controller;
use App\User;
use Illuminate\Foundation\Auth\RegistersUsers;
use Illuminate\Support\Facades\Hash;
use Illuminate\Support\Facades\Validator;
class RegisterController extends Controller
@ -50,7 +51,7 @@ class RegisterController extends Controller
$rules = [
'name' => 'required|string|regex:/^[a-zA-Z0-9_-]+$/u|max:15|unique:users|unique:deactivated_users',
'email' => 'required|string|email|max:255|unique:users',
'password' => 'required|string|min:6|confirmed'
'password' => 'required|string|min:8|confirmed'
];
// reCAPTCHAのキーが設定されている場合、判定を有効化
@ -78,7 +79,7 @@ class RegisterController extends Controller
'name' => $data['name'],
'display_name' => $data['name'],
'email' => $data['email'],
'password' => bcrypt($data['password']),
'password' => Hash::make($data['password']),
'is_protected' => $data['is_protected'] ?? false,
'accept_analytics' => $data['accept_analytics'] ?? false
]);

View File

@ -0,0 +1,41 @@
<?php
namespace App\Http\Controllers\Auth;
use App\Http\Controllers\Controller;
use Illuminate\Foundation\Auth\VerifiesEmails;
class VerificationController extends Controller
{
/*
|--------------------------------------------------------------------------
| Email Verification Controller
|--------------------------------------------------------------------------
|
| This controller is responsible for handling email verification for any
| user that recently registered with the application. Emails may also
| be re-sent if the user didn't receive the original email message.
|
*/
use VerifiesEmails;
/**
* Where to redirect users after verification.
*
* @var string
*/
protected $redirectTo = '/home';
/**
* Create a new controller instance.
*
* @return void
*/
public function __construct()
{
$this->middleware('auth');
$this->middleware('signed')->only('verify');
$this->middleware('throttle:6,1')->only('verify', 'resend');
}
}

View File

@ -9,23 +9,36 @@ use App\User;
use Carbon\Carbon;
use Illuminate\Http\Request;
use Illuminate\Support\Facades\Auth;
use Illuminate\Support\Facades\DB;
use Validator;
class EjaculationController extends Controller
{
public function create(Request $request)
{
$defaults = [
'date' => $request->input('date', date('Y/m/d')),
'time' => $request->input('time', date('H:i')),
'link' => $request->input('link', ''),
'tags' => $request->input('tags', ''),
'note' => $request->input('note', ''),
'is_private' => $request->input('is_private', 0) == 1,
'is_too_sensitive' => $request->input('is_too_sensitive', 0) == 1
$tags = old('tags') ?? $request->input('tags', '');
if (!empty($tags)) {
$tags = explode(' ', $tags);
}
$errors = $request->session()->get('errors');
$initialState = [
'mode' => 'create',
'fields' => [
'date' => old('date') ?? $request->input('date', date('Y/m/d')),
'time' => old('time') ?? $request->input('time', date('H:i')),
'link' => old('link') ?? $request->input('link', ''),
'tags' => $tags,
'note' => old('note') ?? $request->input('note', ''),
'is_private' => old('is_private') ?? $request->input('is_private', 0) == 1,
'is_too_sensitive' => old('is_too_sensitive') ?? $request->input('is_too_sensitive', 0) == 1,
'is_realtime' => old('is_realtime', true),
'discard_elapsed_time' => old('discard_elapsed_time') ?? $request->input('discard_elapsed_time') == 1,
],
'errors' => isset($errors) ? $errors->getMessages() : null
];
return view('ejaculation.checkin')->with('defaults', $defaults);
return view('ejaculation.checkin')->with('initialState', $initialState);
}
public function store(Request $request)
@ -33,15 +46,20 @@ class EjaculationController extends Controller
$inputs = $request->all();
$validator = Validator::make($inputs, [
'date' => 'required|date_format:Y/m/d',
'time' => 'required|date_format:H:i',
'date' => 'required_without:is_realtime|date_format:Y/m/d',
'time' => 'required_without:is_realtime|date_format:H:i',
'note' => 'nullable|string|max:500',
'link' => 'nullable|url|max:2000',
'tags' => 'nullable|string',
])->after(function ($validator) use ($request, $inputs) {
// 日時の重複チェック
if (!$validator->errors()->hasAny(['date', 'time'])) {
$dt = $inputs['date'] . ' ' . $inputs['time'];
if (isset($inputs['date']) && isset($inputs['time'])) {
$dt = Carbon::createFromFormat('Y/m/d H:i', $inputs['date'] . ' ' . $inputs['time']);
} else {
$dt = now();
}
$dt = $dt->startOfMinute();
if (Ejaculation::where(['user_id' => Auth::id(), 'ejaculated_date' => $dt])->count()) {
$validator->errors()->add('datetime', '既にこの日時にチェックインしているため、登録できません。');
}
@ -49,31 +67,45 @@ class EjaculationController extends Controller
});
if ($validator->fails()) {
return redirect()->route('checkin')->withErrors($validator)->withInput();
return redirect()->route('checkin')
->withErrors($validator)
->withInput(array_merge(['is_realtime' => false], $request->input()));
}
$ejaculation = Ejaculation::create([
'user_id' => Auth::id(),
'ejaculated_date' => Carbon::createFromFormat('Y/m/d H:i', $inputs['date'] . ' ' . $inputs['time']),
'note' => $inputs['note'] ?? '',
'link' => $inputs['link'] ?? '',
'is_private' => $request->has('is_private') ?? false,
'is_too_sensitive' => $request->has('is_too_sensitive') ?? false
]);
$tagIds = [];
if (!empty($inputs['tags'])) {
$tags = explode(' ', $inputs['tags']);
foreach ($tags as $tag) {
if ($tag === '') {
continue;
}
$tag = Tag::firstOrCreate(['name' => $tag]);
$tagIds[] = $tag->id;
$ejaculation = DB::transaction(function () use ($request, $inputs) {
if (isset($inputs['date']) && isset($inputs['time'])) {
$ejaculatedDate = Carbon::createFromFormat('Y/m/d H:i', $inputs['date'] . ' ' . $inputs['time']);
} else {
$ejaculatedDate = now();
}
}
$ejaculation->tags()->sync($tagIds);
$ejaculatedDate = $ejaculatedDate->startOfMinute();
$ejaculation = Ejaculation::create([
'user_id' => Auth::id(),
'ejaculated_date' => $ejaculatedDate,
'note' => $inputs['note'] ?? '',
'link' => $inputs['link'] ?? '',
'source' => Ejaculation::SOURCE_WEB,
'is_private' => $request->has('is_private') ?? false,
'is_too_sensitive' => $request->has('is_too_sensitive') ?? false,
'discard_elapsed_time' => $request->has('discard_elapsed_time') ?? false,
]);
$tagIds = [];
if (!empty($inputs['tags'])) {
$tags = explode(' ', $inputs['tags']);
foreach ($tags as $tag) {
if ($tag === '') {
continue;
}
$tag = Tag::firstOrCreate(['name' => $tag]);
$tagIds[] = $tag->id;
}
}
$ejaculation->tags()->sync($tagIds);
return $ejaculation;
});
if (!empty($ejaculation->link)) {
event(new LinkDiscovered($ejaculation->link));
@ -89,30 +121,41 @@ class EjaculationController extends Controller
->firstOrFail();
$user = User::findOrFail($ejaculation->user_id);
// 1つ前のチェックインからの経過時間を求める
$previousEjaculation = Ejaculation::select('ejaculated_date')
->where('user_id', $ejaculation->user_id)
->where('ejaculated_date', '<', $ejaculation->ejaculated_date)
->orderByDesc('ejaculated_date')
->first();
if (!empty($previousEjaculation)) {
$ejaculatedSpan = $ejaculation->ejaculated_date
->diff($previousEjaculation->ejaculated_date)
->format('%a日 %h時間 %i分');
} else {
$ejaculatedSpan = null;
}
return view('ejaculation.show')->with(compact('user', 'ejaculation', 'ejaculatedSpan'));
return view('ejaculation.show')->with(compact('user', 'ejaculation'));
}
public function edit($id)
public function edit(Request $request, $id)
{
$ejaculation = Ejaculation::findOrFail($id);
$this->authorize('edit', $ejaculation);
return view('ejaculation.edit')->with(compact('ejaculation'));
if (old('tags') === null) {
$tags = $ejaculation->tags->pluck('name');
} else {
$tags = old('tags');
if (!empty($tags)) {
$tags = explode(' ', $tags);
}
}
$errors = $request->session()->get('errors');
$initialState = [
'mode' => 'update',
'fields' => [
'date' => old('date') ?? $ejaculation->ejaculated_date->format('Y/m/d'),
'time' => old('time') ?? $ejaculation->ejaculated_date->format('H:i'),
'link' => old('link') ?? $ejaculation->link,
'tags' => $tags,
'note' => old('note') ?? $ejaculation->note,
'is_private' => is_bool(old('is_private')) ? old('is_private') : $ejaculation->is_private,
'is_too_sensitive' => is_bool(old('is_too_sensitive')) ? old('is_too_sensitive') : $ejaculation->is_too_sensitive,
'discard_elapsed_time' => is_bool(old('discard_elapsed_time')) ? old('discard_elapsed_time') : $ejaculation->discard_elapsed_time,
],
'errors' => isset($errors) ? $errors->getMessages() : null
];
return view('ejaculation.edit')->with(compact('ejaculation', 'initialState'));
}
public function update(Request $request, $id)
@ -143,27 +186,30 @@ class EjaculationController extends Controller
return redirect()->route('checkin.edit', ['id' => $id])->withErrors($validator)->withInput();
}
$ejaculation->fill([
'ejaculated_date' => Carbon::createFromFormat('Y/m/d H:i', $inputs['date'] . ' ' . $inputs['time']),
'note' => $inputs['note'] ?? '',
'link' => $inputs['link'] ?? '',
'is_private' => $request->has('is_private') ?? false,
'is_too_sensitive' => $request->has('is_too_sensitive') ?? false
])->save();
DB::transaction(function () use ($ejaculation, $request, $inputs) {
$ejaculation->fill([
'ejaculated_date' => Carbon::createFromFormat('Y/m/d H:i', $inputs['date'] . ' ' . $inputs['time']),
'note' => $inputs['note'] ?? '',
'link' => $inputs['link'] ?? '',
'is_private' => $request->has('is_private') ?? false,
'is_too_sensitive' => $request->has('is_too_sensitive') ?? false,
'discard_elapsed_time' => $request->has('discard_elapsed_time') ?? false,
])->save();
$tagIds = [];
if (!empty($inputs['tags'])) {
$tags = explode(' ', $inputs['tags']);
foreach ($tags as $tag) {
if ($tag === '') {
continue;
$tagIds = [];
if (!empty($inputs['tags'])) {
$tags = explode(' ', $inputs['tags']);
foreach ($tags as $tag) {
if ($tag === '') {
continue;
}
$tag = Tag::firstOrCreate(['name' => $tag]);
$tagIds[] = $tag->id;
}
$tag = Tag::firstOrCreate(['name' => $tag]);
$tagIds[] = $tag->id;
}
}
$ejaculation->tags()->sync($tagIds);
$ejaculation->tags()->sync($tagIds);
});
if (!empty($ejaculation->link)) {
event(new LinkDiscovered($ejaculation->link));
@ -179,9 +225,17 @@ class EjaculationController extends Controller
$this->authorize('edit', $ejaculation);
$user = User::findOrFail($ejaculation->user_id);
$ejaculation->tags()->detach();
$ejaculation->delete();
DB::transaction(function () use ($ejaculation) {
$ejaculation->tags()->detach();
$ejaculation->delete();
});
return redirect()->route('user.profile', ['name' => $user->name])->with('status', '削除しました。');
}
public function tools()
{
return view('ejaculation.tools');
}
}

View File

@ -66,11 +66,13 @@ SQL
->where('users.is_protected', false)
->where('ejaculations.is_private', false)
->where('ejaculations.link', '<>', '')
->where('ejaculations.ejaculated_date', '<=', Carbon::now())
->orderBy('ejaculations.ejaculated_date', 'desc')
->select('ejaculations.*')
->with('user', 'tags')
->withLikes()
->take(10)
->visibleToTimeline()
->take(21)
->get();
return view('home')->with(compact('informations', 'categories', 'globalEjaculationCounts', 'publicLinkedEjaculations'));

View File

@ -4,20 +4,30 @@ namespace App\Http\Controllers;
use App\Ejaculation;
use App\Tag;
use App\Utilities\Formatter;
use Illuminate\Http\Request;
use Illuminate\Support\Facades\Auth;
class SearchController extends Controller
{
/** @var Formatter */
private $formatter;
public function __construct(Formatter $formatter)
{
$this->formatter = $formatter;
}
public function index(Request $request)
{
$inputs = $request->validate([
'q' => 'required'
]);
$q = $this->normalizeQuery($inputs['q']);
$results = Ejaculation::query()
->whereHas('tags', function ($query) use ($inputs) {
$query->where('name', 'like', "%{$inputs['q']}%");
->whereHas('tags', function ($query) use ($q) {
$query->where('normalized_name', 'like', "%{$q}%");
})
->whereHas('user', function ($query) {
$query->where('is_protected', false);
@ -41,11 +51,17 @@ class SearchController extends Controller
'q' => 'required'
]);
$q = $this->normalizeQuery($inputs['q']);
$results = Tag::query()
->where('name', 'like', "%{$inputs['q']}%")
->where('normalized_name', 'like', "%{$q}%")
->paginate(50)
->appends($inputs);
return view('search.relatedTag')->with(compact('inputs', 'results'));
}
private function normalizeQuery(string $query): string
{
return $this->formatter->normalizeTagName($query);
}
}

View File

@ -2,7 +2,12 @@
namespace App\Http\Controllers;
use App\CheckinWebhook;
use App\DeactivatedUser;
use App\Ejaculation;
use App\Exceptions\CsvImportException;
use App\Services\CheckinCsvExporter;
use App\Services\CheckinCsvImporter;
use Illuminate\Http\Request;
use Illuminate\Support\Facades\Auth;
use Illuminate\Support\Facades\DB;
@ -71,6 +76,119 @@ class SettingController extends Controller
return redirect()->route('setting.privacy')->with('status', 'プライバシー設定を更新しました。');
}
public function webhooks()
{
$webhooks = Auth::user()->checkinWebhooks;
$webhooksLimit = CheckinWebhook::PER_USER_LIMIT;
return view('setting.webhooks')->with(compact('webhooks', 'webhooksLimit'));
}
public function storeWebhooks(Request $request)
{
$validated = $request->validate([
'name' => [
'required',
'string',
'max:255',
Rule::unique('checkin_webhooks', 'name')->where(function ($query) {
return $query->where('user_id', Auth::id());
})
]
], [], [
'name' => '名前'
]);
if (Auth::user()->checkinWebhooks()->count() >= CheckinWebhook::PER_USER_LIMIT) {
return redirect()->route('setting.webhooks')
->with('status', CheckinWebhook::PER_USER_LIMIT . '件以上のWebhookを作成することはできません。');
}
Auth::user()->checkinWebhooks()->create($validated);
return redirect()->route('setting.webhooks')->with('status', '作成しました。');
}
public function destroyWebhooks(CheckinWebhook $webhook)
{
$webhook->delete();
return redirect()->route('setting.webhooks')->with('status', '削除しました。');
}
public function import()
{
return view('setting.import');
}
public function storeImport(Request $request)
{
$validated = $request->validate([
'file' => 'required|file'
], [], [
'file' => 'ファイル'
]);
$file = $request->file('file');
if (!$file->isValid()) {
return redirect()->route('setting.import')->withErrors(['file' => 'ファイルのアップロードに失敗しました。']);
}
try {
set_time_limit(0);
$importer = new CheckinCsvImporter(Auth::user(), $file->path());
$imported = $importer->execute();
return redirect()->route('setting.import')->with('status', "{$imported}件のインポートに性交しました。");
} catch (CsvImportException $e) {
return redirect()->route('setting.import')->with('import_errors', $e->getErrors());
}
}
public function destroyImport()
{
Auth::user()
->ejaculations()
->where('ejaculations.source', Ejaculation::SOURCE_CSV)
->delete();
return redirect()->route('setting.import')->with('status', '削除が完了しました。');
}
public function export()
{
return view('setting.export');
}
public function exportToCsv(Request $request)
{
$validated = $request->validate([
'charset' => ['required', Rule::in(['utf8', 'sjis'])]
]);
$charsets = [
'utf8' => 'UTF-8',
'sjis' => 'SJIS-win'
];
$filename = tempnam(sys_get_temp_dir(), 'tissue_export_tmp_');
try {
// 気休め
set_time_limit(0);
$exporter = new CheckinCsvExporter(Auth::user(), $filename, $charsets[$validated['charset']]);
$exporter->execute();
} catch (\Throwable $e) {
unlink($filename);
throw $e;
}
return response()
->download($filename, 'TissueCheckin_' . date('Y-m-d_H-i-s') . '.csv')
->deleteFileAfterSend(true);
}
public function deactivate()
{
return view('setting.deactivate');

View File

@ -4,6 +4,7 @@ namespace App\Http\Controllers;
use App\Ejaculation;
use Illuminate\Http\Request;
use Illuminate\Support\Carbon;
class TimelineController extends Controller
{
@ -13,10 +14,12 @@ class TimelineController extends Controller
->where('users.is_protected', false)
->where('ejaculations.is_private', false)
->where('ejaculations.link', '<>', '')
->where('ejaculations.ejaculated_date', '<=', Carbon::now())
->orderBy('ejaculations.ejaculated_date', 'desc')
->select('ejaculations.*')
->with('user', 'tags')
->withLikes()
->visibleToTimeline()
->paginate(21);
return view('timeline.public')->with(compact('ejaculations'));

View File

@ -5,9 +5,11 @@ namespace App\Http\Controllers;
use App\Ejaculation;
use App\User;
use Carbon\Carbon;
use Carbon\CarbonInterface;
use Illuminate\Http\Request;
use Illuminate\Support\Facades\Auth;
use Illuminate\Support\Facades\DB;
use Validator;
class UserController extends Controller
{
@ -26,16 +28,19 @@ class UserController extends Controller
// チェックインの取得
$query = Ejaculation::select(DB::raw(
<<<'SQL'
id,
ejaculations.id,
ejaculated_date,
note,
is_private,
is_too_sensitive,
link,
to_char(lead(ejaculated_date, 1, NULL) OVER (ORDER BY ejaculated_date DESC), 'YYYY/MM/DD HH24:MI') AS before_date,
to_char(ejaculated_date - (lead(ejaculated_date, 1, NULL) OVER (ORDER BY ejaculated_date DESC)), 'FMDDD日 FMHH24時間 FMMI分') AS ejaculated_span
source,
discard_elapsed_time,
to_char(before_dates.before_date, 'YYYY/MM/DD HH24:MI') AS before_date,
to_char(ejaculated_date - before_dates.before_date, 'FMDDD日 FMHH24時間 FMMI分') AS ejaculated_span
SQL
))
->joinSub($this->queryBeforeEjaculatedDates(), 'before_dates', 'before_dates.id', '=', 'ejaculations.id')
->where('user_id', $user->id);
if (!Auth::check() || $user->id !== Auth::id()) {
$query = $query->where('is_private', false);
@ -59,7 +64,18 @@ SQL
->limit(10)
->get();
return view('user.profile')->with(compact('user', 'ejaculations', 'tags'));
// シコ草
$countByDayQuery = $this->countEjaculationByDay($user)
->where('ejaculated_date', '>=', now()->startOfMonth()->subMonths(9))
->where('ejaculated_date', '<', now()->addMonth()->startOfMonth())
->get();
$countByDay = [];
foreach ($countByDayQuery as $data) {
$date = Carbon::createFromFormat('Y/m/d', $data->date);
$countByDay[$date->timestamp] = $data->count;
}
return view('user.profile')->with(compact('user', 'ejaculations', 'tags', 'countByDay'));
}
public function stats($name)
@ -69,18 +85,161 @@ SQL
abort(404);
}
$dateUntil = now()->addMonth()->startOfMonth();
$availableMonths = $this->makeStatsAvailableMonths($user);
$graphData = $this->makeGraphData($user);
$groupByDay = Ejaculation::select(DB::raw(
return view('user.stats.index')->with(compact('user', 'graphData', 'availableMonths'));
}
public function statsYearly($name, $year)
{
$user = User::where('name', $name)->first();
if (empty($user)) {
abort(404);
}
$validator = Validator::make(compact('year'), [
'year' => 'required|date_format:Y'
]);
if ($validator->fails()) {
return redirect()->route('user.stats', compact('name'));
}
$availableMonths = $this->makeStatsAvailableMonths($user);
if (!isset($availableMonths[$year])) {
return redirect()->route('user.stats', compact('name'));
}
$graphData = $this->makeGraphData(
$user,
Carbon::createFromDate($year, 1, 1, config('app.timezone'))->startOfDay(),
Carbon::createFromDate($year, 1, 1, config('app.timezone'))->addYear()->startOfDay()
);
return view('user.stats.yearly')
->with(compact('user', 'graphData', 'availableMonths'))
->with('currentYear', (int) $year);
}
public function statsMonthly($name, $year, $month)
{
$user = User::where('name', $name)->first();
if (empty($user)) {
abort(404);
}
$validator = Validator::make(compact('year', 'month'), [
'year' => 'required|date_format:Y',
'month' => 'required|date_format:m'
]);
if ($validator->fails()) {
return redirect()->route('user.stats.yearly', compact('name', 'year'));
}
$availableMonths = $this->makeStatsAvailableMonths($user);
if (!isset($availableMonths[$year]) || !in_array($month, $availableMonths[$year], false)) {
return redirect()->route('user.stats.yearly', compact('name', 'year'));
}
$graphData = $this->makeGraphData(
$user,
Carbon::createFromDate($year, $month, 1, config('app.timezone'))->startOfDay(),
Carbon::createFromDate($year, $month, 1, config('app.timezone'))->addMonth()->startOfDay()
);
return view('user.stats.monthly')
->with(compact('user', 'graphData', 'availableMonths'))
->with('currentYear', (int) $year)
->with('currentMonth', (int) $month);
}
public function okazu($name)
{
$user = User::where('name', $name)->first();
if (empty($user)) {
abort(404);
}
// チェックインの取得
$query = Ejaculation::select(DB::raw(
<<<'SQL'
to_char(ejaculated_date, 'YYYY/MM/DD') AS "date",
count(*) AS "count"
ejaculations.id,
ejaculated_date,
note,
is_private,
is_too_sensitive,
link,
source,
discard_elapsed_time,
to_char(before_dates.before_date, 'YYYY/MM/DD HH24:MI') AS before_date,
to_char(ejaculated_date - before_dates.before_date, 'FMDDD日 FMHH24時間 FMMI分') AS ejaculated_span
SQL
))
->joinSub($this->queryBeforeEjaculatedDates(), 'before_dates', 'before_dates.id', '=', 'ejaculations.id')
->where('user_id', $user->id)
->where('ejaculated_date', '<', $dateUntil)
->groupBy(DB::raw("to_char(ejaculated_date, 'YYYY/MM/DD')"))
->orderBy(DB::raw("to_char(ejaculated_date, 'YYYY/MM/DD')"))
->where('link', '<>', '');
if (!Auth::check() || $user->id !== Auth::id()) {
$query = $query->where('is_private', false);
}
$ejaculations = $query->orderBy('ejaculated_date', 'desc')
->with('tags')
->withLikes()
->paginate(20);
return view('user.profile')->with(compact('user', 'ejaculations'));
}
public function likes($name)
{
$user = User::where('name', $name)->first();
if (empty($user)) {
abort(404);
}
$likes = $user->likes()
->orderBy('created_at', 'desc')
->with('ejaculation.user', 'ejaculation.tags')
->whereHas('ejaculation', function ($query) {
$query->where('user_id', Auth::id())
->orWhere('is_private', false);
})
->paginate(20);
return view('user.likes')->with(compact('user', 'likes'));
}
private function makeStatsAvailableMonths(User $user): array
{
$availableMonths = [];
$oldest = $user->ejaculations()->orderBy('ejaculated_date')->first();
if (isset($oldest)) {
$oldestMonth = $oldest->ejaculated_date->startOfMonth();
$currentMonth = now()->startOfMonth();
for ($month = $currentMonth; $oldestMonth <= $currentMonth; $month = $month->subMonth()) {
if (!isset($availableMonths[$month->year])) {
$availableMonths[$month->year] = [];
}
$availableMonths[$month->year][] = $month->month;
}
}
return $availableMonths;
}
private function makeGraphData(User $user, CarbonInterface $dateSince = null, CarbonInterface $dateUntil = null): array
{
if ($dateUntil === null) {
$dateUntil = now()->addMonth()->startOfMonth();
}
$dateCondition = [
['ejaculated_date', '<', $dateUntil],
];
if ($dateSince !== null) {
$dateCondition[] = ['ejaculated_date', '>=', $dateSince];
}
$groupByDay = $this->countEjaculationByDay($user)
->where($dateCondition)
->get();
$groupByHour = Ejaculation::select(DB::raw(
@ -90,7 +249,7 @@ count(*) AS "count"
SQL
))
->where('user_id', $user->id)
->where('ejaculated_date', '<', $dateUntil)
->where($dateCondition)
->groupBy(DB::raw("to_char(ejaculated_date, 'HH24')"))
->orderBy(DB::raw('1'))
->get();
@ -125,7 +284,7 @@ SQL
$hourlySum[$hour] += $data->count;
}
$graphData = [
return [
'dailySum' => $dailySum,
'dowSum' => $dowSum,
'monthlySum' => $monthlySum,
@ -134,58 +293,28 @@ SQL
'hourlyKey' => array_keys($hourlySum),
'hourlySum' => array_values($hourlySum),
];
return view('user.stats')->with(compact('user', 'graphData'));
}
public function okazu($name)
private function countEjaculationByDay(User $user)
{
$user = User::where('name', $name)->first();
if (empty($user)) {
abort(404);
}
// チェックインの取得
$query = Ejaculation::select(DB::raw(
return Ejaculation::select(DB::raw(
<<<'SQL'
id,
ejaculated_date,
note,
is_private,
is_too_sensitive,
link,
to_char(lead(ejaculated_date, 1, NULL) OVER (ORDER BY ejaculated_date DESC), 'YYYY/MM/DD HH24:MI') AS before_date,
to_char(ejaculated_date - (lead(ejaculated_date, 1, NULL) OVER (ORDER BY ejaculated_date DESC)), 'FMDDD日 FMHH24時間 FMMI分') AS ejaculated_span
to_char(ejaculated_date, 'YYYY/MM/DD') AS "date",
count(*) AS "count"
SQL
))
->where('user_id', $user->id)
->where('link', '<>', '');
if (!Auth::check() || $user->id !== Auth::id()) {
$query = $query->where('is_private', false);
}
$ejaculations = $query->orderBy('ejaculated_date', 'desc')
->with('tags')
->paginate(20);
return view('user.profile')->with(compact('user', 'ejaculations'));
->groupBy(DB::raw("to_char(ejaculated_date, 'YYYY/MM/DD')"))
->orderBy(DB::raw("to_char(ejaculated_date, 'YYYY/MM/DD')"));
}
public function likes($name)
private function queryBeforeEjaculatedDates()
{
$user = User::where('name', $name)->first();
if (empty($user)) {
abort(404);
}
$likes = $user->likes()
->orderBy('created_at', 'desc')
->with('ejaculation.user', 'ejaculation.tags')
->whereHas('ejaculation', function ($query) {
$query->where('user_id', Auth::id())
->orWhere('is_private', false);
})
->paginate(20);
return view('user.likes')->with(compact('user', 'likes'));
return DB::table('ejaculations')->selectRaw(
<<<'SQL'
id,
(select ejaculated_date from ejaculations e2 where e2.ejaculated_date < ejaculations.ejaculated_date and e2.user_id = ejaculations.user_id order by e2.ejaculated_date desc limit 1) AS before_date
SQL
);
}
}

View File

@ -14,11 +14,11 @@ class Kernel extends HttpKernel
* @var array
*/
protected $middleware = [
\Illuminate\Foundation\Http\Middleware\CheckForMaintenanceMode::class,
\App\Http\Middleware\TrustProxies::class,
\App\Http\Middleware\CheckForMaintenanceMode::class,
\Illuminate\Foundation\Http\Middleware\ValidatePostSize::class,
\App\Http\Middleware\TrimStrings::class,
\Illuminate\Foundation\Http\Middleware\ConvertEmptyStringsToNull::class,
\App\Http\Middleware\TrustProxies::class,
];
/**
@ -38,15 +38,17 @@ class Kernel extends HttpKernel
\App\Http\Middleware\NormalizeLineEnding::class,
],
// 現時点では内部APIしかないので、認証の手間を省くためにステートフルにしている。
'api' => [
\App\Http\Middleware\EnforceJson::class,
\Illuminate\Routing\Middleware\SubstituteBindings::class,
],
'stateful' => [
\App\Http\Middleware\EncryptCookies::class,
\Illuminate\Cookie\Middleware\AddQueuedCookiesToResponse::class,
\Illuminate\Session\Middleware\StartSession::class,
\App\Http\Middleware\VerifyCsrfToken::class,
'throttle:60,1',
'bindings',
],
]
];
/**
@ -57,11 +59,33 @@ class Kernel extends HttpKernel
* @var array
*/
protected $routeMiddleware = [
'auth' => \Illuminate\Auth\Middleware\Authenticate::class,
'auth' => \App\Http\Middleware\Authenticate::class,
'auth.basic' => \Illuminate\Auth\Middleware\AuthenticateWithBasicAuth::class,
'bindings' => \Illuminate\Routing\Middleware\SubstituteBindings::class,
'cache.headers' => \Illuminate\Http\Middleware\SetCacheHeaders::class,
'can' => \Illuminate\Auth\Middleware\Authorize::class,
'guest' => \App\Http\Middleware\RedirectIfAuthenticated::class,
'password.confirm' => \Illuminate\Auth\Middleware\RequirePassword::class,
'signed' => \Illuminate\Routing\Middleware\ValidateSignature::class,
'throttle' => \Illuminate\Routing\Middleware\ThrottleRequests::class,
'verified' => \Illuminate\Auth\Middleware\EnsureEmailIsVerified::class,
];
/**
* The priority-sorted list of middleware.
*
* This forces non-global middleware to always be in the given order.
*
* @var array
*/
protected $middlewarePriority = [
\App\Http\Middleware\EncryptCookies::class,
\Illuminate\Session\Middleware\StartSession::class,
\Illuminate\View\Middleware\ShareErrorsFromSession::class,
\App\Http\Middleware\Authenticate::class,
\Illuminate\Routing\Middleware\ThrottleRequests::class,
\Illuminate\Session\Middleware\AuthenticateSession::class,
\Illuminate\Routing\Middleware\SubstituteBindings::class,
\Illuminate\Auth\Middleware\Authorize::class,
];
}

View File

@ -0,0 +1,21 @@
<?php
namespace App\Http\Middleware;
use Illuminate\Auth\Middleware\Authenticate as Middleware;
class Authenticate extends Middleware
{
/**
* Get the path the user should be redirected to when they are not authenticated.
*
* @param \Illuminate\Http\Request $request
* @return string|null
*/
protected function redirectTo($request)
{
if (! $request->expectsJson()) {
return route('login');
}
}
}

View File

@ -0,0 +1,17 @@
<?php
namespace App\Http\Middleware;
use Illuminate\Foundation\Http\Middleware\CheckForMaintenanceMode as Middleware;
class CheckForMaintenanceMode extends Middleware
{
/**
* The URIs that should be reachable while maintenance mode is enabled.
*
* @var array
*/
protected $except = [
//
];
}

View File

@ -0,0 +1,25 @@
<?php
namespace App\Http\Middleware;
use Closure;
/**
* Request headerに Accept: application/json を上書きする。APIエンドポイント用。
*/
class EnforceJson
{
/**
* Handle an incoming request.
*
* @param \Illuminate\Http\Request $request
* @param \Closure $next
* @return mixed
*/
public function handle($request, Closure $next)
{
$request->headers->set('Accept', 'application/json');
return $next($request);
}
}

View File

@ -10,20 +10,14 @@ class TrustProxies extends Middleware
/**
* The trusted proxies for this application.
*
* @var array
* @var array|string
*/
protected $proxies = '**';
/**
* The current proxy header mappings.
* The headers that should be used to detect proxies.
*
* @var array
* @var int
*/
protected $headers = [
Request::HEADER_FORWARDED => 'FORWARDED',
Request::HEADER_X_FORWARDED_FOR => 'X_FORWARDED_FOR',
Request::HEADER_X_FORWARDED_HOST => 'X_FORWARDED_HOST',
Request::HEADER_X_FORWARDED_PORT => 'X_FORWARDED_PORT',
Request::HEADER_X_FORWARDED_PROTO => 'X_FORWARDED_PROTO',
];
protected $headers = Request::HEADER_X_FORWARDED_ALL;
}

View File

@ -6,6 +6,13 @@ use Illuminate\Foundation\Http\Middleware\VerifyCsrfToken as BaseVerifier;
class VerifyCsrfToken extends BaseVerifier
{
/**
* Indicates whether the XSRF-TOKEN cookie should be set on the response.
*
* @var bool
*/
protected $addHttpCookie = true;
/**
* The URIs that should be excluded from CSRF verification.
*

View File

@ -0,0 +1,28 @@
<?php
namespace App\Http\Resources;
use Illuminate\Http\Resources\Json\JsonResource;
class EjaculationResource extends JsonResource
{
/**
* Transform the resource into an array.
*
* @param \Illuminate\Http\Request $request
* @return array
*/
public function toArray($request)
{
return [
'id' => $this->id,
'checked_in_at' => $this->ejaculated_date->format(\DateTime::ATOM),
'note' => $this->note,
'link' => $this->link,
'tags' => $this->tags->pluck('name'),
'source' => $this->source,
'is_private' => $this->is_private,
'is_too_sensitive' => $this->is_too_sensitive,
];
}
}

View File

@ -19,6 +19,7 @@ class ProfileStatsComposer
if (!$view->offsetExists('user')) {
throw new \LogicException('View data "user" was not exist.');
}
/** @var \App\User $user */
$user = $view->offsetGet('user');
// 現在のオナ禁セッションの経過時間
@ -35,35 +36,44 @@ class ProfileStatsComposer
}
// 概況欄のデータ取得
$average = DB::select(<<<'SQL'
$average = 0;
$divisor = 0;
$averageSources = DB::select(<<<'SQL'
SELECT
avg(span) AS average
extract(epoch from ejaculated_date - lead(ejaculated_date, 1, NULL) OVER (ORDER BY ejaculated_date DESC)) AS span,
discard_elapsed_time
FROM
(
SELECT
extract(epoch from ejaculated_date - lead(ejaculated_date, 1, NULL) OVER (ORDER BY ejaculated_date DESC)) AS span
FROM
ejaculations
WHERE
user_id = :user_id
ORDER BY
ejaculated_date DESC
LIMIT
30
) AS temp
ejaculations
WHERE
user_id = :user_id
ORDER BY
ejaculated_date DESC
LIMIT
30
SQL
, ['user_id' => $user->id]);
foreach ($averageSources as $item) {
// 経過時間記録対象外のレコードがあったら、それより古いデータは平均の計算に加えない
if ($item->discard_elapsed_time) {
break;
}
$average += $item->span;
$divisor++;
}
if ($divisor > 0) {
$average /= $divisor;
}
$summary = DB::select(<<<'SQL'
SELECT
max(span) AS longest,
min(span) AS shortest,
sum(span) AS total_times,
count(*) AS total_checkins
sum(span) AS total_times
FROM
(
SELECT
extract(epoch from ejaculated_date - lead(ejaculated_date, 1, NULL) OVER (ORDER BY ejaculated_date DESC)) AS span
extract(epoch from ejaculated_date - lead(ejaculated_date, 1, NULL) OVER (ORDER BY ejaculated_date DESC)) AS span,
discard_elapsed_time
FROM
ejaculations
WHERE
@ -71,9 +81,13 @@ FROM
ORDER BY
ejaculated_date DESC
) AS temp
WHERE
discard_elapsed_time = FALSE
SQL
, ['user_id' => $user->id]);
$view->with(compact('latestEjaculation', 'currentSession', 'average', 'summary'));
$total = $user->ejaculations()->count();
$view->with(compact('latestEjaculation', 'currentSession', 'average', 'summary', 'total'));
}
}

View File

@ -3,32 +3,24 @@
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\MetadataResolver\DeniedHostException;
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 +31,13 @@ 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->tags 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 (DeniedHostException $e) {
// ignored
} catch (\Exception $e) {
// 今のところこのイベントは同期実行されるので、上流をクラッシュさせないために雑catchする
report($e);
}
}
}

View File

@ -2,6 +2,8 @@
namespace App;
use Carbon\CarbonInterface;
use GuzzleHttp\Exception\RequestException;
use Illuminate\Database\Eloquent\Model;
class Metadata extends Model
@ -13,10 +15,66 @@ class Metadata extends Model
protected $fillable = ['url', 'title', 'description', 'image', 'expires_at'];
protected $visible = ['url', 'title', 'description', 'image', 'expires_at', 'tags'];
protected $dates = ['created_at', 'updated_at', 'expires_at'];
protected $dates = ['created_at', 'updated_at', 'expires_at', 'error_at'];
public function tags()
{
return $this->belongsToMany(Tag::class)->withTimestamps();
}
public function needRefresh(): bool
{
return $this->isExpired() || $this->error_at !== null;
}
public function isExpired(): bool
{
return $this->expires_at !== null && $this->expires_at < now();
}
public function storeException(CarbonInterface $error_at, \Exception $exception): self
{
$this->prepareFieldsOnError();
$this->error_at = $error_at;
$this->error_exception_class = get_class($exception);
$this->error_body = $exception->getMessage();
if ($exception instanceof RequestException) {
$this->error_http_code = $exception->getCode();
} else {
$this->error_http_code = null;
}
$this->error_count++;
return $this;
}
public function storeError(CarbonInterface $error_at, string $body, ?int $httpCode = null): self
{
$this->prepareFieldsOnError();
$this->error_at = $error_at;
$this->error_exception_class = null;
$this->error_body = $body;
$this->error_http_code = $httpCode;
$this->error_count++;
return $this;
}
public function clearError(): self
{
$this->error_at = null;
$this->error_exception_class = null;
$this->error_body = null;
$this->error_http_code = null;
$this->error_count = 0;
return $this;
}
private function prepareFieldsOnError()
{
$this->title = $this->title ?? '';
$this->description = $this->description ?? '';
$this->image = $this->image ?? '';
}
}

View File

@ -4,6 +4,7 @@ namespace App\MetadataResolver;
use Carbon\Carbon;
use GuzzleHttp\Client;
use Symfony\Component\DomCrawler\Crawler;
class CienResolver extends MetadataResolver
{
@ -25,14 +26,27 @@ class CienResolver extends MetadataResolver
public function resolve(string $url): Metadata
{
$res = $this->client->get($url);
$metadata = $this->ogpResolver->parse((string) $res->getBody());
$html = (string) $res->getBody();
$metadata = $this->ogpResolver->parse($html);
$crawler = new Crawler($html);
// 画像URLから有効期限の起点を拾う
parse_str(parse_url($metadata->image, PHP_URL_QUERY), $params);
if (empty($params['px-time'])) {
throw new \RuntimeException('Parameter "px-time" not found. Image=' . $metadata->image . ' Source=' . $url);
// OGPのデフォルトはバナーなので、投稿に使える画像があればそれを使う
$selector = 'img[data-actual*="image-web"]';
if ($crawler->filter($selector)->count() !== 0) {
$metadata->image = $crawler->filter($selector)->attr('data-actual');
}
// JWTがついていれば画像URLのJWTから有効期限を拾う
parse_str(parse_url($metadata->image, PHP_URL_QUERY), $params);
if (isset($params['jwt'])) {
$parts = explode('.', $params['jwt']);
if (count($parts) !== 3) {
throw new \RuntimeException('Invalid jwt. Image=' . $metadata->image . ' Source=' . $url);
}
$payload = json_decode(base64_decode(str_replace(['-', '_'], ['+', '/'], $parts[1])), true);
$metadata->expires_at = Carbon::createFromTimestamp($payload['exp']);
}
$metadata->expires_at = Carbon::createFromTimestamp($params['px-time'])->addHour(1);
return $metadata;
}

View File

@ -46,6 +46,7 @@ class DLsiteResolver implements Resolver
// 重複削除
$tags = array_values(array_unique($tags));
sort($tags);
return $tags;
}

View File

@ -0,0 +1,30 @@
<?php
namespace App\MetadataResolver;
use Exception;
use Throwable;
/**
* メタデータの解決を禁止しているホストに対して取得を試み、ブロックされたことを表します。
*/
class DeniedHostException extends Exception
{
private $url;
public function __construct(string $url, Throwable $previous = null)
{
parent::__construct("Access denied by system policy: $url", 0, $previous);
$this->url = $url;
}
public function getUrl(): string
{
return $this->url;
}
public function getHost(): string
{
return parse_url($this->url, PHP_URL_HOST);
}
}

View File

@ -0,0 +1,30 @@
<?php
namespace App\MetadataResolver;
use RuntimeException;
use Throwable;
/**
* ContentProviderの提供するrobots.txtによってクロールが拒否された場合にスローされます。
*/
class DisallowedByProviderException extends RuntimeException
{
private $url;
public function __construct(string $url, Throwable $previous = null)
{
parent::__construct("Access denied by robots.txt: $url", 0, $previous);
$this->url = $url;
}
public function getUrl(): string
{
return $this->url;
}
public function getHost(): string
{
return parse_url($this->url, PHP_URL_HOST);
}
}

View File

@ -1,40 +0,0 @@
<?php
namespace App\MetadataResolver;
use GuzzleHttp\Client;
class FC2ContentsResolver implements Resolver
{
/**
* @var Client
*/
private $client;
/**
* @var OGPResolver
*/
private $ogpResolver;
public function __construct(Client $client, OGPResolver $ogpResolver)
{
$this->client = $client;
$this->ogpResolver = $ogpResolver;
}
public function resolve(string $url): Metadata
{
$res = $this->client->get($url);
$metadata = $this->ogpResolver->parse($res->getBody());
$dom = new \DOMDocument();
@$dom->loadHTML(mb_convert_encoding($res->getBody(), 'HTML-ENTITIES', 'UTF-8'));
$xpath = new \DOMXPath($dom);
$thumbnailNode = $xpath->query('//*[@class="main_thum_img"]/a')->item(0);
if ($thumbnailNode) {
$metadata->image = preg_replace('~^http:~', 'https:', $thumbnailNode->getAttribute('href'));
}
return $metadata;
}
}

View File

@ -3,6 +3,7 @@
namespace App\MetadataResolver;
use GuzzleHttp\Client;
use GuzzleHttp\Cookie\CookieJar;
use Symfony\Component\DomCrawler\Crawler;
class FanzaResolver implements Resolver
@ -43,7 +44,9 @@ class FanzaResolver implements Resolver
public function resolve(string $url): Metadata
{
$res = $this->client->get($url);
$cookieJar = CookieJar::fromArray(['age_check_done' => '1'], 'dmm.co.jp');
$res = $this->client->get($url, ['cookies' => $cookieJar]);
$html = (string) $res->getBody();
$crawler = new Crawler($html);
@ -51,9 +54,9 @@ class FanzaResolver implements Resolver
if (preg_match('~www\.dmm\.co\.jp/digital/(videoa|videoc|anime)/-/detail~', $url)) {
$metadata = new Metadata();
$metadata->title = trim($crawler->filter('#title')->text(''));
$metadata->description = trim($crawler->filter('.box-rank+table+div+div')->text(''));
$metadata->description = trim(strip_tags(str_replace('【FANZA(ファンザ)】', '', $crawler->filter('meta[name="description"]')->attr('content'))));
$metadata->image = preg_replace("~(pr|ps)\.jpg$~", 'pl.jpg', $crawler->filter('meta[property="og:image"]')->attr('content'));
$metadata->tags = $this->array_finish($crawler->filter('.box-rank+table a:not([href="#review"])')->extract(['_text']));
$metadata->tags = $this->array_finish($crawler->filter('.box-rank+table a[href*="list/=/article="]')->extract(['_text']));
return $metadata;
}
@ -91,7 +94,7 @@ class FanzaResolver implements Resolver
$metadata->title = trim($crawler->filter('#title')->text(''));
$metadata->description = trim($crawler->filter('.area-detail-read .text-overflow')->text(''));
$metadata->image = preg_replace("~(pr|ps)\.jpg$~", 'pl.jpg', $crawler->filter('meta[property="og:image"]')->attr('content'));
$metadata->tags = $this->array_finish($crawler->filter('.area-bskt table a:not([href="#review"])')->extract(['_text']));
$metadata->tags = $this->array_finish($crawler->filter('.container02 table a[href*="list/article="]')->extract(['_text']));
return $metadata;
}

View File

@ -34,7 +34,7 @@ class IwaraResolver implements Resolver
array_push($tags, $author);
$metadata->title = $title;
$metadata->description = '投稿者: ' . $author . PHP_EOL . $description;
$metadata->description = '投稿者: ' . $author . PHP_EOL . trim($description);
$metadata->tags = $tags;
// iwara video

View File

@ -41,9 +41,12 @@ class KomifloResolver implements Resolver
// タグ
if (!empty($json['content']['attributes']['tags']['children'])) {
$tags = [];
foreach ($json['content']['attributes']['tags']['children'] as $tag) {
$metadata->tags[] = preg_replace('/\s/', '_', $tag['data']['name']);
$tags[] = preg_replace('/\s/', '_', $tag['data']['name']);
}
sort($tags);
$metadata->tags = array_merge($metadata->tags, $tags);
}
return $metadata;

View File

@ -23,4 +23,30 @@ class Metadata
* チェックインタグと同様に保存されるため、スペースや改行文字を含めてはいけません。
*/
public $tags = [];
/**
* 重複を排除し、正規化を行ったタグの集合を返します。
* @return string[]
*/
public function normalizedTags(): array
{
$tags = [];
foreach ($this->tags as $tag) {
$tag = $this->sanitize($tag);
$tag = $this->trim($tag);
$tags[$tag] = true;
}
return array_keys($tags);
}
private function sanitize(string $value): string
{
return preg_replace('/\r?\n/u', ' ', $value);
}
private function trim(string $value): string
{
return trim($value);
}
}

View File

@ -26,14 +26,14 @@ class MetadataResolver implements Resolver
'~dmm\.co\.jp/~' => FanzaResolver::class,
'~www\.patreon\.com/~' => PatreonResolver::class,
'~www\.deviantart\.com/.*/art/.*~' => DeviantArtResolver::class,
'~\.syosetu\.com/n\d+[a-z]{2,}~' => NarouResolver::class,
'~ci-en\.jp/creator/\d+/article/\d+~' => CienResolver::class,
'~\.syosetu\.com/n\d+[a-z]+~' => NarouResolver::class,
'~ci-en\.(jp|net|dlsite\.com)/creator/\d+/article/\d+~' => CienResolver::class,
'~www\.plurk\.com\/p\/.*~' => PlurkResolver::class,
'~(adult\.)?contents\.fc2\.com\/article_search\.php\?id=\d+~' => FC2ContentsResolver::class,
'~store\.steampowered\.com/app/\d+~' => SteamResolver::class,
'~www\.xtube\.com/video-watch/.*-\d+$~'=> XtubeResolver::class,
'~ss\.kb10uy\.org/posts/\d+$~' => Kb10uyShortStoryServerResolver::class,
'~www\.hentai-foundry\.com/pictures/user/.+/\d+/.+~'=> HentaiFoundryResolver::class,
'~(www\.)?((mobile|m)\.)?twitter\.com/(#!/)?[0-9a-zA-Z_]{1,15}/status(es)?/([0-9]+)/?(\\?.+)?$~' => TwitterResolver::class,
];
public $mimeTypes = [

View File

@ -3,6 +3,7 @@
namespace App\MetadataResolver;
use GuzzleHttp\Client;
use Illuminate\Support\Str;
use Symfony\Component\DomCrawler\Crawler;
class NijieResolver implements Resolver
@ -50,8 +51,8 @@ class NijieResolver implements Resolver
$metadata->description = '投稿者: ' . $data['author']['name'] . PHP_EOL . $data['description'];
if (
isset($data['thumbnailUrl']) &&
!ends_with($data['thumbnailUrl'], '.gif') &&
!ends_with($data['thumbnailUrl'], '.mp4')
!Str::endsWith($data['thumbnailUrl'], '.gif') &&
!Str::endsWith($data['thumbnailUrl'], '.mp4')
) {
// サムネイルからメイン画像に
$metadata->image = str_replace('__rs_l160x160/', '', $data['thumbnailUrl']);

View File

@ -3,6 +3,8 @@
namespace App\MetadataResolver;
use GuzzleHttp\Client;
use GuzzleHttp\Cookie\CookieJar;
use GuzzleHttp\RequestOptions;
class OGPResolver implements Resolver, Parser
{
@ -18,7 +20,7 @@ class OGPResolver implements Resolver, Parser
public function resolve(string $url): Metadata
{
return $this->parse($this->client->get($url)->getBody());
return $this->parse($this->client->get($url, [RequestOptions::COOKIES => new CookieJar()])->getBody());
}
public function parse(string $html): Metadata

View File

@ -0,0 +1,16 @@
<?php
namespace App\MetadataResolver;
use Throwable;
/**
* 規定回数以上の解決失敗により、メタデータの取得が不能となっている場合にスローされます。
*/
class ResolverCircuitBreakException extends \RuntimeException
{
public function __construct(int $errorCount, string $url, Throwable $previous = null)
{
parent::__construct("{$errorCount}回失敗しているためメタデータの取得を中断しました: {$url}", 0, $previous);
}
}

View File

@ -0,0 +1,33 @@
<?php
namespace App\MetadataResolver;
use GuzzleHttp\Client;
class TwitterResolver implements Resolver
{
/**
* @var Client
*/
private $client;
/**
* @var OGPResolver
*/
private $ogpResolver;
public function __construct(Client $client, OGPResolver $ogpResolver)
{
$this->client = $client;
$this->ogpResolver = $ogpResolver;
}
public function resolve(string $url): Metadata
{
$url = preg_replace('/(www\.)?(mobile|m)\.twitter\.com/u', 'twitter.com', $url);
$res = $this->client->get($url);
$html = (string) $res->getBody();
return $this->ogpResolver->parse($html);
}
}

View File

@ -0,0 +1,10 @@
<?php
namespace App\MetadataResolver;
/**
* MetadataResolver内で未キャッチの例外が発生した場合にスローされます。
*/
class UncaughtResolverException extends \RuntimeException
{
}

View File

@ -34,7 +34,7 @@ class XtubeResolver implements Resolver
$crawler = new Crawler($html);
$metadata->title = trim($crawler->filter('.underPlayerRateForm h1')->text(''));
$metadata->description = trim($crawler->filter('.fullDescription ')->text(''));
// $metadata->description = trim($crawler->filter('.fullDescription ')->text(''));
$metadata->image = str_replace('m=eSuQ8f', 'm=eaAaaEFb', $metadata->image);
$metadata->image = str_replace('240X180', 'original', $metadata->image);
$metadata->tags = array_map('trim', $crawler->filter('.tagsCategories a')->extract('_text'));

View File

@ -3,6 +3,8 @@
namespace App\Providers;
use App\MetadataResolver\MetadataResolver;
use GuzzleHttp\Client;
use GuzzleHttp\RequestOptions;
use Illuminate\Support\Facades\Blade;
use Illuminate\Support\ServiceProvider;
use Parsedown;
@ -19,6 +21,8 @@ class AppServiceProvider extends ServiceProvider
Blade::directive('parsedown', function ($expression) {
return "<?php echo app('parsedown')->text($expression); ?>";
});
stream_filter_register('convert.mbstring.*', 'Stream_Filter_Mbstring');
}
/**
@ -34,5 +38,12 @@ class AppServiceProvider extends ServiceProvider
$this->app->singleton('parsedown', function () {
return Parsedown::instance();
});
$this->app->bind(Client::class, function () {
return new Client([
RequestOptions::HEADERS => [
'User-Agent' => 'TissueBot/1.0'
]
]);
});
}
}

View File

@ -2,6 +2,8 @@
namespace App\Providers;
use Illuminate\Auth\Events\Registered;
use Illuminate\Auth\Listeners\SendEmailVerificationNotification;
use Illuminate\Foundation\Support\Providers\EventServiceProvider as ServiceProvider;
use Illuminate\Support\Facades\Event;
@ -13,6 +15,9 @@ class EventServiceProvider extends ServiceProvider
* @var array
*/
protected $listen = [
Registered::class => [
SendEmailVerificationNotification::class,
],
'App\Events\LinkDiscovered' => [
'App\Listeners\LinkCollector'
]

82
app/Rules/CsvDateTime.php Normal file
View File

@ -0,0 +1,82 @@
<?php
namespace App\Rules;
use Illuminate\Contracts\Validation\Rule;
/**
* CSVインポート機能の日時バリデーションルール
* @package App\Rules
*/
class CsvDateTime implements Rule
{
const VALID_FORMATS = [
'Y/m/d H:i:s',
'Y/n/j G:i:s',
'Y/m/d H:i',
'Y/n/j G:i',
];
const MINIMUM_TIMESTAMP = 946652400; // 2000-01-01 00:00:00 JST
const MAXIMUM_TIMESTAMP = 4102412399; // 2099-12-31 23:59:59 JST
/** @var string Validation error message */
private $message = ':attributeの形式は "年/月/日 時:分" にしてください。';
/**
* Create a new rule instance.
*
* @return void
*/
public function __construct()
{
//
}
/**
* Determine if the validation rule passes.
*
* @param string $attribute
* @param mixed $value
* @return bool
*/
public function passes($attribute, $value)
{
// この辺の実装の元ネタは、LaravelのValidatesAttributes#validateDateFormat()
if (!is_string($value)) {
return false;
}
foreach (self::VALID_FORMATS as $format) {
$date = \DateTime::createFromFormat('!' . $format, $value);
if (!$date) {
continue;
}
$timestamp = (int) $date->format('U');
if ($timestamp < self::MINIMUM_TIMESTAMP || self::MAXIMUM_TIMESTAMP < $timestamp) {
$this->message = ':attributeは 2000/01/01 00:00 〜 2099/12/31 23:59 の間のみ対応しています。';
return false;
}
$formatted = $date->format($format);
if ($formatted === $value) {
return true;
}
}
return false;
}
/**
* Get the validation error message.
*
* @return string
*/
public function message()
{
return $this->message;
}
}

View File

@ -0,0 +1,62 @@
<?php
namespace App\Rules;
use Illuminate\Contracts\Validation\Rule;
class FuzzyBoolean implements Rule
{
public static function isTruthy($value): bool
{
if ($value === 1 || $value === '1') {
return true;
}
$lower = strtolower((string)$value);
return $lower === 'true';
}
public static function isFalsy($value): bool
{
if ($value === null || $value === '' || $value === 0 || $value === '0') {
return true;
}
$lower = strtolower((string)$value);
return $lower === 'false';
}
/**
* Create a new rule instance.
*
* @return void
*/
public function __construct()
{
//
}
/**
* Determine if the validation rule passes.
*
* @param string $attribute
* @param mixed $value
* @return bool
*/
public function passes($attribute, $value)
{
return self::isTruthy($value) || self::isFalsy($value);
}
/**
* Get the validation error message.
*
* @return string
*/
public function message()
{
return __('validation.boolean');
}
}

View File

@ -0,0 +1,64 @@
<?php
namespace App\Services;
use App\User;
use Illuminate\Support\Facades\DB;
use League\Csv\Writer;
class CheckinCsvExporter
{
/** @var User Target user */
private $user;
/** @var string Output filename */
private $filename;
/** @var string Output charset */
private $charset;
public function __construct(User $user, string $filename, string $charset)
{
$this->user = $user;
$this->filename = $filename;
$this->charset = $charset;
}
public function execute()
{
$csv = Writer::createFromPath($this->filename, 'wb');
$csv->setNewline("\r\n");
if ($this->charset === 'SJIS-win') {
$csv->addStreamFilter('convert.mbstring.encoding.UTF-8:SJIS-win');
}
$header = ['日時', 'ノート', 'オカズリンク', '非公開', 'センシティブ'];
for ($i = 1; $i <= 32; $i++) {
$header[] = "タグ{$i}";
}
$csv->insertOne($header);
DB::transaction(function () use ($csv) {
// TODO: そんなに読み取り整合性を保つ努力はしていないのと、chunkの件数これでいいか分からない
$this->user->ejaculations()->with('tags')->orderBy('ejaculated_date')
->chunk(1000, function ($ejaculations) use ($csv) {
foreach ($ejaculations as $ejaculation) {
$record = [
$ejaculation->ejaculated_date->format('Y/m/d H:i'),
$ejaculation->note,
$ejaculation->link,
self::formatBoolean($ejaculation->is_private),
self::formatBoolean($ejaculation->is_too_sensitive),
];
foreach ($ejaculation->tags->take(32) as $tag) {
$record[] = $tag->name;
}
$csv->insertOne($record);
}
});
});
}
private static function formatBoolean($value): string
{
return $value ? 'true' : 'false';
}
}

View File

@ -0,0 +1,221 @@
<?php
declare(strict_types=1);
namespace App\Services;
use App\Ejaculation;
use App\Exceptions\CsvImportException;
use App\Rules\CsvDateTime;
use App\Rules\FuzzyBoolean;
use App\Tag;
use App\User;
use Carbon\Carbon;
use Illuminate\Database\QueryException;
use Illuminate\Support\Facades\DB;
use Illuminate\Support\Facades\Validator;
use League\Csv\Reader;
use Throwable;
class CheckinCsvImporter
{
/** @var int 取り込み件数の上限 */
private const IMPORT_LIMIT = 5000;
/** @var User Target user */
private $user;
/** @var string CSV filename */
private $filename;
public function __construct(User $user, string $filename)
{
$this->user = $user;
$this->filename = $filename;
}
/**
* インポート処理を実行します。
* @return int 取り込んだ件数
*/
public function execute(): int
{
// Guess charset
$charset = $this->guessCharset($this->filename);
// Open CSV
$csv = Reader::createFromPath($this->filename, 'r');
$csv->setHeaderOffset(0);
if ($charset === 'SJIS-win') {
$csv->addStreamFilter('convert.mbstring.encoding.SJIS-win:UTF-8');
}
// Import
return DB::transaction(function () use ($csv) {
$alreadyImportedCount = $this->user->ejaculations()->where('ejaculations.source', Ejaculation::SOURCE_CSV)->count();
$errors = [];
if (!in_array('日時', $csv->getHeader(), true)) {
$errors[] = '日時列は必須です。';
}
if (!empty($errors)) {
throw new CsvImportException(...$errors);
}
$imported = 0;
foreach ($csv->getRecords() as $offset => $record) {
$line = $offset + 1;
if (self::IMPORT_LIMIT <= $alreadyImportedCount + $imported) {
$limit = self::IMPORT_LIMIT;
$errors[] = "{$line} 行 : インポート機能で取り込めるデータは{$limit}件までに制限されています。これ以上取り込みできません。";
throw new CsvImportException(...$errors);
}
$ejaculation = new Ejaculation(['user_id' => $this->user->id]);
$validator = Validator::make($record, [
'日時' => ['required', new CsvDateTime()],
'ノート' => 'nullable|string|max:500',
'オカズリンク' => 'nullable|url|max:2000',
'非公開' => ['nullable', new FuzzyBoolean()],
'センシティブ' => ['nullable', new FuzzyBoolean()],
]);
if ($validator->fails()) {
foreach ($validator->errors()->all() as $message) {
$errors[] = "{$line} 行 : {$message}";
}
continue;
}
$ejaculation->ejaculated_date = Carbon::createFromFormat('!Y/m/d H:i+', $record['日時']);
$ejaculation->note = str_replace(["\r\n", "\r"], "\n", $record['ノート'] ?? '');
$ejaculation->link = $record['オカズリンク'] ?? '';
$ejaculation->source = Ejaculation::SOURCE_CSV;
if (isset($record['非公開'])) {
$ejaculation->is_private = FuzzyBoolean::isTruthy($record['非公開']);
}
if (isset($record['センシティブ'])) {
$ejaculation->is_too_sensitive = FuzzyBoolean::isTruthy($record['センシティブ']);
}
try {
$tags = $this->parseTags($line, $record);
} catch (CsvImportException $e) {
$errors = array_merge($errors, $e->getErrors());
continue;
}
DB::beginTransaction();
try {
$ejaculation->save();
if (!empty($tags)) {
$ejaculation->tags()->sync(collect($tags)->pluck('id'));
}
DB::commit();
$imported++;
} catch (QueryException $e) {
DB::rollBack();
if ($e->errorInfo[0] === '23505') {
$errors[] = "{$line} 行 : すでにこの日時のチェックインデータが存在します。";
continue;
}
throw $e;
} catch (Throwable $e) {
DB::rollBack();
throw $e;
}
}
if (!empty($errors)) {
throw new CsvImportException(...$errors);
}
return $imported;
});
}
/**
* 指定されたファイルを読み込み、文字コードの判定を行います。
* @param string $filename CSVファイル名
* @param int $samplingLength ファイルの先頭から何バイトを判定に使用するかを指定
* @return string 検出した文字コード (UTF-8, SJIS-win, ...)
* @throws CsvImportException ファイルの読み込みに失敗した、文字コードを判定できなかった、または非対応文字コードを検出した場合にスロー
*/
private function guessCharset(string $filename, int $samplingLength = 1024): string
{
$fp = fopen($filename, 'rb');
if (!$fp) {
throw new CsvImportException('CSVファイルの読み込み中にエラーが発生しました。');
}
try {
$head = fread($fp, $samplingLength);
if ($head === false) {
throw new CsvImportException('CSVファイルの読み込み中にエラーが発生しました。');
}
for ($addition = 0; $addition < 4; $addition++) {
$charset = mb_detect_encoding($head, ['ASCII', 'UTF-8', 'SJIS-win'], true);
if ($charset) {
if (array_search($charset, ['UTF-8', 'SJIS-win'], true) === false) {
throw new CsvImportException('文字コード判定に失敗しました。UTF-8 (BOM無し) または Shift_JIS をお使いください。');
} else {
return $charset;
}
}
// 1バイト追加で読み込んだら、文字境界に到達して上手く判定できるかもしれない
if (feof($fp)) {
break;
}
$next = fread($fp, 1);
if ($next === false) {
throw new CsvImportException('CSVファイルの読み込み中にエラーが発生しました。');
}
$head .= $next;
}
throw new CsvImportException('文字コード判定に失敗しました。UTF-8 (BOM無し) または Shift_JIS をお使いください。');
} finally {
fclose($fp);
}
}
/**
* タグ列をパースします。
* @param int $line 現在の行番号 (1 origin)
* @param array $record 対象行のデータ
* @return Tag[]
* @throws CsvImportException バリデーションエラーが発生した場合にスロー
*/
private function parseTags(int $line, array $record): array
{
$tags = [];
foreach (array_keys($record) as $column) {
if (preg_match('/\Aタグ\d{1,2}\z/u', $column) !== 1) {
continue;
}
$tag = trim($record[$column]);
if (empty($tag)) {
continue;
}
if (mb_strlen($tag) > 255) {
throw new CsvImportException("{$line} 行 : {$column}は255文字以内にしてください。");
}
if (strpos($tag, "\n") !== false) {
throw new CsvImportException("{$line} 行 : {$column}に改行を含めることはできません。");
}
if (strpos($tag, ' ') !== false) {
throw new CsvImportException("{$line} 行 : {$column}にスペースを含めることはできません。");
}
$tags[] = Tag::firstOrCreate(['name' => $tag]);
if (count($tags) >= 32) {
break;
}
}
return $tags;
}
}

View File

@ -0,0 +1,306 @@
<?php
namespace App\Services;
use App\ContentProvider;
use App\Metadata;
use App\MetadataResolver\DeniedHostException;
use App\MetadataResolver\DisallowedByProviderException;
use App\MetadataResolver\MetadataResolver;
use App\MetadataResolver\ResolverCircuitBreakException;
use App\MetadataResolver\UncaughtResolverException;
use App\Tag;
use App\Utilities\Formatter;
use Carbon\Carbon;
use Carbon\CarbonInterface;
use GuzzleHttp\Client;
use Illuminate\Support\Facades\DB;
use Illuminate\Support\Facades\Log;
class MetadataResolveService
{
/** @var int メタデータの解決を中断するエラー回数。この回数以上エラーしていたら処理は行わない。 */
const CIRCUIT_BREAK_COUNT = 5;
/** @var MetadataResolver */
private $resolver;
/** @var Formatter */
private $formatter;
public function __construct(MetadataResolver $resolver, Formatter $formatter)
{
$this->resolver = $resolver;
$this->formatter = $formatter;
}
/**
* メタデータをキャッシュまたはリモートに問い合わせて取得します。
* @param string $url メタデータを取得したいURL
* @return Metadata 取得できたメタデータ
* @throws DeniedHostException アクセス先がブラックリスト入りしているため取得できなかった場合にスロー
* @throws UncaughtResolverException Resolver内で例外が発生して取得できなかった場合にスロー
*/
public function execute(string $url): Metadata
{
// URLの正規化
$url = $this->formatter->normalizeUrl($url);
// 自分自身は解決しない
if (parse_url($url, PHP_URL_HOST) === parse_url(config('app.url'), PHP_URL_HOST)) {
throw new DeniedHostException($url);
}
$metadata = Metadata::find($url);
// 無かったら取得
// TODO: ある程度古かったら再取得とかありだと思う
if ($metadata == null || $metadata->needRefresh()) {
$hostWithPort = $this->getHostWithPortFromUrl($url);
$metadata = $this->hostLock($hostWithPort, function (?CarbonInterface $lastAccess) use ($url) {
// HostLockの解放待ちをしている間に、他のプロセスで取得完了しているかもしれない
$metadata = Metadata::find($url);
if ($metadata !== null && !$metadata->needRefresh()) {
return $metadata;
}
$this->checkProviderPolicy($url, $lastAccess);
return $this->resolve($url, $metadata);
});
}
return $metadata;
}
/**
* URLからホスト部とポート部を抽出
* @param string $url
* @return string
*/
private function getHostWithPortFromUrl(string $url): string
{
$parts = parse_url($url);
$host = $parts['host'];
if (isset($parts['port'])) {
$host .= ':' . $parts['port'];
}
return $host;
}
/**
* アクセス先ホスト単位の排他ロックを取って処理を実行
* @param string $host
* @param callable $fn
* @return mixed return of $fn
* @throws \RuntimeException いろいろな死に方をする
*/
private function hostLock(string $host, callable $fn)
{
$lockDir = storage_path('content_providers_lock');
if (!file_exists($lockDir)) {
if (!mkdir($lockDir)) {
throw new \RuntimeException("Lock failed! Can't create lock directory.");
}
}
$lockFile = $lockDir . DIRECTORY_SEPARATOR . $host;
$fp = fopen($lockFile, 'c+b');
if ($fp === false) {
throw new \RuntimeException("Lock failed! Can't open lock file.");
}
try {
if (!flock($fp, LOCK_EX)) {
throw new \RuntimeException("Lock failed! Can't lock file.");
}
try {
$accessInfoText = stream_get_contents($fp);
if ($accessInfoText !== false) {
$accessInfo = json_decode($accessInfoText, true);
}
$result = $fn(isset($accessInfo['time']) ? new Carbon($accessInfo['time']) : null);
$accessInfo = [
'time' => now()->toIso8601String()
];
fseek($fp, 0);
if (fwrite($fp, json_encode($accessInfo)) === false) {
throw new \RuntimeException("I/O Error! Can't write to lock file.");
}
return $result;
} finally {
if (!flock($fp, LOCK_UN)) {
throw new \RuntimeException("Unlock failed! Can't unlock file.");
}
}
} finally {
if (!fclose($fp)) {
throw new \RuntimeException("Unlock failed! Can't close lock file.");
}
}
}
/**
* 指定したメタデータURLのホストが持つrobots.txtをダウンロードします。
* @param string $url メタデータのURL
* @return string
*/
private function fetchRobotsTxt(string $url): ?string
{
$parts = parse_url($url);
$robotsUrl = http_build_url([
'scheme' => $parts['scheme'],
'host' => $parts['host'],
'port' => $parts['port'] ?? null,
'path' => '/robots.txt'
]);
$client = app(Client::class);
try {
$res = $client->get($robotsUrl);
if (stripos($res->getHeaderLine('Content-Type'), 'text/plain') !== 0) {
Log::error('robots.txtの取得に失敗: 不適切なContent-Type (' . $res->getHeaderLine('Content-Type') . ')');
return null;
}
return (string) $res->getBody();
} catch (\Exception $e) {
Log::error("robots.txtの取得に失敗: {$e}");
return null;
}
}
/**
* ContentProviderポリシー情報との照合を行い、アクセス可能かチェックします。アクセスできない場合は例外をスローします。
* @param string $url メタデータを取得したいURL
* @param CarbonInterface|null $lastAccess アクセス先ホストへの最終アクセス日時 (記録がある場合)
* @throws DeniedHostException アクセス先がTissue内のブラックリストに入っている場合にスロー
* @throws DisallowedByProviderException アクセス先のrobots.txtによって拒否されている場合にスロー
*/
private function checkProviderPolicy(string $url, ?CarbonInterface $lastAccess): void
{
DB::beginTransaction();
try {
$hostWithPort = $this->getHostWithPortFromUrl($url);
$contentProvider = ContentProvider::sharedLock()->find($hostWithPort);
if ($contentProvider === null) {
$contentProvider = ContentProvider::create([
'host' => $hostWithPort,
'robots' => $this->fetchRobotsTxt($url),
'robots_cached_at' => now(),
]);
}
if ($contentProvider->is_blocked) {
throw new DeniedHostException($url);
}
// 連続アクセス制限
if ($lastAccess !== null) {
$elapsedSeconds = $lastAccess->diffInSeconds(now(), false);
if ($elapsedSeconds < $contentProvider->access_interval_sec) {
if ($elapsedSeconds < 0) {
$wait = abs($elapsedSeconds) + $contentProvider->access_interval_sec;
} else {
$wait = $contentProvider->access_interval_sec - $elapsedSeconds;
}
sleep($wait);
}
}
// Fetch robots.txt
if ($contentProvider->robots_cached_at->diffInDays(now()) >= 7) {
$contentProvider->update([
'robots' => $this->fetchRobotsTxt($url),
'robots_cached_at' => now(),
]);
}
// Check robots.txt
$robotsParser = new \RobotsTxtParser($contentProvider->robots);
$robotsParser->setUserAgent('TissueBot');
$robotsDelay = $robotsParser->getDelay();
if ($robotsDelay !== 0 && $robotsDelay >= $contentProvider->access_interval_sec) {
$contentProvider->access_interval_sec = (int) $robotsDelay;
$contentProvider->save();
}
if ($robotsParser->isDisallowed(parse_url($url, PHP_URL_PATH))) {
throw new DisallowedByProviderException($url);
}
DB::commit();
} catch (DeniedHostException | DisallowedByProviderException $e) {
// ContentProviderのデータ更新は行うため
DB::commit();
throw $e;
} catch (\Exception $e) {
DB::rollBack();
throw $e;
}
}
/**
* メタデータをリモートサーバに問い合わせて取得します。
* @param string $url メタデータを取得したいURL
* @param Metadata|null $metadata キャッシュ済のメタデータ (存在する場合)
* @return Metadata 取得できたメタデータ
* @throws UncaughtResolverException Resolver内で例外が発生して取得できなかった場合にスロー
* @throws ResolverCircuitBreakException 規定回数以上の解決失敗により、メタデータの取得が不能となっている場合にスロー
*/
private function resolve(string $url, ?Metadata $metadata): Metadata
{
DB::beginTransaction();
try {
if ($metadata === null) {
$metadata = new Metadata(['url' => $url]);
}
if ($metadata->error_count >= self::CIRCUIT_BREAK_COUNT) {
throw new ResolverCircuitBreakException($metadata->error_count, $url);
}
try {
$resolved = $this->resolver->resolve($url);
} catch (\Exception $e) {
$metadata->storeException(now(), $e);
$metadata->save();
throw new UncaughtResolverException(implode(': ', [
$metadata->error_count . '回目のメタデータ取得失敗', get_class($e), $e->getMessage()
]), 0, $e);
}
$metadata->fill([
'title' => $resolved->title,
'description' => $resolved->description,
'image' => $resolved->image,
'expires_at' => $resolved->expires_at
]);
$metadata->clearError();
$metadata->save();
$tagIds = [];
foreach ($resolved->normalizedTags() as $tagName) {
$tag = Tag::firstOrCreate(['name' => $tagName]);
$tagIds[] = $tag->id;
}
$metadata->tags()->sync($tagIds);
DB::commit();
return $metadata;
} catch (UncaughtResolverException $e) {
// Metadataにエラー情報を記録するため
DB::commit();
throw $e;
} catch (\Exception $e) {
DB::rollBack();
throw $e;
}
}
}

View File

@ -2,6 +2,7 @@
namespace App;
use App\Utilities\Formatter;
use Illuminate\Database\Eloquent\Model;
class Tag extends Model
@ -15,8 +16,22 @@ class Tag extends Model
'name'
];
protected static function boot()
{
parent::boot();
self::creating(function (Tag $tag) {
$tag->normalized_name = app(Formatter::class)->normalizeTagName($tag->name);
});
}
public function ejaculations()
{
return $this->belongsToMany('App\Ejaculation')->withTimestamps();
}
public function metadata()
{
return $this->belongsToMany('App\Metadata')->withTimestamps();
}
}

View File

@ -32,6 +32,15 @@ class User extends Authenticatable
'password', 'remember_token',
];
/**
* The attributes that should be cast to native types.
*
* @var array
*/
protected $casts = [
// 'email_verified_at' => 'datetime',
];
/**
* このユーザのメールアドレスから、Gravatarの画像URLを生成します。
* @param int $size 画像サイズ
@ -62,4 +71,9 @@ class User extends Authenticatable
{
return $this->hasMany(Like::class);
}
public function checkinWebhooks()
{
return $this->hasMany(CheckinWebhook::class);
}
}

View File

@ -2,6 +2,7 @@
namespace App\Utilities;
use Illuminate\Support\Str;
use Misd\Linkify\Linkify;
class Formatter
@ -55,10 +56,10 @@ class Formatter
$parts = parse_url($url);
if (!empty($parts['query'])) {
// Remove query parameters
$url = str_replace_last('?' . $parts['query'], '', $url);
$url = Str::replaceFirst('?' . $parts['query'], '', $url);
if (!empty($parts['fragment'])) {
// Remove fragment identifier
$url = str_replace_last('#' . $parts['fragment'], '', $url);
$url = Str::replaceFirst('#' . $parts['fragment'], '', $url);
} else {
// "http://example.com/?query#" の場合 $parts['fragment'] は unset になるので、個別に判定して除去する必要がある
$url = preg_replace('/#\z/u', '', $url);
@ -75,4 +76,68 @@ class Formatter
return $url;
}
/**
* imgタグのsrcsetで使用できる形式で、プロフィール画像URLを生成します。
* @param object $user Userなど、getProfileImageUrl()が実装されているオブジェクト
* @param int $baseSize 1x解像度における画像サイズ
* @param int $maxDensity 最高密度
* @return string srcset用の文字列
*/
public function profileImageSrcSet($user, int $baseSize, int $maxDensity = 3)
{
$srcset = [];
for ($i = 1; $i <= $maxDensity; $i++) {
$srcset[] = $user->getProfileImageUrl($baseSize * $i) . " {$i}x";
}
return implode(',', $srcset);
}
/**
* php.ini書式のデータサイズを正規化します。
* @param mixed $val データサイズ
* @return string
*/
public function normalizeIniBytes($val)
{
$val = trim($val);
$last = strtolower(substr($val, -1, 1));
if (ord($last) < 0x30 || ord($last) > 0x39) {
$bytes = substr($val, 0, -1);
switch ($last) {
case 'g':
$bytes *= 1024;
// fall through
// no break
case 'm':
$bytes *= 1024;
// fall through
// no break
case 'k':
$bytes *= 1024;
break;
}
} else {
$bytes = $val;
}
if ($bytes >= (1 << 30)) {
return ($bytes >> 30) . 'GB';
} elseif ($bytes >= (1 << 20)) {
return ($bytes >> 20) . 'MB';
} elseif ($bytes >= (1 << 10)) {
return ($bytes >> 10) . 'KB';
}
return $bytes . 'B';
}
public function normalizeTagName(string $name)
{
$name = \Normalizer::normalize($name, \Normalizer::FORM_KC);
$name = mb_strtolower($name);
return $name;
}
}

View File

@ -12,7 +12,7 @@
*/
$app = new Illuminate\Foundation\Application(
realpath(__DIR__.'/../')
$_ENV['APP_BASE_PATH'] ?? dirname(__DIR__)
);
/*

View File

@ -4,28 +4,47 @@
"keywords": ["framework", "laravel"],
"license": "MIT",
"type": "project",
"repositories": [
{
"type": "vcs",
"url": "https://github.com/xcezx/Stream_Filter_Mbstring"
}
],
"require": {
"php": ">=7.1.0",
"php": "^7.2",
"ext-dom": "*",
"ext-intl": "*",
"ext-json": "*",
"ext-libxml": "*",
"ext-mbstring": "*",
"ext-pdo": "*",
"anhskohbo/no-captcha": "^3.0",
"doctrine/dbal": "^2.9",
"fideloper/proxy": "~3.3",
"erusev/parsedown": "^1.7",
"fideloper/proxy": "^4.0",
"guzzlehttp/guzzle": "^6.3",
"jakeasmith/http_build_url": "^1.0",
"laravel/framework": "5.5.*",
"laravel/tinker": "~1.0",
"laravel/framework": "^6.2",
"laravel/helpers": "^1.2",
"laravel/tinker": "^2.0",
"league/csv": "^9.5",
"misd/linkify": "^1.1",
"openpear/stream_filter_mbstring": "dev-master",
"sentry/sentry-laravel": "1.8.0",
"staudenmeir/eloquent-eager-limit": "^1.0",
"symfony/css-selector": "^4.3",
"symfony/dom-crawler": "^4.3"
"symfony/dom-crawler": "^4.3",
"t1gor/robots-txt-parser": "^0.2.4"
},
"require-dev": {
"barryvdh/laravel-debugbar": "^3.1",
"barryvdh/laravel-ide-helper": "^2.5",
"filp/whoops": "~2.0",
"facade/ignition": "^1.4",
"friendsofphp/php-cs-fixer": "^2.14",
"fzaninotto/faker": "~1.4",
"mockery/mockery": "~1.0",
"phpunit/phpunit": "~6.0",
"fzaninotto/faker": "^1.9.1",
"mockery/mockery": "^1.0",
"nunomaduro/collision": "^3.0",
"phpunit/phpunit": "^8.0",
"symfony/thanks": "^1.0"
},
"autoload": {
@ -46,11 +65,11 @@
"@php -r \"file_exists('.env') || copy('.env.example', '.env');\""
],
"post-create-project-cmd": [
"@php artisan key:generate"
"@php artisan key:generate --ansi"
],
"post-autoload-dump": [
"Illuminate\\Foundation\\ComposerScripts::postAutoloadDump",
"@php artisan package:discover"
"@php artisan package:discover --ansi"
],
"fix": [
"php-cs-fixer fix --config=.php_cs.dist"
@ -63,5 +82,12 @@
"preferred-install": "dist",
"sort-packages": true,
"optimize-autoloader": true
}
},
"extra": {
"laravel": {
"dont-discover": []
}
},
"minimum-stability": "dev",
"prefer-stable": true
}

5331
composer.lock generated

File diff suppressed because it is too large Load Diff

View File

@ -38,7 +38,7 @@ return [
|
*/
'debug' => env('APP_DEBUG', false),
'debug' => (bool) env('APP_DEBUG', false),
/*
|--------------------------------------------------------------------------
@ -53,6 +53,8 @@ return [
'url' => env('APP_URL', 'http://localhost'),
'asset_url' => env('ASSET_URL', null),
/*
|--------------------------------------------------------------------------
| Application Timezone
@ -92,6 +94,19 @@ return [
'fallback_locale' => 'en',
/*
|--------------------------------------------------------------------------
| Faker Locale
|--------------------------------------------------------------------------
|
| This locale will be used by the Faker PHP library when generating fake
| data for your database seeds. For example, this will be used to get
| localized telephone numbers, street address information and more.
|
*/
'faker_locale' => 'en_US',
/*
|--------------------------------------------------------------------------
| Encryption Key
@ -107,23 +122,6 @@ return [
'cipher' => 'AES-256-CBC',
/*
|--------------------------------------------------------------------------
| Logging Configuration
|--------------------------------------------------------------------------
|
| Here you may configure the log settings for your application. Out of
| the box, Laravel uses the Monolog PHP logging library. This gives
| you a variety of powerful log handlers / formatters to utilize.
|
| Available Settings: "single", "daily", "syslog", "errorlog"
|
*/
'log' => env('APP_LOG', 'single'),
'log_level' => env('APP_LOG_LEVEL', 'debug'),
/*
|--------------------------------------------------------------------------
| Autoloaded Service Providers
@ -194,6 +192,7 @@ return [
'aliases' => [
'App' => Illuminate\Support\Facades\App::class,
'Arr' => Illuminate\Support\Arr::class,
'Artisan' => Illuminate\Support\Facades\Artisan::class,
'Auth' => Illuminate\Support\Facades\Auth::class,
'Blade' => Illuminate\Support\Facades\Blade::class,
@ -223,6 +222,7 @@ return [
'Schema' => Illuminate\Support\Facades\Schema::class,
'Session' => Illuminate\Support\Facades\Session::class,
'Storage' => Illuminate\Support\Facades\Storage::class,
'Str' => Illuminate\Support\Str::class,
'URL' => Illuminate\Support\Facades\URL::class,
'Validator' => Illuminate\Support\Facades\Validator::class,
'View' => Illuminate\Support\Facades\View::class,

View File

@ -44,6 +44,7 @@ return [
'api' => [
'driver' => 'token',
'provider' => 'users',
'hash' => false,
],
],
@ -96,7 +97,21 @@ return [
'provider' => 'users',
'table' => 'password_resets',
'expire' => 60,
'throttle' => 60,
],
],
/*
|--------------------------------------------------------------------------
| Password Confirmation Timeout
|--------------------------------------------------------------------------
|
| Here you may define the amount of seconds before a password confirmation
| times out and the user is prompted to re-enter their password via the
| confirmation screen. By default, the timeout lasts for three hours.
|
*/
'password_timeout' => 10800,
];

View File

@ -36,7 +36,8 @@ return [
'secret' => env('PUSHER_APP_SECRET'),
'app_id' => env('PUSHER_APP_ID'),
'options' => [
//
'cluster' => env('PUSHER_APP_CLUSTER'),
'useTLS' => true,
],
],

View File

@ -1,5 +1,7 @@
<?php
use Illuminate\Support\Str;
return [
/*
@ -11,7 +13,8 @@ return [
| using this caching library. This connection is used when another is
| not explicitly specified when executing a given caching function.
|
| Supported: "apc", "array", "database", "file", "memcached", "redis"
| Supported: "apc", "array", "database", "file",
| "memcached", "redis", "dynamodb"
|
*/
@ -57,7 +60,7 @@ return [
env('MEMCACHED_PASSWORD'),
],
'options' => [
// Memcached::OPT_CONNECT_TIMEOUT => 2000,
// Memcached::OPT_CONNECT_TIMEOUT => 2000,
],
'servers' => [
[
@ -70,7 +73,16 @@ return [
'redis' => [
'driver' => 'redis',
'connection' => 'default',
'connection' => 'cache',
],
'dynamodb' => [
'driver' => 'dynamodb',
'key' => env('AWS_ACCESS_KEY_ID'),
'secret' => env('AWS_SECRET_ACCESS_KEY'),
'region' => env('AWS_DEFAULT_REGION', 'us-east-1'),
'table' => env('DYNAMODB_CACHE_TABLE', 'cache'),
'endpoint' => env('DYNAMODB_ENDPOINT'),
],
],
@ -86,6 +98,6 @@ return [
|
*/
'prefix' => 'laravel',
'prefix' => env('CACHE_PREFIX', Str::slug(env('APP_NAME', 'laravel'), '_').'_cache'),
];

View File

@ -1,5 +1,7 @@
<?php
use Illuminate\Support\Str;
return [
/*
@ -35,12 +37,15 @@ return [
'sqlite' => [
'driver' => 'sqlite',
'url' => env('DATABASE_URL'),
'database' => env('DB_DATABASE', database_path('database.sqlite')),
'prefix' => '',
'foreign_key_constraints' => env('DB_FOREIGN_KEYS', true),
],
'mysql' => [
'driver' => 'mysql',
'url' => env('DATABASE_URL'),
'host' => env('DB_HOST', '127.0.0.1'),
'port' => env('DB_PORT', '3306'),
'database' => env('DB_DATABASE', 'forge'),
@ -50,12 +55,17 @@ return [
'charset' => 'utf8mb4',
'collation' => 'utf8mb4_unicode_ci',
'prefix' => '',
'prefix_indexes' => true,
'strict' => true,
'engine' => null,
'options' => extension_loaded('pdo_mysql') ? array_filter([
PDO::MYSQL_ATTR_SSL_CA => env('MYSQL_ATTR_SSL_CA'),
]) : [],
],
'pgsql' => [
'driver' => 'pgsql',
'url' => env('DATABASE_URL'),
'host' => env('DB_HOST', '127.0.0.1'),
'port' => env('DB_PORT', '5432'),
'database' => env('DB_DATABASE', 'forge'),
@ -63,12 +73,14 @@ return [
'password' => env('DB_PASSWORD', ''),
'charset' => 'utf8',
'prefix' => '',
'prefix_indexes' => true,
'schema' => 'public',
'sslmode' => 'prefer',
],
'sqlsrv' => [
'driver' => 'sqlsrv',
'url' => env('DATABASE_URL'),
'host' => env('DB_HOST', 'localhost'),
'port' => env('DB_PORT', '1433'),
'database' => env('DB_DATABASE', 'forge'),
@ -76,6 +88,7 @@ return [
'password' => env('DB_PASSWORD', ''),
'charset' => 'utf8',
'prefix' => '',
'prefix_indexes' => true,
],
],
@ -99,20 +112,34 @@ return [
|--------------------------------------------------------------------------
|
| Redis is an open source, fast, and advanced key-value store that also
| provides a richer set of commands than a typical key-value systems
| provides a richer body of commands than a typical key-value system
| such as APC or Memcached. Laravel makes it easy to dig right in.
|
*/
'redis' => [
'client' => 'predis',
'client' => env('REDIS_CLIENT', 'phpredis'),
'options' => [
'cluster' => env('REDIS_CLUSTER', 'redis'),
'prefix' => env('REDIS_PREFIX', Str::slug(env('APP_NAME', 'laravel'), '_').'_database_'),
],
'default' => [
'url' => env('REDIS_URL'),
'host' => env('REDIS_HOST', '127.0.0.1'),
'password' => env('REDIS_PASSWORD', null),
'port' => env('REDIS_PORT', 6379),
'database' => 0,
'port' => env('REDIS_PORT', '6379'),
'database' => env('REDIS_DB', '0'),
],
'cache' => [
'url' => env('REDIS_URL'),
'host' => env('REDIS_HOST', '127.0.0.1'),
'password' => env('REDIS_PASSWORD', null),
'port' => env('REDIS_PORT', '6379'),
'database' => env('REDIS_CACHE_DB', '1'),
],
],

View File

@ -37,7 +37,7 @@ return [
| may even configure multiple disks of the same driver. Defaults have
| been setup for each driver as an example of the required options.
|
| Supported Drivers: "local", "ftp", "s3", "rackspace"
| Supported Drivers: "local", "ftp", "sftp", "s3"
|
*/
@ -61,6 +61,8 @@ return [
'secret' => env('AWS_SECRET'),
'region' => env('AWS_REGION'),
'bucket' => env('AWS_BUCKET'),
'url' => env('AWS_URL'),
'endpoint' => env('AWS_ENDPOINT'),
],
],

52
config/hashing.php Normal file
View File

@ -0,0 +1,52 @@
<?php
return [
/*
|--------------------------------------------------------------------------
| Default Hash Driver
|--------------------------------------------------------------------------
|
| This option controls the default hash driver that will be used to hash
| passwords for your application. By default, the bcrypt algorithm is
| used; however, you remain free to modify this option if you wish.
|
| Supported: "bcrypt", "argon", "argon2id"
|
*/
'driver' => 'bcrypt',
/*
|--------------------------------------------------------------------------
| Bcrypt Options
|--------------------------------------------------------------------------
|
| Here you may specify the configuration options that should be used when
| passwords are hashed using the Bcrypt algorithm. This will allow you
| to control the amount of time it takes to hash the given password.
|
*/
'bcrypt' => [
'rounds' => env('BCRYPT_ROUNDS', 10),
],
/*
|--------------------------------------------------------------------------
| Argon Options
|--------------------------------------------------------------------------
|
| Here you may specify the configuration options that should be used when
| passwords are hashed using the Argon algorithm. These will allow you
| to control the amount of time it takes to hash the given password.
|
*/
'argon' => [
'memory' => 1024,
'threads' => 2,
'time' => 2,
],
];

102
config/logging.php Normal file
View File

@ -0,0 +1,102 @@
<?php
use Monolog\Handler\StreamHandler;
use Monolog\Handler\SyslogUdpHandler;
return [
/*
|--------------------------------------------------------------------------
| Default Log Channel
|--------------------------------------------------------------------------
|
| This option defines the default log channel that gets used when writing
| messages to the logs. The name specified in this option should match
| one of the channels defined in the "channels" configuration array.
|
*/
'default' => env('LOG_CHANNEL', 'stack'),
/*
|--------------------------------------------------------------------------
| Log Channels
|--------------------------------------------------------------------------
|
| Here you may configure the log channels for your application. Out of
| the box, Laravel uses the Monolog PHP logging library. This gives
| you a variety of powerful log handlers / formatters to utilize.
|
| Available Drivers: "single", "daily", "slack", "syslog",
| "errorlog", "monolog",
| "custom", "stack"
|
*/
'channels' => [
'stack' => [
'driver' => 'stack',
'channels' => ['single'],
],
'single' => [
'driver' => 'single',
'path' => storage_path('logs/laravel.log'),
'level' => 'debug',
],
'daily' => [
'driver' => 'daily',
'path' => storage_path('logs/laravel.log'),
'level' => 'debug',
'days' => 14,
],
'slack' => [
'driver' => 'slack',
'url' => env('LOG_SLACK_WEBHOOK_URL'),
'username' => 'Laravel Log',
'emoji' => ':boom:',
'level' => 'critical',
],
'papertrail' => [
'driver' => 'monolog',
'level' => 'debug',
'handler' => SyslogUdpHandler::class,
'handler_with' => [
'host' => env('PAPERTRAIL_URL'),
'port' => env('PAPERTRAIL_PORT'),
],
],
'stderr' => [
'driver' => 'monolog',
'handler' => StreamHandler::class,
'formatter' => env('LOG_STDERR_FORMATTER'),
'with' => [
'stream' => 'php://stderr',
],
],
'syslog' => [
'driver' => 'syslog',
'level' => 'debug',
],
'errorlog' => [
'driver' => 'errorlog',
'level' => 'debug',
],
'null' => [
'driver' => 'monolog',
'handler' => NullHandler::class,
],
'emergency' => [
'path' => storage_path('logs/laravel.log'),
],
],
];

View File

@ -11,8 +11,8 @@ return [
| sending of e-mail. You may specify which one you're using throughout
| your application here. By default, Laravel is setup for SMTP mail.
|
| Supported: "smtp", "sendmail", "mailgun", "mandrill", "ses",
| "sparkpost", "log", "array"
| Supported: "smtp", "sendmail", "mailgun", "ses",
| "postmark", "log", "array"
|
*/
@ -120,4 +120,17 @@ return [
],
],
/*
|--------------------------------------------------------------------------
| Log Channel
|--------------------------------------------------------------------------
|
| If you are using the "log" driver, you may specify the logging channel
| if you prefer to keep mail messages separate from other log entries
| for simpler reading. Otherwise, the default channel will be used.
|
*/
'log_channel' => env('MAIL_LOG_CHANNEL'),
];

View File

@ -4,14 +4,12 @@ return [
/*
|--------------------------------------------------------------------------
| Default Queue Driver
| Default Queue Connection Name
|--------------------------------------------------------------------------
|
| Laravel's queue API supports an assortment of back-ends via a single
| API, giving you convenient access to each back-end using the same
| syntax for each one. Here you may set the default queue driver.
|
| Supported: "sync", "database", "beanstalkd", "sqs", "redis", "null"
| syntax for every one. Here you may define a default connection.
|
*/
@ -26,6 +24,8 @@ return [
| is used by your application. A default configuration has been added
| for each back-end shipped with Laravel. You are free to add more.
|
| Drivers: "sync", "database", "beanstalkd", "sqs", "redis", "null"
|
*/
'connections' => [
@ -46,22 +46,24 @@ return [
'host' => 'localhost',
'queue' => 'default',
'retry_after' => 90,
'block_for' => 0,
],
'sqs' => [
'driver' => 'sqs',
'key' => 'your-public-key',
'secret' => 'your-secret-key',
'prefix' => 'https://sqs.us-east-1.amazonaws.com/your-account-id',
'queue' => 'your-queue-name',
'region' => 'us-east-1',
'key' => env('AWS_ACCESS_KEY_ID'),
'secret' => env('AWS_SECRET_ACCESS_KEY'),
'prefix' => env('SQS_PREFIX', 'https://sqs.us-east-1.amazonaws.com/your-account-id'),
'queue' => env('SQS_QUEUE', 'your-queue-name'),
'region' => env('AWS_DEFAULT_REGION', 'us-east-1'),
],
'redis' => [
'driver' => 'redis',
'connection' => 'default',
'queue' => 'default',
'queue' => env('REDIS_QUEUE', 'default'),
'retry_after' => 90,
'block_for' => null,
],
],
@ -78,6 +80,7 @@ return [
*/
'failed' => [
'driver' => env('QUEUE_FAILED_DRIVER', 'database'),
'database' => env('DB_CONNECTION', 'mysql'),
'table' => 'failed_jobs',
],

30
config/sentry.php Normal file
View File

@ -0,0 +1,30 @@
<?php
return [
'dsn' => env('SENTRY_LARAVEL_DSN', env('SENTRY_DSN')),
// capture release as git sha
// 'release' => trim(exec('git --git-dir ' . base_path('.git') . ' log --pretty="%h" -n1 HEAD')),
'breadcrumbs' => [
// Capture Laravel logs in breadcrumbs
'logs' => true,
// Capture SQL queries in breadcrumbs
'sql_queries' => true,
// Capture bindings on SQL queries logged in breadcrumbs
'sql_bindings' => true,
// Capture queue job information in breadcrumbs
'queue_info' => true,
// Capture command information in breadcrumbs
'command_info' => true,
],
// @see: https://docs.sentry.io/error-reporting/configuration/?platform=php#send-default-pii
'send_default_pii' => false,
];

View File

@ -8,31 +8,26 @@ return [
|--------------------------------------------------------------------------
|
| This file is for storing the credentials for third party services such
| as Stripe, Mailgun, SparkPost and others. This file provides a sane
| default location for this type of information, allowing packages
| to have a conventional place to find your various credentials.
| as Mailgun, SparkPost and others. This file provides a sane default
| location for this type of information, allowing packages to have
| a conventional file to locate the various service credentials.
|
*/
'mailgun' => [
'domain' => env('MAILGUN_DOMAIN'),
'secret' => env('MAILGUN_SECRET'),
'endpoint' => env('MAILGUN_ENDPOINT', 'api.mailgun.net'),
],
'postmark' => [
'token' => env('POSTMARK_TOKEN'),
],
'ses' => [
'key' => env('SES_KEY'),
'secret' => env('SES_SECRET'),
'region' => 'us-east-1',
],
'sparkpost' => [
'secret' => env('SPARKPOST_SECRET'),
],
'stripe' => [
'model' => App\User::class,
'key' => env('STRIPE_KEY'),
'secret' => env('STRIPE_SECRET'),
'key' => env('AWS_ACCESS_KEY_ID'),
'secret' => env('AWS_SECRET_ACCESS_KEY'),
'region' => env('AWS_DEFAULT_REGION', 'us-east-1'),
],
];

View File

@ -1,5 +1,7 @@
<?php
use Illuminate\Support\Str;
return [
/*
@ -29,7 +31,7 @@ return [
|
*/
'lifetime' => 120,
'lifetime' => env('SESSION_LIFETIME', 120),
'expire_on_close' => false,
@ -70,7 +72,7 @@ return [
|
*/
'connection' => null,
'connection' => env('SESSION_CONNECTION', null),
/*
|--------------------------------------------------------------------------
@ -96,7 +98,7 @@ return [
|
*/
'store' => null,
'store' => env('SESSION_STORE', null),
/*
|--------------------------------------------------------------------------
@ -122,7 +124,10 @@ return [
|
*/
'cookie' => 'laravel_session',
'cookie' => env(
'SESSION_COOKIE',
Str::slug(env('APP_NAME', 'laravel'), '_').'_session'
),
/*
|--------------------------------------------------------------------------
@ -176,4 +181,19 @@ return [
'http_only' => true,
/*
|--------------------------------------------------------------------------
| Same-Site Cookies
|--------------------------------------------------------------------------
|
| This option determines how your cookies behave when cross-site requests
| take place, and can be used to mitigate CSRF attacks. By default, we
| do not enable this as other CSRF protection services are in place.
|
| Supported: "lax", "strict", "none"
|
*/
'same_site' => null,
];

View File

@ -28,6 +28,9 @@ return [
|
*/
'compiled' => realpath(storage_path('framework/views')),
'compiled' => env(
'VIEW_COMPILED_PATH',
realpath(storage_path('framework/views'))
),
];

1
database/.gitignore vendored
View File

@ -1 +1,2 @@
*.sqlite
*.sqlite-journal

View File

@ -0,0 +1,12 @@
<?php
/** @var \Illuminate\Database\Eloquent\Factory $factory */
use App\CheckinWebhook;
use Faker\Generator as Faker;
$factory->define(CheckinWebhook::class, function (Faker $faker) {
return [
'name' => 'example'
];
});

View File

@ -0,0 +1,14 @@
<?php
/** @var \Illuminate\Database\Eloquent\Factory $factory */
use App\ContentProvider;
use Faker\Generator as Faker;
$factory->define(ContentProvider::class, function (Faker $faker) {
return [
'host' => 'example.com',
'robots' => null,
'robots_cached_at' => now(),
];
});

View File

@ -8,5 +8,6 @@ $factory->define(Ejaculation::class, function (Faker $faker) {
return [
'ejaculated_date' => $faker->date('Y-m-d H:i:s'),
'note' => $faker->text,
'source' => Ejaculation::SOURCE_WEB,
];
});

View File

@ -1,6 +1,8 @@
<?php
/** @var \Illuminate\Database\Eloquent\Factory $factory */
use Illuminate\Support\Str;
$factory->define(App\User::class, function (Faker\Generator $faker) {
static $password;
@ -8,7 +10,7 @@ $factory->define(App\User::class, function (Faker\Generator $faker) {
'name' => substr($faker->userName, 0, 15),
'email' => $faker->unique()->safeEmail,
'password' => $password ?: $password = bcrypt('secret'),
'remember_token' => str_random(10),
'remember_token' => Str::random(10),
'display_name' => substr($faker->name, 0, 20),
'is_protected' => false,
'accept_analytics' => false,

View File

@ -0,0 +1,38 @@
<?php
use Illuminate\Database\Migrations\Migration;
use Illuminate\Database\Schema\Blueprint;
use Illuminate\Support\Facades\Schema;
class AddSourceToEjaculations extends Migration
{
/**
* Run the migrations.
*
* @return void
*/
public function up()
{
Schema::create('ejaculation_sources', function (Blueprint $table) {
$table->string('name');
$table->primary('name');
});
Schema::table('ejaculations', function (Blueprint $table) {
$table->string('source')->nullable();
$table->foreign('source')->references('name')->on('ejaculation_sources');
});
}
/**
* Reverse the migrations.
*
* @return void
*/
public function down()
{
Schema::table('ejaculations', function (Blueprint $table) {
$table->dropColumn('source');
});
Schema::drop('ejaculation_sources');
}
}

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']);
});
}
}

View File

@ -0,0 +1,34 @@
<?php
use Illuminate\Database\Migrations\Migration;
use Illuminate\Database\Schema\Blueprint;
use Illuminate\Support\Facades\Schema;
class MakePrimaryKeyOnMetadata extends Migration
{
/**
* Run the migrations.
*
* @return void
*/
public function up()
{
Schema::table('metadata', function (Blueprint $table) {
$table->dropIndex(['url']);
$table->primary('url');
});
}
/**
* Reverse the migrations.
*
* @return void
*/
public function down()
{
Schema::table('metadata', function (Blueprint $table) {
$table->dropPrimary(['url']);
$table->index('url');
});
}
}

View File

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

View File

@ -0,0 +1,34 @@
<?php
use App\Ejaculation;
use Illuminate\Database\Migrations\Migration;
use Illuminate\Database\Schema\Blueprint;
use Illuminate\Support\Facades\Schema;
class MakeNonNullableSourceOnEjaculations extends Migration
{
/**
* Run the migrations.
*
* @return void
*/
public function up()
{
DB::statement('UPDATE ejaculations SET source = ? WHERE source IS NULL', [Ejaculation::SOURCE_WEB]);
Schema::table('ejaculations', function (Blueprint $table) {
$table->string('source')->nullable(false)->change();
});
}
/**
* Reverse the migrations.
*
* @return void
*/
public function down()
{
Schema::table('ejaculations', function (Blueprint $table) {
$table->string('source')->nullable()->change();
});
}
}

View File

@ -0,0 +1,39 @@
<?php
use Illuminate\Database\Migrations\Migration;
use Illuminate\Database\Schema\Blueprint;
use Illuminate\Support\Facades\Schema;
class CreateCheckinWebhooksTable extends Migration
{
/**
* Run the migrations.
*
* @return void
*/
public function up()
{
Schema::create('checkin_webhooks', function (Blueprint $table) {
$table->string('id', 64);
$table->integer('user_id')->nullable();
$table->string('name');
$table->timestamps();
$table->softDeletes();
$table->primary('id');
$table->index('user_id');
$table->foreign('user_id')->references('id')->on('users')->onDelete('set null');
});
}
/**
* Reverse the migrations.
*
* @return void
*/
public function down()
{
Schema::dropIfExists('checkin_webhooks');
}
}

View File

@ -0,0 +1,32 @@
<?php
use Illuminate\Database\Migrations\Migration;
use Illuminate\Database\Schema\Blueprint;
use Illuminate\Support\Facades\Schema;
class AddCheckinWebhookIdToEjaculations extends Migration
{
/**
* Run the migrations.
*
* @return void
*/
public function up()
{
Schema::table('ejaculations', function (Blueprint $table) {
$table->string('checkin_webhook_id', 64)->nullable();
});
}
/**
* Reverse the migrations.
*
* @return void
*/
public function down()
{
Schema::table('ejaculations', function (Blueprint $table) {
$table->dropColumn('checkin_webhook_id');
});
}
}

View File

@ -0,0 +1,32 @@
<?php
use Illuminate\Database\Migrations\Migration;
use Illuminate\Database\Schema\Blueprint;
use Illuminate\Support\Facades\Schema;
class AddNormalizedNameToTags extends Migration
{
/**
* Run the migrations.
*
* @return void
*/
public function up()
{
Schema::table('tags', function (Blueprint $table) {
$table->string('normalized_name')->nullable();
});
}
/**
* Reverse the migrations.
*
* @return void
*/
public function down()
{
Schema::table('tags', function (Blueprint $table) {
$table->dropColumn('normalized_name');
});
}
}

View File

@ -0,0 +1,36 @@
<?php
use Illuminate\Database\Migrations\Migration;
use Illuminate\Database\Schema\Blueprint;
use Illuminate\Support\Facades\Schema;
class AddErrorDataToMetadata extends Migration
{
/**
* Run the migrations.
*
* @return void
*/
public function up()
{
Schema::table('metadata', function (Blueprint $table) {
$table->timestamp('error_at')->nullable();
$table->string('error_exception_class')->nullable();
$table->integer('error_http_code')->nullable();
$table->text('error_body')->nullable();
$table->integer('error_count')->default(0);
});
}
/**
* Reverse the migrations.
*
* @return void
*/
public function down()
{
Schema::table('metadata', function (Blueprint $table) {
$table->dropColumn(['error_at', 'error_exception_class', 'error_http_code', 'error_body', 'error_count']);
});
}
}

View File

@ -0,0 +1,37 @@
<?php
use Illuminate\Database\Migrations\Migration;
use Illuminate\Database\Schema\Blueprint;
use Illuminate\Support\Facades\Schema;
class CreateContentProvidersTable extends Migration
{
/**
* Run the migrations.
*
* @return void
*/
public function up()
{
Schema::create('content_providers', function (Blueprint $table) {
$table->string('host');
$table->text('robots')->nullable();
$table->timestamp('robots_cached_at');
$table->boolean('is_blocked')->default(false);
$table->integer('access_interval_sec')->default(5);
$table->timestamps();
$table->primary('host');
});
}
/**
* Reverse the migrations.
*
* @return void
*/
public function down()
{
Schema::dropIfExists('content_providers');
}
}

View File

@ -0,0 +1,32 @@
<?php
use Illuminate\Database\Migrations\Migration;
use Illuminate\Database\Schema\Blueprint;
use Illuminate\Support\Facades\Schema;
class AddDiscardElapsedTimeToEjaculations extends Migration
{
/**
* Run the migrations.
*
* @return void
*/
public function up()
{
Schema::table('ejaculations', function (Blueprint $table) {
$table->boolean('discard_elapsed_time')->default(false);
});
}
/**
* Reverse the migrations.
*
* @return void
*/
public function down()
{
Schema::table('ejaculations', function (Blueprint $table) {
$table->dropColumn('discard_elapsed_time');
});
}
}

View File

@ -5,12 +5,12 @@ use Illuminate\Database\Seeder;
class DatabaseSeeder extends Seeder
{
/**
* Run the database seeds.
* Seed the application's database.
*
* @return void
*/
public function run()
{
// $this->call(UsersTableSeeder::class);
$this->call(EjaculationSourcesSeeder::class);
}
}

View File

@ -0,0 +1,19 @@
<?php
use Illuminate\Database\Seeder;
class EjaculationSourcesSeeder extends Seeder
{
/**
* Run the database seeds.
*
* @return void
*/
public function run()
{
$sources = ['web', 'csv', 'webhook'];
foreach ($sources as $source) {
DB::table('ejaculation_sources')->insertOrIgnore(['name' => $source]);
}
}
}

5
dist/bin/php-debug.sh vendored Executable file
View File

@ -0,0 +1,5 @@
#!/bin/bash
set -e
export APP_DEBUG=true
exec tissue-entrypoint.sh php "$@"

Some files were not shown because too many files have changed in this diff Show More