diff --git a/CHANGELOG.md b/CHANGELOG.md index e5f135e11..d4032e332 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -5,6 +5,7 @@ ### Added - Implement Admin Domain Blocks API (Mastodon API Compatible) [ThisIsMissEm](https://github.com/ThisIsMissEm) ([#5021](https://github.com/pixelfed/pixelfed/pull/5021)) - Authorize Interaction support (for handling remote interactions) ([4ca7c6c3](https://github.com/pixelfed/pixelfed/commit/4ca7c6c3)) +- Contact Form Admin Responses ([52cc6090](https://github.com/pixelfed/pixelfed/commit/52cc6090)) ### Federation - Add ActiveSharedInboxService, for efficient sharedInbox caching ([1a6a3397](https://github.com/pixelfed/pixelfed/commit/1a6a3397)) @@ -31,7 +32,7 @@ - Update layout, add og:logo ([4cc576e1](https://github.com/pixelfed/pixelfed/commit/4cc576e1)) - Update ReblogService, fix cache sync issues ([3de8ceca](https://github.com/pixelfed/pixelfed/commit/3de8ceca)) - Update config, allow Beagle discover service to be disabled ([de4ce3c8](https://github.com/pixelfed/pixelfed/commit/de4ce3c8)) -- ([](https://github.com/pixelfed/pixelfed/commit/)) +- Update ApiV1Dot1Controller, allow upto 5 similar push tokens ([7820b506](https://github.com/pixelfed/pixelfed/commit/7820b506)) - ([](https://github.com/pixelfed/pixelfed/commit/)) ## [v0.12.3 (2024-07-01)](https://github.com/pixelfed/pixelfed/compare/v0.12.2...v0.12.3) diff --git a/app/Contact.php b/app/Contact.php index 4d9bc56e8..2239af7d8 100644 --- a/app/Contact.php +++ b/app/Contact.php @@ -3,16 +3,31 @@ namespace App; use Illuminate\Database\Eloquent\Model; +use Illuminate\Support\Str; class Contact extends Model { + protected $casts = [ + 'responded_at' => 'datetime', + ]; + public function user() { - return $this->belongsTo(User::class); + return $this->belongsTo(User::class); } public function adminUrl() { - return url('/i/admin/messages/show/' . $this->id); + return url('/i/admin/messages/show/'.$this->id); + } + + public function userResponseUrl() + { + return url('/i/contact-admin-response/'.$this->id); + } + + public function getMessageId() + { + return $this->id.'-'.(string) Str::uuid().'@'.strtolower(config('pixelfed.domain.app', 'example.org')); } } diff --git a/app/Http/Controllers/AdminController.php b/app/Http/Controllers/AdminController.php index 3e292037c..b769f3c8e 100644 --- a/app/Http/Controllers/AdminController.php +++ b/app/Http/Controllers/AdminController.php @@ -13,6 +13,7 @@ use App\Http\Controllers\Admin\AdminReportController; use App\Http\Controllers\Admin\AdminSettingsController; use App\Http\Controllers\Admin\AdminUserController; use App\Instance; +use App\Mail\AdminMessageResponse; use App\Models\CustomEmoji; use App\Newsroom; use App\OauthClient; @@ -29,6 +30,7 @@ use Cache; use DB; use Illuminate\Http\Request; use Illuminate\Validation\Rule; +use Mail; use Storage; class AdminController extends Controller @@ -221,18 +223,86 @@ class AdminController extends Controller public function messagesHome(Request $request) { - $messages = Contact::orderByDesc('id')->paginate(10); + $this->validate($request, [ + 'sort' => 'sometimes|string|in:all,open,closed', + ]); + $sort = $request->input('sort', 'open'); - return view('admin.messages.home', compact('messages')); + $messages = Contact::when($sort, function ($query, $sort) { + if ($sort === 'open') { + $query->whereNull('read_at'); + } + if ($sort === 'closed') { + $query->whereNotNull('read_at'); + } + }) + ->orderByDesc('id') + ->paginate(10) + ->withQueryString(); + + return view('admin.messages.home', compact('messages', 'sort')); } public function messagesShow(Request $request, $id) { $message = Contact::findOrFail($id); + $user = User::whereNull('status')->find($message->user_id); + if(!$user) { + $message->read_at = now(); + $message->save(); + return redirect('/i/admin/messages/home')->with('status', 'Redirected from message sent from a deleted account'); + } return view('admin.messages.show', compact('message')); } + public function messagesReply(Request $request, $id) + { + $this->validate($request, [ + 'message' => 'required|string|min:1|max:500', + ]); + + if(config('mail.default') === 'log') { + return redirect('/i/admin/messages/home')->with('error', 'Mail driver not configured, please setup before you can sent email.'); + } + + $message = Contact::whereNull('responded_at')->findOrFail($id); + $user = User::whereNull('status')->find($message->user_id); + if(!$user) { + $message->read_at = now(); + $message->save(); + return redirect('/i/admin/messages/home')->with('status', 'Redirected from message sent from a deleted account'); + } + $message->response = $request->input('message'); + $message->read_at = now(); + $message->responded_at = now(); + $message->save(); + + Mail::to($message->user->email)->send(new AdminMessageResponse($message)); + + return redirect('/i/admin/messages/home')->with('status', 'Sent response to '.$message->user->username); + } + + public function messagesReplyPreview(Request $request, $id) + { + $this->validate($request, [ + 'message' => 'required|string|min:1|max:500', + ]); + + if(config('mail.default') === 'log') { + return redirect('/i/admin/messages/home')->with('error', 'Mail driver not configured, please setup before you can sent email.'); + } + + $message = Contact::whereNull('read_at')->findOrFail($id); + $user = User::whereNull('status')->find($message->user_id); + if(!$user) { + $message->read_at = now(); + $message->save(); + return redirect('/i/admin/messages/home')->with('error', 'Redirected from message sent from a deleted account'); + } + return new AdminMessageResponse($message); + } + public function messagesMarkRead(Request $request) { $this->validate($request, [ @@ -240,12 +310,21 @@ class AdminController extends Controller ]); $id = $request->input('id'); $message = Contact::findOrFail($id); + + $user = User::whereNull('status')->find($message->user_id); + if(!$user) { + $message->read_at = now(); + $message->save(); + return redirect('/i/admin/messages/home')->with('error', 'Redirected from message sent from a deleted account'); + } if ($message->read_at) { return; } $message->read_at = now(); $message->save(); + $request->session()->flash('status', 'Marked response from '.$message->user->username.' as read!'); + return ['status' => 200]; } public function newsroomHome(Request $request) @@ -355,7 +434,7 @@ class AdminController extends Controller if (Newsroom::whereSlug($slug)->exists()) { $slug = $slug.'-'.str_random(4); } - $news = new Newsroom(); + $news = new Newsroom; $fields = [ 'title' => 'string', 'summary' => 'string', diff --git a/app/Http/Controllers/ContactController.php b/app/Http/Controllers/ContactController.php index 3123b8d16..dbba9599a 100644 --- a/app/Http/Controllers/ContactController.php +++ b/app/Http/Controllers/ContactController.php @@ -50,4 +50,15 @@ class ContactController extends Controller return redirect()->back()->with('status', 'Success - Your message has been sent to admins.'); } + + public function showAdminResponse(Request $request, $id) + { + abort_if(!$request->user(), 404); + $uid = $request->user()->id; + $contact = Contact::whereUserId($uid) + ->whereNotNull('response') + ->whereNotNull('responded_at') + ->findOrFail($id); + return view('site.contact.admin-response', compact('contact')); + } } diff --git a/app/Mail/AdminMessageResponse.php b/app/Mail/AdminMessageResponse.php new file mode 100644 index 000000000..a42dfa14d --- /dev/null +++ b/app/Mail/AdminMessageResponse.php @@ -0,0 +1,71 @@ +contact->getMessageId(); + + return new Headers( + messageId: $mid, + text: [ + 'X-Entity-Ref-ID' => $mid, + ], + ); + } + + /** + * Get the message envelope. + */ + public function envelope(): Envelope + { + return new Envelope( + subject: ucfirst(strtolower(config('pixelfed.domain.app'))).' Contact Form Response [Ticket #'.$this->contact->id.']', + ); + } + + /** + * Get the message content definition. + */ + public function content(): Content + { + return new Content( + markdown: 'emails.contact.admin-response', + with: [ + 'url' => $this->contact->userResponseUrl(), + ], + ); + } + + /** + * Get the attachments for the message. + * + * @return array + */ + public function attachments(): array + { + return []; + } +} diff --git a/composer.json b/composer.json index 29fd22327..26b975f41 100644 --- a/composer.json +++ b/composer.json @@ -37,6 +37,7 @@ "pragmarx/google2fa": "^8.0", "predis/predis": "^2.0", "pusher/pusher-php-server": "^7.2", + "resend/resend-php": "^0.13.0", "spatie/laravel-backup": "^8.0.0", "spatie/laravel-image-optimizer": "^1.8.0", "stevebauman/purify": "^6.2.0", diff --git a/composer.lock b/composer.lock index ce0f62013..a45ba803c 100644 --- a/composer.lock +++ b/composer.lock @@ -4,7 +4,7 @@ "Read more about it at https://getcomposer.org/doc/01-basic-usage.md#installing-dependencies", "This file is @generated automatically" ], - "content-hash": "fecc0efcc40880a422690feefedef584", + "content-hash": "0035325cb0240e92fc378e49f76447bd", "packages": [ { "name": "aws/aws-crt-php", @@ -6378,6 +6378,63 @@ ], "time": "2024-04-27T21:32:50+00:00" }, + { + "name": "resend/resend-php", + "version": "v0.13.0", + "source": { + "type": "git", + "url": "https://github.com/resend/resend-php.git", + "reference": "c74926e34472fe3e3e21f150f3e3ce56fcbf8298" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/resend/resend-php/zipball/c74926e34472fe3e3e21f150f3e3ce56fcbf8298", + "reference": "c74926e34472fe3e3e21f150f3e3ce56fcbf8298", + "shasum": "" + }, + "require": { + "guzzlehttp/guzzle": "^7.5", + "php": "^8.1.0" + }, + "require-dev": { + "friendsofphp/php-cs-fixer": "^3.13", + "mockery/mockery": "^1.6", + "pestphp/pest": "^2.0" + }, + "type": "library", + "autoload": { + "files": [ + "src/Resend.php" + ], + "psr-4": { + "Resend\\": "src/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Resend and contributors", + "homepage": "https://github.com/resend/resend-php/contributors" + } + ], + "description": "Resend PHP library.", + "homepage": "https://resend.com/", + "keywords": [ + "api", + "client", + "php", + "resend", + "sdk" + ], + "support": { + "issues": "https://github.com/resend/resend-php/issues", + "source": "https://github.com/resend/resend-php/tree/v0.13.0" + }, + "time": "2024-08-15T03:27:29+00:00" + }, { "name": "spatie/db-dumper", "version": "3.7.0", diff --git a/config/mail.php b/config/mail.php index 1cfb51767..423f61736 100644 --- a/config/mail.php +++ b/config/mail.php @@ -37,8 +37,8 @@ return [ 'smtp' => [ 'transport' => 'smtp', 'url' => env('MAIL_URL'), - 'host' => env('MAIL_HOST', 'smtp.mailgun.org'), - 'port' => env('MAIL_PORT', 587), + 'host' => env('MAIL_HOST', '127.0.0.1'), + 'port' => env('MAIL_PORT', 2525), 'encryption' => env('MAIL_ENCRYPTION', 'tls'), 'username' => env('MAIL_USERNAME'), 'password' => env('MAIL_PASSWORD'), @@ -53,16 +53,21 @@ return [ 'mailgun' => [ 'transport' => 'mailgun', - // 'client' => [ - // 'timeout' => 5, - // ], + 'client' => [ + 'timeout' => 5, + ], ], 'postmark' => [ 'transport' => 'postmark', - // 'client' => [ - // 'timeout' => 5, - // ], + 'message_stream_id' => env('POSTMARK_MESSAGE_STREAM_ID'), + 'client' => [ + 'timeout' => 5, + ], + ], + + 'resend' => [ + 'transport' => 'resend', ], 'sendmail' => [ @@ -82,7 +87,6 @@ return [ 'failover' => [ 'transport' => 'failover', 'mailers' => [ - 'smtp', 'log', ], ], diff --git a/config/services.php b/config/services.php index 8f00697b5..3f73b64aa 100644 --- a/config/services.php +++ b/config/services.php @@ -20,7 +20,7 @@ return [ ], 'ses' => [ - 'key' => env('SES_KEY'), + 'key' => env('SES_KEY'), 'secret' => env('SES_SECRET'), 'region' => env('SES_REGION', 'us-east-1'), ], @@ -30,12 +30,16 @@ return [ ], 'stripe' => [ - 'model' => App\User::class, - 'key' => env('STRIPE_KEY'), + 'model' => App\User::class, + 'key' => env('STRIPE_KEY'), 'secret' => env('STRIPE_SECRET'), ], 'expo' => [ 'access_token' => env('EXPO_ACCESS_TOKEN'), ], + + 'resend' => [ + 'key' => env('RESEND_KEY'), + ], ]; diff --git a/resources/views/admin/messages/home.blade.php b/resources/views/admin/messages/home.blade.php index ee0b86e6e..1a9c2c3bc 100644 --- a/resources/views/admin/messages/home.blade.php +++ b/resources/views/admin/messages/home.blade.php @@ -1,36 +1,101 @@ @extends('admin.partial.template-full') @section('section') -
-

