diff options
Diffstat (limited to 'lib')
-rw-r--r-- | lib/activitypub-actor.php | 75 | ||||
-rw-r--r-- | lib/activitypub-followers.php | 70 | ||||
-rw-r--r-- | lib/activitypub-functions.php | 521 | ||||
-rw-r--r-- | lib/activitypub-inbox.php | 196 | ||||
-rw-r--r-- | lib/activitypub-outbox.php | 72 | ||||
-rw-r--r-- | lib/activitypub-webfinger.php | 24 | ||||
-rw-r--r-- | lib/database.php | 2 | ||||
-rw-r--r-- | lib/functions.php | 24 | ||||
-rw-r--r-- | lib/twitter_api.php | 410 |
9 files changed, 2 insertions, 1392 deletions
diff --git a/lib/activitypub-actor.php b/lib/activitypub-actor.php deleted file mode 100644 index b34f582..0000000 --- a/lib/activitypub-actor.php +++ /dev/null @@ -1,75 +0,0 @@ -<?php - - if(!$config['activitypub']) exit('ActivityPub is disabled via config file.'); - - $public_key = activitypub_get_key('public'); - - // generate a key pair, if neccessary - if(!$public_key) { - $key = activitypub_new_key('sha512', 4096, 'RSA'); - - if(!empty($key)) exit('Fatal error: Could not generate a new key!'); - $public_key = $key['key_public']; - } - - /* - // old, file-based key system - if(!file_exists(ROOT.DS.'keys'.DS.'id_rsa')) { - if(!is_dir(ROOT.DS.'keys')) { - mkdir(ROOT.DS.'keys'); - } - - // generate a key pair, if neccessary - $rsa = openssl_pkey_new([ - 'digest_alg' => 'sha512', - 'private_key_bits' => 4096, - 'private_key_type' => OPENSSL_KEYTYPE_RSA, - ]); - openssl_pkey_export($rsa, $private_key); - $public_key = openssl_pkey_get_details($rsa)['key']; - - file_put_contents(ROOT.DS.'keys'.DS.'id_rsa', $private_key); - file_put_contents(ROOT.DS.'keys'.DS.'id_rsa.pub', $public_key); - } else { - $public_key = file_get_contents(ROOT.DS.'keys'.DS.'id_rsa.pub'); - } - */ - - if(strpos($_SERVER['HTTP_ACCEPT'], 'application/activity+json') !== false): - - header('Content-Type: application/ld+json'); - -?>{ - "@context": [ - "https://www.w3.org/ns/activitystreams", - "https://w3id.org/security/v1" - ], - "id": "<?= $config['url'] ?>/actor", - "type": "Person", - "name": "<?= trim($config['site_title']) ?>", - "summary": "<?= trim($config['site_claim']) ?>", - "preferredUsername": "<?= ltrim($config['microblog_account'], '@') ?>", - "manuallyApprovesFollowers": false, - "discoverable": true, - "publishedDate": "2023-01-01T00:00:00Z", - "icon": { - "url": "<?= $config['url'] ?>/favicon-large.png", - "mediaType": "image/png", - "type": "Image" - }, - "inbox": "<?= $config['url'] ?>/inbox", - "outbox": "<?= $config['url'] ?>/outbox", - "followers": "<?= $config['url'] ?>/followers", - "publicKey": { - "id": "<?= $config['url'] ?>/actor#main-key", - "owner": "<?= $config['url'] ?>/actor", - "publicKeyPem": "<?= preg_replace('/\n/', '\n', $public_key) ?>" - } -} -<?php - else: - // this is for people who click through to the profile URL in their mastodon client - header('Location: '.$config['url']); - exit(); - endif; -?> diff --git a/lib/activitypub-followers.php b/lib/activitypub-followers.php deleted file mode 100644 index bc21ab5..0000000 --- a/lib/activitypub-followers.php +++ /dev/null @@ -1,70 +0,0 @@ -<?php - -if(!$config['activitypub']) exit('ActivityPub is disabled via config file.'); - -// get total amount -$statement = $db->prepare('SELECT COUNT(id) as total FROM followers WHERE follower_actor IS NOT NULL'); -$statement->execute(); -$followers_total = $statement->fetchAll(PDO::FETCH_ASSOC); -$followers_total = (!empty($followers_total)) ? $followers_total[0]['total'] : 0; - -if(!isset($_GET['page'])): - - $output = [ - '@context' => 'https://www.w3.org/ns/activitystreams', - 'id' => $config['url'].'/followers', - 'type' => 'OrderedCollection', - 'totalItems' => $followers_total, - 'first' => $config['url'].'/followers?page=1', - ]; - - header('Content-Type: application/ld+json'); - echo(json_encode($output, JSON_PRETTY_PRINT | JSON_UNESCAPED_SLASHES)); -else: - - // get items - $items_per_page = 12; // mastodon default? - - // pagination - $current_page = (isset($_GET['page']) && is_numeric($_GET['page'])) ? (int) $_GET['page'] : 1; - $total_pages = ceil($followers_total / $items_per_page); - $offset = ($current_page-1)*$items_per_page; - - if($current_page < 1 || $current_page > $total_pages) { - http_response_code(404); - header('Content-Type: application/ld+json'); - die('{}'); - } - - $statement = $db->prepare('SELECT follower_actor FROM followers WHERE follower_actor IS NOT NULL ORDER BY follower_added ASC LIMIT :limit OFFSET :page'); - $statement->bindValue(':limit', $items_per_page, PDO::PARAM_INT); - $statement->bindValue(':page', $offset, PDO::PARAM_INT); - $statement->execute(); - $followers = $statement->fetchAll(PDO::FETCH_ASSOC); - - $ordered_items = []; - if(!empty($followers)) { - $ordered_items = array_column($followers, 'follower_actor'); - } - - $output = [ - '@context' => 'https://www.w3.org/ns/activitystreams', - 'id' => $config['url'].'/followers?page='.$current_page, - 'type' => 'OrderedCollectionPage', - 'totalItems' => $followers_total, - 'partOf' => $config['url'].'/followers' - ]; - - if($current_page > 1) { - $output['prev'] = $config['url'].'/followers?page='.($current_page-1); - } - - if($current_page < $total_pages) { - $output['next'] = $config['url'].'/followers?page='.($current_page+1); - } - - $output['orderedItems'] = $ordered_items; - - header('Content-Type: application/ld+json'); - echo(json_encode($output, JSON_PRETTY_PRINT | JSON_UNESCAPED_SLASHES)); -endif; diff --git a/lib/activitypub-functions.php b/lib/activitypub-functions.php deleted file mode 100644 index 4b427eb..0000000 --- a/lib/activitypub-functions.php +++ /dev/null @@ -1,521 +0,0 @@ -<?php - -if($config['subdir_install'] == true && $config['activitypub'] == true) { - exit('For ActivityPub to work, you can\'t be running in a subdirectory, sadly.'); -} - -function ap_log($name, $data) { - // file_put_contents(ROOT.DS.'inbox-log.txt', date('H:i:s ').$name.":\n".$data."\n\n", FILE_APPEND | LOCK_EX); -} - -function activitypub_new_key($algo = 'sha512', $bits = 4096, $type = 'rsa') { - global $db; - - $key_type = (mb_strtolower($type) == 'rsa') ? OPENSSL_KEYTYPE_RSA : $type; // todo: improve! - - $rsa = openssl_pkey_new([ - 'digest_alg' => $algo, - 'private_key_bits' => $bits, - 'private_key_type' => $key_type - ]); - openssl_pkey_export($rsa, $private_key); - $public_key = openssl_pkey_get_details($rsa)['key']; - $created = time(); - - try { - $statement = $db->prepare('INSERT INTO keys (key_private, key_public, key_algo, key_bits, key_type, key_created) VALUES (:private, :public, :algo, :bits, :type, :created)'); - - $statement->bindValue(':private', $private_key, PDO::PARAM_STR); - $statement->bindValue(':public', $public_key, PDO::PARAM_STR); - $statement->bindValue(':algo', $algo, PDO::PARAM_STR); - $statement->bindValue(':bits', $bits, PDO::PARAM_INT); - $statement->bindValue(':type', mb_strtolower($type), PDO::PARAM_STR); - $statement->bindValue(':created', $created, PDO::PARAM_INT); - - $statement->execute(); - - } catch(PDOException $e) { - ap_log('ERROR', $e->getMessage()); - return false; - } - - if($db->lastInsertId() > 0) { - return [ - 'id' => $db->lastInsertId(), - 'key_private' => $private_key, - 'key_public' => $public_key, - 'key_algo' => $algo, - 'key_bits' => $bits, - 'key_type' => mb_strtolower($type), - 'key_created' => $created - ]; - } - return false; -} - -function activitypub_get_key($type = 'public') { - global $db; - - $sql = ''; - - if($type == 'public') { - $sql = 'SELECT key_public FROM keys ORDER BY key_created DESC LIMIT 1'; - } elseif($type == 'private') { - $sql = 'SELECT key_private FROM keys ORDER BY key_created DESC LIMIT 1'; - } else { - $sql = 'SELECT * FROM keys ORDER BY key_created DESC LIMIT 1'; - } - - try { - $statement = $db->prepare($sql); - - $statement->execute(); - } catch(PDOException $e) { - ap_log('ERROR', $e->getMessage()); - return false; - } - - $key = $statement->fetch(PDO::FETCH_ASSOC); - - if(!empty($key)) { - if($type == 'public') { - return $key['key_public']; - } elseif($type == 'private') { - return $key['key_private']; - } else { - return $key; - } - } - - return false; -} - -function activitypub_get_actor_url($handle, $full_profile = false) { - list($user, $host) = explode('@', ltrim($handle, '@')); - - $ch = curl_init(); - - $url = sprintf('https://%s/.well-known/webfinger?resource=acct%%3A%s', $host, urlencode($user.'@'.$host)); - - curl_setopt($ch, CURLOPT_URL, $url); - curl_setopt($ch, CURLOPT_RETURNTRANSFER, true); - - $server_response = curl_exec($ch); - // ap_log('WEBFINGER RESPONSE', $server_response); - - curl_close($ch); - - $profile = json_decode($server_response, true); - if($full_profile) { - return $profile; - } - - // make this more robust by iterating over links where href = self? - return $profile['links'][1]['href']; -} - -function activitypub_get_actor_data($actor_url='') { - if(empty($actor_url)) return false; - - $opts = [ - "http" => [ - "method" => "GET", - "header" => join("\r\n", [ - "Accept: application/activity+json", - "Content-type: application/activity+json", - ]) - ] - ]; - - $context = stream_context_create($opts); - - $file = @file_get_contents($actor_url, false, $context); // fix? - - if(!empty($file)) { - return json_decode($file, true); - } - - return false; -} - -function activitypub_plaintext($path, $host, $date, $digest, $type='application/activity+json'): string { - $plaintext = sprintf( - "(request-target): post %s\nhost: %s\ndate: %s\ndigest: %s\ncontent-type: %s", - $path, - $host, - $date, - $digest, - $type - ); - - // ap_log('PLAINTEXT', $plaintext); - - return $plaintext; -} - -function activitypub_digest(string $data): string { - return sprintf('SHA-256=%s', base64_encode(hash('sha256', $data, true))); -} - -function activitypub_sign($path, $host, $date, $digest): string { - $private_key = activitypub_get_key('private'); - - openssl_sign(activitypub_plaintext($path, $host, $date, $digest), $signature, openssl_get_privatekey($private_key), OPENSSL_ALGO_SHA256); - - return $signature; -} - -function activitypub_verify(string $signature, string $pubkey, string $plaintext): bool { - return openssl_verify($plaintext, base64_decode($signature), $pubkey, OPENSSL_ALGO_SHA256); -} - -function activitypub_send_request($host, $path, $data): void { - global $config; - - $encoded = json_encode($data); - - $date = gmdate('D, d M Y H:i:s T', time()); - $digest = activitypub_digest($encoded); - - $signature = activitypub_sign( - $path, - $host, - $date, - $digest - ); - - $signature_header = sprintf( - 'keyId="%s",algorithm="rsa-sha256",headers="(request-target) host date digest content-type",signature="%s"', - $config['url'].'/actor#main-key', - base64_encode($signature) - ); - - // DEBUG - $fp = fopen(ROOT.DS.'inbox-log.txt', 'a'); - - $curl_headers = [ - 'Content-Type: application/activity+json', - 'Date: ' . $date, - 'Signature: ' . $signature_header, - 'Digest: ' . $digest - ]; - - ap_log('SEND MESSAGE', json_encode([$data, $curl_headers], JSON_PRETTY_PRINT)); - - $ch = curl_init(); - - curl_setopt($ch, CURLOPT_URL, sprintf('https://%s%s', $host, $path)); - curl_setopt($ch, CURLOPT_POST, 1); - curl_setopt($ch, CURLOPT_POSTFIELDS, $encoded); - curl_setopt($ch, CURLOPT_HTTPHEADER, $curl_headers); - curl_setopt($ch, CURLOPT_HEADER, 1); - curl_setopt($ch, CURLOPT_VERBOSE, false); - curl_setopt($ch, CURLOPT_STDERR, $fp); - curl_setopt($ch, CURLOPT_RETURNTRANSFER, true); - - $server_output = curl_exec($ch); - - curl_close($ch); - fclose($fp); - - ap_log('SERVER RESPONSE', $server_output); -} - -function activitypub_activity_from_post($post, $json=false) { - global $config; - - if(empty($post)) return false; - - $output = [ - '@context' => 'https://www.w3.org/ns/activitystreams', - - 'id' => $config['url'].'/'.$post['id'].'/json', - 'type' => 'Create', - 'actor' => $config['url'].'/actor', - 'to' => ['https://www.w3.org/ns/activitystreams#Public'], - 'cc' => [$config['url'].'/followers'], - 'object' => [ - 'id' => $config['url'].'/'.$post['id'], - 'type' => 'Note', - 'published' => gmdate('Y-m-d\TH:i:s\Z', $post['post_timestamp']), - 'attributedTo' => $config['url'].'/actor', - 'content' => filter_tags($post['post_content']), - 'to' => ['https://www.w3.org/ns/activitystreams#Public'] - ] - ]; - - $attachments = db_get_attached_files($post['id']); - - if(!empty($attachments) && !empty($attachments[$post['id']])) { - $output['object']['attachment'] = []; - - foreach ($attachments[$post['id']] as $key => $a) { - if(strpos($a['file_mime_type'], 'image') !== 0) continue; // skip non-image files - - $url = $config['url'] .'/'. get_file_path($a); - - $output['object']['attachment'][] = [ - 'type' => 'Image', - 'mediaType' => $a['file_mime_type'], - 'url' => $url, - 'name' => null - ]; - } - } - - if ($json) { - return json_encode($output, JSON_PRETTY_PRINT | JSON_UNESCAPED_SLASHES); - } - - return $output; -} - -function activitypub_notify_followers($post_id): void { - global $db; - // todo: make this a queue - - // API ENDPOINTS - // https://mastodon.social/api/v2/instance - - // users without shared inbox - $statement = $db->prepare('SELECT * FROM followers WHERE follower_shared_inbox IS NULL'); - $statement->execute(); - $followers = $statement->fetchAll(PDO::FETCH_ASSOC); - - // users with shared inbox - $statement = $db->prepare('SELECT follower_shared_inbox as shared_inbox, GROUP_CONCAT(follower_name) as shared_inbox_followers FROM followers WHERE follower_shared_inbox IS NOT NULL GROUP BY follower_shared_inbox'); - $statement->execute(); - $shared_inboxes = $statement->fetchAll(PDO::FETCH_ASSOC); - - // get the activity data, eg. https://microblog.oelna.de/11/json - $post = db_select_post($post_id); - $post_activity = activitypub_activity_from_post($post); - - $update = [ - 'id' => null, - 'inbox' => null, - 'actor' => null - ]; - - // prepare db for possible updates - $statement = $db->prepare('UPDATE followers SET follower_inbox = :inbox, follower_actor = :actor WHERE id = :id'); - $statement->bindParam(':id', $update['id'], PDO::PARAM_INT); - $statement->bindParam(':inbox', $update['inbox'], PDO::PARAM_STR); - $statement->bindParam(':actor', $update['actor'], PDO::PARAM_STR); - - // iterate over shared inboxes to deliver those quickly - foreach($shared_inboxes as $inbox) { - $info = parse_url($inbox['shared_inbox']); - // ap_log('SHARED_INBOX_DELIVERY', json_encode([$inbox, $info, $post_activity], JSON_PRETTY_PRINT)); - // todo: verify we don't need to handle single usernames here - // using the followers URL as CC is enough? - activitypub_send_request($info['host'], $info['path'], $post_activity); - } - - // iterate over followers and send create activity - foreach($followers as $follower) { - - // retrieve actor info, if missing (is this necessary?) - if(empty($follower['follower_inbox'])) { - - $actor_url = activitypub_get_actor_url($follower['follower_name'].'@'.$follower['follower_host']); - if (empty($actor_url)) continue; - - $actor_data = activitypub_get_actor_data($actor_url); - if (empty($actor_data) || empty($actor_data['inbox'])) continue; - - // cache this info - $update['id'] = $follower['id']; - $update['inbox'] = $actor_data['inbox']; - $update['actor'] = $actor_url; - - try { - $statement->execute(); - } catch(PDOException $e) { - continue; - } - - $follower['follower_inbox'] = $actor_data['inbox']; - } - - $info = parse_url($follower['follower_inbox']); - - activitypub_send_request($info['host'], $info['path'], $post_activity); - - ap_log('SENDING TO', json_encode([$info['host'], $info['path']], JSON_PRETTY_PRINT)); - } -} - -function activitypub_post_from_url($url="") { - // todo: this should be more robust and conform to url scheme on this site - - $path = parse_url($url, PHP_URL_PATH); - - $items = explode('/', $path); - $post_id = end($items); - - if (is_numeric($post_id)) { - return (int) $post_id; - } - - return false; -} - -function activitypub_do($type, $user, $host, $post_id) { - if (empty($type)) return false; - - global $db; - - $activity = [ - 'actor_name' => $user, - 'actor_host' => $host, - 'type' => (mb_strtolower($type) == 'like') ? 'like' : 'announce', - 'object_id' => (int) $post_id, - 'updated' => time() - ]; - - try { - $statement = $db->prepare('INSERT OR IGNORE INTO activities (activity_actor_name, activity_actor_host, activity_type, activity_object_id, activity_updated) VALUES (:actor_name, :actor_host, :type, :object_id, :updated)'); - - $statement->bindValue(':actor_name', $activity['actor_name'], PDO::PARAM_STR); - $statement->bindValue(':actor_host', $activity['actor_host'], PDO::PARAM_STR); - $statement->bindValue(':type', $activity['type'], PDO::PARAM_STR); - $statement->bindValue(':object_id', $activity['object_id'], PDO::PARAM_INT); - $statement->bindValue(':updated', $activity['updated'], PDO::PARAM_INT); - - $statement->execute(); - - } catch(PDOException $e) { - print 'Exception : '.$e->getMessage(); - ap_log('ERROR', $e->getMessage()); - return false; - } - - ap_log('INSERTED ACTIVITY', json_encode([$activity, $db->lastInsertId()], JSON_PRETTY_PRINT)); - return $db->lastInsertId(); -} - -function activitypub_undo($type, $user, $host, $post_id) { - if (empty($type)) return false; - - global $db; - - $activity = [ - 'actor_name' => $user, - 'actor_host' => $host, - 'type' => (mb_strtolower($type) == 'like') ? 'like' : 'announce', // todo: make this safer - 'object_id' => (int) $post_id - ]; - try { - $statement = $db->prepare('DELETE FROM activities WHERE activity_actor_name = :actor_name AND activity_actor_host = :actor_host AND activity_type = :type AND activity_object_id = :object_id'); - $statement->bindValue(':actor_name', $activity['actor_name'], PDO::PARAM_STR); - $statement->bindValue(':actor_host', $activity['actor_host'], PDO::PARAM_STR); - $statement->bindValue(':type', $activity['type'], PDO::PARAM_STR); - $statement->bindValue(':object_id', $activity['object_id'], PDO::PARAM_INT); - - $statement->execute(); - } catch(PDOException $e) { - print 'Exception : '.$e->getMessage(); - ap_log('ERROR', $e->getMessage()); - return false; - } - - ap_log('SQL DELETE', json_encode([$statement->rowCount()])); - return true; - return $statement->rowCount(); -} - -function activitypub_update_post($post_id) { - // https://www.w3.org/TR/activitypub/#update-activity-inbox -} - -function activitypub_delete_user($name, $host) { - if(empty($name) || empty($host)) return false; - - global $db; - - // delete all records of user as follower - try { - $statement = $db->prepare('DELETE FROM followers WHERE follower_name = :actor_name AND follower_host = :actor_host'); - $statement->bindValue(':actor_name', $name, PDO::PARAM_STR); - $statement->bindValue(':actor_host', $host, PDO::PARAM_STR); - - $statement->execute(); - } catch(PDOException $e) { - print 'Exception : '.$e->getMessage(); - ap_log('ERROR', $e->getMessage()); - return false; - } - - // remove likes and boosts - try { - $statement = $db->prepare('DELETE FROM activities WHERE activity_actor_name = :actor_name AND activity_actor_host = :actor_host'); - $statement->bindValue(':actor_name', $name, PDO::PARAM_STR); - $statement->bindValue(':actor_host', $host, PDO::PARAM_STR); - - $statement->execute(); - } catch(PDOException $e) { - print 'Exception : '.$e->getMessage(); - ap_log('ERROR', $e->getMessage()); - return false; - } - - return true; -} - -function activitypub_get_post_stats($type="like", $post_id=null) { - global $db; - if(empty($db)) return false; - if(empty($post_id)) return false; - - // normalize type input, liberally - if(in_array($type, ['announce', 'announced', 'boost', 'boosts', 'boosted'])) $type = 'announce'; - if($type == 'both' || $type == 'all') $type = 'both'; - if($type !== 'both' && $type !== 'announce') $type = 'like'; - - $type_clause = 'activity_type = "like"'; - if($type == 'both') { - $type_clause = '(activity_type = "like" OR activity_type = "announce")'; - } elseif($type == 'announce') { - $type_clause = 'activity_type = "announce"'; - } - - $sql = 'SELECT activity_type, COUNT(id) AS amount FROM activities WHERE activity_object_id = :post_id AND '.$type_clause.' GROUP BY activity_type ORDER BY activity_type ASC'; - - try { - $statement = $db->prepare($sql); - $statement->bindValue(':post_id', (int) $post_id, PDO::PARAM_INT); - $statement->execute(); - $rows = $statement->fetchAll(PDO::FETCH_ASSOC); - } catch(PDOException $e) { - print 'Exception : '.$e->getMessage(); - return false; - } - - $return = [ - 'announce' => 0, - 'like' => 0 - ]; - - if(!empty($rows)) { - foreach ($rows as $row) { - if($row['activity_type'] == 'announce') { - $return['announce'] = (int) $row['amount']; - } else if($row['activity_type'] == 'like') { - $return['like'] = (int) $row['amount']; - } - } - } - - if($type == 'both') { - return $return; - } elseif($type == 'announce') { - unset($return['like']); - return $return; - } else { - unset($return['announce']); - return $return; - } - - return $return; -} diff --git a/lib/activitypub-inbox.php b/lib/activitypub-inbox.php deleted file mode 100644 index cea4066..0000000 --- a/lib/activitypub-inbox.php +++ /dev/null @@ -1,196 +0,0 @@ -<?php - - // https://paul.kinlan.me/adding-activity-pub-to-your-static-site/ - // https://bovine.readthedocs.io/en/latest/tutorial_server.html - // https://github.com/timmot/activity-pub-tutorial - // https://magazine.joomla.org/all-issues/february-2023/turning-the-joomla-website-into-an-activitypub-server - // https://codeberg.org/mro/activitypub/src/commit/4b1319d5363f4a836f23c784ef780b81bc674013/like.sh#L101 - - // todo: handle account moves - // https://seb.jambor.dev/posts/understanding-activitypub/ - - if(!$config['activitypub']) exit('ActivityPub is disabled via config file.'); - - $postdata = file_get_contents('php://input'); - - if(!empty($postdata)) { - - $data = json_decode($postdata, true); - $inbox = parse_url($config['url'].'/inbox'); - - $request = [ - 'host' => $inbox['host'], - 'path' => $inbox['path'], - 'digest' => $_SERVER['HTTP_DIGEST'], - 'date' => $_SERVER['HTTP_DATE'], - 'length' => $_SERVER['CONTENT_LENGTH'], - 'type' => $_SERVER['CONTENT_TYPE'] - ]; - - header('Content-Type: application/ld+json'); - - ap_log('POSTDATA', $postdata); - // ap_log('REQUEST', json_encode($request)); - - // verify message digest - $digest_verify = activitypub_digest($postdata); - if($digest_verify === $request['digest']) { - // ap_log('DIGEST', 'Passed verification for ' . $digest_verify); - } else { - ap_log('ERROR', json_encode(['digest verification failed!', $request['digest'], $digest_verify], JSON_PRETTY_PRINT)); - } - - // GET ACTOR DETAILS - if(!empty($data) && !empty($data['actor'])) { - $actor = activitypub_get_actor_data($data['actor']); - - if(!empty($actor)) { - $actor_key = $actor['publicKey']; - $info = parse_url($actor['inbox']); - } else { - exit('could not parse actor data'); - } - } else { - exit('no actor provided'); - } - - $signature = []; - $signature_string = $_SERVER['HTTP_SIGNATURE']; - $parts = explode(',', stripslashes($signature_string)); - foreach ($parts as $part) { - $part = trim($part, '"'); - list($k, $v) = explode('=', $part); - $signature[$k] = trim($v, '"'); - } - - // ap_log('SIGNATURE', json_encode($signature)); - // ap_log('ACTOR', json_encode($actor)); - // ap_log('PUBLIC KEY', str_replace("\n", '\n', $actor_key['publicKeyPem'])); - - $plaintext = activitypub_plaintext($request['path'], $request['host'], $request['date'], $request['digest'], $request['type']); - - // verify request signature - $result = activitypub_verify($signature['signature'], $actor_key['publicKeyPem'], $plaintext); - - if($result != 1) { - ap_log('REQUEST', json_encode($request)); - ap_log('SIGNATURE', json_encode($signature)); - ap_log('PUBLIC KEY', str_replace("\n", '\n', $actor_key['publicKeyPem'])); - ap_log('RESULT', json_encode([$result, $plaintext], JSON_PRETTY_PRINT)); - ap_log('SSL ERROR', 'message signature did not match'); - exit('message signature did not match'); - } else { - ap_log('SSL OKAY', json_encode([$request, $signature, $result, $plaintext, $actor_key['publicKeyPem']], JSON_PRETTY_PRINT)); - } - - // message signature was ok, now handle the request - - if(!empty($data['type'])) { - if(mb_strtolower($data['type']) == 'follow') { - // follow - - $accept_data = [ - '@context' => 'https://www.w3.org/ns/activitystreams', - 'id' => sprintf('%s/activity/%s', $config['url'], uniqid()), - 'type' => 'Accept', - 'actor' => sprintf('%s/actor', $config['url']), - 'object' => $data - ]; - - // send back Accept activity - activitypub_send_request($info['host'], $info['path'], $accept_data); - - $now = time(); - $follower = [ - 'name' => $actor['preferredUsername'], - 'host' => $info['host'], - 'actor' => $data['actor'], - 'inbox' => $actor['inbox'], - 'added' => time() - ]; - try { - $statement = $db->prepare('INSERT OR IGNORE INTO followers (follower_name, follower_host, follower_actor, follower_inbox, follower_shared_inbox, follower_added) VALUES (:follower_name, :follower_host, :follower_actor, :follower_inbox, :follower_shared_inbox, :follower_added)'); - - $statement->bindValue(':follower_name', $follower['name'], PDO::PARAM_STR); - $statement->bindValue(':follower_host', $follower['host'], PDO::PARAM_STR); - $statement->bindValue(':follower_actor', $follower['actor'], PDO::PARAM_STR); - $statement->bindValue(':follower_inbox', $follower['inbox'], PDO::PARAM_STR); - $statement->bindValue(':follower_added', $follower['added'], PDO::PARAM_INT); - - // store shared inbox if possible - if(!empty($actor['endpoints']) && !empty($actor['endpoints']['sharedInbox'])) { - $statement->bindValue(':follower_shared_inbox', $actor['endpoints']['sharedInbox'], PDO::PARAM_STR); - } else { - $statement->bindValue(':follower_shared_inbox', null, PDO::PARAM_NULL); - } - - $statement->execute(); - - } catch(PDOException $e) { - print 'Exception : '.$e->getMessage(); - ap_log('ERROR FOLLOWING', $e->getMessage()); - } - - ap_log('FOLLOW', json_encode([$actor['inbox'], $info, $accept_data], JSON_PRETTY_PRINT)); - - } elseif(mb_strtolower($data['type']) == 'like') { - // like/favorite - ap_log('LIKE', json_encode([$actor['inbox'], $info, $data], JSON_PRETTY_PRINT)); - $post_id = activitypub_post_from_url($data['object']); - activitypub_do('like', $actor['preferredUsername'], $info['host'], $post_id); - } elseif(mb_strtolower($data['type']) == 'announce') { - // boost - ap_log('ANNOUNCE/BOOST', json_encode([$actor['inbox'], $info, $data], JSON_PRETTY_PRINT)); - $post_id = activitypub_post_from_url($data['object']); - activitypub_do('announce', $actor['preferredUsername'], $info['host'], $post_id); - } elseif(mb_strtolower($data['type']) == 'undo') { - if(mb_strtolower($data['object']['type']) == 'follow') { - // undo follow - - ap_log('UNDO FOLLOW', json_encode([$plaintext])); - - // remove from db - $follower = [ - 'name' => $actor['preferredUsername'], - 'host' => $info['host'] - ]; - - try { - $statement = $db->prepare('DELETE FROM followers WHERE follower_name = :name AND follower_host = :host'); - $statement->bindValue(':name', $follower['name'], PDO::PARAM_STR); - $statement->bindValue(':host', $follower['host'], PDO::PARAM_STR); - - $statement->execute(); - } catch(PDOException $e) { - print 'Exception : '.$e->getMessage(); - ap_log('ERROR UNFOLLOWING', $e->getMessage()); - } - - } elseif(mb_strtolower($data['object']['type']) == 'like') { - // undo like - $post_id = activitypub_post_from_url($data['object']['object']); - activitypub_undo('like', $actor['preferredUsername'], $info['host'], $post_id); - ap_log('UNDO LIKE', json_encode([$actor['inbox'], $info, $data], JSON_PRETTY_PRINT)); - } elseif(mb_strtolower($data['object']['type']) == 'announce') { - // undo boost - $post_id = activitypub_post_from_url($data['object']['object']); - activitypub_undo('announce', $actor['preferredUsername'], $info['host'], $post_id); - ap_log('UNDO ANNOUNCE/BOOST', json_encode([$actor['inbox'], $info, $data], JSON_PRETTY_PRINT)); - } - } elseif(mb_strtolower($data['type']) == 'delete') { - // user is to be deleted and all references removed or replaced by Tombstone - // https://www.w3.org/TR/activitypub/#delete-activity-inbox - ap_log('DELETE 1', json_encode(['trying to delete', $data])); - activitypub_delete_user($actor['preferredUsername'], $info['host']); - ap_log('DELETE 2', json_encode([$actor['preferredUsername'], $info['host']])); - } - } - - } else { - - if(file_exists(ROOT.DS.'inbox-log.txt')) { - echo(nl2br(file_get_contents(ROOT.DS.'inbox-log.txt'))); - } else { - echo('no inbox activity'); - } - } diff --git a/lib/activitypub-outbox.php b/lib/activitypub-outbox.php deleted file mode 100644 index 242c695..0000000 --- a/lib/activitypub-outbox.php +++ /dev/null @@ -1,72 +0,0 @@ -<?php - -if(!$config['activitypub']) exit('ActivityPub is disabled via config file.'); - -$posts_per_page = 20; // 20 is mastodon default? -$posts_total = db_posts_count(); // get total amount of posts -$total_pages = ceil($posts_total / $posts_per_page); - -if(!isset($_GET['page'])): - - $output = [ - '@context' => 'https://www.w3.org/ns/activitystreams', - 'id' => $config['url'].'/outbox', - 'type' => 'OrderedCollection', - 'totalItems' => $posts_total, - 'first' => $config['url'].'/outbox?page=1', - 'last' => $config['url'].'/outbox?page='.$total_pages - ]; - - header('Content-Type: application/ld+json'); - echo(json_encode($output, JSON_PRETTY_PRINT | JSON_UNESCAPED_SLASHES)); -else: - - // pagination - $current_page = (isset($_GET['page']) && is_numeric($_GET['page'])) ? (int) $_GET['page'] : 1; - $offset = ($current_page-1)*$posts_per_page; - - if($current_page < 1 || $current_page > $total_pages) { - http_response_code(404); - header('Content-Type: application/ld+json'); - die('{}'); - } - - $posts = db_select_posts(NOW, $posts_per_page, 'desc', $offset); - - $ordered_items = []; - if(!empty($posts)) { - foreach ($posts as $post) { - - $item = []; - $item['id'] = $config['url'].'/'.$post['id'].'/json'; - $item['type'] = 'Create'; - $item['actor'] = $config['url'].'/actor'; - $item['published'] = gmdate('Y-m-d\TH:i:s\Z', $post['post_timestamp']); - $item['to'] = ['https://www.w3.org/ns/activitystreams#Public']; - $item['cc'] = [$config['url'].'/followers']; - $item['object'] = $config['url'].'/'.$post['id'].'/'; - - $ordered_items[] = $item; - } - } - - $output = [ - '@context' => 'https://www.w3.org/ns/activitystreams', - 'id' => $config['url'].'/outbox?page='.$current_page, - 'type' => 'OrderedCollectionPage', - 'partOf' => $config['url'].'/outbox' - ]; - - if($current_page > 1) { - $output['prev'] = $config['url'].'/outbox?page='.($current_page-1); - } - - if($current_page < $total_pages) { - $output['next'] = $config['url'].'/outbox?page='.($current_page+1); - } - - $output['orderedItems'] = $ordered_items; - - header('Content-Type: application/ld+json'); - echo(json_encode($output, JSON_PRETTY_PRINT | JSON_UNESCAPED_SLASHES)); -endif; diff --git a/lib/activitypub-webfinger.php b/lib/activitypub-webfinger.php deleted file mode 100644 index 15177cb..0000000 --- a/lib/activitypub-webfinger.php +++ /dev/null @@ -1,24 +0,0 @@ -<?php - - if(!$config['activitypub']) exit(); - - // todo: handle empty usernames - - $actor = ltrim($config['microblog_account'], '@'); - $url = parse_url($config['url']); - $domain = $url['host']; - if(!empty($url['path'])) $domain .= rtrim($url['path'], '/'); - - $data = [ - 'subject' => 'acct:'.$actor.'@'.$domain, - 'links' => [ - [ - 'rel' => 'self', - 'type' => 'application/activity+json', - 'href' => $config['url'].'/actor' - ] - ] - ]; - - header('Content-Type: application/jrd+json'); - echo(json_encode($data, JSON_PRETTY_PRINT | JSON_UNESCAPED_SLASHES)); diff --git a/lib/database.php b/lib/database.php index e64a80c..5d0fa5c 100644 --- a/lib/database.php +++ b/lib/database.php @@ -2,7 +2,7 @@ //connect or create the database try { - $db = new PDO('sqlite:'.ROOT.DS.'posts.db'); + $db = new PDO($config['database_path']); $db->setAttribute(PDO::ATTR_ERRMODE, PDO::ERRMODE_EXCEPTION); $db->setAttribute(PDO::ATTR_EMULATE_PREPARES, false); diff --git a/lib/functions.php b/lib/functions.php index 6fe13d8..608d473 100644 --- a/lib/functions.php +++ b/lib/functions.php @@ -12,7 +12,7 @@ function check_login() { if(isset($_COOKIE['microblog_login'])) { if($_COOKIE['microblog_login'] === sha1($config['url'].$config['admin_pass'])) { // correct auth data, extend cookie life - $domain = ($_SERVER['HTTP_HOST'] != 'localhost') ? $_SERVER['HTTP_HOST'] : false; + $domain = ($_SERVER['SERVER_NAME'] != 'localhost') ? $_SERVER['HTTP_HOST'] : false; setcookie('microblog_login', sha1($config['url'].$config['admin_pass']), NOW+$config['cookie_life'], '/', $domain, false); return true; @@ -607,25 +607,3 @@ function uuidv4($data = null) { // https://stackoverflow.com/a/15875555/3625228 return vsprintf('%s%s-%s-%s-%s-%s%s%s', str_split(bin2hex($data), 4)); } - -function twitter_post_status($status='') { - global $config; - require_once(ROOT.DS.'lib'.DS.'twitter_api.php'); - - if(empty($status)) return array('errors' => 1); - if(empty($config['twitter']['oauth_access_token']) || - empty($config['twitter']['oauth_access_token_secret']) || - empty($config['twitter']['consumer_key']) || - empty($config['twitter']['consumer_secret'])) return array('errors' => 2); - - $url = 'https://api.twitter.com/1.1/statuses/update.json'; - $postfields = array( - 'status' => $status, - 'trim_user' => 1 - ); - - $twitter = new TwitterAPIExchange($config['twitter']); - return $twitter->buildOauth($url, 'POST')->setPostfields($postfields)->performRequest(); -} - -require_once(__DIR__.DS.'activitypub-functions.php'); diff --git a/lib/twitter_api.php b/lib/twitter_api.php deleted file mode 100644 index d6e3a58..0000000 --- a/lib/twitter_api.php +++ /dev/null @@ -1,410 +0,0 @@ -<?php - -/** - * Twitter-API-PHP : Simple PHP wrapper for the v1.1 API - * - * PHP version 5.3.10 - * - * @category Awesomeness - * @package Twitter-API-PHP - * @author James Mallison <me@j7mbo.co.uk> - * @license MIT License - * @version 1.0.4 - * @link http://github.com/j7mbo/twitter-api-php - */ -class TwitterAPIExchange -{ - /** - * @var string - */ - private $oauth_access_token; - - /** - * @var string - */ - private $oauth_access_token_secret; - - /** - * @var string - */ - private $consumer_key; - - /** - * @var string - */ - private $consumer_secret; - - /** - * @var array - */ - private $postfields; - - /** - * @var string - */ - private $getfield; - - /** - * @var mixed - */ - protected $oauth; - - /** - * @var string - */ - public $url; - - /** - * @var string - */ - public $requestMethod; - - /** - * The HTTP status code from the previous request - * - * @var int - */ - protected $httpStatusCode; - - /** - * Create the API access object. Requires an array of settings:: - * oauth access token, oauth access token secret, consumer key, consumer secret - * These are all available by creating your own application on dev.twitter.com - * Requires the cURL library - * - * @throws \RuntimeException When cURL isn't loaded - * @throws \InvalidArgumentException When incomplete settings parameters are provided - * - * @param array $settings - */ - public function __construct(array $settings) - { - if (!function_exists('curl_init')) - { - throw new RuntimeException('TwitterAPIExchange requires cURL extension to be loaded, see: http://curl.haxx.se/docs/install.html'); - } - - if (!isset($settings['oauth_access_token']) - || !isset($settings['oauth_access_token_secret']) - || !isset($settings['consumer_key']) - || !isset($settings['consumer_secret'])) - { - throw new InvalidArgumentException('Incomplete settings passed to TwitterAPIExchange'); - } - - $this->oauth_access_token = $settings['oauth_access_token']; - $this->oauth_access_token_secret = $settings['oauth_access_token_secret']; - $this->consumer_key = $settings['consumer_key']; - $this->consumer_secret = $settings['consumer_secret']; - } - - /** - * Set postfields array, example: array('screen_name' => 'J7mbo') - * - * @param array $array Array of parameters to send to API - * - * @throws \Exception When you are trying to set both get and post fields - * - * @return TwitterAPIExchange Instance of self for method chaining - */ - public function setPostfields(array $array) - { - if (!is_null($this->getGetfield())) - { - throw new Exception('You can only choose get OR post fields (post fields include put).'); - } - - if (isset($array['status']) && substr($array['status'], 0, 1) === '@') - { - $array['status'] = sprintf("\0%s", $array['status']); - } - - foreach ($array as $key => &$value) - { - if (is_bool($value)) - { - $value = ($value === true) ? 'true' : 'false'; - } - } - - $this->postfields = $array; - - // rebuild oAuth - if (isset($this->oauth['oauth_signature'])) - { - $this->buildOauth($this->url, $this->requestMethod); - } - - return $this; - } - - /** - * Set getfield string, example: '?screen_name=J7mbo' - * - * @param string $string Get key and value pairs as string - * - * @throws \Exception - * - * @return \TwitterAPIExchange Instance of self for method chaining - */ - public function setGetfield($string) - { - if (!is_null($this->getPostfields())) - { - throw new Exception('You can only choose get OR post / post fields.'); - } - - $getfields = preg_replace('/^\?/', '', explode('&', $string)); - $params = array(); - - foreach ($getfields as $field) - { - if ($field !== '') - { - list($key, $value) = explode('=', $field); - $params[$key] = $value; - } - } - - $this->getfield = '?' . http_build_query($params, '', '&'); - - return $this; - } - - /** - * Get getfield string (simple getter) - * - * @return string $this->getfields - */ - public function getGetfield() - { - return $this->getfield; - } - - /** - * Get postfields array (simple getter) - * - * @return array $this->postfields - */ - public function getPostfields() - { - return $this->postfields; - } - - /** - * Build the Oauth object using params set in construct and additionals - * passed to this method. For v1.1, see: https://dev.twitter.com/docs/api/1.1 - * - * @param string $url The API url to use. Example: https://api.twitter.com/1.1/search/tweets.json - * @param string $requestMethod Either POST or GET - * - * @throws \Exception - * - * @return \TwitterAPIExchange Instance of self for method chaining - */ - public function buildOauth($url, $requestMethod) - { - if (!in_array(strtolower($requestMethod), array('post', 'get', 'put', 'delete'))) - { - throw new Exception('Request method must be either POST, GET or PUT or DELETE'); - } - - $consumer_key = $this->consumer_key; - $consumer_secret = $this->consumer_secret; - $oauth_access_token = $this->oauth_access_token; - $oauth_access_token_secret = $this->oauth_access_token_secret; - - $oauth = array( - 'oauth_consumer_key' => $consumer_key, - 'oauth_nonce' => time(), - 'oauth_signature_method' => 'HMAC-SHA1', - 'oauth_token' => $oauth_access_token, - 'oauth_timestamp' => time(), - 'oauth_version' => '1.0' - ); - - $getfield = $this->getGetfield(); - - if (!is_null($getfield)) - { - $getfields = str_replace('?', '', explode('&', $getfield)); - - foreach ($getfields as $g) - { - $split = explode('=', $g); - - /** In case a null is passed through **/ - if (isset($split[1])) - { - $oauth[$split[0]] = urldecode($split[1]); - } - } - } - - $postfields = $this->getPostfields(); - - if (!is_null($postfields)) { - foreach ($postfields as $key => $value) { - $oauth[$key] = $value; - } - } - - $base_info = $this->buildBaseString($url, $requestMethod, $oauth); - $composite_key = rawurlencode($consumer_secret) . '&' . rawurlencode($oauth_access_token_secret); - $oauth_signature = base64_encode(hash_hmac('sha1', $base_info, $composite_key, true)); - $oauth['oauth_signature'] = $oauth_signature; - - $this->url = $url; - $this->requestMethod = $requestMethod; - $this->oauth = $oauth; - - return $this; - } - - /** - * Perform the actual data retrieval from the API - * - * @param boolean $return If true, returns data. This is left in for backward compatibility reasons - * @param array $curlOptions Additional Curl options for this request - * - * @throws \Exception - * - * @return string json If $return param is true, returns json data. - */ - public function performRequest($return = true, $curlOptions = array()) - { - if (!is_bool($return)) - { - throw new Exception('performRequest parameter must be true or false'); - } - - $header = array($this->buildAuthorizationHeader($this->oauth), 'Expect:'); - - $getfield = $this->getGetfield(); - $postfields = $this->getPostfields(); - - if (in_array(strtolower($this->requestMethod), array('put', 'delete'))) - { - $curlOptions[CURLOPT_CUSTOMREQUEST] = $this->requestMethod; - } - - $options = $curlOptions + array( - CURLOPT_HTTPHEADER => $header, - CURLOPT_HEADER => false, - CURLOPT_URL => $this->url, - CURLOPT_RETURNTRANSFER => true, - CURLOPT_TIMEOUT => 10, - ); - - if (!is_null($postfields)) - { - $options[CURLOPT_POSTFIELDS] = http_build_query($postfields, '', '&'); - } - else - { - if ($getfield !== '') - { - $options[CURLOPT_URL] .= $getfield; - } - } - - $feed = curl_init(); - curl_setopt_array($feed, $options); - $json = curl_exec($feed); - - $this->httpStatusCode = curl_getinfo($feed, CURLINFO_HTTP_CODE); - - if (($error = curl_error($feed)) !== '') - { - curl_close($feed); - - throw new \Exception($error); - } - - curl_close($feed); - - return $json; - } - - /** - * Private method to generate the base string used by cURL - * - * @param string $baseURI - * @param string $method - * @param array $params - * - * @return string Built base string - */ - private function buildBaseString($baseURI, $method, $params) - { - $return = array(); - ksort($params); - - foreach($params as $key => $value) - { - $return[] = rawurlencode($key) . '=' . rawurlencode($value); - } - - return $method . "&" . rawurlencode($baseURI) . '&' . rawurlencode(implode('&', $return)); - } - - /** - * Private method to generate authorization header used by cURL - * - * @param array $oauth Array of oauth data generated by buildOauth() - * - * @return string $return Header used by cURL for request - */ - private function buildAuthorizationHeader(array $oauth) - { - $return = 'Authorization: OAuth '; - $values = array(); - - foreach($oauth as $key => $value) - { - if (in_array($key, array('oauth_consumer_key', 'oauth_nonce', 'oauth_signature', - 'oauth_signature_method', 'oauth_timestamp', 'oauth_token', 'oauth_version'))) { - $values[] = "$key=\"" . rawurlencode($value) . "\""; - } - } - - $return .= implode(', ', $values); - return $return; - } - - /** - * Helper method to perform our request - * - * @param string $url - * @param string $method - * @param string $data - * @param array $curlOptions - * - * @throws \Exception - * - * @return string The json response from the server - */ - public function request($url, $method = 'get', $data = null, $curlOptions = array()) - { - if (strtolower($method) === 'get') - { - $this->setGetfield($data); - } - else - { - $this->setPostfields($data); - } - - return $this->buildOauth($url, $method)->performRequest(true, $curlOptions); - } - - /** - * Get the HTTP status code for the previous request - * - * @return integer - */ - public function getHttpStatusCode() - { - return $this->httpStatusCode; - } -} |