3 * @copyright Copyright (C) 2020, Friendica
5 * @license GNU AGPL version 3 or any later version
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.
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.
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/>.
22 namespace Friendica\Model;
24 use Friendica\Core\Logger;
25 use Friendica\Database\DBA;
27 use Friendica\Protocol\ActivityPub;
28 use Friendica\Util\DateTimeFormat;
29 use Friendica\Util\Strings;
34 * No discovery of followers/followings
36 const DISCOVERY_NONE = 0;
38 * Discover followers/followings of local contacts
40 const DISCOVERY_LOCAL = 1;
42 * Discover followers/followings of local contacts and contacts that visibly interacted on the system
44 const DISCOVERY_INTERACTOR = 2;
46 * Discover followers/followings of all contacts
48 const DISCOVERY_ALL = 3;
50 public static function store(int $target, int $actor, string $interaction_date)
52 if ($actor == $target) {
56 DBA::update('contact-relation', ['last-interaction' => $interaction_date], ['cid' => $target, 'relation-cid' => $actor], true);
60 * Fetches the followers of a given profile and adds them
62 * @param string $url URL of a profile
65 public static function discoverByUrl(string $url)
67 $contact_discovery = DI::config()->get('system', 'contact_discovery');
69 if ($contact_discovery == self::DISCOVERY_NONE) {
73 $contact = Contact::getByURL($url);
74 if (empty($contact)) {
78 if ($contact['last-discovery'] > DateTimeFormat::utc('now - 1 month')) {
79 Logger::info('Last discovery was less then a month before.', ['id' => $contact['id'], 'url' => $url, 'discovery' => $contact['last-discovery']]);
83 if ($contact_discovery != self::DISCOVERY_ALL) {
84 $local = DBA::exists('contact', ["`nurl` = ? AND `uid` != ?", Strings::normaliseLink($url), 0]);
85 if (($contact_discovery == self::DISCOVERY_LOCAL) && !$local) {
86 Logger::info('No discovery - This contact is not followed/following locally.', ['id' => $contact['id'], 'url' => $url]);
90 if ($contact_discovery == self::DISCOVERY_INTERACTOR) {
91 $interactor = DBA::exists('contact-relation', ["`relation-cid` = ? AND `last-interaction` > ?", $contact['id'], DBA::NULL_DATETIME]);
92 if (!$local && !$interactor) {
93 Logger::info('No discovery - This contact is not interacting locally.', ['id' => $contact['id'], 'url' => $url]);
97 } elseif ($contact['created'] > DateTimeFormat::utc('now - 1 day')) {
98 Logger::info('Newly created contacs are not discovered to avoid DDoS attacks.', ['id' => $contact['id'], 'url' => $url, 'discovery' => $contact['created']]);
102 $apcontact = APContact::getByURL($url);
104 if (!empty($apcontact['followers']) && is_string($apcontact['followers'])) {
105 $followers = ActivityPub::fetchItems($apcontact['followers']);
110 if (!empty($apcontact['following']) && is_string($apcontact['following'])) {
111 $followings = ActivityPub::fetchItems($apcontact['following']);
116 if (empty($followers) && empty($followings)) {
120 $target = $contact['id'];
122 if (!empty($followers)) {
123 // Clear the follower list, since it will be recreated in the next step
124 DBA::update('contact-relation', ['follows' => false], ['cid' => $target]);
128 foreach (array_merge($followers, $followings) as $contact) {
129 if (is_string($contact)) {
130 $contacts[] = $contact;
131 } elseif (!empty($contact['url']) && is_string($contact['url'])) {
132 $contacts[] = $contact['url'];
135 $contacts = array_unique($contacts);
137 Logger::info('Discover contacts', ['id' => $target, 'url' => $url, 'contacts' => count($contacts)]);
138 foreach ($contacts as $contact) {
139 $actor = Contact::getIdForURL($contact);
140 if (!empty($actor)) {
142 if (in_array($contact, $followers)) {
143 $fields = ['cid' => $target, 'relation-cid' => $actor];
144 } elseif (in_array($contact, $followings)) {
145 $fields = ['cid' => $actor, 'relation-cid' => $target];
150 DBA::update('contact-relation', ['follows' => true, 'follow-updated' => DateTimeFormat::utcNow()], $fields, true);
154 if (!empty($followers)) {
155 // Delete all followers that aren't followers anymore (and aren't interacting)
156 DBA::delete('contact-relation', ['cid' => $target, 'follows' => false, 'last-interaction' => DBA::NULL_DATETIME]);
159 DBA::update('contact', ['last-discovery' => DateTimeFormat::utcNow()], ['id' => $target]);
160 Logger::info('Contacts discovery finished, "last-discovery" set', ['id' => $target, 'url' => $url]);