mirror of
https://github.com/pixelfed/pixelfed.git
synced 2024-12-20 20:13:17 +00:00
commit
03e7e24b6b
21 changed files with 282 additions and 19 deletions
14
CHANGELOG.md
14
CHANGELOG.md
|
@ -2,6 +2,12 @@
|
|||
|
||||
## [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 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 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 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/))
|
||||
|
||||
## [v0.11.6 (2023-05-03)](https://github.com/pixelfed/pixelfed/compare/v0.11.5...v0.11.6)
|
||||
|
|
|
@ -12,6 +12,8 @@ class HashtagFollow extends Model
|
|||
'hashtag_id'
|
||||
];
|
||||
|
||||
const MAX_LIMIT = 250;
|
||||
|
||||
public function hashtag()
|
||||
{
|
||||
return $this->belongsTo(Hashtag::class);
|
||||
|
|
|
@ -19,6 +19,7 @@ use App\{
|
|||
Follower,
|
||||
FollowRequest,
|
||||
Hashtag,
|
||||
HashtagFollow,
|
||||
Instance,
|
||||
Like,
|
||||
Media,
|
||||
|
@ -69,6 +70,7 @@ use App\Services\{
|
|||
BouncerService,
|
||||
CollectionService,
|
||||
FollowerService,
|
||||
HashtagService,
|
||||
InstanceService,
|
||||
LikeService,
|
||||
NetworkTimelineService,
|
||||
|
@ -99,6 +101,7 @@ use App\Jobs\FollowPipeline\FollowRejectPipeline;
|
|||
use Illuminate\Support\Facades\RateLimiter;
|
||||
use Purify;
|
||||
use Carbon\Carbon;
|
||||
use App\Http\Resources\MastoApi\FollowedTagResource;
|
||||
|
||||
class ApiV1Controller extends Controller
|
||||
{
|
||||
|
@ -3245,7 +3248,9 @@ class ApiV1Controller extends Controller
|
|||
'page' => 'nullable|integer|max:40',
|
||||
'min_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') {
|
||||
|
@ -3269,6 +3274,18 @@ class ApiV1Controller extends Controller
|
|||
$min = $request->input('min_id');
|
||||
$max = $request->input('max_id');
|
||||
$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) {
|
||||
$id = 1;
|
||||
|
@ -3281,15 +3298,19 @@ class ApiV1Controller extends Controller
|
|||
$res = StatusHashtag::whereHashtagId($tag->id)
|
||||
->whereStatusVisibility('public')
|
||||
->where('status_id', $dir, $id)
|
||||
->latest()
|
||||
->orderBy('status_id', 'desc')
|
||||
->limit($limit)
|
||||
->pluck('status_id')
|
||||
->map(function ($i) {
|
||||
if($i) {
|
||||
return StatusService::getMastodon($i);
|
||||
}
|
||||
->map(function ($i) use($pe) {
|
||||
return $pe ? StatusService::get($i) : 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']);
|
||||
})
|
||||
->values()
|
||||
|
@ -3794,4 +3815,181 @@ class ApiV1Controller extends Controller
|
|||
|
||||
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);
|
||||
}
|
||||
}
|
||||
|
|
33
app/Http/Resources/MastoApi/FollowedTagResource.php
Normal file
33
app/Http/Resources/MastoApi/FollowedTagResource.php
Normal 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,
|
||||
];
|
||||
}
|
||||
}
|
|
@ -28,8 +28,8 @@ class HashtagService {
|
|||
|
||||
public static function count($id)
|
||||
{
|
||||
return Cache::remember('services:hashtag:count:by_id:' . $id, 3600, function() use($id) {
|
||||
return StatusHashtag::whereHashtagId($id)->count();
|
||||
return Cache::remember('services:hashtag:public-count:by_id:' . $id, 86400, function() use($id) {
|
||||
return StatusHashtag::whereHashtagId($id)->whereStatusVisibility('public')->count();
|
||||
});
|
||||
}
|
||||
|
||||
|
@ -64,4 +64,9 @@ class HashtagService {
|
|||
{
|
||||
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);
|
||||
}
|
||||
}
|
||||
|
|
|
@ -85,7 +85,10 @@ class LikeService {
|
|||
return $empty;
|
||||
}
|
||||
$id = $like->profile_id;
|
||||
$profile = ProfileService::get($id);
|
||||
$profile = ProfileService::get($id, true);
|
||||
if(!$profile) {
|
||||
return [];
|
||||
}
|
||||
$profileUrl = "/i/web/profile/{$profile['id']}";
|
||||
$res = [
|
||||
'id' => (string) $profile['id'],
|
||||
|
|
|
@ -57,7 +57,7 @@ class MediaTagService
|
|||
|
||||
protected function idToUsername($id)
|
||||
{
|
||||
$profile = ProfileService::get($id);
|
||||
$profile = ProfileService::get($id, true);
|
||||
|
||||
if(!$profile) {
|
||||
return 'unavailable';
|
||||
|
|
|
@ -4,9 +4,9 @@ namespace App\Services;
|
|||
|
||||
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)
|
||||
|
|
|
@ -47,6 +47,10 @@ class StatusService
|
|||
return null;
|
||||
}
|
||||
|
||||
if(!isset($status['account'])) {
|
||||
return null;
|
||||
}
|
||||
|
||||
$status['replies_count'] = $status['reply_count'];
|
||||
|
||||
if(config('exp.emc') == false) {
|
||||
|
|
|
@ -42,7 +42,7 @@ class StatusTransformer extends Fractal\TransformerAbstract
|
|||
'card' => null,
|
||||
'poll' => null,
|
||||
'media_attachments' => MediaService::get($status->id),
|
||||
'account' => ProfileService::get($status->profile_id),
|
||||
'account' => ProfileService::get($status->profile_id, true),
|
||||
'tags' => StatusHashtagService::statusTags($status->id),
|
||||
];
|
||||
}
|
||||
|
|
|
@ -66,7 +66,7 @@ class StatusTransformer extends Fractal\TransformerAbstract
|
|||
'label' => StatusLabelService::get($status),
|
||||
'liked_by' => LikeService::likedBy($status),
|
||||
'media_attachments' => MediaService::get($status->id),
|
||||
'account' => ProfileService::get($status->profile_id),
|
||||
'account' => ProfileService::get($status->profile_id, true),
|
||||
'tags' => StatusHashtagService::statusTags($status->id),
|
||||
'poll' => $poll,
|
||||
'bookmarked' => BookmarkService::get($pid, $status->id),
|
||||
|
|
BIN
public/css/spa.css
vendored
BIN
public/css/spa.css
vendored
Binary file not shown.
BIN
public/js/discover~hashtag.bundle.279c3460159d3af7.js
vendored
Normal file
BIN
public/js/discover~hashtag.bundle.279c3460159d3af7.js
vendored
Normal file
Binary file not shown.
Binary file not shown.
BIN
public/js/manifest.js
vendored
BIN
public/js/manifest.js
vendored
Binary file not shown.
BIN
public/js/spa.js
vendored
BIN
public/js/spa.js
vendored
Binary file not shown.
|
@ -1,3 +1 @@
|
|||
/*! @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.
|
@ -8,7 +8,7 @@
|
|||
<hr>
|
||||
<div class="form-group row">
|
||||
<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 class="col-sm-9">
|
||||
<p class="lead font-weight-bold mb-0">{{Auth::user()->username}}</p>
|
||||
|
|
|
@ -89,6 +89,11 @@ Route::group(['prefix' => 'api'], function() use($middleware) {
|
|||
Route::get('announcements', 'Api\ApiV1Controller@getAnnouncements')->middleware($middleware);
|
||||
Route::get('markers', 'Api\ApiV1Controller@getMarkers')->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) {
|
||||
|
|
|
@ -407,6 +407,7 @@ Route::domain(config('pixelfed.domain.app'))->middleware(['validemail', 'twofact
|
|||
Route::get('web/username/{id}', 'SpaController@usernameRedirect');
|
||||
Route::get('web/post/{id}', 'SpaController@webPost');
|
||||
Route::get('web/profile/{id}', 'SpaController@webProfile');
|
||||
|
||||
Route::get('web/{q}', 'SpaController@index')->where('q', '.*');
|
||||
Route::get('web', 'SpaController@index');
|
||||
});
|
||||
|
|
Loading…
Reference in a new issue