3 * @copyright Copyright (C) 2010-2022, 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\Core;
24 use Friendica\Database\DBA;
25 use Friendica\Model\User;
26 use Friendica\Network\HTTPException;
27 use Friendica\Protocol\Activity;
28 use Friendica\Protocol\ActivityPub;
29 use Friendica\Protocol\Diaspora;
30 use Friendica\Protocol\OStatus;
31 use Friendica\Protocol\Salmon;
34 * Manage compatibility with federated networks
39 const ACTIVITYPUB = 'apub'; // ActivityPub (Pleroma, Mastodon, Osada, ...)
40 const DFRN = 'dfrn'; // Friendica, Mistpark, other DFRN implementations
41 const DIASPORA = 'dspr'; // Diaspora, Hubzilla, Socialhome, Ganggo
42 const FEED = 'feed'; // RSS/Atom feeds with no known "post/notify" protocol
43 const MAIL = 'mail'; // IMAP/POP
44 const OSTATUS = 'stat'; // GNU Social and other OStatus implementations
46 const NATIVE_SUPPORT = [self::DFRN, self::DIASPORA, self::OSTATUS, self::FEED, self::MAIL, self::ACTIVITYPUB];
48 const FEDERATED = [self::DFRN, self::DIASPORA, self::OSTATUS, self::ACTIVITYPUB];
50 const SUPPORT_PRIVATE = [self::DFRN, self::DIASPORA, self::MAIL, self::ACTIVITYPUB, self::PUMPIO];
52 // Supported through a connector
53 const DIASPORA2 = 'dspc'; // Diaspora connector
54 const LINKEDIN = 'lnkd'; // LinkedIn
55 const PUMPIO = 'pump'; // pump.io
56 const STATUSNET = 'stac'; // Statusnet connector
57 const TWITTER = 'twit'; // Twitter
58 const DISCOURSE = 'dscs'; // Discourse
61 const APPNET = 'apdn'; // app.net - Dead protocol
62 const FACEBOOK = 'face'; // Facebook API - Not working anymore, API is closed
63 const GPLUS = 'goog'; // Google+ - Dead in 2019
65 // Currently unsupported
66 const ICALENDAR = 'ical'; // iCalendar
67 const MYSPACE = 'mysp'; // MySpace
68 const NEWS = 'nntp'; // Network News Transfer Protocol
69 const PNUT = 'pnut'; // pnut.io
70 const XMPP = 'xmpp'; // XMPP
71 const ZOT = 'zot!'; // Zot!
73 const PHANTOM = 'unkn'; // Place holder
76 * Returns whether the provided protocol supports following
80 * @throws HTTPException\InternalServerErrorException
82 public static function supportsFollow($protocol): bool
84 if (in_array($protocol, self::NATIVE_SUPPORT)) {
89 'protocol' => $protocol,
92 Hook::callAll('support_follow', $hook_data);
94 return $hook_data['result'] === true;
98 * Returns whether the provided protocol supports revoking inbound follows
102 * @throws HTTPException\InternalServerErrorException
104 public static function supportsRevokeFollow($protocol): bool
106 if (in_array($protocol, self::NATIVE_SUPPORT)) {
111 'protocol' => $protocol,
114 Hook::callAll('support_revoke_follow', $hook_data);
116 return $hook_data['result'] === true;
120 * Send a follow message to a remote server.
122 * @param int $uid User Id
123 * @param array $contact Contact being followed
124 * @param ?string $protocol Expected protocol
125 * @return bool Only returns false in the unlikely case an ActivityPub contact ID doesn't exist (???)
126 * @throws HTTPException\InternalServerErrorException
127 * @throws \ImagickException
129 public static function follow(int $uid, array $contact, ?string $protocol = null): bool
131 $owner = User::getOwnerDataById($uid);
132 if (!DBA::isResult($owner)) {
136 $protocol = $protocol ?? $contact['protocol'];
138 if (in_array($protocol, [Protocol::OSTATUS, Protocol::DFRN])) {
139 // create a follow slap
141 'verb' => Activity::FOLLOW,
142 'gravity' => GRAVITY_ACTIVITY,
143 'follow' => $contact['url'],
150 $slap = OStatus::salmon($item, $owner);
152 if (!empty($contact['notify'])) {
153 Salmon::slapper($owner, $contact['notify'], $slap);
155 } elseif ($protocol == Protocol::DIASPORA) {
156 $contact = Diaspora::sendShare($owner, $contact);
157 Logger::notice('share returns: ' . $contact);
158 } elseif ($protocol == Protocol::ACTIVITYPUB) {
159 $activity_id = ActivityPub\Transmitter::activityIDFromContact($contact['id']);
160 if (empty($activity_id)) {
161 // This really should never happen
165 $success = ActivityPub\Transmitter::sendActivity('Follow', $contact['url'], $owner['uid'], $activity_id);
166 Logger::notice('Follow returns: ' . $success);
173 * Sends an unfollow message. Does not remove the contact
175 * @param array $contact Target public contact (uid = 0) array
176 * @param array $user Source local user array
177 * @return bool|null true if successful, false if not, null if no remote action was performed
178 * @throws HTTPException\InternalServerErrorException
179 * @throws \ImagickException
181 public static function unfollow(array $contact, array $user): ?bool
183 if (empty($contact['network'])) {
184 throw new \InvalidArgumentException('Missing network key in contact array');
187 $protocol = $contact['network'];
188 if (($protocol == Protocol::DFRN) && !empty($contact['protocol'])) {
189 $protocol = $contact['protocol'];
192 if (in_array($protocol, [Protocol::OSTATUS, Protocol::DFRN])) {
193 // create an unfollow slap
195 $item['verb'] = Activity::O_UNFOLLOW;
196 $item['gravity'] = GRAVITY_ACTIVITY;
197 $item['follow'] = $contact['url'];
202 $slap = OStatus::salmon($item, $user);
204 if (empty($contact['notify'])) {
205 throw new \InvalidArgumentException('Missing expected "notify" key in OStatus/DFRN contact');
208 return Salmon::slapper($user, $contact['notify'], $slap) === 0;
209 } elseif ($protocol == Protocol::DIASPORA) {
210 return Diaspora::sendUnshare($user, $contact) > 0;
211 } elseif ($protocol == Protocol::ACTIVITYPUB) {
212 return ActivityPub\Transmitter::sendContactUndo($contact['url'], $contact['id'], $user['uid']);
215 // Catch-all hook for connector addons
217 'contact' => $contact,
218 'uid' => $user['uid'],
221 Hook::callAll('unfollow', $hook_data);
223 return $hook_data['result'];
227 * Revoke an incoming follow from the provided contact
229 * @param array $contact Target public contact (uid == 0) array
230 * @param int $uid Source local user id
231 * @return bool|null true if successful, false if not, null if no action was performed
232 * @throws \Friendica\Network\HTTPException\InternalServerErrorException
233 * @throws \ImagickException
235 public static function revokeFollow(array $contact, int $uid): ?bool
237 if (empty($contact['network'])) {
238 throw new \InvalidArgumentException('Missing network key in contact array');
241 $protocol = $contact['network'];
242 if ($protocol == Protocol::DFRN && !empty($contact['protocol'])) {
243 $protocol = $contact['protocol'];
246 if ($protocol == Protocol::ACTIVITYPUB) {
247 return ActivityPub\Transmitter::sendContactReject($contact['url'], $contact['hub-verify'], $uid);
250 // Catch-all hook for connector addons
252 'contact' => $contact,
256 Hook::callAll('revoke_follow', $hook_data);
258 return $hook_data['result'];
262 * Send a block message to a remote server. Only useful for connector addons.
264 * @param array $contact Public contact record to block
265 * @param int $uid User issuing the block
266 * @return bool|null true if successful, false if not, null if no action was performed
267 * @throws HTTPException\InternalServerErrorException
269 public static function block(array $contact, int $uid): ?bool
271 // Catch-all hook for connector addons
273 'contact' => $contact,
277 Hook::callAll('block', $hook_data);
279 return $hook_data['result'];
283 * Send an unblock message to a remote server. Only useful for connector addons.
285 * @param array $contact Public contact record to unblock
286 * @param int $uid User revoking the block
287 * @return bool|null true if successful, false if not, null if no action was performed
288 * @throws HTTPException\InternalServerErrorException
290 public static function unblock(array $contact, int $uid): ?bool
292 // Catch-all hook for connector addons
294 'contact' => $contact,
298 Hook::callAll('unblock', $hook_data);
300 return $hook_data['result'];