浏览代码

Add CustomEmoji admin dashboard

Daniel Supernault 3 年之前
父节点
当前提交
efeaf427e1

+ 111 - 1
app/Http/Controllers/AdminController.php

@@ -14,7 +14,7 @@ use App\{
 	Story,
 	Story,
 	User
 	User
 };
 };
-use DB, Cache;
+use DB, Cache, Storage;
 use Carbon\Carbon;
 use Carbon\Carbon;
 use Illuminate\Http\Request;
 use Illuminate\Http\Request;
 use Illuminate\Support\Facades\Redis;
 use Illuminate\Support\Facades\Redis;
@@ -22,8 +22,10 @@ use App\Http\Controllers\Admin\{
 	AdminDiscoverController,
 	AdminDiscoverController,
 	AdminInstanceController,
 	AdminInstanceController,
 	AdminReportController,
 	AdminReportController,
+	// AdminGroupsController,
 	AdminMediaController,
 	AdminMediaController,
 	AdminSettingsController,
 	AdminSettingsController,
+	// AdminStorageController,
 	AdminSupportController,
 	AdminSupportController,
 	AdminUserController
 	AdminUserController
 };
 };
@@ -31,14 +33,17 @@ use Illuminate\Validation\Rule;
 use App\Services\AdminStatsService;
 use App\Services\AdminStatsService;
 use App\Services\StatusService;
 use App\Services\StatusService;
 use App\Services\StoryService;
 use App\Services\StoryService;
+use App\Models\CustomEmoji;
 
 
 class AdminController extends Controller
 class AdminController extends Controller
 {
 {
 	use AdminReportController, 
 	use AdminReportController, 
 	AdminDiscoverController,
 	AdminDiscoverController,
+	// AdminGroupsController,
 	AdminMediaController, 
 	AdminMediaController, 
 	AdminSettingsController, 
 	AdminSettingsController, 
 	AdminInstanceController,
 	AdminInstanceController,
+	// AdminStorageController,
 	AdminUserController;
 	AdminUserController;
 
 
 	public function __construct()
 	public function __construct()
@@ -343,4 +348,109 @@ class AdminController extends Controller
 		$stats = StoryService::adminStats();
 		$stats = StoryService::adminStats();
 		return view('admin.stories.home', compact('stories', 'stats'));
 		return view('admin.stories.home', compact('stories', 'stats'));
 	}
 	}
