FollowerService.php 9.3 KB

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