Merge pull request #5794 from annando/ap1

ActivityPub support
This commit is contained in:
Hypolite Petovan 2018-10-02 11:24:04 -04:00 committed by GitHub
commit 505350c9fb
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
54 changed files with 4014 additions and 1065 deletions

View file

@ -41,7 +41,7 @@ define('FRIENDICA_PLATFORM', 'Friendica');
define('FRIENDICA_CODENAME', 'The Tazmans Flax-lily');
define('FRIENDICA_VERSION', '2018.12-dev');
define('DFRN_PROTOCOL_VERSION', '2.23');
define('DB_UPDATE_VERSION', 1283);
define('DB_UPDATE_VERSION', 1284);
define('NEW_UPDATE_ROUTINE_VERSION', 1170);
/**

View file

@ -37,12 +37,13 @@
"npm-asset/jgrowl": "^1.4",
"npm-asset/fullcalendar": "^3.0.1",
"npm-asset/cropperjs": "1.2.2",
"npm-asset/imagesloaded": "4.1.4"
"npm-asset/imagesloaded": "4.1.4",
"friendica/json-ld": "^1.0"
},
"repositories": [
{
"type": "vcs",
"url": "https://github.com/pear/Text_Highlighter"
"url": "https://git.friendi.ca/friendica/php-json-ld"
}
],
"autoload": {

92
composer.lock generated
View file

@ -4,7 +4,7 @@
"Read more about it at https://getcomposer.org/doc/01-basic-usage.md#composer-lock-the-lock-file",
"This file is @generated automatically"
],
"content-hash": "5f6a43237dc52758484cd21cd76e8ce6",
"content-hash": "ece71ff50417ed5fcc1a439ea554925c",
"packages": [
{
"name": "asika/simple-console",
@ -241,6 +241,50 @@
],
"time": "2015-08-05T01:03:42+00:00"
},
{
"name": "friendica/json-ld",
"version": "1.0.0",
"source": {
"type": "git",
"url": "https://git.friendi.ca/friendica/php-json-ld",
"reference": "a9ac64daf01cfd97e80c36a5104247d37c0ae5ef"
},
"require": {
"ext-json": "*",
"php": ">=5.4.0"
},
"type": "library",
"autoload": {
"files": [
"jsonld.php"
]
},
"license": [
"BSD-3-Clause"
],
"authors": [
{
"name": "Digital Bazaar, Inc.",
"email": "support@digitalbazaar.com",
"url": "http://digitalbazaar.com/"
},
{
"name": "Friendica Team",
"url": "https://friendi.ca/"
}
],
"description": "A JSON-LD Processor and API implementation in PHP.",
"homepage": "https://git.friendi.ca/friendica/php-json-ld",
"keywords": [
"JSON",
"JSON-LD",
"Linked Data",
"RDF",
"Semantic Web",
"jsonld"
],
"time": "2018-09-28T00:01:12+00:00"
},
{
"name": "fxp/composer-asset-plugin",
"version": "v1.4.2",
@ -2000,6 +2044,52 @@
}
],
"packages-dev": [
{
"name": "digitalbazaar/json-ld",
"version": "0.4.7",
"source": {
"type": "git",
"url": "https://github.com/digitalbazaar/php-json-ld.git",
"reference": "dc1bd23f0ee2efd27ccf636d32d2738dabcee182"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/digitalbazaar/php-json-ld/zipball/dc1bd23f0ee2efd27ccf636d32d2738dabcee182",
"reference": "dc1bd23f0ee2efd27ccf636d32d2738dabcee182",
"shasum": ""
},
"require": {
"ext-json": "*",
"php": ">=5.3.0"
},
"type": "library",
"autoload": {
"files": [
"jsonld.php"
]
},
"notification-url": "https://packagist.org/downloads/",
"license": [
"BSD-3-Clause"
],
"authors": [
{
"name": "Digital Bazaar, Inc.",
"email": "support@digitalbazaar.com"
}
],
"description": "A JSON-LD Processor and API implementation in PHP.",
"homepage": "https://github.com/digitalbazaar/php-json-ld",
"keywords": [
"JSON-LD",
"Linked Data",
"RDF",
"Semantic Web",
"json",
"jsonld"
],
"time": "2016-04-25T04:17:52+00:00"
},
{
"name": "doctrine/instantiator",
"version": "1.0.5",

View file

@ -15,6 +15,34 @@
"name": ["UNIQUE", "name"]
}
},
"apcontact": {
"comment": "ActivityPub compatible contacts - used in the ActivityPub implementation",
"fields": {
"url": {"type": "varbinary(255)", "not null": "1", "primary": "1", "comment": "URL of the contact"},
"uuid": {"type": "varchar(255)", "comment": ""},
"type": {"type": "varchar(20)", "not null": "1", "comment": ""},
"following": {"type": "varchar(255)", "comment": ""},
"followers": {"type": "varchar(255)", "comment": ""},
"inbox": {"type": "varchar(255)", "not null": "1", "comment": ""},
"outbox": {"type": "varchar(255)", "comment": ""},
"sharedinbox": {"type": "varchar(255)", "comment": ""},
"nick": {"type": "varchar(255)", "not null": "1", "default": "", "comment": ""},
"name": {"type": "varchar(255)", "comment": ""},
"about": {"type": "text", "comment": ""},
"photo": {"type": "varchar(255)", "comment": ""},
"addr": {"type": "varchar(255)", "comment": ""},
"alias": {"type": "varchar(255)", "comment": ""},
"pubkey": {"type": "text", "comment": ""},
"baseurl": {"type": "varchar(255)", "comment": "baseurl of the ap contact"},
"updated": {"type": "datetime", "not null": "1", "default": "0001-01-01 00:00:00", "comment": ""}
},
"indexes": {
"PRIMARY": ["url"],
"addr": ["addr(32)"],
"url": ["followers(190)"]
}
},
"attach": {
"comment": "file attachments",
"fields": {
@ -215,7 +243,7 @@
"reply-to-uri": {"type": "varbinary(255)", "not null": "1", "default": "", "comment": "URI to which this item is a reply"},
"conversation-uri": {"type": "varbinary(255)", "not null": "1", "default": "", "comment": "GNU Social conversation URI"},
"conversation-href": {"type": "varbinary(255)", "not null": "1", "default": "", "comment": "GNU Social conversation link"},
"protocol": {"type": "tinyint unsigned", "not null": "1", "default": "0", "comment": "The protocol of the item"},
"protocol": {"type": "tinyint unsigned", "not null": "1", "default": "255", "comment": "The protocol of the item"},
"source": {"type": "mediumtext", "comment": "Original source"},
"received": {"type": "datetime", "not null": "1", "default": "0001-01-01 00:00:00", "comment": "Receiving date"}
},

View file

@ -4612,7 +4612,7 @@ function post_photo_item($hash, $allow_cid, $deny_cid, $allow_gid, $deny_gid, $f
$owner_record = DBA::selectFirst('contact', [], ['uid' => api_user(), 'self' => true]);
$arr = [];
$arr['guid'] = System::createGUID(32);
$arr['guid'] = System::createUUID();
$arr['uid'] = intval(api_user());
$arr['uri'] = $uri;
$arr['parent-uri'] = $uri;

View file

@ -556,7 +556,7 @@ function conversation(App $a, array $items, $mode, $update, $preview = false, $o
if (in_array($mode, ['community', 'contacts'])) {
$writable = true;
} else {
$writable = ($items[0]['uid'] == 0) && in_array($items[0]['network'], [Protocol::OSTATUS, Protocol::DIASPORA, Protocol::DFRN]);
$writable = ($items[0]['uid'] == 0) && in_array($items[0]['network'], [Protocol::ACTIVITYPUB, Protocol::OSTATUS, Protocol::DIASPORA, Protocol::DFRN]);
}
if (!local_user()) {
@ -807,7 +807,7 @@ function conversation_add_children(array $parents, $block_authors, $order, $uid)
foreach ($items as $index => $item) {
if ($item['uid'] == 0) {
$items[$index]['writable'] = in_array($item['network'], [Protocol::OSTATUS, Protocol::DIASPORA, Protocol::DFRN]);
$items[$index]['writable'] = in_array($item['network'], [Protocol::ACTIVITYPUB, Protocol::OSTATUS, Protocol::DIASPORA, Protocol::DFRN]);
}
}
@ -877,7 +877,7 @@ function item_photo_menu($item) {
}
if ((($cid == 0) || ($rel == Contact::FOLLOWER)) &&
in_array($item['network'], [Protocol::DFRN, Protocol::OSTATUS, Protocol::DIASPORA])) {
in_array($item['network'], [Protocol::ACTIVITYPUB, Protocol::DFRN, Protocol::OSTATUS, Protocol::DIASPORA])) {
$menu[L10n::t('Connect/Follow')] = 'follow?url=' . urlencode($item['author-link']);
}
} else {

View file

@ -332,15 +332,21 @@ if ($a->module_loaded) {
$a->page['page_title'] = $a->module;
$placeholder = '';
Addon::callHooks($a->module . '_mod_init', $placeholder);
if ($a->module_class) {
Addon::callHooks($a->module . '_mod_init', $placeholder);
call_user_func([$a->module_class, 'init']);
} else if (function_exists($a->module . '_init')) {
Addon::callHooks($a->module . '_mod_init', $placeholder);
$func = $a->module . '_init';
$func($a);
}
// "rawContent" is especially meant for technical endpoints.
// This endpoint doesn't need any theme initialization or other comparable stuff.
if (!$a->error && $a->module_class) {
call_user_func([$a->module_class, 'rawContent']);
}
if (function_exists(str_replace('-', '_', $a->getCurrentTheme()) . '_init')) {
$func = str_replace('-', '_', $a->getCurrentTheme()) . '_init';
$func($a);

View file

@ -1488,7 +1488,7 @@ function admin_page_site(App $a)
'$community_page_style' => ['community_page_style', L10n::t("Community pages for visitors"), Config::get('system','community_page_style'), L10n::t("Which community pages should be available for visitors. Local users always see both pages."), $community_page_style_choices],
'$max_author_posts_community_page' => ['max_author_posts_community_page', L10n::t("Posts per user on community page"), Config::get('system','max_author_posts_community_page'), L10n::t("The maximum number of posts per user on the community page. \x28Not valid for 'Global Community'\x29")],
'$ostatus_disabled' => ['ostatus_disabled', L10n::t("Enable OStatus support"), !Config::get('system','ostatus_disabled'), L10n::t("Provide built-in OStatus \x28StatusNet, GNU Social etc.\x29 compatibility. All communications in OStatus are public, so privacy warnings will be occasionally displayed.")],
'$ostatus_full_threads' => ['ostatus_full_threads', L10n::t("Only import OStatus threads from our contacts"), Config::get('system','ostatus_full_threads'), L10n::t("Normally we import every content from our OStatus contacts. With this option we only store threads that are started by a contact that is known on our system.")],
'$ostatus_full_threads' => ['ostatus_full_threads', L10n::t("Only import OStatus/ActivityPub threads from our contacts"), Config::get('system','ostatus_full_threads'), L10n::t("Normally we import every content from our OStatus and ActivityPub contacts. With this option we only store threads that are started by a contact that is known on our system.")],
'$ostatus_not_able' => L10n::t("OStatus support can only be enabled if threading is enabled."),
'$diaspora_able' => $diaspora_able,
'$diaspora_not_able' => L10n::t("Diaspora support can't be enabled because Friendica was installed into a sub directory."),

View file

@ -533,7 +533,7 @@ function contacts_content(App $a, $update = 0)
$relation_text = '';
}
if (!in_array($contact['network'], [Protocol::DFRN, Protocol::OSTATUS, Protocol::DIASPORA])) {
if (!in_array($contact['network'], [Protocol::ACTIVITYPUB, Protocol::DFRN, Protocol::OSTATUS, Protocol::DIASPORA])) {
$relation_text = "";
}
@ -555,7 +555,7 @@ function contacts_content(App $a, $update = 0)
}
$lblsuggest = (($contact['network'] === Protocol::DFRN) ? L10n::t('Suggest friends') : '');
$poll_enabled = in_array($contact['network'], [Protocol::DFRN, Protocol::OSTATUS, Protocol::FEED, Protocol::MAIL]);
$poll_enabled = in_array($contact['network'], [Protocol::ACTIVITYPUB, Protocol::DFRN, Protocol::OSTATUS, Protocol::FEED, Protocol::MAIL]);
$nettype = L10n::t('Network type: %s', ContactSelector::networkToName($contact['network'], $contact["url"]));
@ -966,7 +966,7 @@ function contact_conversations(App $a, $contact_id, $update)
$profiledata = Contact::getDetailsByURL($contact["url"]);
if (local_user()) {
if (in_array($profiledata["network"], [Protocol::DFRN, Protocol::DIASPORA, Protocol::OSTATUS])) {
if (in_array($profiledata["network"], [Protocol::ACTIVITYPUB, Protocol::DFRN, Protocol::DIASPORA, Protocol::OSTATUS])) {
$profiledata["remoteconnect"] = System::baseUrl()."/follow?url=".urlencode($profiledata["url"]);
}
}
@ -990,7 +990,7 @@ function contact_posts(App $a, $contact_id)
$profiledata = Contact::getDetailsByURL($contact["url"]);
if (local_user()) {
if (in_array($profiledata["network"], [Protocol::DFRN, Protocol::DIASPORA, Protocol::OSTATUS])) {
if (in_array($profiledata["network"], [Protocol::ACTIVITYPUB, Protocol::DFRN, Protocol::DIASPORA, Protocol::OSTATUS])) {
$profiledata["remoteconnect"] = System::baseUrl()."/follow?url=".urlencode($profiledata["url"]);
}
}
@ -1071,7 +1071,7 @@ function _contact_detail_for_template(array $rr)
*/
function contact_actions($contact)
{
$poll_enabled = in_array($contact['network'], [Protocol::DFRN, Protocol::OSTATUS, Protocol::FEED, Protocol::MAIL]);
$poll_enabled = in_array($contact['network'], [Protocol::ACTIVITYPUB, Protocol::DFRN, Protocol::OSTATUS, Protocol::FEED, Protocol::MAIL]);
$contact_actions = [];
// Provide friend suggestion only for Friendica contacts

View file

@ -28,6 +28,7 @@ use Friendica\Model\Group;
use Friendica\Model\User;
use Friendica\Network\Probe;
use Friendica\Protocol\Diaspora;
use Friendica\Protocol\ActivityPub;
use Friendica\Util\Crypto;
use Friendica\Util\DateTimeFormat;
use Friendica\Util\Network;
@ -335,10 +336,17 @@ function dfrn_confirm_post(App $a, $handsfree = null)
intval($contact_id)
);
} else {
if ($network == Protocol::ACTIVITYPUB) {
ActivityPub::transmitContactAccept($contact['url'], $contact['hub-verify'], $uid);
$pending = true;
} else {
$pending = false;
}
// $network !== Protocol::DFRN
$network = defaults($contact, 'network', Protocol::OSTATUS);
$arr = Probe::uri($contact['url']);
$arr = Probe::uri($contact['url'], $network);
$notify = defaults($contact, 'notify' , $arr['notify']);
$poll = defaults($contact, 'poll' , $arr['poll']);
@ -348,7 +356,7 @@ function dfrn_confirm_post(App $a, $handsfree = null)
$new_relation = $contact['rel'];
$writable = $contact['writable'];
if ($network === Protocol::DIASPORA) {
if (in_array($network, [Protocol::DIASPORA, Protocol::ACTIVITYPUB])) {
if ($duplex) {
$new_relation = Contact::FRIEND;
} else {
@ -362,30 +370,12 @@ function dfrn_confirm_post(App $a, $handsfree = null)
DBA::delete('intro', ['id' => $intro_id]);
$r = q("UPDATE `contact` SET `name-date` = '%s',
`uri-date` = '%s',
`addr` = '%s',
`notify` = '%s',
`poll` = '%s',
`blocked` = 0,
`pending` = 0,
`network` = '%s',
`writable` = %d,
`hidden` = %d,
`rel` = %d
WHERE `id` = %d
",
DBA::escape(DateTimeFormat::utcNow()),
DBA::escape(DateTimeFormat::utcNow()),
DBA::escape($addr),
DBA::escape($notify),
DBA::escape($poll),
DBA::escape($network),
intval($writable),
intval($hidden),
intval($new_relation),
intval($contact_id)
);
$fields = ['name-date' => DateTimeFormat::utcNow(),
'uri-date' => DateTimeFormat::utcNow(), 'addr' => $addr,
'notify' => $notify, 'poll' => $poll, 'blocked' => false,
'pending' => $pending, 'network' => $network,
'writable' => $writable, 'hidden' => $hidden, 'rel' => $new_relation];
DBA::update('contact', $fields, ['id' => $contact_id]);
}
if (!DBA::isResult($r)) {
@ -403,6 +393,10 @@ function dfrn_confirm_post(App $a, $handsfree = null)
Group::addMember(User::getDefaultGroup($uid, $contact["network"]), $contact['id']);
if ($network == Protocol::ACTIVITYPUB && $duplex) {
ActivityPub::transmitActivity('Follow', $contact['url'], $uid);
}
// Let's send our user to the contact editor in case they want to
// do anything special with this new friend.
if ($handsfree === null) {

View file

@ -54,7 +54,7 @@ function dirfind_content(App $a, $prefix = "") {
if ((valid_email($search) && Network::isEmailDomainValid($search)) ||
(substr(normalise_link($search), 0, 7) == "http://")) {
$user_data = Probe::uri($search);
$discover_user = (in_array($user_data["network"], [Protocol::DFRN, Protocol::OSTATUS, Protocol::DIASPORA]));
$discover_user = (in_array($user_data["network"], [Protocol::ACTIVITYPUB, Protocol::DFRN, Protocol::OSTATUS, Protocol::DIASPORA]));
}
}

View file

@ -17,6 +17,7 @@ use Friendica\Model\Group;
use Friendica\Model\Item;
use Friendica\Model\Profile;
use Friendica\Protocol\DFRN;
use Friendica\Protocol\ActivityPub;
function display_init(App $a)
{
@ -43,7 +44,7 @@ function display_init(App $a)
$item = null;
$fields = ['id', 'parent', 'author-id', 'body', 'uid'];
$fields = ['id', 'parent', 'author-id', 'body', 'uid', 'guid'];
// If there is only one parameter, then check if this parameter could be a guid
if ($a->argc == 2) {
@ -76,6 +77,10 @@ function display_init(App $a)
displayShowFeed($item["id"], false);
}
if (ActivityPub::isRequest()) {
goaway(str_replace('display/', 'object/', $a->query_string));
}
if ($item["id"] != $item["parent"]) {
$item = Item::selectFirstForUser(local_user(), $fields, ['id' => $item["parent"]]);
}

View file

@ -159,7 +159,7 @@ function item_post(App $a) {
}
// Allow commenting if it is an answer to a public post
$allow_comment = local_user() && ($profile_uid == 0) && $parent && in_array($parent_item['network'], [Protocol::OSTATUS, Protocol::DIASPORA, Protocol::DFRN]);
$allow_comment = local_user() && ($profile_uid == 0) && $parent && in_array($parent_item['network'], [Protocol::ACTIVITYPUB, Protocol::OSTATUS, Protocol::DIASPORA, Protocol::DFRN]);
// Now check that valid personal details have been provided
if (!can_write_wall($profile_uid) && !$allow_comment) {
@ -240,7 +240,7 @@ function item_post(App $a) {
$emailcc = notags(trim(defaults($_REQUEST, 'emailcc' , '')));
$body = escape_tags(trim(defaults($_REQUEST, 'body' , '')));
$network = notags(trim(defaults($_REQUEST, 'network' , Protocol::DFRN)));
$guid = System::createGUID(32);
$guid = System::createUUID();
$postopts = defaults($_REQUEST, 'postopts', '');
@ -343,20 +343,11 @@ function item_post(App $a) {
$tags = get_tags($body);
// Add a tag if the parent contact is from OStatus (This will notify them during delivery)
if ($parent) {
if ($thr_parent_contact['network'] == Protocol::OSTATUS) {
$contact = '@[url=' . $thr_parent_contact['url'] . ']' . $thr_parent_contact['nick'] . '[/url]';
if (!stripos(implode($tags), '[url=' . $thr_parent_contact['url'] . ']')) {
$tags[] = $contact;
}
}
if ($parent_contact['network'] == Protocol::OSTATUS) {
$contact = '@[url=' . $parent_contact['url'] . ']' . $parent_contact['nick'] . '[/url]';
if (!stripos(implode($tags), '[url=' . $parent_contact['url'] . ']')) {
$tags[] = $contact;
}
// Add a tag if the parent contact is from ActivityPub or OStatus (This will notify them)
if ($parent && in_array($thr_parent_contact['network'], [Protocol::OSTATUS, Protocol::ACTIVITYPUB])) {
$contact = '@[url=' . $thr_parent_contact['url'] . ']' . $thr_parent_contact['nick'] . '[/url]';
if (!stripos(implode($tags), '[url=' . $thr_parent_contact['url'] . ']')) {
$tags[] = $contact;
}
}
@ -1026,8 +1017,7 @@ function handle_tag(App $a, &$body, &$inform, &$str_tags, $profile_uid, $tag, $n
$alias = $contact["alias"];
$newname = $contact["nick"];
if (($newname == "") || (($contact["network"] != Protocol::OSTATUS) && ($contact["network"] != Protocol::TWITTER)
&& ($contact["network"] != Protocol::STATUSNET))) {
if (($newname == "") || !in_array($contact["network"], [Protocol::ACTIVITYPUB, Protocol::OSTATUS, Protocol::TWITTER, Protocol::STATUSNET])) {
$newname = $contact["name"];
}
}

View file

@ -472,7 +472,7 @@ function photos_post(App $a)
$uri = Item::newURI($page_owner_uid);
$arr = [];
$arr['guid'] = System::createGUID(32);
$arr['guid'] = System::createUUID();
$arr['uid'] = $page_owner_uid;
$arr['uri'] = $uri;
$arr['parent-uri'] = $uri;
@ -651,7 +651,7 @@ function photos_post(App $a)
$uri = Item::newURI($page_owner_uid);
$arr = [];
$arr['guid'] = System::createGUID(32);
$arr['guid'] = System::createUUID();
$arr['uid'] = $page_owner_uid;
$arr['uri'] = $uri;
$arr['parent-uri'] = $uri;
@ -889,7 +889,7 @@ function photos_post(App $a)
$arr['coord'] = $lat . ' ' . $lon;
}
$arr['guid'] = System::createGUID(32);
$arr['guid'] = System::createUUID();
$arr['uid'] = $page_owner_uid;
$arr['uri'] = $uri;
$arr['parent-uri'] = $uri;

View file

@ -97,7 +97,7 @@ function poke_init(App $a)
$arr = [];
$arr['guid'] = System::createGUID(32);
$arr['guid'] = System::createUUID();
$arr['uid'] = $uid;
$arr['uri'] = $uri;
$arr['parent-uri'] = (!empty($parent_uri) ? $parent_uri : $uri);

View file

@ -20,6 +20,7 @@ use Friendica\Model\Profile;
use Friendica\Module\Login;
use Friendica\Protocol\DFRN;
use Friendica\Util\DateTimeFormat;
use Friendica\Protocol\ActivityPub;
function profile_init(App $a)
{
@ -49,6 +50,16 @@ function profile_init(App $a)
DFRN::autoRedir($a, $which);
}
if (ActivityPub::isRequest()) {
$user = DBA::selectFirst('user', ['uid'], ['nickname' => $which]);
if (DBA::isResult($user)) {
$data = ActivityPub::profile($user['uid']);
echo json_encode($data);
header('Content-Type: application/activity+json');
exit();
}
}
Profile::load($a, $which, $profile);
$blocked = !local_user() && !remote_user() && Config::get('system', 'block_public');

View file

@ -108,7 +108,7 @@ EOT;
$arr = [];
$arr['guid'] = System::createGUID(32);
$arr['guid'] = System::createUUID();
$arr['uri'] = $uri;
$arr['uid'] = $owner_uid;
$arr['contact-id'] = $contact['id'];

View file

@ -115,7 +115,7 @@ EOT;
$arr = [];
$arr['guid'] = System::createGUID(32);
$arr['guid'] = System::createUUID();
$arr['uri'] = $uri;
$arr['uid'] = $owner_uid;
$arr['contact-id'] = $contact['id'];

View file

@ -80,6 +80,7 @@ function xrd_json($a, $uri, $alias, $profile_url, $r)
['rel' => NAMESPACE_DFRN, 'href' => $profile_url],
['rel' => NAMESPACE_FEED, 'type' => 'application/atom+xml', 'href' => System::baseUrl().'/dfrn_poll/'.$r['nickname']],
['rel' => 'http://webfinger.net/rel/profile-page', 'type' => 'text/html', 'href' => $profile_url],
['rel' => 'self', 'type' => 'application/activity+json', 'href' => $profile_url],
['rel' => 'http://microformats.org/profile/hcard', 'type' => 'text/html', 'href' => System::baseUrl().'/hcard/'.$r['nickname']],
['rel' => NAMESPACE_POCO, 'href' => System::baseUrl().'/poco/'.$r['nickname']],
['rel' => 'http://webfinger.net/rel/avatar', 'type' => 'image/jpeg', 'href' => System::baseUrl().'/photo/profile/'.$r['uid'].'.jpg'],
@ -92,6 +93,7 @@ function xrd_json($a, $uri, $alias, $profile_url, $r)
['rel' => 'http://purl.org/openwebauth/v1', 'type' => 'application/x-dfrn+json', 'href' => System::baseUrl().'/owa']
]
];
echo json_encode($json);
killme();
}

View file

@ -21,7 +21,16 @@ abstract class BaseModule extends BaseObject
*/
public static function init()
{
}
/**
* @brief Module GET method to display raw content from technical endpoints
*
* Extend this method if the module is supposed to return communication data,
* e.g. from protocol implementations.
*/
public static function rawContent()
{
}
/**

View file

@ -75,21 +75,22 @@ class ContactSelector
public static function networkToName($s, $profile = "")
{
$nets = [
Protocol::DFRN => L10n::t('Friendica'),
Protocol::OSTATUS => L10n::t('OStatus'),
Protocol::FEED => L10n::t('RSS/Atom'),
Protocol::MAIL => L10n::t('Email'),
Protocol::DIASPORA => L10n::t('Diaspora'),
Protocol::ZOT => L10n::t('Zot!'),
Protocol::LINKEDIN => L10n::t('LinkedIn'),
Protocol::XMPP => L10n::t('XMPP/IM'),
Protocol::MYSPACE => L10n::t('MySpace'),
Protocol::GPLUS => L10n::t('Google+'),
Protocol::PUMPIO => L10n::t('pump.io'),
Protocol::TWITTER => L10n::t('Twitter'),
Protocol::DIASPORA2 => L10n::t('Diaspora Connector'),
Protocol::STATUSNET => L10n::t('GNU Social Connector'),
Protocol::PNUT => L10n::t('pnut'),
Protocol::DFRN => L10n::t('Friendica'),
Protocol::OSTATUS => L10n::t('OStatus'),
Protocol::FEED => L10n::t('RSS/Atom'),
Protocol::MAIL => L10n::t('Email'),
Protocol::DIASPORA => L10n::t('Diaspora'),
Protocol::ZOT => L10n::t('Zot!'),
Protocol::LINKEDIN => L10n::t('LinkedIn'),
Protocol::XMPP => L10n::t('XMPP/IM'),
Protocol::MYSPACE => L10n::t('MySpace'),
Protocol::GPLUS => L10n::t('Google+'),
Protocol::PUMPIO => L10n::t('pump.io'),
Protocol::TWITTER => L10n::t('Twitter'),
Protocol::DIASPORA2 => L10n::t('Diaspora Connector'),
Protocol::STATUSNET => L10n::t('GNU Social Connector'),
Protocol::ACTIVITYPUB => L10n::t('ActivityPub'),
Protocol::PNUT => L10n::t('pnut'),
];
Addon::callHooks('network_to_name', $nets);
@ -99,13 +100,17 @@ class ContactSelector
$networkname = str_replace($search, $replace, $s);
if ((in_array($s, [Protocol::DFRN, Protocol::DIASPORA, Protocol::OSTATUS])) && ($profile != "")) {
if ((in_array($s, [Protocol::ACTIVITYPUB, Protocol::DFRN, Protocol::DIASPORA, Protocol::OSTATUS])) && ($profile != "")) {
$r = DBA::fetchFirst("SELECT `gserver`.`platform` FROM `gcontact`
INNER JOIN `gserver` ON `gserver`.`nurl` = `gcontact`.`server_url`
WHERE `gcontact`.`nurl` = ? AND `platform` != ''", normalise_link($profile));
if (DBA::isResult($r)) {
$networkname = $r['platform'];
if ($s == Protocol::ACTIVITYPUB) {
$networkname .= ' (AP)';
}
}
}

View file

@ -142,10 +142,7 @@ class Widget
$nets = array();
while ($rr = DBA::fetch($r)) {
/// @TODO If 'network' is not there, this triggers an E_NOTICE
if ($rr['network']) {
$nets[] = array('ref' => $rr['network'], 'name' => ContactSelector::networkToName($rr['network']), 'selected' => (($selected == $rr['network']) ? 'selected' : '' ));
}
$nets[] = array('ref' => $rr['network'], 'name' => ContactSelector::networkToName($rr['network']), 'selected' => (($selected == $rr['network']) ? 'selected' : '' ));
}
DBA::close($r);

View file

@ -161,6 +161,18 @@ class System extends BaseObject
killme();
}
/**
* Generates a random string in the UUID format
*
* @param bool|string $prefix A given prefix (default is empty)
* @return string a generated UUID
*/
public static function createUUID($prefix = '')
{
$guid = System::createGUID(32, $prefix);
return substr($guid, 0, 8). '-' . substr($guid, 8, 4) . '-' . substr($guid, 12, 4) . '-' . substr($guid, 16, 4) . '-' . substr($guid, 20, 12);
}
/**
* Generates a GUID with the given parameters
*

175
src/Model/APContact.php Normal file
View file

@ -0,0 +1,175 @@
<?php
/**
* @file src/Model/APContact.php
*/
namespace Friendica\Model;
use Friendica\BaseObject;
use Friendica\Database\DBA;
use Friendica\Protocol\ActivityPub;
use Friendica\Util\Network;
use Friendica\Util\JsonLD;
use Friendica\Util\DateTimeFormat;
require_once 'boot.php';
class APContact extends BaseObject
{
/**
* Resolves the profile url from the address by using webfinger
*
* @param string $addr profile address (user@domain.tld)
* @return string url
*/
private static function addrToUrl($addr)
{
$addr_parts = explode('@', $addr);
if (count($addr_parts) != 2) {
return false;
}
$webfinger = 'https://' . $addr_parts[1] . '/.well-known/webfinger?resource=acct:' . urlencode($addr);
$ret = Network::curl($webfinger, false, $redirects, ['accept_content' => 'application/jrd+json,application/json']);
if (!$ret['success'] || empty($ret['body'])) {
return false;
}
$data = json_decode($ret['body'], true);
if (empty($data['links'])) {
return false;
}
foreach ($data['links'] as $link) {
if (empty($link['href']) || empty($link['rel']) || empty($link['type'])) {
continue;
}
if (($link['rel'] == 'self') && ($link['type'] == 'application/activity+json')) {
return $link['href'];
}
}
return false;
}
/**
* Fetches a profile from a given url
*
* @param string $url profile url
* @param boolean $update true = always update, false = never update, null = update when not found
* @return array profile array
*/
public static function getByURL($url, $update = null)
{
if (empty($url)) {
return false;
}
if (empty($update)) {
$apcontact = DBA::selectFirst('apcontact', [], ['url' => $url]);
if (DBA::isResult($apcontact)) {
return $apcontact;
}
$apcontact = DBA::selectFirst('apcontact', [], ['alias' => $url]);
if (DBA::isResult($apcontact)) {
return $apcontact;
}
$apcontact = DBA::selectFirst('apcontact', [], ['addr' => $url]);
if (DBA::isResult($apcontact)) {
return $apcontact;
}
if (!is_null($update)) {
return false;
}
}
if (empty(parse_url($url, PHP_URL_SCHEME))) {
$url = self::addrToUrl($url);
if (empty($url)) {
return false;
}
}
$data = ActivityPub::fetchContent($url);
if (empty($data) || empty($data['id']) || empty($data['inbox'])) {
return false;
}
$apcontact = [];
$apcontact['url'] = $data['id'];
$apcontact['uuid'] = defaults($data, 'diaspora:guid', null);
$apcontact['type'] = defaults($data, 'type', null);
$apcontact['following'] = defaults($data, 'following', null);
$apcontact['followers'] = defaults($data, 'followers', null);
$apcontact['inbox'] = defaults($data, 'inbox', null);
$apcontact['outbox'] = defaults($data, 'outbox', null);
$apcontact['sharedinbox'] = JsonLD::fetchElement($data, 'endpoints', 'sharedInbox');
$apcontact['nick'] = defaults($data, 'preferredUsername', null);
$apcontact['name'] = defaults($data, 'name', $apcontact['nick']);
$apcontact['about'] = defaults($data, 'summary', '');
$apcontact['photo'] = JsonLD::fetchElement($data, 'icon', 'url');
$apcontact['alias'] = JsonLD::fetchElement($data, 'url', 'href');
$parts = parse_url($apcontact['url']);
unset($parts['scheme']);
unset($parts['path']);
$apcontact['addr'] = $apcontact['nick'] . '@' . str_replace('//', '', Network::unparseURL($parts));
$apcontact['pubkey'] = trim(JsonLD::fetchElement($data, 'publicKey', 'publicKeyPem'));
// To-Do
// manuallyApprovesFollowers
// Unhandled
// @context, tag, attachment, image, nomadicLocations, signature, following, followers, featured, movedTo, liked
// Unhandled from Misskey
// sharedInbox, isCat
// Unhandled from Kroeg
// kroeg:blocks, updated
// Check if the address is resolvable
if (self::addrToUrl($apcontact['addr']) == $apcontact['url']) {
$parts = parse_url($apcontact['url']);
unset($parts['path']);
$apcontact['baseurl'] = Network::unparseURL($parts);
} else {
$apcontact['addr'] = null;
$apcontact['baseurl'] = null;
}
if ($apcontact['url'] == $apcontact['alias']) {
$apcontact['alias'] = null;
}
$apcontact['updated'] = DateTimeFormat::utcNow();
DBA::update('apcontact', $apcontact, ['url' => $url], true);
// Update some data in the contact table with various ways to catch them all
$contact_fields = ['name' => $apcontact['name'], 'about' => $apcontact['about']];
DBA::update('contact', $contact_fields, ['nurl' => normalise_link($url)]);
$contacts = DBA::select('contact', ['uid', 'id'], ['nurl' => normalise_link($url)]);
while ($contact = DBA::fetch($contacts)) {
Contact::updateAvatar($apcontact['photo'], $contact['uid'], $contact['id']);
}
DBA::close($contacts);
// Update the gcontact table
DBA::update('gcontact', $contact_fields, ['nurl' => normalise_link($url)]);
logger('Updated profile for ' . $url, LOGGER_DEBUG);
return $apcontact;
}
}

View file

@ -16,6 +16,7 @@ use Friendica\Database\DBA;
use Friendica\Model\Profile;
use Friendica\Network\Probe;
use Friendica\Object\Image;
use Friendica\Protocol\ActivityPub;
use Friendica\Protocol\Diaspora;
use Friendica\Protocol\DFRN;
use Friendica\Protocol\OStatus;
@ -555,6 +556,12 @@ class Contact extends BaseObject
}
} elseif ($contact['network'] == Protocol::DIASPORA) {
Diaspora::sendUnshare($user, $contact);
} elseif ($contact['network'] == Protocol::ACTIVITYPUB) {
ActivityPub::transmitContactUndo($contact['url'], $user['uid']);
if ($dissolve) {
ActivityPub::transmitContactReject($contact['url'], $contact['hub-verify'], $user['uid']);
}
}
}
@ -775,7 +782,7 @@ class Contact extends BaseObject
}
if ((empty($profile["addr"]) || empty($profile["name"])) && (defaults($profile, "gid", 0) != 0)
&& in_array($profile["network"], [Protocol::DFRN, Protocol::DIASPORA, Protocol::OSTATUS])
&& in_array($profile["network"], [Protocol::ACTIVITYPUB, Protocol::DFRN, Protocol::DIASPORA, Protocol::OSTATUS])
) {
Worker::add(PRIORITY_LOW, "UpdateGContact", $profile["gid"]);
}
@ -1054,7 +1061,6 @@ class Contact extends BaseObject
if (!x($contact, 'avatar')) {
$update_contact = true;
}
if (!$update_contact || $no_update) {
return $contact_id;
}
@ -1088,7 +1094,7 @@ class Contact extends BaseObject
}
// Last try in gcontact for unsupported networks
if (!in_array($data["network"], [Protocol::DFRN, Protocol::OSTATUS, Protocol::DIASPORA, Protocol::PUMPIO, Protocol::MAIL, Protocol::FEED])) {
if (!in_array($data["network"], [Protocol::ACTIVITYPUB, Protocol::DFRN, Protocol::OSTATUS, Protocol::DIASPORA, Protocol::PUMPIO, Protocol::MAIL, Protocol::FEED])) {
if ($uid != 0) {
return 0;
}
@ -1316,33 +1322,27 @@ class Contact extends BaseObject
require_once 'include/conversation.php';
// There are no posts with "uid = 0" with connector networks
// This speeds up the query a lot
$r = q("SELECT `network`, `id` AS `author-id`, `contact-type` FROM `contact`
WHERE `contact`.`nurl` = '%s' AND `contact`.`uid` = 0",
DBA::escape(normalise_link($contact_url))
);
$cid = Self::getIdForURL($contact_url);
if (!DBA::isResult($r)) {
$contact = DBA::selectFirst('contact', ['contact-type', 'network'], ['id' => $cid]);
if (!DBA::isResult($contact)) {
return '';
}
if (in_array($r[0]["network"], [Protocol::DFRN, Protocol::DIASPORA, Protocol::OSTATUS, ""])) {
if (in_array($contact["network"], [Protocol::ACTIVITYPUB, Protocol::DFRN, Protocol::DIASPORA, Protocol::OSTATUS, ""])) {
$sql = "(`item`.`uid` = 0 OR (`item`.`uid` = ? AND NOT `item`.`global`))";
} else {
$sql = "`item`.`uid` = ?";
}
$author_id = intval($r[0]["author-id"]);
$contact = ($r[0]["contact-type"] == self::ACCOUNT_TYPE_COMMUNITY ? 'owner-id' : 'author-id');
$contact_field = ($contact["contact-type"] == self::ACCOUNT_TYPE_COMMUNITY ? 'owner-id' : 'author-id');
if ($thread_mode) {
$condition = ["`$contact` = ? AND `gravity` = ? AND " . $sql,
$author_id, GRAVITY_PARENT, local_user()];
$condition = ["`$contact_field` = ? AND `gravity` = ? AND " . $sql,
$cid, GRAVITY_PARENT, local_user()];
} else {
$condition = ["`$contact` = ? AND `gravity` IN (?, ?) AND " . $sql,
$author_id, GRAVITY_PARENT, GRAVITY_COMMENT, local_user()];
$condition = ["`$contact_field` = ? AND `gravity` IN (?, ?) AND " . $sql,
$cid, GRAVITY_PARENT, GRAVITY_COMMENT, local_user()];
}
$params = ['order' => ['created' => true],
@ -1495,10 +1495,11 @@ class Contact extends BaseObject
}
/**
* @param integer $id contact id
* @param integer $id contact id
* @param string $network Optional network we are probing for
* @return boolean
*/
public static function updateFromProbe($id)
public static function updateFromProbe($id, $network = '')
{
/*
Warning: Never ever fetch the public key via Probe::uri and write it into the contacts.
@ -1511,10 +1512,10 @@ class Contact extends BaseObject
return false;
}
$ret = Probe::uri($contact["url"]);
$ret = Probe::uri($contact["url"], $network);
// If Probe::uri fails the network code will be different
if ($ret["network"] != $contact["network"]) {
if (($ret["network"] != $contact["network"]) && ($ret["network"] != $network)) {
return false;
}
@ -1537,14 +1538,15 @@ class Contact extends BaseObject
DBA::update(
'contact', [
'url' => $ret['url'],
'nurl' => normalise_link($ret['url']),
'addr' => $ret['addr'],
'alias' => $ret['alias'],
'batch' => $ret['batch'],
'notify' => $ret['notify'],
'poll' => $ret['poll'],
'poco' => $ret['poco']
'url' => $ret['url'],
'nurl' => normalise_link($ret['url']),
'network' => $ret['network'],
'addr' => $ret['addr'],
'alias' => $ret['alias'],
'batch' => $ret['batch'],
'notify' => $ret['notify'],
'poll' => $ret['poll'],
'poco' => $ret['poco']
],
['id' => $id]
);
@ -1686,7 +1688,7 @@ class Contact extends BaseObject
$hidden = (($ret['network'] === Protocol::MAIL) ? 1 : 0);
if (in_array($ret['network'], [Protocol::MAIL, Protocol::DIASPORA])) {
if (in_array($ret['network'], [Protocol::MAIL, Protocol::DIASPORA, Protocol::ACTIVITYPUB])) {
$writeable = 1;
}
@ -1766,6 +1768,9 @@ class Contact extends BaseObject
} elseif ($contact['network'] == Protocol::DIASPORA) {
$ret = Diaspora::sendShare($a->user, $contact);
logger('share returns: ' . $ret);
} elseif ($contact['network'] == Protocol::ACTIVITYPUB) {
$ret = ActivityPub::transmitActivity('Follow', $contact['url'], $uid);
logger('Follow returns: ' . $ret);
}
}
@ -1814,7 +1819,7 @@ class Contact extends BaseObject
return $contact;
}
public static function addRelationship($importer, $contact, $datarray, $item, $sharing = false) {
public static function addRelationship($importer, $contact, $datarray, $item = '', $sharing = false) {
// Should always be set
if (empty($datarray['author-id'])) {
return;
@ -1827,7 +1832,7 @@ class Contact extends BaseObject
return;
}
$url = $pub_contact['url'];
$url = defaults($datarray, 'author-link', $pub_contact['url']);
$name = $pub_contact['name'];
$photo = $pub_contact['photo'];
$nick = $pub_contact['nick'];
@ -1839,13 +1844,17 @@ class Contact extends BaseObject
DBA::update('contact', ['rel' => self::FRIEND, 'writable' => true],
['id' => $contact['id'], 'uid' => $importer['uid']]);
}
if ($contact['network'] == Protocol::ACTIVITYPUB) {
ActivityPub::transmitContactAccept($contact['url'], $contact['hub-verify'], $importer['uid']);
}
// send email notification to owner?
} else {
if (DBA::exists('contact', ['nurl' => normalise_link($url), 'uid' => $importer['uid'], 'pending' => true])) {
logger('ignoring duplicated connection request from pending contact ' . $url);
return;
}
// create contact record
q("INSERT INTO `contact` (`uid`, `created`, `url`, `nurl`, `name`, `nick`, `photo`, `network`, `rel`,
`blocked`, `readonly`, `pending`, `writable`)

View file

@ -17,13 +17,14 @@ class Conversation
* These constants represent the parcel format used to transport a conversation independently of the message protocol.
* It currently is stored in the "protocol" field for legacy reasons.
*/
const PARCEL_UNKNOWN = 0;
const PARCEL_ACTIVITYPUB = 0;
const PARCEL_DFRN = 1;
const PARCEL_DIASPORA = 2;
const PARCEL_SALMON = 3;
const PARCEL_FEED = 4; // Deprecated
const PARCEL_SPLIT_CONVERSATION = 6;
const PARCEL_TWITTER = 67;
const PARCEL_UNKNOWN = 255;
/**
* @brief Store the conversation data
@ -34,7 +35,7 @@ class Conversation
public static function insert(array $arr)
{
if (in_array(defaults($arr, 'network', Protocol::PHANTOM),
[Protocol::DFRN, Protocol::DIASPORA, Protocol::OSTATUS, Protocol::TWITTER]) && !empty($arr['uri'])) {
[Protocol::ACTIVITYPUB, Protocol::DFRN, Protocol::DIASPORA, Protocol::OSTATUS, Protocol::TWITTER]) && !empty($arr['uri'])) {
$conversation = ['item-uri' => $arr['uri'], 'received' => DateTimeFormat::utcNow()];
if (isset($arr['parent-uri']) && ($arr['parent-uri'] != $arr['uri'])) {
@ -70,7 +71,8 @@ class Conversation
unset($old_conv['source']);
}
// Update structure data all the time but the source only when its from a better protocol.
if (isset($conversation['protocol']) && isset($conversation['source']) && ($old_conv['protocol'] < $conversation['protocol']) && ($old_conv['protocol'] != 0)) {
if (empty($conversation['source']) || (!empty($old_conv['source']) &&
($old_conv['protocol'] < defaults($conversation, 'protocol', PARCEL_UNKNOWN)))) {
unset($conversation['protocol']);
unset($conversation['source']);
}

View file

@ -314,7 +314,7 @@ class Event extends BaseObject
Addon::callHooks('event_updated', $event['id']);
} else {
$event['guid'] = defaults($arr, 'guid', System::createGUID(32));
$event['guid'] = defaults($arr, 'guid', System::createUUID());
// New event. Store it.
DBA::insert('event', $event);

View file

@ -176,7 +176,7 @@ class Item extends BaseObject
// We can always comment on posts from these networks
if (array_key_exists('writable', $row) &&
in_array($row['internal-network'], [Protocol::DFRN, Protocol::DIASPORA, Protocol::OSTATUS])) {
in_array($row['internal-network'], [Protocol::ACTIVITYPUB, Protocol::DFRN, Protocol::DIASPORA, Protocol::OSTATUS])) {
$row['writable'] = true;
}
@ -1081,9 +1081,8 @@ class Item extends BaseObject
DBA::delete('item-delivery-data', ['iid' => $item['id']]);
if (!empty($item['iaid']) && !self::exists(['iaid' => $item['iaid'], 'deleted' => false])) {
DBA::delete('item-activity', ['id' => $item['iaid']], ['cascade' => false]);
}
// We don't delete the item-activity here, since we need some of the data for ActivityPub
if (!empty($item['icid']) && !self::exists(['icid' => $item['icid'], 'deleted' => false])) {
DBA::delete('item-content', ['id' => $item['icid']], ['cascade' => false]);
}
@ -1205,7 +1204,7 @@ class Item extends BaseObject
} elseif (!empty($item[