Code Coverage
 
Lines
Functions and Methods
Classes and Traits
Total
0.00% covered (danger)
0.00%
0 / 224
0.00% covered (danger)
0.00%
0 / 9
CRAP
0.00% covered (danger)
0.00%
0 / 1
FlyMsgAIController
0.00% covered (danger)
0.00%
0 / 224
0.00% covered (danger)
0.00%
0 / 9
756
0.00% covered (danger)
0.00%
0 / 1
 __construct
0.00% covered (danger)
0.00%
0 / 1
0.00% covered (danger)
0.00%
0 / 1
2
 checkQuota
0.00% covered (danger)
0.00%
0 / 24
0.00% covered (danger)
0.00%
0 / 1
6
 savePrompt
0.00% covered (danger)
0.00%
0 / 22
0.00% covered (danger)
0.00%
0 / 1
6
 generate
0.00% covered (danger)
0.00%
0 / 68
0.00% covered (danger)
0.00%
0 / 1
30
 updatePrompt
0.00% covered (danger)
0.00%
0 / 16
0.00% covered (danger)
0.00%
0 / 1
6
 deletePrompt
0.00% covered (danger)
0.00%
0 / 25
0.00% covered (danger)
0.00%
0 / 1
12
 getGoogleToken
0.00% covered (danger)
0.00%
0 / 19
0.00% covered (danger)
0.00%
0 / 1
6
 resetPrompt
0.00% covered (danger)
0.00%
0 / 32
0.00% covered (danger)
0.00%
0 / 1
30
 getTypingSpeedConfigByFeature
