activitypub-1.0.x-dev/src/Services/Reader.php

src/Services/Reader.php
<?php

namespace Drupal\activitypub\Services;

use ActivityPhp\Server;
use Drupal\activitypub\Entity\ActivityPubActivityInterface;
use Drupal\Core\Datetime\DateFormatterInterface;
use Drupal\Core\Entity\EntityFormBuilderInterface;
use Drupal\Core\Entity\EntityTypeManagerInterface;
use Drupal\Core\Link;
use Drupal\Core\Pager\PagerManagerInterface;
use Drupal\Core\Session\AccountInterface;
use Drupal\Core\Site\Settings;
use Drupal\Core\StringTranslation\StringTranslationTrait;
use Drupal\Core\Url;
use Drupal\reader\ReaderBase;
use Symfony\Component\HttpFoundation\RequestStack;

class Reader extends ReaderBase {

  use StringTranslationTrait;

  /**
   * The ActivityPub Actor storage.
   *
   * @var \Drupal\activitypub\Entity\Storage\ActivityPubActorStorageInterface
   */
  protected $actorStorage;

  /**
   * The current user.
   *
   * @var \Drupal\Core\Session\AccountInterface
   */
  protected $currentUser;

  /**
   * The pager manager.
   *
   * @var \Drupal\Core\Pager\PagerManagerInterface
   */
  protected $pagerManager;

  /**
   * The request stack
   *
   * @var \Symfony\Component\HttpFoundation\RequestStack $requestStack
   */
  protected $requestStack;

  /**
   * The entity type manager.
   *
   * @var \Drupal\Core\Entity\EntityTypeManagerInterface
   */
  protected $entityTypeManager;

  /**
   * The entity form builder.
   *
   * @var \Drupal\Core\Entity\EntityFormBuilderInterface
   */
  protected $entityFormBuilder;

  /**
   * @var \Drupal\Core\Datetime\DateFormatInterface
   */
  protected $dateFormatter;

  /**
   * The ActivityPub Utility service.
   *
   * @var \Drupal\activitypub\Services\ActivityPubUtilityInterface
   */
  protected $activityPubUtility;

  /**
   * The ActivityPub Media cache service.
   *
   * @var \Drupal\activitypub\Services\ActivityPubMediaCacheInterface
   */
  protected $activityPubMediaCache;

  /**
   * ActivityPubFormAlter constructor
   *
   * @param \Drupal\Core\Entity\EntityTypeManagerInterface $entity_type_manager
   *   The entity type manager.
   * @param \Drupal\Core\Entity\EntityFormBuilderInterface $entity_form_builder
   *   The entity form builder.
   * @param \Drupal\Core\Session\AccountInterface $current_user
   *   The current user.
   * @param \Symfony\Component\HttpFoundation\RequestStack $request_stack
   *   The request stack.
   * @param \Drupal\Core\Pager\PagerManagerInterface $pager_manager
   *   The pager manager.
   * @param \Drupal\Core\Datetime\DateFormatterInterface $date_formatter
   *   The date formatter.
   * @param \Drupal\activitypub\Services\ActivityPubUtilityInterface $activitypub_utility
   *   The ActivityPub utility service.
   * @param \Drupal\activitypub\Services\ActivityPubMediaCacheInterface $activitypub_media_cache
   *   The ActivityPub media cache service.
   *
   * @throws \Drupal\Component\Plugin\Exception\InvalidPluginDefinitionException
   * @throws \Drupal\Component\Plugin\Exception\PluginNotFoundException
   */
  public function __construct(EntityTypeManagerInterface $entity_type_manager, EntityFormBuilderInterface $entity_form_builder, AccountInterface $current_user, RequestStack $request_stack, PagerManagerInterface $pager_manager, DateFormatterInterface $date_formatter, ActivityPubUtilityInterface $activitypub_utility, ActivityPubMediaCacheInterface $activitypub_media_cache) {
    $this->entityTypeManager = $entity_type_manager;
    $this->entityFormBuilder = $entity_form_builder;
    $this->actorStorage = $entity_type_manager->getStorage('activitypub_actor');
    $this->currentUser = $current_user;
    $this->requestStack = $request_stack;
    $this->pagerManager = $pager_manager;
    $this->dateFormatter = $date_formatter;
    $this->activityPubUtility = $activitypub_utility;
    $this->activityPubMediaCache = $activitypub_media_cache;
  }

