From c76dbcb8cc502f85871dd80c52296b6502db91bd Mon Sep 17 00:00:00 2001 From: HolgerHatGarKeineNode Date: Fri, 17 Nov 2023 12:48:57 +0100 Subject: [PATCH] add email lists --- .../Api/EmailCampaignController.php | 15 +++ .../Api/EmailCampaignGeneratorController.php | 90 +++++++++++++++ app/Models/EmailCampaign.php | 16 +++ app/Models/EmailTexts.php | 18 +++ app/Nova/EmailCampaign.php | 106 ++++++++++++++++++ app/Nova/EmailText.php | 105 +++++++++++++++++ app/Providers/NovaServiceProvider.php | 87 +++++++------- composer.json | 1 + composer.lock | 90 ++++++++++++++- config/filesystems.php | 10 +- config/openai.php | 18 +++ database/factories/EmailCampaignFactory.php | 23 ++++ database/factories/EmailTextsFactory.php | 23 ++++ ...11_095930_create_email_campaigns_table.php | 28 +++++ ..._11_11_100150_create_email_texts_table.php | 30 +++++ ...28_add_fields_to_email_campaigns_table.php | 28 +++++ docker-compose.yml | 2 +- routes/api.php | 8 ++ 18 files changed, 654 insertions(+), 44 deletions(-) create mode 100644 app/Http/Controllers/Api/EmailCampaignController.php create mode 100644 app/Http/Controllers/Api/EmailCampaignGeneratorController.php create mode 100644 app/Models/EmailCampaign.php create mode 100644 app/Models/EmailTexts.php create mode 100644 app/Nova/EmailCampaign.php create mode 100644 app/Nova/EmailText.php create mode 100644 config/openai.php create mode 100644 database/factories/EmailCampaignFactory.php create mode 100644 database/factories/EmailTextsFactory.php create mode 100644 database/migrations/2023_11_11_095930_create_email_campaigns_table.php create mode 100644 database/migrations/2023_11_11_100150_create_email_texts_table.php create mode 100644 database/migrations/2023_11_11_101728_add_fields_to_email_campaigns_table.php diff --git a/app/Http/Controllers/Api/EmailCampaignController.php b/app/Http/Controllers/Api/EmailCampaignController.php new file mode 100644 index 00000000..fd503abb --- /dev/null +++ b/app/Http/Controllers/Api/EmailCampaignController.php @@ -0,0 +1,15 @@ +get(); + } + +} diff --git a/app/Http/Controllers/Api/EmailCampaignGeneratorController.php b/app/Http/Controllers/Api/EmailCampaignGeneratorController.php new file mode 100644 index 00000000..828864ef --- /dev/null +++ b/app/Http/Controllers/Api/EmailCampaignGeneratorController.php @@ -0,0 +1,90 @@ +get('id'); + $md5 = $request->get('md5'); + + $campaign = \App\Models\EmailCampaign::query()->find($campaignId); + + $subject = $this->generateSubject($campaign); + //check if subject exists in database + $subjectExists = EmailTexts::query()->where('subject', $subject)->exists(); + // loop until subject is unique + while ($subjectExists) { + $subject = $this->generateSubject($campaign); + $subjectExists = EmailTexts::query()->where('subject', $subject)->exists(); + } + + $text = $this->generateText($campaign); + + $emailText = EmailTexts::query()->create([ + 'email_campaign_id' => $campaign->id, + 'sender_md5' => $md5, + 'subject' => $subject, + 'text' => $text, + ]); + $emailText->load('emailCampaign'); + + return $emailText; + } + + public function generateSubject(\Illuminate\Database\Eloquent\Model|\Illuminate\Database\Eloquent\Collection|\Illuminate\Database\Eloquent\Builder|array|null $campaign): string + { + $result = Http::timeout(120)->withHeaders([ + 'Authorization' => 'Bearer ' . config('openai.api_key'), + 'HTTP-Referer' => 'http://localhost', + ])->post('https://openrouter.ai/api/v1/chat/completions', [ + 'model' => $this->model, + 'max_tokens' => 50, + 'temperature' => 1, + 'messages' => [ + ['role' => 'user', 'content' => $campaign->subject_prompt], + ], + ]); + + if ($result->failed()) { + Log::error($result->json()); + abort(500, 'OpenAI API failed'); + } + + return $result->json()['choices'][0]['message']['content']; + } + + public function generateText(\Illuminate\Database\Eloquent\Model|\Illuminate\Database\Eloquent\Collection|\Illuminate\Database\Eloquent\Builder|array|null $campaign): mixed + { + $result = Http::timeout(120)->withHeaders([ + 'Authorization' => 'Bearer ' . config('openai.api_key'), + 'HTTP-Referer' => 'http://localhost', + ])->post('https://openrouter.ai/api/v1/chat/completions', [ + 'model' => $this->model, + 'max_tokens' => $this->maxTokens, + 'temperature' => 1, + 'messages' => [ + ['role' => 'user', 'content' => $campaign->text_prompt], + ], + ]); + + if ($result->failed()) { + Log::error($result->json()); + abort(500, 'OpenAI API failed'); + } + + return $result->json()['choices'][0]['message']['content']; + } + +} diff --git a/app/Models/EmailCampaign.php b/app/Models/EmailCampaign.php new file mode 100644 index 00000000..bcefa51f --- /dev/null +++ b/app/Models/EmailCampaign.php @@ -0,0 +1,16 @@ +hasMany(EmailTexts::class); + } +} diff --git a/app/Models/EmailTexts.php b/app/Models/EmailTexts.php new file mode 100644 index 00000000..a6d66bd1 --- /dev/null +++ b/app/Models/EmailTexts.php @@ -0,0 +1,18 @@ +belongsTo(EmailCampaign::class); + } +} diff --git a/app/Nova/EmailCampaign.php b/app/Nova/EmailCampaign.php new file mode 100644 index 00000000..a48602e4 --- /dev/null +++ b/app/Nova/EmailCampaign.php @@ -0,0 +1,106 @@ +sortable(), + + Text::make(__('Name'), 'name') + ->rules('required', 'string'), + + Text::make(__('List file name'), 'list_file_name') + ->rules('required', 'string'), + + Markdown::make(__('Subject text'), 'subject_prompt') + ->rules('required', 'string')->alwaysShow(), + + Markdown::make(__('Text prompt'), 'text_prompt') + ->rules('required', 'string')->alwaysShow(), + + HasMany::make(__('Email texts'), 'emailTexts', EmailText::class), + ]; + } + + /** + * Get the cards available for the request. + */ + public function cards(Request $request): array + { + return []; + } + + /** + * Get the filters available for the resource. + */ + public function filters(Request $request): array + { + return []; + } + + /** + * Get the lenses available for the resource. + */ + public function lenses(Request $request): array + { + return []; + } + + /** + * Get the actions available for the resource. + */ + public function actions(Request $request): array + { + return []; + } +} diff --git a/app/Nova/EmailText.php b/app/Nova/EmailText.php new file mode 100644 index 00000000..e48e65f1 --- /dev/null +++ b/app/Nova/EmailText.php @@ -0,0 +1,105 @@ +sortable(), + + BelongsTo::make(__('Email Campaign'), 'emailCampaign', EmailCampaign::class), + + Text::make(__('Sender md5'), 'sender_md5') + ->rules('required', 'string'), + + Text::make(__('Subject'), 'subject') + ->rules('required', 'string'), + + Markdown::make(__('Text'), 'text') + ->rules('required', 'string'), + + ]; + } + + /** + * Get the cards available for the request. + */ + public function cards(Request $request): array + { + return []; + } + + /** + * Get the filters available for the resource. + */ + public function filters(Request $request): array + { + return []; + } + + /** + * Get the lenses available for the resource. + */ + public function lenses(Request $request): array + { + return []; + } + + /** + * Get the actions available for the resource. + */ + public function actions(Request $request): array + { + return []; + } +} diff --git a/app/Providers/NovaServiceProvider.php b/app/Providers/NovaServiceProvider.php index 5ce7d3e0..9844ab2f 100644 --- a/app/Providers/NovaServiceProvider.php +++ b/app/Providers/NovaServiceProvider.php @@ -11,6 +11,8 @@ use App\Nova\Country; use App\Nova\Course; use App\Nova\CourseEvent; use App\Nova\Dashboards\Main; +use App\Nova\EmailCampaign; +use App\Nova\EmailText; use App\Nova\Episode; use App\Nova\Language; use App\Nova\Lecturer; @@ -43,18 +45,18 @@ class NovaServiceProvider extends NovaApplicationServiceProvider Nova::mainMenu(function (Request $request) { $comments = $request->user() - ->hasRole('super-admin') || $request->user() - ->can('CommentPolicy.viewAny') ? [ - MenuSection::make('Comments', [ - MenuItem::resource(Comment::class), - ]) - ->icon('chat') - ->collapsable(), - ] : []; + ->hasRole('super-admin') || $request->user() + ->can('CommentPolicy.viewAny') ? [ + MenuSection::make('Comments', [ + MenuItem::resource(Comment::class), + ]) + ->icon('chat') + ->collapsable(), + ] : []; $adminItems = $request->user() - ->hasRole('super-admin') || $request->user() - ->can('NovaAdminPolicy.viewAny') ? + ->hasRole('super-admin') || $request->user() + ->can('NovaAdminPolicy.viewAny') ? [ MenuSection::make('Admin', [ MenuItem::resource(Category::class), @@ -64,53 +66,60 @@ class NovaServiceProvider extends NovaApplicationServiceProvider MenuItem::resource(User::class), MenuItem::resource(Tag::class), ]) - ->icon('key') - ->collapsable(), + ->icon('key') + ->collapsable(), ] : []; $permissions = $request->user() - ->hasRole('super-admin') || $request->user() - ->can('PermissionPolicy.viewAny') ? [ - MenuSection::make(__('nova-spatie-permissions::lang.sidebar_label'), [ - MenuItem::link(__('nova-spatie-permissions::lang.sidebar_label_roles'), 'resources/roles'), - MenuItem::link(__('nova-spatie-permissions::lang.sidebar_label_permissions'), - 'resources/permissions'), - ]) - ->icon('key') - ->collapsable(), - ] : []; + ->hasRole('super-admin') || $request->user() + ->can('PermissionPolicy.viewAny') ? [ + MenuSection::make(__('nova-spatie-permissions::lang.sidebar_label'), [ + MenuItem::link(__('nova-spatie-permissions::lang.sidebar_label_roles'), 'resources/roles'), + MenuItem::link(__('nova-spatie-permissions::lang.sidebar_label_permissions'), + 'resources/permissions'), + ]) + ->icon('key') + ->collapsable(), + ] : []; return array_merge([ MenuSection::dashboard(Main::class) - ->icon('lightning-bolt'), + ->icon('lightning-bolt'), MenuSection::make(__('Locations'), [ MenuItem::resource(City::class), MenuItem::resource(Venue::class), ]) - ->icon('map') - ->collapsable(), + ->icon('map') + ->collapsable(), + + MenuSection::make(__('Bit-Bridge'), [ + MenuItem::resource(EmailCampaign::class), + MenuItem::resource(EmailText::class), + ]) + ->icon('inbox') + ->collapsable(), MenuSection::make('Bitcoiner', [ MenuItem::resource(Lecturer::class), ]) - ->icon('user-group') - ->collapsable(), + ->icon('user-group') + ->collapsable(), MenuSection::make('Meetups', [ MenuItem::resource(Meetup::class), MenuItem::resource(MeetupEvent::class), ]) - ->icon('calendar') - ->collapsable(), + ->icon('calendar') + ->collapsable(), MenuSection::make('Events', [ MenuItem::resource(BitcoinEvent::class), ]) - ->icon('star') - ->collapsable(), + ->icon('star') + ->collapsable(), MenuSection::make('Schule', [ MenuItem::resource(Course::class), @@ -118,29 +127,29 @@ class NovaServiceProvider extends NovaApplicationServiceProvider // MenuItem::resource(Participant::class), // MenuItem::resource(Registration::class), ]) - ->icon('academic-cap') - ->collapsable(), + ->icon('academic-cap') + ->collapsable(), MenuSection::make('Bibliothek', [ MenuItem::resource(Library::class), MenuItem::resource(LibraryItem::class), ]) - ->icon('library') - ->collapsable(), + ->icon('library') + ->collapsable(), MenuSection::make('Podcasts', [ MenuItem::resource(Podcast::class), MenuItem::resource(Episode::class), ]) - ->icon('microphone') - ->collapsable(), + ->icon('microphone') + ->collapsable(), MenuSection::make('Book-Cases', [ MenuItem::resource(BookCase::class), MenuItem::resource(OrangePill::class), ]) - ->icon('book-open') - ->collapsable(), + ->icon('book-open') + ->collapsable(), ], $comments, $adminItems, $permissions); }); diff --git a/composer.json b/composer.json index 1fdeba21..e3ee8309 100644 --- a/composer.json +++ b/composer.json @@ -33,6 +33,7 @@ "nova/start": "*", "oneduo/nova-time-field": "^1.0", "openai-php/client": "^0.4.1", + "openai-php/laravel": "^0.4.3", "podcastindex/podcastindex-php": "^1.0", "pusher/pusher-php-server": "^7.2.2", "qcod/laravel-gamify": "dev-master#6c0a55cf5351be5e7b4f31aa2499984853d895cf", diff --git a/composer.lock b/composer.lock index cd4a2d53..b9adbcc8 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": "e8ac17409ae4db4c622f5d31d870ba34", + "content-hash": "2c9601391937e04775f009c27b16b988", "packages": [ { "name": "akuechler/laravel-geoly", @@ -5999,6 +5999,92 @@ ], "time": "2023-04-12T04:26:02+00:00" }, + { + "name": "openai-php/laravel", + "version": "v0.4.3", + "source": { + "type": "git", + "url": "https://github.com/openai-php/laravel.git", + "reference": "c40ac21d5e5908b10ed370ac2a2b7d7b16978361" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/openai-php/laravel/zipball/c40ac21d5e5908b10ed370ac2a2b7d7b16978361", + "reference": "c40ac21d5e5908b10ed370ac2a2b7d7b16978361", + "shasum": "" + }, + "require": { + "guzzlehttp/guzzle": "^7.5", + "laravel/framework": "^9.46.0|^10.7.1", + "openai-php/client": "^0.4.2", + "php": "^8.1.0" + }, + "require-dev": { + "laravel/pint": "^1.8", + "pestphp/pest": "^2.4.0", + "pestphp/pest-plugin-arch": "^2.1.1", + "pestphp/pest-plugin-mock": "^2.0.0", + "phpstan/phpstan": "^1.10.13", + "symfony/var-dumper": "^6.2.8" + }, + "type": "library", + "extra": { + "laravel": { + "providers": [ + "OpenAI\\Laravel\\ServiceProvider" + ] + } + }, + "autoload": { + "psr-4": { + "OpenAI\\Laravel\\": "src/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Nuno Maduro", + "email": "enunomaduro@gmail.com" + } + ], + "description": "OpenAI PHP for Laravel is a supercharged PHP API client that allows you to interact with the Open AI API", + "keywords": [ + "GPT-3", + "api", + "client", + "codex", + "dall-e", + "language", + "laravel", + "natural", + "openai", + "php", + "processing", + "sdk" + ], + "support": { + "issues": "https://github.com/openai-php/laravel/issues", + "source": "https://github.com/openai-php/laravel/tree/v0.4.3" + }, + "funding": [ + { + "url": "https://www.paypal.com/paypalme/enunomaduro", + "type": "custom" + }, + { + "url": "https://github.com/nunomaduro", + "type": "github" + }, + { + "url": "https://www.patreon.com/nunomaduro", + "type": "patreon" + } + ], + "time": "2023-04-13T13:20:16+00:00" + }, { "name": "openspout/openspout", "version": "v4.18.0", @@ -18014,5 +18100,5 @@ "php": "^8.2" }, "platform-dev": [], - "plugin-api-version": "2.3.0" + "plugin-api-version": "2.6.0" } diff --git a/config/filesystems.php b/config/filesystems.php index 336c5bed..96f93fed 100644 --- a/config/filesystems.php +++ b/config/filesystems.php @@ -42,15 +42,21 @@ return [ 'throw' => false, ], + 'lists' => [ + 'driver' => 'local', + 'root' => storage_path('app/lists'), + 'throw' => false, + ], + 'public' => [ 'driver' => 'local', 'root' => storage_path('app/public'), - 'url' => env('APP_URL').'/storage', + 'url' => env('APP_URL') . '/storage', 'visibility' => 'public', 'throw' => false, ], - 'publicDisk' => [ + 'publicDisk' => [ 'driver' => 'local', 'root' => public_path(), 'throw' => false, diff --git a/config/openai.php b/config/openai.php new file mode 100644 index 00000000..ff684e9e --- /dev/null +++ b/config/openai.php @@ -0,0 +1,18 @@ + env('OPENAI_API_KEY'), + 'organization' => env('OPENAI_ORGANIZATION'), + +]; diff --git a/database/factories/EmailCampaignFactory.php b/database/factories/EmailCampaignFactory.php new file mode 100644 index 00000000..38579237 --- /dev/null +++ b/database/factories/EmailCampaignFactory.php @@ -0,0 +1,23 @@ + + */ +class EmailCampaignFactory extends Factory +{ + /** + * Define the model's default state. + * + * @return array + */ + public function definition(): array + { + return [ + // + ]; + } +} diff --git a/database/factories/EmailTextsFactory.php b/database/factories/EmailTextsFactory.php new file mode 100644 index 00000000..9aa2b0b8 --- /dev/null +++ b/database/factories/EmailTextsFactory.php @@ -0,0 +1,23 @@ + + */ +class EmailTextsFactory extends Factory +{ + /** + * Define the model's default state. + * + * @return array + */ + public function definition(): array + { + return [ + // + ]; + } +} diff --git a/database/migrations/2023_11_11_095930_create_email_campaigns_table.php b/database/migrations/2023_11_11_095930_create_email_campaigns_table.php new file mode 100644 index 00000000..48daca10 --- /dev/null +++ b/database/migrations/2023_11_11_095930_create_email_campaigns_table.php @@ -0,0 +1,28 @@ +id(); + $table->string('name'); + $table->string('list_file_name'); + $table->timestamps(); + }); + } + + /** + * Reverse the migrations. + */ + public function down(): void + { + Schema::dropIfExists('email_campaigns'); + } +}; diff --git a/database/migrations/2023_11_11_100150_create_email_texts_table.php b/database/migrations/2023_11_11_100150_create_email_texts_table.php new file mode 100644 index 00000000..ef82a02f --- /dev/null +++ b/database/migrations/2023_11_11_100150_create_email_texts_table.php @@ -0,0 +1,30 @@ +id(); + $table->foreignId('email_campaign_id')->constrained(); + $table->string('sender_md5'); + $table->string('subject'); + $table->longText('text'); + $table->timestamps(); + }); + } + + /** + * Reverse the migrations. + */ + public function down(): void + { + Schema::dropIfExists('email_texts'); + } +}; diff --git a/database/migrations/2023_11_11_101728_add_fields_to_email_campaigns_table.php b/database/migrations/2023_11_11_101728_add_fields_to_email_campaigns_table.php new file mode 100644 index 00000000..c901f3e6 --- /dev/null +++ b/database/migrations/2023_11_11_101728_add_fields_to_email_campaigns_table.php @@ -0,0 +1,28 @@ +text('subject_prompt')->nullable(); + $table->text('text_prompt')->nullable(); + }); + } + + /** + * Reverse the migrations. + */ + public function down(): void + { + Schema::table('email_campaigns', function (Blueprint $table) { + // + }); + } +}; diff --git a/docker-compose.yml b/docker-compose.yml index f510c2bf..604c591b 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -13,7 +13,7 @@ services: - 'host.docker.internal:host-gateway' ports: - '${APP_PORT:-80}:80' - - '${VITE_PORT:-5173}:${VITE_PORT:-5173}' +# - '${VITE_PORT:-5173}:${VITE_PORT:-5173}' environment: WWWUSER: '${WWWUSER}' LARAVEL_SAIL: 1 diff --git a/routes/api.php b/routes/api.php index fcf528e9..0c9be44d 100644 --- a/routes/api.php +++ b/routes/api.php @@ -1,5 +1,6 @@ as('api.') ->group(function () { + Route::get('email-list/{id}', function ($id) { + $campaign = EmailCampaign::query()->find($id); + return \Illuminate\Support\Facades\Storage::disk('lists')->download($campaign->list_file_name); + }); + Route::get('email-campaigns', \App\Http\Controllers\Api\EmailCampaignController::class); + Route::post('email-campaigns', \App\Http\Controllers\Api\EmailCampaignGeneratorController::class); + Route::resource('countries', \App\Http\Controllers\Api\CountryController::class); Route::resource('meetup', \App\Http\Controllers\Api\MeetupController::class); Route::resource('lecturers', \App\Http\Controllers\Api\LecturerController::class);