Add (article): ai gen, front views

This commit is contained in:
2023-09-24 22:53:40 +08:00
parent 18705bd5e4
commit 322d680961
115 changed files with 9710 additions and 201 deletions

View File

@@ -0,0 +1,44 @@
<?php
namespace App\Console\Commands;
use Illuminate\Console\Command;
use Spatie\Sitemap\SitemapGenerator;
class GenerateSitemap extends Command
{
/**
* The name and signature of the console command.
*
* @var string
*/
protected $signature = 'sitemap:generate';
/**
* The console command description.
*
* @var string
*/
protected $description = 'Generate the sitemap.';
/**
* Create a new command instance.
*
* @return void
*/
public function __construct()
{
parent::__construct();
}
/**
* Execute the console command.
*
* @return int
*/
public function handle()
{
SitemapGenerator::create(config('app.url'))
->writeToFile(public_path('sitemap.xml'));
}
}

View File

@@ -0,0 +1,53 @@
<?php
namespace App\Helpers\FirstParty\OSSUploader;
use Exception;
use Illuminate\Support\Facades\Storage;
class OSSUploader
{
public static function readJson($storage_driver, $relative_directory, $filename)
{
$filepath = rtrim($relative_directory, '/').'/'.$filename;
try {
$jsonContent = Storage::disk($storage_driver)->get($filepath);
$decodedJson = json_decode($jsonContent, false, 512);
return $decodedJson;
} catch (\Exception $e) {
return null;
}
return null;
}
public static function uploadJson($storage_driver, $relative_directory, $filename, $jsonData)
{
$jsonString = json_encode($jsonData, JSON_PRETTY_PRINT);
try {
return self::uploadFile($storage_driver, $relative_directory, $filename, $jsonString);
} catch (Exception $e) {
return false;
}
return false;
}
public static function uploadFile($storage_driver, $relative_directory, $filename, $file)
{
$filepath = rtrim($relative_directory, '/').'/'.$filename;
// if(!Storage::disk($storage_driver)->exists($relative_directory))
// {
// Storage::disk($storage_driver)->makeDirectory($relative_directory);
// }
return Storage::disk($storage_driver)->put($filepath, $file);
}
}

View File

