Compare commits
664 Commits
0.1.0
...
feature/sp
Author | SHA1 | Date | |
---|---|---|---|
![]() |
5e9fa05be5 | ||
![]() |
9f565798c0 | ||
![]() |
ef23f1b12e | ||
![]() |
2edabfa38f | ||
![]() |
ad65475037 | ||
![]() |
e429b2c054 | ||
![]() |
cec9ffc5ac | ||
![]() |
07b315e8af | ||
![]() |
1948d5235e | ||
![]() |
78a255c1e3 | ||
![]() |
d9cf5e54e3 | ||
![]() |
e7db04fd55 | ||
![]() |
37a10b7354 | ||
![]() |
170492b39d | ||
![]() |
ea3f2e595f | ||
![]() |
6ea360bc4e | ||
![]() |
251d7b9108 | ||
![]() |
bba945113b | ||
![]() |
695f457505 | ||
![]() |
00345eedca | ||
![]() |
19ef4d2bbe | ||
![]() |
6577a032e1 | ||
![]() |
010a0a9c8f | ||
![]() |
cdc6335a06 | ||
![]() |
03633440a6 | ||
![]() |
67ae0e159f | ||
![]() |
73ee9f108b | ||
![]() |
f132955be7 | ||
![]() |
3420e053fc | ||
![]() |
a01bc6989e | ||
![]() |
a71aa0c3b6 | ||
![]() |
26be8a086e | ||
![]() |
af5de3ee14 | ||
![]() |
c8cee80144 | ||
![]() |
d8e170ff85 | ||
![]() |
9c101dfb7b | ||
![]() |
12fd228e75 | ||
![]() |
dc0eb0a548 | ||
![]() |
16ed4482f4 | ||
![]() |
57a847baf5 | ||
![]() |
332b6d7dd0 | ||
![]() |
99a92c6106 | ||
![]() |
bcb5abb161 | ||
![]() |
4ca6f00c1b | ||
![]() |
7b8811b894 | ||
![]() |
c7e261d06b | ||
![]() |
b3c98613e7 | ||
![]() |
f7a5948e8e | ||
![]() |
d1abca5416 | ||
![]() |
579708389a | ||
![]() |
900e4c94a7 | ||
![]() |
a434a45e4a | ||
![]() |
e6657a0756 | ||
![]() |
4f6bb0ac15 | ||
![]() |
e27a848b08 | ||
![]() |
8772facadf | ||
![]() |
d5ee59825f | ||
![]() |
4192f22af5 | ||
![]() |
dd07940aea | ||
![]() |
78bb7dae28 | ||
![]() |
e4890f65ae | ||
![]() |
2454a24ee2 | ||
![]() |
7858bd0a5f | ||
![]() |
ce8855510c | ||
![]() |
c42a3d2657 | ||
![]() |
7cb6dd4754 | ||
![]() |
bf1c4e7a21 | ||
![]() |
7a56072765 | ||
![]() |
d6e0512dae | ||
![]() |
1edc70fc4c | ||
![]() |
a20f690cdd | ||
![]() |
eab901d56d | ||
![]() |
8c88d60034 | ||
![]() |
599e3f9557 | ||
![]() |
a9a0f3b99a | ||
![]() |
5fc0c6c1b6 | ||
![]() |
5642e73391 | ||
![]() |
92847fefe0 | ||
![]() |
178ed02d00 | ||
![]() |
3381965896 | ||
![]() |
dc8a70291d | ||
![]() |
a10acdd481 | ||
![]() |
2c4eaccf43 | ||
![]() |
141d5ce77c | ||
![]() |
ccade6ff9f | ||
![]() |
033784bfc8 | ||
![]() |
db39ee35c2 | ||
![]() |
fb6c1a0574 | ||
![]() |
59aec2c038 | ||
![]() |
d45898931a | ||
![]() |
fb50881e74 | ||
![]() |
40fedf59d4 | ||
![]() |
b2014a3db7 | ||
![]() |
f3a4f682a8 | ||
![]() |
c0b76e522b | ||
![]() |
0f530099b4 | ||
![]() |
eecace33bd | ||
![]() |
d049a6f631 | ||
![]() |
4add9a87cc | ||
![]() |
c898487a20 | ||
![]() |
03cb2b0728 | ||
![]() |
3c0b65ff8c | ||
![]() |
b367009c5c | ||
![]() |
22150d0e7a | ||
![]() |
2b98267fa8 | ||
![]() |
a7972046ef | ||
![]() |
524d00d0ed | ||
![]() |
06cc18565e | ||
![]() |
ff6de777d7 | ||
![]() |
743272f8d6 | ||
![]() |
784fb43ae9 | ||
![]() |
de23a37ab3 | ||
![]() |
72fc84a42c | ||
![]() |
f59aa750e4 | ||
![]() |
66f4c45f5c | ||
![]() |
c204a7e934 | ||
![]() |
aa87a8f070 | ||
![]() |
ab20ca5370 | ||
![]() |
bd84f29a27 | ||
![]() |
ac2077af49 | ||
![]() |
27970e3ac5 | ||
![]() |
4940b7a9ca | ||
![]() |
5e02a8ab7a | ||
![]() |
a088444626 | ||
![]() |
8ef9a1f8f4 | ||
![]() |
150a8152a4 | ||
![]() |
5ac1bae73f | ||
![]() |
1bec21f15f | ||
![]() |
2c1976fd2b | ||
![]() |
13c3407a4e | ||
![]() |
91e6cea79a | ||
![]() |
ac40a411da | ||
![]() |
e2aa47151b | ||
![]() |
dd38f4e0eb | ||
![]() |
17bc8cebbf | ||
![]() |
4f23a9404b | ||
![]() |
b7eafd881f | ||
![]() |
0a994884a0 | ||
![]() |
9926cc3357 | ||
![]() |
5069f20b50 | ||
![]() |
93387f1ff5 | ||
![]() |
7baf51fc09 | ||
![]() |
0e3878a808 | ||
![]() |
4c0b245574 | ||
![]() |
0a0047c4c3 | ||
![]() |
831d1668ef | ||
![]() |
51c8199283 | ||
![]() |
78a1bdfb30 | ||
![]() |
ceff57f9f6 | ||
![]() |
bc2f8662fc | ||
![]() |
2112087e89 | ||
![]() |
c93ccb43c8 | ||
![]() |
58ae1bc1c1 | ||
![]() |
f7c9e83b12 | ||
![]() |
a1850b666b | ||
![]() |
8594caade1 | ||
![]() |
dddb47f68a | ||
![]() |
1b2b043be2 | ||
![]() |
c535153e1f | ||
![]() |
830de3a5e3 | ||
![]() |
ab46117138 | ||
![]() |
3c2fec21a0 | ||
![]() |
370d1cc01b | ||
![]() |
5517cd5fab | ||
![]() |
3c083a7c60 | ||
![]() |
fa6b8b87af | ||
![]() |
d290bf4107 | ||
![]() |
b274c6bc40 | ||
![]() |
018532f01f | ||
![]() |
e4ef935dd2 | ||
![]() |
dabae9f251 | ||
![]() |
fb84a1d416 | ||
![]() |
358580a15e | ||
![]() |
000b89f380 | ||
![]() |
c5cbad4475 | ||
![]() |
f4abb08921 | ||
![]() |
38eb0348f9 | ||
![]() |
4f5595dae0 | ||
![]() |
733e97bc58 | ||
![]() |
c4768ded38 | ||
![]() |
598d27f6b8 | ||
![]() |
cf5e269fd8 | ||
![]() |
4747f822ab | ||
![]() |
bcf78df2fc | ||
![]() |
fa171bc3d3 | ||
![]() |
b828a233bc | ||
![]() |
bbfd6a9895 | ||
![]() |
e1dd2b1c8f | ||
![]() |
ec4e0f3dda | ||
![]() |
0aed1d9ebe | ||
![]() |
d8cdf218b7 | ||
![]() |
7c9eefe478 | ||
![]() |
810a1dbc59 | ||
![]() |
a521a26aa5 | ||
![]() |
e80acf79b7 | ||
![]() |
1b5fbfabc4 | ||
![]() |
0ad0b268bc | ||
![]() |
da19806a3d | ||
![]() |
4390af53f9 | ||
![]() |
dd12582dba | ||
![]() |
6e81f091d1 | ||
![]() |
c60d41427d | ||
![]() |
b8482e0e3c | ||
![]() |
a1cb313d4f | ||
![]() |
53f7a17b8e | ||
![]() |
41039a6650 | ||
![]() |
6d4f6e47a3 | ||
![]() |
47eec65101 | ||
![]() |
c2da5eef9d | ||
![]() |
94cabdc827 | ||
![]() |
1e11bd3290 | ||
![]() |
f729fa7908 | ||
![]() |
cb1b2c9902 | ||
![]() |
9a47bc970f | ||
![]() |
0c6cee6fcb | ||
![]() |
5172f1cb70 | ||
![]() |
663888dcb1 | ||
![]() |
762c232aef | ||
![]() |
e2332c7fe6 | ||
![]() |
16e5341de1 | ||
![]() |
8c5a7f4d09 | ||
![]() |
fe09f769e3 | ||
![]() |
85012e13de | ||
![]() |
f19f970bd9 | ||
![]() |
d6127a7268 | ||
![]() |
073ed7e618 | ||
![]() |
32c1d3ff9d | ||
![]() |
ede45ee4e1 | ||
![]() |
fbecd97c03 | ||
![]() |
9306a4376c | ||
![]() |
c061a51f8f | ||
![]() |
b510ea4042 | ||
![]() |
e004a6bced | ||
![]() |
b6864c6fc4 | ||
![]() |
a45d5cd558 | ||
![]() |
5f1a6291f7 | ||
![]() |
ac91e36246 | ||
![]() |
709f10f098 | ||
![]() |
d1da60693a | ||
![]() |
b8f7e5e6ef | ||
![]() |
d926a9ea0d | ||
![]() |
65b0893f47 | ||
![]() |
b2301444c4 | ||
![]() |
e5e0ce08da | ||
![]() |
17ce2784a7 | ||
![]() |
d470193662 | ||
![]() |
ea93cc68fa | ||
![]() |
12dac5916e | ||
![]() |
f47d3454f6 | ||
![]() |
34cb3a1415 | ||
![]() |
9471683741 | ||
![]() |
c7d88076fa | ||
![]() |
0670cb8736 | ||
![]() |
7a95e0979e | ||
![]() |
333f39c9f4 | ||
![]() |
5340d8ead9 | ||
![]() |
348096ecee | ||
![]() |
35cc0c6357 | ||
![]() |
f0cb07c6f5 | ||
![]() |
3f9de593d6 | ||
![]() |
11ddb83424 | ||
![]() |
02367646a1 | ||
![]() |
241bcd6548 | ||
![]() |
f7c8a3010d | ||
![]() |
c6a32da97e | ||
![]() |
50ff2efaaa | ||
![]() |
a958ccaa08 | ||
![]() |
ce3ef3cdff | ||
![]() |
f9ea9cc566 | ||
![]() |
debc350b12 | ||
![]() |
d7f39fcc5a | ||
![]() |
8e6b96fb83 | ||
![]() |
c20bd0ca77 | ||
![]() |
c1330ff6e3 | ||
![]() |
085afd3318 | ||
![]() |
a8a6aecef3 | ||
![]() |
e4c942263a | ||
![]() |
1322e89b86 | ||
![]() |
dbafe2c75e | ||
![]() |
2efb387b6a | ||
![]() |
91c786199a | ||
![]() |
ed295cb7bc | ||
![]() |
8de5fa891e | ||
![]() |
464b690fbd | ||
![]() |
d02b65e4ed | ||
![]() |
0c18965ade | ||
![]() |
51485a8ac1 | ||
![]() |
59a6aa869f | ||
![]() |
b3ea665f0b | ||
![]() |
f791fd8fbd | ||
![]() |
676793cfe5 | ||
![]() |
2ff07cc68d | ||
![]() |
f95f1592f7 | ||
![]() |
d655f24fbf | ||
![]() |
b80d74bae1 | ||
![]() |
9b95f3a8b8 | ||
![]() |
859a186acb | ||
![]() |
fca6b6e98b | ||
![]() |
26b52e1c87 | ||
![]() |
f895996b18 | ||
![]() |
f19621c04a | ||
![]() |
e8cfc48417 | ||
![]() |
566d288395 | ||
![]() |
ddac533539 | ||
![]() |
c20a8066c9 | ||
![]() |
664448b9fc | ||
![]() |
e9c1726567 | ||
![]() |
db7dce5830 | ||
![]() |
4ed2a9048d | ||
![]() |
2cd09402d1 | ||
![]() |
6b1ccc52e5 | ||
![]() |
3cba46bff0 | ||
![]() |
9cec184d21 | ||
![]() |
1b4f621191 | ||
![]() |
5bafe9126a | ||
![]() |
fe880ee599 | ||
![]() |
a15716bb54 | ||
![]() |
02f03165d6 | ||
![]() |
225d0854ef | ||
![]() |
34b7cd6c89 | ||
![]() |
199e5dac05 | ||
![]() |
062034fb83 | ||
![]() |
212fad4d66 | ||
![]() |
04d7116eca | ||
![]() |
cc8ee2e520 | ||
![]() |
04f1a344a0 | ||
![]() |
379d4563c5 | ||
![]() |
ca2aeea4f5 | ||
![]() |
5153de54d2 | ||
![]() |
0e45d27295 | ||
![]() |
8e161252a7 | ||
![]() |
a7859fdda6 | ||
![]() |
34df704fdb | ||
![]() |
80fe53cf20 | ||
![]() |
b8ceac51f7 | ||
![]() |
14b57bf9f5 | ||
![]() |
40fdb587d7 | ||
![]() |
1fd43b5e38 | ||
![]() |
a998d7132f | ||
![]() |
74c8a1b6cb | ||
![]() |
e6b333eea4 | ||
![]() |
285e529aea | ||
![]() |
a72190eda5 | ||
![]() |
ea2db88de3 | ||
![]() |
54477bb214 | ||
![]() |
176ccef20f | ||
![]() |
dd4837ef7b | ||
![]() |
9e786a5469 | ||
![]() |
df3826a6d4 | ||
![]() |
a35b58eb47 | ||
![]() |
27fc5ee6e8 | ||
![]() |
be700ab81b | ||
![]() |
53ac4c9b8f | ||
![]() |
e69adbfbc3 | ||
![]() |
f5fab4b3c1 | ||
![]() |
82ccd623a6 | ||
![]() |
e98ed0c3ca | ||
![]() |
2dd5cbd072 | ||
![]() |
d044b6db20 | ||
![]() |
8e366870b1 | ||
![]() |
54eec1a861 | ||
![]() |
8a39feff29 | ||
![]() |
8a8ca1a26e | ||
![]() |
e262b27d0d | ||
![]() |
bd1976c4cf | ||
![]() |
3fa2d80507 | ||
![]() |
ec5a78db38 | ||
![]() |
2db45951f4 | ||
![]() |
00a819de23 | ||
![]() |
4f559cd1d4 | ||
![]() |
16005931fc | ||
![]() |
a58811a8fe | ||
![]() |
89c0b39755 | ||
![]() |
1fb4352e48 | ||
![]() |
ad747577e4 | ||
![]() |
a2ee4ef505 | ||
![]() |
5d1ffca1ee | ||
![]() |
5c612d7eef | ||
![]() |
cf0d370e61 | ||
![]() |
91ec1c391e | ||
![]() |
83d0fb3a7a | ||
![]() |
b8f7b5dfe0 | ||
![]() |
51fc65e66f | ||
![]() |
a674195db9 | ||
![]() |
1a7b6c8f3c | ||
![]() |
077731495c | ||
![]() |
7190367936 | ||
![]() |
e1eb359887 | ||
![]() |
57b10d98ac | ||
![]() |
7c70e6db7e | ||
![]() |
b430ba7162 | ||
![]() |
d9d6c10a34 | ||
![]() |
1a7d958a1e | ||
![]() |
e983a3da0b | ||
![]() |
ed1cfe94f0 | ||
![]() |
cd56e1bff3 | ||
![]() |
5db60a4524 | ||
![]() |
3f0171fa8b | ||
![]() |
61498133d5 | ||
![]() |
088901fec7 | ||
![]() |
dab1732a1d | ||
![]() |
0b87a35fba | ||
![]() |
5561f0785c | ||
![]() |
1ba7df6e82 | ||
![]() |
94c19235b6 | ||
![]() |
e8438a78a1 | ||
![]() |
da0fc3f3bf | ||
![]() |
d561ee66c2 | ||
![]() |
d571ff1a5b | ||
![]() |
a2f0beb3cb | ||
![]() |
41778844b8 | ||
![]() |
b29bb23b40 | ||
![]() |
a364de7d03 | ||
![]() |
03fcc424d8 | ||
![]() |
ddd2a05607 | ||
![]() |
f25312a987 | ||
![]() |
f7d9c5c1e6 | ||
![]() |
2e1ec0dfc7 | ||
![]() |
f1c56bce83 | ||
![]() |
0ceb0fcf21 | ||
![]() |
37eaefc016 | ||
![]() |
db09bf40a6 | ||
![]() |
1348054858 | ||
![]() |
2c396da84e | ||
![]() |
d4d98db686 | ||
![]() |
81e37034ce | ||
![]() |
8cdf086296 | ||
![]() |
7f087bf446 | ||
![]() |
6a1848e311 | ||
![]() |
70f007b6d2 | ||
![]() |
b7701f39dd | ||
![]() |
7a421d648d | ||
![]() |
8852c6b45b | ||
![]() |
cf5bf2b274 | ||
![]() |
e925a964f1 | ||
![]() |
e966995dea | ||
![]() |
f21e58650d | ||
![]() |
70fed1ddf0 | ||
![]() |
c229947080 | ||
![]() |
08432c847e | ||
![]() |
b961956b63 | ||
![]() |
023857f7d7 | ||
![]() |
5276e8d452 | ||
![]() |
3cba54946a | ||
![]() |
e6fe522f6d | ||
![]() |
7eeb0207de | ||
![]() |
1beb411050 | ||
![]() |
eb97f01c79 | ||
![]() |
0ff253cbfb | ||
![]() |
579bd3e31f | ||
![]() |
c80bb69568 | ||
![]() |
3c6dfeec1c | ||
![]() |
30c861d18c | ||
![]() |
746db0474a | ||
![]() |
307e578d4a | ||
![]() |
be6151f0c7 | ||
![]() |
c9cdd26728 | ||
![]() |
d0dd2db159 | ||
![]() |
ea12f8c9a6 | ||
![]() |
dc98334a6d | ||
![]() |
a14b49b7d2 | ||
![]() |
de740082b7 | ||
![]() |
af964ad82f | ||
![]() |
32b0b76032 | ||
![]() |
db3ba04091 | ||
![]() |
0400bc771c | ||
![]() |
a5b4eeee36 | ||
![]() |
f760ea7093 | ||
![]() |
0f4dfcd816 | ||
![]() |
a934a7fc35 | ||
![]() |
51f097fdf0 | ||
![]() |
24dee801ad | ||
![]() |
9f1cd607d7 | ||
![]() |
4196b1a02d | ||
![]() |
35789befc5 | ||
![]() |
32139cb9da | ||
![]() |
9244b8424d | ||
![]() |
55eb95dda8 | ||
![]() |
72e9d4e3e8 | ||
![]() |
852f1ac88c | ||
![]() |
f09ae32b00 | ||
![]() |
33be0ac8ef | ||
![]() |
9f2e73e511 | ||
![]() |
e36b9c7c1b | ||
![]() |
09bb98876c | ||
![]() |
cedee0a20e | ||
![]() |
116dd3b798 | ||
![]() |
735bb00eba | ||
![]() |
decb1707f1 | ||
![]() |
bec7bdeb36 | ||
![]() |
7f5a4a06d9 | ||
![]() |
d7c7f86ba5 | ||
![]() |
ca212b547a | ||
![]() |
3584625b47 | ||
![]() |
1ba4999a83 | ||
![]() |
03e1c2d60c | ||
![]() |
ab0695ee8d | ||
![]() |
fcafc3c704 | ||
![]() |
7a606be3ba | ||
![]() |
b4a7ec64dd | ||
![]() |
5750eeb3a5 | ||
![]() |
b4dc07a9a3 | ||
![]() |
a77ac3f039 | ||
![]() |
eef0eac887 | ||
![]() |
7337f60491 | ||
![]() |
85e9599654 | ||
![]() |
8a919ca62a | ||
![]() |
d105568c76 | ||
![]() |
1f7723614d | ||
![]() |
41e810c788 | ||
![]() |
82af423c57 | ||
![]() |
4346e1a701 | ||
![]() |
96199c9e46 | ||
![]() |
4962244969 | ||
![]() |
c417dabff2 | ||
![]() |
d9bf673d85 | ||
![]() |
5961d3e27a | ||
![]() |
b57a272611 | ||
![]() |
cbbb2605dd | ||
![]() |
e20bb75e00 | ||
![]() |
e226f43265 | ||
![]() |
e887f2d83e | ||
![]() |
1835776a9c | ||
![]() |
57715b9a82 | ||
![]() |
e320f85c73 | ||
![]() |
f5f4cbb5b6 | ||
![]() |
4ab82ff0e2 | ||
![]() |
8c73bda2ac | ||
![]() |
3c6f802b69 | ||
![]() |
895e9f4b15 | ||
![]() |
648e171a57 | ||
![]() |
dc91180dd4 | ||
![]() |
bbbffcb39e | ||
![]() |
56831c78c3 | ||
![]() |
a30919991c | ||
![]() |
8aa2e6a779 | ||
![]() |
34f45d1ce8 | ||
![]() |
6d66425fc9 | ||
![]() |
626c85c07d | ||
![]() |
b6bf1f99d8 | ||
![]() |
907eb87723 | ||
![]() |
72ec5d8d26 | ||
![]() |
ca5be696c8 | ||
![]() |
48ddac8c85 | ||
![]() |
a2580f29cc | ||
![]() |
85cc865545 | ||
![]() |
5d256519c6 | ||
![]() |
53b459740f | ||
![]() |
550d897561 | ||
![]() |
27532685ba | ||
![]() |
4654962aac | ||
![]() |
ef563f8641 | ||
![]() |
7745b68dae | ||
![]() |
e5ea0528a8 | ||
![]() |
4e1eec66be | ||
![]() |
a33a0e542c | ||
![]() |
473280d9d2 | ||
![]() |
73c697f119 | ||
![]() |
09482ca2c5 | ||
![]() |
3a1dc72cf7 | ||
![]() |
b04f167709 | ||
![]() |
a3b328b55f | ||
![]() |
497c19d06d | ||
![]() |
ade52f40f4 | ||
![]() |
c38d3fa799 | ||
![]() |
2a91aac569 | ||
![]() |
f367ec212f | ||
![]() |
735c7c289a | ||
![]() |
cffe539832 | ||
![]() |
2191a96cac | ||
![]() |
64f8b47ae0 | ||
![]() |
2ca6c4c60d | ||
![]() |
0d4a61ef15 | ||
![]() |
cef23a64cb | ||
![]() |
cd26ef6236 | ||
![]() |
810eea2a59 | ||
![]() |
acb9b5821d | ||
![]() |
5f01cc3430 | ||
![]() |
938a4d6957 | ||
![]() |
a3813f19cf | ||
![]() |
faf0755ebd | ||
![]() |
a2f797cbbe | ||
![]() |
8c6cc0692c | ||
![]() |
dcf31865a1 | ||
![]() |
3dedb57fe4 | ||
![]() |
f134cbefa8 | ||
![]() |
72ab8bf101 | ||
![]() |
11836ddd43 | ||
![]() |
5c6417cdbe | ||
![]() |
0e410ef342 | ||
![]() |
d6e981ac39 | ||
![]() |
98e933b833 | ||
![]() |
6ff247acd7 | ||
![]() |
2d04ed8dd7 | ||
![]() |
d359a41033 | ||
![]() |
2299ac3fe7 | ||
![]() |
fcdb9d7aba | ||
![]() |
e6abcc4402 | ||
![]() |
b0a7504691 | ||
![]() |
a645cb497f | ||
![]() |
6105f6c860 | ||
![]() |
9eb42f1991 | ||
![]() |
dfea7f2f24 | ||
![]() |
7441c26694 | ||
![]() |
630be41833 | ||
![]() |
c4a69cccbe | ||
![]() |
cb3f060ba6 | ||
![]() |
4d7b70f9ad | ||
![]() |
3bb6bf718e | ||
![]() |
fc709a6624 | ||
![]() |
8d5363e978 | ||
![]() |
71137e9ab4 | ||
![]() |
f7a95befbe | ||
![]() |
908790b53d | ||
![]() |
20799dd757 | ||
![]() |
ca02f21812 | ||
![]() |
168ef1c5f6 | ||
![]() |
3c2e475e41 | ||
![]() |
581c1ed952 | ||
![]() |
ad5fbc7ada | ||
![]() |
cf1757319e | ||
![]() |
b39b43e705 | ||
![]() |
bcc9f3acda | ||
![]() |
911957b283 | ||
![]() |
b9e29cc283 | ||
![]() |
316b453cca | ||
![]() |
e6b28993de | ||
![]() |
e875d5da02 | ||
![]() |
233a54eb3e | ||
![]() |
515e24c4e4 | ||
![]() |
af4b60d6e1 | ||
![]() |
5bb44ab232 | ||
![]() |
874e88cc56 | ||
![]() |
bdb1640ceb | ||
![]() |
c52046d51e | ||
![]() |
69f212d705 | ||
![]() |
2441fe78b6 | ||
![]() |
9646f90ce3 | ||
![]() |
9bd22d9f77 | ||
![]() |
a0e4063c47 | ||
![]() |
0882063c0b | ||
![]() |
b4e40ab748 | ||
![]() |
6609965360 | ||
![]() |
9705c2ce5a | ||
![]() |
bd93d9ec24 | ||
![]() |
55bd35ea49 | ||
![]() |
4dc4efe10d | ||
![]() |
dfe149e969 | ||
![]() |
f51aaea94c | ||
![]() |
1dea0a077c | ||
![]() |
d143dc4d84 | ||
![]() |
e033816eab | ||
![]() |
503e8ba093 | ||
![]() |
d224e6bba4 | ||
![]() |
3b2e81818b | ||
![]() |
7ca0acacb4 | ||
![]() |
88456bc609 | ||
![]() |
0f39b502e8 | ||
![]() |
46f049c2b8 | ||
![]() |
9cdfadf12c | ||
![]() |
bc57a482be | ||
![]() |
ba53156beb | ||
![]() |
5b2427a2c9 | ||
![]() |
277ee90379 |
166
.circleci/config.yml
Normal file
166
.circleci/config.yml
Normal file
@@ -0,0 +1,166 @@
|
|||||||
|
version: 2.1
|
||||||
|
|
||||||
|
executors:
|
||||||
|
build:
|
||||||
|
docker:
|
||||||
|
- image: circleci/php:7.3-node-browsers
|
||||||
|
environment:
|
||||||
|
APP_DEBUG: true
|
||||||
|
APP_ENV: testing
|
||||||
|
APP_KEY: base64:f2tcw34GKT8EOtb5myZxJ8QLdgNivmyPhoQIPY2YfK8=
|
||||||
|
DB_CONNECTION: pgsql
|
||||||
|
DB_DATABASE: tissue
|
||||||
|
DB_USERNAME: tissue
|
||||||
|
DB_PASSWORD: tissue
|
||||||
|
- image: circleci/postgres:10-alpine
|
||||||
|
environment:
|
||||||
|
POSTGRES_DB: tissue
|
||||||
|
POSTGRES_USER: tissue
|
||||||
|
POSTGRES_PASSWORD: tissue
|
||||||
|
|
||||||
|
commands:
|
||||||
|
initialize:
|
||||||
|
steps:
|
||||||
|
- checkout
|
||||||
|
- run: sudo apt update
|
||||||
|
- run: sudo apt install -y libpq-dev
|
||||||
|
- run: sudo docker-php-ext-install zip
|
||||||
|
- run: sudo docker-php-ext-install pdo_pgsql
|
||||||
|
restore_composer:
|
||||||
|
steps:
|
||||||
|
- restore_cache:
|
||||||
|
keys:
|
||||||
|
- v1-dependencies-{{ checksum "composer.json" }}
|
||||||
|
- v1-dependencies-
|
||||||
|
save_composer:
|
||||||
|
steps:
|
||||||
|
- save_cache:
|
||||||
|
key: v1-dependencies-{{ checksum "composer.json" }}
|
||||||
|
paths:
|
||||||
|
- ./vendor
|
||||||
|
restore_npm:
|
||||||
|
steps:
|
||||||
|
- restore_cache:
|
||||||
|
keys:
|
||||||
|
- v1-dependencies-{{ checksum "package.json" }}
|
||||||
|
- v1-dependencies-
|
||||||
|
save_npm:
|
||||||
|
steps:
|
||||||
|
- save_cache:
|
||||||
|
key: v1-dependencies-{{ checksum "package.json" }}
|
||||||
|
paths:
|
||||||
|
- ./node_modules
|
||||||
|
- ~/.yarn
|
||||||
|
|
||||||
|
jobs:
|
||||||
|
build:
|
||||||
|
executor: build
|
||||||
|
steps:
|
||||||
|
- initialize
|
||||||
|
|
||||||
|
- restore_composer
|
||||||
|
- run: composer install -n --prefer-dist
|
||||||
|
- save_composer
|
||||||
|
|
||||||
|
- restore_npm
|
||||||
|
- run: yarn install
|
||||||
|
- save_npm
|
||||||
|
|
||||||
|
- run: yarn run prod
|
||||||
|
|
||||||
|
- persist_to_workspace:
|
||||||
|
root: .
|
||||||
|
paths:
|
||||||
|
- public
|
||||||
|
|
||||||
|
test:
|
||||||
|
executor: build
|
||||||
|
steps:
|
||||||
|
- initialize
|
||||||
|
|
||||||
|
- restore_composer
|
||||||
|
- restore_npm
|
||||||
|
|
||||||
|
- attach_workspace:
|
||||||
|
at: .
|
||||||
|
|
||||||
|
- run: php artisan migrate
|
||||||
|
|
||||||
|
# Run linter
|
||||||
|
- run:
|
||||||
|
command: |
|
||||||
|
mkdir -p /tmp/php-cs-fixer
|
||||||
|
./vendor/bin/php-cs-fixer fix --dry-run --diff --format=junit > /tmp/php-cs-fixer/php-cs-fixer.xml
|
||||||
|
when: always
|
||||||
|
- store_test_results:
|
||||||
|
path: /tmp/php-cs-fixer
|
||||||
|
|
||||||
|
# Run stylelint
|
||||||
|
- run:
|
||||||
|
name: stylelint
|
||||||
|
command: yarn run stylelint
|
||||||
|
when: always
|
||||||
|
|
||||||
|
# Run unit test
|
||||||
|
- run:
|
||||||
|
command: |
|
||||||
|
mkdir -p /tmp/phpunit
|
||||||
|
./vendor/bin/phpunit --log-junit /tmp/phpunit/phpunit.xml --coverage-clover=/tmp/phpunit/coverage.xml
|
||||||
|
when: always
|
||||||
|
- store_test_results:
|
||||||
|
path: /tmp/phpunit
|
||||||
|
- store_artifacts:
|
||||||
|
path: /tmp/phpunit/coverage.xml
|
||||||
|
|
||||||
|
# Upload coverage
|
||||||
|
- run:
|
||||||
|
command: bash <(curl -s https://codecov.io/bash) -f /tmp/phpunit/coverage.xml
|
||||||
|
when: always
|
||||||
|
|
||||||
|
test_resolver:
|
||||||
|
executor: build
|
||||||
|
environment:
|
||||||
|
TEST_USE_HTTP_MOCK: false
|
||||||
|
steps:
|
||||||
|
- initialize
|
||||||
|
|
||||||
|
- restore_composer
|
||||||
|
|
||||||
|
- attach_workspace:
|
||||||
|
at: .
|
||||||
|
|
||||||
|
- run: php artisan migrate
|
||||||
|
|
||||||
|
# Run unit test
|
||||||
|
- run:
|
||||||
|
command: |
|
||||||
|
mkdir -p /tmp/phpunit
|
||||||
|
./vendor/bin/phpunit --testsuite MetadataResolver --log-junit /tmp/phpunit/phpunit.xml --coverage-clover=/tmp/phpunit/coverage.xml
|
||||||
|
when: always
|
||||||
|
- store_test_results:
|
||||||
|
path: /tmp/phpunit
|
||||||
|
- store_artifacts:
|
||||||
|
path: /tmp/phpunit/coverage.xml
|
||||||
|
|
||||||
|
workflows:
|
||||||
|
version: 2.1
|
||||||
|
test:
|
||||||
|
jobs:
|
||||||
|
- build
|
||||||
|
- test:
|
||||||
|
requires:
|
||||||
|
- build
|
||||||
|
scheduled_resolver_test:
|
||||||
|
triggers:
|
||||||
|
- schedule:
|
||||||
|
cron: "4 0 * * 1"
|
||||||
|
filters:
|
||||||
|
branches:
|
||||||
|
only:
|
||||||
|
- develop
|
||||||
|
jobs:
|
||||||
|
- build
|
||||||
|
- test_resolver:
|
||||||
|
requires:
|
||||||
|
- build
|
||||||
|
|
4
.dockerignore
Normal file
4
.dockerignore
Normal file
@@ -0,0 +1,4 @@
|
|||||||
|
.idea
|
||||||
|
.git
|
||||||
|
.gitignore
|
||||||
|
.gitattributes
|
21
.editorconfig
Normal file
21
.editorconfig
Normal file
@@ -0,0 +1,21 @@
|
|||||||
|
root = true
|
||||||
|
|
||||||
|
[*]
|
||||||
|
indent_style = space
|
||||||
|
indent_size = 4
|
||||||
|
charset = utf-8
|
||||||
|
end_of_line = lf
|
||||||
|
trim_trailing_whitespace = true
|
||||||
|
insert_final_newline = true
|
||||||
|
|
||||||
|
[*.md]
|
||||||
|
trim_trailing_whitespace = false
|
||||||
|
|
||||||
|
[*.yml]
|
||||||
|
indent_size = 2
|
||||||
|
|
||||||
|
[*.json]
|
||||||
|
indent_size = 2
|
||||||
|
|
||||||
|
[composer.json]
|
||||||
|
indent_size = 4
|
20
.env.example
20
.env.example
@@ -5,12 +5,15 @@ APP_DEBUG=true
|
|||||||
APP_LOG_LEVEL=debug
|
APP_LOG_LEVEL=debug
|
||||||
APP_URL=http://localhost
|
APP_URL=http://localhost
|
||||||
|
|
||||||
DB_CONNECTION=mysql
|
# テストにモックを使用するか falseの場合は実際のHTML等を取得してテストする
|
||||||
DB_HOST=127.0.0.1
|
TEST_USE_HTTP_MOCK=true
|
||||||
DB_PORT=3306
|
|
||||||
DB_DATABASE=homestead
|
DB_CONNECTION=pgsql
|
||||||
DB_USERNAME=homestead
|
DB_HOST=db
|
||||||
DB_PASSWORD=secret
|
DB_PORT=5432
|
||||||
|
DB_DATABASE=tissue
|
||||||
|
DB_USERNAME=tissue
|
||||||
|
DB_PASSWORD=tissue
|
||||||
|
|
||||||
BROADCAST_DRIVER=log
|
BROADCAST_DRIVER=log
|
||||||
CACHE_DRIVER=file
|
CACHE_DRIVER=file
|
||||||
@@ -35,3 +38,8 @@ SPARKPOST_SECRET=
|
|||||||
PUSHER_APP_ID=
|
PUSHER_APP_ID=
|
||||||
PUSHER_APP_KEY=
|
PUSHER_APP_KEY=
|
||||||
PUSHER_APP_SECRET=
|
PUSHER_APP_SECRET=
|
||||||
|
|
||||||
|
# (Optional) reCAPTCHA Key
|
||||||
|
# https://www.google.com/recaptcha
|
||||||
|
NOCAPTCHA_SECRET=
|
||||||
|
NOCAPTCHA_SITEKEY=
|
||||||
|
1
.gitattributes
vendored
1
.gitattributes
vendored
@@ -3,3 +3,4 @@
|
|||||||
*.scss linguist-vendored
|
*.scss linguist-vendored
|
||||||
*.js linguist-vendored
|
*.js linguist-vendored
|
||||||
CHANGELOG.md export-ignore
|
CHANGELOG.md export-ignore
|
||||||
|
*.sh text eol=lf
|
11
.gitignore
vendored
11
.gitignore
vendored
@@ -1,13 +1,22 @@
|
|||||||
/node_modules
|
/node_modules
|
||||||
|
/public/css
|
||||||
|
/public/fonts
|
||||||
|
/public/js
|
||||||
/public/hot
|
/public/hot
|
||||||
/public/storage
|
/public/storage
|
||||||
|
/public/mix-manifest.json
|
||||||
/storage/*.key
|
/storage/*.key
|
||||||
/vendor
|
/vendor
|
||||||
/.idea
|
/.idea
|
||||||
|
/.vscode
|
||||||
/.vagrant
|
/.vagrant
|
||||||
Homestead.json
|
Homestead.json
|
||||||
Homestead.yaml
|
Homestead.yaml
|
||||||
npm-debug.log
|
npm-debug.log
|
||||||
yarn-error.log
|
yarn-error.log
|
||||||
.env
|
.env
|
||||||
*.iml
|
*.iml
|
||||||
|
.php_cs
|
||||||
|
.php_cs.cache
|
||||||
|
.phpstorm.meta.php
|
||||||
|
_ide_helper*.php
|
||||||
|
27
.php_cs.dist
Normal file
27
.php_cs.dist
Normal file
@@ -0,0 +1,27 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
return \PhpCsFixer\Config::create()
|
||||||
|
->setRules([
|
||||||
|
'@PSR2' => true,
|
||||||
|
'array_syntax' => [
|
||||||
|
'syntax' => 'short'
|
||||||
|
],
|
||||||
|
'blank_line_before_return' => true,
|
||||||
|
'function_typehint_space' => true,
|
||||||
|
'method_separation' => true,
|
||||||
|
'ordered_imports' => true,
|
||||||
|
'return_type_declaration' => true,
|
||||||
|
'new_with_braces' => true,
|
||||||
|
'no_empty_statement' => true,
|
||||||
|
'standardize_not_equals' => true,
|
||||||
|
'single_quote' => true
|
||||||
|
])
|
||||||
|
->setFinder(
|
||||||
|
\PhpCsFixer\Finder::create()
|
||||||
|
->exclude('bootstrap/cache')
|
||||||
|
->exclude('resources/views')
|
||||||
|
->exclude('storage')
|
||||||
|
->exclude('vendor')
|
||||||
|
->exclude('node_modules')
|
||||||
|
->in(__DIR__)
|
||||||
|
);
|
1
.stylelintignore
Normal file
1
.stylelintignore
Normal file
@@ -0,0 +1 @@
|
|||||||
|
/tests/fixture/*
|
34
Dockerfile
Normal file
34
Dockerfile
Normal file
@@ -0,0 +1,34 @@
|
|||||||
|
FROM node:10-jessie as node
|
||||||
|
|
||||||
|
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 \
|
||||||
|
&& 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
|
||||||
|
|
||||||
|
COPY dist/bin /usr/local/bin/
|
||||||
|
COPY dist/php.d /usr/local/etc/php/php.d/
|
||||||
|
|
||||||
|
COPY --from=node /usr/local/bin/node /usr/local/bin/
|
||||||
|
COPY --from=node /usr/local/lib/node_modules /usr/local/lib/node_modules
|
||||||
|
COPY --from=node /opt/yarn-* /opt/yarn
|
||||||
|
|
||||||
|
RUN ln -s /opt/yarn/bin/yarn /usr/local/bin/yarn \
|
||||||
|
&& ln -s ../lib/node_modules/npm/bin/npm-cli.js /usr/local/bin/npm \
|
||||||
|
&& ln -s ../lib/node_modules/npm/bin/npx-cli.js /usr/local/bin/npx
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
ENTRYPOINT ["tissue-entrypoint.sh"]
|
||||||
|
CMD ["apache2-foreground"]
|
||||||
|
|
||||||
|
WORKDIR /var/www/html
|
100
README.md
100
README.md
@@ -1,19 +1,101 @@
|
|||||||
Tissue
|
# Tissue
|
||||||
====
|
|
||||||
|
|
||||||
a.k.a. shikorism.net
|
a.k.a. shikorism.net
|
||||||
|
|
||||||
シコリズムネットにて提供している夜のライフログサービスです。
|
シコリズムネットにて提供している夜のライフログサービスです。
|
||||||
(思想的には [shibafu528/SperMaster](https://github.com/shibafu528/SperMaster) の後継となります)
|
(思想的には [shibafu528/SperMaster](https://github.com/shibafu528/SperMaster) の後継となります)
|
||||||
|
|
||||||
## 構成
|
## 構成
|
||||||
* Laravel 5.5
|
|
||||||
* Bootstrap 4.0
|
- Laravel 5.5
|
||||||
|
- Bootstrap 4.3.1
|
||||||
|
|
||||||
## 実行環境
|
## 実行環境
|
||||||
* PHP 7.1
|
|
||||||
* PostgreSQL 9.6
|
- PHP 7.3
|
||||||
|
- PostgreSQL 9.6
|
||||||
|
|
||||||
|
## 開発環境の構築
|
||||||
|
|
||||||
|
Docker を用いた開発環境の構築方法です。
|
||||||
|
|
||||||
|
1. `.env` ファイルを用意します。`.env.example` をコピーすることで用意ができます。
|
||||||
|
|
||||||
|
2. Docker イメージをビルドします
|
||||||
|
|
||||||
|
```
|
||||||
|
docker-compose build
|
||||||
|
```
|
||||||
|
|
||||||
|
3. Docker コンテナを起動します。
|
||||||
|
|
||||||
|
```
|
||||||
|
docker-compose up -d
|
||||||
|
```
|
||||||
|
|
||||||
|
4. Composer と yarn を使い必要なライブラリをインストールします。
|
||||||
|
|
||||||
|
```
|
||||||
|
docker-compose exec web composer install
|
||||||
|
docker-compose exec web yarn install
|
||||||
|
```
|
||||||
|
|
||||||
|
5. 暗号化キーの作成と、データベースのマイグレーションを行います。
|
||||||
|
|
||||||
|
```
|
||||||
|
docker-compose exec web php artisan key:generate
|
||||||
|
docker-compose exec web php artisan migrate
|
||||||
|
```
|
||||||
|
|
||||||
|
6. ファイルに書き込めるように権限を設定します。
|
||||||
|
|
||||||
|
```
|
||||||
|
docker-compose exec web chown -R www-data /var/www/html/storage
|
||||||
|
```
|
||||||
|
|
||||||
|
7. アセットをビルドします。
|
||||||
|
|
||||||
|
```
|
||||||
|
docker-compose exec web yarn dev
|
||||||
|
```
|
||||||
|
|
||||||
|
|
||||||
|
8. 最後に `.env` を読み込み直すために起動し直します。
|
||||||
|
|
||||||
|
```
|
||||||
|
docker-compose up -d
|
||||||
|
```
|
||||||
|
|
||||||
|
これで準備は完了です。Tissue が動いていれば `http://localhost:4545/` でアクセスができます。
|
||||||
|
|
||||||
|
## デバッグ実行
|
||||||
|
|
||||||
|
```
|
||||||
|
docker-compose -f docker-compose.yml -f docker-compose.debug.yml up -d
|
||||||
|
```
|
||||||
|
|
||||||
|
で起動することにより、DB のポート`5432`を開放してホストマシンから接続できるようになります。
|
||||||
|
|
||||||
|
## アセットのリアルタイムビルド
|
||||||
|
`yarn watch`を使うとソースファイルを監視して差分があると差分ビルドしてくれます。フロント開発時は活用しましょう。
|
||||||
|
```
|
||||||
|
docker-compose run --rm web yarn watch
|
||||||
|
```
|
||||||
|
|
||||||
|
もしファイル変更時に更新されない場合は`yarn watch-poll`を試してみてください。
|
||||||
|
現在Docker環境でのHMRはサポートしてません。Docker外ならおそらく動くでしょう。
|
||||||
|
その他詳しくはlaravel-mixのドキュメントなどを当たってください。
|
||||||
|
|
||||||
|
## phpunit によるテスト
|
||||||
|
|
||||||
|
変更をしたらPull Requestを投げる前にテストが通ることを確認してください。
|
||||||
|
テストは以下のコマンドで実行できます。
|
||||||
|
|
||||||
|
```
|
||||||
|
docker-compose exec web composer test
|
||||||
|
```
|
||||||
|
|
||||||
## 環境構築上の諸注意
|
## 環境構築上の諸注意
|
||||||
* 初版時点では、DBサーバとしてPostgreSQLを使うよう .env ファイルを設定するくらいです。
|
|
||||||
当分、PostgreSQLから変える気はないので専用SQL等を平気で使います。
|
- 初版時点では、DB サーバとして PostgreSQL を使うよう .env ファイルを設定するくらいです。
|
||||||
|
当分、PostgreSQL から変える気はないので専用 SQL 等を平気で使います。
|
||||||
|
61
app/Console/Commands/DemoteUser.php
Normal file
61
app/Console/Commands/DemoteUser.php
Normal file
@@ -0,0 +1,61 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
namespace App\Console\Commands;
|
||||||
|
|
||||||
|
use App\User;
|
||||||
|
use Illuminate\Console\Command;
|
||||||
|
|
||||||
|
class DemoteUser extends Command
|
||||||
|
{
|
||||||
|
/**
|
||||||
|
* The name and signature of the console command.
|
||||||
|
*
|
||||||
|
* @var string
|
||||||
|
*/
|
||||||
|
protected $signature = 'tissue:user:demote {username}';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The console command description.
|
||||||
|
*
|
||||||
|
* @var string
|
||||||
|
*/
|
||||||
|
protected $description = 'Demote admin to user';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Create a new command instance.
|
||||||
|
*
|
||||||
|
* @return void
|
||||||
|
*/
|
||||||
|
public function __construct()
|
||||||
|
{
|
||||||
|
parent::__construct();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Execute the console command.
|
||||||
|
*
|
||||||
|
* @return mixed
|
||||||
|
*/
|
||||||
|
public function handle()
|
||||||
|
{
|
||||||
|
$user = User::where('name', $this->argument('username'))->first();
|
||||||
|
if ($user === null) {
|
||||||
|
$this->error('No user with such username');
|
||||||
|
|
||||||
|
return 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!$user->is_admin) {
|
||||||
|
$this->info('@' . $user->name . ' is already an user.');
|
||||||
|
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
$user->is_admin = false;
|
||||||
|
if ($user->save()) {
|
||||||
|
$this->info('@' . $user->name . ' is an user now.');
|
||||||
|
} else {
|
||||||
|
$this->error('Something happened.');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
61
app/Console/Commands/PromoteUser.php
Normal file
61
app/Console/Commands/PromoteUser.php
Normal file
@@ -0,0 +1,61 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
namespace App\Console\Commands;
|
||||||
|
|
||||||
|
use App\User;
|
||||||
|
use Illuminate\Console\Command;
|
||||||
|
|
||||||
|
class PromoteUser extends Command
|
||||||
|
{
|
||||||
|
/**
|
||||||
|
* The name and signature of the console command.
|
||||||
|
*
|
||||||
|
* @var string
|
||||||
|
*/
|
||||||
|
protected $signature = 'tissue:user:promote {username}';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The console command description.
|
||||||
|
*
|
||||||
|
* @var string
|
||||||
|
*/
|
||||||
|
protected $description = 'Promote user to admin';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Create a new command instance.
|
||||||
|
*
|
||||||
|
* @return void
|
||||||
|
*/
|
||||||
|
public function __construct()
|
||||||
|
{
|
||||||
|
parent::__construct();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Execute the console command.
|
||||||
|
*
|
||||||
|
* @return mixed
|
||||||
|
*/
|
||||||
|
public function handle()
|
||||||
|
{
|
||||||
|
$user = User::where('name', $this->argument('username'))->first();
|
||||||
|
if ($user === null) {
|
||||||
|
$this->error('No user with such username');
|
||||||
|
|
||||||
|
return 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
if ($user->is_admin) {
|
||||||
|
$this->info('@' . $user->name . ' is already an administrator.');
|
||||||
|
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
$user->is_admin = true;
|
||||||
|
if ($user->save()) {
|
||||||
|
$this->info('@' . $user->name . ' is an administrator now.');
|
||||||
|
} else {
|
||||||
|
$this->error('Something happened.');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
@@ -2,6 +2,8 @@
|
|||||||
|
|
||||||
namespace App\Console;
|
namespace App\Console;
|
||||||
|
|
||||||
|
use App\Console\Commands\DemoteUser;
|
||||||
|
use App\Console\Commands\PromoteUser;
|
||||||
use Illuminate\Console\Scheduling\Schedule;
|
use Illuminate\Console\Scheduling\Schedule;
|
||||||
use Illuminate\Foundation\Console\Kernel as ConsoleKernel;
|
use Illuminate\Foundation\Console\Kernel as ConsoleKernel;
|
||||||
|
|
||||||
@@ -35,6 +37,8 @@ class Kernel extends ConsoleKernel
|
|||||||
*/
|
*/
|
||||||
protected function commands()
|
protected function commands()
|
||||||
{
|
{
|
||||||
|
$this->load(__DIR__.'/Commands');
|
||||||
|
|
||||||
require base_path('routes/console.php');
|
require base_path('routes/console.php');
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
18
app/DeactivatedUser.php
Normal file
18
app/DeactivatedUser.php
Normal file
@@ -0,0 +1,18 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
namespace App;
|
||||||
|
|
||||||
|
use Illuminate\Database\Eloquent\Model;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 削除済Userのユーザー名履歴
|
||||||
|
*/
|
||||||
|
class DeactivatedUser extends Model
|
||||||
|
{
|
||||||
|
public $incrementing = false;
|
||||||
|
protected $keyType = 'string';
|
||||||
|
|
||||||
|
protected $fillable = [
|
||||||
|
'name'
|
||||||
|
];
|
||||||
|
}
|
@@ -2,16 +2,20 @@
|
|||||||
|
|
||||||
namespace App;
|
namespace App;
|
||||||
|
|
||||||
|
use Illuminate\Database\Eloquent\Builder;
|
||||||
use Illuminate\Database\Eloquent\Model;
|
use Illuminate\Database\Eloquent\Model;
|
||||||
|
use Illuminate\Support\Facades\Auth;
|
||||||
|
use Illuminate\Support\Facades\DB;
|
||||||
|
use Staudenmeir\EloquentEagerLimit\HasEagerLimit;
|
||||||
|
|
||||||
class Ejaculation extends Model
|
class Ejaculation extends Model
|
||||||
{
|
{
|
||||||
//
|
use HasEagerLimit;
|
||||||
|
|
||||||
protected $fillable = [
|
protected $fillable = [
|
||||||
'user_id', 'ejaculated_date',
|
'user_id', 'ejaculated_date',
|
||||||
'note', 'geo_latitude', 'geo_longitude', 'link',
|
'note', 'geo_latitude', 'geo_longitude', 'link',
|
||||||
'is_private'
|
'is_private', 'is_too_sensitive'
|
||||||
];
|
];
|
||||||
|
|
||||||
protected $dates = [
|
protected $dates = [
|
||||||
@@ -27,4 +31,67 @@ class Ejaculation extends Model
|
|||||||
{
|
{
|
||||||
return $this->belongsToMany('App\Tag')->withTimestamps();
|
return $this->belongsToMany('App\Tag')->withTimestamps();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public function textTags()
|
||||||
|
{
|
||||||
|
return implode(' ', $this->tags->map(function ($v) {
|
||||||
|
return $v->name;
|
||||||
|
})->all());
|
||||||
|
}
|
||||||
|
|
||||||
|
public function likes()
|
||||||
|
{
|
||||||
|
return $this->hasMany(Like::class);
|
||||||
|
}
|
||||||
|
|
||||||
|
public function scopeWithLikes(Builder $query)
|
||||||
|
{
|
||||||
|
if (Auth::check()) {
|
||||||
|
// TODO - このスコープを使うことでlikesが常に直近10件で絞られるのは汚染されすぎ感がある。別名を付与できないか?
|
||||||
|
// - (ejaculation_id, user_id) でユニークなわけですが、is_liked はサブクエリ発行させるのとLeft JoinしてNULLかどうかで結果を見るのどっちがいいんでしょうね
|
||||||
|
return $query
|
||||||
|
->with([
|
||||||
|
'likes' => function ($query) {
|
||||||
|
$query->latest()->take(10);
|
||||||
|
},
|
||||||
|
'likes.user' => function ($query) {
|
||||||
|
$query->where('is_protected', false)
|
||||||
|
->where('private_likes', false)
|
||||||
|
->orWhere('id', Auth::id());
|
||||||
|
}
|
||||||
|
])
|
||||||
|
->withCount([
|
||||||
|
'likes',
|
||||||
|
'likes as is_liked' => function ($query) {
|
||||||
|
$query->where('user_id', Auth::id());
|
||||||
|
}
|
||||||
|
]);
|
||||||
|
} else {
|
||||||
|
return $query
|
||||||
|
->with([
|
||||||
|
'likes' => function ($query) {
|
||||||
|
$query->latest()->take(10);
|
||||||
|
},
|
||||||
|
'likes.user' => function ($query) {
|
||||||
|
$query->where('is_protected', false)
|
||||||
|
->where('private_likes', false);
|
||||||
|
}
|
||||||
|
])
|
||||||
|
->withCount('likes')
|
||||||
|
->addSelect(DB::raw('0 as is_liked'));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* このチェックインと同じ情報を流用してチェックインするためのURLを生成
|
||||||
|
* @return string
|
||||||
|
*/
|
||||||
|
public function makeCheckinURL(): string
|
||||||
|
{
|
||||||
|
return route('checkin', [
|
||||||
|
'link' => $this->link,
|
||||||
|
'tags' => $this->textTags(),
|
||||||
|
'is_too_sensitive' => $this->is_too_sensitive,
|
||||||
|
]);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
24
app/Events/LinkDiscovered.php
Normal file
24
app/Events/LinkDiscovered.php
Normal file
@@ -0,0 +1,24 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
namespace App\Events;
|
||||||
|
|
||||||
|
use Illuminate\Contracts\Broadcasting\ShouldBroadcast;
|
||||||
|
use Illuminate\Foundation\Events\Dispatchable;
|
||||||
|
use Illuminate\Queue\SerializesModels;
|
||||||
|
|
||||||
|
class LinkDiscovered
|
||||||
|
{
|
||||||
|
use Dispatchable, SerializesModels;
|
||||||
|
|
||||||
|
public $url;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Create a new event instance.
|
||||||
|
*
|
||||||
|
* @param string $url
|
||||||
|
*/
|
||||||
|
public function __construct(string $url)
|
||||||
|
{
|
||||||
|
$this->url = $url;
|
||||||
|
}
|
||||||
|
}
|
@@ -10,4 +10,4 @@ class Formatter extends Facade
|
|||||||
{
|
{
|
||||||
return \App\Utilities\Formatter::class;
|
return \App\Utilities\Formatter::class;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
14
app/Http/Controllers/Admin/DashboardController.php
Normal file
14
app/Http/Controllers/Admin/DashboardController.php
Normal file
@@ -0,0 +1,14 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
namespace App\Http\Controllers\Admin;
|
||||||
|
|
||||||
|
use App\Http\Controllers\Controller;
|
||||||
|
use Illuminate\Http\Request;
|
||||||
|
|
||||||
|
class DashboardController extends Controller
|
||||||
|
{
|
||||||
|
public function index()
|
||||||
|
{
|
||||||
|
return view('admin.dashboard');
|
||||||
|
}
|
||||||
|
}
|
75
app/Http/Controllers/Admin/InfoController.php
Normal file
75
app/Http/Controllers/Admin/InfoController.php
Normal file
@@ -0,0 +1,75 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
namespace App\Http\Controllers\Admin;
|
||||||
|
|
||||||
|
use App\Http\Controllers\Controller;
|
||||||
|
use App\Http\Requests\AdminInfoStoreRequest;
|
||||||
|
use App\Information;
|
||||||
|
use Illuminate\Http\Request;
|
||||||
|
use Illuminate\Support\Facades\Validator;
|
||||||
|
use Illuminate\Validation\Rule;
|
||||||
|
|
||||||
|
class InfoController extends Controller
|
||||||
|
{
|
||||||
|
public function index()
|
||||||
|
{
|
||||||
|
$informations = Information::query()
|
||||||
|
->select('id', 'category', 'pinned', 'title', 'created_at')
|
||||||
|
->orderByDesc('pinned')
|
||||||
|
->orderByDesc('created_at')
|
||||||
|
->paginate(20);
|
||||||
|
|
||||||
|
return view('admin.info.index')->with([
|
||||||
|
'informations' => $informations,
|
||||||
|
'categories' => Information::CATEGORIES
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
|
||||||
|
public function create()
|
||||||
|
{
|
||||||
|
return view('admin.info.create')->with([
|
||||||
|
'categories' => Information::CATEGORIES
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
|
||||||
|
public function store(AdminInfoStoreRequest $request)
|
||||||
|
{
|
||||||
|
$inputs = $request->all();
|
||||||
|
if (!$request->has('pinned')) {
|
||||||
|
$inputs['pinned'] = false;
|
||||||
|
}
|
||||||
|
|
||||||
|
$info = Information::create($inputs);
|
||||||
|
|
||||||
|
return redirect()->route('admin.info.edit', ['info' => $info])->with('status', 'お知らせを更新しました。');
|
||||||
|
}
|
||||||
|
|
||||||
|
public function edit($id)
|
||||||
|
{
|
||||||
|
$information = Information::findOrFail($id);
|
||||||
|
|
||||||
|
return view('admin.info.edit')->with([
|
||||||
|
'info' => $information,
|
||||||
|
'categories' => Information::CATEGORIES
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
|
||||||
|
public function update(AdminInfoStoreRequest $request, Information $info)
|
||||||
|
{
|
||||||
|
$inputs = $request->all();
|
||||||
|
if (!$request->has('pinned')) {
|
||||||
|
$inputs['pinned'] = false;
|
||||||
|
}
|
||||||
|
|
||||||
|
$info->fill($inputs)->save();
|
||||||
|
|
||||||
|
return redirect()->route('admin.info.edit', ['info' => $info])->with('status', 'お知らせを更新しました。');
|
||||||
|
}
|
||||||
|
|
||||||
|
public function destroy(Information $info)
|
||||||
|
{
|
||||||
|
$info->delete();
|
||||||
|
|
||||||
|
return redirect()->route('admin.info')->with('status', 'お知らせを削除しました。');
|
||||||
|
}
|
||||||
|
}
|
63
app/Http/Controllers/Api/CardController.php
Normal file
63
app/Http/Controllers/Api/CardController.php
Normal file
@@ -0,0 +1,63 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
namespace App\Http\Controllers\Api;
|
||||||
|
|
||||||
|
use App\Metadata;
|
||||||
|
use App\MetadataResolver\MetadataResolver;
|
||||||
|
use App\Tag;
|
||||||
|
use App\Utilities\Formatter;
|
||||||
|
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)
|
||||||
|
{
|
||||||
|
$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);
|
||||||
|
}
|
||||||
|
|
||||||
|
$metadata->load('tags');
|
||||||
|
|
||||||
|
$response = response($metadata);
|
||||||
|
if (!config('app.debug')) {
|
||||||
|
$response = $response->setCache(['public' => true, 'max_age' => 86400]);
|
||||||
|
}
|
||||||
|
|
||||||
|
return $response;
|
||||||
|
}
|
||||||
|
}
|
73
app/Http/Controllers/Api/LikeController.php
Normal file
73
app/Http/Controllers/Api/LikeController.php
Normal file
@@ -0,0 +1,73 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
namespace App\Http\Controllers\Api;
|
||||||
|
|
||||||
|
use App\Ejaculation;
|
||||||
|
use App\Http\Controllers\Controller;
|
||||||
|
use App\Like;
|
||||||
|
use Illuminate\Http\Request;
|
||||||
|
use Illuminate\Support\Facades\Auth;
|
||||||
|
use Illuminate\Support\Facades\Validator;
|
||||||
|
|
||||||
|
class LikeController extends Controller
|
||||||
|
{
|
||||||
|
public function store(Request $request)
|
||||||
|
{
|
||||||
|
$request->validate([
|
||||||
|
'id' => 'required|integer|exists:ejaculations'
|
||||||
|
]);
|
||||||
|
|
||||||
|
$keys = [
|
||||||
|
'user_id' => Auth::id(),
|
||||||
|
'ejaculation_id' => $request->input('id')
|
||||||
|
];
|
||||||
|
|
||||||
|
$like = Like::query()->where($keys)->first();
|
||||||
|
if ($like) {
|
||||||
|
$data = [
|
||||||
|
'errors' => [
|
||||||
|
['message' => 'このチェックインはすでにいいね済です。']
|
||||||
|
],
|
||||||
|
'ejaculation' => $like->ejaculation
|
||||||
|
];
|
||||||
|
|
||||||
|
return response()->json($data, 409);
|
||||||
|
}
|
||||||
|
|
||||||
|
$like = Like::create($keys);
|
||||||
|
|
||||||
|
return [
|
||||||
|
'ejaculation' => $like->ejaculation
|
||||||
|
];
|
||||||
|
}
|
||||||
|
|
||||||
|
public function destroy($id)
|
||||||
|
{
|
||||||
|
Validator::make(compact('id'), [
|
||||||
|
'id' => 'required|integer'
|
||||||
|
])->validate();
|
||||||
|
|
||||||
|
$like = Like::query()->where([
|
||||||
|
'user_id' => Auth::id(),
|
||||||
|
'ejaculation_id' => $id
|
||||||
|
])->first();
|
||||||
|
if ($like === null) {
|
||||||
|
$ejaculation = Ejaculation::find($id);
|
||||||
|
|
||||||
|
$data = [
|
||||||
|
'errors' => [
|
||||||
|
['message' => 'このチェックインはいいねされていません。']
|
||||||
|
],
|
||||||
|
'ejaculation' => $ejaculation
|
||||||
|
];
|
||||||
|
|
||||||
|
return response()->json($data, 404);
|
||||||
|
}
|
||||||
|
|
||||||
|
$like->delete();
|
||||||
|
|
||||||
|
return [
|
||||||
|
'ejaculation' => $like->ejaculation
|
||||||
|
];
|
||||||
|
}
|
||||||
|
}
|
@@ -2,10 +2,10 @@
|
|||||||
|
|
||||||
namespace App\Http\Controllers\Auth;
|
namespace App\Http\Controllers\Auth;
|
||||||
|
|
||||||
use App\User;
|
|
||||||
use App\Http\Controllers\Controller;
|
use App\Http\Controllers\Controller;
|
||||||
use Illuminate\Support\Facades\Validator;
|
use App\User;
|
||||||
use Illuminate\Foundation\Auth\RegistersUsers;
|
use Illuminate\Foundation\Auth\RegistersUsers;
|
||||||
|
use Illuminate\Support\Facades\Validator;
|
||||||
|
|
||||||
class RegisterController extends Controller
|
class RegisterController extends Controller
|
||||||
{
|
{
|
||||||
@@ -47,11 +47,20 @@ class RegisterController extends Controller
|
|||||||
*/
|
*/
|
||||||
protected function validator(array $data)
|
protected function validator(array $data)
|
||||||
{
|
{
|
||||||
return Validator::make($data, [
|
$rules = [
|
||||||
'name' => 'required|string|regex:/^[a-zA-Z0-9_-]+$/u|max:15|unique:users',
|
'name' => 'required|string|regex:/^[a-zA-Z0-9_-]+$/u|max:15|unique:users|unique:deactivated_users',
|
||||||
'email' => 'required|string|email|max:255|unique:users',
|
'email' => 'required|string|email|max:255|unique:users',
|
||||||
'password' => 'required|string|min:6|confirmed',
|
'password' => 'required|string|min:6|confirmed'
|
||||||
],
|
];
|
||||||
|
|
||||||
|
// reCAPTCHAのキーが設定されている場合、判定を有効化
|
||||||
|
if (!empty(config('captcha.secret'))) {
|
||||||
|
$rules['g-recaptcha-response'] = 'required|captcha';
|
||||||
|
}
|
||||||
|
|
||||||
|
return Validator::make(
|
||||||
|
$data,
|
||||||
|
$rules,
|
||||||
['name.regex' => 'ユーザー名には半角英数字とアンダーバー、ハイフンのみ使用できます。'],
|
['name.regex' => 'ユーザー名には半角英数字とアンダーバー、ハイフンのみ使用できます。'],
|
||||||
['name' => 'ユーザー名']
|
['name' => 'ユーザー名']
|
||||||
);
|
);
|
||||||
|
@@ -2,10 +2,10 @@
|
|||||||
|
|
||||||
namespace App\Http\Controllers;
|
namespace App\Http\Controllers;
|
||||||
|
|
||||||
use Illuminate\Foundation\Bus\DispatchesJobs;
|
|
||||||
use Illuminate\Routing\Controller as BaseController;
|
|
||||||
use Illuminate\Foundation\Validation\ValidatesRequests;
|
|
||||||
use Illuminate\Foundation\Auth\Access\AuthorizesRequests;
|
use Illuminate\Foundation\Auth\Access\AuthorizesRequests;
|
||||||
|
use Illuminate\Foundation\Bus\DispatchesJobs;
|
||||||
|
use Illuminate\Foundation\Validation\ValidatesRequests;
|
||||||
|
use Illuminate\Routing\Controller as BaseController;
|
||||||
|
|
||||||
class Controller extends BaseController
|
class Controller extends BaseController
|
||||||
{
|
{
|
||||||
|
@@ -2,33 +2,41 @@
|
|||||||
|
|
||||||
namespace App\Http\Controllers;
|
namespace App\Http\Controllers;
|
||||||
|
|
||||||
|
use App\Ejaculation;
|
||||||
|
use App\Events\LinkDiscovered;
|
||||||
use App\Tag;
|
use App\Tag;
|
||||||
use App\User;
|
use App\User;
|
||||||
use Carbon\Carbon;
|
use Carbon\Carbon;
|
||||||
use Validator;
|
|
||||||
use App\Ejaculation;
|
|
||||||
use Illuminate\Http\Request;
|
use Illuminate\Http\Request;
|
||||||
use Illuminate\Support\Facades\Auth;
|
use Illuminate\Support\Facades\Auth;
|
||||||
|
use Validator;
|
||||||
|
|
||||||
class EjaculationController extends Controller
|
class EjaculationController extends Controller
|
||||||
{
|
{
|
||||||
public function create()
|
public function create(Request $request)
|
||||||
{
|
{
|
||||||
return view('ejaculation.checkin');
|
$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
|
||||||
|
];
|
||||||
|
|
||||||
|
return view('ejaculation.checkin')->with('defaults', $defaults);
|
||||||
}
|
}
|
||||||
|
|
||||||
public function store(Request $request)
|
public function store(Request $request)
|
||||||
{
|
{
|
||||||
$inputs = $request->all();
|
$inputs = $request->all();
|
||||||
if ($request->has('note')) {
|
|
||||||
$inputs['note'] = str_replace(["\r\n", "\r"], "\n", $inputs['note']);
|
|
||||||
}
|
|
||||||
|
|
||||||
Validator::make($inputs, [
|
$validator = Validator::make($inputs, [
|
||||||
'date' => 'required|date_format:Y/m/d',
|
'date' => 'required|date_format:Y/m/d',
|
||||||
'time' => 'required|date_format:H:i',
|
'time' => 'required|date_format:H:i',
|
||||||
'note' => 'nullable|string|max:500',
|
'note' => 'nullable|string|max:500',
|
||||||
'link' => 'nullable|url',
|
'link' => 'nullable|url|max:2000',
|
||||||
'tags' => 'nullable|string',
|
'tags' => 'nullable|string',
|
||||||
])->after(function ($validator) use ($request, $inputs) {
|
])->after(function ($validator) use ($request, $inputs) {
|
||||||
// 日時の重複チェック
|
// 日時の重複チェック
|
||||||
@@ -38,32 +46,47 @@ class EjaculationController extends Controller
|
|||||||
$validator->errors()->add('datetime', '既にこの日時にチェックインしているため、登録できません。');
|
$validator->errors()->add('datetime', '既にこの日時にチェックインしているため、登録できません。');
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
})->validate();
|
});
|
||||||
|
|
||||||
|
if ($validator->fails()) {
|
||||||
|
return redirect()->route('checkin')->withErrors($validator)->withInput();
|
||||||
|
}
|
||||||
|
|
||||||
$ejaculation = Ejaculation::create([
|
$ejaculation = Ejaculation::create([
|
||||||
'user_id' => Auth::id(),
|
'user_id' => Auth::id(),
|
||||||
'ejaculated_date' => Carbon::createFromFormat('Y/m/d H:i', $inputs['date'] . ' ' . $inputs['time']),
|
'ejaculated_date' => Carbon::createFromFormat('Y/m/d H:i', $inputs['date'] . ' ' . $inputs['time']),
|
||||||
'note' => $inputs['note'] ?? '',
|
'note' => $inputs['note'] ?? '',
|
||||||
'link' => $inputs['link'] ?? '',
|
'link' => $inputs['link'] ?? '',
|
||||||
'is_private' => $request->has('is_private') ?? false
|
'is_private' => $request->has('is_private') ?? false,
|
||||||
|
'is_too_sensitive' => $request->has('is_too_sensitive') ?? false
|
||||||
]);
|
]);
|
||||||
|
|
||||||
$tagIds = [];
|
$tagIds = [];
|
||||||
if (!empty($inputs['tags'])) {
|
if (!empty($inputs['tags'])) {
|
||||||
$tags = explode(' ', $inputs['tags']);
|
$tags = explode(' ', $inputs['tags']);
|
||||||
foreach ($tags as $tag) {
|
foreach ($tags as $tag) {
|
||||||
|
if ($tag === '') {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
$tag = Tag::firstOrCreate(['name' => $tag]);
|
$tag = Tag::firstOrCreate(['name' => $tag]);
|
||||||
$tagIds[] = $tag->id;
|
$tagIds[] = $tag->id;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
$ejaculation->tags()->sync($tagIds);
|
$ejaculation->tags()->sync($tagIds);
|
||||||
|
|
||||||
|
if (!empty($ejaculation->link)) {
|
||||||
|
event(new LinkDiscovered($ejaculation->link));
|
||||||
|
}
|
||||||
|
|
||||||
return redirect()->route('checkin.show', ['id' => $ejaculation->id])->with('status', 'チェックインしました!');
|
return redirect()->route('checkin.show', ['id' => $ejaculation->id])->with('status', 'チェックインしました!');
|
||||||
}
|
}
|
||||||
|
|
||||||
public function show($id)
|
public function show($id)
|
||||||
{
|
{
|
||||||
$ejaculation = Ejaculation::findOrFail($id);
|
$ejaculation = Ejaculation::where('id', $id)
|
||||||
|
->withLikes()
|
||||||
|
->firstOrFail();
|
||||||
$user = User::findOrFail($ejaculation->user_id);
|
$user = User::findOrFail($ejaculation->user_id);
|
||||||
|
|
||||||
// 1つ前のチェックインからの経過時間を求める
|
// 1つ前のチェックインからの経過時間を求める
|
||||||
@@ -86,6 +109,9 @@ class EjaculationController extends Controller
|
|||||||
public function edit($id)
|
public function edit($id)
|
||||||
{
|
{
|
||||||
$ejaculation = Ejaculation::findOrFail($id);
|
$ejaculation = Ejaculation::findOrFail($id);
|
||||||
|
|
||||||
|
$this->authorize('edit', $ejaculation);
|
||||||
|
|
||||||
return view('ejaculation.edit')->with(compact('ejaculation'));
|
return view('ejaculation.edit')->with(compact('ejaculation'));
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -93,16 +119,15 @@ class EjaculationController extends Controller
|
|||||||
{
|
{
|
||||||
$ejaculation = Ejaculation::findOrFail($id);
|
$ejaculation = Ejaculation::findOrFail($id);
|
||||||
|
|
||||||
$inputs = $request->all();
|
$this->authorize('edit', $ejaculation);
|
||||||
if ($request->has('note')) {
|
|
||||||
$inputs['note'] = str_replace(["\r\n", "\r"], "\n", $inputs['note']);
|
|
||||||
}
|
|
||||||
|
|
||||||
Validator::make($inputs, [
|
$inputs = $request->all();
|
||||||
|
|
||||||
|
$validator = Validator::make($inputs, [
|
||||||
'date' => 'required|date_format:Y/m/d',
|
'date' => 'required|date_format:Y/m/d',
|
||||||
'time' => 'required|date_format:H:i',
|
'time' => 'required|date_format:H:i',
|
||||||
'note' => 'nullable|string|max:500',
|
'note' => 'nullable|string|max:500',
|
||||||
'link' => 'nullable|url',
|
'link' => 'nullable|url|max:2000',
|
||||||
'tags' => 'nullable|string',
|
'tags' => 'nullable|string',
|
||||||
])->after(function ($validator) use ($id, $request, $inputs) {
|
])->after(function ($validator) use ($id, $request, $inputs) {
|
||||||
// 日時の重複チェック
|
// 日時の重複チェック
|
||||||
@@ -112,34 +137,51 @@ class EjaculationController extends Controller
|
|||||||
$validator->errors()->add('datetime', '既にこの日時にチェックインしているため、登録できません。');
|
$validator->errors()->add('datetime', '既にこの日時にチェックインしているため、登録できません。');
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
})->validate();
|
});
|
||||||
|
|
||||||
|
if ($validator->fails()) {
|
||||||
|
return redirect()->route('checkin.edit', ['id' => $id])->withErrors($validator)->withInput();
|
||||||
|
}
|
||||||
|
|
||||||
$ejaculation->fill([
|
$ejaculation->fill([
|
||||||
'ejaculated_date' => Carbon::createFromFormat('Y/m/d H:i', $inputs['date'] . ' ' . $inputs['time']),
|
'ejaculated_date' => Carbon::createFromFormat('Y/m/d H:i', $inputs['date'] . ' ' . $inputs['time']),
|
||||||
'note' => $inputs['note'] ?? '',
|
'note' => $inputs['note'] ?? '',
|
||||||
'link' => $inputs['link'] ?? '',
|
'link' => $inputs['link'] ?? '',
|
||||||
'is_private' => $request->has('is_private') ?? false
|
'is_private' => $request->has('is_private') ?? false,
|
||||||
|
'is_too_sensitive' => $request->has('is_too_sensitive') ?? false
|
||||||
])->save();
|
])->save();
|
||||||
|
|
||||||
$tagIds = [];
|
$tagIds = [];
|
||||||
if (!empty($inputs['tags'])) {
|
if (!empty($inputs['tags'])) {
|
||||||
$tags = explode(' ', $inputs['tags']);
|
$tags = explode(' ', $inputs['tags']);
|
||||||
foreach ($tags as $tag) {
|
foreach ($tags as $tag) {
|
||||||
|
if ($tag === '') {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
$tag = Tag::firstOrCreate(['name' => $tag]);
|
$tag = Tag::firstOrCreate(['name' => $tag]);
|
||||||
$tagIds[] = $tag->id;
|
$tagIds[] = $tag->id;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
$ejaculation->tags()->sync($tagIds);
|
$ejaculation->tags()->sync($tagIds);
|
||||||
|
|
||||||
|
if (!empty($ejaculation->link)) {
|
||||||
|
event(new LinkDiscovered($ejaculation->link));
|
||||||
|
}
|
||||||
|
|
||||||
return redirect()->route('checkin.show', ['id' => $ejaculation->id])->with('status', 'チェックインを修正しました!');
|
return redirect()->route('checkin.show', ['id' => $ejaculation->id])->with('status', 'チェックインを修正しました!');
|
||||||
}
|
}
|
||||||
|
|
||||||
public function destroy($id)
|
public function destroy($id)
|
||||||
{
|
{
|
||||||
$ejaculation = Ejaculation::findOrFail($id);
|
$ejaculation = Ejaculation::findOrFail($id);
|
||||||
|
|
||||||
|
$this->authorize('edit', $ejaculation);
|
||||||
|
|
||||||
$user = User::findOrFail($ejaculation->user_id);
|
$user = User::findOrFail($ejaculation->user_id);
|
||||||
$ejaculation->tags()->detach();
|
$ejaculation->tags()->detach();
|
||||||
$ejaculation->delete();
|
$ejaculation->delete();
|
||||||
|
|
||||||
return redirect()->route('user.profile', ['name' => $user->name])->with('status', '削除しました。');
|
return redirect()->route('user.profile', ['name' => $user->name])->with('status', '削除しました。');
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@@ -36,6 +36,31 @@ class HomeController extends Controller
|
|||||||
$categories = Information::CATEGORIES;
|
$categories = Information::CATEGORIES;
|
||||||
|
|
||||||
if (Auth::check()) {
|
if (Auth::check()) {
|
||||||
|
// チェックイン動向グラフ用のデータ取得
|
||||||
|
$groupByDay = Ejaculation::select(DB::raw(
|
||||||
|
<<<'SQL'
|
||||||
|
to_char(ejaculated_date, 'YYYY/MM/DD') AS "date",
|
||||||
|
count(*) AS "count"
|
||||||
|
SQL
|
||||||
|
))
|
||||||
|
->join('users', function ($join) {
|
||||||
|
$join->on('users.id', '=', 'ejaculations.user_id')
|
||||||
|
->where('users.accept_analytics', true);
|
||||||
|
})
|
||||||
|
->where('ejaculated_date', '>=', now()->subDays(30))
|
||||||
|
->groupBy(DB::raw("to_char(ejaculated_date, 'YYYY/MM/DD')"))
|
||||||
|
->orderBy(DB::raw("to_char(ejaculated_date, 'YYYY/MM/DD')"))
|
||||||
|
->get()
|
||||||
|
->mapWithKeys(function ($item) {
|
||||||
|
return [$item['date'] => $item['count']];
|
||||||
|
});
|
||||||
|
$globalEjaculationCounts = [];
|
||||||
|
$day = Carbon::now()->subDays(29);
|
||||||
|
for ($i = 0; $i < 30; $i++) {
|
||||||
|
$globalEjaculationCounts[$day->format('Y/m/d') . ' の総チェックイン数'] = $groupByDay[$day->format('Y/m/d')] ?? 0;
|
||||||
|
$day->addDay();
|
||||||
|
}
|
||||||
|
|
||||||
// お惣菜コーナー用のデータ取得
|
// お惣菜コーナー用のデータ取得
|
||||||
$publicLinkedEjaculations = Ejaculation::join('users', 'users.id', '=', 'ejaculations.user_id')
|
$publicLinkedEjaculations = Ejaculation::join('users', 'users.id', '=', 'ejaculations.user_id')
|
||||||
->where('users.is_protected', false)
|
->where('users.is_protected', false)
|
||||||
@@ -44,10 +69,11 @@ class HomeController extends Controller
|
|||||||
->orderBy('ejaculations.ejaculated_date', 'desc')
|
->orderBy('ejaculations.ejaculated_date', 'desc')
|
||||||
->select('ejaculations.*')
|
->select('ejaculations.*')
|
||||||
->with('user', 'tags')
|
->with('user', 'tags')
|
||||||
->take(5)
|
->withLikes()
|
||||||
|
->take(10)
|
||||||
->get();
|
->get();
|
||||||
|
|
||||||
return view('home')->with(compact('informations', 'categories', 'publicLinkedEjaculations'));
|
return view('home')->with(compact('informations', 'categories', 'globalEjaculationCounts', 'publicLinkedEjaculations'));
|
||||||
} else {
|
} else {
|
||||||
return view('guest')->with(compact('informations', 'categories'));
|
return view('guest')->with(compact('informations', 'categories'));
|
||||||
}
|
}
|
||||||
|
@@ -14,6 +14,7 @@ class InfoController extends Controller
|
|||||||
->orderByDesc('pinned')
|
->orderByDesc('pinned')
|
||||||
->orderByDesc('created_at')
|
->orderByDesc('created_at')
|
||||||
->paginate(20);
|
->paginate(20);
|
||||||
|
|
||||||
return view('info.index')->with([
|
return view('info.index')->with([
|
||||||
'informations' => $informations,
|
'informations' => $informations,
|
||||||
'categories' => Information::CATEGORIES
|
'categories' => Information::CATEGORIES
|
||||||
@@ -23,6 +24,7 @@ class InfoController extends Controller
|
|||||||
public function show($id)
|
public function show($id)
|
||||||
{
|
{
|
||||||
$information = Information::findOrFail($id);
|
$information = Information::findOrFail($id);
|
||||||
|
|
||||||
return view('info.show')->with([
|
return view('info.show')->with([
|
||||||
'info' => $information,
|
'info' => $information,
|
||||||
'category' => Information::CATEGORIES[$information->category]
|
'category' => Information::CATEGORIES[$information->category]
|
||||||
|
51
app/Http/Controllers/SearchController.php
Normal file
51
app/Http/Controllers/SearchController.php
Normal file
@@ -0,0 +1,51 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
namespace App\Http\Controllers;
|
||||||
|
|
||||||
|
use App\Ejaculation;
|
||||||
|
use App\Tag;
|
||||||
|
use Illuminate\Http\Request;
|
||||||
|
use Illuminate\Support\Facades\Auth;
|
||||||
|
|
||||||
|
class SearchController extends Controller
|
||||||
|
{
|
||||||
|
public function index(Request $request)
|
||||||
|
{
|
||||||
|
$inputs = $request->validate([
|
||||||
|
'q' => 'required'
|
||||||
|
]);
|
||||||
|
|
||||||
|
$results = Ejaculation::query()
|
||||||
|
->whereHas('tags', function ($query) use ($inputs) {
|
||||||
|
$query->where('name', 'like', "%{$inputs['q']}%");
|
||||||
|
})
|
||||||
|
->whereHas('user', function ($query) {
|
||||||
|
$query->where('is_protected', false);
|
||||||
|
if (Auth::check()) {
|
||||||
|
$query->orWhere('id', Auth::id());
|
||||||
|
}
|
||||||
|
})
|
||||||
|
->where('is_private', false)
|
||||||
|
->orderBy('ejaculated_date', 'desc')
|
||||||
|
->with(['user', 'tags'])
|
||||||
|
->withLikes()
|
||||||
|
->paginate(20)
|
||||||
|
->appends($inputs);
|
||||||
|
|
||||||
|
return view('search.index')->with(compact('inputs', 'results'));
|
||||||
|
}
|
||||||
|
|
||||||
|
public function relatedTag(Request $request)
|
||||||
|
{
|
||||||
|
$inputs = $request->validate([
|
||||||
|
'q' => 'required'
|
||||||
|
]);
|
||||||
|
|
||||||
|
$results = Tag::query()
|
||||||
|
->where('name', 'like', "%{$inputs['q']}%")
|
||||||
|
->paginate(50)
|
||||||
|
->appends($inputs);
|
||||||
|
|
||||||
|
return view('search.relatedTag')->with(compact('inputs', 'results'));
|
||||||
|
}
|
||||||
|
}
|
124
app/Http/Controllers/SettingController.php
Normal file
124
app/Http/Controllers/SettingController.php
Normal file
@@ -0,0 +1,124 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
namespace App\Http\Controllers;
|
||||||
|
|
||||||
|
use App\DeactivatedUser;
|
||||||
|
use Illuminate\Http\Request;
|
||||||
|
use Illuminate\Support\Facades\Auth;
|
||||||
|
use Illuminate\Support\Facades\DB;
|
||||||
|
use Illuminate\Support\Facades\Hash;
|
||||||
|
use Illuminate\Support\Facades\Validator;
|
||||||
|
use Illuminate\Validation\Rule;
|
||||||
|
use Illuminate\Validation\ValidationException;
|
||||||
|
|
||||||
|
class SettingController extends Controller
|
||||||
|
{
|
||||||
|
public function profile()
|
||||||
|
{
|
||||||
|
return view('setting.profile');
|
||||||
|
}
|
||||||
|
|
||||||
|
public function updateProfile(Request $request)
|
||||||
|
{
|
||||||
|
$inputs = $request->all();
|
||||||
|
$validator = Validator::make($inputs, [
|
||||||
|
'display_name' => 'required|string|max:20',
|
||||||
|
'email' => [
|
||||||
|
'required',
|
||||||
|
'string',
|
||||||
|
'email',
|
||||||
|
'max:255',
|
||||||
|
Rule::unique('users')->ignore(Auth::user()->email, 'email')
|
||||||
|
],
|
||||||
|
'bio' => 'nullable|string|max:160',
|
||||||
|
'url' => 'nullable|url|max:2000'
|
||||||
|
], [], [
|
||||||
|
'display_name' => '名前',
|
||||||
|
'email' => 'メールアドレス',
|
||||||
|
'bio' => '自己紹介',
|
||||||
|
'url' => 'URL'
|
||||||
|
]);
|
||||||
|
|
||||||
|
if ($validator->fails()) {
|
||||||
|
return redirect()->route('setting')->withErrors($validator)->withInput();
|
||||||
|
}
|
||||||
|
|
||||||
|
$user = Auth::user();
|
||||||
|
$user->display_name = $inputs['display_name'];
|
||||||
|
$user->email = $inputs['email'];
|
||||||
|
$user->bio = $inputs['bio'] ?? '';
|
||||||
|
$user->url = $inputs['url'] ?? '';
|
||||||
|
$user->save();
|
||||||
|
|
||||||
|
return redirect()->route('setting')->with('status', 'プロフィールを更新しました。');
|
||||||
|
}
|
||||||
|
|
||||||
|
public function privacy()
|
||||||
|
{
|
||||||
|
return view('setting.privacy');
|
||||||
|
}
|
||||||
|
|
||||||
|
public function updatePrivacy(Request $request)
|
||||||
|
{
|
||||||
|
$inputs = $request->all(['is_protected', 'accept_analytics', 'private_likes']);
|
||||||
|
|
||||||
|
$user = Auth::user();
|
||||||
|
$user->is_protected = $inputs['is_protected'] ?? false;
|
||||||
|
$user->accept_analytics = $inputs['accept_analytics'] ?? false;
|
||||||
|
$user->private_likes = $inputs['private_likes'] ?? false;
|
||||||
|
$user->save();
|
||||||
|
|
||||||
|
return redirect()->route('setting.privacy')->with('status', 'プライバシー設定を更新しました。');
|
||||||
|
}
|
||||||
|
|
||||||
|
public function deactivate()
|
||||||
|
{
|
||||||
|
return view('setting.deactivate');
|
||||||
|
}
|
||||||
|
|
||||||
|
public function destroyUser(Request $request)
|
||||||
|
{
|
||||||
|
// パスワードチェック
|
||||||
|
$validated = $request->validate([
|
||||||
|
'password' => 'required|string'
|
||||||
|
]);
|
||||||
|
|
||||||
|
if (!Hash::check($validated['password'], Auth::user()->getAuthPassword())) {
|
||||||
|
throw ValidationException::withMessages([
|
||||||
|
'password' => 'パスワードが正しくありません。'
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
|
||||||
|
// データの削除
|
||||||
|
set_time_limit(0);
|
||||||
|
DB::transaction(function () {
|
||||||
|
$user = Auth::user();
|
||||||
|
|
||||||
|
// 関連レコードの削除
|
||||||
|
// TODO: 別にDELETE文相当のクエリを一発発行するだけでもいい?
|
||||||
|
foreach ($user->ejaculations as $ejaculation) {
|
||||||
|
$ejaculation->delete();
|
||||||
|
}
|
||||||
|
foreach ($user->likes as $like) {
|
||||||
|
$like->delete();
|
||||||
|
}
|
||||||
|
|
||||||
|
// 先にログアウトしないとユーザーは消せない
|
||||||
|
Auth::logout();
|
||||||
|
|
||||||
|
// ユーザーの削除
|
||||||
|
$user->delete();
|
||||||
|
|
||||||
|
// ユーザー名履歴に追記
|
||||||
|
DeactivatedUser::create(['name' => $user->name]);
|
||||||
|
});
|
||||||
|
|
||||||
|
return view('setting.deactivated');
|
||||||
|
}
|
||||||
|
|
||||||
|
// ( ◠‿◠ )☛ここに気づいたか・・・消えてもらう ▂▅▇█▓▒░(’ω’)░▒▓█▇▅▂うわあああああああ
|
||||||
|
// public function password()
|
||||||
|
// {
|
||||||
|
// abort(501);
|
||||||
|
// }
|
||||||
|
}
|
37
app/Http/Controllers/TagController.php
Normal file
37
app/Http/Controllers/TagController.php
Normal file
@@ -0,0 +1,37 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
namespace App\Http\Controllers;
|
||||||
|
|
||||||
|
use App\Tag;
|
||||||
|
use Illuminate\Http\Request;
|
||||||
|
use Illuminate\Support\Facades\Auth;
|
||||||
|
use Illuminate\Support\Facades\DB;
|
||||||
|
|
||||||
|
class TagController extends Controller
|
||||||
|
{
|
||||||
|
public function index()
|
||||||
|
{
|
||||||
|
$tags = Tag::select(DB::raw(
|
||||||
|
<<<'SQL'
|
||||||
|
tags.name,
|
||||||
|
count(*) AS "checkins_count"
|
||||||
|
SQL
|
||||||
|
))
|
||||||
|
->join('ejaculation_tag', 'tags.id', '=', 'ejaculation_tag.tag_id')
|
||||||
|
->join('ejaculations', 'ejaculations.id', '=', 'ejaculation_tag.ejaculation_id')
|
||||||
|
->join('users', 'users.id', '=', 'ejaculations.user_id')
|
||||||
|
->where('ejaculations.is_private', false)
|
||||||
|
->where(function ($query) {
|
||||||
|
$query->where('users.is_protected', false);
|
||||||
|
if (Auth::check()) {
|
||||||
|
$query->orWhere('users.id', Auth::id());
|
||||||
|
}
|
||||||
|
})
|
||||||
|
->groupBy('tags.name')
|
||||||
|
->orderByDesc('checkins_count')
|
||||||
|
->orderBy('tags.name')
|
||||||
|
->paginate(100);
|
||||||
|
|
||||||
|
return view('tag.index', compact('tags'));
|
||||||
|
}
|
||||||
|
}
|
24
app/Http/Controllers/TimelineController.php
Normal file
24
app/Http/Controllers/TimelineController.php
Normal file
@@ -0,0 +1,24 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
namespace App\Http\Controllers;
|
||||||
|
|
||||||
|
use App\Ejaculation;
|
||||||
|
use Illuminate\Http\Request;
|
||||||
|
|
||||||
|
class TimelineController extends Controller
|
||||||
|
{
|
||||||
|
public function showPublic()
|
||||||
|
{
|
||||||
|
$ejaculations = Ejaculation::join('users', 'users.id', '=', 'ejaculations.user_id')
|
||||||
|
->where('users.is_protected', false)
|
||||||
|
->where('ejaculations.is_private', false)
|
||||||
|
->where('ejaculations.link', '<>', '')
|
||||||
|
->orderBy('ejaculations.ejaculated_date', 'desc')
|
||||||
|
->select('ejaculations.*')
|
||||||
|
->with('user', 'tags')
|
||||||
|
->withLikes()
|
||||||
|
->paginate(21);
|
||||||
|
|
||||||
|
return view('timeline.public')->with(compact('ejaculations'));
|
||||||
|
}
|
||||||
|
}
|
@@ -11,7 +11,10 @@ use Illuminate\Support\Facades\DB;
|
|||||||
|
|
||||||
class UserController extends Controller
|
class UserController extends Controller
|
||||||
{
|
{
|
||||||
//
|
public function redirectMypage()
|
||||||
|
{
|
||||||
|
return redirect()->route('user.profile', ['name' => auth()->user()->name]);
|
||||||
|
}
|
||||||
|
|
||||||
public function profile($name)
|
public function profile($name)
|
||||||
{
|
{
|
||||||
@@ -21,11 +24,13 @@ class UserController extends Controller
|
|||||||
}
|
}
|
||||||
|
|
||||||
// チェックインの取得
|
// チェックインの取得
|
||||||
$query = Ejaculation::select(DB::raw(<<<'SQL'
|
$query = Ejaculation::select(DB::raw(
|
||||||
|
<<<'SQL'
|
||||||
id,
|
id,
|
||||||
ejaculated_date,
|
ejaculated_date,
|
||||||
note,
|
note,
|
||||||
is_private,
|
is_private,
|
||||||
|
is_too_sensitive,
|
||||||
link,
|
link,
|
||||||
to_char(lead(ejaculated_date, 1, NULL) OVER (ORDER BY ejaculated_date DESC), 'YYYY/MM/DD HH24:MI') AS before_date,
|
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 - (lead(ejaculated_date, 1, NULL) OVER (ORDER BY ejaculated_date DESC)), 'FMDDD日 FMHH24時間 FMMI分') AS ejaculated_span
|
||||||
@@ -37,9 +42,24 @@ SQL
|
|||||||
}
|
}
|
||||||
$ejaculations = $query->orderBy('ejaculated_date', 'desc')
|
$ejaculations = $query->orderBy('ejaculated_date', 'desc')
|
||||||
->with('tags')
|
->with('tags')
|
||||||
|
->withLikes()
|
||||||
->paginate(20);
|
->paginate(20);
|
||||||
|
|
||||||
return view('user.profile')->with(compact('user', 'ejaculations'));
|
// よく使っているタグ
|
||||||
|
$tagsQuery = DB::table('ejaculations')
|
||||||
|
->join('ejaculation_tag', 'ejaculations.id', '=', 'ejaculation_tag.ejaculation_id')
|
||||||
|
->join('tags', 'ejaculation_tag.tag_id', '=', 'tags.id')
|
||||||
|
->selectRaw('tags.name, count(*) as count')
|
||||||
|
->where('ejaculations.user_id', $user->id);
|
||||||
|
if (!Auth::check() || $user->id !== Auth::id()) {
|
||||||
|
$tagsQuery = $tagsQuery->where('ejaculations.is_private', false);
|
||||||
|
}
|
||||||
|
$tags = $tagsQuery->groupBy('tags.name')
|
||||||
|
->orderBy('count', 'desc')
|
||||||
|
->limit(10)
|
||||||
|
->get();
|
||||||
|
|
||||||
|
return view('user.profile')->with(compact('user', 'ejaculations', 'tags'));
|
||||||
}
|
}
|
||||||
|
|
||||||
public function stats($name)
|
public function stats($name)
|
||||||
@@ -49,19 +69,37 @@ SQL
|
|||||||
abort(404);
|
abort(404);
|
||||||
}
|
}
|
||||||
|
|
||||||
$groupByDay = Ejaculation::select(DB::raw(<<<'SQL'
|
$dateUntil = now()->addMonth()->startOfMonth();
|
||||||
|
|
||||||
|
$groupByDay = Ejaculation::select(DB::raw(
|
||||||
|
<<<'SQL'
|
||||||
to_char(ejaculated_date, 'YYYY/MM/DD') AS "date",
|
to_char(ejaculated_date, 'YYYY/MM/DD') AS "date",
|
||||||
count(*) AS "count"
|
count(*) AS "count"
|
||||||
SQL
|
SQL
|
||||||
))
|
))
|
||||||
->where('user_id', $user->id)
|
->where('user_id', $user->id)
|
||||||
|
->where('ejaculated_date', '<', $dateUntil)
|
||||||
->groupBy(DB::raw("to_char(ejaculated_date, 'YYYY/MM/DD')"))
|
->groupBy(DB::raw("to_char(ejaculated_date, 'YYYY/MM/DD')"))
|
||||||
->orderBy(DB::raw("to_char(ejaculated_date, 'YYYY/MM/DD')"))
|
->orderBy(DB::raw("to_char(ejaculated_date, 'YYYY/MM/DD')"))
|
||||||
->get();
|
->get();
|
||||||
|
|
||||||
|
$groupByHour = Ejaculation::select(DB::raw(
|
||||||
|
<<<'SQL'
|
||||||
|
to_char(ejaculated_date, 'HH24') AS "hour",
|
||||||
|
count(*) AS "count"
|
||||||
|
SQL
|
||||||
|
))
|
||||||
|
->where('user_id', $user->id)
|
||||||
|
->where('ejaculated_date', '<', $dateUntil)
|
||||||
|
->groupBy(DB::raw("to_char(ejaculated_date, 'HH24')"))
|
||||||
|
->orderBy(DB::raw('1'))
|
||||||
|
->get();
|
||||||
|
|
||||||
$dailySum = [];
|
$dailySum = [];
|
||||||
$monthlySum = [];
|
$monthlySum = [];
|
||||||
$yearlySum = [];
|
$yearlySum = [];
|
||||||
|
$dowSum = array_fill(0, 7, 0);
|
||||||
|
$hourlySum = array_fill(0, 24, 0);
|
||||||
|
|
||||||
// 年間グラフ用の配列初期化
|
// 年間グラフ用の配列初期化
|
||||||
if ($groupByDay->first() !== null) {
|
if ($groupByDay->first() !== null) {
|
||||||
@@ -72,25 +110,32 @@ SQL
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// 月間グラフ用の配列初期化
|
|
||||||
$month = Carbon::now()->subMonth(11)->firstOfMonth(); // 直近12ヶ月
|
|
||||||
for ($i = 0; $i < 12; $i++) {
|
|
||||||
$monthlySum[$month->format('Y/m')] = 0;
|
|
||||||
$month->addMonth();
|
|
||||||
}
|
|
||||||
|
|
||||||
foreach ($groupByDay as $data) {
|
foreach ($groupByDay as $data) {
|
||||||
$date = Carbon::createFromFormat('Y/m/d', $data->date);
|
$date = Carbon::createFromFormat('Y/m/d', $data->date);
|
||||||
$yearAndMonth = $date->format('Y/m');
|
$yearAndMonth = $date->format('Y/m');
|
||||||
|
|
||||||
$dailySum[$date->timestamp] = $data->count;
|
$dailySum[$date->timestamp] = $data->count;
|
||||||
$yearlySum[$date->year] += $data->count;
|
$yearlySum[$date->year] += $data->count;
|
||||||
if (isset($monthlySum[$yearAndMonth])) {
|
$dowSum[$date->dayOfWeek] += $data->count;
|
||||||
$monthlySum[$yearAndMonth] += $data->count;
|
$monthlySum[$yearAndMonth] = ($monthlySum[$yearAndMonth] ?? 0) + $data->count;
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
return view('user.stats')->with(compact('user', 'dailySum', 'monthlySum', 'yearlySum'));
|
foreach ($groupByHour as $data) {
|
||||||
|
$hour = (int)$data->hour;
|
||||||
|
$hourlySum[$hour] += $data->count;
|
||||||
|
}
|
||||||
|
|
||||||
|
$graphData = [
|
||||||
|
'dailySum' => $dailySum,
|
||||||
|
'dowSum' => $dowSum,
|
||||||
|
'monthlySum' => $monthlySum,
|
||||||
|
'yearlyKey' => array_keys($yearlySum),
|
||||||
|
'yearlySum' => array_values($yearlySum),
|
||||||
|
'hourlyKey' => array_keys($hourlySum),
|
||||||
|
'hourlySum' => array_values($hourlySum),
|
||||||
|
];
|
||||||
|
|
||||||
|
return view('user.stats')->with(compact('user', 'graphData'));
|
||||||
}
|
}
|
||||||
|
|
||||||
public function okazu($name)
|
public function okazu($name)
|
||||||
@@ -101,11 +146,13 @@ SQL
|
|||||||
}
|
}
|
||||||
|
|
||||||
// チェックインの取得
|
// チェックインの取得
|
||||||
$query = Ejaculation::select(DB::raw(<<<'SQL'
|
$query = Ejaculation::select(DB::raw(
|
||||||
|
<<<'SQL'
|
||||||
id,
|
id,
|
||||||
ejaculated_date,
|
ejaculated_date,
|
||||||
note,
|
note,
|
||||||
is_private,
|
is_private,
|
||||||
|
is_too_sensitive,
|
||||||
link,
|
link,
|
||||||
to_char(lead(ejaculated_date, 1, NULL) OVER (ORDER BY ejaculated_date DESC), 'YYYY/MM/DD HH24:MI') AS before_date,
|
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 - (lead(ejaculated_date, 1, NULL) OVER (ORDER BY ejaculated_date DESC)), 'FMDDD日 FMHH24時間 FMMI分') AS ejaculated_span
|
||||||
@@ -122,4 +169,23 @@ SQL
|
|||||||
|
|
||||||
return view('user.profile')->with(compact('user', 'ejaculations'));
|
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'));
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
@@ -18,6 +18,7 @@ class Kernel extends HttpKernel
|
|||||||
\Illuminate\Foundation\Http\Middleware\ValidatePostSize::class,
|
\Illuminate\Foundation\Http\Middleware\ValidatePostSize::class,
|
||||||
\App\Http\Middleware\TrimStrings::class,
|
\App\Http\Middleware\TrimStrings::class,
|
||||||
\Illuminate\Foundation\Http\Middleware\ConvertEmptyStringsToNull::class,
|
\Illuminate\Foundation\Http\Middleware\ConvertEmptyStringsToNull::class,
|
||||||
|
\App\Http\Middleware\TrustProxies::class,
|
||||||
];
|
];
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -34,9 +35,15 @@ class Kernel extends HttpKernel
|
|||||||
\Illuminate\View\Middleware\ShareErrorsFromSession::class,
|
\Illuminate\View\Middleware\ShareErrorsFromSession::class,
|
||||||
\App\Http\Middleware\VerifyCsrfToken::class,
|
\App\Http\Middleware\VerifyCsrfToken::class,
|
||||||
\Illuminate\Routing\Middleware\SubstituteBindings::class,
|
\Illuminate\Routing\Middleware\SubstituteBindings::class,
|
||||||
|
\App\Http\Middleware\NormalizeLineEnding::class,
|
||||||
],
|
],
|
||||||
|
|
||||||
|
// 現時点では内部APIしかないので、認証の手間を省くためにステートフルにしている。
|
||||||
'api' => [
|
'api' => [
|
||||||
|
\App\Http\Middleware\EncryptCookies::class,
|
||||||
|
\Illuminate\Cookie\Middleware\AddQueuedCookiesToResponse::class,
|
||||||
|
\Illuminate\Session\Middleware\StartSession::class,
|
||||||
|
\App\Http\Middleware\VerifyCsrfToken::class,
|
||||||
'throttle:60,1',
|
'throttle:60,1',
|
||||||
'bindings',
|
'bindings',
|
||||||
],
|
],
|
||||||
|
30
app/Http/Middleware/NormalizeLineEnding.php
Normal file
30
app/Http/Middleware/NormalizeLineEnding.php
Normal file
@@ -0,0 +1,30 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
namespace App\Http\Middleware;
|
||||||
|
|
||||||
|
use Closure;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* リクエスト内の改行コードを正規化する。
|
||||||
|
* @package App\Http\Middleware
|
||||||
|
*/
|
||||||
|
class NormalizeLineEnding
|
||||||
|
{
|
||||||
|
/**
|
||||||
|
* Handle an incoming request.
|
||||||
|
*
|
||||||
|
* @param \Illuminate\Http\Request $request
|
||||||
|
* @param \Closure $next
|
||||||
|
* @return mixed
|
||||||
|
*/
|
||||||
|
public function handle($request, Closure $next)
|
||||||
|
{
|
||||||
|
$newInput = [];
|
||||||
|
foreach ($request->input() as $key => $value) {
|
||||||
|
$newInput[$key] = str_replace(["\r\n", "\r"], "\n", $value);
|
||||||
|
}
|
||||||
|
$request->replace($newInput);
|
||||||
|
|
||||||
|
return $next($request);
|
||||||
|
}
|
||||||
|
}
|
@@ -18,7 +18,7 @@ class RedirectIfAuthenticated
|
|||||||
public function handle($request, Closure $next, $guard = null)
|
public function handle($request, Closure $next, $guard = null)
|
||||||
{
|
{
|
||||||
if (Auth::guard($guard)->check()) {
|
if (Auth::guard($guard)->check()) {
|
||||||
return redirect('/home');
|
return redirect()->route('home');
|
||||||
}
|
}
|
||||||
|
|
||||||
return $next($request);
|
return $next($request);
|
||||||
|
29
app/Http/Middleware/TrustProxies.php
Normal file
29
app/Http/Middleware/TrustProxies.php
Normal file
@@ -0,0 +1,29 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
namespace App\Http\Middleware;
|
||||||
|
|
||||||
|
use Fideloper\Proxy\TrustProxies as Middleware;
|
||||||
|
use Illuminate\Http\Request;
|
||||||
|
|
||||||
|
class TrustProxies extends Middleware
|
||||||
|
{
|
||||||
|
/**
|
||||||
|
* The trusted proxies for this application.
|
||||||
|
*
|
||||||
|
* @var array
|
||||||
|
*/
|
||||||
|
protected $proxies = '**';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The current proxy header mappings.
|
||||||
|
*
|
||||||
|
* @var array
|
||||||
|
*/
|
||||||
|
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',
|
||||||
|
];
|
||||||
|
}
|
35
app/Http/Requests/AdminInfoStoreRequest.php
Normal file
35
app/Http/Requests/AdminInfoStoreRequest.php
Normal file
@@ -0,0 +1,35 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
namespace App\Http\Requests;
|
||||||
|
|
||||||
|
use App\Information;
|
||||||
|
use Illuminate\Foundation\Http\FormRequest;
|
||||||
|
use Illuminate\Validation\Rule;
|
||||||
|
|
||||||
|
class AdminInfoStoreRequest extends FormRequest
|
||||||
|
{
|
||||||
|
/**
|
||||||
|
* Determine if the user is authorized to make this request.
|
||||||
|
*
|
||||||
|
* @return bool
|
||||||
|
*/
|
||||||
|
public function authorize()
|
||||||
|
{
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get the validation rules that apply to the request.
|
||||||
|
*
|
||||||
|
* @return array
|
||||||
|
*/
|
||||||
|
public function rules()
|
||||||
|
{
|
||||||
|
return [
|
||||||
|
'category' => ['required', Rule::in(array_keys(Information::CATEGORIES))],
|
||||||
|
'pinned' => 'nullable|boolean',
|
||||||
|
'title' => 'required|string|max:255',
|
||||||
|
'content' => 'required|string|max:10000'
|
||||||
|
];
|
||||||
|
}
|
||||||
|
}
|
@@ -7,7 +7,7 @@ use Carbon\Carbon;
|
|||||||
use Illuminate\Support\Facades\DB;
|
use Illuminate\Support\Facades\DB;
|
||||||
use Illuminate\View\View;
|
use Illuminate\View\View;
|
||||||
|
|
||||||
class ProfileComposer
|
class ProfileStatsComposer
|
||||||
{
|
{
|
||||||
public function __construct()
|
public function __construct()
|
||||||
{
|
{
|
||||||
@@ -35,9 +35,27 @@ class ProfileComposer
|
|||||||
}
|
}
|
||||||
|
|
||||||
// 概況欄のデータ取得
|
// 概況欄のデータ取得
|
||||||
|
$average = DB::select(<<<'SQL'
|
||||||
|
SELECT
|
||||||
|
avg(span) AS average
|
||||||
|
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
|
||||||
|
SQL
|
||||||
|
, ['user_id' => $user->id]);
|
||||||
|
|
||||||
$summary = DB::select(<<<'SQL'
|
$summary = DB::select(<<<'SQL'
|
||||||
SELECT
|
SELECT
|
||||||
avg(span) AS average,
|
|
||||||
max(span) AS longest,
|
max(span) AS longest,
|
||||||
min(span) AS shortest,
|
min(span) AS shortest,
|
||||||
sum(span) AS total_times,
|
sum(span) AS total_times,
|
||||||
@@ -56,6 +74,6 @@ FROM
|
|||||||
SQL
|
SQL
|
||||||
, ['user_id' => $user->id]);
|
, ['user_id' => $user->id]);
|
||||||
|
|
||||||
$view->with(compact('latestEjaculation', 'currentSession', 'summary'));
|
$view->with(compact('latestEjaculation', 'currentSession', 'average', 'summary'));
|
||||||
}
|
}
|
||||||
}
|
}
|
@@ -16,5 +16,9 @@ class Information extends Model
|
|||||||
3 => ['label' => 'メンテナンス', 'class' => 'badge-warning']
|
3 => ['label' => 'メンテナンス', 'class' => 'badge-warning']
|
||||||
];
|
];
|
||||||
|
|
||||||
|
protected $fillable = [
|
||||||
|
'category', 'pinned', 'title', 'content'
|
||||||
|
];
|
||||||
|
|
||||||
protected $dates = ['deleted_at'];
|
protected $dates = ['deleted_at'];
|
||||||
}
|
}
|
||||||
|
23
app/Like.php
Normal file
23
app/Like.php
Normal file
@@ -0,0 +1,23 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
namespace App;
|
||||||
|
|
||||||
|
use Illuminate\Database\Eloquent\Model;
|
||||||
|
use Staudenmeir\EloquentEagerLimit\HasEagerLimit;
|
||||||
|
|
||||||
|
class Like extends Model
|
||||||
|
{
|
||||||
|
use HasEagerLimit;
|
||||||
|
|
||||||
|
protected $fillable = ['user_id', 'ejaculation_id'];
|
||||||
|
|
||||||
|
public function user()
|
||||||
|
{
|
||||||
|
return $this->belongsTo(User::class);
|
||||||
|
}
|
||||||
|
|
||||||
|
public function ejaculation()
|
||||||
|
{
|
||||||
|
return $this->belongsTo(Ejaculation::class)->withLikes();
|
||||||
|
}
|
||||||
|
}
|
71
app/Listeners/LinkCollector.php
Normal file
71
app/Listeners/LinkCollector.php
Normal file
@@ -0,0 +1,71 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
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 Illuminate\Contracts\Queue\ShouldQueue;
|
||||||
|
use Illuminate\Queue\InteractsWithQueue;
|
||||||
|
use Illuminate\Support\Facades\Log;
|
||||||
|
|
||||||
|
class LinkCollector
|
||||||
|
{
|
||||||
|
/** @var Formatter */
|
||||||
|
private $formatter;
|
||||||
|
/** @var MetadataResolver */
|
||||||
|
private $metadataResolver;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Create the event listener.
|
||||||
|
*
|
||||||
|
* @param Formatter $formatter
|
||||||
|
* @param MetadataResolver $metadataResolver
|
||||||
|
*/
|
||||||
|
public function __construct(Formatter $formatter, MetadataResolver $metadataResolver)
|
||||||
|
{
|
||||||
|
$this->formatter = $formatter;
|
||||||
|
$this->metadataResolver = $metadataResolver;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Handle the event.
|
||||||
|
*
|
||||||
|
* @param LinkDiscovered $event
|
||||||
|
* @return void
|
||||||
|
*/
|
||||||
|
public function handle(LinkDiscovered $event)
|
||||||
|
{
|
||||||
|
// URLの正規化
|
||||||
|
$url = $this->formatter->normalizeUrl($event->url);
|
||||||
|
|
||||||
|
// 無かったら取得
|
||||||
|
// TODO: ある程度古かったら再取得とかありだと思う
|
||||||
|
$metadata = Metadata::find($url);
|
||||||
|
if ($metadata == null || ($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);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
22
app/Metadata.php
Normal file
22
app/Metadata.php
Normal file
@@ -0,0 +1,22 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
namespace App;
|
||||||
|
|
||||||
|
use Illuminate\Database\Eloquent\Model;
|
||||||
|
|
||||||
|
class Metadata extends Model
|
||||||
|
{
|
||||||
|
public $incrementing = false;
|
||||||
|
protected $primaryKey = 'url';
|
||||||
|
protected $keyType = 'string';
|
||||||
|
|
||||||
|
protected $fillable = ['url', 'title', 'description', 'image', 'expires_at'];
|
||||||
|
protected $visible = ['url', 'title', 'description', 'image', 'expires_at', 'tags'];
|
||||||
|
|
||||||
|
protected $dates = ['created_at', 'updated_at', 'expires_at'];
|
||||||
|
|
||||||
|
public function tags()
|
||||||
|
{
|
||||||
|
return $this->belongsToMany(Tag::class)->withTimestamps();
|
||||||
|
}
|
||||||
|
}
|
91
app/MetadataResolver/ActivityPubResolver.php
Normal file
91
app/MetadataResolver/ActivityPubResolver.php
Normal file
@@ -0,0 +1,91 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
namespace App\MetadataResolver;
|
||||||
|
|
||||||
|
use GuzzleHttp\Exception\TransferException;
|
||||||
|
use Illuminate\Support\Facades\Log;
|
||||||
|
use Psr\Http\Message\ResponseInterface;
|
||||||
|
|
||||||
|
class ActivityPubResolver implements Resolver, Parser
|
||||||
|
{
|
||||||
|
/**
|
||||||
|
* @var \GuzzleHttp\Client
|
||||||
|
*/
|
||||||
|
private $activityClient;
|
||||||
|
|
||||||
|
public function __construct()
|
||||||
|
{
|
||||||
|
$this->activityClient = new \GuzzleHttp\Client([
|
||||||
|
'headers' => [
|
||||||
|
'Accept' => 'application/activity+json, application/ld+json; profile="https://www.w3.org/ns/activitystreams"'
|
||||||
|
]
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
|
||||||
|
public function resolve(string $url): Metadata
|
||||||
|
{
|
||||||
|
$res = $this->activityClient->get($url);
|
||||||
|
if ($res->getStatusCode() === 200) {
|
||||||
|
return $this->parse($res->getBody());
|
||||||
|
} else {
|
||||||
|
throw new \RuntimeException("{$res->getStatusCode()}: $url");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public function parse(string $json): Metadata
|
||||||
|
{
|
||||||
|
$activityOrObject = json_decode($json, true);
|
||||||
|
$object = $activityOrObject['object'] ?? $activityOrObject;
|
||||||
|
|
||||||
|
if ($object['type'] !== 'Note') {
|
||||||
|
throw new UnsupportedContentException('Unsupported object type: ' . $object['type']);
|
||||||
|
}
|
||||||
|
|
||||||
|
$metadata = new Metadata();
|
||||||
|
|
||||||
|
$metadata->title = isset($object['attributedTo']) ? $this->getTitleFromActor($object['attributedTo']) : '';
|
||||||
|
$metadata->description .= isset($object['summary']) ? $object['summary'] . ' | ' : '';
|
||||||
|
$metadata->description .= isset($object['content']) ? $this->html2text($object['content']) : '';
|
||||||
|
$metadata->image = $object['attachment'][0]['url'] ?? '';
|
||||||
|
|
||||||
|
return $metadata;
|
||||||
|
}
|
||||||
|
|
||||||
|
private function getTitleFromActor(string $url): string
|
||||||
|
{
|
||||||
|
try {
|
||||||
|
$res = $this->activityClient->get($url);
|
||||||
|
if ($res->getStatusCode() !== 200) {
|
||||||
|
Log::info(self::class . ': Actorの取得に失敗 URL=' . $url);
|
||||||
|
|
||||||
|
return '';
|
||||||
|
}
|
||||||
|
|
||||||
|
$actor = json_decode($res->getBody(), true);
|
||||||
|
$title = $actor['name'] ?? '';
|
||||||
|
if (isset($actor['preferredUsername'])) {
|
||||||
|
$title .= ' (@' . $actor['preferredUsername'] . '@' . parse_url($actor['id'], PHP_URL_HOST) . ')';
|
||||||
|
}
|
||||||
|
|
||||||
|
return $title;
|
||||||
|
} catch (TransferException $e) {
|
||||||
|
Log::info(self::class . ': Actorの取得に失敗 URL=' . $url);
|
||||||
|
|
||||||
|
return '';
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private function html2text(string $html): string
|
||||||
|
{
|
||||||
|
if (empty($html)) {
|
||||||
|
return '';
|
||||||
|
}
|
||||||
|
|
||||||
|
$html = mb_convert_encoding($html, 'HTML-ENTITIES', 'UTF-8');
|
||||||
|
$html = preg_replace('~<br\s*/?\s*>|</p>\s*<p[^>]*>~i', "\n", $html);
|
||||||
|
$dom = new \DOMDocument();
|
||||||
|
$dom->loadHTML($html, LIBXML_HTML_NOIMPLIED | LIBXML_HTML_NODEFDTD);
|
||||||
|
|
||||||
|
return $dom->textContent;
|
||||||
|
}
|
||||||
|
}
|
39
app/MetadataResolver/CienResolver.php
Normal file
39
app/MetadataResolver/CienResolver.php
Normal file
@@ -0,0 +1,39 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
namespace App\MetadataResolver;
|
||||||
|
|
||||||
|
use Carbon\Carbon;
|
||||||
|
use GuzzleHttp\Client;
|
||||||
|
|
||||||
|
class CienResolver extends MetadataResolver
|
||||||
|
{
|
||||||
|
/**
|
||||||
|
* @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((string) $res->getBody());
|
||||||
|
|
||||||
|
// 画像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);
|
||||||
|
}
|
||||||
|
$metadata->expires_at = Carbon::createFromTimestamp($params['px-time'])->addHour(1);
|
||||||
|
|
||||||
|
return $metadata;
|
||||||
|
}
|
||||||
|
}
|
129
app/MetadataResolver/DLsiteResolver.php
Normal file
129
app/MetadataResolver/DLsiteResolver.php
Normal file
@@ -0,0 +1,129 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
namespace App\MetadataResolver;
|
||||||
|
|
||||||
|
use GuzzleHttp\Client;
|
||||||
|
|
||||||
|
class DLsiteResolver implements Resolver
|
||||||
|
{
|
||||||
|
/**
|
||||||
|
* @var Client
|
||||||
|
*/
|
||||||
|
private $client;
|
||||||
|
/**
|
||||||
|
* @var OGPResolver
|
||||||
|
*/
|
||||||
|
private $ogpResolver;
|
||||||
|
|
||||||
|
public function __construct(Client $client, OGPResolver $ogpResolver)
|
||||||
|
{
|
||||||
|
$this->client = $client;
|
||||||
|
$this->ogpResolver = $ogpResolver;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* HTMLからタグとして利用可能な情報を抽出する
|
||||||
|
* @param string $html ページ HTML
|
||||||
|
* @return string[] タグ
|
||||||
|
*/
|
||||||
|
public function extractTags(string $html): array
|
||||||
|
{
|
||||||
|
$dom = new \DOMDocument();
|
||||||
|
@$dom->loadHTML(mb_convert_encoding($html, 'HTML-ENTITIES', 'UTF-8'));
|
||||||
|
$xpath = new \DOMXPath($dom);
|
||||||
|
|
||||||
|
$genreNode = $xpath->query("//div[@class='main_genre'][1]");
|
||||||
|
if ($genreNode->length === 0) {
|
||||||
|
return [];
|
||||||
|
}
|
||||||
|
|
||||||
|
$tagsNode = $genreNode->item(0)->getElementsByTagName('a');
|
||||||
|
$tags = [];
|
||||||
|
|
||||||
|
for ($i = 0; $i <= $tagsNode->length - 1; $i++) {
|
||||||
|
$tags[] = $tagsNode->item($i)->textContent;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 重複削除
|
||||||
|
$tags = array_values(array_unique($tags));
|
||||||
|
|
||||||
|
return $tags;
|
||||||
|
}
|
||||||
|
|
||||||
|
public function resolve(string $url): Metadata
|
||||||
|
{
|
||||||
|
//アフィリエイトの場合は普通のURLに変換
|
||||||
|
// ID型
|
||||||
|
if (preg_match('~/dlaf/=(/.+/.+)?/link/~', $url)) {
|
||||||
|
preg_match('~www\.dlsite\.com/(?P<genre>.+)/dlaf/=(/.+/.+)?/link/work/aid/(?P<AffiliateId>.+)/id/(?P<titleId>..\d+)(\.html)?~', $url, $matches);
|
||||||
|
$url = "https://www.dlsite.com/{$matches['genre']}/work/=/product_id/{$matches['titleId']}.html";
|
||||||
|
}
|
||||||
|
// URL型
|
||||||
|
if (strpos($url, '/dlaf/=/aid/') !== false) {
|
||||||
|
preg_match('~www\.dlsite\.com/.+/dlaf/=/aid/.+/url/(?P<url>.+)~', $url, $matches);
|
||||||
|
$affiliateUrl = urldecode($matches['url']);
|
||||||
|
if (preg_match('~www\.dlsite\.com/.+/(work|announce)/=/product_id/..\d+(\.html)?~', $affiliateUrl, $matches)) {
|
||||||
|
$url = $affiliateUrl;
|
||||||
|
} else {
|
||||||
|
throw new \RuntimeException("アフィリエイト先のリンクがDLsiteのタイトルではありません: $affiliateUrl");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
//スマホページの場合はPCページに正規化
|
||||||
|
if (strpos($url, '-touch') !== false) {
|
||||||
|
$url = str_replace('-touch', '', $url);
|
||||||
|
}
|
||||||
|
|
||||||
|
$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);
|
||||||
|
|
||||||
|
// OGPタイトルから[]に囲まれているmakerを取得する
|
||||||
|
// 複数の作者がいる場合スペース区切りになるためexplodeしている
|
||||||
|
// スペースを含むmakerの場合名前の一部しか取れないが動作には問題ない
|
||||||
|
preg_match('~ \[([^\[\]]*)\] (予告作品 )?\| DLsite(がるまに)?$~', $metadata->title, $match);
|
||||||
|
$makers = explode(' ', $match[1]);
|
||||||
|
|
||||||
|
//フォローボタン(.add_follow)はテキストを含んでしまうことがあるので要素を削除しておく
|
||||||
|
$followButtonNode = $xpath->query('//*[@class="add_follow"]')->item(0);
|
||||||
|
$followButtonNode->parentNode->removeChild($followButtonNode);
|
||||||
|
|
||||||
|
// maker, makerHeadを探す
|
||||||
|
|
||||||
|
// makers
|
||||||
|
// #work_makerから「makerを含むテキスト」を持つ要素を持つtdを探す
|
||||||
|
// 作者名単体の場合もあるし、"作者A / 作者B"のようになることもある
|
||||||
|
$makersNode = $xpath->query('//*[@id="work_maker"]//*[contains(text(), "' . $makers[0] . '")]/ancestor::td')->item(0);
|
||||||
|
// nbspをspaceに置換
|
||||||
|
$makers = trim(str_replace("\xc2\xa0", ' ', $makersNode->textContent));
|
||||||
|
|
||||||
|
// makersHaed
|
||||||
|
// $makerNode(td)に対するthを探す
|
||||||
|
// "著者", "サークル名", "ブランド名"など
|
||||||
|
$makersHeadNode = $xpath->query('preceding-sibling::th', $makersNode)->item(0);
|
||||||
|
$makersHead = trim($makersHeadNode->textContent);
|
||||||
|
|
||||||
|
// 余分な文を消す
|
||||||
|
|
||||||
|
// OGPタイトルから作者名とサイト名を消す
|
||||||
|
$metadata->title = trim(preg_replace('~ \[[^\[\]]*\] (予告作品 )?\| DLsite(がるまに)?$~', '', $metadata->title));
|
||||||
|
|
||||||
|
// OGP説明文から定型文を消す
|
||||||
|
if (strpos($url, 'dlsite.com/eng/') || strpos($url, 'dlsite.com/ecchi-eng/')) {
|
||||||
|
$metadata->description = preg_replace('~DLsite.+ is a download shop for .+With a huge selection of products, we\'re sure you\'ll find whatever tickles your fancy\. DLsite is one of the greatest indie contents download shops in Japan\.$~', '', $metadata->description);
|
||||||
|
} else {
|
||||||
|
$metadata->description = preg_replace('~「DLsite.+」は.+のダウンロードショップ。お気に入りの作品をすぐダウンロードできてすぐ楽しめる!毎日更新しているのであなたが探している作品にきっと出会えます。国内最大級の二次元総合ダウンロードショップ「DLsite」!$~', '', $metadata->description);
|
||||||
|
}
|
||||||
|
$metadata->description = trim(strip_tags($metadata->description));
|
||||||
|
|
||||||
|
// 整形
|
||||||
|
$metadata->description = $makersHead . ': ' . $makers . PHP_EOL . $metadata->description;
|
||||||
|
$metadata->image = str_replace('img_sam.jpg', 'img_main.jpg', $metadata->image);
|
||||||
|
$metadata->tags = $this->extractTags($res->getBody());
|
||||||
|
|
||||||
|
return $metadata;
|
||||||
|
}
|
||||||
|
}
|
39
app/MetadataResolver/DeviantArtResolver.php
Normal file
39
app/MetadataResolver/DeviantArtResolver.php
Normal file
@@ -0,0 +1,39 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
namespace App\MetadataResolver;
|
||||||
|
|
||||||
|
use GuzzleHttp\Client;
|
||||||
|
|
||||||
|
class DeviantArtResolver 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('https://backend.deviantart.com/oembed?url=' . $url);
|
||||||
|
$data = json_decode($res->getBody()->getContents(), true);
|
||||||
|
$metadata = new Metadata();
|
||||||
|
|
||||||
|
$metadata->title = $data['title'] ?? '';
|
||||||
|
$metadata->description = 'By ' . $data['author_name'];
|
||||||
|
$metadata->image = $data['url'];
|
||||||
|
if (isset($data['tags'])) {
|
||||||
|
$metadata->tags = explode(', ', $data['tags']);
|
||||||
|
}
|
||||||
|
|
||||||
|
return $metadata;
|
||||||
|
}
|
||||||
|
}
|
40
app/MetadataResolver/FC2ContentsResolver.php
Normal file
40
app/MetadataResolver/FC2ContentsResolver.php
Normal file
@@ -0,0 +1,40 @@
|
|||||||
|
<?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;
|
||||||
|
}
|
||||||
|
}
|
40
app/MetadataResolver/FantiaResolver.php
Normal file
40
app/MetadataResolver/FantiaResolver.php
Normal file
@@ -0,0 +1,40 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
namespace App\MetadataResolver;
|
||||||
|
|
||||||
|
use GuzzleHttp\Client;
|
||||||
|
|
||||||
|
class FantiaResolver implements Resolver
|
||||||
|
{
|
||||||
|
/**
|
||||||
|
* @var Client
|
||||||
|
*/
|
||||||
|
private $client;
|
||||||
|
|
||||||
|
public function __construct(Client $client)
|
||||||
|
{
|
||||||
|
$this->client = $client;
|
||||||
|
}
|
||||||
|
|
||||||
|
public function resolve(string $url): Metadata
|
||||||
|
{
|
||||||
|
preg_match("~posts/(\d+)~", $url, $match);
|
||||||
|
$postId = $match[1];
|
||||||
|
|
||||||
|
$res = $this->client->get("https://fantia.jp/api/v1/posts/{$postId}");
|
||||||
|
$data = json_decode(str_replace('\r\n', '\n', (string) $res->getBody()), true);
|
||||||
|
$post = $data['post'];
|
||||||
|
|
||||||
|
$tags = array_map(function ($tag) {
|
||||||
|
return $tag['name'];
|
||||||
|
}, $post['tags']);
|
||||||
|
|
||||||
|
$metadata = new Metadata();
|
||||||
|
$metadata->title = $post['title'] ?? '';
|
||||||
|
$metadata->description = 'サークル: ' . $post['fanclub']['fanclub_name_with_creator_name'] . PHP_EOL . $post['comment'];
|
||||||
|
$metadata->image = str_replace('micro', 'main', $post['thumb_micro']) ?? '';
|
||||||
|
$metadata->tags = array_merge($tags, [$post['fanclub']['creator_name']]);
|
||||||
|
|
||||||
|
return $metadata;
|
||||||
|
}
|
||||||
|
}
|
105
app/MetadataResolver/FanzaResolver.php
Normal file
105
app/MetadataResolver/FanzaResolver.php
Normal file
@@ -0,0 +1,105 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
namespace App\MetadataResolver;
|
||||||
|
|
||||||
|
use GuzzleHttp\Client;
|
||||||
|
use Symfony\Component\DomCrawler\Crawler;
|
||||||
|
|
||||||
|
class FanzaResolver implements Resolver
|
||||||
|
{
|
||||||
|
/**
|
||||||
|
* @var Client
|
||||||
|
*/
|
||||||
|
private $client;
|
||||||
|
/**
|
||||||
|
* @var OGPResolver
|
||||||
|
*/
|
||||||
|
private $ogpResolver;
|
||||||
|
|
||||||
|
public function __construct(Client $client, OGPResolver $ogpResolver)
|
||||||
|
{
|
||||||
|
$this->client = $client;
|
||||||
|
$this->ogpResolver = $ogpResolver;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* arrayの各要素をtrim・スペースの_置換をした後、重複した値を削除してキーを詰め直す
|
||||||
|
*
|
||||||
|
* @param array $array
|
||||||
|
*
|
||||||
|
* @return array 処理されたarray
|
||||||
|
*/
|
||||||
|
public function array_finish(array $array): array
|
||||||
|
{
|
||||||
|
$array = array_map('trim', $array);
|
||||||
|
$array = array_map((function ($value) {
|
||||||
|
return str_replace(' ', '_', $value);
|
||||||
|
}), $array);
|
||||||
|
$array = array_unique($array);
|
||||||
|
$array = array_values($array);
|
||||||
|
|
||||||
|
return $array;
|
||||||
|
}
|
||||||
|
|
||||||
|
public function resolve(string $url): Metadata
|
||||||
|
{
|
||||||
|
$res = $this->client->get($url);
|
||||||
|
$html = (string) $res->getBody();
|
||||||
|
$crawler = new Crawler($html);
|
||||||
|
|
||||||
|
// 動画
|
||||||
|
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->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']));
|
||||||
|
|
||||||
|
return $metadata;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 同人
|
||||||
|
if (mb_strpos($url, 'www.dmm.co.jp/dc/doujin/-/detail/') !== false) {
|
||||||
|
$genre = $this->array_finish($crawler->filter('.m-productInformation a:not([href="#update-top"])')->extract(['_text']));
|
||||||
|
$genre = array_filter($genre, (function ($text) {
|
||||||
|
return !preg_match('~%OFF対象$~', $text);
|
||||||
|
}));
|
||||||
|
|
||||||
|
$metadata = new Metadata();
|
||||||
|
$metadata->title = $crawler->filter('meta[property="og:title"]')->attr('content');
|
||||||
|
$metadata->description = trim($crawler->filter('.summary__txt')->text(''));
|
||||||
|
$metadata->image = $crawler->filter('meta[property="og:image"]')->attr('content');
|
||||||
|
$metadata->tags = array_merge($genre, [$crawler->filter('.circleName__txt')->text('')]);
|
||||||
|
|
||||||
|
return $metadata;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 電子書籍
|
||||||
|
if (mb_strpos($url, 'book.dmm.co.jp/detail/') !== false) {
|
||||||
|
$metadata = new Metadata();
|
||||||
|
$metadata->title = trim($crawler->filter('#title')->text(''));
|
||||||
|
$metadata->description = trim($crawler->filter('.m-boxDetailProduct__info__story')->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('.m-boxDetailProductInfoMainList__description__list__item, .m-boxDetailProductInfo__list__description__item a')->extract(['_text']));
|
||||||
|
|
||||||
|
return $metadata;
|
||||||
|
}
|
||||||
|
|
||||||
|
// PCゲーム
|
||||||
|
if (mb_strpos($url, 'dlsoft.dmm.co.jp/detail/') !== false) {
|
||||||
|
$metadata = new Metadata();
|
||||||
|
$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']));
|
||||||
|
|
||||||
|
return $metadata;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 上で特に対応しなかったURL 画像の置換くらいはしておく
|
||||||
|
$metadata = $this->ogpResolver->parse($html);
|
||||||
|
$metadata->image = preg_replace("~(pr|ps)\.jpg$~", 'pl.jpg', $metadata->image);
|
||||||
|
|
||||||
|
return $metadata;
|
||||||
|
}
|
||||||
|
}
|
56
app/MetadataResolver/HentaiFoundryResolver.php
Normal file
56
app/MetadataResolver/HentaiFoundryResolver.php
Normal file
@@ -0,0 +1,56 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
namespace App\MetadataResolver;
|
||||||
|
|
||||||
|
use GuzzleHttp\Client;
|
||||||
|
use GuzzleHttp\Cookie\CookieJar;
|
||||||
|
use Symfony\Component\DomCrawler\Crawler;
|
||||||
|
|
||||||
|
class HentaiFoundryResolver implements Resolver
|
||||||
|
{
|
||||||
|
/**
|
||||||
|
* @var Client
|
||||||
|
*/
|
||||||
|
private $client;
|
||||||
|
/**
|
||||||
|
* @var OGPResolver
|
||||||
|
*/
|
||||||
|
private $ogpResolver;
|
||||||
|
|
||||||
|
public function __construct(Client $client, OGPResolver $ogpResolver)
|
||||||
|
{
|
||||||
|
$this->client = $client;
|
||||||
|
$this->ogpResolver = $ogpResolver;
|
||||||
|
}
|
||||||
|
|
||||||
|
private function nbsp2space(string $string): string
|
||||||
|
{
|
||||||
|
return str_replace("\xc2\xa0", ' ', $string);
|
||||||
|
}
|
||||||
|
|
||||||
|
private function br2nl(string $string): string
|
||||||
|
{
|
||||||
|
return str_replace('<br>', PHP_EOL, $string);
|
||||||
|
}
|
||||||
|
|
||||||
|
public function resolve(string $url): Metadata
|
||||||
|
{
|
||||||
|
$res = $this->client->get(
|
||||||
|
http_build_url($url, ['query' => 'enterAgree=1']),
|
||||||
|
['cookies' => new CookieJar()]
|
||||||
|
);
|
||||||
|
|
||||||
|
$metadata = new Metadata();
|
||||||
|
$crawler = new Crawler((string) $res->getBody());
|
||||||
|
|
||||||
|
$author = $crawler->filter('#picBox .boxtitle a')->text();
|
||||||
|
$description = trim(strip_tags($this->nbsp2space($this->br2nl($crawler->filter('.picDescript')->html()))));
|
||||||
|
|
||||||
|
$metadata->title = $crawler->filter('#picBox .boxtitle .imageTitle')->text();
|
||||||
|
$metadata->description = 'by ' . $author . PHP_EOL . $description;
|
||||||
|
$metadata->image = 'https:' . $crawler->filter('img[src^="//picture"]')->attr('src');
|
||||||
|
$metadata->tags = $crawler->filter('a[rel="tag"]')->extract('_text');
|
||||||
|
|
||||||
|
return $metadata;
|
||||||
|
}
|
||||||
|
}
|
60
app/MetadataResolver/IwaraResolver.php
Normal file
60
app/MetadataResolver/IwaraResolver.php
Normal file
@@ -0,0 +1,60 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
namespace App\MetadataResolver;
|
||||||
|
|
||||||
|
use GuzzleHttp\Client;
|
||||||
|
use Symfony\Component\DomCrawler\Crawler;
|
||||||
|
|
||||||
|
class IwaraResolver implements Resolver
|
||||||
|
{
|
||||||
|
/**
|
||||||
|
* @var Client
|
||||||
|
*/
|
||||||
|
private $client;
|
||||||
|
|
||||||
|
public function __construct(Client $client)
|
||||||
|
{
|
||||||
|
$this->client = $client;
|
||||||
|
}
|
||||||
|
|
||||||
|
public function resolve(string $url): Metadata
|
||||||
|
{
|
||||||
|
$res = $this->client->get($url);
|
||||||
|
$metadata = new Metadata();
|
||||||
|
$html = (string) $res->getBody();
|
||||||
|
$crawler = new Crawler($html);
|
||||||
|
|
||||||
|
$infoElements = $crawler->filter('#video-player + div, .field-name-field-video-url + div, .field-name-field-images + div');
|
||||||
|
$title = $infoElements->filter('h1.title')->text();
|
||||||
|
$author = $infoElements->filter('.username')->text();
|
||||||
|
$description = $infoElements->filter('.field-type-text-with-summary')->text('');
|
||||||
|
$tags = $infoElements->filter('a[href^="/videos"], a[href^="/images"]')->extract('_text');
|
||||||
|
// 役に立たないタグを削除する
|
||||||
|
$tags = array_values(array_diff($tags, ['Uncategorized', 'Other']));
|
||||||
|
array_push($tags, $author);
|
||||||
|
|
||||||
|
$metadata->title = $title;
|
||||||
|
$metadata->description = '投稿者: ' . $author . PHP_EOL . $description;
|
||||||
|
$metadata->tags = $tags;
|
||||||
|
|
||||||
|
// iwara video
|
||||||
|
if ($crawler->filter('#video-player')->count()) {
|
||||||
|
$metadata->image = 'https:' . $crawler->filter('#video-player')->attr('poster');
|
||||||
|
}
|
||||||
|
|
||||||
|
// youtube
|
||||||
|
if ($crawler->filter('iframe[src^="//www.youtube.com"]')->count()) {
|
||||||
|
if (preg_match('~youtube\.com/embed/(\S+)\?~', $crawler->filter('iframe[src^="//www.youtube.com"]')->attr('src'), $matches) === 1) {
|
||||||
|
$youtubeId = $matches[1];
|
||||||
|
$metadata->image = 'https://img.youtube.com/vi/' . $youtubeId . '/maxresdefault.jpg';
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// images
|
||||||
|
if ($crawler->filter('.field-name-field-images')->count()) {
|
||||||
|
$metadata->image = 'https:' . $crawler->filter('.field-name-field-images a')->first()->attr('href');
|
||||||
|
}
|
||||||
|
|
||||||
|
return $metadata;
|
||||||
|
}
|
||||||
|
}
|
36
app/MetadataResolver/Kb10uyShortStoryServerResolver.php
Normal file
36
app/MetadataResolver/Kb10uyShortStoryServerResolver.php
Normal file
@@ -0,0 +1,36 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
namespace App\MetadataResolver;
|
||||||
|
|
||||||
|
use GuzzleHttp\Client;
|
||||||
|
use Symfony\Component\DomCrawler\Crawler;
|
||||||
|
|
||||||
|
class Kb10uyShortStoryServerResolver implements Resolver
|
||||||
|
{
|
||||||
|
protected const EXCLUDED_TAGS = ['R-15', 'R-18'];
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @var Client
|
||||||
|
*/
|
||||||
|
private $client;
|
||||||
|
|
||||||
|
public function __construct(Client $client)
|
||||||
|
{
|
||||||
|
$this->client = $client;
|
||||||
|
}
|
||||||
|
|
||||||
|
public function resolve(string $url): Metadata
|
||||||
|
{
|
||||||
|
$res = $this->client->get($url);
|
||||||
|
$html = (string) $res->getBody();
|
||||||
|
$crawler = new Crawler($html);
|
||||||
|
$infoElement = $crawler->filter('div.post-info');
|
||||||
|
|
||||||
|
$metadata = new Metadata();
|
||||||
|
$metadata->title = $infoElement->filter('h1')->text();
|
||||||
|
$metadata->description = trim($infoElement->filter('p.summary')->text());
|
||||||
|
$metadata->tags = array_values(array_diff($infoElement->filter('ul.tags > li.tag > a')->extract('_text'), self::EXCLUDED_TAGS));
|
||||||
|
|
||||||
|
return $metadata;
|
||||||
|
}
|
||||||
|
}
|
51
app/MetadataResolver/KomifloResolver.php
Normal file
51
app/MetadataResolver/KomifloResolver.php
Normal file
@@ -0,0 +1,51 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
namespace App\MetadataResolver;
|
||||||
|
|
||||||
|
use GuzzleHttp\Client;
|
||||||
|
|
||||||
|
class KomifloResolver implements Resolver
|
||||||
|
{
|
||||||
|
/**
|
||||||
|
* @var Client
|
||||||
|
*/
|
||||||
|
private $client;
|
||||||
|
|
||||||
|
public function __construct(Client $client)
|
||||||
|
{
|
||||||
|
$this->client = $client;
|
||||||
|
}
|
||||||
|
|
||||||
|
public function resolve(string $url): Metadata
|
||||||
|
{
|
||||||
|
if (preg_match('~komiflo\.com(?:/#!)?/comics/(\\d+)~', $url, $matches) !== 1) {
|
||||||
|
throw new \RuntimeException("Unmatched URL Pattern: $url");
|
||||||
|
}
|
||||||
|
$id = $matches[1];
|
||||||
|
|
||||||
|
$res = $this->client->get('https://api.komiflo.com/content/id/' . $id);
|
||||||
|
$json = json_decode($res->getBody()->getContents(), true);
|
||||||
|
$metadata = new Metadata();
|
||||||
|
|
||||||
|
$metadata->title = $json['content']['data']['title'] ?? '';
|
||||||
|
$metadata->description = ($json['content']['attributes']['artists']['children'][0]['data']['name'] ?? '?') .
|
||||||
|
' - ' . ($json['content']['parents'][0]['data']['title'] ?? '?');
|
||||||
|
$metadata->image = 'https://t.komiflo.com/564_mobile_large_3x/' . $json['content']['named_imgs']['cover']['filename'];
|
||||||
|
|
||||||
|
// 作者情報
|
||||||
|
if (!empty($json['content']['attributes']['artists']['children'])) {
|
||||||
|
foreach ($json['content']['attributes']['artists']['children'] as $artist) {
|
||||||
|
$metadata->tags[] = preg_replace('/\s/', '_', $artist['data']['name']);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// タグ
|
||||||
|
if (!empty($json['content']['attributes']['tags']['children'])) {
|
||||||
|
foreach ($json['content']['attributes']['tags']['children'] as $tag) {
|
||||||
|
$metadata->tags[] = preg_replace('/\s/', '_', $tag['data']['name']);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return $metadata;
|
||||||
|
}
|
||||||
|
}
|
68
app/MetadataResolver/MelonbooksResolver.php
Normal file
68
app/MetadataResolver/MelonbooksResolver.php
Normal file
@@ -0,0 +1,68 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
namespace App\MetadataResolver;
|
||||||
|
|
||||||
|
use GuzzleHttp\Client;
|
||||||
|
use GuzzleHttp\Cookie\CookieJar;
|
||||||
|
|
||||||
|
class MelonbooksResolver 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
|
||||||
|
{
|
||||||
|
$cookieJar = CookieJar::fromArray(['AUTH_ADULT' => '1'], 'www.melonbooks.co.jp');
|
||||||
|
|
||||||
|
$res = $this->client->get($url, ['cookies' => $cookieJar]);
|
||||||
|
$metadata = $this->ogpResolver->parse($res->getBody());
|
||||||
|
|
||||||
|
$dom = new \DOMDocument();
|
||||||
|
@$dom->loadHTML(mb_convert_encoding($res->getBody(), 'HTML-ENTITIES', 'UTF-8'));
|
||||||
|
$xpath = new \DOMXPath($dom);
|
||||||
|
$descriptionNodelist = $xpath->query('//div[@id="description"]//p');
|
||||||
|
$specialDescriptionNodelist = $xpath->query('//div[@id="special_description"]//p');
|
||||||
|
|
||||||
|
// censoredフラグの除去
|
||||||
|
if (mb_strpos($metadata->image, '&c=1') !== false) {
|
||||||
|
$metadata->image = preg_replace('/&c=1/u', '', $metadata->image);
|
||||||
|
}
|
||||||
|
|
||||||
|
// 抽出
|
||||||
|
preg_match('~^(.+)((.+))の通販・購入はメロンブックス$~', $metadata->title, $match);
|
||||||
|
$title = $match[1];
|
||||||
|
$maker = $match[2];
|
||||||
|
|
||||||
|
// 整形
|
||||||
|
$description = 'サークル: ' . $maker . "\n";
|
||||||
|
|
||||||
|
if ($specialDescriptionNodelist->length !== 0) {
|
||||||
|
$description .= trim(str_replace('<br>', "\n", $specialDescriptionNodelist->item(0)->nodeValue)) . "\n";
|
||||||
|
if ($specialDescriptionNodelist->length === 2) {
|
||||||
|
$description .= "\n";
|
||||||
|
$description .= trim(str_replace('<br>', "\n", $specialDescriptionNodelist->item(1)->nodeValue)) . "\n";
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if ($descriptionNodelist->length !== 0) {
|
||||||
|
$description .= trim(str_replace('<br>', "\n", $descriptionNodelist->item(0)->nodeValue));
|
||||||
|
}
|
||||||
|
|
||||||
|
$metadata->title = $title;
|
||||||
|
$metadata->description = trim($description);
|
||||||
|
|
||||||
|
return $metadata;
|
||||||
|
}
|
||||||
|
}
|
26
app/MetadataResolver/Metadata.php
Normal file
26
app/MetadataResolver/Metadata.php
Normal file
@@ -0,0 +1,26 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
namespace App\MetadataResolver;
|
||||||
|
|
||||||
|
use Carbon\Carbon;
|
||||||
|
|
||||||
|
class Metadata
|
||||||
|
{
|
||||||
|
/** @var string タイトル */
|
||||||
|
public $title = '';
|
||||||
|
|
||||||
|
/** @var string 概要 */
|
||||||
|
public $description = '';
|
||||||
|
|
||||||
|
/** @var string サムネイルのURL */
|
||||||
|
public $image = '';
|
||||||
|
|
||||||
|
/** @var Carbon|null メタデータの有効期限 */
|
||||||
|
public $expires_at = null;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @var string[] タグ
|
||||||
|
* チェックインタグと同様に保存されるため、スペースや改行文字を含めてはいけません。
|
||||||
|
*/
|
||||||
|
public $tags = [];
|
||||||
|
}
|
124
app/MetadataResolver/MetadataResolver.php
Normal file
124
app/MetadataResolver/MetadataResolver.php
Normal file
@@ -0,0 +1,124 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
namespace App\MetadataResolver;
|
||||||
|
|
||||||
|
use GuzzleHttp\Client;
|
||||||
|
use GuzzleHttp\Exception\ClientException;
|
||||||
|
use GuzzleHttp\Exception\ServerException;
|
||||||
|
|
||||||
|
class MetadataResolver implements Resolver
|
||||||
|
{
|
||||||
|
public $rules = [
|
||||||
|
'~(((sp\.)?seiga\.nicovideo\.jp/seiga(/#!)?|nico\.ms))/im~' => NicoSeigaResolver::class,
|
||||||
|
'~nijie\.info/view(_popup)?\.php~' => NijieResolver::class,
|
||||||
|
'~komiflo\.com(/#!)?/comics/(\\d+)~' => KomifloResolver::class,
|
||||||
|
'~www\.melonbooks\.co\.jp/detail/detail\.php~' => MelonbooksResolver::class,
|
||||||
|
'~ec\.toranoana\.(jp|shop)/(tora|joshi)(_[rd]+)?/(ec|digi)/item/~' => ToranoanaResolver::class,
|
||||||
|
'~iwara\.tv/(videos|images)/.*~' => IwaraResolver::class,
|
||||||
|
'~www\.dlsite\.com/.*/(work|announce)/=/product_id/..\d+(\.html)?~' => DLsiteResolver::class,
|
||||||
|
'~www\.dlsite\.com/.*/dlaf/=(/.+/.+)?/link/work/aid/.+(/id)?/..\d+(\.html)?~' => DLsiteResolver::class,
|
||||||
|
'~www\.dlsite\.com/.*/dlaf/=/aid/.+/url/.+~' => DLsiteResolver::class,
|
||||||
|
'~dlsite\.jp/...tw/..\d+~' => DLsiteResolver::class,
|
||||||
|
'~www\.pixiv\.net/member_illust\.php\?illust_id=\d+~' => PixivResolver::class,
|
||||||
|
'~www\.pixiv\.net/(en/)?artworks/\d+~' => PixivResolver::class,
|
||||||
|
'~www\.pixiv\.net/user/\d+/series/\d+~' => PixivResolver::class,
|
||||||
|
'~fantia\.jp/posts/\d+~' => FantiaResolver::class,
|
||||||
|
'~dmm\.co\.jp/~' => FanzaResolver::class,
|
||||||
|
'~www\.patreon\.com/~' => PatreonResolver::class,
|
||||||
|
'~www\.deviantart\.com/.*/art/.*~' => DeviantArtResolver::class,
|
||||||
|
'~\.syosetu\.com/n\d+[a-z]{2,}~' => NarouResolver::class,
|
||||||
|
'~ci-en\.jp/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,
|
||||||
|
];
|
||||||
|
|
||||||
|
public $mimeTypes = [
|
||||||
|
'application/activity+json' => ActivityPubResolver::class,
|
||||||
|
'application/ld+json' => ActivityPubResolver::class,
|
||||||
|
'text/html' => OGPResolver::class,
|
||||||
|
'*/*' => OGPResolver::class
|
||||||
|
];
|
||||||
|
|
||||||
|
public $defaultResolver = OGPResolver::class;
|
||||||
|
|
||||||
|
public function resolve(string $url): Metadata
|
||||||
|
{
|
||||||
|
foreach ($this->rules as $pattern => $class) {
|
||||||
|
if (preg_match($pattern, $url) === 1) {
|
||||||
|
try {
|
||||||
|
/** @var Resolver $resolver */
|
||||||
|
$resolver = app($class);
|
||||||
|
|
||||||
|
return $resolver->resolve($url);
|
||||||
|
} catch (UnsupportedContentException $e) {
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
return $this->resolveWithAcceptHeader($url);
|
||||||
|
} catch (UnsupportedContentException $e) {
|
||||||
|
}
|
||||||
|
|
||||||
|
if (isset($this->defaultResolver)) {
|
||||||
|
/** @var Resolver $resolver */
|
||||||
|
$resolver = app($this->defaultResolver);
|
||||||
|
|
||||||
|
return $resolver->resolve($url);
|
||||||
|
}
|
||||||
|
|
||||||
|
throw new \UnexpectedValueException('URL not matched.');
|
||||||
|
}
|
||||||
|
|
||||||
|
public function resolveWithAcceptHeader(string $url): Metadata
|
||||||
|
{
|
||||||
|
try {
|
||||||
|
// Rails等はAcceptに */* が入っていると、ブラウザの適当なAcceptヘッダだと判断して全部無視してしまう。
|
||||||
|
// c.f. https://github.com/rails/rails/issues/9940
|
||||||
|
// そこでここでは */* を「Acceptヘッダを無視してきたレスポンス(よくある)」のハンドラとして扱い、
|
||||||
|
// Acceptヘッダには */* を足さないことにする。
|
||||||
|
$acceptTypes = array_diff(array_keys($this->mimeTypes), ['*/*']);
|
||||||
|
|
||||||
|
$client = app(Client::class);
|
||||||
|
$res = $client->request('GET', $url, [
|
||||||
|
'headers' => [
|
||||||
|
'Accept' => implode(', ', $acceptTypes)
|
||||||
|
]
|
||||||
|
]);
|
||||||
|
|
||||||
|
if ($res->getStatusCode() === 200) {
|
||||||
|
preg_match('/^[^;\s]+/', $res->getHeaderLine('Content-Type'), $matches);
|
||||||
|
$mimeType = $matches[0];
|
||||||
|
|
||||||
|
if (isset($this->mimeTypes[$mimeType])) {
|
||||||
|
$class = $this->mimeTypes[$mimeType];
|
||||||
|
$parser = app($class);
|
||||||
|
|
||||||
|
return $parser->parse($res->getBody());
|
||||||
|
}
|
||||||
|
|
||||||
|
if (isset($this->mimeTypes['*/*'])) {
|
||||||
|
$class = $this->mimeTypes['*/*'];
|
||||||
|
$parser = app($class);
|
||||||
|
|
||||||
|
return $parser->parse($res->getBody());
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
// code < 400 && code !== 200 => fallback
|
||||||
|
}
|
||||||
|
} catch (ClientException $e) {
|
||||||
|
// 406 Not Acceptable は多分Acceptが原因なので無視してフォールバック
|
||||||
|
if ($e->getResponse()->getStatusCode() !== 406) {
|
||||||
|
throw $e;
|
||||||
|
}
|
||||||
|
} catch (ServerException $e) {
|
||||||
|
// 5xx は変なAcceptが原因かもしれない(?)ので無視してフォールバック
|
||||||
|
}
|
||||||
|
|
||||||
|
throw new UnsupportedContentException();
|
||||||
|
}
|
||||||
|
}
|
56
app/MetadataResolver/NarouResolver.php
Normal file
56
app/MetadataResolver/NarouResolver.php
Normal file
@@ -0,0 +1,56 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
namespace App\MetadataResolver;
|
||||||
|
|
||||||
|
use GuzzleHttp\Client;
|
||||||
|
use GuzzleHttp\Cookie\CookieJar;
|
||||||
|
|
||||||
|
class NarouResolver 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
|
||||||
|
{
|
||||||
|
$cookieJar = CookieJar::fromArray(['over18' => 'yes'], '.syosetu.com');
|
||||||
|
|
||||||
|
$res = $this->client->get($url, ['cookies' => $cookieJar]);
|
||||||
|
$metadata = $this->ogpResolver->parse($res->getBody());
|
||||||
|
$metadata->description = '';
|
||||||
|
|
||||||
|
$dom = new \DOMDocument();
|
||||||
|
@$dom->loadHTML(mb_convert_encoding($res->getBody(), 'HTML-ENTITIES', 'ASCII,JIS,UTF-8,eucJP-win,SJIS-win'));
|
||||||
|
$xpath = new \DOMXPath($dom);
|
||||||
|
|
||||||
|
$description = [];
|
||||||
|
|
||||||
|
// 作者名
|
||||||
|
$writerNodes = $xpath->query('//*[contains(@class, "novel_writername")]');
|
||||||
|
if ($writerNodes->length !== 0 && !empty($writerNodes->item(0)->textContent)) {
|
||||||
|
$description[] = trim($writerNodes->item(0)->textContent);
|
||||||
|
}
|
||||||
|
|
||||||
|
// あらすじ
|
||||||
|
$exNodes = $xpath->query('//*[@id="novel_ex"]');
|
||||||
|
if ($exNodes->length !== 0 && !empty($exNodes->item(0)->textContent)) {
|
||||||
|
$summary = trim($exNodes->item(0)->textContent);
|
||||||
|
$description[] = mb_strimwidth($summary, 0, 101, '…'); // 100 + '…'(1)
|
||||||
|
}
|
||||||
|
|
||||||
|
$metadata->description = implode(' / ', $description);
|
||||||
|
|
||||||
|
return $metadata;
|
||||||
|
}
|
||||||
|
}
|
42
app/MetadataResolver/NicoSeigaResolver.php
Normal file
42
app/MetadataResolver/NicoSeigaResolver.php
Normal file
@@ -0,0 +1,42 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
namespace App\MetadataResolver;
|
||||||
|
|
||||||
|
use GuzzleHttp\Client;
|
||||||
|
use Symfony\Component\DomCrawler\Crawler;
|
||||||
|
|
||||||
|
class NicoSeigaResolver 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);
|
||||||
|
$html = (string)$res->getBody();
|
||||||
|
$metadata = $this->ogpResolver->parse($html);
|
||||||
|
$crawler = new Crawler($html);
|
||||||
|
|
||||||
|
// タグ
|
||||||
|
$excludeTags = ['R-15'];
|
||||||
|
$metadata->tags = array_values(array_diff($crawler->filter('.tag')->extract(['_text']), $excludeTags));
|
||||||
|
|
||||||
|
// ページURLからサムネイルURLに変換
|
||||||
|
preg_match('~https?://(?:(?:sp\\.)?seiga\\.nicovideo\\.jp/seiga(?:/#!)?|nico\\.ms)/im(\\d+)~', $url, $matches);
|
||||||
|
$metadata->image = "https://lohas.nicoseiga.jp/thumb/${matches[1]}l?";
|
||||||
|
|
||||||
|
return $metadata;
|
||||||
|
}
|
||||||
|
}
|
63
app/MetadataResolver/NijieResolver.php
Normal file
63
app/MetadataResolver/NijieResolver.php
Normal file
@@ -0,0 +1,63 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
namespace App\MetadataResolver;
|
||||||
|
|
||||||
|
use GuzzleHttp\Client;
|
||||||
|
use Symfony\Component\DomCrawler\Crawler;
|
||||||
|
|
||||||
|
class NijieResolver implements Resolver
|
||||||
|
{
|
||||||
|
/**
|
||||||
|
* @var Client
|
||||||
|
*/
|
||||||
|
protected $client;
|
||||||
|
/**
|
||||||
|
* @var OGPResolver
|
||||||
|
*/
|
||||||
|
private $ogpResolver;
|
||||||
|
|
||||||
|
public function __construct(Client $client, OGPResolver $ogpResolver)
|
||||||
|
{
|
||||||
|
$this->client = $client;
|
||||||
|
$this->ogpResolver = $ogpResolver;
|
||||||
|
}
|
||||||
|
|
||||||
|
public function resolve(string $url): Metadata
|
||||||
|
{
|
||||||
|
if (mb_strpos($url, '//sp.nijie.info') !== false) {
|
||||||
|
$url = preg_replace('~//sp\.nijie\.info~', '//nijie.info', $url);
|
||||||
|
}
|
||||||
|
if (mb_strpos($url, 'view_popup.php') !== false) {
|
||||||
|
$url = preg_replace('~view_popup\.php~', 'view.php', $url);
|
||||||
|
}
|
||||||
|
|
||||||
|
$res = $this->client->get($url);
|
||||||
|
$html = (string) $res->getBody();
|
||||||
|
$metadata = $this->ogpResolver->parse($html);
|
||||||
|
$crawler = new Crawler($html);
|
||||||
|
|
||||||
|
$json = $crawler->filter('script[type="application/ld+json"]')->first()->text();
|
||||||
|
|
||||||
|
// 改行がそのまま入っていることがあるのでデコード前にエスケープが必要
|
||||||
|
$data = json_decode(preg_replace('/\r?\n/', '\n', $json), true);
|
||||||
|
|
||||||
|
// DomCrawler内でjson内の日本語がHTMLエンティティに変換されるので、全要素に対してhtml_entity_decode
|
||||||
|
array_walk_recursive($data, function (&$v) {
|
||||||
|
$v = html_entity_decode($v);
|
||||||
|
});
|
||||||
|
|
||||||
|
$metadata->title = $data['name'];
|
||||||
|
$metadata->description = '投稿者: ' . $data['author']['name'] . PHP_EOL . $data['description'];
|
||||||
|
if (
|
||||||
|
isset($data['thumbnailUrl']) &&
|
||||||
|
!ends_with($data['thumbnailUrl'], '.gif') &&
|
||||||
|
!ends_with($data['thumbnailUrl'], '.mp4')
|
||||||
|
) {
|
||||||
|
// サムネイルからメイン画像に
|
||||||
|
$metadata->image = str_replace('__rs_l160x160/', '', $data['thumbnailUrl']);
|
||||||
|
}
|
||||||
|
$metadata->tags = $crawler->filter('#view-tag span.tag_name')->extract('_text');
|
||||||
|
|
||||||
|
return $metadata;
|
||||||
|
}
|
||||||
|
}
|
59
app/MetadataResolver/OGPResolver.php
Normal file
59
app/MetadataResolver/OGPResolver.php
Normal file
@@ -0,0 +1,59 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
namespace App\MetadataResolver;
|
||||||
|
|
||||||
|
use GuzzleHttp\Client;
|
||||||
|
|
||||||
|
class OGPResolver implements Resolver, Parser
|
||||||
|
{
|
||||||
|
/**
|
||||||
|
* @var Client
|
||||||
|
*/
|
||||||
|
private $client;
|
||||||
|
|
||||||
|
public function __construct(Client $client)
|
||||||
|
{
|
||||||
|
$this->client = $client;
|
||||||
|
}
|
||||||
|
|
||||||
|
public function resolve(string $url): Metadata
|
||||||
|
{
|
||||||
|
return $this->parse($this->client->get($url)->getBody());
|
||||||
|
}
|
||||||
|
|
||||||
|
public function parse(string $html): Metadata
|
||||||
|
{
|
||||||
|
$dom = new \DOMDocument();
|
||||||
|
@$dom->loadHTML(mb_convert_encoding($html, 'HTML-ENTITIES', 'ASCII,JIS,UTF-8,eucJP-win,SJIS-win'));
|
||||||
|
$xpath = new \DOMXPath($dom);
|
||||||
|
|
||||||
|
$metadata = new Metadata();
|
||||||
|
|
||||||
|
$metadata->title = $this->findContent($xpath, '//meta[@*="og:title"]', '//meta[@*="twitter:title"]');
|
||||||
|
if (empty($metadata->title)) {
|
||||||
|
$nodes = $xpath->query('//title');
|
||||||
|
if ($nodes->length !== 0) {
|
||||||
|
$metadata->title = $nodes->item(0)->textContent;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
$metadata->description = $this->findContent($xpath, '//meta[@*="og:description"]', '//meta[@*="twitter:description"]', '//meta[@name="description"]');
|
||||||
|
$metadata->image = $this->findContent($xpath, '//meta[@*="og:image"]', '//meta[@*="twitter:image"]');
|
||||||
|
|
||||||
|
return $metadata;
|
||||||
|
}
|
||||||
|
|
||||||
|
private function findContent(\DOMXPath $xpath, string ...$expressions)
|
||||||
|
{
|
||||||
|
foreach ($expressions as $expression) {
|
||||||
|
$nodes = $xpath->query($expression);
|
||||||
|
foreach ($nodes as $node) {
|
||||||
|
$content = $node->getAttribute('content');
|
||||||
|
if (!empty($content)) {
|
||||||
|
return $content;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return '';
|
||||||
|
}
|
||||||
|
}
|
8
app/MetadataResolver/Parser.php
Normal file
8
app/MetadataResolver/Parser.php
Normal file
@@ -0,0 +1,8 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
namespace App\MetadataResolver;
|
||||||
|
|
||||||
|
interface Parser
|
||||||
|
{
|
||||||
|
public function parse(string $body): Metadata;
|
||||||
|
}
|
38
app/MetadataResolver/PatreonResolver.php
Normal file
38
app/MetadataResolver/PatreonResolver.php
Normal file
@@ -0,0 +1,38 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
namespace App\MetadataResolver;
|
||||||
|
|
||||||
|
use Carbon\Carbon;
|
||||||
|
use GuzzleHttp\Client;
|
||||||
|
|
||||||
|
class PatreonResolver 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());
|
||||||
|
|
||||||
|
parse_str(parse_url($metadata->image, PHP_URL_QUERY), $query);
|
||||||
|
if (isset($query['token-time'])) {
|
||||||
|
$expires_at_unixtime = $query['token-time'];
|
||||||
|
$metadata->expires_at = Carbon::createFromTimestamp($expires_at_unixtime);
|
||||||
|
}
|
||||||
|
|
||||||
|
return $metadata;
|
||||||
|
}
|
||||||
|
}
|
85
app/MetadataResolver/PixivResolver.php
Normal file
85
app/MetadataResolver/PixivResolver.php
Normal file
@@ -0,0 +1,85 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
namespace App\MetadataResolver;
|
||||||
|
|
||||||
|
use GuzzleHttp\Client;
|
||||||
|
|
||||||
|
class PixivResolver implements Resolver
|
||||||
|
{
|
||||||
|
/**
|
||||||
|
* @var Client
|
||||||
|
*/
|
||||||
|
private $client;
|
||||||
|
/**
|
||||||
|
* @var OGPResolver
|
||||||
|
*/
|
||||||
|
private $ogpResolver;
|
||||||
|
|
||||||
|
public function __construct(Client $client, OGPResolver $ogpResolver)
|
||||||
|
{
|
||||||
|
$this->client = $client;
|
||||||
|
$this->ogpResolver = $ogpResolver;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 直リン可能な pixiv.cat のプロキシ URL に変換する
|
||||||
|
* HUGE THANKS TO PIXIV.CAT!
|
||||||
|
*
|
||||||
|
* @param string $pixivUrl i.pximg URL
|
||||||
|
*
|
||||||
|
* @return string i.pixiv.cat URL
|
||||||
|
*/
|
||||||
|
public function proxize(string $pixivUrl): string
|
||||||
|
{
|
||||||
|
return str_replace('i.pximg.net', 'i.pixiv.cat', $pixivUrl);
|
||||||
|
}
|
||||||
|
|
||||||
|
public function resolve(string $url): Metadata
|
||||||
|
{
|
||||||
|
if (preg_match('~www\.pixiv\.net/user/\d+/series/\d+~', $url, $matches)) {
|
||||||
|
$res = $this->client->get($url);
|
||||||
|
$metadata = $this->ogpResolver->parse($res->getBody());
|
||||||
|
$metadata->image = $this->proxize($metadata->image);
|
||||||
|
|
||||||
|
return $metadata;
|
||||||
|
}
|
||||||
|
|
||||||
|
$page = 0;
|
||||||
|
if (preg_match('~www\.pixiv\.net/(en/)?artworks/(?P<illustId>\d+)~', $url, $matches)) {
|
||||||
|
$illustId = $matches['illustId'];
|
||||||
|
} else {
|
||||||
|
parse_str(parse_url($url, PHP_URL_QUERY), $params);
|
||||||
|
$illustId = $params['illust_id'];
|
||||||
|
|
||||||
|
// 漫画ページ(ページ数はmanga_bigならあるかも)
|
||||||
|
if ($params['mode'] === 'manga_big' || $params['mode'] === 'manga') {
|
||||||
|
$page = $params['page'] ?? 0;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
$res = $this->client->get('https://www.pixiv.net/ajax/illust/' . $illustId);
|
||||||
|
$json = json_decode($res->getBody()->getContents(), true);
|
||||||
|
$metadata = new Metadata();
|
||||||
|
|
||||||
|
$metadata->title = $json['body']['illustTitle'] ?? '';
|
||||||
|
$metadata->description = '投稿者: ' . $json['body']['userName'] . PHP_EOL . strip_tags(str_replace('<br />', PHP_EOL, $json['body']['illustComment'] ?? ''));
|
||||||
|
$metadata->image = $this->proxize($json['body']['urls']['regular'] ?? '');
|
||||||
|
|
||||||
|
// ページ数の指定がある場合は画像URLをそのページにする
|
||||||
|
if ($page != 0) {
|
||||||
|
$metadata->image = str_replace('_p0', '_p' . $page, $metadata->image);
|
||||||
|
}
|
||||||
|
|
||||||
|
// タグ
|
||||||
|
if (!empty($json['body']['tags']['tags'])) {
|
||||||
|
foreach ($json['body']['tags']['tags'] as $tag) {
|
||||||
|
// 一部の固定キーワードは無視
|
||||||
|
if (array_search($tag['tag'], ['R-18', 'イラスト', 'pixiv', 'ピクシブ'], true) === false) {
|
||||||
|
$metadata->tags[] = preg_replace('/\s/', '_', $tag['tag']);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return $metadata;
|
||||||
|
}
|
||||||
|
}
|
40
app/MetadataResolver/PlurkResolver.php
Normal file
40
app/MetadataResolver/PlurkResolver.php
Normal file
@@ -0,0 +1,40 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
namespace App\MetadataResolver;
|
||||||
|
|
||||||
|
use GuzzleHttp\Client;
|
||||||
|
|
||||||
|
class PlurkResolver 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);
|
||||||
|
$imageNode = $xpath->query('//div[@class="text_holder"]/a[1]')->item(0);
|
||||||
|
|
||||||
|
if ($imageNode) {
|
||||||
|
$metadata->image = $imageNode->getAttribute('href');
|
||||||
|
}
|
||||||
|
|
||||||
|
return $metadata;
|
||||||
|
}
|
||||||
|
}
|
8
app/MetadataResolver/Resolver.php
Normal file
8
app/MetadataResolver/Resolver.php
Normal file
@@ -0,0 +1,8 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
namespace App\MetadataResolver;
|
||||||
|
|
||||||
|
interface Resolver
|
||||||
|
{
|
||||||
|
public function resolve(string $url): Metadata;
|
||||||
|
}
|
40
app/MetadataResolver/SteamResolver.php
Normal file
40
app/MetadataResolver/SteamResolver.php
Normal file
@@ -0,0 +1,40 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
namespace App\MetadataResolver;
|
||||||
|
|
||||||
|
use GuzzleHttp\Client;
|
||||||
|
|
||||||
|
class SteamResolver implements Resolver
|
||||||
|
{
|
||||||
|
/**
|
||||||
|
* @var Client
|
||||||
|
*/
|
||||||
|
private $client;
|
||||||
|
|
||||||
|
public function __construct(Client $client)
|
||||||
|
{
|
||||||
|
$this->client = $client;
|
||||||
|
}
|
||||||
|
|
||||||
|
public function resolve(string $url): Metadata
|
||||||
|
{
|
||||||
|
if (preg_match('~store\.steampowered\.com/app/(\d+)~', $url, $matches) !== 1) {
|
||||||
|
throw new \RuntimeException("Unmatched URL Pattern: $url");
|
||||||
|
}
|
||||||
|
$appid = $matches[1];
|
||||||
|
|
||||||
|
$res = $this->client->get('https://store.steampowered.com/api/appdetails/?l=japanese&appids=' . $appid);
|
||||||
|
$json = json_decode($res->getBody()->getContents(), true);
|
||||||
|
if ($json[$appid]['success'] === false) {
|
||||||
|
throw new \RuntimeException("API response [$appid][success] is false: $url");
|
||||||
|
}
|
||||||
|
$data = $json[$appid]['data'];
|
||||||
|
$metadata = new Metadata();
|
||||||
|
|
||||||
|
$metadata->title = $data['name'] ?? '';
|
||||||
|
$metadata->description = strip_tags(str_replace('<br />', PHP_EOL, html_entity_decode($data['short_description'] ?? '')));
|
||||||
|
$metadata->image = $data['header_image'] ?? '';
|
||||||
|
|
||||||
|
return $metadata;
|
||||||
|
}
|
||||||
|
}
|
40
app/MetadataResolver/ToranoanaResolver.php
Normal file
40
app/MetadataResolver/ToranoanaResolver.php
Normal file
@@ -0,0 +1,40 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
namespace App\MetadataResolver;
|
||||||
|
|
||||||
|
use GuzzleHttp\Client;
|
||||||
|
use GuzzleHttp\Cookie\CookieJar;
|
||||||
|
|
||||||
|
class ToranoanaResolver 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);
|
||||||
|
$imgNode = $xpath->query('//*[@id="preview"]//img')->item(0);
|
||||||
|
if ($imgNode !== null) {
|
||||||
|
$metadata->image = $imgNode->getAttribute('src');
|
||||||
|
}
|
||||||
|
|
||||||
|
return $metadata;
|
||||||
|
}
|
||||||
|
}
|
12
app/MetadataResolver/UnsupportedContentException.php
Normal file
12
app/MetadataResolver/UnsupportedContentException.php
Normal file
@@ -0,0 +1,12 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
namespace App\MetadataResolver;
|
||||||
|
|
||||||
|
use Exception;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* このResolverやParserが対応していないサイトであったことを表わします。
|
||||||
|
*/
|
||||||
|
class UnsupportedContentException extends Exception
|
||||||
|
{
|
||||||
|
}
|
44
app/MetadataResolver/XtubeResolver.php
Normal file
44
app/MetadataResolver/XtubeResolver.php
Normal file
@@ -0,0 +1,44 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
namespace App\MetadataResolver;
|
||||||
|
|
||||||
|
use GuzzleHttp\Client;
|
||||||
|
use Symfony\Component\DomCrawler\Crawler;
|
||||||
|
|
||||||
|
class XtubeResolver 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
|
||||||
|
{
|
||||||
|
if (preg_match('~www\.xtube\.com/video-watch/.*-(\d+)$~', $url) !== 1) {
|
||||||
|
throw new \RuntimeException("Unmatched URL Pattern: $url");
|
||||||
|
}
|
||||||
|
|
||||||
|
$res = $this->client->get($url);
|
||||||
|
$html = (string) $res->getBody();
|
||||||
|
$metadata = $this->ogpResolver->parse($html);
|
||||||
|
$crawler = new Crawler($html);
|
||||||
|
|
||||||
|
$metadata->title = trim($crawler->filter('.underPlayerRateForm h1')->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'));
|
||||||
|
|
||||||
|
return $metadata;
|
||||||
|
}
|
||||||
|
}
|
27
app/Policies/EjaculationPolicy.php
Normal file
27
app/Policies/EjaculationPolicy.php
Normal file
@@ -0,0 +1,27 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
namespace App\Policies;
|
||||||
|
|
||||||
|
use App\Ejaculation;
|
||||||
|
use App\User;
|
||||||
|
use Illuminate\Auth\Access\HandlesAuthorization;
|
||||||
|
|
||||||
|
class EjaculationPolicy
|
||||||
|
{
|
||||||
|
use HandlesAuthorization;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Create a new policy instance.
|
||||||
|
*
|
||||||
|
* @return void
|
||||||
|
*/
|
||||||
|
public function __construct()
|
||||||
|
{
|
||||||
|
//
|
||||||
|
}
|
||||||
|
|
||||||
|
public function edit(User $user, Ejaculation $ejaculation): bool
|
||||||
|
{
|
||||||
|
return $user->id === $ejaculation->user_id;
|
||||||
|
}
|
||||||
|
}
|
@@ -2,7 +2,10 @@
|
|||||||
|
|
||||||
namespace App\Providers;
|
namespace App\Providers;
|
||||||
|
|
||||||
|
use App\MetadataResolver\MetadataResolver;
|
||||||
|
use Illuminate\Support\Facades\Blade;
|
||||||
use Illuminate\Support\ServiceProvider;
|
use Illuminate\Support\ServiceProvider;
|
||||||
|
use Parsedown;
|
||||||
|
|
||||||
class AppServiceProvider extends ServiceProvider
|
class AppServiceProvider extends ServiceProvider
|
||||||
{
|
{
|
||||||
@@ -13,7 +16,9 @@ class AppServiceProvider extends ServiceProvider
|
|||||||
*/
|
*/
|
||||||
public function boot()
|
public function boot()
|
||||||
{
|
{
|
||||||
//
|
Blade::directive('parsedown', function ($expression) {
|
||||||
|
return "<?php echo app('parsedown')->text($expression); ?>";
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -23,6 +28,11 @@ class AppServiceProvider extends ServiceProvider
|
|||||||
*/
|
*/
|
||||||
public function register()
|
public function register()
|
||||||
{
|
{
|
||||||
//
|
$this->app->singleton(MetadataResolver::class, function ($app) {
|
||||||
|
return new MetadataResolver();
|
||||||
|
});
|
||||||
|
$this->app->singleton('parsedown', function () {
|
||||||
|
return Parsedown::instance();
|
||||||
|
});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@@ -2,8 +2,10 @@
|
|||||||
|
|
||||||
namespace App\Providers;
|
namespace App\Providers;
|
||||||
|
|
||||||
use Illuminate\Support\Facades\Gate;
|
use App\Ejaculation;
|
||||||
|
use App\Policies\EjaculationPolicy;
|
||||||
use Illuminate\Foundation\Support\Providers\AuthServiceProvider as ServiceProvider;
|
use Illuminate\Foundation\Support\Providers\AuthServiceProvider as ServiceProvider;
|
||||||
|
use Illuminate\Support\Facades\Gate;
|
||||||
|
|
||||||
class AuthServiceProvider extends ServiceProvider
|
class AuthServiceProvider extends ServiceProvider
|
||||||
{
|
{
|
||||||
@@ -14,6 +16,7 @@ class AuthServiceProvider extends ServiceProvider
|
|||||||
*/
|
*/
|
||||||
protected $policies = [
|
protected $policies = [
|
||||||
'App\Model' => 'App\Policies\ModelPolicy',
|
'App\Model' => 'App\Policies\ModelPolicy',
|
||||||
|
Ejaculation::class => EjaculationPolicy::class,
|
||||||
];
|
];
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -25,6 +28,8 @@ class AuthServiceProvider extends ServiceProvider
|
|||||||
{
|
{
|
||||||
$this->registerPolicies();
|
$this->registerPolicies();
|
||||||
|
|
||||||
//
|
Gate::define('admin', function ($user) {
|
||||||
|
return $user->is_admin;
|
||||||
|
});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@@ -2,8 +2,8 @@
|
|||||||
|
|
||||||
namespace App\Providers;
|
namespace App\Providers;
|
||||||
|
|
||||||
use Illuminate\Support\ServiceProvider;
|
|
||||||
use Illuminate\Support\Facades\Broadcast;
|
use Illuminate\Support\Facades\Broadcast;
|
||||||
|
use Illuminate\Support\ServiceProvider;
|
||||||
|
|
||||||
class BroadcastServiceProvider extends ServiceProvider
|
class BroadcastServiceProvider extends ServiceProvider
|
||||||
{
|
{
|
||||||
|
@@ -2,8 +2,8 @@
|
|||||||
|
|
||||||
namespace App\Providers;
|
namespace App\Providers;
|
||||||
|
|
||||||
use Illuminate\Support\Facades\Event;
|
|
||||||
use Illuminate\Foundation\Support\Providers\EventServiceProvider as ServiceProvider;
|
use Illuminate\Foundation\Support\Providers\EventServiceProvider as ServiceProvider;
|
||||||
|
use Illuminate\Support\Facades\Event;
|
||||||
|
|
||||||
class EventServiceProvider extends ServiceProvider
|
class EventServiceProvider extends ServiceProvider
|
||||||
{
|
{
|
||||||
@@ -13,9 +13,9 @@ class EventServiceProvider extends ServiceProvider
|
|||||||
* @var array
|
* @var array
|
||||||
*/
|
*/
|
||||||
protected $listen = [
|
protected $listen = [
|
||||||
'App\Events\Event' => [
|
'App\Events\LinkDiscovered' => [
|
||||||
'App\Listeners\EventListener',
|
'App\Listeners\LinkCollector'
|
||||||
],
|
]
|
||||||
];
|
];
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
@@ -2,8 +2,8 @@
|
|||||||
|
|
||||||
namespace App\Providers;
|
namespace App\Providers;
|
||||||
|
|
||||||
use Illuminate\Support\Facades\Route;
|
|
||||||
use Illuminate\Foundation\Support\Providers\RouteServiceProvider as ServiceProvider;
|
use Illuminate\Foundation\Support\Providers\RouteServiceProvider as ServiceProvider;
|
||||||
|
use Illuminate\Support\Facades\Route;
|
||||||
|
|
||||||
class RouteServiceProvider extends ServiceProvider
|
class RouteServiceProvider extends ServiceProvider
|
||||||
{
|
{
|
||||||
|
@@ -2,7 +2,7 @@
|
|||||||
|
|
||||||
namespace App\Providers;
|
namespace App\Providers;
|
||||||
|
|
||||||
use App\Http\ViewComposers\ProfileComposer;
|
use App\Http\ViewComposers\ProfileStatsComposer;
|
||||||
use Illuminate\Support\Facades\View;
|
use Illuminate\Support\Facades\View;
|
||||||
use Illuminate\Support\ServiceProvider;
|
use Illuminate\Support\ServiceProvider;
|
||||||
|
|
||||||
@@ -15,7 +15,7 @@ class ViewComposerServiceProvider extends ServiceProvider
|
|||||||
*/
|
*/
|
||||||
public function boot()
|
public function boot()
|
||||||
{
|
{
|
||||||
View::composer('components.profile', ProfileComposer::class);
|
View::composer('components.profile-stats', ProfileStatsComposer::class);
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
@@ -11,6 +11,9 @@ class Tag extends Model
|
|||||||
protected $fillable = [
|
protected $fillable = [
|
||||||
'name'
|
'name'
|
||||||
];
|
];
|
||||||
|
protected $visible = [
|
||||||
|
'name'
|
||||||
|
];
|
||||||
|
|
||||||
public function ejaculations()
|
public function ejaculations()
|
||||||
{
|
{
|
||||||
|
18
app/User.php
18
app/User.php
@@ -2,8 +2,8 @@
|
|||||||
|
|
||||||
namespace App;
|
namespace App;
|
||||||
|
|
||||||
use Illuminate\Notifications\Notifiable;
|
|
||||||
use Illuminate\Foundation\Auth\User as Authenticatable;
|
use Illuminate\Foundation\Auth\User as Authenticatable;
|
||||||
|
use Illuminate\Notifications\Notifiable;
|
||||||
use Illuminate\Support\Facades\Auth;
|
use Illuminate\Support\Facades\Auth;
|
||||||
|
|
||||||
class User extends Authenticatable
|
class User extends Authenticatable
|
||||||
@@ -20,6 +20,7 @@ class User extends Authenticatable
|
|||||||
'is_protected', 'accept_analytics',
|
'is_protected', 'accept_analytics',
|
||||||
'display_name', 'description',
|
'display_name', 'description',
|
||||||
'twitter_id', 'twitter_name',
|
'twitter_id', 'twitter_name',
|
||||||
|
'private_likes',
|
||||||
];
|
];
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -36,10 +37,11 @@ class User extends Authenticatable
|
|||||||
* @param int $size 画像サイズ
|
* @param int $size 画像サイズ
|
||||||
* @return string Gravatar 画像URL
|
* @return string Gravatar 画像URL
|
||||||
*/
|
*/
|
||||||
public function getProfileImageUrl($size = 30) : string
|
public function getProfileImageUrl($size = 30): string
|
||||||
{
|
{
|
||||||
$hash = md5(strtolower(trim($this->email)));
|
$hash = md5(strtolower(trim($this->email)));
|
||||||
return '//www.gravatar.com/avatar/' . $hash . '?s=' . $size;
|
|
||||||
|
return '//www.gravatar.com/avatar/' . $hash . '?s=' . $size . '&d=retro';
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -50,4 +52,14 @@ class User extends Authenticatable
|
|||||||
{
|
{
|
||||||
return Auth::check() && $this->id === Auth::user()->id;
|
return Auth::check() && $this->id === Auth::user()->id;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public function ejaculations()
|
||||||
|
{
|
||||||
|
return $this->hasMany(Ejaculation::class);
|
||||||
|
}
|
||||||
|
|
||||||
|
public function likes()
|
||||||
|
{
|
||||||
|
return $this->hasMany(Like::class);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
@@ -24,6 +24,7 @@ class Formatter
|
|||||||
$days = floor($value / 86400);
|
$days = floor($value / 86400);
|
||||||
$hours = floor($value % 86400 / 3600);
|
$hours = floor($value % 86400 / 3600);
|
||||||
$minutes = floor($value % 3600 / 60);
|
$minutes = floor($value % 3600 / 60);
|
||||||
|
|
||||||
return "{$days}日 {$hours}時間 {$minutes}分";
|
return "{$days}日 {$hours}時間 {$minutes}分";
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -34,6 +35,44 @@ class Formatter
|
|||||||
*/
|
*/
|
||||||
public function linkify($text)
|
public function linkify($text)
|
||||||
{
|
{
|
||||||
return $this->linkify->processUrls($text);
|
return $this->linkify->processUrls($text, ['attr' => ['target' => '_blank', 'rel' => 'noopener']]);
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
/**
|
||||||
|
* URLを正規化します。
|
||||||
|
* @param string $url URL
|
||||||
|
* @return string 正規化されたURL
|
||||||
|
*/
|
||||||
|
public function normalizeUrl($url)
|
||||||
|
{
|
||||||
|
// Decode
|
||||||
|
$url = urldecode($url);
|
||||||
|
|
||||||
|
// Remove Hashbang
|
||||||
|
$url = preg_replace('~/#!/~u', '/', $url);
|
||||||
|
|
||||||
|
// Sort query parameters
|
||||||
|
$parts = parse_url($url);
|
||||||
|
if (!empty($parts['query'])) {
|
||||||
|
// Remove query parameters
|
||||||
|
$url = str_replace_last('?' . $parts['query'], '', $url);
|
||||||
|
if (!empty($parts['fragment'])) {
|
||||||
|
// Remove fragment identifier
|
||||||
|
$url = str_replace_last('#' . $parts['fragment'], '', $url);
|
||||||
|
} else {
|
||||||
|
// "http://example.com/?query#" の場合 $parts['fragment'] は unset になるので、個別に判定して除去する必要がある
|
||||||
|
$url = preg_replace('/#\z/u', '', $url);
|
||||||
|
}
|
||||||
|
|
||||||
|
parse_str($parts['query'], $params);
|
||||||
|
ksort($params);
|
||||||
|
|
||||||
|
$url = $url . '?' . http_build_query($params);
|
||||||
|
if (!empty($parts['fragment'])) {
|
||||||
|
$url .= '#' . $parts['fragment'];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return $url;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
@@ -5,19 +5,28 @@
|
|||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"type": "project",
|
"type": "project",
|
||||||
"require": {
|
"require": {
|
||||||
"php": ">=7.0.0",
|
"php": ">=7.1.0",
|
||||||
|
"anhskohbo/no-captcha": "^3.0",
|
||||||
|
"doctrine/dbal": "^2.9",
|
||||||
|
"fideloper/proxy": "~3.3",
|
||||||
"guzzlehttp/guzzle": "^6.3",
|
"guzzlehttp/guzzle": "^6.3",
|
||||||
|
"jakeasmith/http_build_url": "^1.0",
|
||||||
"laravel/framework": "5.5.*",
|
"laravel/framework": "5.5.*",
|
||||||
"laravel/tinker": "~1.0",
|
"laravel/tinker": "~1.0",
|
||||||
"misd/linkify": "^1.1",
|
"misd/linkify": "^1.1",
|
||||||
"parsedown/laravel": "~1.0"
|
"staudenmeir/eloquent-eager-limit": "^1.0",
|
||||||
|
"symfony/css-selector": "^4.3",
|
||||||
|
"symfony/dom-crawler": "^4.3"
|
||||||
},
|
},
|
||||||
"require-dev": {
|
"require-dev": {
|
||||||
"barryvdh/laravel-debugbar": "^3.1",
|
"barryvdh/laravel-debugbar": "^3.1",
|
||||||
|
"barryvdh/laravel-ide-helper": "^2.5",
|
||||||
"filp/whoops": "~2.0",
|
"filp/whoops": "~2.0",
|
||||||
|
"friendsofphp/php-cs-fixer": "^2.14",
|
||||||
"fzaninotto/faker": "~1.4",
|
"fzaninotto/faker": "~1.4",
|
||||||
"mockery/mockery": "~1.0",
|
"mockery/mockery": "~1.0",
|
||||||
"phpunit/phpunit": "~6.0"
|
"phpunit/phpunit": "~6.0",
|
||||||
|
"symfony/thanks": "^1.0"
|
||||||
},
|
},
|
||||||
"autoload": {
|
"autoload": {
|
||||||
"classmap": [
|
"classmap": [
|
||||||
@@ -42,6 +51,12 @@
|
|||||||
"post-autoload-dump": [
|
"post-autoload-dump": [
|
||||||
"Illuminate\\Foundation\\ComposerScripts::postAutoloadDump",
|
"Illuminate\\Foundation\\ComposerScripts::postAutoloadDump",
|
||||||
"@php artisan package:discover"
|
"@php artisan package:discover"
|
||||||
|
],
|
||||||
|
"fix": [
|
||||||
|
"php-cs-fixer fix --config=.php_cs.dist"
|
||||||
|
],
|
||||||
|
"test": [
|
||||||
|
"phpunit"
|
||||||
]
|
]
|
||||||
},
|
},
|
||||||
"config": {
|
"config": {
|
||||||
|
3480
composer.lock
generated
3480
composer.lock
generated
File diff suppressed because it is too large
Load Diff
12
database/factories/EjaculationFactory.php
Normal file
12
database/factories/EjaculationFactory.php
Normal file
@@ -0,0 +1,12 @@
|
|||||||
|
<?php
|
||||||
|
/** @var \Illuminate\Database\Eloquent\Factory $factory */
|
||||||
|
|
||||||
|
use App\Ejaculation;
|
||||||
|
use Faker\Generator as Faker;
|
||||||
|
|
||||||
|
$factory->define(Ejaculation::class, function (Faker $faker) {
|
||||||
|
return [
|
||||||
|
'ejaculated_date' => $faker->date('Y-m-d H:i:s'),
|
||||||
|
'note' => $faker->text,
|
||||||
|
];
|
||||||
|
});
|
10
database/factories/LikeFactory.php
Normal file
10
database/factories/LikeFactory.php
Normal file
@@ -0,0 +1,10 @@
|
|||||||
|
<?php
|
||||||
|
/** @var \Illuminate\Database\Eloquent\Factory $factory */
|
||||||
|
|
||||||
|
use Faker\Generator as Faker;
|
||||||
|
|
||||||
|
$factory->define(App\Like::class, function (Faker $faker) {
|
||||||
|
return [
|
||||||
|
//
|
||||||
|
];
|
||||||
|
});
|
@@ -1,24 +0,0 @@
|
|||||||
<?php
|
|
||||||
|
|
||||||
/*
|
|
||||||
|--------------------------------------------------------------------------
|
|
||||||
| Model Factories
|
|
||||||
|--------------------------------------------------------------------------
|
|
||||||
|
|
|
||||||
| Here you may define all of your model factories. Model factories give
|
|
||||||
| you a convenient way to create models for testing and seeding your
|
|
||||||
| database. Just tell the factory how a default model should look.
|
|
||||||
|
|
|
||||||
*/
|
|
||||||
|
|
||||||
/** @var \Illuminate\Database\Eloquent\Factory $factory */
|
|
||||||
$factory->define(App\User::class, function (Faker\Generator $faker) {
|
|
||||||
static $password;
|
|
||||||
|
|
||||||
return [
|
|
||||||
'name' => $faker->name,
|
|
||||||
'email' => $faker->unique()->safeEmail,
|
|
||||||
'password' => $password ?: $password = bcrypt('secret'),
|
|
||||||
'remember_token' => str_random(10),
|
|
||||||
];
|
|
||||||
});
|
|
21
database/factories/UserFactory.php
Normal file
21
database/factories/UserFactory.php
Normal file
@@ -0,0 +1,21 @@
|
|||||||
|
<?php
|
||||||
|
/** @var \Illuminate\Database\Eloquent\Factory $factory */
|
||||||
|
|
||||||
|
$factory->define(App\User::class, function (Faker\Generator $faker) {
|
||||||
|
static $password;
|
||||||
|
|
||||||
|
return [
|
||||||
|
'name' => substr($faker->userName, 0, 15),
|
||||||
|
'email' => $faker->unique()->safeEmail,
|
||||||
|
'password' => $password ?: $password = bcrypt('secret'),
|
||||||
|
'remember_token' => str_random(10),
|
||||||
|
'display_name' => substr($faker->name, 0, 20),
|
||||||
|
'is_protected' => false,
|
||||||
|
'accept_analytics' => false,
|
||||||
|
'private_likes' => false,
|
||||||
|
];
|
||||||
|
});
|
||||||
|
|
||||||
|
$factory->state(App\User::class, 'protected', [
|
||||||
|
'is_protected' => true,
|
||||||
|
]);
|
@@ -1,8 +1,8 @@
|
|||||||
<?php
|
<?php
|
||||||
|
|
||||||
use Illuminate\Support\Facades\Schema;
|
|
||||||
use Illuminate\Database\Schema\Blueprint;
|
|
||||||
use Illuminate\Database\Migrations\Migration;
|
use Illuminate\Database\Migrations\Migration;
|
||||||
|
use Illuminate\Database\Schema\Blueprint;
|
||||||
|
use Illuminate\Support\Facades\Schema;
|
||||||
|
|
||||||
class CreateUsersTable extends Migration
|
class CreateUsersTable extends Migration
|
||||||
{
|
{
|
||||||
|
@@ -1,8 +1,8 @@
|
|||||||
<?php
|
<?php
|
||||||
|
|
||||||
use Illuminate\Support\Facades\Schema;
|
|
||||||
use Illuminate\Database\Schema\Blueprint;
|
|
||||||
use Illuminate\Database\Migrations\Migration;
|
use Illuminate\Database\Migrations\Migration;
|
||||||
|
use Illuminate\Database\Schema\Blueprint;
|
||||||
|
use Illuminate\Support\Facades\Schema;
|
||||||
|
|
||||||
class CreatePasswordResetsTable extends Migration
|
class CreatePasswordResetsTable extends Migration
|
||||||
{
|
{
|
||||||
|
@@ -1,8 +1,8 @@
|
|||||||
<?php
|
<?php
|
||||||
|
|
||||||
use Illuminate\Support\Facades\Schema;
|
|
||||||
use Illuminate\Database\Schema\Blueprint;
|
|
||||||
use Illuminate\Database\Migrations\Migration;
|
use Illuminate\Database\Migrations\Migration;
|
||||||
|
use Illuminate\Database\Schema\Blueprint;
|
||||||
|
use Illuminate\Support\Facades\Schema;
|
||||||
|
|
||||||
class CreateEjaculationsTable extends Migration
|
class CreateEjaculationsTable extends Migration
|
||||||
{
|
{
|
||||||
|
@@ -1,8 +1,8 @@
|
|||||||
<?php
|
<?php
|
||||||
|
|
||||||
use Illuminate\Support\Facades\Schema;
|
|
||||||
use Illuminate\Database\Schema\Blueprint;
|
|
||||||
use Illuminate\Database\Migrations\Migration;
|
use Illuminate\Database\Migrations\Migration;
|
||||||
|
use Illuminate\Database\Schema\Blueprint;
|
||||||
|
use Illuminate\Support\Facades\Schema;
|
||||||
|
|
||||||
class CreateInformationTable extends Migration
|
class CreateInformationTable extends Migration
|
||||||
{
|
{
|
||||||
|
@@ -1,8 +1,8 @@
|
|||||||
<?php
|
<?php
|
||||||
|
|
||||||
use Illuminate\Support\Facades\Schema;
|
|
||||||
use Illuminate\Database\Schema\Blueprint;
|
|
||||||
use Illuminate\Database\Migrations\Migration;
|
use Illuminate\Database\Migrations\Migration;
|
||||||
|
use Illuminate\Database\Schema\Blueprint;
|
||||||
|
use Illuminate\Support\Facades\Schema;
|
||||||
|
|
||||||
class AddLinkToEjaculations extends Migration
|
class AddLinkToEjaculations extends Migration
|
||||||
{
|
{
|
||||||
|
@@ -1,8 +1,8 @@
|
|||||||
<?php
|
<?php
|
||||||
|
|
||||||
use Illuminate\Support\Facades\Schema;
|
|
||||||
use Illuminate\Database\Schema\Blueprint;
|
|
||||||
use Illuminate\Database\Migrations\Migration;
|
use Illuminate\Database\Migrations\Migration;
|
||||||
|
use Illuminate\Database\Schema\Blueprint;
|
||||||
|
use Illuminate\Support\Facades\Schema;
|
||||||
|
|
||||||
class CreateTagsTable extends Migration
|
class CreateTagsTable extends Migration
|
||||||
{
|
{
|
||||||
|
@@ -0,0 +1,36 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
use Illuminate\Database\Migrations\Migration;
|
||||||
|
use Illuminate\Database\Schema\Blueprint;
|
||||||
|
use Illuminate\Support\Facades\Schema;
|
||||||
|
|
||||||
|
class CreateMetadataTable extends Migration
|
||||||
|
{
|
||||||
|
/**
|
||||||
|
* Run the migrations.
|
||||||
|
*
|
||||||
|
* @return void
|
||||||
|
*/
|
||||||
|
public function up()
|
||||||
|
{
|
||||||
|
Schema::create('metadata', function (Blueprint $table) {
|
||||||
|
$table->string('url');
|
||||||
|
$table->string('title');
|
||||||
|
$table->string('description');
|
||||||
|
$table->string('image');
|
||||||
|
$table->timestamps();
|
||||||
|
|
||||||
|
$table->index('url');
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Reverse the migrations.
|
||||||
|
*
|
||||||
|
* @return void
|
||||||
|
*/
|
||||||
|
public function down()
|
||||||
|
{
|
||||||
|
Schema::dropIfExists('metadata');
|
||||||
|
}
|
||||||
|
}
|
@@ -0,0 +1,37 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
use Illuminate\Database\Migrations\Migration;
|
||||||
|
use Illuminate\Database\Schema\Blueprint;
|
||||||
|
use Illuminate\Support\Facades\Schema;
|
||||||
|
|
||||||
|
class RecreateMetadataTable extends Migration
|
||||||
|
{
|
||||||
|
/**
|
||||||
|
* Run the migrations.
|
||||||
|
*
|
||||||
|
* @return void
|
||||||
|
*/
|
||||||
|
public function up()
|
||||||
|
{
|
||||||
|
Schema::dropIfExists('metadata');
|
||||||
|
Schema::create('metadata', function (Blueprint $table) {
|
||||||
|
$table->text('url');
|
||||||
|
$table->text('title');
|
||||||
|
$table->text('description');
|
||||||
|
$table->text('image');
|
||||||
|
$table->timestamps();
|
||||||
|
|
||||||
|
$table->index('url');
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Reverse the migrations.
|
||||||
|
*
|
||||||
|
* @return void
|
||||||
|
*/
|
||||||
|
public function down()
|
||||||
|
{
|
||||||
|
Schema::dropIfExists('metadata');
|
||||||
|
}
|
||||||
|
}
|
@@ -0,0 +1,32 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
use Illuminate\Database\Migrations\Migration;
|
||||||
|
use Illuminate\Database\Schema\Blueprint;
|
||||||
|
use Illuminate\Support\Facades\Schema;
|
||||||
|
|
||||||
|
class ChangeLinkOnEjaculations extends Migration
|
||||||
|
{
|
||||||
|
/**
|
||||||
|
* Run the migrations.
|
||||||
|
*
|
||||||
|
* @return void
|
||||||
|
*/
|
||||||
|
public function up()
|
||||||
|
{
|
||||||
|
Schema::table('ejaculations', function (Blueprint $table) {
|
||||||
|
$table->text('link')->default('')->change();
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Reverse the migrations.
|
||||||
|
*
|
||||||
|
* @return void
|
||||||
|
*/
|
||||||
|
public function down()
|
||||||
|
{
|
||||||
|
Schema::table('ejaculations', function (Blueprint $table) {
|
||||||
|
$table->string('link')->default('')->change();
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
@@ -0,0 +1,32 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
use Illuminate\Database\Migrations\Migration;
|
||||||
|
use Illuminate\Database\Schema\Blueprint;
|
||||||
|
use Illuminate\Support\Facades\Schema;
|
||||||
|
|
||||||
|
class AddExpiresOnMetadata extends Migration
|
||||||
|
{
|
||||||
|
/**
|
||||||
|
* Run the migrations.
|
||||||
|
*
|
||||||
|
* @return void
|
||||||
|
*/
|
||||||
|
public function up()
|
||||||
|
{
|
||||||
|
Schema::table('metadata', function (Blueprint $table) {
|
||||||
|
$table->timestamp('expires_at')->nullable();
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Reverse the migrations.
|
||||||
|
*
|
||||||
|
* @return void
|
||||||
|
*/
|
||||||
|
public function down()
|
||||||
|
{
|
||||||
|
Schema::table('metadata', function (Blueprint $table) {
|
||||||
|
$table->removeColumn('expires_at');
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
@@ -0,0 +1,34 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
use Illuminate\Database\Migrations\Migration;
|
||||||
|
use Illuminate\Database\Schema\Blueprint;
|
||||||
|
use Illuminate\Support\Facades\Schema;
|
||||||
|
|
||||||
|
class AddBioAndUrlToUsers extends Migration
|
||||||
|
{
|
||||||
|
/**
|
||||||
|
* Run the migrations.
|
||||||
|
*
|
||||||
|
* @return void
|
||||||
|
*/
|
||||||
|
public function up()
|
||||||
|
{
|
||||||
|
Schema::table('users', function (Blueprint $table) {
|
||||||
|
$table->string('bio', 160)->default('');
|
||||||
|
$table->text('url')->default('');
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Reverse the migrations.
|
||||||
|
*
|
||||||
|
* @return void
|
||||||
|
*/
|
||||||
|
public function down()
|
||||||
|
{
|
||||||
|
Schema::table('users', function (Blueprint $table) {
|
||||||
|
$table->dropColumn('bio');
|
||||||
|
$table->dropColumn('url');
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
@@ -0,0 +1,32 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
use Illuminate\Database\Migrations\Migration;
|
||||||
|
use Illuminate\Database\Schema\Blueprint;
|
||||||
|
use Illuminate\Support\Facades\Schema;
|
||||||
|
|
||||||
|
class AddIsAdminToUsers extends Migration
|
||||||
|
{
|
||||||
|
/**
|
||||||
|
* Run the migrations.
|
||||||
|
*
|
||||||
|
* @return void
|
||||||
|
*/
|
||||||
|
public function up()
|
||||||
|
{
|
||||||
|
Schema::table('users', function (Blueprint $table) {
|
||||||
|
$table->boolean('is_admin')->default(false);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Reverse the migrations.
|
||||||
|
*
|
||||||
|
* @return void
|
||||||
|
*/
|
||||||
|
public function down()
|
||||||
|
{
|
||||||
|
Schema::table('users', function (Blueprint $table) {
|
||||||
|
$table->dropColumn('is_admin');
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
37
database/migrations/2019_03_26_224641_create_likes_table.php
Normal file
37
database/migrations/2019_03_26_224641_create_likes_table.php
Normal file
@@ -0,0 +1,37 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
use Illuminate\Database\Migrations\Migration;
|
||||||
|
use Illuminate\Database\Schema\Blueprint;
|
||||||
|
use Illuminate\Support\Facades\Schema;
|
||||||
|
|
||||||
|
class CreateLikesTable extends Migration
|
||||||
|
{
|
||||||
|
/**
|
||||||
|
* Run the migrations.
|
||||||
|
*
|
||||||
|
* @return void
|
||||||
|
*/
|
||||||
|
public function up()
|
||||||
|
{
|
||||||
|
Schema::create('likes', function (Blueprint $table) {
|
||||||
|
$table->increments('id');
|
||||||
|
$table->integer('user_id')->index();
|
||||||
|
$table->integer('ejaculation_id')->index();
|
||||||
|
$table->timestamps();
|
||||||
|
|
||||||
|
$table->unique(['user_id', 'ejaculation_id']);
|
||||||
|
$table->foreign('user_id')->references('id')->on('users')->onDelete('cascade');
|
||||||
|
$table->foreign('ejaculation_id')->references('id')->on('ejaculations')->onDelete('cascade');
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Reverse the migrations.
|
||||||
|
*
|
||||||
|
* @return void
|
||||||
|
*/
|
||||||
|
public function down()
|
||||||
|
{
|
||||||
|
Schema::dropIfExists('likes');
|
||||||
|
}
|
||||||
|
}
|
@@ -0,0 +1,32 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
use Illuminate\Database\Migrations\Migration;
|
||||||
|
use Illuminate\Database\Schema\Blueprint;
|
||||||
|
use Illuminate\Support\Facades\Schema;
|
||||||
|
|
||||||
|
class AddPrivateLikesToUsers extends Migration
|
||||||
|
{
|
||||||
|
/**
|
||||||
|
* Run the migrations.
|
||||||
|
*
|
||||||
|
* @return void
|
||||||
|
*/
|
||||||
|
public function up()
|
||||||
|
{
|
||||||
|
Schema::table('users', function (Blueprint $table) {
|
||||||
|
$table->boolean('private_likes')->default(false);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Reverse the migrations.
|
||||||
|
*
|
||||||
|
* @return void
|
||||||
|
*/
|
||||||
|
public function down()
|
||||||
|
{
|
||||||
|
Schema::table('users', function (Blueprint $table) {
|
||||||
|
$table->dropColumn('private_likes');
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
@@ -0,0 +1,33 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
use Illuminate\Database\Migrations\Migration;
|
||||||
|
use Illuminate\Database\Schema\Blueprint;
|
||||||
|
use Illuminate\Support\Facades\Schema;
|
||||||
|
|
||||||
|
class CreateMetadataTagTable extends Migration
|
||||||
|
{
|
||||||
|
/**
|
||||||
|
* Run the migrations.
|
||||||
|
*
|
||||||
|
* @return void
|
||||||
|
*/
|
||||||
|
public function up()
|
||||||
|
{
|
||||||
|
Schema::create('metadata_tag', function (Blueprint $table) {
|
||||||
|
$table->increments('id');
|
||||||
|
$table->text('metadata_url')->index();
|
||||||
|
$table->integer('tag_id')->index();
|
||||||
|
$table->timestamps();
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Reverse the migrations.
|
||||||
|
*
|
||||||
|
* @return void
|
||||||
|
*/
|
||||||
|
public function down()
|
||||||
|
{
|
||||||
|
Schema::dropIfExists('metadata_tag');
|
||||||
|
}
|
||||||
|
}
|
@@ -0,0 +1,32 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
use Illuminate\Database\Migrations\Migration;
|
||||||
|
use Illuminate\Database\Schema\Blueprint;
|
||||||
|
use Illuminate\Support\Facades\Schema;
|
||||||
|
|
||||||
|
class AddIsTooSensitiveToEjaculations extends Migration
|
||||||
|
{
|
||||||
|
/**
|
||||||
|
* Run the migrations.
|
||||||
|
*
|
||||||
|
* @return void
|
||||||
|
*/
|
||||||
|
public function up()
|
||||||
|
{
|
||||||
|
Schema::table('ejaculations', function (Blueprint $table) {
|
||||||
|
$table->boolean('is_too_sensitive')->default(false);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Reverse the migrations.
|
||||||
|
*
|
||||||
|
* @return void
|
||||||
|
*/
|
||||||
|
public function down()
|
||||||
|
{
|
||||||
|
Schema::table('ejaculations', function (Blueprint $table) {
|
||||||
|
$table->dropColumn('is_too_sensitive');
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user