3 * @copyright Copyright (C) 2010-2023, the Friendica project
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\Protocol\Diaspora\Repository;
24 use Friendica\BaseRepository;
25 use Friendica\Core\System;
26 use Friendica\Database\Database;
27 use Friendica\Database\Definition\DbaDefinition;
28 use Friendica\Model\APContact;
29 use Friendica\Model\Contact;
30 use Friendica\Model\Item;
31 use Friendica\Model\ItemURI;
32 use Friendica\Network\HTTPException;
33 use Friendica\Protocol\Diaspora\Entity;
34 use Friendica\Protocol\Diaspora\Factory;
35 use Friendica\Protocol\WebFingerUri;
36 use Friendica\Util\DateTimeFormat;
37 use Psr\Http\Message\UriInterface;
38 use Psr\Log\LoggerInterface;
40 class DiasporaContact extends BaseRepository
42 const ALWAYS_UPDATE = true;
43 const NEVER_UPDATE = false;
44 const UPDATE_IF_MISSING_OR_OUTDATED = null;
46 protected static $table_name = 'diaspora-contact-view';
48 /** @var Factory\DiasporaContact */
50 /** @var DbaDefinition */
53 public function __construct(DbaDefinition $definition, Database $database, LoggerInterface $logger, Factory\DiasporaContact $factory)
55 parent::__construct($database, $logger, $factory);
57 $this->definition = $definition;
61 * @param array $condition
62 * @param array $params
63 * @return Entity\DiasporaContact
64 * @throws HTTPException\NotFoundException
66 public function selectOne(array $condition, array $params = []): Entity\DiasporaContact
68 return parent::_selectOne($condition, $params);
73 * @return Entity\DiasporaContact
74 * @throws HTTPException\NotFoundException
76 public function selectOneByUriId(int $uriId): Entity\DiasporaContact
78 return $this->selectOne(['uri-id' => $uriId]);
82 * @param UriInterface $uri
83 * @return Entity\DiasporaContact
84 * @throws HTTPException\NotFoundException
86 public function selectOneByUri(UriInterface $uri): Entity\DiasporaContact
89 return $this->selectOne(['url' => (string) $uri]);
90 } catch (HTTPException\NotFoundException $e) {
94 return $this->selectOne(['addr' => (string) $uri]);
95 } catch (HTTPException\NotFoundException $e) {
98 return $this->selectOne(['alias' => (string) $uri]);
102 * @param WebFingerUri $uri
103 * @return Entity\DiasporaContact
104 * @throws HTTPException\NotFoundException
106 public function selectOneByAddr(WebFingerUri $uri): Entity\DiasporaContact
108 return $this->selectOne(['addr' => $uri->getAddr()]);
116 public function existsByUriId(int $uriId): bool
118 return $this->db->exists(self::$table_name, ['uri-id' => $uriId]);
121 public function save(Entity\DiasporaContact $DiasporaContact): Entity\DiasporaContact
123 $uriId = $DiasporaContact->uriId ?? ItemURI::insert(['uri' => $DiasporaContact->url, 'guid' => $DiasporaContact->guid]);
127 'addr' => $DiasporaContact->addr,
128 'alias' => (string)$DiasporaContact->alias,
129 'nick' => $DiasporaContact->nick,
130 'name' => $DiasporaContact->name,
131 'given-name' => $DiasporaContact->givenName,
132 'family-name' => $DiasporaContact->familyName,
133 'photo' => (string)$DiasporaContact->photo,
134 'photo-medium' => (string)$DiasporaContact->photoMedium,
135 'photo-small' => (string)$DiasporaContact->photoSmall,
136 'batch' => (string)$DiasporaContact->batch,
137 'notify' => (string)$DiasporaContact->notify,
138 'poll' => (string)$DiasporaContact->poll,
139 'subscribe' => (string)$DiasporaContact->subscribe,
140 'searchable' => $DiasporaContact->searchable,
141 'pubkey' => $DiasporaContact->pubKey,
142 'gsid' => $DiasporaContact->gsid,
143 'created' => $DiasporaContact->created->format(DateTimeFormat::MYSQL),
144 'updated' => DateTimeFormat::utcNow(),
145 'interacting_count' => $DiasporaContact->interacting_count,
146 'interacted_count' => $DiasporaContact->interacted_count,
147 'post_count' => $DiasporaContact->post_count,
150 // Limit the length on incoming fields
151 $fields = $this->definition->truncateFieldsForTable('diaspora-contact', $fields);
153 $this->db->insert('diaspora-contact', $fields, Database::INSERT_UPDATE);
155 return $this->selectOneByUriId($uriId);
159 * Fetch a Diaspora profile from a given WebFinger address and updates it depending on the mode
161 * @param WebFingerUri $uri Profile address
162 * @param boolean $update true = always update, false = never update, null = update when not found or outdated
163 * @return Entity\DiasporaContact
164 * @throws HTTPException\NotFoundException
166 public function getByAddr(WebFingerUri $uri, ?bool $update = self::UPDATE_IF_MISSING_OR_OUTDATED): Entity\DiasporaContact
168 if ($update !== self::ALWAYS_UPDATE) {
170 $dcontact = $this->selectOneByAddr($uri);
171 if ($update === self::NEVER_UPDATE) {
174 } catch (HTTPException\NotFoundException $e) {
175 if ($update === self::NEVER_UPDATE) {
179 // This is necessary for Contact::getByURL in case the base contact record doesn't need probing,
180 // but we still need the result of a probe to create the missing diaspora-contact record.
181 $update = self::ALWAYS_UPDATE;
185 $contact = Contact::getByURL($uri, $update, ['uri-id']);
186 if (empty($contact['uri-id'])) {
187 throw new HTTPException\NotFoundException('Diaspora profile with URI ' . $uri . ' not found');
190 return self::selectOneByUriId($contact['uri-id']);
194 * Fetch a Diaspora profile from a given profile URL and updates it depending on the mode
196 * @param UriInterface $uri Profile URL
197 * @param boolean $update true = always update, false = never update, null = update when not found or outdated
198 * @return Entity\DiasporaContact
199 * @throws HTTPException\NotFoundException
201 public function getByUrl(UriInterface $uri, ?bool $update = self::UPDATE_IF_MISSING_OR_OUTDATED): Entity\DiasporaContact
203 if ($update !== self::ALWAYS_UPDATE) {
205 $dcontact = $this->selectOneByUriId(ItemURI::getIdByURI($uri));
206 if ($update === self::NEVER_UPDATE) {
209 } catch (HTTPException\NotFoundException $e) {
210 if ($update === self::NEVER_UPDATE) {
214 // This is necessary for Contact::getByURL in case the base contact record doesn't need probing,
215 // but we still need the result of a probe to create the missing diaspora-contact record.
216 $update = self::ALWAYS_UPDATE;
220 $contact = Contact::getByURL($uri, $update, ['uri-id']);
221 if (empty($contact['uri-id'])) {
222 throw new HTTPException\NotFoundException('Diaspora profile with URI ' . $uri . ' not found');
225 return self::selectOneByUriId($contact['uri-id']);
229 * Update or create a diaspora-contact entry via a probe array
231 * @param array $data Probe array
232 * @return Entity\DiasporaContact
235 public function updateFromProbeArray(array $data): Entity\DiasporaContact
237 if (empty($data['url'])) {
238 throw new \InvalidArgumentException('Missing url key in Diaspora probe data array');
241 if (empty($data['guid'])) {
242 throw new \InvalidArgumentException('Missing guid key in Diaspora probe data array');
245 if (empty($data['pubkey'])) {
246 throw new \InvalidArgumentException('Missing pubkey key in Diaspora probe data array');
249 $uriId = ItemURI::insert(['uri' => $data['url'], 'guid' => $data['guid']]);
251 $contact = Contact::getByUriId($uriId, ['id', 'created']);
252 $apcontact = APContact::getByURL($data['url'], false);
253 if (!empty($apcontact)) {
254 $interacting_count = $apcontact['followers_count'];
255 $interacted_count = $apcontact['following_count'];
256 $post_count = $apcontact['statuses_count'];
257 } elseif (!empty($contact['id'])) {
258 $last_interaction = DateTimeFormat::utc('now - 180 days');
260 $interacting_count = $this->db->count('contact-relation', ["`relation-cid` = ? AND NOT `follows` AND `last-interaction` > ?", $contact['id'], $last_interaction]);
261 $interacted_count = $this->db->count('contact-relation', ["`cid` = ? AND NOT `follows` AND `last-interaction` > ?", $contact['id'], $last_interaction]);
262 $post_count = $this->db->count('post', ['author-id' => $contact['id'], 'gravity' => [Item::GRAVITY_PARENT, Item::GRAVITY_COMMENT]]);
265 $DiasporaContact = $this->factory->createfromProbeData(
268 new \DateTime($contact['created'] ?? 'now', new \DateTimeZone('UTC')),
269 $interacting_count ?? 0,
270 $interacted_count ?? 0,
274 $DiasporaContact = $this->save($DiasporaContact);
276 $this->logger->info('Updated diaspora-contact', ['url' => (string) $DiasporaContact->url, 'callstack' => System::callstack(20)]);
278 return $DiasporaContact;
282 * get a url (scheme://domain.tld/u/user) from a given contact guid
284 * @param mixed $guid Hexadecimal string guid
286 * @return string the contact url or null
289 public function getUrlByGuid(string $guid): ?string
291 $diasporaContact = $this->db->selectFirst(self::$table_name, ['url'], ['guid' => $guid]);
293 return $diasporaContact['url'] ?? null;