Browse Source

Refactor snowflake id generation to improve randomness

Daniel Supernault 3 years ago
parent
commit
e5aea490b1
9 changed files with 369 additions and 345 deletions
  1. 1 1
      app/Collection.php
  2. 1 1
      app/CollectionItem.php
  3. 19 0
      app/HasSnowflakePrimary.php
  4. 15 15
      app/Models/Poll.php
  5. 0 1
      app/Place.php
  6. 315 315
      app/Profile.php
  7. 16 10
      app/Services/SnowflakeService.php
  8. 1 1
      app/Status.php
  9. 1 1
      app/Story.php

+ 1 - 1
app/Collection.php

@@ -4,7 +4,7 @@ namespace App;
 
 use Illuminate\Support\Str;
 use Illuminate\Database\Eloquent\Model;
-use Pixelfed\Snowflake\HasSnowflakePrimary;
+use App\HasSnowflakePrimary;
 
 class Collection extends Model
 {

+ 1 - 1
app/CollectionItem.php

@@ -3,7 +3,7 @@
 namespace App;
 
 use Illuminate\Database\Eloquent\Model;
-use Pixelfed\Snowflake\HasSnowflakePrimary;
+use App\HasSnowflakePrimary;
 
 class CollectionItem extends Model
 {

+ 19 - 0
app/HasSnowflakePrimary.php

@@ -0,0 +1,19 @@
+<?php
+
+namespace App;
+
+use App\Services\SnowflakeService;
+
+trait HasSnowflakePrimary
+{
+	public static function bootHasSnowflakePrimary()
+	{
+		static::saving(function ($model) {
+			if (is_null($model->getKey())) {
+				$keyName = $model->getKeyName();
+				$id = SnowflakeService::next();
+				$model->setAttribute($keyName, $id);
+			}
+		});
+	}
+}

+ 15 - 15
app/Models/Poll.php

@@ -4,11 +4,11 @@ namespace App\Models;
 
 use Illuminate\Database\Eloquent\Factories\HasFactory;
 use Illuminate\Database\Eloquent\Model;
-use Pixelfed\Snowflake\HasSnowflakePrimary;
+use App\HasSnowflakePrimary;
 
 class Poll extends Model
 {
-    use HasSnowflakePrimary, HasFactory;
+	use HasSnowflakePrimary, HasFactory;
 
 	/**
 	 * Indicates if the IDs are auto-incrementing.
@@ -17,19 +17,19 @@ class Poll extends Model
 	 */
 	public $incrementing = false;
 
-    protected $casts = [
-        'poll_options' => 'array',
-        'cached_tallies' => 'array',
-        'expires_at' => 'datetime'
-    ];
+	protected $casts = [
+		'poll_options' => 'array',
+		'cached_tallies' => 'array',
+		'expires_at' => 'datetime'
+	];
 
-    public function votes()
-    {
-    	return $this->hasMany(PollVote::class);
-    }
+	public function votes()
+	{
+		return $this->hasMany(PollVote::class);
+	}
 
-    public function getTallies()
-    {
-    	return $this->cached_tallies;
-    }
+	public function getTallies()
+	{
+		return $this->cached_tallies;
+	}
 }

+ 0 - 1
app/Place.php

@@ -3,7 +3,6 @@
 namespace App;
 
 use Illuminate\Database\Eloquent\Model;
-use Pixelfed\Snowflake\HasSnowflakePrimary;
 
 class Place extends Model
 {

+ 315 - 315
app/Profile.php

@@ -4,324 +4,324 @@ namespace App;
 
 use Auth, Cache, DB, Storage;
 use App\Util\Lexer\PrettyNumber;
-use Pixelfed\Snowflake\HasSnowflakePrimary;
+use App\HasSnowflakePrimary;
 use Illuminate\Database\Eloquent\{Model, SoftDeletes};
 use App\Services\FollowerService;
 
 class Profile extends Model
 {
-    use HasSnowflakePrimary, SoftDeletes;
-
-    /**
-     * Indicates if the IDs are auto-incrementing.
-     *
-     * @var bool
-     */
-    public $incrementing = false;
-
-    protected $dates = [
-        'deleted_at',
-        'last_fetched_at'
-    ];
-    protected $hidden = ['private_key'];
-    protected $visible = ['id', 'user_id', 'username', 'name'];
-    protected $fillable = ['user_id'];
-
-    public function user()
-    {
-        return $this->belongsTo(User::class);
-    }
-
-    public function url($suffix = null)
-    {
-        return $this->remote_url ?? url($this->username . $suffix);
-    }
-
-    public function localUrl($suffix = null)
-    {
-        return url($this->username . $suffix);
-    }
-
-    public function permalink($suffix = null)
-    {
-        return $this->remote_url ?? url('users/' . $this->username . $suffix);
-    }
-
-    public function emailUrl()
-    {
-        if($this->domain) {
-            return $this->username;
-        }
-
-        $domain = parse_url(config('app.url'), PHP_URL_HOST);
-
-        return $this->username.'@'.$domain;
-    }
-
-    public function statuses()
-    {
-        return $this->hasMany(Status::class);
-    }
-
-    public function followingCount($short = false)
-    {
-        $count = Cache::remember('profile:following_count:'.$this->id, now()->addMonths(1), function() {
-            if($this->domain == null && $this->user->settings->show_profile_following_count == false) {
-                return 0;
-            }
-            $count = DB::table('followers')->select('following_id')->where('following_id', $this->id)->count();
-            if($this->following_count != $count) {
-                $this->following_count = $count;
-                $this->save();
-            }
-            return $count;
-        });
-
-        return $short ? PrettyNumber::convert($count) : $count;
-    }
-
-    public function followerCount($short = false)
-    {
-        $count = Cache::remember('profile:follower_count:'.$this->id, now()->addMonths(1), function() {
-            if($this->domain == null && $this->user->settings->show_profile_follower_count == false) {
-                return 0;
-            }
-            $count = $this->followers()->count();
-            if($this->followers_count != $count) {
-                $this->followers_count = $count;
-                $this->save();
-            }
-            return $count;
-        });
-        return $short ? PrettyNumber::convert($count) : $count;
-    }
-
-    public function statusCount()
-    {
-        return $this->status_count;
-    }
-
-    public function following()
-    {
-        return $this->belongsToMany(
-            self::class,
-            'followers',
-            'profile_id',
-            'following_id'
-        );
-    }
-
-    public function followers()
-    {
-        return $this->belongsToMany(
-            self::class,
-            'followers',
-            'following_id',
-            'profile_id'
-        );
-    }
-
-    public function follows($profile) : bool
-    {
-        return Follower::whereProfileId($this->id)->whereFollowingId($profile->id)->exists();
-    }
-
-    public function followedBy($profile) : bool
-    {
-        return Follower::whereProfileId($profile->id)->whereFollowingId($this->id)->exists();
-    }
-
-    public function bookmarks()
-    {
-        return $this->belongsToMany(
-            Status::class,
-            'bookmarks',
-            'profile_id',
-            'status_id'
-        );
-    }
-
-    public function likes()
-    {
-        return $this->hasMany(Like::class);
-    }
-
-    public function avatar()
-    {
-        return $this->hasOne(Avatar::class)->withDefault([
-            'media_path' => 'public/avatars/default.jpg',
-            'change_count' => 0
-        ]);
-    }
-
-    public function avatarUrl()
-    {
-        $url = Cache::remember('avatar:'.$this->id, now()->addYears(1), function () {
-            $avatar = $this->avatar;
-
-            if($avatar->cdn_url) {
-                return $avatar->cdn_url ?? url('/storage/avatars/default.jpg');
-            }
-
-            if($avatar->is_remote) {
-                return $avatar->cdn_url ?? url('/storage/avatars/default.jpg');
-            }
-            
-            $path = $avatar->media_path;
-            $path = "{$path}?v={$avatar->change_count}";
-
-            return config('app.url') . Storage::url($path);
-        });
-
-        return $url;
-    }
-
-    // deprecated
-    public function recommendFollowers()
-    {
-        return collect([]);
-    }
-
-    public function keyId()
-    {
-        if ($this->remote_url) {
-            return;
-        }
-
-        return $this->permalink('#main-key');
-    }
-
-    public function mutedIds()
-    {
-        return UserFilter::whereUserId($this->id)
-            ->whereFilterableType('App\Profile')
-            ->whereFilterType('mute')
-            ->pluck('filterable_id');
-    }
-
-    public function blockedIds()
-    {
-        return UserFilter::whereUserId($this->id)
-            ->whereFilterableType('App\Profile')
-            ->whereFilterType('block')
-            ->pluck('filterable_id');
-    }
-
-    public function mutedProfileUrls()
-    {
-        $ids = $this->mutedIds();
-        return $this->whereIn('id', $ids)->get()->map(function($i) {
-            return $i->url();
-        });
-    }
-
-    public function blockedProfileUrls()
-    {
-        $ids = $this->blockedIds();
-        return $this->whereIn('id', $ids)->get()->map(function($i) {
-            return $i->url();
-        });
-    }
-
-    public function reports()
-    {
-        return $this->hasMany(Report::class, 'profile_id');
-    }
-
-    public function media()
-    {
-        return $this->hasMany(Media::class, 'profile_id');
-    }
-
-    public function inboxUrl()
-    {
-        return $this->inbox_url ?? $this->permalink('/inbox');
-    }
-
-    public function outboxUrl()
-    {
-        return $this->outbox_url ?? $this->permalink('/outbox');
-    }
-
-    public function sharedInbox()
-    {
-        return $this->sharedInbox ?? $this->inboxUrl();
-    }
-
-    public function getDefaultScope()
-    {
-        return $this->is_private == true ? 'private' : 'public';
-    }
-
-    public function getAudience($scope = false)
-    {
-        if($this->remote_url) {
-            return [];
-        }
-        $scope = $scope ?? $this->getDefaultScope();
-        $audience = [];
-        switch ($scope) {
-            case 'public':
-                $audience = [
-                    'to' => [
-                        'https://www.w3.org/ns/activitystreams#Public'
-                    ],
-                    'cc' => [
-                        $this->permalink('/followers')
-                    ]
-                ];
-                break;
-        }
-        return $audience;
-    }
-
-    public function getAudienceInbox($scope = 'public')
-    {
-        return FollowerService::audience($this->id, $scope);
-    }
-
-    public function circles()
-    {
-        return $this->hasMany(Circle::class);
-    }
-
-    public function hashtags()
-    {
-        return $this->hasManyThrough(
-            Hashtag::class,
-            StatusHashtag::class,
-            'profile_id',
-            'id',
-            'id',
-            'hashtag_id'
-        );
-    }
-
-    public function hashtagFollowing()
-    {
-        return $this->hasMany(HashtagFollow::class);
-    }
-
-    public function collections()
-    {
-        return $this->hasMany(Collection::class);
-    }
-
-    public function hasFollowRequestById(int $id)
-    {
-        return FollowRequest::whereFollowerId($id)
-            ->whereFollowingId($this->id)
-            ->exists();
-    }
-
-    public function stories()
-    {
-        return $this->hasMany(Story::class);
-    }
-
-
-    public function reported()
-    {
-        return $this->hasMany(Report::class, 'object_id');
-    }
+	use HasSnowflakePrimary, SoftDeletes;
+
+	/**
+	 * Indicates if the IDs are auto-incrementing.
+	 *
+	 * @var bool
+	 */
+	public $incrementing = false;
+
+	protected $dates = [
+		'deleted_at',
+		'last_fetched_at'
+	];
+	protected $hidden = ['private_key'];
+	protected $visible = ['id', 'user_id', 'username', 'name'];
+	protected $fillable = ['user_id'];
+
+	public function user()
+	{
+		return $this->belongsTo(User::class);
+	}
+
+	public function url($suffix = null)
+	{
+		return $this->remote_url ?? url($this->username . $suffix);
+	}
+
+	public function localUrl($suffix = null)
+	{
+		return url($this->username . $suffix);
+	}
+
+	public function permalink($suffix = null)
+	{
+		return $this->remote_url ?? url('users/' . $this->username . $suffix);
+	}
+
+	public function emailUrl()
+	{
+		if($this->domain) {
+			return $this->username;
+		}
+
+		$domain = parse_url(config('app.url'), PHP_URL_HOST);
+
+		return $this->username.'@'.$domain;
+	}
+
+	public function statuses()
+	{
+		return $this->hasMany(Status::class);
+	}
+
+	public function followingCount($short = false)
+	{
+		$count = Cache::remember('profile:following_count:'.$this->id, now()->addMonths(1), function() {
+			if($this->domain == null && $this->user->settings->show_profile_following_count == false) {
+				return 0;
+			}
+			$count = DB::table('followers')->select('following_id')->where('following_id', $this->id)->count();
+			if($this->following_count != $count) {
+				$this->following_count = $count;
+				$this->save();
+			}
+			return $count;
+		});
+
+		return $short ? PrettyNumber::convert($count) : $count;
+	}
+
+	public function followerCount($short = false)
+	{
+		$count = Cache::remember('profile:follower_count:'.$this->id, now()->addMonths(1), function() {
+			if($this->domain == null && $this->user->settings->show_profile_follower_count == false) {
+				return 0;
+			}
+			$count = $this->followers()->count();
+			if($this->followers_count != $count) {
+				$this->followers_count = $count;
+				$this->save();
+			}
+			return $count;
+		});
+		return $short ? PrettyNumber::convert($count) : $count;
+	}
+
+	public function statusCount()
+	{
+		return $this->status_count;
+	}
+
+	public function following()
+	{
+		return $this->belongsToMany(
+			self::class,
+			'followers',
+			'profile_id',
+			'following_id'
+		);
+	}
+
+	public function followers()
+	{
+		return $this->belongsToMany(
+			self::class,
+			'followers',
+			'following_id',
+			'profile_id'
+		);
+	}
+
+	public function follows($profile) : bool
+	{
+		return Follower::whereProfileId($this->id)->whereFollowingId($profile->id)->exists();
+	}
+
+	public function followedBy($profile) : bool
+	{
+		return Follower::whereProfileId($profile->id)->whereFollowingId($this->id)->exists();
+	}
+
+	public function bookmarks()
+	{
+		return $this->belongsToMany(
+			Status::class,
+			'bookmarks',
+			'profile_id',
+			'status_id'
+		);
+	}
+
+	public function likes()
+	{
+		return $this->hasMany(Like::class);
+	}
+
+	public function avatar()
+	{
+		return $this->hasOne(Avatar::class)->withDefault([
+			'media_path' => 'public/avatars/default.jpg',
+			'change_count' => 0
+		]);
+	}
+
+	public function avatarUrl()
+	{
+		$url = Cache::remember('avatar:'.$this->id, now()->addYears(1), function () {
+			$avatar = $this->avatar;
+
+			if($avatar->cdn_url) {
+				return $avatar->cdn_url ?? url('/storage/avatars/default.jpg');
+			}
+
+			if($avatar->is_remote) {
+				return $avatar->cdn_url ?? url('/storage/avatars/default.jpg');
+			}
+			
+			$path = $avatar->media_path;
+			$path = "{$path}?v={$avatar->change_count}";
+
+			return config('app.url') . Storage::url($path);
+		});
+
+		return $url;
+	}
+
+	// deprecated
+	public function recommendFollowers()
+	{
+		return collect([]);
+	}
+
+	public function keyId()
+	{
+		if ($this->remote_url) {
+			return;
+		}
+
+		return $this->permalink('#main-key');
+	}
+
+	public function mutedIds()
+	{
+		return UserFilter::whereUserId($this->id)
+			->whereFilterableType('App\Profile')
+			->whereFilterType('mute')
+			->pluck('filterable_id');
+	}
+
+	public function blockedIds()
+	{
+		return UserFilter::whereUserId($this->id)
+			->whereFilterableType('App\Profile')
+			->whereFilterType('block')
+			->pluck('filterable_id');
+	}
+
+	public function mutedProfileUrls()
+	{
+		$ids = $this->mutedIds();
+		return $this->whereIn('id', $ids)->get()->map(function($i) {
+			return $i->url();
+		});
+	}
+
+	public function blockedProfileUrls()
+	{
+		$ids = $this->blockedIds();
+		return $this->whereIn('id', $ids)->get()->map(function($i) {
+			return $i->url();
+		});
+	}
+
+	public function reports()
+	{
+		return $this->hasMany(Report::class, 'profile_id');
+	}
+
+	public function media()
+	{
+		return $this->hasMany(Media::class, 'profile_id');
+	}
+
+	public function inboxUrl()
+	{
+		return $this->inbox_url ?? $this->permalink('/inbox');
+	}
+
+	public function outboxUrl()
+	{
+		return $this->outbox_url ?? $this->permalink('/outbox');
+	}
+
+	public function sharedInbox()
+	{
+		return $this->sharedInbox ?? $this->inboxUrl();
+	}
+
+	public function getDefaultScope()
+	{
+		return $this->is_private == true ? 'private' : 'public';
+	}
+
+	public function getAudience($scope = false)
+	{
+		if($this->remote_url) {
+			return [];
+		}
+		$scope = $scope ?? $this->getDefaultScope();
+		$audience = [];
+		switch ($scope) {
+			case 'public':
+				$audience = [
+					'to' => [
+						'https://www.w3.org/ns/activitystreams#Public'
+					],
+					'cc' => [
+						$this->permalink('/followers')
+					]
+				];
+				break;
+		}
+		return $audience;
+	}
+
+	public function getAudienceInbox($scope = 'public')
+	{
+		return FollowerService::audience($this->id, $scope);
+	}
+
+	public function circles()
+	{
+		return $this->hasMany(Circle::class);
+	}
+
+	public function hashtags()
+	{
+		return $this->hasManyThrough(
+			Hashtag::class,
+			StatusHashtag::class,
+			'profile_id',
+			'id',
+			'id',
+			'hashtag_id'
+		);
+	}
+
+	public function hashtagFollowing()
+	{
+		return $this->hasMany(HashtagFollow::class);
+	}
+
+	public function collections()
+	{
+		return $this->hasMany(Collection::class);
+	}
+
+	public function hasFollowRequestById(int $id)
+	{
+		return FollowRequest::whereFollowerId($id)
+			->whereFollowingId($this->id)
+			->exists();
+	}
+
+	public function stories()
+	{
+		return $this->hasMany(Story::class);
+	}
+
+
+	public function reported()
+	{
+		return $this->hasMany(Report::class, 'object_id');
+	}
 }

+ 16 - 10
app/Services/SnowflakeService.php

@@ -8,6 +8,20 @@ use Cache;
 class SnowflakeService {
 
 	public static function byDate(Carbon $ts = null)
+	{
+		if($ts instanceOf Carbon) {
+			$ts = now()->parse($ts)->timestamp;
+		} else {
+			return self::next();
+		}
+
+		return ((round($ts * 1000) - 1549756800000) << 22)
+		| (random_int(1,31) << 17)
+		| (random_int(1,31) << 12)
+		| $seq;
+	}
+
+	public static function next()
 	{
 		$seq = Cache::get('snowflake:seq');
 
@@ -19,19 +33,11 @@ class SnowflakeService {
 		}
 
 		if($seq >= 4095) {
-			$seq = 0;
 			Cache::put('snowflake:seq', 0);
+			$seq = 0;
 		}
 
-		if($ts == null) {
-			$ts = microtime(true);
-		}
-
-		if($ts instanceOf Carbon) {
-			$ts = now()->parse($ts)->timestamp;
-		}
-
-		return ((round($ts * 1000) - 1549756800000) << 22)
+		return ((round(microtime(true) * 1000) - 1549756800000) << 22)
 		| (random_int(1,31) << 17)
 		| (random_int(1,31) << 12)
 		| $seq;

+ 1 - 1
app/Status.php

@@ -4,7 +4,7 @@ namespace App;
 
 use Auth, Cache, Hashids, Storage;
 use Illuminate\Database\Eloquent\Model;
-use Pixelfed\Snowflake\HasSnowflakePrimary;
+use App\HasSnowflakePrimary;
 use App\Http\Controllers\StatusController;
 use Illuminate\Database\Eloquent\SoftDeletes;
 use App\Models\Poll;

+ 1 - 1
app/Story.php

@@ -5,7 +5,7 @@ namespace App;
 use Auth;
 use Storage;
 use Illuminate\Database\Eloquent\Model;
-use Pixelfed\Snowflake\HasSnowflakePrimary;
+use App\HasSnowflakePrimary;
 use App\Util\Lexer\Bearcap;
 
 class Story extends Model