  /**
   * {@inheritdoc}
   */
  public function getChannels() {

    if (!$this->getActor()) {
      return [];
    }

    $conditions = [
      'uid' => $this->currentUser->id(),
      'status' => 1,
      'collection' => 'inbox',
      'is_read' => 0,
      'visibility' => [ActivityPubActivityInterface::VISIBILITY_PUBLIC, ActivityPubActivityInterface::VISIBILITY_FOLLOWERS],
      'type' => $this->activityPubUtility->getTimelineTypes(),
      'entity_id' => 'isNull',
    ];

    $following = $this->getFollowees();
    if (!empty($following[0])) {
      $conditions['actor'] = $following[0];
    }
    $unread_home = $this->entityTypeManager->getStorage('activitypub_activity')->getActivityCount($conditions);

    // Direct messages.
    $conditions['visibility'] = ActivityPubActivityInterface::VISIBILITY_PRIVATE;
    unset($conditions['actor']);
    unset($conditions['entity_id']);
    $unread_direct = $this->entityTypeManager->getStorage('activitypub_activity')->getActivityCount($conditions);

    // Notifications.
    $conditions['visibility'] = [ActivityPubActivityInterface::VISIBILITY_PUBLIC, ActivityPubActivityInterface::VISIBILITY_FOLLOWERS];
    $conditions['type'] = $this->activityPubUtility->getNotificationTypes();
    $conditions['entity_id'] = 'isNotNull';
    $unread_notifications = $this->entityTypeManager->getStorage('activitypub_activity')->getActivityCount($conditions);

    return [
      'label' => $this->t('Fediverse'),
      'channels' => [
        (object) [
          'uid' => 'home',
          'unread' => $unread_home,
          'name' => $this->t('Home'),
        ],
        (object) [
          'uid' => 'notifications',
          'unread' => $unread_notifications,
          'name' => $this->t('Notifications'),
        ],
        (object) [
          'uid' => 'direct',
          'unread' => $unread_direct,
          'name' => $this->t('Direct messages'),
        ],
        (object) [
          'uid' => 'local',
          'unread' => 0,
          'name' => $this->t('Local timeline'),
        ],
      ],
    ];
  }

  /**
   * {@inheritdoc}
   */
  public function getSourcesPage($op) {
    switch ($op) {

      case 'add':
        $build = $this->addSourcesButton($this->t('Back to Fediverse'), 'activitypub');
        $activity = $this->entityTypeManager->getStorage('activitypub_activity')->create();
        $form = $this->entityFormBuilder->getForm($activity);
        $form['#action'] .= '?destination=/reader/sources/activitypub';
        $build['add'] = $form;
        break;

      case 'delete':
        $build = $this->addSourcesButton($this->t('Back to Fediverse'), 'activitypub');
        $activity = $this->entityTypeManager->getStorage('activitypub_activity')->load($_GET['id']);
        if (!$activity->isPublished()) {
          $form = $this->entityFormBuilder->getForm($activity, 'delete');
          $form['actions']['cancel']['#access'] = FALSE;
          $form['#action'] .= '&destination=/reader/sources/activitypub';
          $build['delete'] = $form;
        }
        else {
          $build['delete'] = ['#markup' => '<div class="form-actions side-padding">' . $this->t(' This action cannot be undone.') . '</div>'];
          $build['actions'] = [
            '#prefix' => '<div class="form-actions side-padding">',
            '#suffix' => '</div>',
            'delete' => [
              '#type' => 'link',
              '#title' => 'delete',
              '#url' => $activity->toUrl('undo', ['query' => ['destination' => 'reader/sources/activitypub']]),
              '#attributes' => ['class' => ['button']],
            ],
          ];
        }
        $build['delete']['#prefix'] = $this->addSourcesConfirmDeleteText($activity->getObject());
        break;

      case 'followers':
        $build = $this->addSourcesButton($this->t('Back to Fediverse'), 'activitypub');
        $followers_properties = [
          'collection' => 'inbox',
          'type' => 'Follow',
        ];
        $build['table'] = $this->buildUserTable($followers_properties, $this->t('No followers available.'), FALSE);
        break;

      default:
        $build = [];
        $build['follow'] = $this->addSourcesButton($this->t('+ Follow'), 'activitypub', 'add');
        $build['followers'] = $this->addSourcesButton($this->t('+ View followers'), 'activitypub', 'followers');

        $following_properties = [
          'collection' => 'outbox',
          'type' => 'Follow',
        ];
        $build['table'] = $this->buildUserTable($following_properties, $this->t('You are not following anyone.'));

        break;
    }

    $build['#title'] = $this->t('Manage Fediverse');
    return $build;
  }

