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