SearchController.php 11 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318
  1. <?php
  2. namespace App\Http\Controllers;
  3. use Auth;
  4. use App\Hashtag;
  5. use App\Profile;
  6. use App\Status;
  7. use Illuminate\Http\Request;
  8. use App\Util\ActivityPub\Helpers;
  9. use Illuminate\Support\Facades\Cache;
  10. use Illuminate\Support\Str;
  11. use App\Transformer\Api\{
  12. AccountTransformer,
  13. HashtagTransformer,
  14. StatusTransformer,
  15. };
  16. use App\Services\WebfingerService;
  17. class SearchController extends Controller
  18. {
  19. public $tokens = [];
  20. public $term = '';
  21. public $hash = '';
  22. public $cacheKey = 'api:search:tag:';
  23. public function __construct()
  24. {
  25. $this->middleware('auth');
  26. }
  27. public function searchAPI(Request $request)
  28. {
  29. $this->validate($request, [
  30. 'q' => 'required|string|min:3|max:120',
  31. 'src' => 'required|string|in:metro',
  32. 'v' => 'required|integer|in:1',
  33. 'scope' => 'required|in:all,hashtag,profile,remote,webfinger'
  34. ]);
  35. $scope = $request->input('scope') ?? 'all';
  36. $this->term = e(urldecode($request->input('q')));
  37. $this->hash = hash('sha256', $this->term);
  38. switch ($scope) {
  39. case 'all':
  40. $this->getHashtags();
  41. $this->getPosts();
  42. $this->getProfiles();
  43. break;
  44. case 'hashtag':
  45. $this->getHashtags();
  46. break;
  47. case 'profile':
  48. $this->getProfiles();
  49. break;
  50. case 'webfinger':
  51. $this->webfingerSearch();
  52. break;
  53. case 'remote':
  54. $this->remoteLookupSearch();
  55. break;
  56. default:
  57. break;
  58. }
  59. return response()->json($this->tokens, 200, [], JSON_PRETTY_PRINT);
  60. }
  61. protected function getPosts()
  62. {
  63. $tag = $this->term;
  64. $hash = hash('sha256', $tag);
  65. if( Helpers::validateUrl($tag) != false &&
  66. Helpers::validateLocalUrl($tag) != true &&
  67. config('federation.activitypub.enabled') == true &&
  68. config('federation.activitypub.remoteFollow') == true
  69. ) {
  70. $remote = Helpers::fetchFromUrl($tag);
  71. if( isset($remote['type']) &&
  72. $remote['type'] == 'Note') {
  73. $item = Helpers::statusFetch($tag);
  74. $this->tokens['posts'] = [[
  75. 'count' => 0,
  76. 'url' => $item->url(),
  77. 'type' => 'status',
  78. 'value' => "by {$item->profile->username} <span class='float-right'>{$item->created_at->diffForHumans(null, true, true)}</span>",
  79. 'tokens' => [$item->caption],
  80. 'name' => $item->caption,
  81. 'thumb' => $item->thumb(),
  82. ]];
  83. }
  84. } else {
  85. $posts = Status::select('id', 'profile_id', 'caption', 'created_at')
  86. ->whereHas('media')
  87. ->whereNull('in_reply_to_id')
  88. ->whereNull('reblog_of_id')
  89. ->whereProfileId(Auth::user()->profile_id)
  90. ->where('caption', 'like', '%'.$tag.'%')
  91. ->latest()
  92. ->limit(10)
  93. ->get();
  94. if($posts->count() > 0) {
  95. $posts = $posts->map(function($item, $key) {
  96. return [
  97. 'count' => 0,
  98. 'url' => $item->url(),
  99. 'type' => 'status',
  100. 'value' => "by {$item->profile->username} <span class='float-right'>{$item->created_at->diffForHumans(null, true, true)}</span>",
  101. 'tokens' => [$item->caption],
  102. 'name' => $item->caption,
  103. 'thumb' => $item->thumb(),
  104. 'filter' => $item->firstMedia()->filter_class
  105. ];
  106. });
  107. $this->tokens['posts'] = $posts;
  108. }
  109. }
  110. }
  111. protected function getHashtags()
  112. {
  113. $tag = $this->term;
  114. $key = $this->cacheKey . 'hashtags:' . $this->hash;
  115. $ttl = now()->addMinutes(1);
  116. $tokens = Cache::remember($key, $ttl, function() use($tag) {
  117. $htag = Str::startsWith($tag, '#') == true ? mb_substr($tag, 1) : $tag;
  118. $hashtags = Hashtag::select('id', 'name', 'slug')
  119. ->where('slug', 'like', '%'.$htag.'%')
  120. ->whereHas('posts')
  121. ->limit(20)
  122. ->get();
  123. if($hashtags->count() > 0) {
  124. $tags = $hashtags->map(function ($item, $key) {
  125. return [
  126. 'count' => $item->posts()->count(),
  127. 'url' => $item->url(),
  128. 'type' => 'hashtag',
  129. 'value' => $item->name,
  130. 'tokens' => '',
  131. 'name' => null,
  132. ];
  133. });
  134. return $tags;
  135. }
  136. });
  137. $this->tokens['hashtags'] = $tokens;
  138. }
  139. protected function getProfiles()
  140. {
  141. $tag = $this->term;
  142. $remoteKey = $this->cacheKey . 'profiles:remote:' . $this->hash;
  143. $key = $this->cacheKey . 'profiles:' . $this->hash;
  144. $remoteTtl = now()->addMinutes(15);
  145. $ttl = now()->addHours(2);
  146. if( Helpers::validateUrl($tag) != false &&
  147. Helpers::validateLocalUrl($tag) != true &&
  148. config('federation.activitypub.enabled') == true &&
  149. config('federation.activitypub.remoteFollow') == true
  150. ) {
  151. $remote = Helpers::fetchFromUrl($tag);
  152. if( isset($remote['type']) &&
  153. $remote['type'] == 'Person'
  154. ) {
  155. $this->tokens['profiles'] = Cache::remember($remoteKey, $remoteTtl, function() use($tag) {
  156. $item = Helpers::profileFirstOrNew($tag);
  157. $tokens = [[
  158. 'count' => 1,
  159. 'url' => $item->url(),
  160. 'type' => 'profile',
  161. 'value' => $item->username,
  162. 'tokens' => [$item->username],
  163. 'name' => $item->name,
  164. 'entity' => [
  165. 'id' => (string) $item->id,
  166. 'following' => $item->followedBy(Auth::user()->profile),
  167. 'follow_request' => $item->hasFollowRequestById(Auth::user()->profile_id),
  168. 'thumb' => $item->avatarUrl(),
  169. 'local' => (bool) !$item->domain,
  170. 'post_count' => $item->statuses()->count()
  171. ]
  172. ]];
  173. return $tokens;
  174. });
  175. }
  176. }
  177. else {
  178. $this->tokens['profiles'] = Cache::remember($key, $ttl, function() use($tag) {
  179. if(Str::startsWith($tag, '@')) {
  180. $tag = substr($tag, 1);
  181. }
  182. $users = Profile::select('status', 'domain', 'username', 'name', 'id')
  183. ->whereNull('status')
  184. ->where('username', 'like', '%'.$tag.'%')
  185. ->limit(20)
  186. ->orderBy('domain')
  187. ->get();
  188. if($users->count() > 0) {
  189. return $users->map(function ($item, $key) {
  190. return [
  191. 'count' => 0,
  192. 'url' => $item->url(),
  193. 'type' => 'profile',
  194. 'value' => $item->username,
  195. 'tokens' => [$item->username],
  196. 'name' => $item->name,
  197. 'avatar' => $item->avatarUrl(),
  198. 'id' => (string) $item->id,
  199. 'entity' => [
  200. 'id' => (string) $item->id,
  201. 'following' => $item->followedBy(Auth::user()->profile),
  202. 'follow_request' => $item->hasFollowRequestById(Auth::user()->profile_id),
  203. 'thumb' => $item->avatarUrl(),
  204. 'local' => (bool) !$item->domain,
  205. 'post_count' => $item->statuses()->count()
  206. ]
  207. ];
  208. });
  209. }
  210. });
  211. }
  212. }
  213. public function results(Request $request)
  214. {
  215. $this->validate($request, [
  216. 'q' => 'required|string|min:1',
  217. ]);
  218. return view('search.results');
  219. }
  220. protected function webfingerSearch()
  221. {
  222. $wfs = WebfingerService::lookup($this->term);
  223. if(empty($wfs)) {
  224. return;
  225. }
  226. $this->tokens['profiles'] = [
  227. [
  228. 'count' => 1,
  229. 'url' => $wfs['url'],
  230. 'type' => 'profile',
  231. 'value' => $wfs['username'],
  232. 'tokens' => [$wfs['username']],
  233. 'name' => $wfs['display_name'],
  234. 'entity' => [
  235. 'id' => (string) $wfs['id'],
  236. 'following' => null,
  237. 'follow_request' => null,
  238. 'thumb' => $wfs['avatar'],
  239. 'local' => (bool) $wfs['local']
  240. ]
  241. ]
  242. ];
  243. return;
  244. }
  245. protected function remotePostLookup()
  246. {
  247. $tag = $this->term;
  248. $hash = hash('sha256', $tag);
  249. $local = Helpers::validateLocalUrl($tag);
  250. $valid = Helpers::validateUrl($tag);
  251. if($valid == false || $local == true) {
  252. return;
  253. }
  254. if(Status::whereUri($tag)->whereLocal(false)->exists()) {
  255. $item = Status::whereUri($tag)->first();
  256. $this->tokens['posts'] = [[
  257. 'count' => 0,
  258. 'url' => "/i/web/post/_/$item->profile_id/$item->id",
  259. 'type' => 'status',
  260. 'username' => $item->profile->username,
  261. 'caption' => $item->rendered ?? $item->caption,
  262. 'thumb' => $item->firstMedia()->remote_url,
  263. 'timestamp' => $item->created_at->diffForHumans()
  264. ]];
  265. }
  266. $remote = Helpers::fetchFromUrl($tag);
  267. if(isset($remote['type']) && $remote['type'] == 'Note') {
  268. $item = Helpers::statusFetch($tag);
  269. $this->tokens['posts'] = [[
  270. 'count' => 0,
  271. 'url' => "/i/web/post/_/$item->profile_id/$item->id",
  272. 'type' => 'status',
  273. 'username' => $item->profile->username,
  274. 'caption' => $item->rendered ?? $item->caption,
  275. 'thumb' => $item->firstMedia()->remote_url,
  276. 'timestamp' => $item->created_at->diffForHumans()
  277. ]];
  278. }
  279. }
  280. protected function remoteLookupSearch()
  281. {
  282. if(!Helpers::validateUrl($this->term)) {
  283. return;
  284. }
  285. $this->getProfiles();
  286. $this->remotePostLookup();
  287. }
  288. }