Merge pull request #4363 from pixelfed/staging

Staging
This commit is contained in:
daniel 2023-05-09 04:09:18 -06:00 committed by GitHub
commit 03e7e24b6b
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
21 changed files with 282 additions and 19 deletions

View file

@ -2,6 +2,12 @@
## [Unreleased](https://github.com/pixelfed/pixelfed/compare/v0.11.6...dev) ## [Unreleased](https://github.com/pixelfed/pixelfed/compare/v0.11.6...dev)
### API Changes
- Added [/api/v1/followed_tags](https://docs.joinmastodon.org/methods/followed_tags/) api endpoint ([175a8486](https://github.com/pixelfed/pixelfed/commit/175a8486))
- Added [/api/v1/tags/:id/follow](https://docs.joinmastodon.org/methods/tags/#follow) and [/api/v1/tags/:id/unfollow](https://docs.joinmastodon.org/methods/tags/#unfollow) api endpoints ([4d997bb9](https://github.com/pixelfed/pixelfed/commit/4d997bb9))
- Added [/api/v1/tags/:id](https://docs.joinmastodon.org/methods/tags/) api endpoint ([521b3b4c](https://github.com/pixelfed/pixelfed/commit/521b3b4c))
- Added `only_media` support to /api/v1/timelines/tag/:id api endpoint ([b5fe956a](https://github.com/pixelfed/pixelfed/commit/b5fe956a))
### Added ### Added
- Added store remote media on S3 config setting, disabled by default ([51768083](https://github.com/pixelfed/pixelfed/commit/51768083)) - Added store remote media on S3 config setting, disabled by default ([51768083](https://github.com/pixelfed/pixelfed/commit/51768083))
@ -14,6 +20,14 @@
- Update instance config, enable config cache by default ([970f77b0](https://github.com/pixelfed/pixelfed/commit/970f77b0)) - Update instance config, enable config cache by default ([970f77b0](https://github.com/pixelfed/pixelfed/commit/970f77b0))
- Update Admin Dashboard, allow admins to designate an admin account for the landing page and instance api endpoint ([6ea2bdc7](https://github.com/pixelfed/pixelfed/commit/6ea2bdc7)) - Update Admin Dashboard, allow admins to designate an admin account for the landing page and instance api endpoint ([6ea2bdc7](https://github.com/pixelfed/pixelfed/commit/6ea2bdc7))
- Update config, enable oauth by default ([6a2e9e8f](https://github.com/pixelfed/pixelfed/commit/6a2e9e8f)) - Update config, enable oauth by default ([6a2e9e8f](https://github.com/pixelfed/pixelfed/commit/6a2e9e8f))
- Update StatusService, fix missing account condition ([f48daab3](https://github.com/pixelfed/pixelfed/commit/f48daab3))
- Update ProfileService, add softFail param ([6bc20a37](https://github.com/pixelfed/pixelfed/commit/6bc20a37))
- Update MediaTagService, fix ProfileService to soft fail on missing or deleted accounts ([df444851](https://github.com/pixelfed/pixelfed/commit/df444851))
- Update LikeService, improve likedBy logic to soft fail on missing or deleted accounts ([91ba1398](https://github.com/pixelfed/pixelfed/commit/91ba1398))
- Update StatusTransformers, fix ProfileService to soft fail on missing or deleted accounts ([43d3aa2b](https://github.com/pixelfed/pixelfed/commit/43d3aa2b))
- Update ApiV1Controller, fix hashtag timeline ([fc1a385c](https://github.com/pixelfed/pixelfed/commit/fc1a385c))
- Update settings view, add fallback avatar ([1a83c585](https://github.com/pixelfed/pixelfed/commit/1a83c585))
- Update HashtagFollow model, add MAX_LIMIT of 250 tags per account ([ed352141](https://github.com/pixelfed/pixelfed/commit/ed352141))
- ([](https://github.com/pixelfed/pixelfed/commit/)) - ([](https://github.com/pixelfed/pixelfed/commit/))
## [v0.11.6 (2023-05-03)](https://github.com/pixelfed/pixelfed/compare/v0.11.5...v0.11.6) ## [v0.11.6 (2023-05-03)](https://github.com/pixelfed/pixelfed/compare/v0.11.5...v0.11.6)

View file

@ -12,6 +12,8 @@ class HashtagFollow extends Model
'hashtag_id' 'hashtag_id'
]; ];
const MAX_LIMIT = 250;
public function hashtag() public function hashtag()
{ {
return $this->belongsTo(Hashtag::class); return $this->belongsTo(Hashtag::class);

View file

@ -19,6 +19,7 @@ use App\{
Follower, Follower,
FollowRequest, FollowRequest,
Hashtag, Hashtag,
HashtagFollow,
Instance, Instance,
Like, Like,
Media, Media,
@ -69,6 +70,7 @@ use App\Services\{
BouncerService, BouncerService,
CollectionService, CollectionService,
FollowerService, FollowerService,
HashtagService,
InstanceService, InstanceService,
LikeService, LikeService,
NetworkTimelineService, NetworkTimelineService,
@ -99,6 +101,7 @@ use App\Jobs\FollowPipeline\FollowRejectPipeline;
use Illuminate\Support\Facades\RateLimiter; use Illuminate\Support\Facades\RateLimiter;
use Purify; use Purify;
use Carbon\Carbon; use Carbon\Carbon;
use App\Http\Resources\MastoApi\FollowedTagResource;
class ApiV1Controller extends Controller class ApiV1Controller extends Controller
{ {
@ -3245,7 +3248,9 @@ class ApiV1Controller extends Controller
'page' => 'nullable|integer|max:40', 'page' => 'nullable|integer|max:40',
'min_id' => 'nullable|integer|min:0|max:' . PHP_INT_MAX, 'min_id' => 'nullable|integer|min:0|max:' . PHP_INT_MAX,
'max_id' => 'nullable|integer|min:0|max:' . PHP_INT_MAX, 'max_id' => 'nullable|integer|min:0|max:' . PHP_INT_MAX,
'limit' => 'nullable|integer|max:100' 'limit' => 'nullable|integer|max:100',
'only_media' => 'sometimes|boolean',
'_pe' => 'sometimes'
]); ]);
if(config('database.default') === 'pgsql') { if(config('database.default') === 'pgsql') {
@ -3269,6 +3274,18 @@ class ApiV1Controller extends Controller
$min = $request->input('min_id'); $min = $request->input('min_id');
$max = $request->input('max_id'); $max = $request->input('max_id');
$limit = $request->input('limit', 20); $limit = $request->input('limit', 20);
$onlyMedia = $request->input('only_media', true);
$pe = $request->has(self::PF_API_ENTITY_KEY);
if($min || $max) {
$minMax = SnowflakeService::byDate(now()->subMonths(6));
if($min && intval($min) < $minMax) {
return [];
}
if($max && intval($max) < $minMax) {
return [];
}
}
if(!$min && !$max) { if(!$min && !$max) {
$id = 1; $id = 1;
@ -3281,15 +3298,19 @@ class ApiV1Controller extends Controller
$res = StatusHashtag::whereHashtagId($tag->id) $res = StatusHashtag::whereHashtagId($tag->id)
->whereStatusVisibility('public') ->whereStatusVisibility('public')
->where('status_id', $dir, $id) ->where('status_id', $dir, $id)
->latest() ->orderBy('status_id', 'desc')
->limit($limit) ->limit($limit)
->pluck('status_id') ->pluck('status_id')
->map(function ($i) { ->map(function ($i) use($pe) {
if($i) { return $pe ? StatusService::get($i) : StatusService::getMastodon($i);
return StatusService::getMastodon($i);
}
}) })
->filter(function($i) { ->filter(function($i) use($onlyMedia) {
if(!$i) {
return false;
}
if($onlyMedia && !isset($i['media_attachments']) || !count($i['media_attachments'])) {
return false;
}
return $i && isset($i['account']); return $i && isset($i['account']);
}) })
->values() ->values()
@ -3794,4 +3815,181 @@ class ApiV1Controller extends Controller
return $this->json([]); return $this->json([]);
} }
/**
* GET /api/v1/followed_tags
*
*
* @return array
*/
public function getFollowedTags(Request $request)
{
abort_if(!$request->user(), 403);
if(config('pixelfed.bouncer.cloud_ips.ban_api')) {
abort_if(BouncerService::checkIp($request->ip()), 404);
}
$account = AccountService::get($request->user()->profile_id);
$this->validate($request, [
'cursor' => 'sometimes',
'limit' => 'sometimes|integer|min:1|max:200'
]);
$limit = $request->input('limit', 100);
$res = HashtagFollow::whereProfileId($account['id'])
->orderByDesc('id')
->cursorPaginate($limit)->withQueryString();
$pagination = false;
$prevPage = $res->nextPageUrl();
$nextPage = $res->previousPageUrl();
if($nextPage && $prevPage) {
$pagination = '<' . $nextPage . '>; rel="next", <' . $prevPage . '>; rel="prev"';
} else if($nextPage && !$prevPage) {
$pagination = '<' . $nextPage . '>; rel="next"';
} else if(!$nextPage && $prevPage) {
$pagination = '<' . $prevPage . '>; rel="prev"';
}
if($pagination) {
return response()->json(FollowedTagResource::collection($res)->collection)
->header('Link', $pagination);
}
return response()->json(FollowedTagResource::collection($res)->collection);
}
/**
* POST /api/v1/tags/:id/follow
*
*
* @return object
*/
public function followHashtag(Request $request, $id)
{
abort_if(!$request->user(), 403);
if(config('pixelfed.bouncer.cloud_ips.ban_api')) {
abort_if(BouncerService::checkIp($request->ip()), 404);
}
$pid = $request->user()->profile_id;
$account = AccountService::get($pid);
$operator = config('database.default') == 'pgsql' ? 'ilike' : 'like';
$tag = Hashtag::where('name', $operator, $id)
->orWhere('slug', $operator, $id)
->first();
abort_if(!$tag, 422, 'Unknown hashtag');
abort_if(
HashtagFollow::whereProfileId($pid)->count() >= HashtagFollow::MAX_LIMIT,
422,
'You cannot follow more than ' . HashtagFollow::MAX_LIMIT . ' hashtags.'
);
$follows = HashtagFollow::updateOrCreate(
[
'profile_id' => $account['id'],
'hashtag_id' => $tag->id
],
[
'user_id' => $request->user()->id
]
);
HashtagService::follow($pid, $tag->id);
return response()->json(FollowedTagResource::make($follows)->toArray($request));
}
/**
* POST /api/v1/tags/:id/unfollow
*
*
* @return object
*/
public function unfollowHashtag(Request $request, $id)
{
abort_if(!$request->user(), 403);
if(config('pixelfed.bouncer.cloud_ips.ban_api')) {
abort_if(BouncerService::checkIp($request->ip()), 404);
}
$pid = $request->user()->profile_id;
$account = AccountService::get($pid);
$operator = config('database.default') == 'pgsql' ? 'ilike' : 'like';
$tag = Hashtag::where('name', $operator, $id)
->orWhere('slug', $operator, $id)
->first();
abort_if(!$tag, 422, 'Unknown hashtag');
$follows = HashtagFollow::whereProfileId($pid)
->whereHashtagId($tag->id)
->first();
if(!$follows) {
return [
'name' => $tag->name,
'url' => config('app.url') . '/i/web/hashtag/' . $tag->slug,
'history' => [],
'following' => false
];
}
if($follows) {
HashtagService::unfollow($pid, $tag->id);
$follows->delete();
}
$res = FollowedTagResource::make($follows)->toArray($request);
$res['following'] = false;
return response()->json($res);
}
/**
* GET /api/v1/tags/:id
*
*
* @return object
*/
public function getHashtag(Request $request, $id)
{
abort_if(!$request->user(), 403);
if(config('pixelfed.bouncer.cloud_ips.ban_api')) {
abort_if(BouncerService::checkIp($request->ip()), 404);
}
$pid = $request->user()->profile_id;
$account = AccountService::get($pid);
$operator = config('database.default') == 'pgsql' ? 'ilike' : 'like';
$tag = Hashtag::where('name', $operator, $id)
->orWhere('slug', $operator, $id)
->first();
if(!$tag) {
return [
'name' => $id,
'url' => config('app.url') . '/i/web/hashtag/' . $id,
'history' => [],
'following' => false
];
}
$res = [
'name' => $tag->name,
'url' => config('app.url') . '/i/web/hashtag/' . $tag->slug,
'history' => [],
'following' => HashtagService::isFollowing($pid, $tag->id)
];
if($request->has(self::PF_API_ENTITY_KEY)) {
$res['count'] = HashtagService::count($tag->id);
}
return $this->json($res);
}
} }

View file

@ -0,0 +1,33 @@
<?php
namespace App\Http\Resources\MastoApi;
use Illuminate\Http\Resources\Json\JsonResource;
use Illuminate\Http\JsonResponse;
use Cache;
use App\Services\HashtagService;
class FollowedTagResource extends JsonResource
{
/**
* Transform the resource into an array.
*
* @param \Illuminate\Http\Request $request
* @return array|\Illuminate\Contracts\Support\Arrayable|\JsonSerializable
*/
public function toArray($request)
{
$tag = HashtagService::get($this->hashtag_id);
if(!$tag || !isset($tag['name'])) {
return [];
}
return [
'name' => $tag['name'],
'url' => config('app.url') . '/i/web/hashtag/' . $tag['slug'],
'history' => [],
'following' => true,
];
}
}

View file

@ -28,8 +28,8 @@ class HashtagService {
public static function count($id) public static function count($id)
{ {
return Cache::remember('services:hashtag:count:by_id:' . $id, 3600, function() use($id) { return Cache::remember('services:hashtag:public-count:by_id:' . $id, 86400, function() use($id) {
return StatusHashtag::whereHashtagId($id)->count(); return StatusHashtag::whereHashtagId($id)->whereStatusVisibility('public')->count();
}); });
} }
@ -64,4 +64,9 @@ class HashtagService {
{ {
return Redis::zrem(self::FOLLOW_KEY . $pid, $hid); return Redis::zrem(self::FOLLOW_KEY . $pid, $hid);
} }
public static function following($pid, $start = 0, $limit = 10)
{
return Redis::zrevrange(self::FOLLOW_KEY . $pid, $start, $limit);
}
} }

View file

@ -85,7 +85,10 @@ class LikeService {
return $empty; return $empty;
} }
$id = $like->profile_id; $id = $like->profile_id;
$profile = ProfileService::get($id); $profile = ProfileService::get($id, true);
if(!$profile) {
return [];
}
$profileUrl = "/i/web/profile/{$profile['id']}"; $profileUrl = "/i/web/profile/{$profile['id']}";
$res = [ $res = [
'id' => (string) $profile['id'], 'id' => (string) $profile['id'],

View file

@ -57,7 +57,7 @@ class MediaTagService
protected function idToUsername($id) protected function idToUsername($id)
{ {
$profile = ProfileService::get($id); $profile = ProfileService::get($id, true);
if(!$profile) { if(!$profile) {
return 'unavailable'; return 'unavailable';

View file

@ -4,9 +4,9 @@ namespace App\Services;
class ProfileService class ProfileService
{ {
public static function get($id) public static function get($id, $softFail = false)
{ {
return AccountService::get($id); return AccountService::get($id, $softFail);
} }
public static function del($id) public static function del($id)

View file

@ -47,6 +47,10 @@ class StatusService
return null; return null;
} }
if(!isset($status['account'])) {
return null;
}
$status['replies_count'] = $status['reply_count']; $status['replies_count'] = $status['reply_count'];
if(config('exp.emc') == false) { if(config('exp.emc') == false) {

View file

@ -42,7 +42,7 @@ class StatusTransformer extends Fractal\TransformerAbstract
'card' => null, 'card' => null,
'poll' => null, 'poll' => null,
'media_attachments' => MediaService::get($status->id), 'media_attachments' => MediaService::get($status->id),
'account' => ProfileService::get($status->profile_id), 'account' => ProfileService::get($status->profile_id, true),
'tags' => StatusHashtagService::statusTags($status->id), 'tags' => StatusHashtagService::statusTags($status->id),
]; ];
} }

View file

@ -66,7 +66,7 @@ class StatusTransformer extends Fractal\TransformerAbstract
'label' => StatusLabelService::get($status), 'label' => StatusLabelService::get($status),
'liked_by' => LikeService::likedBy($status), 'liked_by' => LikeService::likedBy($status),
'media_attachments' => MediaService::get($status->id), 'media_attachments' => MediaService::get($status->id),
'account' => ProfileService::get($status->profile_id), 'account' => ProfileService::get($status->profile_id, true),
'tags' => StatusHashtagService::statusTags($status->id), 'tags' => StatusHashtagService::statusTags($status->id),
'poll' => $poll, 'poll' => $poll,
'bookmarked' => BookmarkService::get($pid, $status->id), 'bookmarked' => BookmarkService::get($pid, $status->id),

BIN
public/css/spa.css vendored

Binary file not shown.

Binary file not shown.

BIN
public/js/manifest.js vendored

Binary file not shown.

BIN
public/js/spa.js vendored

Binary file not shown.

View file

@ -1,3 +1 @@
/*! @source http://purl.eligrey.com/github/canvas-toBlob.js/blob/master/canvas-toBlob.js */ /*! @source http://purl.eligrey.com/github/canvas-toBlob.js/blob/master/canvas-toBlob.js */
/*! regenerator-runtime -- Copyright (c) 2014-present, Facebook, Inc. -- license (MIT): https://github.com/facebook/regenerator/blob/main/LICENSE */

Binary file not shown.

View file

@ -8,7 +8,7 @@
<hr> <hr>
<div class="form-group row"> <div class="form-group row">
<div class="col-sm-3"> <div class="col-sm-3">
<img src="{{Auth::user()->profile->avatarUrl()}}" width="38px" height="38px" class="rounded-circle float-right"> <img src="{{Auth::user()->profile->avatarUrl()}}" width="38px" height="38px" class="rounded-circle float-right" draggable="false" onerror="this.src='/storage/avatars/default.jpg?v=0';this.onerror=null;">
</div> </div>
<div class="col-sm-9"> <div class="col-sm-9">
<p class="lead font-weight-bold mb-0">{{Auth::user()->username}}</p> <p class="lead font-weight-bold mb-0">{{Auth::user()->username}}</p>

View file

@ -89,6 +89,11 @@ Route::group(['prefix' => 'api'], function() use($middleware) {
Route::get('announcements', 'Api\ApiV1Controller@getAnnouncements')->middleware($middleware); Route::get('announcements', 'Api\ApiV1Controller@getAnnouncements')->middleware($middleware);
Route::get('markers', 'Api\ApiV1Controller@getMarkers')->middleware($middleware); Route::get('markers', 'Api\ApiV1Controller@getMarkers')->middleware($middleware);
Route::post('markers', 'Api\ApiV1Controller@setMarkers')->middleware($middleware); Route::post('markers', 'Api\ApiV1Controller@setMarkers')->middleware($middleware);
Route::get('followed_tags', 'Api\ApiV1Controller@getFollowedTags')->middleware($middleware);
Route::post('tags/{id}/follow', 'Api\ApiV1Controller@followHashtag')->middleware($middleware);
Route::post('tags/{id}/unfollow', 'Api\ApiV1Controller@unfollowHashtag')->middleware($middleware);
Route::get('tags/{id}', 'Api\ApiV1Controller@getHashtag')->middleware($middleware);
}); });
Route::group(['prefix' => 'v2'], function() use($middleware) { Route::group(['prefix' => 'v2'], function() use($middleware) {

View file

@ -407,6 +407,7 @@ Route::domain(config('pixelfed.domain.app'))->middleware(['validemail', 'twofact
Route::get('web/username/{id}', 'SpaController@usernameRedirect'); Route::get('web/username/{id}', 'SpaController@usernameRedirect');
Route::get('web/post/{id}', 'SpaController@webPost'); Route::get('web/post/{id}', 'SpaController@webPost');
Route::get('web/profile/{id}', 'SpaController@webProfile'); Route::get('web/profile/{id}', 'SpaController@webProfile');
Route::get('web/{q}', 'SpaController@index')->where('q', '.*'); Route::get('web/{q}', 'SpaController@index')->where('q', '.*');
Route::get('web', 'SpaController@index'); Route::get('web', 'SpaController@index');
}); });