Browse Source

Merge pull request #2983 from pixelfed/staging

Staging
daniel 3 years ago
parent
commit
14ee32adf0

+ 5 - 0
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)

+ 32 - 0
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);
     }
 }

+ 25 - 16
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);
 	}
 

+ 1 - 1
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];

+ 102 - 67
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();
+        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();
+	        }
         } 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;
-                      });
-
-            $res = $timeline->toArray();
+	  		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);
     }
-
 }

+ 1 - 1
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();

+ 1 - 1
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();

+ 20 - 0
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;
+		});
+	}
 }

+ 8 - 0
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);
+	}
 }

+ 10 - 0
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);
+	}
 }

+ 2 - 1
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),
 ];

+ 13 - 0
resources/assets/js/components/Activity.vue

@@ -76,6 +76,19 @@
 									<a :href="getProfileUrl(n.account)" class="font-weight-bold text-dark word-break" :title="n.account.username">{{n.account.local == false ? '@':''}}{{truncate(n.account.username)}}</a> tagged you in a <a class="font-weight-bold" v-bind:href="n.tagged.post_url">post</a>.
 								</p>
 							</div>
+
+							<div v-else-if="n.type == 'direct'">
+								<p class="my-0">
+									<a :href="getProfileUrl(n.account)" class="font-weight-bold text-dark word-break" :title="n.account.username">{{n.account.local == false ? '@':''}}{{truncate(n.account.username)}}</a> sent a <a class="font-weight-bold" v-bind:href="'/account/direct/t/'+n.account.id">dm</a>.
+								</p>
+							</div>
+
+							<div v-else-if="n.type == 'direct'">
+								<p class="my-0">
+									<a :href="getProfileUrl(n.account)" class="font-weight-bold text-dark word-break" :title="n.account.username">{{n.account.local == false ? '@':''}}{{truncate(n.account.username)}}</a> sent a <a class="font-weight-bold" v-bind:href="'/account/direct/t/'+n.account.id">dm</a>.
+								</p>
+							</div>
+
 							<div class="align-items-center">
 								<span class="small text-muted" data-toggle="tooltip" data-placement="bottom" :title="n.created_at">{{timeAgo(n.created_at)}}</span>
 							</div>

+ 10 - 2
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');
+					}
 				});
 			},
 

+ 18 - 2
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;

+ 1 - 1
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;
 				}
 

+ 1 - 7
resources/views/layouts/partial/nav.blade.php

@@ -65,7 +65,7 @@
 
 							<div class="dropdown-menu dropdown-menu-right px-0 shadow" aria-labelledby="navbarDropdown" style="min-width: 220px;">
 								@if(config('federation.network_timeline'))
-								<a class="dropdown-item lead" href="{{route('timeline.public')}}">
+								<a class="dropdown-item lead" href="/">
 									<span style="width: 50px;margin-right:14px;">
 										<span class="fal fa-home text-lighter fa-lg"></span>
 									</span>
@@ -104,12 +104,6 @@
 									</span>
 									{{__('navmenu.discover')}}
 								</a>
-								{{-- <a class="dropdown-item lead" href="/groups">
-									<span style="width: 50px;margin-right:14px;">
-										<span class="fal fa-user-friends text-lighter"></span>
-									</span>
-									Groups
-								</a> --}}
 								@if(config_cache('instance.stories.enabled'))
 								<a class="dropdown-item lead" href="/i/stories/new">
 									<span style="width: 50px;margin-right:14px;">

+ 3 - 0
routes/web.php

@@ -202,6 +202,9 @@ Route::domain(config('pixelfed.domain.app'))->middleware(['validemail', 'twofact
 				Route::post('status/{id}/archive', 'ApiController@archive');
 				Route::post('status/{id}/unarchive', 'ApiController@unarchive');
 				Route::get('statuses/archives', 'ApiController@archivedPosts');
+				Route::get('mutes', 'AccountController@accountMutesV2');
+				Route::get('blocks', 'AccountController@accountBlocksV2');
+				Route::get('filters', 'AccountController@accountFiltersV2');
 			});
 		});