Refactor following & relationship logic. Replace FollowerObserver with FollowerService and added RelationshipService to cache results. Removed NotificationTransformer includes and replaced with cached services to improve performance and reduce database queries.

This commit is contained in:
Daniel Supernault 2021-10-07 03:27:13 -06:00
parent 0a8eb81bf0
commit 80d9b9399a
No known key found for this signature in database
GPG key ID: 0DEF1C662C9033F7
8 changed files with 151 additions and 139 deletions

View file

@ -55,6 +55,7 @@ use App\Services\{
MediaPathService, MediaPathService,
PublicTimelineService, PublicTimelineService,
ProfileService, ProfileService,
RelationshipService,
SearchApiV2Service, SearchApiV2Service,
StatusService, StatusService,
MediaBlocklistService MediaBlocklistService
@ -551,7 +552,7 @@ class ApiV1Controller extends Controller
* *
* @param array|integer $id * @param array|integer $id
* *
* @return \App\Transformer\Api\RelationshipTransformer * @return \App\Services\RelationshipService
*/ */
public function accountRelationshipsById(Request $request) public function accountRelationshipsById(Request $request)
{ {
@ -563,12 +564,9 @@ class ApiV1Controller extends Controller
]); ]);
$pid = $request->user()->profile_id ?? $request->user()->profile->id; $pid = $request->user()->profile_id ?? $request->user()->profile->id;
$ids = collect($request->input('id')); $ids = collect($request->input('id'));
$filtered = $ids->filter(function($v) use($pid) { $res = $ids->map(function($id) use($pid) {
return $v != $pid; return RelationshipService::get($pid, $id);
}); });
$relations = Profile::whereNull('status')->findOrFail($filtered->values());
$fractal = new Fractal\Resource\Collection($relations, new RelationshipTransformer());
$res = $this->fractal->createData($fractal)->toArray();
return response()->json($res); return response()->json($res);
} }

View file

@ -12,6 +12,7 @@ use Auth, Cache;
use Illuminate\Http\Request; use Illuminate\Http\Request;
use App\Jobs\FollowPipeline\FollowPipeline; use App\Jobs\FollowPipeline\FollowPipeline;
use App\Util\ActivityPub\Helpers; use App\Util\ActivityPub\Helpers;
use App\Services\FollowerService;
class FollowerController extends Controller class FollowerController extends Controller
{ {
@ -71,6 +72,8 @@ class FollowerController extends Controller
if($remote == true && config('federation.activitypub.remoteFollow') == true) { if($remote == true && config('federation.activitypub.remoteFollow') == true) {
$this->sendFollow($user, $target); $this->sendFollow($user, $target);
} }
FollowerService::add($user->id, $target->id);
} elseif ($private == false && $isFollowing == 0) { } elseif ($private == false && $isFollowing == 0) {
if($user->following()->count() >= Follower::MAX_FOLLOWING) { if($user->following()->count() >= Follower::MAX_FOLLOWING) {
abort(400, 'You cannot follow more than ' . Follower::MAX_FOLLOWING . ' accounts'); abort(400, 'You cannot follow more than ' . Follower::MAX_FOLLOWING . ' accounts');
@ -87,6 +90,7 @@ class FollowerController extends Controller
if($remote == true && config('federation.activitypub.remoteFollow') == true) { if($remote == true && config('federation.activitypub.remoteFollow') == true) {
$this->sendFollow($user, $target); $this->sendFollow($user, $target);
} }
FollowerService::add($user->id, $target->id);
FollowPipeline::dispatch($follower); FollowPipeline::dispatch($follower);
} else { } else {
if($force == true) { if($force == true) {
@ -101,6 +105,7 @@ class FollowerController extends Controller
Follower::whereProfileId($user->id) Follower::whereProfileId($user->id)
->whereFollowingId($target->id) ->whereFollowingId($target->id)
->delete(); ->delete();
FollowerService::remove($user->id, $target->id);
} }
} }

View file

@ -1,64 +0,0 @@
<?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);
}
}

View file

