+
+ /**
+ * Test if the body contains valid content
+ *
+ * @param string $body
+ * @return boolean
+ */
+ private static function invalidBody(string $body)
+ {
+ // Currently we only test for a HTML element.
+ // Possibly we enhance this in the future.
+ return !strpos($body, '>');
+ }
+
+ /**
+ * Update GServer entries
+ */
+ public static function discover()
+ {
+ // Update the server list
+ self::discoverFederation();
+
+ $no_of_queries = 5;
+
+ $requery_days = intval(DI::config()->get('system', 'poco_requery_days'));
+
+ if ($requery_days == 0) {
+ $requery_days = 7;
+ }
+
+ $last_update = date('c', time() - (60 * 60 * 24 * $requery_days));
+
+ $gservers = DBA::select('gserver', ['id', 'url', 'nurl', 'network', 'poco', 'directory-type'],
+ ["NOT `failed` AND `directory-type` != ? AND `last_poco_query` < ?", GServer::DT_NONE, $last_update],
+ ['order' => ['RAND()']]);
+
+ while ($gserver = DBA::fetch($gservers)) {
+ Logger::info('Update peer list', ['server' => $gserver['url'], 'id' => $gserver['id']]);
+ Worker::add(PRIORITY_LOW, 'UpdateServerPeers', $gserver['url']);
+
+ Logger::info('Update directory', ['server' => $gserver['url'], 'id' => $gserver['id']]);
+ Worker::add(PRIORITY_LOW, 'UpdateServerDirectory', $gserver);
+
+ $fields = ['last_poco_query' => DateTimeFormat::utcNow()];
+ DBA::update('gserver', $fields, ['nurl' => $gserver['nurl']]);
+
+ if (--$no_of_queries == 0) {
+ break;
+ }
+ }
+
+ DBA::close($gservers);
+ }
+
+ /**
+ * Discover federated servers
+ */
+ private static function discoverFederation()
+ {
+ $last = DI::config()->get('poco', 'last_federation_discovery');
+
+ if ($last) {
+ $next = $last + (24 * 60 * 60);
+
+ if ($next > time()) {
+ return;
+ }
+ }
+
+ // Discover federated servers
+ $protocols = ['activitypub', 'diaspora', 'dfrn', 'ostatus'];
+ foreach ($protocols as $protocol) {
+ $query = '{nodes(protocol:"' . $protocol . '"){host}}';
+ $curlResult = DI::httpClient()->fetch('https://the-federation.info/graphql?query=' . urlencode($query), 0, HttpClient::ACCEPT_JSON);
+ if (!empty($curlResult)) {
+ $data = json_decode($curlResult, true);
+ if (!empty($data['data']['nodes'])) {
+ foreach ($data['data']['nodes'] as $server) {
+ // Using "only_nodeinfo" since servers that are listed on that page should always have it.
+ self::add('https://' . $server['host'], true);
+ }
+ }
+ }
+ }
+
+ // Disvover Mastodon servers
+ $accesstoken = DI::config()->get('system', 'instances_social_key');
+
+ if (!empty($accesstoken)) {
+ $api = 'https://instances.social/api/1.0/instances/list?count=0';
+ $curlResult = DI::httpClient()->get($api, [HttpClientOptions::HEADERS => ['Authorization' => ['Bearer ' . $accesstoken]], HttpClientOptions::ACCEPT_CONTENT => HttpClient::ACCEPT_JSON]);
+ if ($curlResult->isSuccess()) {
+ $servers = json_decode($curlResult->getBody(), true);
+
+ foreach ($servers['instances'] as $server) {
+ $url = (is_null($server['https_score']) ? 'http' : 'https') . '://' . $server['name'];
+ self::add($url);
+ }
+ }
+ }
+
+ DI::config()->set('poco', 'last_federation_discovery', time());
+ }
+
+ /**
+ * Set the protocol for the given server
+ *
+ * @param int $gsid Server id
+ * @param int $protocol Protocol id
+ * @return void
+ * @throws Exception
+ */
+ public static function setProtocol(int $gsid, int $protocol)
+ {
+ if (empty($gsid)) {
+ return;
+ }
+
+ $gserver = DBA::selectFirst('gserver', ['protocol', 'url'], ['id' => $gsid]);
+ if (!DBA::isResult($gserver)) {
+ return;
+ }
+
+ $old = $gserver['protocol'];
+
+ if (!is_null($old)) {
+ /*
+ The priority for the protocols is:
+ 1. ActivityPub
+ 2. DFRN via Diaspora
+ 3. Legacy DFRN
+ 4. Diaspora
+ 5. OStatus
+ */
+
+ // We don't need to change it when nothing is to be changed
+ if ($old == $protocol) {
+ 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;
+ }
+
+ // Don't change it to anything lower than DFRN if the new one wasn't ActivityPub
+ if (($old == Post\DeliveryData::DFRN) && ($protocol != Post\DeliveryData::ACTIVITYPUB)) {
+ return;
+ }
+
+ // Don't change it to Diaspora when it is a legacy DFRN server
+ if (($old == Post\DeliveryData::LEGACY_DFRN) && ($protocol == Post\DeliveryData::DIASPORA)) {
+ return;
+ }
+ }
+
+ Logger::info('Protocol for server', ['protocol' => $protocol, 'old' => $old, 'id' => $gsid, 'url' => $gserver['url'], 'callstack' => System::callstack(20)]);
+ DBA::update('gserver', ['protocol' => $protocol], ['id' => $gsid]);
+ }
+
+ /**
+ * Fetch the protocol of the given server
+ *
+ * @param int $gsid Server id
+ * @return int
+ * @throws Exception
+ */
+ public static function getProtocol(int $gsid)
+ {
+ if (empty($gsid)) {
+ return null;
+ }
+
+ $gserver = DBA::selectFirst('gserver', ['protocol'], ['id' => $gsid]);
+ if (DBA::isResult($gserver)) {
+ return $gserver['protocol'];
+ }
+
+ return null;
+ }