From afa40f66ae41b0a539b1c9d25a1de23c74297440 Mon Sep 17 00:00:00 2001 From: Jessica Smith Date: Tue, 31 Dec 2024 01:20:29 +0000 Subject: [PATCH] First work in progress for trustscore/eigenkarma --- app/Console/Commands/PartyCalculateTrust.php | 38 +++++++++ app/Models/Party.php | 85 +++++++++++++++++++ app/Models/PartyMember.php | 2 + composer.json | 1 + composer.lock | 57 ++++++++++++- .../2024_12_31_005027_add_trustscore.php | 39 +++++++++ 6 files changed, 221 insertions(+), 1 deletion(-) create mode 100644 app/Console/Commands/PartyCalculateTrust.php create mode 100644 database/migrations/2024_12_31_005027_add_trustscore.php diff --git a/app/Console/Commands/PartyCalculateTrust.php b/app/Console/Commands/PartyCalculateTrust.php new file mode 100644 index 0000000..1c84dda --- /dev/null +++ b/app/Console/Commands/PartyCalculateTrust.php @@ -0,0 +1,38 @@ +input->getArgument('party'); + $party = Party::whereCode($code)->first(); + if (!$party) { + $this->error("Unable to find a party with code {$code}"); + return self::FAILURE; + } + $party->calculateTrustScores(); + return self::SUCCESS; + } +} diff --git a/app/Models/Party.php b/app/Models/Party.php index 85dd9cf..7018f70 100644 --- a/app/Models/Party.php +++ b/app/Models/Party.php @@ -13,6 +13,7 @@ use Illuminate\Support\Collection; use Illuminate\Support\Facades\Cache; use Illuminate\Support\Facades\Log; +use NumPHP\LinAlg\LinAlg; /** * @mixin IdeHelperParty @@ -58,6 +59,11 @@ public function user(): BelongsTo return $this->belongsTo(User::class); } + public function trustedUser(): BelongsTo + { + return $this->belongsTo(User::class, 'trusted_user_id'); + } + public function upcoming(): HasMany { return $this->hasMany(UpcomingSong::class); @@ -396,6 +402,23 @@ protected function syncPlaylist(object $playlist) } } } + $remaining = $toAdd = $songsToAdd->count(); + + if ($remaining > 0) { + // Didn't add enough songs, try and find *any* using old method + $ids = $songsToAdd->pluck('id'); + $toAdd = $this->upcoming() + ->whereNull('queued_at') + ->whereNotIn('id', $ids) + ->orderBy('score', 'DESC') + ->orderBy('created_at', 'ASC') + ->orderBy('id', 'ASC') + ->limit($remaining) + ->get(); + foreach ($toAdd as $song) { + $songsToAdd->push($song); + } + } } else { $songsToAdd = $this->upcoming() ->whereNull('queued_at') @@ -680,4 +703,66 @@ public function checkDownvotesForUser(User $user): void throw new VoteException('You have downvoted too many songs'); } } + + public function calculateTrustScores(): void + { + if ($this->trustedUser === null) { + Log::debug("No trusted user, unable to calculate trust score"); + return; + } + + $voteMap = []; + $votes = Vote::whereHas('upcomingSong', function ($query) { + $query->wherePartyId($this->id); + })->where('value', '>', 0)->with(['user', 'upcomingSong', 'upcomingSong.user'])->get(); + + $userIds = $votes->pluck('user_id'); + $userIds->push(0); + $fill = []; + foreach ($userIds as $id) { + $fill[$id] = 0; + } + foreach ($userIds as $id) { + $voteMap[$id] = $fill; + } + foreach ($votes as $vote) { + $songUserId = $vote->upcomingSong->user->id ?? 0; + $voteMap[$vote->user->id][$songUserId] += $vote->value; + } + $userMap = array_keys($voteMap); + $voteMap = array_values($voteMap); + foreach ($voteMap as $i => $row) { + $voteMap[$i] = array_values($row); + $voteMap[$i][$i] = -1; + if ($userMap[$i] === $this->trustedUser->id) { + $voteMap[$i][$i] = 1; + } + } + + $trustedUserMatrix = array_fill(0, count($voteMap), 0); + $trustedUserIndex = array_search($this->trustedUser->id, $userMap); + $trustedUserMatrix[$trustedUserIndex] = 1; + + $solved = LinAlg::solve($voteMap, $trustedUserMatrix); + $scores = $solved->getData(); + + $membersIndexed = []; + $members = $this->members()->with('user')->get(); + foreach ($members as $member) { + $membersIndexed[$member->user->id] = $member; + } + + foreach ($scores as $index => $score) { + $userId = $userMap[$index] ?? null; + if ($userId === null) { + continue; + } + if (!isset($membersIndexed[$userId])) { + continue; + } + $membersIndexed[$userId]->trustscore = $score; + $membersIndexed[$userId]->save(); + } + Log::debug("Finished calculating trust scores"); + } } diff --git a/app/Models/PartyMember.php b/app/Models/PartyMember.php index eb0ae17..7fc7032 100644 --- a/app/Models/PartyMember.php +++ b/app/Models/PartyMember.php @@ -5,6 +5,8 @@ use Illuminate\Database\Eloquent\Factories\HasFactory; use Illuminate\Database\Eloquent\Model; use Illuminate\Database\Eloquent\Relations\BelongsTo; +use NumPHP\Core\NumArray; +use NumPHP\LinAlg\LinAlg; /** * @mixin IdeHelperPartyMember diff --git a/composer.json b/composer.json index b319a78..2b57a6a 100644 --- a/composer.json +++ b/composer.json @@ -15,6 +15,7 @@ "laravel/socialite": "^5.11", "laravel/telescope": "^5.0", "laravel/tinker": "^2.8", + "numphp/numphp": "^1.2", "pusher/pusher-php-server": "^7.2", "ramsey/uuid": "^4.7", "socialiteproviders/discord": "^4.2", diff --git a/composer.lock b/composer.lock index e3dcd80..8b08c6e 100644 --- a/composer.lock +++ b/composer.lock @@ -4,7 +4,7 @@ "Read more about it at https://getcomposer.org/doc/01-basic-usage.md#installing-dependencies", "This file is @generated automatically" ], - "content-hash": "c94cf01f1fa5b4ffc22b99232e0bb567", + "content-hash": "99d3a245427892af8c930c22eb9a4fda", "packages": [ { "name": "brick/math", @@ -3156,6 +3156,61 @@ }, "time": "2024-03-05T20:51:40+00:00" }, + { + "name": "numphp/numphp", + "version": "v1.2.0", + "source": { + "type": "git", + "url": "https://github.com/NumPHP/NumPHP.git", + "reference": "cae62d83debee8b0f1e1c53e060c6688014dc0c8" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/NumPHP/NumPHP/zipball/cae62d83debee8b0f1e1c53e060c6688014dc0c8", + "reference": "cae62d83debee8b0f1e1c53e060c6688014dc0c8", + "shasum": "" + }, + "require": { + "php": ">=5.4" + }, + "require-dev": { + "phpmd/phpmd": "^2.7", + "phpunit/phpunit": "^5.7", + "satooshi/php-coveralls": "^2.1", + "sebastian/phpcpd": "^3.0", + "squizlabs/php_codesniffer": "^3.5" + }, + "type": "library", + "autoload": { + "psr-0": { + "NumPHP\\": "src/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Gordon Lesti", + "email": "info@gordonlesti.com", + "homepage": "http://gordonlesti.com/", + "role": "developer" + } + ], + "description": "Mathematical PHP library for scientific computing", + "homepage": "http://numphp.org/", + "keywords": [ + "linalg", + "matrix", + "numeric" + ], + "support": { + "issues": "https://github.com/NumPHP/NumPHP/issues", + "source": "https://github.com/NumPHP/NumPHP/tree/v1.2.0" + }, + "time": "2019-11-05T17:22:03+00:00" + }, { "name": "nunomaduro/termwind", "version": "v2.0.1", diff --git a/database/migrations/2024_12_31_005027_add_trustscore.php b/database/migrations/2024_12_31_005027_add_trustscore.php new file mode 100644 index 0000000..74a5e02 --- /dev/null +++ b/database/migrations/2024_12_31_005027_add_trustscore.php @@ -0,0 +1,39 @@ +boolean('trustscore')->default(false)->after('weighted'); + $table->unsignedBigInteger('trusted_user_id')->nullable()->default(null)->after('trustscore'); + $table->foreign('trusted_user_id')->references('id')->on('users')->nullOnDelete(); + }); + + Schema::table('party_members', function (Blueprint $table) { + $table->float('trustscore')->default(0)->after('banned'); + }); + } + + /** + * Reverse the migrations. + */ + public function down(): void + { + Schema::table('parties', function (Blueprint $table) { + $table->dropForeign('parties_trusted_user_id_foreign'); + $table->dropColumn(['trustscore', 'trusted_user_id']); + }); + + Schema::table('party_members', function (Blueprint $table) { + $table->dropColumn('trustscore'); + }); + } +};