Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

New group sync endpoints with separate add/remove lists #3538

Open
wants to merge 2 commits into
base: develop
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
123 changes: 123 additions & 0 deletions core/classes/Group_Sync/GroupSyncManager.php
Original file line number Diff line number Diff line change
Expand Up @@ -153,6 +153,10 @@ private function compileValidatorMessages(Language $language): array
* @param string $sending_injector_class Class name of injector broadcasting this change
* @param array $group_ids Array of Group IDs native to the sending injector which were added/removed to the user
*
* @deprecated broadcastGroupChange should be used instead. This function has issues, such as that on the first sync it will replace
* groups on one side with groups from the other side, depending on which side happens to sync first. On the first sync,
* the desired behaviour is for the new roles on both sides to become the union of roles on both sides.
*
* @return array Array of logs of changed groups
*/
public function broadcastChange(User $user, string $sending_injector_class, array $group_ids): array
Expand Down Expand Up @@ -283,6 +287,125 @@ public function broadcastChange(User $user, string $sending_injector_class, arra
return $logs;
}

/**
* Execute respective `addGroup()` or `removeGroup()` function on each of the injectors (e.g. Nameless itself, Minecraft, Discord)
* synced to the changed group.
*
* @param User $user NamelessMC user to apply changes to
* @param string $sending_injector_class Class name of injector broadcasting this change
* @param array $group_ids_add Array of injector-native Group IDs were added to the user
* @param array $group_ids_remove Array of injector-native Group IDs were removed from the user
*
* @return array Array of logs of changed groups
*/
public function broadcastGroupChange(User $user, string $sending_injector_class, array $group_ids_add, array $group_ids_remove): array
{
$sending_injector = $this->getInjectorByClass($sending_injector_class);

if ($sending_injector === null) {
throw new InvalidArgumentException("Can't find injector by class: " . $sending_injector_class);
}

$logs = [];

$modified = [];

$namelessmc_injector = $this->getInjectorByClass(NamelessMCGroupSyncInjector::class);
$namelessmc_column = $namelessmc_injector->getColumnName();

// Get all group sync rules where this injector is not null
$rules = DB::getInstance()->query("SELECT * FROM nl2_group_sync WHERE {$sending_injector->getColumnName()} IS NOT NULL")->results();

$batched_changes = [];
foreach ($rules as $rule) {
foreach ($this->getEnabledInjectors() as $injector) {
if ($injector == $sending_injector) {
continue;
}

$injector_class = get_class($injector);

$batchable = $injector instanceof BatchableGroupSyncInjector;
if ($batchable && !array_key_exists($injector_class, $batched_changes)) {
$batched_changes[$injector_class] = [
'add' => [],
'remove' => [],
];
}

$injector_column = $injector->getColumnName();
$injector_group_id = $rule->{$injector_column};
$sending_group_id = $rule->{$sending_injector->getColumnName()};

// Skip this injector if it doesn't have a group id setup for this rule
if ($injector_group_id === null) {
continue;
}

if (!isset($modified[$injector_column])) {
$modified[$injector_column] = [];
}

// Skip this specific injector for this rule if we have already modified the user
// with the same injector group id
if (in_array($injector_group_id, $modified[$injector_column])) {
continue;
}

if (in_array($sending_group_id, $group_ids_add)) {
// Add group to user
$modified[$injector_column][] = $injector_group_id;
if ($batchable) {
$batched_changes[$injector_class]['add'][] = $injector_group_id;
} elseif ($injector->addGroup($user, $injector_group_id)) {
$logs['added'][] = "{$injector_column} -> {$injector_group_id}";
}
} elseif (in_array($sending_group_id, $group_ids_remove)) {
// Remove group from user
$modified[$injector_column][] = $injector_group_id;
if ($batchable) {
$batched_changes[$injector_class]['remove'][] = $injector_group_id;
} elseif ($injector->removeGroup($user, $injector_group_id)) {
$logs['removed'][] = "{$injector_column} -> {$injector_group_id}";
}
}
}
}

foreach ($batched_changes as $injector_class => $data) {
/** @var GroupSyncInjector&BatchableGroupSyncInjector $injector */
$injector = $this->getInjectorByClass($injector_class);
$injector_column = $injector->getColumnName();

$add = $data['add'];
$remove = $data['remove'];

if (count($add)) {
$result = $injector->batchAddGroups($user, $add);
if (is_array($result)) {
foreach ($result as $res) {
if ($res['status'] === 'added') {
$logs['added'][] = "{$injector_column} -> {$res['group_id']}";
}
}
}
}

if (count($remove)) {
$result = $injector->batchRemoveGroups($user, $remove);
if (is_array($result)) {
foreach ($result as $res) {
if ($res['status'] === 'removed') {
$logs['removed'][] = "{$injector_column} -> {$res['group_id']}";
}
}
}
}
}

return $logs;
}

