]> git.mxchange.org Git - friendica.git/blob - src/Model/Contact/Relation.php
Merge remote-tracking branch 'upstream/develop' into contact-tabs
[friendica.git] / src / Model / Contact / Relation.php
1 <?php
2 /**
3  * @copyright Copyright (C) 2020, Friendica
4  *
5  * @license GNU AGPL version 3 or any later version
6  *
7  * This program is free software: you can redistribute it and/or modify
8  * it under the terms of the GNU Affero General Public License as
9  * published by the Free Software Foundation, either version 3 of the
10  * License, or (at your option) any later version.
11  *
12  * This program is distributed in the hope that it will be useful,
13  * but WITHOUT ANY WARRANTY; without even the implied warranty of
14  * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
15  * GNU Affero General Public License for more details.
16  *
17  * You should have received a copy of the GNU Affero General Public License
18  * along with this program.  If not, see <https://www.gnu.org/licenses/>.
19  *
20  */
21
22 namespace Friendica\Model\Contact;
23
24 use Exception;
25 use Friendica\Core\Logger;
26 use Friendica\Core\Protocol;
27 use Friendica\Database\DBA;
28 use Friendica\DI;
29 use Friendica\Protocol\ActivityPub;
30 use Friendica\Util\DateTimeFormat;
31 use Friendica\Util\Strings;
32
33 /**
34  * This class provides relationship information based on the `contact-relation` table.
35  * This table is directional (cid = source, relation-cid = target), references public contacts (with uid=0) and records both
36  * follows and the last interaction (likes/comments) on public posts.
37  */
38 class Relation
39 {
40         /**
41          * No discovery of followers/followings
42          */
43         const DISCOVERY_NONE = 0;
44         /**
45          * Discover followers/followings of local contacts
46          */
47         const DISCOVERY_LOCAL = 1;
48         /**
49          * Discover followers/followings of local contacts and contacts that visibly interacted on the system
50          */
51         const DISCOVERY_INTERACTOR = 2;
52         /**
53          * Discover followers/followings of all contacts
54          */
55         const DISCOVERY_ALL = 3;
56
57         public static function store(int $target, int $actor, string $interaction_date)
58         {
59                 if ($actor == $target) {
60                         return;
61                 }
62
63                 DBA::update('contact-relation', ['last-interaction' => $interaction_date], ['cid' => $target, 'relation-cid' => $actor], true);
64         }
65
66         /**
67          * Fetches the followers of a given profile and adds them
68          *
69          * @param string $url URL of a profile
70          * @return void
71          */
72         public static function discoverByUrl(string $url)
73         {
74                 $contact = Contact::getByURL($url);
75                 if (empty($contact)) {
76                         return;
77                 }
78
79                 if (!self::isDiscoverable($url, $contact)) {
80                         return;
81                 }
82
83                 $apcontact = APContact::getByURL($url, false);
84
85                 if (!empty($apcontact['followers']) && is_string($apcontact['followers'])) {
86                         $followers = ActivityPub::fetchItems($apcontact['followers']);
87                 } else {
88                         $followers = [];
89                 }
90
91                 if (!empty($apcontact['following']) && is_string($apcontact['following'])) {
92                         $followings = ActivityPub::fetchItems($apcontact['following']);
93                 } else {
94                         $followings = [];
95                 }
96
97                 if (empty($followers) && empty($followings)) {
98                         DBA::update('contact', ['last-discovery' => DateTimeFormat::utcNow()], ['id' => $contact['id']]);
99                         Logger::info('The contact does not offer discoverable data', ['id' => $contact['id'], 'url' => $url, 'network' => $contact['network']]);
100                         return;
101                 }
102
103                 $target = $contact['id'];
104
105                 if (!empty($followers)) {
106                         // Clear the follower list, since it will be recreated in the next step
107                         DBA::update('contact-relation', ['follows' => false], ['cid' => $target]);
108                 }
109
110                 $contacts = [];
111                 foreach (array_merge($followers, $followings) as $contact) {
112                         if (is_string($contact)) {
113                                 $contacts[] = $contact;
114                         } elseif (!empty($contact['url']) && is_string($contact['url'])) {
115                                 $contacts[] = $contact['url'];
116                         }
117                 }
118                 $contacts = array_unique($contacts);
119
120                 $follower_counter = 0;
121                 $following_counter = 0;
122
123                 Logger::info('Discover contacts', ['id' => $target, 'url' => $url, 'contacts' => count($contacts)]);
124                 foreach ($contacts as $contact) {
125                         $actor = Contact::getIdForURL($contact);
126                         if (!empty($actor)) {
127                                 if (in_array($contact, $followers)) {
128                                         $fields = ['cid' => $target, 'relation-cid' => $actor];
129                                         DBA::update('contact-relation', ['follows' => true, 'follow-updated' => DateTimeFormat::utcNow()], $fields, true);
130                                         $follower_counter++;
131                                 }
132
133                                 if (in_array($contact, $followings)) {
134                                         $fields = ['cid' => $actor, 'relation-cid' => $target];
135                                         DBA::update('contact-relation', ['follows' => true, 'follow-updated' => DateTimeFormat::utcNow()], $fields, true);
136                                         $following_counter++;
137                                 }
138                         }
139                 }
140
141                 if (!empty($followers)) {
142                         // Delete all followers that aren't followers anymore (and aren't interacting)
143                         DBA::delete('contact-relation', ['cid' => $target, 'follows' => false, 'last-interaction' => DBA::NULL_DATETIME]);
144                 }
145
146                 DBA::update('contact', ['last-discovery' => DateTimeFormat::utcNow()], ['id' => $target]);
147                 Logger::info('Contacts discovery finished', ['id' => $target, 'url' => $url, 'follower' => $follower_counter, 'following' => $following_counter]);
148                 return;
149         }
150
151         /**
152          * Tests if a given contact url is discoverable
153          *
154          * @param string $url     Contact url
155          * @param array  $contact Contact array
156          * @return boolean True if contact is discoverable
157          */
158         public static function isDiscoverable(string $url, array $contact = [])
159         {
160                 $contact_discovery = DI::config()->get('system', 'contact_discovery');
161
162                 if ($contact_discovery == self::DISCOVERY_NONE) {
163                         return false;
164                 }
165
166                 if (empty($contact)) {
167                         $contact = Contact::getByURL($url);
168                 }
169
170                 if (empty($contact)) {
171                         return false;
172                 }
173
174                 if ($contact['last-discovery'] > DateTimeFormat::utc('now - 1 month')) {
175                         Logger::info('No discovery - Last was less than a month ago.', ['id' => $contact['id'], 'url' => $url, 'discovery' => $contact['last-discovery']]);
176                         return false;
177                 }
178
179                 if ($contact_discovery != self::DISCOVERY_ALL) {
180                         $local = DBA::exists('contact', ["`nurl` = ? AND `uid` != ?", Strings::normaliseLink($url), 0]);
181                         if (($contact_discovery == self::DISCOVERY_LOCAL) && !$local) {
182                                 Logger::info('No discovery - This contact is not followed/following locally.', ['id' => $contact['id'], 'url' => $url]);
183                                 return false;
184                         }
185
186                         if ($contact_discovery == self::DISCOVERY_INTERACTOR) {
187                                 $interactor = DBA::exists('contact-relation', ["`relation-cid` = ? AND `last-interaction` > ?", $contact['id'], DBA::NULL_DATETIME]);
188                                 if (!$local && !$interactor) {
189                                         Logger::info('No discovery - This contact is not interacting locally.', ['id' => $contact['id'], 'url' => $url]);
190                                         return false;
191                                 }
192                         }
193                 } elseif ($contact['created'] > DateTimeFormat::utc('now - 1 day')) {
194                         // Newly created contacts are not discovered to avoid DDoS attacks
195                         Logger::info('No discovery - Contact record is less than a day old.', ['id' => $contact['id'], 'url' => $url, 'discovery' => $contact['created']]);
196                         return false;
197                 }
198
199                 if (!in_array($contact['network'], [Protocol::ACTIVITYPUB, Protocol::DFRN, Protocol::OSTATUS])) {
200                         $apcontact = APContact::getByURL($url, false);
201                         if (empty($apcontact)) {
202                                 Logger::info('No discovery - The contact does not seem to speak ActivityPub.', ['id' => $contact['id'], 'url' => $url, 'network' => $contact['network']]);
203                                 return false;
204                         }
205                 }
206
207                 return true;
208         }
209
210         /**
211          * Counts all the known follows of the provided public contact
212          *
213          * @param int   $cid       Public contact id
214          * @param array $condition Additional condition on the contact table
215          * @return int
216          * @throws Exception
217          */
218         public static function countFollows(int $cid, array $condition = [])
219         {
220                 $condition = DBA::mergeConditions($condition,
221                         ['`id` IN (
222     SELECT `relation-cid`
223     FROM `contact-relation`
224     WHERE `cid` = ?
225     AND `follows`
226 )', $cid]
227                 );
228
229                 return DI::dba()->count('contact', $condition);
230         }
231
232         /**
233          * Returns a paginated list of contacts that are followed the provided public contact.
234          *
235          * @param int   $cid       Public contact id
236          * @param array $condition Additional condition on the contact table
237          * @param int   $count
238          * @param int   $offset
239          * @param bool  $shuffle
240          * @return array
241          * @throws Exception
242          */
243         public static function listFollows(int $cid, array $condition = [], int $count = 30, int $offset = 0, bool $shuffle = false)
244         {
245                 $condition = DBA::mergeConditions($condition,
246                         ['`id` IN (
247     SELECT `relation-cid`
248     FROM `contact-relation`
249     WHERE `cid` = ?
250     AND `follows`
251 )', $cid]
252                 );
253
254                 $follows = DI::dba()->selectToArray(
255                         'contact',
256                         $condition,
257                         [
258                                 'limit' => [$offset, $count],
259                                 'order' => [$shuffle ? 'RAND()' : 'name']
260                         ]
261                 );
262
263                 return $follows;
264         }
265
266         /**
267          * Counts all the known followers of the provided public contact
268          *
269          * @param int   $cid       Public contact id
270          * @param array $condition Additional condition on the contact table
271          * @return int
272          * @throws Exception
273          */
274         public static function countFollowers(int $cid, array $condition = [])
275         {
276                 $condition = DBA::mergeConditions($condition,
277                         ['`id` IN (
278     SELECT `cid`
279     FROM `contact-relation`
280     WHERE `relation-cid` = ?
281     AND `follows`
282 )', $cid]
283                 );
284
285                 return DI::dba()->count('contact', $condition);
286         }
287
288         /**
289          * Returns a paginated list of contacts that follow the provided public contact.
290          *
291          * @param int   $cid       Public contact id
292          * @param array $condition Additional condition on the contact table
293          * @param int   $count
294          * @param int   $offset
295          * @param bool  $shuffle
296          * @return array
297          * @throws Exception
298          */
299         public static function listFollowers(int $cid, array $condition = [], int $count = 30, int $offset = 0, bool $shuffle = false)
300         {
301                 $condition = DBA::mergeConditions($condition,
302                         ['`id` IN (
303     SELECT `cid`
304     FROM `contact-relation`
305     WHERE `relation-cid` = ?
306     AND `follows`
307 )', $cid]
308                 );
309
310                 $followers = DI::dba()->selectToArray(
311                         'contact',
312                         $condition,
313                         [
314                                 'limit' => [$offset, $count],
315                                 'order' => [$shuffle ? 'RAND()' : 'name']
316                         ]
317                 );
318
319                 return $followers;
320         }
321
322         /**
323          * Counts the number of contacts that both provided public contacts have interacted with at least once.
324          * Interactions include follows and likes and comments on public posts.
325          *
326          * @param int   $sourceId  Public contact id
327          * @param int   $targetId  Public contact id
328          * @param array $condition Additional condition array on the contact table
329          * @return int
330          * @throws Exception
331          */
332         public static function countCommon(int $sourceId, int $targetId, array $condition = [])
333         {
334                 $condition = DBA::mergeConditions($condition,
335                         ['`id` IN (
336     SELECT `relation-cid`
337     FROM `contact-relation`
338     WHERE `cid` = ?
339
340   AND `id` IN (
341     SELECT `relation-cid`
342     FROM `contact-relation`
343     WHERE `cid` = ?
344 )', $sourceId, $targetId]
345                 );
346
347                 $total = DI::dba()->count('contact', $condition);
348
349                 return $total;
350         }
351
352         /**
353          * Returns a paginated list of contacts that both provided public contacts have interacted with at least once.
354          * Interactions include follows and likes and comments on public posts.
355          *
356          * @param int   $sourceId  Public contact id
357          * @param int   $targetId  Public contact id
358          * @param array $condition Additional condition on the contact table
359          * @param int   $count
360          * @param int   $offset
361          * @param bool  $shuffle
362          * @return array
363          * @throws Exception
364          */
365         public static function listCommon(int $sourceId, int $targetId, array $condition = [], int $count = 30, int $offset = 0, bool $shuffle = false)
366         {
367                 $condition = DBA::mergeConditions($condition,
368                         ["`id` IN (
369     SELECT `relation-cid`
370     FROM `contact-relation`
371     WHERE `cid` = ?
372       AND `follows`
373
374   AND `id` IN (
375     SELECT `relation-cid`
376     FROM `contact-relation`
377     WHERE `cid` = ?
378       AND `follows`
379 )", $sourceId, $targetId]
380                 );
381
382                 $contacts = DI::dba()->selectToArray(
383                         'contact',
384                         $condition,
385                         [
386                                 'limit' => [$offset, $count],
387                                 'order' => [$shuffle ? 'name' : 'RAND()'],
388                         ]
389                 );
390
391                 return $contacts;
392         }
393
394
395         /**
396          * Counts the number of contacts that are followed by both provided public contacts.
397          *
398          * @param int   $sourceId  Public contact id
399          * @param int   $targetId  Public contact id
400          * @param array $condition Additional condition array on the contact table
401          * @return int
402          * @throws Exception
403          */
404         public static function countCommonFollows(int $sourceId, int $targetId, array $condition = [])
405         {
406                 $condition = DBA::mergeConditions($condition,
407                         ['`id` IN (
408     SELECT `relation-cid`
409     FROM `contact-relation`
410     WHERE `cid` = ?
411       AND `follows`
412
413   AND `id` IN (
414     SELECT `relation-cid`
415     FROM `contact-relation`
416     WHERE `cid` = ?
417       AND `follows`
418 )', $sourceId, $targetId]
419                 );
420
421                 $total = DI::dba()->count('contact', $condition);
422
423                 return $total;
424         }
425
426         /**
427          * Returns a paginated list of contacts that are followed by both provided public contacts.
428          *
429          * @param int   $sourceId  Public contact id
430          * @param int   $targetId  Public contact id
431          * @param array $condition Additional condition array on the contact table
432          * @param int   $count
433          * @param int   $offset
434          * @param bool  $shuffle
435          * @return array
436          * @throws Exception
437          */
438         public static function listCommonFollows(int $sourceId, int $targetId, array $condition = [], int $count = 30, int $offset = 0, bool $shuffle = false)
439         {
440                 $condition = DBA::mergeConditions($condition,
441                         ["`id` IN (
442     SELECT `relation-cid`
443     FROM `contact-relation`
444     WHERE `cid` = ?
445       AND `follows`
446
447   AND `id` IN (
448     SELECT `relation-cid`
449     FROM `contact-relation`
450     WHERE `cid` = ?
451       AND `follows`
452 )", $sourceId, $targetId]
453                 );
454
455                 $contacts = DI::dba()->selectToArray(
456                         'contact',
457                         $condition,
458                         [
459                                 'limit' => [$offset, $count],
460                                 'order' => [$shuffle ? 'name' : 'RAND()'],
461                         ]
462                 );
463
464                 return $contacts;
465         }
466
467         /**
468          * Counts the number of contacts that follow both provided public contacts.
469          *
470          * @param int   $sourceId  Public contact id
471          * @param int   $targetId  Public contact id
472          * @param array $condition Additional condition on the contact table
473          * @return int
474          * @throws Exception
475          */
476         public static function countCommonFollowers(int $sourceId, int $targetId, array $condition = [])
477         {
478                 $condition = DBA::mergeConditions($condition,
479                         ['`id` IN (
480     SELECT `cid`
481     FROM `contact-relation`
482     WHERE `relation-cid` = ?
483       AND `follows`
484
485   AND `id` IN (
486     SELECT `cid`
487     FROM `contact-relation`
488     WHERE `relation-cid` = ?
489       AND `follows`
490 )', $sourceId, $targetId]
491                 );
492
493                 $total = DI::dba()->count('contact', $condition);
494
495                 return $total;
496         }
497
498         /**
499          * Returns a paginated list of contacts that follow both provided public contacts.
500          *
501          * @param int   $sourceId  Public contact id
502          * @param int   $targetId  Public contact id
503          * @param array $condition Additional condition on the contact table
504          * @param int   $count
505          * @param int   $offset
506          * @param bool  $shuffle
507          * @return array
508          * @throws Exception
509          */
510         public static function listCommonFollowers(int $sourceId, int $targetId, array $condition = [], int $count = 30, int $offset = 0, bool $shuffle = false)
511         {
512                 $condition = DBA::mergeConditions($condition,
513                         ["`id` IN (
514     SELECT `cid`
515     FROM `contact-relation`
516     WHERE `relation-cid` = ?
517       AND `follows`
518
519   AND `id` IN (
520     SELECT `cid`
521     FROM `contact-relation`
522     WHERE `relation-cid` = ?
523       AND `follows`
524 )", $sourceId, $targetId]
525                 );
526
527                 $contacts = DI::dba()->selectToArray(
528                         'contact',
529                         $condition,
530                         [
531                                 'limit' => [$offset, $count],
532                                 'order' => [$shuffle ? 'name' : 'RAND()'],
533                         ]
534                 );
535
536                 return $contacts;
537         }
538 }