Messages

+
+
+
+
+
+

Messages

+
+
+
+
+
+
-
-
- - - - - - - - - - - @foreach($messages as $msg) - - - - - - - @endforeach - -
#UserMessageCreated
- - {{$msg->id}} - - {{$msg->user->username}}{{str_limit($msg->message, 40)}}{{$msg->created_at->diffForHumans()}}
+
+ @if (session('status')) +
+
+ {{ session('status') }} +
+
+ @endif + @if (session('error')) +
+
+ {{ session('error') }} +
+
+ @endif +
+ +
+
+
+ + + + + + + + + + + @foreach($messages as $msg) + + + + + + + @endforeach + +
#UserMessageCreated
+ + {{$msg->id}} + + {{$msg->user->username}}{{str_limit($msg->message, 40)}}{{$msg->created_at->diffForHumans()}}
+
+
+ {{$messages->links()}} +
+
-{{$messages->links()}} -@endsection \ No newline at end of file +@endsection + +@push('scripts') + +@endpush diff --git a/resources/views/admin/messages/show.blade.php b/resources/views/admin/messages/show.blade.php index a86372e15..93d6775e2 100644 --- a/resources/views/admin/messages/show.blade.php +++ b/resources/views/admin/messages/show.blade.php @@ -4,56 +4,161 @@
# {{$message->id}}
-
Message
-
- @if($message->read_at) - Read - @else - - @endif -
+
Contact Form Message
+
-
+
-
- -
- @if($message->response_requested) -