+
+	public function customEmojiHome(Request $request)
+	{
+		if(!config('federation.custom_emoji.enabled')) {
+			return view('admin.custom-emoji.not-enabled');
+		}
+		$this->validate($request, [
+			'sort' => 'sometimes|in:all,local,remote,duplicates,disabled'
+		]);
+
+		if($request->has('cc')) {
+			Cache::forget('pf:admin:custom_emoji:stats');
+			return redirect(route('admin.custom-emoji'));
+		}
+
+		$sort = $request->input('sort') ?? 'all';
+		$emojis = CustomEmoji::when($sort, function($query, $sort) {
+			if($sort == 'all') {
+				return $query->groupBy('shortcode')->latest();
+			} else if($sort == 'local') {
+				return $query->latest()->where('domain', '=', config('pixelfed.domain.app'));
+			} else if($sort == 'remote') {
+				return $query->latest()->where('domain', '!=', config('pixelfed.domain.app'));
+			} else if($sort == 'duplicates') {
+				return $query->latest()->groupBy('shortcode')->havingRaw('count(*) > 1');
+			} else if($sort == 'disabled') {
+				return $query->latest()->whereDisabled(true);
+			}
+		})->cursorPaginate(10);
+
+		$stats = Cache::remember('pf:admin:custom_emoji:stats', 43200, function() {
+			return [
+				'total' => CustomEmoji::count(),
+				'active' => CustomEmoji::whereDisabled(false)->count(),
+				'remote' => CustomEmoji::where('domain', '!=', config('pixelfed.domain.app'))->count(),
+				'duplicate' => CustomEmoji::groupBy('shortcode')->havingRaw('count(*) > 1')->count()
+			];
+		});
+
+		return view('admin.custom-emoji.home', compact('emojis', 'sort', 'stats'));
+	}
+
+	public function customEmojiToggleActive(Request $request, $id)
+	{
+		abort_unless(config('federation.custom_emoji.enabled'), 404);
+		$emoji = CustomEmoji::findOrFail($id);
+		$emoji->disabled = !$emoji->disabled;
+		$emoji->save();
+		$key = CustomEmoji::CACHE_KEY . str_replace(':', '', $emoji->shortcode);
+		Cache::forget($key);
+		return redirect()->back();
+	}
+
+	public function customEmojiAdd(Request $request)
+	{
+		abort_unless(config('federation.custom_emoji.enabled'), 404);
+		return view('admin.custom-emoji.add');
+	}
+
+	public function customEmojiStore(Request $request)
+	{
+		abort_unless(config('federation.custom_emoji.enabled'), 404);
+		$this->validate($request, [
+			'shortcode' => [
+				'required',
+				'min:3',
+				'max:80',
+				'starts_with::',
+				'ends_with::',
+				Rule::unique('custom_emoji')->where(function ($query) use($request) {
+					return $query->whereDomain(config('pixelfed.domain.app'))
+					->whereShortcode($request->input('shortcode'));
+				})
+			],
+			'emoji' => 'required|file|mimes:jpg,png|max:' . (config('federation.custom_emoji.max_size') / 1000)
+		]);
+
+		$emoji = new CustomEmoji;
+		$emoji->shortcode = $request->input('shortcode');
+		$emoji->domain = config('pixelfed.domain.app');
+		$emoji->save();
+
+		$fileName = $emoji->id . '.' . $request->emoji->extension();
+		$request->emoji->storeAs('public/emoji', $fileName);
+		$emoji->media_path = 'emoji/' . $fileName;
+		$emoji->save();
+		return redirect(route('admin.custom-emoji'));
+	}
+
+	public function customEmojiDelete(Request $request, $id)
+	{
+		abort_unless(config('federation.custom_emoji.enabled'), 404);
+		$emoji = CustomEmoji::findOrFail($id);
+		Storage::delete("public/{$emoji->media_path}");
+		$emoji->delete();
+		return redirect(route('admin.custom-emoji'));
+	}
+
+	public function customEmojiShowDuplicates(Request $request, $id)
+	{
+		abort_unless(config('federation.custom_emoji.enabled'), 404);
+		$emoji = CustomEmoji::orderBy('id')->whereDisabled(false)->whereShortcode($id)->firstOrFail();
+		$emojis = CustomEmoji::whereShortcode($id)->where('id', '!=', $emoji->id)->cursorPaginate(10);
+		return view('admin.custom-emoji.duplicates', compact('emoji', 'emojis'));
+	}
 }
 }

+ 1 - 1
app/Models/CustomEmoji.php

