FollowerService.php 8.1 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250
  1. <?php
  2. namespace App\Services;
  3. use Illuminate\Support\Facades\Redis;
  4. use Cache;
  5. use DB;
  6. use App\{
  7. Follower,
  8. Profile,
  9. User
  10. };
  11. use App\Jobs\FollowPipeline\FollowServiceWarmCache;
  12. class FollowerService
  13. {
  14. const CACHE_KEY = 'pf:services:followers:';
  15. const FOLLOWERS_SYNC_KEY = 'pf:services:followers:sync-followers:';
  16. const FOLLOWING_SYNC_KEY = 'pf:services:followers:sync-following:';
  17. const FOLLOWING_KEY = 'pf:services:follow:following:id:';
  18. const FOLLOWERS_KEY = 'pf:services:follow:followers:id:';
  19. const FOLLOWERS_LOCAL_KEY = 'pf:services:follow:local-follower-ids:';
  20. const FOLLOWERS_INTER_KEY = 'pf:services:follow:followers:inter:id:';
  21. public static function add($actor, $target, $refresh = true)
  22. {
  23. $ts = (int) microtime(true);
  24. if($refresh) {
  25. RelationshipService::refresh($actor, $target);
  26. } else {
  27. RelationshipService::forget($actor, $target);
  28. }
  29. Redis::zadd(self::FOLLOWING_KEY . $actor, $ts, $target);
  30. Redis::zadd(self::FOLLOWERS_KEY . $target, $ts, $actor);
  31. Cache::forget('profile:following:' . $actor);
  32. }
  33. public static function remove($actor, $target, $silent = false)
  34. {
  35. Redis::zrem(self::FOLLOWING_KEY . $actor, $target);
  36. Redis::zrem(self::FOLLOWERS_KEY . $target, $actor);
  37. if($silent !== true) {
  38. AccountService::del($actor);
  39. AccountService::del($target);
  40. RelationshipService::refresh($actor, $target);
  41. Cache::forget('profile:following:' . $actor);
  42. } else {
  43. RelationshipService::forget($actor, $target);
  44. }
  45. }
  46. public static function followers($id, $start = 0, $stop = 10)
  47. {
  48. self::cacheSyncCheck($id, 'followers');
  49. return Redis::zrevrange(self::FOLLOWERS_KEY . $id, $start, $stop);
  50. }
  51. public static function following($id, $start = 0, $stop = 10)
  52. {
  53. self::cacheSyncCheck($id, 'following');
  54. return Redis::zrevrange(self::FOLLOWING_KEY . $id, $start, $stop);
  55. }
  56. public static function followersPaginate($id, $page = 1, $limit = 10)
  57. {
  58. $start = $page == 1 ? 0 : $page * $limit - $limit;
  59. $end = $start + ($limit - 1);
  60. return self::followers($id, $start, $end);
  61. }
  62. public static function followingPaginate($id, $page = 1, $limit = 10)
  63. {
  64. $start = $page == 1 ? 0 : $page * $limit - $limit;
  65. $end = $start + ($limit - 1);
  66. return self::following($id, $start, $end);
  67. }
  68. public static function followerCount($id, $warmCache = true)
  69. {
  70. if($warmCache) {
  71. self::cacheSyncCheck($id, 'followers');
  72. }
  73. return Redis::zCard(self::FOLLOWERS_KEY . $id);
  74. }
  75. public static function followingCount($id, $warmCache = true)
  76. {
  77. if($warmCache) {
  78. self::cacheSyncCheck($id, 'following');
  79. }
  80. return Redis::zCard(self::FOLLOWING_KEY . $id);
  81. }
  82. public static function follows(string $actor, string $target, $quickCheck = false)
  83. {
  84. if($actor == $target) {
  85. return false;
  86. }
  87. if($quickCheck) {
  88. return (bool) Redis::zScore(self::FOLLOWERS_KEY . $target, $actor);
  89. }
  90. if(self::followerCount($target, false) && self::followingCount($actor, false)) {
  91. self::cacheSyncCheck($target, 'followers');
  92. return (bool) Redis::zScore(self::FOLLOWERS_KEY . $target, $actor);
  93. } else {
  94. self::cacheSyncCheck($target, 'followers');
  95. self::cacheSyncCheck($actor, 'following');
  96. return Follower::whereProfileId($actor)->whereFollowingId($target)->exists();
  97. }
  98. }
  99. public static function cacheSyncCheck($id, $scope = 'followers')
  100. {
  101. if($scope === 'followers') {
  102. if(Cache::get(self::FOLLOWERS_SYNC_KEY . $id) != null) {
  103. return;
  104. }
  105. FollowServiceWarmCache::dispatch($id)->onQueue('low');
  106. }
  107. if($scope === 'following') {
  108. if(Cache::get(self::FOLLOWING_SYNC_KEY . $id) != null) {
  109. return;
  110. }
  111. FollowServiceWarmCache::dispatch($id)->onQueue('low');
  112. }
  113. return;
  114. }
  115. public static function audience($profile, $scope = null)
  116. {
  117. return (new self)->getAudienceInboxes($profile, $scope);
  118. }
  119. public static function softwareAudience($profile, $software = 'pixelfed')
  120. {
  121. return collect(self::audience($profile))
  122. ->filter(function($inbox) use($software) {
  123. $domain = parse_url($inbox, PHP_URL_HOST);
  124. if(!$domain) {
  125. return false;
  126. }
  127. return InstanceService::software($domain) === strtolower($software);
  128. })
  129. ->unique()
  130. ->values()
  131. ->toArray();
  132. }
  133. protected function getAudienceInboxes($pid, $scope = null)
  134. {
  135. $key = 'pf:services:follower:audience:' . $pid;
  136. $domains = Cache::remember($key, 432000, function() use($pid) {
  137. $profile = Profile::whereNull(['status', 'domain'])->find($pid);
  138. if(!$profile) {
  139. return [];
  140. }
  141. return $profile
  142. ->followers()
  143. ->get()
  144. ->map(function($follow) {
  145. return $follow->sharedInbox ?? $follow->inbox_url;
  146. })
  147. ->filter()
  148. ->unique()
  149. ->values();
  150. });
  151. if(!$domains || !$domains->count()) {
  152. return [];
  153. }
  154. $banned = InstanceService::getBannedDomains();
  155. if(!$banned || count($banned) === 0) {
  156. return $domains->toArray();
  157. }
  158. $res = $domains->filter(function($domain) use($banned) {
  159. $parsed = parse_url($domain, PHP_URL_HOST);
  160. return !in_array($parsed, $banned);
  161. })
  162. ->values()
  163. ->toArray();
  164. return $res;
  165. }
  166. public static function mutualCount($pid, $mid)
  167. {
  168. return Cache::remember(self::CACHE_KEY . ':mutualcount:' . $pid . ':' . $mid, 3600, function() use($pid, $mid) {
  169. return DB::table('followers as u')
  170. ->join('followers as s', 'u.following_id', '=', 's.following_id')
  171. ->where('s.profile_id', $mid)
  172. ->where('u.profile_id', $pid)
  173. ->count();
  174. });
  175. }
  176. public static function mutualIds($pid, $mid, $limit = 3)
  177. {
  178. $key = self::CACHE_KEY . ':mutualids:' . $pid . ':' . $mid . ':limit_' . $limit;
  179. return Cache::remember($key, 3600, function() use($pid, $mid, $limit) {
  180. return DB::table('followers as u')
  181. ->join('followers as s', 'u.following_id', '=', 's.following_id')
  182. ->where('s.profile_id', $mid)
  183. ->where('u.profile_id', $pid)
  184. ->limit($limit)
  185. ->pluck('s.following_id')
  186. ->toArray();
  187. });
  188. }
  189. public static function mutualAccounts($actorId, $profileId)
  190. {
  191. if($actorId == $profileId) {
  192. return [];
  193. }
  194. $actorKey = self::FOLLOWING_KEY . $actorId;
  195. $profileKey = self::FOLLOWERS_KEY . $profileId;
  196. $key = self::FOLLOWERS_INTER_KEY . $actorId . ':' . $profileId;
  197. $res = Redis::zinterstore($key, [$actorKey, $profileKey]);
  198. if($res) {
  199. return Redis::zrange($key, 0, -1);
  200. } else {
  201. return [];
  202. }
  203. }
  204. public static function delCache($id)
  205. {
  206. Redis::del(self::CACHE_KEY . $id);
  207. Redis::del(self::FOLLOWING_KEY . $id);
  208. Redis::del(self::FOLLOWERS_KEY . $id);
  209. Cache::forget(self::FOLLOWERS_SYNC_KEY . $id);
  210. Cache::forget(self::FOLLOWING_SYNC_KEY . $id);
  211. }
  212. public static function localFollowerIds($pid, $limit = 0)
  213. {
  214. $key = self::FOLLOWERS_LOCAL_KEY . $pid;
  215. $res = Cache::remember($key, 7200, function() use($pid) {
  216. return DB::table('followers')->whereFollowingId($pid)->whereLocalProfile(true)->pluck('profile_id')->sort();
  217. });
  218. return $limit ?
  219. $res->take($limit)->values()->toArray() :
  220. $res->values()->toArray();
  221. }
  222. }