kopia lustrzana https://github.com/pixelfed/pixelfed
Porównaj commity
1351 Commity
Autor | SHA1 | Data |
---|---|---|
daniel | ab383ba55b | |
Daniel Supernault | 29e472d6ca | |
Daniel Supernault | 9a5e3471d4 | |
Daniel Supernault | 61d105fd25 | |
daniel | 101e620bf5 | |
Daniel Supernault | 2d3f1df003 | |
Daniel Supernault | 7c19baf5dc | |
Daniel Supernault | 326bb93b8b | |
daniel | 64631c7233 | |
Daniel Supernault | 902572ed51 | |
Daniel Supernault | 60a62b59c9 | |
daniel | 3e59dd2868 | |
Daniel Supernault | cbf996c9b6 | |
daniel | cf10fca74f | |
Daniel Supernault | aa94dde376 | |
daniel | eecc7bef61 | |
Daniel Supernault | 3c877523b1 | |
Daniel Supernault | 4223119f58 | |
daniel | 74a3d5c6c0 | |
Daniel Supernault | 7f8bba4415 | |
daniel | 6263e90a13 | |
Daniel Supernault | e46bd6cc06 | |
daniel | ae60c99679 | |
Daniel Supernault | a54b4fb038 | |
daniel | 2f7481205c | |
Daniel Supernault | cde17f5af7 | |
Daniel Supernault | 433bc4c286 | |
daniel | 5d407ededf | |
Daniel Supernault | 65166570c5 | |
daniel | 610326e7b0 | |
Daniel Supernault | 6fc066a213 | |
Daniel Supernault | 8b8b1ffc5c | |
Daniel Supernault | 9a7acc12a6 | |
Daniel Supernault | 51b6fe7dc8 | |
daniel | c4ffa62242 | |
Daniel Supernault | 87ee0633fe | |
Daniel Supernault | ded660b2c4 | |
daniel | 4d04227d41 | |
Daniel Supernault | 26f92c93ce | |
Daniel Supernault | 81566987e4 | |
Daniel Supernault | 8af2360779 | |
daniel | b3fb69c5b1 | |
Daniel Supernault | f30f7d79fb | |
Daniel Supernault | 2deb65d874 | |
Daniel Supernault | ad03291699 | |
daniel | 6be21891d5 | |
daniel | 141f6d38a7 | |
Daniel Supernault | 4608c66c0b | |
Christian Winther | e227ee1bd5 | |
Christian Winther | dbc5df849f | |
Daniel Supernault | 6bdf73de4d | |
daniel | 5b3d0206ae | |
Daniel Supernault | 858fcbf606 | |
daniel | 57df06a0b6 | |
Daniel Supernault | db1a4c9f8e | |
Daniel Supernault | ce4beab9c8 | |
Daniel Supernault | 9d5479de39 | |
Daniel Supernault | 512518d319 | |
Daniel Supernault | b06a3455c2 | |
Daniel Supernault | 94a6e8614a | |
Daniel Supernault | 81d1e0fdab | |
Daniel Supernault | b8e96a5ff3 | |
Daniel Supernault | b7322b6874 | |
daniel | 142db2c41e | |
Daniel Supernault | 8c6936409d | |
daniel | e2c2952fda | |
daniel | dfab7e945a | |
daniel | 363196883d | |
daniel | f1eaaa80be | |
daniel | 0b162dc15e | |
Daniel Supernault | a9e54aa540 | |
Daniel Supernault | 3871a80391 | |
Daniel Supernault | 4147f7c521 | |
Daniel Supernault | 039dfaa6c3 | |
Daniel Supernault | f318bd7a30 | |
Daniel Supernault | d946afcc5c | |
Daniel Supernault | 2dcbc1d5ef | |
Daniel Supernault | ec2fdd61f7 | |
Daniel Supernault | aba1e13d43 | |
Daniel Supernault | dcc5f416ef | |
Emelia Smith | 1eadff9d2e | |
Daniel Supernault | ac1f074889 | |
Daniel Supernault | 5162c0704a | |
Daniel Supernault | 3628b4625c | |
Daniel Supernault | 674e560f04 | |
Daniel Supernault | eb4871237b | |
Daniel Supernault | 704e7b12e0 | |
Daniel Supernault | cee979eda8 | |
Daniel Supernault | 828a456f36 | |
Daniel Supernault | 087b27916f | |
Daniel Supernault | 6ce513f8c3 | |
Daniel Supernault | 949e99798e | |
Daniel Supernault | 911446c03e | |
Daniel Supernault | a76cb5f4f8 | |
Daniel Supernault | da0e0ffabf | |
Daniel Supernault | 2d113de536 | |
Daniel Supernault | d1adb109de | |
Daniel Supernault | ce228f7fa4 | |
Daniel Supernault | 5071aaf408 | |
Daniel Supernault | 40478f258a | |
Daniel Supernault | d670de175e | |
Daniel Supernault | fecbe1897b | |
Daniel Supernault | 665581d80c | |
Daniel Supernault | a72188a7db | |
Daniel Supernault | ad506e901d | |
Daniel Supernault | f2f2a8097c | |
Daniel Supernault | f08aab2231 | |
Daniel Supernault | 8a0c456edc | |
Daniel Supernault | 75081e609a | |
Daniel Supernault | 481314cd23 | |
Daniel Supernault | 8a89e3c963 | |
Daniel Supernault | c96167f2f7 | |
Christian Winther | ad382f8f55 | |
Christian Winther | 3a1f4789e6 | |
Christian Winther | 4942f7fbd4 | |
Christian Winther | cc8c5ccd37 | |
Christian Winther | 56d47dd1bc | |
Christian Winther | 1892f68ebd | |
Christian Winther | ca7c2d34f2 | |
daniel | 8aae92d75b | |
Daniel Supernault | bf46f6f5f4 | |
Daniel Supernault | b0cb4456a9 | |
Daniel Supernault | 7785a2dae4 | |
daniel | 57f4457637 | |
Daniel Supernault | 5e4d4eff9d | |
daniel | 3132523798 | |
Daniel Supernault | a4bc5ce3d0 | |
daniel | f4086d4381 | |
Daniel Supernault | 37a82cfb90 | |
Daniel Supernault | 4aa0e25f4c | |
daniel | 24c467c558 | |
Daniel Supernault | bcce1df6fc | |
Daniel Supernault | a969ca502f | |
Daniel Supernault | 36c518fe2c | |
Daniel Supernault | 95199843e3 | |
Daniel Supernault | 853a729f76 | |
Daniel Supernault | e742d595a6 | |
Daniel Supernault | 2e5e68e447 | |
Daniel Supernault | 63100fe950 | |
Daniel Supernault | 25f3fa06af | |
daniel | f09313a512 | |
Daniel Supernault | 15ad69f76e | |
Christian Winther | 091de696c2 | |
daniel | 7c2ecd8706 | |
Daniel Supernault | b89cd4c44f | |
Daniel Supernault | cee618e844 | |
daniel | 4516760ced | |
Daniel Supernault | 94503a1cf9 | |
daniel | 5d7e091978 | |
Daniel Supernault | 18382e8a1f | |
Daniel Supernault | 592c84125c | |
daniel | b6437380b4 | |
Daniel Supernault | e68fe64ffc | |
Daniel Supernault | 3a27e637f8 | |
Christian Winther | dd5878b256 | |
Christian Winther | ae645ddd15 | |
Christian Winther | e38aa65dad | |
daniel | 0355830c5c | |
Daniel Supernault | eccdbe1f57 | |
Daniel Supernault | 632f2cb619 | |
daniel | 23f7b74400 | |
Daniel Supernault | b1cdf4464f | |
Daniel Supernault | b122c60de7 | |
Daniel Supernault | 6036d96e3f | |
Daniel Supernault | ff150ca6c9 | |
daniel | fc8462d565 | |
Daniel Supernault | 6231994253 | |
daniel | 5d21bba7b5 | |
Daniel Supernault | d18824e719 | |
Daniel Supernault | d3f6c71b8e | |
daniel | 0bd3e0ab80 | |
Daniel Supernault | 5fb26a78bc | |
daniel | 1251bf532c | |
Daniel Supernault | 03165ea46f | |
Daniel Supernault | 3b5500b3a5 | |
Daniel Supernault | 1a811b1840 | |
Daniel Supernault | e3826c587d | |
Daniel Supernault | d6eac65555 | |
daniel | eb19c35343 | |
Daniel Supernault | 6cb1484b3e | |
Daniel Supernault | 01535a6cfe | |
Daniel Supernault | 31e6487dc9 | |
Daniel Supernault | a933615b8d | |
daniel | a9b99d8f9d | |
Daniel Supernault | 3f0539978e | |
daniel | 712b6d27a9 | |
Daniel Supernault | 4a6be62128 | |
Daniel Supernault | 45bdfe1efd | |
Shlee | 7fd5599fc4 | |
Daniel Supernault | 7613eec476 | |
Daniel Supernault | 9bc5338dbd | |
Daniel Supernault | f8145a78cf | |
Christian Winther | d92cf7f92f | |
daniel | 99611f90ea | |
Daniel Supernault | d5a6d9cc8d | |
Daniel Supernault | 542d110673 | |
Daniel Supernault | 402a4607c9 | |
Christian Winther | 5d56460082 | |
daniel | 189e87f28a | |
Daniel Supernault | c4190eec08 | |
Daniel Supernault | 0bb7d379c5 | |
Daniel Supernault | 372a116a2c | |
Daniel Supernault | ef0ff78e4a | |
Daniel Supernault | ab9ecb6efd | |
daniel | e4f33e823d | |
Daniel Supernault | 2f48df8ca8 | |
Christian Winther | 6fa112162f | |
Daniel Supernault | a16309ac18 | |
Daniel Supernault | 767522a85c | |
daniel | 8c1e136ce9 | |
Daniel Supernault | 071163b47b | |
Christian Winther | acb699bf13 | |
Christian Winther | 02369cce66 | |
Christian Winther | c1c361ef9b | |
Christian Winther | b08bb3669d | |
Christian Winther | 1976af6dd1 | |
Christian Winther | 020bda85db | |
daniel | 507f45f139 | |
Daniel Supernault | 795e91e3bc | |
Daniel Supernault | 2b5d723582 | |
daniel | 7159d5cb3e | |
Daniel Supernault | 84aeec3b4e | |
Daniel Supernault | 089ba3c471 | |
Christian Winther | df1f62e734 | |
Christian Winther | c8c2e1c2eb | |
Christian Winther | 2d8e81c83f | |
Christian Winther | 515198b28c | |
Christian Winther | f0e30c8ab6 | |
Christian Winther | 7ffbd5d44a | |
Christian Winther | 5a43d7a65d | |
Christian Winther | 027f858d85 | |
Christian Winther | e2821adcca | |
Christian Winther | af47d91e7d | |
Christian Winther | 193d536ca1 | |
Christian Winther | f264dd1cbb | |
Christian Winther | d9d2a475d8 | |
Christian Winther | 8fd27c6f0c | |
Christian Winther | 0addfe5605 | |
Christian Winther | 28b83b575f | |
Christian Winther | 3bfd043792 | |
Christian Winther | 0ecebbb8bf | |
Christian Winther | 9c26bf26dd | |
Christian Winther | ae358e47cb | |
Christian Winther | 14f8478e6a | |
Christian Winther | 26d6f8f9fe | |
daniel | 36f84db03b | |
Daniel Supernault | eadf2e9d1d | |
Daniel Supernault | b0ecdc8162 | |
Daniel Supernault | 59c70239f8 | |
daniel | b2f29a4590 | |
Daniel Supernault | 4c5e8288b0 | |
Daniel Supernault | c4dde64119 | |
Christian Winther | f486bfb73e | |
Christian Winther | adf1af3703 | |
Christian Winther | abee7d4d62 | |
Christian Winther | 5a9cfe1f2a | |
daniel | 1f6dc94a34 | |
Daniel Supernault | e1715d40b8 | |
Daniel Supernault | 0ad3654da3 | |
Daniel Supernault | 06655c3a8b | |
Daniel Supernault | cae26c666d | |
Daniel Supernault | 8355d5d00c | |
Daniel Supernault | 8dac2caf1d | |
daniel | 4057ee3bb3 | |
Daniel Supernault | 9409c569bd | |
Daniel Supernault | b6c97d1d26 | |
Daniel Supernault | eb0e76f8e2 | |
Daniel Supernault | 1f74a95d0c | |
daniel | 6c0c61e45a | |
daniel | 8cb7ebdd8b | |
Daniel Supernault | ce9c0e0b24 | |
Daniel Supernault | 545f7d5e70 | |
daniel | f97afcf47e | |
daniel | b339f4a5b0 | |
Daniel Supernault | 147113cc95 | |
Daniel Supernault | ea6b162340 | |
Daniel Supernault | 4c26f59cd0 | |
Christian Winther | 9117df186c | |
Christian Winther | 4dc15bb37d | |
daniel | 4eb0c36480 | |
Daniel Supernault | 17027c3487 | |
Daniel Supernault | 011834f473 | |
Christian Winther | 9a1c4d42b5 | |
Christian Winther | 6edd712581 | |
Christian Winther | d4198b3262 | |
Emelia Smith | 9978b2b959 | |
daniel | 01a86009e6 | |
Daniel Supernault | d835e0adaa | |
daniel | f45d293707 | |
Daniel Supernault | 66f6640072 | |
daniel | 25dd2e46fe | |
Daniel Supernault | f07843a0f2 | |
Daniel Supernault | 56b736c325 | |
Daniel Supernault | b0fb198829 | |
daniel | e1d579b10b | |
Daniel Supernault | 83eadbb811 | |
Daniel Supernault | 40b45b2a11 | |
daniel | ccbba91e70 | |
Daniel Supernault | bc4d223714 | |
daniel | 0032415459 | |
Daniel Supernault | 70fc44dfe5 | |
Daniel Supernault | 0f3ca19461 | |
daniel | 0dc54e9ac0 | |
Daniel Supernault | df5e61266c | |
Daniel Supernault | 1232cfc86a | |
daniel | af935c729b | |
Daniel Supernault | 4c6ec20e36 | |
Daniel Supernault | fb0bb9a34f | |
Christian Winther | d3bbfdb6e0 | |
daniel | b10c60584b | |
Daniel Supernault | 221fe43638 | |
Daniel Supernault | 97c131fdf2 | |
Daniel Supernault | 78da12004f | |
Daniel Supernault | 8b4ac5cc0b | |
daniel | bbd3688333 | |
Daniel Supernault | 7b8977e9cc | |
Daniel Supernault | 0faf59e3b7 | |
Christian Winther | 49a778d128 | |
Christian Winther | fd62962d20 | |
Christian Winther | e18d6083a2 | |
Christian Winther | 143d5703dd | |
Christian Winther | bc66b6da18 | |
Christian Winther | d8e1caec53 | |
daniel | 8fa6ae421b | |
Daniel Supernault | e5bbe9340a | |
Daniel Supernault | 7a7b4bc717 | |
daniel | 8ab9951909 | |
Daniel Supernault | 62b9eef805 | |
daniel | 67167a5b90 | |
Daniel Supernault | fd7f5dbba1 | |
daniel | 0649bb4754 | |
Daniel Supernault | e354750808 | |
daniel | 0dbbc6a6b4 | |
Daniel Supernault | 607b239c1a | |
Daniel Supernault | 2e6100f275 | |
daniel | 7e47d6dccb | |
Christian Winther | d9a9507cc8 | |
Christian Winther | 5bd93b0f5e | |
Christian Winther | 3d6efd098d | |
Emelia Smith | 0f8e45fe75 | |
Emelia Smith | 9330cd02f7 | |
Emelia Smith | 7b0a6060b2 | |
Christian Winther | d374d73ba7 | |
daniel | 73b4dab9a8 | |
Daniel Supernault | 2becd273c4 | |
Daniel Supernault | 4d02d6f12e | |
daniel | a1b0d3d3c0 | |
Daniel Supernault | 97b7cb2719 | |
daniel | 6ea20716bc | |
Daniel Supernault | 1f3f0cae65 | |
Daniel Supernault | 6921d3568e | |
Daniel Supernault | 5b284cacea | |
Daniel Supernault | 01b33fb37e | |
Daniel Supernault | 1e3acadefb | |
Daniel Supernault | ac01f51ab6 | |
Daniel Supernault | 289cad470b | |
Daniel Supernault | 240e6bbe4f | |
daniel | 111ba70473 | |
Daniel Supernault | 80e0ada946 | |
Daniel Supernault | fa97a1f38e | |
Daniel Supernault | 4d4013896c | |
daniel | 3a557d7ffc | |
Daniel Supernault | 152b6eab9a | |
Daniel Supernault | 00ed330cf3 | |
daniel | 2483832754 | |
Daniel Supernault | 09ca96cc2b | |
Daniel Supernault | 8b843d620c | |
Daniel Supernault | b81ae5773f | |
daniel | 5d89fe8130 | |
Daniel Supernault | ddf7f09ad4 | |
Daniel Supernault | 7caed381fb | |
daniel | 3d5fd48a22 | |
Daniel Supernault | d3ff89e538 | |
Daniel Supernault | 32c59f0440 | |
daniel | 4050055e5e | |
Daniel Supernault | 04c5e550a5 | |
Daniel Supernault | 081360b905 | |
Daniel Supernault | edbb07cc37 | |
Daniel Supernault | 622e9cee97 | |
daniel | 9dcb25c8e2 | |
Daniel Supernault | 5b7111c56f | |
Daniel Supernault | cf00542336 | |
Daniel Supernault | 59aa6a4b02 | |
daniel | 33d1faf734 | |
Daniel Supernault | 339857ffa2 | |
Daniel Supernault | 0aff126aa0 | |
mbliznikova | fd4f41a14e | |
daniel | 29785b5654 | |
Daniel Supernault | 8a9a7c0e47 | |
Daniel Supernault | 61b1523368 | |
Daniel Supernault | 92ff114d2d | |
daniel | 6d8ba64e0b | |
daniel | 44f92f4888 | |
daniel | d4e4c4e1dd | |
Christian Winther | 2aeccf885f | |
Christian Winther | 043f914c8c | |
Christian Winther | 3723f36043 | |
Christian Winther | ef37c8f234 | |
Christian Winther | b73d452255 | |
Christian Winther | 36850235a8 | |
Christian Winther | 1a6e97c98b | |
Christian Winther | 8bdb0ca77b | |
Christian Winther | c4f984b205 | |
Christian Winther | 1616c7cb11 | |
Christian Winther | ca5710b5ae | |
Christian Winther | a665168031 | |
Christian Winther | 335e6954d2 | |
Christian Winther | 5c208d0519 | |
Christian Winther | aa2669c327 | |
Christian Winther | 8189b01a26 | |
Christian Winther | d372b9dee7 | |
Christian Winther | d2ed117d3f | |
Christian Winther | 8d61b8d250 | |
Christian Winther | c859367e10 | |
Christian Winther | 6fee842b7a | |
Christian Winther | 627fffd1ce | |
Christian Winther | f263dfc4e1 | |
Shlee | 934f2ffdb4 | |
Christian Winther | c9a3e3aea7 | |
Christian Winther | 8672453596 | |
daniel | 51c6935e05 | |
Daniel Supernault | 6167ebc654 | |
daniel | c77e427fa3 | |
Daniel Supernault | efe8e89046 | |
daniel | 1f6577c947 | |
Daniel Supernault | 96366ab3de | |
daniel | dbf59367df | |
Daniel Supernault | c26a3d2817 | |
Daniel Supernault | 5afe7abdfb | |
Daniel Supernault | 67c650b195 | |
Daniel Supernault | 0325e17115 | |
daniel | ca05279bb6 | |
Daniel Supernault | 74423b52ca | |
Daniel Supernault | d76f01685c | |
Christian Winther | 347ac6f82b | |
Christian Winther | 70f4bc06a8 | |
Christian Winther | a940bedf9e | |
Christian Winther | 2d223d61ed | |
Christian Winther | f2b28ece6e | |
Christian Winther | 3598f9f8f4 | |
Christian Winther | 29564a5809 | |
Christian Winther | 033db841f4 | |
Christian Winther | a383233710 | |
Christian Winther | 32ad4266d0 | |
Christian Winther | 1500791198 | |
Christian Winther | e858a453be | |
Christian Winther | 4729ffb7d5 | |
Christian Winther | 83d92c4819 | |
Christian Winther | eba2db76f2 | |
Christian Winther | a3fd373796 | |
Christian Winther | 98bae1316f | |
Christian Winther | 068143639f | |
Christian Winther | f135a240cd | |
Christian Winther | a094a0bd66 | |
Christian Winther | dc95d4d800 | |
Christian Winther | ca0a25912a | |
Christian Winther | 62efe8b3d4 | |
Christian Winther | ead7c33275 | |
Christian Winther | cc9f673eea | |
Christian Winther | 921f34d42e | |
Christian Winther | 82ab545f1a | |
Christian Winther | eee17fe9f2 | |
Christian Winther | adbd66eb38 | |
Christian Winther | 3feb93b034 | |
Christian Winther | a4646df8f2 | |
Christian Winther | 2d05eccb87 | |
Christian Winther | e70e13e265 | |
Christian Winther | 90c9d8b5a6 | |
Christian Winther | 9ad04a285a | |
Christian Winther | 3a7fd8eac9 | |
Christian Winther | 45f1df78b0 | |
Christian Winther | 44266b950b | |
Christian Winther | be2ba79dc2 | |
Christian Winther | d8b37e6870 | |
Christian Winther | 6563d4d0b9 | |
Christian Winther | afa335b7b5 | |
Christian Winther | a70f108616 | |
Christian Winther | bb960fd485 | |
Christian Winther | 88ad5d6a4f | |
Christian Winther | 24220ef2a8 | |
daniel | 84b63f8aa9 | |
Daniel Supernault | 979aa55135 | |
Christian Winther | daba285ea7 | |
Christian Winther | de96c5f06d | |
Christian Winther | 72b454143b | |
Christian Winther | af1df5edfd | |
Christian Winther | 2135199c97 | |
Christian Winther | 9c426b48a1 | |
Christian Winther | 48e5d45b3f | |
Christian Winther | 98660760c9 | |
Christian Winther | 53eb9c11fc | |
Christian Winther | 903aeb7608 | |
Christian Winther | 685f62a5d0 | |
Christian Winther | 7f99bb1024 | |
Christian Winther | fa10fe999e | |
Christian Winther | b2d6d3dbe7 | |
Christian Winther | f2f2517503 | |
Christian Winther | ed0f9d64c8 | |
Christian Winther | 901d11df60 | |
Christian Winther | 20ef1c7b94 | |
Christian Winther | 20a15c2b65 | |
Christian Winther | 01ecde1592 | |
Christian Winther | 9814a39fd8 | |
Christian Winther | 519704cbe8 | |
Christian Winther | 543dac34f6 | |
Christian Winther | edbc1e4d60 | |
Christian Winther | c258a15761 | |
Christian Winther | 84c9aeb514 | |
Christian Winther | 73b6db168a | |
daniel | 187d1e1af9 | |
Daniel Supernault | 85a612742d | |
Daniel Supernault | c91f1c595a | |
Daniel Supernault | db1b466792 | |
Daniel Supernault | c7ed684a5c | |
Daniel Supernault | 71c148c61e | |
Daniel Supernault | fe30cd25d1 | |
Daniel Supernault | fd9b5ad443 | |
Daniel Supernault | 9d365d07f9 | |
Daniel Supernault | 2dcfc81495 | |
Daniel Supernault | 1a16ec2078 | |
Daniel Supernault | 42298a2e9c | |
Daniel Supernault | 58745a8808 | |
Daniel Supernault | 5f6ed85770 | |
Daniel Supernault | 319a20b473 | |
Daniel Supernault | ef57d471e5 | |
Daniel Supernault | c53894fe16 | |
mbliznikova | 4e567e3411 | |
Christian Winther | 6f0a6aeb3d | |
nexryai | 19e8037c85 | |
daniel | 0a556d1ac1 | |
Daniel Supernault | d25209f74a | |
Christian Winther | 2e3c7e862c | |
Christian Winther | 284bb26d92 | |
Christian Winther | 9445980e04 | |
Christian Winther | bd1cd9c4fc | |
Christian Winther | e228a1622d | |
Christian Winther | c9b11a4a29 | |
Christian Winther | 092f7f704c | |
Christian Winther | 6edf266a14 | |
Christian Winther | a8c5585e19 | |
Christian Winther | a25b7910b2 | |
Christian Winther | 7db513b366 | |
Christian Winther | 76e1199dc7 | |
Christian Winther | 2e2ffc5519 | |
Christian Winther | d876533991 | |
Christian Winther | c4404590f2 | |
Christian Winther | c1fbccb07c | |
Christian Winther | 052c11882c | |
Christian Winther | 215b49ea3d | |
Christian Winther | 10674ac523 | |
Christian Winther | f2eb3df85f | |
Christian Winther | 5cfd8e15a9 | |
Christian Winther | 99e2a045a6 | |
Christian Winther | d13895a3e0 | |
Christian Winther | 895b51fd9f | |
Christian Winther | 890827d60e | |
Christian Winther | c12ef66c56 | |
Christian Winther | c64571e46d | |
Christian Winther | f2c8497136 | |
Christian Winther | ce34e4d046 | |
Christian Winther | a08a5e7cde | |
Christian Winther | e05575283a | |
Christian Winther | c369ef50a7 | |
Christian Winther | 7dcca09c65 | |
Christian Winther | 7b3e11012f | |
Christian Winther | 0aee66810d | |
Christian Winther | 6244511cf8 | |
Christian Winther | f390c3c3e9 | |
Christian Winther | cf080dda09 | |
Christian Winther | b19d3a20dd | |
daniel | d8a5dc00bb | |
Daniel Supernault | bca2484994 | |
daniel | 5f5cb0616d | |
daniel | d7efe1a7ee | |
daniel | d0f7865508 | |
Daniel Supernault | 5087a87885 | |
Daniel Supernault | fd44c80ce9 | |
daniel | 5b4214cb80 | |
Daniel Supernault | 0ef6812709 | |
Daniel Supernault | cbe75ce871 | |
Daniel Supernault | 75b0f2dda0 | |
Daniel Supernault | d39946b045 | |
Daniel Supernault | 7b6c9c7428 | |
Christian Winther | 98211d3620 | |
Daniel Supernault | 7dbdbf15a5 | |
daniel | 238f646306 | |
Daniel Supernault | f66b9fe74e | |
Daniel Supernault | adfaa2b140 | |
daniel | 25a4289dc3 | |
Daniel Supernault | 1be21c76f3 | |
daniel | eebed73a5e | |
Daniel Supernault | 73a0f528ab | |
Daniel Supernault | d8f46f47a1 | |
Daniel Supernault | fa0380ac3b | |
Daniel Supernault | 519c7a3735 | |
Daniel Supernault | f3f0175c84 | |
Daniel Supernault | 3e28cf661b | |
Daniel Supernault | e98df1196f | |
Daniel Supernault | 6c39df7fb3 | |
Daniel Supernault | 5169936062 | |
Daniel Supernault | 89b8e87477 | |
Daniel Supernault | fcbcd7ec73 | |
Daniel Supernault | c3f16c87a3 | |
Daniel Supernault | 21947835f8 | |
Daniel Supernault | 6d81214138 | |
Daniel Supernault | 6d55cb27ee | |
Daniel Supernault | b3148b788e | |
Daniel Supernault | 29aa87c282 | |
Daniel Supernault | 0455dd1996 | |
Daniel Supernault | ae1db1e3ab | |
Daniel Supernault | dd16189fc8 | |
Daniel Supernault | 795132df18 | |
Daniel Supernault | 87bba03d23 | |
Daniel Supernault | 54adbeb059 | |
Daniel Supernault | 9d621108b0 | |
Daniel Supernault | 484a377a44 | |
Daniel Supernault | 1664a5bc52 | |
Daniel Supernault | a492a95a0e | |
Daniel Supernault | 5c1591fdff | |
Daniel Supernault | 819e7d3b32 | |
Daniel Supernault | 8a0ceaf801 | |
Daniel Supernault | 491468612f | |
Daniel Supernault | e32e50da7b | |
Daniel Supernault | 3fbf8f159e | |
Daniel Supernault | 279fb28e2a | |
Daniel Supernault | c89dc45e8d | |
Daniel Supernault | a7f96d8194 | |
Daniel Supernault | e7c08fbbb2 | |
Daniel Supernault | 7016d19520 | |
Daniel Supernault | 60e053c936 | |
Daniel Supernault | d3f032b2ec | |
Daniel Supernault | e5d789e0ab | |
Daniel Supernault | 28da107f66 | |
Daniel Supernault | 63c9ebe81f | |
Daniel Supernault | 28da44beec | |
Daniel Supernault | cef451e588 | |
Daniel Supernault | 2438324369 | |
Daniel Supernault | 2136ffe3d8 | |
Daniel Supernault | 5cea5aab3c | |
daniel | c2a535bfa1 | |
Daniel Supernault | f22a36fe30 | |
Daniel Supernault | b641954549 | |
Daniel Supernault | ebbd98e743 | |
daniel | 5e6658de25 | |
Daniel Supernault | 85839b220a | |
daniel | 11eef54b0c | |
Daniel Supernault | ff92015c87 | |
daniel | a5a2f77871 | |
Daniel Supernault | 8d98e3dc97 | |
Daniel Supernault | 759a439334 | |
daniel | 1c40762921 | |
Daniel Supernault | 6dceb6f05b | |
Daniel Supernault | 33dbbe467d | |
daniel | 7ac1e398b8 | |
Daniel Supernault | 041c01359b | |
daniel | d66cf5d028 | |
Daniel Supernault | 38fee418a9 | |
daniel | abcaa19ff1 | |
Daniel Supernault | ed5e956a54 | |
Daniel Supernault | 9c43e7e265 | |
Daniel Supernault | 822e9888bb | |
Daniel Supernault | 0a0681199f | |
Daniel Supernault | 4c3823b0c4 | |
Daniel Supernault | 4c95306f12 | |
Daniel Supernault | 9818656425 | |
daniel | f01f4bf23e | |
Daniel Supernault | 93a6f1e224 | |
Daniel Supernault | 957bbbc2bd | |
Daniel Supernault | 06bee36c52 | |
daniel | 4bb97e1547 | |
Daniel Supernault | d1c297d1ad | |
daniel | 128415dbf8 | |
Daniel Supernault | 7f462a8055 | |
Daniel Supernault | d848792ad4 | |
daniel | a4030faa9d | |
Daniel Supernault | 4cc66a838d | |
Daniel Supernault | a0157fce0c | |
daniel | b74e813cba | |
Daniel Supernault | dec061f5ae | |
daniel | f9badbf4dd | |
Daniel Supernault | 3204fb9669 | |
daniel | 6ffc964371 | |
daniel | baa653d7de | |
daniel | cdd153d385 | |
daniel | c2ce63ecd3 | |
daniel | d83df5cd64 | |
Daniel Supernault | 4a1363b929 | |
daniel | 7a6ef5fcbc | |
Daniel Supernault | fadb4d6ea4 | |
Daniel Supernault | 8548294c7a | |
Daniel Supernault | fe9b4c5a37 | |
mbliznikova | 7cb075dbf9 | |
mbliznikova | a7320535e9 | |
Daniel Supernault | 1ef885c1a1 | |
daniel | 66dc955d11 | |
Daniel Supernault | b0e8810a91 | |
Daniel Supernault | bcb88d5b0a | |
daniel | 6eb256860c | |
Daniel Supernault | e5e3be0598 | |
daniel | 54b6c96112 | |
Daniel Supernault | d62a60a4ee | |
Daniel Supernault | 176b4ed793 | |
Daniel Supernault | aa166ab11a | |
Daniel Supernault | 287f903bf3 | |
Daniel Supernault | 175203089b | |
daniel | 47b0354e16 | |
Daniel Supernault | 051eb962e1 | |
Daniel Supernault | 15f29f7d79 | |
daniel | 0899f909d8 | |
Daniel Supernault | 4aca04729b | |
Daniel Supernault | 1e31fee6a6 | |
daniel | 7f6d64b517 | |
Daniel Supernault | e6d3c7f4d7 | |
Daniel Supernault | f105f4e8f6 | |
Daniel Supernault | e5401f8558 | |
daniel | dfbc453b03 | |
Daniel Supernault | 33a60e767d | |
daniel | 23dea20024 | |
Daniel Supernault | e1b39bcf6f | |
Daniel Supernault | 3327a008fa | |
daniel | 8b8fb5f6c0 | |
Daniel Supernault | 3e96fa8a56 | |
daniel | 0d48cf1c2e | |
Daniel Supernault | 19233cc976 | |
Daniel Supernault | c6a6b3ae30 | |
daniel | 57584391a4 | |
Daniel Supernault | b365aa7e06 | |
daniel | 40651c036a | |
Daniel Supernault | c8092116e5 | |
daniel | c5cb2c0a1c | |
Daniel Supernault | 84f4e88573 | |
daniel | f203d0540f | |
Daniel Supernault | d8fbb4ff32 | |
daniel | 4071a4687d | |
Daniel Supernault | cf50618696 | |
Daniel Supernault | a5204f3e67 | |
Daniel Supernault | b2c9cc2318 | |
daniel | cac7c6bf3c | |
Daniel Supernault | dde858bd5f | |
Daniel Supernault | 015b1b80b4 | |
daniel | 2e2a200659 | |
Daniel Supernault | 446ca3a878 | |
daniel | 0a2a3b996d | |
Daniel Supernault | 06bf0c14bf | |
Daniel Supernault | 0e43127197 | |
daniel | e8439358cb | |
Daniel Supernault | 05d646c034 | |
Daniel Supernault | c39b9afbfd | |
Daniel Supernault | 386e64d5e8 | |
Daniel Supernault | 125208fb9e | |
Daniel Supernault | e917341651 | |
Daniel Supernault | 7deaaed4dd | |
Daniel Supernault | 43443503a1 | |
Daniel Supernault | 115a9d2dec | |
Daniel Supernault | 73cb8b43b3 | |
Daniel Supernault | 24c370ee22 | |
Daniel Supernault | 2a8a299058 | |
Daniel Supernault | ce63c4997b | |
Daniel Supernault | de2b5ba4e9 | |
Daniel Supernault | df1f98d5f7 | |
Daniel Supernault | 20a560bfd1 | |
Daniel Supernault | 0fce5de6cd | |
Daniel Supernault | c806bbce3f | |
Daniel Supernault | 6aa65b9a21 | |
Daniel Supernault | 1cd96ced2a | |
Daniel Supernault | 9dfc377322 | |
Daniel Supernault | 448c061070 | |
Daniel Supernault | 1f35da0d4b | |
Daniel Supernault | ce54d29c69 | |
mbliznikova | 2c6edf37a7 | |
mbliznikova | 170f877c26 | |
daniel | d20efd2c61 | |
Daniel Supernault | d24c60576f | |
Daniel Supernault | 21218c794b | |
mbliznikova | 439c8fc0ea | |
daniel | 1bdd0b3609 | |
Daniel Supernault | ff272292ef | |
Daniel Supernault | ddc217147c | |
daniel | c0575ae3bf | |
Daniel Supernault | d84c84c1e2 | |
Daniel Supernault | 5b3a56102f | |
mbliznikova | 3425821b55 | |
daniel | aaa0c7f76c | |
Daniel Supernault | c7b304ef20 | |
daniel | 091fa1a62b | |
Daniel Supernault | a3fd0b032b | |
daniel | e45ede5a12 | |
Daniel Supernault | 960594f90d | |
Daniel Supernault | c09a7d1127 | |
Daniel Supernault | 9c24157ab3 | |
Daniel Supernault | 5a2d7e3eca | |
daniel | e6301bfa51 | |
mbliznikova | 3269481148 | |
paule | 950baef58b | |
daniel | 4b9d0dc6ef | |
Daniel Supernault | da510089e2 | |
mbliznikova | 770409c4a4 | |
daniel | 7960ab9222 | |
Daniel Supernault | eb291efe00 | |
Daniel Supernault | 4c6a0719ca | |
Daniel Supernault | 1686fc68e8 | |
Daniel Supernault | 7cd9fa6e5b | |
daniel | 12fc8fd0c0 | |
Daniel Supernault | 3249695066 | |
daniel | e0208a7dd9 | |
Daniel Supernault | 432acb491a | |
daniel | 81db60df32 | |
Daniel Supernault | 1f82d47ce5 | |
daniel | 4bac21d5d5 | |
Daniel Supernault | 28a808031b | |
Daniel Supernault | b58ed0ad01 | |
mbliznikova | a8f78aa2ab | |
daniel | 2b17cc2c0d | |
daniel | 1be012e439 | |
daniel | 381e23e172 | |
daniel | d85c0c3d0a | |
mbliznikova | e3de4c3e68 | |
daniel | 42fb713092 | |
Daniel Supernault | 31fafb1b68 | |
Daniel Supernault | 7edfea0951 | |
Daniel Supernault | 6ab7e37a48 | |
Daniel Supernault | 0405ef1248 | |
Daniel Supernault | c63707b3ec | |
Daniel Supernault | f11ce7009f | |
Daniel Supernault | e3f8cfb49e | |
Daniel Supernault | 5c358010b0 | |
Daniel Supernault | 6cf4363c50 | |
Daniel Supernault | f0ba2dfc69 | |
Daniel Supernault | 3f292459ff | |
Daniel Supernault | f9bbb05575 | |
Daniel Supernault | fac7c3c5e7 | |
Daniel Supernault | 4e3e23db36 | |
Daniel Supernault | a144301085 | |
Daniel Supernault | 4cd53247a6 | |
Daniel Supernault | 82fc36b2b3 | |
Daniel Supernault | 00823545a5 | |
daniel | b4a918ef42 | |
Daniel Supernault | 56e315f69f | |
Happyfeet01 | 2a0ef7620d | |
mbliznikova | b838f90b77 | |
mbliznikova | fdb51d1f5a | |
daniel | 352786144b | |
Daniel Supernault | 65a048cdd5 | |
daniel | 7cbdac7adb | |
Daniel Supernault | dfe2379b93 | |
daniel | 9f968d134e | |
Daniel Supernault | 9677791bef | |
Daniel Supernault | 778e83d398 | |
daniel | a1b280ec33 | |
Daniel Supernault | 36df0d8373 | |
Andy Neillans | e9d9c4d8cc | |
daniel | 2fa595d2cf | |
Daniel Supernault | b76ad7cfe0 | |
daniel | c906dbb26b | |
Daniel Supernault | 0d35f1a3e5 | |
daniel | 627be42b36 | |
Daniel Supernault | f481f3d248 | |
Daniel Supernault | edbcf3ed79 | |
daniel | 8f4f64d737 | |
Daniel Supernault | 4e35f0d32e | |
Daniel Supernault | c37b7cde30 | |
Daniel Supernault | 319ced4054 | |
Daniel Supernault | 95a1eddcb2 | |
Daniel Supernault | 82798b5ea3 | |
daniel | ffa44c4fad | |
Daniel Supernault | 36b23fe34e | |
daniel | 6f314fa0d2 | |
Daniel Supernault | 01bac51104 | |
daniel | dadb6ab416 | |
Daniel Supernault | 7bfe43095b | |
Daniel Supernault | c6408fd79d | |
daniel | a276b2ce70 | |
Daniel Supernault | 457d5454f8 | |
daniel | f38226c527 | |
Daniel Supernault | e4d3b19642 | |
daniel | d679ae4f11 | |
Daniel Supernault | 135798eb68 | |
mbliznikova | 6c1e56fcb2 | |
daniel | ed20344c3e | |
Daniel Supernault | ede5ec3bf4 | |
Vivianne Langdon | 4508697563 | |
daniel | eb517aa8bf | |
Daniel Supernault | a1e162f095 | |
Daniel Supernault | 895dc4fa9e | |
daniel | 705f30b865 | |
Daniel Supernault | fcb4933369 | |
Daniel Supernault | 8c96919119 | |
Daniel Supernault | ce1afe2711 | |
daniel | d5baf2627a | |
Daniel Supernault | 79b378cdb1 | |
daniel | dcc6f65e33 | |
Daniel Supernault | 9989d6c66f | |
daniel | 7fdb87ef9b | |
Daniel Supernault | bf5b72f082 | |
Daniel Supernault | d295e6059b | |
daniel | b2195ca837 | |
Daniel Supernault | 1f0a45b7f4 | |
daniel | 7b5999496e | |
Daniel Supernault | 2d428f43e8 | |
Daniel Supernault | d969a97360 | |
daniel | 8a89570b4a | |
Daniel Supernault | 439638f7d7 | |
Daniel Supernault | fb1deb6e28 | |
daniel | b91d263237 | |
Daniel Supernault | dcdfb28dcd | |
Daniel Supernault | dc23c21db0 | |
daniel | 5a9a159708 | |
Daniel Supernault | 0210f8aa2a | |
daniel | 19015f18b0 | |
Daniel Supernault | 61d235b797 | |
daniel | 4112ab5f83 | |
Daniel Supernault | 5ab7f9958c | |
Daniel Supernault | 223661ecb2 | |
Daniel Supernault | 2496386d9b | |
daniel | 155e1704ff | |
Daniel Supernault | 33ed7a8c91 | |
Daniel Supernault | a510c3e89c | |
daniel | e36d7da841 | |
Daniel Supernault | 3979e33b57 | |
Daniel Supernault | 8fa2afe016 | |
Daniel Supernault | 941736ce6c | |
daniel | c4843e823e | |
daniel | 6ec4077549 | |
Emelia Smith | 74ad26fee6 | |
David Gabriel | 2e5c141724 | |
David Gabriel | 480394f3d8 | |
daniel | e286f98762 | |
Daniel Supernault | 83900a3b00 | |
daniel | 5dc397ec1f | |
Daniel Supernault | 817b494703 | |
daniel | 24db7d71cf | |
Daniel Supernault | 23bc985b36 | |
Daniel Supernault | fbdcdd9dbc | |
Daniel Supernault | fc24630eba | |
daniel | 28bca423d7 | |
Daniel Supernault | a3696dac95 | |
Daniel Supernault | 61a6d90403 | |
Daniel Supernault | 93c7ad9779 | |
Daniel Supernault | 347e4f59a3 | |
Daniel Supernault | a04ba18113 | |
daniel | 3cb50af8a3 | |
Daniel Supernault | a7e4305043 | |
Daniel Supernault | ca746717cb | |
Happyfeet01 | 1ea65db70d | |
Happyfeet01 | a6a0333170 | |
daniel | b6c3ac4b13 | |
Daniel Supernault | 526807f01c | |
Daniel Supernault | 781d3c0ec3 | |
Daniel Supernault | 47e5c07061 | |
Daniel Supernault | dc7973de62 | |
Daniel Supernault | 9378c65396 | |
Daniel Supernault | eab16e7fd8 | |
Daniel Supernault | 3103af2fe4 | |
Daniel Supernault | 3c60362648 | |
Daniel Supernault | a9220e4e01 | |
daniel | 211a9057fc | |
Daniel Supernault | 618b67271a | |
daniel | 268804856b | |
Daniel Supernault | fab8f25e9b | |
Daniel Supernault | 4f19a58b2c | |
Daniel Supernault | 6161cf45aa | |
Daniel Supernault | 71e92261f4 | |
Daniel Supernault | 45be6e10b8 | |
Daniel Supernault | ed87ddb923 | |
Daniel Supernault | 3d1b6516fe | |
daniel | 2a63ff1d40 | |
Daniel Supernault | f2dfe12ac3 | |
Daniel Supernault | 3e90f6cee5 | |
Daniel Supernault | 911504fa54 | |
daniel | 5cdf076527 | |
Daniel Supernault | c527858ac4 | |
Daniel Supernault | 75bfd21104 | |
Daniel Supernault | e2705b9ae9 | |
Daniel Supernault | dccec7d5a9 | |
daniel | 2cdf8917da | |
Daniel Supernault | 74a6b169d3 | |
Daniel Supernault | f4d46d8148 | |
Daniel Supernault | 29de91e5d0 | |
Daniel Supernault | ec2a1ed99c | |
Daniel Supernault | 5a19daabce | |
Daniel Supernault | 13bdaa2ed4 | |
daniel | e559187411 | |
Daniel Supernault | 685d45a8df | |
Daniel Supernault | b86d47bfec | |
Daniel Supernault | 8efb4047b1 | |
Daniel Supernault | 3b885709b8 | |
Daniel Supernault | f54cf0b2d7 | |
Daniel Supernault | 0eca48f1a4 | |
Daniel Supernault | 1c13b518be | |
Daniel Supernault | c469d47552 | |
daniel | acadd1b473 | |
Daniel Supernault | 1c105a6ce3 | |
daniel | 93b2dbab17 | |
Daniel Supernault | 6c36995083 | |
Daniel Supernault | 59b643789f | |
daniel | 8e7963c0c5 | |
Daniel Supernault | ecc697a241 | |
Daniel Supernault | ff58f9707f | |
daniel | 1a3176c996 | |
Daniel Supernault | 3f22640644 | |
Daniel Supernault | 95fb893f95 | |
Daniel Supernault | acabf603f0 | |
daniel | 8911ace102 | |
Daniel Supernault | 780e78f21a | |
Daniel Supernault | a3dd7c95df | |
Daniel Supernault | 852dbd8d34 | |
Daniel Supernault | 9cfa89dab4 | |
Daniel Supernault | 0b90d629d5 | |
Daniel Supernault | 45b9404ec1 | |
daniel | 45090a0e76 | |
Daniel Supernault | ce02f05718 | |
Daniel Supernault | 0d802c313b | |
Daniel Supernault | 7a431af93a | |
Daniel Supernault | ff2c16fe74 | |
Daniel Supernault | 3590adbd87 | |
daniel | 1a30e488f9 | |
Daniel Supernault | 4b611be2d9 | |
daniel | ef58c3b304 | |
Daniel Supernault | c07233a1c1 | |
Daniel Supernault | e0b48b2976 | |
Daniel Supernault | 2bef3e415d | |
daniel | 57582b1b2e | |
Daniel Supernault | a00a520bf3 | |
Daniel Supernault | 84669ac614 | |
Daniel Supernault | ba7551d8a9 | |
Daniel Supernault | 0b5157675f | |
Daniel Supernault | c61d0b915f | |
Daniel Supernault | 0704c7e05e | |
Daniel Supernault | 9fa6b3f7aa | |
Daniel Supernault | 4b2c66f557 | |
Daniel Supernault | 1cc6274ac0 | |
Daniel Supernault | 9233cd8f5b | |
daniel | cc561c0522 | |
Daniel Supernault | 947847898a | |
Daniel Supernault | 37fd03428a | |
Daniel Supernault | c244d8b5c8 | |
daniel | 1809cb217c | |
Daniel Supernault | 4276d3f248 | |
Daniel Supernault | 73b35d3231 | |
Daniel Supernault | 625a76a51d | |
Daniel Supernault | 2d959fb354 | |
daniel | 924ce5eea9 | |
Daniel Supernault | 43b101dbfa | |
Daniel Supernault | 72844c0715 | |
Daniel Supernault | ea54413e95 | |
daniel | f041f5f60a | |
Daniel Supernault | 60747bfb15 | |
Daniel Supernault | 04f4f8baf1 | |
daniel | da90bf630a | |
Daniel Supernault | 2f2e446c1f | |
Daniel Supernault | fe6123c820 | |
Daniel Supernault | 6fd53a3001 | |
Daniel Supernault | 10dd348c28 | |
Daniel Supernault | d6d60a8574 | |
Daniel Supernault | afe6948da8 | |
Daniel Supernault | b47e8f8e3e | |
Daniel Supernault | 892907d5d1 | |
Daniel Supernault | b18f3fba8b | |
daniel | 48cd829572 | |
Daniel Supernault | c64c4aa1cb | |
Daniel Supernault | 63a7879c29 | |
Daniel Supernault | b89c4f1cdc | |
daniel | 4412a6b5dd | |
Daniel Supernault | 763ce19a0a | |
Daniel Supernault | 71ad7d5d43 | |
Daniel Supernault | 4f850e54ad | |
daniel | 8606040858 | |
Daniel Supernault | 52f9999fcc | |
Daniel Supernault | 5c5541fc01 | |
Daniel Supernault | a6d10f0389 | |
Daniel Supernault | 89c3710d3c | |
daniel | 2dcfd68f01 | |
Daniel Supernault | ba58aaba36 | |
daniel | 29554d20b7 | |
Daniel Supernault | c394fb76c6 | |
daniel | d76ae33eb9 | |
Daniel Supernault | 6d0c5994da | |
Daniel Supernault | 477986abdd | |
Daniel Supernault | aabc20dd2e | |
Daniel Supernault | cf3078c569 | |
Daniel Supernault | 7de67650c7 | |
Daniel Supernault | 5d8599a497 | |
Daniel Supernault | 828f369373 | |
Daniel Supernault | 9f3e809f26 | |
Daniel Supernault | 7e0335b246 | |
Daniel Supernault | 49e5703198 | |
Daniel Supernault | b64af89d40 | |
Daniel Supernault | 7dd45c23b7 | |
Daniel Supernault | 8c9f4da48a | |
Daniel Supernault | c50e0966db | |
daniel | 1dd9617da2 | |
Daniel Supernault | 7217c962bf | |
Daniel Supernault | 1f6d11736a | |
Daniel Supernault | cbf086ccb4 | |
Daniel Supernault | adf015be43 | |
Daniel Supernault | d48e8d9832 | |
Daniel Supernault | 67bf3d10e0 | |
Daniel Supernault | 36f5f2e8b4 | |
Daniel Supernault | bffd8f0771 | |
Daniel Supernault | 9817025578 | |
Daniel Supernault | f5dbc8281a | |
Daniel Supernault | 5361082026 | |
Daniel Supernault | fff692a25c | |
Daniel Supernault | 751f30d845 | |
Daniel Supernault | add6cf0fe1 | |
Daniel Supernault | b12b956b7d | |
Daniel Supernault | b447db082f | |
Daniel Supernault | bb97b55c66 | |
Daniel Supernault | 495b78afba | |
Daniel Supernault | 163cf5b3c9 | |
Daniel Supernault | ab2a11341c | |
daniel | 398a42b383 | |
Daniel Supernault | 7cd59f75e4 | |
daniel | 7736324ee7 | |
Daniel Supernault | 8be4582fc4 | |
Daniel Supernault | 36b6bf480e | |
daniel | 341a032949 | |
Daniel Supernault | 2cbf1acec3 | |
daniel | 61346b104c | |
Daniel Supernault | 0f72b33c0e | |
Daniel Supernault | e3be04db2b | |
Daniel Supernault | e8d4ce1888 | |
Daniel Supernault | d6374cfe70 | |
Daniel Supernault | ab9a8ba314 | |
Daniel Supernault | 5bea903409 | |
Daniel Supernault | 1bf3ad7ed9 | |
Daniel Supernault | 31afaba3d0 | |
Daniel Supernault | 0eb51ed74d | |
Daniel Supernault | 29961c4a80 | |
daniel | 2e79a89706 | |
Daniel Supernault | 8cc91babd7 | |
Daniel Supernault | a8453e7719 | |
daniel | 498d7fec80 | |
Daniel Supernault | b49310a2d3 | |
Daniel Supernault | fbdc635829 | |
daniel | 4c0c29ee37 | |
Daniel Supernault | fccd927c0b | |
Daniel Supernault | fe8728c0ba | |
daniel | 97ce98b234 | |
Daniel Supernault | eaff1a7607 | |
Daniel Supernault | 6197a7b1e1 | |
daniel | ad88a07118 | |
Daniel Supernault | 69a53d0fa1 | |
Daniel Supernault | b495918b3c | |
Daniel Supernault | 3df9b53f4e | |
daniel | 70e82038a7 | |
Daniel Supernault | 9c0d0a31f2 | |
Daniel Supernault | 884e2f196f | |
Daniel Supernault | a3303b072b | |
daniel | caefbc9777 | |
Daniel Supernault | cd08348f4c | |
Daniel Supernault | a2305d5fdc | |
daniel | ce960266f9 | |
Daniel Supernault | 5154869c8c | |
Daniel Supernault | 992d910b9c | |
daniel | 696804d62e | |
Daniel Supernault | d4f92da0e5 | |
Daniel Supernault | ac9fb5f661 | |
Daniel Supernault | 1430f5328f | |
daniel | 392ee2e290 | |
Daniel Supernault | 8b06af3f6c | |
daniel | ab0d87e07e | |
Daniel Supernault | a456fa63c9 | |
daniel | 718359941b | |
Daniel Supernault | 100a4771f8 | |
Daniel Supernault | ee3b6e09b0 | |
daniel | e46ee70500 | |
Daniel Supernault | fe05d79b9f | |
Daniel Supernault | 4479055e1e | |
daniel | 5cfe8cd56a | |
Daniel Supernault | 57d4f67b85 | |
Daniel Supernault | 1f2183ee67 | |
daniel | 483010e0b0 | |
Daniel Supernault | 52d076a6bd | |
Daniel Supernault | 109d041908 | |
daniel | b7ae41d4a8 | |
Daniel Supernault | 53e0df4169 | |
Daniel Supernault | fa6df8fbd8 | |
Daniel Supernault | 0f803446dd | |
daniel | b15d939163 | |
Daniel Supernault | 58523b6d98 | |
Daniel Supernault | c0190d8436 | |
daniel | ae38f2d8a6 | |
Daniel Supernault | 18c744c458 | |
Daniel Supernault | a6f96b4bb3 | |
Daniel Supernault | d4e17f75b4 | |
Daniel Supernault | f57d22ee66 | |
Daniel Supernault | 2a10809fa3 | |
Daniel Supernault | 6bf3142e8b | |
Daniel Supernault | 8b3d1eeb5e | |
Daniel Supernault | 559978db46 | |
Daniel Supernault | 859ed621b0 | |
Daniel Supernault | b0634bfd8f | |
Daniel Supernault | aad4259a47 | |
Daniel Supernault | 734136a7e7 | |
Daniel Supernault | ced085bc1d | |
Daniel Supernault | 98cf8f32a0 | |
daniel | 53013dc32d | |
Daniel Supernault | 013656000c | |
daniel | f4e7ca6b0e | |
Daniel Supernault | fb77ee764b | |
Daniel Supernault | f32eabdf19 | |
daniel | c47de37c16 | |
Daniel Supernault | 86e20e927d | |
Daniel Supernault | 4d8b4dcf35 | |
daniel | 8068f1eb0a | |
Daniel Supernault | 88f7dc26bb | |
Daniel Supernault | c8eff6dff0 | |
Daniel Supernault | 808e89328c | |
daniel | dba21ef92f | |
daniel | 1b1378ac64 | |
daniel | a73541ab95 | |
Daniel Supernault | 566787c2b7 | |
Daniel Supernault | 221ddce0fa | |
daniel | 9d763bded1 | |
Daniel Supernault | f9eb99c897 | |
Daniel Supernault | c071c7195e | |
daniel | 9a8a840b4e | |
Daniel Supernault | 39a42c637f | |
Daniel Supernault | 2800c8886a | |
daniel | 24afe169ad | |
Daniel Supernault | 361d5f0374 | |
Daniel Supernault | 9f901d65c9 | |
daniel | ce8470562f | |
Daniel Supernault | cbe9458244 | |
Daniel Supernault | c54cdd3eb4 | |
daniel | 01c0902b3d | |
Daniel Supernault | f452509e17 | |
Daniel Supernault | 4973cb4611 | |
daniel | 79642bea7e | |
Daniel Supernault | a1ab88a8ca | |
Daniel Supernault | adb070f178 | |
daniel | c96f0008e1 | |
Daniel Supernault | 0abc5723bc | |
Daniel Supernault | b8426ccea7 | |
daniel | 7b5949f55d | |
Daniel Supernault | 12ea6f1950 | |
Daniel Supernault | c6ffda9618 | |
daniel | 9279771e94 | |
Daniel Supernault | 79bcaadd49 | |
Daniel Supernault | 607c64ae72 | |
Daniel Supernault | f42c114058 | |
daniel | a36d2b1d2b | |
Daniel Supernault | 2b6cf7f8de | |
Daniel Supernault | 3662d3defe | |
daniel | eaf94cab0c | |
Daniel Supernault | 6622a851bb | |
Daniel Supernault | dd2f5bb96a | |
Daniel Supernault | c167af43a4 | |
Daniel Supernault | 63b72c429c | |
daniel | c8a34a4dbd | |
Daniel Supernault | ae0d5d2d40 | |
daniel | 92d2533068 | |
Daniel Supernault | 46ccbf2bfd | |
Daniel Supernault | 18cddd43f3 | |
daniel | 6db781aa91 | |
Daniel Supernault | a11e1ee3f8 | |
Daniel Supernault | 5abc2445a7 | |
daniel | 3fd334da0e | |
Daniel Supernault | 053b30bca0 | |
Daniel Supernault | d1880ee6b8 | |
Daniel Supernault | 132a58de54 | |
Daniel Supernault | 75db5116b7 | |
daniel | acde00283c | |
Daniel Supernault | 127343ccda | |
Daniel Supernault | ad25ed6792 | |
daniel | dd103fd600 | |
Daniel Supernault | af35fe93ac | |
Daniel Supernault | 6a2daf1f63 | |
daniel | e3bf01c5a2 | |
Daniel Supernault | 764315666e | |
daniel | cac9041e3d | |
Daniel Supernault | 8b007f9ee9 | |
Daniel Supernault | 56ec083db5 | |
Anil Kulkarni | 91660a2477 | |
daniel | dd7cb8b597 | |
Daniel Supernault | 83bc84dd97 | |
daniel | 5a8e0ab5df | |
Daniel Supernault | 95f118b2dc | |
Daniel Supernault | a91a5e4872 | |
daniel | c643dc307b | |
Daniel Supernault | 4f23c250f4 | |
Daniel Supernault | a1bd044c0c | |
Daniel Supernault | 8a7e5f7b2b | |
Daniel Supernault | 8153e7a746 | |
Daniel Supernault | 588ca653a8 | |
Daniel Supernault | d5f63f8a71 | |
Daniel Supernault | 0d3b4bc225 | |
Daniel Supernault | ea943333a5 | |
Daniel Supernault | 58ec49fd57 | |
Daniel Supernault | 026842dd93 | |
Daniel Supernault | 3aad75abcf | |
Daniel Supernault | 6cdb5bc672 | |
daniel | 1dd962eaa6 | |
Daniel Supernault | eb5bb9fede | |
Daniel Supernault | 167dbcdd43 | |
Anil Kulkarni | b76ea33890 | |
daniel | 03e7e24b6b | |
Daniel Supernault | e559bdbf57 | |
Daniel Supernault | bd30e671cc | |
Daniel Supernault | 0b42fe0f00 | |
Daniel Supernault | b5fe956acf | |
Daniel Supernault | ed35214161 | |
Daniel Supernault | 1a83c5858d | |
Daniel Supernault | 521b3b4c82 | |
Daniel Supernault | b4ad6668e9 | |
Daniel Supernault | 4d997bb959 | |
Daniel Supernault | 175a848665 | |
Daniel Supernault | fc1a385cfd | |
Daniel Supernault | 43d3aa2b94 | |
Daniel Supernault | 91ba139808 | |
Daniel Supernault | df444851b5 | |
Daniel Supernault | 6bc20a37ed | |
Daniel Supernault | f48daab37e | |
daniel | c996f37366 | |
Daniel Supernault | db2da84bec | |
Daniel Supernault | a0e299c28d | |
Daniel Supernault | 6a2e9e8f7d | |
Daniel Supernault | add5eaf094 | |
Daniel Supernault | 637cdca27a | |
Daniel Supernault | eda7607fe8 | |
Daniel Supernault | 9b054d2bae | |
Daniel Supernault | ccbba56633 | |
Daniel Supernault | 6ea2bdc782 | |
Daniel Supernault | 970f77b078 | |
daniel | 4a97e8003f | |
Daniel Supernault | 4b309d1acf | |
daniel | 56f64ad5f3 | |
Daniel Supernault | 51768083fe | |
Daniel Supernault | 37bd2ee51b | |
daniel | e4db35c6dd | |
Daniel Supernault | a96a3cfc31 | |
Daniel Supernault | e297efcae3 | |
Daniel Supernault | 633351f6dc | |
daniel | 2b5437a3b9 | |
Daniel Supernault | 73429ac975 | |
Daniel Supernault | 6bc3bcbef5 | |
daniel | 7e2bad3df6 | |
Daniel Supernault | e6250db0de | |
Daniel Supernault | 11552d1273 | |
Daniel Supernault | d15bd60d87 | |
Daniel Supernault | 666e5732a5 | |
daniel | bc7d431369 | |
Daniel Supernault | d34f078887 | |
Daniel Supernault | fce14f6568 | |
Daniel Supernault | 8ef900bf8e | |
Daniel Supernault | dac0d08319 | |
Shlee | 031290d987 | |
Shlee | 312bc06685 | |
Nils van Lück | c96bcd559d | |
Nils van Lück | af28aecf21 |
|
@ -21,7 +21,12 @@ jobs:
|
|||
steps:
|
||||
- checkout
|
||||
|
||||
- run: sudo apt update && sudo apt install zlib1g-dev libsqlite3-dev
|
||||
- run:
|
||||
name: "Create Environment file and generate app key"
|
||||
command: |
|
||||
mv .env.testing .env
|
||||
|
||||
- run: sudo apt install zlib1g-dev libsqlite3-dev
|
||||
|
||||
# Download and cache dependencies
|
||||
|
||||
|
@ -36,18 +41,17 @@ jobs:
|
|||
- run: composer install -n --prefer-dist
|
||||
|
||||
- save_cache:
|
||||
key: composer-v2-{{ checksum "composer.lock" }}
|
||||
key: v2-dependencies-{{ checksum "composer.json" }}
|
||||
paths:
|
||||
- vendor
|
||||
|
||||
- run: cp .env.testing .env
|
||||
- run: php artisan config:cache
|
||||
- run: php artisan route:clear
|
||||
- run: php artisan storage:link
|
||||
- run: php artisan key:generate
|
||||
- run: php artisan config:clear
|
||||
|
||||
# run tests with phpunit or codecept
|
||||
- run: ./vendor/bin/phpunit
|
||||
- run: php artisan test
|
||||
- store_test_results:
|
||||
path: tests/_output
|
||||
- store_artifacts:
|
||||
|
|
|
@ -4,4 +4,4 @@
|
|||
## Usage: redis-cli [flags] [args]
|
||||
## Example: "redis-cli KEYS *" or "ddev redis-cli INFO" or "ddev redis-cli --version"
|
||||
|
||||
redis-cli -p 6379 -h redis $@
|
||||
exec redis-cli -p 6379 -h redis "$@"
|
||||
|
|
|
@ -1,8 +1,30 @@
|
|||
data
|
||||
Dockerfile
|
||||
contrib/docker/Dockerfile.*
|
||||
docker-compose*.yml
|
||||
.dockerignore
|
||||
.git
|
||||
.gitignore
|
||||
.env
|
||||
.DS_Store
|
||||
/.bash_history
|
||||
/.bash_profile
|
||||
/.bashrc
|
||||
/.composer
|
||||
/.env
|
||||
/.env.dottie-backup
|
||||
/.git
|
||||
/.git-credentials
|
||||
/.gitconfig
|
||||
/.gitignore
|
||||
/.idea
|
||||
/.vagrant
|
||||
/bootstrap/cache
|
||||
/docker-compose-state/
|
||||
/Homestead.json
|
||||
/Homestead.yaml
|
||||
/node_modules
|
||||
/npm-debug.log
|
||||
/public/hot
|
||||
/public/storage
|
||||
/public/vendor/horizon
|
||||
/storage/*.key
|
||||
/storage/docker
|
||||
/vendor
|
||||
/yarn-error.log
|
||||
|
||||
# Exceptions - these *MUST* be last
|
||||
!/bootstrap/cache/.gitignore
|
||||
!/public/vendor/horizon/.gitignore
|
||||
|
|
|
@ -1,9 +1,27 @@
|
|||
root = true
|
||||
|
||||
[*]
|
||||
indent_style = space
|
||||
indent_size = 4
|
||||
indent_style = tab
|
||||
end_of_line = lf
|
||||
charset = utf-8
|
||||
trim_trailing_whitespace = true
|
||||
insert_final_newline = true
|
||||
|
||||
[*.{yml,yaml}]
|
||||
indent_style = space
|
||||
indent_size = 2
|
||||
|
||||
[*.{sh,envsh,env,env*}]
|
||||
indent_style = space
|
||||
indent_size = 4
|
||||
|
||||
# ShellCheck config
|
||||
shell_variant = bash # like -ln=bash
|
||||
binary_next_line = true # like -bn
|
||||
switch_case_indent = true # like -ci
|
||||
space_redirects = false # like -sr
|
||||
keep_padding = false # like -kp
|
||||
function_next_line = true # like -fn
|
||||
never_split = true # like -ns
|
||||
simplify = true
|
||||
|
|
1392
.env.docker
1392
.env.docker
Plik diff jest za duży
Load Diff
|
@ -8,6 +8,7 @@ OPEN_REGISTRATION="false"
|
|||
ENFORCE_EMAIL_VERIFICATION="false"
|
||||
PF_MAX_USERS="1000"
|
||||
OAUTH_ENABLED="true"
|
||||
ENABLE_CONFIG_CACHE=true
|
||||
|
||||
# Media Configuration
|
||||
PF_OPTIMIZE_IMAGES="true"
|
||||
|
|
|
@ -1,3 +1,5 @@
|
|||
# shellcheck disable=SC2034,SC2148
|
||||
|
||||
APP_NAME="Pixelfed Test"
|
||||
APP_ENV=local
|
||||
APP_KEY=base64:lwX95GbNWX3XsucdMe0XwtOKECta3h/B+p9NbH2jd0E=
|
||||
|
@ -62,6 +64,8 @@ CS_BLOCKED_DOMAINS='example.org,example.net,example.com'
|
|||
CS_CW_DOMAINS='example.org,example.net,example.com'
|
||||
CS_UNLISTED_DOMAINS='example.org,example.net,example.com'
|
||||
|
||||
## Optional
|
||||
## Optional
|
||||
#HORIZON_DARKMODE=false # Horizon theme darkmode
|
||||
#HORIZON_EMBED=false # Single Docker Container mode
|
||||
#HORIZON_EMBED=false # Single Docker Container mode
|
||||
|
||||
ENABLE_CONFIG_CACHE=false
|
||||
|
|
|
@ -3,3 +3,10 @@
|
|||
*.scss linguist-vendored
|
||||
*.js linguist-vendored
|
||||
CHANGELOG.md export-ignore
|
||||
|
||||
# Collapse diffs for generated files:
|
||||
public/**/*.js text -diff
|
||||
public/**/*.json text -diff
|
||||
public/**/*.css text -diff
|
||||
public/img/* binary -diff
|
||||
public/fonts/* binary -diff
|
||||
|
|
|
@ -1,125 +0,0 @@
|
|||
---
|
||||
name: Build Docker image
|
||||
|
||||
on:
|
||||
workflow_dispatch:
|
||||
push:
|
||||
branches:
|
||||
- dev
|
||||
tags:
|
||||
- '*'
|
||||
pull_request:
|
||||
paths:
|
||||
- .github/workflows/build-docker.yml
|
||||
- contrib/docker/Dockerfile.apache
|
||||
- contrib/docker/Dockerfile.fpm
|
||||
permissions:
|
||||
contents: read
|
||||
|
||||
jobs:
|
||||
build-docker-apache:
|
||||
runs-on: ubuntu-latest
|
||||
|
||||
steps:
|
||||
- name: Checkout Code
|
||||
uses: actions/checkout@v3
|
||||
|
||||
- name: Docker Lint
|
||||
uses: hadolint/hadolint-action@v3.0.0
|
||||
with:
|
||||
dockerfile: contrib/docker/Dockerfile.apache
|
||||
failure-threshold: error
|
||||
|
||||
- name: Set up QEMU
|
||||
uses: docker/setup-qemu-action@v2
|
||||
|
||||
- name: Set up Docker Buildx
|
||||
uses: docker/setup-buildx-action@v2
|
||||
|
||||
- name: Login to DockerHub
|
||||
uses: docker/login-action@v2
|
||||
secrets: inherit
|
||||
with:
|
||||
username: ${{ secrets.DOCKER_HUB_USERNAME }}
|
||||
password: ${{ secrets.DOCKER_HUB_TOKEN }}
|
||||
if: github.event_name != 'pull_request'
|
||||
|
||||
- name: Fetch tags
|
||||
uses: docker/metadata-action@v4
|
||||
secrets: inherit
|
||||
id: meta
|
||||
with:
|
||||
images: ${{ secrets.DOCKER_HUB_ORGANISATION }}/pixelfed
|
||||
flavor: |
|
||||
latest=auto
|
||||
suffix=-apache
|
||||
tags: |
|
||||
type=edge,branch=dev
|
||||
type=pep440,pattern={{raw}}
|
||||
type=pep440,pattern=v{{major}}.{{minor}}
|
||||
type=ref,event=pr
|
||||
|
||||
- name: Build and push Docker image
|
||||
uses: docker/build-push-action@v3
|
||||
with:
|
||||
context: .
|
||||
file: contrib/docker/Dockerfile.apache
|
||||
platforms: linux/amd64,linux/arm64
|
||||
builder: ${{ steps.buildx.outputs.name }}
|
||||
push: ${{ github.event_name != 'pull_request' }}
|
||||
tags: ${{ steps.meta.outputs.tags }}
|
||||
cache-from: type=gha
|
||||
cache-to: type=gha,mode=max
|
||||
|
||||
build-docker-fpm:
|
||||
runs-on: ubuntu-latest
|
||||
|
||||
steps:
|
||||
- name: Checkout Code
|
||||
uses: actions/checkout@v3
|
||||
|
||||
- name: Docker Lint
|
||||
uses: hadolint/hadolint-action@v3.0.0
|
||||
with:
|
||||
dockerfile: contrib/docker/Dockerfile.fpm
|
||||
failure-threshold: error
|
||||
|
||||
- name: Set up QEMU
|
||||
uses: docker/setup-qemu-action@v2
|
||||
|
||||
- name: Set up Docker Buildx
|
||||
uses: docker/setup-buildx-action@v2
|
||||
|
||||
- name: Login to DockerHub
|
||||
uses: docker/login-action@v2
|
||||
secrets: inherit
|
||||
with:
|
||||
username: ${{ secrets.DOCKER_HUB_USERNAME }}
|
||||
password: ${{ secrets.DOCKER_HUB_TOKEN }}
|
||||
if: github.event_name != 'pull_request'
|
||||
|
||||
- name: Fetch tags
|
||||
uses: docker/metadata-action@v4
|
||||
secrets: inherit
|
||||
id: meta
|
||||
with:
|
||||
images: ${{ secrets.DOCKER_HUB_ORGANISATION }}/pixelfed
|
||||
flavor: |
|
||||
suffix=-fpm
|
||||
tags: |
|
||||
type=edge,branch=dev
|
||||
type=pep440,pattern={{raw}}
|
||||
type=pep440,pattern=v{{major}}.{{minor}}
|
||||
type=ref,event=pr
|
||||
|
||||
- name: Build and push Docker image
|
||||
uses: docker/build-push-action@v3
|
||||
with:
|
||||
context: .
|
||||
file: contrib/docker/Dockerfile.fpm
|
||||
platforms: linux/amd64,linux/arm64
|
||||
builder: ${{ steps.buildx.outputs.name }}
|
||||
push: ${{ github.event_name != 'pull_request' }}
|
||||
tags: ${{ steps.meta.outputs.tags }}
|
||||
cache-from: type=gha
|
||||
cache-to: type=gha,mode=max
|
|
@ -0,0 +1,230 @@
|
|||
---
|
||||
name: Docker
|
||||
|
||||
on:
|
||||
# See: https://docs.github.com/en/actions/using-workflows/events-that-trigger-workflows#workflow_dispatch
|
||||
workflow_dispatch:
|
||||
|
||||
# See: https://docs.github.com/en/actions/using-workflows/events-that-trigger-workflows#push
|
||||
push:
|
||||
branches:
|
||||
- dev
|
||||
- staging
|
||||
tags:
|
||||
- "*"
|
||||
|
||||
# See: https://docs.github.com/en/actions/using-workflows/events-that-trigger-workflows#pull_request
|
||||
pull_request:
|
||||
types:
|
||||
- opened
|
||||
- reopened
|
||||
- synchronize
|
||||
|
||||
jobs:
|
||||
lint:
|
||||
name: hadolint
|
||||
runs-on: ubuntu-latest
|
||||
|
||||
permissions:
|
||||
contents: read
|
||||
|
||||
steps:
|
||||
- name: Checkout Code
|
||||
uses: actions/checkout@v4
|
||||
|
||||
- name: Docker Lint
|
||||
uses: hadolint/hadolint-action@v3.1.0
|
||||
with:
|
||||
dockerfile: Dockerfile
|
||||
failure-threshold: error
|
||||
|
||||
shellcheck:
|
||||
name: ShellCheck
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
- name: Run ShellCheck
|
||||
uses: ludeeus/action-shellcheck@master
|
||||
env:
|
||||
SHELLCHECK_OPTS: --shell=bash --external-sources
|
||||
with:
|
||||
version: v0.9.0
|
||||
additional_files: "*.envsh .env .env.docker .env.example .env.testing"
|
||||
|
||||
bats:
|
||||
name: Bats Testing
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
- name: Run bats
|
||||
run: docker run -v "$PWD:/var/www" bats/bats:latest /var/www/tests/bats
|
||||
|
||||
build:
|
||||
name: Build, Test, and Push
|
||||
runs-on: ubuntu-latest
|
||||
|
||||
strategy:
|
||||
fail-fast: false
|
||||
|
||||
# See: https://docs.github.com/en/actions/using-jobs/using-a-matrix-for-your-jobs
|
||||
matrix:
|
||||
php_version:
|
||||
- 8.2
|
||||
- 8.3
|
||||
target_runtime:
|
||||
- apache
|
||||
- fpm
|
||||
- nginx
|
||||
php_base:
|
||||
- apache
|
||||
- fpm
|
||||
|
||||
# See: https://docs.github.com/en/actions/using-jobs/using-a-matrix-for-your-jobs#excluding-matrix-configurations
|
||||
# See: https://docs.github.com/en/actions/using-workflows/workflow-syntax-for-github-actions#jobsjob_idstrategymatrixexclude
|
||||
exclude:
|
||||
# targeting [apache] runtime with [fpm] base type doesn't make sense
|
||||
- target_runtime: apache
|
||||
php_base: fpm
|
||||
|
||||
# targeting [fpm] runtime with [apache] base type doesn't make sense
|
||||
- target_runtime: fpm
|
||||
php_base: apache
|
||||
|
||||
# targeting [nginx] runtime with [apache] base type doesn't make sense
|
||||
- target_runtime: nginx
|
||||
php_base: apache
|
||||
|
||||
# See: https://docs.github.com/en/actions/using-workflows/workflow-syntax-for-github-actions#example-using-concurrency-and-the-default-behavior
|
||||
concurrency:
|
||||
group: docker-build-${{ github.ref }}-${{ matrix.php_base }}-${{ matrix.php_version }}-${{ matrix.target_runtime }}
|
||||
cancel-in-progress: true
|
||||
|
||||
permissions:
|
||||
contents: read
|
||||
packages: write
|
||||
|
||||
env:
|
||||
# Set the repo variable [DOCKER_HUB_USERNAME] to override the default
|
||||
# at https://github.com/<user>/<project>/settings/variables/actions
|
||||
DOCKER_HUB_USERNAME: ${{ vars.DOCKER_HUB_USERNAME || 'pixelfed' }}
|
||||
|
||||
# Set the repo variable [DOCKER_HUB_ORGANISATION] to override the default
|
||||
# at https://github.com/<user>/<project>/settings/variables/actions
|
||||
DOCKER_HUB_ORGANISATION: ${{ vars.DOCKER_HUB_ORGANISATION || 'pixelfed' }}
|
||||
|
||||
# Set the repo variable [DOCKER_HUB_REPO] to override the default
|
||||
# at https://github.com/<user>/<project>/settings/variables/actions
|
||||
DOCKER_HUB_REPO: ${{ vars.DOCKER_HUB_REPO || 'pixelfed' }}
|
||||
|
||||
# For Docker Hub pushing to work, you need the secret [DOCKER_HUB_TOKEN]
|
||||
# set to your Personal Access Token at https://github.com/<user>/<project>/settings/secrets/actions
|
||||
#
|
||||
# ! NOTE: no [login] or [push] will happen to Docker Hub until this secret is set!
|
||||
HAS_DOCKER_HUB_CONFIGURED: ${{ secrets.DOCKER_HUB_TOKEN != '' }}
|
||||
|
||||
steps:
|
||||
- name: Checkout Code
|
||||
uses: actions/checkout@v4
|
||||
|
||||
- name: Set up QEMU
|
||||
uses: docker/setup-qemu-action@v3
|
||||
|
||||
- name: Set up Docker Buildx
|
||||
uses: docker/setup-buildx-action@v3
|
||||
id: buildx
|
||||
with:
|
||||
version: v0.12.0 # *or* newer, needed for annotations to work
|
||||
|
||||
# See: https://github.com/docker/login-action?tab=readme-ov-file#github-container-registry
|
||||
- name: Log in to the GitHub Container registry
|
||||
uses: docker/login-action@v3
|
||||
with:
|
||||
registry: ghcr.io
|
||||
username: ${{ github.actor }}
|
||||
password: ${{ secrets.GITHUB_TOKEN }}
|
||||
|
||||
# See: https://github.com/docker/login-action?tab=readme-ov-file#docker-hub
|
||||
- name: Login to Docker Hub registry (conditionally)
|
||||
if: ${{ env.HAS_DOCKER_HUB_CONFIGURED == true }}
|
||||
uses: docker/login-action@v3
|
||||
with:
|
||||
username: ${{ env.DOCKER_HUB_USERNAME }}
|
||||
password: ${{ secrets.DOCKER_HUB_TOKEN }}
|
||||
|
||||
- name: Docker meta
|
||||
uses: docker/metadata-action@v5
|
||||
id: meta
|
||||
with:
|
||||
images: |
|
||||
name=ghcr.io/${{ github.repository }},enable=true
|
||||
name=${{ env.DOCKER_HUB_ORGANISATION }}/${{ env.DOCKER_HUB_REPO }},enable=${{ env.HAS_DOCKER_HUB_CONFIGURED }}
|
||||
flavor: |
|
||||
latest=auto
|
||||
suffix=-${{ matrix.target_runtime }}-${{ matrix.php_version }}
|
||||
tags: |
|
||||
type=raw,value=dev,enable=${{ github.ref == format('refs/heads/{0}', 'dev') }}
|
||||
type=raw,value=staging,enable=${{ github.ref == format('refs/heads/{0}', 'staging') }}
|
||||
type=pep440,pattern={{raw}}
|
||||
type=pep440,pattern=v{{major}}.{{minor}}
|
||||
type=ref,event=branch,prefix=branch-
|
||||
type=ref,event=pr,prefix=pr-
|
||||
type=ref,event=tag
|
||||
env:
|
||||
DOCKER_METADATA_ANNOTATIONS_LEVELS: manifest,index
|
||||
|
||||
- name: Docker meta (Cache)
|
||||
uses: docker/metadata-action@v5
|
||||
id: cache
|
||||
with:
|
||||
images: |
|
||||
name=ghcr.io/${{ github.repository }}-cache,enable=true
|
||||
name=${{ env.DOCKER_HUB_ORGANISATION }}/${{ env.DOCKER_HUB_REPO }}-cache,enable=${{ env.HAS_DOCKER_HUB_CONFIGURED }}
|
||||
flavor: |
|
||||
latest=auto
|
||||
suffix=-${{ matrix.target_runtime }}-${{ matrix.php_version }}
|
||||
tags: |
|
||||
type=raw,value=dev,enable=${{ github.ref == format('refs/heads/{0}', 'dev') }}
|
||||
type=raw,value=staging,enable=${{ github.ref == format('refs/heads/{0}', 'staging') }}
|
||||
type=pep440,pattern={{raw}}
|
||||
type=pep440,pattern=v{{major}}.{{minor}}
|
||||
type=ref,event=branch,prefix=branch-
|
||||
type=ref,event=pr,prefix=pr-
|
||||
type=ref,event=tag
|
||||
env:
|
||||
DOCKER_METADATA_ANNOTATIONS_LEVELS: manifest,index
|
||||
|
||||
- name: Build and push Docker image
|
||||
uses: docker/build-push-action@v5
|
||||
with:
|
||||
context: .
|
||||
file: Dockerfile
|
||||
target: ${{ matrix.target_runtime }}-runtime
|
||||
platforms: linux/amd64,linux/arm64
|
||||
builder: ${{ steps.buildx.outputs.name }}
|
||||
tags: ${{ steps.meta.outputs.tags }}
|
||||
annotations: ${{ steps.meta.outputs.annotations }}
|
||||
push: true
|
||||
sbom: true
|
||||
provenance: true
|
||||
build-args: |
|
||||
PHP_VERSION=${{ matrix.php_version }}
|
||||
PHP_BASE_TYPE=${{ matrix.php_base }}
|
||||
cache-from: |
|
||||
type=gha,scope=${{ matrix.target_runtime }}-${{ matrix.php_base }}-${{ matrix.php_version }}
|
||||
cache-to: |
|
||||
type=gha,mode=max,scope=${{ matrix.target_runtime }}-${{ matrix.php_base }}-${{ matrix.php_version }}
|
||||
${{ steps.cache.outputs.tags }}
|
||||
|
||||
# goss validate the image
|
||||
#
|
||||
# See: https://github.com/goss-org/goss
|
||||
- uses: e1himself/goss-installation-action@v1
|
||||
with:
|
||||
version: "v0.4.4"
|
||||
- name: Execute Goss tests
|
||||
run: |
|
||||
dgoss run \
|
||||
-v "./.env.testing:/var/www/.env" \
|
||||
-e "EXPECTED_PHP_VERSION=${{ matrix.php_version }}" \
|
||||
-e "PHP_BASE_TYPE=${{ matrix.php_base }}" \
|
||||
${{ steps.meta.outputs.tags }}
|
|
@ -1,22 +1,31 @@
|
|||
.DS_Store
|
||||
/.bash_history
|
||||
/.bash_profile
|
||||
/.bashrc
|
||||
/.composer
|
||||
/.env
|
||||
/.env.dottie-backup
|
||||
#/.git
|
||||
/.git-credentials
|
||||
/.gitconfig
|
||||
#/.gitignore
|
||||
/.idea
|
||||
/.vagrant
|
||||
/bootstrap/cache
|
||||
/docker-compose-state/
|
||||
/Homestead.json
|
||||
/Homestead.yaml
|
||||
/node_modules
|
||||
/npm-debug.log
|
||||
/public/hot
|
||||
/public/storage
|
||||
/public/vendor/horizon
|
||||
/storage/*.key
|
||||
/storage/docker
|
||||
/vendor
|
||||
/.idea
|
||||
/.vscode
|
||||
/.vagrant
|
||||
/docker-volumes
|
||||
Homestead.json
|
||||
Homestead.yaml
|
||||
npm-debug.log
|
||||
yarn-error.log
|
||||
.env
|
||||
.DS_Store
|
||||
.bash_profile
|
||||
.bash_history
|
||||
.bashrc
|
||||
.gitconfig
|
||||
.git-credentials
|
||||
/.composer/
|
||||
/nginx.conf
|
||||
/yarn-error.log
|
||||
/public/build
|
||||
|
||||
# Exceptions - these *MUST* be last
|
||||
!/bootstrap/cache/.gitignore
|
||||
!/public/vendor/horizon/.gitignore
|
||||
|
|
|
@ -0,0 +1,5 @@
|
|||
ignored:
|
||||
- DL3002 # warning: Last USER should not be root
|
||||
- DL3008 # warning: Pin versions in apt get install. Instead of `apt-get install <package>` use `apt-get install <package>=<version>`
|
||||
- SC2046 # warning: Quote this to prevent word splitting.
|
||||
- SC2086 # info: Double quote to prevent globbing and word splitting.
|
|
@ -0,0 +1,4 @@
|
|||
{
|
||||
"MD013": false,
|
||||
"MD014": false
|
||||
}
|
|
@ -0,0 +1,12 @@
|
|||
# See: https://github.com/koalaman/shellcheck/blob/master/shellcheck.1.md#rc-files
|
||||
|
||||
source-path=SCRIPTDIR
|
||||
|
||||
# Allow opening any 'source'd file, even if not specified as input
|
||||
external-sources=true
|
||||
|
||||
# Turn on warnings for unquoted variables with safe values
|
||||
enable=quote-safe-variables
|
||||
|
||||
# Turn on warnings for unassigned uppercase variables
|
||||
enable=check-unassigned-uppercase
|
|
@ -0,0 +1,14 @@
|
|||
{
|
||||
"recommendations": [
|
||||
"foxundermoon.shell-format",
|
||||
"timonwong.shellcheck",
|
||||
"jetmartin.bats",
|
||||
"aaron-bond.better-comments",
|
||||
"streetsidesoftware.code-spell-checker",
|
||||
"editorconfig.editorconfig",
|
||||
"github.vscode-github-actions",
|
||||
"bmewburn.vscode-intelephense-client",
|
||||
"redhat.vscode-yaml",
|
||||
"ms-azuretools.vscode-docker"
|
||||
]
|
||||
}
|
|
@ -0,0 +1,21 @@
|
|||
{
|
||||
"shellformat.useEditorConfig": true,
|
||||
"[shellscript]": {
|
||||
"files.eol": "\n",
|
||||
"editor.defaultFormatter": "foxundermoon.shell-format"
|
||||
},
|
||||
"[yaml]": {
|
||||
"editor.defaultFormatter": "redhat.vscode-yaml"
|
||||
},
|
||||
"[dockercompose]": {
|
||||
"editor.defaultFormatter": "redhat.vscode-yaml",
|
||||
"editor.autoIndent": "advanced",
|
||||
},
|
||||
"yaml.schemas": {
|
||||
"https://json.schemastore.org/composer": "https://raw.githubusercontent.com/compose-spec/compose-spec/master/schema/compose-spec.json"
|
||||
},
|
||||
"files.associations": {
|
||||
".env": "shellscript",
|
||||
".env.*": "shellscript"
|
||||
}
|
||||
}
|
380
CHANGELOG.md
380
CHANGELOG.md
|
@ -1,7 +1,381 @@
|
|||
# Release Notes
|
||||
|
||||
## [Unreleased](https://github.com/pixelfed/pixelfed/compare/v0.11.6...dev)
|
||||
- ([](https://github.com/pixelfed/pixelfed/commit/))
|
||||
## [Unreleased](https://github.com/pixelfed/pixelfed/compare/v0.12.1...dev)
|
||||
|
||||
### Updates
|
||||
- Update DirectMessageController, add 72 hour delay for new accounts before they can send a DM ([61d105fd](https://github.com/pixelfed/pixelfed/commit/61d105fd))
|
||||
- Update AdminCuratedRegisterController, increase message length from 1000 to 3000 ([9a5e3471](https://github.com/pixelfed/pixelfed/commit/))
|
||||
- ([](https://github.com/pixelfed/pixelfed/commit/9a5e3471))
|
||||
|
||||
## [v0.12.1 (2024-05-07)](https://github.com/pixelfed/pixelfed/compare/v0.12.0...v0.12.1)
|
||||
|
||||
### Updates
|
||||
- Update ApiV1Dot1Controller, fix in app registration bug that prevents proper auth flow due to missing oauth scopes ([cbf996c9](https://github.com/pixelfed/pixelfed/commit/cbf996c9))
|
||||
- Update ConfigCacheService, fix database race condition and fallback to file config and enable by default ([60a62b59](https://github.com/pixelfed/pixelfed/commit/60a62b59))
|
||||
|
||||
## [v0.12.0 (2024-04-29)](https://github.com/pixelfed/pixelfed/compare/v0.11.13...v0.12.0)
|
||||
|
||||
### Updates
|
||||
|
||||
- Update SoftwareUpdateService, add command to refresh latest versions ([632f2cb6](https://github.com/pixelfed/pixelfed/commit/632f2cb6))
|
||||
- Update Post.vue, fix cache bug ([3a27e637](https://github.com/pixelfed/pixelfed/commit/3a27e637))
|
||||
- Update StatusHashtagService, use more efficient cached count ([592c8412](https://github.com/pixelfed/pixelfed/commit/592c8412))
|
||||
- Update DiscoverController, handle discover hashtag redirects ([18382e8a](https://github.com/pixelfed/pixelfed/commit/18382e8a))
|
||||
- Update ApiV1Controller, use admin filter service ([94503a1c](https://github.com/pixelfed/pixelfed/commit/94503a1c))
|
||||
- Update SearchApiV2Service, use more efficient query ([cee618e8](https://github.com/pixelfed/pixelfed/commit/cee618e8))
|
||||
- Update Curated Onboarding view, fix concierge form ([15ad69f7](https://github.com/pixelfed/pixelfed/commit/15ad69f7))
|
||||
- Update AP Profile Transformer, add `suspended` attribute ([25f3fa06](https://github.com/pixelfed/pixelfed/commit/25f3fa06))
|
||||
- Update AP Profile Transformer, fix movedTo attribute ([63100fe9](https://github.com/pixelfed/pixelfed/commit/63100fe9))
|
||||
- Update AP Profile Transformer, fix suspended attributes ([2e5e68e4](https://github.com/pixelfed/pixelfed/commit/2e5e68e4))
|
||||
- Update PrivacySettings controller, add cache invalidation ([e742d595](https://github.com/pixelfed/pixelfed/commit/e742d595))
|
||||
- Update ProfileController, preserve deleted actor objects for federated account deletion and use more efficient account cache lookup ([853a729f](https://github.com/pixelfed/pixelfed/commit/853a729f))
|
||||
- Update SiteController, add curatedOnboarding method that gracefully falls back to open registration when applicable ([95199843](https://github.com/pixelfed/pixelfed/commit/95199843))
|
||||
- Update AP transformers, add DeleteActor activity ([bcce1df6](https://github.com/pixelfed/pixelfed/commit/bcce1df6))
|
||||
- Update commands, add user account delete cli command to federate account deletion ([4aa0e25f](https://github.com/pixelfed/pixelfed/commit/4aa0e25f))
|
||||
- Update web-api popular accounts route to its own method to remove the breaking oauth scope bug ([a4bc5ce3](https://github.com/pixelfed/pixelfed/commit/a4bc5ce3))
|
||||
- Update config cache ([5e4d4eff](https://github.com/pixelfed/pixelfed/commit/5e4d4eff))
|
||||
- Update Config, use config_cache ([7785a2da](https://github.com/pixelfed/pixelfed/commit/7785a2da))
|
||||
- Update ApiV1Dot1Controller, use config_cache for in-app registration ([b0cb4456](https://github.com/pixelfed/pixelfed/commit/b0cb4456))
|
||||
- Update captcha, use config_cache helper ([8a89e3c9](https://github.com/pixelfed/pixelfed/commit/8a89e3c9))
|
||||
- Update custom emoji, add config_cache support ([481314cd](https://github.com/pixelfed/pixelfed/commit/481314cd))
|
||||
- Update ProfileController, fix permalink redirect bug ([75081e60](https://github.com/pixelfed/pixelfed/commit/75081e60))
|
||||
- Update admin css, use font-display:swap for nucleo icons ([8a0c456e](https://github.com/pixelfed/pixelfed/commit/8a0c456e))
|
||||
- Update PixelfedDirectoryController, fix boolean cast bug ([f08aab22](https://github.com/pixelfed/pixelfed/commit/f08aab22))
|
||||
- Update PixelfedDirectoryController, use cached stats ([f2f2a809](https://github.com/pixelfed/pixelfed/commit/f2f2a809))
|
||||
- Update AdminDirectoryController, fix type casting ([ad506e90](https://github.com/pixelfed/pixelfed/commit/ad506e90))
|
||||
- Update image pipeline, use config_cache ([a72188a7](https://github.com/pixelfed/pixelfed/commit/a72188a7))
|
||||
- Update cloud storage, use config_cache ([665581d8](https://github.com/pixelfed/pixelfed/commit/665581d8))
|
||||
- Update pixelfed.max_album_length, use config_cache ([fecbe189](https://github.com/pixelfed/pixelfed/commit/fecbe189))
|
||||
- Update media_types, use config_cache ([d670de17](https://github.com/pixelfed/pixelfed/commit/d670de17))
|
||||
- Update landing settings, use config_cache ([40478f25](https://github.com/pixelfed/pixelfed/commit/40478f25))
|
||||
- Update activitypub setting, use config_cache ([5071aaf4](https://github.com/pixelfed/pixelfed/commit/5071aaf4))
|
||||
- Update oauth setting, use config_cache ([ce228f7f](https://github.com/pixelfed/pixelfed/commit/ce228f7f))
|
||||
- Update stories config, use config_cache ([d1adb109](https://github.com/pixelfed/pixelfed/commit/d1adb109))
|
||||
- Update ig import, use config_cache ([da0e0ffa](https://github.com/pixelfed/pixelfed/commit/da0e0ffa))
|
||||
- Update autospam config, use config_cache ([a76cb5f4](https://github.com/pixelfed/pixelfed/commit/a76cb5f4))
|
||||
- Update app.name config, use config_cache ([911446c0](https://github.com/pixelfed/pixelfed/commit/911446c0))
|
||||
- Update UserObserver, fix type casting ([949e9979](https://github.com/pixelfed/pixelfed/commit/949e9979))
|
||||
- Update user_filters, use config_cache ([6ce513f8](https://github.com/pixelfed/pixelfed/commit/6ce513f8))
|
||||
- Update filesystems config, add to config_cache ([087b2791](https://github.com/pixelfed/pixelfed/commit/087b2791))
|
||||
- Update web-admin routes, add setting api routes ([828a456f](https://github.com/pixelfed/pixelfed/commit/828a456f))
|
||||
- Update hashtag component ([cee979ed](https://github.com/pixelfed/pixelfed/commit/cee979ed))
|
||||
- Update AdminReadMore component, add .prevent to click action ([704e7b12](https://github.com/pixelfed/pixelfed/commit/704e7b12))
|
||||
- Update admin dashboard, add admin settings partials ([eb487123](https://github.com/pixelfed/pixelfed/commit/eb487123))
|
||||
- Update admin settings, refactor to vue component ([674e560f](https://github.com/pixelfed/pixelfed/commit/674e560f))
|
||||
- Update ConfigCacheService, encrypt keys at rest ([3628b462](https://github.com/pixelfed/pixelfed/commit/3628b462))
|
||||
- Update RemoteFollowImportRecent, use MediaPathService ([5162c070](https://github.com/pixelfed/pixelfed/commit/5162c070))
|
||||
- Update AdminSettingsController, add user filter max limit settings ([ac1f0748](https://github.com/pixelfed/pixelfed/commit/ac1f0748))
|
||||
- Update AdminSettingsController, add AdminSettingsService ([dcc5f416](https://github.com/pixelfed/pixelfed/commit/dcc5f416))
|
||||
- Update AdminSettings component, fix user settings ([aba1e13d](https://github.com/pixelfed/pixelfed/commit/aba1e13d))
|
||||
- Update AdminInstances component ([ec2fdd61](https://github.com/pixelfed/pixelfed/commit/ec2fdd61))
|
||||
- Update AdminSettings, add max_account_size support ([2dcbc1d5](https://github.com/pixelfed/pixelfed/commit/2dcbc1d5))
|
||||
- Update AdminSettings, use better validation for user integer settings ([d946afcc](https://github.com/pixelfed/pixelfed/commit/d946afcc))
|
||||
- Update spa sass, fix timestamp dark mode bug ([4147f7c5](https://github.com/pixelfed/pixelfed/commit/4147f7c5))
|
||||
- Update relationships view, fix unfollow hashtag bug. Fixes #5008 ([8c693640](https://github.com/pixelfed/pixelfed/commit/8c693640))
|
||||
- Update PrivacySettings controller, refresh RelationshipService when unmute/unblocking ([b7322b68](https://github.com/pixelfed/pixelfed/commit/b7322b68))
|
||||
- Update ApiV1Controller, improve refresh relations logic when (un)muting or (un)blocking ([b8e96a5f](https://github.com/pixelfed/pixelfed/commit/b8e96a5f))
|
||||
- Update context menu, add mute/block/unfollow actions and update relationship store accordingly ([81d1e0fd](https://github.com/pixelfed/pixelfed/commit/81d1e0fd))
|
||||
- Update docker env, fix config_cache. Fixes #5033 ([858fcbf6](https://github.com/pixelfed/pixelfed/commit/858fcbf6))
|
||||
- Update UnfollowPipeline, fix follower count cache bug ([6bdf73de](https://github.com/pixelfed/pixelfed/commit/6bdf73de))
|
||||
- Update VideoPresenter component, add webkit-playsinline attribute to video element to prevent the full screen video player ([ad032916](https://github.com/pixelfed/pixelfed/commit/ad032916))
|
||||
- Update VideoPlayer component, add playsinline attribute to video element ([8af23607](https://github.com/pixelfed/pixelfed/commit/8af23607))
|
||||
- Update StatusController, refactor status embeds ([9a7acc12](https://github.com/pixelfed/pixelfed/commit/9a7acc12))
|
||||
- Update ProfileController, refactor profile embeds ([8b8b1ffc](https://github.com/pixelfed/pixelfed/commit/8b8b1ffc))
|
||||
- Update profile embed view, fix height bug ([65166570](https://github.com/pixelfed/pixelfed/commit/65166570))
|
||||
- Update CustomEmojiService, only return local emoji ([7f8bba44](https://github.com/pixelfed/pixelfed/commit/7f8bba44))
|
||||
- Update Like model, increase max likes per day from 500 to 1500 ([4223119f](https://github.com/pixelfed/pixelfed/commit/4223119f))
|
||||
|
||||
## [v0.11.13 (2024-03-05)](https://github.com/pixelfed/pixelfed/compare/v0.11.12...v0.11.13)
|
||||
|
||||
### Features
|
||||
|
||||
- Account Migrations ([#4968](https://github.com/pixelfed/pixelfed/pull/4968)) ([4a6be6212](https://github.com/pixelfed/pixelfed/pull/4968/commits/4a6be6212))
|
||||
- Curated Onboarding ([#4946](https://github.com/pixelfed/pixelfed/pull/4946)) ([8dac2caf](https://github.com/pixelfed/pixelfed/commit/8dac2caf))
|
||||
- Add Curated Onboarding Templates ([071163b4](https://github.com/pixelfed/pixelfed/commit/071163b4))
|
||||
- Add Remote Reports to Admin Dashboard Reports page ([ef0ff78e](https://github.com/pixelfed/pixelfed/commit/ef0ff78e))
|
||||
- Improved Docker Support ([#4844](https://github.com/pixelfed/pixelfed/pull/4844)) ([d92cf7f](https://github.com/pixelfed/pixelfed/commit/d92cf7f))
|
||||
|
||||
### Updates
|
||||
|
||||
- Update Inbox, cast live filters to lowercase ([d835e0ad](https://github.com/pixelfed/pixelfed/commit/d835e0ad))
|
||||
- Update federation config, increase default timeline days falloff to 90 days from 2 days. Fixes #4905 ([011834f4](https://github.com/pixelfed/pixelfed/commit/011834f4))
|
||||
- Update cache config, use predis as default redis driver client ([ea6b1623](https://github.com/pixelfed/pixelfed/commit/ea6b1623))
|
||||
- Update .gitattributes to collapse diffs on generated files ([ThisIsMissEm](https://github.com/pixelfed/pixelfed/commit/9978b2b9))
|
||||
- Update api v1/v2 instance endpoints, bump mastoapi version from 2.7.2 to 3.5.3 ([545f7d5e](https://github.com/pixelfed/pixelfed/commit/545f7d5e))
|
||||
- Update ApiV1Controller, implement better limit logic to gracefully handle requests with limits that exceed the max ([1f74a95d](https://github.com/pixelfed/pixelfed/commit/1f74a95d))
|
||||
- Update AdminCuratedRegisterController, show oldest applications first ([c4dde641](https://github.com/pixelfed/pixelfed/commit/c4dde641))
|
||||
- Update Directory logic, add curated onboarding support ([59c70239](https://github.com/pixelfed/pixelfed/commit/59c70239))
|
||||
- Update Inbox and StatusObserver, fix silently rejected direct messages due to saveQuietly which failed to generate a snowflake id ([089ba3c4](https://github.com/pixelfed/pixelfed/commit/089ba3c4))
|
||||
- Update Curated Onboarding dashboard, improve application filtering and make it easier to distinguish response state ([2b5d7235](https://github.com/pixelfed/pixelfed/commit/2b5d7235))
|
||||
- Update AdminReports, add story reports and fix cs ([767522a8](https://github.com/pixelfed/pixelfed/commit/767522a8))
|
||||
- Update AdminReportController, add story report support ([a16309ac](https://github.com/pixelfed/pixelfed/commit/a16309ac))
|
||||
- Update kb, add email confirmation issues page ([2f48df8c](https://github.com/pixelfed/pixelfed/commit/2f48df8c))
|
||||
- Update AdminCuratedRegisterController, filter confirmation activities from activitylog ([ab9ecb6e](https://github.com/pixelfed/pixelfed/commit/ab9ecb6e))
|
||||
- Update Inbox, fix flag validation condition, allow profile reports ([402a4607](https://github.com/pixelfed/pixelfed/commit/402a4607))
|
||||
- Update AccountTransformer, fix follower/following count visibility bug ([542d1106](https://github.com/pixelfed/pixelfed/commit/542d1106))
|
||||
- Update ProfileMigration model, add target relation ([3f053997](https://github.com/pixelfed/pixelfed/commit/3f053997))
|
||||
- Update ApiV1Controller, update Notifications endpoint to filter notifications with missing activities ([a933615b](https://github.com/pixelfed/pixelfed/commit/a933615b))
|
||||
- Update ApiV1Controller, fix public timeline scope, properly support both local + remote parameters ([d6eac655](https://github.com/pixelfed/pixelfed/commit/d6eac655))
|
||||
- Update ApiV1Controller, handle public feed parameter bug to gracefully fallback to min_id=1 when max_id=0 ([e3826c58](https://github.com/pixelfed/pixelfed/commit/e3826c58))
|
||||
- Update ApiV1Controller, fix hashtag feed to include private posts from accounts you follow or your own, and your own unlisted posts ([3b5500b3](https://github.com/pixelfed/pixelfed/commit/3b5500b3))
|
||||
- Update checkpoint view, improve input autocomplete. Fixes ([#4959](https://github.com/pixelfed/pixelfed/pull/4959)) ([d18824e7](https://github.com/pixelfed/pixelfed/commit/d18824e7))
|
||||
- Update navbar.vue, removes the 50px limit ([#4969](https://github.com/pixelfed/pixelfed/pull/4969)) ([7fd5599](https://github.com/pixelfed/pixelfed/commit/7fd5599))
|
||||
- Update ComposeModal.vue, add an informative UI error message when trying to create a mixed media album ([#4886](https://github.com/pixelfed/pixelfed/pull/4886)) ([fd4f41a](https://github.com/pixelfed/pixelfed/commit/fd4f41a))
|
||||
|
||||
## [v0.11.12 (2024-02-16)](https://github.com/pixelfed/pixelfed/compare/v0.11.11...v0.11.12)
|
||||
|
||||
### Features
|
||||
- Autospam Live Filters - block remote activities based on comma separated keywords ([40b45b2a](https://github.com/pixelfed/pixelfed/commit/40b45b2a))
|
||||
- Added Software Update banner to admin home feeds ([b0fb1988](https://github.com/pixelfed/pixelfed/commit/b0fb1988))
|
||||
|
||||
### Updates
|
||||
|
||||
- Update ApiV1Controller, fix network timeline ([0faf59e3](https://github.com/pixelfed/pixelfed/commit/0faf59e3))
|
||||
- Update public/network timelines, fix non-redis response and fix reblogs in home feed ([8b4ac5cc](https://github.com/pixelfed/pixelfed/commit/8b4ac5cc))
|
||||
- Update Federation, use proper Content-Type headers for following/follower collections ([fb0bb9a3](https://github.com/pixelfed/pixelfed/commit/fb0bb9a3))
|
||||
- Update ActivityPubFetchService, enforce stricter Content-Type validation ([1232cfc8](https://github.com/pixelfed/pixelfed/commit/1232cfc8))
|
||||
- Update status view, fix unlisted/private scope bug ([0f3ca194](https://github.com/pixelfed/pixelfed/commit/0f3ca194))
|
||||
|
||||
## [v0.11.11 (2024-02-09)](https://github.com/pixelfed/pixelfed/compare/v0.11.10...v0.11.11)
|
||||
|
||||
### Fixes
|
||||
- Fix api endpoints ([fd7f5dbb](https://github.com/pixelfed/pixelfed/commit/fd7f5dbb))
|
||||
|
||||
## [v0.11.10 (2024-02-09)](https://github.com/pixelfed/pixelfed/compare/v0.11.9...v0.11.10)
|
||||
|
||||
### Added
|
||||
- Resilient Media Storage ([#4665](https://github.com/pixelfed/pixelfed/pull/4665)) ([fb1deb6](https://github.com/pixelfed/pixelfed/commit/fb1deb6))
|
||||
- Video WebP2P ([#4713](https://github.com/pixelfed/pixelfed/pull/4713)) ([0405ef12](https://github.com/pixelfed/pixelfed/commit/0405ef12))
|
||||
- Added user:2fa command to easily disable 2FA for given account ([c6408fd7](https://github.com/pixelfed/pixelfed/commit/c6408fd7))
|
||||
- Added `avatar:storage-deep-clean` command to dispatch remote avatar storage cleanup jobs ([c37b7cde](https://github.com/pixelfed/pixelfed/commit/c37b7cde))
|
||||
- Added S3 command to rewrite media urls ([5b3a5610](https://github.com/pixelfed/pixelfed/commit/5b3a5610))
|
||||
- Experimental home feed ([#4752](https://github.com/pixelfed/pixelfed/pull/4752)) ([c39b9afb](https://github.com/pixelfed/pixelfed/commit/c39b9afb))
|
||||
- Added `app:hashtag-cached-count-update` command to update cached_count of hashtags and add to scheduler to run every 25 minutes past the hour ([1e31fee6](https://github.com/pixelfed/pixelfed/commit/1e31fee6))
|
||||
- Added `app:hashtag-related-generate` command to generate related hashtags ([176b4ed7](https://github.com/pixelfed/pixelfed/commit/176b4ed7))
|
||||
- Added Mutual Followers API endpoint ([33dbbe46](https://github.com/pixelfed/pixelfed/commit/33dbbe46))
|
||||
- Added User Domain Blocks ([#4834](https://github.com/pixelfed/pixelfed/pull/4834)) ([fa0380ac](https://github.com/pixelfed/pixelfed/commit/fa0380ac))
|
||||
- Added Parental Controls ([#4862](https://github.com/pixelfed/pixelfed/pull/4862)) ([c91f1c59](https://github.com/pixelfed/pixelfed/commit/c91f1c59))
|
||||
- Added Forgot Email Feature ([67c650b1](https://github.com/pixelfed/pixelfed/commit/67c650b1))
|
||||
- Added S3 IG Import Media Storage support ([#4891](https://github.com/pixelfed/pixelfed/pull/4891)) ([081360b9](https://github.com/pixelfed/pixelfed/commit/081360b9))
|
||||
|
||||
### Federation
|
||||
- Update Privacy Settings, add support for Mastodon `indexable` search flag ([fc24630e](https://github.com/pixelfed/pixelfed/commit/fc24630e))
|
||||
- Update AP Helpers, consume actor `indexable` attribute ([fbdcdd9d](https://github.com/pixelfed/pixelfed/commit/fbdcdd9d))
|
||||
|
||||
### Updates
|
||||
- Update FollowerService, add forget method to RelationshipService call to reduce load when mass purging ([347e4f59](https://github.com/pixelfed/pixelfed/commit/347e4f59))
|
||||
- Update FollowServiceWarmCache, improve handling larger following/follower lists ([61a6d904](https://github.com/pixelfed/pixelfed/commit/61a6d904))
|
||||
- Update StoryApiV1Controller, add viewers route to view story viewers ([941736ce](https://github.com/pixelfed/pixelfed/commit/941736ce))
|
||||
- Update NotificationService, improve cache warming query ([2496386d](https://github.com/pixelfed/pixelfed/commit/2496386d))
|
||||
- Update StatusService, hydrate accounts on request instead of caching them along with status objects ([223661ec](https://github.com/pixelfed/pixelfed/commit/223661ec))
|
||||
- Update profile embed, fix resize ([dc23c21d](https://github.com/pixelfed/pixelfed/commit/dc23c21d))
|
||||
- Update Status model, improve thumb logic ([d969a973](https://github.com/pixelfed/pixelfed/commit/d969a973))
|
||||
- Update Status model, allow unlisted thumbnails ([1f0a45b7](https://github.com/pixelfed/pixelfed/commit/1f0a45b7))
|
||||
- Update StatusTagsPipeline, fix object tags and slug normalization ([d295e605](https://github.com/pixelfed/pixelfed/commit/d295e605))
|
||||
- Update Note and CreateNote transformers, include attachment blurhash, width and height ([ce1afe27](https://github.com/pixelfed/pixelfed/commit/ce1afe27))
|
||||
- Update ap helpers, store media attachment width and height if present ([8c969191](https://github.com/pixelfed/pixelfed/commit/8c969191))
|
||||
- Update Sign-in with Mastodon, allow usage when registrations are closed ([895dc4fa](https://github.com/pixelfed/pixelfed/commit/895dc4fa))
|
||||
- Update profile embeds, filter sensitive posts ([ede5ec3b](https://github.com/pixelfed/pixelfed/commit/ede5ec3b))
|
||||
- Update ApiV1Controller, hydrate reblog interactions. Fixes ([#4686](https://github.com/pixelfed/pixelfed/issues/4686)) ([135798eb](https://github.com/pixelfed/pixelfed/commit/135798eb))
|
||||
- Update AdminReportController, add `profile_id` to group by. Fixes ([#4685](https://github.com/pixelfed/pixelfed/issues/4685)) ([e4d3b196](https://github.com/pixelfed/pixelfed/commit/e4d3b196))
|
||||
- Update user:admin command, improve logic. Fixes ([#2465](https://github.com/pixelfed/pixelfed/issues/2465)) ([01bac511](https://github.com/pixelfed/pixelfed/commit/01bac511))
|
||||
- Update AP helpers, adjust RemoteAvatarFetch ttl from 24h to 3 months ([36b23fe3](https://github.com/pixelfed/pixelfed/commit/36b23fe3))
|
||||
- Update AvatarPipeline, improve refresh logic and garbage collection to purge old avatars ([82798b5e](https://github.com/pixelfed/pixelfed/commit/82798b5e))
|
||||
- Update CreateAvatar job, add processing constraints and set `is_remote` attribute ([319ced40](https://github.com/pixelfed/pixelfed/commit/319ced40))
|
||||
- Update RemoteStatusDelete and DecrementPostCount pipelines ([edbcf3ed](https://github.com/pixelfed/pixelfed/commit/edbcf3ed))
|
||||
- Update lexer regex, fix mention regex and add more tests ([778e83d3](https://github.com/pixelfed/pixelfed/commit/778e83d3))
|
||||
- Update StatusTransformer, generate autolink on request ([dfe2379b](https://github.com/pixelfed/pixelfed/commit/dfe2379b))
|
||||
- Update ComposeModal component, fix multi filter bug and allow media re-ordering before upload/posting ([56e315f6](https://github.com/pixelfed/pixelfed/commit/56e315f6))
|
||||
- Update ApiV1Dot1Controller, allow iar rate limits to be configurable ([28a80803](https://github.com/pixelfed/pixelfed/commit/28a80803))
|
||||
- Update ApiV1Dot1Controller, add domain to iar redirect ([1f82d47c](https://github.com/pixelfed/pixelfed/commit/1f82d47c))
|
||||
- Update ApiV1Dot1Controller, add configurable app confirm rate limit ttl ([4c6a0719](https://github.com/pixelfed/pixelfed/commit/4c6a0719))
|
||||
- Update LikePipeline, dispatch to feed queue. Fixes ([#4723](https://github.com/pixelfed/pixelfed/issues/4723)) ([da510089](https://github.com/pixelfed/pixelfed/commit/da510089))
|
||||
- Update AccountImport ([5a2d7e3e](https://github.com/pixelfed/pixelfed/commit/5a2d7e3e))
|
||||
- Update ImportPostController, fix IG bug with missing spaces between hashtags ([9c24157a](https://github.com/pixelfed/pixelfed/commit/9c24157a))
|
||||
- Update ApiV1Controller, fix mutes in home feed ([ddc21714](https://github.com/pixelfed/pixelfed/commit/ddc21714))
|
||||
- Update AP helpers, improve preferredUsername validation ([21218c79](https://github.com/pixelfed/pixelfed/commit/21218c79))
|
||||
- Update delete pipelines, properly invoke StatusHashtag delete events ([ce54d29c](https://github.com/pixelfed/pixelfed/commit/ce54d29c))
|
||||
- Update mail config ([0e431271](https://github.com/pixelfed/pixelfed/commit/0e431271))
|
||||
- Update hashtag following ([015b1b80](https://github.com/pixelfed/pixelfed/commit/015b1b80))
|
||||
- Update IncrementPostCount job, prevent overlap ([b2c9cc23](https://github.com/pixelfed/pixelfed/commit/b2c9cc23))
|
||||
- Update HashtagFollowService, fix cache invalidation bug ([84f4e885](https://github.com/pixelfed/pixelfed/commit/84f4e885))
|
||||
- Update Experimental Home Feed, fix remote posts, shares and reblogs ([c6a6b3ae](https://github.com/pixelfed/pixelfed/commit/c6a6b3ae))
|
||||
- Update HashtagService, improve count perf ([3327a008](https://github.com/pixelfed/pixelfed/commit/3327a008))
|
||||
- Update StatusHashtagService, remove problematic cache layer ([e5401f85](https://github.com/pixelfed/pixelfed/commit/e5401f85))
|
||||
- Update HomeFeedPipeline, fix tag filtering ([f105f4e8](https://github.com/pixelfed/pixelfed/commit/f105f4e8))
|
||||
- Update HashtagService, reduce cached_count cache ttl ([15f29f7d](https://github.com/pixelfed/pixelfed/commit/15f29f7d))
|
||||
- Update ApiV1Controller, fix include_reblogs param on timelines/home endpoint, and improve limit pagination logic ([287f903b](https://github.com/pixelfed/pixelfed/commit/287f903b))
|
||||
- Update StoryApiV1Controller, add self-carousel endpoint. Fixes ([#4352](https://github.com/pixelfed/pixelfed/issues/4352)) ([bcb88d5b](https://github.com/pixelfed/pixelfed/commit/bcb88d5b))
|
||||
- Update FollowServiceWarmCache, use more efficient query ([fe9b4c5a](https://github.com/pixelfed/pixelfed/commit/fe9b4c5a))
|
||||
- Update HomeFeedPipeline, observe mutes/blocks during fanout ([8548294c](https://github.com/pixelfed/pixelfed/commit/8548294c))
|
||||
- Update FederationController, add proper following/follower counts ([3204fb96](https://github.com/pixelfed/pixelfed/commit/3204fb96))
|
||||
- Update FederationController, add proper statuses counts ([3204fb96](https://github.com/pixelfed/pixelfed/commit/3204fb96))
|
||||
- Update Inbox handler, fix missing object_url and uri fields for direct statuses ([a0157fce](https://github.com/pixelfed/pixelfed/commit/a0157fce))
|
||||
- Update DirectMessageController, deliver direct delete activities to user inbox instead of sharedInbox ([d848792a](https://github.com/pixelfed/pixelfed/commit/d848792a))
|
||||
- Update DirectMessageController, dispatch deliver and delete actions to the job queue ([7f462a80](https://github.com/pixelfed/pixelfed/commit/7f462a80))
|
||||
- Update Inbox, improve story attribute collection ([06bee36c](https://github.com/pixelfed/pixelfed/commit/06bee36c))
|
||||
- Update DirectMessageController, dispatch local deletes to pipeline ([98186564](https://github.com/pixelfed/pixelfed/commit/98186564))
|
||||
- Update StatusPipeline, fix Direct and Story notification deletion ([4c95306f](https://github.com/pixelfed/pixelfed/commit/4c95306f))
|
||||
- Update Notifications.vue, fix deprecated DM action links for story activities ([4c3823b0](https://github.com/pixelfed/pixelfed/commit/4c3823b0))
|
||||
- Update ComposeModal, fix missing alttext post state ([0a068119](https://github.com/pixelfed/pixelfed/commit/0a068119))
|
||||
- Update PhotoAlbumPresenter.vue, fix fullscreen mode ([822e9888](https://github.com/pixelfed/pixelfed/commit/822e9888))
|
||||
- Update Timeline.vue, improve CHT pagination ([9c43e7e2](https://github.com/pixelfed/pixelfed/commit/9c43e7e2))
|
||||
- Update HomeFeedPipeline, fix StatusService validation ([041c0135](https://github.com/pixelfed/pixelfed/commit/041c0135))
|
||||
- Update Inbox, improve tombstone query efficiency ([759a4393](https://github.com/pixelfed/pixelfed/commit/759a4393))
|
||||
- Update AccountService, add setLastActive method ([ebbd98e7](https://github.com/pixelfed/pixelfed/commit/ebbd98e7))
|
||||
- Update ApiV1Controller, set last_active_at ([b6419545](https://github.com/pixelfed/pixelfed/commit/b6419545))
|
||||
- Update AdminShadowFilter, fix deleted profile bug ([a492a95a](https://github.com/pixelfed/pixelfed/commit/a492a95a))
|
||||
- Update FollowerService, add $silent param to remove method to more efficently purge relationships ([1664a5bc](https://github.com/pixelfed/pixelfed/commit/1664a5bc))
|
||||
- Update AP ProfileTransformer, add published attribute ([adfaa2b1](https://github.com/pixelfed/pixelfed/commit/adfaa2b1))
|
||||
- Update meta tags, improve descriptions and seo/og tags ([fd44c80c](https://github.com/pixelfed/pixelfed/commit/fd44c80c))
|
||||
- Update login view, add email prefill logic ([d76f0168](https://github.com/pixelfed/pixelfed/commit/d76f0168))
|
||||
- Update LoginController, fix captcha validation error message ([0325e171](https://github.com/pixelfed/pixelfed/commit/0325e171))
|
||||
- Update ApiV1Controller, properly cast boolean sensitive parameter. Fixes #4888 ([0aff126a](https://github.com/pixelfed/pixelfed/commit/0aff126a))
|
||||
- Update AccountImport.vue, fix new IG export format ([59aa6a4b](https://github.com/pixelfed/pixelfed/commit/59aa6a4b))
|
||||
- Update TransformImports command, fix import service condition ([32c59f04](https://github.com/pixelfed/pixelfed/commit/32c59f04))
|
||||
- Update AP helpers, more efficently update post count ([7caed381](https://github.com/pixelfed/pixelfed/commit/7caed381))
|
||||
- Update AP helpers, refactor post count decrement logic ([b81ae577](https://github.com/pixelfed/pixelfed/commit/b81ae577))
|
||||
- Update AP helpers, fix sensitive bug ([00ed330c](https://github.com/pixelfed/pixelfed/commit/00ed330c))
|
||||
- Update NotificationEpochUpdatePipeline, use more efficient query ([4d401389](https://github.com/pixelfed/pixelfed/commit/4d401389))
|
||||
- Update notification pipelines, fix non-local saving ([fa97a1f3](https://github.com/pixelfed/pixelfed/commit/fa97a1f3))
|
||||
- Update NodeinfoService, disable redirects ([240e6bbe](https://github.com/pixelfed/pixelfed/commit/240e6bbe))
|
||||
- Update Instance model, add entity casts ([289cad47](https://github.com/pixelfed/pixelfed/commit/289cad47))
|
||||
- Update FetchNodeinfoPipeline, use more efficient dispatch ([ac01f51a](https://github.com/pixelfed/pixelfed/commit/ac01f51a))
|
||||
- Update horizon.php config ([1e3acade](https://github.com/pixelfed/pixelfed/commit/1e3acade))
|
||||
- Update PublicApiController, consume InstanceService blocked domains for account and statuses endpoints ([01b33fb3](https://github.com/pixelfed/pixelfed/commit/01b33fb3))
|
||||
- Update ApiV1Controller, enforce blocked instance domain logic ([5b284cac](https://github.com/pixelfed/pixelfed/commit/5b284cac))
|
||||
- Update ApiV2Controller, add vapid key to instance object. Thanks thisismissem! ([4d02d6f1](https://github.com/pixelfed/pixelfed/commit/4d02d6f1))
|
||||
|
||||
## [v0.11.9 (2023-08-21)](https://github.com/pixelfed/pixelfed/compare/v0.11.8...v0.11.9)
|
||||
|
||||
### Added
|
||||
- Import from Instagram ([#4466](https://github.com/pixelfed/pixelfed/pull/4466)) ([cf3078c5](https://github.com/pixelfed/pixelfed/commit/cf3078c5))
|
||||
- Sign-in with Mastodon ([#4545](https://github.com/pixelfed/pixelfed/pull/4545)) ([45b9404e](https://github.com/pixelfed/pixelfed/commit/45b9404e))
|
||||
- Health check endpoint at /api/service/health-check ([ff58f970](https://github.com/pixelfed/pixelfed/commit/ff58f970))
|
||||
- Reblogs in home feed ([#4563](https://github.com/pixelfed/pixelfed/pull/4563)) ([b86d47bf](https://github.com/pixelfed/pixelfed/commit/b86d47bf))
|
||||
- Account Migrations ([#4578](https://github.com/pixelfed/pixelfed/pull/4578)) ([a9220e4e](https://github.com/pixelfed/pixelfed/commit/a9220e4e))
|
||||
|
||||
### Updates
|
||||
- Update Notifications.vue component, fix filtering logic to prevent endless spinner ([3df9b53f](https://github.com/pixelfed/pixelfed/commit/3df9b53f))
|
||||
- Update Direct Messages, fix api endpoint ([fe8728c0](https://github.com/pixelfed/pixelfed/commit/fe8728c0))
|
||||
- Update nginx config ([fbdc6358](https://github.com/pixelfed/pixelfed/commit/fbdc6358))
|
||||
- Update api routes, add DeprecatedEndpoint middleware. For more info, visit [pixelfed.org/kb/10404](https://pixelfed.org/kb/10404) ([a8453e77](https://github.com/pixelfed/pixelfed/commit/a8453e77))
|
||||
- Update admin dashboard, improve users section ([36b6bf48](https://github.com/pixelfed/pixelfed/commit/36b6bf48))
|
||||
- Update AdminApiController, add instance stats endpoint ([89c3710d](https://github.com/pixelfed/pixelfed/commit/89c3710d))
|
||||
- Update config, re-add `PF_MAX_USERS` .env variable to limit max users to 1000 by default ([a6d10f03](https://github.com/pixelfed/pixelfed/commit/a6d10f03))
|
||||
- Update AdminApiController, fix stats ([5c5541fc](https://github.com/pixelfed/pixelfed/commit/5c5541fc))
|
||||
- Update AdminApiController, include more data for getUser method ([4f850e54](https://github.com/pixelfed/pixelfed/commit/4f850e54))
|
||||
- Update AdminApiController, improve admin moderation tools ([763ce19a](https://github.com/pixelfed/pixelfed/commit/763ce19a))
|
||||
- Update ActivityPubFetchService, fix authorized_fetch compatibility. Closes #1850, #2713, #2935 ([63a7879c](https://github.com/pixelfed/pixelfed/commit/63a7879c))
|
||||
- Update IG Import commands, fix stalled import queue ([b18f3fba](https://github.com/pixelfed/pixelfed/commit/b18f3fba))
|
||||
- Update TransformImports command, improve handling of imported posts that already exist or are from deleted accounts ([892907d5](https://github.com/pixelfed/pixelfed/commit/892907d5))
|
||||
- Update console kernel, add import upload gc ([afe6948d](https://github.com/pixelfed/pixelfed/commit/afe6948d))
|
||||
- Update ImportService, filter deleted posts from getImportedPosts endpoint ([10dd348c](https://github.com/pixelfed/pixelfed/commit/10dd348c))
|
||||
- Update FixStatusCount, improve command and support remote count resync ([04f4f8ba](https://github.com/pixelfed/pixelfed/commit/04f4f8ba))
|
||||
- Update StatusRemoteUpdatePipeline, fix missing mime and size attributes that cause empty media previews on our mobile app ([ea54413e](https://github.com/pixelfed/pixelfed/commit/ea54413e))
|
||||
- Update ComposeModal.vue, fix scroll issue and dont hide scrollbar ([2d959fb3](https://github.com/pixelfed/pixelfed/commit/2d959fb3))
|
||||
- Update AccountImport, add select first 100 posts button ([625a76a5](https://github.com/pixelfed/pixelfed/commit/625a76a5))
|
||||
- Update ApiV1Controller, add include_reblogs attribute to home timeline ([37fd0342](https://github.com/pixelfed/pixelfed/commit/37fd0342))
|
||||
- Update rate limits, fixes #4537 ([1cc6274a](https://github.com/pixelfed/pixelfed/commit/1cc6274a))
|
||||
- Update Services, use zpopmin on predis ([4b2c66f5](https://github.com/pixelfed/pixelfed/commit/4b2c66f5))
|
||||
- Update Inbox, allow storing Create->Note activities without any local followers, disabled by default ([9fa6b3f7](https://github.com/pixelfed/pixelfed/commit/9fa6b3f7))
|
||||
- Update AP Helpers, preserve admin unlisted state before adding to NetworkTimelineService ([0704c7e0](https://github.com/pixelfed/pixelfed/commit/0704c7e0))
|
||||
- Update SearchApiV2Service, improve resolve query logic to better handle remote posts/profiles and local posts/profiles ([c61d0b91](https://github.com/pixelfed/pixelfed/commit/c61d0b91))
|
||||
- Update FollowPipeline, improve follower/following count calculation ([0b515767](https://github.com/pixelfed/pixelfed/commit/0b515767))
|
||||
- Update TransformImports command, increment status_count on profile model ([ba7551d8](https://github.com/pixelfed/pixelfed/commit/ba7551d8))
|
||||
- Update AP Helpers, improve url validation and add optional dns verification, disabled by default ([2bef3e41](https://github.com/pixelfed/pixelfed/commit/2bef3e41))
|
||||
- Update admin users blade view, show last_active_at and other info ([e0b48b29](https://github.com/pixelfed/pixelfed/commit/e0b48b29))
|
||||
- Update MediaStorageService, improve head header handling ([3590adbd](https://github.com/pixelfed/pixelfed/commit/3590adbd))
|
||||
- Update admin user view, improve previews ([ff2c16fe](https://github.com/pixelfed/pixelfed/commit/ff2c16fe))
|
||||
- Update FanoutDeletePipeline, fix AP object ([0d802c31](https://github.com/pixelfed/pixelfed/commit/0d802c31))
|
||||
- Update Remote Auth feature, fix custom domain bug and enforce banned domains ([acabf603](https://github.com/pixelfed/pixelfed/commit/acabf603))
|
||||
- Update StatusService, reduce cache ttl from 7 days to 6 hours ([59b64378](https://github.com/pixelfed/pixelfed/commit/59b64378))
|
||||
- Update ProfileController, allow albums in atom feed. Closes #4561. Fixes #4526 ([1c105a6c](https://github.com/pixelfed/pixelfed/commit/1c105a6c))
|
||||
- Update admin users view, fix website value. Closes #4557 ([c469d475](https://github.com/pixelfed/pixelfed/commit/c469d475))
|
||||
- Update StatusStatelessTransformer, allow unlisted reblogs ([1c13b518](https://github.com/pixelfed/pixelfed/commit/1c13b518))
|
||||
- Update ApiV1Controller, hydrate reblog state in home timeline ([13bdaa2e](https://github.com/pixelfed/pixelfed/commit/13bdaa2e))
|
||||
- Update Timeline component, improve reblog support ([29de91e5](https://github.com/pixelfed/pixelfed/commit/29de91e5))
|
||||
- Update timeline settings, add photo reblogs only option ([e2705b9a](https://github.com/pixelfed/pixelfed/commit/e2705b9a))
|
||||
- Update PostContent, add text cw warning ([911504fa](https://github.com/pixelfed/pixelfed/commit/911504fa))
|
||||
- Update ActivityPubFetchService, add validateUrl parameter to bypass url validation to fetch content from blocked instances ([3d1b6516](https://github.com/pixelfed/pixelfed/commit/3d1b6516))
|
||||
- Update RemoteStatusDelete pipeline ([71e92261](https://github.com/pixelfed/pixelfed/commit/71e92261))
|
||||
- Update RemoteStatusDelete pipeline ([fab8f25e](https://github.com/pixelfed/pixelfed/commit/fab8f25e))
|
||||
- Update RemoteStatusPipeline, fix reply check ([618b6727](https://github.com/pixelfed/pixelfed/commit/618b6727))
|
||||
- Update ApiV1Controller, add bookmarked to timeline entities ([ca746717](https://github.com/pixelfed/pixelfed/commit/ca746717))
|
||||
|
||||
## [v0.11.8 (2023-05-29)](https://github.com/pixelfed/pixelfed/compare/v0.11.7...v0.11.8)
|
||||
|
||||
### API Changes
|
||||
- Added `following_since` attribute to `/api/v1/accounts/relationships` endpoint when `_pe=1` (pixelfed entity) parameter is present ([992d910b](https://github.com/pixelfed/pixelfed/commit/992d910b))
|
||||
- Added `/api/v1.1/accounts/app/settings` endpoint and UserAppSettings model to store app specific settings ([a2305d5f](https://github.com/pixelfed/pixelfed/commit/a2305d5f))
|
||||
|
||||
### Added
|
||||
- Post edits ([#4416](https://github.com/pixelfed/pixelfed/pull/4416)) ([98cf8f3](https://github.com/pixelfed/pixelfed/commit/98cf8f3))
|
||||
|
||||
### Updates
|
||||
- Update StatusService, fix bug in getFull method ([4d8b4dcf](https://github.com/pixelfed/pixelfed/commit/4d8b4dcf))
|
||||
- Update Config, bump version for post edit support without having to clear cache ([c0190d84](https://github.com/pixelfed/pixelfed/commit/c0190d84))
|
||||
- Update EditHistoryModal, fix caption rendering ([0f803446](https://github.com/pixelfed/pixelfed/commit/0f803446))
|
||||
- Update StatusRemoteUpdatePipeline, fix typo ([109d0419](https://github.com/pixelfed/pixelfed/commit/109d0419))
|
||||
- Update StatusActivityPubDeliver, fix delivery addressing ([1f2183ee](https://github.com/pixelfed/pixelfed/commit/1f2183ee))
|
||||
- Update UpdateStatusService, fix formatting issue. Fixes #4423 ([4479055e](https://github.com/pixelfed/pixelfed/commit/4479055e))
|
||||
- Update nginx config ([ee3b6e09](https://github.com/pixelfed/pixelfed/commit/ee3b6e09))
|
||||
- Update Status model, increase max mentions, hashtags and links ([1430f532](https://github.com/pixelfed/pixelfed/commit/1430f532))
|
||||
|
||||
## [v0.11.7 (2023-05-24)](https://github.com/pixelfed/pixelfed/compare/v0.11.6...v0.11.7)
|
||||
|
||||
### API Changes
|
||||
- Added [/api/v1/followed_tags](https://docs.joinmastodon.org/methods/followed_tags/) api endpoint ([175a8486](https://github.com/pixelfed/pixelfed/commit/175a8486))
|
||||
- Added [/api/v1/tags/:id/follow](https://docs.joinmastodon.org/methods/tags/#follow) and [/api/v1/tags/:id/unfollow](https://docs.joinmastodon.org/methods/tags/#unfollow) api endpoints ([4d997bb9](https://github.com/pixelfed/pixelfed/commit/4d997bb9))
|
||||
- Added [/api/v1/tags/:id](https://docs.joinmastodon.org/methods/tags/) api endpoint ([521b3b4c](https://github.com/pixelfed/pixelfed/commit/521b3b4c))
|
||||
- Added `only_media` support to /api/v1/timelines/tag/:id api endpoint ([b5fe956a](https://github.com/pixelfed/pixelfed/commit/b5fe956a))
|
||||
- Added /api/v2/instance api endpoint ([167dbcdd](https://github.com/pixelfed/pixelfed/commit/167dbcdd))
|
||||
- Removed api endpoint cloud ip block logic ([6a2daf1f](https://github.com/pixelfed/pixelfed/commit/6a2daf1f))
|
||||
- Added idempotency-key support to /api/v1/statuses endpoint ([c54cdd3e](https://github.com/pixelfed/pixelfed/commit/c54cdd3e))
|
||||
|
||||
### Added
|
||||
- Added store remote media on S3 config setting, disabled by default ([51768083](https://github.com/pixelfed/pixelfed/commit/51768083))
|
||||
- Added Autospam Advanced Detection ([132a58de](https://github.com/pixelfed/pixelfed/commit/132a58de))
|
||||
|
||||
### Updates
|
||||
- Update admin dashboard, fix search and dropdown menu ([dac0d083](https://github.com/pixelfed/pixelfed/commit/dac0d083))
|
||||
- Update sudo mode view, fix trusted device checkbox ([8ef900bf](https://github.com/pixelfed/pixelfed/commit/8ef900bf))
|
||||
- Update SearchApiV2Service, improve postgres support ([666e5732](https://github.com/pixelfed/pixelfed/commit/666e5732))
|
||||
- Update StoryController, show active self stories on home timeline ([633351f6](https://github.com/pixelfed/pixelfed/commit/633351f6))
|
||||
- Update ApiV1Controller, fix trending accounts format. Closes #4356 ([37bd2ee5](https://github.com/pixelfed/pixelfed/commit/37bd2ee5))
|
||||
- Update instance config, enable config cache by default ([970f77b0](https://github.com/pixelfed/pixelfed/commit/970f77b0))
|
||||
- Update Admin Dashboard, allow admins to designate an admin account for the landing page and instance api endpoint ([6ea2bdc7](https://github.com/pixelfed/pixelfed/commit/6ea2bdc7))
|
||||
- Update config, enable oauth by default ([6a2e9e8f](https://github.com/pixelfed/pixelfed/commit/6a2e9e8f))
|
||||
- Update StatusService, fix missing account condition ([f48daab3](https://github.com/pixelfed/pixelfed/commit/f48daab3))
|
||||
- Update ProfileService, add softFail param ([6bc20a37](https://github.com/pixelfed/pixelfed/commit/6bc20a37))
|
||||
- Update MediaTagService, fix ProfileService to soft fail on missing or deleted accounts ([df444851](https://github.com/pixelfed/pixelfed/commit/df444851))
|
||||
- Update LikeService, improve likedBy logic to soft fail on missing or deleted accounts ([91ba1398](https://github.com/pixelfed/pixelfed/commit/91ba1398))
|
||||
- Update StatusTransformers, fix ProfileService to soft fail on missing or deleted accounts ([43d3aa2b](https://github.com/pixelfed/pixelfed/commit/43d3aa2b))
|
||||
- Update ApiV1Controller, fix hashtag timeline ([fc1a385c](https://github.com/pixelfed/pixelfed/commit/fc1a385c))
|
||||
- Update settings view, add fallback avatar ([1a83c585](https://github.com/pixelfed/pixelfed/commit/1a83c585))
|
||||
- Update HashtagFollow model, add MAX_LIMIT of 250 tags per account ([ed352141](https://github.com/pixelfed/pixelfed/commit/ed352141))
|
||||
- Update Notification logic, remove message and rendered fields ([6cdb5bc6](https://github.com/pixelfed/pixelfed/commit/6cdb5bc6))
|
||||
- Update InstanceService, fix banner blurhash memory bug ([3aad75ab](https://github.com/pixelfed/pixelfed/commit/3aad75ab))
|
||||
- Update models, remove deprecated toText and toHtml method ([ea943333](https://github.com/pixelfed/pixelfed/commit/ea943333))
|
||||
- Update Notification components, add autospam notification support ([0d3b4bc2](https://github.com/pixelfed/pixelfed/commit/0d3b4bc2))
|
||||
- Update AutoSpam Bouncer, generate notification on positive detections ([d5f63f8a](https://github.com/pixelfed/pixelfed/commit/d5f63f8a))
|
||||
- Update admin autospam apis, remove autospam warning notifications when appropriate ([588ca653](https://github.com/pixelfed/pixelfed/commit/588ca653))
|
||||
- Update StatusEntityLexer, stop saving entities ([a91a5e48](https://github.com/pixelfed/pixelfed/commit/a91a5e48))
|
||||
- Update UserCreate command, fix is_admin flag ([ad25ed67](https://github.com/pixelfed/pixelfed/commit/ad25ed67))
|
||||
- Update Bouncer, adjust advanced Autospam logic ([18cddd43](https://github.com/pixelfed/pixelfed/commit/18cddd43))
|
||||
- Update atom view, fix atom feed bug ([63b72c42](https://github.com/pixelfed/pixelfed/commit/63b72c42))
|
||||
- Update StatusController, disable post embeds from spam accounts ([c167af43](https://github.com/pixelfed/pixelfed/commit/c167af43))
|
||||
- Update ProfileController, require login to view spam accounts, and disable profile embeds and atom feeds for spam accounts ([dd2f5bb9](https://github.com/pixelfed/pixelfed/commit/dd2f5bb9))
|
||||
- Update Settings, allow users to disable atom feeds ([3662d3de](https://github.com/pixelfed/pixelfed/commit/3662d3de))
|
||||
- Update ApiV1Controller, filter muted/blocked accounts from tag timeline ([f42c1140](https://github.com/pixelfed/pixelfed/commit/f42c1140))
|
||||
- Update admin moderation logic, only re-add top level posts ([c6ffda96](https://github.com/pixelfed/pixelfed/commit/c6ffda96))
|
||||
- Update admin dashboard, add mass account deletes ([b8426cce](https://github.com/pixelfed/pixelfed/commit/b8426cce))
|
||||
- Update scheduler, fix S3 media garbage collection not being executed when cloud storage is enabled via dashboard without .env/config being enabled ([adb070f1](https://github.com/pixelfed/pixelfed/commit/adb070f1))
|
||||
- Update MediaController, add fallback for local files that are later stored on S3 but still are referenced in cached objects remotely ([4973cb46](https://github.com/pixelfed/pixelfed/commit/4973cb46))
|
||||
- Update PublicTimelineService, improve warmCache query ([9f901d65](https://github.com/pixelfed/pixelfed/commit/9f901d65))
|
||||
- Update AP Inbox, fix delete handling ([2800c888](https://github.com/pixelfed/pixelfed/commit/2800c888))
|
||||
- Update login/register views and captcha config, enable login or register captchas or both ([c071c719](https://github.com/pixelfed/pixelfed/commit/c071c719))
|
||||
- Update login form, allow admins to enable captcha after X failed attempts. Admins can set the number of attempts before captcha is shown, default is 2 attempts before captcha is required ([221ddce0](https://github.com/pixelfed/pixelfed/commit/221ddce0))
|
||||
|
||||
## [v0.11.6 (2023-05-03)](https://github.com/pixelfed/pixelfed/compare/v0.11.5...v0.11.6)
|
||||
|
||||
|
@ -1031,7 +1405,7 @@
|
|||
- Added post embeds ([1fecf717](https://github.com/pixelfed/pixelfed/commit/1fecf717))
|
||||
- Added profile embeds ([fb7a3cf0](https://github.com/pixelfed/pixelfed/commit/fb7a3cf0))
|
||||
- Added Force MetroUI labs experiment ([#1889](https://github.com/pixelfed/pixelfed/pull/1889))
|
||||
- Added Stories, to enable add ```STORIES_ENABLED=true``` to ```.env``` and run ```php artisan config:cache && php artisan cache:clear```. If opcache is enabled you may need to reload the web server.
|
||||
- Added Stories, to enable add ```STORIES_ENABLED=true``` to ```.env``` and run ```php artisan config:cache && php artisan cache:clear```. If opcache is enabled you may need to reload the web server.
|
||||
|
||||
### Fixed
|
||||
- Fixed like and share/reblog count on profiles ([86cb7d09](https://github.com/pixelfed/pixelfed/commit/86cb7d09))
|
||||
|
|
|
@ -0,0 +1,18 @@
|
|||
# See: https://docs.github.com/en/repositories/managing-your-repositorys-settings-and-features/customizing-your-repository/about-code-owners
|
||||
|
||||
# These owners will be the default owners for everything in
|
||||
# the repo. Unless a later match takes precedence,
|
||||
* @dansup
|
||||
|
||||
# Docker related files
|
||||
.editorconfig @jippi @dansup
|
||||
.env @jippi @dansup
|
||||
.env.* @jippi @dansup
|
||||
.hadolint.yaml @jippi @dansup
|
||||
.shellcheckrc @jippi @dansup
|
||||
/.github/ @jippi @dansup
|
||||
/docker/ @jippi @dansup
|
||||
/tests/ @jippi @dansup
|
||||
docker-compose.migrate.yml @jippi @dansup
|
||||
docker-compose.yml @jippi @dansup
|
||||
goss.yaml @jippi @dansup
|
|
@ -0,0 +1,307 @@
|
|||
# syntax=docker/dockerfile:1
|
||||
# See https://hub.docker.com/r/docker/dockerfile
|
||||
|
||||
#######################################################
|
||||
# Configuration
|
||||
#######################################################
|
||||
|
||||
# See: https://github.com/mlocati/docker-php-extension-installer
|
||||
ARG DOCKER_PHP_EXTENSION_INSTALLER_VERSION="2.1.80"
|
||||
|
||||
# See: https://github.com/composer/composer
|
||||
ARG COMPOSER_VERSION="2.6"
|
||||
|
||||
# See: https://nginx.org/
|
||||
ARG NGINX_VERSION="1.25.3"
|
||||
|
||||
# See: https://github.com/ddollar/forego
|
||||
ARG FOREGO_VERSION="0.17.2"
|
||||
|
||||
# See: https://github.com/hairyhenderson/gomplate
|
||||
ARG GOMPLATE_VERSION="v3.11.6"
|
||||
|
||||
# See: https://github.com/jippi/dottie
|
||||
ARG DOTTIE_VERSION="v0.9.5"
|
||||
|
||||
###
|
||||
# PHP base configuration
|
||||
###
|
||||
|
||||
# See: https://hub.docker.com/_/php/tags
|
||||
ARG PHP_VERSION="8.1"
|
||||
|
||||
# See: https://github.com/docker-library/docs/blob/master/php/README.md#image-variants
|
||||
ARG PHP_BASE_TYPE="apache"
|
||||
ARG PHP_DEBIAN_RELEASE="bullseye"
|
||||
|
||||
ARG RUNTIME_UID=33 # often called 'www-data'
|
||||
ARG RUNTIME_GID=33 # often called 'www-data'
|
||||
|
||||
# APT extra packages
|
||||
ARG APT_PACKAGES_EXTRA=
|
||||
|
||||
# Extensions installed via [pecl install]
|
||||
# ! NOTE: imagick is installed from [master] branch on GitHub due to 8.3 bug on ARM that haven't
|
||||
# ! been released yet (after +10 months)!
|
||||
# ! See: https://github.com/Imagick/imagick/pull/641
|
||||
ARG PHP_PECL_EXTENSIONS="redis https://codeload.github.com/Imagick/imagick/tar.gz/28f27044e435a2b203e32675e942eb8de620ee58"
|
||||
ARG PHP_PECL_EXTENSIONS_EXTRA=
|
||||
|
||||
# Extensions installed via [docker-php-ext-install]
|
||||
ARG PHP_EXTENSIONS="intl bcmath zip pcntl exif curl gd"
|
||||
ARG PHP_EXTENSIONS_EXTRA=""
|
||||
ARG PHP_EXTENSIONS_DATABASE="pdo_pgsql pdo_mysql pdo_sqlite"
|
||||
|
||||
# GPG key for nginx apt repository
|
||||
ARG NGINX_GPGKEY="573BFD6B3D8FBC641079A6ABABF5BD827BD9BF62"
|
||||
|
||||
# GPP key path for nginx apt repository
|
||||
ARG NGINX_GPGKEY_PATH="/usr/share/keyrings/nginx-archive-keyring.gpg"
|
||||
|
||||
#######################################################
|
||||
# Docker "copy from" images
|
||||
#######################################################
|
||||
|
||||
# Composer docker image from Docker Hub
|
||||
#
|
||||
# NOTE: Docker will *not* pull this image unless it's referenced (via build target)
|
||||
FROM composer:${COMPOSER_VERSION} AS composer-image
|
||||
|
||||
# php-extension-installer image from Docker Hub
|
||||
#
|
||||
# NOTE: Docker will *not* pull this image unless it's referenced (via build target)
|
||||
FROM mlocati/php-extension-installer:${DOCKER_PHP_EXTENSION_INSTALLER_VERSION} AS php-extension-installer
|
||||
|
||||
# nginx webserver from Docker Hub.
|
||||
# Used to copy some docker-entrypoint files for [nginx-runtime]
|
||||
#
|
||||
# NOTE: Docker will *not* pull this image unless it's referenced (via build target)
|
||||
FROM nginx:${NGINX_VERSION} AS nginx-image
|
||||
|
||||
# Forego is a Procfile "runner" that makes it trival to run multiple
|
||||
# processes under a simple init / PID 1 process.
|
||||
#
|
||||
# NOTE: Docker will *not* pull this image unless it's referenced (via build target)
|
||||
#
|
||||
# See: https://github.com/nginx-proxy/forego
|
||||
FROM nginxproxy/forego:${FOREGO_VERSION}-debian AS forego-image
|
||||
|
||||
# Dottie makes working with .env files easier and safer
|
||||
#
|
||||
# NOTE: Docker will *not* pull this image unless it's referenced (via build target)
|
||||
#
|
||||
# See: https://github.com/jippi/dottie
|
||||
FROM ghcr.io/jippi/dottie:${DOTTIE_VERSION} AS dottie-image
|
||||
|
||||
# gomplate-image grabs the gomplate binary from GitHub releases
|
||||
#
|
||||
# It's in its own layer so it can be fetched in parallel with other build steps
|
||||
FROM php:${PHP_VERSION}-${PHP_BASE_TYPE}-${PHP_DEBIAN_RELEASE} AS gomplate-image
|
||||
|
||||
ARG TARGETARCH
|
||||
ARG TARGETOS
|
||||
ARG GOMPLATE_VERSION
|
||||
|
||||
RUN set -ex \
|
||||
&& curl \
|
||||
--silent \
|
||||
--show-error \
|
||||
--location \
|
||||
--output /usr/local/bin/gomplate \
|
||||
https://github.com/hairyhenderson/gomplate/releases/download/${GOMPLATE_VERSION}/gomplate_${TARGETOS}-${TARGETARCH} \
|
||||
&& chmod +x /usr/local/bin/gomplate \
|
||||
&& /usr/local/bin/gomplate --version
|
||||
|
||||
#######################################################
|
||||
# Base image
|
||||
#######################################################
|
||||
|
||||
FROM php:${PHP_VERSION}-${PHP_BASE_TYPE}-${PHP_DEBIAN_RELEASE} AS base
|
||||
|
||||
ARG BUILDKIT_SBOM_SCAN_STAGE="true"
|
||||
|
||||
ARG APT_PACKAGES_EXTRA
|
||||
ARG PHP_DEBIAN_RELEASE
|
||||
ARG PHP_VERSION
|
||||
ARG RUNTIME_GID
|
||||
ARG RUNTIME_UID
|
||||
ARG TARGETPLATFORM
|
||||
|
||||
ENV DEBIAN_FRONTEND="noninteractive"
|
||||
|
||||
# Ensure we run all scripts through 'bash' rather than 'sh'
|
||||
SHELL ["/bin/bash", "-c"]
|
||||
|
||||
RUN set -ex \
|
||||
&& mkdir -pv /var/www/ \
|
||||
&& chown -R ${RUNTIME_UID}:${RUNTIME_GID} /var/www
|
||||
|
||||
WORKDIR /var/www/
|
||||
|
||||
ENV APT_PACKAGES_EXTRA=${APT_PACKAGES_EXTRA}
|
||||
|
||||
# Install and configure base layer
|
||||
COPY docker/shared/root/docker/install/base.sh /docker/install/base.sh
|
||||
|
||||
RUN --mount=type=cache,id=pixelfed-apt-${PHP_VERSION}-${PHP_DEBIAN_RELEASE}-${TARGETPLATFORM},sharing=locked,target=/var/lib/apt \
|
||||
--mount=type=cache,id=pixelfed-apt-cache-${PHP_VERSION}-${PHP_DEBIAN_RELEASE}-${TARGETPLATFORM},sharing=locked,target=/var/cache/apt \
|
||||
/docker/install/base.sh
|
||||
|
||||
#######################################################
|
||||
# PHP: extensions
|
||||
#######################################################
|
||||
|
||||
FROM base AS php-extensions
|
||||
|
||||
ARG PHP_DEBIAN_RELEASE
|
||||
ARG PHP_EXTENSIONS
|
||||
ARG PHP_EXTENSIONS_DATABASE
|
||||
ARG PHP_EXTENSIONS_EXTRA
|
||||
ARG PHP_PECL_EXTENSIONS
|
||||
ARG PHP_PECL_EXTENSIONS_EXTRA
|
||||
ARG PHP_VERSION
|
||||
ARG TARGETPLATFORM
|
||||
|
||||
COPY --from=php-extension-installer /usr/bin/install-php-extensions /usr/local/bin/
|
||||
|
||||
COPY docker/shared/root/docker/install/php-extensions.sh /docker/install/php-extensions.sh
|
||||
|
||||
RUN --mount=type=cache,id=pixelfed-pear-${PHP_VERSION}-${PHP_DEBIAN_RELEASE}-${TARGETPLATFORM},sharing=locked,target=/tmp/pear \
|
||||
--mount=type=cache,id=pixelfed-apt-${PHP_VERSION}-${PHP_DEBIAN_RELEASE}-${TARGETPLATFORM},sharing=locked,target=/var/lib/apt \
|
||||
--mount=type=cache,id=pixelfed-apt-cache-${PHP_VERSION}-${PHP_DEBIAN_RELEASE}-${TARGETPLATFORM},sharing=locked,target=/var/cache/apt \
|
||||
PHP_EXTENSIONS=${PHP_EXTENSIONS} \
|
||||
PHP_EXTENSIONS_DATABASE=${PHP_EXTENSIONS_DATABASE} \
|
||||
PHP_EXTENSIONS_EXTRA=${PHP_EXTENSIONS_EXTRA} \
|
||||
PHP_PECL_EXTENSIONS=${PHP_PECL_EXTENSIONS} \
|
||||
PHP_PECL_EXTENSIONS_EXTRA=${PHP_PECL_EXTENSIONS_EXTRA} \
|
||||
/docker/install/php-extensions.sh
|
||||
|
||||
#######################################################
|
||||
# PHP: composer and source code
|
||||
#######################################################
|
||||
|
||||
FROM php-extensions AS composer-and-src
|
||||
|
||||
ARG PHP_VERSION
|
||||
ARG PHP_DEBIAN_RELEASE
|
||||
ARG RUNTIME_UID
|
||||
ARG RUNTIME_GID
|
||||
ARG TARGETPLATFORM
|
||||
|
||||
# Make sure composer cache is targeting our cache mount later
|
||||
ENV COMPOSER_CACHE_DIR="/cache/composer"
|
||||
|
||||
# Don't enforce any memory limits for composer
|
||||
ENV COMPOSER_MEMORY_LIMIT=-1
|
||||
|
||||
# Disable interactvitity from composer
|
||||
ENV COMPOSER_NO_INTERACTION=1
|
||||
|
||||
# Copy composer from https://hub.docker.com/_/composer
|
||||
COPY --link --from=composer-image /usr/bin/composer /usr/bin/composer
|
||||
|
||||
#! Changing user to runtime user
|
||||
USER ${RUNTIME_UID}:${RUNTIME_GID}
|
||||
|
||||
# Install composer dependencies
|
||||
# NOTE: we skip the autoloader generation here since we don't have all files avaliable (yet)
|
||||
RUN --mount=type=cache,id=pixelfed-composer-${PHP_VERSION},sharing=locked,target=/cache/composer \
|
||||
--mount=type=bind,source=composer.json,target=/var/www/composer.json \
|
||||
--mount=type=bind,source=composer.lock,target=/var/www/composer.lock \
|
||||
set -ex \
|
||||
&& composer install --prefer-dist --no-autoloader --ignore-platform-reqs
|
||||
|
||||
# Copy all other files over
|
||||
COPY --chown=${RUNTIME_UID}:${RUNTIME_GID} . /var/www/
|
||||
|
||||
#######################################################
|
||||
# Runtime: base
|
||||
#######################################################
|
||||
|
||||
FROM php-extensions AS shared-runtime
|
||||
|
||||
ARG RUNTIME_GID
|
||||
ARG RUNTIME_UID
|
||||
|
||||
ENV RUNTIME_UID=${RUNTIME_UID}
|
||||
ENV RUNTIME_GID=${RUNTIME_GID}
|
||||
|
||||
COPY --link --from=forego-image /usr/local/bin/forego /usr/local/bin/forego
|
||||
COPY --link --from=dottie-image /dottie /usr/local/bin/dottie
|
||||
COPY --link --from=gomplate-image /usr/local/bin/gomplate /usr/local/bin/gomplate
|
||||
COPY --link --from=composer-image /usr/bin/composer /usr/bin/composer
|
||||
COPY --link --from=composer-and-src --chown=${RUNTIME_UID}:${RUNTIME_GID} /var/www /var/www
|
||||
|
||||
#! Changing user to runtime user
|
||||
USER ${RUNTIME_UID}:${RUNTIME_GID}
|
||||
|
||||
# Generate optimized autoloader now that we have all files around
|
||||
RUN set -ex \
|
||||
&& ENABLE_CONFIG_CACHE=false composer dump-autoload --optimize
|
||||
|
||||
USER root
|
||||
|
||||
# for detail why storage is copied this way, pls refer to https://github.com/pixelfed/pixelfed/pull/2137#discussion_r434468862
|
||||
RUN set -ex \
|
||||
&& cp --recursive --link --preserve=all storage storage.skel \
|
||||
&& rm -rf html && ln -s public html
|
||||
|
||||
COPY docker/shared/root /
|
||||
|
||||
ENTRYPOINT ["/docker/entrypoint.sh"]
|
||||
|
||||
#######################################################
|
||||
# Runtime: apache
|
||||
#######################################################
|
||||
|
||||
FROM shared-runtime AS apache-runtime
|
||||
|
||||
COPY docker/apache/root /
|
||||
|
||||
RUN set -ex \
|
||||
&& a2enmod rewrite remoteip proxy proxy_http \
|
||||
&& a2enconf remoteip
|
||||
|
||||
CMD ["apache2-foreground"]
|
||||
|
||||
#######################################################
|
||||
# Runtime: fpm
|
||||
#######################################################
|
||||
|
||||
FROM shared-runtime AS fpm-runtime
|
||||
|
||||
COPY docker/fpm/root /
|
||||
|
||||
CMD ["php-fpm"]
|
||||
|
||||
#######################################################
|
||||
# Runtime: nginx
|
||||
#######################################################
|
||||
|
||||
FROM shared-runtime AS nginx-runtime
|
||||
|
||||
ARG NGINX_GPGKEY
|
||||
ARG NGINX_GPGKEY_PATH
|
||||
ARG NGINX_VERSION
|
||||
ARG PHP_DEBIAN_RELEASE
|
||||
ARG PHP_VERSION
|
||||
ARG TARGETPLATFORM
|
||||
|
||||
# Install nginx dependencies
|
||||
RUN --mount=type=cache,id=pixelfed-apt-lists-${PHP_VERSION}-${PHP_DEBIAN_RELEASE}-${TARGETPLATFORM},sharing=locked,target=/var/lib/apt/lists \
|
||||
--mount=type=cache,id=pixelfed-apt-cache-${PHP_VERSION}-${PHP_DEBIAN_RELEASE}-${TARGETPLATFORM},sharing=locked,target=/var/cache/apt \
|
||||
set -ex \
|
||||
&& gpg1 --keyserver "hkp://keyserver.ubuntu.com:80" --keyserver-options timeout=10 --recv-keys "${NGINX_GPGKEY}" \
|
||||
&& gpg1 --export "$NGINX_GPGKEY" > "$NGINX_GPGKEY_PATH" \
|
||||
&& echo "deb [signed-by=${NGINX_GPGKEY_PATH}] https://nginx.org/packages/mainline/debian/ ${PHP_DEBIAN_RELEASE} nginx" >> /etc/apt/sources.list.d/nginx.list \
|
||||
&& apt-get update \
|
||||
&& apt-get install -y --no-install-recommends nginx=${NGINX_VERSION}*
|
||||
|
||||
# copy docker entrypoints from the *real* nginx image directly
|
||||
COPY --link --from=nginx-image /docker-entrypoint.d /docker/entrypoint.d/
|
||||
COPY docker/nginx/root /
|
||||
COPY docker/nginx/Procfile .
|
||||
|
||||
STOPSIGNAL SIGQUIT
|
||||
|
||||
CMD ["forego", "start", "-r"]
|
|
@ -18,8 +18,7 @@ class BearerTokenResponse extends \League\OAuth2\Server\ResponseTypes\BearerToke
|
|||
protected function getExtraParams(AccessTokenEntityInterface $accessToken)
|
||||
{
|
||||
return [
|
||||
'created_at' => time(),
|
||||
'scope' => 'read write follow push'
|
||||
'created_at' => time(),
|
||||
];
|
||||
}
|
||||
}
|
||||
|
|
|
@ -0,0 +1,57 @@
|
|||
<?php
|
||||
|
||||
namespace App\Console\Commands;
|
||||
|
||||
use Illuminate\Console\Command;
|
||||
use App\Services\AccountService;
|
||||
use App\Services\Account\AccountStatService;
|
||||
use App\Status;
|
||||
use App\Profile;
|
||||
|
||||
class AccountPostCountStatUpdate extends Command
|
||||
{
|
||||
/**
|
||||
* The name and signature of the console command.
|
||||
*
|
||||
* @var string
|
||||
*/
|
||||
protected $signature = 'app:account-post-count-stat-update';
|
||||
|
||||
/**
|
||||
* The console command description.
|
||||
*
|
||||
* @var string
|
||||
*/
|
||||
protected $description = 'Update post counts from recent activities';
|
||||
|
||||
/**
|
||||
* Execute the console command.
|
||||
*/
|
||||
public function handle()
|
||||
{
|
||||
$ids = AccountStatService::getAllPostCountIncr();
|
||||
if(!$ids || !count($ids)) {
|
||||
return;
|
||||
}
|
||||
foreach($ids as $id) {
|
||||
$acct = AccountService::get($id, true);
|
||||
if(!$acct) {
|
||||
AccountStatService::removeFromPostCount($id);
|
||||
continue;
|
||||
}
|
||||
$statusCount = Status::whereProfileId($id)->count();
|
||||
if($statusCount != $acct['statuses_count']) {
|
||||
$profile = Profile::find($id);
|
||||
if(!$profile) {
|
||||
AccountStatService::removeFromPostCount($id);
|
||||
continue;
|
||||
}
|
||||
$profile->status_count = $statusCount;
|
||||
$profile->save();
|
||||
AccountService::del($id);
|
||||
}
|
||||
AccountStatService::removeFromPostCount($id);
|
||||
}
|
||||
return;
|
||||
}
|
||||
}
|
|
@ -0,0 +1,106 @@
|
|||
<?php
|
||||
|
||||
namespace App\Console\Commands;
|
||||
|
||||
use Illuminate\Console\Command;
|
||||
use App\User;
|
||||
use App\Models\DefaultDomainBlock;
|
||||
use App\Models\UserDomainBlock;
|
||||
use function Laravel\Prompts\text;
|
||||
use function Laravel\Prompts\confirm;
|
||||
use function Laravel\Prompts\progress;
|
||||
|
||||
class AddUserDomainBlock extends Command
|
||||
{
|
||||
/**
|
||||
* The name and signature of the console command.
|
||||
*
|
||||
* @var string
|
||||
*/
|
||||
protected $signature = 'app:add-user-domain-block';
|
||||
|
||||
/**
|
||||
* The console command description.
|
||||
*
|
||||
* @var string
|
||||
*/
|
||||
protected $description = 'Apply a domain block to all users';
|
||||
|
||||
/**
|
||||
* Execute the console command.
|
||||
*/
|
||||
public function handle()
|
||||
{
|
||||
$domain = text('Enter domain you want to block');
|
||||
$domain = strtolower($domain);
|
||||
$domain = $this->validateDomain($domain);
|
||||
if(!$domain || empty($domain)) {
|
||||
$this->error('Invalid domain');
|
||||
return;
|
||||
}
|
||||
$this->processBlocks($domain);
|
||||
return;
|
||||
}
|
||||
|
||||
protected function validateDomain($domain)
|
||||
{
|
||||
if(!strpos($domain, '.')) {
|
||||
return;
|
||||
}
|
||||
|
||||
if(str_starts_with($domain, 'https://')) {
|
||||
$domain = str_replace('https://', '', $domain);
|
||||
}
|
||||
|
||||
if(str_starts_with($domain, 'http://')) {
|
||||
$domain = str_replace('http://', '', $domain);
|
||||
}
|
||||
|
||||
$domain = strtolower(parse_url('https://' . $domain, PHP_URL_HOST));
|
||||
|
||||
$valid = filter_var($domain, FILTER_VALIDATE_DOMAIN, FILTER_FLAG_HOSTNAME|FILTER_NULL_ON_FAILURE);
|
||||
if(!$valid) {
|
||||
return;
|
||||
}
|
||||
|
||||
if($domain === config('pixelfed.domain.app')) {
|
||||
$this->error('Invalid domain');
|
||||
return;
|
||||
}
|
||||
|
||||
$confirmed = confirm('Are you sure you want to block ' . $domain . '?');
|
||||
if(!$confirmed) {
|
||||
return;
|
||||
}
|
||||
|
||||
return $domain;
|
||||
}
|
||||
|
||||
protected function processBlocks($domain)
|
||||
{
|
||||
DefaultDomainBlock::updateOrCreate([
|
||||
'domain' => $domain
|
||||
]);
|
||||
progress(
|
||||
label: 'Updating user domain blocks...',
|
||||
steps: User::lazyById(500),
|
||||
callback: fn ($user) => $this->performTask($user, $domain),
|
||||
);
|
||||
}
|
||||
|
||||
protected function performTask($user, $domain)
|
||||
{
|
||||
if(!$user->profile_id || $user->delete_after) {
|
||||
return;
|
||||
}
|
||||
|
||||
if($user->status != null && $user->status != 'disabled') {
|
||||
return;
|
||||
}
|
||||
|
||||
UserDomainBlock::updateOrCreate([
|
||||
'profile_id' => $user->profile_id,
|
||||
'domain' => $domain
|
||||
]);
|
||||
}
|
||||
}
|
|
@ -82,7 +82,7 @@ class AvatarStorage extends Command
|
|||
|
||||
$this->line(' ');
|
||||
|
||||
if(config_cache('pixelfed.cloud_storage')) {
|
||||
if((bool) config_cache('pixelfed.cloud_storage')) {
|
||||
$this->info('✅ - Cloud storage configured');
|
||||
$this->line(' ');
|
||||
}
|
||||
|
@ -92,7 +92,7 @@ class AvatarStorage extends Command
|
|||
$this->line(' ');
|
||||
}
|
||||
|
||||
if(config_cache('pixelfed.cloud_storage') && config('instance.avatar.local_to_cloud')) {
|
||||
if((bool) config_cache('pixelfed.cloud_storage') && config('instance.avatar.local_to_cloud')) {
|
||||
$disk = Storage::disk(config_cache('filesystems.cloud'));
|
||||
$exists = $disk->exists('cache/avatars/default.jpg');
|
||||
$state = $exists ? '✅' : '❌';
|
||||
|
@ -100,7 +100,7 @@ class AvatarStorage extends Command
|
|||
$this->info($msg);
|
||||
}
|
||||
|
||||
$options = config_cache('pixelfed.cloud_storage') && config('instance.avatar.local_to_cloud') ?
|
||||
$options = (bool) config_cache('pixelfed.cloud_storage') && config('instance.avatar.local_to_cloud') ?
|
||||
[
|
||||
'Cancel',
|
||||
'Upload default avatar to cloud',
|
||||
|
@ -164,7 +164,7 @@ class AvatarStorage extends Command
|
|||
|
||||
protected function uploadAvatarsToCloud()
|
||||
{
|
||||
if(!config_cache('pixelfed.cloud_storage') || !config('instance.avatar.local_to_cloud')) {
|
||||
if(!(bool) config_cache('pixelfed.cloud_storage') || !config('instance.avatar.local_to_cloud')) {
|
||||
$this->error('Enable cloud storage and avatar cloud storage to perform this action');
|
||||
return;
|
||||
}
|
||||
|
@ -213,7 +213,7 @@ class AvatarStorage extends Command
|
|||
return;
|
||||
}
|
||||
|
||||
if(config_cache('pixelfed.cloud_storage') == false && config_cache('federation.avatars.store_local') == false) {
|
||||
if((bool) config_cache('pixelfed.cloud_storage') == false && config_cache('federation.avatars.store_local') == false) {
|
||||
$this->error('You have cloud storage disabled and local avatar storage disabled, we cannot refetch avatars.');
|
||||
return;
|
||||
}
|
||||
|
|
|
@ -0,0 +1,115 @@
|
|||
<?php
|
||||
|
||||
namespace App\Console\Commands;
|
||||
|
||||
use Illuminate\Console\Command;
|
||||
use Cache;
|
||||
use Storage;
|
||||
use App\Avatar;
|
||||
use App\Jobs\AvatarPipeline\AvatarStorageCleanup;
|
||||
|
||||
class AvatarStorageDeepClean extends Command
|
||||
{
|
||||
/**
|
||||
* The name and signature of the console command.
|
||||
*
|
||||
* @var string
|
||||
*/
|
||||
protected $signature = 'avatar:storage-deep-clean';
|
||||
|
||||
/**
|
||||
* The console command description.
|
||||
*
|
||||
* @var string
|
||||
*/
|
||||
protected $description = 'Cleanup avatar storage';
|
||||
|
||||
protected $shouldKeepRunning = true;
|
||||
protected $counter = 0;
|
||||
|
||||
/**
|
||||
* Execute the console command.
|
||||
*/
|
||||
public function handle(): void
|
||||
{
|
||||
$this->info(' ____ _ ______ __ ');
|
||||
$this->info(' / __ \(_) _____ / / __/__ ____/ / ');
|
||||
$this->info(' / /_/ / / |/_/ _ \/ / /_/ _ \/ __ / ');
|
||||
$this->info(' / ____/ /> </ __/ / __/ __/ /_/ / ');
|
||||
$this->info(' /_/ /_/_/|_|\___/_/_/ \___/\__,_/ ');
|
||||
$this->info(' ');
|
||||
$this->info(' Pixelfed Avatar Deep Cleaner');
|
||||
$this->line(' ');
|
||||
$this->info(' Purge/delete old and outdated avatars from remote accounts');
|
||||
$this->line(' ');
|
||||
|
||||
$storage = [
|
||||
'cloud' => (bool) config_cache('pixelfed.cloud_storage'),
|
||||
'local' => boolval(config_cache('federation.avatars.store_local'))
|
||||
];
|
||||
|
||||
if(!$storage['cloud'] && !$storage['local']) {
|
||||
$this->error('Remote avatars are not cached locally, there is nothing to purge. Aborting...');
|
||||
exit;
|
||||
}
|
||||
|
||||
$start = 0;
|
||||
|
||||
if(!$this->confirm('Are you sure you want to proceed?')) {
|
||||
$this->error('Aborting...');
|
||||
exit;
|
||||
}
|
||||
|
||||
if(!$this->activeCheck()) {
|
||||
$this->info('Found existing deep cleaning job');
|
||||
if(!$this->confirm('Do you want to continue where you left off?')) {
|
||||
$this->error('Aborting...');
|
||||
exit;
|
||||
} else {
|
||||
$start = Cache::has('cmd:asdp') ? (int) Cache::get('cmd:asdp') : (int) Storage::get('avatar-deep-clean.json');
|
||||
|
||||
if($start && $start < 1 || $start > PHP_INT_MAX) {
|
||||
$this->error('Error fetching cached value');
|
||||
$this->error('Aborting...');
|
||||
exit;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
$count = Avatar::whereNotNull('cdn_url')->where('is_remote', true)->where('id', '>', $start)->count();
|
||||
$bar = $this->output->createProgressBar($count);
|
||||
|
||||
foreach(Avatar::whereNotNull('cdn_url')->where('is_remote', true)->where('id', '>', $start)->lazyById(10, 'id') as $avatar) {
|
||||
usleep(random_int(50, 1000));
|
||||
$this->counter++;
|
||||
$this->handleAvatar($avatar);
|
||||
$bar->advance();
|
||||
}
|
||||
$bar->finish();
|
||||
}
|
||||
|
||||
protected function updateCache($id)
|
||||
{
|
||||
Cache::put('cmd:asdp', $id);
|
||||
if($this->counter % 5 === 0) {
|
||||
Storage::put('avatar-deep-clean.json', $id);
|
||||
}
|
||||
}
|
||||
|
||||
protected function activeCheck()
|
||||
{
|
||||
if(Storage::exists('avatar-deep-clean.json') || Cache::has('cmd:asdp')) {
|
||||
return false;
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
protected function handleAvatar($avatar)
|
||||
{
|
||||
$this->updateCache($avatar->id);
|
||||
$queues = ['feed', 'mmo', 'feed', 'mmo', 'feed', 'feed', 'mmo', 'low'];
|
||||
$queue = $queues[random_int(0, 7)];
|
||||
AvatarStorageCleanup::dispatch($avatar)->onQueue($queue);
|
||||
}
|
||||
}
|
|
@ -35,12 +35,16 @@ class CloudMediaMigrate extends Command
|
|||
*/
|
||||
public function handle()
|
||||
{
|
||||
$enabled = config('pixelfed.cloud_storage');
|
||||
$enabled = (bool) config_cache('pixelfed.cloud_storage');
|
||||
if(!$enabled) {
|
||||
$this->error('Cloud storage not enabled. Exiting...');
|
||||
return;
|
||||
}
|
||||
|
||||
if(!$this->confirm('Are you sure you want to proceed?')) {
|
||||
return;
|
||||
}
|
||||
|
||||
$limit = $this->option('limit');
|
||||
$hugeMode = $this->option('huge');
|
||||
|
||||
|
|
|
@ -0,0 +1,96 @@
|
|||
<?php
|
||||
|
||||
namespace App\Console\Commands;
|
||||
|
||||
use Illuminate\Console\Command;
|
||||
use App\User;
|
||||
use App\Models\DefaultDomainBlock;
|
||||
use App\Models\UserDomainBlock;
|
||||
use function Laravel\Prompts\text;
|
||||
use function Laravel\Prompts\confirm;
|
||||
use function Laravel\Prompts\progress;
|
||||
|
||||
class DeleteUserDomainBlock extends Command
|
||||
{
|
||||
/**
|
||||
* The name and signature of the console command.
|
||||
*
|
||||
* @var string
|
||||
*/
|
||||
protected $signature = 'app:delete-user-domain-block';
|
||||
|
||||
/**
|
||||
* The console command description.
|
||||
*
|
||||
* @var string
|
||||
*/
|
||||
protected $description = 'Remove a domain block for all users';
|
||||
|
||||
/**
|
||||
* Execute the console command.
|
||||
*/
|
||||
public function handle()
|
||||
{
|
||||
$domain = text('Enter domain you want to unblock');
|
||||
$domain = strtolower($domain);
|
||||
$domain = $this->validateDomain($domain);
|
||||
if(!$domain || empty($domain)) {
|
||||
$this->error('Invalid domain');
|
||||
return;
|
||||
}
|
||||
$this->processUnblocks($domain);
|
||||
return;
|
||||
}
|
||||
|
||||
protected function validateDomain($domain)
|
||||
{
|
||||
if(!strpos($domain, '.')) {
|
||||
return;
|
||||
}
|
||||
|
||||
if(str_starts_with($domain, 'https://')) {
|
||||
$domain = str_replace('https://', '', $domain);
|
||||
}
|
||||
|
||||
if(str_starts_with($domain, 'http://')) {
|
||||
$domain = str_replace('http://', '', $domain);
|
||||
}
|
||||
|
||||
$domain = strtolower(parse_url('https://' . $domain, PHP_URL_HOST));
|
||||
|
||||
$valid = filter_var($domain, FILTER_VALIDATE_DOMAIN, FILTER_FLAG_HOSTNAME|FILTER_NULL_ON_FAILURE);
|
||||
if(!$valid) {
|
||||
return;
|
||||
}
|
||||
|
||||
if($domain === config('pixelfed.domain.app')) {
|
||||
return;
|
||||
}
|
||||
|
||||
$confirmed = confirm('Are you sure you want to unblock ' . $domain . '?');
|
||||
if(!$confirmed) {
|
||||
return;
|
||||
}
|
||||
|
||||
return $domain;
|
||||
}
|
||||
|
||||
protected function processUnblocks($domain)
|
||||
{
|
||||
DefaultDomainBlock::whereDomain($domain)->delete();
|
||||
if(!UserDomainBlock::whereDomain($domain)->count()) {
|
||||
$this->info('No results found!');
|
||||
return;
|
||||
}
|
||||
progress(
|
||||
label: 'Updating user domain blocks...',
|
||||
steps: UserDomainBlock::whereDomain($domain)->lazyById(500),
|
||||
callback: fn ($domainBlock) => $this->performTask($domainBlock),
|
||||
);
|
||||
}
|
||||
|
||||
protected function performTask($domainBlock)
|
||||
{
|
||||
$domainBlock->deleteQuietly();
|
||||
}
|
||||
}
|
|
@ -0,0 +1,56 @@
|
|||
<?php
|
||||
|
||||
namespace App\Console\Commands;
|
||||
|
||||
use App\Media;
|
||||
use App\Services\MediaService;
|
||||
use App\Services\StatusService;
|
||||
use Illuminate\Console\Command;
|
||||
use Illuminate\Support\Facades\Http;
|
||||
|
||||
class FetchMissingMediaMimeType extends Command
|
||||
{
|
||||
/**
|
||||
* The name and signature of the console command.
|
||||
*
|
||||
* @var string
|
||||
*/
|
||||
protected $signature = 'app:fetch-missing-media-mime-type';
|
||||
|
||||
/**
|
||||
* The console command description.
|
||||
*
|
||||
* @var string
|
||||
*/
|
||||
protected $description = 'Command description';
|
||||
|
||||
/**
|
||||
* Execute the console command.
|
||||
*/
|
||||
public function handle()
|
||||
{
|
||||
foreach (Media::whereNotNull(['remote_url', 'status_id'])->whereNull('mime')->lazyByIdDesc(50, 'id') as $media) {
|
||||
$res = Http::retry(2, 100, throw: false)->head($media->remote_url);
|
||||
|
||||
if (! $res->successful()) {
|
||||
continue;
|
||||
}
|
||||
|
||||
if (! in_array($res->header('content-type'), explode(',', config_cache('pixelfed.media_types')))) {
|
||||
continue;
|
||||
}
|
||||
|
||||
$media->mime = $res->header('content-type');
|
||||
|
||||
if ($res->hasHeader('content-length')) {
|
||||
$media->size = $res->header('content-length');
|
||||
}
|
||||
|
||||
$media->save();
|
||||
|
||||
MediaService::del($media->status_id);
|
||||
StatusService::del($media->status_id);
|
||||
$this->info('mid:'.$media->id.' ('.$res->header('content-type').':'.$res->header('content-length').' bytes)');
|
||||
}
|
||||
}
|
||||
}
|
|
@ -37,7 +37,7 @@ class FixMediaDriver extends Command
|
|||
return Command::SUCCESS;
|
||||
}
|
||||
|
||||
if(config_cache('pixelfed.cloud_storage') == false) {
|
||||
if((bool) config_cache('pixelfed.cloud_storage') == false) {
|
||||
$this->error('Cloud storage not enabled, exiting...');
|
||||
return Command::SUCCESS;
|
||||
}
|
||||
|
|
|
@ -4,6 +4,7 @@ namespace App\Console\Commands;
|
|||
|
||||
use Illuminate\Console\Command;
|
||||
use App\Profile;
|
||||
use App\Services\AccountService;
|
||||
|
||||
class FixStatusCount extends Command
|
||||
{
|
||||
|
@ -12,7 +13,7 @@ class FixStatusCount extends Command
|
|||
*
|
||||
* @var string
|
||||
*/
|
||||
protected $signature = 'fix:statuscount';
|
||||
protected $signature = 'fix:statuscount {--remote} {--resync} {--remote-only} {--dlog}';
|
||||
|
||||
/**
|
||||
* The console command description.
|
||||
|
@ -38,18 +39,100 @@ class FixStatusCount extends Command
|
|||
*/
|
||||
public function handle()
|
||||
{
|
||||
Profile::whereNull('domain')
|
||||
->chunk(50, function($profiles) {
|
||||
foreach($profiles as $profile) {
|
||||
$profile->status_count = $profile->statuses()
|
||||
->getQuery()
|
||||
->whereIn('type', ['photo', 'photo:album', 'video', 'video:album', 'photo:video:album'])
|
||||
->whereNull('in_reply_to_id')
|
||||
->whereNull('reblog_of_id')
|
||||
->count();
|
||||
$profile->save();
|
||||
if(!$this->confirm('Are you sure you want to run the fix status command?')) {
|
||||
return;
|
||||
}
|
||||
$this->line(' ');
|
||||
$this->info('Running fix status command...');
|
||||
$now = now();
|
||||
|
||||
$nulls = ['domain', 'status', 'last_fetched_at'];
|
||||
|
||||
$resync = $this->option('resync');
|
||||
$resync24hours = false;
|
||||
|
||||
if($resync) {
|
||||
$resyncChoices = ['Only resync accounts that havent been synced in 24 hours', 'Resync all accounts'];
|
||||
$rsc = $this->choice(
|
||||
'Do you want to resync all accounts, or just accounts that havent been resynced for 24 hours?',
|
||||
$resyncChoices,
|
||||
0
|
||||
);
|
||||
$rsci = array_search($rsc, $resyncChoices);
|
||||
if($rsci === 0) {
|
||||
$resync24hours = true;
|
||||
$nulls = ['status', 'domain', 'last_fetched_at'];
|
||||
} else {
|
||||
$resync24hours = false;
|
||||
$nulls = ['status', 'domain'];
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
$remote = $this->option('remote');
|
||||
|
||||
if($remote) {
|
||||
$ni = array_search('domain', $nulls);
|
||||
unset($nulls[$ni]);
|
||||
$ni = array_search('last_fetched_at', $nulls);
|
||||
unset($nulls[$ni]);
|
||||
}
|
||||
|
||||
$remoteOnly = $this->option('remote-only');
|
||||
|
||||
if($remoteOnly) {
|
||||
$ni = array_search('domain', $nulls);
|
||||
unset($nulls[$ni]);
|
||||
$ni = array_search('last_fetched_at', $nulls);
|
||||
unset($nulls[$ni]);
|
||||
$nulls[] = 'user_id';
|
||||
}
|
||||
|
||||
$dlog = $this->option('dlog');
|
||||
|
||||
$nulls = array_values($nulls);
|
||||
|
||||
foreach(
|
||||
Profile::when($resync24hours, function($query, $resync24hours) use($nulls) {
|
||||
if(in_array('domain', $nulls)) {
|
||||
return $query->whereNull('domain')
|
||||
->whereNull('last_fetched_at')
|
||||
->orWhere('last_fetched_at', '<', now()->subHours(24));
|
||||
} else {
|
||||
return $query->whereNull('last_fetched_at')
|
||||
->orWhere('last_fetched_at', '<', now()->subHours(24));
|
||||
}
|
||||
})
|
||||
->when($remoteOnly, function($query, $remoteOnly) {
|
||||
return $query->whereNull('last_fetched_at')
|
||||
->orWhere('last_fetched_at', '<', now()->subHours(24));
|
||||
})
|
||||
->whereNull($nulls)
|
||||
->lazyById(50, 'id') as $profile
|
||||
) {
|
||||
$ogc = $profile->status_count;
|
||||
$upc = $profile->statuses()
|
||||
->getQuery()
|
||||
->whereIn('scope', ['public', 'private', 'unlisted'])
|
||||
->count();
|
||||
if($ogc != $upc) {
|
||||
$profile->status_count = $upc;
|
||||
$profile->last_fetched_at = $now;
|
||||
$profile->save();
|
||||
AccountService::del($profile->id);
|
||||
if($dlog) {
|
||||
$this->info($profile->id . ':' . $profile->username . ' : ' . $upc);
|
||||
}
|
||||
} else {
|
||||
$profile->last_fetched_at = $now;
|
||||
$profile->save();
|
||||
if($dlog) {
|
||||
$this->info($profile->id . ':' . $profile->username . ' : ' . $upc);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
$this->line(' ');
|
||||
$this->info('Finished fix status count command!');
|
||||
|
||||
return 0;
|
||||
}
|
||||
|
|
|
@ -0,0 +1,57 @@
|
|||
<?php
|
||||
|
||||
namespace App\Console\Commands;
|
||||
|
||||
use Illuminate\Console\Command;
|
||||
use App\Hashtag;
|
||||
use App\StatusHashtag;
|
||||
use DB;
|
||||
|
||||
class HashtagCachedCountUpdate extends Command
|
||||
{
|
||||
/**
|
||||
* The name and signature of the console command.
|
||||
*
|
||||
* @var string
|
||||
*/
|
||||
protected $signature = 'app:hashtag-cached-count-update {--limit=100}';
|
||||
|
||||
/**
|
||||
* The console command description.
|
||||
*
|
||||
* @var string
|
||||
*/
|
||||
protected $description = 'Update cached counter of hashtags';
|
||||
|
||||
/**
|
||||
* Execute the console command.
|
||||
*/
|
||||
public function handle()
|
||||
{
|
||||
$limit = $this->option('limit');
|
||||
$tags = Hashtag::whereNull('cached_count')->limit($limit)->get();
|
||||
$count = count($tags);
|
||||
if(!$count) {
|
||||
return;
|
||||
}
|
||||
|
||||
$bar = $this->output->createProgressBar($count);
|
||||
$bar->start();
|
||||
|
||||
foreach($tags as $tag) {
|
||||
$count = DB::table('status_hashtags')->whereHashtagId($tag->id)->count();
|
||||
if(!$count) {
|
||||
$tag->cached_count = 0;
|
||||
$tag->saveQuietly();
|
||||
$bar->advance();
|
||||
continue;
|
||||
}
|
||||
$tag->cached_count = $count;
|
||||
$tag->saveQuietly();
|
||||
$bar->advance();
|
||||
}
|
||||
$bar->finish();
|
||||
$this->line(' ');
|
||||
return;
|
||||
}
|
||||
}
|
|
@ -0,0 +1,94 @@
|
|||
<?php
|
||||
|
||||
namespace App\Console\Commands;
|
||||
|
||||
use Illuminate\Console\Command;
|
||||
use App\Hashtag;
|
||||
use App\StatusHashtag;
|
||||
use App\Models\HashtagRelated;
|
||||
use App\Services\HashtagRelatedService;
|
||||
use Illuminate\Contracts\Console\PromptsForMissingInput;
|
||||
use function Laravel\Prompts\multiselect;
|
||||
use function Laravel\Prompts\confirm;
|
||||
|
||||
class HashtagRelatedGenerate extends Command implements PromptsForMissingInput
|
||||
{
|
||||
/**
|
||||
* The name and signature of the console command.
|
||||
*
|
||||
* @var string
|
||||
*/
|
||||
protected $signature = 'app:hashtag-related-generate {tag}';
|
||||
|
||||
/**
|
||||
* The console command description.
|
||||
*
|
||||
* @var string
|
||||
*/
|
||||
protected $description = 'Command description';
|
||||
|
||||
/**
|
||||
* Prompt for missing input arguments using the returned questions.
|
||||
*
|
||||
* @return array
|
||||
*/
|
||||
protected function promptForMissingArgumentsUsing()
|
||||
{
|
||||
return [
|
||||
'tag' => 'Which hashtag should we generate related tags for?',
|
||||
];
|
||||
}
|
||||
|
||||
/**
|
||||
* Execute the console command.
|
||||
*/
|
||||
public function handle()
|
||||
{
|
||||
$tag = $this->argument('tag');
|
||||
$hashtag = Hashtag::whereName($tag)->orWhere('slug', $tag)->first();
|
||||
if(!$hashtag) {
|
||||
$this->error('Hashtag not found, aborting...');
|
||||
exit;
|
||||
}
|
||||
|
||||
$exists = HashtagRelated::whereHashtagId($hashtag->id)->exists();
|
||||
|
||||
if($exists) {
|
||||
$confirmed = confirm('Found existing related tags, do you want to regenerate them?');
|
||||
if(!$confirmed) {
|
||||
$this->error('Aborting...');
|
||||
exit;
|
||||
}
|
||||
}
|
||||
|
||||
$this->info('Looking up #' . $tag . '...');
|
||||
|
||||
$tags = StatusHashtag::whereHashtagId($hashtag->id)->count();
|
||||
if(!$tags || $tags < 100) {
|
||||
$this->error('Not enough posts found to generate related hashtags!');
|
||||
exit;
|
||||
}
|
||||
|
||||
$this->info('Found ' . $tags . ' posts that use that hashtag');
|
||||
$related = collect(HashtagRelatedService::fetchRelatedTags($tag));
|
||||
|
||||
$selected = multiselect(
|
||||
label: 'Which tags do you want to generate?',
|
||||
options: $related->pluck('name'),
|
||||
required: true,
|
||||
);
|
||||
|
||||
$filtered = $related->filter(fn($i) => in_array($i['name'], $selected))->all();
|
||||
$agg_score = $related->filter(fn($i) => in_array($i['name'], $selected))->sum('related_count');
|
||||
|
||||
HashtagRelated::updateOrCreate([
|
||||
'hashtag_id' => $hashtag->id,
|
||||
], [
|
||||
'related_tags' => array_values($filtered),
|
||||
'agg_score' => $agg_score,
|
||||
'last_calculated_at' => now()
|
||||
]);
|
||||
|
||||
$this->info('Finished!');
|
||||
}
|
||||
}
|
|
@ -0,0 +1,118 @@
|
|||
<?php
|
||||
|
||||
namespace App\Console\Commands;
|
||||
|
||||
use App\Models\CustomEmoji;
|
||||
use Illuminate\Console\Command;
|
||||
use Illuminate\Support\Facades\Cache;
|
||||
use Illuminate\Support\Facades\Storage;
|
||||
|
||||
class ImportEmojis extends Command
|
||||
{
|
||||
/**
|
||||
* The name and signature of the console command.
|
||||
*
|
||||
* @var string
|
||||
*/
|
||||
protected $signature = 'import:emojis
|
||||
{path : Path to a tar.gz archive with the emojis}
|
||||
{--prefix : Define a prefix for the emjoi shortcode}
|
||||
{--suffix : Define a suffix for the emjoi shortcode}
|
||||
{--overwrite : Overwrite existing emojis}
|
||||
{--disabled : Import all emojis as disabled}';
|
||||
|
||||
|
||||
/**
|
||||
* The console command description.
|
||||
*
|
||||
* @var string
|
||||
*/
|
||||
protected $description = 'Import emojis to the database';
|
||||
|
||||
/**
|
||||
* Execute the console command.
|
||||
*
|
||||
* @return int
|
||||
*/
|
||||
public function handle()
|
||||
{
|
||||
$path = $this->argument('path');
|
||||
|
||||
if (!file_exists($path) || !mime_content_type($path) == 'application/x-tar') {
|
||||
$this->error('Path does not exist or is not a tarfile');
|
||||
return Command::FAILURE;
|
||||
}
|
||||
|
||||
$imported = 0;
|
||||
$skipped = 0;
|
||||
$failed = 0;
|
||||
|
||||
$tar = new \PharData($path);
|
||||
$tar->decompress();
|
||||
|
||||
foreach (new \RecursiveIteratorIterator($tar) as $entry) {
|
||||
$this->line("Processing {$entry->getFilename()}");
|
||||
if (!$entry->isFile() || !$this->isImage($entry) || !$this->isEmoji($entry->getPathname())) {
|
||||
$failed++;
|
||||
continue;
|
||||
}
|
||||
|
||||
$filename = pathinfo($entry->getFilename(), PATHINFO_FILENAME);
|
||||
$extension = pathinfo($entry->getFilename(), PATHINFO_EXTENSION);
|
||||
|
||||
// Skip macOS shadow files
|
||||
if (str_starts_with($filename, '._')) {
|
||||
continue;
|
||||
}
|
||||
|
||||
$shortcode = implode('', [
|
||||
$this->option('prefix'),
|
||||
$filename,
|
||||
$this->option('suffix'),
|
||||
]);
|
||||
|
||||
$customEmoji = CustomEmoji::whereShortcode($shortcode)->first();
|
||||
|
||||
if ($customEmoji && !$this->option('overwrite')) {
|
||||
$skipped++;
|
||||
continue;
|
||||
}
|
||||
|
||||
$emoji = $customEmoji ?? new CustomEmoji();
|
||||
$emoji->shortcode = $shortcode;
|
||||
$emoji->domain = config('pixelfed.domain.app');
|
||||
$emoji->disabled = $this->option('disabled');
|
||||
$emoji->save();
|
||||
|
||||
$fileName = $emoji->id . '.' . $extension;
|
||||
Storage::putFileAs('public/emoji', $entry->getPathname(), $fileName);
|
||||
$emoji->media_path = 'emoji/' . $fileName;
|
||||
$emoji->save();
|
||||
$imported++;
|
||||
Cache::forget('pf:custom_emoji');
|
||||
}
|
||||
|
||||
$this->line("Imported: {$imported}");
|
||||
$this->line("Skipped: {$skipped}");
|
||||
$this->line("Failed: {$failed}");
|
||||
|
||||
//delete file
|
||||
unlink(str_replace('.tar.gz', '.tar', $path));
|
||||
|
||||
return Command::SUCCESS;
|
||||
}
|
||||
|
||||
private function isImage($file)
|
||||
{
|
||||
$image = getimagesize($file->getPathname());
|
||||
return $image !== false;
|
||||
}
|
||||
|
||||
private function isEmoji($filename)
|
||||
{
|
||||
$allowedMimeTypes = ['image/png', 'image/jpeg', 'image/webp'];
|
||||
$mimeType = mime_content_type($filename);
|
||||
|
||||
return in_array($mimeType, $allowedMimeTypes);
|
||||
}
|
||||
}
|
|
@ -0,0 +1,61 @@
|
|||
<?php
|
||||
|
||||
namespace App\Console\Commands;
|
||||
|
||||
use Illuminate\Console\Command;
|
||||
use Illuminate\Support\Facades\Cache;
|
||||
use Illuminate\Support\Facades\Storage;
|
||||
use App\User;
|
||||
use App\Models\ImportPost;
|
||||
use App\Services\ImportService;
|
||||
|
||||
class ImportRemoveDeletedAccounts extends Command
|
||||
{
|
||||
/**
|
||||
* The name and signature of the console command.
|
||||
*
|
||||
* @var string
|
||||
*/
|
||||
protected $signature = 'app:import-remove-deleted-accounts';
|
||||
|
||||
/**
|
||||
* The console command description.
|
||||
*
|
||||
* @var string
|
||||
*/
|
||||
protected $description = 'Command description';
|
||||
|
||||
const CACHE_KEY = 'pf:services:import:gc-accounts:skip_min_id';
|
||||
|
||||
/**
|
||||
* Execute the console command.
|
||||
*/
|
||||
public function handle()
|
||||
{
|
||||
$skipMinId = Cache::remember(self::CACHE_KEY, 864000, function() {
|
||||
return 1;
|
||||
});
|
||||
|
||||
$deletedIds = User::withTrashed()
|
||||
->whereNotNull('status')
|
||||
->whereIn('status', ['deleted', 'delete'])
|
||||
->where('id', '>', $skipMinId)
|
||||
->limit(500)
|
||||
->pluck('id');
|
||||
|
||||
if(!$deletedIds || !$deletedIds->count()) {
|
||||
return;
|
||||
}
|
||||
|
||||
foreach($deletedIds as $did) {
|
||||
if(Storage::exists('imports/' . $did)) {
|
||||
Storage::deleteDirectory('imports/' . $did);
|
||||
}
|
||||
|
||||
ImportPost::where('user_id', $did)->delete();
|
||||
$skipMinId = $did;
|
||||
}
|
||||
|
||||
Cache::put(self::CACHE_KEY, $skipMinId, 864000);
|
||||
}
|
||||
}
|
|
@ -0,0 +1,42 @@
|
|||
<?php
|
||||
|
||||
namespace App\Console\Commands;
|
||||
|
||||
use Illuminate\Console\Command;
|
||||
use App\Models\ImportPost;
|
||||
use Storage;
|
||||
use App\Services\ImportService;
|
||||
use App\User;
|
||||
|
||||
class ImportUploadCleanStorage extends Command
|
||||
{
|
||||
/**
|
||||
* The name and signature of the console command.
|
||||
*
|
||||
* @var string
|
||||
*/
|
||||
protected $signature = 'app:import-upload-clean-storage';
|
||||
|
||||
/**
|
||||
* The console command description.
|
||||
*
|
||||
* @var string
|
||||
*/
|
||||
protected $description = 'Command description';
|
||||
|
||||
/**
|
||||
* Execute the console command.
|
||||
*/
|
||||
public function handle()
|
||||
{
|
||||
$dirs = Storage::allDirectories('imports');
|
||||
|
||||
foreach($dirs as $dir) {
|
||||
$uid = last(explode('/', $dir));
|
||||
$skip = User::whereNull('status')->find($uid);
|
||||
if(!$skip) {
|
||||
Storage::deleteDirectory($dir);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
|
@ -0,0 +1,49 @@
|
|||
<?php
|
||||
|
||||
namespace App\Console\Commands;
|
||||
|
||||
use Illuminate\Console\Command;
|
||||
use App\Models\ImportPost;
|
||||
use Storage;
|
||||
use App\Services\ImportService;
|
||||
|
||||
class ImportUploadGarbageCollection extends Command
|
||||
{
|
||||
/**
|
||||
* The name and signature of the console command.
|
||||
*
|
||||
* @var string
|
||||
*/
|
||||
protected $signature = 'app:import-upload-garbage-collection';
|
||||
|
||||
/**
|
||||
* The console command description.
|
||||
*
|
||||
* @var string
|
||||
*/
|
||||
protected $description = 'Command description';
|
||||
|
||||
/**
|
||||
* Execute the console command.
|
||||
*/
|
||||
public function handle()
|
||||
{
|
||||
if(!config('import.instagram.enabled')) {
|
||||
return;
|
||||
}
|
||||
|
||||
$ips = ImportPost::whereNull('status_id')->where('skip_missing_media', true)->take(100)->get();
|
||||
|
||||
if(!$ips->count()) {
|
||||
return;
|
||||
}
|
||||
|
||||
foreach($ips as $ip) {
|
||||
$pid = $ip->profile_id;
|
||||
$ip->delete();
|
||||
ImportService::getPostCount($pid, true);
|
||||
ImportService::clearAttempts($pid);
|
||||
ImportService::getImportedFiles($pid, true);
|
||||
}
|
||||
}
|
||||
}
|
|
@ -0,0 +1,54 @@
|
|||
<?php
|
||||
|
||||
namespace App\Console\Commands;
|
||||
|
||||
use Illuminate\Console\Command;
|
||||
use App\Models\ImportPost;
|
||||
use App\Jobs\ImportPipeline\ImportMediaToCloudPipeline;
|
||||
use function Laravel\Prompts\progress;
|
||||
|
||||
class ImportUploadMediaToCloudStorage extends Command
|
||||
{
|
||||
/**
|
||||
* The name and signature of the console command.
|
||||
*
|
||||
* @var string
|
||||
*/
|
||||
protected $signature = 'app:import-upload-media-to-cloud-storage {--limit=500}';
|
||||
|
||||
/**
|
||||
* The console command description.
|
||||
*
|
||||
* @var string
|
||||
*/
|
||||
protected $description = 'Migrate media imported from Instagram to S3 cloud storage.';
|
||||
|
||||
/**
|
||||
* Execute the console command.
|
||||
*/
|
||||
public function handle()
|
||||
{
|
||||
if(
|
||||
(bool) config('import.instagram.storage.cloud.enabled') === false ||
|
||||
(bool) config_cache('pixelfed.cloud_storage') === false
|
||||
) {
|
||||
$this->error('Aborted. Cloud storage is not enabled for IG imports.');
|
||||
return;
|
||||
}
|
||||
|
||||
$limit = $this->option('limit');
|
||||
|
||||
$progress = progress(label: 'Migrating import media', steps: $limit);
|
||||
|
||||
$progress->start();
|
||||
|
||||
$posts = ImportPost::whereUploadedToS3(false)->take($limit)->get();
|
||||
|
||||
foreach($posts as $post) {
|
||||
ImportMediaToCloudPipeline::dispatch($post)->onQueue('low');
|
||||
$progress->advance();
|
||||
}
|
||||
|
||||
$progress->finish();
|
||||
}
|
||||
}
|
|
@ -0,0 +1,298 @@
|
|||
<?php
|
||||
|
||||
namespace App\Console\Commands;
|
||||
|
||||
use Illuminate\Console\Command;
|
||||
use App\Instance;
|
||||
use App\Profile;
|
||||
use App\Services\InstanceService;
|
||||
use App\Jobs\InstancePipeline\FetchNodeinfoPipeline;
|
||||
use function Laravel\Prompts\select;
|
||||
use function Laravel\Prompts\confirm;
|
||||
use function Laravel\Prompts\progress;
|
||||
use function Laravel\Prompts\search;
|
||||
use function Laravel\Prompts\table;
|
||||
|
||||
class InstanceManager extends Command
|
||||
{
|
||||
/**
|
||||
* The name and signature of the console command.
|
||||
*
|
||||
* @var string
|
||||
*/
|
||||
protected $signature = 'app:instance-manager';
|
||||
|
||||
/**
|
||||
* The console command description.
|
||||
*
|
||||
* @var string
|
||||
*/
|
||||
protected $description = 'Manage Instances';
|
||||
|
||||
/**
|
||||
* Execute the console command.
|
||||
*/
|
||||
public function handle()
|
||||
{
|
||||
$action = select(
|
||||
'What action do you want to perform?',
|
||||
[
|
||||
'Recalculate Stats',
|
||||
'Ban Instance',
|
||||
'Unlist Instance',
|
||||
'Unlisted Instances',
|
||||
'Banned Instances',
|
||||
'Unban Instance',
|
||||
'Relist Instance',
|
||||
],
|
||||
);
|
||||
|
||||
switch($action) {
|
||||
case 'Recalculate Stats':
|
||||
return $this->recalculateStats();
|
||||
break;
|
||||
|
||||
case 'Unlisted Instances':
|
||||
return $this->viewUnlistedInstances();
|
||||
break;
|
||||
|
||||
case 'Banned Instances':
|
||||
return $this->viewBannedInstances();
|
||||
break;
|
||||
|
||||
case 'Unlist Instance':
|
||||
return $this->unlistInstance();
|
||||
break;
|
||||
|
||||
case 'Ban Instance':
|
||||
return $this->banInstance();
|
||||
break;
|
||||
|
||||
case 'Unban Instance':
|
||||
return $this->unbanInstance();
|
||||
break;
|
||||
|
||||
case 'Relist Instance':
|
||||
return $this->relistInstance();
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
protected function recalculateStats()
|
||||
{
|
||||
$instanceCount = Instance::count();
|
||||
$confirmed = confirm('Do you want to recalculate stats for all ' . $instanceCount . ' instances?');
|
||||
if(!$confirmed) {
|
||||
$this->error('Aborting...');
|
||||
exit;
|
||||
}
|
||||
|
||||
$users = progress(
|
||||
label: 'Updating instance stats...',
|
||||
steps: Instance::all(),
|
||||
callback: fn ($instance) => $this->updateInstanceStats($instance),
|
||||
);
|
||||
}
|
||||
|
||||
protected function updateInstanceStats($instance)
|
||||
{
|
||||
FetchNodeinfoPipeline::dispatch($instance)->onQueue('intbg');
|
||||
}
|
||||
|
||||
protected function unlistInstance()
|
||||
{
|
||||
$id = search(
|
||||
'Search by domain',
|
||||
fn (string $value) => strlen($value) > 0
|
||||
? Instance::whereUnlisted(false)->where('domain', 'like', "%{$value}%")->pluck('domain', 'id')->all()
|
||||
: []
|
||||
);
|
||||
|
||||
$instance = Instance::find($id);
|
||||
if(!$instance) {
|
||||
$this->error('Oops, an error occured');
|
||||
exit;
|
||||
}
|
||||
|
||||
$tbl = [
|
||||
[
|
||||
$instance->domain,
|
||||
number_format($instance->status_count),
|
||||
number_format($instance->user_count),
|
||||
]
|
||||
];
|
||||
table(
|
||||
['Domain', 'Status Count', 'User Count'],
|
||||
$tbl
|
||||
);
|
||||
|
||||
$confirmed = confirm('Are you sure you want to unlist this instance?');
|
||||
if(!$confirmed) {
|
||||
$this->error('Aborting instance unlisting');
|
||||
exit;
|
||||
}
|
||||
|
||||
$instance->unlisted = true;
|
||||
$instance->save();
|
||||
InstanceService::refresh();
|
||||
$this->info('Successfully unlisted ' . $instance->domain . '!');
|
||||
exit;
|
||||
}
|
||||
|
||||
protected function relistInstance()
|
||||
{
|
||||
$id = search(
|
||||
'Search by domain',
|
||||
fn (string $value) => strlen($value) > 0
|
||||
? Instance::whereUnlisted(true)->where('domain', 'like', "%{$value}%")->pluck('domain', 'id')->all()
|
||||
: []
|
||||
);
|
||||
|
||||
$instance = Instance::find($id);
|
||||
if(!$instance) {
|
||||
$this->error('Oops, an error occured');
|
||||
exit;
|
||||
}
|
||||
|
||||
$tbl = [
|
||||
[
|
||||
$instance->domain,
|
||||
number_format($instance->status_count),
|
||||
number_format($instance->user_count),
|
||||
]
|
||||
];
|
||||
table(
|
||||
['Domain', 'Status Count', 'User Count'],
|
||||
$tbl
|
||||
);
|
||||
|
||||
$confirmed = confirm('Are you sure you want to re-list this instance?');
|
||||
if(!$confirmed) {
|
||||
$this->error('Aborting instance re-listing');
|
||||
exit;
|
||||
}
|
||||
|
||||
$instance->unlisted = false;
|
||||
$instance->save();
|
||||
InstanceService::refresh();
|
||||
$this->info('Successfully re-listed ' . $instance->domain . '!');
|
||||
exit;
|
||||
}
|
||||
|
||||
protected function banInstance()
|
||||
{
|
||||
$id = search(
|
||||
'Search by domain',
|
||||
fn (string $value) => strlen($value) > 0
|
||||
? Instance::whereBanned(false)->where('domain', 'like', "%{$value}%")->pluck('domain', 'id')->all()
|
||||
: []
|
||||
);
|
||||
|
||||
$instance = Instance::find($id);
|
||||
if(!$instance) {
|
||||
$this->error('Oops, an error occured');
|
||||
exit;
|
||||
}
|
||||
|
||||
$tbl = [
|
||||
[
|
||||
$instance->domain,
|
||||
number_format($instance->status_count),
|
||||
number_format($instance->user_count),
|
||||
]
|
||||
];
|
||||
table(
|
||||
['Domain', 'Status Count', 'User Count'],
|
||||
$tbl
|
||||
);
|
||||
|
||||
$confirmed = confirm('Are you sure you want to ban this instance?');
|
||||
if(!$confirmed) {
|
||||
$this->error('Aborting instance ban');
|
||||
exit;
|
||||
}
|
||||
|
||||
$instance->banned = true;
|
||||
$instance->save();
|
||||
InstanceService::refresh();
|
||||
$this->info('Successfully banned ' . $instance->domain . '!');
|
||||
exit;
|
||||
}
|
||||
|
||||
protected function unbanInstance()
|
||||
{
|
||||
$id = search(
|
||||
'Search by domain',
|
||||
fn (string $value) => strlen($value) > 0
|
||||
? Instance::whereBanned(true)->where('domain', 'like', "%{$value}%")->pluck('domain', 'id')->all()
|
||||
: []
|
||||
);
|
||||
|
||||
$instance = Instance::find($id);
|
||||
if(!$instance) {
|
||||
$this->error('Oops, an error occured');
|
||||
exit;
|
||||
}
|
||||
|
||||
$tbl = [
|
||||
[
|
||||
$instance->domain,
|
||||
number_format($instance->status_count),
|
||||
number_format($instance->user_count),
|
||||
]
|
||||
];
|
||||
table(
|
||||
['Domain', 'Status Count', 'User Count'],
|
||||
$tbl
|
||||
);
|
||||
|
||||
$confirmed = confirm('Are you sure you want to unban this instance?');
|
||||
if(!$confirmed) {
|
||||
$this->error('Aborting instance unban');
|
||||
exit;
|
||||
}
|
||||
|
||||
$instance->banned = false;
|
||||
$instance->save();
|
||||
InstanceService::refresh();
|
||||
$this->info('Successfully un-banned ' . $instance->domain . '!');
|
||||
exit;
|
||||
}
|
||||
|
||||
protected function viewBannedInstances()
|
||||
{
|
||||
$data = Instance::whereBanned(true)
|
||||
->get(['domain', 'user_count', 'status_count'])
|
||||
->map(function($d) {
|
||||
return [
|
||||
'domain' => $d->domain,
|
||||
'user_count' => number_format($d->user_count),
|
||||
'status_count' => number_format($d->status_count),
|
||||
];
|
||||
})
|
||||
->toArray();
|
||||
table(
|
||||
['Domain', 'User Count', 'Status Count'],
|
||||
$data
|
||||
);
|
||||
}
|
||||
|
||||
protected function viewUnlistedInstances()
|
||||
{
|
||||
$data = Instance::whereUnlisted(true)
|
||||
->get(['domain', 'user_count', 'status_count', 'banned'])
|
||||
->map(function($d) {
|
||||
return [
|
||||
'domain' => $d->domain,
|
||||
'user_count' => number_format($d->user_count),
|
||||
'status_count' => number_format($d->status_count),
|
||||
'banned' => $d->banned ? '✅' : null
|
||||
];
|
||||
})
|
||||
->toArray();
|
||||
table(
|
||||
['Domain', 'User Count', 'Status Count', 'Banned'],
|
||||
$data
|
||||
);
|
||||
}
|
||||
}
|
|
@ -0,0 +1,140 @@
|
|||
<?php
|
||||
|
||||
namespace App\Console\Commands;
|
||||
|
||||
use Illuminate\Console\Command;
|
||||
use App\Media;
|
||||
use Cache, Storage;
|
||||
use Illuminate\Contracts\Console\PromptsForMissingInput;
|
||||
|
||||
class MediaCloudUrlRewrite extends Command implements PromptsForMissingInput
|
||||
{
|
||||
/**
|
||||
* The name and signature of the console command.
|
||||
*
|
||||
* @var string
|
||||
*/
|
||||
protected $signature = 'media:cloud-url-rewrite {oldDomain} {newDomain}';
|
||||
|
||||
/**
|
||||
* Prompt for missing input arguments using the returned questions.
|
||||
*
|
||||
* @return array
|
||||
*/
|
||||
protected function promptForMissingArgumentsUsing()
|
||||
{
|
||||
return [
|
||||
'oldDomain' => 'The old S3 domain',
|
||||
'newDomain' => 'The new S3 domain'
|
||||
];
|
||||
}
|
||||
/**
|
||||
* The console command description.
|
||||
*
|
||||
* @var string
|
||||
*/
|
||||
protected $description = 'Rewrite S3 media urls from local users';
|
||||
|
||||
/**
|
||||
* Execute the console command.
|
||||
*/
|
||||
public function handle()
|
||||
{
|
||||
$this->preflightCheck();
|
||||
$this->bootMessage();
|
||||
$this->confirmCloudUrl();
|
||||
}
|
||||
|
||||
protected function preflightCheck()
|
||||
{
|
||||
if(!(bool) config_cache('pixelfed.cloud_storage')) {
|
||||
$this->info('Error: Cloud storage is not enabled!');
|
||||
$this->error('Aborting...');
|
||||
exit;
|
||||
}
|
||||
}
|
||||
|
||||
protected function bootMessage()
|
||||
{
|
||||
$this->info(' ____ _ ______ __ ');
|
||||
$this->info(' / __ \(_) _____ / / __/__ ____/ / ');
|
||||
$this->info(' / /_/ / / |/_/ _ \/ / /_/ _ \/ __ / ');
|
||||
$this->info(' / ____/ /> </ __/ / __/ __/ /_/ / ');
|
||||
$this->info(' /_/ /_/_/|_|\___/_/_/ \___/\__,_/ ');
|
||||
$this->info(' ');
|
||||
$this->info(' Media Cloud Url Rewrite Tool');
|
||||
$this->info(' ===');
|
||||
$this->info(' Old S3: ' . trim($this->argument('oldDomain')));
|
||||
$this->info(' New S3: ' . trim($this->argument('newDomain')));
|
||||
$this->info(' ');
|
||||
}
|
||||
|
||||
protected function confirmCloudUrl()
|
||||
{
|
||||
$disk = Storage::disk(config('filesystems.cloud'))->url('test');
|
||||
$domain = parse_url($disk, PHP_URL_HOST);
|
||||
if(trim($this->argument('newDomain')) !== $domain) {
|
||||
$this->error('Error: The new S3 domain you entered is not currently configured');
|
||||
exit;
|
||||
}
|
||||
|
||||
if(!$this->confirm('Confirm this is correct')) {
|
||||
$this->error('Aborting...');
|
||||
exit;
|
||||
}
|
||||
|
||||
$this->updateUrls();
|
||||
}
|
||||
|
||||
protected function updateUrls()
|
||||
{
|
||||
$this->info('Updating urls...');
|
||||
$oldDomain = trim($this->argument('oldDomain'));
|
||||
$newDomain = trim($this->argument('newDomain'));
|
||||
$disk = Storage::disk(config('filesystems.cloud'));
|
||||
$count = Media::whereNotNull('cdn_url')->count();
|
||||
$bar = $this->output->createProgressBar($count);
|
||||
$counter = 0;
|
||||
$bar->start();
|
||||
foreach(Media::whereNotNull('cdn_url')->lazyById(1000, 'id') as $media) {
|
||||
if(strncmp($media->media_path, 'http', 4) === 0) {
|
||||
$bar->advance();
|
||||
continue;
|
||||
}
|
||||
$cdnHost = parse_url($media->cdn_url, PHP_URL_HOST);
|
||||
if($oldDomain != $cdnHost || $newDomain == $cdnHost) {
|
||||
$bar->advance();
|
||||
continue;
|
||||
}
|
||||
|
||||
$media->cdn_url = str_replace($oldDomain, $newDomain, $media->cdn_url);
|
||||
|
||||
if($media->thumbnail_url != null) {
|
||||
$thumbHost = parse_url($media->thumbnail_url, PHP_URL_HOST);
|
||||
if($thumbHost == $oldDomain) {
|
||||
$thumbUrl = $disk->url($media->thumbnail_path);
|
||||
$media->thumbnail_url = $thumbUrl;
|
||||
}
|
||||
}
|
||||
|
||||
if($media->optimized_url != null) {
|
||||
$optiHost = parse_url($media->optimized_url, PHP_URL_HOST);
|
||||
if($optiHost == $oldDomain) {
|
||||
$optiUrl = str_replace($oldDomain, $newDomain, $media->optimized_url);
|
||||
$media->optimized_url = $optiUrl;
|
||||
}
|
||||
}
|
||||
|
||||
$media->save();
|
||||
$counter++;
|
||||
$bar->advance();
|
||||
}
|
||||
|
||||
$bar->finish();
|
||||
|
||||
$this->line(' ');
|
||||
$this->info('Finished! Updated ' . $counter . ' total records!');
|
||||
$this->line(' ');
|
||||
$this->info('Tip: Run `php artisan cache:clear` to purge cached urls');
|
||||
}
|
||||
}
|
|
@ -45,7 +45,7 @@ class MediaS3GarbageCollector extends Command
|
|||
*/
|
||||
public function handle()
|
||||
{
|
||||
$enabled = config('pixelfed.cloud_storage');
|
||||
$enabled = (bool) config_cache('pixelfed.cloud_storage');
|
||||
if(!$enabled) {
|
||||
$this->error('Cloud storage not enabled. Exiting...');
|
||||
return;
|
||||
|
|
|
@ -0,0 +1,31 @@
|
|||
<?php
|
||||
|
||||
namespace App\Console\Commands;
|
||||
|
||||
use Illuminate\Console\Command;
|
||||
use App\Jobs\InternalPipeline\NotificationEpochUpdatePipeline;
|
||||
|
||||
class NotificationEpochUpdate extends Command
|
||||
{
|
||||
/**
|
||||
* The name and signature of the console command.
|
||||
*
|
||||
* @var string
|
||||
*/
|
||||
protected $signature = 'app:notification-epoch-update';
|
||||
|
||||
/**
|
||||
* The console command description.
|
||||
*
|
||||
* @var string
|
||||
*/
|
||||
protected $description = 'Update notification epoch';
|
||||
|
||||
/**
|
||||
* Execute the console command.
|
||||
*/
|
||||
public function handle()
|
||||
{
|
||||
NotificationEpochUpdatePipeline::dispatch();
|
||||
}
|
||||
}
|
|
@ -0,0 +1,37 @@
|
|||
<?php
|
||||
|
||||
namespace App\Console\Commands;
|
||||
|
||||
use Illuminate\Console\Command;
|
||||
use App\Services\Internal\SoftwareUpdateService;
|
||||
use Cache;
|
||||
|
||||
class SoftwareUpdateRefresh extends Command
|
||||
{
|
||||
/**
|
||||
* The name and signature of the console command.
|
||||
*
|
||||
* @var string
|
||||
*/
|
||||
protected $signature = 'app:software-update-refresh';
|
||||
|
||||
/**
|
||||
* The console command description.
|
||||
*
|
||||
* @var string
|
||||
*/
|
||||
protected $description = 'Refresh latest software version data';
|
||||
|
||||
/**
|
||||
* Execute the console command.
|
||||
*/
|
||||
public function handle()
|
||||
{
|
||||
$key = SoftwareUpdateService::cacheKey();
|
||||
Cache::forget($key);
|
||||
Cache::remember($key, 1209600, function() {
|
||||
return SoftwareUpdateService::fetchLatest();
|
||||
});
|
||||
$this->info('Succesfully updated software versions!');
|
||||
}
|
||||
}
|
|
@ -0,0 +1,153 @@
|
|||
<?php
|
||||
|
||||
namespace App\Console\Commands;
|
||||
|
||||
use Illuminate\Console\Command;
|
||||
use App\Models\ImportPost;
|
||||
use App\Services\ImportService;
|
||||
use App\Media;
|
||||
use App\Profile;
|
||||
use App\Status;
|
||||
use Storage;
|
||||
use App\Services\AccountService;
|
||||
use App\Services\MediaPathService;
|
||||
use Illuminate\Support\Str;
|
||||
use App\Util\Lexer\Autolink;
|
||||
|
||||
class TransformImports extends Command
|
||||
{
|
||||
/**
|
||||
* The name and signature of the console command.
|
||||
*
|
||||
* @var string
|
||||
*/
|
||||
protected $signature = 'app:transform-imports';
|
||||
|
||||
/**
|
||||
* The console command description.
|
||||
*
|
||||
* @var string
|
||||
*/
|
||||
protected $description = 'Transform imports into statuses';
|
||||
|
||||
/**
|
||||
* Execute the console command.
|
||||
*/
|
||||
public function handle()
|
||||
{
|
||||
if(!config('import.instagram.enabled')) {
|
||||
return;
|
||||
}
|
||||
|
||||
$ips = ImportPost::whereNull('status_id')->where('skip_missing_media', '!=', true)->take(500)->get();
|
||||
|
||||
if(!$ips->count()) {
|
||||
return;
|
||||
}
|
||||
|
||||
foreach($ips as $ip) {
|
||||
$id = $ip->user_id;
|
||||
$pid = $ip->profile_id;
|
||||
$profile = Profile::find($pid);
|
||||
if(!$profile) {
|
||||
$ip->skip_missing_media = true;
|
||||
$ip->save();
|
||||
continue;
|
||||
}
|
||||
|
||||
$exists = ImportPost::whereUserId($id)
|
||||
->whereNotNull('status_id')
|
||||
->where('filename', $ip->filename)
|
||||
->where('creation_year', $ip->creation_year)
|
||||
->where('creation_month', $ip->creation_month)
|
||||
->where('creation_day', $ip->creation_day)
|
||||
->exists();
|
||||
|
||||
if($exists == true) {
|
||||
$ip->skip_missing_media = true;
|
||||
$ip->save();
|
||||
continue;
|
||||
}
|
||||
|
||||
$idk = ImportService::getId($ip->user_id, $ip->creation_year, $ip->creation_month, $ip->creation_day);
|
||||
if(!$idk) {
|
||||
$ip->skip_missing_media = true;
|
||||
$ip->save();
|
||||
continue;
|
||||
}
|
||||
|
||||
if(Storage::exists('imports/' . $id . '/' . $ip->filename) === false) {
|
||||
ImportService::clearAttempts($profile->id);
|
||||
ImportService::getPostCount($profile->id, true);
|
||||
$ip->skip_missing_media = true;
|
||||
$ip->save();
|
||||
continue;
|
||||
}
|
||||
|
||||
$missingMedia = false;
|
||||
foreach($ip->media as $ipm) {
|
||||
$fileName = last(explode('/', $ipm['uri']));
|
||||
$og = 'imports/' . $id . '/' . $fileName;
|
||||
if(!Storage::exists($og)) {
|
||||
$missingMedia = true;
|
||||
}
|
||||
}
|
||||
|
||||
if($missingMedia === true) {
|
||||
$ip->skip_missing_media = true;
|
||||
$ip->save();
|
||||
continue;
|
||||
}
|
||||
|
||||
$caption = $ip->caption;
|
||||
$status = new Status;
|
||||
$status->profile_id = $pid;
|
||||
$status->caption = $caption;
|
||||
$status->rendered = strlen(trim($caption)) ? Autolink::create()->autolink($ip->caption) : null;
|
||||
$status->type = $ip->post_type;
|
||||
|
||||
$status->scope = 'unlisted';
|
||||
$status->visibility = 'unlisted';
|
||||
$status->id = $idk['id'];
|
||||
$status->created_at = now()->parse($ip->creation_date);
|
||||
$status->save();
|
||||
|
||||
foreach($ip->media as $ipm) {
|
||||
$fileName = last(explode('/', $ipm['uri']));
|
||||
$ext = last(explode('.', $fileName));
|
||||
$basePath = MediaPathService::get($profile);
|
||||
$og = 'imports/' . $id . '/' . $fileName;
|
||||
if(!Storage::exists($og)) {
|
||||
$ip->skip_missing_media = true;
|
||||
$ip->save();
|
||||
continue;
|
||||
}
|
||||
$size = Storage::size($og);
|
||||
$mime = Storage::mimeType($og);
|
||||
$newFile = Str::random(40) . '.' . $ext;
|
||||
$np = $basePath . '/' . $newFile;
|
||||
Storage::move($og, $np);
|
||||
$media = new Media;
|
||||
$media->profile_id = $pid;
|
||||
$media->user_id = $id;
|
||||
$media->status_id = $status->id;
|
||||
$media->media_path = $np;
|
||||
$media->mime = $mime;
|
||||
$media->size = $size;
|
||||
$media->save();
|
||||
}
|
||||
|
||||
$ip->status_id = $status->id;
|
||||
$ip->creation_id = $idk['incr'];
|
||||
$ip->save();
|
||||
|
||||
$profile->status_count = $profile->status_count + 1;
|
||||
$profile->save();
|
||||
|
||||
AccountService::del($profile->id);
|
||||
|
||||
ImportService::clearAttempts($profile->id);
|
||||
ImportService::getPostCount($profile->id, true);
|
||||
}
|
||||
}
|
||||
}
|
|
@ -0,0 +1,123 @@
|
|||
<?php
|
||||
|
||||
namespace App\Console\Commands;
|
||||
|
||||
use App\Instance;
|
||||
use App\Profile;
|
||||
use App\Transformer\ActivityPub\Verb\DeleteActor;
|
||||
use App\User;
|
||||
use App\Util\ActivityPub\HttpSignature;
|
||||
use GuzzleHttp\Client;
|
||||
use GuzzleHttp\Pool;
|
||||
use Illuminate\Console\Command;
|
||||
use League\Fractal;
|
||||
use League\Fractal\Serializer\ArraySerializer;
|
||||
|
||||
use function Laravel\Prompts\confirm;
|
||||
use function Laravel\Prompts\search;
|
||||
use function Laravel\Prompts\table;
|
||||
|
||||
class UserAccountDelete extends Command
|
||||
{
|
||||
/**
|
||||
* The name and signature of the console command.
|
||||
*
|
||||
* @var string
|
||||
*/
|
||||
protected $signature = 'app:user-account-delete';
|
||||
|
||||
/**
|
||||
* The console command description.
|
||||
*
|
||||
* @var string
|
||||
*/
|
||||
protected $description = 'Federate Account Deletion';
|
||||
|
||||
/**
|
||||
* Execute the console command.
|
||||
*/
|
||||
public function handle()
|
||||
{
|
||||
$id = search(
|
||||
label: 'Search for the account to delete by username',
|
||||
placeholder: 'john.appleseed',
|
||||
options: fn (string $value) => strlen($value) > 0
|
||||
? User::withTrashed()->whereStatus('deleted')->where('username', 'like', "%{$value}%")->pluck('username', 'id')->all()
|
||||
: [],
|
||||
);
|
||||
|
||||
$user = User::withTrashed()->find($id);
|
||||
|
||||
table(
|
||||
['Username', 'Name', 'Email', 'Created'],
|
||||
[[$user->username, $user->name, $user->email, $user->created_at]]
|
||||
);
|
||||
|
||||
$confirmed = confirm(
|
||||
label: 'Do you want to federate this account deletion?',
|
||||
default: false,
|
||||
yes: 'Proceed',
|
||||
no: 'Cancel',
|
||||
hint: 'This action is irreversible'
|
||||
);
|
||||
|
||||
if (! $confirmed) {
|
||||
$this->error('Aborting...');
|
||||
exit;
|
||||
}
|
||||
|
||||
$profile = Profile::withTrashed()->find($user->profile_id);
|
||||
|
||||
$fractal = new Fractal\Manager();
|
||||
$fractal->setSerializer(new ArraySerializer());
|
||||
$resource = new Fractal\Resource\Item($profile, new DeleteActor());
|
||||
$activity = $fractal->createData($resource)->toArray();
|
||||
|
||||
$audience = Instance::whereNotNull(['shared_inbox', 'nodeinfo_last_fetched'])
|
||||
->where('nodeinfo_last_fetched', '>', now()->subHours(12))
|
||||
->distinct()
|
||||
->pluck('shared_inbox');
|
||||
|
||||
$payload = json_encode($activity);
|
||||
|
||||
$client = new Client([
|
||||
'timeout' => 10,
|
||||
]);
|
||||
|
||||
$version = config('pixelfed.version');
|
||||
$appUrl = config('app.url');
|
||||
$userAgent = "(Pixelfed/{$version}; +{$appUrl})";
|
||||
|
||||
$requests = function ($audience) use ($client, $activity, $profile, $payload, $userAgent) {
|
||||
foreach ($audience as $url) {
|
||||
$headers = HttpSignature::sign($profile, $url, $activity, [
|
||||
'Content-Type' => 'application/ld+json; profile="https://www.w3.org/ns/activitystreams"',
|
||||
'User-Agent' => $userAgent,
|
||||
]);
|
||||
yield function () use ($client, $url, $headers, $payload) {
|
||||
return $client->postAsync($url, [
|
||||
'curl' => [
|
||||
CURLOPT_HTTPHEADER => $headers,
|
||||
CURLOPT_POSTFIELDS => $payload,
|
||||
CURLOPT_HEADER => true,
|
||||
CURLOPT_SSL_VERIFYPEER => false,
|
||||
CURLOPT_SSL_VERIFYHOST => false,
|
||||
],
|
||||
]);
|
||||
};
|
||||
}
|
||||
};
|
||||
|
||||
$pool = new Pool($client, $requests($audience), [
|
||||
'concurrency' => 50,
|
||||
'fulfilled' => function ($response, $index) {
|
||||
},
|
||||
'rejected' => function ($reason, $index) {
|
||||
},
|
||||
]);
|
||||
|
||||
$promise = $pool->promise();
|
||||
|
||||
$promise->wait();
|
||||
}
|
||||
}
|
|
@ -3,16 +3,17 @@
|
|||
namespace App\Console\Commands;
|
||||
|
||||
use Illuminate\Console\Command;
|
||||
use Illuminate\Contracts\Console\PromptsForMissingInput;
|
||||
use App\User;
|
||||
|
||||
class UserAdmin extends Command
|
||||
class UserAdmin extends Command implements PromptsForMissingInput
|
||||
{
|
||||
/**
|
||||
* The name and signature of the console command.
|
||||
*
|
||||
* @var string
|
||||
*/
|
||||
protected $signature = 'user:admin {id}';
|
||||
protected $signature = 'user:admin {username}';
|
||||
|
||||
/**
|
||||
* The console command description.
|
||||
|
@ -22,13 +23,15 @@ class UserAdmin extends Command
|
|||
protected $description = 'Make a user an admin, or remove admin privileges.';
|
||||
|
||||
/**
|
||||
* Create a new command instance.
|
||||
* Prompt for missing input arguments using the returned questions.
|
||||
*
|
||||
* @return void
|
||||
* @return array
|
||||
*/
|
||||
public function __construct()
|
||||
protected function promptForMissingArgumentsUsing()
|
||||
{
|
||||
parent::__construct();
|
||||
return [
|
||||
'username' => 'Which username should we toggle admin privileges for?',
|
||||
];
|
||||
}
|
||||
|
||||
/**
|
||||
|
@ -38,16 +41,15 @@ class UserAdmin extends Command
|
|||
*/
|
||||
public function handle()
|
||||
{
|
||||
$id = $this->argument('id');
|
||||
if(ctype_digit($id) == true) {
|
||||
$user = User::find($id);
|
||||
} else {
|
||||
$user = User::whereUsername($id)->first();
|
||||
}
|
||||
$id = $this->argument('username');
|
||||
|
||||
$user = User::whereUsername($id)->first();
|
||||
|
||||
if(!$user) {
|
||||
$this->error('Could not find any user with that username or id.');
|
||||
exit;
|
||||
}
|
||||
|
||||
$this->info('Found username: ' . $user->username);
|
||||
$state = $user->is_admin ? 'Remove admin privileges from this user?' : 'Add admin privileges to this user?';
|
||||
$confirmed = $this->confirm($state);
|
||||
|
|
|
@ -52,7 +52,7 @@ class UserCreate extends Command
|
|||
$user->name = $o['name'];
|
||||
$user->email = $o['email'];
|
||||
$user->password = bcrypt($o['password']);
|
||||
$user->is_admin = (bool) $o['is_admin'];
|
||||
$user->is_admin = $o['is_admin'] == 'true';
|
||||
$user->email_verified_at = $o['confirm_email'] ? now() : null;
|
||||
$user->save();
|
||||
|
||||
|
|
|
@ -0,0 +1,61 @@
|
|||
<?php
|
||||
|
||||
namespace App\Console\Commands;
|
||||
|
||||
use Illuminate\Console\Command;
|
||||
use Illuminate\Contracts\Console\PromptsForMissingInput;
|
||||
use App\User;
|
||||
|
||||
class UserToggle2FA extends Command implements PromptsForMissingInput
|
||||
{
|
||||
/**
|
||||
* The name and signature of the console command.
|
||||
*
|
||||
* @var string
|
||||
*/
|
||||
protected $signature = 'user:2fa {username}';
|
||||
|
||||
/**
|
||||
* The console command description.
|
||||
*
|
||||
* @var string
|
||||
*/
|
||||
protected $description = 'Disable two factor authentication for given username';
|
||||
|
||||
/**
|
||||
* Prompt for missing input arguments using the returned questions.
|
||||
*
|
||||
* @return array
|
||||
*/
|
||||
protected function promptForMissingArgumentsUsing()
|
||||
{
|
||||
return [
|
||||
'username' => 'Which username should we disable 2FA for?',
|
||||
];
|
||||
}
|
||||
|
||||
/**
|
||||
* Execute the console command.
|
||||
*/
|
||||
public function handle()
|
||||
{
|
||||
$user = User::whereUsername($this->argument('username'))->first();
|
||||
|
||||
if(!$user) {
|
||||
$this->error('Could not find any user with that username');
|
||||
exit;
|
||||
}
|
||||
|
||||
if(!$user->{'2fa_enabled'}) {
|
||||
$this->info('User did not have 2FA enabled!');
|
||||
return;
|
||||
}
|
||||
|
||||
$user->{'2fa_enabled'} = false;
|
||||
$user->{'2fa_secret'} = null;
|
||||
$user->{'2fa_backup_codes'} = null;
|
||||
$user->save();
|
||||
|
||||
$this->info('Successfully disabled 2FA on this account!');
|
||||
}
|
||||
}
|
|
@ -25,17 +25,32 @@ class Kernel extends ConsoleKernel
|
|||
*/
|
||||
protected function schedule(Schedule $schedule)
|
||||
{
|
||||
$schedule->command('media:optimize')->hourlyAt(40);
|
||||
$schedule->command('media:gc')->hourlyAt(5);
|
||||
$schedule->command('horizon:snapshot')->everyFiveMinutes();
|
||||
$schedule->command('story:gc')->everyFiveMinutes();
|
||||
$schedule->command('gc:failedjobs')->dailyAt(3);
|
||||
$schedule->command('gc:passwordreset')->dailyAt('09:41');
|
||||
$schedule->command('gc:sessions')->twiceDaily(13, 23);
|
||||
$schedule->command('media:optimize')->hourlyAt(40)->onOneServer();
|
||||
$schedule->command('media:gc')->hourlyAt(5)->onOneServer();
|
||||
$schedule->command('horizon:snapshot')->everyFiveMinutes()->onOneServer();
|
||||
$schedule->command('story:gc')->everyFiveMinutes()->onOneServer();
|
||||
$schedule->command('gc:failedjobs')->dailyAt(3)->onOneServer();
|
||||
$schedule->command('gc:passwordreset')->dailyAt('09:41')->onOneServer();
|
||||
$schedule->command('gc:sessions')->twiceDaily(13, 23)->onOneServer();
|
||||
|
||||
if(config('pixelfed.cloud_storage') && config('media.delete_local_after_cloud')) {
|
||||
if ((bool) config_cache('pixelfed.cloud_storage') && (bool) config_cache('media.delete_local_after_cloud')) {
|
||||
$schedule->command('media:s3gc')->hourlyAt(15);
|
||||
}
|
||||
|
||||
if (config('import.instagram.enabled')) {
|
||||
$schedule->command('app:transform-imports')->everyTenMinutes()->onOneServer();
|
||||
$schedule->command('app:import-upload-garbage-collection')->hourlyAt(51)->onOneServer();
|
||||
$schedule->command('app:import-remove-deleted-accounts')->hourlyAt(37)->onOneServer();
|
||||
$schedule->command('app:import-upload-clean-storage')->twiceDailyAt(1, 13, 32)->onOneServer();
|
||||
|
||||
if (config('import.instagram.storage.cloud.enabled') && (bool) config_cache('pixelfed.cloud_storage')) {
|
||||
$schedule->command('app:import-upload-media-to-cloud-storage')->hourlyAt(39)->onOneServer();
|
||||
}
|
||||
}
|
||||
|
||||
$schedule->command('app:notification-epoch-update')->weeklyOn(1, '2:21')->onOneServer();
|
||||
$schedule->command('app:hashtag-cached-count-update')->hourlyAt(25)->onOneServer();
|
||||
$schedule->command('app:account-post-count-stat-update')->everySixHours(25)->onOneServer();
|
||||
}
|
||||
|
||||
/**
|
||||
|
@ -45,7 +60,7 @@ class Kernel extends ConsoleKernel
|
|||
*/
|
||||
protected function commands()
|
||||
{
|
||||
$this->load(__DIR__.'/Commands');
|
||||
$this->load(__DIR__ . '/Commands');
|
||||
|
||||
require base_path('routes/console.php');
|
||||
}
|
||||
|
|
|
@ -31,20 +31,4 @@ class DirectMessage extends Model
|
|||
{
|
||||
return Auth::user()->profile->id === $this->from_id;
|
||||
}
|
||||
|
||||
public function toText()
|
||||
{
|
||||
$actorName = $this->author->username;
|
||||
|
||||
return "{$actorName} sent a direct message.";
|
||||
}
|
||||
|
||||
public function toHtml()
|
||||
{
|
||||
$actorName = $this->author->username;
|
||||
$actorUrl = $this->author->url();
|
||||
$url = $this->url();
|
||||
|
||||
return "{$actorName} sent a direct message.";
|
||||
}
|
||||
}
|
||||
|
|
|
@ -32,20 +32,4 @@ class Follower extends Model
|
|||
$path = $this->actor->permalink("#accepts/follows/{$this->id}{$append}");
|
||||
return url($path);
|
||||
}
|
||||
|
||||
public function toText()
|
||||
{
|
||||
$actorName = $this->actor->username;
|
||||
|
||||
return "{$actorName} ".__('notification.startedFollowingYou');
|
||||
}
|
||||
|
||||
public function toHtml()
|
||||
{
|
||||
$actorName = $this->actor->username;
|
||||
$actorUrl = $this->actor->url();
|
||||
|
||||
return "<a href='{$actorUrl}' class='profile-link'>{$actorName}</a> ".
|
||||
__('notification.startedFollowingYou');
|
||||
}
|
||||
}
|
||||
|
|
|
@ -12,6 +12,8 @@ class HashtagFollow extends Model
|
|||
'hashtag_id'
|
||||
];
|
||||
|
||||
const MAX_LIMIT = 250;
|
||||
|
||||
public function hashtag()
|
||||
{
|
||||
return $this->belongsTo(Hashtag::class);
|
||||
|
|
|
@ -157,7 +157,7 @@ class AccountController extends Controller
|
|||
|
||||
$pid = $request->user()->profile_id;
|
||||
$count = UserFilterService::muteCount($pid);
|
||||
$maxLimit = intval(config('instance.user_filters.max_user_mutes'));
|
||||
$maxLimit = (int) config_cache('instance.user_filters.max_user_mutes');
|
||||
abort_if($count >= $maxLimit, 422, self::FILTER_LIMIT_MUTE_TEXT . $maxLimit . ' accounts');
|
||||
if($count == 0) {
|
||||
$filterCount = UserFilter::whereUserId($pid)->count();
|
||||
|
@ -260,7 +260,7 @@ class AccountController extends Controller
|
|||
]);
|
||||
$pid = $request->user()->profile_id;
|
||||
$count = UserFilterService::blockCount($pid);
|
||||
$maxLimit = intval(config('instance.user_filters.max_user_blocks'));
|
||||
$maxLimit = (int) config_cache('instance.user_filters.max_user_blocks');
|
||||
abort_if($count >= $maxLimit, 422, self::FILTER_LIMIT_BLOCK_TEXT . $maxLimit . ' accounts');
|
||||
if($count == 0) {
|
||||
$filterCount = UserFilter::whereUserId($pid)->whereFilterType('block')->count();
|
||||
|
|
|
@ -0,0 +1,255 @@
|
|||
<?php
|
||||
|
||||
namespace App\Http\Controllers\Admin;
|
||||
|
||||
use DB, Cache;
|
||||
use App\{
|
||||
AccountInterstitial,
|
||||
DiscoverCategory,
|
||||
DiscoverCategoryHashtag,
|
||||
Hashtag,
|
||||
Media,
|
||||
Profile,
|
||||
Status,
|
||||
StatusHashtag,
|
||||
User
|
||||
};
|
||||
use App\Models\ConfigCache;
|
||||
use App\Models\AutospamCustomTokens;
|
||||
use App\Services\AccountService;
|
||||
use App\Services\ConfigCacheService;
|
||||
use App\Services\StatusService;
|
||||
use Carbon\Carbon;
|
||||
use Illuminate\Http\Request;
|
||||
use Illuminate\Validation\Rule;
|
||||
use League\ISO3166\ISO3166;
|
||||
use Illuminate\Support\Str;
|
||||
use Illuminate\Support\Facades\Storage;
|
||||
use Illuminate\Support\Facades\Validator;
|
||||
use Illuminate\Support\Facades\Http;
|
||||
use App\Http\Controllers\PixelfedDirectoryController;
|
||||
use \DateInterval;
|
||||
use \DatePeriod;
|
||||
use App\Http\Resources\AdminSpamReport;
|
||||
use App\Util\Lexer\Classifier;
|
||||
use App\Jobs\AutospamPipeline\AutospamPretrainPipeline;
|
||||
use App\Jobs\AutospamPipeline\AutospamPretrainNonSpamPipeline;
|
||||
use App\Jobs\AutospamPipeline\AutospamUpdateCachedDataPipeline;
|
||||
use Illuminate\Support\Facades\URL;
|
||||
use App\Services\AutospamService;
|
||||
|
||||
trait AdminAutospamController
|
||||
{
|
||||
public function autospamHome(Request $request)
|
||||
{
|
||||
return view('admin.autospam.home');
|
||||
}
|
||||
|
||||
public function getAutospamConfigApi(Request $request)
|
||||
{
|
||||
$open = Cache::remember('admin-dash:reports:spam-count', 3600, function() {
|
||||
return AccountInterstitial::whereType('post.autospam')->whereNull('appeal_handled_at')->count();
|
||||
});
|
||||
|
||||
$closed = Cache::remember('admin-dash:reports:spam-count-closed', 3600, function() {
|
||||
return AccountInterstitial::whereType('post.autospam')->whereNotNull('appeal_handled_at')->count();
|
||||
});
|
||||
|
||||
$thisWeek = Cache::remember('admin-dash:reports:spam-count-stats-this-week ', 86400, function() {
|
||||
$sr = config('database.default') == 'pgsql' ? "to_char(created_at, 'MM-YYYY')" : "DATE_FORMAT(created_at, '%m-%Y')";
|
||||
$gb = config('database.default') == 'pgsql' ? [DB::raw($sr)] : DB::raw($sr);
|
||||
$s = AccountInterstitial::select(
|
||||
DB::raw('count(id) as count'),
|
||||
DB::raw($sr . " as month_year")
|
||||
)
|
||||
->where('created_at', '>=', now()->subWeeks(52))
|
||||
->groupBy($gb)
|
||||
->get()
|
||||
->map(function($s) {
|
||||
$dt = now()->parse('01-' . $s->month_year);
|
||||
return [
|
||||
'id' => $dt->format('Ym'),
|
||||
'x' => $dt->format('M Y'),
|
||||
'y' => $s->count
|
||||
];
|
||||
})
|
||||
->sortBy('id')
|
||||
->values()
|
||||
->toArray();
|
||||
return $s;
|
||||
});
|
||||
|
||||
$files = [
|
||||
'spam' => [
|
||||
'exists' => Storage::exists(AutospamService::MODEL_SPAM_PATH),
|
||||
'size' => 0
|
||||
],
|
||||
'ham' => [
|
||||
'exists' => Storage::exists(AutospamService::MODEL_HAM_PATH),
|
||||
'size' => 0
|
||||
],
|
||||
'combined' => [
|
||||
'exists' => Storage::exists(AutospamService::MODEL_FILE_PATH),
|
||||
'size' => 0
|
||||
]
|
||||
];
|
||||
|
||||
if($files['spam']['exists']) {
|
||||
$files['spam']['size'] = Storage::size(AutospamService::MODEL_SPAM_PATH);
|
||||
}
|
||||
|
||||
if($files['ham']['exists']) {
|
||||
$files['ham']['size'] = Storage::size(AutospamService::MODEL_HAM_PATH);
|
||||
}
|
||||
|
||||
if($files['combined']['exists']) {
|
||||
$files['combined']['size'] = Storage::size(AutospamService::MODEL_FILE_PATH);
|
||||
}
|
||||
|
||||
return [
|
||||
'autospam_enabled' => (bool) config_cache('pixelfed.bouncer.enabled') ?? false,
|
||||
'nlp_enabled' => (bool) AutospamService::active(),
|
||||
'files' => $files,
|
||||
'open' => $open,
|
||||
'closed' => $closed,
|
||||
'graph' => collect($thisWeek)->map(fn($s) => $s['y'])->values(),
|
||||
'graphLabels' => collect($thisWeek)->map(fn($s) => $s['x'])->values()
|
||||
];
|
||||
}
|
||||
|
||||
public function getAutospamReportsClosedApi(Request $request)
|
||||
{
|
||||
$appeals = AdminSpamReport::collection(
|
||||
AccountInterstitial::orderBy('id', 'desc')
|
||||
->whereType('post.autospam')
|
||||
->whereIsSpam(true)
|
||||
->whereNotNull('appeal_handled_at')
|
||||
->cursorPaginate(6)
|
||||
->withQueryString()
|
||||
);
|
||||
|
||||
return $appeals;
|
||||
}
|
||||
|
||||
public function postAutospamTrainSpamApi(Request $request)
|
||||
{
|
||||
$aiCount = AccountInterstitial::whereItemType('App\Status')
|
||||
->whereIsSpam(true)
|
||||
->count();
|
||||
abort_if($aiCount < 100, 422, 'You don\'t have enough data to pre-train against.');
|
||||
|
||||
$existing = Cache::get('pf:admin:autospam:pretrain:recent');
|
||||
abort_if($existing, 422, 'You\'ve already run this recently, please wait 30 minutes before pre-training again');
|
||||
AutospamPretrainPipeline::dispatch();
|
||||
Cache::put('pf:admin:autospam:pretrain:recent', 1, 1440);
|
||||
|
||||
return [
|
||||
'msg' => 'Success!'
|
||||
];
|
||||
}
|
||||
|
||||
public function postAutospamTrainNonSpamSearchApi(Request $request)
|
||||
{
|
||||
$this->validate($request, [
|
||||
'q' => 'required|string|min:1'
|
||||
]);
|
||||
|
||||
$q = $request->input('q');
|
||||
|
||||
$res = Profile::whereNull(['status', 'domain'])
|
||||
->where('username', 'like', '%' . $q . '%')
|
||||
->orderByDesc('followers_count')
|
||||
->take(10)
|
||||
->get()
|
||||
->map(function($p) {
|
||||
$acct = AccountService::get($p->id, true);
|
||||
return [
|
||||
'id' => (string) $p->id,
|
||||
'avatar' => $acct['avatar'],
|
||||
'username' => $p->username
|
||||
];
|
||||
})
|
||||
->values();
|
||||
return $res;
|
||||
}
|
||||
|
||||
public function postAutospamTrainNonSpamSubmitApi(Request $request)
|
||||
{
|
||||
$this->validate($request, [
|
||||
'accounts' => 'required|array|min:1|max:10'
|
||||
]);
|
||||
|
||||
$accts = $request->input('accounts');
|
||||
|
||||
$accounts = Profile::whereNull(['domain', 'status'])->find(collect($accts)->map(function($a) { return $a['id'];}));
|
||||
|
||||
abort_if(!$accounts || !$accounts->count(), 422, 'One or more of the selected accounts are not valid');
|
||||
|
||||
AutospamPretrainNonSpamPipeline::dispatch($accounts);
|
||||
return $accounts;
|
||||
}
|
||||
|
||||
public function getAutospamCustomTokensApi(Request $request)
|
||||
{
|
||||
return AutospamCustomTokens::latest()->cursorPaginate(6);
|
||||
}
|
||||
|
||||
public function saveNewAutospamCustomTokensApi(Request $request)
|
||||
{
|
||||
$this->validate($request, [
|
||||
'token' => 'required|unique:autospam_custom_tokens,token',
|
||||
]);
|
||||
|
||||
$ct = new AutospamCustomTokens;
|
||||
$ct->token = $request->input('token');
|
||||
$ct->weight = $request->input('weight');
|
||||
$ct->category = $request->input('category') === 'spam' ? 'spam' : 'ham';
|
||||
$ct->note = $request->input('note');
|
||||
$ct->active = $request->input('active');
|
||||
$ct->save();
|
||||
|
||||
AutospamUpdateCachedDataPipeline::dispatch();
|
||||
return $ct;
|
||||
}
|
||||
|
||||
public function updateAutospamCustomTokensApi(Request $request)
|
||||
{
|
||||
$this->validate($request, [
|
||||
'id' => 'required',
|
||||
'token' => 'required',
|
||||
'category' => 'required|in:spam,ham',
|
||||
'active' => 'required|boolean'
|
||||
]);
|
||||
|
||||
$ct = AutospamCustomTokens::findOrFail($request->input('id'));
|
||||
$ct->weight = $request->input('weight');
|
||||
$ct->category = $request->input('category');
|
||||
$ct->note = $request->input('note');
|
||||
$ct->active = $request->input('active');
|
||||
$ct->save();
|
||||
|
||||
AutospamUpdateCachedDataPipeline::dispatch();
|
||||
|
||||
return $ct;
|
||||
}
|
||||
|
||||
public function exportAutospamCustomTokensApi(Request $request)
|
||||
{
|
||||
abort_if(!Storage::exists(AutospamService::MODEL_SPAM_PATH), 422, 'Autospam Dataset does not exist, please train spam before attempting to export');
|
||||
return Storage::download(AutospamService::MODEL_SPAM_PATH);
|
||||
}
|
||||
|
||||
public function enableAutospamApi(Request $request)
|
||||
{
|
||||
ConfigCacheService::put('autospam.nlp.enabled', true);
|
||||
Cache::forget(AutospamService::CHCKD_CACHE_KEY);
|
||||
return ['msg' => 'Success'];
|
||||
}
|
||||
|
||||
public function disableAutospamApi(Request $request)
|
||||
{
|
||||
ConfigCacheService::put('autospam.nlp.enabled', false);
|
||||
Cache::forget(AutospamService::CHCKD_CACHE_KEY);
|
||||
return ['msg' => 'Success'];
|
||||
}
|
||||
}
|
|
@ -2,30 +2,20 @@
|
|||
|
||||
namespace App\Http\Controllers\Admin;
|
||||
|
||||
use DB, Cache;
|
||||
use App\{
|
||||
DiscoverCategory,
|
||||
DiscoverCategoryHashtag,
|
||||
Hashtag,
|
||||
Media,
|
||||
Profile,
|
||||
Status,
|
||||
StatusHashtag,
|
||||
User
|
||||
};
|
||||
use App\Http\Controllers\PixelfedDirectoryController;
|
||||
use App\Models\ConfigCache;
|
||||
use App\Services\AccountService;
|
||||
use App\Services\ConfigCacheService;
|
||||
use App\Services\StatusService;
|
||||
use Carbon\Carbon;
|
||||
use App\Status;
|
||||
use App\User;
|
||||
use Cache;
|
||||
use Illuminate\Http\Request;
|
||||
use Illuminate\Validation\Rule;
|
||||
use League\ISO3166\ISO3166;
|
||||
use Illuminate\Support\Str;
|
||||
use Illuminate\Support\Facades\Http;
|
||||
use Illuminate\Support\Facades\Storage;
|
||||
use Illuminate\Support\Facades\Validator;
|
||||
use Illuminate\Support\Facades\Http;
|
||||
use App\Http\Controllers\PixelfedDirectoryController;
|
||||
use Illuminate\Support\Str;
|
||||
use League\ISO3166\ISO3166;
|
||||
|
||||
trait AdminDirectoryController
|
||||
{
|
||||
|
@ -41,40 +31,41 @@ trait AdminDirectoryController
|
|||
$res['countries'] = collect((new ISO3166)->all())->pluck('name');
|
||||
$res['admins'] = User::whereIsAdmin(true)
|
||||
->where('2fa_enabled', true)
|
||||
->get()->map(function($user) {
|
||||
return [
|
||||
'uid' => (string) $user->id,
|
||||
'pid' => (string) $user->profile_id,
|
||||
'username' => $user->username,
|
||||
'created_at' => $user->created_at
|
||||
];
|
||||
});
|
||||
->get()->map(function ($user) {
|
||||
return [
|
||||
'uid' => (string) $user->id,
|
||||
'pid' => (string) $user->profile_id,
|
||||
'username' => $user->username,
|
||||
'created_at' => $user->created_at,
|
||||
];
|
||||
});
|
||||
$config = ConfigCache::whereK('pixelfed.directory')->first();
|
||||
if($config) {
|
||||
if ($config) {
|
||||
$data = $config->v ? json_decode($config->v, true) : [];
|
||||
$res = array_merge($res, $data);
|
||||
}
|
||||
|
||||
if(empty($res['summary'])) {
|
||||
if (empty($res['summary'])) {
|
||||
$summary = ConfigCache::whereK('app.short_description')->pluck('v');
|
||||
$res['summary'] = $summary ? $summary[0] : null;
|
||||
}
|
||||
|
||||
if(isset($res['banner_image']) && !empty($res['banner_image'])) {
|
||||
if (isset($res['banner_image']) && ! empty($res['banner_image'])) {
|
||||
$res['banner_image'] = url(Storage::url($res['banner_image']));
|
||||
}
|
||||
|
||||
if(isset($res['favourite_posts'])) {
|
||||
$res['favourite_posts'] = collect($res['favourite_posts'])->map(function($id) {
|
||||
if (isset($res['favourite_posts'])) {
|
||||
$res['favourite_posts'] = collect($res['favourite_posts'])->map(function ($id) {
|
||||
return StatusService::get($id);
|
||||
})
|
||||
->filter(function($post) {
|
||||
return $post && isset($post['account']);
|
||||
})
|
||||
->values();
|
||||
->filter(function ($post) {
|
||||
return $post && isset($post['account']);
|
||||
})
|
||||
->values();
|
||||
}
|
||||
|
||||
$res['community_guidelines'] = config_cache('app.rules') ? json_decode(config_cache('app.rules'), true) : [];
|
||||
$res['curated_onboarding'] = (bool) config_cache('instance.curated_registration.enabled');
|
||||
$res['open_registration'] = (bool) config_cache('pixelfed.open_registration');
|
||||
$res['oauth_enabled'] = (bool) config_cache('pixelfed.oauth_enabled') && file_exists(storage_path('oauth-public.key')) && file_exists(storage_path('oauth-private.key'));
|
||||
|
||||
|
@ -83,22 +74,22 @@ trait AdminDirectoryController
|
|||
$res['feature_config'] = [
|
||||
'media_types' => Str::of(config_cache('pixelfed.media_types'))->explode(','),
|
||||
'image_quality' => config_cache('pixelfed.image_quality'),
|
||||
'optimize_image' => config_cache('pixelfed.optimize_image'),
|
||||
'optimize_image' => (bool) config_cache('pixelfed.optimize_image'),
|
||||
'max_photo_size' => config_cache('pixelfed.max_photo_size'),
|
||||
'max_caption_length' => config_cache('pixelfed.max_caption_length'),
|
||||
'max_altext_length' => config_cache('pixelfed.max_altext_length'),
|
||||
'enforce_account_limit' => config_cache('pixelfed.enforce_account_limit'),
|
||||
'enforce_account_limit' => (bool) config_cache('pixelfed.enforce_account_limit'),
|
||||
'max_account_size' => config_cache('pixelfed.max_account_size'),
|
||||
'max_album_length' => config_cache('pixelfed.max_album_length'),
|
||||
'account_deletion' => config_cache('pixelfed.account_deletion'),
|
||||
'account_deletion' => (bool) config_cache('pixelfed.account_deletion'),
|
||||
];
|
||||
|
||||
if(config_cache('pixelfed.directory.testimonials')) {
|
||||
$testimonials = collect(json_decode(config_cache('pixelfed.directory.testimonials'),true))
|
||||
->map(function($t) {
|
||||
if (config_cache('pixelfed.directory.testimonials')) {
|
||||
$testimonials = collect(json_decode(config_cache('pixelfed.directory.testimonials'), true))
|
||||
->map(function ($t) {
|
||||
return [
|
||||
'profile' => AccountService::get($t['profile_id']),
|
||||
'body' => $t['body']
|
||||
'body' => $t['body'],
|
||||
];
|
||||
});
|
||||
$res['testimonials'] = $testimonials;
|
||||
|
@ -107,8 +98,8 @@ trait AdminDirectoryController
|
|||
$validator = Validator::make($res['feature_config'], [
|
||||
'media_types' => [
|
||||
'required',
|
||||
function ($attribute, $value, $fail) {
|
||||
if (!in_array('image/jpeg', $value->toArray()) || !in_array('image/png', $value->toArray())) {
|
||||
function ($attribute, $value, $fail) {
|
||||
if (! in_array('image/jpeg', $value->toArray()) || ! in_array('image/png', $value->toArray())) {
|
||||
$fail('You must enable image/jpeg and image/png support.');
|
||||
}
|
||||
},
|
||||
|
@ -119,12 +110,12 @@ trait AdminDirectoryController
|
|||
'max_account_size' => 'required_if:enforce_account_limit,true|integer|min:1000000',
|
||||
'max_album_length' => 'required|integer|min:4|max:20',
|
||||
'account_deletion' => 'required|accepted',
|
||||
'max_caption_length' => 'required|integer|min:500|max:10000'
|
||||
'max_caption_length' => 'required|integer|min:500|max:10000',
|
||||
]);
|
||||
|
||||
$res['requirements_validator'] = $validator->errors();
|
||||
|
||||
$res['is_eligible'] = $res['open_registration'] &&
|
||||
$res['is_eligible'] = ($res['open_registration'] || $res['curated_onboarding']) &&
|
||||
$res['oauth_enabled'] &&
|
||||
$res['activitypub_enabled'] &&
|
||||
count($res['requirements_validator']) === 0 &&
|
||||
|
@ -145,11 +136,11 @@ trait AdminDirectoryController
|
|||
foreach (new \DirectoryIterator($path) as $io) {
|
||||
$name = $io->getFilename();
|
||||
$skip = ['vendor'];
|
||||
if($io->isDot() || in_array($name, $skip)) {
|
||||
if ($io->isDot() || in_array($name, $skip)) {
|
||||
continue;
|
||||
}
|
||||
|
||||
if($io->isDir()) {
|
||||
if ($io->isDir()) {
|
||||
$langs->push(['code' => $name, 'name' => locale_get_display_name($name)]);
|
||||
}
|
||||
}
|
||||
|
@ -158,25 +149,26 @@ trait AdminDirectoryController
|
|||
$res['primary_locale'] = config('app.locale');
|
||||
|
||||
$submissionState = Http::withoutVerifying()
|
||||
->post('https://pixelfed.org/api/v1/directory/check-submission', [
|
||||
'domain' => config('pixelfed.domain.app')
|
||||
]);
|
||||
->post('https://pixelfed.org/api/v1/directory/check-submission', [
|
||||
'domain' => config('pixelfed.domain.app'),
|
||||
]);
|
||||
|
||||
$res['submission_state'] = $submissionState->json();
|
||||
|
||||
return $res;
|
||||
}
|
||||
|
||||
protected function validVal($res, $val, $count = false, $minLen = false)
|
||||
{
|
||||
if(!isset($res[$val])) {
|
||||
if (! isset($res[$val])) {
|
||||
return false;
|
||||
}
|
||||
|
||||
if($count) {
|
||||
if ($count) {
|
||||
return count($res[$val]) >= $count;
|
||||
}
|
||||
|
||||
if($minLen) {
|
||||
if ($minLen) {
|
||||
return strlen($res[$val]) >= $minLen;
|
||||
}
|
||||
|
||||
|
@ -193,11 +185,11 @@ trait AdminDirectoryController
|
|||
'favourite_posts' => 'array|max:12',
|
||||
'favourite_posts.*' => 'distinct',
|
||||
'privacy_pledge' => 'sometimes',
|
||||
'banner_image' => 'sometimes|mimes:jpg,png|dimensions:width=1920,height:1080|max:5000'
|
||||
'banner_image' => 'sometimes|mimes:jpg,png|dimensions:width=1920,height:1080|max:5000',
|
||||
]);
|
||||
|
||||
$config = ConfigCache::firstOrNew([
|
||||
'k' => 'pixelfed.directory'
|
||||
'k' => 'pixelfed.directory',
|
||||
]);
|
||||
|
||||
$res = $config->v ? json_decode($config->v, true) : [];
|
||||
|
@ -207,27 +199,28 @@ trait AdminDirectoryController
|
|||
$res['contact_email'] = $request->input('contact_email');
|
||||
$res['privacy_pledge'] = (bool) $request->input('privacy_pledge');
|
||||
|
||||
if($request->filled('location')) {
|
||||
if ($request->filled('location')) {
|
||||
$exists = (new ISO3166)->name($request->location);
|
||||
if($exists) {
|
||||
if ($exists) {
|
||||
$res['location'] = $request->input('location');
|
||||
}
|
||||
}
|
||||
|
||||
if($request->hasFile('banner_image')) {
|
||||
if ($request->hasFile('banner_image')) {
|
||||
collect(Storage::files('public/headers'))
|
||||
->filter(function($name) {
|
||||
$protected = [
|
||||
'public/headers/.gitignore',
|
||||
'public/headers/default.jpg',
|
||||
'public/headers/missing.png'
|
||||
];
|
||||
return !in_array($name, $protected);
|
||||
})
|
||||
->each(function($name) {
|
||||
Storage::delete($name);
|
||||
});
|
||||
$path = $request->file('banner_image')->store('public/headers');
|
||||
->filter(function ($name) {
|
||||
$protected = [
|
||||
'public/headers/.gitignore',
|
||||
'public/headers/default.jpg',
|
||||
'public/headers/missing.png',
|
||||
];
|
||||
|
||||
return ! in_array($name, $protected);
|
||||
})
|
||||
->each(function ($name) {
|
||||
Storage::delete($name);
|
||||
});
|
||||
$path = $request->file('banner_image')->storePublicly('public/headers');
|
||||
$res['banner_image'] = $path;
|
||||
ConfigCacheService::put('app.banner_image', url(Storage::url($path)));
|
||||
|
||||
|
@ -239,9 +232,10 @@ trait AdminDirectoryController
|
|||
|
||||
ConfigCacheService::put('pixelfed.directory', $config->v);
|
||||
$updated = json_decode($config->v, true);
|
||||
if(isset($updated['banner_image'])) {
|
||||
if (isset($updated['banner_image'])) {
|
||||
$updated['banner_image'] = url(Storage::url($updated['banner_image']));
|
||||
}
|
||||
|
||||
return $updated;
|
||||
}
|
||||
|
||||
|
@ -249,9 +243,10 @@ trait AdminDirectoryController
|
|||
{
|
||||
$reqs = [];
|
||||
$reqs['feature_config'] = [
|
||||
'open_registration' => config_cache('pixelfed.open_registration'),
|
||||
'open_registration' => (bool) config_cache('pixelfed.open_registration'),
|
||||
'curated_onboarding' => (bool) config_cache('instance.curated_registration.enabled'),
|
||||
'activitypub_enabled' => config_cache('federation.activitypub.enabled'),
|
||||
'oauth_enabled' => config_cache('pixelfed.oauth_enabled'),
|
||||
'oauth_enabled' => (bool) config_cache('pixelfed.oauth_enabled'),
|
||||
'media_types' => Str::of(config_cache('pixelfed.media_types'))->explode(','),
|
||||
'image_quality' => config_cache('pixelfed.image_quality'),
|
||||
'optimize_image' => config_cache('pixelfed.optimize_image'),
|
||||
|
@ -265,13 +260,14 @@ trait AdminDirectoryController
|
|||
];
|
||||
|
||||
$validator = Validator::make($reqs['feature_config'], [
|
||||
'open_registration' => 'required|accepted',
|
||||
'open_registration' => 'required_unless:curated_onboarding,true',
|
||||
'curated_onboarding' => 'required_unless:open_registration,true',
|
||||
'activitypub_enabled' => 'required|accepted',
|
||||
'oauth_enabled' => 'required|accepted',
|
||||
'media_types' => [
|
||||
'required',
|
||||
function ($attribute, $value, $fail) {
|
||||
if (!in_array('image/jpeg', $value->toArray()) || !in_array('image/png', $value->toArray())) {
|
||||
function ($attribute, $value, $fail) {
|
||||
if (! in_array('image/jpeg', $value->toArray()) || ! in_array('image/png', $value->toArray())) {
|
||||
$fail('You must enable image/jpeg and image/png support.');
|
||||
}
|
||||
},
|
||||
|
@ -282,10 +278,10 @@ trait AdminDirectoryController
|
|||
'max_account_size' => 'required_if:enforce_account_limit,true|integer|min:1000000',
|
||||
'max_album_length' => 'required|integer|min:4|max:20',
|
||||
'account_deletion' => 'required|accepted',
|
||||
'max_caption_length' => 'required|integer|min:500|max:10000'
|
||||
'max_caption_length' => 'required|integer|min:500|max:10000',
|
||||
]);
|
||||
|
||||
if(!$validator->validate()) {
|
||||
if (! $validator->validate()) {
|
||||
return response()->json($validator->errors(), 422);
|
||||
}
|
||||
|
||||
|
@ -294,6 +290,7 @@ trait AdminDirectoryController
|
|||
|
||||
$data = (new PixelfedDirectoryController())->buildListing();
|
||||
$res = Http::withoutVerifying()->post('https://pixelfed.org/api/v1/directory/submission', $data);
|
||||
|
||||
return 200;
|
||||
}
|
||||
|
||||
|
@ -301,7 +298,7 @@ trait AdminDirectoryController
|
|||
{
|
||||
$bannerImage = ConfigCache::whereK('app.banner_image')->first();
|
||||
$directory = ConfigCache::whereK('pixelfed.directory')->first();
|
||||
if(!$bannerImage && !$directory || empty($directory->v)) {
|
||||
if (! $bannerImage && ! $directory || empty($directory->v)) {
|
||||
return;
|
||||
}
|
||||
$directoryArr = json_decode($directory->v, true);
|
||||
|
@ -309,12 +306,12 @@ trait AdminDirectoryController
|
|||
$protected = [
|
||||
'public/headers/.gitignore',
|
||||
'public/headers/default.jpg',
|
||||
'public/headers/missing.png'
|
||||
'public/headers/missing.png',
|
||||
];
|
||||
if(!$path || in_array($path, $protected)) {
|
||||
if (! $path || in_array($path, $protected)) {
|
||||
return;
|
||||
}
|
||||
if(Storage::exists($directoryArr['banner_image'])) {
|
||||
if (Storage::exists($directoryArr['banner_image'])) {
|
||||
Storage::delete($directoryArr['banner_image']);
|
||||
}
|
||||
|
||||
|
@ -325,12 +322,13 @@ trait AdminDirectoryController
|
|||
$bannerImage->save();
|
||||
Cache::forget('api:v1:instance-data-response-v1');
|
||||
ConfigCacheService::put('pixelfed.directory', $directory);
|
||||
|
||||
return $bannerImage->v;
|
||||
}
|
||||
|
||||
public function directoryGetPopularPosts(Request $request)
|
||||
{
|
||||
$ids = Cache::remember('admin:api:popular_posts', 86400, function() {
|
||||
$ids = Cache::remember('admin:api:popular_posts', 86400, function () {
|
||||
return Status::whereLocal(true)
|
||||
->whereScope('public')
|
||||
->whereType('photo')
|
||||
|
@ -340,21 +338,21 @@ trait AdminDirectoryController
|
|||
->pluck('id');
|
||||
});
|
||||
|
||||
$res = $ids->map(function($id) {
|
||||
$res = $ids->map(function ($id) {
|
||||
return StatusService::get($id);
|
||||
})
|
||||
->filter(function($post) {
|
||||
return $post && isset($post['account']);
|
||||
})
|
||||
->values();
|
||||
->filter(function ($post) {
|
||||
return $post && isset($post['account']);
|
||||
})
|
||||
->values();
|
||||
|
||||
return response()->json($res, 200, [], JSON_PRETTY_PRINT|JSON_UNESCAPED_SLASHES);
|
||||
return response()->json($res, 200, [], JSON_PRETTY_PRINT | JSON_UNESCAPED_SLASHES);
|
||||
}
|
||||
|
||||
public function directoryGetAddPostByIdSearch(Request $request)
|
||||
{
|
||||
$this->validate($request, [
|
||||
'q' => 'required|integer'
|
||||
'q' => 'required|integer',
|
||||
]);
|
||||
|
||||
$id = $request->input('q');
|
||||
|
@ -377,11 +375,12 @@ trait AdminDirectoryController
|
|||
$profile_id = $request->input('profile_id');
|
||||
$testimonials = ConfigCache::whereK('pixelfed.directory.testimonials')->firstOrFail();
|
||||
$existing = collect(json_decode($testimonials->v, true))
|
||||
->filter(function($t) use($profile_id) {
|
||||
->filter(function ($t) use ($profile_id) {
|
||||
return $t['profile_id'] !== $profile_id;
|
||||
})
|
||||
->values();
|
||||
ConfigCacheService::put('pixelfed.directory.testimonials', $existing);
|
||||
|
||||
return $existing;
|
||||
}
|
||||
|
||||
|
@ -389,13 +388,13 @@ trait AdminDirectoryController
|
|||
{
|
||||
$this->validate($request, [
|
||||
'username' => 'required',
|
||||
'body' => 'required|string|min:5|max:500'
|
||||
'body' => 'required|string|min:5|max:500',
|
||||
]);
|
||||
|
||||
$user = User::whereUsername($request->input('username'))->whereNull('status')->firstOrFail();
|
||||
|
||||
$configCache = ConfigCache::firstOrCreate([
|
||||
'k' => 'pixelfed.directory.testimonials'
|
||||
'k' => 'pixelfed.directory.testimonials',
|
||||
]);
|
||||
|
||||
$testimonials = $configCache->v ? collect(json_decode($configCache->v, true)) : collect([]);
|
||||
|
@ -406,7 +405,7 @@ trait AdminDirectoryController
|
|||
$testimonials->push([
|
||||
'profile_id' => (string) $user->profile_id,
|
||||
'username' => $request->input('username'),
|
||||
'body' => $request->input('body')
|
||||
'body' => $request->input('body'),
|
||||
]);
|
||||
|
||||
$configCache->v = json_encode($testimonials->toArray());
|
||||
|
@ -414,8 +413,9 @@ trait AdminDirectoryController
|
|||
ConfigCacheService::put('pixelfed.directory.testimonials', $configCache->v);
|
||||
$res = [
|
||||
'profile' => AccountService::get($user->profile_id),
|
||||
'body' => $request->input('body')
|
||||
'body' => $request->input('body'),
|
||||
];
|
||||
|
||||
return $res;
|
||||
}
|
||||
|
||||
|
@ -423,7 +423,7 @@ trait AdminDirectoryController
|
|||
{
|
||||
$this->validate($request, [
|
||||
'profile_id' => 'required',
|
||||
'body' => 'required|string|min:5|max:500'
|
||||
'body' => 'required|string|min:5|max:500',
|
||||
]);
|
||||
|
||||
$profile_id = $request->input('profile_id');
|
||||
|
@ -431,18 +431,19 @@ trait AdminDirectoryController
|
|||
$user = User::whereProfileId($profile_id)->firstOrFail();
|
||||
|
||||
$configCache = ConfigCache::firstOrCreate([
|
||||
'k' => 'pixelfed.directory.testimonials'
|
||||
'k' => 'pixelfed.directory.testimonials',
|
||||
]);
|
||||
|
||||
$testimonials = $configCache->v ? collect(json_decode($configCache->v, true)) : collect([]);
|
||||
|
||||
$updated = $testimonials->map(function($t) use($profile_id, $body) {
|
||||
if($t['profile_id'] == $profile_id) {
|
||||
$updated = $testimonials->map(function ($t) use ($profile_id, $body) {
|
||||
if ($t['profile_id'] == $profile_id) {
|
||||
$t['body'] = $body;
|
||||
}
|
||||
|
||||
return $t;
|
||||
})
|
||||
->values();
|
||||
->values();
|
||||
|
||||
$configCache->v = json_encode($updated);
|
||||
$configCache->save();
|
||||
|
|
Plik diff jest za duży
Load Diff
Plik diff jest za duży
Load Diff
|
@ -11,6 +11,7 @@ use App\Mail\AdminMessage;
|
|||
use Illuminate\Support\Facades\Mail;
|
||||
use App\Services\ModLogService;
|
||||
use App\Jobs\DeletePipeline\DeleteAccountPipeline;
|
||||
use App\Services\AccountService;
|
||||
|
||||
trait AdminUserController
|
||||
{
|
||||
|
@ -25,7 +26,7 @@ trait AdminUserController
|
|||
'next' => $offset + 1,
|
||||
'query' => $search ? '&a=search&q=' . $search : null
|
||||
];
|
||||
$users = User::select('id', 'username', 'status', 'profile_id')
|
||||
$users = User::select('id', 'username', 'status', 'profile_id', 'is_admin')
|
||||
->orderBy($col, $dir)
|
||||
->when($search, function($q, $search) {
|
||||
return $q->where('username', 'like', "%{$search}%");
|
||||
|
@ -34,7 +35,11 @@ trait AdminUserController
|
|||
return $q->offset(($offset * 10));
|
||||
})
|
||||
->limit(10)
|
||||
->get();
|
||||
->get()
|
||||
->map(function($u) {
|
||||
$u['account'] = AccountService::get($u->profile_id, true);
|
||||
return $u;
|
||||
});
|
||||
|
||||
return view('admin.users.home', compact('users', 'pagination'));
|
||||
}
|
||||
|
|
|
@ -21,6 +21,7 @@ use Carbon\Carbon;
|
|||
use Illuminate\Http\Request;
|
||||
use Illuminate\Support\Facades\Redis;
|
||||
use App\Http\Controllers\Admin\{
|
||||
AdminAutospamController,
|
||||
AdminDirectoryController,
|
||||
AdminDiscoverController,
|
||||
AdminHashtagsController,
|
||||
|
@ -43,6 +44,7 @@ use App\Models\CustomEmoji;
|
|||
class AdminController extends Controller
|
||||
{
|
||||
use AdminReportController,
|
||||
AdminAutospamController,
|
||||
AdminDirectoryController,
|
||||
AdminDiscoverController,
|
||||
AdminHashtagsController,
|
||||
|
@ -422,7 +424,7 @@ class AdminController extends Controller
|
|||
|
||||
public function customEmojiHome(Request $request)
|
||||
{
|
||||
if(!config('federation.custom_emoji.enabled')) {
|
||||
if(!(bool) config_cache('federation.custom_emoji.enabled')) {
|
||||
return view('admin.custom-emoji.not-enabled');
|
||||
}
|
||||
$this->validate($request, [
|
||||
|
@ -495,7 +497,7 @@ class AdminController extends Controller
|
|||
|
||||
public function customEmojiToggleActive(Request $request, $id)
|
||||
{
|
||||
abort_unless(config('federation.custom_emoji.enabled'), 404);
|
||||
abort_unless((bool) config_cache('federation.custom_emoji.enabled'), 404);
|
||||
$emoji = CustomEmoji::findOrFail($id);
|
||||
$emoji->disabled = !$emoji->disabled;
|
||||
$emoji->save();
|
||||
|
@ -506,13 +508,13 @@ class AdminController extends Controller
|
|||
|
||||
public function customEmojiAdd(Request $request)
|
||||
{
|
||||
abort_unless(config('federation.custom_emoji.enabled'), 404);
|
||||
abort_unless((bool) config_cache('federation.custom_emoji.enabled'), 404);
|
||||
return view('admin.custom-emoji.add');
|
||||
}
|
||||
|
||||
public function customEmojiStore(Request $request)
|
||||
{
|
||||
abort_unless(config('federation.custom_emoji.enabled'), 404);
|
||||
abort_unless((bool) config_cache('federation.custom_emoji.enabled'), 404);
|
||||
$this->validate($request, [
|
||||
'shortcode' => [
|
||||
'required',
|
||||
|
@ -543,7 +545,7 @@ class AdminController extends Controller
|
|||
|
||||
public function customEmojiDelete(Request $request, $id)
|
||||
{
|
||||
abort_unless(config('federation.custom_emoji.enabled'), 404);
|
||||
abort_unless((bool) config_cache('federation.custom_emoji.enabled'), 404);
|
||||
$emoji = CustomEmoji::findOrFail($id);
|
||||
Storage::delete("public/{$emoji->media_path}");
|
||||
Cache::forget('pf:custom_emoji');
|
||||
|
@ -553,7 +555,7 @@ class AdminController extends Controller
|
|||
|
||||
public function customEmojiShowDuplicates(Request $request, $id)
|
||||
{
|
||||
abort_unless(config('federation.custom_emoji.enabled'), 404);
|
||||
abort_unless((bool) config_cache('federation.custom_emoji.enabled'), 404);
|
||||
$emoji = CustomEmoji::orderBy('id')->whereDisabled(false)->whereShortcode($id)->firstOrFail();
|
||||
$emojis = CustomEmoji::whereShortcode($id)->where('id', '!=', $emoji->id)->cursorPaginate(10);
|
||||
return view('admin.custom-emoji.duplicates', compact('emoji', 'emojis'));
|
||||
|
|
|
@ -0,0 +1,335 @@
|
|||
<?php
|
||||
|
||||
namespace App\Http\Controllers;
|
||||
|
||||
use App\Mail\CuratedRegisterAcceptUser;
|
||||
use App\Mail\CuratedRegisterRejectUser;
|
||||
use App\Mail\CuratedRegisterRequestDetailsFromUser;
|
||||
use App\Models\CuratedRegister;
|
||||
use App\Models\CuratedRegisterActivity;
|
||||
use App\Models\CuratedRegisterTemplate;
|
||||
use App\User;
|
||||
use Illuminate\Http\Request;
|
||||
use Illuminate\Support\Facades\Mail;
|
||||
use Illuminate\Support\Str;
|
||||
|
||||
class AdminCuratedRegisterController extends Controller
|
||||
{
|
||||
public function __construct()
|
||||
{
|
||||
$this->middleware(['auth', 'admin']);
|
||||
}
|
||||
|
||||
public function index(Request $request)
|
||||
{
|
||||
$this->validate($request, [
|
||||
'filter' => 'sometimes|in:open,all,awaiting,approved,rejected,responses',
|
||||
'sort' => 'sometimes|in:asc,desc',
|
||||
]);
|
||||
$filter = $request->input('filter', 'open');
|
||||
$sort = $request->input('sort', 'asc');
|
||||
$records = CuratedRegister::when($filter, function ($q, $filter) {
|
||||
if ($filter === 'open') {
|
||||
return $q->where('is_rejected', false)
|
||||
->where(function ($query) {
|
||||
return $query->where('user_has_responded', true)->orWhere('is_awaiting_more_info', false);
|
||||
})
|
||||
->whereNotNull('email_verified_at')
|
||||
->whereIsClosed(false);
|
||||
} elseif ($filter === 'all') {
|
||||
return $q;
|
||||
} elseif ($filter === 'responses') {
|
||||
return $q->whereIsClosed(false)
|
||||
->whereNotNull('email_verified_at')
|
||||
->where('user_has_responded', true)
|
||||
->where('is_awaiting_more_info', true);
|
||||
} elseif ($filter === 'awaiting') {
|
||||
return $q->whereIsClosed(false)
|
||||
->where('is_rejected', false)
|
||||
->where('is_approved', false)
|
||||
->where('user_has_responded', false)
|
||||
->where('is_awaiting_more_info', true);
|
||||
} elseif ($filter === 'approved') {
|
||||
return $q->whereIsClosed(true)->whereIsApproved(true);
|
||||
} elseif ($filter === 'rejected') {
|
||||
return $q->whereIsClosed(true)->whereIsRejected(true);
|
||||
}
|
||||
})
|
||||
->when($sort, function ($query, $sort) {
|
||||
return $query->orderBy('id', $sort);
|
||||
})
|
||||
->paginate(10)
|
||||
->withQueryString();
|
||||
|
||||
return view('admin.curated-register.index', compact('records', 'filter'));
|
||||
}
|
||||
|
||||
public function show(Request $request, $id)
|
||||
{
|
||||
$record = CuratedRegister::findOrFail($id);
|
||||
|
||||
return view('admin.curated-register.show', compact('record'));
|
||||
}
|
||||
|
||||
public function apiActivityLog(Request $request, $id)
|
||||
{
|
||||
$record = CuratedRegister::findOrFail($id);
|
||||
|
||||
$res = collect([
|
||||
[
|
||||
'id' => 1,
|
||||
'action' => 'created',
|
||||
'title' => 'Onboarding application created',
|
||||
'message' => null,
|
||||
'link' => null,
|
||||
'timestamp' => $record->created_at,
|
||||
],
|
||||
]);
|
||||
|
||||
if ($record->email_verified_at) {
|
||||
$res->push([
|
||||
'id' => 3,
|
||||
'action' => 'email_verified_at',
|
||||
'title' => 'Applicant successfully verified email address',
|
||||
'message' => null,
|
||||
'link' => null,
|
||||
'timestamp' => $record->email_verified_at,
|
||||
]);
|
||||
}
|
||||
|
||||
$activities = CuratedRegisterActivity::whereRegisterId($record->id)->get();
|
||||
|
||||
$idx = 4;
|
||||
$userResponses = collect([]);
|
||||
|
||||
foreach ($activities as $activity) {
|
||||
$idx++;
|
||||
|
||||
if ($activity->type === 'user_resend_email_confirmation') {
|
||||
continue;
|
||||
}
|
||||
if ($activity->from_user) {
|
||||
$userResponses->push($activity);
|
||||
|
||||
continue;
|
||||
}
|
||||
$res->push([
|
||||
'id' => $idx,
|
||||
'aid' => $activity->id,
|
||||
'action' => $activity->type,
|
||||
'title' => $activity->from_admin ? 'Admin requested info' : 'User responded',
|
||||
'message' => $activity->message,
|
||||
'link' => $activity->adminReviewUrl(),
|
||||
'timestamp' => $activity->created_at,
|
||||
]);
|
||||
}
|
||||
|
||||
foreach ($userResponses as $ur) {
|
||||
$res = $res->map(function ($r) use ($ur) {
|
||||
if (! isset($r['aid'])) {
|
||||
return $r;
|
||||
}
|
||||
if ($ur->reply_to_id === $r['aid']) {
|
||||
$r['user_response'] = $ur;
|
||||
|
||||
return $r;
|
||||
}
|
||||
|
||||
return $r;
|
||||
});
|
||||
}
|
||||
|
||||
if ($record->is_approved) {
|
||||
$idx++;
|
||||
$res->push([
|
||||
'id' => $idx,
|
||||
'action' => 'approved',
|
||||
'title' => 'Application Approved',
|
||||
'message' => null,
|
||||
'link' => null,
|
||||
'timestamp' => $record->action_taken_at,
|
||||
]);
|
||||
} elseif ($record->is_rejected) {
|
||||
$idx++;
|
||||
$res->push([
|
||||
'id' => $idx,
|
||||
'action' => 'rejected',
|
||||
'title' => 'Application Rejected',
|
||||
'message' => null,
|
||||
'link' => null,
|
||||
'timestamp' => $record->action_taken_at,
|
||||
]);
|
||||
}
|
||||
|
||||
return $res->reverse()->values();
|
||||
}
|
||||
|
||||
public function apiMessagePreviewStore(Request $request, $id)
|
||||
{
|
||||
$record = CuratedRegister::findOrFail($id);
|
||||
|
||||
return $request->all();
|
||||
}
|
||||
|
||||
public function apiMessageSendStore(Request $request, $id)
|
||||
{
|
||||
$this->validate($request, [
|
||||
'message' => 'required|string|min:5|max:3000',
|
||||
]);
|
||||
$record = CuratedRegister::findOrFail($id);
|
||||
abort_if($record->email_verified_at === null, 400, 'Cannot message an unverified email');
|
||||
$activity = new CuratedRegisterActivity;
|
||||
$activity->register_id = $record->id;
|
||||
$activity->admin_id = $request->user()->id;
|
||||
$activity->secret_code = Str::random(32);
|
||||
$activity->type = 'request_details';
|
||||
$activity->from_admin = true;
|
||||
$activity->message = $request->input('message');
|
||||
$activity->save();
|
||||
$record->is_awaiting_more_info = true;
|
||||
$record->user_has_responded = false;
|
||||
$record->save();
|
||||
Mail::to($record->email)->send(new CuratedRegisterRequestDetailsFromUser($record, $activity));
|
||||
|
||||
return $request->all();
|
||||
}
|
||||
|
||||
public function previewDetailsMessageShow(Request $request, $id)
|
||||
{
|
||||
$record = CuratedRegister::findOrFail($id);
|
||||
abort_if($record->email_verified_at === null, 400, 'Cannot message an unverified email');
|
||||
$activity = new CuratedRegisterActivity;
|
||||
$activity->message = $request->input('message');
|
||||
|
||||
return new \App\Mail\CuratedRegisterRequestDetailsFromUser($record, $activity);
|
||||
}
|
||||
|
||||
public function previewMessageShow(Request $request, $id)
|
||||
{
|
||||
$record = CuratedRegister::findOrFail($id);
|
||||
abort_if($record->email_verified_at === null, 400, 'Cannot message an unverified email');
|
||||
$record->message = $request->input('message');
|
||||
|
||||
return new \App\Mail\CuratedRegisterSendMessage($record);
|
||||
}
|
||||
|
||||
public function apiHandleReject(Request $request, $id)
|
||||
{
|
||||
$this->validate($request, [
|
||||
'action' => 'required|in:reject-email,reject-silent',
|
||||
]);
|
||||
$action = $request->input('action');
|
||||
$record = CuratedRegister::findOrFail($id);
|
||||
abort_if($record->email_verified_at === null, 400, 'Cannot reject an unverified email');
|
||||
$record->is_rejected = true;
|
||||
$record->is_closed = true;
|
||||
$record->action_taken_at = now();
|
||||
$record->save();
|
||||
if ($action === 'reject-email') {
|
||||
Mail::to($record->email)->send(new CuratedRegisterRejectUser($record));
|
||||
}
|
||||
|
||||
return [200];
|
||||
}
|
||||
|
||||
public function apiHandleApprove(Request $request, $id)
|
||||
{
|
||||
$record = CuratedRegister::findOrFail($id);
|
||||
abort_if($record->email_verified_at === null, 400, 'Cannot reject an unverified email');
|
||||
$record->is_approved = true;
|
||||
$record->is_closed = true;
|
||||
$record->action_taken_at = now();
|
||||
$record->save();
|
||||
$user = User::create([
|
||||
'name' => $record->username,
|
||||
'username' => $record->username,
|
||||
'email' => $record->email,
|
||||
'password' => $record->password,
|
||||
'app_register_ip' => $record->ip_address,
|
||||
'email_verified_at' => now(),
|
||||
'register_source' => 'cur_onboarding',
|
||||
]);
|
||||
|
||||
Mail::to($record->email)->send(new CuratedRegisterAcceptUser($record));
|
||||
|
||||
return [200];
|
||||
}
|
||||
|
||||
public function templates(Request $request)
|
||||
{
|
||||
$templates = CuratedRegisterTemplate::paginate(10);
|
||||
|
||||
return view('admin.curated-register.templates', compact('templates'));
|
||||
}
|
||||
|
||||
public function templateCreate(Request $request)
|
||||
{
|
||||
return view('admin.curated-register.template-create');
|
||||
}
|
||||
|
||||
public function templateEdit(Request $request, $id)
|
||||
{
|
||||
$template = CuratedRegisterTemplate::findOrFail($id);
|
||||
|
||||
return view('admin.curated-register.template-edit', compact('template'));
|
||||
}
|
||||
|
||||
public function templateEditStore(Request $request, $id)
|
||||
{
|
||||
$this->validate($request, [
|
||||
'name' => 'required|string|max:30',
|
||||
'content' => 'required|string|min:5|max:3000',
|
||||
'description' => 'nullable|sometimes|string|max:1000',
|
||||
'active' => 'sometimes',
|
||||
]);
|
||||
$template = CuratedRegisterTemplate::findOrFail($id);
|
||||
$template->name = $request->input('name');
|
||||
$template->content = $request->input('content');
|
||||
$template->description = $request->input('description');
|
||||
$template->is_active = $request->boolean('active');
|
||||
$template->save();
|
||||
|
||||
return redirect()->back()->with('status', 'Successfully updated template!');
|
||||
}
|
||||
|
||||
public function templateDelete(Request $request, $id)
|
||||
{
|
||||
$template = CuratedRegisterTemplate::findOrFail($id);
|
||||
$template->delete();
|
||||
|
||||
return redirect(route('admin.curated-onboarding.templates'))->with('status', 'Successfully deleted template!');
|
||||
}
|
||||
|
||||
public function templateStore(Request $request)
|
||||
{
|
||||
$this->validate($request, [
|
||||
'name' => 'required|string|max:30',
|
||||
'content' => 'required|string|min:5|max:3000',
|
||||
'description' => 'nullable|sometimes|string|max:1000',
|
||||
'active' => 'sometimes',
|
||||
]);
|
||||
CuratedRegisterTemplate::create([
|
||||
'name' => $request->input('name'),
|
||||
'content' => $request->input('content'),
|
||||
'description' => $request->input('description'),
|
||||
'is_active' => $request->boolean('active'),
|
||||
]);
|
||||
|
||||
return redirect(route('admin.curated-onboarding.templates'))->with('status', 'Successfully created new template!');
|
||||
}
|
||||
|
||||
public function getActiveTemplates(Request $request)
|
||||
{
|
||||
$templates = CuratedRegisterTemplate::whereIsActive(true)
|
||||
->orderBy('order')
|
||||
->get()
|
||||
->map(function ($tmp) {
|
||||
return [
|
||||
'name' => $tmp->name,
|
||||
'content' => $tmp->content,
|
||||
];
|
||||
});
|
||||
|
||||
return response()->json($templates);
|
||||
}
|
||||
}
|
|
@ -0,0 +1,123 @@
|
|||
<?php
|
||||
|
||||
namespace App\Http\Controllers;
|
||||
|
||||
use Illuminate\Http\Request;
|
||||
use App\Models\AdminShadowFilter;
|
||||
use App\Profile;
|
||||
use App\Services\AccountService;
|
||||
use App\Services\AdminShadowFilterService;
|
||||
|
||||
class AdminShadowFilterController extends Controller
|
||||
{
|
||||
public function __construct()
|
||||
{
|
||||
$this->middleware(['auth','admin']);
|
||||
}
|
||||
|
||||
public function home(Request $request)
|
||||
{
|
||||
$filter = $request->input('filter');
|
||||
$searchQuery = $request->input('q');
|
||||
$filters = AdminShadowFilter::whereHas('profile')
|
||||
->when($filter, function($q, $filter) {
|
||||
if($filter == 'all') {
|
||||
return $q;
|
||||
} else if($filter == 'inactive') {
|
||||
return $q->whereActive(false);
|
||||
} else {
|
||||
return $q;
|
||||
}
|
||||
}, function($q, $filter) {
|
||||
return $q->whereActive(true);
|
||||
})
|
||||
->when($searchQuery, function($q, $searchQuery) {
|
||||
$ids = Profile::where('username', 'like', '%' . $searchQuery . '%')
|
||||
->limit(100)
|
||||
->pluck('id')
|
||||
->toArray();
|
||||
return $q->where('item_type', 'App\Profile')->whereIn('item_id', $ids);
|
||||
})
|
||||
->latest()
|
||||
->paginate(10)
|
||||
->withQueryString();
|
||||
|
||||
return view('admin.asf.home', compact('filters'));
|
||||
}
|
||||
|
||||
public function create(Request $request)
|
||||
{
|
||||
return view('admin.asf.create');
|
||||
}
|
||||
|
||||
public function edit(Request $request, $id)
|
||||
{
|
||||
$filter = AdminShadowFilter::findOrFail($id);
|
||||
$profile = AccountService::get($filter->item_id);
|
||||
return view('admin.asf.edit', compact('filter', 'profile'));
|
||||
}
|
||||
|
||||
public function store(Request $request)
|
||||
{
|
||||
$this->validate($request, [
|
||||
'username' => 'required',
|
||||
'active' => 'sometimes',
|
||||
'note' => 'sometimes',
|
||||
'hide_from_public_feeds' => 'sometimes'
|
||||
]);
|
||||
|
||||
$profile = Profile::whereUsername($request->input('username'))->first();
|
||||
|
||||
if(!$profile) {
|
||||
return back()->withErrors(['Invalid account']);
|
||||
}
|
||||
|
||||
if($profile->user && $profile->user->is_admin) {
|
||||
return back()->withErrors(['Cannot filter an admin account']);
|
||||
}
|
||||
|
||||
$active = $request->has('active') && $request->has('hide_from_public_feeds');
|
||||
|
||||
AdminShadowFilter::updateOrCreate([
|
||||
'item_id' => $profile->id,
|
||||
'item_type' => get_class($profile)
|
||||
], [
|
||||
'is_local' => $profile->domain === null,
|
||||
'note' => $request->input('note'),
|
||||
'hide_from_public_feeds' => $request->has('hide_from_public_feeds'),
|
||||
'admin_id' => $request->user()->profile_id,
|
||||
'active' => $active
|
||||
]);
|
||||
|
||||
AdminShadowFilterService::refresh();
|
||||
|
||||
return redirect('/i/admin/asf/home');
|
||||
}
|
||||
|
||||
public function storeEdit(Request $request, $id)
|
||||
{
|
||||
$this->validate($request, [
|
||||
'active' => 'sometimes',
|
||||
'note' => 'sometimes',
|
||||
'hide_from_public_feeds' => 'sometimes'
|
||||
]);
|
||||
|
||||
$filter = AdminShadowFilter::findOrFail($id);
|
||||
|
||||
$profile = Profile::findOrFail($filter->item_id);
|
||||
|
||||
if($profile->user && $profile->user->is_admin) {
|
||||
return back()->withErrors(['Cannot filter an admin account']);
|
||||
}
|
||||
|
||||
$active = $request->has('active');
|
||||
$filter->active = $active;
|
||||
$filter->hide_from_public_feeds = $request->has('hide_from_public_feeds');
|
||||
$filter->note = $request->input('note');
|
||||
$filter->save();
|
||||
|
||||
AdminShadowFilterService::refresh();
|
||||
|
||||
return redirect('/i/admin/asf/home');
|
||||
}
|
||||
}
|
|
@ -11,37 +11,49 @@ use App\{
|
|||
AccountInterstitial,
|
||||
Instance,
|
||||
Like,
|
||||
Notification,
|
||||
Media,
|
||||
Profile,
|
||||
Report,
|
||||
Status,
|
||||
User
|
||||
};
|
||||
use App\Models\Conversation;
|
||||
use App\Models\RemoteReport;
|
||||
use App\Services\AccountService;
|
||||
use App\Services\AdminStatsService;
|
||||
use App\Services\ConfigCacheService;
|
||||
use App\Services\InstanceService;
|
||||
use App\Services\ModLogService;
|
||||
use App\Services\SnowflakeService;
|
||||
use App\Services\StatusService;
|
||||
use App\Services\PublicTimelineService;
|
||||
use App\Services\NetworkTimelineService;
|
||||
use App\Services\NotificationService;
|
||||
use App\Http\Resources\AdminInstance;
|
||||
use App\Http\Resources\AdminUser;
|
||||
use App\Jobs\DeletePipeline\DeleteAccountPipeline;
|
||||
use App\Jobs\DeletePipeline\DeleteRemoteProfilePipeline;
|
||||
use App\Jobs\DeletePipeline\DeleteRemoteStatusPipeline;
|
||||
|
||||
class AdminApiController extends Controller
|
||||
{
|
||||
public function supported(Request $request)
|
||||
{
|
||||
abort_if(!$request->user(), 404);
|
||||
abort_if(!$request->user() || !$request->user()->token(), 404);
|
||||
|
||||
abort_unless($request->user()->is_admin == 1, 404);
|
||||
abort_unless($request->user()->tokenCan('admin:read'), 404);
|
||||
|
||||
return response()->json(['supported' => true]);
|
||||
}
|
||||
|
||||
public function getStats(Request $request)
|
||||
{
|
||||
abort_if(!$request->user(), 404);
|
||||
abort_if(!$request->user() || !$request->user()->token(), 404);
|
||||
|
||||
abort_unless($request->user()->is_admin == 1, 404);
|
||||
abort_unless($request->user()->tokenCan('admin:read'), 404);
|
||||
|
||||
$res = AdminStatsService::summary();
|
||||
$res['autospam_count'] = AccountInterstitial::whereType('post.autospam')
|
||||
|
@ -52,8 +64,10 @@ class AdminApiController extends Controller
|
|||
|
||||
public function autospam(Request $request)
|
||||
{
|
||||
abort_if(!$request->user(), 404);
|
||||
abort_if(!$request->user() || !$request->user()->token(), 404);
|
||||
|
||||
abort_unless($request->user()->is_admin == 1, 404);
|
||||
abort_unless($request->user()->tokenCan('admin:read'), 404);
|
||||
|
||||
$appeals = AccountInterstitial::whereType('post.autospam')
|
||||
->whereNull('appeal_handled_at')
|
||||
|
@ -87,11 +101,13 @@ class AdminApiController extends Controller
|
|||
|
||||
public function autospamHandle(Request $request)
|
||||
{
|
||||
abort_if(!$request->user(), 404);
|
||||
abort_if(!$request->user() || !$request->user()->token(), 404);
|
||||
|
||||
abort_unless($request->user()->is_admin == 1, 404);
|
||||
abort_unless($request->user()->tokenCan('admin:write'), 404);
|
||||
|
||||
$this->validate($request, [
|
||||
'action' => 'required|in:dismiss,approve,dismiss-all,approve-all',
|
||||
'action' => 'required|in:dismiss,approve,dismiss-all,approve-all,delete-post,delete-account',
|
||||
'id' => 'required'
|
||||
]);
|
||||
|
||||
|
@ -103,14 +119,53 @@ class AdminApiController extends Controller
|
|||
$now = now();
|
||||
$res = ['status' => 'success'];
|
||||
$meta = json_decode($appeal->meta);
|
||||
$user = $appeal->user;
|
||||
$profile = $user->profile;
|
||||
|
||||
if($action == 'dismiss') {
|
||||
$appeal->is_spam = true;
|
||||
$appeal->appeal_handled_at = $now;
|
||||
$appeal->save();
|
||||
|
||||
Cache::forget('pf:bouncer_v0:exemption_by_pid:' . $appeal->user->profile_id);
|
||||
Cache::forget('pf:bouncer_v0:recent_by_pid:' . $appeal->user->profile_id);
|
||||
Cache::forget('pf:bouncer_v0:exemption_by_pid:' . $profile->id);
|
||||
Cache::forget('pf:bouncer_v0:recent_by_pid:' . $profile->id);
|
||||
Cache::forget('admin-dash:reports:spam-count');
|
||||
return $res;
|
||||
}
|
||||
|
||||
if($action == 'delete-post') {
|
||||
$appeal->appeal_handled_at = now();
|
||||
$appeal->is_spam = true;
|
||||
$appeal->save();
|
||||
ModLogService::boot()
|
||||
->objectUid($profile->id)
|
||||
->objectId($appeal->status->id)
|
||||
->objectType('App\Status::class')
|
||||
->user($request->user())
|
||||
->action('admin.status.delete')
|
||||
->accessLevel('admin')
|
||||
->save();
|
||||
PublicTimelineService::deleteByProfileId($profile->id);
|
||||
StatusDelete::dispatch($appeal->status)->onQueue('high');
|
||||
Cache::forget('admin-dash:reports:spam-count');
|
||||
return $res;
|
||||
}
|
||||
|
||||
if($action == 'delete-account') {
|
||||
abort_if($user->is_admin, 400, 'Cannot delete an admin account.');
|
||||
$appeal->appeal_handled_at = now();
|
||||
$appeal->is_spam = true;
|
||||
$appeal->save();
|
||||
ModLogService::boot()
|
||||
->objectUid($profile->id)
|
||||
->objectId($profile->id)
|
||||
->objectType('App\User::class')
|
||||
->user($request->user())
|
||||
->action('admin.user.delete')
|
||||
->accessLevel('admin')
|
||||
->save();
|
||||
PublicTimelineService::deleteByProfileId($profile->id);
|
||||
DeleteAccountPipeline::dispatch($appeal->user)->onQueue('high');
|
||||
Cache::forget('admin-dash:reports:spam-count');
|
||||
return $res;
|
||||
}
|
||||
|
@ -140,6 +195,14 @@ class AdminApiController extends Controller
|
|||
|
||||
StatusService::del($status->id);
|
||||
|
||||
Notification::whereAction('autospam.warning')
|
||||
->whereProfileId($appeal->user->profile_id)
|
||||
->get()
|
||||
->each(function($n) use($appeal) {
|
||||
NotificationService::del($appeal->user->profile_id, $n->id);
|
||||
$n->forceDelete();
|
||||
});
|
||||
|
||||
Cache::forget('pf:bouncer_v0:exemption_by_pid:' . $appeal->user->profile_id);
|
||||
Cache::forget('pf:bouncer_v0:recent_by_pid:' . $appeal->user->profile_id);
|
||||
Cache::forget('admin-dash:reports:spam-count');
|
||||
|
@ -164,6 +227,14 @@ class AdminApiController extends Controller
|
|||
$status->save();
|
||||
StatusService::del($status->id, true);
|
||||
}
|
||||
|
||||
Notification::whereAction('autospam.warning')
|
||||
->whereProfileId($report->user->profile_id)
|
||||
->get()
|
||||
->each(function($n) use($report) {
|
||||
NotificationService::del($report->user->profile_id, $n->id);
|
||||
$n->forceDelete();
|
||||
});
|
||||
});
|
||||
Cache::forget('pf:bouncer_v0:exemption_by_pid:' . $appeal->user->profile_id);
|
||||
Cache::forget('pf:bouncer_v0:recent_by_pid:' . $appeal->user->profile_id);
|
||||
|
@ -176,8 +247,10 @@ class AdminApiController extends Controller
|
|||
|
||||
public function modReports(Request $request)
|
||||
{
|
||||
abort_if(!$request->user(), 404);
|
||||
abort_if(!$request->user() || !$request->user()->token(), 404);
|
||||
|
||||
abort_unless($request->user()->is_admin == 1, 404);
|
||||
abort_unless($request->user()->tokenCan('admin:read'), 404);
|
||||
|
||||
$reports = Report::whereNull('admin_seen')
|
||||
->orderBy('created_at','desc')
|
||||
|
@ -222,8 +295,10 @@ class AdminApiController extends Controller
|
|||
|
||||
public function modReportHandle(Request $request)
|
||||
{
|
||||
abort_if(!$request->user(), 404);
|
||||
abort_if(!$request->user() || !$request->user()->token(), 404);
|
||||
|
||||
abort_unless($request->user()->is_admin == 1, 404);
|
||||
abort_unless($request->user()->tokenCan('admin:write'), 404);
|
||||
|
||||
$this->validate($request, [
|
||||
'action' => 'required|string',
|
||||
|
@ -280,8 +355,11 @@ class AdminApiController extends Controller
|
|||
|
||||
public function getConfiguration(Request $request)
|
||||
{
|
||||
abort_if(!$request->user(), 404);
|
||||
abort_if(!$request->user() || !$request->user()->token(), 404);
|
||||
|
||||
abort_unless($request->user()->is_admin == 1, 404);
|
||||
abort_unless($request->user()->tokenCan('admin:read'), 404);
|
||||
|
||||
abort_unless(config('instance.enable_cc'), 400);
|
||||
|
||||
return collect([
|
||||
|
@ -323,8 +401,11 @@ class AdminApiController extends Controller
|
|||
|
||||
public function updateConfiguration(Request $request)
|
||||
{
|
||||
abort_if(!$request->user(), 404);
|
||||
abort_if(!$request->user() || !$request->user()->token(), 404);
|
||||
|
||||
abort_unless($request->user()->is_admin == 1, 404);
|
||||
abort_unless($request->user()->tokenCan('admin:write'), 404);
|
||||
|
||||
abort_unless(config('instance.enable_cc'), 400);
|
||||
|
||||
$this->validate($request, [
|
||||
|
@ -385,8 +466,14 @@ class AdminApiController extends Controller
|
|||
|
||||
public function getUsers(Request $request)
|
||||
{
|
||||
abort_if(!$request->user(), 404);
|
||||
abort_if(!$request->user() || !$request->user()->token(), 404);
|
||||
|
||||
abort_unless($request->user()->is_admin == 1, 404);
|
||||
abort_unless($request->user()->tokenCan('admin:read'), 404);
|
||||
|
||||
$this->validate($request, [
|
||||
'sort' => 'sometimes|in:asc,desc',
|
||||
]);
|
||||
$q = $request->input('q');
|
||||
$sort = $request->input('sort', 'desc') === 'asc' ? 'asc' : 'desc';
|
||||
$res = User::whereNull('status')
|
||||
|
@ -400,31 +487,47 @@ class AdminApiController extends Controller
|
|||
|
||||
public function getUser(Request $request)
|
||||
{
|
||||
abort_if(!$request->user(), 404);
|
||||
abort_if(!$request->user() || !$request->user()->token(), 404);
|
||||
|
||||
abort_unless($request->user()->is_admin == 1, 404);
|
||||
abort_unless($request->user()->tokenCan('admin:read'), 404);
|
||||
|
||||
$id = $request->input('user_id');
|
||||
$user = User::findOrFail($id);
|
||||
$profile = $user->profile;
|
||||
$account = AccountService::get($user->profile_id, true);
|
||||
return (new AdminUser($user))->additional(['meta' => [
|
||||
'account' => $account,
|
||||
'moderation' => [
|
||||
'unlisted' => (bool) $profile->unlisted,
|
||||
'cw' => (bool) $profile->cw,
|
||||
'no_autolink' => (bool) $profile->no_autolink
|
||||
]
|
||||
]]);
|
||||
$key = 'pf-admin-api:getUser:byId:' . $id;
|
||||
if($request->has('refresh')) {
|
||||
Cache::forget($key);
|
||||
}
|
||||
return Cache::remember($key, 86400, function() use($id) {
|
||||
$user = User::findOrFail($id);
|
||||
$profile = $user->profile;
|
||||
$account = AccountService::get($user->profile_id, true);
|
||||
$res = (new AdminUser($user))->additional(['meta' => [
|
||||
'cached_at' => str_replace('+00:00', 'Z', now()->format(DATE_RFC3339_EXTENDED)),
|
||||
'account' => $account,
|
||||
'dms_sent' => Conversation::whereFromId($profile->id)->count(),
|
||||
'report_count' => Report::where('object_id', $profile->id)->orWhere('reported_profile_id', $profile->id)->count(),
|
||||
'remote_report_count' => RemoteReport::whereAccountId($profile->id)->count(),
|
||||
'moderation' => [
|
||||
'unlisted' => (bool) $profile->unlisted,
|
||||
'cw' => (bool) $profile->cw,
|
||||
'no_autolink' => (bool) $profile->no_autolink
|
||||
]
|
||||
]]);
|
||||
|
||||
return $res;
|
||||
});
|
||||
}
|
||||
|
||||
public function userAdminAction(Request $request)
|
||||
{
|
||||
abort_if(!$request->user(), 404);
|
||||
abort_if(!$request->user() || !$request->user()->token(), 404);
|
||||
|
||||
abort_unless($request->user()->is_admin == 1, 404);
|
||||
abort_unless($request->user()->tokenCan('admin:write'), 404);
|
||||
|
||||
$this->validate($request, [
|
||||
'id' => 'required',
|
||||
'action' => 'required|in:unlisted,cw,no_autolink,refresh_stats,verify_email',
|
||||
'action' => 'required|in:unlisted,cw,no_autolink,refresh_stats,verify_email,delete',
|
||||
'value' => 'sometimes'
|
||||
]);
|
||||
|
||||
|
@ -435,7 +538,59 @@ class AdminApiController extends Controller
|
|||
|
||||
abort_if($user->is_admin == true && $action !== 'refresh_stats', 400, 'Cannot moderate admin accounts');
|
||||
|
||||
if($action === 'refresh_stats') {
|
||||
if($action === 'delete') {
|
||||
if(config('pixelfed.account_deletion') == false) {
|
||||
abort(404);
|
||||
}
|
||||
|
||||
abort_if($user->is_admin, 400, 'Cannot delete an admin account.');
|
||||
|
||||
$ts = now()->addMonth();
|
||||
|
||||
$user->status = 'delete';
|
||||
$user->delete_after = $ts;
|
||||
$user->save();
|
||||
|
||||
$profile->status = 'delete';
|
||||
$profile->delete_after = $ts;
|
||||
$profile->save();
|
||||
|
||||
ModLogService::boot()
|
||||
->objectUid($profile->id)
|
||||
->objectId($profile->id)
|
||||
->objectType('App\Profile::class')
|
||||
->user($request->user())
|
||||
->action('admin.user.delete')
|
||||
->accessLevel('admin')
|
||||
->save();
|
||||
|
||||
PublicTimelineService::deleteByProfileId($profile->id);
|
||||
NetworkTimelineService::deleteByProfileId($profile->id);
|
||||
|
||||
if($profile->user_id) {
|
||||
DB::table('oauth_access_tokens')->whereUserId($user->id)->delete();
|
||||
DB::table('oauth_auth_codes')->whereUserId($user->id)->delete();
|
||||
$user->email = $user->id;
|
||||
$user->password = '';
|
||||
$user->status = 'delete';
|
||||
$user->save();
|
||||
$profile->status = 'delete';
|
||||
$profile->delete_after = now()->addMonth();
|
||||
$profile->save();
|
||||
AccountService::del($profile->id);
|
||||
DeleteAccountPipeline::dispatch($user)->onQueue('high');
|
||||
} else {
|
||||
$profile->status = 'delete';
|
||||
$profile->delete_after = now()->addMonth();
|
||||
$profile->save();
|
||||
AccountService::del($profile->id);
|
||||
DeleteRemoteProfilePipeline::dispatch($profile)->onQueue('high');
|
||||
}
|
||||
return [
|
||||
'status' => 200,
|
||||
'msg' => 'deleted',
|
||||
];
|
||||
} else if($action === 'refresh_stats') {
|
||||
$profile->following_count = DB::table('followers')->whereProfileId($user->profile_id)->count();
|
||||
$profile->followers_count = DB::table('followers')->whereFollowingId($user->profile_id)->count();
|
||||
$statusCount = Status::whereProfileId($user->profile_id)
|
||||
|
@ -461,6 +616,51 @@ class AdminApiController extends Controller
|
|||
])
|
||||
->accessLevel('admin')
|
||||
->save();
|
||||
} else if($action === 'unlisted') {
|
||||
ModLogService::boot()
|
||||
->objectUid($profile->id)
|
||||
->objectId($profile->id)
|
||||
->objectType('App\Profile::class')
|
||||
->user($request->user())
|
||||
->action('admin.user.moderate')
|
||||
->metadata([
|
||||
'action' => $action,
|
||||
'message' => 'Success!'
|
||||
])
|
||||
->accessLevel('admin')
|
||||
->save();
|
||||
$profile->unlisted = !$profile->unlisted;
|
||||
$profile->save();
|
||||
} else if($action === 'cw') {
|
||||
ModLogService::boot()
|
||||
->objectUid($profile->id)
|
||||
->objectId($profile->id)
|
||||
->objectType('App\Profile::class')
|
||||
->user($request->user())
|
||||
->action('admin.user.moderate')
|
||||
->metadata([
|
||||
'action' => $action,
|
||||
'message' => 'Success!'
|
||||
])
|
||||
->accessLevel('admin')
|
||||
->save();
|
||||
$profile->cw = !$profile->cw;
|
||||
$profile->save();
|
||||
} else if($action === 'no_autolink') {
|
||||
ModLogService::boot()
|
||||
->objectUid($profile->id)
|
||||
->objectId($profile->id)
|
||||
->objectType('App\Profile::class')
|
||||
->user($request->user())
|
||||
->action('admin.user.moderate')
|
||||
->metadata([
|
||||
'action' => $action,
|
||||
'message' => 'Success!'
|
||||
])
|
||||
->accessLevel('admin')
|
||||
->save();
|
||||
$profile->no_autolink = !$profile->no_autolink;
|
||||
$profile->save();
|
||||
} else {
|
||||
$profile->{$action} = filter_var($request->input('value'), FILTER_VALIDATE_BOOLEAN);
|
||||
$profile->save();
|
||||
|
@ -494,8 +694,10 @@ class AdminApiController extends Controller
|
|||
|
||||
public function instances(Request $request)
|
||||
{
|
||||
abort_if(!$request->user(), 404);
|
||||
abort_if(!$request->user() || !$request->user()->token(), 404);
|
||||
|
||||
abort_unless($request->user()->is_admin == 1, 404);
|
||||
abort_unless($request->user()->tokenCan('admin:write'), 404);
|
||||
|
||||
$this->validate($request, [
|
||||
'q' => 'sometimes',
|
||||
|
@ -532,8 +734,10 @@ class AdminApiController extends Controller
|
|||
|
||||
public function getInstance(Request $request)
|
||||
{
|
||||
abort_if(!$request->user(), 404);
|
||||
abort_if(!$request->user() || !$request->user()->token(), 404);
|
||||
|
||||
abort_unless($request->user()->is_admin == 1, 404);
|
||||
abort_unless($request->user()->tokenCan('admin:read'), 404);
|
||||
|
||||
$id = $request->input('id');
|
||||
$res = Instance::findOrFail($id);
|
||||
|
@ -543,8 +747,10 @@ class AdminApiController extends Controller
|
|||
|
||||
public function moderateInstance(Request $request)
|
||||
{
|
||||
abort_if(!$request->user(), 404);
|
||||
abort_if(!$request->user() || !$request->user()->token(), 404);
|
||||
|
||||
abort_unless($request->user()->is_admin == 1, 404);
|
||||
abort_unless($request->user()->tokenCan('admin:write'), 404);
|
||||
|
||||
$this->validate($request, [
|
||||
'id' => 'required',
|
||||
|
@ -567,8 +773,10 @@ class AdminApiController extends Controller
|
|||
|
||||
public function refreshInstanceStats(Request $request)
|
||||
{
|
||||
abort_if(!$request->user(), 404);
|
||||
abort_if(!$request->user() || !$request->user()->token(), 404);
|
||||
|
||||
abort_unless($request->user()->is_admin == 1, 404);
|
||||
abort_unless($request->user()->tokenCan('admin:write'), 404);
|
||||
|
||||
$this->validate($request, [
|
||||
'id' => 'required',
|
||||
|
@ -582,4 +790,64 @@ class AdminApiController extends Controller
|
|||
|
||||
return new AdminInstance($instance);
|
||||
}
|
||||
|
||||
public function getAllStats(Request $request)
|
||||
{
|
||||
abort_if(!$request->user() || !$request->user()->token(), 404);
|
||||
|
||||
abort_unless($request->user()->is_admin === 1, 404);
|
||||
abort_unless($request->user()->tokenCan('admin:read'), 404);
|
||||
|
||||
if($request->has('refresh')) {
|
||||
Cache::forget('admin-api:instance-all-stats-v1');
|
||||
}
|
||||
|
||||
return Cache::remember('admin-api:instance-all-stats-v1', 1209600, function() {
|
||||
$days = range(1, 7);
|
||||
$res = [
|
||||
'cached_at' => now()->format('c'),
|
||||
];
|
||||
$minStatusId = SnowflakeService::byDate(now()->subDays(7));
|
||||
|
||||
foreach($days as $day) {
|
||||
$label = now()->subDays($day)->format('D');
|
||||
$labelShort = substr($label, 0, 1);
|
||||
$res['users']['days'][] = [
|
||||
'date' => now()->subDays($day)->format('M j Y'),
|
||||
'label_full' => $label,
|
||||
'label' => $labelShort,
|
||||
'count' => User::whereDate('created_at', now()->subDays($day))->count()
|
||||
];
|
||||
|
||||
$res['posts']['days'][] = [
|
||||
'date' => now()->subDays($day)->format('M j Y'),
|
||||
'label_full' => $label,
|
||||
'label' => $labelShort,
|
||||
'count' => Status::whereNull('uri')->where('id', '>', $minStatusId)->whereDate('created_at', now()->subDays($day))->count()
|
||||
];
|
||||
|
||||
$res['instances']['days'][] = [
|
||||
'date' => now()->subDays($day)->format('M j Y'),
|
||||
'label_full' => $label,
|
||||
'label' => $labelShort,
|
||||
'count' => Instance::whereDate('created_at', now()->subDays($day))->count()
|
||||
];
|
||||
}
|
||||
|
||||
$res['users']['total'] = DB::table('users')->count();
|
||||
$res['users']['min'] = collect($res['users']['days'])->min('count');
|
||||
$res['users']['max'] = collect($res['users']['days'])->max('count');
|
||||
$res['users']['change'] = collect($res['users']['days'])->sum('count');;
|
||||
$res['posts']['total'] = DB::table('statuses')->whereNull('uri')->count();
|
||||
$res['posts']['min'] = collect($res['posts']['days'])->min('count');
|
||||
$res['posts']['max'] = collect($res['posts']['days'])->max('count');
|
||||
$res['posts']['change'] = collect($res['posts']['days'])->sum('count');
|
||||
$res['instances']['total'] = DB::table('instances')->count();
|
||||
$res['instances']['min'] = collect($res['instances']['days'])->min('count');
|
||||
$res['instances']['max'] = collect($res['instances']['days'])->max('count');
|
||||
$res['instances']['change'] = collect($res['instances']['days'])->sum('count');
|
||||
|
||||
return $res;
|
||||
});
|
||||
}
|
||||
}
|
||||
|
|
Plik diff jest za duży
Load Diff
Plik diff jest za duży
Load Diff
|
@ -0,0 +1,339 @@
|
|||
<?php
|
||||
|
||||
namespace App\Http\Controllers\Api;
|
||||
|
||||
use Illuminate\Http\Request;
|
||||
use App\Http\Controllers\Controller;
|
||||
use App\Media;
|
||||
use App\UserSetting;
|
||||
use App\User;
|
||||
use Illuminate\Support\Facades\Cache;
|
||||
use App\Services\AccountService;
|
||||
use App\Services\BouncerService;
|
||||
use App\Services\InstanceService;
|
||||
use App\Services\MediaBlocklistService;
|
||||
use App\Services\MediaPathService;
|
||||
use App\Services\SearchApiV2Service;
|
||||
use App\Util\Media\Filter;
|
||||
use App\Jobs\MediaPipeline\MediaDeletePipeline;
|
||||
use App\Jobs\VideoPipeline\{
|
||||
VideoOptimize,
|
||||
VideoPostProcess,
|
||||
VideoThumbnail
|
||||
};
|
||||
use App\Jobs\ImageOptimizePipeline\ImageOptimize;
|
||||
use League\Fractal;
|
||||
use League\Fractal\Serializer\ArraySerializer;
|
||||
use League\Fractal\Pagination\IlluminatePaginatorAdapter;
|
||||
use App\Transformer\Api\Mastodon\v1\{
|
||||
AccountTransformer,
|
||||
MediaTransformer,
|
||||
NotificationTransformer,
|
||||
StatusTransformer,
|
||||
};
|
||||
use App\Transformer\Api\{
|
||||
RelationshipTransformer,
|
||||
};
|
||||
use App\Util\Site\Nodeinfo;
|
||||
use App\Services\UserRoleService;
|
||||
|
||||
class ApiV2Controller extends Controller
|
||||
{
|
||||
const PF_API_ENTITY_KEY = "_pe";
|
||||
|
||||
public function json($res, $code = 200, $headers = [])
|
||||
{
|
||||
return response()->json($res, $code, $headers, JSON_UNESCAPED_SLASHES);
|
||||
}
|
||||
|
||||
public function instance(Request $request)
|
||||
{
|
||||
$contact = Cache::remember('api:v1:instance-data:contact', 604800, function () {
|
||||
if(config_cache('instance.admin.pid')) {
|
||||
return AccountService::getMastodon(config_cache('instance.admin.pid'), true);
|
||||
}
|
||||
$admin = User::whereIsAdmin(true)->first();
|
||||
return $admin && isset($admin->profile_id) ?
|
||||
AccountService::getMastodon($admin->profile_id, true) :
|
||||
null;
|
||||
});
|
||||
|
||||
$rules = Cache::remember('api:v1:instance-data:rules', 604800, function () {
|
||||
return config_cache('app.rules') ?
|
||||
collect(json_decode(config_cache('app.rules'), true))
|
||||
->map(function($rule, $key) {
|
||||
$id = $key + 1;
|
||||
return [
|
||||
'id' => "{$id}",
|
||||
'text' => $rule
|
||||
];
|
||||
})
|
||||
->toArray() : [];
|
||||
});
|
||||
|
||||
$res = Cache::remember('api:v2:instance-data-response-v2', 1800, function () use($contact, $rules) {
|
||||
return [
|
||||
'domain' => config('pixelfed.domain.app'),
|
||||
'title' => config_cache('app.name'),
|
||||
'version' => '3.5.3 (compatible; Pixelfed ' . config('pixelfed.version') .')',
|
||||
'source_url' => 'https://github.com/pixelfed/pixelfed',
|
||||
'description' => config_cache('app.short_description'),
|
||||
'usage' => [
|
||||
'users' => [
|
||||
'active_month' => (int) Nodeinfo::activeUsersMonthly()
|
||||
]
|
||||
],
|
||||
'thumbnail' => [
|
||||
'url' => config_cache('app.banner_image') ?? url(Storage::url('public/headers/default.jpg')),
|
||||
'blurhash' => InstanceService::headerBlurhash(),
|
||||
'versions' => [
|
||||
'@1x' => config_cache('app.banner_image') ?? url(Storage::url('public/headers/default.jpg')),
|
||||
'@2x' => config_cache('app.banner_image') ?? url(Storage::url('public/headers/default.jpg'))
|
||||
]
|
||||
],
|
||||
'languages' => [config('app.locale')],
|
||||
'configuration' => [
|
||||
'urls' => [
|
||||
'streaming' => null,
|
||||
'status' => null
|
||||
],
|
||||
'vapid' => [
|
||||
'public_key' => config('webpush.vapid.public_key'),
|
||||
],
|
||||
'accounts' => [
|
||||
'max_featured_tags' => 0,
|
||||
],
|
||||
'statuses' => [
|
||||
'max_characters' => (int) config_cache('pixelfed.max_caption_length'),
|
||||
'max_media_attachments' => (int) config_cache('pixelfed.max_album_length'),
|
||||
'characters_reserved_per_url' => 23
|
||||
],
|
||||
'media_attachments' => [
|
||||
'supported_mime_types' => explode(',', config_cache('pixelfed.media_types')),
|
||||
'image_size_limit' => config_cache('pixelfed.max_photo_size') * 1024,
|
||||
'image_matrix_limit' => 3686400,
|
||||
'video_size_limit' => config_cache('pixelfed.max_photo_size') * 1024,
|
||||
'video_frame_rate_limit' => 240,
|
||||
'video_matrix_limit' => 3686400
|
||||
],
|
||||
'polls' => [
|
||||
'max_options' => 0,
|
||||
'max_characters_per_option' => 0,
|
||||
'min_expiration' => 0,
|
||||
'max_expiration' => 0,
|
||||
],
|
||||
'translation' => [
|
||||
'enabled' => false,
|
||||
],
|
||||
],
|
||||
'registrations' => [
|
||||
'enabled' => null,
|
||||
'approval_required' => false,
|
||||
'message' => null,
|
||||
'url' => null,
|
||||
],
|
||||
'contact' => [
|
||||
'email' => config('instance.email'),
|
||||
'account' => $contact
|
||||
],
|
||||
'rules' => $rules
|
||||
];
|
||||
});
|
||||
|
||||
$res['registrations']['enabled'] = (bool) config_cache('pixelfed.open_registration');
|
||||
$res['registrations']['approval_required'] = (bool) config_cache('instance.curated_registration.enabled');
|
||||
return response()->json($res, 200, [], JSON_UNESCAPED_SLASHES);
|
||||
}
|
||||
|
||||
/**
|
||||
* GET /api/v2/search
|
||||
*
|
||||
*
|
||||
* @return array
|
||||
*/
|
||||
public function search(Request $request)
|
||||
{
|
||||
abort_if(!$request->user() || !$request->user()->token(), 403);
|
||||
abort_unless($request->user()->tokenCan('read'), 403);
|
||||
|
||||
$this->validate($request, [
|
||||
'q' => 'required|string|min:1|max:100',
|
||||
'account_id' => 'nullable|string',
|
||||
'max_id' => 'nullable|string',
|
||||
'min_id' => 'nullable|string',
|
||||
'type' => 'nullable|in:accounts,hashtags,statuses',
|
||||
'exclude_unreviewed' => 'nullable',
|
||||
'resolve' => 'nullable',
|
||||
'limit' => 'nullable|integer|max:40',
|
||||
'offset' => 'nullable|integer',
|
||||
'following' => 'nullable'
|
||||
]);
|
||||
|
||||
if($request->user()->has_roles && !UserRoleService::can('can-view-discover', $request->user()->id)) {
|
||||
return [
|
||||
'accounts' => [],
|
||||
'hashtags' => [],
|
||||
'statuses' => []
|
||||
];
|
||||
}
|
||||
|
||||
$mastodonMode = !$request->has('_pe');
|
||||
return $this->json(SearchApiV2Service::query($request, $mastodonMode));
|
||||
}
|
||||
|
||||
/**
|
||||
* GET /api/v2/streaming/config
|
||||
*
|
||||
*
|
||||
* @return object
|
||||
*/
|
||||
public function getWebsocketConfig()
|
||||
{
|
||||
return config('broadcasting.default') === 'pusher' ? [
|
||||
'host' => config('broadcasting.connections.pusher.options.host'),
|
||||
'port' => config('broadcasting.connections.pusher.options.port'),
|
||||
'key' => config('broadcasting.connections.pusher.key'),
|
||||
'cluster' => config('broadcasting.connections.pusher.options.cluster')
|
||||
] : [];
|
||||
}
|
||||
|
||||
/**
|
||||
* POST /api/v2/media
|
||||
*
|
||||
*
|
||||
* @return MediaTransformer
|
||||
*/
|
||||
public function mediaUploadV2(Request $request)
|
||||
{
|
||||
abort_if(!$request->user() || !$request->user()->token(), 403);
|
||||
abort_unless($request->user()->tokenCan('write'), 403);
|
||||
|
||||
$this->validate($request, [
|
||||
'file.*' => [
|
||||
'required_without:file',
|
||||
'mimetypes:' . config_cache('pixelfed.media_types'),
|
||||
'max:' . config_cache('pixelfed.max_photo_size'),
|
||||
],
|
||||
'file' => [
|
||||
'required_without:file.*',
|
||||
'mimetypes:' . config_cache('pixelfed.media_types'),
|
||||
'max:' . config_cache('pixelfed.max_photo_size'),
|
||||
],
|
||||
'filter_name' => 'nullable|string|max:24',
|
||||
'filter_class' => 'nullable|alpha_dash|max:24',
|
||||
'description' => 'nullable|string|max:' . config_cache('pixelfed.max_altext_length'),
|
||||
'replace_id' => 'sometimes'
|
||||
]);
|
||||
|
||||
$user = $request->user();
|
||||
|
||||
if($user->last_active_at == null) {
|
||||
return [];
|
||||
}
|
||||
|
||||
if(empty($request->file('file'))) {
|
||||
return response('', 422);
|
||||
}
|
||||
|
||||
$limitKey = 'compose:rate-limit:media-upload:' . $user->id;
|
||||
$limitTtl = now()->addMinutes(15);
|
||||
$limitReached = Cache::remember($limitKey, $limitTtl, function() use($user) {
|
||||
$dailyLimit = Media::whereUserId($user->id)->where('created_at', '>', now()->subDays(1))->count();
|
||||
|
||||
return $dailyLimit >= 1250;
|
||||
});
|
||||
abort_if($limitReached == true, 429);
|
||||
|
||||
$profile = $user->profile;
|
||||
|
||||
if(config_cache('pixelfed.enforce_account_limit') == true) {
|
||||
$size = Cache::remember($user->storageUsedKey(), now()->addDays(3), function() use($user) {
|
||||
return Media::whereUserId($user->id)->sum('size') / 1000;
|
||||
});
|
||||
$limit = (int) config_cache('pixelfed.max_account_size');
|
||||
if ($size >= $limit) {
|
||||
abort(403, 'Account size limit reached.');
|
||||
}
|
||||
}
|
||||
|
||||
$filterClass = in_array($request->input('filter_class'), Filter::classes()) ? $request->input('filter_class') : null;
|
||||
$filterName = in_array($request->input('filter_name'), Filter::names()) ? $request->input('filter_name') : null;
|
||||
|
||||
$photo = $request->file('file');
|
||||
|
||||
$mimes = explode(',', config_cache('pixelfed.media_types'));
|
||||
if(in_array($photo->getMimeType(), $mimes) == false) {
|
||||
abort(403, 'Invalid or unsupported mime type.');
|
||||
}
|
||||
|
||||
$storagePath = MediaPathService::get($user, 2);
|
||||
$path = $photo->storePublicly($storagePath);
|
||||
$hash = \hash_file('sha256', $photo);
|
||||
$license = null;
|
||||
$mime = $photo->getMimeType();
|
||||
|
||||
$settings = UserSetting::whereUserId($user->id)->first();
|
||||
|
||||
if($settings && !empty($settings->compose_settings)) {
|
||||
$compose = $settings->compose_settings;
|
||||
|
||||
if(isset($compose['default_license']) && $compose['default_license'] != 1) {
|
||||
$license = $compose['default_license'];
|
||||
}
|
||||
}
|
||||
|
||||
abort_if(MediaBlocklistService::exists($hash) == true, 451);
|
||||
|
||||
if($request->has('replace_id')) {
|
||||
$rpid = $request->input('replace_id');
|
||||
$removeMedia = Media::whereNull('status_id')
|
||||
->whereUserId($user->id)
|
||||
->whereProfileId($profile->id)
|
||||
->where('created_at', '>', now()->subHours(2))
|
||||
->find($rpid);
|
||||
if($removeMedia) {
|
||||
MediaDeletePipeline::dispatch($removeMedia)
|
||||
->onQueue('mmo')
|
||||
->delay(now()->addMinutes(15));
|
||||
}
|
||||
}
|
||||
|
||||
$media = new Media();
|
||||
$media->status_id = null;
|
||||
$media->profile_id = $profile->id;
|
||||
$media->user_id = $user->id;
|
||||
$media->media_path = $path;
|
||||
$media->original_sha256 = $hash;
|
||||
$media->size = $photo->getSize();
|
||||
$media->mime = $mime;
|
||||
$media->caption = $request->input('description');
|
||||
$media->filter_class = $filterClass;
|
||||
$media->filter_name = $filterName;
|
||||
if($license) {
|
||||
$media->license = $license;
|
||||
}
|
||||
$media->save();
|
||||
|
||||
switch ($media->mime) {
|
||||
case 'image/jpeg':
|
||||
case 'image/png':
|
||||
ImageOptimize::dispatch($media)->onQueue('mmo');
|
||||
break;
|
||||
|
||||
case 'video/mp4':
|
||||
VideoThumbnail::dispatch($media)->onQueue('mmo');
|
||||
$preview_url = '/storage/no-preview.png';
|
||||
$url = '/storage/no-preview.png';
|
||||
break;
|
||||
}
|
||||
|
||||
Cache::forget($limitKey);
|
||||
$fractal = new Fractal\Manager();
|
||||
$fractal->setSerializer(new ArraySerializer());
|
||||
$resource = new Fractal\Resource\Item($media, new MediaTransformer());
|
||||
$res = $fractal->createData($resource)->toArray();
|
||||
$res['preview_url'] = $media->url(). '?v=' . time();
|
||||
$res['url'] = null;
|
||||
return $this->json($res, 202);
|
||||
}
|
||||
}
|
|
@ -99,6 +99,7 @@ class BaseApiController extends Controller
|
|||
public function avatarUpdate(Request $request)
|
||||
{
|
||||
abort_if(!$request->user(), 403);
|
||||
|
||||
$this->validate($request, [
|
||||
'upload' => 'required|mimetypes:image/jpeg,image/jpg,image/png|max:'.config('pixelfed.max_avatar_size'),
|
||||
]);
|
||||
|
@ -134,9 +135,10 @@ class BaseApiController extends Controller
|
|||
|
||||
public function verifyCredentials(Request $request)
|
||||
{
|
||||
abort_if(!$request->user(), 403);
|
||||
|
||||
$user = $request->user();
|
||||
abort_if(!$user, 403);
|
||||
if($user->status != null) {
|
||||
if ($user->status != null) {
|
||||
Auth::logout();
|
||||
abort(403);
|
||||
}
|
||||
|
@ -147,6 +149,7 @@ class BaseApiController extends Controller
|
|||
public function accountLikes(Request $request)
|
||||
{
|
||||
abort_if(!$request->user(), 403);
|
||||
|
||||
$this->validate($request, [
|
||||
'page' => 'sometimes|int|min:1|max:20',
|
||||
'limit' => 'sometimes|int|min:1|max:10'
|
||||
|
|
|
@ -0,0 +1,119 @@
|
|||
<?php
|
||||
|
||||
namespace App\Http\Controllers\Api\V1;
|
||||
|
||||
use Illuminate\Http\Request;
|
||||
use App\Http\Controllers\Controller;
|
||||
use App\Models\UserDomainBlock;
|
||||
use App\Util\ActivityPub\Helpers;
|
||||
use App\Services\UserFilterService;
|
||||
use Illuminate\Bus\Batch;
|
||||
use Illuminate\Support\Facades\Bus;
|
||||
use Illuminate\Support\Facades\Cache;
|
||||
use App\Jobs\HomeFeedPipeline\FeedRemoveDomainPipeline;
|
||||
use App\Jobs\ProfilePipeline\ProfilePurgeNotificationsByDomain;
|
||||
use App\Jobs\ProfilePipeline\ProfilePurgeFollowersByDomain;
|
||||
|
||||
class DomainBlockController extends Controller
|
||||
{
|
||||
public function json($res, $code = 200, $headers = [])
|
||||
{
|
||||
return response()->json($res, $code, $headers, JSON_UNESCAPED_SLASHES);
|
||||
}
|
||||
|
||||
public function index(Request $request)
|
||||
{
|
||||
abort_if(!$request->user(), 403);
|
||||
|
||||
$this->validate($request, [
|
||||
'limit' => 'sometimes|integer|min:1|max:200'
|
||||
]);
|
||||
$limit = $request->input('limit', 100);
|
||||
$id = $request->user()->profile_id;
|
||||
$filters = UserDomainBlock::whereProfileId($id)->orderByDesc('id')->cursorPaginate($limit);
|
||||
$links = null;
|
||||
$headers = [];
|
||||
|
||||
if($filters->nextCursor()) {
|
||||
$links .= '<'.$filters->nextPageUrl().'&limit='.$limit.'>; rel="next"';
|
||||
}
|
||||
|
||||
if($filters->previousCursor()) {
|
||||
if($links != null) {
|
||||
$links .= ', ';
|
||||
}
|
||||
$links .= '<'.$filters->previousPageUrl().'&limit='.$limit.'>; rel="prev"';
|
||||
}
|
||||
|
||||
if($links) {
|
||||
$headers = ['Link' => $links];
|
||||
}
|
||||
return $this->json($filters->pluck('domain'), 200, $headers);
|
||||
}
|
||||
|
||||
public function store(Request $request)
|
||||
{
|
||||
abort_if(!$request->user(), 403);
|
||||
|
||||
$this->validate($request, [
|
||||
'domain' => 'required|active_url|min:1|max:120'
|
||||
]);
|
||||
|
||||
$pid = $request->user()->profile_id;
|
||||
|
||||
$domain = trim($request->input('domain'));
|
||||
|
||||
if(Helpers::validateUrl($domain) == false) {
|
||||
return abort(500, 'Invalid domain or already blocked by server admins');
|
||||
}
|
||||
|
||||
$domain = strtolower(parse_url($domain, PHP_URL_HOST));
|
||||
|
||||
abort_if(config_cache('pixelfed.domain.app') == $domain, 400, 'Cannot ban your own server');
|
||||
|
||||
$existingCount = UserDomainBlock::whereProfileId($pid)->count();
|
||||
$maxLimit = (int) config_cache('instance.user_filters.max_domain_blocks');
|
||||
$errorMsg = __('profile.block.domain.max', ['max' => $maxLimit]);
|
||||
|
||||
abort_if($existingCount >= $maxLimit, 400, $errorMsg);
|
||||
|
||||
$block = UserDomainBlock::updateOrCreate([
|
||||
'profile_id' => $pid,
|
||||
'domain' => $domain
|
||||
]);
|
||||
|
||||
if($block->wasRecentlyCreated) {
|
||||
Bus::batch([
|
||||
[
|
||||
new FeedRemoveDomainPipeline($pid, $domain),
|
||||
new ProfilePurgeNotificationsByDomain($pid, $domain),
|
||||
new ProfilePurgeFollowersByDomain($pid, $domain)
|
||||
]
|
||||
])->allowFailures()->onQueue('feed')->dispatch();
|
||||
|
||||
Cache::forget('profile:following:' . $pid);
|
||||
UserFilterService::domainBlocks($pid, true);
|
||||
}
|
||||
|
||||
return $this->json([]);
|
||||
}
|
||||
|
||||
public function delete(Request $request)
|
||||
{
|
||||
abort_if(!$request->user(), 403);
|
||||
|
||||
$this->validate($request, [
|
||||
'domain' => 'required|min:1|max:120'
|
||||
]);
|
||||
|
||||
$pid = $request->user()->profile_id;
|
||||
|
||||
$domain = strtolower(trim($request->input('domain')));
|
||||
|
||||
$filters = UserDomainBlock::whereProfileId($pid)->whereDomain($domain)->delete();
|
||||
|
||||
UserFilterService::domainBlocks($pid, true);
|
||||
|
||||
return $this->json([]);
|
||||
}
|
||||
}
|
|
@ -0,0 +1,209 @@
|
|||
<?php
|
||||
|
||||
namespace App\Http\Controllers\Api\V1;
|
||||
|
||||
use Illuminate\Http\Request;
|
||||
use App\Http\Controllers\Controller;
|
||||
use App\Hashtag;
|
||||
use App\HashtagFollow;
|
||||
use App\StatusHashtag;
|
||||
use App\Services\AccountService;
|
||||
use App\Services\HashtagService;
|
||||
use App\Services\HashtagFollowService;
|
||||
use App\Services\HashtagRelatedService;
|
||||
use App\Http\Resources\MastoApi\FollowedTagResource;
|
||||
use App\Jobs\HomeFeedPipeline\FeedWarmCachePipeline;
|
||||
use App\Jobs\HomeFeedPipeline\HashtagUnfollowPipeline;
|
||||
|
||||
class TagsController extends Controller
|
||||
{
|
||||
const PF_API_ENTITY_KEY = "_pe";
|
||||
|
||||
public function json($res, $code = 200, $headers = [])
|
||||
{
|
||||
return response()->json($res, $code, $headers, JSON_UNESCAPED_SLASHES);
|
||||
}
|
||||
|
||||
/**
|
||||
* GET /api/v1/tags/:id/related
|
||||
*
|
||||
*
|
||||
* @return array
|
||||
*/
|
||||
public function relatedTags(Request $request, $tag)
|
||||
{
|
||||
abort_if(!$request->user() || !$request->user()->token(), 403);
|
||||
abort_unless($request->user()->tokenCan('read'), 403);
|
||||
|
||||
$tag = Hashtag::whereSlug($tag)->firstOrFail();
|
||||
return HashtagRelatedService::get($tag->id);
|
||||
}
|
||||
|
||||
/**
|
||||
* POST /api/v1/tags/:id/follow
|
||||
*
|
||||
*
|
||||
* @return object
|
||||
*/
|
||||
public function followHashtag(Request $request, $id)
|
||||
{
|
||||
abort_if(!$request->user(), 403);
|
||||
|
||||
$pid = $request->user()->profile_id;
|
||||
$account = AccountService::get($pid);
|
||||
|
||||
$operator = config('database.default') == 'pgsql' ? 'ilike' : 'like';
|
||||
$tag = Hashtag::where('name', $operator, $id)
|
||||
->orWhere('slug', $operator, $id)
|
||||
->first();
|
||||
|
||||
abort_if(!$tag, 422, 'Unknown hashtag');
|
||||
|
||||
abort_if(
|
||||
HashtagFollow::whereProfileId($pid)->count() >= HashtagFollow::MAX_LIMIT,
|
||||
422,
|
||||
'You cannot follow more than ' . HashtagFollow::MAX_LIMIT . ' hashtags.'
|
||||
);
|
||||
|
||||
$follows = HashtagFollow::updateOrCreate(
|
||||
[
|
||||
'profile_id' => $account['id'],
|
||||
'hashtag_id' => $tag->id
|
||||
],
|
||||
[
|
||||
'user_id' => $request->user()->id
|
||||
]
|
||||
);
|
||||
|
||||
HashtagService::follow($pid, $tag->id);
|
||||
HashtagFollowService::add($tag->id, $pid);
|
||||
|
||||
return response()->json(FollowedTagResource::make($follows)->toArray($request));
|
||||
}
|
||||
|
||||
/**
|
||||
* POST /api/v1/tags/:id/unfollow
|
||||
*
|
||||
*
|
||||
* @return object
|
||||
*/
|
||||
public function unfollowHashtag(Request $request, $id)
|
||||
{
|
||||
abort_if(!$request->user(), 403);
|
||||
|
||||
$pid = $request->user()->profile_id;
|
||||
$account = AccountService::get($pid);
|
||||
|
||||
$operator = config('database.default') == 'pgsql' ? 'ilike' : 'like';
|
||||
$tag = Hashtag::where('name', $operator, $id)
|
||||
->orWhere('slug', $operator, $id)
|
||||
->first();
|
||||
|
||||
abort_if(!$tag, 422, 'Unknown hashtag');
|
||||
|
||||
$follows = HashtagFollow::whereProfileId($pid)
|
||||
->whereHashtagId($tag->id)
|
||||
->first();
|
||||
|
||||
if(!$follows) {
|
||||
return [
|
||||
'name' => $tag->name,
|
||||
'url' => config('app.url') . '/i/web/hashtag/' . $tag->slug,
|
||||
'history' => [],
|
||||
'following' => false
|
||||
];
|
||||
}
|
||||
|
||||
if($follows) {
|
||||
HashtagService::unfollow($pid, $tag->id);
|
||||
HashtagFollowService::unfollow($tag->id, $pid);
|
||||
HashtagUnfollowPipeline::dispatch($tag->id, $pid, $tag->slug)->onQueue('feed');
|
||||
$follows->delete();
|
||||
}
|
||||
|
||||
$res = FollowedTagResource::make($follows)->toArray($request);
|
||||
$res['following'] = false;
|
||||
return response()->json($res);
|
||||
}
|
||||
|
||||
/**
|
||||
* GET /api/v1/tags/:id
|
||||
*
|
||||
*
|
||||
* @return object
|
||||
*/
|
||||
public function getHashtag(Request $request, $id)
|
||||
{
|
||||
abort_if(!$request->user(), 403);
|
||||
|
||||
$pid = $request->user()->profile_id;
|
||||
$account = AccountService::get($pid);
|
||||
$operator = config('database.default') == 'pgsql' ? 'ilike' : 'like';
|
||||
$tag = Hashtag::where('name', $operator, $id)
|
||||
->orWhere('slug', $operator, $id)
|
||||
->first();
|
||||
|
||||
if(!$tag) {
|
||||
return [
|
||||
'name' => $id,
|
||||
'url' => config('app.url') . '/i/web/hashtag/' . $id,
|
||||
'history' => [],
|
||||
'following' => false
|
||||
];
|
||||
}
|
||||
|
||||
$res = [
|
||||
'name' => $tag->name,
|
||||
'url' => config('app.url') . '/i/web/hashtag/' . $tag->slug,
|
||||
'history' => [],
|
||||
'following' => HashtagService::isFollowing($pid, $tag->id)
|
||||
];
|
||||
|
||||
if($request->has(self::PF_API_ENTITY_KEY)) {
|
||||
$res['count'] = HashtagService::count($tag->id);
|
||||
}
|
||||
|
||||
return $this->json($res);
|
||||
}
|
||||
|
||||
/**
|
||||
* GET /api/v1/followed_tags
|
||||
*
|
||||
*
|
||||
* @return array
|
||||
*/
|
||||
public function getFollowedTags(Request $request)
|
||||
{
|
||||
abort_if(!$request->user(), 403);
|
||||
|
||||
$account = AccountService::get($request->user()->profile_id);
|
||||
|
||||
$this->validate($request, [
|
||||
'cursor' => 'sometimes',
|
||||
'limit' => 'sometimes|integer|min:1|max:200'
|
||||
]);
|
||||
$limit = $request->input('limit', 100);
|
||||
|
||||
$res = HashtagFollow::whereProfileId($account['id'])
|
||||
->orderByDesc('id')
|
||||
->cursorPaginate($limit)
|
||||
->withQueryString();
|
||||
|
||||
$pagination = false;
|
||||
$prevPage = $res->nextPageUrl();
|
||||
$nextPage = $res->previousPageUrl();
|
||||
if($nextPage && $prevPage) {
|
||||
$pagination = '<' . $nextPage . '>; rel="next", <' . $prevPage . '>; rel="prev"';
|
||||
} else if($nextPage && !$prevPage) {
|
||||
$pagination = '<' . $nextPage . '>; rel="next"';
|
||||
} else if(!$nextPage && $prevPage) {
|
||||
$pagination = '<' . $prevPage . '>; rel="prev"';
|
||||
}
|
||||
|
||||
if($pagination) {
|
||||
return response()->json(FollowedTagResource::collection($res)->collection)
|
||||
->header('Link', $pagination);
|
||||
}
|
||||
return response()->json(FollowedTagResource::collection($res)->collection);
|
||||
}
|
||||
}
|
|
@ -62,7 +62,7 @@ class ForgotPasswordController extends Controller
|
|||
|
||||
usleep(random_int(100000, 3000000));
|
||||
|
||||
if(config('captcha.enabled')) {
|
||||
if((bool) config_cache('captcha.enabled')) {
|
||||
$rules = [
|
||||
'email' => 'required|email',
|
||||
'h-captcha-response' => 'required|captcha'
|
||||
|
|
|
@ -7,6 +7,8 @@ use App\Http\Controllers\Controller;
|
|||
use App\User;
|
||||
use Illuminate\Foundation\Auth\AuthenticatesUsers;
|
||||
use App\Services\BouncerService;
|
||||
use Illuminate\Http\Request;
|
||||
use Illuminate\Validation\ValidationException;
|
||||
|
||||
class LoginController extends Controller
|
||||
{
|
||||
|
@ -69,12 +71,21 @@ class LoginController extends Controller
|
|||
$this->username() => 'required|email',
|
||||
'password' => 'required|string|min:6',
|
||||
];
|
||||
$messages = [];
|
||||
|
||||
if(config('captcha.enabled')) {
|
||||
$rules['h-captcha-response'] = 'required|captcha';
|
||||
if(
|
||||
(bool) config_cache('captcha.enabled') &&
|
||||
(bool) config_cache('captcha.active.login') ||
|
||||
(
|
||||
(bool) config_cache('captcha.triggers.login.enabled') &&
|
||||
request()->session()->has('login_attempts') &&
|
||||
request()->session()->get('login_attempts') >= config('captcha.triggers.login.attempts')
|
||||
)
|
||||
) {
|
||||
$rules['h-captcha-response'] = 'required|filled|captcha|min:5';
|
||||
$messages['h-captcha-response.required'] = 'The captcha must be filled';
|
||||
}
|
||||
|
||||
$this->validate($request, $rules);
|
||||
$request->validate($rules, $messages);
|
||||
}
|
||||
|
||||
/**
|
||||
|
@ -102,4 +113,28 @@ class LoginController extends Controller
|
|||
$log->user_agent = $request->userAgent();
|
||||
$log->save();
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the failed login response instance.
|
||||
*
|
||||
* @param \Illuminate\Http\Request $request
|
||||
* @return \Symfony\Component\HttpFoundation\Response
|
||||
*
|
||||
* @throws \Illuminate\Validation\ValidationException
|
||||
*/
|
||||
protected function sendFailedLoginResponse(Request $request)
|
||||
{
|
||||
if(config('captcha.triggers.login.enabled')) {
|
||||
if ($request->session()->has('login_attempts')) {
|
||||
$ct = $request->session()->get('login_attempts');
|
||||
$request->session()->put('login_attempts', $ct + 1);
|
||||
} else {
|
||||
$request->session()->put('login_attempts', 1);
|
||||
}
|
||||
}
|
||||
|
||||
throw ValidationException::withMessages([
|
||||
$this->username() => [trans('auth.failed')],
|
||||
]);
|
||||
}
|
||||
}
|
||||
|
|
|
@ -60,7 +60,7 @@ class RegisterController extends Controller
|
|||
*
|
||||
* @return \Illuminate\Contracts\Validation\Validator
|
||||
*/
|
||||
protected function validator(array $data)
|
||||
public function validator(array $data)
|
||||
{
|
||||
if(config('database.default') == 'pgsql') {
|
||||
$data['username'] = strtolower($data['username']);
|
||||
|
@ -137,7 +137,7 @@ class RegisterController extends Controller
|
|||
'password' => 'required|string|min:'.config('pixelfed.min_password_length').'|confirmed',
|
||||
];
|
||||
|
||||
if(config('captcha.enabled')) {
|
||||
if((bool) config_cache('captcha.enabled') && (bool) config_cache('captcha.active.register')) {
|
||||
$rules['h-captcha-response'] = 'required|captcha';
|
||||
}
|
||||
|
||||
|
@ -151,7 +151,7 @@ class RegisterController extends Controller
|
|||
*
|
||||
* @return \App\User
|
||||
*/
|
||||
protected function create(array $data)
|
||||
public function create(array $data)
|
||||
{
|
||||
if(config('database.default') == 'pgsql') {
|
||||
$data['username'] = strtolower($data['username']);
|
||||
|
@ -174,12 +174,13 @@ class RegisterController extends Controller
|
|||
*/
|
||||
public function showRegistrationForm()
|
||||
{
|
||||
if(config_cache('pixelfed.open_registration')) {
|
||||
if((bool) config_cache('pixelfed.open_registration')) {
|
||||
if(config('pixelfed.bouncer.cloud_ips.ban_signups')) {
|
||||
abort_if(BouncerService::checkIp(request()->ip()), 404);
|
||||
}
|
||||
$limit = config('pixelfed.max_users');
|
||||
if($limit) {
|
||||
$hasLimit = config('pixelfed.enforce_max_users');
|
||||
if($hasLimit) {
|
||||
$limit = config('pixelfed.max_users');
|
||||
$count = User::where(function($q){ return $q->whereNull('status')->orWhereNotIn('status', ['deleted','delete']); })->count();
|
||||
if($limit <= $count) {
|
||||
return redirect(route('help.instance-max-users-limit'));
|
||||
|
@ -190,7 +191,11 @@ class RegisterController extends Controller
|
|||
return view('auth.register');
|
||||
}
|
||||
} else {
|
||||
abort(404);
|
||||
if((bool) config_cache('instance.curated_registration.enabled') && config('instance.curated_registration.state.fallback_on_closed_reg')) {
|
||||
return redirect('/auth/sign_up');
|
||||
} else {
|
||||
abort(404);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -208,13 +213,17 @@ class RegisterController extends Controller
|
|||
abort_if(BouncerService::checkIp($request->ip()), 404);
|
||||
}
|
||||
|
||||
$count = User::where(function($q){ return $q->whereNull('status')->orWhereNotIn('status', ['deleted','delete']); })->count();
|
||||
$limit = config('pixelfed.max_users');
|
||||
$hasLimit = config('pixelfed.enforce_max_users');
|
||||
if($hasLimit) {
|
||||
$count = User::where(function($q){ return $q->whereNull('status')->orWhereNotIn('status', ['deleted','delete']); })->count();
|
||||
$limit = config('pixelfed.max_users');
|
||||
|
||||
if(false == config_cache('pixelfed.open_registration') || $limit && $limit <= $count) {
|
||||
return redirect(route('help.instance-max-users-limit'));
|
||||
if($limit && $limit <= $count) {
|
||||
return redirect(route('help.instance-max-users-limit'));
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
$this->validator($request->all())->validate();
|
||||
|
||||
event(new Registered($user = $this->create($request->all())));
|
||||
|
|
|
@ -50,7 +50,7 @@ class ResetPasswordController extends Controller
|
|||
{
|
||||
usleep(random_int(100000, 3000000));
|
||||
|
||||
if(config('captcha.enabled')) {
|
||||
if((bool) config_cache('captcha.enabled')) {
|
||||
return [
|
||||
'token' => 'required',
|
||||
'email' => 'required|email',
|
||||
|
|
|
@ -8,60 +8,56 @@ use Auth;
|
|||
use Illuminate\Http\Request;
|
||||
use App\Services\BookmarkService;
|
||||
use App\Services\FollowerService;
|
||||
use App\Services\UserRoleService;
|
||||
|
||||
class BookmarkController extends Controller
|
||||
{
|
||||
public function __construct()
|
||||
{
|
||||
$this->middleware('auth');
|
||||
}
|
||||
public function __construct()
|
||||
{
|
||||
$this->middleware('auth');
|
||||
}
|
||||
|
||||
public function store(Request $request)
|
||||
{
|
||||
$this->validate($request, [
|
||||
'item' => 'required|integer|min:1',
|
||||
]);
|
||||
public function store(Request $request)
|
||||
{
|
||||
$this->validate($request, [
|
||||
'item' => 'required|integer|min:1',
|
||||
]);
|
||||
|
||||
$profile = Auth::user()->profile;
|
||||
$status = Status::findOrFail($request->input('item'));
|
||||
$user = $request->user();
|
||||
$status = Status::findOrFail($request->input('item'));
|
||||
|
||||
abort_if($status->in_reply_to_id || $status->reblog_of_id, 404);
|
||||
abort_if(!in_array($status->scope, ['public', 'unlisted', 'private']), 404);
|
||||
abort_if(!in_array($status->type, ['photo','photo:album', 'video', 'video:album', 'photo:video:album']), 404);
|
||||
abort_if($user->has_roles && !UserRoleService::can('can-bookmark', $user->id), 403, 'Invalid permissions for this action');
|
||||
abort_if($status->in_reply_to_id || $status->reblog_of_id, 404);
|
||||
abort_if(!in_array($status->scope, ['public', 'unlisted', 'private']), 404);
|
||||
abort_if(!in_array($status->type, ['photo','photo:album', 'video', 'video:album', 'photo:video:album']), 404);
|
||||
|
||||
if($status->scope == 'private') {
|
||||
if($profile->id !== $status->profile_id && !FollowerService::follows($profile->id, $status->profile_id)) {
|
||||
if($exists = Bookmark::whereStatusId($status->id)->whereProfileId($profile->id)->first()) {
|
||||
BookmarkService::del($profile->id, $status->id);
|
||||
$exists->delete();
|
||||
if($status->scope == 'private') {
|
||||
if($user->profile_id !== $status->profile_id && !FollowerService::follows($user->profile_id, $status->profile_id)) {
|
||||
if($exists = Bookmark::whereStatusId($status->id)->whereProfileId($user->profile_id)->first()) {
|
||||
BookmarkService::del($user->profile_id, $status->id);
|
||||
$exists->delete();
|
||||
|
||||
if ($request->ajax()) {
|
||||
return ['code' => 200, 'msg' => 'Bookmark removed!'];
|
||||
} else {
|
||||
return redirect()->back();
|
||||
}
|
||||
}
|
||||
abort(404, 'Error: You cannot bookmark private posts from accounts you do not follow.');
|
||||
}
|
||||
}
|
||||
if ($request->ajax()) {
|
||||
return ['code' => 200, 'msg' => 'Bookmark removed!'];
|
||||
} else {
|
||||
return redirect()->back();
|
||||
}
|
||||
}
|
||||
abort(404, 'Error: You cannot bookmark private posts from accounts you do not follow.');
|
||||
}
|
||||
}
|
||||
|
||||
$bookmark = Bookmark::firstOrCreate(
|
||||
['status_id' => $status->id], ['profile_id' => $profile->id]
|
||||
);
|
||||
$bookmark = Bookmark::firstOrCreate(
|
||||
['status_id' => $status->id], ['profile_id' => $user->profile_id]
|
||||
);
|
||||
|
||||
if (!$bookmark->wasRecentlyCreated) {
|
||||
BookmarkService::del($profile->id, $status->id);
|
||||
$bookmark->delete();
|
||||
} else {
|
||||
BookmarkService::add($profile->id, $status->id);
|
||||
}
|
||||
if (!$bookmark->wasRecentlyCreated) {
|
||||
BookmarkService::del($user->profile_id, $status->id);
|
||||
$bookmark->delete();
|
||||
} else {
|
||||
BookmarkService::add($user->profile_id, $status->id);
|
||||
}
|
||||
|
||||
if ($request->ajax()) {
|
||||
$response = ['code' => 200, 'msg' => 'Bookmark saved!'];
|
||||
} else {
|
||||
$response = redirect()->back();
|
||||
}
|
||||
|
||||
return $response;
|
||||
}
|
||||
return $request->expectsJson() ? ['code' => 200, 'msg' => 'Bookmark saved!'] : redirect()->back();
|
||||
}
|
||||
}
|
||||
|
|
|
@ -153,7 +153,7 @@ class CollectionController extends Controller
|
|||
abort(400, 'You can only add '.$max.' posts per collection');
|
||||
}
|
||||
|
||||
$status = Status::whereScope('public')
|
||||
$status = Status::whereIn('scope', ['public', 'unlisted'])
|
||||
->whereProfileId($profileId)
|
||||
->whereIn('type', ['photo', 'photo:album', 'video'])
|
||||
->findOrFail($postId);
|
||||
|
@ -166,17 +166,13 @@ class CollectionController extends Controller
|
|||
'order' => $count,
|
||||
]);
|
||||
|
||||
CollectionService::addItem(
|
||||
$collection->id,
|
||||
$status->id,
|
||||
$count
|
||||
);
|
||||
CollectionService::deleteCollection($collection->id);
|
||||
|
||||
$collection->updated_at = now();
|
||||
$collection->save();
|
||||
CollectionService::setCollection($collection->id, $collection);
|
||||
|
||||
return StatusService::get($status->id);
|
||||
return StatusService::get($status->id, false);
|
||||
}
|
||||
|
||||
public function getCollection(Request $request, $id)
|
||||
|
@ -226,10 +222,10 @@ class CollectionController extends Controller
|
|||
|
||||
return collect($items)
|
||||
->map(function($id) {
|
||||
return StatusService::get($id);
|
||||
return StatusService::get($id, false);
|
||||
})
|
||||
->filter(function($item) {
|
||||
return $item && isset($item['account'], $item['media_attachments']);
|
||||
return $item && ($item['visibility'] == 'public' || $item['visibility'] == 'unlisted') && isset($item['account'], $item['media_attachments']);
|
||||
})
|
||||
->values();
|
||||
}
|
||||
|
@ -298,7 +294,7 @@ class CollectionController extends Controller
|
|||
abort(400, 'You cannot delete the only post of a collection!');
|
||||
}
|
||||
|
||||
$status = Status::whereScope('public')
|
||||
$status = Status::whereIn('scope', ['public', 'unlisted'])
|
||||
->whereIn('type', ['photo', 'photo:album', 'video'])
|
||||
->findOrFail($postId);
|
||||
|
||||
|
|
|
@ -2,23 +2,18 @@
|
|||
|
||||
namespace App\Http\Controllers;
|
||||
|
||||
use Illuminate\Http\Request;
|
||||
use Auth;
|
||||
use DB;
|
||||
use Cache;
|
||||
|
||||
use App\Comment;
|
||||
use App\Jobs\CommentPipeline\CommentPipeline;
|
||||
use App\Jobs\StatusPipeline\NewStatusPipeline;
|
||||
use App\Util\Lexer\Autolink;
|
||||
use App\Profile;
|
||||
use App\Status;
|
||||
use App\UserFilter;
|
||||
use League\Fractal;
|
||||
use App\Transformer\Api\StatusTransformer;
|
||||
use League\Fractal\Serializer\ArraySerializer;
|
||||
use League\Fractal\Pagination\IlluminatePaginatorAdapter;
|
||||
use App\Services\StatusService;
|
||||
use App\Status;
|
||||
use App\Transformer\Api\StatusTransformer;
|
||||
use App\UserFilter;
|
||||
use App\Util\Lexer\Autolink;
|
||||
use Auth;
|
||||
use DB;
|
||||
use Illuminate\Http\Request;
|
||||
use League\Fractal;
|
||||
use League\Fractal\Serializer\ArraySerializer;
|
||||
|
||||
class CommentController extends Controller
|
||||
{
|
||||
|
@ -33,9 +28,9 @@ class CommentController extends Controller
|
|||
abort(403);
|
||||
}
|
||||
$this->validate($request, [
|
||||
'item' => 'required|integer|min:1',
|
||||
'comment' => 'required|string|max:'.(int) config('pixelfed.max_caption_length'),
|
||||
'sensitive' => 'nullable|boolean'
|
||||
'item' => 'required|integer|min:1',
|
||||
'comment' => 'required|string|max:'.config_cache('pixelfed.max_caption_length'),
|
||||
'sensitive' => 'nullable|boolean',
|
||||
]);
|
||||
$comment = $request->input('comment');
|
||||
$statusId = $request->input('item');
|
||||
|
@ -45,7 +40,7 @@ class CommentController extends Controller
|
|||
$profile = $user->profile;
|
||||
$status = Status::findOrFail($statusId);
|
||||
|
||||
if($status->comments_disabled == true) {
|
||||
if ($status->comments_disabled == true) {
|
||||
return;
|
||||
}
|
||||
|
||||
|
@ -55,11 +50,11 @@ class CommentController extends Controller
|
|||
->whereFilterableId($profile->id)
|
||||
->exists();
|
||||
|
||||
if($filtered == true) {
|
||||
if ($filtered == true) {
|
||||
return;
|
||||
}
|
||||
|
||||
$reply = DB::transaction(function() use($comment, $status, $profile, $nsfw) {
|
||||
$reply = DB::transaction(function () use ($comment, $status, $profile, $nsfw) {
|
||||
$scope = $profile->is_private == true ? 'private' : 'public';
|
||||
$autolink = Autolink::create()->autolink($comment);
|
||||
$reply = new Status();
|
||||
|
|
Plik diff jest za duży
Load Diff
|
@ -0,0 +1,399 @@
|
|||
<?php
|
||||
|
||||
namespace App\Http\Controllers;
|
||||
|
||||
use Illuminate\Http\Request;
|
||||
use Illuminate\Support\Str;
|
||||
use App\User;
|
||||
use App\Models\CuratedRegister;
|
||||
use App\Models\CuratedRegisterActivity;
|
||||
use App\Services\EmailService;
|
||||
use App\Services\BouncerService;
|
||||
use App\Util\Lexer\RestrictedNames;
|
||||
use App\Mail\CuratedRegisterConfirmEmail;
|
||||
use App\Mail\CuratedRegisterNotifyAdmin;
|
||||
use Illuminate\Support\Facades\Mail;
|
||||
use App\Jobs\CuratedOnboarding\CuratedOnboardingNotifyAdminNewApplicationPipeline;
|
||||
|
||||
class CuratedRegisterController extends Controller
|
||||
{
|
||||
public function __construct()
|
||||
{
|
||||
abort_unless((bool) config_cache('instance.curated_registration.enabled'), 404);
|
||||
|
||||
if((bool) config_cache('pixelfed.open_registration')) {
|
||||
abort_if(config('instance.curated_registration.state.only_enabled_on_closed_reg'), 404);
|
||||
} else {
|
||||
abort_unless(config('instance.curated_registration.state.fallback_on_closed_reg'), 404);
|
||||
}
|
||||
}
|
||||
|
||||
public function index(Request $request)
|
||||
{
|
||||
abort_if($request->user(), 404);
|
||||
return view('auth.curated-register.index', ['step' => 1]);
|
||||
}
|
||||
|
||||
public function concierge(Request $request)
|
||||
{
|
||||
abort_if($request->user(), 404);
|
||||
$emailConfirmed = $request->session()->has('cur-reg-con.email-confirmed') &&
|
||||
$request->has('next') &&
|
||||
$request->session()->has('cur-reg-con.cr-id');
|
||||
return view('auth.curated-register.concierge', compact('emailConfirmed'));
|
||||
}
|
||||
|
||||
public function conciergeResponseSent(Request $request)
|
||||
{
|
||||
return view('auth.curated-register.user_response_sent');
|
||||
}
|
||||
|
||||
public function conciergeFormShow(Request $request)
|
||||
{
|
||||
abort_if($request->user(), 404);
|
||||
abort_unless(
|
||||
$request->session()->has('cur-reg-con.email-confirmed') &&
|
||||
$request->session()->has('cur-reg-con.cr-id') &&
|
||||
$request->session()->has('cur-reg-con.ac-id'), 404);
|
||||
$crid = $request->session()->get('cur-reg-con.cr-id');
|
||||
$arid = $request->session()->get('cur-reg-con.ac-id');
|
||||
$showCaptcha = config('instance.curated_registration.captcha_enabled');
|
||||
if($attempts = $request->session()->get('cur-reg-con-attempt')) {
|
||||
$showCaptcha = $attempts && $attempts >= 2;
|
||||
} else {
|
||||
$showCaptcha = false;
|
||||
}
|
||||
$activity = CuratedRegisterActivity::whereRegisterId($crid)->whereFromAdmin(true)->findOrFail($arid);
|
||||
return view('auth.curated-register.concierge_form', compact('activity', 'showCaptcha'));
|
||||
}
|
||||
|
||||
public function conciergeFormStore(Request $request)
|
||||
{
|
||||
abort_if($request->user(), 404);
|
||||
$request->session()->increment('cur-reg-con-attempt');
|
||||
abort_unless(
|
||||
$request->session()->has('cur-reg-con.email-confirmed') &&
|
||||
$request->session()->has('cur-reg-con.cr-id') &&
|
||||
$request->session()->has('cur-reg-con.ac-id'), 404);
|
||||
$attempts = $request->session()->get('cur-reg-con-attempt');
|
||||
$messages = [];
|
||||
$rules = [
|
||||
'response' => 'required|string|min:5|max:1000',
|
||||
'crid' => 'required|integer|min:1',
|
||||
'acid' => 'required|integer|min:1'
|
||||
];
|
||||
if(config('instance.curated_registration.captcha_enabled') && $attempts >= 3) {
|
||||
$rules['h-captcha-response'] = 'required|captcha';
|
||||
$messages['h-captcha-response.required'] = 'The captcha must be filled';
|
||||
}
|
||||
$this->validate($request, $rules, $messages);
|
||||
$crid = $request->session()->get('cur-reg-con.cr-id');
|
||||
$acid = $request->session()->get('cur-reg-con.ac-id');
|
||||
abort_if((string) $crid !== $request->input('crid'), 404);
|
||||
abort_if((string) $acid !== $request->input('acid'), 404);
|
||||
|
||||
if(CuratedRegisterActivity::whereRegisterId($crid)->whereReplyToId($acid)->exists()) {
|
||||
return redirect()->back()->withErrors(['code' => 'You already replied to this request.']);
|
||||
}
|
||||
|
||||
$act = CuratedRegisterActivity::create([
|
||||
'register_id' => $crid,
|
||||
'reply_to_id' => $acid,
|
||||
'type' => 'user_response',
|
||||
'message' => $request->input('response'),
|
||||
'from_user' => true,
|
||||
'action_required' => true,
|
||||
]);
|
||||
|
||||
CuratedRegister::findOrFail($crid)->update(['user_has_responded' => true]);
|
||||
$request->session()->pull('cur-reg-con');
|
||||
$request->session()->pull('cur-reg-con-attempt');
|
||||
|
||||
return view('auth.curated-register.user_response_sent');
|
||||
}
|
||||
|
||||
public function conciergeStore(Request $request)
|
||||
{
|
||||
abort_if($request->user(), 404);
|
||||
$rules = [
|
||||
'sid' => 'required_if:action,email|integer|min:1|max:20000000',
|
||||
'id' => 'required_if:action,email|integer|min:1|max:20000000',
|
||||
'code' => 'required_if:action,email',
|
||||
'action' => 'required|string|in:email,message',
|
||||
'email' => 'required_if:action,email|email',
|
||||
'response' => 'required_if:action,message|string|min:20|max:1000',
|
||||
];
|
||||
$messages = [];
|
||||
if(config('instance.curated_registration.captcha_enabled')) {
|
||||
$rules['h-captcha-response'] = 'required|captcha';
|
||||
$messages['h-captcha-response.required'] = 'The captcha must be filled';
|
||||
}
|
||||
$this->validate($request, $rules, $messages);
|
||||
|
||||
$action = $request->input('action');
|
||||
$sid = $request->input('sid');
|
||||
$id = $request->input('id');
|
||||
$code = $request->input('code');
|
||||
$email = $request->input('email');
|
||||
|
||||
$cr = CuratedRegister::whereIsClosed(false)->findOrFail($sid);
|
||||
$ac = CuratedRegisterActivity::whereRegisterId($cr->id)->whereFromAdmin(true)->findOrFail($id);
|
||||
|
||||
if(!hash_equals($ac->secret_code, $code)) {
|
||||
return redirect()->back()->withErrors(['code' => 'Invalid code']);
|
||||
}
|
||||
|
||||
if(!hash_equals($cr->email, $email)) {
|
||||
return redirect()->back()->withErrors(['email' => 'Invalid email']);
|
||||
}
|
||||
|
||||
$request->session()->put('cur-reg-con.email-confirmed', true);
|
||||
$request->session()->put('cur-reg-con.cr-id', $cr->id);
|
||||
$request->session()->put('cur-reg-con.ac-id', $ac->id);
|
||||
$emailConfirmed = true;
|
||||
return redirect('/auth/sign_up/concierge/form');
|
||||
}
|
||||
|
||||
public function confirmEmail(Request $request)
|
||||
{
|
||||
if($request->user()) {
|
||||
return redirect(route('help.email-confirmation-issues'));
|
||||
}
|
||||
return view('auth.curated-register.confirm_email');
|
||||
}
|
||||
|
||||
public function emailConfirmed(Request $request)
|
||||
{
|
||||
if($request->user()) {
|
||||
return redirect(route('help.email-confirmation-issues'));
|
||||
}
|
||||
return view('auth.curated-register.email_confirmed');
|
||||
}
|
||||
|
||||
public function resendConfirmation(Request $request)
|
||||
{
|
||||
return view('auth.curated-register.resend-confirmation');
|
||||
}
|
||||
|
||||
public function resendConfirmationProcess(Request $request)
|
||||
{
|
||||
$rules = [
|
||||
'email' => [
|
||||
'required',
|
||||
'string',
|
||||
app()->environment() === 'production' ? 'email:rfc,dns,spoof' : 'email',
|
||||
'exists:curated_registers',
|
||||
]
|
||||
];
|
||||
|
||||
$messages = [];
|
||||
|
||||
if(config('instance.curated_registration.captcha_enabled')) {
|
||||
$rules['h-captcha-response'] = 'required|captcha';
|
||||
$messages['h-captcha-response.required'] = 'The captcha must be filled';
|
||||
}
|
||||
|
||||
$this->validate($request, $rules, $messages);
|
||||
|
||||
$cur = CuratedRegister::whereEmail($request->input('email'))->whereIsClosed(false)->first();
|
||||
if(!$cur) {
|
||||
return redirect()->back()->withErrors(['email' => 'The selected email is invalid.']);
|
||||
}
|
||||
|
||||
$totalCount = CuratedRegisterActivity::whereRegisterId($cur->id)
|
||||
->whereType('user_resend_email_confirmation')
|
||||
->count();
|
||||
|
||||
if($totalCount && $totalCount >= config('instance.curated_registration.resend_confirmation_limit')) {
|
||||
return redirect()->back()->withErrors(['email' => 'You have re-attempted too many times. To proceed with your application, please <a href="/site/contact" class="text-white" style="text-decoration: underline;">contact the admin team</a>.']);
|
||||
}
|
||||
|
||||
$count = CuratedRegisterActivity::whereRegisterId($cur->id)
|
||||
->whereType('user_resend_email_confirmation')
|
||||
->where('created_at', '>', now()->subHours(12))
|
||||
->count();
|
||||
|
||||
if($count) {
|
||||
return redirect()->back()->withErrors(['email' => 'You can only re-send the confirmation email once per 12 hours. Try again later.']);
|
||||
}
|
||||
|
||||
CuratedRegisterActivity::create([
|
||||
'register_id' => $cur->id,
|
||||
'type' => 'user_resend_email_confirmation',
|
||||
'admin_only_view' => true,
|
||||
'from_admin' => false,
|
||||
'from_user' => false,
|
||||
'action_required' => false,
|
||||
]);
|
||||
|
||||
Mail::to($cur->email)->send(new CuratedRegisterConfirmEmail($cur));
|
||||
return view('auth.curated-register.resent-confirmation');
|
||||
return $request->all();
|
||||
}
|
||||
|
||||
public function confirmEmailHandle(Request $request)
|
||||
{
|
||||
$rules = [
|
||||
'sid' => 'required',
|
||||
'code' => 'required'
|
||||
];
|
||||
$messages = [];
|
||||
if(config('instance.curated_registration.captcha_enabled')) {
|
||||
$rules['h-captcha-response'] = 'required|captcha';
|
||||
$messages['h-captcha-response.required'] = 'The captcha must be filled';
|
||||
}
|
||||
$this->validate($request, $rules, $messages);
|
||||
|
||||
$cr = CuratedRegister::whereNull('email_verified_at')
|
||||
->where('created_at', '>', now()->subHours(24))
|
||||
->find($request->input('sid'));
|
||||
if(!$cr) {
|
||||
return redirect(route('help.email-confirmation-issues'));
|
||||
}
|
||||
if(!hash_equals($cr->verify_code, $request->input('code'))) {
|
||||
return redirect(route('help.email-confirmation-issues'));
|
||||
}
|
||||
$cr->email_verified_at = now();
|
||||
$cr->save();
|
||||
|
||||
if(config('instance.curated_registration.notify.admin.on_verify_email.enabled')) {
|
||||
CuratedOnboardingNotifyAdminNewApplicationPipeline::dispatch($cr);
|
||||
}
|
||||
return view('auth.curated-register.email_confirmed');
|
||||
}
|
||||
|
||||
public function proceed(Request $request)
|
||||
{
|
||||
$this->validate($request, [
|
||||
'step' => 'required|integer|in:1,2,3,4'
|
||||
]);
|
||||
$step = $request->input('step');
|
||||
|
||||
switch($step) {
|
||||
case 1:
|
||||
$step = 2;
|
||||
$request->session()->put('cur-step', 1);
|
||||
return view('auth.curated-register.index', compact('step'));
|
||||
break;
|
||||
|
||||
case 2:
|
||||
$this->stepTwo($request);
|
||||
$step = 3;
|
||||
$request->session()->put('cur-step', 2);
|
||||
return view('auth.curated-register.index', compact('step'));
|
||||
break;
|
||||
|
||||
case 3:
|
||||
$this->stepThree($request);
|
||||
$step = 3;
|
||||
$request->session()->put('cur-step', 3);
|
||||
$verifiedEmail = true;
|
||||
$request->session()->pull('cur-reg');
|
||||
return view('auth.curated-register.index', compact('step', 'verifiedEmail'));
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
protected function stepTwo($request)
|
||||
{
|
||||
if($request->filled('reason')) {
|
||||
$request->session()->put('cur-reg.form-reason', $request->input('reason'));
|
||||
}
|
||||
if($request->filled('username')) {
|
||||
$request->session()->put('cur-reg.form-username', $request->input('username'));
|
||||
}
|
||||
if($request->filled('email')) {
|
||||
$request->session()->put('cur-reg.form-email', $request->input('email'));
|
||||
}
|
||||
$this->validate($request, [
|
||||
'username' => [
|
||||
'required',
|
||||
'min:2',
|
||||
'max:15',
|
||||
'unique:curated_registers',
|
||||
'unique:users',
|
||||
function ($attribute, $value, $fail) {
|
||||
$dash = substr_count($value, '-');
|
||||
$underscore = substr_count($value, '_');
|
||||
$period = substr_count($value, '.');
|
||||
|
||||
if(ends_with($value, ['.php', '.js', '.css'])) {
|
||||
return $fail('Username is invalid.');
|
||||
}
|
||||
|
||||
if(($dash + $underscore + $period) > 1) {
|
||||
return $fail('Username is invalid. Can only contain one dash (-), period (.) or underscore (_).');
|
||||
}
|
||||
|
||||
if (!ctype_alnum($value[0])) {
|
||||
return $fail('Username is invalid. Must start with a letter or number.');
|
||||
}
|
||||
|
||||
if (!ctype_alnum($value[strlen($value) - 1])) {
|
||||
return $fail('Username is invalid. Must end with a letter or number.');
|
||||
}
|
||||
|
||||
$val = str_replace(['_', '.', '-'], '', $value);
|
||||
if(!ctype_alnum($val)) {
|
||||
return $fail('Username is invalid. Username must be alpha-numeric and may contain dashes (-), periods (.) and underscores (_).');
|
||||
}
|
||||
|
||||
$restricted = RestrictedNames::get();
|
||||
if (in_array(strtolower($value), array_map('strtolower', $restricted))) {
|
||||
return $fail('Username cannot be used.');
|
||||
}
|
||||
},
|
||||
],
|
||||
'email' => [
|
||||
'required',
|
||||
'string',
|
||||
app()->environment() === 'production' ? 'email:rfc,dns,spoof' : 'email',
|
||||
'max:255',
|
||||
'unique:users',
|
||||
'unique:curated_registers',
|
||||
function ($attribute, $value, $fail) {
|
||||
$banned = EmailService::isBanned($value);
|
||||
if($banned) {
|
||||
return $fail('Email is invalid.');
|
||||
}
|
||||
},
|
||||
],
|
||||
'password' => 'required|min:8',
|
||||
'password_confirmation' => 'required|same:password',
|
||||
'reason' => 'required|min:20|max:1000',
|
||||
'agree' => 'required|accepted'
|
||||
]);
|
||||
$request->session()->put('cur-reg.form-email', $request->input('email'));
|
||||
$request->session()->put('cur-reg.form-password', $request->input('password'));
|
||||
}
|
||||
|
||||
protected function stepThree($request)
|
||||
{
|
||||
$this->validate($request, [
|
||||
'email' => [
|
||||
'required',
|
||||
'string',
|
||||
app()->environment() === 'production' ? 'email:rfc,dns,spoof' : 'email',
|
||||
'max:255',
|
||||
'unique:users',
|
||||
'unique:curated_registers',
|
||||
function ($attribute, $value, $fail) {
|
||||
$banned = EmailService::isBanned($value);
|
||||
if($banned) {
|
||||
return $fail('Email is invalid.');
|
||||
}
|
||||
},
|
||||
]
|
||||
]);
|
||||
$cr = new CuratedRegister;
|
||||
$cr->email = $request->email;
|
||||
$cr->username = $request->session()->get('cur-reg.form-username');
|
||||
$cr->password = bcrypt($request->session()->get('cur-reg.form-password'));
|
||||
$cr->ip_address = $request->ip();
|
||||
$cr->reason_to_join = $request->session()->get('cur-reg.form-reason');
|
||||
$cr->verify_code = Str::random(40);
|
||||
$cr->save();
|
||||
|
||||
Mail::to($cr->email)->send(new CuratedRegisterConfirmEmail($cr));
|
||||
}
|
||||
}
|
Plik diff jest za duży
Load Diff
|
@ -2,366 +2,422 @@
|
|||
|
||||
namespace App\Http\Controllers;
|
||||
|
||||
use App\{
|
||||
DiscoverCategory,
|
||||
Follower,
|
||||
Hashtag,
|
||||
HashtagFollow,
|
||||
Instance,
|
||||
Like,
|
||||
Profile,
|
||||
Status,
|
||||
StatusHashtag,
|
||||
UserFilter
|
||||
};
|
||||
use Auth, DB, Cache;
|
||||
use Illuminate\Http\Request;
|
||||
use App\Hashtag;
|
||||
use App\Instance;
|
||||
use App\Like;
|
||||
use App\Services\AccountService;
|
||||
use App\Services\AdminShadowFilterService;
|
||||
use App\Services\BookmarkService;
|
||||
use App\Services\ConfigCacheService;
|
||||
use App\Services\FollowerService;
|
||||
use App\Services\HashtagService;
|
||||
use App\Services\LikeService;
|
||||
use App\Services\ReblogService;
|
||||
use App\Services\StatusHashtagService;
|
||||
use App\Services\SnowflakeService;
|
||||
use App\Services\StatusHashtagService;
|
||||
use App\Services\StatusService;
|
||||
use App\Services\TrendingHashtagService;
|
||||
use App\Services\UserFilterService;
|
||||
use App\Status;
|
||||
use Auth;
|
||||
use Cache;
|
||||
use DB;
|
||||
use Illuminate\Http\Request;
|
||||
|
||||
class DiscoverController extends Controller
|
||||
{
|
||||
public function home(Request $request)
|
||||
{
|
||||
abort_if(!Auth::check() && config('instance.discover.public') == false, 403);
|
||||
return view('discover.home');
|
||||
}
|
||||
public function home(Request $request)
|
||||
{
|
||||
abort_if(! Auth::check() && config('instance.discover.public') == false, 403);
|
||||
|
||||
public function showTags(Request $request, $hashtag)
|
||||
{
|
||||
abort_if(!config('instance.discover.tags.is_public') && !Auth::check(), 403);
|
||||
return view('discover.home');
|
||||
}
|
||||
|
||||
$tag = Hashtag::whereName($hashtag)
|
||||
->orWhere('slug', $hashtag)
|
||||
->where('is_banned', '!=', true)
|
||||
->firstOrFail();
|
||||
$tagCount = StatusHashtagService::count($tag->id);
|
||||
return view('discover.tags.show', compact('tag', 'tagCount'));
|
||||
}
|
||||
public function showTags(Request $request, $hashtag)
|
||||
{
|
||||
if ($request->user()) {
|
||||
return redirect('/i/web/hashtag/'.$hashtag.'?src=pd');
|
||||
}
|
||||
abort_if(! config('instance.discover.tags.is_public') && ! Auth::check(), 403);
|
||||
|
||||
public function getHashtags(Request $request)
|
||||
{
|
||||
$user = $request->user();
|
||||
abort_if(!config('instance.discover.tags.is_public') && !$user, 403);
|
||||
$tag = Hashtag::whereName($hashtag)
|
||||
->orWhere('slug', $hashtag)
|
||||
->where('is_banned', '!=', true)
|
||||
->firstOrFail();
|
||||
$tagCount = $tag->cached_count ?? 0;
|
||||
|
||||
$this->validate($request, [
|
||||
'hashtag' => 'required|string|min:1|max:124',
|
||||
'page' => 'nullable|integer|min:1|max:' . ($user ? 29 : 3)
|
||||
]);
|
||||
return view('discover.tags.show', compact('tag', 'tagCount'));
|
||||
}
|
||||
|
||||
$page = $request->input('page') ?? '1';
|
||||
$end = $page > 1 ? $page * 9 : 0;
|
||||
$tag = $request->input('hashtag');
|
||||
public function getHashtags(Request $request)
|
||||
{
|
||||
$user = $request->user();
|
||||
abort_if(! config('instance.discover.tags.is_public') && ! $user, 403);
|
||||
|
||||
if(config('database.default') === 'pgsql') {
|
||||
$hashtag = Hashtag::where('name', 'ilike', $tag)->firstOrFail();
|
||||
} else {
|
||||
$hashtag = Hashtag::whereName($tag)->firstOrFail();
|
||||
}
|
||||
$this->validate($request, [
|
||||
'hashtag' => 'required|string|min:1|max:124',
|
||||
'page' => 'nullable|integer|min:1|max:'.($user ? 29 : 3),
|
||||
]);
|
||||
|
||||
if($hashtag->is_banned == true) {
|
||||
return [];
|
||||
}
|
||||
if($user) {
|
||||
$res['follows'] = HashtagService::isFollowing($user->profile_id, $hashtag->id);
|
||||
}
|
||||
$res['hashtag'] = [
|
||||
'name' => $hashtag->name,
|
||||
'url' => $hashtag->url()
|
||||
];
|
||||
if($user) {
|
||||
$tags = StatusHashtagService::get($hashtag->id, $page, $end);
|
||||
$res['tags'] = collect($tags)
|
||||
->map(function($tag) use($user) {
|
||||
$tag['status']['favourited'] = (bool) LikeService::liked($user->profile_id, $tag['status']['id']);
|
||||
$tag['status']['reblogged'] = (bool) ReblogService::get($user->profile_id, $tag['status']['id']);
|
||||
$tag['status']['bookmarked'] = (bool) BookmarkService::get($user->profile_id, $tag['status']['id']);
|
||||
return $tag;
|
||||
})
|
||||
->filter(function($tag) {
|
||||
if(!StatusService::get($tag['status']['id'])) {
|
||||
return false;
|
||||
}
|
||||
return true;
|
||||
})
|
||||
->values();
|
||||
} else {
|
||||
if($page != 1) {
|
||||
$res['tags'] = [];
|
||||
return $res;
|
||||
}
|
||||
$key = 'discover:tags:public_feed:' . $hashtag->id . ':page:' . $page;
|
||||
$tags = Cache::remember($key, 43200, function() use($hashtag, $page, $end) {
|
||||
return collect(StatusHashtagService::get($hashtag->id, $page, $end))
|
||||
->filter(function($tag) {
|
||||
if(!$tag['status']['local']) {
|
||||
return false;
|
||||
}
|
||||
return true;
|
||||
})
|
||||
->values();
|
||||
});
|
||||
$res['tags'] = collect($tags)
|
||||
->filter(function($tag) {
|
||||
if(!StatusService::get($tag['status']['id'])) {
|
||||
return false;
|
||||
}
|
||||
return true;
|
||||
})
|
||||
->values();
|
||||
}
|
||||
return $res;
|
||||
}
|
||||
$page = $request->input('page') ?? '1';
|
||||
$end = $page > 1 ? $page * 9 : 0;
|
||||
$tag = $request->input('hashtag');
|
||||
|
||||
public function profilesDirectory(Request $request)
|
||||
{
|
||||
return redirect('/')->with('statusRedirect', 'The Profile Directory is unavailable at this time.');
|
||||
}
|
||||
if (config('database.default') === 'pgsql') {
|
||||
$hashtag = Hashtag::where('name', 'ilike', $tag)->firstOrFail();
|
||||
} else {
|
||||
$hashtag = Hashtag::whereName($tag)->firstOrFail();
|
||||
}
|
||||
|
||||
public function profilesDirectoryApi(Request $request)
|
||||
{
|
||||
return ['error' => 'Temporarily unavailable.'];
|
||||
}
|
||||
if ($hashtag->is_banned == true) {
|
||||
return [];
|
||||
}
|
||||
if ($user) {
|
||||
$res['follows'] = HashtagService::isFollowing($user->profile_id, $hashtag->id);
|
||||
}
|
||||
$res['hashtag'] = [
|
||||
'name' => $hashtag->name,
|
||||
'url' => $hashtag->url(),
|
||||
];
|
||||
if ($user) {
|
||||
$tags = StatusHashtagService::get($hashtag->id, $page, $end);
|
||||
$res['tags'] = collect($tags)
|
||||
->map(function ($tag) use ($user) {
|
||||
$tag['status']['favourited'] = (bool) LikeService::liked($user->profile_id, $tag['status']['id']);
|
||||
$tag['status']['reblogged'] = (bool) ReblogService::get($user->profile_id, $tag['status']['id']);
|
||||
$tag['status']['bookmarked'] = (bool) BookmarkService::get($user->profile_id, $tag['status']['id']);
|
||||
|
||||
public function trendingApi(Request $request)
|
||||
{
|
||||
abort_if(config('instance.discover.public') == false && !$request->user(), 403);
|
||||
return $tag;
|
||||
})
|
||||
->filter(function ($tag) {
|
||||
if (! StatusService::get($tag['status']['id'])) {
|
||||
return false;
|
||||
}
|
||||
|
||||
$this->validate($request, [
|
||||
'range' => 'nullable|string|in:daily,monthly,yearly',
|
||||
]);
|
||||
return true;
|
||||
})
|
||||
->values();
|
||||
} else {
|
||||
if ($page != 1) {
|
||||
$res['tags'] = [];
|
||||
|
||||
$range = $request->input('range');
|
||||
$days = $range == 'monthly' ? 31 : ($range == 'daily' ? 1 : 365);
|
||||
$ttls = [
|
||||
1 => 1500,
|
||||
31 => 14400,
|
||||
365 => 86400
|
||||
];
|
||||
$key = ':api:discover:trending:v2.12:range:' . $days;
|
||||
return $res;
|
||||
}
|
||||
$key = 'discover:tags:public_feed:'.$hashtag->id.':page:'.$page;
|
||||
$tags = Cache::remember($key, 43200, function () use ($hashtag, $page, $end) {
|
||||
return collect(StatusHashtagService::get($hashtag->id, $page, $end))
|
||||
->filter(function ($tag) {
|
||||
if (! $tag['status']['local']) {
|
||||
return false;
|
||||
}
|
||||
|
||||
$ids = Cache::remember($key, $ttls[$days], function() use($days) {
|
||||
$min_id = SnowflakeService::byDate(now()->subDays($days));
|
||||
return DB::table('statuses')
|
||||
->select(
|
||||
'id',
|
||||
'scope',
|
||||
'type',
|
||||
'is_nsfw',
|
||||
'likes_count',
|
||||
'created_at'
|
||||
)
|
||||
->where('id', '>', $min_id)
|
||||
->whereNull('uri')
|
||||
->whereScope('public')
|
||||
->whereIn('type', [
|
||||
'photo',
|
||||
'photo:album',
|
||||
'video'
|
||||
])
|
||||
->whereIsNsfw(false)
|
||||
->orderBy('likes_count','desc')
|
||||
->take(30)
|
||||
->pluck('id');
|
||||
});
|
||||
return true;
|
||||
})
|
||||
->values();
|
||||
});
|
||||
$res['tags'] = collect($tags)
|
||||
->filter(function ($tag) {
|
||||
if (! StatusService::get($tag['status']['id'])) {
|
||||
return false;
|
||||
}
|
||||
|
||||
$filtered = Auth::check() ? UserFilterService::filters(Auth::user()->profile_id) : [];
|
||||
return true;
|
||||
})
|
||||
->values();
|
||||
}
|
||||
|
||||
$res = $ids->map(function($s) {
|
||||
return StatusService::get($s);
|
||||
})->filter(function($s) use($filtered) {
|
||||
return
|
||||
$s &&
|
||||
!in_array($s['account']['id'], $filtered) &&
|
||||
isset($s['account']);
|
||||
})->values();
|
||||
return $res;
|
||||
}
|
||||
|
||||
return response()->json($res);
|
||||
}
|
||||
public function profilesDirectory(Request $request)
|
||||
{
|
||||
return redirect('/')->with('statusRedirect', 'The Profile Directory is unavailable at this time.');
|
||||
}
|
||||
|
||||
public function trendingHashtags(Request $request)
|
||||
{
|
||||
abort_if(!$request->user(), 403);
|
||||
public function profilesDirectoryApi(Request $request)
|
||||
{
|
||||
return ['error' => 'Temporarily unavailable.'];
|
||||
}
|
||||
|
||||
$res = TrendingHashtagService::getTrending();
|
||||
return $res;
|
||||
}
|
||||
public function trendingApi(Request $request)
|
||||
{
|
||||
abort_if(config('instance.discover.public') == false && ! $request->user(), 403);
|
||||
|
||||
public function trendingPlaces(Request $request)
|
||||
{
|
||||
return [];
|
||||
}
|
||||
$this->validate($request, [
|
||||
'range' => 'nullable|string|in:daily,monthly,yearly',
|
||||
]);
|
||||
|
||||
public function myMemories(Request $request)
|
||||
{
|
||||
abort_if(!$request->user(), 404);
|
||||
$pid = $request->user()->profile_id;
|
||||
abort_if(!$this->config()['memories']['enabled'], 404);
|
||||
$type = $request->input('type') ?? 'posts';
|
||||
$range = $request->input('range');
|
||||
$days = $range == 'monthly' ? 31 : ($range == 'daily' ? 1 : 365);
|
||||
$ttls = [
|
||||
1 => 1500,
|
||||
31 => 14400,
|
||||
365 => 86400,
|
||||
];
|
||||
$key = ':api:discover:trending:v2.12:range:'.$days;
|
||||
|
||||
switch($type) {
|
||||
case 'posts':
|
||||
$res = Status::whereProfileId($pid)
|
||||
->whereDay('created_at', date('d'))
|
||||
->whereMonth('created_at', date('m'))
|
||||
->whereYear('created_at', '!=', date('Y'))
|
||||
->whereNull(['reblog_of_id', 'in_reply_to_id'])
|
||||
->limit(20)
|
||||
->pluck('id')
|
||||
->map(function($id) {
|
||||
return StatusService::get($id, false);
|
||||
})
|
||||
->filter(function($post) {
|
||||
return $post && isset($post['account']);
|
||||
})
|
||||
->values();
|
||||
break;
|
||||
$ids = Cache::remember($key, $ttls[$days], function () use ($days) {
|
||||
$min_id = SnowflakeService::byDate(now()->subDays($days));
|
||||
|
||||
case 'liked':
|
||||
$res = Like::whereProfileId($pid)
|
||||
->whereDay('created_at', date('d'))
|
||||
->whereMonth('created_at', date('m'))
|
||||
->whereYear('created_at', '!=', date('Y'))
|
||||
->orderByDesc('status_id')
|
||||
->limit(20)
|
||||
->pluck('status_id')
|
||||
->map(function($id) {
|
||||
$status = StatusService::get($id, false);
|
||||
$status['favourited'] = true;
|
||||
return $status;
|
||||
})
|
||||
->filter(function($post) {
|
||||
return $post && isset($post['account']);
|
||||
})
|
||||
->values();
|
||||
break;
|
||||
}
|
||||
return DB::table('statuses')
|
||||
->select(
|
||||
'id',
|
||||
'scope',
|
||||
'type',
|
||||
'is_nsfw',
|
||||
'likes_count',
|
||||
'created_at'
|
||||
)
|
||||
->where('id', '>', $min_id)
|
||||
->whereNull('uri')
|
||||
->whereScope('public')
|
||||
->whereIn('type', [
|
||||
'photo',
|
||||
'photo:album',
|
||||
'video',
|
||||
])
|
||||
->whereIsNsfw(false)
|
||||
->orderBy('likes_count', 'desc')
|
||||
->take(30)
|
||||
->pluck('id');
|
||||
});
|
||||
|
||||
return $res;
|
||||
}
|
||||
$filtered = Auth::check() ? UserFilterService::filters(Auth::user()->profile_id) : [];
|
||||
|
||||
public function accountInsightsPopularPosts(Request $request)
|
||||
{
|
||||
abort_if(!$request->user(), 404);
|
||||
$pid = $request->user()->profile_id;
|
||||
abort_if(!$this->config()['insights']['enabled'], 404);
|
||||
$posts = Cache::remember('pf:discover:metro2:accinsights:popular:' . $pid, 43200, function() use ($pid) {
|
||||
return Status::whereProfileId($pid)
|
||||
->whereNotNull('likes_count')
|
||||
->orderByDesc('likes_count')
|
||||
->limit(12)
|
||||
->pluck('id')
|
||||
->map(function($id) {
|
||||
return StatusService::get($id, false);
|
||||
})
|
||||
->filter(function($post) {
|
||||
return $post && isset($post['account']);
|
||||
})
|
||||
->values();
|
||||
});
|
||||
$res = $ids->map(function ($s) {
|
||||
return StatusService::get($s);
|
||||
})->filter(function ($s) use ($filtered) {
|
||||
return
|
||||
$s &&
|
||||
! in_array($s['account']['id'], $filtered) &&
|
||||
isset($s['account']);
|
||||
})->values();
|
||||
|
||||
return $posts;
|
||||
}
|
||||
return response()->json($res);
|
||||
}
|
||||
|
||||
public function config()
|
||||
{
|
||||
$cc = ConfigCacheService::get('config.discover.features');
|
||||
if($cc) {
|
||||
return is_string($cc) ? json_decode($cc, true) : $cc;
|
||||
}
|
||||
return [
|
||||
'hashtags' => [
|
||||
'enabled' => false,
|
||||
],
|
||||
'memories' => [
|
||||
'enabled' => false,
|
||||
],
|
||||
'insights' => [
|
||||
'enabled' => false,
|
||||
],
|
||||
'friends' => [
|
||||
'enabled' => false,
|
||||
],
|
||||
'server' => [
|
||||
'enabled' => false,
|
||||
'mode' => 'allowlist',
|
||||
'domains' => []
|
||||
]
|
||||
];
|
||||
}
|
||||
public function trendingHashtags(Request $request)
|
||||
{
|
||||
abort_if(! $request->user(), 403);
|
||||
|
||||
public function serverTimeline(Request $request)
|
||||
{
|
||||
abort_if(!$request->user(), 404);
|
||||
abort_if(!$this->config()['server']['enabled'], 404);
|
||||
$pid = $request->user()->profile_id;
|
||||
$domain = $request->input('domain');
|
||||
$config = $this->config();
|
||||
$domains = explode(',', $config['server']['domains']);
|
||||
abort_unless(in_array($domain, $domains), 400);
|
||||
$res = TrendingHashtagService::getTrending();
|
||||
|
||||
$res = Status::whereNotNull('uri')
|
||||
->where('uri', 'like', 'https://' . $domain . '%')
|
||||
->whereNull(['in_reply_to_id', 'reblog_of_id'])
|
||||
->orderByDesc('id')
|
||||
->limit(12)
|
||||
->pluck('id')
|
||||
->map(function($id) {
|
||||
return StatusService::get($id);
|
||||
})
|
||||
->filter(function($post) {
|
||||
return $post && isset($post['account']);
|
||||
})
|
||||
->values();
|
||||
return $res;
|
||||
}
|
||||
return $res;
|
||||
}
|
||||
|
||||
public function enabledFeatures(Request $request)
|
||||
{
|
||||
abort_if(!$request->user(), 404);
|
||||
return $this->config();
|
||||
}
|
||||
public function trendingPlaces(Request $request)
|
||||
{
|
||||
return [];
|
||||
}
|
||||
|
||||
public function updateFeatures(Request $request)
|
||||
{
|
||||
abort_if(!$request->user(), 404);
|
||||
abort_if(!$request->user()->is_admin, 404);
|
||||
$pid = $request->user()->profile_id;
|
||||
$this->validate($request, [
|
||||
'features.friends.enabled' => 'boolean',
|
||||
'features.hashtags.enabled' => 'boolean',
|
||||
'features.insights.enabled' => 'boolean',
|
||||
'features.memories.enabled' => 'boolean',
|
||||
'features.server.enabled' => 'boolean',
|
||||
]);
|
||||
$res = $request->input('features');
|
||||
if($res['server'] && isset($res['server']['domains']) && !empty($res['server']['domains'])) {
|
||||
$parts = explode(',', $res['server']['domains']);
|
||||
$parts = array_filter($parts, function($v) {
|
||||
$len = strlen($v);
|
||||
$pos = strpos($v, '.');
|
||||
$domain = trim($v);
|
||||
if($pos == false || $pos == ($len + 1)) {
|
||||
return false;
|
||||
}
|
||||
if(!Instance::whereDomain($domain)->exists()) {
|
||||
return false;
|
||||
}
|
||||
return true;
|
||||
});
|
||||
$parts = array_slice($parts, 0, 10);
|
||||
$d = implode(',', array_map('trim', $parts));
|
||||
$res['server']['domains'] = $d;
|
||||
}
|
||||
ConfigCacheService::put('config.discover.features', json_encode($res));
|
||||
return $res;
|
||||
}
|
||||
public function myMemories(Request $request)
|
||||
{
|
||||
abort_if(! $request->user(), 404);
|
||||
$pid = $request->user()->profile_id;
|
||||
abort_if(! $this->config()['memories']['enabled'], 404);
|
||||
$type = $request->input('type') ?? 'posts';
|
||||
|
||||
switch ($type) {
|
||||
case 'posts':
|
||||
$res = Status::whereProfileId($pid)
|
||||
->whereDay('created_at', date('d'))
|
||||
->whereMonth('created_at', date('m'))
|
||||
->whereYear('created_at', '!=', date('Y'))
|
||||
->whereNull(['reblog_of_id', 'in_reply_to_id'])
|
||||
->limit(20)
|
||||
->pluck('id')
|
||||
->map(function ($id) {
|
||||
return StatusService::get($id, false);
|
||||
})
|
||||
->filter(function ($post) {
|
||||
return $post && isset($post['account']);
|
||||
})
|
||||
->values();
|
||||
break;
|
||||
|
||||
case 'liked':
|
||||
$res = Like::whereProfileId($pid)
|
||||
->whereDay('created_at', date('d'))
|
||||
->whereMonth('created_at', date('m'))
|
||||
->whereYear('created_at', '!=', date('Y'))
|
||||
->orderByDesc('status_id')
|
||||
->limit(20)
|
||||
->pluck('status_id')
|
||||
->map(function ($id) {
|
||||
$status = StatusService::get($id, false);
|
||||
$status['favourited'] = true;
|
||||
|
||||
return $status;
|
||||
})
|
||||
->filter(function ($post) {
|
||||
return $post && isset($post['account']);
|
||||
})
|
||||
->values();
|
||||
break;
|
||||
}
|
||||
|
||||
return $res;
|
||||
}
|
||||
|
||||
public function accountInsightsPopularPosts(Request $request)
|
||||
{
|
||||
abort_if(! $request->user(), 404);
|
||||
$pid = $request->user()->profile_id;
|
||||
abort_if(! $this->config()['insights']['enabled'], 404);
|
||||
$posts = Cache::remember('pf:discover:metro2:accinsights:popular:'.$pid, 43200, function () use ($pid) {
|
||||
return Status::whereProfileId($pid)
|
||||
->whereNotNull('likes_count')
|
||||
->orderByDesc('likes_count')
|
||||
->limit(12)
|
||||
->pluck('id')
|
||||
->map(function ($id) {
|
||||
return StatusService::get($id, false);
|
||||
})
|
||||
->filter(function ($post) {
|
||||
return $post && isset($post['account']);
|
||||
})
|
||||
->values();
|
||||
});
|
||||
|
||||
return $posts;
|
||||
}
|
||||
|
||||
public function config()
|
||||
{
|
||||
$cc = ConfigCacheService::get('config.discover.features');
|
||||
if ($cc) {
|
||||
return is_string($cc) ? json_decode($cc, true) : $cc;
|
||||
}
|
||||
|
||||
return [
|
||||
'hashtags' => [
|
||||
'enabled' => false,
|
||||
],
|
||||
'memories' => [
|
||||
'enabled' => false,
|
||||
],
|
||||
'insights' => [
|
||||
'enabled' => false,
|
||||
],
|
||||
'friends' => [
|
||||
'enabled' => false,
|
||||
],
|
||||
'server' => [
|
||||
'enabled' => false,
|
||||
'mode' => 'allowlist',
|
||||
'domains' => [],
|
||||
],
|
||||
];
|
||||
}
|
||||
|
||||
public function serverTimeline(Request $request)
|
||||
{
|
||||
abort_if(! $request->user(), 404);
|
||||
abort_if(! $this->config()['server']['enabled'], 404);
|
||||
$pid = $request->user()->profile_id;
|
||||
$domain = $request->input('domain');
|
||||
$config = $this->config();
|
||||
$domains = explode(',', $config['server']['domains']);
|
||||
abort_unless(in_array($domain, $domains), 400);
|
||||
|
||||
$res = Status::whereNotNull('uri')
|
||||
->where('uri', 'like', 'https://'.$domain.'%')
|
||||
->whereNull(['in_reply_to_id', 'reblog_of_id'])
|
||||
->orderByDesc('id')
|
||||
->limit(12)
|
||||
->pluck('id')
|
||||
->map(function ($id) {
|
||||
return StatusService::get($id);
|
||||
})
|
||||
->filter(function ($post) {
|
||||
return $post && isset($post['account']);
|
||||
})
|
||||
->values();
|
||||
|
||||
return $res;
|
||||
}
|
||||
|
||||
public function enabledFeatures(Request $request)
|
||||
{
|
||||
abort_if(! $request->user(), 404);
|
||||
|
||||
return $this->config();
|
||||
}
|
||||
|
||||
public function updateFeatures(Request $request)
|
||||
{
|
||||
abort_if(! $request->user(), 404);
|
||||
abort_if(! $request->user()->is_admin, 404);
|
||||
$pid = $request->user()->profile_id;
|
||||
$this->validate($request, [
|
||||
'features.friends.enabled' => 'boolean',
|
||||
'features.hashtags.enabled' => 'boolean',
|
||||
'features.insights.enabled' => 'boolean',
|
||||
'features.memories.enabled' => 'boolean',
|
||||
'features.server.enabled' => 'boolean',
|
||||
]);
|
||||
$res = $request->input('features');
|
||||
if ($res['server'] && isset($res['server']['domains']) && ! empty($res['server']['domains'])) {
|
||||
$parts = explode(',', $res['server']['domains']);
|
||||
$parts = array_filter($parts, function ($v) {
|
||||
$len = strlen($v);
|
||||
$pos = strpos($v, '.');
|
||||
$domain = trim($v);
|
||||
if ($pos == false || $pos == ($len + 1)) {
|
||||
return false;
|
||||
}
|
||||
if (! Instance::whereDomain($domain)->exists()) {
|
||||
return false;
|
||||
}
|
||||
|
||||
return true;
|
||||
});
|
||||
$parts = array_slice($parts, 0, 10);
|
||||
$d = implode(',', array_map('trim', $parts));
|
||||
$res['server']['domains'] = $d;
|
||||
}
|
||||
ConfigCacheService::put('config.discover.features', json_encode($res));
|
||||
|
||||
return $res;
|
||||
}
|
||||
|
||||
public function discoverAccountsPopular(Request $request)
|
||||
{
|
||||
abort_if(! $request->user(), 403);
|
||||
|
||||
$pid = $request->user()->profile_id;
|
||||
|
||||
$ids = Cache::remember('api:v1.1:discover:accounts:popular', 14400, function () {
|
||||
return DB::table('profiles')
|
||||
->where('is_private', false)
|
||||
->whereNull('status')
|
||||
->orderByDesc('profiles.followers_count')
|
||||
->limit(30)
|
||||
->get();
|
||||
});
|
||||
$filters = UserFilterService::filters($pid);
|
||||
$asf = AdminShadowFilterService::getHideFromPublicFeedsList();
|
||||
$ids = $ids->map(function ($profile) {
|
||||
return AccountService::get($profile->id, true);
|
||||
})
|
||||
->filter(function ($profile) {
|
||||
return $profile && isset($profile['id'], $profile['locked']) && ! $profile['locked'];
|
||||
})
|
||||
->filter(function ($profile) use ($pid) {
|
||||
return $profile['id'] != $pid;
|
||||
})
|
||||
->filter(function ($profile) use ($pid) {
|
||||
return ! FollowerService::follows($pid, $profile['id'], true);
|
||||
})
|
||||
->filter(function ($profile) use ($asf) {
|
||||
return ! in_array($profile['id'], $asf);
|
||||
})
|
||||
->filter(function ($profile) use ($filters) {
|
||||
return ! in_array($profile['id'], $filters);
|
||||
})
|
||||
->take(16)
|
||||
->values();
|
||||
|
||||
return response()->json($ids, 200, [], JSON_UNESCAPED_SLASHES);
|
||||
}
|
||||
}
|
||||
|
|
|
@ -2,265 +2,269 @@
|
|||
|
||||
namespace App\Http\Controllers;
|
||||
|
||||
use App\Jobs\InboxPipeline\{
|
||||
DeleteWorker,
|
||||
InboxWorker,
|
||||
InboxValidator
|
||||
};
|
||||
use App\Jobs\RemoteFollowPipeline\RemoteFollowPipeline;
|
||||
use App\{
|
||||
AccountLog,
|
||||
Like,
|
||||
Profile,
|
||||
Status,
|
||||
User
|
||||
};
|
||||
use App\Util\Lexer\Nickname;
|
||||
use App\Util\Webfinger\Webfinger;
|
||||
use Auth;
|
||||
use Cache;
|
||||
use Carbon\Carbon;
|
||||
use Illuminate\Http\Request;
|
||||
use League\Fractal;
|
||||
use App\Util\Site\Nodeinfo;
|
||||
use App\Util\ActivityPub\{
|
||||
Helpers,
|
||||
HttpSignature,
|
||||
Outbox
|
||||
};
|
||||
use Zttp\Zttp;
|
||||
use App\Jobs\InboxPipeline\DeleteWorker;
|
||||
use App\Jobs\InboxPipeline\InboxValidator;
|
||||
use App\Jobs\InboxPipeline\InboxWorker;
|
||||
use App\Profile;
|
||||
use App\Services\AccountService;
|
||||
use App\Services\InstanceService;
|
||||
use App\Status;
|
||||
use App\Util\Lexer\Nickname;
|
||||
use App\Util\Site\Nodeinfo;
|
||||
use App\Util\Webfinger\Webfinger;
|
||||
use Cache;
|
||||
use Illuminate\Http\Request;
|
||||
|
||||
class FederationController extends Controller
|
||||
{
|
||||
public function nodeinfoWellKnown()
|
||||
{
|
||||
abort_if(!config('federation.nodeinfo.enabled'), 404);
|
||||
return response()->json(Nodeinfo::wellKnown(), 200, [], JSON_UNESCAPED_SLASHES)
|
||||
->header('Access-Control-Allow-Origin','*');
|
||||
}
|
||||
public function nodeinfoWellKnown()
|
||||
{
|
||||
abort_if(! config('federation.nodeinfo.enabled'), 404);
|
||||
|
||||
public function nodeinfo()
|
||||
{
|
||||
abort_if(!config('federation.nodeinfo.enabled'), 404);
|
||||
return response()->json(Nodeinfo::get(), 200, [], JSON_UNESCAPED_SLASHES)
|
||||
->header('Access-Control-Allow-Origin','*');
|
||||
}
|
||||
return response()->json(Nodeinfo::wellKnown(), 200, [], JSON_UNESCAPED_SLASHES)
|
||||
->header('Access-Control-Allow-Origin', '*');
|
||||
}
|
||||
|
||||
public function webfinger(Request $request)
|
||||
{
|
||||
if (!config('federation.webfinger.enabled') ||
|
||||
!$request->has('resource') ||
|
||||
!$request->filled('resource')
|
||||
) {
|
||||
return response('', 400);
|
||||
}
|
||||
public function nodeinfo()
|
||||
{
|
||||
abort_if(! config('federation.nodeinfo.enabled'), 404);
|
||||
|
||||
$resource = $request->input('resource');
|
||||
$domain = config('pixelfed.domain.app');
|
||||
return response()->json(Nodeinfo::get(), 200, [], JSON_UNESCAPED_SLASHES)
|
||||
->header('Access-Control-Allow-Origin', '*');
|
||||
}
|
||||
|
||||
if(config('federation.activitypub.sharedInbox') &&
|
||||
$resource == 'acct:' . $domain . '@' . $domain) {
|
||||
$res = [
|
||||
'subject' => 'acct:' . $domain . '@' . $domain,
|
||||
'aliases' => [
|
||||
'https://' . $domain . '/i/actor'
|
||||
],
|
||||
'links' => [
|
||||
[
|
||||
'rel' => 'http://webfinger.net/rel/profile-page',
|
||||
'type' => 'text/html',
|
||||
'href' => 'https://' . $domain . '/site/kb/instance-actor'
|
||||
],
|
||||
[
|
||||
'rel' => 'self',
|
||||
'type' => 'application/activity+json',
|
||||
'href' => 'https://' . $domain . '/i/actor'
|
||||
]
|
||||
]
|
||||
];
|
||||
return response()->json($res, 200, [], JSON_UNESCAPED_SLASHES);
|
||||
}
|
||||
$hash = hash('sha256', $resource);
|
||||
$key = 'federation:webfinger:sha256:' . $hash;
|
||||
if($cached = Cache::get($key)) {
|
||||
return response()->json($cached, 200, [], JSON_UNESCAPED_SLASHES);
|
||||
}
|
||||
if(strpos($resource, $domain) == false) {
|
||||
return response('', 400);
|
||||
}
|
||||
$parsed = Nickname::normalizeProfileUrl($resource);
|
||||
if(empty($parsed) || $parsed['domain'] !== $domain) {
|
||||
return response('', 400);
|
||||
}
|
||||
$username = $parsed['username'];
|
||||
$profile = Profile::whereNull('domain')->whereUsername($username)->first();
|
||||
if(!$profile || $profile->status !== null) {
|
||||
return response('', 400);
|
||||
}
|
||||
$webfinger = (new Webfinger($profile))->generate();
|
||||
Cache::put($key, $webfinger, 1209600);
|
||||
public function webfinger(Request $request)
|
||||
{
|
||||
if (! config('federation.webfinger.enabled') ||
|
||||
! $request->has('resource') ||
|
||||
! $request->filled('resource')
|
||||
) {
|
||||
return response('', 400);
|
||||
}
|
||||
|
||||
return response()->json($webfinger, 200, [], JSON_UNESCAPED_SLASHES)
|
||||
->header('Access-Control-Allow-Origin','*');
|
||||
}
|
||||
$resource = $request->input('resource');
|
||||
$domain = config('pixelfed.domain.app');
|
||||
|
||||
public function hostMeta(Request $request)
|
||||
{
|
||||
abort_if(!config('federation.webfinger.enabled'), 404);
|
||||
if (config('federation.activitypub.sharedInbox') &&
|
||||
$resource == 'acct:'.$domain.'@'.$domain) {
|
||||
$res = [
|
||||
'subject' => 'acct:'.$domain.'@'.$domain,
|
||||
'aliases' => [
|
||||
'https://'.$domain.'/i/actor',
|
||||
],
|
||||
'links' => [
|
||||
[
|
||||
'rel' => 'http://webfinger.net/rel/profile-page',
|
||||
'type' => 'text/html',
|
||||
'href' => 'https://'.$domain.'/site/kb/instance-actor',
|
||||
],
|
||||
[
|
||||
'rel' => 'self',
|
||||
'type' => 'application/activity+json',
|
||||
'href' => 'https://'.$domain.'/i/actor',
|
||||
],
|
||||
],
|
||||
];
|
||||
|
||||
$path = route('well-known.webfinger');
|
||||
$xml = '<?xml version="1.0" encoding="UTF-8"?><XRD xmlns="http://docs.oasis-open.org/ns/xri/xrd-1.0"><Link rel="lrdd" type="application/xrd+xml" template="'.$path.'?resource={uri}"/></XRD>';
|
||||
return response()->json($res, 200, [], JSON_UNESCAPED_SLASHES);
|
||||
}
|
||||
$hash = hash('sha256', $resource);
|
||||
$key = 'federation:webfinger:sha256:'.$hash;
|
||||
if ($cached = Cache::get($key)) {
|
||||
return response()->json($cached, 200, [], JSON_UNESCAPED_SLASHES);
|
||||
}
|
||||
if (strpos($resource, $domain) == false) {
|
||||
return response('', 400);
|
||||
}
|
||||
$parsed = Nickname::normalizeProfileUrl($resource);
|
||||
if (empty($parsed) || $parsed['domain'] !== $domain) {
|
||||
return response('', 400);
|
||||
}
|
||||
$username = $parsed['username'];
|
||||
$profile = Profile::whereNull('domain')->whereUsername($username)->first();
|
||||
if (! $profile || $profile->status !== null) {
|
||||
return response('', 400);
|
||||
}
|
||||
$webfinger = (new Webfinger($profile))->generate();
|
||||
Cache::put($key, $webfinger, 1209600);
|
||||
|
||||
return response($xml)->header('Content-Type', 'application/xrd+xml');
|
||||
}
|
||||
return response()->json($webfinger, 200, [], JSON_UNESCAPED_SLASHES)
|
||||
->header('Access-Control-Allow-Origin', '*');
|
||||
}
|
||||
|
||||
public function userOutbox(Request $request, $username)
|
||||
{
|
||||
abort_if(!config_cache('federation.activitypub.enabled'), 404);
|
||||
public function hostMeta(Request $request)
|
||||
{
|
||||
abort_if(! config('federation.webfinger.enabled'), 404);
|
||||
|
||||
if(!$request->wantsJson()) {
|
||||
return redirect('/' . $username);
|
||||
}
|
||||
$path = route('well-known.webfinger');
|
||||
$xml = '<?xml version="1.0" encoding="UTF-8"?><XRD xmlns="http://docs.oasis-open.org/ns/xri/xrd-1.0"><Link rel="lrdd" type="application/xrd+xml" template="'.$path.'?resource={uri}"/></XRD>';
|
||||
|
||||
$res = [
|
||||
'@context' => 'https://www.w3.org/ns/activitystreams',
|
||||
'id' => 'https://' . config('pixelfed.domain.app') . '/users/' . $username . '/outbox',
|
||||
'type' => 'OrderedCollection',
|
||||
'totalItems' => 0,
|
||||
'orderedItems' => []
|
||||
];
|
||||
return response($xml)->header('Content-Type', 'application/xrd+xml');
|
||||
}
|
||||
|
||||
return response(json_encode($res, JSON_UNESCAPED_SLASHES))->header('Content-Type', 'application/activity+json');
|
||||
}
|
||||
public function userOutbox(Request $request, $username)
|
||||
{
|
||||
abort_if(! (bool) config_cache('federation.activitypub.enabled'), 404);
|
||||
|
||||
public function userInbox(Request $request, $username)
|
||||
{
|
||||
abort_if(!config_cache('federation.activitypub.enabled'), 404);
|
||||
abort_if(!config('federation.activitypub.inbox'), 404);
|
||||
if (! $request->wantsJson()) {
|
||||
return redirect('/'.$username);
|
||||
}
|
||||
|
||||
$headers = $request->headers->all();
|
||||
$payload = $request->getContent();
|
||||
if(!$payload || empty($payload)) {
|
||||
return;
|
||||
}
|
||||
$obj = json_decode($payload, true, 8);
|
||||
if(!isset($obj['id'])) {
|
||||
return;
|
||||
}
|
||||
$domain = parse_url($obj['id'], PHP_URL_HOST);
|
||||
if(in_array($domain, InstanceService::getBannedDomains())) {
|
||||
return;
|
||||
}
|
||||
$id = AccountService::usernameToId($username);
|
||||
abort_if(! $id, 404);
|
||||
$account = AccountService::get($id);
|
||||
abort_if(! $account || ! isset($account['statuses_count']), 404);
|
||||
$res = [
|
||||
'@context' => 'https://www.w3.org/ns/activitystreams',
|
||||
'id' => 'https://'.config('pixelfed.domain.app').'/users/'.$username.'/outbox',
|
||||
'type' => 'OrderedCollection',
|
||||
'totalItems' => $account['statuses_count'] ?? 0,
|
||||
];
|
||||
|
||||
if(isset($obj['type']) && $obj['type'] === 'Delete') {
|
||||
if(isset($obj['object']) && isset($obj['object']['type']) && isset($obj['object']['id'])) {
|
||||
if($obj['object']['type'] === 'Person') {
|
||||
if(Profile::whereRemoteUrl($obj['object']['id'])->exists()) {
|
||||
dispatch(new DeleteWorker($headers, $payload))->onQueue('inbox');
|
||||
return;
|
||||
}
|
||||
}
|
||||
return response(json_encode($res, JSON_UNESCAPED_SLASHES))->header('Content-Type', 'application/activity+json');
|
||||
}
|
||||
|
||||
if($obj['object']['type'] === 'Tombstone') {
|
||||
if(Status::whereObjectUrl($obj['object']['id'])->exists()) {
|
||||
dispatch(new DeleteWorker($headers, $payload))->onQueue('delete');
|
||||
return;
|
||||
}
|
||||
}
|
||||
public function userInbox(Request $request, $username)
|
||||
{
|
||||
abort_if(! (bool) config_cache('federation.activitypub.enabled'), 404);
|
||||
abort_if(! config('federation.activitypub.inbox'), 404);
|
||||
|
||||
if($obj['object']['type'] === 'Story') {
|
||||
dispatch(new DeleteWorker($headers, $payload))->onQueue('story');
|
||||
return;
|
||||
}
|
||||
}
|
||||
return;
|
||||
} else if( isset($obj['type']) && in_array($obj['type'], ['Follow', 'Accept'])) {
|
||||
dispatch(new InboxValidator($username, $headers, $payload))->onQueue('follow');
|
||||
} else {
|
||||
dispatch(new InboxValidator($username, $headers, $payload))->onQueue('high');
|
||||
}
|
||||
return;
|
||||
}
|
||||
$headers = $request->headers->all();
|
||||
$payload = $request->getContent();
|
||||
if (! $payload || empty($payload)) {
|
||||
return;
|
||||
}
|
||||
$obj = json_decode($payload, true, 8);
|
||||
if (! isset($obj['id'])) {
|
||||
return;
|
||||
}
|
||||
$domain = parse_url($obj['id'], PHP_URL_HOST);
|
||||
if (in_array($domain, InstanceService::getBannedDomains())) {
|
||||
return;
|
||||
}
|
||||
|
||||
public function sharedInbox(Request $request)
|
||||
{
|
||||
abort_if(!config_cache('federation.activitypub.enabled'), 404);
|
||||
abort_if(!config('federation.activitypub.sharedInbox'), 404);
|
||||
if (isset($obj['type']) && $obj['type'] === 'Delete') {
|
||||
if (isset($obj['object']) && isset($obj['object']['type']) && isset($obj['object']['id'])) {
|
||||
if ($obj['object']['type'] === 'Person') {
|
||||
if (Profile::whereRemoteUrl($obj['object']['id'])->exists()) {
|
||||
dispatch(new DeleteWorker($headers, $payload))->onQueue('inbox');
|
||||
|
||||
$headers = $request->headers->all();
|
||||
$payload = $request->getContent();
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
if(!$payload || empty($payload)) {
|
||||
return;
|
||||
}
|
||||
if ($obj['object']['type'] === 'Tombstone') {
|
||||
if (Status::whereObjectUrl($obj['object']['id'])->exists()) {
|
||||
dispatch(new DeleteWorker($headers, $payload))->onQueue('delete');
|
||||
|
||||
$obj = json_decode($payload, true, 8);
|
||||
if(!isset($obj['id'])) {
|
||||
return;
|
||||
}
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
$domain = parse_url($obj['id'], PHP_URL_HOST);
|
||||
if(in_array($domain, InstanceService::getBannedDomains())) {
|
||||
return;
|
||||
}
|
||||
if ($obj['object']['type'] === 'Story') {
|
||||
dispatch(new DeleteWorker($headers, $payload))->onQueue('story');
|
||||
|
||||
if(isset($obj['type']) && $obj['type'] === 'Delete') {
|
||||
if(isset($obj['object']) && isset($obj['object']['type']) && isset($obj['object']['id'])) {
|
||||
if($obj['object']['type'] === 'Person') {
|
||||
if(Profile::whereRemoteUrl($obj['object']['id'])->exists()) {
|
||||
dispatch(new DeleteWorker($headers, $payload))->onQueue('inbox');
|
||||
return;
|
||||
}
|
||||
}
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
if($obj['object']['type'] === 'Tombstone') {
|
||||
if(Status::whereObjectUrl($obj['object']['id'])->exists()) {
|
||||
dispatch(new DeleteWorker($headers, $payload))->onQueue('delete');
|
||||
return;
|
||||
}
|
||||
}
|
||||
return;
|
||||
} elseif (isset($obj['type']) && in_array($obj['type'], ['Follow', 'Accept'])) {
|
||||
dispatch(new InboxValidator($username, $headers, $payload))->onQueue('follow');
|
||||
} else {
|
||||
dispatch(new InboxValidator($username, $headers, $payload))->onQueue('high');
|
||||
}
|
||||
|
||||
if($obj['object']['type'] === 'Story') {
|
||||
dispatch(new DeleteWorker($headers, $payload))->onQueue('story');
|
||||
return;
|
||||
}
|
||||
}
|
||||
return;
|
||||
} else if( isset($obj['type']) && in_array($obj['type'], ['Follow', 'Accept'])) {
|
||||
dispatch(new InboxWorker($headers, $payload))->onQueue('follow');
|
||||
} else {
|
||||
dispatch(new InboxWorker($headers, $payload))->onQueue('shared');
|
||||
}
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
public function userFollowing(Request $request, $username)
|
||||
{
|
||||
abort_if(!config_cache('federation.activitypub.enabled'), 404);
|
||||
public function sharedInbox(Request $request)
|
||||
{
|
||||
abort_if(! (bool) config_cache('federation.activitypub.enabled'), 404);
|
||||
abort_if(! config('federation.activitypub.sharedInbox'), 404);
|
||||
|
||||
$obj = [
|
||||
'@context' => 'https://www.w3.org/ns/activitystreams',
|
||||
'id' => $request->getUri(),
|
||||
'type' => 'OrderedCollectionPage',
|
||||
'totalItems' => 0,
|
||||
'orderedItems' => []
|
||||
];
|
||||
return response()->json($obj);
|
||||
}
|
||||
$headers = $request->headers->all();
|
||||
$payload = $request->getContent();
|
||||
|
||||
public function userFollowers(Request $request, $username)
|
||||
{
|
||||
abort_if(!config_cache('federation.activitypub.enabled'), 404);
|
||||
if (! $payload || empty($payload)) {
|
||||
return;
|
||||
}
|
||||
|
||||
$obj = [
|
||||
'@context' => 'https://www.w3.org/ns/activitystreams',
|
||||
'id' => $request->getUri(),
|
||||
'type' => 'OrderedCollectionPage',
|
||||
'totalItems' => 0,
|
||||
'orderedItems' => []
|
||||
];
|
||||
$obj = json_decode($payload, true, 8);
|
||||
if (! isset($obj['id'])) {
|
||||
return;
|
||||
}
|
||||
|
||||
return response()->json($obj);
|
||||
}
|
||||
$domain = parse_url($obj['id'], PHP_URL_HOST);
|
||||
if (in_array($domain, InstanceService::getBannedDomains())) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (isset($obj['type']) && $obj['type'] === 'Delete') {
|
||||
if (isset($obj['object']) && isset($obj['object']['type']) && isset($obj['object']['id'])) {
|
||||
if ($obj['object']['type'] === 'Person') {
|
||||
if (Profile::whereRemoteUrl($obj['object']['id'])->exists()) {
|
||||
dispatch(new DeleteWorker($headers, $payload))->onQueue('inbox');
|
||||
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
if ($obj['object']['type'] === 'Tombstone') {
|
||||
if (Status::whereObjectUrl($obj['object']['id'])->exists()) {
|
||||
dispatch(new DeleteWorker($headers, $payload))->onQueue('delete');
|
||||
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
if ($obj['object']['type'] === 'Story') {
|
||||
dispatch(new DeleteWorker($headers, $payload))->onQueue('story');
|
||||
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
return;
|
||||
} elseif (isset($obj['type']) && in_array($obj['type'], ['Follow', 'Accept'])) {
|
||||
dispatch(new InboxWorker($headers, $payload))->onQueue('follow');
|
||||
} else {
|
||||
dispatch(new InboxWorker($headers, $payload))->onQueue('shared');
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
public function userFollowing(Request $request, $username)
|
||||
{
|
||||
abort_if(! (bool) config_cache('federation.activitypub.enabled'), 404);
|
||||
|
||||
$id = AccountService::usernameToId($username);
|
||||
abort_if(! $id, 404);
|
||||
$account = AccountService::get($id);
|
||||
abort_if(! $account || ! isset($account['following_count']), 404);
|
||||
$obj = [
|
||||
'@context' => 'https://www.w3.org/ns/activitystreams',
|
||||
'id' => $request->getUri(),
|
||||
'type' => 'OrderedCollection',
|
||||
'totalItems' => $account['following_count'] ?? 0,
|
||||
];
|
||||
|
||||
return response()->json($obj)->header('Content-Type', 'application/activity+json');
|
||||
}
|
||||
|
||||
public function userFollowers(Request $request, $username)
|
||||
{
|
||||
abort_if(! (bool) config_cache('federation.activitypub.enabled'), 404);
|
||||
$id = AccountService::usernameToId($username);
|
||||
abort_if(! $id, 404);
|
||||
$account = AccountService::get($id);
|
||||
abort_if(! $account || ! isset($account['followers_count']), 404);
|
||||
$obj = [
|
||||
'@context' => 'https://www.w3.org/ns/activitystreams',
|
||||
'id' => $request->getUri(),
|
||||
'type' => 'OrderedCollection',
|
||||
'totalItems' => $account['followers_count'] ?? 0,
|
||||
];
|
||||
|
||||
return response()->json($obj)->header('Content-Type', 'application/activity+json');
|
||||
}
|
||||
}
|
||||
|
|
|
@ -0,0 +1,16 @@
|
|||
<?php
|
||||
|
||||
namespace App\Http\Controllers;
|
||||
|
||||
use Illuminate\Http\Request;
|
||||
|
||||
class HealthCheckController extends Controller
|
||||
{
|
||||
public function get(Request $request)
|
||||
{
|
||||
return response('OK')->withHeaders([
|
||||
'Content-Type' => 'text/plain',
|
||||
'Cache-Control' => 'max-age=0, must-revalidate, no-cache, no-store'
|
||||
]);
|
||||
}
|
||||
}
|
|
@ -17,7 +17,7 @@ trait Instagram
|
|||
{
|
||||
public function instagram()
|
||||
{
|
||||
if(config_cache('pixelfed.import.instagram.enabled') != true) {
|
||||
if((bool) config_cache('pixelfed.import.instagram.enabled') != true) {
|
||||
abort(404, 'Feature not enabled');
|
||||
}
|
||||
return view('settings.import.instagram.home');
|
||||
|
@ -25,6 +25,9 @@ trait Instagram
|
|||
|
||||
public function instagramStart(Request $request)
|
||||
{
|
||||
if((bool) config_cache('pixelfed.import.instagram.enabled') != true) {
|
||||
abort(404, 'Feature not enabled');
|
||||
}
|
||||
$completed = ImportJob::whereProfileId(Auth::user()->profile->id)
|
||||
->whereService('instagram')
|
||||
->whereNotNull('completed_at')
|
||||
|
@ -38,6 +41,9 @@ trait Instagram
|
|||
|
||||
protected function instagramRedirectOrNew()
|
||||
{
|
||||
if((bool) config_cache('pixelfed.import.instagram.enabled') != true) {
|
||||
abort(404, 'Feature not enabled');
|
||||
}
|
||||
$profile = Auth::user()->profile;
|
||||
$exists = ImportJob::whereProfileId($profile->id)
|
||||
->whereService('instagram')
|
||||
|
@ -61,6 +67,9 @@ trait Instagram
|
|||
|
||||
public function instagramStepOne(Request $request, $uuid)
|
||||
{
|
||||
if((bool) config_cache('pixelfed.import.instagram.enabled') != true) {
|
||||
abort(404, 'Feature not enabled');
|
||||
}
|
||||
$profile = Auth::user()->profile;
|
||||
$job = ImportJob::whereProfileId($profile->id)
|
||||
->whereNull('completed_at')
|
||||
|
@ -72,6 +81,9 @@ trait Instagram
|
|||
|
||||
public function instagramStepOneStore(Request $request, $uuid)
|
||||
{
|
||||
if((bool) config_cache('pixelfed.import.instagram.enabled') != true) {
|
||||
abort(404, 'Feature not enabled');
|
||||
}
|
||||
$max = 'max:' . config('pixelfed.import.instagram.limits.size');
|
||||
$this->validate($request, [
|
||||
'media.*' => 'required|mimes:bin,jpeg,png,gif|'.$max,
|
||||
|
@ -114,6 +126,9 @@ trait Instagram
|
|||
|
||||
public function instagramStepTwo(Request $request, $uuid)
|
||||
{
|
||||
if((bool) config_cache('pixelfed.import.instagram.enabled') != true) {
|
||||
abort(404, 'Feature not enabled');
|
||||
}
|
||||
$profile = Auth::user()->profile;
|
||||
$job = ImportJob::whereProfileId($profile->id)
|
||||
->whereNull('completed_at')
|
||||
|
@ -125,6 +140,9 @@ trait Instagram
|
|||
|
||||
public function instagramStepTwoStore(Request $request, $uuid)
|
||||
{
|
||||
if((bool) config_cache('pixelfed.import.instagram.enabled') != true) {
|
||||
abort(404, 'Feature not enabled');
|
||||
}
|
||||
$this->validate($request, [
|
||||
'media' => 'required|file|max:1000'
|
||||
]);
|
||||
|
@ -150,6 +168,9 @@ trait Instagram
|
|||
|
||||
public function instagramStepThree(Request $request, $uuid)
|
||||
{
|
||||
if((bool) config_cache('pixelfed.import.instagram.enabled') != true) {
|
||||
abort(404, 'Feature not enabled');
|
||||
}
|
||||
$profile = Auth::user()->profile;
|
||||
$job = ImportJob::whereProfileId($profile->id)
|
||||
->whereService('instagram')
|
||||
|
@ -162,6 +183,9 @@ trait Instagram
|
|||
|
||||
public function instagramStepThreeStore(Request $request, $uuid)
|
||||
{
|
||||
if((bool) config_cache('pixelfed.import.instagram.enabled') != true) {
|
||||
abort(404, 'Feature not enabled');
|
||||
}
|
||||
$profile = Auth::user()->profile;
|
||||
|
||||
try {
|
||||
|
|
|
@ -0,0 +1,309 @@
|
|||
<?php
|
||||
|
||||
namespace App\Http\Controllers;
|
||||
|
||||
use Illuminate\Http\Request;
|
||||
use App\Models\ImportPost;
|
||||
use App\Services\ImportService;
|
||||
use App\Services\StatusService;
|
||||
use App\Http\Resources\ImportStatus;
|
||||
use App\Follower;
|
||||
use App\User;
|
||||
|
||||
class ImportPostController extends Controller
|
||||
{
|
||||
public function __construct()
|
||||
{
|
||||
$this->middleware('auth');
|
||||
}
|
||||
|
||||
public function getConfig(Request $request)
|
||||
{
|
||||
return [
|
||||
'enabled' => config('import.instagram.enabled'),
|
||||
|
||||
'limits' => [
|
||||
'max_posts' => config('import.instagram.limits.max_posts'),
|
||||
'max_attempts' => config('import.instagram.limits.max_attempts'),
|
||||
],
|
||||
|
||||
'allow_video_posts' => config('import.instagram.allow_video_posts'),
|
||||
|
||||
'permissions' => [
|
||||
'admins_only' => config('import.instagram.permissions.admins_only'),
|
||||
'admin_follows_only' => config('import.instagram.permissions.admin_follows_only'),
|
||||
'min_account_age' => config('import.instagram.permissions.min_account_age'),
|
||||
'min_follower_count' => config('import.instagram.permissions.min_follower_count'),
|
||||
],
|
||||
|
||||
'allowed' => $this->checkPermissions($request, false)
|
||||
];
|
||||
}
|
||||
|
||||
public function getProcessingCount(Request $request)
|
||||
{
|
||||
abort_unless(config('import.instagram.enabled'), 404);
|
||||
|
||||
$processing = ImportPost::whereProfileId($request->user()->profile_id)
|
||||
->whereNull('status_id')
|
||||
->whereSkipMissingMedia(false)
|
||||
->count();
|
||||
|
||||
$finished = ImportPost::whereProfileId($request->user()->profile_id)
|
||||
->whereNotNull('status_id')
|
||||
->whereSkipMissingMedia(false)
|
||||
->count();
|
||||
|
||||
return response()->json([
|
||||
'processing_count' => $processing,
|
||||
'finished_count' => $finished,
|
||||
]);
|
||||
}
|
||||
|
||||
public function getImportedFiles(Request $request)
|
||||
{
|
||||
abort_unless(config('import.instagram.enabled'), 404);
|
||||
|
||||
return response()->json(
|
||||
ImportService::getImportedFiles($request->user()->profile_id),
|
||||
200,
|
||||
[],
|
||||
JSON_UNESCAPED_SLASHES
|
||||
);
|
||||
}
|
||||
|
||||
public function getImportedPosts(Request $request)
|
||||
{
|
||||
abort_unless(config('import.instagram.enabled'), 404);
|
||||
|
||||
return ImportStatus::collection(
|
||||
ImportPost::whereProfileId($request->user()->profile_id)
|
||||
->has('status')
|
||||
->cursorPaginate(9)
|
||||
);
|
||||
}
|
||||
|
||||
public function formatHashtags($val = false)
|
||||
{
|
||||
if(!$val || !strlen($val)) {
|
||||
return null;
|
||||
}
|
||||
|
||||
$groupedHashtagRegex = '/#\w+(?=#)/';
|
||||
|
||||
return preg_replace($groupedHashtagRegex, '$0 ', $val);
|
||||
}
|
||||
|
||||
public function store(Request $request)
|
||||
{
|
||||
abort_unless(config('import.instagram.enabled'), 404);
|
||||
$this->checkPermissions($request);
|
||||
|
||||
$uid = $request->user()->id;
|
||||
$pid = $request->user()->profile_id;
|
||||
foreach($request->input('files') as $file) {
|
||||
$media = $file['media'];
|
||||
$c = collect($media);
|
||||
$postHash = hash('sha256', $c->toJson());
|
||||
$exts = $c->map(function($m) {
|
||||
$fn = last(explode('/', $m['uri']));
|
||||
return last(explode('.', $fn));
|
||||
});
|
||||
$postType = 'photo';
|
||||
|
||||
if($exts->count() > 1) {
|
||||
if($exts->contains('mp4')) {
|
||||
if($exts->contains('jpg', 'png')) {
|
||||
$postType = 'photo:video:album';
|
||||
} else {
|
||||
$postType = 'video:album';
|
||||
}
|
||||
} else {
|
||||
$postType = 'photo:album';
|
||||
}
|
||||
} else {
|
||||
if(in_array($exts[0], ['jpg', 'png'])) {
|
||||
$postType = 'photo';
|
||||
} else if(in_array($exts[0], ['mp4'])) {
|
||||
$postType = 'video';
|
||||
}
|
||||
}
|
||||
|
||||
$ip = new ImportPost;
|
||||
$ip->user_id = $uid;
|
||||
$ip->profile_id = $pid;
|
||||
$ip->post_hash = $postHash;
|
||||
$ip->service = 'instagram';
|
||||
$ip->post_type = $postType;
|
||||
$ip->media_count = $c->count();
|
||||
$ip->media = $c->map(function($m) {
|
||||
return [
|
||||
'uri' => $m['uri'],
|
||||
'title' => $this->formatHashtags($m['title']),
|
||||
'creation_timestamp' => $m['creation_timestamp']
|
||||
];
|
||||
})->toArray();
|
||||
$ip->caption = $c->count() > 1 ? $this->formatHashtags($file['title']) : $this->formatHashtags($ip->media[0]['title']);
|
||||
$ip->filename = last(explode('/', $ip->media[0]['uri']));
|
||||
$ip->metadata = $c->map(function($m) {
|
||||
return [
|
||||
'uri' => $m['uri'],
|
||||
'media_metadata' => isset($m['media_metadata']) ? $m['media_metadata'] : null
|
||||
];
|
||||
})->toArray();
|
||||
$ip->creation_date = $c->count() > 1 ? now()->parse($file['creation_timestamp']) : now()->parse($media[0]['creation_timestamp']);
|
||||
$ip->creation_year = now()->parse($ip->creation_date)->format('y');
|
||||
$ip->creation_month = now()->parse($ip->creation_date)->format('m');
|
||||
$ip->creation_day = now()->parse($ip->creation_date)->format('d');
|
||||
$ip->save();
|
||||
|
||||
ImportService::getImportedFiles($pid, true);
|
||||
ImportService::getPostCount($pid, true);
|
||||
}
|
||||
return [
|
||||
'msg' => 'Success'
|
||||
];
|
||||
}
|
||||
|
||||
public function storeMedia(Request $request)
|
||||
{
|
||||
abort_unless(config('import.instagram.enabled'), 404);
|
||||
|
||||
$this->checkPermissions($request);
|
||||
|
||||
$mimes = config('import.instagram.allow_video_posts') ? 'mimetypes:image/png,image/jpeg,video/mp4' : 'mimetypes:image/png,image/jpeg';
|
||||
|
||||
$this->validate($request, [
|
||||
'file' => 'required|array|max:10',
|
||||
'file.*' => [
|
||||
'required',
|
||||
'file',
|
||||
$mimes,
|
||||
'max:' . config_cache('pixelfed.max_photo_size')
|
||||
]
|
||||
]);
|
||||
|
||||
foreach($request->file('file') as $file) {
|
||||
$fileName = $file->getClientOriginalName();
|
||||
$file->storeAs('imports/' . $request->user()->id . '/', $fileName);
|
||||
}
|
||||
|
||||
ImportService::getImportedFiles($request->user()->profile_id, true);
|
||||
|
||||
return [
|
||||
'msg' => 'Success'
|
||||
];
|
||||
}
|
||||
|
||||
protected function checkPermissions($request, $abortOnFail = true)
|
||||
{
|
||||
$user = $request->user();
|
||||
|
||||
if($abortOnFail) {
|
||||
abort_unless(config('import.instagram.enabled'), 404);
|
||||
}
|
||||
|
||||
if($user->is_admin) {
|
||||
if(!$abortOnFail) {
|
||||
return true;
|
||||
} else {
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
$admin = User::whereIsAdmin(true)->first();
|
||||
|
||||
if(config('import.instagram.permissions.admins_only')) {
|
||||
if($abortOnFail) {
|
||||
abort_unless($user->is_admin, 404, 'Only admins can use this feature.');
|
||||
} else {
|
||||
if(!$user->is_admin) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if(config('import.instagram.permissions.admin_follows_only')) {
|
||||
$exists = Follower::whereProfileId($admin->profile_id)
|
||||
->whereFollowingId($user->profile_id)
|
||||
->exists();
|
||||
if($abortOnFail) {
|
||||
abort_unless(
|
||||
$exists,
|
||||
404,
|
||||
'Only admins, and accounts they follow can use this feature'
|
||||
);
|
||||
} else {
|
||||
if(!$exists) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if(config('import.instagram.permissions.min_account_age')) {
|
||||
$res = $user->created_at->lt(
|
||||
now()->subDays(config('import.instagram.permissions.min_account_age'))
|
||||
);
|
||||
if($abortOnFail) {
|
||||
abort_unless(
|
||||
$res,
|
||||
404,
|
||||
'Your account is too new to use this feature'
|
||||
);
|
||||
} else {
|
||||
if(!$res) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if(config('import.instagram.permissions.min_follower_count')) {
|
||||
$res = Follower::whereFollowingId($user->profile_id)->count() >= config('import.instagram.permissions.min_follower_count');
|
||||
if($abortOnFail) {
|
||||
abort_unless(
|
||||
$res,
|
||||
404,
|
||||
'You don\'t have enough followers to use this feature'
|
||||
);
|
||||
} else {
|
||||
if(!$res) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if(intval(config('import.instagram.limits.max_posts')) > 0) {
|
||||
$res = ImportService::getPostCount($user->profile_id) >= intval(config('import.instagram.limits.max_posts'));
|
||||
if($abortOnFail) {
|
||||
abort_if(
|
||||
$res,
|
||||
404,
|
||||
'You have reached the limit of post imports and cannot import any more posts'
|
||||
);
|
||||
} else {
|
||||
if($res) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if(intval(config('import.instagram.limits.max_attempts')) > 0) {
|
||||
$res = ImportService::getAttempts($user->profile_id) >= intval(config('import.instagram.limits.max_attempts'));
|
||||
if($abortOnFail) {
|
||||
abort_if(
|
||||
$res,
|
||||
404,
|
||||
'You have reached the limit of post import attempts and cannot import any more posts'
|
||||
);
|
||||
} else {
|
||||
if($res) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if(!$abortOnFail) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
}
|
|
@ -25,7 +25,61 @@ class InstanceActorController extends Controller
|
|||
public function outbox()
|
||||
{
|
||||
$res = json_encode([
|
||||
'@context' => 'https://www.w3.org/ns/activitystreams',
|
||||
"@context" => [
|
||||
"https://www.w3.org/ns/activitystreams",
|
||||
"https://w3id.org/security/v1",
|
||||
[
|
||||
"manuallyApprovesFollowers" => "as:manuallyApprovesFollowers",
|
||||
"toot" => "http://joinmastodon.org/ns#",
|
||||
"featured" => [
|
||||
"@id" => "toot:featured",
|
||||
"@type" => "@id"
|
||||
],
|
||||
"featuredTags" => [
|
||||
"@id" => "toot:featuredTags",
|
||||
"@type" => "@id"
|
||||
],
|
||||
"alsoKnownAs" => [
|
||||
"@id" => "as:alsoKnownAs",
|
||||
"@type" => "@id"
|
||||
],
|
||||
"movedTo" => [
|
||||
"@id" => "as:movedTo",
|
||||
"@type" => "@id"
|
||||
],
|
||||
"schema" => "http://schema.org#",
|
||||
"PropertyValue" => "schema:PropertyValue",
|
||||
"value" => "schema:value",
|
||||
"discoverable" => "toot:discoverable",
|
||||
"Device" => "toot:Device",
|
||||
"Ed25519Signature" => "toot:Ed25519Signature",
|
||||
"Ed25519Key" => "toot:Ed25519Key",
|
||||
"Curve25519Key" => "toot:Curve25519Key",
|
||||
"EncryptedMessage" => "toot:EncryptedMessage",
|
||||
"publicKeyBase64" => "toot:publicKeyBase64",
|
||||
"deviceId" => "toot:deviceId",
|
||||
"claim" => [
|
||||
"@type" => "@id",
|
||||
"@id" => "toot:claim"
|
||||
],
|
||||
"fingerprintKey" => [
|
||||
"@type" => "@id",
|
||||
"@id" => "toot:fingerprintKey"
|
||||
],
|
||||
"identityKey" => [
|
||||
"@type" => "@id",
|
||||
"@id" => "toot:identityKey"
|
||||
],
|
||||
"devices" => [
|
||||
"@type" => "@id",
|
||||
"@id" => "toot:devices"
|
||||
],
|
||||
"messageFranking" => "toot:messageFranking",
|
||||
"messageType" => "toot:messageType",
|
||||
"cipherText" => "toot:cipherText",
|
||||
"suspended" => "toot:suspended"
|
||||
]
|
||||
],
|
||||
'id' => config('app.url') . '/i/actor/outbox',
|
||||
'type' => 'OrderedCollection',
|
||||
'totalItems' => 0,
|
||||
|
|
|
@ -2,44 +2,43 @@
|
|||
|
||||
namespace App\Http\Controllers;
|
||||
|
||||
use Illuminate\Http\Request;
|
||||
use App\Profile;
|
||||
use App\Services\AccountService;
|
||||
use App\Http\Resources\DirectoryProfile;
|
||||
use App\Profile;
|
||||
use Illuminate\Http\Request;
|
||||
|
||||
class LandingController extends Controller
|
||||
{
|
||||
public function directoryRedirect(Request $request)
|
||||
{
|
||||
if($request->user()) {
|
||||
return redirect('/');
|
||||
}
|
||||
if ($request->user()) {
|
||||
return redirect('/');
|
||||
}
|
||||
|
||||
abort_if(config('instance.landing.show_directory') == false, 404);
|
||||
abort_if((bool) config_cache('instance.landing.show_directory') == false, 404);
|
||||
|
||||
return view('site.index');
|
||||
return view('site.index');
|
||||
}
|
||||
|
||||
public function exploreRedirect(Request $request)
|
||||
{
|
||||
if($request->user()) {
|
||||
return redirect('/');
|
||||
}
|
||||
if ($request->user()) {
|
||||
return redirect('/');
|
||||
}
|
||||
|
||||
abort_if(config('instance.landing.show_explore') == false, 404);
|
||||
abort_if((bool) config_cache('instance.landing.show_explore') == false, 404);
|
||||
|
||||
return view('site.index');
|
||||
return view('site.index');
|
||||
}
|
||||
|
||||
public function getDirectoryApi(Request $request)
|
||||
{
|
||||
abort_if(config('instance.landing.show_directory') == false, 404);
|
||||
abort_if((bool) config_cache('instance.landing.show_directory') == false, 404);
|
||||
|
||||
return DirectoryProfile::collection(
|
||||
Profile::whereNull('domain')
|
||||
->whereIsSuggestable(true)
|
||||
->orderByDesc('updated_at')
|
||||
->cursorPaginate(20)
|
||||
);
|
||||
return DirectoryProfile::collection(
|
||||
Profile::whereNull('domain')
|
||||
->whereIsSuggestable(true)
|
||||
->orderByDesc('updated_at')
|
||||
->cursorPaginate(20)
|
||||
);
|
||||
}
|
||||
}
|
||||
|
|
|
@ -25,8 +25,7 @@ class LikeController extends Controller
|
|||
'item' => 'required|integer|min:1',
|
||||
]);
|
||||
|
||||
// API deprecated
|
||||
return;
|
||||
abort(422, 'Deprecated API Endpoint');
|
||||
|
||||
$user = Auth::user();
|
||||
$profile = $user->profile;
|
||||
|
@ -34,7 +33,7 @@ class LikeController extends Controller
|
|||
|
||||
if (Like::whereStatusId($status->id)->whereProfileId($profile->id)->exists()) {
|
||||
$like = Like::whereProfileId($profile->id)->whereStatusId($status->id)->firstOrFail();
|
||||
UnlikePipeline::dispatch($like);
|
||||
UnlikePipeline::dispatch($like)->onQueue('feed');
|
||||
} else {
|
||||
abort_if(
|
||||
Like::whereProfileId($user->profile_id)
|
||||
|
@ -60,7 +59,7 @@ class LikeController extends Controller
|
|||
]) == false;
|
||||
$like->save();
|
||||
$status->save();
|
||||
LikePipeline::dispatch($like);
|
||||
LikePipeline::dispatch($like)->onQueue('feed');
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -2,26 +2,31 @@
|
|||
|
||||
namespace App\Http\Controllers;
|
||||
|
||||
use Illuminate\Http\Request;
|
||||
use Auth, Storage, URL;
|
||||
use App\Media;
|
||||
use Image as Intervention;
|
||||
use App\Jobs\ImageOptimizePipeline\ImageOptimize;
|
||||
use Illuminate\Http\Request;
|
||||
|
||||
class MediaController extends Controller
|
||||
{
|
||||
public function __construct()
|
||||
{
|
||||
$this->middleware('auth');
|
||||
}
|
||||
public function index(Request $request)
|
||||
{
|
||||
//return view('settings.drive.index');
|
||||
abort(404);
|
||||
}
|
||||
|
||||
public function index(Request $request)
|
||||
{
|
||||
//return view('settings.drive.index');
|
||||
}
|
||||
|
||||
public function composeUpdate(Request $request, $id)
|
||||
{
|
||||
public function composeUpdate(Request $request, $id)
|
||||
{
|
||||
abort(400, 'Endpoint deprecated');
|
||||
}
|
||||
}
|
||||
|
||||
public function fallbackRedirect(Request $request, $pid, $mhash, $uhash, $f)
|
||||
{
|
||||
abort_if(! (bool) config_cache('pixelfed.cloud_storage'), 404);
|
||||
$path = 'public/m/_v2/'.$pid.'/'.$mhash.'/'.$uhash.'/'.$f;
|
||||
$media = Media::whereProfileId($pid)
|
||||
->whereMediaPath($path)
|
||||
->whereNotNull('cdn_url')
|
||||
->firstOrFail();
|
||||
|
||||
return redirect()->away($media->cdn_url);
|
||||
}
|
||||
}
|
||||
|
|
|
@ -0,0 +1,231 @@
|
|||
<?php
|
||||
|
||||
namespace App\Http\Controllers;
|
||||
|
||||
use Illuminate\Http\Request;
|
||||
use App\Models\ParentalControls;
|
||||
use App\Models\UserRoles;
|
||||
use App\Profile;
|
||||
use App\User;
|
||||
use App\Http\Controllers\Auth\RegisterController;
|
||||
use Illuminate\Auth\Events\Registered;
|
||||
use Illuminate\Support\Facades\Auth;
|
||||
use App\Services\UserRoleService;
|
||||
use App\Jobs\ParentalControlsPipeline\DispatchChildInvitePipeline;
|
||||
|
||||
class ParentalControlsController extends Controller
|
||||
{
|
||||
public function authPreflight($request, $maxUserCheck = false, $authCheck = true)
|
||||
{
|
||||
if($authCheck) {
|
||||
abort_unless($request->user(), 404);
|
||||
abort_unless($request->user()->has_roles === 0, 404);
|
||||
}
|
||||
abort_unless(config('instance.parental_controls.enabled'), 404);
|
||||
if(config_cache('pixelfed.open_registration') == false) {
|
||||
abort_if(config('instance.parental_controls.limits.respect_open_registration'), 404);
|
||||
}
|
||||
if($maxUserCheck == true) {
|
||||
$hasLimit = config('pixelfed.enforce_max_users');
|
||||
if($hasLimit) {
|
||||
$count = User::where(function($q){ return $q->whereNull('status')->orWhereNotIn('status', ['deleted','delete']); })->count();
|
||||
$limit = (int) config('pixelfed.max_users');
|
||||
|
||||
abort_if($limit && $limit <= $count, 404);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public function index(Request $request)
|
||||
{
|
||||
$this->authPreflight($request);
|
||||
$children = ParentalControls::whereParentId($request->user()->id)->latest()->paginate(5);
|
||||
return view('settings.parental-controls.index', compact('children'));
|
||||
}
|
||||
|
||||
public function add(Request $request)
|
||||
{
|
||||
$this->authPreflight($request, true);
|
||||
return view('settings.parental-controls.add');
|
||||
}
|
||||
|
||||
public function view(Request $request, $id)
|
||||
{
|
||||
$this->authPreflight($request);
|
||||
$uid = $request->user()->id;
|
||||
$pc = ParentalControls::whereParentId($uid)->findOrFail($id);
|
||||
return view('settings.parental-controls.manage', compact('pc'));
|
||||
}
|
||||
|
||||
public function update(Request $request, $id)
|
||||
{
|
||||
$this->authPreflight($request);
|
||||
$uid = $request->user()->id;
|
||||
$ff = $this->requestFormFields($request);
|
||||
$pc = ParentalControls::whereParentId($uid)->findOrFail($id);
|
||||
$pc->permissions = $ff;
|
||||
$pc->save();
|
||||
|
||||
$roles = UserRoleService::mapActions($pc->child_id, $ff);
|
||||
if(isset($roles['account-force-private'])) {
|
||||
$c = Profile::whereUserId($pc->child_id)->first();
|
||||
$c->is_private = $roles['account-force-private'];
|
||||
$c->save();
|
||||
}
|
||||
UserRoles::whereUserId($pc->child_id)->update(['roles' => $roles]);
|
||||
return redirect($pc->manageUrl() . '?permissions');
|
||||
}
|
||||
|
||||
public function store(Request $request)
|
||||
{
|
||||
$this->authPreflight($request, true);
|
||||
$this->validate($request, [
|
||||
'email' => 'required|email|unique:parental_controls,email|unique:users,email',
|
||||
]);
|
||||
|
||||
$state = $this->requestFormFields($request);
|
||||
|
||||
$pc = new ParentalControls;
|
||||
$pc->parent_id = $request->user()->id;
|
||||
$pc->email = $request->input('email');
|
||||
$pc->verify_code = str_random(32);
|
||||
$pc->permissions = $state;
|
||||
$pc->save();
|
||||
|
||||
DispatchChildInvitePipeline::dispatch($pc);
|
||||
return redirect($pc->manageUrl());
|
||||
}
|
||||
|
||||
public function inviteRegister(Request $request, $id, $code)
|
||||
{
|
||||
if($request->user()) {
|
||||
$title = 'You cannot complete this action on this device.';
|
||||
$body = 'Please log out or use a different device or browser to complete the invitation registration.';
|
||||
return view('errors.custom', compact('title', 'body'));
|
||||
}
|
||||
|
||||
$this->authPreflight($request, true, false);
|
||||
|
||||
$pc = ParentalControls::whereRaw('verify_code = BINARY ?', $code)->whereNull(['email_verified_at', 'child_id'])->findOrFail($id);
|
||||
abort_unless(User::whereId($pc->parent_id)->exists(), 404);
|
||||
return view('settings.parental-controls.invite-register-form', compact('pc'));
|
||||
}
|
||||
|
||||
public function inviteRegisterStore(Request $request, $id, $code)
|
||||
{
|
||||
if($request->user()) {
|
||||
$title = 'You cannot complete this action on this device.';
|
||||
$body = 'Please log out or use a different device or browser to complete the invitation registration.';
|
||||
return view('errors.custom', compact('title', 'body'));
|
||||
}
|
||||
|
||||
$this->authPreflight($request, true, false);
|
||||
|
||||
$pc = ParentalControls::whereRaw('verify_code = BINARY ?', $code)->whereNull('email_verified_at')->findOrFail($id);
|
||||
|
||||
$fields = $request->all();
|
||||
$fields['email'] = $pc->email;
|
||||
$defaults = UserRoleService::defaultRoles();
|
||||
$validator = (new RegisterController)->validator($fields);
|
||||
$valid = $validator->validate();
|
||||
abort_if(!$valid, 404);
|
||||
event(new Registered($user = (new RegisterController)->create($fields)));
|
||||
sleep(5);
|
||||
$user->has_roles = true;
|
||||
$user->parent_id = $pc->parent_id;
|
||||
if(config('instance.parental_controls.limits.auto_verify_email')) {
|
||||
$user->email_verified_at = now();
|
||||
$user->save();
|
||||
sleep(3);
|
||||
} else {
|
||||
$user->save();
|
||||
sleep(3);
|
||||
}
|
||||
$ur = UserRoles::updateOrCreate([
|
||||
'user_id' => $user->id,
|
||||
],[
|
||||
'roles' => UserRoleService::mapInvite($user->id, $pc->permissions)
|
||||
]);
|
||||
$pc->email_verified_at = now();
|
||||
$pc->child_id = $user->id;
|
||||
$pc->save();
|
||||
sleep(2);
|
||||
Auth::guard()->login($user);
|
||||
|
||||
return redirect('/i/web');
|
||||
}
|
||||
|
||||
public function cancelInvite(Request $request, $id)
|
||||
{
|
||||
$this->authPreflight($request);
|
||||
$pc = ParentalControls::whereParentId($request->user()->id)
|
||||
->whereNull(['email_verified_at', 'child_id'])
|
||||
->findOrFail($id);
|
||||
|
||||
return view('settings.parental-controls.delete-invite', compact('pc'));
|
||||
}
|
||||
|
||||
public function cancelInviteHandle(Request $request, $id)
|
||||
{
|
||||
$this->authPreflight($request);
|
||||
$pc = ParentalControls::whereParentId($request->user()->id)
|
||||
->whereNull(['email_verified_at', 'child_id'])
|
||||
->findOrFail($id);
|
||||
|
||||
$pc->delete();
|
||||
|
||||
return redirect('/settings/parental-controls');
|
||||
}
|
||||
|
||||
public function stopManaging(Request $request, $id)
|
||||
{
|
||||
$this->authPreflight($request);
|
||||
$pc = ParentalControls::whereParentId($request->user()->id)
|
||||
->whereNotNull(['email_verified_at', 'child_id'])
|
||||
->findOrFail($id);
|
||||
|
||||
return view('settings.parental-controls.stop-managing', compact('pc'));
|
||||
}
|
||||
|
||||
public function stopManagingHandle(Request $request, $id)
|
||||
{
|
||||
$this->authPreflight($request);
|
||||
$pc = ParentalControls::whereParentId($request->user()->id)
|
||||
->whereNotNull(['email_verified_at', 'child_id'])
|
||||
->findOrFail($id);
|
||||
$pc->child()->update([
|
||||
'has_roles' => false,
|
||||
'parent_id' => null,
|
||||
]);
|
||||
$pc->delete();
|
||||
|
||||
return redirect('/settings/parental-controls');
|
||||
}
|
||||
|
||||
protected function requestFormFields($request)
|
||||
{
|
||||
$state = [];
|
||||
$fields = [
|
||||
'post',
|
||||
'comment',
|
||||
'like',
|
||||
'share',
|
||||
'follow',
|
||||
'bookmark',
|
||||
'story',
|
||||
'collection',
|
||||
'discovery_feeds',
|
||||
'dms',
|
||||
'federation',
|
||||
'hide_network',
|
||||
'private',
|
||||
'hide_cw'
|
||||
];
|
||||
|
||||
foreach ($fields as $field) {
|
||||
$state[$field] = $request->input($field) == 'on';
|
||||
}
|
||||
|
||||
return $state;
|
||||
}
|
||||
}
|
|
@ -2,37 +2,41 @@
|
|||
|
||||
namespace App\Http\Controllers;
|
||||
|
||||
use Illuminate\Http\Request;
|
||||
use App\Models\ConfigCache;
|
||||
use Storage;
|
||||
use App\Services\AccountService;
|
||||
use App\Services\StatusService;
|
||||
use Illuminate\Http\Request;
|
||||
use Illuminate\Support\Str;
|
||||
use Cache;
|
||||
use Storage;
|
||||
use App\Status;
|
||||
use App\User;
|
||||
|
||||
class PixelfedDirectoryController extends Controller
|
||||
{
|
||||
public function get(Request $request)
|
||||
{
|
||||
if(!$request->filled('sk')) {
|
||||
if (! $request->filled('sk')) {
|
||||
abort(404);
|
||||
}
|
||||
|
||||
if(!config_cache('pixelfed.directory.submission-key')) {
|
||||
if (! config_cache('pixelfed.directory.submission-key')) {
|
||||
abort(404);
|
||||
}
|
||||
|
||||
if(!hash_equals(config_cache('pixelfed.directory.submission-key'), $request->input('sk'))) {
|
||||
if (! hash_equals(config_cache('pixelfed.directory.submission-key'), $request->input('sk'))) {
|
||||
abort(403);
|
||||
}
|
||||
|
||||
$res = $this->buildListing();
|
||||
return response()->json($res, 200, [], JSON_PRETTY_PRINT|JSON_UNESCAPED_SLASHES);
|
||||
|
||||
return response()->json($res, 200, [], JSON_PRETTY_PRINT | JSON_UNESCAPED_SLASHES);
|
||||
}
|
||||
|
||||
public function buildListing()
|
||||
{
|
||||
$res = config_cache('pixelfed.directory');
|
||||
if($res) {
|
||||
if ($res) {
|
||||
$res = is_string($res) ? json_decode($res, true) : $res;
|
||||
}
|
||||
|
||||
|
@ -41,70 +45,71 @@ class PixelfedDirectoryController extends Controller
|
|||
$res['_ts'] = config_cache('pixelfed.directory.submission-ts');
|
||||
$res['version'] = config_cache('pixelfed.version');
|
||||
|
||||
if(empty($res['summary'])) {
|
||||
if (empty($res['summary'])) {
|
||||
$summary = ConfigCache::whereK('app.short_description')->pluck('v');
|
||||
$res['summary'] = $summary ? $summary[0] : null;
|
||||
}
|
||||
|
||||
if(isset($res['admin'])) {
|
||||
if (isset($res['admin'])) {
|
||||
$res['admin'] = AccountService::get($res['admin'], true);
|
||||
}
|
||||
|
||||
if(isset($res['banner_image']) && !empty($res['banner_image'])) {
|
||||
if (isset($res['banner_image']) && ! empty($res['banner_image'])) {
|
||||
$res['banner_image'] = url(Storage::url($res['banner_image']));
|
||||
}
|
||||
|
||||
if(isset($res['favourite_posts'])) {
|
||||
$res['favourite_posts'] = collect($res['favourite_posts'])->map(function($id) {
|
||||
if (isset($res['favourite_posts'])) {
|
||||
$res['favourite_posts'] = collect($res['favourite_posts'])->map(function ($id) {
|
||||
return StatusService::get($id);
|
||||
})
|
||||
->filter(function($post) {
|
||||
return $post && isset($post['account']);
|
||||
})
|
||||
->map(function($post) {
|
||||
return [
|
||||
'avatar' => $post['account']['avatar'],
|
||||
'display_name' => $post['account']['display_name'],
|
||||
'username' => $post['account']['username'],
|
||||
'media' => $post['media_attachments'][0]['url'],
|
||||
'url' => $post['url']
|
||||
];
|
||||
})
|
||||
->values();
|
||||
->filter(function ($post) {
|
||||
return $post && isset($post['account']);
|
||||
})
|
||||
->map(function ($post) {
|
||||
return [
|
||||
'avatar' => $post['account']['avatar'],
|
||||
'display_name' => $post['account']['display_name'],
|
||||
'username' => $post['account']['username'],
|
||||
'media' => $post['media_attachments'][0]['url'],
|
||||
'url' => $post['url'],
|
||||
];
|
||||
})
|
||||
->values();
|
||||
}
|
||||
|
||||
$guidelines = ConfigCache::whereK('app.rules')->first();
|
||||
if($guidelines) {
|
||||
if ($guidelines) {
|
||||
$res['community_guidelines'] = json_decode($guidelines->v, true);
|
||||
}
|
||||
|
||||
$openRegistration = ConfigCache::whereK('pixelfed.open_registration')->first();
|
||||
if($openRegistration) {
|
||||
$res['open_registration'] = (bool) $openRegistration;
|
||||
}
|
||||
$openRegistration = (bool) config_cache('pixelfed.open_registration');
|
||||
$res['open_registration'] = $openRegistration;
|
||||
|
||||
$curatedOnboarding = (bool) config_cache('instance.curated_registration.enabled');
|
||||
$res['curated_onboarding'] = $curatedOnboarding;
|
||||
|
||||
$oauthEnabled = ConfigCache::whereK('pixelfed.oauth_enabled')->first();
|
||||
if($oauthEnabled) {
|
||||
if ($oauthEnabled) {
|
||||
$keys = file_exists(storage_path('oauth-public.key')) && file_exists(storage_path('oauth-private.key'));
|
||||
$res['oauth_enabled'] = (bool) $oauthEnabled && $keys;
|
||||
}
|
||||
|
||||
$activityPubEnabled = ConfigCache::whereK('federation.activitypub.enabled')->first();
|
||||
if($activityPubEnabled) {
|
||||
if ($activityPubEnabled) {
|
||||
$res['activitypub_enabled'] = (bool) $activityPubEnabled;
|
||||
}
|
||||
|
||||
$res['feature_config'] = [
|
||||
'media_types' => Str::of(config_cache('pixelfed.media_types'))->explode(','),
|
||||
'image_quality' => config_cache('pixelfed.image_quality'),
|
||||
'optimize_image' => config_cache('pixelfed.optimize_image'),
|
||||
'optimize_image' => (bool) config_cache('pixelfed.optimize_image'),
|
||||
'max_photo_size' => config_cache('pixelfed.max_photo_size'),
|
||||
'max_caption_length' => config_cache('pixelfed.max_caption_length'),
|
||||
'max_altext_length' => config_cache('pixelfed.max_altext_length'),
|
||||
'enforce_account_limit' => config_cache('pixelfed.enforce_account_limit'),
|
||||
'enforce_account_limit' => (bool) config_cache('pixelfed.enforce_account_limit'),
|
||||
'max_account_size' => config_cache('pixelfed.max_account_size'),
|
||||
'max_album_length' => config_cache('pixelfed.max_album_length'),
|
||||
'account_deletion' => config_cache('pixelfed.account_deletion'),
|
||||
'account_deletion' => (bool) config_cache('pixelfed.account_deletion'),
|
||||
];
|
||||
|
||||
$res['is_eligible'] = $this->validVal($res, 'admin') &&
|
||||
|
@ -114,29 +119,36 @@ class PixelfedDirectoryController extends Controller
|
|||
$this->validVal($res, 'privacy_pledge') &&
|
||||
$this->validVal($res, 'location');
|
||||
|
||||
if(config_cache('pixelfed.directory.testimonials')) {
|
||||
if (config_cache('pixelfed.directory.testimonials')) {
|
||||
$res['testimonials'] = collect(json_decode(config_cache('pixelfed.directory.testimonials'), true))
|
||||
->map(function($testimonial) {
|
||||
->map(function ($testimonial) {
|
||||
$profile = AccountService::get($testimonial['profile_id']);
|
||||
|
||||
return [
|
||||
'profile' => [
|
||||
'username' => $profile['username'],
|
||||
'display_name' => $profile['display_name'],
|
||||
'avatar' => $profile['avatar'],
|
||||
'created_at' => $profile['created_at']
|
||||
'created_at' => $profile['created_at'],
|
||||
],
|
||||
'body' => $testimonial['body']
|
||||
'body' => $testimonial['body'],
|
||||
];
|
||||
});
|
||||
}
|
||||
|
||||
$res['features_enabled'] = [
|
||||
'stories' => (bool) config_cache('instance.stories.enabled')
|
||||
'stories' => (bool) config_cache('instance.stories.enabled'),
|
||||
];
|
||||
|
||||
$statusesCount = Cache::remember('api:nodeinfo:statuses', 21600, function() {
|
||||
return Status::whereLocal(true)->count();
|
||||
});
|
||||
$usersCount = Cache::remember('api:nodeinfo:users', 43200, function() {
|
||||
return User::count();
|
||||
});
|
||||
$res['stats'] = [
|
||||
'user_count' => \App\User::count(),
|
||||
'post_count' => \App\Status::whereNull('uri')->count(),
|
||||
'user_count' => (int) $usersCount,
|
||||
'post_count' => (int) $statusesCount,
|
||||
];
|
||||
|
||||
$res['primary_locale'] = config('app.locale');
|
||||
|
@ -149,19 +161,18 @@ class PixelfedDirectoryController extends Controller
|
|||
|
||||
protected function validVal($res, $val, $count = false, $minLen = false)
|
||||
{
|
||||
if(!isset($res[$val])) {
|
||||
if (! isset($res[$val])) {
|
||||
return false;
|
||||
}
|
||||
|
||||
if($count) {
|
||||
if ($count) {
|
||||
return count($res[$val]) >= $count;
|
||||
}
|
||||
|
||||
if($minLen) {
|
||||
if ($minLen) {
|
||||
return strlen($res[$val]) >= $minLen;
|
||||
}
|
||||
|
||||
return $res[$val];
|
||||
}
|
||||
|
||||
}
|
||||
|
|
|
@ -0,0 +1,93 @@
|
|||
<?php
|
||||
|
||||
namespace App\Http\Controllers;
|
||||
|
||||
use App\Models\ProfileAlias;
|
||||
use App\Models\ProfileMigration;
|
||||
use App\Services\AccountService;
|
||||
use App\Services\WebfingerService;
|
||||
use App\Util\Lexer\Nickname;
|
||||
use Cache;
|
||||
use Illuminate\Http\Request;
|
||||
|
||||
class ProfileAliasController extends Controller
|
||||
{
|
||||
public function __construct()
|
||||
{
|
||||
$this->middleware('auth');
|
||||
}
|
||||
|
||||
public function index(Request $request)
|
||||
{
|
||||
$aliases = $request->user()->profile->aliases;
|
||||
|
||||
return view('settings.aliases.index', compact('aliases'));
|
||||
}
|
||||
|
||||
public function store(Request $request)
|
||||
{
|
||||
$this->validate($request, [
|
||||
'acct' => 'required',
|
||||
]);
|
||||
|
||||
$acct = $request->input('acct');
|
||||
|
||||
$nn = Nickname::normalizeProfileUrl($acct);
|
||||
if (! $nn) {
|
||||
return back()->with('error', 'Invalid account alias.');
|
||||
}
|
||||
|
||||
if ($nn['domain'] === config('pixelfed.domain.app')) {
|
||||
if (strtolower($nn['username']) == ($request->user()->username)) {
|
||||
return back()->with('error', 'You cannot add an alias to your own account.');
|
||||
}
|
||||
}
|
||||
|
||||
if ($request->user()->profile->aliases->count() >= 3) {
|
||||
return back()->with('error', 'You can only add 3 account aliases.');
|
||||
}
|
||||
|
||||
$webfingerService = WebfingerService::lookup($acct);
|
||||
$webfingerUrl = WebfingerService::rawGet($acct);
|
||||
|
||||
if (! $webfingerService || ! isset($webfingerService['url']) || ! $webfingerUrl || empty($webfingerUrl)) {
|
||||
return back()->with('error', 'Invalid account, cannot add alias at this time.');
|
||||
}
|
||||
$alias = new ProfileAlias;
|
||||
$alias->profile_id = $request->user()->profile_id;
|
||||
$alias->acct = $acct;
|
||||
$alias->uri = $webfingerUrl;
|
||||
$alias->save();
|
||||
|
||||
Cache::forget('pf:activitypub:user-object:by-id:'.$request->user()->profile_id);
|
||||
|
||||
return back()->with('status', 'Successfully added alias!');
|
||||
}
|
||||
|
||||
public function delete(Request $request)
|
||||
{
|
||||
$this->validate($request, [
|
||||
'acct' => 'required',
|
||||
'id' => 'required|exists:profile_aliases',
|
||||
]);
|
||||
$pid = $request->user()->profile_id;
|
||||
$acct = $request->input('acct');
|
||||
$alias = ProfileAlias::where('profile_id', $pid)
|
||||
->where('acct', $acct)
|
||||
->findOrFail($request->input('id'));
|
||||
$migration = ProfileMigration::whereProfileId($pid)
|
||||
->whereAcct($acct)
|
||||
->first();
|
||||
if ($migration) {
|
||||
$request->user()->profile->update([
|
||||
'moved_to_profile_id' => null,
|
||||
]);
|
||||
}
|
||||
|
||||
$alias->delete();
|
||||
Cache::forget('pf:activitypub:user-object:by-id:'.$pid);
|
||||
AccountService::del($pid);
|
||||
|
||||
return back()->with('status', 'Successfully deleted alias!');
|
||||
}
|
||||
}
|
|
@ -2,298 +2,387 @@
|
|||
|
||||
namespace App\Http\Controllers;
|
||||
|
||||
use Illuminate\Http\Request;
|
||||
use Auth;
|
||||
use Cache;
|
||||
use DB;
|
||||
use View;
|
||||
use App\AccountInterstitial;
|
||||
use App\Follower;
|
||||
use App\FollowRequest;
|
||||
use App\Profile;
|
||||
use App\Story;
|
||||
use App\User;
|
||||
use App\UserFilter;
|
||||
use League\Fractal;
|
||||
use App\Services\AccountService;
|
||||
use App\Services\FollowerService;
|
||||
use App\Services\StatusService;
|
||||
use App\Util\Lexer\Nickname;
|
||||
use App\Util\Webfinger\Webfinger;
|
||||
use App\Transformer\ActivityPub\ProfileOutbox;
|
||||
use App\Status;
|
||||
use App\Story;
|
||||
use App\Transformer\ActivityPub\ProfileTransformer;
|
||||
use App\User;
|
||||
use App\UserFilter;
|
||||
use App\UserSetting;
|
||||
use Auth;
|
||||
use Cache;
|
||||
use Illuminate\Http\Request;
|
||||
use League\Fractal;
|
||||
use View;
|
||||
|
||||
class ProfileController extends Controller
|
||||
{
|
||||
public function show(Request $request, $username)
|
||||
{
|
||||
// redirect authed users to Metro 2.0
|
||||
if($request->user()) {
|
||||
// unless they force static view
|
||||
if(!$request->has('fs') || $request->input('fs') != '1') {
|
||||
$pid = AccountService::usernameToId($username);
|
||||
if($pid) {
|
||||
return redirect('/i/web/profile/' . $pid);
|
||||
}
|
||||
}
|
||||
}
|
||||
public function show(Request $request, $username)
|
||||
{
|
||||
if ($request->wantsJson() && (bool) config_cache('federation.activitypub.enabled')) {
|
||||
$user = $this->getCachedUser($username, true);
|
||||
abort_if(! $user, 404, 'Not found');
|
||||
|
||||
$user = Profile::whereNull('domain')
|
||||
->whereNull('status')
|
||||
->whereUsername($username)
|
||||
->firstOrFail();
|
||||
return $this->showActivityPub($request, $user);
|
||||
}
|
||||
|
||||
if($request->wantsJson() && config_cache('federation.activitypub.enabled')) {
|
||||
return $this->showActivityPub($request, $user);
|
||||
}
|
||||
return $this->buildProfile($request, $user);
|
||||
}
|
||||
// redirect authed users to Metro 2.0
|
||||
if ($request->user()) {
|
||||
// unless they force static view
|
||||
if (! $request->has('fs') || $request->input('fs') != '1') {
|
||||
$pid = AccountService::usernameToId($username);
|
||||
if ($pid) {
|
||||
return redirect('/i/web/profile/'.$pid);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
protected function buildProfile(Request $request, $user)
|
||||
{
|
||||
$username = $user->username;
|
||||
$loggedIn = Auth::check();
|
||||
$isPrivate = false;
|
||||
$isBlocked = false;
|
||||
if(!$loggedIn) {
|
||||
$key = 'profile:settings:' . $user->id;
|
||||
$ttl = now()->addHours(6);
|
||||
$settings = Cache::remember($key, $ttl, function() use($user) {
|
||||
return $user->user->settings;
|
||||
});
|
||||
$user = $this->getCachedUser($username);
|
||||
|
||||
if ($user->is_private == true) {
|
||||
$profile = null;
|
||||
return view('profile.private', compact('user'));
|
||||
}
|
||||
abort_unless($user, 404);
|
||||
|
||||
$owner = false;
|
||||
$is_following = false;
|
||||
$aiCheck = Cache::remember('profile:ai-check:spam-login:'.$user->id, 3600, function () use ($user) {
|
||||
$exists = AccountInterstitial::whereUserId($user->user_id)->where('is_spam', 1)->count();
|
||||
if ($exists) {
|
||||
return true;
|
||||
}
|
||||
|
||||
$profile = $user;
|
||||
$settings = [
|
||||
'crawlable' => $settings->crawlable,
|
||||
'following' => [
|
||||
'count' => $settings->show_profile_following_count,
|
||||
'list' => $settings->show_profile_following
|
||||
],
|
||||
'followers' => [
|
||||
'count' => $settings->show_profile_follower_count,
|
||||
'list' => $settings->show_profile_followers
|
||||
]
|
||||
];
|
||||
return view('profile.show', compact('profile', 'settings'));
|
||||
} else {
|
||||
$key = 'profile:settings:' . $user->id;
|
||||
$ttl = now()->addHours(6);
|
||||
$settings = Cache::remember($key, $ttl, function() use($user) {
|
||||
return $user->user->settings;
|
||||
});
|
||||
return false;
|
||||
});
|
||||
if ($aiCheck) {
|
||||
return redirect('/login');
|
||||
}
|
||||
|
||||
if ($user->is_private == true) {
|
||||
$isPrivate = $this->privateProfileCheck($user, $loggedIn);
|
||||
}
|
||||
return $this->buildProfile($request, $user);
|
||||
}
|
||||
|
||||
$isBlocked = $this->blockedProfileCheck($user);
|
||||
protected function buildProfile(Request $request, $user)
|
||||
{
|
||||
$username = $user->username;
|
||||
$loggedIn = Auth::check();
|
||||
$isPrivate = false;
|
||||
$isBlocked = false;
|
||||
if (! $loggedIn) {
|
||||
$key = 'profile:settings:'.$user->id;
|
||||
$ttl = now()->addHours(6);
|
||||
$settings = Cache::remember($key, $ttl, function () use ($user) {
|
||||
return $user->user->settings;
|
||||
});
|
||||
|
||||
$owner = $loggedIn && Auth::id() === $user->user_id;
|
||||
$is_following = ($owner == false && Auth::check()) ? $user->followedBy(Auth::user()->profile) : false;
|
||||
if ($user->is_private == true) {
|
||||
$profile = null;
|
||||
|
||||
if ($isPrivate == true || $isBlocked == true) {
|
||||
$requested = Auth::check() ? FollowRequest::whereFollowerId(Auth::user()->profile_id)
|
||||
->whereFollowingId($user->id)
|
||||
->exists() : false;
|
||||
return view('profile.private', compact('user', 'is_following', 'requested'));
|
||||
}
|
||||
return view('profile.private', compact('user'));
|
||||
}
|
||||
|
||||
$is_admin = is_null($user->domain) ? $user->user->is_admin : false;
|
||||
$profile = $user;
|
||||
$settings = [
|
||||
'crawlable' => $settings->crawlable,
|
||||
'following' => [
|
||||
'count' => $settings->show_profile_following_count,
|
||||
'list' => $settings->show_profile_following
|
||||
],
|
||||
'followers' => [
|
||||
'count' => $settings->show_profile_follower_count,
|
||||
'list' => $settings->show_profile_followers
|
||||
]
|
||||
];
|
||||
return view('profile.show', compact('profile', 'settings'));
|
||||
}
|
||||
}
|
||||
$owner = false;
|
||||
$is_following = false;
|
||||
|
||||
public function permalinkRedirect(Request $request, $username)
|
||||
{
|
||||
$user = Profile::whereNull('domain')->whereUsername($username)->firstOrFail();
|
||||
$profile = $user;
|
||||
$settings = [
|
||||
'crawlable' => $settings->crawlable,
|
||||
'following' => [
|
||||
'count' => $settings->show_profile_following_count,
|
||||
'list' => $settings->show_profile_following,
|
||||
],
|
||||
'followers' => [
|
||||
'count' => $settings->show_profile_follower_count,
|
||||
'list' => $settings->show_profile_followers,
|
||||
],
|
||||
];
|
||||
|
||||
if ($request->wantsJson() && config_cache('federation.activitypub.enabled')) {
|
||||
return $this->showActivityPub($request, $user);
|
||||
}
|
||||
return view('profile.show', compact('profile', 'settings'));
|
||||
} else {
|
||||
$key = 'profile:settings:'.$user->id;
|
||||
$ttl = now()->addHours(6);
|
||||
$settings = Cache::remember($key, $ttl, function () use ($user) {
|
||||
return $user->user->settings;
|
||||
});
|
||||
|
||||
return redirect($user->url());
|
||||
}
|
||||
if ($user->is_private == true) {
|
||||
$isPrivate = $this->privateProfileCheck($user, $loggedIn);
|
||||
}
|
||||
|
||||
protected function privateProfileCheck(Profile $profile, $loggedIn)
|
||||
{
|
||||
if (!Auth::check()) {
|
||||
return true;
|
||||
}
|
||||
$isBlocked = $this->blockedProfileCheck($user);
|
||||
|
||||
$user = Auth::user()->profile;
|
||||
if($user->id == $profile->id || !$profile->is_private) {
|
||||
return false;
|
||||
}
|
||||
$owner = $loggedIn && Auth::id() === $user->user_id;
|
||||
$is_following = ($owner == false && Auth::check()) ? $user->followedBy(Auth::user()->profile) : false;
|
||||
|
||||
$follows = Follower::whereProfileId($user->id)->whereFollowingId($profile->id)->exists();
|
||||
if ($follows == false) {
|
||||
return true;
|
||||
}
|
||||
if ($isPrivate == true || $isBlocked == true) {
|
||||
$requested = Auth::check() ? FollowRequest::whereFollowerId(Auth::user()->profile_id)
|
||||
->whereFollowingId($user->id)
|
||||
->exists() : false;
|
||||
|
||||
return false;
|
||||
}
|
||||
return view('profile.private', compact('user', 'is_following', 'requested'));
|
||||
}
|
||||
|
||||
public static function accountCheck(Profile $profile)
|
||||
{
|
||||
switch ($profile->status) {
|
||||
case 'disabled':
|
||||
case 'suspended':
|
||||
case 'delete':
|
||||
return view('profile.disabled');
|
||||
break;
|
||||
$is_admin = is_null($user->domain) ? $user->user->is_admin : false;
|
||||
$profile = $user;
|
||||
$settings = [
|
||||
'crawlable' => $settings->crawlable,
|
||||
'following' => [
|
||||
'count' => $settings->show_profile_following_count,
|
||||
'list' => $settings->show_profile_following,
|
||||
],
|
||||
'followers' => [
|
||||
'count' => $settings->show_profile_follower_count,
|
||||
'list' => $settings->show_profile_followers,
|
||||
],
|
||||
];
|
||||
|
||||
default:
|
||||
break;
|
||||
}
|
||||
return abort(404);
|
||||
}
|
||||
return view('profile.show', compact('profile', 'settings'));
|
||||
}
|
||||
}
|
||||
|
||||
protected function blockedProfileCheck(Profile $profile)
|
||||
{
|
||||
$pid = Auth::user()->profile->id;
|
||||
$blocks = UserFilter::whereUserId($profile->id)
|
||||
->whereFilterType('block')
|
||||
->whereFilterableType('App\Profile')
|
||||
->pluck('filterable_id')
|
||||
->toArray();
|
||||
if (in_array($pid, $blocks)) {
|
||||
return true;
|
||||
}
|
||||
protected function getCachedUser($username, $withTrashed = false)
|
||||
{
|
||||
$val = str_replace(['_', '.', '-'], '', $username);
|
||||
if (! ctype_alnum($val)) {
|
||||
return;
|
||||
}
|
||||
$hash = ($withTrashed ? 'wt:' : 'wot:').strtolower($username);
|
||||
|
||||
return false;
|
||||
}
|
||||
return Cache::remember('pfc:cached-user:'.$hash, ($withTrashed ? 14400 : 900), function () use ($username, $withTrashed) {
|
||||
if (! $withTrashed) {
|
||||
return Profile::whereNull(['domain', 'status'])
|
||||
->whereUsername($username)
|
||||
->first();
|
||||
} else {
|
||||
return Profile::withTrashed()
|
||||
->whereNull('domain')
|
||||
->whereUsername($username)
|
||||
->first();
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
public function showActivityPub(Request $request, $user)
|
||||
{
|
||||
abort_if(!config_cache('federation.activitypub.enabled'), 404);
|
||||
abort_if($user->domain, 404);
|
||||
public function permalinkRedirect(Request $request, $username)
|
||||
{
|
||||
if ($request->wantsJson() && (bool) config_cache('federation.activitypub.enabled')) {
|
||||
$user = $this->getCachedUser($username, true);
|
||||
|
||||
return Cache::remember('pf:activitypub:user-object:by-id:' . $user->id, 3600, function() use($user) {
|
||||
$fractal = new Fractal\Manager();
|
||||
$resource = new Fractal\Resource\Item($user, new ProfileTransformer);
|
||||
$res = $fractal->createData($resource)->toArray();
|
||||
return response(json_encode($res['data']))->header('Content-Type', 'application/activity+json');
|
||||
});
|
||||
}
|
||||
return $this->showActivityPub($request, $user);
|
||||
}
|
||||
|
||||
public function showAtomFeed(Request $request, $user)
|
||||
{
|
||||
abort_if(!config('federation.atom.enabled'), 404);
|
||||
$user = $this->getCachedUser($username);
|
||||
|
||||
$pid = AccountService::usernameToId($user);
|
||||
abort_if(! $user, 404);
|
||||
|
||||
abort_if(!$pid, 404);
|
||||
return redirect($user->url());
|
||||
}
|
||||
|
||||
$profile = AccountService::get($pid, true);
|
||||
protected function privateProfileCheck(Profile $profile, $loggedIn)
|
||||
{
|
||||
if (! Auth::check()) {
|
||||
return true;
|
||||
}
|
||||
|
||||
abort_if(!$profile || $profile['locked'] || !$profile['local'], 404);
|
||||
$user = Auth::user()->profile;
|
||||
if ($user->id == $profile->id || ! $profile->is_private) {
|
||||
return false;
|
||||
}
|
||||
|
||||
$data = Cache::remember('pf:atom:user-feed:by-id:' . $profile['id'], 43200, function() use($pid, $profile) {
|
||||
$items = DB::table('statuses')
|
||||
->whereProfileId($pid)
|
||||
->whereVisibility('public')
|
||||
->whereType('photo')
|
||||
->orderByDesc('id')
|
||||
->take(10)
|
||||
->get()
|
||||
->map(function($status) {
|
||||
return StatusService::get($status->id);
|
||||
})
|
||||
->filter(function($status) {
|
||||
return $status &&
|
||||
isset($status['account']) &&
|
||||
isset($status['media_attachments']) &&
|
||||
count($status['media_attachments']);
|
||||
})
|
||||
->values();
|
||||
$permalink = config('app.url') . "/users/{$profile['username']}.atom";
|
||||
$headers = ['Content-Type' => 'application/atom+xml'];
|
||||
$follows = Follower::whereProfileId($user->id)->whereFollowingId($profile->id)->exists();
|
||||
if ($follows == false) {
|
||||
return true;
|
||||
}
|
||||
|
||||
if($items && $items->count()) {
|
||||
$headers['Last-Modified'] = now()->parse($items->first()['created_at'])->toRfc7231String();
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
return compact('items', 'permalink', 'headers');
|
||||
});
|
||||
abort_if(!$data, 404);
|
||||
return response()
|
||||
->view('atom.user',
|
||||
[
|
||||
'profile' => $profile,
|
||||
'items' => $data['items'],
|
||||
'permalink' => $data['permalink']
|
||||
]
|
||||
)
|
||||
->withHeaders($data['headers']);
|
||||
}
|
||||
public static function accountCheck(Profile $profile)
|
||||
{
|
||||
switch ($profile->status) {
|
||||
case 'disabled':
|
||||
case 'suspended':
|
||||
case 'delete':
|
||||
return view('profile.disabled');
|
||||
break;
|
||||
|
||||
public function meRedirect()
|
||||
{
|
||||
abort_if(!Auth::check(), 404);
|
||||
return redirect(Auth::user()->url());
|
||||
}
|
||||
default:
|
||||
break;
|
||||
}
|
||||
|
||||
public function embed(Request $request, $username)
|
||||
{
|
||||
$res = view('profile.embed-removed');
|
||||
return abort(404);
|
||||
}
|
||||
|
||||
if(!config('instance.embed.profile')) {
|
||||
return response($res)->withHeaders(['X-Frame-Options' => 'ALLOWALL']);
|
||||
}
|
||||
protected function blockedProfileCheck(Profile $profile)
|
||||
{
|
||||
$pid = Auth::user()->profile->id;
|
||||
$blocks = UserFilter::whereUserId($profile->id)
|
||||
->whereFilterType('block')
|
||||
->whereFilterableType('App\Profile')
|
||||
->pluck('filterable_id')
|
||||
->toArray();
|
||||
if (in_array($pid, $blocks)) {
|
||||
return true;
|
||||
}
|
||||
|
||||
if(strlen($username) > 15 || strlen($username) < 2) {
|
||||
return response($res)->withHeaders(['X-Frame-Options' => 'ALLOWALL']);
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
$profile = Profile::whereUsername($username)
|
||||
->whereIsPrivate(false)
|
||||
->whereNull('status')
|
||||
->whereNull('domain')
|
||||
->first();
|
||||
public function showActivityPub(Request $request, $user)
|
||||
{
|
||||
abort_if(! config_cache('federation.activitypub.enabled'), 404);
|
||||
abort_if(! $user, 404, 'Not found');
|
||||
abort_if($user->domain, 404);
|
||||
|
||||
if(!$profile) {
|
||||
return response($res)->withHeaders(['X-Frame-Options' => 'ALLOWALL']);
|
||||
}
|
||||
return Cache::remember('pf:activitypub:user-object:by-id:'.$user->id, 1800, function () use ($user) {
|
||||
$fractal = new Fractal\Manager();
|
||||
$resource = new Fractal\Resource\Item($user, new ProfileTransformer);
|
||||
$res = $fractal->createData($resource)->toArray();
|
||||
|
||||
if(AccountService::canEmbed($profile->user_id) == false) {
|
||||
return response($res)->withHeaders(['X-Frame-Options' => 'ALLOWALL']);
|
||||
}
|
||||
return response(json_encode($res['data']))->header('Content-Type', 'application/activity+json');
|
||||
});
|
||||
}
|
||||
|
||||
$profile = AccountService::get($profile->id);
|
||||
$res = view('profile.embed', compact('profile'));
|
||||
return response($res)->withHeaders(['X-Frame-Options' => 'ALLOWALL']);
|
||||
}
|
||||
public function showAtomFeed(Request $request, $user)
|
||||
{
|
||||
abort_if(! config('federation.atom.enabled'), 404);
|
||||
|
||||
public function stories(Request $request, $username)
|
||||
{
|
||||
abort_if(!config_cache('instance.stories.enabled') || !$request->user(), 404);
|
||||
$profile = Profile::whereNull('domain')->whereUsername($username)->firstOrFail();
|
||||
$pid = $profile->id;
|
||||
$authed = Auth::user()->profile_id;
|
||||
abort_if($pid != $authed && !FollowerService::follows($authed, $pid), 404);
|
||||
$exists = Story::whereProfileId($pid)
|
||||
->whereActive(true)
|
||||
->exists();
|
||||
abort_unless($exists, 404);
|
||||
return view('profile.story', compact('pid', 'profile'));
|
||||
}
|
||||
$pid = AccountService::usernameToId($user);
|
||||
|
||||
abort_if(! $pid, 404);
|
||||
|
||||
$profile = AccountService::get($pid, true);
|
||||
|
||||
abort_if(! $profile || $profile['locked'] || ! $profile['local'], 404);
|
||||
|
||||
$aiCheck = Cache::remember('profile:ai-check:spam-login:'.$profile['id'], 3600, function () use ($profile) {
|
||||
$uid = User::whereProfileId($profile['id'])->first();
|
||||
if (! $uid) {
|
||||
return true;
|
||||
}
|
||||
$exists = AccountInterstitial::whereUserId($uid->id)->where('is_spam', 1)->count();
|
||||
if ($exists) {
|
||||
return true;
|
||||
}
|
||||
|
||||
return false;
|
||||
});
|
||||
|
||||
abort_if($aiCheck, 404);
|
||||
|
||||
$enabled = Cache::remember('profile:atom:enabled:'.$profile['id'], 84600, function () use ($profile) {
|
||||
$uid = User::whereProfileId($profile['id'])->first();
|
||||
if (! $uid) {
|
||||
return false;
|
||||
}
|
||||
$settings = UserSetting::whereUserId($uid->id)->first();
|
||||
if (! $settings) {
|
||||
return false;
|
||||
}
|
||||
|
||||
return $settings->show_atom;
|
||||
});
|
||||
|
||||
abort_if(! $enabled, 404);
|
||||
|
||||
$data = Cache::remember('pf:atom:user-feed:by-id:'.$profile['id'], 14400, function () use ($pid, $profile) {
|
||||
$items = Status::whereProfileId($pid)
|
||||
->whereScope('public')
|
||||
->whereIn('type', ['photo', 'photo:album'])
|
||||
->orderByDesc('id')
|
||||
->take(10)
|
||||
->get()
|
||||
->map(function ($status) {
|
||||
return StatusService::get($status->id, true);
|
||||
})
|
||||
->filter(function ($status) {
|
||||
return $status &&
|
||||
isset($status['account']) &&
|
||||
isset($status['media_attachments']) &&
|
||||
count($status['media_attachments']);
|
||||
})
|
||||
->values();
|
||||
$permalink = config('app.url')."/users/{$profile['username']}.atom";
|
||||
$headers = ['Content-Type' => 'application/atom+xml'];
|
||||
|
||||
if ($items && $items->count()) {
|
||||
$headers['Last-Modified'] = now()->parse($items->first()['created_at'])->toRfc7231String();
|
||||
}
|
||||
|
||||
return compact('items', 'permalink', 'headers');
|
||||
});
|
||||
abort_if(! $data || ! isset($data['items']) || ! isset($data['permalink']), 404);
|
||||
|
||||
return response()
|
||||
->view('atom.user',
|
||||
[
|
||||
'profile' => $profile,
|
||||
'items' => $data['items'],
|
||||
'permalink' => $data['permalink'],
|
||||
]
|
||||
)
|
||||
->withHeaders($data['headers']);
|
||||
}
|
||||
|
||||
public function meRedirect()
|
||||
{
|
||||
abort_if(! Auth::check(), 404);
|
||||
|
||||
return redirect(Auth::user()->url());
|
||||
}
|
||||
|
||||
public function embed(Request $request, $username)
|
||||
{
|
||||
$res = view('profile.embed-removed');
|
||||
|
||||
if (! (bool) config_cache('instance.embed.profile')) {
|
||||
return response($res)->withHeaders(['X-Frame-Options' => 'ALLOWALL']);
|
||||
}
|
||||
|
||||
if (strlen($username) > 15 || strlen($username) < 2) {
|
||||
return response($res)->withHeaders(['X-Frame-Options' => 'ALLOWALL']);
|
||||
}
|
||||
|
||||
$profile = $this->getCachedUser($username);
|
||||
|
||||
if (! $profile || $profile->is_private) {
|
||||
return response($res)->withHeaders(['X-Frame-Options' => 'ALLOWALL']);
|
||||
}
|
||||
|
||||
$aiCheck = Cache::remember('profile:ai-check:spam-login:'.$profile->id, 3600, function () use ($profile) {
|
||||
$exists = AccountInterstitial::whereUserId($profile->user_id)->where('is_spam', 1)->count();
|
||||
if ($exists) {
|
||||
return true;
|
||||
}
|
||||
|
||||
return false;
|
||||
});
|
||||
|
||||
if ($aiCheck) {
|
||||
return response($res)->withHeaders(['X-Frame-Options' => 'ALLOWALL']);
|
||||
}
|
||||
|
||||
if (AccountService::canEmbed($profile->user_id) == false) {
|
||||
return response($res)->withHeaders(['X-Frame-Options' => 'ALLOWALL']);
|
||||
}
|
||||
|
||||
$profile = AccountService::get($profile->id);
|
||||
$res = view('profile.embed', compact('profile'));
|
||||
|
||||
return response($res)->withHeaders(['X-Frame-Options' => 'ALLOWALL']);
|
||||
}
|
||||
|
||||
public function stories(Request $request, $username)
|
||||
{
|
||||
abort_if(! (bool) config_cache('instance.stories.enabled') || ! $request->user(), 404);
|
||||
$profile = Profile::whereNull('domain')->whereUsername($username)->firstOrFail();
|
||||
$pid = $profile->id;
|
||||
$authed = Auth::user()->profile_id;
|
||||
abort_if($pid != $authed && ! FollowerService::follows($authed, $pid), 404);
|
||||
$exists = Story::whereProfileId($pid)
|
||||
->whereActive(true)
|
||||
->exists();
|
||||
abort_unless($exists, 404);
|
||||
|
||||
return view('profile.story', compact('pid', 'profile'));
|
||||
}
|
||||
}
|
||||
|
|
|
@ -0,0 +1,72 @@
|
|||
<?php
|
||||
|
||||
namespace App\Http\Controllers;
|
||||
|
||||
use App\Http\Requests\ProfileMigrationStoreRequest;
|
||||
use App\Jobs\ProfilePipeline\ProfileMigrationDeliverMoveActivityPipeline;
|
||||
use App\Jobs\ProfilePipeline\ProfileMigrationMoveFollowersPipeline;
|
||||
use App\Models\ProfileAlias;
|
||||
use App\Models\ProfileMigration;
|
||||
use App\Services\AccountService;
|
||||
use App\Services\WebfingerService;
|
||||
use App\Util\ActivityPub\Helpers;
|
||||
use Illuminate\Http\Request;
|
||||
use Illuminate\Support\Facades\Bus;
|
||||
|
||||
class ProfileMigrationController extends Controller
|
||||
{
|
||||
public function __construct()
|
||||
{
|
||||
$this->middleware('auth');
|
||||
}
|
||||
|
||||
public function index(Request $request)
|
||||
{
|
||||
abort_if((bool) config_cache('federation.activitypub.enabled') === false, 404);
|
||||
if ((bool) config_cache('federation.migration') === false) {
|
||||
return redirect(route('help.account-migration'));
|
||||
}
|
||||
$hasExistingMigration = ProfileMigration::whereProfileId($request->user()->profile_id)
|
||||
->where('created_at', '>', now()->subDays(30))
|
||||
->exists();
|
||||
|
||||
return view('settings.migration.index', compact('hasExistingMigration'));
|
||||
}
|
||||
|
||||
public function store(ProfileMigrationStoreRequest $request)
|
||||
{
|
||||
abort_if((bool) config_cache('federation.activitypub.enabled') === false, 404);
|
||||
$acct = WebfingerService::rawGet($request->safe()->acct);
|
||||
if (! $acct) {
|
||||
return redirect()->back()->withErrors(['acct' => 'The new account you provided is not responding to our requests.']);
|
||||
}
|
||||
$newAccount = Helpers::profileFetch($acct);
|
||||
if (! $newAccount) {
|
||||
return redirect()->back()->withErrors(['acct' => 'An error occured, please try again later. Code: res-failed-account-fetch']);
|
||||
}
|
||||
$user = $request->user();
|
||||
ProfileAlias::updateOrCreate([
|
||||
'profile_id' => $user->profile_id,
|
||||
'acct' => $request->safe()->acct,
|
||||
'uri' => $acct,
|
||||
]);
|
||||
$migration = ProfileMigration::create([
|
||||
'profile_id' => $request->user()->profile_id,
|
||||
'acct' => $request->safe()->acct,
|
||||
'followers_count' => $request->user()->profile->followers_count,
|
||||
'target_profile_id' => $newAccount['id'],
|
||||
]);
|
||||
$user->profile->update([
|
||||
'moved_to_profile_id' => $newAccount->id,
|
||||
'indexable' => false,
|
||||
]);
|
||||
AccountService::del($user->profile_id);
|
||||
|
||||
Bus::batch([
|
||||
new ProfileMigrationDeliverMoveActivityPipeline($migration, $user->profile, $newAccount),
|
||||
new ProfileMigrationMoveFollowersPipeline($user->profile_id, $newAccount->id),
|
||||
])->onQueue('follow')->dispatch();
|
||||
|
||||
return redirect()->back()->with(['status' => 'Succesfully migrated account!']);
|
||||
}
|
||||
}
|
|
@ -42,6 +42,7 @@ use App\Services\{
|
|||
use App\Jobs\StatusPipeline\NewStatusPipeline;
|
||||
use League\Fractal\Serializer\ArraySerializer;
|
||||
use League\Fractal\Pagination\IlluminatePaginatorAdapter;
|
||||
use App\Services\InstanceService;
|
||||
|
||||
class PublicApiController extends Controller
|
||||
{
|
||||
|
@ -661,6 +662,10 @@ class PublicApiController extends Controller
|
|||
public function account(Request $request, $id)
|
||||
{
|
||||
$res = AccountService::get($id);
|
||||
if($res && isset($res['local'], $res['url']) && !$res['local']) {
|
||||
$domain = parse_url($res['url'], PHP_URL_HOST);
|
||||
abort_if(in_array($domain, InstanceService::getBannedDomains()), 404);
|
||||
}
|
||||
return response()->json($res);
|
||||
}
|
||||
|
||||
|
@ -680,6 +685,11 @@ class PublicApiController extends Controller
|
|||
$profile = AccountService::get($id);
|
||||
abort_if(!$profile, 404);
|
||||
|
||||
if($profile && isset($profile['local'], $profile['url']) && !$profile['local']) {
|
||||
$domain = parse_url($profile['url'], PHP_URL_HOST);
|
||||
abort_if(in_array($domain, InstanceService::getBannedDomains()), 404);
|
||||
}
|
||||
|
||||
$limit = $request->limit ?? 9;
|
||||
$max_id = $request->max_id;
|
||||
$min_id = $request->min_id;
|
||||
|
|
|
@ -0,0 +1,728 @@
|
|||
<?php
|
||||
|
||||
namespace App\Http\Controllers;
|
||||
|
||||
use App\Models\RemoteAuth;
|
||||
use App\Services\Account\RemoteAuthService;
|
||||
use App\Services\EmailService;
|
||||
use App\Services\MediaStorageService;
|
||||
use App\User;
|
||||
use App\Util\ActivityPub\Helpers;
|
||||
use App\Util\Lexer\RestrictedNames;
|
||||
use Illuminate\Auth\Events\Registered;
|
||||
use Illuminate\Http\Request;
|
||||
use Illuminate\Support\Facades\Auth;
|
||||
use Illuminate\Support\Facades\Hash;
|
||||
use Illuminate\Support\Str;
|
||||
use InvalidArgumentException;
|
||||
use Purify;
|
||||
|
||||
class RemoteAuthController extends Controller
|
||||
{
|
||||
public function start(Request $request)
|
||||
{
|
||||
abort_unless((
|
||||
config_cache('pixelfed.open_registration') &&
|
||||
config('remote-auth.mastodon.enabled')
|
||||
) || (
|
||||
config('remote-auth.mastodon.ignore_closed_state') &&
|
||||
config('remote-auth.mastodon.enabled')
|
||||
), 404);
|
||||
if ($request->user()) {
|
||||
return redirect('/');
|
||||
}
|
||||
|
||||
return view('auth.remote.start');
|
||||
}
|
||||
|
||||
public function startRedirect(Request $request)
|
||||
{
|
||||
return redirect('/login');
|
||||
}
|
||||
|
||||
public function getAuthDomains(Request $request)
|
||||
{
|
||||
abort_unless((
|
||||
config_cache('pixelfed.open_registration') &&
|
||||
config('remote-auth.mastodon.enabled')
|
||||
) || (
|
||||
config('remote-auth.mastodon.ignore_closed_state') &&
|
||||
config('remote-auth.mastodon.enabled')
|
||||
), 404);
|
||||
|
||||
if (config('remote-auth.mastodon.domains.only_custom')) {
|
||||
$res = config('remote-auth.mastodon.domains.custom');
|
||||
if (! $res || ! strlen($res)) {
|
||||
return [];
|
||||
}
|
||||
$res = explode(',', $res);
|
||||
|
||||
return response()->json($res);
|
||||
}
|
||||
|
||||
if (config('remote-auth.mastodon.domains.custom') &&
|
||||
! config('remote-auth.mastodon.domains.only_default') &&
|
||||
strlen(config('remote-auth.mastodon.domains.custom')) > 3 &&
|
||||
strpos(config('remote-auth.mastodon.domains.custom'), '.') > -1
|
||||
) {
|
||||
$res = config('remote-auth.mastodon.domains.custom');
|
||||
if (! $res || ! strlen($res)) {
|
||||
return [];
|
||||
}
|
||||
$res = explode(',', $res);
|
||||
|
||||
return response()->json($res);
|
||||
}
|
||||
|
||||
$res = config('remote-auth.mastodon.domains.default');
|
||||
$res = explode(',', $res);
|
||||
|
||||
return response()->json($res);
|
||||
}
|
||||
|
||||
public function redirect(Request $request)
|
||||
{
|
||||
abort_unless((
|
||||
config_cache('pixelfed.open_registration') &&
|
||||
config('remote-auth.mastodon.enabled')
|
||||
) || (
|
||||
config('remote-auth.mastodon.ignore_closed_state') &&
|
||||
config('remote-auth.mastodon.enabled')
|
||||
), 404);
|
||||
|
||||
$this->validate($request, ['domain' => 'required']);
|
||||
|
||||
$domain = $request->input('domain');
|
||||
|
||||
if (str_starts_with(strtolower($domain), 'http')) {
|
||||
$res = [
|
||||
'domain' => $domain,
|
||||
'ready' => false,
|
||||
'action' => 'incompatible_domain',
|
||||
];
|
||||
|
||||
return response()->json($res);
|
||||
}
|
||||
|
||||
$validateInstance = Helpers::validateUrl('https://'.$domain.'/?block-check='.time());
|
||||
|
||||
if (! $validateInstance) {
|
||||
$res = [
|
||||
'domain' => $domain,
|
||||
'ready' => false,
|
||||
'action' => 'blocked_domain',
|
||||
];
|
||||
|
||||
return response()->json($res);
|
||||
}
|
||||
|
||||
$compatible = RemoteAuthService::isDomainCompatible($domain);
|
||||
|
||||
if (! $compatible) {
|
||||
$res = [
|
||||
'domain' => $domain,
|
||||
'ready' => false,
|
||||
'action' => 'incompatible_domain',
|
||||
];
|
||||
|
||||
return response()->json($res);
|
||||
}
|
||||
|
||||
if (config('remote-auth.mastodon.domains.only_default')) {
|
||||
$defaultDomains = explode(',', config('remote-auth.mastodon.domains.default'));
|
||||
if (! in_array($domain, $defaultDomains)) {
|
||||
$res = [
|
||||
'domain' => $domain,
|
||||
'ready' => false,
|
||||
'action' => 'incompatible_domain',
|
||||
];
|
||||
|
||||
return response()->json($res);
|
||||
}
|
||||
}
|
||||
|
||||
if (config('remote-auth.mastodon.domains.only_custom') && config('remote-auth.mastodon.domains.custom')) {
|
||||
$customDomains = explode(',', config('remote-auth.mastodon.domains.custom'));
|
||||
if (! in_array($domain, $customDomains)) {
|
||||
$res = [
|
||||
'domain' => $domain,
|
||||
'ready' => false,
|
||||
'action' => 'incompatible_domain',
|
||||
];
|
||||
|
||||
return response()->json($res);
|
||||
}
|
||||
}
|
||||
|
||||
$client = RemoteAuthService::getMastodonClient($domain);
|
||||
|
||||
abort_unless($client, 422, 'Invalid mastodon client');
|
||||
|
||||
$request->session()->put('state', $state = Str::random(40));
|
||||
$request->session()->put('oauth_domain', $domain);
|
||||
|
||||
$query = http_build_query([
|
||||
'client_id' => $client->client_id,
|
||||
'redirect_uri' => $client->redirect_uri,
|
||||
'response_type' => 'code',
|
||||
'scope' => 'read',
|
||||
'state' => $state,
|
||||
]);
|
||||
|
||||
$request->session()->put('oauth_redirect_to', 'https://'.$domain.'/oauth/authorize?'.$query);
|
||||
|
||||
$dsh = Str::random(17);
|
||||
$res = [
|
||||
'domain' => $domain,
|
||||
'ready' => true,
|
||||
'dsh' => $dsh,
|
||||
];
|
||||
|
||||
return response()->json($res);
|
||||
}
|
||||
|
||||
public function preflight(Request $request)
|
||||
{
|
||||
abort_unless((
|
||||
config_cache('pixelfed.open_registration') &&
|
||||
config('remote-auth.mastodon.enabled')
|
||||
) || (
|
||||
config('remote-auth.mastodon.ignore_closed_state') &&
|
||||
config('remote-auth.mastodon.enabled')
|
||||
), 404);
|
||||
|
||||
if (! $request->filled('d') || ! $request->filled('dsh') || ! $request->session()->exists('oauth_redirect_to')) {
|
||||
return redirect('/login');
|
||||
}
|
||||
|
||||
return redirect()->away($request->session()->pull('oauth_redirect_to'));
|
||||
}
|
||||
|
||||
public function handleCallback(Request $request)
|
||||
{
|
||||
abort_unless((
|
||||
config_cache('pixelfed.open_registration') &&
|
||||
config('remote-auth.mastodon.enabled')
|
||||
) || (
|
||||
config('remote-auth.mastodon.ignore_closed_state') &&
|
||||
config('remote-auth.mastodon.enabled')
|
||||
), 404);
|
||||
|
||||
$domain = $request->session()->get('oauth_domain');
|
||||
|
||||
if ($request->filled('code')) {
|
||||
$code = $request->input('code');
|
||||
$state = $request->session()->pull('state');
|
||||
|
||||
throw_unless(
|
||||
strlen($state) > 0 && $state === $request->state,
|
||||
InvalidArgumentException::class,
|
||||
'Invalid state value.'
|
||||
);
|
||||
|
||||
$res = RemoteAuthService::getToken($domain, $code);
|
||||
|
||||
if (! $res || ! isset($res['access_token'])) {
|
||||
$request->session()->regenerate();
|
||||
|
||||
return redirect('/login');
|
||||
}
|
||||
|
||||
$request->session()->put('oauth_remote_session_token', $res['access_token']);
|
||||
|
||||
return redirect('/auth/mastodon/getting-started');
|
||||
}
|
||||
|
||||
return redirect('/login');
|
||||
}
|
||||
|
||||
public function onboarding(Request $request)
|
||||
{
|
||||
abort_unless((
|
||||
config_cache('pixelfed.open_registration') &&
|
||||
config('remote-auth.mastodon.enabled')
|
||||
) || (
|
||||
config('remote-auth.mastodon.ignore_closed_state') &&
|
||||
config('remote-auth.mastodon.enabled')
|
||||
), 404);
|
||||
if ($request->user()) {
|
||||
return redirect('/');
|
||||
}
|
||||
|
||||
return view('auth.remote.onboarding');
|
||||
}
|
||||
|
||||
public function sessionCheck(Request $request)
|
||||
{
|
||||
abort_unless((
|
||||
config_cache('pixelfed.open_registration') &&
|
||||
config('remote-auth.mastodon.enabled')
|
||||
) || (
|
||||
config('remote-auth.mastodon.ignore_closed_state') &&
|
||||
config('remote-auth.mastodon.enabled')
|
||||
), 404);
|
||||
abort_if($request->user(), 403);
|
||||
abort_unless($request->session()->exists('oauth_domain'), 403);
|
||||
abort_unless($request->session()->exists('oauth_remote_session_token'), 403);
|
||||
|
||||
$domain = $request->session()->get('oauth_domain');
|
||||
$token = $request->session()->get('oauth_remote_session_token');
|
||||
|
||||
$res = RemoteAuthService::getVerifyCredentials($domain, $token);
|
||||
|
||||
abort_if(! $res || ! isset($res['acct']), 403, 'Invalid credentials');
|
||||
|
||||
$webfinger = strtolower('@'.$res['acct'].'@'.$domain);
|
||||
$request->session()->put('oauth_masto_webfinger', $webfinger);
|
||||
|
||||
if (config('remote-auth.mastodon.max_uses.enabled')) {
|
||||
$limit = config('remote-auth.mastodon.max_uses.limit');
|
||||
$uses = RemoteAuthService::lookupWebfingerUses($webfinger);
|
||||
if ($uses >= $limit) {
|
||||
return response()->json([
|
||||
'code' => 200,
|
||||
'msg' => 'Success!',
|
||||
'action' => 'max_uses_reached',
|
||||
]);
|
||||
}
|
||||
}
|
||||
|
||||
$exists = RemoteAuth::whereDomain($domain)->where('webfinger', $webfinger)->whereNotNull('user_id')->first();
|
||||
if ($exists && $exists->user_id) {
|
||||
return response()->json([
|
||||
'code' => 200,
|
||||
'msg' => 'Success!',
|
||||
'action' => 'redirect_existing_user',
|
||||
]);
|
||||
}
|
||||
|
||||
return response()->json([
|
||||
'code' => 200,
|
||||
'msg' => 'Success!',
|
||||
'action' => 'onboard',
|
||||
]);
|
||||
}
|
||||
|
||||
public function sessionGetMastodonData(Request $request)
|
||||
{
|
||||
abort_unless((
|
||||
config_cache('pixelfed.open_registration') &&
|
||||
config('remote-auth.mastodon.enabled')
|
||||
) || (
|
||||
config('remote-auth.mastodon.ignore_closed_state') &&
|
||||
config('remote-auth.mastodon.enabled')
|
||||
), 404);
|
||||
abort_if($request->user(), 403);
|
||||
abort_unless($request->session()->exists('oauth_domain'), 403);
|
||||
abort_unless($request->session()->exists('oauth_remote_session_token'), 403);
|
||||
|
||||
$domain = $request->session()->get('oauth_domain');
|
||||
$token = $request->session()->get('oauth_remote_session_token');
|
||||
|
||||
$res = RemoteAuthService::getVerifyCredentials($domain, $token);
|
||||
$res['_webfinger'] = strtolower('@'.$res['acct'].'@'.$domain);
|
||||
$res['_domain'] = strtolower($domain);
|
||||
$request->session()->put('oauth_remasto_id', $res['id']);
|
||||
|
||||
$ra = RemoteAuth::updateOrCreate([
|
||||
'domain' => $domain,
|
||||
'webfinger' => $res['_webfinger'],
|
||||
], [
|
||||
'software' => 'mastodon',
|
||||
'ip_address' => $request->ip(),
|
||||
'bearer_token' => $token,
|
||||
'verify_credentials' => $res,
|
||||
'last_verify_credentials_at' => now(),
|
||||
'last_successful_login_at' => now(),
|
||||
]);
|
||||
|
||||
$request->session()->put('oauth_masto_raid', $ra->id);
|
||||
|
||||
return response()->json($res);
|
||||
}
|
||||
|
||||
public function sessionValidateUsername(Request $request)
|
||||
{
|
||||
abort_unless((
|
||||
config_cache('pixelfed.open_registration') &&
|
||||
config('remote-auth.mastodon.enabled')
|
||||
) || (
|
||||
config('remote-auth.mastodon.ignore_closed_state') &&
|
||||
config('remote-auth.mastodon.enabled')
|
||||
), 404);
|
||||
abort_if($request->user(), 403);
|
||||
abort_unless($request->session()->exists('oauth_domain'), 403);
|
||||
abort_unless($request->session()->exists('oauth_remote_session_token'), 403);
|
||||
|
||||
$this->validate($request, [
|
||||
'username' => [
|
||||
'required',
|
||||
'min:2',
|
||||
'max:15',
|
||||
function ($attribute, $value, $fail) {
|
||||
$dash = substr_count($value, '-');
|
||||
$underscore = substr_count($value, '_');
|
||||
$period = substr_count($value, '.');
|
||||
|
||||
if (ends_with($value, ['.php', '.js', '.css'])) {
|
||||
return $fail('Username is invalid.');
|
||||
}
|
||||
|
||||
if (($dash + $underscore + $period) > 1) {
|
||||
return $fail('Username is invalid. Can only contain one dash (-), period (.) or underscore (_).');
|
||||
}
|
||||
|
||||
if (! ctype_alnum($value[0])) {
|
||||
return $fail('Username is invalid. Must start with a letter or number.');
|
||||
}
|
||||
|
||||
if (! ctype_alnum($value[strlen($value) - 1])) {
|
||||
return $fail('Username is invalid. Must end with a letter or number.');
|
||||
}
|
||||
|
||||
$val = str_replace(['_', '.', '-'], '', $value);
|
||||
if (! ctype_alnum($val)) {
|
||||
return $fail('Username is invalid. Username must be alpha-numeric and may contain dashes (-), periods (.) and underscores (_).');
|
||||
}
|
||||
|
||||
$restricted = RestrictedNames::get();
|
||||
if (in_array(strtolower($value), array_map('strtolower', $restricted))) {
|
||||
return $fail('Username cannot be used.');
|
||||
}
|
||||
},
|
||||
],
|
||||
]);
|
||||
$username = strtolower($request->input('username'));
|
||||
|
||||
$exists = User::where('username', $username)->exists();
|
||||
|
||||
return response()->json([
|
||||
'code' => 200,
|
||||
'username' => $username,
|
||||
'exists' => $exists,
|
||||
]);
|
||||
}
|
||||
|
||||
public function sessionValidateEmail(Request $request)
|
||||
{
|
||||
abort_unless((
|
||||
config_cache('pixelfed.open_registration') &&
|
||||
config('remote-auth.mastodon.enabled')
|
||||
) || (
|
||||
config('remote-auth.mastodon.ignore_closed_state') &&
|
||||
config('remote-auth.mastodon.enabled')
|
||||
), 404);
|
||||
abort_if($request->user(), 403);
|
||||
abort_unless($request->session()->exists('oauth_domain'), 403);
|
||||
abort_unless($request->session()->exists('oauth_remote_session_token'), 403);
|
||||
|
||||
$this->validate($request, [
|
||||
'email' => [
|
||||
'required',
|
||||
'email:strict,filter_unicode,dns,spoof',
|
||||
],
|
||||
]);
|
||||
|
||||
$email = $request->input('email');
|
||||
$banned = EmailService::isBanned($email);
|
||||
$exists = User::where('email', $email)->exists();
|
||||
|
||||
return response()->json([
|
||||
'code' => 200,
|
||||
'email' => $email,
|
||||
'exists' => $exists,
|
||||
'banned' => $banned,
|
||||
]);
|
||||
}
|
||||
|
||||
public function sessionGetMastodonFollowers(Request $request)
|
||||
{
|
||||
abort_unless((
|
||||
config_cache('pixelfed.open_registration') &&
|
||||
config('remote-auth.mastodon.enabled')
|
||||
) || (
|
||||
config('remote-auth.mastodon.ignore_closed_state') &&
|
||||
config('remote-auth.mastodon.enabled')
|
||||
), 404);
|
||||
abort_unless($request->session()->exists('oauth_domain'), 403);
|
||||
abort_unless($request->session()->exists('oauth_remote_session_token'), 403);
|
||||
abort_unless($request->session()->exists('oauth_remasto_id'), 403);
|
||||
|
||||
$domain = $request->session()->get('oauth_domain');
|
||||
$token = $request->session()->get('oauth_remote_session_token');
|
||||
$id = $request->session()->get('oauth_remasto_id');
|
||||
|
||||
$res = RemoteAuthService::getFollowing($domain, $token, $id);
|
||||
|
||||
if (! $res) {
|
||||
return response()->json([
|
||||
'code' => 200,
|
||||
'following' => [],
|
||||
]);
|
||||
}
|
||||
|
||||
$res = collect($res)->filter(fn ($acct) => Helpers::validateUrl($acct['url']))->values()->toArray();
|
||||
|
||||
return response()->json([
|
||||
'code' => 200,
|
||||
'following' => $res,
|
||||
]);
|
||||
}
|
||||
|
||||
public function handleSubmit(Request $request)
|
||||
{
|
||||
abort_unless((
|
||||
config_cache('pixelfed.open_registration') &&
|
||||
config('remote-auth.mastodon.enabled')
|
||||
) || (
|
||||
config('remote-auth.mastodon.ignore_closed_state') &&
|
||||
config('remote-auth.mastodon.enabled')
|
||||
), 404);
|
||||
abort_unless($request->session()->exists('oauth_domain'), 403);
|
||||
abort_unless($request->session()->exists('oauth_remote_session_token'), 403);
|
||||
abort_unless($request->session()->exists('oauth_remasto_id'), 403);
|
||||
abort_unless($request->session()->exists('oauth_masto_webfinger'), 403);
|
||||
abort_unless($request->session()->exists('oauth_masto_raid'), 403);
|
||||
|
||||
$this->validate($request, [
|
||||
'email' => 'required|email:strict,filter_unicode,dns,spoof',
|
||||
'username' => [
|
||||
'required',
|
||||
'min:2',
|
||||
'max:15',
|
||||
'unique:users,username',
|
||||
function ($attribute, $value, $fail) {
|
||||
$dash = substr_count($value, '-');
|
||||
$underscore = substr_count($value, '_');
|
||||
$period = substr_count($value, '.');
|
||||
|
||||
if (ends_with($value, ['.php', '.js', '.css'])) {
|
||||
return $fail('Username is invalid.');
|
||||
}
|
||||
|
||||
if (($dash + $underscore + $period) > 1) {
|
||||
return $fail('Username is invalid. Can only contain one dash (-), period (.) or underscore (_).');
|
||||
}
|
||||
|
||||
if (! ctype_alnum($value[0])) {
|
||||
return $fail('Username is invalid. Must start with a letter or number.');
|
||||
}
|
||||
|
||||
if (! ctype_alnum($value[strlen($value) - 1])) {
|
||||
return $fail('Username is invalid. Must end with a letter or number.');
|
||||
}
|
||||
|
||||
$val = str_replace(['_', '.', '-'], '', $value);
|
||||
if (! ctype_alnum($val)) {
|
||||
return $fail('Username is invalid. Username must be alpha-numeric and may contain dashes (-), periods (.) and underscores (_).');
|
||||
}
|
||||
|
||||
$restricted = RestrictedNames::get();
|
||||
if (in_array(strtolower($value), array_map('strtolower', $restricted))) {
|
||||
return $fail('Username cannot be used.');
|
||||
}
|
||||
},
|
||||
],
|
||||
'password' => 'required|string|min:8|confirmed',
|
||||
'name' => 'nullable|max:30',
|
||||
]);
|
||||
|
||||
$email = $request->input('email');
|
||||
$username = $request->input('username');
|
||||
$password = $request->input('password');
|
||||
$name = $request->input('name');
|
||||
|
||||
$user = $this->createUser([
|
||||
'name' => $name,
|
||||
'username' => $username,
|
||||
'password' => $password,
|
||||
'email' => $email,
|
||||
]);
|
||||
|
||||
$raid = $request->session()->pull('oauth_masto_raid');
|
||||
$webfinger = $request->session()->pull('oauth_masto_webfinger');
|
||||
$token = $user->createToken('Onboarding')->accessToken;
|
||||
|
||||
$ra = RemoteAuth::where('id', $raid)->where('webfinger', $webfinger)->firstOrFail();
|
||||
$ra->user_id = $user->id;
|
||||
$ra->save();
|
||||
|
||||
return [
|
||||
'code' => 200,
|
||||
'msg' => 'Success',
|
||||
'token' => $token,
|
||||
];
|
||||
}
|
||||
|
||||
public function storeBio(Request $request)
|
||||
{
|
||||
abort_unless((
|
||||
config_cache('pixelfed.open_registration') &&
|
||||
config('remote-auth.mastodon.enabled')
|
||||
) || (
|
||||
config('remote-auth.mastodon.ignore_closed_state') &&
|
||||
config('remote-auth.mastodon.enabled')
|
||||
), 404);
|
||||
abort_unless($request->user(), 404);
|
||||
abort_unless($request->session()->exists('oauth_domain'), 403);
|
||||
abort_unless($request->session()->exists('oauth_remote_session_token'), 403);
|
||||
abort_unless($request->session()->exists('oauth_remasto_id'), 403);
|
||||
|
||||
$this->validate($request, [
|
||||
'bio' => 'required|nullable|max:500',
|
||||
]);
|
||||
|
||||
$profile = $request->user()->profile;
|
||||
$profile->bio = Purify::clean($request->input('bio'));
|
||||
$profile->save();
|
||||
|
||||
return [200];
|
||||
}
|
||||
|
||||
public function accountToId(Request $request)
|
||||
{
|
||||
abort_unless((
|
||||
config_cache('pixelfed.open_registration') &&
|
||||
config('remote-auth.mastodon.enabled')
|
||||
) || (
|
||||
config('remote-auth.mastodon.ignore_closed_state') &&
|
||||
config('remote-auth.mastodon.enabled')
|
||||
), 404);
|
||||
abort_if($request->user(), 404);
|
||||
abort_unless($request->session()->exists('oauth_domain'), 403);
|
||||
abort_unless($request->session()->exists('oauth_remote_session_token'), 403);
|
||||
abort_unless($request->session()->exists('oauth_remasto_id'), 403);
|
||||
|
||||
$this->validate($request, [
|
||||
'account' => 'required|url',
|
||||
]);
|
||||
|
||||
$account = $request->input('account');
|
||||
abort_unless(substr(strtolower($account), 0, 8) === 'https://', 404);
|
||||
|
||||
$host = strtolower(config('pixelfed.domain.app'));
|
||||
$domain = strtolower(parse_url($account, PHP_URL_HOST));
|
||||
|
||||
if ($domain == $host) {
|
||||
$username = Str::of($account)->explode('/')->last();
|
||||
$user = User::where('username', $username)->first();
|
||||
if ($user) {
|
||||
return ['id' => (string) $user->profile_id];
|
||||
} else {
|
||||
return [];
|
||||
}
|
||||
} else {
|
||||
try {
|
||||
$profile = Helpers::profileFetch($account);
|
||||
if ($profile) {
|
||||
return ['id' => (string) $profile->id];
|
||||
} else {
|
||||
return [];
|
||||
}
|
||||
} catch (\GuzzleHttp\Exception\RequestException $e) {
|
||||
return;
|
||||
} catch (Exception $e) {
|
||||
return [];
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public function storeAvatar(Request $request)
|
||||
{
|
||||
abort_unless((
|
||||
config_cache('pixelfed.open_registration') &&
|
||||
config('remote-auth.mastodon.enabled')
|
||||
) || (
|
||||
config('remote-auth.mastodon.ignore_closed_state') &&
|
||||
config('remote-auth.mastodon.enabled')
|
||||
), 404);
|
||||
abort_unless($request->user(), 404);
|
||||
$this->validate($request, [
|
||||
'avatar_url' => 'required|active_url',
|
||||
]);
|
||||
|
||||
$user = $request->user();
|
||||
$profile = $user->profile;
|
||||
|
||||
abort_if(! $profile->avatar, 404, 'Missing avatar');
|
||||
|
||||
$avatar = $profile->avatar;
|
||||
$avatar->remote_url = $request->input('avatar_url');
|
||||
$avatar->save();
|
||||
|
||||
MediaStorageService::avatar($avatar, (bool) config_cache('pixelfed.cloud_storage') == false);
|
||||
|
||||
return [200];
|
||||
}
|
||||
|
||||
public function finishUp(Request $request)
|
||||
{
|
||||
abort_unless((
|
||||
config_cache('pixelfed.open_registration') &&
|
||||
config('remote-auth.mastodon.enabled')
|
||||
) || (
|
||||
config('remote-auth.mastodon.ignore_closed_state') &&
|
||||
config('remote-auth.mastodon.enabled')
|
||||
), 404);
|
||||
abort_unless($request->user(), 404);
|
||||
|
||||
$currentWebfinger = '@'.$request->user()->username.'@'.config('pixelfed.domain.app');
|
||||
$ra = RemoteAuth::where('user_id', $request->user()->id)->firstOrFail();
|
||||
RemoteAuthService::submitToBeagle(
|
||||
$ra->webfinger,
|
||||
$ra->verify_credentials['url'],
|
||||
$currentWebfinger,
|
||||
$request->user()->url()
|
||||
);
|
||||
|
||||
return [200];
|
||||
}
|
||||
|
||||
public function handleLogin(Request $request)
|
||||
{
|
||||
abort_unless((
|
||||
config_cache('pixelfed.open_registration') &&
|
||||
config('remote-auth.mastodon.enabled')
|
||||
) || (
|
||||
config('remote-auth.mastodon.ignore_closed_state') &&
|
||||
config('remote-auth.mastodon.enabled')
|
||||
), 404);
|
||||
abort_if($request->user(), 404);
|
||||
abort_unless($request->session()->exists('oauth_domain'), 403);
|
||||
abort_unless($request->session()->exists('oauth_remote_session_token'), 403);
|
||||
abort_unless($request->session()->exists('oauth_masto_webfinger'), 403);
|
||||
|
||||
$domain = $request->session()->get('oauth_domain');
|
||||
$wf = $request->session()->get('oauth_masto_webfinger');
|
||||
|
||||
$ra = RemoteAuth::where('webfinger', $wf)->where('domain', $domain)->whereNotNull('user_id')->firstOrFail();
|
||||
|
||||
$user = User::findOrFail($ra->user_id);
|
||||
abort_if($user->is_admin || $user->status != null, 422, 'Invalid auth action');
|
||||
Auth::loginUsingId($ra->user_id);
|
||||
|
||||
return [200];
|
||||
}
|
||||
|
||||
protected function createUser($data)
|
||||
{
|
||||
event(new Registered($user = User::create([
|
||||
'name' => Purify::clean($data['name']),
|
||||
'username' => $data['username'],
|
||||
'email' => $data['email'],
|
||||
'password' => Hash::make($data['password']),
|
||||
'email_verified_at' => config('remote-auth.mastodon.contraints.skip_email_verification') ? now() : null,
|
||||
'app_register_ip' => request()->ip(),
|
||||
'register_source' => 'mastodon',
|
||||
])));
|
||||
|
||||
$this->guarder()->login($user);
|
||||
|
||||
return $user;
|
||||
}
|
||||
|
||||
protected function guarder()
|
||||
{
|
||||
return Auth::guard();
|
||||
}
|
||||
}
|
|
@ -2,368 +2,367 @@
|
|||
|
||||
namespace App\Http\Controllers;
|
||||
|
||||
use Auth;
|
||||
use App\Hashtag;
|
||||
use App\Place;
|
||||
use App\Profile;
|
||||
use App\Services\WebfingerService;
|
||||
use App\Status;
|
||||
use Illuminate\Http\Request;
|
||||
use App\Util\ActivityPub\Helpers;
|
||||
use Auth;
|
||||
use Illuminate\Http\Request;
|
||||
use Illuminate\Support\Facades\Cache;
|
||||
use Illuminate\Support\Str;
|
||||
use App\Transformer\Api\{
|
||||
AccountTransformer,
|
||||
HashtagTransformer,
|
||||
StatusTransformer,
|
||||
};
|
||||
use App\Services\WebfingerService;
|
||||
|
||||
class SearchController extends Controller
|
||||
{
|
||||
public $tokens = [];
|
||||
public $term = '';
|
||||
public $hash = '';
|
||||
public $cacheKey = 'api:search:tag:';
|
||||
public $tokens = [];
|
||||
|
||||
public function __construct()
|
||||
{
|
||||
$this->middleware('auth');
|
||||
}
|
||||
public $term = '';
|
||||
|
||||
public function searchAPI(Request $request)
|
||||
{
|
||||
$this->validate($request, [
|
||||
'q' => 'required|string|min:3|max:120',
|
||||
'src' => 'required|string|in:metro',
|
||||
'v' => 'required|integer|in:2',
|
||||
'scope' => 'required|in:all,hashtag,profile,remote,webfinger'
|
||||
]);
|
||||
public $hash = '';
|
||||
|
||||
$scope = $request->input('scope') ?? 'all';
|
||||
$this->term = e(urldecode($request->input('q')));
|
||||
$this->hash = hash('sha256', $this->term);
|
||||
public $cacheKey = 'api:search:tag:';
|
||||
|
||||
switch ($scope) {
|
||||
case 'all':
|
||||
$this->getHashtags();
|
||||
$this->getPosts();
|
||||
$this->getProfiles();
|
||||
// $this->getPlaces();
|
||||
break;
|
||||
public function __construct()
|
||||
{
|
||||
$this->middleware('auth');
|
||||
}
|
||||
|
||||
case 'hashtag':
|
||||
$this->getHashtags();
|
||||
break;
|
||||
public function searchAPI(Request $request)
|
||||
{
|
||||
$this->validate($request, [
|
||||
'q' => 'required|string|min:3|max:120',
|
||||
'src' => 'required|string|in:metro',
|
||||
'v' => 'required|integer|in:2',
|
||||
'scope' => 'required|in:all,hashtag,profile,remote,webfinger',
|
||||
]);
|
||||
|
||||
case 'profile':
|
||||
$this->getProfiles();
|
||||
break;
|
||||
$scope = $request->input('scope') ?? 'all';
|
||||
$this->term = e(urldecode($request->input('q')));
|
||||
$this->hash = hash('sha256', $this->term);
|
||||
|
||||
case 'webfinger':
|
||||
$this->webfingerSearch();
|
||||
break;
|
||||
switch ($scope) {
|
||||
case 'all':
|
||||
$this->getHashtags();
|
||||
$this->getPosts();
|
||||
$this->getProfiles();
|
||||
// $this->getPlaces();
|
||||
break;
|
||||
|
||||
case 'remote':
|
||||
$this->remoteLookupSearch();
|
||||
break;
|
||||
case 'hashtag':
|
||||
$this->getHashtags();
|
||||
break;
|
||||
|
||||
case 'place':
|
||||
$this->getPlaces();
|
||||
break;
|
||||
case 'profile':
|
||||
$this->getProfiles();
|
||||
break;
|
||||
|
||||
default:
|
||||
break;
|
||||
}
|
||||
case 'webfinger':
|
||||
$this->webfingerSearch();
|
||||
break;
|
||||
|
||||
return response()->json($this->tokens, 200, [], JSON_PRETTY_PRINT);
|
||||
}
|
||||
case 'remote':
|
||||
$this->remoteLookupSearch();
|
||||
break;
|
||||
|
||||
protected function getPosts()
|
||||
{
|
||||
$tag = $this->term;
|
||||
$hash = hash('sha256', $tag);
|
||||
if( Helpers::validateUrl($tag) != false &&
|
||||
Helpers::validateLocalUrl($tag) != true &&
|
||||
config_cache('federation.activitypub.enabled') == true &&
|
||||
config('federation.activitypub.remoteFollow') == true
|
||||
) {
|
||||
$remote = Helpers::fetchFromUrl($tag);
|
||||
if( isset($remote['type']) &&
|
||||
$remote['type'] == 'Note') {
|
||||
$item = Helpers::statusFetch($tag);
|
||||
$this->tokens['posts'] = [[
|
||||
'count' => 0,
|
||||
'url' => $item->url(),
|
||||
'type' => 'status',
|
||||
'value' => "by {$item->profile->username} <span class='float-right'>{$item->created_at->diffForHumans(null, true, true)}</span>",
|
||||
'tokens' => [$item->caption],
|
||||
'name' => $item->caption,
|
||||
'thumb' => $item->thumb(),
|
||||
]];
|
||||
}
|
||||
} else {
|
||||
$posts = Status::select('id', 'profile_id', 'caption', 'created_at')
|
||||
->whereHas('media')
|
||||
->whereNull('in_reply_to_id')
|
||||
->whereNull('reblog_of_id')
|
||||
->whereProfileId(Auth::user()->profile_id)
|
||||
->where('caption', 'like', '%'.$tag.'%')
|
||||
->latest()
|
||||
->limit(10)
|
||||
->get();
|
||||
case 'place':
|
||||
$this->getPlaces();
|
||||
break;
|
||||
|
||||
if($posts->count() > 0) {
|
||||
$posts = $posts->map(function($item, $key) {
|
||||
return [
|
||||
'count' => 0,
|
||||
'url' => $item->url(),
|
||||
'type' => 'status',
|
||||
'value' => "by {$item->profile->username} <span class='float-right'>{$item->created_at->diffForHumans(null, true, true)}</span>",
|
||||
'tokens' => [$item->caption],
|
||||
'name' => $item->caption,
|
||||
'thumb' => $item->thumb(),
|
||||
'filter' => $item->firstMedia()->filter_class
|
||||
];
|
||||
});
|
||||
$this->tokens['posts'] = $posts;
|
||||
}
|
||||
}
|
||||
}
|
||||
default:
|
||||
break;
|
||||
}
|
||||
|
||||
protected function getHashtags()
|
||||
{
|
||||
$tag = $this->term;
|
||||
$key = $this->cacheKey . 'hashtags:' . $this->hash;
|
||||
$ttl = now()->addMinutes(1);
|
||||
$tokens = Cache::remember($key, $ttl, function() use($tag) {
|
||||
$htag = Str::startsWith($tag, '#') == true ? mb_substr($tag, 1) : $tag;
|
||||
$hashtags = Hashtag::select('id', 'name', 'slug')
|
||||
->where('slug', 'like', '%'.$htag.'%')
|
||||
->whereHas('posts')
|
||||
->limit(20)
|
||||
->get();
|
||||
if($hashtags->count() > 0) {
|
||||
$tags = $hashtags->map(function ($item, $key) {
|
||||
return [
|
||||
'count' => $item->posts()->count(),
|
||||
'url' => $item->url(),
|
||||
'type' => 'hashtag',
|
||||
'value' => $item->name,
|
||||
'tokens' => '',
|
||||
'name' => null,
|
||||
];
|
||||
});
|
||||
return $tags;
|
||||
}
|
||||
});
|
||||
$this->tokens['hashtags'] = $tokens;
|
||||
}
|
||||
return response()->json($this->tokens, 200, [], JSON_PRETTY_PRINT);
|
||||
}
|
||||
|
||||
protected function getPlaces()
|
||||
{
|
||||
$tag = $this->term;
|
||||
// $key = $this->cacheKey . 'places:' . $this->hash;
|
||||
// $ttl = now()->addHours(12);
|
||||
// $tokens = Cache::remember($key, $ttl, function() use($tag) {
|
||||
$htag = Str::contains($tag, ',') == true ? explode(',', $tag) : [$tag];
|
||||
$hashtags = Place::select('id', 'name', 'slug', 'country')
|
||||
->where('name', 'like', '%'.$htag[0].'%')
|
||||
->paginate(20);
|
||||
$tags = [];
|
||||
if($hashtags->count() > 0) {
|
||||
$tags = $hashtags->map(function ($item, $key) {
|
||||
return [
|
||||
'count' => null,
|
||||
'url' => $item->url(),
|
||||
'type' => 'place',
|
||||
'value' => $item->name . ', ' . $item->country,
|
||||
'tokens' => '',
|
||||
'name' => null,
|
||||
'city' => $item->name,
|
||||
'country' => $item->country
|
||||
];
|
||||
});
|
||||
// return $tags;
|
||||
}
|
||||
// });
|
||||
$this->tokens['places'] = $tags;
|
||||
$this->tokens['placesPagination'] = [
|
||||
'total' => $hashtags->total(),
|
||||
'current_page' => $hashtags->currentPage(),
|
||||
'last_page' => $hashtags->lastPage()
|
||||
];
|
||||
}
|
||||
protected function getPosts()
|
||||
{
|
||||
$tag = $this->term;
|
||||
$hash = hash('sha256', $tag);
|
||||
if (Helpers::validateUrl($tag) != false &&
|
||||
Helpers::validateLocalUrl($tag) != true &&
|
||||
(bool) config_cache('federation.activitypub.enabled') == true &&
|
||||
config('federation.activitypub.remoteFollow') == true
|
||||
) {
|
||||
$remote = Helpers::fetchFromUrl($tag);
|
||||
if (isset($remote['type']) &&
|
||||
in_array($remote['type'], ['Note', 'Question'])
|
||||
) {
|
||||
$item = Helpers::statusFetch($tag);
|
||||
$this->tokens['posts'] = [[
|
||||
'count' => 0,
|
||||
'url' => $item->url(),
|
||||
'type' => 'status',
|
||||
'value' => "by {$item->profile->username} <span class='float-right'>{$item->created_at->diffForHumans(null, true, true)}</span>",
|
||||
'tokens' => [$item->caption],
|
||||
'name' => $item->caption,
|
||||
'thumb' => $item->thumb(),
|
||||
]];
|
||||
}
|
||||
} else {
|
||||
$posts = Status::select('id', 'profile_id', 'caption', 'created_at')
|
||||
->whereHas('media')
|
||||
->whereNull('in_reply_to_id')
|
||||
->whereNull('reblog_of_id')
|
||||
->whereProfileId(Auth::user()->profile_id)
|
||||
->where('caption', 'like', '%'.$tag.'%')
|
||||
->latest()
|
||||
->limit(10)
|
||||
->get();
|
||||
|
||||
protected function getProfiles()
|
||||
{
|
||||
$tag = $this->term;
|
||||
$remoteKey = $this->cacheKey . 'profiles:remote:' . $this->hash;
|
||||
$key = $this->cacheKey . 'profiles:' . $this->hash;
|
||||
$remoteTtl = now()->addMinutes(15);
|
||||
$ttl = now()->addHours(2);
|
||||
if( Helpers::validateUrl($tag) != false &&
|
||||
Helpers::validateLocalUrl($tag) != true &&
|
||||
config_cache('federation.activitypub.enabled') == true &&
|
||||
config('federation.activitypub.remoteFollow') == true
|
||||
) {
|
||||
$remote = Helpers::fetchFromUrl($tag);
|
||||
if( isset($remote['type']) &&
|
||||
$remote['type'] == 'Person'
|
||||
) {
|
||||
$this->tokens['profiles'] = Cache::remember($remoteKey, $remoteTtl, function() use($tag) {
|
||||
$item = Helpers::profileFirstOrNew($tag);
|
||||
$tokens = [[
|
||||
'count' => 1,
|
||||
'url' => $item->url(),
|
||||
'type' => 'profile',
|
||||
'value' => $item->username,
|
||||
'tokens' => [$item->username],
|
||||
'name' => $item->name,
|
||||
'entity' => [
|
||||
'id' => (string) $item->id,
|
||||
'following' => $item->followedBy(Auth::user()->profile),
|
||||
'follow_request' => $item->hasFollowRequestById(Auth::user()->profile_id),
|
||||
'thumb' => $item->avatarUrl(),
|
||||
'local' => (bool) !$item->domain,
|
||||
'post_count' => $item->statuses()->count()
|
||||
]
|
||||
]];
|
||||
return $tokens;
|
||||
});
|
||||
}
|
||||
}
|
||||
if ($posts->count() > 0) {
|
||||
$posts = $posts->map(function ($item, $key) {
|
||||
return [
|
||||
'count' => 0,
|
||||
'url' => $item->url(),
|
||||
'type' => 'status',
|
||||
'value' => "by {$item->profile->username} <span class='float-right'>{$item->created_at->diffForHumans(null, true, true)}</span>",
|
||||
'tokens' => [$item->caption],
|
||||
'name' => $item->caption,
|
||||
'thumb' => $item->thumb(),
|
||||
'filter' => $item->firstMedia()->filter_class,
|
||||
];
|
||||
});
|
||||
$this->tokens['posts'] = $posts;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
else {
|
||||
$this->tokens['profiles'] = Cache::remember($key, $ttl, function() use($tag) {
|
||||
if(Str::startsWith($tag, '@')) {
|
||||
$tag = substr($tag, 1);
|
||||
}
|
||||
$users = Profile::select('status', 'domain', 'username', 'name', 'id')
|
||||
->whereNull('status')
|
||||
->where('username', 'like', '%'.$tag.'%')
|
||||
->limit(20)
|
||||
->orderBy('domain')
|
||||
->get();
|
||||
protected function getHashtags()
|
||||
{
|
||||
$tag = $this->term;
|
||||
$key = $this->cacheKey.'hashtags:'.$this->hash;
|
||||
$ttl = now()->addMinutes(1);
|
||||
$tokens = Cache::remember($key, $ttl, function () use ($tag) {
|
||||
$htag = Str::startsWith($tag, '#') == true ? mb_substr($tag, 1) : $tag;
|
||||
$hashtags = Hashtag::select('id', 'name', 'slug')
|
||||
->where('slug', 'like', '%'.$htag.'%')
|
||||
->whereHas('posts')
|
||||
->limit(20)
|
||||
->get();
|
||||
if ($hashtags->count() > 0) {
|
||||
$tags = $hashtags->map(function ($item, $key) {
|
||||
return [
|
||||
'count' => $item->posts()->count(),
|
||||
'url' => $item->url(),
|
||||
'type' => 'hashtag',
|
||||
'value' => $item->name,
|
||||
'tokens' => '',
|
||||
'name' => null,
|
||||
];
|
||||
});
|
||||
|
||||
if($users->count() > 0) {
|
||||
return $users->map(function ($item, $key) {
|
||||
return [
|
||||
'count' => 0,
|
||||
'url' => $item->url(),
|
||||
'type' => 'profile',
|
||||
'value' => $item->username,
|
||||
'tokens' => [$item->username],
|
||||
'name' => $item->name,
|
||||
'avatar' => $item->avatarUrl(),
|
||||
'id' => (string) $item->id,
|
||||
'entity' => [
|
||||
'id' => (string) $item->id,
|
||||
'following' => $item->followedBy(Auth::user()->profile),
|
||||
'follow_request' => $item->hasFollowRequestById(Auth::user()->profile_id),
|
||||
'thumb' => $item->avatarUrl(),
|
||||
'local' => (bool) !$item->domain,
|
||||
'post_count' => $item->statuses()->count()
|
||||
]
|
||||
];
|
||||
});
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
return $tags;
|
||||
}
|
||||
});
|
||||
$this->tokens['hashtags'] = $tokens;
|
||||
}
|
||||
|
||||
public function results(Request $request)
|
||||
{
|
||||
$this->validate($request, [
|
||||
'q' => 'required|string|min:1',
|
||||
]);
|
||||
protected function getPlaces()
|
||||
{
|
||||
$tag = $this->term;
|
||||
// $key = $this->cacheKey . 'places:' . $this->hash;
|
||||
// $ttl = now()->addHours(12);
|
||||
// $tokens = Cache::remember($key, $ttl, function() use($tag) {
|
||||
$htag = Str::contains($tag, ',') == true ? explode(',', $tag) : [$tag];
|
||||
$hashtags = Place::select('id', 'name', 'slug', 'country')
|
||||
->where('name', 'like', '%'.$htag[0].'%')
|
||||
->paginate(20);
|
||||
$tags = [];
|
||||
if ($hashtags->count() > 0) {
|
||||
$tags = $hashtags->map(function ($item, $key) {
|
||||
return [
|
||||
'count' => null,
|
||||
'url' => $item->url(),
|
||||
'type' => 'place',
|
||||
'value' => $item->name.', '.$item->country,
|
||||
'tokens' => '',
|
||||
'name' => null,
|
||||
'city' => $item->name,
|
||||
'country' => $item->country,
|
||||
];
|
||||
});
|
||||
// return $tags;
|
||||
}
|
||||
// });
|
||||
$this->tokens['places'] = $tags;
|
||||
$this->tokens['placesPagination'] = [
|
||||
'total' => $hashtags->total(),
|
||||
'current_page' => $hashtags->currentPage(),
|
||||
'last_page' => $hashtags->lastPage(),
|
||||
];
|
||||
}
|
||||
|
||||
return view('search.results');
|
||||
}
|
||||
protected function getProfiles()
|
||||
{
|
||||
$tag = $this->term;
|
||||
$remoteKey = $this->cacheKey.'profiles:remote:'.$this->hash;
|
||||
$key = $this->cacheKey.'profiles:'.$this->hash;
|
||||
$remoteTtl = now()->addMinutes(15);
|
||||
$ttl = now()->addHours(2);
|
||||
if (Helpers::validateUrl($tag) != false &&
|
||||
Helpers::validateLocalUrl($tag) != true &&
|
||||
(bool) config_cache('federation.activitypub.enabled') == true &&
|
||||
config('federation.activitypub.remoteFollow') == true
|
||||
) {
|
||||
$remote = Helpers::fetchFromUrl($tag);
|
||||
if (isset($remote['type']) &&
|
||||
$remote['type'] == 'Person'
|
||||
) {
|
||||
$this->tokens['profiles'] = Cache::remember($remoteKey, $remoteTtl, function () use ($tag) {
|
||||
$item = Helpers::profileFirstOrNew($tag);
|
||||
$tokens = [[
|
||||
'count' => 1,
|
||||
'url' => $item->url(),
|
||||
'type' => 'profile',
|
||||
'value' => $item->username,
|
||||
'tokens' => [$item->username],
|
||||
'name' => $item->name,
|
||||
'entity' => [
|
||||
'id' => (string) $item->id,
|
||||
'following' => $item->followedBy(Auth::user()->profile),
|
||||
'follow_request' => $item->hasFollowRequestById(Auth::user()->profile_id),
|
||||
'thumb' => $item->avatarUrl(),
|
||||
'local' => (bool) ! $item->domain,
|
||||
'post_count' => $item->statuses()->count(),
|
||||
],
|
||||
]];
|
||||
|
||||
protected function webfingerSearch()
|
||||
{
|
||||
$wfs = WebfingerService::lookup($this->term);
|
||||
return $tokens;
|
||||
});
|
||||
}
|
||||
} else {
|
||||
$this->tokens['profiles'] = Cache::remember($key, $ttl, function () use ($tag) {
|
||||
if (Str::startsWith($tag, '@')) {
|
||||
$tag = substr($tag, 1);
|
||||
}
|
||||
$users = Profile::select('status', 'domain', 'username', 'name', 'id')
|
||||
->whereNull('status')
|
||||
->where('username', 'like', '%'.$tag.'%')
|
||||
->limit(20)
|
||||
->orderBy('domain')
|
||||
->get();
|
||||
|
||||
if(empty($wfs)) {
|
||||
return;
|
||||
}
|
||||
if ($users->count() > 0) {
|
||||
return $users->map(function ($item, $key) {
|
||||
return [
|
||||
'count' => 0,
|
||||
'url' => $item->url(),
|
||||
'type' => 'profile',
|
||||
'value' => $item->username,
|
||||
'tokens' => [$item->username],
|
||||
'name' => $item->name,
|
||||
'avatar' => $item->avatarUrl(),
|
||||
'id' => (string) $item->id,
|
||||
'entity' => [
|
||||
'id' => (string) $item->id,
|
||||
'following' => $item->followedBy(Auth::user()->profile),
|
||||
'follow_request' => $item->hasFollowRequestById(Auth::user()->profile_id),
|
||||
'thumb' => $item->avatarUrl(),
|
||||
'local' => (bool) ! $item->domain,
|
||||
'post_count' => $item->statuses()->count(),
|
||||
],
|
||||
];
|
||||
});
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
$this->tokens['profiles'] = [
|
||||
[
|
||||
'count' => 1,
|
||||
'url' => $wfs['url'],
|
||||
'type' => 'profile',
|
||||
'value' => $wfs['username'],
|
||||
'tokens' => [$wfs['username']],
|
||||
'name' => $wfs['display_name'],
|
||||
'entity' => [
|
||||
'id' => (string) $wfs['id'],
|
||||
'following' => null,
|
||||
'follow_request' => null,
|
||||
'thumb' => $wfs['avatar'],
|
||||
'local' => (bool) $wfs['local']
|
||||
]
|
||||
]
|
||||
];
|
||||
return;
|
||||
}
|
||||
public function results(Request $request)
|
||||
{
|
||||
$this->validate($request, [
|
||||
'q' => 'required|string|min:1',
|
||||
]);
|
||||
|
||||
protected function remotePostLookup()
|
||||
{
|
||||
$tag = $this->term;
|
||||
$hash = hash('sha256', $tag);
|
||||
$local = Helpers::validateLocalUrl($tag);
|
||||
$valid = Helpers::validateUrl($tag);
|
||||
return view('search.results');
|
||||
}
|
||||
|
||||
if($valid == false || $local == true) {
|
||||
return;
|
||||
}
|
||||
protected function webfingerSearch()
|
||||
{
|
||||
$wfs = WebfingerService::lookup($this->term);
|
||||
|
||||
if(Status::whereUri($tag)->whereLocal(false)->exists()) {
|
||||
$item = Status::whereUri($tag)->first();
|
||||
$media = $item->firstMedia();
|
||||
$url = null;
|
||||
if($media) {
|
||||
$url = $media->remote_url;
|
||||
}
|
||||
$this->tokens['posts'] = [[
|
||||
'count' => 0,
|
||||
'url' => "/i/web/post/_/$item->profile_id/$item->id",
|
||||
'type' => 'status',
|
||||
'username' => $item->profile->username,
|
||||
'caption' => $item->rendered ?? $item->caption,
|
||||
'thumb' => $url,
|
||||
'timestamp' => $item->created_at->diffForHumans()
|
||||
]];
|
||||
}
|
||||
if (empty($wfs)) {
|
||||
return;
|
||||
}
|
||||
|
||||
$remote = Helpers::fetchFromUrl($tag);
|
||||
$this->tokens['profiles'] = [
|
||||
[
|
||||
'count' => 1,
|
||||
'url' => $wfs['url'],
|
||||
'type' => 'profile',
|
||||
'value' => $wfs['username'],
|
||||
'tokens' => [$wfs['username']],
|
||||
'name' => $wfs['display_name'],
|
||||
'entity' => [
|
||||
'id' => (string) $wfs['id'],
|
||||
'following' => null,
|
||||
'follow_request' => null,
|
||||
'thumb' => $wfs['avatar'],
|
||||
'local' => (bool) $wfs['local'],
|
||||
],
|
||||
],
|
||||
];
|
||||
|
||||
if(isset($remote['type']) && $remote['type'] == 'Note') {
|
||||
$item = Helpers::statusFetch($tag);
|
||||
$media = $item->firstMedia();
|
||||
$url = null;
|
||||
if($media) {
|
||||
$url = $media->remote_url;
|
||||
}
|
||||
$this->tokens['posts'] = [[
|
||||
'count' => 0,
|
||||
'url' => "/i/web/post/_/$item->profile_id/$item->id",
|
||||
'type' => 'status',
|
||||
'username' => $item->profile->username,
|
||||
'caption' => $item->rendered ?? $item->caption,
|
||||
'thumb' => $url,
|
||||
'timestamp' => $item->created_at->diffForHumans()
|
||||
]];
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
protected function remoteLookupSearch()
|
||||
{
|
||||
if(!Helpers::validateUrl($this->term)) {
|
||||
return;
|
||||
}
|
||||
$this->getProfiles();
|
||||
$this->remotePostLookup();
|
||||
}
|
||||
protected function remotePostLookup()
|
||||
{
|
||||
$tag = $this->term;
|
||||
$hash = hash('sha256', $tag);
|
||||
$local = Helpers::validateLocalUrl($tag);
|
||||
$valid = Helpers::validateUrl($tag);
|
||||
|
||||
if ($valid == false || $local == true) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (Status::whereUri($tag)->whereLocal(false)->exists()) {
|
||||
$item = Status::whereUri($tag)->first();
|
||||
$media = $item->firstMedia();
|
||||
$url = null;
|
||||
if ($media) {
|
||||
$url = $media->remote_url;
|
||||
}
|
||||
$this->tokens['posts'] = [[
|
||||
'count' => 0,
|
||||
'url' => "/i/web/post/_/$item->profile_id/$item->id",
|
||||
'type' => 'status',
|
||||
'username' => $item->profile->username,
|
||||
'caption' => $item->rendered ?? $item->caption,
|
||||
'thumb' => $url,
|
||||
'timestamp' => $item->created_at->diffForHumans(),
|
||||
]];
|
||||
}
|
||||
|
||||
$remote = Helpers::fetchFromUrl($tag);
|
||||
|
||||
if (isset($remote['type']) && $remote['type'] == 'Note') {
|
||||
$item = Helpers::statusFetch($tag);
|
||||
$media = $item->firstMedia();
|
||||
$url = null;
|
||||
if ($media) {
|
||||
$url = $media->remote_url;
|
||||
}
|
||||
$this->tokens['posts'] = [[
|
||||
'count' => 0,
|
||||
'url' => "/i/web/post/_/$item->profile_id/$item->id",
|
||||
'type' => 'status',
|
||||
'username' => $item->profile->username,
|
||||
'caption' => $item->rendered ?? $item->caption,
|
||||
'thumb' => $url,
|
||||
'timestamp' => $item->created_at->diffForHumans(),
|
||||
]];
|
||||
}
|
||||
}
|
||||
|
||||
protected function remoteLookupSearch()
|
||||
{
|
||||
if (! Helpers::validateUrl($this->term)) {
|
||||
return;
|
||||
}
|
||||
$this->getProfiles();
|
||||
$this->remotePostLookup();
|
||||
}
|
||||
}
|
||||
|
|
|
@ -22,189 +22,189 @@ use App\Services\PronounService;
|
|||
|
||||
trait HomeSettings
|
||||
{
|
||||
public function home()
|
||||
{
|
||||
$id = Auth::user()->profile->id;
|
||||
$storage = [];
|
||||
$used = Media::whereProfileId($id)->sum('size');
|
||||
$storage['limit'] = config_cache('pixelfed.max_account_size') * 1024;
|
||||
$storage['used'] = $used;
|
||||
$storage['percentUsed'] = ceil($storage['used'] / $storage['limit'] * 100);
|
||||
$storage['limitPretty'] = PrettyNumber::size($storage['limit']);
|
||||
$storage['usedPretty'] = PrettyNumber::size($storage['used']);
|
||||
$pronouns = PronounService::get($id);
|
||||
public function home()
|
||||
{
|
||||
$id = Auth::user()->profile->id;
|
||||
$storage = [];
|
||||
$used = Media::whereProfileId($id)->sum('size');
|
||||
$storage['limit'] = config_cache('pixelfed.max_account_size') * 1024;
|
||||
$storage['used'] = $used;
|
||||
$storage['percentUsed'] = ceil($storage['used'] / $storage['limit'] * 100);
|
||||
$storage['limitPretty'] = PrettyNumber::size($storage['limit']);
|
||||
$storage['usedPretty'] = PrettyNumber::size($storage['used']);
|
||||
$pronouns = PronounService::get($id);
|
||||
|
||||
return view('settings.home', compact('storage', 'pronouns'));
|
||||
}
|
||||
return view('settings.home', compact('storage', 'pronouns'));
|
||||
}
|
||||
|
||||
public function homeUpdate(Request $request)
|
||||
{
|
||||
$this->validate($request, [
|
||||
'name' => 'nullable|string|max:'.config('pixelfed.max_name_length'),
|
||||
'bio' => 'nullable|string|max:'.config('pixelfed.max_bio_length'),
|
||||
'website' => 'nullable|url',
|
||||
'language' => 'nullable|string|min:2|max:5',
|
||||
'pronouns' => 'nullable|array|max:4'
|
||||
]);
|
||||
public function homeUpdate(Request $request)
|
||||
{
|
||||
$this->validate($request, [
|
||||
'name' => 'nullable|string|max:'.config('pixelfed.max_name_length'),
|
||||
'bio' => 'nullable|string|max:'.config('pixelfed.max_bio_length'),
|
||||
'website' => 'nullable|url',
|
||||
'language' => 'nullable|string|min:2|max:5',
|
||||
'pronouns' => 'nullable|array|max:4'
|
||||
]);
|
||||
|
||||
$changes = false;
|
||||
$name = strip_tags(Purify::clean($request->input('name')));
|
||||
$bio = $request->filled('bio') ? strip_tags(Purify::clean($request->input('bio'))) : null;
|
||||
$website = $request->input('website');
|
||||
$language = $request->input('language');
|
||||
$user = Auth::user();
|
||||
$profile = $user->profile;
|
||||
$pronouns = $request->input('pronouns');
|
||||
$existingPronouns = PronounService::get($profile->id);
|
||||
$layout = $request->input('profile_layout');
|
||||
if($layout) {
|
||||
$layout = !in_array($layout, ['metro', 'moment']) ? 'metro' : $layout;
|
||||
}
|
||||
$changes = false;
|
||||
$name = strip_tags(Purify::clean($request->input('name')));
|
||||
$bio = $request->filled('bio') ? strip_tags(Purify::clean($request->input('bio'))) : null;
|
||||
$website = $request->input('website');
|
||||
$language = $request->input('language');
|
||||
$user = Auth::user();
|
||||
$profile = $user->profile;
|
||||
$pronouns = $request->input('pronouns');
|
||||
$existingPronouns = PronounService::get($profile->id);
|
||||
$layout = $request->input('profile_layout');
|
||||
if($layout) {
|
||||
$layout = !in_array($layout, ['metro', 'moment']) ? 'metro' : $layout;
|
||||
}
|
||||
|
||||
$enforceEmailVerification = config_cache('pixelfed.enforce_email_verification');
|
||||
$enforceEmailVerification = config_cache('pixelfed.enforce_email_verification');
|
||||
|
||||
// Only allow email to be updated if not yet verified
|
||||
if (!$enforceEmailVerification || !$changes && $user->email_verified_at) {
|
||||
if ($profile->name != $name) {
|
||||
$changes = true;
|
||||
$user->name = $name;
|
||||
$profile->name = $name;
|
||||
}
|
||||
// Only allow email to be updated if not yet verified
|
||||
if (!$enforceEmailVerification || !$changes && $user->email_verified_at) {
|
||||
if ($profile->name != $name) {
|
||||
$changes = true;
|
||||
$user->name = $name;
|
||||
$profile->name = $name;
|
||||
}
|
||||
|
||||
if ($profile->website != $website) {
|
||||
$changes = true;
|
||||
$profile->website = $website;
|
||||
}
|
||||
if ($profile->website != $website) {
|
||||
$changes = true;
|
||||
$profile->website = $website;
|
||||
}
|
||||
|
||||
if (strip_tags($profile->bio) != $bio) {
|
||||
$changes = true;
|
||||
$profile->bio = Autolink::create()->autolink($bio);
|
||||
}
|
||||
if (strip_tags($profile->bio) != $bio) {
|
||||
$changes = true;
|
||||
$profile->bio = Autolink::create()->autolink($bio);
|
||||
}
|
||||
|
||||
if($user->language != $language &&
|
||||
in_array($language, \App\Util\Localization\Localization::languages())
|
||||
) {
|
||||
$changes = true;
|
||||
$user->language = $language;
|
||||
session()->put('locale', $language);
|
||||
}
|
||||
if($user->language != $language &&
|
||||
in_array($language, \App\Util\Localization\Localization::languages())
|
||||
) {
|
||||
$changes = true;
|
||||
$user->language = $language;
|
||||
session()->put('locale', $language);
|
||||
}
|
||||
|
||||
if($existingPronouns != $pronouns) {
|
||||
if($pronouns && in_array('Select Pronoun(s)', $pronouns)) {
|
||||
PronounService::clear($profile->id);
|
||||
} else {
|
||||
PronounService::put($profile->id, $pronouns);
|
||||
}
|
||||
}
|
||||
}
|
||||
if($existingPronouns != $pronouns) {
|
||||
if($pronouns && in_array('Select Pronoun(s)', $pronouns)) {
|
||||
PronounService::clear($profile->id);
|
||||
} else {
|
||||
PronounService::put($profile->id, $pronouns);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if ($changes === true) {
|
||||
$user->save();
|
||||
$profile->save();
|
||||
Cache::forget('user:account:id:'.$user->id);
|
||||
AccountService::del($profile->id);
|
||||
return redirect('/settings/home')->with('status', 'Profile successfully updated!');
|
||||
}
|
||||
if ($changes === true) {
|
||||
$user->save();
|
||||
$profile->save();
|
||||
Cache::forget('user:account:id:'.$user->id);
|
||||
AccountService::del($profile->id);
|
||||
return redirect('/settings/home')->with('status', 'Profile successfully updated!');
|
||||
}
|
||||
|
||||
return redirect('/settings/home');
|
||||
}
|
||||
return redirect('/settings/home');
|
||||
}
|
||||
|
||||
public function password()
|
||||
{
|
||||
return view('settings.password');
|
||||
}
|
||||
public function password()
|
||||
{
|
||||
return view('settings.password');
|
||||
}
|
||||
|
||||
public function passwordUpdate(Request $request)
|
||||
{
|
||||
$this->validate($request, [
|
||||
'current' => 'required|string',
|
||||
'password' => 'required|string',
|
||||
'password_confirmation' => 'required|string',
|
||||
]);
|
||||
public function passwordUpdate(Request $request)
|
||||
{
|
||||
$this->validate($request, [
|
||||
'current' => 'required|string',
|
||||
'password' => 'required|string',
|
||||
'password_confirmation' => 'required|string',
|
||||
]);
|
||||
|
||||
$current = $request->input('current');
|
||||
$new = $request->input('password');
|
||||
$confirm = $request->input('password_confirmation');
|
||||
$current = $request->input('current');
|
||||
$new = $request->input('password');
|
||||
$confirm = $request->input('password_confirmation');
|
||||
|
||||
$user = Auth::user();
|
||||
$user = Auth::user();
|
||||
|
||||
if (password_verify($current, $user->password) && $new === $confirm) {
|
||||
$user->password = bcrypt($new);
|
||||
$user->save();
|
||||
if (password_verify($current, $user->password) && $new === $confirm) {
|
||||
$user->password = bcrypt($new);
|
||||
$user->save();
|
||||
|
||||
$log = new AccountLog();
|
||||
$log->user_id = $user->id;
|
||||
$log->item_id = $user->id;
|
||||
$log->item_type = 'App\User';
|
||||
$log->action = 'account.edit.password';
|
||||
$log->message = 'Password changed';
|
||||
$log->link = null;
|
||||
$log->ip_address = $request->ip();
|
||||
$log->user_agent = $request->userAgent();
|
||||
$log->save();
|
||||
$log = new AccountLog();
|
||||
$log->user_id = $user->id;
|
||||
$log->item_id = $user->id;
|
||||
$log->item_type = 'App\User';
|
||||
$log->action = 'account.edit.password';
|
||||
$log->message = 'Password changed';
|
||||
$log->link = null;
|
||||
$log->ip_address = $request->ip();
|
||||
$log->user_agent = $request->userAgent();
|
||||
$log->save();
|
||||
|
||||
Mail::to($request->user())->send(new PasswordChange($user));
|
||||
return redirect('/settings/home')->with('status', 'Password successfully updated!');
|
||||
} else {
|
||||
return redirect()->back()->with('error', 'There was an error with your request! Please try again.');
|
||||
}
|
||||
Mail::to($request->user())->send(new PasswordChange($user));
|
||||
return redirect('/settings/home')->with('status', 'Password successfully updated!');
|
||||
} else {
|
||||
return redirect()->back()->with('error', 'There was an error with your request! Please try again.');
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
public function email()
|
||||
{
|
||||
return view('settings.email');
|
||||
}
|
||||
public function email()
|
||||
{
|
||||
return view('settings.email');
|
||||
}
|
||||
|
||||
public function emailUpdate(Request $request)
|
||||
{
|
||||
$this->validate($request, [
|
||||
'email' => 'required|email|unique:users,email',
|
||||
]);
|
||||
$changes = false;
|
||||
$email = $request->input('email');
|
||||
$user = Auth::user();
|
||||
$profile = $user->profile;
|
||||
public function emailUpdate(Request $request)
|
||||
{
|
||||
$this->validate($request, [
|
||||
'email' => 'required|email|unique:users,email',
|
||||
]);
|
||||
$changes = false;
|
||||
$email = $request->input('email');
|
||||
$user = Auth::user();
|
||||
$profile = $user->profile;
|
||||
|
||||
$validate = config_cache('pixelfed.enforce_email_verification');
|
||||
$validate = config_cache('pixelfed.enforce_email_verification');
|
||||
|
||||
if ($user->email != $email) {
|
||||
$changes = true;
|
||||
$user->email = $email;
|
||||
if ($user->email != $email) {
|
||||
$changes = true;
|
||||
$user->email = $email;
|
||||
|
||||
if ($validate) {
|
||||
$user->email_verified_at = null;
|
||||
// Prevent old verifications from working
|
||||
EmailVerification::whereUserId($user->id)->delete();
|
||||
}
|
||||
if ($validate) {
|
||||
// auto verify admin email addresses
|
||||
$user->email_verified_at = $user->is_admin == true ? now() : null;
|
||||
// Prevent old verifications from working
|
||||
EmailVerification::whereUserId($user->id)->delete();
|
||||
}
|
||||
|
||||
$log = new AccountLog();
|
||||
$log->user_id = $user->id;
|
||||
$log->item_id = $user->id;
|
||||
$log->item_type = 'App\User';
|
||||
$log->action = 'account.edit.email';
|
||||
$log->message = 'Email changed';
|
||||
$log->link = null;
|
||||
$log->ip_address = $request->ip();
|
||||
$log->user_agent = $request->userAgent();
|
||||
$log->save();
|
||||
}
|
||||
$log = new AccountLog();
|
||||
$log->user_id = $user->id;
|
||||
$log->item_id = $user->id;
|
||||
$log->item_type = 'App\User';
|
||||
$log->action = 'account.edit.email';
|
||||
$log->message = 'Email changed';
|
||||
$log->link = null;
|
||||
$log->ip_address = $request->ip();
|
||||
$log->user_agent = $request->userAgent();
|
||||
$log->save();
|
||||
}
|
||||
|
||||
if ($changes === true) {
|
||||
Cache::forget('user:account:id:'.$user->id);
|
||||
$user->save();
|
||||
$profile->save();
|
||||
if ($changes === true) {
|
||||
Cache::forget('user:account:id:'.$user->id);
|
||||
$user->save();
|
||||
$profile->save();
|
||||
|
||||
return redirect('/settings/home')->with('status', 'Email successfully updated!');
|
||||
} else {
|
||||
return redirect('/settings/email');
|
||||
}
|
||||
return redirect('/settings/email')->with('status', 'Email successfully updated!');
|
||||
} else {
|
||||
return redirect('/settings/email');
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
public function avatar()
|
||||
{
|
||||
return view('settings.avatar');
|
||||
}
|
||||
}
|
||||
|
||||
public function avatar()
|
||||
{
|
||||
return view('settings.avatar');
|
||||
}
|
||||
}
|
||||
|
|
|
@ -2,29 +2,26 @@
|
|||
|
||||
namespace App\Http\Controllers\Settings;
|
||||
|
||||
use App\AccountLog;
|
||||
use App\EmailVerification;
|
||||
use App\Instance;
|
||||
use App\Follower;
|
||||
use App\Media;
|
||||
use App\Profile;
|
||||
use App\User;
|
||||
use App\Services\RelationshipService;
|
||||
use App\UserFilter;
|
||||
use App\Util\Lexer\PrettyNumber;
|
||||
use App\Util\ActivityPub\Helpers;
|
||||
use Auth, Cache, DB;
|
||||
use Auth;
|
||||
use Cache;
|
||||
use DB;
|
||||
use Illuminate\Http\Request;
|
||||
|
||||
trait PrivacySettings
|
||||
{
|
||||
|
||||
public function privacy()
|
||||
{
|
||||
$settings = Auth::user()->settings;
|
||||
$is_private = Auth::user()->profile->is_private;
|
||||
$user = Auth::user();
|
||||
$settings = $user->settings;
|
||||
$profile = $user->profile;
|
||||
$is_private = $profile->is_private;
|
||||
$settings['is_private'] = (bool) $is_private;
|
||||
|
||||
return view('settings.privacy', compact('settings'));
|
||||
return view('settings.privacy', compact('settings', 'profile'));
|
||||
}
|
||||
|
||||
public function privacyStore(Request $request)
|
||||
|
@ -32,15 +29,18 @@ trait PrivacySettings
|
|||
$settings = $request->user()->settings;
|
||||
$profile = $request->user()->profile;
|
||||
$fields = [
|
||||
'is_private',
|
||||
'crawlable',
|
||||
'public_dm',
|
||||
'show_profile_follower_count',
|
||||
'show_profile_following_count',
|
||||
'is_private',
|
||||
'crawlable',
|
||||
'public_dm',
|
||||
'show_profile_follower_count',
|
||||
'show_profile_following_count',
|
||||
'indexable',
|
||||
'show_atom',
|
||||
];
|
||||
|
||||
$profile->is_suggestable = $request->input('is_suggestable') == 'on';
|
||||
$profile->save();
|
||||
$profile->indexable = $request->input('indexable') == 'on';
|
||||
$profile->is_suggestable = $request->input('is_suggestable') == 'on';
|
||||
$profile->save();
|
||||
|
||||
foreach ($fields as $field) {
|
||||
$form = $request->input($field);
|
||||
|
@ -61,12 +61,14 @@ trait PrivacySettings
|
|||
} else {
|
||||
$settings->{$field} = true;
|
||||
}
|
||||
} elseif ($field == 'public_dm') {
|
||||
} elseif ($field == 'public_dm') {
|
||||
if ($form == 'on') {
|
||||
$settings->{$field} = true;
|
||||
} else {
|
||||
$settings->{$field} = false;
|
||||
}
|
||||
} elseif ($field == 'indexable') {
|
||||
|
||||
} else {
|
||||
if ($form == 'on') {
|
||||
$settings->{$field} = true;
|
||||
|
@ -76,28 +78,36 @@ trait PrivacySettings
|
|||
}
|
||||
$settings->save();
|
||||
}
|
||||
Cache::forget('profile:settings:' . $profile->id);
|
||||
Cache::forget('user:account:id:' . $profile->user_id);
|
||||
Cache::forget('profile:follower_count:' . $profile->id);
|
||||
Cache::forget('profile:following_count:' . $profile->id);
|
||||
Cache::forget('profile:embed:' . $profile->id);
|
||||
Cache::forget('pf:acct:settings:hidden-followers:' . $profile->id);
|
||||
Cache::forget('pf:acct:settings:hidden-following:' . $profile->id);
|
||||
$pid = $profile->id;
|
||||
Cache::forget('profile:settings:'.$pid);
|
||||
Cache::forget('user:account:id:'.$profile->user_id);
|
||||
Cache::forget('profile:follower_count:'.$pid);
|
||||
Cache::forget('profile:following_count:'.$pid);
|
||||
Cache::forget('profile:atom:enabled:'.$pid);
|
||||
Cache::forget('profile:embed:'.$pid);
|
||||
Cache::forget('pf:acct:settings:hidden-followers:'.$pid);
|
||||
Cache::forget('pf:acct:settings:hidden-following:'.$pid);
|
||||
Cache::forget('pf:acct-trans:hideFollowing:'.$pid);
|
||||
Cache::forget('pf:acct-trans:hideFollowers:'.$pid);
|
||||
Cache::forget('pfc:cached-user:wt:'.strtolower($profile->username));
|
||||
Cache::forget('pfc:cached-user:wot:'.strtolower($profile->username));
|
||||
|
||||
return redirect(route('settings.privacy'))->with('status', 'Settings successfully updated!');
|
||||
}
|
||||
|
||||
public function mutedUsers()
|
||||
{
|
||||
{
|
||||
$pid = Auth::user()->profile->id;
|
||||
$ids = (new UserFilter())->mutedUserIds($pid);
|
||||
$users = Profile::whereIn('id', $ids)->simplePaginate(15);
|
||||
|
||||
return view('settings.privacy.muted', compact('users'));
|
||||
}
|
||||
|
||||
public function mutedUsersUpdate(Request $request)
|
||||
{
|
||||
{
|
||||
$this->validate($request, [
|
||||
'profile_id' => 'required|integer|min:1'
|
||||
'profile_id' => 'required|integer|min:1',
|
||||
]);
|
||||
$fid = $request->input('profile_id');
|
||||
$pid = Auth::user()->profile->id;
|
||||
|
@ -109,6 +119,8 @@ trait PrivacySettings
|
|||
->firstOrFail();
|
||||
$filter->delete();
|
||||
});
|
||||
RelationshipService::refresh($pid, $fid);
|
||||
|
||||
return redirect()->back();
|
||||
}
|
||||
|
||||
|
@ -117,14 +129,14 @@ trait PrivacySettings
|
|||
$pid = Auth::user()->profile->id;
|
||||
$ids = (new UserFilter())->blockedUserIds($pid);
|
||||
$users = Profile::whereIn('id', $ids)->simplePaginate(15);
|
||||
|
||||
return view('settings.privacy.blocked', compact('users'));
|
||||
}
|
||||
|
||||
|
||||
public function blockedUsersUpdate(Request $request)
|
||||
{
|
||||
{
|
||||
$this->validate($request, [
|
||||
'profile_id' => 'required|integer|min:1'
|
||||
'profile_id' => 'required|integer|min:1',
|
||||
]);
|
||||
$fid = $request->input('profile_id');
|
||||
$pid = Auth::user()->profile->id;
|
||||
|
@ -136,52 +148,32 @@ trait PrivacySettings
|
|||
->firstOrFail();
|
||||
$filter->delete();
|
||||
});
|
||||
RelationshipService::refresh($pid, $fid);
|
||||
|
||||
return redirect()->back();
|
||||
}
|
||||
|
||||
public function blockedInstances()
|
||||
{
|
||||
$pid = Auth::user()->profile->id;
|
||||
$filters = UserFilter::whereUserId($pid)
|
||||
->whereFilterableType('App\Instance')
|
||||
->whereFilterType('block')
|
||||
->orderByDesc('id')
|
||||
->paginate(10);
|
||||
return view('settings.privacy.blocked-instances', compact('filters'));
|
||||
// deprecated
|
||||
abort(404);
|
||||
}
|
||||
|
||||
public function domainBlocks()
|
||||
{
|
||||
return view('settings.privacy.domain-blocks');
|
||||
}
|
||||
|
||||
public function blockedInstanceStore(Request $request)
|
||||
{
|
||||
$this->validate($request, [
|
||||
'domain' => 'required|url|min:1|max:120'
|
||||
]);
|
||||
$domain = $request->input('domain');
|
||||
if(Helpers::validateUrl($domain) == false) {
|
||||
return abort(400, 'Invalid domain');
|
||||
}
|
||||
$domain = parse_url($domain, PHP_URL_HOST);
|
||||
$instance = Instance::firstOrCreate(['domain' => $domain]);
|
||||
$filter = new UserFilter;
|
||||
$filter->user_id = Auth::user()->profile->id;
|
||||
$filter->filterable_id = $instance->id;
|
||||
$filter->filterable_type = 'App\Instance';
|
||||
$filter->filter_type = 'block';
|
||||
$filter->save();
|
||||
return response()->json(['msg' => 200]);
|
||||
// deprecated
|
||||
abort(404);
|
||||
}
|
||||
|
||||
public function blockedInstanceUnblock(Request $request)
|
||||
{
|
||||
$this->validate($request, [
|
||||
'id' => 'required|integer|min:1'
|
||||
]);
|
||||
$pid = Auth::user()->profile->id;
|
||||
|
||||
$filter = UserFilter::whereFilterableType('App\Instance')
|
||||
->whereUserId($pid)
|
||||
->findOrFail($request->input('id'));
|
||||
$filter->delete();
|
||||
return redirect(route('settings.privacy.blocked-instances'));
|
||||
// deprecated
|
||||
abort(404);
|
||||
}
|
||||
|
||||
public function blockedKeywords()
|
||||
|
@ -202,7 +194,7 @@ trait PrivacySettings
|
|||
$profile = Auth::user()->profile;
|
||||
$settings = Auth::user()->settings;
|
||||
|
||||
if($mode !== 'keep-all') {
|
||||
if ($mode !== 'keep-all') {
|
||||
switch ($mode) {
|
||||
case 'mutual-only':
|
||||
$following = $profile->following()->pluck('profiles.id');
|
||||
|
@ -217,9 +209,9 @@ trait PrivacySettings
|
|||
case 'remove-all':
|
||||
Follower::whereFollowingId($profile->id)->delete();
|
||||
break;
|
||||
|
||||
|
||||
default:
|
||||
# code...
|
||||
// code...
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
@ -229,6 +221,7 @@ trait PrivacySettings
|
|||
$settings->save();
|
||||
$profile->save();
|
||||
Cache::forget('profiles:private');
|
||||
|
||||
return [200];
|
||||
}
|
||||
}
|
||||
|
|
|
@ -81,14 +81,12 @@ class SettingsController extends Controller
|
|||
|
||||
public function dataImport()
|
||||
{
|
||||
abort_if(!config_cache('pixelfed.import.instagram.enabled'), 404);
|
||||
return view('settings.import.home');
|
||||
}
|
||||
|
||||
public function dataImportInstagram()
|
||||
{
|
||||
abort_if(!config_cache('pixelfed.import.instagram.enabled'), 404);
|
||||
return view('settings.import.instagram.home');
|
||||
abort(404);
|
||||
}
|
||||
|
||||
public function developers()
|
||||
|
@ -232,29 +230,51 @@ class SettingsController extends Controller
|
|||
|
||||
public function timelineSettings(Request $request)
|
||||
{
|
||||
$uid = $request->user()->id;
|
||||
$pid = $request->user()->profile_id;
|
||||
$top = Redis::zscore('pf:tl:top', $pid) != false;
|
||||
$replies = Redis::zscore('pf:tl:replies', $pid) != false;
|
||||
return view('settings.timeline', compact('top', 'replies'));
|
||||
$userSettings = UserSetting::firstOrCreate([
|
||||
'user_id' => $uid
|
||||
]);
|
||||
if(!$userSettings || !$userSettings->other) {
|
||||
$userSettings = [
|
||||
'enable_reblogs' => false,
|
||||
'photo_reblogs_only' => false
|
||||
];
|
||||
} else {
|
||||
$userSettings = array_merge([
|
||||
'enable_reblogs' => false,
|
||||
'photo_reblogs_only' => false
|
||||
],
|
||||
$userSettings->other);
|
||||
}
|
||||
return view('settings.timeline', compact('top', 'replies', 'userSettings'));
|
||||
}
|
||||
|
||||
public function updateTimelineSettings(Request $request)
|
||||
{
|
||||
$pid = $request->user()->profile_id;
|
||||
$top = $request->has('top') && $request->input('top') === 'on';
|
||||
$replies = $request->has('replies') && $request->input('replies') === 'on';
|
||||
|
||||
if($top) {
|
||||
Redis::zadd('pf:tl:top', $pid, $pid);
|
||||
} else {
|
||||
Redis::zrem('pf:tl:top', $pid);
|
||||
}
|
||||
|
||||
if($replies) {
|
||||
Redis::zadd('pf:tl:replies', $pid, $pid);
|
||||
} else {
|
||||
Redis::zrem('pf:tl:replies', $pid);
|
||||
}
|
||||
$pid = $request->user()->profile_id;
|
||||
$uid = $request->user()->id;
|
||||
$this->validate($request, [
|
||||
'enable_reblogs' => 'sometimes',
|
||||
'photo_reblogs_only' => 'sometimes'
|
||||
]);
|
||||
Redis::zrem('pf:tl:top', $pid);
|
||||
Redis::zrem('pf:tl:replies', $pid);
|
||||
$userSettings = UserSetting::firstOrCreate([
|
||||
'user_id' => $uid
|
||||
]);
|
||||
if($userSettings->other) {
|
||||
$other = $userSettings->other;
|
||||
$other['enable_reblogs'] = $request->has('enable_reblogs');
|
||||
$other['photo_reblogs_only'] = $request->has('photo_reblogs_only');
|
||||
} else {
|
||||
$other['enable_reblogs'] = $request->has('enable_reblogs');
|
||||
$other['photo_reblogs_only'] = $request->has('photo_reblogs_only');
|
||||
}
|
||||
$userSettings->other = $other;
|
||||
$userSettings->save();
|
||||
return redirect(route('settings'))->with('status', 'Timeline settings successfully updated!');
|
||||
}
|
||||
|
||||
|
|
|
@ -2,166 +2,202 @@
|
|||
|
||||
namespace App\Http\Controllers;
|
||||
|
||||
use App\Page;
|
||||
use App\Profile;
|
||||
use App\Services\FollowerService;
|
||||
use App\Status;
|
||||
use App\User;
|
||||
use App\Util\ActivityPub\Helpers;
|
||||
use App\Util\Localization\Localization;
|
||||
use Auth;
|
||||
use Cache;
|
||||
use Illuminate\Http\Request;
|
||||
use Illuminate\Support\Str;
|
||||
use App, Auth, Cache, View;
|
||||
use App\Util\Lexer\PrettyNumber;
|
||||
use App\{Follower, Page, Profile, Status, User, UserFilter};
|
||||
use App\Util\Localization\Localization;
|
||||
use App\Services\FollowerService;
|
||||
use App\Util\ActivityPub\Helpers;
|
||||
use View;
|
||||
|
||||
class SiteController extends Controller
|
||||
{
|
||||
public function home(Request $request)
|
||||
{
|
||||
if (Auth::check()) {
|
||||
return $this->homeTimeline($request);
|
||||
} else {
|
||||
return $this->homeGuest();
|
||||
}
|
||||
}
|
||||
public function home(Request $request)
|
||||
{
|
||||
if (Auth::check()) {
|
||||
return $this->homeTimeline($request);
|
||||
} else {
|
||||
return $this->homeGuest();
|
||||
}
|
||||
}
|
||||
|
||||
public function homeGuest()
|
||||
{
|
||||
return view('site.index');
|
||||
}
|
||||
public function homeGuest()
|
||||
{
|
||||
return view('site.index');
|
||||
}
|
||||
|
||||
public function homeTimeline(Request $request)
|
||||
{
|
||||
if($request->has('force_old_ui')) {
|
||||
return view('timeline.home', ['layout' => 'feed']);
|
||||
}
|
||||
public function homeTimeline(Request $request)
|
||||
{
|
||||
if ($request->has('force_old_ui')) {
|
||||
return view('timeline.home', ['layout' => 'feed']);
|
||||
}
|
||||
|
||||
return redirect('/i/web');
|
||||
}
|
||||
return redirect('/i/web');
|
||||
}
|
||||
|
||||
public function changeLocale(Request $request, $locale)
|
||||
{
|
||||
// todo: add other locales after pushing new l10n strings
|
||||
$locales = Localization::languages();
|
||||
if(in_array($locale, $locales)) {
|
||||
if($request->user()) {
|
||||
$user = $request->user();
|
||||
$user->language = $locale;
|
||||
$user->save();
|
||||
}
|
||||
session()->put('locale', $locale);
|
||||
}
|
||||
public function changeLocale(Request $request, $locale)
|
||||
{
|
||||
// todo: add other locales after pushing new l10n strings
|
||||
$locales = Localization::languages();
|
||||
if (in_array($locale, $locales)) {
|
||||
if ($request->user()) {
|
||||
$user = $request->user();
|
||||
$user->language = $locale;
|
||||
$user->save();
|
||||
}
|
||||
session()->put('locale', $locale);
|
||||
}
|
||||
|
||||
return redirect(route('site.language'));
|
||||
}
|
||||
return redirect(route('site.language'));
|
||||
}
|
||||
|
||||
public function about()
|
||||
{
|
||||
return Cache::remember('site.about_v2', now()->addMinutes(15), function() {
|
||||
$user_count = number_format(User::count());
|
||||
$post_count = number_format(Status::count());
|
||||
$rules = config_cache('app.rules') ? json_decode(config_cache('app.rules'), true) : null;
|
||||
return view('site.about', compact('rules', 'user_count', 'post_count'))->render();
|
||||
});
|
||||
}
|
||||
public function about()
|
||||
{
|
||||
return Cache::remember('site.about_v2', now()->addMinutes(15), function () {
|
||||
$user_count = number_format(User::count());
|
||||
$post_count = number_format(Status::count());
|
||||
$rules = config_cache('app.rules') ? json_decode(config_cache('app.rules'), true) : null;
|
||||
|
||||
public function language()
|
||||
{
|
||||
return view('site.language');
|
||||
}
|
||||
return view('site.about', compact('rules', 'user_count', 'post_count'))->render();
|
||||
});
|
||||
}
|
||||
|
||||
public function communityGuidelines(Request $request)
|
||||
{
|
||||
return Cache::remember('site:help:community-guidelines', now()->addDays(120), function() {
|
||||
$slug = '/site/kb/community-guidelines';
|
||||
$page = Page::whereSlug($slug)->whereActive(true)->first();
|
||||
return View::make('site.help.community-guidelines')->with(compact('page'))->render();
|
||||
});
|
||||
}
|
||||
public function language()
|
||||
{
|
||||
return view('site.language');
|
||||
}
|
||||
|
||||
public function privacy(Request $request)
|
||||
{
|
||||
$page = Cache::remember('site:privacy', now()->addDays(120), function() {
|
||||
$slug = '/site/privacy';
|
||||
return Page::whereSlug($slug)->whereActive(true)->first();
|
||||
});
|
||||
return View::make('site.privacy')->with(compact('page'))->render();
|
||||
}
|
||||
public function communityGuidelines(Request $request)
|
||||
{
|
||||
return Cache::remember('site:help:community-guidelines', now()->addDays(120), function () {
|
||||
$slug = '/site/kb/community-guidelines';
|
||||
$page = Page::whereSlug($slug)->whereActive(true)->first();
|
||||
|
||||
public function terms(Request $request)
|
||||
{
|
||||
$page = Cache::remember('site:terms', now()->addDays(120), function() {
|
||||
$slug = '/site/terms';
|
||||
return Page::whereSlug($slug)->whereActive(true)->first();
|
||||
});
|
||||
return View::make('site.terms')->with(compact('page'))->render();
|
||||
}
|
||||
return View::make('site.help.community-guidelines')->with(compact('page'))->render();
|
||||
});
|
||||
}
|
||||
|
||||
public function redirectUrl(Request $request)
|
||||
{
|
||||
abort_if(!$request->user(), 404);
|
||||
$this->validate($request, [
|
||||
'url' => 'required|url'
|
||||
]);
|
||||
$url = request()->input('url');
|
||||
abort_if(Helpers::validateUrl($url) == false, 404);
|
||||
return view('site.redirect', compact('url'));
|
||||
}
|
||||
public function privacy(Request $request)
|
||||
{
|
||||
$page = Cache::remember('site:privacy', now()->addDays(120), function () {
|
||||
$slug = '/site/privacy';
|
||||
|
||||
public function followIntent(Request $request)
|
||||
{
|
||||
$this->validate($request, [
|
||||
'user' => 'string|min:1|max:15|exists:users,username',
|
||||
]);
|
||||
$profile = Profile::whereUsername($request->input('user'))->firstOrFail();
|
||||
$user = $request->user();
|
||||
abort_if($user && $profile->id == $user->profile_id, 404);
|
||||
$following = $user != null ? FollowerService::follows($user->profile_id, $profile->id) : false;
|
||||
return view('site.intents.follow', compact('profile', 'user', 'following'));
|
||||
}
|
||||
return Page::whereSlug($slug)->whereActive(true)->first();
|
||||
});
|
||||
|
||||
public function legacyProfileRedirect(Request $request, $username)
|
||||
{
|
||||
$username = Str::contains($username, '@') ? '@' . $username : $username;
|
||||
if(str_contains($username, '@')) {
|
||||
$profile = Profile::whereUsername($username)
|
||||
->firstOrFail();
|
||||
return View::make('site.privacy')->with(compact('page'))->render();
|
||||
}
|
||||
|
||||
if($profile->domain == null) {
|
||||
$url = "/$profile->username";
|
||||
} else {
|
||||
$url = "/i/web/profile/_/{$profile->id}";
|
||||
}
|
||||
public function terms(Request $request)
|
||||
{
|
||||
$page = Cache::remember('site:terms', now()->addDays(120), function () {
|
||||
$slug = '/site/terms';
|
||||
|
||||
} else {
|
||||
$profile = Profile::whereUsername($username)
|
||||
->whereNull('domain')
|
||||
->firstOrFail();
|
||||
$url = "/$profile->username";
|
||||
}
|
||||
return Page::whereSlug($slug)->whereActive(true)->first();
|
||||
});
|
||||
|
||||
return redirect($url);
|
||||
}
|
||||
return View::make('site.terms')->with(compact('page'))->render();
|
||||
}
|
||||
|
||||
public function legacyWebfingerRedirect(Request $request, $username, $domain)
|
||||
{
|
||||
$un = '@'.$username.'@'.$domain;
|
||||
$profile = Profile::whereUsername($un)
|
||||
->firstOrFail();
|
||||
public function redirectUrl(Request $request)
|
||||
{
|
||||
abort_if(! $request->user(), 404);
|
||||
$this->validate($request, [
|
||||
'url' => 'required|url',
|
||||
]);
|
||||
$url = request()->input('url');
|
||||
abort_if(Helpers::validateUrl($url) == false, 404);
|
||||
|
||||
if($profile->domain == null) {
|
||||
$url = "/$profile->username";
|
||||
} else {
|
||||
$url = $request->user() ? "/i/web/profile/_/{$profile->id}" : $profile->url();
|
||||
}
|
||||
return view('site.redirect', compact('url'));
|
||||
}
|
||||
|
||||
return redirect($url);
|
||||
}
|
||||
public function followIntent(Request $request)
|
||||
{
|
||||
$this->validate($request, [
|
||||
'user' => 'string|min:1|max:15|exists:users,username',
|
||||
]);
|
||||
$profile = Profile::whereUsername($request->input('user'))->firstOrFail();
|
||||
$user = $request->user();
|
||||
abort_if($user && $profile->id == $user->profile_id, 404);
|
||||
$following = $user != null ? FollowerService::follows($user->profile_id, $profile->id) : false;
|
||||
|
||||
public function legalNotice(Request $request)
|
||||
{
|
||||
$page = Cache::remember('site:legal-notice', now()->addDays(120), function() {
|
||||
$slug = '/site/legal-notice';
|
||||
return Page::whereSlug($slug)->whereActive(true)->first();
|
||||
});
|
||||
abort_if(!$page, 404);
|
||||
return View::make('site.legal-notice')->with(compact('page'))->render();
|
||||
}
|
||||
return view('site.intents.follow', compact('profile', 'user', 'following'));
|
||||
}
|
||||
|
||||
public function legacyProfileRedirect(Request $request, $username)
|
||||
{
|
||||
$username = Str::contains($username, '@') ? '@'.$username : $username;
|
||||
if (str_contains($username, '@')) {
|
||||
$profile = Profile::whereUsername($username)
|
||||
->firstOrFail();
|
||||
|
||||
if ($profile->domain == null) {
|
||||
$url = "/$profile->username";
|
||||
} else {
|
||||
$url = "/i/web/profile/_/{$profile->id}";
|
||||
}
|
||||
|
||||
} else {
|
||||
$profile = Profile::whereUsername($username)
|
||||
->whereNull('domain')
|
||||
->firstOrFail();
|
||||
$url = "/$profile->username";
|
||||
}
|
||||
|
||||
return redirect($url);
|
||||
}
|
||||
|
||||
public function legacyWebfingerRedirect(Request $request, $username, $domain)
|
||||
{
|
||||
$un = '@'.$username.'@'.$domain;
|
||||
$profile = Profile::whereUsername($un)
|
||||
->firstOrFail();
|
||||
|
||||
if ($profile->domain == null) {
|
||||
$url = "/$profile->username";
|
||||
} else {
|
||||
$url = $request->user() ? "/i/web/profile/_/{$profile->id}" : $profile->url();
|
||||
}
|
||||
|
||||
return redirect($url);
|
||||
}
|
||||
|
||||
public function legalNotice(Request $request)
|
||||
{
|
||||
$page = Cache::remember('site:legal-notice', now()->addDays(120), function () {
|
||||
$slug = '/site/legal-notice';
|
||||
|
||||
return Page::whereSlug($slug)->whereActive(true)->first();
|
||||
});
|
||||
abort_if(! $page, 404);
|
||||
|
||||
return View::make('site.legal-notice')->with(compact('page'))->render();
|
||||
}
|
||||
|
||||
public function curatedOnboarding(Request $request)
|
||||
{
|
||||
if ($request->user()) {
|
||||
return redirect('/i/web');
|
||||
}
|
||||
|
||||
$regOpen = (bool) config_cache('pixelfed.open_registration');
|
||||
$curOnboarding = (bool) config_cache('instance.curated_registration.enabled');
|
||||
$curOnlyClosed = (bool) config('instance.curated_registration.state.only_enabled_on_closed_reg');
|
||||
if ($regOpen) {
|
||||
if ($curOnlyClosed) {
|
||||
return redirect('/register');
|
||||
}
|
||||
} else {
|
||||
if (! $curOnboarding) {
|
||||
return redirect('/');
|
||||
}
|
||||
}
|
||||
|
||||
return view('auth.curated-register.index', ['step' => 1]);
|
||||
}
|
||||
}
|
||||
|
|
|
@ -0,0 +1,21 @@
|
|||
<?php
|
||||
|
||||
namespace App\Http\Controllers;
|
||||
|
||||
use Illuminate\Http\Request;
|
||||
use App\Services\Internal\SoftwareUpdateService;
|
||||
|
||||
class SoftwareUpdateController extends Controller
|
||||
{
|
||||
public function __construct()
|
||||
{
|
||||
$this->middleware('auth');
|
||||
$this->middleware('admin');
|
||||
}
|
||||
|
||||
public function getSoftwareUpdateCheck(Request $request)
|
||||
{
|
||||
$res = SoftwareUpdateService::get();
|
||||
return $res;
|
||||
}
|
||||
}
|
|
@ -2,442 +2,486 @@
|
|||
|
||||
namespace App\Http\Controllers;
|
||||
|
||||
use App\Jobs\ImageOptimizePipeline\ImageOptimize;
|
||||
use App\Jobs\StatusPipeline\NewStatusPipeline;
|
||||
use App\Jobs\StatusPipeline\StatusDelete;
|
||||
use App\AccountInterstitial;
|
||||
use App\Jobs\SharePipeline\SharePipeline;
|
||||
use App\Jobs\SharePipeline\UndoSharePipeline;
|
||||
use App\AccountInterstitial;
|
||||
use App\Media;
|
||||
use App\Jobs\StatusPipeline\RemoteStatusDelete;
|
||||
use App\Jobs\StatusPipeline\StatusDelete;
|
||||
use App\Profile;
|
||||
use App\Services\AccountService;
|
||||
use App\Services\HashidService;
|
||||
use App\Services\ReblogService;
|
||||
use App\Services\StatusService;
|
||||
use App\Status;
|
||||
use App\StatusArchived;
|
||||
use App\StatusView;
|
||||
use App\Transformer\ActivityPub\StatusTransformer;
|
||||
use App\Transformer\ActivityPub\Verb\Note;
|
||||
use App\Transformer\ActivityPub\Verb\Question;
|
||||
use App\User;
|
||||
use Auth, DB, Cache;
|
||||
use App\Util\Media\License;
|
||||
use Auth;
|
||||
use Cache;
|
||||
use DB;
|
||||
use Illuminate\Http\Request;
|
||||
use League\Fractal;
|
||||
use App\Util\Media\Filter;
|
||||
use Illuminate\Support\Str;
|
||||
use App\Services\HashidService;
|
||||
use App\Services\StatusService;
|
||||
use App\Util\Media\License;
|
||||
use App\Services\ReblogService;
|
||||
|
||||
class StatusController extends Controller
|
||||
{
|
||||
public function show(Request $request, $username, $id)
|
||||
{
|
||||
// redirect authed users to Metro 2.0
|
||||
if($request->user()) {
|
||||
// unless they force static view
|
||||
if(!$request->has('fs') || $request->input('fs') != '1') {
|
||||
return redirect('/i/web/post/' . $id);
|
||||
}
|
||||
}
|
||||
public function show(Request $request, $username, $id)
|
||||
{
|
||||
// redirect authed users to Metro 2.0
|
||||
if ($request->user()) {
|
||||
// unless they force static view
|
||||
if (! $request->has('fs') || $request->input('fs') != '1') {
|
||||
return redirect('/i/web/post/'.$id);
|
||||
}
|
||||
}
|
||||
|
||||
$user = Profile::whereNull('domain')->whereUsername($username)->firstOrFail();
|
||||
$user = Profile::whereNull('domain')->whereUsername($username)->firstOrFail();
|
||||
|
||||
if($user->status != null) {
|
||||
return ProfileController::accountCheck($user);
|
||||
}
|
||||
if ($user->status != null) {
|
||||
return ProfileController::accountCheck($user);
|
||||
}
|
||||
|
||||
$status = Status::whereProfileId($user->id)
|
||||
->whereNull('reblog_of_id')
|
||||
->whereIn('scope', ['public','unlisted', 'private'])
|
||||
->findOrFail($id);
|
||||
$status = Status::whereProfileId($user->id)
|
||||
->whereNull('reblog_of_id')
|
||||
->whereIn('scope', ['public', 'unlisted', 'private'])
|
||||
->findOrFail($id);
|
||||
|
||||
if($status->uri || $status->url) {
|
||||
$url = $status->uri ?? $status->url;
|
||||
if(ends_with($url, '/activity')) {
|
||||
$url = str_replace('/activity', '', $url);
|
||||
}
|
||||
return redirect($url);
|
||||
}
|
||||
if ($status->uri || $status->url) {
|
||||
$url = $status->uri ?? $status->url;
|
||||
if (ends_with($url, '/activity')) {
|
||||
$url = str_replace('/activity', '', $url);
|
||||
}
|
||||
|
||||
if($status->visibility == 'private' || $user->is_private) {
|
||||
if(!Auth::check()) {
|
||||
abort(404);
|
||||
}
|
||||
$pid = Auth::user()->profile;
|
||||
if($user->followedBy($pid) == false && $user->id !== $pid->id && Auth::user()->is_admin == false) {
|
||||
abort(404);
|
||||
}
|
||||
}
|
||||
return redirect($url);
|
||||
}
|
||||
|
||||
if($status->type == 'archived') {
|
||||
if(Auth::user()->profile_id !== $status->profile_id) {
|
||||
abort(404);
|
||||
}
|
||||
}
|
||||
if ($status->visibility == 'private' || $user->is_private) {
|
||||
if (! Auth::check()) {
|
||||
abort(404);
|
||||
}
|
||||
$pid = Auth::user()->profile;
|
||||
if ($user->followedBy($pid) == false && $user->id !== $pid->id && Auth::user()->is_admin == false) {
|
||||
abort(404);
|
||||
}
|
||||
}
|
||||
|
||||
if($request->user() && $request->user()->profile_id != $status->profile_id) {
|
||||
StatusView::firstOrCreate([
|
||||
'status_id' => $status->id,
|
||||
'status_profile_id' => $status->profile_id,
|
||||
'profile_id' => $request->user()->profile_id
|
||||
]);
|
||||
}
|
||||
if ($status->type == 'archived') {
|
||||
if (Auth::user()->profile_id !== $status->profile_id) {
|
||||
abort(404);
|
||||
}
|
||||
}
|
||||
|
||||
if ($request->wantsJson() && config_cache('federation.activitypub.enabled')) {
|
||||
return $this->showActivityPub($request, $status);
|
||||
}
|
||||
if ($request->user() && $request->user()->profile_id != $status->profile_id) {
|
||||
StatusView::firstOrCreate([
|
||||
'status_id' => $status->id,
|
||||
'status_profile_id' => $status->profile_id,
|
||||
'profile_id' => $request->user()->profile_id,
|
||||
]);
|
||||
}
|
||||
|
||||
$template = $status->in_reply_to_id ? 'status.reply' : 'status.show';
|
||||
return view($template, compact('user', 'status'));
|
||||
}
|
||||
if ($request->wantsJson() && (bool) config_cache('federation.activitypub.enabled')) {
|
||||
return $this->showActivityPub($request, $status);
|
||||
}
|
||||
|
||||
public function shortcodeRedirect(Request $request, $id)
|
||||
{
|
||||
abort(404);
|
||||
}
|
||||
$template = $status->in_reply_to_id ? 'status.reply' : 'status.show';
|
||||
|
||||
public function showId(int $id)
|
||||
{
|
||||
abort(404);
|
||||
$status = Status::whereNull('reblog_of_id')
|
||||
->whereIn('scope', ['public', 'unlisted'])
|
||||
->findOrFail($id);
|
||||
return redirect($status->url());
|
||||
}
|
||||
return view($template, compact('user', 'status'));
|
||||
}
|
||||
|
||||
public function showEmbed(Request $request, $username, int $id)
|
||||
{
|
||||
if(!config('instance.embed.post')) {
|
||||
$res = view('status.embed-removed');
|
||||
return response($res)->withHeaders(['X-Frame-Options' => 'ALLOWALL']);
|
||||
}
|
||||
public function shortcodeRedirect(Request $request, $id)
|
||||
{
|
||||
$hid = HashidService::decode($id);
|
||||
abort_if(! $hid, 404);
|
||||
|
||||
$profile = Profile::whereNull(['domain','status'])
|
||||
->whereIsPrivate(false)
|
||||
->whereUsername($username)
|
||||
->first();
|
||||
if(!$profile) {
|
||||
$content = view('status.embed-removed');
|
||||
return response($content)->header('X-Frame-Options', 'ALLOWALL');
|
||||
}
|
||||
$status = Status::whereProfileId($profile->id)
|
||||
->whereNull('uri')
|
||||
->whereScope('public')
|
||||
->whereIsNsfw(false)
|
||||
->whereIn('type', ['photo', 'video','photo:album'])
|
||||
->find($id);
|
||||
if(!$status) {
|
||||
$content = view('status.embed-removed');
|
||||
return response($content)->header('X-Frame-Options', 'ALLOWALL');
|
||||
}
|
||||
$showLikes = $request->filled('likes') && $request->likes == true;
|
||||
$showCaption = $request->filled('caption') && $request->caption !== false;
|
||||
$layout = $request->filled('layout') && $request->layout == 'compact' ? 'compact' : 'full';
|
||||
$content = view('status.embed', compact('status', 'showLikes', 'showCaption', 'layout'));
|
||||
return response($content)->withHeaders(['X-Frame-Options' => 'ALLOWALL']);
|
||||
}
|
||||
return redirect('/i/web/post/'.$hid);
|
||||
}
|
||||
|
||||
public function showObject(Request $request, $username, int $id)
|
||||
{
|
||||
$user = Profile::whereNull('domain')->whereUsername($username)->firstOrFail();
|
||||
public function showId(int $id)
|
||||
{
|
||||
abort(404);
|
||||
$status = Status::whereNull('reblog_of_id')
|
||||
->whereIn('scope', ['public', 'unlisted'])
|
||||
->findOrFail($id);
|
||||
|
||||
if($user->status != null) {
|
||||
return ProfileController::accountCheck($user);
|
||||
}
|
||||
return redirect($status->url());
|
||||
}
|
||||
|
||||
$status = Status::whereProfileId($user->id)
|
||||
->whereNotIn('visibility',['draft','direct'])
|
||||
->findOrFail($id);
|
||||
public function showEmbed(Request $request, $username, int $id)
|
||||
{
|
||||
if (! (bool) config_cache('instance.embed.post')) {
|
||||
$res = view('status.embed-removed');
|
||||
|
||||
abort_if($status->uri, 404);
|
||||
return response($res)->withHeaders(['X-Frame-Options' => 'ALLOWALL']);
|
||||
}
|
||||
|
||||
if($status->visibility == 'private' || $user->is_private) {
|
||||
if(!Auth::check()) {
|
||||
abort(403);
|
||||
}
|
||||
$pid = Auth::user()->profile;
|
||||
if($user->followedBy($pid) == false && $user->id !== $pid->id) {
|
||||
abort(403);
|
||||
}
|
||||
}
|
||||
$status = StatusService::get($id);
|
||||
|
||||
return $this->showActivityPub($request, $status);
|
||||
}
|
||||
if (
|
||||
! $status ||
|
||||
! isset($status['account'], $status['account']['id'], $status['local']) ||
|
||||
! $status['local'] ||
|
||||
strtolower($status['account']['username']) !== strtolower($username)
|
||||
) {
|
||||
$content = view('status.embed-removed');
|
||||
|
||||
public function compose()
|
||||
{
|
||||
$this->authCheck();
|
||||
return response($content, 404)->header('X-Frame-Options', 'ALLOWALL');
|
||||
}
|
||||
|
||||
return view('status.compose');
|
||||
}
|
||||
$profile = AccountService::get($status['account']['id'], true);
|
||||
|
||||
public function store(Request $request)
|
||||
{
|
||||
return;
|
||||
}
|
||||
if (! $profile || $profile['locked'] || ! $profile['local']) {
|
||||
$content = view('status.embed-removed');
|
||||
|
||||
public function delete(Request $request)
|
||||
{
|
||||
$this->authCheck();
|
||||
return response($content)->header('X-Frame-Options', 'ALLOWALL');
|
||||
}
|
||||
|
||||
$this->validate($request, [
|
||||
'item' => 'required|integer|min:1',
|
||||
]);
|
||||
$aiCheck = Cache::remember('profile:ai-check:spam-login:'.$profile['id'], 3600, function () use ($profile) {
|
||||
$user = Profile::find($profile['id']);
|
||||
if (! $user) {
|
||||
return true;
|
||||
}
|
||||
$exists = AccountInterstitial::whereUserId($user->user_id)->where('is_spam', 1)->count();
|
||||
if ($exists) {
|
||||
return true;
|
||||
}
|
||||
|
||||
$status = Status::findOrFail($request->input('item'));
|
||||
return false;
|
||||
});
|
||||
|
||||
$user = Auth::user();
|
||||
if ($aiCheck) {
|
||||
$res = view('status.embed-removed');
|
||||
|
||||
if($status->profile_id != $user->profile->id &&
|
||||
$user->is_admin == true &&
|
||||
$status->uri == null
|
||||
) {
|
||||
$media = $status->media;
|
||||
return response($res)->withHeaders(['X-Frame-Options' => 'ALLOWALL']);
|
||||
}
|
||||
|
||||
$ai = new AccountInterstitial;
|
||||
$ai->user_id = $status->profile->user_id;
|
||||
$ai->type = 'post.removed';
|
||||
$ai->view = 'account.moderation.post.removed';
|
||||
$ai->item_type = 'App\Status';
|
||||
$ai->item_id = $status->id;
|
||||
$ai->has_media = (bool) $media->count();
|
||||
$ai->blurhash = $media->count() ? $media->first()->blurhash : null;
|
||||
$ai->meta = json_encode([
|
||||
'caption' => $status->caption,
|
||||
'created_at' => $status->created_at,
|
||||
'type' => $status->type,
|
||||
'url' => $status->url(),
|
||||
'is_nsfw' => $status->is_nsfw,
|
||||
'scope' => $status->scope,
|
||||
'reblog' => $status->reblog_of_id,
|
||||
'likes_count' => $status->likes_count,
|
||||
'reblogs_count' => $status->reblogs_count,
|
||||
]);
|
||||
$ai->save();
|
||||
$status = StatusService::get($id);
|
||||
|
||||
$u = $status->profile->user;
|
||||
$u->has_interstitial = true;
|
||||
$u->save();
|
||||
}
|
||||
if (
|
||||
! $status ||
|
||||
! isset($status['account'], $status['account']['id']) ||
|
||||
intval($status['account']['id']) !== intval($profile['id']) ||
|
||||
$status['sensitive'] ||
|
||||
$status['visibility'] !== 'public' ||
|
||||
$status['pf_type'] !== 'photo'
|
||||
) {
|
||||
$content = view('status.embed-removed');
|
||||
|
||||
if($status->in_reply_to_id) {
|
||||
$parent = Status::find($status->in_reply_to_id);
|
||||
if($parent && ($parent->profile_id == $user->profile_id) || ($status->profile_id == $user->profile_id) || $user->is_admin) {
|
||||
Cache::forget('_api:statuses:recent_9:' . $status->profile_id);
|
||||
Cache::forget('profile:status_count:' . $status->profile_id);
|
||||
Cache::forget('profile:embed:' . $status->profile_id);
|
||||
StatusService::del($status->id, true);
|
||||
Cache::forget('profile:status_count:'.$status->profile_id);
|
||||
StatusDelete::dispatch($status);
|
||||
}
|
||||
} else if ($status->profile_id == $user->profile_id || $user->is_admin == true) {
|
||||
Cache::forget('_api:statuses:recent_9:' . $status->profile_id);
|
||||
Cache::forget('profile:status_count:' . $status->profile_id);
|
||||
Cache::forget('profile:embed:' . $status->profile_id);
|
||||
StatusService::del($status->id, true);
|
||||
Cache::forget('profile:status_count:'.$status->profile_id);
|
||||
StatusDelete::dispatch($status);
|
||||
}
|
||||
return response($content)->header('X-Frame-Options', 'ALLOWALL');
|
||||
}
|
||||
|
||||
if($request->wantsJson()) {
|
||||
return response()->json(['Status successfully deleted.']);
|
||||
} else {
|
||||
return redirect($user->url());
|
||||
}
|
||||
}
|
||||
$showLikes = $request->filled('likes') && $request->likes == true;
|
||||
$showCaption = $request->filled('caption') && $request->caption !== false;
|
||||
$layout = $request->filled('layout') && $request->layout == 'compact' ? 'compact' : 'full';
|
||||
$content = view('status.embed', compact('status', 'showLikes', 'showCaption', 'layout'));
|
||||
|
||||
public function storeShare(Request $request)
|
||||
{
|
||||
$this->authCheck();
|
||||
return response($content)->withHeaders(['X-Frame-Options' => 'ALLOWALL']);
|
||||
}
|
||||
|
||||
$this->validate($request, [
|
||||
'item' => 'required|integer|min:1',
|
||||
]);
|
||||
public function showObject(Request $request, $username, int $id)
|
||||
{
|
||||
$user = Profile::whereNull('domain')->whereUsername($username)->firstOrFail();
|
||||
|
||||
$user = Auth::user();
|
||||
$profile = $user->profile;
|
||||
$status = Status::whereScope('public')
|
||||
->findOrFail($request->input('item'));
|
||||
if ($user->status != null) {
|
||||
return ProfileController::accountCheck($user);
|
||||
}
|
||||
|
||||
$count = $status->reblogs_count;
|
||||
$status = Status::whereProfileId($user->id)
|
||||
->whereNotIn('visibility', ['draft', 'direct'])
|
||||
->findOrFail($id);
|
||||
|
||||
$exists = Status::whereProfileId(Auth::user()->profile->id)
|
||||
->whereReblogOfId($status->id)
|
||||
->exists();
|
||||
if ($exists == true) {
|
||||
$shares = Status::whereProfileId(Auth::user()->profile->id)
|
||||
->whereReblogOfId($status->id)
|
||||
->get();
|
||||
foreach ($shares as $share) {
|
||||
UndoSharePipeline::dispatch($share);
|
||||
ReblogService::del($profile->id, $status->id);
|
||||
$count--;
|
||||
}
|
||||
} else {
|
||||
$share = new Status();
|
||||
$share->profile_id = $profile->id;
|
||||
$share->reblog_of_id = $status->id;
|
||||
$share->in_reply_to_profile_id = $status->profile_id;
|
||||
$share->type = 'share';
|
||||
$share->save();
|
||||
$count++;
|
||||
SharePipeline::dispatch($share);
|
||||
ReblogService::add($profile->id, $status->id);
|
||||
}
|
||||
abort_if($status->uri, 404);
|
||||
|
||||
Cache::forget('status:'.$status->id.':sharedby:userid:'.$user->id);
|
||||
StatusService::del($status->id);
|
||||
if ($status->visibility == 'private' || $user->is_private) {
|
||||
if (! Auth::check()) {
|
||||
abort(403);
|
||||
}
|
||||
$pid = Auth::user()->profile;
|
||||
if ($user->followedBy($pid) == false && $user->id !== $pid->id) {
|
||||
abort(403);
|
||||
}
|
||||
}
|
||||
|
||||
if ($request->ajax()) {
|
||||
$response = ['code' => 200, 'msg' => 'Share saved', 'count' => $count];
|
||||
} else {
|
||||
$response = redirect($status->url());
|
||||
}
|
||||
return $this->showActivityPub($request, $status);
|
||||
}
|
||||
|
||||
return $response;
|
||||
}
|
||||
public function compose()
|
||||
{
|
||||
$this->authCheck();
|
||||
|
||||
public function showActivityPub(Request $request, $status)
|
||||
{
|
||||
$object = $status->type == 'poll' ? new Question() : new Note();
|
||||
$fractal = new Fractal\Manager();
|
||||
$resource = new Fractal\Resource\Item($status, $object);
|
||||
$res = $fractal->createData($resource)->toArray();
|
||||
return view('status.compose');
|
||||
}
|
||||
|
||||
return response()->json($res['data'], 200, ['Content-Type' => 'application/activity+json'], JSON_PRETTY_PRINT|JSON_UNESCAPED_SLASHES);
|
||||
}
|
||||
public function store(Request $request)
|
||||
{
|
||||
|
||||
public function edit(Request $request, $username, $id)
|
||||
{
|
||||
$this->authCheck();
|
||||
$user = Auth::user()->profile;
|
||||
$status = Status::whereProfileId($user->id)
|
||||
->with(['media'])
|
||||
->findOrFail($id);
|
||||
$licenses = License::get();
|
||||
return view('status.edit', compact('user', 'status', 'licenses'));
|
||||
}
|
||||
}
|
||||
|
||||
public function editStore(Request $request, $username, $id)
|
||||
{
|
||||
$this->authCheck();
|
||||
$user = Auth::user()->profile;
|
||||
$status = Status::whereProfileId($user->id)
|
||||
->with(['media'])
|
||||
->findOrFail($id);
|
||||
public function delete(Request $request)
|
||||
{
|
||||
$this->authCheck();
|
||||
|
||||
$this->validate($request, [
|
||||
'license' => 'nullable|integer|min:1|max:16',
|
||||
]);
|
||||
$this->validate($request, [
|
||||
'item' => 'required|integer|min:1',
|
||||
]);
|
||||
|
||||
$licenseId = $request->input('license');
|
||||
$status = Status::findOrFail($request->input('item'));
|
||||
|
||||
$status->media->each(function($media) use($licenseId) {
|
||||
$media->license = $licenseId;
|
||||
$media->save();
|
||||
Cache::forget('status:transformer:media:attachments:'.$media->status_id);
|
||||
});
|
||||
$user = Auth::user();
|
||||
|
||||
return redirect($status->url());
|
||||
}
|
||||
if ($status->profile_id != $user->profile->id &&
|
||||
$user->is_admin == true &&
|
||||
$status->uri == null
|
||||
) {
|
||||
$media = $status->media;
|
||||
|
||||
protected function authCheck()
|
||||
{
|
||||
if (Auth::check() == false) {
|
||||
abort(403);
|
||||
}
|
||||
}
|
||||
$ai = new AccountInterstitial;
|
||||
$ai->user_id = $status->profile->user_id;
|
||||
$ai->type = 'post.removed';
|
||||
$ai->view = 'account.moderation.post.removed';
|
||||
$ai->item_type = 'App\Status';
|
||||
$ai->item_id = $status->id;
|
||||
$ai->has_media = (bool) $media->count();
|
||||
$ai->blurhash = $media->count() ? $media->first()->blurhash : null;
|
||||
$ai->meta = json_encode([
|
||||
'caption' => $status->caption,
|
||||
'created_at' => $status->created_at,
|
||||
'type' => $status->type,
|
||||
'url' => $status->url(),
|
||||
'is_nsfw' => $status->is_nsfw,
|
||||
'scope' => $status->scope,
|
||||
'reblog' => $status->reblog_of_id,
|
||||
'likes_count' => $status->likes_count,
|
||||
'reblogs_count' => $status->reblogs_count,
|
||||
]);
|
||||
$ai->save();
|
||||
|
||||
protected function validateVisibility($visibility)
|
||||
{
|
||||
$allowed = ['public', 'unlisted', 'private'];
|
||||
return in_array($visibility, $allowed) ? $visibility : 'public';
|
||||
}
|
||||
$u = $status->profile->user;
|
||||
$u->has_interstitial = true;
|
||||
$u->save();
|
||||
}
|
||||
|
||||
public static function mimeTypeCheck($mimes)
|
||||
{
|
||||
$allowed = explode(',', config_cache('pixelfed.media_types'));
|
||||
$count = count($mimes);
|
||||
$photos = 0;
|
||||
$videos = 0;
|
||||
foreach($mimes as $mime) {
|
||||
if(in_array($mime, $allowed) == false && $mime !== 'video/mp4') {
|
||||
continue;
|
||||
}
|
||||
if(str_contains($mime, 'image/')) {
|
||||
$photos++;
|
||||
}
|
||||
if(str_contains($mime, 'video/')) {
|
||||
$videos++;
|
||||
}
|
||||
}
|
||||
if($photos == 1 && $videos == 0) {
|
||||
return 'photo';
|
||||
}
|
||||
if($videos == 1 && $photos == 0) {
|
||||
return 'video';
|
||||
}
|
||||
if($photos > 1 && $videos == 0) {
|
||||
return 'photo:album';
|
||||
}
|
||||
if($videos > 1 && $photos == 0) {
|
||||
return 'video:album';
|
||||
}
|
||||
if($photos >= 1 && $videos >= 1) {
|
||||
return 'photo:video:album';
|
||||
}
|
||||
if ($status->in_reply_to_id) {
|
||||
$parent = Status::find($status->in_reply_to_id);
|
||||
if ($parent && ($parent->profile_id == $user->profile_id) || ($status->profile_id == $user->profile_id) || $user->is_admin) {
|
||||
Cache::forget('_api:statuses:recent_9:'.$status->profile_id);
|
||||
Cache::forget('profile:status_count:'.$status->profile_id);
|
||||
Cache::forget('profile:embed:'.$status->profile_id);
|
||||
StatusService::del($status->id, true);
|
||||
Cache::forget('profile:status_count:'.$status->profile_id);
|
||||
$status->uri ? RemoteStatusDelete::dispatch($status) : StatusDelete::dispatch($status);
|
||||
}
|
||||
} elseif ($status->profile_id == $user->profile_id || $user->is_admin == true) {
|
||||
Cache::forget('_api:statuses:recent_9:'.$status->profile_id);
|
||||
Cache::forget('profile:status_count:'.$status->profile_id);
|
||||
Cache::forget('profile:embed:'.$status->profile_id);
|
||||
StatusService::del($status->id, true);
|
||||
Cache::forget('profile:status_count:'.$status->profile_id);
|
||||
$status->uri ? RemoteStatusDelete::dispatch($status) : StatusDelete::dispatch($status);
|
||||
}
|
||||
|
||||
return 'text';
|
||||
}
|
||||
if ($request->wantsJson()) {
|
||||
return response()->json(['Status successfully deleted.']);
|
||||
} else {
|
||||
return redirect($user->url());
|
||||
}
|
||||
}
|
||||
|
||||
public function toggleVisibility(Request $request) {
|
||||
$this->authCheck();
|
||||
$this->validate($request, [
|
||||
'item' => 'required|string|min:1|max:20',
|
||||
'disableComments' => 'required|boolean'
|
||||
]);
|
||||
public function storeShare(Request $request)
|
||||
{
|
||||
$this->authCheck();
|
||||
|
||||
$user = Auth::user();
|
||||
$id = $request->input('item');
|
||||
$state = $request->input('disableComments');
|
||||
$this->validate($request, [
|
||||
'item' => 'required|integer|min:1',
|
||||
]);
|
||||
|
||||
$status = Status::findOrFail($id);
|
||||
$user = Auth::user();
|
||||
$profile = $user->profile;
|
||||
$status = Status::whereScope('public')
|
||||
->findOrFail($request->input('item'));
|
||||
|
||||
if($status->profile_id != $user->profile->id && $user->is_admin == false) {
|
||||
abort(403);
|
||||
}
|
||||
$count = $status->reblogs_count;
|
||||
|
||||
$status->comments_disabled = $status->comments_disabled == true ? false : true;
|
||||
$status->save();
|
||||
$exists = Status::whereProfileId(Auth::user()->profile->id)
|
||||
->whereReblogOfId($status->id)
|
||||
->exists();
|
||||
if ($exists == true) {
|
||||
$shares = Status::whereProfileId(Auth::user()->profile->id)
|
||||
->whereReblogOfId($status->id)
|
||||
->get();
|
||||
foreach ($shares as $share) {
|
||||
UndoSharePipeline::dispatch($share);
|
||||
ReblogService::del($profile->id, $status->id);
|
||||
$count--;
|
||||
}
|
||||
} else {
|
||||
$share = new Status();
|
||||
$share->profile_id = $profile->id;
|
||||
$share->reblog_of_id = $status->id;
|
||||
$share->in_reply_to_profile_id = $status->profile_id;
|
||||
$share->type = 'share';
|
||||
$share->save();
|
||||
$count++;
|
||||
SharePipeline::dispatch($share);
|
||||
ReblogService::add($profile->id, $status->id);
|
||||
}
|
||||
|
||||
return response()->json([200]);
|
||||
}
|
||||
Cache::forget('status:'.$status->id.':sharedby:userid:'.$user->id);
|
||||
StatusService::del($status->id);
|
||||
|
||||
public function storeView(Request $request)
|
||||
{
|
||||
abort_if(!$request->user(), 403);
|
||||
if ($request->ajax()) {
|
||||
$response = ['code' => 200, 'msg' => 'Share saved', 'count' => $count];
|
||||
} else {
|
||||
$response = redirect($status->url());
|
||||
}
|
||||
|
||||
$views = $request->input('_v');
|
||||
$uid = $request->user()->profile_id;
|
||||
return $response;
|
||||
}
|
||||
|
||||
if(empty($views) || !is_array($views)) {
|
||||
return response()->json(0);
|
||||
}
|
||||
public function showActivityPub(Request $request, $status)
|
||||
{
|
||||
$object = $status->type == 'poll' ? new Question() : new Note();
|
||||
$fractal = new Fractal\Manager();
|
||||
$resource = new Fractal\Resource\Item($status, $object);
|
||||
$res = $fractal->createData($resource)->toArray();
|
||||
|
||||
Cache::forget('profile:home-timeline-cursor:' . $request->user()->id);
|
||||
return response()->json($res['data'], 200, ['Content-Type' => 'application/activity+json'], JSON_PRETTY_PRINT | JSON_UNESCAPED_SLASHES);
|
||||
}
|
||||
|
||||
foreach($views as $view) {
|
||||
if(!isset($view['sid']) || !isset($view['pid'])) {
|
||||
continue;
|
||||
}
|
||||
DB::transaction(function () use($view, $uid) {
|
||||
StatusView::firstOrCreate([
|
||||
'status_id' => $view['sid'],
|
||||
'status_profile_id' => $view['pid'],
|
||||
'profile_id' => $uid
|
||||
]);
|
||||
});
|
||||
}
|
||||
public function edit(Request $request, $username, $id)
|
||||
{
|
||||
$this->authCheck();
|
||||
$user = Auth::user()->profile;
|
||||
$status = Status::whereProfileId($user->id)
|
||||
->with(['media'])
|
||||
->findOrFail($id);
|
||||
$licenses = License::get();
|
||||
|
||||
return response()->json(1);
|
||||
}
|
||||
return view('status.edit', compact('user', 'status', 'licenses'));
|
||||
}
|
||||
|
||||
public function editStore(Request $request, $username, $id)
|
||||
{
|
||||
$this->authCheck();
|
||||
$user = Auth::user()->profile;
|
||||
$status = Status::whereProfileId($user->id)
|
||||
->with(['media'])
|
||||
->findOrFail($id);
|
||||
|
||||
$this->validate($request, [
|
||||
'license' => 'nullable|integer|min:1|max:16',
|
||||
]);
|
||||
|
||||
$licenseId = $request->input('license');
|
||||
|
||||
$status->media->each(function ($media) use ($licenseId) {
|
||||
$media->license = $licenseId;
|
||||
$media->save();
|
||||
Cache::forget('status:transformer:media:attachments:'.$media->status_id);
|
||||
});
|
||||
|
||||
return redirect($status->url());
|
||||
}
|
||||
|
||||
protected function authCheck()
|
||||
{
|
||||
if (Auth::check() == false) {
|
||||
abort(403);
|
||||
}
|
||||
}
|
||||
|
||||
protected function validateVisibility($visibility)
|
||||
{
|
||||
$allowed = ['public', 'unlisted', 'private'];
|
||||
|
||||
return in_array($visibility, $allowed) ? $visibility : 'public';
|
||||
}
|
||||
|
||||
public static function mimeTypeCheck($mimes)
|
||||
{
|
||||
$allowed = explode(',', config_cache('pixelfed.media_types'));
|
||||
$count = count($mimes);
|
||||
$photos = 0;
|
||||
$videos = 0;
|
||||
foreach ($mimes as $mime) {
|
||||
if (in_array($mime, $allowed) == false && $mime !== 'video/mp4') {
|
||||
continue;
|
||||
}
|
||||
if (str_contains($mime, 'image/')) {
|
||||
$photos++;
|
||||
}
|
||||
if (str_contains($mime, 'video/')) {
|
||||
$videos++;
|
||||
}
|
||||
}
|
||||
if ($photos == 1 && $videos == 0) {
|
||||
return 'photo';
|
||||
}
|
||||
if ($videos == 1 && $photos == 0) {
|
||||
return 'video';
|
||||
}
|
||||
if ($photos > 1 && $videos == 0) {
|
||||
return 'photo:album';
|
||||
}
|
||||
if ($videos > 1 && $photos == 0) {
|
||||
return 'video:album';
|
||||
}
|
||||
if ($photos >= 1 && $videos >= 1) {
|
||||
return 'photo:video:album';
|
||||
}
|
||||
|
||||
return 'text';
|
||||
}
|
||||
|
||||
public function toggleVisibility(Request $request)
|
||||
{
|
||||
$this->authCheck();
|
||||
$this->validate($request, [
|
||||
'item' => 'required|string|min:1|max:20',
|
||||
'disableComments' => 'required|boolean',
|
||||
]);
|
||||
|
||||
$user = Auth::user();
|
||||
$id = $request->input('item');
|
||||
$state = $request->input('disableComments');
|
||||
|
||||
$status = Status::findOrFail($id);
|
||||
|
||||
if ($status->profile_id != $user->profile->id && $user->is_admin == false) {
|
||||
abort(403);
|
||||
}
|
||||
|
||||
$status->comments_disabled = $status->comments_disabled == true ? false : true;
|
||||
$status->save();
|
||||
|
||||
return response()->json([200]);
|
||||
}
|
||||
|
||||
public function storeView(Request $request)
|
||||
{
|
||||
abort_if(! $request->user(), 403);
|
||||
|
||||
$views = $request->input('_v');
|
||||
$uid = $request->user()->profile_id;
|
||||
|
||||
if (empty($views) || ! is_array($views)) {
|
||||
return response()->json(0);
|
||||
}
|
||||
|
||||
Cache::forget('profile:home-timeline-cursor:'.$request->user()->id);
|
||||
|
||||
foreach ($views as $view) {
|
||||
if (! isset($view['sid']) || ! isset($view['pid'])) {
|
||||
continue;
|
||||
}
|
||||
DB::transaction(function () use ($view, $uid) {
|
||||
StatusView::firstOrCreate([
|
||||
'status_id' => $view['sid'],
|
||||
'status_profile_id' => $view['pid'],
|
||||
'profile_id' => $uid,
|
||||
]);
|
||||
});
|
||||
}
|
||||
|
||||
return response()->json(1);
|
||||
}
|
||||
}
|
||||
|
|
|
@ -0,0 +1,60 @@
|
|||
<?php
|
||||
|
||||
namespace App\Http\Controllers;
|
||||
|
||||
use Illuminate\Http\Request;
|
||||
use App\Http\Requests\Status\StoreStatusEditRequest;
|
||||
use App\Status;
|
||||
use App\Models\StatusEdit;
|
||||
use Purify;
|
||||
use App\Services\Status\UpdateStatusService;
|
||||
use App\Services\StatusService;
|
||||
use App\Util\Lexer\Autolink;
|
||||
use App\Jobs\StatusPipeline\StatusLocalUpdateActivityPubDeliverPipeline;
|
||||
|
||||
class StatusEditController extends Controller
|
||||
{
|
||||
public function __construct()
|
||||
{
|
||||
$this->middleware('auth');
|
||||
abort_if(!config('exp.pue'), 404, 'Post editing is not enabled on this server.');
|
||||
}
|
||||
|
||||
public function store(StoreStatusEditRequest $request, $id)
|
||||
{
|
||||
$validated = $request->validated();
|
||||
|
||||
$status = Status::findOrFail($id);
|
||||
abort_if(StatusEdit::whereStatusId($status->id)->count() >= 10, 400, 'You cannot edit your post more than 10 times.');
|
||||
$res = UpdateStatusService::call($status, $validated);
|
||||
|
||||
$status = Status::findOrFail($id);
|
||||
StatusLocalUpdateActivityPubDeliverPipeline::dispatch($status)->delay(now()->addMinutes(1));
|
||||
return $res;
|
||||
}
|
||||
|
||||
public function history(Request $request, $id)
|
||||
{
|
||||
abort_if(!$request->user(), 403);
|
||||
$status = Status::whereNull('reblog_of_id')->findOrFail($id);
|
||||
abort_if(!in_array($status->scope, ['public', 'unlisted']), 403);
|
||||
if(!$status->edits()->count()) {
|
||||
return [];
|
||||
}
|
||||
$cached = StatusService::get($status->id, false);
|
||||
|
||||
$res = $status->edits->map(function($edit) use($cached) {
|
||||
$caption = nl2br(strip_tags(str_replace('</p>', "\n", $edit->caption)));
|
||||
return [
|
||||
'content' => Autolink::create()->autolink($caption),
|
||||
'spoiler_text' => $edit->spoiler_text,
|
||||
'sensitive' => (bool) $edit->is_nsfw,
|
||||
'created_at' => str_replace('+00:00', 'Z', $edit->created_at->format(DATE_RFC3339_EXTENDED)),
|
||||
'account' => $cached['account'],
|
||||
'media_attachments' => $cached['media_attachments'],
|
||||
'emojis' => $cached['emojis'],
|
||||
];
|
||||
})->reverse()->values()->toArray();
|
||||
return $res;
|
||||
}
|
||||
}
|
Some files were not shown because too many files have changed in this diff Show More
Ładowanie…
Reference in New Issue