diff --git a/CHANGELOG.md b/CHANGELOG.md index 0e0b77c00..dac4fdeed 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -17,6 +17,11 @@ - Updated Timeline component, cascade relationship state change. ([f4bd5672](https://github.com/pixelfed/pixelfed/commit/f4bd5672)) - Updated Activity component, only show context button for actionable activities. ([7886fd59](https://github.com/pixelfed/pixelfed/commit/7886fd59)) - Updated Autospam service, use silent classification for better user experience. ([f0d4c172](https://github.com/pixelfed/pixelfed/commit/f0d4c172)) +- Updated Profile component, improve error messages when block/mute limit reached. ([02237845](https://github.com/pixelfed/pixelfed/commit/02237845)) +- Updated Activity component, fix missing types. ([5167c68d](https://github.com/pixelfed/pixelfed/commit/5167c68d)) +- Updated Timeline component, apply block/mute filters client side for local and network timelines. ([be194b8a](https://github.com/pixelfed/pixelfed/commit/be194b8a)) +- Updated public timeline api, use cached sorted set and client side block/mute filtering. ([37abcf38](https://github.com/pixelfed/pixelfed/commit/37abcf38)) +- Updated public timeline api, add experimental cache. ([192553ff](https://github.com/pixelfed/pixelfed/commit/192553ff)) - ([](https://github.com/pixelfed/pixelfed/commit/)) ## [v0.11.1 (2021-09-07)](https://github.com/pixelfed/pixelfed/compare/v0.11.0...v0.11.1) diff --git a/app/Http/Controllers/AccountController.php b/app/Http/Controllers/AccountController.php index 36cdd258a..1103900fa 100644 --- a/app/Http/Controllers/AccountController.php +++ b/app/Http/Controllers/AccountController.php @@ -26,6 +26,8 @@ use League\Fractal; use League\Fractal\Serializer\ArraySerializer; use League\Fractal\Pagination\IlluminatePaginatorAdapter; use App\Transformer\Api\Mastodon\v1\AccountTransformer; +use App\Services\AccountService; +use App\Services\UserFilterService; class AccountController extends Controller { @@ -34,6 +36,8 @@ class AccountController extends Controller 'user.block', ]; + const FILTER_LIMIT = 'You cannot block or mute more than 100 accounts'; + public function __construct() { $this->middleware('auth'); @@ -140,6 +144,12 @@ class AccountController extends Controller ]); $user = Auth::user()->profile; + $count = UserFilterService::muteCount($user->id); + abort_if($count >= 100, 422, self::FILTER_LIMIT); + if($count == 0) { + $filterCount = UserFilter::whereUserId($user->id)->count(); + abort_if($filterCount >= 100, 422, self::FILTER_LIMIT); + } $type = $request->input('type'); $item = $request->input('item'); $action = $type . '.mute'; @@ -237,6 +247,12 @@ class AccountController extends Controller ]); $user = Auth::user()->profile; + $count = UserFilterService::blockCount($user->id); + abort_if($count >= 100, 422, self::FILTER_LIMIT); + if($count == 0) { + $filterCount = UserFilter::whereUserId($user->id)->count(); + abort_if($filterCount >= 100, 422, self::FILTER_LIMIT); + } $type = $request->input('type'); $item = $request->input('item'); $action = $type.'.block'; @@ -552,5 +568,21 @@ class AccountController extends Controller $prev = $page > 1 ? $page - 1 : 1; $links = '<'.$url.'?page='.$next.'&limit='.$limit.'>; rel="next", <'.$url.'?page='.$prev.'&limit='.$limit.'>; rel="prev"'; return response()->json($res, 200, ['Link' => $links]); + + } + + public function accountBlocksV2(Request $request) + { + return response()->json(UserFilterService::blocks($request->user()->profile_id), 200, [], JSON_UNESCAPED_SLASHES); + } + + public function accountMutesV2(Request $request) + { + return response()->json(UserFilterService::mutes($request->user()->profile_id), 200, [], JSON_UNESCAPED_SLASHES); + } + + public function accountFiltersV2(Request $request) + { + return response()->json(UserFilterService::filters($request->user()->profile_id), 200, [], JSON_UNESCAPED_SLASHES); } } diff --git a/app/Http/Controllers/Api/ApiV1Controller.php b/app/Http/Controllers/Api/ApiV1Controller.php index 5e737e9cd..31be3f471 100644 --- a/app/Http/Controllers/Api/ApiV1Controller.php +++ b/app/Http/Controllers/Api/ApiV1Controller.php @@ -58,7 +58,8 @@ use App\Services\{ RelationshipService, SearchApiV2Service, StatusService, - MediaBlocklistService + MediaBlocklistService, + UserFilterService }; use App\Util\Lexer\Autolink; @@ -563,9 +564,12 @@ class ApiV1Controller extends Controller 'id.*' => 'required|integer|min:1|max:' . PHP_INT_MAX ]); $pid = $request->user()->profile_id ?? $request->user()->profile->id; - $ids = collect($request->input('id')); - $res = $ids->map(function($id) use($pid) { - return RelationshipService::get($pid, $id); + $res = collect($request->input('id')) + ->filter(function($id) use($pid) { + return $id != $pid; + }) + ->map(function($id) use($pid) { + return RelationshipService::get($pid, $id); }); return response()->json($res); } @@ -1484,14 +1488,15 @@ class ApiV1Controller extends Controller $max = $request->input('max_id'); $limit = $request->input('limit') ?? 3; $user = $request->user(); + $filtered = $user ? UserFilterService::filters($user->profile_id) : []; - Cache::remember('api:v1:timelines:public:cache_check', 3600, function() { + Cache::remember('api:v1:timelines:public:cache_check', 10368000, function() { if(PublicTimelineService::count() == 0) { - PublicTimelineService::warmCache(true, 400); - } + PublicTimelineService::warmCache(true, 400); + } }); - if ($max) { + if ($max) { $feed = PublicTimelineService::getRankedMaxId($max, $limit); } else if ($min) { $feed = PublicTimelineService::getRankedMinId($min, $limit); @@ -1500,14 +1505,18 @@ class ApiV1Controller extends Controller } $res = collect($feed) - ->map(function($k) use($user) { - $status = StatusService::get($k); - if($user) { - $status['favourited'] = (bool) LikeService::liked($user->profile_id, $k); - } - return $status; - }) - ->toArray(); + ->map(function($k) use($user) { + $status = StatusService::get($k); + if($user) { + $status['favourited'] = (bool) LikeService::liked($user->profile_id, $k); + $status['relationship'] = RelationshipService::get($user->profile_id, $status['account']['id']); + } + return $status; + }) + ->filter(function($s) use($filtered) { + return in_array($s['account']['id'], $filtered) == false; + }) + ->toArray(); return response()->json($res); } diff --git a/app/Http/Controllers/LikeController.php b/app/Http/Controllers/LikeController.php index 725e2eb2a..8a56ae0e8 100644 --- a/app/Http/Controllers/LikeController.php +++ b/app/Http/Controllers/LikeController.php @@ -56,7 +56,7 @@ class LikeController extends Controller } Cache::forget('status:'.$status->id.':likedby:userid:'.$user->id); - StatusService::del($status->id); + StatusService::refresh($status->id); if ($request->ajax()) { $response = ['code' => 200, 'msg' => 'Like saved', 'count' => 0]; diff --git a/app/Http/Controllers/PublicApiController.php b/app/Http/Controllers/PublicApiController.php index 6a814388a..db3cc17bb 100644 --- a/app/Http/Controllers/PublicApiController.php +++ b/app/Http/Controllers/PublicApiController.php @@ -30,6 +30,7 @@ use App\Services\{ LikeService, PublicTimelineService, ProfileService, + RelationshipService, StatusService, SnowflakeService, UserFilterService @@ -38,7 +39,6 @@ use App\Jobs\StatusPipeline\NewStatusPipeline; use League\Fractal\Serializer\ArraySerializer; use League\Fractal\Pagination\IlluminatePaginatorAdapter; - class PublicApiController extends Controller { protected $fractal; @@ -287,69 +287,102 @@ class PublicApiController extends Controller $max = $request->input('max_id'); $limit = $request->input('limit') ?? 3; $user = $request->user(); - $filtered = $user ? UserFilterService::filters($user->profile_id) : []; - if($min || $max) { - $dir = $min ? '>' : '<'; - $id = $min ?? $max; - $timeline = Status::select( - 'id', - 'profile_id', - 'type', - 'scope', - 'local' - ) - ->where('id', $dir, $id) - ->whereIn('type', ['photo', 'photo:album', 'video', 'video:album', 'photo:video:album']) - ->whereNotIn('profile_id', $filtered) - ->whereLocal(true) - ->whereScope('public') - ->orderBy('id', 'desc') - ->limit($limit) - ->get() - ->map(function($s) use ($user) { - $status = StatusService::getFull($s->id, $user->profile_id); - $status['favourited'] = (bool) LikeService::liked($user->profile_id, $s->id); - return $status; - }); - $res = $timeline->toArray(); - } else { - $timeline = Status::select( - 'id', - 'uri', - 'caption', - 'rendered', - 'profile_id', - 'type', - 'in_reply_to_id', - 'reblog_of_id', - 'is_nsfw', - 'scope', - 'local', - 'reply_count', - 'comments_disabled', - 'created_at', - 'place_id', - 'likes_count', - 'reblogs_count', - 'updated_at' - ) - ->whereIn('type', ['photo', 'photo:album', 'video', 'video:album', 'photo:video:album']) - ->whereNotIn('profile_id', $filtered) - ->with('profile', 'hashtags', 'mentions') - ->whereLocal(true) - ->whereScope('public') - ->orderBy('id', 'desc') - ->limit($limit) - ->get() - ->map(function($s) use ($user) { - $status = StatusService::getFull($s->id, $user->profile_id); - $status['favourited'] = (bool) LikeService::liked($user->profile_id, $s->id); - return $status; - }); + if(config('exp.cached_public_timeline') == false) { + if($min || $max) { + $dir = $min ? '>' : '<'; + $id = $min ?? $max; + $timeline = Status::select( + 'id', + 'profile_id', + 'type', + 'scope', + 'local' + ) + ->where('id', $dir, $id) + ->whereIn('type', ['photo', 'photo:album', 'video', 'video:album', 'photo:video:album']) + ->whereLocal(true) + ->whereScope('public') + ->orderBy('id', 'desc') + ->limit($limit) + ->get() + ->map(function($s) use ($user) { + $status = StatusService::getFull($s->id, $user->profile_id); + $status['favourited'] = (bool) LikeService::liked($user->profile_id, $s->id); + return $status; + }) + ->filter(function($s) use($filtered) { + return in_array($s['account']['id'], $filtered) == false; + }); + $res = $timeline->toArray(); + } else { + $timeline = Status::select( + 'id', + 'uri', + 'caption', + 'rendered', + 'profile_id', + 'type', + 'in_reply_to_id', + 'reblog_of_id', + 'is_nsfw', + 'scope', + 'local', + 'reply_count', + 'comments_disabled', + 'created_at', + 'place_id', + 'likes_count', + 'reblogs_count', + 'updated_at' + ) + ->whereIn('type', ['photo', 'photo:album', 'video', 'video:album', 'photo:video:album']) + ->with('profile', 'hashtags', 'mentions') + ->whereLocal(true) + ->whereScope('public') + ->orderBy('id', 'desc') + ->limit($limit) + ->get() + ->map(function($s) use ($user) { + $status = StatusService::getFull($s->id, $user->profile_id); + $status['favourited'] = (bool) LikeService::liked($user->profile_id, $s->id); + return $status; + }) + ->filter(function($s) use($filtered) { + return in_array($s['account']['id'], $filtered) == false; + }); - $res = $timeline->toArray(); + $res = $timeline->toArray(); + } + } else { + Cache::remember('api:v1:timelines:public:cache_check', 10368000, function() { + if(PublicTimelineService::count() == 0) { + PublicTimelineService::warmCache(true, 400); + } + }); + + if ($max) { + $feed = PublicTimelineService::getRankedMaxId($max, $limit); + } else if ($min) { + $feed = PublicTimelineService::getRankedMinId($min, $limit); + } else { + $feed = PublicTimelineService::get(0, $limit); + } + + $res = collect($feed) + ->map(function($k) use($user) { + $status = StatusService::get($k); + if($user) { + $status['favourited'] = (bool) LikeService::liked($user->profile_id, $k); + $status['relationship'] = RelationshipService::get($user->profile_id, $status['account']['id']); + } + return $status; + }) + ->filter(function($s) use($filtered) { + return in_array($s['account']['id'], $filtered) == false; + }) + ->toArray(); } return response()->json($res); @@ -580,17 +613,20 @@ class PublicApiController extends Controller return response()->json([]); } + $pid = $request->user()->profile_id; + $this->validate($request, [ 'id' => 'required|array|min:1|max:20', 'id.*' => 'required|integer' ]); $ids = collect($request->input('id')); - $filtered = $ids->filter(function($v) { - return $v != Auth::user()->profile->id; + $res = $ids->filter(function($v) use($pid) { + return $v != $pid; + }) + ->map(function($id) use($pid) { + return RelationshipService::get($pid, $id); }); - $relations = Profile::whereNull('status')->findOrFail($filtered->all()); - $fractal = new Fractal\Resource\Collection($relations, new RelationshipTransformer()); - $res = $this->fractal->createData($fractal)->toArray(); + return response()->json($res); } @@ -741,5 +777,4 @@ class PublicApiController extends Controller return response()->json($res); } - } diff --git a/app/Jobs/LikePipeline/LikePipeline.php b/app/Jobs/LikePipeline/LikePipeline.php index 8ad725c9f..ce70790ca 100644 --- a/app/Jobs/LikePipeline/LikePipeline.php +++ b/app/Jobs/LikePipeline/LikePipeline.php @@ -59,7 +59,7 @@ class LikePipeline implements ShouldQueue return; } - StatusService::del($status->id); + StatusService::refresh($status->id); if($status->url && $actor->domain == null) { return $this->remoteLikeDeliver(); diff --git a/app/Jobs/LikePipeline/UnlikePipeline.php b/app/Jobs/LikePipeline/UnlikePipeline.php index 1a8afb58b..0e3ff4785 100644 --- a/app/Jobs/LikePipeline/UnlikePipeline.php +++ b/app/Jobs/LikePipeline/UnlikePipeline.php @@ -63,7 +63,7 @@ class UnlikePipeline implements ShouldQueue $status->likes_count = $count - 1; $status->save(); - StatusService::del($status->id); + StatusService::refresh($status->id); if($actor->id !== $status->profile_id && $status->url && $actor->domain == null) { $this->remoteLikeDeliver(); diff --git a/app/Services/AccountService.php b/app/Services/AccountService.php index d65f95943..d046b8c85 100644 --- a/app/Services/AccountService.php +++ b/app/Services/AccountService.php @@ -8,6 +8,8 @@ use App\Status; use App\Transformer\Api\AccountTransformer; use League\Fractal; use League\Fractal\Serializer\ArraySerializer; +use Illuminate\Support\Facades\DB; +use Illuminate\Support\Str; class AccountService { @@ -62,4 +64,22 @@ class AccountService Cache::put($key, 1, 900); return true; } + + public static function usernameToId($username) + { + $key = self::CACHE_KEY . 'u2id:' . hash('sha256', $username); + return Cache::remember($key, 900, function() use($username) { + $s = Str::of($username); + if($s->contains('@') && !$s->startsWith('@')) { + $username = "@{$username}"; + } + $profile = DB::table('profiles') + ->whereUsername($username) + ->first(); + if(!$profile) { + return null; + } + return (string) $profile->id; + }); + } } diff --git a/app/Services/StatusService.php b/app/Services/StatusService.php index 2b550a734..a4cfc4b75 100644 --- a/app/Services/StatusService.php +++ b/app/Services/StatusService.php @@ -62,4 +62,12 @@ class StatusService { Cache::forget(self::key($id, false)); return Cache::forget(self::key($id)); } + + public static function refresh($id) + { + Cache::forget(self::key($id, false)); + Cache::forget(self::key($id, true)); + self::get($id, false); + self::get($id, true); + } } diff --git a/app/Services/UserFilterService.php b/app/Services/UserFilterService.php index 4d155fa69..49118b579 100644 --- a/app/Services/UserFilterService.php +++ b/app/Services/UserFilterService.php @@ -98,4 +98,14 @@ class UserFilterService { } return $exists; } + + public static function blockCount(int $profile_id) + { + return Redis::zcard(self::USER_BLOCKS_KEY . $profile_id); + } + + public static function muteCount(int $profile_id) + { + return Redis::zcard(self::USER_MUTES_KEY . $profile_id); + } } diff --git a/config/exp.php b/config/exp.php index 76b4861f4..c90a2d33b 100644 --- a/config/exp.php +++ b/config/exp.php @@ -6,5 +6,6 @@ return [ 'rec' => false, 'loops' => false, 'top' => env('EXP_TOP', false), - 'polls' => env('EXP_POLLS', false) + 'polls' => env('EXP_POLLS', false), + 'cached_public_timeline' => env('EXP_CPT', false), ]; diff --git a/resources/assets/js/components/Activity.vue b/resources/assets/js/components/Activity.vue index ca775b4f3..3442b139e 100644 --- a/resources/assets/js/components/Activity.vue +++ b/resources/assets/js/components/Activity.vue @@ -76,6 +76,19 @@ {{n.account.local == false ? '@':''}}{{truncate(n.account.username)}} tagged you in a post.

+ +
+

+ {{n.account.local == false ? '@':''}}{{truncate(n.account.username)}} sent a dm. +

+
+ +
+

+ {{n.account.local == false ? '@':''}}{{truncate(n.account.username)}} sent a dm. +

+
+
{{timeAgo(n.created_at)}}
diff --git a/resources/assets/js/components/Profile.vue b/resources/assets/js/components/Profile.vue index e4c48fccb..187ac9ad5 100644 --- a/resources/assets/js/components/Profile.vue +++ b/resources/assets/js/components/Profile.vue @@ -1078,7 +1078,11 @@ this.$refs.visitorContextMenu.hide(); swal('Success', 'You have successfully muted ' + this.profile.acct, 'success'); }).catch(err => { - swal('Error', 'Something went wrong. Please try again later.', 'error'); + if(err.response.status == 422) { + swal('Error', err.response.data.error, 'error'); + } else { + swal('Error', 'Something went wrong. Please try again later.', 'error'); + } }); }, @@ -1113,7 +1117,11 @@ this.$refs.visitorContextMenu.hide(); swal('Success', 'You have successfully blocked ' + this.profile.acct, 'success'); }).catch(err => { - swal('Error', 'Something went wrong. Please try again later.', 'error'); + if(err.response.status == 422) { + swal('Error', err.response.data.error, 'error'); + } else { + swal('Error', 'Something went wrong. Please try again later.', 'error'); + } }); }, diff --git a/resources/assets/js/components/Timeline.vue b/resources/assets/js/components/Timeline.vue index 03bee79f8..6745a3d3b 100644 --- a/resources/assets/js/components/Timeline.vue +++ b/resources/assets/js/components/Timeline.vue @@ -508,7 +508,8 @@ recentFeedMin: null, recentFeedMax: null, reactionBar: true, - emptyFeed: false + emptyFeed: false, + filters: [] } }, @@ -567,7 +568,16 @@ break; } } - this.fetchTimelineApi(); + + if(this.scope != 'home') { + axios.get('/api/pixelfed/v2/filters') + .then(res => { + this.filters = res.data; + this.fetchTimelineApi(); + }); + } else { + this.fetchTimelineApi(); + } }); }, @@ -629,6 +639,12 @@ return; } + if(this.filters.length) { + data = data.filter(d => { + return this.filters.includes(d.account.id) == false; + }); + } + this.feed.push(...data); let ids = data.map(status => status.id); this.ids = ids; diff --git a/resources/assets/js/components/partials/StatusCard.vue b/resources/assets/js/components/partials/StatusCard.vue index d00d03dbe..4d2422e4d 100644 --- a/resources/assets/js/components/partials/StatusCard.vue +++ b/resources/assets/js/components/partials/StatusCard.vue @@ -401,7 +401,7 @@ return false; } - if(status.account.id === this.profile.id) { + if(status.account.id == this.profile.id) { return false; } diff --git a/resources/views/layouts/partial/nav.blade.php b/resources/views/layouts/partial/nav.blade.php index 8b6051690..032474f9b 100644 --- a/resources/views/layouts/partial/nav.blade.php +++ b/resources/views/layouts/partial/nav.blade.php @@ -65,7 +65,7 @@