Response Requested

- @endif -

Sent {{$message->created_at->diffForHumans()}}

+
+
+
+
+ @if($message->responded_at) +
+
+
Admin Response Sent
+
+ + {{$message->responded_at->diffForHumans()}} + +
+
+
+ @endif + +
+
+
Status
+ @if($message->read_at == null) +
Open
+ @else +
Closed
+ @endif +
+
+
+
+
Response Requested
+ @if($message->response_requested == 1) +
Yes
+ @else +
No
+ @endif +
+
+ +
+
+
Created
+
+ + {{$message->created_at->diffForHumans()}} + +
+
+
+ + @if($message->user && $message->user->last_active_at) +
+
+
User Last Active
+
+ + {{$message->user->last_active_at->diffForHumans()}} + +
+
+
+ @endif + + @if(!$message->read_at) +
+ +
+ @endif +
+
-
- -
-
-
- -
-
@{{$message->user->username}}
- {{$message->user->email}} + +
+
+
+
+
+
+ +
+
@{{$message->user->username}}
+ {{$message->user->email}} +
+
-
-
-
-

{{$message->message}}

+
+

Message Body

+

{{$message->message}}

+ +
+

Admin Reply:

+ + @if($message->responded_at) +

{{$message->response}}

+ @else + @if(config('mail.default') === 'log') +
+