@@ -0,0 +1,90 @@
<?php
namespace App\Helpers\FirstParty\OpenAI;
use Exception;
use Illuminate\Support\Facades\Http;
class OpenAI
{
public static function writeArticle($title, $description, $article_type, $min, $max)
{
$system_prompt = "
Using the general article structure, please create a Markdown format article on the topic given. The article should prioritize accuracy and provide genuine value to readers. Avoid making assumptions or adding unverified facts. Ensure the article is between {$min}-{$max} words. Write with 8th & 9th grade US english standard.\n\n
Structure:\n\n
Title\n
Provide a headline that captures the essence of the article's focus and is tailored to the reader's needs.\n\n
Introduction\n
Offer a brief overview or background of the topic, ensuring it's engaging and invites readers to continue reading.\n\n
Main Body\n\n
Subsection\n
Introduce foundational information about the topic, ensuring content is accurate and based on known facts. Avoid generic or speculative statements.\n\n
Subsection (if applicable)\n
If helpful, use Markdown to create tables to convey comparison of data. Ensure data is accurate and relevant to the reader.\n\n
Subsection\n
Dive deep into primary elements or facets of the topic, ensuring content is factual and offers value.\n\n
Subsection\n
Discuss real-world applications or significance, highlighting practical implications or actionable insights for the reader.\n\n
Subsection (optional)\n
Provide context or relate the topic to relevant past events or trends, making it relatable and more comprehensive.\n\n
Subsection (if applicable)\n
Predict outcomes, trends, or ramifications, but ensure predictions are rooted in known information or logical reasoning.\n\n
Subsection\n
Summarise key points or lessons, ensuring they resonate with the initial objectives of the article and provide clear takeaways.\n\n
Conclusion\n
Revisit main points and offer final thoughts or recommendations that are actionable and beneficial to the reader.\n\n
FAQs\n
Address anticipated questions or misconceptions about the topic. Prioritize questions that readers are most likely to have and provide clear, concise answers based on factual information.\n
Q: Question\n
A: Answer\n
";
$user_prompt = "Title: {$title}\nDescription: {$description}\nArticleType: {$article_type}";
return self::chatCompletion($system_prompt, $user_prompt, 'gpt-3.5-turbo');
}
public static function suggestArticleTitles($current_title, $supporting_data, $suggestion_counts)
{
$system_prompt = "Based on provided article title, identify the main keyword in 1-2 words. Once identified, use the main keyword only to generate {$suggestion_counts} easy-to-read unique, helpful title articles.\n\n
Requirements:\n
2 descriptive photos keywords to represent article title when put together side-by-side\n
No realtime information required\n
No guides and how tos\n
No punctuation in titles especially colons :\n
90-130 characters\n\n
return in following json format {\"main_keyword\":\"(Main Keyword)\",\"suggestions\":[{\"title\":\"(Title in 90-130 letters)\",\"short_title\":\"(Short Title in 30-40 letters)\",\"article_type\":\"(How-tos|Guides|Interview|Review|Commentary|Feature|News|Editorial|Report|Research|Case-study|Overview|Tutorial|Update|Spotlight|Insights)\",\"description\":\"(SEO description based on main keyword)\",\"photo_keywords\":[\"photo keyword 1\",\"photo keyword 2\"]}]}";
$user_prompt = "Article Title: {$current_title}";
$reply = self::chatCompletion($system_prompt, $current_title, 'gpt-3.5-turbo');
try {
return json_decode($reply, false);
} catch (Exception $e) {
return null;
}
}
public static function chatCompletion($system_prompt, $user_prompt, $model)
{
$response = Http::timeout(500)->withToken(config('platform.ai.openai.api_key'))
->post('https://api.openai.com/v1/chat/completions', [
'model' => $model,
'max_tokens' => 2500,
'messages' => [
['role' => 'user', 'content' => $system_prompt.' '.$user_prompt],
],
]);
$json_response = json_decode($response->body());
$reply = $json_response?->choices[0]?->message?->content;
return $reply;
}
}

View File

@@ -0,0 +1,46 @@
<?php
if (! function_exists('get_current_ip')) {
function get_current_ip()
{
$ip = null;
if (app()->environment() == 'local') {
return config('platform.general.dev_default_ip');
}
if (getenv('HTTP_CF_CONNECTING_IP')) {
$ip = getenv('HTTP_CF_CONNECTING_IP');
} elseif (isset($_SERVER['HTTP_CF_CONNECTING_IP'])) {
$ip = $_SERVER['HTTP_CF_CONNECTING_IP'];
}
if (! is_empty($ip)) {
return $ip;
}
$ip_add_set = null;
if (getenv('HTTP_CLIENT_IP')) {
$ip_add_set = getenv('HTTP_CLIENT_IP');
} elseif (getenv('HTTP_X_FORWARDED_FOR')) {
$ip_add_set = getenv('HTTP_X_FORWARDED_FOR');
} elseif (getenv('HTTP_X_FORWARDED')) {
$ip_add_set = getenv('HTTP_X_FORWARDED');
} elseif (getenv('HTTP_FORWARDED_FOR')) {
$ip_add_set = getenv('HTTP_FORWARDED_FOR');
} elseif (getenv('HTTP_FORWARDED')) {
$ip_add_set = getenv('HTTP_FORWARDED');
} elseif (getenv('REMOTE_ADDR')) {
$ip_add_set = getenv('REMOTE_ADDR');
} elseif (isset($_SERVER['REMOTE_ADDR'])) {
$ip_add_set = $_SERVER['REMOTE_ADDR'];
} else {
$ip_add_set = '127.0.0.0';
}
$ip_add_set = explode(',', $ip_add_set);
return $ip_add_set[0];
}
}

View File

@@ -0,0 +1,5 @@
<?php
require 'string_helper.php';
require 'geo_helper.php';
require 'platform_helper.php';

View File

@@ -0,0 +1,13 @@
<?php
if (! function_exists('get_slack_channel_by_env')) {
function get_slack_channel_by_env($slack_channel = 'slack')
{
if (app()->environment() == 'local') {
return config("platform.notifications.{$slack_channel}.development_channel");
} else {
return config("platform.notifications.{$slack_channel}.production_channel");
}
}
}

View File

@@ -0,0 +1,120 @@
<?php
use Illuminate\Support\Str;
if (! function_exists('epoch_now_timestamp')) {
function epoch_now_timestamp()
{
return (int) round(microtime(true) * 1000);
}
}
if (! function_exists('unslug')) {
function unslug($slug, $delimiter = '-')
{
return ucwords(str_replace($delimiter, ' ', $slug));
}
}
if (! function_exists('str_slug')) {
function str_slug($string, $delimiter = '-')
{
return Str::of(trim($string))->slug($delimiter);
}
}
if (! function_exists('str_first_sentence')) {
function str_first_sentence($str)
{
// Split the string at ., !, or ?
$sentences = preg_split('/(\.|!|\?)(\s|$)/', $str, 2);
// Return the first part of the array if available
if (isset($sentences[0])) {
return trim($sentences[0]);
}
// If no sentence ending found, return the whole string
return $str;
}
}
if (! function_exists('is_empty')) {
/**
* A better function to check if a value is empty or null. Strings, arrays, and Objects are supported.
*
* @param mixed $value
*/
function is_empty($value): bool
{
if (is_null($value)) {
return true;
}
if (is_string($value)) {
if ($value === '') {
return true;
}
}
if (is_array($value)) {
if (count($value) === 0) {
return true;
}
}
if (is_object($value)) {
$value = (array) $value;
if (count($value) === 0) {
return true;
}
}
return false;
}
}
if (! function_exists('get_country_name_by_iso')) {
function get_country_name_by_iso($country_iso)
{
if (! is_empty($country_iso)) {
$country_iso = strtoupper($country_iso);
try {
return config("platform.country_codes.{$country_iso}")['name'];
} catch (\Exception $e) {
}
}
return 'International';
}
}
if (! function_exists('get_country_emoji_by_iso')) {
function get_country_emoji_by_iso($country_iso)
{
if (! is_empty($country_iso)) {
$country_iso = strtoupper($country_iso);
try {
return config("platform.country_codes.{$country_iso}")['emoji'];
} catch (\Exception $e) {
}
}
return '🌎';
}
}
if (! function_exists('str_random')) {
function str_random($length = 10)
{
return Str::random($length);
}
}

View File

@@ -0,0 +1,580 @@
<?php
namespace App\Helpers\ThirdParty\DFS;
use DFSClientV3\Bootstrap\Application;
use DFSClientV3\Models\DataMapper;
use DFSClientV3\Services\HttpClient\Handlers\Responses;
use DFSClientV3\Services\HttpClient\HttpClient;
abstract class AbstractModel
{
/**
* @var null|int timeout for http request
*/
protected $timeOut = null;
protected $apiVersion = null;
/**
* @var string | null url to DataForSeo API
*/
protected $url = null;
/**
* @var int|null PostId is needed for a request, for more information check an example or DFSApi
*/
protected $postId = null;
/**
* @var null This field is not working at the moment
*/
public $statusCode;
/**
* @var null This field is not working at the moment
*/
public $statusMessage;
/**
* @var array
*/
protected $headers = [];
/**
* @var null This field is not working at the moment
*/
protected $requiredField;
/**
* @var null This field is not working at the moment
*/
protected $mainData;
/**
* @var static RequestType, this param will be sent to DFS API, .xml, .gzip etc...
*/
protected $requestType;
/**
* @var string Method for http request. POST, GET , PUT, DELETE
*/
protected $method = 'POST';
/**
* This variable is required for all extended Classes from AbstractModel.
*
* @var null|string It is a name, when you want to send a request to api, use this param as an api endpoint, example: cmn_user
*/
protected $requestToFunction;
/**
* This variable is required for all extended Classes from AbstractModel
* example for path: results->0->related.
*
* results - is object link from DFSResponse
* 0 - is Index of array or postID
* related - is object link containing main data
*
* @var null|string It is a system variable, it contains a path to main data from response and creates iterable(IteratorAggregator) response.
*/
protected $pathToMainData = null;
/**
* @var bool If payload contains postId
*/
protected $isSupportedMerge = false;
/**
* @var null|string
*/
private $DFSLogin = null;
/**
* @var null|string
*/
private $DFSPassword = null;
public $response;
protected $application;
protected $config;
/**
* @var array
*/
protected $payload;
protected $isProcessed;
protected $mappedMainModel;
protected $seTypes = ['organic', 'maps', 'local_finder', 'news', 'images', 'search_by_image'];
/**
* @var bool
*/
protected $jsonContainVariadicType = false;
/**
* @var array
*/
protected $pathsToVariadicTypesAndValue = [];
/**
* @var array
*/
protected $customFunctionForPaths = [];
/**
* @var array
*/
protected $pathsToDictionary = [];
/**
* @var bool Temp variable, for detect when use new mapper
*/
protected $useNewMapper = false;
/**
* new version of DataForSeo has two variations of result
* 1. Object contains other objects for response.
* 2. Object contains other objects, but they is iterable.
*
* if model has property $resultShouldBeTransformedToArray as true, result index will be transformed to array
*
* @var bool
*/
protected $resultShouldBeTransformedToArray = false;
public function __construct()
{
$this->application = Application::getInstance();
$this->config = $this->application->getConfig();
$this->initDefaultMethods();
}
/**
* @param $postID mixed
*/
public function setPostID($postID)
{
$this->postId = $postID;
return $this;
}
/**
* @param $headers array
*/
public function setHeaders($headers)
{
if (count($headers) > 0) {
foreach ($headers as $key => $value) {
$this->config['headers'][$key] = $value;
}
}
return $this;
}
/**
* This method will run request to api.
*/
protected function get()
{
$response = $this->process();
// check if response contain valid json
$validResponse = json_decode($response->getResponse());
if (json_last_error() !== JSON_ERROR_NONE) {
$validResponse = ['status_code' => 50000, 'status_message' => 'error.'];
}
return $this->mapData(json_encode($validResponse), $response->getStatus());
}
/**
* @return mixed
*
* @throws \Exception
*/
public static function getAfterMerge(array $modelPool)
{
if (empty($modelPool)) {
throw new \Exception('Model pool can not be empty');
}
$payload = [];
$url = null;
$apiVersion = null;
$timeOut = null;
$login = null;
$password = null;
$method = null;
$requestToFunction = null;
$config = Application::getInstance()->getConfig();
$pathToMainData = null;
$resultShouldTransformedToArray = false;
$useNewMapper = false;
$isJsonContainVariadicType = false;
$pathsToVariadicTypesAndValue = [];
$customFunctionForPaths = [];
foreach ($modelPool as $key => $model) {
// kostyl, all variable will be rewrite every iteration
$url = $model->url;
$apiVersion = $model->apiVersion;
$timeOut = $model->timeOut;
$login = $model->DFSLogin;
$password = $model->DFSPassword;
$method = $model->method;
$requestToFunction = $model->requestToFunction;
$pathToMainData = $model->pathToMainData;
$resultShouldTransformedToArray = $model->resultShouldBeTransformedToArray;
$useNewMapper = $model->isUseNewMapper();
$isJsonContainVariadicType = $model->isJsonContainVariadicType();
$pathsToVariadicTypesAndValue = $model->getPathsToVariadicTypesAndValue();
$customFunctionForPaths = $model->getCustomFunctionForPaths();
if ($model->isSupportedMerge) {
if ($model->postId === null) {
$payload['json'][] = $model->payload;
}
if ($model->postId !== null) {
$payload['json'][$model->postId] = $model->payload;
}
} else {
throw new \Exception('Provided model '.get_class($model).' is not supported merge');
}
}
$payload['headers'] = $config['headers'];
$http = new HttpClient($url, $apiVersion, $timeOut, $login, $password);
$res = $http->sendSingleRequest($method, $requestToFunction, $payload);
// check if response contain valid json
$validResponse = json_decode($res->getResponse());
if (json_last_error() !== JSON_ERROR_NONE) {
$validResponse = ['status_code' => 50000, 'status_message' => 'error.'];
}
//get called class.
$calledClassNameWithNapeSpace = get_called_class();
$classNameArray = explode('\\', $calledClassNameWithNapeSpace);
//for php 7.3 can be use array_last_key
$classNameArray[count($classNameArray) - 1];
$mapper = new DataMapper($classNameArray[count($classNameArray) - 1], $res->getStatus(), $pathToMainData);
if ($useNewMapper) {
$paveDataOptions = new PaveDataOptions();
$paveDataOptions->setJson(json_encode($validResponse));
$paveDataOptions->setJsonContainVariadicType($isJsonContainVariadicType);
$paveDataOptions->setPathsToVariadicTypesAndValue($pathsToVariadicTypesAndValue);
$paveDataOptions->setCustomFunctionForPaths($customFunctionForPaths);
$mappedModel = $mapper->paveDataNew($paveDataOptions);
return $mappedModel;
}
$mappedModel = $mapper->paveData(json_encode($validResponse), null, $resultShouldTransformedToArray);
return $mappedModel;
}
/**
* @param array|AbstractModel[] $modelPool
* @return array|null
*/
public static function getAsync(array $modelPool, int $timeout = 100)
{
if (count($modelPool) > 100) {
return null;
}
$requestsPool = [];
$results = [];
$payload = [];
$url = null;
$apiVersion = null;
$timeOut = null;
$login = null;
$password = null;
$config = Application::getInstance()->getConfig();
$pathToMainData = null;
$resultShouldTransformedToArray = false;
$useNewMapper = false;
$isJsonContainVariadicType = false;
$pathsToVariadicTypesAndValue = [];
$customFunctionForPaths = [];
$pathsToDictionary = [];
foreach ($modelPool as $key => $model) {
// kostyl, all variable will be rewrite every iteration
$url = $model->url;
$apiVersion = $model->apiVersion;
$login = $model->DFSLogin;
$password = $model->DFSPassword;
$pathToMainData = $model->pathToMainData;
$resultShouldTransformedToArray = $model->resultShouldBeTransformedToArray;
$useNewMapper = $model->isUseNewMapper();
$isJsonContainVariadicType = $model->isJsonContainVariadicType();
$pathsToVariadicTypesAndValue = $model->getPathsToVariadicTypesAndValue();
$customFunctionForPaths = $model->getCustomFunctionForPaths();
$pathsToDictionary = $model->getPathsToDictionary();
$payload['json'][] = $model->getPayload();
$payload['headers'] = $config['headers'];
$requestsPool[$key]['method'] = $model->getHttpMethod();
$requestsPool[$key]['url'] = $model->getRequestToFunction();
$requestsPool[$key]['params'] = $payload;
$requestsPool[$key]['pathToMainData'] = $model->getPathToMainData();
}
$http = new HttpClient($url, $apiVersion, $timeout, $login, $password);
$responses = $http->sendAsyncRequests($requestsPool, null);
foreach ($responses as $response) {
/**
* @var Responses $response
*/
//get called class.
$calledClassNameWithNapeSpace = get_called_class();
$classNameArray = explode('\\', $calledClassNameWithNapeSpace);
//for php 7.3 can be use array_last_key
$classNameArray[count($classNameArray) - 1];
$mapper = new DataMapper($classNameArray[count($classNameArray) - 1], $response->getStatus(), $pathToMainData);
if ($useNewMapper) {
$paveDataOptions = new PaveDataOptions();
$paveDataOptions->setJson($response->getResponse());
$paveDataOptions->setJsonContainVariadicType($isJsonContainVariadicType);
$paveDataOptions->setPathsToDictionary($pathsToDictionary);
$paveDataOptions->setPathsToVariadicTypesAndValue($pathsToVariadicTypesAndValue);
$paveDataOptions->setCustomFunctionForPaths($customFunctionForPaths);
$mappedModel = $mapper->paveDataNew($paveDataOptions);
$results[] = $mappedModel;
}
if (! $useNewMapper) {
$results[] = $mapper->paveData($response->getResponse(), null, $resultShouldTransformedToArray);
}
}
return $results;
}
/**
* @return \DFSClientV3\Services\HttpClient\Handlers\Responses
*/
public function process()
{
$http = new HttpClient($this->url, $this->apiVersion, $this->timeOut, $this->DFSLogin, $this->DFSPassword);
$payload = [];
if (! $this->application) {
dd('DFSClient was not init, add to your code: $DFSClient = new DFSClient() ');
}
if (! $this->requestToFunction) {
dd('requestFunction can not be null, set this field in your model: '.get_called_class());
}
if ($this->postId === null) {
$payload['json'][0] = $this->payload;
}
if ($this->postId != null) {
$payload['json'][$this->postId] = $this->payload;
}
$payload['headers'] = $this->config['headers'];
$res = $http->sendSingleRequest($this->method, $this->requestToFunction, $payload);
return $res;
}
public function getAsJson()
{
$result = $this->process()->getResponse();
return $result;
}
/**
* @return string|null
*/
public function getPathToMainData()
{
return $this->pathToMainData;
}
/**
* @return mixed
*/
public function getCalledClass()
{
$calledClassNameWithNapeSpace = get_called_class();
$classNameArray = explode('\\', $calledClassNameWithNapeSpace);
//for php 7.3 can be use array_last_key
return $classNameArray[count($classNameArray) - 1];
}
public function setOpt()
{
}
public function setLogin(string $newLogin)
{
$this->DFSLogin = $newLogin;
return $this;
}
public function setPassword(string $newPassword)
{
$this->DFSPassword = $newPassword;
return $this;
}
/**
* @param bool $isSuccesful
*
* return mixed;
*/
protected function mapData(string $json, bool $isSuccesful = false)
{
$mapper = new DataMapper($this->getCalledClass(), $isSuccesful, $this->pathToMainData);
if ($this->useNewMapper) {
$paveDataOptions = new PaveDataOptions();
$paveDataOptions->setJson($json);
$paveDataOptions->setJsonContainVariadicType($this->isJsonContainVariadicType());
$paveDataOptions->setPathsToVariadicTypesAndValue($this->getPathsToVariadicTypesAndValue());
$paveDataOptions->setPathsToDictionary($this->getPathsToDictionary());
$paveDataOptions->setCustomFunctionForPaths($this->getCustomFunctionForPaths());
$mappedModel = $mapper->paveDataNew($paveDataOptions);
return $mappedModel;
}
$mappedModel = $mapper->paveData($json, null, $this->resultShouldBeTransformedToArray);
return $mappedModel;
}
/**
* @return bool
*/
public function resultShouldBeTransformedToArray()
{
return $this->resultShouldBeTransformedToArray;
}
public function useSandbox(string $url = null)
{
$this->url = $this->config['sandboxUrl'];
}
/**
* @param $timeOut int
*/
public function setTimeOut(int $timeOut)
{
$this->timeOut = $timeOut;
return $this;
}
public function isJsonContainVariadicType(): bool
{
return $this->jsonContainVariadicType;
}
public function getPathsToVariadicTypesAndValue(): array
{
return $this->pathsToVariadicTypesAndValue;
}
public function getPathsToDictionary(): array
{
return $this->pathsToDictionary;
}
public function addCustomFunctionForPath(array $customFunction)
{
$this->customFunctionForPaths = array_merge($this->customFunctionForPaths, $customFunction);
return $this;
}
public function getCustomFunctionForPaths(): array
{
return $this->customFunctionForPaths;
}
private function initDefaultMethods()
{
$this->addCustomFunctionForPath($this->initCustomFunctionForPaths());
}
protected function initCustomFunctionForPaths(): array
{
return [];
}
public function isUseNewMapper(): bool
{
return $this->useNewMapper;
}
/**
* @return string
*/
public function getHttpMethod()
{
return $this->method;
}
/**
* @return string|null
*/
public function getRequestToFunction()
{
return $this->requestToFunction;
}
public function getPayload(): array
{
return $this->payload ?? [];
}
}

View File

@@ -0,0 +1,203 @@
<?php
namespace App\Helpers\ThirdParty\DFS;
class SettingSerpLiveAdvanced extends AbstractModel
{
protected $method = 'POST';
protected $isSupportedMerge = true;
protected $pathToMainData = 'tasks->{$postID}->result';
protected $requestToFunction = 'serp/{$se}/{$seType}/live/advanced';
protected $resultShouldBeTransformedToArray = true;
protected $jsonContainVariadicType = true;
protected $pathsToVariadicTypesAndValue = ['tasks->(:number)->result->(:number)->items->(:number)' => 'type'];
protected $useNewMapper = true;
/**
* @return $this
*/
public function setUrl(string $url)
{
$this->payload['url'] = $url;
return $this;
}
/**
* @return $this
*/
public function setLanguageCode(string $langCode)
{
$this->payload['language_code'] = $langCode;
return $this;
}
/**
* @return $this
*/
public function setKeyword(string $keyword)
{
$this->payload['keyword'] = $keyword;
return $this;
}
/**
* @return $this
*/
public function setPriority(string $priority)
{
$this->payload['priority'] = $priority;
return $this;
}
/**
* @return $this
*/
public function setLocationName(string $locationName)
{
$this->payload['location_name'] = $locationName;
return $this;
}
/**
* @return $this
*/
public function setLocationCode(int $locationCode)
{
$this->payload['location_code'] = $locationCode;
return $this;
}
/**
* @return $this
*/
public function setLocationCoordinate(string $locationCoordinate)
{
$this->payload['location_coordinate'] = $locationCoordinate;
return $this;
}
/**
* @return $this
*/
public function setLanguageName(string $languageName)
{
$this->payload['language_name'] = $languageName;
return $this;
}
/**
* @return $this
*/
public function setDevice(string $device)
{
$this->payload['device'] = $device;
return $this;
}
/**
* @return $this
*/
public function setOs(string $os)
{
$this->payload['os'] = $os;
return $this;
}
/**
* @return $this
*/
public function setSeDomain(string $seDomain)
{
$this->payload['se_domain'] = $seDomain;
return $this;
}
/**
* @return $this
*/
public function setDepth(int $depth)
{
$this->payload['depth'] = $depth;
return $this;
}
/**
* @return $this
*/
public function setSearchParam(string $searchParam)
{
$this->payload['search_param'] = $searchParam;
return $this;
}
/**
* @return $this
*/
public function setTag(string $tag)
{
$this->payload['tag'] = $tag;
return $this;
}
/**
* @return $this
*
* @throws \Exception
*/
public function setSeType(string $seType)
{
if (! in_array($seType, $this->seTypes)) {
throw new \Exception('Provided se type not allowed');
}
$this->requestToFunction = str_replace('{$seType}', $seType, $this->requestToFunction);
return $this;
}
/**
* @return $this
*/
public function setSe(string $seName)
{
$this->requestToFunction = str_replace('{$se}', $seName, $this->requestToFunction);
return $this;
}
public function get(): \DFSClientV3\Entity\Custom\SettingSerpLiveAdvancedEntityMain
{
return parent::get();
}
/**
* @return array
*
* @throws \Exception
*/
public static function getAfterMerge(array $modelPool)
{
return parent::getAfterMerge($modelPool); // TODO: Change the autogenerated stub
}
}

View File

@@ -0,0 +1,89 @@
<?php
namespace App\Http\Controllers\Front;
use App\Http\Controllers\Controller;
use App\Models\Post;
use Artesaos\SEOTools\Facades\SEOMeta;
use Artesaos\SEOTools\Facades\SEOTools;
use GrahamCampbell\Markdown\Facades\Markdown;
use Illuminate\Http\Request;
class FrontHomeController extends Controller
{
public function home(Request $request)
{
$featured_post = Post::where('status', 'publish')->orderBy('published_at', 'desc')->first();
$latest_posts = Post::where(function ($query) use ($featured_post) {
$query->whereNotIn('id', [$featured_post->id]);
})->where('status', 'publish')->orderBy('published_at', 'desc')->limit(5)->get();
return response(view('front.welcome', compact('featured_post', 'latest_posts')), 200);
}
public function terms(Request $request)
{
$markdown = file_get_contents(resource_path('markdown/terms.md'));
$title = 'Terms of Service';
$description = 'Our Terms of Service outline your rights, responsibilities, and the standards we uphold for a seamless experience.';
SEOTools::metatags();
SEOTools::twitter();
SEOTools::opengraph();
SEOTools::jsonLd();
SEOTools::setTitle($title);
SEOTools::setDescription($description);
SEOMeta::setRobots('noindex');
$content = Markdown::convert($markdown)->getContent();
//$content = $this->injectTailwindClasses($content);
return view('front.pages', compact('content', 'title', 'description'));
}
public function privacy(Request $request)
{
$markdown = file_get_contents(resource_path('markdown/privacy.md'));
$title = 'Privacy Policy';
$description = 'Our Privacy Policy provides clarity about the data we collect and how we use it, ensuring your peace of mind.';
SEOTools::metatags();
SEOTools::twitter();
SEOTools::opengraph();
SEOTools::jsonLd();
SEOTools::setTitle($title);
SEOTools::setDescription($description);
SEOMeta::setRobots('noindex');
$content = Markdown::convert($markdown)->getContent();
//$content = $this->injectTailwindClasses($content);
return view('front.pages', compact('content', 'title', 'description'));
}
public function disclaimer(Request $request)
{
$markdown = file_get_contents(resource_path('markdown/disclaimer.md'));
$title = 'Disclaimer';
$description = 'EchoScoop provides the content on this website purely for informational purposes and should not be interpreted as legal, financial, or medical guidance.';
SEOTools::metatags();
SEOTools::twitter();
SEOTools::opengraph();
SEOTools::jsonLd();
SEOTools::setTitle($title);
SEOTools::setDescription($description);
SEOMeta::setRobots('noindex');
$content = Markdown::convert($markdown)->getContent();
//$content = $this->injectTailwindClasses($content);
return view('front.pages', compact('content', 'title', 'description'));
}
}

View File

@@ -0,0 +1,27 @@
<?php
namespace App\Http\Controllers\Front;
use App\Http\Controllers\Controller;
use App\Models\Category;
use App\Models\Post;
use Illuminate\Http\Request;
class FrontListController extends Controller
{
public function index(Request $request)
{
$posts = Post::where('status', 'publish')->orderBy('published_at', 'desc')->simplePaginate(10) ?? collect();
return view('front.post_list', compact('posts'));
}
public function category(Request $request, $category_slug)
{
$category = Category::where('slug', $category_slug)->first();
$posts = $category?->posts()->where('status', 'publish')->orderBy('published_at', 'desc')->simplePaginate(10) ?? collect();
return view('front.post_list', compact('category', 'posts'));
}
}

View File

@@ -0,0 +1,132 @@
<?php
namespace App\Http\Controllers\Front;
use App\Http\Controllers\Controller;
use App\Models\Post;
use GrahamCampbell\Markdown\Facades\Markdown;
use Illuminate\Http\Request;
use Symfony\Component\DomCrawler\Crawler;
class FrontPostController extends Controller
{
public function index(Request $request, $slug)
{
$post = Post::where('slug', $slug)->where('status', 'publish')->first();
if (is_null($post)) {
return abort(404);
}
$content = Markdown::convert($post->body)->getContent();
//dd($content);
$content = $this->injectBootstrapClasses($content);
$content = $this->injectTableOfContents($content);
$content = $this->injectFeaturedImage($post, $content);
return view('front.single_post', compact('post', 'content'));
}
private function injectBootstrapClasses($content)
{
$crawler = new Crawler($content);
$crawler->filter('h1')->each(function (Crawler $node) {
$node->getNode(0)->setAttribute('class', trim($node->attr('class').' display-6 fw-bolder mt-3 mb-4'));
});
$crawler->filter('h2')->each(function (Crawler $node) {
$node->getNode(0)->setAttribute('class', trim($node->attr('class').'h4 mb-3'));
});
$crawler->filter('h3')->each(function (Crawler $node) {
$node->getNode(0)->setAttribute('class', trim($node->attr('class').'h6 mb-2'));
});
$crawler->filter('p')->each(function (Crawler $pNode) {
$precedingHeaders = $pNode->previousAll()->filter('h2');
// If there are no preceding <h2> tags, just process the <p>
if (! $precedingHeaders->count()) {
$existingClasses = $pNode->attr('class');
$newClasses = trim($existingClasses.' mt-2 mb-4');
$pNode->getNode(0)->setAttribute('class', $newClasses);
return;
}
$precedingHeader = $precedingHeaders->first();
if (trim($precedingHeader->text()) !== 'FAQs') {
$existingClasses = $pNode->attr('class');
$newClasses = trim($existingClasses.' mt-2 mb-4');
$pNode->getNode(0)->setAttribute('class', $newClasses);
}
if (strpos($pNode->text(), 'Q:') === 0) {
$currentClasses = $pNode->attr('class');
$newClasses = trim($currentClasses.' fw-bold');
$pNode->getNode(0)->setAttribute('class', $newClasses);
}
});
$crawler->filter('ul')->each(function (Crawler $node) {
$node->getNode(0)->setAttribute('class', trim($node->attr('class').'py-2'));
});
$crawler->filter('ol')->each(function (Crawler $node) {
$node->getNode(0)->setAttribute('class', trim($node->attr('class').'py-2'));
});
// Convert the modified DOM back to string
$modifiedContent = '';
foreach ($crawler as $domElement) {
$modifiedContent .= $domElement->ownerDocument->saveHTML($domElement);
}
return $modifiedContent;
}
private function injectTableOfContents($html)
{
$crawler = new Crawler($html);
// Create the Table of Contents
$toc = '<div class="p-3 rounded-3 bg-light mb-3"><ol>';
$crawler->filter('h2')->each(function (Crawler $node, $i) use (&$toc) {
$content = $node->text();
$id = 'link-'.$i; // Creating a simple id based on the index
$node->getNode(0)->setAttribute('id', $id); // Set the id to the h2 tag
$toc .= "<li class=\"py-1\"><a class=\"text-decoration-none hover-text-decoration-underline\" href='#{$id}'>{$content}</a></li>";
});
$toc .= '</ol></div>';
// Insert TOC after h1
$domDocument = $crawler->getNode(0)->ownerDocument;
$fragment = $domDocument->createDocumentFragment();
$fragment->appendXML($toc);
$h1Node = $crawler->filter('h1')->getNode(0);
$h1Node->parentNode->insertBefore($fragment, $h1Node->nextSibling);
// Get the updated HTML
$updatedHtml = $crawler->filter('body')->html();
return $updatedHtml;
}
private function injectFeaturedImage($post, $content)
{
if (! is_empty($post->featured_image)) {
$featured_image_alt = strtolower($post->short_title);
$featured_image_alt_caps = strtoupper($post->short_title);
$featured_image = "<div class=\"w-100 d-flex justify-content-center\"><figure class=\"text-center\"><img decoding=\"async\" class=\"img-fluid rounded mb-2 shadow\" src=\"{$post->featured_image_cdn}\" alt=\"{$featured_image_alt}\"><figcaption class=\"text-secondary small\">{$featured_image_alt_caps}</figcaption></figure></div>";
$content = preg_replace('/(<\/h1>)/', '$1'.$featured_image, $content, 1);
}
return $content;
}
}

View File

@@ -0,0 +1,37 @@
<?php
namespace App\Jobs;
use App\Jobs\Tasks\GenerateArticleFeaturedImageTask;
use Illuminate\Bus\Queueable;
use Illuminate\Contracts\Queue\ShouldQueue;
use Illuminate\Foundation\Bus\Dispatchable;
use Illuminate\Queue\InteractsWithQueue;
use Illuminate\Queue\SerializesModels;
class GenerateArticleFeaturedImageJob implements ShouldQueue
{
use Dispatchable, InteractsWithQueue, Queueable, SerializesModels;
protected $post;
public $timeout = 600;
/**
* Create a new job instance.
*/
public function __construct($post)
{
$this->post = $post;
}
/**
* Execute the job.
*/
public function handle(): void
{
if (! is_null($this->post)) {
GenerateArticleFeaturedImageTask::handle($this->post);
}
}
}

View File

@@ -0,0 +1,37 @@
<?php
namespace App\Jobs;
use App\Jobs\Tasks\GenerateArticleTask;
use Illuminate\Bus\Queueable;
use Illuminate\Contracts\Queue\ShouldQueue;
use Illuminate\Foundation\Bus\Dispatchable;
use Illuminate\Queue\InteractsWithQueue;
use Illuminate\Queue\SerializesModels;
class GenerateArticleJob implements ShouldQueue
{
use Dispatchable, InteractsWithQueue, Queueable, SerializesModels;
protected $serp_url;
public $timeout = 600;
/**
* Create a new job instance.
*/
public function __construct($serp_url)
{
$this->serp_url = $serp_url;
}
/**
* Execute the job.
*/
public function handle(): void
{
if (! is_null($this->serp_url)) {
GenerateArticleTask::handle($this->serp_url);
}
}
}

View File

@@ -0,0 +1,243 @@
<?php
namespace App\Jobs\Tasks;
use App\Helpers\FirstParty\OSSUploader\OSSUploader;
use App\Helpers\ThirdParty\DFS\SettingSerpLiveAdvanced;
use App\Models\NewsSerpResult;
use DFSClientV3\DFSClient;
use Exception;
use Illuminate\Support\Facades\Http;
use Image;
class GenerateArticleFeaturedImageTask
{
public static function handle($post)
{
$keyword = $post->main_keyword;
$title = $post->short_title;
$article_type = $post->type;
$country_iso = 'US';
$country_name = get_country_name_by_iso($country_iso);
$images = [];
$client = new DFSClient(
config('dataforseo.login'),
config('dataforseo.password'),
config('dataforseo.timeout'),
config('dataforseo.api_version'),
config('dataforseo.url'),
);
// You will receive SERP data specific to the indicated keyword, search engine, and location parameters
$serp_model = new SettingSerpLiveAdvanced();
$serp_model->setSe('google');
$serp_model->setSeType('images');
$serp_model->setKeyword($keyword);
$serp_model->setLocationName($country_name);
$serp_model->setDepth(100);
$serp_model->setLanguageCode('en');
$serp_res = $serp_model->getAsJson();
// try {
$serp_obj = json_decode($serp_res, false, 512, JSON_THROW_ON_ERROR);
//dd($serp_obj);
if ($serp_obj?->status_code == 20000) {
$json_file_name = config('platform.dataset.news.images_serp.file_prefix').str_slug($keyword).'-'.epoch_now_timestamp().'.json';
$upload_status = OSSUploader::uploadJson(
config('platform.dataset.news.images_serp.driver'),
config('platform.dataset.news.images_serp.path'),
$json_file_name,
$serp_obj);
if ($upload_status) {
$news_serp_result = new NewsSerpResult();
$news_serp_result->serp_provider = 'dfs';
$news_serp_result->serp_se = 'google';
$news_serp_result->serp_se_type = 'images';
$news_serp_result->serp_keyword = $keyword;
$news_serp_result->serp_country_iso = strtoupper($country_iso);
$news_serp_result->serp_cost = $serp_obj?->cost;
$news_serp_result->result_count = $serp_obj?->tasks[0]?->result[0]?->items_count;
$news_serp_result->filename = $json_file_name;
$news_serp_result->status = 'initial';
if ($news_serp_result->save()) {
$serp_items = $serp_obj?->tasks[0]?->result[0]?->items;
//dd($serp_items);
foreach ($serp_items as $item) {
if ($item->type == 'images_search') {
//dd($item);
$images[] = $item->source_url;
if (count($images) > 20) {
break;
}
}
}
}
//return $news_serp_result;
} else {
throw new Exception('Uploading failed', 1);
}
} else {
throw new Exception('Data failed', 1);
}
// } catch (Exception $e) {
// return null;
// }
$numImagesInCanvas = 2;
if ($numImagesInCanvas > count($images)) {
$numImagesInCanvas = count($images);
}
$canvasWidth = 720;
$canvasHeight = 405;
$canvas = Image::canvas($canvasWidth, $canvasHeight);
// Add Images
$imageWidth = $canvasWidth / $numImagesInCanvas;
// Process and place each image
$xOffset = 0; // Horizontal offset to place each image
for ($i = 0; $i < count($images); $i++) {
$url = $images[$i];
try {
$imageResponse = Http::timeout(300)->withHeaders([
'User-Agent' => 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/117.0.0.0 Safari/537.36',
])->get($url);
$imageContent = $imageResponse->body();
$image = Image::make($imageContent)
->resize(null, $canvasHeight, function ($constraint) {
$constraint->aspectRatio();
})
->resizeCanvas($imageWidth, $canvasHeight, 'center', false, [255, 255, 255, 0]);
//->blur(6)
$canvas->insert($image, 'top-left', $xOffset, 0);
$xOffset += $imageWidth;
} catch (Exception $e) {
continue;
}
}
///
$fontSize = 28;
$articleTypeFontSize = 24;
$padding = 15;
$fontPath = resource_path('fonts/Inter/Inter-Black.ttf');
// Split title into words and reconstruct lines
$words = explode(' ', $title);
$lines = [''];
$currentLineIndex = 0;
foreach ($words as $word) {
$potentialLine = $lines[$currentLineIndex] ? $lines[$currentLineIndex].' '.$word : $word;
$box = imagettfbbox($fontSize, 0, $fontPath, $potentialLine);
$textWidth = abs($box[2] - $box[0]);
if ($textWidth < $canvasWidth * 0.9) {
$lines[$currentLineIndex] = $potentialLine;
} else {
$currentLineIndex++;
$lines[$currentLineIndex] = $word;
}
}
// Calculate the dimensions of the article_type text
$articleTypeBox = imagettfbbox($articleTypeFontSize, 0, $fontPath, $article_type);
$articleTypeWidth = abs($articleTypeBox[2] - $articleTypeBox[0]);
$articleTypeHeight = abs($articleTypeBox[7] - $articleTypeBox[1]);
// Define the start Y position for the article type overlay
$articleOverlayStartY = $canvasHeight - ($fontSize * count($lines)) - ($articleTypeHeight + 4 * $padding);
// Create the blue overlay for the article type
$overlayWidth = $articleTypeWidth + 2 * $padding;
$canvas->rectangle(20, $articleOverlayStartY, 10 + $overlayWidth, $articleOverlayStartY + $articleTypeHeight + 2 * $padding, function ($draw) {
$draw->background([255, 255, 255, 0.8]);
});
// Overlay the article_type text within its overlay, centered horizontally and vertically
$textStartX = 20 + ($overlayWidth - $articleTypeWidth) / 2; // Center the text horizontally
$canvas->text(strtoupper($article_type), $textStartX, $articleOverlayStartY + ($articleTypeHeight + 2 * $padding) / 2, function ($font) use ($articleTypeFontSize, $fontPath) {
$font->file($fontPath);
$font->size($articleTypeFontSize);
$font->color('#0000FF');
$font->align('left');
$font->valign('middle'); // This ensures the text is vertically centered within the overlay
});
// Create the blue overlay for the title
$titleOverlayStartY = $articleOverlayStartY + $articleTypeHeight + 2 * $padding;
$canvas->rectangle(0, $titleOverlayStartY, $canvasWidth, $canvasHeight, function ($draw) {
$draw->background([0, 0, 255, 0.5]);
});
// Draw each line for the title
$yPosition = $titleOverlayStartY + $padding;
foreach ($lines as $line) {
$canvas->text($line, $canvasWidth / 2, $yPosition, function ($font) use ($fontSize, $fontPath) {
$font->file($fontPath);
$font->size($fontSize);
$font->color('#FFFFFF');
$font->align('center');
$font->valign('top');
});
$yPosition += $fontSize + $padding;
}
$filename = $post->slug.'-'.epoch_now_timestamp().'.jpg';
$ok = OSSUploader::uploadFile('r2', 'post_images/', $filename, (string) $canvas->stream('jpeg'));
// LQIP
// Clone the main image for LQIP version
$lqipImage = clone $canvas;
// Create the LQIP version of the image
$lqipImage->fit(10, 10, function ($constraint) {
$constraint->aspectRatio();
});
$lqipImage->encode('jpg', 5);
// LQIP filename
$lqip_filename = $post->slug.'-'.epoch_now_timestamp().'_lqip.jpg';
// Upload the LQIP version using OSSUploader
$lqip_ok = OSSUploader::uploadFile('r2', 'post_images/', $lqip_filename, (string) $lqipImage->stream('jpeg'));
if ($ok && $lqip_ok) {
$post->featured_image = 'post_images/'.$filename;
$post->status = 'publish';
$post->save();
return $post;
}
return null;
}
}

View File

@@ -0,0 +1,82 @@
<?php
namespace App\Jobs\Tasks;
use App\Helpers\FirstParty\OpenAI\OpenAI;
use App\Models\Author;
use App\Models\Post;
use App\Models\PostCategory;
use App\Models\SerpUrl;
use Exception;
class GenerateArticleTask
{
public static function handle(SerpUrl $serp_url)
{
$ai_titles = OpenAI::suggestArticleTitles($serp_url->title, $serp_url->description, 1);
if (is_null($ai_titles)) {
return self::saveAndReturnSerpProcessStatus($serp_url, -2);
}
$suggestion = null;
// dump($ai_titles);
try {
$random_key = array_rand($ai_titles?->suggestions, 1);
$suggestion = $ai_titles->suggestions[$random_key];
} catch (Exception $e) {
return self::saveAndReturnSerpProcessStatus($serp_url, -1);
}
if (is_null($suggestion)) {
return self::saveAndReturnSerpProcessStatus($serp_url, -3);
}
$markdown = OpenAI::writeArticle($suggestion->title, $suggestion->description, $suggestion->article_type, 500, 800);
if (is_empty($markdown)) {
return self::saveAndReturnSerpProcessStatus($serp_url, -4);
}
$post = new Post;
$post->title = $suggestion->title;
$post->type = $suggestion->article_type;
$post->short_title = $ai_titles->short_title;
$post->main_keyword = $ai_titles->main_keyword;
$post->keywords = $suggestion->photo_keywords;
$post->slug = str_slug($suggestion->title);
$post->excerpt = $suggestion->description;
$post->author_id = Author::find(1)->id;
$post->featured = false;
$post->featured_image = null;
$post->body = $markdown;
$post->status = 'draft';
if ($post->save()) {
$post_category = new PostCategory;
$post_category->post_id = $post->id;
$post_category->category_id = $serp_url->category->id;
if ($post_category->save()) {
return self::saveAndReturnSerpProcessStatus($serp_url, 1);
} else {
return self::saveAndReturnSerpProcessStatus($serp_url, -5);
}
}
return self::saveAndReturnSerpProcessStatus($serp_url, -6);
}
private static function saveAndReturnSerpProcessStatus($serp_url, $process_status)
{
$serp_url->process_status = $process_status;
$serp_url->save();
return $serp_url->process_status;
}
}

View File

@@ -0,0 +1,79 @@
<?php
namespace App\Jobs\Tasks;
use App\Helpers\FirstParty\OSSUploader\OSSUploader;
use App\Helpers\ThirdParty\DFS\SettingSerpLiveAdvanced;
use App\Models\Category;
use App\Models\NewsSerpResult;
use DFSClientV3\DFSClient;
use Exception;
class GetNewsSerpTask
{
public static function handle(Category $category, $country_iso)
{
$country_name = get_country_name_by_iso($country_iso);
$keyword = strtolower("{$category->name}");
$client = new DFSClient(
config('dataforseo.login'),
config('dataforseo.password'),
config('dataforseo.timeout'),
config('dataforseo.api_version'),
config('dataforseo.url'),
);
// You will receive SERP data specific to the indicated keyword, search engine, and location parameters
$serp_model = new SettingSerpLiveAdvanced();
$serp_model->setSe('google');
$serp_model->setSeType('news');
$serp_model->setKeyword($keyword);
$serp_model->setLocationName($country_name);
$serp_model->setDepth(100);
$serp_model->setLanguageCode('en');
$serp_res = $serp_model->getAsJson();
try {
$serp_obj = json_decode($serp_res, false, 512, JSON_THROW_ON_ERROR);
if ($serp_obj?->status_code == 20000) {
$json_file_name = config('platform.dataset.news.news_serp.file_prefix').str_slug($category->name).'-'.epoch_now_timestamp().'.json';
$upload_status = OSSUploader::uploadJson(
config('platform.dataset.news.news_serp.driver'),
config('platform.dataset.news.news_serp.path'),
$json_file_name,
$serp_obj);
if ($upload_status) {
$news_serp_result = new NewsSerpResult;
$news_serp_result->category_id = $category->id;
$news_serp_result->category_name = $category->name;
$news_serp_result->serp_provider = 'dfs';
$news_serp_result->serp_se = 'google';
$news_serp_result->serp_se_type = 'news';
$news_serp_result->serp_keyword = $keyword;
$news_serp_result->serp_country_iso = strtoupper($country_iso);
$news_serp_result->serp_cost = $serp_obj?->cost;
$news_serp_result->result_count = $serp_obj?->tasks[0]?->result[0]?->items_count;
$news_serp_result->filename = $json_file_name;
$news_serp_result->status = 'initial';
if ($news_serp_result->save()) {
$category->serp_at = now();
$category->save();
}
return $news_serp_result;
} else {
throw new Exception('Uploading failed', 1);
}
}
} catch (Exception $e) {
return null;
}
}
}

View File

@@ -0,0 +1,146 @@
<?php
namespace App\Jobs\Tasks;
use App\Helpers\FirstParty\OSSUploader\OSSUploader;
use App\Models\Category;
use App\Models\NewsSerpResult;
use App\Models\SerpUrl;
use Carbon\Carbon;
use Exception;
class ParseNewsSerpDomainsTask
{
public static function handle(NewsSerpResult $news_serp_result, $serp_counts = 1)
{
//dd($news_serp_result->category->serp_at);
$serp_results = null;
$success = false;
try {
$serp_results = OSSUploader::readJson(
config('platform.dataset.news.news_serp.driver'),
config('platform.dataset.news.news_serp.path'),
$news_serp_result->filename)?->tasks[0]?->result[0]?->items;
} catch (Exception $e) {
$serp_results = null;
}
if (! is_null($serp_results)) {
$valid_serps = [];
foreach ($serp_results as $serp_item) {
$news_date = Carbon::parse($serp_item->timestamp);
if (is_empty($serp_item->url)) {
continue;
}
// if (!str_contains($serp_item->time_published, "hours"))
// {
// continue;
// }
$serp_url = SerpUrl::where('url', $serp_item->url)->first();
if (! is_null($serp_url)) {
if ($serp_url->status == 'blocked') {
continue;
}
}
if (str_contains($serp_item->title, ':')) {
continue;
}
$valid_serps[] = $serp_item;
if (count($valid_serps) >= $serp_counts) {
break;
}
}
//dd($valid_serps);
foreach ($valid_serps as $serp_item) {
//dd($serp_item);
if (is_null($serp_url)) {
$serp_url = new SerpUrl;
$serp_url->category_id = $news_serp_result->category_id;
$serp_url->category_name = $news_serp_result->category_name;
$serp_url->news_serp_result_id = $news_serp_result->id;
}
$serp_url->source = 'serp';
$serp_url->url = self::normalizeUrl($serp_item->url);
$serp_url->country_iso = $news_serp_result->serp_country_iso;
if (! is_empty($serp_item->title)) {
$serp_url->title = $serp_item->title;
}
if (! is_empty($serp_item->snippet)) {
$serp_url->description = $serp_item->snippet;
}
if ($serp_url->isDirty()) {
$serp_url->serp_at = $news_serp_result->category->serp_at;
}
if ($serp_url->save()) {
$success = true;
}
}
}
return $success;
}
private static function normalizeUrl($url)
{
try {
$parsedUrl = parse_url($url);
// Force the scheme to https to avoid duplicate content issues
$parsedUrl['scheme'] = 'https';
if (! isset($parsedUrl['host'])) {
// If the host is not present, throw an exception
throw new \Exception('Host not found in URL');
}
// Check if the path is set and ends with a trailing slash, if so, remove it
if (isset($parsedUrl['path']) && substr($parsedUrl['path'], -1) === '/') {
$parsedUrl['path'] = rtrim($parsedUrl['path'], '/');
}
// Remove query parameters
unset($parsedUrl['query']);
$normalizedUrl = sprintf(
'%s://%s%s',
$parsedUrl['scheme'],
$parsedUrl['host'],
$parsedUrl['path'] ?? ''
);
// Remove fragment if exists
$normalizedUrl = preg_replace('/#.*$/', '', $normalizedUrl);
return $normalizedUrl;
} catch (\Exception $e) {
// In case of an exception, return the original URL
return $url;
}
}
}

47
app/Models/Author.php Normal file
View File

@@ -0,0 +1,47 @@
<?php
/**
* Created by Reliese Model.
*/
namespace App\Models;
use Carbon\Carbon;
use Illuminate\Database\Eloquent\Collection;
use Illuminate\Database\Eloquent\Model;
/**
* Class Author
*
* @property int $id
* @property string $name
* @property string $avatar
* @property string $bio
* @property bool $enabled
* @property bool $public
* @property Carbon|null $created_at
* @property Carbon|null $updated_at
* @property Collection|Post[] $posts
*/
class Author extends Model
{
protected $table = 'authors';
protected $casts = [
'enabled' => 'bool',
'public' => 'bool',
];
protected $fillable = [
'name',
'avatar',
'bio',
'enabled',
'public',
];
public function posts()
{
return $this->hasMany(Post::class);
}
}

View File

@@ -16,6 +16,7 @@
*
* @property int $id
* @property string $name
* @property string $short_name
* @property string|null $slug
* @property bool $enabled
* @property Carbon|null $created_at
@@ -39,6 +40,7 @@ class Category extends Model
protected $fillable = [
'name',
'short_name',
'slug',
'enabled',
'_lft',
@@ -64,4 +66,16 @@ public function saveQuietly(array $options = [])
return $this->save($options);
});
}
public function posts()
{
return $this->hasManyThrough(
Post::class,
PostCategory::class,
'category_id', // Foreign key on PostCategory table
'id', // Local key on Post table
'id', // Local key on Category table
'post_id' // Foreign key on PostCategory table
);
}
}

