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

feat: DiscordRoleFilter #68

Open
wants to merge 1 commit into
base: 5.0.x
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
2 changes: 2 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,5 +1,7 @@
# Changelog

- Added DiscordRoleFilter feature and settings

## 5.0.0
- Replace the API Throttler by a Guzzle Middleware which is more efficient
- Make driver compatible with connector 2.0.x
Expand Down
20 changes: 20 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -96,6 +96,26 @@ Simply confirm using the `Authorize` button which will redirect you to the SeAT

You'll be invited automatically to the Discord Server and attached channels.

## Settings

The SeAT global_settings table has a key 'seat-discord-connector' used by this module. This contains serialized settings use by this module.

| Field | Description | Default Value |
|------------------|-------------------------------------------------------------------------------------------------------------------|----------------------------------------|
| can_add_roles | List of discord role names or role ids seperated by colon, that this connector is allowed to add. | |
| | The special role name of "@@everyrole" acts as wildcard, while blank "" means none. | |
| | A role needs to be visible through visibleRoles to be affected by this setting. | @@everyrole:RoleName Example2 RemoveMe |
| can_remove_roles | List of discord role names or role ids seperated by colon, that this connector is allowed to remove. | |
| | The special role name of "@@everyrole" acts as wildcard, while blank "" means none. | |
| | A role needs to be visible through visibleRoles to be affected by this setting. | @@everyrole:RoleName Example2 RemoveMe |
| visible_roles | List of discord role names or role ids seperated by colon, that this connector has visibiliy of from | |
| | the discord server. The special role name of "@@everyrole" acts as wildcard, while blank "" means none. | |
| | For most usage of this feature configuring visibleRoles to a non-default value will prevent all changes to roles. | @@everyrole:RoleName Example2 RemoveMe |

By default this connector expects to fully manage all roles at the Discord server.
Through careful manipulation of these values you can define the scope of roles that can be manipulated by this connector.
The use of "RoleName Example2 RemoveMe" in the default setting has been chosen to allow the administrators best guess at understanding how the field should be configured for multiple roles from looking at the setting without reading this documentation.

## Upgrade

If you're coming from a version prior to 4.x, you may want to convert your data into the new connector structure.
Expand Down
15 changes: 15 additions & 0 deletions src/Config/seat-connector.config.php
Original file line number Diff line number Diff line change
Expand Up @@ -44,5 +44,20 @@
'label' => 'seat-connector-discord::seat.use_email_scope',
'type' => 'checkbox',
],
[
'name' => 'visible_roles',
'label' => 'seat-connector-discord::seat.visible_roles',
'type' => 'text',
],
[
'name' => 'can_add_roles',
'label' => 'seat-connector-discord::seat.can_add_roles',
'type' => 'text',
],
[
'name' => 'can_remove_roles',
'label' => 'seat-connector-discord::seat.can_remove_roles',
'type' => 'text',
],
],
];
57 changes: 54 additions & 3 deletions src/Driver/DiscordClient.php
Original file line number Diff line number Diff line change
Expand Up @@ -78,6 +78,21 @@ class DiscordClient implements IClient
*/
private $owner_id;

/**
* @var Warlof\Seat\Connector\Drivers\Discord\Driver\DiscordRoleFilter
*/
private $visible_roles;

/**
* @var Warlof\Seat\Connector\Drivers\Discord\Driver\DiscordRoleFilter
*/
private $can_add_roles;

/**
* @var Warlof\Seat\Connector\Drivers\Discord\Driver\DiscordRoleFilter
*/
private $can_remove_roles;