/**
* Get an enabled `GroupSyncInjector` from its class name, if it exists.
*
Expand Down
3 changes: 3 additions & 0 deletions docker-compose.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,9 @@
# Reinstall:
# docker compose exec php php -f dev/scripts/cli_install.php '--' '--iSwearIKnowWhatImDoing' '--reinstall'
#
# Run phpstan:
# docker compose exec php vendor/bin/phpstan --configuration=dev/phpstan.neon
#
# Uninstall
# docker compose down
# rm -rf core/config.php cache/*
Expand Down
36 changes: 36 additions & 0 deletions modules/Core/includes/endpoints/SyncMinecraftGroupsEndpoint.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,36 @@
<?php

class SyncMinecraftGroupsEndpoint extends KeyAuthEndpoint {

public function __construct() {
$this->_route = 'minecraft/{user}/sync-groups';
$this->_module = 'Core';
$this->_description = 'Update a users groups based on added or removed groups from the Minecraft server';
$this->_method = 'POST';
}

public function execute(Nameless2API $api, User $user): void {
$api->validateParams($_POST, ['server_id']);

$server_id = $_POST['server_id'];
$integration = Integrations::getInstance()->getIntegration('Minecraft');

if (!$integration || $server_id != Settings::get('group_sync_mc_server')) {
$api->returnArray(['message' => $api->getLanguage()->get('api', 'groups_updates_ignored')]);
}

$log = GroupSyncManager::getInstance()->broadcastGroupChange(
$user,
MinecraftGroupSyncInjector::class,
$_POST['add'] ?? [],
$_POST['remove'] ?? [],
);

Log::getInstance()->log(Log::Action('mc_group_sync/role_set'), json_encode($log), $user->data()->id);

$api->returnArray([
'message' => $api->getLanguage()->get('api', 'groups_updates_successfully'),
'log' => $log,
]);
}
}
3 changes: 3 additions & 0 deletions modules/Core/includes/endpoints/UpdateGroupsEndpoint.php
Original file line number Diff line number Diff line change
@@ -1,5 +1,8 @@
<?php

/**
* @deprecated SyncMinecraftGroupsEndpoint should be used instead
*/
Comment on lines +3 to +5
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Sidebar, never considered that API endpoints could be deprecated. Wouldn't be too hard to add something like extends DeprecatedEndpoint and make Nameless log every time a deprecated endpoint is hit for visibility

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It might be nice. I do worry that it will result in too much log spam

class UpdateGroupsEndpoint extends KeyAuthEndpoint {

public function __construct() {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@
/**
* @param int $user The NamelessMC user ID to edit
* @param string $roles An array of Discord Role ID to give to the user
*
* @deprecated Use SyncDiscordRolesEndpoint instead
* @return string JSON Array
*/
class SetDiscordRolesEndpoint extends KeyAuthEndpoint {
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
<?php

class SyncDiscordRolesEndpoint extends KeyAuthEndpoint {

public function __construct() {
$this->_route = 'discord/{user}/sync-roles';
$this->_module = 'Discord Integration';
$this->_description = 'Set a NamelessMC user\'s according to the supplied Discord Role ID list';
$this->_method = 'POST';
}

public function execute(Nameless2API $api, User $user): void {
$api->validateParams($_POST, []);

if (!Discord::isBotSetup()) {
$api->throwError(DiscordApiErrors::ERROR_DISCORD_INTEGRATION_DISABLED);
}

$log_array = GroupSyncManager::getInstance()->broadcastGroupChange(
$user,
DiscordGroupSyncInjector::class,
$_POST['add'] ?? [],
$_POST['remove'] ?? []
);

if (count($log_array)) {
Log::getInstance()->log(Log::Action('discord/role_set'), json_encode($log_array), $user->data()->id);
}

$api->returnArray(array_merge(['message' => Discord::getLanguageTerm('group_updated')], $log_array));
}
}
Loading