You need to configure your mail driver before you can send outgoing emails.

+
+ @else +
+ @csrf +
+ + @if ($errors->any()) + @foreach ($errors->all() as $error) +

+ {{ $error }} +

+ @endforeach + @endif +
+
+
+ + +
+ +
+ + 0/500 + +
+
+
+ @endif + @endif +
+
-
- {{-- @if($message->responded_at == null) - -
- @endif - --}} -
- @endsection @push('scripts') +@if($message->responded_at == null) -@endpush \ No newline at end of file +@endif +@endpush diff --git a/resources/views/emails/contact/admin-response.blade.php b/resources/views/emails/contact/admin-response.blade.php new file mode 100644 index 000000000..d04d3218a --- /dev/null +++ b/resources/views/emails/contact/admin-response.blade.php @@ -0,0 +1,24 @@ + +Hello **@{{$contact->user->username}}**, + +You contacted the admin team of {{config('pixelfed.domain.app')}} with the following inquiry: + + +{{str_limit($contact->message, 80)}} + + + + View Admin Response + + + +or copy and paste the following url: {{$url}} + +
+
+
+ +Thanks,
+The {{ ucfirst(config('pixelfed.domain.app')) }} Admin Team +
+
diff --git a/resources/views/site/contact/admin-response.blade.php b/resources/views/site/contact/admin-response.blade.php new file mode 100644 index 000000000..7006d17e7 --- /dev/null +++ b/resources/views/site/contact/admin-response.blade.php @@ -0,0 +1,55 @@ +@extends('layouts.blank') + +@section('content') +
+
+
+
+
+ Back to Pixelfed +