@@ -24,7 +24,7 @@ class CustomEmoji extends Model
 		->matchAll(self::SCAN_RE)
 		->matchAll(self::SCAN_RE)
 		->map(function($match) use($activitypub) {
 		->map(function($match) use($activitypub) {
 			$tag = Cache::remember(self::CACHE_KEY . $match, 14400, function() use($match) {
 			$tag = Cache::remember(self::CACHE_KEY . $match, 14400, function() use($match) {
-				return self::whereShortcode(':' . $match . ':')->first();
+				return self::orderBy('id')->whereDisabled(false)->whereShortcode(':' . $match . ':')->first();
 			});
 			});
 
 
 			if($tag) {
 			if($tag) {

+ 63 - 0
resources/views/admin/custom-emoji/add.blade.php

@@ -0,0 +1,63 @@
+@extends('admin.partial.template-full')
+
+@section('section')
+</div>
+<div class="header bg-primary pb-3 mt-n4">
+	<div class="container-fluid">
+		<div class="header-body">
+			<div class="row align-items-center py-4">
+				<div class="col-lg-6 col-7">
+					<p class="display-1 text-white d-inline-block mb-0">Add Custom Emoji</p>
+				</div>
+			</div>
+		</div>
+	</div>
+</div>
+<div class="container mt-5">
+	<div class="row justify-content-center">
+		<div class="col-12 col-md-6">
+		@if ($errors->any())
+			@foreach ($errors->all() as $error)
+			<div class="alert alert-danger py-2 {{$loop->last?'mb-4':'mb-2'}}">
+				<p class="mb-0"><i class="far fa-exclamation-triangle mr-2"></i> {{ $error }}</p>
+			</div>
+			@endforeach
+		@endif
+
+			<div class="card">
+				<div class="card-header font-weight-bold">
+					New Custom Emoji
+				</div>
+
+				<div class="card-body">
+					<form method="post" enctype="multipart/form-data">
+						@csrf
+						<div class="form-group">
+							<label for="shortcode" class="font-weight-light">Shortcode</label>
+							<input class="form-control" id="shortcode" name="shortcode" placeholder=":pixelfed:" required>
+							<p class="form-text small font-weight-bold">Must start and end with :</p>
+						</div>
+
+						<div class="form-group">
+							<label for="media" class="font-weight-light">Emoji Image</label>
+							<input type="file" class="form-control-file" id="media" name="emoji" required>
+							<p class="form-text font-weight-bold"><span class="small">Must be a <kbd>png</kbd> or <kbd>jpg</kbd> under</span> <span class="badge badge-info filesize" data-filesize="{{config('federation.custom_emoji.max_size')}}"></span></p>
+						</div>
+						<hr>
+						<button class="btn btn-primary btn-block">Add Emoji</button>
+					</form>
+				</div>
+			</div>
+		</div>
+	</div>
+</div>
+@endsection
+
+@push('scripts')
+<script type="text/javascript">
+	$('.filesize').each(function(el, i) {
+		let size = filesize($(i).data('filesize'));
+		i.innerText = size;
+	})
+</script>
+@endpush

+ 101 - 0
resources/views/admin/custom-emoji/duplicates.blade.php

@@ -0,0 +1,101 @@
+@extends('admin.partial.template-full')
+
+@section('section')
+</div>
+<div class="header bg-primary pb-3 mt-n4">
+	<div class="container-fluid">
+		<div class="header-body">
+			<div class="row align-items-center py-4">
+				<div class="col-lg-6 col-7">
+					<p class="display-1 text-white d-inline-block mb-1">Custom Emoji</p>
+					<p class="h1 text-white font-weight-light d-inline-block mb-0">Showing duplicates of {{$emoji->shortcode}}</p>
+				</div>
+			</div>
+		</div>
+	</div>
+</div>
+<div class="container mt-5">
+	<div class="row justify-content-center">
+		<div class="col-12 col-md-6">
+			<div class="alert alert-warning py-2 mb-4">
+				<p class="mb-0">
+					<i class="far fa-exclamation-triangle mr-2"></i> Duplicate emoji shortcodes can lead to unpredictible results
+				</p>
+				<p class="mb-0 small">If you change the primary/in-use emoji, you will need to clear the cache by running the <strong>php artisan cache:clear</strong> command for the changes to take effect immediately.</p>
+			</div>
+
+			<p class="font-weight-bold">In Use</p>
+			<div class="list-group">
+				<div class="list-group-item">
+					<div class="media align-items-center">
+						<img src="{{url('storage/' . $emoji->media_path)}}" width="40" height="40" class="mr-3">
+
+						<div class="media-body">
+							<p class="font-weight-bold mb-0">{{ $emoji->shortcode }}</p>
+							<p class="text-muted small mb-0">{{ $emoji->domain }}</p>
+						</div>
+
+						<div class="ml-3 badge badge-info">Added {{$emoji->created_at->diffForHumans(null, true, true)}}</div>
+
+						<form
+							class="form-inline"
+							action="/i/admin/custom-emoji/toggle-active/{{$emoji->id}}"
+							method="post">
+							@csrf
+							<button
+								type="submit"
+								class="ml-3 btn btn-sm {{$emoji->disabled ? 'btn-danger' : 'btn-success'}}">
+								{{$emoji->disabled ? 'Disabled' : 'Active' }}
+							</button>
+						</form>
+
+						<button class="btn btn-danger px-2 py-1 ml-3 delete-emoji" data-id="{{$emoji->id}}">
+							<i class="far fa-trash-alt"></i>
+						</button>
+
+					</div>
+				</div>
+			</div>
+			<hr>
+			<p class="font-weight-bold">Not used (due to conflicting shortcode)</p>
+			<div class="list-group">
+				@foreach($emojis as $emoji)
+				<div class="list-group-item">
+					<div class="media align-items-center">
+						<img src="{{url('storage/' . $emoji->media_path)}}" width="40" height="40" class="mr-3">
+
+						<div class="media-body">
+							<p class="font-weight-bold mb-0">{{ $emoji->shortcode }}</p>
+							<p class="text-muted small mb-0">{{ $emoji->domain }}</p>
+						</div>
+
+						<div class="ml-3 badge badge-info">Added {{$emoji->created_at->diffForHumans(null, true, true)}}</div>
+
+						<form
+							class="form-inline"
+							action="/i/admin/custom-emoji/toggle-active/{{$emoji->id}}"
+							method="post">
+							@csrf
+							<button
+								type="submit"
+								class="ml-3 btn btn-sm {{$emoji->disabled ? 'btn-danger' : 'btn-success'}}">
+								{{$emoji->disabled ? 'Disabled' : 'Active' }}
+							</button>
+						</form>
+
+						<button class="btn btn-danger px-2 py-1 ml-3 delete-emoji" data-id="{{$emoji->id}}">
+							<i class="far fa-trash-alt"></i>
+						</button>
+
+					</div>
+				</div>
+				@endforeach
+			</div>
+
+			<div class="d-flex justify-content-center mt-3">
+				{{ $emojis->links() }}
+			</div>
+		</div>
+	</div>
+</div>
+@endsection

+ 155 - 0
resources/views/admin/custom-emoji/home.blade.php

@@ -0,0 +1,155 @@
+@extends('admin.partial.template-full')
+
+@section('section')
+</div>
+<div class="header bg-primary pb-3 mt-n4">
+	<div class="container-fluid">
+		<div class="header-body">
+			<div class="row align-items-center py-4">
+				<div class="col-lg-6 col-7">
+					<p class="display-1 text-white d-inline-block mb-0">Custom Emoji</p>
+				</div>
+			</div>
+			<div class="row">
+				<div class="col-xl-2 col-md-6">
+					<div class="mb-3">
+						<h5 class="text-light text-uppercase mb-0">Total Emoji</h5>
+						<span class="text-white h2 font-weight-bold mb-0 human-size">{{$stats['total']}}</span>
+					</div>
+				</div>
+				<div class="col-xl-2 col-md-6">
+					<div class="mb-3">
+						<h5 class="text-light text-uppercase mb-0">Total Active</h5>
+						<span class="text-white h2 font-weight-bold mb-0 human-size">{{$stats['active']}}</span>
+					</div>
+				</div>
+				<div class="col-xl-2 col-md-6">
+					<div class="mb-3">
+						<h5 class="text-light text-uppercase mb-0">Remote Emoji</h5>
+						<span class="text-white h2 font-weight-bold mb-0 human-size">{{$stats['remote']}}</span>
+					</div>
+				</div>
+				<div class="col-xl-2 col-md-6">
+					<div class="mb-3">
+						<h5 class="text-light text-uppercase mb-0">Duplicate Emoji</h5>
+						<span class="text-white h2 font-weight-bold mb-0 human-size">{{$stats['duplicate']}}</span>
+					</div>
+				</div>
+				<div class="col-xl-4 col-md-6">
+					<a
+						class="btn btn-dark btn-lg px-3"
+						href="/i/admin/custom-emoji/new">
+						<i class="far fa-plus mr-1"></i>
+						Add Custom Emoji
+					</a>
+				</div>
+			</div>
+			<div class="row">
+				<div class="col-12 mt-2">
+					<p class="font-weight-light text-white small mb-0">
+						Stats are cached for 12 hours and may not reflect the latest data.<br /> To refresh the cache and view the most recent data, <a href="/i/admin/custom-emoji/home?cc=1" class="font-weight-bold text-white">click here</a>.
+					</p>
+				</div>
+			</div>
+		</div>
+	</div>
+</div>
+<div class="container mt-5">
+	<div class="row justify-content-center">
+		<div class="col-12 col-md-6">
+
+			<ul class="nav nav-pills mb-3 nav-fill">
+				<li class="nav-item">
+					<a class="nav-link {{$sort=='all'?'active':''}}" href="?sort=all">All</a>
+				</li>
+
+				<li class="nav-item">
+					<a class="nav-link {{$sort=='local'?'active':''}}" href="?sort=local">Local</a>
+				</li>
+
+				<li class="nav-item">
+					<a class="nav-link {{$sort=='remote'?'active':''}}" href="?sort=remote">Remote</a>
+				</li>
+
+				<li class="nav-item">
+					<a class="nav-link {{$sort=='duplicates'?'active':''}}" href="?sort=duplicates">Duplicates</a>
+				</li>
+
+				<li class="nav-item">
+					<a class="nav-link {{$sort=='disabled'?'active':''}}" href="?sort=disabled">Disabled</a>
+				</li>
+			</ul>
+
+			@if($sort == 'duplicates')
+			<div class="alert alert-warning py-2 mt-4">
+				<p class="mb-0">
+					<i class="far fa-exclamation-triangle mr-2"></i> Duplicate emoji shortcodes can lead to unpredictible results
+				</p>
+			</div>
+			@endif
+
+			<div class="list-group">
+				@foreach($emojis as $emoji)
+				<div class="list-group-item">
+					<div class="media align-items-center">
+						<img src="{{url('storage/' . $emoji->media_path)}}" width="40" height="40" class="mr-3">
+
+						<div class="media-body">
+							<p class="font-weight-bold mb-0">{{ $emoji->shortcode }}</p>
+							<p class="text-muted small mb-0">{{ $emoji->domain }}</p>
+						</div>
+
+					@if($sort == 'duplicates')
+						<a
+							class="btn btn-primary rounded-pill btn-sm px-2 py-1 ml-3"
+							href="/i/admin/custom-emoji/duplicates/{{$emoji->shortcode}}">
+							View duplicates
+						</a>
+						{{-- <div class="ml-3 badge badge-info">Updated {{$emoji->updated_at->diffForHumans(null, true, true)}}</div> --}}
+					@else
+						<div class="ml-3 badge badge-info">Updated {{$emoji->updated_at->diffForHumans(null, true, true)}}</div>
+
+						<form
+							class="form-inline"
+							action="/i/admin/custom-emoji/toggle-active/{{$emoji->id}}"
+							method="post">
+							@csrf
+							<button
+								type="submit"
+								class="ml-3 btn btn-sm {{$emoji->disabled ? 'btn-danger' : 'btn-success'}}">
+								{{$emoji->disabled ? 'Disabled' : 'Active' }}
+							</button>
+						</form>
+
+						<button class="btn btn-danger px-2 py-1 ml-3 delete-emoji" data-id="{{$emoji->id}}">
+							<i class="far fa-trash-alt"></i>
+						</button>
+					@endif
+
+					</div>
+				</div>
+				@endforeach
+			</div>
+
+			<div class="d-flex justify-content-center mt-3">
+				{{ $emojis->links() }}
+			</div>
+		</div>
+	</div>
+</div>
+@endsection
+
+@push('scripts')
+<script type="text/javascript">
+	$('.delete-emoji').click(function(i) {
+		if(!window.confirm('Are you sure you want to delete this custom emoji?')) {
+			return;
+		}
+		let id = i.currentTarget.getAttribute('data-id');
+		axios.post('/i/admin/custom-emoji/delete/' + id)
+		.then(res => {
+			$(i.currentTarget).closest('.list-group-item').remove();
+		})
+	});
+</script>
+@endpush

+ 24 - 0
resources/views/admin/custom-emoji/not-enabled.blade.php

@@ -0,0 +1,24 @@
+@extends('admin.partial.template-full')
+
+@section('section')
+</div>
+<div class="header bg-primary pb-3 mt-n4">
+	<div class="container-fluid">
+		<div class="header-body">
+			<div class="row align-items-center py-4">
+				<div class="col-lg-6 col-7">
+					<p class="display-1 text-white d-inline-block mb-0">Custom Emoji</p>
+				</div>
+			</div>
+		</div>
+	</div>
+</div>
+<div class="container mt-5">
+	<div class="row justify-content-center">
+		<div class="col-12 col-md-6">
+			<h1 class="text-center">This feature is not enabled</h1>
+			<p class="text-center">To enable this feature, set <code>CUSTOM_EMOJI=true</code> in<br /> your .env file and run <code>php artisan config:cache</code></p>
+		</div>
+	</div>
+</div>
+@endsection

+ 9 - 2
resources/views/admin/partial/sidenav.blade.php

@@ -1,7 +1,7 @@
 <nav class="sidenav navbar navbar-vertical fixed-left navbar-expand-xs navbar-light bg-white" id="sidenav-main">
 <nav class="sidenav navbar navbar-vertical fixed-left navbar-expand-xs navbar-light bg-white" id="sidenav-main">
 	<div class="scrollbar-inner">
 	<div class="scrollbar-inner">
 		<div class="sidenav-header  align-items-center">
 		<div class="sidenav-header  align-items-center">
-			<a class="navbar-brand" href="/">
+			<a class="navbar-brand" href="/i/web">
 				<img src="/img/pixelfed-icon-color.svg" class="navbar-brand-img">
 				<img src="/img/pixelfed-icon-color.svg" class="navbar-brand-img">
 			</a>
 			</a>
 		</div>
 		</div>
@@ -69,6 +69,13 @@
 						</a>
 						</a>
 					</li>
 					</li>
 
 
+					<li class="nav-item">
+						<a class="nav-link {{request()->is('*custom-emoji*')?'active':''}}" href="{{route('admin.custom-emoji')}}">
+							<i class="ni ni-bold-right text-primary"></i>
+							<span class="nav-link-text">Custom Emoji <span class="badge badge-primary ml-1">NEW</span></span>
+						</a>
+					</li>
+
 					<li class="nav-item">
 					<li class="nav-item">
 						<a class="nav-link {{request()->is('*diagnostics*')?'active':''}}" href="{{route('admin.diagnostics')}}">
 						<a class="nav-link {{request()->is('*diagnostics*')?'active':''}}" href="{{route('admin.diagnostics')}}">
 							<i class="ni ni-bold-right text-primary"></i>
 							<i class="ni ni-bold-right text-primary"></i>
@@ -119,7 +126,7 @@
 					</li>
 					</li>
 
 
 					<li class="nav-item">
 					<li class="nav-item">
-						<a class="nav-link {{request()->is('*settings/pages')?'active':''}}" href="/i/admin/settings/pages">
+						<a class="nav-link {{request()->is('*settings/pages*')?'active':''}}" href="/i/admin/settings/pages">
 							<i class="ni ni-bold-right text-primary"></i>
 							<i class="ni ni-bold-right text-primary"></i>
 							<span class="nav-link-text">Pages</span>
 							<span class="nav-link-text">Pages</span>
 						</a>
 						</a>

+ 6 - 0
routes/web.php

@@ -84,6 +84,12 @@ Route::domain(config('pixelfed.domain.admin'))->prefix('i/admin')->group(functio
 
 
 	Route::get('diagnostics/home', 'AdminController@diagnosticsHome')->name('admin.diagnostics');
 	Route::get('diagnostics/home', 'AdminController@diagnosticsHome')->name('admin.diagnostics');
 	Route::post('diagnostics/decrypt', 'AdminController@diagnosticsDecrypt')->name('admin.diagnostics.decrypt');
 	Route::post('diagnostics/decrypt', 'AdminController@diagnosticsDecrypt')->name('admin.diagnostics.decrypt');
+	Route::get('custom-emoji/home', 'AdminController@customEmojiHome')->name('admin.custom-emoji');
+	Route::post('custom-emoji/toggle-active/{id}', 'AdminController@customEmojiToggleActive');
+	Route::get('custom-emoji/new', 'AdminController@customEmojiAdd');
+	Route::post('custom-emoji/new', 'AdminController@customEmojiStore');
+	Route::post('custom-emoji/delete/{id}', 'AdminController@customEmojiDelete');
+	Route::get('custom-emoji/duplicates/{id}', 'AdminController@customEmojiShowDuplicates');
 });
 });
 
 
 Route::domain(config('pixelfed.domain.app'))->middleware(['validemail', 'twofactor', 'localization'])->group(function () {
 Route::domain(config('pixelfed.domain.app'))->middleware(['validemail', 'twofactor', 'localization'])->group(function () {