  /**
   * {@inheritdoc}
   */
  public function getTimelineActions($id) {

    if (!$this->getActor()) {
      return [];
    }

    $status_title = isset($_SESSION['activitypub_status']) ? $this->t('View all items') : $this->t('View unread items');

    return [
      ['action' => 'mark-read', 'title' => $this->t('Mark read')],
      ['action' => 'status', 'title' => $status_title],
    ];
  }

  /**
   * {@inheritdoc}
   */
  public function doTimelineAction($action, $id) {
    if ($this->getActor()) {
      if ($action == 'mark-read') {
        $this->entityTypeManager->getStorage('activitypub_activity')->changeReadStatus(1, $id);
      }
      if ($action == 'status') {
        if (isset($_SESSION['activitypub_status'])) {
          unset($_SESSION['activitypub_status']);
        }
        else {
          $_SESSION['activitypub_status'] = TRUE;
        }
      }
    }
  }

  /**
   * {@inheritdoc}
   */
  public function getPostActions($id, $item) {

    if (!$this->getActor()) {
      return [];
    }

    $actions = [];

    // Mute / unmute.
    if ($id == 'home') {
      $actions[] = [
        'action' => 'mute',
        'title' => $this->t('Mute user')
      ];
    }

    if ($id == 'local') {
      $actors = $this->getFollowees();
      if (isset($item->author->url) && isset($actors[1][$item->author->url])) {
        $actions[] = [
          'action' => 'unmute',
          'title' => $this->t('Unmute user')
        ];
      }
    }

    // Delete Direct message.
    if ($id == 'direct') {
      $actions[] = [
        'action' => 'delete-message',
        'title' => $this->t('Delete message')
      ];
    }

    // Mark read / unread.
    $action = 'mark-unread';
    $title = $this->t('Mark unread');
    if (isset($item->_is_read) && !$item->_is_read) {
      $action = 'mark-read';
      $title = $this->t('Mark read');
    }

    $actions[] = [
      'action' => $action,
      'title' => $title,
    ];

    return $actions;
  }

  /**
   * {@inheritdoc}
   */
  public function doPostAction($action, $id, $items) {

    // Mark read / unread.
    if ($action == 'mark-read' || $action == 'mark-unread') {
      $status = $action == 'mark-read' ? 1 : 0;
      $this->entityTypeManager->getStorage('activitypub_activity')->changeReadStatus($status, $id, $items);
    }

    // Mute / unmute.
    if ($action == 'mute' || $action == 'unmute') {
      $aid = !empty($items[0]) ? $items[0] : 0;
      if (!$aid) {
        return;
      }

      /** @var \Drupal\activitypub\Entity\Storage\ActivityPubActivityStorageInterface $storage */
      $storage = $this->entityTypeManager->getStorage('activitypub_activity');

      // When coming from home or local.
      if ($id == 'home' || $id == 'local') {
        /** @var \Drupal\activitypub\Entity\ActivityPubActivityInterface $activity */
        $activity = $storage->load($aid);
        $follows = $storage->getActivities(['type' => 'Follow', 'object' => $activity->getActor(), 'uid' => $this->currentUser->id()]);
        if (!empty($follows)) {
          $a = array_shift($follows);
          $aid = $a->id();
        }
      }

      /** @var \Drupal\activitypub\Entity\ActivityPubActivityInterface $follow_activity */
      $follow_activity = $this->entityTypeManager->getStorage('activitypub_activity')->load($aid);
      if ($follow_activity) {
        $action == 'mute' ? $follow_activity->mute() : $follow_activity->unMute();
        try {
          $follow_activity->save();
        }
        catch (\Exception $ignored) {}
      }
    }

    // Delete message.
    if ($action == 'delete-message') {
      $aid = !empty($items[0]) ? $items[0] : 0;
      if (!empty($aid)) {
        try {
          $activity = $this->entityTypeManager->getStorage('activitypub_activity')->load($aid);
          if ($activity) {
            $activity->delete();
          }
        }
        catch (\Exception $ignored) {}
      }
    }

  }