Contact Form Response

+

Ticket ID #{{$contact->id}}

+
+ +
+
+
+ +
+
@{{$contact->user->username}}
+ {{$contact->user->name}} +
+
+

You sent the following inquiry:

+
+
+ {{ $contact->message }} +
+
+

You sent this inquiry on {{$contact->created_at->format('M d, Y')}} at {{$contact->created_at->format('h:i:s a e')}}

+
+ + @if($contact->response) +
+

The admin(s) responded to your inquiry:

+
+
+ {{ $contact->response }} +
+
+ @if($contact->responded_at) +

The response was created on {{$contact->responded_at->format('M d, Y')}} at {{$contact->responded_at->format('h:i:s a e')}}

+ @endif +
+
+
+

If you would like to respond, use the contact form.

+
+
+ @endif +
+
+
+
+
+@endsection diff --git a/routes/web-admin.php b/routes/web-admin.php index bb206aaaa..3d5b1cfd9 100644 --- a/routes/web-admin.php +++ b/routes/web-admin.php @@ -77,6 +77,8 @@ Route::domain(config('pixelfed.domain.admin'))->prefix('i/admin')->group(functio Route::get('messages/home', 'AdminController@messagesHome')->name('admin.messages'); Route::get('messages/show/{id}', 'AdminController@messagesShow'); Route::post('messages/mark-read', 'AdminController@messagesMarkRead'); + Route::post('messages/show/{id}', 'AdminController@messagesReply'); + Route::post('messages/preview/{id}', 'AdminController@messagesReplyPreview'); Route::redirect('site-news', '/i/admin/newsroom'); Route::get('newsroom', 'AdminController@newsroomHome')->name('admin.newsroom.home'); Route::get('newsroom/create', 'AdminController@newsroomCreate')->name('admin.newsroom.create'); diff --git a/routes/web.php b/routes/web.php index 16d05b22a..de2143d87 100644 --- a/routes/web.php +++ b/routes/web.php @@ -125,7 +125,8 @@ Route::domain(config('pixelfed.domain.app'))->middleware(['validemail', 'twofact Route::get('warning', 'AccountInterstitialController@get'); Route::post('warning', 'AccountInterstitialController@read'); - Route::get('my2020', 'SeasonalController@yearInReview'); + + Route::get('contact-admin-response/{id}', 'ContactController@showAdminResponse'); Route::get('web/my-portfolio', 'PortfolioController@myRedirect'); Route::get('web/hashtag/{tag}', 'SpaController@hashtagRedirect');