0.00% covered (danger)
0.00%
0 / 17
0.00% covered (danger)
0.00%
0 / 1
30
1<?php
2
3namespace App\Http\Controllers\v1\FlyMsgAI;
4
5use App\Events\TrackFlyMsgAIUsageEvent;
6use App\Helpers\Constants;
7use App\Helpers\FlyMSGLogger;
8use App\Http\Controllers\Controller;
9use App\Http\Models\FlyMsgAI\SavedPrompt;
10use App\Http\Models\Parameter;
11use App\Http\Models\Prompts\PromptModel;
12use App\Http\Models\Prompts\PromptSetting;
13use App\Http\Models\Prompts\PromptType;
14use App\Http\Models\Setting;
15use App\Http\Requests\UpdatePromptRequest;
16use App\Http\Services\SavedPromptsService;
17use App\Services\FlyMsgAI\FlyMsgAIService;
18use App\Services\FlyMsgAI\GeminiAPI;
19use App\Services\FlyMsgAI\GoogleTranslate;
20use App\Traits\AccountCenter\Reporting\FlyMsgAITrackingTrait;
21use App\Traits\SubscriptionTrait;
22use Illuminate\Http\JsonResponse;
23use Illuminate\Http\Request;
24use Illuminate\Http\Response;
25use Illuminate\Support\Facades\Log;
26
27class FlyMsgAIController extends Controller
28{
29    use FlyMsgAITrackingTrait, SubscriptionTrait;
30
31    public function __construct(
32        private SavedPromptsService $savedPromptsService
33    ) {}
34
35    /**
36     * Get a users AI prompt usage.
37     */
38    public function checkQuota(Request $request): JsonResponse
39    {
40        $feature = $request->feature;
41
42        if (! $feature) {
43            return response()->json([
44                'status' => 'error',
45                'message' => 'Feature is required.',
46            ], 400);
47        }
48
49        $user = $request->user();
50
51        $currentSubscriptionPlan = $this->getCurrentPlan($user);
52        $total = Constants::CURRENT_SUBSCRIPTION_PLAN_IDENTIFIERS[$currentSubscriptionPlan->identifier];
53
54        $used = $this->getQuotaUsed($user->id);
55
56        $quota = [
57            'used' => $used,
58            'total' => $total,
59            'remaining' => max($total - $used, 0),
60            'depleted' => $used >= $total,
61        ];
62
63        // Retrieve saved prompts
64        $saved_prompts = FlyMsgAIService::getUserPrompts($user, $feature);
65
66        return response()->json([
67            'status' => 'success',
68            'data' => [
69                'quota' => $quota,
70                'saved_prompts' => $saved_prompts,
71            ],
72        ]);
73    }
74
75    /**
76     * Save a prompt to the database.
77     */
78    public function savePrompt(Request $request): JsonResponse
79    {
80        try {
81            $data = $request->validate([
82                'name' => 'required|max:100|string|unique:saved_prompts,name,NULL,id,user_id,'.$request->user()->id,
83                'prompt' => 'required|string',
84                'feature' => 'required|string',
85            ]);
86
87            $saved_prompt = $request->user()->saved_prompts()
88                ->updateOrCreate(
89                    ['name' => $data['name'], 'feature' => $data['feature']],
90                    ['prompt' => $data['prompt']]
91                );
92
93            return response()->json([
94                'status' => 'success',
95                'data' => [
96                    'prompt' => $saved_prompt,
97                ],
98            ]);
99        } catch (\Throwable $th) {
100            FlyMSGLogger::logError('savePrompt', $th);
101
102            return response()->json([
103                'status' => 'error',
104                'message' => 'The given data failed validation or something went wrong.'.$th->getMessage(),
105            ], 400);
106        }
107    }
108
109    /**
110     * Generates a response to a prompt. This uses a waterfall approach
111     * of retrying the reqsuest until it is successful.
112     *
113     * Here is an example: Where we hit a rate limit with OpenAI, we will retry the request
114     * to another model. If that fails for rate limit again, we go to the next.
115     * If all of OpenAI models are maxed out, we will retry the request to the Google GeminiAPI api
116     * and repeat the cycle again.
117     */
118    public function generate(Request $request): JsonResponse
119    {
120        $user = $request->user();
121
122        $currentSubscriptionPlan = $this->getCurrentPlan($user);
123        $total = Constants::CURRENT_SUBSCRIPTION_PLAN_IDENTIFIERS[$currentSubscriptionPlan->identifier];
124
125        $used = $this->getQuotaUsed($user->id);
126
127        if ($used >= $total) {
128            return response()->json([
129                'status' => 'error',
130                'message' => 'You have used up your quota for the day. Please upgrade your plan.',
131            ], Response::HTTP_FORBIDDEN);
132        }
133
134        $request->validate([
135            'prompt' => 'required|string',
136            'context' => 'sometimes|string',
137            'name' => 'required|string',
138            'feature' => 'required|string|in:flyengage,flypost',
139            'is_regenerate' => 'sometimes|boolean',
140            'uniqueId' => 'sometimes|nullable|string',
141        ], [
142            'feature.in' => 'The feature field must be either flyengage or flypost.',
143        ]);
144
145        $name = $request->name;
146        $prompt = $request->prompt;
147        $feature = $request->feature;
148        $context = strip_tags($request->context);
149        $is_regenerate = $request->is_regenerate;
150        $uniqueId = null;
151
152        if ($feature == 'flyengage') {
153            $uniqueId = "https://www.linkedin.com/feed/update/{$request->uniqueId}";
154        } elseif ($feature == 'flypost') {
155            $uniqueId = str_slug($request->uniqueId);
156        }
157
158        try {
159            $ai_service = new FlyMsgAIService;
160            $aiResult = $ai_service->generate($name, strip_tags($context), $prompt, $feature, $uniqueId, $is_regenerate);
161            $prompt_response = $ai_service->transform($aiResult['response']);
162
163            TrackFlyMsgAIUsageEvent::dispatch(
164                $user,
165                $prompt_response,
166                $request->browser,
167                $request->name,
168                $request->prompt."\n\n Post:\n\n ".$request->context,
169                $request->feature,
170                $uniqueId,
171                $aiResult,
172                null
173            );
174
175            $used++;
176
177            $quota = [
178                'used' => $used,
179                'total' => $total,
180                'remaining' => max($total - $used, 0),
181                'depleted' => $used >= $total,
182            ];
183
184            $words_per_minute_typing_speed_control = $this->getTypingSpeedConfigByFeature($request->feature, $user);
185
186            return response()->json([
187                'status' => 'success',
188                'data' => [
189                    'prompt' => $request->prompt."\n\n Post:\n\n ".$request->context,
190                    'prompt_response' => $prompt_response,
191                    'quota' => $quota,
192                    'typingConfig' => $words_per_minute_typing_speed_control,
193                ],
194            ]);
195        } catch (\Throwable $th) {
196            FlyMSGLogger::logError('generate', $th);
197
198            return response()->json([
199                'status' => 'error',
200                'data' => [
201                    'message' => 'Something went wrong. '.$th->getMessage(),
202                ],
203            ], Response::HTTP_INTERNAL_SERVER_ERROR);
204        }
205    }
206
207    public function updatePrompt(UpdatePromptRequest $request, SavedPrompt $prompt): JsonResponse
208    {
209        try {
210            $data = $request->validated();
211
212            $prompt = $this->savedPromptsService->update($prompt->id, $data);
213
214            return response()->json([
215                'status' => 'updated',
216                'data' => [
217                    'prompt' => $prompt,
218                ],
219            ], 200);
220        } catch (\Throwable $th) {
221            FlyMSGLogger::logError('updatePrompt', $th);
222
223            return response()->json([
224                'status' => 'error',
225                'data' => [
226                    'message' => 'Something went wrong.'.$th->getMessage(),
227                ],
228            ], 500);
229        }
230    }
231
232    public function deletePrompt(Request $request, SavedPrompt $prompt): JsonResponse
233    {
234        try {
235            $request->validate([
236                'feature' => 'required|string',
237            ]);
238
239            if ($request->feature == 'flyengage') {
240                $prompt->delete();
241            } else {
242                return response()->json([
243                    'status' => 'error',
244                    'data' => [
245                        'message' => 'Feature not supported yet. Only flyengage is supported.',
246                    ],
247                ], 400);
248            }
249
250            return response()->json([
251                'status' => 'deleted',
252                'data' => [
253                    'prompt' => null,
254                ],
255            ], 200);
256        } catch (\Throwable $th) {
257            FlyMSGLogger::logError('deletePrompt', $th);
258
259            return response()->json([
260                'status' => 'error',
261                'data' => [
262                    'message' => 'Something went wrong. '.$th->getMessage(),
263                ],
264            ], 500);
265        }
266    }
267
268    /**
269     * Gets google refresh token
270     */
271    public function getGoogleToken(Request $request): JsonResponse
272    {
273        $method = $request->method();
274        $url = $request->url();
275        $headers = $request->headers->all();
276        $params = $request->all();
277
278        if ($method != 'GET') {
279            $headers = json_encode($request->headers->all());
280            $params = json_encode($request->all());
281        }
282
283        Log::info('GOOGLE HTTP Request: '.$method.' '.$url, [
284            'headers' => $headers,
285            'params' => $params,
286            'user' => $request->user(),
287        ]);
288
289        $geminiapi_token = GeminiAPI::refreshAccessToken();
290        $translate_token = GoogleTranslate::refreshGoogleTranslateAccessToken();
291
292        return response()->json([
293            'message' => 'Refreshed Google Token',
294            'geminiapi_token' => $geminiapi_token,
295            'translate_token' => $translate_token,
296        ]);
297    }
298
299    /**
300     * Resets a users default prompt to vengresos' default.
301     */
302    public function resetPrompt(Request $request, SavedPrompt $prompt): JsonResponse
303    {
304        try {
305            $data = $request->validate([
306                'name' => 'required|string',
307                'feature' => 'required|string',
308            ]);
309
310            // Determine the default prompt based on the feature
311            if ($data['feature'] == 'flyengage') {
312                $model = PromptModel::where('is_active', true)->latest()->first();
313                $setting = PromptSetting::where('prompt_model_id', $model->id)->where('is_active', true)->where('feature', 'flyengage')->latest()->first();
314                $promptBase = PromptType::where('prompt_setting_id', $setting->id)->where('is_active', true)->where('feature', 'flyengage')->where('name', $data['name'])->first();
315
316                $defaultPrompt = $promptBase->mission;
317            } elseif ($data['feature'] == 'flypost') {
318                $defaultPrompt = Constants::DEFAULT_FLYPOST_PROMPTS[$data['name']] ?? null;
319            } else {
320                throw new \Exception('Feature not supported yet. Only flyengage and flypost are supported.');
321            }
322
323            // If the default prompt is not found, throw an error
324            if (! $defaultPrompt) {
325                throw new \Exception('No default prompt found for the specified feature and name.');
326            }
327
328            // Update the saved prompt with the default
329            $prompt->update([
330                'prompt' => $defaultPrompt,
331                'name' => $data['name'],
332            ]);
333
334            return response()->json([
335                'status' => 'success',
336                'data' => [
337                    'prompt' => $prompt,
338                ],
339            ], 200);
340        } catch (\Throwable $th) {
341            FlyMSGLogger::logError('resetPrompt', $th);
342
343            return response()->json([
344                'status' => 'error',
345                'data' => [
346                    'message' => 'Something went wrong. '.$th->getMessage(),
347                ],
348            ], 500);
349        }
350    }
351
352    public function getTypingSpeedConfigByFeature($feature, $user)
353    {
354        $parameters = Parameter::where('name', 'like', 'flywrite_typing%')->get()->keyBy('name');
355
356        $flypostTypingMode = $parameters['flywrite_typing_post_typing_mode']->value ?? 'letter';
357        $flypostTypingSpeed = $parameters['flywrite_typing_post_typing_speed']->value ?? Constants::FLYPOST_FE_TYPING_SPEED_PER_MINUTE;
358        $flyengageTypingMode = $parameters['flywrite_typing_engage_typing_mode']->value ?? 'letter';
359        $flyengageTypingSpeed = $parameters['flywrite_typing_engage_typing_speed']->value ?? Constants::FLYENGAGE_FE_TYPING_SPEED_PER_MINUTE;
360
361        $userSetting = Setting::where('user_id', $user->id)->first();
362
363        return match ($feature) {
364            'flypost' => [
365                'type' => $userSetting?->typing_style ?? $flypostTypingMode,
366                'words_per_minute' => (! empty($userSetting?->typing_style) && $userSetting?->typing_style == 'letter') ? ($userSetting->typing_speed ?? $flypostTypingSpeed) : $flypostTypingSpeed,
367            ],
368            'flyengage' => [
369                'type' => $userSetting?->typing_style ?? $flyengageTypingMode,
370                'words_per_minute' => (! empty($userSetting?->typing_style) && $userSetting?->typing_style == 'letter') ? ($userSetting->typing_speed ?? $flyengageTypingSpeed) : $flyengageTypingSpeed,
371            ],
372            default => null
373        };
374    }
375}