From bf6c0346d8d35a719dd1bff1cb4d573d422f99ff Mon Sep 17 00:00:00 2001 From: Thomas Citharel Date: Wed, 31 May 2017 09:31:18 +0200 Subject: WIP Signed-off-by: Thomas Citharel --- .../Command/CreateAccountsCommand.php | 126 ++++++ .../Controller/InboxController.php | 43 +++ .../Controller/LikedController.php | 19 + .../Controller/MetadataController.php | 69 ++++ .../Controller/OutboxController.php | 23 ++ .../Controller/ProfileController.php | 425 +++++++++++++++++++++ .../Controller/RecommandController.php | 34 ++ .../DependencyInjection/Configuration.php | 25 ++ .../WallabagFederationExtension.php | 26 ++ src/Wallabag/FederationBundle/Entity/Account.php | 307 +++++++++++++++ src/Wallabag/FederationBundle/Entity/Instance.php | 111 ++++++ .../EventListener/CreateAccountListener.php | 54 +++ .../FederationBundle/Federation/CloudId.php | 78 ++++ .../FederationBundle/Form/Type/AccountType.php | 46 +++ .../Repository/AccountRepository.php | 48 +++ .../Repository/InstanceRepository.php | 10 + .../FederationBundle/Resources/config/services.yml | 8 + .../views/themes/material/User/followers.html.twig | 88 +++++ .../views/themes/material/User/profile.html.twig | 77 ++++ .../themes/material/User/profile_header.html.twig | 35 ++ .../themes/material/User/recommendations.html.twig | 4 + .../FederationBundle/WallabagFederationBundle.php | 9 + 22 files changed, 1665 insertions(+) create mode 100644 src/Wallabag/FederationBundle/Command/CreateAccountsCommand.php create mode 100644 src/Wallabag/FederationBundle/Controller/InboxController.php create mode 100644 src/Wallabag/FederationBundle/Controller/LikedController.php create mode 100644 src/Wallabag/FederationBundle/Controller/MetadataController.php create mode 100644 src/Wallabag/FederationBundle/Controller/OutboxController.php create mode 100644 src/Wallabag/FederationBundle/Controller/ProfileController.php create mode 100644 src/Wallabag/FederationBundle/Controller/RecommandController.php create mode 100644 src/Wallabag/FederationBundle/DependencyInjection/Configuration.php create mode 100644 src/Wallabag/FederationBundle/DependencyInjection/WallabagFederationExtension.php create mode 100644 src/Wallabag/FederationBundle/Entity/Account.php create mode 100644 src/Wallabag/FederationBundle/Entity/Instance.php create mode 100644 src/Wallabag/FederationBundle/EventListener/CreateAccountListener.php create mode 100644 src/Wallabag/FederationBundle/Federation/CloudId.php create mode 100644 src/Wallabag/FederationBundle/Form/Type/AccountType.php create mode 100644 src/Wallabag/FederationBundle/Repository/AccountRepository.php create mode 100644 src/Wallabag/FederationBundle/Repository/InstanceRepository.php create mode 100644 src/Wallabag/FederationBundle/Resources/config/services.yml create mode 100644 src/Wallabag/FederationBundle/Resources/views/themes/material/User/followers.html.twig create mode 100644 src/Wallabag/FederationBundle/Resources/views/themes/material/User/profile.html.twig create mode 100644 src/Wallabag/FederationBundle/Resources/views/themes/material/User/profile_header.html.twig create mode 100644 src/Wallabag/FederationBundle/Resources/views/themes/material/User/recommendations.html.twig create mode 100644 src/Wallabag/FederationBundle/WallabagFederationBundle.php (limited to 'src/Wallabag/FederationBundle') diff --git a/src/Wallabag/FederationBundle/Command/CreateAccountsCommand.php b/src/Wallabag/FederationBundle/Command/CreateAccountsCommand.php new file mode 100644 index 00000000..f2ca3b06 --- /dev/null +++ b/src/Wallabag/FederationBundle/Command/CreateAccountsCommand.php @@ -0,0 +1,126 @@ +setName('wallabag:federation:create-accounts') + ->setDescription('Creates missing federation accounts') + ->setHelp('This command creates accounts for federation for missing users') + ->addArgument( + 'username', + InputArgument::OPTIONAL, + 'User to create an account for' + ); + } + + protected function execute(InputInterface $input, OutputInterface $output) + { + $this->output = $output; + + $domainName = $this->getContainer()->getParameter('domain_name'); + $instance = $this->checkInstance($domainName); + + $username = $input->getArgument('username'); + + if ($username) { + try { + $user = $this->getUser($username); + $this->createAccount($user, $instance); + } catch (NoResultException $e) { + $output->writeln(sprintf('User "%s" not found.', $username)); + + return 1; + } + } else { + $users = $this->getDoctrine()->getRepository('WallabagUserBundle:User')->findAll(); + + $output->writeln(sprintf('Creating through %d user federated accounts', count($users))); + + foreach ($users as $user) { + $output->writeln(sprintf('Processing user %s', $user->getUsername())); + $this->createAccount($user, $instance); + } + $output->writeln(sprintf('Creating user federated accounts. %d accounts created in total', $this->created)); + } + + return 0; + } + + /** + * @param User $user + * @param $instance + */ + private function createAccount(User $user, Instance $instance) + { + $em = $this->getContainer()->get('doctrine.orm.entity_manager'); + $repo = $em->getRepository('WallabagFederationBundle:Account'); + + if ($repo->findBy(['user' => $user->getId()])) { + return; + } + + $account = new Account(); + $account->setUsername($user->getUsername()) + ->setUser($user) + ->setServer($instance); + + $em->persist($account); + $em->flush(); + + $user->setAccount($account); + $em->persist($account); + $em->flush(); + + ++$this->created; + } + + private function checkInstance($domainName) + { + $em = $this->getContainer()->get('doctrine.orm.entity_manager'); + $repo = $em->getRepository('WallabagFederationBundle:Instance'); + + $instance = $repo->findOneByDomain($domainName); + if (!$instance) { + $instance = new Instance($domainName); + + $em->persist($instance); + $em->flush(); + } + return $instance; + } + + /** + * Fetches a user from its username. + * + * @param string $username + * + * @return \Wallabag\UserBundle\Entity\User + */ + private function getUser($username) + { + return $this->getDoctrine()->getRepository('WallabagUserBundle:User')->findOneByUserName($username); + } + + private function getDoctrine() + { + return $this->getContainer()->get('doctrine'); + } +} diff --git a/src/Wallabag/FederationBundle/Controller/InboxController.php b/src/Wallabag/FederationBundle/Controller/InboxController.php new file mode 100644 index 00000000..99cfeadf --- /dev/null +++ b/src/Wallabag/FederationBundle/Controller/InboxController.php @@ -0,0 +1,43 @@ +getDoctrine()->getManager(); + $response = new Response(); + + if ($activity = json_decode($request->getContent())) { + if ($activity->type === 'Follow' && isset($activity->actor->id)) { + $cloudId = new CloudId($activity->actor->id); + $account = new Account(); + $account->setServer($cloudId->getRemote()) + ->setUsername($cloudId->getUser()); + $em->persist($account); + $em->flush(); + + $response->setStatusCode(201); + } else { + $response->setStatusCode(400); + } + } + return $response; + } +} diff --git a/src/Wallabag/FederationBundle/Controller/LikedController.php b/src/Wallabag/FederationBundle/Controller/LikedController.php new file mode 100644 index 00000000..3e86d50d --- /dev/null +++ b/src/Wallabag/FederationBundle/Controller/LikedController.php @@ -0,0 +1,19 @@ +setContent(' +generateUrl('webfinger') . '?resource={uri}\"/> +'); + $response->headers->set('Content-Type', 'application/xrd+xml'); + return $response; + } + + /** + * @Route("/.well-known/webfinger", name="webfinger") + * @param Request $request + * @return JsonResponse + */ + public function getWebfingerData(Request $request) + { + $subject = $request->query->get('resource'); + + if (!$subject || strlen($subject) < 12 || 0 !== strpos($subject, 'acct:') || false !== strpos($subject, '@')) { + return new JsonResponse([]); + } + $subjectId = substr($subject, 5); + + $cloudId = $this->getCloudId($subjectId); + + + $data = [ + 'subject' => $subject, + 'aliases' => [$this->generateUrl('profile', $cloudId->getUser())], + 'links' => [ + [ + 'rel' => 'http://webfinger.net/rel/profile-page', + 'type' => 'text/html', + 'href' => $this->generateUrl('profile', $cloudId->getUser()) + ], + ] + ]; + return new JsonResponse($data); + } + + private function getCloudId($subjectId) + { + return new CloudId($subjectId); + } + + + # + # {"subject":"acct:tcit@social.tcit.fr","aliases":["https://social.tcit.fr/@tcit","https://social.tcit.fr/users/tcit"],"links":[{"rel":"http://webfinger.net/rel/profile-page","type":"text/html","href":"https://social.tcit.fr/@tcit"},{"rel":"http://schemas.google.com/g/2010#updates-from","type":"application/atom+xml","href":"https://social.tcit.fr/users/tcit.atom"},{"rel":"self","type":"application/activity+json","href":"https://social.tcit.fr/@tcit"},{"rel":"salmon","href":"https://social.tcit.fr/api/salmon/1"},{"rel":"magic-public-key","href":"data:application/magic-public-key,RSA.pXwYMUdFg3XUd-bGsh8CyiMRGpRGAWuCdM5pDWx5uM4pW2pM3xbHbcI21j9h8BmlAiPg6hbZD73KGly2N8Rt5iIS0I-l6i8kA1JCCdlAaDTRd41RKMggZDoQvjVZQtsyE1VzMeU2kbqqTFN6ew7Hvbd6O0NhixoKoZ5f3jwuBDZoT0p1TAcaMdmG8oqHD97isizkDnRn8cOBA6wtI-xb5xP2zxZMsLpTDZLiKU8XcPKZCw4OfQfmDmKkHtrFb77jCAQj_s_FxjVnvxRwmfhNnWy0D-LUV_g63nHh_b5zXIeV92QZLvDYbgbezmzUzv9UeA1s70GGbaDqCIy85gw9-w==.AQAB"},{"rel":"http://ostatus.org/schema/1.0/subscribe","template":"https://social.tcit.fr/authorize_follow?acct={uri}"}]} + # +} diff --git a/src/Wallabag/FederationBundle/Controller/OutboxController.php b/src/Wallabag/FederationBundle/Controller/OutboxController.php new file mode 100644 index 00000000..87ebdba1 --- /dev/null +++ b/src/Wallabag/FederationBundle/Controller/OutboxController.php @@ -0,0 +1,23 @@ +getAcceptableContentTypes(), true)) { + $data = [ + '@context' => 'https://www.w3.org/ns/activitystreams', + 'type' => 'Person', + 'id' => CloudId::getCloudIdFromAccount($user, $this->generateUrl('homepage', [], UrlGeneratorInterface::ABSOLUTE_URL))->getDisplayId(), + 'following' => $this->generateUrl('following', ['user' => $user->getUsername()], UrlGeneratorInterface::ABSOLUTE_URL), + 'followers' => $this->generateUrl('followers', ['user' => $user->getUsername()], UrlGeneratorInterface::ABSOLUTE_URL), + //'liked' => $this->generateUrl('recommended', ['user' => $user], UrlGeneratorInterface::ABSOLUTE_URL), + 'inbox' => $this->generateUrl('user-inbox', ['user' => $user], UrlGeneratorInterface::ABSOLUTE_URL), + 'outbox' => $this->generateUrl('user-outbox', ['user' => $user->getUsername()], UrlGeneratorInterface::ABSOLUTE_URL), + 'preferredUsername' => $user->getUser()->getName(), + 'name' => $user->getUsername(), + //'oauthAuthorizationEndpoint' => $this->generateUrl('fos_oauth_server_authorize', [], UrlGeneratorInterface::ABSOLUTE_URL), + 'oauthTokenEndpoint' => $this->generateUrl('fos_oauth_server_token', [], UrlGeneratorInterface::ABSOLUTE_URL), + //'publicInbox' => $this->generateUrl('public_inbox', [], UrlGeneratorInterface::ABSOLUTE_URL), + ]; + return new JsonResponse($data); + } + return $this->render( + 'WallabagFederationBundle:User:profile.html.twig', [ + 'user' => $user, + 'registration_enabled' => $this->getParameter('wallabag_user.registration_enabled'), + ] + ); + } + + /** + * @Route("/profile/@{user}/followings/{page}", name="following", defaults={"page" : 0}) + * @ParamConverter("user", class="WallabagFederationBundle:Account", options={ + * "repository_method" = "findOneByUsername"}) + * + * @param Request $request + * @param Account $user + * @param int $page + * @return JsonResponse|Response + */ + public function getUsersFollowing(Request $request, Account $user, $page = 0) + { + $qb = $this->getDoctrine()->getRepository('WallabagFederationBundle:Account')->getBuilderForFollowingsByAccount($user->getId()); + + $pagerAdapter = new DoctrineORMAdapter($qb->getQuery(), true, false); + + $following = new Pagerfanta($pagerAdapter); + $totalFollowing = $following->getNbResults(); + + $activityStream = in_array('application/ld+json; profile="https://www.w3.org/ns/activitystreams', $request->getAcceptableContentTypes(), true); + + if ($page === 0 && $activityStream) { + /** Home page */ + $dataPrez = [ + '@context' => 'https://www.w3.org/ns/activitystreams', + 'summary' => $user->getUsername() . " followings'", + 'type' => 'Collection', + 'id' => $this->generateUrl('following', ['user' => $user->getUsername()], UrlGeneratorInterface::ABSOLUTE_URL), + 'totalItems' => $totalFollowing, + 'first' => [ + '@context' => 'https://www.w3.org/ns/activitystreams', + 'type' => 'Link', + 'href' => $this->generateUrl('following', ['user' => $user->getUsername(), 'page' => 1], UrlGeneratorInterface::ABSOLUTE_URL), + 'name' => 'First page of ' . $user->getUsername() . ' followings' + ], + 'last' => [ + '@context' => 'https://www.w3.org/ns/activitystreams', + 'type' => 'Link', + 'href' => $this->generateUrl('following', ['user' => $user->getUsername(), 'page' => $following->getNbPages()], UrlGeneratorInterface::ABSOLUTE_URL), + 'name' => 'Last page of ' . $user->getUsername() . ' followings' + ] + ]; + return new JsonResponse($dataPrez); + //} + } + + $following->setMaxPerPage(30); + $following->setCurrentPage($page); + + if (!$activityStream) { + return $this->render('WallabagFederationBundle:User:followers.html.twig', [ + 'users' => $following, + 'user' => $user, + 'registration_enabled' => $this->getParameter('wallabag_user.registration_enabled'), + ]); + } + + $items = []; + + foreach ($following->getCurrentPageResults() as $follow) { + /** @var Account $follow */ + /** Items in the page */ + $items[] = [ + '@context' => 'https://www.w3.org/ns/activitystreams', + 'type' => 'Person', + 'name' => $follow->getUsername(), + 'id' => CloudId::getCloudIdFromAccount($follow), + ]; + } + + $data = [ + 'summary' => 'Page ' . $page . ' of ' . $user->getUsername() . ' followers', + 'partOf' => $this->generateUrl('following', ['user' => $user->getUsername()], UrlGeneratorInterface::ABSOLUTE_URL), + 'type' => 'OrderedCollectionPage', + 'startIndex' => ($page - 1) * 30, + 'orderedItems' => $items, + 'first' => [ + '@context' => 'https://www.w3.org/ns/activitystreams', + 'type' => 'Link', + 'href' => $this->generateUrl('following', ['user' => $user->getUsername(), 'page' => 1], UrlGeneratorInterface::ABSOLUTE_URL), + 'name' => 'First page of ' . $user->getUsername() . ' followings' + ], + 'last' => [ + '@context' => 'https://www.w3.org/ns/activitystreams', + 'type' => 'Link', + 'href' => $this->generateUrl('following', ['user' => $user->getUsername(), 'page' => $following->getNbPages()], UrlGeneratorInterface::ABSOLUTE_URL), + 'name' => 'Last page of ' . $user->getUsername() . ' followings' + ], + ]; + + /** Previous page */ + if ($page > 1) { + $data['prev'] = [ + '@context' => 'https://www.w3.org/ns/activitystreams', + 'type' => 'Link', + 'href' => $this->generateUrl('following', ['user' => $user->getUsername(), 'page' => $page - 1], UrlGeneratorInterface::ABSOLUTE_URL), + 'name' => 'Previous page of ' . $user->getUsername() . ' followings' + ]; + } + + /** Next page */ + if ($page < $following->getNbPages()) { + $data['next'] = [ + '@context' => 'https://www.w3.org/ns/activitystreams', + 'type' => 'Link', + 'href' => $this->generateUrl('following', ['user' => $user->getUsername(), 'page' => $page + 1], UrlGeneratorInterface::ABSOLUTE_URL), + 'name' => 'Next page of ' . $user->getUsername() . ' followings' + ]; + } + + return new JsonResponse($data); + } + + /** + * @Route("/profile/@{user}/followers/{page}", name="followers", defaults={"page" : 0}) + * @ParamConverter("user", class="WallabagFederationBundle:Account", options={ + * "repository_method" = "findOneByUsername"}) + * + * @param Request $request + * @param Account $user + * @return JsonResponse + */ + public function getUsersFollowers(Request $request, Account $user, $page) + { + $qb = $this->getDoctrine()->getRepository('WallabagFederationBundle:Account')->getBuilderForFollowersByAccount($user->getId()); + + $pagerAdapter = new DoctrineORMAdapter($qb->getQuery(), true, false); + + $followers = new Pagerfanta($pagerAdapter); + $totalFollowers = $followers->getNbResults(); + + $activityStream = in_array('application/ld+json; profile="https://www.w3.org/ns/activitystreams', $request->getAcceptableContentTypes(), true); + + if ($page === 0 && $activityStream) { + /** Home page */ + $dataPrez = [ + '@context' => 'https://www.w3.org/ns/activitystreams', + 'summary' => $user->getUsername() . " followers'", + 'type' => 'Collection', + 'id' => $this->generateUrl('followers', ['user' => $user->getUsername()], UrlGeneratorInterface::ABSOLUTE_URL), + 'totalItems' => $totalFollowers, + 'first' => [ + '@context' => 'https://www.w3.org/ns/activitystreams', + 'type' => 'Link', + 'href' => $this->generateUrl('followers', ['user' => $user->getUsername(), 'page' => 1], UrlGeneratorInterface::ABSOLUTE_URL), + 'name' => 'First page of ' . $user->getUsername() . ' followers' + ], + 'last' => [ + '@context' => 'https://www.w3.org/ns/activitystreams', + 'type' => 'Link', + 'href' => $this->generateUrl('followers', ['user' => $user->getUsername(), 'page' => $followers->getNbPages()], UrlGeneratorInterface::ABSOLUTE_URL), + 'name' => 'Last page of ' . $user->getUsername() . ' followers' + ] + ]; + return new JsonResponse($dataPrez); + } + + $followers->setMaxPerPage(30); + if (!$activityStream && $page === 0) { + $followers->setCurrentPage(1); + } else { + $followers->setCurrentPage($page); + } + + if (!$activityStream) { + return $this->render('WallabagFederationBundle:User:followers.html.twig', [ + 'users' => $followers, + 'user' => $user, + 'registration_enabled' => $this->getParameter('wallabag_user.registration_enabled'), + ]); + } + + $items = []; + + foreach ($followers->getCurrentPageResults() as $follow) { + /** @var Account $follow */ + /** Items in the page */ + $items[] = [ + '@context' => 'https://www.w3.org/ns/activitystreams', + 'type' => 'Person', + 'name' => $follow->getUsername(), + 'id' => CloudId::getCloudIdFromAccount($follow)->getDisplayId(), + ]; + } + $data = [ + 'summary' => 'Page ' . $page . ' of ' . $user->getUsername() . ' followers', + 'partOf' => $this->generateUrl('followers', ['user' => $user->getUsername()], UrlGeneratorInterface::ABSOLUTE_URL), + 'type' => 'OrderedCollectionPage', + 'startIndex' => ($page - 1) * 30, + 'orderedItems' => $items, + 'first' => [ + '@context' => 'https://www.w3.org/ns/activitystreams', + 'type' => 'Link', + 'href' => $this->generateUrl('followers', ['user' => $user->getUsername(), 'page' => 1], UrlGeneratorInterface::ABSOLUTE_URL), + 'name' => 'First page of ' . $user->getUsername() . ' followers' + ], + 'last' => [ + '@context' => 'https://www.w3.org/ns/activitystreams', + 'type' => 'Link', + 'href' => $this->generateUrl('followers', ['user' => $user->getUsername(), 'page' => $followers->getNbPages()], UrlGeneratorInterface::ABSOLUTE_URL), + 'name' => 'Last page of ' . $user->getUsername() . ' followers' + ], + ]; + + /** Previous page */ + if ($page > 1) { + $data['prev'] = [ + '@context' => 'https://www.w3.org/ns/activitystreams', + 'type' => 'Link', + 'href' => $this->generateUrl('followers', ['user' => $user->getUsername(), 'page' => $page - 1], UrlGeneratorInterface::ABSOLUTE_URL), + 'name' => 'Previous page of ' . $user->getUsername() . ' followers' + ]; + } + + /** Next page */ + if ($page < $followers->getNbPages()) { + $data['next'] = [ + '@context' => 'https://www.w3.org/ns/activitystreams', + 'type' => 'Link', + 'href' => $this->generateUrl('followers', ['user' => $user->getUsername(), 'page' => $page + 1], UrlGeneratorInterface::ABSOLUTE_URL), + 'name' => 'Next page of ' . $user->getUsername() . ' followers' + ]; + } + + return new JsonResponse($data); + } + + /** + * @Route("/profile/@{userToFollow}/follow", name="follow-user") + * @ParamConverter("userToFollow", class="WallabagFederationBundle:Account", options={ + * "repository_method" = "findOneByUsername"}) + * @param Account $userToFollow + */ + public function followAccountAction(Account $userToFollow) + { + // if we're on our own instance + if ($this->get('security.authorization_checker')->isGranted('IS_AUTHENTICATED_FULLY')) { + + /** @var Account $userAccount */ + $userAccount = $this->getUser()->getAccount(); + + if ($userToFollow === $userAccount) { + $this->createAccessDeniedException("You can't follow yourself"); + } + + $em = $this->getDoctrine()->getManager(); + + $userAccount->addFollowing($userToFollow); + $userToFollow->addFollower($userAccount); + + $em->persist($userAccount); + $em->persist($userToFollow); + + $em->flush(); + } else { + // ask cloud id and redirect to instance + } + } + + /** + * @Route("/profile/@{user}/recommendations", name="user-recommendations", defaults={"page" : 0}) + * @ParamConverter("user", class="WallabagFederationBundle:Account", options={ + * "repository_method" = "findOneByUsername"}) + * + * @param Request $request + * @param Account $user + * @param int $page + * @return JsonResponse|Response + */ + public function getUsersRecommendationsAction(Request $request, Account $user, $page = 0) + { + $qb = $this->getDoctrine()->getRepository('WallabagCoreBundle:Entry')->getBuilderForRecommendationsByUser($user->getUser()->getId()); + + $pagerAdapter = new DoctrineORMAdapter($qb->getQuery(), true, false); + + $recommendations = new Pagerfanta($pagerAdapter); + $totalRecommendations = $recommendations->getNbResults(); + + $activityStream = in_array('application/ld+json; profile="https://www.w3.org/ns/activitystreams', $request->getAcceptableContentTypes(), true); + + if ($page === 0 && $activityStream) { + $dataPrez = [ + '@context' => 'https://www.w3.org/ns/activitystreams', + 'summary' => $user->getUsername() . " recommendations'", + 'type' => 'Collection', + 'id' => $this->generateUrl('user-recommendations', ['user' => $user->getUsername()], UrlGeneratorInterface::ABSOLUTE_URL), + 'totalItems' => $totalRecommendations, + 'first' => [ + '@context' => 'https://www.w3.org/ns/activitystreams', + 'type' => 'Link', + 'href' => $this->generateUrl('user-recommendations', ['user' => $user->getUsername(), 'page' => 1], UrlGeneratorInterface::ABSOLUTE_URL), + 'name' => 'First page of ' . $user->getUsername() . ' followers' + ], + 'last' => [ + '@context' => 'https://www.w3.org/ns/activitystreams', + 'type' => 'Link', + 'href' => $this->generateUrl('user-recommendations', ['user' => $user->getUsername(), 'page' => $recommendations->getNbPages()], UrlGeneratorInterface::ABSOLUTE_URL), + 'name' => 'Last page of ' . $user->getUsername() . ' followers' + ] + ]; + return new JsonResponse($dataPrez); + } + + $recommendations->setMaxPerPage(30); + if (!$activityStream && $page === 0) { + $recommendations->setCurrentPage(1); + } else { + $recommendations->setCurrentPage($page); + } + + if (!$activityStream) { + return $this->render('WallabagFederationBundle:User:recommendations.html.twig', [ + 'recommendations' => $recommendations, + 'registration_enabled' => $this->getParameter('wallabag_user.registration_enabled'), + ]); + } + + $items = []; + + foreach ($recommendations->getCurrentPageResults() as $recommendation) { + $items[] = [ + '@context' => 'https://www.w3.org/ns/activitystreams', + 'type' => 'Person', + 'name' => $recommendation->getTitle(), + 'id' => $recommendation->getUrl(), + ]; + } + $data = [ + 'summary' => 'Page ' . $page . ' of ' . $user->getUsername() . ' recommendations', + 'partOf' => $this->generateUrl('user-recommendations', ['user' => $user->getUsername()], UrlGeneratorInterface::ABSOLUTE_URL), + 'type' => 'OrderedCollectionPage', + 'startIndex' => ($page - 1) * 30, + 'orderedItems' => $items, + 'first' => [ + '@context' => 'https://www.w3.org/ns/activitystreams', + 'type' => 'Link', + 'href' => $this->generateUrl('user-recommendations', ['user' => $user->getUsername(), 'page' => 1], UrlGeneratorInterface::ABSOLUTE_URL), + 'name' => 'First page of ' . $user->getUsername() . ' recommendations' + ], + 'last' => [ + '@context' => 'https://www.w3.org/ns/activitystreams', + 'type' => 'Link', + 'href' => $this->generateUrl('user-recommendations', ['user' => $user->getUsername(), 'page' => $recommendations->getNbPages()], UrlGeneratorInterface::ABSOLUTE_URL), + 'name' => 'Last page of ' . $user->getUsername() . ' recommendations' + ], + ]; + + if ($page > 1) { + $data['prev'] = [ + '@context' => 'https://www.w3.org/ns/activitystreams', + 'type' => 'Link', + 'href' => $this->generateUrl('user-recommendations', ['user' => $user->getUsername(), 'page' => $page - 1], UrlGeneratorInterface::ABSOLUTE_URL), + 'name' => 'Previous page of ' . $user->getUsername() . ' recommendations' + ]; + } + + if ($page < $recommendations->getNbPages()) { + $data['next'] = [ + '@context' => 'https://www.w3.org/ns/activitystreams', + 'type' => 'Link', + 'href' => $this->generateUrl('user-recommendations', ['user' => $user->getUsername(), 'page' => $page + 1], UrlGeneratorInterface::ABSOLUTE_URL), + 'name' => 'Next page of ' . $user->getUsername() . ' recommendations' + ]; + } + + return new JsonResponse($data); + } + +} diff --git a/src/Wallabag/FederationBundle/Controller/RecommandController.php b/src/Wallabag/FederationBundle/Controller/RecommandController.php new file mode 100644 index 00000000..ea5a6660 --- /dev/null +++ b/src/Wallabag/FederationBundle/Controller/RecommandController.php @@ -0,0 +1,34 @@ +getUser() !== $this->getUser()) { + $this->createAccessDeniedException("You can't recommend entries which are not your own"); + } + $em = $this->getDoctrine()->getManager(); + + $entry->setRecommended(true); + + $em->persist($entry); + $em->flush(); + + $this->get('event_dispatcher')->dispatch(RecommendedEntryEvent::NAME, new RecommendedEntryEvent($entry)); + + $this->redirectToRoute('view', ['id' => $entry->getId()]); + } +} diff --git a/src/Wallabag/FederationBundle/DependencyInjection/Configuration.php b/src/Wallabag/FederationBundle/DependencyInjection/Configuration.php new file mode 100644 index 00000000..7754e8e4 --- /dev/null +++ b/src/Wallabag/FederationBundle/DependencyInjection/Configuration.php @@ -0,0 +1,25 @@ +root('wallabag_user'); + + $rootNode + ->children() + ->booleanNode('registration_enabled') + ->defaultValue(true) + ->end() + ->end() + ; + + return $treeBuilder; + } +} diff --git a/src/Wallabag/FederationBundle/DependencyInjection/WallabagFederationExtension.php b/src/Wallabag/FederationBundle/DependencyInjection/WallabagFederationExtension.php new file mode 100644 index 00000000..ceeb8379 --- /dev/null +++ b/src/Wallabag/FederationBundle/DependencyInjection/WallabagFederationExtension.php @@ -0,0 +1,26 @@ +processConfiguration($configuration, $configs); + + $loader = new Loader\YamlFileLoader($container, new FileLocator(__DIR__.'/../Resources/config')); + $loader->load('services.yml'); + $container->setParameter('wallabag_user.registration_enabled', $config['registration_enabled']); + } + + public function getAlias() + { + return 'wallabag_federation'; + } +} diff --git a/src/Wallabag/FederationBundle/Entity/Account.php b/src/Wallabag/FederationBundle/Entity/Account.php new file mode 100644 index 00000000..c44050d9 --- /dev/null +++ b/src/Wallabag/FederationBundle/Entity/Account.php @@ -0,0 +1,307 @@ +followers = new ArrayCollection(); + $this->following = new ArrayCollection(); + $this->liked = new ArrayCollection(); + } + + /** + * @return int + */ + public function getId() + { + return $this->id; + } + + /** + * @return string + */ + public function getUsername() + { + return $this->username; + } + + /** + * @param string $username + * @return Account + */ + public function setUsername($username) + { + $this->username = $username; + return $this; + } + + /** + * @return string + */ + public function getServer() + { + return $this->server; + } + + /** + * @param string $server + * @return Account + */ + public function setServer($server) + { + $this->server = $server; + return $this; + } + + /** + * @return User + */ + public function getUser() + { + return $this->user; + } + + /** + * @param User $user + * @return Account + */ + public function setUser(User $user) + { + $this->user = $user; + return $this; + } + + /** + * @return Collection + */ + public function getFollowers() + { + return $this->followers; + } + + /** + * @param Collection $followers + * @return Account + */ + public function setFollowers($followers) + { + $this->followers = $followers; + return $this; + } + + /** + * @param Account $account + * @return Account + */ + public function addFollower(Account $account) + { + $this->followers->add($account); + return $this; + } + + /** + * @return Collection + */ + public function getFollowing() + { + return $this->following; + } + + /** + * @param Collection $following + * @return Account + */ + public function setFollowing(Collection $following) + { + $this->following = $following; + return $this; + } + + /** + * @param Account $account + * @return Account + */ + public function addFollowing(Account $account) + { + $this->following->add($account); + return $this; + } + + /** + * @return Collection + */ + public function getLiked() + { + return $this->liked; + } + + /** + * @param Collection $liked + * @return Account + */ + public function setLiked(Collection $liked) + { + $this->liked = $liked; + return $this; + } + + /** + * @param Entry $entry + * @return Account + */ + public function addLiked(Entry $entry) + { + $this->liked->add($entry); + return $this; + } + + /** + * @return string + */ + public function getAvatar() + { + return $this->avatar; + } + + /** + * @param string $avatar + * @return Account + */ + public function setAvatar($avatar) + { + $this->avatar = $avatar; + return $this; + } + + /** + * @return string + */ + public function getBanner() + { + return $this->banner; + } + + /** + * @param string $banner + * @return Account + */ + public function setBanner($banner) + { + $this->banner = $banner; + return $this; + } + + /** + * @return string + */ + public function getDescription() + { + return $this->description; + } + + /** + * @param string $description + * @return Account + */ + public function setDescription($description) + { + $this->description = $description; + return $this; + } + +} diff --git a/src/Wallabag/FederationBundle/Entity/Instance.php b/src/Wallabag/FederationBundle/Entity/Instance.php new file mode 100644 index 00000000..ff8960cd --- /dev/null +++ b/src/Wallabag/FederationBundle/Entity/Instance.php @@ -0,0 +1,111 @@ +domain = $domain; + } + + /** + * @return int + */ + public function getId() + { + return $this->id; + } + + /** + * @return string + */ + public function getDomain() + { + return $this->domain; + } + + /** + * @param string $domain + */ + public function setDomain($domain) + { + $this->domain = $domain; + } + + /** + * @return float + */ + public function getScore() + { + return $this->score; + } + + /** + * @param float $score + */ + public function setScore($score) + { + $this->score = $score; + } + + /** + * @return array + */ + public function getUsers() + { + return $this->users; + } + + /** + * @param array $users + */ + public function setUsers($users) + { + $this->users = $users; + } +} diff --git a/src/Wallabag/FederationBundle/EventListener/CreateAccountListener.php b/src/Wallabag/FederationBundle/EventListener/CreateAccountListener.php new file mode 100644 index 00000000..92626b15 --- /dev/null +++ b/src/Wallabag/FederationBundle/EventListener/CreateAccountListener.php @@ -0,0 +1,54 @@ +em = $em; + $this->domainName = $domainName; + } + + public static function getSubscribedEvents() + { + return [ + // when a user register using the normal form + FOSUserEvents::REGISTRATION_COMPLETED => 'createAccount', + // when we manually create a user using the command line + // OR when we create it from the config UI + FOSUserEvents::USER_CREATED => 'createAccount', + ]; + } + + public function createAccount(UserEvent $event) + { + $user = $event->getUser(); + $account = new Account(); + $account->setUser($user) + ->setUsername($user->getUsername()) + ->setServer($this->domainName); + + $this->em->persist($account); + + $user->setAccount($account); + + $this->em->persist($user); + $this->em->flush(); + } +} diff --git a/src/Wallabag/FederationBundle/Federation/CloudId.php b/src/Wallabag/FederationBundle/Federation/CloudId.php new file mode 100644 index 00000000..038ea5e8 --- /dev/null +++ b/src/Wallabag/FederationBundle/Federation/CloudId.php @@ -0,0 +1,78 @@ +id = $id; + + $atPos = strpos($id, '@'); + $user = substr($id, 0, $atPos); + $remote = substr($id, $atPos + 1); + if (!empty($user) && !empty($remote)) { + $this->user = $user; + $this->remote = $remote; + } + } + + /** + * The full remote cloud id + * + * @return string + */ + public function getId() { + return $this->id; + } + + public function getDisplayId() { + return str_replace('https://', '', str_replace('http://', '', $this->getId())); + } + + /** + * The username on the remote server + * + * @return string + */ + public function getUser() { + return $this->user; + } + + /** + * The base address of the remote server + * + * @return string + */ + public function getRemote() { + return $this->remote; + } + + /** + * @param Account $account + * @param string $domain + * @return CloudId + */ + public static function getCloudIdFromAccount(Account $account, $domain = '') + { + if ($account->getServer() !== null) { + return new self($account->getUsername() . '@' . $account->getServer()); + } + return new self($account->getUsername() . '@' . $domain); + } +} diff --git a/src/Wallabag/FederationBundle/Form/Type/AccountType.php b/src/Wallabag/FederationBundle/Form/Type/AccountType.php new file mode 100644 index 00000000..af291ced --- /dev/null +++ b/src/Wallabag/FederationBundle/Form/Type/AccountType.php @@ -0,0 +1,46 @@ +add('description', TextareaType::class, ['label' => 'config.form_account.description_label']) + ->add('avatar', FileType::class, [ + 'label' => 'config.form_account.avatar_label', + 'required' => false, + 'data_class' => null, + ]) + ->add('banner', FileType::class, [ + 'label' => 'config.form_account.banner_label', + 'required' => false, + 'data_class' => null, + ]) + ->add('save', SubmitType::class, [ + 'label' => 'config.form.save', + ]) + ; + } + + public function configureOptions(OptionsResolver $resolver) + { + $resolver->setDefaults(array( + 'data_class' => Account::class, + )); + } + + public function getBlockPrefix() + { + return 'update_account'; + } +} diff --git a/src/Wallabag/FederationBundle/Repository/AccountRepository.php b/src/Wallabag/FederationBundle/Repository/AccountRepository.php new file mode 100644 index 00000000..e39bc582 --- /dev/null +++ b/src/Wallabag/FederationBundle/Repository/AccountRepository.php @@ -0,0 +1,48 @@ +createQueryBuilder('a') + ->select('f.id, f.username') + ->innerJoin('a.following', 'f') + ->where('a.id = :accountId')->setParameter('accountId', $accountId) + ; + } + + /** + * @param $accountId + * @return QueryBuilder + */ + public function getBuilderForFollowersByAccount($accountId) + { + return $this->createQueryBuilder('a') + ->innerJoin('a.followers', 'f') + ->where('a.id = :accountId')->setParameter('accountId', $accountId) + ; + } + + /** + * @param $username + * @return QueryBuilder + * @throws \Doctrine\ORM\NonUniqueResultException + */ + public function findAccountByUsername($username) + { + return $this->createQueryBuilder('a') + ->where('a.username = :username')->setParameter('username', $username) + ->andWhere('a.server = null') + ->getQuery() + ->getOneOrNullResult(); + } +} diff --git a/src/Wallabag/FederationBundle/Repository/InstanceRepository.php b/src/Wallabag/FederationBundle/Repository/InstanceRepository.php new file mode 100644 index 00000000..6365d5c4 --- /dev/null +++ b/src/Wallabag/FederationBundle/Repository/InstanceRepository.php @@ -0,0 +1,10 @@ + + {% endif %} +{% endblock %} + +{% block scripts %} + {{ parent() }} + +{% endblock %} + +{% block header %} +{% endblock %} + +{% block body_class %}reset-left{% endblock %} + +{% block messages %} + {% for flashMessage in app.session.flashbag.get('notice') %} + + {% endfor %} +{% endblock %} + +{% block menu %} + + +{% endblock %} + +{% block content %} +
+
+
+ {{ include('@WallabagFederation/themes/material/User/profile_header.html.twig') }} +
    + {% for account in users %} +
  • + folder + {{ account.username }}@{{ account.server }} +

    First Line

    + grade +
  • + {% endfor %} +
+
+
+
+{% endblock %} + +{% block footer %} +
+ +
+{% endblock %} diff --git a/src/Wallabag/FederationBundle/Resources/views/themes/material/User/profile.html.twig b/src/Wallabag/FederationBundle/Resources/views/themes/material/User/profile.html.twig new file mode 100644 index 00000000..c52d0ca4 --- /dev/null +++ b/src/Wallabag/FederationBundle/Resources/views/themes/material/User/profile.html.twig @@ -0,0 +1,77 @@ +{% extends "WallabagCoreBundle::base.html.twig" %} + +{% block css %} + {{ parent() }} + {% if not app.debug %} + + {% endif %} +{% endblock %} + +{% block scripts %} + {{ parent() }} + +{% endblock %} + +{% block header %} +{% endblock %} + +{% block body_class %}reset-left{% endblock %} + +{% block messages %} + {% for flashMessage in app.session.flashbag.get('notice') %} + + {% endfor %} +{% endblock %} + +{% block menu %} + + +{% endblock %} + +{% block content %} +
+
+
+ {{ include('@WallabagFederation/themes/material/User/profile_header.html.twig') }} +
+
+{% endblock %} + +{% block footer %} +
+ +
+{% endblock %} diff --git a/src/Wallabag/FederationBundle/Resources/views/themes/material/User/profile_header.html.twig b/src/Wallabag/FederationBundle/Resources/views/themes/material/User/profile_header.html.twig new file mode 100644 index 00000000..703de79f --- /dev/null +++ b/src/Wallabag/FederationBundle/Resources/views/themes/material/User/profile_header.html.twig @@ -0,0 +1,35 @@ +{% set usershow = user.username %} +{% if user.user.name is not null %} + {% set usershow = user.user.name %} +{% endif %} + +

+ {{ usershow }} utilise wallabag pour lire et archiver son contenu. Vous pouvez le/la suivre et interagir si vous possédez un compte quelque part dans le "fediverse". Si ce n'est pas le cas, vous pouvez en créer un ici. +

+
+
+ +

{{ usershow }}

+
@{{ user.username }}
+ person_add + +
+
diff --git a/src/Wallabag/FederationBundle/Resources/views/themes/material/User/recommendations.html.twig b/src/Wallabag/FederationBundle/Resources/views/themes/material/User/recommendations.html.twig new file mode 100644 index 00000000..b3d0d2cf --- /dev/null +++ b/src/Wallabag/FederationBundle/Resources/views/themes/material/User/recommendations.html.twig @@ -0,0 +1,4 @@ +{{ dump(recommendations) }} +{% for entry in recommendations %} +{{ include('@WallabagCore/themes/material/Entry/_card_list.html.twig') }} +{% endfor %} diff --git a/src/Wallabag/FederationBundle/WallabagFederationBundle.php b/src/Wallabag/FederationBundle/WallabagFederationBundle.php new file mode 100644 index 00000000..f9bd665a --- /dev/null +++ b/src/Wallabag/FederationBundle/WallabagFederationBundle.php @@ -0,0 +1,9 @@ +