1
0

SearchController.php 13 KB

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