  /**
   * {@inheritdoc}
   */
  public function getTimeline($id, $search = NULL) {

    if (!$this->getActor()) {
      return ['items' => []];
    }

    return $this->getResponse($id, $search);
  }

  /**
   * Build user table.
   *
   * @param $query_properties
   * @param $empty_message
   * @param bool $follow
   *
   * @return array
   */
  protected function buildUserTable($query_properties, $empty_message, bool $follow = TRUE) {
    $rows = [];
    $activities = [];
    try {
      /** @var \Drupal\activitypub\Entity\Storage\ActivityPubActivityStorageInterface $storage */
      $storage = $this->entityTypeManager->getStorage('activitypub_activity');
      $activities = $storage->getActivities($query_properties, [['id', 'DESC']], 20);
    }
    catch (\Exception $ignored) {}
    foreach ($activities as $activity) {
      $row = [];

      if ($follow) {
        $row[] = ['data' => ['#markup' => $activity->getObject() . (!$activity->isPublished() ? '<br /><div class="smaller-text">' . $this->t('Waiting for confirmation') . '</div>' : '')]];
        $row[] = Link::fromTextAndUrl($this->t('Unfollow'), Url::fromRoute('reader.sources', [
          'module' => 'activitypub',
          'op' => 'delete',
        ], ['query' => ['id' => $activity->id()]]))->toString();
        $mute_title = $activity->isMuted() ? $this->t('Unmute') : $this->t('Mute');
        $mute_action = $activity->isMuted() ? 'unmute' : 'mute';
        $row[] = Link::fromTextAndUrl($mute_title, Url::fromRoute('reader.post.action', [
          'module' => 'activitypub',
          'action' => $mute_action,
          'id' => 'user-table',
          'post_id' => $activity->id(),
        ], ['query' => \Drupal::destination()->getAsArray()]))->toString();
      }
      else {
        $row[] = Link::fromTextAndUrl($activity->getActor(), Url::fromUri($activity->getActor(), ['attributes' => ['target' => '_blank']]))->toString();
        $row[] = Link::fromTextAndUrl($this->t('Follow'), Url::fromRoute('reader.sources', [
          'module' => 'activitypub',
          'op' => 'add',
        ], ['query' => ['follow' => $activity->getActor(), 'destination' => Url::fromRoute('reader.sources', ['module' => 'activitypub', 'op' => 'followers'])->toString()]]))->toString();
        $row[] = \Drupal::service('date.formatter')->format($activity->getCreatedTime());
      }
      $rows[] = $row;
    }

    $build = [];
    $build['table'] = [
      '#type' => 'table',
      '#rows' => $rows,
      '#empty' => $empty_message,
      '#attributes' => ['class' => ['reader-content-table']],
    ];

    $build['pager'] = [
      '#type' => 'pager',
    ];

    return $build;
  }

  /**
   * Get the actor for the current user.
   *
   * @return bool|NULL|\Drupal\activitypub\Entity\ActivitypubActorInterface
   */
  protected function getActor() {
    if ($this->currentUser->hasPermission('access reader')) {
      return $this->actorStorage->loadActorByEntityIdAndType($this->currentUser->id(), 'person');
    }

    return FALSE;
  }

