|
|
|
/**
|
|
|
|
* Feature for managing Acedia's user groups. Supports both config- and
|
|
|
|
* database-defined information about group sources. An instance of this
|
|
|
|
* feature is necessary for functioning of Acedia's `UserAPI` methods related
|
|
|
|
* to user groups.
|
|
|
|
* Copyright 2022 Anton Tarasenko
|
|
|
|
*------------------------------------------------------------------------------
|
|
|
|
* This file is part of Acedia.
|
|
|
|
*
|
|
|
|
* Acedia 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, version 3 of the License, or
|
|
|
|
* (at your option) any later version.
|
|
|
|
*
|
|
|
|
* Acedia 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 Acedia. If not, see <https://www.gnu.org/licenses/>.
|
|
|
|
*/
|
|
|
|
class Users_Feature extends Feature;
|
|
|
|
|
|
|
|
var private /*config*/ bool useDatabase;
|
|
|
|
var private /*config*/ string databaseLink;
|
|
|
|
var private /*config*/ array<string> availableUserGroups;
|
|
|
|
|
|
|
|
// List of all available user groups for current config
|
|
|
|
var private array<Text> loadedUserGroups;
|
|
|
|
// `HashTable` (with group name keys) that stores `HashTable`s used as
|
|
|
|
// a set data structure (has user id as keys and always `none` as a value).
|
|
|
|
var private HashTable loadedGroupToUsersMap;
|
|
|
|
|
|
|
|
protected function SwapConfig(FeatureConfig config)
|
|
|
|
{
|
|
|
|
local Users newConfig;
|
|
|
|
|
|
|
|
newConfig = Users(config);
|
|
|
|
if (newConfig == none) {
|
|
|
|
return;
|
|
|
|
}
|
|
|
|
useDatabase = newConfig.useDatabase;
|
|
|
|
databaseLink = newConfig.databaseLink;
|
|
|
|
availableUserGroups = newConfig.localUserGroup;
|
|
|
|
LoadLocalData();
|
|
|
|
}
|
|
|
|
|
|
|
|
private final function LoadLocalData()
|
|
|
|
{
|
|
|
|
LoadLocalGroupNames();
|
|
|
|
LoadLocalGroupToUserMap();
|
|
|
|
}
|
|
|
|
|
|
|
|
private final function LoadLocalGroupNames()
|
|
|
|
{
|
|
|
|
local int i, j;
|
|
|
|
local bool isDuplicate;
|
|
|
|
local Text nextUserGroup;
|
|
|
|
|
|
|
|
_.memory.FreeMany(loadedUserGroups);
|
|
|
|
loadedUserGroups.length = 0;
|
|
|
|
for (i = 0; i < availableUserGroups.length; i += 1)
|
|
|
|
{
|
|
|
|
isDuplicate = false;
|
|
|
|
nextUserGroup = _.text.FromString(availableUserGroups[i]);
|
|
|
|
for(j = 0; j < loadedUserGroups.length; j += 1)
|
|
|
|
{
|
|
|
|
if (loadedUserGroups[j].Compare(nextUserGroup, SCASE_INSENSITIVE))
|
|
|
|
{
|
|
|
|
isDuplicate = true;
|
|
|
|
break;
|
|
|
|
}
|
|
|
|
}
|
|
|
|
if (!isDuplicate)
|
|
|
|
{
|
|
|
|
loadedUserGroups[loadedUserGroups.length] =
|
|
|
|
nextUserGroup.LowerCopy();
|
|
|
|
}
|
|
|
|
nextUserGroup.FreeSelf();
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
private final function LoadLocalGroupToUserMap()
|
|
|
|
{
|
|
|
|
local int i, j;
|
|
|
|
local Text newSteamID;
|
|
|
|
local HashTable newPlayerSet;
|
|
|
|
local UserGroup nextGroupConfig;
|
|
|
|
local array<string> nextGroupUserArray;
|
|
|
|
|
|
|
|
_.memory.Free(loadedGroupToUsersMap);
|
|
|
|
loadedGroupToUsersMap = _.collections.EmptyHashTable();
|
|
|
|
class'UserGroup'.static.Initialize();
|
|
|
|
// Go over every group
|
|
|
|
for (i = 0; i < loadedUserGroups.length; i += 1)
|
|
|
|
{
|
|
|
|
nextGroupConfig = UserGroup(
|
|
|
|
class'UserGroup'.static.GetConfigInstance(loadedUserGroups[i]));
|
|
|
|
if (nextGroupConfig == none)
|
|
|
|
{
|
|
|
|
// !!! Log missing group
|
|
|
|
continue;
|
|
|
|
}
|
|
|
|
// Copy player IDs from `string` array into `HashTable`
|
|
|
|
// that is serving as a set data structure
|
|
|
|
newPlayerSet = _.collections.EmptyHashTable();
|
|
|
|
nextGroupUserArray = nextGroupConfig.user;
|
|
|
|
for (j = 0; j < nextGroupUserArray.length; j += 1)
|
|
|
|
{
|
|
|
|
newSteamID = _.text.FromString(nextGroupUserArray[j]);
|
|
|
|
newPlayerSet.SetItem(newSteamID, none);
|
|
|
|
newSteamID.FreeSelf();
|
|
|
|
}
|
|
|
|
loadedGroupToUsersMap.SetItem(loadedUserGroups[i], newPlayerSet);
|
|
|
|
newPlayerSet.FreeSelf();
|
|
|
|
nextGroupConfig.FreeSelf();
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
private final function SaveLocalData()
|
|
|
|
{
|
|
|
|
local Text nextGroup, activeConfigName;
|
|
|
|
local Users currentConfig;
|
|
|
|
local HashTableIterator iter;
|
|
|
|
|
|
|
|
if (useDatabase) return;
|
|
|
|
if (loadedGroupToUsersMap == none) return;
|
|
|
|
|
|
|
|
availableUserGroups.length = 0;
|
|
|
|
iter = HashTableIterator(loadedGroupToUsersMap.Iterate());
|
|
|
|
while (!iter.HasFinished())
|
|
|
|
{
|
|
|
|
nextGroup = Text(iter.GetKey());
|
|
|
|
if (nextGroup != none)
|
|
|
|
{
|
|
|
|
availableUserGroups[availableUserGroups.length] =
|
|
|
|
nextGroup.ToString();
|
|
|
|
nextGroup.FreeSelf();
|
|
|
|
}
|
|
|
|
iter.Next();
|
|
|
|
}
|
|
|
|
iter.FreeSelf();
|
|
|
|
activeConfigName = GetCurrentConfig();
|
|
|
|
if (activeConfigName != none)
|
|
|
|
{
|
|
|
|
currentConfig = Users(class'Users'.static
|
|
|
|
.GetConfigInstance(activeConfigName));
|
|
|
|
}
|
|
|
|
if (currentConfig != none)
|
|
|
|
{
|
|
|
|
currentConfig.localUserGroup = availableUserGroups;
|
|
|
|
// !!! save config !!!
|
|
|
|
}
|
|
|
|
_.memory.Free(currentConfig);
|
|
|
|
_.memory.Free(activeConfigName);
|
|
|
|
}
|
|
|
|
|
|
|
|
/**
|
|
|
|
* Returns names of all groups available for the user with a SteamID given by
|
|
|
|
* `steamID`.
|
|
|
|
*
|
|
|
|
* In case this feature is configured to load user groups from a database
|
|
|
|
* (either local or remote), the returned value is a locally cached one.
|
|
|
|
* This helps us avoid having to query database each time we want to check
|
|
|
|
* something about user groups, but it also means we might have an outdated
|
|
|
|
* information.
|
|
|
|
*
|
|
|
|
* @see `GetGroupsForUserID()` / `GetGroupsForUser()`.
|
|
|
|
*
|
|
|
|
* @param steamID SteamID of the user.
|
|
|
|
* Must be specified in a SteamID64 format, e.g. "76561197960287930".
|
|
|
|
* @return Array with names of the groups of the specified user.
|
|
|
|
* All array elements are guaranteed to be not-`none`, unique and in
|
|
|
|
* lower case.
|
|
|
|
* If passed SteamID is `none` or data wasn't yet loaded - returns empty
|
|
|
|
* array.
|
|
|
|
*/
|
|
|
|
public final /*unreal*/ function array<Text> GetGroupsForSteamID(
|
|
|
|
BaseText steamID)
|
|
|
|
{
|
|
|
|
local Text immutableSteamID;
|
|
|
|
local array<Text> result;
|
|
|
|
local HashTableIterator iter;
|
|
|
|
local Text nextGroup;
|
|
|
|
local HashTable nextGroupUsers;
|
|
|
|
|
|
|
|
if (loadedGroupToUsersMap == none) return result;
|
|
|
|
if (steamID == none) return result;
|
|
|
|
|
|
|
|
immutableSteamID = steamID.LowerCopy();
|
|
|
|
iter = HashTableIterator(loadedGroupToUsersMap.Iterate());
|
|
|
|
while (!iter.HasFinished())
|
|
|
|
{
|
|
|
|
nextGroup = Text(iter.GetKey());
|
|
|
|
nextGroupUsers = HashTable(iter.Get());
|
|
|
|
if ( nextGroup != none && nextGroupUsers != none
|
|
|
|
&& nextGroupUsers.HasKey(steamID))
|
|
|
|
{
|
|
|
|
result[result.length] = nextGroup.Copy();
|
|
|
|
}
|
|
|
|
iter.Next();
|
|
|
|
_.memory.Free(nextGroup);
|
|
|
|
_.memory.Free(nextGroupUsers);
|
|
|
|
}
|
|
|
|
immutableSteamID.FreeSelf();
|
|
|
|
return result;
|
|
|
|
}
|
|
|
|
|
|
|
|
/**
|
|
|
|
* Returns names of all groups available for the user with a SteamID given by
|
|
|
|
* `steamID`.
|
|
|
|
*
|
|
|
|
* In case this feature is configured to load user groups from a database
|
|
|
|
* (either local or remote), the returned value is a locally cached one.
|
|
|
|
* This helps us avoid having to query database each time we want to check
|
|
|
|
* something about user groups, but it also means we might have an outdated
|
|
|
|
* information.
|
|
|
|
*
|
|
|
|
* @see `GetGroupsForUserID()` / `GetGroupsForUser()`.
|
|
|
|
*
|
|
|
|
* @param steamID SteamID of the user.
|
|
|
|
* Must be specified in a SteamID64 format, e.g. "76561197960287930".
|
|
|
|
* @return Array with names of the groups of the specified user.
|
|
|
|
* All array elements are guaranteed to be not-`none`, unique and in
|
|
|
|
* lower case.
|
|
|
|
* If data wasn't yet loaded - returns empty array.
|
|
|
|
*/
|
|
|
|
public final /*unreal*/ function array<Text> GetGroupsForSteamID_S(
|
|
|
|
string steamID)
|
|
|
|
{
|
|
|
|
local array<Text> result;
|
|
|
|
local MutableText wrapper;
|
|
|
|
|
|
|
|
wrapper = _.text.FromStringM(steamID);
|
|
|
|
result = GetGroupsForSteamID(wrapper);
|
|
|
|
wrapper.FreeSelf();
|
|
|
|
return result;
|
|
|
|
}
|
|
|
|
|
|
|
|
/**
|
|
|
|
* Returns names of all groups available for the user with an ID given by `id`.
|
|
|
|
*
|
|
|
|
* In case this feature is configured to load user groups from a database
|
|
|
|
* (either local or remote), the returned value is a locally cached one.
|
|
|
|
* This helps us avoid having to query database each time we want to check
|
|
|
|
* something about user groups, but it also means we might have an outdated
|
|
|
|
* information.
|
|
|
|
*
|
|
|
|
* @see `GetGroupsForSteamID()` / `GetGroupsForUser()`.
|
|
|
|
*
|
|
|
|
* @param id ID of the user.
|
|
|
|
* @return Array with names of the groups of the specified user.
|
|
|
|
* All array elements are guaranteed to be not-`none`, unique and in
|
|
|
|
* lower case.
|
|
|
|
* If data wasn't yet loaded - returns empty array.
|
|
|
|
*/
|
|
|
|
public final function array<Text> GetGroupsForUserID(UserID id)
|
|
|
|
{
|
|
|
|
local Text steamID;
|
|
|
|
local array<Text> result;
|
|
|
|
|
|
|
|
if (id == none) return result;
|
|
|
|
steamID = id.GetSteamID64String();
|
|
|
|
if (steamID == none) return result;
|
|
|
|
|
|
|
|
result = GetGroupsForSteamID(steamID);
|
|
|
|
steamID.FreeSelf();
|
|
|
|
return result;
|
|
|
|
}
|
|
|
|
|
|
|
|
/**
|
|
|
|
* Returns names of all groups available for the user given by `user`.
|
|
|
|
*
|
|
|
|
* In case this feature is configured to load user groups from a database
|
|
|
|
* (either local or remote), the returned value is a locally cached one.
|
|
|
|
* This helps us avoid having to query database each time we want to check
|
|
|
|
* something about user groups, but it also means we might have an outdated
|
|
|
|
* information.
|
|
|
|
*
|
|
|
|
* @see `GetGroupsForSteamID()` / `GetGroupsForUserID()`.
|
|
|
|
*
|
|
|
|
* @param user Reference to `User` object that represent the user we are to
|
|
|
|
* find groups for.
|
|
|
|
* @return Array with names of the groups of the specified user.
|
|
|
|
* All array elements are guaranteed to be not-`none`, unique and in
|
|
|
|
* lower case.
|
|
|
|
* If data wasn't yet loaded - returns empty array.
|
|
|
|
*/
|
|
|
|
public final function array<Text> GetGroupsForUser(User user)
|
|
|
|
{
|
|
|
|
local UserID id;
|
|
|
|
local array<Text> result;
|
|
|
|
|
|
|
|
if (user == none) {
|
|
|
|
return result;
|
|
|
|
}
|
|
|
|
id = user.GetID();
|
|
|
|
result = GetGroupsForUserID(id);
|
|
|
|
_.memory.Free(id);
|
|
|
|
return result;
|
|
|
|
}
|
|
|
|
|
|
|
|
/**
|
|
|
|
* Returns `UserID`s of all users that belong into the group named `groupName`.
|
|
|
|
*
|
|
|
|
* In case this feature is configured to load user groups from a database
|
|
|
|
* (either local or remote), the returned value is a locally cached one.
|
|
|
|
* This helps us avoid having to query database each time we want to check
|
|
|
|
* something about user groups, but it also means we might have an outdated
|
|
|
|
* information.
|
|
|
|
*
|
|
|
|
* @param groupName Name of the group. Case-insensitive.
|
|
|
|
* @return Array with `UserID`s for every user in the user group named
|
|
|
|
* `groupName`. All array elements are guaranteed to be not-`none` and
|
|
|
|
* correspond to unique players.
|
|
|
|
* If data wasn't yet loaded - returns empty array.
|
|
|
|
*/
|
|
|
|
public final function array<UserID> GetGroupMembers(Text groupName)
|
|
|
|
{
|
|
|
|
local int i;
|
|
|
|
local Text lowerCaseGroupName;
|
|
|
|
local HashTable groupUsers;
|
|
|
|
local array<Text> groupUsersNames;
|
|
|
|
local UserID nextUserID;
|
|
|
|
local array<UserID> result;
|
|
|
|
|
|
|
|
if (loadedGroupToUsersMap == none) return result;
|
|
|
|
if (groupName == none) return result;
|
|
|
|
|
|
|
|
lowerCaseGroupName = groupName.LowerCopy();
|
|
|
|
groupUsers = loadedGroupToUsersMap.GetHashTable(lowerCaseGroupName);
|
|
|
|
lowerCaseGroupName.FreeSelf();
|
|
|
|
if (groupUsers == none) {
|
|
|
|
groupUsersNames = groupUsers.GetTextKeys();
|
|
|
|
}
|
|
|
|
_.memory.Free(groupUsers);
|
|
|
|
for (i = 0; i < groupUsersNames.length; i += 1)
|
|
|
|
{
|
|
|
|
nextUserID = UserID(_.memory.Allocate(class'UserID'));
|
|
|
|
nextUserID.Initialize(groupUsersNames[i]);
|
|
|
|
if (nextUserID.IsInitialized()) {
|
|
|
|
result[result.length] = nextUserID;
|
|
|
|
}
|
|
|
|
else {
|
|
|
|
nextUserID.FreeSelf();
|
|
|
|
}
|
|
|
|
}
|
|
|
|
_.memory.FreeMany(groupUsersNames);
|
|
|
|
return result;
|
|
|
|
}
|
|
|
|
|
|
|
|
/**
|
|
|
|
* Checks whether user with an ID given by `id` belongs to the group named
|
|
|
|
* `groupName`.
|
|
|
|
*
|
|
|
|
* In case this feature is configured to load user groups from a database
|
|
|
|
* (either local or remote), the returned value is a locally cached one.
|
|
|
|
* This helps us avoid having to query database each time we want to check
|
|
|
|
* something about user groups, but it also means we might have an outdated
|
|
|
|
* information.
|
|
|
|
*
|
|
|
|
* @param id ID of the user.
|
|
|
|
* @param groupName Name of the group. Case-insensitive.
|
|
|
|
* @return `true` if user with an ID given by `id` belongs to the group named
|
|
|
|
* `groupName` and false if: it does not, either of the parameters is
|
|
|
|
* invalid or group data wasn't yet properly loaded.
|
|
|
|
*/
|
|
|
|
public final function bool IsUserIDInGroup(UserID id, Text groupName)
|
|
|
|
{
|
|
|
|
local bool result;
|
|
|
|
local Text steamID;
|
|
|
|
local Text lowerGroupName;
|
|
|
|
local HashTable nextGroupUsers;
|
|
|
|
|
|
|
|
if (loadedGroupToUsersMap == none) return false;
|
|
|
|
if (groupName == none) return false;
|
|
|
|
if (id == none) return false;
|
|
|
|
steamID = id.GetSteamID64String();
|
|
|
|
if (steamID == none) return false;
|
|
|
|
|
|
|
|
lowerGroupName = groupName.LowerCopy();
|
|
|
|
nextGroupUsers = loadedGroupToUsersMap.GetHashTable(lowerGroupName);
|
|
|
|
lowerGroupName.FreeSelf();
|
|
|
|
if (nextGroupUsers != none) {
|
|
|
|
result = nextGroupUsers.HasKey(steamID);
|
|
|
|
}
|
|
|
|
_.memory.Free(nextGroupUsers);
|
|
|
|
steamID.FreeSelf();
|
|
|
|
return result;
|
|
|
|
}
|
|
|
|
|
|
|
|
/**
|
|
|
|
* Checks whether user given by `user` belongs to the group named `groupName`.
|
|
|
|
*
|
|
|
|
* In case this feature is configured to load user groups from a database
|
|
|
|
* (either local or remote), the returned value is a locally cached one.
|
|
|
|
* This helps us avoid having to query database each time we want to check
|
|
|
|
* something about user groups, but it also means we might have an outdated
|
|
|
|
* information.
|
|
|
|
*
|
|
|
|
* @param user Reference to `User` object that represent the user we are to
|
|
|
|
* check for belonging to the group.
|
|
|
|
* @param groupName Name of the group. Case-insensitive.
|
|
|
|
* @return `true` if user with an ID given by `id` belongs to the group named
|
|
|
|
* `groupName` and false if: it does not, either of the parameters is
|
|
|
|
* invalid or group data wasn't yet properly loaded.
|
|
|
|
*/
|
|
|
|
public final function bool IsUserInGroup(User user, Text groupName)
|
|
|
|
{
|
|
|
|
local UserID id;
|
|
|
|
local bool result;
|
|
|
|
|
|
|
|
if (user == none) {
|
|
|
|
return false;
|
|
|
|
}
|
|
|
|
id = user.GetID();
|
|
|
|
result = IsUserIDInGroup(id, groupName);
|
|
|
|
_.memory.Free(id);
|
|
|
|
return result;
|
|
|
|
}
|
|
|
|
|
|
|
|
/**
|
|
|
|
* Checks whether user groups' data was already loaded from the source
|
|
|
|
* (either config file or local/remote database).
|
|
|
|
*
|
|
|
|
* Data loaded once is cached and this method returning `true` does not
|
|
|
|
* guarantee that is isn't outdated. Additional, asynchronous queries must be
|
|
|
|
* made to check for that.
|
|
|
|
*
|
|
|
|
* @return `true` if user groups' data was loaded and `false` otherwise.
|
|
|
|
*/
|
|
|
|
public final function bool IsUserGroupDataLoaded()
|
|
|
|
{
|
|
|
|
return true;
|
|
|
|
}
|
|
|
|
|
|
|
|
defaultproperties
|
|
|
|
{
|
|
|
|
configClass = class'Users'
|
|
|
|
}
|