From d208afe8990c0289109e0e048574dbc380cb04e3 Mon Sep 17 00:00:00 2001 From: Sascha Nitsch Date: Fri, 2 Aug 2024 20:13:19 +0200 Subject: [PATCH] incomplete support for fetching data from user outbox (only article yet) --- contentnation.ini | 2 - lang/federator/de/article.inc | 12 + lang/federator/en/article.inc | 12 + php/federator/api/fedusers.php | 50 +- .../api/fedusers/fedusersinterface.php | 28 + php/federator/api/fedusers/outbox.php | 95 ++ php/federator/cache/cache.php | 9 + php/federator/connector/connector.php | 11 + .../data/activitypub/common/activity.php | 89 ++ .../data/activitypub/common/apobject.php | 951 ++++++++++++++++++ .../data/activitypub/common/article.php | 27 + .../data/activitypub/common/collection.php | 68 ++ .../data/activitypub/common/create.php | 43 + .../data/activitypub/common/image.php | 62 ++ .../data/activitypub/common/note.php | 95 ++ .../activitypub/common/orderedcollection.php | 91 ++ .../common/orderedcollectionpage.php | 82 ++ .../data/activitypub/common/outbox.php | 50 + php/federator/data/activitypub/common/tag.php | 34 + php/federator/data/activitypub/factory.php | 108 ++ php/federator/dio/posts.php | 59 ++ php/federator/dio/user.php | 9 +- php/federator/language.php | 4 +- php/federator/maintenance.php | 4 + plugins/federator/contentnation.php | 107 +- plugins/federator/dummyconnector.php | 13 + plugins/federator/rediscache.php | 27 + templates/federator/webfinger_acct.json | 4 +- 28 files changed, 2125 insertions(+), 21 deletions(-) delete mode 100644 contentnation.ini create mode 100644 lang/federator/de/article.inc create mode 100644 lang/federator/en/article.inc create mode 100644 php/federator/api/fedusers/fedusersinterface.php create mode 100644 php/federator/api/fedusers/outbox.php create mode 100644 php/federator/data/activitypub/common/activity.php create mode 100644 php/federator/data/activitypub/common/apobject.php create mode 100644 php/federator/data/activitypub/common/article.php create mode 100644 php/federator/data/activitypub/common/collection.php create mode 100644 php/federator/data/activitypub/common/create.php create mode 100644 php/federator/data/activitypub/common/image.php create mode 100644 php/federator/data/activitypub/common/note.php create mode 100644 php/federator/data/activitypub/common/orderedcollection.php create mode 100644 php/federator/data/activitypub/common/orderedcollectionpage.php create mode 100644 php/federator/data/activitypub/common/outbox.php create mode 100644 php/federator/data/activitypub/common/tag.php create mode 100644 php/federator/data/activitypub/factory.php create mode 100644 php/federator/dio/posts.php diff --git a/contentnation.ini b/contentnation.ini deleted file mode 100644 index 140802f..0000000 --- a/contentnation.ini +++ /dev/null @@ -1,2 +0,0 @@ -[contentnation] -service-uri = http://local.contentnation.net diff --git a/lang/federator/de/article.inc b/lang/federator/de/article.inc new file mode 100644 index 0000000..1b81789 --- /dev/null +++ b/lang/federator/de/article.inc @@ -0,0 +1,12 @@ + 'Artikelbild', + 'newarticle' => 'Ein neuer Artikel wurde veröffentlicht', +]; diff --git a/lang/federator/en/article.inc b/lang/federator/en/article.inc new file mode 100644 index 0000000..6b63d4b --- /dev/null +++ b/lang/federator/en/article.inc @@ -0,0 +1,12 @@ + 'article image', + 'newarticle' => 'A new Artikel was published', +]; diff --git a/php/federator/api/fedusers.php b/php/federator/api/fedusers.php index e94517b..77958e7 100644 --- a/php/federator/api/fedusers.php +++ b/php/federator/api/fedusers.php @@ -6,7 +6,7 @@ * @author Sascha Nitsch (grumpydeveloper) **/ - namespace Federator\Api; +namespace Federator\Api; /** * /@username or /users/ handlers @@ -48,14 +48,50 @@ class FedUsers implements APIInterface public function exec($paths, $user) { $method = $_SERVER["REQUEST_METHOD"]; - switch ($method) { - case 'GET': - switch (sizeof($paths)) { - case 2: - // /users/username or /@username - return $this->returnUserProfile($paths[1]); + $handler = null; + switch (sizeof($paths)) { + case 2: + if ($method === 'GET') { + // /users/username or /@username + return $this->returnUserProfile($paths[1]); } break; + case 3: + // /users/username/(inbox|outbox|following|followers) + switch ($paths[2]) { + case 'following': + // $handler = new FedUsers\Following(); + break; + case 'followers': + // $handler = new FedUsers\Followers(); + break; + case 'inbox': + // $handler = new FedUsers\Inbox(); + break; + case 'outbox': + $handler = new FedUsers\Outbox($this->main); + break; + } + break; + case 4: + // /users/username/collections/(features|tags) + // not yet implemented + break; + } + if ($handler !== null) { + $ret = false; + switch ($method) { + case 'GET': + $ret = $handler->get($paths[1]); + break; + case 'POST': + $ret = $handler->post($paths[1]); + break; + } + if ($ret !== false) { + $this->response = $ret; + return true; + } } $this->main->setResponseCode(404); return false; diff --git a/php/federator/api/fedusers/fedusersinterface.php b/php/federator/api/fedusers/fedusersinterface.php new file mode 100644 index 0000000..1782323 --- /dev/null +++ b/php/federator/api/fedusers/fedusersinterface.php @@ -0,0 +1,28 @@ +main = $main; + } + + /** + * handle get call + * + * @param string $_user user to fetch outbox for + * @return string|false response + */ + public function get($_user) + { + $dbh = $this->main->getDatabase(); + $cache = $this->main->getCache(); + $connector = $this->main->getConnector(); + // get user + $user = \Federator\DIO\User::getUserByName( + $dbh, + $_user, + $connector, + $cache + ); + if ($user->id === null) { + return false; + } + // get posts from user + $outbox = new \Federator\Data\ActivityPub\Common\Outbox(); + $min = $this->main->extractFromURI("min", ""); + $max = $this->main->extractFromURI("max", ""); + $page = $this->main->extractFromURI("page", ""); + if ($page !== "") { + $items = \Federator\DIO\Posts::getPostsByUser($dbh, $user->id, $connector, $cache, $min, $max); + $outbox->setItems($items); + } else { + $items = []; + } + $host = $_SERVER['SERVER_NAME']; + $id = 'https://' . $host .'/' . $_user . '/outbox'; + $outbox->setPartOf($id); + $outbox->setID($id); + if ($page !== '') { + $id .= '?page=' . urlencode($page); + } + if ($page === '' || $outbox->count() == 0) { + $outbox->setFirst($id); + $outbox->setLast($id . '&min=0'); + } + if (sizeof($items)>0) { + $newestId = $items[0]->getPublished(); + $oldestId = $items[sizeof($items)-1]->getPublished(); + $outbox->setNext($id . '&max=' . $newestId); + $outbox->setPrev($id . '&min=' . $oldestId); + } + $obj = $outbox->toObject(); + return json_encode($obj); + } + + /** + * handle post call + * + * @param string $_user user to add data to outbox @unused-param + * @return string|false response + */ + public function post($_user) + { + return false; + } +} diff --git a/php/federator/cache/cache.php b/php/federator/cache/cache.php index fa1fae8..3e692ef 100644 --- a/php/federator/cache/cache.php +++ b/php/federator/cache/cache.php @@ -13,6 +13,15 @@ namespace Federator\Cache; */ interface Cache extends \Federator\Connector\Connector { + /** + * save remote posts by user + * + * @param string $user user name + * @param \Federator\Data\ActivityPub\Common\APObject[]|false $posts user posts + * @return void + */ + public function saveRemotePostsByUser($user, $posts); + /** * save remote stats * diff --git a/php/federator/connector/connector.php b/php/federator/connector/connector.php index e877111..aab10e7 100644 --- a/php/federator/connector/connector.php +++ b/php/federator/connector/connector.php @@ -13,6 +13,17 @@ namespace Federator\Connector; */ interface Connector { + /** + * get posts by given user + * + * @param string $id user id + * @param string $minId min ID + * @param string $maxId max ID + + * @return \Federator\Data\ActivityPub\Common\APObject[]|false + */ + public function getRemotePostsByUser($id, $minId, $maxId); + /** * get remote user by given name * diff --git a/php/federator/data/activitypub/common/activity.php b/php/federator/data/activitypub/common/activity.php new file mode 100644 index 0000000..9137d6c --- /dev/null +++ b/php/federator/data/activitypub/common/activity.php @@ -0,0 +1,89 @@ +actor = $actor; + return $this; + } + + public function getAActor() : string + { + return $this->actor; + } + + /** + * create from json/array + * + * @param mixed $json + * @return bool true on success + */ + public function fromJson($json) + { + if (array_key_exists('actor', $json)) { + $this->actor = $json['actor']; + unset($json['actor']); + } + if (!parent::fromJson($json)) { + return false; + } + return true; + } + + /** + * convert internal state to php array + * + * @return array + */ + public function toObject() + { + $return = parent::toObject(); + if ($this->actor !== '') { + $return['actor'] = $this->actor; + } + return $return; + } + + /** + * get Child Object + * + * @return APObject|null + */ + public function getObject() + { + return parent::getObject(); + } +} diff --git a/php/federator/data/activitypub/common/apobject.php b/php/federator/data/activitypub/common/apobject.php new file mode 100644 index 0000000..735f3d6 --- /dev/null +++ b/php/federator/data/activitypub/common/apobject.php @@ -0,0 +1,951 @@ + $to + */ + private $to = array(); + + // bto + + /** + * list of cc ids + * + * @var array $cc + */ + private $cc = array(); + + // bcc + + /** + * media type + * + * @var string $mediaType + */ + private $mediaType = ""; + + // duration + + /** + * list of contexts + * + * @var array $context + */ + private $context = []; + + /** + * key value map of contexts + * @var array $contexts + */ + private $contexts = []; + + /** + * atom URI + * + * @var string $atomURI + */ + private $atomURI = ''; + + /** + * reply to atom URI + * + * @var ?string $replyAtomURI + */ + private $replyAtomURI = null; + + // found items + + /** + * generated unique id + * + * @var string $uuid + */ + private $uuid = ""; + + /** + * sensitive flag + * + * @var ?bool $sensitive + */ + private $sensitive = null; + + /** + * blur hash + * @var string $blurhash + */ + private $blurhash = ""; + + /** + * conversation id + * @var string $conversation + */ + private $conversation = ''; + + /** + * constructor + * + * @param string $type type of object + */ + public function __construct(string $type) + { + $this->type = $type; + } + + /** + * set note content + * + * @param string $content note content + * @return APObject current instance + */ + public function setContent(string $content) + { + $this->content = $content; + return $this; + } + + /** + * get note content + * + * @return string + */ + public function getContent() + { + return $this->content; + } + + /** + * add context to list + * + * @param string $context new context + * @return void + */ + final public function addContext($context) + { + if (!in_array($context, $this->context, false)) { + $this->context[] = $context; + } + } + + /** + * add multiple contexts to list + * + * @param array $contexts new contexts + * @return void + */ + final public function addContexts($contexts) + { + foreach ($contexts as $key => $value) { + if (!in_array($key, $this->contexts, false)) { + $this->contexts[$key] = $value; + } + } + } + + /** + * set id + * + * @param string $id new id + * @return APObject current object + */ + final public function setID($id) + { + $this->id = $id; + return $this; + } + + final public function getID() : string + { + return $this->id; + } + + /** + * set href + * + * @param string $href href + * @return APObject + */ + public function setHref(string $href) + { + $this->href = $href; + return $this; + } + + /** + * set child object + * + * @param APObject $object + * @return void + */ + public function setObject($object) + { + $this->object = $object; + } + + /** + * get child object + * + * @return APObject|null child object + */ + public function getObject() + { + return $this->object; + } + + /** + * set summary + * + * @param string $summary summary + * @return APObject this + */ + public function setSummary($summary) + { + $this->summary = $summary; + return $this; + } + /** + * set type + * + * @param string $type type + * @return void + */ + public function setType(string $type) + { + $this->type = $type; + } + + public function getType() : string + { + return $this->type; + } + + /** + * set attachments + * + * @param APObject[] $attachment + * @return void + */ + public function setAttachment($attachment) + { + $this->attachment = $attachment; + } + + public function addAttachment(APObject $attachment) : void + { + $this->attachment[] = $attachment; + } + + /** + * get attachments + * + * @return APObject[] attachments + */ + public function getAttachment() + { + return $this->attachment; + } + + public function getAttachmentsAsJson() : string + { + if ($this->attachment === []) { + return "{}"; + } + $obj = []; + foreach ($this->attachment as $a) { + $obj[] = $a->toObject(); + } + return json_encode($obj) | ''; + } + + /** + * set attributed to + * + * @param string $to attribute to + * @return APObject + */ + public function setAttributedTo(string $to) + { + $this->attributedTo = $to; + return $this; + } + + public function getAttributedTo() : string + { + return $this->attributedTo; + } + + /** + * set name + * + * @param string $name name + * @return APObject + */ + public function setName(string $name) + { + $this->name = $name; + return $this; + } + + /** + * add Image + * + * @param Image $image image to add + * @return void + */ + public function addImage(Image $image) + { + $this->image[] = $image; + } + + public function setInReplyTo(string $reply) : void + { + $this->inReplyTo = $reply; + } + + public function getInReplyTo() : string + { + return $this->inReplyTo ?? ""; + } + + /** + * set published timestamp + * + * @param int $published published timestamp + * @return APObject + */ + public function setPublished(int $published) + { + $this->published = $published; + return $this; + } + + public function getPublished() : int + { + return $this->published; + } + + /** + * add Tag + * + * @param Tag $tag tag to add + * @return void + */ + public function addTag(Tag $tag) + { + $this->tag[] = $tag; + } + + /** + * set url + * + * @param string $url URL + * @return APObject + */ + public function setURL(string $url) + { + $this->url = $url; + return $this; + } + + /** + * get URL + * + * @return string URL + */ + public function getURL() + { + return $this->url; + } + + /** + * add to + * + * @param string $to additional to address + * @return APObject + */ + public function addTo(string $to) + { + $this->to[] = $to; + return $this; + } + + /** + * get to + * + * @return array + */ + public function getTo() + { + return $this->to; + } + + /** + * add cc + * + * @param string $cc additional cc address + * @return void + */ + public function addCC($cc) + { + $this->cc[] = $cc; + } + + /** + * get cc + * + * @return array + */ + public function getCC() + { + return $this->cc; + } + + public function getMediaType() : string + { + return $this->mediaType; + } + + /** + * set atom URI + * + * @param string $uri atom URI + * @return void + */ + public function setAtomURI($uri) + { + $this->atomURI = $uri; + } + + /** + * set reply atom URI + * @param string $uri reply atom URI + * @return void + */ + public function setReplyAtomURI($uri) + { + $this->replyAtomURI = $uri; + } + + /** + * set sensitive + * + * @param bool $sensitive status + * @return void + */ + public function setSensitive($sensitive) + { + $this->sensitive = $sensitive; + } + + /** + * set conversation id + * @param string $conversation conversation ID + * @return void + */ + public function setConversation(string $conversation) + { + $this->conversation = $conversation; + } + + public function getConversation() : ?string + { + return $this->conversation; + } + + /** + * create from json + * + * @param array $json input + * @return bool true on success + */ + public function fromJson($json) + { + if (!is_array($json)) { + error_log("fromJson called with ".gettype($json). " => ". debug_backtrace()[1]['function'] + . " json: " . print_r($json, true)); + return false; + } + if (array_key_exists('id', $json)) { + $this->id = $json['id']; + } + if (array_key_exists('content', $json)) { + $this->content = $json['content']; + } + if (array_key_exists('duration', $json)) { + try { + $this->duration = new \DateInterval($json['duration']); + } catch (\Exception $unused_e) { + error_log("error parsing duration ". $json['duration']); + } + } + if (array_key_exists('height', $json)) { + $this->height = intval($json['height'], 10); + } + if (array_key_exists('href', $json)) { + $this->href = $json['href']; + } + if (array_key_exists('endTime', $json) && $json['endTime'] !== null) { + $this->endTime = $this->parseDateTime($json['endTime']); + } + if (array_key_exists('width', $json)) { + $this->width = intval($json['width'], 10); + } + if (array_key_exists('attachment', $json) && $json['attachment'] !== null) { + $attachment = []; + foreach ($json['attachment'] as $a) { + $att = \Federator\Data\ActivityPub\Factory::newFromJson($a, ""); + if ($att !== null) { + $attachment[] = $att; + } + } + $this->attachment = $attachment; + } + if (array_key_exists('attributedTo', $json)) { + if (is_array($json['attributedTo']) && array_key_exists(0, $json['attributedTo'])) { + if (is_array($json['attributedTo'][0])) { + $this->attributedTo = $json['attributedTo'][0]['id']; + } else { + $this->attributedTo = $json['attributedTo'][0]; + } + } else { + $this->attributedTo = (string)$json['attributedTo']; + } + } + if (array_key_exists('name', $json)) { + $this->name = $json['name']; + } + if (array_key_exists('icon', $json)) { + if (array_key_exists('type', $json['icon'])) { + $image = new Image(); + $image->fromJson($json['icon']); + $this->icon[] = $image; + } else { + foreach ($json['icon'] as $icon) { + $image = new Image(); + $image->fromJson($icon); + $this->icon[] = $image; + } + } + } + if (array_key_exists('image', $json)) { + $image = new Image(); + $image->fromJson($json['image']); + $this->image[] = $image; + } + if (array_key_exists('inReplyTo', $json)) { + $this->inReplyTo = $json['inReplyTo']; + } + if (array_key_exists('published', $json)) { + $this->published = $this->parseDateTime($json['published']); + } + if (array_key_exists('summary', $json)) { + $this->summary = $json['summary']; + } + if (array_key_exists('tag', $json)) { + $tags = []; + foreach ($json['tag'] as $t) { + $tag = new Tag(); + $tag->fromJson($t); + $tags[] = $tag; + } + $this->tag = $tags; + } + if (array_key_exists('updated', $json)) { + $this->updated = $this->parseDateTime($json['updated']); + } + if (array_key_exists('url', $json)) { + $this->url = $json['url']; + } + if (array_key_exists('to', $json)) { + if (is_array($json['to'])) { + foreach ($json['to'] as $to) { + $this->to[] = $to; + } + } else { + $this->to[] = $json['to']; + } + } + if (array_key_exists('cc', $json)) { + if (is_array($json['cc'])) { + foreach ($json['cc'] as $cc) { + $this->cc[] = $cc; + } + } else { + $this->cc[] = $json['cc']; + } + } + if (array_key_exists('mediaType', $json)) { + $this->mediaType = $json['mediaType']; + } + if (array_key_exists('object', $json)) { + $this->object = \Federator\Data\ActivityPub\Factory::newFromJson($json['object'], ""); + } + if (array_key_exists('sensitive', $json)) { + $this->sensitive = $json['sensitive']; + } + if (array_key_exists('blurhash', $json)) { + $this->blurhash = $json['blurhash']; + } + if (array_key_exists('uuid', $json)) { + $this->uuid = $json['uuid']; + } + if (array_key_exists('conversation', $json)) { + $this->conversation = $json['conversation']; + } + return true; + } + + /** + * + * {@inheritDoc} + * @see JsonSerializable::jsonSerialize() + */ + public function jsonSerialize() + { + return $this->toObject(); + } + + /** + * convert internal state to php array + * + * @return array + */ + public function toObject() + { + $return = []; + if (sizeof($this->context) == 1) { + $return['@context'] = array_values($this->context)[0]; + } elseif (sizeof($this->context) > 1) { + $c = []; + foreach (array_values($this->context) as $context) { + $c[] = $context; + } + $return['@context'] = $c; + } + if (sizeof($this->contexts) > 0) { + if (array_key_exists('@context', $return)) { + $return['@context'] = [$return['@context']]; + } else { + $return['@context'] = []; + } + $return['@context'][] = $this->contexts; + } + if ($this->id !== "") { + $return['id'] = $this->id; + } + $return['type'] = $this->type; + + if ($this->content !== "") { + $return['content'] = $this->content; + } + + if ($this->duration !== false) { + $return['duration'] = $this->duration->format("P%yY%mM%dDT%hH%iM%sS"); + } + if ($this->height != -1) { + $return['height'] = $this->height; + } + if ($this->href !== "") { + $return['href'] = $this->href; + } + if ($this->endTime > 0) { + $return['endTime'] = gmdate("Y-m-d\TH:i:s\Z", $this->endTime); + } + if ($this->width != -1) { + $return['width'] = $this->width; + } + if (sizeof($this->attachment) > 0) { + $attachment = []; + foreach ($this->attachment as $a) { + $attachment[] = $a->toObject(); + } + $return['attachment'] = $attachment; + } + if ($this->attributedTo !== "") { + $return['attributedTo'] = $this->attributedTo; + } + if ($this->name !== "") { + $return['name'] = $this->name; + } + if (sizeof($this->icon) > 0) { + if (sizeof($this->icon) > 1) { + $icons = []; + foreach ($this->icon as $icon) { + $icons[] = $icon->toObject(); + } + $return['icon'] = $icons; + } else { + $return['icon'] = $this->icon[0]->toObject(); + } + } + if (sizeof($this->image) > 0) { + $images = []; + foreach ($this->image as $image) { + $images[] = $image->toObject(); + } + $return['image'] = $images; + } + if ($this->inReplyTo !== "") { + $return['inReplyTo'] = $this->inReplyTo; + } + if ($this->published > 0) { + $return['published'] = gmdate("Y-m-d\TH:i:s\Z", $this->published); + } + if ($this->summary !== "") { + $return['summary'] = $this->summary; + } + if (sizeof($this->tag) > 0) { + $tags = []; + foreach ($this->tag as $tag) { + $tags[] = $tag->toObject(); + } + $return['tag'] = $tags; + } + if ($this->updated > 0) { + $return['updated'] = gmdate("Y-m-d\TH:i:S\Z", $this->updated); + } + if ($this->url !== '') { + $return['url'] = $this->url; + } + if (sizeof($this->to) > 0) { + $return['to'] = $this->to; + } + if (sizeof($this->cc) > 0) { + $return['cc'] = $this->cc; + } + if ($this->mediaType !== '') { + $return['mediaType'] = $this->mediaType; + } + if ($this->object !== null) { + $return['object'] = $this->object->toObject(); + } + if ($this->atomURI !== '') { + $return['atomUri'] = $this->atomURI; + } + if ($this->replyAtomURI !== null) { + $return['inReplyTo'] = $this->replyAtomURI; + $return['inReplyToAtomUri'] = $this->replyAtomURI; + } + if ($this->sensitive !== null) { + $return['sensitive'] = $this->sensitive; + } + if ($this->blurhash !== '') { + $return['blurhash'] = $this->blurhash; + } + if ($this->conversation !== '') { + $return['conversation'] = $this->conversation; + } + if ($this->uuid !== '') { + $return['uuid'] = $this->uuid; + } + return $return; + } + + /** + * set uuid + * + * @param string $id + * @return void + */ + public function setUUID($id) + { + $this->uuid = $id; + } + + public function getUUID() : string + { + return $this->uuid; + } + + public static function parseDateTime(string $input) : int + { + $timestamp = 0; + if (strpos($input, "T")!== false) { + $date = \DateTime::createFromFormat('Y-m-d\TH:i:sT', $input); + if ($date === false) { + $date = \DateTime::createFromFormat('Y-m-d\TH:i:s+', $input); + } + if ($date !== false) { + $timestamp = $date->getTimestamp(); + } else { + error_log("date parsing error ". $input); + } + } else { + $timestamp = intval($input, 10); + } + return $timestamp; + } +} diff --git a/php/federator/data/activitypub/common/article.php b/php/federator/data/activitypub/common/article.php new file mode 100644 index 0000000..0c26918 --- /dev/null +++ b/php/federator/data/activitypub/common/article.php @@ -0,0 +1,27 @@ + + */ + public function toObject() + { + $return = parent::toObject(); + $return['type'] = 'Collection'; + if ($this->totalItems > 0) { + $return['totalItems'] = $this->totalItems; + } + if ($this->first !== '') { + $return['first'] = $this->first; + } + if ($this->last !== '') { + $return['last'] = $this->last; + } + return $return; + } + + /** + * create object from json + * + * @param array $json input json + * @return bool true on success + */ + public function fromJson($json) + { + return parent::fromJson($json); + } + + public function count() : int + { + return $this->totalItems; + } + + public function setFirst(string $url) : void + { + $this->first = $url; + } + + public function setLast(string $url) : void + { + $this->last = $url; + } +} diff --git a/php/federator/data/activitypub/common/create.php b/php/federator/data/activitypub/common/create.php new file mode 100644 index 0000000..0dfa555 --- /dev/null +++ b/php/federator/data/activitypub/common/create.php @@ -0,0 +1,43 @@ + + */ + public function toObject() + { + $return = parent::toObject(); + $return['type'] = 'Create'; + // overwrite id from url + $return['id'] = $this->getURL(); + return $return; + } + + /** + * create object from json + * + * @param array $json input json + * @return bool true on success + */ + public function fromJson($json) + { + return parent::fromJson($json); + } +} diff --git a/php/federator/data/activitypub/common/image.php b/php/federator/data/activitypub/common/image.php new file mode 100644 index 0000000..58b74a0 --- /dev/null +++ b/php/federator/data/activitypub/common/image.php @@ -0,0 +1,62 @@ +mediaType = $mediaType; + return $this; + } + + /** + * create object from json + * @param mixed $json input + * @return bool true on success + */ + public function fromJson($json) + { + if (!parent::fromJson($json)) { + return false; + } + if (array_key_exists('url', $json)) { + $this->url = $json['url']; + } + if (array_key_exists('mediaType', $json)) { + $this->mediaType = $json['mediaType']; + } + return true; + } +} diff --git a/php/federator/data/activitypub/common/note.php b/php/federator/data/activitypub/common/note.php new file mode 100644 index 0000000..3fcee12 --- /dev/null +++ b/php/federator/data/activitypub/common/note.php @@ -0,0 +1,95 @@ +sender = $sender; + return $this; + } + + /** + * get sender + * + * @return string sender + */ + public function getSender() + { + return $this->sender; + } + + /** + * set receiver + * + * @param string $receiver note receiver + * @return Note current instance + */ + public function setReceiver(string $receiver) + { + $this->receiver = $receiver; + return $this; + } + + /** + * convert internal state to php array + * @return array + */ + public function toObject() + { + $return = parent::toObject(); + if ($this->sender !== "") { + $return['sender'] = $this->sender; + } + if ($this->receiver !== "") { + $return['receiver'] = $this->receiver; + } + return $return; + } + + /** + * create object from json + * @param mixed $json input json + * @return bool true on success + */ + public function fromJson($json) + { + if (!parent::fromJson($json)) { + return false; + } + $this->receiver = ""; + return true; + } +} diff --git a/php/federator/data/activitypub/common/orderedcollection.php b/php/federator/data/activitypub/common/orderedcollection.php new file mode 100644 index 0000000..b29aba8 --- /dev/null +++ b/php/federator/data/activitypub/common/orderedcollection.php @@ -0,0 +1,91 @@ + + */ + public function toObject() + { + $return = parent::toObject(); + $return['type'] = 'OrderedCollection'; + if ($this->totalItems > 0) { + foreach ($this->items as $item) { + $return['OrderedItems'][] = $item->toObject(); + } + } + return $return; + } + + /** + * create object from json + * + * @param array $json input json + * @return bool true on success + */ + public function fromJson($json) + { + return parent::fromJson($json); + } + + public function append(APObject &$item) : void + { + $this->items[] = $item; + $this->totalItems = sizeof($this->items); + } + + /** + * get item with given index + * + * @return APObject|false + */ + public function get(int $index) + { + if ($index >= 0) { + if ($index >= $this->totalItems) { + return false; + } + return $this->items[$index]; + } else { + if ($this->totalItems+ $index < 0) { + return false; + } + return $this->items[$this->totalItems + $index]; + } + } + + /** + * set items + * + * @param APObject[] $items + * @return void + */ + public function setItems(&$items) + { + $this->items = $items; + $this->totalItems = sizeof($items); + } +} diff --git a/php/federator/data/activitypub/common/orderedcollectionpage.php b/php/federator/data/activitypub/common/orderedcollectionpage.php new file mode 100644 index 0000000..3d928ff --- /dev/null +++ b/php/federator/data/activitypub/common/orderedcollectionpage.php @@ -0,0 +1,82 @@ + + */ + public function toObject() + { + $return = parent::toObject(); + if ($this->next !== '') { + $return['next'] = $this->next; + } + if ($this->prev !== '') { + $return['prev'] = $this->prev; + } + if ($this->partOf !== '') { + $return['partOf'] = $this->partOf; + } + $return['type'] = 'OrderedCollectionPage'; + return $return; + } + + /** + * create object from json + * + * @param array $json input json + * @return bool true on success + */ + public function fromJson($json) + { + return parent::fromJson($json); + } + + /** + * set next url + * + * @param string $url new next URL + * @return void + */ + public function setNext($url) + { + $this->next = $url; + } + + /** + * set prev url + * + * @param string $url new prev URL + * @return void + */ + public function setPrev($url) + { + $this->prev = $url; + } + + public function setPartOf(string $url) : void + { + $this->partOf = $url; + } +} diff --git a/php/federator/data/activitypub/common/outbox.php b/php/federator/data/activitypub/common/outbox.php new file mode 100644 index 0000000..4439ebe --- /dev/null +++ b/php/federator/data/activitypub/common/outbox.php @@ -0,0 +1,50 @@ + "http://ostatus.org#", + "atomUri" => "ostatus:atomUri", + "inReplyToAtomUri" => "ostatus:inReplyToAtomUri", + "conversation" => "ostatus:conversation", + "sensitive" => "as:sensitive", + "toot" => "http://joinmastodon.org/ns#", + "votersCount" => "toot:votersCount", + "Hashtag" => "as:Hashtag" + ]); + } + + /** + * convert internal state to php array + * + * @return array + */ + public function toObject() + { + $return = parent::toObject(); + return $return; + } + + /** + * create object from json + * + * @param array $json input json + * @return bool true on success + */ + public function fromJson($json) + { + return parent::fromJson($json); + } +} diff --git a/php/federator/data/activitypub/common/tag.php b/php/federator/data/activitypub/common/tag.php new file mode 100644 index 0000000..2560c46 --- /dev/null +++ b/php/federator/data/activitypub/common/tag.php @@ -0,0 +1,34 @@ +setType($json['type']); + } + return true; + } +} diff --git a/php/federator/data/activitypub/factory.php b/php/federator/data/activitypub/factory.php new file mode 100644 index 0000000..ef88ff8 --- /dev/null +++ b/php/federator/data/activitypub/factory.php @@ -0,0 +1,108 @@ + $json input json + * @return Common\APObject|null object or false on error + */ + public static function newFromJson($json, string $jsonstring) + { + if (gettype($json) !== "array") { + error_log("newFromJson called with ".gettype($json). " => ". debug_backtrace()[1]['function'] + . " json: " . print_r($json, true)); + return null; + } + if (!array_key_exists('type', $json)) { + return null; + } + $return = null; + switch ($json['type']) { + case 'Article': + $return = new Common\Article(); + break; + /*case 'Document': + $return = new Common\Document(); + break; + case 'Event': + $return = new Common\Event(); + break; + case 'Follow': + $return = new Common\Follow(); + break;*/ + case 'Image': + $return = new Common\Image(); + break; + /*case 'Note': + $return = new Common\Note(); + break; + case 'Question': + $return = new \Common\Question(); + break; + case 'Video': + $return = new \Common\Video(); + break;*/ + default: + error_log("newFromJson: unknown type: '" . $json['type'] . "' " . $jsonstring); + error_log(print_r($json, true)); + } + if ($return !== null && $return->fromJson($json)) { + return $return; + } + return null; + } + + /** + * create object tree from json + * @param array $json input json + * @return Common\Activity|false object or false on error + */ + public static function newActivityFromJson($json) + { + if (!array_key_exists('type', $json)) { + return false; + } + //$return = false; + switch ($json['type']) { + case 'MakePhanHappy': + break; +/* case 'Accept': + $return = new Common\Accept(); + break; + case 'Announce': + $return = new Common\Announce(); + break; + case 'Create': + $return = new Common\Create(); + break; + case 'Delete': + $return = new Common\Delete(); + break; + case 'Follow': + $return = new Common\Follow(); + break; + case 'Undo': + $return = new \Common\Undo(); + break;*/ + default: + error_log("newActivityFromJson " . print_r($json, true)); + } + /*if ($return !== false && $return->fromJson($json) !== null) { + return $return; + }*/ + return false; + } +} diff --git a/php/federator/dio/posts.php b/php/federator/dio/posts.php new file mode 100644 index 0000000..7be8c0e --- /dev/null +++ b/php/federator/dio/posts.php @@ -0,0 +1,59 @@ +getRemotePostsByUser($id, $minId, $maxId); + if ($posts !== false) { + return $posts; + } + } + $posts = []; + // TODO: check our db + + if ($posts === []) { + // ask connector for user-id + $posts = $connector->getRemotePostsByUser($id, $minId, $maxId); + if ($posts === false) { + $posts = []; + } + } + // save posts to DB + if ($cache !== null) { + $cache->saveRemotePostsByUser($id, $posts); + } + return $posts; + } +} diff --git a/php/federator/dio/user.php b/php/federator/dio/user.php index 82084fa..24ab009 100644 --- a/php/federator/dio/user.php +++ b/php/federator/dio/user.php @@ -44,7 +44,8 @@ class User $public = openssl_pkey_get_details($private_key)['key']; $private = ''; openssl_pkey_export($private_key, $private); - $sql = 'insert into users (id, externalid, rsapublic, rsaprivate, validuntil, type, name, summary, registered, iconmediatype, iconurl, imagemediatype, imageurl)'; + $sql = 'insert into users (id, externalid, rsapublic, rsaprivate, validuntil,'; + $sql .= ' type, name, summary, registered, iconmediatype, iconurl, imagemediatype, imageurl)'; $sql .= ' values (?, ?, ?, ?, now() + interval 1 day, ?, ?, ?, ?, ?, ?, ?, ?)'; $stmt = $dbh->prepare($sql); if ($stmt === false) { @@ -68,7 +69,8 @@ class User ); } else { // update to existing user - $sql = 'update users set validuntil=now() + interval 1 day, type=?, name=?, summary=?, registered=?, iconmediatype=?, iconurl=?, imagemediatype=?, imageurl=? where id=?'; + $sql = 'update users set validuntil=now() + interval 1 day, type=?, name=?, summary=?, registered=?,'; + $sql .= ' iconmediatype=?, iconurl=?, imagemediatype=?, imageurl=? where id=?'; $stmt = $dbh->prepare($sql); if ($stmt === false) { throw new \Federator\Exceptions\ServerError(); @@ -151,7 +153,8 @@ class User return $user; } // check our db - $sql = 'select id,externalid,type,name,summary,unix_timestamp(registered),rsapublic,iconmediatype,iconurl,imagemediatype,imageurl from users where id=? and validuntil>=now()'; + $sql = 'select id,externalid,type,name,summary,unix_timestamp(registered),rsapublic,'; + $sql .= 'iconmediatype,iconurl,imagemediatype,imageurl from users where id=? and validuntil>=now()'; $stmt = $dbh->prepare($sql); if ($stmt === false) { throw new \Federator\Exceptions\ServerError(); diff --git a/php/federator/language.php b/php/federator/language.php index 247eb29..2e58133 100644 --- a/php/federator/language.php +++ b/php/federator/language.php @@ -96,8 +96,8 @@ class Language if ($root === '') { $root = '.'; } - if (@file_exists($root . '/../lang/' . $this->uselang . "/$group.inc")) { - require($root . '/../lang/' . $this->uselang . "/$group.inc"); + if (@file_exists($root . '../lang/federator/' . $this->uselang . "/$group.inc")) { + require($root . '../lang/federator/' . $this->uselang . "/$group.inc"); $this->lang[$group] = $l; } } diff --git a/php/federator/maintenance.php b/php/federator/maintenance.php index 950eebd..4177498 100644 --- a/php/federator/maintenance.php +++ b/php/federator/maintenance.php @@ -5,8 +5,12 @@ * * @author Sascha Nitsch (grumpydeveloper) **/ + namespace Federator; +/** + * maintenance functions + */ class Maintenance { /** diff --git a/plugins/federator/contentnation.php b/plugins/federator/contentnation.php index 95f649c..6ce0383 100644 --- a/plugins/federator/contentnation.php +++ b/plugins/federator/contentnation.php @@ -20,6 +20,13 @@ class ContentNation implements Connector */ private $config; + /** + * main instance + * + * @var \Federator\Main $main + */ + private $main; + /** * service-URL * @@ -30,11 +37,102 @@ class ContentNation implements Connector /** * constructor * + * @param \Federator\Main $main */ - public function __construct() + public function __construct($main) { - $config = parse_ini_file($_SERVER['DOCUMENT_ROOT'] . '../contentnation.ini'); - $this->service = $config['service-uri']; + $config = parse_ini_file($_SERVER['DOCUMENT_ROOT'] . '../contentnation.ini', true); + if ($config !== false) { + $this->config = $config; + } + $this->service = $config['contentnation']['service-uri']; + $this->main = $main; + } + + /** + * get posts by given user + * + * @param string $userId user id + * @param string $min min date + * @param string $max max date + * @return \Federator\Data\ActivityPub\Common\APObject[]|false + */ + public function getRemotePostsByUser($userId, $min, $max) + { + $remoteURL = $this->service . '/api/profile/' . $userId . '/activities'; + if ($min !== '') { + $remoteURL .= '&minTS=' . urlencode($min); + } + if ($max !== '') { + $remoteURL .= '&maxTS=' . urlencode($max); + } + [$response, $info] = \Federator\Main::getFromRemote($remoteURL, []); + if ($info['http_code'] != 200) { + print_r($info); + return false; + } + $r = json_decode($response, true); + if ($r === false || $r === null || !is_array($r)) { + return false; + } + $posts = []; + if (array_key_exists('articles', $r)) { + $articles = $r['articles']; + $host = $_SERVER['SERVER_NAME']; + $imgpath = $this->config['userdata']['path']; + $userdata = $this->config['userdata']['url']; + foreach ($articles as $article) { + $create = new \Federator\Data\ActivityPub\Common\Create(); + $create->setAActor('https://' . $host .'/' . $article['profilename']); + $create->setID($article['id']) + ->setURL('https://'.$host.'/' . $article['profilename'] + . '/statuses/' . $article['id'] . '/activity') + ->setPublished(max($article['published'], $article['modified'])) + ->addTo("https://www.w3.org/ns/activitystreams#Public") + ->addCC('https://' . $host . '/' . $article['profilename'] . '/followers.json'); + $apArticle = new \Federator\Data\ActivityPub\Common\Article(); + if (array_key_exists('tags', $article)) { + foreach ($article['tags'] as $tag) { + $href = 'https://' . $host . '/' . $article['language'] + . '/search.htm?tagsearch=' . urlencode($tag); + $tagObj = new \Federator\Data\ActivityPub\Common\Tag(); + $tagObj->setHref($href) + ->setName('#' . urlencode(str_replace(' ', '', $tag))) + ->setType('Hashtag'); + $article->addTag($tagObj); + } + } + $apArticle->setPublished($article['published']) + ->setName($article['title']) + ->setAttributedTo('https://' . $host .'/' . $article['profilename']) + ->setContent( + $article['teaser'] ?? + $this->main->translate( + $article['language'], + 'article', + 'newarticle' + ) + ) + ->addTo("https://www.w3.org/ns/activitystreams#Public") + ->addCC('https://' . $host . '/' . $article['profilename'] . '/followers.json'); + $articleimage = $article['imagealt'] ?? + $this->main->translate($article['language'], 'article', 'image'); + $idurl = 'https://' . $host . '/' . $article['language'] + . '/' . $article['profilename'] . '/'. $article['name']; + $apArticle->setID($idurl) + ->setURL($idurl); + $image = $article['image'] !== "" ? $article['image'] : $article['profileimg']; + $mediaType = @mime_content_type($imgpath . $article['profile'] . '/' . $image) | 'text/plain'; + $img = new \Federator\Data\ActivityPub\Common\Image(); + $img->setMediaType($mediaType) + ->setName($articleimage) + ->setURL($userdata . $article['profile'] . '/' . $image); + $apArticle->addImage($img); + $create->setObject($apArticle); + $posts[] = $create; + } + } + return $posts; } /** @@ -47,7 +145,6 @@ class ContentNation implements Connector $remoteURL = $this->service . '/api/stats'; [$response, $info] = \Federator\Main::getFromRemote($remoteURL, []); if ($info['http_code'] != 200) { - print_r($info); return false; } $r = json_decode($response, true); @@ -147,6 +244,6 @@ namespace Federator; */ function contentnation_load($main) { - $cn = new Connector\ContentNation(); + $cn = new Connector\ContentNation($main); $main->setConnector($cn); } diff --git a/plugins/federator/dummyconnector.php b/plugins/federator/dummyconnector.php index ee98424..ee0e6c3 100644 --- a/plugins/federator/dummyconnector.php +++ b/plugins/federator/dummyconnector.php @@ -19,6 +19,19 @@ class DummyConnector implements Connector { } + /** + * get posts by given user + * + * @param string $id user id @unused-param + * @param string $minId min ID @unused-param + * @param string $maxId max ID @unused-param + * @return \Federator\Data\ActivityPub\Common\APObject[]|false + */ + public function getRemotePostsByUser($id, $minId, $maxId) + { + return false; + } + /** * get statistics from remote system * diff --git a/plugins/federator/rediscache.php b/plugins/federator/rediscache.php index 9e838da..5c96eeb 100644 --- a/plugins/federator/rediscache.php +++ b/plugins/federator/rediscache.php @@ -77,6 +77,21 @@ class RedisCache implements Cache return $prefix . '_' . md5($input); } + /** + * get posts by given user + * + * @param string $id user id @unused-param + * @param string $minId min ID @unused-param + * @param string $maxId max ID @unused-param + + * @return \Federator\Data\ActivityPub\Common\APObject[]|false + */ + public function getRemotePostsByUser($id, $minId, $maxId) + { + error_log("rediscache::getRemotePostsByUser not implemented"); + return false; + } + /** * get statistics from remote system * @@ -137,6 +152,18 @@ class RedisCache implements Cache return $user; } + /** + * save remote posts by user + * + * @param string $user user name @unused-param + * @param \Federator\Data\ActivityPub\Common\APObject[]|false $posts user posts @unused-param + * @return void + */ + public function saveRemotePostsByUser($user, $posts) + { + error_log("rediscache::saveRemotePostsByUser not implemented"); + } + /** * save remote stats * diff --git a/templates/federator/webfinger_acct.json b/templates/federator/webfinger_acct.json index ab532ce..39978f4 100644 --- a/templates/federator/webfinger_acct.json +++ b/templates/federator/webfinger_acct.json @@ -1,7 +1,7 @@ {ldelim} "subject": "acct:{$username}@{$domain}", "aliases": [ - "https://{$domain}/@{$username}" + "https://{$domain}/@{$username}", "https://{$domain}/users/{$username}" ], "links": [ @@ -11,4 +11,4 @@ {/if} {ldelim}"rel": "http://ostatus.org/schema/1.0/subscribe", "template": "https://{$domain}/authorize_interaction?uri={ldelim}uri{rdelim}"{rdelim} ] -{rdelim} \ No newline at end of file +{rdelim}