FederationController.php 7.4 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226
  1. <?php
  2. namespace App\Http\Controllers;
  3. use App\Jobs\InboxPipeline\InboxWorker;
  4. use App\Jobs\RemoteFollowPipeline\RemoteFollowPipeline;
  5. use App\Profile;
  6. use App\Transformer\ActivityPub\ProfileOutbox;
  7. use App\Util\Lexer\Nickname;
  8. use App\Util\Webfinger\Webfinger;
  9. use Auth;
  10. use Cache;
  11. use Carbon\Carbon;
  12. use Illuminate\Http\Request;
  13. use League\Fractal;
  14. use App\Util\ActivityPub\Helpers;
  15. class FederationController extends Controller
  16. {
  17. public function authCheck()
  18. {
  19. if (!Auth::check()) {
  20. return abort(403);
  21. }
  22. }
  23. public function authorizeFollow(Request $request)
  24. {
  25. $this->authCheck();
  26. $this->validate($request, [
  27. 'acct' => 'required|string|min:3|max:255',
  28. ]);
  29. $acct = $request->input('acct');
  30. $nickname = Nickname::normalizeProfileUrl($acct);
  31. return view('federation.authorizefollow', compact('acct', 'nickname'));
  32. }
  33. public function remoteFollow()
  34. {
  35. $this->authCheck();
  36. return view('federation.remotefollow');
  37. }
  38. public function remoteFollowStore(Request $request)
  39. {
  40. $this->authCheck();
  41. $this->validate($request, [
  42. 'url' => 'required|string',
  43. ]);
  44. if (config('pixelfed.remote_follow_enabled') !== true) {
  45. abort(403);
  46. }
  47. $follower = Auth::user()->profile;
  48. $url = $request->input('url');
  49. RemoteFollowPipeline::dispatch($follower, $url);
  50. return redirect()->back();
  51. }
  52. public function nodeinfoWellKnown()
  53. {
  54. $res = [
  55. 'links' => [
  56. [
  57. 'href' => config('pixelfed.nodeinfo.url'),
  58. 'rel' => 'http://nodeinfo.diaspora.software/ns/schema/2.0',
  59. ],
  60. ],
  61. ];
  62. return response()->json($res);
  63. }
  64. public function nodeinfo()
  65. {
  66. $res = Cache::remember('api:nodeinfo', 60, function () {
  67. return [
  68. 'metadata' => [
  69. 'nodeName' => config('app.name'),
  70. 'software' => [
  71. 'homepage' => 'https://pixelfed.org',
  72. 'github' => 'https://github.com/pixelfed',
  73. 'follow' => 'https://mastodon.social/@pixelfed',
  74. ],
  75. ],
  76. 'openRegistrations' => config('pixelfed.open_registration'),
  77. 'protocols' => [
  78. 'activitypub',
  79. ],
  80. 'services' => [
  81. 'inbound' => [],
  82. 'outbound' => [],
  83. ],
  84. 'software' => [
  85. 'name' => 'pixelfed',
  86. 'version' => config('pixelfed.version'),
  87. ],
  88. 'usage' => [
  89. 'localPosts' => \App\Status::whereLocal(true)->whereHas('media')->count(),
  90. 'localComments' => \App\Status::whereLocal(true)->whereNotNull('in_reply_to_id')->count(),
  91. 'users' => [
  92. 'total' => \App\User::count(),
  93. 'activeHalfyear' => \App\AccountLog::select('user_id')->whereAction('auth.login')->where('updated_at', '>',Carbon::now()->subMonths(6)->toDateTimeString())->groupBy('user_id')->get()->count(),
  94. 'activeMonth' => \App\AccountLog::select('user_id')->whereAction('auth.login')->where('updated_at', '>',Carbon::now()->subMonths(1)->toDateTimeString())->groupBy('user_id')->get()->count(),
  95. ],
  96. ],
  97. 'version' => '2.0',
  98. ];
  99. });
  100. return response()->json($res, 200, [], JSON_PRETTY_PRINT);
  101. }
  102. public function webfinger(Request $request)
  103. {
  104. $this->validate($request, ['resource'=>'required|string|min:3|max:255']);
  105. $hash = hash('sha256', $request->input('resource'));
  106. $webfinger = Cache::remember('api:webfinger:'.$hash, 1440, function () use ($request) {
  107. $resource = $request->input('resource');
  108. $parsed = Nickname::normalizeProfileUrl($resource);
  109. $username = $parsed['username'];
  110. $user = Profile::whereUsername($username)->firstOrFail();
  111. return (new Webfinger($user))->generate();
  112. });
  113. return response()->json($webfinger, 200, [], JSON_PRETTY_PRINT);
  114. }
  115. public function hostMeta(Request $request)
  116. {
  117. $path = route('well-known.webfinger');
  118. $xml = <<<XML
  119. <?xml version="1.0" encoding="UTF-8"?>
  120. <XRD xmlns="http://docs.oasis-open.org/ns/xri/xrd-1.0">
  121. <Link rel="lrdd" type="application/xrd+xml" template="{$path}?resource={uri}"/>
  122. </XRD>
  123. XML;
  124. return response($xml)->header('Content-Type', 'application/xrd+xml');
  125. }
  126. public function userOutbox(Request $request, $username)
  127. {
  128. if (config('pixelfed.activitypub_enabled') == false) {
  129. abort(403);
  130. }
  131. $user = Profile::whereNull('remote_url')->whereUsername($username)->firstOrFail();
  132. if($user->is_private) {
  133. return response()->json(['error'=>'403', 'msg' => 'private profile'], 403);
  134. }
  135. $timeline = $user->statuses()->whereVisibility('public')->orderBy('created_at', 'desc')->paginate(10);
  136. $fractal = new Fractal\Manager();
  137. $resource = new Fractal\Resource\Item($user, new ProfileOutbox());
  138. $res = $fractal->createData($resource)->toArray();
  139. return response(json_encode($res['data']))->header('Content-Type', 'application/activity+json');
  140. }
  141. public function userInbox(Request $request, $username)
  142. {
  143. if (config('pixelfed.activitypub_enabled') == false) {
  144. abort(403);
  145. }
  146. $type = [
  147. 'application/activity+json'
  148. ];
  149. if (in_array($request->header('Content-Type'), $type) == false) {
  150. abort(500, 'Invalid request');
  151. }
  152. $profile = Profile::whereUsername($username)->firstOrFail();
  153. $headers = [
  154. 'date' => $request->header('date'),
  155. 'signature' => $request->header('signature'),
  156. 'digest' => $request->header('digest'),
  157. 'content-type' => $request->header('content-type'),
  158. 'path' => $request->getRequestUri(),
  159. 'host' => $request->getHttpHost()
  160. ];
  161. InboxWorker::dispatch($headers, $profile, $request->all());
  162. }
  163. public function userFollowing(Request $request, $username)
  164. {
  165. if (config('pixelfed.activitypub_enabled') == false) {
  166. abort(403);
  167. }
  168. $profile = Profile::whereNull('remote_url')->whereUsername($username)->firstOrFail();
  169. $obj = [
  170. '@context' => 'https://www.w3.org/ns/activitystreams',
  171. 'id' => $request->getUri(),
  172. 'type' => 'OrderedCollectionPage',
  173. 'totalItems' => $profile->following()->count(),
  174. 'orderedItems' => $profile->following->map(function($f) {
  175. return $f->permalink();
  176. })
  177. ];
  178. return response()->json($obj);
  179. }
  180. public function userFollowers(Request $request, $username)
  181. {
  182. if (config('pixelfed.activitypub_enabled') == false) {
  183. abort(403);
  184. }
  185. $profile = Profile::whereNull('remote_url')->whereUsername($username)->firstOrFail();
  186. $obj = [
  187. '@context' => 'https://www.w3.org/ns/activitystreams',
  188. 'id' => $request->getUri(),
  189. 'type' => 'OrderedCollectionPage',
  190. 'totalItems' => $profile->followers()->count(),
  191. 'orderedItems' => $profile->followers->map(function($f) {
  192. return $f->permalink();
  193. })
  194. ];
  195. return response()->json($obj);
  196. }
  197. }