AvatarSync.php 4.5 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219
  1. <?php
  2. namespace App\Console\Commands;
  3. use Illuminate\Console\Command;
  4. use App\Avatar;
  5. use App\Profile;
  6. use App\Jobs\AvatarPipeline\RemoteAvatarFetch;
  7. use App\Util\ActivityPub\Helpers;
  8. use Illuminate\Support\Facades\DB;
  9. use Illuminate\Support\Str;
  10. class AvatarSync extends Command
  11. {
  12. /**
  13. * The name and signature of the console command.
  14. *
  15. * @var string
  16. */
  17. protected $signature = 'avatars:sync';
  18. /**
  19. * The console command description.
  20. *
  21. * @var string
  22. */
  23. protected $description = 'Perform actions on avatars';
  24. public $found = 0;
  25. public $notFetched = 0;
  26. public $fixed = 0;
  27. /**
  28. * Create a new command instance.
  29. *
  30. * @return void
  31. */
  32. public function __construct()
  33. {
  34. parent::__construct();
  35. }
  36. /**
  37. * Execute the console command.
  38. *
  39. * @return int
  40. */
  41. public function handle()
  42. {
  43. $this->info('Welcome to the avatar sync manager');
  44. $actions = [
  45. 'Analyze',
  46. 'Full Analyze',
  47. 'Fetch - Fetch missing remote avatars',
  48. 'Fix - Fix remote accounts without avatar record',
  49. 'Sync - Store latest remote avatars',
  50. ];
  51. $name = $this->choice(
  52. 'Select an action',
  53. $actions,
  54. 0,
  55. 1,
  56. false
  57. );
  58. $this->info('Selected: ' . $name);
  59. switch($name) {
  60. case $actions[0]:
  61. $this->analyze();
  62. break;
  63. case $actions[1]:
  64. $this->fullAnalyze();
  65. break;
  66. case $actions[2]:
  67. $this->fetch();
  68. break;
  69. case $actions[3]:
  70. $this->fix();
  71. break;
  72. case $actions[4]:
  73. $this->sync();
  74. break;
  75. }
  76. return Command::SUCCESS;
  77. }
  78. protected function incr($name)
  79. {
  80. switch($name) {
  81. case 'found':
  82. $this->found = $this->found + 1;
  83. break;
  84. case 'notFetched':
  85. $this->notFetched = $this->notFetched + 1;
  86. break;
  87. case 'fixed':
  88. $this->fixed++;
  89. break;
  90. }
  91. }
  92. protected function analyze()
  93. {
  94. $count = Avatar::whereIsRemote(true)->whereNull('cdn_url')->count();
  95. $this->info('Found ' . $count . ' profiles with blank avatars.');
  96. $this->line(' ');
  97. $this->comment('We suggest running php artisan avatars:sync again and selecting the sync option');
  98. $this->line(' ');
  99. }
  100. protected function fullAnalyze()
  101. {
  102. $count = Profile::count();
  103. $bar = $this->output->createProgressBar($count);
  104. $bar->start();
  105. Profile::chunk(5000, function($profiles) use ($bar) {
  106. foreach($profiles as $profile) {
  107. if($profile->domain == null) {
  108. $bar->advance();
  109. continue;
  110. }
  111. $avatar = Avatar::whereProfileId($profile->id)->first();
  112. if(!$avatar || $avatar->cdn_url == null) {
  113. $this->incr('notFetched');
  114. }
  115. $this->incr('found');
  116. $bar->advance();
  117. }
  118. });
  119. $this->line(' ');
  120. $this->line(' ');
  121. $this->info('Found ' . $this->found . ' remote accounts');
  122. $this->info('Found ' . $this->notFetched . ' remote avatars to fetch');
  123. }
  124. protected function fetch()
  125. {
  126. $this->info('Fetching ....');
  127. Avatar::whereIsRemote(true)
  128. ->whereNull('cdn_url')
  129. // ->with('profile')
  130. ->chunk(10, function($avatars) {
  131. foreach($avatars as $avatar) {
  132. if(!$avatar || !$avatar->profile) {
  133. continue;
  134. }
  135. $url = $avatar->profile->remote_url;
  136. if(!$url || !Helpers::validateUrl($url)) {
  137. continue;
  138. }
  139. try {
  140. $res = Helpers::fetchFromUrl($url);
  141. if(
  142. !is_array($res) ||
  143. !isset($res['@context']) ||
  144. !isset($res['icon']) ||
  145. !isset($res['icon']['type']) ||
  146. !isset($res['icon']['url']) ||
  147. !Str::endsWith($res['icon']['url'], ['.png', '.jpg', '.jpeg'])
  148. ) {
  149. continue;
  150. }
  151. } catch (\GuzzleHttp\Exception\RequestException $e) {
  152. continue;
  153. } catch(\Illuminate\Http\Client\ConnectionException $e) {
  154. continue;
  155. }
  156. $avatar->remote_url = $res['icon']['url'];
  157. $avatar->save();
  158. RemoteAvatarFetch::dispatch($avatar->profile);
  159. }
  160. });
  161. }
  162. protected function fix()
  163. {
  164. Profile::chunk(5000, function($profiles) {
  165. foreach($profiles as $profile) {
  166. if($profile->domain == null || $profile->private_key) {
  167. continue;
  168. }
  169. $avatar = Avatar::whereProfileId($profile->id)->first();
  170. if($avatar) {
  171. continue;
  172. }
  173. $avatar = new Avatar;
  174. $avatar->is_remote = true;
  175. $avatar->profile_id = $profile->id;
  176. $avatar->save();
  177. $this->incr('fixed');
  178. }
  179. });
  180. $this->line(' ');
  181. $this->line(' ');
  182. $this->info('Fixed ' . $this->fixed . ' accounts with a blank avatar');
  183. }
  184. protected function sync()
  185. {
  186. Avatar::whereIsRemote(true)
  187. ->with('profile')
  188. ->chunk(10, function($avatars) {
  189. foreach($avatars as $avatar) {
  190. RemoteAvatarFetch::dispatch($avatar->profile);
  191. }
  192. });
  193. }
  194. }