]> git.mxchange.org Git - quix0rs-gnu-social.git/blob - plugins/Favorite/classes/Fave.php
Move AlreadyFulfilled check to Fave::addNew
[quix0rs-gnu-social.git] / plugins / Favorite / classes / Fave.php
1 <?php
2 /**
3  * Table Definition for fave
4  */
5
6 class Fave extends Managed_DataObject
7 {
8     public $__table = 'fave';                            // table name
9     public $notice_id;                       // int(4)  primary_key not_null
10     public $user_id;                         // int(4)  primary_key not_null
11     public $uri;                             // varchar(191)   not 255 because utf8mb4 takes more space   not 255 because utf8mb4 takes more space
12     public $created;                         // datetime  multiple_key not_null
13     public $modified;                        // timestamp()   not_null default_CURRENT_TIMESTAMP
14
15     public static function schemaDef()
16     {
17         return array(
18             'fields' => array(
19                 'notice_id' => array('type' => 'int', 'not null' => true, 'description' => 'notice that is the favorite'),
20                 'user_id' => array('type' => 'int', 'not null' => true, 'description' => 'user who likes this notice'),
21                 'uri' => array('type' => 'varchar', 'length' => 191, 'description' => 'universally unique identifier, usually a tag URI'),
22                 'created' => array('type' => 'datetime', 'not null' => true, 'description' => 'date this record was created'),
23                 'modified' => array('type' => 'timestamp', 'not null' => true, 'description' => 'date this record was modified'),
24             ),
25             'primary key' => array('notice_id', 'user_id'),
26             'unique keys' => array(
27                 'fave_uri_key' => array('uri'),
28             ),
29             'foreign keys' => array(
30                 'fave_notice_id_fkey' => array('notice', array('notice_id' => 'id')),
31                 'fave_user_id_fkey' => array('profile', array('user_id' => 'id')), // note: formerly referenced notice.id, but we can now record remote users' favorites
32             ),
33             'indexes' => array(
34                 'fave_notice_id_idx' => array('notice_id'),
35                 'fave_user_id_idx' => array('user_id', 'modified'),
36                 'fave_modified_idx' => array('modified'),
37             ),
38         );
39     }
40
41     /**
42      * Save a favorite record.
43      * @fixme post-author notification should be moved here
44      *
45      * @param Profile $actor  the local or remote Profile who favorites
46      * @param Notice  $target the notice that is favorited
47      * @return Fave record on success
48      * @throws Exception on failure
49      */
50     static function addNew(Profile $actor, Notice $target) {
51         if (self::existsForProfile($target, $actor)) {
52             // TRANS: Client error displayed when trying to mark a notice as favorite that already is a favorite.
53             throw new AlreadyFulfilledException(_('You have already favorited this!'));
54         }
55
56         $act = new Activity();
57         $act->type    = ActivityObject::ACTIVITY;
58         $act->verb    = ActivityVerb::FAVORITE;
59         $act->time    = time();
60         $act->id      = self::newUri($actor, $target, common_sql_date($act->time));
61         $act->title   = _("Favor");
62         // TRANS: Message that is the "content" of a favorite (%1$s is the actor's nickname, %2$ is the favorited
63         //        notice's nickname and %3$s is the content of the favorited notice.)
64         $act->content = sprintf(_('%1$s favorited something by %2$s: %3$s'),
65                                 $actor->getNickname(), $target->getProfile()->getNickname(),
66                                 $target->rendered ?: $target->content);
67         $act->actor   = $actor->asActivityObject();
68         $act->target  = $target->asActivityObject();
69         $act->objects = array(clone($act->target));
70
71         $url = common_local_url('AtomPubShowFavorite', array('profile'=>$actor->id, 'notice'=>$target->id));
72         $act->selfLink = $url;
73         $act->editLink = $url;
74
75         // saveActivity will in turn also call Fave::saveActivityObject which does
76         // what this function used to do before this commit.
77         $stored = Notice::saveActivity($act, $actor);
78
79         return $stored;
80     }
81
82     // exception throwing takeover!
83     public function insert()
84     {
85         if (parent::insert()===false) {
86             common_log_db_error($this, 'INSERT', __FILE__);
87             throw new ServerException(sprintf(_m('Could not store new object of type %s'), get_called_class()));
88         }
89         self::blowCacheForProfileId($this->user_id);
90         self::blowCacheForNoticeId($this->notice_id);
91         return $this;
92     }
93
94     public function delete($useWhere=false)
95     {
96         $profile = Profile::getKV('id', $this->user_id);
97         $notice  = Notice::getKV('id', $this->notice_id);
98
99         $result = null;
100
101         if (Event::handle('StartDisfavorNotice', array($profile, $notice, &$result))) {
102
103             $result = parent::delete($useWhere);
104
105             self::blowCacheForProfileId($this->user_id);
106             self::blowCacheForNoticeId($this->notice_id);
107             self::blow('popular');
108
109             if ($result) {
110                 Event::handle('EndDisfavorNotice', array($profile, $notice));
111             }
112         }
113
114         return $result;
115     }
116
117     function stream($user_id, $offset=0, $limit=NOTICES_PER_PAGE, $own=false, $since_id=0, $max_id=0)
118     {
119         $stream = new FaveNoticeStream($user_id, $own);
120
121         return $stream->getNotices($offset, $limit, $since_id, $max_id);
122     }
123
124     function idStream($user_id, $offset=0, $limit=NOTICES_PER_PAGE, $own=false, $since_id=0, $max_id=0)
125     {
126         $stream = new FaveNoticeStream($user_id, $own);
127
128         return $stream->getNoticeIds($offset, $limit, $since_id, $max_id);
129     }
130
131     function asActivity()
132     {
133         $target = $this->getTarget();
134         $actor  = $this->getActor();
135
136         $act = new Activity();
137
138         $act->verb = ActivityVerb::FAVORITE;
139
140         // FIXME: rationalize this with URL below
141
142         $act->id   = $this->getUri();
143
144         $act->time    = strtotime($this->created);
145         // TRANS: Activity title when marking a notice as favorite.
146         $act->title   = _("Favor");
147         // TRANS: Message that is the "content" of a favorite (%1$s is the actor's nickname, %2$ is the favorited
148         //        notice's nickname and %3$s is the content of the favorited notice.)
149         $act->content = sprintf(_('%1$s favorited something by %2$s: %3$s'),
150                                 $actor->getNickname(), $target->getProfile()->getNickname(),
151                                 $target->rendered ?: $target->content);
152
153         $act->actor     = $actor->asActivityObject();
154         $act->target    = $target->asActivityObject();
155         $act->objects   = array(clone($act->target));
156
157         $url = common_local_url('AtomPubShowFavorite',
158                                           array('profile' => $actor->id,
159                                                 'notice'  => $target->id));
160
161         $act->selfLink = $url;
162         $act->editLink = $url;
163
164         return $act;
165     }
166
167     static function existsForProfile($notice, Profile $scoped)
168     {
169         $fave = self::pkeyGet(array('user_id'=>$scoped->id, 'notice_id'=>$notice->id));
170
171         return ($fave instanceof Fave);
172     }
173
174     /**
175      * Fetch a stream of favorites by profile
176      *
177      * @param integer $profileId Profile that faved
178      * @param integer $offset    Offset from last
179      * @param integer $limit     Number to get
180      *
181      * @return mixed stream of faves, use fetch() to iterate
182      *
183      * @todo Cache results
184      * @todo integrate with Fave::stream()
185      */
186
187     static function byProfile($profileId, $offset, $limit)
188     {
189         $fav = new Fave();
190
191         $fav->user_id = $profileId;
192
193         $fav->orderBy('modified DESC');
194
195         $fav->limit($offset, $limit);
196
197         $fav->find();
198
199         return $fav;
200     }
201
202     static function countByProfile(Profile $profile)
203     {
204         $c = Cache::instance();
205         if (!empty($c)) {
206             $cnt = $c->get(Cache::key('fave:count_by_profile:'.$profile->id));
207             if (is_integer($cnt)) {
208                 return $cnt;
209             }
210         }
211
212         $faves = new Fave();
213         $faves->user_id = $profile->id;
214         $cnt = (int) $faves->count('notice_id');
215
216         if (!empty($c)) {
217             $c->set(Cache::key('fave:count_by_profile:'.$profile->id), $cnt);
218         }
219
220         return $cnt;
221     }
222
223     static protected $_faves = array();
224
225     /**
226      * All faves of this notice
227      *
228      * @param Notice $notice A notice we wish to get faves for (may still be ArrayWrapper)
229      *
230      * @return array Array of Fave objects
231      */
232     static public function byNotice($notice)
233     {
234         if (!isset(self::$_faves[$notice->id])) {
235             self::fillFaves(array($notice->id));
236         }
237         return self::$_faves[$notice->id];
238     }
239
240     static public function fillFaves(array $notice_ids)
241     {
242         $faveMap = Fave::listGet('notice_id', $notice_ids);
243         self::$_faves = array_replace(self::$_faves, $faveMap);
244     }
245
246     static public function blowCacheForProfileId($profile_id)
247     {
248         $cache = Cache::instance();
249         if ($cache) {
250             // Faves don't happen chronologically, so we need to blow
251             // ;last cache, too
252             $cache->delete(Cache::key('fave:ids_by_user:'.$profile_id));
253             $cache->delete(Cache::key('fave:ids_by_user:'.$profile_id.';last'));
254             $cache->delete(Cache::key('fave:ids_by_user_own:'.$profile_id));
255             $cache->delete(Cache::key('fave:ids_by_user_own:'.$profile_id.';last'));
256             $cache->delete(Cache::key('fave:count_by_profile:'.$profile_id));
257         }
258     }
259     static public function blowCacheForNoticeId($notice_id)
260     {
261         $cache = Cache::instance();
262         if ($cache) {
263             $cache->delete(Cache::key('fave:list-ids:notice_id:'.$notice_id));
264         }
265     }
266
267     // Remember that we want the _activity_ notice here, not faves applied
268     // to the supplied Notice (as with byNotice)!
269     static public function fromStored(Notice $stored)
270     {
271         $class = get_called_class();
272         $object = new $class;
273         $object->uri = $stored->uri;
274         if (!$object->find(true)) {
275             throw new NoResultException($object);
276         }
277         return $object;
278     }
279
280     /**
281      * Retrieves the _targeted_ notice of a verb (such as the notice that was
282      * _favorited_, but not the favorite activity itself).
283      *
284      * @param Notice $stored    The activity notice.
285      *
286      * @throws NoResultException when it can't find what it's looking for.
287      */
288     static public function getTargetFromStored(Notice $stored)
289     {
290         return self::fromStored($stored)->getTarget();
291     }
292
293     static public function getObjectType()
294     {
295         return 'activity';
296     }
297
298     public function asActivityObject(Profile $scoped=null)
299     {
300         $actobj = new ActivityObject();
301         $actobj->id = $this->getUri();
302         $actobj->type = ActivityUtils::resolveUri(self::getObjectType());
303         $actobj->actor = $this->getActorObject();
304         $actobj->target = $this->getTargetObject();
305         $actobj->objects = array(clone($actobj->target));
306         $actobj->verb = ActivityVerb::FAVORITE;
307         $actobj->title = ActivityUtils::verbToTitle($actobj->verb);
308         $actobj->content = $this->getTarget()->rendered ?: $this->getTarget()->content;
309         return $actobj;
310     }
311
312     /**
313      * @param ActivityObject $actobj The _favored_ notice (which we're "in-reply-to")
314      * @param Notice         $stored The _activity_ notice, i.e. the favor itself.
315      */
316     static public function parseActivityObject(ActivityObject $actobj, Notice $stored)
317     {
318         $local = ActivityUtils::findLocalObject($actobj->getIdentifiers());
319         if (!$local instanceof Notice) {
320             // $local always returns something, but this was not what we expected. Something is wrong.
321             throw new Exception('Something other than a Notice was returned from findLocalObject');
322         }
323  
324         $actor = $stored->getProfile();
325         $object = new Fave();
326         $object->user_id = $stored->getProfile()->id;
327         $object->notice_id = $local->id;
328         $object->uri = $stored->uri;
329         $object->created = $stored->created;
330         $object->modified = $stored->modified;
331         return $object;
332     }
333
334     static public function extendActivity(Notice $stored, Activity $act, Profile $scoped=null)
335     {
336         $target = self::getTargetFromStored($stored);
337
338         // The following logic was copied from StatusNet's Activity plugin
339         if (ActivityUtils::compareTypes($target->verb, array(ActivityVerb::POST))) {
340             // "I like the thing you posted"
341             $act->objects = $target->asActivity()->objects;
342         } else {
343             // "I like that you did whatever you did"
344             $act->target = $target->asActivityObject();
345             $act->objects = array(clone($act->target));
346         }
347         $act->context->replyToID = $target->getUri();
348         $act->context->replyToUrl = $target->getUrl();
349         $act->title = ActivityUtils::verbToTitle($act->verb);
350     }
351
352     static function saveActivityObject(ActivityObject $actobj, Notice $stored)
353     {
354         $object = self::parseActivityObject($actobj, $stored);
355         $object->insert();  // exception throwing in Fave's case!
356
357         self::blowCacheForProfileId($object->user_id);
358         self::blowCacheForNoticeId($object->notice_id);
359         self::blow('popular');
360
361         Event::handle('EndFavorNotice', array($stored->getProfile(), $object->getTarget()));
362         return $object;
363     }
364
365     public function getAttentionArray() {
366         // not all objects can/should carry attentions, so we don't require extending this
367         // the format should be an array with URIs to mentioned profiles
368         return array();
369     }
370
371     public function getTarget()
372     {
373         // throws exception on failure
374         $target = new Notice();
375         $target->id = $this->notice_id;
376         if (!$target->find(true)) {
377             throw new NoResultException($target);
378         }
379
380         return $target;
381     }
382
383     public function getTargetObject()
384     {
385         return $this->getTarget()->asActivityObject();
386     }
387
388     protected $_stored = array();
389
390     public function getStored()
391     {
392         if (!isset($this->_stored[$this->uri])) {
393             $stored = new Notice();
394             $stored->uri = $this->uri;
395             if (!$stored->find(true)) {
396                 throw new NoResultException($stored);
397             }
398             $this->_stored[$this->uri] = $stored;
399         }
400         return $this->_stored[$this->uri];
401     }
402
403     public function getActor()
404     {
405         $profile = new Profile();
406         $profile->id = $this->user_id;
407         if (!$profile->find(true)) {
408             throw new NoResultException($profile);
409         }
410         return $profile;
411     }
412
413     public function getActorObject()
414     {
415         return $this->getActor()->asActivityObject();
416     }
417
418     public function getUri()
419     {
420         if (!empty($this->uri)) {
421             return $this->uri;
422         }
423
424         // We (should've in this case) created it ourselves, so we tag it ourselves
425         return self::newUri($this->getActor(), $this->getTarget(), $this->created);
426     }
427
428     static function newUri(Profile $actor, Managed_DataObject $target, $created=null)
429     {
430         if (is_null($created)) {
431             $created = common_sql_now();
432         }
433         return TagURI::mint(strtolower(get_called_class()).':%d:%s:%d:%s',
434                                         $actor->id,
435                                         ActivityUtils::resolveUri(self::getObjectType(), true),
436                                         $target->id,
437                                         common_date_iso8601($created));
438     }
439 }