mirror of
https://github.com/pixelfed/pixelfed.git
synced 2024-11-26 00:03:16 +00:00
Merge pull request #2895 from pixelfed/staging
Archives, Polls and Stories
This commit is contained in:
commit
f593e2b709
157 changed files with 7388 additions and 2269 deletions
27
CHANGELOG.md
27
CHANGELOG.md
|
@ -6,6 +6,11 @@
|
|||
- Auto Following support for admins ([68aa2540](https://github.com/pixelfed/pixelfed/commit/68aa2540))
|
||||
- Mark as spammer mod tool, unlists and applies content warning to existing and future post ([6d956a86](https://github.com/pixelfed/pixelfed/commit/6d956a86))
|
||||
- Diagnostics for error page and admin dashboard ([64725ecc](https://github.com/pixelfed/pixelfed/commit/64725ecc))
|
||||
- Default media licenses and media license sync ([ea0fc90c](https://github.com/pixelfed/pixelfed/commit/ea0fc90c))
|
||||
- Customize media description/alt-text length limit ([072d55d1](https://github.com/pixelfed/pixelfed/commit/072d55d1))
|
||||
- Federate Media Licenses ([14a1367a](https://github.com/pixelfed/pixelfed/commit/14a1367a))
|
||||
- Archive Posts ([e9ef0c88](https://github.com/pixelfed/pixelfed/commit/e9ef0c88))
|
||||
- Polls ([77092200](https://github.com/pixelfed/pixelfed/commit/77092200))
|
||||
|
||||
### Updated
|
||||
- Updated PrettyNumber, fix deprecated warning. ([20ec870b](https://github.com/pixelfed/pixelfed/commit/20ec870b))
|
||||
|
@ -73,6 +78,28 @@
|
|||
- Updated RemotePost.vue, improve text only post UI. ([b0257be2](https://github.com/pixelfed/pixelfed/commit/b0257be2))
|
||||
- Updated Timeline, make text-only posts opt-in by default. ([0153ed6d](https://github.com/pixelfed/pixelfed/commit/0153ed6d))
|
||||
- Updated LikeController, add UndoLikePipeline and federate Undo Like activities. ([8ac8fcad](https://github.com/pixelfed/pixelfed/commit/8ac8fcad))
|
||||
- Updated Settings, add default license and enforced media descriptions. ([67e3f604](https://github.com/pixelfed/pixelfed/commit/67e3f604))
|
||||
- Updated Compose Apis, make media descriptions/alt text length limit configurable. Default length: 1000. ([072d55d1](https://github.com/pixelfed/pixelfed/commit/072d55d1))
|
||||
- Updated ApiV1Controller, add default license support. ([2a791f19](https://github.com/pixelfed/pixelfed/commit/2a791f19))
|
||||
- Updated StatusTransformers, remove includes and use cached services. ([09d5198c](https://github.com/pixelfed/pixelfed/commit/09d5198c))
|
||||
- Updated RemotePost component, update likes reaction bar. ([1060dd23](https://github.com/pixelfed/pixelfed/commit/1060dd23))
|
||||
- Updated FollowPipeline, fix cache invalidation bug. ([c1f14f89](https://github.com/pixelfed/pixelfed/commit/c1f14f89))
|
||||
- Updated PublicApiController, improve accountStatuses api perf. ([bce8edd9](https://github.com/pixelfed/pixelfed/commit/bce8edd9))
|
||||
- Updated ApiControllers, use NotificationService. ([f9516ac3](https://github.com/pixelfed/pixelfed/commit/f9516ac3))
|
||||
- Updated Notification components, fix old notifications with missing attributes. ([b6e226ae](https://github.com/pixelfed/pixelfed/commit/b6e226ae))
|
||||
- Updated LikeController, improve query perf. ([f3d6023e](https://github.com/pixelfed/pixelfed/commit/f3d6023e))
|
||||
- Updated License util, add nameToId method. ([f6131ed7](https://github.com/pixelfed/pixelfed/commit/f6131ed7))
|
||||
- Updated RemoteProfile, add warning about potentially out of date information. ([7274574c](https://github.com/pixelfed/pixelfed/commit/7274574c))
|
||||
- Updated NotifcationCard.vue component, add refresh button for cold notification cache. ([0e178a33](https://github.com/pixelfed/pixelfed/commit/0e178a33))
|
||||
- Updated RemoteProfile component, add follower modals. ([c4146a30](https://github.com/pixelfed/pixelfed/commit/c4146a30))
|
||||
- Updated FollowerService, cache audience. ([22257cc2](https://github.com/pixelfed/pixelfed/commit/22257cc2))
|
||||
- Updated StatusService, add non-public option and improve cache invalidation. ([15c4fdd9](https://github.com/pixelfed/pixelfed/commit/15c4fdd9))
|
||||
- Updated ContactAdmin mail, set New Support Message subject. ([bc3add05](https://github.com/pixelfed/pixelfed/commit/bc3add05))
|
||||
- Updated StatusTransformer, prioritize scope over deprecated visibility attribute. ([6e45021f](https://github.com/pixelfed/pixelfed/commit/6e45021f))
|
||||
- Updated StatusService, invalidate profile embed cache on deletion. ([acaf630d](https://github.com/pixelfed/pixelfed/commit/acaf630d))
|
||||
- Updated status.reply view, fix archived post leakage. ([4fb3d1fa](https://github.com/pixelfed/pixelfed/commit/4fb3d1fa))
|
||||
- Updated PostComponents, re-add time to timestamp. ([c5281dcd](https://github.com/pixelfed/pixelfed/commit/c5281dcd))
|
||||
- Updated follow intent, fix follower count leak. ([03199e2f](https://github.com/pixelfed/pixelfed/commit/03199e2f))
|
||||
- ([](https://github.com/pixelfed/pixelfed/commit/))
|
||||
|
||||
## [v0.11.0 (2021-06-01)](https://github.com/pixelfed/pixelfed/compare/v0.10.10...v0.11.0)
|
||||
|
|
|
@ -4,7 +4,7 @@ namespace App;
|
|||
|
||||
use Illuminate\Support\Str;
|
||||
use Illuminate\Database\Eloquent\Model;
|
||||
use Pixelfed\Snowflake\HasSnowflakePrimary;
|
||||
use App\HasSnowflakePrimary;
|
||||
|
||||
class Collection extends Model
|
||||
{
|
||||
|
|
|
@ -3,7 +3,7 @@
|
|||
namespace App;
|
||||
|
||||
use Illuminate\Database\Eloquent\Model;
|
||||
use Pixelfed\Snowflake\HasSnowflakePrimary;
|
||||
use App\HasSnowflakePrimary;
|
||||
|
||||
class CollectionItem extends Model
|
||||
{
|
||||
|
|
|
@ -40,7 +40,7 @@ class FailedJobGC extends Command
|
|||
{
|
||||
FailedJob::chunk(50, function($jobs) {
|
||||
foreach($jobs as $job) {
|
||||
if($job->failed_at->lt(now()->subMonth())) {
|
||||
if($job->failed_at->lt(now()->subHours(48))) {
|
||||
$job->delete();
|
||||
}
|
||||
}
|
||||
|
|
|
@ -7,6 +7,9 @@ use Illuminate\Support\Facades\DB;
|
|||
use Illuminate\Support\Facades\Storage;
|
||||
use App\Story;
|
||||
use App\StoryView;
|
||||
use App\Jobs\StoryPipeline\StoryExpire;
|
||||
use App\Jobs\StoryPipeline\StoryRotateMedia;
|
||||
use App\Services\StoryService;
|
||||
|
||||
class StoryGC extends Command
|
||||
{
|
||||
|
@ -41,89 +44,41 @@ class StoryGC extends Command
|
|||
*/
|
||||
public function handle()
|
||||
{
|
||||
$this->directoryScan();
|
||||
$this->deleteViews();
|
||||
$this->deleteStories();
|
||||
$this->archiveExpiredStories();
|
||||
$this->rotateMedia();
|
||||
}
|
||||
|
||||
protected function directoryScan()
|
||||
protected function archiveExpiredStories()
|
||||
{
|
||||
$day = now()->day;
|
||||
$stories = Story::whereActive(true)
|
||||
->where('expires_at', '<', now())
|
||||
->get();
|
||||
|
||||
if($day !== 3) {
|
||||
foreach($stories as $story) {
|
||||
StoryExpire::dispatch($story)->onQueue('story');
|
||||
}
|
||||
}
|
||||
|
||||
protected function rotateMedia()
|
||||
{
|
||||
$queue = StoryService::rotateQueue();
|
||||
|
||||
if(!$queue || count($queue) == 0) {
|
||||
return;
|
||||
}
|
||||
|
||||
$monthHash = substr(hash('sha1', date('Y').date('m')), 0, 12);
|
||||
|
||||
$t1 = Storage::directories('public/_esm.t1');
|
||||
$t2 = Storage::directories('public/_esm.t2');
|
||||
|
||||
$dirs = array_merge($t1, $t2);
|
||||
|
||||
foreach($dirs as $dir) {
|
||||
$hash = last(explode('/', $dir));
|
||||
if($hash != $monthHash) {
|
||||
$this->info('Found directory to delete: ' . $dir);
|
||||
$this->deleteDirectory($dir);
|
||||
}
|
||||
}
|
||||
|
||||
$mh = hash('sha256', date('Y').'-.-'.date('m'));
|
||||
$monthHash = date('Y').date('m').substr($mh, 0, 6).substr($mh, 58, 6);
|
||||
$dirs = Storage::directories('public/_esm.t3');
|
||||
|
||||
foreach($dirs as $dir) {
|
||||
$hash = last(explode('/', $dir));
|
||||
if($hash != $monthHash) {
|
||||
$this->info('Found directory to delete: ' . $dir);
|
||||
$this->deleteDirectory($dir);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
protected function deleteDirectory($path)
|
||||
{
|
||||
Storage::deleteDirectory($path);
|
||||
}
|
||||
|
||||
protected function deleteViews()
|
||||
{
|
||||
StoryView::where('created_at', '<', now()->subMinutes(1441))->delete();
|
||||
}
|
||||
|
||||
protected function deleteStories()
|
||||
{
|
||||
$stories = Story::where('created_at', '>', now()->subMinutes(30))
|
||||
->whereNull('active')
|
||||
->get();
|
||||
|
||||
foreach($stories as $story) {
|
||||
if(Storage::exists($story->path) == true) {
|
||||
Storage::delete($story->path);
|
||||
}
|
||||
DB::transaction(function() use($story) {
|
||||
StoryView::whereStoryId($story->id)->delete();
|
||||
$story->delete();
|
||||
collect($queue)
|
||||
->each(function($id) {
|
||||
$story = StoryService::getById($id);
|
||||
if(!$story) {
|
||||
StoryService::removeRotateQueue($id);
|
||||
return;
|
||||
}
|
||||
if($story->created_at->gt(now()->subMinutes(20))) {
|
||||
return;
|
||||
}
|
||||
StoryRotateMedia::dispatch($story)->onQueue('story');
|
||||
StoryService::removeRotateQueue($id);
|
||||
});
|
||||
}
|
||||
|
||||
$stories = Story::where('created_at', '<', now()
|
||||
->subMinutes(1441))
|
||||
->get();
|
||||
|
||||
if($stories->count() == 0) {
|
||||
exit;
|
||||
}
|
||||
|
||||
foreach($stories as $story) {
|
||||
if(Storage::exists($story->path) == true) {
|
||||
Storage::delete($story->path);
|
||||
}
|
||||
DB::transaction(function() use($story) {
|
||||
StoryView::whereStoryId($story->id)->delete();
|
||||
$story->delete();
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
19
app/HasSnowflakePrimary.php
Normal file
19
app/HasSnowflakePrimary.php
Normal file
|
@ -0,0 +1,19 @@
|
|||
<?php
|
||||
|
||||
namespace App;
|
||||
|
||||
use App\Services\SnowflakeService;
|
||||
|
||||
trait HasSnowflakePrimary
|
||||
{
|
||||
public static function bootHasSnowflakePrimary()
|
||||
{
|
||||
static::saving(function ($model) {
|
||||
if (is_null($model->getKey())) {
|
||||
$keyName = $model->getKeyName();
|
||||
$id = SnowflakeService::next();
|
||||
$model->setAttribute($keyName, $id);
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
|
@ -11,6 +11,7 @@ use App\{
|
|||
Profile,
|
||||
Report,
|
||||
Status,
|
||||
Story,
|
||||
User
|
||||
};
|
||||
use DB, Cache;
|
||||
|
@ -27,6 +28,7 @@ use App\Http\Controllers\Admin\{
|
|||
};
|
||||
use Illuminate\Validation\Rule;
|
||||
use App\Services\AdminStatsService;
|
||||
use App\Services\StoryService;
|
||||
|
||||
class AdminController extends Controller
|
||||
{
|
||||
|
@ -465,4 +467,11 @@ class AdminController extends Controller
|
|||
|
||||
return response()->json($res);
|
||||
}
|
||||
|
||||
public function stories(Request $request)
|
||||
{
|
||||
$stories = Story::with('profile')->latest()->paginate(10);
|
||||
$stats = StoryService::adminStats();
|
||||
return view('admin.stories.home', compact('stories', 'stats'));
|
||||
}
|
||||
}
|
||||
|
|
|
@ -1048,7 +1048,7 @@ class ApiV1Controller extends Controller
|
|||
},
|
||||
'filter_name' => 'nullable|string|max:24',
|
||||
'filter_class' => 'nullable|alpha_dash|max:24',
|
||||
'description' => 'nullable|string|max:420'
|
||||
'description' => 'nullable|string|max:' . config_cache('pixelfed.max_altext_length')
|
||||
]);
|
||||
|
||||
$user = $request->user();
|
||||
|
@ -1091,6 +1091,17 @@ class ApiV1Controller extends Controller
|
|||
$storagePath = MediaPathService::get($user, 2);
|
||||
$path = $photo->store($storagePath);
|
||||
$hash = \hash_file('sha256', $photo);
|
||||
$license = null;
|
||||
|
||||
$settings = UserSetting::whereUserId($user->id)->first();
|
||||
|
||||
if($settings && !empty($settings->compose_settings)) {
|
||||
$compose = json_decode($settings->compose_settings, true);
|
||||
|
||||
if(isset($compose['default_license']) && $compose['default_license'] != 1) {
|
||||
$license = $compose['default_license'];
|
||||
}
|
||||
}
|
||||
|
||||
abort_if(MediaBlocklistService::exists($hash) == true, 451);
|
||||
|
||||
|
@ -1105,6 +1116,9 @@ class ApiV1Controller extends Controller
|
|||
$media->caption = $request->input('description');
|
||||
$media->filter_class = $filterClass;
|
||||
$media->filter_name = $filterName;
|
||||
if($license) {
|
||||
$media->license = $license;
|
||||
}
|
||||
$media->save();
|
||||
|
||||
switch ($media->mime) {
|
||||
|
@ -1140,7 +1154,7 @@ class ApiV1Controller extends Controller
|
|||
abort_if(!$request->user(), 403);
|
||||
|
||||
$this->validate($request, [
|
||||
'description' => 'nullable|string|max:420'
|
||||
'description' => 'nullable|string|max:' . config_cache('pixelfed.max_altext_length')
|
||||
]);
|
||||
|
||||
$user = $request->user();
|
||||
|
@ -1302,6 +1316,11 @@ class ApiV1Controller extends Controller
|
|||
}
|
||||
}
|
||||
|
||||
if(empty($res) && !Cache::has('pf:services:notifications:hasSynced:'.$pid)) {
|
||||
Cache::put('pf:services:notifications:hasSynced:'.$pid, 1, 1209600);
|
||||
NotificationService::warmCache($pid, 400, true);
|
||||
}
|
||||
|
||||
$baseUrl = config('app.url') . '/api/v1/notifications?';
|
||||
|
||||
if($minId == $maxId) {
|
||||
|
|
|
@ -15,7 +15,8 @@ use App\{
|
|||
Media,
|
||||
Notification,
|
||||
Profile,
|
||||
Status
|
||||
Status,
|
||||
StatusArchived
|
||||
};
|
||||
use App\Transformer\Api\{
|
||||
AccountTransformer,
|
||||
|
@ -36,9 +37,11 @@ use App\Jobs\VideoPipeline\{
|
|||
VideoPostProcess,
|
||||
VideoThumbnail
|
||||
};
|
||||
use App\Services\AccountService;
|
||||
use App\Services\NotificationService;
|
||||
use App\Services\MediaPathService;
|
||||
use App\Services\MediaBlocklistService;
|
||||
use App\Services\StatusService;
|
||||
|
||||
class BaseApiController extends Controller
|
||||
{
|
||||
|
@ -54,26 +57,40 @@ class BaseApiController extends Controller
|
|||
public function notifications(Request $request)
|
||||
{
|
||||
abort_if(!$request->user(), 403);
|
||||
$pid = $request->user()->profile_id;
|
||||
$pg = $request->input('pg');
|
||||
if($pg == true) {
|
||||
$timeago = Carbon::now()->subMonths(6);
|
||||
$notifications = Notification::whereProfileId($pid)
|
||||
->whereDate('created_at', '>', $timeago)
|
||||
->latest()
|
||||
->simplePaginate(10);
|
||||
$resource = new Fractal\Resource\Collection($notifications, new NotificationTransformer());
|
||||
$res = $this->fractal->createData($resource)->toArray();
|
||||
} else {
|
||||
$this->validate($request, [
|
||||
'page' => 'nullable|integer|min:1|max:10',
|
||||
'limit' => 'nullable|integer|min:1|max:40'
|
||||
]);
|
||||
$limit = $request->input('limit') ?? 10;
|
||||
$page = $request->input('page') ?? 1;
|
||||
$end = (int) $page * $limit;
|
||||
$start = (int) $end - $limit;
|
||||
$res = NotificationService::get($pid, $start, $end);
|
||||
|
||||
$pid = $request->user()->profile_id;
|
||||
$limit = $request->input('limit', 20);
|
||||
|
||||
$since = $request->input('since_id');
|
||||
$min = $request->input('min_id');
|
||||
$max = $request->input('max_id');
|
||||
|
||||
if(!$since && !$min && !$max) {
|
||||
$min = 1;
|
||||
}
|
||||
|
||||
$maxId = null;
|
||||
$minId = null;
|
||||
|
||||
if($max) {
|
||||
$res = NotificationService::getMax($pid, $max, $limit);
|
||||
$ids = NotificationService::getRankedMaxId($pid, $max, $limit);
|
||||
if(!empty($ids)) {
|
||||
$maxId = max($ids);
|
||||
$minId = min($ids);
|
||||
}
|
||||
} else {
|
||||
$res = NotificationService::getMin($pid, $min ?? $since, $limit);
|
||||
$ids = NotificationService::getRankedMinId($pid, $min ?? $since, $limit);
|
||||
if(!empty($ids)) {
|
||||
$maxId = max($ids);
|
||||
$minId = min($ids);
|
||||
}
|
||||
}
|
||||
|
||||
if(empty($res) && !Cache::has('pf:services:notifications:hasSynced:'.$pid)) {
|
||||
Cache::put('pf:services:notifications:hasSynced:'.$pid, 1, 1209600);
|
||||
NotificationService::warmCache($pid, 400, true);
|
||||
}
|
||||
|
||||
return response()->json($res);
|
||||
|
@ -272,4 +289,74 @@ class BaseApiController extends Controller
|
|||
|
||||
return response()->json($res, 200, [], JSON_PRETTY_PRINT|JSON_UNESCAPED_SLASHES);
|
||||
}
|
||||
|
||||
public function archive(Request $request, $id)
|
||||
{
|
||||
abort_if(!$request->user(), 403);
|
||||
|
||||
$status = Status::whereNull('in_reply_to_id')
|
||||
->whereNull('reblog_of_id')
|
||||
->whereProfileId($request->user()->profile_id)
|
||||
->findOrFail($id);
|
||||
|
||||
if($status->scope === 'archived') {
|
||||
return [200];
|
||||
}
|
||||
|
||||
$archive = new StatusArchived;
|
||||
$archive->status_id = $status->id;
|
||||
$archive->profile_id = $status->profile_id;
|
||||
$archive->original_scope = $status->scope;
|
||||
$archive->save();
|
||||
|
||||
$status->scope = 'archived';
|
||||
$status->visibility = 'draft';
|
||||
$status->save();
|
||||
StatusService::del($status->id);
|
||||
AccountService::syncPostCount($status->profile_id);
|
||||
|
||||
return [200];
|
||||
}
|
||||
|
||||
public function unarchive(Request $request, $id)
|
||||
{
|
||||
abort_if(!$request->user(), 403);
|
||||
|
||||
$status = Status::whereNull('in_reply_to_id')
|
||||
->whereNull('reblog_of_id')
|
||||
->whereProfileId($request->user()->profile_id)
|
||||
->findOrFail($id);
|
||||
|
||||
if($status->scope !== 'archived') {
|
||||
return [200];
|
||||
}
|
||||
|
||||
$archive = StatusArchived::whereStatusId($status->id)
|
||||
->whereProfileId($status->profile_id)
|
||||
->firstOrFail();
|
||||
|
||||
$status->scope = $archive->original_scope;
|
||||
$status->visibility = $archive->original_scope;
|
||||
$status->save();
|
||||
$archive->delete();
|
||||
StatusService::del($status->id);
|
||||
AccountService::syncPostCount($status->profile_id);
|
||||
|
||||
return [200];
|
||||
}
|
||||
|
||||
public function archivedPosts(Request $request)
|
||||
{
|
||||
abort_if(!$request->user(), 403);
|
||||
|
||||
$statuses = Status::whereProfileId($request->user()->profile_id)
|
||||
->whereScope('archived')
|
||||
->orderByDesc('id')
|
||||
->simplePaginate(10);
|
||||
|
||||
$fractal = new Fractal\Manager();
|
||||
$fractal->setSerializer(new ArraySerializer());
|
||||
$resource = new Fractal\Resource\Collection($statuses, new StatusStatelessTransformer());
|
||||
return $fractal->createData($resource)->toArray();
|
||||
}
|
||||
}
|
||||
|
|
|
@ -15,8 +15,10 @@ use App\{
|
|||
Profile,
|
||||
Place,
|
||||
Status,
|
||||
UserFilter
|
||||
UserFilter,
|
||||
UserSetting
|
||||
};
|
||||
use App\Models\Poll;
|
||||
use App\Transformer\Api\{
|
||||
MediaTransformer,
|
||||
MediaDraftTransformer,
|
||||
|
@ -41,7 +43,7 @@ use App\Services\MediaPathService;
|
|||
use App\Services\MediaBlocklistService;
|
||||
use App\Services\MediaStorageService;
|
||||
use App\Services\MediaTagService;
|
||||
use App\Services\ServiceService;
|
||||
use App\Services\StatusService;
|
||||
use Illuminate\Support\Str;
|
||||
use App\Util\Lexer\Autolink;
|
||||
use App\Util\Lexer\Extractor;
|
||||
|
@ -403,7 +405,7 @@ class ComposeController extends Controller
|
|||
'media.*.id' => 'required|integer|min:1',
|
||||
'media.*.filter_class' => 'nullable|alpha_dash|max:30',
|
||||
'media.*.license' => 'nullable|string|max:140',
|
||||
'media.*.alt' => 'nullable|string|max:140',
|
||||
'media.*.alt' => 'nullable|string|max:'.config_cache('pixelfed.max_altext_length'),
|
||||
'cw' => 'nullable|boolean',
|
||||
'visibility' => 'required|string|in:public,private,unlisted|min:2|max:10',
|
||||
'place' => 'nullable',
|
||||
|
@ -661,4 +663,73 @@ class ComposeController extends Controller
|
|||
'finished' => $finished
|
||||
];
|
||||
}
|
||||
|
||||
public function composeSettings(Request $request)
|
||||
{
|
||||
$uid = $request->user()->id;
|
||||
$default = [
|
||||
'default_license' => 1,
|
||||
'media_descriptions' => false,
|
||||
'max_altext_length' => config_cache('pixelfed.max_altext_length')
|
||||
];
|
||||
|
||||
return array_merge($default, Cache::remember('profile:compose:settings:' . $uid, now()->addHours(12), function() use($uid) {
|
||||
$res = UserSetting::whereUserId($uid)->first();
|
||||
|
||||
if(!$res || empty($res->compose_settings)) {
|
||||
return [];
|
||||
}
|
||||
|
||||
return json_decode($res->compose_settings, true);
|
||||
}));
|
||||
}
|
||||
|
||||
public function createPoll(Request $request)
|
||||
{
|
||||
$this->validate($request, [
|
||||
'caption' => 'nullable|string|max:'.config('pixelfed.max_caption_length', 500),
|
||||
'cw' => 'nullable|boolean',
|
||||
'visibility' => 'required|string|in:public,private',
|
||||
'comments_disabled' => 'nullable',
|
||||
'expiry' => 'required|in:60,360,1440,10080',
|
||||
'pollOptions' => 'required|array|min:1|max:4'
|
||||
]);
|
||||
|
||||
abort_if(config('instance.polls.enabled') == false, 404, 'Polls not enabled');
|
||||
|
||||
abort_if(Status::whereType('poll')
|
||||
->whereProfileId($request->user()->profile_id)
|
||||
->whereCaption($request->input('caption'))
|
||||
->where('created_at', '>', now()->subDays(2))
|
||||
->exists()
|
||||
, 422, 'Duplicate detected.');
|
||||
|
||||
$status = new Status;
|
||||
$status->profile_id = $request->user()->profile_id;
|
||||
$status->caption = $request->input('caption');
|
||||
$status->rendered = Autolink::create()->autolink($status->caption);
|
||||
$status->visibility = 'draft';
|
||||
$status->scope = 'draft';
|
||||
$status->type = 'poll';
|
||||
$status->local = true;
|
||||
$status->save();
|
||||
|
||||
$poll = new Poll;
|
||||
$poll->status_id = $status->id;
|
||||
$poll->profile_id = $status->profile_id;
|
||||
$poll->poll_options = $request->input('pollOptions');
|
||||
$poll->expires_at = now()->addMinutes($request->input('expiry'));
|
||||
$poll->cached_tallies = collect($poll->poll_options)->map(function($o) {
|
||||
return 0;
|
||||
})->toArray();
|
||||
$poll->save();
|
||||
|
||||
$status->visibility = $request->input('visibility');
|
||||
$status->scope = $request->input('visibility');
|
||||
$status->save();
|
||||
|
||||
NewStatusPipeline::dispatch($status);
|
||||
|
||||
return ['url' => $status->url()];
|
||||
}
|
||||
}
|
||||
|
|
|
@ -29,8 +29,7 @@ class LikeController extends Controller
|
|||
$profile = $user->profile;
|
||||
$status = Status::findOrFail($request->input('item'));
|
||||
|
||||
|
||||
if ($status->likes()->whereProfileId($profile->id)->count() !== 0) {
|
||||
if (Like::whereStatusId($status->id)->whereProfileId($profile->id)->exists()) {
|
||||
$like = Like::whereProfileId($profile->id)->whereStatusId($status->id)->firstOrFail();
|
||||
UnlikePipeline::dispatch($like);
|
||||
} else {
|
||||
|
|
73
app/Http/Controllers/PollController.php
Normal file
73
app/Http/Controllers/PollController.php
Normal file
|
@ -0,0 +1,73 @@
|
|||
<?php
|
||||
|
||||
namespace App\Http\Controllers;
|
||||
|
||||
use Illuminate\Http\Request;
|
||||
use App\Status;
|
||||
use App\Models\Poll;
|
||||
use App\Models\PollVote;
|
||||
use App\Services\PollService;
|
||||
use App\Services\FollowerService;
|
||||
|
||||
class PollController extends Controller
|
||||
{
|
||||
|
||||
public function __construct()
|
||||
{
|
||||
abort_if(!config_cache('instance.polls.enabled'), 404);
|
||||
}
|
||||
|
||||
public function getPoll(Request $request, $id)
|
||||
{
|
||||
$poll = Poll::findOrFail($id);
|
||||
$status = Status::findOrFail($poll->status_id);
|
||||
if($status->scope != 'public') {
|
||||
abort_if(!$request->user(), 403);
|
||||
if($request->user()->profile_id != $status->profile_id) {
|
||||
abort_if(!FollowerService::follows($request->user()->profile_id, $status->profile_id), 404);
|
||||
}
|
||||
}
|
||||
$pid = $request->user() ? $request->user()->profile_id : false;
|
||||
$poll = PollService::getById($id, $pid);
|
||||
return $poll;
|
||||
}
|
||||
|
||||
public function vote(Request $request, $id)
|
||||
{
|
||||
abort_unless($request->user(), 403);
|
||||
|
||||
$this->validate($request, [
|
||||
'choices' => 'required|array'
|
||||
]);
|
||||
|
||||
$pid = $request->user()->profile_id;
|
||||
$poll_id = $id;
|
||||
$choices = $request->input('choices');
|
||||
|
||||
// todo: implement multiple choice
|
||||
$choice = $choices[0];
|
||||
|
||||
$poll = Poll::findOrFail($poll_id);
|
||||
|
||||
abort_if(now()->gt($poll->expires_at), 422, 'Poll expired.');
|
||||
|
||||
abort_if(PollVote::wherePollId($poll_id)->whereProfileId($pid)->exists(), 400, 'Already voted.');
|
||||
|
||||
$vote = new PollVote;
|
||||
$vote->status_id = $poll->status_id;
|
||||
$vote->profile_id = $pid;
|
||||
$vote->poll_id = $poll->id;
|
||||
$vote->choice = $choice;
|
||||
$vote->save();
|
||||
|
||||
$poll->votes_count = $poll->votes_count + 1;
|
||||
$poll->cached_tallies = collect($poll->getTallies())->map(function($tally, $key) use($choice) {
|
||||
return $choice == $key ? $tally + 1 : $tally;
|
||||
})->toArray();
|
||||
$poll->save();
|
||||
|
||||
PollService::del($poll->status_id);
|
||||
$res = PollService::get($poll->status_id, $pid);
|
||||
return $res;
|
||||
}
|
||||
}
|
|
@ -13,6 +13,7 @@ use App\Story;
|
|||
use App\User;
|
||||
use App\UserFilter;
|
||||
use League\Fractal;
|
||||
use App\Services\FollowerService;
|
||||
use App\Util\Lexer\Nickname;
|
||||
use App\Util\Webfinger\Webfinger;
|
||||
use App\Transformer\ActivityPub\ProfileOutbox;
|
||||
|
@ -238,12 +239,12 @@ class ProfileController extends Controller
|
|||
abort_if(!config_cache('instance.stories.enabled') || !$request->user(), 404);
|
||||
$profile = Profile::whereNull('domain')->whereUsername($username)->firstOrFail();
|
||||
$pid = $profile->id;
|
||||
$authed = Auth::user()->profile;
|
||||
abort_if($pid != $authed->id && $profile->followedBy($authed) == false, 404);
|
||||
$authed = Auth::user()->profile_id;
|
||||
abort_if($pid != $authed && !FollowerService::follows($authed, $pid), 404);
|
||||
$exists = Story::whereProfileId($pid)
|
||||
->where('expires_at', '>', now())
|
||||
->count();
|
||||
abort_unless($exists > 0, 404);
|
||||
->whereActive(true)
|
||||
->exists();
|
||||
abort_unless($exists, 404);
|
||||
return view('profile.story', compact('pid', 'profile'));
|
||||
}
|
||||
}
|
||||
|
|
|
@ -29,6 +29,7 @@ use App\Services\{
|
|||
AccountService,
|
||||
LikeService,
|
||||
PublicTimelineService,
|
||||
ProfileService,
|
||||
StatusService,
|
||||
SnowflakeService,
|
||||
UserFilterService
|
||||
|
@ -92,20 +93,15 @@ class PublicApiController extends Controller
|
|||
$profile = Profile::whereUsername($username)->whereNull('status')->firstOrFail();
|
||||
$status = Status::whereProfileId($profile->id)->findOrFail($postid);
|
||||
$this->scopeCheck($profile, $status);
|
||||
if(!Auth::check()) {
|
||||
$res = Cache::remember('wapi:v1:status:stateless_byid:' . $status->id, now()->addMinutes(30), function() use($status) {
|
||||
$item = new Fractal\Resource\Item($status, new StatusStatelessTransformer());
|
||||
$res = [
|
||||
'status' => $this->fractal->createData($item)->toArray(),
|
||||
];
|
||||
return $res;
|
||||
});
|
||||
return response()->json($res);
|
||||
if(!$request->user()) {
|
||||
$res = ['status' => StatusService::get($status->id)];
|
||||
} else {
|
||||
$item = new Fractal\Resource\Item($status, new StatusStatelessTransformer());
|
||||
$res = [
|
||||
'status' => $this->fractal->createData($item)->toArray(),
|
||||
];
|
||||
}
|
||||
$item = new Fractal\Resource\Item($status, new StatusStatelessTransformer());
|
||||
$res = [
|
||||
'status' => $this->fractal->createData($item)->toArray(),
|
||||
];
|
||||
|
||||
return response()->json($res);
|
||||
}
|
||||
|
||||
|
@ -402,11 +398,22 @@ class PublicApiController extends Controller
|
|||
}
|
||||
|
||||
$filtered = $user ? UserFilterService::filters($user->profile_id) : [];
|
||||
$textOnlyPosts = (bool) Redis::zscore('pf:tl:top', $pid);
|
||||
$textOnlyReplies = (bool) Redis::zscore('pf:tl:replies', $pid);
|
||||
$types = $textOnlyPosts ?
|
||||
['text', 'photo', 'photo:album', 'video', 'video:album', 'photo:video:album'] :
|
||||
['photo', 'photo:album', 'video', 'video:album', 'photo:video:album'];
|
||||
$types = ['photo', 'photo:album', 'video', 'video:album', 'photo:video:album'];
|
||||
|
||||
$textOnlyReplies = false;
|
||||
|
||||
if(config('exp.top')) {
|
||||
$textOnlyPosts = (bool) Redis::zscore('pf:tl:top', $pid);
|
||||
$textOnlyReplies = (bool) Redis::zscore('pf:tl:replies', $pid);
|
||||
|
||||
if($textOnlyPosts) {
|
||||
array_push($types, 'text');
|
||||
}
|
||||
}
|
||||
|
||||
if(config('exp.polls') == true) {
|
||||
array_push($types, 'poll');
|
||||
}
|
||||
|
||||
if($min || $max) {
|
||||
$dir = $min ? '>' : '<';
|
||||
|
@ -432,7 +439,7 @@ class PublicApiController extends Controller
|
|||
'updated_at'
|
||||
)
|
||||
->whereIn('type', $types)
|
||||
->when(!$textOnlyReplies, function($q, $textOnlyReplies) {
|
||||
->when($textOnlyReplies != true, function($q, $textOnlyReplies) {
|
||||
return $q->whereNull('in_reply_to_id');
|
||||
})
|
||||
->with('profile', 'hashtags', 'mentions')
|
||||
|
@ -591,17 +598,27 @@ class PublicApiController extends Controller
|
|||
public function accountFollowers(Request $request, $id)
|
||||
{
|
||||
abort_unless(Auth::check(), 403);
|
||||
$profile = Profile::with('user')->whereNull('status')->whereNull('domain')->findOrFail($id);
|
||||
$profile = Profile::with('user')->whereNull('status')->findOrFail($id);
|
||||
$owner = Auth::id() == $profile->user_id;
|
||||
if(Auth::id() != $profile->user_id && $profile->is_private || !$profile->user->settings->show_profile_followers) {
|
||||
|
||||
if(Auth::id() != $profile->user_id && $profile->is_private) {
|
||||
return response()->json([]);
|
||||
}
|
||||
if(!$profile->domain && !$profile->user->settings->show_profile_followers) {
|
||||
return response()->json([]);
|
||||
}
|
||||
if(!$owner && $request->page > 5) {
|
||||
return [];
|
||||
}
|
||||
$followers = $profile->followers()->orderByDesc('followers.created_at')->paginate(10);
|
||||
$resource = new Fractal\Resource\Collection($followers, new AccountTransformer());
|
||||
$res = $this->fractal->createData($resource)->toArray();
|
||||
|
||||
$res = Follower::select('id', 'profile_id', 'following_id')
|
||||
->whereFollowingId($profile->id)
|
||||
->orderByDesc('id')
|
||||
->simplePaginate(10)
|
||||
->map(function($follower) {
|
||||
return ProfileService::get($follower['profile_id']);
|
||||
})
|
||||
->toArray();
|
||||
|
||||
return response()->json($res);
|
||||
}
|
||||
|
@ -612,7 +629,6 @@ class PublicApiController extends Controller
|
|||
|
||||
$profile = Profile::with('user')
|
||||
->whereNull('status')
|
||||
->whereNull('domain')
|
||||
->findOrFail($id);
|
||||
|
||||
// filter by username
|
||||
|
@ -621,7 +637,10 @@ class PublicApiController extends Controller
|
|||
$filter = ($owner == true) && ($search != null);
|
||||
|
||||
abort_if($owner == false && $profile->is_private == true && !$profile->followedBy(Auth::user()->profile), 404);
|
||||
abort_if($profile->user->settings->show_profile_following == false && $owner == false, 404);
|
||||
|
||||
if(!$profile->domain) {
|
||||
abort_if($profile->user->settings->show_profile_following == false && $owner == false, 404);
|
||||
}
|
||||
|
||||
if(!$owner && $request->page > 5) {
|
||||
return [];
|
||||
|
@ -656,28 +675,27 @@ class PublicApiController extends Controller
|
|||
'limit' => 'nullable|integer|min:1|max:24'
|
||||
]);
|
||||
|
||||
$user = $request->user();
|
||||
$profile = Profile::whereNull('status')->findOrFail($id);
|
||||
|
||||
$limit = $request->limit ?? 9;
|
||||
$max_id = $request->max_id;
|
||||
$min_id = $request->min_id;
|
||||
$scope = $request->only_media == true ?
|
||||
['photo', 'photo:album', 'video', 'video:album'] :
|
||||
['photo', 'photo:album', 'video', 'video:album', 'share', 'reply'];
|
||||
$scope = ['photo', 'photo:album', 'video', 'video:album'];
|
||||
|
||||
if($profile->is_private) {
|
||||
if(!Auth::check()) {
|
||||
if(!$user) {
|
||||
return response()->json([]);
|
||||
}
|
||||
$pid = Auth::user()->profile->id;
|
||||
$pid = $user->profile_id;
|
||||
$following = Cache::remember('profile:following:'.$pid, now()->addMinutes(1440), function() use($pid) {
|
||||
$following = Follower::whereProfileId($pid)->pluck('following_id');
|
||||
return $following->push($pid)->toArray();
|
||||
});
|
||||
$visibility = true == in_array($profile->id, $following) ? ['public', 'unlisted', 'private'] : [];
|
||||
} else {
|
||||
if(Auth::check()) {
|
||||
$pid = Auth::user()->profile->id;
|
||||
if($user) {
|
||||
$pid = $user->profile_id;
|
||||
$following = Cache::remember('profile:following:'.$pid, now()->addMinutes(1440), function() use($pid) {
|
||||
$following = Follower::whereProfileId($pid)->pluck('following_id');
|
||||
return $following->push($pid)->toArray();
|
||||
|
@ -688,84 +706,42 @@ class PublicApiController extends Controller
|
|||
}
|
||||
}
|
||||
|
||||
$tag = in_array('private', $visibility) ? 'private' : 'public';
|
||||
if($min_id == 1 && $limit == 9 && $tag == 'public') {
|
||||
$limit = 9;
|
||||
$scope = ['photo', 'photo:album', 'video', 'video:album'];
|
||||
$key = '_api:statuses:recent_9:'.$profile->id;
|
||||
$res = Cache::remember($key, now()->addHours(24), function() use($profile, $scope, $visibility, $limit) {
|
||||
$dir = '>';
|
||||
$id = 1;
|
||||
$timeline = Status::select(
|
||||
'id',
|
||||
'uri',
|
||||
'caption',
|
||||
'rendered',
|
||||
'profile_id',
|
||||
'type',
|
||||
'in_reply_to_id',
|
||||
'reblog_of_id',
|
||||
'is_nsfw',
|
||||
'likes_count',
|
||||
'reblogs_count',
|
||||
'scope',
|
||||
'visibility',
|
||||
'local',
|
||||
'place_id',
|
||||
'comments_disabled',
|
||||
'cw_summary',
|
||||
'created_at',
|
||||
'updated_at'
|
||||
)->whereProfileId($profile->id)
|
||||
->whereIn('type', $scope)
|
||||
->where('id', $dir, $id)
|
||||
->whereIn('visibility', $visibility)
|
||||
->limit($limit)
|
||||
->orderByDesc('id')
|
||||
->get();
|
||||
|
||||
$resource = new Fractal\Resource\Collection($timeline, new StatusStatelessTransformer());
|
||||
$res = $this->fractal->createData($resource)->toArray();
|
||||
|
||||
return response()->json($res, 200, [], JSON_PRETTY_PRINT|JSON_UNESCAPED_SLASHES);
|
||||
});
|
||||
return $res;
|
||||
}
|
||||
|
||||
$dir = $min_id ? '>' : '<';
|
||||
$id = $min_id ?? $max_id;
|
||||
$timeline = Status::select(
|
||||
$res = Status::select(
|
||||
'id',
|
||||
'uri',
|
||||
'caption',
|
||||
'rendered',
|
||||
'profile_id',
|
||||
'type',
|
||||
'in_reply_to_id',
|
||||
'reblog_of_id',
|
||||
'is_nsfw',
|
||||
'likes_count',
|
||||
'reblogs_count',
|
||||
'scope',
|
||||
'visibility',
|
||||
'local',
|
||||
'place_id',
|
||||
'comments_disabled',
|
||||
'cw_summary',
|
||||
'created_at',
|
||||
'updated_at'
|
||||
)->whereProfileId($profile->id)
|
||||
->whereIn('type', $scope)
|
||||
->where('id', $dir, $id)
|
||||
->whereIn('visibility', $visibility)
|
||||
->limit($limit)
|
||||
->orderByDesc('id')
|
||||
->get();
|
||||
'created_at'
|
||||
)
|
||||
->whereProfileId($profile->id)
|
||||
->whereNull('in_reply_to_id')
|
||||
->whereNull('reblog_of_id')
|
||||
->whereIn('type', $scope)
|
||||
->where('id', $dir, $id)
|
||||
->whereIn('scope', $visibility)
|
||||
->limit($limit)
|
||||
->orderByDesc('id')
|
||||
->get()
|
||||
->map(function($s) use($user) {
|
||||
try {
|
||||
$status = StatusService::get($s->id, false);
|
||||
} catch (\Exception $e) {
|
||||
$status = false;
|
||||
}
|
||||
if($user && $status) {
|
||||
$status['favourited'] = (bool) LikeService::liked($user->profile_id, $s->id);
|
||||
}
|
||||
return $status;
|
||||
})
|
||||
->filter(function($s) {
|
||||
return $s;
|
||||
})
|
||||
->values();
|
||||
|
||||
$resource = new Fractal\Resource\Collection($timeline, new StatusStatelessTransformer());
|
||||
$res = $this->fractal->createData($resource)->toArray();
|
||||
|
||||
return response()->json($res, 200, [], JSON_PRETTY_PRINT|JSON_UNESCAPED_SLASHES);
|
||||
return response()->json($res);
|
||||
}
|
||||
|
||||
}
|
||||
|
|
|
@ -7,6 +7,7 @@ use App\Following;
|
|||
use App\ProfileSponsor;
|
||||
use App\Report;
|
||||
use App\UserFilter;
|
||||
use App\UserSetting;
|
||||
use Auth, Cookie, DB, Cache, Purify;
|
||||
use Illuminate\Support\Facades\Redis;
|
||||
use Carbon\Carbon;
|
||||
|
@ -21,6 +22,7 @@ use App\Http\Controllers\Settings\{
|
|||
SecuritySettings
|
||||
};
|
||||
use App\Jobs\DeletePipeline\DeleteAccountPipeline;
|
||||
use App\Jobs\MediaPipeline\MediaSyncLicensePipeline;
|
||||
|
||||
class SettingsController extends Controller
|
||||
{
|
||||
|
@ -221,7 +223,7 @@ class SettingsController extends Controller
|
|||
$sponsors->sponsors = json_encode($res);
|
||||
$sponsors->save();
|
||||
$sponsors = $res;
|
||||
return redirect(route('settings'))->with('status', 'Sponsor settings successfully updated!');;
|
||||
return redirect(route('settings'))->with('status', 'Sponsor settings successfully updated!');
|
||||
}
|
||||
|
||||
public function timelineSettings(Request $request)
|
||||
|
@ -249,7 +251,69 @@ class SettingsController extends Controller
|
|||
} else {
|
||||
Redis::zrem('pf:tl:replies', $pid);
|
||||
}
|
||||
return redirect(route('settings.timeline'));
|
||||
return redirect(route('settings'))->with('status', 'Timeline settings successfully updated!');;
|
||||
}
|
||||
|
||||
public function mediaSettings(Request $request)
|
||||
{
|
||||
$setting = UserSetting::whereUserId($request->user()->id)->firstOrFail();
|
||||
$compose = $setting->compose_settings ? json_decode($setting->compose_settings, true) : [
|
||||
'default_license' => null,
|
||||
'media_descriptions' => false
|
||||
];
|
||||
return view('settings.media', compact('compose'));
|
||||
}
|
||||
|
||||
public function updateMediaSettings(Request $request)
|
||||
{
|
||||
$this->validate($request, [
|
||||
'default' => 'required|int|min:1|max:16',
|
||||
'sync' => 'nullable',
|
||||
'media_descriptions' => 'nullable'
|
||||
]);
|
||||
|
||||
$license = $request->input('default');
|
||||
$sync = $request->input('sync') == 'on';
|
||||
$media_descriptions = $request->input('media_descriptions') == 'on';
|
||||
$uid = $request->user()->id;
|
||||
|
||||
$setting = UserSetting::whereUserId($uid)->firstOrFail();
|
||||
$compose = json_decode($setting->compose_settings, true);
|
||||
$changed = false;
|
||||
|
||||
if($sync) {
|
||||
$key = 'pf:settings:mls_recently:'.$uid;
|
||||
if(Cache::get($key) == 2) {
|
||||
$msg = 'You can only sync licenses twice per 24 hours. Try again later.';
|
||||
return redirect(route('settings'))
|
||||
->with('error', $msg);
|
||||
}
|
||||
}
|
||||
|
||||
if(!isset($compose['default_license']) || $compose['default_license'] !== $license) {
|
||||
$compose['default_license'] = (int) $license;
|
||||
$changed = true;
|
||||
}
|
||||
|
||||
if(!isset($compose['media_descriptions']) || $compose['media_descriptions'] !== $media_descriptions) {
|
||||
$compose['media_descriptions'] = $media_descriptions;
|
||||
$changed = true;
|
||||
}
|
||||
|
||||
if($changed) {
|
||||
$setting->compose_settings = json_encode($compose);
|
||||
$setting->save();
|
||||
Cache::forget('profile:compose:settings:' . $request->user()->id);
|
||||
}
|
||||
|
||||
if($sync) {
|
||||
$val = Cache::has($key) ? 2 : 1;
|
||||
Cache::put($key, $val, 86400);
|
||||
MediaSyncLicensePipeline::dispatch($uid, $license);
|
||||
return redirect(route('settings'))->with('status', 'Media licenses successfully synced! It may take a few minutes to take effect for every post.');
|
||||
}
|
||||
|
||||
return redirect(route('settings'))->with('status', 'Media settings successfully updated!');
|
||||
}
|
||||
|
||||
}
|
||||
|
|
501
app/Http/Controllers/StoryComposeController.php
Normal file
501
app/Http/Controllers/StoryComposeController.php
Normal file
|
@ -0,0 +1,501 @@
|
|||
<?php
|
||||
|
||||
namespace App\Http\Controllers;
|
||||
|
||||
use Illuminate\Http\Request;
|
||||
use Illuminate\Support\Str;
|
||||
use App\Media;
|
||||
use App\Profile;
|
||||
use App\Report;
|
||||
use App\DirectMessage;
|
||||
use App\Notification;
|
||||
use App\Status;
|
||||
use App\Story;
|
||||
use App\StoryView;
|
||||
use App\Models\Poll;
|
||||
use App\Models\PollVote;
|
||||
use App\Services\ProfileService;
|
||||
use App\Services\StoryService;
|
||||
use Cache, Storage;
|
||||
use Image as Intervention;
|
||||
use App\Services\FollowerService;
|
||||
use App\Services\MediaPathService;
|
||||
use FFMpeg;
|
||||
use FFMpeg\Coordinate\Dimension;
|
||||
use FFMpeg\Format\Video\X264;
|
||||
use App\Jobs\StoryPipeline\StoryReactionDeliver;
|
||||
use App\Jobs\StoryPipeline\StoryReplyDeliver;
|
||||
use App\Jobs\StoryPipeline\StoryFanout;
|
||||
use App\Jobs\StoryPipeline\StoryDelete;
|
||||
use ImageOptimizer;
|
||||
|
||||
class StoryComposeController extends Controller
|
||||
{
|
||||
public function apiV1Add(Request $request)
|
||||
{
|
||||
abort_if(!config_cache('instance.stories.enabled') || !$request->user(), 404);
|
||||
|
||||
$this->validate($request, [
|
||||
'file' => function() {
|
||||
return [
|
||||
'required',
|
||||
'mimes:image/jpeg,image/png,video/mp4',
|
||||
'max:' . config_cache('pixelfed.max_photo_size'),
|
||||
];
|
||||
},
|
||||
]);
|
||||
|
||||
$user = $request->user();
|
||||
|
||||
$count = Story::whereProfileId($user->profile_id)
|
||||
->whereActive(true)
|
||||
->where('expires_at', '>', now())
|
||||
->count();
|
||||
|
||||
if($count >= Story::MAX_PER_DAY) {
|
||||
abort(418, 'You have reached your limit for new Stories today.');
|
||||
}
|
||||
|
||||
$photo = $request->file('file');
|
||||
$path = $this->storePhoto($photo, $user);
|
||||
|
||||
$story = new Story();
|
||||
$story->duration = 3;
|
||||
$story->profile_id = $user->profile_id;
|
||||
$story->type = Str::endsWith($photo->getMimeType(), 'mp4') ? 'video' :'photo';
|
||||
$story->mime = $photo->getMimeType();
|
||||
$story->path = $path;
|
||||
$story->local = true;
|
||||
$story->size = $photo->getSize();
|
||||
$story->bearcap_token = str_random(64);
|
||||
$story->save();
|
||||
|
||||
$url = $story->path;
|
||||
|
||||
$res = [
|
||||
'code' => 200,
|
||||
'msg' => 'Successfully added',
|
||||
'media_id' => (string) $story->id,
|
||||
'media_url' => url(Storage::url($url)) . '?v=' . time(),
|
||||
'media_type' => $story->type
|
||||
];
|
||||
|
||||
if($story->type === 'video') {
|
||||
$video = FFMpeg::open($path);
|
||||
$duration = $video->getDurationInSeconds();
|
||||
$res['media_duration'] = $duration;
|
||||
if($duration > 500) {
|
||||
Storage::delete($story->path);
|
||||
$story->delete();
|
||||
return response()->json([
|
||||
'message' => 'Video duration cannot exceed 60 seconds'
|
||||
], 422);
|
||||
}
|
||||
}
|
||||
|
||||
return $res;
|
||||
}
|
||||
|
||||
protected function storePhoto($photo, $user)
|
||||
{
|
||||
$mimes = explode(',', config_cache('pixelfed.media_types'));
|
||||
if(in_array($photo->getMimeType(), [
|
||||
'image/jpeg',
|
||||
'image/png',
|
||||
'video/mp4'
|
||||
]) == false) {
|
||||
abort(400, 'Invalid media type');
|
||||
return;
|
||||
}
|
||||
|
||||
$storagePath = MediaPathService::story($user->profile);
|
||||
$path = $photo->storeAs($storagePath, Str::random(random_int(2, 12)) . '_' . Str::random(random_int(32, 35)) . '_' . Str::random(random_int(1, 14)) . '.' . $photo->extension());
|
||||
if(in_array($photo->getMimeType(), ['image/jpeg','image/png'])) {
|
||||
$fpath = storage_path('app/' . $path);
|
||||
$img = Intervention::make($fpath);
|
||||
$img->orientate();
|
||||
$img->save($fpath, config_cache('pixelfed.image_quality'));
|
||||
$img->destroy();
|
||||
}
|
||||
return $path;
|
||||
}
|
||||
|
||||
public function cropPhoto(Request $request)
|
||||
{
|
||||
abort_if(!config_cache('instance.stories.enabled') || !$request->user(), 404);
|
||||
|
||||
$this->validate($request, [
|
||||
'media_id' => 'required|integer|min:1',
|
||||
'width' => 'required',
|
||||
'height' => 'required',
|
||||
'x' => 'required',
|
||||
'y' => 'required'
|
||||
]);
|
||||
|
||||
$user = $request->user();
|
||||
$id = $request->input('media_id');
|
||||
$width = round($request->input('width'));
|
||||
$height = round($request->input('height'));
|
||||
$x = round($request->input('x'));
|
||||
$y = round($request->input('y'));
|
||||
|
||||
$story = Story::whereProfileId($user->profile_id)->findOrFail($id);
|
||||
|
||||
$path = storage_path('app/' . $story->path);
|
||||
|
||||
if(!is_file($path)) {
|
||||
abort(400, 'Invalid or missing media.');
|
||||
}
|
||||
|
||||
if($story->type === 'photo') {
|
||||
$img = Intervention::make($path);
|
||||
$img->crop($width, $height, $x, $y);
|
||||
$img->resize(1080, 1920, function ($constraint) {
|
||||
$constraint->aspectRatio();
|
||||
});
|
||||
$img->save($path, config_cache('pixelfed.image_quality'));
|
||||
}
|
||||
|
||||
return [
|
||||
'code' => 200,
|
||||
'msg' => 'Successfully cropped',
|
||||
];
|
||||
}
|
||||
|
||||
public function publishStory(Request $request)
|
||||
{
|
||||
abort_if(!config_cache('instance.stories.enabled') || !$request->user(), 404);
|
||||
|
||||
$this->validate($request, [
|
||||
'media_id' => 'required',
|
||||
'duration' => 'required|integer|min:3|max:120',
|
||||
'can_reply' => 'required|boolean',
|
||||
'can_react' => 'required|boolean'
|
||||
]);
|
||||
|
||||
$id = $request->input('media_id');
|
||||
$user = $request->user();
|
||||
$story = Story::whereProfileId($user->profile_id)
|
||||
->findOrFail($id);
|
||||
|
||||
$story->active = true;
|
||||
$story->expires_at = now()->addMinutes(1440);
|
||||
$story->duration = $request->input('duration', 10);
|
||||
$story->can_reply = $request->input('can_reply');
|
||||
$story->can_react = $request->input('can_react');
|
||||
$story->save();
|
||||
|
||||
StoryService::delLatest($story->profile_id);
|
||||
StoryFanout::dispatch($story)->onQueue('story');
|
||||
StoryService::addRotateQueue($story->id);
|
||||
|
||||
return [
|
||||
'code' => 200,
|
||||
'msg' => 'Successfully published',
|
||||
];
|
||||
}
|
||||
|
||||
public function apiV1Delete(Request $request, $id)
|
||||
{
|
||||
abort_if(!config_cache('instance.stories.enabled') || !$request->user(), 404);
|
||||
|
||||
$user = $request->user();
|
||||
|
||||
$story = Story::whereProfileId($user->profile_id)
|
||||
->findOrFail($id);
|
||||
$story->active = false;
|
||||
$story->save();
|
||||
|
||||
StoryDelete::dispatch($story)->onQueue('story');
|
||||
|
||||
return [
|
||||
'code' => 200,
|
||||
'msg' => 'Successfully deleted'
|
||||
];
|
||||
}
|
||||
|
||||
public function compose(Request $request)
|
||||
{
|
||||
abort_if(!config_cache('instance.stories.enabled') || !$request->user(), 404);
|
||||
|
||||
return view('stories.compose');
|
||||
}
|
||||
|
||||
public function createPoll(Request $request)
|
||||
{
|
||||
abort_if(!config_cache('instance.stories.enabled') || !$request->user(), 404);
|
||||
abort_if(!config_cache('instance.polls.enabled'), 404);
|
||||
|
||||
return $request->all();
|
||||
}
|
||||
|
||||
public function publishStoryPoll(Request $request)
|
||||
{
|
||||
abort_if(!config_cache('instance.stories.enabled') || !$request->user(), 404);
|
||||
|
||||
$this->validate($request, [
|
||||
'question' => 'required|string|min:6|max:140',
|
||||
'options' => 'required|array|min:2|max:4',
|
||||
'can_reply' => 'required|boolean',
|
||||
'can_react' => 'required|boolean'
|
||||
]);
|
||||
|
||||
$pid = $request->user()->profile_id;
|
||||
|
||||
$count = Story::whereProfileId($pid)
|
||||
->whereActive(true)
|
||||
->where('expires_at', '>', now())
|
||||
->count();
|
||||
|
||||
if($count >= Story::MAX_PER_DAY) {
|
||||
abort(418, 'You have reached your limit for new Stories today.');
|
||||
}
|
||||
|
||||
$story = new Story;
|
||||
$story->type = 'poll';
|
||||
$story->story = json_encode([
|
||||
'question' => $request->input('question'),
|
||||
'options' => $request->input('options')
|
||||
]);
|
||||
$story->public = false;
|
||||
$story->local = true;
|
||||
$story->profile_id = $pid;
|
||||
$story->expires_at = now()->addMinutes(1440);
|
||||
$story->duration = 30;
|
||||
$story->can_reply = false;
|
||||
$story->can_react = false;
|
||||
$story->save();
|
||||
|
||||
$poll = new Poll;
|
||||
$poll->story_id = $story->id;
|
||||
$poll->profile_id = $pid;
|
||||
$poll->poll_options = $request->input('options');
|
||||
$poll->expires_at = $story->expires_at;
|
||||
$poll->cached_tallies = collect($poll->poll_options)->map(function($o) {
|
||||
return 0;
|
||||
})->toArray();
|
||||
$poll->save();
|
||||
|
||||
$story->active = true;
|
||||
$story->save();
|
||||
|
||||
StoryService::delLatest($story->profile_id);
|
||||
|
||||
return [
|
||||
'code' => 200,
|
||||
'msg' => 'Successfully published',
|
||||
];
|
||||
}
|
||||
|
||||
public function storyPollVote(Request $request)
|
||||
{
|
||||
abort_if(!config_cache('instance.stories.enabled') || !$request->user(), 404);
|
||||
|
||||
$this->validate($request, [
|
||||
'sid' => 'required',
|
||||
'ci' => 'required|integer|min:0|max:3'
|
||||
]);
|
||||
|
||||
$pid = $request->user()->profile_id;
|
||||
$ci = $request->input('ci');
|
||||
$story = Story::findOrFail($request->input('sid'));
|
||||
abort_if(!FollowerService::follows($pid, $story->profile_id), 403);
|
||||
$poll = Poll::whereStoryId($story->id)->firstOrFail();
|
||||
|
||||
$vote = new PollVote;
|
||||
$vote->profile_id = $pid;
|
||||
$vote->poll_id = $poll->id;
|
||||
$vote->story_id = $story->id;
|
||||
$vote->status_id = null;
|
||||
$vote->choice = $ci;
|
||||
$vote->save();
|
||||
|
||||
$poll->votes_count = $poll->votes_count + 1;
|
||||
$poll->cached_tallies = collect($poll->getTallies())->map(function($tally, $key) use($ci) {
|
||||
return $ci == $key ? $tally + 1 : $tally;
|
||||
})->toArray();
|
||||
$poll->save();
|
||||
|
||||
return 200;
|
||||
}
|
||||
|
||||
public function storeReport(Request $request)
|
||||
{
|
||||
abort_if(!config_cache('instance.stories.enabled') || !$request->user(), 404);
|
||||
|
||||
$this->validate($request, [
|
||||
'type' => 'required|alpha_dash',
|
||||
'id' => 'required|integer|min:1',
|
||||
]);
|
||||
|
||||
$pid = $request->user()->profile_id;
|
||||
$sid = $request->input('id');
|
||||
$type = $request->input('type');
|
||||
|
||||
$types = [
|
||||
// original 3
|
||||
'spam',
|
||||
'sensitive',
|
||||
'abusive',
|
||||
|
||||
// new
|
||||
'underage',
|
||||
'copyright',
|
||||
'impersonation',
|
||||
'scam',
|
||||
'terrorism'
|
||||
];
|
||||
|
||||
abort_if(!in_array($type, $types), 422, 'Invalid story report type');
|
||||
|
||||
$story = Story::findOrFail($sid);
|
||||
|
||||
abort_if($story->profile_id == $pid, 422, 'Cannot report your own story');
|
||||
abort_if(!FollowerService::follows($pid, $story->profile_id), 422, 'Cannot report a story from an account you do not follow');
|
||||
|
||||
if( Report::whereProfileId($pid)
|
||||
->whereObjectType('App\Story')
|
||||
->whereObjectId($story->id)
|
||||
->exists()
|
||||
) {
|
||||
return response()->json(['error' => [
|
||||
'code' => 409,
|
||||
'message' => 'Cannot report the same story again'
|
||||
]], 409);
|
||||
}
|
||||
|
||||
$report = new Report;
|
||||
$report->profile_id = $pid;
|
||||
$report->user_id = $request->user()->id;
|
||||
$report->object_id = $story->id;
|
||||
$report->object_type = 'App\Story';
|
||||
$report->reported_profile_id = $story->profile_id;
|
||||
$report->type = $type;
|
||||
$report->message = null;
|
||||
$report->save();
|
||||
|
||||
return [200];
|
||||
}
|
||||
|
||||
public function react(Request $request)
|
||||
{
|
||||
abort_if(!config_cache('instance.stories.enabled') || !$request->user(), 404);
|
||||
$this->validate($request, [
|
||||
'sid' => 'required',
|
||||
'reaction' => 'required|string'
|
||||
]);
|
||||
$pid = $request->user()->profile_id;
|
||||
$text = $request->input('reaction');
|
||||
|
||||
$story = Story::findOrFail($request->input('sid'));
|
||||
|
||||
abort_if(!$story->can_react, 422);
|
||||
abort_if(StoryService::reactCounter($story->id, $pid) >= 5, 422, 'You have already reacted to this story');
|
||||
|
||||
$status = new Status;
|
||||
$status->profile_id = $pid;
|
||||
$status->type = 'story:reaction';
|
||||
$status->caption = $text;
|
||||
$status->rendered = $text;
|
||||
$status->scope = 'direct';
|
||||
$status->visibility = 'direct';
|
||||
$status->in_reply_to_profile_id = $story->profile_id;
|
||||
$status->entities = json_encode([
|
||||
'story_id' => $story->id,
|
||||
'reaction' => $text
|
||||
]);
|
||||
$status->save();
|
||||
|
||||
$dm = new DirectMessage;
|
||||
$dm->to_id = $story->profile_id;
|
||||
$dm->from_id = $pid;
|
||||
$dm->type = 'story:react';
|
||||
$dm->status_id = $status->id;
|
||||
$dm->meta = json_encode([
|
||||
'story_username' => $story->profile->username,
|
||||
'story_actor_username' => $request->user()->username,
|
||||
'story_id' => $story->id,
|
||||
'story_media_url' => url(Storage::url($story->path)),
|
||||
'reaction' => $text
|
||||
]);
|
||||
$dm->save();
|
||||
|
||||
if($story->local) {
|
||||
// generate notification
|
||||
$n = new Notification;
|
||||
$n->profile_id = $dm->to_id;
|
||||
$n->actor_id = $dm->from_id;
|
||||
$n->item_id = $dm->id;
|
||||
$n->item_type = 'App\DirectMessage';
|
||||
$n->action = 'story:react';
|
||||
$n->message = "{$request->user()->username} reacted to your story";
|
||||
$n->rendered = "{$request->user()->username} reacted to your story";
|
||||
$n->save();
|
||||
} else {
|
||||
StoryReactionDeliver::dispatch($story, $status)->onQueue('story');
|
||||
}
|
||||
|
||||
StoryService::reactIncrement($story->id, $pid);
|
||||
|
||||
return 200;
|
||||
}
|
||||
|
||||
public function comment(Request $request)
|
||||
{
|
||||
abort_if(!config_cache('instance.stories.enabled') || !$request->user(), 404);
|
||||
$this->validate($request, [
|
||||
'sid' => 'required',
|
||||
'caption' => 'required|string'
|
||||
]);
|
||||
$pid = $request->user()->profile_id;
|
||||
$text = $request->input('caption');
|
||||
|
||||
$story = Story::findOrFail($request->input('sid'));
|
||||
|
||||
abort_if(!$story->can_reply, 422);
|
||||
|
||||
$status = new Status;
|
||||
$status->type = 'story:reply';
|
||||
$status->profile_id = $pid;
|
||||
$status->caption = $text;
|
||||
$status->rendered = $text;
|
||||
$status->scope = 'direct';
|
||||
$status->visibility = 'direct';
|
||||
$status->in_reply_to_profile_id = $story->profile_id;
|
||||
$status->entities = json_encode([
|
||||
'story_id' => $story->id
|
||||
]);
|
||||
$status->save();
|
||||
|
||||
$dm = new DirectMessage;
|
||||
$dm->to_id = $story->profile_id;
|
||||
$dm->from_id = $pid;
|
||||
$dm->type = 'story:comment';
|
||||
$dm->status_id = $status->id;
|
||||
$dm->meta = json_encode([
|
||||
'story_username' => $story->profile->username,
|
||||
'story_actor_username' => $request->user()->username,
|
||||
'story_id' => $story->id,
|
||||
'story_media_url' => url(Storage::url($story->path)),
|
||||
'caption' => $text
|
||||
]);
|
||||
$dm->save();
|
||||
|
||||
if($story->local) {
|
||||
// generate notification
|
||||
$n = new Notification;
|
||||
$n->profile_id = $dm->to_id;
|
||||
$n->actor_id = $dm->from_id;
|
||||
$n->item_id = $dm->id;
|
||||
$n->item_type = 'App\DirectMessage';
|
||||
$n->action = 'story:comment';
|
||||
$n->message = "{$request->user()->username} commented on story";
|
||||
$n->rendered = "{$request->user()->username} commented on story";
|
||||
$n->save();
|
||||
} else {
|
||||
StoryReplyDeliver::dispatch($story, $status)->onQueue('story');
|
||||
}
|
||||
|
||||
return 200;
|
||||
}
|
||||
}
|
|
@ -4,337 +4,106 @@ namespace App\Http\Controllers;
|
|||
|
||||
use Illuminate\Http\Request;
|
||||
use Illuminate\Support\Str;
|
||||
use App\DirectMessage;
|
||||
use App\Follower;
|
||||
use App\Notification;
|
||||
use App\Media;
|
||||
use App\Profile;
|
||||
use App\Status;
|
||||
use App\Story;
|
||||
use App\StoryView;
|
||||
use App\Services\PollService;
|
||||
use App\Services\ProfileService;
|
||||
use App\Services\StoryService;
|
||||
use Cache, Storage;
|
||||
use Image as Intervention;
|
||||
use App\Services\AccountService;
|
||||
use App\Services\FollowerService;
|
||||
use App\Services\MediaPathService;
|
||||
use FFMpeg;
|
||||
use FFMpeg\Coordinate\Dimension;
|
||||
use FFMpeg\Format\Video\X264;
|
||||
use League\Fractal\Manager;
|
||||
use League\Fractal\Serializer\ArraySerializer;
|
||||
use League\Fractal\Resource\Item;
|
||||
use App\Transformer\ActivityPub\Verb\StoryVerb;
|
||||
use App\Jobs\StoryPipeline\StoryViewDeliver;
|
||||
|
||||
class StoryController extends Controller
|
||||
class StoryController extends StoryComposeController
|
||||
{
|
||||
public function apiV1Add(Request $request)
|
||||
public function recent(Request $request)
|
||||
{
|
||||
abort_if(!config_cache('instance.stories.enabled') || !$request->user(), 404);
|
||||
$pid = $request->user()->profile_id;
|
||||
|
||||
$this->validate($request, [
|
||||
'file' => function() {
|
||||
return [
|
||||
'required',
|
||||
'mimes:image/jpeg,image/png,video/mp4',
|
||||
'max:' . config_cache('pixelfed.max_photo_size'),
|
||||
];
|
||||
},
|
||||
]);
|
||||
|
||||
$user = $request->user();
|
||||
|
||||
if(Story::whereProfileId($user->profile_id)->where('expires_at', '>', now())->count() >= Story::MAX_PER_DAY) {
|
||||
abort(400, 'You have reached your limit for new Stories today.');
|
||||
}
|
||||
|
||||
$photo = $request->file('file');
|
||||
$path = $this->storePhoto($photo, $user);
|
||||
|
||||
$story = new Story();
|
||||
$story->duration = 3;
|
||||
$story->profile_id = $user->profile_id;
|
||||
$story->type = Str::endsWith($photo->getMimeType(), 'mp4') ? 'video' :'photo';
|
||||
$story->mime = $photo->getMimeType();
|
||||
$story->path = $path;
|
||||
$story->local = true;
|
||||
$story->size = $photo->getSize();
|
||||
$story->save();
|
||||
|
||||
$url = $story->path;
|
||||
|
||||
if($story->type === 'video') {
|
||||
$video = FFMpeg::open($path);
|
||||
$width = $video->getVideoStream()->get('width');
|
||||
$height = $video->getVideoStream()->get('height');
|
||||
|
||||
|
||||
if($width !== 1080 || $height !== 1920) {
|
||||
Storage::delete($story->path);
|
||||
$story->delete();
|
||||
abort(422, 'Invalid video dimensions, must be 1080x1920');
|
||||
}
|
||||
}
|
||||
|
||||
return [
|
||||
'code' => 200,
|
||||
'msg' => 'Successfully added',
|
||||
'media_id' => (string) $story->id,
|
||||
'media_url' => url(Storage::url($url)) . '?v=' . time(),
|
||||
'media_type' => $story->type
|
||||
];
|
||||
}
|
||||
|
||||
protected function storePhoto($photo, $user)
|
||||
{
|
||||
$mimes = explode(',', config_cache('pixelfed.media_types'));
|
||||
if(in_array($photo->getMimeType(), [
|
||||
'image/jpeg',
|
||||
'image/png',
|
||||
'video/mp4'
|
||||
]) == false) {
|
||||
abort(400, 'Invalid media type');
|
||||
return;
|
||||
}
|
||||
|
||||
$storagePath = MediaPathService::story($user->profile);
|
||||
$path = $photo->store($storagePath);
|
||||
if(in_array($photo->getMimeType(), ['image/jpeg','image/png'])) {
|
||||
$fpath = storage_path('app/' . $path);
|
||||
$img = Intervention::make($fpath);
|
||||
$img->orientate();
|
||||
$img->save($fpath, config_cache('pixelfed.image_quality'));
|
||||
$img->destroy();
|
||||
}
|
||||
return $path;
|
||||
}
|
||||
|
||||
public function cropPhoto(Request $request)
|
||||
{
|
||||
abort_if(!config_cache('instance.stories.enabled') || !$request->user(), 404);
|
||||
|
||||
$this->validate($request, [
|
||||
'media_id' => 'required|integer|min:1',
|
||||
'width' => 'required',
|
||||
'height' => 'required',
|
||||
'x' => 'required',
|
||||
'y' => 'required'
|
||||
]);
|
||||
|
||||
$user = $request->user();
|
||||
$id = $request->input('media_id');
|
||||
$width = round($request->input('width'));
|
||||
$height = round($request->input('height'));
|
||||
$x = round($request->input('x'));
|
||||
$y = round($request->input('y'));
|
||||
|
||||
$story = Story::whereProfileId($user->profile_id)->findOrFail($id);
|
||||
|
||||
$path = storage_path('app/' . $story->path);
|
||||
|
||||
if(!is_file($path)) {
|
||||
abort(400, 'Invalid or missing media.');
|
||||
}
|
||||
|
||||
if($story->type === 'photo') {
|
||||
$img = Intervention::make($path);
|
||||
$img->crop($width, $height, $x, $y);
|
||||
$img->resize(1080, 1920, function ($constraint) {
|
||||
$constraint->aspectRatio();
|
||||
});
|
||||
$img->save($path, config_cache('pixelfed.image_quality'));
|
||||
}
|
||||
|
||||
return [
|
||||
'code' => 200,
|
||||
'msg' => 'Successfully cropped',
|
||||
];
|
||||
}
|
||||
|
||||
public function publishStory(Request $request)
|
||||
{
|
||||
abort_if(!config_cache('instance.stories.enabled') || !$request->user(), 404);
|
||||
|
||||
$this->validate($request, [
|
||||
'media_id' => 'required',
|
||||
'duration' => 'required|integer|min:3|max:10'
|
||||
]);
|
||||
|
||||
$id = $request->input('media_id');
|
||||
$user = $request->user();
|
||||
$story = Story::whereProfileId($user->profile_id)
|
||||
->findOrFail($id);
|
||||
|
||||
$story->active = true;
|
||||
$story->duration = $request->input('duration', 10);
|
||||
$story->expires_at = now()->addMinutes(1450);
|
||||
$story->save();
|
||||
|
||||
return [
|
||||
'code' => 200,
|
||||
'msg' => 'Successfully published',
|
||||
];
|
||||
}
|
||||
|
||||
public function apiV1Delete(Request $request, $id)
|
||||
{
|
||||
abort_if(!config_cache('instance.stories.enabled') || !$request->user(), 404);
|
||||
|
||||
$user = $request->user();
|
||||
|
||||
$story = Story::whereProfileId($user->profile_id)
|
||||
->findOrFail($id);
|
||||
|
||||
if(Storage::exists($story->path) == true) {
|
||||
Storage::delete($story->path);
|
||||
}
|
||||
|
||||
$story->delete();
|
||||
|
||||
return [
|
||||
'code' => 200,
|
||||
'msg' => 'Successfully deleted'
|
||||
];
|
||||
}
|
||||
|
||||
public function apiV1Recent(Request $request)
|
||||
{
|
||||
abort_if(!config_cache('instance.stories.enabled') || !$request->user(), 404);
|
||||
|
||||
$profile = $request->user()->profile;
|
||||
$following = $profile->following->pluck('id')->toArray();
|
||||
|
||||
if(config('database.default') == 'pgsql') {
|
||||
$db = Story::with('profile')
|
||||
->whereActive(true)
|
||||
->whereIn('profile_id', $following)
|
||||
->where('expires_at', '>', now())
|
||||
->distinct('profile_id')
|
||||
->take(9)
|
||||
$s = 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();
|
||||
} else {
|
||||
$db = Story::with('profile')
|
||||
->whereActive(true)
|
||||
->whereIn('profile_id', $following)
|
||||
->where('created_at', '>', now()->subDay())
|
||||
->orderByDesc('expires_at')
|
||||
->groupBy('profile_id')
|
||||
->take(9)
|
||||
->get();
|
||||
}
|
||||
|
||||
$stories = $db->map(function($s, $k) {
|
||||
$res = $s->map(function($s) use($pid) {
|
||||
$profile = AccountService::get($s->profile_id);
|
||||
$url = $profile['local'] ? url("/stories/{$profile['username']}") :
|
||||
url("/i/rs/{$profile['id']}");
|
||||
return [
|
||||
'id' => (string) $s->id,
|
||||
'photo' => $s->profile->avatarUrl(),
|
||||
'name' => $s->profile->username,
|
||||
'link' => $s->profile->url(),
|
||||
'lastUpdated' => (int) $s->created_at->format('U'),
|
||||
'seen' => $s->seen(),
|
||||
'items' => [],
|
||||
'pid' => (string) $s->profile->id
|
||||
'pid' => $profile['id'],
|
||||
'avatar' => $profile['avatar'],
|
||||
'local' => $profile['local'],
|
||||
'username' => $profile['acct'],
|
||||
'url' => $url,
|
||||
'seen' => StoryService::hasSeen($pid, StoryService::latest($s->profile_id)),
|
||||
'sid' => $s->id
|
||||
];
|
||||
});
|
||||
|
||||
return response()->json($stories, 200, [], JSON_PRETTY_PRINT|JSON_UNESCAPED_SLASHES);
|
||||
}
|
||||
|
||||
public function apiV1Fetch(Request $request, $id)
|
||||
{
|
||||
abort_if(!config_cache('instance.stories.enabled') || !$request->user(), 404);
|
||||
|
||||
$authed = $request->user()->profile;
|
||||
$profile = Profile::findOrFail($id);
|
||||
if($id == $authed->id) {
|
||||
$publicOnly = true;
|
||||
} else {
|
||||
$publicOnly = (bool) $profile->followedBy($authed);
|
||||
}
|
||||
|
||||
$stories = Story::whereProfileId($profile->id)
|
||||
->whereActive(true)
|
||||
->orderBy('expires_at', 'desc')
|
||||
->where('expires_at', '>', now())
|
||||
->when(!$publicOnly, function($query, $publicOnly) {
|
||||
return $query->wherePublic(true);
|
||||
})
|
||||
->get()
|
||||
->map(function($s, $k) {
|
||||
return [
|
||||
'id' => (string) $s->id,
|
||||
'type' => Str::endsWith($s->path, '.mp4') ? 'video' :'photo',
|
||||
'length' => 3,
|
||||
'src' => url(Storage::url($s->path)),
|
||||
'preview' => null,
|
||||
'link' => null,
|
||||
'linkText' => null,
|
||||
'time' => $s->created_at->format('U'),
|
||||
'expires_at' => (int) $s->expires_at->format('U'),
|
||||
'created_ago' => $s->created_at->diffForHumans(null, true, true),
|
||||
'seen' => $s->seen()
|
||||
];
|
||||
})->toArray();
|
||||
return response()->json($stories, 200, [], JSON_PRETTY_PRINT|JSON_UNESCAPED_SLASHES);
|
||||
}
|
||||
|
||||
public function apiV1Item(Request $request, $id)
|
||||
{
|
||||
abort_if(!config_cache('instance.stories.enabled') || !$request->user(), 404);
|
||||
|
||||
$authed = $request->user()->profile;
|
||||
$story = Story::with('profile')
|
||||
->whereActive(true)
|
||||
->where('expires_at', '>', now())
|
||||
->findOrFail($id);
|
||||
|
||||
$profile = $story->profile;
|
||||
if($story->profile_id == $authed->id) {
|
||||
$publicOnly = true;
|
||||
} else {
|
||||
$publicOnly = (bool) $profile->followedBy($authed);
|
||||
}
|
||||
|
||||
abort_if(!$publicOnly, 403);
|
||||
|
||||
$res = [
|
||||
'id' => (string) $story->id,
|
||||
'type' => Str::endsWith($story->path, '.mp4') ? 'video' :'photo',
|
||||
'length' => 10,
|
||||
'src' => url(Storage::url($story->path)),
|
||||
'preview' => null,
|
||||
'link' => null,
|
||||
'linkText' => null,
|
||||
'time' => $story->created_at->format('U'),
|
||||
'expires_at' => (int) $story->expires_at->format('U'),
|
||||
'seen' => $story->seen()
|
||||
];
|
||||
->sortBy('seen')
|
||||
->values();
|
||||
return response()->json($res, 200, [], JSON_PRETTY_PRINT|JSON_UNESCAPED_SLASHES);
|
||||
}
|
||||
|
||||
public function apiV1Profile(Request $request, $id)
|
||||
public function profile(Request $request, $id)
|
||||
{
|
||||
abort_if(!config_cache('instance.stories.enabled') || !$request->user(), 404);
|
||||
|
||||
$authed = $request->user()->profile;
|
||||
$authed = $request->user()->profile_id;
|
||||
$profile = Profile::findOrFail($id);
|
||||
if($id == $authed->id) {
|
||||
$publicOnly = true;
|
||||
} else {
|
||||
$publicOnly = (bool) $profile->followedBy($authed);
|
||||
|
||||
if($authed != $profile->id && !FollowerService::follows($authed, $profile->id)) {
|
||||
return [];
|
||||
}
|
||||
|
||||
$stories = Story::whereProfileId($profile->id)
|
||||
->whereActive(true)
|
||||
->orderBy('expires_at')
|
||||
->where('expires_at', '>', now())
|
||||
->when(!$publicOnly, function($query, $publicOnly) {
|
||||
return $query->wherePublic(true);
|
||||
})
|
||||
->get()
|
||||
->map(function($s, $k) {
|
||||
return [
|
||||
'id' => $s->id,
|
||||
'type' => Str::endsWith($s->path, '.mp4') ? 'video' :'photo',
|
||||
'length' => 10,
|
||||
->map(function($s, $k) use($authed) {
|
||||
$seen = StoryService::hasSeen($authed, $s->id);
|
||||
$res = [
|
||||
'id' => (string) $s->id,
|
||||
'type' => $s->type,
|
||||
'duration' => $s->duration,
|
||||
'src' => url(Storage::url($s->path)),
|
||||
'preview' => null,
|
||||
'link' => null,
|
||||
'linkText' => null,
|
||||
'time' => $s->created_at->format('U'),
|
||||
'expires_at' => (int) $s->expires_at->format('U'),
|
||||
'seen' => $s->seen()
|
||||
'created_at' => $s->created_at->toAtomString(),
|
||||
'expires_at' => $s->expires_at->toAtomString(),
|
||||
'view_count' => ($authed == $s->profile_id) ? ($s->view_count ?? 0) : null,
|
||||
'seen' => $seen,
|
||||
'progress' => $seen ? 100 : 0,
|
||||
'can_reply' => (bool) $s->can_reply,
|
||||
'can_react' => (bool) $s->can_react
|
||||
];
|
||||
|
||||
if($s->type == 'poll') {
|
||||
$res['question'] = json_decode($s->story, true)['question'];
|
||||
$res['options'] = json_decode($s->story, true)['options'];
|
||||
$res['voted'] = PollService::votedStory($s->id, $authed);
|
||||
if($res['voted']) {
|
||||
$res['voted_index'] = PollService::storyChoice($s->id, $authed);
|
||||
}
|
||||
}
|
||||
|
||||
return $res;
|
||||
})->toArray();
|
||||
if(count($stories) == 0) {
|
||||
return [];
|
||||
|
@ -342,32 +111,27 @@ class StoryController extends Controller
|
|||
$cursor = count($stories) - 1;
|
||||
$stories = [[
|
||||
'id' => (string) $stories[$cursor]['id'],
|
||||
'photo' => $profile->avatarUrl(),
|
||||
'name' => $profile->username,
|
||||
'link' => $profile->url(),
|
||||
'lastUpdated' => (int) now()->format('U'),
|
||||
'seen' => null,
|
||||
'items' => $stories,
|
||||
'nodes' => $stories,
|
||||
'account' => AccountService::get($profile->id),
|
||||
'pid' => (string) $profile->id
|
||||
]];
|
||||
return response()->json($stories, 200, [], JSON_PRETTY_PRINT|JSON_UNESCAPED_SLASHES);
|
||||
}
|
||||
|
||||
public function apiV1Viewed(Request $request)
|
||||
public function viewed(Request $request)
|
||||
{
|
||||
abort_if(!config_cache('instance.stories.enabled') || !$request->user(), 404);
|
||||
|
||||
$this->validate($request, [
|
||||
'id' => 'required|integer|min:1|exists:stories',
|
||||
'id' => 'required|min:1',
|
||||
]);
|
||||
$id = $request->input('id');
|
||||
|
||||
$authed = $request->user()->profile;
|
||||
|
||||
$story = Story::with('profile')
|
||||
->where('expires_at', '>', now())
|
||||
->orderByDesc('expires_at')
|
||||
->findOrFail($id);
|
||||
$exp = $story->expires_at;
|
||||
|
||||
$profile = $story->profile;
|
||||
|
||||
|
@ -378,72 +142,32 @@ class StoryController extends Controller
|
|||
$publicOnly = (bool) $profile->followedBy($authed);
|
||||
abort_if(!$publicOnly, 403);
|
||||
|
||||
StoryView::firstOrCreate([
|
||||
|
||||
$v = StoryView::firstOrCreate([
|
||||
'story_id' => $id,
|
||||
'profile_id' => $authed->id
|
||||
]);
|
||||
|
||||
$story->view_count = $story->view_count + 1;
|
||||
$story->save();
|
||||
if($v->wasRecentlyCreated) {
|
||||
Story::findOrFail($story->id)->increment('view_count');
|
||||
|
||||
if($story->local == false) {
|
||||
StoryViewDeliver::dispatch($story, $authed)->onQueue('story');
|
||||
}
|
||||
}
|
||||
|
||||
Cache::forget('stories:recent:by_id:' . $authed->id);
|
||||
StoryService::addSeen($authed->id, $story->id);
|
||||
return ['code' => 200];
|
||||
}
|
||||
|
||||
public function apiV1Exists(Request $request, $id)
|
||||
public function exists(Request $request, $id)
|
||||
{
|
||||
abort_if(!config_cache('instance.stories.enabled') || !$request->user(), 404);
|
||||
|
||||
$res = (bool) Story::whereProfileId($id)
|
||||
return response()->json(Story::whereProfileId($id)
|
||||
->whereActive(true)
|
||||
->where('expires_at', '>', now())
|
||||
->count();
|
||||
|
||||
return response()->json($res);
|
||||
}
|
||||
|
||||
public function apiV1Me(Request $request)
|
||||
{
|
||||
abort_if(!config_cache('instance.stories.enabled') || !$request->user(), 404);
|
||||
|
||||
$profile = $request->user()->profile;
|
||||
$stories = Story::whereProfileId($profile->id)
|
||||
->whereActive(true)
|
||||
->orderBy('expires_at')
|
||||
->where('expires_at', '>', now())
|
||||
->get()
|
||||
->map(function($s, $k) {
|
||||
return [
|
||||
'id' => $s->id,
|
||||
'type' => Str::endsWith($s->path, '.mp4') ? 'video' :'photo',
|
||||
'length' => 3,
|
||||
'src' => url(Storage::url($s->path)),
|
||||
'preview' => null,
|
||||
'link' => null,
|
||||
'linkText' => null,
|
||||
'time' => $s->created_at->format('U'),
|
||||
'expires_at' => (int) $s->expires_at->format('U'),
|
||||
'seen' => true
|
||||
];
|
||||
})->toArray();
|
||||
$ts = count($stories) ? last($stories)['time'] : null;
|
||||
$res = [
|
||||
'id' => (string) $profile->id,
|
||||
'photo' => $profile->avatarUrl(),
|
||||
'name' => $profile->username,
|
||||
'link' => $profile->url(),
|
||||
'lastUpdated' => $ts,
|
||||
'seen' => true,
|
||||
'items' => $stories
|
||||
];
|
||||
|
||||
return response()->json($res, 200, [], JSON_PRETTY_PRINT|JSON_UNESCAPED_SLASHES);
|
||||
}
|
||||
|
||||
public function compose(Request $request)
|
||||
{
|
||||
abort_if(!config_cache('instance.stories.enabled') || !$request->user(), 404);
|
||||
|
||||
return view('stories.compose');
|
||||
->exists());
|
||||
}
|
||||
|
||||
public function iRedirect(Request $request)
|
||||
|
@ -455,4 +179,91 @@ class StoryController extends Controller
|
|||
$username = $user->username;
|
||||
return redirect("/stories/{$username}");
|
||||
}
|
||||
|
||||
public function viewers(Request $request)
|
||||
{
|
||||
abort_if(!config_cache('instance.stories.enabled') || !$request->user(), 404);
|
||||
|
||||
$this->validate($request, [
|
||||
'sid' => 'required|string'
|
||||
]);
|
||||
|
||||
$pid = $request->user()->profile_id;
|
||||
$sid = $request->input('sid');
|
||||
|
||||
$story = Story::whereProfileId($pid)
|
||||
->whereActive(true)
|
||||
->findOrFail($sid);
|
||||
|
||||
$viewers = StoryView::whereStoryId($story->id)
|
||||
->latest()
|
||||
->simplePaginate(10)
|
||||
->map(function($view) {
|
||||
return AccountService::get($view->profile_id);
|
||||
})
|
||||
->values();
|
||||
|
||||
return response()->json($viewers, 200, [], JSON_PRETTY_PRINT|JSON_UNESCAPED_SLASHES);
|
||||
}
|
||||
|
||||
public function remoteStory(Request $request, $id)
|
||||
{
|
||||
abort_if(!config_cache('instance.stories.enabled') || !$request->user(), 404);
|
||||
|
||||
$profile = Profile::findOrFail($id);
|
||||
if($profile->user_id != null || $profile->domain == null) {
|
||||
return redirect('/stories/' . $profile->username);
|
||||
}
|
||||
$pid = $profile->id;
|
||||
return view('stories.show_remote', compact('pid'));
|
||||
}
|
||||
|
||||
public function pollResults(Request $request)
|
||||
{
|
||||
abort_if(!config_cache('instance.stories.enabled') || !$request->user(), 404);
|
||||
|
||||
$this->validate($request, [
|
||||
'sid' => 'required|string'
|
||||
]);
|
||||
|
||||
$pid = $request->user()->profile_id;
|
||||
$sid = $request->input('sid');
|
||||
|
||||
$story = Story::whereProfileId($pid)
|
||||
->whereActive(true)
|
||||
->findOrFail($sid);
|
||||
|
||||
return PollService::storyResults($sid);
|
||||
}
|
||||
|
||||
public function getActivityObject(Request $request, $username, $id)
|
||||
{
|
||||
abort_if(!config_cache('instance.stories.enabled'), 404);
|
||||
|
||||
if(!$request->wantsJson()) {
|
||||
return redirect('/stories/' . $username);
|
||||
}
|
||||
|
||||
abort_if(!$request->hasHeader('Authorization'), 404);
|
||||
|
||||
$profile = Profile::whereUsername($username)->whereNull('domain')->firstOrFail();
|
||||
$story = Story::whereActive(true)->whereProfileId($profile->id)->findOrFail($id);
|
||||
|
||||
abort_if($story->bearcap_token == null, 404);
|
||||
abort_if(now()->gt($story->expires_at), 404);
|
||||
$token = substr($request->header('Authorization'), 7);
|
||||
abort_if(hash_equals($story->bearcap_token, $token) === false, 404);
|
||||
abort_if($story->created_at->lt(now()->subMinutes(20)), 404);
|
||||
|
||||
$fractal = new Manager();
|
||||
$fractal->setSerializer(new ArraySerializer());
|
||||
$resource = new Item($story, new StoryVerb());
|
||||
$res = $fractal->createData($resource)->toArray();
|
||||
return response()->json($res, 200, [], JSON_PRETTY_PRINT|JSON_UNESCAPED_SLASHES);
|
||||
}
|
||||
|
||||
public function showSystemStory()
|
||||
{
|
||||
// return view('stories.system');
|
||||
}
|
||||
}
|
||||
|
|
|
@ -14,59 +14,62 @@ use Illuminate\Support\Facades\Redis;
|
|||
|
||||
class FollowPipeline implements ShouldQueue
|
||||
{
|
||||
use Dispatchable, InteractsWithQueue, Queueable, SerializesModels;
|
||||
use Dispatchable, InteractsWithQueue, Queueable, SerializesModels;
|
||||
|
||||
protected $follower;
|
||||
protected $follower;
|
||||
|
||||
/**
|
||||
* Delete the job if its models no longer exist.
|
||||
*
|
||||
* @var bool
|
||||
*/
|
||||
public $deleteWhenMissingModels = true;
|
||||
|
||||
/**
|
||||
* Create a new job instance.
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
public function __construct($follower)
|
||||
{
|
||||
$this->follower = $follower;
|
||||
}
|
||||
/**
|
||||
* Delete the job if its models no longer exist.
|
||||
*
|
||||
* @var bool
|
||||
*/
|
||||
public $deleteWhenMissingModels = true;
|
||||
|
||||
/**
|
||||
* Create a new job instance.
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
public function __construct($follower)
|
||||
{
|
||||
$this->follower = $follower;
|
||||
}
|
||||
|
||||
/**
|
||||
* Execute the job.
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
public function handle()
|
||||
{
|
||||
$follower = $this->follower;
|
||||
$actor = $follower->actor;
|
||||
$target = $follower->target;
|
||||
/**
|
||||
* Execute the job.
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
public function handle()
|
||||
{
|
||||
$follower = $this->follower;
|
||||
$actor = $follower->actor;
|
||||
$target = $follower->target;
|
||||
|
||||
if($target->domain || !$target->private_key) {
|
||||
return;
|
||||
}
|
||||
Cache::forget('profile:following:' . $actor->id);
|
||||
Cache::forget('profile:following:' . $target->id);
|
||||
|
||||
try {
|
||||
$notification = new Notification();
|
||||
$notification->profile_id = $target->id;
|
||||
$notification->actor_id = $actor->id;
|
||||
$notification->action = 'follow';
|
||||
$notification->message = $follower->toText();
|
||||
$notification->rendered = $follower->toHtml();
|
||||
$notification->item_id = $target->id;
|
||||
$notification->item_type = "App\Profile";
|
||||
$notification->save();
|
||||
if($target->domain || !$target->private_key) {
|
||||
return;
|
||||
}
|
||||
|
||||
$redis = Redis::connection();
|
||||
try {
|
||||
$notification = new Notification();
|
||||
$notification->profile_id = $target->id;
|
||||
$notification->actor_id = $actor->id;
|
||||
$notification->action = 'follow';
|
||||
$notification->message = $follower->toText();
|
||||
$notification->rendered = $follower->toHtml();
|
||||
$notification->item_id = $target->id;
|
||||
$notification->item_type = "App\Profile";
|
||||
$notification->save();
|
||||
|
||||
$nkey = config('cache.prefix').':user.'.$target->id.'.notifications';
|
||||
$redis->lpush($nkey, $notification->id);
|
||||
} catch (Exception $e) {
|
||||
Log::error($e);
|
||||
}
|
||||
}
|
||||
$redis = Redis::connection();
|
||||
|
||||
$nkey = config('cache.prefix').':user.'.$target->id.'.notifications';
|
||||
$redis->lpush($nkey, $notification->id);
|
||||
} catch (Exception $e) {
|
||||
Log::error($e);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
56
app/Jobs/InstancePipeline/FetchNodeinfoPipeline.php
Normal file
56
app/Jobs/InstancePipeline/FetchNodeinfoPipeline.php
Normal file
|
@ -0,0 +1,56 @@
|
|||
<?php
|
||||
|
||||
namespace App\Jobs\InstancePipeline;
|
||||
|
||||
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 Illuminate\Support\Facades\Http;
|
||||
use App\Instance;
|
||||
use App\Profile;
|
||||
use App\Services\NodeinfoService;
|
||||
|
||||
class FetchNodeinfoPipeline implements ShouldQueue
|
||||
{
|
||||
use Dispatchable, InteractsWithQueue, Queueable, SerializesModels;
|
||||
|
||||
protected $instance;
|
||||
|
||||
/**
|
||||
* Create a new job instance.
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
public function __construct(Instance $instance)
|
||||
{
|
||||
$this->instance = $instance;
|
||||
}
|
||||
|
||||
/**
|
||||
* Execute the job.
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
public function handle()
|
||||
{
|
||||
$instance = $this->instance;
|
||||
|
||||
$ni = NodeinfoService::get($instance->domain);
|
||||
if($ni) {
|
||||
if(isset($ni['software']) && is_array($ni['software']) && isset($ni['software']['name'])) {
|
||||
$software = $ni['software']['name'];
|
||||
$instance->software = strtolower(strip_tags($software));
|
||||
$instance->last_crawled_at = now();
|
||||
$instance->user_count = Profile::whereDomain($instance->domain)->count();
|
||||
$instance->save();
|
||||
}
|
||||
} else {
|
||||
$instance->user_count = Profile::whereDomain($instance->domain)->count();
|
||||
$instance->last_crawled_at = now();
|
||||
$instance->save();
|
||||
}
|
||||
}
|
||||
}
|
43
app/Jobs/InstancePipeline/InstanceCrawlPipeline.php
Normal file
43
app/Jobs/InstancePipeline/InstanceCrawlPipeline.php
Normal file
|
@ -0,0 +1,43 @@
|
|||
<?php
|
||||
|
||||
namespace App\Jobs\InstancePipeline;
|
||||
|
||||
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 Illuminate\Support\Facades\Http;
|
||||
use App\Instance;
|
||||
use App\Profile;
|
||||
use App\Services\NodeinfoService;
|
||||
|
||||
class InstanceCrawlPipeline implements ShouldQueue
|
||||
{
|
||||
use Dispatchable, InteractsWithQueue, Queueable, SerializesModels;
|
||||
|
||||
/**
|
||||
* Create a new job instance.
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
public function __construct()
|
||||
{
|
||||
//
|
||||
}
|
||||
|
||||
/**
|
||||
* Execute the job.
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
public function handle()
|
||||
{
|
||||
Instance::whereNull('last_crawled_at')->whereNull('software')->chunk(50, function($instances) use($headers) {
|
||||
foreach($instances as $instance) {
|
||||
FetchNodeinfoPipeline::dispatch($instance)->onQueue('low');
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
47
app/Jobs/MediaPipeline/MediaSyncLicensePipeline.php
Normal file
47
app/Jobs/MediaPipeline/MediaSyncLicensePipeline.php
Normal file
|
@ -0,0 +1,47 @@
|
|||
<?php
|
||||
|
||||
namespace App\Jobs\MediaPipeline;
|
||||
|
||||
use App\Media;
|
||||
use App\User;
|
||||
use Cache;
|
||||
use Illuminate\Bus\Queueable;
|
||||
use Illuminate\Contracts\Queue\ShouldQueue;
|
||||
use Illuminate\Foundation\Bus\Dispatchable;
|
||||
use Illuminate\Queue\InteractsWithQueue;
|
||||
use Illuminate\Queue\SerializesModels;
|
||||
use App\Services\StatusService;
|
||||
|
||||
class MediaSyncLicensePipeline implements ShouldQueue
|
||||
{
|
||||
use Dispatchable, InteractsWithQueue, Queueable, SerializesModels;
|
||||
|
||||
protected $userId;
|
||||
protected $licenseId;
|
||||
|
||||
public function __construct($userId, $licenseId)
|
||||
{
|
||||
$this->userId = $userId;
|
||||
$this->licenseId = $licenseId;
|
||||
}
|
||||
|
||||
public function handle()
|
||||
{
|
||||
$licenseId = $this->licenseId;
|
||||
|
||||
if(!$licenseId || !$this->userId) {
|
||||
return 1;
|
||||
}
|
||||
|
||||
Media::whereUserId($this->userId)
|
||||
->chunk(100, function($medias) use($licenseId) {
|
||||
foreach($medias as $media) {
|
||||
$media->license = $licenseId;
|
||||
$media->save();
|
||||
Cache::forget('status:transformer:media:attachments:'. $media->status_id);
|
||||
StatusService::del($media->status_id);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
}
|
|
@ -12,6 +12,7 @@ use Illuminate\Queue\SerializesModels;
|
|||
use League\Fractal;
|
||||
use League\Fractal\Serializer\ArraySerializer;
|
||||
use App\Transformer\ActivityPub\Verb\CreateNote;
|
||||
use App\Transformer\ActivityPub\Verb\CreateQuestion;
|
||||
use App\Util\ActivityPub\Helpers;
|
||||
use GuzzleHttp\Pool;
|
||||
use GuzzleHttp\Client;
|
||||
|
@ -20,85 +21,97 @@ use App\Util\ActivityPub\HttpSignature;
|
|||
|
||||
class StatusActivityPubDeliver implements ShouldQueue
|
||||
{
|
||||
use Dispatchable, InteractsWithQueue, Queueable, SerializesModels;
|
||||
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;
|
||||
}
|
||||
protected $status;
|
||||
|
||||
/**
|
||||
* Execute the job.
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
public function handle()
|
||||
{
|
||||
$status = $this->status;
|
||||
$profile = $status->profile;
|
||||
/**
|
||||
* Delete the job if its models no longer exist.
|
||||
*
|
||||
* @var bool
|
||||
*/
|
||||
public $deleteWhenMissingModels = true;
|
||||
|
||||
if($status->local == false || $status->url || $status->uri) {
|
||||
return;
|
||||
}
|
||||
/**
|
||||
* Create a new job instance.
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
public function __construct(Status $status)
|
||||
{
|
||||
$this->status = $status;
|
||||
}
|
||||
|
||||
$audience = $status->profile->getAudienceInbox();
|
||||
/**
|
||||
* Execute the job.
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
public function handle()
|
||||
{
|
||||
$status = $this->status;
|
||||
$profile = $status->profile;
|
||||
|
||||
if(empty($audience) || !in_array($status->scope, ['public', 'unlisted', 'private'])) {
|
||||
// Return on profiles with no remote followers
|
||||
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':
|
||||
$activitypubObject = new CreateQuestion();
|
||||
break;
|
||||
|
||||
default:
|
||||
$activitypubObject = new CreateNote();
|
||||
break;
|
||||
}
|
||||
|
||||
|
||||
$fractal = new Fractal\Manager();
|
||||
$fractal->setSerializer(new ArraySerializer());
|
||||
$resource = new Fractal\Resource\Item($status, new CreateNote());
|
||||
$activity = $fractal->createData($resource)->toArray();
|
||||
$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')
|
||||
]);
|
||||
$payload = json_encode($activity);
|
||||
|
||||
$requests = function($audience) use ($client, $activity, $profile, $payload) {
|
||||
foreach($audience as $url) {
|
||||
$headers = HttpSignature::sign($profile, $url, $activity);
|
||||
yield function() use ($client, $url, $headers, $payload) {
|
||||
return $client->postAsync($url, [
|
||||
'curl' => [
|
||||
CURLOPT_HTTPHEADER => $headers,
|
||||
CURLOPT_POSTFIELDS => $payload,
|
||||
CURLOPT_HEADER => true
|
||||
]
|
||||
]);
|
||||
};
|
||||
}
|
||||
};
|
||||
$client = new Client([
|
||||
'timeout' => config('federation.activitypub.delivery.timeout')
|
||||
]);
|
||||
|
||||
$pool = new Pool($client, $requests($audience), [
|
||||
'concurrency' => config('federation.activitypub.delivery.concurrency'),
|
||||
'fulfilled' => function ($response, $index) {
|
||||
},
|
||||
'rejected' => function ($reason, $index) {
|
||||
}
|
||||
]);
|
||||
|
||||
$promise = $pool->promise();
|
||||
$requests = function($audience) use ($client, $activity, $profile, $payload) {
|
||||
foreach($audience as $url) {
|
||||
$headers = HttpSignature::sign($profile, $url, $activity);
|
||||
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
|
||||
]
|
||||
]);
|
||||
};
|
||||
}
|
||||
};
|
||||
|
||||
$promise->wait();
|
||||
}
|
||||
$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();
|
||||
}
|
||||
}
|
||||
|
|
136
app/Jobs/StoryPipeline/StoryDelete.php
Normal file
136
app/Jobs/StoryPipeline/StoryDelete.php
Normal file
|
@ -0,0 +1,136 @@
|
|||
<?php
|
||||
|
||||
namespace App\Jobs\StoryPipeline;
|
||||
|
||||
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 Storage;
|
||||
use App\Story;
|
||||
use League\Fractal;
|
||||
use League\Fractal\Serializer\ArraySerializer;
|
||||
use App\Transformer\ActivityPub\Verb\DeleteStory;
|
||||
use App\Util\ActivityPub\Helpers;
|
||||
use GuzzleHttp\Pool;
|
||||
use GuzzleHttp\Client;
|
||||
use GuzzleHttp\Promise;
|
||||
use App\Util\ActivityPub\HttpSignature;
|
||||
use App\Services\FollowerService;
|
||||
use App\Services\StoryService;
|
||||
|
||||
class StoryDelete implements ShouldQueue
|
||||
{
|
||||
use Dispatchable, InteractsWithQueue, Queueable, SerializesModels;
|
||||
|
||||
protected $story;
|
||||
|
||||
/**
|
||||
* Delete the job if its models no longer exist.
|
||||
*
|
||||
* @var bool
|
||||
*/
|
||||
public $deleteWhenMissingModels = true;
|
||||
|
||||
/**
|
||||
* Create a new job instance.
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
public function __construct(Story $story)
|
||||
{
|
||||
$this->story = $story;
|
||||
}
|
||||
|
||||
/**
|
||||
* Execute the job.
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
public function handle()
|
||||
{
|
||||
$story = $this->story;
|
||||
|
||||
if($story->local == false) {
|
||||
return;
|
||||
}
|
||||
|
||||
StoryService::removeRotateQueue($story->id);
|
||||
StoryService::delLatest($story->profile_id);
|
||||
StoryService::delById($story->id);
|
||||
|
||||
if(Storage::exists($story->path) == true) {
|
||||
Storage::delete($story->path);
|
||||
}
|
||||
|
||||
$story->views()->delete();
|
||||
|
||||
$profile = $story->profile;
|
||||
|
||||
$activity = [
|
||||
'@context' => 'https://www.w3.org/ns/activitystreams',
|
||||
'id' => $story->url() . '#delete',
|
||||
'type' => 'Delete',
|
||||
'actor' => $profile->permalink(),
|
||||
'object' => [
|
||||
'id' => $story->url(),
|
||||
'type' => 'Story',
|
||||
],
|
||||
];
|
||||
|
||||
$this->fanoutExpiry($profile, $activity);
|
||||
|
||||
// delete notifications
|
||||
// delete polls
|
||||
// delete reports
|
||||
|
||||
$story->delete();
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
protected function fanoutExpiry($profile, $activity)
|
||||
{
|
||||
$audience = FollowerService::softwareAudience($profile->id, 'pixelfed');
|
||||
|
||||
if(empty($audience)) {
|
||||
// Return on profiles with no remote followers
|
||||
return;
|
||||
}
|
||||
|
||||
$payload = json_encode($activity);
|
||||
|
||||
$client = new Client([
|
||||
'timeout' => config('federation.activitypub.delivery.timeout')
|
||||
]);
|
||||
|
||||
$requests = function($audience) use ($client, $activity, $profile, $payload) {
|
||||
foreach($audience as $url) {
|
||||
$headers = HttpSignature::sign($profile, $url, $activity);
|
||||
yield function() use ($client, $url, $headers, $payload) {
|
||||
return $client->postAsync($url, [
|
||||
'curl' => [
|
||||
CURLOPT_HTTPHEADER => $headers,
|
||||
CURLOPT_POSTFIELDS => $payload,
|
||||
CURLOPT_HEADER => true
|
||||
]
|
||||
]);
|
||||
};
|
||||
}
|
||||
};
|
||||
|
||||
$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();
|
||||
}
|
||||
}
|
169
app/Jobs/StoryPipeline/StoryExpire.php
Normal file
169
app/Jobs/StoryPipeline/StoryExpire.php
Normal file
|
@ -0,0 +1,169 @@
|
|||
<?php
|
||||
|
||||
namespace App\Jobs\StoryPipeline;
|
||||
|
||||
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 Storage;
|
||||
use App\Story;
|
||||
use League\Fractal;
|
||||
use League\Fractal\Serializer\ArraySerializer;
|
||||
use App\Transformer\ActivityPub\Verb\DeleteStory;
|
||||
use App\Util\ActivityPub\Helpers;
|
||||
use GuzzleHttp\Pool;
|
||||
use GuzzleHttp\Client;
|
||||
use GuzzleHttp\Promise;
|
||||
use App\Util\ActivityPub\HttpSignature;
|
||||
use App\Services\FollowerService;
|
||||
use App\Services\StoryService;
|
||||
|
||||
class StoryExpire implements ShouldQueue
|
||||
{
|
||||
use Dispatchable, InteractsWithQueue, Queueable, SerializesModels;
|
||||
|
||||
protected $story;
|
||||
|
||||
/**
|
||||
* Delete the job if its models no longer exist.
|
||||
*
|
||||
* @var bool
|
||||
*/
|
||||
public $deleteWhenMissingModels = true;
|
||||
|
||||
/**
|
||||
* Create a new job instance.
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
public function __construct(Story $story)
|
||||
{
|
||||
$this->story = $story;
|
||||
}
|
||||
|
||||
/**
|
||||
* Execute the job.
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
public function handle()
|
||||
{
|
||||
$story = $this->story;
|
||||
|
||||
if($story->local == false) {
|
||||
$this->handleRemoteExpiry();
|
||||
return;
|
||||
}
|
||||
|
||||
if($story->active == false) {
|
||||
return;
|
||||
}
|
||||
|
||||
if($story->expires_at->gt(now())) {
|
||||
return;
|
||||
}
|
||||
|
||||
$story->active = false;
|
||||
$story->save();
|
||||
|
||||
$this->rotateMediaPath();
|
||||
$this->fanoutExpiry();
|
||||
|
||||
StoryService::delLatest($story->profile_id);
|
||||
}
|
||||
|
||||
protected function rotateMediaPath()
|
||||
{
|
||||
$story = $this->story;
|
||||
$date = date('Y').date('m');
|
||||
$old = $story->path;
|
||||
$base = "story_archives/{$story->profile_id}/{$date}/";
|
||||
$paths = explode('/', $old);
|
||||
$path = array_pop($paths);
|
||||
$newPath = $base . $path;
|
||||
|
||||
if(Storage::exists($old) == true) {
|
||||
$dir = implode('/', $paths);
|
||||
Storage::move($old, $newPath);
|
||||
Storage::delete($old);
|
||||
$story->bearcap_token = null;
|
||||
$story->path = $newPath;
|
||||
$story->save();
|
||||
Storage::deleteDirectory($dir);
|
||||
}
|
||||
}
|
||||
|
||||
protected function fanoutExpiry()
|
||||
{
|
||||
$story = $this->story;
|
||||
$profile = $story->profile;
|
||||
|
||||
if($story->local == false || $story->remote_url) {
|
||||
return;
|
||||
}
|
||||
|
||||
$audience = FollowerService::softwareAudience($story->profile_id, 'pixelfed');
|
||||
|
||||
if(empty($audience)) {
|
||||
// Return on profiles with no remote followers
|
||||
return;
|
||||
}
|
||||
|
||||
$fractal = new Fractal\Manager();
|
||||
$fractal->setSerializer(new ArraySerializer());
|
||||
$resource = new Fractal\Resource\Item($story, new DeleteStory());
|
||||
$activity = $fractal->createData($resource)->toArray();
|
||||
|
||||
$payload = json_encode($activity);
|
||||
|
||||
$client = new Client([
|
||||
'timeout' => config('federation.activitypub.delivery.timeout')
|
||||
]);
|
||||
|
||||
$requests = function($audience) use ($client, $activity, $profile, $payload) {
|
||||
foreach($audience as $url) {
|
||||
$headers = HttpSignature::sign($profile, $url, $activity);
|
||||
yield function() use ($client, $url, $headers, $payload) {
|
||||
return $client->postAsync($url, [
|
||||
'curl' => [
|
||||
CURLOPT_HTTPHEADER => $headers,
|
||||
CURLOPT_POSTFIELDS => $payload,
|
||||
CURLOPT_HEADER => true
|
||||
]
|
||||
]);
|
||||
};
|
||||
}
|
||||
};
|
||||
|
||||
$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();
|
||||
}
|
||||
|
||||
protected function handleRemoteExpiry()
|
||||
{
|
||||
$story = $this->story;
|
||||
$story->active = false;
|
||||
$story->save();
|
||||
|
||||
$path = $story->path;
|
||||
|
||||
if(Storage::exists($path) == true) {
|
||||
Storage::delete($path);
|
||||
}
|
||||
|
||||
$story->views()->delete();
|
||||
$story->delete();
|
||||
}
|
||||
}
|
107
app/Jobs/StoryPipeline/StoryFanout.php
Normal file
107
app/Jobs/StoryPipeline/StoryFanout.php
Normal file
|
@ -0,0 +1,107 @@
|
|||
<?php
|
||||
|
||||
namespace App\Jobs\StoryPipeline;
|
||||
|
||||
use Cache, Log;
|
||||
use App\Story;
|
||||
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\CreateStory;
|
||||
use App\Util\ActivityPub\Helpers;
|
||||
use GuzzleHttp\Pool;
|
||||
use GuzzleHttp\Client;
|
||||
use GuzzleHttp\Promise;
|
||||
use App\Util\ActivityPub\HttpSignature;
|
||||
use App\Services\FollowerService;
|
||||
use App\Services\StoryService;
|
||||
|
||||
class StoryFanout implements ShouldQueue
|
||||
{
|
||||
use Dispatchable, InteractsWithQueue, Queueable, SerializesModels;
|
||||
|
||||
protected $story;
|
||||
|
||||
/**
|
||||
* Delete the job if its models no longer exist.
|
||||
*
|
||||
* @var bool
|
||||
*/
|
||||
public $deleteWhenMissingModels = true;
|
||||
|
||||
/**
|
||||
* Create a new job instance.
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
public function __construct(Story $story)
|
||||
{
|
||||
$this->story = $story;
|
||||
}
|
||||
|
||||
/**
|
||||
* Execute the job.
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
public function handle()
|
||||
{
|
||||
$story = $this->story;
|
||||
$profile = $story->profile;
|
||||
|
||||
if($story->local == false || $story->remote_url) {
|
||||
return;
|
||||
}
|
||||
|
||||
StoryService::delLatest($story->profile_id);
|
||||
|
||||
$audience = FollowerService::softwareAudience($story->profile_id, 'pixelfed');
|
||||
|
||||
if(empty($audience)) {
|
||||
// Return on profiles with no remote followers
|
||||
return;
|
||||
}
|
||||
|
||||
$fractal = new Fractal\Manager();
|
||||
$fractal->setSerializer(new ArraySerializer());
|
||||
$resource = new Fractal\Resource\Item($story, new CreateStory());
|
||||
$activity = $fractal->createData($resource)->toArray();
|
||||
|
||||
$payload = json_encode($activity);
|
||||
|
||||
$client = new Client([
|
||||
'timeout' => config('federation.activitypub.delivery.timeout')
|
||||
]);
|
||||
|
||||
$requests = function($audience) use ($client, $activity, $profile, $payload) {
|
||||
foreach($audience as $url) {
|
||||
$headers = HttpSignature::sign($profile, $url, $activity);
|
||||
yield function() use ($client, $url, $headers, $payload) {
|
||||
return $client->postAsync($url, [
|
||||
'curl' => [
|
||||
CURLOPT_HTTPHEADER => $headers,
|
||||
CURLOPT_POSTFIELDS => $payload,
|
||||
CURLOPT_HEADER => true
|
||||
]
|
||||
]);
|
||||
};
|
||||
}
|
||||
};
|
||||
|
||||
$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();
|
||||
}
|
||||
}
|
144
app/Jobs/StoryPipeline/StoryFetch.php
Normal file
144
app/Jobs/StoryPipeline/StoryFetch.php
Normal file
|
@ -0,0 +1,144 @@
|
|||
<?php
|
||||
|
||||
namespace App\Jobs\StoryPipeline;
|
||||
|
||||
use Cache, Log;
|
||||
use App\Story;
|
||||
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 App\Services\FollowerService;
|
||||
use App\Util\Lexer\Bearcap;
|
||||
use Illuminate\Support\Facades\Http;
|
||||
use Illuminate\Http\Client\RequestException;
|
||||
use Illuminate\Http\Client\ConnectionException;
|
||||
use App\Util\ActivityPub\Validator\StoryValidator;
|
||||
use App\Services\StoryService;
|
||||
use App\Services\MediaPathService;
|
||||
use Illuminate\Support\Str;
|
||||
use Illuminate\Http\File;
|
||||
use Illuminate\Support\Facades\Storage;
|
||||
|
||||
class StoryFetch implements ShouldQueue
|
||||
{
|
||||
use Dispatchable, InteractsWithQueue, Queueable, SerializesModels;
|
||||
|
||||
protected $activity;
|
||||
|
||||
/**
|
||||
* Create a new job instance.
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
public function __construct($activity)
|
||||
{
|
||||
$this->activity = $activity;
|
||||
}
|
||||
|
||||
/**
|
||||
* Execute the job.
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
public function handle()
|
||||
{
|
||||
$activity = $this->activity;
|
||||
$activityId = $activity['id'];
|
||||
$activityActor = $activity['actor'];
|
||||
|
||||
if(parse_url($activityId, PHP_URL_HOST) !== parse_url($activityActor, PHP_URL_HOST)) {
|
||||
return;
|
||||
}
|
||||
|
||||
$bearcap = Bearcap::decode($activity['object']['object']);
|
||||
|
||||
if(!$bearcap) {
|
||||
return;
|
||||
}
|
||||
|
||||
$url = $bearcap['url'];
|
||||
$token = $bearcap['token'];
|
||||
|
||||
if(parse_url($activityId, PHP_URL_HOST) !== parse_url($url, PHP_URL_HOST)) {
|
||||
return;
|
||||
}
|
||||
|
||||
$version = config('pixelfed.version');
|
||||
$appUrl = config('app.url');
|
||||
$headers = [
|
||||
'Accept' => 'application/json',
|
||||
'Authorization' => 'Bearer ' . $token,
|
||||
'User-Agent' => "(Pixelfed/{$version}; +{$appUrl})",
|
||||
];
|
||||
|
||||
try {
|
||||
$res = Http::withHeaders($headers)
|
||||
->timeout(30)
|
||||
->get($url);
|
||||
} catch (RequestException $e) {
|
||||
return false;
|
||||
} catch (ConnectionException $e) {
|
||||
return false;
|
||||
} catch (\Exception $e) {
|
||||
return false;
|
||||
}
|
||||
|
||||
$payload = $res->json();
|
||||
|
||||
if(StoryValidator::validate($payload) == false) {
|
||||
return;
|
||||
}
|
||||
|
||||
if(Helpers::validateUrl($payload['attachment']['url']) == false) {
|
||||
return;
|
||||
}
|
||||
|
||||
$type = $payload['attachment']['type'] == 'Image' ? 'photo' : 'video';
|
||||
|
||||
$profile = Helpers::profileFetch($payload['attributedTo']);
|
||||
|
||||
$ext = pathinfo($payload['attachment']['url'], PATHINFO_EXTENSION);
|
||||
$storagePath = MediaPathService::story($profile);
|
||||
$fileName = Str::random(random_int(2, 12)) . '_' . Str::random(random_int(32, 35)) . '_' . Str::random(random_int(1, 14)) . '.' . $ext;
|
||||
$contextOptions = [
|
||||
'ssl' => [
|
||||
'verify_peer' => false,
|
||||
'verify_peername' => false
|
||||
]
|
||||
];
|
||||
$ctx = stream_context_create($contextOptions);
|
||||
$data = file_get_contents($payload['attachment']['url'], false, $ctx);
|
||||
$tmpBase = storage_path('app/remcache/');
|
||||
$tmpPath = $profile->id . '-' . $fileName;
|
||||
$tmpName = $tmpBase . $tmpPath;
|
||||
file_put_contents($tmpName, $data);
|
||||
$disk = Storage::disk(config('filesystems.default'));
|
||||
$path = $disk->putFileAs($storagePath, new File($tmpName), $fileName, 'public');
|
||||
$size = filesize($tmpName);
|
||||
unlink($tmpName);
|
||||
|
||||
$story = new Story;
|
||||
$story->profile_id = $profile->id;
|
||||
$story->object_id = $payload['id'];
|
||||
$story->size = $size;
|
||||
$story->mime = $payload['attachment']['mediaType'];
|
||||
$story->duration = $payload['duration'];
|
||||
$story->media_url = $payload['attachment']['url'];
|
||||
$story->type = $type;
|
||||
$story->public = false;
|
||||
$story->local = false;
|
||||
$story->active = true;
|
||||
$story->path = $path;
|
||||
$story->view_count = 0;
|
||||
$story->can_reply = $payload['can_reply'];
|
||||
$story->can_react = $payload['can_react'];
|
||||
$story->created_at = now()->parse($payload['published']);
|
||||
$story->expires_at = now()->parse($payload['expiresAt']);
|
||||
$story->save();
|
||||
|
||||
StoryService::delLatest($story->profile_id);
|
||||
}
|
||||
}
|
70
app/Jobs/StoryPipeline/StoryReactionDeliver.php
Normal file
70
app/Jobs/StoryPipeline/StoryReactionDeliver.php
Normal file
|
@ -0,0 +1,70 @@
|
|||
<?php
|
||||
|
||||
namespace App\Jobs\StoryPipeline;
|
||||
|
||||
use App\Story;
|
||||
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 App\Util\ActivityPub\Helpers;
|
||||
|
||||
class StoryReactionDeliver implements ShouldQueue
|
||||
{
|
||||
use Dispatchable, InteractsWithQueue, Queueable, SerializesModels;
|
||||
|
||||
protected $story;
|
||||
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(Story $story, Status $status)
|
||||
{
|
||||
$this->story = $story;
|
||||
$this->status = $status;
|
||||
}
|
||||
|
||||
/**
|
||||
* Execute the job.
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
public function handle()
|
||||
{
|
||||
$story = $this->story;
|
||||
$status = $this->status;
|
||||
|
||||
if($story->local == true) {
|
||||
return;
|
||||
}
|
||||
|
||||
$target = $story->profile;
|
||||
$actor = $status->profile;
|
||||
$to = $target->inbox_url;
|
||||
|
||||
$payload = [
|
||||
'@context' => 'https://www.w3.org/ns/activitystreams',
|
||||
'id' => $status->permalink(),
|
||||
'type' => 'Story:Reaction',
|
||||
'to' => $target->permalink(),
|
||||
'actor' => $actor->permalink(),
|
||||
'content' => $status->caption,
|
||||
'inReplyTo' => $story->object_id,
|
||||
'published' => $status->created_at->toAtomString()
|
||||
];
|
||||
|
||||
Helpers::sendSignedObject($actor, $to, $payload);
|
||||
}
|
||||
}
|
70
app/Jobs/StoryPipeline/StoryReplyDeliver.php
Normal file
70
app/Jobs/StoryPipeline/StoryReplyDeliver.php
Normal file
|
@ -0,0 +1,70 @@
|
|||
<?php
|
||||
|
||||
namespace App\Jobs\StoryPipeline;
|
||||
|
||||
use App\Story;
|
||||
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 App\Util\ActivityPub\Helpers;
|
||||
|
||||
class StoryReplyDeliver implements ShouldQueue
|
||||
{
|
||||
use Dispatchable, InteractsWithQueue, Queueable, SerializesModels;
|
||||
|
||||
protected $story;
|
||||
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(Story $story, Status $status)
|
||||
{
|
||||
$this->story = $story;
|
||||
$this->status = $status;
|
||||
}
|
||||
|
||||
/**
|
||||
* Execute the job.
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
public function handle()
|
||||
{
|
||||
$story = $this->story;
|
||||
$status = $this->status;
|
||||
|
||||
if($story->local == true) {
|
||||
return;
|
||||
}
|
||||
|
||||
$target = $story->profile;
|
||||
$actor = $status->profile;
|
||||
$to = $target->inbox_url;
|
||||
|
||||
$payload = [
|
||||
'@context' => 'https://www.w3.org/ns/activitystreams',
|
||||
'id' => $status->permalink(),
|
||||
'type' => 'Story:Reply',
|
||||
'to' => $target->permalink(),
|
||||
'actor' => $actor->permalink(),
|
||||
'content' => $status->caption,
|
||||
'inReplyTo' => $story->object_id,
|
||||
'published' => $status->created_at->toAtomString()
|
||||
];
|
||||
|
||||
Helpers::sendSignedObject($actor, $to, $payload);
|
||||
}
|
||||
}
|
61
app/Jobs/StoryPipeline/StoryRotateMedia.php
Normal file
61
app/Jobs/StoryPipeline/StoryRotateMedia.php
Normal file
|
@ -0,0 +1,61 @@
|
|||
<?php
|
||||
|
||||
namespace App\Jobs\StoryPipeline;
|
||||
|
||||
use Illuminate\Support\Facades\Storage;
|
||||
use Illuminate\Support\Str;
|
||||
use App\Story;
|
||||
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;
|
||||
|
||||
class StoryRotateMedia implements ShouldQueue
|
||||
{
|
||||
use Dispatchable, InteractsWithQueue, Queueable, SerializesModels;
|
||||
|
||||
protected $story;
|
||||
|
||||
/**
|
||||
* Create a new job instance.
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
public function __construct(Story $story)
|
||||
{
|
||||
$this->story = $story;
|
||||
}
|
||||
|
||||
/**
|
||||
* Execute the job.
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
public function handle()
|
||||
{
|
||||
$story = $this->story;
|
||||
|
||||
if($story->local == false) {
|
||||
return;
|
||||
}
|
||||
|
||||
$paths = explode('/', $story->path);
|
||||
$name = array_pop($paths);
|
||||
|
||||
$oldPath = $story->path;
|
||||
$ext = pathinfo($name, PATHINFO_EXTENSION);
|
||||
$new = Str::random(13) . '_' . Str::random(24) . '_' . Str::random(3) . '.' . $ext;
|
||||
array_push($paths, $new);
|
||||
$newPath = implode('/', $paths);
|
||||
|
||||
if(Storage::exists($oldPath)) {
|
||||
Storage::copy($oldPath, $newPath);
|
||||
$story->path = $newPath;
|
||||
$story->bearcap_token = null;
|
||||
$story->save();
|
||||
Storage::delete($oldPath);
|
||||
}
|
||||
}
|
||||
}
|
70
app/Jobs/StoryPipeline/StoryViewDeliver.php
Normal file
70
app/Jobs/StoryPipeline/StoryViewDeliver.php
Normal file
|
@ -0,0 +1,70 @@
|
|||
<?php
|
||||
|
||||
namespace App\Jobs\StoryPipeline;
|
||||
|
||||
use App\Story;
|
||||
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;
|
||||
|
||||
class StoryViewDeliver implements ShouldQueue
|
||||
{
|
||||
use Dispatchable, InteractsWithQueue, Queueable, SerializesModels;
|
||||
|
||||
protected $story;
|
||||
protected $profile;
|
||||
|
||||
/**
|
||||
* Delete the job if its models no longer exist.
|
||||
*
|
||||
* @var bool
|
||||
*/
|
||||
public $deleteWhenMissingModels = true;
|
||||
|
||||
/**
|
||||
* Create a new job instance.
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
public function __construct(Story $story, Profile $profile)
|
||||
{
|
||||
$this->story = $story;
|
||||
$this->profile = $profile;
|
||||
}
|
||||
|
||||
/**
|
||||
* Execute the job.
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
public function handle()
|
||||
{
|
||||
$story = $this->story;
|
||||
|
||||
if($story->local == true) {
|
||||
return;
|
||||
}
|
||||
|
||||
$actor = $this->profile;
|
||||
$target = $story->profile;
|
||||
$to = $target->inbox_url;
|
||||
|
||||
$payload = [
|
||||
'@context' => 'https://www.w3.org/ns/activitystreams',
|
||||
'id' => $actor->permalink('#stories/' . $story->id . '/view'),
|
||||
'type' => 'View',
|
||||
'to' => $target->permalink(),
|
||||
'actor' => $actor->permalink(),
|
||||
'object' => [
|
||||
'type' => 'Story',
|
||||
'object' => $story->object_id
|
||||
]
|
||||
];
|
||||
|
||||
Helpers::sendSignedObject($actor, $to, $payload);
|
||||
}
|
||||
}
|
|
@ -32,6 +32,6 @@ class ContactAdmin extends Mailable
|
|||
public function build()
|
||||
{
|
||||
$contact = $this->contact;
|
||||
return $this->markdown('emails.contact.admin')->with(compact('contact'));
|
||||
return $this->subject('New Support Message')->markdown('emails.contact.admin')->with(compact('contact'));
|
||||
}
|
||||
}
|
||||
|
|
|
@ -18,6 +18,10 @@ class Media extends Model
|
|||
*/
|
||||
protected $dates = ['deleted_at'];
|
||||
|
||||
protected $casts = [
|
||||
'srcset' => 'array'
|
||||
];
|
||||
|
||||
public function status()
|
||||
{
|
||||
return $this->belongsTo(Status::class);
|
||||
|
|
35
app/Models/Poll.php
Normal file
35
app/Models/Poll.php
Normal file
|
@ -0,0 +1,35 @@
|
|||
<?php
|
||||
|
||||
namespace App\Models;
|
||||
|
||||
use Illuminate\Database\Eloquent\Factories\HasFactory;
|
||||
use Illuminate\Database\Eloquent\Model;
|
||||
use App\HasSnowflakePrimary;
|
||||
|
||||
class Poll extends Model
|
||||
{
|
||||
use HasSnowflakePrimary, HasFactory;
|
||||
|
||||
/**
|
||||
* Indicates if the IDs are auto-incrementing.
|
||||
*
|
||||
* @var bool
|
||||
*/
|
||||
public $incrementing = false;
|
||||
|
||||
protected $casts = [
|
||||
'poll_options' => 'array',
|
||||
'cached_tallies' => 'array',
|
||||
'expires_at' => 'datetime'
|
||||
];
|
||||
|
||||
public function votes()
|
||||
{
|
||||
return $this->hasMany(PollVote::class);
|
||||
}
|
||||
|
||||
public function getTallies()
|
||||
{
|
||||
return $this->cached_tallies;
|
||||
}
|
||||
}
|
11
app/Models/PollVote.php
Normal file
11
app/Models/PollVote.php
Normal file
|
@ -0,0 +1,11 @@
|
|||
<?php
|
||||
|
||||
namespace App\Models;
|
||||
|
||||
use Illuminate\Database\Eloquent\Factories\HasFactory;
|
||||
use Illuminate\Database\Eloquent\Model;
|
||||
|
||||
class PollVote extends Model
|
||||
{
|
||||
use HasFactory;
|
||||
}
|
64
app/Observers/FollowerObserver.php
Normal file
64
app/Observers/FollowerObserver.php
Normal file
|
@ -0,0 +1,64 @@
|
|||
<?php
|
||||
|
||||
namespace App\Observers;
|
||||
|
||||
use App\Follower;
|
||||
use App\Services\FollowerService;
|
||||
|
||||
class FollowerObserver
|
||||
{
|
||||
/**
|
||||
* Handle the Follower "created" event.
|
||||
*
|
||||
* @param \App\Models\Follower $follower
|
||||
* @return void
|
||||
*/
|
||||
public function created(Follower $follower)
|
||||
{
|
||||
FollowerService::add($follower->profile_id, $follower->following_id);
|
||||
}
|
||||
|
||||
/**
|
||||
* Handle the Follower "updated" event.
|
||||
*
|
||||
* @param \App\Models\Follower $follower
|
||||
* @return void
|
||||
*/
|
||||
public function updated(Follower $follower)
|
||||
{
|
||||
FollowerService::add($follower->profile_id, $follower->following_id);
|
||||
}
|
||||
|
||||
/**
|
||||
* Handle the Follower "deleted" event.
|
||||
*
|
||||
* @param \App\Models\Follower $follower
|
||||
* @return void
|
||||
*/
|
||||
public function deleted(Follower $follower)
|
||||
{
|
||||
FollowerService::remove($follower->profile_id, $follower->following_id);
|
||||
}
|
||||
|
||||
/**
|
||||
* Handle the Follower "restored" event.
|
||||
*
|
||||
* @param \App\Models\Follower $follower
|
||||
* @return void
|
||||
*/
|
||||
public function restored(Follower $follower)
|
||||
{
|
||||
FollowerService::add($follower->profile_id, $follower->following_id);
|
||||
}
|
||||
|
||||
/**
|
||||
* Handle the Follower "force deleted" event.
|
||||
*
|
||||
* @param \App\Models\Follower $follower
|
||||
* @return void
|
||||
*/
|
||||
public function forceDeleted(Follower $follower)
|
||||
{
|
||||
FollowerService::remove($follower->profile_id, $follower->following_id);
|
||||
}
|
||||
}
|
|
@ -3,7 +3,6 @@
|
|||
namespace App;
|
||||
|
||||
use Illuminate\Database\Eloquent\Model;
|
||||
use Pixelfed\Snowflake\HasSnowflakePrimary;
|
||||
|
||||
class Place extends Model
|
||||
{
|
||||
|
|
547
app/Profile.php
547
app/Profile.php
|
@ -2,333 +2,326 @@
|
|||
|
||||
namespace App;
|
||||
|
||||
use Auth, Cache, Storage;
|
||||
use Auth, Cache, DB, Storage;
|
||||
use App\Util\Lexer\PrettyNumber;
|
||||
use Pixelfed\Snowflake\HasSnowflakePrimary;
|
||||
use App\HasSnowflakePrimary;
|
||||
use Illuminate\Database\Eloquent\{Model, SoftDeletes};
|
||||
use App\Services\FollowerService;
|
||||
|
||||
class Profile extends Model
|
||||
{
|
||||
use HasSnowflakePrimary, SoftDeletes;
|
||||
use HasSnowflakePrimary, SoftDeletes;
|
||||
|
||||
/**
|
||||
* Indicates if the IDs are auto-incrementing.
|
||||
*
|
||||
* @var bool
|
||||
*/
|
||||
public $incrementing = false;
|
||||
|
||||
protected $dates = [
|
||||
'deleted_at',
|
||||
'last_fetched_at'
|
||||
];
|
||||
protected $hidden = ['private_key'];
|
||||
protected $visible = ['id', 'user_id', 'username', 'name'];
|
||||
protected $fillable = ['user_id'];
|
||||
/**
|
||||
* Indicates if the IDs are auto-incrementing.
|
||||
*
|
||||
* @var bool
|
||||
*/
|
||||
public $incrementing = false;
|
||||
|
||||
public function user()
|
||||
{
|
||||
return $this->belongsTo(User::class);
|
||||
}
|
||||
protected $dates = [
|
||||
'deleted_at',
|
||||
'last_fetched_at'
|
||||
];
|
||||
protected $hidden = ['private_key'];
|
||||
protected $visible = ['id', 'user_id', 'username', 'name'];
|
||||
protected $fillable = ['user_id'];
|
||||
|
||||
public function url($suffix = null)
|
||||
{
|
||||
return $this->remote_url ?? url($this->username . $suffix);
|
||||
}
|
||||
public function user()
|
||||
{
|
||||
return $this->belongsTo(User::class);
|
||||
}
|
||||
|
||||
public function localUrl($suffix = null)
|
||||
{
|
||||
return url($this->username . $suffix);
|
||||
}
|
||||
public function url($suffix = null)
|
||||
{
|
||||
return $this->remote_url ?? url($this->username . $suffix);
|
||||
}
|
||||
|
||||
public function permalink($suffix = null)
|
||||
{
|
||||
return $this->remote_url ?? url('users/' . $this->username . $suffix);
|
||||
}
|
||||
public function localUrl($suffix = null)
|
||||
{
|
||||
return url($this->username . $suffix);
|
||||
}
|
||||
|
||||
public function emailUrl()
|
||||
{
|
||||
if($this->domain) {
|
||||
return $this->username;
|
||||
}
|
||||
|
||||
$domain = parse_url(config('app.url'), PHP_URL_HOST);
|
||||
public function permalink($suffix = null)
|
||||
{
|
||||
return $this->remote_url ?? url('users/' . $this->username . $suffix);
|
||||
}
|
||||
|
||||
return $this->username.'@'.$domain;
|
||||
}
|
||||
public function emailUrl()
|
||||
{
|
||||
if($this->domain) {
|
||||
return $this->username;
|
||||
}
|
||||
|
||||
public function statuses()
|
||||
{
|
||||
return $this->hasMany(Status::class);
|
||||
}
|
||||
$domain = parse_url(config('app.url'), PHP_URL_HOST);
|
||||
|
||||
public function followingCount($short = false)
|
||||
{
|
||||
$count = Cache::remember('profile:following_count:'.$this->id, now()->addMonths(1), function() {
|
||||
if($this->domain == null && $this->user->settings->show_profile_following_count == false) {
|
||||
return 0;
|
||||
}
|
||||
$count = $this->following()->count();
|
||||
if($this->following_count != $count) {
|
||||
$this->following_count = $count;
|
||||
$this->save();
|
||||
}
|
||||
return $count;
|
||||
});
|
||||
return $this->username.'@'.$domain;
|
||||
}
|
||||
|
||||
return $short ? PrettyNumber::convert($count) : $count;
|
||||
}
|
||||
public function statuses()
|
||||
{
|
||||
return $this->hasMany(Status::class);
|
||||
}
|
||||
|
||||
public function followerCount($short = false)
|
||||
{
|
||||
$count = Cache::remember('profile:follower_count:'.$this->id, now()->addMonths(1), function() {
|
||||
if($this->domain == null && $this->user->settings->show_profile_follower_count == false) {
|
||||
return 0;
|
||||
}
|
||||
$count = $this->followers()->count();
|
||||
if($this->followers_count != $count) {
|
||||
$this->followers_count = $count;
|
||||
$this->save();
|
||||
}
|
||||
return $count;
|
||||
});
|
||||
return $short ? PrettyNumber::convert($count) : $count;
|
||||
}
|
||||
public function followingCount($short = false)
|
||||
{
|
||||
$count = Cache::remember('profile:following_count:'.$this->id, now()->addMonths(1), function() {
|
||||
if($this->domain == null && $this->user->settings->show_profile_following_count == false) {
|
||||
return 0;
|
||||
}
|
||||
$count = DB::table('followers')->select('following_id')->where('following_id', $this->id)->count();
|
||||
if($this->following_count != $count) {
|
||||
$this->following_count = $count;
|
||||
$this->save();
|
||||
}
|
||||
return $count;
|
||||
});
|
||||
|
||||
public function statusCount()
|
||||
{
|
||||
return $this->status_count;
|
||||
}
|
||||
return $short ? PrettyNumber::convert($count) : $count;
|
||||
}
|
||||
|
||||
public function following()
|
||||
{
|
||||
return $this->belongsToMany(
|
||||
self::class,
|
||||
'followers',
|
||||
'profile_id',
|
||||
'following_id'
|
||||
);
|
||||
}
|
||||
public function followerCount($short = false)
|
||||
{
|
||||
$count = Cache::remember('profile:follower_count:'.$this->id, now()->addMonths(1), function() {
|
||||
if($this->domain == null && $this->user->settings->show_profile_follower_count == false) {
|
||||
return 0;
|
||||
}
|
||||
$count = $this->followers()->count();
|
||||
if($this->followers_count != $count) {
|
||||
$this->followers_count = $count;
|
||||
$this->save();
|
||||
}
|
||||
return $count;
|
||||
});
|
||||
return $short ? PrettyNumber::convert($count) : $count;
|
||||
}
|
||||
|
||||
public function followers()
|
||||
{
|
||||
return $this->belongsToMany(
|
||||
self::class,
|
||||
'followers',
|
||||
'following_id',
|
||||
'profile_id'
|
||||
);
|
||||
}
|
||||
public function statusCount()
|
||||
{
|
||||
return $this->status_count;
|
||||
}
|
||||
|
||||
public function follows($profile) : bool
|
||||
{
|
||||
return Follower::whereProfileId($this->id)->whereFollowingId($profile->id)->exists();
|
||||
}
|
||||
public function following()
|
||||
{
|
||||
return $this->belongsToMany(
|
||||
self::class,
|
||||
'followers',
|
||||
'profile_id',
|
||||
'following_id'
|
||||
);
|
||||
}
|
||||
|
||||
public function followedBy($profile) : bool
|
||||
{
|
||||
return Follower::whereProfileId($profile->id)->whereFollowingId($this->id)->exists();
|
||||
}
|
||||
public function followers()
|
||||
{
|
||||
return $this->belongsToMany(
|
||||
self::class,
|
||||
'followers',
|
||||
'following_id',
|
||||
'profile_id'
|
||||
);
|
||||
}
|
||||
|
||||
public function bookmarks()
|
||||
{
|
||||
return $this->belongsToMany(
|
||||
Status::class,
|
||||
'bookmarks',
|
||||
'profile_id',
|
||||
'status_id'
|
||||
);
|
||||
}
|
||||
public function follows($profile) : bool
|
||||
{
|
||||
return Follower::whereProfileId($this->id)->whereFollowingId($profile->id)->exists();
|
||||
}
|
||||
|
||||
public function likes()
|
||||
{
|
||||
return $this->hasMany(Like::class);
|
||||
}
|
||||
public function followedBy($profile) : bool
|
||||
{
|
||||
return Follower::whereProfileId($profile->id)->whereFollowingId($this->id)->exists();
|
||||
}
|
||||
|
||||
public function avatar()
|
||||
{
|
||||
return $this->hasOne(Avatar::class)->withDefault([
|
||||
'media_path' => 'public/avatars/default.jpg',
|
||||
'change_count' => 0
|
||||
]);
|
||||
}
|
||||
public function bookmarks()
|
||||
{
|
||||
return $this->belongsToMany(
|
||||
Status::class,
|
||||
'bookmarks',
|
||||
'profile_id',
|
||||
'status_id'
|
||||
);
|
||||
}
|
||||
|
||||
public function avatarUrl()
|
||||
{
|
||||
$url = Cache::remember('avatar:'.$this->id, now()->addYears(1), function () {
|
||||
$avatar = $this->avatar;
|
||||
public function likes()
|
||||
{
|
||||
return $this->hasMany(Like::class);
|
||||
}
|
||||
|
||||
if($avatar->cdn_url) {
|
||||
return $avatar->cdn_url ?? url('/storage/avatars/default.jpg');
|
||||
}
|
||||
public function avatar()
|
||||
{
|
||||
return $this->hasOne(Avatar::class)->withDefault([
|
||||
'media_path' => 'public/avatars/default.jpg',
|
||||
'change_count' => 0
|
||||
]);
|
||||
}
|
||||
|
||||
if($avatar->is_remote) {
|
||||
return $avatar->cdn_url ?? url('/storage/avatars/default.jpg');
|
||||
}
|
||||
|
||||
$path = $avatar->media_path;
|
||||
$path = "{$path}?v={$avatar->change_count}";
|
||||
public function avatarUrl()
|
||||
{
|
||||
$url = Cache::remember('avatar:'.$this->id, now()->addYears(1), function () {
|
||||
$avatar = $this->avatar;
|
||||
|
||||
return config('app.url') . Storage::url($path);
|
||||
});
|
||||
if($avatar->cdn_url) {
|
||||
return $avatar->cdn_url ?? url('/storage/avatars/default.jpg');
|
||||
}
|
||||
|
||||
return $url;
|
||||
}
|
||||
if($avatar->is_remote) {
|
||||
return $avatar->cdn_url ?? url('/storage/avatars/default.jpg');
|
||||
}
|
||||
|
||||
$path = $avatar->media_path;
|
||||
$path = "{$path}?v={$avatar->change_count}";
|
||||
|
||||
// deprecated
|
||||
public function recommendFollowers()
|
||||
{
|
||||
return collect([]);
|
||||
}
|
||||
return config('app.url') . Storage::url($path);
|
||||
});
|
||||
|
||||
public function keyId()
|
||||
{
|
||||
if ($this->remote_url) {
|
||||
return;
|
||||
}
|
||||
return $url;
|
||||
}
|
||||
|
||||
return $this->permalink('#main-key');
|
||||
}
|
||||
// deprecated
|
||||
public function recommendFollowers()
|
||||
{
|
||||
return collect([]);
|
||||
}
|
||||
|
||||
public function mutedIds()
|
||||
{
|
||||
return UserFilter::whereUserId($this->id)
|
||||
->whereFilterableType('App\Profile')
|
||||
->whereFilterType('mute')
|
||||
->pluck('filterable_id');
|
||||
}
|
||||
public function keyId()
|
||||
{
|
||||
if ($this->remote_url) {
|
||||
return;
|
||||
}
|
||||
|
||||
public function blockedIds()
|
||||
{
|
||||
return UserFilter::whereUserId($this->id)
|
||||
->whereFilterableType('App\Profile')
|
||||
->whereFilterType('block')
|
||||
->pluck('filterable_id');
|
||||
}
|
||||
return $this->permalink('#main-key');
|
||||
}
|
||||
|
||||
public function mutedProfileUrls()
|
||||
{
|
||||
$ids = $this->mutedIds();
|
||||
return $this->whereIn('id', $ids)->get()->map(function($i) {
|
||||
return $i->url();
|
||||
});
|
||||
}
|
||||
public function mutedIds()
|
||||
{
|
||||
return UserFilter::whereUserId($this->id)
|
||||
->whereFilterableType('App\Profile')
|
||||
->whereFilterType('mute')
|
||||
->pluck('filterable_id');
|
||||
}
|
||||
|
||||
public function blockedProfileUrls()
|
||||
{
|
||||
$ids = $this->blockedIds();
|
||||
return $this->whereIn('id', $ids)->get()->map(function($i) {
|
||||
return $i->url();
|
||||
});
|
||||
}
|
||||
public function blockedIds()
|
||||
{
|
||||
return UserFilter::whereUserId($this->id)
|
||||
->whereFilterableType('App\Profile')
|
||||
->whereFilterType('block')
|
||||
->pluck('filterable_id');
|
||||
}
|
||||
|
||||
public function reports()
|
||||
{
|
||||
return $this->hasMany(Report::class, 'profile_id');
|
||||
}
|
||||
public function mutedProfileUrls()
|
||||
{
|
||||
$ids = $this->mutedIds();
|
||||
return $this->whereIn('id', $ids)->get()->map(function($i) {
|
||||
return $i->url();
|
||||
});
|
||||
}
|
||||
|
||||
public function media()
|
||||
{
|
||||
return $this->hasMany(Media::class, 'profile_id');
|
||||
}
|
||||
public function blockedProfileUrls()
|
||||
{
|
||||
$ids = $this->blockedIds();
|
||||
return $this->whereIn('id', $ids)->get()->map(function($i) {
|
||||
return $i->url();
|
||||
});
|
||||
}
|
||||
|
||||
public function inboxUrl()
|
||||
{
|
||||
return $this->inbox_url ?? $this->permalink('/inbox');
|
||||
}
|
||||
public function reports()
|
||||
{
|
||||
return $this->hasMany(Report::class, 'profile_id');
|
||||
}
|
||||
|
||||
public function outboxUrl()
|
||||
{
|
||||
return $this->outbox_url ?? $this->permalink('/outbox');
|
||||
}
|
||||
public function media()
|
||||
{
|
||||
return $this->hasMany(Media::class, 'profile_id');
|
||||
}
|
||||
|
||||
public function sharedInbox()
|
||||
{
|
||||
return $this->sharedInbox ?? $this->inboxUrl();
|
||||
}
|
||||
public function inboxUrl()
|
||||
{
|
||||
return $this->inbox_url ?? $this->permalink('/inbox');
|
||||
}
|
||||
|
||||
public function getDefaultScope()
|
||||
{
|
||||
return $this->is_private == true ? 'private' : 'public';
|
||||
}
|
||||
public function outboxUrl()
|
||||
{
|
||||
return $this->outbox_url ?? $this->permalink('/outbox');
|
||||
}
|
||||
|
||||
public function getAudience($scope = false)
|
||||
{
|
||||
if($this->remote_url) {
|
||||
return [];
|
||||
}
|
||||
$scope = $scope ?? $this->getDefaultScope();
|
||||
$audience = [];
|
||||
switch ($scope) {
|
||||
case 'public':
|
||||
$audience = [
|
||||
'to' => [
|
||||
'https://www.w3.org/ns/activitystreams#Public'
|
||||
],
|
||||
'cc' => [
|
||||
$this->permalink('/followers')
|
||||
]
|
||||
];
|
||||
break;
|
||||
}
|
||||
return $audience;
|
||||
}
|
||||
public function sharedInbox()
|
||||
{
|
||||
return $this->sharedInbox ?? $this->inboxUrl();
|
||||
}
|
||||
|
||||
public function getAudienceInbox($scope = 'public')
|
||||
{
|
||||
return $this
|
||||
->followers()
|
||||
->whereLocalProfile(false)
|
||||
->get()
|
||||
->map(function($follow) {
|
||||
return $follow->sharedInbox ?? $follow->inbox_url;
|
||||
})
|
||||
->unique()
|
||||
->toArray();
|
||||
}
|
||||
public function getDefaultScope()
|
||||
{
|
||||
return $this->is_private == true ? 'private' : 'public';
|
||||
}
|
||||
|
||||
public function circles()
|
||||
{
|
||||
return $this->hasMany(Circle::class);
|
||||
}
|
||||
public function getAudience($scope = false)
|
||||
{
|
||||
if($this->remote_url) {
|
||||
return [];
|
||||
}
|
||||
$scope = $scope ?? $this->getDefaultScope();
|
||||
$audience = [];
|
||||
switch ($scope) {
|
||||
case 'public':
|
||||
$audience = [
|
||||
'to' => [
|
||||
'https://www.w3.org/ns/activitystreams#Public'
|
||||
],
|
||||
'cc' => [
|
||||
$this->permalink('/followers')
|
||||
]
|
||||
];
|
||||
break;
|
||||
}
|
||||
return $audience;
|
||||
}
|
||||
|
||||
public function hashtags()
|
||||
{
|
||||
return $this->hasManyThrough(
|
||||
Hashtag::class,
|
||||
StatusHashtag::class,
|
||||
'profile_id',
|
||||
'id',
|
||||
'id',
|
||||
'hashtag_id'
|
||||
);
|
||||
}
|
||||
public function getAudienceInbox($scope = 'public')
|
||||
{
|
||||
return FollowerService::audience($this->id, $scope);
|
||||
}
|
||||
|
||||
public function hashtagFollowing()
|
||||
{
|
||||
return $this->hasMany(HashtagFollow::class);
|
||||
}
|
||||
public function circles()
|
||||
{
|
||||
return $this->hasMany(Circle::class);
|
||||
}
|
||||
|
||||
public function collections()
|
||||
{
|
||||
return $this->hasMany(Collection::class);
|
||||
}
|
||||
public function hashtags()
|
||||
{
|
||||
return $this->hasManyThrough(
|
||||
Hashtag::class,
|
||||
StatusHashtag::class,
|
||||
'profile_id',
|
||||
'id',
|
||||
'id',
|
||||
'hashtag_id'
|
||||
);
|
||||
}
|
||||
|
||||
public function hasFollowRequestById(int $id)
|
||||
{
|
||||
return FollowRequest::whereFollowerId($id)
|
||||
->whereFollowingId($this->id)
|
||||
->exists();
|
||||
}
|
||||
public function hashtagFollowing()
|
||||
{
|
||||
return $this->hasMany(HashtagFollow::class);
|
||||
}
|
||||
|
||||
public function stories()
|
||||
{
|
||||
return $this->hasMany(Story::class);
|
||||
}
|
||||
public function collections()
|
||||
{
|
||||
return $this->hasMany(Collection::class);
|
||||
}
|
||||
|
||||
public function hasFollowRequestById(int $id)
|
||||
{
|
||||
return FollowRequest::whereFollowerId($id)
|
||||
->whereFollowingId($this->id)
|
||||
->exists();
|
||||
}
|
||||
|
||||
public function stories()
|
||||
{
|
||||
return $this->hasMany(Story::class);
|
||||
}
|
||||
|
||||
|
||||
public function reported()
|
||||
{
|
||||
return $this->hasMany(Report::class, 'object_id');
|
||||
}
|
||||
public function reported()
|
||||
{
|
||||
return $this->hasMany(Report::class, 'object_id');
|
||||
}
|
||||
}
|
||||
|
|
|
@ -4,6 +4,7 @@ namespace App\Providers;
|
|||
|
||||
use App\Observers\{
|
||||
AvatarObserver,
|
||||
FollowerObserver,
|
||||
LikeObserver,
|
||||
NotificationObserver,
|
||||
ModLogObserver,
|
||||
|
@ -14,6 +15,7 @@ use App\Observers\{
|
|||
};
|
||||
use App\{
|
||||
Avatar,
|
||||
Follower,
|
||||
Like,
|
||||
Notification,
|
||||
ModLog,
|
||||
|
@ -48,6 +50,7 @@ class AppServiceProvider extends ServiceProvider
|
|||
StatusHashtag::observe(StatusHashtagObserver::class);
|
||||
User::observe(UserObserver::class);
|
||||
UserFilter::observe(UserFilterObserver::class);
|
||||
Follower::observe(FollowerObserver::class);
|
||||
Horizon::auth(function ($request) {
|
||||
return Auth::check() && $request->user()->is_admin;
|
||||
});
|
||||
|
|
|
@ -4,12 +4,13 @@ namespace App\Services;
|
|||
|
||||
use Cache;
|
||||
use App\Profile;
|
||||
use App\Status;
|
||||
use App\Transformer\Api\AccountTransformer;
|
||||
use League\Fractal;
|
||||
use League\Fractal\Serializer\ArraySerializer;
|
||||
|
||||
class AccountService {
|
||||
|
||||
class AccountService
|
||||
{
|
||||
const CACHE_KEY = 'pf:services:account:';
|
||||
|
||||
public static function get($id)
|
||||
|
@ -19,7 +20,7 @@ class AccountService {
|
|||
}
|
||||
|
||||
$key = self::CACHE_KEY . $id;
|
||||
$ttl = now()->addMinutes(15);
|
||||
$ttl = now()->addHours(12);
|
||||
|
||||
return Cache::remember($key, $ttl, function() use($id) {
|
||||
$fractal = new Fractal\Manager();
|
||||
|
@ -35,4 +36,30 @@ class AccountService {
|
|||
return Cache::forget(self::CACHE_KEY . $id);
|
||||
}
|
||||
|
||||
}
|
||||
public static function syncPostCount($id)
|
||||
{
|
||||
$profile = Profile::find($id);
|
||||
|
||||
if(!$profile) {
|
||||
return false;
|
||||
}
|
||||
|
||||
$key = self::CACHE_KEY . 'pcs:' . $id;
|
||||
|
||||
if(Cache::has($key)) {
|
||||
return;
|
||||
}
|
||||
|
||||
$count = Status::whereProfileId($id)
|
||||
->whereNull('in_reply_to_id')
|
||||
->whereNull('reblog_of_id')
|
||||
->whereIn('scope', ['public', 'unlisted', 'private'])
|
||||
->count();
|
||||
|
||||
$profile->status_count = $count;
|
||||
$profile->save();
|
||||
|
||||
Cache::put($key, 1, 900);
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
|
|
@ -3,7 +3,7 @@
|
|||
namespace App\Services;
|
||||
|
||||
use Illuminate\Support\Facades\Redis;
|
||||
|
||||
use Cache;
|
||||
use App\{
|
||||
Follower,
|
||||
Profile,
|
||||
|
@ -25,6 +25,8 @@ class FollowerService
|
|||
{
|
||||
Redis::zrem(self::FOLLOWING_KEY . $actor, $target);
|
||||
Redis::zrem(self::FOLLOWERS_KEY . $target, $actor);
|
||||
Cache::forget('pf:services:follow:audience:' . $actor);
|
||||
Cache::forget('pf:services:follow:audience:' . $target);
|
||||
}
|
||||
|
||||
public static function followers($id, $start = 0, $stop = 10)
|
||||
|
@ -42,46 +44,34 @@ class FollowerService
|
|||
return Follower::whereProfileId($actor)->whereFollowingId($target)->exists();
|
||||
}
|
||||
|
||||
public static function audience($profile)
|
||||
public static function audience($profile, $scope = null)
|
||||
{
|
||||
return (new self)->getAudienceInboxes($profile);
|
||||
return (new self)->getAudienceInboxes($profile, $scope);
|
||||
}
|
||||
|
||||
protected function getAudienceInboxes($profile)
|
||||
public static function softwareAudience($profile, $software = 'pixelfed')
|
||||
{
|
||||
if($profile instanceOf User) {
|
||||
return $profile
|
||||
->profile
|
||||
->followers()
|
||||
->whereLocalProfile(false)
|
||||
->get()
|
||||
->map(function($follow) {
|
||||
return $follow->sharedInbox ?? $follow->inbox_url;
|
||||
})
|
||||
->unique()
|
||||
->values()
|
||||
->toArray();
|
||||
}
|
||||
return collect(self::audience($profile))
|
||||
->filter(function($inbox) use($software) {
|
||||
$domain = parse_url($inbox, PHP_URL_HOST);
|
||||
if(!$domain) {
|
||||
return false;
|
||||
}
|
||||
return InstanceService::software($domain) === strtolower($software);
|
||||
})
|
||||
->unique()
|
||||
->values()
|
||||
->toArray();
|
||||
}
|
||||
|
||||
if($profile instanceOf Profile) {
|
||||
return $profile
|
||||
->followers()
|
||||
->whereLocalProfile(false)
|
||||
->get()
|
||||
->map(function($follow) {
|
||||
return $follow->sharedInbox ?? $follow->inbox_url;
|
||||
})
|
||||
->unique()
|
||||
->values()
|
||||
->toArray();
|
||||
}
|
||||
|
||||
if(is_string($profile) || is_integer($profile)) {
|
||||
$profile = Profile::whereNull('domain')->find($profile);
|
||||
protected function getAudienceInboxes($pid, $scope = null)
|
||||
{
|
||||
$key = 'pf:services:follow:audience:' . $pid;
|
||||
return Cache::remember($key, 86400, function() use($pid) {
|
||||
$profile = Profile::find($pid);
|
||||
if(!$profile) {
|
||||
return [];
|
||||
}
|
||||
|
||||
return $profile
|
||||
->followers()
|
||||
->whereLocalProfile(false)
|
||||
|
@ -92,9 +82,7 @@ class FollowerService
|
|||
->unique()
|
||||
->values()
|
||||
->toArray();
|
||||
}
|
||||
|
||||
return [];
|
||||
});
|
||||
}
|
||||
|
||||
}
|
||||
|
|
|
@ -27,4 +27,16 @@ class InstanceService
|
|||
return Instance::whereAutoCw(true)->pluck('domain')->toArray();
|
||||
});
|
||||
}
|
||||
|
||||
public static function software($domain)
|
||||
{
|
||||
$key = 'instances:software:' . strtolower($domain);
|
||||
return Cache::remember($key, 86400, function() use($domain) {
|
||||
$instance = Instance::whereDomain($domain)->first();
|
||||
if(!$instance) {
|
||||
return;
|
||||
}
|
||||
return $instance->software;
|
||||
});
|
||||
}
|
||||
}
|
||||
|
|
|
@ -80,4 +80,9 @@ class LikeService {
|
|||
|
||||
return $res;
|
||||
}
|
||||
|
||||
public static function count($id)
|
||||
{
|
||||
return Like::whereStatusId($id)->count();
|
||||
}
|
||||
}
|
||||
|
|
|
@ -8,6 +8,7 @@ use Illuminate\Support\Str;
|
|||
use App\Media;
|
||||
use App\Profile;
|
||||
use App\User;
|
||||
use App\Services\HashidService;
|
||||
|
||||
class MediaPathService {
|
||||
|
||||
|
@ -51,27 +52,27 @@ class MediaPathService {
|
|||
public static function story($account, $version = 1)
|
||||
{
|
||||
$mh = hash('sha256', date('Y').'-.-'.date('m'));
|
||||
$monthHash = date('Y').date('m').substr($mh, 0, 6).substr($mh, 58, 6);
|
||||
$random = '03'.Str::random(random_int(6,9)).'_'.Str::random(random_int(6,17));
|
||||
$monthHash = HashidService::encode(date('Y').date('m'));
|
||||
$random = date('d').Str::random(32);
|
||||
|
||||
if($account instanceOf User) {
|
||||
switch ($version) {
|
||||
case 1:
|
||||
$userHash = $account->profile_id;
|
||||
$userHash = HashidService::encode($account->profile_id);
|
||||
$path = "public/_esm.t3/{$monthHash}/{$userHash}/{$random}";
|
||||
break;
|
||||
|
||||
default:
|
||||
$userHash = $account->profile_id;
|
||||
$userHash = HashidService::encode($account->profile_id);
|
||||
$path = "public/_esm.t3/{$monthHash}/{$userHash}/{$random}";
|
||||
break;
|
||||
}
|
||||
}
|
||||
if($account instanceOf Profile) {
|
||||
$userHash = $account->id;
|
||||
$userHash = HashidService::encode($account->id);
|
||||
$path = "public/_esm.t3/{$monthHash}/{$userHash}/{$random}";
|
||||
}
|
||||
return $path;
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
|
|
62
app/Services/MediaService.php
Normal file
62
app/Services/MediaService.php
Normal file
|
@ -0,0 +1,62 @@
|
|||
<?php
|
||||
|
||||
namespace App\Services;
|
||||
|
||||
use Cache;
|
||||
use Illuminate\Support\Facades\Redis;
|
||||
use App\Media;
|
||||
use App\Status;
|
||||
use League\Fractal;
|
||||
use League\Fractal\Serializer\ArraySerializer;
|
||||
use League\Fractal\Pagination\IlluminatePaginatorAdapter;
|
||||
use App\Transformer\Api\MediaTransformer;
|
||||
use App\Util\Media\License;
|
||||
|
||||
class MediaService
|
||||
{
|
||||
const CACHE_KEY = 'status:transformer:media:attachments:';
|
||||
|
||||
public static function get($statusId)
|
||||
{
|
||||
$status = Status::find($statusId);
|
||||
$ttl = $status->created_at->lt(now()->subMinutes(30)) ? 129600 : 30;
|
||||
return Cache::remember(self::CACHE_KEY.$statusId, $ttl, function() use($status) {
|
||||
if(!$status) {
|
||||
return [];
|
||||
}
|
||||
if(in_array($status->type, ['group:post', 'photo', 'video', 'video:album', 'photo:album', 'loop', 'photo:video:album'])) {
|
||||
$media = Media::whereStatusId($status->id)->orderBy('order')->get();
|
||||
$fractal = new Fractal\Manager();
|
||||
$fractal->setSerializer(new ArraySerializer());
|
||||
$resource = new Fractal\Resource\Collection($media, new MediaTransformer());
|
||||
return $fractal->createData($resource)->toArray();
|
||||
}
|
||||
return [];
|
||||
});
|
||||
}
|
||||
|
||||
public static function del($statusId)
|
||||
{
|
||||
return Cache::forget(self::CACHE_KEY . $statusId);
|
||||
}
|
||||
|
||||
public static function activitypub($statusId)
|
||||
{
|
||||
$status = self::get($statusId);
|
||||
if(!$status) {
|
||||
return [];
|
||||
}
|
||||
|
||||
return collect($status)->map(function($s) {
|
||||
$license = isset($s['license']) && $s['license']['title'] ? $s['license']['title'] : null;
|
||||
return [
|
||||
'type' => 'Document',
|
||||
'mediaType' => $s['mime'],
|
||||
'url' => $s['url'],
|
||||
'name' => $s['description'],
|
||||
'blurhash' => $s['blurhash'],
|
||||
'license' => $license
|
||||
];
|
||||
});
|
||||
}
|
||||
}
|
|
@ -70,7 +70,7 @@ class MediaStorageService {
|
|||
protected function localToCloud($media)
|
||||
{
|
||||
$path = storage_path('app/'.$media->media_path);
|
||||
$thumb = storage_path('app/'.$media->thumbnail_path);
|
||||
$thumb = storage_path('app/'.$media->thumbnail_path);
|
||||
|
||||
$p = explode('/', $media->media_path);
|
||||
$name = array_pop($p);
|
||||
|
|
76
app/Services/NodeinfoService.php
Normal file
76
app/Services/NodeinfoService.php
Normal file
|
@ -0,0 +1,76 @@
|
|||
<?php
|
||||
|
||||
namespace App\Services;
|
||||
|
||||
use Illuminate\Support\Str;
|
||||
use Illuminate\Support\Facades\Http;
|
||||
use Illuminate\Http\Client\RequestException;
|
||||
use Illuminate\Http\Client\ConnectionException;
|
||||
|
||||
class NodeinfoService
|
||||
{
|
||||
public static function get($domain)
|
||||
{
|
||||
$version = config('pixelfed.version');
|
||||
$appUrl = config('app.url');
|
||||
$headers = [
|
||||
'Accept' => 'application/json',
|
||||
'User-Agent' => "(Pixelfed/{$version}; +{$appUrl})",
|
||||
];
|
||||
|
||||
$url = 'https://' . $domain;
|
||||
$wk = $url . '/.well-known/nodeinfo';
|
||||
|
||||
try {
|
||||
$res = Http::withHeaders($headers)
|
||||
->timeout(5)
|
||||
->get($wk);
|
||||
} catch (RequestException $e) {
|
||||
return false;
|
||||
} catch (ConnectionException $e) {
|
||||
return false;
|
||||
} catch (\Exception $e) {
|
||||
return false;
|
||||
}
|
||||
|
||||
if(!$res) {
|
||||
return false;
|
||||
}
|
||||
|
||||
$json = $res->json();
|
||||
|
||||
if( !isset($json['links'])) {
|
||||
return false;
|
||||
}
|
||||
|
||||
if(is_array($json['links'])) {
|
||||
if(isset($json['links']['href'])) {
|
||||
$href = $json['links']['href'];
|
||||
} else {
|
||||
$href = $json['links'][0]['href'];
|
||||
}
|
||||
} else {
|
||||
return false;
|
||||
}
|
||||
|
||||
$domain = parse_url($url, PHP_URL_HOST);
|
||||
$hrefDomain = parse_url($href, PHP_URL_HOST);
|
||||
|
||||
if($domain !== $hrefDomain) {
|
||||
return 60;
|
||||
}
|
||||
|
||||
try {
|
||||
$res = Http::withHeaders($headers)
|
||||
->timeout(5)
|
||||
->get($href);
|
||||
} catch (RequestException $e) {
|
||||
return false;
|
||||
} catch (ConnectionException $e) {
|
||||
return false;
|
||||
} catch (\Exception $e) {
|
||||
return false;
|
||||
}
|
||||
return $res->json();
|
||||
}
|
||||
}
|
|
@ -46,7 +46,7 @@ class NotificationService {
|
|||
return $ids;
|
||||
}
|
||||
|
||||
public static function getMax($id = false, $start, $limit = 10)
|
||||
public static function getMax($id = false, $start = 0, $limit = 10)
|
||||
{
|
||||
$ids = self::getRankedMaxId($id, $start, $limit);
|
||||
|
||||
|
@ -61,7 +61,7 @@ class NotificationService {
|
|||
return $res->toArray();
|
||||
}
|
||||
|
||||
public static function getMin($id = false, $start, $limit = 10)
|
||||
public static function getMin($id = false, $start = 0, $limit = 10)
|
||||
{
|
||||
$ids = self::getRankedMinId($id, $start, $limit);
|
||||
|
||||
|
|
97
app/Services/PollService.php
Normal file
97
app/Services/PollService.php
Normal file
|
@ -0,0 +1,97 @@
|
|||
<?php
|
||||
|
||||
namespace App\Services;
|
||||
|
||||
use App\Models\Poll;
|
||||
use App\Models\PollVote;
|
||||
use App\Status;
|
||||
use Illuminate\Support\Facades\Cache;
|
||||
|
||||
class PollService
|
||||
{
|
||||
const CACHE_KEY = 'pf:services:poll:status_id:';
|
||||
|
||||
public static function get($id, $profileId = false)
|
||||
{
|
||||
$key = self::CACHE_KEY . $id;
|
||||
|
||||
$res = Cache::remember($key, 1800, function() use($id) {
|
||||
$poll = Poll::whereStatusId($id)->firstOrFail();
|
||||
return [
|
||||
'id' => (string) $poll->id,
|
||||
'expires_at' => $poll->expires_at->format('c'),
|
||||
'expired' => null,
|
||||
'multiple' => $poll->multiple,
|
||||
'votes_count' => $poll->votes_count,
|
||||
'voters_count' => null,
|
||||
'voted' => false,
|
||||
'own_votes' => [],
|
||||
'options' => collect($poll->poll_options)->map(function($option, $key) use($poll) {
|
||||
$tally = $poll->cached_tallies && isset($poll->cached_tallies[$key]) ? $poll->cached_tallies[$key] : 0;
|
||||
return [
|
||||
'title' => $option,
|
||||
'votes_count' => $tally
|
||||
];
|
||||
})->toArray(),
|
||||
'emojis' => []
|
||||
];
|
||||
});
|
||||
|
||||
if($profileId) {
|
||||
$res['voted'] = self::voted($id, $profileId);
|
||||
$res['own_votes'] = self::ownVotes($id, $profileId);
|
||||
}
|
||||
|
||||
return $res;
|
||||
}
|
||||
|
||||
public static function getById($id, $pid)
|
||||
{
|
||||
$poll = Poll::findOrFail($id);
|
||||
return self::get($poll->status_id, $pid);
|
||||
}
|
||||
|
||||
public static function del($id)
|
||||
{
|
||||
Cache::forget(self::CACHE_KEY . $id);
|
||||
}
|
||||
|
||||
public static function voted($id, $profileId = false)
|
||||
{
|
||||
return !$profileId ? false : PollVote::whereStatusId($id)
|
||||
->whereProfileId($profileId)
|
||||
->exists();
|
||||
}
|
||||
|
||||
public static function votedStory($id, $profileId = false)
|
||||
{
|
||||
return !$profileId ? false : PollVote::whereStoryId($id)
|
||||
->whereProfileId($profileId)
|
||||
->exists();
|
||||
}
|
||||
|
||||
public static function storyResults($sid)
|
||||
{
|
||||
$key = self::CACHE_KEY . 'story_poll_results:' . $sid;
|
||||
return Cache::remember($key, 60, function() use($sid) {
|
||||
return Poll::whereStoryId($sid)
|
||||
->firstOrFail()
|
||||
->cached_tallies;
|
||||
});
|
||||
}
|
||||
|
||||
public static function storyChoice($id, $profileId = false)
|
||||
{
|
||||
return !$profileId ? false : PollVote::whereStoryId($id)
|
||||
->whereProfileId($profileId)
|
||||
->pluck('choice')
|
||||
->first();
|
||||
}
|
||||
|
||||
public static function ownVotes($id, $profileId = false)
|
||||
{
|
||||
return !$profileId ? [] : PollVote::whereStatusId($id)
|
||||
->whereProfileId($profileId)
|
||||
->pluck('choice') ?? [];
|
||||
}
|
||||
}
|
|
@ -2,31 +2,15 @@
|
|||
|
||||
namespace App\Services;
|
||||
|
||||
use Cache;
|
||||
use Illuminate\Support\Facades\Redis;
|
||||
use App\Transformer\Api\AccountTransformer;
|
||||
use League\Fractal;
|
||||
use League\Fractal\Serializer\ArraySerializer;
|
||||
use League\Fractal\Pagination\IlluminatePaginatorAdapter;
|
||||
use App\Profile;
|
||||
|
||||
class ProfileService {
|
||||
|
||||
class ProfileService
|
||||
{
|
||||
public static function get($id)
|
||||
{
|
||||
$key = 'profile:model:' . $id;
|
||||
$ttl = now()->addHours(4);
|
||||
$res = Cache::remember($key, $ttl, function() use($id) {
|
||||
$profile = Profile::find($id);
|
||||
if(!$profile) {
|
||||
return false;
|
||||
}
|
||||
$fractal = new Fractal\Manager();
|
||||
$fractal->setSerializer(new ArraySerializer());
|
||||
$resource = new Fractal\Resource\Item($profile, new AccountTransformer());
|
||||
return $fractal->createData($resource)->toArray();
|
||||
});
|
||||
return $res;
|
||||
return AccountService::get($id);
|
||||
}
|
||||
|
||||
public static function del($id)
|
||||
{
|
||||
return AccountService::del($id);
|
||||
}
|
||||
}
|
||||
|
|
|
@ -3,16 +3,44 @@
|
|||
namespace App\Services;
|
||||
|
||||
use Illuminate\Support\Carbon;
|
||||
use Cache;
|
||||
|
||||
class SnowflakeService {
|
||||
|
||||
public static function byDate(Carbon $ts = null)
|
||||
{
|
||||
$ts = $ts ? now()->parse($ts)->timestamp : microtime(true);
|
||||
if($ts instanceOf Carbon) {
|
||||
$ts = now()->parse($ts)->timestamp;
|
||||
} else {
|
||||
return self::next();
|
||||
}
|
||||
|
||||
return ((round($ts * 1000) - 1549756800000) << 22)
|
||||
| (1 << 17)
|
||||
| (1 << 12)
|
||||
| (random_int(1,31) << 17)
|
||||
| (random_int(1,31) << 12)
|
||||
| 0;
|
||||
}
|
||||
|
||||
}
|
||||
public static function next()
|
||||
{
|
||||
$seq = Cache::get('snowflake:seq');
|
||||
|
||||
if(!$seq) {
|
||||
Cache::put('snowflake:seq', 1);
|
||||
$seq = 1;
|
||||
} else {
|
||||
Cache::increment('snowflake:seq');
|
||||
}
|
||||
|
||||
if($seq >= 4095) {
|
||||
Cache::put('snowflake:seq', 0);
|
||||
$seq = 0;
|
||||
}
|
||||
|
||||
return ((round(microtime(true) * 1000) - 1549756800000) << 22)
|
||||
| (random_int(1,31) << 17)
|
||||
| (random_int(1,31) << 12)
|
||||
| $seq;
|
||||
}
|
||||
|
||||
}
|
||||
|
|
|
@ -6,6 +6,7 @@ use Cache;
|
|||
use Illuminate\Support\Facades\Redis;
|
||||
use App\{Status, StatusHashtag};
|
||||
use App\Transformer\Api\StatusHashtagTransformer;
|
||||
use App\Transformer\Api\HashtagTransformer;
|
||||
use League\Fractal;
|
||||
use League\Fractal\Serializer\ArraySerializer;
|
||||
use League\Fractal\Pagination\IlluminatePaginatorAdapter;
|
||||
|
@ -78,4 +79,21 @@ class StatusHashtagService {
|
|||
{
|
||||
return ['status' => StatusService::get($statusId)];
|
||||
}
|
||||
|
||||
public static function statusTags($statusId)
|
||||
{
|
||||
$key = 'pf:services:sh:id:' . $statusId;
|
||||
|
||||
return Cache::remember($key, 604800, function() use($statusId) {
|
||||
$status = Status::find($statusId);
|
||||
if(!$status) {
|
||||
return [];
|
||||
}
|
||||
|
||||
$fractal = new Fractal\Manager();
|
||||
$fractal->setSerializer(new ArraySerializer());
|
||||
$resource = new Fractal\Resource\Collection($status->hashtags, new HashtagTransformer());
|
||||
return $fractal->createData($resource)->toArray();
|
||||
});
|
||||
}
|
||||
}
|
||||
|
|
|
@ -16,15 +16,20 @@ class StatusService {
|
|||
|
||||
const CACHE_KEY = 'pf:services:status:';
|
||||
|
||||
public static function key($id)
|
||||
public static function key($id, $publicOnly = true)
|
||||
{
|
||||
return self::CACHE_KEY . $id;
|
||||
$p = $publicOnly ? '' : 'all:';
|
||||
return self::CACHE_KEY . $p . $id;
|
||||
}
|
||||
|
||||
public static function get($id)
|
||||
public static function get($id, $publicOnly = true)
|
||||
{
|
||||
return Cache::remember(self::key($id), now()->addDays(7), function() use($id) {
|
||||
$status = Status::whereScope('public')->find($id);
|
||||
return Cache::remember(self::key($id, $publicOnly), now()->addDays(7), function() use($id, $publicOnly) {
|
||||
if($publicOnly) {
|
||||
$status = Status::whereScope('public')->find($id);
|
||||
} else {
|
||||
$status = Status::whereIn('scope', ['public', 'private', 'unlisted', 'group'])->find($id);
|
||||
}
|
||||
if(!$status) {
|
||||
return null;
|
||||
}
|
||||
|
@ -37,7 +42,17 @@ class StatusService {
|
|||
|
||||
public static function del($id)
|
||||
{
|
||||
$status = self::get($id);
|
||||
if($status && isset($status['account']) && isset($status['account']['id'])) {
|
||||
Cache::forget('profile:embed:' . $status['account']['id']);
|
||||
}
|
||||
Cache::forget('status:transformer:media:attachments:' . $id);
|
||||
MediaService::del($id);
|
||||
Cache::forget('status:thumb:nsfw0' . $id);
|
||||
Cache::forget('status:thumb:nsfw1' . $id);
|
||||
Cache::forget('pf:services:sh:id:' . $id);
|
||||
PublicTimelineService::rem($id);
|
||||
Cache::forget(self::key($id, false));
|
||||
return Cache::forget(self::key($id));
|
||||
}
|
||||
}
|
||||
|
|
162
app/Services/StoryService.php
Normal file
162
app/Services/StoryService.php
Normal file
|
@ -0,0 +1,162 @@
|
|||
<?php
|
||||
|
||||
namespace App\Services;
|
||||
|
||||
use Illuminate\Support\Facades\Cache;
|
||||
use Illuminate\Support\Facades\Redis;
|
||||
use Illuminate\Support\Facades\Storage;
|
||||
use App\Story;
|
||||
use App\StoryView;
|
||||
|
||||
class StoryService
|
||||
{
|
||||
const STORY_KEY = 'pf:services:stories:v1:';
|
||||
|
||||
public static function get($id)
|
||||
{
|
||||
$account = AccountService::get($id);
|
||||
if(!$account) {
|
||||
return false;
|
||||
}
|
||||
|
||||
$res = [
|
||||
'profile' => [
|
||||
'id' => (string) $account['id'],
|
||||
'avatar' => $account['avatar'],
|
||||
'username' => $account['username'],
|
||||
'url' => $account['url']
|
||||
]
|
||||
];
|
||||
|
||||
$res['stories'] = self::getStories($id);
|
||||
return $res;
|
||||
}
|
||||
|
||||
public static function getById($id)
|
||||
{
|
||||
return Cache::remember(self::STORY_KEY . 'by-id:id-' . $id, 3600, function() use ($id) {
|
||||
return Story::find($id);
|
||||
});
|
||||
}
|
||||
|
||||
public static function delById($id)
|
||||
{
|
||||
return Cache::forget(self::STORY_KEY . 'by-id:id-' . $id);
|
||||
}
|
||||
|
||||
public static function getStories($id, $pid)
|
||||
{
|
||||
return Story::whereProfileId($id)
|
||||
->latest()
|
||||
->get()
|
||||
->map(function($s) use($pid) {
|
||||
return [
|
||||
'id' => (string) $s->id,
|
||||
'type' => $s->type,
|
||||
'duration' => 10,
|
||||
'seen' => in_array($pid, self::views($s->id)),
|
||||
'created_at' => $s->created_at->toAtomString(),
|
||||
'expires_at' => $s->expires_at->toAtomString(),
|
||||
'media' => url(Storage::url($s->path)),
|
||||
'can_reply' => (bool) $s->can_reply,
|
||||
'can_react' => (bool) $s->can_react,
|
||||
'poll' => $s->type == 'poll' ? PollService::storyPoll($s->id) : null
|
||||
];
|
||||
})
|
||||
->toArray();
|
||||
}
|
||||
|
||||
public static function views($id)
|
||||
{
|
||||
return StoryView::whereStoryId($id)
|
||||
->pluck('profile_id')
|
||||
->toArray();
|
||||
}
|
||||
|
||||
public static function hasSeen($pid, $sid)
|
||||
{
|
||||
$key = self::STORY_KEY . 'seen:' . $pid . ':' . $sid;
|
||||
return Cache::remember($key, 3600, function() use($pid, $sid) {
|
||||
return StoryView::whereStoryId($sid)
|
||||
->whereProfileId($pid)
|
||||
->exists();
|
||||
});
|
||||
}
|
||||
|
||||
public static function latest($pid)
|
||||
{
|
||||
return Cache::remember(self::STORY_KEY . 'latest:pid-' . $pid, 3600, function() use ($pid) {
|
||||
return Story::whereProfileId($pid)
|
||||
->latest()
|
||||
->first()
|
||||
->id;
|
||||
});
|
||||
}
|
||||
|
||||
public static function delLatest($pid)
|
||||
{
|
||||
return Cache::forget(self::STORY_KEY . 'latest:pid-' . $pid);
|
||||
}
|
||||
|
||||
public static function addSeen($pid, $sid)
|
||||
{
|
||||
return Cache::put(self::STORY_KEY . 'seen:' . $pid . ':' . $sid, true, 86400);
|
||||
}
|
||||
|
||||
public static function adminStats()
|
||||
{
|
||||
return Cache::remember('pf:admin:stories:stats', 300, function() {
|
||||
$total = Story::count();
|
||||
return [
|
||||
'active' => [
|
||||
'today' => Story::whereDate('created_at', now()->today())->count(),
|
||||
'month' => Story::whereMonth('created_at', now()->month)->whereYear('created_at', now()->year)->count()
|
||||
],
|
||||
'total' => $total,
|
||||
'remote' => [
|
||||
'today' => Story::whereLocal(false)->whereDate('created_at', now()->today())->count(),
|
||||
'month' => Story::whereLocal(false)->whereMonth('created_at', now()->month)->whereYear('created_at', now()->year)->count()
|
||||
],
|
||||
'storage' => [
|
||||
'sum' => (int) Story::sum('size'),
|
||||
'average' => (int) Story::avg('size')
|
||||
],
|
||||
'avg_spu' => (int) ($total / Story::groupBy('profile_id')->pluck('profile_id')->count()),
|
||||
'avg_duration' => (int) floor(Story::avg('duration')),
|
||||
'avg_type' => Story::selectRaw('type, count(id) as count')->groupBy('type')->orderByDesc('count')->first()->type
|
||||
];
|
||||
});
|
||||
}
|
||||
|
||||
public static function rotateQueue()
|
||||
{
|
||||
return Redis::smembers('pf:stories:rotate-queue');
|
||||
}
|
||||
|
||||
public static function addRotateQueue($id)
|
||||
{
|
||||
return Redis::sadd('pf:stories:rotate-queue', $id);
|
||||
}
|
||||
|
||||
public static function removeRotateQueue($id)
|
||||
{
|
||||
self::delById($id);
|
||||
return Redis::srem('pf:stories:rotate-queue', $id);
|
||||
}
|
||||
|
||||
public static function reactIncrement($storyId, $profileId)
|
||||
{
|
||||
$key = 'pf:stories:react-counter:storyid-' . $storyId . ':profileid-' . $profileId;
|
||||
if(Redis::get($key) == null) {
|
||||
Redis::setex($key, 86400, 1);
|
||||
} else {
|
||||
return Redis::incr($key);
|
||||
}
|
||||
}
|
||||
|
||||
public static function reactCounter($storyId, $profileId)
|
||||
{
|
||||
$key = 'pf:stories:react-counter:storyid-' . $storyId . ':profileid-' . $profileId;
|
||||
return (int) Redis::get($key) ?? 0;
|
||||
}
|
||||
}
|
699
app/Status.php
699
app/Status.php
|
@ -4,414 +4,419 @@ namespace App;
|
|||
|
||||
use Auth, Cache, Hashids, Storage;
|
||||
use Illuminate\Database\Eloquent\Model;
|
||||
use Pixelfed\Snowflake\HasSnowflakePrimary;
|
||||
use App\HasSnowflakePrimary;
|
||||
use App\Http\Controllers\StatusController;
|
||||
use Illuminate\Database\Eloquent\SoftDeletes;
|
||||
use App\Models\Poll;
|
||||
|
||||
class Status extends Model
|
||||
{
|
||||
use HasSnowflakePrimary, SoftDeletes;
|
||||
use HasSnowflakePrimary, SoftDeletes;
|
||||
|
||||
/**
|
||||
* Indicates if the IDs are auto-incrementing.
|
||||
*
|
||||
* @var bool
|
||||
*/
|
||||
public $incrementing = false;
|
||||
/**
|
||||
* Indicates if the IDs are auto-incrementing.
|
||||
*
|
||||
* @var bool
|
||||
*/
|
||||
public $incrementing = false;
|
||||
|
||||
/**
|
||||
* The attributes that should be mutated to dates.
|
||||
*
|
||||
* @var array
|
||||
*/
|
||||
protected $dates = ['deleted_at'];
|
||||
/**
|
||||
* The attributes that should be mutated to dates.
|
||||
*
|
||||
* @var array
|
||||
*/
|
||||
protected $dates = ['deleted_at'];
|
||||
|
||||
protected $fillable = ['profile_id', 'visibility', 'in_reply_to_id', 'reblog_of_id', 'type'];
|
||||
protected $fillable = ['profile_id', 'visibility', 'in_reply_to_id', 'reblog_of_id', 'type'];
|
||||
|
||||
const STATUS_TYPES = [
|
||||
'text',
|
||||
'photo',
|
||||
'photo:album',
|
||||
'video',
|
||||
'video:album',
|
||||
'photo:video:album',
|
||||
'share',
|
||||
'reply',
|
||||
'story',
|
||||
'story:reply',
|
||||
'story:reaction',
|
||||
'story:live',
|
||||
'loop'
|
||||
];
|
||||
const STATUS_TYPES = [
|
||||
'text',
|
||||
'photo',
|
||||
'photo:album',
|
||||
'video',
|
||||
'video:album',
|
||||
'photo:video:album',
|
||||
'share',
|
||||
'reply',
|
||||
'story',
|
||||
'story:reply',
|
||||
'story:reaction',
|
||||
'story:live',
|
||||
'loop'
|
||||
];
|
||||
|
||||
const MAX_MENTIONS = 5;
|
||||
const MAX_MENTIONS = 5;
|
||||
|
||||
const MAX_HASHTAGS = 30;
|
||||
const MAX_HASHTAGS = 30;
|
||||
|
||||
const MAX_LINKS = 0;
|
||||
const MAX_LINKS = 2;
|
||||
|
||||
public function profile()
|
||||
{
|
||||
return $this->belongsTo(Profile::class);
|
||||
}
|
||||
public function profile()
|
||||
{
|
||||
return $this->belongsTo(Profile::class);
|
||||
}
|
||||
|
||||
public function media()
|
||||
{
|
||||
return $this->hasMany(Media::class);
|
||||
}
|
||||
public function media()
|
||||
{
|
||||
return $this->hasMany(Media::class);
|
||||
}
|
||||
|
||||
public function firstMedia()
|
||||
{
|
||||
return $this->hasMany(Media::class)->orderBy('order', 'asc')->first();
|
||||
}
|
||||
public function firstMedia()
|
||||
{
|
||||
return $this->hasMany(Media::class)->orderBy('order', 'asc')->first();
|
||||
}
|
||||
|
||||
public function viewType()
|
||||
{
|
||||
if($this->type) {
|
||||
return $this->type;
|
||||
}
|
||||
return $this->setType();
|
||||
}
|
||||
public function viewType()
|
||||
{
|
||||
if($this->type) {
|
||||
return $this->type;
|
||||
}
|
||||
return $this->setType();
|
||||
}
|
||||
|
||||
public function setType()
|
||||
{
|
||||
if(in_array($this->type, self::STATUS_TYPES)) {
|
||||
return $this->type;
|
||||
}
|
||||
$mimes = $this->media->pluck('mime')->toArray();
|
||||
$type = StatusController::mimeTypeCheck($mimes);
|
||||
if($type) {
|
||||
$this->type = $type;
|
||||
$this->save();
|
||||
return $type;
|
||||
}
|
||||
}
|
||||
public function setType()
|
||||
{
|
||||
if(in_array($this->type, self::STATUS_TYPES)) {
|
||||
return $this->type;
|
||||
}
|
||||
$mimes = $this->media->pluck('mime')->toArray();
|
||||
$type = StatusController::mimeTypeCheck($mimes);
|
||||
if($type) {
|
||||
$this->type = $type;
|
||||
$this->save();
|
||||
return $type;
|
||||
}
|
||||
}
|
||||
|
||||
public function thumb($showNsfw = false)
|
||||
{
|
||||
$key = $showNsfw ? 'status:thumb:nsfw1'.$this->id : 'status:thumb:nsfw0'.$this->id;
|
||||
return Cache::remember($key, now()->addMinutes(15), function() use ($showNsfw) {
|
||||
$type = $this->type ?? $this->setType();
|
||||
$is_nsfw = !$showNsfw ? $this->is_nsfw : false;
|
||||
if ($this->media->count() == 0 || $is_nsfw || !in_array($type,['photo', 'photo:album', 'video'])) {
|
||||
return url(Storage::url('public/no-preview.png'));
|
||||
}
|
||||
public function thumb($showNsfw = false)
|
||||
{
|
||||
$key = $showNsfw ? 'status:thumb:nsfw1'.$this->id : 'status:thumb:nsfw0'.$this->id;
|
||||
return Cache::remember($key, now()->addMinutes(15), function() use ($showNsfw) {
|
||||
$type = $this->type ?? $this->setType();
|
||||
$is_nsfw = !$showNsfw ? $this->is_nsfw : false;
|
||||
if ($this->media->count() == 0 || $is_nsfw || !in_array($type,['photo', 'photo:album', 'video'])) {
|
||||
return url(Storage::url('public/no-preview.png'));
|
||||
}
|
||||
|
||||
return url(Storage::url($this->firstMedia()->thumbnail_path));
|
||||
});
|
||||
}
|
||||
return url(Storage::url($this->firstMedia()->thumbnail_path));
|
||||
});
|
||||
}
|
||||
|
||||
public function url()
|
||||
{
|
||||
if($this->uri) {
|
||||
return $this->uri;
|
||||
} else {
|
||||
$id = $this->id;
|
||||
$username = $this->profile->username;
|
||||
$path = url(config('app.url')."/p/{$username}/{$id}");
|
||||
return $path;
|
||||
}
|
||||
}
|
||||
public function url($forceLocal = false)
|
||||
{
|
||||
if($this->uri) {
|
||||
return $forceLocal ? "/i/web/post/_/{$this->profile_id}/{$this->id}" : $this->uri;
|
||||
} else {
|
||||
$id = $this->id;
|
||||
$username = $this->profile->username;
|
||||
$path = url(config('app.url')."/p/{$username}/{$id}");
|
||||
return $path;
|
||||
}
|
||||
}
|
||||
|
||||
public function permalink($suffix = '/activity')
|
||||
{
|
||||
$id = $this->id;
|
||||
$username = $this->profile->username;
|
||||
$path = config('app.url')."/p/{$username}/{$id}{$suffix}";
|
||||
public function permalink($suffix = '/activity')
|
||||
{
|
||||
$id = $this->id;
|
||||
$username = $this->profile->username;
|
||||
$path = config('app.url')."/p/{$username}/{$id}{$suffix}";
|
||||
|
||||
return url($path);
|
||||
}
|
||||
return url($path);
|
||||
}
|
||||
|
||||
public function editUrl()
|
||||
{
|
||||
return $this->url().'/edit';
|
||||
}
|
||||
public function editUrl()
|
||||
{
|
||||
return $this->url().'/edit';
|
||||
}
|
||||
|
||||
public function mediaUrl()
|
||||
{
|
||||
$media = $this->firstMedia();
|
||||
$path = $media->media_path;
|
||||
$hash = is_null($media->processed_at) ? md5('unprocessed') : md5($media->created_at);
|
||||
$url = $media->cdn_url ? $media->cdn_url . "?v={$hash}" : url(Storage::url($path)."?v={$hash}");
|
||||
public function mediaUrl()
|
||||
{
|
||||
$media = $this->firstMedia();
|
||||
$path = $media->media_path;
|
||||
$hash = is_null($media->processed_at) ? md5('unprocessed') : md5($media->created_at);
|
||||
$url = $media->cdn_url ? $media->cdn_url . "?v={$hash}" : url(Storage::url($path)."?v={$hash}");
|
||||
|
||||
return $url;
|
||||
}
|
||||
return $url;
|
||||
}
|
||||
|
||||
public function likes()
|
||||
{
|
||||
return $this->hasMany(Like::class);
|
||||
}
|
||||
public function likes()
|
||||
{
|
||||
return $this->hasMany(Like::class);
|
||||
}
|
||||
|
||||
public function liked() : bool
|
||||
{
|
||||
if(!Auth::check()) {
|
||||
return false;
|
||||
}
|
||||
public function liked() : bool
|
||||
{
|
||||
if(!Auth::check()) {
|
||||
return false;
|
||||
}
|
||||
|
||||
$pid = Auth::user()->profile_id;
|
||||
$pid = Auth::user()->profile_id;
|
||||
|
||||
return Like::select('status_id', 'profile_id')
|
||||
->whereStatusId($this->id)
|
||||
->whereProfileId($pid)
|
||||
->exists();
|
||||
}
|
||||
return Like::select('status_id', 'profile_id')
|
||||
->whereStatusId($this->id)
|
||||
->whereProfileId($pid)
|
||||
->exists();
|
||||
}
|
||||
|
||||
public function likedBy()
|
||||
{
|
||||
return $this->hasManyThrough(
|
||||
Profile::class,
|
||||
Like::class,
|
||||
'status_id',
|
||||
'id',
|
||||
'id',
|
||||
'profile_id'
|
||||
);
|
||||
}
|
||||
public function likedBy()
|
||||
{
|
||||
return $this->hasManyThrough(
|
||||
Profile::class,
|
||||
Like::class,
|
||||
'status_id',
|
||||
'id',
|
||||
'id',
|
||||
'profile_id'
|
||||
);
|
||||
}
|
||||
|
||||
public function comments()
|
||||
{
|
||||
return $this->hasMany(self::class, 'in_reply_to_id');
|
||||
}
|
||||
public function comments()
|
||||
{
|
||||
return $this->hasMany(self::class, 'in_reply_to_id');
|
||||
}
|
||||
|
||||
public function bookmarked()
|
||||
{
|
||||
if (!Auth::check()) {
|
||||
return false;
|
||||
}
|
||||
$profile = Auth::user()->profile;
|
||||
public function bookmarked()
|
||||
{
|
||||
if (!Auth::check()) {
|
||||
return false;
|
||||
}
|
||||
$profile = Auth::user()->profile;
|
||||
|
||||
return Bookmark::whereProfileId($profile->id)->whereStatusId($this->id)->count();
|
||||
}
|
||||
return Bookmark::whereProfileId($profile->id)->whereStatusId($this->id)->count();
|
||||
}
|
||||
|
||||
public function shares()
|
||||
{
|
||||
return $this->hasMany(self::class, 'reblog_of_id');
|
||||
}
|
||||
public function shares()
|
||||
{
|
||||
return $this->hasMany(self::class, 'reblog_of_id');
|
||||
}
|
||||
|
||||
public function shared() : bool
|
||||
{
|
||||
if(!Auth::check()) {
|
||||
return false;
|
||||
}
|
||||
$pid = Auth::user()->profile_id;
|
||||
public function shared() : bool
|
||||
{
|
||||
if(!Auth::check()) {
|
||||
return false;
|
||||
}
|
||||
$pid = Auth::user()->profile_id;
|
||||
|
||||
return $this->select('profile_id', 'reblog_of_id')
|
||||
->whereProfileId($pid)
|
||||
->whereReblogOfId($this->id)
|
||||
->exists();
|
||||
}
|
||||
return $this->select('profile_id', 'reblog_of_id')
|
||||
->whereProfileId($pid)
|
||||
->whereReblogOfId($this->id)
|
||||
->exists();
|
||||
}
|
||||
|
||||
public function sharedBy()
|
||||
{
|
||||
return $this->hasManyThrough(
|
||||
Profile::class,
|
||||
Status::class,
|
||||
'reblog_of_id',
|
||||
'id',
|
||||
'id',
|
||||
'profile_id'
|
||||
);
|
||||
}
|
||||
public function sharedBy()
|
||||
{
|
||||
return $this->hasManyThrough(
|
||||
Profile::class,
|
||||
Status::class,
|
||||
'reblog_of_id',
|
||||
'id',
|
||||
'id',
|
||||
'profile_id'
|
||||
);
|
||||
}
|
||||
|
||||
public function parent()
|
||||
{
|
||||
$parent = $this->in_reply_to_id ?? $this->reblog_of_id;
|
||||
if (!empty($parent)) {
|
||||
return $this->findOrFail($parent);
|
||||
} else {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
public function parent()
|
||||
{
|
||||
$parent = $this->in_reply_to_id ?? $this->reblog_of_id;
|
||||
if (!empty($parent)) {
|
||||
return $this->findOrFail($parent);
|
||||
} else {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
public function conversation()
|
||||
{
|
||||
return $this->hasOne(Conversation::class);
|
||||
}
|
||||
public function conversation()
|
||||
{
|
||||
return $this->hasOne(Conversation::class);
|
||||
}
|
||||
|
||||
public function hashtags()
|
||||
{
|
||||
return $this->hasManyThrough(
|
||||
Hashtag::class,
|
||||
StatusHashtag::class,
|
||||
'status_id',
|
||||
'id',
|
||||
'id',
|
||||
'hashtag_id'
|
||||
);
|
||||
}
|
||||
public function hashtags()
|
||||
{
|
||||
return $this->hasManyThrough(
|
||||
Hashtag::class,
|
||||
StatusHashtag::class,
|
||||
'status_id',
|
||||
'id',
|
||||
'id',
|
||||
'hashtag_id'
|
||||
);
|
||||
}
|
||||
|
||||
public function mentions()
|
||||
{
|
||||
return $this->hasManyThrough(
|
||||
Profile::class,
|
||||
Mention::class,
|
||||
'status_id',
|
||||
'id',
|
||||
'id',
|
||||
'profile_id'
|
||||
);
|
||||
}
|
||||
public function mentions()
|
||||
{
|
||||
return $this->hasManyThrough(
|
||||
Profile::class,
|
||||
Mention::class,
|
||||
'status_id',
|
||||
'id',
|
||||
'id',
|
||||
'profile_id'
|
||||
);
|
||||
}
|
||||
|
||||
public function reportUrl()
|
||||
{
|
||||
return route('report.form')."?type=post&id={$this->id}";
|
||||
}
|
||||
public function reportUrl()
|
||||
{
|
||||
return route('report.form')."?type=post&id={$this->id}";
|
||||
}
|
||||
|
||||
public function toActivityStream()
|
||||
{
|
||||
$media = $this->media;
|
||||
$mediaCollection = [];
|
||||
foreach ($media as $image) {
|
||||
$mediaCollection[] = [
|
||||
'type' => 'Link',
|
||||
'href' => $image->url(),
|
||||
'mediaType' => $image->mime,
|
||||
];
|
||||
}
|
||||
$obj = [
|
||||
'@context' => 'https://www.w3.org/ns/activitystreams',
|
||||
'type' => 'Image',
|
||||
'name' => null,
|
||||
'url' => $mediaCollection,
|
||||
];
|
||||
public function toActivityStream()
|
||||
{
|
||||
$media = $this->media;
|
||||
$mediaCollection = [];
|
||||
foreach ($media as $image) {
|
||||
$mediaCollection[] = [
|
||||
'type' => 'Link',
|
||||
'href' => $image->url(),
|
||||
'mediaType' => $image->mime,
|
||||
];
|
||||
}
|
||||
$obj = [
|
||||
'@context' => 'https://www.w3.org/ns/activitystreams',
|
||||
'type' => 'Image',
|
||||
'name' => null,
|
||||
'url' => $mediaCollection,
|
||||
];
|
||||
|
||||
return $obj;
|
||||
}
|
||||
return $obj;
|
||||
}
|
||||
|
||||
public function replyToText()
|
||||
{
|
||||
$actorName = $this->profile->username;
|
||||
public function replyToText()
|
||||
{
|
||||
$actorName = $this->profile->username;
|
||||
|
||||
return "{$actorName} ".__('notification.commented');
|
||||
}
|
||||
return "{$actorName} ".__('notification.commented');
|
||||
}
|
||||
|
||||
public function replyToHtml()
|
||||
{
|
||||
$actorName = $this->profile->username;
|
||||
$actorUrl = $this->profile->url();
|
||||
public function replyToHtml()
|
||||
{
|
||||
$actorName = $this->profile->username;
|
||||
$actorUrl = $this->profile->url();
|
||||
|
||||
return "<a href='{$actorUrl}' class='profile-link'>{$actorName}</a> ".
|
||||
__('notification.commented');
|
||||
}
|
||||
return "<a href='{$actorUrl}' class='profile-link'>{$actorName}</a> ".
|
||||
__('notification.commented');
|
||||
}
|
||||
|
||||
public function shareToText()
|
||||
{
|
||||
$actorName = $this->profile->username;
|
||||
public function shareToText()
|
||||
{
|
||||
$actorName = $this->profile->username;
|
||||
|
||||
return "{$actorName} ".__('notification.shared');
|
||||
}
|
||||
return "{$actorName} ".__('notification.shared');
|
||||
}
|
||||
|
||||
public function shareToHtml()
|
||||
{
|
||||
$actorName = $this->profile->username;
|
||||
$actorUrl = $this->profile->url();
|
||||
public function shareToHtml()
|
||||
{
|
||||
$actorName = $this->profile->username;
|
||||
$actorUrl = $this->profile->url();
|
||||
|
||||
return "<a href='{$actorUrl}' class='profile-link'>{$actorName}</a> ".
|
||||
__('notification.shared');
|
||||
}
|
||||
return "<a href='{$actorUrl}' class='profile-link'>{$actorName}</a> ".
|
||||
__('notification.shared');
|
||||
}
|
||||
|
||||
public function recentComments()
|
||||
{
|
||||
return $this->comments()->orderBy('created_at', 'desc')->take(3);
|
||||
}
|
||||
public function recentComments()
|
||||
{
|
||||
return $this->comments()->orderBy('created_at', 'desc')->take(3);
|
||||
}
|
||||
|
||||
public function toActivityPubObject()
|
||||
{
|
||||
if($this->local == false) {
|
||||
return;
|
||||
}
|
||||
$profile = $this->profile;
|
||||
$to = $this->scopeToAudience('to');
|
||||
$cc = $this->scopeToAudience('cc');
|
||||
return [
|
||||
'@context' => 'https://www.w3.org/ns/activitystreams',
|
||||
'id' => $this->permalink(),
|
||||
'type' => 'Create',
|
||||
'actor' => $profile->permalink(),
|
||||
'published' => $this->created_at->format('c'),
|
||||
'to' => $to,
|
||||
'cc' => $cc,
|
||||
'object' => [
|
||||
'id' => $this->url(),
|
||||
'type' => 'Note',
|
||||
'summary' => null,
|
||||
'inReplyTo' => null,
|
||||
'published' => $this->created_at->format('c'),
|
||||
'url' => $this->url(),
|
||||
'attributedTo' => $this->profile->url(),
|
||||
'to' => $to,
|
||||
'cc' => $cc,
|
||||
'sensitive' => (bool) $this->is_nsfw,
|
||||
'content' => $this->rendered,
|
||||
'attachment' => $this->media->map(function($media) {
|
||||
return [
|
||||
'type' => 'Document',
|
||||
'mediaType' => $media->mime,
|
||||
'url' => $media->url(),
|
||||
'name' => null
|
||||
];
|
||||
})->toArray()
|
||||
]
|
||||
];
|
||||
}
|
||||
public function toActivityPubObject()
|
||||
{
|
||||
if($this->local == false) {
|
||||
return;
|
||||
}
|
||||
$profile = $this->profile;
|
||||
$to = $this->scopeToAudience('to');
|
||||
$cc = $this->scopeToAudience('cc');
|
||||
return [
|
||||
'@context' => 'https://www.w3.org/ns/activitystreams',
|
||||
'id' => $this->permalink(),
|
||||
'type' => 'Create',
|
||||
'actor' => $profile->permalink(),
|
||||
'published' => $this->created_at->format('c'),
|
||||
'to' => $to,
|
||||
'cc' => $cc,
|
||||
'object' => [
|
||||
'id' => $this->url(),
|
||||
'type' => 'Note',
|
||||
'summary' => null,
|
||||
'inReplyTo' => null,
|
||||
'published' => $this->created_at->format('c'),
|
||||
'url' => $this->url(),
|
||||
'attributedTo' => $this->profile->url(),
|
||||
'to' => $to,
|
||||
'cc' => $cc,
|
||||
'sensitive' => (bool) $this->is_nsfw,
|
||||
'content' => $this->rendered,
|
||||
'attachment' => $this->media->map(function($media) {
|
||||
return [
|
||||
'type' => 'Document',
|
||||
'mediaType' => $media->mime,
|
||||
'url' => $media->url(),
|
||||
'name' => null
|
||||
];
|
||||
})->toArray()
|
||||
]
|
||||
];
|
||||
}
|
||||
|
||||
public function scopeToAudience($audience)
|
||||
{
|
||||
if(!in_array($audience, ['to', 'cc']) || $this->local == false) {
|
||||
return;
|
||||
}
|
||||
$res = [];
|
||||
$res['to'] = [];
|
||||
$res['cc'] = [];
|
||||
$scope = $this->scope;
|
||||
$mentions = $this->mentions->map(function ($mention) {
|
||||
return $mention->permalink();
|
||||
})->toArray();
|
||||
public function scopeToAudience($audience)
|
||||
{
|
||||
if(!in_array($audience, ['to', 'cc']) || $this->local == false) {
|
||||
return;
|
||||
}
|
||||
$res = [];
|
||||
$res['to'] = [];
|
||||
$res['cc'] = [];
|
||||
$scope = $this->scope;
|
||||
$mentions = $this->mentions->map(function ($mention) {
|
||||
return $mention->permalink();
|
||||
})->toArray();
|
||||
|
||||
if($this->in_reply_to_id != null) {
|
||||
$parent = $this->parent();
|
||||
if($parent) {
|
||||
$mentions = array_merge([$parent->profile->permalink()], $mentions);
|
||||
}
|
||||
}
|
||||
if($this->in_reply_to_id != null) {
|
||||
$parent = $this->parent();
|
||||
if($parent) {
|
||||
$mentions = array_merge([$parent->profile->permalink()], $mentions);
|
||||
}
|
||||
}
|
||||
|
||||
switch ($scope) {
|
||||
case 'public':
|
||||
$res['to'] = [
|
||||
"https://www.w3.org/ns/activitystreams#Public"
|
||||
];
|
||||
$res['cc'] = array_merge([$this->profile->permalink('/followers')], $mentions);
|
||||
break;
|
||||
switch ($scope) {
|
||||
case 'public':
|
||||
$res['to'] = [
|
||||
"https://www.w3.org/ns/activitystreams#Public"
|
||||
];
|
||||
$res['cc'] = array_merge([$this->profile->permalink('/followers')], $mentions);
|
||||
break;
|
||||
|
||||
case 'unlisted':
|
||||
$res['to'] = array_merge([$this->profile->permalink('/followers')], $mentions);
|
||||
$res['cc'] = [
|
||||
"https://www.w3.org/ns/activitystreams#Public"
|
||||
];
|
||||
break;
|
||||
case 'unlisted':
|
||||
$res['to'] = array_merge([$this->profile->permalink('/followers')], $mentions);
|
||||
$res['cc'] = [
|
||||
"https://www.w3.org/ns/activitystreams#Public"
|
||||
];
|
||||
break;
|
||||
|
||||
case 'private':
|
||||
$res['to'] = array_merge([$this->profile->permalink('/followers')], $mentions);
|
||||
$res['cc'] = [];
|
||||
break;
|
||||
case 'private':
|
||||
$res['to'] = array_merge([$this->profile->permalink('/followers')], $mentions);
|
||||
$res['cc'] = [];
|
||||
break;
|
||||
|
||||
// TODO: Update scope when DMs are supported
|
||||
case 'direct':
|
||||
$res['to'] = [];
|
||||
$res['cc'] = [];
|
||||
break;
|
||||
}
|
||||
return $res[$audience];
|
||||
}
|
||||
// TODO: Update scope when DMs are supported
|
||||
case 'direct':
|
||||
$res['to'] = [];
|
||||
$res['cc'] = [];
|
||||
break;
|
||||
}
|
||||
return $res[$audience];
|
||||
}
|
||||
|
||||
public function place()
|
||||
{
|
||||
return $this->belongsTo(Place::class);
|
||||
}
|
||||
public function place()
|
||||
{
|
||||
return $this->belongsTo(Place::class);
|
||||
}
|
||||
|
||||
public function directMessage()
|
||||
{
|
||||
return $this->hasOne(DirectMessage::class);
|
||||
}
|
||||
public function directMessage()
|
||||
{
|
||||
return $this->hasOne(DirectMessage::class);
|
||||
}
|
||||
|
||||
public function poll()
|
||||
{
|
||||
return $this->hasOne(Poll::class);
|
||||
}
|
||||
}
|
||||
|
|
|
@ -3,8 +3,10 @@
|
|||
namespace App;
|
||||
|
||||
use Auth;
|
||||
use Storage;
|
||||
use Illuminate\Database\Eloquent\Model;
|
||||
use Pixelfed\Snowflake\HasSnowflakePrimary;
|
||||
use App\HasSnowflakePrimary;
|
||||
use App\Util\Lexer\Bearcap;
|
||||
|
||||
class Story extends Model
|
||||
{
|
||||
|
@ -19,14 +21,11 @@ class Story extends Model
|
|||
*/
|
||||
public $incrementing = false;
|
||||
|
||||
/**
|
||||
* The attributes that should be mutated to dates.
|
||||
*
|
||||
* @var array
|
||||
*/
|
||||
protected $dates = ['published_at', 'expires_at'];
|
||||
protected $casts = [
|
||||
'expires_at' => 'datetime'
|
||||
];
|
||||
|
||||
protected $fillable = ['profile_id'];
|
||||
protected $fillable = ['profile_id', 'view_count'];
|
||||
|
||||
protected $visible = ['id'];
|
||||
|
||||
|
@ -51,6 +50,42 @@ class Story extends Model
|
|||
|
||||
public function permalink()
|
||||
{
|
||||
return url("/story/$this->id");
|
||||
$username = $this->profile->username;
|
||||
return url("/stories/{$username}/{$this->id}/activity");
|
||||
}
|
||||
|
||||
public function url()
|
||||
{
|
||||
$username = $this->profile->username;
|
||||
return url("/stories/{$username}/{$this->id}");
|
||||
}
|
||||
|
||||
public function mediaUrl()
|
||||
{
|
||||
return url(Storage::url($this->path));
|
||||
}
|
||||
|
||||
public function bearcapUrl()
|
||||
{
|
||||
return Bearcap::encode($this->url(), $this->bearcap_token);
|
||||
}
|
||||
|
||||
public function scopeToAudience($scope)
|
||||
{
|
||||
$res = [];
|
||||
|
||||
switch ($scope) {
|
||||
case 'to':
|
||||
$res = [
|
||||
$this->profile->permalink('/followers')
|
||||
];
|
||||
break;
|
||||
|
||||
default:
|
||||
$res = [];
|
||||
break;
|
||||
}
|
||||
|
||||
return $res;
|
||||
}
|
||||
}
|
||||
|
|
|
@ -4,6 +4,7 @@ namespace App\Transformer\ActivityPub;
|
|||
|
||||
use App\Status;
|
||||
use League\Fractal;
|
||||
use App\Services\MediaService;
|
||||
|
||||
class StatusTransformer extends Fractal\TransformerAbstract
|
||||
{
|
||||
|
@ -45,15 +46,7 @@ class StatusTransformer extends Fractal\TransformerAbstract
|
|||
'sensitive' => (bool) $status->is_nsfw,
|
||||
'atomUri' => $status->url(),
|
||||
'inReplyToAtomUri' => null,
|
||||
'attachment' => $status->media->map(function ($media) {
|
||||
return [
|
||||
'type' => 'Document',
|
||||
'mediaType' => $media->mime,
|
||||
'url' => $media->url(),
|
||||
'name' => $media->caption,
|
||||
'blurhash' => $media->blurhash
|
||||
];
|
||||
}),
|
||||
'attachment' => MediaService::activitypub($status->id),
|
||||
'tag' => [],
|
||||
'location' => $status->place_id ? [
|
||||
'type' => 'Place',
|
||||
|
|
46
app/Transformer/ActivityPub/Verb/CreateQuestion.php
Normal file
46
app/Transformer/ActivityPub/Verb/CreateQuestion.php
Normal file
|
@ -0,0 +1,46 @@
|
|||
<?php
|
||||
|
||||
namespace App\Transformer\ActivityPub\Verb;
|
||||
|
||||
use App\Status;
|
||||
use League\Fractal;
|
||||
use Illuminate\Support\Str;
|
||||
|
||||
class CreateQuestion extends Fractal\TransformerAbstract
|
||||
{
|
||||
protected $defaultIncludes = [
|
||||
'object',
|
||||
];
|
||||
|
||||
public function transform(Status $status)
|
||||
{
|
||||
return [
|
||||
'@context' => [
|
||||
'https://www.w3.org/ns/activitystreams',
|
||||
'https://w3id.org/security/v1',
|
||||
[
|
||||
'sc' => 'http://schema.org#',
|
||||
'Hashtag' => 'as:Hashtag',
|
||||
'sensitive' => 'as:sensitive',
|
||||
'commentsEnabled' => 'sc:Boolean',
|
||||
'capabilities' => [
|
||||
'announce' => ['@type' => '@id'],
|
||||
'like' => ['@type' => '@id'],
|
||||
'reply' => ['@type' => '@id']
|
||||
]
|
||||
]
|
||||
],
|
||||
'id' => $status->permalink(),
|
||||
'type' => 'Create',
|
||||
'actor' => $status->profile->permalink(),
|
||||
'published' => $status->created_at->toAtomString(),
|
||||
'to' => $status->scopeToAudience('to'),
|
||||
'cc' => $status->scopeToAudience('cc'),
|
||||
];
|
||||
}
|
||||
|
||||
public function includeObject(Status $status)
|
||||
{
|
||||
return $this->item($status, new Question());
|
||||
}
|
||||
}
|
29
app/Transformer/ActivityPub/Verb/CreateStory.php
Normal file
29
app/Transformer/ActivityPub/Verb/CreateStory.php
Normal file
|
@ -0,0 +1,29 @@
|
|||
<?php
|
||||
|
||||
namespace App\Transformer\ActivityPub\Verb;
|
||||
|
||||
use Storage;
|
||||
use App\Story;
|
||||
use League\Fractal;
|
||||
use Illuminate\Support\Str;
|
||||
|
||||
class CreateStory extends Fractal\TransformerAbstract
|
||||
{
|
||||
public function transform(Story $story)
|
||||
{
|
||||
return [
|
||||
'@context' => 'https://www.w3.org/ns/activitystreams',
|
||||
'id' => $story->permalink(),
|
||||
'type' => 'Add',
|
||||
'actor' => $story->profile->permalink(),
|
||||
'to' => [
|
||||
$story->profile->permalink('/followers')
|
||||
],
|
||||
'object' => [
|
||||
'id' => $story->url(),
|
||||
'type' => 'Story',
|
||||
'object' => $story->bearcapUrl(),
|
||||
]
|
||||
];
|
||||
}
|
||||
}
|
25
app/Transformer/ActivityPub/Verb/DeleteStory.php
Normal file
25
app/Transformer/ActivityPub/Verb/DeleteStory.php
Normal file
|
@ -0,0 +1,25 @@
|
|||
<?php
|
||||
|
||||
namespace App\Transformer\ActivityPub\Verb;
|
||||
|
||||
use Storage;
|
||||
use App\Story;
|
||||
use League\Fractal;
|
||||
use Illuminate\Support\Str;
|
||||
|
||||
class DeleteStory extends Fractal\TransformerAbstract
|
||||
{
|
||||
public function transform(Story $story)
|
||||
{
|
||||
return [
|
||||
'@context' => 'https://www.w3.org/ns/activitystreams',
|
||||
'id' => $story->url() . '#delete',
|
||||
'type' => 'Delete',
|
||||
'actor' => $story->profile->permalink(),
|
||||
'object' => [
|
||||
'id' => $story->url(),
|
||||
'type' => 'Story',
|
||||
],
|
||||
];
|
||||
}
|
||||
}
|
89
app/Transformer/ActivityPub/Verb/Question.php
Normal file
89
app/Transformer/ActivityPub/Verb/Question.php
Normal file
|
@ -0,0 +1,89 @@
|
|||
<?php
|
||||
|
||||
namespace App\Transformer\ActivityPub\Verb;
|
||||
|
||||
use App\Status;
|
||||
use League\Fractal;
|
||||
use Illuminate\Support\Str;
|
||||
|
||||
class Question 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();
|
||||
|
||||
$hashtags = $status->hashtags->map(function ($hashtag) {
|
||||
return [
|
||||
'type' => 'Hashtag',
|
||||
'href' => $hashtag->url(),
|
||||
'name' => "#{$hashtag->name}",
|
||||
];
|
||||
})->toArray();
|
||||
$tags = array_merge($mentions, $hashtags);
|
||||
|
||||
return [
|
||||
'@context' => [
|
||||
'https://www.w3.org/ns/activitystreams',
|
||||
'https://w3id.org/security/v1',
|
||||
[
|
||||
'sc' => 'http://schema.org#',
|
||||
'Hashtag' => 'as:Hashtag',
|
||||
'sensitive' => 'as:sensitive',
|
||||
'commentsEnabled' => 'sc:Boolean',
|
||||
'capabilities' => [
|
||||
'announce' => ['@type' => '@id'],
|
||||
'like' => ['@type' => '@id'],
|
||||
'reply' => ['@type' => '@id']
|
||||
]
|
||||
]
|
||||
],
|
||||
'id' => $status->url(),
|
||||
'type' => 'Question',
|
||||
'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' => [],
|
||||
'tag' => $tags,
|
||||
'commentsEnabled' => (bool) !$status->comments_disabled,
|
||||
'capabilities' => [
|
||||
'announce' => 'https://www.w3.org/ns/activitystreams#Public',
|
||||
'like' => 'https://www.w3.org/ns/activitystreams#Public',
|
||||
'reply' => $status->comments_disabled == true ? null : '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,
|
||||
'endTime' => $status->poll->expires_at->toAtomString(),
|
||||
'oneOf' => collect($status->poll->poll_options)->map(function($option, $index) use($status) {
|
||||
return [
|
||||
'type' => 'Note',
|
||||
'name' => $option,
|
||||
'replies' => [
|
||||
'type' => 'Collection',
|
||||
'totalItems' => $status->poll->cached_tallies[$index]
|
||||
]
|
||||
];
|
||||
})
|
||||
];
|
||||
}
|
||||
}
|
39
app/Transformer/ActivityPub/Verb/StoryVerb.php
Normal file
39
app/Transformer/ActivityPub/Verb/StoryVerb.php
Normal file
|
@ -0,0 +1,39 @@
|
|||
<?php
|
||||
|
||||
namespace App\Transformer\ActivityPub\Verb;
|
||||
|
||||
use Storage;
|
||||
use App\Story;
|
||||
use League\Fractal;
|
||||
use Illuminate\Support\Str;
|
||||
|
||||
class StoryVerb extends Fractal\TransformerAbstract
|
||||
{
|
||||
public function transform(Story $story)
|
||||
{
|
||||
$type = $story->type == 'photo' ? 'Image' :
|
||||
( $story->type == 'video' ? 'Video' :
|
||||
'Document' );
|
||||
|
||||
return [
|
||||
'@context' => 'https://www.w3.org/ns/activitystreams',
|
||||
'id' => $story->url(),
|
||||
'type' => 'Story',
|
||||
'to' => [
|
||||
$story->profile->permalink('/followers')
|
||||
],
|
||||
'cc' => [],
|
||||
'attributedTo' => $story->profile->permalink(),
|
||||
'published' => $story->created_at->toAtomString(),
|
||||
'expiresAt' => $story->expires_at->toAtomString(),
|
||||
'duration' => $story->duration,
|
||||
'can_reply' => (bool) $story->can_reply,
|
||||
'can_react' => (bool) $story->can_react,
|
||||
'attachment' => [
|
||||
'type' => $type,
|
||||
'url' => url(Storage::url($story->path)),
|
||||
'mediaType' => $story->mime,
|
||||
],
|
||||
];
|
||||
}
|
||||
}
|
|
@ -5,81 +5,47 @@ namespace App\Transformer\Api\Mastodon\v1;
|
|||
use App\Status;
|
||||
use League\Fractal;
|
||||
use Cache;
|
||||
use App\Services\MediaService;
|
||||
use App\Services\ProfileService;
|
||||
use App\Services\StatusHashtagService;
|
||||
|
||||
class StatusTransformer extends Fractal\TransformerAbstract
|
||||
{
|
||||
protected $defaultIncludes = [
|
||||
'account',
|
||||
'media_attachments',
|
||||
'mentions',
|
||||
'tags',
|
||||
];
|
||||
|
||||
public function transform(Status $status)
|
||||
{
|
||||
return [
|
||||
'id' => (string) $status->id,
|
||||
'created_at' => $status->created_at->toJSON(),
|
||||
'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,
|
||||
'sensitive' => (bool) $status->is_nsfw,
|
||||
'spoiler_text' => $status->cw_summary ?? '',
|
||||
'visibility' => $status->visibility ?? $status->scope,
|
||||
'language' => 'en',
|
||||
'uri' => $status->url(),
|
||||
'url' => $status->url(),
|
||||
'replies_count' => 0,
|
||||
'reblogs_count' => $status->reblogs_count ?? 0,
|
||||
'favourites_count' => $status->likes_count ?? 0,
|
||||
'reblogged' => $status->shared(),
|
||||
'favourited' => $status->liked(),
|
||||
'muted' => false,
|
||||
'bookmarked' => false,
|
||||
'pinned' => false,
|
||||
'content' => $status->rendered ?? $status->caption ?? '',
|
||||
'reblog' => null,
|
||||
'application' => [
|
||||
'name' => 'web',
|
||||
'website' => null
|
||||
],
|
||||
'mentions' => [],
|
||||
'tags' => [],
|
||||
'emojis' => [],
|
||||
'card' => null,
|
||||
'poll' => null,
|
||||
];
|
||||
}
|
||||
|
||||
public function includeAccount(Status $status)
|
||||
{
|
||||
$account = $status->profile;
|
||||
|
||||
return $this->item($account, new AccountTransformer());
|
||||
}
|
||||
|
||||
public function includeMediaAttachments(Status $status)
|
||||
{
|
||||
return Cache::remember('mastoapi:status:transformer:media:attachments:'.$status->id, now()->addDays(14), function() use($status) {
|
||||
if(in_array($status->type, ['photo', 'video', 'photo:album', 'loop', 'photo:video:album'])) {
|
||||
$media = $status->media()->orderBy('order')->get();
|
||||
return $this->collection($media, new MediaTransformer());
|
||||
} else {
|
||||
return $this->collection([], new MediaTransformer());
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
public function includeMentions(Status $status)
|
||||
{
|
||||
$mentions = $status->mentions;
|
||||
|
||||
return $this->collection($mentions, new MentionTransformer());
|
||||
}
|
||||
|
||||
public function includeTags(Status $status)
|
||||
{
|
||||
$hashtags = $status->hashtags;
|
||||
|
||||
return $this->collection($hashtags, new HashtagTransformer());
|
||||
}
|
||||
}
|
||||
public function transform(Status $status)
|
||||
{
|
||||
return [
|
||||
'id' => (string) $status->id,
|
||||
'created_at' => $status->created_at->toJSON(),
|
||||
'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,
|
||||
'sensitive' => (bool) $status->is_nsfw,
|
||||
'spoiler_text' => $status->cw_summary ?? '',
|
||||
'visibility' => $status->visibility ?? $status->scope,
|
||||
'language' => 'en',
|
||||
'uri' => $status->url(),
|
||||
'url' => $status->url(),
|
||||
'replies_count' => 0,
|
||||
'reblogs_count' => $status->reblogs_count ?? 0,
|
||||
'favourites_count' => $status->likes_count ?? 0,
|
||||
'reblogged' => $status->shared(),
|
||||
'favourited' => $status->liked(),
|
||||
'muted' => false,
|
||||
'bookmarked' => false,
|
||||
'pinned' => false,
|
||||
'content' => $status->rendered ?? $status->caption ?? '',
|
||||
'reblog' => null,
|
||||
'application' => [
|
||||
'name' => 'web',
|
||||
'website' => null
|
||||
],
|
||||
'mentions' => [],
|
||||
'tags' => [],
|
||||
'emojis' => [],
|
||||
'card' => null,
|
||||
'poll' => null,
|
||||
'media_attachments' => MediaService::get($status->id),
|
||||
'account' => ProfileService::get($status->profile_id),
|
||||
'tags' => StatusHashtagService::statusTags($status->id),
|
||||
];
|
||||
}
|
||||
}
|
||||
|
|
|
@ -59,7 +59,10 @@ class NotificationTransformer extends Fractal\TransformerAbstract
|
|||
'like' => 'favourite',
|
||||
'comment' => 'comment',
|
||||
'admin.user.modlog.comment' => 'modlog',
|
||||
'tagged' => 'tagged'
|
||||
'tagged' => 'tagged',
|
||||
'group:comment' => 'group:comment',
|
||||
'story:react' => 'story:react',
|
||||
'story:comment' => 'story:comment'
|
||||
];
|
||||
return $verbs[$verb];
|
||||
}
|
||||
|
@ -90,7 +93,6 @@ class NotificationTransformer extends Fractal\TransformerAbstract
|
|||
}
|
||||
}
|
||||
|
||||
|
||||
public function includeTagged(Notification $notification)
|
||||
{
|
||||
$n = $notification;
|
||||
|
|
|
@ -7,21 +7,19 @@ use League\Fractal;
|
|||
use Cache;
|
||||
use App\Services\HashidService;
|
||||
use App\Services\LikeService;
|
||||
use App\Services\MediaService;
|
||||
use App\Services\MediaTagService;
|
||||
use App\Services\StatusHashtagService;
|
||||
use App\Services\StatusLabelService;
|
||||
use App\Services\ProfileService;
|
||||
use App\Services\PollService;
|
||||
|
||||
class StatusStatelessTransformer extends Fractal\TransformerAbstract
|
||||
{
|
||||
protected $defaultIncludes = [
|
||||
'account',
|
||||
'tags',
|
||||
'media_attachments',
|
||||
];
|
||||
|
||||
public function transform(Status $status)
|
||||
{
|
||||
$taggedPeople = MediaTagService::get($status->id);
|
||||
$poll = $status->type === 'poll' ? PollService::get($status->id) : null;
|
||||
|
||||
return [
|
||||
'_v' => 1,
|
||||
|
@ -29,8 +27,8 @@ class StatusStatelessTransformer extends Fractal\TransformerAbstract
|
|||
'shortcode' => HashidService::encode($status->id),
|
||||
'uri' => $status->url(),
|
||||
'url' => $status->url(),
|
||||
'in_reply_to_id' => $status->in_reply_to_id,
|
||||
'in_reply_to_account_id' => $status->in_reply_to_profile_id,
|
||||
'in_reply_to_id' => (string) $status->in_reply_to_id,
|
||||
'in_reply_to_account_id' => (string) $status->in_reply_to_profile_id,
|
||||
'reblog' => null,
|
||||
'content' => $status->rendered ?? $status->caption,
|
||||
'content_text' => $status->caption,
|
||||
|
@ -43,7 +41,7 @@ class StatusStatelessTransformer extends Fractal\TransformerAbstract
|
|||
'muted' => null,
|
||||
'sensitive' => (bool) $status->is_nsfw,
|
||||
'spoiler_text' => $status->cw_summary ?? '',
|
||||
'visibility' => $status->visibility ?? $status->scope,
|
||||
'visibility' => $status->scope ?? $status->visibility,
|
||||
'application' => [
|
||||
'name' => 'web',
|
||||
'website' => null
|
||||
|
@ -62,31 +60,11 @@ class StatusStatelessTransformer extends Fractal\TransformerAbstract
|
|||
'local' => (bool) $status->local,
|
||||
'taggedPeople' => $taggedPeople,
|
||||
'label' => StatusLabelService::get($status),
|
||||
'liked_by' => LikeService::likedBy($status)
|
||||
'liked_by' => LikeService::likedBy($status),
|
||||
'media_attachments' => MediaService::get($status->id),
|
||||
'account' => ProfileService::get($status->profile_id),
|
||||
'tags' => StatusHashtagService::statusTags($status->id),
|
||||
'poll' => $poll
|
||||
];
|
||||
}
|
||||
|
||||
public function includeAccount(Status $status)
|
||||
{
|
||||
$account = $status->profile;
|
||||
|
||||
return $this->item($account, new AccountTransformer());
|
||||
}
|
||||
|
||||
public function includeTags(Status $status)
|
||||
{
|
||||
$tags = $status->hashtags;
|
||||
|
||||
return $this->collection($tags, new HashtagTransformer());
|
||||
}
|
||||
|
||||
public function includeMediaAttachments(Status $status)
|
||||
{
|
||||
return Cache::remember('status:transformer:media:attachments:'.$status->id, now()->addMinutes(3), function() use($status) {
|
||||
if(in_array($status->type, ['photo', 'video', 'video:album', 'photo:album', 'loop', 'photo:video:album'])) {
|
||||
$media = $status->media()->orderBy('order')->get();
|
||||
return $this->collection($media, new MediaTransformer());
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
|
|
|
@ -8,22 +8,20 @@ use League\Fractal;
|
|||
use Cache;
|
||||
use App\Services\HashidService;
|
||||
use App\Services\LikeService;
|
||||
use App\Services\MediaService;
|
||||
use App\Services\MediaTagService;
|
||||
use App\Services\StatusHashtagService;
|
||||
use App\Services\StatusLabelService;
|
||||
use App\Services\ProfileService;
|
||||
use Illuminate\Support\Str;
|
||||
use App\Services\PollService;
|
||||
|
||||
class StatusTransformer extends Fractal\TransformerAbstract
|
||||
{
|
||||
protected $defaultIncludes = [
|
||||
'account',
|
||||
'tags',
|
||||
'media_attachments',
|
||||
];
|
||||
|
||||
public function transform(Status $status)
|
||||
{
|
||||
$taggedPeople = MediaTagService::get($status->id);
|
||||
$poll = $status->type === 'poll' ? PollService::get($status->id, request()->user()->profile_id) : null;
|
||||
|
||||
return [
|
||||
'_v' => 1,
|
||||
|
@ -45,7 +43,7 @@ class StatusTransformer extends Fractal\TransformerAbstract
|
|||
'muted' => null,
|
||||
'sensitive' => (bool) $status->is_nsfw,
|
||||
'spoiler_text' => $status->cw_summary ?? '',
|
||||
'visibility' => $status->visibility ?? $status->scope,
|
||||
'visibility' => $status->scope ?? $status->visibility,
|
||||
'application' => [
|
||||
'name' => 'web',
|
||||
'website' => null
|
||||
|
@ -64,31 +62,11 @@ class StatusTransformer extends Fractal\TransformerAbstract
|
|||
'local' => (bool) $status->local,
|
||||
'taggedPeople' => $taggedPeople,
|
||||
'label' => StatusLabelService::get($status),
|
||||
'liked_by' => LikeService::likedBy($status)
|
||||
'liked_by' => LikeService::likedBy($status),
|
||||
'media_attachments' => MediaService::get($status->id),
|
||||
'account' => ProfileService::get($status->profile_id),
|
||||
'tags' => StatusHashtagService::statusTags($status->id),
|
||||
'poll' => $poll,
|
||||
];
|
||||
}
|
||||
|
||||
public function includeAccount(Status $status)
|
||||
{
|
||||
$account = $status->profile;
|
||||
|
||||
return $this->item($account, new AccountTransformer());
|
||||
}
|
||||
|
||||
public function includeTags(Status $status)
|
||||
{
|
||||
$tags = $status->hashtags;
|
||||
|
||||
return $this->collection($tags, new HashtagTransformer());
|
||||
}
|
||||
|
||||
public function includeMediaAttachments(Status $status)
|
||||
{
|
||||
return Cache::remember('status:transformer:media:attachments:'.$status->id, now()->addMinutes(14), function() use($status) {
|
||||
if(in_array($status->type, ['photo', 'video', 'video:album', 'photo:album', 'loop', 'photo:video:album'])) {
|
||||
$media = $status->media()->orderBy('order')->get();
|
||||
return $this->collection($media, new MediaTransformer());
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
|
|
|
@ -32,6 +32,8 @@ use App\Services\MediaPathService;
|
|||
use App\Services\MediaStorageService;
|
||||
use App\Jobs\MediaPipeline\MediaStoragePipeline;
|
||||
use App\Jobs\AvatarPipeline\RemoteAvatarFetch;
|
||||
use App\Util\Media\License;
|
||||
use App\Models\Poll;
|
||||
|
||||
class Helpers {
|
||||
|
||||
|
@ -269,7 +271,7 @@ class Helpers {
|
|||
|
||||
$res = self::fetchFromUrl($url);
|
||||
|
||||
if(!$res || empty($res) || isset($res['error']) ) {
|
||||
if(!$res || empty($res) || isset($res['error']) || !isset($res['@context']) ) {
|
||||
return;
|
||||
}
|
||||
|
||||
|
@ -330,7 +332,6 @@ class Helpers {
|
|||
$idDomain = parse_url($id, PHP_URL_HOST);
|
||||
$urlDomain = parse_url($url, PHP_URL_HOST);
|
||||
|
||||
|
||||
if(!self::validateUrl($id)) {
|
||||
return;
|
||||
}
|
||||
|
@ -367,6 +368,7 @@ class Helpers {
|
|||
$cw = true;
|
||||
}
|
||||
|
||||
|
||||
$statusLockKey = 'helpers:status-lock:' . hash('sha256', $res['id']);
|
||||
$status = Cache::lock($statusLockKey)
|
||||
->get(function () use(
|
||||
|
@ -379,6 +381,19 @@ class Helpers {
|
|||
$scope,
|
||||
$id
|
||||
) {
|
||||
if($res['type'] === 'Question') {
|
||||
$status = self::storePoll(
|
||||
$profile,
|
||||
$res,
|
||||
$url,
|
||||
$ts,
|
||||
$reply_to,
|
||||
$cw,
|
||||
$scope,
|
||||
$id
|
||||
);
|
||||
return $status;
|
||||
}
|
||||
return DB::transaction(function() use($profile, $res, $url, $ts, $reply_to, $cw, $scope, $id) {
|
||||
$status = new Status;
|
||||
$status->profile_id = $profile->id;
|
||||
|
@ -408,6 +423,55 @@ class Helpers {
|
|||
return $status;
|
||||
}
|
||||
|
||||
private static function storePoll($profile, $res, $url, $ts, $reply_to, $cw, $scope, $id)
|
||||
{
|
||||
if(!isset($res['endTime']) || !isset($res['oneOf']) || !is_array($res['oneOf']) || count($res['oneOf']) > 4) {
|
||||
return;
|
||||
}
|
||||
|
||||
$options = collect($res['oneOf'])->map(function($option) {
|
||||
return $option['name'];
|
||||
})->toArray();
|
||||
|
||||
$cachedTallies = collect($res['oneOf'])->map(function($option) {
|
||||
return $option['replies']['totalItems'] ?? 0;
|
||||
})->toArray();
|
||||
|
||||
$status = new Status;
|
||||
$status->profile_id = $profile->id;
|
||||
$status->url = isset($res['url']) ? $res['url'] : $url;
|
||||
$status->uri = isset($res['url']) ? $res['url'] : $url;
|
||||
$status->object_url = $id;
|
||||
$status->caption = strip_tags($res['content']);
|
||||
$status->rendered = Purify::clean($res['content']);
|
||||
$status->created_at = Carbon::parse($ts);
|
||||
$status->in_reply_to_id = null;
|
||||
$status->local = false;
|
||||
$status->is_nsfw = $cw;
|
||||
$status->scope = 'draft';
|
||||
$status->visibility = 'draft';
|
||||
$status->cw_summary = $cw == true && isset($res['summary']) ?
|
||||
Purify::clean(strip_tags($res['summary'])) : null;
|
||||
$status->save();
|
||||
|
||||
$poll = new Poll;
|
||||
$poll->status_id = $status->id;
|
||||
$poll->profile_id = $status->profile_id;
|
||||
$poll->poll_options = $options;
|
||||
$poll->cached_tallies = $cachedTallies;
|
||||
$poll->votes_count = array_sum($cachedTallies);
|
||||
$poll->expires_at = now()->parse($res['endTime']);
|
||||
$poll->last_fetched_at = now();
|
||||
$poll->save();
|
||||
|
||||
$status->type = 'poll';
|
||||
$status->scope = $scope;
|
||||
$status->visibility = $scope;
|
||||
$status->save();
|
||||
|
||||
return $status;
|
||||
}
|
||||
|
||||
public static function statusFetch($url)
|
||||
{
|
||||
return self::statusFirstOrFetch($url);
|
||||
|
@ -428,6 +492,7 @@ class Helpers {
|
|||
$type = $media['mediaType'];
|
||||
$url = $media['url'];
|
||||
$blurhash = isset($media['blurhash']) ? $media['blurhash'] : null;
|
||||
$license = isset($media['license']) ? License::nameToId($media['license']) : null;
|
||||
$valid = self::validateUrl($url);
|
||||
if(in_array($type, $allowed) == false || $valid == false) {
|
||||
continue;
|
||||
|
@ -441,6 +506,9 @@ class Helpers {
|
|||
$media->user_id = null;
|
||||
$media->media_path = $url;
|
||||
$media->remote_url = $url;
|
||||
if($license) {
|
||||
$media->license = $license;
|
||||
}
|
||||
$media->mime = $type;
|
||||
$media->version = 3;
|
||||
$media->save();
|
||||
|
@ -495,9 +563,12 @@ class Helpers {
|
|||
|
||||
$profile = Profile::whereRemoteUrl($res['id'])->first();
|
||||
if(!$profile) {
|
||||
Instance::firstOrCreate([
|
||||
$instance = Instance::firstOrCreate([
|
||||
'domain' => $domain
|
||||
]);
|
||||
if($instance->wasRecentlyCreated == true) {
|
||||
\App\Jobs\InstancePipeline\FetchNodeinfoPipeline::dispatch($instance)->onQueue('low');
|
||||
}
|
||||
$profileLockKey = 'helpers:profile-lock:' . hash('sha256', $res['id']);
|
||||
$profile = Cache::lock($profileLockKey)->get(function () use($domain, $webfinger, $res, $runJobs) {
|
||||
return DB::transaction(function() use($domain, $webfinger, $res, $runJobs) {
|
||||
|
|
|
@ -2,7 +2,7 @@
|
|||
|
||||
namespace App\Util\ActivityPub;
|
||||
|
||||
use Cache, DB, Log, Purify, Redis, Validator;
|
||||
use Cache, DB, Log, Purify, Redis, Storage, Validator;
|
||||
use App\{
|
||||
Activity,
|
||||
DirectMessage,
|
||||
|
@ -14,6 +14,8 @@ use App\{
|
|||
Profile,
|
||||
Status,
|
||||
StatusHashtag,
|
||||
Story,
|
||||
StoryView,
|
||||
UserFilter
|
||||
};
|
||||
use Carbon\Carbon;
|
||||
|
@ -22,6 +24,8 @@ use Illuminate\Support\Str;
|
|||
use App\Jobs\LikePipeline\LikePipeline;
|
||||
use App\Jobs\FollowPipeline\FollowPipeline;
|
||||
use App\Jobs\DeletePipeline\DeleteRemoteProfilePipeline;
|
||||
use App\Jobs\StoryPipeline\StoryExpire;
|
||||
use App\Jobs\StoryPipeline\StoryFetch;
|
||||
|
||||
use App\Util\ActivityPub\Validator\Accept as AcceptValidator;
|
||||
use App\Util\ActivityPub\Validator\Add as AddValidator;
|
||||
|
@ -30,6 +34,9 @@ use App\Util\ActivityPub\Validator\Follow as FollowValidator;
|
|||
use App\Util\ActivityPub\Validator\Like as LikeValidator;
|
||||
use App\Util\ActivityPub\Validator\UndoFollow as UndoFollowValidator;
|
||||
|
||||
use App\Services\PollService;
|
||||
use App\Services\FollowerService;
|
||||
|
||||
class Inbox
|
||||
{
|
||||
protected $headers;
|
||||
|
@ -47,16 +54,7 @@ class Inbox
|
|||
public function handle()
|
||||
{
|
||||
$this->handleVerb();
|
||||
|
||||
// if(!Activity::where('data->id', $this->payload['id'])->exists()) {
|
||||
// (new Activity())->create([
|
||||
// 'to_id' => $this->profile->id,
|
||||
// 'data' => json_encode($this->payload)
|
||||
// ]);
|
||||
// }
|
||||
|
||||
return;
|
||||
|
||||
}
|
||||
|
||||
public function handleVerb()
|
||||
|
@ -105,6 +103,18 @@ class Inbox
|
|||
$this->handleUndoActivity();
|
||||
break;
|
||||
|
||||
case 'View':
|
||||
$this->handleViewActivity();
|
||||
break;
|
||||
|
||||
case 'Story:Reaction':
|
||||
$this->handleStoryReactionActivity();
|
||||
break;
|
||||
|
||||
case 'Story:Reply':
|
||||
$this->handleStoryReplyActivity();
|
||||
break;
|
||||
|
||||
default:
|
||||
// TODO: decide how to handle invalid verbs.
|
||||
break;
|
||||
|
@ -136,6 +146,30 @@ class Inbox
|
|||
public function handleAddActivity()
|
||||
{
|
||||
// stories ;)
|
||||
|
||||
if(!isset(
|
||||
$this->payload['actor'],
|
||||
$this->payload['object']
|
||||
)) {
|
||||
return;
|
||||
}
|
||||
|
||||
$actor = $this->payload['actor'];
|
||||
$obj = $this->payload['object'];
|
||||
|
||||
if(!Helpers::validateUrl($actor)) {
|
||||
return;
|
||||
}
|
||||
|
||||
if(!isset($obj['type'])) {
|
||||
return;
|
||||
}
|
||||
|
||||
switch($obj['type']) {
|
||||
case 'Story':
|
||||
StoryFetch::dispatch($this->payload)->onQueue('story');
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
public function handleCreateActivity()
|
||||
|
@ -147,6 +181,12 @@ class Inbox
|
|||
}
|
||||
$to = $activity['to'];
|
||||
$cc = isset($activity['cc']) ? $activity['cc'] : [];
|
||||
|
||||
if($activity['type'] == 'Question') {
|
||||
$this->handlePollCreate();
|
||||
return;
|
||||
}
|
||||
|
||||
if(count($to) == 1 &&
|
||||
count($cc) == 0 &&
|
||||
parse_url($to[0], PHP_URL_HOST) == config('pixelfed.domain.app')
|
||||
|
@ -154,10 +194,11 @@ class Inbox
|
|||
$this->handleDirectMessage();
|
||||
return;
|
||||
}
|
||||
|
||||
if($activity['type'] == 'Note' && !empty($activity['inReplyTo'])) {
|
||||
$this->handleNoteReply();
|
||||
|
||||
} elseif($activity['type'] == 'Note' && !empty($activity['attachment'])) {
|
||||
} elseif ($activity['type'] == 'Note' && !empty($activity['attachment'])) {
|
||||
if(!$this->verifyNoteAttachment()) {
|
||||
return;
|
||||
}
|
||||
|
@ -180,6 +221,18 @@ class Inbox
|
|||
return;
|
||||
}
|
||||
|
||||
public function handlePollCreate()
|
||||
{
|
||||
$activity = $this->payload['object'];
|
||||
$actor = $this->actorFirstOrCreate($this->payload['actor']);
|
||||
if(!$actor || $actor->domain == null) {
|
||||
return;
|
||||
}
|
||||
$url = isset($activity['url']) ? $activity['url'] : $activity['id'];
|
||||
Helpers::statusFirstOrFetch($url);
|
||||
return;
|
||||
}
|
||||
|
||||
public function handleNoteCreate()
|
||||
{
|
||||
$activity = $this->payload['object'];
|
||||
|
@ -188,6 +241,16 @@ class Inbox
|
|||
return;
|
||||
}
|
||||
|
||||
if( isset($activity['inReplyTo']) &&
|
||||
isset($activity['name']) &&
|
||||
!isset($activity['content']) &&
|
||||
!isset($activity['attachment']) &&
|
||||
Helpers::validateLocalUrl($activity['inReplyTo'])
|
||||
) {
|
||||
$this->handlePollVote();
|
||||
return;
|
||||
}
|
||||
|
||||
if($actor->followers()->count() == 0) {
|
||||
return;
|
||||
}
|
||||
|
@ -200,6 +263,51 @@ class Inbox
|
|||
return;
|
||||
}
|
||||
|
||||
public function handlePollVote()
|
||||
{
|
||||
$activity = $this->payload['object'];
|
||||
$actor = $this->actorFirstOrCreate($this->payload['actor']);
|
||||
$status = Helpers::statusFetch($activity['inReplyTo']);
|
||||
$poll = $status->poll;
|
||||
|
||||
if(!$status || !$poll) {
|
||||
return;
|
||||
}
|
||||
|
||||
if(now()->gt($poll->expires_at)) {
|
||||
return;
|
||||
}
|
||||
|
||||
$choices = $poll->poll_options;
|
||||
$choice = array_search($activity['name'], $choices);
|
||||
|
||||
if($choice === false) {
|
||||
return;
|
||||
}
|
||||
|
||||
if(PollVote::whereStatusId($status->id)->whereProfileId($actor->id)->exists()) {
|
||||
return;
|
||||
}
|
||||
|
||||
$vote = new PollVote;
|
||||
$vote->status_id = $status->id;
|
||||
$vote->profile_id = $actor->id;
|
||||
$vote->poll_id = $poll->id;
|
||||
$vote->choice = $choice;
|
||||
$vote->uri = isset($activity['id']) ? $activity['id'] : null;
|
||||
$vote->save();
|
||||
|
||||
$tallies = $poll->cached_tallies;
|
||||
$tallies[$choice] = $tallies[$choice] + 1;
|
||||
$poll->cached_tallies = $tallies;
|
||||
$poll->votes_count = array_sum($tallies);
|
||||
$poll->save();
|
||||
|
||||
PollService::del($status->id);
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
public function handleDirectMessage()
|
||||
{
|
||||
$activity = $this->payload['object'];
|
||||
|
@ -420,7 +528,6 @@ class Inbox
|
|||
|
||||
public function handleAcceptActivity()
|
||||
{
|
||||
|
||||
$actor = $this->payload['object']['actor'];
|
||||
$obj = $this->payload['object']['object'];
|
||||
$type = $this->payload['object']['type'];
|
||||
|
@ -480,7 +587,7 @@ class Inbox
|
|||
return;
|
||||
} else {
|
||||
$type = $this->payload['object']['type'];
|
||||
$typeCheck = in_array($type, ['Person', 'Tombstone']);
|
||||
$typeCheck = in_array($type, ['Person', 'Tombstone', 'Story']);
|
||||
if(!Helpers::validateUrl($actor) || !Helpers::validateUrl($obj['id']) || !$typeCheck) {
|
||||
return;
|
||||
}
|
||||
|
@ -520,6 +627,13 @@ class Inbox
|
|||
return;
|
||||
break;
|
||||
|
||||
case 'Story':
|
||||
$story = Story::whereObjectId($id)
|
||||
->first();
|
||||
if($story) {
|
||||
StoryExpire::dispatch($story)->onQueue('story');
|
||||
}
|
||||
|
||||
default:
|
||||
return;
|
||||
break;
|
||||
|
@ -558,10 +672,8 @@ class Inbox
|
|||
return;
|
||||
}
|
||||
|
||||
|
||||
public function handleRejectActivity()
|
||||
{
|
||||
|
||||
}
|
||||
|
||||
public function handleUndoActivity()
|
||||
|
@ -631,4 +743,250 @@ class Inbox
|
|||
}
|
||||
return;
|
||||
}
|
||||
|
||||
public function handleViewActivity()
|
||||
{
|
||||
if(!isset(
|
||||
$this->payload['actor'],
|
||||
$this->payload['object']
|
||||
)) {
|
||||
return;
|
||||
}
|
||||
|
||||
$actor = $this->payload['actor'];
|
||||
$obj = $this->payload['object'];
|
||||
|
||||
if(!Helpers::validateUrl($actor)) {
|
||||
return;
|
||||
}
|
||||
|
||||
if(!$obj || !is_array($obj)) {
|
||||
return;
|
||||
}
|
||||
|
||||
if(!isset($obj['type']) || !isset($obj['object']) || $obj['type'] != 'Story') {
|
||||
return;
|
||||
}
|
||||
|
||||
if(!Helpers::validateLocalUrl($obj['object'])) {
|
||||
return;
|
||||
}
|
||||
|
||||
$profile = Helpers::profileFetch($actor);
|
||||
$storyId = Str::of($obj['object'])->explode('/')->last();
|
||||
|
||||
$story = Story::whereActive(true)
|
||||
->whereLocal(true)
|
||||
->find($storyId);
|
||||
|
||||
if(!$story) {
|
||||
return;
|
||||
}
|
||||
|
||||
if(!FollowerService::follows($profile->id, $story->profile_id)) {
|
||||
return;
|
||||
}
|
||||
|
||||
$view = StoryView::firstOrCreate([
|
||||
'story_id' => $story->id,
|
||||
'profile_id' => $profile->id
|
||||
]);
|
||||
|
||||
if($view->wasRecentlyCreated == true) {
|
||||
$story->view_count++;
|
||||
$story->save();
|
||||
}
|
||||
}
|
||||
|
||||
public function handleStoryReactionActivity()
|
||||
{
|
||||
if(!isset(
|
||||
$this->payload['actor'],
|
||||
$this->payload['id'],
|
||||
$this->payload['inReplyTo'],
|
||||
$this->payload['content']
|
||||
)) {
|
||||
return;
|
||||
}
|
||||
|
||||
$id = $this->payload['id'];
|
||||
$actor = $this->payload['actor'];
|
||||
$storyUrl = $this->payload['inReplyTo'];
|
||||
$to = $this->payload['to'];
|
||||
$text = Purify::clean($this->payload['content']);
|
||||
|
||||
if(parse_url($id, PHP_URL_HOST) !== parse_url($actor, PHP_URL_HOST)) {
|
||||
return;
|
||||
}
|
||||
|
||||
if(!Helpers::validateUrl($id) || !Helpers::validateUrl($actor)) {
|
||||
return;
|
||||
}
|
||||
|
||||
if(!Helpers::validateLocalUrl($storyUrl)) {
|
||||
return;
|
||||
}
|
||||
|
||||
if(!Helpers::validateLocalUrl($to)) {
|
||||
return;
|
||||
}
|
||||
|
||||
if(Status::whereObjectUrl($id)->exists()) {
|
||||
return;
|
||||
}
|
||||
|
||||
$storyId = Str::of($storyUrl)->explode('/')->last();
|
||||
$targetProfile = Helpers::profileFetch($to);
|
||||
|
||||
$story = Story::whereProfileId($targetProfile->id)
|
||||
->find($storyId);
|
||||
|
||||
if(!$story) {
|
||||
return;
|
||||
}
|
||||
|
||||
if($story->can_react == false) {
|
||||
return;
|
||||
}
|
||||
|
||||
$actorProfile = Helpers::profileFetch($actor);
|
||||
|
||||
if(!FollowerService::follows($actorProfile->id, $targetProfile->id)) {
|
||||
return;
|
||||
}
|
||||
|
||||
$status = new Status;
|
||||
$status->profile_id = $actorProfile->id;
|
||||
$status->type = 'story:reaction';
|
||||
$status->caption = $text;
|
||||
$status->rendered = $text;
|
||||
$status->scope = 'direct';
|
||||
$status->visibility = 'direct';
|
||||
$status->in_reply_to_profile_id = $story->profile_id;
|
||||
$status->entities = json_encode([
|
||||
'story_id' => $story->id,
|
||||
'reaction' => $text
|
||||
]);
|
||||
$status->save();
|
||||
|
||||
$dm = new DirectMessage;
|
||||
$dm->to_id = $story->profile_id;
|
||||
$dm->from_id = $actorProfile->id;
|
||||
$dm->type = 'story:react';
|
||||
$dm->status_id = $status->id;
|
||||
$dm->meta = json_encode([
|
||||
'story_username' => $targetProfile->username,
|
||||
'story_actor_username' => $actorProfile->username,
|
||||
'story_id' => $story->id,
|
||||
'story_media_url' => url(Storage::url($story->path)),
|
||||
'reaction' => $text
|
||||
]);
|
||||
$dm->save();
|
||||
|
||||
$n = new Notification;
|
||||
$n->profile_id = $dm->to_id;
|
||||
$n->actor_id = $dm->from_id;
|
||||
$n->item_id = $dm->id;
|
||||
$n->item_type = 'App\DirectMessage';
|
||||
$n->action = 'story:react';
|
||||
$n->message = "{$actorProfile->username} reacted to your story";
|
||||
$n->rendered = "{$actorProfile->username} reacted to your story";
|
||||
$n->save();
|
||||
}
|
||||
|
||||
public function handleStoryReplyActivity()
|
||||
{
|
||||
if(!isset(
|
||||
$this->payload['actor'],
|
||||
$this->payload['id'],
|
||||
$this->payload['inReplyTo'],
|
||||
$this->payload['content']
|
||||
)) {
|
||||
return;
|
||||
}
|
||||
|
||||
$id = $this->payload['id'];
|
||||
$actor = $this->payload['actor'];
|
||||
$storyUrl = $this->payload['inReplyTo'];
|
||||
$to = $this->payload['to'];
|
||||
$text = Purify::clean($this->payload['content']);
|
||||
|
||||
if(parse_url($id, PHP_URL_HOST) !== parse_url($actor, PHP_URL_HOST)) {
|
||||
return;
|
||||
}
|
||||
|
||||
if(!Helpers::validateUrl($id) || !Helpers::validateUrl($actor)) {
|
||||
return;
|
||||
}
|
||||
|
||||
if(!Helpers::validateLocalUrl($storyUrl)) {
|
||||
return;
|
||||
}
|
||||
|
||||
if(!Helpers::validateLocalUrl($to)) {
|
||||
return;
|
||||
}
|
||||
|
||||
if(Status::whereObjectUrl($id)->exists()) {
|
||||
return;
|
||||
}
|
||||
|
||||
$storyId = Str::of($storyUrl)->explode('/')->last();
|
||||
$targetProfile = Helpers::profileFetch($to);
|
||||
|
||||
$story = Story::whereProfileId($targetProfile->id)
|
||||
->find($storyId);
|
||||
|
||||
if(!$story) {
|
||||
return;
|
||||
}
|
||||
|
||||
if($story->can_react == false) {
|
||||
return;
|
||||
}
|
||||
|
||||
$actorProfile = Helpers::profileFetch($actor);
|
||||
|
||||
if(!FollowerService::follows($actorProfile->id, $targetProfile->id)) {
|
||||
return;
|
||||
}
|
||||
|
||||
$status = new Status;
|
||||
$status->profile_id = $actorProfile->id;
|
||||
$status->type = 'story:reply';
|
||||
$status->caption = $text;
|
||||
$status->rendered = $text;
|
||||
$status->scope = 'direct';
|
||||
$status->visibility = 'direct';
|
||||
$status->in_reply_to_profile_id = $story->profile_id;
|
||||
$status->entities = json_encode([
|
||||
'story_id' => $story->id,
|
||||
'caption' => $text
|
||||
]);
|
||||
$status->save();
|
||||
|
||||
$dm = new DirectMessage;
|
||||
$dm->to_id = $story->profile_id;
|
||||
$dm->from_id = $actorProfile->id;
|
||||
$dm->type = 'story:comment';
|
||||
$dm->status_id = $status->id;
|
||||
$dm->meta = json_encode([
|
||||
'story_username' => $targetProfile->username,
|
||||
'story_actor_username' => $actorProfile->username,
|
||||
'story_id' => $story->id,
|
||||
'story_media_url' => url(Storage::url($story->path)),
|
||||
'caption' => $text
|
||||
]);
|
||||
$dm->save();
|
||||
|
||||
$n = new Notification;
|
||||
$n->profile_id = $dm->to_id;
|
||||
$n->actor_id = $dm->from_id;
|
||||
$n->item_id = $dm->id;
|
||||
$n->item_type = 'App\DirectMessage';
|
||||
$n->action = 'story:comment';
|
||||
$n->message = "{$actorProfile->username} commented on story";
|
||||
$n->rendered = "{$actorProfile->username} commented on story";
|
||||
$n->save();
|
||||
}
|
||||
}
|
||||
|
|
34
app/Util/ActivityPub/Validator/StoryValidator.php
Normal file
34
app/Util/ActivityPub/Validator/StoryValidator.php
Normal file
|
@ -0,0 +1,34 @@
|
|||
<?php
|
||||
|
||||
namespace App\Util\ActivityPub\Validator;
|
||||
|
||||
use Validator;
|
||||
use Illuminate\Validation\Rule;
|
||||
|
||||
class StoryValidator {
|
||||
|
||||
public static function validate($payload)
|
||||
{
|
||||
$valid = Validator::make($payload, [
|
||||
'@context' => 'required',
|
||||
'id' => 'required|string',
|
||||
'type' => [
|
||||
'required',
|
||||
Rule::in(['Story'])
|
||||
],
|
||||
'to' => 'required',
|
||||
'attributedTo' => 'required|url',
|
||||
'published' => 'required|date',
|
||||
'expiresAt' => 'required|date',
|
||||
'duration' => 'required|integer|min:1|max:300',
|
||||
'can_react' => 'required|boolean',
|
||||
'can_reply' => 'required|boolean',
|
||||
'attachment' => 'required',
|
||||
'attachment.type' => 'required|in:Image,Video',
|
||||
'attachment.url' => 'required|url',
|
||||
'attachment.mediaType' => 'required'
|
||||
])->passes();
|
||||
|
||||
return $valid;
|
||||
}
|
||||
}
|
57
app/Util/Lexer/Bearcap.php
Normal file
57
app/Util/Lexer/Bearcap.php
Normal file
|
@ -0,0 +1,57 @@
|
|||
<?php
|
||||
|
||||
namespace App\Util\Lexer;
|
||||
|
||||
use Illuminate\Support\Str;
|
||||
use App\Util\ActivityPub\Helpers;
|
||||
|
||||
class Bearcap
|
||||
{
|
||||
public static function encode($url, $token)
|
||||
{
|
||||
return "bear:?t={$token}&u={$url}";
|
||||
}
|
||||
|
||||
public static function decode($str)
|
||||
{
|
||||
if(!Str::startsWith($str, 'bear:')) {
|
||||
return false;
|
||||
}
|
||||
|
||||
$query = parse_url($str, PHP_URL_QUERY);
|
||||
|
||||
if(!$query) {
|
||||
return false;
|
||||
}
|
||||
|
||||
$res = [];
|
||||
|
||||
$parts = Str::of($str)->substr(6)->explode('&')->toArray();
|
||||
|
||||
foreach($parts as $part) {
|
||||
if(Str::startsWith($part, 't=')) {
|
||||
$res['token'] = substr($part, 2);
|
||||
}
|
||||
|
||||
if(Str::startsWith($part, 'u=')) {
|
||||
$res['url'] = substr($part, 2);
|
||||
}
|
||||
}
|
||||
|
||||
if( !isset($res['token']) ||
|
||||
!isset($res['url'])
|
||||
) {
|
||||
return false;
|
||||
}
|
||||
|
||||
$url = $res['url'];
|
||||
if(mb_substr($url, 0, 8) !== 'https://') {
|
||||
return false;
|
||||
}
|
||||
$valid = filter_var($url, FILTER_VALIDATE_URL);
|
||||
if(!$valid) {
|
||||
return false;
|
||||
}
|
||||
return $res;
|
||||
}
|
||||
}
|
|
@ -120,4 +120,19 @@ class License {
|
|||
->values()
|
||||
->toArray();
|
||||
}
|
||||
|
||||
public static function nameToId($name)
|
||||
{
|
||||
$license = collect(self::get())
|
||||
->filter(function($l) use($name) {
|
||||
return $l['title'] == $name;
|
||||
})
|
||||
->first();
|
||||
|
||||
if(!$license || $license['id'] < 2) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return $license['id'];
|
||||
}
|
||||
}
|
||||
|
|
|
@ -7,7 +7,7 @@ use Illuminate\Support\Str;
|
|||
|
||||
class Config {
|
||||
|
||||
const CACHE_KEY = 'api:site:configuration:_v0.3';
|
||||
const CACHE_KEY = 'api:site:configuration:_v0.4';
|
||||
|
||||
public static function get() {
|
||||
return Cache::remember(self::CACHE_KEY, now()->addMinutes(5), function() {
|
||||
|
@ -37,7 +37,8 @@ class Config {
|
|||
'lc' => config('exp.lc'),
|
||||
'rec' => config('exp.rec'),
|
||||
'loops' => config('exp.loops'),
|
||||
'top' => config('exp.top')
|
||||
'top' => config('exp.top'),
|
||||
'polls' => config('exp.polls')
|
||||
],
|
||||
|
||||
'site' => [
|
||||
|
|
|
@ -1,7 +1,8 @@
|
|||
<?php
|
||||
|
||||
return [
|
||||
use Illuminate\Database\DBAL\TimestampType;
|
||||
|
||||
return [
|
||||
/*
|
||||
|--------------------------------------------------------------------------
|
||||
| Default Database Connection Name
|
||||
|
@ -119,4 +120,9 @@ return [
|
|||
|
||||
],
|
||||
|
||||
'dbal' => [
|
||||
'types' => [
|
||||
'timestamp' => TimestampType::class,
|
||||
],
|
||||
],
|
||||
];
|
||||
|
|
|
@ -6,4 +6,5 @@ return [
|
|||
'rec' => false,
|
||||
'loops' => false,
|
||||
'top' => env('EXP_TOP', false),
|
||||
'polls' => env('EXP_POLLS', false)
|
||||
];
|
||||
|
|
|
@ -81,6 +81,7 @@ return [
|
|||
'waits' => [
|
||||
'redis:feed' => 30,
|
||||
'redis:default' => 30,
|
||||
'redis:low' => 30,
|
||||
'redis:high' => 30,
|
||||
'redis:delete' => 30
|
||||
],
|
||||
|
@ -166,7 +167,7 @@ return [
|
|||
'production' => [
|
||||
'supervisor-1' => [
|
||||
'connection' => 'redis',
|
||||
'queue' => ['high', 'default', 'feed', 'delete'],
|
||||
'queue' => ['high', 'default', 'feed', 'low', 'story', 'delete'],
|
||||
'balance' => 'auto',
|
||||
'maxProcesses' => 20,
|
||||
'memory' => 128,
|
||||
|
@ -178,7 +179,7 @@ return [
|
|||
'local' => [
|
||||
'supervisor-1' => [
|
||||
'connection' => 'redis',
|
||||
'queue' => ['high', 'default', 'feed', 'delete'],
|
||||
'queue' => ['high', 'default', 'feed', 'low', 'story', 'delete'],
|
||||
'balance' => 'auto',
|
||||
'maxProcesses' => 20,
|
||||
'memory' => 128,
|
||||
|
|
|
@ -14,7 +14,7 @@ return [
|
|||
'optimizers' => [
|
||||
|
||||
Jpegoptim::class => [
|
||||
'-m75', // set maximum quality to 75%
|
||||
'-m' . (int) env('IMAGE_QUALITY', 80),
|
||||
'--strip-all', // this strips out all text information such as comments and EXIF data
|
||||
'--all-progressive', // this will make sure the resulting image is a progressive one
|
||||
],
|
||||
|
|
|
@ -47,6 +47,10 @@ return [
|
|||
]
|
||||
],
|
||||
|
||||
'polls' => [
|
||||
'enabled' => false
|
||||
],
|
||||
|
||||
'stories' => [
|
||||
'enabled' => env('STORIES_ENABLED', false),
|
||||
],
|
||||
|
|
|
@ -278,4 +278,6 @@ return [
|
|||
|
|
||||
*/
|
||||
'media_fast_process' => env('PF_MEDIA_FAST_PROCESS', true),
|
||||
|
||||
'max_altext_length' => env('PF_MEDIA_MAX_ALTTEXT_LENGTH', 1000),
|
||||
];
|
||||
|
|
|
@ -0,0 +1,46 @@
|
|||
<?php
|
||||
|
||||
use Illuminate\Database\Migrations\Migration;
|
||||
use Illuminate\Database\Schema\Blueprint;
|
||||
use Illuminate\Support\Facades\Schema;
|
||||
|
||||
class AddComposeSettingsToUserSettingsTable extends Migration
|
||||
{
|
||||
/**
|
||||
* Run the migrations.
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
public function up()
|
||||
{
|
||||
Schema::table('user_settings', function (Blueprint $table) {
|
||||
$table->json('compose_settings')->nullable();
|
||||
});
|
||||
|
||||
Schema::table('media', function (Blueprint $table) {
|
||||
$table->text('caption')->change();
|
||||
$table->index('profile_id');
|
||||
$table->index('mime');
|
||||
$table->index('license');
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Reverse the migrations.
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
public function down()
|
||||
{
|
||||
Schema::table('user_settings', function (Blueprint $table) {
|
||||
$table->dropColumn('compose_settings');
|
||||
});
|
||||
|
||||
Schema::table('media', function (Blueprint $table) {
|
||||
$table->string('caption')->change();
|
||||
$table->dropIndex('profile_id');
|
||||
$table->dropIndex('mime');
|
||||
$table->dropIndex('license');
|
||||
});
|
||||
}
|
||||
}
|
42
database/migrations/2021_07_29_014835_create_polls_table.php
Normal file
42
database/migrations/2021_07_29_014835_create_polls_table.php
Normal file
|
@ -0,0 +1,42 @@
|
|||
<?php
|
||||
|
||||
use Illuminate\Database\Migrations\Migration;
|
||||
use Illuminate\Database\Schema\Blueprint;
|
||||
use Illuminate\Support\Facades\Schema;
|
||||
|
||||
class CreatePollsTable extends Migration
|
||||
{
|
||||
/**
|
||||
* Run the migrations.
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
public function up()
|
||||
{
|
||||
Schema::create('polls', function (Blueprint $table) {
|
||||
$table->bigInteger('id')->unsigned()->primary();
|
||||
$table->bigInteger('story_id')->unsigned()->nullable()->index();
|
||||
$table->bigInteger('status_id')->unsigned()->nullable()->index();
|
||||
$table->bigInteger('group_id')->unsigned()->nullable()->index();
|
||||
$table->bigInteger('profile_id')->unsigned()->index();
|
||||
$table->json('poll_options')->nullable();
|
||||
$table->json('cached_tallies')->nullable();
|
||||
$table->boolean('multiple')->default(false);
|
||||
$table->boolean('hide_totals')->default(false);
|
||||
$table->unsignedInteger('votes_count')->default(0);
|
||||
$table->timestamp('last_fetched_at')->nullable();
|
||||
$table->timestamp('expires_at')->nullable();
|
||||
$table->timestamps();
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Reverse the migrations.
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
public function down()
|
||||
{
|
||||
Schema::dropIfExists('polls');
|
||||
}
|
||||
}
|
|
@ -0,0 +1,37 @@
|
|||
<?php
|
||||
|
||||
use Illuminate\Database\Migrations\Migration;
|
||||
use Illuminate\Database\Schema\Blueprint;
|
||||
use Illuminate\Support\Facades\Schema;
|
||||
|
||||
class CreatePollVotesTable extends Migration
|
||||
{
|
||||
/**
|
||||
* Run the migrations.
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
public function up()
|
||||
{
|
||||
Schema::create('poll_votes', function (Blueprint $table) {
|
||||
$table->id();
|
||||
$table->bigInteger('story_id')->unsigned()->nullable()->index();
|
||||
$table->bigInteger('status_id')->unsigned()->nullable()->index();
|
||||
$table->bigInteger('profile_id')->unsigned()->index();
|
||||
$table->bigInteger('poll_id')->unsigned()->index();
|
||||
$table->unsignedInteger('choice')->default(0)->index();
|
||||
$table->string('uri')->nullable();
|
||||
$table->timestamps();
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Reverse the migrations.
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
public function down()
|
||||
{
|
||||
Schema::dropIfExists('poll_votes');
|
||||
}
|
||||
}
|
|
@ -0,0 +1,54 @@
|
|||
<?php
|
||||
|
||||
use Illuminate\Database\Migrations\Migration;
|
||||
use Illuminate\Database\Schema\Blueprint;
|
||||
use Illuminate\Support\Facades\Schema;
|
||||
|
||||
class UpdateStoriesTableFixExpiresAtColumn extends Migration
|
||||
{
|
||||
/**
|
||||
* Run the migrations.
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
public function up()
|
||||
{
|
||||
Schema::table('stories', function (Blueprint $table) {
|
||||
$sm = Schema::getConnection()->getDoctrineSchemaManager();
|
||||
$doctrineTable = $sm->listTableDetails('stories');
|
||||
|
||||
if($doctrineTable->hasIndex('stories_expires_at_index')) {
|
||||
$table->dropIndex('stories_expires_at_index');
|
||||
}
|
||||
$table->timestamp('expires_at')->default(null)->index()->nullable()->change();
|
||||
$table->boolean('can_reply')->default(true);
|
||||
$table->boolean('can_react')->default(true);
|
||||
$table->string('object_id')->nullable()->unique();
|
||||
$table->string('object_uri')->nullable()->unique();
|
||||
$table->string('bearcap_token')->nullable();
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Reverse the migrations.
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
public function down()
|
||||
{
|
||||
Schema::table('stories', function (Blueprint $table) {
|
||||
$sm = Schema::getConnection()->getDoctrineSchemaManager();
|
||||
$doctrineTable = $sm->listTableDetails('stories');
|
||||
|
||||
if($doctrineTable->hasIndex('stories_expires_at_index')) {
|
||||
$table->dropIndex('stories_expires_at_index');
|
||||
}
|
||||
$table->timestamp('expires_at')->default(null)->index()->nullable()->change();
|
||||
$table->dropColumn('can_reply');
|
||||
$table->dropColumn('can_react');
|
||||
$table->dropColumn('object_id');
|
||||
$table->dropColumn('object_uri');
|
||||
$table->dropColumn('bearcap_token');
|
||||
});
|
||||
}
|
||||
}
|
|
@ -0,0 +1,46 @@
|
|||
<?php
|
||||
|
||||
use Illuminate\Database\Migrations\Migration;
|
||||
use Illuminate\Database\Schema\Blueprint;
|
||||
use Illuminate\Support\Facades\Schema;
|
||||
use App\Jobs\InstancePipeline\InstanceCrawlPipeline;
|
||||
|
||||
class AddSoftwareColumnToInstancesTable extends Migration
|
||||
{
|
||||
/**
|
||||
* Run the migrations.
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
public function up()
|
||||
{
|
||||
Schema::table('instances', function (Blueprint $table) {
|
||||
$table->string('software')->nullable()->index();
|
||||
$table->unsignedInteger('user_count')->nullable();
|
||||
$table->unsignedInteger('status_count')->nullable();
|
||||
$table->timestamp('last_crawled_at')->nullable();
|
||||
});
|
||||
|
||||
$this->runPostMigration();
|
||||
}
|
||||
|
||||
/**
|
||||
* Reverse the migrations.
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
public function down()
|
||||
{
|
||||
Schema::table('instances', function (Blueprint $table) {
|
||||
$table->dropColumn('software');
|
||||
$table->dropColumn('user_count');
|
||||
$table->dropColumn('status_count');
|
||||
$table->dropColumn('last_crawled_at');
|
||||
});
|
||||
}
|
||||
|
||||
protected function runPostMigration()
|
||||
{
|
||||
InstanceCrawlPipeline::dispatch();
|
||||
}
|
||||
}
|
BIN
public/css/app.css
vendored
BIN
public/css/app.css
vendored
Binary file not shown.
BIN
public/css/appdark.css
vendored
BIN
public/css/appdark.css
vendored
Binary file not shown.
BIN
public/css/landing.css
vendored
BIN
public/css/landing.css
vendored
Binary file not shown.
BIN
public/fonts/fa-light-300.eot
Normal file
BIN
public/fonts/fa-light-300.eot
Normal file
Binary file not shown.
BIN
public/fonts/fa-light-300.svg
Normal file
BIN
public/fonts/fa-light-300.svg
Normal file
Binary file not shown.
After Width: | Height: | Size: 2.3 MiB |
BIN
public/fonts/fa-light-300.ttf
Normal file
BIN
public/fonts/fa-light-300.ttf
Normal file
Binary file not shown.
BIN
public/fonts/fa-light-300.woff
Normal file
BIN
public/fonts/fa-light-300.woff
Normal file
Binary file not shown.
BIN
public/fonts/fa-light-300.woff2
Normal file
BIN
public/fonts/fa-light-300.woff2
Normal file
Binary file not shown.
Binary file not shown.
Binary file not shown.
Before Width: | Height: | Size: 141 KiB After Width: | Height: | Size: 2.1 MiB |
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Before Width: | Height: | Size: 797 KiB After Width: | Height: | Size: 1.7 MiB |
Binary file not shown.
Binary file not shown.
Binary file not shown.
Some files were not shown because too many files have changed in this diff Show more
Loading…
Reference in a new issue