Merge pull request #2474 from pixelfed/staging

Staging
This commit is contained in:
daniel 2020-12-09 22:28:49 -07:00 committed by GitHub
commit f13ba6d08f
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
60 changed files with 1492 additions and 21 deletions

View file

@ -19,6 +19,8 @@
- Add Password change email notification ([de1cca4f](https://github.com/pixelfed/pixelfed/commit/de1cca4f))
- Add shared inbox ([4733ca9f](https://github.com/pixelfed/pixelfed/commit/4733ca9f))
- Add federated photo filters ([0a5a0e86](https://github.com/pixelfed/pixelfed/commit/0a5a0e86))
- Add AccountInterstitial model and controller ([8766ccfe](https://github.com/pixelfed/pixelfed/commit/8766ccfe))
- Add Blurhash encoder ([fad102bf](https://github.com/pixelfed/pixelfed/commit/fad102bf))
### Updated
- Updated PostComponent, fix remote urls ([42716ccc](https://github.com/pixelfed/pixelfed/commit/42716ccc))
@ -106,6 +108,20 @@
- Updated federation config, make sharedInbox enabled by default. ([6e3522c0](https://github.com/pixelfed/pixelfed/commit/6e3522c0))
- Updated PostComponent, change timestamp format. ([e51665f6](https://github.com/pixelfed/pixelfed/commit/e51665f6))
- Updated PostComponent, use proper username context for reply mentions. Fixes ([#2421](https://github.com/pixelfed/pixelfed/issues/2421)). ([dac06088](https://github.com/pixelfed/pixelfed/commit/dac06088))
- Updated Navbar, added profile avatar. ([19abf1b4](https://github.com/pixelfed/pixelfed/commit/19abf1b4))
- Updated package.json, add blurhash. ([cc1b081a](https://github.com/pixelfed/pixelfed/commit/cc1b081a))
- Updated Status model, fix thumb nsfw caching. ([327ef138](https://github.com/pixelfed/pixelfed/commit/327ef138))
- Updated User model, add interstitial relation. ([bd321a72](https://github.com/pixelfed/pixelfed/commit/bd321a72))
- Updated StatusStatelessTransformer, add missing attributes. ([4d22426d](https://github.com/pixelfed/pixelfed/commit/4d22426d))
- Updated media pipeline, add blurhash support. ([473e0495](https://github.com/pixelfed/pixelfed/commit/473e0495))
- Updated DeleteAccountPipeline, add AccountInterstitial and DirectMessage purging. ([b3078f27](https://github.com/pixelfed/pixelfed/commit/b3078f27))
- Updated ComposeModal.vue component, reuse sharedData. ([e28d022f](https://github.com/pixelfed/pixelfed/commit/e28d022f))
- Updated ApiController, return status object after deletion. ([0718711d](https://github.com/pixelfed/pixelfed/commit/0718711d))
- Updated InternalApiController, add interstitial logic. ([20681bcf](https://github.com/pixelfed/pixelfed/commit/20681bcf))
- Updated PublicApiController, improve stateless object caching. ([342e7a50](https://github.com/pixelfed/pixelfed/commit/342e7a50))
- Updated StatusController, add interstitial logic. ([003caf7e](https://github.com/pixelfed/pixelfed/commit/003caf7e))
- Updated middleware, add AccountInterstitial support. ([19d6e7df](https://github.com/pixelfed/pixelfed/commit/19d6e7df))
- Updated BaseApiController, add favourites method. ([76353ca9](https://github.com/pixelfed/pixelfed/commit/76353ca9))
## [v0.10.9 (2020-04-17)](https://github.com/pixelfed/pixelfed/compare/v0.10.8...v0.10.9)
### Added

View file

@ -0,0 +1,30 @@
<?php
namespace App;
use Illuminate\Database\Eloquent\Model;
class AccountInterstitial extends Model
{
/**
* The attributes that should be mutated to dates.
*
* @var array
*/
protected $dates = ['read_at', 'appeal_requested_at'];
public const JSON_MESSAGE = 'Please use web browser to proceed.';
public function user()
{
return $this->belongsTo(User::class);
}
public function status()
{
if($this->item_type != 'App\Status') {
return;
}
return $this->hasOne(Status::class, 'id', 'item_id');
}
}

View file

@ -0,0 +1,76 @@
<?php
namespace App\Http\Controllers;
use Illuminate\Http\Request;
use App\Status;
use App\AccountInterstitial;
class AccountInterstitialController extends Controller
{
public function __construct()
{
$this->middleware('auth');
}
public function get(Request $request)
{
$interstitial = $request->user()
->interstitials()
->whereNull('read_at')
->first();
if(!$interstitial) {
$user = $request->user();
$user->has_interstitial = false;
$user->save();
return redirect('/');
}
$meta = json_decode($interstitial->meta);
$view = $interstitial->view;
return view($view, compact('interstitial', 'meta'));
}
public function read(Request $request)
{
$this->validate($request, [
'id' => 'required',
'type' => 'required|in:post.cw,post.removed,post.unlist',
'action' => 'required|in:appeal,confirm',
'appeal_message' => 'nullable|max:500'
]);
$redirect = '/';
$id = decrypt($request->input('id'));
$action = $request->input('action');
$user = $request->user();
$ai = AccountInterstitial::whereUserId($user->id)
->whereType($request->input('type'))
->findOrFail($id);
if($action == 'appeal') {
$ai->appeal_requested_at = now();
$ai->appeal_message = $request->input('appeal_message');
}
$ai->read_at = now();
$ai->save();
$more = AccountInterstitial::whereUserId($user->id)
->whereNull('read_at')
->exists();
if(!$more) {
$user->has_interstitial = false;
$user->save();
}
if(in_array($ai->type, ['post.cw', 'post.unlist'])) {
$redirect = Status::findOrFail($ai->item_id)->url();
}
return redirect($redirect);
}
}

View file

@ -3,6 +3,7 @@
namespace App\Http\Controllers;
use App\{
AccountInterstitial,
Contact,
Hashtag,
Newsroom,
@ -85,6 +86,67 @@ class AdminController extends Controller
return view('admin.reports.show', compact('report'));
}
public function appeals(Request $request)
{
$appeals = AccountInterstitial::whereNotNull('appeal_requested_at')
->whereNull('appeal_handled_at')
->latest()
->paginate(6);
return view('admin.reports.appeals', compact('appeals'));
}
public function showAppeal(Request $request, $id)
{
$appeal = AccountInterstitial::whereNotNull('appeal_requested_at')
->whereNull('appeal_handled_at')
->findOrFail($id);
$meta = json_decode($appeal->meta);
return view('admin.reports.show_appeal', compact('appeal', 'meta'));
}
public function updateAppeal(Request $request, $id)
{
$this->validate($request, [
'action' => 'required|in:dismiss,approve'
]);
$action = $request->input('action');
$appeal = AccountInterstitial::whereNotNull('appeal_requested_at')
->whereNull('appeal_handled_at')
->findOrFail($id);
if($action == 'dismiss') {
$appeal->appeal_handled_at = now();
$appeal->save();
return redirect('/i/admin/reports/appeals');
}
switch ($appeal->type) {
case 'post.cw':
$status = $appeal->status;
$status->is_nsfw = false;
$status->save();
break;
case 'post.unlist':
$status = $appeal->status;
$status->scope = 'public';
$status->visibility = 'public';
$status->save();
break;
default:
# code...
break;
}
$appeal->appeal_handled_at = now();
$appeal->save();
return redirect('/i/admin/reports/appeals');
}
public function profiles(Request $request)
{
$this->validate($request, [

View file

@ -1761,6 +1761,7 @@ class ApiV1Controller extends Controller
NewStatusPipeline::dispatch($status);
Cache::forget('user:account:id:'.$user->id);
Cache::forget('_api:statuses:recent_9:'.$user->profile_id);
Cache::forget('profile:status_count:'.$user->profile_id);
Cache::forget($user->storageUsedKey());
@ -1783,10 +1784,15 @@ class ApiV1Controller extends Controller
$status = Status::whereProfileId($request->user()->profile->id)
->findOrFail($id);
$resource = new Fractal\Resource\Item($status, new StatusTransformer());
Cache::forget('profile:status_count:'.$status->profile_id);
StatusDelete::dispatch($status);
return response()->json(['Status successfully deleted.']);
$res = $this->fractal->createData($resource)->toArray();
$res['text'] = $res['content'];
unset($res['content']);
return response()->json($res);
}
/**

View file

@ -11,8 +11,9 @@ use Auth, Cache, Storage, URL;
use Carbon\Carbon;
use App\{
Avatar,
Notification,
Like,
Media,
Notification,
Profile,
Status
};
@ -21,7 +22,8 @@ use App\Transformer\Api\{
NotificationTransformer,
MediaTransformer,
MediaDraftTransformer,
StatusTransformer
StatusTransformer,
StatusStatelessTransformer
};
use League\Fractal;
use App\Util\Media\Filter;
@ -338,4 +340,29 @@ class BaseApiController extends Controller
$res = $this->fractal->createData($resource)->toArray();
return response()->json($res, 200, [], JSON_PRETTY_PRINT|JSON_UNESCAPED_SLASHES);
}
public function accountLikes(Request $request)
{
$user = $request->user();
abort_if(!$request->user(), 403);
$limit = 10;
$page = (int) $request->input('page', 1);
if($page > 20) {
return [];
}
$favourites = $user->profile->likes()
->latest()
->simplePaginate($limit)
->pluck('status_id');
$statuses = Status::find($favourites)->reverse();
$resource = new Fractal\Resource\Collection($statuses, new StatusStatelessTransformer());
$res = $this->fractal->createData($resource)->toArray();
return response()->json($res, 200, [], JSON_PRETTY_PRINT|JSON_UNESCAPED_SLASHES);
}
}

View file

@ -4,6 +4,7 @@ namespace App\Http\Controllers;
use Illuminate\Http\Request;
use App\{
AccountInterstitial,
DirectMessage,
DiscoverCategory,
Hashtag,
@ -213,6 +214,35 @@ class InternalApiController extends Controller
])
->accessLevel('admin')
->save();
if($status->uri == null) {
$media = $status->media;
$ai = new AccountInterstitial;
$ai->user_id = $status->profile->user_id;
$ai->type = 'post.cw';
$ai->view = 'account.moderation.post.cw';
$ai->item_type = 'App\Status';
$ai->item_id = $status->id;
$ai->has_media = (bool) $media->count();
$ai->blurhash = $media->count() ? $media->first()->blurhash : null;
$ai->meta = json_encode([
'caption' => $status->caption,
'created_at' => $status->created_at,
'type' => $status->type,
'url' => $status->url(),
'is_nsfw' => $status->is_nsfw,
'scope' => $status->scope,
'reblog' => $status->reblog_of_id,
'likes_count' => $status->likes_count,
'reblogs_count' => $status->reblogs_count,
]);
$ai->save();
$u = $status->profile->user;
$u->has_interstitial = true;
$u->save();
}
break;
case 'remcw':
@ -231,6 +261,14 @@ class InternalApiController extends Controller
])
->accessLevel('admin')
->save();
if($status->uri == null) {
$ai = AccountInterstitial::whereUserId($status->profile->user_id)
->whereType('post.cw')
->whereItemId($status->id)
->whereItemType('App\Status')
->first();
$ai->delete();
}
break;
case 'unlist':
@ -250,6 +288,34 @@ class InternalApiController extends Controller
])
->accessLevel('admin')
->save();
if($status->uri == null) {
$media = $status->media;
$ai = new AccountInterstitial;
$ai->user_id = $status->profile->user_id;
$ai->type = 'post.unlist';
$ai->view = 'account.moderation.post.unlist';
$ai->item_type = 'App\Status';
$ai->item_id = $status->id;
$ai->has_media = (bool) $media->count();
$ai->blurhash = $media->count() ? $media->first()->blurhash : null;
$ai->meta = json_encode([
'caption' => $status->caption,
'created_at' => $status->created_at,
'type' => $status->type,
'url' => $status->url(),
'is_nsfw' => $status->is_nsfw,
'scope' => $status->scope,
'reblog' => $status->reblog_of_id,
'likes_count' => $status->likes_count,
'reblogs_count' => $status->reblogs_count,
]);
$ai->save();
$u = $status->profile->user;
$u->has_interstitial = true;
$u->save();
}
break;
}
return ['msg' => 200];
@ -364,6 +430,7 @@ class InternalApiController extends Controller
NewStatusPipeline::dispatch($status);
Cache::forget('user:account:id:'.$profile->user_id);
Cache::forget('_api:statuses:recent_9:'.$profile->id);
Cache::forget('profile:status_count:'.$profile->id);
Cache::forget($user->storageUsedKey());
return $status->url();

View file

@ -66,7 +66,9 @@ class ProfileController extends Controller
'list' => $settings->show_profile_followers
]
];
return view('profile.show', compact('profile', 'settings'));
$ui = $request->has('ui') && $request->input('ui') == 'memory' ? 'profile.memory' : 'profile.show';
return view($ui, compact('profile', 'settings'));
} else {
$key = 'profile:settings:' . $user->id;
$ttl = now()->addHours(6);
@ -103,7 +105,8 @@ class ProfileController extends Controller
'list' => $settings->show_profile_followers
]
];
return view('profile.show', compact('profile', 'settings'));
$ui = $request->has('ui') && $request->input('ui') == 'memory' ? 'profile.memory' : 'profile.show';
return view($ui, compact('profile', 'settings'));
}
}

View file

@ -20,7 +20,8 @@ use League\Fractal;
use App\Transformer\Api\{
AccountTransformer,
RelationshipTransformer,
StatusTransformer
StatusTransformer,
StatusStatelessTransformer
};
use App\Services\{
AccountService,
@ -86,6 +87,24 @@ class PublicApiController extends Controller
$profile = Profile::whereUsername($username)->whereNull('status')->firstOrFail();
$status = Status::whereProfileId($profile->id)->findOrFail($postid);
$this->scopeCheck($profile, $status);
if(!Auth::check()) {
$res = Cache::remember('wapi:v1:status:stateless_byid:' . $status->id, now()->addMinutes(30), function() use($status) {
$item = new Fractal\Resource\Item($status, new StatusStatelessTransformer());
$res = [
'status' => $this->fractal->createData($item)->toArray(),
'user' => [],
'likes' => [],
'shares' => [],
'reactions' => [
'liked' => false,
'shared' => false,
'bookmarked' => false,
],
];
return response()->json($res, 200, [], JSON_PRETTY_PRINT|JSON_UNESCAPED_SLASHES);
});
return $res;
}
$item = new Fractal\Resource\Item($status, new StatusTransformer());
$res = [
'status' => $this->fractal->createData($item)->toArray(),
@ -419,7 +438,6 @@ class PublicApiController extends Controller
}
public function networkTimelineApi(Request $request)
{
return response()->json([]);
@ -543,6 +561,50 @@ class PublicApiController extends Controller
}
}
$tag = in_array('private', $visibility) ? 'private' : 'public';
if($min_id == 1 && $limit == 9 && $tag == 'public') {
$limit = 9;
$scope = ['photo', 'photo:album', 'video', 'video:album'];
$key = '_api:statuses:recent_9:'.$profile->id;
$res = Cache::remember($key, now()->addHours(24), function() use($profile, $scope, $visibility, $limit) {
$dir = '>';
$id = 1;
$timeline = Status::select(
'id',
'uri',
'caption',
'rendered',
'profile_id',
'type',
'in_reply_to_id',
'reblog_of_id',
'is_nsfw',
'likes_count',
'reblogs_count',
'scope',
'visibility',
'local',
'place_id',
'comments_disabled',
'cw_summary',
'created_at',
'updated_at'
)->whereProfileId($profile->id)
->whereIn('type', $scope)
->where('id', $dir, $id)
->whereIn('visibility', $visibility)
->limit($limit)
->orderByDesc('id')
->get();
$resource = new Fractal\Resource\Collection($timeline, new StatusStatelessTransformer());
$res = $this->fractal->createData($resource)->toArray();
return response()->json($res, 200, [], JSON_PRETTY_PRINT|JSON_UNESCAPED_SLASHES);
});
return $res;
}
$dir = $min_id ? '>' : '<';
$id = $min_id ?? $max_id;
$timeline = Status::select(
@ -560,6 +622,8 @@ class PublicApiController extends Controller
'scope',
'visibility',
'local',
'place_id',
'comments_disabled',
'cw_summary',
'created_at',
'updated_at'

View file

@ -6,6 +6,7 @@ use App\Jobs\ImageOptimizePipeline\ImageOptimize;
use App\Jobs\StatusPipeline\NewStatusPipeline;
use App\Jobs\StatusPipeline\StatusDelete;
use App\Jobs\SharePipeline\SharePipeline;
use App\AccountInterstitial;
use App\Media;
use App\Profile;
use App\Status;
@ -162,14 +163,49 @@ class StatusController extends Controller
$status = Status::findOrFail($request->input('item'));
if ($status->profile_id === Auth::user()->profile->id || Auth::user()->is_admin == true) {
$user = Auth::user();
if($status->profile_id != $user->profile->id &&
$user->is_admin == true &&
$status->uri == null
) {
$media = $status->media;
$ai = new AccountInterstitial;
$ai->user_id = $status->profile->user_id;
$ai->type = 'post.removed';
$ai->view = 'account.moderation.post.removed';
$ai->item_type = 'App\Status';
$ai->item_id = $status->id;
$ai->has_media = (bool) $media->count();
$ai->blurhash = $media->count() ? $media->first()->blurhash : null;
$ai->meta = json_encode([
'caption' => $status->caption,
'created_at' => $status->created_at,
'type' => $status->type,
'url' => $status->url(),
'is_nsfw' => $status->is_nsfw,
'scope' => $status->scope,
'reblog' => $status->reblog_of_id,
'likes_count' => $status->likes_count,
'reblogs_count' => $status->reblogs_count,
]);
$ai->save();
$u = $status->profile->user;
$u->has_interstitial = true;
$u->save();
}
if ($status->profile_id == $user->profile->id || $user->is_admin == true) {
Cache::forget('profile:status_count:'.$status->profile_id);
StatusDelete::dispatch($status);
}
if($request->wantsJson()) {
return response()->json(['Status successfully deleted.']);
} else {
return redirect(Auth::user()->url());
return redirect($user->url());
}
}

View file

@ -66,6 +66,7 @@ class Kernel extends HttpKernel
'throttle' => \Illuminate\Routing\Middleware\ThrottleRequests::class,
'twofactor' => \App\Http\Middleware\TwoFactorAuth::class,
'validemail' => \App\Http\Middleware\EmailVerificationCheck::class,
'interstitial' => \App\Http\Middleware\AccountInterstitial::class,
// 'restricted' => \App\Http\Middleware\RestrictedAccess::class,
];
}

View file

@ -0,0 +1,48 @@
<?php
namespace App\Http\Middleware;
use Closure;
use Auth;
use App\User;
class AccountInterstitial
{
/**
* Handle an incoming request.
*
* @param \Illuminate\Http\Request $request
* @param \Closure $next
* @return mixed
*/
public function handle($request, Closure $next)
{
$ar = [
'login',
'logout',
'password*',
'loginAs*',
'i/warning*',
'i/auth/checkpoint',
'i/auth/sudo',
'site/privacy',
'site/terms',
'site/kb/community-guidelines',
];
if(Auth::check() && !$request->is($ar)) {
if($request->user()->has_interstitial) {
if($request->wantsJson()) {
$res = ['_refresh'=>true,'error' => 403, 'message' => \App\AccountInterstitial::JSON_MESSAGE];
return response()->json($res, 403);
} else {
return redirect('/i/warning');
}
} else {
return $next($request);
}
} else {
return $next($request);
}
}
}

View file

@ -10,6 +10,7 @@ use Illuminate\Foundation\Bus\Dispatchable;
use DB;
use Illuminate\Support\Str;
use App\{
AccountInterstitial,
AccountLog,
Activity,
Avatar,
@ -68,6 +69,10 @@ class DeleteAccountPipeline implements ShouldQueue
});
});
DB::transaction(function() use ($user) {
AccountInterstitial::whereUserId($user->id)->delete();
});
DB::transaction(function() use ($user) {
if($user->profile) {
$avatar = $user->profile->avatar;
@ -79,6 +84,7 @@ class DeleteAccountPipeline implements ShouldQueue
Bookmark::whereProfileId($user->profile_id)->forceDelete();
EmailVerification::whereUserId($user->id)->forceDelete();
StatusHashtag::whereProfileId($id)->delete();
DirectMessage::whereFromId($user->profile_id)->delete();
FollowRequest::whereFollowingId($id)
->orWhere('follower_id', $id)
->forceDelete();

View file

@ -89,7 +89,8 @@ class Status extends Model
public function thumb($showNsfw = false)
{
return Cache::remember('status:thumb:'.$this->id, now()->addMinutes(15), function() use ($showNsfw) {
$key = $showNsfw ? 'status:thumb:nsfw1'.$this->id : 'status:thumb:nsfw0'.$this->id;
return Cache::remember($key, now()->addMinutes(15), function() use ($showNsfw) {
$type = $this->type ?? $this->setType();
$is_nsfw = !$showNsfw ? $this->is_nsfw : false;
if ($this->media->count() == 0 || $is_nsfw || !in_array($type,['photo', 'photo:album', 'video'])) {

View file

@ -5,6 +5,8 @@ namespace App\Transformer\Api;
use App\Status;
use League\Fractal;
use Cache;
use App\Services\HashidService;
use App\Services\MediaTagService;
class StatusStatelessTransformer extends Fractal\TransformerAbstract
{
@ -17,8 +19,11 @@ class StatusStatelessTransformer extends Fractal\TransformerAbstract
public function transform(Status $status)
{
$taggedPeople = MediaTagService::get($status->id);
return [
'id' => (string) $status->id,
'shortcode' => HashidService::encode($status->id),
'uri' => $status->url(),
'url' => $status->url(),
'in_reply_to_id' => $status->in_reply_to_id,
@ -42,13 +47,17 @@ class StatusStatelessTransformer extends Fractal\TransformerAbstract
'language' => null,
'pinned' => null,
'mentions' => [],
'tags' => [],
'pf_type' => $status->type ?? $status->setType(),
'reply_count' => (int) $status->reply_count,
'comments_disabled' => $status->comments_disabled ? true : false,
'thread' => false,
'replies' => [],
'parent' => $status->parent() ? $this->transform($status->parent()) : [],
'parent' => [],
'place' => $status->place,
'local' => (bool) $status->local,
'taggedPeople' => $taggedPeople
];
}

View file

@ -88,4 +88,9 @@ class User extends Authenticatable
return $this->hasMany(AccountLog::class);
}
public function interstitials()
{
return $this->hasMany(AccountInterstitial::class);
}
}

34
app/Util/Blurhash/AC.php Normal file
View file

@ -0,0 +1,34 @@
<?php
namespace App\Util\Blurhash;
final class AC {
public static function encode(array $value, float $max_value): float {
$quant_r = static::quantise($value[0] / $max_value);
$quant_g = static::quantise($value[1] / $max_value);
$quant_b = static::quantise($value[2] / $max_value);
return $quant_r * 19 * 19 + $quant_g * 19 + $quant_b;
}
public static function decode(int $value, float $max_value): array {
$quant_r = floor($value / (19 * 19));
$quant_g = floor($value / 19) % 19;
$quant_b = $value % 19;
return [
static::signPow(($quant_r - 9) / 9, 2) * $max_value,
static::signPow(($quant_g - 9) / 9, 2) * $max_value,
static::signPow(($quant_b - 9) / 9, 2) * $max_value
];
}
private static function quantise(float $value): float {
return floor(max(0, min(18, floor(static::signPow($value, 0.5) * 9 + 9.5))));
}
private static function signPow(float $base, float $exp): float {
$sign = $base <=> 0;
return $sign * pow(abs($base), $exp);
}
}

View file

@ -0,0 +1,39 @@
<?php
namespace App\Util\Blurhash;
use InvalidArgumentException;
class Base83 {
private const ALPHABET = [
'0', '1', '2', '3', '4', '5', '6', '7', '8', '9', 'A', 'B', 'C', 'D',
'E', 'F', 'G', 'H', 'I', 'J', 'K', 'L', 'M', 'N', 'O', 'P', 'Q', 'R',
'S', 'T', 'U', 'V', 'W', 'X', 'Y', 'Z', 'a', 'b', 'c', 'd', 'e', 'f',
'g', 'h', 'i', 'j', 'k', 'l', 'm', 'n', 'o', 'p', 'q', 'r', 's', 't',
'u', 'v', 'w', 'x', 'y', 'z', '#', '$', '%', '*', '+', ',', '-', '.',
':', ';', '=', '?', '@', '[', ']', '^', '_', '{', '|', '}', '~'
];
private const BASE = 83;
public static function encode(int $value, int $length): string {
if (floor($value / (self::BASE ** $length)) != 0) {
throw new InvalidArgumentException('Specified length is too short to encode given value.');
}
$result = '';
for ($i = 1; $i <= $length; $i++) {
$digit = floor($value / (self::BASE ** ($length - $i))) % self::BASE;
$result .= self::ALPHABET[$digit];
}
return $result;
}
public static function decode(string $hash): int {
$result = 0;
foreach (str_split($hash) as $char) {
$result = $result * self::BASE + (int) array_search($char, self::ALPHABET, true);
}
return (int) $result;
}
}

View file

@ -0,0 +1,139 @@
<?php
namespace App\Util\Blurhash;
use InvalidArgumentException;
class Blurhash {
public static function encode(array $image, int $components_x = 4, int $components_y = 4, bool $linear = false): string {
if (($components_x < 1 || $components_x > 9) || ($components_y < 1 || $components_y > 9)) {
throw new InvalidArgumentException("x and y component counts must be between 1 and 9 inclusive.");
}
$height = count($image);
$width = count($image[0]);
$image_linear = $image;
if (!$linear) {
$image_linear = [];
for ($y = 0; $y < $height; $y++) {
$line = [];
for ($x = 0; $x < $width; $x++) {
$pixel = $image[$y][$x];
$line[] = [
Color::toLinear($pixel[0]),
Color::toLinear($pixel[1]),
Color::toLinear($pixel[2])
];
}
$image_linear[] = $line;
}
}
$components = [];
$scale = 1 / ($width * $height);
for ($y = 0; $y < $components_y; $y++) {
for ($x = 0; $x < $components_x; $x++) {
$normalisation = $x == 0 && $y == 0 ? 1 : 2;
$r = $g = $b = 0;
for ($i = 0; $i < $width; $i++) {
for ($j = 0; $j < $height; $j++) {
$color = $image_linear[$j][$i];
$basis = $normalisation
* cos(M_PI * $i * $x / $width)
* cos(M_PI * $j * $y / $height);
$r += $basis * $color[0];
$g += $basis * $color[1];
$b += $basis * $color[2];
}
}
$components[] = [
$r * $scale,
$g * $scale,
$b * $scale
];
}
}
$dc_value = DC::encode(array_shift($components) ?: []);
$max_ac_component = 0;
foreach ($components as $component) {
$component[] = $max_ac_component;
$max_ac_component = max ($component);
}
$quant_max_ac_component = (int) max(0, min(82, floor($max_ac_component * 166 - 0.5)));
$ac_component_norm_factor = ($quant_max_ac_component + 1) / 166;
$ac_values = [];
foreach ($components as $component) {
$ac_values[] = AC::encode($component, $ac_component_norm_factor);
}
$blurhash = Base83::encode($components_x - 1 + ($components_y - 1) * 9, 1);
$blurhash .= Base83::encode($quant_max_ac_component, 1);
$blurhash .= Base83::encode($dc_value, 4);
foreach ($ac_values as $ac_value) {
$blurhash .= Base83::encode((int) $ac_value, 2);
}
return $blurhash;
}
public static function decode (string $blurhash, int $width, int $height, float $punch = 1.0, bool $linear = false): array {
if (empty($blurhash) || strlen($blurhash) < 6) {
throw new InvalidArgumentException("Blurhash string must be at least 6 characters");
}
$size_info = Base83::decode($blurhash[0]);
$size_y = floor($size_info / 9) + 1;
$size_x = ($size_info % 9) + 1;
$length = (int) strlen($blurhash);
$expected_length = (int) (4 + (2 * $size_y * $size_x));
if ($length !== $expected_length) {
throw new InvalidArgumentException("Blurhash length mismatch: length is {$length} but it should be {$expected_length}");
}
$colors = [DC::decode(Base83::decode(substr($blurhash, 2, 4)))];
$quant_max_ac_component = Base83::decode($blurhash[1]);
$max_value = ($quant_max_ac_component + 1) / 166;
for ($i = 1; $i < $size_x * $size_y; $i++) {
$value = Base83::decode(substr($blurhash, 4 + $i * 2, 2));
$colors[$i] = AC::decode($value, $max_value * $punch);
}
$pixels = [];
for ($y = 0; $y < $height; $y++) {
$row = [];
for ($x = 0; $x < $width; $x++) {
$r = $g = $b = 0;
for ($j = 0; $j < $size_y; $j++) {
for ($i = 0; $i < $size_x; $i++) {
$color = $colors[$i + $j * $size_x];
$basis =
cos((M_PI * $x * $i) / $width) *
cos((M_PI * $y * $j) / $height);
$r += $color[0] * $basis;
$g += $color[1] * $basis;
$b += $color[2] * $basis;
}
}
$row[] = $linear ? [$r, $g, $b] : [
Color::toSRGB($r),
Color::toSRGB($g),
Color::toSRGB($b)
];
}
$pixels[] = $row;
}
return $pixels;
}
}

View file

@ -0,0 +1,19 @@
<?php
namespace App\Util\Blurhash;
final class Color {
public static function toLinear(int $value): float {
$value = $value / 255;
return ($value <= 0.04045)
? $value / 12.92
: pow(($value + 0.055) / 1.055, 2.4);
}
public static function tosRGB(float $value): int {
$normalized = max(0, min(1, $value));
return ($normalized <= 0.0031308)
? (int) round($normalized * 12.92 * 255 + 0.5)
: (int) round((1.055 * pow($normalized, 1 / 2.4) - 0.055) * 255 + 0.5);
}
}

24
app/Util/Blurhash/DC.php Normal file
View file

@ -0,0 +1,24 @@
<?php
namespace App\Util\Blurhash;
final class DC {
public static function encode(array $value): int {
$rounded_r = Color::tosRGB($value[0]);
$rounded_g = Color::tosRGB($value[1]);
$rounded_b = Color::tosRGB($value[2]);
return ($rounded_r << 16) + ($rounded_g << 8) + $rounded_b;
}
public static function decode(int $value): array {
$r = $value >> 16;
$g = ($value >> 8) & 255;
$b = $value & 255;
return [
Color::toLinear($r),
Color::toLinear($g),
Color::toLinear($b)
];
}
}

View file

@ -0,0 +1,47 @@
<?php
namespace App\Util\Media;
use App\Util\Blurhash\Blurhash as BlurhashEngine;
use App\Media;
class Blurhash {
public static function generate(Media $media)
{
if(!in_array($media->mime, ['image/png', 'image/jpeg'])) {
return;
}
$file = storage_path('app/' . $media->thumbnail_path);
if(!is_file($file)) {
return;
}
$image = imagecreatefromstring(file_get_contents($file));
$width = imagesx($image);
$height = imagesy($image);
$pixels = [];
for ($y = 0; $y < $height; ++$y) {
$row = [];
for ($x = 0; $x < $width; ++$x) {
$index = imagecolorat($image, $x, $y);
$colors = imagecolorsforindex($image, $index);
$row[] = [$colors['red'], $colors['green'], $colors['blue']];
}
$pixels[] = $row;
}
$components_x = 4;
$components_y = 4;
$blurhash = BlurhashEngine::encode($pixels, $components_x, $components_y);
if(strlen($blurhash) > 191) {
return;
}
return $blurhash;
}
}

View file

@ -182,6 +182,10 @@ class Image
$media->save();
if($thumbnail) {
$this->generateBlurhash($media);
}
Cache::forget('status:transformer:media:attachments:'.$media->status_id);
Cache::forget('status:thumb:'.$media->status_id);
} catch (Exception $e) {
@ -198,4 +202,13 @@ class Image
return ['path' => $basePath, 'png' => $png];
}
protected function generateBlurhash($media)
{
$blurhash = Blurhash::generate($media);
if($blurhash) {
$media->blurhash = $blurhash;
$media->save();
}
}
}

View file

@ -0,0 +1,34 @@
<?php
use Illuminate\Database\Migrations\Migration;
use Illuminate\Database\Schema\Blueprint;
use Illuminate\Support\Facades\Schema;
class AddIndexesToLikesTable extends Migration
{
/**
* Run the migrations.
*
* @return void
*/
public function up()
{
Schema::table('likes', function (Blueprint $table) {
$table->index('profile_id', 'likes_profile_id_index');
$table->index('status_id', 'likes_status_id_index');
});
}
/**
* Reverse the migrations.
*
* @return void
*/
public function down()
{
Schema::table('likes', function (Blueprint $table) {
$table->dropIndex('likes_profile_id_index');
$table->dropIndex('likes_status_id_index');
});
}
}

View file

@ -0,0 +1,53 @@
<?php
use Illuminate\Database\Migrations\Migration;
use Illuminate\Database\Schema\Blueprint;
use Illuminate\Support\Facades\Schema;
class CreateAccountInterstitialsTable extends Migration
{
/**
* Run the migrations.
*
* @return void
*/
public function up()
{
Schema::create('account_interstitials', function (Blueprint $table) {
$table->id();
$table->unsignedInteger('user_id')->nullable()->index();
$table->string('type')->nullable();
$table->string('view')->nullable();
$table->bigInteger('item_id')->unsigned()->nullable();
$table->string('item_type')->nullable();
$table->boolean('has_media')->default(false)->nullable();
$table->string('blurhash')->nullable();
$table->text('message')->nullable();
$table->text('violation_header')->nullable();
$table->text('violation_body')->nullable();
$table->json('meta')->nullable();
$table->text('appeal_message')->nullable();
$table->timestamp('appeal_requested_at')->nullable()->index();
$table->timestamp('appeal_handled_at')->nullable()->index();
$table->timestamp('read_at')->nullable()->index();
$table->timestamps();
});
Schema::table('users', function(Blueprint $table) {
$table->boolean('has_interstitial')->default(false)->index();
});
}
/**
* Reverse the migrations.
*
* @return void
*/
public function down()
{
Schema::dropIfExists('account_interstitials');
Schema::table('users', function (Blueprint $table) {
$table->dropColumn('has_interstitial');
});
}
}

5
package-lock.json generated
View file

@ -1649,6 +1649,11 @@
"resolved": "https://registry.npmjs.org/bluebird/-/bluebird-3.7.1.tgz",
"integrity": "sha512-DdmyoGCleJnkbp3nkbxTLJ18rjDsE4yCggEwKNXkeV123sPNfOCYeDoeuOY+F2FrSjO1YXcTU+dsy96KMy+gcg=="
},
"blurhash": {
"version": "1.1.3",
"resolved": "https://registry.npmjs.org/blurhash/-/blurhash-1.1.3.tgz",
"integrity": "sha512-yUhPJvXexbqbyijCIE/T2NCXcj9iNPhWmOKbPTuR/cm7Q5snXYIfnVnz6m7MWOXxODMz/Cr3UcVkRdHiuDVRDw=="
},
"bn.js": {
"version": "5.1.1",
"resolved": "https://registry.npmjs.org/bn.js/-/bn.js-5.1.1.tgz",

View file

@ -27,6 +27,7 @@
"dependencies": {
"@trevoreyre/autocomplete-vue": "^2.2.0",
"animate.css": "^4.1.0",
"blurhash": "^1.1.3",
"bootstrap-vue": "^2.16.0",
"filesize": "^3.6.1",
"howler": "^2.2.0",

BIN
public/js/activity.js vendored

Binary file not shown.

BIN
public/js/app.js vendored

Binary file not shown.

Binary file not shown.

Binary file not shown.

BIN
public/js/compose.js vendored

Binary file not shown.

BIN
public/js/direct.js vendored

Binary file not shown.

BIN
public/js/discover.js vendored

Binary file not shown.

BIN
public/js/hashtag.js vendored

Binary file not shown.

BIN
public/js/memoryprofile.js vendored Normal file

Binary file not shown.

BIN
public/js/mode-dot.js vendored

Binary file not shown.

Binary file not shown.

BIN
public/js/profile.js vendored

Binary file not shown.

BIN
public/js/quill.js vendored

Binary file not shown.

BIN
public/js/rempos.js vendored

Binary file not shown.

BIN
public/js/rempro.js vendored

Binary file not shown.

BIN
public/js/search.js vendored

Binary file not shown.

BIN
public/js/status.js vendored

Binary file not shown.

Binary file not shown.

Binary file not shown.

BIN
public/js/timeline.js vendored

Binary file not shown.

BIN
public/js/vendor.js vendored

Binary file not shown.

Binary file not shown.

View file

@ -6,6 +6,7 @@ require('bootstrap');
window.axios = require('axios');
window.axios.defaults.headers.common['X-Requested-With'] = 'XMLHttpRequest';
require('readmore-js');
window.blurhash = require("blurhash");
let token = document.head.querySelector('meta[name="csrf-token"]');
if (token) {

View file

@ -635,6 +635,13 @@ export default {
methods: {
fetchProfile() {
let self = this;
if(window._sharedData.curUser) {
self.profile = window._sharedData.curUser;
if(self.profile.locked == true) {
self.visibility = 'private';
self.visibilityTag = 'Followers Only';
}
} else {
axios.get('/api/pixelfed/v1/accounts/verify_credentials').then(res => {
self.profile = res.data;
window.pixelfed.currentUser = res.data;
@ -644,6 +651,7 @@ export default {
}
}).catch(err => {
});
}
},
addMedia(event) {

View file

@ -0,0 +1,130 @@
@extends('layouts.blank')
@section('content')
<div class="container mt-5">
<div class="row">
<div class="col-12 col-md-6 offset-md-3 text-center">
<p class="h1 pb-2" style="font-weight: 200">Your Post Contains Sensitive or Offensive Material</p>
<p class="lead py-3">We applied a Content Warning to your post because it doesn't follow our <a class="font-weight-bold text-dark" href="{{route('help.community-guidelines')}}">Community Guidelines</a>.</p>
<p class="font-weight-bold alert alert-danger text-left">To continue you must click the "I Understand" button or "REQUEST APPEAL" button at the bottom of this page.</p>
</div>
<div class="col-12 col-md-6 offset-md-3">
<hr>
</div>
<div class="col-12 col-md-6 offset-md-3 mt-3">
<p class="h4 font-weight-bold">Post Details</p>
@if($interstitial->has_media)
<div class="py-4 align-items-center">
<div class="d-block text-center text-truncate">
@if($interstitial->blurhash)
<canvas id="mblur" width="400" height="400" class="rounded shadow"></canvas>
@else
<img src="/storage/no-preview.png" class="mr-3 img-fluid" alt="No preview available">
@endif
</div>
<div class="mt-2 border rounded p-3">
@if($meta->caption)
<p class="text-break">
Caption: <span class="font-weight-bold">{{$meta->caption}}</span>
</p>
@endif
<p class="mb-0">
Like Count: <span class="font-weight-bold">{{$meta->likes_count}}</span>
</p>
<p class="mb-0">
Share Count: <span class="font-weight-bold">{{$meta->reblogs_count}}</span>
</p>
<p class="">
Timestamp: <span class="font-weight-bold">{{now()->parse($meta->created_at)->format('r')}}</span>
</p>
<p class="mb-0" style="word-break: break-all !important;">
URL: <span class="font-weight-bold text-primary">{{$meta->url}}</span>
</p>
</div>
</div>
@else
<div class="py-4 align-items-center">
<div class="mt-2 border rounded p-3">
@if($meta->caption)
<p class="text-break">
Comment: <span class="font-weight-bold">{{$meta->caption}}</span>
</p>
@endif
<p class="mb-0">
Like Count: <span class="font-weight-bold">{{$meta->likes_count}}</span>
</p>
<p class="mb-0">
Share Count: <span class="font-weight-bold">{{$meta->reblogs_count}}</span>
</p>
<p class="">
Timestamp: <span class="font-weight-bold">{{now()->parse($meta->created_at)->format('r')}}</span>
</p>
<p class="mb-0" style="word-break: break-all !important;">
URL: <span class="font-weight-bold text-primary">{{$meta->url}}</span>
</p>
</div>
</div>
@endif
</div>
<div class="col-12 col-md-6 offset-md-3 my-3">
<div class="border rounded p-3 border-primary">
<p class="h4 font-weight-bold pt-2 text-primary">Review the Community Guidelines</p>
<p class="lead pt-4 text-primary">We want to keep {{config('app.name')}} a safe place for everyone, and we created these <a class="font-weight-bold text-primary" href="{{route('help.community-guidelines')}}">Community Guidelines</a> to support and protect our community.</p>
</div>
</div>
<div id="appealButton" class="col-12 col-md-6 offset-md-3 mt-3">
<button type="button" class="btn btn-outline-primary btn-block font-weight-bold" onclick="requestAppeal()">REQUEST APPEAL</button>
</div>
<div id="appealForm" class="col-12 col-md-6 offset-md-3 d-none mt-3">
<form method="post" action="/i/warning">
@csrf
<p class="h4 font-weight-bold">Request Appeal</p>
<p class="pt-4">
<div class="form-group">
<textarea class="form-control" rows="4" placeholder="Write your appeal request message here" name="appeal_message"></textarea>
</div>
</p>
{{-- <p class="lead"><span class="font-weight-bold">Learn more</span> about what we remove.</p> --}}
<input type="hidden" name="id" value="{{encrypt($interstitial->id)}}">
<input type="hidden" name="type" value="{{$interstitial->type}}">
<input type="hidden" name="action" value="appeal">
<button type="submit" class="btn btn-outline-primary btn-block font-weight-bold">REQUEST APPEAL</button>
</form>
</div>
<div class="col-12 col-md-6 offset-md-3 mt-4 mb-4">
<form method="post" action="/i/warning">
@csrf
<input type="hidden" name="id" value="{{encrypt($interstitial->id)}}">
<input type="hidden" name="type" value="{{$interstitial->type}}">
<input type="hidden" name="action" value="confirm">
<button type="submit" class="btn btn-primary btn-block font-weight-bold">I Understand</button>
</form>
</div>
</div>
</div>
@endsection
@push('scripts')
<script type="text/javascript">
function requestAppeal() {
$('#appealButton').addClass('d-none');
$('#appealForm').removeClass('d-none');
}
</script>
@if($interstitial->blurhash)
<script type="text/javascript">
const pixels = window.blurhash.decode("{{$interstitial->blurhash}}", 400, 400);
const canvas = document.getElementById("mblur");
const ctx = canvas.getContext("2d");
const imageData = ctx.createImageData(400, 400);
imageData.data.set(pixels);
ctx.putImageData(imageData, 0, 0);
</script>
@endif
@endpush

View file

@ -0,0 +1,99 @@
@extends('layouts.blank')
@section('content')
<div class="container mt-5">
<div class="row">
<div class="col-12 col-md-6 offset-md-3 text-center">
<p class="h1 pb-2" style="font-weight: 200">Your Post Has Been Deleted</p>
<p class="lead py-1">We removed your post because it doesn't follow our <a class="font-weight-bold text-dark" href="{{route('help.community-guidelines')}}">Community Guidelines</a>. If you violate our guidelines again, your account may be restricted or disabled.</p>
<p class="font-weight-bold alert alert-danger text-left">To continue you must click the "I Understand" button at the bottom of this page.</p>
</div>
<div class="col-12 col-md-6 offset-md-3">
<hr>
</div>
<div class="col-12 col-md-6 offset-md-3 mt-3">
<p class="h4 font-weight-bold">Post Details</p>
@if($interstitial->has_media)
<div class="py-4 align-items-center">
<div class="d-block text-center text-truncate">
@if($interstitial->blurhash)
<canvas id="mblur" width="400" height="400" class="rounded shadow"></canvas>
@else
<img src="/storage/no-preview.png" class="mr-3 img-fluid" alt="No preview available">
@endif
</div>
<div class="mt-2 border rounded p-3">
@if($meta->caption)
<p class="text-break">
Caption: <span class="font-weight-bold">{{$meta->caption}}</span>
</p>
@endif
<p class="mb-0">
Like Count: <span class="font-weight-bold">{{$meta->likes_count}}</span>
</p>
<p class="mb-0">
Share Count: <span class="font-weight-bold">{{$meta->reblogs_count}}</span>
</p>
<p class="">
Timestamp: <span class="font-weight-bold">{{now()->parse($meta->created_at)->format('r')}}</span>
</p>
<p class="mb-0" style="word-break: break-all !important;">
URL: <span class="font-weight-bold text-primary">{{$meta->url}}</span>
</p>
</div>
</div>
@else
<div class="media py-4 align-items-center">
<div class="media-body ml-2">
<p class="">
Comment: <span class="lead text-break font-weight-bold">{{$meta->caption}}</span>
</p>
<p class="mb-0 small">
Posted on {{$meta->created_at}}
</p>
<p class="mb-0 font-weight-bold text-primary">
{{$meta->url}}
</p>
</div>
</div>
@endif
</div>
<div class="col-12 col-md-6 offset-md-3 my-3">
<div class="border rounded p-3 border-primary">
<p class="h4 font-weight-bold pt-2 text-primary">Review the Community Guidelines</p>
<p class="lead pt-4 text-primary">We want to keep {{config('app.name')}} a safe place for everyone, and we created these <a class="font-weight-bold text-primary" href="{{route('help.community-guidelines')}}">Community Guidelines</a> to support and protect our community.</p>
</div>
</div>
<div class="col-12 col-md-6 offset-md-3 mt-4 mb-5">
<form method="post" action="/i/warning">
@csrf
<input type="hidden" name="id" value="{{encrypt($interstitial->id)}}">
<input type="hidden" name="type" value="{{$interstitial->type}}">
<input type="hidden" name="action" value="confirm">
<button type="submit" class="btn btn-primary btn-block font-weight-bold">I Understand</button>
</form>
</div>
</div>
</div>
@endsection
@push('scripts')
<script type="text/javascript">
function requestAppeal() {
$('#appealButton').addClass('d-none');
$('#appealForm').removeClass('d-none');
}
</script>
@if($interstitial->blurhash)
<script type="text/javascript">
const pixels = window.blurhash.decode("{{$interstitial->blurhash}}", 400, 400);
const canvas = document.getElementById("mblur");
const ctx = canvas.getContext("2d");
const imageData = ctx.createImageData(400, 400);
imageData.data.set(pixels);
ctx.putImageData(imageData, 0, 0);
</script>
@endif
@endpush

View file

@ -0,0 +1,128 @@
@extends('layouts.blank')
@section('content')
<div class="container mt-5">
<div class="row">
<div class="col-12 col-md-6 offset-md-3 text-center">
<p class="h1 pb-2" style="font-weight: 200">Your Post Was Unlisted</p>
<p class="lead py-3">We removed your post from public timelines because it doesn't follow our <a class="font-weight-bold text-dark" href="{{route('help.community-guidelines')}}">Community Guidelines</a>.</p>
</div>
<div class="col-12 col-md-6 offset-md-3">
<hr>
</div>
<div class="col-12 col-md-6 offset-md-3">
<p class="h4 font-weight-bold">Post Details</p>
@if($interstitial->has_media)
<div class="py-4 align-items-center">
<div class="d-block text-center text-truncate">
@if($interstitial->blurhash)
<canvas id="mblur" width="400" height="400" class="rounded shadow"></canvas>
@else
<img src="/storage/no-preview.png" class="mr-3 img-fluid" alt="No preview available">
@endif
</div>
<div class="mt-2 border rounded p-3">
@if($meta->caption)
<p class="text-break">
Caption: <span class="font-weight-bold">{{$meta->caption}}</span>
</p>
@endif
<p class="mb-0">
Like Count: <span class="font-weight-bold">{{$meta->likes_count}}</span>
</p>
<p class="mb-0">
Share Count: <span class="font-weight-bold">{{$meta->reblogs_count}}</span>
</p>
<p class="">
Timestamp: <span class="font-weight-bold">{{now()->parse($meta->created_at)->format('r')}}</span>
</p>
<p class="mb-0" style="word-break: break-all !important;">
URL: <span class="font-weight-bold text-primary">{{$meta->url}}</span>
</p>
</div>
</div>
@else
<div class="py-4 align-items-center">
<div class="mt-2 border rounded p-3">
@if($meta->caption)
<p class="text-break">
Comment: <span class="font-weight-bold">{{$meta->caption}}</span>
</p>
@endif
<p class="mb-0">
Like Count: <span class="font-weight-bold">{{$meta->likes_count}}</span>
</p>
<p class="mb-0">
Share Count: <span class="font-weight-bold">{{$meta->reblogs_count}}</span>
</p>
<p class="">
Timestamp: <span class="font-weight-bold">{{now()->parse($meta->created_at)->format('r')}}</span>
</p>
<p class="mb-0" style="word-break: break-all !important;">
URL: <span class="font-weight-bold text-primary">{{$meta->url}}</span>
</p>
</div>
</div>
@endif
</div>
<div class="col-12 col-md-6 offset-md-3 my-3">
<div class="border rounded p-3 border-primary">
<p class="h4 font-weight-bold pt-2 text-primary">Review the Community Guidelines</p>
<p class="lead pt-4 text-primary">We want to keep {{config('app.name')}} a safe place for everyone, and we created these <a class="font-weight-bold text-primary" href="{{route('help.community-guidelines')}}">Community Guidelines</a> to support and protect our community.</p>
</div>
</div>
<div id="appealButton" class="col-12 col-md-6 offset-md-3">
<button type="button" class="btn btn-outline-primary btn-block font-weight-bold" onclick="requestAppeal()">REQUEST APPEAL</button>
</div>
<div id="appealForm" class="col-12 col-md-6 offset-md-3 d-none">
<form method="post" action="/i/warning">
@csrf
<p class="h4 font-weight-bold">Request Appeal</p>
<p class="pt-4">
<div class="form-group">
<textarea class="form-control" rows="4" placeholder="Write your appeal request message here" name="appeal_message"></textarea>
</div>
</p>
<input type="hidden" name="id" value="{{encrypt($interstitial->id)}}">
<input type="hidden" name="type" value="{{$interstitial->type}}">
<input type="hidden" name="action" value="appeal">
<button type="submit" class="btn btn-outline-primary btn-block font-weight-bold">REQUEST APPEAL</button>
</form>
</div>
<div class="col-12 col-md-6 offset-md-3 mt-4 mb-4">
<form method="post" action="/i/warning">
@csrf
<input type="hidden" name="id" value="{{encrypt($interstitial->id)}}">
<input type="hidden" name="type" value="{{$interstitial->type}}">
<input type="hidden" name="action" value="confirm">
<button type="submit" class="btn btn-primary btn-block font-weight-bold">I Understand</button>
</form>
</div>
</div>
</div>
@endsection
@push('scripts')
<script type="text/javascript">
function requestAppeal() {
$('#appealButton').addClass('d-none');
$('#appealForm').removeClass('d-none');
}
</script>
@if($interstitial->blurhash)
<script type="text/javascript">
const pixels = window.blurhash.decode("{{$interstitial->blurhash}}", 400, 400);
const canvas = document.getElementById("mblur");
const ctx = canvas.getContext("2d");
const imageData = ctx.createImageData(400, 400);
imageData.data.set(pixels);
ctx.putImageData(imageData, 0, 0);
</script>
@endif
@endpush

View file

@ -0,0 +1,63 @@
@extends('admin.partial.template-full')
@section('section')
<div class="title mb-3">
<h3 class="font-weight-bold d-inline-block">Appeals</h3>
<span class="float-right">
</span>
</div>
<div class="row">
<div class="col-12 col-md-3 mb-3">
<div class="card border bg-primary text-white rounded-pill shadow">
<div class="card-body pl-4 ml-3">
<p class="h1 font-weight-bold mb-1" style="font-weight: 700">{{App\AccountInterstitial::whereNull('appeal_handled_at')->whereNotNull('appeal_requested_at')->count()}}</p>
<p class="lead mb-0 font-weight-lighter">active appeals</p>
</div>
</div>
<div class="mt-3 card border bg-warning text-dark rounded-pill shadow">
<div class="card-body pl-4 ml-3">
<p class="h1 font-weight-bold mb-1" style="font-weight: 700">{{App\AccountInterstitial::whereNotNull('appeal_handled_at')->whereNotNull('appeal_requested_at')->count()}}</p>
<p class="lead mb-0 font-weight-lighter">closed appeals</p>
</div>
</div>
</div>
<div class="col-12 col-md-8 offset-md-1">
<ul class="list-group">
@if($appeals->count() == 0)
<li class="list-group-item text-center py-5">
<p class="mb-0 py-5 font-weight-bold">No appeals found!</p>
</li>
@endif
@foreach($appeals as $appeal)
<a class="list-group-item text-decoration-none text-dark" href="/i/admin/reports/appeal/{{$appeal->id}}">
<div class="d-flex justify-content-between align-items-center">
<div class="d-flex align-items-center">
<img src="{{$appeal->has_media ? $appeal->status->thumb(true) : '/storage/no-preview.png'}}" width="64" height="64" class="rounded border">
<div class="ml-2">
<span class="d-inline-block text-truncate">
<p class="mb-0 small font-weight-bold text-primary">{{$appeal->type}}</p>
@if($appeal->item_type)
<p class="mb-0 font-weight-bold">{{starts_with($appeal->item_type, 'App\\') ? explode('\\',$appeal->item_type)[1] : $appeal->item_type}}</p>
@endif
</span>
</div>
</div>
<div class="d-block">
<p class="mb-0 font-weight-bold">&commat;{{$appeal->user->username}}</p>
<p class="mb-0 small text-muted font-weight-bold">{{$appeal->created_at->diffForHumans(null, null, true)}}</p>
</div>
<div class="d-inline-block">
<p class="mb-0 small">
<i class="fas fa-chevron-right fa-2x text-lighter"></i>
</p>
</div>
</div>
</a>
@endforeach
</ul>
<p>{!!$appeals->render()!!}</p>
</div>
</div>
@endsection

View file

@ -15,6 +15,15 @@
</a>
</span>
</div>
@php($ai = App\AccountInterstitial::whereNotNull('appeal_requested_at')->whereNull('appeal_handled_at')->count())
@if($ai)
<div class="mb-4">
<a class="btn btn-outline-primary px-5 py-3" href="/i/admin/reports/appeals">
<p class="font-weight-bold h4 mb-0">{{$ai}}</p>
Appeal {{$ai == 1 ? 'Request' : 'Requests'}}
</a>
</div>
@endif
@if($reports->count())
<div class="card shadow-none border">
<div class="list-group list-group-flush">

View file

@ -0,0 +1,125 @@
@extends('admin.partial.template-full')
@section('section')
<div class="d-flex justify-content-between title mb-3">
<div>
<p class="font-weight-bold h3">Moderation Appeal</p>
<p class="text-muted mb-0 lead">From <a href="{{$appeal->user->url()}}" class="text-muted font-weight-bold">&commat;{{$appeal->user->username}}</a> about {{$appeal->appeal_requested_at->diffForHumans()}}.</p>
</div>
<div>
</div>
</div>
<div class="row">
<div class="col-12 col-md-8 mt-3">
@if($appeal->type == 'post.cw')
<div class="card shadow-none border">
<div class="card-header bg-light h5 font-weight-bold py-4">Content Warning applied to {{$appeal->has_media ? 'Post' : 'Comment'}}</div>
@if($appeal->has_media)
<img class="card-img-top border-bottom" src="{{$appeal->status->thumb(true)}}">
@endif
<div class="card-body">
<div class="mt-2 p-3">
@if($meta->caption)
<p class="text-break">
{{$appeal->has_media ? 'Caption' : 'Comment'}}: <span class="font-weight-bold">{{$meta->caption}}</span>
</p>
@endif
<p class="mb-0">
Like Count: <span class="font-weight-bold">{{$meta->likes_count}}</span>
</p>
<p class="mb-0">
Share Count: <span class="font-weight-bold">{{$meta->reblogs_count}}</span>
</p>
<p class="mb-0">
Timestamp: <span class="font-weight-bold">{{now()->parse($meta->created_at)->format('r')}}</span>
</p>
<p class="" style="word-break: break-all !important;">
URL: <span class="font-weight-bold text-primary"><a href="{{$meta->url}}">{{$meta->url}}</a></span>
</p>
<p class="mb-0">
Message: <span class="font-weight-bold">{{$appeal->appeal_message}}</span>
</p>
</div>
</div>
</div>
@elseif($appeal->type == 'post.unlist')
<div class="card shadow-none border">
<div class="card-header bg-light h5 font-weight-bold py-4">{{$appeal->has_media ? 'Post' : 'Comment'}} was unlisted from timelines</div>
@if($appeal->has_media)
<img class="card-img-top border-bottom" src="{{$appeal->status->thumb(true)}}">
@endif
<div class="card-body">
<div class="mt-2 p-3">
@if($meta->caption)
<p class="text-break">
{{$appeal->has_media ? 'Caption' : 'Comment'}}: <span class="font-weight-bold">{{$meta->caption}}</span>
</p>
@endif
<p class="mb-0">
Like Count: <span class="font-weight-bold">{{$meta->likes_count}}</span>
</p>
<p class="mb-0">
Share Count: <span class="font-weight-bold">{{$meta->reblogs_count}}</span>
</p>
<p class="mb-0">
Timestamp: <span class="font-weight-bold">{{now()->parse($meta->created_at)->format('r')}}</span>
</p>
<p class="" style="word-break: break-all !important;">
URL: <span class="font-weight-bold text-primary"><a href="{{$meta->url}}">{{$meta->url}}</a></span>
</p>
<p class="mb-0">
Message: <span class="font-weight-bold">{{$appeal->appeal_message}}</span>
</p>
</div>
</div>
</div>
@endif
</div>
<div class="col-12 col-md-4 mt-3">
<form method="post">
@csrf
<input type="hidden" name="action" value="dismiss">
<button type="submit" class="btn btn-primary btn-block font-weight-bold mb-3">Dismiss Appeal Request</button>
</form>
<button type="button" class="btn btn-light border btn-block font-weight-bold mb-3" onclick="approveWarning()">Approve Appeal</button>
<div class="card shadow-none border mt-5">
<div class="card-header text-center font-weight-bold bg-light">
&commat;{{$appeal->user->username}} stats
</div>
<div class="card-body">
<p class="">
Open Appeals: <span class="font-weight-bold">{{App\AccountInterstitial::whereUserId($appeal->user_id)->whereNotNull('appeal_requested_at')->whereNull('appeal_handled_at')->count()}}</span>
</p>
<p class="">
Total Appeals: <span class="font-weight-bold">{{App\AccountInterstitial::whereUserId($appeal->user_id)->whereNotNull('appeal_requested_at')->count()}}</span>
</p>
<p class="">
Total Warnings: <span class="font-weight-bold">{{App\AccountInterstitial::whereUserId($appeal->user_id)->count()}}</span>
</p>
<p class="">
Status Count: <span class="font-weight-bold">{{$appeal->user->statuses()->count()}}</span>
</p>
<p class="mb-0">
Joined: <span class="font-weight-bold">{{$appeal->user->created_at->diffForHumans(null, null, false)}}</span>
</p>
</div>
</div>
</div>
</div>
@endsection
@push('scripts')
<script type="text/javascript">
function approveWarning() {
if(window.confirm('Are you sure you want to approve this appeal?') == true) {
axios.post(window.location.href, {
action: 'approve'
}).then(res => {
window.location.href = '/i/admin/reports/appeals';
}).catch(err => {
swal('Oops!', 'An error occured, please try again later.', 'error');
});
}
}
</script>
@endpush

View file

@ -8,6 +8,5 @@
@push('scripts')
<script type="text/javascript" src="{{ mix('js/discover.js') }}"></script>
<script type="text/javascript" src="{{ mix('js/compose.js') }}"></script>
<script type="text/javascript">App.boot();</script>
@endpush

View file

@ -0,0 +1,35 @@
@extends('layouts.app',['title' => $profile->username . " on " . config('app.name')])
@section('content')
@if (session('error'))
<div class="alert alert-danger text-center font-weight-bold mb-0">
{{ session('error') }}
</div>
@endif
<memory-profile profile-id="{{$profile->id}}" profile-username="{{$profile->username}}" :profile-settings="{{json_encode($settings)}}" profile-layout="{{$profile->profile_layout ?? 'metro'}}"></memory-profile>
@if($profile->website)
<a class="d-none" href="{{$profile->website}}" rel="me">{{$profile->website}}</a>
@endif
<noscript>
<div class="container">
<p class="pt-5 text-center lead">Please enable javascript to view this content.</p>
</div>
</noscript>
@endsection
@push('meta')<meta property="og:description" content="{{$profile->bio}}">
@if(false == $settings['crawlable'] || $profile->remote_url)
<meta name="robots" content="noindex, nofollow">
@else <meta property="og:image" content="{{$profile->avatarUrl()}}">
<link href="{{$profile->permalink('.atom')}}" rel="alternate" title="{{$profile->username}} on Pixelfed" type="application/atom+xml">
<link href='{{$profile->permalink()}}' rel='alternate' type='application/activity+json'>
@endif
@endpush
@push('scripts')<script type="text/javascript" src="{{ mix('js/memoryprofile.js') }}"></script>
<script type="text/javascript" defer>App.boot();</script>
@endpush

View file

@ -8,6 +8,9 @@ Route::domain(config('pixelfed.domain.admin'))->prefix('i/admin')->group(functio
Route::get('reports/show/{id}', 'AdminController@showReport');
Route::post('reports/show/{id}', 'AdminController@updateReport');
Route::post('reports/bulk', 'AdminController@bulkUpdateReport');
Route::get('reports/appeals', 'AdminController@appeals');
Route::get('reports/appeal/{id}', 'AdminController@showAppeal');
Route::post('reports/appeal/{id}', 'AdminController@updateAppeal');
Route::redirect('statuses', '/statuses/list');
Route::get('statuses/list', 'AdminController@statuses')->name('admin.statuses');
Route::get('statuses/show/{id}', 'AdminController@showStatus');
@ -73,7 +76,7 @@ Route::domain(config('pixelfed.domain.admin'))->prefix('i/admin')->group(functio
Route::post('newsroom/create', 'AdminController@newsroomStore');
});
Route::domain(config('pixelfed.domain.app'))->middleware(['validemail', 'twofactor', 'localization'])->group(function () {
Route::domain(config('pixelfed.domain.app'))->middleware(['validemail', 'twofactor', 'localization','interstitial'])->group(function () {
Route::get('/', 'SiteController@home')->name('timeline.personal');
Route::post('/', 'StatusController@store');
@ -125,6 +128,7 @@ Route::domain(config('pixelfed.domain.app'))->middleware(['validemail', 'twofact
Route::get('discover/tag', 'DiscoverController@getHashtags');
Route::post('status/compose', 'InternalApiController@composePost')->middleware('throttle:maxPostsPerHour,60')->middleware('throttle:maxPostsPerDay,1440');
});
Route::group(['prefix' => 'pixelfed'], function() {
Route::group(['prefix' => 'v1'], function() {
Route::get('accounts/verify_credentials', 'ApiController@verifyCredentials');
@ -146,6 +150,7 @@ Route::domain(config('pixelfed.domain.app'))->middleware(['validemail', 'twofact
Route::get('timelines/home', 'PublicApiController@homeTimelineApi');
Route::get('newsroom/timeline', 'NewsroomController@timelineApi');
Route::post('newsroom/markasread', 'NewsroomController@markAsRead');
Route::get('favourites', 'Api\BaseApiController@accountLikes');
});
Route::group(['prefix' => 'v2'], function() {
@ -169,6 +174,7 @@ Route::domain(config('pixelfed.domain.app'))->middleware(['validemail', 'twofact
Route::get('discover/posts/places', 'DiscoverController@trendingPlaces');
});
});
Route::group(['prefix' => 'local'], function () {
// Route::get('accounts/verify_credentials', 'ApiController@verifyCredentials');
// Route::get('accounts/relationships', 'PublicApiController@relationships');
@ -295,6 +301,9 @@ Route::domain(config('pixelfed.domain.app'))->middleware(['validemail', 'twofact
Route::get('redirect', 'SiteController@redirectUrl');
Route::post('admin/media/block/add', 'MediaBlocklistController@add');
Route::post('admin/media/block/delete', 'MediaBlocklistController@delete');
Route::get('warning', 'AccountInterstitialController@get');
Route::post('warning', 'AccountInterstitialController@read');
});
Route::group(['prefix' => 'account'], function () {