Compare commits
1088 Commits
0.0.2
...
GoToコヅクリ~お
Author | SHA1 | Date |
---|---|---|
Irie Aoi | cb218c435d | |
Irie Aoi | db9d93ed90 | |
Irie Aoi | 69ec50a6c0 | |
Irie Aoi | f408c23982 | |
Irie Aoi | 3d9179268f | |
Irie Aoi | e1e4ffbdc8 | |
Irie Aoi | 1febc0866d | |
Irie Aoi | eacc7bf5f8 | |
Irie Aoi | ed9307c078 | |
Irie Aoi | 330facf974 | |
Irie Aoi | 2778d34711 | |
Irie Aoi | 8d53c0f175 | |
Irie Aoi | c73141c1d3 | |
Irie Aoi | a667767858 | |
Irie Aoi | daa12d93a5 | |
Irie Aoi | 4b51f6dfec | |
Irie Aoi | 5a1560fef9 | |
Irie Aoi | 3b3fc6c973 | |
Irie Aoi | 21a7d3f9e7 | |
dependabot[bot] | 7bcf48c8bf | |
dependabot[bot] | 8f1a94b863 | |
dependabot[bot] | 1b54af63ce | |
dependabot[bot] | 3ba946ec11 | |
dependabot[bot] | 32e25a9f7a | |
dependabot[bot] | 31626f48d9 | |
dependabot[bot] | 174d802edf | |
shibafu | 9c9db69662 | |
Irie Aoi | 8c5fdfb7f8 | |
Irie Aoi | c616800da1 | |
Irie Aoi | 6449094b78 | |
Irie Aoi | 072b3a0910 | |
shibafu | 83d22b807f | |
shibafu | 914d92e545 | |
shibafu | dcef270790 | |
shibafu | 5808ac58ee | |
shibafu | b60932b6c5 | |
shibafu | 28520007e4 | |
shibafu | 63af49ba70 | |
shibafu | 3825228344 | |
shibafu | 855011c624 | |
shibafu | bb7b05435e | |
shibafu | 5c26e58c1d | |
shibafu | ddb11ee96c | |
shibafu | 38c7755757 | |
shibafu | c0cfbe1bc4 | |
shibafu | 96bf90104e | |
shibafu | 2bcd050fed | |
shibafu | ea1483a90a | |
shibafu | 7e7d0f80c1 | |
shibafu | 32398fea73 | |
shibafu | 076e7d5d0d | |
dependabot[bot] | 41c7679423 | |
dependabot[bot] | 486e5bad0a | |
dependabot[bot] | a026897986 | |
dependabot[bot] | 96658ac34c | |
dependabot[bot] | 622964ec01 | |
shibafu | 141067beab | |
shibafu | 61b83c18a7 | |
shibafu | 581ae56173 | |
shibafu | b50a15c109 | |
shibafu | 060bdeef43 | |
shibafu | be098c38cb | |
shibafu | d7d2bc2397 | |
shibafu | 0f435c09b3 | |
shibafu | 16071f7cff | |
shibafu | a22f41766a | |
shibafu | 552ff421dd | |
shibafu | f8952474b5 | |
shibafu | 3fd62dcd6f | |
shibafu | 4360144d6f | |
shibafu | e74a9675ce | |
shibafu | 0bc546ac64 | |
shibafu | 95ed292f4f | |
shibafu | 032533666d | |
shibafu | 5fc8a22cca | |
shibafu | 3406b326f0 | |
shibafu | 7cf1a362ac | |
shibafu | 7708e705cc | |
dependabot[bot] | 494eb7a17b | |
shibafu | 24a74c4c2c | |
shibafu | d1f14ed271 | |
shibafu | 16d71a1621 | |
shibafu | c956a96605 | |
dependabot[bot] | f301c56a1a | |
dependabot[bot] | 2c2b17653e | |
dependabot[bot] | 569363c72e | |
dependabot[bot] | 3ba3165b93 | |
dependabot[bot] | 083373ad7d | |
dependabot[bot] | 2b109163fc | |
dependabot[bot] | beca7fb0c6 | |
dependabot[bot] | fa733417dd | |
dependabot[bot] | 7dca83d12a | |
dependabot[bot] | d8c88fab63 | |
dependabot[bot] | 9bac56f912 | |
dependabot[bot] | 28c0060679 | |
dependabot[bot] | b5af600336 | |
shibafu | 301fc83e7e | |
shibafu | 3bb2b9afe0 | |
shibafu | 4e521baf56 | |
shibafu | 59e4cb8d91 | |
shibafu | d0fcbd79ca | |
shibafu | c680cd8d8e | |
shibafu | 2d42f48bea | |
shibafu | a54d57827f | |
dependabot[bot] | f8a5cc5d54 | |
shibafu | 6f8c81cef2 | |
shibafu | 915b575e6e | |
shibafu | 27e9a86be8 | |
shibafu | 3015f82611 | |
dependabot[bot] | d894897b82 | |
shibafu | 0ccb04c651 | |
shibafu | acc3c05c86 | |
shibafu | 4405a2b526 | |
shibafu | 7d8969e5f1 | |
shibafu | 8641f26350 | |
shibafu | 432c2bf4d5 | |
shibafu | 228b1cdaaa | |
shibafu | f37a9c79f4 | |
shibafu | 6968ca7333 | |
shibafu | 867cafa26b | |
shibafu | 04d546b1f0 | |
shibafu | e961a2d4b4 | |
shibafu | 064cfff211 | |
shibafu | 969ddb94a9 | |
shibafu | ae79f0225e | |
shibafu | 196819270b | |
shibafu | 45fd630e1a | |
shibafu | b71b7e5cb2 | |
shibafu | f715e7feee | |
shibafu | ca070e773a | |
shibafu | da7be61698 | |
shibafu | c372c11867 | |
shibafu | 578b9934f5 | |
shibafu | 4acebcec7e | |
shibafu | 0ca16459a4 | |
shibafu | b99930e145 | |
shibafu | 8093b22981 | |
shibafu | 8cde943cf8 | |
shibafu | 1c6959bfcb | |
shibafu | 73c64f0f27 | |
shibafu | 7aa11275cc | |
shibafu | e6950a5dfb | |
shibafu | 134983d13d | |
shibafu | 35aa7b3916 | |
shibafu | 0587a0f1d4 | |
shibafu | 969bc79cbc | |
shibafu | 723587b0ac | |
shibafu | 23189b76e4 | |
shibafu | 8a0a29feef | |
hina | 2f928a29e1 | |
Hinaloe | 62a3e883e1 | |
dependabot[bot] | 65077571e7 | |
shibafu | a4fdd220ba | |
shibafu | a52211758e | |
shibafu | 1482c33448 | |
shibafu | 7c32ab068d | |
shibafu | 3a1ec763ea | |
shibafu | 77620c1699 | |
shibafu | 126e44bcd9 | |
shibafu | 422891e237 | |
shibafu | 081dd8da28 | |
shibafu | 87c97d27a8 | |
shibafu | 8f1a4d3e88 | |
shibafu | 82bb10ae24 | |
dependabot[bot] | 54e112fa57 | |
shibafu | a63c39a56f | |
shibafu | f8a93fdf45 | |
shibafu | 978d54cf12 | |
shibafu | 43ed36ccb7 | |
shibafu | 18ae64a870 | |
shibafu | b5901f26bf | |
shibafu | c9efcb538c | |
shibafu | 561c9d028d | |
shibafu | d18f245129 | |
shibafu | e2c43fef80 | |
shibafu | c7aa002625 | |
shibafu | 66322dfec0 | |
shibafu | fcdc00f165 | |
shibafu | e2ba3581f9 | |
shibafu | d58afc0324 | |
dependabot[bot] | d69fe6a22a | |
shibafu | ef18d26b5d | |
shibafu | 94446e0174 | |
shibafu | d3ecfd1fb8 | |
shibafu | 08ab0c6543 | |
shibafu | bbbea73a05 | |
shibafu | 059c6d69cf | |
shibafu | 08e12cd218 | |
shibafu | 134a11ad51 | |
مرزم اليَغميصاء | 978eccd643 | |
shibafu | 0a9920b11c | |
shibafu | 16b5fb3533 | |
shibafu | 35dea402ab | |
shibafu | ab960cda7c | |
shibafu | e6252c49d1 | |
shibafu | de07e950f2 | |
shibafu | 5926c6e640 | |
shibafu | 034a47cd25 | |
shibafu | 45645d9ae5 | |
shibafu | 73b63a1f39 | |
shibafu | c5ab67d547 | |
shibafu | 19d4ba5e40 | |
dependabot[bot] | 6c3bcd57c5 | |
dependabot[bot] | ce0d9e7163 | |
dependabot[bot] | 26f07e2b54 | |
dependabot[bot] | 405a968ad8 | |
dependabot[bot] | 9c6990979a | |
shibafu | d83516f394 | |
shibafu | b232279c25 | |
dependabot[bot] | a04faa7001 | |
dependabot[bot] | 831c099247 | |
dependabot[bot] | 48815eed89 | |
dependabot[bot] | dc65645269 | |
dependabot[bot] | d9fc2a4fca | |
shibafu | 5215398149 | |
Hinaloe | 7afcf3f339 | |
dependabot[bot] | 261e27a3eb | |
dependabot[bot] | 9db8fe78bb | |
dependabot[bot] | 688f24193f | |
dependabot[bot] | dc055c9017 | |
dependabot[bot] | 28e7497e53 | |
dependabot[bot] | 06a5bfac6b | |
dependabot[bot] | 9a94f2ce42 | |
shibafu | 43e22045ae | |
shibafu | 4fbb047485 | |
hina | c28967a5ae | |
hina | a3e687bdb4 | |
dependabot[bot] | 6b15dcaef3 | |
dependabot[bot] | f28043e192 | |
dependabot[bot] | 79b873308a | |
dependabot[bot] | ecaa6e8110 | |
dependabot[bot] | 9fd0f0774d | |
shibafu | d0edea659e | |
hina | b9ed86b69a | |
hina | 2693d340c6 | |
hina | b909035fe8 | |
hina | b908351a35 | |
hina | edd7a6f4c8 | |
hina | 334c810b8d | |
hina | 51d9b74697 | |
hina | c8c278653a | |
Hinaloe | d3592f9fea | |
shibafu | a67a6a8be9 | |
shibafu | 57c6eff442 | |
hina | 5d16c3b680 | |
hina | aae9e0c502 | |
hina | 2a988275f3 | |
hina | 350360e2a9 | |
dependabot[bot] | 9aa3bb43d3 | |
dependabot[bot] | d0966e14ea | |
dependabot[bot] | 9389593c3a | |
dependabot[bot] | 037cfdfba9 | |
dependabot[bot] | c559237ffd | |
dependabot[bot] | 6c5332081c | |
dependabot[bot] | 204a0b2b70 | |
dependabot[bot] | b66896632a | |
shibafu | 4bac437d39 | |
dependabot[bot] | e4e065c614 | |
dependabot[bot] | 5284b93d40 | |
dependabot[bot] | f6fe04b766 | |
shibafu | 5f7a25e1b8 | |
Hinaloe | b71ee87691 | |
shibafu | 83b0ccf770 | |
shibafu | 77e83e48d3 | |
shibafu | c93106cab9 | |
shibafu | 4c04deb80f | |
shibafu | da8af95043 | |
shibafu | 9fcf9cd49e | |
shibafu | b5a72e33eb | |
shibafu | 36c8d84555 | |
shibafu | ced289b95d | |
shibafu | 40f45da282 | |
shibafu | 0218035aeb | |
shibafu | 1a3cead932 | |
shibafu | f3a5644a32 | |
shibafu | e2f01561a8 | |
shibafu | 8fb8677406 | |
shibafu | 8ac65f1cc6 | |
shibafu | 5a3e41c82e | |
shibafu | c9fd46d408 | |
shibafu | ea2894cd4b | |
shibafu | 2f4c61c900 | |
shibafu | 019412c72a | |
shibafu | 00f75f33cc | |
shibafu | 3a4c734bcf | |
shibafu | ee7b7cf274 | |
shibafu | 34c4f13376 | |
shibafu | 80590888df | |
shibafu | 74fee83f4e | |
shibafu | a48fe596e1 | |
shibafu | cb4dacdaf6 | |
shibafu | a531e5c9bb | |
shibafu | 17ee207281 | |
shibafu | 015bcaa563 | |
shibafu | 36a2ab5fe2 | |
shibafu | 3a05d2c9cc | |
shibafu | 948b517c4d | |
shibafu | cc0e0271b8 | |
shibafu | 5af55fa6b4 | |
shibafu | bd84effedc | |
shibafu | c0d62f5112 | |
shibafu | 1853c28093 | |
shibafu | 77f4bbcfce | |
shibafu | fb5b34b239 | |
shibafu | 531067fb9c | |
shibafu | 3a2d0e67aa | |
shibafu | 54b6ff2282 | |
shibafu | 023446e0a8 | |
shibafu | 8681c328d0 | |
shibafu | fa4827f382 | |
shibafu | 15c462449f | |
shibafu | 03b05a48cd | |
shibafu | 1671070d0c | |
shibafu | 9f047544b9 | |
shibafu | 988ad9d992 | |
shibafu | 5668296e7d | |
shibafu | 8a764c756c | |
shibafu | cf9df74ed3 | |
shibafu | e872964144 | |
shibafu | 5db25ce017 | |
shibafu | a58a206b86 | |
shibafu | dcdd33925b | |
shibafu | 552c2d4460 | |
shibafu | c11bbbbff7 | |
shibafu | d33d7b58a4 | |
shibafu | 2a50ee4f5b | |
shibafu | e9414517b9 | |
shibafu | ab9ecd6bfa | |
shibafu | 59c6e4a52a | |
shibafu | 0ca7b8b7e1 | |
shibafu | 882f239d58 | |
shibafu | 4c3c5f18d2 | |
shibafu | 1953143ab2 | |
shibafu | 8c2f7d4212 | |
shibafu | f2b91b71f8 | |
dependabot[bot] | ef6fd71cee | |
dependabot[bot] | 55fba9e2a2 | |
dependabot[bot] | 7f4dd703db | |
dependabot[bot] | 49bc254e8b | |
shibafu | 631ae820f3 | |
shibafu | 77d3ebd452 | |
shibafu | 631d2ea298 | |
shibafu | cb0af4113d | |
shibafu | 46ca276eab | |
shibafu | 41ce5229c7 | |
shibafu | 6387d4e853 | |
shibafu | 0a53199399 | |
shibafu | 794cdf2be6 | |
shibafu | 84b955b195 | |
shibafu | 12553321e1 | |
shibafu | 24a5017334 | |
shibafu | 272e7ecc61 | |
shibafu | 22845fe279 | |
shibafu | 45eba30528 | |
shibafu | 7259ee3647 | |
shibafu | 3def26ddb9 | |
shibafu | cef69a1545 | |
shibafu | ea59bcf150 | |
shibafu | b29a82435c | |
shibafu | 67b697a600 | |
shibafu | 64065ce9e6 | |
shibafu | 8b9abe2d7b | |
shibafu | a5b9021e95 | |
shibafu | 92e1131d20 | |
shibafu | af02375475 | |
shibafu | 267eb48589 | |
shibafu | 38ae540009 | |
shibafu | b2eed9a9c5 | |
eai04191 | e7e4195a10 | |
shibafu | 7968019e1c | |
shibafu | 1d0c51c284 | |
hina | c9af8130f4 | |
shibafu | 9431cd5b5d | |
shibafu | 9f565798c0 | |
shibafu | ef23f1b12e | |
shibafu | 2edabfa38f | |
shibafu | ad65475037 | |
shibafu | e429b2c054 | |
shibafu | cec9ffc5ac | |
eai04191 | 07b315e8af | |
eai04191 | 1948d5235e | |
eai04191 | 78a255c1e3 | |
shibafu | d9cf5e54e3 | |
shibafu | e7db04fd55 | |
shibafu | 37a10b7354 | |
shibafu | 170492b39d | |
shibafu | ea3f2e595f | |
shibafu | 6ea360bc4e | |
shibafu | 251d7b9108 | |
shibafu | bba945113b | |
shibafu | 695f457505 | |
shibafu | 00345eedca | |
shibafu | 19ef4d2bbe | |
shibafu | 6577a032e1 | |
eai04191 | 010a0a9c8f | |
eai04191 | cdc6335a06 | |
eai04191 | 03633440a6 | |
unarist | 67ae0e159f | |
shibafu | 73ee9f108b | |
shibafu | f132955be7 | |
shibafu | 3420e053fc | |
shibafu | a01bc6989e | |
shibafu | a71aa0c3b6 | |
shibafu | 26be8a086e | |
shibafu | af5de3ee14 | |
eai04191 | c8cee80144 | |
eai04191 | d8e170ff85 | |
shibafu | 9c101dfb7b | |
eai04191 | 12fd228e75 | |
eai04191 | dc0eb0a548 | |
eai04191 | 16ed4482f4 | |
eai04191 | 57a847baf5 | |
shibafu | 332b6d7dd0 | |
shibafu | 99a92c6106 | |
shibafu | bcb5abb161 | |
shibafu | 4ca6f00c1b | |
shibafu | 7b8811b894 | |
shibafu | c7e261d06b | |
shibafu | b3c98613e7 | |
shibafu | f7a5948e8e | |
Yuu Kobayashi | d1abca5416 | |
Yuu Kobayashi | 579708389a | |
MitarashiDango | 900e4c94a7 | |
MitarashiDango | a434a45e4a | |
shibafu | e6657a0756 | |
shibafu | 4f6bb0ac15 | |
eai04191 | e27a848b08 | |
shibafu | 8772facadf | |
MitarashiDango | d5ee59825f | |
shibafu | 4192f22af5 | |
MitarashiDango | dd07940aea | |
MitarashiDango | 78bb7dae28 | |
shibafu | e4890f65ae | |
shibafu | 2454a24ee2 | |
shibafu | 7858bd0a5f | |
shibafu | ce8855510c | |
shibafu | c42a3d2657 | |
shibafu | 7cb6dd4754 | |
shibafu | bf1c4e7a21 | |
shibafu | 7a56072765 | |
eai04191 | d6e0512dae | |
eai04191 | 1edc70fc4c | |
shibafu | a20f690cdd | |
shibafu | eab901d56d | |
eai04191 | 8c88d60034 | |
eai04191 | 599e3f9557 | |
eai04191 | a9a0f3b99a | |
eai04191 | 5fc0c6c1b6 | |
shibafu | 5642e73391 | |
shibafu | 92847fefe0 | |
shibafu | 178ed02d00 | |
shibafu | 3381965896 | |
eai04191 | dc8a70291d | |
shibafu | a10acdd481 | |
shibafu | 2c4eaccf43 | |
shibafu | 141d5ce77c | |
eai04191 | ccade6ff9f | |
eai04191 | 033784bfc8 | |
eai04191 | db39ee35c2 | |
shibafu | fb6c1a0574 | |
shibafu | 59aec2c038 | |
shibafu | d45898931a | |
shibafu | fb50881e74 | |
shibafu | 40fedf59d4 | |
shibafu | b2014a3db7 | |
shibafu | f3a4f682a8 | |
eai04191 | c0b76e522b | |
eai04191 | 0f530099b4 | |
eai04191 | eecace33bd | |
eai04191 | d049a6f631 | |
eai04191 | 4add9a87cc | |
eai04191 | c898487a20 | |
eai04191 | 03cb2b0728 | |
eai04191 | 3c0b65ff8c | |
eai04191 | b367009c5c | |
shibafu | 22150d0e7a | |
MitarashiDango | 2b98267fa8 | |
MitarashiDango | a7972046ef | |
MitarashiDango | 524d00d0ed | |
MitarashiDango | 06cc18565e | |
shibafu | ff6de777d7 | |
shibafu | 743272f8d6 | |
shibafu | 784fb43ae9 | |
shibafu | de23a37ab3 | |
shibafu | 72fc84a42c | |
shibafu | f59aa750e4 | |
shibafu | 66f4c45f5c | |
dependabot[bot] | c204a7e934 | |
shibafu | aa87a8f070 | |
Aoi Irie | ab20ca5370 | |
Aoi Irie | bd84f29a27 | |
shibafu | ac2077af49 | |
shibafu | 27970e3ac5 | |
shibafu | 4940b7a9ca | |
eai04191 | 5e02a8ab7a | |
eai04191 | a088444626 | |
eai04191 | 8ef9a1f8f4 | |
eai04191 | 150a8152a4 | |
shibafu | 5ac1bae73f | |
Aoi Irie | 1bec21f15f | |
shibafu | 2c1976fd2b | |
eai04191 | 13c3407a4e | |
eai04191 | 91e6cea79a | |
eai04191 | ac40a411da | |
shibafu | e2aa47151b | |
eai04191 | dd38f4e0eb | |
eai04191 | 17bc8cebbf | |
eai04191 | 4f23a9404b | |
eai04191 | b7eafd881f | |
eai04191 | 0a994884a0 | |
eai04191 | 9926cc3357 | |
eai04191 | 5069f20b50 | |
eai04191 | 93387f1ff5 | |
eai04191 | 7baf51fc09 | |
shibafu | 0e3878a808 | |
shibafu | 4c0b245574 | |
shibafu | 0a0047c4c3 | |
shibafu | 831d1668ef | |
shibafu | 51c8199283 | |
shibafu | 78a1bdfb30 | |
eai04191 | ceff57f9f6 | |
eai04191 | bc2f8662fc | |
eai04191 | 2112087e89 | |
eai04191 | c93ccb43c8 | |
shibafu | 58ae1bc1c1 | |
shibafu | f7c9e83b12 | |
shibafu | a1850b666b | |
MitarashiDango | 8594caade1 | |
shibafu | dddb47f68a | |
shibafu | 1b2b043be2 | |
shibafu | c535153e1f | |
eai04191 | 830de3a5e3 | |
shibafu | ab46117138 | |
shibafu | 3c2fec21a0 | |
shibafu | 370d1cc01b | |
shibafu | 5517cd5fab | |
shibafu | 3c083a7c60 | |
shibafu | fa6b8b87af | |
shibafu | d290bf4107 | |
shibafu | b274c6bc40 | |
shibafu | 018532f01f | |
shibafu | e4ef935dd2 | |
shibafu | dabae9f251 | |
shibafu | fb84a1d416 | |
dependabot[bot] | 358580a15e | |
shibafu | 000b89f380 | |
shibafu | c5cbad4475 | |
shibafu | f4abb08921 | |
shibafu | 38eb0348f9 | |
eai04191 | 4f5595dae0 | |
eai04191 | 733e97bc58 | |
shibafu | c4768ded38 | |
eai04191 | 598d27f6b8 | |
shibafu | cf5e269fd8 | |
eai04191 | 4747f822ab | |
shibafu | bcf78df2fc | |
shibafu | fa171bc3d3 | |
shibafu | b828a233bc | |
shibafu | bbfd6a9895 | |
shibafu | e1dd2b1c8f | |
shibafu | ec4e0f3dda | |
shibafu | 0aed1d9ebe | |
shibafu | d8cdf218b7 | |
eai04191 | 7c9eefe478 | |
eai04191 | 810a1dbc59 | |
shibafu | a521a26aa5 | |
eai04191 | e80acf79b7 | |
shibafu | 1b5fbfabc4 | |
shibafu | 0ad0b268bc | |
shibafu | da19806a3d | |
eai04191 | 4390af53f9 | |
shibafu | dd12582dba | |
eai04191 | 6e81f091d1 | |
eai04191 | c60d41427d | |
shibafu | b8482e0e3c | |
shibafu | a1cb313d4f | |
eai04191 | 53f7a17b8e | |
shibafu | 41039a6650 | |
shibafu | 6d4f6e47a3 | |
shibafu | 47eec65101 | |
shibafu | c2da5eef9d | |
shibafu | 94cabdc827 | |
shibafu | 1e11bd3290 | |
shibafu | f729fa7908 | |
shibafu | cb1b2c9902 | |
shibafu | 9a47bc970f | |
shibafu | 0c6cee6fcb | |
eai04191 | 5172f1cb70 | |
eai04191 | 663888dcb1 | |
eai04191 | 762c232aef | |
eai04191 | e2332c7fe6 | |
eai04191 | 16e5341de1 | |
shibafu | 8c5a7f4d09 | |
shibafu | fe09f769e3 | |
shibafu | 85012e13de | |
shibafu | f19f970bd9 | |
shibafu | d6127a7268 | |
eai04191 | 073ed7e618 | |
eai04191 | 32c1d3ff9d | |
eai04191 | ede45ee4e1 | |
eai04191 | fbecd97c03 | |
eai04191 | 9306a4376c | |
eai04191 | c061a51f8f | |
shibafu | b510ea4042 | |
shibafu | e004a6bced | |
eai04191 | b6864c6fc4 | |
eai04191 | a45d5cd558 | |
eai04191 | 5f1a6291f7 | |
shibafu | ac91e36246 | |
shibafu | 709f10f098 | |
eai04191 | d1da60693a | |
eai04191 | b8f7e5e6ef | |
shibafu | d926a9ea0d | |
shibafu | 65b0893f47 | |
shibafu | b2301444c4 | |
shibafu | e5e0ce08da | |
shibafu | 17ce2784a7 | |
shibafu | d470193662 | |
dependabot[bot] | ea93cc68fa | |
shibafu | 12dac5916e | |
shibafu | f47d3454f6 | |
shibafu | 34cb3a1415 | |
shibafu | 9471683741 | |
shibafu | c7d88076fa | |
shibafu | 0670cb8736 | |
shibafu | 7a95e0979e | |
shibafu | 333f39c9f4 | |
shibafu | 5340d8ead9 | |
dependabot[bot] | 348096ecee | |
shibafu | 35cc0c6357 | |
shibafu | f0cb07c6f5 | |
shibafu | 3f9de593d6 | |
eai04191 | 11ddb83424 | |
eai04191 | 02367646a1 | |
eai04191 | 241bcd6548 | |
eai04191 | f7c8a3010d | |
eai04191 | c6a32da97e | |
eai04191 | 50ff2efaaa | |
eai04191 | a958ccaa08 | |
shibafu | ce3ef3cdff | |
shibafu | f9ea9cc566 | |
shibafu | debc350b12 | |
shibafu | d7f39fcc5a | |
eai04191 | 8e6b96fb83 | |
eai04191 | c20bd0ca77 | |
shibafu | c1330ff6e3 | |
shibafu | 085afd3318 | |
shibafu | a8a6aecef3 | |
shibafu | e4c942263a | |
shibafu | 1322e89b86 | |
MitarashiDango | dbafe2c75e | |
shibafu | 2efb387b6a | |
shibafu | 91c786199a | |
shibafu | ed295cb7bc | |
shibafu | 8de5fa891e | |
shibafu | 464b690fbd | |
shibafu | d02b65e4ed | |
shibafu | 0c18965ade | |
shibafu | 51485a8ac1 | |
shibafu | 59a6aa869f | |
shibafu | b3ea665f0b | |
shibafu | f791fd8fbd | |
shibafu | 676793cfe5 | |
shibafu | 2ff07cc68d | |
shibafu | f95f1592f7 | |
shibafu | d655f24fbf | |
shibafu | b80d74bae1 | |
shibafu | 9b95f3a8b8 | |
shibafu | 859a186acb | |
shibafu | fca6b6e98b | |
shibafu | 26b52e1c87 | |
eai04191 | f895996b18 | |
eai04191 | f19621c04a | |
eai04191 | e8cfc48417 | |
shibafu | 566d288395 | |
shibafu | ddac533539 | |
shibafu | c20a8066c9 | |
shibafu | 664448b9fc | |
shibafu | e9c1726567 | |
shibafu | db7dce5830 | |
shibafu | 4ed2a9048d | |
shibafu | 2cd09402d1 | |
shibafu | 6b1ccc52e5 | |
shibafu | 3cba46bff0 | |
shibafu | 9cec184d21 | |
shibafu | 1b4f621191 | |
shibafu | 5bafe9126a | |
shibafu | fe880ee599 | |
shibafu | a15716bb54 | |
shibafu | 02f03165d6 | |
shibafu | 225d0854ef | |
shibafu | 34b7cd6c89 | |
Eai | 199e5dac05 | |
ImgBotApp | 062034fb83 | |
shibafu | 212fad4d66 | |
shibafu | 04d7116eca | |
shibafu | cc8ee2e520 | |
shibafu | 04f1a344a0 | |
shibafu | 379d4563c5 | |
unarist | ca2aeea4f5 | |
shibafu | 5153de54d2 | |
shibafu | 0e45d27295 | |
shibafu | 8e161252a7 | |
shibafu | a7859fdda6 | |
shibafu | 34df704fdb | |
shibafu | 80fe53cf20 | |
shibafu | b8ceac51f7 | |
shibafu | 14b57bf9f5 | |
shibafu | 40fdb587d7 | |
shibafu | 1fd43b5e38 | |
shibafu | a998d7132f | |
shibafu | 74c8a1b6cb | |
shibafu | e6b333eea4 | |
shibafu | 285e529aea | |
shibafu | a72190eda5 | |
shibafu | ea2db88de3 | |
eai04191 | 54477bb214 | |
hina | 176ccef20f | |
hina | dd4837ef7b | |
shibafu | 9e786a5469 | |
shibafu | df3826a6d4 | |
shibafu | a35b58eb47 | |
shibafu | 27fc5ee6e8 | |
shibafu | be700ab81b | |
shibafu | 53ac4c9b8f | |
shibafu | e69adbfbc3 | |
shibafu | f5fab4b3c1 | |
shibafu | 82ccd623a6 | |
shibafu | e98ed0c3ca | |
shibafu | 2dd5cbd072 | |
shibafu | d044b6db20 | |
shibafu | 8e366870b1 | |
shibafu | 54eec1a861 | |
shibafu | 8a39feff29 | |
shibafu | 8a8ca1a26e | |
shibafu | e262b27d0d | |
eai04191 | bd1976c4cf | |
eai04191 | 3fa2d80507 | |
eai04191 | ec5a78db38 | |
eai04191 | 2db45951f4 | |
shibafu | 00a819de23 | |
eai04191 | 4f559cd1d4 | |
shibafu | 16005931fc | |
shibafu | a58811a8fe | |
shibafu | 89c0b39755 | |
shibafu | 1fb4352e48 | |
shibafu | ad747577e4 | |
eai04191 | a2ee4ef505 | |
eai04191 | 5d1ffca1ee | |
shibafu | 5c612d7eef | |
shibafu | cf0d370e61 | |
shibafu | 91ec1c391e | |
eai04191 | 83d0fb3a7a | |
eai04191 | b8f7b5dfe0 | |
eai04191 | 51fc65e66f | |
eai04191 | a674195db9 | |
shibafu | 1a7b6c8f3c | |
shibafu | 077731495c | |
eai04191 | 7190367936 | |
eai04191 | e1eb359887 | |
eai04191 | 57b10d98ac | |
eai04191 | 7c70e6db7e | |
eai04191 | b430ba7162 | |
shibafu | d9d6c10a34 | |
shibafu | 1a7d958a1e | |
eai04191 | e983a3da0b | |
eai04191 | ed1cfe94f0 | |
shibafu | cd56e1bff3 | |
shibafu | 5db60a4524 | |
shibafu | 3f0171fa8b | |
shibafu | 61498133d5 | |
shibafu | 088901fec7 | |
shibafu | dab1732a1d | |
shibafu | 0b87a35fba | |
hina | 5561f0785c | |
hina | 1ba7df6e82 | |
hina | 94c19235b6 | |
hina | e8438a78a1 | |
eai04191 | da0fc3f3bf | |
eai04191 | d561ee66c2 | |
eai04191 | d571ff1a5b | |
eai04191 | a2f0beb3cb | |
eai04191 | 41778844b8 | |
eai04191 | b29bb23b40 | |
eai04191 | a364de7d03 | |
shibafu | 03fcc424d8 | |
shibafu | ddd2a05607 | |
shibafu | f25312a987 | |
shibafu | f7d9c5c1e6 | |
shibafu | 2e1ec0dfc7 | |
shibafu | f1c56bce83 | |
shibafu | 0ceb0fcf21 | |
shibafu | 37eaefc016 | |
shibafu | db09bf40a6 | |
shibafu | 1348054858 | |
shibafu | 2c396da84e | |
shibafu | d4d98db686 | |
shibafu | 81e37034ce | |
shibafu | 8cdf086296 | |
shibafu | 7f087bf446 | |
shibafu | 6a1848e311 | |
shibafu | 70f007b6d2 | |
shibafu | b7701f39dd | |
shibafu | 7a421d648d | |
shibafu | 8852c6b45b | |
shibafu | cf5bf2b274 | |
shibafu | e925a964f1 | |
shibafu | e966995dea | |
shibafu | f21e58650d | |
shibafu | 70fed1ddf0 | |
unarist | c229947080 | |
shibafu | 08432c847e | |
shibafu | b961956b63 | |
shibafu | 023857f7d7 | |
shibafu | 5276e8d452 | |
shibafu | 3cba54946a | |
shibafu | e6fe522f6d | |
eai04191 | 7eeb0207de | |
eai04191 | 1beb411050 | |
shibafu | eb97f01c79 | |
eai04191 | 0ff253cbfb | |
shibafu | 579bd3e31f | |
shibafu | c80bb69568 | |
eai04191 | 3c6dfeec1c | |
Eai | 30c861d18c | |
Hinaloe | 746db0474a | |
Hinaloe | 307e578d4a | |
shibafu | be6151f0c7 | |
shibafu | c9cdd26728 | |
Eai | d0dd2db159 | |
Eai | ea12f8c9a6 | |
eai04191 | dc98334a6d | |
hina | a14b49b7d2 | |
eai04191 | de740082b7 | |
eai04191 | af964ad82f | |
shibafu | 32b0b76032 | |
hina | db3ba04091 | |
hina | 0400bc771c | |
shibafu | a5b4eeee36 | |
shibafu | f760ea7093 | |
shibafu | 0f4dfcd816 | |
hina | a934a7fc35 | |
hina | 51f097fdf0 | |
hina | 24dee801ad | |
hina | 9f1cd607d7 | |
hina | 4196b1a02d | |
Eai | 35789befc5 | |
shibafu | 32139cb9da | |
shibafu | 9244b8424d | |
shibafu | 55eb95dda8 | |
shibafu | 72e9d4e3e8 | |
shibafu | 852f1ac88c | |
shibafu | f09ae32b00 | |
shibafu | 33be0ac8ef | |
shibafu | 9f2e73e511 | |
shibafu | e36b9c7c1b | |
shibafu | 09bb98876c | |
Eai | cedee0a20e | |
unarist | 116dd3b798 | |
unarist | 735bb00eba | |
shibafu | decb1707f1 | |
shibafu | bec7bdeb36 | |
shibafu | 7f5a4a06d9 | |
unarist | d7c7f86ba5 | |
unarist | ca212b547a | |
unarist | 3584625b47 | |
unarist | 1ba4999a83 | |
shibafu | 03e1c2d60c | |
shibafu | ab0695ee8d | |
shibafu | fcafc3c704 | |
shibafu | 7a606be3ba | |
shibafu | b4a7ec64dd | |
unarist | 5750eeb3a5 | |
shibafu | b4dc07a9a3 | |
shibafu | a77ac3f039 | |
shibafu | eef0eac887 | |
Eai | 7337f60491 | |
shibafu | 85e9599654 | |
shibafu | 8a919ca62a | |
shibafu | d105568c76 | |
shibafu | 1f7723614d | |
shibafu | 41e810c788 | |
shibafu | 82af423c57 | |
shibafu | 4346e1a701 | |
Eai | 96199c9e46 | |
shibafu | 4962244969 | |
unarist | c417dabff2 | |
shibafu | d9bf673d85 | |
shibafu | 5961d3e27a | |
unarist | b57a272611 | |
shibafu | cbbb2605dd | |
Eai | e20bb75e00 | |
eai04191 | e226f43265 | |
shibafu | e887f2d83e | |
shibafu | 1835776a9c | |
shibafu | 57715b9a82 | |
shibafu | e320f85c73 | |
shibafu | f5f4cbb5b6 | |
shibafu | 4ab82ff0e2 | |
shibafu | 8c73bda2ac | |
shibafu | 3c6f802b69 | |
shibafu | 895e9f4b15 | |
shibafu | 648e171a57 | |
shibafu | dc91180dd4 | |
Eai | bbbffcb39e | |
shibafu | 56831c78c3 | |
shibafu | a30919991c | |
shibafu | 8aa2e6a779 | |
shibafu | 34f45d1ce8 | |
shibafu | 6d66425fc9 | |
shibafu | 626c85c07d | |
shibafu | b6bf1f99d8 | |
eai04191 | 907eb87723 | |
eai04191 | 72ec5d8d26 | |
eai04191 | ca5be696c8 | |
eai04191 | 48ddac8c85 | |
shibafu | a2580f29cc | |
shibafu | 85cc865545 | |
shibafu | 5d256519c6 | |
shibafu | 53b459740f | |
shibafu | 550d897561 | |
shibafu | 27532685ba | |
shibafu | 4654962aac | |
Eai | ef563f8641 | |
Eai | 7745b68dae | |
Eai | e5ea0528a8 | |
Eai | 4e1eec66be | |
shibafu | a33a0e542c | |
Eai | 473280d9d2 | |
Eai | 73c697f119 | |
shibafu | 09482ca2c5 | |
shibafu | 3a1dc72cf7 | |
Eai | b04f167709 | |
shibafu | a3b328b55f | |
shibafu | 497c19d06d | |
eai04191 | ade52f40f4 | |
eai04191 | c38d3fa799 | |
shibafu | 2a91aac569 | |
eai04191 | f367ec212f | |
eai04191 | 735c7c289a | |
eai04191 | cffe539832 | |
eai04191 | 2191a96cac | |
eai04191 | 64f8b47ae0 | |
shibafu | 2ca6c4c60d | |
shibafu | 0d4a61ef15 | |
shibafu | cef23a64cb | |
shibafu | cd26ef6236 | |
shibafu | 810eea2a59 | |
shibafu | acb9b5821d | |
shibafu | 5f01cc3430 | |
eai04191 | 938a4d6957 | |
eai04191 | a3813f19cf | |
shibafu | faf0755ebd | |
shibafu | a2f797cbbe | |
shibafu | 8c6cc0692c | |
eai04191 | dcf31865a1 | |
eai04191 | 3dedb57fe4 | |
eai04191 | f134cbefa8 | |
eai04191 | 72ab8bf101 | |
eai04191 | 11836ddd43 | |
shibafu | 5c6417cdbe | |
mohemohe | 0e410ef342 | |
eai04191 | d6e981ac39 | |
eai04191 | 98e933b833 | |
eai04191 | 6ff247acd7 | |
eai04191 | 2d04ed8dd7 | |
eai04191 | d359a41033 | |
shibafu | 2299ac3fe7 | |
shibafu | fcdb9d7aba | |
shibafu | e6abcc4402 | |
eai04191 | b0a7504691 | |
eai04191 | a645cb497f | |
eai04191 | 6105f6c860 | |
Eai | 9eb42f1991 | |
eai04191 | dfea7f2f24 | |
shibafu | 7441c26694 | |
shibafu | 630be41833 | |
shibafu | c4a69cccbe | |
eai04191 | cb3f060ba6 | |
eai04191 | 4d7b70f9ad | |
shibafu | 3bb6bf718e | |
eai04191 | fc709a6624 | |
eai04191 | 8d5363e978 | |
eai04191 | 71137e9ab4 | |
shibafu | f7a95befbe | |
shibafu | 908790b53d | |
shibafu | 20799dd757 | |
shibafu | ca02f21812 | |
shibafu | 168ef1c5f6 | |
shibafu | 3c2e475e41 | |
shibafu | 581c1ed952 | |
shibafu | ad5fbc7ada | |
shibafu | cf1757319e | |
shibafu | b39b43e705 | |
shibafu | bcc9f3acda | |
shibafu | 911957b283 | |
shibafu | b9e29cc283 | |
shibafu | 316b453cca | |
shibafu | e6b28993de | |
shibafu | e875d5da02 | |
shibafu | 233a54eb3e | |
shibafu | 515e24c4e4 | |
shibafu | af4b60d6e1 | |
shibafu | 5bb44ab232 | |
shibafu | 874e88cc56 | |
shibafu | bdb1640ceb | |
shibafu | c52046d51e | |
shibafu | 69f212d705 | |
shibafu | 2441fe78b6 | |
shibafu | 9646f90ce3 | |
shibafu | 9bd22d9f77 | |
shibafu | a0e4063c47 | |
shibafu | 0882063c0b | |
shibafu | b4e40ab748 | |
shibafu | 6609965360 | |
shibafu | 9705c2ce5a | |
shibafu | bd93d9ec24 | |
shibafu | 55bd35ea49 | |
shibafu | 4dc4efe10d | |
shibafu | dfe149e969 | |
shibafu | f51aaea94c | |
shibafu | 1dea0a077c | |
shibafu | d143dc4d84 | |
shibafu | e033816eab | |
shibafu | 503e8ba093 | |
shibafu | d224e6bba4 | |
shibafu | 3b2e81818b | |
shibafu | 7ca0acacb4 | |
shibafu | 88456bc609 | |
shibafu | 0f39b502e8 | |
shibafu | 46f049c2b8 | |
shibafu | 9cdfadf12c | |
shibafu | bc57a482be | |
shibafu | ba53156beb | |
Shibafu | 5b2427a2c9 | |
shibafu | 277ee90379 | |
shibafu | 336d368369 | |
shibafu | 917675f9bd | |
shibafu | 0c216e79c2 | |
shibafu | e0942889e9 | |
shibafu | 24a3b0ebb5 | |
shibafu | b1dcc36565 | |
shibafu | d7b16cd6d5 | |
shibafu | 6bfc1425a6 | |
shibafu | 767947ee6c | |
shibafu | efd5eab5e6 | |
shibafu | eb1884200b | |
shibafu | 6c90cc2383 | |
shibafu | ef38485dfe | |
shibafu | b5a723cb3c | |
shibafu | 2bd56a8606 | |
shibafu | 7a386d4d89 | |
shibafu | f2ed4f85ee | |
shibafu | 69f619e2af | |
shibafu | 3ea9476aa8 | |
shibafu | b562b3b400 | |
shibafu | 2fe3d7ac49 | |
shibafu | 7a3a1c1ada | |
shibafu | c04ec89c3e | |
shibafu | bd19018699 | |
shibafu | 4f475aed3e | |
shibafu | 6ace47c27e | |
shibafu | 88456abb15 | |
shibafu | 9d6a76a7f8 | |
shibafu | 6d0472c14b | |
shibafu | 58258f8fe3 | |
shibafu | 2038b5e7c3 | |
shibafu | 95204736ef | |
shibafu | 7e9501ab9d | |
shibafu | d0c9e3a3af | |
shibafu | fca4c21d3a | |
shibafu | 016d4d8e3f | |
shibafu | 94918fc337 | |
shibafu | 723bb236c9 | |
shibafu | 6ed0938694 | |
shibafu | 53f34c12cc | |
shibafu | efea60fc81 | |
shibafu | d94c444b55 | |
shibafu | edcc2bceaf | |
shibafu | abf8b05253 | |
shibafu | e9b19e7be1 | |
shibafu | 70421eabb0 | |
shibafu | b5f04cb153 | |
shibafu | 70ae2fd982 | |
shibafu | 1dbc573833 | |
shibafu | eb6487c4cf | |
shibafu | df07aecd88 | |
shibafu | 12d06dc88b |
|
@ -0,0 +1,177 @@
|
|||
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 pecl install -f xdebug-2.9.8 && sudo docker-php-ext-enable xdebug
|
||||
- run: sudo docker-php-ext-install zip
|
||||
- run: sudo docker-php-ext-install pdo_pgsql
|
||||
- run:
|
||||
command: |
|
||||
curl -sSL "https://nodejs.org/dist/v12.16.3/node-v12.16.3-linux-x64.tar.xz" | sudo tar --strip-components=2 -xJ -C /usr/local/bin/ node-v12.16.3-linux-x64/bin/node
|
||||
curl https://www.npmjs.com/install.sh | sudo bash
|
||||
restore_composer:
|
||||
steps:
|
||||
- restore_cache:
|
||||
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 eslint
|
||||
- run:
|
||||
name: eslint
|
||||
command: yarn run eslint
|
||||
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
|
||||
|
|
@ -0,0 +1,4 @@
|
|||
.idea
|
||||
.git
|
||||
.gitignore
|
||||
.gitattributes
|
|
@ -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,yaml}]
|
||||
indent_size = 2
|
||||
|
||||
[*.json]
|
||||
indent_size = 2
|
||||
|
||||
[composer.json]
|
||||
indent_size = 4
|
50
.env.example
50
.env.example
|
@ -1,16 +1,23 @@
|
|||
APP_NAME=Laravel
|
||||
APP_NAME=Tissue
|
||||
APP_ENV=local
|
||||
APP_KEY=
|
||||
APP_DEBUG=true
|
||||
APP_LOG_LEVEL=debug
|
||||
APP_URL=http://localhost
|
||||
|
||||
DB_CONNECTION=mysql
|
||||
DB_HOST=127.0.0.1
|
||||
DB_PORT=3306
|
||||
DB_DATABASE=homestead
|
||||
DB_USERNAME=homestead
|
||||
DB_PASSWORD=secret
|
||||
LOG_CHANNEL=stack
|
||||
|
||||
# テストにモックを使用するか falseの場合は実際のHTML等を取得してテストする
|
||||
TEST_USE_HTTP_MOCK=true
|
||||
|
||||
# テスト用のスナップショットを更新する場合はtrueにする (TEST_USE_HTTP_MOCKと重複させないよう注意)
|
||||
TEST_UPDATE_SNAPSHOT=false
|
||||
|
||||
DB_CONNECTION=pgsql
|
||||
DB_HOST=db
|
||||
DB_PORT=5432
|
||||
DB_DATABASE=tissue
|
||||
DB_USERNAME=tissue
|
||||
DB_PASSWORD=tissue
|
||||
|
||||
BROADCAST_DRIVER=log
|
||||
CACHE_DRIVER=file
|
||||
|
@ -22,12 +29,31 @@ REDIS_PASSWORD=null
|
|||
REDIS_PORT=6379
|
||||
|
||||
MAIL_DRIVER=smtp
|
||||
MAIL_HOST=smtp.mailtrap.io
|
||||
MAIL_PORT=2525
|
||||
MAIL_USERNAME=null
|
||||
MAIL_HOST=smtp.sparkpostmail.com
|
||||
MAIL_PORT=587
|
||||
MAIL_USERNAME=SMTP_Injection
|
||||
MAIL_PASSWORD=null
|
||||
MAIL_ENCRYPTION=null
|
||||
MAIL_ENCRYPTION=tls
|
||||
MAIL_FROM_ADDRESS=support@mail.shikorism.net
|
||||
MAIL_FROM_NAME=Tissue
|
||||
|
||||
AWS_ACCESS_KEY_ID=
|
||||
AWS_SECRET_ACCESS_KEY=
|
||||
AWS_DEFAULT_REGION=us-east-1
|
||||
AWS_BUCKET=
|
||||
|
||||
SPARKPOST_SECRET=
|
||||
|
||||
PUSHER_APP_ID=
|
||||
PUSHER_APP_KEY=
|
||||
PUSHER_APP_SECRET=
|
||||
|
||||
MIX_PUSHER_APP_KEY="${PUSHER_APP_KEY}"
|
||||
MIX_PUSHER_APP_CLUSTER="${PUSHER_APP_CLUSTER}"
|
||||
|
||||
# (Optional) reCAPTCHA Key
|
||||
# https://www.google.com/recaptcha
|
||||
NOCAPTCHA_SECRET=
|
||||
NOCAPTCHA_SITEKEY=
|
||||
|
||||
SENTRY_LARAVEL_DSN=
|
||||
|
|
|
@ -0,0 +1,38 @@
|
|||
module.exports = {
|
||||
env: {
|
||||
browser: true,
|
||||
es2020: true,
|
||||
node: true,
|
||||
},
|
||||
extends: [
|
||||
'eslint:recommended',
|
||||
'plugin:react/recommended',
|
||||
'plugin:prettier/recommended',
|
||||
'plugin:@typescript-eslint/recommended',
|
||||
'prettier',
|
||||
'prettier/@typescript-eslint',
|
||||
'prettier/react',
|
||||
],
|
||||
parser: '@typescript-eslint/parser',
|
||||
parserOptions: {
|
||||
ecmaVersion: 11,
|
||||
ecmaFeatures: {
|
||||
jsx: true,
|
||||
},
|
||||
sourceType: 'module',
|
||||
},
|
||||
plugins: ['prettier', 'react', '@typescript-eslint', 'jquery'],
|
||||
rules: {
|
||||
'@typescript-eslint/explicit-module-boundary-types': 0,
|
||||
'@typescript-eslint/no-explicit-any': 0,
|
||||
'@typescript-eslint/no-unused-vars': ['warn', { argsIgnorePattern: '^_' }],
|
||||
'jquery/no-ajax': 2,
|
||||
'jquery/no-ajax-events': 2,
|
||||
'react/prop-types': 0,
|
||||
},
|
||||
settings: {
|
||||
react: {
|
||||
version: 'detect',
|
||||
},
|
||||
},
|
||||
};
|
|
@ -3,3 +3,4 @@
|
|||
*.scss linguist-vendored
|
||||
*.js linguist-vendored
|
||||
CHANGELOG.md export-ignore
|
||||
*.sh text eol=lf
|
|
@ -0,0 +1,14 @@
|
|||
version: 2
|
||||
updates:
|
||||
|
||||
# Maintain dependencies for npm
|
||||
- package-ecosystem: "npm"
|
||||
directory: "/"
|
||||
schedule:
|
||||
interval: "daily"
|
||||
|
||||
# Maintain dependencies for Composer
|
||||
- package-ecosystem: "composer"
|
||||
directory: "/"
|
||||
schedule:
|
||||
interval: "daily"
|
|
@ -1,13 +1,26 @@
|
|||
/node_modules
|
||||
/public/css
|
||||
/public/fonts
|
||||
/public/js
|
||||
/public/hot
|
||||
/public/storage
|
||||
/public/mix-manifest.json
|
||||
/public/report.html
|
||||
/public/apidoc.html
|
||||
/storage/*.key
|
||||
/vendor
|
||||
/.idea
|
||||
/.vscode
|
||||
/.vagrant
|
||||
Homestead.json
|
||||
Homestead.yaml
|
||||
npm-debug.log
|
||||
yarn-error.log
|
||||
.env
|
||||
*.iml
|
||||
.env.backup
|
||||
.phpunit.result.cache
|
||||
*.iml
|
||||
.php_cs
|
||||
.php_cs.cache
|
||||
.phpstorm.meta.php
|
||||
_ide_helper*.php
|
||||
|
|
|
@ -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__)
|
||||
);
|
|
@ -0,0 +1,5 @@
|
|||
{
|
||||
"arrowParens": "always",
|
||||
"singleQuote": true,
|
||||
"printWidth": 120
|
||||
}
|
|
@ -0,0 +1 @@
|
|||
/tests/fixture/*
|
|
@ -0,0 +1,33 @@
|
|||
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 libicu-dev \
|
||||
&& docker-php-ext-install pdo_pgsql intl \
|
||||
&& pecl install xdebug \
|
||||
&& curl -sS https://getcomposer.org/installer | php \
|
||||
&& mv composer.phar /usr/local/bin/composer \
|
||||
&& 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
|
||||
|
||||
シコリズムネットにて提供している夜のライフログサービスです。
|
||||
シコリズムネットにて提供している夜のライフログサービスです。
|
||||
(思想的には [shibafu528/SperMaster](https://github.com/shibafu528/SperMaster) の後継となります)
|
||||
|
||||
## 構成
|
||||
* Laravel 5.4
|
||||
* Materialize 0.99.0 (Materialでデザインする気が失せたので変更予定)
|
||||
|
||||
- Laravel 6
|
||||
- Bootstrap 4.4.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 等を平気で使います。
|
||||
|
|
|
@ -0,0 +1,39 @@
|
|||
<?php
|
||||
|
||||
namespace App;
|
||||
|
||||
use Illuminate\Database\Eloquent\Model;
|
||||
use Illuminate\Database\Eloquent\SoftDeletes;
|
||||
use Illuminate\Support\Str;
|
||||
|
||||
class CheckinWebhook extends Model
|
||||
{
|
||||
use SoftDeletes;
|
||||
|
||||
/** @var int ユーザーごとの作成数制限 */
|
||||
const PER_USER_LIMIT = 10;
|
||||
|
||||
public $incrementing = false;
|
||||
protected $keyType = 'string';
|
||||
|
||||
protected $fillable = ['name'];
|
||||
|
||||
protected static function boot()
|
||||
{
|
||||
parent::boot();
|
||||
|
||||
self::creating(function (CheckinWebhook $webhook) {
|
||||
$webhook->id = Str::random(64);
|
||||
});
|
||||
}
|
||||
|
||||
public function user()
|
||||
{
|
||||
return $this->belongsTo(User::class);
|
||||
}
|
||||
|
||||
public function isAvailable()
|
||||
{
|
||||
return $this->user !== null;
|
||||
}
|
||||
}
|
|
@ -0,0 +1,113 @@
|
|||
<?php
|
||||
|
||||
namespace App\Console\Commands;
|
||||
|
||||
use App\Tag;
|
||||
use Illuminate\Console\Command;
|
||||
use Illuminate\Support\Facades\DB;
|
||||
|
||||
class DedupTags extends Command
|
||||
{
|
||||
/**
|
||||
* The name and signature of the console command.
|
||||
*
|
||||
* @var string
|
||||
*/
|
||||
protected $signature = 'tissue:tag:dedup {--dry-run}';
|
||||
|
||||
/**
|
||||
* The console command description.
|
||||
*
|
||||
* @var string
|
||||
*/
|
||||
protected $description = 'Deduplicate tags';
|
||||
|
||||
/**
|
||||
* Create a new command instance.
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
public function __construct()
|
||||
{
|
||||
parent::__construct();
|
||||
}
|
||||
|
||||
/**
|
||||
* Execute the console command.
|
||||
*
|
||||
* @return mixed
|
||||
*/
|
||||
public function handle()
|
||||
{
|
||||
if ($this->option('dry-run')) {
|
||||
$this->warn('dry-runモードで実行します。');
|
||||
} else {
|
||||
if (!$this->confirm('dry-runオプションが付いてないけど、本当に実行しますか?')) {
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
DB::transaction(function () {
|
||||
$duplicatedTags = DB::table('tags')
|
||||
->select('name', DB::raw('count(*)'))
|
||||
->groupBy('name')
|
||||
->having(DB::raw('count(*)'), '>=', 2)
|
||||
->get();
|
||||
|
||||
$this->info($duplicatedTags->count() . ' duplicated tags found.');
|
||||
|
||||
foreach ($duplicatedTags as $tag) {
|
||||
$this->line('Tag name: ' . $tag->name);
|
||||
|
||||
$tagIds = Tag::where('name', $tag->name)->orderBy('id')->pluck('id');
|
||||
$newId = $tagIds->first();
|
||||
$dropIds = $tagIds->slice(1);
|
||||
|
||||
$this->line(' New ID: ' . $newId);
|
||||
$this->line(' Drop IDs: ' . $dropIds->implode(', '));
|
||||
|
||||
if ($this->option('dry-run')) {
|
||||
continue;
|
||||
}
|
||||
|
||||
// 同じタグ名でIDが違うものについて、全て統一する
|
||||
foreach (['ejaculation_tag', 'metadata_tag'] as $table) {
|
||||
DB::table($table)
|
||||
->whereIn('tag_id', $dropIds)
|
||||
->update(['tag_id' => $newId]);
|
||||
}
|
||||
DB::table('tags')->whereIn('id', $dropIds)->delete();
|
||||
|
||||
// 統一した上で、重複しているレコードを削除する
|
||||
DB::delete(
|
||||
<<<SQL
|
||||
DELETE FROM ejaculation_tag
|
||||
WHERE id IN (
|
||||
SELECT id
|
||||
FROM (
|
||||
SELECT id, row_number() OVER (PARTITION BY ejaculation_id, tag_id ORDER BY id) AS ord
|
||||
FROM ejaculation_tag
|
||||
) t
|
||||
WHERE ord > 1
|
||||
)
|
||||
SQL
|
||||
);
|
||||
DB::delete(
|
||||
<<<SQL
|
||||
DELETE FROM metadata_tag
|
||||
WHERE id IN (
|
||||
SELECT id
|
||||
FROM (
|
||||
SELECT id, row_number() OVER (PARTITION BY metadata_url, tag_id ORDER BY id) AS ord
|
||||
FROM metadata_tag
|
||||
) t
|
||||
WHERE ord > 1
|
||||
)
|
||||
SQL
|
||||
);
|
||||
}
|
||||
});
|
||||
|
||||
$this->info('Done!');
|
||||
}
|
||||
}
|
|
@ -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.');
|
||||
}
|
||||
}
|
||||
}
|
|
@ -0,0 +1,61 @@
|
|||
<?php
|
||||
|
||||
namespace App\Console\Commands;
|
||||
|
||||
use App\Tag;
|
||||
use App\Utilities\Formatter;
|
||||
use DB;
|
||||
use Illuminate\Console\Command;
|
||||
|
||||
class NormalizeTags extends Command
|
||||
{
|
||||
/**
|
||||
* The name and signature of the console command.
|
||||
*
|
||||
* @var string
|
||||
*/
|
||||
protected $signature = 'tissue:tag:normalize';
|
||||
|
||||
/**
|
||||
* The console command description.
|
||||
*
|
||||
* @var string
|
||||
*/
|
||||
protected $description = 'Normalize tags';
|
||||
|
||||
private $formatter;
|
||||
|
||||
/**
|
||||
* Create a new command instance.
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
public function __construct(Formatter $formatter)
|
||||
{
|
||||
parent::__construct();
|
||||
$this->formatter = $formatter;
|
||||
}
|
||||
|
||||
/**
|
||||
* Execute the console command.
|
||||
*
|
||||
* @return mixed
|
||||
*/
|
||||
public function handle()
|
||||
{
|
||||
$start = hrtime(true);
|
||||
|
||||
DB::transaction(function () {
|
||||
/** @var Tag $tag */
|
||||
foreach (Tag::query()->cursor() as $tag) {
|
||||
$normalizedName = $this->formatter->normalizeTagName($tag->name);
|
||||
$this->line("{$tag->name} : {$normalizedName}");
|
||||
$tag->normalized_name = $normalizedName;
|
||||
$tag->save();
|
||||
}
|
||||
});
|
||||
|
||||
$elapsed = (hrtime(true) - $start) / 1e+9;
|
||||
$this->info("Done! ({$elapsed} sec)");
|
||||
}
|
||||
}
|
|
@ -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;
|
||||
|
||||
use App\Console\Commands\DemoteUser;
|
||||
use App\Console\Commands\PromoteUser;
|
||||
use Illuminate\Console\Scheduling\Schedule;
|
||||
use Illuminate\Foundation\Console\Kernel as ConsoleKernel;
|
||||
|
||||
|
@ -35,6 +37,8 @@ class Kernel extends ConsoleKernel
|
|||
*/
|
||||
protected function commands()
|
||||
{
|
||||
$this->load(__DIR__.'/Commands');
|
||||
|
||||
require base_path('routes/console.php');
|
||||
}
|
||||
}
|
||||
|
|
|
@ -0,0 +1,24 @@
|
|||
<?php
|
||||
|
||||
namespace App;
|
||||
|
||||
use Illuminate\Database\Eloquent\Model;
|
||||
|
||||
class ContentProvider extends Model
|
||||
{
|
||||
public $incrementing = false;
|
||||
protected $primaryKey = 'host';
|
||||
protected $keyType = 'string';
|
||||
|
||||
protected $fillable = [
|
||||
'host',
|
||||
'robots',
|
||||
'robots_cached_at',
|
||||
];
|
||||
|
||||
protected $dates = [
|
||||
'created_at',
|
||||
'updated_at',
|
||||
'robots_cached_at',
|
||||
];
|
||||
}
|
|
@ -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,15 +2,136 @@
|
|||
|
||||
namespace App;
|
||||
|
||||
use Illuminate\Database\Eloquent\Builder;
|
||||
use Illuminate\Database\Eloquent\Model;
|
||||
use Illuminate\Support\Facades\Auth;
|
||||
use Illuminate\Support\Facades\DB;
|
||||
use Staudenmeir\EloquentEagerLimit\HasEagerLimit;
|
||||
|
||||
class Ejaculation extends Model
|
||||
{
|
||||
//
|
||||
use HasEagerLimit;
|
||||
|
||||
const SOURCE_WEB = 'web';
|
||||
const SOURCE_CSV = 'csv';
|
||||
const SOURCE_WEBHOOK = 'webhook';
|
||||
|
||||
protected $fillable = [
|
||||
'user_id', 'ejaculated_date',
|
||||
'note', 'geo_latitude', 'geo_longitude',
|
||||
'is_private'
|
||||
'note', 'geo_latitude', 'geo_longitude', 'link', 'source',
|
||||
'is_private', 'is_too_sensitive', 'discard_elapsed_time',
|
||||
'checkin_webhook_id'
|
||||
];
|
||||
|
||||
protected $dates = [
|
||||
'ejaculated_date'
|
||||
];
|
||||
|
||||
public function user()
|
||||
{
|
||||
return $this->belongsTo('App\User');
|
||||
}
|
||||
|
||||
public function tags()
|
||||
{
|
||||
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 scopeVisibleToTimeline(Builder $query)
|
||||
{
|
||||
return $query->whereIn('ejaculations.source', [Ejaculation::SOURCE_WEB, Ejaculation::SOURCE_WEBHOOK]);
|
||||
}
|
||||
|
||||
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_private' => $this->is_private,
|
||||
'is_too_sensitive' => $this->is_too_sensitive,
|
||||
]);
|
||||
}
|
||||
|
||||
public function ejaculatedSpan(): string
|
||||
{
|
||||
if (array_key_exists('ejaculated_span', $this->attributes)) {
|
||||
if ($this->ejaculated_span === null) {
|
||||
return '精通';
|
||||
}
|
||||
if ($this->discard_elapsed_time) {
|
||||
return '0日 0時間 0分'; // TODO: 気の効いたフレーズにする
|
||||
}
|
||||
|
||||
return $this->ejaculated_span;
|
||||
} else {
|
||||
$previous = Ejaculation::select('ejaculated_date')
|
||||
->where('user_id', $this->user_id)
|
||||
->where('ejaculated_date', '<', $this->ejaculated_date)
|
||||
->orderByDesc('ejaculated_date')
|
||||
->first();
|
||||
|
||||
if ($previous === null) {
|
||||
return '精通';
|
||||
}
|
||||
if ($this->discard_elapsed_time) {
|
||||
return '0日 0時間 0分';
|
||||
}
|
||||
|
||||
return $this->ejaculated_date->diff($previous->ejaculated_date)->format('%a日 %h時間 %i分');
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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;
|
||||
}
|
||||
}
|
|
@ -0,0 +1,30 @@
|
|||
<?php
|
||||
|
||||
namespace App\Exceptions;
|
||||
|
||||
use Illuminate\Support\Arr;
|
||||
use Throwable;
|
||||
|
||||
class CsvImportException extends \RuntimeException
|
||||
{
|
||||
/** @var string[] */
|
||||
private $errors;
|
||||
|
||||
/**
|
||||
* CsvImportException constructor.
|
||||
* @param string[] $errors
|
||||
*/
|
||||
public function __construct(...$errors)
|
||||
{
|
||||
parent::__construct(Arr::first($errors));
|
||||
$this->errors = $errors;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return string[]
|
||||
*/
|
||||
public function getErrors(): array
|
||||
{
|
||||
return $this->errors;
|
||||
}
|
||||
}
|
|
@ -4,7 +4,10 @@ namespace App\Exceptions;
|
|||
|
||||
use Exception;
|
||||
use Illuminate\Auth\AuthenticationException;
|
||||
use Illuminate\Database\Eloquent\ModelNotFoundException;
|
||||
use Illuminate\Foundation\Exceptions\Handler as ExceptionHandler;
|
||||
use Illuminate\Http\JsonResponse;
|
||||
use Symfony\Component\HttpKernel\Exception\NotFoundHttpException;
|
||||
|
||||
class Handler extends ExceptionHandler
|
||||
{
|
||||
|
@ -20,18 +23,23 @@ class Handler extends ExceptionHandler
|
|||
\Illuminate\Database\Eloquent\ModelNotFoundException::class,
|
||||
\Illuminate\Session\TokenMismatchException::class,
|
||||
\Illuminate\Validation\ValidationException::class,
|
||||
\App\MetadataResolver\ResolverCircuitBreakException::class,
|
||||
];
|
||||
|
||||
/**
|
||||
* Report or log an exception.
|
||||
*
|
||||
* This is a great spot to send exceptions to Sentry, Bugsnag, etc.
|
||||
*
|
||||
* @param \Exception $exception
|
||||
* @return void
|
||||
*
|
||||
* @throws \Exception
|
||||
*/
|
||||
public function report(Exception $exception)
|
||||
{
|
||||
if (app()->bound('sentry') && $this->shouldReport($exception)) {
|
||||
app('sentry')->captureException($exception);
|
||||
}
|
||||
|
||||
parent::report($exception);
|
||||
}
|
||||
|
||||
|
@ -40,7 +48,9 @@ class Handler extends ExceptionHandler
|
|||
*
|
||||
* @param \Illuminate\Http\Request $request
|
||||
* @param \Exception $exception
|
||||
* @return \Illuminate\Http\Response
|
||||
* @return \Symfony\Component\HttpFoundation\Response
|
||||
*
|
||||
* @throws \Exception
|
||||
*/
|
||||
public function render($request, Exception $exception)
|
||||
{
|
||||
|
@ -62,4 +72,28 @@ class Handler extends ExceptionHandler
|
|||
|
||||
return redirect()->guest(route('login'));
|
||||
}
|
||||
|
||||
protected function prepareException(Exception $e)
|
||||
{
|
||||
if (!config('app.debug') && $e instanceof ModelNotFoundException) {
|
||||
return new NotFoundHttpException('Resource not found.', $e);
|
||||
}
|
||||
|
||||
return parent::prepareException($e);
|
||||
}
|
||||
|
||||
protected function prepareJsonResponse($request, Exception $e)
|
||||
{
|
||||
$status = $this->isHttpException($e) ? $e->getStatusCode() : 500;
|
||||
|
||||
return new JsonResponse(
|
||||
[
|
||||
'status' => $status,
|
||||
'error' => $this->convertExceptionToArray($e),
|
||||
],
|
||||
$status,
|
||||
$this->isHttpException($e) ? $e->getHeaders() : [],
|
||||
JSON_PRETTY_PRINT | JSON_UNESCAPED_SLASHES
|
||||
);
|
||||
}
|
||||
}
|
||||
|
|
|
@ -0,0 +1,13 @@
|
|||
<?php
|
||||
|
||||
namespace App\Facades;
|
||||
|
||||
use Illuminate\Support\Facades\Facade;
|
||||
|
||||
class Formatter extends Facade
|
||||
{
|
||||
protected static function getFacadeAccessor()
|
||||
{
|
||||
return \App\Utilities\Formatter::class;
|
||||
}
|
||||
}
|
|
@ -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');
|
||||
}
|
||||
}
|
|
@ -0,0 +1,73 @@
|
|||
<?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(Information $info)
|
||||
{
|
||||
return view('admin.info.edit')->with([
|
||||
'info' => $info,
|
||||
'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', 'お知らせを削除しました。');
|
||||
}
|
||||
}
|
|
@ -0,0 +1,31 @@
|
|||
<?php
|
||||
|
||||
namespace App\Http\Controllers\Api;
|
||||
|
||||
use App\MetadataResolver\DeniedHostException;
|
||||
use App\Services\MetadataResolveService;
|
||||
use Illuminate\Http\Request;
|
||||
|
||||
class CardController
|
||||
{
|
||||
public function show(Request $request, MetadataResolveService $service)
|
||||
{
|
||||
$request->validate([
|
||||
'url:required|url'
|
||||
]);
|
||||
|
||||
try {
|
||||
$metadata = $service->execute($request->input('url'));
|
||||
} catch (DeniedHostException $e) {
|
||||
abort(403, $e->getMessage());
|
||||
}
|
||||
$metadata->load('tags');
|
||||
|
||||
$response = response($metadata);
|
||||
if (!config('app.debug')) {
|
||||
$response = $response->setCache(['public' => true, 'max_age' => 86400]);
|
||||
}
|
||||
|
||||
return $response;
|
||||
}
|
||||
}
|
|
@ -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
|
||||
];
|
||||
}
|
||||
}
|
|
@ -0,0 +1,105 @@
|
|||
<?php
|
||||
|
||||
namespace App\Http\Controllers\Api;
|
||||
|
||||
use App\CheckinWebhook;
|
||||
use App\Ejaculation;
|
||||
use App\Events\LinkDiscovered;
|
||||
use App\Http\Controllers\Controller;
|
||||
use App\Http\Resources\EjaculationResource;
|
||||
use App\Tag;
|
||||
use Carbon\Carbon;
|
||||
use Illuminate\Http\Request;
|
||||
use Illuminate\Support\Facades\DB;
|
||||
use Illuminate\Support\Facades\Validator;
|
||||
use Illuminate\Validation\ValidationException;
|
||||
|
||||
class WebhookController extends Controller
|
||||
{
|
||||
public function checkin(CheckinWebhook $webhook, Request $request)
|
||||
{
|
||||
if (!$webhook->isAvailable()) {
|
||||
return response()->json([
|
||||
'status' => 404,
|
||||
'error' => [
|
||||
'message' => 'The webhook is unavailable'
|
||||
]
|
||||
], 404);
|
||||
}
|
||||
|
||||
$validator = Validator::make($request->all(), [
|
||||
'checked_in_at' => 'nullable|date|after_or_equal:2000-01-01 00:00:00|before_or_equal:2099-12-31 23:59:59',
|
||||
'note' => 'nullable|string|max:500',
|
||||
'link' => 'nullable|url|max:2000',
|
||||
'tags' => 'nullable|array',
|
||||
'tags.*' => ['string', 'not_regex:/[\s\r\n]/u', 'max:255'],
|
||||
'is_private' => 'nullable|boolean',
|
||||
'is_too_sensitive' => 'nullable|boolean',
|
||||
'discard_elapsed_time' => 'nullable|boolean',
|
||||
], [
|
||||
'tags.*.not_regex' => 'The :attribute cannot contain spaces, tabs and newlines.'
|
||||
]);
|
||||
|
||||
try {
|
||||
$inputs = $validator->validate();
|
||||
} catch (ValidationException $e) {
|
||||
return response()->json([
|
||||
'status' => 422,
|
||||
'error' => [
|
||||
'message' => 'Validation failed',
|
||||
'violations' => $validator->errors()->all(),
|
||||
]
|
||||
], 422);
|
||||
}
|
||||
|
||||
$ejaculatedDate = empty($inputs['checked_in_at']) ? now() : new Carbon($inputs['checked_in_at']);
|
||||
$ejaculatedDate = $ejaculatedDate->setTimezone(date_default_timezone_get())->startOfMinute();
|
||||
if (Ejaculation::where(['user_id' => $webhook->user_id, 'ejaculated_date' => $ejaculatedDate])->count()) {
|
||||
return response()->json([
|
||||
'status' => 422,
|
||||
'error' => [
|
||||
'message' => 'Checkin already exists in this time',
|
||||
]
|
||||
], 422);
|
||||
}
|
||||
|
||||
$ejaculation = DB::transaction(function () use ($inputs, $webhook, $ejaculatedDate) {
|
||||
$ejaculation = Ejaculation::create([
|
||||
'user_id' => $webhook->user_id,
|
||||
'ejaculated_date' => $ejaculatedDate,
|
||||
'note' => $inputs['note'] ?? '',
|
||||
'link' => $inputs['link'] ?? '',
|
||||
'source' => Ejaculation::SOURCE_WEBHOOK,
|
||||
'is_private' => (bool)($inputs['is_private'] ?? false),
|
||||
'is_too_sensitive' => (bool)($inputs['is_too_sensitive'] ?? false),
|
||||
'discard_elapsed_time' => (bool)($inputs['discard_elapsed_time'] ?? false),
|
||||
'checkin_webhook_id' => $webhook->id
|
||||
]);
|
||||
|
||||
$tagIds = [];
|
||||
if (!empty($inputs['tags'])) {
|
||||
foreach ($inputs['tags'] as $tag) {
|
||||
$tag = trim($tag);
|
||||
if ($tag === '') {
|
||||
continue;
|
||||
}
|
||||
|
||||
$tag = Tag::firstOrCreate(['name' => $tag]);
|
||||
$tagIds[] = $tag->id;
|
||||
}
|
||||
}
|
||||
$ejaculation->tags()->sync($tagIds);
|
||||
|
||||
return $ejaculation;
|
||||
});
|
||||
|
||||
if (!empty($ejaculation->link)) {
|
||||
event(new LinkDiscovered($ejaculation->link));
|
||||
}
|
||||
|
||||
return response()->json([
|
||||
'status' => 200,
|
||||
'checkin' => new EjaculationResource($ejaculation)
|
||||
]);
|
||||
}
|
||||
}
|
|
@ -2,10 +2,11 @@
|
|||
|
||||
namespace App\Http\Controllers\Auth;
|
||||
|
||||
use App\User;
|
||||
use App\Http\Controllers\Controller;
|
||||
use Illuminate\Support\Facades\Validator;
|
||||
use App\User;
|
||||
use Illuminate\Foundation\Auth\RegistersUsers;
|
||||
use Illuminate\Support\Facades\Hash;
|
||||
use Illuminate\Support\Facades\Validator;
|
||||
|
||||
class RegisterController extends Controller
|
||||
{
|
||||
|
@ -47,11 +48,20 @@ class RegisterController extends Controller
|
|||
*/
|
||||
protected function validator(array $data)
|
||||
{
|
||||
return Validator::make($data, [
|
||||
'name' => 'required|string|regex:/^[a-zA-Z0-9_-]+$/u|max:15|unique:users',
|
||||
$rules = [
|
||||
'name' => 'required|string|regex:/^[a-zA-Z0-9_-]+$/u|max:15|unique:users|unique:deactivated_users',
|
||||
'email' => 'required|string|email|max:255|unique:users',
|
||||
'password' => 'required|string|min:6|confirmed',
|
||||
],
|
||||
'password' => 'required|string|min:8|confirmed'
|
||||
];
|
||||
|
||||
// reCAPTCHAのキーが設定されている場合、判定を有効化
|
||||
if (!empty(config('captcha.secret'))) {
|
||||
$rules['g-recaptcha-response'] = 'required|captcha';
|
||||
}
|
||||
|
||||
return Validator::make(
|
||||
$data,
|
||||
$rules,
|
||||
['name.regex' => 'ユーザー名には半角英数字とアンダーバー、ハイフンのみ使用できます。'],
|
||||
['name' => 'ユーザー名']
|
||||
);
|
||||
|
@ -69,7 +79,7 @@ class RegisterController extends Controller
|
|||
'name' => $data['name'],
|
||||
'display_name' => $data['name'],
|
||||
'email' => $data['email'],
|
||||
'password' => bcrypt($data['password']),
|
||||
'password' => Hash::make($data['password']),
|
||||
'is_protected' => $data['is_protected'] ?? false,
|
||||
'accept_analytics' => $data['accept_analytics'] ?? false
|
||||
]);
|
||||
|
|
|
@ -0,0 +1,41 @@
|
|||
<?php
|
||||
|
||||
namespace App\Http\Controllers\Auth;
|
||||
|
||||
use App\Http\Controllers\Controller;
|
||||
use Illuminate\Foundation\Auth\VerifiesEmails;
|
||||
|
||||
class VerificationController extends Controller
|
||||
{
|
||||
/*
|
||||
|--------------------------------------------------------------------------
|
||||
| Email Verification Controller
|
||||
|--------------------------------------------------------------------------
|
||||
|
|
||||
| This controller is responsible for handling email verification for any
|
||||
| user that recently registered with the application. Emails may also
|
||||
| be re-sent if the user didn't receive the original email message.
|
||||
|
|
||||
*/
|
||||
|
||||
use VerifiesEmails;
|
||||
|
||||
/**
|
||||
* Where to redirect users after verification.
|
||||
*
|
||||
* @var string
|
||||
*/
|
||||
protected $redirectTo = '/home';
|
||||
|
||||
/**
|
||||
* Create a new controller instance.
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
public function __construct()
|
||||
{
|
||||
$this->middleware('auth');
|
||||
$this->middleware('signed')->only('verify');
|
||||
$this->middleware('throttle:6,1')->only('verify', 'resend');
|
||||
}
|
||||
}
|
|
@ -2,10 +2,10 @@
|
|||
|
||||
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\Bus\DispatchesJobs;
|
||||
use Illuminate\Foundation\Validation\ValidatesRequests;
|
||||
use Illuminate\Routing\Controller as BaseController;
|
||||
|
||||
class Controller extends BaseController
|
||||
{
|
||||
|
|
|
@ -2,59 +2,240 @@
|
|||
|
||||
namespace App\Http\Controllers;
|
||||
|
||||
use Validator;
|
||||
use App\Ejaculation;
|
||||
use App\Events\LinkDiscovered;
|
||||
use App\Tag;
|
||||
use App\User;
|
||||
use Carbon\Carbon;
|
||||
use Illuminate\Http\Request;
|
||||
use Illuminate\Support\Facades\Auth;
|
||||
use Illuminate\Support\Facades\DB;
|
||||
use Validator;
|
||||
|
||||
class EjaculationController extends Controller
|
||||
{
|
||||
public function create()
|
||||
public function create(Request $request)
|
||||
{
|
||||
return view('ejaculation.checkin');
|
||||
$tags = old('tags') ?? $request->input('tags', '');
|
||||
if (!empty($tags)) {
|
||||
$tags = explode(' ', $tags);
|
||||
}
|
||||
|
||||
$errors = $request->session()->get('errors');
|
||||
$initialState = [
|
||||
'mode' => 'create',
|
||||
'fields' => [
|
||||
'date' => old('date') ?? $request->input('date', date('Y/m/d')),
|
||||
'time' => old('time') ?? $request->input('time', date('H:i')),
|
||||
'link' => old('link') ?? $request->input('link', ''),
|
||||
'tags' => $tags,
|
||||
'note' => old('note') ?? $request->input('note', ''),
|
||||
'is_private' => old('is_private') ?? $request->input('is_private', 0) == 1,
|
||||
'is_too_sensitive' => old('is_too_sensitive') ?? $request->input('is_too_sensitive', 0) == 1,
|
||||
'is_realtime' => old('is_realtime', true),
|
||||
'discard_elapsed_time' => old('discard_elapsed_time') ?? $request->input('discard_elapsed_time') == 1,
|
||||
],
|
||||
'errors' => isset($errors) ? $errors->getMessages() : null
|
||||
];
|
||||
|
||||
return view('ejaculation.checkin')->with('initialState', $initialState);
|
||||
}
|
||||
|
||||
public function store(Request $request)
|
||||
{
|
||||
Validator::make($request->all(), [
|
||||
$inputs = $request->all();
|
||||
|
||||
$validator = Validator::make($inputs, [
|
||||
'date' => 'required_without:is_realtime|date_format:Y/m/d',
|
||||
'time' => 'required_without:is_realtime|date_format:H:i',
|
||||
'note' => 'nullable|string|max:500',
|
||||
'link' => 'nullable|url|max:2000',
|
||||
'tags' => 'nullable|string',
|
||||
])->after(function ($validator) use ($request, $inputs) {
|
||||
// 日時の重複チェック
|
||||
if (!$validator->errors()->hasAny(['date', 'time'])) {
|
||||
if (isset($inputs['date']) && isset($inputs['time'])) {
|
||||
$dt = Carbon::createFromFormat('Y/m/d H:i', $inputs['date'] . ' ' . $inputs['time']);
|
||||
} else {
|
||||
$dt = now();
|
||||
}
|
||||
$dt = $dt->startOfMinute();
|
||||
if (Ejaculation::where(['user_id' => Auth::id(), 'ejaculated_date' => $dt])->count()) {
|
||||
$validator->errors()->add('datetime', '既にこの日時にチェックインしているため、登録できません。');
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
if ($validator->fails()) {
|
||||
return redirect()->route('checkin')
|
||||
->withErrors($validator)
|
||||
->withInput(array_merge(['is_realtime' => false], $request->input()));
|
||||
}
|
||||
|
||||
$ejaculation = DB::transaction(function () use ($request, $inputs) {
|
||||
if (isset($inputs['date']) && isset($inputs['time'])) {
|
||||
$ejaculatedDate = Carbon::createFromFormat('Y/m/d H:i', $inputs['date'] . ' ' . $inputs['time']);
|
||||
} else {
|
||||
$ejaculatedDate = now();
|
||||
}
|
||||
$ejaculatedDate = $ejaculatedDate->startOfMinute();
|
||||
$ejaculation = Ejaculation::create([
|
||||
'user_id' => Auth::id(),
|
||||
'ejaculated_date' => $ejaculatedDate,
|
||||
'note' => $inputs['note'] ?? '',
|
||||
'link' => $inputs['link'] ?? '',
|
||||
'source' => Ejaculation::SOURCE_WEB,
|
||||
'is_private' => $request->has('is_private') ?? false,
|
||||
'is_too_sensitive' => $request->has('is_too_sensitive') ?? false,
|
||||
'discard_elapsed_time' => $request->has('discard_elapsed_time') ?? false,
|
||||
]);
|
||||
|
||||
$tagIds = [];
|
||||
if (!empty($inputs['tags'])) {
|
||||
$tags = explode(' ', $inputs['tags']);
|
||||
foreach ($tags as $tag) {
|
||||
if ($tag === '') {
|
||||
continue;
|
||||
}
|
||||
|
||||
$tag = Tag::firstOrCreate(['name' => $tag]);
|
||||
$tagIds[] = $tag->id;
|
||||
}
|
||||
}
|
||||
$ejaculation->tags()->sync($tagIds);
|
||||
|
||||
return $ejaculation;
|
||||
});
|
||||
|
||||
if (!empty($ejaculation->link)) {
|
||||
event(new LinkDiscovered($ejaculation->link));
|
||||
}
|
||||
|
||||
return redirect()->route('checkin.show', ['id' => $ejaculation->id])->with('status', 'チェックインしました!');
|
||||
}
|
||||
|
||||
public function show($id)
|
||||
{
|
||||
$ejaculation = Ejaculation::where('id', $id)
|
||||
->withLikes()
|
||||
->firstOrFail();
|
||||
$user = User::findOrFail($ejaculation->user_id);
|
||||
|
||||
return view('ejaculation.show')->with(compact('user', 'ejaculation'));
|
||||
}
|
||||
|
||||
public function edit(Request $request, $id)
|
||||
{
|
||||
$ejaculation = Ejaculation::findOrFail($id);
|
||||
|
||||
$this->authorize('edit', $ejaculation);
|
||||
|
||||
if (old('tags') === null) {
|
||||
$tags = $ejaculation->tags->pluck('name');
|
||||
} else {
|
||||
$tags = old('tags');
|
||||
if (!empty($tags)) {
|
||||
$tags = explode(' ', $tags);
|
||||
}
|
||||
}
|
||||
|
||||
$errors = $request->session()->get('errors');
|
||||
$initialState = [
|
||||
'mode' => 'update',
|
||||
'fields' => [
|
||||
'date' => old('date') ?? $ejaculation->ejaculated_date->format('Y/m/d'),
|
||||
'time' => old('time') ?? $ejaculation->ejaculated_date->format('H:i'),
|
||||
'link' => old('link') ?? $ejaculation->link,
|
||||
'tags' => $tags,
|
||||
'note' => old('note') ?? $ejaculation->note,
|
||||
'is_private' => is_bool(old('is_private')) ? old('is_private') : $ejaculation->is_private,
|
||||
'is_too_sensitive' => is_bool(old('is_too_sensitive')) ? old('is_too_sensitive') : $ejaculation->is_too_sensitive,
|
||||
'discard_elapsed_time' => is_bool(old('discard_elapsed_time')) ? old('discard_elapsed_time') : $ejaculation->discard_elapsed_time,
|
||||
],
|
||||
'errors' => isset($errors) ? $errors->getMessages() : null
|
||||
];
|
||||
|
||||
return view('ejaculation.edit')->with(compact('ejaculation', 'initialState'));
|
||||
}
|
||||
|
||||
public function update(Request $request, $id)
|
||||
{
|
||||
$ejaculation = Ejaculation::findOrFail($id);
|
||||
|
||||
$this->authorize('edit', $ejaculation);
|
||||
|
||||
$inputs = $request->all();
|
||||
|
||||
$validator = Validator::make($inputs, [
|
||||
'date' => 'required|date_format:Y/m/d',
|
||||
'time' => 'required|date_format:H:i',
|
||||
'note' => 'nullable|string|max:500',
|
||||
])->after(function ($validator) use ($request) {
|
||||
'link' => 'nullable|url|max:2000',
|
||||
'tags' => 'nullable|string',
|
||||
])->after(function ($validator) use ($id, $request, $inputs) {
|
||||
// 日時の重複チェック
|
||||
$dt = $request->input('date') . ' ' . $request->input('time');
|
||||
if (Ejaculation::where(['user_id' => Auth::id(), 'ejaculated_date' => $dt])->count()) {
|
||||
$validator->errors()->add('datetime', '既にこの日時にチェックインしているため、登録できません。');
|
||||
if (!$validator->errors()->hasAny(['date', 'time'])) {
|
||||
$dt = $inputs['date'] . ' ' . $inputs['time'];
|
||||
if (Ejaculation::where(['user_id' => Auth::id(), 'ejaculated_date' => $dt])->where('id', '<>', $id)->count()) {
|
||||
$validator->errors()->add('datetime', '既にこの日時にチェックインしているため、登録できません。');
|
||||
}
|
||||
}
|
||||
})->validate();
|
||||
});
|
||||
|
||||
Ejaculation::create([
|
||||
'user_id' => Auth::id(),
|
||||
'ejaculated_date' => $request->input('date') . ' ' . $request->input('time'),
|
||||
'note' => $request->input('note') ?? '',
|
||||
'is_private' => $request->has('is_private') ?? false
|
||||
]);
|
||||
if ($validator->fails()) {
|
||||
return redirect()->route('checkin.edit', ['id' => $id])->withErrors($validator)->withInput();
|
||||
}
|
||||
|
||||
return redirect()->route('home')->with('status', 'チェックインしました!');
|
||||
DB::transaction(function () use ($ejaculation, $request, $inputs) {
|
||||
$ejaculation->fill([
|
||||
'ejaculated_date' => Carbon::createFromFormat('Y/m/d H:i', $inputs['date'] . ' ' . $inputs['time']),
|
||||
'note' => $inputs['note'] ?? '',
|
||||
'link' => $inputs['link'] ?? '',
|
||||
'is_private' => $request->has('is_private') ?? false,
|
||||
'is_too_sensitive' => $request->has('is_too_sensitive') ?? false,
|
||||
'discard_elapsed_time' => $request->has('discard_elapsed_time') ?? false,
|
||||
])->save();
|
||||
|
||||
$tagIds = [];
|
||||
if (!empty($inputs['tags'])) {
|
||||
$tags = explode(' ', $inputs['tags']);
|
||||
foreach ($tags as $tag) {
|
||||
if ($tag === '') {
|
||||
continue;
|
||||
}
|
||||
|
||||
$tag = Tag::firstOrCreate(['name' => $tag]);
|
||||
$tagIds[] = $tag->id;
|
||||
}
|
||||
}
|
||||
$ejaculation->tags()->sync($tagIds);
|
||||
});
|
||||
|
||||
if (!empty($ejaculation->link)) {
|
||||
event(new LinkDiscovered($ejaculation->link));
|
||||
}
|
||||
|
||||
return redirect()->route('checkin.show', ['id' => $ejaculation->id])->with('status', 'チェックインを修正しました!');
|
||||
}
|
||||
|
||||
public function show()
|
||||
public function destroy($id)
|
||||
{
|
||||
// TODO: not implemented
|
||||
$ejaculation = Ejaculation::findOrFail($id);
|
||||
|
||||
$this->authorize('edit', $ejaculation);
|
||||
|
||||
$user = User::findOrFail($ejaculation->user_id);
|
||||
|
||||
DB::transaction(function () use ($ejaculation) {
|
||||
$ejaculation->tags()->detach();
|
||||
$ejaculation->delete();
|
||||
});
|
||||
|
||||
return redirect()->route('user.profile', ['name' => $user->name])->with('status', '削除しました。');
|
||||
}
|
||||
|
||||
public function edit()
|
||||
public function tools()
|
||||
{
|
||||
// TODO: not implemented
|
||||
return view('ejaculation.tools');
|
||||
}
|
||||
|
||||
public function update()
|
||||
{
|
||||
// TODO: not implemented
|
||||
}
|
||||
|
||||
public function destroy()
|
||||
{
|
||||
// TODO: not implemented
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -3,6 +3,7 @@
|
|||
namespace App\Http\Controllers;
|
||||
|
||||
use App\Ejaculation;
|
||||
use App\Information;
|
||||
use Carbon\Carbon;
|
||||
use Illuminate\Http\Request;
|
||||
use Illuminate\Support\Facades\Auth;
|
||||
|
@ -26,54 +27,57 @@ class HomeController extends Controller
|
|||
*/
|
||||
public function index()
|
||||
{
|
||||
if (Auth::check()) {
|
||||
$ejaculations = Ejaculation::select(DB::raw(<<<'SQL'
|
||||
to_char(ejaculated_date, 'YYYY/MM/DD HH24:MI') AS ejaculated_date,
|
||||
note,
|
||||
is_private,
|
||||
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
|
||||
SQL
|
||||
))
|
||||
->where(['user_id' => Auth::id()])
|
||||
->orderBy('ejaculated_date', 'desc')
|
||||
->limit(9)
|
||||
->get();
|
||||
$informations = Information::query()
|
||||
->select('id', 'category', 'pinned', 'title', 'created_at')
|
||||
->orderByDesc('pinned')
|
||||
->orderByDesc('created_at')
|
||||
->take(3)
|
||||
->get();
|
||||
$categories = Information::CATEGORIES;
|
||||
|
||||
// 現在のオナ禁セッションの経過時間
|
||||
if (count($ejaculations) > 0) {
|
||||
$currentSession = Carbon::parse($ejaculations[0]['ejaculated_date'])
|
||||
->diff(Carbon::now())
|
||||
->format('%d日 %h時間 %i分');
|
||||
} else {
|
||||
$currentSession = null;
|
||||
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();
|
||||
}
|
||||
|
||||
// 概況欄のデータ取得
|
||||
$summary = DB::select(<<<'SQL'
|
||||
SELECT
|
||||
to_char(avg(span), 'FMDDD日 FMHH24時間 FMMI分') AS average,
|
||||
to_char(max(span), 'FMDDD日 FMHH24時間 FMMI分') AS longest,
|
||||
to_char(min(span), 'FMDDD日 FMHH24時間 FMMI分') AS shortest,
|
||||
to_char(sum(span), 'FMDDD日 FMHH24時間 FMMI分') AS total_times,
|
||||
count(*) AS total_checkins
|
||||
FROM
|
||||
(
|
||||
SELECT
|
||||
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
|
||||
) AS temp
|
||||
SQL
|
||||
, ['user_id' => Auth::id()]);
|
||||
// お惣菜コーナー用のデータ取得
|
||||
$publicLinkedEjaculations = Ejaculation::join('users', 'users.id', '=', 'ejaculations.user_id')
|
||||
->where('users.is_protected', false)
|
||||
->where('ejaculations.is_private', false)
|
||||
->where('ejaculations.link', '<>', '')
|
||||
->where('ejaculations.ejaculated_date', '<=', Carbon::now())
|
||||
->orderBy('ejaculations.ejaculated_date', 'desc')
|
||||
->select('ejaculations.*')
|
||||
->with('user', 'tags')
|
||||
->withLikes()
|
||||
->visibleToTimeline()
|
||||
->take(21)
|
||||
->get();
|
||||
|
||||
return view('home')->with(compact('ejaculations', 'currentSession', 'summary'));
|
||||
return view('home')->with(compact('informations', 'categories', 'globalEjaculationCounts', 'publicLinkedEjaculations'));
|
||||
} else {
|
||||
return view('guest');
|
||||
return view('guest')->with(compact('informations', 'categories'));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -0,0 +1,33 @@
|
|||
<?php
|
||||
|
||||
namespace App\Http\Controllers;
|
||||
|
||||
use App\Information;
|
||||
use Illuminate\Http\Request;
|
||||
|
||||
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('info.index')->with([
|
||||
'informations' => $informations,
|
||||
'categories' => Information::CATEGORIES
|
||||
]);
|
||||
}
|
||||
|
||||
public function show($id)
|
||||
{
|
||||
$information = Information::findOrFail($id);
|
||||
|
||||
return view('info.show')->with([
|
||||
'info' => $information,
|
||||
'category' => Information::CATEGORIES[$information->category]
|
||||
]);
|
||||
}
|
||||
}
|
|
@ -0,0 +1,67 @@
|
|||
<?php
|
||||
|
||||
namespace App\Http\Controllers;
|
||||
|
||||
use App\Ejaculation;
|
||||
use App\Tag;
|
||||
use App\Utilities\Formatter;
|
||||
use Illuminate\Http\Request;
|
||||
use Illuminate\Support\Facades\Auth;
|
||||
|
||||
class SearchController extends Controller
|
||||
{
|
||||
/** @var Formatter */
|
||||
private $formatter;
|
||||
|
||||
public function __construct(Formatter $formatter)
|
||||
{
|
||||
$this->formatter = $formatter;
|
||||
}
|
||||
|
||||
public function index(Request $request)
|
||||
{
|
||||
$inputs = $request->validate([
|
||||
'q' => 'required'
|
||||
]);
|
||||
|
||||
$q = $this->normalizeQuery($inputs['q']);
|
||||
$results = Ejaculation::query()
|
||||
->whereHas('tags', function ($query) use ($q) {
|
||||
$query->where('normalized_name', 'like', "%{$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'
|
||||
]);
|
||||
|
||||
$q = $this->normalizeQuery($inputs['q']);
|
||||
$results = Tag::query()
|
||||
->where('normalized_name', 'like', "%{$q}%")
|
||||
->paginate(50)
|
||||
->appends($inputs);
|
||||
|
||||
return view('search.relatedTag')->with(compact('inputs', 'results'));
|
||||
}
|
||||
|
||||
private function normalizeQuery(string $query): string
|
||||
{
|
||||
return $this->formatter->normalizeTagName($query);
|
||||
}
|
||||
}
|
|
@ -0,0 +1,242 @@
|
|||
<?php
|
||||
|
||||
namespace App\Http\Controllers;
|
||||
|
||||
use App\CheckinWebhook;
|
||||
use App\DeactivatedUser;
|
||||
use App\Ejaculation;
|
||||
use App\Exceptions\CsvImportException;
|
||||
use App\Services\CheckinCsvExporter;
|
||||
use App\Services\CheckinCsvImporter;
|
||||
use Illuminate\Http\Request;
|
||||
use Illuminate\Support\Facades\Auth;
|
||||
use Illuminate\Support\Facades\DB;
|
||||
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 webhooks()
|
||||
{
|
||||
$webhooks = Auth::user()->checkinWebhooks;
|
||||
$webhooksLimit = CheckinWebhook::PER_USER_LIMIT;
|
||||
|
||||
return view('setting.webhooks')->with(compact('webhooks', 'webhooksLimit'));
|
||||
}
|
||||
|
||||
public function storeWebhooks(Request $request)
|
||||
{
|
||||
$validated = $request->validate([
|
||||
'name' => [
|
||||
'required',
|
||||
'string',
|
||||
'max:255',
|
||||
Rule::unique('checkin_webhooks', 'name')->where(function ($query) {
|
||||
return $query->where('user_id', Auth::id());
|
||||
})
|
||||
]
|
||||
], [], [
|
||||
'name' => '名前'
|
||||
]);
|
||||
|
||||
if (Auth::user()->checkinWebhooks()->count() >= CheckinWebhook::PER_USER_LIMIT) {
|
||||
return redirect()->route('setting.webhooks')
|
||||
->with('status', CheckinWebhook::PER_USER_LIMIT . '件以上のWebhookを作成することはできません。');
|
||||
}
|
||||
|
||||
Auth::user()->checkinWebhooks()->create($validated);
|
||||
|
||||
return redirect()->route('setting.webhooks')->with('status', '作成しました。');
|
||||
}
|
||||
|
||||
public function destroyWebhooks(CheckinWebhook $webhook)
|
||||
{
|
||||
$webhook->delete();
|
||||
|
||||
return redirect()->route('setting.webhooks')->with('status', '削除しました。');
|
||||
}
|
||||
|
||||
public function import()
|
||||
{
|
||||
return view('setting.import');
|
||||
}
|
||||
|
||||
public function storeImport(Request $request)
|
||||
{
|
||||
$validated = $request->validate([
|
||||
'file' => 'required|file'
|
||||
], [], [
|
||||
'file' => 'ファイル'
|
||||
]);
|
||||
|
||||
$file = $request->file('file');
|
||||
if (!$file->isValid()) {
|
||||
return redirect()->route('setting.import')->withErrors(['file' => 'ファイルのアップロードに失敗しました。']);
|
||||
}
|
||||
|
||||
try {
|
||||
set_time_limit(0);
|
||||
|
||||
$importer = new CheckinCsvImporter(Auth::user(), $file->path());
|
||||
$imported = $importer->execute();
|
||||
|
||||
return redirect()->route('setting.import')->with('status', "{$imported}件のインポートに性交しました。");
|
||||
} catch (CsvImportException $e) {
|
||||
return redirect()->route('setting.import')->with('import_errors', $e->getErrors());
|
||||
}
|
||||
}
|
||||
|
||||
public function destroyImport()
|
||||
{
|
||||
Auth::user()
|
||||
->ejaculations()
|
||||
->where('ejaculations.source', Ejaculation::SOURCE_CSV)
|
||||
->delete();
|
||||
|
||||
return redirect()->route('setting.import')->with('status', '削除が完了しました。');
|
||||
}
|
||||
|
||||
public function export()
|
||||
{
|
||||
return view('setting.export');
|
||||
}
|
||||
|
||||
public function exportToCsv(Request $request)
|
||||
{
|
||||
$validated = $request->validate([
|
||||
'charset' => ['required', Rule::in(['utf8', 'sjis'])]
|
||||
]);
|
||||
|
||||
$charsets = [
|
||||
'utf8' => 'UTF-8',
|
||||
'sjis' => 'SJIS-win'
|
||||
];
|
||||
|
||||
$filename = tempnam(sys_get_temp_dir(), 'tissue_export_tmp_');
|
||||
try {
|
||||
// 気休め
|
||||
set_time_limit(0);
|
||||
|
||||
$exporter = new CheckinCsvExporter(Auth::user(), $filename, $charsets[$validated['charset']]);
|
||||
$exporter->execute();
|
||||
} catch (\Throwable $e) {
|
||||
unlink($filename);
|
||||
throw $e;
|
||||
}
|
||||
|
||||
return response()
|
||||
->download($filename, 'TissueCheckin_' . date('Y-m-d_H-i-s') . '.csv')
|
||||
->deleteFileAfterSend(true);
|
||||
}
|
||||
|
||||
public function deactivate()
|
||||
{
|
||||
return view('setting.deactivate');
|
||||
}
|
||||
|
||||
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);
|
||||
// }
|
||||
}
|
|
@ -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'));
|
||||
}
|
||||
}
|
|
@ -0,0 +1,27 @@
|
|||
<?php
|
||||
|
||||
namespace App\Http\Controllers;
|
||||
|
||||
use App\Ejaculation;
|
||||
use Illuminate\Http\Request;
|
||||
use Illuminate\Support\Carbon;
|
||||
|
||||
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', '<>', '')
|
||||
->where('ejaculations.ejaculated_date', '<=', Carbon::now())
|
||||
->orderBy('ejaculations.ejaculated_date', 'desc')
|
||||
->select('ejaculations.*')
|
||||
->with('user', 'tags')
|
||||
->withLikes()
|
||||
->visibleToTimeline()
|
||||
->paginate(21);
|
||||
|
||||
return view('timeline.public')->with(compact('ejaculations'));
|
||||
}
|
||||
}
|
|
@ -5,13 +5,18 @@ namespace App\Http\Controllers;
|
|||
use App\Ejaculation;
|
||||
use App\User;
|
||||
use Carbon\Carbon;
|
||||
use Carbon\CarbonInterface;
|
||||
use Illuminate\Http\Request;
|
||||
use Illuminate\Support\Facades\Auth;
|
||||
use Illuminate\Support\Facades\DB;
|
||||
use Validator;
|
||||
|
||||
class UserController extends Controller
|
||||
{
|
||||
//
|
||||
public function redirectMypage()
|
||||
{
|
||||
return redirect()->route('user.profile', ['name' => auth()->user()->name]);
|
||||
}
|
||||
|
||||
public function profile($name)
|
||||
{
|
||||
|
@ -21,52 +26,295 @@ class UserController extends Controller
|
|||
}
|
||||
|
||||
// チェックインの取得
|
||||
$query = Ejaculation::select(DB::raw(<<<'SQL'
|
||||
to_char(ejaculated_date, 'YYYY/MM/DD HH24:MI') AS ejaculated_date,
|
||||
$query = Ejaculation::select(DB::raw(
|
||||
<<<'SQL'
|
||||
ejaculations.id,
|
||||
ejaculated_date,
|
||||
note,
|
||||
is_private,
|
||||
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
|
||||
is_too_sensitive,
|
||||
link,
|
||||
source,
|
||||
discard_elapsed_time,
|
||||
to_char(before_dates.before_date, 'YYYY/MM/DD HH24:MI') AS before_date,
|
||||
to_char(ejaculated_date - before_dates.before_date, 'FMDDD日 FMHH24時間 FMMI分') AS ejaculated_span
|
||||
SQL
|
||||
))
|
||||
->joinSub($this->queryBeforeEjaculatedDates(), 'before_dates', 'before_dates.id', '=', 'ejaculations.id')
|
||||
->where('user_id', $user->id);
|
||||
if (!Auth::check() || $user->id !== Auth::id()) {
|
||||
$query = $query->where('is_private', false);
|
||||
}
|
||||
$ejaculations = $query->orderBy('ejaculated_date', 'desc')
|
||||
->with('tags')
|
||||
->withLikes()
|
||||
->paginate(20);
|
||||
|
||||
// 現在のオナ禁セッションの経過時間
|
||||
if (count($ejaculations) > 0) {
|
||||
$currentSession = Carbon::parse($ejaculations[0]['ejaculated_date'])
|
||||
->diff(Carbon::now())
|
||||
->format('%d日 %h時間 %i分');
|
||||
} else {
|
||||
$currentSession = null;
|
||||
// よく使っているタグ
|
||||
$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();
|
||||
|
||||
// シコ草
|
||||
$countByDayQuery = $this->countEjaculationByDay($user)
|
||||
->where('ejaculated_date', '>=', now()->startOfMonth()->subMonths(9))
|
||||
->where('ejaculated_date', '<', now()->addMonth()->startOfMonth())
|
||||
->get();
|
||||
$countByDay = [];
|
||||
foreach ($countByDayQuery as $data) {
|
||||
$date = Carbon::createFromFormat('Y/m/d', $data->date);
|
||||
$countByDay[$date->timestamp] = $data->count;
|
||||
}
|
||||
|
||||
// 概況欄のデータ取得
|
||||
$summary = DB::select(<<<'SQL'
|
||||
SELECT
|
||||
to_char(avg(span), 'FMDDD日 FMHH24時間 FMMI分') AS average,
|
||||
to_char(max(span), 'FMDDD日 FMHH24時間 FMMI分') AS longest,
|
||||
to_char(min(span), 'FMDDD日 FMHH24時間 FMMI分') AS shortest,
|
||||
to_char(sum(span), 'FMDDD日 FMHH24時間 FMMI分') AS total_times,
|
||||
count(*) AS total_checkins
|
||||
FROM
|
||||
(
|
||||
SELECT
|
||||
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
|
||||
) AS temp
|
||||
SQL
|
||||
, ['user_id' => $user->id]);
|
||||
return view('user.profile')->with(compact('user', 'ejaculations', 'tags', 'countByDay'));
|
||||
}
|
||||
|
||||
return view('user.profile')->with(compact('user', 'ejaculations', 'currentSession', 'summary'));
|
||||
public function stats($name)
|
||||
{
|
||||
$user = User::where('name', $name)->first();
|
||||
if (empty($user)) {
|
||||
abort(404);
|
||||
}
|
||||
|
||||
$availableMonths = $this->makeStatsAvailableMonths($user);
|
||||
$graphData = $this->makeGraphData($user);
|
||||
|
||||
return view('user.stats.index')->with(compact('user', 'graphData', 'availableMonths'));
|
||||
}
|
||||
|
||||
public function statsYearly($name, $year)
|
||||
{
|
||||
$user = User::where('name', $name)->first();
|
||||
if (empty($user)) {
|
||||
abort(404);
|
||||
}
|
||||
|
||||
$validator = Validator::make(compact('year'), [
|
||||
'year' => 'required|date_format:Y'
|
||||
]);
|
||||
if ($validator->fails()) {
|
||||
return redirect()->route('user.stats', compact('name'));
|
||||
}
|
||||
|
||||
$availableMonths = $this->makeStatsAvailableMonths($user);
|
||||
if (!isset($availableMonths[$year])) {
|
||||
return redirect()->route('user.stats', compact('name'));
|
||||
}
|
||||
|
||||
$graphData = $this->makeGraphData(
|
||||
$user,
|
||||
Carbon::createFromDate($year, 1, 1, config('app.timezone'))->startOfDay(),
|
||||
Carbon::createFromDate($year, 1, 1, config('app.timezone'))->addYear()->startOfDay()
|
||||
);
|
||||
|
||||
return view('user.stats.yearly')
|
||||
->with(compact('user', 'graphData', 'availableMonths'))
|
||||
->with('currentYear', (int) $year);
|
||||
}
|
||||
|
||||
public function statsMonthly($name, $year, $month)
|
||||
{
|
||||
$user = User::where('name', $name)->first();
|
||||
if (empty($user)) {
|
||||
abort(404);
|
||||
}
|
||||
|
||||
$validator = Validator::make(compact('year', 'month'), [
|
||||
'year' => 'required|date_format:Y',
|
||||
'month' => 'required|date_format:m'
|
||||
]);
|
||||
if ($validator->fails()) {
|
||||
return redirect()->route('user.stats.yearly', compact('name', 'year'));
|
||||
}
|
||||
|
||||
$availableMonths = $this->makeStatsAvailableMonths($user);
|
||||
if (!isset($availableMonths[$year]) || !in_array($month, $availableMonths[$year], false)) {
|
||||
return redirect()->route('user.stats.yearly', compact('name', 'year'));
|
||||
}
|
||||
|
||||
$graphData = $this->makeGraphData(
|
||||
$user,
|
||||
Carbon::createFromDate($year, $month, 1, config('app.timezone'))->startOfDay(),
|
||||
Carbon::createFromDate($year, $month, 1, config('app.timezone'))->addMonth()->startOfDay()
|
||||
);
|
||||
|
||||
return view('user.stats.monthly')
|
||||
->with(compact('user', 'graphData', 'availableMonths'))
|
||||
->with('currentYear', (int) $year)
|
||||
->with('currentMonth', (int) $month);
|
||||
}
|
||||
|
||||
public function okazu($name)
|
||||
{
|
||||
$user = User::where('name', $name)->first();
|
||||
if (empty($user)) {
|
||||
abort(404);
|
||||
}
|
||||
|
||||
// チェックインの取得
|
||||
$query = Ejaculation::select(DB::raw(
|
||||
<<<'SQL'
|
||||
ejaculations.id,
|
||||
ejaculated_date,
|
||||
note,
|
||||
is_private,
|
||||
is_too_sensitive,
|
||||
link,
|
||||
source,
|
||||
discard_elapsed_time,
|
||||
to_char(before_dates.before_date, 'YYYY/MM/DD HH24:MI') AS before_date,
|
||||
to_char(ejaculated_date - before_dates.before_date, 'FMDDD日 FMHH24時間 FMMI分') AS ejaculated_span
|
||||
SQL
|
||||
))
|
||||
->joinSub($this->queryBeforeEjaculatedDates(), 'before_dates', 'before_dates.id', '=', 'ejaculations.id')
|
||||
->where('user_id', $user->id)
|
||||
->where('link', '<>', '');
|
||||
if (!Auth::check() || $user->id !== Auth::id()) {
|
||||
$query = $query->where('is_private', false);
|
||||
}
|
||||
$ejaculations = $query->orderBy('ejaculated_date', 'desc')
|
||||
->with('tags')
|
||||
->withLikes()
|
||||
->paginate(20);
|
||||
|
||||
return view('user.profile')->with(compact('user', 'ejaculations'));
|
||||
}
|
||||
|
||||
public function likes($name)
|
||||
{
|
||||
$user = User::where('name', $name)->first();
|
||||
if (empty($user)) {
|
||||
abort(404);
|
||||
}
|
||||
|
||||
$likes = $user->likes()
|
||||
->orderBy('created_at', 'desc')
|
||||
->with('ejaculation.user', 'ejaculation.tags')
|
||||
->whereHas('ejaculation', function ($query) {
|
||||
$query->where('user_id', Auth::id())
|
||||
->orWhere('is_private', false);
|
||||
})
|
||||
->paginate(20);
|
||||
|
||||
return view('user.likes')->with(compact('user', 'likes'));
|
||||
}
|
||||
|
||||
private function makeStatsAvailableMonths(User $user): array
|
||||
{
|
||||
$availableMonths = [];
|
||||
$oldest = $user->ejaculations()->orderBy('ejaculated_date')->first();
|
||||
if (isset($oldest)) {
|
||||
$oldestMonth = $oldest->ejaculated_date->startOfMonth();
|
||||
$currentMonth = now()->startOfMonth();
|
||||
for ($month = $currentMonth; $oldestMonth <= $currentMonth; $month = $month->subMonth()) {
|
||||
if (!isset($availableMonths[$month->year])) {
|
||||
$availableMonths[$month->year] = [];
|
||||
}
|
||||
$availableMonths[$month->year][] = $month->month;
|
||||
}
|
||||
}
|
||||
|
||||
return $availableMonths;
|
||||
}
|
||||
|
||||
private function makeGraphData(User $user, CarbonInterface $dateSince = null, CarbonInterface $dateUntil = null): array
|
||||
{
|
||||
if ($dateUntil === null) {
|
||||
$dateUntil = now()->addMonth()->startOfMonth();
|
||||
}
|
||||
$dateCondition = [
|
||||
['ejaculated_date', '<', $dateUntil],
|
||||
];
|
||||
if ($dateSince !== null) {
|
||||
$dateCondition[] = ['ejaculated_date', '>=', $dateSince];
|
||||
}
|
||||
|
||||
$groupByDay = $this->countEjaculationByDay($user)
|
||||
->where($dateCondition)
|
||||
->get();
|
||||
|
||||
$groupByHour = Ejaculation::select(DB::raw(
|
||||
<<<'SQL'
|
||||
to_char(ejaculated_date, 'HH24') AS "hour",
|
||||
count(*) AS "count"
|
||||
SQL
|
||||
))
|
||||
->where('user_id', $user->id)
|
||||
->where($dateCondition)
|
||||
->groupBy(DB::raw("to_char(ejaculated_date, 'HH24')"))
|
||||
->orderBy(DB::raw('1'))
|
||||
->get();
|
||||
|
||||
$dailySum = [];
|
||||
$monthlySum = [];
|
||||
$yearlySum = [];
|
||||
$dowSum = array_fill(0, 7, 0);
|
||||
$hourlySum = array_fill(0, 24, 0);
|
||||
|
||||
// 年間グラフ用の配列初期化
|
||||
if ($groupByDay->first() !== null) {
|
||||
$year = Carbon::createFromFormat('Y/m/d', $groupByDay->first()->date)->year;
|
||||
$currentYear = date('Y');
|
||||
for (; $year <= $currentYear; $year++) {
|
||||
$yearlySum[$year] = 0;
|
||||
}
|
||||
}
|
||||
|
||||
foreach ($groupByDay as $data) {
|
||||
$date = Carbon::createFromFormat('Y/m/d', $data->date);
|
||||
$yearAndMonth = $date->format('Y/m');
|
||||
|
||||
$dailySum[$date->timestamp] = $data->count;
|
||||
$yearlySum[$date->year] += $data->count;
|
||||
$dowSum[$date->dayOfWeek] += $data->count;
|
||||
$monthlySum[$yearAndMonth] = ($monthlySum[$yearAndMonth] ?? 0) + $data->count;
|
||||
}
|
||||
|
||||
foreach ($groupByHour as $data) {
|
||||
$hour = (int)$data->hour;
|
||||
$hourlySum[$hour] += $data->count;
|
||||
}
|
||||
|
||||
return [
|
||||
'dailySum' => $dailySum,
|
||||
'dowSum' => $dowSum,
|
||||
'monthlySum' => $monthlySum,
|
||||
'yearlyKey' => array_keys($yearlySum),
|
||||
'yearlySum' => array_values($yearlySum),
|
||||
'hourlyKey' => array_keys($hourlySum),
|
||||
'hourlySum' => array_values($hourlySum),
|
||||
];
|
||||
}
|
||||
|
||||
private function countEjaculationByDay(User $user)
|
||||
{
|
||||
return Ejaculation::select(DB::raw(
|
||||
<<<'SQL'
|
||||
to_char(ejaculated_date, 'YYYY/MM/DD') AS "date",
|
||||
count(*) AS "count"
|
||||
SQL
|
||||
))
|
||||
->where('user_id', $user->id)
|
||||
->groupBy(DB::raw("to_char(ejaculated_date, 'YYYY/MM/DD')"))
|
||||
->orderBy(DB::raw("to_char(ejaculated_date, 'YYYY/MM/DD')"));
|
||||
}
|
||||
|
||||
private function queryBeforeEjaculatedDates()
|
||||
{
|
||||
return DB::table('ejaculations')->selectRaw(
|
||||
<<<'SQL'
|
||||
id,
|
||||
(select ejaculated_date from ejaculations e2 where e2.ejaculated_date < ejaculations.ejaculated_date and e2.user_id = ejaculations.user_id order by e2.ejaculated_date desc limit 1) AS before_date
|
||||
SQL
|
||||
);
|
||||
}
|
||||
}
|
||||
|
|
|
@ -14,7 +14,8 @@ class Kernel extends HttpKernel
|
|||
* @var array
|
||||
*/
|
||||
protected $middleware = [
|
||||
\Illuminate\Foundation\Http\Middleware\CheckForMaintenanceMode::class,
|
||||
\App\Http\Middleware\TrustProxies::class,
|
||||
\App\Http\Middleware\CheckForMaintenanceMode::class,
|
||||
\Illuminate\Foundation\Http\Middleware\ValidatePostSize::class,
|
||||
\App\Http\Middleware\TrimStrings::class,
|
||||
\Illuminate\Foundation\Http\Middleware\ConvertEmptyStringsToNull::class,
|
||||
|
@ -34,12 +35,20 @@ class Kernel extends HttpKernel
|
|||
\Illuminate\View\Middleware\ShareErrorsFromSession::class,
|
||||
\App\Http\Middleware\VerifyCsrfToken::class,
|
||||
\Illuminate\Routing\Middleware\SubstituteBindings::class,
|
||||
\App\Http\Middleware\NormalizeLineEnding::class,
|
||||
],
|
||||
|
||||
'api' => [
|
||||
'throttle:60,1',
|
||||
'bindings',
|
||||
\App\Http\Middleware\EnforceJson::class,
|
||||
\Illuminate\Routing\Middleware\SubstituteBindings::class,
|
||||
],
|
||||
|
||||
'stateful' => [
|
||||
\App\Http\Middleware\EncryptCookies::class,
|
||||
\Illuminate\Cookie\Middleware\AddQueuedCookiesToResponse::class,
|
||||
\Illuminate\Session\Middleware\StartSession::class,
|
||||
\App\Http\Middleware\VerifyCsrfToken::class,
|
||||
]
|
||||
];
|
||||
|
||||
/**
|
||||
|
@ -50,11 +59,33 @@ class Kernel extends HttpKernel
|
|||
* @var array
|
||||
*/
|
||||
protected $routeMiddleware = [
|
||||
'auth' => \Illuminate\Auth\Middleware\Authenticate::class,
|
||||
'auth' => \App\Http\Middleware\Authenticate::class,
|
||||
'auth.basic' => \Illuminate\Auth\Middleware\AuthenticateWithBasicAuth::class,
|
||||
'bindings' => \Illuminate\Routing\Middleware\SubstituteBindings::class,
|
||||
'cache.headers' => \Illuminate\Http\Middleware\SetCacheHeaders::class,
|
||||
'can' => \Illuminate\Auth\Middleware\Authorize::class,
|
||||
'guest' => \App\Http\Middleware\RedirectIfAuthenticated::class,
|
||||
'password.confirm' => \Illuminate\Auth\Middleware\RequirePassword::class,
|
||||
'signed' => \Illuminate\Routing\Middleware\ValidateSignature::class,
|
||||
'throttle' => \Illuminate\Routing\Middleware\ThrottleRequests::class,
|
||||
'verified' => \Illuminate\Auth\Middleware\EnsureEmailIsVerified::class,
|
||||
];
|
||||
|
||||
/**
|
||||
* The priority-sorted list of middleware.
|
||||
*
|
||||
* This forces non-global middleware to always be in the given order.
|
||||
*
|
||||
* @var array
|
||||
*/
|
||||
protected $middlewarePriority = [
|
||||
\App\Http\Middleware\EncryptCookies::class,
|
||||
\Illuminate\Session\Middleware\StartSession::class,
|
||||
\Illuminate\View\Middleware\ShareErrorsFromSession::class,
|
||||
\App\Http\Middleware\Authenticate::class,
|
||||
\Illuminate\Routing\Middleware\ThrottleRequests::class,
|
||||
\Illuminate\Session\Middleware\AuthenticateSession::class,
|
||||
\Illuminate\Routing\Middleware\SubstituteBindings::class,
|
||||
\Illuminate\Auth\Middleware\Authorize::class,
|
||||
];
|
||||
}
|
||||
|
|
|
@ -0,0 +1,21 @@
|
|||
<?php
|
||||
|
||||
namespace App\Http\Middleware;
|
||||
|
||||
use Illuminate\Auth\Middleware\Authenticate as Middleware;
|
||||
|
||||
class Authenticate extends Middleware
|
||||
{
|
||||
/**
|
||||
* Get the path the user should be redirected to when they are not authenticated.
|
||||
*
|
||||
* @param \Illuminate\Http\Request $request
|
||||
* @return string|null
|
||||
*/
|
||||
protected function redirectTo($request)
|
||||
{
|
||||
if (! $request->expectsJson()) {
|
||||
return route('login');
|
||||
}
|
||||
}
|
||||
}
|
|
@ -0,0 +1,17 @@
|
|||
<?php
|
||||
|
||||
namespace App\Http\Middleware;
|
||||
|
||||
use Illuminate\Foundation\Http\Middleware\CheckForMaintenanceMode as Middleware;
|
||||
|
||||
class CheckForMaintenanceMode extends Middleware
|
||||
{
|
||||
/**
|
||||
* The URIs that should be reachable while maintenance mode is enabled.
|
||||
*
|
||||
* @var array
|
||||
*/
|
||||
protected $except = [
|
||||
//
|
||||
];
|
||||
}
|
|
@ -0,0 +1,25 @@
|
|||
<?php
|
||||
|
||||
namespace App\Http\Middleware;
|
||||
|
||||
use Closure;
|
||||
|
||||
/**
|
||||
* Request headerに Accept: application/json を上書きする。APIエンドポイント用。
|
||||
*/
|
||||
class EnforceJson
|
||||
{
|
||||
/**
|
||||
* Handle an incoming request.
|
||||
*
|
||||
* @param \Illuminate\Http\Request $request
|
||||
* @param \Closure $next
|
||||
* @return mixed
|
||||
*/
|
||||
public function handle($request, Closure $next)
|
||||
{
|
||||
$request->headers->set('Accept', 'application/json');
|
||||
|
||||
return $next($request);
|
||||
}
|
||||
}
|
|
@ -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)
|
||||
{
|
||||
if (Auth::guard($guard)->check()) {
|
||||
return redirect('/home');
|
||||
return redirect()->route('home');
|
||||
}
|
||||
|
||||
return $next($request);
|
||||
|
|
|
@ -0,0 +1,23 @@
|
|||
<?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|string
|
||||
*/
|
||||
protected $proxies = '**';
|
||||
|
||||
/**
|
||||
* The headers that should be used to detect proxies.
|
||||
*
|
||||
* @var int
|
||||
*/
|
||||
protected $headers = Request::HEADER_X_FORWARDED_ALL;
|
||||
}
|
|
@ -6,6 +6,13 @@ use Illuminate\Foundation\Http\Middleware\VerifyCsrfToken as BaseVerifier;
|
|||
|
||||
class VerifyCsrfToken extends BaseVerifier
|
||||
{
|
||||
/**
|
||||
* Indicates whether the XSRF-TOKEN cookie should be set on the response.
|
||||
*
|
||||
* @var bool
|
||||
*/
|
||||
protected $addHttpCookie = true;
|
||||
|
||||
/**
|
||||
* The URIs that should be excluded from CSRF verification.
|
||||
*
|
||||
|
|
|
@ -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'
|
||||
];
|
||||
}
|
||||
}
|
|
@ -0,0 +1,28 @@
|
|||
<?php
|
||||
|
||||
namespace App\Http\Resources;
|
||||
|
||||
use Illuminate\Http\Resources\Json\JsonResource;
|
||||
|
||||
class EjaculationResource extends JsonResource
|
||||
{
|
||||
/**
|
||||
* Transform the resource into an array.
|
||||
*
|
||||
* @param \Illuminate\Http\Request $request
|
||||
* @return array
|
||||
*/
|
||||
public function toArray($request)
|
||||
{
|
||||
return [
|
||||
'id' => $this->id,
|
||||
'checked_in_at' => $this->ejaculated_date->format(\DateTime::ATOM),
|
||||
'note' => $this->note,
|
||||
'link' => $this->link,
|
||||
'tags' => $this->tags->pluck('name'),
|
||||
'source' => $this->source,
|
||||
'is_private' => $this->is_private,
|
||||
'is_too_sensitive' => $this->is_too_sensitive,
|
||||
];
|
||||
}
|
||||
}
|
|
@ -0,0 +1,93 @@
|
|||
<?php
|
||||
|
||||
namespace App\Http\ViewComposers;
|
||||
|
||||
use App\Ejaculation;
|
||||
use Carbon\Carbon;
|
||||
use Illuminate\Support\Facades\DB;
|
||||
use Illuminate\View\View;
|
||||
|
||||
class ProfileStatsComposer
|
||||
{
|
||||
public function __construct()
|
||||
{
|
||||
}
|
||||
|
||||
public function compose(View $view)
|
||||
{
|
||||
// user変数に値が設定されてない場合は落とす
|
||||
if (!$view->offsetExists('user')) {
|
||||
throw new \LogicException('View data "user" was not exist.');
|
||||
}
|
||||
/** @var \App\User $user */
|
||||
$user = $view->offsetGet('user');
|
||||
|
||||
// 現在のオナ禁セッションの経過時間
|
||||
$latestEjaculation = Ejaculation::select('ejaculated_date')
|
||||
->where('user_id', $user->id)
|
||||
->orderByDesc('ejaculated_date')
|
||||
->first();
|
||||
if (!empty($latestEjaculation)) {
|
||||
$currentSession = $latestEjaculation->ejaculated_date
|
||||
->diff(Carbon::now())
|
||||
->format('%a日 %h時間 %i分');
|
||||
} else {
|
||||
$currentSession = null;
|
||||
}
|
||||
|
||||
// 概況欄のデータ取得
|
||||
$average = 0;
|
||||
$divisor = 0;
|
||||
$averageSources = DB::select(<<<'SQL'
|
||||
SELECT
|
||||
extract(epoch from ejaculated_date - lead(ejaculated_date, 1, NULL) OVER (ORDER BY ejaculated_date DESC)) AS span,
|
||||
discard_elapsed_time
|
||||
FROM
|
||||
ejaculations
|
||||
WHERE
|
||||
user_id = :user_id
|
||||
ORDER BY
|
||||
ejaculated_date DESC
|
||||
LIMIT
|
||||
30
|
||||
SQL
|
||||
, ['user_id' => $user->id]);
|
||||
foreach ($averageSources as $item) {
|
||||
// 経過時間記録対象外のレコードがあったら、それより古いデータは平均の計算に加えない
|
||||
if ($item->discard_elapsed_time) {
|
||||
break;
|
||||
}
|
||||
$average += $item->span;
|
||||
$divisor++;
|
||||
}
|
||||
if ($divisor > 0) {
|
||||
$average /= $divisor;
|
||||
}
|
||||
|
||||
$summary = DB::select(<<<'SQL'
|
||||
SELECT
|
||||
max(span) AS longest,
|
||||
min(span) AS shortest,
|
||||
sum(span) AS total_times
|
||||
FROM
|
||||
(
|
||||
SELECT
|
||||
extract(epoch from ejaculated_date - lead(ejaculated_date, 1, NULL) OVER (ORDER BY ejaculated_date DESC)) AS span,
|
||||
discard_elapsed_time
|
||||
FROM
|
||||
ejaculations
|
||||
WHERE
|
||||
user_id = :user_id
|
||||
ORDER BY
|
||||
ejaculated_date DESC
|
||||
) AS temp
|
||||
WHERE
|
||||
discard_elapsed_time = FALSE
|
||||
SQL
|
||||
, ['user_id' => $user->id]);
|
||||
|
||||
$total = $user->ejaculations()->count();
|
||||
|
||||
$view->with(compact('latestEjaculation', 'currentSession', 'average', 'summary', 'total'));
|
||||
}
|
||||
}
|
|
@ -0,0 +1,24 @@
|
|||
<?php
|
||||
|
||||
namespace App;
|
||||
|
||||
use Illuminate\Database\Eloquent\Model;
|
||||
use Illuminate\Database\Eloquent\SoftDeletes;
|
||||
|
||||
class Information extends Model
|
||||
{
|
||||
use SoftDeletes;
|
||||
|
||||
const CATEGORIES = [
|
||||
0 => ['label' => 'お知らせ', 'class' => 'badge-info'],
|
||||
1 => ['label' => 'アップデート', 'class' => 'badge-success'],
|
||||
2 => ['label' => '不具合情報', 'class' => 'badge-danger'],
|
||||
3 => ['label' => 'メンテナンス', 'class' => 'badge-warning']
|
||||
];
|
||||
|
||||
protected $fillable = [
|
||||
'category', 'pinned', 'title', 'content'
|
||||
];
|
||||
|
||||
protected $dates = ['deleted_at'];
|
||||
}
|
|
@ -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();
|
||||
}
|
||||
}
|
|
@ -0,0 +1,43 @@
|
|||
<?php
|
||||
|
||||
namespace App\Listeners;
|
||||
|
||||
use App\Events\LinkDiscovered;
|
||||
use App\MetadataResolver\DeniedHostException;
|
||||
use App\Services\MetadataResolveService;
|
||||
use Illuminate\Contracts\Queue\ShouldQueue;
|
||||
use Illuminate\Queue\InteractsWithQueue;
|
||||
|
||||
class LinkCollector
|
||||
{
|
||||
/** @var MetadataResolveService */
|
||||
private $metadataResolveService;
|
||||
|
||||
/**
|
||||
* Create the event listener.
|
||||
*
|
||||
* @param MetadataResolveService $metadataResolveService
|
||||
*/
|
||||
public function __construct(MetadataResolveService $metadataResolveService)
|
||||
{
|
||||
$this->metadataResolveService = $metadataResolveService;
|
||||
}
|
||||
|
||||
/**
|
||||
* Handle the event.
|
||||
*
|
||||
* @param LinkDiscovered $event
|
||||
* @return void
|
||||
*/
|
||||
public function handle(LinkDiscovered $event)
|
||||
{
|
||||
try {
|
||||
$this->metadataResolveService->execute($event->url);
|
||||
} catch (DeniedHostException $e) {
|
||||
// ignored
|
||||
} catch (\Exception $e) {
|
||||
// 今のところこのイベントは同期実行されるので、上流をクラッシュさせないために雑catchする
|
||||
report($e);
|
||||
}
|
||||
}
|
||||
}
|
|
@ -0,0 +1,80 @@
|
|||
<?php
|
||||
|
||||
namespace App;
|
||||
|
||||
use Carbon\CarbonInterface;
|
||||
use GuzzleHttp\Exception\RequestException;
|
||||
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', 'error_at'];
|
||||
|
||||
public function tags()
|
||||
{
|
||||
return $this->belongsToMany(Tag::class)->withTimestamps();
|
||||
}
|
||||
|
||||
public function needRefresh(): bool
|
||||
{
|
||||
return $this->isExpired() || $this->error_at !== null;
|
||||
}
|
||||
|
||||
public function isExpired(): bool
|
||||
{
|
||||
return $this->expires_at !== null && $this->expires_at < now();
|
||||
}
|
||||
|
||||
public function storeException(CarbonInterface $error_at, \Exception $exception): self
|
||||
{
|
||||
$this->prepareFieldsOnError();
|
||||
$this->error_at = $error_at;
|
||||
$this->error_exception_class = get_class($exception);
|
||||
$this->error_body = $exception->getMessage();
|
||||
if ($exception instanceof RequestException) {
|
||||
$this->error_http_code = $exception->getCode();
|
||||
} else {
|
||||
$this->error_http_code = null;
|
||||
}
|
||||
$this->error_count++;
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
public function storeError(CarbonInterface $error_at, string $body, ?int $httpCode = null): self
|
||||
{
|
||||
$this->prepareFieldsOnError();
|
||||
$this->error_at = $error_at;
|
||||
$this->error_exception_class = null;
|
||||
$this->error_body = $body;
|
||||
$this->error_http_code = $httpCode;
|
||||
$this->error_count++;
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
public function clearError(): self
|
||||
{
|
||||
$this->error_at = null;
|
||||
$this->error_exception_class = null;
|
||||
$this->error_body = null;
|
||||
$this->error_http_code = null;
|
||||
$this->error_count = 0;
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
private function prepareFieldsOnError()
|
||||
{
|
||||
$this->title = $this->title ?? '';
|
||||
$this->description = $this->description ?? '';
|
||||
$this->image = $this->image ?? '';
|
||||
}
|
||||
}
|
|
@ -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;
|
||||
}
|
||||
}
|
|
@ -0,0 +1,53 @@
|
|||
<?php
|
||||
|
||||
namespace App\MetadataResolver;
|
||||
|
||||
use Carbon\Carbon;
|
||||
use GuzzleHttp\Client;
|
||||
use Symfony\Component\DomCrawler\Crawler;
|
||||
|
||||
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);
|
||||
$html = (string) $res->getBody();
|
||||
$metadata = $this->ogpResolver->parse($html);
|
||||
$crawler = new Crawler($html);
|
||||
|
||||
// OGPのデフォルトはバナーなので、投稿に使える画像があればそれを使う
|
||||
$selector = 'img[data-actual*="image-web"]';
|
||||
if ($crawler->filter($selector)->count() !== 0) {
|
||||
$metadata->image = $crawler->filter($selector)->attr('data-actual');
|
||||
}
|
||||
|
||||
// JWTがついていれば画像URLのJWTから有効期限を拾う
|
||||
parse_str(parse_url($metadata->image, PHP_URL_QUERY), $params);
|
||||
if (isset($params['jwt'])) {
|
||||
$parts = explode('.', $params['jwt']);
|
||||
if (count($parts) !== 3) {
|
||||
throw new \RuntimeException('Invalid jwt. Image=' . $metadata->image . ' Source=' . $url);
|
||||
}
|
||||
$payload = json_decode(base64_decode(str_replace(['-', '_'], ['+', '/'], $parts[1])), true);
|
||||
|
||||
$metadata->expires_at = Carbon::createFromTimestamp($payload['exp']);
|
||||
}
|
||||
|
||||
return $metadata;
|
||||
}
|
||||
}
|
|
@ -0,0 +1,130 @@
|
|||
<?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));
|
||||
sort($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;
|
||||
}
|
||||
}
|
|
@ -0,0 +1,30 @@
|
|||
<?php
|
||||
|
||||
namespace App\MetadataResolver;
|
||||
|
||||
use Exception;
|
||||
use Throwable;
|
||||
|
||||
/**
|
||||
* メタデータの解決を禁止しているホストに対して取得を試み、ブロックされたことを表します。
|
||||
*/
|
||||
class DeniedHostException extends Exception
|
||||
{
|
||||
private $url;
|
||||
|
||||
public function __construct(string $url, Throwable $previous = null)
|
||||
{
|
||||
parent::__construct("Access denied by system policy: $url", 0, $previous);
|
||||
$this->url = $url;
|
||||
}
|
||||
|
||||
public function getUrl(): string
|
||||
{
|
||||
return $this->url;
|
||||
}
|
||||
|
||||
public function getHost(): string
|
||||
{
|
||||
return parse_url($this->url, PHP_URL_HOST);
|
||||
}
|
||||
}
|
|
@ -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;
|
||||
}
|
||||
}
|
|
@ -0,0 +1,30 @@
|
|||
<?php
|
||||
|
||||
namespace App\MetadataResolver;
|
||||
|
||||
use RuntimeException;
|
||||
use Throwable;
|
||||
|
||||
/**
|
||||
* ContentProviderの提供するrobots.txtによってクロールが拒否された場合にスローされます。
|
||||
*/
|
||||
class DisallowedByProviderException extends RuntimeException
|
||||
{
|
||||
private $url;
|
||||
|
||||
public function __construct(string $url, Throwable $previous = null)
|
||||
{
|
||||
parent::__construct("Access denied by robots.txt: $url", 0, $previous);
|
||||
$this->url = $url;
|
||||
}
|
||||
|
||||
public function getUrl(): string
|
||||
{
|
||||
return $this->url;
|
||||
}
|
||||
|
||||
public function getHost(): string
|
||||
{
|
||||
return parse_url($this->url, PHP_URL_HOST);
|
||||
}
|
||||
}
|
|
@ -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;
|
||||
}
|
||||
}
|
|
@ -0,0 +1,108 @@
|
|||
<?php
|
||||
|
||||
namespace App\MetadataResolver;
|
||||
|
||||
use GuzzleHttp\Client;
|
||||
use GuzzleHttp\Cookie\CookieJar;
|
||||
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
|
||||
{
|
||||
$cookieJar = CookieJar::fromArray(['age_check_done' => '1'], 'dmm.co.jp');
|
||||
|
||||
$res = $this->client->get($url, ['cookies' => $cookieJar]);
|
||||
$html = (string) $res->getBody();
|
||||
$crawler = new Crawler($html);
|
||||
|
||||
// 動画
|
||||
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(strip_tags(str_replace('【FANZA(ファンザ)】', '', $crawler->filter('meta[name="description"]')->attr('content'))));
|
||||
$metadata->image = preg_replace("~(pr|ps)\.jpg$~", 'pl.jpg', $crawler->filter('meta[property="og:image"]')->attr('content'));
|
||||
$metadata->tags = $this->array_finish($crawler->filter('.box-rank+table a[href*="list/=/article="]')->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('.container02 table a[href*="list/article="]')->extract(['_text']));
|
||||
|
||||
return $metadata;
|
||||
}
|
||||
|
||||
// 上で特に対応しなかったURL 画像の置換くらいはしておく
|
||||
$metadata = $this->ogpResolver->parse($html);
|
||||
$metadata->image = preg_replace("~(pr|ps)\.jpg$~", 'pl.jpg', $metadata->image);
|
||||
|
||||
return $metadata;
|
||||
}
|
||||
}
|
|
@ -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;
|
||||
}
|
||||
}
|
|
@ -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 . trim($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;
|
||||
}
|
||||
}
|
|
@ -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;
|
||||
}
|
||||
}
|
|
@ -0,0 +1,54 @@
|
|||
<?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'])) {
|
||||
$tags = [];
|
||||
foreach ($json['content']['attributes']['tags']['children'] as $tag) {
|
||||
$tags[] = preg_replace('/\s/', '_', $tag['data']['name']);
|
||||
}
|
||||
sort($tags);
|
||||
$metadata->tags = array_merge($metadata->tags, $tags);
|
||||
}
|
||||
|
||||
return $metadata;
|
||||
}
|
||||
}
|
|
@ -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;
|
||||
}
|
||||
}
|
|
@ -0,0 +1,52 @@
|
|||
<?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 = [];
|
||||
|
||||
/**
|
||||
* 重複を排除し、正規化を行ったタグの集合を返します。
|
||||
* @return string[]
|
||||
*/
|
||||
public function normalizedTags(): array
|
||||
{
|
||||
$tags = [];
|
||||
foreach ($this->tags as $tag) {
|
||||
$tag = $this->sanitize($tag);
|
||||
$tag = $this->trim($tag);
|
||||
$tags[$tag] = true;
|
||||
}
|
||||
|
||||
return array_keys($tags);
|
||||
}
|
||||
|
||||
private function sanitize(string $value): string
|
||||
{
|
||||
return preg_replace('/\r?\n/u', ' ', $value);
|
||||
}
|
||||
|
||||
private function trim(string $value): string
|
||||
{
|
||||
return trim($value);
|
||||
}
|
||||
}
|
|
@ -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]+~' => NarouResolver::class,
|
||||
'~ci-en\.(jp|net|dlsite\.com)/creator/\d+/article/\d+~' => CienResolver::class,
|
||||
'~www\.plurk\.com\/p\/.*~' => PlurkResolver::class,
|
||||
'~store\.steampowered\.com/app/\d+~' => SteamResolver::class,
|
||||
'~www\.xtube\.com/video-watch/.*-\d+$~'=> XtubeResolver::class,
|
||||
'~ss\.kb10uy\.org/posts/\d+$~' => Kb10uyShortStoryServerResolver::class,
|
||||
'~www\.hentai-foundry\.com/pictures/user/.+/\d+/.+~'=> HentaiFoundryResolver::class,
|
||||
'~(www\.)?((mobile|m)\.)?twitter\.com/(#!/)?[0-9a-zA-Z_]{1,15}/status(es)?/([0-9]+)/?(\\?.+)?$~' => TwitterResolver::class,
|
||||
];
|
||||
|
||||
public $mimeTypes = [
|
||||
'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();
|
||||
}
|
||||
}
|
|
@ -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;
|
||||
}
|
||||
}
|
|
@ -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;
|
||||
}
|
||||
}
|
|
@ -0,0 +1,64 @@
|
|||
<?php
|
||||
|
||||
namespace App\MetadataResolver;
|
||||
|
||||
use GuzzleHttp\Client;
|
||||
use Illuminate\Support\Str;
|
||||
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']) &&
|
||||
!Str::endsWith($data['thumbnailUrl'], '.gif') &&
|
||||
!Str::endsWith($data['thumbnailUrl'], '.mp4')
|
||||
) {
|
||||
// サムネイルからメイン画像に
|
||||
$metadata->image = str_replace('__rs_l160x160/', '', $data['thumbnailUrl']);
|
||||
}
|
||||
$metadata->tags = $crawler->filter('#view-tag span.tag_name')->extract('_text');
|
||||
|
||||
return $metadata;
|
||||
}
|
||||
}
|
|
@ -0,0 +1,61 @@
|
|||
<?php
|
||||
|
||||
namespace App\MetadataResolver;
|
||||
|
||||
use GuzzleHttp\Client;
|
||||
use GuzzleHttp\Cookie\CookieJar;
|
||||
use GuzzleHttp\RequestOptions;
|
||||
|
||||
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, [RequestOptions::COOKIES => new CookieJar()])->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 '';
|
||||
}
|
||||
}
|
|
@ -0,0 +1,8 @@
|
|||
<?php
|
||||
|
||||
namespace App\MetadataResolver;
|
||||
|
||||
interface Parser
|
||||
{
|
||||
public function parse(string $body): Metadata;
|
||||
}
|
|
@ -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;
|
||||
}
|
||||
}
|
|
@ -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;
|
||||
}
|
||||
}
|
|
@ -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;
|
||||
}
|
||||
}
|
|
@ -0,0 +1,8 @@
|
|||
<?php
|
||||
|
||||
namespace App\MetadataResolver;
|
||||
|
||||
interface Resolver
|
||||
{
|
||||
public function resolve(string $url): Metadata;
|
||||
}
|
|
@ -0,0 +1,16 @@
|
|||
<?php
|
||||
|
||||
namespace App\MetadataResolver;
|
||||
|
||||
use Throwable;
|
||||
|
||||
/**
|
||||
* 規定回数以上の解決失敗により、メタデータの取得が不能となっている場合にスローされます。
|
||||
*/
|
||||
class ResolverCircuitBreakException extends \RuntimeException
|
||||
{
|
||||
public function __construct(int $errorCount, string $url, Throwable $previous = null)
|
||||
{
|
||||
parent::__construct("{$errorCount}回失敗しているためメタデータの取得を中断しました: {$url}", 0, $previous);
|
||||
}
|
||||
}
|
|
@ -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;
|
||||
}
|
||||
}
|
|
@ -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;
|
||||
}
|
||||
}
|
|
@ -0,0 +1,33 @@
|
|||
<?php
|
||||
|
||||
namespace App\MetadataResolver;
|
||||
|
||||
use GuzzleHttp\Client;
|
||||
|
||||
class TwitterResolver implements Resolver
|
||||
{
|
||||
/**
|
||||
* @var Client
|
||||
*/
|
||||
private $client;
|
||||
/**
|
||||
* @var OGPResolver
|
||||
*/
|
||||
private $ogpResolver;
|
||||
|
||||
public function __construct(Client $client, OGPResolver $ogpResolver)
|
||||
{
|
||||
$this->client = $client;
|
||||
$this->ogpResolver = $ogpResolver;
|
||||
}
|
||||
|
||||
public function resolve(string $url): Metadata
|
||||
{
|
||||
$url = preg_replace('/(www\.)?(mobile|m)\.twitter\.com/u', 'twitter.com', $url);
|
||||
|
||||
$res = $this->client->get($url);
|
||||
$html = (string) $res->getBody();
|
||||
|
||||
return $this->ogpResolver->parse($html);
|
||||
}
|
||||
}
|
|
@ -0,0 +1,10 @@
|
|||
<?php
|
||||
|
||||
namespace App\MetadataResolver;
|
||||
|
||||
/**
|
||||
* MetadataResolver内で未キャッチの例外が発生した場合にスローされます。
|
||||
*/
|
||||
class UncaughtResolverException extends \RuntimeException
|
||||
{
|
||||
}
|
|
@ -0,0 +1,12 @@
|
|||
<?php
|
||||
|
||||
namespace App\MetadataResolver;
|
||||
|
||||
use Exception;
|
||||
|
||||
/**
|
||||
* このResolverやParserが対応していないサイトであったことを表わします。
|
||||
*/
|
||||
class UnsupportedContentException extends Exception
|
||||
{
|
||||
}
|
|
@ -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;
|
||||
}
|
||||
}
|
|
@ -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,12 @@
|
|||
|
||||
namespace App\Providers;
|
||||
|
||||
use App\MetadataResolver\MetadataResolver;
|
||||
use GuzzleHttp\Client;
|
||||
use GuzzleHttp\RequestOptions;
|
||||
use Illuminate\Support\Facades\Blade;
|
||||
use Illuminate\Support\ServiceProvider;
|
||||
use Parsedown;
|
||||
|
||||
class AppServiceProvider extends ServiceProvider
|
||||
{
|
||||
|
@ -13,7 +18,11 @@ class AppServiceProvider extends ServiceProvider
|
|||
*/
|
||||
public function boot()
|
||||
{
|
||||
//
|
||||
Blade::directive('parsedown', function ($expression) {
|
||||
return "<?php echo app('parsedown')->text($expression); ?>";
|
||||
});
|
||||
|
||||
stream_filter_register('convert.mbstring.*', 'Stream_Filter_Mbstring');
|
||||
}
|
||||
|
||||
/**
|
||||
|
@ -23,6 +32,18 @@ class AppServiceProvider extends ServiceProvider
|
|||
*/
|
||||
public function register()
|
||||
{
|
||||
//
|
||||
$this->app->singleton(MetadataResolver::class, function ($app) {
|
||||
return new MetadataResolver();
|
||||
});
|
||||
$this->app->singleton('parsedown', function () {
|
||||
return Parsedown::instance();
|
||||
});
|
||||
$this->app->bind(Client::class, function () {
|
||||
return new Client([
|
||||
RequestOptions::HEADERS => [
|
||||
'User-Agent' => 'TissueBot/1.0'
|
||||
]
|
||||
]);
|
||||
});
|
||||
}
|
||||
}
|
||||
|
|
|
@ -2,8 +2,10 @@
|
|||
|
||||
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\Support\Facades\Gate;
|
||||
|
||||
class AuthServiceProvider extends ServiceProvider
|
||||
{
|
||||
|
@ -14,6 +16,7 @@ class AuthServiceProvider extends ServiceProvider
|
|||
*/
|
||||
protected $policies = [
|
||||
'App\Model' => 'App\Policies\ModelPolicy',
|
||||
Ejaculation::class => EjaculationPolicy::class,
|
||||
];
|
||||
|
||||
/**
|
||||
|
@ -25,6 +28,8 @@ class AuthServiceProvider extends ServiceProvider
|
|||
{
|
||||
$this->registerPolicies();
|
||||
|
||||
//
|
||||
Gate::define('admin', function ($user) {
|
||||
return $user->is_admin;
|
||||
});
|
||||
}
|
||||
}
|
||||
|
|
|
@ -2,8 +2,8 @@
|
|||
|
||||
namespace App\Providers;
|
||||
|
||||
use Illuminate\Support\ServiceProvider;
|
||||
use Illuminate\Support\Facades\Broadcast;
|
||||
use Illuminate\Support\ServiceProvider;
|
||||
|
||||
class BroadcastServiceProvider extends ServiceProvider
|
||||
{
|
||||
|
|
|
@ -2,8 +2,10 @@
|
|||
|
||||
namespace App\Providers;
|
||||
|
||||
use Illuminate\Support\Facades\Event;
|
||||
use Illuminate\Auth\Events\Registered;
|
||||
use Illuminate\Auth\Listeners\SendEmailVerificationNotification;
|
||||
use Illuminate\Foundation\Support\Providers\EventServiceProvider as ServiceProvider;
|
||||
use Illuminate\Support\Facades\Event;
|
||||
|
||||
class EventServiceProvider extends ServiceProvider
|
||||
{
|
||||
|
@ -13,9 +15,12 @@ class EventServiceProvider extends ServiceProvider
|
|||
* @var array
|
||||
*/
|
||||
protected $listen = [
|
||||
'App\Events\Event' => [
|
||||
'App\Listeners\EventListener',
|
||||
Registered::class => [
|
||||
SendEmailVerificationNotification::class,
|
||||
],
|
||||
'App\Events\LinkDiscovered' => [
|
||||
'App\Listeners\LinkCollector'
|
||||
]
|
||||
];
|
||||
|
||||
/**
|
||||
|
|
|
@ -0,0 +1,31 @@
|
|||
<?php
|
||||
|
||||
namespace App\Providers;
|
||||
|
||||
use App\Utilities\Formatter;
|
||||
use Illuminate\Support\ServiceProvider;
|
||||
|
||||
class FormatterServiceProvider extends ServiceProvider
|
||||
{
|
||||
/**
|
||||
* Bootstrap the application services.
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
public function boot()
|
||||
{
|
||||
//
|
||||
}
|
||||
|
||||
/**
|
||||
* Register the application services.
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
public function register()
|
||||
{
|
||||
$this->app->singleton(Formatter::class, function ($app) {
|
||||
return new Formatter();
|
||||
});
|
||||
}
|
||||
}
|
|
@ -2,8 +2,8 @@
|
|||
|
||||
namespace App\Providers;
|
||||
|
||||
use Illuminate\Support\Facades\Route;
|
||||
use Illuminate\Foundation\Support\Providers\RouteServiceProvider as ServiceProvider;
|
||||
use Illuminate\Support\Facades\Route;
|
||||
|
||||
class RouteServiceProvider extends ServiceProvider
|
||||
{
|
||||
|
|
|
@ -0,0 +1,30 @@
|
|||
<?php
|
||||
|
||||
namespace App\Providers;
|
||||
|
||||
use App\Http\ViewComposers\ProfileStatsComposer;
|
||||
use Illuminate\Support\Facades\View;
|
||||
use Illuminate\Support\ServiceProvider;
|
||||
|
||||
class ViewComposerServiceProvider extends ServiceProvider
|
||||
{
|
||||
/**
|
||||
* Bootstrap the application services.
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
public function boot()
|
||||
{
|
||||
View::composer('components.profile-stats', ProfileStatsComposer::class);
|
||||
}
|
||||
|
||||
/**
|
||||
* Register the application services.
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
public function register()
|
||||
{
|
||||
//
|
||||
}
|
||||
}
|
|
@ -0,0 +1,82 @@
|
|||
<?php
|
||||
|
||||
namespace App\Rules;
|
||||
|
||||
use Illuminate\Contracts\Validation\Rule;
|
||||
|
||||
/**
|
||||
* CSVインポート機能の日時バリデーションルール
|
||||
* @package App\Rules
|
||||
*/
|
||||
class CsvDateTime implements Rule
|
||||
{
|
||||
const VALID_FORMATS = [
|
||||
'Y/m/d H:i:s',
|
||||
'Y/n/j G:i:s',
|
||||
'Y/m/d H:i',
|
||||
'Y/n/j G:i',
|
||||
];
|
||||
|
||||
const MINIMUM_TIMESTAMP = 946652400; // 2000-01-01 00:00:00 JST
|
||||
const MAXIMUM_TIMESTAMP = 4102412399; // 2099-12-31 23:59:59 JST
|
||||
|
||||
/** @var string Validation error message */
|
||||
private $message = ':attributeの形式は "年/月/日 時:分" にしてください。';
|
||||
|
||||
/**
|
||||
* Create a new rule instance.
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
public function __construct()
|
||||
{
|
||||
//
|
||||
}
|
||||
|
||||
/**
|
||||
* Determine if the validation rule passes.
|
||||
*
|
||||
* @param string $attribute
|
||||
* @param mixed $value
|
||||
* @return bool
|
||||
*/
|
||||
public function passes($attribute, $value)
|
||||
{
|
||||
// この辺の実装の元ネタは、LaravelのValidatesAttributes#validateDateFormat()
|
||||
|
||||
if (!is_string($value)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
foreach (self::VALID_FORMATS as $format) {
|
||||
$date = \DateTime::createFromFormat('!' . $format, $value);
|
||||
if (!$date) {
|
||||
continue;
|
||||
}
|
||||
|
||||
$timestamp = (int) $date->format('U');
|
||||
if ($timestamp < self::MINIMUM_TIMESTAMP || self::MAXIMUM_TIMESTAMP < $timestamp) {
|
||||
$this->message = ':attributeは 2000/01/01 00:00 〜 2099/12/31 23:59 の間のみ対応しています。';
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
$formatted = $date->format($format);
|
||||
if ($formatted === $value) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the validation error message.
|
||||
*
|
||||
* @return string
|
||||
*/
|
||||
public function message()
|
||||
{
|
||||
return $this->message;
|
||||
}
|
||||
}
|
|
@ -0,0 +1,62 @@
|
|||
<?php
|
||||
|
||||
namespace App\Rules;
|
||||
|
||||
use Illuminate\Contracts\Validation\Rule;
|
||||
|
||||
class FuzzyBoolean implements Rule
|
||||
{
|
||||
public static function isTruthy($value): bool
|
||||
{
|
||||
if ($value === 1 || $value === '1') {
|
||||
return true;
|
||||
}
|
||||
|
||||
$lower = strtolower((string)$value);
|
||||
|
||||
return $lower === 'true';
|
||||
}
|
||||
|
||||
public static function isFalsy($value): bool
|
||||
{
|
||||
if ($value === null || $value === '' || $value === 0 || $value === '0') {
|
||||
return true;
|
||||
}
|
||||
|
||||
$lower = strtolower((string)$value);
|
||||
|
||||
return $lower === 'false';
|
||||
}
|
||||
|
||||
/**
|
||||
* Create a new rule instance.
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
public function __construct()
|
||||
{
|
||||
//
|
||||
}
|
||||
|
||||
/**
|
||||
* Determine if the validation rule passes.
|
||||
*
|
||||
* @param string $attribute
|
||||
* @param mixed $value
|
||||
* @return bool
|
||||
*/
|
||||
public function passes($attribute, $value)
|
||||
{
|
||||
return self::isTruthy($value) || self::isFalsy($value);
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the validation error message.
|
||||
*
|
||||
* @return string
|
||||
*/
|
||||
public function message()
|
||||
{
|
||||
return __('validation.boolean');
|
||||
}
|
||||
}
|
|
@ -0,0 +1,64 @@
|
|||
<?php
|
||||
|
||||
namespace App\Services;
|
||||
|
||||
use App\User;
|
||||
use Illuminate\Support\Facades\DB;
|
||||
use League\Csv\Writer;
|
||||
|
||||
class CheckinCsvExporter
|
||||
{
|
||||
/** @var User Target user */
|
||||
private $user;
|
||||
/** @var string Output filename */
|
||||
private $filename;
|
||||
/** @var string Output charset */
|
||||
private $charset;
|
||||
|
||||
public function __construct(User $user, string $filename, string $charset)
|
||||
{
|
||||
$this->user = $user;
|
||||
$this->filename = $filename;
|
||||
$this->charset = $charset;
|
||||
}
|
||||
|
||||
public function execute()
|
||||
{
|
||||
$csv = Writer::createFromPath($this->filename, 'wb');
|
||||
$csv->setNewline("\r\n");
|
||||
if ($this->charset === 'SJIS-win') {
|
||||
$csv->addStreamFilter('convert.mbstring.encoding.UTF-8:SJIS-win');
|
||||
}
|
||||
|
||||
$header = ['日時', 'ノート', 'オカズリンク', '非公開', 'センシティブ'];
|
||||
for ($i = 1; $i <= 32; $i++) {
|
||||
$header[] = "タグ{$i}";
|
||||
}
|
||||
$csv->insertOne($header);
|
||||
|
||||
DB::transaction(function () use ($csv) {
|
||||
// TODO: そんなに読み取り整合性を保つ努力はしていないのと、chunkの件数これでいいか分からない
|
||||
$this->user->ejaculations()->with('tags')->orderBy('ejaculated_date')
|
||||
->chunk(1000, function ($ejaculations) use ($csv) {
|
||||
foreach ($ejaculations as $ejaculation) {
|
||||
$record = [
|
||||
$ejaculation->ejaculated_date->format('Y/m/d H:i'),
|
||||
$ejaculation->note,
|
||||
$ejaculation->link,
|
||||
self::formatBoolean($ejaculation->is_private),
|
||||
self::formatBoolean($ejaculation->is_too_sensitive),
|
||||
];
|
||||
foreach ($ejaculation->tags->take(32) as $tag) {
|
||||
$record[] = $tag->name;
|
||||
}
|
||||
$csv->insertOne($record);
|
||||
}
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
private static function formatBoolean($value): string
|
||||
{
|
||||
return $value ? 'true' : 'false';
|
||||
}
|
||||
}
|
Some files were not shown because too many files have changed in this diff Show More
Loading…
Reference in New Issue