/**
* DiscordClient constructor.
*
Expand All @@ -88,6 +103,9 @@ private function __construct(array $parameters)
$this->guild_id = $parameters['guild_id'];
$this->bot_token = $parameters['bot_token'];
$this->owner_id = $parameters['owner_id'];
$this->visible_roles = new DiscordRoleFilter($parameters['visible_roles']);
$this->can_add_roles = new DiscordRoleFilter($parameters['can_add_roles']);
$this->can_remove_roles = new DiscordRoleFilter($parameters['can_remove_roles']);

$this->members = collect();
$this->roles = collect();
Expand Down Expand Up @@ -120,9 +138,12 @@ public static function getInstance(): IClient
throw new DriverSettingsException('Parameter bot_token is missing.');

self::$instance = new DiscordClient([
'guild_id' => $settings->guild_id,
'bot_token' => $settings->bot_token,
'owner_id' => property_exists($settings, 'owner_id') ? $settings->owner_id : null,
'guild_id' => $settings->guild_id,
'bot_token' => $settings->bot_token,
'owner_id' => property_exists($settings, 'owner_id') ? $settings->owner_id : null,
'visible_roles' => property_exists($settings, 'visible_roles') ? $settings->visible_roles : DiscordRoleFilter::DEFAULT_VISIBLE_ROLES,
'can_add_roles' => property_exists($settings, 'can_add_roles') ? $settings->can_add_roles : DiscordRoleFilter::DEFAULT_CAN_ADD_ROLES,
'can_remove_roles' => property_exists($settings, 'can_remove_roles') ? $settings->can_remove_roles : DiscordRoleFilter::DEFAULT_CAN_REMOVE_ROLES,
]);
}

Expand Down Expand Up @@ -354,8 +375,38 @@ private function seedRoles()
// ignore managed roles (ie: booster)
if ($role_attributes['managed']) continue;

if (! $this->checkVisibleRoles($role_attributes['name'], $role_attributes['id'])) continue;

$role = new DiscordRole($role_attributes);
$this->roles->put($role->getId(), $role);
}
}

/**
* @params any $args
* @return bool
*/
public function checkVisibleRoles(...$args) : bool
{
return $this->visible_roles->check(...$args);
}

/**
* @params any $args
* @return bool
*/
public function checkCanAddRoles(...$args) : bool
{
return $this->can_add_roles->check(...$args);
}

/**
* @params any $args
* @return bool
*/
public function checkCanRemoveRoles(...$args) : bool
{
return $this->can_remove_roles->check(...$args);
}

}
8 changes: 8 additions & 0 deletions src/Driver/DiscordMember.php
Original file line number Diff line number Diff line change
Expand Up @@ -145,6 +145,8 @@ public function getSets(): array

if (is_null($set)) continue;

if (! DiscordClient::getInstance()->checkVisibleRoles($set->getName(), $set->getId())) continue;

$this->roles->put($role_id, $set);
}
}
Expand Down Expand Up @@ -179,6 +181,9 @@ public function addSet(ISet $group)
if (in_array($group->getId(), $this->role_ids) || $this->isOwner())
return;

if (! DiscordClient::getInstance()->checkCanAddRoles($group->getName(), $group->getId()))
return;