  /**
   * Get followees.
   *
   * @return array
   *
   * @throws \Drupal\Component\Plugin\Exception\InvalidPluginDefinitionException
   * @throws \Drupal\Component\Plugin\Exception\PluginNotFoundException
   */
  protected function getFollowees() {
    static $loaded = FALSE;
    static $items = [];

    if (!$loaded) {
      $actor = $this->getActor();
      $conditions = ['type' => 'Follow', 'status' => 1];
      $url = Url::fromRoute('activitypub.user.self', ['user' => $actor->getOwnerId(), 'activitypub_actor' => $actor->getName()], ['absolute' => TRUE])->toString();
      $conditions['actor'] = $url;
      $conditions['collection'] = ActivityPubActivityInterface::OUTBOX;
      /** @var \Drupal\activitypub\Entity\Storage\ActivityPubActivityStorageInterface $storage */
      $storage = $this->entityTypeManager->getStorage('activitypub_activity');
      $records = $storage->getActivityRecords($conditions);
      foreach ($records as $record) {
        $items[$record->mute][$record->object] = $record->object;
      }
    }

    return $items;
  }

  /**
   * Get items for a timeline.
   *
   * @param $id
   *   The channel id
   * @param $search
   *   The search param.
   *
   * @return array
   *
   * @throws \Drupal\Component\Plugin\Exception\InvalidPluginDefinitionException
   * @throws \Drupal\Component\Plugin\Exception\PluginNotFoundException
   */
  protected function getResponse($id, $search) {

    $items = [];
    $paging = [];

    $limit = Settings::get('reader_activitypub_timeline_limit', 20);

    // Get page.
    $page = $this->requestStack->getCurrentRequest()->get('page', 0);
    $server = $this->activityPubUtility->getServer();

    /** @var \Drupal\activitypub\Entity\ActivityPubActivityInterface[] $activities */
    $storage = $this->entityTypeManager->getStorage('activitypub_activity');
    $conditions = [
      'uid' => $this->currentUser->id(),
      'status' => 1,
      'collection' => 'inbox',
      'type' => $this->activityPubUtility->getTimelineTypes(),
    ];
    if (isset($_SESSION['activitypub_status']) && empty($search)) {
      $conditions['is_read'] = 0;
    }

    switch ($id) {
      case 'home':
        $conditions['entity_id'] = 'isNull';
        $conditions['visibility'] = [ActivityPubActivityInterface::VISIBILITY_PUBLIC, ActivityPubActivityInterface::VISIBILITY_FOLLOWERS];
        $following = $this->getFollowees();
        if (!empty($following[0])) {
          $conditions['actor'] = $following[0];
        }
        break;
      case 'direct':
        $conditions['visibility'] = ActivityPubActivityInterface::VISIBILITY_PRIVATE;
        break;
      case 'notifications':
        $conditions['type'] = $this->activityPubUtility->getNotificationTypes();
        $conditions['entity_id'] = 'isNotNull';
        break;
      case 'internal-search':
        $conditions['type'] = 'Create';
        $conditions['search'] = $search;
        break;
      default:
        $conditions['entity_id'] = 'isNull';
        $conditions['visibility'] = ActivityPubActivityInterface::VISIBILITY_PUBLIC;
        break;
    }

    $activities = $storage->getActivities($conditions, [['id', 'DESC']], $limit);

    foreach ($activities as $activity) {
      if ($item = $this->buildMicrosubItem($activity, $server, $id)) {
        $items[] = $item;
      }
    }

    $pager = $this->pagerManager->getPager();
    $pager_total = $pager->getTotalPages();
    $page++;
    if ((isset($pager_total) && is_numeric($pager_total) && $pager_total > $page) || $page > 0) {
      $paging = ['after' => $page];
    }

    $response = ['items' => $items];
    if (!empty($paging)) {
      $response['paging'] = (object) $paging;
    }

    return $response;
  }

