mirror of
https://github.com/pixelfed/pixelfed.git
synced 2025-02-03 10:20:46 +00:00
Merge branch 'pixelfed:dev' into main
This commit is contained in:
commit
cc26bfadfb
293 changed files with 27519 additions and 10607 deletions
|
@ -41,10 +41,10 @@ jobs:
|
||||||
- vendor
|
- vendor
|
||||||
|
|
||||||
- run: cp .env.testing .env
|
- run: cp .env.testing .env
|
||||||
|
- run: php artisan config:cache
|
||||||
- run: php artisan route:clear
|
- run: php artisan route:clear
|
||||||
- run: php artisan storage:link
|
- run: php artisan storage:link
|
||||||
- run: php artisan key:generate
|
- run: php artisan key:generate
|
||||||
- run: php artisan config:clear
|
|
||||||
|
|
||||||
# run tests with phpunit or codecept
|
# run tests with phpunit or codecept
|
||||||
- run: ./vendor/bin/phpunit
|
- run: ./vendor/bin/phpunit
|
||||||
|
|
|
@ -65,3 +65,5 @@ CS_UNLISTED_DOMAINS='example.org,example.net,example.com'
|
||||||
## Optional
|
## Optional
|
||||||
#HORIZON_DARKMODE=false # Horizon theme darkmode
|
#HORIZON_DARKMODE=false # Horizon theme darkmode
|
||||||
#HORIZON_EMBED=false # Single Docker Container mode
|
#HORIZON_EMBED=false # Single Docker Container mode
|
||||||
|
|
||||||
|
ENABLE_CONFIG_CACHE=false
|
||||||
|
|
103
CHANGELOG.md
103
CHANGELOG.md
|
@ -1,8 +1,107 @@
|
||||||
# Release Notes
|
# Release Notes
|
||||||
|
|
||||||
## [Unreleased](https://github.com/pixelfed/pixelfed/compare/v0.11.6...dev)
|
## [Unreleased](https://github.com/pixelfed/pixelfed/compare/v0.11.8...dev)
|
||||||
|
|
||||||
|
### Added
|
||||||
|
- Import from Instagram ([#4466](https://github.com/pixelfed/pixelfed/pull/4466)) ([cf3078c5](https://github.com/pixelfed/pixelfed/commit/cf3078c5))
|
||||||
|
|
||||||
|
### 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))
|
||||||
- ([](https://github.com/pixelfed/pixelfed/commit/))
|
- ([](https://github.com/pixelfed/pixelfed/commit/))
|
||||||
|
|
||||||
|
## [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)
|
## [v0.11.6 (2023-05-03)](https://github.com/pixelfed/pixelfed/compare/v0.11.5...v0.11.6)
|
||||||
|
|
||||||
### Added
|
### Added
|
||||||
|
@ -1031,7 +1130,7 @@
|
||||||
- Added post embeds ([1fecf717](https://github.com/pixelfed/pixelfed/commit/1fecf717))
|
- Added post embeds ([1fecf717](https://github.com/pixelfed/pixelfed/commit/1fecf717))
|
||||||
- Added profile embeds ([fb7a3cf0](https://github.com/pixelfed/pixelfed/commit/fb7a3cf0))
|
- 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 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
|
||||||
- Fixed like and share/reblog count on profiles ([86cb7d09](https://github.com/pixelfed/pixelfed/commit/86cb7d09))
|
- Fixed like and share/reblog count on profiles ([86cb7d09](https://github.com/pixelfed/pixelfed/commit/86cb7d09))
|
||||||
|
|
56
app/Console/Commands/FetchMissingMediaMimeType.php
Normal file
56
app/Console/Commands/FetchMissingMediaMimeType.php
Normal file
|
@ -0,0 +1,56 @@
|
||||||
|
<?php
|
||||||
|
|
||||||
|
namespace App\Console\Commands;
|
||||||
|
|
||||||
|
use Illuminate\Console\Command;
|
||||||
|
use App\Media;
|
||||||
|
use Illuminate\Support\Facades\Http;
|
||||||
|
use App\Services\MediaService;
|
||||||
|
use App\Services\StatusService;
|
||||||
|
|
||||||
|
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('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)');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
|
@ -4,6 +4,7 @@ namespace App\Console\Commands;
|
||||||
|
|
||||||
use Illuminate\Console\Command;
|
use Illuminate\Console\Command;
|
||||||
use App\Profile;
|
use App\Profile;
|
||||||
|
use App\Services\AccountService;
|
||||||
|
|
||||||
class FixStatusCount extends Command
|
class FixStatusCount extends Command
|
||||||
{
|
{
|
||||||
|
@ -12,7 +13,7 @@ class FixStatusCount extends Command
|
||||||
*
|
*
|
||||||
* @var string
|
* @var string
|
||||||
*/
|
*/
|
||||||
protected $signature = 'fix:statuscount';
|
protected $signature = 'fix:statuscount {--remote} {--resync} {--remote-only} {--dlog}';
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* The console command description.
|
* The console command description.
|
||||||
|
@ -38,18 +39,100 @@ class FixStatusCount extends Command
|
||||||
*/
|
*/
|
||||||
public function handle()
|
public function handle()
|
||||||
{
|
{
|
||||||
Profile::whereNull('domain')
|
if(!$this->confirm('Are you sure you want to run the fix status command?')) {
|
||||||
->chunk(50, function($profiles) {
|
return;
|
||||||
foreach($profiles as $profile) {
|
}
|
||||||
$profile->status_count = $profile->statuses()
|
$this->line(' ');
|
||||||
->getQuery()
|
$this->info('Running fix status command...');
|
||||||
->whereIn('type', ['photo', 'photo:album', 'video', 'video:album', 'photo:video:album'])
|
$now = now();
|
||||||
->whereNull('in_reply_to_id')
|
|
||||||
->whereNull('reblog_of_id')
|
$nulls = ['domain', 'status', 'last_fetched_at'];
|
||||||
->count();
|
|
||||||
$profile->save();
|
$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;
|
return 0;
|
||||||
}
|
}
|
||||||
|
|
61
app/Console/Commands/ImportRemoveDeletedAccounts.php
Normal file
61
app/Console/Commands/ImportRemoveDeletedAccounts.php
Normal file
|
@ -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);
|
||||||
|
}
|
||||||
|
}
|
42
app/Console/Commands/ImportUploadCleanStorage.php
Normal file
42
app/Console/Commands/ImportUploadCleanStorage.php
Normal file
|
@ -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);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
49
app/Console/Commands/ImportUploadGarbageCollection.php
Normal file
49
app/Console/Commands/ImportUploadGarbageCollection.php
Normal file
|
@ -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);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
|
@ -45,7 +45,7 @@ class MediaS3GarbageCollector extends Command
|
||||||
*/
|
*/
|
||||||
public function handle()
|
public function handle()
|
||||||
{
|
{
|
||||||
$enabled = config('pixelfed.cloud_storage');
|
$enabled = in_array(config_cache('pixelfed.cloud_storage'), ['1', true, 'true']);
|
||||||
if(!$enabled) {
|
if(!$enabled) {
|
||||||
$this->error('Cloud storage not enabled. Exiting...');
|
$this->error('Cloud storage not enabled. Exiting...');
|
||||||
return;
|
return;
|
||||||
|
|
142
app/Console/Commands/TransformImports.php
Normal file
142
app/Console/Commands/TransformImports.php
Normal file
|
@ -0,0 +1,142 @@
|
||||||
|
<?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\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(200)->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(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();
|
||||||
|
|
||||||
|
ImportService::clearAttempts($profile->id);
|
||||||
|
ImportService::getPostCount($profile->id, true);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
|
@ -52,7 +52,7 @@ class UserCreate extends Command
|
||||||
$user->name = $o['name'];
|
$user->name = $o['name'];
|
||||||
$user->email = $o['email'];
|
$user->email = $o['email'];
|
||||||
$user->password = bcrypt($o['password']);
|
$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->email_verified_at = $o['confirm_email'] ? now() : null;
|
||||||
$user->save();
|
$user->save();
|
||||||
|
|
||||||
|
|
|
@ -33,9 +33,16 @@ class Kernel extends ConsoleKernel
|
||||||
$schedule->command('gc:passwordreset')->dailyAt('09:41');
|
$schedule->command('gc:passwordreset')->dailyAt('09:41');
|
||||||
$schedule->command('gc:sessions')->twiceDaily(13, 23);
|
$schedule->command('gc:sessions')->twiceDaily(13, 23);
|
||||||
|
|
||||||
if(config('pixelfed.cloud_storage') && config('media.delete_local_after_cloud')) {
|
if(in_array(config_cache('pixelfed.cloud_storage'), ['1', true, 'true']) && config('media.delete_local_after_cloud')) {
|
||||||
$schedule->command('media:s3gc')->hourlyAt(15);
|
$schedule->command('media:s3gc')->hourlyAt(15);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if(config('import.instagram.enabled')) {
|
||||||
|
$schedule->command('app:transform-imports')->everyFourMinutes();
|
||||||
|
$schedule->command('app:import-upload-garbage-collection')->hourlyAt(51);
|
||||||
|
$schedule->command('app:import-remove-deleted-accounts')->hourlyAt(37);
|
||||||
|
$schedule->command('app:import-upload-clean-storage')->twiceDailyAt(1, 13, 32);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|
|
@ -31,20 +31,4 @@ class DirectMessage extends Model
|
||||||
{
|
{
|
||||||
return Auth::user()->profile->id === $this->from_id;
|
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}");
|
$path = $this->actor->permalink("#accepts/follows/{$this->id}{$append}");
|
||||||
return url($path);
|
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'
|
'hashtag_id'
|
||||||
];
|
];
|
||||||
|
|
||||||
|
const MAX_LIMIT = 250;
|
||||||
|
|
||||||
public function hashtag()
|
public function hashtag()
|
||||||
{
|
{
|
||||||
return $this->belongsTo(Hashtag::class);
|
return $this->belongsTo(Hashtag::class);
|
||||||
|
|
255
app/Http/Controllers/Admin/AdminAutospamController.php
Normal file
255
app/Http/Controllers/Admin/AdminAutospamController.php
Normal file
|
@ -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'];
|
||||||
|
}
|
||||||
|
}
|
|
@ -14,6 +14,7 @@ use App\{
|
||||||
Contact,
|
Contact,
|
||||||
Hashtag,
|
Hashtag,
|
||||||
Newsroom,
|
Newsroom,
|
||||||
|
Notification,
|
||||||
OauthClient,
|
OauthClient,
|
||||||
Profile,
|
Profile,
|
||||||
Report,
|
Report,
|
||||||
|
@ -30,6 +31,7 @@ use App\Jobs\DeletePipeline\DeleteRemoteStatusPipeline;
|
||||||
use App\Jobs\StatusPipeline\StatusDelete;
|
use App\Jobs\StatusPipeline\StatusDelete;
|
||||||
use App\Http\Resources\AdminReport;
|
use App\Http\Resources\AdminReport;
|
||||||
use App\Http\Resources\AdminSpamReport;
|
use App\Http\Resources\AdminSpamReport;
|
||||||
|
use App\Services\NotificationService;
|
||||||
use App\Services\PublicTimelineService;
|
use App\Services\PublicTimelineService;
|
||||||
use App\Services\NetworkTimelineService;
|
use App\Services\NetworkTimelineService;
|
||||||
|
|
||||||
|
@ -1101,7 +1103,6 @@ trait AdminReportController
|
||||||
Cache::forget('admin-dash:reports:spam-count');
|
Cache::forget('admin-dash:reports:spam-count');
|
||||||
Cache::forget('pf:bouncer_v0:exemption_by_pid:' . $report->user->profile_id);
|
Cache::forget('pf:bouncer_v0:exemption_by_pid:' . $report->user->profile_id);
|
||||||
Cache::forget('pf:bouncer_v0:recent_by_pid:' . $report->user->profile_id);
|
Cache::forget('pf:bouncer_v0:recent_by_pid:' . $report->user->profile_id);
|
||||||
PublicTimelineService::warmCache(true, 400);
|
|
||||||
return [$action, $report];
|
return [$action, $report];
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -1113,6 +1114,7 @@ trait AdminReportController
|
||||||
$appeal->is_spam = true;
|
$appeal->is_spam = true;
|
||||||
$appeal->appeal_handled_at = now();
|
$appeal->appeal_handled_at = now();
|
||||||
$appeal->save();
|
$appeal->save();
|
||||||
|
PublicTimelineService::del($appeal->item_id);
|
||||||
}
|
}
|
||||||
|
|
||||||
if($action == 'mark-not-spam') {
|
if($action == 'mark-not-spam') {
|
||||||
|
@ -1126,7 +1128,19 @@ trait AdminReportController
|
||||||
$appeal->appeal_handled_at = now();
|
$appeal->appeal_handled_at = now();
|
||||||
$appeal->save();
|
$appeal->save();
|
||||||
|
|
||||||
|
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();
|
||||||
|
});
|
||||||
|
|
||||||
StatusService::del($status->id);
|
StatusService::del($status->id);
|
||||||
|
StatusService::get($status->id);
|
||||||
|
if($status->in_reply_to_id == null && $status->reblog_of_id == null) {
|
||||||
|
PublicTimelineService::add($status->id);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if($action == 'mark-all-read') {
|
if($action == 'mark-all-read') {
|
||||||
|
@ -1157,6 +1171,13 @@ trait AdminReportController
|
||||||
$status->save();
|
$status->save();
|
||||||
StatusService::del($status->id);
|
StatusService::del($status->id);
|
||||||
}
|
}
|
||||||
|
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();
|
||||||
|
});
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -10,8 +10,10 @@ use App\Models\InstanceActor;
|
||||||
use App\Http\Controllers\Controller;
|
use App\Http\Controllers\Controller;
|
||||||
use App\Util\Lexer\PrettyNumber;
|
use App\Util\Lexer\PrettyNumber;
|
||||||
use App\Models\ConfigCache;
|
use App\Models\ConfigCache;
|
||||||
|
use App\Services\AccountService;
|
||||||
use App\Services\ConfigCacheService;
|
use App\Services\ConfigCacheService;
|
||||||
use App\Util\Site\Config;
|
use App\Util\Site\Config;
|
||||||
|
use Illuminate\Support\Str;
|
||||||
|
|
||||||
trait AdminSettingsController
|
trait AdminSettingsController
|
||||||
{
|
{
|
||||||
|
@ -28,6 +30,9 @@ trait AdminSettingsController
|
||||||
$mp4 = in_array('video/mp4', $types);
|
$mp4 = in_array('video/mp4', $types);
|
||||||
$webp = in_array('image/webp', $types);
|
$webp = in_array('image/webp', $types);
|
||||||
|
|
||||||
|
$availableAdmins = User::whereIsAdmin(true)->get();
|
||||||
|
$currentAdmin = config_cache('instance.admin.pid') ? AccountService::get(config_cache('instance.admin.pid'), true) : null;
|
||||||
|
|
||||||
// $system = [
|
// $system = [
|
||||||
// 'permissions' => is_writable(base_path('storage')) && is_writable(base_path('bootstrap')),
|
// 'permissions' => is_writable(base_path('storage')) && is_writable(base_path('bootstrap')),
|
||||||
// 'max_upload_size' => ini_get('post_max_size'),
|
// 'max_upload_size' => ini_get('post_max_size'),
|
||||||
|
@ -45,6 +50,8 @@ trait AdminSettingsController
|
||||||
'cloud_storage',
|
'cloud_storage',
|
||||||
'cloud_disk',
|
'cloud_disk',
|
||||||
'cloud_ready',
|
'cloud_ready',
|
||||||
|
'availableAdmins',
|
||||||
|
'currentAdmin'
|
||||||
// 'system'
|
// 'system'
|
||||||
));
|
));
|
||||||
}
|
}
|
||||||
|
@ -63,8 +70,14 @@ trait AdminSettingsController
|
||||||
'type_gif' => 'nullable',
|
'type_gif' => 'nullable',
|
||||||
'type_mp4' => 'nullable',
|
'type_mp4' => 'nullable',
|
||||||
'type_webp' => 'nullable',
|
'type_webp' => 'nullable',
|
||||||
|
'admin_account_id' => 'nullable',
|
||||||
]);
|
]);
|
||||||
|
|
||||||
|
if($request->filled('admin_account_id')) {
|
||||||
|
ConfigCacheService::put('instance.admin.pid', $request->admin_account_id);
|
||||||
|
Cache::forget('api:v1:instance-data:contact');
|
||||||
|
Cache::forget('api:v1:instance-data-response-v1');
|
||||||
|
}
|
||||||
if($request->filled('rule_delete')) {
|
if($request->filled('rule_delete')) {
|
||||||
$index = (int) $request->input('rule_delete');
|
$index = (int) $request->input('rule_delete');
|
||||||
$rules = ConfigCacheService::get('app.rules');
|
$rules = ConfigCacheService::get('app.rules');
|
||||||
|
@ -75,8 +88,8 @@ trait AdminSettingsController
|
||||||
unset($json[$index]);
|
unset($json[$index]);
|
||||||
$json = json_encode(array_values($json));
|
$json = json_encode(array_values($json));
|
||||||
ConfigCacheService::put('app.rules', $json);
|
ConfigCacheService::put('app.rules', $json);
|
||||||
Cache::forget('api:v1:instance-data:rules');
|
Cache::forget('api:v1:instance-data:rules');
|
||||||
Cache::forget('api:v1:instance-data-response-v1');
|
Cache::forget('api:v1:instance-data-response-v1');
|
||||||
return 200;
|
return 200;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -124,8 +137,8 @@ trait AdminSettingsController
|
||||||
if($cc && $cc->v != $val) {
|
if($cc && $cc->v != $val) {
|
||||||
ConfigCacheService::put($value, $val);
|
ConfigCacheService::put($value, $val);
|
||||||
} else if(!empty($val)) {
|
} else if(!empty($val)) {
|
||||||
ConfigCacheService::put($value, $val);
|
ConfigCacheService::put($value, $val);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
$bools = [
|
$bools = [
|
||||||
|
@ -141,8 +154,8 @@ trait AdminSettingsController
|
||||||
'show_custom_js' => 'uikit.show_custom.js',
|
'show_custom_js' => 'uikit.show_custom.js',
|
||||||
'cloud_storage' => 'pixelfed.cloud_storage',
|
'cloud_storage' => 'pixelfed.cloud_storage',
|
||||||
'account_autofollow' => 'account.autofollow',
|
'account_autofollow' => 'account.autofollow',
|
||||||
'show_directory' => 'landing.show_directory',
|
'show_directory' => 'instance.landing.show_directory',
|
||||||
'show_explore_feed' => 'landing.show_explore_feed',
|
'show_explore_feed' => 'instance.landing.show_explore',
|
||||||
];
|
];
|
||||||
|
|
||||||
foreach ($bools as $key => $value) {
|
foreach ($bools as $key => $value) {
|
||||||
|
|
|
@ -11,6 +11,7 @@ use App\Mail\AdminMessage;
|
||||||
use Illuminate\Support\Facades\Mail;
|
use Illuminate\Support\Facades\Mail;
|
||||||
use App\Services\ModLogService;
|
use App\Services\ModLogService;
|
||||||
use App\Jobs\DeletePipeline\DeleteAccountPipeline;
|
use App\Jobs\DeletePipeline\DeleteAccountPipeline;
|
||||||
|
use App\Services\AccountService;
|
||||||
|
|
||||||
trait AdminUserController
|
trait AdminUserController
|
||||||
{
|
{
|
||||||
|
@ -25,7 +26,7 @@ trait AdminUserController
|
||||||
'next' => $offset + 1,
|
'next' => $offset + 1,
|
||||||
'query' => $search ? '&a=search&q=' . $search : null
|
'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)
|
->orderBy($col, $dir)
|
||||||
->when($search, function($q, $search) {
|
->when($search, function($q, $search) {
|
||||||
return $q->where('username', 'like', "%{$search}%");
|
return $q->where('username', 'like', "%{$search}%");
|
||||||
|
@ -34,7 +35,11 @@ trait AdminUserController
|
||||||
return $q->offset(($offset * 10));
|
return $q->offset(($offset * 10));
|
||||||
})
|
})
|
||||||
->limit(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'));
|
return view('admin.users.home', compact('users', 'pagination'));
|
||||||
}
|
}
|
||||||
|
|
|
@ -21,6 +21,7 @@ use Carbon\Carbon;
|
||||||
use Illuminate\Http\Request;
|
use Illuminate\Http\Request;
|
||||||
use Illuminate\Support\Facades\Redis;
|
use Illuminate\Support\Facades\Redis;
|
||||||
use App\Http\Controllers\Admin\{
|
use App\Http\Controllers\Admin\{
|
||||||
|
AdminAutospamController,
|
||||||
AdminDirectoryController,
|
AdminDirectoryController,
|
||||||
AdminDiscoverController,
|
AdminDiscoverController,
|
||||||
AdminHashtagsController,
|
AdminHashtagsController,
|
||||||
|
@ -43,6 +44,7 @@ use App\Models\CustomEmoji;
|
||||||
class AdminController extends Controller
|
class AdminController extends Controller
|
||||||
{
|
{
|
||||||
use AdminReportController,
|
use AdminReportController,
|
||||||
|
AdminAutospamController,
|
||||||
AdminDirectoryController,
|
AdminDirectoryController,
|
||||||
AdminDiscoverController,
|
AdminDiscoverController,
|
||||||
AdminHashtagsController,
|
AdminHashtagsController,
|
||||||
|
|
|
@ -11,22 +11,30 @@ use App\{
|
||||||
AccountInterstitial,
|
AccountInterstitial,
|
||||||
Instance,
|
Instance,
|
||||||
Like,
|
Like,
|
||||||
|
Notification,
|
||||||
Media,
|
Media,
|
||||||
Profile,
|
Profile,
|
||||||
Report,
|
Report,
|
||||||
Status,
|
Status,
|
||||||
User
|
User
|
||||||
};
|
};
|
||||||
|
use App\Models\Conversation;
|
||||||
|
use App\Models\RemoteReport;
|
||||||
use App\Services\AccountService;
|
use App\Services\AccountService;
|
||||||
use App\Services\AdminStatsService;
|
use App\Services\AdminStatsService;
|
||||||
use App\Services\ConfigCacheService;
|
use App\Services\ConfigCacheService;
|
||||||
use App\Services\InstanceService;
|
use App\Services\InstanceService;
|
||||||
use App\Services\ModLogService;
|
use App\Services\ModLogService;
|
||||||
|
use App\Services\SnowflakeService;
|
||||||
use App\Services\StatusService;
|
use App\Services\StatusService;
|
||||||
|
use App\Services\PublicTimelineService;
|
||||||
use App\Services\NetworkTimelineService;
|
use App\Services\NetworkTimelineService;
|
||||||
use App\Services\NotificationService;
|
use App\Services\NotificationService;
|
||||||
use App\Http\Resources\AdminInstance;
|
use App\Http\Resources\AdminInstance;
|
||||||
use App\Http\Resources\AdminUser;
|
use App\Http\Resources\AdminUser;
|
||||||
|
use App\Jobs\DeletePipeline\DeleteAccountPipeline;
|
||||||
|
use App\Jobs\DeletePipeline\DeleteRemoteProfilePipeline;
|
||||||
|
use App\Jobs\DeletePipeline\DeleteRemoteStatusPipeline;
|
||||||
|
|
||||||
class AdminApiController extends Controller
|
class AdminApiController extends Controller
|
||||||
{
|
{
|
||||||
|
@ -91,7 +99,7 @@ class AdminApiController extends Controller
|
||||||
abort_unless($request->user()->is_admin == 1, 404);
|
abort_unless($request->user()->is_admin == 1, 404);
|
||||||
|
|
||||||
$this->validate($request, [
|
$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'
|
'id' => 'required'
|
||||||
]);
|
]);
|
||||||
|
|
||||||
|
@ -103,14 +111,53 @@ class AdminApiController extends Controller
|
||||||
$now = now();
|
$now = now();
|
||||||
$res = ['status' => 'success'];
|
$res = ['status' => 'success'];
|
||||||
$meta = json_decode($appeal->meta);
|
$meta = json_decode($appeal->meta);
|
||||||
|
$user = $appeal->user;
|
||||||
|
$profile = $user->profile;
|
||||||
|
|
||||||
if($action == 'dismiss') {
|
if($action == 'dismiss') {
|
||||||
$appeal->is_spam = true;
|
$appeal->is_spam = true;
|
||||||
$appeal->appeal_handled_at = $now;
|
$appeal->appeal_handled_at = $now;
|
||||||
$appeal->save();
|
$appeal->save();
|
||||||
|
|
||||||
Cache::forget('pf:bouncer_v0:exemption_by_pid:' . $appeal->user->profile_id);
|
Cache::forget('pf:bouncer_v0:exemption_by_pid:' . $profile->id);
|
||||||
Cache::forget('pf:bouncer_v0:recent_by_pid:' . $appeal->user->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');
|
Cache::forget('admin-dash:reports:spam-count');
|
||||||
return $res;
|
return $res;
|
||||||
}
|
}
|
||||||
|
@ -140,6 +187,14 @@ class AdminApiController extends Controller
|
||||||
|
|
||||||
StatusService::del($status->id);
|
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:exemption_by_pid:' . $appeal->user->profile_id);
|
||||||
Cache::forget('pf:bouncer_v0:recent_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');
|
Cache::forget('admin-dash:reports:spam-count');
|
||||||
|
@ -164,6 +219,14 @@ class AdminApiController extends Controller
|
||||||
$status->save();
|
$status->save();
|
||||||
StatusService::del($status->id, true);
|
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:exemption_by_pid:' . $appeal->user->profile_id);
|
||||||
Cache::forget('pf:bouncer_v0:recent_by_pid:' . $appeal->user->profile_id);
|
Cache::forget('pf:bouncer_v0:recent_by_pid:' . $appeal->user->profile_id);
|
||||||
|
@ -387,6 +450,9 @@ class AdminApiController extends Controller
|
||||||
{
|
{
|
||||||
abort_if(!$request->user(), 404);
|
abort_if(!$request->user(), 404);
|
||||||
abort_unless($request->user()->is_admin == 1, 404);
|
abort_unless($request->user()->is_admin == 1, 404);
|
||||||
|
$this->validate($request, [
|
||||||
|
'sort' => 'sometimes|in:asc,desc',
|
||||||
|
]);
|
||||||
$q = $request->input('q');
|
$q = $request->input('q');
|
||||||
$sort = $request->input('sort', 'desc') === 'asc' ? 'asc' : 'desc';
|
$sort = $request->input('sort', 'desc') === 'asc' ? 'asc' : 'desc';
|
||||||
$res = User::whereNull('status')
|
$res = User::whereNull('status')
|
||||||
|
@ -404,17 +470,29 @@ class AdminApiController extends Controller
|
||||||
abort_unless($request->user()->is_admin == 1, 404);
|
abort_unless($request->user()->is_admin == 1, 404);
|
||||||
|
|
||||||
$id = $request->input('user_id');
|
$id = $request->input('user_id');
|
||||||
$user = User::findOrFail($id);
|
$key = 'pf-admin-api:getUser:byId:' . $id;
|
||||||
$profile = $user->profile;
|
if($request->has('refresh')) {
|
||||||
$account = AccountService::get($user->profile_id, true);
|
Cache::forget($key);
|
||||||
return (new AdminUser($user))->additional(['meta' => [
|
}
|
||||||
'account' => $account,
|
return Cache::remember($key, 86400, function() use($id) {
|
||||||
'moderation' => [
|
$user = User::findOrFail($id);
|
||||||
'unlisted' => (bool) $profile->unlisted,
|
$profile = $user->profile;
|
||||||
'cw' => (bool) $profile->cw,
|
$account = AccountService::get($user->profile_id, true);
|
||||||
'no_autolink' => (bool) $profile->no_autolink
|
$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)
|
public function userAdminAction(Request $request)
|
||||||
|
@ -424,7 +502,7 @@ class AdminApiController extends Controller
|
||||||
|
|
||||||
$this->validate($request, [
|
$this->validate($request, [
|
||||||
'id' => 'required',
|
'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'
|
'value' => 'sometimes'
|
||||||
]);
|
]);
|
||||||
|
|
||||||
|
@ -435,7 +513,59 @@ class AdminApiController extends Controller
|
||||||
|
|
||||||
abort_if($user->is_admin == true && $action !== 'refresh_stats', 400, 'Cannot moderate admin accounts');
|
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->following_count = DB::table('followers')->whereProfileId($user->profile_id)->count();
|
||||||
$profile->followers_count = DB::table('followers')->whereFollowingId($user->profile_id)->count();
|
$profile->followers_count = DB::table('followers')->whereFollowingId($user->profile_id)->count();
|
||||||
$statusCount = Status::whereProfileId($user->profile_id)
|
$statusCount = Status::whereProfileId($user->profile_id)
|
||||||
|
@ -461,6 +591,51 @@ class AdminApiController extends Controller
|
||||||
])
|
])
|
||||||
->accessLevel('admin')
|
->accessLevel('admin')
|
||||||
->save();
|
->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 {
|
} else {
|
||||||
$profile->{$action} = filter_var($request->input('value'), FILTER_VALIDATE_BOOLEAN);
|
$profile->{$action} = filter_var($request->input('value'), FILTER_VALIDATE_BOOLEAN);
|
||||||
$profile->save();
|
$profile->save();
|
||||||
|
@ -582,4 +757,62 @@ class AdminApiController extends Controller
|
||||||
|
|
||||||
return new AdminInstance($instance);
|
return new AdminInstance($instance);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public function getAllStats(Request $request)
|
||||||
|
{
|
||||||
|
abort_if(!$request->user(), 404);
|
||||||
|
abort_unless($request->user()->is_admin === 1, 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;
|
||||||
|
});
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -19,6 +19,7 @@ use App\{
|
||||||
Follower,
|
Follower,
|
||||||
FollowRequest,
|
FollowRequest,
|
||||||
Hashtag,
|
Hashtag,
|
||||||
|
HashtagFollow,
|
||||||
Instance,
|
Instance,
|
||||||
Like,
|
Like,
|
||||||
Media,
|
Media,
|
||||||
|
@ -69,6 +70,7 @@ use App\Services\{
|
||||||
BouncerService,
|
BouncerService,
|
||||||
CollectionService,
|
CollectionService,
|
||||||
FollowerService,
|
FollowerService,
|
||||||
|
HashtagService,
|
||||||
InstanceService,
|
InstanceService,
|
||||||
LikeService,
|
LikeService,
|
||||||
NetworkTimelineService,
|
NetworkTimelineService,
|
||||||
|
@ -99,6 +101,7 @@ use App\Jobs\FollowPipeline\FollowRejectPipeline;
|
||||||
use Illuminate\Support\Facades\RateLimiter;
|
use Illuminate\Support\Facades\RateLimiter;
|
||||||
use Purify;
|
use Purify;
|
||||||
use Carbon\Carbon;
|
use Carbon\Carbon;
|
||||||
|
use App\Http\Resources\MastoApi\FollowedTagResource;
|
||||||
|
|
||||||
class ApiV1Controller extends Controller
|
class ApiV1Controller extends Controller
|
||||||
{
|
{
|
||||||
|
@ -116,26 +119,12 @@ class ApiV1Controller extends Controller
|
||||||
return response()->json($res, $code, $headers, JSON_UNESCAPED_SLASHES);
|
return response()->json($res, $code, $headers, JSON_UNESCAPED_SLASHES);
|
||||||
}
|
}
|
||||||
|
|
||||||
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')
|
|
||||||
] : [];
|
|
||||||
}
|
|
||||||
|
|
||||||
public function getApp(Request $request)
|
public function getApp(Request $request)
|
||||||
{
|
{
|
||||||
if(!$request->user()) {
|
if(!$request->user()) {
|
||||||
return response('', 403);
|
return response('', 403);
|
||||||
}
|
}
|
||||||
|
|
||||||
if(config('pixelfed.bouncer.cloud_ips.ban_signups')) {
|
|
||||||
abort_if(BouncerService::checkIp($request->ip()), 404);
|
|
||||||
}
|
|
||||||
|
|
||||||
$client = $request->user()->token()->client;
|
$client = $request->user()->token()->client;
|
||||||
$res = [
|
$res = [
|
||||||
'name' => $client->name,
|
'name' => $client->name,
|
||||||
|
@ -155,10 +144,6 @@ class ApiV1Controller extends Controller
|
||||||
'redirect_uris' => 'required'
|
'redirect_uris' => 'required'
|
||||||
]);
|
]);
|
||||||
|
|
||||||
if(config('pixelfed.bouncer.cloud_ips.ban_signups')) {
|
|
||||||
abort_if(BouncerService::checkIp($request->ip()), 404);
|
|
||||||
}
|
|
||||||
|
|
||||||
$uris = implode(',', explode('\n', $request->redirect_uris));
|
$uris = implode(',', explode('\n', $request->redirect_uris));
|
||||||
|
|
||||||
$client = Passport::client()->forceFill([
|
$client = Passport::client()->forceFill([
|
||||||
|
@ -201,10 +186,6 @@ class ApiV1Controller extends Controller
|
||||||
abort_if(!$user, 403);
|
abort_if(!$user, 403);
|
||||||
abort_if($user->status != null, 403);
|
abort_if($user->status != null, 403);
|
||||||
|
|
||||||
if(config('pixelfed.bouncer.cloud_ips.ban_signups')) {
|
|
||||||
abort_if(BouncerService::checkIp($request->ip()), 404);
|
|
||||||
}
|
|
||||||
|
|
||||||
$res = $request->has(self::PF_API_ENTITY_KEY) ? AccountService::get($user->profile_id) : AccountService::getMastodon($user->profile_id);
|
$res = $request->has(self::PF_API_ENTITY_KEY) ? AccountService::get($user->profile_id) : AccountService::getMastodon($user->profile_id);
|
||||||
|
|
||||||
$res['source'] = [
|
$res['source'] = [
|
||||||
|
@ -227,10 +208,6 @@ class ApiV1Controller extends Controller
|
||||||
*/
|
*/
|
||||||
public function accountById(Request $request, $id)
|
public function accountById(Request $request, $id)
|
||||||
{
|
{
|
||||||
if(config('pixelfed.bouncer.cloud_ips.ban_api')) {
|
|
||||||
abort_if(BouncerService::checkIp($request->ip()), 404);
|
|
||||||
}
|
|
||||||
|
|
||||||
$res = $request->has(self::PF_API_ENTITY_KEY) ? AccountService::get($id, true) : AccountService::getMastodon($id, true);
|
$res = $request->has(self::PF_API_ENTITY_KEY) ? AccountService::get($id, true) : AccountService::getMastodon($id, true);
|
||||||
if(!$res) {
|
if(!$res) {
|
||||||
return response()->json(['error' => 'Record not found'], 404);
|
return response()->json(['error' => 'Record not found'], 404);
|
||||||
|
@ -489,10 +466,6 @@ class ApiV1Controller extends Controller
|
||||||
{
|
{
|
||||||
abort_if(!$request->user(), 403);
|
abort_if(!$request->user(), 403);
|
||||||
|
|
||||||
if(config('pixelfed.bouncer.cloud_ips.ban_api')) {
|
|
||||||
abort_if(BouncerService::checkIp($request->ip()), 404);
|
|
||||||
}
|
|
||||||
|
|
||||||
$account = AccountService::get($id);
|
$account = AccountService::get($id);
|
||||||
abort_if(!$account, 404);
|
abort_if(!$account, 404);
|
||||||
$pid = $request->user()->profile_id;
|
$pid = $request->user()->profile_id;
|
||||||
|
@ -585,10 +558,6 @@ class ApiV1Controller extends Controller
|
||||||
{
|
{
|
||||||
abort_if(!$request->user(), 403);
|
abort_if(!$request->user(), 403);
|
||||||
|
|
||||||
if(config('pixelfed.bouncer.cloud_ips.ban_api')) {
|
|
||||||
abort_if(BouncerService::checkIp($request->ip()), 404);
|
|
||||||
}
|
|
||||||
|
|
||||||
$account = AccountService::get($id);
|
$account = AccountService::get($id);
|
||||||
abort_if(!$account, 404);
|
abort_if(!$account, 404);
|
||||||
$pid = $request->user()->profile_id;
|
$pid = $request->user()->profile_id;
|
||||||
|
@ -679,10 +648,6 @@ class ApiV1Controller extends Controller
|
||||||
*/
|
*/
|
||||||
public function accountStatusesById(Request $request, $id)
|
public function accountStatusesById(Request $request, $id)
|
||||||
{
|
{
|
||||||
if(config('pixelfed.bouncer.cloud_ips.ban_api')) {
|
|
||||||
abort_if(BouncerService::checkIp($request->ip()), 404);
|
|
||||||
}
|
|
||||||
|
|
||||||
$user = $request->user();
|
$user = $request->user();
|
||||||
|
|
||||||
$this->validate($request, [
|
$this->validate($request, [
|
||||||
|
@ -784,10 +749,6 @@ class ApiV1Controller extends Controller
|
||||||
{
|
{
|
||||||
abort_if(!$request->user(), 403);
|
abort_if(!$request->user(), 403);
|
||||||
|
|
||||||
if(config('pixelfed.bouncer.cloud_ips.ban_api')) {
|
|
||||||
abort_if(BouncerService::checkIp($request->ip()), 404);
|
|
||||||
}
|
|
||||||
|
|
||||||
$user = $request->user();
|
$user = $request->user();
|
||||||
|
|
||||||
$target = Profile::where('id', '!=', $user->profile_id)
|
$target = Profile::where('id', '!=', $user->profile_id)
|
||||||
|
@ -872,10 +833,6 @@ class ApiV1Controller extends Controller
|
||||||
{
|
{
|
||||||
abort_if(!$request->user(), 403);
|
abort_if(!$request->user(), 403);
|
||||||
|
|
||||||
if(config('pixelfed.bouncer.cloud_ips.ban_api')) {
|
|
||||||
abort_if(BouncerService::checkIp($request->ip()), 404);
|
|
||||||
}
|
|
||||||
|
|
||||||
$user = $request->user();
|
$user = $request->user();
|
||||||
|
|
||||||
$target = Profile::where('id', '!=', $user->profile_id)
|
$target = Profile::where('id', '!=', $user->profile_id)
|
||||||
|
@ -944,21 +901,20 @@ class ApiV1Controller extends Controller
|
||||||
{
|
{
|
||||||
abort_if(!$request->user(), 403);
|
abort_if(!$request->user(), 403);
|
||||||
|
|
||||||
if(config('pixelfed.bouncer.cloud_ips.ban_api')) {
|
|
||||||
abort_if(BouncerService::checkIp($request->ip()), 404);
|
|
||||||
}
|
|
||||||
|
|
||||||
$this->validate($request, [
|
$this->validate($request, [
|
||||||
'id' => 'required|array|min:1|max:20',
|
'id' => 'required|array|min:1|max:20',
|
||||||
'id.*' => 'required|integer|min:1|max:' . PHP_INT_MAX
|
'id.*' => 'required|integer|min:1|max:' . PHP_INT_MAX
|
||||||
]);
|
]);
|
||||||
|
$napi = $request->has(self::PF_API_ENTITY_KEY);
|
||||||
$pid = $request->user()->profile_id ?? $request->user()->profile->id;
|
$pid = $request->user()->profile_id ?? $request->user()->profile->id;
|
||||||
$res = collect($request->input('id'))
|
$res = collect($request->input('id'))
|
||||||
->filter(function($id) use($pid) {
|
->filter(function($id) use($pid) {
|
||||||
return intval($id) !== intval($pid);
|
return intval($id) !== intval($pid);
|
||||||
})
|
})
|
||||||
->map(function($id) use($pid) {
|
->map(function($id) use($pid, $napi) {
|
||||||
return RelationshipService::get($pid, $id);
|
return $napi ?
|
||||||
|
RelationshipService::getWithDate($pid, $id) :
|
||||||
|
RelationshipService::get($pid, $id);
|
||||||
});
|
});
|
||||||
return $this->json($res);
|
return $this->json($res);
|
||||||
}
|
}
|
||||||
|
@ -980,10 +936,6 @@ class ApiV1Controller extends Controller
|
||||||
'resolve' => 'nullable'
|
'resolve' => 'nullable'
|
||||||
]);
|
]);
|
||||||
|
|
||||||
if(config('pixelfed.bouncer.cloud_ips.ban_api')) {
|
|
||||||
abort_if(BouncerService::checkIp($request->ip()), 404);
|
|
||||||
}
|
|
||||||
|
|
||||||
$user = $request->user();
|
$user = $request->user();
|
||||||
$query = $request->input('q');
|
$query = $request->input('q');
|
||||||
$limit = $request->input('limit') ?? 20;
|
$limit = $request->input('limit') ?? 20;
|
||||||
|
@ -1023,10 +975,6 @@ class ApiV1Controller extends Controller
|
||||||
'page' => 'nullable|integer|min:1|max:10'
|
'page' => 'nullable|integer|min:1|max:10'
|
||||||
]);
|
]);
|
||||||
|
|
||||||
if(config('pixelfed.bouncer.cloud_ips.ban_api')) {
|
|
||||||
abort_if(BouncerService::checkIp($request->ip()), 404);
|
|
||||||
}
|
|
||||||
|
|
||||||
$user = $request->user();
|
$user = $request->user();
|
||||||
$limit = $request->input('limit') ?? 40;
|
$limit = $request->input('limit') ?? 40;
|
||||||
|
|
||||||
|
@ -1059,10 +1007,6 @@ class ApiV1Controller extends Controller
|
||||||
{
|
{
|
||||||
abort_if(!$request->user(), 403);
|
abort_if(!$request->user(), 403);
|
||||||
|
|
||||||
if(config('pixelfed.bouncer.cloud_ips.ban_api')) {
|
|
||||||
abort_if(BouncerService::checkIp($request->ip()), 404);
|
|
||||||
}
|
|
||||||
|
|
||||||
$user = $request->user();
|
$user = $request->user();
|
||||||
$pid = $user->profile_id ?? $user->profile->id;
|
$pid = $user->profile_id ?? $user->profile->id;
|
||||||
|
|
||||||
|
@ -1155,10 +1099,6 @@ class ApiV1Controller extends Controller
|
||||||
{
|
{
|
||||||
abort_if(!$request->user(), 403);
|
abort_if(!$request->user(), 403);
|
||||||
|
|
||||||
if(config('pixelfed.bouncer.cloud_ips.ban_api')) {
|
|
||||||
abort_if(BouncerService::checkIp($request->ip()), 404);
|
|
||||||
}
|
|
||||||
|
|
||||||
$user = $request->user();
|
$user = $request->user();
|
||||||
$pid = $user->profile_id ?? $user->profile->id;
|
$pid = $user->profile_id ?? $user->profile->id;
|
||||||
|
|
||||||
|
@ -1238,10 +1178,6 @@ class ApiV1Controller extends Controller
|
||||||
'limit' => 'sometimes|integer|min:1|max:20'
|
'limit' => 'sometimes|integer|min:1|max:20'
|
||||||
]);
|
]);
|
||||||
|
|
||||||
if(config('pixelfed.bouncer.cloud_ips.ban_api')) {
|
|
||||||
abort_if(BouncerService::checkIp($request->ip()), 404);
|
|
||||||
}
|
|
||||||
|
|
||||||
$user = $request->user();
|
$user = $request->user();
|
||||||
$maxId = $request->input('max_id');
|
$maxId = $request->input('max_id');
|
||||||
$minId = $request->input('min_id');
|
$minId = $request->input('min_id');
|
||||||
|
@ -1295,10 +1231,6 @@ class ApiV1Controller extends Controller
|
||||||
{
|
{
|
||||||
abort_if(!$request->user(), 403);
|
abort_if(!$request->user(), 403);
|
||||||
|
|
||||||
if(config('pixelfed.bouncer.cloud_ips.ban_api')) {
|
|
||||||
abort_if(BouncerService::checkIp($request->ip()), 404);
|
|
||||||
}
|
|
||||||
|
|
||||||
$user = $request->user();
|
$user = $request->user();
|
||||||
|
|
||||||
$status = StatusService::getMastodon($id, false);
|
$status = StatusService::getMastodon($id, false);
|
||||||
|
@ -1358,10 +1290,6 @@ class ApiV1Controller extends Controller
|
||||||
{
|
{
|
||||||
abort_if(!$request->user(), 403);
|
abort_if(!$request->user(), 403);
|
||||||
|
|
||||||
if(config('pixelfed.bouncer.cloud_ips.ban_api')) {
|
|
||||||
abort_if(BouncerService::checkIp($request->ip()), 404);
|
|
||||||
}
|
|
||||||
|
|
||||||
$user = $request->user();
|
$user = $request->user();
|
||||||
|
|
||||||
$status = Status::findOrFail($id);
|
$status = Status::findOrFail($id);
|
||||||
|
@ -1419,10 +1347,6 @@ class ApiV1Controller extends Controller
|
||||||
'limit' => 'sometimes|integer|min:1|max:100'
|
'limit' => 'sometimes|integer|min:1|max:100'
|
||||||
]);
|
]);
|
||||||
|
|
||||||
if(config('pixelfed.bouncer.cloud_ips.ban_api')) {
|
|
||||||
abort_if(BouncerService::checkIp($request->ip()), 404);
|
|
||||||
}
|
|
||||||
|
|
||||||
$user = $request->user();
|
$user = $request->user();
|
||||||
|
|
||||||
$res = FollowRequest::whereFollowingId($user->profile->id)
|
$res = FollowRequest::whereFollowingId($user->profile->id)
|
||||||
|
@ -1554,6 +1478,9 @@ class ApiV1Controller extends Controller
|
||||||
{
|
{
|
||||||
$res = Cache::remember('api:v1:instance-data-response-v1', 1800, function () {
|
$res = Cache::remember('api:v1:instance-data-response-v1', 1800, function () {
|
||||||
$contact = Cache::remember('api:v1:instance-data:contact', 604800, function () {
|
$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();
|
$admin = User::whereIsAdmin(true)->first();
|
||||||
return $admin && isset($admin->profile_id) ?
|
return $admin && isset($admin->profile_id) ?
|
||||||
AccountService::getMastodon($admin->profile_id, true) :
|
AccountService::getMastodon($admin->profile_id, true) :
|
||||||
|
@ -1663,10 +1590,6 @@ class ApiV1Controller extends Controller
|
||||||
{
|
{
|
||||||
abort_if(!$request->user(), 403);
|
abort_if(!$request->user(), 403);
|
||||||
|
|
||||||
if(config('pixelfed.bouncer.cloud_ips.ban_api')) {
|
|
||||||
abort_if(BouncerService::checkIp($request->ip()), 404);
|
|
||||||
}
|
|
||||||
|
|
||||||
$this->validate($request, [
|
$this->validate($request, [
|
||||||
'file.*' => [
|
'file.*' => [
|
||||||
'required_without:file',
|
'required_without:file',
|
||||||
|
@ -1800,10 +1723,6 @@ class ApiV1Controller extends Controller
|
||||||
{
|
{
|
||||||
abort_if(!$request->user(), 403);
|
abort_if(!$request->user(), 403);
|
||||||
|
|
||||||
if(config('pixelfed.bouncer.cloud_ips.ban_api')) {
|
|
||||||
abort_if(BouncerService::checkIp($request->ip()), 404);
|
|
||||||
}
|
|
||||||
|
|
||||||
$this->validate($request, [
|
$this->validate($request, [
|
||||||
'description' => 'nullable|string|max:' . config_cache('pixelfed.max_altext_length')
|
'description' => 'nullable|string|max:' . config_cache('pixelfed.max_altext_length')
|
||||||
]);
|
]);
|
||||||
|
@ -1854,10 +1773,6 @@ class ApiV1Controller extends Controller
|
||||||
{
|
{
|
||||||
abort_if(!$request->user(), 403);
|
abort_if(!$request->user(), 403);
|
||||||
|
|
||||||
if(config('pixelfed.bouncer.cloud_ips.ban_api')) {
|
|
||||||
abort_if(BouncerService::checkIp($request->ip()), 404);
|
|
||||||
}
|
|
||||||
|
|
||||||
$user = $request->user();
|
$user = $request->user();
|
||||||
|
|
||||||
$media = Media::whereUserId($user->id)
|
$media = Media::whereUserId($user->id)
|
||||||
|
@ -1879,10 +1794,6 @@ class ApiV1Controller extends Controller
|
||||||
{
|
{
|
||||||
abort_if(!$request->user(), 403);
|
abort_if(!$request->user(), 403);
|
||||||
|
|
||||||
if(config('pixelfed.bouncer.cloud_ips.ban_api')) {
|
|
||||||
abort_if(BouncerService::checkIp($request->ip()), 404);
|
|
||||||
}
|
|
||||||
|
|
||||||
$this->validate($request, [
|
$this->validate($request, [
|
||||||
'file.*' => [
|
'file.*' => [
|
||||||
'required_without:file',
|
'required_without:file',
|
||||||
|
@ -2056,10 +1967,6 @@ class ApiV1Controller extends Controller
|
||||||
{
|
{
|
||||||
abort_if(!$request->user(), 403);
|
abort_if(!$request->user(), 403);
|
||||||
|
|
||||||
if(config('pixelfed.bouncer.cloud_ips.ban_api')) {
|
|
||||||
abort_if(BouncerService::checkIp($request->ip()), 404);
|
|
||||||
}
|
|
||||||
|
|
||||||
$user = $request->user();
|
$user = $request->user();
|
||||||
$pid = $user->profile_id;
|
$pid = $user->profile_id;
|
||||||
|
|
||||||
|
@ -2113,10 +2020,6 @@ class ApiV1Controller extends Controller
|
||||||
{
|
{
|
||||||
abort_if(!$request->user(), 403);
|
abort_if(!$request->user(), 403);
|
||||||
|
|
||||||
if(config('pixelfed.bouncer.cloud_ips.ban_api')) {
|
|
||||||
abort_if(BouncerService::checkIp($request->ip()), 404);
|
|
||||||
}
|
|
||||||
|
|
||||||
$user = $request->user();
|
$user = $request->user();
|
||||||
$pid = $user->profile_id;
|
$pid = $user->profile_id;
|
||||||
|
|
||||||
|
@ -2153,10 +2056,6 @@ class ApiV1Controller extends Controller
|
||||||
{
|
{
|
||||||
abort_if(!$request->user(), 403);
|
abort_if(!$request->user(), 403);
|
||||||
|
|
||||||
if(config('pixelfed.bouncer.cloud_ips.ban_api_strict_mode')) {
|
|
||||||
abort_if(BouncerService::checkIp($request->ip()), 404);
|
|
||||||
}
|
|
||||||
|
|
||||||
$this->validate($request, [
|
$this->validate($request, [
|
||||||
'limit' => 'nullable|integer|min:1|max:100',
|
'limit' => 'nullable|integer|min:1|max:100',
|
||||||
'min_id' => 'nullable|integer|min:1|max:'.PHP_INT_MAX,
|
'min_id' => 'nullable|integer|min:1|max:'.PHP_INT_MAX,
|
||||||
|
@ -2233,19 +2132,23 @@ class ApiV1Controller extends Controller
|
||||||
'page' => 'sometimes|integer|max:40',
|
'page' => 'sometimes|integer|max:40',
|
||||||
'min_id' => 'sometimes|integer|min:0|max:' . PHP_INT_MAX,
|
'min_id' => 'sometimes|integer|min:0|max:' . PHP_INT_MAX,
|
||||||
'max_id' => 'sometimes|integer|min:0|max:' . PHP_INT_MAX,
|
'max_id' => 'sometimes|integer|min:0|max:' . PHP_INT_MAX,
|
||||||
'limit' => 'sometimes|integer|min:1|max:100'
|
'limit' => 'sometimes|integer|min:1|max:100',
|
||||||
|
'include_reblogs' => 'sometimes',
|
||||||
]);
|
]);
|
||||||
|
|
||||||
if(config('pixelfed.bouncer.cloud_ips.ban_api_strict_mode')) {
|
|
||||||
abort_if(BouncerService::checkIp($request->ip()), 404);
|
|
||||||
}
|
|
||||||
|
|
||||||
$napi = $request->has(self::PF_API_ENTITY_KEY);
|
$napi = $request->has(self::PF_API_ENTITY_KEY);
|
||||||
$page = $request->input('page');
|
$page = $request->input('page');
|
||||||
$min = $request->input('min_id');
|
$min = $request->input('min_id');
|
||||||
$max = $request->input('max_id');
|
$max = $request->input('max_id');
|
||||||
$limit = $request->input('limit') ?? 20;
|
$limit = $request->input('limit') ?? 20;
|
||||||
$pid = $request->user()->profile_id;
|
$pid = $request->user()->profile_id;
|
||||||
|
$includeReblogs = $request->filled('include_reblogs');
|
||||||
|
$nullFields = $includeReblogs ?
|
||||||
|
['in_reply_to_id'] :
|
||||||
|
['in_reply_to_id', 'reblog_of_id'];
|
||||||
|
$inTypes = $includeReblogs ?
|
||||||
|
['photo', 'photo:album', 'video', 'video:album', 'photo:video:album', 'share'] :
|
||||||
|
['photo', 'photo:album', 'video', 'video:album', 'photo:video:album'];
|
||||||
|
|
||||||
$following = Cache::remember('profile:following:'.$pid, 1209600, function() use($pid) {
|
$following = Cache::remember('profile:following:'.$pid, 1209600, function() use($pid) {
|
||||||
$following = Follower::whereProfileId($pid)->pluck('following_id');
|
$following = Follower::whereProfileId($pid)->pluck('following_id');
|
||||||
|
@ -2264,9 +2167,9 @@ class ApiV1Controller extends Controller
|
||||||
'reblog_of_id'
|
'reblog_of_id'
|
||||||
)
|
)
|
||||||
->where('id', $dir, $id)
|
->where('id', $dir, $id)
|
||||||
->whereNull(['in_reply_to_id', 'reblog_of_id'])
|
->whereNull($nullFields)
|
||||||
->whereIntegerInRaw('profile_id', $following)
|
->whereIntegerInRaw('profile_id', $following)
|
||||||
->whereIn('type', ['photo', 'photo:album', 'video', 'video:album', 'photo:video:album'])
|
->whereIn('type', $inTypes)
|
||||||
->whereIn('visibility',['public', 'unlisted', 'private'])
|
->whereIn('visibility',['public', 'unlisted', 'private'])
|
||||||
->orderByDesc('id')
|
->orderByDesc('id')
|
||||||
->take(($limit * 2))
|
->take(($limit * 2))
|
||||||
|
@ -2307,9 +2210,9 @@ class ApiV1Controller extends Controller
|
||||||
'in_reply_to_id',
|
'in_reply_to_id',
|
||||||
'reblog_of_id',
|
'reblog_of_id',
|
||||||
)
|
)
|
||||||
->whereNull(['in_reply_to_id', 'reblog_of_id'])
|
->whereNull($nullFields)
|
||||||
->whereIntegerInRaw('profile_id', $following)
|
->whereIntegerInRaw('profile_id', $following)
|
||||||
->whereIn('type', ['photo', 'photo:album', 'video', 'video:album', 'photo:video:album'])
|
->whereIn('type', $inTypes)
|
||||||
->whereIn('visibility',['public', 'unlisted', 'private'])
|
->whereIn('visibility',['public', 'unlisted', 'private'])
|
||||||
->orderByDesc('id')
|
->orderByDesc('id')
|
||||||
->take(($limit * 2))
|
->take(($limit * 2))
|
||||||
|
@ -2387,10 +2290,6 @@ class ApiV1Controller extends Controller
|
||||||
'local' => 'sometimes'
|
'local' => 'sometimes'
|
||||||
]);
|
]);
|
||||||
|
|
||||||
if(config('pixelfed.bouncer.cloud_ips.ban_api_strict_mode')) {
|
|
||||||
abort_if(BouncerService::checkIp($request->ip()), 404);
|
|
||||||
}
|
|
||||||
|
|
||||||
$napi = $request->has(self::PF_API_ENTITY_KEY);
|
$napi = $request->has(self::PF_API_ENTITY_KEY);
|
||||||
$min = $request->input('min_id');
|
$min = $request->input('min_id');
|
||||||
$max = $request->input('max_id');
|
$max = $request->input('max_id');
|
||||||
|
@ -2518,10 +2417,6 @@ class ApiV1Controller extends Controller
|
||||||
'scope' => 'nullable|in:inbox,sent,requests'
|
'scope' => 'nullable|in:inbox,sent,requests'
|
||||||
]);
|
]);
|
||||||
|
|
||||||
if(config('pixelfed.bouncer.cloud_ips.ban_api_strict_mode')) {
|
|
||||||
abort_if(BouncerService::checkIp($request->ip()), 404);
|
|
||||||
}
|
|
||||||
|
|
||||||
$limit = $request->input('limit', 20);
|
$limit = $request->input('limit', 20);
|
||||||
$scope = $request->input('scope', 'inbox');
|
$scope = $request->input('scope', 'inbox');
|
||||||
$pid = $request->user()->profile_id;
|
$pid = $request->user()->profile_id;
|
||||||
|
@ -2560,14 +2455,17 @@ class ApiV1Controller extends Controller
|
||||||
'id' => $dm->id,
|
'id' => $dm->id,
|
||||||
'unread' => false,
|
'unread' => false,
|
||||||
'accounts' => [
|
'accounts' => [
|
||||||
AccountService::getMastodon($from)
|
AccountService::getMastodon($from, true)
|
||||||
],
|
],
|
||||||
'last_status' => StatusService::getDirectMessage($dm->status_id)
|
'last_status' => StatusService::getDirectMessage($dm->status_id)
|
||||||
];
|
];
|
||||||
return $res;
|
return $res;
|
||||||
})
|
})
|
||||||
->filter(function($dm) {
|
->filter(function($dm) {
|
||||||
return isset($dm['accounts']) && count($dm['accounts']) && !empty($dm['last_status']);
|
if(!$dm || empty($dm['last_status']) || !isset($dm['accounts']) || !count($dm['accounts']) || !isset($dm['accounts'][0]) || !isset($dm['accounts'][0]['id'])) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
return true;
|
||||||
})
|
})
|
||||||
->unique(function($item, $key) {
|
->unique(function($item, $key) {
|
||||||
return $item['accounts'][0]['id'];
|
return $item['accounts'][0]['id'];
|
||||||
|
@ -2588,10 +2486,6 @@ class ApiV1Controller extends Controller
|
||||||
{
|
{
|
||||||
abort_if(!$request->user(), 403);
|
abort_if(!$request->user(), 403);
|
||||||
|
|
||||||
if(config('pixelfed.bouncer.cloud_ips.ban_api_strict_mode')) {
|
|
||||||
abort_if(BouncerService::checkIp($request->ip()), 404);
|
|
||||||
}
|
|
||||||
|
|
||||||
$user = $request->user();
|
$user = $request->user();
|
||||||
|
|
||||||
$res = $request->has(self::PF_API_ENTITY_KEY) ? StatusService::get($id, false) : StatusService::getMastodon($id, false);
|
$res = $request->has(self::PF_API_ENTITY_KEY) ? StatusService::get($id, false) : StatusService::getMastodon($id, false);
|
||||||
|
@ -2628,10 +2522,6 @@ class ApiV1Controller extends Controller
|
||||||
{
|
{
|
||||||
abort_if(!$request->user(), 403);
|
abort_if(!$request->user(), 403);
|
||||||
|
|
||||||
if(config('pixelfed.bouncer.cloud_ips.ban_api_strict_mode')) {
|
|
||||||
abort_if(BouncerService::checkIp($request->ip()), 404);
|
|
||||||
}
|
|
||||||
|
|
||||||
$user = $request->user();
|
$user = $request->user();
|
||||||
$pid = $user->profile_id;
|
$pid = $user->profile_id;
|
||||||
$status = StatusService::getMastodon($id, false);
|
$status = StatusService::getMastodon($id, false);
|
||||||
|
@ -2717,10 +2607,6 @@ class ApiV1Controller extends Controller
|
||||||
'limit' => 'sometimes|integer|min:1|max:80'
|
'limit' => 'sometimes|integer|min:1|max:80'
|
||||||
]);
|
]);
|
||||||
|
|
||||||
if(config('pixelfed.bouncer.cloud_ips.ban_api')) {
|
|
||||||
abort_if(BouncerService::checkIp($request->ip()), 404);
|
|
||||||
}
|
|
||||||
|
|
||||||
$limit = $request->input('limit', 10);
|
$limit = $request->input('limit', 10);
|
||||||
$user = $request->user();
|
$user = $request->user();
|
||||||
$pid = $user->profile_id;
|
$pid = $user->profile_id;
|
||||||
|
@ -2813,10 +2699,6 @@ class ApiV1Controller extends Controller
|
||||||
'limit' => 'nullable|integer|min:1|max:80'
|
'limit' => 'nullable|integer|min:1|max:80'
|
||||||
]);
|
]);
|
||||||
|
|
||||||
if(config('pixelfed.bouncer.cloud_ips.ban_api')) {
|
|
||||||
abort_if(BouncerService::checkIp($request->ip()), 404);
|
|
||||||
}
|
|
||||||
|
|
||||||
$limit = $request->input('limit', 10);
|
$limit = $request->input('limit', 10);
|
||||||
$user = $request->user();
|
$user = $request->user();
|
||||||
$pid = $user->profile_id;
|
$pid = $user->profile_id;
|
||||||
|
@ -2906,10 +2788,6 @@ class ApiV1Controller extends Controller
|
||||||
{
|
{
|
||||||
abort_if(!$request->user(), 403);
|
abort_if(!$request->user(), 403);
|
||||||
|
|
||||||
if(config('pixelfed.bouncer.cloud_ips.ban_api')) {
|
|
||||||
abort_if(BouncerService::checkIp($request->ip()), 404);
|
|
||||||
}
|
|
||||||
|
|
||||||
$this->validate($request, [
|
$this->validate($request, [
|
||||||
'status' => 'nullable|string',
|
'status' => 'nullable|string',
|
||||||
'in_reply_to_id' => 'nullable',
|
'in_reply_to_id' => 'nullable',
|
||||||
|
@ -2922,6 +2800,13 @@ class ApiV1Controller extends Controller
|
||||||
'comments_disabled' => 'sometimes|boolean',
|
'comments_disabled' => 'sometimes|boolean',
|
||||||
]);
|
]);
|
||||||
|
|
||||||
|
if($request->hasHeader('idempotency-key')) {
|
||||||
|
$key = 'pf:api:v1:status:idempotency-key:' . $request->user()->id . ':' . hash('sha1', $request->header('idempotency-key'));
|
||||||
|
$exists = Cache::has($key);
|
||||||
|
abort_if($exists, 400, 'Duplicate idempotency key.');
|
||||||
|
Cache::put($key, 1, 3600);
|
||||||
|
}
|
||||||
|
|
||||||
if(config('costar.enabled') == true) {
|
if(config('costar.enabled') == true) {
|
||||||
$blockedKeywords = config('costar.keyword.block');
|
$blockedKeywords = config('costar.keyword.block');
|
||||||
if($blockedKeywords !== null && $request->status) {
|
if($blockedKeywords !== null && $request->status) {
|
||||||
|
@ -3109,10 +2994,6 @@ class ApiV1Controller extends Controller
|
||||||
{
|
{
|
||||||
abort_if(!$request->user(), 403);
|
abort_if(!$request->user(), 403);
|
||||||
|
|
||||||
if(config('pixelfed.bouncer.cloud_ips.ban_api')) {
|
|
||||||
abort_if(BouncerService::checkIp($request->ip()), 404);
|
|
||||||
}
|
|
||||||
|
|
||||||
$status = Status::whereProfileId($request->user()->profile->id)
|
$status = Status::whereProfileId($request->user()->profile->id)
|
||||||
->findOrFail($id);
|
->findOrFail($id);
|
||||||
|
|
||||||
|
@ -3139,10 +3020,6 @@ class ApiV1Controller extends Controller
|
||||||
{
|
{
|
||||||
abort_if(!$request->user(), 403);
|
abort_if(!$request->user(), 403);
|
||||||
|
|
||||||
if(config('pixelfed.bouncer.cloud_ips.ban_api')) {
|
|
||||||
abort_if(BouncerService::checkIp($request->ip()), 404);
|
|
||||||
}
|
|
||||||
|
|
||||||
$user = $request->user();
|
$user = $request->user();
|
||||||
$status = Status::whereScope('public')->findOrFail($id);
|
$status = Status::whereScope('public')->findOrFail($id);
|
||||||
|
|
||||||
|
@ -3189,10 +3066,6 @@ class ApiV1Controller extends Controller
|
||||||
{
|
{
|
||||||
abort_if(!$request->user(), 403);
|
abort_if(!$request->user(), 403);
|
||||||
|
|
||||||
if(config('pixelfed.bouncer.cloud_ips.ban_api')) {
|
|
||||||
abort_if(BouncerService::checkIp($request->ip()), 404);
|
|
||||||
}
|
|
||||||
|
|
||||||
$user = $request->user();
|
$user = $request->user();
|
||||||
$status = Status::whereScope('public')->findOrFail($id);
|
$status = Status::whereScope('public')->findOrFail($id);
|
||||||
|
|
||||||
|
@ -3234,15 +3107,13 @@ class ApiV1Controller extends Controller
|
||||||
{
|
{
|
||||||
abort_if(!$request->user(), 403);
|
abort_if(!$request->user(), 403);
|
||||||
|
|
||||||
if(config('pixelfed.bouncer.cloud_ips.ban_api')) {
|
|
||||||
abort_if(BouncerService::checkIp($request->ip()), 404);
|
|
||||||
}
|
|
||||||
|
|
||||||
$this->validate($request,[
|
$this->validate($request,[
|
||||||
'page' => 'nullable|integer|max:40',
|
'page' => 'nullable|integer|max:40',
|
||||||
'min_id' => 'nullable|integer|min:0|max:' . PHP_INT_MAX,
|
'min_id' => 'nullable|integer|min:0|max:' . PHP_INT_MAX,
|
||||||
'max_id' => 'nullable|integer|min:0|max:' . PHP_INT_MAX,
|
'max_id' => 'nullable|integer|min:0|max:' . PHP_INT_MAX,
|
||||||
'limit' => 'nullable|integer|max:100'
|
'limit' => 'nullable|integer|max:100',
|
||||||
|
'only_media' => 'sometimes|boolean',
|
||||||
|
'_pe' => 'sometimes'
|
||||||
]);
|
]);
|
||||||
|
|
||||||
if(config('database.default') === 'pgsql') {
|
if(config('database.default') === 'pgsql') {
|
||||||
|
@ -3266,6 +3137,20 @@ class ApiV1Controller extends Controller
|
||||||
$min = $request->input('min_id');
|
$min = $request->input('min_id');
|
||||||
$max = $request->input('max_id');
|
$max = $request->input('max_id');
|
||||||
$limit = $request->input('limit', 20);
|
$limit = $request->input('limit', 20);
|
||||||
|
$onlyMedia = $request->input('only_media', true);
|
||||||
|
$pe = $request->has(self::PF_API_ENTITY_KEY);
|
||||||
|
|
||||||
|
if($min || $max) {
|
||||||
|
$minMax = SnowflakeService::byDate(now()->subMonths(6));
|
||||||
|
if($min && intval($min) < $minMax) {
|
||||||
|
return [];
|
||||||
|
}
|
||||||
|
if($max && intval($max) < $minMax) {
|
||||||
|
return [];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
$filters = UserFilterService::filters($request->user()->profile_id);
|
||||||
|
|
||||||
if(!$min && !$max) {
|
if(!$min && !$max) {
|
||||||
$id = 1;
|
$id = 1;
|
||||||
|
@ -3278,17 +3163,24 @@ class ApiV1Controller extends Controller
|
||||||
$res = StatusHashtag::whereHashtagId($tag->id)
|
$res = StatusHashtag::whereHashtagId($tag->id)
|
||||||
->whereStatusVisibility('public')
|
->whereStatusVisibility('public')
|
||||||
->where('status_id', $dir, $id)
|
->where('status_id', $dir, $id)
|
||||||
->latest()
|
->orderBy('status_id', 'desc')
|
||||||
->limit($limit)
|
->limit($limit)
|
||||||
->pluck('status_id')
|
->pluck('status_id')
|
||||||
->map(function ($i) {
|
->map(function ($i) use($pe) {
|
||||||
if($i) {
|
return $pe ? StatusService::get($i) : StatusService::getMastodon($i);
|
||||||
return StatusService::getMastodon($i);
|
|
||||||
}
|
|
||||||
})
|
})
|
||||||
->filter(function($i) {
|
->filter(function($i) use($onlyMedia) {
|
||||||
|
if(!$i) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
if($onlyMedia && !isset($i['media_attachments']) || !count($i['media_attachments'])) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
return $i && isset($i['account']);
|
return $i && isset($i['account']);
|
||||||
})
|
})
|
||||||
|
->filter(function($i) use($filters) {
|
||||||
|
return !in_array($i['account']['id'], $filters);
|
||||||
|
})
|
||||||
->values()
|
->values()
|
||||||
->toArray();
|
->toArray();
|
||||||
|
|
||||||
|
@ -3306,10 +3198,6 @@ class ApiV1Controller extends Controller
|
||||||
{
|
{
|
||||||
abort_if(!$request->user(), 403);
|
abort_if(!$request->user(), 403);
|
||||||
|
|
||||||
if(config('pixelfed.bouncer.cloud_ips.ban_api')) {
|
|
||||||
abort_if(BouncerService::checkIp($request->ip()), 404);
|
|
||||||
}
|
|
||||||
|
|
||||||
$this->validate($request, [
|
$this->validate($request, [
|
||||||
'limit' => 'nullable|integer|min:1|max:40',
|
'limit' => 'nullable|integer|min:1|max:40',
|
||||||
'max_id' => 'nullable|integer|min:0',
|
'max_id' => 'nullable|integer|min:0',
|
||||||
|
@ -3377,10 +3265,6 @@ class ApiV1Controller extends Controller
|
||||||
{
|
{
|
||||||
abort_if(!$request->user(), 403);
|
abort_if(!$request->user(), 403);
|
||||||
|
|
||||||
if(config('pixelfed.bouncer.cloud_ips.ban_api')) {
|
|
||||||
abort_if(BouncerService::checkIp($request->ip()), 404);
|
|
||||||
}
|
|
||||||
|
|
||||||
$status = Status::findOrFail($id);
|
$status = Status::findOrFail($id);
|
||||||
$pid = $request->user()->profile_id;
|
$pid = $request->user()->profile_id;
|
||||||
|
|
||||||
|
@ -3420,10 +3304,6 @@ class ApiV1Controller extends Controller
|
||||||
{
|
{
|
||||||
abort_if(!$request->user(), 403);
|
abort_if(!$request->user(), 403);
|
||||||
|
|
||||||
if(config('pixelfed.bouncer.cloud_ips.ban_api')) {
|
|
||||||
abort_if(BouncerService::checkIp($request->ip()), 404);
|
|
||||||
}
|
|
||||||
|
|
||||||
$status = Status::findOrFail($id);
|
$status = Status::findOrFail($id);
|
||||||
$pid = $request->user()->profile_id;
|
$pid = $request->user()->profile_id;
|
||||||
|
|
||||||
|
@ -3445,37 +3325,6 @@ class ApiV1Controller extends Controller
|
||||||
return $this->json($res);
|
return $this->json($res);
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
|
||||||
* GET /api/v2/search
|
|
||||||
*
|
|
||||||
*
|
|
||||||
* @return array
|
|
||||||
*/
|
|
||||||
public function searchV2(Request $request)
|
|
||||||
{
|
|
||||||
abort_if(!$request->user(), 403);
|
|
||||||
|
|
||||||
if(config('pixelfed.bouncer.cloud_ips.ban_api')) {
|
|
||||||
abort_if(BouncerService::checkIp($request->ip()), 404);
|
|
||||||
}
|
|
||||||
|
|
||||||
$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'
|
|
||||||
]);
|
|
||||||
|
|
||||||
$mastodonMode = !$request->has('_pe');
|
|
||||||
return $this->json(SearchApiV2Service::query($request, $mastodonMode));
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* GET /api/v1/discover/posts
|
* GET /api/v1/discover/posts
|
||||||
*
|
*
|
||||||
|
@ -3486,10 +3335,6 @@ class ApiV1Controller extends Controller
|
||||||
{
|
{
|
||||||
abort_if(!$request->user(), 403);
|
abort_if(!$request->user(), 403);
|
||||||
|
|
||||||
if(config('pixelfed.bouncer.cloud_ips.ban_api')) {
|
|
||||||
abort_if(BouncerService::checkIp($request->ip()), 404);
|
|
||||||
}
|
|
||||||
|
|
||||||
$this->validate($request, [
|
$this->validate($request, [
|
||||||
'limit' => 'integer|min:1|max:40'
|
'limit' => 'integer|min:1|max:40'
|
||||||
]);
|
]);
|
||||||
|
@ -3527,10 +3372,6 @@ class ApiV1Controller extends Controller
|
||||||
'sort' => 'in:all,newest,popular'
|
'sort' => 'in:all,newest,popular'
|
||||||
]);
|
]);
|
||||||
|
|
||||||
if(config('pixelfed.bouncer.cloud_ips.ban_api')) {
|
|
||||||
abort_if(BouncerService::checkIp($request->ip()), 404);
|
|
||||||
}
|
|
||||||
|
|
||||||
$limit = $request->input('limit', 3);
|
$limit = $request->input('limit', 3);
|
||||||
$pid = $request->user()->profile_id;
|
$pid = $request->user()->profile_id;
|
||||||
$status = StatusService::getMastodon($id, false);
|
$status = StatusService::getMastodon($id, false);
|
||||||
|
@ -3622,10 +3463,6 @@ class ApiV1Controller extends Controller
|
||||||
{
|
{
|
||||||
abort_if(!$request->user(), 403);
|
abort_if(!$request->user(), 403);
|
||||||
|
|
||||||
if(config('pixelfed.bouncer.cloud_ips.ban_api')) {
|
|
||||||
abort_if(BouncerService::checkIp($request->ip()), 404);
|
|
||||||
}
|
|
||||||
|
|
||||||
$status = Status::findOrFail($id);
|
$status = Status::findOrFail($id);
|
||||||
$pid = $request->user()->profile_id;
|
$pid = $request->user()->profile_id;
|
||||||
abort_if(!in_array($status->scope, ['public', 'unlisted', 'private']), 404);
|
abort_if(!in_array($status->scope, ['public', 'unlisted', 'private']), 404);
|
||||||
|
@ -3633,7 +3470,7 @@ class ApiV1Controller extends Controller
|
||||||
return $this->json(StatusService::getState($status->id, $pid));
|
return $this->json(StatusService::getState($status->id, $pid));
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* GET /api/v1.1/discover/accounts/popular
|
* GET /api/v1.1/discover/accounts/popular
|
||||||
*
|
*
|
||||||
*
|
*
|
||||||
|
@ -3643,10 +3480,6 @@ class ApiV1Controller extends Controller
|
||||||
{
|
{
|
||||||
abort_if(!$request->user(), 403);
|
abort_if(!$request->user(), 403);
|
||||||
|
|
||||||
if(config('pixelfed.bouncer.cloud_ips.ban_api')) {
|
|
||||||
abort_if(BouncerService::checkIp($request->ip()), 404);
|
|
||||||
}
|
|
||||||
|
|
||||||
$pid = $request->user()->profile_id;
|
$pid = $request->user()->profile_id;
|
||||||
|
|
||||||
$ids = Cache::remember('api:v1.1:discover:accounts:popular', 86400, function() {
|
$ids = Cache::remember('api:v1.1:discover:accounts:popular', 86400, function() {
|
||||||
|
@ -3675,7 +3508,8 @@ class ApiV1Controller extends Controller
|
||||||
->filter(function($post) {
|
->filter(function($post) {
|
||||||
return $post && isset($post['id']);
|
return $post && isset($post['id']);
|
||||||
})
|
})
|
||||||
->take(3);
|
->take(3)
|
||||||
|
->values();
|
||||||
$profile['recent_posts'] = $ids;
|
$profile['recent_posts'] = $ids;
|
||||||
return $profile;
|
return $profile;
|
||||||
})
|
})
|
||||||
|
@ -3695,10 +3529,6 @@ class ApiV1Controller extends Controller
|
||||||
{
|
{
|
||||||
abort_if(!$request->user(), 403);
|
abort_if(!$request->user(), 403);
|
||||||
|
|
||||||
if(config('pixelfed.bouncer.cloud_ips.ban_api')) {
|
|
||||||
abort_if(BouncerService::checkIp($request->ip()), 404);
|
|
||||||
}
|
|
||||||
|
|
||||||
$pid = $request->user()->profile_id;
|
$pid = $request->user()->profile_id;
|
||||||
$account = AccountService::get($pid);
|
$account = AccountService::get($pid);
|
||||||
|
|
||||||
|
@ -3747,10 +3577,6 @@ class ApiV1Controller extends Controller
|
||||||
{
|
{
|
||||||
abort_if(!$request->user(), 403);
|
abort_if(!$request->user(), 403);
|
||||||
|
|
||||||
if(config('pixelfed.bouncer.cloud_ips.ban_api')) {
|
|
||||||
abort_if(BouncerService::checkIp($request->ip()), 404);
|
|
||||||
}
|
|
||||||
|
|
||||||
$type = $request->input('timeline');
|
$type = $request->input('timeline');
|
||||||
if(is_array($type)) {
|
if(is_array($type)) {
|
||||||
$type = $type[0];
|
$type = $type[0];
|
||||||
|
@ -3772,10 +3598,6 @@ class ApiV1Controller extends Controller
|
||||||
{
|
{
|
||||||
abort_if(!$request->user(), 403);
|
abort_if(!$request->user(), 403);
|
||||||
|
|
||||||
if(config('pixelfed.bouncer.cloud_ips.ban_api')) {
|
|
||||||
abort_if(BouncerService::checkIp($request->ip()), 404);
|
|
||||||
}
|
|
||||||
|
|
||||||
$pid = $request->user()->profile_id;
|
$pid = $request->user()->profile_id;
|
||||||
$home = $request->input('home.last_read_id');
|
$home = $request->input('home.last_read_id');
|
||||||
$notifications = $request->input('notifications.last_read_id');
|
$notifications = $request->input('notifications.last_read_id');
|
||||||
|
@ -3790,4 +3612,168 @@ class ApiV1Controller extends Controller
|
||||||
|
|
||||||
return $this->json([]);
|
return $this->json([]);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 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);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 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);
|
||||||
|
|
||||||
|
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);
|
||||||
|
$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);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -800,9 +800,13 @@ class ApiV1Dot1Controller extends Controller
|
||||||
StatusService::del($status->id, true);
|
StatusService::del($status->id, true);
|
||||||
if($state !== 'public') {
|
if($state !== 'public') {
|
||||||
if($status->uri) {
|
if($status->uri) {
|
||||||
NetworkTimelineService::add($status->id);
|
if($status->in_reply_to_id == null && $status->reblog_of_id == null) {
|
||||||
|
NetworkTimelineService::add($status->id);
|
||||||
|
}
|
||||||
} else {
|
} else {
|
||||||
PublicTimelineService::add($status->id);
|
if($status->in_reply_to_id == null && $status->reblog_of_id == null) {
|
||||||
|
PublicTimelineService::add($status->id);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
} else if ($action == 'mark-unlisted') {
|
} else if ($action == 'mark-unlisted') {
|
||||||
|
|
324
app/Http/Controllers/Api/ApiV2Controller.php
Normal file
324
app/Http/Controllers/Api/ApiV2Controller.php
Normal file
|
@ -0,0 +1,324 @@
|
||||||
|
<?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,
|
||||||
|
};
|
||||||
|
|
||||||
|
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 = [
|
||||||
|
'domain' => config('pixelfed.domain.app'),
|
||||||
|
'title' => config_cache('app.name'),
|
||||||
|
'version' => config('pixelfed.version'),
|
||||||
|
'source_url' => 'https://github.com/pixelfed/pixelfed',
|
||||||
|
'description' => config_cache('app.short_description'),
|
||||||
|
'usage' => [
|
||||||
|
'users' => [
|
||||||
|
'active_month' => (int) Cache::remember('api:nodeinfo:am', 172800, function() {
|
||||||
|
return User::select('last_active_at', 'created_at')
|
||||||
|
->where('last_active_at', '>', now()->subMonths(1))
|
||||||
|
->orWhere('created_at', '>', now()->subMonths(1))
|
||||||
|
->count();
|
||||||
|
})
|
||||||
|
]
|
||||||
|
],
|
||||||
|
'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' => 'wss://' . config('pixelfed.domain.app'),
|
||||||
|
'status' => null
|
||||||
|
],
|
||||||
|
'accounts' => [
|
||||||
|
'max_featured_tags' => 0,
|
||||||
|
],
|
||||||
|
'statuses' => [
|
||||||
|
'max_characters' => (int) config('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' => 4,
|
||||||
|
'max_characters_per_option' => 50,
|
||||||
|
'min_expiration' => 300,
|
||||||
|
'max_expiration' => 2629746,
|
||||||
|
],
|
||||||
|
'translation' => [
|
||||||
|
'enabled' => false,
|
||||||
|
],
|
||||||
|
],
|
||||||
|
'registrations' => [
|
||||||
|
'enabled' => (bool) config_cache('pixelfed.open_registration'),
|
||||||
|
'approval_required' => false,
|
||||||
|
'message' => null
|
||||||
|
],
|
||||||
|
'contact' => [
|
||||||
|
'email' => config('instance.email'),
|
||||||
|
'account' => $contact
|
||||||
|
],
|
||||||
|
'rules' => $rules
|
||||||
|
];
|
||||||
|
|
||||||
|
return response()->json($res, 200, [], JSON_UNESCAPED_SLASHES);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* GET /api/v2/search
|
||||||
|
*
|
||||||
|
*
|
||||||
|
* @return array
|
||||||
|
*/
|
||||||
|
public function search(Request $request)
|
||||||
|
{
|
||||||
|
abort_if(!$request->user(), 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'
|
||||||
|
]);
|
||||||
|
|
||||||
|
$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(), 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 >= 250;
|
||||||
|
});
|
||||||
|
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);
|
||||||
|
}
|
||||||
|
}
|
|
@ -7,6 +7,8 @@ use App\Http\Controllers\Controller;
|
||||||
use App\User;
|
use App\User;
|
||||||
use Illuminate\Foundation\Auth\AuthenticatesUsers;
|
use Illuminate\Foundation\Auth\AuthenticatesUsers;
|
||||||
use App\Services\BouncerService;
|
use App\Services\BouncerService;
|
||||||
|
use Illuminate\Http\Request;
|
||||||
|
use Illuminate\Validation\ValidationException;
|
||||||
|
|
||||||
class LoginController extends Controller
|
class LoginController extends Controller
|
||||||
{
|
{
|
||||||
|
@ -70,8 +72,16 @@ class LoginController extends Controller
|
||||||
'password' => 'required|string|min:6',
|
'password' => 'required|string|min:6',
|
||||||
];
|
];
|
||||||
|
|
||||||
if(config('captcha.enabled')) {
|
if(
|
||||||
$rules['h-captcha-response'] = 'required|captcha';
|
config('captcha.enabled') ||
|
||||||
|
config('captcha.active.login') ||
|
||||||
|
(
|
||||||
|
config('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';
|
||||||
}
|
}
|
||||||
|
|
||||||
$this->validate($request, $rules);
|
$this->validate($request, $rules);
|
||||||
|
@ -102,4 +112,28 @@ class LoginController extends Controller
|
||||||
$log->user_agent = $request->userAgent();
|
$log->user_agent = $request->userAgent();
|
||||||
$log->save();
|
$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')],
|
||||||
|
]);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -137,7 +137,7 @@ class RegisterController extends Controller
|
||||||
'password' => 'required|string|min:'.config('pixelfed.min_password_length').'|confirmed',
|
'password' => 'required|string|min:'.config('pixelfed.min_password_length').'|confirmed',
|
||||||
];
|
];
|
||||||
|
|
||||||
if(config('captcha.enabled')) {
|
if(config('captcha.enabled') || config('captcha.active.register')) {
|
||||||
$rules['h-captcha-response'] = 'required|captcha';
|
$rules['h-captcha-response'] = 'required|captcha';
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -178,8 +178,9 @@ class RegisterController extends Controller
|
||||||
if(config('pixelfed.bouncer.cloud_ips.ban_signups')) {
|
if(config('pixelfed.bouncer.cloud_ips.ban_signups')) {
|
||||||
abort_if(BouncerService::checkIp(request()->ip()), 404);
|
abort_if(BouncerService::checkIp(request()->ip()), 404);
|
||||||
}
|
}
|
||||||
$limit = config('pixelfed.max_users');
|
$hasLimit = config('pixelfed.enforce_max_users');
|
||||||
if($limit) {
|
if($hasLimit) {
|
||||||
|
$limit = config('pixelfed.max_users');
|
||||||
$count = User::where(function($q){ return $q->whereNull('status')->orWhereNotIn('status', ['deleted','delete']); })->count();
|
$count = User::where(function($q){ return $q->whereNull('status')->orWhereNotIn('status', ['deleted','delete']); })->count();
|
||||||
if($limit <= $count) {
|
if($limit <= $count) {
|
||||||
return redirect(route('help.instance-max-users-limit'));
|
return redirect(route('help.instance-max-users-limit'));
|
||||||
|
@ -208,13 +209,17 @@ class RegisterController extends Controller
|
||||||
abort_if(BouncerService::checkIp($request->ip()), 404);
|
abort_if(BouncerService::checkIp($request->ip()), 404);
|
||||||
}
|
}
|
||||||
|
|
||||||
$count = User::where(function($q){ return $q->whereNull('status')->orWhereNotIn('status', ['deleted','delete']); })->count();
|
$hasLimit = config('pixelfed.enforce_max_users');
|
||||||
$limit = config('pixelfed.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) {
|
if($limit && $limit <= $count) {
|
||||||
return redirect(route('help.instance-max-users-limit'));
|
return redirect(route('help.instance-max-users-limit'));
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
$this->validator($request->all())->validate();
|
$this->validator($request->all())->validate();
|
||||||
|
|
||||||
event(new Registered($user = $this->create($request->all())));
|
event(new Registered($user = $this->create($request->all())));
|
||||||
|
|
|
@ -673,7 +673,7 @@ class ComposeController extends Controller
|
||||||
|
|
||||||
$status->caption = strip_tags($request->caption);
|
$status->caption = strip_tags($request->caption);
|
||||||
$status->profile_id = $profile->id;
|
$status->profile_id = $profile->id;
|
||||||
$entities = Extractor::create()->extract($status->caption);
|
$entities = [];
|
||||||
$visibility = $profile->unlisted == true && $visibility == 'public' ? 'unlisted' : $visibility;
|
$visibility = $profile->unlisted == true && $visibility == 'public' ? 'unlisted' : $visibility;
|
||||||
$cw = $profile->cw == true ? true : $cw;
|
$cw = $profile->cw == true ? true : $cw;
|
||||||
$status->is_nsfw = $cw;
|
$status->is_nsfw = $cw;
|
||||||
|
|
|
@ -368,8 +368,6 @@ class DirectMessageController extends Controller
|
||||||
$notification->profile_id = $recipient->id;
|
$notification->profile_id = $recipient->id;
|
||||||
$notification->actor_id = $profile->id;
|
$notification->actor_id = $profile->id;
|
||||||
$notification->action = 'dm';
|
$notification->action = 'dm';
|
||||||
$notification->message = $dm->toText();
|
|
||||||
$notification->rendered = $dm->toHtml();
|
|
||||||
$notification->item_id = $dm->id;
|
$notification->item_id = $dm->id;
|
||||||
$notification->item_type = "App\DirectMessage";
|
$notification->item_type = "App\DirectMessage";
|
||||||
$notification->save();
|
$notification->save();
|
||||||
|
|
298
app/Http/Controllers/ImportPostController.php
Normal file
298
app/Http/Controllers/ImportPostController.php
Normal file
|
@ -0,0 +1,298 @@
|
||||||
|
<?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 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' => $m['title'],
|
||||||
|
'creation_timestamp' => $m['creation_timestamp']
|
||||||
|
];
|
||||||
|
})->toArray();
|
||||||
|
$ip->caption = $c->count() > 1 ? $file['title'] : $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('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()
|
public function outbox()
|
||||||
{
|
{
|
||||||
$res = json_encode([
|
$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',
|
'id' => config('app.url') . '/i/actor/outbox',
|
||||||
'type' => 'OrderedCollection',
|
'type' => 'OrderedCollection',
|
||||||
'totalItems' => 0,
|
'totalItems' => 0,
|
||||||
|
|
|
@ -15,7 +15,7 @@ class LandingController extends Controller
|
||||||
return redirect('/');
|
return redirect('/');
|
||||||
}
|
}
|
||||||
|
|
||||||
abort_if(config('instance.landing.show_directory') == false, 404);
|
abort_if(config_cache('instance.landing.show_directory') == false, 404);
|
||||||
|
|
||||||
return view('site.index');
|
return view('site.index');
|
||||||
}
|
}
|
||||||
|
@ -26,14 +26,14 @@ class LandingController extends Controller
|
||||||
return redirect('/');
|
return redirect('/');
|
||||||
}
|
}
|
||||||
|
|
||||||
abort_if(config('instance.landing.show_explore') == false, 404);
|
abort_if(config_cache('instance.landing.show_explore') == false, 404);
|
||||||
|
|
||||||
return view('site.index');
|
return view('site.index');
|
||||||
}
|
}
|
||||||
|
|
||||||
public function getDirectoryApi(Request $request)
|
public function getDirectoryApi(Request $request)
|
||||||
{
|
{
|
||||||
abort_if(config('instance.landing.show_directory') == false, 404);
|
abort_if(config_cache('instance.landing.show_directory') == false, 404);
|
||||||
|
|
||||||
return DirectoryProfile::collection(
|
return DirectoryProfile::collection(
|
||||||
Profile::whereNull('domain')
|
Profile::whereNull('domain')
|
||||||
|
|
|
@ -3,18 +3,10 @@
|
||||||
namespace App\Http\Controllers;
|
namespace App\Http\Controllers;
|
||||||
|
|
||||||
use Illuminate\Http\Request;
|
use Illuminate\Http\Request;
|
||||||
use Auth, Storage, URL;
|
|
||||||
use App\Media;
|
use App\Media;
|
||||||
use Image as Intervention;
|
|
||||||
use App\Jobs\ImageOptimizePipeline\ImageOptimize;
|
|
||||||
|
|
||||||
class MediaController extends Controller
|
class MediaController extends Controller
|
||||||
{
|
{
|
||||||
public function __construct()
|
|
||||||
{
|
|
||||||
$this->middleware('auth');
|
|
||||||
}
|
|
||||||
|
|
||||||
public function index(Request $request)
|
public function index(Request $request)
|
||||||
{
|
{
|
||||||
//return view('settings.drive.index');
|
//return view('settings.drive.index');
|
||||||
|
@ -24,4 +16,16 @@ class MediaController extends Controller
|
||||||
{
|
{
|
||||||
abort(400, 'Endpoint deprecated');
|
abort(400, 'Endpoint deprecated');
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public function fallbackRedirect(Request $request, $pid, $mhash, $uhash, $f)
|
||||||
|
{
|
||||||
|
abort_if(!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);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -7,11 +7,13 @@ use Auth;
|
||||||
use Cache;
|
use Cache;
|
||||||
use DB;
|
use DB;
|
||||||
use View;
|
use View;
|
||||||
|
use App\AccountInterstitial;
|
||||||
use App\Follower;
|
use App\Follower;
|
||||||
use App\FollowRequest;
|
use App\FollowRequest;
|
||||||
use App\Profile;
|
use App\Profile;
|
||||||
use App\Story;
|
use App\Story;
|
||||||
use App\User;
|
use App\User;
|
||||||
|
use App\UserSetting;
|
||||||
use App\UserFilter;
|
use App\UserFilter;
|
||||||
use League\Fractal;
|
use League\Fractal;
|
||||||
use App\Services\AccountService;
|
use App\Services\AccountService;
|
||||||
|
@ -42,9 +44,22 @@ class ProfileController extends Controller
|
||||||
->whereUsername($username)
|
->whereUsername($username)
|
||||||
->firstOrFail();
|
->firstOrFail();
|
||||||
|
|
||||||
|
|
||||||
if($request->wantsJson() && config_cache('federation.activitypub.enabled')) {
|
if($request->wantsJson() && config_cache('federation.activitypub.enabled')) {
|
||||||
return $this->showActivityPub($request, $user);
|
return $this->showActivityPub($request, $user);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
$aiCheck = Cache::remember('profile:ai-check:spam-login:' . $user->id, 86400, function() use($user) {
|
||||||
|
$exists = AccountInterstitial::whereUserId($user->user_id)->where('is_spam', 1)->count();
|
||||||
|
if($exists) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
return false;
|
||||||
|
});
|
||||||
|
if($aiCheck) {
|
||||||
|
return redirect('/login');
|
||||||
|
}
|
||||||
return $this->buildProfile($request, $user);
|
return $this->buildProfile($request, $user);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -207,7 +222,37 @@ class ProfileController extends Controller
|
||||||
|
|
||||||
abort_if(!$profile || $profile['locked'] || !$profile['local'], 404);
|
abort_if(!$profile || $profile['locked'] || !$profile['local'], 404);
|
||||||
|
|
||||||
$data = Cache::remember('pf:atom:user-feed:by-id:' . $profile['id'], 43200, function() use($pid, $profile) {
|
$aiCheck = Cache::remember('profile:ai-check:spam-login:' . $profile['id'], 86400, 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'], 900, function() use($pid, $profile) {
|
||||||
$items = DB::table('statuses')
|
$items = DB::table('statuses')
|
||||||
->whereProfileId($pid)
|
->whereProfileId($pid)
|
||||||
->whereVisibility('public')
|
->whereVisibility('public')
|
||||||
|
@ -234,7 +279,7 @@ class ProfileController extends Controller
|
||||||
|
|
||||||
return compact('items', 'permalink', 'headers');
|
return compact('items', 'permalink', 'headers');
|
||||||
});
|
});
|
||||||
abort_if(!$data, 404);
|
abort_if(!$data || !isset($data['items']) || !isset($data['permalink']), 404);
|
||||||
return response()
|
return response()
|
||||||
->view('atom.user',
|
->view('atom.user',
|
||||||
[
|
[
|
||||||
|
@ -274,6 +319,19 @@ class ProfileController extends Controller
|
||||||
return response($res)->withHeaders(['X-Frame-Options' => 'ALLOWALL']);
|
return response($res)->withHeaders(['X-Frame-Options' => 'ALLOWALL']);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
$aiCheck = Cache::remember('profile:ai-check:spam-login:' . $profile->id, 86400, 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) {
|
if(AccountService::canEmbed($profile->user_id) == false) {
|
||||||
return response($res)->withHeaders(['X-Frame-Options' => 'ALLOWALL']);
|
return response($res)->withHeaders(['X-Frame-Options' => 'ALLOWALL']);
|
||||||
}
|
}
|
||||||
|
|
|
@ -20,11 +20,13 @@ trait PrivacySettings
|
||||||
|
|
||||||
public function privacy()
|
public function privacy()
|
||||||
{
|
{
|
||||||
$settings = Auth::user()->settings;
|
$user = Auth::user();
|
||||||
$is_private = Auth::user()->profile->is_private;
|
$settings = $user->settings;
|
||||||
$settings['is_private'] = (bool) $is_private;
|
$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)
|
public function privacyStore(Request $request)
|
||||||
|
@ -37,6 +39,7 @@ trait PrivacySettings
|
||||||
'public_dm',
|
'public_dm',
|
||||||
'show_profile_follower_count',
|
'show_profile_follower_count',
|
||||||
'show_profile_following_count',
|
'show_profile_following_count',
|
||||||
|
'show_atom',
|
||||||
];
|
];
|
||||||
|
|
||||||
$profile->is_suggestable = $request->input('is_suggestable') == 'on';
|
$profile->is_suggestable = $request->input('is_suggestable') == 'on';
|
||||||
|
@ -80,6 +83,7 @@ trait PrivacySettings
|
||||||
Cache::forget('user:account:id:' . $profile->user_id);
|
Cache::forget('user:account:id:' . $profile->user_id);
|
||||||
Cache::forget('profile:follower_count:' . $profile->id);
|
Cache::forget('profile:follower_count:' . $profile->id);
|
||||||
Cache::forget('profile:following_count:' . $profile->id);
|
Cache::forget('profile:following_count:' . $profile->id);
|
||||||
|
Cache::forget('profile:atom:enabled:' . $profile->id);
|
||||||
Cache::forget('profile:embed:' . $profile->id);
|
Cache::forget('profile:embed:' . $profile->id);
|
||||||
Cache::forget('pf:acct:settings:hidden-followers:' . $profile->id);
|
Cache::forget('pf:acct:settings:hidden-followers:' . $profile->id);
|
||||||
Cache::forget('pf:acct:settings:hidden-following:' . $profile->id);
|
Cache::forget('pf:acct:settings:hidden-following:' . $profile->id);
|
||||||
|
|
|
@ -81,14 +81,12 @@ class SettingsController extends Controller
|
||||||
|
|
||||||
public function dataImport()
|
public function dataImport()
|
||||||
{
|
{
|
||||||
abort_if(!config_cache('pixelfed.import.instagram.enabled'), 404);
|
|
||||||
return view('settings.import.home');
|
return view('settings.import.home');
|
||||||
}
|
}
|
||||||
|
|
||||||
public function dataImportInstagram()
|
public function dataImportInstagram()
|
||||||
{
|
{
|
||||||
abort_if(!config_cache('pixelfed.import.instagram.enabled'), 404);
|
abort(404);
|
||||||
return view('settings.import.instagram.home');
|
|
||||||
}
|
}
|
||||||
|
|
||||||
public function developers()
|
public function developers()
|
||||||
|
|
|
@ -115,10 +115,25 @@ class StatusController extends Controller
|
||||||
->whereIsPrivate(false)
|
->whereIsPrivate(false)
|
||||||
->whereUsername($username)
|
->whereUsername($username)
|
||||||
->first();
|
->first();
|
||||||
|
|
||||||
if(!$profile) {
|
if(!$profile) {
|
||||||
$content = view('status.embed-removed');
|
$content = view('status.embed-removed');
|
||||||
return response($content)->header('X-Frame-Options', 'ALLOWALL');
|
return response($content)->header('X-Frame-Options', 'ALLOWALL');
|
||||||
}
|
}
|
||||||
|
|
||||||
|
$aiCheck = Cache::remember('profile:ai-check:spam-login:' . $profile->id, 86400, function() use($profile) {
|
||||||
|
$exists = AccountInterstitial::whereUserId($profile->user_id)->where('is_spam', 1)->count();
|
||||||
|
if($exists) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
return false;
|
||||||
|
});
|
||||||
|
|
||||||
|
if($aiCheck) {
|
||||||
|
$res = view('status.embed-removed');
|
||||||
|
return response($res)->withHeaders(['X-Frame-Options' => 'ALLOWALL']);
|
||||||
|
}
|
||||||
$status = Status::whereProfileId($profile->id)
|
$status = Status::whereProfileId($profile->id)
|
||||||
->whereNull('uri')
|
->whereNull('uri')
|
||||||
->whereScope('public')
|
->whereScope('public')
|
||||||
|
|
60
app/Http/Controllers/StatusEditController.php
Normal file
60
app/Http/Controllers/StatusEditController.php
Normal file
|
@ -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;
|
||||||
|
}
|
||||||
|
}
|
|
@ -328,8 +328,6 @@ class StoryApiV1Controller extends Controller
|
||||||
$n->item_id = $dm->id;
|
$n->item_id = $dm->id;
|
||||||
$n->item_type = 'App\DirectMessage';
|
$n->item_type = 'App\DirectMessage';
|
||||||
$n->action = 'story:comment';
|
$n->action = 'story:comment';
|
||||||
$n->message = "{$request->user()->username} commented on story";
|
|
||||||
$n->rendered = "{$request->user()->username} commented on story";
|
|
||||||
$n->save();
|
$n->save();
|
||||||
} else {
|
} else {
|
||||||
StoryReplyDeliver::dispatch($story, $status)->onQueue('story');
|
StoryReplyDeliver::dispatch($story, $status)->onQueue('story');
|
||||||
|
|
|
@ -442,8 +442,6 @@ class StoryComposeController extends Controller
|
||||||
$n->item_id = $dm->id;
|
$n->item_id = $dm->id;
|
||||||
$n->item_type = 'App\DirectMessage';
|
$n->item_type = 'App\DirectMessage';
|
||||||
$n->action = 'story:react';
|
$n->action = 'story:react';
|
||||||
$n->message = "{$request->user()->username} reacted to your story";
|
|
||||||
$n->rendered = "{$request->user()->username} reacted to your story";
|
|
||||||
$n->save();
|
$n->save();
|
||||||
} else {
|
} else {
|
||||||
StoryReactionDeliver::dispatch($story, $status)->onQueue('story');
|
StoryReactionDeliver::dispatch($story, $status)->onQueue('story');
|
||||||
|
@ -516,8 +514,6 @@ class StoryComposeController extends Controller
|
||||||
$n->item_id = $dm->id;
|
$n->item_id = $dm->id;
|
||||||
$n->item_type = 'App\DirectMessage';
|
$n->item_type = 'App\DirectMessage';
|
||||||
$n->action = 'story:comment';
|
$n->action = 'story:comment';
|
||||||
$n->message = "{$request->user()->username} commented on story";
|
|
||||||
$n->rendered = "{$request->user()->username} commented on story";
|
|
||||||
$n->save();
|
$n->save();
|
||||||
} else {
|
} else {
|
||||||
StoryReplyDeliver::dispatch($story, $status)->onQueue('story');
|
StoryReplyDeliver::dispatch($story, $status)->onQueue('story');
|
||||||
|
|
|
@ -37,28 +37,53 @@ class StoryController extends StoryComposeController
|
||||||
$pid = $request->user()->profile_id;
|
$pid = $request->user()->profile_id;
|
||||||
|
|
||||||
if(config('database.default') == 'pgsql') {
|
if(config('database.default') == 'pgsql') {
|
||||||
$s = Story::select('stories.*', 'followers.following_id')
|
$s = Cache::remember('pf:stories:recent-by-id:' . $pid, 900, function() use($pid) {
|
||||||
->leftJoin('followers', 'followers.following_id', 'stories.profile_id')
|
return Story::select('stories.*', 'followers.following_id')
|
||||||
->where('followers.profile_id', $pid)
|
->leftJoin('followers', 'followers.following_id', 'stories.profile_id')
|
||||||
->where('stories.active', true)
|
->where('followers.profile_id', $pid)
|
||||||
|
->where('stories.active', true)
|
||||||
|
->get()
|
||||||
|
->map(function($s) {
|
||||||
|
$r = new \StdClass;
|
||||||
|
$r->id = $s->id;
|
||||||
|
$r->profile_id = $s->profile_id;
|
||||||
|
$r->type = $s->type;
|
||||||
|
$r->path = $s->path;
|
||||||
|
return $r;
|
||||||
|
})
|
||||||
|
->unique('profile_id');
|
||||||
|
});
|
||||||
|
|
||||||
|
} else {
|
||||||
|
$s = Cache::remember('pf:stories:recent-by-id:' . $pid, 900, function() use($pid) {
|
||||||
|
return Story::select('stories.*', 'followers.following_id')
|
||||||
|
->leftJoin('followers', 'followers.following_id', 'stories.profile_id')
|
||||||
|
->where('followers.profile_id', $pid)
|
||||||
|
->where('stories.active', true)
|
||||||
|
->groupBy('followers.following_id')
|
||||||
|
->orderByDesc('id')
|
||||||
|
->get();
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
$self = Cache::remember('pf:stories:recent-self:' . $pid, 21600, function() use($pid) {
|
||||||
|
return Story::whereProfileId($pid)
|
||||||
|
->whereActive(true)
|
||||||
|
->orderByDesc('id')
|
||||||
|
->limit(1)
|
||||||
->get()
|
->get()
|
||||||
->map(function($s) {
|
->map(function($s) use($pid) {
|
||||||
$r = new \StdClass;
|
$r = new \StdClass;
|
||||||
$r->id = $s->id;
|
$r->id = $s->id;
|
||||||
$r->profile_id = $s->profile_id;
|
$r->profile_id = $pid;
|
||||||
$r->type = $s->type;
|
$r->type = $s->type;
|
||||||
$r->path = $s->path;
|
$r->path = $s->path;
|
||||||
return $r;
|
return $r;
|
||||||
})
|
});
|
||||||
->unique('profile_id');
|
});
|
||||||
} else {
|
|
||||||
$s = Story::select('stories.*', 'followers.following_id')
|
if($self->count()) {
|
||||||
->leftJoin('followers', 'followers.following_id', 'stories.profile_id')
|
$s->prepend($self->first());
|
||||||
->where('followers.profile_id', $pid)
|
|
||||||
->where('stories.active', true)
|
|
||||||
->groupBy('followers.following_id')
|
|
||||||
->orderByDesc('id')
|
|
||||||
->get();
|
|
||||||
}
|
}
|
||||||
|
|
||||||
$res = $s->map(function($s) use($pid) {
|
$res = $s->map(function($s) use($pid) {
|
||||||
|
@ -93,7 +118,7 @@ class StoryController extends StoryComposeController
|
||||||
$profile = Profile::findOrFail($id);
|
$profile = Profile::findOrFail($id);
|
||||||
|
|
||||||
if($authed != $profile->id && !FollowerService::follows($authed, $profile->id)) {
|
if($authed != $profile->id && !FollowerService::follows($authed, $profile->id)) {
|
||||||
return [];
|
return abort([], 403);
|
||||||
}
|
}
|
||||||
|
|
||||||
$stories = Story::whereProfileId($profile->id)
|
$stories = Story::whereProfileId($profile->id)
|
||||||
|
@ -164,7 +189,6 @@ class StoryController extends StoryComposeController
|
||||||
$publicOnly = (bool) $profile->followedBy($authed);
|
$publicOnly = (bool) $profile->followedBy($authed);
|
||||||
abort_if(!$publicOnly, 403);
|
abort_if(!$publicOnly, 403);
|
||||||
|
|
||||||
|
|
||||||
$v = StoryView::firstOrCreate([
|
$v = StoryView::firstOrCreate([
|
||||||
'story_id' => $id,
|
'story_id' => $id,
|
||||||
'profile_id' => $authed->id
|
'profile_id' => $authed->id
|
||||||
|
|
48
app/Http/Controllers/UserAppSettingsController.php
Normal file
48
app/Http/Controllers/UserAppSettingsController.php
Normal file
|
@ -0,0 +1,48 @@
|
||||||
|
<?php
|
||||||
|
|
||||||
|
namespace App\Http\Controllers;
|
||||||
|
|
||||||
|
use Illuminate\Http\Request;
|
||||||
|
use App\Services\Account\AccountAppSettingsService;
|
||||||
|
use App\Http\Requests\StoreUserAppSettings;
|
||||||
|
use App\Models\UserAppSettings;
|
||||||
|
use App\Http\Resources\UserAppSettingsResource;
|
||||||
|
|
||||||
|
class UserAppSettingsController extends Controller
|
||||||
|
{
|
||||||
|
public function __construct()
|
||||||
|
{
|
||||||
|
$this->middleware('auth');
|
||||||
|
}
|
||||||
|
|
||||||
|
public function get(Request $request)
|
||||||
|
{
|
||||||
|
abort_if(!$request->user(), 403);
|
||||||
|
|
||||||
|
$settings = UserAppSettings::whereUserId($request->user()->id)->first();
|
||||||
|
|
||||||
|
if(!$settings) {
|
||||||
|
return [
|
||||||
|
'id' => (string) $request->user()->profile_id,
|
||||||
|
'username' => $request->user()->username,
|
||||||
|
'updated_at' => null,
|
||||||
|
'common' => AccountAppSettingsService::default(),
|
||||||
|
];
|
||||||
|
}
|
||||||
|
|
||||||
|
return new UserAppSettingsResource($settings);
|
||||||
|
}
|
||||||
|
|
||||||
|
public function store(StoreUserAppSettings $request)
|
||||||
|
{
|
||||||
|
$res = UserAppSettings::updateOrCreate([
|
||||||
|
'user_id' => $request->user()->id,
|
||||||
|
],[
|
||||||
|
'profile_id' => $request->user()->profile_id,
|
||||||
|
'common' => $request->common,
|
||||||
|
]
|
||||||
|
);
|
||||||
|
|
||||||
|
return new UserAppSettingsResource($res);
|
||||||
|
}
|
||||||
|
}
|
28
app/Http/Middleware/DeprecatedEndpoint.php
Normal file
28
app/Http/Middleware/DeprecatedEndpoint.php
Normal file
|
@ -0,0 +1,28 @@
|
||||||
|
<?php
|
||||||
|
|
||||||
|
namespace App\Http\Middleware;
|
||||||
|
|
||||||
|
use Closure;
|
||||||
|
use Illuminate\Http\Request;
|
||||||
|
use Symfony\Component\HttpFoundation\Response;
|
||||||
|
|
||||||
|
class DeprecatedEndpoint
|
||||||
|
{
|
||||||
|
/**
|
||||||
|
* Handle an incoming request.
|
||||||
|
*
|
||||||
|
* @param \Closure(\Illuminate\Http\Request): (\Symfony\Component\HttpFoundation\Response) $next
|
||||||
|
*/
|
||||||
|
public function handle(Request $request, Closure $next): Response
|
||||||
|
{
|
||||||
|
abort_if(now()->gt('Jan 01, 2024'), 404);
|
||||||
|
$response = $next($request);
|
||||||
|
$link = $response->headers->has('link') ? $response->headers->get('link') . ',<https://pixelfed.org/kb/10404>; rel="deprecation"' : '<https://pixelfed.org/kb/10404>; rel="deprecation"';
|
||||||
|
$response->withHeaders([
|
||||||
|
'Deprecation' => 'Sat, 01 Jul 2023 00:00:00 GMT',
|
||||||
|
'Sunset' => 'Mon, 01 Jan 2024 00:00:00 GMT',
|
||||||
|
'Link' => $link
|
||||||
|
]);
|
||||||
|
return $response;
|
||||||
|
}
|
||||||
|
}
|
69
app/Http/Requests/Status/StoreStatusEditRequest.php
Normal file
69
app/Http/Requests/Status/StoreStatusEditRequest.php
Normal file
|
@ -0,0 +1,69 @@
|
||||||
|
<?php
|
||||||
|
|
||||||
|
namespace App\Http\Requests\Status;
|
||||||
|
|
||||||
|
use Illuminate\Foundation\Http\FormRequest;
|
||||||
|
use App\Media;
|
||||||
|
use App\Status;
|
||||||
|
use Closure;
|
||||||
|
|
||||||
|
class StoreStatusEditRequest extends FormRequest
|
||||||
|
{
|
||||||
|
/**
|
||||||
|
* Determine if the user is authorized to make this request.
|
||||||
|
*/
|
||||||
|
public function authorize(): bool
|
||||||
|
{
|
||||||
|
$profile = $this->user()->profile;
|
||||||
|
if($profile->status != null) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
if($profile->unlisted == true && $profile->cw == true) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
$types = [
|
||||||
|
"photo",
|
||||||
|
"photo:album",
|
||||||
|
"photo:video:album",
|
||||||
|
"reply",
|
||||||
|
"text",
|
||||||
|
"video",
|
||||||
|
"video:album"
|
||||||
|
];
|
||||||
|
$scopes = ['public', 'unlisted', 'private'];
|
||||||
|
$status = Status::whereNull('reblog_of_id')->whereIn('type', $types)->whereIn('scope', $scopes)->find($this->route('id'));
|
||||||
|
return $status && $this->user()->profile_id === $status->profile_id;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get the validation rules that apply to the request.
|
||||||
|
*
|
||||||
|
* @return array<string, \Illuminate\Contracts\Validation\ValidationRule|array|string>
|
||||||
|
*/
|
||||||
|
public function rules(): array
|
||||||
|
{
|
||||||
|
return [
|
||||||
|
'status' => 'sometimes|max:'.config('pixelfed.max_caption_length', 500),
|
||||||
|
'spoiler_text' => 'nullable|string|max:140',
|
||||||
|
'sensitive' => 'sometimes|boolean',
|
||||||
|
'media_ids' => [
|
||||||
|
'nullable',
|
||||||
|
'required_without:status',
|
||||||
|
'array',
|
||||||
|
'max:' . config('pixelfed.max_album_length'),
|
||||||
|
function (string $attribute, mixed $value, Closure $fail) {
|
||||||
|
Media::whereProfileId($this->user()->profile_id)
|
||||||
|
->where(function($query) {
|
||||||
|
return $query->whereNull('status_id')
|
||||||
|
->orWhere('status_id', '=', $this->route('id'));
|
||||||
|
})
|
||||||
|
->findOrFail($value);
|
||||||
|
},
|
||||||
|
],
|
||||||
|
'location' => 'sometimes|nullable',
|
||||||
|
'location.id' => 'sometimes|integer|min:1|max:128769',
|
||||||
|
'location.country' => 'required_with:location.id',
|
||||||
|
'location.name' => 'required_with:location.id',
|
||||||
|
];
|
||||||
|
}
|
||||||
|
}
|
82
app/Http/Requests/StoreUserAppSettings.php
Normal file
82
app/Http/Requests/StoreUserAppSettings.php
Normal file
|
@ -0,0 +1,82 @@
|
||||||
|
<?php
|
||||||
|
|
||||||
|
namespace App\Http\Requests;
|
||||||
|
|
||||||
|
use Illuminate\Foundation\Http\FormRequest;
|
||||||
|
|
||||||
|
class StoreUserAppSettings extends FormRequest
|
||||||
|
{
|
||||||
|
/**
|
||||||
|
* Determine if the user is authorized to make this request.
|
||||||
|
*/
|
||||||
|
public function authorize(): bool
|
||||||
|
{
|
||||||
|
if(!$this->user() || $this->user()->status) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get the validation rules that apply to the request.
|
||||||
|
*
|
||||||
|
* @return array<string, \Illuminate\Contracts\Validation\ValidationRule|array|string>
|
||||||
|
*/
|
||||||
|
public function rules(): array
|
||||||
|
{
|
||||||
|
return [
|
||||||
|
'common' => 'required|array',
|
||||||
|
'common.timelines.show_public' => 'required|boolean',
|
||||||
|
'common.timelines.show_network' => 'required|boolean',
|
||||||
|
'common.timelines.hide_likes_shares' => 'required|boolean',
|
||||||
|
'common.media.hide_public_behind_cw' => 'required|boolean',
|
||||||
|
'common.media.always_show_cw' => 'required|boolean',
|
||||||
|
'common.media.show_alt_text' => 'required|boolean',
|
||||||
|
'common.appearance.links_use_in_app_browser' => 'required|boolean',
|
||||||
|
'common.appearance.theme' => 'required|string|in:light,dark,system',
|
||||||
|
];
|
||||||
|
}
|
||||||
|
/**
|
||||||
|
* Prepare inputs for validation.
|
||||||
|
*
|
||||||
|
* @return void
|
||||||
|
*/
|
||||||
|
protected function prepareForValidation()
|
||||||
|
{
|
||||||
|
$this->merge([
|
||||||
|
'common' => array_merge(
|
||||||
|
$this->input('common'),
|
||||||
|
[
|
||||||
|
'timelines' => [
|
||||||
|
'show_public' => $this->toBoolean($this->input('common.timelines.show_public')),
|
||||||
|
'show_network' => $this->toBoolean($this->input('common.timelines.show_network')),
|
||||||
|
'hide_likes_shares' => $this->toBoolean($this->input('common.timelines.hide_likes_shares'))
|
||||||
|
],
|
||||||
|
|
||||||
|
'media' => [
|
||||||
|
'hide_public_behind_cw' => $this->toBoolean($this->input('common.media.hide_public_behind_cw')),
|
||||||
|
'always_show_cw' => $this->toBoolean($this->input('common.media.always_show_cw')),
|
||||||
|
'show_alt_text' => $this->toBoolean($this->input('common.media.show_alt_text')),
|
||||||
|
],
|
||||||
|
|
||||||
|
'appearance' => [
|
||||||
|
'links_use_in_app_browser' => $this->toBoolean($this->input('common.appearance.links_use_in_app_browser')),
|
||||||
|
'theme' => $this->input('common.appearance.theme'),
|
||||||
|
]
|
||||||
|
]
|
||||||
|
)
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Convert to boolean
|
||||||
|
*
|
||||||
|
* @param $booleable
|
||||||
|
* @return boolean
|
||||||
|
*/
|
||||||
|
private function toBoolean($booleable)
|
||||||
|
{
|
||||||
|
return filter_var($booleable, FILTER_VALIDATE_BOOLEAN, FILTER_NULL_ON_FAILURE);
|
||||||
|
}
|
||||||
|
}
|
|
@ -23,9 +23,11 @@ class AdminUser extends JsonResource
|
||||||
'name' => $this->name,
|
'name' => $this->name,
|
||||||
'username' => $this->username,
|
'username' => $this->username,
|
||||||
'is_admin' => (bool) $this->is_admin,
|
'is_admin' => (bool) $this->is_admin,
|
||||||
|
'email' => $this->email,
|
||||||
'email_verified_at' => $this->email_verified_at,
|
'email_verified_at' => $this->email_verified_at,
|
||||||
'two_factor_enabled' => (bool) $this->{'2fa_enabled'},
|
'two_factor_enabled' => (bool) $this->{'2fa_enabled'},
|
||||||
'register_source' => $this->register_source,
|
'register_source' => $this->register_source,
|
||||||
|
'app_register_ip' => $this->app_register_ip,
|
||||||
'last_active_at' => $this->last_active_at,
|
'last_active_at' => $this->last_active_at,
|
||||||
'created_at' => $this->created_at,
|
'created_at' => $this->created_at,
|
||||||
];
|
];
|
||||||
|
|
20
app/Http/Resources/ImportStatus.php
Normal file
20
app/Http/Resources/ImportStatus.php
Normal file
|
@ -0,0 +1,20 @@
|
||||||
|
<?php
|
||||||
|
|
||||||
|
namespace App\Http\Resources;
|
||||||
|
|
||||||
|
use Illuminate\Http\Request;
|
||||||
|
use Illuminate\Http\Resources\Json\JsonResource;
|
||||||
|
use App\Services\StatusService;
|
||||||
|
|
||||||
|
class ImportStatus extends JsonResource
|
||||||
|
{
|
||||||
|
/**
|
||||||
|
* Transform the resource into an array.
|
||||||
|
*
|
||||||
|
* @return array<string, mixed>
|
||||||
|
*/
|
||||||
|
public function toArray(Request $request): array
|
||||||
|
{
|
||||||
|
return StatusService::get($this->status_id, false);
|
||||||
|
}
|
||||||
|
}
|
33
app/Http/Resources/MastoApi/FollowedTagResource.php
Normal file
33
app/Http/Resources/MastoApi/FollowedTagResource.php
Normal file
|
@ -0,0 +1,33 @@
|
||||||
|
<?php
|
||||||
|
|
||||||
|
namespace App\Http\Resources\MastoApi;
|
||||||
|
|
||||||
|
use Illuminate\Http\Resources\Json\JsonResource;
|
||||||
|
use Illuminate\Http\JsonResponse;
|
||||||
|
use Cache;
|
||||||
|
use App\Services\HashtagService;
|
||||||
|
|
||||||
|
class FollowedTagResource extends JsonResource
|
||||||
|
{
|
||||||
|
/**
|
||||||
|
* Transform the resource into an array.
|
||||||
|
*
|
||||||
|
* @param \Illuminate\Http\Request $request
|
||||||
|
* @return array|\Illuminate\Contracts\Support\Arrayable|\JsonSerializable
|
||||||
|
*/
|
||||||
|
public function toArray($request)
|
||||||
|
{
|
||||||
|
$tag = HashtagService::get($this->hashtag_id);
|
||||||
|
|
||||||
|
if(!$tag || !isset($tag['name'])) {
|
||||||
|
return [];
|
||||||
|
}
|
||||||
|
|
||||||
|
return [
|
||||||
|
'name' => $tag['name'],
|
||||||
|
'url' => config('app.url') . '/i/web/hashtag/' . $tag['slug'],
|
||||||
|
'history' => [],
|
||||||
|
'following' => true,
|
||||||
|
];
|
||||||
|
}
|
||||||
|
}
|
27
app/Http/Resources/UserAppSettingsResource.php
Normal file
27
app/Http/Resources/UserAppSettingsResource.php
Normal file
|
@ -0,0 +1,27 @@
|
||||||
|
<?php
|
||||||
|
|
||||||
|
namespace App\Http\Resources;
|
||||||
|
|
||||||
|
use Illuminate\Http\Request;
|
||||||
|
use Illuminate\Http\Resources\Json\JsonResource;
|
||||||
|
|
||||||
|
class UserAppSettingsResource extends JsonResource
|
||||||
|
{
|
||||||
|
|
||||||
|
public static $wrap = null;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Transform the resource into an array.
|
||||||
|
*
|
||||||
|
* @return array<string, mixed>
|
||||||
|
*/
|
||||||
|
public function toArray(Request $request): array
|
||||||
|
{
|
||||||
|
return [
|
||||||
|
'id' => (string) $this->profile_id,
|
||||||
|
'username' => $request->user()->username,
|
||||||
|
'updated_at' => str_replace('+00:00', 'Z', $this->updated_at->format(DATE_RFC3339_EXTENDED)),
|
||||||
|
'common' => $this->common,
|
||||||
|
];
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,58 @@
|
||||||
|
<?php
|
||||||
|
|
||||||
|
namespace App\Jobs\AutospamPipeline;
|
||||||
|
|
||||||
|
use Illuminate\Bus\Queueable;
|
||||||
|
use Illuminate\Contracts\Queue\ShouldBeUnique;
|
||||||
|
use Illuminate\Contracts\Queue\ShouldQueue;
|
||||||
|
use Illuminate\Foundation\Bus\Dispatchable;
|
||||||
|
use Illuminate\Queue\InteractsWithQueue;
|
||||||
|
use Illuminate\Queue\SerializesModels;
|
||||||
|
use App\Util\Lexer\Classifier;
|
||||||
|
use App\AccountInterstitial;
|
||||||
|
use App\Profile;
|
||||||
|
use App\Status;
|
||||||
|
use Illuminate\Support\Facades\Storage;
|
||||||
|
use App\Services\AutospamService;
|
||||||
|
|
||||||
|
class AutospamPretrainNonSpamPipeline implements ShouldQueue
|
||||||
|
{
|
||||||
|
use Dispatchable, InteractsWithQueue, Queueable, SerializesModels;
|
||||||
|
|
||||||
|
public $classifier;
|
||||||
|
public $accounts;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Create a new job instance.
|
||||||
|
*/
|
||||||
|
public function __construct($accounts)
|
||||||
|
{
|
||||||
|
$this->accounts = $accounts;
|
||||||
|
$this->classifier = new Classifier();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Execute the job.
|
||||||
|
*/
|
||||||
|
public function handle(): void
|
||||||
|
{
|
||||||
|
$classifier = $this->classifier;
|
||||||
|
$accounts = $this->accounts;
|
||||||
|
|
||||||
|
foreach($accounts as $acct) {
|
||||||
|
Status::whereNotNull('caption')
|
||||||
|
->whereScope('public')
|
||||||
|
->whereProfileId($acct->id)
|
||||||
|
->inRandomOrder()
|
||||||
|
->take(400)
|
||||||
|
->pluck('caption')
|
||||||
|
->each(function($c) use ($classifier) {
|
||||||
|
$classifier->learn($c, 'ham');
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
Storage::put(AutospamService::MODEL_HAM_PATH, $classifier->export());
|
||||||
|
|
||||||
|
AutospamUpdateCachedDataPipeline::dispatch()->delay(5);
|
||||||
|
}
|
||||||
|
}
|
63
app/Jobs/AutospamPipeline/AutospamPretrainPipeline.php
Normal file
63
app/Jobs/AutospamPipeline/AutospamPretrainPipeline.php
Normal file
|
@ -0,0 +1,63 @@
|
||||||
|
<?php
|
||||||
|
|
||||||
|
namespace App\Jobs\AutospamPipeline;
|
||||||
|
|
||||||
|
use Illuminate\Bus\Queueable;
|
||||||
|
use Illuminate\Contracts\Queue\ShouldBeUnique;
|
||||||
|
use Illuminate\Contracts\Queue\ShouldQueue;
|
||||||
|
use Illuminate\Foundation\Bus\Dispatchable;
|
||||||
|
use Illuminate\Queue\InteractsWithQueue;
|
||||||
|
use Illuminate\Queue\SerializesModels;
|
||||||
|
use App\Util\Lexer\Classifier;
|
||||||
|
use App\AccountInterstitial;
|
||||||
|
use App\Status;
|
||||||
|
use Illuminate\Support\Facades\Storage;
|
||||||
|
use App\Services\AutospamService;
|
||||||
|
|
||||||
|
class AutospamPretrainPipeline implements ShouldQueue
|
||||||
|
{
|
||||||
|
use Dispatchable, InteractsWithQueue, Queueable, SerializesModels;
|
||||||
|
|
||||||
|
public $classifier;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Create a new job instance.
|
||||||
|
*/
|
||||||
|
public function __construct()
|
||||||
|
{
|
||||||
|
$this->classifier = new Classifier();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Execute the job.
|
||||||
|
*/
|
||||||
|
public function handle(): void
|
||||||
|
{
|
||||||
|
$classifier = $this->classifier;
|
||||||
|
|
||||||
|
$aiCount = AccountInterstitial::whereItemType('App\Status')
|
||||||
|
->whereIsSpam(true)
|
||||||
|
->count();
|
||||||
|
|
||||||
|
if($aiCount < 100) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
AccountInterstitial::whereItemType('App\Status')
|
||||||
|
->whereIsSpam(true)
|
||||||
|
->inRandomOrder()
|
||||||
|
->take(config('autospam.nlp.spam_sample_limit'))
|
||||||
|
->pluck('item_id')
|
||||||
|
->each(function ($ai) use($classifier) {
|
||||||
|
$status = Status::whereNotNull('caption')->find($ai);
|
||||||
|
if(!$status) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
$classifier->learn($status->caption, 'spam');
|
||||||
|
});
|
||||||
|
|
||||||
|
Storage::put(AutospamService::MODEL_SPAM_PATH, $classifier->export());
|
||||||
|
|
||||||
|
AutospamUpdateCachedDataPipeline::dispatch()->delay(5);
|
||||||
|
}
|
||||||
|
}
|
109
app/Jobs/AutospamPipeline/AutospamUpdateCachedDataPipeline.php
Normal file
109
app/Jobs/AutospamPipeline/AutospamUpdateCachedDataPipeline.php
Normal file
|
@ -0,0 +1,109 @@
|
||||||
|
<?php
|
||||||
|
|
||||||
|
namespace App\Jobs\AutospamPipeline;
|
||||||
|
|
||||||
|
use Illuminate\Bus\Queueable;
|
||||||
|
use Illuminate\Contracts\Queue\ShouldBeUnique;
|
||||||
|
use Illuminate\Contracts\Queue\ShouldQueue;
|
||||||
|
use Illuminate\Foundation\Bus\Dispatchable;
|
||||||
|
use Illuminate\Queue\InteractsWithQueue;
|
||||||
|
use Illuminate\Queue\SerializesModels;
|
||||||
|
use App\Models\AutospamCustomTokens;
|
||||||
|
use Illuminate\Support\Facades\Storage;
|
||||||
|
use App\Services\AutospamService;
|
||||||
|
use Cache;
|
||||||
|
|
||||||
|
class AutospamUpdateCachedDataPipeline implements ShouldQueue
|
||||||
|
{
|
||||||
|
use Dispatchable, InteractsWithQueue, Queueable, SerializesModels;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Create a new job instance.
|
||||||
|
*/
|
||||||
|
public function __construct()
|
||||||
|
{
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Execute the job.
|
||||||
|
*/
|
||||||
|
public function handle(): void
|
||||||
|
{
|
||||||
|
$spamExists = Storage::exists(AutospamService::MODEL_SPAM_PATH);
|
||||||
|
if($spamExists) {
|
||||||
|
$spam = json_decode(Storage::get(AutospamService::MODEL_SPAM_PATH), true);
|
||||||
|
} else {
|
||||||
|
$spam = [
|
||||||
|
'documents' => [
|
||||||
|
'spam' => 0
|
||||||
|
],
|
||||||
|
'words' => [
|
||||||
|
'spam' => []
|
||||||
|
]
|
||||||
|
];
|
||||||
|
}
|
||||||
|
$newSpam = AutospamCustomTokens::whereCategory('spam')->get();
|
||||||
|
foreach($newSpam as $ns) {
|
||||||
|
$key = strtolower($ns->token);
|
||||||
|
if(isset($spam['words']['spam'][$key])) {
|
||||||
|
$spam['words']['spam'][$key] = $spam['words']['spam'][$key] + $ns->weight;
|
||||||
|
} else {
|
||||||
|
$spam['words']['spam'][$key] = $ns->weight;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
$newSpamCount = count($spam['words']['spam']);
|
||||||
|
if($newSpamCount) {
|
||||||
|
$spam['documents']['spam'] = $newSpamCount;
|
||||||
|
arsort($spam['words']['spam']);
|
||||||
|
Storage::put(AutospamService::MODEL_SPAM_PATH, json_encode($spam, JSON_UNESCAPED_SLASHES|JSON_PRETTY_PRINT));
|
||||||
|
}
|
||||||
|
|
||||||
|
$hamExists = Storage::exists(AutospamService::MODEL_HAM_PATH);
|
||||||
|
if($hamExists) {
|
||||||
|
$ham = json_decode(Storage::get(AutospamService::MODEL_HAM_PATH), true);
|
||||||
|
} else {
|
||||||
|
$ham = [
|
||||||
|
'documents' => [
|
||||||
|
'ham' => 0
|
||||||
|
],
|
||||||
|
'words' => [
|
||||||
|
'ham' => []
|
||||||
|
]
|
||||||
|
];
|
||||||
|
}
|
||||||
|
$newHam = AutospamCustomTokens::whereCategory('ham')->get();
|
||||||
|
foreach($newHam as $ns) {
|
||||||
|
$key = strtolower($ns->token);
|
||||||
|
if(isset($spam['words']['ham'][$key])) {
|
||||||
|
$ham['words']['ham'][$key] = $ham['words']['ham'][$key] + $ns->weight;
|
||||||
|
} else {
|
||||||
|
$ham['words']['ham'][$key] = $ns->weight;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
$newHamCount = count($ham['words']['ham']);
|
||||||
|
if($newHamCount) {
|
||||||
|
$ham['documents']['ham'] = $newHamCount;
|
||||||
|
arsort($ham['words']['ham']);
|
||||||
|
Storage::put(AutospamService::MODEL_HAM_PATH, json_encode($ham, JSON_UNESCAPED_SLASHES|JSON_PRETTY_PRINT));
|
||||||
|
}
|
||||||
|
|
||||||
|
if($newSpamCount && $newHamCount) {
|
||||||
|
$combined = [
|
||||||
|
'documents' => [
|
||||||
|
'spam' => $newSpamCount,
|
||||||
|
'ham' => $newHamCount,
|
||||||
|
],
|
||||||
|
'words' => [
|
||||||
|
'spam' => $spam['words']['spam'],
|
||||||
|
'ham' => $ham['words']['ham']
|
||||||
|
]
|
||||||
|
];
|
||||||
|
|
||||||
|
Storage::put(AutospamService::MODEL_FILE_PATH, json_encode($combined, JSON_PRETTY_PRINT,JSON_UNESCAPED_SLASHES));
|
||||||
|
}
|
||||||
|
|
||||||
|
Cache::forget(AutospamService::MODEL_CACHE_KEY);
|
||||||
|
Cache::forget(AutospamService::CHCKD_CACHE_KEY);
|
||||||
|
}
|
||||||
|
}
|
|
@ -60,7 +60,7 @@ class RemoteAvatarFetch implements ShouldQueue
|
||||||
{
|
{
|
||||||
$profile = $this->profile;
|
$profile = $this->profile;
|
||||||
|
|
||||||
if(config_cache('pixelfed.cloud_storage') == false && config_cache('federation.avatars.store_local') == false) {
|
if(boolval(config_cache('pixelfed.cloud_storage')) == false && boolval(config_cache('federation.avatars.store_local')) == false) {
|
||||||
return 1;
|
return 1;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -108,7 +108,7 @@ class RemoteAvatarFetch implements ShouldQueue
|
||||||
$avatar->remote_url = $icon['url'];
|
$avatar->remote_url = $icon['url'];
|
||||||
$avatar->save();
|
$avatar->save();
|
||||||
|
|
||||||
MediaStorageService::avatar($avatar, config_cache('pixelfed.cloud_storage') == false);
|
MediaStorageService::avatar($avatar, boolval(config_cache('pixelfed.cloud_storage')) == false);
|
||||||
|
|
||||||
return 1;
|
return 1;
|
||||||
}
|
}
|
||||||
|
|
97
app/Jobs/AvatarPipeline/RemoteAvatarFetchFromUrl.php
Normal file
97
app/Jobs/AvatarPipeline/RemoteAvatarFetchFromUrl.php
Normal file
|
@ -0,0 +1,97 @@
|
||||||
|
<?php
|
||||||
|
|
||||||
|
namespace App\Jobs\AvatarPipeline;
|
||||||
|
|
||||||
|
use App\Avatar;
|
||||||
|
use App\Profile;
|
||||||
|
use Illuminate\Bus\Queueable;
|
||||||
|
use Illuminate\Contracts\Queue\ShouldQueue;
|
||||||
|
use Illuminate\Foundation\Bus\Dispatchable;
|
||||||
|
use Illuminate\Queue\InteractsWithQueue;
|
||||||
|
use Illuminate\Queue\SerializesModels;
|
||||||
|
use App\Util\ActivityPub\Helpers;
|
||||||
|
use Illuminate\Support\Str;
|
||||||
|
use Zttp\Zttp;
|
||||||
|
use App\Http\Controllers\AvatarController;
|
||||||
|
use Cache;
|
||||||
|
use Storage;
|
||||||
|
use Log;
|
||||||
|
use Illuminate\Http\File;
|
||||||
|
use App\Services\AccountService;
|
||||||
|
use App\Services\MediaStorageService;
|
||||||
|
use App\Services\ActivityPubFetchService;
|
||||||
|
|
||||||
|
class RemoteAvatarFetchFromUrl implements ShouldQueue
|
||||||
|
{
|
||||||
|
use Dispatchable, InteractsWithQueue, Queueable, SerializesModels;
|
||||||
|
|
||||||
|
protected $profile;
|
||||||
|
protected $url;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Delete the job if its models no longer exist.
|
||||||
|
*
|
||||||
|
* @var bool
|
||||||
|
*/
|
||||||
|
public $deleteWhenMissingModels = true;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The number of times the job may be attempted.
|
||||||
|
*
|
||||||
|
* @var int
|
||||||
|
*/
|
||||||
|
public $tries = 1;
|
||||||
|
public $timeout = 300;
|
||||||
|
public $maxExceptions = 1;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Create a new job instance.
|
||||||
|
*
|
||||||
|
* @return void
|
||||||
|
*/
|
||||||
|
public function __construct(Profile $profile, $url)
|
||||||
|
{
|
||||||
|
$this->profile = $profile;
|
||||||
|
$this->url = $url;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Execute the job.
|
||||||
|
*
|
||||||
|
* @return void
|
||||||
|
*/
|
||||||
|
public function handle()
|
||||||
|
{
|
||||||
|
$profile = $this->profile;
|
||||||
|
|
||||||
|
Cache::forget('avatar:' . $profile->id);
|
||||||
|
AccountService::del($profile->id);
|
||||||
|
|
||||||
|
if(boolval(config_cache('pixelfed.cloud_storage')) == false && boolval(config_cache('federation.avatars.store_local')) == false) {
|
||||||
|
return 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
if($profile->domain == null || $profile->private_key) {
|
||||||
|
return 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
$avatar = Avatar::whereProfileId($profile->id)->first();
|
||||||
|
|
||||||
|
if(!$avatar) {
|
||||||
|
$avatar = new Avatar;
|
||||||
|
$avatar->profile_id = $profile->id;
|
||||||
|
$avatar->is_remote = true;
|
||||||
|
$avatar->remote_url = $this->url;
|
||||||
|
$avatar->save();
|
||||||
|
} else {
|
||||||
|
$avatar->remote_url = $this->url;
|
||||||
|
$avatar->is_remote = true;
|
||||||
|
$avatar->save();
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
MediaStorageService::avatar($avatar, boolval(config_cache('pixelfed.cloud_storage')) == false, true);
|
||||||
|
|
||||||
|
return 1;
|
||||||
|
}
|
||||||
|
}
|
|
@ -60,10 +60,12 @@ class CommentPipeline implements ShouldQueue
|
||||||
$actor = $comment->profile;
|
$actor = $comment->profile;
|
||||||
|
|
||||||
if(config('database.default') === 'mysql') {
|
if(config('database.default') === 'mysql') {
|
||||||
$exp = DB::raw("select id, in_reply_to_id from statuses, (select @pv := :kid) initialisation where id > @pv and find_in_set(in_reply_to_id, @pv) > 0 and @pv := concat(@pv, ',', id)");
|
// todo: refactor
|
||||||
$expQuery = $exp->getValue(DB::connection()->getQueryGrammar());
|
// $exp = DB::raw("select id, in_reply_to_id from statuses, (select @pv := :kid) initialisation where id > @pv and find_in_set(in_reply_to_id, @pv) > 0 and @pv := concat(@pv, ',', id)");
|
||||||
$count = DB::select($expQuery, [ 'kid' => $status->id ]);
|
// $expQuery = $exp->getValue(DB::connection()->getQueryGrammar());
|
||||||
$status->reply_count = count($count);
|
// $count = DB::select($expQuery, [ 'kid' => $status->id ]);
|
||||||
|
// $status->reply_count = count($count);
|
||||||
|
$status->reply_count = $status->reply_count + 1;
|
||||||
$status->save();
|
$status->save();
|
||||||
} else {
|
} else {
|
||||||
$status->reply_count = $status->reply_count + 1;
|
$status->reply_count = $status->reply_count + 1;
|
||||||
|
@ -94,8 +96,6 @@ class CommentPipeline implements ShouldQueue
|
||||||
$notification->profile_id = $target->id;
|
$notification->profile_id = $target->id;
|
||||||
$notification->actor_id = $actor->id;
|
$notification->actor_id = $actor->id;
|
||||||
$notification->action = 'comment';
|
$notification->action = 'comment';
|
||||||
$notification->message = $comment->replyToText();
|
|
||||||
$notification->rendered = $comment->replyToHtml();
|
|
||||||
$notification->item_id = $comment->id;
|
$notification->item_id = $comment->id;
|
||||||
$notification->item_type = "App\Status";
|
$notification->item_type = "App\Status";
|
||||||
$notification->save();
|
$notification->save();
|
||||||
|
|
|
@ -97,8 +97,6 @@ class FollowPipeline implements ShouldQueue
|
||||||
$notification->profile_id = $target->id;
|
$notification->profile_id = $target->id;
|
||||||
$notification->actor_id = $actor->id;
|
$notification->actor_id = $actor->id;
|
||||||
$notification->action = 'follow';
|
$notification->action = 'follow';
|
||||||
$notification->message = $follower->toText();
|
|
||||||
$notification->rendered = $follower->toHtml();
|
|
||||||
$notification->item_id = $target->id;
|
$notification->item_id = $target->id;
|
||||||
$notification->item_type = "App\Profile";
|
$notification->item_type = "App\Profile";
|
||||||
$notification->save();
|
$notification->save();
|
||||||
|
|
|
@ -84,8 +84,6 @@ class LikePipeline implements ShouldQueue
|
||||||
$notification->profile_id = $status->profile_id;
|
$notification->profile_id = $status->profile_id;
|
||||||
$notification->actor_id = $actor->id;
|
$notification->actor_id = $actor->id;
|
||||||
$notification->action = 'like';
|
$notification->action = 'like';
|
||||||
$notification->message = $like->toText($status->in_reply_to_id ? 'comment' : 'post');
|
|
||||||
$notification->rendered = $like->toHtml($status->in_reply_to_id ? 'comment' : 'post');
|
|
||||||
$notification->item_id = $status->id;
|
$notification->item_id = $status->id;
|
||||||
$notification->item_type = "App\Status";
|
$notification->item_type = "App\Status";
|
||||||
$notification->save();
|
$notification->save();
|
||||||
|
|
|
@ -67,10 +67,6 @@ class MentionPipeline implements ShouldQueue
|
||||||
'action' => 'mention',
|
'action' => 'mention',
|
||||||
'item_type' => 'App\Status',
|
'item_type' => 'App\Status',
|
||||||
'item_id' => $status->id,
|
'item_id' => $status->id,
|
||||||
],
|
|
||||||
[
|
|
||||||
'message' => $mention->toText(),
|
|
||||||
'rendered' => $mention->toHtml()
|
|
||||||
]
|
]
|
||||||
);
|
);
|
||||||
|
|
||||||
|
|
94
app/Jobs/ProfilePipeline/HandleUpdateActivity.php
Normal file
94
app/Jobs/ProfilePipeline/HandleUpdateActivity.php
Normal file
|
@ -0,0 +1,94 @@
|
||||||
|
<?php
|
||||||
|
|
||||||
|
namespace App\Jobs\ProfilePipeline;
|
||||||
|
|
||||||
|
use Illuminate\Bus\Queueable;
|
||||||
|
use Illuminate\Contracts\Queue\ShouldBeUnique;
|
||||||
|
use Illuminate\Contracts\Queue\ShouldQueue;
|
||||||
|
use Illuminate\Foundation\Bus\Dispatchable;
|
||||||
|
use Illuminate\Queue\InteractsWithQueue;
|
||||||
|
use Illuminate\Queue\SerializesModels;
|
||||||
|
use App\Avatar;
|
||||||
|
use App\Profile;
|
||||||
|
use App\Util\ActivityPub\Helpers;
|
||||||
|
use Cache;
|
||||||
|
use Purify;
|
||||||
|
use App\Jobs\AvatarPipeline\RemoteAvatarFetchFromUrl;
|
||||||
|
use App\Util\Lexer\Autolink;
|
||||||
|
|
||||||
|
class HandleUpdateActivity implements ShouldQueue
|
||||||
|
{
|
||||||
|
use Dispatchable, InteractsWithQueue, Queueable, SerializesModels;
|
||||||
|
|
||||||
|
protected $payload;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Create a new job instance.
|
||||||
|
*
|
||||||
|
* @return void
|
||||||
|
*/
|
||||||
|
public function __construct($payload)
|
||||||
|
{
|
||||||
|
$this->payload = $payload;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Execute the job.
|
||||||
|
*
|
||||||
|
* @return void
|
||||||
|
*/
|
||||||
|
public function handle(): void
|
||||||
|
{
|
||||||
|
$payload = $this->payload;
|
||||||
|
|
||||||
|
if(empty($payload) || !isset($payload['actor'])) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
$profile = Profile::whereRemoteUrl($payload['actor'])->first();
|
||||||
|
|
||||||
|
if(!$profile || $profile->domain === null || $profile->private_key) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if($profile->sharedInbox == null || $profile->sharedInbox != $payload['object']['endpoints']['sharedInbox']) {
|
||||||
|
$profile->sharedInbox = $payload['object']['endpoints']['sharedInbox'];
|
||||||
|
}
|
||||||
|
|
||||||
|
if($profile->public_key !== $payload['object']['publicKey']['publicKeyPem']) {
|
||||||
|
$profile->public_key = $payload['object']['publicKey']['publicKeyPem'];
|
||||||
|
}
|
||||||
|
|
||||||
|
if($profile->bio !== $payload['object']['summary']) {
|
||||||
|
$len = strlen(strip_tags($payload['object']['summary']));
|
||||||
|
if($len) {
|
||||||
|
if($len > 500) {
|
||||||
|
$updated = strip_tags($payload['object']['summary']);
|
||||||
|
$updated = substr($updated, 0, config('pixelfed.max_bio_length'));
|
||||||
|
$profile->bio = Autolink::create()->autolink($updated);
|
||||||
|
} else {
|
||||||
|
$profile->bio = Purify::clean($payload['object']['summary']);
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
$profile->bio = null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if($profile->name !== $payload['object']['name']) {
|
||||||
|
$profile->name = Purify::clean(substr($payload['object']['name'], 0, config('pixelfed.max_name_length')));
|
||||||
|
}
|
||||||
|
|
||||||
|
if($profile->isDirty()) {
|
||||||
|
$profile->save();
|
||||||
|
}
|
||||||
|
|
||||||
|
if(isset($payload['object']['icon']) && isset($payload['object']['icon']['url'])) {
|
||||||
|
RemoteAvatarFetchFromUrl::dispatch($profile, $payload['object']['icon']['url'])->onQueue('low');
|
||||||
|
} else {
|
||||||
|
$profile->avatar->update(['remote_url' => null]);
|
||||||
|
Cache::forget('avatar:' . $profile->id);
|
||||||
|
}
|
||||||
|
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
}
|
|
@ -76,10 +76,6 @@ class SharePipeline implements ShouldQueue
|
||||||
'action' => 'share',
|
'action' => 'share',
|
||||||
'item_type' => 'App\Status',
|
'item_type' => 'App\Status',
|
||||||
'item_id' => $status->reblog_of_id ?? $status->id,
|
'item_id' => $status->reblog_of_id ?? $status->id,
|
||||||
],
|
|
||||||
[
|
|
||||||
'message' => $status->shareToText(),
|
|
||||||
'rendered' => $status->shareToHtml()
|
|
||||||
]
|
]
|
||||||
);
|
);
|
||||||
|
|
||||||
|
|
|
@ -3,6 +3,7 @@
|
||||||
namespace App\Jobs\StatusPipeline;
|
namespace App\Jobs\StatusPipeline;
|
||||||
|
|
||||||
use Cache, Log;
|
use Cache, Log;
|
||||||
|
use App\Profile;
|
||||||
use App\Status;
|
use App\Status;
|
||||||
use Illuminate\Bus\Queueable;
|
use Illuminate\Bus\Queueable;
|
||||||
use Illuminate\Contracts\Queue\ShouldQueue;
|
use Illuminate\Contracts\Queue\ShouldQueue;
|
||||||
|
@ -52,12 +53,36 @@ class StatusActivityPubDeliver implements ShouldQueue
|
||||||
$status = $this->status;
|
$status = $this->status;
|
||||||
$profile = $status->profile;
|
$profile = $status->profile;
|
||||||
|
|
||||||
|
// ignore group posts
|
||||||
|
// if($status->group_id != null) {
|
||||||
|
// return;
|
||||||
|
// }
|
||||||
|
|
||||||
if($status->local == false || $status->url || $status->uri) {
|
if($status->local == false || $status->url || $status->uri) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
$audience = $status->profile->getAudienceInbox();
|
$audience = $status->profile->getAudienceInbox();
|
||||||
|
|
||||||
|
$parentInbox = [];
|
||||||
|
|
||||||
|
$mentions = $status->mentions
|
||||||
|
->filter(function($f) { return $f->domain !== null;})
|
||||||
|
->values()
|
||||||
|
->map(function($m) { return $m->sharedInbox ?? $m->inbox_url; })
|
||||||
|
->toArray();
|
||||||
|
|
||||||
|
if($status->in_reply_to_profile_id) {
|
||||||
|
$parent = Profile::find($status->in_reply_to_profile_id);
|
||||||
|
if($parent && $parent->domain !== null) {
|
||||||
|
$parentInbox = [
|
||||||
|
$parent->sharedInbox ?? $parent->inbox_url
|
||||||
|
];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
$audience = array_values(array_unique(array_merge($audience, $mentions, $parentInbox)));
|
||||||
|
|
||||||
if(empty($audience) || !in_array($status->scope, ['public', 'unlisted', 'private'])) {
|
if(empty($audience) || !in_array($status->scope, ['public', 'unlisted', 'private'])) {
|
||||||
// Return on profiles with no remote followers
|
// Return on profiles with no remote followers
|
||||||
return;
|
return;
|
||||||
|
|
|
@ -89,7 +89,6 @@ class StatusEntityLexer implements ShouldQueue
|
||||||
DB::transaction(function () {
|
DB::transaction(function () {
|
||||||
$status = $this->status;
|
$status = $this->status;
|
||||||
$status->rendered = nl2br($this->autolink);
|
$status->rendered = nl2br($this->autolink);
|
||||||
$status->entities = json_encode($this->entities);
|
|
||||||
$status->save();
|
$status->save();
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
|
@ -0,0 +1,129 @@
|
||||||
|
<?php
|
||||||
|
|
||||||
|
namespace App\Jobs\StatusPipeline;
|
||||||
|
|
||||||
|
use Cache, Log;
|
||||||
|
use App\Status;
|
||||||
|
use Illuminate\Bus\Queueable;
|
||||||
|
use Illuminate\Contracts\Queue\ShouldQueue;
|
||||||
|
use Illuminate\Foundation\Bus\Dispatchable;
|
||||||
|
use Illuminate\Queue\InteractsWithQueue;
|
||||||
|
use Illuminate\Queue\SerializesModels;
|
||||||
|
use League\Fractal;
|
||||||
|
use League\Fractal\Serializer\ArraySerializer;
|
||||||
|
use App\Transformer\ActivityPub\Verb\UpdateNote;
|
||||||
|
use App\Util\ActivityPub\Helpers;
|
||||||
|
use GuzzleHttp\Pool;
|
||||||
|
use GuzzleHttp\Client;
|
||||||
|
use GuzzleHttp\Promise;
|
||||||
|
use App\Util\ActivityPub\HttpSignature;
|
||||||
|
|
||||||
|
class StatusLocalUpdateActivityPubDeliverPipeline implements ShouldQueue
|
||||||
|
{
|
||||||
|
use Dispatchable, InteractsWithQueue, Queueable, SerializesModels;
|
||||||
|
|
||||||
|
protected $status;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Delete the job if its models no longer exist.
|
||||||
|
*
|
||||||
|
* @var bool
|
||||||
|
*/
|
||||||
|
public $deleteWhenMissingModels = true;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Create a new job instance.
|
||||||
|
*
|
||||||
|
* @return void
|
||||||
|
*/
|
||||||
|
public function __construct(Status $status)
|
||||||
|
{
|
||||||
|
$this->status = $status;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Execute the job.
|
||||||
|
*
|
||||||
|
* @return void
|
||||||
|
*/
|
||||||
|
public function handle()
|
||||||
|
{
|
||||||
|
$status = $this->status;
|
||||||
|
$profile = $status->profile;
|
||||||
|
|
||||||
|
// ignore group posts
|
||||||
|
// if($status->group_id != null) {
|
||||||
|
// return;
|
||||||
|
// }
|
||||||
|
|
||||||
|
if($status->local == false || $status->url || $status->uri) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
$audience = $status->profile->getAudienceInbox();
|
||||||
|
|
||||||
|
if(empty($audience) || !in_array($status->scope, ['public', 'unlisted', 'private'])) {
|
||||||
|
// Return on profiles with no remote followers
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
switch($status->type) {
|
||||||
|
case 'poll':
|
||||||
|
// Polls not yet supported
|
||||||
|
return;
|
||||||
|
break;
|
||||||
|
|
||||||
|
default:
|
||||||
|
$activitypubObject = new UpdateNote();
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
$fractal = new Fractal\Manager();
|
||||||
|
$fractal->setSerializer(new ArraySerializer());
|
||||||
|
$resource = new Fractal\Resource\Item($status, $activitypubObject);
|
||||||
|
$activity = $fractal->createData($resource)->toArray();
|
||||||
|
|
||||||
|
$payload = json_encode($activity);
|
||||||
|
|
||||||
|
$client = new Client([
|
||||||
|
'timeout' => config('federation.activitypub.delivery.timeout')
|
||||||
|
]);
|
||||||
|
|
||||||
|
$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' => config('federation.activitypub.delivery.concurrency'),
|
||||||
|
'fulfilled' => function ($response, $index) {
|
||||||
|
},
|
||||||
|
'rejected' => function ($reason, $index) {
|
||||||
|
}
|
||||||
|
]);
|
||||||
|
|
||||||
|
$promise = $pool->promise();
|
||||||
|
|
||||||
|
$promise->wait();
|
||||||
|
}
|
||||||
|
}
|
173
app/Jobs/StatusPipeline/StatusRemoteUpdatePipeline.php
Normal file
173
app/Jobs/StatusPipeline/StatusRemoteUpdatePipeline.php
Normal file
|
@ -0,0 +1,173 @@
|
||||||
|
<?php
|
||||||
|
|
||||||
|
namespace App\Jobs\StatusPipeline;
|
||||||
|
|
||||||
|
use Illuminate\Bus\Queueable;
|
||||||
|
use Illuminate\Contracts\Queue\ShouldBeUnique;
|
||||||
|
use Illuminate\Contracts\Queue\ShouldQueue;
|
||||||
|
use Illuminate\Foundation\Bus\Dispatchable;
|
||||||
|
use Illuminate\Queue\InteractsWithQueue;
|
||||||
|
use Illuminate\Queue\SerializesModels;
|
||||||
|
use App\Media;
|
||||||
|
use App\ModLog;
|
||||||
|
use App\Profile;
|
||||||
|
use App\Status;
|
||||||
|
use App\Models\StatusEdit;
|
||||||
|
use App\Services\StatusService;
|
||||||
|
use Purify;
|
||||||
|
use Illuminate\Support\Facades\Http;
|
||||||
|
|
||||||
|
class StatusRemoteUpdatePipeline implements ShouldQueue
|
||||||
|
{
|
||||||
|
use Dispatchable, InteractsWithQueue, Queueable, SerializesModels;
|
||||||
|
|
||||||
|
public $activity;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Create a new job instance.
|
||||||
|
*/
|
||||||
|
public function __construct($activity)
|
||||||
|
{
|
||||||
|
$this->activity = $activity;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Execute the job.
|
||||||
|
*/
|
||||||
|
public function handle(): void
|
||||||
|
{
|
||||||
|
$activity = $this->activity;
|
||||||
|
$status = Status::with('media')->whereObjectUrl($activity['id'])->first();
|
||||||
|
if(!$status) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
$this->createPreviousEdit($status);
|
||||||
|
$this->updateMedia($status, $activity);
|
||||||
|
$this->updateImmediateAttributes($status, $activity);
|
||||||
|
$this->createEdit($status, $activity);
|
||||||
|
}
|
||||||
|
|
||||||
|
protected function createPreviousEdit($status)
|
||||||
|
{
|
||||||
|
if(!$status->edits()->count()) {
|
||||||
|
StatusEdit::create([
|
||||||
|
'status_id' => $status->id,
|
||||||
|
'profile_id' => $status->profile_id,
|
||||||
|
'caption' => $status->caption,
|
||||||
|
'spoiler_text' => $status->cw_summary,
|
||||||
|
'is_nsfw' => $status->is_nsfw,
|
||||||
|
'ordered_media_attachment_ids' => $status->media()->orderBy('order')->pluck('id')->toArray(),
|
||||||
|
'created_at' => $status->created_at
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
protected function updateMedia($status, $activity)
|
||||||
|
{
|
||||||
|
if(!isset($activity['attachment'])) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
$ogm = $status->media->count() ? $status->media()->orderBy('order')->get() : collect([]);
|
||||||
|
$nm = collect($activity['attachment'])->filter(function($nm) {
|
||||||
|
return isset(
|
||||||
|
$nm['type'],
|
||||||
|
$nm['mediaType'],
|
||||||
|
$nm['url']
|
||||||
|
) &&
|
||||||
|
in_array($nm['type'], ['Document', 'Image', 'Video']) &&
|
||||||
|
in_array($nm['mediaType'], explode(',', config('pixelfed.media_types')));
|
||||||
|
});
|
||||||
|
|
||||||
|
// Skip when no media
|
||||||
|
if(!$ogm->count() && !$nm->count()) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
Media::whereProfileId($status->profile_id)
|
||||||
|
->whereStatusId($status->id)
|
||||||
|
->update([
|
||||||
|
'status_id' => null
|
||||||
|
]);
|
||||||
|
|
||||||
|
$nm->each(function($n, $key) use($status) {
|
||||||
|
$res = Http::retry(3, 100, throw: false)->head($n['url']);
|
||||||
|
|
||||||
|
if(!$res->successful()) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if(!in_array($res->header('content-type'), explode(',',config('pixelfed.media_types')))) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
$m = new Media;
|
||||||
|
$m->status_id = $status->id;
|
||||||
|
$m->profile_id = $status->profile_id;
|
||||||
|
$m->remote_media = true;
|
||||||
|
$m->media_path = $n['url'];
|
||||||
|
$m->mime = $res->header('content-type');
|
||||||
|
$m->size = $res->hasHeader('content-length') ? $res->header('content-length') : null;
|
||||||
|
$m->caption = isset($n['name']) && !empty($n['name']) ? Purify::clean($n['name']) : null;
|
||||||
|
$m->remote_url = $n['url'];
|
||||||
|
$m->blurhash = isset($n['blurhash']) && (strlen($n['blurhash']) < 50) ? $n['blurhash'] : null;
|
||||||
|
$m->width = isset($n['width']) && !empty($n['width']) ? $n['width'] : null;
|
||||||
|
$m->height = isset($n['height']) && !empty($n['height']) ? $n['height'] : null;
|
||||||
|
$m->skip_optimize = true;
|
||||||
|
$m->order = $key + 1;
|
||||||
|
$m->save();
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
protected function updateImmediateAttributes($status, $activity)
|
||||||
|
{
|
||||||
|
if(isset($activity['content'])) {
|
||||||
|
$status->caption = strip_tags($activity['content']);
|
||||||
|
$status->rendered = Purify::clean($activity['content']);
|
||||||
|
}
|
||||||
|
|
||||||
|
if(isset($activity['sensitive'])) {
|
||||||
|
if((bool) $activity['sensitive'] == false) {
|
||||||
|
$status->is_nsfw = false;
|
||||||
|
$exists = ModLog::whereObjectType('App\Status::class')
|
||||||
|
->whereObjectId($status->id)
|
||||||
|
->whereAction('admin.status.moderate')
|
||||||
|
->exists();
|
||||||
|
if($exists == true) {
|
||||||
|
$status->is_nsfw = true;
|
||||||
|
}
|
||||||
|
$profile = Profile::find($status->profile_id);
|
||||||
|
if(!$profile || $profile->cw == true) {
|
||||||
|
$status->is_nsfw = true;
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
$status->is_nsfw = true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if(isset($activity['summary'])) {
|
||||||
|
$status->cw_summary = Purify::clean($activity['summary']);
|
||||||
|
} else {
|
||||||
|
$status->cw_summary = null;
|
||||||
|
}
|
||||||
|
|
||||||
|
$status->edited_at = now();
|
||||||
|
$status->save();
|
||||||
|
StatusService::del($status->id);
|
||||||
|
}
|
||||||
|
|
||||||
|
protected function createEdit($status, $activity)
|
||||||
|
{
|
||||||
|
$cleaned = isset($activity['content']) ? Purify::clean($activity['content']) : null;
|
||||||
|
$spoiler_text = isset($activity['summary']) ? Purify::clean($activity['summary']) : null;
|
||||||
|
$sensitive = isset($activity['sensitive']) ? $activity['sensitive'] : null;
|
||||||
|
$mids = $status->media()->count() ? $status->media()->orderBy('order')->pluck('id')->toArray() : null;
|
||||||
|
StatusEdit::create([
|
||||||
|
'status_id' => $status->id,
|
||||||
|
'profile_id' => $status->profile_id,
|
||||||
|
'caption' => $cleaned,
|
||||||
|
'spoiler_text' => $spoiler_text,
|
||||||
|
'is_nsfw' => $sensitive,
|
||||||
|
'ordered_media_attachment_ids' => $mids
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
}
|
|
@ -70,10 +70,12 @@ class StatusReplyPipeline implements ShouldQueue
|
||||||
}
|
}
|
||||||
|
|
||||||
if(config('database.default') === 'mysql') {
|
if(config('database.default') === 'mysql') {
|
||||||
$exp = DB::raw("select id, in_reply_to_id from statuses, (select @pv := :kid) initialisation where id > @pv and find_in_set(in_reply_to_id, @pv) > 0 and @pv := concat(@pv, ',', id)");
|
// todo: refactor
|
||||||
$expQuery = $exp->getValue(DB::connection()->getQueryGrammar());
|
// $exp = DB::raw("select id, in_reply_to_id from statuses, (select @pv := :kid) initialisation where id > @pv and find_in_set(in_reply_to_id, @pv) > 0 and @pv := concat(@pv, ',', id)");
|
||||||
$count = DB::select($expQuery, [ 'kid' => $reply->id ]);
|
// $expQuery = $exp->getValue(DB::connection()->getQueryGrammar());
|
||||||
$reply->reply_count = count($count);
|
// $count = DB::select($expQuery, [ 'kid' => $reply->id ]);
|
||||||
|
// $reply->reply_count = count($count);
|
||||||
|
$reply->reply_count = $reply->reply_count + 1;
|
||||||
$reply->save();
|
$reply->save();
|
||||||
} else {
|
} else {
|
||||||
$reply->reply_count = $reply->reply_count + 1;
|
$reply->reply_count = $reply->reply_count + 1;
|
||||||
|
@ -90,8 +92,6 @@ class StatusReplyPipeline implements ShouldQueue
|
||||||
$notification->profile_id = $target->id;
|
$notification->profile_id = $target->id;
|
||||||
$notification->actor_id = $actor->id;
|
$notification->actor_id = $actor->id;
|
||||||
$notification->action = 'comment';
|
$notification->action = 'comment';
|
||||||
$notification->message = $status->replyToText();
|
|
||||||
$notification->rendered = $status->replyToHtml();
|
|
||||||
$notification->item_id = $status->id;
|
$notification->item_id = $status->id;
|
||||||
$notification->item_type = "App\Status";
|
$notification->item_type = "App\Status";
|
||||||
$notification->save();
|
$notification->save();
|
||||||
|
|
17
app/Like.php
17
app/Like.php
|
@ -31,21 +31,4 @@ class Like extends Model
|
||||||
{
|
{
|
||||||
return $this->belongsTo(Status::class);
|
return $this->belongsTo(Status::class);
|
||||||
}
|
}
|
||||||
|
|
||||||
public function toText($type = 'post')
|
|
||||||
{
|
|
||||||
$actorName = $this->actor->username;
|
|
||||||
$msg = $type == 'post' ? __('notification.likedPhoto') : __('notification.likedComment');
|
|
||||||
|
|
||||||
return "{$actorName} ".$msg;
|
|
||||||
}
|
|
||||||
|
|
||||||
public function toHtml($type = 'post')
|
|
||||||
{
|
|
||||||
$actorName = $this->actor->username;
|
|
||||||
$actorUrl = $this->actor->url();
|
|
||||||
$msg = $type == 'post' ? __('notification.likedPhoto') : __('notification.likedComment');
|
|
||||||
|
|
||||||
return "<a href='{$actorUrl}' class='profile-link'>{$actorName}</a> ".$msg;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
|
@ -29,20 +29,4 @@ class Mention extends Model
|
||||||
{
|
{
|
||||||
return $this->belongsTo(Status::class, 'status_id', 'id');
|
return $this->belongsTo(Status::class, 'status_id', 'id');
|
||||||
}
|
}
|
||||||
|
|
||||||
public function toText()
|
|
||||||
{
|
|
||||||
$actorName = $this->status->profile->username;
|
|
||||||
|
|
||||||
return "{$actorName} ".__('notification.mentionedYou');
|
|
||||||
}
|
|
||||||
|
|
||||||
public function toHtml()
|
|
||||||
{
|
|
||||||
$actorName = $this->status->profile->username;
|
|
||||||
$actorUrl = $this->status->profile->url();
|
|
||||||
|
|
||||||
return "<a href='{$actorUrl}' class='profile-link'>{$actorName}</a> ".
|
|
||||||
__('notification.mentionedYou');
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
11
app/Models/AutospamCustomTokens.php
Normal file
11
app/Models/AutospamCustomTokens.php
Normal file
|
@ -0,0 +1,11 @@
|
||||||
|
<?php
|
||||||
|
|
||||||
|
namespace App\Models;
|
||||||
|
|
||||||
|
use Illuminate\Database\Eloquent\Factories\HasFactory;
|
||||||
|
use Illuminate\Database\Eloquent\Model;
|
||||||
|
|
||||||
|
class AutospamCustomTokens extends Model
|
||||||
|
{
|
||||||
|
use HasFactory;
|
||||||
|
}
|
23
app/Models/ImportPost.php
Normal file
23
app/Models/ImportPost.php
Normal file
|
@ -0,0 +1,23 @@
|
||||||
|
<?php
|
||||||
|
|
||||||
|
namespace App\Models;
|
||||||
|
|
||||||
|
use Illuminate\Database\Eloquent\Factories\HasFactory;
|
||||||
|
use Illuminate\Database\Eloquent\Model;
|
||||||
|
use App\Status;
|
||||||
|
|
||||||
|
class ImportPost extends Model
|
||||||
|
{
|
||||||
|
use HasFactory;
|
||||||
|
|
||||||
|
protected $casts = [
|
||||||
|
'media' => 'array',
|
||||||
|
'creation_date' => 'datetime',
|
||||||
|
'metadata' => 'json'
|
||||||
|
];
|
||||||
|
|
||||||
|
public function status()
|
||||||
|
{
|
||||||
|
return $this->hasOne(Status::class, 'id', 'status_id');
|
||||||
|
}
|
||||||
|
}
|
|
@ -23,7 +23,61 @@ class InstanceActor extends Model
|
||||||
public function getActor()
|
public function getActor()
|
||||||
{
|
{
|
||||||
return [
|
return [
|
||||||
'@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' => $this->permalink(),
|
'id' => $this->permalink(),
|
||||||
'type' => 'Application',
|
'type' => 'Application',
|
||||||
'inbox' => $this->permalink('/inbox'),
|
'inbox' => $this->permalink('/inbox'),
|
||||||
|
|
19
app/Models/StatusEdit.php
Normal file
19
app/Models/StatusEdit.php
Normal file
|
@ -0,0 +1,19 @@
|
||||||
|
<?php
|
||||||
|
|
||||||
|
namespace App\Models;
|
||||||
|
|
||||||
|
use Illuminate\Database\Eloquent\Factories\HasFactory;
|
||||||
|
use Illuminate\Database\Eloquent\Model;
|
||||||
|
|
||||||
|
class StatusEdit extends Model
|
||||||
|
{
|
||||||
|
use HasFactory;
|
||||||
|
|
||||||
|
protected $casts = [
|
||||||
|
'ordered_media_attachment_ids' => 'array',
|
||||||
|
'media_descriptions' => 'array',
|
||||||
|
'poll_options' => 'array'
|
||||||
|
];
|
||||||
|
|
||||||
|
protected $guarded = [];
|
||||||
|
}
|
30
app/Models/UserAppSettings.php
Normal file
30
app/Models/UserAppSettings.php
Normal file
|
@ -0,0 +1,30 @@
|
||||||
|
<?php
|
||||||
|
|
||||||
|
namespace App\Models;
|
||||||
|
|
||||||
|
use Illuminate\Database\Eloquent\Factories\HasFactory;
|
||||||
|
use Illuminate\Database\Eloquent\Model;
|
||||||
|
use App\User;
|
||||||
|
|
||||||
|
class UserAppSettings extends Model
|
||||||
|
{
|
||||||
|
use HasFactory;
|
||||||
|
|
||||||
|
protected $guarded = [];
|
||||||
|
|
||||||
|
protected $casts = [
|
||||||
|
'common' => 'json',
|
||||||
|
'custom' => 'json',
|
||||||
|
'common.timelines.show_public' => 'boolean',
|
||||||
|
'common.timelines.show_network' => 'boolean',
|
||||||
|
'common.timelines.hide_likes_shares' => 'boolean',
|
||||||
|
'common.media.hide_public_behind_cw' => 'boolean',
|
||||||
|
'common.media.always_show_cw' => 'boolean',
|
||||||
|
'common.media.show_alt_text' => 'boolean',
|
||||||
|
];
|
||||||
|
|
||||||
|
public function user()
|
||||||
|
{
|
||||||
|
return $this->belongsTo(User::class);
|
||||||
|
}
|
||||||
|
}
|
|
@ -5,6 +5,8 @@ namespace App\Observers;
|
||||||
use App\Status;
|
use App\Status;
|
||||||
use App\Services\ProfileStatusService;
|
use App\Services\ProfileStatusService;
|
||||||
use Cache;
|
use Cache;
|
||||||
|
use App\Models\ImportPost;
|
||||||
|
use App\Services\ImportService;
|
||||||
|
|
||||||
class StatusObserver
|
class StatusObserver
|
||||||
{
|
{
|
||||||
|
@ -56,6 +58,11 @@ class StatusObserver
|
||||||
}
|
}
|
||||||
|
|
||||||
ProfileStatusService::delete($status->profile_id, $status->id);
|
ProfileStatusService::delete($status->profile_id, $status->id);
|
||||||
|
|
||||||
|
if($status->uri == null) {
|
||||||
|
ImportPost::whereProfileId($status->profile_id)->whereStatusId($status->id)->delete();
|
||||||
|
ImportService::clearImportedFiles($status->profile_id);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|
|
@ -178,6 +178,13 @@ class Profile extends Model
|
||||||
return url('/storage/avatars/default.jpg');
|
return url('/storage/avatars/default.jpg');
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if( $avatar->is_remote &&
|
||||||
|
$avatar->remote_url &&
|
||||||
|
boolval(config_cache('federation.avatars.store_local')) == true
|
||||||
|
) {
|
||||||
|
return $avatar->remote_url;
|
||||||
|
}
|
||||||
|
|
||||||
if($path === 'public/avatars/default.jpg') {
|
if($path === 'public/avatars/default.jpg') {
|
||||||
return url('/storage/avatars/default.jpg');
|
return url('/storage/avatars/default.jpg');
|
||||||
}
|
}
|
||||||
|
|
|
@ -24,7 +24,7 @@ class AuthServiceProvider extends ServiceProvider
|
||||||
*/
|
*/
|
||||||
public function boot()
|
public function boot()
|
||||||
{
|
{
|
||||||
if(config_cache('pixelfed.oauth_enabled') == true) {
|
if(config('app.env') === 'production' && config('pixelfed.oauth_enabled') == true) {
|
||||||
Passport::tokensExpireIn(now()->addDays(config('instance.oauth.token_expiration', 356)));
|
Passport::tokensExpireIn(now()->addDays(config('instance.oauth.token_expiration', 356)));
|
||||||
Passport::refreshTokensExpireIn(now()->addDays(config('instance.oauth.refresh_expiration', 400)));
|
Passport::refreshTokensExpireIn(now()->addDays(config('instance.oauth.refresh_expiration', 400)));
|
||||||
Passport::enableImplicitGrant();
|
Passport::enableImplicitGrant();
|
||||||
|
|
43
app/Services/Account/AccountAppSettingsService.php
Normal file
43
app/Services/Account/AccountAppSettingsService.php
Normal file
|
@ -0,0 +1,43 @@
|
||||||
|
<?php
|
||||||
|
|
||||||
|
namespace App\Services\Account;
|
||||||
|
|
||||||
|
use App\Models\UserAppSettings;
|
||||||
|
|
||||||
|
class AccountAppSettingsService
|
||||||
|
{
|
||||||
|
public static function default()
|
||||||
|
{
|
||||||
|
return [
|
||||||
|
'timelines' => [
|
||||||
|
// Show public timeline feed
|
||||||
|
'show_public' => false,
|
||||||
|
|
||||||
|
// Show network timeline feed
|
||||||
|
'show_network' => false,
|
||||||
|
|
||||||
|
// Hide likes and share counts
|
||||||
|
'hide_likes_shares' => false,
|
||||||
|
],
|
||||||
|
|
||||||
|
'media' => [
|
||||||
|
// Hide media on Public/Network timelines behind CW
|
||||||
|
'hide_public_behind_cw' => true,
|
||||||
|
|
||||||
|
// Always show media with CW
|
||||||
|
'always_show_cw' => false,
|
||||||
|
|
||||||
|
// Show alt text if present below media
|
||||||
|
'show_alt_text' => false,
|
||||||
|
],
|
||||||
|
|
||||||
|
'appearance' => [
|
||||||
|
// Use in-app browser when opening links
|
||||||
|
'links_use_in_app_browser' => true,
|
||||||
|
|
||||||
|
// App theme, can be 'light', 'dark' or 'system'
|
||||||
|
'theme' => 'system',
|
||||||
|
]
|
||||||
|
];
|
||||||
|
}
|
||||||
|
}
|
|
@ -19,12 +19,11 @@ class ActivityPubFetchService
|
||||||
|
|
||||||
$baseHeaders = [
|
$baseHeaders = [
|
||||||
'Accept' => 'application/activity+json, application/ld+json',
|
'Accept' => 'application/activity+json, application/ld+json',
|
||||||
'User-Agent' => '(Pixelfed/'.config('pixelfed.version').'; +'.config('app.url').')'
|
|
||||||
];
|
];
|
||||||
|
|
||||||
$headers = HttpSignature::instanceActorSign($url, false, $baseHeaders);
|
$headers = HttpSignature::instanceActorSign($url, false, $baseHeaders, 'get');
|
||||||
$headers['Accept'] = 'application/activity+json, application/ld+json';
|
$headers['Accept'] = 'application/activity+json, application/ld+json';
|
||||||
$headers['User-Agent'] = '(Pixelfed/'.config('pixelfed.version').'; +'.config('app.url').')';
|
$headers['User-Agent'] = 'PixelFedBot/1.0.0 (Pixelfed/'.config('pixelfed.version').'; +'.config('app.url').')';
|
||||||
|
|
||||||
try {
|
try {
|
||||||
$res = Http::withHeaders($headers)
|
$res = Http::withHeaders($headers)
|
||||||
|
|
78
app/Services/AutospamService.php
Normal file
78
app/Services/AutospamService.php
Normal file
|
@ -0,0 +1,78 @@
|
||||||
|
<?php
|
||||||
|
|
||||||
|
namespace App\Services;
|
||||||
|
|
||||||
|
use Illuminate\Support\Facades\Cache;
|
||||||
|
use Illuminate\Support\Facades\Storage;
|
||||||
|
use App\Util\Lexer\Classifier;
|
||||||
|
|
||||||
|
class AutospamService
|
||||||
|
{
|
||||||
|
const CHCKD_CACHE_KEY = 'pf:services:autospam:nlp:checked';
|
||||||
|
const MODEL_CACHE_KEY = 'pf:services:autospam:nlp:model-cache';
|
||||||
|
const MODEL_FILE_PATH = 'nlp/active-training-data.json';
|
||||||
|
const MODEL_SPAM_PATH = 'nlp/spam.json';
|
||||||
|
const MODEL_HAM_PATH = 'nlp/ham.json';
|
||||||
|
|
||||||
|
public static function check($text)
|
||||||
|
{
|
||||||
|
if(!$text || strlen($text) == 0) {
|
||||||
|
false;
|
||||||
|
}
|
||||||
|
if(!self::active()) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
$model = self::getCachedModel();
|
||||||
|
$classifier = new Classifier;
|
||||||
|
$classifier->import($model['documents'], $model['words']);
|
||||||
|
return $classifier->most($text) === 'spam';
|
||||||
|
}
|
||||||
|
|
||||||
|
public static function eligible()
|
||||||
|
{
|
||||||
|
return Cache::remember(self::CHCKD_CACHE_KEY, 86400, function() {
|
||||||
|
if(!config_cache('pixelfed.bouncer.enabled') || !config('autospam.enabled')) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
if(!Storage::exists(self::MODEL_SPAM_PATH)) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
if(!Storage::exists(self::MODEL_HAM_PATH)) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
if(!Storage::exists(self::MODEL_FILE_PATH)) {
|
||||||
|
return false;
|
||||||
|
} else {
|
||||||
|
if(Storage::size(self::MODEL_FILE_PATH) < 1000) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return true;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
public static function active()
|
||||||
|
{
|
||||||
|
return config_cache('autospam.nlp.enabled') && self::eligible();
|
||||||
|
}
|
||||||
|
|
||||||
|
public static function getCachedModel()
|
||||||
|
{
|
||||||
|
if(!self::active()) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
return Cache::remember(self::MODEL_CACHE_KEY, 86400, function() {
|
||||||
|
$res = Storage::get(self::MODEL_FILE_PATH);
|
||||||
|
if(!$res || empty($res)) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
return json_decode($res, true);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
23
app/Services/AvatarService.php
Normal file
23
app/Services/AvatarService.php
Normal file
|
@ -0,0 +1,23 @@
|
||||||
|
<?php
|
||||||
|
|
||||||
|
namespace App\Services;
|
||||||
|
|
||||||
|
use Cache;
|
||||||
|
use App\Profile;
|
||||||
|
|
||||||
|
class AvatarService
|
||||||
|
{
|
||||||
|
public static function get($profile_id)
|
||||||
|
{
|
||||||
|
$exists = Cache::get('avatar:' . $profile_id);
|
||||||
|
if($exists) {
|
||||||
|
return $exists;
|
||||||
|
}
|
||||||
|
|
||||||
|
$profile = Profile::find($profile_id);
|
||||||
|
if(!$profile) {
|
||||||
|
return config('app.url') . '/storage/avatars/default.jpg';
|
||||||
|
}
|
||||||
|
return $profile->avatarUrl();
|
||||||
|
}
|
||||||
|
}
|
|
@ -65,6 +65,13 @@ class ConfigCacheService
|
||||||
'pixelfed.directory.latest_response',
|
'pixelfed.directory.latest_response',
|
||||||
'pixelfed.directory.is_synced',
|
'pixelfed.directory.is_synced',
|
||||||
'pixelfed.directory.testimonials',
|
'pixelfed.directory.testimonials',
|
||||||
|
|
||||||
|
'instance.landing.show_directory',
|
||||||
|
'instance.landing.show_explore',
|
||||||
|
'instance.admin.pid',
|
||||||
|
'instance.banner.blurhash',
|
||||||
|
|
||||||
|
'autospam.nlp.enabled',
|
||||||
// 'system.user_mode'
|
// 'system.user_mode'
|
||||||
];
|
];
|
||||||
|
|
||||||
|
|
|
@ -28,8 +28,8 @@ class HashtagService {
|
||||||
|
|
||||||
public static function count($id)
|
public static function count($id)
|
||||||
{
|
{
|
||||||
return Cache::remember('services:hashtag:count:by_id:' . $id, 3600, function() use($id) {
|
return Cache::remember('services:hashtag:public-count:by_id:' . $id, 86400, function() use($id) {
|
||||||
return StatusHashtag::whereHashtagId($id)->count();
|
return StatusHashtag::whereHashtagId($id)->whereStatusVisibility('public')->count();
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -64,4 +64,9 @@ class HashtagService {
|
||||||
{
|
{
|
||||||
return Redis::zrem(self::FOLLOW_KEY . $pid, $hid);
|
return Redis::zrem(self::FOLLOW_KEY . $pid, $hid);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public static function following($pid, $start = 0, $limit = 10)
|
||||||
|
{
|
||||||
|
return Redis::zrevrange(self::FOLLOW_KEY . $pid, $start, $limit);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
114
app/Services/ImportService.php
Normal file
114
app/Services/ImportService.php
Normal file
|
@ -0,0 +1,114 @@
|
||||||
|
<?php
|
||||||
|
|
||||||
|
namespace App\Services;
|
||||||
|
|
||||||
|
use App\Models\ImportPost;
|
||||||
|
use Cache;
|
||||||
|
|
||||||
|
class ImportService
|
||||||
|
{
|
||||||
|
const CACHE_KEY = 'pf:import-service:';
|
||||||
|
|
||||||
|
public static function getId($userId, $year, $month, $day)
|
||||||
|
{
|
||||||
|
if($userId > 999999) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if($year < 9 || $year > 23) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if($month < 1 || $month > 12) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if($day < 1 || $day > 31) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
$start = 1;
|
||||||
|
$key = self::CACHE_KEY . 'getIdRange:incr:byUserId:' . $userId . ':y-' . $year . ':m-' . $month . ':d-' . $day;
|
||||||
|
$incr = Cache::increment($key, random_int(5, 19));
|
||||||
|
if($incr > 999) {
|
||||||
|
$daysInMonth = now()->parse($day . '-' . $month . '-' . $year)->daysInMonth;
|
||||||
|
|
||||||
|
if($month == 12) {
|
||||||
|
$year = $year + 1;
|
||||||
|
$month = 1;
|
||||||
|
$day = 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
if($day + 1 >= $daysInMonth) {
|
||||||
|
$day = 1;
|
||||||
|
$month = $month + 1;
|
||||||
|
} else {
|
||||||
|
$day = $day + 1;
|
||||||
|
}
|
||||||
|
return self::getId($userId, $year, $month, $day);
|
||||||
|
}
|
||||||
|
$uid = str_pad($userId, 6, 0, STR_PAD_LEFT);
|
||||||
|
$year = str_pad($year, 2, 0, STR_PAD_LEFT);
|
||||||
|
$month = str_pad($month, 2, 0, STR_PAD_LEFT);
|
||||||
|
$day = str_pad($day, 2, 0, STR_PAD_LEFT);
|
||||||
|
$zone = $year . $month . $day . str_pad($incr, 3, 0, STR_PAD_LEFT);
|
||||||
|
return [
|
||||||
|
'id' => $start . $uid . $zone,
|
||||||
|
'year' => $year,
|
||||||
|
'month' => $month,
|
||||||
|
'day' => $day,
|
||||||
|
'incr' => $incr,
|
||||||
|
];
|
||||||
|
}
|
||||||
|
|
||||||
|
public static function getPostCount($profileId, $refresh = false)
|
||||||
|
{
|
||||||
|
$key = self::CACHE_KEY . 'totalPostCountByProfileId:' . $profileId;
|
||||||
|
if($refresh) {
|
||||||
|
Cache::forget($key);
|
||||||
|
}
|
||||||
|
return intval(Cache::remember($key, 21600, function() use($profileId) {
|
||||||
|
return ImportPost::whereProfileId($profileId)->whereSkipMissingMedia(false)->count();
|
||||||
|
}));
|
||||||
|
}
|
||||||
|
|
||||||
|
public static function getAttempts($profileId)
|
||||||
|
{
|
||||||
|
$key = self::CACHE_KEY . 'attemptsByProfileId:' . $profileId;
|
||||||
|
return intval(Cache::remember($key, 21600, function() use($profileId) {
|
||||||
|
return ImportPost::whereProfileId($profileId)
|
||||||
|
->whereSkipMissingMedia(false)
|
||||||
|
->get()
|
||||||
|
->groupBy(function($item) {
|
||||||
|
return $item->created_at->format('Y-m-d');
|
||||||
|
})
|
||||||
|
->count();
|
||||||
|
}));
|
||||||
|
}
|
||||||
|
|
||||||
|
public static function clearAttempts($profileId)
|
||||||
|
{
|
||||||
|
$key = self::CACHE_KEY . 'attemptsByProfileId:' . $profileId;
|
||||||
|
return Cache::forget($key);
|
||||||
|
}
|
||||||
|
|
||||||
|
public static function getImportedFiles($profileId, $refresh = false)
|
||||||
|
{
|
||||||
|
$key = self::CACHE_KEY . 'importedPostsByProfileId:' . $profileId;
|
||||||
|
if($refresh) {
|
||||||
|
Cache::forget($key);
|
||||||
|
}
|
||||||
|
return Cache::remember($key, 21600, function() use($profileId) {
|
||||||
|
return ImportPost::whereProfileId($profileId)
|
||||||
|
->get()
|
||||||
|
->filter(function($ip) {
|
||||||
|
return StatusService::get($ip->status_id) == null;
|
||||||
|
})
|
||||||
|
->map(function($ip) {
|
||||||
|
return collect($ip->media)->map(function($m) { return $m['uri']; });
|
||||||
|
})->values()->flatten();
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
public static function clearImportedFiles($profileId)
|
||||||
|
{
|
||||||
|
$key = self::CACHE_KEY . 'importedPostsByProfileId:' . $profileId;
|
||||||
|
return Cache::forget($key);
|
||||||
|
}
|
||||||
|
}
|
|
@ -4,6 +4,8 @@ namespace App\Services;
|
||||||
|
|
||||||
use Cache;
|
use Cache;
|
||||||
use App\Instance;
|
use App\Instance;
|
||||||
|
use App\Util\Blurhash\Blurhash;
|
||||||
|
use App\Services\ConfigCacheService;
|
||||||
|
|
||||||
class InstanceService
|
class InstanceService
|
||||||
{
|
{
|
||||||
|
@ -12,6 +14,12 @@ class InstanceService
|
||||||
const CACHE_KEY_UNLISTED_DOMAINS = 'instances:unlisted:domains';
|
const CACHE_KEY_UNLISTED_DOMAINS = 'instances:unlisted:domains';
|
||||||
const CACHE_KEY_NSFW_DOMAINS = 'instances:auto_cw:domains';
|
const CACHE_KEY_NSFW_DOMAINS = 'instances:auto_cw:domains';
|
||||||
const CACHE_KEY_STATS = 'pf:services:instances:stats';
|
const CACHE_KEY_STATS = 'pf:services:instances:stats';
|
||||||
|
const CACHE_KEY_BANNER_BLURHASH = 'pf:services:instance:header-blurhash:v1';
|
||||||
|
|
||||||
|
public function __construct()
|
||||||
|
{
|
||||||
|
ini_set('memory_limit', config('pixelfed.memory_limit', '1024M'));
|
||||||
|
}
|
||||||
|
|
||||||
public static function getByDomain($domain)
|
public static function getByDomain($domain)
|
||||||
{
|
{
|
||||||
|
@ -78,4 +86,50 @@ class InstanceService
|
||||||
|
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public static function headerBlurhash()
|
||||||
|
{
|
||||||
|
return Cache::rememberForever(self::CACHE_KEY_BANNER_BLURHASH, function() {
|
||||||
|
if(str_ends_with(config_cache('app.banner_image'), 'headers/default.jpg')) {
|
||||||
|
return 'UzJR]l{wHZRjM}R%XRkCH?X9xaWEjZj]kAjt';
|
||||||
|
}
|
||||||
|
$cached = config_cache('instance.banner.blurhash');
|
||||||
|
|
||||||
|
if($cached) {
|
||||||
|
return $cached;
|
||||||
|
}
|
||||||
|
|
||||||
|
$file = config_cache('app.banner_image') ?? url(Storage::url('public/headers/default.jpg'));
|
||||||
|
|
||||||
|
$image = imagecreatefromstring(file_get_contents($file));
|
||||||
|
if(!$image) {
|
||||||
|
return 'UzJR]l{wHZRjM}R%XRkCH?X9xaWEjZj]kAjt';
|
||||||
|
}
|
||||||
|
$width = imagesx($image);
|
||||||
|
$height = imagesy($image);
|
||||||
|
|
||||||
|
$pixels = [];
|
||||||
|
for ($y = 0; $y < $height; ++$y) {
|
||||||
|
$row = [];
|
||||||
|
for ($x = 0; $x < $width; ++$x) {
|
||||||
|
$index = imagecolorat($image, $x, $y);
|
||||||
|
$colors = imagecolorsforindex($image, $index);
|
||||||
|
|
||||||
|
$row[] = [$colors['red'], $colors['green'], $colors['blue']];
|
||||||
|
}
|
||||||
|
$pixels[] = $row;
|
||||||
|
}
|
||||||
|
|
||||||
|
$components_x = 4;
|
||||||
|
$components_y = 4;
|
||||||
|
$blurhash = Blurhash::encode($pixels, $components_x, $components_y);
|
||||||
|
if(strlen($blurhash) > 191) {
|
||||||
|
return 'UzJR]l{wHZRjM}R%XRkCH?X9xaWEjZj]kAjt';
|
||||||
|
}
|
||||||
|
|
||||||
|
ConfigCacheService::put('instance.banner.blurhash', $blurhash);
|
||||||
|
|
||||||
|
return $blurhash;
|
||||||
|
});
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -30,6 +30,9 @@ class LandingService
|
||||||
});
|
});
|
||||||
|
|
||||||
$contactAccount = Cache::remember('api:v1:instance-data:contact', 604800, function () {
|
$contactAccount = 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();
|
$admin = User::whereIsAdmin(true)->first();
|
||||||
return $admin && isset($admin->profile_id) ?
|
return $admin && isset($admin->profile_id) ?
|
||||||
AccountService::getMastodon($admin->profile_id, true) :
|
AccountService::getMastodon($admin->profile_id, true) :
|
||||||
|
@ -53,8 +56,8 @@ class LandingService
|
||||||
'name' => config_cache('app.name'),
|
'name' => config_cache('app.name'),
|
||||||
'url' => config_cache('app.url'),
|
'url' => config_cache('app.url'),
|
||||||
'domain' => config('pixelfed.domain.app'),
|
'domain' => config('pixelfed.domain.app'),
|
||||||
'show_directory' => config('instance.landing.show_directory'),
|
'show_directory' => config_cache('instance.landing.show_directory'),
|
||||||
'show_explore_feed' => config('instance.landing.show_explore'),
|
'show_explore_feed' => config_cache('instance.landing.show_explore'),
|
||||||
'open_registration' => config_cache('pixelfed.open_registration') == 1,
|
'open_registration' => config_cache('pixelfed.open_registration') == 1,
|
||||||
'version' => config('pixelfed.version'),
|
'version' => config('pixelfed.version'),
|
||||||
'about' => [
|
'about' => [
|
||||||
|
|
|
@ -85,7 +85,10 @@ class LikeService {
|
||||||
return $empty;
|
return $empty;
|
||||||
}
|
}
|
||||||
$id = $like->profile_id;
|
$id = $like->profile_id;
|
||||||
$profile = ProfileService::get($id);
|
$profile = ProfileService::get($id, true);
|
||||||
|
if(!$profile) {
|
||||||
|
return [];
|
||||||
|
}
|
||||||
$profileUrl = "/i/web/profile/{$profile['id']}";
|
$profileUrl = "/i/web/profile/{$profile['id']}";
|
||||||
$res = [
|
$res = [
|
||||||
'id' => (string) $profile['id'],
|
'id' => (string) $profile['id'],
|
||||||
|
|
|
@ -77,7 +77,9 @@ class MediaStorageService {
|
||||||
protected function cloudStore($media)
|
protected function cloudStore($media)
|
||||||
{
|
{
|
||||||
if($media->remote_media == true) {
|
if($media->remote_media == true) {
|
||||||
(new self())->remoteToCloud($media);
|
if(config('media.storage.remote.cloud')) {
|
||||||
|
(new self())->remoteToCloud($media);
|
||||||
|
}
|
||||||
} else {
|
} else {
|
||||||
(new self())->localToCloud($media);
|
(new self())->localToCloud($media);
|
||||||
}
|
}
|
||||||
|
@ -189,7 +191,7 @@ class MediaStorageService {
|
||||||
unlink($tmpName);
|
unlink($tmpName);
|
||||||
}
|
}
|
||||||
|
|
||||||
protected function fetchAvatar($avatar, $local = false)
|
protected function fetchAvatar($avatar, $local = false, $skipRecentCheck = false)
|
||||||
{
|
{
|
||||||
$url = $avatar->remote_url;
|
$url = $avatar->remote_url;
|
||||||
$driver = $local ? 'local' : config('filesystems.cloud');
|
$driver = $local ? 'local' : config('filesystems.cloud');
|
||||||
|
@ -213,10 +215,15 @@ class MediaStorageService {
|
||||||
$mime = $head['mime'];
|
$mime = $head['mime'];
|
||||||
$max_size = (int) config('pixelfed.max_avatar_size') * 1000;
|
$max_size = (int) config('pixelfed.max_avatar_size') * 1000;
|
||||||
|
|
||||||
if($avatar->last_fetched_at && $avatar->last_fetched_at->gt(now()->subDay())) {
|
if(!$skipRecentCheck) {
|
||||||
return;
|
if($avatar->last_fetched_at && $avatar->last_fetched_at->gt(now()->subDay())) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
Cache::forget('avatar:' . $avatar->profile_id);
|
||||||
|
AccountService::del($avatar->profile_id);
|
||||||
|
|
||||||
// handle pleroma edge case
|
// handle pleroma edge case
|
||||||
if(Str::endsWith($mime, '; charset=utf-8')) {
|
if(Str::endsWith($mime, '; charset=utf-8')) {
|
||||||
$mime = str_replace('; charset=utf-8', '', $mime);
|
$mime = str_replace('; charset=utf-8', '', $mime);
|
||||||
|
@ -264,7 +271,7 @@ class MediaStorageService {
|
||||||
$avatar->save();
|
$avatar->save();
|
||||||
|
|
||||||
Cache::forget('avatar:' . $avatar->profile_id);
|
Cache::forget('avatar:' . $avatar->profile_id);
|
||||||
Cache::forget(AccountService::CACHE_KEY . $avatar->profile_id);
|
AccountService::del($avatar->profile_id);
|
||||||
|
|
||||||
unlink($tmpName);
|
unlink($tmpName);
|
||||||
}
|
}
|
||||||
|
|
|
@ -57,7 +57,7 @@ class MediaTagService
|
||||||
|
|
||||||
protected function idToUsername($id)
|
protected function idToUsername($id)
|
||||||
{
|
{
|
||||||
$profile = ProfileService::get($id);
|
$profile = ProfileService::get($id, true);
|
||||||
|
|
||||||
if(!$profile) {
|
if(!$profile) {
|
||||||
return 'unavailable';
|
return 'unavailable';
|
||||||
|
@ -74,16 +74,13 @@ class MediaTagService
|
||||||
{
|
{
|
||||||
$p = $tag->status->profile;
|
$p = $tag->status->profile;
|
||||||
$actor = $p->username;
|
$actor = $p->username;
|
||||||
$message = "{$actor} tagged you in a post.";
|
|
||||||
$rendered = "<a href='/{$actor}' class='profile-link'>{$actor}</a> tagged you in a post.";
|
|
||||||
$n = new Notification;
|
$n = new Notification;
|
||||||
$n->profile_id = $tag->profile_id;
|
$n->profile_id = $tag->profile_id;
|
||||||
$n->actor_id = $p->id;
|
$n->actor_id = $p->id;
|
||||||
$n->item_id = $tag->id;
|
$n->item_id = $tag->id;
|
||||||
$n->item_type = 'App\MediaTag';
|
$n->item_type = 'App\MediaTag';
|
||||||
$n->action = 'tagged';
|
$n->action = 'tagged';
|
||||||
$n->message = $message;
|
|
||||||
$n->rendered = $rendered;
|
|
||||||
$n->save();
|
$n->save();
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
|
@ -108,8 +108,6 @@ class ModLogService {
|
||||||
{
|
{
|
||||||
$log = $this->log;
|
$log = $this->log;
|
||||||
|
|
||||||
$msg = "{$log->user_username} commented on a modlog";
|
|
||||||
$rendered = "<span class='font-weight-bold'>{$log->user_username}</span> commented on a <a href='/i/admin/users/modlogs/{$log->user_id}}' class='font-weight-bold text-decoration-none'>modlog</a>";
|
|
||||||
$item_id = $log->id;
|
$item_id = $log->id;
|
||||||
$item_type = 'App\ModLog';
|
$item_type = 'App\ModLog';
|
||||||
$action = 'admin.user.modlog.comment';
|
$action = 'admin.user.modlog.comment';
|
||||||
|
@ -127,8 +125,6 @@ class ModLogService {
|
||||||
$n->item_id = $item_id;
|
$n->item_id = $item_id;
|
||||||
$n->item_type = $item_type;
|
$n->item_type = $item_type;
|
||||||
$n->action = $action;
|
$n->action = $action;
|
||||||
$n->message = $msg;
|
|
||||||
$n->rendered = $rendered;
|
|
||||||
$n->save();
|
$n->save();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -139,4 +135,4 @@ class ModLogService {
|
||||||
->whereItemId($this->log->id)
|
->whereItemId($this->log->id)
|
||||||
->delete();
|
->delete();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -72,6 +72,26 @@ class NetworkTimelineService
|
||||||
return Redis::zcard(self::CACHE_KEY);
|
return Redis::zcard(self::CACHE_KEY);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public static function deleteByProfileId($profileId)
|
||||||
|
{
|
||||||
|
$res = Redis::zrange(self::CACHE_KEY, 0, '-1');
|
||||||
|
if(!$res) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
foreach($res as $postId) {
|
||||||
|
$s = StatusService::get($postId);
|
||||||
|
if(!$s) {
|
||||||
|
self::rem($postId);
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
if($s['account']['id'] == $profileId) {
|
||||||
|
self::rem($postId);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
public static function warmCache($force = false, $limit = 100)
|
public static function warmCache($force = false, $limit = 100)
|
||||||
{
|
{
|
||||||
if(self::count() == 0 || $force == true) {
|
if(self::count() == 0 || $force == true) {
|
||||||
|
|
|
@ -4,9 +4,9 @@ namespace App\Services;
|
||||||
|
|
||||||
class ProfileService
|
class ProfileService
|
||||||
{
|
{
|
||||||
public static function get($id)
|
public static function get($id, $softFail = false)
|
||||||
{
|
{
|
||||||
return AccountService::get($id);
|
return AccountService::get($id, $softFail);
|
||||||
}
|
}
|
||||||
|
|
||||||
public static function del($id)
|
public static function del($id)
|
||||||
|
|
|
@ -72,17 +72,37 @@ class PublicTimelineService {
|
||||||
return Redis::zcard(self::CACHE_KEY);
|
return Redis::zcard(self::CACHE_KEY);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public static function deleteByProfileId($profileId)
|
||||||
|
{
|
||||||
|
$res = Redis::zrange(self::CACHE_KEY, 0, '-1');
|
||||||
|
if(!$res) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
foreach($res as $postId) {
|
||||||
|
$s = StatusService::get($postId);
|
||||||
|
if(!$s) {
|
||||||
|
self::rem($postId);
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
if($s['account']['id'] == $profileId) {
|
||||||
|
self::rem($postId);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
public static function warmCache($force = false, $limit = 100)
|
public static function warmCache($force = false, $limit = 100)
|
||||||
{
|
{
|
||||||
if(self::count() == 0 || $force == true) {
|
if(self::count() == 0 || $force == true) {
|
||||||
$hideNsfw = config('instance.hide_nsfw_on_public_feeds');
|
$hideNsfw = config('instance.hide_nsfw_on_public_feeds');
|
||||||
Redis::del(self::CACHE_KEY);
|
Redis::del(self::CACHE_KEY);
|
||||||
$ids = Status::whereNull('uri')
|
$minId = SnowflakeService::byDate(now()->subDays(14));
|
||||||
->whereNull('in_reply_to_id')
|
$ids = Status::where('id', '>', $minId)
|
||||||
|
->whereNull(['uri', 'in_reply_to_id', 'reblog_of_id'])
|
||||||
->when($hideNsfw, function($q, $hideNsfw) {
|
->when($hideNsfw, function($q, $hideNsfw) {
|
||||||
return $q->where('is_nsfw', false);
|
return $q->where('is_nsfw', false);
|
||||||
})
|
})
|
||||||
->whereNull('reblog_of_id')
|
|
||||||
->whereIn('type', ['photo', 'photo:album', 'video', 'video:album', 'photo:video:album'])
|
->whereIn('type', ['photo', 'photo:album', 'video', 'video:album', 'photo:video:album'])
|
||||||
->whereScope('public')
|
->whereScope('public')
|
||||||
->orderByDesc('id')
|
->orderByDesc('id')
|
||||||
|
|
|
@ -52,6 +52,7 @@ class RelationshipService
|
||||||
|
|
||||||
public static function delete($aid, $tid)
|
public static function delete($aid, $tid)
|
||||||
{
|
{
|
||||||
|
Cache::forget(self::key("wd:a_{$aid}:t_{$tid}"));
|
||||||
return Cache::forget(self::key("a_{$aid}:t_{$tid}"));
|
return Cache::forget(self::key("a_{$aid}:t_{$tid}"));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -85,4 +86,24 @@ class RelationshipService
|
||||||
{
|
{
|
||||||
return self::CACHE_KEY . $suffix;
|
return self::CACHE_KEY . $suffix;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public static function getWithDate($aid, $tid)
|
||||||
|
{
|
||||||
|
$res = self::get($aid, $tid);
|
||||||
|
|
||||||
|
if(!$res || !$res['following']) {
|
||||||
|
$res['following_since'] = null;
|
||||||
|
return $res;
|
||||||
|
}
|
||||||
|
|
||||||
|
return Cache::remember(self::key("wd:a_{$aid}:t_{$tid}"), 1209600, function() use($aid, $tid, $res) {
|
||||||
|
$tmp = Follower::whereProfileId($aid)->whereFollowingId($tid)->first();
|
||||||
|
if(!$tmp) {
|
||||||
|
$res['following_since'] = null;
|
||||||
|
return $res;
|
||||||
|
}
|
||||||
|
$res['following_since'] = str_replace('+00:00', 'Z', $tmp->created_at->format(DATE_RFC3339_EXTENDED));
|
||||||
|
return $res;
|
||||||
|
});
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -96,9 +96,10 @@ class SearchApiV2Service
|
||||||
$webfingerQuery = '@' . $webfingerQuery;
|
$webfingerQuery = '@' . $webfingerQuery;
|
||||||
}
|
}
|
||||||
$banned = InstanceService::getBannedDomains();
|
$banned = InstanceService::getBannedDomains();
|
||||||
|
$operator = config('database.default') === 'pgsql' ? 'ilike' : 'like';
|
||||||
$results = Profile::select('username', 'id', 'followers_count', 'domain')
|
$results = Profile::select('username', 'id', 'followers_count', 'domain')
|
||||||
->where('username', 'like', $query)
|
->where('username', $operator, $query)
|
||||||
->orWhere('webfinger', 'like', $webfingerQuery)
|
->orWhere('webfinger', $operator, $webfingerQuery)
|
||||||
->orderByDesc('profiles.followers_count')
|
->orderByDesc('profiles.followers_count')
|
||||||
->offset($offset)
|
->offset($offset)
|
||||||
->limit($limit)
|
->limit($limit)
|
||||||
|
@ -160,23 +161,8 @@ class SearchApiV2Service
|
||||||
|
|
||||||
protected function statusesById()
|
protected function statusesById()
|
||||||
{
|
{
|
||||||
$mastodonMode = self::$mastodonMode;
|
// Removed until we provide more relevent sorting/results
|
||||||
$accountId = $this->query->input('account_id');
|
return [];
|
||||||
$limit = $this->query->input('limit', 20);
|
|
||||||
$query = '%' . $this->query->input('q') . '%';
|
|
||||||
$results = Status::where('caption', 'like', $query)
|
|
||||||
->whereProfileId($accountId)
|
|
||||||
->limit($limit)
|
|
||||||
->get()
|
|
||||||
->map(function($status) use($mastodonMode) {
|
|
||||||
return $mastodonMode ?
|
|
||||||
StatusService::getMastodon($status->id) :
|
|
||||||
StatusService::get($status->id);
|
|
||||||
})
|
|
||||||
->filter(function($status) {
|
|
||||||
return $status && isset($status['account']);
|
|
||||||
});
|
|
||||||
return $results;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
protected function resolveQuery()
|
protected function resolveQuery()
|
||||||
|
|
137
app/Services/Status/UpdateStatusService.php
Normal file
137
app/Services/Status/UpdateStatusService.php
Normal file
|
@ -0,0 +1,137 @@
|
||||||
|
<?php
|
||||||
|
|
||||||
|
namespace App\Services\Status;
|
||||||
|
|
||||||
|
use App\Media;
|
||||||
|
use App\ModLog;
|
||||||
|
use App\Status;
|
||||||
|
use App\Models\StatusEdit;
|
||||||
|
use Purify;
|
||||||
|
use App\Util\Lexer\Autolink;
|
||||||
|
use App\Services\MediaService;
|
||||||
|
use App\Services\MediaStorageService;
|
||||||
|
use App\Services\StatusService;
|
||||||
|
|
||||||
|
class UpdateStatusService
|
||||||
|
{
|
||||||
|
public static function call(Status $status, $attributes)
|
||||||
|
{
|
||||||
|
self::createPreviousEdit($status);
|
||||||
|
self::updateMediaAttachements($status, $attributes);
|
||||||
|
self::handleImmediateAttributes($status, $attributes);
|
||||||
|
self::createEdit($status, $attributes);
|
||||||
|
|
||||||
|
return StatusService::get($status->id);
|
||||||
|
}
|
||||||
|
|
||||||
|
public static function updateMediaAttachements(Status $status, $attributes)
|
||||||
|
{
|
||||||
|
$count = $status->media()->count();
|
||||||
|
if($count === 0 || $count === 1) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
$oids = $status->media()->orderBy('order')->pluck('id')->map(function($m) { return (string) $m; });
|
||||||
|
$nids = collect($attributes['media_ids']);
|
||||||
|
|
||||||
|
if($oids->toArray() === $nids->toArray()) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
foreach($oids->diff($nids)->values()->toArray() as $mid) {
|
||||||
|
$media = Media::find($mid);
|
||||||
|
if(!$media) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
$media->status_id = null;
|
||||||
|
$media->save();
|
||||||
|
MediaStorageService::delete($media, true);
|
||||||
|
}
|
||||||
|
|
||||||
|
$nids->each(function($nid, $idx) {
|
||||||
|
$media = Media::find($nid);
|
||||||
|
if(!$media) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
$media->order = $idx;
|
||||||
|
$media->save();
|
||||||
|
});
|
||||||
|
MediaService::del($status->id);
|
||||||
|
}
|
||||||
|
|
||||||
|
public static function handleImmediateAttributes(Status $status, $attributes)
|
||||||
|
{
|
||||||
|
if(isset($attributes['status'])) {
|
||||||
|
$cleaned = Purify::clean($attributes['status']);
|
||||||
|
$status->caption = $cleaned;
|
||||||
|
$status->rendered = nl2br(Autolink::create()->autolink($cleaned));
|
||||||
|
} else {
|
||||||
|
$status->caption = null;
|
||||||
|
$status->rendered = null;
|
||||||
|
}
|
||||||
|
if(isset($attributes['sensitive'])) {
|
||||||
|
if($status->is_nsfw != (bool) $attributes['sensitive'] &&
|
||||||
|
(bool) $attributes['sensitive'] == false)
|
||||||
|
{
|
||||||
|
$exists = ModLog::whereObjectType('App\Status::class')
|
||||||
|
->whereObjectId($status->id)
|
||||||
|
->whereAction('admin.status.moderate')
|
||||||
|
->exists();
|
||||||
|
if(!$exists) {
|
||||||
|
$status->is_nsfw = (bool) $attributes['sensitive'];
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
$status->is_nsfw = (bool) $attributes['sensitive'];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if(isset($attributes['spoiler_text'])) {
|
||||||
|
$status->cw_summary = Purify::clean($attributes['spoiler_text']);
|
||||||
|
} else {
|
||||||
|
$status->cw_summary = null;
|
||||||
|
}
|
||||||
|
if(isset($attributes['location'])) {
|
||||||
|
if (isset($attributes['location']['id'])) {
|
||||||
|
$status->place_id = $attributes['location']['id'];
|
||||||
|
} else {
|
||||||
|
$status->place_id = null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if($status->cw_summary && !$status->is_nsfw) {
|
||||||
|
$status->cw_summary = null;
|
||||||
|
}
|
||||||
|
$status->edited_at = now();
|
||||||
|
$status->save();
|
||||||
|
StatusService::del($status->id);
|
||||||
|
}
|
||||||
|
|
||||||
|
public static function createPreviousEdit(Status $status)
|
||||||
|
{
|
||||||
|
if(!$status->edits()->count()) {
|
||||||
|
StatusEdit::create([
|
||||||
|
'status_id' => $status->id,
|
||||||
|
'profile_id' => $status->profile_id,
|
||||||
|
'caption' => $status->caption,
|
||||||
|
'spoiler_text' => $status->cw_summary,
|
||||||
|
'is_nsfw' => $status->is_nsfw,
|
||||||
|
'ordered_media_attachment_ids' => $status->media()->orderBy('order')->pluck('id')->toArray(),
|
||||||
|
'created_at' => $status->created_at
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public static function createEdit(Status $status, $attributes)
|
||||||
|
{
|
||||||
|
$cleaned = isset($attributes['status']) ? Purify::clean($attributes['status']) : null;
|
||||||
|
$spoiler_text = isset($attributes['spoiler_text']) ? Purify::clean($attributes['spoiler_text']) : null;
|
||||||
|
$sensitive = isset($attributes['sensitive']) ? $attributes['sensitive'] : null;
|
||||||
|
$mids = $status->media()->count() ? $status->media()->orderBy('order')->pluck('id')->toArray() : null;
|
||||||
|
StatusEdit::create([
|
||||||
|
'status_id' => $status->id,
|
||||||
|
'profile_id' => $status->profile_id,
|
||||||
|
'caption' => $cleaned,
|
||||||
|
'spoiler_text' => $spoiler_text,
|
||||||
|
'is_nsfw' => $sensitive,
|
||||||
|
'ordered_media_attachment_ids' => $mids
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
}
|
|
@ -47,6 +47,10 @@ class StatusService
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if(!isset($status['account'])) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
$status['replies_count'] = $status['reply_count'];
|
$status['replies_count'] = $status['reply_count'];
|
||||||
|
|
||||||
if(config('exp.emc') == false) {
|
if(config('exp.emc') == false) {
|
||||||
|
@ -117,6 +121,9 @@ class StatusService
|
||||||
public static function getFull($id, $pid, $publicOnly = true)
|
public static function getFull($id, $pid, $publicOnly = true)
|
||||||
{
|
{
|
||||||
$res = self::get($id, $publicOnly);
|
$res = self::get($id, $publicOnly);
|
||||||
|
if(!$res || !isset($res['account']) || !isset($res['account']['id'])) {
|
||||||
|
return $res;
|
||||||
|
}
|
||||||
$res['relationship'] = RelationshipService::get($pid, $res['account']['id']);
|
$res['relationship'] = RelationshipService::get($pid, $res['account']['id']);
|
||||||
return $res;
|
return $res;
|
||||||
}
|
}
|
||||||
|
|
|
@ -95,7 +95,8 @@ class StoryService
|
||||||
|
|
||||||
public static function delLatest($pid)
|
public static function delLatest($pid)
|
||||||
{
|
{
|
||||||
return Cache::forget(self::STORY_KEY . 'latest:pid-' . $pid);
|
Cache::forget(self::STORY_KEY . 'latest:pid-' . $pid);
|
||||||
|
return Cache::forget('pf:stories:recent-self:' . $pid);
|
||||||
}
|
}
|
||||||
|
|
||||||
public static function addSeen($pid, $sid)
|
public static function addSeen($pid, $sid)
|
||||||
|
|
|
@ -9,6 +9,7 @@ use App\Http\Controllers\StatusController;
|
||||||
use Illuminate\Database\Eloquent\SoftDeletes;
|
use Illuminate\Database\Eloquent\SoftDeletes;
|
||||||
use App\Models\Poll;
|
use App\Models\Poll;
|
||||||
use App\Services\AccountService;
|
use App\Services\AccountService;
|
||||||
|
use App\Models\StatusEdit;
|
||||||
|
|
||||||
class Status extends Model
|
class Status extends Model
|
||||||
{
|
{
|
||||||
|
@ -27,7 +28,8 @@ class Status extends Model
|
||||||
* @var array
|
* @var array
|
||||||
*/
|
*/
|
||||||
protected $casts = [
|
protected $casts = [
|
||||||
'deleted_at' => 'datetime'
|
'deleted_at' => 'datetime',
|
||||||
|
'edited_at' => 'datetime'
|
||||||
];
|
];
|
||||||
|
|
||||||
protected $guarded = [];
|
protected $guarded = [];
|
||||||
|
@ -48,11 +50,11 @@ class Status extends Model
|
||||||
'loop'
|
'loop'
|
||||||
];
|
];
|
||||||
|
|
||||||
const MAX_MENTIONS = 5;
|
const MAX_MENTIONS = 20;
|
||||||
|
|
||||||
const MAX_HASHTAGS = 30;
|
const MAX_HASHTAGS = 60;
|
||||||
|
|
||||||
const MAX_LINKS = 2;
|
const MAX_LINKS = 5;
|
||||||
|
|
||||||
public function profile()
|
public function profile()
|
||||||
{
|
{
|
||||||
|
@ -285,38 +287,6 @@ class Status extends Model
|
||||||
return $obj;
|
return $obj;
|
||||||
}
|
}
|
||||||
|
|
||||||
public function replyToText()
|
|
||||||
{
|
|
||||||
$actorName = $this->profile->username;
|
|
||||||
|
|
||||||
return "{$actorName} ".__('notification.commented');
|
|
||||||
}
|
|
||||||
|
|
||||||
public function replyToHtml()
|
|
||||||
{
|
|
||||||
$actorName = $this->profile->username;
|
|
||||||
$actorUrl = $this->profile->url();
|
|
||||||
|
|
||||||
return "<a href='{$actorUrl}' class='profile-link'>{$actorName}</a> ".
|
|
||||||
__('notification.commented');
|
|
||||||
}
|
|
||||||
|
|
||||||
public function shareToText()
|
|
||||||
{
|
|
||||||
$actorName = $this->profile->username;
|
|
||||||
|
|
||||||
return "{$actorName} ".__('notification.shared');
|
|
||||||
}
|
|
||||||
|
|
||||||
public function shareToHtml()
|
|
||||||
{
|
|
||||||
$actorName = $this->profile->username;
|
|
||||||
$actorUrl = $this->profile->url();
|
|
||||||
|
|
||||||
return "<a href='{$actorUrl}' class='profile-link'>{$actorName}</a> ".
|
|
||||||
__('notification.shared');
|
|
||||||
}
|
|
||||||
|
|
||||||
public function recentComments()
|
public function recentComments()
|
||||||
{
|
{
|
||||||
return $this->comments()->orderBy('created_at', 'desc')->take(3);
|
return $this->comments()->orderBy('created_at', 'desc')->take(3);
|
||||||
|
@ -425,4 +395,9 @@ class Status extends Model
|
||||||
{
|
{
|
||||||
return $this->hasOne(Poll::class);
|
return $this->hasOne(Poll::class);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public function edits()
|
||||||
|
{
|
||||||
|
return $this->hasMany(StatusEdit::class);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
133
app/Transformer/ActivityPub/Verb/UpdateNote.php
Normal file
133
app/Transformer/ActivityPub/Verb/UpdateNote.php
Normal file
|
@ -0,0 +1,133 @@
|
||||||
|
<?php
|
||||||
|
|
||||||
|
namespace App\Transformer\ActivityPub\Verb;
|
||||||
|
|
||||||
|
use App\Status;
|
||||||
|
use League\Fractal;
|
||||||
|
use App\Models\CustomEmoji;
|
||||||
|
use Illuminate\Support\Str;
|
||||||
|
|
||||||
|
class UpdateNote extends Fractal\TransformerAbstract
|
||||||
|
{
|
||||||
|
public function transform(Status $status)
|
||||||
|
{
|
||||||
|
$mentions = $status->mentions->map(function ($mention) {
|
||||||
|
$webfinger = $mention->emailUrl();
|
||||||
|
$name = Str::startsWith($webfinger, '@') ?
|
||||||
|
$webfinger :
|
||||||
|
'@' . $webfinger;
|
||||||
|
return [
|
||||||
|
'type' => 'Mention',
|
||||||
|
'href' => $mention->permalink(),
|
||||||
|
'name' => $name
|
||||||
|
];
|
||||||
|
})->toArray();
|
||||||
|
|
||||||
|
if($status->in_reply_to_id != null) {
|
||||||
|
$parent = $status->parent()->profile;
|
||||||
|
if($parent) {
|
||||||
|
$webfinger = $parent->emailUrl();
|
||||||
|
$name = Str::startsWith($webfinger, '@') ?
|
||||||
|
$webfinger :
|
||||||
|
'@' . $webfinger;
|
||||||
|
$reply = [
|
||||||
|
'type' => 'Mention',
|
||||||
|
'href' => $parent->permalink(),
|
||||||
|
'name' => $name
|
||||||
|
];
|
||||||
|
$mentions = array_merge($reply, $mentions);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
$hashtags = $status->hashtags->map(function ($hashtag) {
|
||||||
|
return [
|
||||||
|
'type' => 'Hashtag',
|
||||||
|
'href' => $hashtag->url(),
|
||||||
|
'name' => "#{$hashtag->name}",
|
||||||
|
];
|
||||||
|
})->toArray();
|
||||||
|
|
||||||
|
$emojis = CustomEmoji::scan($status->caption, true) ?? [];
|
||||||
|
$emoji = array_merge($emojis, $mentions);
|
||||||
|
$tags = array_merge($emoji, $hashtags);
|
||||||
|
|
||||||
|
$latestEdit = $status->edits()->latest()->first();
|
||||||
|
|
||||||
|
return [
|
||||||
|
'@context' => [
|
||||||
|
'https://w3id.org/security/v1',
|
||||||
|
'https://www.w3.org/ns/activitystreams',
|
||||||
|
[
|
||||||
|
'Hashtag' => 'as:Hashtag',
|
||||||
|
'sensitive' => 'as:sensitive',
|
||||||
|
'schema' => 'http://schema.org/',
|
||||||
|
'pixelfed' => 'http://pixelfed.org/ns#',
|
||||||
|
'commentsEnabled' => [
|
||||||
|
'@id' => 'pixelfed:commentsEnabled',
|
||||||
|
'@type' => 'schema:Boolean'
|
||||||
|
],
|
||||||
|
'capabilities' => [
|
||||||
|
'@id' => 'pixelfed:capabilities',
|
||||||
|
'@container' => '@set'
|
||||||
|
],
|
||||||
|
'announce' => [
|
||||||
|
'@id' => 'pixelfed:canAnnounce',
|
||||||
|
'@type' => '@id'
|
||||||
|
],
|
||||||
|
'like' => [
|
||||||
|
'@id' => 'pixelfed:canLike',
|
||||||
|
'@type' => '@id'
|
||||||
|
],
|
||||||
|
'reply' => [
|
||||||
|
'@id' => 'pixelfed:canReply',
|
||||||
|
'@type' => '@id'
|
||||||
|
],
|
||||||
|
'toot' => 'http://joinmastodon.org/ns#',
|
||||||
|
'Emoji' => 'toot:Emoji'
|
||||||
|
]
|
||||||
|
],
|
||||||
|
'id' => $status->permalink('#updates/' . $latestEdit->id),
|
||||||
|
'type' => 'Update',
|
||||||
|
'actor' => $status->profile->permalink(),
|
||||||
|
'published' => $latestEdit->created_at->toAtomString(),
|
||||||
|
'to' => $status->scopeToAudience('to'),
|
||||||
|
'cc' => $status->scopeToAudience('cc'),
|
||||||
|
'object' => [
|
||||||
|
'id' => $status->url(),
|
||||||
|
'type' => 'Note',
|
||||||
|
'summary' => $status->is_nsfw ? $status->cw_summary : null,
|
||||||
|
'content' => $status->rendered ?? $status->caption,
|
||||||
|
'inReplyTo' => $status->in_reply_to_id ? $status->parent()->url() : null,
|
||||||
|
'published' => $status->created_at->toAtomString(),
|
||||||
|
'url' => $status->url(),
|
||||||
|
'attributedTo' => $status->profile->permalink(),
|
||||||
|
'to' => $status->scopeToAudience('to'),
|
||||||
|
'cc' => $status->scopeToAudience('cc'),
|
||||||
|
'sensitive' => (bool) $status->is_nsfw,
|
||||||
|
'attachment' => $status->media()->orderBy('order')->get()->map(function ($media) {
|
||||||
|
return [
|
||||||
|
'type' => $media->activityVerb(),
|
||||||
|
'mediaType' => $media->mime,
|
||||||
|
'url' => $media->url(),
|
||||||
|
'name' => $media->caption,
|
||||||
|
];
|
||||||
|
})->toArray(),
|
||||||
|
'tag' => $tags,
|
||||||
|
'commentsEnabled' => (bool) !$status->comments_disabled,
|
||||||
|
'updated' => $latestEdit->created_at->toAtomString(),
|
||||||
|
'capabilities' => [
|
||||||
|
'announce' => 'https://www.w3.org/ns/activitystreams#Public',
|
||||||
|
'like' => 'https://www.w3.org/ns/activitystreams#Public',
|
||||||
|
'reply' => $status->comments_disabled == true ? '[]' : 'https://www.w3.org/ns/activitystreams#Public'
|
||||||
|
],
|
||||||
|
'location' => $status->place_id ? [
|
||||||
|
'type' => 'Place',
|
||||||
|
'name' => $status->place->name,
|
||||||
|
'longitude' => $status->place->long,
|
||||||
|
'latitude' => $status->place->lat,
|
||||||
|
'country' => $status->place->country
|
||||||
|
] : null,
|
||||||
|
]
|
||||||
|
];
|
||||||
|
}
|
||||||
|
}
|
|
@ -42,7 +42,7 @@ class StatusTransformer extends Fractal\TransformerAbstract
|
||||||
'card' => null,
|
'card' => null,
|
||||||
'poll' => null,
|
'poll' => null,
|
||||||
'media_attachments' => MediaService::get($status->id),
|
'media_attachments' => MediaService::get($status->id),
|
||||||
'account' => ProfileService::get($status->profile_id),
|
'account' => ProfileService::get($status->profile_id, true),
|
||||||
'tags' => StatusHashtagService::statusTags($status->id),
|
'tags' => StatusHashtagService::statusTags($status->id),
|
||||||
];
|
];
|
||||||
}
|
}
|
||||||
|
|
|
@ -23,7 +23,9 @@ class NotificationTransformer extends Fractal\TransformerAbstract
|
||||||
|
|
||||||
if($n->actor_id) {
|
if($n->actor_id) {
|
||||||
$res['account'] = AccountService::get($n->actor_id);
|
$res['account'] = AccountService::get($n->actor_id);
|
||||||
$res['relationship'] = RelationshipService::get($n->actor_id, $n->profile_id);
|
if($n->profile_id != $n->actor_id) {
|
||||||
|
$res['relationship'] = RelationshipService::get($n->actor_id, $n->profile_id);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if($n->item_id && $n->item_type == 'App\Status') {
|
if($n->item_id && $n->item_type == 'App\Status') {
|
||||||
|
@ -66,11 +68,8 @@ class NotificationTransformer extends Fractal\TransformerAbstract
|
||||||
'comment' => 'comment',
|
'comment' => 'comment',
|
||||||
'admin.user.modlog.comment' => 'modlog',
|
'admin.user.modlog.comment' => 'modlog',
|
||||||
'tagged' => 'tagged',
|
'tagged' => 'tagged',
|
||||||
'group:comment' => 'group:comment',
|
|
||||||
'story:react' => 'story:react',
|
'story:react' => 'story:react',
|
||||||
'story:comment' => 'story:comment',
|
'story:comment' => 'story:comment',
|
||||||
'group:join:approved' => 'group:join:approved',
|
|
||||||
'group:join:rejected' => 'group:join:rejected'
|
|
||||||
];
|
];
|
||||||
|
|
||||||
if(!isset($verbs[$verb])) {
|
if(!isset($verbs[$verb])) {
|
||||||
|
|
|
@ -10,6 +10,7 @@ use App\Services\HashidService;
|
||||||
use App\Services\LikeService;
|
use App\Services\LikeService;
|
||||||
use App\Services\MediaService;
|
use App\Services\MediaService;
|
||||||
use App\Services\MediaTagService;
|
use App\Services\MediaTagService;
|
||||||
|
use App\Services\StatusService;
|
||||||
use App\Services\StatusHashtagService;
|
use App\Services\StatusHashtagService;
|
||||||
use App\Services\StatusLabelService;
|
use App\Services\StatusLabelService;
|
||||||
use App\Services\StatusMentionService;
|
use App\Services\StatusMentionService;
|
||||||
|
@ -32,7 +33,7 @@ class StatusStatelessTransformer extends Fractal\TransformerAbstract
|
||||||
'url' => $status->url(),
|
'url' => $status->url(),
|
||||||
'in_reply_to_id' => $status->in_reply_to_id ? (string) $status->in_reply_to_id : null,
|
'in_reply_to_id' => $status->in_reply_to_id ? (string) $status->in_reply_to_id : null,
|
||||||
'in_reply_to_account_id' => $status->in_reply_to_profile_id ? (string) $status->in_reply_to_profile_id : null,
|
'in_reply_to_account_id' => $status->in_reply_to_profile_id ? (string) $status->in_reply_to_profile_id : null,
|
||||||
'reblog' => null,
|
'reblog' => $status->reblog_of_id ? StatusService::get($status->reblog_of_id) : null,
|
||||||
'content' => $status->rendered ?? $status->caption,
|
'content' => $status->rendered ?? $status->caption,
|
||||||
'content_text' => $status->caption,
|
'content_text' => $status->caption,
|
||||||
'created_at' => str_replace('+00:00', 'Z', $status->created_at->format(DATE_RFC3339_EXTENDED)),
|
'created_at' => str_replace('+00:00', 'Z', $status->created_at->format(DATE_RFC3339_EXTENDED)),
|
||||||
|
@ -65,7 +66,8 @@ class StatusStatelessTransformer extends Fractal\TransformerAbstract
|
||||||
'media_attachments' => MediaService::get($status->id),
|
'media_attachments' => MediaService::get($status->id),
|
||||||
'account' => AccountService::get($status->profile_id, true),
|
'account' => AccountService::get($status->profile_id, true),
|
||||||
'tags' => StatusHashtagService::statusTags($status->id),
|
'tags' => StatusHashtagService::statusTags($status->id),
|
||||||
'poll' => $poll
|
'poll' => $poll,
|
||||||
|
'edited_at' => $status->edited_at ? str_replace('+00:00', 'Z', $status->edited_at->format(DATE_RFC3339_EXTENDED)) : null,
|
||||||
];
|
];
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
Some files were not shown because too many files have changed in this diff Show more
Loading…
Reference in a new issue