mirror of
https://github.com/pixelfed/pixelfed.git
synced 2024-11-22 14:31:26 +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);
|
||||
collect($queue)
|
||||
->each(function($id) {
|
||||
$story = StoryService::getById($id);
|
||||
if(!$story) {
|
||||
StoryService::removeRotateQueue($id);
|
||||
return;
|
||||
}
|
||||
if($story->created_at->gt(now()->subMinutes(20))) {
|
||||
return;
|
||||
}
|
||||
|
||||
$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();
|
||||
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();
|
||||
$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 {
|
||||
$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);
|
||||
$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) {
|
||||
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(),
|
||||
];
|
||||
return $res;
|
||||
});
|
||||
return response()->json($res);
|
||||
}
|
||||
$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) : [];
|
||||
$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);
|
||||
$types = $textOnlyPosts ?
|
||||
['text', 'photo', 'photo:album', 'video', 'video:album', 'photo:video:album'] :
|
||||
['photo', 'photo:album', 'video', 'video:album', 'photo:video:album'];
|
||||
|
||||
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);
|
||||
|
||||
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)
|
||||
'created_at'
|
||||
)
|
||||
->whereProfileId($profile->id)
|
||||
->whereNull('in_reply_to_id')
|
||||
->whereNull('reblog_of_id')
|
||||
->whereIn('type', $scope)
|
||||
->where('id', $dir, $id)
|
||||
->whereIn('visibility', $visibility)
|
||||
->whereIn('scope', $visibility)
|
||||
->limit($limit)
|
||||
->orderByDesc('id')
|
||||
->get();
|
||||
->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');
|
||||
}
|
||||
}
|
||||
|
|
|
@ -46,6 +46,9 @@ class FollowPipeline implements ShouldQueue
|
|||
$actor = $follower->actor;
|
||||
$target = $follower->target;
|
||||
|
||||
Cache::forget('profile:following:' . $actor->id);
|
||||
Cache::forget('profile:following:' . $target->id);
|
||||
|
||||
if($target->domain || !$target->private_key) {
|
||||
return;
|
||||
}
|
||||
|
|
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;
|
||||
|
@ -62,10 +63,20 @@ class StatusActivityPubDeliver implements ShouldQueue
|
|||
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());
|
||||
$resource = new Fractal\Resource\Item($status, $activitypubObject);
|
||||
$activity = $fractal->createData($resource)->toArray();
|
||||
|
||||
$payload = json_encode($activity);
|
||||
|
@ -82,7 +93,9 @@ class StatusActivityPubDeliver implements ShouldQueue
|
|||
'curl' => [
|
||||
CURLOPT_HTTPHEADER => $headers,
|
||||
CURLOPT_POSTFIELDS => $payload,
|
||||
CURLOPT_HEADER => true
|
||||
CURLOPT_HEADER => true,
|
||||
CURLOPT_SSL_VERIFYPEER => false,
|
||||
CURLOPT_SSL_VERIFYHOST => false
|
||||
]
|
||||
]);
|
||||
};
|
||||
|
|
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
|
||||
{
|
||||
|
|
|
@ -2,10 +2,11 @@
|
|||
|
||||
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
|
||||
{
|
||||
|
@ -68,7 +69,7 @@ class Profile extends Model
|
|||
if($this->domain == null && $this->user->settings->show_profile_following_count == false) {
|
||||
return 0;
|
||||
}
|
||||
$count = $this->following()->count();
|
||||
$count = DB::table('followers')->select('following_id')->where('following_id', $this->id)->count();
|
||||
if($this->following_count != $count) {
|
||||
$this->following_count = $count;
|
||||
$this->save();
|
||||
|
@ -276,15 +277,7 @@ class Profile extends Model
|
|||
|
||||
public function getAudienceInbox($scope = 'public')
|
||||
{
|
||||
return $this
|
||||
->followers()
|
||||
->whereLocalProfile(false)
|
||||
->get()
|
||||
->map(function($follow) {
|
||||
return $follow->sharedInbox ?? $follow->inbox_url;
|
||||
})
|
||||
->unique()
|
||||
->toArray();
|
||||
return FollowerService::audience($this->id, $scope);
|
||||
}
|
||||
|
||||
public function circles()
|
||||
|
|
|
@ -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;
|
||||
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,24 +52,24 @@ 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
|
||||
];
|
||||
});
|
||||
}
|
||||
}
|
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) {
|
||||
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;
|
||||
}
|
||||
}
|
|
@ -4,9 +4,10 @@ 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
|
||||
{
|
||||
|
@ -48,7 +49,7 @@ class Status extends Model
|
|||
|
||||
const MAX_HASHTAGS = 30;
|
||||
|
||||
const MAX_LINKS = 0;
|
||||
const MAX_LINKS = 2;
|
||||
|
||||
public function profile()
|
||||
{
|
||||
|
@ -101,10 +102,10 @@ class Status extends Model
|
|||
});
|
||||
}
|
||||
|
||||
public function url()
|
||||
public function url($forceLocal = false)
|
||||
{
|
||||
if($this->uri) {
|
||||
return $this->uri;
|
||||
return $forceLocal ? "/i/web/post/_/{$this->profile_id}/{$this->id}" : $this->uri;
|
||||
} else {
|
||||
$id = $this->id;
|
||||
$username = $this->profile->username;
|
||||
|
@ -414,4 +415,8 @@ class Status extends Model
|
|||
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,16 +5,12 @@ 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 [
|
||||
|
@ -47,39 +43,9 @@ class StatusTransformer extends Fractal\TransformerAbstract
|
|||
'emojis' => [],
|
||||
'card' => null,
|
||||
'poll' => null,
|
||||
'media_attachments' => MediaService::get($status->id),
|
||||
'account' => ProfileService::get($status->profile_id),
|
||||
'tags' => StatusHashtagService::statusTags($status->id),
|
||||
];
|
||||
}
|
||||
|
||||
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());
|
||||
}
|
||||
}
|
|
@ -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.
BIN
public/js/activity.js
vendored
BIN
public/js/activity.js
vendored
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