]> git.mxchange.org Git - friendica.git/blob - src/Model/Contact/Relation.php
Adding $fields
[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\Model\APContact;
30 use Friendica\Model\Contact;
31 use Friendica\Protocol\ActivityPub;
32 use Friendica\Util\DateTimeFormat;
33 use Friendica\Util\Strings;
34
35 /**
36  * This class provides relationship information based on the `contact-relation` table.
37  * This table is directional (cid = source, relation-cid = target), references public contacts (with uid=0) and records both
38  * follows and the last interaction (likes/comments) on public posts.
39  */
40 class Relation
41 {
42         /**
43          * No discovery of followers/followings
44          */
45         const DISCOVERY_NONE = 0;
46         /**
47          * Discover followers/followings of local contacts
48          */
49         const DISCOVERY_LOCAL = 1;
50         /**
51          * Discover followers/followings of local contacts and contacts that visibly interacted on the system
52          */
53         const DISCOVERY_INTERACTOR = 2;
54         /**
55          * Discover followers/followings of all contacts
56          */
57         const DISCOVERY_ALL = 3;
58
59         public static function store(int $target, int $actor, string $interaction_date)
60         {
61                 if ($actor == $target) {
62                         return;
63                 }
64
65                 DBA::update('contact-relation', ['last-interaction' => $interaction_date], ['cid' => $target, 'relation-cid' => $actor], true);
66         }
67
68         /**
69          * Fetches the followers of a given profile and adds them
70          *
71          * @param string $url URL of a profile
72          * @return void
73          */
74         public static function discoverByUrl(string $url)
75         {
76                 $contact = Contact::getByURL($url);
77                 if (empty($contact)) {
78                         return;
79                 }
80
81                 if (!self::isDiscoverable($url, $contact)) {
82                         return;
83                 }
84
85                 $apcontact = APContact::getByURL($url, false);
86
87                 if (!empty($apcontact['followers']) && is_string($apcontact['followers'])) {
88                         $followers = ActivityPub::fetchItems($apcontact['followers']);
89                 } else {
90                         $followers = [];
91                 }
92
93                 if (!empty($apcontact['following']) && is_string($apcontact['following'])) {
94                         $followings = ActivityPub::fetchItems($apcontact['following']);
95                 } else {
96                         $followings = [];
97                 }
98
99                 if (empty($followers) && empty($followings)) {
100                         DBA::update('contact', ['last-discovery' => DateTimeFormat::utcNow()], ['id' => $contact['id']]);
101                         Logger::info('The contact does not offer discoverable data', ['id' => $contact['id'], 'url' => $url, 'network' => $contact['network']]);
102                         return;
103                 }
104
105                 $target = $contact['id'];
106
107                 if (!empty($followers)) {
108                         // Clear the follower list, since it will be recreated in the next step
109                         DBA::update('contact-relation', ['follows' => false], ['cid' => $target]);
110                 }
111
112                 $contacts = [];
113                 foreach (array_merge($followers, $followings) as $contact) {
114                         if (is_string($contact)) {
115                                 $contacts[] = $contact;
116                         } elseif (!empty($contact['url']) && is_string($contact['url'])) {
117                                 $contacts[] = $contact['url'];
118                         }
119                 }
120                 $contacts = array_unique($contacts);
121
122                 $follower_counter = 0;
123                 $following_counter = 0;
124
125                 Logger::info('Discover contacts', ['id' => $target, 'url' => $url, 'contacts' => count($contacts)]);
126                 foreach ($contacts as $contact) {
127                         $actor = Contact::getIdForURL($contact);
128                         if (!empty($actor)) {
129                                 if (in_array($contact, $followers)) {
130                                         $fields = ['cid' => $target, 'relation-cid' => $actor];
131                                         DBA::update('contact-relation', ['follows' => true, 'follow-updated' => DateTimeFormat::utcNow()], $fields, true);
132                                         $follower_counter++;
133                                 }
134
135                                 if (in_array($contact, $followings)) {
136                                         $fields = ['cid' => $actor, 'relation-cid' => $target];
137                                         DBA::update('contact-relation', ['follows' => true, 'follow-updated' => DateTimeFormat::utcNow()], $fields, true);
138                                         $following_counter++;
139                                 }
140                         }
141                 }
142
143                 if (!empty($followers)) {
144                         // Delete all followers that aren't followers anymore (and aren't interacting)
145                         DBA::delete('contact-relation', ['cid' => $target, 'follows' => false, 'last-interaction' => DBA::NULL_DATETIME]);
146                 }
147
148                 DBA::update('contact', ['last-discovery' => DateTimeFormat::utcNow()], ['id' => $target]);
149                 Logger::info('Contacts discovery finished', ['id' => $target, 'url' => $url, 'follower' => $follower_counter, 'following' => $following_counter]);
150                 return;
151         }
152
153         /**
154          * Tests if a given contact url is discoverable
155          *
156          * @param string $url     Contact url
157          * @param array  $contact Contact array
158          * @return boolean True if contact is discoverable
159          */
160         public static function isDiscoverable(string $url, array $contact = [])
161         {
162                 $contact_discovery = DI::config()->get('system', 'contact_discovery');
163
164                 if ($contact_discovery == self::DISCOVERY_NONE) {
165                         return false;
166                 }
167
168                 if (empty($contact)) {
169                         $contact = Contact::getByURL($url);
170                 }
171
172                 if (empty($contact)) {
173                         return false;
174                 }
175
176                 if ($contact['last-discovery'] > DateTimeFormat::utc('now - 1 month')) {
177                         Logger::info('No discovery - Last was less than a month ago.', ['id' => $contact['id'], 'url' => $url, 'discovery' => $contact['last-discovery']]);
178                         return false;
179                 }
180
181                 if ($contact_discovery != self::DISCOVERY_ALL) {
182                         $local = DBA::exists('contact', ["`nurl` = ? AND `uid` != ?", Strings::normaliseLink($url), 0]);
183                         if (($contact_discovery == self::DISCOVERY_LOCAL) && !$local) {
184                                 Logger::info('No discovery - This contact is not followed/following locally.', ['id' => $contact['id'], 'url' => $url]);
185                                 return false;
186                         }
187
188                         if ($contact_discovery == self::DISCOVERY_INTERACTOR) {
189                                 $interactor = DBA::exists('contact-relation', ["`relation-cid` = ? AND `last-interaction` > ?", $contact['id'], DBA::NULL_DATETIME]);
190                                 if (!$local && !$interactor) {
191                                         Logger::info('No discovery - This contact is not interacting locally.', ['id' => $contact['id'], 'url' => $url]);
192                                         return false;
193                                 }
194                         }
195                 } elseif ($contact['created'] > DateTimeFormat::utc('now - 1 day')) {
196                         // Newly created contacts are not discovered to avoid DDoS attacks
197                         Logger::info('No discovery - Contact record is less than a day old.', ['id' => $contact['id'], 'url' => $url, 'discovery' => $contact['created']]);
198                         return false;
199                 }
200
201                 if (!in_array($contact['network'], [Protocol::ACTIVITYPUB, Protocol::DFRN, Protocol::OSTATUS])) {
202                         $apcontact = APContact::getByURL($url, false);
203                         if (empty($apcontact)) {
204                                 Logger::info('No discovery - The contact does not seem to speak ActivityPub.', ['id' => $contact['id'], 'url' => $url, 'network' => $contact['network']]);
205                                 return false;
206                         }
207                 }
208
209                 return true;
210         }
211
212         /**
213          * Counts all the known follows of the provided public contact
214          *
215          * @param int   $cid       Public contact id
216          * @param array $condition Additional condition on the contact table
217          * @return int
218          * @throws Exception
219          */
220         public static function countFollows(int $cid, array $condition = [])
221         {
222                 $condition = DBA::mergeConditions($condition,
223                         ['`id` IN (SELECT `relation-cid` FROM `contact-relation` WHERE `cid` = ? AND `follows`)', 
224                         $cid]
225                 );
226
227                 return DI::dba()->count('contact', $condition);
228         }
229
230         /**
231          * Returns a paginated list of contacts that are followed the provided public contact.
232          *
233          * @param int   $cid       Public contact id
234          * @param array $field     Field list
235          * @param array $condition Additional condition on the contact table
236          * @param int   $count
237          * @param int   $offset
238          * @param bool  $shuffle
239          * @return array
240          * @throws Exception
241          */
242         public static function listFollows(int $cid, array $fields = [], array $condition = [], int $count = 30, int $offset = 0, bool $shuffle = false)
243         {
244                 $condition = DBA::mergeConditions($condition,
245                         ['`id` IN (SELECT `relation-cid` FROM `contact-relation` WHERE `cid` = ? AND `follows`)', 
246                         $cid]
247                 );
248
249                 return DI::dba()->selectToArray('contact', $fields, $condition,
250                         ['limit' => [$offset, $count], 'order' => [$shuffle ? 'RAND()' : 'name']]
251                 );
252         }
253
254         /**
255          * Counts all the known followers of the provided public contact
256          *
257          * @param int   $cid       Public contact id
258          * @param array $condition Additional condition on the contact table
259          * @return int
260          * @throws Exception
261          */
262         public static function countFollowers(int $cid, array $condition = [])
263         {
264                 $condition = DBA::mergeConditions($condition,
265                         ['`id` IN (SELECT `cid` FROM `contact-relation` WHERE `relation-cid` = ? AND `follows`)',
266                         $cid]
267                 );
268
269                 return DI::dba()->count('contact', $condition);
270         }
271
272         /**
273          * Returns a paginated list of contacts that follow the provided public contact.
274          *
275          * @param int   $cid       Public contact id
276          * @param array $field     Field list
277          * @param array $condition Additional condition on the contact table
278          * @param int   $count
279          * @param int   $offset
280          * @param bool  $shuffle
281          * @return array
282          * @throws Exception
283          */
284         public static function listFollowers(int $cid, array $fields = [], array $condition = [], int $count = 30, int $offset = 0, bool $shuffle = false)
285         {
286                 $condition = DBA::mergeConditions($condition,
287                         ['`id` IN (SELECT `cid` FROM `contact-relation` WHERE `relation-cid` = ? AND `follows`)', $cid]
288                 );
289
290                 return DI::dba()->selectToArray('contact', $fields, $condition,
291                         ['limit' => [$offset, $count], 'order' => [$shuffle ? 'RAND()' : 'name']]
292                 );
293         }
294
295         /**
296          * Counts the number of contacts that both provided public contacts have interacted with at least once.
297          * Interactions include follows and likes and comments on public posts.
298          *
299          * @param int   $sourceId  Public contact id
300          * @param int   $targetId  Public contact id
301          * @param array $condition Additional condition array on the contact table
302          * @return int
303          * @throws Exception
304          */
305         public static function countCommon(int $sourceId, int $targetId, array $condition = [])
306         {
307                 $condition = DBA::mergeConditions($condition,
308                         ['`id` IN (SELECT `relation-cid` FROM `contact-relation` WHERE `cid` = ?) 
309                         AND `id` IN (SELECT `relation-cid` FROM `contact-relation` WHERE `cid` = ?)',
310                         $sourceId, $targetId]
311                 );
312
313                 return DI::dba()->count('contact', $condition);
314         }
315
316         /**
317          * Returns a paginated list of contacts that both provided public contacts have interacted with at least once.
318          * Interactions include follows and likes and comments on public posts.
319          *
320          * @param int   $sourceId  Public contact id
321          * @param int   $targetId  Public contact id
322          * @param array $field     Field list
323          * @param array $condition Additional condition on the contact table
324          * @param int   $count
325          * @param int   $offset
326          * @param bool  $shuffle
327          * @return array
328          * @throws Exception
329          */
330         public static function listCommon(int $sourceId, int $targetId, array $fields = [], array $condition = [], int $count = 30, int $offset = 0, bool $shuffle = false)
331         {
332                 $condition = DBA::mergeConditions($condition,
333                         ["`id` IN (SELECT `relation-cid` FROM `contact-relation` WHERE `cid` = ? AND `follows`) 
334                         AND `id` IN (SELECT `relation-cid` FROM `contact-relation` WHERE `cid` = ? AND `follows`)",
335                         $sourceId, $targetId]
336                 );
337
338                 return DI::dba()->selectToArray('contact', $fields, $condition,
339                         ['limit' => [$offset, $count], 'order' => [$shuffle ? 'name' : 'RAND()']]
340                 );
341         }
342
343
344         /**
345          * Counts the number of contacts that are followed by both provided public contacts.
346          *
347          * @param int   $sourceId  Public contact id
348          * @param int   $targetId  Public contact id
349          * @param array $condition Additional condition array on the contact table
350          * @return int
351          * @throws Exception
352          */
353         public static function countCommonFollows(int $sourceId, int $targetId, array $condition = [])
354         {
355                 $condition = DBA::mergeConditions($condition,
356                         ['`id` IN (SELECT `relation-cid` FROM `contact-relation` WHERE `cid` = ? AND `follows`) 
357                         AND `id` IN (SELECT `relation-cid` FROM `contact-relation` WHERE `cid` = ? AND `follows`)',
358                         $sourceId, $targetId]
359                 );
360
361                 return DI::dba()->count('contact', $condition);
362         }
363
364         /**
365          * Returns a paginated list of contacts that are followed by both provided public contacts.
366          *
367          * @param int   $sourceId  Public contact id
368          * @param int   $targetId  Public contact id
369          * @param array $field     Field list
370          * @param array $condition Additional condition array on the contact table
371          * @param int   $count
372          * @param int   $offset
373          * @param bool  $shuffle
374          * @return array
375          * @throws Exception
376          */
377         public static function listCommonFollows(int $sourceId, int $targetId, array $fields = [], array $condition = [], int $count = 30, int $offset = 0, bool $shuffle = false)
378         {
379                 $condition = DBA::mergeConditions($condition,
380                         ["`id` IN (SELECT `relation-cid` FROM `contact-relation` WHERE `cid` = ? AND `follows`) 
381                         AND `id` IN (SELECT `relation-cid` FROM `contact-relation` WHERE `cid` = ? AND `follows`)",
382                         $sourceId, $targetId]
383                 );
384
385                 return DI::dba()->selectToArray('contact', $fields, $condition,
386                         ['limit' => [$offset, $count], 'order' => [$shuffle ? 'name' : 'RAND()']]
387                 );
388         }
389
390         /**
391          * Counts the number of contacts that follow both provided public contacts.
392          *
393          * @param int   $sourceId  Public contact id
394          * @param int   $targetId  Public contact id
395          * @param array $condition Additional condition on the contact table
396          * @return int
397          * @throws Exception
398          */
399         public static function countCommonFollowers(int $sourceId, int $targetId, array $condition = [])
400         {
401                 $condition = DBA::mergeConditions($condition,
402                         ["`id` IN (SELECT `cid` FROM `contact-relation` WHERE `relation-cid` = ? AND `follows`) 
403                         AND `id` IN (SELECT `cid` FROM `contact-relation` WHERE `relation-cid` = ? AND `follows`)",
404                         $sourceId, $targetId]
405                 );
406
407                 return DI::dba()->count('contact', $condition);
408         }
409
410         /**
411          * Returns a paginated list of contacts that follow both provided public contacts.
412          *
413          * @param int   $sourceId  Public contact id
414          * @param int   $targetId  Public contact id
415          * @param array $field     Field list
416          * @param array $condition Additional condition on the contact table
417          * @param int   $count
418          * @param int   $offset
419          * @param bool  $shuffle
420          * @return array
421          * @throws Exception
422          */
423         public static function listCommonFollowers(int $sourceId, int $targetId, array $fields = [], array $condition = [], int $count = 30, int $offset = 0, bool $shuffle = false)
424         {
425                 $condition = DBA::mergeConditions($condition,
426                         ["`id` IN (SELECT `cid` FROM `contact-relation` WHERE `relation-cid` = ? AND `follows`) 
427                         AND `id` IN (SELECT `cid` FROM `contact-relation` WHERE `relation-cid` = ? AND `follows`)",
428                         $sourceId, $targetId]
429                 );
430
431                 return DI::dba()->selectToArray('contact', $fields, $condition,
432                         ['limit' => [$offset, $count],  'order' => [$shuffle ? 'name' : 'RAND()']]
433                 );
434         }
435 }