@ -4,7 +4,6 @@ namespace App\Providers;
use App\Observers\{ use App\Observers\{
AvatarObserver, AvatarObserver,
FollowerObserver,
LikeObserver, LikeObserver,
NotificationObserver, NotificationObserver,
ModLogObserver, ModLogObserver,
@ -15,7 +14,6 @@ use App\Observers\{
}; };
use App\{ use App\{
Avatar, Avatar,
Follower,
Like, Like,
Notification, Notification,
ModLog, ModLog,
@ -50,7 +48,6 @@ class AppServiceProvider extends ServiceProvider
StatusHashtag::observe(StatusHashtagObserver::class); StatusHashtag::observe(StatusHashtagObserver::class);
User::observe(UserObserver::class); User::observe(UserObserver::class);
UserFilter::observe(UserFilterObserver::class); UserFilter::observe(UserFilterObserver::class);
Follower::observe(FollowerObserver::class);
Horizon::auth(function ($request) { Horizon::auth(function ($request) {
return Auth::check() && $request->user()->is_admin; return Auth::check() && $request->user()->is_admin;
}); });

View file

@ -17,12 +17,14 @@ class FollowerService
public static function add($actor, $target) public static function add($actor, $target)
{ {
RelationshipService::refresh($actor, $target);
Redis::zadd(self::FOLLOWING_KEY . $actor, $target, $target); Redis::zadd(self::FOLLOWING_KEY . $actor, $target, $target);
Redis::zadd(self::FOLLOWERS_KEY . $target, $actor, $actor); Redis::zadd(self::FOLLOWERS_KEY . $target, $actor, $actor);
} }
public static function remove($actor, $target) public static function remove($actor, $target)
{ {
RelationshipService::refresh($actor, $target);
Redis::zrem(self::FOLLOWING_KEY . $actor, $target); Redis::zrem(self::FOLLOWING_KEY . $actor, $target);
Redis::zrem(self::FOLLOWERS_KEY . $target, $actor); Redis::zrem(self::FOLLOWERS_KEY . $target, $actor);
Cache::forget('pf:services:follow:audience:' . $actor); Cache::forget('pf:services:follow:audience:' . $actor);

View file

@ -0,0 +1,86 @@
<?php
namespace App\Services;
use Illuminate\Support\Facades\Cache;
use App\Follower;
use App\FollowRequest;
use App\Profile;
use App\UserFilter;
class RelationshipService
{
const CACHE_KEY = 'pf:services:urel:';
public static function get($aid, $tid)
{
$actor = AccountService::get($aid);
$target = AccountService::get($tid);
if(!$actor || !$target) {
return self::defaultRelation($tid);
}
if($actor['id'] === $target['id']) {
return self::defaultRelation($tid);
}
return Cache::remember(self::key("a_{$aid}:t_{$tid}"), 1209600, function() use($aid, $tid) {
return [
'id' => (string) $tid,
'following' => Follower::whereProfileId($aid)->whereFollowingId($tid)->exists(),
'followed_by' => Follower::whereProfileId($tid)->whereFollowingId($aid)->exists(),
'blocking' => UserFilter::whereUserId($aid)
->whereFilterableType('App\Profile')
->whereFilterableId($tid)
->whereFilterType('block')
->exists(),
'muting' => UserFilter::whereUserId($aid)
->whereFilterableType('App\Profile')
->whereFilterableId($tid)
->whereFilterType('mute')
->exists(),
'muting_notifications' => null,
'requested' => FollowRequest::whereFollowerId($aid)
->whereFollowingId($tid)
->exists(),
'domain_blocking' => null,
'showing_reblogs' => null,
'endorsed' => false
];
});
}
public static function delete($aid, $tid)
{
return Cache::forget(self::key("a_{$aid}:t_{$tid}"));
}
public static function refresh($aid, $tid)
{
self::delete($tid, $aid);
self::delete($aid, $tid);
self::get($tid, $aid);
return self::get($aid, $tid);
}
public static function defaultRelation($tid)
{
return [
'id' => (string) $tid,
'following' => false,
'followed_by' => false,
'blocking' => false,
'muting' => false,
'muting_notifications' => null,
'requested' => false,
'domain_blocking' => null,
'showing_reblogs' => null,
'endorsed' => false
];
}
protected static function key($suffix)
{
return self::CACHE_KEY . $suffix;
}
}

View file

@ -2,50 +2,65 @@
namespace App\Transformer\Api; namespace App\Transformer\Api;
use App\{ use App\Notification;
Notification, use App\Services\AccountService;
Status
};
use App\Services\HashidService; use App\Services\HashidService;
use App\Services\StatusService;
use League\Fractal; use League\Fractal;
class NotificationTransformer extends Fractal\TransformerAbstract class NotificationTransformer extends Fractal\TransformerAbstract
{ {
protected $defaultIncludes = [ protected $defaultIncludes = [
'account', // 'relationship',
'status',
'relationship',
'modlog',
'tagged'
]; ];
public function transform(Notification $notification) public function transform(Notification $notification)
{ {
return [ $res = [
'id' => (string) $notification->id, 'id' => (string) $notification->id,
'type' => $this->replaceTypeVerb($notification->action), 'type' => $this->replaceTypeVerb($notification->action),
'created_at' => (string) $notification->created_at->format('c'), 'created_at' => (string) $notification->created_at->format('c'),
]; ];
}
public function includeAccount(Notification $notification) $n = $notification;
{ if($n->item_id && $n->item_type == 'App\Status' && in_array($n->action, ['group:comment'])) {
return $this->item($notification->actor, new AccountTransformer()); $status = $n->status;
} $res['group_id'] = $status->group_id;
public function includeStatus(Notification $notification) if($n->action == 'group:comment') {
{ $res['group_post_url'] = GroupPost::whereStatusId($status->id)->first()->url();
$item = $notification;
if($item->item_id && $item->item_type == 'App\Status') {
$status = Status::with('media')->find($item->item_id);
if($status) {
return $this->item($status, new StatusTransformer());
} else {
return null;
} }
} else {
return null;
} }
if(in_array($n->action, ['group.join.approved', 'group.join.rejected', 'group.like'])) {
$res['group'] = GroupService::get($n->item_id);
}
if($n->actor_id) {
$res['account'] = AccountService::get($n->actor_id);
}
if($n->item_id && $n->item_type == 'App\Status') {
$res['status'] = StatusService::get($n->item_id, false);
}
if($n->item_id && $n->item_type == 'App\ModLog') {
$ml = $n->item;
$res['modlog'] = [
'id' => $ml->object_uid,
'url' => url('/i/admin/users/modlogs/' . $ml->object_uid)
];
}
if($n->item_id && $n->item_type == 'App\MediaTag') {
$ml = $n->item;
$res['tagged'] = [
'username' => $ml->tagged_username,
'post_url' => '/p/'.HashidService::encode($ml->status_id)
];
}
return $res;
} }
public function replaceTypeVerb($verb) public function replaceTypeVerb($verb)
@ -57,13 +72,21 @@ class NotificationTransformer extends Fractal\TransformerAbstract
'reblog' => 'share', 'reblog' => 'share',
'share' => 'share', 'share' => 'share',
'like' => 'favourite', 'like' => 'favourite',
'group:like' => 'favourite',
'comment' => 'comment', 'comment' => 'comment',
'admin.user.modlog.comment' => 'modlog', 'admin.user.modlog.comment' => 'modlog',
'tagged' => 'tagged', 'tagged' => 'tagged',
'group:comment' => 'group:comment', 'group:comment' => 'group:comment',
'story:react' => 'story:react', 'story:react' => 'story:react',
'story:comment' => 'story:comment' 'story:comment' => 'story:comment',
'group:join:approved' => 'group:join:approved',
'group:join:rejected' => 'group:join:rejected'
]; ];
if(!isset($verbs[$verb])) {
return $verb;
}
return $verbs[$verb]; return $verbs[$verb];
} }
@ -71,42 +94,4 @@ class NotificationTransformer extends Fractal\TransformerAbstract
{ {
return $this->item($notification->actor, new RelationshipTransformer()); return $this->item($notification->actor, new RelationshipTransformer());
} }
public function includeModlog(Notification $notification)
{
$n = $notification;
if($n->item_id && $n->item_type == 'App\ModLog') {
$ml = $n->item;
if(!empty($ml)) {
$res = $this->item($ml, function($ml) {
return [
'id' => $ml->object_uid,
'url' => url('/i/admin/users/modlogs/' . $ml->object_uid)
];
});
return $res;
} else {
return null;
}
} else {
return null;
}
}
public function includeTagged(Notification $notification)
{
$n = $notification;
if($n->item_id && $n->item_type == 'App\MediaTag') {
$ml = $n->item;
$res = $this->item($ml, function($ml) {
return [
'username' => $ml->tagged_username,
'post_url' => '/p/'.HashidService::encode($ml->status_id)
];
});
return $res;
} else {
return null;
}
}
} }

View file

@ -455,6 +455,7 @@ class Inbox
Cache::forget('profile:follower_count:'.$actor->id); Cache::forget('profile:follower_count:'.$actor->id);
Cache::forget('profile:following_count:'.$target->id); Cache::forget('profile:following_count:'.$target->id);
Cache::forget('profile:following_count:'.$actor->id); Cache::forget('profile:following_count:'.$actor->id);
FollowerService::add($actor->id, $target->id);
} else { } else {
$follower = new Follower; $follower = new Follower;
@ -464,6 +465,7 @@ class Inbox
$follower->save(); $follower->save();
FollowPipeline::dispatch($follower); FollowPipeline::dispatch($follower);
FollowerService::add($actor->id, $target->id);
// send Accept to remote profile // send Accept to remote profile
$accept = [ $accept = [
@ -722,6 +724,7 @@ class Inbox
->whereItemId($following->id) ->whereItemId($following->id)
->whereItemType('App\Profile') ->whereItemType('App\Profile')
->forceDelete(); ->forceDelete();
FollowerService::remove($profile->id, $following->id);
break; break;
case 'Like': case 'Like':