More refactoring/cleanup
All checks were successful
timw4mail/HummingBirdAnimeClient/develop This commit looks good
All checks were successful
timw4mail/HummingBirdAnimeClient/develop This commit looks good
This commit is contained in:
parent
698025146f
commit
e4b8e6ce51
@ -60,6 +60,7 @@
|
||||
"clean": "vendor/bin/robo clean",
|
||||
"coverage": "phpdbg -qrr -- vendor/bin/phpunit -c build",
|
||||
"phpstan": "phpstan analyse -c phpstan.neon",
|
||||
"psalm": "vendor/bin/psalm",
|
||||
"watch:css": "cd public && npm run watch:css",
|
||||
"watch:js": "cd public && npm run watch:js",
|
||||
"test": "vendor/bin/phpunit -c build --no-coverage",
|
||||
|
@ -1,12 +1,12 @@
|
||||
parameters:
|
||||
checkGenericClassInNonGenericObjectType: false
|
||||
checkMissingIterableValueType: false
|
||||
inferPrivatePropertyTypeFromConstructor: true
|
||||
level: 7
|
||||
autoload_files:
|
||||
- %rootDir%/../../../tests/mocks.php
|
||||
paths:
|
||||
- src
|
||||
- tests
|
||||
- ./console
|
||||
- index.php
|
||||
ignoreErrors:
|
||||
|
36
psalm.xml
36
psalm.xml
@ -15,5 +15,41 @@
|
||||
|
||||
<issueHandlers>
|
||||
<LessSpecificReturnType errorLevel="info" />
|
||||
|
||||
<!-- level 3 issues - slightly lazy code writing, but provably low false-negatives -->
|
||||
|
||||
<DeprecatedMethod errorLevel="info" />
|
||||
<DeprecatedProperty errorLevel="info" />
|
||||
<DeprecatedClass errorLevel="info" />
|
||||
<DeprecatedConstant errorLevel="info" />
|
||||
<DeprecatedFunction errorLevel="info" />
|
||||
<DeprecatedInterface errorLevel="info" />
|
||||
<DeprecatedTrait errorLevel="info" />
|
||||
|
||||
<InternalMethod errorLevel="info" />
|
||||
<InternalProperty errorLevel="info" />
|
||||
<InternalClass errorLevel="info" />
|
||||
|
||||
<MissingClosureReturnType errorLevel="info" />
|
||||
<MissingReturnType errorLevel="info" />
|
||||
<MissingPropertyType errorLevel="info" />
|
||||
<InvalidDocblock errorLevel="info" />
|
||||
<MisplacedRequiredParam errorLevel="info" />
|
||||
|
||||
<PropertyNotSetInConstructor errorLevel="info" />
|
||||
<MissingConstructor errorLevel="info" />
|
||||
<MissingClosureParamType errorLevel="info" />
|
||||
<MissingParamType errorLevel="info" />
|
||||
|
||||
<RedundantCondition errorLevel="info" />
|
||||
|
||||
<DocblockTypeContradiction errorLevel="info" />
|
||||
<RedundantConditionGivenDocblockType errorLevel="info" />
|
||||
|
||||
<UnresolvableInclude errorLevel="info" />
|
||||
|
||||
<RawObjectIteration errorLevel="info" />
|
||||
|
||||
<InvalidStringClass errorLevel="info" />
|
||||
</issueHandlers>
|
||||
</psalm>
|
||||
|
@ -28,8 +28,9 @@ final class CharacterTransformer extends AbstractTransformer {
|
||||
|
||||
/**
|
||||
* @param array $characterData
|
||||
* @return Character
|
||||
*/
|
||||
public function transform(array $characterData): Character
|
||||
public function transform($characterData): Character
|
||||
{
|
||||
$data = JsonAPI::organizeData($characterData);
|
||||
$attributes = $data[0]['attributes'];
|
||||
|
@ -25,7 +25,11 @@ use Aviat\Ion\Transformer\AbstractTransformer;
|
||||
*/
|
||||
final class PersonTransformer extends AbstractTransformer {
|
||||
|
||||
public function transform(array $personData): Person
|
||||
/**
|
||||
* @param array|object $personData
|
||||
* @return Person
|
||||
*/
|
||||
public function transform($personData): Person
|
||||
{
|
||||
$data = JsonAPI::organizeData($personData);
|
||||
$included = JsonAPI::organizeIncludes($personData['included']);
|
||||
|
@ -24,9 +24,12 @@ use Aviat\Ion\Transformer\AbstractTransformer;
|
||||
|
||||
/**
|
||||
* Transform user profile data for display
|
||||
*
|
||||
* @param array|object $profileData
|
||||
* @return User
|
||||
*/
|
||||
final class UserTransformer extends AbstractTransformer {
|
||||
public function transform(array $profileData): User
|
||||
public function transform($profileData): User
|
||||
{
|
||||
$orgData = JsonAPI::organizeData($profileData)[0];
|
||||
$attributes = $orgData['attributes'];
|
||||
|
@ -75,6 +75,11 @@ final class AnimeCollection extends Collection {
|
||||
*/
|
||||
public function getMediaTypeList(): array
|
||||
{
|
||||
if ($this->validDatabase === FALSE)
|
||||
{
|
||||
return [];
|
||||
}
|
||||
|
||||
$output = [];
|
||||
|
||||
$query = $this->db->select('id, type')
|
||||
@ -136,6 +141,11 @@ final class AnimeCollection extends Collection {
|
||||
*/
|
||||
public function add($data): void
|
||||
{
|
||||
if ($this->validDatabase === FALSE)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
$id = $data['id'];
|
||||
|
||||
// Check that the anime doesn't already exist
|
||||
@ -166,13 +176,16 @@ final class AnimeCollection extends Collection {
|
||||
/**
|
||||
* Verify that an item was added
|
||||
*
|
||||
* @param $data
|
||||
* @param array|null|object $data
|
||||
*
|
||||
* @return bool
|
||||
*/
|
||||
public function wasAdded($data): bool
|
||||
{
|
||||
if ($this->validDatabase === FALSE)
|
||||
{
|
||||
return FALSE;
|
||||
}
|
||||
|
||||
$row = $this->get($data['id']);
|
||||
|
||||
return ! empty($row);
|
||||
@ -186,6 +199,11 @@ final class AnimeCollection extends Collection {
|
||||
*/
|
||||
public function update($data): void
|
||||
{
|
||||
if ($this->validDatabase === FALSE)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
// If there's no id to update, don't update
|
||||
if ( ! array_key_exists('hummingbird_id', $data))
|
||||
{
|
||||
@ -206,13 +224,17 @@ final class AnimeCollection extends Collection {
|
||||
/**
|
||||
* Verify that the collection item was updated
|
||||
*
|
||||
* @param $data
|
||||
* @param array|null|object $data
|
||||
*
|
||||
* @return bool
|
||||
*/
|
||||
public function wasUpdated($data): bool
|
||||
{
|
||||
if ($this->validDatabase === FALSE)
|
||||
{
|
||||
return FALSE;
|
||||
}
|
||||
|
||||
$row = $this->get($data['hummingbird_id']);
|
||||
|
||||
foreach ($data as $key => $value)
|
||||
@ -234,6 +256,11 @@ final class AnimeCollection extends Collection {
|
||||
*/
|
||||
public function delete($data): void
|
||||
{
|
||||
if ($this->validDatabase === FALSE)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
// If there's no id to update, don't delete
|
||||
if ( ! array_key_exists('hummingbird_id', $data))
|
||||
{
|
||||
@ -249,9 +276,15 @@ final class AnimeCollection extends Collection {
|
||||
|
||||
/**
|
||||
* @param array|null|object $data
|
||||
* @return bool
|
||||
*/
|
||||
public function wasDeleted($data): bool
|
||||
{
|
||||
if ($this->validDatabase === FALSE)
|
||||
{
|
||||
return FALSE;
|
||||
}
|
||||
|
||||
$animeRow = $this->get($data['hummingbird_id']);
|
||||
|
||||
return empty($animeRow);
|
||||
@ -265,6 +298,11 @@ final class AnimeCollection extends Collection {
|
||||
*/
|
||||
public function get($kitsuId)
|
||||
{
|
||||
if ($this->validDatabase === FALSE)
|
||||
{
|
||||
return FALSE;
|
||||
}
|
||||
|
||||
$query = $this->db->from('anime_set')
|
||||
->where('hummingbird_id', $kitsuId)
|
||||
->get();
|
||||
@ -280,6 +318,11 @@ final class AnimeCollection extends Collection {
|
||||
*/
|
||||
public function getGenreList(array $filter = []): array
|
||||
{
|
||||
if ($this->validDatabase === FALSE)
|
||||
{
|
||||
return [];
|
||||
}
|
||||
|
||||
$output = [];
|
||||
|
||||
// Catch the missing table PDOException
|
||||
@ -337,6 +380,11 @@ final class AnimeCollection extends Collection {
|
||||
*/
|
||||
private function updateGenre($animeId): void
|
||||
{
|
||||
if ($this->validDatabase === FALSE)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
// Get api information
|
||||
$anime = $this->animeModel->getAnimeById($animeId);
|
||||
|
||||
@ -379,6 +427,11 @@ final class AnimeCollection extends Collection {
|
||||
*/
|
||||
private function addNewGenres(array $genres): void
|
||||
{
|
||||
if ($this->validDatabase === FALSE)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
$existingGenres = $this->getExistingGenres();
|
||||
$newGenres = array_diff($genres, $existingGenres);
|
||||
|
||||
@ -416,6 +469,11 @@ final class AnimeCollection extends Collection {
|
||||
|
||||
private function getExistingGenres(): array
|
||||
{
|
||||
if ($this->validDatabase === FALSE)
|
||||
{
|
||||
return [];
|
||||
}
|
||||
|
||||
$genres = [];
|
||||
|
||||
// Get existing genres
|
||||
@ -435,6 +493,11 @@ final class AnimeCollection extends Collection {
|
||||
|
||||
private function getExistingGenreLinkEntries(): array
|
||||
{
|
||||
if ($this->validDatabase === FALSE)
|
||||
{
|
||||
return [];
|
||||
}
|
||||
|
||||
$links = [];
|
||||
|
||||
$query = $this->db->select('hummingbird_id, genre_id')
|
||||
|
@ -19,6 +19,7 @@ namespace Aviat\AnimeClient\Model;
|
||||
use Aviat\Ion\Di\ContainerInterface;
|
||||
use PDOException;
|
||||
|
||||
use Query\Query_Builder_Interface;
|
||||
use function Query;
|
||||
|
||||
/**
|
||||
@ -28,7 +29,7 @@ class Collection extends DB {
|
||||
|
||||
/**
|
||||
* The query builder object
|
||||
* @var \Query\Query_Builder_Interface
|
||||
* @var Query_Builder_Interface
|
||||
*/
|
||||
protected $db;
|
||||
|
||||
@ -52,7 +53,13 @@ class Collection extends DB {
|
||||
$this->db = Query($this->dbConfig);
|
||||
$this->validDatabase = TRUE;
|
||||
}
|
||||
catch (PDOException $e) {}
|
||||
catch (PDOException $e)
|
||||
{
|
||||
$this->db = Query([
|
||||
'type' => 'sqlite',
|
||||
'file' => ':memory:',
|
||||
]);
|
||||
}
|
||||
|
||||
// Is database valid? If not, set a flag so the
|
||||
// app can be run without a valid database
|
||||
|
@ -17,7 +17,6 @@
|
||||
namespace Aviat\AnimeClient\Model;
|
||||
|
||||
use Aviat\Ion\Di\ContainerInterface;
|
||||
use PDO;
|
||||
|
||||
/**
|
||||
* Model for getting anime collection data
|
||||
@ -40,249 +39,5 @@ final class MangaCollection extends Collection {
|
||||
parent::__construct($container);
|
||||
$this->mangaModel = $container->get('manga-model');
|
||||
}
|
||||
|
||||
/**
|
||||
* Get collection from the database, and organize by media type
|
||||
*
|
||||
* @return array
|
||||
*/
|
||||
public function getCollection(): array
|
||||
{
|
||||
$rawCollection = $this->getCollectionFromDatabase();
|
||||
|
||||
$collection = [];
|
||||
|
||||
foreach ($rawCollection as $row)
|
||||
{
|
||||
if (array_key_exists($row['media'], $collection))
|
||||
{
|
||||
$collection[$row['media']][] = $row;
|
||||
}
|
||||
else
|
||||
{
|
||||
$collection[$row['media']] = [$row];
|
||||
}
|
||||
}
|
||||
|
||||
return $collection;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get list of media types
|
||||
*
|
||||
* @return array
|
||||
*/
|
||||
public function getMediaTypeList(): array
|
||||
{
|
||||
$output = [];
|
||||
|
||||
$query = $this->db->select('id, type')
|
||||
->from('media')
|
||||
->get();
|
||||
|
||||
foreach ($query->fetchAll(PDO::FETCH_ASSOC) as $row)
|
||||
{
|
||||
$output[$row['id']] = $row['type'];
|
||||
}
|
||||
|
||||
return $output;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get full collection from the database
|
||||
*
|
||||
* @return array
|
||||
*/
|
||||
private function getCollectionFromDatabase(): array
|
||||
{
|
||||
if ( ! $this->validDatabase)
|
||||
{
|
||||
return [];
|
||||
}
|
||||
|
||||
$query = $this->db->select('hummingbird_id, slug, title, alternate_title, show_type,
|
||||
age_rating, episode_count, episode_length, cover_image, notes, media.type as media')
|
||||
->from('manga_set a')
|
||||
->join('media', 'media.id=a.media_id', 'inner')
|
||||
->order_by('media')
|
||||
->order_by('title')
|
||||
->get();
|
||||
|
||||
return $query->fetchAll(PDO::FETCH_ASSOC);
|
||||
}
|
||||
|
||||
/**
|
||||
* Add an item to the anime collection
|
||||
*
|
||||
* @param array $data
|
||||
* @return void
|
||||
*/
|
||||
public function add($data): void
|
||||
{
|
||||
$anime = (object)$this->mangaModel->getMangaById($data['id']);
|
||||
$this->db->set([
|
||||
'hummingbird_id' => $data['id'],
|
||||
'slug' => $anime->slug,
|
||||
'title' => array_shift($anime->titles),
|
||||
'alternate_title' => implode('<br />', $anime->titles),
|
||||
'show_type' => $anime->show_type,
|
||||
'age_rating' => $anime->age_rating,
|
||||
'cover_image' => $anime->cover_image,
|
||||
'episode_count' => $anime->episode_count,
|
||||
'episode_length' => $anime->episode_length,
|
||||
'media_id' => $data['media_id'],
|
||||
'notes' => $data['notes']
|
||||
])->insert('manga_set');
|
||||
|
||||
$this->updateGenre($data['id']);
|
||||
}
|
||||
|
||||
/**
|
||||
* Update a collection item
|
||||
*
|
||||
* @param array $data
|
||||
* @return void
|
||||
*/
|
||||
public function update($data): void
|
||||
{
|
||||
// If there's no id to update, don't update
|
||||
if ( ! array_key_exists('hummingbird_id', $data))
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
$id = $data['hummingbird_id'];
|
||||
unset($data['hummingbird_id']);
|
||||
|
||||
$this->db->set($data)
|
||||
->where('hummingbird_id', $id)
|
||||
->update('manga_set');
|
||||
}
|
||||
|
||||
/**
|
||||
* Remove a collection item
|
||||
*
|
||||
* @param array $data
|
||||
* @return void
|
||||
*/
|
||||
public function delete($data): void
|
||||
{
|
||||
// If there's no id to update, don't delete
|
||||
if ( ! array_key_exists('hummingbird_id', $data))
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
$this->db->where('hummingbird_id', $data['hummingbird_id'])
|
||||
->delete('genre_manga_set_link');
|
||||
|
||||
$this->db->where('hummingbird_id', $data['hummingbird_id'])
|
||||
->delete('manga_set');
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the details of a collection item
|
||||
*
|
||||
* @param string $kitsuId
|
||||
* @return array
|
||||
*/
|
||||
public function get($kitsuId): array
|
||||
{
|
||||
$query = $this->db->from('manga_set')
|
||||
->where('hummingbird_id', $kitsuId)
|
||||
->get();
|
||||
|
||||
return $query->fetch(PDO::FETCH_ASSOC);
|
||||
}
|
||||
|
||||
/**
|
||||
* Update genre information for selected manga
|
||||
*
|
||||
* @param string $mangaId The current manga
|
||||
* @return void
|
||||
*/
|
||||
private function updateGenre($mangaId): void
|
||||
{
|
||||
$genreInfo = $this->getGenreData();
|
||||
$genres = $genreInfo['genres'];
|
||||
$links = $genreInfo['links'];
|
||||
|
||||
// Get api information
|
||||
$manga = $this->mangaModel->getMangaById($mangaId);
|
||||
|
||||
foreach ($manga['genres'] as $genre)
|
||||
{
|
||||
// Add genres that don't currently exist
|
||||
if ( ! \in_array($genre, $genres, TRUE))
|
||||
{
|
||||
$this->db->set('genre', $genre)
|
||||
->insert('genres');
|
||||
|
||||
$genres[] = $genre;
|
||||
}
|
||||
|
||||
// Update link table
|
||||
// Get id of genre to put in link table
|
||||
$flippedGenres = array_flip($genres);
|
||||
|
||||
$insertArray = [
|
||||
'hummingbird_id' => $mangaId,
|
||||
'genre_id' => $flippedGenres[$genre]
|
||||
];
|
||||
|
||||
if (array_key_exists($mangaId, $links))
|
||||
{
|
||||
if ( ! \in_array($flippedGenres[$genre], $links[$mangaId], TRUE))
|
||||
{
|
||||
$this->db->set($insertArray)->insert('genre_manga_set_link');
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
$this->db->set($insertArray)->insert('genre_manga_set_link');
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Get list of existing genres
|
||||
*
|
||||
* @return array
|
||||
*/
|
||||
private function getGenreData(): array
|
||||
{
|
||||
$genres = [];
|
||||
$links = [];
|
||||
|
||||
// Get existing genres
|
||||
$query = $this->db->select('id, genre')
|
||||
->from('genres')
|
||||
->get();
|
||||
foreach ($query->fetchAll(PDO::FETCH_ASSOC) as $genre)
|
||||
{
|
||||
$genres[$genre['id']] = $genre['genre'];
|
||||
}
|
||||
|
||||
// Get existing link table entries
|
||||
$query = $this->db->select('hummingbird_id, genre_id')
|
||||
->from('genre_manga_set_link')
|
||||
->get();
|
||||
foreach ($query->fetchAll(PDO::FETCH_ASSOC) as $link)
|
||||
{
|
||||
if (array_key_exists($link['hummingbird_id'], $links))
|
||||
{
|
||||
$links[$link['hummingbird_id']][] = $link['genre_id'];
|
||||
}
|
||||
else
|
||||
{
|
||||
$links[$link['hummingbird_id']] = [$link['genre_id']];
|
||||
}
|
||||
}
|
||||
|
||||
return [
|
||||
'genres' => $genres,
|
||||
'links' => $links
|
||||
];
|
||||
}
|
||||
}
|
||||
// End of MangaCollectionModel.php
|
@ -179,7 +179,7 @@ abstract class AbstractType implements ArrayAccess, Countable {
|
||||
/**
|
||||
* Recursively cast properties to an array
|
||||
*
|
||||
* @param null $parent
|
||||
* @param mixed $parent
|
||||
* @return mixed
|
||||
*/
|
||||
public function toArray($parent = null)
|
||||
|
@ -38,12 +38,12 @@ class Anime extends AbstractType {
|
||||
public $cover_image;
|
||||
|
||||
/**
|
||||
* @var string|number
|
||||
* @var string|int
|
||||
*/
|
||||
public $episode_count;
|
||||
|
||||
/**
|
||||
* @var string|number
|
||||
* @var string|int
|
||||
*/
|
||||
public $episode_length;
|
||||
|
||||
|
@ -74,12 +74,12 @@ final class AnimeListItem extends AbstractType {
|
||||
public $rewatching;
|
||||
|
||||
/**
|
||||
* @var number
|
||||
* @var int
|
||||
*/
|
||||
public $rewatched;
|
||||
|
||||
/**
|
||||
* @var number
|
||||
* @var int
|
||||
*/
|
||||
public $user_rating;
|
||||
|
||||
|
@ -41,7 +41,7 @@ final class Character extends AbstractType {
|
||||
public $included;
|
||||
|
||||
/**
|
||||
* @var array
|
||||
* @var Media
|
||||
*/
|
||||
public $media;
|
||||
|
||||
@ -62,9 +62,6 @@ final class Character extends AbstractType {
|
||||
|
||||
public function setMedia ($media): void
|
||||
{
|
||||
$this->media = new class($media) extends AbstractType {
|
||||
public $anime;
|
||||
public $manga;
|
||||
};
|
||||
$this->media = new Media($media);
|
||||
}
|
||||
}
|
29
src/Types/Characters.php
Normal file
29
src/Types/Characters.php
Normal file
@ -0,0 +1,29 @@
|
||||
<?php declare(strict_types=1);
|
||||
/**
|
||||
* Hummingbird Anime List Client
|
||||
*
|
||||
* An API client for Kitsu to manage anime and manga watch lists
|
||||
*
|
||||
* PHP version 7.2
|
||||
*
|
||||
* @package HummingbirdAnimeClient
|
||||
* @author Timothy J. Warren <tim@timshomepage.net>
|
||||
* @copyright 2015 - 2019 Timothy J. Warren
|
||||
* @license http://www.opensource.org/licenses/mit-license.html MIT License
|
||||
* @version 4.2
|
||||
* @link https://git.timshomepage.net/timw4mail/HummingBirdAnimeClient
|
||||
*/
|
||||
|
||||
namespace Aviat\AnimeClient\Types;
|
||||
|
||||
final class Characters extends AbstractType {
|
||||
/**
|
||||
* @var array
|
||||
*/
|
||||
public $main;
|
||||
|
||||
/**
|
||||
* @var array
|
||||
*/
|
||||
public $supporting;
|
||||
}
|
@ -40,7 +40,7 @@ class Database extends AbstractType {
|
||||
public $pass;
|
||||
|
||||
/**
|
||||
* @var string|number
|
||||
* @var string|int
|
||||
*/
|
||||
public $port;
|
||||
|
||||
|
29
src/Types/Media.php
Normal file
29
src/Types/Media.php
Normal file
@ -0,0 +1,29 @@
|
||||
<?php declare(strict_types=1);
|
||||
/**
|
||||
* Hummingbird Anime List Client
|
||||
*
|
||||
* An API client for Kitsu to manage anime and manga watch lists
|
||||
*
|
||||
* PHP version 7.2
|
||||
*
|
||||
* @package HummingbirdAnimeClient
|
||||
* @author Timothy J. Warren <tim@timshomepage.net>
|
||||
* @copyright 2015 - 2019 Timothy J. Warren
|
||||
* @license http://www.opensource.org/licenses/mit-license.html MIT License
|
||||
* @version 4.2
|
||||
* @link https://git.timshomepage.net/timw4mail/HummingBirdAnimeClient
|
||||
*/
|
||||
|
||||
namespace Aviat\AnimeClient\Types;
|
||||
|
||||
final class Media extends AbstractType {
|
||||
/**
|
||||
* @var array
|
||||
*/
|
||||
public $anime = [];
|
||||
|
||||
/**
|
||||
* @var array
|
||||
*/
|
||||
public $manga = [];
|
||||
}
|
@ -31,7 +31,7 @@ final class Person extends AbstractType {
|
||||
public $name;
|
||||
|
||||
/**
|
||||
* @var object
|
||||
* @var Characters
|
||||
*/
|
||||
public $characters;
|
||||
|
||||
@ -42,9 +42,6 @@ final class Person extends AbstractType {
|
||||
|
||||
public function setCharacters($characters): void
|
||||
{
|
||||
$this->characters = new class($characters) extends AbstractType {
|
||||
public $main;
|
||||
public $supporting;
|
||||
};
|
||||
$this->characters = new Characters($characters);
|
||||
}
|
||||
}
|
Loading…
Reference in New Issue
Block a user