PageRenderTime 62ms CodeModel.GetById 25ms RepoModel.GetById 0ms app.codeStats 0ms

/model/story.php

https://github.com/NeoRazorX/feedstorm
PHP | 742 lines | 608 code | 105 blank | 29 comment | 88 complexity | 6145fd21b0cd6a809d1885c64570f8aa MD5 | raw file
Possible License(s): LGPL-3.0
  1. <?php
  2. /*
  3. * This file is part of FeedStorm
  4. * Copyright (C) 2014 Carlos Garcia Gomez neorazorx@gmail.com
  5. *
  6. * This program is free software: you can redistribute it and/or modify
  7. * it under the terms of the GNU Affero General Public License as
  8. * published by the Free Software Foundation, either version 3 of the
  9. * License, or (at your option) any later version.
  10. *
  11. * This program is distributed in the hope that it will be useful,
  12. * but WITHOUT ANY WARRANTY; without even the implied warranty of
  13. * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
  14. * GNU Affero General Public License for more details.
  15. *
  16. * You should have received a copy of the GNU Affero General Public License
  17. * along with this program. If not, see <http://www.gnu.org/licenses/>.
  18. */
  19. require_once 'base/fs_model.php';
  20. require_once 'model/comment.php';
  21. require_once 'model/feed_story.php';
  22. require_once 'model/story_edition.php';
  23. require_once 'model/topic_story.php';
  24. class story extends fs_model
  25. {
  26. /*
  27. * Google prefiere urls del tipo:
  28. * http://www.locierto.es/story/titulo-de-la-noticia.html
  29. * así que $name = 'titulo-de-la-noticia.html'
  30. */
  31. public $name;
  32. public $date;
  33. public $published; /// sólo para las publicadas
  34. public $title;
  35. public $description;
  36. public $link;
  37. public $clics;
  38. public $tweets;
  39. public $meneos;
  40. public $likes;
  41. public $plusones;
  42. public $popularity;
  43. public $native_lang; /// ¿El artículo está en español?
  44. public $parody;
  45. public $penalize;
  46. public $featured;
  47. public $keywords;
  48. public $topics;
  49. public $related_id;
  50. public $edition_id;
  51. public $num_editions;
  52. public $num_feeds;
  53. public $num_comments;
  54. public function __construct($item=FALSE)
  55. {
  56. parent::__construct('stories');
  57. $this->id = NULL;
  58. $this->name = '';
  59. $this->date = time();
  60. $this->published = NULL;
  61. $this->title = NULL;
  62. $this->description = NULL;
  63. $this->link = NULL;
  64. $this->clics = 0;
  65. $this->tweets = 0;
  66. $this->meneos = 0;
  67. $this->likes = 0;
  68. $this->plusones = 0;
  69. $this->popularity = 0;
  70. $this->native_lang = TRUE;
  71. $this->parody = FALSE;
  72. $this->penalize = FALSE;
  73. $this->featured = FALSE;
  74. $this->keywords = '';
  75. $this->topics = array();
  76. $this->related_id = NULL;
  77. $this->edition_id = NULL;
  78. $this->num_editions = 0;
  79. $this->num_feeds = 0;
  80. $this->num_comments = 0;
  81. if($item)
  82. {
  83. $this->id = $item['_id'];
  84. $this->name = $item['name'];
  85. $this->date = $item['date'];
  86. if( isset($item['published']) )
  87. $this->published = $item['published'];
  88. $this->title = $item['title'];
  89. $this->description = $item['description'];
  90. $this->link = $item['link'];
  91. $this->clics = $item['clics'];
  92. $this->tweets = $item['tweets'];
  93. $this->meneos = $item['meneos'];
  94. $this->likes = $item['likes'];
  95. $this->plusones = $item['plusones'];
  96. $this->popularity = $item['popularity'];
  97. $this->native_lang = $item['native_lang'];
  98. if( isset($item['parody']) )
  99. $this->parody = $item['parody'];
  100. if( isset($item['penalize']) )
  101. $this->penalize = $item['penalize'];
  102. if( isset($item['featured']) )
  103. $this->featured = $item['featured'];
  104. if( isset($item['topics']) )
  105. {
  106. $this->topics = array_unique($item['topics']);
  107. $this->keywords = $item['keywords'];
  108. $this->related_id = $item['related_id'];
  109. }
  110. if( isset($item['edition_id']) )
  111. $this->edition_id = $item['edition_id'];
  112. if( isset($item['num_editions']) )
  113. $this->num_editions = $item['num_editions'];
  114. if( isset($item['num_feeds']) )
  115. $this->num_feeds = $item['num_feeds'];
  116. if( isset($item['num_comments']) )
  117. $this->num_comments = $item['num_comments'];
  118. }
  119. }
  120. public function install_indexes()
  121. {
  122. $this->collection->ensureIndex( array('popularity' => -1) );
  123. $this->collection->ensureIndex( array('date' => -1) );
  124. $this->collection->ensureIndex( array('published' => -1) );
  125. $this->collection->ensureIndex('link');
  126. $this->collection->ensureIndex('name');
  127. }
  128. public function url($w3c = TRUE)
  129. {
  130. if( is_null($this->id) )
  131. return FS_PATH.'index.php';
  132. else if($this->name != '')
  133. return FS_PATH.'show_story/'.$this->name;
  134. else if($w3c)
  135. return FS_PATH.'index.php?page=show_story&amp;id='.$this->id;
  136. else
  137. return FS_PATH.'index.php?page=show_story&id='.$this->id;
  138. }
  139. public function link()
  140. {
  141. if( is_null($this->id) )
  142. return $this->url();
  143. else
  144. return $this->link;
  145. }
  146. public function edit_url()
  147. {
  148. if( is_null($this->id) )
  149. return FS_PATH.'index.php';
  150. else
  151. return FS_PATH.'edit_story/'.$this->id;
  152. }
  153. public function show_date($iso=FALSE)
  154. {
  155. if($iso)
  156. return Date('c', $this->date);
  157. else
  158. return Date('Y-m-d H:m', $this->date);
  159. }
  160. public function timesince($published=FALSE)
  161. {
  162. if($published)
  163. return $this->time2timesince($this->published);
  164. else
  165. return $this->time2timesince($this->date);
  166. }
  167. public function popularity()
  168. {
  169. return number_format($this->popularity, 2, ',', ' ');
  170. }
  171. public function feed_links()
  172. {
  173. $feed_story = new feed_story();
  174. $feed_links = $feed_story->all4story($this->id);
  175. if($this->num_feeds != count($feed_links))
  176. {
  177. $this->num_feeds = count($feed_links);
  178. $this->save();
  179. }
  180. return $feed_links;
  181. }
  182. public function editions()
  183. {
  184. $edition = new story_edition();
  185. $editions = $edition->all4story($this->id);
  186. if($this->num_editions != count($editions))
  187. {
  188. $this->num_editions = count($editions);
  189. $this->save();
  190. }
  191. return $editions;
  192. }
  193. public function comments()
  194. {
  195. $comment = new comment();
  196. $comments = array_reverse($comment->all4thread($this->id));
  197. if($this->num_comments != count($comments))
  198. {
  199. $this->num_comments = count($comments);
  200. $this->save();
  201. }
  202. return $comments;
  203. }
  204. public function related_story()
  205. {
  206. if( isset($this->related_id) )
  207. return $this->get($this->related_id);
  208. else
  209. return FALSE;
  210. }
  211. public function description($width=250)
  212. {
  213. return $this->true_text_break($this->description, $width);
  214. }
  215. public function description_uncut()
  216. {
  217. return $this->uncut($this->description);
  218. }
  219. public function description_plus($exclude = FALSE)
  220. {
  221. $desc = $this->description;
  222. if($this->keywords != '')
  223. {
  224. $no_keys = array();
  225. if($exclude)
  226. $no_keys = explode(', ', $exclude);
  227. $keys = explode(', ', $this->keywords);
  228. if($keys)
  229. {
  230. foreach($keys as $k)
  231. {
  232. if( !in_array($k, $no_keys) )
  233. {
  234. $desc = preg_replace_callback("#\b".$k."\b#iu", function($coincidencia) {
  235. return '<b>'.$coincidencia[0].'</b>';
  236. }, $desc);
  237. }
  238. }
  239. }
  240. }
  241. return $desc;
  242. }
  243. private function calculate_popularity()
  244. {
  245. $tclics = $this->clics;
  246. if($this->native_lang AND !$this->penalize AND mb_strlen($this->description) > 0)
  247. {
  248. if(mb_strlen($this->description) > 70)
  249. $tclics += $this->num_editions + $this->num_feeds + $this->num_comments + count($this->topics);
  250. if($this->related_id)
  251. $tclics++;
  252. if( mb_strlen($this->description) > 250 )
  253. $tclics += 2 * count($this->topics);
  254. if($this->tweets > 1000)
  255. $tclics += min( array($this->tweets, 10 + 2*$this->clics) );
  256. else
  257. $tclics += min( array($this->tweets, 1 + $this->clics) );
  258. if($this->likes > 1000)
  259. $tclics += min( array($this->likes, 10 + 2*$this->clics) );
  260. else
  261. $tclics += min( array($this->likes, 1 + $this->clics) );
  262. if($this->meneos > 150)
  263. $tclics += min( array($this->meneos, 10 + 2*$this->clics) );
  264. else
  265. $tclics += min( array($this->meneos, 1 + $this->clics) );
  266. $tclics += min( array($this->plusones, 1 + 2*$this->clics) );
  267. }
  268. $dias = 1 + intval( (time() - $this->date) / 86400 );
  269. $semanas = pow(2, intval($dias/7));
  270. if($tclics > 0)
  271. $this->popularity = $tclics / ($dias * $semanas);
  272. else
  273. $this->popularity = 0;
  274. }
  275. public function max_popularity()
  276. {
  277. $total = 0;
  278. if($this->native_lang AND !$this->penalize AND !$this->parody AND mb_strlen($this->description) > 0)
  279. {
  280. $total = $this->clics + $this->tweets + $this->likes + $this->meneos + $this->plusones;
  281. }
  282. return $total;
  283. }
  284. public function readed()
  285. {
  286. return isset($_COOKIE['s_'.$this->id]);
  287. }
  288. public function read()
  289. {
  290. if( !isset($_COOKIE['s_'.$this->id]) )
  291. {
  292. /*
  293. * Eliminamos cookies para no sobrepasar el límite
  294. */
  295. if( count($_COOKIE) > 50 )
  296. {
  297. $num = 10;
  298. foreach($_COOKIE as $k => $value)
  299. {
  300. if($num <= 0)
  301. break;
  302. else if( substr($k, 0, 2) == 's_' )
  303. {
  304. setcookie($k, $value, time()-86400, FS_PATH);
  305. $num--;
  306. }
  307. }
  308. }
  309. setcookie('s_'.$this->id, $this->id, time()+86400, FS_PATH);
  310. }
  311. }
  312. public function tweet_count()
  313. {
  314. $json_string = $this->curl_download('http://urls.api.twitter.com/1/urls/count.json?url='.
  315. rawurlencode($this->link), FALSE);
  316. $json = json_decode($json_string, TRUE);
  317. $tweets = isset($json['count']) ? intval($json['count']) : 0;
  318. if($tweets > $this->tweets)
  319. $this->tweets = $tweets;
  320. }
  321. public function facebook_count()
  322. {
  323. $json_string = $this->curl_download('http://api.facebook.com/restserver.php?method=links.getStats&format=json&urls='.
  324. rawurlencode($this->link), FALSE);
  325. $json = json_decode($json_string, TRUE);
  326. $likes = isset($json[0]['total_count']) ? intval($json[0]['total_count']) : 0;
  327. if($likes > $this->likes)
  328. $this->likes = $likes;
  329. }
  330. public function meneame_count()
  331. {
  332. $string = $this->curl_download('http://www.meneame.net/api/url.php?url='.rawurlencode($this->link), FALSE);
  333. $vars = explode( ' ', $string);
  334. if( count($vars) == 4 )
  335. {
  336. $meneos = intval( $vars[2] );
  337. if($meneos > $this->meneos)
  338. $this->meneos = $meneos;
  339. }
  340. }
  341. public function plusones_count()
  342. {
  343. $curl = curl_init();
  344. curl_setopt($curl, CURLOPT_URL, "https://clients6.google.com/rpc");
  345. curl_setopt($curl, CURLOPT_POST, TRUE);
  346. curl_setopt($curl, CURLOPT_SSL_VERIFYPEER, FALSE);
  347. curl_setopt($curl, CURLOPT_POSTFIELDS, '[{"method":"pos.plusones.get","id":"p","params":{"nolog":true,"id":"'.
  348. rawurldecode($this->link).'","source":"widget","userId":"@viewer","groupId":"@self"},"jsonrpc":"2.0","key":"p","apiVersion":"v1"}]');
  349. curl_setopt($curl, CURLOPT_RETURNTRANSFER, TRUE);
  350. curl_setopt($curl, CURLOPT_HTTPHEADER, array('Content-type: application/json'));
  351. curl_setopt($curl, CURLOPT_TIMEOUT, FS_TIMEOUT);
  352. $curl_results = curl_exec ($curl);
  353. curl_close ($curl);
  354. $json = json_decode($curl_results, TRUE);
  355. $plusones = isset($json[0]['result']['metadata']['globalCounts']['count'])?intval( $json[0]['result']['metadata']['globalCounts']['count'] ):0;
  356. if($plusones > $this->plusones)
  357. $this->plusones = $plusones;
  358. }
  359. public function random_count($meneame = TRUE)
  360. {
  361. if( isset($this->link) AND $this->native_lang AND !$this->penalize )
  362. {
  363. switch( mt_rand(0, 3) )
  364. {
  365. case 0:
  366. $this->tweet_count();
  367. break;
  368. case 1:
  369. $this->facebook_count();
  370. break;
  371. case 2:
  372. if($meneame)
  373. $this->meneame_count();
  374. else if($this->likes == 0)
  375. $this->facebook_count();
  376. else if($this->tweets == 0)
  377. $this->tweet_count();
  378. else
  379. $this->plusones_count();
  380. break;
  381. default:
  382. $this->plusones_count();
  383. break;
  384. }
  385. }
  386. }
  387. public function get($id)
  388. {
  389. $this->add2history(__CLASS__.'::'.__FUNCTION__);
  390. try
  391. {
  392. if( substr($id, -5) == '.html' )
  393. {
  394. $data = $this->collection->findone( array('name' => $id) );
  395. if($data)
  396. return new story($data);
  397. else
  398. {
  399. /// buscamos la raiz
  400. $parts = explode('-', substr($id, 0, -5));
  401. $new_name = '';
  402. for($i = 0; $i < count($parts)-1; $i++)
  403. $new_name .= $parts[$i].'-';
  404. $data = $this->collection->findone( array('name' => new MongoRegex('/'.$new_name.'/')) );
  405. if($data)
  406. return new story($data);
  407. else
  408. return FALSE;
  409. }
  410. }
  411. else
  412. {
  413. $data = $this->collection->findone( array('_id' => new MongoId($id)) );
  414. if($data)
  415. return new story($data);
  416. else
  417. return FALSE;
  418. }
  419. }
  420. catch(Exception $e)
  421. {
  422. $this->new_error($e);
  423. return FALSE;
  424. }
  425. }
  426. public function get_by_link($url)
  427. {
  428. $this->add2history(__CLASS__.'::'.__FUNCTION__);
  429. $data = $this->collection->findone( array('link' => $url) );
  430. if($data)
  431. return new story($data);
  432. else
  433. return FALSE;
  434. }
  435. public function exists()
  436. {
  437. if( is_null($this->id) )
  438. return FALSE;
  439. else
  440. {
  441. $this->add2history(__CLASS__.'::'.__FUNCTION__);
  442. $data = $this->collection->findone( array('_id' => $this->id) );
  443. if($data)
  444. return TRUE;
  445. else
  446. return FALSE;
  447. }
  448. }
  449. public function save()
  450. {
  451. $this->title = $this->true_text_break($this->title, 140, 18);
  452. $this->description = $this->true_text_break($this->description, 999, 25);
  453. $this->related_id = $this->var2str($this->related_id);
  454. $this->edition_id = $this->var2str($this->edition_id);
  455. $this->calculate_popularity();
  456. $data = array(
  457. 'name' => $this->name,
  458. 'date' => $this->date,
  459. 'published' => $this->published,
  460. 'title' => $this->title,
  461. 'description' => $this->description,
  462. 'link' => $this->link,
  463. 'clics' => $this->clics,
  464. 'tweets' => $this->tweets,
  465. 'meneos' => $this->meneos,
  466. 'likes' => $this->likes,
  467. 'plusones' => $this->plusones,
  468. 'popularity' => $this->popularity,
  469. 'native_lang' => $this->native_lang,
  470. 'parody' => $this->parody,
  471. 'penalize' => $this->penalize,
  472. 'featured' => $this->featured,
  473. 'keywords' => $this->keywords,
  474. 'topics' => $this->topics,
  475. 'related_id' => $this->related_id,
  476. 'edition_id' => $this->edition_id,
  477. 'num_editions' => $this->num_editions,
  478. 'num_feeds' => $this->num_feeds,
  479. 'num_comments' => $this->num_comments
  480. );
  481. if( $this->exists() )
  482. {
  483. $this->add2history(__CLASS__.'::'.__FUNCTION__.'@update');
  484. $filter = array('_id' => $this->id);
  485. $this->collection->update($filter, $data);
  486. }
  487. else
  488. {
  489. $this->add2history(__CLASS__.'::'.__FUNCTION__.'@insert');
  490. if($this->name == '')
  491. $data['name'] = $this->new_name();
  492. $this->collection->insert($data);
  493. $this->id = $data['_id'];
  494. }
  495. }
  496. public function new_name()
  497. {
  498. $this->name = strtolower( $this->true_text_break($this->title, 85) );
  499. $changes = array('/à/' => 'a', '/á/' => 'a', '/â/' => 'a', '/ã/' => 'a', '/ä/' => 'a',
  500. '/å/' => 'a', '/æ/' => 'ae', '/ç/' => 'c', '/è/' => 'e', '/é/' => 'e', '/ê/' => 'e',
  501. '/ë/' => 'e', '/ì/' => 'i', '/í/' => 'i', '/î/' => 'i', '/ï/' => 'i', '/ð/' => 'd',
  502. '/ñ/' => 'n', '/ò/' => 'o', '/ó/' => 'o', '/ô/' => 'o', '/õ/' => 'o', '/ö/' => 'o',
  503. '/ő/' => 'o', '/ø/' => 'o', '/ù/' => 'u', '/ú/' => 'u', '/û/' => 'u', '/ü/' => 'u',
  504. '/ű/' => 'u', '/ý/' => 'y', '/þ/' => 'th', '/ÿ/' => 'y', '/ñ/' => 'ny',
  505. '/&quot;/' => '-'
  506. );
  507. $this->name = preg_replace(array_keys($changes), $changes, $this->name);
  508. $this->name = preg_replace('/[^a-z0-9]/i', '-', $this->name);
  509. $this->name = preg_replace('/-+/', '-', $this->name);
  510. if( substr($this->name, 0, 1) == '-' )
  511. $this->name = substr($this->name, 1);
  512. if( substr($this->name, -1) == '-' )
  513. $this->name = substr($this->name, 0, -1);
  514. $this->name .= '-'.mt_rand(0, 999).'.html';
  515. return $this->name;
  516. }
  517. public function delete()
  518. {
  519. $this->add2history(__CLASS__.'::'.__FUNCTION__);
  520. $this->collection->remove( array('_id' => $this->id) );
  521. foreach($this->editions() as $edi)
  522. $edi->delete();
  523. foreach($this->feed_links() as $fs)
  524. $fs->delete();
  525. foreach($this->comments() as $com)
  526. $com->delete();
  527. $ts0 = new topic_story();
  528. foreach($ts0->all4story($this->id) as $ts)
  529. $ts->delete();
  530. }
  531. public function all()
  532. {
  533. $this->add2history(__CLASS__.'::'.__FUNCTION__);
  534. $stlist = array();
  535. foreach($this->collection->find()->sort(array('date'=>-1)) as $s)
  536. $stlist[] = new story($s);
  537. return $stlist;
  538. }
  539. public function all_from_array($selection)
  540. {
  541. $stories = array();
  542. if($selection)
  543. {
  544. $this->add2history(__CLASS__.'::'.__FUNCTION__);
  545. foreach($this->collection->find(array('_id'=>array('$in'=>$selection))) as $s)
  546. $stories[] = new story($s);
  547. }
  548. return $stories;
  549. }
  550. public function last_stories($num = FS_MAX_STORIES)
  551. {
  552. $this->add2history(__CLASS__.'::'.__FUNCTION__);
  553. $stlist = array();
  554. foreach($this->collection->find()->sort(array('date'=>-1))->limit($num) as $s)
  555. $stlist[] = new story($s);
  556. return $stlist;
  557. }
  558. public function popular_stories($num = FS_MAX_STORIES, $clicks=FALSE)
  559. {
  560. $this->add2history(__CLASS__.'::'.__FUNCTION__);
  561. $stlist = array();
  562. if($clicks)
  563. $search = array('clics'=>-1);
  564. else
  565. $search = array('popularity'=>-1);
  566. foreach($this->collection->find()->sort($search)->limit($num) as $s)
  567. $stlist[] = new story($s);
  568. return $stlist;
  569. }
  570. public function published_stories()
  571. {
  572. $this->add2history(__CLASS__.'::'.__FUNCTION__);
  573. $stlist = array();
  574. foreach($this->collection->find()->sort(array('published'=>-1))->limit(FS_MAX_STORIES) as $s)
  575. {
  576. if( isset($s['published']) )
  577. $stlist[] = new story($s);
  578. }
  579. return $stlist;
  580. }
  581. public function random_stories($limit=FS_MAX_STORIES)
  582. {
  583. $this->add2history(__CLASS__.'::'.__FUNCTION__);
  584. $stlist = array();
  585. if( mt_rand(0, 1) == 0 )
  586. $order = array('date' => -1);
  587. else
  588. $order = array('popularity' => -1);
  589. $offset = mt_rand(0, max( array(0, $this->count()-$limit) ) );
  590. foreach($this->collection->find()->sort($order)->skip($offset)->limit($limit) as $a)
  591. $stlist[] = new story($a);
  592. return $stlist;
  593. }
  594. public function search($query, $title = TRUE)
  595. {
  596. $this->add2history(__CLASS__.'::'.__FUNCTION__);
  597. $stlist = array();
  598. if($title)
  599. $search = array( 'title' => new MongoRegex('/'.$query.'/iu') );
  600. else
  601. $search = array( 'description' => new MongoRegex('/'.$query.'/iu') );
  602. foreach($this->collection->find($search)->sort(array('popularity'=>-1))->limit(FS_MAX_STORIES) as $s)
  603. {
  604. /// parece ser que las expresiones regulares no funciona muy bien en mongodb
  605. if( $title AND preg_match('/\b'.$query.'\b/iu', $s['title']) )
  606. {
  607. $stlist[] = new story($s);
  608. }
  609. else if( !$title AND preg_match('/\b'.$query.'\b/iu', $s['description']) )
  610. {
  611. $stlist[] = new story($s);
  612. }
  613. }
  614. return $stlist;
  615. }
  616. public function cron_job()
  617. {
  618. echo "\nActualizamos los artículos populares y publicamos...";
  619. $j = 0;
  620. $max_public = 2;
  621. foreach($this->popular_stories(FS_MAX_STORIES * 4) as $ps)
  622. {
  623. /// obtenemos las menciones del artículo
  624. if( is_null($ps->published) )
  625. $ps->random_count();
  626. /// si la noticia alcanza el TOP FS_MAX_STORIES, entonces la publicamos (pero sólo 2)
  627. if($j < FS_MAX_STORIES AND is_null($ps->published) AND $ps->popularity > 1 AND $max_public > 0)
  628. {
  629. $ps->published = time();
  630. $max_public--;
  631. }
  632. $ps->save();
  633. $j++;
  634. echo '.';
  635. }
  636. }
  637. }