DiscordClient::getInstance()->sendCall('PUT', '/guilds/{guild.id}/members/{user.id}/roles/{role.id}', [
'guild.id' => DiscordClient::getInstance()->getGuildId(),
'role.id' => $group->getId(),
Expand All @@ -205,6 +210,9 @@ public function removeSet(ISet $group)
if (! in_array($group->getId(), $this->role_ids) || $this->isOwner())
return;

if (! DiscordClient::getInstance()->checkCanRemoveRoles($group->getName(), $group->getId()))
return;

DiscordClient::getInstance()->sendCall('DELETE', '/guilds/{guild.id}/members/{user.id}/roles/{role.id}', [
'guild.id' => DiscordClient::getInstance()->getGuildId(),
'role.id' => $group->getId(),
Expand Down
6 changes: 6 additions & 0 deletions src/Driver/DiscordRole.php
Original file line number Diff line number Diff line change
Expand Up @@ -101,6 +101,9 @@ public function addMember(IUser $user)
if (in_array($user, $this->getMembers()))
return;

if (! DiscordClient::getInstance()->checkCanAddRoles($this->name, $this->id))
return;

try {
DiscordClient::getInstance()->sendCall('PUT', '/guilds/{guild.id}/members/{user.id}/roles/{role.id}', [
'guild.id' => DiscordClient::getInstance()->getGuildId(),
Expand All @@ -126,6 +129,9 @@ public function removeMember(IUser $user)
if (! in_array($user, $this->getMembers()))
return;

if (! DiscordClient::getInstance()->checkCanRemoveRoles($this->name, $this->id))
return;

try {
DiscordClient::getInstance()->sendCall('DELETE', '/guilds/{guild.id}/members/{user.id}/roles/{role.id}', [
'guild.id' => DiscordClient::getInstance()->getGuildId(),
Expand Down
156 changes: 156 additions & 0 deletions src/Driver/DiscordRoleFilter.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,156 @@
<?php

/**
* This file is part of SeAT Discord Connector.
*
* Copyright (C) 2021 Troyburn <[email protected]>
*
* SeAT Discord Connector is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* any later version.
*
* SeAT Discord Connector is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/


namespace Warlof\Seat\Connector\Drivers\Discord\Driver;

class DiscordRoleFilter
{
/**
*
* @var array of string
*/
private $filter_specs;

/**
*
* @var bool
*/
private $everyrole;

/**
*
* @var string EVERYROLE_ENTRY
*/
public const EVERYROLE_ENTRY = '@@everyrole';

/**
* Arbitrary string "RoleName Example2 RemoveMe" exist to self document a valid setting in database.
*
* @var string DEFAULT_CAN_REMOVE_ROLES
*/
public const DEFAULT_CAN_REMOVE_ROLES = '@@everyrole:RoleName Example2 RemoveMe';
Copy link
Owner

Choose a reason for hiding this comment

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

instead providing hardcoded roles list as value, wouldn't it be better to put this as field tooltip into connector settings ?

Copy link
Author

@troyburn troyburn Jan 30, 2022

Choose a reason for hiding this comment

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

IIRC the design goal is not to break peoples systems during an upgrade.

An empty value does not make the overall module (seat-discord-connector) work as most users might expect, the empty value will disable functionality.

Not sure tooltip information alone helps. I'm sure the tooltip information can be improved, I didn't spend much time on it (since I know what it does lol).

This is about a fresh install or an upgrade to this version and then opening the discord settings and ensuring the new fields are populated with defaults that continue to maintain old behavior where possible. In the case of this new feature that should be possible, hence this value is in code as well as documentation (README).

If you only put information in documentation and tooltips many people won't read/notice during upgrade and the first time they save the settings after upgrade (which could be months) their connector will stop working like it did before pressing Discord Settings -> Save. Because it will save an empty string value by default, which will change module behaviour.

How many support issues might this cause, due to not reading documentation around the uprade ?

The @@everyrole is an illegal role name on Discord, so was picked for its special meaning.
That role names in discord can contain spaces.
That the use of * it also a valid role name in discord.
Discord has a @everyone with special meaning, so it wasn't a far stretch to use @@everyrole

I'm trying to convey to someone not reading the seat-discord-connector documentation and making a guess how to configure seat-discord-connector with 2 or more roles and when roles contain spaces all at the same time. i.e. space is not the role name delimiter, space is allowed and considered part of the role name. So ideally the SeAT administrators best guess on what to do will be correct first time.


/**
* Arbitrary string "RoleName Example2 RemoveMe" exist to self document a valid setting in database.
*
* @var string DEFAULT_CAN_ADD_ROLES
*/
public const DEFAULT_CAN_ADD_ROLES = '@@everyrole:RoleName Example2 RemoveMe';

/**
* Arbitrary string "RoleName Example2 RemoveMe" exist to self document a valid setting in database.
*
* @var string DEFAULT_VISIBLE_ROLES
*/
public const DEFAULT_VISIBLE_ROLES = '@@everyrole:RoleName Example2 RemoveMe';

/**
* DiscordRoleFilter constructor.
*
* @param string $spec
* @param string $explodeOn
*/
public function __construct(string $spec = '', string $explodeOn = ':')
{
$this->filter_specs = explode($explodeOn, $spec);
Copy link
Owner

Choose a reason for hiding this comment

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

maybe collection could be useful here ?
https://laravel.com/docs/6.x/collections

Copy link
Author

Choose a reason for hiding this comment

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

does the configuration system allow storage and default conversion to collections ?
so the primary usage and input data types/format is directly from the string types in the settings dialogs
or does PHP now allow overloading of the constructor so an additional ctor can added

Copy link
Owner

Choose a reason for hiding this comment

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

collection will allow you to manipulate array of things, including way to exclude data from final result.
ie: https://laravel.com/docs/6.x/collections#method-filter

while (($key = array_search('', $this->filter_specs)) !== FALSE) {
unset($this->filter_specs[$key]); /* remove empty string items */
}
$this->everyrole = in_array(self::EVERYROLE_ENTRY, $this->filter_specs);
}

/**
* Check a single role against the filter spec.
*
* @param string|null $s
* @return mixed
*/
public function checkOne(string $s = null, $defaultValue = false)
{
if (empty($s)) {
return $defaultValue;
}

if ($this->everyrole === true) {
return true;
}

/* theoretically it should be possible to modify this matching
* element to return true, false or $defaultValue (with an implied
* non-bool type) to implement ordered precedence rules and
* question mark prefixed inversion of a spec written like:
* "Role.*:!RoleABC:RoleXYZ"
*/
return in_array($s, $this->filter_specs) ? true : $defaultValue;
}

/**
* Check all the roles in an array of role against the filter spec.
*
* @param array $ary
* @param mixed $defaultValue Expected to be bool or null.
* @return mixed or type of $defaultValue
*/
public function checkAll(array $ary = null, $defaultValue = false)
{
if (is_null($ary) || empty($ary)) {
return $defaultValue;
}

if ($this->everyrole === true) { // superflous?
return true;
}

foreach ($ary as $item) {
$res = $this->checkOne($item, null);
if (! is_null($res)) {
return $res;
}
}

return $defaultValue;
}

/**
* Check the arguments given as role against the filter spec.
* Returns true|false accordingly.
* @param $args
* @return bool
*/
public function check(...$args) : bool
{
// $args = func_get_args();
foreach ($args as $val) {
if(is_array($val))
$res = $this->checkAll($val, null);
else
$res = $this->checkOne($val, null);
if (! is_null($res)) {
return $res;
}
}
return false;
}

}

?>
Copy link
Owner

Choose a reason for hiding this comment

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

php closing tag is non longer expected

Copy link
Author

@troyburn troyburn Jan 30, 2022

Choose a reason for hiding this comment

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

habbit, PHP is my 15th language

22 changes: 14 additions & 8 deletions src/Http/Controllers/SettingsController.php
Original file line number Diff line number Diff line change
Expand Up @@ -55,17 +55,23 @@ class SettingsController extends Controller
public function store(Request $request)
{
$request->validate([
'client_id' => 'required|string',
'client_secret' => 'required|string',
'bot_token' => 'required|string',
'use_email_scope' => 'boolean',
'client_id' => 'required|string',
'client_secret' => 'required|string',
'bot_token' => 'required|string',
'use_email_scope' => 'boolean',
'visible_roles' => 'string',
'can_add_roles' => 'string',
'can_remove_roles' => 'string',
]);

$settings = (object) [
'client_id' => $request->input('client_id'),
'client_secret' => $request->input('client_secret'),
'bot_token' => $request->input('bot_token'),
'use_email_scope' => $request->input('use_email_scope', 0),
'client_id' => $request->input('client_id'),
'client_secret' => $request->input('client_secret'),
'bot_token' => $request->input('bot_token'),
'use_email_scope' => $request->input('use_email_scope', 0),
'visible_roles' => $request->input('visible_roles'),
'can_add_roles' => $request->input('can_add_roles'),
'can_remove_roles' => $request->input('can_remove_roles'),
];

setting(['seat-connector.drivers.discord', $settings], true);
Expand Down
11 changes: 7 additions & 4 deletions src/lang/en/seat.php
Original file line number Diff line number Diff line change
@@ -1,8 +1,11 @@
<?php

return [
'client_id' => 'Client ID',
'client_secret' => 'Client Secret',
'bot_token' => 'Bot Token',
'use_email_scope' => 'Use email as Unique ID? Email scope will be requested from users.',
'client_id' => 'Client ID',
'client_secret' => 'Client Secret',
'bot_token' => 'Bot Token',
'use_email_scope' => 'Use email as Unique ID? Email scope will be requested from users.',
'visible_roles' => 'Manage visibleRoles for Discord Role filter',
'can_add_roles' => 'Manage canAddRoles for Discord Role filter',
'can_remove_roles' => 'Manage canRemoveRoles for Discord Role filter',
];
Loading