Refactor snowflake id generation to improve randomness

This commit is contained in:
Daniel Supernault 2021-09-01 22:46:57 -06:00
parent e95b702e23
commit e5aea490b1
No known key found for this signature in database
GPG Key ID: 0DEF1C662C9033F7
9 changed files with 320 additions and 296 deletions

View File

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

View File

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

View File

@ -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);
}
});
}
}

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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