SearchController.php 13 KB

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