View File

@@ -0,0 +1,59 @@
<?php
/**
* Created by Reliese Model.
*/
namespace App\Models;
use Carbon\Carbon;
use Illuminate\Database\Eloquent\Model;
/**
* Class NewsSerpResult
*
* @property int $id
* @property int $category_id
* @property string $category_name
* @property string $serp_provider
* @property string $serp_se
* @property string $serp_se_type
* @property string $serp_keyword
* @property string $serp_country_iso
* @property float|null $serp_cost
* @property int|null $result_count
* @property string $filename
* @property string $status
* @property Carbon|null $created_at
* @property Carbon|null $updated_at
* @property Category $category
*/
class NewsSerpResult extends Model
{
protected $table = 'news_serp_results';
protected $casts = [
'category_id' => 'int',
'serp_cost' => 'float',
'result_count' => 'int',
];
protected $fillable = [
'category_id',
'category_name',
'serp_provider',
'serp_se',
'serp_se_type',
'serp_keyword',
'serp_country_iso',
'serp_cost',
'result_count',
'filename',
'status',
];
public function category()
{
return $this->belongsTo(Category::class);
}
}

123
app/Models/Post.php Normal file
View File

@@ -0,0 +1,123 @@
<?php
/**
* Created by Reliese Model.
*/
namespace App\Models;
use Carbon\Carbon;
use Illuminate\Database\Eloquent\Collection;
use Illuminate\Database\Eloquent\Model;
use Illuminate\Support\Facades\Storage;
use Spatie\Feed\Feedable;
use Spatie\Feed\FeedItem;
/**
* Class Post
*
* @property int $id
* @property string|null $title
* @property string|null $slug
* @property string|null $type
* @property string|null $excerpt
* @property int|null $author_id
* @property bool $featured
* @property string|null $featured_image
* @property string|null $body
* @property int $views_count
* @property string $status
* @property Carbon|null $created_at
* @property Carbon|null $updated_at
* @property Author|null $author
* @property Collection|PostCategory[] $post_categories
* @property Carbon $published_at
*/
class Post extends Model implements Feedable
{
protected $table = 'posts';
protected $casts = [
'author_id' => 'int',
'featured' => 'bool',
'views_count' => 'int',
'keywords' => 'array',
'published_at' => 'datetime',
];
protected $fillable = [
'title',
'short_title',
'slug',
'type',
'excerpt',
'author_id',
'featured',
'featured_image',
'body',
'views_count',
'status',
'main_keyword',
'keywords',
'published_at',
];
public function getFeaturedImageLqipCdnAttribute()
{
if (! is_empty($this->featured_image)) {
// Get the extension of the original featured image
$extension = pathinfo($this->featured_image, PATHINFO_EXTENSION);
// Append "_lqip" before the extension to create the LQIP image URL
$lqipFeaturedImage = str_replace(".{$extension}", "_lqip.{$extension}", $this->featured_image);
return 'https://'.Storage::disk('r2')->url($lqipFeaturedImage);
}
return null;
}
public function getFeaturedImageCdnAttribute()
{
if (! is_empty($this->featured_image)) {
return 'https://'.Storage::disk('r2')->url($this->featured_image);
}
return null;
}
public function author()
{
return $this->belongsTo(Author::class);
}
public function category()
{
return $this->hasOneThrough(
Category::class, // The target model
PostCategory::class, // The through model
'post_id', // The foreign key on the through model
'id', // The local key on the parent model (Post)
'id', // The local key on the through model (PostCategory)
'category_id' // The foreign key on the target model (Category)
);
}
public function toFeedItem(): FeedItem
{
return FeedItem::create([
'id' => $this->id,
'title' => $this->title,
'summary' => $this->excerpt,
'updated' => $this->updated_at,
'link' => route('posts.show', $this->slug),
'authorName' => optional($this->author)->name,
]);
}
public static function getFeedItems()
{
return self::where('status', 'published')->latest('published_at')->take(10)->get();
}
}