  /**
   * Build a Microsub item from an activity payload.
   *
   * @param \Drupal\activitypub\Entity\ActivityPubActivityInterface $activity
   * @param \ActivityPhp\Server $server
   * @param string $id
   *
   * @return \stdClass $item
   */
  protected function buildMicrosubItem(ActivityPubActivityInterface $activity, Server $server, string $id) {

    $payload = json_decode($activity->getPayLoad() ?: '', TRUE);
    $context = json_decode($activity->getContext() ?: '', TRUE);

    $item = [];
    $item['_id'] = $activity->id();
    $item['_is_read'] = !($id == 'home' || $id == 'direct' || $id == 'notifications') || $activity->isRead();
    $item['type'] = 'entry';
    if ($activity->isPrivate()) {
      $item['url'] = !empty($payload['object']['id']) ? $payload['object']['id'] : str_replace('/activity', '', $activity->getExternalId());
    }
    else {
      $item['url'] = $activity->getObject();
    }
    $item['published'] = $this->dateFormatter->format($activity->getCreatedTime(), 'custom','Y-m-dTH:i:s');

    // Content and response type.
    if ($activity->getType() == 'Like') {
      $item['like-of'] = [$activity->getObject()];
    }
    elseif ($activity->getType() == 'Announce') {
      $item['repost-of'] = [$activity->getObject()];
    }
    elseif ($activity->getType() == 'Follow') {
      $item['content'] = (object) ['html' => 'This person is now following you!'];
    }
    elseif ($activity->getType() == 'Create') {
      if (!empty($payload['object']) && is_array($payload['object'])) {
        if (!empty($payload['object']['inReplyTo'])) {
          $item['in-reply-to'] = [$payload['object']['inReplyTo']];
        }

        if (!empty($payload['object']['name'])) {
          $item['name'] = $payload['object']['name'];
        }

        if (!empty($payload['object']['content'])) {
          $item['content'] = (object) ['html' => $payload['object']['content']];
        }

        if (!empty($payload['object']['attachment'])) {
          $this->handleAttachments($payload['object']['attachment'], $item);
        }

      }
      else {
        if (!empty($payload['name'])) {
          $item['name'] = $payload['name'];
        }
        else {
          // currently treat as repost
          $item['repost-of'] = [$activity->getObject()];
        }
      }
    }

    // Context.
    if (!empty($context) && (isset($item['repost-of']) || isset($item['like-of']) || isset($item['in-reply-to']))) {
      $url = NULL;
      if (isset($item['repost-of'])) {
        $url = $item['repost-of'][0];
      }
      elseif (isset($item['like-of'])) {
        $url = $item['like-of'][0];
      }
      elseif (isset($item['in-reply-to'])) {
        $url = $item['in-reply-to'][0];
      }

      if (isset($url) && isset($context['id']) && $context['id'] == $url && empty($activity->getTargetEntityId())) {
        $ref = [];
        if (!empty($context['name'])) {
          $ref['name'] = $context['name'];
        }
        if (!empty($context['content'])) {
          $ref['content'] = (object) ['html' => $context['content']];
        }
        if (!empty($context['summary'])) {
          $ref['summary'] = $context['summary'];
        }
        if (!empty($context['attachment'])) {
          $this->handleAttachments($context['attachment'], $ref);
        }

        $item['refs'] = new \stdClass();
        $item['refs']->{$url} = (object) $ref;
      }
    }

    // Author information.
    $name = $photo = '';
    try {
      $target_actor = $server->actor($activity->getActor());
      $name = $target_actor->get('name');
      $icon = $target_actor->get('icon');
      if (!empty($icon)) {
        $photo = $this->activityPubMediaCache->applyImageCache($icon->get('url'));
      }
    }
    catch (\Exception $ignored) {}

    $item['author'] = (object) ['url' => $activity->getActor(), 'name' => $name, 'photo' => $photo];

    return (object) $item;
  }

  /**
   * Handle attachments.
   *
   * @param $attachments
   * @param $o
   */
  protected function handleAttachments($attachments, &$o) {
    foreach ($attachments as $attachment) {
      if (isset($attachment['mediaType'])) {
        switch ($attachment['mediaType']) {
          case 'image/jpg':
          case 'image/png':
          case 'image/jpeg':
          case 'image/gif':
            if (!isset($o['photo'])) {
              $o['photo'] = [];
            }
            $o['photo'][] = $this->activityPubMediaCache->applyImageCache($attachment['url'], 'cache_attachment');
            break;
          case 'video/mp4':
            if (!isset($o['video'])) {
              $o['video'] = [];
            }
            $o['video'][] = $attachment['url'];
            break;
          case 'audio/mp3':
            if (!isset($o['audio'])) {
              $o['audio'] = [];
            }
            $o['audio'][] = $attachment['url'];
            break;
        }
      }
    }
  }

}

Главная | Обратная связь

drupal hosting | друпал хостинг | it patrol .inc