├── tests
├── .gitignore
├── bootstrap.php
├── Trait
│ └── GuzzleCachedClientTraitTest.php
└── Doctrine
│ └── Builder
│ ├── SkillValueBuilder.php
│ ├── ActivityBuilder.php
│ └── QuestBuilder.php
├── migrations
├── .gitignore
├── Version20231020183902.php
├── Version20240130182934.php
├── Version20250212235809.php
├── Version20230318074226.php
├── Version20250209124013.php
├── Version20250208113115.php
└── Version20230304174431.php
├── src
├── Entity
│ ├── .gitignore
│ ├── KnownPlayer.php
│ └── Player.php
├── Controller
│ ├── .gitignore
│ ├── NewsFeedController.php
│ ├── DashboardController.php
│ ├── WelcomeController.php
│ ├── GrandExchangeController.php
│ ├── AbstractBaseController.php
│ ├── QuestsController.php
│ ├── ActivityController.php
│ └── XpTrackerController.php
├── Repository
│ ├── .gitignore
│ └── KnownPlayerRepository.php
├── Dto
│ ├── JsonbDTOInterface.php
│ ├── SkillValue.php
│ ├── Quest.php
│ ├── QuestResponse.php
│ └── Activity.php
├── Message
│ ├── AsyncEventInterface.php
│ ├── Clan
│ │ └── UpdateAllPlayerClanNamesMessage.php
│ └── Stats
│ │ ├── UpdateSingularPlayerStatsMessage.php
│ │ └── UpdateAllPlayerStatsMessage.php
├── Exception
│ ├── PlayerApi
│ │ ├── PlayerNotFoundException.php
│ │ ├── PlayerNotAMemberException.php
│ │ ├── PlayerApiHttpException.php
│ │ └── PlayerApiDataConversionException.php
│ ├── RateLimitedException.php
│ └── Jsonb
│ │ ├── JsonbConvertToPHPValueException.php
│ │ └── JsonbConvertToDatabaseValueException.php
├── Enum
│ ├── UpdateAllPlayersType.php
│ ├── QuestStatus.php
│ ├── QuestDifficulty.php
│ ├── XpChartTimeUnit.php
│ ├── KnownPlayers.php
│ ├── ActivityFilter.php
│ ├── CatalogueCategory.php
│ ├── SkillEnum.php
│ ├── SkillLevelXpEnum.php
│ └── EliteSkillLevelXpEnum.php
├── Kernel.php
├── ValueObject
│ └── GrandExchange
│ │ ├── CataloguePriceDetails.php
│ │ ├── CatalogueResponseCategory.php
│ │ ├── CatalogueItem.php
│ │ ├── CatalogueResponse.php
│ │ └── CatalogueResponseCollection.php
├── Doctrine
│ └── Type
│ │ ├── QuestType.php
│ │ ├── SkillValueType.php
│ │ ├── ActivityType.php
│ │ └── CustomJsonbType.php
├── Scheduler
│ └── CronScheduler.php
├── MessageHandler
│ ├── Clan
│ │ └── UpdateAllPlayerClanNamesHandler.php
│ └── Stats
│ │ ├── UpdateAllPlayerStatsHandler.php
│ │ └── UpdateSingularPlayerStatsHandler.php
├── Command
│ ├── RsApiUpdate
│ │ ├── UpdateClanCommand.php
│ │ ├── Singular.php
│ │ ├── All.php
│ │ └── HandleSingularPlayerTrait.php
│ └── VerifyPlayerDataIntegrityCommand.php
├── Service
│ ├── Chart
│ │ ├── QuestChartService.php
│ │ └── Xp
│ │ │ ├── DailyChartService.php
│ │ │ └── HourlyChartService.php
│ ├── GuzzleCachedClient.php
│ ├── DoubleXpService.php
│ └── GrandExchangeApiService.php
├── EventListener
│ └── RequestListener.php
└── Trait
│ ├── SerializerAwareTrait.php
│ ├── GuzzleCachedClientTrait.php
│ └── CreateXpChartTrait.php
├── translations
└── .gitignore
├── assets
├── controllers
│ ├── .gitignore
│ └── activities_controller.js
├── app.js
├── controllers.json
└── bootstrap.js
├── config
├── routes.yaml
├── routes
│ ├── framework.yaml
│ └── web_profiler.yaml
├── packages
│ ├── twig.yaml
│ ├── translation.yaml
│ ├── doctrine_migrations.yaml
│ ├── csrf.yaml
│ ├── stof_doctrine_extensions.yaml
│ ├── routing.yaml
│ ├── web_profiler.yaml
│ ├── messenger.yaml
│ ├── datatables.yaml
│ ├── cache.yaml
│ ├── framework.yaml
│ ├── monolog.yaml
│ ├── webpack_encore.yaml
│ └── doctrine.yaml
├── preload.php
├── services.yaml
└── bundles.php
├── phpstan.neon
├── docker-compose.override.yml
├── .env.test
├── public
└── index.php
├── templates
├── includes
│ ├── alerts.html.twig
│ ├── badges
│ │ ├── github.html.twig
│ │ └── discord.html.twig
│ ├── header.html.twig
│ └── sidemenu.html.twig
├── quests.html.twig
├── base.html.twig
├── xp_tracker.html.twig
├── levels_progress.html.twig
├── newsfeed.html.twig
├── grand_exchange
│ └── index.html.twig
├── activities.html.twig
└── welcome.html.twig
├── docker-compose.yml
├── bin
├── console
└── phpunit
├── phpcs.xml.dist
├── .github
└── workflows
│ └── lintAndTests.yml
├── .gitignore
├── LICENSE
├── package.json
├── phpunit.xml.dist
├── .env
├── webpack.config.js
├── composer.json
└── README.md
/tests/.gitignore:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/migrations/.gitignore:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/src/Entity/.gitignore:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/translations/.gitignore:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/src/Controller/.gitignore:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/src/Repository/.gitignore:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/assets/controllers/.gitignore:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/src/Dto/JsonbDTOInterface.php:
--------------------------------------------------------------------------------
1 | doctrine/doctrine-bundle ###
5 | database:
6 | ports:
7 | - "5432:5432"
8 | ###< doctrine/doctrine-bundle ###
9 |
--------------------------------------------------------------------------------
/src/Exception/PlayerApi/PlayerApiHttpException.php:
--------------------------------------------------------------------------------
1 |
5 | {{ message }}
6 |
7 | {% endfor %}
8 | {% endfor %}
9 |
--------------------------------------------------------------------------------
/src/Message/Stats/UpdateSingularPlayerStatsMessage.php:
--------------------------------------------------------------------------------
1 | bootEnv(dirname(__DIR__) . '/.env');
11 | }
12 |
13 | if ($_SERVER['APP_DEBUG']) {
14 | umask(0000);
15 | }
16 |
--------------------------------------------------------------------------------
/assets/bootstrap.js:
--------------------------------------------------------------------------------
1 | import { startStimulusApp } from '@symfony/stimulus-bridge';
2 |
3 | // Registers Stimulus controllers from controllers.json and in the controllers/ directory
4 | export const app = startStimulusApp(require.context(
5 | '@symfony/stimulus-bridge/lazy-controller-loader!./controllers',
6 | true,
7 | /\.[jt]sx?$/
8 | ));
9 |
10 | // register any custom, 3rd party controllers here
11 | // app.register('some_controller_name', SomeImportedController);
12 |
--------------------------------------------------------------------------------
/src/Enum/KnownPlayers.php:
--------------------------------------------------------------------------------
1 | value;
19 | }
20 | }
21 |
--------------------------------------------------------------------------------
/docker-compose.yml:
--------------------------------------------------------------------------------
1 | version: '3'
2 | services:
3 | ###> doctrine/doctrine-bundle ###
4 | database:
5 | image: postgres:${POSTGRES_VERSION:-15}-alpine
6 | restart: always
7 | environment:
8 | POSTGRES_DB: ${POSTGRES_DB:-app}
9 | # You should definitely change the password in production
10 | POSTGRES_PASSWORD: ${POSTGRES_PASSWORD:-!ChangeMe!}
11 | POSTGRES_USER: ${POSTGRES_USER:-app}
12 | volumes:
13 | - database_data:/var/lib/postgresql/data:rw
14 |
15 | volumes:
16 | database_data:
--------------------------------------------------------------------------------
/bin/console:
--------------------------------------------------------------------------------
1 | #!/usr/bin/env php
2 |
2 |
3 |
5 |
6 |
7 |
8 |
9 |
10 |
11 |
12 |
13 | bin/
14 | config/
15 | public/
16 | src/
17 | tests/
18 |
19 |
--------------------------------------------------------------------------------
/src/ValueObject/GrandExchange/CatalogueItem.php:
--------------------------------------------------------------------------------
1 | value] = strtolower($enumValue->name);
24 | }
25 |
26 | return $values;
27 | }
28 | }
29 |
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
1 | ###> symfony/framework-bundle ###
2 | /.env.local
3 | /.env.local.php
4 | /.env.*.local
5 | /config/secrets/prod/prod.decrypt.private.php
6 | /public/bundles/
7 | /var/
8 | /vendor/
9 | ###< symfony/framework-bundle ###
10 |
11 | ###> symfony/webpack-encore-bundle ###
12 | /node_modules/
13 | /public/build/
14 | npm-debug.log
15 | yarn-error.log
16 | ###< symfony/webpack-encore-bundle ###
17 |
18 | ###> .idea ###
19 | /.idea/
20 | ###< .idea ###
21 |
22 | ###> squizlabs/php_codesniffer ###
23 | /.phpcs-cache
24 | /phpcs.xml
25 | ###< squizlabs/php_codesniffer ###
26 |
27 | ###> symfony/phpunit-bridge ###
28 | .phpunit.result.cache
29 | /phpunit.xml
30 | ###< symfony/phpunit-bridge ###
31 |
--------------------------------------------------------------------------------
/bin/phpunit:
--------------------------------------------------------------------------------
1 | #!/usr/bin/env php
2 | getClient();
16 | $this->assertInstanceOf(Client::class, $client);
17 | }
18 |
19 | public function testGetClientReturnsSameInstance(): void
20 | {
21 | $client1 = $this->getClient();
22 | $client2 = $this->getClient();
23 |
24 | $this->assertSame($client1, $client2);
25 | }
26 | }
27 |
--------------------------------------------------------------------------------
/migrations/Version20231020183902.php:
--------------------------------------------------------------------------------
1 | addSql('ALTER TABLE player ALTER total_xp TYPE BIGINT');
23 | }
24 |
25 | public function down(Schema $schema): void
26 | {
27 | $this->addSql('ALTER TABLE player ALTER total_xp TYPE INT');
28 | }
29 | }
30 |
--------------------------------------------------------------------------------
/config/packages/datatables.yaml:
--------------------------------------------------------------------------------
1 | # Latest documentation available at https://omines.github.io/datatables-bundle/#configuration
2 | datatables:
3 | # Set options, as documented at https://datatables.net/reference/option/
4 | options:
5 | lengthMenu : [10, 25, 50, 100, 250, 500, 1000, 2500]
6 | pageLength: 50
7 | dom: "<'row' <'col-sm-12' tr>><'row' <'col-sm-6'l><'col-sm-6 text-right'pi>>"
8 |
9 | template_parameters:
10 | # Example classes to integrate nicely with Bootstrap 3.x
11 | className: 'table table-striped table-bordered table-hover data-table'
12 | columnFilter: 'both'
13 |
14 | # You can for example override this to "tables" to keep the translation domains separated nicely
15 | translation_domain: 'messages'
16 |
--------------------------------------------------------------------------------
/config/packages/cache.yaml:
--------------------------------------------------------------------------------
1 | framework:
2 | cache:
3 | app: cache.adapter.filesystem
4 | system: cache.adapter.system
5 | # Unique name of your app: used to compute stable namespaces for cache keys.
6 | #prefix_seed: your_vendor_name/app_name
7 |
8 | # The "app" cache stores to the filesystem by default.
9 | # The data in this cache should persist between deploys.
10 | # Other options include:
11 |
12 | # Redis
13 | #app: cache.adapter.redis
14 | #default_redis_provider: redis://localhost
15 |
16 | # APCu (not recommended with heavy random-write workloads as memory fragmentation can cause perf issues)
17 | #app: cache.adapter.apcu
18 |
19 | # Namespaced pools use the above "app" backend by default
20 | #pools:
21 | #my.dedicated.cache: null
22 |
--------------------------------------------------------------------------------
/src/Scheduler/CronScheduler.php:
--------------------------------------------------------------------------------
1 | add(RecurringMessage::cron('*/2 * * * *', new UpdateAllPlayerStatsMessage(UpdateAllPlayersType::ACTIVE)))
20 | ->add(RecurringMessage::cron('0 * * * *', new UpdateAllPlayerClanNamesMessage()));
21 | }
22 | }
23 |
--------------------------------------------------------------------------------
/src/ValueObject/GrandExchange/CatalogueResponse.php:
--------------------------------------------------------------------------------
1 | items;
23 | }
24 |
25 | /**
26 | * @param CatalogueItem[] $items
27 | */
28 | public function setItems(array $items): void
29 | {
30 | $this->items = $items;
31 | }
32 |
33 | /**
34 | * @param CatalogueItem $item
35 | */
36 | public function addItem(CatalogueItem $item): void
37 | {
38 | $this->items[] = $item;
39 | }
40 | }
41 |
--------------------------------------------------------------------------------
/templates/includes/badges/github.html.twig:
--------------------------------------------------------------------------------
1 |
2 |
11 | View on GitHub
12 |
13 |
--------------------------------------------------------------------------------
/config/packages/framework.yaml:
--------------------------------------------------------------------------------
1 | # see https://symfony.com/doc/current/reference/configuration/framework.html
2 | framework:
3 | secret: '%env(APP_SECRET)%'
4 | #csrf_protection: true
5 | http_method_override: false
6 | handle_all_throwables: true
7 |
8 | #disable the translation component
9 | translator:
10 | enabled: false
11 |
12 | # Enables session support. Note that the session will ONLY be started if you read or write from it.
13 | # Remove or comment this section to explicitly disable session support.
14 | session:
15 | handler_id: null
16 | cookie_secure: auto
17 | cookie_samesite: lax
18 | storage_factory_id: session.storage.factory.native
19 |
20 | #esi: true
21 | #fragments: true
22 | php_errors:
23 | log: true
24 |
25 | when@test:
26 | framework:
27 | test: true
28 | session:
29 | storage_factory_id: session.storage.factory.mock_file
30 |
--------------------------------------------------------------------------------
/src/ValueObject/GrandExchange/CatalogueResponseCollection.php:
--------------------------------------------------------------------------------
1 | categories;
21 | }
22 |
23 | /**
24 | * @param CatalogueResponseCategory[] $categories
25 | */
26 | public function setCategories(array $categories): void
27 | {
28 | $this->categories = $categories;
29 | }
30 |
31 | /**
32 | * @param CatalogueResponseCategory $category
33 | */
34 | public function addCategory(CatalogueResponseCategory $category): void
35 | {
36 | $this->categories[] = $category;
37 | }
38 | }
39 |
--------------------------------------------------------------------------------
/src/MessageHandler/Clan/UpdateAllPlayerClanNamesHandler.php:
--------------------------------------------------------------------------------
1 | knownPlayerRepository->findAllNames();
22 | $players = $this->rsApiService->getClanNames($knownPlayers);
23 |
24 | foreach ($players as $player) {
25 | $this->knownPlayerRepository->updateClanName($player['name'], $player['clan']);
26 | }
27 | }
28 | }
29 |
--------------------------------------------------------------------------------
/config/services.yaml:
--------------------------------------------------------------------------------
1 | # This file is the entry point to configure your own services.
2 | # Files in the packages/ subdirectory configure your dependencies.
3 |
4 | # Put parameters here that don't need to change on each machine where the app is deployed
5 | # https://symfony.com/doc/current/best_practices.html#use-parameters-for-application-configuration
6 | parameters:
7 |
8 | services:
9 | # default configuration for services in *this* file
10 | _defaults:
11 | autowire: true # Automatically injects dependencies in your services.
12 | autoconfigure: true # Automatically registers your services as commands, event subscribers, etc.
13 |
14 | # makes classes in src/ available to be used as services
15 | # this creates a service per class whose id is the fully-qualified class name
16 | App\:
17 | resource: '../src/'
18 | exclude:
19 | - '../src/DependencyInjection/'
20 | - '../src/Entity/'
21 | - '../src/Kernel.php'
22 |
--------------------------------------------------------------------------------
/config/bundles.php:
--------------------------------------------------------------------------------
1 | ['all' => true],
5 | Symfony\Bundle\TwigBundle\TwigBundle::class => ['all' => true],
6 | Symfony\Bundle\WebProfilerBundle\WebProfilerBundle::class => ['dev' => true, 'test' => true],
7 | Symfony\UX\Chartjs\ChartjsBundle::class => ['all' => true],
8 | Symfony\WebpackEncoreBundle\WebpackEncoreBundle::class => ['all' => true],
9 | Doctrine\Bundle\DoctrineBundle\DoctrineBundle::class => ['all' => true],
10 | Doctrine\Bundle\MigrationsBundle\DoctrineMigrationsBundle::class => ['all' => true],
11 | Symfony\Bundle\MakerBundle\MakerBundle::class => ['dev' => true],
12 | Stof\DoctrineExtensionsBundle\StofDoctrineExtensionsBundle::class => ['all' => true],
13 | Symfony\Bundle\MonologBundle\MonologBundle::class => ['all' => true],
14 | Symfony\UX\StimulusBundle\StimulusBundle::class => ['all' => true],
15 | Omines\DataTablesBundle\DataTablesBundle::class => ['all' => true],
16 | ];
17 |
--------------------------------------------------------------------------------
/assets/controllers/activities_controller.js:
--------------------------------------------------------------------------------
1 | import {Controller} from "@hotwired/stimulus";
2 |
3 | export default class extends Controller {
4 | connect() {
5 | super.connect();
6 |
7 | //Get the button
8 | let mybutton = document.getElementById("btn-back-to-top");
9 |
10 | // When the user scrolls down 20px from the top of the document, show the button
11 | window.onscroll = function () {
12 | if (
13 | document.body.scrollTop > 20 ||
14 | document.documentElement.scrollTop > 20
15 | ) {
16 | mybutton.style.display = "block";
17 | } else {
18 | mybutton.style.display = "none";
19 | }
20 | };
21 |
22 | // When the user clicks on the button, scroll to the top of the document
23 | mybutton.addEventListener("click", function () {
24 | document.body.scrollTop = 0;
25 | document.documentElement.scrollTop = 0;
26 | });
27 | }
28 | }
29 |
--------------------------------------------------------------------------------
/src/Dto/QuestResponse.php:
--------------------------------------------------------------------------------
1 | loggedIn;
15 | }
16 |
17 | public function setLoggedIn(?string $loggedIn): QuestResponse
18 | {
19 | $this->loggedIn = $loggedIn;
20 | return $this;
21 | }
22 |
23 | /**
24 | * @return Quest[]|null
25 | */
26 | public function getQuests(): ?array
27 | {
28 | return $this->quests;
29 | }
30 |
31 | /**
32 | * @param Quest[]|null $quests
33 | * @return QuestResponse
34 | */
35 | public function setQuests(?array $quests): QuestResponse
36 | {
37 | $this->quests = $quests;
38 | return $this;
39 | }
40 |
41 | public function addQuest(Quest $quest): QuestResponse
42 | {
43 | $this->quests[] = $quest;
44 | return $this;
45 | }
46 | }
47 |
--------------------------------------------------------------------------------
/tests/Doctrine/Builder/SkillValueBuilder.php:
--------------------------------------------------------------------------------
1 | skillValue = new SkillValue();
15 | }
16 |
17 | public function build(): SkillValue
18 | {
19 | return $this->skillValue;
20 | }
21 |
22 | public function withId(?SkillEnum $id): self
23 | {
24 | $this->skillValue->id = $id;
25 | return $this;
26 | }
27 |
28 | public function withLevel(?int $level): self
29 | {
30 | $this->skillValue->level = $level;
31 | return $this;
32 | }
33 |
34 | public function withXp(int|float|null $xp): self
35 | {
36 | $this->skillValue->xp = $xp;
37 | return $this;
38 | }
39 |
40 | public function withRank(?int $rank): self
41 | {
42 | $this->skillValue->rank = $rank;
43 | return $this;
44 | }
45 | }
46 |
--------------------------------------------------------------------------------
/templates/includes/header.html.twig:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 | {% include 'includes/badges/github.html.twig' %}
9 | {% include 'includes/badges/discord.html.twig' %}
10 |
11 |
12 |
13 | {% if app.current_route != 'welcome' %}
14 |
{% include 'includes/alerts.html.twig' %}
15 | {% if form is defined %}
16 | {{ form(form) }}
17 | {% endif %}
18 | {% endif %}
19 |
20 |
21 |
22 |
23 |
24 |
25 |
--------------------------------------------------------------------------------
/tests/Doctrine/Builder/ActivityBuilder.php:
--------------------------------------------------------------------------------
1 | activity = new Activity();
15 | }
16 |
17 | public function build(): Activity
18 | {
19 | return $this->activity;
20 | }
21 |
22 | public function withDate(?DateTimeImmutable $dateTimeImmutable = null): self
23 | {
24 | if ($dateTimeImmutable === null) {
25 | $dateTimeImmutable = new DateTimeImmutable();
26 | }
27 |
28 | $this->activity->date = $dateTimeImmutable;
29 | return $this;
30 | }
31 |
32 | public function withDetails(?string $details = 'details'): self
33 | {
34 | $this->activity->details = $details;
35 | return $this;
36 | }
37 |
38 | public function withText(?string $text = 'text'): self
39 | {
40 | $this->activity->text = $text;
41 | return $this;
42 | }
43 | }
44 |
--------------------------------------------------------------------------------
/src/Dto/Activity.php:
--------------------------------------------------------------------------------
1 | 'Y-m-d H:i:s']),
24 | new ArrayDenormalizer(),
25 | new ObjectNormalizer(propertyTypeExtractor: new ReflectionExtractor())
26 | ],
27 | encoders: [
28 | new JsonEncoder()
29 | ]
30 | );
31 | }
32 | }
33 |
--------------------------------------------------------------------------------
/LICENSE:
--------------------------------------------------------------------------------
1 | MIT License
2 |
3 | Copyright (c) 2023 VincentPS
4 |
5 | Permission is hereby granted, free of charge, to any person obtaining a copy
6 | of this software and associated documentation files (the "Software"), to deal
7 | in the Software without restriction, including without limitation the rights
8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9 | copies of the Software, and to permit persons to whom the Software is
10 | furnished to do so, subject to the following conditions:
11 |
12 | The above copyright notice and this permission notice shall be included in all
13 | copies or substantial portions of the Software.
14 |
15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21 | SOFTWARE.
22 |
--------------------------------------------------------------------------------
/src/Controller/NewsFeedController.php:
--------------------------------------------------------------------------------
1 | addFlash(
21 | 'danger',
22 | 'Unable to load the news feed. This is likely due to a temporary issue with the RSS feed.' .
23 | ' Please try again later.'
24 | );
25 | return $this->redirectToRoute('summary');
26 | }
27 |
28 | return $this->render('newsfeed.html.twig', [
29 | 'rss' => $feed,
30 | ]);
31 | }
32 | }
33 |
--------------------------------------------------------------------------------
/config/packages/monolog.yaml:
--------------------------------------------------------------------------------
1 | monolog:
2 | use_microseconds: false
3 | channels:
4 | - deprecation # Deprecations are logged in the dedicated "deprecation" channel when it exists
5 |
6 | when@dev:
7 | monolog:
8 | handlers:
9 | main:
10 | type: stream
11 | path: "%kernel.logs_dir%/%kernel.environment%.log"
12 | level: debug
13 | channels: [ "!event", "!doctrine" ]
14 | console:
15 | type: console
16 | process_psr_3_messages: false
17 | channels: [ "!event", "!doctrine", "!console" ]
18 |
19 | when@test:
20 | monolog:
21 | handlers:
22 | main:
23 | type: fingers_crossed
24 | action_level: error
25 | handler: nested
26 | excluded_http_codes: [ 404, 405 ]
27 | channels: [ "!event" ]
28 | nested:
29 | type: stream
30 | path: "%kernel.logs_dir%/%kernel.environment%.log"
31 | level: debug
32 |
33 | when@prod:
34 | monolog:
35 | handlers:
36 | graylog:
37 | type: gelf
38 | publisher:
39 | hostname: "%env(GRAYLOG_HOST)%"
40 | port: "%env(GRAYLOG_PORT)%"
41 | level: '%env(LOG_LEVEL)%'
42 |
--------------------------------------------------------------------------------
/migrations/Version20240130182934.php:
--------------------------------------------------------------------------------
1 | addSql('CREATE INDEX IDX_98197A65B5F1AFE5 ON player USING gin(activities)');
24 | $this->addSql('CREATE INDEX IDX_98197A6515ED18F2 ON player USING gin(skill_values)');
25 | $this->addSql('CREATE INDEX IDX_98197A65989E5D34 ON player USING gin(quests)');
26 | }
27 |
28 | public function down(Schema $schema): void
29 | {
30 | // this down() migration is auto-generated, please modify it to your needs
31 | $this->addSql('DROP INDEX IDX_98197A65B5F1AFE5');
32 | $this->addSql('DROP INDEX IDX_98197A6515ED18F2');
33 | $this->addSql('DROP INDEX IDX_98197A65989E5D34');
34 | }
35 | }
36 |
--------------------------------------------------------------------------------
/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "devDependencies": {
3 | "@babel/core": "^7.17.0",
4 | "@babel/plugin-proposal-class-properties": "^7.18.6",
5 | "@babel/preset-env": "^7.16.0",
6 | "@hotwired/stimulus": "^3.0.0",
7 | "@symfony/stimulus-bridge": "^3.2.0",
8 | "@symfony/stimulus-bundle": "file:vendor/symfony/stimulus-bundle/assets",
9 | "@symfony/ux-chartjs": "file:vendor/symfony/ux-chartjs/assets",
10 | "@symfony/webpack-encore": "^4.2.0",
11 | "bootstrap": "^5.3.2",
12 | "chart.js": "^3.4.1",
13 | "chartjs-plugin-trendline": "^2.1.6",
14 | "chartjs-plugin-zoom": "^2.2.0",
15 | "core-js": "^3.23.0",
16 | "datatables.net": "^1.12.1",
17 | "datatables.net-dt": "^1.12.1",
18 | "jquery": "^3.6.1",
19 | "regenerator-runtime": "^0.13.9",
20 | "webpack": "^5.74.0",
21 | "webpack-cli": "^4.10.0",
22 | "webpack-notifier": "^1.15.0"
23 | },
24 | "license": "UNLICENSED",
25 | "private": true,
26 | "scripts": {
27 | "dev-server": "encore dev-server",
28 | "dev": "encore dev",
29 | "watch": "encore dev --watch",
30 | "build": "encore production --progress"
31 | }
32 | }
33 |
--------------------------------------------------------------------------------
/src/Command/RsApiUpdate/UpdateClanCommand.php:
--------------------------------------------------------------------------------
1 | messageBus->dispatch(new UpdateAllPlayerClanNamesMessage());
29 | $io->success('Clan data update initiated for all known players.');
30 |
31 | return Command::SUCCESS;
32 | }
33 | }
34 |
--------------------------------------------------------------------------------
/templates/includes/badges/discord.html.twig:
--------------------------------------------------------------------------------
1 |
2 |
6 | Join us on Discord
7 |
8 |
--------------------------------------------------------------------------------
/src/Service/Chart/QuestChartService.php:
--------------------------------------------------------------------------------
1 | chartBuilder->createChart(Chart::TYPE_DOUGHNUT)
19 | ->setOptions(['color' => 'rgb(255, 255, 255)'])
20 | ->setData([
21 | 'labels' => ['Completed', 'In Progress', 'Not Started'],
22 | 'datasets' => [
23 | [
24 | 'backgroundColor' => ['rgb(225,187,52)', 'rgb(52,189,209)', 'rgb(197,32,55)'],
25 | 'borderColor' => 'rgb(0, 0, 0)',
26 | 'data' => [
27 | $player->getQuestsCompleted(),
28 | $player->getQuestsStarted(),
29 | $player->getQuestsNotStarted()
30 | ]
31 | ]
32 | ]
33 | ]);
34 | }
35 | }
36 |
--------------------------------------------------------------------------------
/phpunit.xml.dist:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
9 |
10 |
11 |
12 |
13 |
14 |
15 |
16 |
17 |
18 |
19 |
20 | tests
21 |
22 |
23 |
24 |
25 |
26 | src
27 |
28 |
29 |
30 |
31 |
32 |
33 |
34 |
35 |
36 |
37 |
--------------------------------------------------------------------------------
/migrations/Version20250212235809.php:
--------------------------------------------------------------------------------
1 | addSql('ALTER TABLE known_player ADD last_used_at TIMESTAMP(0) WITHOUT TIME ZONE DEFAULT NULL');
24 | $this->addSql('COMMENT ON COLUMN known_player.last_used_at IS \'(DC2Type:datetime_immutable)\'');
25 | $this->addSql('UPDATE known_player SET last_used_at = NOW()');
26 | $this->addSql('ALTER TABLE known_player ALTER last_used_at SET NOT NULL');
27 | }
28 |
29 | public function down(Schema $schema): void
30 | {
31 | // this down() migration is auto-generated, please modify it to your needs
32 | $this->addSql('ALTER TABLE known_player DROP last_used_at');
33 | }
34 | }
35 |
--------------------------------------------------------------------------------
/src/Doctrine/Type/ActivityType.php:
--------------------------------------------------------------------------------
1 |
30 | */
31 | protected function normalizers(): array
32 | {
33 | return [
34 | new DateTimeNormalizer(['datetime_format' => 'Y-m-d H:i:s']),
35 | new ArrayDenormalizer(),
36 | new BackedEnumNormalizer(),
37 | new ObjectNormalizer(propertyTypeExtractor: new ReflectionExtractor())
38 | ];
39 | }
40 | }
41 |
--------------------------------------------------------------------------------
/src/Service/GuzzleCachedClient.php:
--------------------------------------------------------------------------------
1 | kernel->getProjectDir() . '/var/cache';
24 | $cache_storage = new Psr6CacheStorage(
25 | new FilesystemAdapter(
26 | $requestCacheFolderName,
27 | 600,
28 | $cacheFolderPath
29 | )
30 | );
31 |
32 | $stack = HandlerStack::create();
33 | $stack->push(
34 | new CacheMiddleware(
35 | new GreedyCacheStrategy(
36 | $cache_storage,
37 | 600
38 | )
39 | ),
40 | 'greedy-cache'
41 | );
42 |
43 | return new Client(['handler' => $stack]);
44 | }
45 | }
46 |
--------------------------------------------------------------------------------
/src/EventListener/RequestListener.php:
--------------------------------------------------------------------------------
1 | isMainRequest()) {
15 | // don't do anything if it's not the main request
16 | return;
17 | }
18 |
19 | $ignoredRoutes = ['welcome'];
20 |
21 | /** @var string $currentRoute */
22 | $currentRoute = $event->getRequest()->attributes->get('_route');
23 |
24 | if (
25 | !str_starts_with($currentRoute, '_wdt')
26 | && !str_starts_with($currentRoute, '_profiler')
27 | && !in_array($currentRoute, $ignoredRoutes, true)
28 | && empty($event->getRequest()->getSession()->get('currentPlayerName'))
29 | ) {
30 | $event->setResponse(new RedirectResponse('/welcome'));
31 | }
32 |
33 | if ($currentRoute === 'welcome' && !empty($event->getRequest()->getSession()->get('currentPlayerName'))) {
34 | $event->setResponse(new RedirectResponse('/'));
35 | }
36 | }
37 | }
38 |
--------------------------------------------------------------------------------
/templates/quests.html.twig:
--------------------------------------------------------------------------------
1 | {% extends 'base.html.twig' %}
2 |
3 | {% block title %}RM Tracker - Quests{% endblock %}
4 |
5 | {% block body %}
6 |
7 | {% include 'includes/header.html.twig' %}
8 |
9 |
10 |
11 | {% include 'includes/sidemenu.html.twig' %}
12 |
13 |
14 |
15 |
18 |
19 |
20 |
QUESTS
21 |
22 |
27 |
28 |
29 |
30 |
31 |
32 |
33 | {% endblock %}
34 |
--------------------------------------------------------------------------------
/tests/Doctrine/Builder/QuestBuilder.php:
--------------------------------------------------------------------------------
1 | quest = new Quest();
16 | }
17 |
18 | public function build(): Quest
19 | {
20 | return $this->quest;
21 | }
22 |
23 | public function withTitle(?string $title): self
24 | {
25 | $this->quest->title = $title;
26 | return $this;
27 | }
28 |
29 | public function withStatus(?QuestStatus $status): self
30 | {
31 | $this->quest->status = $status;
32 | return $this;
33 | }
34 |
35 | public function withDifficulty(?QuestDifficulty $difficulty): self
36 | {
37 | $this->quest->difficulty = $difficulty;
38 | return $this;
39 | }
40 |
41 | public function withMembers(?bool $members): self
42 | {
43 | $this->quest->members = $members;
44 | return $this;
45 | }
46 |
47 | public function withQuestPoints(?int $questPoints): self
48 | {
49 | $this->quest->questPoints = $questPoints;
50 | return $this;
51 | }
52 |
53 | public function withUserEligible(?bool $userEligible): self
54 | {
55 | $this->quest->userEligible = $userEligible;
56 | return $this;
57 | }
58 | }
59 |
--------------------------------------------------------------------------------
/src/Controller/DashboardController.php:
--------------------------------------------------------------------------------
1 | headerSearchForm();
22 | $playerName = $this->getCurrentPlayerName();
23 | $player = $playerRepository->findLatestByName($playerName);
24 | $knownPlayer = $knownPlayerRepository->findOneByName($playerName);
25 |
26 | if (is_null($player)) {
27 | return $this->redirectToRoute('welcome');
28 | }
29 |
30 | return $this->render('summary.html.twig', [
31 | 'chart' => $questChartService->getChart($player),
32 | 'playerInfo' => $player,
33 | 'clanName' => $knownPlayer?->getClanName(),
34 | 'form' => $form->createView(),
35 | 'isDoubleXpLive' => $doubleXpService->isDoubleXpLive(),
36 | ]);
37 | }
38 | }
39 |
--------------------------------------------------------------------------------
/src/MessageHandler/Stats/UpdateAllPlayerStatsHandler.php:
--------------------------------------------------------------------------------
1 | type) {
27 | UpdateAllPlayersType::ACTIVE => $this->knownPlayerRepository->findAllActive(),
28 | UpdateAllPlayersType::INACTIVE => $this->knownPlayerRepository->findAllInactive(),
29 | };
30 | } catch (DateMalformedStringException) {
31 | // Do nothing because this cannot happen unless the ACTIVITY_THRESHOLD is changed incorrectly
32 | return;
33 | }
34 |
35 | foreach ($knownPlayers as $knownPlayer) {
36 | $this->messageBus->dispatch(new UpdateSingularPlayerStatsMessage((string)$knownPlayer->getName()));
37 | }
38 | }
39 | }
40 |
--------------------------------------------------------------------------------
/src/Trait/SerializerAwareTrait.php:
--------------------------------------------------------------------------------
1 | serializer)) {
22 | $this->serializer = new Serializer(
23 | [
24 | new DateTimeNormalizer(),
25 | new ArrayDenormalizer(),
26 | new BackedEnumNormalizer(),
27 | new ObjectNormalizer(
28 | classMetadataFactory: new ClassMetadataFactory(new AttributeLoader()),
29 | propertyTypeExtractor: new ReflectionExtractor()
30 | )
31 | ],
32 | [
33 | new JsonEncoder()
34 | ]
35 | );
36 | }
37 |
38 | return $this->serializer;
39 | }
40 | }
41 |
--------------------------------------------------------------------------------
/.env:
--------------------------------------------------------------------------------
1 | # In all environments, the following files are loaded if they exist,
2 | # the latter taking precedence over the former:
3 | #
4 | # * .env contains default values for the environment variables needed by the app
5 | # * .env.local uncommitted file with local overrides
6 | # * .env.$APP_ENV committed environment-specific defaults
7 | # * .env.$APP_ENV.local uncommitted environment-specific overrides
8 | #
9 | # Real environment variables win over .env files.
10 | #
11 | # DO NOT DEFINE PRODUCTION SECRETS IN THIS FILE NOR IN ANY OTHER COMMITTED FILES.
12 | # https://symfony.com/doc/current/configuration/secrets.html
13 | #
14 | # Run "composer dump-env prod" to compile .env files for production use (requires symfony/flex >=1.2).
15 | # https://symfony.com/doc/current/best_practices.html#use-environment-variables-for-infrastructure-configuration
16 |
17 | ###> symfony/framework-bundle ###
18 | APP_ENV=dev
19 | APP_SECRET=4f7cf8431ac48d54c64fb10a6abb3cc9
20 | ###< symfony/framework-bundle ###
21 |
22 | ###> doctrine/doctrine-bundle ###
23 | DATABASE_URL="postgresql://app:!ChangeMe!@127.0.0.1:5432/app?serverVersion=15&charset=utf8"
24 | ###< doctrine/doctrine-bundle ###
25 |
26 | ###> symfony/messenger ###
27 | # Choose one of the transports below
28 | # MESSENGER_TRANSPORT_DSN=amqp://guest:guest@localhost:5672/%2f/messages
29 | # MESSENGER_TRANSPORT_DSN=redis://localhost:6379/messages
30 | MESSENGER_TRANSPORT_DSN=doctrine://default?auto_setup=0
31 | ###< symfony/messenger ###
32 |
33 | ###> graylog2/gelf-php ###
34 | GRAYLOG_HOST=""
35 | GRAYLOG_PORT=""
36 | LOG_LEVEL=debug
37 | ###< graylog2/gelf-php ###
--------------------------------------------------------------------------------
/migrations/Version20230318074226.php:
--------------------------------------------------------------------------------
1 | addSql('ALTER TABLE player ALTER activities TYPE jsonb');
24 | $this->addSql('ALTER TABLE player ALTER skill_values TYPE jsonb');
25 | $this->addSql('ALTER TABLE player ALTER quests TYPE jsonb');
26 | $this->addSql('CREATE INDEX IDX_98197A655E237E068B8E8428A5D4CED1 ON player (name, created_at, total_xp)');
27 | $this->addSql('CREATE INDEX IDX_98197A658B8E8428A5D4CED1 ON player (created_at, total_xp)');
28 | }
29 |
30 | public function down(Schema $schema): void
31 | {
32 | // this down() migration is auto-generated, please modify it to your needs
33 | $this->addSql('CREATE SCHEMA public');
34 | $this->addSql('DROP INDEX IDX_98197A655E237E068B8E8428A5D4CED1');
35 | $this->addSql('DROP INDEX IDX_98197A658B8E8428A5D4CED1');
36 | $this->addSql('ALTER TABLE player ALTER activities TYPE jsonb');
37 | $this->addSql('ALTER TABLE player ALTER skill_values TYPE jsonb');
38 | $this->addSql('ALTER TABLE player ALTER quests TYPE jsonb');
39 | }
40 | }
41 |
--------------------------------------------------------------------------------
/src/Command/RsApiUpdate/Singular.php:
--------------------------------------------------------------------------------
1 | addArgument('playerName', InputArgument::REQUIRED, 'Player to be updated');
34 | }
35 |
36 | protected function execute(InputInterface $input, OutputInterface $output): int
37 | {
38 | $io = new SymfonyStyle($input, $output);
39 |
40 | $playerName = $input->getArgument('playerName');
41 |
42 | if (!is_string($playerName)) {
43 | $io->error('Please provide a player name as a string.');
44 | return Command::FAILURE;
45 | }
46 |
47 | return $this->handleSinglePlayer($io, $playerName);
48 | }
49 | }
50 |
--------------------------------------------------------------------------------
/src/Command/RsApiUpdate/All.php:
--------------------------------------------------------------------------------
1 | knownPlayerRepository->findAll();
36 |
37 | foreach ($knownPlayers as $knownPlayer) {
38 | $io->info('Updating data for player: ' . $knownPlayer->getName());
39 | $this->handleSinglePlayer($io, (string)$knownPlayer->getName());
40 | }
41 |
42 | $io->success(sprintf('Data updated for %d known players.', count($knownPlayers)));
43 | return Command::SUCCESS;
44 | }
45 | }
46 |
--------------------------------------------------------------------------------
/migrations/Version20250209124013.php:
--------------------------------------------------------------------------------
1 | addSql('ALTER TABLE known_player ADD clan_name VARCHAR(125) DEFAULT NULL');
24 | $this->addSql('ALTER TABLE known_player ADD updated_at TIMESTAMP(0) WITHOUT TIME ZONE DEFAULT NULL');
25 | $this->addSql('COMMENT ON COLUMN known_player.updated_at IS \'(DC2Type:datetime_immutable)\'');
26 | $this->addSql('UPDATE known_player SET updated_at = NOW()');
27 | $this->addSql('ALTER TABLE known_player ALTER updated_at SET NOT NULL');
28 |
29 | //set clan name based on player table
30 | $this->addSql("UPDATE known_player kp SET clan_name = NULLIF((SELECT clan FROM player p WHERE p.name = kp.name ORDER BY created_at DESC LIMIT 1), '');");
31 | $this->addSql('ALTER TABLE player DROP COLUMN clan');
32 | }
33 |
34 | public function down(Schema $schema): void
35 | {
36 | // this down() migration is auto-generated, please modify it to your needs
37 | $this->addSql('ALTER TABLE known_player DROP clan_name');
38 | $this->addSql('ALTER TABLE known_player DROP updated_at');
39 |
40 | $this->addSql('ALTER TABLE player ADD clan VARCHAR(125) DEFAULT NULL');
41 | }
42 | }
43 |
--------------------------------------------------------------------------------
/config/packages/webpack_encore.yaml:
--------------------------------------------------------------------------------
1 | webpack_encore:
2 | # The path where Encore is building the assets - i.e. Encore.setOutputPath()
3 | output_path: '%kernel.project_dir%/public/build'
4 | # If multiple builds are defined (as shown below), you can disable the default build:
5 | # output_path: false
6 |
7 | # Set attributes that will be rendered on all script and link tags
8 | script_attributes:
9 | defer: true
10 | # Uncomment (also under link_attributes) if using Turbo Drive
11 | # https://turbo.hotwired.dev/handbook/drive#reloading-when-assets-change
12 | # 'data-turbo-track': reload
13 | # link_attributes:
14 | # Uncomment if using Turbo Drive
15 | # 'data-turbo-track': reload
16 |
17 | # If using Encore.enableIntegrityHashes() and need the crossorigin attribute (default: false, or use 'anonymous' or 'use-credentials')
18 | # crossorigin: 'anonymous'
19 |
20 | # Preload all rendered script and link tags automatically via the HTTP/2 Link header
21 | # preload: true
22 |
23 | # Throw an exception if the entrypoints.json file is missing or an entry is missing from the data
24 | # strict_mode: false
25 |
26 | # If you have multiple builds:
27 | # builds:
28 | # frontend: '%kernel.project_dir%/public/frontend/build'
29 |
30 | # pass the build name as the 3rd argument to the Twig functions
31 | # {{ encore_entry_script_tags('entry1', null, 'frontend') }}
32 |
33 | framework:
34 | assets:
35 | json_manifest_path: '%kernel.project_dir%/public/build/manifest.json'
36 |
37 | #when@prod:
38 | # webpack_encore:
39 | # # Cache the entrypoints.json (rebuild Symfony's cache when entrypoints.json changes)
40 | # # Available in version 1.2
41 | # cache: true
42 |
43 | #when@test:
44 | # webpack_encore:
45 | # strict_mode: false
46 |
--------------------------------------------------------------------------------
/src/Trait/GuzzleCachedClientTrait.php:
--------------------------------------------------------------------------------
1 | client)) {
19 | $requestCacheFolderName = 'GuzzleFileCache';
20 | $cacheFolderPath = $this->getProjectDir() . '/var/cache';
21 | $cache_storage = new Psr6CacheStorage(
22 | new FilesystemAdapter(
23 | $requestCacheFolderName,
24 | 600,
25 | $cacheFolderPath
26 | )
27 | );
28 |
29 | $stack = HandlerStack::create();
30 | $stack->push(
31 | new CacheMiddleware(
32 | new GreedyCacheStrategy($cache_storage, 600)
33 | ),
34 | 'greedy-cache'
35 | );
36 |
37 | $this->client = new Client(['handler' => $stack]);
38 | }
39 |
40 | return $this->client;
41 | }
42 |
43 | private function getProjectDir(): ?string
44 | {
45 | $markerFiles = ['.env', 'composer.json'];
46 |
47 | $currentDir = realpath((string)getcwd());
48 |
49 | while ($currentDir !== '/') {
50 | foreach ($markerFiles as $marker) {
51 | $markerPath = $currentDir . DIRECTORY_SEPARATOR . $marker;
52 |
53 | if (file_exists($markerPath)) {
54 | return (string)$currentDir;
55 | }
56 | }
57 |
58 | $currentDir = dirname((string)$currentDir);
59 | }
60 |
61 | return null;
62 | }
63 | }
64 |
--------------------------------------------------------------------------------
/src/Service/DoubleXpService.php:
--------------------------------------------------------------------------------
1 | getClient()->get('https://rs.runescape.com/en-GB/double-xp');
17 | $content = $response->getBody()->getContents();
18 |
19 | preg_match('/
13 |
16 |
17 |
18 |
20 |
21 |
22 |
23 | {% block stylesheets %}
24 | {{ encore_entry_link_tags('app') }}
25 | {% endblock %}
26 |
27 | {% block javascripts %}
28 | {{ encore_entry_script_tags('app') }}
29 | {% endblock %}
30 |
31 |
32 | {% block body %}{% endblock %}
33 |
34 |