View File

@@ -0,0 +1,46 @@
<?php
/**
* Created by Reliese Model.
*/
namespace App\Models;
use Carbon\Carbon;
use Illuminate\Database\Eloquent\Model;
/**
* Class PostCategory
*
* @property int $id
* @property int $post_id
* @property int $category_id
* @property Carbon|null $created_at
* @property Carbon|null $updated_at
* @property Post $post
* @property Category $category
*/
class PostCategory extends Model
{
protected $table = 'post_categories';
protected $casts = [
'post_id' => 'int',
'category_id' => 'int',
];
protected $fillable = [
'post_id',
'category_id',
];
public function post()
{
return $this->belongsTo(Post::class);
}
public function category()
{
return $this->belongsTo(Category::class);
}
}

65
app/Models/SerpUrl.php Normal file
View File

@@ -0,0 +1,65 @@
<?php
/**
* Created by Reliese Model.
*/
namespace App\Models;
use Carbon\Carbon;
use Illuminate\Database\Eloquent\Model;
/**
* Class SerpUrl
*
* @property int $id
* @property int $news_serp_result_id
* @property int $category_id
* @property string $category_name
* @property string $source
* @property string $url
* @property string $country_iso
* @property string|null $title
* @property string|null $description
* @property Carbon|null $serp_at
* @property string $status
* @property Carbon|null $created_at
* @property Carbon|null $updated_at
* @property NewsSerpResult $news_serp_result
* @property Category $category
*/
class SerpUrl extends Model
{
protected $table = 'serp_urls';
protected $casts = [
'news_serp_result_id' => 'int',
'category_id' => 'int',
'process_status' => 'int',
'serp_at' => 'datetime',
];
protected $fillable = [
'news_serp_result_id',
'category_id',
'category_name',
'source',
'url',
'country_iso',
'title',
'description',
'process_status',
'serp_at',
'status',
];
public function news_serp_result()
{
return $this->belongsTo(NewsSerpResult::class);
}
public function category()
{
return $this->belongsTo(Category::class);
}
}

View File

@@ -35,6 +35,10 @@ public function boot(): void
Route::middleware('web')
->group(base_path('routes/web.php'));
Route::middleware('web')
->prefix('tests')
->group(base_path('routes/tests.php'));
});
}
}

View File

@@ -0,0 +1,32 @@
<?php
namespace App\Providers;
use App\Views\Composers\ParentCategoryComposer;
use Illuminate\Support\Facades\View;
use Illuminate\Support\ServiceProvider;
class ViewServiceProvider extends ServiceProvider
{
/**
* Register any application services.
*
* @return void
*/
public function register()
{
//
}
/**
* Bootstrap any application services.
*
* @return void
*/
public function boot()
{
// Using class based composers...
View::composer('front.layouts.partials.nav', ParentCategoryComposer::class);
}
}

View File

@@ -0,0 +1,14 @@
<?php
namespace App\Views\Composers;
use App\Models\Category;
use Illuminate\View\View;
class ParentCategoryComposer
{
public function compose(View $view)
{
$view->with('parent_categories', Category::whereNull('parent_id')->get());
}
}