mirror of https://github.com/pixelfed/pixelfed
commit
f593e2b709
@ -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);
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
@ -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;
|
||||
}
|
||||
}
|
@ -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;
|
||||
}
|
||||
}
|
@ -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();
|
||||
}
|
||||
}
|
||||
}
|
@ -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');
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
@ -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);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
}
|
@ -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();
|
||||
}
|
||||
}
|
@ -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();
|
||||
}
|
||||
}
|
@ -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();
|
||||
}
|
||||
}
|
@ -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);
|
||||
}
|
||||
}
|
@ -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);
|
||||
}
|
||||
}
|
@ -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);
|
||||
}
|
||||
}
|
@ -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);
|
||||
}
|
||||
}
|
||||
}
|
@ -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);
|
||||
}
|
||||
}
|
@ -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;
|
||||
}
|
||||
}
|
@ -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;
|
||||
}
|
@ -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);
|
||||
}
|
||||
}
|
@ -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
|
||||
];
|
||||
});
|
||||
}
|
||||
}
|
@ -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();
|
||||
}
|
||||
}
|
@ -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') ?? [];
|
||||
}
|
||||
}
|
@ -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;
|
||||
}
|
||||
}
|
@ -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());
|
||||
}
|
||||
}
|
@ -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(),
|
||||
]
|
||||
];
|
||||
}
|
||||
}
|
@ -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',
|
||||
],
|
||||
];
|
||||
}
|
||||
}
|
@ -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]
|
||||
]
|
||||
];
|
||||
})
|
||||
];
|
||||
}
|
||||
}
|
@ -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,
|
||||
],
|
||||
];
|
||||
}
|
||||
}
|
@ -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;
|
||||
}
|
||||
}
|
@ -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;
|
||||
}
|
||||
}
|
@ -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');
|
||||
});
|
||||
}
|
||||
}
|
@ -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();
|
||||
}
|
||||
}
|
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
Binary file not shown.
File diff suppressed because it is too large
Load Diff
After Width: | Height: | Size: 2.3 MiB |
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
File diff suppressed because it is too large
Load Diff
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.
File diff suppressed because it is too large
Load Diff
Before Width: | Height: | Size: 797 KiB After Width: | Height: | Size: 1.7 MiB |
Binary file not shown.
Binary file not shown.
Binary file not shown.
Some files were not shown because too many files have changed in this diff Show More
Loading…
Reference in New Issue