OStatus support removed

This commit is contained in:
Michael 2024-08-24 04:27:00 +00:00
commit e8a3be6820
87 changed files with 773 additions and 4383 deletions

View file

@ -70,8 +70,6 @@ CREATE TABLE IF NOT EXISTS `user` (
`theme` varchar(255) NOT NULL DEFAULT '' COMMENT 'user theme preference',
`pubkey` text COMMENT 'RSA public key 4096 bit',
`prvkey` text COMMENT 'RSA private key 4096 bit',
`spubkey` text COMMENT '',
`sprvkey` text COMMENT '',
`verified` boolean NOT NULL DEFAULT '0' COMMENT 'user is verified through email',
`blocked` boolean NOT NULL DEFAULT '0' COMMENT '1 for user is blocked',
`blockwall` boolean NOT NULL DEFAULT '0' COMMENT 'Prohibit contacts to post to the profile page of the user',
@ -183,7 +181,6 @@ CREATE TABLE IF NOT EXISTS `contact` (
`remote_self` boolean NOT NULL DEFAULT '0' COMMENT '',
`rel` tinyint unsigned NOT NULL DEFAULT 0 COMMENT 'The kind of the relation between the user and the contact',
`protocol` char(4) NOT NULL DEFAULT '' COMMENT 'Protocol of the contact',
`subhub` boolean NOT NULL DEFAULT '0' COMMENT '',
`hub-verify` varbinary(383) NOT NULL DEFAULT '' COMMENT '',
`rating` tinyint NOT NULL DEFAULT 0 COMMENT 'Automatically detected feed poll frequency',
`priority` tinyint unsigned NOT NULL DEFAULT 0 COMMENT 'Feed poll priority',
@ -1345,7 +1342,6 @@ CREATE TABLE IF NOT EXISTS `post-delivery-data` (
`dfrn` mediumint NOT NULL DEFAULT 0 COMMENT 'Number of successful deliveries via DFRN',
`legacy_dfrn` mediumint NOT NULL DEFAULT 0 COMMENT 'Number of successful deliveries via legacy DFRN',
`diaspora` mediumint NOT NULL DEFAULT 0 COMMENT 'Number of successful deliveries via Diaspora',
`ostatus` mediumint NOT NULL DEFAULT 0 COMMENT 'Number of successful deliveries via OStatus',
PRIMARY KEY(`uri-id`),
FOREIGN KEY (`uri-id`) REFERENCES `item-uri` (`id`) ON UPDATE RESTRICT ON DELETE CASCADE
) DEFAULT COLLATE utf8mb4_general_ci COMMENT='Delivery data for items';
@ -1812,26 +1808,6 @@ CREATE TABLE IF NOT EXISTS `profile_field` (
FOREIGN KEY (`psid`) REFERENCES `permissionset` (`id`) ON UPDATE RESTRICT ON DELETE RESTRICT
) DEFAULT COLLATE utf8mb4_general_ci COMMENT='Custom profile fields';
--
-- TABLE push_subscriber
--
CREATE TABLE IF NOT EXISTS `push_subscriber` (
`id` int unsigned NOT NULL auto_increment COMMENT 'sequential ID',
`uid` mediumint unsigned NOT NULL DEFAULT 0 COMMENT 'User id',
`callback_url` varbinary(383) NOT NULL DEFAULT '' COMMENT '',
`topic` varchar(255) NOT NULL DEFAULT '' COMMENT '',
`nickname` varchar(255) NOT NULL DEFAULT '' COMMENT '',
`push` tinyint NOT NULL DEFAULT 0 COMMENT 'Retrial counter',
`last_update` datetime NOT NULL DEFAULT '0001-01-01 00:00:00' COMMENT 'Date of last successful trial',
`next_try` datetime NOT NULL DEFAULT '0001-01-01 00:00:00' COMMENT 'Next retrial date',
`renewed` datetime NOT NULL DEFAULT '0001-01-01 00:00:00' COMMENT 'Date of last subscription renewal',
`secret` varchar(255) NOT NULL DEFAULT '' COMMENT '',
PRIMARY KEY(`id`),
INDEX `next_try` (`next_try`),
INDEX `uid` (`uid`),
FOREIGN KEY (`uid`) REFERENCES `user` (`uid`) ON UPDATE RESTRICT ON DELETE CASCADE
) DEFAULT COLLATE utf8mb4_general_ci COMMENT='Used for OStatus: Contains feed subscribers';
--
-- TABLE register
--
@ -2010,7 +1986,6 @@ CREATE TABLE IF NOT EXISTS `user-contact` (
`remote_self` tinyint unsigned COMMENT '0 => No mirroring, 1-2 => Mirror as own post, 3 => Mirror as reshare',
`fetch_further_information` tinyint unsigned COMMENT '0 => None, 1 => Fetch information, 3 => Fetch keywords, 2 => Fetch both',
`ffi_keyword_denylist` text COMMENT '',
`subhub` boolean COMMENT '',
`hub-verify` varbinary(383) COMMENT '',
`protocol` char(4) COMMENT 'Protocol of the contact',
`rating` tinyint COMMENT 'Automatically detected feed poll frequency',
@ -3516,7 +3491,6 @@ CREATE VIEW `owner-view` AS SELECT
`contact`.`poll` AS `poll`,
`contact`.`confirm` AS `confirm`,
`contact`.`poco` AS `poco`,
`contact`.`subhub` AS `subhub`,
`contact`.`hub-verify` AS `hub-verify`,
`contact`.`last-update` AS `last-update`,
`contact`.`success_update` AS `success_update`,
@ -3565,8 +3539,6 @@ CREATE VIEW `owner-view` AS SELECT
`user`.`theme` AS `theme`,
`user`.`pubkey` AS `upubkey`,
`user`.`prvkey` AS `uprvkey`,
`user`.`sprvkey` AS `sprvkey`,
`user`.`spubkey` AS `spubkey`,
`user`.`verified` AS `verified`,
`user`.`blockwall` AS `blockwall`,
`user`.`hidewall` AS `hidewall`,
@ -3763,7 +3735,6 @@ CREATE VIEW `account-user-view` AS SELECT
`ucontact`.`readonly` AS `readonly`,
`ucontact`.`blocked` AS `blocked`,
`ucontact`.`block_reason` AS `block_reason`,
`ucontact`.`subhub` AS `subhub`,
`ucontact`.`hub-verify` AS `hub-verify`,
`ucontact`.`reason` AS `reason`,
`contact`.`notify` AS `dfrn-notify`,

View file

@ -84,7 +84,6 @@ Alternatives are presented with "|".
* network:dspr | network:diaspora - The Diaspora protocol is mainly used by Diaspora itself. Some other systems support the protocol as well like Hubzilla, Socialhome or Ganggo.
* network:feed - RSS/Atom feeds
* network:mail - Mails that had been imported via IMAP.
* network:stat | network:ostatus - The OStatus protocol is mainly used by old GNU Social installations.
* network:dscs | network:discourse - Posts that are received by the Discourse connector.
* network:tmbl | network:tumblr - Posts that are received by the Tumblr connector.
* network:bsky | network:bluesky - Posts that are received by the Bluesky connector.

View file

@ -28,23 +28,6 @@ Diaspora
Add the Diaspora 'handle' to the 'Connect/Follow' text box on your [Contacts](contacts) page.
GNU Social
---
This is described as the "federated social web" or OStatus contacts.
Please note that there are **no** privacy provisions on the OStatus network.
Any message which is delivered to **any** OStatus member is visible to anybody in the world and will negate any privacy settings that you have in effect.
These messages will also turn up in public searches.
Since OStatus communications do not use authentication, if you select the profile privacy option to hide your profile and messages from unknown viewers, OStatus members will **not** be able to receive your communications.
To connect with an OStatus member insert their profile URL or Identity address into the Connect box on your [Contacts](contacts) page.
The GNU Social connector may be used if you wish posts to appear on an OStatus site using an existing OStatus account.
It is not necessary to do this, as you may 'follow' OStatus members from Friendica and they may follow you (by placing their own Identity Address into your 'Connect' page).
Blogger, Wordpress, RSS feeds, arbitrary web pages
---

View file

@ -14,10 +14,6 @@ Please go to /admin/site/ on your system and change the following values:
This value reduces the data that is send from the server to the client. 50 is a value that doesn't influences image quality too much.
Set "OStatus conversation completion interval" to "never".
If you have many OStatus contacts then completing of conversations can take some time. Since you will miss several comments in OStatus threads, you maybe should consider the option "At post arrival" instead.
Enable "Use MySQL full text engine"
When using MyISAM (default) or InnoDB on MariaDB 10 this speeds up search.

View file

@ -21,11 +21,6 @@ Friendica will recreate your account on the new server, with your contacts and c
A message is sent to Friendica contacts, to inform them about your move:
If your contacts are running on an updated server, your details on their side will be automatically updated.
GNU Social contacts
---
Contacts on GNU Social will be archived, as we can't inform them about your move.
You should ask them to remove your contact from their lists and re-add you, and you should do the same with their contact.
Diaspora contacts
---
Newer Diaspora servers are able to process "account migration" messages.

View file

@ -20,23 +20,9 @@ Additional types are used for non standard activities.
* [Link to the specification](http://activitystrea.ms/head/activity-schema.html)
* [List of used ActivityStreams verbs and object types.](https://github.com/friendica/friendica/wiki/ActivityStreams)
Salmon
---
Salmon is used as a message exchange protocol for replies and mentions.
* [Link to the protocol summary](http://www.salmon-protocol.org/salmon-protocol-summary)
Portable Contacts
---
Portable Contacts is used for friends lists.
* [Link to the specification](https://web.archive.org/web/20160426223008/http://portablecontacts.net/draft-spec.html) (Link to archive.org)
pubsubhubbub
---
pubsubhubbub is used for OStatus.
* [Link to the specification](https://pubsubhubbub.github.io/PubSubHubbub/pubsubhubbub-core-0.4.html)

View file

@ -80,7 +80,6 @@ Database Tables
| [process](help/database/db_process) | Currently running system processes |
| [profile](help/database/db_profile) | user profiles data |
| [profile_field](help/database/db_profile_field) | Custom profile fields |
| [push_subscriber](help/database/db_push_subscriber) | Used for OStatus: Contains feed subscribers |
| [register](help/database/db_register) | registrations requiring admin approval |
| [report](help/database/db_report) | |
| [report-post](help/database/db_report-post) | Individual posts attached to a moderation report |

View file

@ -59,7 +59,6 @@ Fields
| remote_self | | boolean | NO | | 0 | |
| rel | The kind of the relation between the user and the contact | tinyint unsigned | NO | | 0 | |
| protocol | Protocol of the contact | char(4) | NO | | | |
| subhub | | boolean | NO | | 0 | |
| hub-verify | | varbinary(383) | NO | | | |
| rating | Automatically detected feed poll frequency | tinyint | NO | | 0 | |
| priority | Feed poll priority | tinyint unsigned | NO | | 0 | |

View file

@ -18,7 +18,6 @@ Fields
| dfrn | Number of successful deliveries via DFRN | mediumint | NO | | 0 | |
| legacy_dfrn | Number of successful deliveries via legacy DFRN | mediumint | NO | | 0 | |
| diaspora | Number of successful deliveries via Diaspora | mediumint | NO | | 0 | |
| ostatus | Number of successful deliveries via OStatus | mediumint | NO | | 0 | |
Indexes
------------

View file

@ -1,38 +0,0 @@
Table push_subscriber
===========
Used for OStatus: Contains feed subscribers
Fields
------
| Field | Description | Type | Null | Key | Default | Extra |
| ------------ | --------------------------------- | ------------------ | ---- | --- | ------------------- | -------------- |
| id | sequential ID | int unsigned | NO | PRI | NULL | auto_increment |
| uid | User id | mediumint unsigned | NO | | 0 | |
| callback_url | | varbinary(383) | NO | | | |
| topic | | varchar(255) | NO | | | |
| nickname | | varchar(255) | NO | | | |
| push | Retrial counter | tinyint | NO | | 0 | |
| last_update | Date of last successful trial | datetime | NO | | 0001-01-01 00:00:00 | |
| next_try | Next retrial date | datetime | NO | | 0001-01-01 00:00:00 | |
| renewed | Date of last subscription renewal | datetime | NO | | 0001-01-01 00:00:00 | |
| secret | | varchar(255) | NO | | | |
Indexes
------------
| Name | Fields |
| -------- | -------- |
| PRIMARY | id |
| next_try | next_try |
| uid | uid |
Foreign Keys
------------
| Field | Target Table | Target Field |
|-------|--------------|--------------|
| uid | [user](help/database/db_user) | uid |
Return to [database documentation](help/database)

View file

@ -25,7 +25,6 @@ Fields
| remote_self | 0 => No mirroring, 1-2 => Mirror as own post, 3 => Mirror as reshare | tinyint unsigned | YES | | NULL | |
| fetch_further_information | 0 => None, 1 => Fetch information, 3 => Fetch keywords, 2 => Fetch both | tinyint unsigned | YES | | NULL | |
| ffi_keyword_denylist | | text | YES | | NULL | |
| subhub | | boolean | YES | | NULL | |
| hub-verify | | varbinary(383) | YES | | NULL | |
| protocol | Protocol of the contact | char(4) | YES | | NULL | |
| rating | Automatically detected feed poll frequency | tinyint | YES | | NULL | |

View file

@ -27,8 +27,6 @@ Fields
| theme | user theme preference | varchar(255) | NO | | | |
| pubkey | RSA public key 4096 bit | text | YES | | NULL | |
| prvkey | RSA private key 4096 bit | text | YES | | NULL | |
| spubkey | | text | YES | | NULL | |
| sprvkey | | text | YES | | NULL | |
| verified | user is verified through email | boolean | NO | | 0 | |
| blocked | 1 for user is blocked | boolean | NO | | 0 | |
| blockwall | Prohibit contacts to post to the profile page of the user | boolean | NO | | 0 | |

View file

@ -84,7 +84,6 @@ Alternativen werden durch "|" dargestellt.
* network:dspr | network:diaspora - Das Diaspora-Protokoll wird hauptsächlich von Diaspora selbst genutzt. Ein paar andere Systeme unterstützen dieses Protokoll ebenfalls, wie Hubzilla, Socialhome or Ganggo.
* network:feed - RSS/Atom feeds
* network:mail - Mails die via IMAP importiert worden sind.
* network:stat | network:ostatus - Das OStatus-Protokoll wird hauptsächlich von alten GNU Social-Installationen genutzt.
* network:dscs | network:discourse - Beiträge, die über den Discourse connector empfangen werden.
* network:tmbl | network:tumblr - Beiträge, die über den Tumblr connector empfangen werden.
* network:bsky | network:bluesky - Beiträge, die über den Bluesky connector empfangen werden.

View file

@ -27,24 +27,6 @@ Ebenso kannst Du deren Identitäts-Adresse in der "Verbinden"-Box auf Deiner ["K
Füge die Diaspora-Identitäts-Adresse (z.B. name@diasporapod.com)auf Deiner ["Kontakte"-Seite](contacts) in das Feld "Neuen Kontakt hinzufügen" ein.
**GNU Social**
Dieses Netzwerk wird als "federated social web" bzw. "OStatus"-Kontakte bezeichnet.
Bitte beachte, dass es **keine** Einstellungen zur Privatssphäre im OStatus-Netzwerk gibt.
**Jede** Nachricht, die an eines dieser OStatus-Mitglieder verschickt wird, ist für jeden auf der Welt sichtbar; alle Privatssphäreneinstellungen verlieren ihre Wirkung.
Diese Nachrichten erscheinen ebenfalls in öffentlichen Suchergebnissen.
Da die OStatus-Kommunikation keine Authentifizierung benutzt, können OStatus-Nutzer *keine* Nachrichten empfangen, wenn Du in Deinen Privatssphäreneinstellungen "Profil und Nachrichten vor Unbekannten verbergen" wählst.
Um Dich mit einem OStatus-Mitglied zu verbinden, trage deren Profil-URL oder Identitäts-Adresse auf Deiner ["Kontakte"-Seite](contacts) in das Feld "Neuen Kontakt hinzufügen" ein.
Der GNU Social-Konnektor kann genutzt werden, wenn Du Beiträge schreiben willst, die auf einer OStatus-Seite über einen existierenden OStatus-Account erscheinen sollen.
Das ist nicht notwendig, wenn Du OStatus-Mitgliedern von Friendica aus folgst und diese Dir auch folgen, indem sie auf Deiner Kontaktseite ihre eigene Identitäts-Adresse eingeben.
**Blogger, Wordpress, RSS feeds, andere Webseiten**
Trage die URL auf Deiner ["Kontakte"-Seite](contacts) in das Feld "Neuen Kontakt hinzufügen" ein.

View file

@ -388,38 +388,14 @@ function photos_post()
foreach ($tags as $tag) {
if (strpos($tag, '@') === 0) {
$profile = '';
$contact = null;
$name = substr($tag, 1);
if ((strpos($name, '@')) || (strpos($name, 'http://'))) {
$contact = Contact::getByURL($name);
if (empty($contact)) {
$newname = $name;
$links = @Probe::lrdd($name);
if (count($links)) {
foreach ($links as $link) {
if ($link['@attributes']['rel'] === ActivityNamespace::WEBFINGERPROFILE) {
$profile = $link['@attributes']['href'];
}
if ($link['@attributes']['rel'] === 'salmon') {
$salmon = '$url:' . str_replace(',', '%sc', $link['@attributes']['href']);
if (strlen($inform)) {
$inform .= ',';
}
$inform .= $salmon;
}
}
}
$taginfo[] = [$newname, $profile, $salmon];
} else {
$newname = $name;
$tagcid = 0;
if (strrpos($newname, '+')) {
$tagcid = intval(substr($newname, strrpos($newname, '+') + 1));
} else {
$tagcid = 0;
}
if ($tagcid) {
@ -439,17 +415,17 @@ function photos_post()
);
}
}
}
if (DBA::isResult($contact)) {
$newname = $contact['name'];
$profile = $contact['url'];
if (DBA::isResult($contact)) {
$newname = $contact['name'];
$profile = $contact['url'];
$notify = 'cid:' . $contact['id'];
if (strlen($inform)) {
$inform .= ',';
}
$inform .= $notify;
$notify = 'cid:' . $contact['id'];
if (strlen($inform)) {
$inform .= ',';
}
$inform .= $notify;
}
if ($profile) {

View file

@ -53,11 +53,8 @@ class Mode
'objects',
'outbox',
'poco',
'pubsub',
'pubsubhubbub',
'receive',
'rsd_xml',
'salmon',
'statistics_json',
'xrd',
];

View file

@ -25,7 +25,6 @@ use Friendica\Model\Contact;
* @property-read int $remoteSelf
* @property-read int $fetchFurtherInformation
* @property-read string $ffiKeywordDenylist
* @property-read bool $subhub
* @property-read string $hubVerify
* @property-read string $protocol
* @property-read int $rating
@ -69,8 +68,6 @@ class LocalRelationship extends \Friendica\BaseEntity
protected $fetchFurtherInformation;
/** @var string */
protected $ffiKeywordDenylist;
/** @var bool */
protected $subhub;
/** @var string */
protected $hubVerify;
/** @var string */
@ -80,7 +77,7 @@ class LocalRelationship extends \Friendica\BaseEntity
/** @var int */
protected $priority;
public function __construct(int $userId, int $contactId, bool $blocked = false, bool $ignored = false, bool $collapsed = false, bool $hidden = false, bool $pending = false, int $rel = Contact::NOTHING, string $info = '', bool $notifyNewPosts = false, int $remoteSelf = self::MIRROR_DEACTIVATED, int $fetchFurtherInformation = self::FFI_NONE, string $ffiKeywordDenylist = '', bool $subhub = false, string $hubVerify = '', string $protocol = Protocol::PHANTOM, ?int $rating = null, ?int $priority = null)
public function __construct(int $userId, int $contactId, bool $blocked = false, bool $ignored = false, bool $collapsed = false, bool $hidden = false, bool $pending = false, int $rel = Contact::NOTHING, string $info = '', bool $notifyNewPosts = false, int $remoteSelf = self::MIRROR_DEACTIVATED, int $fetchFurtherInformation = self::FFI_NONE, string $ffiKeywordDenylist = '', string $hubVerify = '', string $protocol = Protocol::PHANTOM, ?int $rating = null, ?int $priority = null)
{
$this->userId = $userId;
$this->contactId = $contactId;
@ -95,7 +92,6 @@ class LocalRelationship extends \Friendica\BaseEntity
$this->remoteSelf = $remoteSelf;
$this->fetchFurtherInformation = $fetchFurtherInformation;
$this->ffiKeywordDenylist = $ffiKeywordDenylist;
$this->subhub = $subhub;
$this->hubVerify = $hubVerify;
$this->protocol = $protocol;
$this->rating = $rating;

View file

@ -34,7 +34,6 @@ class LocalRelationship extends BaseFactory implements ICanCreateFromTableRow
$row['remote_self'] ?? Entity\LocalRelationship::MIRROR_DEACTIVATED,
$row['fetch_further_information'] ?? Entity\LocalRelationship::FFI_NONE,
$row['ffi_keyword_denylist'] ?? '',
$row['subhub'] ?? false,
$row['hub-verify'] ?? '',
$row['protocol'] ?? Protocol::PHANTOM,
$row['rating'] ?? null,

View file

@ -89,7 +89,6 @@ class LocalRelationship extends \Friendica\BaseRepository
'remote_self' => $localRelationship->remoteSelf,
'fetch_further_information' => $localRelationship->fetchFurtherInformation,
'ffi_keyword_denylist' => $localRelationship->ffiKeywordDenylist,
'subhub' => $localRelationship->subhub,
'hub-verify' => $localRelationship->hubVerify,
'protocol' => $localRelationship->protocol,
'rating' => $localRelationship->rating,

View file

@ -48,7 +48,6 @@ class BBCode
const CONNECTORS = 4;
const TWITTER_API = 5;
const NPF = 6;
const OSTATUS = 7;
const TWITTER = 8;
const BACKLINK = 8;
const ACTIVITYPUB = 9;
@ -910,9 +909,6 @@ class BBCode
$text = ($is_quote_share ? '<hr />' : '') . $headline . '<blockquote class="shared_content" dir="auto">' . trim($content) . '</blockquote>' . "\n";
break;
case self::OSTATUS:
$text = ($is_quote_share ? '<br>' : '') . '<p>' . html_entity_decode('&#x2672; ', ENT_QUOTES, 'UTF-8') . ' @' . $author_contact['addr'] . ': ' . $content . '</p>' . "\n";
break;
case self::ACTIVITYPUB:
$author = '@<span class="vcard"><a href="' . $author_contact['url'] . '" class="url u-url mention" title="' . $author_contact['addr'] . '"><span class="fn nickname mention">' . $author_contact['addr'] . '</span></a>:</span>';
@ -1252,7 +1248,7 @@ class BBCode
* - 4: Used for WordPress, Libertree (before Markdown), pump.io and tumblr
* - 5: Unused
* - 6: Unused
* - 7: Used for dfrn, OStatus
* - 7: Used for dfrn
* - 8: Used for Twitter, WP backlink text setting
* - 9: ActivityPub
*
@ -1283,7 +1279,7 @@ class BBCode
* - 4: Used for WordPress, Libertree (before Markdown), pump.io and tumblr
* - 5: Unused
* - 6: Unused
* - 7: Used for dfrn, OStatus
* - 7: Used for dfrn
* - 8: Used for Twitter, WP backlink text setting
* - 9: ActivityPub
*
@ -1971,7 +1967,7 @@ class BBCode
'@<a href="$2">$3</a>',
$text
);
} elseif (in_array($simple_html, [self::OSTATUS, self::ACTIVITYPUB])) {
} elseif (in_array($simple_html, [self::ACTIVITYPUB])) {
$text = preg_replace(
"/([@!])\[url\=(.*?)\](.*?)\[\/url\]/ism",
'<span class="h-card"><a href="$2" class="u-url mention">$1<span>$3</span></a></span>',
@ -2017,7 +2013,7 @@ class BBCode
$text
);
if (in_array($simple_html, [self::OSTATUS, self::TWITTER, self::BLUESKY])) {
if (in_array($simple_html, [self::TWITTER, self::BLUESKY])) {
$text = preg_replace_callback("/([^#@!])\[url\=([^\]]*)\](.*?)\[\/url\]/ism", [self::class, 'expandLinksCallback'], $text);
//$text = preg_replace("/[^#@!]\[url\=([^\]]*)\](.*?)\[\/url\]/ism", ' $2 [url]$1[/url]', $text);
$text = preg_replace("/\[bookmark\=([^\]]*)\](.*?)\[\/bookmark\]/ism", ' $2 [url]$1[/url]', $text);
@ -2075,7 +2071,7 @@ class BBCode
// Red compatibility, though the link can't be authenticated on Friendica
$text = preg_replace("/\[zrl\=(.*?)\](.*?)\[\/zrl\]/ism", '[url=$1]$2[/url]', $text);
if (in_array($simple_html, [self::INTERNAL, self::EXTERNAL, self::DIASPORA, self::OSTATUS, self::MASTODON_API, self::TWITTER_API, self::ACTIVITYPUB])) {
if (in_array($simple_html, [self::INTERNAL, self::EXTERNAL, self::DIASPORA, self::MASTODON_API, self::TWITTER_API, self::ACTIVITYPUB])) {
$text = self::shortenLinkDescription($text, $simple_html);
} else {
$text = self::unifyLinks($text);

View file

@ -119,10 +119,6 @@ class Plaintext
$abstract = BBCode::getAbstract($item['body'], Protocol::TWITTER);
break;
case BBCode::OSTATUS:
$abstract = BBCode::getAbstract($item['body'], Protocol::STATUSNET);
break;
case BBCode::BLUESKY:
$abstract = BBCode::getAbstract($item['body'], Protocol::BLUESKY);
break;

View file

@ -88,17 +88,13 @@ class Widget
public static function unavailableNetworks(): array
{
// Always hide content from these networks
$networks = [Protocol::PHANTOM, Protocol::FACEBOOK, Protocol::APPNET, Protocol::TWITTER, Protocol::ZOT];
$networks = [Protocol::PHANTOM, Protocol::FACEBOOK, Protocol::APPNET, Protocol::TWITTER, Protocol::ZOT, Protocol::OSTATUS, Protocol::STATUSNET];
Addon::loadAddons();
if (!Addon::isEnabled("discourse")) {
$networks[] = Protocol::DISCOURSE;
}
if (!Addon::isEnabled("statusnet")) {
$networks[] = Protocol::STATUSNET;
}
if (!Addon::isEnabled("pumpio")) {
$networks[] = Protocol::PUMPIO;
}
@ -107,10 +103,6 @@ class Widget
$networks[] = Protocol::TUMBLR;
}
if (DI::config()->get("system", "ostatus_disabled")) {
$networks[] = Protocol::OSTATUS;
}
if (!DI::config()->get("system", "diaspora_enabled")) {
$networks[] = Protocol::DIASPORA;
}

View file

@ -59,7 +59,7 @@ class ContactBlock
'hidden' => false,
'archive' => false,
'failed' => false,
'network' => [Protocol::DFRN, Protocol::ACTIVITYPUB, Protocol::OSTATUS, Protocol::DIASPORA, Protocol::FEED],
'network' => [Protocol::DFRN, Protocol::ACTIVITYPUB, Protocol::DIASPORA, Protocol::FEED],
]);
$contacts_title = DI::l10n()->t('No contacts');

View file

@ -8,14 +8,10 @@
namespace Friendica\Core;
use Friendica\Database\DBA;
use Friendica\Model\Item;
use Friendica\Model\User;
use Friendica\Network\HTTPException;
use Friendica\Protocol\Activity;
use Friendica\Protocol\ActivityPub;
use Friendica\Protocol\Diaspora;
use Friendica\Protocol\OStatus;
use Friendica\Protocol\Salmon;
/**
* Manage compatibility with federated networks
@ -28,24 +24,24 @@ class Protocol
const DIASPORA = 'dspr'; // Diaspora, Hubzilla, Socialhome, Ganggo
const FEED = 'feed'; // RSS/Atom feeds with no known "post/notify" protocol
const MAIL = 'mail'; // IMAP/POP
const OSTATUS = 'stat'; // GNU Social and other OStatus implementations
const NATIVE_SUPPORT = [self::DFRN, self::DIASPORA, self::OSTATUS, self::FEED, self::MAIL, self::ACTIVITYPUB];
const NATIVE_SUPPORT = [self::DFRN, self::DIASPORA, self::FEED, self::MAIL, self::ACTIVITYPUB];
const FEDERATED = [self::DFRN, self::DIASPORA, self::OSTATUS, self::ACTIVITYPUB];
const FEDERATED = [self::DFRN, self::DIASPORA, self::ACTIVITYPUB];
const SUPPORT_PRIVATE = [self::DFRN, self::DIASPORA, self::MAIL, self::ACTIVITYPUB, self::PUMPIO];
// Supported through a connector
const DIASPORA2 = 'dspc'; // Diaspora connector
const PUMPIO = 'pump'; // pump.io
const STATUSNET = 'stac'; // Statusnet connector
const TWITTER = 'twit'; // Twitter
const DISCOURSE = 'dscs'; // Discourse
const TUMBLR = 'tmbl'; // Tumblr
const BLUESKY = 'bsky'; // Bluesky
// Dead protocols
const OSTATUS = 'stat'; // GNU Social and other OStatus implementations
const STATUSNET = 'stac'; // Statusnet connector
const APPNET = 'apdn'; // app.net - Dead protocol
const FACEBOOK = 'face'; // Facebook API - Not working anymore, API is closed
const GPLUS = 'goog'; // Google+ - Dead in 2019
@ -124,27 +120,10 @@ class Protocol
$protocol = $protocol ?? $contact['protocol'];
if (in_array($protocol, [Protocol::OSTATUS, Protocol::DFRN])) {
// create a follow slap
$item = [
'verb' => Activity::FOLLOW,
'gravity' => Item::GRAVITY_ACTIVITY,
'follow' => $contact['url'],
'body' => '',
'title' => '',
'guid' => '',
'uri-id' => 0,
];
$slap = OStatus::salmon($item, $owner);
if (!empty($contact['notify'])) {
Salmon::slapper($owner, $contact['notify'], $slap);
}
} elseif ($protocol == Protocol::DIASPORA) {
if ($protocol == self::DIASPORA) {
$contact = Diaspora::sendShare($owner, $contact);
Logger::notice('share returns: ' . $contact);
} elseif ($protocol == Protocol::ACTIVITYPUB) {
} elseif (in_array($protocol, [self::ACTIVITYPUB, self::DFRN])) {
$activity_id = ActivityPub\Transmitter::activityIDFromContact($contact['id']);
if (empty($activity_id)) {
// This really should never happen
@ -175,33 +154,13 @@ class Protocol
}
$protocol = $contact['network'];
if (($protocol == Protocol::DFRN) && !empty($contact['protocol'])) {
if (($protocol == self::DFRN) && !empty($contact['protocol'])) {
$protocol = $contact['protocol'];
}
if (in_array($protocol, [Protocol::OSTATUS, Protocol::DFRN])) {
// create an unfollow slap
$item = [
'verb' => Activity::O_UNFOLLOW,
'gravity' => Item::GRAVITY_ACTIVITY,
'follow' => $contact['url'],
'body' => '',
'title' => '',
'guid' => '',
'uri-id' => 0,
];
$slap = OStatus::salmon($item, $owner);
if (empty($contact['notify'])) {
Logger::notice('OStatus/DFRN Contact is missing notify, we quit here', ['id' => $contact['id']]);
return null;
}
return Salmon::slapper($owner, $contact['notify'], $slap) === 0;
} elseif ($protocol == Protocol::DIASPORA) {
if ($protocol == self::DIASPORA) {
return Diaspora::sendUnshare($owner, $contact) > 0;
} elseif ($protocol == Protocol::ACTIVITYPUB) {
} elseif (in_array($protocol, [self::ACTIVITYPUB, self::DFRN])) {
return ActivityPub\Transmitter::sendContactUndo($contact['url'], $contact['id'], $owner);
}
@ -232,11 +191,11 @@ class Protocol
}
$protocol = $contact['network'];
if ($protocol == Protocol::DFRN && !empty($contact['protocol'])) {
if ($protocol == self::DFRN && !empty($contact['protocol'])) {
$protocol = $contact['protocol'];
}
if ($protocol == Protocol::ACTIVITYPUB) {
if ($protocol == self::ACTIVITYPUB) {
return ActivityPub\Transmitter::sendContactReject($contact['url'], $contact['hub-verify'], $owner);
}

View file

@ -72,7 +72,7 @@ class DBStructure
$old_tables = ['fserver', 'gcign', 'gcontact', 'gcontact-relation', 'gfollower' ,'glink', 'item-delivery-data',
'item-activity', 'item-content', 'item_id', 'participation', 'poll', 'poll_result', 'queue', 'retriever_rule',
'deliverq', 'dsprphotoq', 'ffinder', 'sign', 'spam', 'term', 'user-item', 'thread', 'item', 'challenge',
'auth_codes', 'tokens', 'clients', 'profile_check', 'host', 'conversation', 'fcontact', 'addon'];
'auth_codes', 'tokens', 'clients', 'profile_check', 'host', 'conversation', 'fcontact', 'addon', 'push_subscriber'];
$tables = DBA::selectToArray('INFORMATION_SCHEMA.TABLES', ['TABLE_NAME'],
['TABLE_SCHEMA' => DBA::databaseName(), 'TABLE_TYPE' => 'BASE TABLE']);

View file

@ -788,10 +788,8 @@ class Contact
'url' => DI::baseUrl() . '/profile/' . $user['nickname'],
'nurl' => Strings::normaliseLink(DI::baseUrl() . '/profile/' . $user['nickname']),
'addr' => $user['nickname'] . '@' . substr(DI::baseUrl(), strpos(DI::baseUrl(), '://') + 3),
'request' => DI::baseUrl() . '/dfrn_request/' . $user['nickname'],
'notify' => DI::baseUrl() . '/dfrn_notify/' . $user['nickname'],
'poll' => DI::baseUrl() . '/dfrn_poll/' . $user['nickname'],
'confirm' => DI::baseUrl() . '/dfrn_confirm/' . $user['nickname'],
'notify' => DI::baseUrl() . '/dfrn_notify/' . $user['nickname'],
'poll' => DI::baseUrl() . '/feed/' . $user['nickname'],
'name-date' => DateTimeFormat::utcNow(),
'uri-date' => DateTimeFormat::utcNow(),
'avatar-date' => DateTimeFormat::utcNow(),
@ -873,10 +871,8 @@ class Contact
'nurl' => Strings::normaliseLink($url),
'uri-id' => ItemURI::getIdByURI($url),
'addr' => $user['nickname'] . '@' . substr(DI::baseUrl(), strpos(DI::baseUrl(), '://') + 3),
'request' => DI::baseUrl() . '/dfrn_request/' . $user['nickname'],
'notify' => DI::baseUrl() . '/dfrn_notify/' . $user['nickname'],
'poll' => DI::baseUrl() . '/dfrn_poll/' . $user['nickname'],
'confirm' => DI::baseUrl() . '/dfrn_confirm/' . $user['nickname'],
'poll' => DI::baseUrl() . '/feed/' . $user['nickname'],
];
$avatar = Photo::selectFirst(['resource-id', 'type'], ['uid' => $uid, 'profile' => true]);
@ -2532,22 +2528,6 @@ class Contact
}
self::update($fields, $condition);
// We mustn't set the update fields for OStatus contacts since they are updated in OnePoll
$condition['network'] = Protocol::OSTATUS;
// If the contact failed, propagate the update fields to all contacts
if (empty($fields['failed'])) {
unset($fields['last-update']);
unset($fields['success_update']);
unset($fields['failure_update']);
}
if (empty($fields)) {
return;
}
self::update($fields, $condition);
}
/**
@ -3065,11 +3045,6 @@ class Contact
/**
* Takes a $uid and a url/handle and adds a new contact
*
* Currently if the contact is DFRN, interactive needs to be true, to redirect to the
* dfrn_request page.
*
* Otherwise this can be used to bulk add StatusNet contacts, Twitter contacts, etc.
*
* Returns an array
* $return['success'] boolean true if successful
* $return['message'] error text if success is false.
@ -3170,35 +3145,24 @@ class Contact
return $result;
}
if ($protocol === Protocol::OSTATUS && DI::config()->get('system', 'ostatus_disabled')) {
$result['message'] .= DI::l10n()->t('The profile address specified belongs to a network which has been disabled on this site.') . '<br />';
$ret['notify'] = '';
}
if (!$ret['notify']) {
$result['message'] .= DI::l10n()->t('Limited profile. This person will be unable to receive direct/personal notifications from you.') . '<br />';
}
$writeable = ((($protocol === Protocol::OSTATUS) && ($ret['notify'])) ? 1 : 0);
$subhub = (($protocol === Protocol::OSTATUS) ? true : false);
$hidden = (($protocol === Protocol::MAIL) ? 1 : 0);
$hidden = ($protocol === Protocol::MAIL);
$pending = false;
if (($protocol == Protocol::ACTIVITYPUB) && isset($ret['manually-approve'])) {
$pending = (bool)$ret['manually-approve'];
}
if (in_array($protocol, [Protocol::MAIL, Protocol::DIASPORA, Protocol::ACTIVITYPUB])) {
$writeable = 1;
}
$writeable = in_array($protocol, [Protocol::MAIL, Protocol::DIASPORA, Protocol::ACTIVITYPUB]);
if (DBA::isResult($contact)) {
// update contact
$new_relation = (in_array($contact['rel'], [self::FOLLOWER, self::FRIEND]) ? self::FRIEND : self::SHARING);
$fields = ['rel' => $new_relation, 'subhub' => $subhub, 'readonly' => false, 'network' => $ret['network']];
$fields = ['rel' => $new_relation, 'readonly' => false, 'network' => $ret['network']];
if ($contact['pending'] && !empty($contact['hub-verify'])) {
ActivityPub\Transmitter::sendContactAccept($contact['url'], $contact['hub-verify'], $uid);
@ -3236,7 +3200,6 @@ class Contact
'blocked' => 0,
'readonly' => 0,
'pending' => $pending,
'subhub' => $subhub
]);
}
@ -3258,11 +3221,6 @@ class Contact
// Update the avatar
self::updateAvatar($contact_id, $ret['photo']);
// pull feed and consume it, which should subscribe to the hub.
if ($contact['network'] == Protocol::OSTATUS) {
Worker::add(Worker::PRIORITY_HIGH, 'OnePoll', $contact_id, 'force');
}
if ($probed) {
GServer::updateFromProbeArray($ret);
self::updateFromProbeArray($contact_id, $ret);
@ -3733,10 +3691,6 @@ class Contact
$networks[] = Protocol::DIASPORA;
}
if (!DI::config()->get('system', 'ostatus_disabled')) {
$networks[] = Protocol::OSTATUS;
}
$condition = [
'network' => $networks,
'server-failed' => false,

View file

@ -284,7 +284,7 @@ class Relation
return false;
}
if (!in_array($contact['network'], [Protocol::ACTIVITYPUB, Protocol::DFRN, Protocol::OSTATUS])) {
if (!in_array($contact['network'], [Protocol::ACTIVITYPUB, Protocol::DFRN])) {
$apcontact = APContact::getByURL($url, false);
if (empty($apcontact)) {
Logger::info('No discovery - The contact does not seem to speak ActivityPub.', ['id' => $contact['id'], 'url' => $url, 'network' => $contact['network']]);
@ -369,7 +369,6 @@ class Relation
Logger::info('Collecting suggestions', ['uid' => $uid, 'cid' => $cid, 'start' => $start, 'limit' => $limit]);
$diaspora = DI::config()->get('system', 'diaspora_enabled') ? Protocol::DIASPORA : Protocol::ACTIVITYPUB;
$ostatus = !DI::config()->get('system', 'ostatus_disabled') ? Protocol::OSTATUS : Protocol::ACTIVITYPUB;
// The query returns contacts where contacts interacted with whom the given user follows.
// Contacts who already are in the user's contact table are ignored.
@ -377,12 +376,12 @@ class Relation
(SELECT `cid` FROM `contact-relation` WHERE `relation-cid` = ?)
AND NOT `cid` IN (SELECT `id` FROM `contact` WHERE `uid` = ? AND `nurl` IN
(SELECT `nurl` FROM `contact` WHERE `uid` = ? AND `rel` IN (?, ?))) AND `id` = `cid`)
AND NOT `hidden` AND `network` IN (?, ?, ?, ?)
AND NOT `hidden` AND `network` IN (?, ?, ?)
AND NOT `uri-id` IN (SELECT `uri-id` FROM `account-suggestion` WHERE `uri-id` = `contact`.`uri-id` AND `uid` = ?)",
$cid,
0,
$uid, Contact::FRIEND, Contact::SHARING,
Protocol::ACTIVITYPUB, Protocol::DFRN, $diaspora, $ostatus, $uid
Protocol::ACTIVITYPUB, Protocol::DFRN, $diaspora, $uid
], [
'order' => ['last-item' => true],
'limit' => $totallimit,
@ -408,10 +407,10 @@ class Relation
(SELECT `relation-cid` FROM `contact-relation` WHERE `cid` = ?)
AND NOT `cid` IN (SELECT `id` FROM `contact` WHERE `uid` = ? AND `nurl` IN
(SELECT `nurl` FROM `contact` WHERE `uid` = ? AND `rel` IN (?, ?))) AND `id` = `cid`)
AND NOT `hidden` AND `network` IN (?, ?, ?, ?)
AND NOT `hidden` AND `network` IN (?, ?, ?)
AND NOT `uri-id` IN (SELECT `uri-id` FROM `account-suggestion` WHERE `uri-id` = `contact`.`uri-id` AND `uid` = ?)",
$cid, 0, $uid, Contact::FRIEND, Contact::SHARING,
Protocol::ACTIVITYPUB, Protocol::DFRN, $diaspora, $ostatus, $uid],
Protocol::ACTIVITYPUB, Protocol::DFRN, $diaspora, $uid],
['order' => ['last-item' => true], 'limit' => $totallimit]
);
@ -429,10 +428,10 @@ class Relation
// The query returns contacts that follow the given user but aren't followed by that user.
$results = DBA::select('contact', [],
["`nurl` IN (SELECT `nurl` FROM `contact` WHERE `uid` = ? AND `rel` = ?)
AND NOT `hidden` AND `uid` = ? AND `network` IN (?, ?, ?, ?)
AND NOT `hidden` AND `uid` = ? AND `network` IN (?, ?, ?)
AND NOT `uri-id` IN (SELECT `uri-id` FROM `account-suggestion` WHERE `uri-id` = `contact`.`uri-id` AND `uid` = ?)",
$uid, Contact::FOLLOWER, 0,
Protocol::ACTIVITYPUB, Protocol::DFRN, $diaspora, $ostatus, $uid],
Protocol::ACTIVITYPUB, Protocol::DFRN, $diaspora, $uid],
['order' => ['last-item' => true], 'limit' => $totallimit]
);
@ -450,10 +449,10 @@ class Relation
// The query returns any contact that isn't followed by that user.
$results = DBA::select('contact', [],
["NOT `nurl` IN (SELECT `nurl` FROM `contact` WHERE `uid` = ? AND `rel` IN (?, ?) AND `nurl` = `nurl`)
AND NOT `hidden` AND `uid` = ? AND `network` IN (?, ?, ?, ?)
AND NOT `hidden` AND `uid` = ? AND `network` IN (?, ?, ?)
AND NOT `uri-id` IN (SELECT `uri-id` FROM `account-suggestion` WHERE `uri-id` = `contact`.`uri-id` AND `uid` = ?)",
$uid, Contact::FRIEND, Contact::SHARING, 0,
Protocol::ACTIVITYPUB, Protocol::DFRN, $diaspora, $ostatus, $uid],
Protocol::ACTIVITYPUB, Protocol::DFRN, $diaspora, $uid],
['order' => ['last-item' => true], 'limit' => $totallimit]
);

View file

@ -16,7 +16,7 @@ class Conversation
const PARCEL_ACTIVITYPUB = 0;
const PARCEL_DFRN = 1; // Deprecated
const PARCEL_DIASPORA = 2;
const PARCEL_SALMON = 3;
const PARCEL_SALMON = 3; // @deprecated since version 2024.09
const PARCEL_FEED = 4; // Deprecated
const PARCEL_SPLIT_CONVERSATION = 6; // @deprecated since version 2022.09
const PARCEL_LEGACY_DFRN = 7; // @deprecated since version 2021.09

View file

@ -2617,11 +2617,6 @@ class GServer
return;
}
// We don't want to mark a server as OStatus when it had been marked with any other protocol before
if ($protocol == Post\DeliveryData::OSTATUS) {
return;
}
// If the server is marked as ActivityPub then we won't change it to anything different
if ($old == Post\DeliveryData::ACTIVITYPUB) {
return;

View file

@ -602,16 +602,6 @@ class Item
Logger::notice('duplicated item with the same guid found.', $condition);
return true;
}
} elseif ($item['network'] == Protocol::OSTATUS) {
// Check for an existing post with the same content. There seems to be a problem with OStatus.
$condition = [
"`body` = ? AND `network` = ? AND `created` = ? AND `contact-id` = ? AND `uid` = ?",
$item['body'], $item['network'], $item['created'], $item['contact-id'], $item['uid']
];
if (Post::exists($condition)) {
Logger::notice('duplicated item with the same body found.', $item);
return true;
}
}
/*
@ -705,13 +695,12 @@ class Item
{
if (empty($item['network']) || in_array($item['network'], Protocol::FEDERATED)) {
$condition = [
'`uri-id` = ? AND `uid` = ? AND `network` IN (?, ?, ?, ?)',
'`uri-id` = ? AND `uid` = ? AND `network` IN (?, ?, ?)',
$item['uri-id'],
$item['uid'],
Protocol::ACTIVITYPUB,
Protocol::DIASPORA,
Protocol::DFRN,
Protocol::OSTATUS
Protocol::DFRN
];
$existing = Post::selectFirst(['id', 'network'], $condition);
if (DBA::isResult($existing)) {
@ -908,7 +897,7 @@ class Item
/*
* Do we already have this item?
* We have to check several networks since Friendica posts could be repeated
* via OStatus (maybe Diaspora as well)
* via Diaspora.
*/
$duplicate = self::getDuplicateID($item);
if ($duplicate) {

View file

@ -29,7 +29,7 @@ class DeliveryData
const DFRN = 2;
const LEGACY_DFRN = 3; // @deprecated since version 2021.09
const DIASPORA = 4;
const OSTATUS = 5;
const OSTATUS = 5; // @deprecated since version 2024.09
const MAIL = 6;
/**
@ -83,9 +83,6 @@ class DeliveryData
case self::DIASPORA:
$increments[] = "`diaspora` = `diaspora` + 1";
break;
case self::OSTATUS:
$increments[] = "`ostatus` = `ostatus` + 1";
break;
}
return DBA::update('post-delivery-data', $increments, ['uri-id' => $uri_id]);

View file

@ -30,8 +30,8 @@ class Engagement
const ALTERNATIVES = ['source:news' => 'source:service', 'source:relay' => 'source:application',
'media:picture' => 'media:image', 'media:photo' => 'media:image',
'network:activitypub' => 'network:apub', 'network:friendica' => 'network:dfrn',
'network:diaspora' => 'network:dspr', 'network:ostatus' => 'network:stat',
'network:discourse' => 'network:dscs', 'network:tumblr' => 'network:tmbl', 'network:bluesky' => 'network:bsky'];
'network:diaspora' => 'network:dspr', 'network:discourse' => 'network:dscs',
'network:tumblr' => 'network:tmbl', 'network:bluesky' => 'network:bsky'];
const MEDIA_NONE = 0;
const MEDIA_IMAGE = 1;
const MEDIA_VIDEO = 2;

View file

@ -1,174 +0,0 @@
<?php
// Copyright (C) 2010-2024, the Friendica project
// SPDX-FileCopyrightText: 2010-2024 the Friendica project
//
// SPDX-License-Identifier: AGPL-3.0-or-later
namespace Friendica\Model;
use Friendica\Core\Logger;
use Friendica\Core\Worker;
use Friendica\Database\DBA;
use Friendica\Util\DateTimeFormat;
use GuzzleHttp\Psr7\Uri;
class PushSubscriber
{
/**
* Send subscription notifications for the given user
*
* @param integer $uid User ID
* @param int $default_priority
* @return void
* @throws \Friendica\Network\HTTPException\InternalServerErrorException
*/
public static function publishFeed(int $uid, int $default_priority = Worker::PRIORITY_HIGH)
{
$condition = ['push' => 0, 'uid' => $uid];
DBA::update('push_subscriber', ['push' => 1, 'next_try' => DBA::NULL_DATETIME], $condition);
self::requeue($default_priority);
}
/**
* start workers to transmit the feed data
*
* @param int $default_priority
* @return void
* @throws \Friendica\Network\HTTPException\InternalServerErrorException
*/
public static function requeue(int $default_priority = Worker::PRIORITY_HIGH)
{
// We'll push to each subscriber that has push > 0,
// i.e. there has been an update (set in notifier.php).
$subscribers = DBA::select('push_subscriber', ['id', 'push', 'callback_url', 'nickname'], ["`push` > 0 AND `next_try` < ?", DateTimeFormat::utcNow()]);
while ($subscriber = DBA::fetch($subscribers)) {
// We always handle retries with low priority
if ($subscriber['push'] > 1) {
$priority = Worker::PRIORITY_LOW;
} else {
$priority = $default_priority;
}
Logger::info('Publish feed to ' . $subscriber['callback_url'] . ' for ' . $subscriber['nickname'] . ' with priority ' . $priority);
Worker::add($priority, 'PubSubPublish', (int)$subscriber['id']);
}
DBA::close($subscribers);
}
/**
* Renew the feed subscription
*
* @param integer $uid User ID
* @param string $nick Priority for push workers
* @param integer $subscribe Subscribe (Unsubscribe = false)
* @param string $hub_callback Callback address
* @param string $hub_topic Feed topic
* @param string $hub_secret Subscription secret
* @return void
* @throws \Exception
*/
public static function renew(int $uid, string $nick, int $subscribe, string $hub_callback, string $hub_topic, string $hub_secret)
{
// fetch the old subscription if it exists
$subscriber = DBA::selectFirst('push_subscriber', ['last_update', 'push'], ['callback_url' => $hub_callback]);
// delete old subscription if it exists
DBA::delete('push_subscriber', ['callback_url' => $hub_callback]);
if ($subscribe) {
// if we are just updating an old subscription, keep the
// old values for last_update but reset the push
if (DBA::isResult($subscriber)) {
$last_update = $subscriber['last_update'];
$push_flag = min($subscriber['push'], 1);
} else {
$last_update = DateTimeFormat::utcNow();
$push_flag = 0;
}
// subscribe means adding the row to the table
$fields = ['uid' => $uid, 'callback_url' => $hub_callback,
'topic' => $hub_topic, 'nickname' => $nick, 'push' => $push_flag,
'last_update' => $last_update, 'renewed' => DateTimeFormat::utcNow(),
'secret' => $hub_secret];
DBA::insert('push_subscriber', $fields);
Logger::notice("Successfully subscribed [$hub_callback] for $nick");
} else {
Logger::notice("Successfully unsubscribed [$hub_callback] for $nick");
// we do nothing here, since the row was already deleted
}
}
/**
* Delay the push subscriber
*
* @param integer $id Subscriber ID
* @return void
* @throws \Exception
*/
public static function delay(int $id)
{
$subscriber = DBA::selectFirst('push_subscriber', ['push', 'callback_url', 'renewed', 'nickname'], ['id' => $id]);
if (!DBA::isResult($subscriber)) {
return;
}
$retrial = $subscriber['push'];
if ($retrial > 14) {
// End subscriptions if they weren't renewed for more than two months
$days = round((time() - strtotime($subscriber['renewed'])) / (60 * 60 * 24));
if ($days > 60) {
DBA::update('push_subscriber', ['push' => -1, 'next_try' => DBA::NULL_DATETIME], ['id' => $id]);
Logger::info('Delivery error: Subscription ' . $subscriber['callback_url'] . ' for ' . $subscriber['nickname'] . ' is marked as ended.');
} else {
DBA::update('push_subscriber', ['push' => 0, 'next_try' => DBA::NULL_DATETIME], ['id' => $id]);
Logger::info('Delivery error: Giving up ' . $subscriber['callback_url'] . ' for ' . $subscriber['nickname'] . ' for now.');
}
} else {
// Calculate the delay until the next trial
$delay = (($retrial + 3) ** 4) + (rand(1, 30) * ($retrial + 1));
$next = DateTimeFormat::utc('now + ' . $delay . ' seconds');
$retrial = $retrial + 1;
DBA::update('push_subscriber', ['push' => $retrial, 'next_try' => $next], ['id' => $id]);
Logger::info('Delivery error: Next try (' . $retrial . ') ' . $subscriber['callback_url'] . ' for ' . $subscriber['nickname'] . ' at ' . $next);
}
}
/**
* Reset the push subscriber
*
* @param integer $id Subscriber ID
* @param string $last_update Date of last transmitted item
* @return void
* @throws \Exception
*/
public static function reset(int $id, string $last_update)
{
$subscriber = DBA::selectFirst('push_subscriber', ['callback_url', 'nickname'], ['id' => $id]);
if (!DBA::isResult($subscriber)) {
return;
}
// set last_update to the 'created' date of the last item, and reset push=0
$fields = ['push' => 0, 'next_try' => DBA::NULL_DATETIME, 'last_update' => $last_update];
DBA::update('push_subscriber', $fields, ['id' => $id]);
Logger::info('Subscriber ' . $subscriber['callback_url'] . ' for ' . $subscriber['nickname'] . ' is marked as vital');
$parts = parse_url($subscriber['callback_url']);
unset($parts['path']);
$server_url = (string)Uri::fromParts((array)$parts);
$gsid = GServer::getID($server_url, true);
if (!empty($gsid)) {
GServer::setProtocol($gsid, Post\DeliveryData::OSTATUS);
}
}
}

View file

@ -594,9 +594,9 @@ class Tag
public static function countByTag(string $search, int $uid = 0): int
{
$condition = ["`name` = ? AND (`uid` = ? OR (`uid` = ? AND NOT `global`))
AND (`network` IN (?, ?, ?, ?) OR (`uid` = ? AND `uid` != ?))",
AND (`network` IN (?, ?, ?) OR (`uid` = ? AND `uid` != ?))",
$search, 0, $uid,
Protocol::ACTIVITYPUB, Protocol::DFRN, Protocol::DIASPORA, Protocol::OSTATUS, $uid, 0,
Protocol::ACTIVITYPUB, Protocol::DFRN, Protocol::DIASPORA, $uid, 0,
];
return DBA::count('tag-search-view', $condition);
@ -615,9 +615,9 @@ class Tag
public static function getURIIdListByTag(string $search, int $uid = 0, int $start = 0, int $limit = 100, int $last_uriid = 0): array
{
$condition = ["`name` = ? AND (`uid` = ? OR (`uid` = ? AND NOT `global`))
AND (`network` IN (?, ?, ?, ?) OR (`uid` = ? AND `uid` != ?))",
AND (`network` IN (?, ?, ?) OR (`uid` = ? AND `uid` != ?))",
$search, 0, $uid,
Protocol::ACTIVITYPUB, Protocol::DFRN, Protocol::DIASPORA, Protocol::OSTATUS, $uid, 0,
Protocol::ACTIVITYPUB, Protocol::DFRN, Protocol::DIASPORA, $uid, 0,
];
if (!empty($last_uriid)) {

View file

@ -152,8 +152,8 @@ class User
}
$system['name'] = App::PLATFORM . " '" . App::CODENAME . "' " . App::VERSION . '-' . DB_UPDATE_VERSION;
$system['sprvkey'] = $system['uprvkey'] = $system['prvkey'];
$system['spubkey'] = $system['upubkey'] = $system['pubkey'];
$system['uprvkey'] = $system['prvkey'];
$system['upubkey'] = $system['pubkey'];
$system['nickname'] = $system['nick'];
$system['page-flags'] = self::PAGE_FLAGS_SOAPBOX;
$system['account-type'] = $system['contact-type'];
@ -183,8 +183,6 @@ class User
'register_date' => $system['created'],
'pubkey' => $system['pubkey'],
'prvkey' => $system['prvkey'],
'spubkey' => $system['spubkey'],
'sprvkey' => $system['sprvkey'],
'guid' => System::createUUID(),
'verified' => true,
'page-flags' => self::PAGE_FLAGS_SOAPBOX,
@ -1315,11 +1313,6 @@ class User
$prvkey = $keys['prvkey'];
$pubkey = $keys['pubkey'];
// Create another keypair for signing/verifying salmon protocol messages.
$sres = Crypto::newKeypair(512);
$sprvkey = $sres['prvkey'];
$spubkey = $sres['pubkey'];
$insert_result = DBA::insert('user', [
'guid' => System::createUUID(),
'username' => $username,
@ -1329,8 +1322,6 @@ class User
'nickname' => $nickname,
'pubkey' => $pubkey,
'prvkey' => $prvkey,
'spubkey' => $spubkey,
'sprvkey' => $sprvkey,
'verified' => $verified,
'blocked' => $blocked,
'language' => $language,

View file

@ -111,7 +111,6 @@ class Site extends BaseAdmin
$poco_local_search = !empty($_POST['poco_local_search']);
$nodeinfo = !empty($_POST['nodeinfo']);
$mail_enabled = !empty($_POST['mail_enabled']);
$ostatus_enabled = !empty($_POST['ostatus_enabled']);
$diaspora_enabled = !empty($_POST['diaspora_enabled']);
$force_ssl = !empty($_POST['force_ssl']);
$show_help = !empty($_POST['show_help']);
@ -280,7 +279,6 @@ class Site extends BaseAdmin
$transactionConfig->set('system', 'proxy' , $proxy);
$transactionConfig->set('system', 'curl_timeout' , $timeout);
$transactionConfig->set('system', 'imap_disabled' , !$mail_enabled && function_exists('imap_open'));
$transactionConfig->set('system', 'ostatus_disabled' , !$ostatus_enabled);
$transactionConfig->set('system', 'diaspora_enabled' , $diaspora_enabled);
$transactionConfig->set('config', 'private_addons' , $private_addons);
@ -531,7 +529,6 @@ class Site extends BaseAdmin
'$mail_able' => function_exists('imap_open'),
'$mail_enabled' => ['mail_enabled', DI::l10n()->t('Enable Mail support'), !DI::config()->get('system', 'imap_disabled', !function_exists('imap_open')), DI::l10n()->t('Enable built-in mail support to poll IMAP folders and to reply via mail.')],
'$mail_not_able' => DI::l10n()->t('Mail support can\'t be enabled because the PHP IMAP module is not installed.'),
'$ostatus_enabled' => ['ostatus_enabled', DI::l10n()->t('Enable OStatus support'), !DI::config()->get('system', 'ostatus_disabled'), DI::l10n()->t('Enable built-in OStatus (StatusNet, GNU Social etc.) compatibility. All communications in OStatus are public.')],
'$diaspora_able' => $diaspora_able,
'$diaspora_not_able' => DI::l10n()->t('Diaspora support can\'t be enabled because Friendica was installed into a sub directory.'),
'$diaspora_enabled' => ['diaspora_enabled', DI::l10n()->t('Enable Diaspora support'), DI::config()->get('system', 'diaspora_enabled', $diaspora_able), DI::l10n()->t('Enable built-in Diaspora network compatibility for communicating with diaspora servers.')],
@ -562,7 +559,7 @@ class Site extends BaseAdmin
'$dbclean' => ['dbclean', DI::l10n()->t('Clean database'), DI::config()->get('system', 'dbclean'), DI::l10n()->t('Remove old remote items, orphaned database records and old content from some other helper tables.')],
'$dbclean_expire_days' => ['dbclean_expire_days', DI::l10n()->t('Lifespan of remote items'), DI::config()->get('system', 'dbclean-expire-days'), DI::l10n()->t('When the database cleanup is enabled, this defines the days after which remote items will be deleted. Own items, and marked or filed items are always kept. 0 disables this behaviour.')],
'$dbclean_unclaimed' => ['dbclean_unclaimed', DI::l10n()->t('Lifespan of unclaimed items'), DI::config()->get('system', 'dbclean-expire-unclaimed'), DI::l10n()->t('When the database cleanup is enabled, this defines the days after which unclaimed remote items (mostly content from the relay) will be deleted. Default value is 90 days. Defaults to the general lifespan value of remote items if set to 0.')],
'$dbclean_expire_conv' => ['dbclean_expire_conv', DI::l10n()->t('Lifespan of raw conversation data'), DI::config()->get('system', 'dbclean_expire_conversation'), DI::l10n()->t('The conversation data is used for ActivityPub and OStatus, as well as for debug purposes. It should be safe to remove it after 14 days, default is 90 days.')],
'$dbclean_expire_conv' => ['dbclean_expire_conv', DI::l10n()->t('Lifespan of raw conversation data'), DI::config()->get('system', 'dbclean_expire_conversation'), DI::l10n()->t('The conversation data is used for ActivityPub, as well as for debug purposes. It should be safe to remove it after 14 days, default is 90 days.')],
'$max_comments' => ['max_comments', DI::l10n()->t('Maximum numbers of comments per post'), DI::config()->get('system', 'max_comments'), DI::l10n()->t('How much comments should be shown for each post? Default value is 100.')],
'$max_display_comments' => ['max_display_comments', DI::l10n()->t('Maximum numbers of comments per post on the display page'), DI::config()->get('system', 'max_display_comments'), DI::l10n()->t('How many comments should be shown on the single view for each post? Default value is 1000.')],
'$itemspage_network' => ['itemspage_network', DI::l10n()->t('Items per page'), DI::config()->get('system', 'itemspage_network'), DI::l10n()->t('Number of items per page in stream pages (network, community, profile/contact statuses, search).')],

View file

@ -140,8 +140,8 @@ class Search extends BaseApi
if (substr($q, 0, 1) == '#') {
$condition = ["`name` = ? AND (`uid` = ? OR (`uid` = ? AND NOT `global`))
AND (`network` IN (?, ?, ?, ?) OR (`uid` = ? AND `uid` != ?))",
substr($q, 1), 0, $uid, Protocol::ACTIVITYPUB, Protocol::DFRN, Protocol::DIASPORA, Protocol::OSTATUS, $uid, 0];
AND (`network` IN (?, ?, ?) OR (`uid` = ? AND `uid` != ?))",
substr($q, 1), 0, $uid, Protocol::ACTIVITYPUB, Protocol::DFRN, Protocol::DIASPORA, $uid, 0];
$table = 'tag-search-view';
} else {
$q = Post\Engagement::escapeKeywords($q);

View file

@ -56,8 +56,8 @@ class Tag extends BaseApi
$params = ['order' => ['uri-id' => true], 'limit' => $request['limit']];
$condition = ["`name` = ? AND (`uid` = ? OR (`uid` = ? AND NOT `global`))
AND (`network` IN (?, ?, ?, ?) OR (`uid` = ? AND `uid` != ?))",
$this->parameters['hashtag'], 0, $uid, Protocol::ACTIVITYPUB, Protocol::DFRN, Protocol::DIASPORA, Protocol::OSTATUS, $uid, 0];
AND (`network` IN (?, ?, ?) OR (`uid` = ? AND `uid` != ?))",
$this->parameters['hashtag'], 0, $uid, Protocol::ACTIVITYPUB, Protocol::DFRN, Protocol::DIASPORA, $uid, 0];
if ($request['local']) {
$condition = DBA::mergeConditions($condition, ["`uri-id` IN (SELECT `uri-id` FROM `post-user` WHERE `origin`)"]);

View file

@ -118,21 +118,10 @@ class Contact extends BaseModule
return;
}
if ($contact['network'] == Protocol::OSTATUS) {
$result = Model\Contact::createFromProbeForUser($contact['uid'], $contact['url'], $contact['network']);
if ($result['success']) {
Model\Contact::update(['subhub' => 1], ['id' => $contact_id]);
}
// pull feed and consume it, which should subscribe to the hub.
Worker::add(Worker::PRIORITY_HIGH, 'OnePoll', $contact_id, 'force');
} else {
try {
UpdateContact::add(Worker::PRIORITY_HIGH, $contact_id);
} catch (\InvalidArgumentException $e) {
Logger::notice($e->getMessage(), ['contact' => $contact]);
}
try {
UpdateContact::add(Worker::PRIORITY_HIGH, $contact_id);
} catch (\InvalidArgumentException $e) {
Logger::notice($e->getMessage(), ['contact' => $contact]);
}
}

View file

@ -125,11 +125,6 @@ class Follow extends BaseModule
$submit = '';
}
if (($protocol == Protocol::OSTATUS) && $this->config->get('system', 'ostatus_disabled')) {
$this->sysMessages->addNotice($this->t("OStatus support is disabled. Contact can't be added."));
$submit = '';
}
if ($protocol == Protocol::MAIL) {
$contact['url'] = $contact['addr'];
}

View file

@ -294,7 +294,7 @@ class Profile extends BaseModule
}
$lblsuggest = (($contact['network'] === Protocol::DFRN) ? $this->t('Suggest friends') : '');
$poll_enabled = in_array($contact['network'], [Protocol::DFRN, Protocol::OSTATUS, Protocol::FEED, Protocol::MAIL]);
$poll_enabled = in_array($contact['network'], [Protocol::DFRN, Protocol::FEED, Protocol::MAIL]);
$nettype = $this->t('Network type: %s', ContactSelector::networkToName($contact['network'], $contact['url'], $contact['protocol'], $contact['gsid']));
@ -460,7 +460,7 @@ class Profile extends BaseModule
*/
private function getContactActions(array $contact, LocalRelationship\Entity\LocalRelationship $localRelationship): array
{
$poll_enabled = in_array($contact['network'], [Protocol::ACTIVITYPUB, Protocol::DFRN, Protocol::OSTATUS, Protocol::FEED, Protocol::MAIL]);
$poll_enabled = in_array($contact['network'], [Protocol::ACTIVITYPUB, Protocol::DFRN, Protocol::FEED, Protocol::MAIL]);
$contact_actions = [];
$formSecurityToken = self::getFormSecurityToken('contact_action');

View file

@ -51,7 +51,6 @@ class Notify extends BaseModule
{
$msg = Diaspora::decodeRaw($postdata, '', true);
if (!is_array($msg)) {
// We have to fail silently to be able to hand it over to the salmon parser
$this->logger->warning('Diaspora::decodeRaw() has failed for some reason.', ['post-data' => $postdata]);
return false;
}

View file

@ -1,39 +0,0 @@
<?php
// Copyright (C) 2010-2024, the Friendica project
// SPDX-FileCopyrightText: 2010-2024 the Friendica project
//
// SPDX-License-Identifier: AGPL-3.0-or-later
namespace Friendica\Module\DFRN;
use Friendica\BaseModule;
use Friendica\Core\System;
use Friendica\Model\User;
use Friendica\Module\Response;
use Friendica\Network\HTTPException;
use Friendica\Protocol\OStatus;
/**
* DFRN Poll
*/
class Poll extends BaseModule
{
protected function rawContent(array $request = [])
{
$owner = User::getByNickname(
$this->parameters['nickname'] ?? '',
['nickname', 'blocked', 'account_expired', 'account_removed']
);
if (!$owner || $owner['account_expired'] || $owner['account_removed']) {
throw new HTTPException\NotFoundException($this->t('User not found.'));
}
if ($owner['blocked']) {
throw new HTTPException\UnauthorizedException($this->t('Access to this profile has been restricted.'));
}
$last_update = $request['last_update'] ?? '';
$this->httpExit(OStatus::feed($owner['nickname'], $last_update, 10) ?? '', Response::TYPE_ATOM);
}
}

View file

@ -55,11 +55,11 @@ class HCard extends BaseModule
$uri = urlencode('acct:' . $profile['nickname'] . '@' . DI::baseUrl()->getHost() . (DI::baseUrl()->getPath() ? '/' . DI::baseUrl()->getPath() : ''));
$page['htmlhead'] .= '<meta name="dfrn-global-visibility" content="' . ($profile['net-publish'] ? 'true' : 'false') . '" />' . "\r\n";
$page['htmlhead'] .= '<link rel="alternate" type="application/atom+xml" href="' . $baseUrl . '/dfrn_poll/' . $nickname . '" />' . "\r\n";
$page['htmlhead'] .= '<link rel="alternate" type="application/atom+xml" href="' . $profile['poll'] . '" />' . "\r\n";
$page['htmlhead'] .= '<link rel="lrdd" type="application/xrd+xml" href="' . $baseUrl . '/xrd/?uri=' . $uri . '" />' . "\r\n";
header('Link: <' . $baseUrl . '/xrd/?uri=' . $uri . '>; rel="lrdd"; type="application/xrd+xml"', false);
foreach (['request', 'confirm', 'notify', 'poll'] as $dfrn) {
foreach (['notify', 'poll'] as $dfrn) {
$page['htmlhead'] .= "<link rel=\"dfrn-{$dfrn}\" href=\"" . $baseUrl . "/dfrn_{$dfrn}/{$nickname}\" />\r\n";
}

View file

@ -50,7 +50,7 @@ class NoScrape extends BaseModule
'account-type' => $owner['account-type'],
];
$dfrn_pages = ['request', 'confirm', 'notify', 'poll'];
$dfrn_pages = ['notify', 'poll'];
foreach ($dfrn_pages as $dfrn) {
$json_info["dfrn-{$dfrn}"] = DI::baseUrl() . "/dfrn_{$dfrn}/{$which}";
}

View file

@ -61,11 +61,6 @@ class NodeInfo110 extends BaseModule
$nodeinfo['protocols']['outbound'][] = 'diaspora';
}
if (empty($this->config->get('system', 'ostatus_disabled'))) {
$nodeinfo['protocols']['inbound'][] = 'gnusocial';
$nodeinfo['protocols']['outbound'][] = 'gnusocial';
}
$nodeinfo['metadata']['protocols'] = $nodeinfo['protocols'];
$nodeinfo['metadata']['protocols']['outbound'][] = 'atom1.0';
$nodeinfo['metadata']['protocols']['inbound'][] = 'atom1.0';

View file

@ -54,10 +54,6 @@ class NodeInfo120 extends BaseModule
$nodeinfo['protocols'][] = 'diaspora';
}
if (empty($this->config->get('system', 'ostatus_disabled'))) {
$nodeinfo['protocols'][] = 'ostatus';
}
$nodeinfo['services']['inbound'][] = 'atom1.0';
$nodeinfo['services']['inbound'][] = 'rss2.0';
$nodeinfo['services']['outbound'][] = 'atom1.0';

View file

@ -56,10 +56,6 @@ class NodeInfo121 extends BaseModule
$nodeinfo['protocols'][] = 'diaspora';
}
if (empty($this->config->get('system', 'ostatus_disabled'))) {
$nodeinfo['protocols'][] = 'ostatus';
}
$nodeinfo['services']['inbound'][] = 'atom1.0';
$nodeinfo['services']['inbound'][] = 'rss2.0';
$nodeinfo['services']['outbound'][] = 'atom1.0';

View file

@ -57,10 +57,6 @@ class NodeInfo122 extends BaseModule
$nodeinfo['protocols'][] = 'diaspora';
}
if (empty($this->config->get('system', 'ostatus_disabled'))) {
$nodeinfo['protocols'][] = 'ostatus';
}
$nodeinfo['services']['inbound'][] = 'atom1.0';
$nodeinfo['services']['inbound'][] = 'rss2.0';
$nodeinfo['services']['outbound'][] = 'atom1.0';

View file

@ -52,10 +52,6 @@ class NodeInfo210 extends BaseModule
$nodeinfo['protocols'][] = 'diaspora';
}
if (empty($this->config->get('system', 'ostatus_disabled'))) {
$nodeinfo['protocols'][] = 'ostatus';
}
$nodeinfo['services']['inbound'][] = 'atom1.0';
$nodeinfo['services']['inbound'][] = 'rss2.0';
$nodeinfo['services']['outbound'][] = 'atom1.0';

View file

@ -1,148 +0,0 @@
<?php
// Copyright (C) 2010-2024, the Friendica project
// SPDX-FileCopyrightText: 2010-2024 the Friendica project
//
// SPDX-License-Identifier: AGPL-3.0-or-later
namespace Friendica\Module\OStatus;
use Friendica\App;
use Friendica\Core\L10n;
use Friendica\Core\Protocol;
use Friendica\Core\System;
use Friendica\Database\Database;
use Friendica\Model\Contact;
use Friendica\Model\GServer;
use Friendica\Model\Item;
use Friendica\Model\Post;
use Friendica\Module\Response;
use Friendica\Network\HTTPException;
use Friendica\Protocol\OStatus;
use Friendica\Util\Network;
use Friendica\Util\Profiler;
use Friendica\Util\Strings;
use Psr\Log\LoggerInterface;
class PubSub extends \Friendica\BaseModule
{
/** @var Database */
private $database;
/** @var App\Request */
private $request;
public function __construct(App\Request $request, Database $database, L10n $l10n, App\BaseURL $baseUrl, App\Arguments $args, LoggerInterface $logger, Profiler $profiler, Response $response, array $server, array $parameters = [])
{
parent::__construct($l10n, $baseUrl, $args, $logger, $profiler, $response, $server, $parameters);
$this->database = $database;
$this->request = $request;
}
protected function post(array $request = [])
{
$xml = Network::postdata();
$this->logger->info('Feed arrived.', ['from' => $this->request->getRemoteAddress(), 'for' => $this->args->getCommand(), 'user-agent' => $this->server['HTTP_USER_AGENT']]);
$this->logger->debug('Data stream.', ['xml' => $xml]);
$this->logger->debug('Got request data.', ['request' => $request]);
$nickname = $this->parameters['nickname'] ?? '';
$contact_id = $this->parameters['cid'] ?? 0;
$importer = $this->database->selectFirst('user', [], ['nickname' => $nickname, 'verified' => true, 'blocked' => false, 'account_removed' => false, 'account_expired' => false]);
if (!$importer) {
throw new HTTPException\OKException();
}
$condition = ['id' => $contact_id, 'uid' => $importer['uid'], 'subhub' => true, 'blocked' => false];
$contact = $this->database->selectFirst('contact', [], $condition);
if (!$contact) {
$author = OStatus::salmonAuthor($xml, $importer);
if (!empty($author['contact-id'])) {
$condition = ['id' => $author['contact-id'], 'uid' => $importer['uid'], 'subhub' => true, 'blocked' => false];
$contact = $this->database->selectFirst('contact', [], $condition);
$this->logger->notice('No record found for nickname, using author entry instead.', ['nickname' => $nickname, 'contact-id' => $contact_id, 'author-contact-id' => $author['contact-id']]);
}
if (!$contact) {
$this->logger->notice("Contact wasn't found - ignored.", ['author-link' => $author['author-link'], 'contact-id' => $contact_id, 'nickname' => $nickname, 'xml' => $xml]);
throw new HTTPException\OKException();
}
}
if (!empty($contact['gsid'])) {
GServer::setProtocol($contact['gsid'], Post\DeliveryData::OSTATUS);
}
if (!in_array($contact['rel'], [Contact::SHARING, Contact::FRIEND]) && ($contact['network'] != Protocol::FEED)) {
$this->logger->notice('Contact is not expected to share with us - ignored.', ['contact-id' => $contact['id']]);
throw new HTTPException\OKException();
}
// We only import feeds from OStatus here
if (!in_array($contact['network'], [Protocol::ACTIVITYPUB, Protocol::OSTATUS])) {
$this->logger->warning('Unexpected network', ['contact' => $contact, 'network' => $contact['network']]);
throw new HTTPException\OKException();
}
$this->logger->info('Import item from Contact.', ['nickname' => $nickname, 'contact-nickname' => $contact['nick'], 'contact-id' => $contact['id']]);
$feedhub = '';
Item::incrementOutbound(Protocol::OSTATUS);
OStatus::import($xml, $importer, $contact, $feedhub);
throw new HTTPException\OKException();
}
protected function rawContent(array $request = [])
{
$nickname = $this->parameters['nickname'] ?? '';
$contact_id = $this->parameters['cid'] ?? 0;
$hub_mode = trim($request['hub_mode'] ?? '');
$hub_topic = trim($request['hub_topic'] ?? '');
$hub_challenge = trim($request['hub_challenge'] ?? '');
$hub_verify = trim($request['hub_verify_token'] ?? '');
$this->logger->notice('Subscription start.', ['from' => $this->request->getRemoteAddress(), 'mode' => $hub_mode, 'nickname' => $nickname]);
$this->logger->debug('Data: ', ['get' => $request]);
$owner = $this->database->selectFirst('user', ['uid'], ['nickname' => $nickname, 'verified' => true, 'blocked' => false, 'account_removed' => false, 'account_expired' => false]);
if (!$owner) {
$this->logger->notice('Local account not found.', ['nickname' => $nickname]);
throw new HTTPException\NotFoundException();
}
$condition = ['uid' => $owner['uid'], 'id' => $contact_id, 'blocked' => false, 'pending' => false];
if (!empty($hub_verify)) {
$condition['hub-verify'] = $hub_verify;
}
$contact = $this->database->selectFirst('contact', ['id', 'poll'], $condition);
if (!$contact) {
$this->logger->notice('Contact not found.', ['contact' => $contact_id]);
throw new HTTPException\NotFoundException();
}
if (!empty($hub_topic) && !Strings::compareLink($hub_topic, $contact['poll'])) {
$this->logger->notice("Hub topic isn't valid for Contact.", ['hub_topic' => $hub_topic, 'contact_poll' => $contact['poll']]);
throw new HTTPException\NotFoundException();
}
// We must initiate an unsubscribe request with a verify_token.
// Don't allow outsiders to unsubscribe us.
if (($hub_mode === 'unsubscribe') && empty($hub_verify)) {
$this->logger->notice('Bogus unsubscribe');
throw new HTTPException\NotFoundException();
}
if (!empty($hub_mode)) {
Contact::update(['subhub' => $hub_mode === 'subscribe'], ['id' => $contact['id']]);
$this->logger->notice('Success for contact.', ['mode' => $hub_mode, 'contact' => $contact_id]);
}
$this->httpExit($hub_challenge);
}
}

View file

@ -1,165 +0,0 @@
<?php
// Copyright (C) 2010-2024, the Friendica project
// SPDX-FileCopyrightText: 2010-2024 the Friendica project
//
// SPDX-License-Identifier: AGPL-3.0-or-later
namespace Friendica\Module\OStatus;
use Friendica\App;
use Friendica\Core\Config\Capability\IManageConfigValues;
use Friendica\Core\L10n;
use Friendica\Database\Database;
use Friendica\Model\PushSubscriber;
use Friendica\Module\Response;
use Friendica\Network\HTTPClient\Capability\ICanSendHttpRequests;
use Friendica\Network\HTTPClient\Client\HttpClientAccept;
use Friendica\Network\HTTPClient\Client\HttpClientOptions;
use Friendica\Network\HTTPClient\Client\HttpClientRequest;
use Friendica\Network\HTTPException;
use Friendica\Util\Profiler;
use Friendica\Util\Strings;
use Psr\Log\LoggerInterface;
/**
* An open, simple, web-scale and decentralized pubsub protocol.
*
* Part of the OStatus stack.
*
* See https://pubsubhubbub.github.io/PubSubHubbub/pubsubhubbub-core-0.4.html
*
* @version 0.4
*/
class PubSubHubBub extends \Friendica\BaseModule
{
/** @var IManageConfigValues */
private $config;
/** @var Database */
private $database;
/** @var ICanSendHttpRequests */
private $httpClient;
/** @var App\Request */
private $request;
public function __construct(App\Request $request, ICanSendHttpRequests $httpClient, Database $database, IManageConfigValues $config, L10n $l10n, App\BaseURL $baseUrl, App\Arguments $args, LoggerInterface $logger, Profiler $profiler, Response $response, array $server, array $parameters = [])
{
parent::__construct($l10n, $baseUrl, $args, $logger, $profiler, $response, $server, $parameters);
$this->config = $config;
$this->database = $database;
$this->httpClient = $httpClient;
$this->request = $request;
}
protected function post(array $request = [])
{
// PuSH subscription must be considered "public" so just block it
// if public access isn't enabled.
if ($this->config->get('system', 'block_public')) {
throw new HTTPException\ForbiddenException();
}
$this->logger->debug('Got request data.', ['request' => $request]);
// Subscription request from subscriber
// https://pubsubhubbub.github.io/PubSubHubbub/pubsubhubbub-core-0.4.html#rfc.section.5.1
// Example from GNU Social:
// [hub_mode] => subscribe
// [hub_callback] => http://status.local/main/push/callback/1
// [hub_verify] => sync
// [hub_verify_token] => af11...
// [hub_secret] => af11...
// [hub_topic] => http://friendica.local/dfrn_poll/sazius
$hub_mode = $request['hub_mode'] ?? '';
$hub_callback = $request['hub_callback'] ?? '';
$hub_verify_token = $request['hub_verify_token'] ?? '';
$hub_secret = $request['hub_secret'] ?? '';
$hub_topic = $request['hub_topic'] ?? '';
// check for valid hub_mode
if ($hub_mode === 'subscribe') {
$subscribe = 1;
} elseif ($hub_mode === 'unsubscribe') {
$subscribe = 0;
} else {
$this->logger->notice('Invalid hub_mod - ignored.', ['mode' => $hub_mode]);
throw new HTTPException\NotFoundException();
}
$this->logger->info('hub_mode request details.', ['from' => $this->request->getRemoteAddress(), 'mode' => $hub_mode]);
$nickname = $this->parameters['nickname'] ?? $hub_topic;
// Extract nickname and strip any .atom extension
$nickname = basename($nickname, '.atom');
if (!$nickname) {
$this->logger->notice('Empty nick, ignoring.');
throw new HTTPException\NotFoundException();
}
// fetch user from database given the nickname
$condition = ['nickname' => $nickname, 'verified' => true, 'blocked' => false, 'account_removed' => false, 'account_expired' => false];
$owner = $this->database->selectFirst('user', ['uid', 'nickname'], $condition);
if (!$owner) {
$this->logger->notice('Local account not found', ['nickname' => $nickname, 'topic' => $hub_topic, 'callback' => $hub_callback]);
throw new HTTPException\NotFoundException();
}
// get corresponding row from contact table
$condition = ['uid' => $owner['uid'], 'blocked' => false, 'pending' => false, 'self' => true];
$contact = $this->database->selectFirst('contact', ['poll'], $condition);
if (!$contact) {
$this->logger->notice('Self contact for user not found.', ['uid' => $owner['uid']]);
throw new HTTPException\NotFoundException();
}
// sanity check that topic URLs are the same
$hub_topic2 = str_replace('/feed/', '/dfrn_poll/', $hub_topic);
$self = $this->baseUrl . '/api/statuses/user_timeline/' . $owner['nickname'] . '.atom';
if (!Strings::compareLink($hub_topic, $contact['poll']) && !Strings::compareLink($hub_topic2, $contact['poll']) && !Strings::compareLink($hub_topic, $self)) {
$this->logger->notice('Hub topic invalid', ['hub_topic' => $hub_topic, 'poll' => $contact['poll']]);
throw new HTTPException\NotFoundException();
}
// do subscriber verification according to the PuSH protocol
$hub_challenge = Strings::getRandomHex(40);
$params = http_build_query([
'hub.mode' => $subscribe == 1 ? 'subscribe' : 'unsubscribe',
'hub.topic' => $hub_topic,
'hub.challenge' => $hub_challenge,
'hub.verify_token' => $hub_verify_token,
// lease time is hard coded to one week (in seconds)
// we don't actually enforce the lease time because GNU
// Social/StatusNet doesn't honour it (yet)
'hub.lease_seconds' => 604800,
]);
$hub_callback = rtrim($hub_callback, ' ?&#');
$separator = parse_url($hub_callback, PHP_URL_QUERY) === null ? '?' : '&';
$fetchResult = $this->httpClient->get($hub_callback . $separator . $params, HttpClientAccept::DEFAULT, [HttpClientOptions::REQUEST => HttpClientRequest::PUBSUB]);
$body = $fetchResult->getBodyString();
$returnCode = $fetchResult->getReturnCode();
// give up if the HTTP return code wasn't a success (2xx)
if ($returnCode < 200 || $returnCode > 299) {
$this->logger->notice('Subscriber verification ignored', ['hub_topic' => $hub_topic, 'callback' => $hub_callback, 'returnCode' => $returnCode]);
throw new HTTPException\NotFoundException();
}
// check that the correct hub_challenge code was echoed back
if (trim($body) !== $hub_challenge) {
$this->logger->notice('Subscriber did not echo back hub.challenge, ignoring.', ['hub_challenge' => $hub_challenge, 'body' => trim($body)]);
throw new HTTPException\NotFoundException();
}
PushSubscriber::renew($owner['uid'], $nickname, $subscribe, $hub_callback, $hub_topic, $hub_secret);
throw new HTTPException\AcceptedException();
}
}

View file

@ -1,79 +0,0 @@
<?php
// Copyright (C) 2010-2024, the Friendica project
// SPDX-FileCopyrightText: 2010-2024 the Friendica project
//
// SPDX-License-Identifier: AGPL-3.0-or-later
namespace Friendica\Module\OStatus;
use Friendica\App;
use Friendica\Core\L10n;
use Friendica\Core\Protocol;
use Friendica\Core\Renderer;
use Friendica\Core\Session\Capability\IHandleUserSessions;
use Friendica\Database\Database;
use Friendica\Model\Contact;
use Friendica\Module\Response;
use Friendica\Navigation\SystemMessages;
use Friendica\Util\Profiler;
use Psr\Log\LoggerInterface;
class Repair extends \Friendica\BaseModule
{
/** @var IHandleUserSessions */
private $session;
/** @var SystemMessages */
private $systemMessages;
/** @var Database */
private $database;
/** @var App\Page */
private $page;
public function __construct(App\Page $page, Database $database, SystemMessages $systemMessages, IHandleUserSessions $session, L10n $l10n, App\BaseURL $baseUrl, App\Arguments $args, LoggerInterface $logger, Profiler $profiler, Response $response, array $server, array $parameters = [])
{
parent::__construct($l10n, $baseUrl, $args, $logger, $profiler, $response, $server, $parameters);
$this->session = $session;
$this->systemMessages = $systemMessages;
$this->database = $database;
$this->page = $page;
}
protected function content(array $request = []): string
{
if (!$this->session->getLocalUserId()) {
$this->systemMessages->addNotice($this->t('Permission denied.'));
$this->baseUrl->redirect('login');
}
$uid = $this->session->getLocalUserId();
$counter = intval($request['counter'] ?? 0);
$condition = ['uid' => $uid, 'network' => Protocol::OSTATUS, 'rel' => [Contact::FRIEND, Contact::SHARING]];
$total = $this->database->count('contact', $condition);
if ($total) {
$contacts = Contact::selectToArray(['url'], $condition, ['order' => ['url'], 'limit' => [$counter++, 1]]);
if ($contacts) {
Contact::createFromProbeForUser($this->session->getLocalUserId(), $contacts[0]['url']);
$this->page['htmlhead'] .= '<meta http-equiv="refresh" content="5; url=ostatus/repair?counter=' . $counter . '">';
}
}
$tpl = Renderer::getMarkupTemplate('ostatus/repair.tpl');
return Renderer::replaceMacros($tpl, [
'$l10n' => [
'title' => $this->t('Resubscribing to OStatus contacts'),
'keep' => $this->t('Keep this window open until done.'),
'done' => $this->t('✔ Done'),
'nocontacts' => $this->t('No OStatus contacts to resubscribe to.'),
],
'$total' => $total,
'$counter' => $counter,
'$contact' => $contacts[0] ?? null,
]);
}
}

View file

@ -1,205 +0,0 @@
<?php
// Copyright (C) 2010-2024, the Friendica project
// SPDX-FileCopyrightText: 2010-2024 the Friendica project
//
// SPDX-License-Identifier: AGPL-3.0-or-later
namespace Friendica\Module\OStatus;
use Friendica\App;
use Friendica\Core\L10n;
use Friendica\Core\Protocol;
use Friendica\Database\Database;
use Friendica\Model\GServer;
use Friendica\Model\Item;
use Friendica\Model\Post;
use Friendica\Module\Response;
use Friendica\Protocol\ActivityNamespace;
use Friendica\Protocol\OStatus;
use Friendica\Util\Crypto;
use Friendica\Util\Network;
use Friendica\Network\HTTPException;
use Friendica\Protocol\Salmon as SalmonProtocol;
use Friendica\Util\Profiler;
use Friendica\Util\Strings;
use Psr\Log\LoggerInterface;
/**
* Technical endpoint for the Salmon protocol
*/
class Salmon extends \Friendica\BaseModule
{
/** @var Database */
private $database;
public function __construct(Database $database, L10n $l10n, App\BaseURL $baseUrl, App\Arguments $args, LoggerInterface $logger, Profiler $profiler, Response $response, array $server, array $parameters = [])
{
parent::__construct($l10n, $baseUrl, $args, $logger, $profiler, $response, $server, $parameters);
$this->database = $database;
}
/**
* @param array $request
* @return void
* @throws HTTPException\AcceptedException
* @throws HTTPException\BadRequestException
* @throws HTTPException\InternalServerErrorException
* @throws HTTPException\OKException
* @throws \ImagickException
*/
protected function post(array $request = [])
{
$xml = Network::postdata();
$this->logger->debug('Got request data.', ['request' => $request]);
$nickname = $this->parameters['nickname'] ?? '';
if (empty($nickname)) {
throw new HTTPException\BadRequestException('nickname parameter is mandatory');
}
$this->logger->debug('New Salmon', ['nickname' => $nickname, 'xml' => $xml]);
$importer = $this->database->selectFirst('user', [], ['nickname' => $nickname, 'verified' => true, 'blocked' => false, 'account_removed' => false, 'account_expired' => false]);
if (!$this->database->isResult($importer)) {
throw new HTTPException\InternalServerErrorException();
}
// parse the xml
$dom = simplexml_load_string($xml, 'SimpleXMLElement', 0, ActivityNamespace::SALMON_ME);
$base = null;
// figure out where in the DOM tree our data is hiding
if (!empty($dom->provenance->data)) {
$base = $dom->provenance;
} elseif (!empty($dom->env->data)) {
$base = $dom->env;
} elseif (!empty($dom->data)) {
$base = $dom;
}
if (empty($base)) {
$this->logger->notice('unable to locate salmon data in xml');
throw new HTTPException\BadRequestException();
}
// Stash the signature away for now. We have to find their key or it won't be good for anything.
$signature = Strings::base64UrlDecode($base->sig);
// unpack the data
// strip whitespace so our data element will return to one big base64 blob
$data = str_replace([" ", "\t", "\r", "\n"], ["", "", "", ""], $base->data);
// stash away some other stuff for later
$type = $base->data[0]->attributes()->type[0];
$keyhash = $base->sig[0]->attributes()->keyhash[0] ?? '';
$encoding = $base->encoding;
$alg = $base->alg;
// Salmon magic signatures have evolved and there is no way of knowing ahead of time which
// flavour we have. We'll try and verify it regardless.
$stnet_signed_data = $data;
$signed_data = $data . '.' . Strings::base64UrlEncode($type) . '.' . Strings::base64UrlEncode($encoding) . '.' . Strings::base64UrlEncode($alg);
$compliant_format = str_replace('=', '', $signed_data);
// decode the data
$data = Strings::base64UrlDecode($data);
$author = OStatus::salmonAuthor($data, $importer);
$author_link = $author["author-link"];
if (!$author_link) {
$this->logger->notice('Could not retrieve author URI.');
throw new HTTPException\BadRequestException();
}
// Once we have the author URI, go to the web and try to find their public key
$this->logger->notice('Fetching key for ' . $author_link);
$key = SalmonProtocol::getKey($author_link, $keyhash);
if (!$key) {
$this->logger->notice('Could not retrieve author key.');
throw new HTTPException\BadRequestException();
}
$this->logger->info('Key details', ['info' => $key]);
$pubkey = SalmonProtocol::magicKeyToPem($key);
// We should have everything we need now. Let's see if it verifies.
// Try GNU Social format
$verify = Crypto::rsaVerify($signed_data, $signature, $pubkey);
$mode = 1;
if (!$verify) {
$this->logger->notice('Message did not verify using protocol. Trying compliant format.');
$verify = Crypto::rsaVerify($compliant_format, $signature, $pubkey);
$mode = 2;
}
if (!$verify) {
$this->logger->notice('Message did not verify using padding. Trying old statusnet format.');
$verify = Crypto::rsaVerify($stnet_signed_data, $signature, $pubkey);
$mode = 3;
}
if (!$verify) {
$this->logger->notice('Message did not verify. Discarding.');
throw new HTTPException\BadRequestException();
}
$this->logger->notice('Message verified with mode ' . $mode);
/*
*
* If we reached this point, the message is good. Now let's figure out if the author is allowed to send us stuff.
*
*/
$contact = $this->database->selectFirst(
'contact',
[],
[
"`network` IN (?, ?)
AND (`nurl` = ? OR `alias` = ? OR `alias` = ?)
AND `uid` = ?",
Protocol::OSTATUS, Protocol::DFRN,
Strings::normaliseLink($author_link), $author_link, Strings::normaliseLink($author_link),
$importer['uid']
]
);
if (!empty($contact['gsid'])) {
GServer::setProtocol($contact['gsid'], Post\DeliveryData::OSTATUS);
}
// Have we ignored the person?
// If so we can not accept this post.
if (!empty($contact['blocked'])) {
$this->logger->notice('Ignoring this author.');
throw new HTTPException\AcceptedException();
}
// Placeholder for hub discovery.
$hub = '';
$contact = $contact ?: [];
Item::incrementOutbound(Protocol::OSTATUS);
OStatus::import($data, $importer, $contact, $hub);
throw new HTTPException\OKException();
}
}

View file

@ -77,7 +77,7 @@ class Common extends BaseProfile
$condition = [
'blocked' => false,
'deleted' => false,
'network' => [Protocol::ACTIVITYPUB, Protocol::DFRN, Protocol::DIASPORA, Protocol::OSTATUS, Protocol::FEED],
'network' => [Protocol::ACTIVITYPUB, Protocol::DFRN, Protocol::DIASPORA, Protocol::FEED],
];
$total = Contact\Relation::countCommon($sourceId, $targetId, $condition);

View file

@ -78,7 +78,7 @@ class Contacts extends Module\BaseProfile
'archive' => false,
'failed' => false,
'self' => false,
'network' => [Protocol::ACTIVITYPUB, Protocol::DFRN, Protocol::DIASPORA, Protocol::OSTATUS]
'network' => [Protocol::ACTIVITYPUB, Protocol::DFRN, Protocol::DIASPORA]
];
switch ($type) {

View file

@ -89,7 +89,6 @@ class Conversations extends BaseProfile
$this->page['htmlhead'] .= '<meta content="noindex, noarchive" name="robots" />' . "\n";
}
$this->page['htmlhead'] .= '<link rel="alternate" type="application/atom+xml" href="' . $this->baseUrl . '/dfrn_poll/' . $this->parameters['nickname'] . '" title="DFRN: ' . $this->t('%s\'s timeline', Strings::escapeHtml($profile['name'])) . '"/>' . "\n";
$this->page['htmlhead'] .= '<link rel="alternate" type="application/atom+xml" href="' . $this->baseUrl . '/feed/' . $this->parameters['nickname'] . '/" title="' . $this->t('%s\'s posts', Strings::escapeHtml($profile['name'])) . '"/>' . "\n";
$this->page['htmlhead'] .= '<link rel="alternate" type="application/atom+xml" href="' . $this->baseUrl . '/feed/' . $this->parameters['nickname'] . '/comments" title="' . $this->t('%s\'s comments', Strings::escapeHtml($profile['name'])) . '"/>' . "\n";
$this->page['htmlhead'] .= '<link rel="alternate" type="application/atom+xml" href="' . $this->baseUrl . '/feed/' . $this->parameters['nickname'] . '/activity" title="' . $this->t('%s\'s timeline', Strings::escapeHtml($profile['name'])) . '"/>' . "\n";

View file

@ -336,7 +336,6 @@ class Profile extends BaseProfile
$htmlhead .= '<meta content="noindex, noarchive" name="robots" />' . "\n";
}
$htmlhead .= '<link rel="alternate" type="application/atom+xml" href="' . $this->baseUrl . '/dfrn_poll/' . $nickname . '" title="DFRN: ' . $this->t('%s\'s timeline', htmlspecialchars($profile['name'], ENT_COMPAT, 'UTF-8', true)) . '"/>' . "\n";
$htmlhead .= '<link rel="alternate" type="application/atom+xml" href="' . $this->baseUrl . '/feed/' . $nickname . '/" title="' . $this->t('%s\'s posts', htmlspecialchars($profile['name'], ENT_COMPAT, 'UTF-8', true)) . '"/>' . "\n";
$htmlhead .= '<link rel="alternate" type="application/atom+xml" href="' . $this->baseUrl . '/feed/' . $nickname . '/comments" title="' . $this->t('%s\'s comments', htmlspecialchars($profile['name'], ENT_COMPAT, 'UTF-8', true)) . '"/>' . "\n";
$htmlhead .= '<link rel="alternate" type="application/atom+xml" href="' . $this->baseUrl . '/feed/' . $nickname . '/activity" title="' . $this->t('%s\'s timeline', htmlspecialchars($profile['name'], ENT_COMPAT, 'UTF-8', true)) . '"/>' . "\n";
@ -344,7 +343,7 @@ class Profile extends BaseProfile
$htmlhead .= '<link rel="lrdd" type="application/xrd+xml" href="' . $this->baseUrl . '/xrd/?uri=' . $uri . '" />' . "\n";
header('Link: <' . $this->baseUrl . '/xrd/?uri=' . $uri . '>; rel="lrdd"; type="application/xrd+xml"', false);
$dfrn_pages = ['request', 'confirm', 'notify', 'poll'];
$dfrn_pages = ['notify', 'poll'];
foreach ($dfrn_pages as $dfrn) {
$htmlhead .= '<link rel="dfrn-' . $dfrn . '" href="' . $this->baseUrl . '/dfrn_' . $dfrn . '/' . $nickname . '" />' . "\n";
}

View file

@ -1,40 +0,0 @@
<?php
// Copyright (C) 2010-2024, the Friendica project
// SPDX-FileCopyrightText: 2010-2024 the Friendica project
//
// SPDX-License-Identifier: AGPL-3.0-or-later
namespace Friendica\Module;
use Friendica\BaseModule;
use Friendica\Core\System;
use Friendica\Model\User;
use Friendica\Network\HTTPException\BadRequestException;
use Friendica\Protocol\Salmon;
/**
* prints the public RSA key of a user
*/
class PublicRSAKey extends BaseModule
{
protected function rawContent(array $request = [])
{
if (empty($this->parameters['nick'])) {
throw new BadRequestException();
}
$nick = $this->parameters['nick'];
$user = User::getByNickname($nick, ['spubkey']);
if (empty($user) || empty($user['spubkey'])) {
throw new BadRequestException();
}
$this->httpExit(
Salmon::salmonKey($user['spubkey']),
Response::TYPE_BLANK,
'application/magic-public-key'
);
}
}

View file

@ -141,11 +141,6 @@ class Acl extends BaseModule
switch ($type) {
case self::TYPE_MENTION_CONTACT_CIRCLE:
$condition = DBA::mergeConditions($condition,
["NOT `self` AND NOT `blocked` AND `network` != ?", Protocol::OSTATUS
]);
break;
case self::TYPE_MENTION_CONTACT:
$condition = DBA::mergeConditions($condition,
["NOT `self` AND NOT `blocked`",

View file

@ -154,12 +154,8 @@ class Connectors extends BaseSettings
$diasp_enabled = $this->config->get('system', 'diaspora_enabled') ?
$this->t('Built-in support for %s connectivity is enabled', $this->t('Diaspora (Socialhome, Hubzilla)')) :
$this->t('Built-in support for %s connectivity is disabled', $this->t('Diaspora (Socialhome, Hubzilla)'));
$ostat_enabled = $this->config->get('system', 'ostatus_disabled') ?
$this->t('Built-in support for %s connectivity is disabled', $this->t('OStatus (GNU Social)')) :
$this->t('Built-in support for %s connectivity is enabled', $this->t('OStatus (GNU Social)'));
} else {
$diasp_enabled = '';
$ostat_enabled = '';
}
$mail_enabled = function_exists('imap_open') && !$this->config->get('system', 'imap_disabled');
@ -199,7 +195,6 @@ class Connectors extends BaseSettings
'$title' => $this->t('Social Networks'),
'$diasp_enabled' => $diasp_enabled,
'$ostat_enabled' => $ostat_enabled,
'$general_settings' => $this->t('General Social Media Settings'),
'$accept_only_sharer' => [
@ -221,8 +216,6 @@ class Connectors extends BaseSettings
'$api_auto_attach' => ['api_auto_attach', $this->t('API: Automatically links at the end of the post as attached posts'), $api_auto_attach, $this->t('When activated, added links at the end of the post react the same way as added links in the web interface.')],
'$article_mode' => ['article_mode', $this->t('Article Mode'), $article_mode, $this->t("Controls how posts with titles are transmitted. Mastodon and its forks don't display the content of these posts if the post is created in the correct (default) way."), $article_modes],
'$legacy_contact' => ['legacy_contact', $this->t('Your legacy ActivityPub/GNU Social account'), $legacy_contact, $this->t('If you enter your old account name from an ActivityPub based system or your GNU Social/Statusnet account name here (in the format user@domain.tld), your contacts will be added automatically. The field will be emptied when done.')],
'$repair_ostatus_url' => 'ostatus/repair',
'$repair_ostatus_text' => $this->t('Repair OStatus subscriptions'),
'$connector_settings_forms' => $connector_settings_forms,

View file

@ -8,10 +8,8 @@
namespace Friendica\Module\WellKnown;
use Friendica\BaseModule;
use Friendica\Core\System;
use Friendica\DI;
use Friendica\Module\Response;
use Friendica\Protocol\Salmon;
use Friendica\Util\Crypto;
use Friendica\Util\XML;
@ -66,15 +64,8 @@ class HostMeta extends BaseModule
'href' => $domain . '/amcd'
]
],
'Property' => [
'@attributes' => [
'type' => 'http://salmon-protocol.org/ns/magic-key',
'mk:key_id' => '1'
],
Salmon::salmonKey($config->get('system', 'site_pubkey'))
]
],
], $xml, false, ['hm' => 'http://host-meta.net/xrd/1.0', 'mk' => 'http://salmon-protocol.org/ns/magic-key']);
], $xml, false, ['hm' => 'http://host-meta.net/xrd/1.0']);
$this->httpExit($xml->saveXML(), Response::TYPE_XML, 'application/xrd+xml');
}

View file

@ -8,14 +8,12 @@
namespace Friendica\Module;
use Friendica\BaseModule;
use Friendica\Core\System;
use Friendica\DI;
use Friendica\Model\Photo;
use Friendica\Model\User;
use Friendica\Network\HTTPException\BadRequestException;
use Friendica\Network\HTTPException\NotFoundException;
use Friendica\Protocol\ActivityNamespace;
use Friendica\Protocol\Salmon;
use Friendica\Util\Network;
use Friendica\Util\XML;
@ -132,6 +130,11 @@ class Xrd extends BaseModule
'subject' => 'acct:' . $owner['addr'],
'aliases' => [$owner['url']],
'links' => [
[
'rel' => ActivityNamespace::FEED,
'type' => 'application/atom+xml',
'href' => $owner['poll'] ?? $baseURL,
],
[
'rel' => ActivityNamespace::WEBFINGERPROFILE,
'type' => 'text/html',
@ -142,19 +145,6 @@ class Xrd extends BaseModule
'type' => 'application/activity+json',
'href' => $owner['url'],
],
[
'rel' => ActivityNamespace::OSTATUSSUB,
'template' => $baseURL . '/contact/follow?url={uri}',
],
[
'rel' => ActivityNamespace::FEED,
'type' => 'application/atom+xml',
'href' => $owner['poll'] ?? $baseURL,
],
[
'rel' => 'salmon',
'href' => $baseURL . '/salmon/' . $owner['nickname'],
],
[
'rel' => ActivityNamespace::HCARD,
'type' => 'text/html',
@ -165,6 +155,14 @@ class Xrd extends BaseModule
'type' => 'text/html',
'href' => $baseURL,
],
[
'rel' => 'salmon',
'href' => $baseURL . '/receive/users/' . $owner['guid'],
],
[
'rel' => ActivityNamespace::OSTATUSSUB,
'template' => $baseURL . '/contact/follow?url={uri}',
],
]
];
header('Access-Control-Allow-Origin: *');
@ -218,24 +216,12 @@ class Xrd extends BaseModule
],
[
'rel' => 'salmon',
'href' => $baseURL . '/salmon/' . $owner['nickname'],
],
[
'rel' => 'http://salmon-protocol.org/ns/salmon-replies',
'href' => $baseURL . '/salmon/' . $owner['nickname'],
],
[
'rel' => 'http://salmon-protocol.org/ns/salmon-mention',
'href' => $baseURL . '/salmon/' . $owner['nickname'] . '/mention',
'href' => $baseURL . '/receive/users/' . $owner['guid'],
],
[
'rel' => ActivityNamespace::OSTATUSSUB,
'template' => $baseURL . '/contact/follow?url={uri}',
],
[
'rel' => 'magic-public-key',
'href' => 'data:application/magic-public-key,' . Salmon::salmonKey($owner['spubkey']),
],
[
'rel' => ActivityNamespace::OPENWEBAUTH,
'type' => 'application/x-zot+json',
@ -262,13 +248,13 @@ class Xrd extends BaseModule
'2:Alias' => $alias,
'1:link' => [
'@attributes' => [
'rel' => 'http://purl.org/macgirvin/dfrn/1.0',
'rel' => ActivityNamespace::DFRN,
'href' => $owner['url']
]
],
'2:link' => [
'@attributes' => [
'rel' => 'http://schemas.google.com/g/2010#updates-from',
'rel' => ActivityNamespace::FEED,
'type' => 'application/atom+xml',
'href' => $owner['poll']
]
@ -311,34 +297,16 @@ class Xrd extends BaseModule
'8:link' => [
'@attributes' => [
'rel' => 'salmon',
'href' => $baseURL . '/salmon/' . $owner['nickname']
'href' => $baseURL . '/receive/users/' . $owner['guid']
]
],
'9:link' => [
'@attributes' => [
'rel' => 'http://salmon-protocol.org/ns/salmon-replies',
'href' => $baseURL . '/salmon/' . $owner['nickname']
]
],
'10:link' => [
'@attributes' => [
'rel' => 'http://salmon-protocol.org/ns/salmon-mention',
'href' => $baseURL . '/salmon/' . $owner['nickname'] . '/mention'
]
],
'11:link' => [
'@attributes' => [
'rel' => ActivityNamespace::OSTATUSSUB,
'template' => $baseURL . '/contact/follow?url={uri}'
]
],
'12:link' => [
'@attributes' => [
'rel' => 'magic-public-key',
'href' => 'data:application/magic-public-key,' . Salmon::salmonKey($owner['spubkey'])
]
],
'13:link' => [
'10:link' => [
'@attributes' => [
'rel' => ActivityNamespace::OPENWEBAUTH,
'type' => 'application/x-zot+json',

View file

@ -140,8 +140,8 @@ class Introduction extends BaseFactory
}
$formattedIntroductions[] = new ValueObject\Introduction([
'label' => (($intro['network'] !== Protocol::OSTATUS) ? 'friend_request' : 'follower'),
'str_type' => (($intro['network'] !== Protocol::OSTATUS) ? $this->l10n->t('Friend/Connect Request') : $this->l10n->t('New Follower')),
'label' => 'friend_request',
'str_type' => $this->l10n->t('Friend/Connect Request'),
'dfrn_id' => $intro['issued-id'],
'uid' => $this->session->getLocalUserId(),
'intro_id' => $intro['intro_id'],

View file

@ -24,7 +24,6 @@ class HttpClientRequest
public const MEDIAPROXY = 'MediaProxy/1';
public const MEDIAVERIFIER = 'MediaVerifier/1';
public const OSTATUS = 'OStatus/1';
public const SALMON = 'Salmon/1';
public const SERVERINFO = 'ServerInfo/1';
public const SERVERDISCOVER = 'ServerDiscover/1';
public const SITEINFO = 'SiteInfo/1';

View file

@ -869,20 +869,6 @@ class Probe
$data['baseurl'] = trim($link['href'], '/');
} elseif (($link['rel'] == 'http://webfinger.net/rel/blog') && !empty($link['href'])) {
$data['url'] = $link['href'];
} elseif (($link['rel'] == 'magic-public-key') && !empty($link['href'])) {
$pubkey = $link['href'];
if (substr($pubkey, 0, 5) === 'data:') {
if (strstr($pubkey, ',')) {
$pubkey = substr($pubkey, strpos($pubkey, ',') + 1);
} else {
$pubkey = substr($pubkey, 5);
}
try {
$data['pubkey'] = Salmon::magicKeyToPem($pubkey);
} catch (\Throwable $e) {
}
}
}
}

View file

@ -98,8 +98,8 @@ class Post
continue;
}
// You can always comment on Diaspora and OStatus items
if (in_array($item['network'], [Protocol::OSTATUS, Protocol::DIASPORA]) && (DI::userSession()->getLocalUserId() == $item['uid'])) {
// You can always comment on Diaspora items
if (in_array($item['network'], [Protocol::DIASPORA]) && (DI::userSession()->getLocalUserId() == $item['uid'])) {
$item['writable'] = true;
}

View file

@ -1285,8 +1285,6 @@ class Receiver
DBA::close($parents);
}
self::switchContacts($receivers, $actor);
// "birdsitelive" is a service that mirrors tweets into the fediverse
// These posts can be fetched without authentication, but are not marked as public
// We treat them as unlisted posts to be able to handle them.
@ -1369,62 +1367,6 @@ class Receiver
return false;
}
/**
* Switches existing contacts to ActivityPub
*
* @param integer $cid Contact ID
* @param integer $uid User ID
* @param string $url Profile URL
* @return void
* @throws \Friendica\Network\HTTPException\InternalServerErrorException
* @throws \ImagickException
*/
public static function switchContact(int $cid, int $uid, string $url)
{
if (DBA::exists('contact', ['id' => $cid, 'network' => Protocol::ACTIVITYPUB])) {
Logger::info('Contact is already ActivityPub', ['id' => $cid, 'uid' => $uid, 'url' => $url]);
return;
}
if (Contact::updateFromProbe($cid)) {
Logger::info('Update was successful', ['id' => $cid, 'uid' => $uid, 'url' => $url]);
}
// Send a new follow request to be sure that the connection still exists
if (($uid != 0) && DBA::exists('contact', ['id' => $cid, 'rel' => [Contact::SHARING, Contact::FRIEND], 'network' => Protocol::ACTIVITYPUB])) {
Logger::info('Contact had been switched to ActivityPub. Sending a new follow request.', ['uid' => $uid, 'url' => $url]);
ActivityPub\Transmitter::sendActivity('Follow', $url, $uid);
}
}
/**
* @TODO Fix documentation and type-hints
*
* @param $receivers
* @param $actor
* @return void
* @throws \Friendica\Network\HTTPException\InternalServerErrorException
* @throws \ImagickException
*/
private static function switchContacts($receivers, $actor)
{
if (empty($actor)) {
return;
}
foreach ($receivers as $receiver) {
$contact = DBA::selectFirst('contact', ['id'], ['uid' => $receiver['uid'], 'network' => Protocol::OSTATUS, 'nurl' => Strings::normaliseLink($actor)]);
if (DBA::isResult($contact)) {
self::switchContact($contact['id'], $receiver['uid'], $actor);
}
$contact = DBA::selectFirst('contact', ['id'], ['uid' => $receiver['uid'], 'network' => Protocol::OSTATUS, 'alias' => [Strings::normaliseLink($actor), $actor]]);
if (DBA::isResult($contact)) {
self::switchContact($contact['id'], $receiver['uid'], $actor);
}
}
}
/**
* @TODO Fix documentation and type-hints
*

View file

@ -642,7 +642,7 @@ class Transmitter
$networks = Protocol::FEDERATED;
} else {
// For now only send to these contacts:
$networks = [Protocol::ACTIVITYPUB, Protocol::OSTATUS];
$networks = [Protocol::ACTIVITYPUB];
}
$data = ['to' => [], 'cc' => [], 'bto' => [], 'bcc' => [], 'audience' => $audience];
@ -1019,7 +1019,7 @@ class Transmitter
$networks = Protocol::FEDERATED;
} else {
// For now only send to these contacts:
$networks = [Protocol::ACTIVITYPUB, Protocol::OSTATUS];
$networks = [Protocol::ACTIVITYPUB];
}
$condition = [

View file

@ -85,8 +85,7 @@ class DFRN
$user['uprvkey'] = $user['prvkey'];
} else {
$user = ['importer_uid' => 0, 'uprvkey' => '', 'timezone' => 'UTC',
'nickname' => '', 'sprvkey' => '', 'spubkey' => '',
'page-flags' => 0, 'account-type' => 0, 'prvnets' => 0];
'nickname' => '', 'page-flags' => 0, 'account-type' => 0, 'prvnets' => 0];
}
return array_merge($contact, $user);
@ -363,21 +362,6 @@ class DFRN
$attributes = ['rel' => 'alternate', 'type' => 'text/html', 'href' => $alternatelink];
XML::addElement($doc, $root, 'link', '', $attributes);
if ($public) {
// DFRN itself doesn't uses this. But maybe someone else wants to subscribe to the public feed.
OStatus::addHubLink($doc, $root, $owner['nick']);
$attributes = ['rel' => 'salmon', 'href' => DI::baseUrl() . '/salmon/' . $owner['nick']];
XML::addElement($doc, $root, 'link', '', $attributes);
$attributes = ['rel' => 'http://salmon-protocol.org/ns/salmon-replies', 'href' => DI::baseUrl() . '/salmon/' . $owner['nick']];
XML::addElement($doc, $root, 'link', '', $attributes);
$attributes = ['rel' => 'http://salmon-protocol.org/ns/salmon-mention', 'href' => DI::baseUrl() . '/salmon/' . $owner['nick']];
XML::addElement($doc, $root, 'link', '', $attributes);
}
// For backward compatibility we keep this element
if (in_array($owner['page-flags'], [User::PAGE_FLAGS_COMMUNITY, User::PAGE_FLAGS_COMM_MAN])) {
XML::addElement($doc, $root, 'dfrn:community', 1);

View file

@ -799,7 +799,7 @@ class Feed
$frequency = [];
$oldest = time();
$newest = 0;
$oldest_date = $newest_date = '';
$newest_date = '';
foreach ($creation_dates as $date) {
$timestamp = strtotime($date);
@ -822,7 +822,6 @@ class Feed
}
if ($oldest > $day) {
$oldest = $day;
$oldest_date = $date;
}
if ($newest < $day) {
@ -919,11 +918,6 @@ class Feed
$rating = 9;
}
// Friendica and OStatus are checked once a day
if (in_array($contact['network'], [Protocol::DFRN, Protocol::OSTATUS])) {
$rating = 8;
}
// Check archived contacts or contacts with unsupported protocols once a month
if ($contact['archive'] || in_array($contact['network'], [Protocol::ZOT, Protocol::PHANTOM])) {
$rating = 10;
@ -1030,10 +1024,9 @@ class Feed
$condition = [
"`uid` = ? AND `received` > ? AND NOT `deleted` AND `gravity` IN (?, ?)
AND `private` != ? AND `visible` AND `wall` AND `parent-network` IN (?, ?, ?, ?)",
AND `private` != ? AND `visible` AND `wall` AND `parent-network` IN (?, ?, ?)",
$owner['uid'], $check_date, Item::GRAVITY_PARENT, Item::GRAVITY_COMMENT,
Item::PRIVATE, Protocol::ACTIVITYPUB,
Protocol::OSTATUS, Protocol::DFRN, Protocol::DIASPORA
Item::PRIVATE, Protocol::ACTIVITYPUB, Protocol::DFRN, Protocol::DIASPORA
];
if ($filter === 'comments') {
@ -1123,8 +1116,6 @@ class Feed
$attributes = ['href' => $owner['url'], 'rel' => 'alternate', 'type' => 'text/html'];
XML::addElement($doc, $root, 'link', '', $attributes);
OStatus::addHubLink($doc, $root, $owner['nick']);
$attributes = ['href' => DI::baseUrl() . $selfUri, 'rel' => 'self', 'type' => 'application/atom+xml'];
XML::addElement($doc, $root, 'link', '', $attributes);
@ -1166,7 +1157,7 @@ class Feed
Logger::info('Feed entry author does not match feed owner', ['owner' => $owner['url'], 'author' => $item['author-link']]);
}
$entry = OStatus::entryHeader($doc, $owner, $item, false);
$entry = self::entryHeader($doc, $owner, $item, false);
self::entryContent($doc, $entry, $item, self::getTitle($item), '', true);
@ -1191,7 +1182,7 @@ class Feed
private static function entryContent(DOMDocument $doc, DOMElement $entry, array $item, $title, string $verb = '', bool $complete = true)
{
if ($verb == '') {
$verb = OStatus::constructVerb($item);
$verb = self::constructVerb($item);
}
XML::addElement($doc, $entry, 'id', $item['uri']);
@ -1278,7 +1269,7 @@ class Feed
}
}
OStatus::getAttachment($doc, $entry, $item);
self::getAttachment($doc, $entry, $item);
}
/**
@ -1346,4 +1337,89 @@ class Feed
}
return $replace;
}
/**
* Adds attachment data to the XML document
*
* @param DOMDocument $doc XML document
* @param DOMElement $root XML root element where the hub links are added
* @param array $item Data of the item that is to be posted
* @return void
* @throws \Friendica\Network\HTTPException\InternalServerErrorException
*/
private static function getAttachment(DOMDocument $doc, DOMElement $root, array $item)
{
foreach (Post\Media::getByURIId($item['uri-id'], [Post\Media::AUDIO, Post\Media::IMAGE, Post\Media::VIDEO, Post\Media::DOCUMENT, Post\Media::TORRENT]) as $attachment) {
$attributes = ['rel' => 'enclosure',
'href' => $attachment['url'],
'type' => $attachment['mimetype']];
if (!empty($attachment['size'])) {
$attributes['length'] = intval($attachment['size']);
}
if (!empty($attachment['description'])) {
$attributes['title'] = $attachment['description'];
}
XML::addElement($doc, $root, 'link', '', $attributes);
}
}
/**
* @TODO Picture attachments should look like this:
* <a href="https://status.pirati.ca/attachment/572819" title="https://status.pirati.ca/file/heluecht-20151202T222602-rd3u49p.gif"
* class="attachment thumbnail" id="attachment-572819" rel="nofollow external">https://status.pirati.ca/attachment/572819</a>
*/
/**
* Returns the given activity if present - otherwise returns the "post" activity
*
* @param array $item Data of the item that is to be posted
* @return string activity
*/
private static function constructVerb(array $item): string
{
if (!empty($item['verb'])) {
return $item['verb'];
}
return Activity::POST;
}
/**
* Adds a header element to the XML document
*
* @param DOMDocument $doc XML document
* @param array $owner Contact data of the poster
* @param array $item
* @param bool $toplevel Is it for en entry element (false) or a feed entry (true)?
* @return DOMElement The entry element where the elements are added
* @throws \Friendica\Network\HTTPException\InternalServerErrorException
* @throws \ImagickException
*/
private static function entryHeader(DOMDocument $doc, array $owner, array $item, bool $toplevel): DOMElement
{
if (!$toplevel) {
$entry = $doc->createElement('entry');
if ($owner['contact-type'] == Contact::TYPE_COMMUNITY) {
$entry->setAttribute('xmlns:activity', ActivityNamespace::ACTIVITY);
$contact = Contact::getByURL($item['author-link']) ?: $owner;
$contact['nickname'] = $contact['nickname'] ?? $contact['nick'];
$author = self::addAuthor($doc, $contact, false);
$entry->appendChild($author);
}
} else {
$entry = $doc->createElementNS(ActivityNamespace::ATOM1, 'entry');
$entry->setAttribute('xmlns:thr', ActivityNamespace::THREAD);
$entry->setAttribute('xmlns:poco', ActivityNamespace::POCO);
$author = self::addAuthor($doc, $owner);
$entry->appendChild($author);
}
return $entry;
}
}

View file

@ -1,1822 +0,0 @@
<?php
// Copyright (C) 2010-2024, the Friendica project
// SPDX-FileCopyrightText: 2010-2024 the Friendica project
//
// SPDX-License-Identifier: AGPL-3.0-or-later
namespace Friendica\Protocol;
use DOMDocument;
use DOMElement;
use DOMXPath;
use Friendica\App;
use Friendica\Content\Text\BBCode;
use Friendica\Content\Text\HTML;
use Friendica\Core\Cache\Enum\Duration;
use Friendica\Core\Logger;
use Friendica\Core\Protocol;
use Friendica\Database\DBA;
use Friendica\DI;
use Friendica\Model\APContact;
use Friendica\Model\Contact;
use Friendica\Model\Conversation;
use Friendica\Model\Item;
use Friendica\Model\ItemURI;
use Friendica\Model\Post;
use Friendica\Model\Tag;
use Friendica\Model\User;
use Friendica\Network\HTTPClient\Client\HttpClientAccept;
use Friendica\Network\HTTPClient\Client\HttpClientOptions;
use Friendica\Network\HTTPClient\Client\HttpClientRequest;
use Friendica\Network\Probe;
use Friendica\Util\DateTimeFormat;
use Friendica\Util\Images;
use Friendica\Util\Proxy;
use Friendica\Util\Strings;
use Friendica\Util\XML;
/**
* This class contain functions for the OStatus protocol
*/
class OStatus
{
private static $itemlist;
private static $conv_list = [];
/**
* Fetches author data
*
* @param DOMXPath $xpath The xpath object
* @param object $context The xml context of the author details
* @param array $importer user record of the importing user
* @param array $contact Called by reference, will contain the fetched contact
* @param bool $onlyfetch Only fetch the header without updating the contact entries
*
* @return array Array of author related entries for the item
* @throws \Friendica\Network\HTTPException\InternalServerErrorException
* @throws \ImagickException
*/
private static function fetchAuthor(DOMXPath $xpath, $context, array $importer, array &$contact = null, bool $onlyfetch): array
{
$author = [];
$author['author-link'] = XML::getFirstNodeValue($xpath, 'atom:author/atom:uri/text()', $context);
$author['author-name'] = XML::getFirstNodeValue($xpath, 'atom:author/atom:name/text()', $context);
$addr = XML::getFirstNodeValue($xpath, 'atom:author/atom:email/text()', $context);
$aliaslink = $author['author-link'];
$alternate_item = $xpath->query("atom:author/atom:link[@rel='alternate']", $context)->item(0);
if (is_object($alternate_item)) {
foreach ($alternate_item->attributes as $attributes) {
if (($attributes->name == 'href') && ($attributes->textContent != '')) {
$author['author-link'] = $attributes->textContent;
}
}
}
$author['author-id'] = Contact::getIdForURL($author['author-link']);
$author['contact-id'] = ($contact['id'] ?? 0) ?: $author['author-id'];
$contact = [];
/*
This here would be better, but we would get problems with contacts from the statusnet addon
This is kept here as a reminder for the future
$cid = Contact::getIdForURL($author['author-link'], $importer['uid']);
if ($cid) {
$contact = DBA::selectFirst('contact', [], ['id' => $cid]);
}
*/
if ($aliaslink != '') {
$contact = DBA::selectFirst('contact', [], [
"`uid` = ? AND `alias` = ? AND `rel` IN (?, ?)",
$importer['uid'],
$aliaslink,
Contact::SHARING, Contact::FRIEND,
]);
}
if (!DBA::isResult($contact) && $author['author-link'] != '') {
if ($aliaslink == '') {
$aliaslink = $author['author-link'];
}
$contact = DBA::selectFirst('contact', [], [
"`uid` = ? AND `nurl` IN (?, ?) AND `rel` IN (?, ?)",
$importer['uid'],
Strings::normaliseLink($author['author-link']),
Strings::normaliseLink($aliaslink),
Contact::SHARING,
Contact::FRIEND,
]);
}
if (!DBA::isResult($contact) && ($addr != '')) {
$contact = DBA::selectFirst('contact', [], [
"`uid` = ? AND `addr` = ? AND `rel` IN (?, ?)",
$importer['uid'],
$addr,
Contact::SHARING,
Contact::FRIEND,
]);
}
if (DBA::isResult($contact)) {
if ($contact['blocked']) {
$contact['id'] = -1;
} elseif (!empty(APContact::getByURL($contact['url'], false))) {
ActivityPub\Receiver::switchContact($contact['id'], $importer['uid'], $contact['url']);
}
$author['contact-id'] = $contact['id'];
}
$avatarlist = [];
$avatars = $xpath->query("atom:author/atom:link[@rel='avatar']", $context);
foreach ($avatars as $avatar) {
$href = '';
$width = 0;
foreach ($avatar->attributes as $attributes) {
if ($attributes->name == 'href') {
$href = $attributes->textContent;
}
if ($attributes->name == 'width') {
$width = $attributes->textContent;
}
}
if ($href != '') {
$avatarlist[$width] = $href;
}
}
if (count($avatarlist) > 0) {
krsort($avatarlist);
$author['author-avatar'] = Probe::fixAvatar(current($avatarlist), $author['author-link']);
}
$displayname = XML::getFirstNodeValue($xpath, 'atom:author/poco:displayName/text()', $context);
if ($displayname != '') {
$author['author-name'] = $displayname;
}
$author['owner-id'] = $author['author-id'];
// Only update the contacts if it is an OStatus contact
if (DBA::isResult($contact) && ($contact['id'] > 0) && !$onlyfetch && ($contact['network'] == Protocol::OSTATUS)) {
// Update contact data
$current = $contact;
unset($current['name-date']);
// This query doesn't seem to work
// $value = $xpath->query("atom:link[@rel='salmon']", $context)->item(0)->nodeValue;
// if ($value != "")
// $contact["notify"] = $value;
// This query doesn't seem to work as well - I hate these queries
// $value = $xpath->query("atom:link[@rel='self' and @type='application/atom+xml']", $context)->item(0)->nodeValue;
// if ($value != "")
// $contact["poll"] = $value;
$contact['url'] = $author['author-link'];
$contact['nurl'] = Strings::normaliseLink($contact['url']);
$value = XML::getFirstNodeValue($xpath, 'atom:author/atom:uri/text()', $context);
if ($value != '') {
$contact['alias'] = $value;
}
$value = XML::getFirstNodeValue($xpath, 'atom:author/poco:displayName/text()', $context);
if ($value != '') {
$contact['name'] = $value;
}
$value = XML::getFirstNodeValue($xpath, 'atom:author/poco:preferredUsername/text()', $context);
if ($value != '') {
$contact['nick'] = $value;
}
$value = XML::getFirstNodeValue($xpath, 'atom:author/poco:note/text()', $context);
if ($value != '') {
$contact['about'] = HTML::toBBCode($value);
}
$value = XML::getFirstNodeValue($xpath, 'atom:author/poco:address/poco:formatted/text()', $context);
if ($value != '') {
$contact['location'] = $value;
}
$contact['name-date'] = DateTimeFormat::utcNow();
Contact::update($contact, ['id' => $contact['id']], $current);
if (!empty($author['author-avatar']) && ($author['author-avatar'] != $current['avatar'])) {
Logger::info('Update profile picture for contact ' . $contact['id']);
Contact::updateAvatar($contact['id'], $author['author-avatar']);
}
// Ensure that we are having this contact (with uid=0)
$cid = Contact::getIdForURL($aliaslink);
if ($cid) {
$fields = ['url', 'nurl', 'name', 'nick', 'alias', 'about', 'location'];
$old_contact = DBA::selectFirst('contact', $fields, ['id' => $cid]);
// Update it with the current values
$fields = [
'url' => $author['author-link'],
'name' => $contact['name'],
'nurl' => Strings::normaliseLink($author['author-link']),
'nick' => $contact['nick'],
'alias' => $contact['alias'],
'about' => $contact['about'],
'location' => $contact['location'],
'success_update' => DateTimeFormat::utcNow(),
'last-update' => DateTimeFormat::utcNow(),
];
Contact::update($fields, ['id' => $cid], $old_contact);
// Update the avatar
if (!empty($author['author-avatar'])) {
Contact::updateAvatar($cid, $author['author-avatar']);
}
}
} elseif (empty($contact['network']) || ($contact['network'] != Protocol::DFRN)) {
$contact = [];
}
return $author;
}
/**
* Fetches author data from a given XML string
*
* @param string $xml The XML
* @param array $importer user record of the importing user
*
* @return array Array of author related entries for the item
* @throws \Friendica\Network\HTTPException\InternalServerErrorException
* @throws \ImagickException
*/
public static function salmonAuthor(string $xml, array $importer): array
{
if (empty($xml)) {
return [];
}
$doc = new DOMDocument();
@$doc->loadXML($xml);
$xpath = new DOMXPath($doc);
$xpath->registerNamespace('atom', ActivityNamespace::ATOM1);
$xpath->registerNamespace('thr', ActivityNamespace::THREAD);
$xpath->registerNamespace('georss', ActivityNamespace::GEORSS);
$xpath->registerNamespace('activity', ActivityNamespace::ACTIVITY);
$xpath->registerNamespace('media', ActivityNamespace::MEDIA);
$xpath->registerNamespace('poco', ActivityNamespace::POCO);
$xpath->registerNamespace('ostatus', ActivityNamespace::OSTATUS);
$xpath->registerNamespace('statusnet', ActivityNamespace::STATUSNET);
$contact = ['id' => 0];
// Fetch the first author
$authordata = $xpath->query('//author')->item(0);
$author = self::fetchAuthor($xpath, $authordata, $importer, $contact, true);
return $author;
}
/**
* Read attributes from element
*
* @param object $element Element object
* @return array attributes
*/
private static function readAttributes($element): array
{
$attribute = [];
foreach ($element->attributes as $attributes) {
$attribute[$attributes->name] = $attributes->textContent;
}
return $attribute;
}
/**
* Imports an XML string containing OStatus elements
*
* @param string $xml The XML
* @param array $importer user record of the importing user
* @param array $contact contact
* @param string $hub Called by reference, returns the fetched hub data
* @return void
* @throws \Friendica\Network\HTTPException\InternalServerErrorException
* @throws \ImagickException
*/
public static function import($xml, array $importer, array &$contact, &$hub)
{
self::process($xml, $importer, $contact, $hub, false, true, Conversation::PUSH);
}
/**
* Internal feed processing
*
* @param string $xml The XML
* @param array $importer user record of the importing user
* @param array $contact contact
* @param string $hub Called by reference, returns the fetched hub data
* @param boolean $stored Is the post fresh imported or from the database?
* @param boolean $initialize Is it the leading post so that data has to be initialized?
* @param integer $direction Direction, default UNKNOWN(0)
* @return boolean Could the XML be processed?
* @throws \Friendica\Network\HTTPException\InternalServerErrorException
* @throws \ImagickException
*/
private static function process(string $xml, array $importer, array &$contact = null, string &$hub, bool $stored = false, bool $initialize = true, int $direction = Conversation::UNKNOWN)
{
if ($initialize) {
self::$itemlist = [];
self::$conv_list = [];
}
Logger::info('Import OStatus message for user ' . $importer['uid']);
if (empty($xml)) {
return false;
}
$doc = new DOMDocument();
@$doc->loadXML($xml);
$xpath = new DOMXPath($doc);
$xpath->registerNamespace('atom', ActivityNamespace::ATOM1);
$xpath->registerNamespace('thr', ActivityNamespace::THREAD);
$xpath->registerNamespace('georss', ActivityNamespace::GEORSS);
$xpath->registerNamespace('activity', ActivityNamespace::ACTIVITY);
$xpath->registerNamespace('media', ActivityNamespace::MEDIA);
$xpath->registerNamespace('poco', ActivityNamespace::POCO);
$xpath->registerNamespace('ostatus', ActivityNamespace::OSTATUS);
$xpath->registerNamespace('statusnet', ActivityNamespace::STATUSNET);
$hub = '';
$hub_items = $xpath->query("/atom:feed/atom:link[@rel='hub']")->item(0);
if (is_object($hub_items)) {
$hub_attributes = $hub_items->attributes;
if (is_object($hub_attributes)) {
foreach ($hub_attributes as $hub_attribute) {
if ($hub_attribute->name == 'href') {
$hub = $hub_attribute->textContent;
Logger::info('Found hub ', ['hub' => $hub]);
}
}
}
}
// Initial header elements
$header = [
'uid' => $importer['uid'],
'network' => Protocol::OSTATUS,
'wall' => 0,
'origin' => 0,
'gravity' => Item::GRAVITY_COMMENT,
];
if (!is_object($doc->firstChild) || empty($doc->firstChild->tagName)) {
return false;
}
$first_child = $doc->firstChild->tagName;
if ($first_child == 'feed') {
$entries = $xpath->query('/atom:feed/atom:entry');
} else {
$entries = $xpath->query('/atom:entry');
}
if ($entries->length == 1) {
// We reformat the XML to make it better readable
$doc2 = new DOMDocument();
$doc2->loadXML($xml);
$doc2->preserveWhiteSpace = false;
$doc2->formatOutput = true;
$xml2 = $doc2->saveXML();
$header['protocol'] = Conversation::PARCEL_SALMON;
$header['source'] = $xml2;
$header['direction'] = $direction;
} elseif (!$initialize) {
return false;
}
// Fetch the first author
$authordata = $xpath->query('//author')->item(0);
$author = self::fetchAuthor($xpath, $authordata, $importer, $contact, $stored);
// Reverse the order of the entries
$entrylist = [];
foreach ($entries as $entry) {
$entrylist[] = $entry;
}
foreach (array_reverse($entrylist) as $entry) {
// fetch the author
$authorelement = $xpath->query('/atom:entry/atom:author', $entry);
if ($authorelement->length == 0) {
$authorelement = $xpath->query('atom:author', $entry);
}
if ($authorelement->length > 0) {
$author = self::fetchAuthor($xpath, $entry, $importer, $contact, $stored);
}
$item = array_merge($header, $author);
$item['uri'] = XML::getFirstNodeValue($xpath, 'atom:id/text()', $entry);
$item['uri-id'] = ItemURI::insert(['uri' => $item['uri']]);
$item['verb'] = XML::getFirstNodeValue($xpath, 'activity:verb/text()', $entry);
// Delete a message
if (in_array($item['verb'], ['qvitter-delete-notice', Activity::DELETE, 'delete'])) {
self::deleteNotice($item);
continue;
}
if (in_array($item['verb'], [Activity::O_UNFAVOURITE, Activity::UNFAVORITE])) {
// Ignore "Unfavorite" message
Logger::info('Ignore unfavorite message ', ['item' => $item]);
continue;
}
// Deletions come with the same uri, so we check for duplicates after processing deletions
if (Post::exists(['uid' => $importer['uid'], 'uri' => $item['uri']])) {
Logger::info('Post with URI ' . $item['uri'] . ' already existed for user ' . $importer['uid'] . '.');
continue;
} else {
Logger::info('Processing post with URI ' . $item['uri'] . ' for user ' . $importer['uid'] . '.');
}
if ($item['verb'] == Activity::JOIN) {
// ignore "Join" messages
Logger::info('Ignore join message ', ['item' => $item]);
continue;
}
if ($item['verb'] == 'http://mastodon.social/schema/1.0/block') {
// ignore mastodon "block" messages
Logger::info('Ignore block message ', ['item' => $item]);
continue;
}
if ($item['verb'] == Activity::FOLLOW) {
Contact::addRelationship($importer, $contact, $item);
continue;
}
if ($item['verb'] == Activity::O_UNFOLLOW) {
$dummy = null;
Contact::removeFollower($contact);
continue;
}
if ($item['verb'] == Activity::FAVORITE) {
$orig_uri = $xpath->query('activity:object/atom:id', $entry)->item(0)->nodeValue;
Logger::notice('Favorite', ['uri' => $orig_uri, 'item' => $item]);
$item['body'] = $item['verb'] = Activity::LIKE;
$item['thr-parent'] = $orig_uri;
$item['gravity'] = Item::GRAVITY_ACTIVITY;
$item['object-type'] = Activity\ObjectType::NOTE;
}
// http://activitystrea.ms/schema/1.0/rsvp-yes
if (!in_array($item['verb'], [Activity::POST, Activity::LIKE, Activity::SHARE])) {
Logger::info('Unhandled verb', ['verb' => $item['verb'], 'item' => $item]);
}
self::processPost($xpath, $entry, $item, $importer);
if ($initialize && (count(self::$itemlist) > 0)) {
if (self::$itemlist[0]['uri'] == self::$itemlist[0]['thr-parent']) {
$uid = self::$itemlist[0]['uid'];
// We will import it everytime, when it is started by our contacts
$valid = Contact::isSharingByURL(self::$itemlist[0]['author-link'], $uid);
if (!$valid) {
// If not, then it depends on this setting
$valid = !$uid || DI::pConfig()->get($uid, 'system', 'accept_only_sharer') != Item::COMPLETION_NONE;
if ($valid) {
Logger::info('Item with URI ' . self::$itemlist[0]['uri'] . ' will be imported due to the system settings.');
}
} else {
Logger::info('Item with URI ' . self::$itemlist[0]['uri'] . ' belongs to a contact (' . self::$itemlist[0]['contact-id'] . '). It will be imported.');
}
if ($valid && DI::pConfig()->get($uid, 'system', 'accept_only_sharer') != Item::COMPLETION_LIKE) {
// Never post a thread when the only interaction by our contact was a like
$valid = false;
$verbs = [Activity::POST, Activity::SHARE];
foreach (self::$itemlist as $item) {
if (in_array($item['verb'], $verbs) && Contact::isSharingByURL($item['author-link'], $item['uid'])) {
$valid = true;
}
}
if ($valid) {
Logger::info('Item with URI ' . self::$itemlist[0]['uri'] . ' will be imported since the thread contains posts or shares.');
}
}
} else {
$valid = true;
}
if ($valid) {
$default_contact = 0;
for ($key = count(self::$itemlist) - 1; $key >= 0; $key--) {
if (empty(self::$itemlist[$key]['contact-id'])) {
self::$itemlist[$key]['contact-id'] = $default_contact;
} else {
$default_contact = $item['contact-id'];
}
}
foreach (self::$itemlist as $item) {
$found = Post::exists(['uid' => $importer['uid'], 'uri' => $item['uri']]);
if ($found) {
Logger::notice('Item with URI ' . $item['uri'] . ' for user ' . $importer['uid'] . ' already exists.');
} elseif ($item['contact-id'] < 0) {
Logger::notice('Item with URI ' . $item['uri'] . ' is from a blocked contact.');
} else {
$ret = Item::insert($item);
Logger::info('Item with URI ' . $item['uri'] . ' for user ' . $importer['uid'] . ' stored. Return value: ' . $ret);
}
}
}
self::$itemlist = [];
}
Logger::info('Processing done for post with URI ' . $item['uri'] . ' for user '.$importer['uid'] . '.');
}
return true;
}
/**
* Removes notice item from database
*
* @param array $item item
* @return void
* @throws \Exception
*/
private static function deleteNotice(array $item)
{
$condition = ['uid' => $item['uid'], 'author-id' => $item['author-id'], 'uri' => $item['uri']];
if (!Post::exists($condition)) {
Logger::notice('Item from ' . $item['author-link'] . ' with uri ' . $item['uri'] . ' for user ' . $item['uid'] . " wasn't found. We don't delete it.");
return;
}
Item::markForDeletion($condition);
Logger::notice('Deleted item with URI ' . $item['uri'] . ' for user ' . $item['uid']);
}
/**
* Processes the XML for a post
*
* @param DOMXPath $xpath The xpath object
* @param object $entry The xml entry that is processed
* @param array $item The item array
* @param array $importer user record of the importing user
* @return void
* @throws \Friendica\Network\HTTPException\InternalServerErrorException
* @throws \ImagickException
*/
private static function processPost(DOMXPath $xpath, $entry, array &$item, array $importer)
{
$item['body'] = HTML::toBBCode(XML::getFirstNodeValue($xpath, 'atom:content/text()', $entry));
$item['object-type'] = XML::getFirstNodeValue($xpath, 'activity:object-type/text()', $entry);
if (($item['object-type'] == Activity\ObjectType::BOOKMARK) || ($item['object-type'] == Activity\ObjectType::EVENT)) {
$item['title'] = XML::getFirstNodeValue($xpath, 'atom:title/text()', $entry);
$item['body'] = XML::getFirstNodeValue($xpath, 'atom:summary/text()', $entry);
} elseif ($item['object-type'] == Activity\ObjectType::QUESTION) {
$item['title'] = XML::getFirstNodeValue($xpath, 'atom:title/text()', $entry);
}
$item['created'] = XML::getFirstNodeValue($xpath, 'atom:published/text()', $entry);
$item['edited'] = XML::getFirstNodeValue($xpath, 'atom:updated/text()', $entry);
$item['conversation'] = XML::getFirstNodeValue($xpath, 'ostatus:conversation/text()', $entry);
$conv = $xpath->query('ostatus:conversation', $entry);
if (is_object($conv->item(0))) {
foreach ($conv->item(0)->attributes as $attributes) {
if ($attributes->name == 'ref') {
$item['conversation'] = $attributes->textContent;
}
if ($attributes->name == 'href') {
$item['conversation'] = $attributes->textContent;
}
}
}
$related = '';
$inreplyto = $xpath->query('thr:in-reply-to', $entry);
if (is_object($inreplyto->item(0))) {
foreach ($inreplyto->item(0)->attributes as $attributes) {
if ($attributes->name == 'ref') {
$item['thr-parent'] = $attributes->textContent;
}
if ($attributes->name == 'href') {
$related = $attributes->textContent;
}
}
}
$georsspoint = $xpath->query('georss:point', $entry);
if (!empty($georsspoint) && ($georsspoint->length > 0)) {
$item['coord'] = $georsspoint->item(0)->nodeValue;
}
$categories = $xpath->query('atom:category', $entry);
if ($categories) {
foreach ($categories as $category) {
foreach ($category->attributes as $attributes) {
if ($attributes->name == 'term') {
// Store the hashtag
Tag::store($item['uri-id'], Tag::HASHTAG, $attributes->textContent);
}
}
}
}
$self = '';
$add_body = '';
$links = $xpath->query('atom:link', $entry);
if ($links) {
$link_data = self::processLinks($links, $item);
$self = $link_data['self'];
$add_body = $link_data['add_body'];
}
$repeat_of = '';
$notice_info = $xpath->query('statusnet:notice_info', $entry);
if ($notice_info && ($notice_info->length > 0)) {
foreach ($notice_info->item(0)->attributes as $attributes) {
if ($attributes->name == 'source') {
$item['app'] = strip_tags($attributes->textContent);
}
if ($attributes->name == 'repeat_of') {
$repeat_of = $attributes->textContent;
}
}
}
// Is it a repeated post?
if (($repeat_of != '') || ($item['verb'] == Activity::SHARE)) {
$link_data = self::processRepeatedItem($xpath, $entry, $item, $importer);
if (!empty($link_data['add_body'])) {
$add_body .= $link_data['add_body'];
}
}
$item['body'] .= $add_body;
Tag::storeFromBody($item['uri-id'], $item['body']);
// Mastodon Content Warning
if (($item['verb'] == Activity::POST) && $xpath->evaluate('boolean(atom:summary)', $entry)) {
$clear_text = XML::getFirstNodeValue($xpath, 'atom:summary/text()', $entry);
if (!empty($clear_text)) {
$item['content-warning'] = HTML::toBBCode($clear_text);
}
}
if (isset($item['thr-parent'])) {
if (!Post::exists(['uid' => $importer['uid'], 'uri' => $item['thr-parent']])) {
if ($related != '') {
self::fetchRelated($related, $item['thr-parent'], $importer);
}
} else {
Logger::info('Reply with URI ' . $item['uri'] . ' already existed for user ' . $importer['uid'] . '.');
}
} else {
$item['thr-parent'] = $item['uri'];
$item['gravity'] = Item::GRAVITY_PARENT;
}
self::$itemlist[] = $item;
}
/**
* Fetch related posts and processes them
*
* @param string $related The link to the related item
* @param string $related_uri The related item in "uri" format
* @param array $importer user record of the importing user
* @return void
* @throws \Friendica\Network\HTTPException\InternalServerErrorException
* @throws \ImagickException
*/
private static function fetchRelated(string $related, string $related_uri, array $importer)
{
$stored = false;
$curlResult = DI::httpClient()->get($related, HttpClientAccept::ATOM_XML, [HttpClientOptions::REQUEST => HttpClientRequest::OSTATUS]);
if (!$curlResult->isSuccess() || empty($curlResult->getBodyString())) {
return;
}
$xml = '';
if ($curlResult->inHeader('Content-Type') &&
in_array('application/atom+xml', $curlResult->getHeader('Content-Type'))) {
Logger::info('Directly fetched XML for URI ' . $related_uri);
$xml = $curlResult->getBodyString();
}
if ($xml == '') {
$doc = new DOMDocument();
if (!@$doc->loadHTML($curlResult->getBodyString())) {
return;
}
$xpath = new DOMXPath($doc);
$atom_file = '';
$links = $xpath->query('//link');
if ($links) {
foreach ($links as $link) {
$attribute = self::readAttributes($link);
if (($attribute['rel'] == 'alternate') && ($attribute['type'] == 'application/atom+xml')) {
$atom_file = $attribute['href'];
}
}
if ($atom_file != '') {
$curlResult = DI::httpClient()->get($atom_file, HttpClientAccept::ATOM_XML, [HttpClientOptions::REQUEST => HttpClientRequest::OSTATUS]);
if ($curlResult->isSuccess()) {
Logger::info('Fetched XML for URI ' . $related_uri);
$xml = $curlResult->getBodyString();
}
}
}
}
// Workaround for older GNU Social servers
if (($xml == '') && strstr($related, '/notice/')) {
$curlResult = DI::httpClient()->get(str_replace('/notice/', '/api/statuses/show/', $related) . '.atom', HttpClientAccept::ATOM_XML, [HttpClientOptions::REQUEST => HttpClientRequest::OSTATUS]);
if ($curlResult->isSuccess()) {
Logger::info('GNU Social workaround to fetch XML for URI ' . $related_uri);
$xml = $curlResult->getBodyString();
}
}
// Even more worse workaround for GNU Social ;-)
if ($xml == '') {
$related_guess = self::convertHref($related_uri);
$curlResult = DI::httpClient()->get(str_replace('/notice/', '/api/statuses/show/', $related_guess) . '.atom', HttpClientAccept::ATOM_XML, [HttpClientOptions::REQUEST => HttpClientRequest::OSTATUS]);
if ($curlResult->isSuccess()) {
Logger::info('GNU Social workaround 2 to fetch XML for URI ' . $related_uri);
$xml = $curlResult->getBodyString();
}
}
if ($xml != '') {
$hub = '';
self::process($xml, $importer, $contact, $hub, $stored, false, Conversation::PULL);
} else {
Logger::info('XML could not be fetched for URI: ' . $related_uri . ' - href: ' . $related);
}
return;
}
/**
* Processes the XML for a repeated post
*
* @param DOMXPath $xpath The xpath object
* @param object $entry The xml entry that is processed
* @param array $item The item array
* @param array $importer user record of the importing user
*
* @return array with data from links
* @throws \Friendica\Network\HTTPException\InternalServerErrorException
* @throws \ImagickException
*/
private static function processRepeatedItem(DOMXPath $xpath, $entry, array &$item, array $importer): array
{
$activityobject = $xpath->query('activity:object', $entry)->item(0);
if (!is_object($activityobject)) {
return [];
}
$link_data = [];
$orig_uri = XML::getFirstNodeValue($xpath, 'atom:id/text()', $activityobject);
$links = $xpath->query('atom:link', $activityobject);
if ($links) {
$link_data = self::processLinks($links, $item);
}
$orig_body = XML::getFirstNodeValue($xpath, 'atom:content/text()', $activityobject);
$orig_created = XML::getFirstNodeValue($xpath, 'atom:published/text()', $activityobject);
$orig_edited = XML::getFirstNodeValue($xpath, 'atom:updated/text()', $activityobject);
$orig_author = self::fetchAuthor($xpath, $activityobject, $importer, $dummy, false);
$item['author-name'] = $orig_author['author-name'];
$item['author-link'] = $orig_author['author-link'];
$item['author-id'] = $orig_author['author-id'];
$item['body'] = HTML::toBBCode($orig_body);
$item['created'] = $orig_created;
$item['edited'] = $orig_edited;
$item['uri'] = $orig_uri;
$item['verb'] = XML::getFirstNodeValue($xpath, 'activity:verb/text()', $activityobject);
$item['object-type'] = XML::getFirstNodeValue($xpath, 'activity:object-type/text()', $activityobject);
// Mastodon Content Warning
if (($item['verb'] == Activity::POST) && $xpath->evaluate('boolean(atom:summary)', $activityobject)) {
$clear_text = XML::getFirstNodeValue($xpath, 'atom:summary/text()', $activityobject);
if (!empty($clear_text)) {
$item['content-warning'] = HTML::toBBCode($clear_text);
}
}
$inreplyto = $xpath->query('thr:in-reply-to', $activityobject);
if (is_object($inreplyto->item(0))) {
foreach ($inreplyto->item(0)->attributes as $attributes) {
if ($attributes->name == 'ref') {
$item['thr-parent'] = $attributes->textContent;
}
}
}
return $link_data;
}
/**
* Processes links in the XML
*
* @param object $links The xml data that contain links
* @param array $item The item array
* @return array with data from the links
*/
private static function processLinks($links, array &$item): array
{
$link_data = ['add_body' => '', 'self' => ''];
foreach ($links as $link) {
$attribute = self::readAttributes($link);
if (!empty($attribute['rel']) && !empty($attribute['href'])) {
switch ($attribute['rel']) {
case 'alternate':
$item['plink'] = $attribute['href'];
if (($item['object-type'] == Activity\ObjectType::QUESTION)
|| ($item['object-type'] == Activity\ObjectType::EVENT)
) {
Post\Media::insert(['uri-id' => $item['uri-id'], 'type' => Post\Media::UNKNOWN,
'url' => $attribute['href'], 'mimetype' => $attribute['type'] ?? null,
'size' => $attribute['length'] ?? null, 'description' => $attribute['title'] ?? null]);
}
break;
case 'ostatus:conversation':
$link_data['conversation'] = $attribute['href'];
$item['conversation'] = $link_data['conversation'];
break;
case 'enclosure':
$filetype = strtolower(substr($attribute['type'], 0, strpos($attribute['type'], '/')));
if ($filetype == 'image') {
$link_data['add_body'] .= "\n[img]".$attribute['href'].'[/img]';
} else {
Post\Media::insert(['uri-id' => $item['uri-id'], 'type' => Post\Media::DOCUMENT,
'url' => $attribute['href'], 'mimetype' => $attribute['type'],
'size' => $attribute['length'] ?? null, 'description' => $attribute['title'] ?? null]);
}
break;
case 'related':
if ($item['object-type'] != Activity\ObjectType::BOOKMARK) {
if (!isset($item['thr-parent'])) {
$item['thr-parent'] = $attribute['href'];
}
$link_data['related'] = $attribute['href'];
} else {
Post\Media::insert(['uri-id' => $item['uri-id'], 'type' => Post\Media::UNKNOWN,
'url' => $attribute['href'], 'mimetype' => $attribute['type'] ?? null,
'size' => $attribute['length'] ?? null, 'description' => $attribute['title'] ?? null]);
}
break;
case 'self':
if (empty($item['plink'])) {
$item['plink'] = $attribute['href'];
}
$link_data['self'] = $attribute['href'];
break;
default:
Logger::notice('Unsupported rel=' . $attribute['rel'] . ', href=' . $attribute['href'] . ', object-type=' . $item['object-type']);
}
}
}
return $link_data;
}
/**
* Create an url out of an uri
*
* @param string $href URI in the format "parameter1:parameter1:..."
* @return string URL in the format http(s)://....
*/
private static function convertHref(string $href): string
{
$elements = explode(':', $href);
if ((count($elements) <= 2) || ($elements[0] != 'tag')) {
return $href;
}
$server = explode(',', $elements[1]);
$conversation = explode('=', $elements[2]);
if ((count($elements) == 4) && ($elements[2] == 'post')) {
return 'http://' . $server[0] . '/notice/' . $elements[3];
}
if ((count($conversation) != 2) || ($conversation[1] == '')) {
return $href;
}
if ($elements[3] == 'objectType=thread') {
return 'http://' . $server[0] . '/conversation/' . $conversation[1];
} else {
return 'http://' . $server[0] . '/notice/' . $conversation[1];
}
}
/**
* Adds the header elements to the XML document
*
* @param DOMDocument $doc XML document
* @param array $owner Contact data of the poster
* @param string $filter The related feed filter (activity, posts or comments)
*
* @return DOMElement Header root element
* @throws \Friendica\Network\HTTPException\InternalServerErrorException
*/
private static function addHeader(DOMDocument $doc, array $owner, string $filter): DOMElement
{
$root = $doc->createElementNS(ActivityNamespace::ATOM1, 'feed');
$doc->appendChild($root);
$root->setAttribute('xmlns:thr', ActivityNamespace::THREAD);
$root->setAttribute('xmlns:georss', ActivityNamespace::GEORSS);
$root->setAttribute('xmlns:activity', ActivityNamespace::ACTIVITY);
$root->setAttribute('xmlns:media', ActivityNamespace::MEDIA);
$root->setAttribute('xmlns:poco', ActivityNamespace::POCO);
$root->setAttribute('xmlns:ostatus', ActivityNamespace::OSTATUS);
$root->setAttribute('xmlns:statusnet', ActivityNamespace::STATUSNET);
$root->setAttribute('xmlns:mastodon', ActivityNamespace::MASTODON);
$title = '';
$selfUri = '/feed/' . $owner['nick'] . '/';
switch ($filter) {
case 'activity':
$title = DI::l10n()->t('%s\'s timeline', $owner['name']);
$selfUri .= $filter;
break;
case 'posts':
$title = DI::l10n()->t('%s\'s posts', $owner['name']);
break;
case 'comments':
$title = DI::l10n()->t('%s\'s comments', $owner['name']);
$selfUri .= $filter;
break;
}
$selfUri = '/dfrn_poll/' . $owner['nick'];
$attributes = [
'uri' => 'https://friendi.ca',
'version' => App::VERSION . '-' . DB_UPDATE_VERSION,
];
XML::addElement($doc, $root, 'generator', App::PLATFORM, $attributes);
XML::addElement($doc, $root, 'id', DI::baseUrl() . '/profile/' . $owner['nick']);
XML::addElement($doc, $root, 'title', $title);
XML::addElement($doc, $root, 'subtitle', sprintf("Updates from %s on %s", $owner['name'], DI::config()->get('config', 'sitename')));
XML::addElement($doc, $root, 'logo', User::getAvatarUrl($owner, Proxy::SIZE_SMALL));
XML::addElement($doc, $root, 'updated', DateTimeFormat::utcNow(DateTimeFormat::ATOM));
$author = self::addAuthor($doc, $owner, true);
$root->appendChild($author);
$attributes = [
'href' => $owner['url'],
'rel' => 'alternate',
'type' => 'text/html',
];
XML::addElement($doc, $root, 'link', '', $attributes);
/// @TODO We have to find out what this is
/// $attributes = array("href" => DI::baseUrl()."/sup",
/// "rel" => "http://api.friendfeed.com/2008/03#sup",
/// "type" => "application/json");
/// XML::addElement($doc, $root, "link", "", $attributes);
self::addHubLink($doc, $root, $owner['nick']);
$attributes = ['href' => DI::baseUrl() . '/salmon/' . $owner['nick'], 'rel' => 'salmon'];
XML::addElement($doc, $root, 'link', '', $attributes);
$attributes = ['href' => DI::baseUrl() . '/salmon/' . $owner['nick'], 'rel' => 'http://salmon-protocol.org/ns/salmon-replies'];
XML::addElement($doc, $root, 'link', '', $attributes);
$attributes = ['href' => DI::baseUrl() . '/salmon/' . $owner['nick'], 'rel' => 'http://salmon-protocol.org/ns/salmon-mention'];
XML::addElement($doc, $root, 'link', '', $attributes);
$attributes = ['href' => DI::baseUrl() . $selfUri, 'rel' => 'self', 'type' => 'application/atom+xml'];
XML::addElement($doc, $root, 'link', '', $attributes);
if ($owner['contact-type'] == Contact::TYPE_COMMUNITY) {
$members = DBA::count('contact', [
'uid' => $owner['uid'],
'self' => false,
'pending' => false,
'archive' => false,
'hidden' => false,
'blocked' => false,
]);
XML::addElement($doc, $root, 'statusnet:group_info', '', ['member_count' => $members]);
}
return $root;
}
/**
* Add the link to the push hubs to the XML document
*
* @param DOMDocument $doc XML document
* @param DOMElement $root XML root element where the hub links are added
* @param string $nick Nickname
* @return void
* @throws \Friendica\Network\HTTPException\InternalServerErrorException
*/
public static function addHubLink(DOMDocument $doc, DOMElement $root, string $nick)
{
$h = DI::baseUrl() . '/pubsubhubbub/' . $nick;
XML::addElement($doc, $root, 'link', '', ['href' => $h, 'rel' => 'hub']);
}
/**
* Adds attachment data to the XML document
*
* @param DOMDocument $doc XML document
* @param DOMElement $root XML root element where the hub links are added
* @param array $item Data of the item that is to be posted
* @return void
* @throws \Friendica\Network\HTTPException\InternalServerErrorException
*/
public static function getAttachment(DOMDocument $doc, DOMElement $root, array $item)
{
foreach (Post\Media::getByURIId($item['uri-id'], [Post\Media::AUDIO, Post\Media::IMAGE, Post\Media::VIDEO, Post\Media::DOCUMENT, Post\Media::TORRENT]) as $attachment) {
$attributes = ['rel' => 'enclosure',
'href' => $attachment['url'],
'type' => $attachment['mimetype']];
if (!empty($attachment['size'])) {
$attributes['length'] = intval($attachment['size']);
}
if (!empty($attachment['description'])) {
$attributes['title'] = $attachment['description'];
}
XML::addElement($doc, $root, 'link', '', $attributes);
}
}
/**
* Adds the author element to the XML document
*
* @param DOMDocument $doc XML document
* @param array $owner Contact data of the poster
* @param bool $show_profile Whether to show profile
* @return DOMElement Author element
* @throws \Friendica\Network\HTTPException\InternalServerErrorException
*/
private static function addAuthor(DOMDocument $doc, array $owner, bool $show_profile = true): DOMElement
{
$profile = DBA::selectFirst('profile', ['homepage', 'publish'], ['uid' => $owner['uid']]);
$author = $doc->createElement('author');
XML::addElement($doc, $author, 'id', $owner['url']);
if ($owner['contact-type'] == Contact::TYPE_COMMUNITY) {
XML::addElement($doc, $author, 'activity:object-type', Activity\ObjectType::GROUP);
} else {
XML::addElement($doc, $author, 'activity:object-type', Activity\ObjectType::PERSON);
}
XML::addElement($doc, $author, 'uri', $owner['url']);
XML::addElement($doc, $author, 'name', $owner['nick']);
XML::addElement($doc, $author, 'email', $owner['addr']);
if ($show_profile) {
XML::addElement($doc, $author, 'summary', BBCode::convertForUriId($owner['uri-id'], $owner['about'], BBCode::OSTATUS));
}
$attributes = [
'rel' => 'alternate',
'type' => 'text/html',
'href' => $owner['url'],
];
XML::addElement($doc, $author, 'link', '', $attributes);
$attributes = [
'rel' => 'avatar',
'type' => 'image/jpeg', // To-Do?
'media:width' => Proxy::PIXEL_SMALL,
'media:height' => Proxy::PIXEL_SMALL,
'href' => User::getAvatarUrl($owner, Proxy::SIZE_SMALL),
];
XML::addElement($doc, $author, 'link', '', $attributes);
if (isset($owner['thumb'])) {
$attributes = [
'rel' => 'avatar',
'type' => 'image/jpeg', // To-Do?
'media:width' => Proxy::PIXEL_THUMB,
'media:height' => Proxy::PIXEL_THUMB,
'href' => User::getAvatarUrl($owner, Proxy::SIZE_THUMB),
];
XML::addElement($doc, $author, 'link', '', $attributes);
}
XML::addElement($doc, $author, 'poco:preferredUsername', $owner['nick']);
XML::addElement($doc, $author, 'poco:displayName', $owner['name']);
if ($show_profile) {
XML::addElement($doc, $author, 'poco:note', BBCode::convertForUriId($owner['uri-id'], $owner['about'], BBCode::OSTATUS));
if (trim($owner['location']) != '') {
$element = $doc->createElement('poco:address');
XML::addElement($doc, $element, 'poco:formatted', $owner['location']);
$author->appendChild($element);
}
}
if (DBA::isResult($profile) && !$show_profile) {
if (trim($profile['homepage']) != '') {
$urls = $doc->createElement('poco:urls');
XML::addElement($doc, $urls, 'poco:type', 'homepage');
XML::addElement($doc, $urls, 'poco:value', $profile['homepage']);
XML::addElement($doc, $urls, 'poco:primary', 'true');
$author->appendChild($urls);
}
XML::addElement($doc, $author, 'followers', '', ['url' => DI::baseUrl() . '/profile/' . $owner['nick'] . '/contacts/followers']);
XML::addElement($doc, $author, 'statusnet:profile_info', '', ['local_id' => $owner['uid']]);
if ($profile['publish']) {
XML::addElement($doc, $author, 'mastodon:scope', 'public');
}
}
return $author;
}
/**
* @TODO Picture attachments should look like this:
* <a href="https://status.pirati.ca/attachment/572819" title="https://status.pirati.ca/file/heluecht-20151202T222602-rd3u49p.gif"
* class="attachment thumbnail" id="attachment-572819" rel="nofollow external">https://status.pirati.ca/attachment/572819</a>
*/
/**
* Returns the given activity if present - otherwise returns the "post" activity
*
* @param array $item Data of the item that is to be posted
* @return string activity
*/
public static function constructVerb(array $item): string
{
if (!empty($item['verb'])) {
return $item['verb'];
}
return Activity::POST;
}
/**
* Returns the given object type if present - otherwise returns the "note" object type
*
* @param array $item Data of the item that is to be posted
* @return string Object type
*/
private static function constructObjecttype(array $item): string
{
if (!empty($item['object-type']) && in_array($item['object-type'], [Activity\ObjectType::NOTE, Activity\ObjectType::COMMENT])) {
return $item['object-type'];
}
return Activity\ObjectType::NOTE;
}
/**
* Adds an entry element to the XML document
*
* @param DOMDocument $doc XML document
* @param array $item Data of the item that is to be posted
* @param array $owner Contact data of the poster
* @param bool $toplevel optional default false
*
* @return DOMElement Entry element
* @throws \Friendica\Network\HTTPException\InternalServerErrorException
* @throws \ImagickException
*/
private static function entry(DOMDocument $doc, array $item, array $owner, bool $toplevel = false): DOMElement
{
if ($item['verb'] == Activity::LIKE) {
return self::likeEntry($doc, $item, $owner, $toplevel);
} elseif (in_array($item['verb'], [Activity::FOLLOW, Activity::O_UNFOLLOW])) {
return self::followEntry($doc, $item, $owner, $toplevel);
} else {
return self::noteEntry($doc, $item, $owner, $toplevel);
}
}
/**
* Adds an entry element with a "like"
*
* @param DOMDocument $doc XML document
* @param array $item Data of the item that is to be posted
* @param array $owner Contact data of the poster
* @param bool $toplevel Is it for en entry element (false) or a feed entry (true)?
* @return DOMElement Entry element with "like"
* @throws \Friendica\Network\HTTPException\InternalServerErrorException
* @throws \ImagickException
*/
private static function likeEntry(DOMDocument $doc, array $item, array $owner, bool $toplevel): DOMElement
{
if (($item['gravity'] != Item::GRAVITY_PARENT) && (Strings::normaliseLink($item['author-link']) != Strings::normaliseLink($owner['url']))) {
Logger::info('OStatus entry is from author ' . $owner['url'] . ' - not from ' . $item['author-link'] . '. Quitting.');
}
$entry = self::entryHeader($doc, $owner, $item, $toplevel);
$verb = ActivityNamespace::ACTIVITY_SCHEMA . 'favorite';
self::entryContent($doc, $entry, $item, $owner, 'Favorite', $verb, false);
$parent = Post::selectFirst([], ['uri' => $item['thr-parent'], 'uid' => $item['uid']]);
if (DBA::isResult($parent)) {
$as_object = $doc->createElement('activity:object');
XML::addElement($doc, $as_object, 'activity:object-type', self::constructObjecttype($parent));
self::entryContent($doc, $as_object, $parent, $owner, 'New entry');
$entry->appendChild($as_object);
}
self::entryFooter($doc, $entry, $item, $owner);
return $entry;
}
/**
* Adds the person object element to the XML document
*
* @param DOMDocument $doc XML document
* @param array $owner Contact data of the poster
* @param array $contact Contact data of the target
* @return DOMElement author element
*/
private static function addPersonObject(DOMDocument $doc, array $owner, array $contact): DOMElement
{
$object = $doc->createElement('activity:object');
XML::addElement($doc, $object, 'activity:object-type', Activity\ObjectType::PERSON);
if ($contact['network'] == Protocol::PHANTOM) {
XML::addElement($doc, $object, 'id', $contact['url']);
return $object;
}
XML::addElement($doc, $object, 'id', $contact['alias']);
XML::addElement($doc, $object, 'title', $contact['nick']);
XML::addElement($doc, $object, 'link', '', [
'rel' => 'alternate',
'type' => 'text/html',
'href' => $contact['url'],
]);
$attributes = [
'rel' => 'avatar',
'type' => 'image/jpeg', // To-Do?
'media:width' => 300,
'media:height' => 300,
'href' => $contact['photo'],
];
XML::addElement($doc, $object, 'link', '', $attributes);
XML::addElement($doc, $object, 'poco:preferredUsername', $contact['nick']);
XML::addElement($doc, $object, 'poco:displayName', $contact['name']);
if (trim($contact['location']) != '') {
$element = $doc->createElement('poco:address');
XML::addElement($doc, $element, 'poco:formatted', $contact['location']);
$object->appendChild($element);
}
return $object;
}
/**
* Adds a follow/unfollow entry element
*
* @param DOMDocument $doc XML document
* @param array $item Data of the follow/unfollow message
* @param array $owner Contact data of the poster
* @param bool $toplevel Is it for en entry element (false) or a feed entry (true)?
* @return DOMElement Entry element
* @throws \Friendica\Network\HTTPException\InternalServerErrorException
* @throws \ImagickException
*/
private static function followEntry(DOMDocument $doc, array $item, array $owner, bool $toplevel): DOMElement
{
$item['id'] = $item['parent'] = 0;
$item['created'] = $item['edited'] = date('c');
$item['private'] = Item::PRIVATE;
$contact = Contact::getByURL($item['follow']);
$item['follow'] = $contact['url'];
if ($contact['alias']) {
$item['follow'] = $contact['alias'];
} else {
$contact['alias'] = $contact['url'];
}
$condition = ['uid' => $owner['uid'], 'nurl' => Strings::normaliseLink($contact['url'])];
$user_contact = DBA::selectFirst('contact', ['id'], $condition);
if (DBA::isResult($user_contact)) {
$connect_id = $user_contact['id'];
} else {
$connect_id = 0;
}
if ($item['verb'] == Activity::FOLLOW) {
$message = DI::l10n()->t('%s is now following %s.');
$title = DI::l10n()->t('following');
$action = 'subscription';
} else {
$message = DI::l10n()->t('%s stopped following %s.');
$title = DI::l10n()->t('stopped following');
$action = 'unfollow';
}
$item['uri'] = $item['parent-uri'] = $item['thr-parent']
= 'tag:' . DI::baseUrl()->getHost() .
','.date('Y-m-d').':'.$action.':'.$owner['uid'].
':person:'.$connect_id.':'.$item['created'];
$item['body'] = sprintf($message, $owner['nick'], $contact['nick']);
$entry = self::entryHeader($doc, $owner, $item, $toplevel);
self::entryContent($doc, $entry, $item, $owner, $title);
$object = self::addPersonObject($doc, $owner, $contact);
$entry->appendChild($object);
self::entryFooter($doc, $entry, $item, $owner);
return $entry;
}
/**
* Adds a regular entry element
*
* @param DOMDocument $doc XML document
* @param array $item Data of the item that is to be posted
* @param array $owner Contact data of the poster
* @param bool $toplevel Is it for en entry element (false) or a feed entry (true)?
* @return DOMElement Entry element
* @throws \Friendica\Network\HTTPException\InternalServerErrorException
* @throws \ImagickException
*/
private static function noteEntry(DOMDocument $doc, array $item, array $owner, bool $toplevel): DOMElement
{
if (($item['gravity'] != Item::GRAVITY_PARENT) && (Strings::normaliseLink($item['author-link']) != Strings::normaliseLink($owner['url']))) {
Logger::info('OStatus entry is from author ' . $owner['url'] . ' - not from ' . $item['author-link'] . '. Quitting.');
}
if (!$toplevel) {
if (!empty($item['title'])) {
$title = BBCode::convertForUriId($item['uri-id'], $item['title'], BBCode::OSTATUS);
} else {
$title = sprintf('New note by %s', $owner['nick']);
}
} else {
$title = sprintf('New comment by %s', $owner['nick']);
}
$entry = self::entryHeader($doc, $owner, $item, $toplevel);
XML::addElement($doc, $entry, 'activity:object-type', Activity\ObjectType::NOTE);
self::entryContent($doc, $entry, $item, $owner, $title, '', true);
self::entryFooter($doc, $entry, $item, $owner, true);
return $entry;
}
/**
* Adds a header element to the XML document
*
* @param DOMDocument $doc XML document
* @param array $owner Contact data of the poster
* @param array $item
* @param bool $toplevel Is it for en entry element (false) or a feed entry (true)?
* @return DOMElement The entry element where the elements are added
* @throws \Friendica\Network\HTTPException\InternalServerErrorException
* @throws \ImagickException
*/
public static function entryHeader(DOMDocument $doc, array $owner, array $item, bool $toplevel): DOMElement
{
if (!$toplevel) {
$entry = $doc->createElement('entry');
if ($owner['contact-type'] == Contact::TYPE_COMMUNITY) {
$entry->setAttribute('xmlns:activity', ActivityNamespace::ACTIVITY);
$contact = Contact::getByURL($item['author-link']) ?: $owner;
$contact['nickname'] = $contact['nickname'] ?? $contact['nick'];
$author = self::addAuthor($doc, $contact, false);
$entry->appendChild($author);
}
} else {
$entry = $doc->createElementNS(ActivityNamespace::ATOM1, 'entry');
$entry->setAttribute('xmlns:thr', ActivityNamespace::THREAD);
$entry->setAttribute('xmlns:georss', ActivityNamespace::GEORSS);
$entry->setAttribute('xmlns:activity', ActivityNamespace::ACTIVITY);
$entry->setAttribute('xmlns:media', ActivityNamespace::MEDIA);
$entry->setAttribute('xmlns:poco', ActivityNamespace::POCO);
$entry->setAttribute('xmlns:ostatus', ActivityNamespace::OSTATUS);
$entry->setAttribute('xmlns:statusnet', ActivityNamespace::STATUSNET);
$entry->setAttribute('xmlns:mastodon', ActivityNamespace::MASTODON);
$author = self::addAuthor($doc, $owner);
$entry->appendChild($author);
}
return $entry;
}
/**
* Adds elements to the XML document
*
* @param DOMDocument $doc XML document
* @param DOMElement $entry Entry element where the content is added
* @param array $item Data of the item that is to be posted
* @param array $owner Contact data of the poster
* @param string $title Title for the post
* @param string $verb The activity verb
* @param bool $complete Add the "status_net" element?
* @return void
* @throws \Friendica\Network\HTTPException\InternalServerErrorException
*/
private static function entryContent(DOMDocument $doc, DOMElement $entry, array $item, array $owner, string $title, string $verb = '', bool $complete = true)
{
if ($verb == '') {
$verb = self::constructVerb($item);
}
XML::addElement($doc, $entry, 'id', $item['uri']);
XML::addElement($doc, $entry, 'title', html_entity_decode($title, ENT_QUOTES, 'UTF-8'));
$body = Post\Media::addAttachmentsToBody($item['uri-id'], DI::contentItem()->addSharedPost($item));
$body = Post\Media::addHTMLLinkToBody($item['uri-id'], $body);
if (!empty($item['title'])) {
$body = '[b]' . $item['title'] . "[/b]\n\n" . $body;
}
$body = BBCode::convertForUriId($item['uri-id'], $body, BBCode::OSTATUS);
XML::addElement($doc, $entry, 'content', $body, ['type' => 'html']);
XML::addElement($doc, $entry, 'link', '', [
'rel' => 'alternate',
'type' => 'text/html',
'href' => DI::baseUrl() . '/display/' . $item['guid'],
]);
if ($complete && ($item['id'] > 0)) {
XML::addElement($doc, $entry, 'status_net', '', ['notice_id' => $item['id']]);
}
XML::addElement($doc, $entry, 'activity:verb', $verb);
XML::addElement($doc, $entry, 'published', DateTimeFormat::utc($item['created'] . '+00:00', DateTimeFormat::ATOM));
XML::addElement($doc, $entry, 'updated', DateTimeFormat::utc($item['edited'] . '+00:00', DateTimeFormat::ATOM));
}
/**
* Adds the elements at the foot of an entry to the XML document
*
* @param DOMDocument $doc XML document
* @param object $entry The entry element where the elements are added
* @param array $item Data of the item that is to be posted
* @param array $owner Contact data of the poster
* @param bool $complete default true
* @return void
* @throws \Friendica\Network\HTTPException\InternalServerErrorException
*/
private static function entryFooter(DOMDocument $doc, $entry, array $item, array $owner, bool $complete = true)
{
$mentioned = [];
if ($item['gravity'] != Item::GRAVITY_PARENT) {
$parent = Post::selectFirst(['guid', 'author-link', 'owner-link'], ['id' => $item['parent']]);
$thrparent = Post::selectFirst(['guid', 'author-link', 'owner-link', 'plink'], ['uid' => $owner['uid'], 'uri' => $item['thr-parent']]);
if (DBA::isResult($thrparent)) {
$mentioned[$thrparent['author-link']] = $thrparent['author-link'];
$mentioned[$thrparent['owner-link']] = $thrparent['owner-link'];
$parent_plink = $thrparent['plink'];
} elseif (DBA::isResult($parent)) {
$mentioned[$parent['author-link']] = $parent['author-link'];
$mentioned[$parent['owner-link']] = $parent['owner-link'];
$parent_plink = DI::baseUrl() . '/display/' . $parent['guid'];
} else {
DI::logger()->notice('Missing parent and thr-parent for child item', ['item' => $item]);
}
if (isset($parent_plink)) {
$attributes = [
'ref' => $item['thr-parent'],
'href' => $parent_plink];
XML::addElement($doc, $entry, 'thr:in-reply-to', '', $attributes);
$attributes = [
'rel' => 'related',
'href' => $parent_plink];
XML::addElement($doc, $entry, 'link', '', $attributes);
}
}
if (intval($item['parent']) > 0) {
$conversation_href = $conversation_uri = $item['conversation'];
XML::addElement($doc, $entry, 'link', '', ['rel' => 'ostatus:conversation', 'href' => $conversation_href]);
$attributes = [
'href' => $conversation_href,
'local_id' => $item['parent'],
'ref' => $conversation_uri,
];
XML::addElement($doc, $entry, 'ostatus:conversation', $conversation_uri, $attributes);
}
// uri-id isn't present for follow entry pseudo-items
$tags = Tag::getByURIId($item['uri-id'] ?? 0);
foreach ($tags as $tag) {
$mentioned[$tag['url']] = $tag['url'];
}
// Make sure that mentions are accepted (GNU Social has problems with mixing HTTP and HTTPS)
$newmentions = [];
foreach ($mentioned as $mention) {
$newmentions[str_replace('http://', 'https://', $mention)] = str_replace('http://', 'https://', $mention);
$newmentions[str_replace('https://', 'http://', $mention)] = str_replace('https://', 'http://', $mention);
}
$mentioned = $newmentions;
foreach ($mentioned as $mention) {
$contact = Contact::getByURL($mention, false, ['contact-type']);
if (!empty($contact) && ($contact['contact-type'] == Contact::TYPE_COMMUNITY)) {
XML::addElement($doc, $entry, 'link', '', [
'rel' => 'mentioned',
'ostatus:object-type' => Activity\ObjectType::GROUP,
'href' => $mention,
]);
} else {
XML::addElement($doc, $entry, 'link', '', [
'rel' => 'mentioned',
'ostatus:object-type' => Activity\ObjectType::PERSON,
'href' => $mention,
]);
}
}
if ($owner['contact-type'] == Contact::TYPE_COMMUNITY) {
XML::addElement($doc, $entry, 'link', '', [
'rel' => 'mentioned',
'ostatus:object-type' => 'http://activitystrea.ms/schema/1.0/group',
'href' => $owner['url']
]);
}
if ($item['private'] != Item::PRIVATE) {
XML::addElement($doc, $entry, 'link', '', ['rel' => 'ostatus:attention',
'href' => 'http://activityschema.org/collection/public']);
XML::addElement($doc, $entry, 'link', '', ['rel' => 'mentioned',
'ostatus:object-type' => 'http://activitystrea.ms/schema/1.0/collection',
'href' => 'http://activityschema.org/collection/public']);
XML::addElement($doc, $entry, 'mastodon:scope', 'public');
}
foreach ($tags as $tag) {
if ($tag['type'] == Tag::HASHTAG) {
XML::addElement($doc, $entry, 'category', '', ['term' => $tag['name']]);
}
}
self::getAttachment($doc, $entry, $item);
if ($complete && ($item['id'] > 0)) {
$app = $item['app'];
if ($app == '') {
$app = 'web';
}
$attributes = ['local_id' => $item['id'], 'source' => $app];
if (isset($parent['id'])) {
$attributes['repeat_of'] = $parent['id'];
}
if ($item['coord'] != '') {
XML::addElement($doc, $entry, 'georss:point', $item['coord']);
}
XML::addElement($doc, $entry, 'statusnet:notice_info', '', $attributes);
}
}
/**
* Creates the XML feed for a given nickname
*
* Supported filters:
* - activity (default): all the public posts
* - posts: all the public top-level posts
* - comments: all the public replies
*
* Updates the provided last_update parameter if the result comes from the
* cache or it is empty
*
* @param string $owner_nick Nickname of the feed owner
* @param string $last_update Date of the last update (in "Y-m-d H:i:s" format)
* @param integer $max_items Number of maximum items to fetch
* @param string $filter Feed items filter (activity, posts or comments)
* @param boolean $nocache Wether to bypass caching
* @return string XML feed or empty string on error
* @throws \Friendica\Network\HTTPException\InternalServerErrorException
* @throws \ImagickException
*/
public static function feed(string $owner_nick, string &$last_update, int $max_items = 300, string $filter = 'activity', bool $nocache = false): string
{
$stamp = microtime(true);
$owner = User::getOwnerDataByNick($owner_nick);
if (!$owner) {
return '';
}
$cachekey = 'ostatus:feed:' . $owner_nick . ':' . $filter . ':' . $last_update;
$previous_created = $last_update;
// Don't cache when the last item was posted less than 15 minutes ago (Cache duration)
if ((time() - strtotime($owner['last-item'])) < 15*60) {
$result = DI::cache()->get($cachekey);
if (!$nocache && !is_null($result)) {
Logger::info('Feed duration: ' . number_format(microtime(true) - $stamp, 3) . ' - ' . $owner_nick . ' - ' . $filter . ' - ' . $previous_created . ' (cached)');
$last_update = $result['last_update'];
return $result['feed'];
}
}
if (!strlen($last_update)) {
$last_update = 'now -30 days';
}
$check_date = DateTimeFormat::utc($last_update);
$authorid = Contact::getIdForURL($owner['url']);
$condition = [
"`uid` = ? AND `received` > ? AND NOT `deleted` AND `private` != ? AND `visible` AND `wall` AND `parent-network` IN (?, ?)",
$owner['uid'],
$check_date,
Item::PRIVATE,
Protocol::OSTATUS,
Protocol::DFRN,
];
if ($filter === 'comments') {
$condition[0] .= " AND `object-type` = ? ";
$condition[] = Activity\ObjectType::COMMENT;
}
if ($owner['contact-type'] != Contact::TYPE_COMMUNITY) {
$condition[0] .= " AND `contact-id` = ? AND `author-id` = ?";
$condition[] = $owner['id'];
$condition[] = $authorid;
}
$params = ['order' => ['received' => true], 'limit' => $max_items];
if ($filter === 'posts') {
$ret = Post::selectThread([], $condition, $params);
} else {
$ret = Post::select([], $condition, $params);
}
$items = Post::toArray($ret);
$doc = new DOMDocument('1.0', 'utf-8');
$doc->formatOutput = true;
$root = self::addHeader($doc, $owner, $filter);
foreach ($items as $item) {
if (DI::config()->get('system', 'ostatus_debug')) {
$item['body'] .= '🍼';
}
if (in_array($item['verb'], [Activity::FOLLOW, Activity::O_UNFOLLOW, Activity::LIKE])) {
continue;
}
$entry = self::entry($doc, $item, $owner, false);
$root->appendChild($entry);
if ($last_update < $item['created']) {
$last_update = $item['created'];
}
}
$feeddata = trim($doc->saveXML());
$msg = ['feed' => $feeddata, 'last_update' => $last_update];
DI::cache()->set($cachekey, $msg, Duration::QUARTER_HOUR);
Logger::info('Feed duration: ' . number_format(microtime(true) - $stamp, 3) . ' - ' . $owner_nick . ' - ' . $filter . ' - ' . $previous_created);
return $feeddata;
}
/**
* Creates the XML for a salmon message
*
* @param array $item Data of the item that is to be posted
* @param array $owner Contact data of the poster
*
* @return string XML for the salmon
* @throws \Friendica\Network\HTTPException\InternalServerErrorException
* @throws \ImagickException
*/
public static function salmon(array $item, array $owner): string
{
$doc = new DOMDocument('1.0', 'utf-8');
$doc->formatOutput = true;
if (DI::config()->get('system', 'ostatus_debug')) {
$item['body'] .= '🐟';
}
$entry = self::entry($doc, $item, $owner, true);
$doc->appendChild($entry);
return trim($doc->saveXML());
}
/**
* Checks if the given contact url does support OStatus
*
* @param string $url profile url
* @return boolean
* @throws \Friendica\Network\HTTPException\InternalServerErrorException
* @throws \ImagickException
*/
public static function isSupportedByContactUrl(string $url): bool
{
$probe = Probe::uri($url, Protocol::OSTATUS);
return $probe['network'] == Protocol::OSTATUS;
}
}

View file

@ -7,17 +7,7 @@
namespace Friendica\Protocol;
use Friendica\Core\Logger;
use Friendica\Core\Protocol;
use Friendica\DI;
use Friendica\Model\Item;
use Friendica\Network\HTTPClient\Client\HttpClientAccept;
use Friendica\Network\HTTPClient\Client\HttpClientRequest;
use Friendica\Network\Probe;
use Friendica\Protocol\Salmon\Format\Magic;
use Friendica\Util\Crypto;
use Friendica\Util\Strings;
use Friendica\Util\XML;
use phpseclib3\Crypt\PublicKeyLoader;
/**
@ -28,206 +18,6 @@ use phpseclib3\Crypt\PublicKeyLoader;
*/
class Salmon
{
/**
* @param string $uri Uniform Resource Identifier
* @param string $keyhash encoded key
* @return string Key or empty string on any errors
* @throws \Friendica\Network\HTTPException\InternalServerErrorException
*/
public static function getKey(string $uri, string $keyhash): string
{
$ret = [];
Logger::info('Fetching salmon key for ' . $uri);
$arr = Probe::lrdd($uri);
if (is_array($arr)) {
foreach ($arr as $a) {
if ($a['@attributes']['rel'] === 'magic-public-key') {
$ret[] = $a['@attributes']['href'];
}
}
} else {
return '';
}
// We have found at least one key URL
// If it's inline, parse it - otherwise get the key
if (count($ret) > 0) {
for ($x = 0; $x < count($ret); $x++) {
if (substr($ret[$x], 0, 5) === 'data:') {
if (strstr($ret[$x], ',')) {
$ret[$x] = substr($ret[$x], strpos($ret[$x], ',') + 1);
} else {
$ret[$x] = substr($ret[$x], 5);
}
} elseif (Strings::normaliseLink($ret[$x]) == 'http://') {
$ret[$x] = DI::httpClient()->fetch($ret[$x], HttpClientAccept::MAGIC_KEY, 0, '', HttpClientRequest::SALMON);
Logger::debug('Fetched public key', ['url' => $ret[$x]]);
}
}
}
Logger::notice('Key located', ['ret' => $ret]);
if (count($ret) == 1) {
/* We only found one key so we don't care if the hash matches.
* If it's the wrong key we'll find out soon enough because
* message verification will fail. This also covers some older
* software which don't supply a keyhash. As long as they only
* have one key we'll be right.
*/
return (string) $ret[0];
} else {
foreach ($ret as $a) {
$hash = Strings::base64UrlEncode(hash('sha256', $a));
if ($hash == $keyhash) {
return $a;
}
}
}
return '';
}
/**
* @param array $owner owner
* @param string $url url
* @param string $slap slap
* @return integer
* @throws \Friendica\Network\HTTPException\InternalServerErrorException
*/
public static function slapper(array $owner, string $url, string $slap): int
{
// does contact have a salmon endpoint?
if (!strlen($url)) {
return -1;
}
if (!$owner['sprvkey']) {
Logger::notice(sprintf(
"user '%s' (%d) does not have a salmon private key. Send failed.",
$owner['name'],
$owner['uid']
));
return -1;
}
Logger::info('slapper called for ' . $url . '. Data: ' . $slap);
// create a magic envelope
$data = Strings::base64UrlEncode($slap);
$data_type = 'application/atom+xml';
$encoding = 'base64url';
$algorithm = 'RSA-SHA256';
$keyhash = Strings::base64UrlEncode(hash('sha256', self::salmonKey($owner['spubkey'])), true);
$precomputed = '.' . Strings::base64UrlEncode($data_type) . '.' . Strings::base64UrlEncode($encoding) . '.' . Strings::base64UrlEncode($algorithm);
// GNU Social format
$signature = Strings::base64UrlEncode(Crypto::rsaSign($data . $precomputed, $owner['sprvkey']));
// Compliant format
$signature2 = Strings::base64UrlEncode(Crypto::rsaSign(str_replace('=', '', $data . $precomputed), $owner['sprvkey']));
// Old Status.net format
$signature3 = Strings::base64UrlEncode(Crypto::rsaSign($data, $owner['sprvkey']));
// At first try the non compliant method that works for GNU Social
$xmldata = [
'me:env' => [
'me:data' => $data,
'@attributes' => ['type' => $data_type],
'me:encoding' => $encoding,
'me:alg' => $algorithm,
'me:sig' => $signature,
'@attributes2' => ['key_id' => $keyhash],
]
];
$namespaces = ['me' => ActivityNamespace::SALMON_ME];
$salmon = XML::fromArray($xmldata, $dummy, false, $namespaces);
// slap them
$postResult = DI::httpClient()->post($url, $salmon, [
'Content-type' => 'application/magic-envelope+xml',
'Content-length' => strlen($salmon),
], 0, HttpClientRequest::SALMON);
$return_code = $postResult->getReturnCode();
// check for success, e.g. 2xx
if ($return_code > 299) {
Logger::notice('GNU Social salmon failed. Falling back to compliant mode');
// Now try the compliant mode that normally isn't used for GNU Social
$xmldata = [
'me:env' => [
'me:data' => $data,
'@attributes' => ['type' => $data_type],
'me:encoding' => $encoding,
'me:alg' => $algorithm,
'me:sig' => $signature2,
'@attributes2' => ['key_id' => $keyhash]
]
];
$salmon = XML::fromArray($xmldata, $dummy, false, $namespaces);
// slap them
$postResult = DI::httpClient()->post($url, $salmon, [
'Content-type' => 'application/magic-envelope+xml',
'Content-length' => strlen($salmon),
], 0, HttpClientRequest::SALMON);
$return_code = $postResult->getReturnCode();
}
if ($return_code > 299) {
Logger::notice('compliant salmon failed. Falling back to old status.net');
// Last try. This will most likely fail as well.
$xmldata = [
'me:env' => [
'me:data' => $data,
'@attributes' => ['type' => $data_type],
'me:encoding' => $encoding,
'me:alg' => $algorithm,
'me:sig' => $signature3,
'@attributes2' => ['key_id' => $keyhash],
]
];
$salmon = XML::fromArray($xmldata, $dummy, false, $namespaces);
// slap them
$postResult = DI::httpClient()->post($url, $salmon, [
'Content-type' => 'application/magic-envelope+xml',
'Content-length' => strlen($salmon)
], 0, HttpClientRequest::SALMON);
$return_code = $postResult->getReturnCode();
}
Item::incrementOutbound(Protocol::OSTATUS);
Logger::info('slapper for ' . $url . ' returned ' . $return_code);
if (!$return_code) {
return -1;
}
if (($return_code == 503) && $postResult->inHeader('retry-after')) {
return -1;
}
return (($return_code >= 200) && ($return_code < 300)) ? 0 : 1;
}
/**
* @param string $pubkey public key
* @return string

View file

@ -71,7 +71,7 @@ class MergeContact
*/
private static function mergePersonalContacts(int $first, int $duplicate)
{
$fields = ['self', 'remote_self', 'rel', 'prvkey', 'subhub', 'hub-verify', 'priority', 'writable', 'archive', 'pending',
$fields = ['self', 'remote_self', 'rel', 'prvkey', 'hub-verify', 'priority', 'writable', 'archive', 'pending',
'rating', 'notify_new_posts', 'fetch_further_information', 'ffi_keyword_denylist', 'block_reason'];
$c1 = Contact::getById($first, $fields);
$c2 = Contact::getById($duplicate, $fields);
@ -87,7 +87,7 @@ class MergeContact
$ctarget[$field] = $c1[$field] ?: $c2[$field];
}
foreach (['remote_self', 'subhub', 'writable', 'notify_new_posts'] as $field) {
foreach (['remote_self', 'writable', 'notify_new_posts'] as $field) {
$ctarget[$field] = $c1[$field] || $c2[$field];
}

View file

@ -19,17 +19,13 @@ use Friendica\Model\Circle;
use Friendica\Model\GServer;
use Friendica\Model\Item;
use Friendica\Model\Post;
use Friendica\Model\PushSubscriber;
use Friendica\Model\Tag;
use Friendica\Model\User;
use Friendica\Protocol\Activity;
use Friendica\Protocol\ActivityPub;
use Friendica\Protocol\Diaspora;
use Friendica\Protocol\Delivery;
use Friendica\Protocol\OStatus;
use Friendica\Protocol\Salmon;
use Friendica\Util\LDSignature;
use Friendica\Util\Network;
use Friendica\Util\Strings;
/*
@ -52,7 +48,6 @@ class Notifier
$target_id = $post_uriid;
$top_level = false;
$recipients = [];
$url_recipients = [];
$delivery_contacts_stmt = null;
$target_item = [];
@ -149,12 +144,6 @@ class Notifier
$unlisted = false;
// Do a PuSH
$push_notify = false;
// Deliver directly to a group, don't PuSH
$direct_group_delivery = false;
$only_ap_delivery = false;
$followup = false;
@ -268,32 +257,6 @@ class Notifier
$recipients_followup = [$parent['contact-id']];
Logger::info('Followup', ['target' => $target_id, 'guid' => $target_item['guid'], 'to' => $parent['contact-id']]);
if (($target_item['private'] != Item::PRIVATE) &&
(strlen($target_item['allow_cid'].$target_item['allow_gid'].
$target_item['deny_cid'].$target_item['deny_gid']) == 0))
$push_notify = true;
if (($thr_parent && ($thr_parent['network'] == Protocol::OSTATUS)) || ($parent['network'] == Protocol::OSTATUS)) {
$push_notify = true;
if ($parent["network"] == Protocol::OSTATUS) {
// Distribute the message to the DFRN contacts as if this wasn't a followup since OStatus can't relay comments
// Currently it is work at progress
$condition = ['uid' => $uid, 'network' => Protocol::DFRN, 'blocked' => false, 'pending' => false, 'archive' => false];
$followup_contacts_stmt = DBA::select('contact', ['id'], $condition);
while($followup_contact = DBA::fetch($followup_contacts_stmt)) {
$recipients_followup[] = $followup_contact['id'];
}
DBA::close($followup_contacts_stmt);
}
}
if ($direct_group_delivery) {
$push_notify = false;
}
Logger::info('Notify ' . $target_item["guid"] .' via PuSH: ' . ($push_notify ? "Yes":"No"));
} elseif ($exclusive_delivery) {
$followup = true;
@ -338,17 +301,11 @@ class Notifier
foreach ($people as $person) {
if (substr($person,0,4) === 'cid:') {
$recipients[] = intval(substr($person,4));
} else {
$url_recipients[] = substr($person,4);
}
}
}
}
if (count($url_recipients)) {
Logger::notice('Deliver', ['target' => $target_id, 'guid' => $target_item['guid'], 'recipients' => $url_recipients]);
}
$recipients = array_unique(array_merge($recipients, $allow_people, $allow_circles));
$deny = array_unique(array_merge($deny_people, $deny_circles));
$recipients = array_diff($recipients, $deny);
@ -368,39 +325,7 @@ class Notifier
}
}
// If the thread parent is OStatus then do some magic to distribute the messages.
// We have not only to look at the parent, since it could be a Friendica thread.
if (($thr_parent && ($thr_parent['network'] == Protocol::OSTATUS)) || ($parent['network'] == Protocol::OSTATUS)) {
$diaspora_delivery = false;
Logger::info('Some parent is OStatus for ' . $target_item['guid'] . ' - Author: ' . $thr_parent['author-id'] . ' - Owner: ' . $thr_parent['owner-id']);
// Send a salmon to the parent author
$probed_contact = DBA::selectFirst('contact', ['url', 'notify'], ['id' => $thr_parent['author-id']]);
if (DBA::isResult($probed_contact) && !empty($probed_contact['notify'])) {
Logger::notice('Notify parent author', ['url' => $probed_contact['url'], 'notify' => $probed_contact['notify']]);
$url_recipients[$probed_contact['notify']] = $probed_contact['notify'];
}
// Send a salmon to the parent owner
$probed_contact = DBA::selectFirst('contact', ['url', 'notify'], ['id' => $thr_parent['owner-id']]);
if (DBA::isResult($probed_contact) && !empty($probed_contact['notify'])) {
Logger::notice('Notify parent owner', ['url' => $probed_contact['url'], 'notify' => $probed_contact['notify']]);
$url_recipients[$probed_contact['notify']] = $probed_contact['notify'];
}
// Send a salmon notification to every person we mentioned in the post
foreach (Tag::getByURIId($target_item['uri-id'], [Tag::MENTION, Tag::EXCLUSIVE_MENTION, Tag::IMPLICIT_MENTION]) as $tag) {
$probed_contact = Contact::getByURL($tag['url']);
if (!empty($probed_contact['notify'])) {
Logger::notice('Notify mentioned user', ['url' => $probed_contact['url'], 'notify' => $probed_contact['notify']]);
$url_recipients[$probed_contact['notify']] = $probed_contact['notify'];
}
}
// It only makes sense to distribute answers to OStatus messages to Friendica and OStatus - but not Diaspora
$networks = [Protocol::DFRN];
} elseif ($diaspora_delivery) {
if ($diaspora_delivery) {
$networks = [Protocol::DFRN, Protocol::DIASPORA, Protocol::MAIL];
if (($parent['network'] == Protocol::DIASPORA) || ($thr_parent['network'] == Protocol::DIASPORA)) {
Logger::info('Add AP contacts', ['target' => $target_id, 'guid' => $target_item['guid']]);
@ -453,15 +378,11 @@ class Notifier
$conversants = array_merge($contacts, $participants);
$delivery_queue_count += self::delivery($cmd, $post_uriid, $sender_uid, $target_item, $thr_parent, $owner, $batch_delivery, true, $conversants, $ap_contacts, []);
$push_notify = true;
}
$contacts = DBA::toArray($delivery_contacts_stmt);
$delivery_queue_count += self::delivery($cmd, $post_uriid, $sender_uid, $target_item, $thr_parent, $owner, $batch_delivery, false, $contacts, $ap_contacts, $conversants);
$delivery_queue_count += self::deliverOStatus($target_id, $target_item, $owner, $url_recipients, $public_message, $push_notify);
if (!empty($target_item)) {
Logger::info('Calling hooks for ' . $cmd . ' ' . $target_id);
@ -612,50 +533,6 @@ class Notifier
return $delivery_queue_count;
}
/**
* Deliver the message via OStatus
*
* @param int $target_id
* @param array $target_item
* @param array $owner
* @param array $url_recipients
* @param bool $public_message
* @param bool $push_notify
*
* @return int Count of sent Salmon notifications
* @throws InternalServerErrorException
* @throws Exception
*/
private static function deliverOStatus(int $target_id, array $target_item, array $owner, array $url_recipients, bool $public_message, bool $push_notify): int
{
$a = DI::app();
$delivery_queue_count = 0;
$url_recipients = array_filter($url_recipients);
// send salmon slaps to mentioned remote tags (@foo@example.com) in OStatus posts
// They are especially used for notifications to OStatus users that don't follow us.
if (count($url_recipients) && ($public_message || $push_notify) && !empty($target_item)) {
$slap = OStatus::salmon($target_item, $owner);
foreach ($url_recipients as $url) {
Logger::info('Salmon delivery', ['item' => $target_id, 'to' => $url]);
$delivery_queue_count++;
Salmon::slapper($owner, $url, $slap);
Item::incrementOutbound(Protocol::OSTATUS);
Post\DeliveryData::incrementQueueDone($target_item['uri-id'], Post\DeliveryData::OSTATUS);
}
}
// Notify PuSH subscribers (Used for OStatus distribution of regular posts)
if ($push_notify) {
Logger::info('Activating internal PuSH', ['uid' => $owner['uid']]);
// Handling the pubsubhubbub requests
PushSubscriber::publishFeed($owner['uid'], $a->getQueueValue('priority'));
}
return $delivery_queue_count;
}
/**
* Checks if the current delivery shouldn't be transported to Diaspora.
* This is done for posts from AP authors or posts that are comments to AP authors.

View file

@ -59,15 +59,10 @@ class OnePoll
$updated = DateTimeFormat::utcNow();
// Possibly switch the remote contact to AP
if ($success && ($contact['network'] === Protocol::OSTATUS)) {
ActivityPub\Receiver::switchContact($contact['id'], $importer_uid, $contact['url']);
}
$contact = DBA::selectFirst('contact', [], ['id' => $contact_id]);
if ($success && ($importer_uid != 0) && in_array($contact['rel'], [Contact::SHARING, Contact::FRIEND])
&& in_array($contact['network'], [Protocol::FEED, Protocol::MAIL, Protocol::OSTATUS])) {
&& in_array($contact['network'], [Protocol::FEED, Protocol::MAIL])) {
$importer = User::getOwnerDataById($importer_uid);
if (empty($importer)) {
Logger::warning('No self contact for user', ['uid' => $importer_uid]);
@ -113,7 +108,7 @@ class OnePoll
*/
private static function updateContact(array $contact, array $fields)
{
if (in_array($contact['network'], [Protocol::FEED, Protocol::MAIL, Protocol::OSTATUS])) {
if (in_array($contact['network'], [Protocol::FEED, Protocol::MAIL])) {
// Update the user's contact
Contact::update($fields, ['id' => $contact['id']]);

View file

@ -27,7 +27,7 @@ class PollContacts
$abandon_days = 0;
}
$condition = ['network' => [Protocol::FEED, Protocol::MAIL, Protocol::OSTATUS], 'self' => false, 'blocked' => false, 'archive' => false];
$condition = ['network' => [Protocol::FEED, Protocol::MAIL], 'self' => false, 'blocked' => false, 'archive' => false];
if (!empty($abandon_days)) {
$condition = DBA::mergeConditions($condition,

View file

@ -1,86 +0,0 @@
<?php
// Copyright (C) 2010-2024, the Friendica project
// SPDX-FileCopyrightText: 2010-2024 the Friendica project
//
// SPDX-License-Identifier: AGPL-3.0-or-later
namespace Friendica\Worker;
use Friendica\Core\Logger;
use Friendica\Database\DBA;
use Friendica\DI;
use Friendica\Model\PushSubscriber;
use Friendica\Network\HTTPClient\Client\HttpClientRequest;
use Friendica\Protocol\OStatus;
class PubSubPublish
{
/**
* Publishes subscriber id
*
* @param int $pubsubpublish_id Push subscriber id
* @return void
*/
public static function execute(int $pubsubpublish_id = 0)
{
if ($pubsubpublish_id == 0) {
return;
}
self::publish($pubsubpublish_id);
}
/**
* Publishes push subscriber
*
* @param int $id Push subscriber id
* @return void
*/
private static function publish(int $id)
{
$subscriber = DBA::selectFirst('push_subscriber', [], ['id' => $id]);
if (!DBA::isResult($subscriber)) {
return;
}
/// @todo Check server status with GServer::check()
// Before this can be done we need a way to safely detect the server url.
Logger::info('Generate feed of user ' . $subscriber['nickname'] . ' to ' . $subscriber['callback_url'] . ' - last updated ' . $subscriber['last_update']);
$last_update = $subscriber['last_update'];
$params = OStatus::feed($subscriber['nickname'], $last_update);
if (!$params) {
return;
}
$hmac_sig = hash_hmac('sha1', $params, $subscriber['secret']);
$headers = [
'Content-type' => 'application/atom+xml',
'Link' => sprintf(
'<%s>;rel=hub,<%s>;rel=self',
DI::baseUrl() . '/pubsubhubbub/' . $subscriber['nickname'],
$subscriber['topic']
),
'X-Hub-Signature' => 'sha1=' . $hmac_sig
];
Logger::debug('POST', ['headers' => $headers, 'params' => $params]);
$postResult = DI::httpClient()->post($subscriber['callback_url'], $params, $headers, 0, HttpClientRequest::PUBSUB);
$ret = $postResult->getReturnCode();
if ($ret >= 200 && $ret <= 299) {
Logger::info('Successfully pushed to ' . $subscriber['callback_url']);
PushSubscriber::reset($subscriber['id'], $last_update);
} else {
Logger::notice('Delivery error when pushing to ' . $subscriber['callback_url'] . ' HTTP: ' . $ret);
PushSubscriber::delay($subscriber['id']);
}
}
}

View file

@ -115,8 +115,6 @@ return [
"theme" => ["type" => "varchar(255)", "not null" => "1", "default" => "", "comment" => "user theme preference"],
"pubkey" => ["type" => "text", "comment" => "RSA public key 4096 bit"],
"prvkey" => ["type" => "text", "comment" => "RSA private key 4096 bit"],
"spubkey" => ["type" => "text", "comment" => ""],
"sprvkey" => ["type" => "text", "comment" => ""],
"verified" => ["type" => "boolean", "not null" => "1", "default" => "0", "comment" => "user is verified through email"],
"blocked" => ["type" => "boolean", "not null" => "1", "default" => "0", "comment" => "1 for user is blocked"],
"blockwall" => ["type" => "boolean", "not null" => "1", "default" => "0", "comment" => "Prohibit contacts to post to the profile page of the user"],
@ -229,7 +227,6 @@ return [
"remote_self" => ["type" => "boolean", "not null" => "1", "default" => "0", "comment" => ""],
"rel" => ["type" => "tinyint unsigned", "not null" => "1", "default" => "0", "comment" => "The kind of the relation between the user and the contact"],
"protocol" => ["type" => "char(4)", "not null" => "1", "default" => "", "comment" => "Protocol of the contact"],
"subhub" => ["type" => "boolean", "not null" => "1", "default" => "0", "comment" => ""],
"hub-verify" => ["type" => "varbinary(383)", "not null" => "1", "default" => "", "comment" => ""],
"rating" => ["type" => "tinyint", "not null" => "1", "default" => "0", "comment" => "Automatically detected feed poll frequency"],
"priority" => ["type" => "tinyint unsigned", "not null" => "1", "default" => "0", "comment" => "Feed poll priority"],
@ -1353,7 +1350,6 @@ return [
"dfrn" => ["type" => "mediumint", "not null" => "1", "default" => "0", "comment" => "Number of successful deliveries via DFRN"],
"legacy_dfrn" => ["type" => "mediumint", "not null" => "1", "default" => "0", "comment" => "Number of successful deliveries via legacy DFRN"],
"diaspora" => ["type" => "mediumint", "not null" => "1", "default" => "0", "comment" => "Number of successful deliveries via Diaspora"],
"ostatus" => ["type" => "mediumint", "not null" => "1", "default" => "0", "comment" => "Number of successful deliveries via OStatus"],
],
"indexes" => [
"PRIMARY" => ["uri-id"],
@ -1785,26 +1781,6 @@ return [
"psid" => ["psid"],
]
],
"push_subscriber" => [
"comment" => "Used for OStatus: Contains feed subscribers",
"fields" => [
"id" => ["type" => "int unsigned", "not null" => "1", "extra" => "auto_increment", "primary" => "1", "comment" => "sequential ID"],
"uid" => ["type" => "mediumint unsigned", "not null" => "1", "default" => "0", "foreign" => ["user" => "uid"], "comment" => "User id"],
"callback_url" => ["type" => "varbinary(383)", "not null" => "1", "default" => "", "comment" => ""],
"topic" => ["type" => "varchar(255)", "not null" => "1", "default" => "", "comment" => ""],
"nickname" => ["type" => "varchar(255)", "not null" => "1", "default" => "", "comment" => ""],
"push" => ["type" => "tinyint", "not null" => "1", "default" => "0", "comment" => "Retrial counter"],
"last_update" => ["type" => "datetime", "not null" => "1", "default" => DBA::NULL_DATETIME, "comment" => "Date of last successful trial"],
"next_try" => ["type" => "datetime", "not null" => "1", "default" => DBA::NULL_DATETIME, "comment" => "Next retrial date"],
"renewed" => ["type" => "datetime", "not null" => "1", "default" => DBA::NULL_DATETIME, "comment" => "Date of last subscription renewal"],
"secret" => ["type" => "varchar(255)", "not null" => "1", "default" => "", "comment" => ""],
],
"indexes" => [
"PRIMARY" => ["id"],
"next_try" => ["next_try"],
"uid" => ["uid"]
]
],
"register" => [
"comment" => "registrations requiring admin approval",
"fields" => [
@ -1979,7 +1955,6 @@ return [
"remote_self" => ["type" => "tinyint unsigned", "comment" => "0 => No mirroring, 1-2 => Mirror as own post, 3 => Mirror as reshare"],
"fetch_further_information" => ["type" => "tinyint unsigned", "comment" => "0 => None, 1 => Fetch information, 3 => Fetch keywords, 2 => Fetch both"],
"ffi_keyword_denylist" => ["type" => "text", "comment" => ""],
"subhub" => ["type" => "boolean", "comment" => ""],
"hub-verify" => ["type" => "varbinary(383)", "comment" => ""],
"protocol" => ["type" => "char(4)", "comment" => "Protocol of the contact"],
"rating" => ["type" => "tinyint", "comment" => "Automatically detected feed poll frequency"],

View file

@ -1450,7 +1450,6 @@
"poll" => ["contact", "poll"],
"confirm" => ["contact", "confirm"],
"poco" => ["contact", "poco"],
"subhub" => ["contact", "subhub"],
"hub-verify" => ["contact", "hub-verify"],
"last-update" => ["contact", "last-update"],
"success_update" => ["contact", "success_update"],
@ -1499,8 +1498,6 @@
"theme" => ["user", "theme"],
"upubkey" => ["user", "pubkey"],
"uprvkey" => ["user", "prvkey"],
"sprvkey" => ["user", "sprvkey"],
"spubkey" => ["user", "spubkey"],
"verified" => ["user", "verified"],
"blockwall" => ["user", "blockwall"],
"hidewall" => ["user", "hidewall"],
@ -1693,7 +1690,6 @@
"readonly" => ["ucontact", "readonly"],
"blocked" => ["ucontact", "blocked"],
"block_reason" => ["ucontact", "block_reason"],
"subhub" => ["ucontact", "subhub"],
"hub-verify" => ["ucontact", "hub-verify"],
"reason" => ["ucontact", "reason"],
"dfrn-notify" => ["contact", "notify"],

View file

@ -412,7 +412,7 @@ return [
'/credits' => [Module\Credits::class, [R::GET]],
'/delegation' => [Module\User\Delegation::class, [R::GET, R::POST]],
'/dfrn_notify[/{nickname}]' => [Module\DFRN\Notify::class, [ R::POST]],
'/dfrn_poll/{nickname}' => [Module\DFRN\Poll::class, [R::GET]],
'/dfrn_poll/{nickname}' => [Module\Feed::class, [R::GET]],
'/dirfind' => [Module\Search\Directory::class, [R::GET]],
'/directory' => [Module\Directory::class, [R::GET]],
@ -510,7 +510,6 @@ return [
'/users/deleted' => [Module\Moderation\Users\Deleted::class, [R::GET ]],
'/users/create' => [Module\Moderation\Users\Create::class, [R::GET, R::POST]],
],
'/modexp/{nick}' => [Module\PublicRSAKey::class, [R::GET]],
'/newmember' => [Module\Welcome::class, [R::GET]],
'/nodeinfo/1.0' => [Module\NodeInfo110::class, [R::GET]],
'/nodeinfo/2.0' => [Module\NodeInfo120::class, [R::GET]],
@ -587,14 +586,8 @@ return [
'/u/{nickname}' => $profileRoutes,
'/~{nickname}' => $profileRoutes,
// OStatus stack modules
'/ostatus/repair' => [Module\OStatus\Repair::class, [R::GET ]],
'/ostatus/subscribe' => [Module\OStatus\Subscribe::class, [R::GET ]],
'/poco' => [Module\User\PortableContacts::class, [R::GET ]],
'/pubsub' => [Module\OStatus\PubSub::class, [R::GET, R::POST]],
'/pubsub/{nickname}[/{cid:\d+}]' => [Module\OStatus\PubSub::class, [R::GET, R::POST]],
'/pubsubhubbub[/{nickname}]' => [Module\OStatus\PubSubHubBub::class, [ R::POST]],
'/salmon[/{nickname}]' => [Module\OStatus\Salmon::class, [ R::POST]],
'/search' => [
'[/]' => [Module\Search\Index::class, [R::GET ]],

View file

@ -452,7 +452,7 @@ function pre_update_1364()
return Update::FAILED;
}
if (!DBA::e("DELETE FROM `push_subscriber` WHERE NOT `uid` IN (SELECT `uid` FROM `user`)")) {
if (DBStructure::existsTable('push_subscriber') && !DBA::e("DELETE FROM `push_subscriber` WHERE NOT `uid` IN (SELECT `uid` FROM `user`)")) {
return Update::FAILED;
}

View file

@ -8,7 +8,7 @@ msgid ""
msgstr ""
"Project-Id-Version: 2024.09-dev\n"
"Report-Msgid-Bugs-To: \n"
"POT-Creation-Date: 2024-08-30 05:35+0000\n"
"POT-Creation-Date: 2024-09-02 05:44+0000\n"
"PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n"
"Last-Translator: FULL NAME <EMAIL@ADDRESS>\n"
"Language-Team: LANGUAGE <LL@li.org>\n"
@ -45,30 +45,30 @@ msgid "Item not found."
msgstr ""
#: mod/item.php:449 mod/message.php:54 mod/message.php:100 mod/notes.php:33
#: mod/photos.php:135 mod/photos.php:651 src/Model/Event.php:506
#: mod/photos.php:135 mod/photos.php:627 src/Model/Event.php:506
#: src/Module/Attach.php:41 src/Module/BaseApi.php:89
#: src/Module/BaseNotifications.php:84 src/Module/BaseSettings.php:36
#: src/Module/Calendar/Event/API.php:74 src/Module/Calendar/Event/Form.php:70
#: src/Module/Calendar/Export.php:68 src/Module/Calendar/Show.php:68
#: src/Module/Circle.php:27 src/Module/Circle.php:70
#: src/Module/Contact/Advanced.php:46 src/Module/Contact/Follow.php:73
#: src/Module/Contact/Follow.php:146 src/Module/Contact/MatchInterests.php:73
#: src/Module/Contact/Follow.php:141 src/Module/Contact/MatchInterests.php:73
#: src/Module/Contact/Suggestions.php:40 src/Module/Contact/Unfollow.php:52
#: src/Module/Contact/Unfollow.php:66 src/Module/Contact/Unfollow.php:98
#: src/Module/FollowConfirm.php:24 src/Module/FriendSuggest.php:43
#: src/Module/Invite.php:28 src/Module/Invite.php:117
#: src/Module/Notifications/Notification.php:62
#: src/Module/Notifications/Notification.php:93
#: src/Module/OStatus/Repair.php:46 src/Module/OStatus/Subscribe.php:54
#: src/Module/Post/Edit.php:62 src/Module/Profile/Common.php:61
#: src/Module/Profile/Contacts.php:64 src/Module/Profile/Photos.php:78
#: src/Module/Profile/Schedule.php:25 src/Module/Profile/Schedule.php:42
#: src/Module/Register.php:70 src/Module/Register.php:83
#: src/Module/Register.php:199 src/Module/Register.php:238
#: src/Module/Search/Directory.php:23 src/Module/Settings/Account.php:36
#: src/Module/Settings/Account.php:377 src/Module/Settings/Channels.php:52
#: src/Module/Settings/Channels.php:127 src/Module/Settings/Delegation.php:76
#: src/Module/Settings/Display.php:76 src/Module/Settings/Display.php:183
#: src/Module/OStatus/Subscribe.php:54 src/Module/Post/Edit.php:62
#: src/Module/Profile/Common.php:61 src/Module/Profile/Contacts.php:64
#: src/Module/Profile/Photos.php:78 src/Module/Profile/Schedule.php:25
#: src/Module/Profile/Schedule.php:42 src/Module/Register.php:70
#: src/Module/Register.php:83 src/Module/Register.php:199
#: src/Module/Register.php:238 src/Module/Search/Directory.php:23
#: src/Module/Settings/Account.php:36 src/Module/Settings/Account.php:377
#: src/Module/Settings/Channels.php:52 src/Module/Settings/Channels.php:127
#: src/Module/Settings/Delegation.php:76 src/Module/Settings/Display.php:76
#: src/Module/Settings/Display.php:183
#: src/Module/Settings/Profile/Photo/Crop.php:151
#: src/Module/Settings/Profile/Photo/Index.php:96
#: src/Module/Settings/RemoveMe.php:105 src/Module/Settings/UserExport.php:64
@ -280,16 +280,16 @@ msgstr ""
msgid "Insert web link"
msgstr ""
#: mod/message.php:188 mod/message.php:344 mod/photos.php:1279
#: mod/message.php:188 mod/message.php:344 mod/photos.php:1255
#: src/Content/Conversation.php:386 src/Content/Conversation.php:1562
#: src/Module/Item/Compose.php:199 src/Module/Post/Edit.php:131
#: src/Object/Post.php:604
msgid "Please wait"
msgstr ""
#: mod/message.php:189 mod/message.php:343 mod/photos.php:682
#: mod/photos.php:802 mod/photos.php:1079 mod/photos.php:1120
#: mod/photos.php:1176 mod/photos.php:1256
#: mod/message.php:189 mod/message.php:343 mod/photos.php:658
#: mod/photos.php:778 mod/photos.php:1055 mod/photos.php:1096
#: mod/photos.php:1152 mod/photos.php:1232
#: src/Module/Calendar/Event/Form.php:236 src/Module/Contact/Advanced.php:118
#: src/Module/Contact/Profile.php:370
#: src/Module/Debug/ActivityPubConversion.php:132
@ -375,10 +375,10 @@ msgstr ""
msgid "Save"
msgstr ""
#: mod/photos.php:54 mod/photos.php:117 mod/photos.php:561
#: mod/photos.php:54 mod/photos.php:117 mod/photos.php:537
#: src/Model/Event.php:498 src/Model/Profile.php:213
#: src/Module/Calendar/Export.php:60 src/Module/Calendar/Show.php:60
#: src/Module/DFRN/Poll.php:29 src/Module/Feed.php:52 src/Module/HCard.php:37
#: src/Module/Feed.php:52 src/Module/HCard.php:37
#: src/Module/Profile/Common.php:48 src/Module/Profile/Common.php:57
#: src/Module/Profile/Contacts.php:50 src/Module/Profile/Contacts.php:58
#: src/Module/Profile/Conversations.php:77 src/Module/Profile/Media.php:42
@ -397,7 +397,7 @@ msgstr ""
msgid "Recent Photos"
msgstr ""
#: mod/photos.php:94 mod/photos.php:850 src/Module/Profile/Photos.php:364
#: mod/photos.php:94 mod/photos.php:826 src/Module/Profile/Photos.php:364
#: src/Module/Profile/Photos.php:384
msgid "Upload New Photos"
msgstr ""
@ -427,61 +427,61 @@ msgstr ""
msgid "Failed to delete the photo."
msgstr ""
#: mod/photos.php:528
#: mod/photos.php:504
msgid "a photo"
msgstr ""
#: mod/photos.php:528
#: mod/photos.php:504
#, php-format
msgid "%1$s was tagged in %2$s by %3$s"
msgstr ""
#: mod/photos.php:565 src/Module/Conversation/Community.php:148
#: mod/photos.php:541 src/Module/Conversation/Community.php:148
#: src/Module/Directory.php:35 src/Module/Profile/Photos.php:279
#: src/Module/Search/Index.php:51
msgid "Public access denied."
msgstr ""
#: mod/photos.php:570
#: mod/photos.php:546
msgid "No photos selected"
msgstr ""
#: mod/photos.php:698
#: mod/photos.php:674
#, php-format
msgid "The maximum accepted image size is %s"
msgstr ""
#: mod/photos.php:705
#: mod/photos.php:681
msgid "Upload Photos"
msgstr ""
#: mod/photos.php:709 mod/photos.php:798
#: mod/photos.php:685 mod/photos.php:774
msgid "New album name: "
msgstr ""
#: mod/photos.php:710
#: mod/photos.php:686
msgid "or select existing album:"
msgstr ""
#: mod/photos.php:711
#: mod/photos.php:687
msgid "Do not show a status post for this upload"
msgstr ""
#: mod/photos.php:714 mod/photos.php:1075 src/Content/Conversation.php:388
#: mod/photos.php:690 mod/photos.php:1051 src/Content/Conversation.php:388
#: src/Module/Calendar/Event/Form.php:239 src/Module/Post/Edit.php:169
msgid "Permissions"
msgstr ""
#: mod/photos.php:779
#: mod/photos.php:755
msgid "Do you really want to delete this photo album and all its photos?"
msgstr ""
#: mod/photos.php:780 mod/photos.php:803
#: mod/photos.php:756 mod/photos.php:779
msgid "Delete Album"
msgstr ""
#: mod/photos.php:781 mod/photos.php:881 src/Content/Conversation.php:403
#: src/Module/Contact/Follow.php:159 src/Module/Contact/Revoke.php:92
#: mod/photos.php:757 mod/photos.php:857 src/Content/Conversation.php:403
#: src/Module/Contact/Follow.php:154 src/Module/Contact/Revoke.php:92
#: src/Module/Contact/Unfollow.php:112
#: src/Module/Media/Attachment/Browser.php:63
#: src/Module/Media/Photo/Browser.php:74 src/Module/Post/Edit.php:153
@ -490,157 +490,157 @@ msgstr ""
msgid "Cancel"
msgstr ""
#: mod/photos.php:807
#: mod/photos.php:783
msgid "Edit Album"
msgstr ""
#: mod/photos.php:808
#: mod/photos.php:784
msgid "Drop Album"
msgstr ""
#: mod/photos.php:812
#: mod/photos.php:788
msgid "Show Newest First"
msgstr ""
#: mod/photos.php:814
#: mod/photos.php:790
msgid "Show Oldest First"
msgstr ""
#: mod/photos.php:835 src/Module/Profile/Photos.php:332
#: mod/photos.php:811 src/Module/Profile/Photos.php:332
msgid "View Photo"
msgstr ""
#: mod/photos.php:867
#: mod/photos.php:843
msgid "Permission denied. Access to this item may be restricted."
msgstr ""
#: mod/photos.php:869
#: mod/photos.php:845
msgid "Photo not available"
msgstr ""
#: mod/photos.php:879
#: mod/photos.php:855
msgid "Do you really want to delete this photo?"
msgstr ""
#: mod/photos.php:880 mod/photos.php:1080
#: mod/photos.php:856 mod/photos.php:1056
msgid "Delete Photo"
msgstr ""
#: mod/photos.php:978
#: mod/photos.php:954
msgid "View photo"
msgstr ""
#: mod/photos.php:980
#: mod/photos.php:956
msgid "Edit photo"
msgstr ""
#: mod/photos.php:981
#: mod/photos.php:957
msgid "Delete photo"
msgstr ""
#: mod/photos.php:982
#: mod/photos.php:958
msgid "Use as profile photo"
msgstr ""
#: mod/photos.php:989
#: mod/photos.php:965
msgid "Private Photo"
msgstr ""
#: mod/photos.php:995
#: mod/photos.php:971
msgid "View Full Size"
msgstr ""
#: mod/photos.php:1048
#: mod/photos.php:1024
msgid "Tags: "
msgstr ""
#: mod/photos.php:1051
#: mod/photos.php:1027
msgid "[Select tags to remove]"
msgstr ""
#: mod/photos.php:1066
#: mod/photos.php:1042
msgid "New album name"
msgstr ""
#: mod/photos.php:1067
#: mod/photos.php:1043
msgid "Caption"
msgstr ""
#: mod/photos.php:1068
#: mod/photos.php:1044
msgid "Add a Tag"
msgstr ""
#: mod/photos.php:1068
#: mod/photos.php:1044
msgid "Example: @bob, @Barbara_Jensen, @jim@example.com, #California, #camping"
msgstr ""
#: mod/photos.php:1069
#: mod/photos.php:1045
msgid "Do not rotate"
msgstr ""
#: mod/photos.php:1070
#: mod/photos.php:1046
msgid "Rotate CW (right)"
msgstr ""
#: mod/photos.php:1071
#: mod/photos.php:1047
msgid "Rotate CCW (left)"
msgstr ""
#: mod/photos.php:1117 mod/photos.php:1173 mod/photos.php:1253
#: src/Module/Contact.php:611 src/Module/Item/Compose.php:181
#: mod/photos.php:1093 mod/photos.php:1149 mod/photos.php:1229
#: src/Module/Contact.php:600 src/Module/Item/Compose.php:181
#: src/Object/Post.php:1142
msgid "This is you"
msgstr ""
#: mod/photos.php:1119 mod/photos.php:1175 mod/photos.php:1255
#: mod/photos.php:1095 mod/photos.php:1151 mod/photos.php:1231
#: src/Module/Moderation/Reports.php:102 src/Object/Post.php:598
#: src/Object/Post.php:1144
msgid "Comment"
msgstr ""
#: mod/photos.php:1121 mod/photos.php:1177 mod/photos.php:1257
#: mod/photos.php:1097 mod/photos.php:1153 mod/photos.php:1233
#: src/Content/Conversation.php:400 src/Module/Calendar/Event/Form.php:234
#: src/Module/Item/Compose.php:194 src/Module/Post/Edit.php:151
#: src/Object/Post.php:1158
msgid "Preview"
msgstr ""
#: mod/photos.php:1122 src/Content/Conversation.php:354
#: mod/photos.php:1098 src/Content/Conversation.php:354
#: src/Module/Post/Edit.php:116 src/Object/Post.php:1146
msgid "Loading..."
msgstr ""
#: mod/photos.php:1214 src/Content/Conversation.php:1484
#: mod/photos.php:1190 src/Content/Conversation.php:1484
#: src/Object/Post.php:260
msgid "Select"
msgstr ""
#: mod/photos.php:1215 src/Content/Conversation.php:1485
#: mod/photos.php:1191 src/Content/Conversation.php:1485
#: src/Module/Moderation/Users/Active.php:122
#: src/Module/Moderation/Users/Blocked.php:122
#: src/Module/Moderation/Users/Index.php:137
#: src/Module/Settings/Connectors.php:240
#: src/Module/Settings/Connectors.php:233
#: src/Module/Settings/Server/Index.php:95
msgid "Delete"
msgstr ""
#: mod/photos.php:1276 src/Object/Post.php:426
#: mod/photos.php:1252 src/Object/Post.php:426
msgid "Like"
msgstr ""
#: mod/photos.php:1277 src/Object/Post.php:426
#: mod/photos.php:1253 src/Object/Post.php:426
msgid "I like this (toggle)"
msgstr ""
#: mod/photos.php:1278 src/Object/Post.php:427
#: mod/photos.php:1254 src/Object/Post.php:427
msgid "Dislike"
msgstr ""
#: mod/photos.php:1280 src/Object/Post.php:427
#: mod/photos.php:1256 src/Object/Post.php:427
msgid "I don't like this (toggle)"
msgstr ""
#: mod/photos.php:1302
#: mod/photos.php:1278
msgid "Map"
msgstr ""
@ -772,19 +772,19 @@ msgid "All contacts"
msgstr ""
#: src/BaseModule.php:425 src/Content/Conversation/Factory/Channel.php:32
#: src/Content/Widget.php:226 src/Core/ACL.php:181 src/Module/Contact.php:406
#: src/Content/Widget.php:218 src/Core/ACL.php:181 src/Module/Contact.php:395
#: src/Module/Privacy/PermissionTooltip.php:150
#: src/Module/Privacy/PermissionTooltip.php:172
#: src/Module/Settings/Channels.php:146
msgid "Followers"
msgstr ""
#: src/BaseModule.php:430 src/Content/Widget.php:227 src/Module/Contact.php:409
#: src/BaseModule.php:430 src/Content/Widget.php:219 src/Module/Contact.php:398
#: src/Module/Settings/Channels.php:145
msgid "Following"
msgstr ""
#: src/BaseModule.php:435 src/Content/Widget.php:228 src/Module/Contact.php:412
#: src/BaseModule.php:435 src/Content/Widget.php:220 src/Module/Contact.php:401
msgid "Mutual friends"
msgstr ""
@ -942,7 +942,7 @@ msgstr ""
msgid "Enter user nickname: "
msgstr ""
#: src/Console/User.php:168 src/Model/User.php:833
#: src/Console/User.php:168 src/Model/User.php:831
#: src/Module/Api/Twitter/ContactEndpoint.php:60
#: src/Module/Moderation/Users/Active.php:57
#: src/Module/Moderation/Users/Blocked.php:57
@ -1694,8 +1694,8 @@ msgstr ""
msgid "Network Widgets"
msgstr ""
#: src/Content/Feature.php:115 src/Content/Widget.php:202
#: src/Model/Circle.php:587 src/Module/Contact.php:392
#: src/Content/Feature.php:115 src/Content/Widget.php:194
#: src/Model/Circle.php:587 src/Module/Contact.php:381
#: src/Module/Welcome.php:62
msgid "Circles"
msgstr ""
@ -1706,7 +1706,7 @@ msgstr ""
#: src/Content/Feature.php:116 src/Content/GroupManager.php:133
#: src/Content/Nav.php:264 src/Content/Text/HTML.php:868
#: src/Content/Widget.php:524 src/Model/User.php:1399
#: src/Content/Widget.php:516 src/Model/User.php:1390
msgid "Groups"
msgstr ""
@ -1714,7 +1714,7 @@ msgstr ""
msgid "Display posts that have been distributed by the selected group."
msgstr ""
#: src/Content/Feature.php:117 src/Content/Widget.php:493
#: src/Content/Feature.php:117 src/Content/Widget.php:485
msgid "Archives"
msgstr ""
@ -1722,7 +1722,7 @@ msgstr ""
msgid "Display an archive where posts can be selected by month and year."
msgstr ""
#: src/Content/Feature.php:118 src/Content/Widget.php:275
#: src/Content/Feature.php:118 src/Content/Widget.php:267
msgid "Protocols"
msgstr ""
@ -1730,7 +1730,7 @@ msgstr ""
msgid "Display posts with the selected protocols."
msgstr ""
#: src/Content/Feature.php:119 src/Content/Widget.php:530
#: src/Content/Feature.php:119 src/Content/Widget.php:522
#: src/Module/Settings/Account.php:433
msgid "Account Types"
msgstr ""
@ -1739,8 +1739,8 @@ msgstr ""
msgid "Display posts done by accounts with the selected account type."
msgstr ""
#: src/Content/Feature.php:120 src/Content/Widget.php:579
#: src/Module/Admin/Site.php:466 src/Module/BaseSettings.php:111
#: src/Content/Feature.php:120 src/Content/Widget.php:571
#: src/Module/Admin/Site.php:464 src/Module/BaseSettings.php:111
#: src/Module/Settings/Channels.php:211 src/Module/Settings/Display.php:301
msgid "Channels"
msgstr ""
@ -1757,7 +1757,7 @@ msgstr ""
msgid "Display posts that contain subscribed hashtags."
msgstr ""
#: src/Content/Feature.php:122 src/Content/Widget.php:305
#: src/Content/Feature.php:122 src/Content/Widget.php:297
msgid "Saved Folders"
msgstr ""
@ -1817,12 +1817,12 @@ msgstr ""
msgid "External link to group"
msgstr ""
#: src/Content/GroupManager.php:139 src/Content/Widget.php:499
#: src/Content/GroupManager.php:139 src/Content/Widget.php:491
msgid "show less"
msgstr ""
#: src/Content/GroupManager.php:140 src/Content/Widget.php:397
#: src/Content/Widget.php:500
#: src/Content/GroupManager.php:140 src/Content/Widget.php:389
#: src/Content/Widget.php:492
msgid "show more"
msgstr ""
@ -1830,7 +1830,7 @@ msgstr ""
msgid "Create new group"
msgstr ""
#: src/Content/Item.php:317 src/Model/Item.php:3282
#: src/Content/Item.php:317 src/Model/Item.php:3271
msgid "event"
msgstr ""
@ -1838,7 +1838,7 @@ msgstr ""
msgid "status"
msgstr ""
#: src/Content/Item.php:326 src/Model/Item.php:3284
#: src/Content/Item.php:326 src/Model/Item.php:3273
#: src/Module/Post/Tag/Add.php:109
msgid "photo"
msgstr ""
@ -1852,35 +1852,35 @@ msgstr ""
msgid "Follow Thread"
msgstr ""
#: src/Content/Item.php:415 src/Model/Contact.php:1253
#: src/Content/Item.php:415 src/Model/Contact.php:1249
msgid "View Status"
msgstr ""
#: src/Content/Item.php:416 src/Content/Item.php:439 src/Model/Contact.php:1188
#: src/Model/Contact.php:1244 src/Model/Contact.php:1254
#: src/Content/Item.php:416 src/Content/Item.php:439 src/Model/Contact.php:1184
#: src/Model/Contact.php:1240 src/Model/Contact.php:1250
#: src/Module/Directory.php:144 src/Module/Settings/Profile/Index.php:250
msgid "View Profile"
msgstr ""
#: src/Content/Item.php:417 src/Model/Contact.php:1255
#: src/Content/Item.php:417 src/Model/Contact.php:1251
msgid "View Photos"
msgstr ""
#: src/Content/Item.php:418 src/Model/Contact.php:1222
#: src/Content/Item.php:418 src/Model/Contact.php:1218
#: src/Model/Profile.php:447
msgid "Network Posts"
msgstr ""
#: src/Content/Item.php:419 src/Model/Contact.php:1246
#: src/Model/Contact.php:1257
#: src/Content/Item.php:419 src/Model/Contact.php:1242
#: src/Model/Contact.php:1253
msgid "View Contact"
msgstr ""
#: src/Content/Item.php:420 src/Model/Contact.php:1258
#: src/Content/Item.php:420 src/Model/Contact.php:1254
msgid "Send PM"
msgstr ""
#: src/Content/Item.php:421 src/Module/Contact.php:460
#: src/Content/Item.php:421 src/Module/Contact.php:449
#: src/Module/Contact/Profile.php:518
#: src/Module/Moderation/Blocklist/Contact.php:102
#: src/Module/Moderation/Users/Active.php:123
@ -1888,7 +1888,7 @@ msgstr ""
msgid "Block"
msgstr ""
#: src/Content/Item.php:422 src/Module/Contact.php:461
#: src/Content/Item.php:422 src/Module/Contact.php:450
#: src/Module/Contact/Profile.php:526
#: src/Module/Notifications/Introductions.php:126
#: src/Module/Notifications/Introductions.php:198
@ -1896,7 +1896,7 @@ msgstr ""
msgid "Ignore"
msgstr ""
#: src/Content/Item.php:423 src/Module/Contact.php:462
#: src/Content/Item.php:423 src/Module/Contact.php:451
#: src/Module/Contact/Profile.php:534
msgid "Collapse"
msgstr ""
@ -1916,8 +1916,8 @@ msgid "Search Text"
msgstr ""
#: src/Content/Item.php:436 src/Content/Widget.php:66
#: src/Model/Contact.php:1247 src/Model/Contact.php:1259
#: src/Module/Contact/Follow.php:153 view/theme/vier/theme.php:183
#: src/Model/Contact.php:1243 src/Model/Contact.php:1255
#: src/Module/Contact/Follow.php:148 view/theme/vier/theme.php:183
msgid "Connect/Follow"
msgstr ""
@ -1959,7 +1959,7 @@ msgid "Sign in"
msgstr ""
#: src/Content/Nav.php:215 src/Module/BaseProfile.php:43
#: src/Module/Contact.php:504
#: src/Module/Contact.php:493
msgid "Conversations"
msgstr ""
@ -1968,7 +1968,7 @@ msgid "Conversations you started"
msgstr ""
#: src/Content/Nav.php:216 src/Module/BaseProfile.php:35
#: src/Module/BaseSettings.php:84 src/Module/Contact.php:496
#: src/Module/BaseSettings.php:84 src/Module/Contact.php:485
#: src/Module/Contact/Profile.php:425 src/Module/Profile/Profile.php:256
#: src/Module/Welcome.php:43 view/theme/frio/theme.php:221
msgid "Profile"
@ -1988,7 +1988,7 @@ msgid "Your photos"
msgstr ""
#: src/Content/Nav.php:218 src/Module/BaseProfile.php:59
#: src/Module/BaseProfile.php:62 src/Module/Contact.php:520
#: src/Module/BaseProfile.php:62 src/Module/Contact.php:509
#: view/theme/frio/theme.php:226
msgid "Media"
msgstr ""
@ -2074,8 +2074,8 @@ msgstr ""
#: src/Content/Nav.php:260 src/Content/Nav.php:315
#: src/Content/Text/HTML.php:864 src/Module/BaseProfile.php:113
#: src/Module/BaseProfile.php:116 src/Module/Contact.php:418
#: src/Module/Contact.php:528 view/theme/frio/theme.php:234
#: src/Module/BaseProfile.php:116 src/Module/Contact.php:407
#: src/Module/Contact.php:517 view/theme/frio/theme.php:234
msgid "Contacts"
msgstr ""
@ -2143,7 +2143,7 @@ msgstr ""
msgid "See all notifications"
msgstr ""
#: src/Content/Nav.php:301 src/Module/Settings/Connectors.php:240
#: src/Content/Nav.php:301 src/Module/Settings/Connectors.php:233
msgid "Mark as seen"
msgstr ""
@ -2235,38 +2235,38 @@ msgstr ""
msgid "last"
msgstr ""
#: src/Content/Text/BBCode.php:690 src/Content/Text/BBCode.php:1866
#: src/Content/Text/BBCode.php:1867
#: src/Content/Text/BBCode.php:689 src/Content/Text/BBCode.php:1862
#: src/Content/Text/BBCode.php:1863
msgid "Image/photo"
msgstr ""
#: src/Content/Text/BBCode.php:908
#: src/Content/Text/BBCode.php:907
#, php-format
msgid "<a href=\"%1$s\" target=\"_blank\" rel=\"noopener noreferrer\">%2$s</a> %3$s"
msgstr ""
#: src/Content/Text/BBCode.php:933 src/Model/Item.php:4038
#: src/Model/Item.php:4044 src/Model/Item.php:4045
#: src/Content/Text/BBCode.php:929 src/Model/Item.php:4027
#: src/Model/Item.php:4033 src/Model/Item.php:4034
msgid "Link to source"
msgstr ""
#: src/Content/Text/BBCode.php:1747 src/Content/Text/HTML.php:892
#: src/Content/Text/BBCode.php:1743 src/Content/Text/HTML.php:892
msgid "Click to open/close"
msgstr ""
#: src/Content/Text/BBCode.php:1802
#: src/Content/Text/BBCode.php:1798
msgid "$1 wrote:"
msgstr ""
#: src/Content/Text/BBCode.php:1876 src/Content/Text/BBCode.php:1877
#: src/Content/Text/BBCode.php:1872 src/Content/Text/BBCode.php:1873
msgid "Encrypted content"
msgstr ""
#: src/Content/Text/BBCode.php:2209
#: src/Content/Text/BBCode.php:2205
msgid "Invalid source protocol"
msgstr ""
#: src/Content/Text/BBCode.php:2228
#: src/Content/Text/BBCode.php:2224
msgid "Invalid link protocol"
msgstr ""
@ -2318,7 +2318,7 @@ msgstr ""
msgid "Examples: Robert Morgenstein, Fishing"
msgstr ""
#: src/Content/Widget.php:68 src/Module/Contact.php:452
#: src/Content/Widget.php:68 src/Module/Contact.php:441
#: src/Module/Directory.php:83 view/theme/vier/theme.php:185
msgid "Find"
msgstr ""
@ -2349,63 +2349,63 @@ msgstr ""
msgid "Local Directory"
msgstr ""
#: src/Content/Widget.php:204
#: src/Content/Widget.php:196
msgid "Everyone"
msgstr ""
#: src/Content/Widget.php:229 src/Module/Contact.php:415
#: src/Content/Widget.php:221 src/Module/Contact.php:404
msgid "No relationship"
msgstr ""
#: src/Content/Widget.php:234
#: src/Content/Widget.php:226
msgid "Relationships"
msgstr ""
#: src/Content/Widget.php:236 src/Module/Circle.php:280
#: src/Module/Contact.php:336
#: src/Content/Widget.php:228 src/Module/Circle.php:280
#: src/Module/Contact.php:325
msgid "All Contacts"
msgstr ""
#: src/Content/Widget.php:277
#: src/Content/Widget.php:269
msgid "All Protocols"
msgstr ""
#: src/Content/Widget.php:307 src/Content/Widget.php:338
#: src/Content/Widget.php:299 src/Content/Widget.php:330
msgid "Everything"
msgstr ""
#: src/Content/Widget.php:336
#: src/Content/Widget.php:328
msgid "Categories"
msgstr ""
#: src/Content/Widget.php:393
#: src/Content/Widget.php:385
#, php-format
msgid "%d contact in common"
msgid_plural "%d contacts in common"
msgstr[0] ""
msgstr[1] ""
#: src/Content/Widget.php:501
#: src/Content/Widget.php:493
msgid "On this date"
msgstr ""
#: src/Content/Widget.php:521
#: src/Content/Widget.php:513
msgid "Persons"
msgstr ""
#: src/Content/Widget.php:522
#: src/Content/Widget.php:514
msgid "Organisations"
msgstr ""
#: src/Content/Widget.php:523 src/Model/Contact.php:1751
#: src/Content/Widget.php:515 src/Model/Contact.php:1747
msgid "News"
msgstr ""
#: src/Content/Widget.php:525
#: src/Content/Widget.php:517
msgid "Relays"
msgstr ""
#: src/Content/Widget.php:532 src/Module/Moderation/BaseUsers.php:56
#: src/Content/Widget.php:524 src/Module/Moderation/BaseUsers.php:56
msgid "All"
msgstr ""
@ -2451,12 +2451,12 @@ msgstr[1] ""
msgid "More Trending Tags"
msgstr ""
#: src/Content/Widget/VCard.php:91 src/Model/Contact.php:1216
#: src/Content/Widget/VCard.php:91 src/Model/Contact.php:1212
#: src/Model/Profile.php:441
msgid "Post to group"
msgstr ""
#: src/Content/Widget/VCard.php:96 src/Model/Contact.php:1220
#: src/Content/Widget/VCard.php:96 src/Model/Contact.php:1216
#: src/Model/Profile.php:445 src/Module/Moderation/Item/Source.php:77
msgid "Mention"
msgstr ""
@ -2484,13 +2484,13 @@ msgstr ""
msgid "Network:"
msgstr ""
#: src/Content/Widget/VCard.php:115 src/Model/Contact.php:1248
#: src/Model/Contact.php:1260 src/Model/Profile.php:458
#: src/Content/Widget/VCard.php:115 src/Model/Contact.php:1244
#: src/Model/Contact.php:1256 src/Model/Profile.php:458
#: src/Module/Contact/Profile.php:470
msgid "Unfollow"
msgstr ""
#: src/Content/Widget/VCard.php:121 src/Model/Contact.php:1218
#: src/Content/Widget/VCard.php:121 src/Model/Contact.php:1214
#: src/Model/Profile.php:443
msgid "View group"
msgstr ""
@ -2834,7 +2834,7 @@ msgstr ""
msgid "Could not connect to database."
msgstr ""
#: src/Core/L10n.php:430 src/Model/Item.php:2326
#: src/Core/L10n.php:430 src/Model/Item.php:2315
msgid "Undetermined"
msgstr ""
@ -3194,82 +3194,78 @@ msgstr ""
msgid "Edit circles"
msgstr ""
#: src/Model/Contact.php:1267 src/Module/Moderation/Users/Pending.php:88
#: src/Model/Contact.php:1263 src/Module/Moderation/Users/Pending.php:88
#: src/Module/Notifications/Introductions.php:124
#: src/Module/Notifications/Introductions.php:196
msgid "Approve"
msgstr ""
#: src/Model/Contact.php:1747
#: src/Model/Contact.php:1743
msgid "Organisation"
msgstr ""
#: src/Model/Contact.php:1755
#: src/Model/Contact.php:1751
msgid "Group"
msgstr ""
#: src/Model/Contact.php:1759 src/Module/Moderation/BaseUsers.php:120
#: src/Model/Contact.php:1755 src/Module/Moderation/BaseUsers.php:120
msgid "Relay"
msgstr ""
#: src/Model/Contact.php:3095
#: src/Model/Contact.php:3070
msgid "Disallowed profile URL."
msgstr ""
#: src/Model/Contact.php:3100 src/Module/Friendica.php:86
#: src/Model/Contact.php:3075 src/Module/Friendica.php:86
msgid "Blocked domain"
msgstr ""
#: src/Model/Contact.php:3105
#: src/Model/Contact.php:3080
msgid "Connect URL missing."
msgstr ""
#: src/Model/Contact.php:3114
#: src/Model/Contact.php:3089
msgid "The contact could not be added. Please check the relevant network credentials in your Settings -> Social Networks page."
msgstr ""
#: src/Model/Contact.php:3132
#: src/Model/Contact.php:3107
#, php-format
msgid "Expected network %s does not match actual network %s"
msgstr ""
#: src/Model/Contact.php:3149
#: src/Model/Contact.php:3124
msgid "This seems to be a relay account. They can't be followed by users."
msgstr ""
#: src/Model/Contact.php:3156
#: src/Model/Contact.php:3131
msgid "The profile address specified does not provide adequate information."
msgstr ""
#: src/Model/Contact.php:3158
#: src/Model/Contact.php:3133
msgid "No compatible communication protocols or feeds were discovered."
msgstr ""
#: src/Model/Contact.php:3161
#: src/Model/Contact.php:3136
msgid "An author or name was not found."
msgstr ""
#: src/Model/Contact.php:3164
#: src/Model/Contact.php:3139
msgid "No browser URL could be matched to this address."
msgstr ""
#: src/Model/Contact.php:3167
#: src/Model/Contact.php:3142
msgid "Unable to match @-style Identity Address with a known protocol or email contact."
msgstr ""
#: src/Model/Contact.php:3168
#: src/Model/Contact.php:3143
msgid "Use mailto: in front of address to force email check."
msgstr ""
#: src/Model/Contact.php:3174
msgid "The profile address specified belongs to a network which has been disabled on this site."
msgstr ""
#: src/Model/Contact.php:3179
#: src/Model/Contact.php:3149
msgid "Limited profile. This person will be unable to receive direct/personal notifications from you."
msgstr ""
#: src/Model/Contact.php:3245
#: src/Model/Contact.php:3208
msgid "Unable to retrieve contact information."
msgstr ""
@ -3319,7 +3315,7 @@ msgstr ""
msgid "No events to display"
msgstr ""
#: src/Model/Event.php:502 src/Module/DFRN/Poll.php:33 src/Module/Feed.php:56
#: src/Model/Event.php:502 src/Module/Feed.php:56
#: src/Module/Update/Profile.php:42
msgid "Access to this profile has been restricted."
msgstr ""
@ -3374,90 +3370,90 @@ msgstr ""
msgid "Happy Birthday %s"
msgstr ""
#: src/Model/Item.php:2333
#: src/Model/Item.php:2322
#, php-format
msgid "%s (%s - %s): %s"
msgstr ""
#: src/Model/Item.php:2335
#: src/Model/Item.php:2324
#, php-format
msgid "%s (%s): %s"
msgstr ""
#: src/Model/Item.php:2338
#: src/Model/Item.php:2327
#, php-format
msgid "Detected languages in this post:\\n%s"
msgstr ""
#: src/Model/Item.php:3286
#: src/Model/Item.php:3275
msgid "activity"
msgstr ""
#: src/Model/Item.php:3288
#: src/Model/Item.php:3277
msgid "comment"
msgstr ""
#: src/Model/Item.php:3291 src/Module/Post/Tag/Add.php:109
#: src/Model/Item.php:3280 src/Module/Post/Tag/Add.php:109
msgid "post"
msgstr ""
#: src/Model/Item.php:3461
#: src/Model/Item.php:3450
#, php-format
msgid "%s is blocked"
msgstr ""
#: src/Model/Item.php:3463
#: src/Model/Item.php:3452
#, php-format
msgid "%s is ignored"
msgstr ""
#: src/Model/Item.php:3465
#: src/Model/Item.php:3454
#, php-format
msgid "Content from %s is collapsed"
msgstr ""
#: src/Model/Item.php:3469
#: src/Model/Item.php:3458
msgid "Sensitive content"
msgstr ""
#: src/Model/Item.php:3938
#: src/Model/Item.php:3927
msgid "bytes"
msgstr ""
#: src/Model/Item.php:3969
#: src/Model/Item.php:3958
#, php-format
msgid "%2$s (%3$d%%, %1$d vote)"
msgid_plural "%2$s (%3$d%%, %1$d votes)"
msgstr[0] ""
msgstr[1] ""
#: src/Model/Item.php:3971
#: src/Model/Item.php:3960
#, php-format
msgid "%2$s (%1$d vote)"
msgid_plural "%2$s (%1$d votes)"
msgstr[0] ""
msgstr[1] ""
#: src/Model/Item.php:3976
#: src/Model/Item.php:3965
#, php-format
msgid "%d voter. Poll end: %s"
msgid_plural "%d voters. Poll end: %s"
msgstr[0] ""
msgstr[1] ""
#: src/Model/Item.php:3978
#: src/Model/Item.php:3967
#, php-format
msgid "%d voter."
msgid_plural "%d voters."
msgstr[0] ""
msgstr[1] ""
#: src/Model/Item.php:3980
#: src/Model/Item.php:3969
#, php-format
msgid "Poll end: %s"
msgstr ""
#: src/Model/Item.php:4021 src/Model/Item.php:4022
#: src/Model/Item.php:4010 src/Model/Item.php:4011
msgid "View on separate page"
msgstr ""
@ -3615,138 +3611,138 @@ msgstr ""
msgid "Responsible account: %s"
msgstr ""
#: src/Model/User.php:219 src/Model/User.php:1312
#: src/Model/User.php:217 src/Model/User.php:1310
msgid "SERIOUS ERROR: Generation of security keys failed."
msgstr ""
#: src/Model/User.php:742 src/Model/User.php:775
#: src/Model/User.php:740 src/Model/User.php:773
msgid "Login failed"
msgstr ""
#: src/Model/User.php:807
#: src/Model/User.php:805
msgid "Not enough information to authenticate"
msgstr ""
#: src/Model/User.php:932
#: src/Model/User.php:930
msgid "Password can't be empty"
msgstr ""
#: src/Model/User.php:974
#: src/Model/User.php:972
msgid "Empty passwords are not allowed."
msgstr ""
#: src/Model/User.php:978
#: src/Model/User.php:976
msgid "The new password has been exposed in a public data dump, please choose another."
msgstr ""
#: src/Model/User.php:982
#: src/Model/User.php:980
msgid "The password length is limited to 72 characters."
msgstr ""
#: src/Model/User.php:986
#: src/Model/User.php:984
msgid "The password can't contain white spaces nor accentuated letters"
msgstr ""
#: src/Model/User.php:1195
#: src/Model/User.php:1193
msgid "Passwords do not match. Password unchanged."
msgstr ""
#: src/Model/User.php:1202
#: src/Model/User.php:1200
msgid "An invitation is required."
msgstr ""
#: src/Model/User.php:1206
#: src/Model/User.php:1204
msgid "Invitation could not be verified."
msgstr ""
#: src/Model/User.php:1214
#: src/Model/User.php:1212
msgid "Invalid OpenID url"
msgstr ""
#: src/Model/User.php:1227 src/Security/Authentication.php:214
#: src/Model/User.php:1225 src/Security/Authentication.php:214
msgid "We encountered a problem while logging in with the OpenID you provided. Please check the correct spelling of the ID."
msgstr ""
#: src/Model/User.php:1227 src/Security/Authentication.php:214
#: src/Model/User.php:1225 src/Security/Authentication.php:214
msgid "The error message was:"
msgstr ""
#: src/Model/User.php:1233
#: src/Model/User.php:1231
msgid "Please enter the required information."
msgstr ""
#: src/Model/User.php:1247
#: src/Model/User.php:1245
#, php-format
msgid "system.username_min_length (%s) and system.username_max_length (%s) are excluding each other, swapping values."
msgstr ""
#: src/Model/User.php:1254
#: src/Model/User.php:1252
#, php-format
msgid "Username should be at least %s character."
msgid_plural "Username should be at least %s characters."
msgstr[0] ""
msgstr[1] ""
#: src/Model/User.php:1258
#: src/Model/User.php:1256
#, php-format
msgid "Username should be at most %s character."
msgid_plural "Username should be at most %s characters."
msgstr[0] ""
msgstr[1] ""
#: src/Model/User.php:1266
#: src/Model/User.php:1264
msgid "That doesn't appear to be your full (First Last) name."
msgstr ""
#: src/Model/User.php:1271
#: src/Model/User.php:1269
msgid "Your email domain is not among those allowed on this site."
msgstr ""
#: src/Model/User.php:1275
#: src/Model/User.php:1273
msgid "Not a valid email address."
msgstr ""
#: src/Model/User.php:1278
#: src/Model/User.php:1276
msgid "The nickname was blocked from registration by the nodes admin."
msgstr ""
#: src/Model/User.php:1282 src/Model/User.php:1288
#: src/Model/User.php:1280 src/Model/User.php:1286
msgid "Cannot use that email."
msgstr ""
#: src/Model/User.php:1294
#: src/Model/User.php:1292
msgid "Your nickname can only contain a-z, 0-9 and _."
msgstr ""
#: src/Model/User.php:1302 src/Model/User.php:1359
#: src/Model/User.php:1300 src/Model/User.php:1350
msgid "Nickname is already registered. Please choose another."
msgstr ""
#: src/Model/User.php:1346 src/Model/User.php:1350
#: src/Model/User.php:1337 src/Model/User.php:1341
msgid "An error occurred during registration. Please try again."
msgstr ""
#: src/Model/User.php:1373
#: src/Model/User.php:1364
msgid "An error occurred creating your default profile. Please try again."
msgstr ""
#: src/Model/User.php:1380
#: src/Model/User.php:1371
msgid "An error occurred creating your self contact. Please try again."
msgstr ""
#: src/Model/User.php:1385
#: src/Model/User.php:1376
msgid "Friends"
msgstr ""
#: src/Model/User.php:1389
#: src/Model/User.php:1380
msgid "An error occurred creating your default contact circle. Please try again."
msgstr ""
#: src/Model/User.php:1437
#: src/Model/User.php:1428
msgid "Profile Photos"
msgstr ""
#: src/Model/User.php:1625
#: src/Model/User.php:1616
#, php-format
msgid ""
"\n"
@ -3754,7 +3750,7 @@ msgid ""
"\t\t\tthe administrator of %2$s has set up an account for you."
msgstr ""
#: src/Model/User.php:1628
#: src/Model/User.php:1619
#, php-format
msgid ""
"\n"
@ -3785,12 +3781,12 @@ msgid ""
"\t\tThank you and welcome to %4$s."
msgstr ""
#: src/Model/User.php:1660 src/Model/User.php:1766
#: src/Model/User.php:1651 src/Model/User.php:1757
#, php-format
msgid "Registration details for %s"
msgstr ""
#: src/Model/User.php:1680
#: src/Model/User.php:1671
#, php-format
msgid ""
"\n"
@ -3805,12 +3801,12 @@ msgid ""
"\t\t"
msgstr ""
#: src/Model/User.php:1699
#: src/Model/User.php:1690
#, php-format
msgid "Registration at %s"
msgstr ""
#: src/Model/User.php:1723
#: src/Model/User.php:1714
#, php-format
msgid ""
"\n"
@ -3819,7 +3815,7 @@ msgid ""
"\t\t\t"
msgstr ""
#: src/Model/User.php:1731
#: src/Model/User.php:1722
#, php-format
msgid ""
"\n"
@ -3850,7 +3846,7 @@ msgid ""
"\t\t\tThank you and welcome to %2$s."
msgstr ""
#: src/Model/User.php:1793
#: src/Model/User.php:1784
msgid "User with delegates can't be removed, please remove delegate users first"
msgstr ""
@ -3881,7 +3877,7 @@ msgstr ""
#: src/Module/Admin/Addons/Details.php:97 src/Module/Admin/Addons/Index.php:59
#: src/Module/Admin/Federation.php:206 src/Module/Admin/Logs/Settings.php:74
#: src/Module/Admin/Logs/View.php:71 src/Module/Admin/Queue.php:59
#: src/Module/Admin/Site.php:449 src/Module/Admin/Storage.php:124
#: src/Module/Admin/Site.php:447 src/Module/Admin/Storage.php:124
#: src/Module/Admin/Summary.php:182 src/Module/Admin/Themes/Details.php:82
#: src/Module/Admin/Themes/Index.php:103 src/Module/Admin/Tos.php:63
#: src/Module/Moderation/Users/Create.php:47
@ -3919,11 +3915,11 @@ msgid "Addon %s failed to install."
msgstr ""
#: src/Module/Admin/Addons/Index.php:61 src/Module/Admin/Features.php:69
#: src/Module/Admin/Logs/Settings.php:76 src/Module/Admin/Site.php:452
#: src/Module/Admin/Logs/Settings.php:76 src/Module/Admin/Site.php:450
#: src/Module/Admin/Themes/Index.php:105 src/Module/Admin/Tos.php:72
#: src/Module/Settings/Account.php:549 src/Module/Settings/Addons.php:64
#: src/Module/Settings/Connectors.php:149
#: src/Module/Settings/Connectors.php:242
#: src/Module/Settings/Connectors.php:235
#: src/Module/Settings/Delegation.php:179 src/Module/Settings/Display.php:295
#: src/Module/Settings/Features.php:61
msgid "Save Settings"
@ -4119,8 +4115,8 @@ msgid "Enable Debugging"
msgstr ""
#: src/Module/Admin/Logs/Settings.php:80 src/Module/Admin/Logs/Settings.php:81
#: src/Module/Admin/Logs/Settings.php:82 src/Module/Admin/Site.php:472
#: src/Module/Admin/Site.php:480
#: src/Module/Admin/Logs/Settings.php:82 src/Module/Admin/Site.php:470
#: src/Module/Admin/Site.php:478
msgid "<strong>Read-only</strong> because it is set by an environment variable"
msgstr ""
@ -4269,257 +4265,257 @@ msgstr ""
msgid "Priority"
msgstr ""
#: src/Module/Admin/Site.php:231
#: src/Module/Admin/Site.php:230
#, php-format
msgid "%s is no valid input for maximum media size"
msgstr ""
#: src/Module/Admin/Site.php:236
#: src/Module/Admin/Site.php:235
#, php-format
msgid "%s is no valid input for maximum image size"
msgstr ""
#: src/Module/Admin/Site.php:364 src/Module/Settings/Display.php:201
#: src/Module/Admin/Site.php:362 src/Module/Settings/Display.php:201
msgid "No special theme for mobile devices"
msgstr ""
#: src/Module/Admin/Site.php:381 src/Module/Settings/Display.php:211
#: src/Module/Admin/Site.php:379 src/Module/Settings/Display.php:211
#, php-format
msgid "%s - (Experimental)"
msgstr ""
#: src/Module/Admin/Site.php:393
#: src/Module/Admin/Site.php:391
msgid "No community page"
msgstr ""
#: src/Module/Admin/Site.php:394
#: src/Module/Admin/Site.php:392
msgid "No community page for visitors"
msgstr ""
#: src/Module/Admin/Site.php:395
#: src/Module/Admin/Site.php:393
msgid "Public postings from users of this site"
msgstr ""
#: src/Module/Admin/Site.php:396
#: src/Module/Admin/Site.php:394
msgid "Public postings from the federated network"
msgstr ""
#: src/Module/Admin/Site.php:397
#: src/Module/Admin/Site.php:395
msgid "Public postings from local users and the federated network"
msgstr ""
#: src/Module/Admin/Site.php:403
#: src/Module/Admin/Site.php:401
msgid "Multi user instance"
msgstr ""
#: src/Module/Admin/Site.php:426
#: src/Module/Admin/Site.php:424
msgid "Closed"
msgstr ""
#: src/Module/Admin/Site.php:427
#: src/Module/Admin/Site.php:425
msgid "Requires approval"
msgstr ""
#: src/Module/Admin/Site.php:428
#: src/Module/Admin/Site.php:426
msgid "Open"
msgstr ""
#: src/Module/Admin/Site.php:432
#: src/Module/Admin/Site.php:430
msgid "Don't check"
msgstr ""
#: src/Module/Admin/Site.php:433
#: src/Module/Admin/Site.php:431
msgid "check the stable version"
msgstr ""
#: src/Module/Admin/Site.php:434
#: src/Module/Admin/Site.php:432
msgid "check the development version"
msgstr ""
#: src/Module/Admin/Site.php:438
#: src/Module/Admin/Site.php:436
msgid "none"
msgstr ""
#: src/Module/Admin/Site.php:439
#: src/Module/Admin/Site.php:437
msgid "Local contacts"
msgstr ""
#: src/Module/Admin/Site.php:440
#: src/Module/Admin/Site.php:438
msgid "Interactors"
msgstr ""
#: src/Module/Admin/Site.php:450 src/Module/BaseAdmin.php:76
#: src/Module/Admin/Site.php:448 src/Module/BaseAdmin.php:76
msgid "Site"
msgstr ""
#: src/Module/Admin/Site.php:451
#: src/Module/Admin/Site.php:449
msgid "General Information"
msgstr ""
#: src/Module/Admin/Site.php:453
#: src/Module/Admin/Site.php:451
msgid "Republish users to directory"
msgstr ""
#: src/Module/Admin/Site.php:454 src/Module/Register.php:145
#: src/Module/Admin/Site.php:452 src/Module/Register.php:145
msgid "Registration"
msgstr ""
#: src/Module/Admin/Site.php:455
#: src/Module/Admin/Site.php:453
msgid "File upload"
msgstr ""
#: src/Module/Admin/Site.php:456
#: src/Module/Admin/Site.php:454
msgid "Policies"
msgstr ""
#: src/Module/Admin/Site.php:457 src/Module/Calendar/Event/Form.php:238
#: src/Module/Contact.php:539 src/Module/Profile/Profile.php:264
#: src/Module/Admin/Site.php:455 src/Module/Calendar/Event/Form.php:238
#: src/Module/Contact.php:528 src/Module/Profile/Profile.php:264
msgid "Advanced"
msgstr ""
#: src/Module/Admin/Site.php:458
#: src/Module/Admin/Site.php:456
msgid "Auto Discovered Contact Directory"
msgstr ""
#: src/Module/Admin/Site.php:459
#: src/Module/Admin/Site.php:457
msgid "Performance"
msgstr ""