From 19dea8f983b8da14dd9f4ebdaf4c933e64ffba61 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Berg=20J=C3=A1nos?= Date: Sat, 3 May 2025 11:09:41 +0200 Subject: [PATCH] ! Published. --- .dockerignore | 9 + .github/workflows/latest.yaml | 49 + .github/workflows/testing.yaml | 45 + .gitignore | 3 + LICENSE | 9 + README.md | 186 + build/default/Dockerfile | 68 + build/default/build.sh | 57 + res/docker-compose.yaml | 15 + res/first_step_1.png | Bin 0 -> 7373 bytes res/first_step_2.png | Bin 0 -> 11435 bytes res/logo.svg | 706 ++ src/.env.example | 35 + src/.gitignore | 32 + src/.php-cs-fixer.php | 33 + src/Bootstrap/Commands.php | 32 + src/Bootstrap/Config.php | 7 + src/Bootstrap/Discord.php | 27 + src/Bootstrap/Environment.php | 15 + src/Bootstrap/Events.php | 44 + src/Bootstrap/Requires.php | 7 + src/Bot.php | 10 + src/Bot/Cache.php | 176 + src/Bot/CacheItem.php | 79 + src/Bot/DevLogger.php | 102 + src/Bot/DiscordBot.php | 224 + src/Bot/ObjectCache.php | 89 + src/BotDev.php | 161 + src/Client/ApiClient.php | 240 + src/Client/ApiResponse.php | 225 + src/Client/ClientMessages.php | 347 + src/Client/Models/DiscordUser.php | 88 + src/Client/Models/Remainder.php | 129 + src/Client/Responses/DiscordUserResponse.php | 43 + src/Client/Responses/Loadable.php | 141 + .../Responses/RemainderListResponse.php | 59 + src/Client/Responses/RemainderResponse.php | 54 + src/Client/Template.php | 133 + src/Client/Traits/AssureTimezoneSet.php | 66 + src/Client/Traits/FromJson.php | 36 + src/Client/Traits/HasApiClient.php | 24 + src/Client/Traits/HasCache.php | 24 + src/Client/Traits/HasDiscord.php | 32 + src/Client/Traits/HasTemplate.php | 24 + src/Client/Traits/RemainderListCommand.php | 130 + src/Client/Traits/Singleton.php | 50 + src/Commands/CreateRemainder.php | 222 + src/Commands/EditRemainder.php | 300 + src/Commands/ListRemainders.php | 132 + src/Commands/Profile.php | 257 + src/Commands/RemoveRemainder.php | 210 + src/Core/Commands/Command.php | 19 + src/Core/Commands/CommandHandler.php | 15 + src/Core/Commands/CommandQueue.php | 164 + src/Core/Commands/QueuedCommand.php | 77 + src/Core/Disabled.php | 10 + src/Core/Env.php | 22 + .../ApplicationCommandPermissionsUpdate.php | 12 + src/Core/Events/AutoModerationRuleCreate.php | 13 + src/Core/Events/Event.php | 14 + src/Core/Events/Init.php | 11 + src/Core/Events/MessageCreate.php | 13 + src/Core/Events/MessageDelete.php | 12 + src/Core/Events/MessageDeleteBulk.php | 13 + src/Core/Events/MessageUpdate.php | 13 + src/Core/HMR/HotDirectory.php | 58 + src/Core/HMR/HotFile.php | 66 + src/Core/functions.php | 274 + src/Core/helpers.php | 122 + src/Events/Message.php | 28 + src/Events/Ready.php | 52 + src/LICENSE | 21 + src/README.md | 95 + src/Services/ReminderService.php | 230 + src/Storage/.gitignore | 2 + src/TODO | 286 + src/Tests/CommandAttributeTest.php | 24 + src/Tests/FunctionsTest.php | 32 + src/composer.json | 58 + src/composer.lock | 7448 +++++++++++++++++ src/phpunit.xml | 17 + src/version | 1 + 82 files changed, 14408 insertions(+) create mode 100644 .dockerignore create mode 100644 .github/workflows/latest.yaml create mode 100644 .github/workflows/testing.yaml create mode 100644 .gitignore create mode 100644 LICENSE create mode 100644 README.md create mode 100644 build/default/Dockerfile create mode 100755 build/default/build.sh create mode 100644 res/docker-compose.yaml create mode 100644 res/first_step_1.png create mode 100644 res/first_step_2.png create mode 100644 res/logo.svg create mode 100644 src/.env.example create mode 100644 src/.gitignore create mode 100644 src/.php-cs-fixer.php create mode 100644 src/Bootstrap/Commands.php create mode 100644 src/Bootstrap/Config.php create mode 100644 src/Bootstrap/Discord.php create mode 100644 src/Bootstrap/Environment.php create mode 100644 src/Bootstrap/Events.php create mode 100644 src/Bootstrap/Requires.php create mode 100644 src/Bot.php create mode 100644 src/Bot/Cache.php create mode 100644 src/Bot/CacheItem.php create mode 100644 src/Bot/DevLogger.php create mode 100644 src/Bot/DiscordBot.php create mode 100644 src/Bot/ObjectCache.php create mode 100644 src/BotDev.php create mode 100644 src/Client/ApiClient.php create mode 100644 src/Client/ApiResponse.php create mode 100644 src/Client/ClientMessages.php create mode 100644 src/Client/Models/DiscordUser.php create mode 100644 src/Client/Models/Remainder.php create mode 100644 src/Client/Responses/DiscordUserResponse.php create mode 100644 src/Client/Responses/Loadable.php create mode 100644 src/Client/Responses/RemainderListResponse.php create mode 100644 src/Client/Responses/RemainderResponse.php create mode 100644 src/Client/Template.php create mode 100644 src/Client/Traits/AssureTimezoneSet.php create mode 100644 src/Client/Traits/FromJson.php create mode 100644 src/Client/Traits/HasApiClient.php create mode 100644 src/Client/Traits/HasCache.php create mode 100644 src/Client/Traits/HasDiscord.php create mode 100644 src/Client/Traits/HasTemplate.php create mode 100644 src/Client/Traits/RemainderListCommand.php create mode 100644 src/Client/Traits/Singleton.php create mode 100644 src/Commands/CreateRemainder.php create mode 100644 src/Commands/EditRemainder.php create mode 100644 src/Commands/ListRemainders.php create mode 100644 src/Commands/Profile.php create mode 100644 src/Commands/RemoveRemainder.php create mode 100644 src/Core/Commands/Command.php create mode 100644 src/Core/Commands/CommandHandler.php create mode 100644 src/Core/Commands/CommandQueue.php create mode 100644 src/Core/Commands/QueuedCommand.php create mode 100644 src/Core/Disabled.php create mode 100644 src/Core/Env.php create mode 100644 src/Core/Events/ApplicationCommandPermissionsUpdate.php create mode 100644 src/Core/Events/AutoModerationRuleCreate.php create mode 100644 src/Core/Events/Event.php create mode 100644 src/Core/Events/Init.php create mode 100644 src/Core/Events/MessageCreate.php create mode 100644 src/Core/Events/MessageDelete.php create mode 100644 src/Core/Events/MessageDeleteBulk.php create mode 100644 src/Core/Events/MessageUpdate.php create mode 100644 src/Core/HMR/HotDirectory.php create mode 100644 src/Core/HMR/HotFile.php create mode 100644 src/Core/functions.php create mode 100644 src/Core/helpers.php create mode 100644 src/Events/Message.php create mode 100644 src/Events/Ready.php create mode 100644 src/LICENSE create mode 100644 src/README.md create mode 100644 src/Services/ReminderService.php create mode 100644 src/Storage/.gitignore create mode 100644 src/TODO create mode 100644 src/Tests/CommandAttributeTest.php create mode 100644 src/Tests/FunctionsTest.php create mode 100644 src/composer.json create mode 100644 src/composer.lock create mode 100644 src/phpunit.xml create mode 100644 src/version diff --git a/.dockerignore b/.dockerignore new file mode 100644 index 0000000..e387bf7 --- /dev/null +++ b/.dockerignore @@ -0,0 +1,9 @@ +src/Tests +src/vendor +src/.env +src/.env.example +src/debug/ +volumes/ +.git/ +.git-crypt/ +.github/ diff --git a/.github/workflows/latest.yaml b/.github/workflows/latest.yaml new file mode 100644 index 0000000..19bda9d --- /dev/null +++ b/.github/workflows/latest.yaml @@ -0,0 +1,49 @@ +name: Build the "latest" docker image + +env: + PACKAGE_NAME: discord-bot + PACKAGE_TAG: latest + +run-name: ${{ github.actor }} is building the "latest" docker image + +on: + push: +# tags: +# - v** + branches: + - 'master' + - 'main' + paths-ignore: + - README.md + - src/README.md + +jobs: + build_docker_image: + runs-on: ubuntu-latest + + steps: + - name: Checking out repository + uses: actions/checkout@v4 + + - name: Login to Goliath Container Registry + uses: docker/login-action@v3 + with: + registry: ${{ vars.PACKAGE_REPOSITORY }} + username: ${{ secrets.PACKAGE_USER }} + password: ${{ secrets.PACKAGE_PASS }} + + - name: Set up Docker Buildx + uses: docker/setup-buildx-action@v3 + + - name: Create Docker Buildx contex + run: docker buildx create --name goliath; docker buildx use goliath; docker buildx inspect --bootstrap; + + - name: Building docker image + run: | + docker buildx build . \ + --file build/default/Dockerfile \ + --platform linux/amd64,linux/arm64 \ + --tag ${{ vars.PACKAGE_REPOSITORY }}/${{ vars.PACKAGE_ORGANIZATION }}/${{ env.PACKAGE_NAME }}:${{ env.PACKAGE_TAG }} \ + --provenance=false \ + --sbom=false \ + --push diff --git a/.github/workflows/testing.yaml b/.github/workflows/testing.yaml new file mode 100644 index 0000000..2ccb1dc --- /dev/null +++ b/.github/workflows/testing.yaml @@ -0,0 +1,45 @@ +name: Build docker image + +env: + PACKAGE_NAME: discord-bot + PACKAGE_TAG: testing + + +run-name: ${{ github.actor }} is building the docker image + +on: + push: + branches: +# - 'test-**' + - 'dev' + +jobs: + build_docker_image: + runs-on: ubuntu-latest + + steps: + - name: Checking out repository + uses: actions/checkout@v4 + + - name: Login to Goliath Container Registry + uses: docker/login-action@v3 + with: + registry: ${{ vars.PACKAGE_REPOSITORY }} + username: ${{ secrets.PACKAGE_USER }} + password: ${{ secrets.PACKAGE_PASS }} + + - name: Set up Docker Buildx + uses: docker/setup-buildx-action@v3 + + - name: Create Docker Buildx contex + run: docker buildx create --name goliath; docker buildx use goliath; docker buildx inspect --bootstrap; + + - name: Building docker image + run: | + docker buildx build . \ + --file build/default/Dockerfile \ + --platform linux/amd64,linux/arm64 \ + --tag ${{ vars.PACKAGE_REPOSITORY }}/${{ vars.PACKAGE_ORGANIZATION }}/${{ env.PACKAGE_NAME }}:${{ env.PACKAGE_TAG }} \ + --provenance=false \ + --sbom=false \ + --push diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..9152406 --- /dev/null +++ b/.gitignore @@ -0,0 +1,3 @@ +.vscode +volumes/debug/* +volumes/env/* diff --git a/LICENSE b/LICENSE new file mode 100644 index 0000000..1fd825a --- /dev/null +++ b/LICENSE @@ -0,0 +1,9 @@ +MIT License + +Copyright (c) 2025 proxima + +Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. diff --git a/README.md b/README.md new file mode 100644 index 0000000..8825f5f --- /dev/null +++ b/README.md @@ -0,0 +1,186 @@ + +![Latest](https://proxima.goliath.hu/proxima/discord-bot/actions/workflows/latest.yaml/badge.svg?branch=main) +![Testing](https://proxima.goliath.hu/proxima/discord-bot/actions/workflows/testing.yaml/badge.svg?branch=dev) + +![Proxima Discord bot](res/logo.svg) + +# Proxima Discord Bot + +[Proxima](https://proxima.goliath.hu) is a **Remainder Application**, like many others, which primarily uses [Discord](https://discord.com/) to deliver the remainders to the user. + +It provides an API interface to be embedded in other services, using Bearer Token Authentication. + +The Discord bot provides basic functionality for convenience to the user as well. + +This is the source code for the discord bot. + +The bot allows the user to manage (add, edit, remove, list) its own remainders on the installed servers. + +This bot is intended to be run as a docker container, see below. + +For more details, see: [Proxima -> Discord Bot](https://proxima.goliath.hu#discord-bot) + +# Main Technologies used +- Based on [commandstring/dphp-bot](https://github.com/CommandString/discordphp-bot-template) package +- [PHP 8.2+](https://www.php.net/) +- [composer](https://getcomposer.org/) +- [ReactPHP ](https://reactphp.org/) +- [discord-php/DiscordPHP](https://github.com/discord-php/DiscordPHP) +- [team-reflex/discord-php](https://github.com/discord-php/DiscordPHP) +- [Smarty](https://www.smarty.net/) +- and many more... + +# What is needed to run the bot + +- A working docker instance with compose. +- A Discord account: [Discord](https://discord.com/) +- A Discord application: [Discord Developer Portal](https://discord.com/developers/applications) +- A dedicated Discord server where the bot can send important notifications. + +# Running with Docker compose + +### Follow these easy steps to set up and your own bot: + +> **NOTE:** The program inside the container is running under a **non-privileged** user with +> +> `UID:1000` and `GID:1000`. +> +> The storage folder **must** be writable for that user! +> +> In the example, we just make it writable to everyone (please use proper access control instead!) +> +> *Or you can build your own image with different `UID`/`GID` values.* + +#### 1. Create the directory structure (bash) + +```bash +mkdir -p volumes/bot/{storage,env} +``` + +#### 2. Update directory permissions to allow write access to the restricted docker user +***This is for demonstration only, please use proper access control!*** + +```bash +chmod o+w volumes/bot/storage +``` + +#### 3. Create the `.env` file from [src/.env.example](src/.env.example) file + + + +```bash +wget -O volumes/bot/env/.env https://proxima.goliath.hu/proxima/discord-bot/raw/branch/main/src/.env.example + +-OR- + +curl -f -o volumes/bot/env/.env https://proxima.goliath.hu/proxima/discord-bot/raw/branch/main/src/.env.example +``` + +#### 4. Customize the `.env.config` file + +```bash +nano volumes/bot/env/.env +``` + +#### 4.1 Fill in the bot application informations + +- Change the `APPLICATION_ID` for the discord bot application id. + + see: [Discord Developer Portal](https://discord.com/developers/applications) + + `` -> `General Information` -> `Application ID` + +- Change the `PUBLIC_KEY` for the discord bot public key. + + see: [Discord Developer Portal](https://discord.com/developers/applications) + + `` -> `General Information` -> `Public Key` + +- Change the `TOKEN` for the discord bot token. + + see: [Discord Developer Portal](https://discord.com/developers/applications) + + `` -> `Bot` -> `Token` + +### 4.2 Fill in the bot home server informations + +- Change the `HOME_SERVER_ID` for dedicated discord server ID. + + see: [Discord](https://discord.com/) + + ***NOTE: This can be used to manage the bot.*** + +- Change the `LOG_CHANNEL_ID` for dedicated channel on the discord server. + + see: [Discord](https://discord.com/) + + ***NOTE: The system messages will be sent here.*** + +### 4.3 Fill in the backend informations + +- Fill in the `API_URL` for backend api url. + + ex.: `http:///api/v1/` + + ***NOTE: If it runs on localhost, teh address is: + `http://backend:9000/api/v1/`*** + +#### 4.3.1 Get the authentication token from the backend + +- Go visit the webpage backend `/admin` page and log in. + +- Go to the your `Profile` page. + + ![First step](res/first_step_1.png) + ![Second step](res/first_step_2.png) + +- Here you can change your `name`, `email`, `password` at any time. +hjfNdG68K2vbOrTn9bXVqDZBpNDER20rt5FRB1Sv2acd4353 +- Click on the `Craete Token` button, and create an access token for the `Bot`. + +- You can name the token anything you want. + +- Make sure you check all the `Ablities` checkboxes. + +- You can set an expire date if you want or leave empty (the token will not expire). + +- Press `create` and copy the new `token` and temporary save it. +(If you lose it, you can always generate a new one, just delete the old now useless one). + +#### 4.3.2 Fill in the authentication token + +- Fill in the `BACKEND_TOKEN` for backend api token created by the **4.3.1** section. + +### 5. Create the [`docker-compose.yaml`](res/docker-compose.yaml) file: + +```yaml +--- +services: + discord-bot: + image: proxima.goliath.hu/proxima/discord-bot:latest + container_name: bot + volumes: + - "./volumes/bot/env/.env:/app/Bot/.env:ro" + - "./volumes/bot/storage:/app/Bot/Storage" + restart: unless-stopped + networks: + - proxima + +networks: + proxima: + external: true + +``` + +***NOTE: the external network is only neded if the backend and bot are on the same host, so they can communicate by the hostname.*** + +***You can create the network with the following command:*** +```bash +docker network create proxima +``` + +### 6 Start up service + +```bash +docker compose up -d +``` diff --git a/build/default/Dockerfile b/build/default/Dockerfile new file mode 100644 index 0000000..0109c12 --- /dev/null +++ b/build/default/Dockerfile @@ -0,0 +1,68 @@ +ARG ALPINE_VERSION=3.21 + +FROM alpine:${ALPINE_VERSION} + +# initialize arguments +ARG PHP_VERSION=82 +ARG USER_ID=1000 +ARG GROUP_ID=1000 +ARG WORKDIR=/app/Bot + +# install dependencies +RUN apk add \ + bash \ + git \ + icu-data-full \ + php${PHP_VERSION} \ + php${PHP_VERSION}-fpm \ + php${PHP_VERSION}-curl \ + php${PHP_VERSION}-intl \ + php${PHP_VERSION}-dom \ + php${PHP_VERSION}-fileinfo \ + php${PHP_VERSION}-iconv \ + php${PHP_VERSION}-mbstring \ + php${PHP_VERSION}-openssl \ + php${PHP_VERSION}-pdo \ + php${PHP_VERSION}-pdo_sqlite \ + php${PHP_VERSION}-phar \ + php${PHP_VERSION}-opcache \ + php${PHP_VERSION}-session \ + php${PHP_VERSION}-simplexml \ + php${PHP_VERSION}-sqlite3 \ + php${PHP_VERSION}-tokenizer \ + php${PHP_VERSION}-xml \ + php${PHP_VERSION}-xmlreader \ + php${PHP_VERSION}-xmlwriter \ + php${PHP_VERSION}-zip \ + && ln -s /usr/bin/php${PHP_VERSION} /usr/bin/php + + +# add user and group for the app +RUN addgroup -g ${GROUP_ID} bot \ + && adduser -u ${USER_ID} -G bot -D bot \ + && mkdir -p ${WORKDIR}/Storage/Logs \ + && chown -R ${USER_ID}:${GROUP_ID} ${WORKDIR} + +# install composer +COPY --from=composer/composer:latest-bin /composer /usr/bin/composer + +# set the workdir +WORKDIR ${WORKDIR} + +# copy the application +COPY --chown=${USER_ID}:${GROUP_ID} src/ . + +# change to the user +USER ${USER_ID}:${GROUP_ID} + +# initialize composer +RUN composer install \ + --no-interaction \ + --no-dev \ + --optimize-autoloader + +# define the storage volume +VOLUME [ "/app/Bot/Storage" ] + +# setting entrypoint +ENTRYPOINT [ "php", "Bot.php" ] diff --git a/build/default/build.sh b/build/default/build.sh new file mode 100755 index 0000000..21efc54 --- /dev/null +++ b/build/default/build.sh @@ -0,0 +1,57 @@ +#!/bin/bash + +# --------------------------------------------------------------------------- +# setting constants +PACKAGE_REPOSITORY=proxima.goliath.hu/proxima/discord-bot +BUILD_TAG=default +UID=$(id -u) +GID=$(id -g) + +# --------------------------------------------------------------------------- + +clear + +echo This is a sample build script, if the workflow cannot be used. +echo Please customize the repository address/name for your own. +echo + +# --------------------------------------------------------------------------- +echo Building "${BUILD_TAG}" package .. +echo + +# --------------------------------------------------------------------------- +echo Changing to project root directory... + +pushd ../.. > /dev/null + +# --------------------------------------------------------------------------- +echo Determining tag name... + +branch=$(git branch --show-current) +tag=temp + +[[ $branch == dev ]] && tag=testing +[[ $branch == main ]] && tag=latest +[[ $branch == master ]] && tag=latest + +# --------------------------------------------------------------------------- +echo Building image... + + +docker buildx build . \ + --tag ${PACKAGE_REPOSITORY}:$tag \ + --build-arg GROUP_ID=${GID} \ + --build-arg USER_ID=${UID} \ + --file build/${BUILD_TAG}/Dockerfile \ + --platform linux/amd64,linux/arm64 \ + --provenance=false \ + --sbom=false \ +# --push \ + +# --------------------------------------------------------------------------- +echo Changing back to build directory... + +popd > /dev/null + +# --------------------------------------------------------------------------- +echo Done. diff --git a/res/docker-compose.yaml b/res/docker-compose.yaml new file mode 100644 index 0000000..086e447 --- /dev/null +++ b/res/docker-compose.yaml @@ -0,0 +1,15 @@ +--- +services: + discord-bot: + image: proxima.goliath.hu/proxima/discord-bot:latest + container_name: bot + volumes: + - "./volumes/bot/env/.env:/app/Bot/.env:ro" + - "./volumes/bot/storage:/app/Bot/Storage" + restart: unless-stopped + networks: + - proxima + +networks: + proxima: + external: true diff --git a/res/first_step_1.png b/res/first_step_1.png new file mode 100644 index 0000000000000000000000000000000000000000..14885cb52e1c665401fb57744383be9257513c12 GIT binary patch literal 7373 zcmeHs(hQAAOE*YL+#(HvfON+UEu{=dGr)*6 zLn;jKxSqe^UF-Sqe7NGQ>v!$7`|Q0>{0oRW`GZFf@bK`+H8oWA@$m4=aqlhniE#If zX?{W6gTzb2%m)vTV(8z6pCv%SjEBb(rm3oA7-)5{669-v4p}`p>Tbdl$0NdPpu*D( z|NA7P#9qIXZ?u8i)2~Z5U1agbH%(j%MrIMc}^78Uz6cjspiE$&eLv~NAtE)kwUAjLWW}1#Assbe> zC1+&jgK&6eXb+jQGQsFNF8YU(`OlfZ)0ggi4(S|BHj^Tm*p3gxJYWoQu&8?C}=F+9pnPo2)R zBLlhkInA`$!8RB_RBJH{O|k{0=bnsJ{bw`K=(x?!4Fsaa5EH**0P*(T#%sOFPsMvm zj7>%3<3wmak7>SRBWQv-HE@iFE!C3mKDKR3#AJ1<4>HKFi zc*3^!V$%^`p`~SzH9ruTnWbfYMfs=7H{tc;3`IF-hR3mw1n?`5av4D9F$uFHx&v1o475yvuo;mBXzZ1`I|G({F-4lXsFzZSR5%KvOBRi zaO#aRs^_k)>vYfaIP=!FW#z(bb(RsY(K8#;m}){PKY7Hqs_yjQi=72*$!XzdDEnII z)?Xxy>9s+-v0#?>0 zCN63Vi^btKF0W085V{}m+I+|9FAi2d1Pon+eK>Pns_DIc`+6#|GUH;3h={1l+EjMe zrV&W$Tnm}&z~BkBvzPbuDA068=#oAm>HXOsZ4IN#gVsK2>DiX4PQ4=yxa++=Lu=(` zbBXv!gfg>-pnmKJZ3nPulTZK1S^+FUM7W{`z4RCgEuk(&7LwaMf53j#vgYK4zf0#n z@Aw?8PUQcxy`aBJanGvX{3&Ehp@^kfLD{PX?!Ge4r)OI*n~?P|9o~%n2{L|Gmcb^Y z>vTAwY`<|Ip*tO1o%jvb<3E73K{mjASS|HJbBHg=>=p5f1G8E8>cW!%244$Q0%W~4>?He>$W zYz&y<4{YEig&Lp!l5b2LMzHByO#UXXF?(g8HKIGa=2wtI(L9r5oG7Jh5RXg}iG)8Q-*^h>#HGGn&wHnTclk$;wkX`!CzWqh_ zlV}PnK?K!qbAyXL8XIgItZ)V&1MAPM+ESR(l^2@#77dYub-~sw}V}Qs!Nzm%nASdmElb*}o;+ndJ7gVE?pQ ze*5{^{B>zmh~ClG$%+u!kg?D9jvB4`voHDDEgt{ztRRXCWdpYsxrPLgu&4-RdIx1- zYUn|B)^{CjF~3-ivLvZlnw*mP6njs5?uJy+T0<;q&LXU-r0l(@vYmawtkaVLZL?mn zz8RC0Os%ipZSn*8eu++Z2A51$41bGUKb=)mo;C!yqfd?ehRRzrB;5_#|GRMa43EhHe`RA^4Lp8F|)ERsH?up&g7?i=)s5C!hKR7=f zv0dwC(Z4;8?AKtgZ|NH7M}?G<5xV9yO1#Zjkxlg;1H@rwBWPUetaMaPG+$*_EZ%LQ z2*vqli8wU(MjZsJgn8Y_?Q`zw7C>Y3!Ql6QcNm0h;dUP`R{lDF3rTvkT+Cr*qGNX! zur$4N0~871dD=IvJzTle69?d?UvXknA0i%5Q#0kN$g7$F<>aBGjf4N zyba84hkC06F*Kw#l?By&T?bIVB)r20^=oWzOB()wuB#an2oa@j@GOBVcBy^)kDmk< zyKt5>{ooBbn|$Y;NWi7_w`LQ!o$^b|!KH%Gl%6k9v21I4!`D-GR|D!Ey@mCm+egpu zu&I};-W#-5#!I@kdh4DDvnk3wlQ4eHr$0vL=>l51g)R~vP?x~%bLeV;)sJ#sSiXGj zETL7~Mdm8nx0o-Y^5nxSyoEA{q1w=&!!4_Ug9S3+0$IF1t~{M&DeGL{dXod_R zgQ`r{04;-)36-E}Xj|+}I1V&Hikzbi8onJPPPLP9OZ*ef>sC0$Vh{%VVHakKgyPciZgHGblTZ zzq%AQ$y3*HNUzZNB{%qR3BHT5Kh|mDX-~EY2_cYP^?iu647&xRHoS7Rr98Ia1jOH% z!i47B>0(K#bktLu8(gl%gqOPFS*PSaD9VpD3bt5uP(#Vw52A}R9~JlOUig#*g zhkflzIn8Nu?Jd8&FMCla*GfIWSn$3-LC|imd5@nNmk#TMA;lW&f~Lb+KcJNKdA(vQ zmS@lBrw!eGeBHPRY_cT0ud`h`E_EEiQ&eHQtv7*A#6{g{HLHK6vi(^3$bh4>PN?lWY2@>Cmu4ih31VZ58M0w%Eqzzwo1qH4942}Bh}?#Zkm38wy+WHcOl;ux zm8J}Ya+7GV{`RA=!cZE2MYXriH8kr5(p3HW7nmo#y+))qGvwlh}N$>S}{b}s=!R;kx z^|Cua-&TAP%{EnQqVum<-2d4fJIO4vQGS4gHGWF`_|YxZ(ZK;s>gswTmuj$5VfOun z-rYKtdOmM!LC&FqW<~q^WKUD|nFbFceX6Qg6Zv%9XRM1;;rMeQNzf{Hw;sWWkkUlX zhh?^tM8>g8fp+TRDx-v48!p)rhH1Zj_^Vy)ZcBh8pU08~erRIiw=H0!&o@Qb%4Atf zTy|eceJ5+|Y|0!xZ;I>srb9U;-xk%(-Vwdz`Pmj-^yy57n!YZSz{EYt>sO8}SRwgG zgFS|#$n6f~`4&~Se>>yx#~an30r>u--6>}wS-a#&BC4J045EMt=pk7Jd!8;jbWP8w z2Oci)BUZ=0$~NRcms*-W@~isPbu}nEd>6FPn8`&I1X5O@qHm)RVdOS{tR2JtHg}yL z9PuD!_Vbg&Oo(3_?5}hZBhTA5 z5X?ba8?l&*VLCF1qH+1%eSpm3jN0Wc+am@cJ6lU3fow_Y2eFz?vt4gCAB(U%JMt+^ zlxxp7*&evu%Z6`oO#24+G?@z733Vmhu?eB9ozH3i41y8=3 zx9Uhfk8ta}BAx2dQO_rRk(n@2t1}D!w6YO66O)!1YU3$7TRVjyHs~4 zZDI6iK)Yb)s-L*+s9`^LlT=6|-V1a!t1am2GQrN)?0;T;^ARbLhc1e1ZVTNA>UC$9 zcHeyM>WV$CU2F@*=~V=EobE```}T3zD^+`|&homH)u`AJQ+kTDhQ^$fGW_92+(J0% zn{0b;YKXYr<4k~wt~fUf#blwb1@B&8|6m|ux~)7AWpip!_-_VL#evQCms znGlQ#i3uWsqIA*2!tWu4A_6?PCi}t`NdOGT7L-^qoa$C zr)5A>>cxM<_4Zd9egaS=72N|u&P|v{RA2qr_@V&~JWv=R-d2>DW5No!Cxy$( zkJX=-uTVYBOr}8b^X1|u9+lN~JoJhD1*)o!5r8hHtD8f4LG7eoCR=mR#I8-+IAUVh z{X;!+-X^KP#ckH((G*5>^=jNdeba8>qiB_{%k06{hF4sP*G#RUCKNY3z$;|sHEXcr z-qndo$L!Mx1A)=zAGA)QS3~RKQddlOYq8J5WoxQI#o5fnMafhdX(cIDA+#YTo-L}*3}^-jGkhyy0{mo21rZDgRYy}~~#t$bHf-Fhpy zKFP(8_2&C_x*T19{c6A#+tZTGJFX>a8rV)DJB6v#tcXjgdPhWn-}BO(0AXWJ&xDjm z-%MLbZ`;sNr(>2fMvvkBpe3Hlv^SCXJw}#;B2VlB{cXO&IF}~YHT)SdKKzNN3!&d zDDi3T@;CA#bjC{3k3mMtamJQ5w%c?&nL#zv>}Yfl2tvf0Im#xh7pX~?88<>!eJrOS zU9f(!LHH4tJulZ7{PXrG-vxs9sCJSQY2}LQ_p=~dTkxlTH+%OCCJHsshW_`4T<7Q3C8SHXmzV`>sToJS}>bq~&6ljOf}}m%O(s&Q>s3hx@%{ z2gchdW6z=H@rbc9N{onr5eeTCp2|C307Gz@QA|^SB~~^jraj311`%Vsv=dB9e>_+X zNHV1lB!V_FQzs!8(t6zj6f(j7!umKCX)AwXu2N5IJy;>4t0A_QI5!saj0x2-Dw!$cnyj04gHtL zU=}%A82Crdc_+l0a-}@&CUdjojWGSVBzM7*8M;H5(W`s8j_g5cBtOnD2-7{#&5v~| zX&tW|WPkW%Ky~$%rUQUUR`ijK)uF|#FEcY?Bzz|0<|%F$fP6-sI)!w1IeGmmSjsL= zZx9SMG=g_GH!}KBDXkBt3^IsQG(RL9 zt=j}*>%8&*CMJv!WhA1RSa^LP9EI&>zuzu39iEri(4;3O0G9VV-hxqI93wfL$pP0c z+xOVwpFd3_EwwO+LT&DNjG?#vJzsE43Ia0YEF@Y2+3ti;UMqy3*ScFK@2YFpE6N<& z{4ZK)oUkA!r>}P2mc+w;Q&#nl#kFwGfU)}Y^ahzU_?NdWct6eordONPIWEB1*fdF2 zgxYLheSP1dUf27%r`pG7`SLg$6C19n&N5~h8)Ue+;hIb8Nz;*gn-?Lr6&z$Dyozw$Hc!=la<9n!H`Up@re26-&T!@ z0d&pzUF#!kuL+gBLA0ASq)&o?H1Nj8O9}54H$Pv;T}U!{y$DXe3DPCG)cR6!w+|c| z9ry5itjzkSw8uqTqG3&~!Y0tDJ6uTGL(DzMtjFxHpiVcCPTTR#&IZQ2imrlp4i3!B zVv9Qf-prTKD9V4VL8`@KL_iemGb^P4j=o=j#`fb;P|73bxBt}S z^KG(jT(Mkyi06Jr>?dk$uo}0}hA2^Hr&;kM=A8!zS-e0Wh>!QXXwOhmM2QM=BG61G_sNgcRNX1E0Bp6hF^Q08 z0}y;^J0$?7!iyyv9U}{Orwg1_K-RKq7fEbb9($GL`&bOeNFl7 ztp3>eIEIogZw_a1AH%*Dn3!|l3nQaw_dTT94)N{d#0eOE+NgbHZS6!cOyq`aTgp1vQVJ) z;wP9M@T3Rv6ha5h4Bv4c;kJdsOv$XPs~5s4MN%sOJfD) g#a916-S&UDgXI1K2& z_p2l(3}QGq3OE@_aWxNv<7IarJ&UZ<^U<`@_cB!T@wPI_G`da+TPJfdo79Q{3g3g_ z_Nh@(6+tq8oKErGC~sB}UZdW;CQ!-zV*pD20T*!frihN!@zg273{SWHCQV>h_$D^4 zH=SypOomE(`^K;RDq_bon~RDm{W7a4bJsF$p9>d+ObI14uEj^D48kIaf0KX(3ghT8 zW(i9kO_5Y_!ZxTPNSQ=QH@E!|gk?e@`wkg#|LEvvVRP`z3_dtgMuGT6gW*ivaqNDHwFk?I8IGOsmmTy)?k;U~qYQ%EL_0H9}U-TEiJ7T>Ku6L>@+lV<0~t6EWjS#nI0b> zQ`7W`rlNKKtZ0Yt6f>$I;_ejtsOyasVyshU@}bn|!mHgSW+Jr+3nXt2NXw($0DS&y zuSMKj3TUEL7Ba*R8Z9}j_8u`%Mo1XkhmN^|s{y4Da=D1O*jh#rhR8@+?1t%06eQ*s z)N<{J6cq=eC=kc_CjyP6WlK`+w8rMAdGdGd3)P0Crt(o~0mW`AYP!42hn|k~vPY$| zhK1b=LbMzNvW9X}kn03L&K-32cK|eeYM}|k;%2rqXvv+)R};q ziW5~NS`@9CUI$QQH#HfbVsUJAZMg_)wKRPT>7{M(Y3tTO{|E*XXEfUGM#!-lZ%@!n z&9n>E(S*fFnhzWPUW#I%ZlM@k`CGDKN+Pm6f}pEvDQCQ|&g4Mm6{63sD(zUv zx)nb-`^)Uw1=~iSdpgnC;qbwR$*u>E#FOu0G-tF=VC}AN?RKzp?FLD&yyNgtFXbx| zh)%A(EL>Hp;e6KpTKO1>n;d*{PgKU3y*}>F^!{E5+TZrYKWi~%&&J=J%0Br<#D7k@ zbEE9>>F7!L{>m-=&mo-6P{%d#QvCqmnOUo2x&m>IIoOmN6{*e4pf0;igN4F5g1DeP zIsQr4!~4=PJ;Z^LxN?nML-}QAe>FZku8SwuPcJtN^e#Ar6#ccv6s zmci|0zEAy-?W>y`@24J`8QNWl=U;=4^qy?N7?K5r0z)cMp2%Q14V)_5uYDzU`FEGt zCN<8GitQMo*#y1%j0$@@i0B@rZ5JbJ^ENA_^OoD6!KRzuGo>hdDFPC(qdR0FCKel! zVqm6OfI|N`?1#(xalGz^O}MiF14k%3k%YD5p6JBE^>Qv!YQL`IR!N|?f0U=V#Jz^X zp4L7kMe5vm>ZjA-si%n{J^%7$<@(6|f1>%+BL!;FxtgGJU;obaXOvM9BiJbOzL{ z4~YwOs>fHx#pUFf_PHRKf~nl2Xf-PEi8dnAj-%wNs;W3ydeHb1RfnunlPf8`U_y&J zTn&Bq_a$GeF1K?RxYKL|Gvxk0P*kJ*`weg^`5qB zo|rF&aZ)>r+limYRZ;4uRhPMH*V4wLIR&lCEAqU)nU3Cb%}r>vaYv`e@?Pv@Cyybm zO%~DH*E;Ur+f0ytNd$9M= zK~*)$boQ{KA<6ihToV;NBPx^SSJM)&#Ynd&(=^y9s2Ll*x_eJuPqH(dRfMw8W;Qfx zD!!wJO@c_OFpWzppy+v+*7-Tq9-ql+_eb2;pS|Lu+cnAXvf*u1}! zq1|fE1F^cuI`s+t`IA~l2hp84CaqwV6b>F{*1)fiYvSE%G3`x+Wft`j2`i z7Vt$)f9gh;G3iSKO|`?cQ%$kSYD0z3uhvxjRUcLZ!32JEho3c;>aQkWZy(LKq`5cd z$-YWFCStr;s5m=vw`&IRL-CH>ARn;UYIpp zRNjJFJrB1Ig9Z_TzRY@}DOTTKEZ%bDXHJ^4*B^3kqd$FNEiK5@&$v zmbe|%^iw=t{OqoQ;auyaotA*KtdPkls%Wf@JZYIZY|FhvA+Od?7+S|`WZ zA6EABXIVBpx$ifsJ-S@D7W$IR?(!*WJsmkqdZl_*ub*K94cUer-uTUFyo_$-xy8Gt zYKH-e3zHjrXq}CC(Vs&3HI{>4FJPp^H~7)2Zt@&>;h-oY+H*nz*?ghL{9?WAdhSJ}zwYoWQ&l zaI6aivyD<6R!pp8=6!;3rE(uVQ+?sB+M3{zYd(OuF6Ht$JH42%Jg>p2@8% z4t$ZAe@8SBV{$_s$IF$0fT~%|YKKHlV%v2n2i=!38m8kKDSsZf;kQ}#*hEHWvb0mF zF^@~nln$JYWMg<&F+GwdYKY}eUk~?-hI(T%y=1TTs`n_jp{Ahd)i)W<6tT8j5W?#> zzc#}y!~625Kj=f10^*LQo#FYf zT$D96K%_>80}$J!l!y(JhmX>2zC5* zf8Nfa8W`;{Cw?aBIm=lWehh#Ux*??>loxh1~to8pFK6wNmtBKHk4 zmXzA}I5dqs&~&L0eic3d{0Mp$=PT;G@R_hxkMH97zc#h`ULklAb9H;LGi_1&%Dp=}1I}apMo6G0KSfx+ROmlHjW$n!H zD&nHs@v|vu%GB%hZbV)voc z(`H;x3SD4cZ)>t}VJ`OE79S?fhkh>?Wv#4y^!0Xm$qn#FR2uH%S8`;S^*?aF z*?}r0%UZ^!8_>Fi{NPwor)zoR_u9Br)QJ2Bce!pH&iAkydb(AULDQ=BEb2ZuZkvH4}#J%^Xrb*40S`Dz9iJQ zcY_>o(OX}0)=$3rF4k9Hc9N~vpY=$azhsAkdGDOF5<7L5PHPLS**2tm=$bkGc`LIq zn;YtQmO6|Tax`eC$sg0{M9ZU{`odrNA;L}nSrHRM)hxGwZTk-{yrDC<5cwKxe>U`x zhuKpwED+{EFYJ>lFt0tc@hkXpS37dVioMF0SI7K5>hgI3pH2Hue>^t$JWM;Y^T$%A z03bE!xG7!vGyqcbhWq>1*8<>YHpKUj4kB7+ahX_CxM(w)V){ofkx*lu@|%Ln)85Z8 z0{BVUq4gd^CniR#edp{!k80~I&Xh#dX-J1T2cwM^jg9gi34DEa2E2qwNDvk%GXrA99u{dj+IM&!wRfjGS(iG* z1OqTALZJ8&hn5vE!p71O22z005kAY@G4DqE$~zP~g~_lRFH_#2S*B8p!=s}p&kt`Y zgZ4%b&&TH9!)Az-9r=%_p?!TWTBCNDB1NWet~DYVQ+3jH!(wCos5JfoK^*d&k8;jO zY{QL) zM@MQ}>NO{HzfRheK^YSeNS;6tjrt(G;Gk62$$1wWpcF($S=Rv~0+DWH)A(QtHzE?L zdUF_S{+PFo3^TilKd>dH@{lkj+$w!tzn|xS#rZMs(u#ZF7D$04h-a1R|m|@ zXn^YhFid%N=Odq$4)XN83%gG4jTA?~`36uGEG(p<-QrSG|4CJd7JK^xaMcWAMdTI= zU)3k!)E>;MJ@?K>#%P_Uw+f}aFKMMcHR&MrdcRAwg? z8nopgD5IDSvemB$ipQdopLMl9v<4mob_?8!+?G@SYau89GcS+qYdT=KP-G>DjD&

wWfr844$Y9=TMIE=jxHH<4g2M1T`B03j@AL7fZ_pNDf(B|sh6bL|oo)AmJv4Mw2P&$nTGeLC zlJnAc9uF8Am3mezF#{8YgC0-Od}$)y4fV2N9Dv@#L;(xi8cHKo)=<^$i_!Pq+uQqD z(Ei<4$ZdRVjBwn%tF76i?=}WUT|KGoW~CKi&9uq@!vZ9*XKG+qSzm03{0a!EpM)Fc zkGFpL0t>n~3`R(7q&yep4~ zv<80L>Kf&GUH34}vp%u*-%=TkXHVphaGIlvqs`9F^UYvB6qG#T=JuzN)+LATA`hF* zO`G|aXtBrjSZ00vJ0g%CGPXSEYW@7GGj=Fye^RU63E|v;?HuL2T;KQIq>5_AFmR4% z@@t)%^@K0ZOkLdFolo0u&W`QqMG><(q1~Rz|loI{5Xu?YY~>cfNk_%U;08Z;N)V!PnQ< zXIXBmeuuWntlDpXx5%;4(C{QJ+IO<}OiE00dt6mVukU^vBbD%fSX_O5_UPQX8-PT% zRwQ7&57ECON-Zp29`s~zh5o{&!fbmf;FBljZUsKT0r6kbMymQ}RVP;12UoGp% z6MbwqCZsR41lyvgAiVa|W3{-gR`={lgR{1_e*JqB7BU#rgatT&)TBz>;q+e1lL{qm z{cc=2cJ)kA1xH7QdRQJ-YIgd-(h>=$1w^W@th_YF!-sWJrRe;MYg9>35BhN0ISS!> z8JPGgr|sR};y^r%-z-%NL3bbBd7jet3Ipj$W6PTijem;{fNMNNB>f$UAvC%`YSXaD zUJ-3t-n2!1hq_x}MI^4j+dZsXQd${q>0DY9ubPx(W^U%N-xBFKcbp25iQB%1I5%6u z7&kUHnqE2{{;WG}t80cFMn))}ZL|5O1qHoXSX_KXPfwTyE(~fK%O}SrFi9+DSFoFv z(0zo$yYC@Z-93?EKZ9D-99`8RD9U zG72fFMac3e$~C--il?k^Qsv?#H!5&K`?8(;Wg^UZL&>UkT-NI81Ldnp!gTYRq|R&CRip-_SF#(5>CTuer6RaS7GQx0bgB z#=0NHE9eF9nKorK$+CxY$V}Qmn=|!nYaN~4`DWeR)&)i-+fg3p4%he7xb{zuJ~Hqe z^*`NuusPS%{+0e>DyzZ?;go1>FD}imD-WzZ8P`jvgx*qg0r`N!-8qA0ZQx)9CE zl=iOl!`NrJO)vT;tZN;o=7~S%ttdHWX6$5>k_=_%s>N65S4p1bvx` zF@jl{PJ-j!uZ#I85>--LVi4(ZA@AQ(A|n)9g}j!5p>?q!A3T9dR_u1O_b2CpY)U21 zAg=Q8&7&XMBy zVvkHpq@7pD3gX+`?A&ld4OJ0aFE6Ppk}|d8zNv?%zwDOd-0@!7jbr;;+3&fET?D;B zGI>bf-@JZ>M#bIvGZAeSg=0mAJzB!eC`OPAbE*}K63{{olYPQ)FGot`rXrnG7DkMa zK5p}J-L;Lmbr#ZFnA?!U7rG_geUw}W&o5s-mng;V51Hb%INPk62AHqeZ*DZLvKPCw z&ZlNdrYy&BqN6Q;oON{(;r1bzg8B8KQuflJKse;#In^xpSPWqX1ZFj3?2fv`nBtmH z;U5X6tORsRtOqu{xFW_l81IAMABka3=@MWNPm6u?g74-Yeg(g$cfDZaGrbKxgqd*3 z+laK68nK6s*S;QQM^jrROT#kJOE4m$il0ss$Q3TsG7H%W@CvOC>4uAVc%xKIx3yI_ z87{te=lvbT!2UhYm>&#>&W(wSk=3!9k!Q^7wTKo8qH-ZdF6s`j3h|PHc$$mKXaHiW zmS&IUWV6@_01Z}4l&w#M_`b$0ap3>cp1bmOo=#u!^%YuKF#O)p<;T35zw6B_W6B~T z$y}7UZ*$|&kS4jky*{;zd0TdUXLiCeF^M-KqQszRwAizadkd&HgH`}CZ!eUUXZdSU zqHEJ7By>GunRQ{*Nn}iEC*U6O2Mk;n^k)=`hB!XP#eTP}p3Y5mAUCLf{bLmW=+ZZ44<7!={zP z0NE_Xz#uQAcJVy5En%hG7IMUFBtb4csV$Ww<0x|aJwxb)}?cO{$Y7`o?@jZDmWkg&^#oa@}9a)4YibQ<} zg__sSJG3q*u>SCxKXyBE69U3&=PFzGDnZzrNr$VYAlBm02BvxjT_|I7z)rxQJe+sZ zoS4+TA^jw>zCe#9J3f8*CA&{i!YGjSJ#@ToNlkI?3>Fe@3s8|i8}bO+DGG-Mf0iOj zp(rr8Vv%YHj1>-8*&cej5jzksQCE9~mQW9Hlt59g7@?UMcy4w;$RIpSRqdjG-H>C;+mM9QrR*_FwP;NIt_*M|RcaW$Kb6`epIOkU1hWD)eGh zBJ{nlwaaKLA}`ncm#j8FhHvEL)XXRv$aF}~lzj>8F?`bZ6*qOdW92j%O;dlg88*PeZOJXr!3 zmDxk3$Xvbd+Mxvg1 zYydB<2D5i^1bA8*3PiOQXe6STe#+f;#E)l5sak-#!m8DoQZD<;D%bMy2LF5&a5fL^ zJWa~*hS=nq($>^5z=wVjZ|L>+QAbr63zv}taFw#r0Bz^HmdBZRuF7WV&G82c7;_{g zzB%aH1zeIUZ3f_S!o_Cqs`{{v2~PZNmMD5AMnSGt?lBIW`D)c))&ccbQG@taHuu#V z-QOKHxaYW2dGxEP`+A3PJ-n+n$jNHSc+M>wy4*=aorbi4EFDTe*Z4x=rDU=G=$SgO zws($$OD14v%WGw`P>{uy3YUoyU6#^4J?JerlIif$!u5C@RG4UTk|~gPTA;_f;vI18 z(TS7^RAEqA_Ds;!)itR2)Fc909MAx4m zYLK-|6x+sG_XYNHpCccU6)C#Z7QE2XA`bIQRGFHZIe(VYEmz+hm|x`pg&8Xk`~;LA zlTH6NMVrr2#Ov{mL2Rg_U*7W%;dT^GRoq5{_se3USV1qaC=>tidUF$ND_he^p;M_u z-|LV=X=(W|dN3HgTR6KCMo)YSXp@lAm#y-cXN9%(#fjzx%E+C0ZRvlYO{}|eX%P}C z@7WN>#=iF_A{Y~mIR(4qK|ME1Dobndr4#^H@Q=OupMCep; zeF_E8u=p(UAS`2u)x`94@Z>&p-NC4Fu|k2CyPJK?Ro_dNAQc!lKgWojp3cIt>#tJw zztS9lEdbb&)DOULSSIpu0<;{I{fhG-J^mJfwhWDb^oxw0It4TsXE(YLA3XwX`#%_{ z|KA?|?*ac`3=43!|M#u`>*hv4@aIE+%HMQ#rnKbr2#D1?S6POGgTwg-Ujo~j@6A|L zAYiGHu#}2!D|d(DGvom$!}r4w4nDfj!P$!K`pynKWxAj+bU;|l*f`jLfd@SyVd|Fs z`47(pl>2N*fBDjwjKX0@KmDSZHd&-l&{p29L&Bplc@O*Np$_<Go&h%zXH%+Q02<7oDrHsdMwpmSw3oQ5(@sP!3}tY9THuQB7nW(j>pdK$Wqtki zQz?*~E>pCE0>I`zY-G7t8+Rj;d0)MYArX+((7<24-Qi0Uqc7KM2+~eVYr8!-OiaoFsHDXD{#@Vd-q9Mz zO;h2UC1<#!)y~+R9omzQT{56riCe+%`7%d02HRvKVS59V`5j_au3sC`7L3ZTtY%X| zhOf$(%K6n=a$btu7n)`GXyOreaqPbG^$AZi=+M69&fLHg#?xT4+ml#+S*Tcz%lw7I)5przXRqhmB7(UZSK>|}xUsfhO*kwM z^R50l^?P{`tys^vSsoqrKle`)^%WFyKknUkxi~9`y(l7hCK*=@%lj7&b{*~t!D&3s zwBq`7R8(0}QO=|Nyu5t1-lL-6^KaRnJMTMg*1nsWn_u)ER#lt!VLbYE)@#qX&Kiz3 zcEB(n`(pj&jn|DUo38~=nl>?jVlS!%+qqu>IpPoX$7PYuAA$S@d8&--}wY$qeb5m54E;I&Uf=I=vA3a>}y z`f%KM*}D1^lVa$H@-08qzo({9j=5rO)ZZ^4OUV88S()K;yr!XdR@!^65`J}KD-^@~ zR<_Hd+R=-RNVeBXYHE^-iWu%IuCt!s`?(c}4v3KvzjM#X*X<5S!!7>suZ`+WEdi7*v5`F%?b2zU$+~yXPDK-Jf_7idgm0GJ#flWw9X6njGgY1O2$uwChk`{#InU@Xm;O&eMU0H} z!cM6?Wo$bwhn1U$tH|gyWOM%CN*Gk{dihQ_D-<%c8c(p3Sqv48jg4;w4h|2rI!pxc zikOOp_207+m=ptU^Ourr-u#HDLE(H|>J@g!&9eVyws&W*CZCOw^#v{w5wYhcY<8~6 zv7nv-j_q;(3qWl+ygXt4NKAx66f!)X?{>9amCdTa>??i`wtB35{x3;YKeDqGt+W|l zZew4v+Py)j3Q}5{wAN?4w2=bLz9qqj2L0Uddo~U4r3NvLtgU(<$SqAIF zgFBDD%CL)Zhsj~S%eztEfSjkCm*M@=$*tXO=gsJx&A3qV23|Y>I+7(2-k2<#5|sf- z{mqIuo2LIp!cWLg6@4GkD6$p zmYfPW=;)oR_WyaSL0R9>z`;XQMy|!$b#%};`T0$2@$hMvr-|HH>lVj5EDz- zmHG8qKNi#%2Ip4xZE=F`=ox6a^=GYYY@o+(o6$ELE`rsI_my+1c^@;3mK@0r4i77O zK$0}+4jgC+AK5=jc_S?^i#W@UlMo{O+l5sSzT|D|)74ekA4IgmSEzj{{IQ`63pzl- kC{-?9bY1E;_6vEYSLXaF@)aLYeGDh_Nl~&AWEA*605DExKmY&$ literal 0 HcmV?d00001 diff --git a/res/logo.svg b/res/logo.svg new file mode 100644 index 0000000..df0294a --- /dev/null +++ b/res/logo.svg @@ -0,0 +1,706 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/src/.env.example b/src/.env.example new file mode 100644 index 0000000..f4ba622 --- /dev/null +++ b/src/.env.example @@ -0,0 +1,35 @@ +### discord provided configuration +# see: https://discord.com/developers/applications + +# the application id of the bot +APPLICATION_ID= + +# the authentication token of the bot +TOKEN= + +# the public key of the bot +PUBLIC_KEY= + + +### bot dedicated discord server configuration + +# the server reserved for the bot development/managment/control/etc. +HOME_SERVER_ID= + +# the bot will send all errors/warning/etc. to this channel +LOG_CHANNEL_ID= + + +### backend provided configuration + +# the url of the backend api endpoints ex.: https://backend.example.com/api/v1/ +API_URL= + +# the authentication token provided by the backend +BACKEND_TOKEN= + + +### general configuration + +# The number of seconds after a cached item should expire. Default:30 sec +CACHE_TTL=30 diff --git a/src/.gitignore b/src/.gitignore new file mode 100644 index 0000000..0ee5a0b --- /dev/null +++ b/src/.gitignore @@ -0,0 +1,32 @@ +### PHP-CS-Fixer +.php-cs-fixer.cache +#.php-cs-fixer.php + +### Composer +composer.phar +/vendor/ +#composer.lock + +### JetBrains IDEs +/.idea/ + +### VSCode +/.vscode/ + +### PHPUnit +/.phpunit.cache/ + + +### Misc +#.env +Core/HMR/Cached + + +debug/ + +Storage/smarty/cache +Storage/smarty/templates_c + +Logs/ + +.env diff --git a/src/.php-cs-fixer.php b/src/.php-cs-fixer.php new file mode 100644 index 0000000..7c5383d --- /dev/null +++ b/src/.php-cs-fixer.php @@ -0,0 +1,33 @@ +setRules([ + '@PER-CS' => true, + '@PHP82Migration' => true, + 'new_with_parentheses' => [ + 'anonymous_class' => false, + ], + 'braces_position' => [ + 'anonymous_classes_opening_brace' => 'next_line_unless_newline_at_signature_end', + ], + 'function_declaration' => [ + 'closure_fn_spacing' => 'one', + 'closure_function_spacing' => 'one', + ], + 'single_trait_insert_per_statement' => false, + 'no_blank_lines_after_class_opening' => false, + + ]) + ->setFinder((new PhpCsFixer\Finder()) + ->in(__DIR__) + ->exclude([ + 'Bootstrap', // skip original package files + 'Core', // skip original package files + 'Storage/Smarty', // skip temporary files + ]) + ->notPath([ + 'BotDev.php', // skip original package files + 'Client/ClientMessages.php', // fixer don't understand template, would messing up sapcing + ]) + ) +; \ No newline at end of file diff --git a/src/Bootstrap/Commands.php b/src/Bootstrap/Commands.php new file mode 100644 index 0000000..789ede0 --- /dev/null +++ b/src/Bootstrap/Commands.php @@ -0,0 +1,32 @@ +appendCommand(new QueuedCommand( + $attribute->newInstance(), + new $className() + )); +}); + +$commandQueue->runQueue(registerCommands: Config::AUTO_REGISTER_COMMANDS)->otherwise(static fn (Throwable $e) => error($e->getMessage())); diff --git a/src/Bootstrap/Config.php b/src/Bootstrap/Config.php new file mode 100644 index 0000000..5f0344f --- /dev/null +++ b/src/Bootstrap/Config.php @@ -0,0 +1,7 @@ +remainderService = new ReminderService(); + +Env::get()->bot = DiscordBot::getInstance(); + +Env::get()->discord = new Discord([ + 'token' => Env::get()->TOKEN, + 'intents' => Intents::getAllIntents(), +]); + +require_once BOT_ROOT . '/Bootstrap/Events.php'; + +d()->on('init', static function (Discord $discord) { + debug('Bootstrapping Commands...'); + require_once BOT_ROOT . '/Bootstrap/Commands.php'; +}); diff --git a/src/Bootstrap/Environment.php b/src/Bootstrap/Environment.php new file mode 100644 index 0000000..17aa261 --- /dev/null +++ b/src/Bootstrap/Environment.php @@ -0,0 +1,15 @@ +TOKEN)) { + throw new RuntimeException('No token supplied to environment!'); +} diff --git a/src/Bootstrap/Events.php b/src/Bootstrap/Events.php new file mode 100644 index 0000000..9506a0c --- /dev/null +++ b/src/Bootstrap/Events.php @@ -0,0 +1,44 @@ +newInstance()->name; +}); + +loopClasses(BOT_ROOT . '/Events', static function (string $className) use ($events, $discord) { + if (doesClassHaveAttribute($className, Disabled::class) !== false) { + return; + } + + $event = new $className(); + $reflection = new ReflectionClass($event); + + foreach ($reflection->getInterfaceNames() as $interface) { + $eventName = $events['\\' . $interface] ?? null; + + if ($eventName === null) { + continue; + } + + $discord->on($eventName, $event->handle(...)); + } +}); diff --git a/src/Bootstrap/Requires.php b/src/Bootstrap/Requires.php new file mode 100644 index 0000000..3a6cd15 --- /dev/null +++ b/src/Bootstrap/Requires.php @@ -0,0 +1,7 @@ +run(); // Run the bot diff --git a/src/Bot/Cache.php b/src/Bot/Cache.php new file mode 100644 index 0000000..0f13da6 --- /dev/null +++ b/src/Bot/Cache.php @@ -0,0 +1,176 @@ +discordUsers = new ObjectCache(env()->CACHE_TTL); + $this->remainderLists = new ObjectCache(env()->CACHE_TTL); + } + + // -------------------------------------------------------------------------------------------------------------- + /** + * Retrives the DiscordUser object from the cache + * + * If the DiscordUser object is in the cache, returns it, + * otherwise fetches it from the backand beforhand, and then returns it + * + * @param DiscordUser $discordUser + * + * @return PromiseInterface + * @promise-fulfilled fn (DiscordUser $discordUser): void + * @promise-rejected fn (mixed $reason): void + * + */ + public function getDiscordUser(DiscordUser $discordUser): PromiseInterface + { + $deferred = new Deferred(); + + $result = $this->discordUsers->get($discordUser->snowflake); + + // if it is already in cache, return it + if (null !== $result) { + $deferred->resolve($result); + return $deferred->promise(); + } + + //not in cache, request it from the backend and cache it + ApiClient::getInstance()->identifyDiscordUser($discordUser)->then( + onFulfilled: function (Response $response) use ($deferred): void { + $apiResponse = DiscordUserResponse::make($response); + + $this->storeDiscordUser($apiResponse->discordUser); + + $deferred->resolve($apiResponse->discordUser); + }, + onRejected: fn ($error) => $deferred->reject($error) + ); + + return $deferred->promise(); + } + + // -------------------------------------------------------------------------------------------------------------- + /** + * Stores the DiscordUser obect in the cache + * + * @param DiscordUser $discordUser + * + * @return DiscordUser The stored DiscordUser object + * + */ + public function storeDiscordUser(DiscordUser $discordUser): DiscordUser + { + return $this->discordUsers->store($discordUser->snowflake, $discordUser); + } + + // -------------------------------------------------------------------------------------------------------------- + /** + * Removes the DiscordUser object from the cache + * + * @param DiscordUser $discordUser + * + * @return void + * + */ + public function forgetDiscordUser(DiscordUser $discordUser): void + { + $this->discordUsers->forget($discordUser->snowflake); + } + + // -------------------------------------------------------------------------------------------------------------- + /** + * Retrives the Remainder[] array from the cache + * + * @param DiscordUser $discordUser + * + * @return PromiseInterface + * @promise-fulfilled fn (array $remainders): void + * @promise-rejected fn (mixed $reason): void + * + */ + public function getRemainderList(DiscordUser $discordUser): PromiseInterface + { + $deferred = new Deferred(); + + $result = $this->remainderLists->get($discordUser->snowflake); + + // if it is already in cache, return it + if (null !== $result) { + $deferred->resolve($result); + return $deferred->promise(); + + } + //not in cache, request it from the backend and cache it + ApiClient::getInstance()->getRemainders($discordUser)->then( + onFulfilled: function (Response $response) use ($deferred, $discordUser): void { + $apiResponse = RemainderListResponse::make($response); + + $this->storeRemainderList($discordUser, $apiResponse->remainderList); + + $deferred->resolve($apiResponse->remainderList); + }, + onRejected: fn ($error) => $deferred->reject($error) + ); + + return $deferred->promise(); + } + + // -------------------------------------------------------------------------------------------------------------- + /** + * Stores the Remaindre[] array in the cache + * + * @param DiscordUser $discordUser + * @param array $remainderList + * + * @return array The stored Reaminder[] list + * + */ + public function storeRemainderList(DiscordUser $discordUser, array $remainderList): array + { + return $this->remainderLists->store($discordUser->snowflake, $remainderList); + } + + // -------------------------------------------------------------------------------------------------------------- + /** + * Removes the Remainder[] array from the cache + * + * @param DiscordUser $discordUser + * + * @return void + * + */ + public function forgetRemainderList(DiscordUser $discordUser): void + { + $this->remainderLists->forget($discordUser->snowflake); + } + +} diff --git a/src/Bot/CacheItem.php b/src/Bot/CacheItem.php new file mode 100644 index 0000000..5088ce7 --- /dev/null +++ b/src/Bot/CacheItem.php @@ -0,0 +1,79 @@ +created = time(); + $this->expires = $this->created + $this->ttl; + } + + /** + * Resets the time of expiration to current time + ttl + * + * @return void + * + */ + public function refresh(): void + { + $this->expires = time() + $this->ttl; + } + + /** + * Checks if the data is expired + * + * @return bool true, if the data is expired, false otherwise + * + */ + public function isExpired(): bool + { + return time() > $this->expires; + } + + /** + * Returns the remaining ttl of the data + * + * @return int the seconds until the data expires + * + */ + public function ttl(): int + { + return $this->expires - time(); + } + + /** + * Returns the stored data + * + * @return mixed + * + */ + public function getData(): mixed + { + return $this->data; + } +} diff --git a/src/Bot/DevLogger.php b/src/Bot/DevLogger.php new file mode 100644 index 0000000..97bcd6a --- /dev/null +++ b/src/Bot/DevLogger.php @@ -0,0 +1,102 @@ +logger = new Logger('dev'); + + $handler = new StreamHandler(BOT_ROOT . '/Storage/Logs/dev.log'); + $handler->setFormatter(new JsonFormatter()); + + $this->logger->pushHandler($handler); + $this->logger->pushProcessor(new GitProcessor()); + $this->logger->pushProcessor(new IntrospectionProcessor(skipClassesPartials: [ + __CLASS__, + 'Bot\\DiscordBot', + 'React\\Promise\\RejectedPromise', + 'React\\Promise\\Promise', + 'React\\Promise\\Deferred', + ])); + + } + + // -------------------------------------------------------------------------------------------------------------- + /** + * Makes a log entry + * + * @param string $level The lavel the log should be marked + * @param string|\Stringable $message The message to be logged + * @param array $context All additional data to be logged + * + * @return void + * + */ + protected function log(string $level, string|\Stringable $message, array $context = []): void + { + $this->logger->log($level, $message, $context); + } + + + // -------------------------------------------------------------------------------------------------------------- + /** + * Handles all the level variations and logs them + * + * For a list of available levels, see the docblock on top of the class + * + * @static + * @param mixed $method + * @param mixed $args + * + * @throws \BadMethodCallException + * + * @return void + * + */ + public static function __callStatic($method, $args): void + { + // get the list of available levels + $logFunctions = array_map('strtolower', Level::NAMES); + + // log if method is valid + if (in_array($method, $logFunctions)) { + static::getInstance()->log($method, ...$args); + return; + } + + // inform the developer of a bad level call + $message = "Bad programmer detected, a non existing function got called: \"{$method}\". Please correct the code."; + static::getInstance()->log('critical', $message); + + throw new \BadMethodCallException($message, 404); + } + +} diff --git a/src/Bot/DiscordBot.php b/src/Bot/DiscordBot.php new file mode 100644 index 0000000..3e2bdee --- /dev/null +++ b/src/Bot/DiscordBot.php @@ -0,0 +1,224 @@ +respondWithMessage(messageWithContent(Template::ansi($template, $variables))); + } catch (Exception $exception) { + + // log the error + DevLogger::error( + message: 'respondToInteraction failed', + context: [ + 'exception' => $exception, + ], + ); + self::failInteraction($interaction); + + } + } + + // -------------------------------------------------------------------------------------------------------------- + /** + * Sends an error back to the discord client + * + * @param Interaction $interaction + * @param array $variables + * + * @return void + * + */ + public static function failInteraction(Interaction $interaction, array $variables = []): void + { + static::respondToInteraction( + interaction: $interaction, + template: ClientMessages::errorGeneralError, + variables: $variables + ); + } + + // -------------------------------------------------------------------------------------------------------------- + /** + * Fails the interaction and sends an error back to the discord client + * + * @param Interaction $interaction The interaction to resopnd to + * @param Exception $exception The exception that caused the failure + * + */ + public static function failApiRequestWithException(Interaction $interaction, Exception $exception) + { + DevLogger::warning( + message: 'Api request failed', + context: [ + 'exception' => $exception, + ] + ); + + static::respondToInteraction( + interaction: $interaction, + template: ClientMessages::errorGeneralError, + ); + } + + // -------------------------------------------------------------------------------------------------------------- + /** + * Fails the interaction and sends an error back to the discord client + * + * @param Interaction $interaction The interaction to resopnd to + * @param ApiResponse $apiResponse The ApiResponse that caused the failure + * + */ + public static function failApiRequestWithApiResponse(Interaction $interaction, ApiResponse $apiResponse) + { + DevLogger::warning( + message: 'Api request failed', + context: [ + 'response' => $apiResponse->toJsonLogData(), + ] + ); + + static::respondToInteraction( + interaction: $interaction, + template: ClientMessages::errorGeneralError, + ); + } + + + // -------------------------------------------------------------------------------------------------------------- + /** + * Sends a fail message to the discord client. + * + * @param Interaction $interaction The interaction with the discord client. + * @param ApiResponse|Exception $reason The reason the api call failed. + * + * @return mixed This may or may not return anything. + * NOTE: not set the return type to void so it can be used in arrow functions. + * NOTE: this is a wrapper to simulate methode overloading. + * + */ + public static function failApirequest(Interaction $interaction, ApiResponse|Exception $reason) + { + return match (true) { + is_a($reason, Exception::class) => self::failApiRequestWithException($interaction, $reason), + is_a($reason, ApiResponse::class) => self::failApiRequestWithApiResponse($interaction, $reason), + }; + } + + /** + * Returns a standardised dynamic event handler for an PromiseInterface onReject event + * + * @param Interaction $interaction + * + * @return callable + * + */ + public static function onPromiseRejected(Interaction $interaction): callable + { + return fn (ApiResponse|Exception $reason) => self::failApiRequest($interaction, $reason); + } + + + // -------------------------------------------------------------------------------------------------------------- + /** + * Returns a DiscordUsers with it's $remainder property populated + * + * This uses sepatare API calls for the DiscordUser and Remainder[], so they can be used/cached separatly + * + * @param Interaction $interaction + * @param DiscordUser $discordUser + * + * @return PromiseInterface + * @promise-fulfilled fn (DiscordUser $discordUser): void + * @promise-rejected fn (mixed $reason): void + * + */ + public function getDiscordUserRemainders(Interaction $interaction, DiscordUser $discordUser): PromiseInterface + { + + $deferred = new Deferred(); + + // get the DiscordUser + $this->getCache()->getDiscordUser($discordUser)->then( + onFulfilled: function (DiscordUser $discordUser) use ($interaction, $deferred): void { + + // fail and send error message to the discord client if the discorduser does not have a valid timezone + if ($this->failIfTimezoneNotSet($interaction, $discordUser)) { + $deferred->reject(new Exception("DiscordUser has no timezone set.")); + } + + // get the Remainders + $this->getCache()->getRemainderList($discordUser)->then( + onFulfilled: function (array $reaminders) use ($discordUser, $deferred): void { + $discordUser->remainders = $reaminders; + $deferred->resolve($discordUser); + }, + onRejected: DiscordBot::onPromiseRejected($interaction) // getRemainderList + ); + + }, + onRejected: DiscordBot::onPromiseRejected($interaction) // getDiscordUser + ); + + return $deferred->promise(); + + } + + /** + * Returns the preferred dtaetime format. + * + * @return string the datetime format + * + */ + public static function getDateTimeFormat(): string + { + return 'Y-m-d H:i'; + } + +} diff --git a/src/Bot/ObjectCache.php b/src/Bot/ObjectCache.php new file mode 100644 index 0000000..cc6bea9 --- /dev/null +++ b/src/Bot/ObjectCache.php @@ -0,0 +1,89 @@ +value pair data + */ +class ObjectCache +{ + + + /** + * @var array The cached objects + */ + protected array $data = []; + + + /** + * Instantiates a new ObjectCache object + * + * @param int $ttl time to live for an item in the caches + * + */ + public function __construct(protected int $ttl = 30) + { + // + } + + // -------------------------------------------------------------------------------------------------------------- + /** + * Store the provided data + * + * If the key already exists, the data will be replaced + * + * @param mixed $key + * @param mixed $data + * + * @return mixed Returns the stored object + * + */ + public function store(mixed $key, mixed $data): mixed + { + $item = new CacheItem($data, $this->ttl); + $this->data[$key] = $item; + return $item->getData(); + } + + // -------------------------------------------------------------------------------------------------------------- + /** + * Removes the stored data from the cache + * + * @param mixed $key + * + * @return void + * + */ + public function forget(mixed $key): void + { + if (array_key_exists($key, $this->data)) { + unset($this->data[$key]); + } + } + + // -------------------------------------------------------------------------------------------------------------- + /** + * Retrives the stored data from the cache + * + * @param mixed $key The key that was used to store the data + * + * @return mixed The store data if exists, null otherwise + * + */ + public function get(mixed $key): mixed + { + if (array_key_exists($key, $this->data)) { + + $item = $this->data[$key]; + + if ($item->isExpired()) { + $this->forget($key); + return null; + } + + return $item->getData(); + } + return null; + } + +} diff --git a/src/BotDev.php b/src/BotDev.php new file mode 100644 index 0000000..ecd0809 --- /dev/null +++ b/src/BotDev.php @@ -0,0 +1,161 @@ + ['file', $stdout, 'w'], + 2 => ['file', $stderr, 'w'], + ], + $pipes + ); + + return [ + 'files' => [$stdout, $stderr], + 'command' => $command, + 'process' => &$process, + ]; + } + + public function execute(): PromiseInterface + { + if ($this->isRunning()) { + throw new LogicException('Command is already running'); + } + + return new Promise(function ($resolve, $reject) { + $this->process = $this->procExecute($this->command . ' ' . implode(' ', $this->args)); + Loop::addPeriodicTimer( + 1, + function (TimerInterface $timer) use (&$stdout, &$stderr, $reject, $resolve) { + $status = proc_get_status($this->process['process']); + $stdout = file_get_contents($this->process['files'][0]); + $stderr = file_get_contents($this->process['files'][1]); + + if ($status['running']) { + if ($this->stdoutPos < strlen($stdout)) { + echo substr($stdout, $this->stdoutPos); + $this->stdoutPos = strlen($stdout); + } + + if ($this->stderrPos < strlen($stderr)) { + echo substr($stderr, $this->stderrPos); + $this->stderrPos = strlen($stderr); + } + + return; + } + + if ($status['exitcode'] !== 0) { + $reject([$stderr, $this->command, $this->args]); + } else { + $resolve($stdout, $stderr); + } + + $this->process = []; + + Loop::cancelTimer($timer); + } + ); + }); + } + + public function isRunning(): bool + { + return $this->process !== []; + } + + public function getProcess(): array + { + return $this->process; + } + + public function kill(): void + { + if (!$this->isRunning()) { + return; + } + + $this->stdoutPos = $this->stderrPos = 0; + + proc_terminate($this->process['process']); + } +} + +$directory = new HotDirectory(__DIR__); + +$restart = static function () use (&$command) { + $command ??= new Command('php', ['Bot.php']); + $time = date('H:i:s'); + echo "\nRestarting bot ({$time})...\n"; + + $command->kill(); + + await(new Promise(static function ($resolve, $reject) use (&$command) { + Loop::addPeriodicTimer(2, static function (TimerInterface $timer) use (&$command, $resolve) { + if ($command->isRunning()) { + echo "Command is still running\n"; + + return; + } + try { + Loop::cancelTimer($timer); + } catch (Throwable $e) { + } + + $resolve(); + }); + })); + + try { + $command->execute(); + } catch (Throwable $e) { + echo "Error: {$e->getMessage()}\n"; + } +}; + +$restart(); + +$directory->on(HotDirectory::EVENT_FILE_ADDED, $restart(...)); +$directory->on(HotDirectory::EVENT_FILE_CHANGED, $restart(...)); +$directory->on(HotDirectory::EVENT_FILE_REMOVED, $restart(...)); + +try { + Loop::run(); +} catch (Throwable $e) { + Loop::run(); +} diff --git a/src/Client/ApiClient.php b/src/Client/ApiClient.php new file mode 100644 index 0000000..0cb6969 --- /dev/null +++ b/src/Client/ApiClient.php @@ -0,0 +1,240 @@ +token = env()->BACKEND_TOKEN; + $this->baseUrl = env()->API_URL; + + $this->client = (new Browser(null, $this->getDiscord()->getLoop())) + ->withBase($this->baseUrl) + ->withHeader('Accept', 'application/json') + ->withHeader('Content-Type', 'application/json') + ->withHeader('Authorization', "Bearer $this->token"); + } + + // -------------------------------------------------------------------------------------------------------------- + /** + * Fetches DiscordUser identified by snowflake from backend. + * + * @see /docs#discord-user-by-snowflake-managment-GETapi-v1-discord-user-by-snowflake--discord_user_snowflake- + * + * @param string $snowflake + * + * @return PromiseInterface + * @api-response DiscordUserResponse + * + */ + public function getDiscordBySnowflake(string $snowflake): PromiseInterface + { + return $this->client->withRejectErrorResponse(true)->get( + url: "discord-user-by-snowflake/$snowflake" + ); + } + + // -------------------------------------------------------------------------------------------------------------- + /** + * Fetches DiscordUser identified by snowflake from backend. + * + * If the DiscordUserdoes does not exists, it will be created using the given data. + * + * @see /docs#discord-user-by-snowflake-managment-PUTapi-v1-discord-user-by-snowflake--snowflake- + * + * @param DiscordUser $discordUser + * + * @return PromiseInterface + * @api-response DiscordUserResponse + * + */ + public function identifyDiscordUser(DiscordUser $discordUser): PromiseInterface + { + return $this->client->withRejectErrorResponse(true)->put( + url: "discord-user-by-snowflake/$discordUser->snowflake", + body: $discordUser->toJson(true) + ); + } + + // -------------------------------------------------------------------------------------------------------------- + /** + * Creates a new DiscordUser using the given data on the backend. + * + * @see /docs#discord-user-managment-POSTapi-v1-discord-users + * + * @param DiscordUser $discordUser + * + * @return PromiseInterface + * @api-response DiscordUserResponse + * + */ + public function createDiscordUser(DiscordUser $discordUser): PromiseInterface + { + return $this->client->withRejectErrorResponse(true)->post( + url: 'discord-users', + body: json_encode($discordUser) + ); + } + + // -------------------------------------------------------------------------------------------------------------- + /** + * Update the specified DiscordUser on the backend. + * + * @see /docs#discord-user-managment-PUTapi-v1-discord-users--id- + * + * @param DiscordUser $discordUser + * + * @return PromiseInterface + * @api-response DiscordUserResponse + * + */ + public function updateDiscordUser(DiscordUser $discordUser): PromiseInterface + { + return $this->client->withRejectErrorResponse(true)->put( + url: "discord-users/$discordUser->id", + body: json_encode($discordUser) + ); + } + + // -------------------------------------------------------------------------------------------------------------- + /** + * Update the specified Remainder on the backend. + * + * @see /docs#remainder-managment-PUTapi-v1-discord-users--discord_user_id--remainders--id- + * + * @param Remainder $remainder + * @param array $changes + * + * @return PromiseInterface + * @api-response RemainderResponse + * + */ + public function updateRemainder(Remainder $remainder, array $changes): PromiseInterface + { + return $this->client->withRejectErrorResponse(true)->put( + url: "discord-users/$remainder->discord_user_id/remainders/$remainder->id", + body: json_encode($changes) + ); + } + + // -------------------------------------------------------------------------------------------------------------- + /** + * Create a new Remainder on the backend. + * + * @see /docs#remainder-managment-POSTapi-v1-discord-users--discord_user_id--remainders + * + * @param Remainder $remainder + * + * @return PromiseInterface + * @api-response RemainderResponse + * + */ + public function createRemainder(Remainder $remainder): PromiseInterface + { + return $this->client->withRejectErrorResponse(true)->post( + url: "discord-users/$remainder->discord_user_id/remainders", + body: json_encode($remainder) + ); + } + + // -------------------------------------------------------------------------------------------------------------- + /** + * Remove the specified Remainder on the backend. + * + * @see /docs#remainder-managment-DELETEapi-v1-discord-users--discord_user_id--remainders--id- + * + * @param DiscordUser $discordUser + * @param Remainder $remainder + * + * @return PromiseInterface + * @api-response + * + */ + public function deleteRemainder(DiscordUser $discordUser, Remainder $remainder): PromiseInterface + { + return $this->client->withRejectErrorResponse(true)->delete( + url: "discord-users/$remainder->discord_user_id/remainders/$remainder->id", + body: json_encode([ + 'snowflake' => $discordUser->snowflake, + ]) + ); + } + + + // -------------------------------------------------------------------------------------------------------------- + /** + * Fetches all the "actual" reaminders for the given second. + * + * @see /docs#remainder-by-dueat-managment-GETapi-v1-remainder-by-due-at--due_at- + * + * @return PromiseInterface + * @api-response RemainderListResponse + * + */ + public function getActualRemainders(): PromiseInterface + { + return $this->client->withRejectErrorResponse(true)->get( + url: 'remainder-by-due-at/' . time() . '?withDiscordUser' + ); + } + + // -------------------------------------------------------------------------------------------------------------- + /** + * Fetches the Remainders for the DiscordUser from the backend. + * + * @see /docs#remainder-managment-GETapi-v1-discord-users--discord_user_id--remainders + * @endpoint GET api/v1/discord-users/{discord_user_id}/remainders + * + * @param DiscordUser $discordUser + * + * @return PromiseInterface + * @api-response RemainderListResponse + * + */ + public function getRemainders(DiscordUser $discordUser): PromiseInterface + { + return $this->client->withRejectErrorResponse(true)->get( + url: "discord-users/$discordUser->id/remainders" + ); + } + + +} diff --git a/src/Client/ApiResponse.php b/src/Client/ApiResponse.php new file mode 100644 index 0000000..cd8f437 --- /dev/null +++ b/src/Client/ApiResponse.php @@ -0,0 +1,225 @@ + $this->responseCode, + 'response' => $this->responseData, + 'errors' => [ + 'internalErrors' => $this->internalError, + 'responseErrors' => (array_key_exists('errors', $this->responseData)) + ? $this->responseData['errors'] + : [], + ], + + ]; + + return $result; + } + + // -------------------------------------------------------------------------------------------------------------- + public function __construct( + public readonly Response $response + ) { + + // get HTTP response code + $this->responseCode = $response->getStatusCode(); + + // parse response + try { + $this->responseData = json_decode(json: $response->getBody(), associative: true, flags: JSON_THROW_ON_ERROR); + } catch (Exception $exception) { + // store internal error + $this->internalError[] = $error = [ + 'type' => 'json_decode_error', + 'error_code' => $exception->getCode(), + 'error_message' => $exception->getMessage(), + 'response-code' => $this->responseCode, + 'response-data' => $response->getBody(), + ]; + + //NOTE: if needed, more details can be added here + // log the error for the developer(s) + DevLogger::error( + message: "JSON decoding failed", + context: $error, + ); + } + } + + // visual sugar + // -------------------------------------------------------------------------------------------------------------- + /** + * Instantiate a new ApiResponse object + * + * @param Response $response + * + * @return static + * + */ + public static function make(Response $response): static + { + return new static($response); + } + + // -------------------------------------------------------------------------------------------------------------- + /** + * Checks if the API returns 401|Unauthorised response + * + * @return bool + * + */ + public function isUnauthenticated(): bool + { + return $this->responseCode === 401; + } + + // -------------------------------------------------------------------------------------------------------------- + /** + * Checks if the respons has errors + * + * @return bool true if any errors wer found, false otherwise + * + */ + public function hasErrors(): bool + { + if (count($this->internalError) > 0) { + return true; + } + + if (array_key_exists('errors', $this->responseData)) { + return true; + } + + return false; + } + + // -------------------------------------------------------------------------------------------------------------- + /** + * Checks if the response contains the specified error + * + * @param string $error + * + * @return bool true if the specified error was found, false otherwise + * + */ + public function hasError(string $error): bool + { + return array_key_exists('errors', $this->responseData) + && array_key_exists($error, $this->responseData['errors']); + } + + // -------------------------------------------------------------------------------------------------------------- + /** + * Checks if the response has the specified path + * + * @param string $path Comma separated path Ex.: 'data.error.reason' + * + * @return bool true, if the path is present, false otherwise + * + */ + public function hasPath(string $path): bool + { + // get the list of path nodes + $nodes = explode('.', $path); + $current = &$this->responseData; + + // check all the nodes + foreach ($nodes as $node) { + if (!array_key_exists($node, $current)) { + return false; + } + $current = &$current[$node]; + } + + return true; + } + + // -------------------------------------------------------------------------------------------------------------- + /** + * Gets the specified path from the response + * + * @param string $path Comma separated path Ex.: 'data.error.reason' + * + * @return mixed the value at the path in the response + * + * @throws Exception If the specified path don't exists + * + */ + public function getPath(string $path): mixed + { + $nodes = explode('.', $path); + $current = &$this->responseData; + + $pathErrorNode = ''; + foreach ($nodes as $node) { + if (!array_key_exists($node, $current)) { + $pathErrorNode .= ">>$node<<"; + throw new Exception(message: "Path ($pathErrorNode) not found", code: 404); + } + $pathErrorNode .= ".$node"; + $current = &$current[$node]; + } + + return $current; + } + + // -------------------------------------------------------------------------------------------------------------- + /** + * Checks if the respons was a success + * + * It was a success, if the HTTP return code is a 2xx + * + * @return bool true if the response was successful, false otherwise + * + */ + public function success(): bool + { + + if ($this->hasErrors()) { + return false; + } + + if ($this->responseCode >= 200 && $this->responseCode < 300) { + return true; + } + + return false; + } + + // -------------------------------------------------------------------------------------------------------------- + /** + * Checks if the respons was a failure + * + * @return bool true if the response failed, false otherwise + * + */ + public function failed(): bool + { + return !$this->success(); + } + +} diff --git a/src/Client/ClientMessages.php b/src/Client/ClientMessages.php new file mode 100644 index 0000000..b5cc634 --- /dev/null +++ b/src/Client/ClientMessages.php @@ -0,0 +1,347 @@ +timezone}{$darkCyan}"{$reset}, + Your {$darkYellow}local time{$reset} is: {$darkCyan}"{$green}{$localTime}{$darkCyan}"{$reset} + Your {$darkYellow}locale{$reset} is: {$darkCyan}"{$green}{$discordUser->locale|default:'n/a'} - {$localeName|default:'not defined'}{$darkCyan}"{$reset} + + EOL; + + // -------------------------------------------------------------------------------------------------------------- + /** + * Remainder created succesfully + * + * @var string + * @category Info|Success + * @param DiscordUser $discordUser + * @param Remainder $remainder + * + */ + public const successRemainderCreated = <<<'EOL' + + You're new remainder is created. + {$darkYellow}Due at{$darkCyan}:{$reset} "{$green}{$remainder->due_at|carbon:{$discordUser->timezone}}{$reset}" ({$yellow}{$remainder->humanReadable()}{$reset}) + {$darkYellow}Message{$darkCyan}:{$reset} "{$green}{$remainder->message}{$reset}" + + EOL; + + // -------------------------------------------------------------------------------------------------------------- + /** + * Remainder updated succesfully + * + * @var string + * @category Info|Success + * @param DiscordUser $discordUser + * @param Remainder $remainder + * + */ + public const successRemainderUpdated = <<<'EOL' + + You're remainder is updated. + {$darkYellow}Due at{$darkCyan}:{$reset} "{$green}{$remainder->due_at|carbon:{$discordUser->timezone}}{$reset}" ({$yellow}{$remainder->humanReadable()}{$reset}) + {$darkYellow}Message{$darkCyan}:{$reset} "{$green}{$remainder->message}{$reset}" + + EOL; + + // -------------------------------------------------------------------------------------------------------------- + /** + * Shows general details of the error. + * + * @var string + * @category Error + * @param int $code The error code or HTTP response code + * @param string $message The error description + * + */ + public const errorDetaiedError = <<<'EOL' + + {$red}Error{$reset}: Something went wrong... + {$darkYellow} Code{$darkCyan}:{$reset} {$yellow}{$code}{$reset} + {$darkYellow} Message{$darkCyan}:{$reset} {$yellow}{$message}{$reset} + + EOL; + + // -------------------------------------------------------------------------------------------------------------- + /** + * Displays the remainders of the DiscordUser + * + * @deprecated Use listRemaindersCompacted instead + * + * @var string + * @category Info + */ + public const listRemainders = <<<'EOL' + + {foreach $remainders as $remainder} + {$remainder@index|string_format:"%02d"}: Due at: {$darkYellow}{$remainder->due_at|carbon:{$discordUser->timezone}}{$reset} with Message: {$darkYellow}{$remainder->message|truncate:30:"..."}{$reset} + {foreachelse} + No remainders found. + {/foreach} + + EOL; + + // -------------------------------------------------------------------------------------------------------------- + /** + * Displays the remainders of the DiscordUser + * + * @var string + * @category Info + * @param DiscordUser $discordUser + * @param array $remainders + * @param array $paginate + * 'pageSize' int The count of remainders to show on one page. Default: 20 + * 'pageCount' int The count of available pages. (1 based) + * 'page' int The index of the current page. (1 based) + * 'itemCount' int The count of ALL items, (1 based) + * 'first' int The index of the first item (from all items) (1 based) + * 'last' int The index of the last item (from all items) (1 based) + */ + public const listRemaindersCompacted = <<<'EOL' + + {if $paginate['pageCount']>1} + Shown {$blue}{$paginate['first']}{$reset}..{$blue}{$paginate['last']}{$reset} of {$blue}{$paginate['itemCount']}{$reset} remainders, page {$blue}{$paginate['page']}{$reset} of {$blue}{$paginate['pageCount']}{$reset}: + + {/if} + {foreach $remainders as $remainder} + {if $remainder->isOverDue()} + {assign var='dueAtColor' value=$darkRed} + {else} + {assign var='dueAtColor' value=$darkYellow} + {/if} + {($remainder@iteration+$paginate['first']-1)|string_format:"%02d"}: {$dueAtColor}{$remainder->due_at|carbon:{$discordUser->timezone}} {$darkCyan}- "{$green}{$remainder->message|truncate:20:"..."}{$darkCyan}"{$reset} + {foreachelse} + No remainders found. + {/foreach} + + EOL; + + // -------------------------------------------------------------------------------------------------------------- + /** + * Invalid page index provided. + * + * If the user tries to view a non-existing page. + * + * @var string + * @category Error + * @param int $page The index of the requested (non-existing) page. + * @param int $pageCount The number of available pages (1 based) + * + */ + public const errorListPageInvalid = <<<'EOL' + + {$red}Error{$reset}: The page {$yellow}{$page}{$reset} is invalid! + Please chose between {$yellow}1{$reset} and {$yellow}{$pageCount}{$reset}. + + EOL; + + + // -------------------------------------------------------------------------------------------------------------- + /** + * The template to be shown in the autocomplete list for teh remainder option. + * + * Used in Editremainder and RemoveRemainder to list remainders in autocomplete list. + * + * @deprecated Autocomplete list does not allow ansi coloring, use simple templating in code. Ex.: sprintf(...) + * + * @var string + * @category Info + * @param DiscordUser $discordUser + * @param Remainder $remainder + */ + public const editRemainder = <<<'EOL' + + {$index}: Due at: {$darkYellow}{$remainder->due_at|carbon:{$discordUser->timezone}}{$reset} with Message: {$darkYellow}{$remainder->message}{$reset} + + EOL; + + // -------------------------------------------------------------------------------------------------------------- + /** + * General error to be shown to the user in the discord client. + * + * @var string + * @category Error + * + */ + public const errorGeneralError = <<<'EOL' + + {$red}Error{$reset}: Something went wrong on our side, sorry... + {$white}Please try again later.{$reset} + + EOL; + + // -------------------------------------------------------------------------------------------------------------- + /** + * Error reasons while the updating of the DiscordUser profile faild. + * + * @var string + * @category Error + * @param array $errors + * 'timezone' string If present the timezone is not a valid timezone + * 'locale' string If present the locale is not a valid locale + */ + public const errorUpdateProfileError = <<<'EOL' + + {if isset($errors['timezone'])} + {$red}Error{$reset}: The timezone {$darkCyan}"{$red}{$errors['timezone']}{$darkCyan}"{$reset} is not a valid timezone! + Please provide a valid timezone! + {/if} + {if isset($errors['locale'])} + {$red}Error{$reset}: The locale {$darkCyan}"{$red}{$errors['locale']}{$darkCyan}"{$reset} is not a valid locale! + Please provide a valid locale! + {/if} + + EOL; + + // -------------------------------------------------------------------------------------------------------------- + /** + * Profile updated succesfully. + * + * @var string + * @category Info|Success + * @param DiscordUser $discordUser + * @param string $localTime The local time for the user based on the DiscordUser's timezone + * @param array $updated The list of properties updated. + * 'timezone' array If present the DiscordUsers timezone updated succesfully. + * 'od' string The old value for the timezone. + * 'new' string The new value for the timezone. + * 'locale' array If present the DiscordUsers locale updated succesfully. + * 'old' string The old value for the locale. + * 'new' string The new value for the locale. + * 'name' string The display name for the locale. Ex.: "Hungarian (Hungary)" + * + */ + public const successProfileUpdated = <<<'EOL' + + {if isset($updated['timezone'])} + Your {$darkYellow}timezone{$reset} succesfully updated to {$darkCyan}"{$green}{$discordUser->timezone}{$darkCyan}"{$reset}. + Your {$darkYellow}local time{$reset} is: {$darkCyan}"{$green}{$localTime}{$darkCyan}"{$reset} + {/if} + {if isset($updated['locale'])} + Your {$darkYellow}locale{$reset} succesfully updated to {$darkCyan}"{$green}{$discordUser->locale|default:'n/a'} ({$updated['locale']['name']|default:'not defined'}){$darkCyan}"{$reset} + {/if} + + EOL; + + // -------------------------------------------------------------------------------------------------------------- + /** + * The provided emainderAlias (template by self::editRemainder) is invalid. + * + * If the user in the discord client does not chooses a valid item from the autocomplete list. + * + * @var string + * @category Error + * @param string $remainder The remainderAlias given by the user in the discord client. Ex.: "bad remainder index" + */ + public const errorInvalidRemainderAlias = <<<'EOL' + + {$red}Error{$reset}: The remainder {$darkCyan}"{$red}{$remainder}{$darkCyan}"{$reset} is not a valid remainder! + Please chose one from the selection list! + + EOL; + +} diff --git a/src/Client/Models/DiscordUser.php b/src/Client/Models/DiscordUser.php new file mode 100644 index 0000000..f6bb625 --- /dev/null +++ b/src/Client/Models/DiscordUser.php @@ -0,0 +1,88 @@ +remainders = Remainder::collectionFromArray($remainders); + } + } + + // -------------------------------------------------------------------------------------------------------------- + /** + * Returns the local time based on the timezone + * + * @return Carbon + * + */ + public function localTime(): Carbon + { + return Carbon::now($this->timezone ?? 'UTC'); + } + + // -------------------------------------------------------------------------------------------------------------- + /** + * Creates a new instance using the data from teh interaction + * + * NOTE: This uses only the data available in the discord interaction object + * + * @param Interaction $interaction + * + * @return static + * + */ + public static function fromInteraction(Interaction $interaction): static + { + return new static( + id: null, + snowflake: $interaction->user->id, + user_name: $interaction->user->username, + global_name: $interaction->user->global_name, + locale: $interaction->user?->locale, + timezone: null + ); + } + + // -------------------------------------------------------------------------------------------------------------- + /** + * Determines if thetimezone is set + * + * @return bool true if the timezone is set, false otherwise + * + */ + public function hasTimeZone(): bool + { + return null !== $this->timezone; + } + +} diff --git a/src/Client/Models/Remainder.php b/src/Client/Models/Remainder.php new file mode 100644 index 0000000..43a45df --- /dev/null +++ b/src/Client/Models/Remainder.php @@ -0,0 +1,129 @@ +discord_user = DiscordUser::makeFromArray($discord_user); + } + } + + // -------------------------------------------------------------------------------------------------------------- + /** + * Returns a human readable string of the relative difference to the current time. + * + * @return string + * + * NOTE: do not remove this, it is used in the smarty template + */ + public function humanReadable(): string + { + return $this->dueAtAsCarbon()->diffForHumans(); + } + + protected function dueAtAsCarbon(): Carbon + { + return is_a($this->due_at, 'Carbon\Carbon') + ? $this->due_at + : Carbon::createFromTimestamp($this->due_at); + } + + // -------------------------------------------------------------------------------------------------------------- + /** + * Returns a human readable string based on the discorduser's timezone if available, otherwise defaults to UTC. + * + * @param DiscordUser|string $timezone=null + * + * @return string + * + * NOTE: a supplied valid timezone will be used even if the remainder defines their own! + * this is the intended behaviour, so the time can be shown to the viewers own timezone + * + */ + public function dueAt(DiscordUser|string $timezone = null): string + { + $defaulted = false; + + // try to find timezone + $timezone = match (true) { + is_a($timezone, DiscordUser::class) => $timezone->timezone, + is_string($timezone) && isTimeZoneValid($timezone) => $timezone, + $timezone === null && $this->discord_user !== null => $this->discord_user->timezone, + default => false + }; + + // if timezone was not found, set as default to UTC + if (false === $timezone) { + $defaulted = true; + $timezone = 'UTC'; + } + + // make sure result is a Carbon object + $result = $this->dueAtAsCarbon(); + + // apply the timezone + $result->setTimezone($timezone); + + // append UTC for notification in case the timezone may differ from the discorduser's timezone + if ($defaulted) { + $result .= ' (UTC)'; + } + + return $result; + + } + + /** + * Checks if the DueAt time is in the past and the Remainder is not closed + * + * @return bool true if the remainder is not colsed and the due_at is in the past, false otherwise + * + */ + public function isOverDue(): bool + { + return + $this->dueAtAsCarbon() < Carbon::now() + && $this->status !== 'finished' + && $this->status !== 'failed' + ; + } + +} diff --git a/src/Client/Responses/DiscordUserResponse.php b/src/Client/Responses/DiscordUserResponse.php new file mode 100644 index 0000000..9d9cd84 --- /dev/null +++ b/src/Client/Responses/DiscordUserResponse.php @@ -0,0 +1,43 @@ +hasErrors()) { + return; + } // add error handling and/or reporting for this situation + + if (!$this->hasPath('data')) { + return; + } // add error handling and/or reporting for this situation + + $this->discordUser = DiscordUser::makeFromArray($this->getPath('data')); + + } + +} diff --git a/src/Client/Responses/Loadable.php b/src/Client/Responses/Loadable.php new file mode 100644 index 0000000..6cb30c4 --- /dev/null +++ b/src/Client/Responses/Loadable.php @@ -0,0 +1,141 @@ + array_filter(get_object_vars($this), fn ($value) => $value !== null), + false => get_object_vars($this) + }; + } + + // -------------------------------------------------------------------------------------------------------------- + /** + * returns self as a json serialize ready array + * + * @return mixed + * + */ + public function jsonSerialize(): mixed + { + return $this->getProperties(true); + } + + // -------------------------------------------------------------------------------------------------------------- + /** + * Returns self as a json string + * + * @param bool $ignoreNullValues=false if true, the result will ignore null valued properties + * + * @return mixed (string|false) + * + */ + public function toJson(bool $ignoreNullValues = false): mixed + { + return json_encode($this->getProperties($ignoreNullValues)); + } + + // -------------------------------------------------------------------------------------------------------------- + /** + * Instantiates static from provided parameters + * + * NOTE: This must be defined by the descendant class + * + * @param array|null|bool $data + * + * @return self|null + * + */ + public static function makeFromArray(array|null|bool $data): ?self + { + return match ($data) { + false => null, + default => new static(...$data) + }; + } + + // -------------------------------------------------------------------------------------------------------------- + /** + * Created a specific object as $className object + * + * @param string $className + * @param object $object + * + * @return mixed + * + */ + public static function objToClass(string $className, object $object): mixed + { + return unserialize( + str_replace( + 'O:8:"stdClass"', + sprintf('O:%d:"%s"', strlen($className), $className), + serialize($object) + ) + ); + + } + + // -------------------------------------------------------------------------------------------------------------- + /** + * Instantiates a list of static obejcts from the provided json string + * + * @param string $source + * + * @return array + * + */ + public static function collectionFromJson(string $source): array + { + return static::collectionFromArray(json_decode(json: $source, associative: true)); + } + + // -------------------------------------------------------------------------------------------------------------- + /** + * Instantiates a list of static obejcts from the provided array + * + * @param array|null|bool $data + * + * @return array + * + */ + public static function collectionFromArray(array|null|bool $data): array + { + $result = []; + + if ($data === null || $data === false) { + return $result; + } + + foreach ($data as $item) { + $result[] = new static(...$item); + } + + return $result; + } + +} diff --git a/src/Client/Responses/RemainderListResponse.php b/src/Client/Responses/RemainderListResponse.php new file mode 100644 index 0000000..8132a9b --- /dev/null +++ b/src/Client/Responses/RemainderListResponse.php @@ -0,0 +1,59 @@ +hasErrors()) { + return; + } // NOTE: add error handling and/or reporting for this situation + + if (!$this->hasPath('data')) { + return; + } // NOTE: add error handling and/or reporting for this situation + + $this->remainderList = Remainder::collectionFromArray($this->getPath('data')); + + } + + /** + * Searches the Remainder by id + * + * @param int $id The ID to search for + * + * @return Remainder|null returns the Remainder if found, null otherwise + * + */ + public function remainderById(int $id): ?Remainder + { + foreach ($this->remainderList as $remainder) { + if ($remainder->id === $id) { + return $remainder; + } + } + return null; + } + +} diff --git a/src/Client/Responses/RemainderResponse.php b/src/Client/Responses/RemainderResponse.php new file mode 100644 index 0000000..00602b2 --- /dev/null +++ b/src/Client/Responses/RemainderResponse.php @@ -0,0 +1,54 @@ +hasErrors()) { + return; + } // add error handling and/or reporting for this situation + + if (!$this->hasPath('data')) { + return; + } // add error handling and/or reporting for this situation + + $this->remainder = Remainder::makeFromArray($this->getPath('data')); + + if ($this->hasPath('changes')) { + $this->changes = $this->getPath('changes'); + } + } + +} diff --git a/src/Client/Template.php b/src/Client/Template.php new file mode 100644 index 0000000..d339426 --- /dev/null +++ b/src/Client/Template.php @@ -0,0 +1,133 @@ + "\033[0;30m", + 'red' => "\033[1;31m", + 'green' => "\033[1;32m", + 'yellow' => "\033[1;33m", + 'blue' => "\033[1;34m", + 'magenta' => "\033[1;35m", + 'cyan' => "\033[1;36m", + 'white' => "\033[1;37m", + 'gray' => "\033[0;37m", + 'darkRed' => "\033[0;31m", + 'darkGreen' => "\033[0;32m", + 'darkYellow' => "\033[0;33m", + 'darkBlue' => "\033[0;34m", + 'darkMagenta' => "\033[0;35m", + 'darkCyan' => "\033[0;36m", + 'darkWhite' => "\033[0;37m", + 'darkGray' => "\033[1;30m", + 'bgBlack' => "\033[40m", + 'bgRed' => "\033[41m", + 'bgGreen' => "\033[42m", + 'bgYellow' => "\033[43m", + 'bgBlue' => "\033[44m", + 'bgMagenta' => "\033[45m", + 'bgCyan' => "\033[46m", + 'bgWhite' => "\033[47m", + 'bold' => "\033[1m", + 'italics' => "\033[3m", + 'reset' => "\033[0m", + ]; + + + // -------------------------------------------------------------------------------------------------------------- + private function __construct() + { + // initialize template engine + $this->smarty = new Smarty(); + $this->smarty->setTemplateDir(BOT_ROOT . '/Storage/Smarty/templates'); + $this->smarty->setConfigDir(BOT_ROOT . '/Storage/Smarty/config'); + $this->smarty->setCompileDir(BOT_ROOT . '/Storage/Smarty/templates_c'); + $this->smarty->setCacheDir(BOT_ROOT . '/Storage/Smarty/cache'); + + // create color data container + $this->colorData = $this->smarty->createData(); + foreach ($this->colors as $key => $value) { + $this->colorData->assign($key, $value); + } + + // add carbon modifier + $this->smarty->registerPlugin( + type: Smarty::PLUGIN_MODIFIER, + name: 'carbon', + callback: fn (string $date, string $timeZone = null): string + => Carbon::createFromTimestamp($date) + ->setTimezone($timeZone) + ->format(DiscordBot::getDateTimeFormat()) + ); + + } + + // -------------------------------------------------------------------------------------------------------------- + /** + * Compiles and returns the template as an ansi sequence + * + * @param string $stringTemplate + * @param array $variables + * + * @return string + * + */ + public function fetchAnsi(string $stringTemplate, array $variables = []): string + { + // create template + $template = $this->smarty->createTemplate( + template_name: 'string:' . $stringTemplate, + parent: $this->colorData, + ); + + // assign variables to the template + foreach ($variables as $key => $value) { + $template->assign($key, $value); + } + + // compile template + $result = $template->fetch(); + + $result = "```ansi\n{$result}\n```"; + + return $result; + } + + /** + * Static wrapper for the fetchAnsi() function + * + * @param string $template + * @param array $variables + * + * @return string + * + */ + public static function ansi(string $template, array $variables = []): string + { + return static::getInstance()->fetchAnsi($template, $variables); + } + +} diff --git a/src/Client/Traits/AssureTimezoneSet.php b/src/Client/Traits/AssureTimezoneSet.php new file mode 100644 index 0000000..7fc6528 --- /dev/null +++ b/src/Client/Traits/AssureTimezoneSet.php @@ -0,0 +1,66 @@ +hasTimeZone()) { + DiscordBot::respondToInteraction( + interaction: $interaction, + template: ClientMessages::warningTimezoneNotset + ); + return true; + } + + return false; + } + + // -------------------------------------------------------------------------------------------------------------- + /** + * Sned error optionChoises as the autocomplete list if the discordUser has no timezone set. + * + * @param Interaction $interaction Interaction object of the discord client + * @param DiscordUser $discordUser DiscordUser to check for a valid timezone + * + * @return bool true if the error list was sent to the discord client, false if no action was taken + * + */ + public function failAutoCompleteIfTimezoneNotSet(Interaction $interaction, DiscordUser $discordUser): bool + { + $result = []; + if (!$discordUser->hasTimeZone()) { + $result[] = optionChoise("-1"); + $result[] = optionChoise("Warning: you're timezone is not set."); + $result[] = optionChoise("Please run \"/profile timezone\" to specify your timezone!"); + $interaction->autoCompleteResult($result); + return true; + } + return false; + } +} diff --git a/src/Client/Traits/FromJson.php b/src/Client/Traits/FromJson.php new file mode 100644 index 0000000..397da03 --- /dev/null +++ b/src/Client/Traits/FromJson.php @@ -0,0 +1,36 @@ +discord) { + $this->discord = env()->discord; + } + + return $this->discord; + } +} diff --git a/src/Client/Traits/HasTemplate.php b/src/Client/Traits/HasTemplate.php new file mode 100644 index 0000000..fa3a751 --- /dev/null +++ b/src/Client/Traits/HasTemplate.php @@ -0,0 +1,24 @@ +data->options->get('name', 'remainder')->value; + + $result[] = optionChoise(sprintf('Error: The remainder "%s" is not a valid remainder!', $remainderAlias)); + $result[] = optionChoise('Please chose one from the selection list!'); + $interaction->autoCompleteResult($result); + return false; + } + + // -------------------------------------------------------------------------------------------------------------- + /** + * Gets the actual remainder from the dispaly list (remainder alias list) or false if not found. + * + * @param Interaction $interaction + * @param array $remainders + * + * @return Remainder|false + * + */ + protected function getActualRemainder(Interaction $interaction, array $remainders): Remainder|false + { + $remainderAlias = $interaction->data->options->get('name', 'remainder')->value; + // extract the index from the alias + $result = preg_match('/\(#(\d*)\)/', $remainderAlias, $matches); + if (1 !== $result) { + return false; + } + + $remainderIndex = $matches[1]; + // select the remainder + $remainder = $remainders[$remainderIndex]; + + return $remainder; + } + + // -------------------------------------------------------------------------------------------------------------- + /** + * Generates autocomplete list for the remainder parameter. + * + * @param Interaction $interaction + * @param RequestOption $option + * + * @return array + * + */ + protected function autoCompleteRemainder(Interaction $interaction, RequestOption $option, DiscordUser $discordUser): void + { + $searchString = $option->value; + + $result = []; + + $index = 0; + foreach ($discordUser->remainders as $remainder) { + $message = sprintf( + "(#%d): %s -> \"%s\"", + $index, + Carbon::parse($remainder->due_at) + ->setTimezone($discordUser->timezone) + ->format(DiscordBot::getDateTimeFormat()), + $remainder->message + ); + + //NOTE: max 100 chars.... + $message = mb_strimwidth($message, 0, 80, '...'); + + if ($searchString === '' || false !== stripos($message, $searchString)) { + $result[] = optionChoise($message); + } + + $index++; + } + + $interaction->autoCompleteResult($result); + + } + + /** + * Send an error for the invalid remainder alias to the client as a response message. + * + * @param Interaction $interaction + * @param string $remainderAlias + * + * @return void + * + */ + protected function failInvalidRemainderAlias(Interaction $interaction, string $remainderAlias): void + { + DiscordBot::respondToInteraction( + interaction: $interaction, + template: ClientMessages::errorInvalidRemainderAlias, + variables: [ + 'remainder' => $remainderAlias, + ] + ); + } + +} diff --git a/src/Client/Traits/Singleton.php b/src/Client/Traits/Singleton.php new file mode 100644 index 0000000..8296641 --- /dev/null +++ b/src/Client/Traits/Singleton.php @@ -0,0 +1,50 @@ + message [channel] - Create a remainder. + * + */ +#[Command] +class CreateRemainder implements CommandHandler +{ + use HasApiClient, HasCache, HasDiscord, AssureTimezoneSet; + + // -------------------------------------------------------------------------------------------------------------- + /** + * Handles the request from the discord client. + * + * @param Interaction $interaction + * + * @return void + * + */ + public function handle(Interaction $interaction): void + { + $when = $interaction->data->options->get('name', 'when')->value; + $message = $interaction->data->options->get('name', 'message')->value; + $channel = $interaction->data->options->get('name', 'channel')?->value; + + $discordUser = DiscordUser::fromInteraction($interaction); + + $this->getCache()->getDiscordUser($discordUser)->then( + onFulfilled: function (DiscordUser $discordUser) use ($when, $message, $channel, $interaction) { + + // fail and send error message to the discord client if the discorduser does not have a valid timezone + if ($this->failIfTimezoneNotSet($interaction, $discordUser)) { + return; + } + + if (!isDateTimeValid($when)) { + DiscordBot::respondToInteraction( + interaction: $interaction, + template: ClientMessages::errorDateTimeNotValid, + variables: [ + 'time' => $when, + ] + ); + return; + } + + // get the due_at time based on the discord users timezone + $due_at = Carbon::parse($when, $discordUser->timezone); + + $newRemainder = new Remainder( + id: null, + discord_user_id: $discordUser->id, + channel_id: $channel ?? null, + due_at: $due_at->getTimestamp(), + message: $message, + status: 'new', + error: null, + discord_user: $discordUser, + ); + + // create remainder + $this->getApiClient()->createRemainder(remainder: $newRemainder)->then( + onFulfilled: function (Response $response) use ($interaction, $discordUser) { + + $remainder = (RemainderResponse::make($response))->remainder; + + $this->getCache()->forgetRemainderList($discordUser); + + DiscordBot::respondToInteraction( + interaction: $interaction, + template: ClientMessages::successRemainderCreated, + variables: [ + 'discordUser' => $discordUser, + 'remainder' => $remainder, + ] + ); + + }, + onRejected: DiscordBot::onPromiseRejected($interaction) + ); + }, + onRejected: DiscordBot::onPromiseRejected($interaction) + ); + } + + // -------------------------------------------------------------------------------------------------------------- + /** + * Generates autocomplete list. + * + * @param Interaction $interaction + * + * @return void + * + */ + public function autocomplete(Interaction $interaction): void + { + $option = $interaction->data->options->get('focused', 1); + + match ($option->name) { + 'when' => $this->autoCompleteWhen($interaction, $option), + default => $interaction->autoCompleteResult([]), + }; + + } + + // -------------------------------------------------------------------------------------------------------------- + /** + * Generates autocomplete list for the when/due_at parameter. + * + * @param Interaction $interaction + * @param RequestOption $option + * + * @return array + * + */ + protected function autoCompleteWhen(Interaction $interaction, RequestOption $option): void + { + $searchString = $option->value; + + $discordUser = DiscordUser::fromInteraction($interaction); + + $this->getCache()->getDiscordUser($discordUser)->then( + onFulfilled: function (DiscordUser $discordUser) use ($interaction, $searchString): void { + + + if ($this->failAutoCompleteIfTimezoneNotSet($interaction, $discordUser)) { + return; + } + + $result = []; + $due_at = 'n/a'; //NOTE: "Must be between 1 and 100 in length.", no empty string allowed... + + if ($searchString === '') { + // no data jet, dispay placeholder + $result[] = optionChoise("Start typing a time..."); + } else { + // try to parse the time + + if (isDateTimeValid($searchString)) { + $due_at = Carbon::parse($searchString, $discordUser->timezone)->diffForHumans(); + } else { + $result[] = optionChoise('Error: invalid time'); + } + + $result[] = optionChoise($searchString); + $result[] = optionChoise($due_at); + } + + $interaction->autoCompleteResult($result); + }, + onRejected: DiscordBot::onPromiseRejected($interaction) + ); + } + + + // -------------------------------------------------------------------------------------------------------------- + /** + * Defines the structure of the command + * + * @return CommandBuilder + * + */ + public function getConfig(): CommandBuilder + { + $discord = $this->getDiscord(); + + return (new CommandBuilder()) + ->setName('rem') + ->setDescription('Sets a reminder') + ->addOption( + (new Option($discord)) + ->setName('when') + ->setType(Option::STRING) + ->setDescription('The time to remind you') + ->setRequired(true) + ->setAutoComplete(true) + ) + ->addOption( + (new Option($discord)) + ->setName('message') + ->setType(Option::STRING) + ->setDescription('The body of the remainder') + ->setRequired(true) + ) + ->addOption( + (new Option($discord)) + ->setName('channel') + ->setType(Option::CHANNEL) + ->setDescription('The channel of the remainder') + ->setRequired(false) + ) + ; + } +} diff --git a/src/Commands/EditRemainder.php b/src/Commands/EditRemainder.php new file mode 100644 index 0000000..957ceb2 --- /dev/null +++ b/src/Commands/EditRemainder.php @@ -0,0 +1,300 @@ + [when] [message] [channel] - Edit a remainder. + * + */ +#[Command] +class EditRemainder implements CommandHandler +{ + use + AssureTimezoneSet, + HasCache, + HasApiClient, + HasDiscord, + HasTemplate, + RemainderListCommand; + + // -------------------------------------------------------------------------------------------------------------- + /** + * Handles the request from the discord client. + * + * @param Interaction $interaction + * + * @return void + * + */ + public function handle(Interaction $interaction): void + { + + $discordUser = DiscordUser::fromInteraction($interaction); + + DiscordBot::getInstance()->getDiscordUserRemainders($interaction, $discordUser)->then( + onFulfilled: function (DiscordUser $discordUser) use ($interaction): void { + + // get the remainder to edit + $remainder = $this->getActualRemainder($interaction, $discordUser->remainders); + + //fail if the actual remainder cannot be evaulated + if (false === $remainder) { + $remainderAlias = $interaction->data->options->get('name', 'remainder')->value; + $this->failInvalidRemainderAlias($interaction, $remainderAlias); + return; + } + + // get the option values + $when = $interaction->data->options->get('name', 'when')?->value; + $message = $interaction->data->options->get('name', 'message')?->value; + $channel = $interaction->data->options->get('name', 'channel')?->value; + + $changes = []; + + // fail if when/due_at was provided, but is invalid + if ($when && !isDateTimeValid($when)) { + DiscordBot::respondToInteraction( + interaction: $interaction, + template: ClientMessages::errorDateTimeNotValid, + variables: [ + 'time' => $when, + ] + ); + return; + } + + // if when/due_at was provided, update it + if ($when) { + $changes['due_at'] = Carbon::parse($when, $discordUser->timezone)->getTimestamp(); + + // fail if the new time is already past + if (Carbon::now()->getTimestamp() >= $changes['due_at']) { + DiscordBot::respondToInteraction( + interaction: $interaction, + template: ClientMessages::errorDateTimeInThePast, + variables: [ + 'time' => $when, + ] + ); + return; + } + } + + // if message was provided, update it + if ($message) { + $changes['message'] = $message; + } + + // if channel was provided, update it + if ($channel) { + $changes['channel_id'] = $channel; + } + + // update the remiander + $this->getApiClient()->updateRemainder($remainder, $changes)->then( + onFulfilled: function (Response $response) use ($interaction, $discordUser) { + + $remainder = (RemainderResponse::make($response))->remainder; + + $this->getCache()->forgetRemainderList($discordUser); + + DiscordBot::respondToInteraction( + interaction: $interaction, + template: ClientMessages::successRemainderUpdated, + variables: [ + 'discordUser' => $discordUser, + 'remainder' => $remainder, + ] + ); + + }, + onRejected: DiscordBot::onPromiseRejected($interaction) // updateRemainder + ); + + } + ); + } + + // -------------------------------------------------------------------------------------------------------------- + /** + * Generates autocomplete list. + * + * @param Interaction $interaction + * + * @return void + * + */ + public function autocomplete(Interaction $interaction): void + { + $option = $interaction->data->options->get('focused', 1); + + $discordUser = DiscordUser::fromInteraction($interaction); + + DiscordBot::getInstance()->getDiscordUserRemainders($interaction, $discordUser)->then( + onFulfilled: function (DiscordUser $discordUser) use ($interaction, $option): void { + + $parameters = [$interaction, $option, $discordUser]; + + // fill the lkist for the specified option + match ($option->name) { + 'remainder' => $this->autoCompleteRemainder(...$parameters), + 'when' => $this->autoCompleteWhen(...$parameters), + 'message' => $this->autoCompleteMessage(...$parameters), + default => $interaction->autoCompleteResult([]), + }; + + } + ); + + } + + // -------------------------------------------------------------------------------------------------------------- + /** + * Generates autocomplete list for the when/due_at parameter. + * + * @param Interaction $interaction + * @param RequestOption $option + * + * @return array + * + */ + protected function autoCompleteWhen(Interaction $interaction, RequestOption $option, DiscordUser $discordUser): void + { + $searchString = $option->value; + + $result = []; + $timezone = $discordUser->timezone; + $remainder = $this->getActualRemainder($interaction, $discordUser->remainders); + + // fail, if the remainder cannot be evaluated + if (false === $remainder) { + $this->invalidRemainderAlias($interaction); + return; + } + + // set the current value es default + if ($searchString == '') { + $searchString = Carbon::createFromTimestamp($remainder->due_at) + ->setTimezone($timezone) + ->format(DiscordBot::getDateTimeFormat()); + } + + // fill the human readable value or show an error in case of an invalid value + $due_at = isDateTimeValid($searchString) + ? Carbon::parse($searchString, $timezone)->diffForHumans() + : 'Error: invalid time'; + + // add values to result list + $result[] = optionChoise($searchString); + $result[] = optionChoise($due_at); + + //send autocomplete results + $interaction->autoCompleteResult($result); + + } + + // -------------------------------------------------------------------------------------------------------------- + /** + * Generates autocomplete list for the message parameter. + * + * @param Interaction $interaction + * @param RequestOption $option + * + * @return array + * + */ + protected function autoCompleteMessage(Interaction $interaction, RequestOption $option, DiscordUser $discordUser): void + { + $searchString = $option->value; + + $result = []; + $remainder = $this->getActualRemainder($interaction, $discordUser->remainders); + + // fail, if the remainder cannot be evaluated + if (false === $remainder) { + $this->invalidRemainderAlias($interaction); + return; + } + + if ($searchString == '') { + $searchString = $remainder->message; + } + + $result[] = optionChoise($searchString); + + $interaction->autoCompleteResult($result); + + } + + // -------------------------------------------------------------------------------------------------------------- + /** + * Defines the structure of the command + * + * @return CommandBuilder + * + */ + public function getConfig(): CommandBuilder + { + return (new CommandBuilder()) + ->setName('edit') + ->setDescription('Edit a reminder.') + ->addOption( + (new Option($this->getDiscord())) + ->setName('remainder') + ->setType(Option::STRING) + ->setDescription('The reminder to edit.') + ->setRequired(true) + ->setAutoComplete(true) + ) + ->addOption( + (new Option($this->getDiscord())) + ->setName('when') + ->setType(Option::STRING) + ->setDescription('The time to remind you') + ->setAutoComplete(true) + ->setRequired(false) + ) + ->addOption( + (new Option($this->getDiscord())) + ->setName('message') + ->setType(Option::STRING) + ->setDescription('The body of the remainder') + ->setAutoComplete(true) + ->setRequired(false) + ) + ->addOption( + (new Option($this->getDiscord())) + ->setName('channel') + ->setType(Option::CHANNEL) + ->setDescription('The channel of the remainder') + ->setRequired(false) + ) + ; + } +} diff --git a/src/Commands/ListRemainders.php b/src/Commands/ListRemainders.php new file mode 100644 index 0000000..d01e68e --- /dev/null +++ b/src/Commands/ListRemainders.php @@ -0,0 +1,132 @@ + - Shows the paginated list of remainders for the DiscordUser. + * + */ +#[Command] +class ListRemainders implements CommandHandler +{ + use HasCache, HasDiscord, AssureTimezoneSet; + + // -------------------------------------------------------------------------------------------------------------- + /** + * Handles the request from the discord client. + * + * @param Interaction $interaction + * + * @return void + * + */ + public function handle(Interaction $interaction): void + { + + $discordUser = DiscordUser::fromInteraction($interaction); + + DiscordBot::getInstance()->getDiscordUserRemainders($interaction, $discordUser)->then( + onFulfilled: function (DiscordUser $discordUser) use ($interaction) { + + $pageSize = 20; // keep it low, so the message will fit in the 2000 character limit + $itemCount = count($discordUser->remainders); + $pageCount = match ($itemCount) { + 0 => 1, + default => ceil($itemCount / $pageSize) + }; + + $page = $interaction->data->options->get('name', 'page')?->value ?? 1; + + // fail if the page is not valid + if ($page < 1 || $page > $pageCount) { + DiscordBot::respondToInteraction( + interaction: $interaction, + template: ClientMessages::errorListPageInvalid, + variables: [ + 'page' => $page, + 'pageCount' => $pageCount, + ] + ); + } + + // paginate remainders + $first = $pageSize * ($page - 1); + $currnetRemainders = array_slice($discordUser->remainders, $first, $pageSize); + + // start counting from 1 instead of 0 + $first++; + + DiscordBot::respondToInteraction( + interaction: $interaction, + template: ClientMessages::listRemaindersCompacted, + variables: [ + 'discordUser' => $discordUser, + 'remainders' => $currnetRemainders, + 'paginate' => [ + 'pageSize' => $pageSize, + 'pageCount' => $pageCount, + 'page' => $page, + 'itemCount' => $itemCount, + 'first' => $first, + 'last' => $first + count($currnetRemainders) - 1, + ], + ] + ); + } + ); + } + + + // -------------------------------------------------------------------------------------------------------------- + /** + * Generates autocomplete list. + * + * @param Interaction $interaction + * + * @return void + * + */ + public function autocomplete(Interaction $interaction): void + { + // + } + + // -------------------------------------------------------------------------------------------------------------- + /** + * Defines the structure of the command + * + * @return CommandBuilder + * + */ + public function getConfig(): CommandBuilder + { + return (new CommandBuilder()) + ->setName('list') + ->setDescription('Lists the current reminders.') + ->addOption( + (new Option($this->getDiscord())) + ->setName('page') + ->setType(Option::INTEGER) + ->setDescription('The page to show. (defulats: 1).') + //->setRequired(false) + //->setAutoComplete(true) + ) + ; + } +} diff --git a/src/Commands/Profile.php b/src/Commands/Profile.php new file mode 100644 index 0000000..5f03a47 --- /dev/null +++ b/src/Commands/Profile.php @@ -0,0 +1,257 @@ + - Updates the timezone. + * @example /profile locale - Updates the locale. + * @example /profile timezone locale - Updates timezone and locale. + * + */ +#[Command] +class Profile implements CommandHandler +{ + use HasCache, HasDiscord, AssureTimezoneSet; + + // -------------------------------------------------------------------------------------------------------------- + /** + * Displays the current profile of the DiscordUser. + * + * @param Interaction $interaction + * + * @return void + * + */ + protected function showProfileInfo(Interaction $interaction): void + { + + $discordUser = DiscordUser::fromInteraction($interaction); + + $this->getCache()->getDiscordUser($discordUser)->then( + onFulfilled: function (DiscordUser $discordUser) use ($interaction) { + + // fail and send error message to the discord client if the discorduser does not have a valid timezone + if ($this->failIfTimezoneNotSet($interaction, $discordUser)) { + return; + } + + DiscordBot::respondToInteraction( + interaction: $interaction, + template: ClientMessages::infoProfile, + variables: [ + 'discordUser' => $discordUser, + 'localTime' => $discordUser->localTime(), + 'localeName' => locale_get_display_name($discordUser->locale ?? 'not defined', 'en'), + ] + ); + + }, + onRejected: DiscordBot::onPromiseRejected($interaction) + ); + } + + // -------------------------------------------------------------------------------------------------------------- + /** + * Handles the request from the discord client. + * + * @param Interaction $interaction + * + * @return void + * + */ + public function handle(Interaction $interaction): void + { + + // Show info if no update was requested + if ($interaction->data->options->count() == 0) { + $this->showProfileInfo($interaction); + return; + } + + $discordUser = DiscordUser::fromInteraction($interaction); + $updated = []; + $errors = []; + + // update timezone if present + if ($interaction->data->options->has('timezone')) { + $timezone = $interaction->data->options->get('name', 'timezone'); + if (!isTimeZoneValid($timezone->value)) { + $errors['timezone'] = $timezone->value; + } else { + $updated['timezone'] = [ + 'old' => $discordUser->timezone, + 'new' => $timezone->value, + ]; + $discordUser->timezone = $timezone->value; + } + } + + // update locale if present + if ($interaction->data->options->has('locale')) { + $locale = $interaction->data->options->get('name', 'locale'); + if (!isLocaleValid($locale->value)) { + $errors['locale'] = $locale->value; + } else { + $updated['locale'] = [ + 'old' => $discordUser->locale, + 'new' => $locale->value, + 'name' => locale_get_display_name($locale->value ?? 'not defined', 'en'), + ]; + $discordUser->locale = $locale->value; + } + } + + + //fail if errors were found + if (count($errors) > 0) { + var_dump($errors); + DiscordBot::respondToInteraction( + $interaction, + ClientMessages::errorUpdateProfileError, + ['errors' => $errors] + ); + return; + } + + // update profile + if (count($updated) > 0) { + $this->getCache()->forgetDiscordUser($discordUser); + $this->getCache()->getDiscordUser($discordUser)->then( + onFulfilled: function (DiscordUser $discordUser) use ($interaction, $updated) { + + $this->getCache()->storeDiscordUser($discordUser); + + DiscordBot::respondToInteraction( + interaction: $interaction, + template: ClientMessages::successProfileUpdated, + variables: [ + 'discordUser' => $discordUser, + 'localTime' => $discordUser->localTime(), + 'updated' => $updated, + ] + ); + }, + onRejected: DiscordBot::onPromiseRejected($interaction) + ); + } + + } + + // -------------------------------------------------------------------------------------------------------------- + /** + * Generates autocomplete list. + * + * @param Interaction $interaction + * + * @return void + * + */ + public function autocomplete(Interaction $interaction): void + { + $option = $interaction->data->options->get('focused', 1); + + $interaction->autoCompleteResult(match ($option->name) { + 'timezone' => $this->autoCompleteTimeZone($interaction, $option), + 'locale' => $this->autoCompleteLocale($interaction, $option), + }); + } + + // -------------------------------------------------------------------------------------------------------------- + /** + * Generates autocomplete list for the timezone parameter. + * + * @param Interaction $interaction + * @param RequestOption $option + * + * @return array + * + */ + protected function autoCompleteTimeZone(Interaction $interaction, RequestOption $option): array + { + $searchString = $option->value; + + $timezoneList = DateTimeZone::listIdentifiers(); + $matches = array_filter($timezoneList, fn (string $value) => stripos($value, $searchString) !== false); + sort($matches); + $matches = array_slice($matches, 0, 25); + $result = array_map(fn (string $value) => ['name' => $value, 'value' => $value], $matches); + + return $result; + + } + + // -------------------------------------------------------------------------------------------------------------- + /** + * Generates autocomplete list for the locale parameter. + * + * @param Interaction $interaction + * @param RequestOption $option + * + * @return array + * + */ + protected function autoCompleteLocale(Interaction $interaction, RequestOption $option): array + { + + $searchString = $option->value; + $matches = array_filter(LOCALES, fn (string $value) => stripos($value, $searchString) !== false); + sort($matches); + $matches = array_slice($matches, 0, 25); + $result = array_map(fn (string $value) => ['name' => $value, 'value' => $value], $matches); + + return $result; + + } + + // -------------------------------------------------------------------------------------------------------------- + /** + * Defines the structure of the command + * + * @return CommandBuilder + * + */ + public function getConfig(): CommandBuilder + { + $discord = $this->getDiscord(); + + return (new CommandBuilder()) + ->setName('profile') + ->setDescription('Manages your profile') + ->addOption( + (new Option($discord)) + ->setName('timezone') + ->setType(Option::STRING) + ->setDescription('TimeZone') + ->setAutoComplete(true) + ) + ->addOption( + (new Option($discord)) + ->setName('locale') + ->setType(Option::STRING) + ->setDescription('Locale') + ->setAutoComplete(true) + ); + } +} diff --git a/src/Commands/RemoveRemainder.php b/src/Commands/RemoveRemainder.php new file mode 100644 index 0000000..f21a39b --- /dev/null +++ b/src/Commands/RemoveRemainder.php @@ -0,0 +1,210 @@ + + $interaction->updateOriginalResponse($messageBuilder + ->setContent('Kept reaminder.') + ->removeComponent($actionRow)) + ->otherwise(DiscordBot::onPromiseRejected($interaction)); + ; + } + + // -------------------------------------------------------------------------------------------------------------- + protected function btnOkListener( + Interaction $interaction, + DiscordUser $discordUser, + MessageBuilder $messageBuilder, + ActionRow $actionRow, + ): callable { + return function (Interaction $iAnswer) use ($interaction, $discordUser, $messageBuilder, $actionRow) { + + $remainder = $this->getActualRemainder($interaction, $discordUser->remainders); + + $this->getApiClient()->deleteRemainder($discordUser, $remainder)->then( + onFulfilled: function ($data) use ($interaction, $discordUser, $iAnswer, $messageBuilder, $actionRow, $remainder): void { + $this->getCache()->forgetRemainderList($discordUser); + + // update client message + $interaction->updateOriginalResponse($messageBuilder + ->setContent('Remainder deleted succesfully.') + ->removeComponent($actionRow)) + ->otherwise(DiscordBot::onPromiseRejected($interaction)); + + }, + onRejected: function (Exception $exception) use ($iAnswer, $interaction) { + + $interaction->deleteOriginalResponse(); + DiscordBot::failApiRequestWithException($iAnswer, $exception); + } + ); + }; + + } + + // -------------------------------------------------------------------------------------------------------------- + /** + * Handles the request from the discord client. + * + * @param Interaction $interaction + * + * @return void + * + */ + public function handle(Interaction $interaction): void + { + + $discordUser = DiscordUser::fromInteraction($interaction); + + DiscordBot::getInstance()->getDiscordUserRemainders($interaction, $discordUser)->then( + onFulfilled: function (DiscordUser $discordUser) use ($interaction): void { + + $remainder = $this->getActualRemainder($interaction, $discordUser->remainders); + //fail if the actual remainder cannot be evaulated + if (false === $remainder) { + $remainderAlias = $interaction->data->options->get('name', 'remainder')->value; + $this->failInvalidRemainderAlias($interaction, $remainderAlias); + return; + } + + // create message handlers + $messageBuilder = MessageBuilder::new(); + $actionRow = ActionRow::new(); + + // add OK button + $btnOK = Button::new(Button::STYLE_SUCCESS) + ->setLabel('Yes, delete the remainder!') + ->setEmoji('👎') + ->setListener($this->btnOkListener( + interaction: $interaction, + discordUser: $discordUser, + messageBuilder: $messageBuilder, + actionRow: $actionRow, + ), $this->getDiscord()) + ; + + // add CANCEL button + $btnCancel = Button::new(Button::STYLE_DANGER) + ->setLabel('No, keep the remainder.') + ->setEmoji('👍') + ->setListener($this->btnCancelListener( + interaction: $interaction, + discordUser: $discordUser, + messageBuilder: $messageBuilder, + actionRow: $actionRow, + ), $this->getDiscord()) + ; + + $actionRow->addComponent($btnOK)->addComponent($btnCancel); + + // send temporary response + //TODO: maybe test for success/failure here as well... + $interaction->acknowledgeWithResponse(true)->done(function () use ($interaction, $messageBuilder, $actionRow) { + $interaction->updateOriginalResponse( + builder: $messageBuilder + ->setContent('Are you sure you want to delete this remainder?') + ->addComponent($actionRow) + )->otherwise(DiscordBot::onPromiseRejected($interaction)); + + }); + + } + ); + + } + // -------------------------------------------------------------------------------------------------------------- + /** + * Generates autocomplete list. + * + * @param Interaction $interaction + * + * @return void + * + */ + public function autocomplete(Interaction $interaction): void + { + $option = $interaction->data->options->get('focused', 1); + + $discordUser = DiscordUser::fromInteraction($interaction); + + DiscordBot::getInstance()->getDiscordUserRemainders($interaction, $discordUser)->then( + onFulfilled: function (DiscordUser $discordUser) use ($interaction, $option): void { + + $parameters = [$interaction, $option, $discordUser]; + + // fill the list for the specified option + match ($option->name) { + 'remainder' => $this->autoCompleteRemainder(...$parameters), + default => $interaction->autoCompleteResult([]), + }; + + } + ); + + } + + // -------------------------------------------------------------------------------------------------------------- + /** + * Defines the structure of the command + * + * @return CommandBuilder + * + */ + public function getConfig(): CommandBuilder + { + return (new CommandBuilder()) + ->setName('delete') + ->setDescription('Delete a reminder.') + ->addOption( + (new Option($this->getDiscord())) + ->setName('remainder') + ->setType(Option::STRING) + ->setDescription('The reminder to delete.') + ->setRequired(true) + ->setAutoComplete(true) + ) + ; + } + + +} + +//👍 ☠ 👎 diff --git a/src/Core/Commands/Command.php b/src/Core/Commands/Command.php new file mode 100644 index 0000000..24eab46 --- /dev/null +++ b/src/Core/Commands/Command.php @@ -0,0 +1,19 @@ +guild)) { + throw new LogicException('Guild ID must be alphanumeric'); + } + } +} diff --git a/src/Core/Commands/CommandHandler.php b/src/Core/Commands/CommandHandler.php new file mode 100644 index 0000000..ff61abb --- /dev/null +++ b/src/Core/Commands/CommandHandler.php @@ -0,0 +1,15 @@ +queue[] = $command; + + return $this; + } + + public function runQueue(bool $loadCommands = true, bool $registerCommands = true): PromiseInterface + { + $discord = discord(); + $discord->getLogger()->info('Running command queue...'); + + return new Promise(function ($resolve) use ($registerCommands, $discord, $loadCommands) { + debug('Running Loop for ' . count($this->queue) . ' commands...'); + async(function () use ($registerCommands, $discord, $loadCommands, $resolve) { + if ($registerCommands) { + debug('Getting commands...'); + /** @var GlobalCommandRepository $globalCommands */ + $globalCommands = await($discord->application->commands->freshen()); + + /** @var GuildCommandRepository[] $guildCommands */ + $guildCommands = []; + + foreach ($this->queue as $command) { + debug("Checking {$command->getName()}..."); + /** @var GlobalCommandRepository|GuildCommandRepository $rCommands */ + $rCommands = $command->properties->guild === null ? + $globalCommands : + $guildCommands[$command->properties->guild] ??= await($discord->guilds->get('id', $command->properties->guild)->commands->freshen()); + + $rCommand = $rCommands->get('name', $command->getName()); + + if ($rCommand === null || $command->hasCommandChanged($rCommand)) { + debug("Command {$command->getName()} has changed, re-registering it..."); + $command->setNeedsRegistered(true); + } + } + } + + if ($loadCommands) { + $this->loadCommands(); + } + + $resolve(); + })(); + }); + } + + protected function loadCommands(): void + { + debug('Loading commands...'); + $discord = discord(); + + $listen = static function (string|array $name, QueuedCommand $command) use ($discord) { + try { + $registered = $discord->listenCommand($command->getName(), $command->handler->handle(...), $command->handler->autocomplete(...)); + + if (!is_array($command->name) || count($command->name) === 1) { + return; + } + + $loop = static function (array $commands) use (&$loop, $registered, $command) { + foreach ($commands as $commandName) { + if (is_array($commandName)) { + $loop($commandName); + } + + $registered->addSubCommand($commandName, $command->handler->handle(...), $command->handler->autocomplete(...)); + } + }; + $names = $command->name; + array_shift($names); + + $loop($names); + } catch (Throwable $e) { + if (preg_match_all('/The command `(\w+)` already exists\./m', $e->getMessage(), $matches, PREG_SET_ORDER)) { + return; + } + + error($e); + } + }; + + foreach ($this->queue as $command) { + $listen($command->name, $command); + + debug("Loaded command {$command->getName()}"); + + if (!$command->needsRegistered()) { + debug("Command {$command->getName()} does not need to be registered"); + + continue; + } + + $this->registerCommand($command); + debug("Command {$command->getName()} was registered"); + } + } + + protected function registerCommand(QueuedCommand $command): PromiseInterface + { + return new Promise(static function ($resolve, $reject) use ($command) { + $discord = discord(); + $commands = $command->properties->guild === null ? + $discord->application->commands : + $discord->guilds->get('id', $command->properties->guild)?->commands ?? null; + + if ($commands === null && $command->properties->guild !== null) { + $discord->getLogger()->error("Failed to register command {$command->getName()}: Guild {$command->properties->guild} not found"); + + return; + } + + try { + $commands->save( + $commands->create( + $command->handler->getConfig()->toArray() + ) + )->then(static function () use ($command, $resolve) { + debug("Command {$command->getName()} was registered"); + $resolve(); + })->otherwise(static function (Throwable $e) use ($command, $reject) { + error("Failed to register command {$command->getName()}: {$e->getMessage()}"); + $reject($e); + }); + } catch (Throwable $e) { + error("Failed to register command {$command->getName()}: {$e->getMessage()}"); + $reject($e); + } + }); + } + + public static function queueAndRunCommands(bool $loadCommands = true, bool $registerCommands = true, QueuedCommand ...$commands): PromiseInterface + { + $queue = (new self()); + + foreach ($commands as $command) { + $queue->appendCommand($command); + } + + return $queue->runQueue($loadCommands, $registerCommands); + } +} diff --git a/src/Core/Commands/QueuedCommand.php b/src/Core/Commands/QueuedCommand.php new file mode 100644 index 0000000..64a0de6 --- /dev/null +++ b/src/Core/Commands/QueuedCommand.php @@ -0,0 +1,77 @@ +properties->name ?? $this->handler->getConfig()->toArray()['name'] ?? null; + + if ($name === null) { + $className = get_class($this->handler); + throw new LogicException("Command {$className} has no name"); + } + + $this->name = $name; + } + + public function getName(): string + { + return is_array($this->name) ? $this->name[0] : $this->name; + } + + public function hasCommandChanged(DiscordCommand $rCommand): bool + { + $command = $this->handler->getConfig(); + $rCommand = $rCommand->jsonSerialize(); + + if ($command instanceof CommandBuilder) { + $command = $command->jsonSerialize(); + } + + $areTheSame = static function (array|ArrayAccess $a, array|ArrayAccess $b) use (&$areTheSame): bool { + $ignoreFields = ['default_permission', 'required']; + + foreach ($a as $key => $value) { + $bValue = $b[$key] ?? null; + + if ($value === $bValue || in_array($key, $ignoreFields)) { + continue; + } + + if (is_array($value) && (is_array($bValue) || $bValue instanceof ArrayAccess)) { + if (!$areTheSame($value, $bValue)) { + return false; + } + } else { + return false; + } + } + + return true; + }; + + return !$areTheSame($command, $rCommand); + } + + public function setNeedsRegistered(bool $needsRegistered): void + { + $this->needsRegistered = $needsRegistered; + } + + public function needsRegistered(): bool + { + return $this->needsRegistered; + } +} diff --git a/src/Core/Disabled.php b/src/Core/Disabled.php new file mode 100644 index 0000000..c8ab8e6 --- /dev/null +++ b/src/Core/Disabled.php @@ -0,0 +1,10 @@ +files[$file] = $hotFile; + + $hotFile + ->on(HotFile::EVENT_CHANGED, fn (HotFile $file) => $this->emit(self::EVENT_FILE_CHANGED, [$this, $file])) + ->on(HotFile::EVENT_REMOVED, fn (HotFile $file) => $this->emit(self::EVENT_FILE_REMOVED, [$this, $file])); + } + + Loop::addPeriodicTimer($interval, function () use ($interval) { + foreach (FileSystemUtils::getAllFiles($this->directory) as $file) { + if (isset($this->files[$file])) { + continue; + } + + $this->files[$file] = new HotFile($file, $interval); + + $this->emit(self::EVENT_FILE_ADDED, [$this, $file]); + } + }); + } + + /** + * @return HotFile[] + */ + public function getFiles(): array + { + return $this->files; + } +} diff --git a/src/Core/HMR/HotFile.php b/src/Core/HMR/HotFile.php new file mode 100644 index 0000000..12d7f95 --- /dev/null +++ b/src/Core/HMR/HotFile.php @@ -0,0 +1,66 @@ +name = basename($file); + $this->hash = $this->createHash(); + + $this->timer = Loop::addPeriodicTimer($interval, function () { + if (!file_exists($this->file)) { + $this->emit(self::EVENT_REMOVED, [$this]); + $this->__destruct(); + + return; + } + + if (!$this->hasChanged()) { + return; + } + + $this->hash = $this->createHash(); + $this->emit(self::EVENT_CHANGED, [$this]); + }); + } + + public function getContents(): string + { + return file_get_contents($this->file); + } + + private function createHash(): string + { + return hash('sha256', $this->getContents()); + } + + public function hasChanged(): bool + { + return $this->createHash() !== $this->hash; + } + + public function __destruct() + { + Loop::cancelTimer($this->timer); + } +} diff --git a/src/Core/functions.php b/src/Core/functions.php new file mode 100644 index 0000000..9f0b980 --- /dev/null +++ b/src/Core/functions.php @@ -0,0 +1,274 @@ +discord)) { + return null; + } + + return env()->discord; +} + +/** + * Create a new Option used for building slash commands + */ +function newSlashCommandOption(string $name, string $description, int $type, bool $required = false): CommandOption +{ + return newDiscordPart(CommandOption::class) + ->setName($name) + ->setDescription($description) + ->setType($type) + ->setRequired($required); +} + +/** + * Create a new Choice used for building slash commands + */ +function newSlashCommandChoice(string $name, float|int|string $value): Choice +{ + return newDiscordPart(Choice::class) + ->setName($name) + ->setValue($value); +} + +/** + * Create a new MessageBuilder object with the content define for creating simple MessageBuilders quickly + * + * ```php + * $message = messageWithContent("Hello World"); + * ``` + */ +function messageWithContent(string $content): MessageBuilder +{ + return MessageBuilder::new()->setContent($content); +} + +/** + * Append to grab and empty array field. You can supply an embed to have the empty field added, or + * if you leave the `$embed` option `null`, then an array containing the empty field will be returned + * + * ```php + * $embed = newDiscordPart("\Discord\Parts\Embed\Embed"); + * emptyEmbedField($embed); + * ``` + * + * or + * + * ```php + * $embed = newDiscordPart("\Discord\Parts\Embed\Embed"); + * $emptyField = emptyEmbedField(); + * ``` + */ +function emptyEmbedField(?Embed $embed = null): array|Embed +{ + $emptyField = ['name' => "\u{200b}", 'value' => "\u{200b}"]; + + if ($embed !== null) { + return $embed->addField($emptyField); + } + + return $emptyField; +} + +/** + * @template T + * + * @param class-string $class + * + * @return T + */ +function newDiscordPart(string $class, mixed ...$args): mixed +{ + return new $class(discord(), ...$args); +} + +/** + * Quickly build an action row with multiple buttons + * + * ```php + * $banButton = (new Button(Button::STYLE_DANGER))->setLabel("Ban User"); + * $kickButton = (new Button(Button::STYLE_DANGER))->setLabel("Kick User"); + * $actionRow = buildActionRowWithButtons($banButton, $kickButton); + * ``` + * + * *This can also be paired with newButton* + * + * ```php + * $actionRow = buildActionWithButtons( + * newButton(Button::STYLE_DANGER, "Ban User") + * newButton(Button::STYLE_DANGER, "Kick User") + * ); + * ``` + */ +function buildActionRowWithButtons(Button ...$buttons): ActionRow +{ + $actionRow = new ActionRow(); + + foreach ($buttons as $button) { + $actionRow->addComponent($button); + } + + return $actionRow; +} + +/** + * Quickly create button objects + * + * ```php + * $button = newButton(Button::STYLE_DANGER, "Kick User", "Kick|Command_String"); + * ``` + */ +function newButton(int $style, string $label, ?string $custom_id = null): Button +{ + return (new Button($style, $custom_id))->setLabel($label); +} + +/** + * Get an option from an Interaction/Interaction Repository by specifying the option(s) name + * + * For regular slash commands + * `/ban :user` + * + * ```php + * $user = getOptionFromInteraction($interaction, "user"); + * ``` + * + * For sub commands / sub command groups you can stack the names + * `/admin ban :user` + * + * ```php + * $user = getOptionFromInteraction($interaction->data->options, "ban", "user"); + * ``` + */ +function getOptionFromInteraction(Collection|Interaction $options, string ...$names): ?Option +{ + if ($options instanceof Interaction) { + $options = $options->data->options; + } + + $option = null; + foreach ($names as $key => $name) { + $option = $options->get('name', $name); + + if ($key !== count($names) - 1) { + $options = $option?->options; + } + + if ($options === null || $option === null) { + break; + } + } + + return $option; +} + +// Logging Functions + +function log($level, string $message, array $context = []): void +{ + env()->discord->getLogger()->log($level, $message, $context); +} + +function debug(string $message, array $context = []): void +{ + env()->discord->getLogger()->debug($message, $context); +} + +function error(string $message, array $context = []): void +{ + env()->discord->getLogger()->error($message, $context); +} + +function info(string $message, array $context = []): void +{ + env()->discord->getLogger()->info($message, $context); +} + +function warning(string $message, array $context = []): void +{ + env()->discord->getLogger()->warning($message, $context); +} + +// Internal Functions // + +/** + * Loop through all the classes in a directory and call a callback function with the class name + */ +function loopClasses(string $directory, callable $callback): void +{ + $convertPathToNamespace = static fn (string $path): string => str_replace([realpath(BOT_ROOT), '/'], ['', '\\'], $path); + + foreach (FileSystemUtils::getAllFilesWithExtensions($directory, ['php']) as $file) { + $className = basename($file, '.php'); + $path = dirname($file); + $namespace = $convertPathToNamespace($path); + $className = $namespace . '\\' . $className; + + $callback($className, $namespace, $file, $path); + } +} + +/** + * @template T + * + * @param class-string $class + * @param class-string $attribute + * + * @throws ReflectionException + * + * @return T|false + */ +function doesClassHaveAttribute(string $class, string $attribute): object|false +{ + return (new ReflectionClass($class))->getAttributes($attribute, ReflectionAttribute::IS_INSTANCEOF)[0] ?? false; +} + +function deleteAllFilesInDirectory(string $directory): void +{ + if (is_dir($directory) === false) { + return; + } + + foreach (FileSystemUtils::getAllFiles($directory) as $file) { + unlink($file); + } +} diff --git a/src/Core/helpers.php b/src/Core/helpers.php new file mode 100644 index 0000000..8edf485 --- /dev/null +++ b/src/Core/helpers.php @@ -0,0 +1,122 @@ + $message, 'value' => $message ]; +} + +/** + * Removed the slash command from the discord registry + * + * Sometime the DiscordPhp package fails to automaically remove/update the unused/removed/disabled commands, + * or does not update it if the structure is changed (ex.: option is csnged from required to optional) + * this function will remove it, after the bot should be restarted, + * after the updated command will be regitered again. + * + * ```php + * deleteSlashCommand('listall','Was just for testing, no longer needed.'); + * ``` + * + * @param string $name The name of the command + * @param string $reason='' The reason sent to the discord registry for audit logging (if supported) + * + * @return void + * + */ +function deleteSlashCommand(string $name, string $reason=''): void { + + $x = discord()->application->commands->find(function (Command $command) use ($name, $reason) { + if ($command->name === $name) { + discord()->application->commands->delete($command->id, $reason) + ->done(fn ($result) => + DevLogger::warning('Deleted slash command', [ + 'name' => $name, + 'reason' => $reason, + 'result' => $result + ]) + ); + } + }); +} diff --git a/src/Events/Message.php b/src/Events/Message.php new file mode 100644 index 0000000..3a5e2c0 --- /dev/null +++ b/src/Events/Message.php @@ -0,0 +1,28 @@ +LOG_CHANNEL_ID; + $appID = env()->APPLICATION_ID; + $appVersion = BOT_BUILD; + $logChannel = $discord->getChannel($logChannelID); + + // create start notice + $message = MessageBuilder::new() + ->setContent("<@$appID>(v$appVersion) ONLINE.\n") + ->setAllowedMentions([ + 'parse' => ['users'], + ]); + + // send start notice to the log channel + $logChannel->sendMessage($message); + + // regiter RemainderService handler + $loop = $discord->getLoop(); + debug('Registering onLoop event handler'); + $timer = $loop->addPeriodicTimer(1, [env()->remainderService, 'onLoop']); + debug('Registered onLoop event handler'); + + } +} diff --git a/src/LICENSE b/src/LICENSE new file mode 100644 index 0000000..8d8f959 --- /dev/null +++ b/src/LICENSE @@ -0,0 +1,21 @@ +MIT License + +Copyright (c) Totally Not Another PHP Framework + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. diff --git a/src/README.md b/src/README.md new file mode 100644 index 0000000..b3f669c --- /dev/null +++ b/src/README.md @@ -0,0 +1,95 @@ +![Latest](https://proxima.goliath.hu/proxima/discord-bot/actions/workflows/latest.yaml/badge.svg?branch=main) +![Testing](https://proxima.goliath.hu/proxima/discord-bot/actions/workflows/testing.yaml/badge.svg?branch=dev) + +![Proxima Discord bot](../res/logo.svg) + + +## The source code of the bot. + +- [Bootstrap](Bootstrap)
+ The startup files to boot the application. + +- [Bot](Bot) + - [Cache.php](Bot/Cache.php), [CacheItem.php](Bot/CacheItem.php), [ObjectCache.php](Bot/ObjectCache.php)
+ Minimal caching to minimize API calls to the backend. + + - [DevLogger.php](Bot/DevLogger.php)
+ Helper class to log "impossible" events, that should not happen. + The log is written in [JSON](https://www.json.org/) format in the `/app/Bot/Storag/Logs/dev.log` file. + +- [Client](Client)
+ The helper classes to communicate with the backend and the discord client + + - [Models](Client/Models)
+ The data models + + - [Responses](Client/Responses)
+ The responses from the backend + + - [Traits](Client/Traits)
+ Commonly used classes + + - [ApiClient.php](Client/ApiClient.php)
+ The main class to manage all user data and communication with both ends. + + - [ApiResponse.php](Client/ApiResponse.php)
+ Helper class to manage all the communication in one place. + + - [ClientMessages.php](Client/ClientMessages.php)
+ The message templates sent to the discord client. Uses the [Smarty](https://www.smarty.net/) template engine. + + - [Template.php](Client/Template.php)
+ Minimal template "engine" to generate [ANSI](https://gist.github.com/kkrypt0nn/a02506f3712ff2d1c8ca7c9e0aed7c06) colored messages for the discord client. + +- [Commands](Commands)
+ The classes to handle [slash command](https://discord.com/developers/docs/tutorials/upgrading-to-application-commands)s from the discord client. + + - [CreateRemainder.php](Commands/CreateRemainder.php)
+ The `/rem (channel)` command to create a new remainder + + - [EditRemainder.php](Commands/EditRemainder.php)
+ The `/edit (when) (message) (channel)` command to create a new remainder + + - [ListRemainders.php](Commands/ListRemainders.php)
+ The `/list (page)` command to show a paginated list of the current remainders + + - [Profile.php](Commands/Profile.php)
+ The `/profile (timezone) (locale)` command to display/modify the actual users profile + + - [RemoveRemainder.php](Commands/RemoveRemainder.php)
+ The `/delete ` command to remove a remainder (needs confirmation) + +- [Core](Core)
+ The core components of the [commandstring/dphp-bot](https://github.com/CommandString/discordphp-bot-template) package + +- [Events](Events)
+ The main event handling for the discord client + + - [Message.php](Events/Ready.php)
+ Handles all messages comming from the discord client
+ ***NOTE: currentky no custom handling is done here*** + + - [Ready.php](Events/Ready.php)
+ Starts the main remainder pull service when the discord server becomes ready + + +- [Services](Services)
+ The main services to handle background tasks + + - [ReminderService.php](Services/ReminderService.php)
+ Periodically pulls actual remainder from the backend and sends remainders to the discord client + +- [Storage](Storage)
+ Stores temporary files and program logs + +- [Test](Test)
+ A rather scarce list of test, the function testings is handled by an outside service currently + +- [.env.example](.env.example)
+ The sample configuration file to be filled before deploying the bot + +- [Bot.php](Bot.php)
+ The main entrypoint for the bot + +- [BotDev.php](BotDev.php)
+ Main entripoint if not run in container \ No newline at end of file diff --git a/src/Services/ReminderService.php b/src/Services/ReminderService.php new file mode 100644 index 0000000..20e1ac2 --- /dev/null +++ b/src/Services/ReminderService.php @@ -0,0 +1,230 @@ +id, $user->id, $channel->id)); + + $channel->sendMessage("Remainder: $remainder->message")->then( + onFulfilled: function ($result) use (&$remainder, $channel) { + + debug(sprintf('Remainder (%d) sent to specified channel (#%d)', $remainder->id, $channel->id)); + + $this->getApiClient()->updateRemainder($remainder, ['status' => 'finished'])->then( + onFulfilled: function (Response $response) use ($remainder) { + debug(sprintf('Remainder (%d) updated as finished', $remainder->id)); + }, + onRejected: function (Exception $exception) use ($remainder) { + DevLogger::warning( + message: 'Api request failed', + context: [ + 'exception' => $exception, + ] + ); + //TODO: use $this->onLoopReject maybe??? + debug(sprintf('Remainder (%d) update as finished FAILED with message: "%s"', $remainder->id, $exception->getMessage())); + } + ); + }, + onRejected: fn (Exception $exception) => DevLogger::warning( + message: 'Send Message failed', + context: [ + 'exception' => $exception, + 'remainder' => $remainder, + ] + ) + ); + + } + + /** + * Sends a remainder trough the discord api + * + * @param Remainder $remainder The Remainder to send + * + */ + protected function sendRemainder(Remainder $remainder)//: PromiseInterface + { + // get the discord User + $this->getDiscord()->users->fetch($remainder->discord_user->snowflake)->then( + onFulfilled: function (User $user) use (&$remainder) { + + debug(sprintf('Remainder (%d) Discord::User (@%d) found', $remainder->id, $user->id)); + + // if the remainder _DOES_NOT_ have a channel set, get a private channel to the DiscordUser + if (null === $remainder->channel_id) { + $user->getPrivateChannel()->then( + onFulfilled: fn (Channel $channel) => + $this->sendRemainderToChannel($remainder, $channel, $user), + onRejected: fn (Exception $exception) => DevLogger::warning( + message: 'Send Message ot Private channel failed', + context: [ + 'exception' => $exception, + 'remainder' => $remainder, + ] + ) + ); + } else { + $channel = $this->getDiscord()->getChannel($remainder->channel_id); + + // if the channel cannot be found (maybe deleted) or inaccesible (not authorised to see it) + if ($channel === null) { + + $this->getApiClient()->updateRemainder( + remainder: $remainder, + changes: ['status' => 'failed', 'error' => 'Channel not found.'] + )->then( + onFulfilled: function ($result) use (&$remainder) { + DevLogger::warning( + message: 'Remainder had an invalid channel', + context: [ + 'reaminder' => $remainder, + ] + ); + warning('Channel not found event detected. See dev.log for more details.'); + return; + }, + onRejected: fn (Exception $exception) => DevLogger::warning( + message: 'Api request failed', + context: [ + 'exception' => $exception, + ] + ) + ); + } else { + $this->sendRemainderToChannel($remainder, $channel, $user); + } + } + }, + onRejected: function (Exception $exception) use (&$remainder) { + DevLogger::warning( + message: 'Remainder had an invalid user snowflake', + context: [ + 'exception' => $exception, + 'reaminder' => $remainder, + ] + ); + warning('User not found event detected. See dev.log for more details.'); + return; + } + ); + } + // ------------------------------------------------------------------------------------------------------------ + /** + * The periodically called handler + * + * Retrievs the actual remainders from the backend and sends them trough the discord api + * + * @param mixed $timer + * + */ + public function onLoop($timer) + { + + // get actual remaindres + $this->getApiClient()->getActualRemainders()->then( + onFulfilled: function (Response $response) { + + $actualRemainders = RemainderListResponse::make($response); + + // print debug info + debug( + sprintf( + 'Gettnig actual remainders at "%s", got %d remainder.', + Carbon::now('Europe/Budapest'), + count($actualRemainders->remainderList) + ) + ); + + // send each remainder + foreach ($actualRemainders->remainderList as $remiainder) { + $this->getApiClient()->updateRemainder($remiainder, ['status' => 'pending'])->then( + onFulfilled: function (Response $response) use ($remiainder) { + $this->sendRemainder($remiainder); + }, + onRejected: $this->onLoopRejected(false) + ); + } + + }, + onRejected: $this->onLoopRejected(true) + ); + + } + + + /** + * Returns a function to handle Promise onReject + * + * Saves the reject reason to the dev log and optionally send a debug meseg to the output + * + * @param bool $showDebug if true, a debug message is written to the output, otherwise no message + * + * @return callable The function to handle the onReject callback + * + */ + private function onLoopRejected(bool $showDebug = false): callable + { + return function (Exception|ApiResponse $reason) use ($showDebug) { + $keyName = is_a($reason, ApiClient::class) ? 'apiResponse' : 'exception'; + + DevLogger::warning( + message: 'Api request failed', + context: [ + $keyName => $reason, + ] + ); + + if ($showDebug) { + $debugMessage = sprintf( + 'Gettnig actual remainders at "%s", failed, see dev.log for details.', + Carbon::now('Europe/Budapest') + ); + debug($debugMessage); + } + }; + + } + +} diff --git a/src/Storage/.gitignore b/src/Storage/.gitignore new file mode 100644 index 0000000..c96a04f --- /dev/null +++ b/src/Storage/.gitignore @@ -0,0 +1,2 @@ +* +!.gitignore \ No newline at end of file diff --git a/src/TODO b/src/TODO new file mode 100644 index 0000000..e6ffa7b --- /dev/null +++ b/src/TODO @@ -0,0 +1,286 @@ +☐ Add initial script on entry to cehck if: (maybe entrypoint.sh) + //- intl is installed + the storage files ar writable: + - Storage/Logs + - Storage/Smarty/templates_c +# ---- Completed ---- +✔ Create install instructions for folders to create @done(25-04-28 18:21) + +✘ MAYBEE - Add .git folder to image @cancelled(25-04-28 12:39) + OR - remove git version logging from dev log... + NOTE: it works, just there is no git info in the dev log... + NOTE: no real need for it now, during dev it works, otherwise not needed... + +✘ Test to use without intl, define locale names it would save lots of space in both image sizes... @cancelled(25-04-28 12:37) + (locale_get_display_name, etc...) + NOTE: postponed for later + +✔ @critical /profile fails (BOT not serponding) @done(25-04-16 11:53) + if no profile is found - OK + if setting timezone - OK + Your timezone succesfully updated to "Europe/Budapest". + Your local time is: "2025-04-14 12:08:51" + if requesting now - FAIL + if setting timezone - OK + if setting locale - FAIL (autocomplete OK) + if bot is run on Beast, /profile works... (backend still runs on hercules) + Your timezone is: "Europe/Budapest", + Your local time is: "2025-04-14 12:27:27" + Your locale is: "n/a - not defined" + NOTE: Fatal error: Uncaught Error: Call to undefined function locale_get_display_name() + NOTE: updated Dockerfile to start from the default alpine image instead of php + +✔ @critical If there are no remainders, a /list fails with: @done(25-04-16 11:04) + Error: The page 1 is invalid! + Please chose between 1 and 0. + +✔ @high The usage of 'withRejectErrorResponse(false)' is not needed anymore. Remaove it or change parameter to true. @done(25-01-08 11:09) + NOTE: there is no need for the more detailed response, client cannot do anything with that extra information, + it can be useed/handled by the developer... + NOTE: for now, the function is kept in place, only the parameter is set to true. + +✔ @low Run cs-fixer befor deployment to github. @done(25-01-01 13:21) + +✔ @low Cache::getDiscordUser() (and all others) have this description: @done(25-01-01 13:24) + @promise-rejected fn (mixed $reason): void + theretically those will return an Exception or ApiResponse as a reason, maybe narrow down the "mixed" type... + NOTE: things can cheange outside of my code, so this cannot be guarantead, so i keep it this way for now... + +✔ @low ->format('Y-m-d H:i') used in many places, maybe this should be adjustable by the discorduser... @done(24-12-30 14:36) + or maybe put in the .env file + NOTE: added DiscordBot::getDateTimeFormat(), this can be overwritten later if needed... + +✔ @critical Update all browser response handling to make use of responseCodes @done(24-12-30 14:27) + +✔ @critical update all api calls to use 'withRejectErrorResponse(false)' @done(24-12-30 14:27) + +✘ @high Add mode to change default behaviour to send messages to the user. (DM/default channel/etc.) (DM may be not optimal..) @cancelled(24-12-30 14:20) + NOTE: emphemeral messages can only be sent as a reply to an interaction, so the bot cannot sent them... + NOTE: there could be two options: send DM ort send to the channel where the remainder was added by default, + but that can be achieved now, so skip this for now, maybe later if needed... + +✔ @high Add a .env variable to ENABLE RemainderService. @done(24-12-30 14:16) + NOTE: On the first run in the stack, both the backend abd the bot are running, + but the bot needs an api token before it can access the backend, which should be created by the user + _OR_ find a way to automate it + so the current setup steps are: - + - customize .env (pw, url, etc) + - start stack (or even better "docker compose up backend") + - generate backend token + - set beckend token in bot .env + - restart stack (or only the bot) + BUT! in the meantime, the bot logs unauth errors in the dev.log !!! + for deployment ideas see: https://github.com/refactorian/laravel-docker/blob/main/docker-compose.yml + NOTE: did no add switch, created a setup readme instead + detailed setup instruction are in the discord-bot-docker repository + +✘ @medium deprecated warning in the https://github.com/discord-php/DiscordPHP/blob/master/src/Discord/Builders/CommandAttributes.php file line 260 @cancelled(24-12-30 14:15) + NOTE: in the composer, we have a "dev-master" for this package. Why? - master has an exception on start, maybe will dig in later + PHP Deprecated: Creation of dynamic property Discord\Builders\CommandBuilder::$options is deprecated in /mnt/devel/Projects/discord-bot-goliath/src/vendor/team-reflex/discord-php/src/Discord/Builders/CommandAttributes.php on line 260 + NOTE: not my program, the DiscordPHP package + +✘ @low MAYBE make use of the "changes" array of the api results... @cancelled(24-12-29 14:37) + NOTE: backend api updated, so it will return the changes list if anything changes, + currently the bot is not using it, no need for it... + NOTE: maybe later... + +✘ @high Add failApiRequest like without the interaction (like: we get the actual remainders every second) @cancelled(24-12-29 14:36) + NOTE: for now the RemainderService is the only place for them, but with two different datasets, not worth the effort + +✔ @high Clean up testing/dev stuff (USER_VONJAN_ID, devtest command, ReminderService::seedTest, etc...) @done(24-12-29 14:32) + +✔ @today Clean up ReminderService! @done(24-12-29 14:20) + +✔ @low Smarty needs wiritable cache directories !!! @done(24-12-28 14:51) + NOTE: this is a docker building thing!!! + NOTE: added a storage volume and updated the name for smarty (from "Storage" to "storage") + +✘ @low _MAYBE_ convert DiscordBot to HasDiscordBot/HasBot trait... @cancelled(24-12-28 11:12) + NOTE: nope! This way is a clear indication for the responsoibilities of that class, + better than use self/$this for that functionality! + +✘ @low The following code is duplicate inEditRemainder: (autoCompleteMessage, autoCompleteWhen) @cancelled(24-12-28 11:07) + ```php + $remainder = $this->getActualRemainder($interaction, $discordUser->remainders); + + // fail, if the remainder cannot be evaluated + if (false === $remainder) { + $this->invalidRemainderAlias($interaction); + return; + } + ``` + NOTE: Not worth to factor it out to a new function, keeping it as is + + ✘ @low _MAYBE_ add an admin command for maintance... @cancelled(24-12-28 09:52) + NOTE: not really needed, maybe add later... + +✘ @low Add "admin" interface/config/option/etc. to temporary suspend/disable remainderservice (possible even on a running bot) @cancelled(24-12-28 09:52) + NOTE: not really needed, maybe add later... + +✔ @high Make all messages the sam color scema! (/list differs from /profile) @done(24-12-28 09:19) + +✔ @low _MAYBE_ remove bool|null possibilities (from all functions) in Loadable class @done(24-12-27 16:13) + NOTE: in plases, wher it can safeguard from other errors, the null is kept... + +✔ @low Maybe add phpdoc to all functions... @done(24-12-27 16:07) + NOTE: all self coded functions/classes have PHPDoc blocks now, the original template code is untouched... + +✔ @high DevLogger - add exact time for the log! @done(24-12-27 16:06) + NOTE: datetime field already exists... + +✔ @high Add a way to notify the the Bot to invalidate cached data... or set a time limit for the cache... @done(24-12-27 16:04) + NOTE: currently, if on the admin a new remainder is added or an old one is removed/updated/finished/cancelled the cached list does not update... + webhook maybe? or add a message server to the bot... + IDEA: add a server @see: https://reactphp.org/http/#server-usage + NOTE: added ttl to cache (admin modifications are not normal/intended behaviour, user does not have a dedicated dashboard for it), + no reason to overcomplicate it unless the functionality is needed... + +✘ @low _MAYBE_ add more info to DevLogger in DiscordBot::failApiRequest - like method (GET/PUT/etc), url (/remainder-by-due-at/{timestamp}?withDiscordUser) @cancelled(24-12-27 15:57) + to make easier for dev to categorize the problem + IDEA: make an apiClient interface, with all the calls + make normal implementation, without debug + make a debug implementation + on startup, if debug is needed, use the debug version, otherwise the normal one + That needs a new PromiseResponse class, which channels the then() params to the promise then... + TODO: make a new debug class with a getDebugInfo($object) function, which use reflaction to see, if the $object has $debugInfo or not + if it has, return $object->getDebugInfo(), + if not, return [] + NOTE: method/url is not accessable from the response nor from debug_backtrace, it would need way to much time to figure it out, skipping... + +✘ @low Refactor Cache @cancelled(24-12-27 11:17) + - rename "getRemainderList" to "remainders" + - make the {get|store|forget)DiscordUser magic functions, + - rename getDiscordUserBySnowflake to getDiscordUser (snowflake is not used anywhere else!) - DONE + NOTE: a large scale refactoring could be nice, but is to much work for this small project, + so it is not happening now. + basicly the idea is: use like `$this->getCache($discordUser)->forget()` + but that would need to define a remainderList class (instead of the current array), + the DiscordUser->remainders needed to be modified and all occurances it, the list still neededa key in the cache, + which now is the discordUser->id.. + The Cache::getInstance() had to be rewritten, it should return the ObjectCache basesd on the parameter's class, + Iz could only hold 'cacheable' obejcts which needed caching methods defined... + etc... it needed many works with very few or nothing benefits... + Maybe some day... + +✔ @critical WHY IS THE SINGLETON A TRAIT NOT A CLASS ???? WTF ??? @done(24-12-27 09:51) + NOTE: becouse of the late static binding. + It is the "best" way to make this usable. + +✘ @low (BAD IDEA) (MAKE THIS HAPPEN!!! IT IS A COOL WAY!!!) maybe make possible to call Cache::forgetDiscordUser($discordUser); @cancelled(24-12-19 11:37) + or maybe use Cache::getInstance()->forgetDiscordUser($discordUser) + or use HasCache + $this->forgetDiscordUser($discordUser) + magic method __call_static, if methode starts with "get|store|forget" run "return $this->{$methode}($params)" + NOTE: using a static method to access/change the values of the instantiated class is bad practise, can be confusing... + REJECTED! See note above. + +✔ @high Make sure, all commands return an error to the user in case of a failure. (profile return 401, but list does silently fail) @done(24-12-19 11:33) + - list - FAIL - OK! + - delete - autocomplete - FAIL - OK + - edit - autocomplete - FAIL - OK + Done. + +✘ @low AssureTimeZoneSet and it's methods could use better names... @cancelled(24-12-19 11:37) + Nope, it is good enough for now, but renamed part from "TimeZone" to "Timezone". + +✔ @low timezone and timeZone are used as well, maybe simply use "timezone" everywhere... @done(24-12-19 11:26) + Renamed everyhing to timezone. + +✔ @low HasClient and getApiClient() names does not match, maybe rename the trait to HasApiClient... @done(24-12-19 11:17) + Renamed HasClient trait to HasApiClient. + +✔ @high in the PHPDoc (ApiClient) the return type of theonFulfilled() fv should be declared! @done(24-12-19 11:11) + NOTE: added @api-response to the doc (non-standard, just for this purpose) + +✔ @low ClientMessages::listRemaindersCompacted - describe the 'paginate' array fields in the PHPdoc!! @done(24-12-19 10:44) + see: https://stackoverflow.com/questions/15414103/best-way-to-document-array-options-in-phpdoc + NOTE: no standards for this one, it is sort of "compatible", refctor it only if absolutly neccessary. + +✔ @high in the ClientMessages "warning" and "error" are mixed, fix them! @done(24-12-19 10:19) + like: errorDateTimeInThePast says "Worning", but it is an error (?maybe???) + +✔ @low Rename msgToArray to a more appropirate name. @done(24-12-18 10:59) + Renamed to optionChoise. + +✔ @high The "//-------------..." separator lines are different length, make them all the same length!!! (for now 118 is empty) @done(24-12-18 09:19) + +✔ @high All server errors should only show a mininmal error to the discord client, concrete errors should only be sent to the dev log!!! @done(24-12-17 16:01) + - /profile shows a 401 error!!! + NOTE: done, the user sees a "general error", the dev.log is for the operator to handle it + +✔ @high /delete command should handle "-1" as remainder position id - or any non valid value - for the remainder record iindex. @done(24-12-17 15:56) + NOTE: added parameter checking and error handling/reporting, aloso the same for /edit + +✔ @high Paginate /list output (2000 char max/messaged - discord limitation), show x, and if more are there, add a paging @done(24-12-17 14:50) + button bar there... + NOPE! add a header on the /list page (first row: shown 1..20 remainders, page 1/3) + and add an optional page parameter for /list + NOTE: added header info and optional "page" parameter to the command. + +✔ @low RemoveRemainder has a lot in common with EditRemainder, maybe some code could be reused here... @done(24-12-17 08:52) + commonly used code moved into RemainderListCommand trait + +✔ @low errorApiError is the one with "Something went wrong on our side, sorry", @done(24-12-16 09:18) + but this is more like a general response, not only api error response, rename this to general!!! + and update all occurances!!! + NOTE: Renamed the old ApiError to GeneralError, and the old GeneralError to errorDetaiedError, this can be removed... + +✔ @critical The DiscordBot uses the HasDiscord trait, but that requires instantiation of the class, so it is BAAAADDDDD!!! @done(24-12-13 11:50) + Ehh, it is a singleton, so DiscordBot::getInstance()->getDiscord() is technically usable, soooo... + +✔ @high Handle edge cases like if no remainders exists jet (list/edit/delete) @done(24-12-13 11:47) + NOTE: /list added, /dedit and /delete is wip, but this is testing, not functionality... + +✔ @high create structured resopnse for the bot communication @done(24-12-13 11:46) + +✔ @high Remainder->dueAt() Add an ' (UTC)' string to the end of the time if no timezone info was present!!! @done(24-12-13 11:46) + +✔ @critical CreateRemainder: 19:00 megadva, 18:00 van mentve... @done(24-12-12 18:49) + note: in sql, utc is stored, we need to asjust the shown value based on the users timezone... + the Remainder should have an accessor, but for that it needs the DU... + NOTE: /list already uses this, the smarty carbon plugin has a timezone parameter for this, use it!!! + NOTE: copied from /list + +✔ @high Add .env.example @done(24-12-09 20:03) + - is the public-key needed? + - only add variables that are needed! + +✔ @low add tesing for malformed api response. @done(24-11-08 13:46) + NOTE: ApiResponse saves the error ('type' => 'json_decode_error') to the "internalError" property if the parsing fails... + and an DevLogger::error is saved. + +✔ @low maybe create a jsonResponse for the apiClient, which parses/handles error/data/code/stc... @done(24-11-08 13:44) + NOTE: apiClient handles and saves teh status of the response + +✔ @high DiscordUserBySnowflakeResponse is in theory the same as /discord_users/{discord_user} @done(24-11-08 13:36) + eighter make another class for it or rename the current one to match both cases + @crytical DO THIS ! remove DiscordUserBySnowflakeResponse, replace it with DiscordUserResponse + NOTE: done! backend is updated as well... + +✔ @high DevLogger with the magic method and all the phpdoc block, we could have wrote that directly as well... @done(24-11-08 13:34) + The "direct" methode is more readable, but way too redundant and boring, i kepp it this way. + +✔ @high add deleteRemainder command. @done(24-11-08 13:34) + +✔ @critical Add a DEV monolog target, and log errors/warnings there for the dev! @done(24-11-04 09:52) + +✔ @high Add a 'LogAndCall' type function to log failed api requests (like. 422/401/etc) and call the callback @done(24-09-23 10:26) + NOTE: the callback has ben replaced wit a response to interaction + +✔ @today Maybe add 'Content-Type' => 'application/json' as a default header to the ApiClient->client @done(24-09-19 12:17) + +✔ @low MAYBE make ApiClient a singleton as well as the Template... @done(24-09-19 12:11) + +✔ @high Make sure, that the /profile timezone cannot be sent without timezona data, and check for a valid timezone on server side. @done(24-09-11 22:25) + value checking added, + new functionality added to /profile if called without any options + +✔ @high /profile fails if the DU is not found by snowflake, create it first @done(24-09-11 22:24) + __OR__ maybe use that call as a get_or_create ??? + Added put method (update) to the controller (firstOrCreate) + +✔ @high Maybe replace Loadable->nullSafe() with Loadable->toJson() which using nullsafe ans json_encode @done(24-09-11 22:01) + NOTE: implemeted JsonSerializable interface, so json_encode is safe to use wit skipping null properties, + added toJson(bool $unfiltered=false), which can be called with true parameter, to get all properties (even null ones) + +✔ @critical Implement GetOrRegisterDiscordUserBySnowflake on the backend and call that instead of only get !!! @done(24-09-06 15:39) + diff --git a/src/Tests/CommandAttributeTest.php b/src/Tests/CommandAttributeTest.php new file mode 100644 index 0000000..9b7f3eb --- /dev/null +++ b/src/Tests/CommandAttributeTest.php @@ -0,0 +1,24 @@ +expectException(\LogicException::class); + $this->expectExceptionMessage('Guild ID must be alphanumeric'); + + new Command(guild: 'not a snowflake'); + } + + public function testItAcceptsGoodSnowflakes(): void + { + $this->expectNotToPerformAssertions(); + + new Command(guild: '1234567890'); + } +} diff --git a/src/Tests/FunctionsTest.php b/src/Tests/FunctionsTest.php new file mode 100644 index 0000000..74abda8 --- /dev/null +++ b/src/Tests/FunctionsTest.php @@ -0,0 +1,32 @@ +assertEquals([ + 'content' => 'Hello World', + ], $message->jsonSerialize()); + } + + public function testItCreatesAButton(): void + { + $button = newButton(Button::STYLE_DANGER, 'DANGER'); + + $this->assertEquals($button->getLabel(), 'DANGER'); + $this->assertEquals($button->getStyle(), Button::STYLE_DANGER); + + $button = newButton(Button::STYLE_PRIMARY, 'PRIMARY', 'primary_button'); + $this->assertEquals($button->getLabel(), 'PRIMARY'); + $this->assertEquals($button->getStyle(), Button::STYLE_PRIMARY); + $this->assertEquals($button->getCustomId(), 'primary_button'); + } +} diff --git a/src/composer.json b/src/composer.json new file mode 100644 index 0000000..77abe9b --- /dev/null +++ b/src/composer.json @@ -0,0 +1,58 @@ +{ + "require": { + "php": ">=8.1", + "commandstring/utils": "^1.7", + "react/async": "^4.1", + "smarty/smarty": "^5.4", + "team-reflex/discord-php": "dev-master", + "tnapf/env": "^1.1" + }, + "require-dev": { + "ergebnis/composer-normalize": "^2.31", + "fakerphp/faker": "^1.21", + "friendsofphp/php-cs-fixer": "^3.16", + "jetbrains/phpstorm-attributes": "^1.0", + "phpunit/phpunit": "^10.1", + "roave/security-advisories": "dev-latest", + "xheaven/composer-git-hooks": "^3.0" + }, + "autoload": { + "psr-4": { + "Bot\\": "Bot/", + "Client\\": "Client/", + "Commands\\": "Commands/", + "Core\\": "Core/", + "Events\\": "Events/", + "Services\\": "Services/", + "Tests\\": "Tests/" + }, + "files": [ + "Core/functions.php", + "Core/helpers.php" + ] + }, + "config": { + "allow-plugins": { + "ergebnis/composer-normalize": true + }, + "sort-packages": true + }, + "extra": { + "composer-normalize": { + "indent-size": 2, + "indent-style": "space" + }, + "hooks": { + "pre-commit": "composer fix:dry", + "pre-push": "composer test" + } + }, + "scripts": { + "post-autoload-dump": "[ $COMPOSER_DEV_MODE -eq 0 ] || composer normalize", + "fix": "php-cs-fixer fix --using-cache=no", + "fix:dry": "php-cs-fixer fix --using-cache=no --diff --dry-run", + "fix:list": "php-cs-fixer fix --using-cache=no --dry-run", + "test": "phpunit", + "test:coverage": "phpunit --coverage-html .phpunit.cache/cov-html" + } +} diff --git a/src/composer.lock b/src/composer.lock new file mode 100644 index 0000000..f38b657 --- /dev/null +++ b/src/composer.lock @@ -0,0 +1,7448 @@ +{ + "_readme": [ + "This file locks the dependencies of your project to a known state", + "Read more about it at https://getcomposer.org/doc/01-basic-usage.md#installing-dependencies", + "This file is @generated automatically" + ], + "content-hash": "cbafdc36a2e23799c1b07bad702fcaef", + "packages": [ + { + "name": "carbonphp/carbon-doctrine-types", + "version": "3.2.0", + "source": { + "type": "git", + "url": "https://github.com/CarbonPHP/carbon-doctrine-types.git", + "reference": "18ba5ddfec8976260ead6e866180bd5d2f71aa1d" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/CarbonPHP/carbon-doctrine-types/zipball/18ba5ddfec8976260ead6e866180bd5d2f71aa1d", + "reference": "18ba5ddfec8976260ead6e866180bd5d2f71aa1d", + "shasum": "" + }, + "require": { + "php": "^8.1" + }, + "conflict": { + "doctrine/dbal": "<4.0.0 || >=5.0.0" + }, + "require-dev": { + "doctrine/dbal": "^4.0.0", + "nesbot/carbon": "^2.71.0 || ^3.0.0", + "phpunit/phpunit": "^10.3" + }, + "type": "library", + "autoload": { + "psr-4": { + "Carbon\\Doctrine\\": "src/Carbon/Doctrine/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "KyleKatarn", + "email": "kylekatarnls@gmail.com" + } + ], + "description": "Types to use Carbon in Doctrine", + "keywords": [ + "carbon", + "date", + "datetime", + "doctrine", + "time" + ], + "support": { + "issues": "https://github.com/CarbonPHP/carbon-doctrine-types/issues", + "source": "https://github.com/CarbonPHP/carbon-doctrine-types/tree/3.2.0" + }, + "funding": [ + { + "url": "https://github.com/kylekatarnls", + "type": "github" + }, + { + "url": "https://opencollective.com/Carbon", + "type": "open_collective" + }, + { + "url": "https://tidelift.com/funding/github/packagist/nesbot/carbon", + "type": "tidelift" + } + ], + "time": "2024-02-09T16:56:22+00:00" + }, + { + "name": "commandstring/utils", + "version": "v1.7.2", + "source": { + "type": "git", + "url": "https://github.com/CommandString/Utils.git", + "reference": "b07ae426455c34ce654f266779a2f52bb9134432" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/CommandString/Utils/zipball/b07ae426455c34ce654f266779a2f52bb9134432", + "reference": "b07ae426455c34ce654f266779a2f52bb9134432", + "shasum": "" + }, + "require-dev": { + "phpunit/phpunit": "^10", + "squizlabs/php_codesniffer": "^3.7" + }, + "type": "library", + "autoload": { + "psr-4": { + "CommandString\\Utils\\": "src/", + "Tests\\CommandString\\Utils\\": "tests/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "description": "A collection of useful PHP utilities", + "support": { + "issues": "https://github.com/CommandString/Utils/issues", + "source": "https://github.com/CommandString/Utils/tree/v1.7.2" + }, + "time": "2023-05-01T18:29:10+00:00" + }, + { + "name": "discord-php/http", + "version": "v10.3.5", + "source": { + "type": "git", + "url": "https://github.com/discord-php/DiscordPHP-Http.git", + "reference": "6186d74b54d8d3ea2b33b87147bfa53adb23609a" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/discord-php/DiscordPHP-Http/zipball/6186d74b54d8d3ea2b33b87147bfa53adb23609a", + "reference": "6186d74b54d8d3ea2b33b87147bfa53adb23609a", + "shasum": "" + }, + "require": { + "php": "^7.4|^8.0", + "psr/log": "^1.1 || ^2.0 || ^3.0", + "react/http": "^1.2", + "react/promise": "^2.2" + }, + "require-dev": { + "friendsofphp/php-cs-fixer": "^2.17", + "guzzlehttp/guzzle": "^6.0|^7.0", + "mockery/mockery": "^1.5", + "monolog/monolog": "^2.2", + "phpunit/phpunit": "^9.5", + "psy/psysh": "^0.10.6", + "react/async": "^4 || ^3" + }, + "suggest": { + "guzzlehttp/guzzle": "For alternative to ReactPHP/Http Browser" + }, + "type": "library", + "autoload": { + "psr-4": { + "Discord\\Http\\": "src/Discord", + "Tests\\Discord\\Http\\": "tests/Discord" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "David Cole", + "email": "david.cole1340@gmail.com" + } + ], + "description": "Handles HTTP requests to Discord servers", + "support": { + "issues": "https://github.com/discord-php/DiscordPHP-Http/issues", + "source": "https://github.com/discord-php/DiscordPHP-Http/tree/v10.3.5" + }, + "time": "2024-09-29T12:04:58+00:00" + }, + { + "name": "discord/interactions", + "version": "2.2.0", + "source": { + "type": "git", + "url": "https://github.com/discord/discord-interactions-php.git", + "reference": "a6fc0c877b75cf5ff5811f2ea69c5cc4ad6ac457" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/discord/discord-interactions-php/zipball/a6fc0c877b75cf5ff5811f2ea69c5cc4ad6ac457", + "reference": "a6fc0c877b75cf5ff5811f2ea69c5cc4ad6ac457", + "shasum": "" + }, + "conflict": { + "simplito/elliptic-php": "<1.0,>=1.1" + }, + "require-dev": { + "simplito/elliptic-php": "^1.0" + }, + "suggest": { + "simplito/elliptic-php": "Required to validate interaction signatures." + }, + "type": "library", + "autoload": { + "psr-4": { + "Discord\\": "discord" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Ian Webster", + "email": "ianw_php@ianww.com" + } + ], + "description": "Utils for implementing the Discord Interactions API", + "keywords": [ + "discord" + ], + "support": { + "issues": "https://github.com/discord/discord-interactions-php/issues", + "source": "https://github.com/discord/discord-interactions-php/tree/2.2.0" + }, + "time": "2022-02-09T17:58:51+00:00" + }, + { + "name": "evenement/evenement", + "version": "v3.0.2", + "source": { + "type": "git", + "url": "https://github.com/igorw/evenement.git", + "reference": "0a16b0d71ab13284339abb99d9d2bd813640efbc" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/igorw/evenement/zipball/0a16b0d71ab13284339abb99d9d2bd813640efbc", + "reference": "0a16b0d71ab13284339abb99d9d2bd813640efbc", + "shasum": "" + }, + "require": { + "php": ">=7.0" + }, + "require-dev": { + "phpunit/phpunit": "^9 || ^6" + }, + "type": "library", + "autoload": { + "psr-4": { + "Evenement\\": "src/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Igor Wiedler", + "email": "igor@wiedler.ch" + } + ], + "description": "Événement is a very simple event dispatching library for PHP", + "keywords": [ + "event-dispatcher", + "event-emitter" + ], + "support": { + "issues": "https://github.com/igorw/evenement/issues", + "source": "https://github.com/igorw/evenement/tree/v3.0.2" + }, + "time": "2023-08-08T05:53:35+00:00" + }, + { + "name": "fig/http-message-util", + "version": "1.1.5", + "source": { + "type": "git", + "url": "https://github.com/php-fig/http-message-util.git", + "reference": "9d94dc0154230ac39e5bf89398b324a86f63f765" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/php-fig/http-message-util/zipball/9d94dc0154230ac39e5bf89398b324a86f63f765", + "reference": "9d94dc0154230ac39e5bf89398b324a86f63f765", + "shasum": "" + }, + "require": { + "php": "^5.3 || ^7.0 || ^8.0" + }, + "suggest": { + "psr/http-message": "The package containing the PSR-7 interfaces" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "1.1.x-dev" + } + }, + "autoload": { + "psr-4": { + "Fig\\Http\\Message\\": "src/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "PHP-FIG", + "homepage": "https://www.php-fig.org/" + } + ], + "description": "Utility classes and constants for use with PSR-7 (psr/http-message)", + "keywords": [ + "http", + "http-message", + "psr", + "psr-7", + "request", + "response" + ], + "support": { + "issues": "https://github.com/php-fig/http-message-util/issues", + "source": "https://github.com/php-fig/http-message-util/tree/1.1.5" + }, + "time": "2020-11-24T22:02:12+00:00" + }, + { + "name": "guzzlehttp/psr7", + "version": "2.7.0", + "source": { + "type": "git", + "url": "https://github.com/guzzle/psr7.git", + "reference": "a70f5c95fb43bc83f07c9c948baa0dc1829bf201" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/guzzle/psr7/zipball/a70f5c95fb43bc83f07c9c948baa0dc1829bf201", + "reference": "a70f5c95fb43bc83f07c9c948baa0dc1829bf201", + "shasum": "" + }, + "require": { + "php": "^7.2.5 || ^8.0", + "psr/http-factory": "^1.0", + "psr/http-message": "^1.1 || ^2.0", + "ralouphie/getallheaders": "^3.0" + }, + "provide": { + "psr/http-factory-implementation": "1.0", + "psr/http-message-implementation": "1.0" + }, + "require-dev": { + "bamarni/composer-bin-plugin": "^1.8.2", + "http-interop/http-factory-tests": "0.9.0", + "phpunit/phpunit": "^8.5.39 || ^9.6.20" + }, + "suggest": { + "laminas/laminas-httphandlerrunner": "Emit PSR-7 responses" + }, + "type": "library", + "extra": { + "bamarni-bin": { + "bin-links": true, + "forward-command": false + } + }, + "autoload": { + "psr-4": { + "GuzzleHttp\\Psr7\\": "src/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Graham Campbell", + "email": "hello@gjcampbell.co.uk", + "homepage": "https://github.com/GrahamCampbell" + }, + { + "name": "Michael Dowling", + "email": "mtdowling@gmail.com", + "homepage": "https://github.com/mtdowling" + }, + { + "name": "George Mponos", + "email": "gmponos@gmail.com", + "homepage": "https://github.com/gmponos" + }, + { + "name": "Tobias Nyholm", + "email": "tobias.nyholm@gmail.com", + "homepage": "https://github.com/Nyholm" + }, + { + "name": "Márk Sági-Kazár", + "email": "mark.sagikazar@gmail.com", + "homepage": "https://github.com/sagikazarmark" + }, + { + "name": "Tobias Schultze", + "email": "webmaster@tubo-world.de", + "homepage": "https://github.com/Tobion" + }, + { + "name": "Márk Sági-Kazár", + "email": "mark.sagikazar@gmail.com", + "homepage": "https://sagikazarmark.hu" + } + ], + "description": "PSR-7 message implementation that also provides common utility methods", + "keywords": [ + "http", + "message", + "psr-7", + "request", + "response", + "stream", + "uri", + "url" + ], + "support": { + "issues": "https://github.com/guzzle/psr7/issues", + "source": "https://github.com/guzzle/psr7/tree/2.7.0" + }, + "funding": [ + { + "url": "https://github.com/GrahamCampbell", + "type": "github" + }, + { + "url": "https://github.com/Nyholm", + "type": "github" + }, + { + "url": "https://tidelift.com/funding/github/packagist/guzzlehttp/psr7", + "type": "tidelift" + } + ], + "time": "2024-07-18T11:15:46+00:00" + }, + { + "name": "monolog/monolog", + "version": "3.7.0", + "source": { + "type": "git", + "url": "https://github.com/Seldaek/monolog.git", + "reference": "f4393b648b78a5408747de94fca38beb5f7e9ef8" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/Seldaek/monolog/zipball/f4393b648b78a5408747de94fca38beb5f7e9ef8", + "reference": "f4393b648b78a5408747de94fca38beb5f7e9ef8", + "shasum": "" + }, + "require": { + "php": ">=8.1", + "psr/log": "^2.0 || ^3.0" + }, + "provide": { + "psr/log-implementation": "3.0.0" + }, + "require-dev": { + "aws/aws-sdk-php": "^3.0", + "doctrine/couchdb": "~1.0@dev", + "elasticsearch/elasticsearch": "^7 || ^8", + "ext-json": "*", + "graylog2/gelf-php": "^1.4.2 || ^2.0", + "guzzlehttp/guzzle": "^7.4.5", + "guzzlehttp/psr7": "^2.2", + "mongodb/mongodb": "^1.8", + "php-amqplib/php-amqplib": "~2.4 || ^3", + "phpstan/phpstan": "^1.9", + "phpstan/phpstan-deprecation-rules": "^1.0", + "phpstan/phpstan-strict-rules": "^1.4", + "phpunit/phpunit": "^10.5.17", + "predis/predis": "^1.1 || ^2", + "ruflin/elastica": "^7", + "symfony/mailer": "^5.4 || ^6", + "symfony/mime": "^5.4 || ^6" + }, + "suggest": { + "aws/aws-sdk-php": "Allow sending log messages to AWS services like DynamoDB", + "doctrine/couchdb": "Allow sending log messages to a CouchDB server", + "elasticsearch/elasticsearch": "Allow sending log messages to an Elasticsearch server via official client", + "ext-amqp": "Allow sending log messages to an AMQP server (1.0+ required)", + "ext-curl": "Required to send log messages using the IFTTTHandler, the LogglyHandler, the SendGridHandler, the SlackWebhookHandler or the TelegramBotHandler", + "ext-mbstring": "Allow to work properly with unicode symbols", + "ext-mongodb": "Allow sending log messages to a MongoDB server (via driver)", + "ext-openssl": "Required to send log messages using SSL", + "ext-sockets": "Allow sending log messages to a Syslog server (via UDP driver)", + "graylog2/gelf-php": "Allow sending log messages to a GrayLog2 server", + "mongodb/mongodb": "Allow sending log messages to a MongoDB server (via library)", + "php-amqplib/php-amqplib": "Allow sending log messages to an AMQP server using php-amqplib", + "rollbar/rollbar": "Allow sending log messages to Rollbar", + "ruflin/elastica": "Allow sending log messages to an Elastic Search server" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-main": "3.x-dev" + } + }, + "autoload": { + "psr-4": { + "Monolog\\": "src/Monolog" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Jordi Boggiano", + "email": "j.boggiano@seld.be", + "homepage": "https://seld.be" + } + ], + "description": "Sends your logs to files, sockets, inboxes, databases and various web services", + "homepage": "https://github.com/Seldaek/monolog", + "keywords": [ + "log", + "logging", + "psr-3" + ], + "support": { + "issues": "https://github.com/Seldaek/monolog/issues", + "source": "https://github.com/Seldaek/monolog/tree/3.7.0" + }, + "funding": [ + { + "url": "https://github.com/Seldaek", + "type": "github" + }, + { + "url": "https://tidelift.com/funding/github/packagist/monolog/monolog", + "type": "tidelift" + } + ], + "time": "2024-06-28T09:40:51+00:00" + }, + { + "name": "nesbot/carbon", + "version": "3.8.2", + "source": { + "type": "git", + "url": "https://github.com/briannesbitt/Carbon.git", + "reference": "e1268cdbc486d97ce23fef2c666dc3c6b6de9947" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/briannesbitt/Carbon/zipball/e1268cdbc486d97ce23fef2c666dc3c6b6de9947", + "reference": "e1268cdbc486d97ce23fef2c666dc3c6b6de9947", + "shasum": "" + }, + "require": { + "carbonphp/carbon-doctrine-types": "<100.0", + "ext-json": "*", + "php": "^8.1", + "psr/clock": "^1.0", + "symfony/clock": "^6.3 || ^7.0", + "symfony/polyfill-mbstring": "^1.0", + "symfony/translation": "^4.4.18 || ^5.2.1|| ^6.0 || ^7.0" + }, + "provide": { + "psr/clock-implementation": "1.0" + }, + "require-dev": { + "doctrine/dbal": "^3.6.3 || ^4.0", + "doctrine/orm": "^2.15.2 || ^3.0", + "friendsofphp/php-cs-fixer": "^3.57.2", + "kylekatarnls/multi-tester": "^2.5.3", + "ondrejmirtes/better-reflection": "^6.25.0.4", + "phpmd/phpmd": "^2.15.0", + "phpstan/extension-installer": "^1.3.1", + "phpstan/phpstan": "^1.11.2", + "phpunit/phpunit": "^10.5.20", + "squizlabs/php_codesniffer": "^3.9.0" + }, + "bin": [ + "bin/carbon" + ], + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "3.x-dev", + "dev-2.x": "2.x-dev" + }, + "laravel": { + "providers": [ + "Carbon\\Laravel\\ServiceProvider" + ] + }, + "phpstan": { + "includes": [ + "extension.neon" + ] + } + }, + "autoload": { + "psr-4": { + "Carbon\\": "src/Carbon/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Brian Nesbitt", + "email": "brian@nesbot.com", + "homepage": "https://markido.com" + }, + { + "name": "kylekatarnls", + "homepage": "https://github.com/kylekatarnls" + } + ], + "description": "An API extension for DateTime that supports 281 different languages.", + "homepage": "https://carbon.nesbot.com", + "keywords": [ + "date", + "datetime", + "time" + ], + "support": { + "docs": "https://carbon.nesbot.com/docs", + "issues": "https://github.com/briannesbitt/Carbon/issues", + "source": "https://github.com/briannesbitt/Carbon" + }, + "funding": [ + { + "url": "https://github.com/sponsors/kylekatarnls", + "type": "github" + }, + { + "url": "https://opencollective.com/Carbon#sponsor", + "type": "opencollective" + }, + { + "url": "https://tidelift.com/subscription/pkg/packagist-nesbot-carbon?utm_source=packagist-nesbot-carbon&utm_medium=referral&utm_campaign=readme", + "type": "tidelift" + } + ], + "time": "2024-11-07T17:46:48+00:00" + }, + { + "name": "psr/clock", + "version": "1.0.0", + "source": { + "type": "git", + "url": "https://github.com/php-fig/clock.git", + "reference": "e41a24703d4560fd0acb709162f73b8adfc3aa0d" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/php-fig/clock/zipball/e41a24703d4560fd0acb709162f73b8adfc3aa0d", + "reference": "e41a24703d4560fd0acb709162f73b8adfc3aa0d", + "shasum": "" + }, + "require": { + "php": "^7.0 || ^8.0" + }, + "type": "library", + "autoload": { + "psr-4": { + "Psr\\Clock\\": "src/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "PHP-FIG", + "homepage": "https://www.php-fig.org/" + } + ], + "description": "Common interface for reading the clock.", + "homepage": "https://github.com/php-fig/clock", + "keywords": [ + "clock", + "now", + "psr", + "psr-20", + "time" + ], + "support": { + "issues": "https://github.com/php-fig/clock/issues", + "source": "https://github.com/php-fig/clock/tree/1.0.0" + }, + "time": "2022-11-25T14:36:26+00:00" + }, + { + "name": "psr/http-factory", + "version": "1.1.0", + "source": { + "type": "git", + "url": "https://github.com/php-fig/http-factory.git", + "reference": "2b4765fddfe3b508ac62f829e852b1501d3f6e8a" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/php-fig/http-factory/zipball/2b4765fddfe3b508ac62f829e852b1501d3f6e8a", + "reference": "2b4765fddfe3b508ac62f829e852b1501d3f6e8a", + "shasum": "" + }, + "require": { + "php": ">=7.1", + "psr/http-message": "^1.0 || ^2.0" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "1.0.x-dev" + } + }, + "autoload": { + "psr-4": { + "Psr\\Http\\Message\\": "src/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "PHP-FIG", + "homepage": "https://www.php-fig.org/" + } + ], + "description": "PSR-17: Common interfaces for PSR-7 HTTP message factories", + "keywords": [ + "factory", + "http", + "message", + "psr", + "psr-17", + "psr-7", + "request", + "response" + ], + "support": { + "source": "https://github.com/php-fig/http-factory" + }, + "time": "2024-04-15T12:06:14+00:00" + }, + { + "name": "psr/http-message", + "version": "1.1", + "source": { + "type": "git", + "url": "https://github.com/php-fig/http-message.git", + "reference": "cb6ce4845ce34a8ad9e68117c10ee90a29919eba" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/php-fig/http-message/zipball/cb6ce4845ce34a8ad9e68117c10ee90a29919eba", + "reference": "cb6ce4845ce34a8ad9e68117c10ee90a29919eba", + "shasum": "" + }, + "require": { + "php": "^7.2 || ^8.0" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "1.1.x-dev" + } + }, + "autoload": { + "psr-4": { + "Psr\\Http\\Message\\": "src/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "PHP-FIG", + "homepage": "http://www.php-fig.org/" + } + ], + "description": "Common interface for HTTP messages", + "homepage": "https://github.com/php-fig/http-message", + "keywords": [ + "http", + "http-message", + "psr", + "psr-7", + "request", + "response" + ], + "support": { + "source": "https://github.com/php-fig/http-message/tree/1.1" + }, + "time": "2023-04-04T09:50:52+00:00" + }, + { + "name": "psr/log", + "version": "3.0.2", + "source": { + "type": "git", + "url": "https://github.com/php-fig/log.git", + "reference": "f16e1d5863e37f8d8c2a01719f5b34baa2b714d3" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/php-fig/log/zipball/f16e1d5863e37f8d8c2a01719f5b34baa2b714d3", + "reference": "f16e1d5863e37f8d8c2a01719f5b34baa2b714d3", + "shasum": "" + }, + "require": { + "php": ">=8.0.0" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "3.x-dev" + } + }, + "autoload": { + "psr-4": { + "Psr\\Log\\": "src" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "PHP-FIG", + "homepage": "https://www.php-fig.org/" + } + ], + "description": "Common interface for logging libraries", + "homepage": "https://github.com/php-fig/log", + "keywords": [ + "log", + "psr", + "psr-3" + ], + "support": { + "source": "https://github.com/php-fig/log/tree/3.0.2" + }, + "time": "2024-09-11T13:17:53+00:00" + }, + { + "name": "psr/simple-cache", + "version": "3.0.0", + "source": { + "type": "git", + "url": "https://github.com/php-fig/simple-cache.git", + "reference": "764e0b3939f5ca87cb904f570ef9be2d78a07865" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/php-fig/simple-cache/zipball/764e0b3939f5ca87cb904f570ef9be2d78a07865", + "reference": "764e0b3939f5ca87cb904f570ef9be2d78a07865", + "shasum": "" + }, + "require": { + "php": ">=8.0.0" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "3.0.x-dev" + } + }, + "autoload": { + "psr-4": { + "Psr\\SimpleCache\\": "src/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "PHP-FIG", + "homepage": "https://www.php-fig.org/" + } + ], + "description": "Common interfaces for simple caching", + "keywords": [ + "cache", + "caching", + "psr", + "psr-16", + "simple-cache" + ], + "support": { + "source": "https://github.com/php-fig/simple-cache/tree/3.0.0" + }, + "time": "2021-10-29T13:26:27+00:00" + }, + { + "name": "ralouphie/getallheaders", + "version": "3.0.3", + "source": { + "type": "git", + "url": "https://github.com/ralouphie/getallheaders.git", + "reference": "120b605dfeb996808c31b6477290a714d356e822" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/ralouphie/getallheaders/zipball/120b605dfeb996808c31b6477290a714d356e822", + "reference": "120b605dfeb996808c31b6477290a714d356e822", + "shasum": "" + }, + "require": { + "php": ">=5.6" + }, + "require-dev": { + "php-coveralls/php-coveralls": "^2.1", + "phpunit/phpunit": "^5 || ^6.5" + }, + "type": "library", + "autoload": { + "files": [ + "src/getallheaders.php" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Ralph Khattar", + "email": "ralph.khattar@gmail.com" + } + ], + "description": "A polyfill for getallheaders.", + "support": { + "issues": "https://github.com/ralouphie/getallheaders/issues", + "source": "https://github.com/ralouphie/getallheaders/tree/develop" + }, + "time": "2019-03-08T08:55:37+00:00" + }, + { + "name": "ratchet/pawl", + "version": "v0.4.1", + "source": { + "type": "git", + "url": "https://github.com/ratchetphp/Pawl.git", + "reference": "af70198bab77a582b31169d3cc3982bed25c161f" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/ratchetphp/Pawl/zipball/af70198bab77a582b31169d3cc3982bed25c161f", + "reference": "af70198bab77a582b31169d3cc3982bed25c161f", + "shasum": "" + }, + "require": { + "evenement/evenement": "^3.0 || ^2.0", + "guzzlehttp/psr7": "^2.0 || ^1.7", + "php": ">=5.4", + "ratchet/rfc6455": "^0.3.1", + "react/socket": "^1.9" + }, + "require-dev": { + "phpunit/phpunit": "^9.3 || ^5.7 || ^4.8" + }, + "suggest": { + "reactivex/rxphp": "~2.0" + }, + "type": "library", + "autoload": { + "files": [ + "src/functions_include.php" + ], + "psr-4": { + "Ratchet\\Client\\": "src" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "description": "Asynchronous WebSocket client", + "keywords": [ + "Ratchet", + "async", + "client", + "websocket", + "websocket client" + ], + "support": { + "issues": "https://github.com/ratchetphp/Pawl/issues", + "source": "https://github.com/ratchetphp/Pawl/tree/v0.4.1" + }, + "time": "2021-12-10T14:32:34+00:00" + }, + { + "name": "ratchet/rfc6455", + "version": "v0.3.1", + "source": { + "type": "git", + "url": "https://github.com/ratchetphp/RFC6455.git", + "reference": "7c964514e93456a52a99a20fcfa0de242a43ccdb" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/ratchetphp/RFC6455/zipball/7c964514e93456a52a99a20fcfa0de242a43ccdb", + "reference": "7c964514e93456a52a99a20fcfa0de242a43ccdb", + "shasum": "" + }, + "require": { + "guzzlehttp/psr7": "^2 || ^1.7", + "php": ">=5.4.2" + }, + "require-dev": { + "phpunit/phpunit": "^5.7", + "react/socket": "^1.3" + }, + "type": "library", + "autoload": { + "psr-4": { + "Ratchet\\RFC6455\\": "src" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Chris Boden", + "email": "cboden@gmail.com", + "role": "Developer" + }, + { + "name": "Matt Bonneau", + "role": "Developer" + } + ], + "description": "RFC6455 WebSocket protocol handler", + "homepage": "http://socketo.me", + "keywords": [ + "WebSockets", + "rfc6455", + "websocket" + ], + "support": { + "chat": "https://gitter.im/reactphp/reactphp", + "issues": "https://github.com/ratchetphp/RFC6455/issues", + "source": "https://github.com/ratchetphp/RFC6455/tree/v0.3.1" + }, + "time": "2021-12-09T23:20:49+00:00" + }, + { + "name": "react/async", + "version": "v4.3.0", + "source": { + "type": "git", + "url": "https://github.com/reactphp/async.git", + "reference": "635d50e30844a484495713e8cb8d9e079c0008a5" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/reactphp/async/zipball/635d50e30844a484495713e8cb8d9e079c0008a5", + "reference": "635d50e30844a484495713e8cb8d9e079c0008a5", + "shasum": "" + }, + "require": { + "php": ">=8.1", + "react/event-loop": "^1.2", + "react/promise": "^3.2 || ^2.8 || ^1.2.1" + }, + "require-dev": { + "phpstan/phpstan": "1.10.39", + "phpunit/phpunit": "^9.6" + }, + "type": "library", + "autoload": { + "files": [ + "src/functions_include.php" + ], + "psr-4": { + "React\\Async\\": "src/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Christian Lück", + "email": "christian@clue.engineering", + "homepage": "https://clue.engineering/" + }, + { + "name": "Cees-Jan Kiewiet", + "email": "reactphp@ceesjankiewiet.nl", + "homepage": "https://wyrihaximus.net/" + }, + { + "name": "Jan Sorgalla", + "email": "jsorgalla@gmail.com", + "homepage": "https://sorgalla.com/" + }, + { + "name": "Chris Boden", + "email": "cboden@gmail.com", + "homepage": "https://cboden.dev/" + } + ], + "description": "Async utilities and fibers for ReactPHP", + "keywords": [ + "async", + "reactphp" + ], + "support": { + "issues": "https://github.com/reactphp/async/issues", + "source": "https://github.com/reactphp/async/tree/v4.3.0" + }, + "funding": [ + { + "url": "https://opencollective.com/reactphp", + "type": "open_collective" + } + ], + "time": "2024-06-04T14:40:02+00:00" + }, + { + "name": "react/cache", + "version": "v1.2.0", + "source": { + "type": "git", + "url": "https://github.com/reactphp/cache.git", + "reference": "d47c472b64aa5608225f47965a484b75c7817d5b" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/reactphp/cache/zipball/d47c472b64aa5608225f47965a484b75c7817d5b", + "reference": "d47c472b64aa5608225f47965a484b75c7817d5b", + "shasum": "" + }, + "require": { + "php": ">=5.3.0", + "react/promise": "^3.0 || ^2.0 || ^1.1" + }, + "require-dev": { + "phpunit/phpunit": "^9.5 || ^5.7 || ^4.8.35" + }, + "type": "library", + "autoload": { + "psr-4": { + "React\\Cache\\": "src/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Christian Lück", + "email": "christian@clue.engineering", + "homepage": "https://clue.engineering/" + }, + { + "name": "Cees-Jan Kiewiet", + "email": "reactphp@ceesjankiewiet.nl", + "homepage": "https://wyrihaximus.net/" + }, + { + "name": "Jan Sorgalla", + "email": "jsorgalla@gmail.com", + "homepage": "https://sorgalla.com/" + }, + { + "name": "Chris Boden", + "email": "cboden@gmail.com", + "homepage": "https://cboden.dev/" + } + ], + "description": "Async, Promise-based cache interface for ReactPHP", + "keywords": [ + "cache", + "caching", + "promise", + "reactphp" + ], + "support": { + "issues": "https://github.com/reactphp/cache/issues", + "source": "https://github.com/reactphp/cache/tree/v1.2.0" + }, + "funding": [ + { + "url": "https://opencollective.com/reactphp", + "type": "open_collective" + } + ], + "time": "2022-11-30T15:59:55+00:00" + }, + { + "name": "react/child-process", + "version": "v0.6.5", + "source": { + "type": "git", + "url": "https://github.com/reactphp/child-process.git", + "reference": "e71eb1aa55f057c7a4a0d08d06b0b0a484bead43" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/reactphp/child-process/zipball/e71eb1aa55f057c7a4a0d08d06b0b0a484bead43", + "reference": "e71eb1aa55f057c7a4a0d08d06b0b0a484bead43", + "shasum": "" + }, + "require": { + "evenement/evenement": "^3.0 || ^2.0 || ^1.0", + "php": ">=5.3.0", + "react/event-loop": "^1.2", + "react/stream": "^1.2" + }, + "require-dev": { + "phpunit/phpunit": "^9.3 || ^5.7 || ^4.8.35", + "react/socket": "^1.8", + "sebastian/environment": "^5.0 || ^3.0 || ^2.0 || ^1.0" + }, + "type": "library", + "autoload": { + "psr-4": { + "React\\ChildProcess\\": "src" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Christian Lück", + "email": "christian@clue.engineering", + "homepage": "https://clue.engineering/" + }, + { + "name": "Cees-Jan Kiewiet", + "email": "reactphp@ceesjankiewiet.nl", + "homepage": "https://wyrihaximus.net/" + }, + { + "name": "Jan Sorgalla", + "email": "jsorgalla@gmail.com", + "homepage": "https://sorgalla.com/" + }, + { + "name": "Chris Boden", + "email": "cboden@gmail.com", + "homepage": "https://cboden.dev/" + } + ], + "description": "Event-driven library for executing child processes with ReactPHP.", + "keywords": [ + "event-driven", + "process", + "reactphp" + ], + "support": { + "issues": "https://github.com/reactphp/child-process/issues", + "source": "https://github.com/reactphp/child-process/tree/v0.6.5" + }, + "funding": [ + { + "url": "https://github.com/WyriHaximus", + "type": "github" + }, + { + "url": "https://github.com/clue", + "type": "github" + } + ], + "time": "2022-09-16T13:41:56+00:00" + }, + { + "name": "react/datagram", + "version": "v1.10.0", + "source": { + "type": "git", + "url": "https://github.com/reactphp/datagram.git", + "reference": "9236e1f5a67a6029be17d551e9858c487836c301" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/reactphp/datagram/zipball/9236e1f5a67a6029be17d551e9858c487836c301", + "reference": "9236e1f5a67a6029be17d551e9858c487836c301", + "shasum": "" + }, + "require": { + "evenement/evenement": "^3.0 || ^2.0 || ^1.0", + "php": ">=5.3", + "react/dns": "^1.13", + "react/event-loop": "^1.2", + "react/promise": "^3.2 || ^2.1 || ^1.2" + }, + "require-dev": { + "phpunit/phpunit": "^9.6 || ^5.7 || ^4.8.36", + "react/async": "^4.3 || ^3 || ^2" + }, + "type": "library", + "autoload": { + "psr-4": { + "React\\Datagram\\": "src/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Christian Lück", + "email": "christian@clue.engineering", + "homepage": "https://clue.engineering/" + }, + { + "name": "Cees-Jan Kiewiet", + "email": "reactphp@ceesjankiewiet.nl", + "homepage": "https://wyrihaximus.net/" + }, + { + "name": "Jan Sorgalla", + "email": "jsorgalla@gmail.com", + "homepage": "https://sorgalla.com/" + }, + { + "name": "Chris Boden", + "email": "cboden@gmail.com", + "homepage": "https://cboden.dev/" + } + ], + "description": "Event-driven UDP datagram socket client and server for ReactPHP", + "homepage": "https://github.com/reactphp/datagram", + "keywords": [ + "Socket", + "async", + "client", + "datagram", + "dgram", + "reactphp", + "server", + "udp" + ], + "support": { + "issues": "https://github.com/reactphp/datagram/issues", + "source": "https://github.com/reactphp/datagram/tree/v1.10.0" + }, + "funding": [ + { + "url": "https://opencollective.com/reactphp", + "type": "open_collective" + } + ], + "time": "2024-09-06T11:22:35+00:00" + }, + { + "name": "react/dns", + "version": "v1.13.0", + "source": { + "type": "git", + "url": "https://github.com/reactphp/dns.git", + "reference": "eb8ae001b5a455665c89c1df97f6fb682f8fb0f5" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/reactphp/dns/zipball/eb8ae001b5a455665c89c1df97f6fb682f8fb0f5", + "reference": "eb8ae001b5a455665c89c1df97f6fb682f8fb0f5", + "shasum": "" + }, + "require": { + "php": ">=5.3.0", + "react/cache": "^1.0 || ^0.6 || ^0.5", + "react/event-loop": "^1.2", + "react/promise": "^3.2 || ^2.7 || ^1.2.1" + }, + "require-dev": { + "phpunit/phpunit": "^9.6 || ^5.7 || ^4.8.36", + "react/async": "^4.3 || ^3 || ^2", + "react/promise-timer": "^1.11" + }, + "type": "library", + "autoload": { + "psr-4": { + "React\\Dns\\": "src/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Christian Lück", + "email": "christian@clue.engineering", + "homepage": "https://clue.engineering/" + }, + { + "name": "Cees-Jan Kiewiet", + "email": "reactphp@ceesjankiewiet.nl", + "homepage": "https://wyrihaximus.net/" + }, + { + "name": "Jan Sorgalla", + "email": "jsorgalla@gmail.com", + "homepage": "https://sorgalla.com/" + }, + { + "name": "Chris Boden", + "email": "cboden@gmail.com", + "homepage": "https://cboden.dev/" + } + ], + "description": "Async DNS resolver for ReactPHP", + "keywords": [ + "async", + "dns", + "dns-resolver", + "reactphp" + ], + "support": { + "issues": "https://github.com/reactphp/dns/issues", + "source": "https://github.com/reactphp/dns/tree/v1.13.0" + }, + "funding": [ + { + "url": "https://opencollective.com/reactphp", + "type": "open_collective" + } + ], + "time": "2024-06-13T14:18:03+00:00" + }, + { + "name": "react/event-loop", + "version": "v1.5.0", + "source": { + "type": "git", + "url": "https://github.com/reactphp/event-loop.git", + "reference": "bbe0bd8c51ffc05ee43f1729087ed3bdf7d53354" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/reactphp/event-loop/zipball/bbe0bd8c51ffc05ee43f1729087ed3bdf7d53354", + "reference": "bbe0bd8c51ffc05ee43f1729087ed3bdf7d53354", + "shasum": "" + }, + "require": { + "php": ">=5.3.0" + }, + "require-dev": { + "phpunit/phpunit": "^9.6 || ^5.7 || ^4.8.36" + }, + "suggest": { + "ext-pcntl": "For signal handling support when using the StreamSelectLoop" + }, + "type": "library", + "autoload": { + "psr-4": { + "React\\EventLoop\\": "src/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Christian Lück", + "email": "christian@clue.engineering", + "homepage": "https://clue.engineering/" + }, + { + "name": "Cees-Jan Kiewiet", + "email": "reactphp@ceesjankiewiet.nl", + "homepage": "https://wyrihaximus.net/" + }, + { + "name": "Jan Sorgalla", + "email": "jsorgalla@gmail.com", + "homepage": "https://sorgalla.com/" + }, + { + "name": "Chris Boden", + "email": "cboden@gmail.com", + "homepage": "https://cboden.dev/" + } + ], + "description": "ReactPHP's core reactor event loop that libraries can use for evented I/O.", + "keywords": [ + "asynchronous", + "event-loop" + ], + "support": { + "issues": "https://github.com/reactphp/event-loop/issues", + "source": "https://github.com/reactphp/event-loop/tree/v1.5.0" + }, + "funding": [ + { + "url": "https://opencollective.com/reactphp", + "type": "open_collective" + } + ], + "time": "2023-11-13T13:48:05+00:00" + }, + { + "name": "react/http", + "version": "v1.10.0", + "source": { + "type": "git", + "url": "https://github.com/reactphp/http.git", + "reference": "8111281ee57f22b7194f5dba225e609ba7ce4d20" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/reactphp/http/zipball/8111281ee57f22b7194f5dba225e609ba7ce4d20", + "reference": "8111281ee57f22b7194f5dba225e609ba7ce4d20", + "shasum": "" + }, + "require": { + "evenement/evenement": "^3.0 || ^2.0 || ^1.0", + "fig/http-message-util": "^1.1", + "php": ">=5.3.0", + "psr/http-message": "^1.0", + "react/event-loop": "^1.2", + "react/promise": "^3 || ^2.3 || ^1.2.1", + "react/socket": "^1.12", + "react/stream": "^1.2" + }, + "require-dev": { + "clue/http-proxy-react": "^1.8", + "clue/reactphp-ssh-proxy": "^1.4", + "clue/socks-react": "^1.4", + "phpunit/phpunit": "^9.6 || ^5.7 || ^4.8.36", + "react/async": "^4 || ^3 || ^2", + "react/promise-stream": "^1.4", + "react/promise-timer": "^1.9" + }, + "type": "library", + "autoload": { + "psr-4": { + "React\\Http\\": "src/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Christian Lück", + "email": "christian@clue.engineering", + "homepage": "https://clue.engineering/" + }, + { + "name": "Cees-Jan Kiewiet", + "email": "reactphp@ceesjankiewiet.nl", + "homepage": "https://wyrihaximus.net/" + }, + { + "name": "Jan Sorgalla", + "email": "jsorgalla@gmail.com", + "homepage": "https://sorgalla.com/" + }, + { + "name": "Chris Boden", + "email": "cboden@gmail.com", + "homepage": "https://cboden.dev/" + } + ], + "description": "Event-driven, streaming HTTP client and server implementation for ReactPHP", + "keywords": [ + "async", + "client", + "event-driven", + "http", + "http client", + "http server", + "https", + "psr-7", + "reactphp", + "server", + "streaming" + ], + "support": { + "issues": "https://github.com/reactphp/http/issues", + "source": "https://github.com/reactphp/http/tree/v1.10.0" + }, + "funding": [ + { + "url": "https://opencollective.com/reactphp", + "type": "open_collective" + } + ], + "time": "2024-03-27T17:20:46+00:00" + }, + { + "name": "react/promise", + "version": "v2.11.0", + "source": { + "type": "git", + "url": "https://github.com/reactphp/promise.git", + "reference": "1a8460931ea36dc5c76838fec5734d55c88c6831" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/reactphp/promise/zipball/1a8460931ea36dc5c76838fec5734d55c88c6831", + "reference": "1a8460931ea36dc5c76838fec5734d55c88c6831", + "shasum": "" + }, + "require": { + "php": ">=5.4.0" + }, + "require-dev": { + "phpunit/phpunit": "^9.6 || ^5.7 || ^4.8.36" + }, + "type": "library", + "autoload": { + "files": [ + "src/functions_include.php" + ], + "psr-4": { + "React\\Promise\\": "src/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Jan Sorgalla", + "email": "jsorgalla@gmail.com", + "homepage": "https://sorgalla.com/" + }, + { + "name": "Christian Lück", + "email": "christian@clue.engineering", + "homepage": "https://clue.engineering/" + }, + { + "name": "Cees-Jan Kiewiet", + "email": "reactphp@ceesjankiewiet.nl", + "homepage": "https://wyrihaximus.net/" + }, + { + "name": "Chris Boden", + "email": "cboden@gmail.com", + "homepage": "https://cboden.dev/" + } + ], + "description": "A lightweight implementation of CommonJS Promises/A for PHP", + "keywords": [ + "promise", + "promises" + ], + "support": { + "issues": "https://github.com/reactphp/promise/issues", + "source": "https://github.com/reactphp/promise/tree/v2.11.0" + }, + "funding": [ + { + "url": "https://opencollective.com/reactphp", + "type": "open_collective" + } + ], + "time": "2023-11-16T16:16:50+00:00" + }, + { + "name": "react/socket", + "version": "v1.16.0", + "source": { + "type": "git", + "url": "https://github.com/reactphp/socket.git", + "reference": "23e4ff33ea3e160d2d1f59a0e6050e4b0fb0eac1" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/reactphp/socket/zipball/23e4ff33ea3e160d2d1f59a0e6050e4b0fb0eac1", + "reference": "23e4ff33ea3e160d2d1f59a0e6050e4b0fb0eac1", + "shasum": "" + }, + "require": { + "evenement/evenement": "^3.0 || ^2.0 || ^1.0", + "php": ">=5.3.0", + "react/dns": "^1.13", + "react/event-loop": "^1.2", + "react/promise": "^3.2 || ^2.6 || ^1.2.1", + "react/stream": "^1.4" + }, + "require-dev": { + "phpunit/phpunit": "^9.6 || ^5.7 || ^4.8.36", + "react/async": "^4.3 || ^3.3 || ^2", + "react/promise-stream": "^1.4", + "react/promise-timer": "^1.11" + }, + "type": "library", + "autoload": { + "psr-4": { + "React\\Socket\\": "src/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Christian Lück", + "email": "christian@clue.engineering", + "homepage": "https://clue.engineering/" + }, + { + "name": "Cees-Jan Kiewiet", + "email": "reactphp@ceesjankiewiet.nl", + "homepage": "https://wyrihaximus.net/" + }, + { + "name": "Jan Sorgalla", + "email": "jsorgalla@gmail.com", + "homepage": "https://sorgalla.com/" + }, + { + "name": "Chris Boden", + "email": "cboden@gmail.com", + "homepage": "https://cboden.dev/" + } + ], + "description": "Async, streaming plaintext TCP/IP and secure TLS socket server and client connections for ReactPHP", + "keywords": [ + "Connection", + "Socket", + "async", + "reactphp", + "stream" + ], + "support": { + "issues": "https://github.com/reactphp/socket/issues", + "source": "https://github.com/reactphp/socket/tree/v1.16.0" + }, + "funding": [ + { + "url": "https://opencollective.com/reactphp", + "type": "open_collective" + } + ], + "time": "2024-07-26T10:38:09+00:00" + }, + { + "name": "react/stream", + "version": "v1.4.0", + "source": { + "type": "git", + "url": "https://github.com/reactphp/stream.git", + "reference": "1e5b0acb8fe55143b5b426817155190eb6f5b18d" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/reactphp/stream/zipball/1e5b0acb8fe55143b5b426817155190eb6f5b18d", + "reference": "1e5b0acb8fe55143b5b426817155190eb6f5b18d", + "shasum": "" + }, + "require": { + "evenement/evenement": "^3.0 || ^2.0 || ^1.0", + "php": ">=5.3.8", + "react/event-loop": "^1.2" + }, + "require-dev": { + "clue/stream-filter": "~1.2", + "phpunit/phpunit": "^9.6 || ^5.7 || ^4.8.36" + }, + "type": "library", + "autoload": { + "psr-4": { + "React\\Stream\\": "src/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Christian Lück", + "email": "christian@clue.engineering", + "homepage": "https://clue.engineering/" + }, + { + "name": "Cees-Jan Kiewiet", + "email": "reactphp@ceesjankiewiet.nl", + "homepage": "https://wyrihaximus.net/" + }, + { + "name": "Jan Sorgalla", + "email": "jsorgalla@gmail.com", + "homepage": "https://sorgalla.com/" + }, + { + "name": "Chris Boden", + "email": "cboden@gmail.com", + "homepage": "https://cboden.dev/" + } + ], + "description": "Event-driven readable and writable streams for non-blocking I/O in ReactPHP", + "keywords": [ + "event-driven", + "io", + "non-blocking", + "pipe", + "reactphp", + "readable", + "stream", + "writable" + ], + "support": { + "issues": "https://github.com/reactphp/stream/issues", + "source": "https://github.com/reactphp/stream/tree/v1.4.0" + }, + "funding": [ + { + "url": "https://opencollective.com/reactphp", + "type": "open_collective" + } + ], + "time": "2024-06-11T12:45:25+00:00" + }, + { + "name": "smarty/smarty", + "version": "v5.4.4", + "source": { + "type": "git", + "url": "https://github.com/smarty-php/smarty.git", + "reference": "df4795b803df99645abaebca57db65442754246d" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/smarty-php/smarty/zipball/df4795b803df99645abaebca57db65442754246d", + "reference": "df4795b803df99645abaebca57db65442754246d", + "shasum": "" + }, + "require": { + "php": "^7.2 || ^8.0", + "symfony/polyfill-mbstring": "^1.27" + }, + "require-dev": { + "phpunit/phpunit": "^8.5 || ^7.5", + "smarty/smarty-lexer": "^4.0.2" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "5.0.x-dev" + } + }, + "autoload": { + "files": [ + "src/functions.php" + ], + "psr-4": { + "Smarty\\": "src/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "LGPL-3.0" + ], + "authors": [ + { + "name": "Monte Ohrt", + "email": "monte@ohrt.com" + }, + { + "name": "Uwe Tews", + "email": "uwe.tews@googlemail.com" + }, + { + "name": "Rodney Rehm", + "email": "rodney.rehm@medialize.de" + }, + { + "name": "Simon Wisselink", + "homepage": "https://www.iwink.nl/" + } + ], + "description": "Smarty - the compiling PHP template engine", + "homepage": "https://smarty-php.github.io/smarty/", + "keywords": [ + "templating" + ], + "support": { + "forum": "https://github.com/smarty-php/smarty/discussions", + "issues": "https://github.com/smarty-php/smarty/issues", + "source": "https://github.com/smarty-php/smarty/tree/v5.4.4" + }, + "funding": [ + { + "url": "https://github.com/wisskid", + "type": "github" + } + ], + "time": "2025-04-13T20:12:10+00:00" + }, + { + "name": "symfony/clock", + "version": "v7.1.6", + "source": { + "type": "git", + "url": "https://github.com/symfony/clock.git", + "reference": "97bebc53548684c17ed696bc8af016880f0f098d" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/symfony/clock/zipball/97bebc53548684c17ed696bc8af016880f0f098d", + "reference": "97bebc53548684c17ed696bc8af016880f0f098d", + "shasum": "" + }, + "require": { + "php": ">=8.2", + "psr/clock": "^1.0", + "symfony/polyfill-php83": "^1.28" + }, + "provide": { + "psr/clock-implementation": "1.0" + }, + "type": "library", + "autoload": { + "files": [ + "Resources/now.php" + ], + "psr-4": { + "Symfony\\Component\\Clock\\": "" + }, + "exclude-from-classmap": [ + "/Tests/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Nicolas Grekas", + "email": "p@tchwork.com" + }, + { + "name": "Symfony Community", + "homepage": "https://symfony.com/contributors" + } + ], + "description": "Decouples applications from the system clock", + "homepage": "https://symfony.com", + "keywords": [ + "clock", + "psr20", + "time" + ], + "support": { + "source": "https://github.com/symfony/clock/tree/v7.1.6" + }, + "funding": [ + { + "url": "https://symfony.com/sponsor", + "type": "custom" + }, + { + "url": "https://github.com/fabpot", + "type": "github" + }, + { + "url": "https://tidelift.com/funding/github/packagist/symfony/symfony", + "type": "tidelift" + } + ], + "time": "2024-09-25T14:20:29+00:00" + }, + { + "name": "symfony/deprecation-contracts", + "version": "v3.5.0", + "source": { + "type": "git", + "url": "https://github.com/symfony/deprecation-contracts.git", + "reference": "0e0d29ce1f20deffb4ab1b016a7257c4f1e789a1" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/symfony/deprecation-contracts/zipball/0e0d29ce1f20deffb4ab1b016a7257c4f1e789a1", + "reference": "0e0d29ce1f20deffb4ab1b016a7257c4f1e789a1", + "shasum": "" + }, + "require": { + "php": ">=8.1" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-main": "3.5-dev" + }, + "thanks": { + "name": "symfony/contracts", + "url": "https://github.com/symfony/contracts" + } + }, + "autoload": { + "files": [ + "function.php" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Nicolas Grekas", + "email": "p@tchwork.com" + }, + { + "name": "Symfony Community", + "homepage": "https://symfony.com/contributors" + } + ], + "description": "A generic function and convention to trigger deprecation notices", + "homepage": "https://symfony.com", + "support": { + "source": "https://github.com/symfony/deprecation-contracts/tree/v3.5.0" + }, + "funding": [ + { + "url": "https://symfony.com/sponsor", + "type": "custom" + }, + { + "url": "https://github.com/fabpot", + "type": "github" + }, + { + "url": "https://tidelift.com/funding/github/packagist/symfony/symfony", + "type": "tidelift" + } + ], + "time": "2024-04-18T09:32:20+00:00" + }, + { + "name": "symfony/options-resolver", + "version": "v7.1.6", + "source": { + "type": "git", + "url": "https://github.com/symfony/options-resolver.git", + "reference": "85e95eeede2d41cd146146e98c9c81d9214cae85" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/symfony/options-resolver/zipball/85e95eeede2d41cd146146e98c9c81d9214cae85", + "reference": "85e95eeede2d41cd146146e98c9c81d9214cae85", + "shasum": "" + }, + "require": { + "php": ">=8.2", + "symfony/deprecation-contracts": "^2.5|^3" + }, + "type": "library", + "autoload": { + "psr-4": { + "Symfony\\Component\\OptionsResolver\\": "" + }, + "exclude-from-classmap": [ + "/Tests/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Fabien Potencier", + "email": "fabien@symfony.com" + }, + { + "name": "Symfony Community", + "homepage": "https://symfony.com/contributors" + } + ], + "description": "Provides an improved replacement for the array_replace PHP function", + "homepage": "https://symfony.com", + "keywords": [ + "config", + "configuration", + "options" + ], + "support": { + "source": "https://github.com/symfony/options-resolver/tree/v7.1.6" + }, + "funding": [ + { + "url": "https://symfony.com/sponsor", + "type": "custom" + }, + { + "url": "https://github.com/fabpot", + "type": "github" + }, + { + "url": "https://tidelift.com/funding/github/packagist/symfony/symfony", + "type": "tidelift" + } + ], + "time": "2024-09-25T14:20:29+00:00" + }, + { + "name": "symfony/polyfill-mbstring", + "version": "v1.31.0", + "source": { + "type": "git", + "url": "https://github.com/symfony/polyfill-mbstring.git", + "reference": "85181ba99b2345b0ef10ce42ecac37612d9fd341" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/symfony/polyfill-mbstring/zipball/85181ba99b2345b0ef10ce42ecac37612d9fd341", + "reference": "85181ba99b2345b0ef10ce42ecac37612d9fd341", + "shasum": "" + }, + "require": { + "php": ">=7.2" + }, + "provide": { + "ext-mbstring": "*" + }, + "suggest": { + "ext-mbstring": "For best performance" + }, + "type": "library", + "extra": { + "thanks": { + "name": "symfony/polyfill", + "url": "https://github.com/symfony/polyfill" + } + }, + "autoload": { + "files": [ + "bootstrap.php" + ], + "psr-4": { + "Symfony\\Polyfill\\Mbstring\\": "" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Nicolas Grekas", + "email": "p@tchwork.com" + }, + { + "name": "Symfony Community", + "homepage": "https://symfony.com/contributors" + } + ], + "description": "Symfony polyfill for the Mbstring extension", + "homepage": "https://symfony.com", + "keywords": [ + "compatibility", + "mbstring", + "polyfill", + "portable", + "shim" + ], + "support": { + "source": "https://github.com/symfony/polyfill-mbstring/tree/v1.31.0" + }, + "funding": [ + { + "url": "https://symfony.com/sponsor", + "type": "custom" + }, + { + "url": "https://github.com/fabpot", + "type": "github" + }, + { + "url": "https://tidelift.com/funding/github/packagist/symfony/symfony", + "type": "tidelift" + } + ], + "time": "2024-09-09T11:45:10+00:00" + }, + { + "name": "symfony/polyfill-php83", + "version": "v1.31.0", + "source": { + "type": "git", + "url": "https://github.com/symfony/polyfill-php83.git", + "reference": "2fb86d65e2d424369ad2905e83b236a8805ba491" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/symfony/polyfill-php83/zipball/2fb86d65e2d424369ad2905e83b236a8805ba491", + "reference": "2fb86d65e2d424369ad2905e83b236a8805ba491", + "shasum": "" + }, + "require": { + "php": ">=7.2" + }, + "type": "library", + "extra": { + "thanks": { + "name": "symfony/polyfill", + "url": "https://github.com/symfony/polyfill" + } + }, + "autoload": { + "files": [ + "bootstrap.php" + ], + "psr-4": { + "Symfony\\Polyfill\\Php83\\": "" + }, + "classmap": [ + "Resources/stubs" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Nicolas Grekas", + "email": "p@tchwork.com" + }, + { + "name": "Symfony Community", + "homepage": "https://symfony.com/contributors" + } + ], + "description": "Symfony polyfill backporting some PHP 8.3+ features to lower PHP versions", + "homepage": "https://symfony.com", + "keywords": [ + "compatibility", + "polyfill", + "portable", + "shim" + ], + "support": { + "source": "https://github.com/symfony/polyfill-php83/tree/v1.31.0" + }, + "funding": [ + { + "url": "https://symfony.com/sponsor", + "type": "custom" + }, + { + "url": "https://github.com/fabpot", + "type": "github" + }, + { + "url": "https://tidelift.com/funding/github/packagist/symfony/symfony", + "type": "tidelift" + } + ], + "time": "2024-09-09T11:45:10+00:00" + }, + { + "name": "symfony/translation", + "version": "v7.1.6", + "source": { + "type": "git", + "url": "https://github.com/symfony/translation.git", + "reference": "b9f72ab14efdb6b772f85041fa12f820dee8d55f" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/symfony/translation/zipball/b9f72ab14efdb6b772f85041fa12f820dee8d55f", + "reference": "b9f72ab14efdb6b772f85041fa12f820dee8d55f", + "shasum": "" + }, + "require": { + "php": ">=8.2", + "symfony/polyfill-mbstring": "~1.0", + "symfony/translation-contracts": "^2.5|^3.0" + }, + "conflict": { + "symfony/config": "<6.4", + "symfony/console": "<6.4", + "symfony/dependency-injection": "<6.4", + "symfony/http-client-contracts": "<2.5", + "symfony/http-kernel": "<6.4", + "symfony/service-contracts": "<2.5", + "symfony/twig-bundle": "<6.4", + "symfony/yaml": "<6.4" + }, + "provide": { + "symfony/translation-implementation": "2.3|3.0" + }, + "require-dev": { + "nikic/php-parser": "^4.18|^5.0", + "psr/log": "^1|^2|^3", + "symfony/config": "^6.4|^7.0", + "symfony/console": "^6.4|^7.0", + "symfony/dependency-injection": "^6.4|^7.0", + "symfony/finder": "^6.4|^7.0", + "symfony/http-client-contracts": "^2.5|^3.0", + "symfony/http-kernel": "^6.4|^7.0", + "symfony/intl": "^6.4|^7.0", + "symfony/polyfill-intl-icu": "^1.21", + "symfony/routing": "^6.4|^7.0", + "symfony/service-contracts": "^2.5|^3", + "symfony/yaml": "^6.4|^7.0" + }, + "type": "library", + "autoload": { + "files": [ + "Resources/functions.php" + ], + "psr-4": { + "Symfony\\Component\\Translation\\": "" + }, + "exclude-from-classmap": [ + "/Tests/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Fabien Potencier", + "email": "fabien@symfony.com" + }, + { + "name": "Symfony Community", + "homepage": "https://symfony.com/contributors" + } + ], + "description": "Provides tools to internationalize your application", + "homepage": "https://symfony.com", + "support": { + "source": "https://github.com/symfony/translation/tree/v7.1.6" + }, + "funding": [ + { + "url": "https://symfony.com/sponsor", + "type": "custom" + }, + { + "url": "https://github.com/fabpot", + "type": "github" + }, + { + "url": "https://tidelift.com/funding/github/packagist/symfony/symfony", + "type": "tidelift" + } + ], + "time": "2024-09-28T12:35:13+00:00" + }, + { + "name": "symfony/translation-contracts", + "version": "v3.5.0", + "source": { + "type": "git", + "url": "https://github.com/symfony/translation-contracts.git", + "reference": "b9d2189887bb6b2e0367a9fc7136c5239ab9b05a" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/symfony/translation-contracts/zipball/b9d2189887bb6b2e0367a9fc7136c5239ab9b05a", + "reference": "b9d2189887bb6b2e0367a9fc7136c5239ab9b05a", + "shasum": "" + }, + "require": { + "php": ">=8.1" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-main": "3.5-dev" + }, + "thanks": { + "name": "symfony/contracts", + "url": "https://github.com/symfony/contracts" + } + }, + "autoload": { + "psr-4": { + "Symfony\\Contracts\\Translation\\": "" + }, + "exclude-from-classmap": [ + "/Test/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Nicolas Grekas", + "email": "p@tchwork.com" + }, + { + "name": "Symfony Community", + "homepage": "https://symfony.com/contributors" + } + ], + "description": "Generic abstractions related to translation", + "homepage": "https://symfony.com", + "keywords": [ + "abstractions", + "contracts", + "decoupling", + "interfaces", + "interoperability", + "standards" + ], + "support": { + "source": "https://github.com/symfony/translation-contracts/tree/v3.5.0" + }, + "funding": [ + { + "url": "https://symfony.com/sponsor", + "type": "custom" + }, + { + "url": "https://github.com/fabpot", + "type": "github" + }, + { + "url": "https://tidelift.com/funding/github/packagist/symfony/symfony", + "type": "tidelift" + } + ], + "time": "2024-04-18T09:32:20+00:00" + }, + { + "name": "team-reflex/discord-php", + "version": "dev-master", + "source": { + "type": "git", + "url": "https://github.com/discord-php/DiscordPHP.git", + "reference": "6fbb669643942f7e836fb450b695f326616c85f3" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/discord-php/DiscordPHP/zipball/6fbb669643942f7e836fb450b695f326616c85f3", + "reference": "6fbb669643942f7e836fb450b695f326616c85f3", + "shasum": "" + }, + "require": { + "discord-php/http": "^10.1.7", + "discord/interactions": "^2.2", + "ext-json": "*", + "ext-zlib": "*", + "monolog/monolog": "^2.1.1 || ^3.0", + "nesbot/carbon": "^2.38 || ^3.0", + "php": "^8.0", + "psr/simple-cache": "^1.0 || ^2.0 || ^3.0", + "ratchet/pawl": "^0.4.1", + "react/async": "^4.0 || ^3.0", + "react/cache": "^0.5 || ^0.6 || ^1.0", + "react/child-process": "^0.6.3", + "react/datagram": "^1.8", + "react/event-loop": "^1.2", + "react/promise": "^2.2", + "symfony/options-resolver": "^5.1.11 || ^6.0 || ^7.0", + "trafficcophp/bytebuffer": "^0.3" + }, + "require-dev": { + "davidcole1340/reactsh": "dev-master", + "friendsofphp/php-cs-fixer": "^3", + "phpunit/phpunit": "^9.4.4", + "symfony/cache": "^5.4", + "symfony/var-dumper": "*", + "wyrihaximus/react-cache-redis": "^3.0 || >=4.0 <4.4" + }, + "suggest": { + "ext-ev": "For a faster, and more performant loop.", + "ext-event": "For a faster, and more performant loop.", + "ext-fileinfo": "For function mime_content_type().", + "ext-gmp": "For 64 bit calculations on x86 (32 bit) PHP.", + "ext-mbstring": "For accurate calculations of string length when handling non-english characters.", + "ext-uv": "For a faster, and more performant loop. Preferred." + }, + "default-branch": true, + "type": "library", + "autoload": { + "files": [ + "src/Discord/functions.php" + ], + "psr-4": { + "Discord\\": "src/Discord" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "David Cole", + "email": "david.cole1340@gmail.com" + } + ], + "description": "An unofficial API to interact with the voice and text service Discord.", + "support": { + "chat": "https://discord.gg/dphp", + "docs": "https://discord-php.github.io/DiscordPHP/", + "issues": "https://github.com/discord-php/DiscordPHP/issues", + "source": "https://github.com/discord-php/DiscordPHP/tree/v10.0.0-RC12", + "wiki": "https://github.com/discord-php/DiscordPHP/wiki" + }, + "time": "2024-11-01T18:12:36+00:00" + }, + { + "name": "tnapf/env", + "version": "v1.2.1", + "source": { + "type": "git", + "url": "https://github.com/tnapf/Env.git", + "reference": "e6bedbd47f1989750940256f510c0616f9cf9178" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/tnapf/Env/zipball/e6bedbd47f1989750940256f510c0616f9cf9178", + "reference": "e6bedbd47f1989750940256f510c0616f9cf9178", + "shasum": "" + }, + "require": { + "php": ">=8.1" + }, + "require-dev": { + "ergebnis/composer-normalize": "^2.31", + "fakerphp/faker": "^1.21", + "friendsofphp/php-cs-fixer": "^3.16", + "jetbrains/phpstorm-attributes": "^1.0", + "phpunit/phpunit": "^10.1", + "roave/security-advisories": "dev-latest", + "xheaven/composer-git-hooks": "^3.0" + }, + "type": "library", + "extra": { + "composer-normalize": { + "indent-size": 2, + "indent-style": "space" + }, + "hooks": { + "pre-commit": "composer fix:dry", + "pre-push": "composer test" + } + }, + "autoload": { + "psr-4": { + "Tnapf\\Env\\": "src/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Robert Snedeker", + "email": "rsnedeker20@gmail.com" + }, + { + "name": "Mark Magyar", + "email": "hello@mmark.me" + } + ], + "description": "A environment package for PHP", + "support": { + "issues": "https://github.com/tnapf/Env/issues", + "source": "https://github.com/tnapf/Env/tree/v1.2.1" + }, + "time": "2023-07-01T04:48:07+00:00" + }, + { + "name": "trafficcophp/bytebuffer", + "version": "v0.3", + "source": { + "type": "git", + "url": "https://github.com/nesQuick/ByteBuffer.git", + "reference": "e94e5c87c41bc79c0f738b0fa89bad11d27ae0b4" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/nesQuick/ByteBuffer/zipball/e94e5c87c41bc79c0f738b0fa89bad11d27ae0b4", + "reference": "e94e5c87c41bc79c0f738b0fa89bad11d27ae0b4", + "shasum": "" + }, + "type": "library", + "autoload": { + "psr-0": { + "TrafficCophp": "src/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Ole 'nesQuick' Michaelis", + "email": "ole.michaelis@googlemail.com", + "homepage": "http://www.codestars.eu" + } + ], + "description": "Node.js inspired byte stream buffer for PHP.", + "keywords": [ + "Buffer", + "Bytehandling", + "Socket", + "binary data", + "library", + "pack", + "stream", + "wrapper" + ], + "support": { + "issues": "https://github.com/nesQuick/ByteBuffer/issues", + "source": "https://github.com/nesQuick/ByteBuffer/tree/master" + }, + "time": "2016-01-13T22:50:58+00:00" + } + ], + "packages-dev": [ + { + "name": "clue/ndjson-react", + "version": "v1.3.0", + "source": { + "type": "git", + "url": "https://github.com/clue/reactphp-ndjson.git", + "reference": "392dc165fce93b5bb5c637b67e59619223c931b0" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/clue/reactphp-ndjson/zipball/392dc165fce93b5bb5c637b67e59619223c931b0", + "reference": "392dc165fce93b5bb5c637b67e59619223c931b0", + "shasum": "" + }, + "require": { + "php": ">=5.3", + "react/stream": "^1.2" + }, + "require-dev": { + "phpunit/phpunit": "^9.5 || ^5.7 || ^4.8.35", + "react/event-loop": "^1.2" + }, + "type": "library", + "autoload": { + "psr-4": { + "Clue\\React\\NDJson\\": "src/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Christian Lück", + "email": "christian@clue.engineering" + } + ], + "description": "Streaming newline-delimited JSON (NDJSON) parser and encoder for ReactPHP.", + "homepage": "https://github.com/clue/reactphp-ndjson", + "keywords": [ + "NDJSON", + "json", + "jsonlines", + "newline", + "reactphp", + "streaming" + ], + "support": { + "issues": "https://github.com/clue/reactphp-ndjson/issues", + "source": "https://github.com/clue/reactphp-ndjson/tree/v1.3.0" + }, + "funding": [ + { + "url": "https://clue.engineering/support", + "type": "custom" + }, + { + "url": "https://github.com/clue", + "type": "github" + } + ], + "time": "2022-12-23T10:58:28+00:00" + }, + { + "name": "composer/pcre", + "version": "3.3.1", + "source": { + "type": "git", + "url": "https://github.com/composer/pcre.git", + "reference": "63aaeac21d7e775ff9bc9d45021e1745c97521c4" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/composer/pcre/zipball/63aaeac21d7e775ff9bc9d45021e1745c97521c4", + "reference": "63aaeac21d7e775ff9bc9d45021e1745c97521c4", + "shasum": "" + }, + "require": { + "php": "^7.4 || ^8.0" + }, + "conflict": { + "phpstan/phpstan": "<1.11.10" + }, + "require-dev": { + "phpstan/phpstan": "^1.11.10", + "phpstan/phpstan-strict-rules": "^1.1", + "phpunit/phpunit": "^8 || ^9" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-main": "3.x-dev" + }, + "phpstan": { + "includes": [ + "extension.neon" + ] + } + }, + "autoload": { + "psr-4": { + "Composer\\Pcre\\": "src" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Jordi Boggiano", + "email": "j.boggiano@seld.be", + "homepage": "http://seld.be" + } + ], + "description": "PCRE wrapping library that offers type-safe preg_* replacements.", + "keywords": [ + "PCRE", + "preg", + "regex", + "regular expression" + ], + "support": { + "issues": "https://github.com/composer/pcre/issues", + "source": "https://github.com/composer/pcre/tree/3.3.1" + }, + "funding": [ + { + "url": "https://packagist.com", + "type": "custom" + }, + { + "url": "https://github.com/composer", + "type": "github" + }, + { + "url": "https://tidelift.com/funding/github/packagist/composer/composer", + "type": "tidelift" + } + ], + "time": "2024-08-27T18:44:43+00:00" + }, + { + "name": "composer/semver", + "version": "3.4.3", + "source": { + "type": "git", + "url": "https://github.com/composer/semver.git", + "reference": "4313d26ada5e0c4edfbd1dc481a92ff7bff91f12" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/composer/semver/zipball/4313d26ada5e0c4edfbd1dc481a92ff7bff91f12", + "reference": "4313d26ada5e0c4edfbd1dc481a92ff7bff91f12", + "shasum": "" + }, + "require": { + "php": "^5.3.2 || ^7.0 || ^8.0" + }, + "require-dev": { + "phpstan/phpstan": "^1.11", + "symfony/phpunit-bridge": "^3 || ^7" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-main": "3.x-dev" + } + }, + "autoload": { + "psr-4": { + "Composer\\Semver\\": "src" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Nils Adermann", + "email": "naderman@naderman.de", + "homepage": "http://www.naderman.de" + }, + { + "name": "Jordi Boggiano", + "email": "j.boggiano@seld.be", + "homepage": "http://seld.be" + }, + { + "name": "Rob Bast", + "email": "rob.bast@gmail.com", + "homepage": "http://robbast.nl" + } + ], + "description": "Semver library that offers utilities, version constraint parsing and validation.", + "keywords": [ + "semantic", + "semver", + "validation", + "versioning" + ], + "support": { + "irc": "ircs://irc.libera.chat:6697/composer", + "issues": "https://github.com/composer/semver/issues", + "source": "https://github.com/composer/semver/tree/3.4.3" + }, + "funding": [ + { + "url": "https://packagist.com", + "type": "custom" + }, + { + "url": "https://github.com/composer", + "type": "github" + }, + { + "url": "https://tidelift.com/funding/github/packagist/composer/composer", + "type": "tidelift" + } + ], + "time": "2024-09-19T14:15:21+00:00" + }, + { + "name": "composer/xdebug-handler", + "version": "3.0.5", + "source": { + "type": "git", + "url": "https://github.com/composer/xdebug-handler.git", + "reference": "6c1925561632e83d60a44492e0b344cf48ab85ef" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/composer/xdebug-handler/zipball/6c1925561632e83d60a44492e0b344cf48ab85ef", + "reference": "6c1925561632e83d60a44492e0b344cf48ab85ef", + "shasum": "" + }, + "require": { + "composer/pcre": "^1 || ^2 || ^3", + "php": "^7.2.5 || ^8.0", + "psr/log": "^1 || ^2 || ^3" + }, + "require-dev": { + "phpstan/phpstan": "^1.0", + "phpstan/phpstan-strict-rules": "^1.1", + "phpunit/phpunit": "^8.5 || ^9.6 || ^10.5" + }, + "type": "library", + "autoload": { + "psr-4": { + "Composer\\XdebugHandler\\": "src" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "John Stevenson", + "email": "john-stevenson@blueyonder.co.uk" + } + ], + "description": "Restarts a process without Xdebug.", + "keywords": [ + "Xdebug", + "performance" + ], + "support": { + "irc": "ircs://irc.libera.chat:6697/composer", + "issues": "https://github.com/composer/xdebug-handler/issues", + "source": "https://github.com/composer/xdebug-handler/tree/3.0.5" + }, + "funding": [ + { + "url": "https://packagist.com", + "type": "custom" + }, + { + "url": "https://github.com/composer", + "type": "github" + }, + { + "url": "https://tidelift.com/funding/github/packagist/composer/composer", + "type": "tidelift" + } + ], + "time": "2024-05-06T16:37:16+00:00" + }, + { + "name": "ergebnis/composer-normalize", + "version": "2.44.0", + "source": { + "type": "git", + "url": "https://github.com/ergebnis/composer-normalize.git", + "reference": "bd0c446426bb837ae0cc9f97948167e658bd11d2" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/ergebnis/composer-normalize/zipball/bd0c446426bb837ae0cc9f97948167e658bd11d2", + "reference": "bd0c446426bb837ae0cc9f97948167e658bd11d2", + "shasum": "" + }, + "require": { + "composer-plugin-api": "^2.0.0", + "ergebnis/json": "^1.2.0", + "ergebnis/json-normalizer": "^4.5.0", + "ergebnis/json-printer": "^3.5.0", + "ext-json": "*", + "justinrainbow/json-schema": "^5.2.12", + "localheinz/diff": "^1.1.1", + "php": "~7.4.0 || ~8.0.0 || ~8.1.0 || ~8.2.0 || ~8.3.0 || ~8.4.0" + }, + "require-dev": { + "composer/composer": "^2.7.7", + "ergebnis/license": "^2.5.0", + "ergebnis/php-cs-fixer-config": "^6.37.0", + "ergebnis/phpunit-slow-test-detector": "^2.16.0", + "fakerphp/faker": "^1.23.1", + "infection/infection": "~0.26.6", + "phpunit/phpunit": "^9.6.20", + "psalm/plugin-phpunit": "~0.19.0", + "rector/rector": "^1.2.5", + "symfony/filesystem": "^5.4.41", + "vimeo/psalm": "^5.26.1" + }, + "type": "composer-plugin", + "extra": { + "class": "Ergebnis\\Composer\\Normalize\\NormalizePlugin", + "composer-normalize": { + "indent-size": 2, + "indent-style": "space" + }, + "plugin-optional": true + }, + "autoload": { + "psr-4": { + "Ergebnis\\Composer\\Normalize\\": "src/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Andreas Möller", + "email": "am@localheinz.com", + "homepage": "https://localheinz.com" + } + ], + "description": "Provides a composer plugin for normalizing composer.json.", + "homepage": "https://github.com/ergebnis/composer-normalize", + "keywords": [ + "composer", + "normalize", + "normalizer", + "plugin" + ], + "support": { + "issues": "https://github.com/ergebnis/composer-normalize/issues", + "security": "https://github.com/ergebnis/composer-normalize/blob/main/.github/SECURITY.md", + "source": "https://github.com/ergebnis/composer-normalize" + }, + "time": "2024-09-30T21:56:22+00:00" + }, + { + "name": "ergebnis/json", + "version": "1.3.0", + "source": { + "type": "git", + "url": "https://github.com/ergebnis/json.git", + "reference": "84051b4e243d6a8e2f8271604b11ffa52d29bc7a" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/ergebnis/json/zipball/84051b4e243d6a8e2f8271604b11ffa52d29bc7a", + "reference": "84051b4e243d6a8e2f8271604b11ffa52d29bc7a", + "shasum": "" + }, + "require": { + "ext-json": "*", + "php": "~7.4.0 || ~8.0.0 || ~8.1.0 || ~8.2.0 || ~8.3.0 || ~8.4.0" + }, + "require-dev": { + "ergebnis/data-provider": "^3.2.0", + "ergebnis/license": "^2.4.0", + "ergebnis/php-cs-fixer-config": "^6.36.0", + "ergebnis/phpunit-slow-test-detector": "^2.15.1", + "fakerphp/faker": "^1.23.1", + "infection/infection": "~0.26.6", + "phpunit/phpunit": "^9.6.18", + "psalm/plugin-phpunit": "~0.19.0", + "rector/rector": "^1.2.5", + "vimeo/psalm": "^5.26.1" + }, + "type": "library", + "extra": { + "composer-normalize": { + "indent-size": 2, + "indent-style": "space" + } + }, + "autoload": { + "psr-4": { + "Ergebnis\\Json\\": "src/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Andreas Möller", + "email": "am@localheinz.com", + "homepage": "https://localheinz.com" + } + ], + "description": "Provides a Json value object for representing a valid JSON string.", + "homepage": "https://github.com/ergebnis/json", + "keywords": [ + "json" + ], + "support": { + "issues": "https://github.com/ergebnis/json/issues", + "security": "https://github.com/ergebnis/json/blob/main/.github/SECURITY.md", + "source": "https://github.com/ergebnis/json" + }, + "time": "2024-09-27T15:01:05+00:00" + }, + { + "name": "ergebnis/json-normalizer", + "version": "4.6.0", + "source": { + "type": "git", + "url": "https://github.com/ergebnis/json-normalizer.git", + "reference": "859fd3cee417f0b10a8e6ffb8dbeb03587106b8b" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/ergebnis/json-normalizer/zipball/859fd3cee417f0b10a8e6ffb8dbeb03587106b8b", + "reference": "859fd3cee417f0b10a8e6ffb8dbeb03587106b8b", + "shasum": "" + }, + "require": { + "ergebnis/json": "^1.2.0", + "ergebnis/json-pointer": "^3.4.0", + "ergebnis/json-printer": "^3.5.0", + "ergebnis/json-schema-validator": "^4.2.0", + "ext-json": "*", + "justinrainbow/json-schema": "^5.2.12", + "php": "~7.4.0 || ~8.0.0 || ~8.1.0 || ~8.2.0 || ~8.3.0 || ~8.4.0" + }, + "require-dev": { + "composer/semver": "^3.4.3", + "ergebnis/data-provider": "^3.2.0", + "ergebnis/license": "^2.4.0", + "ergebnis/php-cs-fixer-config": "^6.36.0", + "ergebnis/phpunit-slow-test-detector": "^2.15.1", + "fakerphp/faker": "^1.23.1", + "infection/infection": "~0.26.6", + "phpunit/phpunit": "^9.6.19", + "psalm/plugin-phpunit": "~0.19.0", + "rector/rector": "^1.2.5", + "vimeo/psalm": "^5.26.1" + }, + "suggest": { + "composer/semver": "If you want to use ComposerJsonNormalizer or VersionConstraintNormalizer" + }, + "type": "library", + "autoload": { + "psr-4": { + "Ergebnis\\Json\\Normalizer\\": "src/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Andreas Möller", + "email": "am@localheinz.com", + "homepage": "https://localheinz.com" + } + ], + "description": "Provides generic and vendor-specific normalizers for normalizing JSON documents.", + "homepage": "https://github.com/ergebnis/json-normalizer", + "keywords": [ + "json", + "normalizer" + ], + "support": { + "issues": "https://github.com/ergebnis/json-normalizer/issues", + "security": "https://github.com/ergebnis/json-normalizer/blob/main/.github/SECURITY.md", + "source": "https://github.com/ergebnis/json-normalizer" + }, + "time": "2024-09-27T15:11:59+00:00" + }, + { + "name": "ergebnis/json-pointer", + "version": "3.5.0", + "source": { + "type": "git", + "url": "https://github.com/ergebnis/json-pointer.git", + "reference": "f6ff71e69305b8ab5e4457e374b35dcd0812609b" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/ergebnis/json-pointer/zipball/f6ff71e69305b8ab5e4457e374b35dcd0812609b", + "reference": "f6ff71e69305b8ab5e4457e374b35dcd0812609b", + "shasum": "" + }, + "require": { + "php": "~7.4.0 || ~8.0.0 || ~8.1.0 || ~8.2.0 || ~8.3.0 || ~8.4.0" + }, + "require-dev": { + "ergebnis/composer-normalize": "^2.43.0", + "ergebnis/data-provider": "^3.2.0", + "ergebnis/license": "^2.4.0", + "ergebnis/php-cs-fixer-config": "^6.32.0", + "ergebnis/phpunit-slow-test-detector": "^2.15.0", + "fakerphp/faker": "^1.23.1", + "infection/infection": "~0.26.6", + "phpunit/phpunit": "^9.6.19", + "psalm/plugin-phpunit": "~0.19.0", + "rector/rector": "^1.2.1", + "vimeo/psalm": "^5.25.0" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-main": "3.4-dev" + }, + "composer-normalize": { + "indent-size": 2, + "indent-style": "space" + } + }, + "autoload": { + "psr-4": { + "Ergebnis\\Json\\Pointer\\": "src/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Andreas Möller", + "email": "am@localheinz.com", + "homepage": "https://localheinz.com" + } + ], + "description": "Provides an abstraction of a JSON pointer.", + "homepage": "https://github.com/ergebnis/json-pointer", + "keywords": [ + "RFC6901", + "json", + "pointer" + ], + "support": { + "issues": "https://github.com/ergebnis/json-pointer/issues", + "security": "https://github.com/ergebnis/json-pointer/blob/main/.github/SECURITY.md", + "source": "https://github.com/ergebnis/json-pointer" + }, + "time": "2024-09-27T15:47:15+00:00" + }, + { + "name": "ergebnis/json-printer", + "version": "3.6.0", + "source": { + "type": "git", + "url": "https://github.com/ergebnis/json-printer.git", + "reference": "d2e51379dc62d73017a779a78fcfba568de39e0a" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/ergebnis/json-printer/zipball/d2e51379dc62d73017a779a78fcfba568de39e0a", + "reference": "d2e51379dc62d73017a779a78fcfba568de39e0a", + "shasum": "" + }, + "require": { + "ext-json": "*", + "ext-mbstring": "*", + "php": "~7.4.0 || ~8.0.0 || ~8.1.0 || ~8.2.0 || ~8.3.0 || ~8.4.0" + }, + "require-dev": { + "ergebnis/data-provider": "^3.2.0", + "ergebnis/license": "^2.4.0", + "ergebnis/php-cs-fixer-config": "^6.36.0", + "ergebnis/phpunit-slow-test-detector": "^2.15.1", + "fakerphp/faker": "^1.23.1", + "infection/infection": "~0.26.6", + "phpunit/phpunit": "^9.6.19", + "psalm/plugin-phpunit": "~0.19.0", + "rector/rector": "~1.2.5", + "vimeo/psalm": "^5.26.1" + }, + "type": "library", + "autoload": { + "psr-4": { + "Ergebnis\\Json\\Printer\\": "src/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Andreas Möller", + "email": "am@localheinz.com", + "homepage": "https://localheinz.com" + } + ], + "description": "Provides a JSON printer, allowing for flexible indentation.", + "homepage": "https://github.com/ergebnis/json-printer", + "keywords": [ + "formatter", + "json", + "printer" + ], + "support": { + "issues": "https://github.com/ergebnis/json-printer/issues", + "security": "https://github.com/ergebnis/json-printer/blob/main/.github/SECURITY.md", + "source": "https://github.com/ergebnis/json-printer" + }, + "time": "2024-09-27T15:19:56+00:00" + }, + { + "name": "ergebnis/json-schema-validator", + "version": "4.3.0", + "source": { + "type": "git", + "url": "https://github.com/ergebnis/json-schema-validator.git", + "reference": "73f938f8995c6ad1e37d2c1dfeaa8336861f9db8" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/ergebnis/json-schema-validator/zipball/73f938f8995c6ad1e37d2c1dfeaa8336861f9db8", + "reference": "73f938f8995c6ad1e37d2c1dfeaa8336861f9db8", + "shasum": "" + }, + "require": { + "ergebnis/json": "^1.2.0", + "ergebnis/json-pointer": "^3.4.0", + "ext-json": "*", + "justinrainbow/json-schema": "^5.2.12", + "php": "~7.4.0 || ~8.0.0 || ~8.1.0 || ~8.2.0 || ~8.3.0 || ~8.4.0" + }, + "require-dev": { + "ergebnis/data-provider": "^3.2.0", + "ergebnis/license": "^2.4.0", + "ergebnis/php-cs-fixer-config": "^6.36.0", + "ergebnis/phpunit-slow-test-detector": "^2.15.1", + "fakerphp/faker": "^1.23.1", + "infection/infection": "~0.26.6", + "phpunit/phpunit": "^9.6.20", + "psalm/plugin-phpunit": "~0.19.0", + "rector/rector": "^1.2.5", + "vimeo/psalm": "^5.26.1" + }, + "type": "library", + "extra": { + "composer-normalize": { + "indent-size": 2, + "indent-style": "space" + } + }, + "autoload": { + "psr-4": { + "Ergebnis\\Json\\SchemaValidator\\": "src/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Andreas Möller", + "email": "am@localheinz.com", + "homepage": "https://localheinz.com" + } + ], + "description": "Provides a JSON schema validator, building on top of justinrainbow/json-schema.", + "homepage": "https://github.com/ergebnis/json-schema-validator", + "keywords": [ + "json", + "schema", + "validator" + ], + "support": { + "issues": "https://github.com/ergebnis/json-schema-validator/issues", + "security": "https://github.com/ergebnis/json-schema-validator/blob/main/.github/SECURITY.md", + "source": "https://github.com/ergebnis/json-schema-validator" + }, + "time": "2024-09-27T15:16:33+00:00" + }, + { + "name": "fakerphp/faker", + "version": "v1.23.1", + "source": { + "type": "git", + "url": "https://github.com/FakerPHP/Faker.git", + "reference": "bfb4fe148adbf78eff521199619b93a52ae3554b" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/FakerPHP/Faker/zipball/bfb4fe148adbf78eff521199619b93a52ae3554b", + "reference": "bfb4fe148adbf78eff521199619b93a52ae3554b", + "shasum": "" + }, + "require": { + "php": "^7.4 || ^8.0", + "psr/container": "^1.0 || ^2.0", + "symfony/deprecation-contracts": "^2.2 || ^3.0" + }, + "conflict": { + "fzaninotto/faker": "*" + }, + "require-dev": { + "bamarni/composer-bin-plugin": "^1.4.1", + "doctrine/persistence": "^1.3 || ^2.0", + "ext-intl": "*", + "phpunit/phpunit": "^9.5.26", + "symfony/phpunit-bridge": "^5.4.16" + }, + "suggest": { + "doctrine/orm": "Required to use Faker\\ORM\\Doctrine", + "ext-curl": "Required by Faker\\Provider\\Image to download images.", + "ext-dom": "Required by Faker\\Provider\\HtmlLorem for generating random HTML.", + "ext-iconv": "Required by Faker\\Provider\\ru_RU\\Text::realText() for generating real Russian text.", + "ext-mbstring": "Required for multibyte Unicode string functionality." + }, + "type": "library", + "autoload": { + "psr-4": { + "Faker\\": "src/Faker/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "François Zaninotto" + } + ], + "description": "Faker is a PHP library that generates fake data for you.", + "keywords": [ + "data", + "faker", + "fixtures" + ], + "support": { + "issues": "https://github.com/FakerPHP/Faker/issues", + "source": "https://github.com/FakerPHP/Faker/tree/v1.23.1" + }, + "time": "2024-01-02T13:46:09+00:00" + }, + { + "name": "fidry/cpu-core-counter", + "version": "1.2.0", + "source": { + "type": "git", + "url": "https://github.com/theofidry/cpu-core-counter.git", + "reference": "8520451a140d3f46ac33042715115e290cf5785f" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/theofidry/cpu-core-counter/zipball/8520451a140d3f46ac33042715115e290cf5785f", + "reference": "8520451a140d3f46ac33042715115e290cf5785f", + "shasum": "" + }, + "require": { + "php": "^7.2 || ^8.0" + }, + "require-dev": { + "fidry/makefile": "^0.2.0", + "fidry/php-cs-fixer-config": "^1.1.2", + "phpstan/extension-installer": "^1.2.0", + "phpstan/phpstan": "^1.9.2", + "phpstan/phpstan-deprecation-rules": "^1.0.0", + "phpstan/phpstan-phpunit": "^1.2.2", + "phpstan/phpstan-strict-rules": "^1.4.4", + "phpunit/phpunit": "^8.5.31 || ^9.5.26", + "webmozarts/strict-phpunit": "^7.5" + }, + "type": "library", + "autoload": { + "psr-4": { + "Fidry\\CpuCoreCounter\\": "src/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Théo FIDRY", + "email": "theo.fidry@gmail.com" + } + ], + "description": "Tiny utility to get the number of CPU cores.", + "keywords": [ + "CPU", + "core" + ], + "support": { + "issues": "https://github.com/theofidry/cpu-core-counter/issues", + "source": "https://github.com/theofidry/cpu-core-counter/tree/1.2.0" + }, + "funding": [ + { + "url": "https://github.com/theofidry", + "type": "github" + } + ], + "time": "2024-08-06T10:04:20+00:00" + }, + { + "name": "friendsofphp/php-cs-fixer", + "version": "v3.64.0", + "source": { + "type": "git", + "url": "https://github.com/PHP-CS-Fixer/PHP-CS-Fixer.git", + "reference": "58dd9c931c785a79739310aef5178928305ffa67" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/PHP-CS-Fixer/PHP-CS-Fixer/zipball/58dd9c931c785a79739310aef5178928305ffa67", + "reference": "58dd9c931c785a79739310aef5178928305ffa67", + "shasum": "" + }, + "require": { + "clue/ndjson-react": "^1.0", + "composer/semver": "^3.4", + "composer/xdebug-handler": "^3.0.3", + "ext-filter": "*", + "ext-json": "*", + "ext-tokenizer": "*", + "fidry/cpu-core-counter": "^1.0", + "php": "^7.4 || ^8.0", + "react/child-process": "^0.6.5", + "react/event-loop": "^1.0", + "react/promise": "^2.0 || ^3.0", + "react/socket": "^1.0", + "react/stream": "^1.0", + "sebastian/diff": "^4.0 || ^5.0 || ^6.0", + "symfony/console": "^5.4 || ^6.0 || ^7.0", + "symfony/event-dispatcher": "^5.4 || ^6.0 || ^7.0", + "symfony/filesystem": "^5.4 || ^6.0 || ^7.0", + "symfony/finder": "^5.4 || ^6.0 || ^7.0", + "symfony/options-resolver": "^5.4 || ^6.0 || ^7.0", + "symfony/polyfill-mbstring": "^1.28", + "symfony/polyfill-php80": "^1.28", + "symfony/polyfill-php81": "^1.28", + "symfony/process": "^5.4 || ^6.0 || ^7.0", + "symfony/stopwatch": "^5.4 || ^6.0 || ^7.0" + }, + "require-dev": { + "facile-it/paraunit": "^1.3 || ^2.3", + "infection/infection": "^0.29.5", + "justinrainbow/json-schema": "^5.2", + "keradus/cli-executor": "^2.1", + "mikey179/vfsstream": "^1.6.11", + "php-coveralls/php-coveralls": "^2.7", + "php-cs-fixer/accessible-object": "^1.1", + "php-cs-fixer/phpunit-constraint-isidenticalstring": "^1.5", + "php-cs-fixer/phpunit-constraint-xmlmatchesxsd": "^1.5", + "phpunit/phpunit": "^9.6.19 || ^10.5.21 || ^11.2", + "symfony/var-dumper": "^5.4 || ^6.0 || ^7.0", + "symfony/yaml": "^5.4 || ^6.0 || ^7.0" + }, + "suggest": { + "ext-dom": "For handling output formats in XML", + "ext-mbstring": "For handling non-UTF8 characters." + }, + "bin": [ + "php-cs-fixer" + ], + "type": "application", + "autoload": { + "psr-4": { + "PhpCsFixer\\": "src/" + }, + "exclude-from-classmap": [ + "src/Fixer/Internal/*" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Fabien Potencier", + "email": "fabien@symfony.com" + }, + { + "name": "Dariusz Rumiński", + "email": "dariusz.ruminski@gmail.com" + } + ], + "description": "A tool to automatically fix PHP code style", + "keywords": [ + "Static code analysis", + "fixer", + "standards", + "static analysis" + ], + "support": { + "issues": "https://github.com/PHP-CS-Fixer/PHP-CS-Fixer/issues", + "source": "https://github.com/PHP-CS-Fixer/PHP-CS-Fixer/tree/v3.64.0" + }, + "funding": [ + { + "url": "https://github.com/keradus", + "type": "github" + } + ], + "time": "2024-08-30T23:09:38+00:00" + }, + { + "name": "jetbrains/phpstorm-attributes", + "version": "1.1", + "source": { + "type": "git", + "url": "https://github.com/JetBrains/phpstorm-attributes.git", + "reference": "22fb28d679deceedba8366dbae65cc8ebfc17e26" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/JetBrains/phpstorm-attributes/zipball/22fb28d679deceedba8366dbae65cc8ebfc17e26", + "reference": "22fb28d679deceedba8366dbae65cc8ebfc17e26", + "shasum": "" + }, + "type": "library", + "autoload": { + "psr-4": { + "JetBrains\\PhpStorm\\": "src/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "Apache-2.0" + ], + "authors": [ + { + "name": "JetBrains", + "homepage": "https://www.jetbrains.com" + } + ], + "description": "PhpStorm specific attributes", + "keywords": [ + "attributes", + "jetbrains", + "phpstorm" + ], + "support": { + "issues": "https://youtrack.jetbrains.com/newIssue?project=WI", + "source": "https://github.com/JetBrains/phpstorm-attributes/tree/1.1" + }, + "time": "2023-09-01T08:50:25+00:00" + }, + { + "name": "justinrainbow/json-schema", + "version": "5.3.0", + "source": { + "type": "git", + "url": "https://github.com/jsonrainbow/json-schema.git", + "reference": "feb2ca6dd1cebdaf1ed60a4c8de2e53ce11c4fd8" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/jsonrainbow/json-schema/zipball/feb2ca6dd1cebdaf1ed60a4c8de2e53ce11c4fd8", + "reference": "feb2ca6dd1cebdaf1ed60a4c8de2e53ce11c4fd8", + "shasum": "" + }, + "require": { + "php": ">=7.1" + }, + "require-dev": { + "friendsofphp/php-cs-fixer": "~2.2.20||~2.15.1", + "json-schema/json-schema-test-suite": "1.2.0", + "phpunit/phpunit": "^4.8.35" + }, + "bin": [ + "bin/validate-json" + ], + "type": "library", + "autoload": { + "psr-4": { + "JsonSchema\\": "src/JsonSchema/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Bruno Prieto Reis", + "email": "bruno.p.reis@gmail.com" + }, + { + "name": "Justin Rainbow", + "email": "justin.rainbow@gmail.com" + }, + { + "name": "Igor Wiedler", + "email": "igor@wiedler.ch" + }, + { + "name": "Robert Schönthal", + "email": "seroscho@googlemail.com" + } + ], + "description": "A library to validate a json schema.", + "homepage": "https://github.com/justinrainbow/json-schema", + "keywords": [ + "json", + "schema" + ], + "support": { + "issues": "https://github.com/jsonrainbow/json-schema/issues", + "source": "https://github.com/jsonrainbow/json-schema/tree/5.3.0" + }, + "time": "2024-07-06T21:00:26+00:00" + }, + { + "name": "localheinz/diff", + "version": "1.1.1", + "source": { + "type": "git", + "url": "https://github.com/localheinz/diff.git", + "reference": "851bb20ea8358c86f677f5f111c4ab031b1c764c" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/localheinz/diff/zipball/851bb20ea8358c86f677f5f111c4ab031b1c764c", + "reference": "851bb20ea8358c86f677f5f111c4ab031b1c764c", + "shasum": "" + }, + "require": { + "php": "^7.1 || ^8.0" + }, + "require-dev": { + "phpunit/phpunit": "^7.5 || ^8.0", + "symfony/process": "^4.2 || ^5" + }, + "type": "library", + "autoload": { + "classmap": [ + "src/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "BSD-3-Clause" + ], + "authors": [ + { + "name": "Sebastian Bergmann", + "email": "sebastian@phpunit.de" + }, + { + "name": "Kore Nordmann", + "email": "mail@kore-nordmann.de" + } + ], + "description": "Fork of sebastian/diff for use with ergebnis/composer-normalize", + "homepage": "https://github.com/localheinz/diff", + "keywords": [ + "diff", + "udiff", + "unidiff", + "unified diff" + ], + "support": { + "source": "https://github.com/localheinz/diff/tree/main" + }, + "funding": [ + { + "url": "https://github.com/sebastianbergmann", + "type": "github" + } + ], + "time": "2020-07-06T04:49:32+00:00" + }, + { + "name": "myclabs/deep-copy", + "version": "1.12.0", + "source": { + "type": "git", + "url": "https://github.com/myclabs/DeepCopy.git", + "reference": "3a6b9a42cd8f8771bd4295d13e1423fa7f3d942c" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/myclabs/DeepCopy/zipball/3a6b9a42cd8f8771bd4295d13e1423fa7f3d942c", + "reference": "3a6b9a42cd8f8771bd4295d13e1423fa7f3d942c", + "shasum": "" + }, + "require": { + "php": "^7.1 || ^8.0" + }, + "conflict": { + "doctrine/collections": "<1.6.8", + "doctrine/common": "<2.13.3 || >=3 <3.2.2" + }, + "require-dev": { + "doctrine/collections": "^1.6.8", + "doctrine/common": "^2.13.3 || ^3.2.2", + "phpspec/prophecy": "^1.10", + "phpunit/phpunit": "^7.5.20 || ^8.5.23 || ^9.5.13" + }, + "type": "library", + "autoload": { + "files": [ + "src/DeepCopy/deep_copy.php" + ], + "psr-4": { + "DeepCopy\\": "src/DeepCopy/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "description": "Create deep copies (clones) of your objects", + "keywords": [ + "clone", + "copy", + "duplicate", + "object", + "object graph" + ], + "support": { + "issues": "https://github.com/myclabs/DeepCopy/issues", + "source": "https://github.com/myclabs/DeepCopy/tree/1.12.0" + }, + "funding": [ + { + "url": "https://tidelift.com/funding/github/packagist/myclabs/deep-copy", + "type": "tidelift" + } + ], + "time": "2024-06-12T14:39:25+00:00" + }, + { + "name": "nikic/php-parser", + "version": "v5.3.1", + "source": { + "type": "git", + "url": "https://github.com/nikic/PHP-Parser.git", + "reference": "8eea230464783aa9671db8eea6f8c6ac5285794b" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/nikic/PHP-Parser/zipball/8eea230464783aa9671db8eea6f8c6ac5285794b", + "reference": "8eea230464783aa9671db8eea6f8c6ac5285794b", + "shasum": "" + }, + "require": { + "ext-ctype": "*", + "ext-json": "*", + "ext-tokenizer": "*", + "php": ">=7.4" + }, + "require-dev": { + "ircmaxell/php-yacc": "^0.0.7", + "phpunit/phpunit": "^9.0" + }, + "bin": [ + "bin/php-parse" + ], + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "5.0-dev" + } + }, + "autoload": { + "psr-4": { + "PhpParser\\": "lib/PhpParser" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "BSD-3-Clause" + ], + "authors": [ + { + "name": "Nikita Popov" + } + ], + "description": "A PHP parser written in PHP", + "keywords": [ + "parser", + "php" + ], + "support": { + "issues": "https://github.com/nikic/PHP-Parser/issues", + "source": "https://github.com/nikic/PHP-Parser/tree/v5.3.1" + }, + "time": "2024-10-08T18:51:32+00:00" + }, + { + "name": "phar-io/manifest", + "version": "2.0.4", + "source": { + "type": "git", + "url": "https://github.com/phar-io/manifest.git", + "reference": "54750ef60c58e43759730615a392c31c80e23176" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/phar-io/manifest/zipball/54750ef60c58e43759730615a392c31c80e23176", + "reference": "54750ef60c58e43759730615a392c31c80e23176", + "shasum": "" + }, + "require": { + "ext-dom": "*", + "ext-libxml": "*", + "ext-phar": "*", + "ext-xmlwriter": "*", + "phar-io/version": "^3.0.1", + "php": "^7.2 || ^8.0" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "2.0.x-dev" + } + }, + "autoload": { + "classmap": [ + "src/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "BSD-3-Clause" + ], + "authors": [ + { + "name": "Arne Blankerts", + "email": "arne@blankerts.de", + "role": "Developer" + }, + { + "name": "Sebastian Heuer", + "email": "sebastian@phpeople.de", + "role": "Developer" + }, + { + "name": "Sebastian Bergmann", + "email": "sebastian@phpunit.de", + "role": "Developer" + } + ], + "description": "Component for reading phar.io manifest information from a PHP Archive (PHAR)", + "support": { + "issues": "https://github.com/phar-io/manifest/issues", + "source": "https://github.com/phar-io/manifest/tree/2.0.4" + }, + "funding": [ + { + "url": "https://github.com/theseer", + "type": "github" + } + ], + "time": "2024-03-03T12:33:53+00:00" + }, + { + "name": "phar-io/version", + "version": "3.2.1", + "source": { + "type": "git", + "url": "https://github.com/phar-io/version.git", + "reference": "4f7fd7836c6f332bb2933569e566a0d6c4cbed74" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/phar-io/version/zipball/4f7fd7836c6f332bb2933569e566a0d6c4cbed74", + "reference": "4f7fd7836c6f332bb2933569e566a0d6c4cbed74", + "shasum": "" + }, + "require": { + "php": "^7.2 || ^8.0" + }, + "type": "library", + "autoload": { + "classmap": [ + "src/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "BSD-3-Clause" + ], + "authors": [ + { + "name": "Arne Blankerts", + "email": "arne@blankerts.de", + "role": "Developer" + }, + { + "name": "Sebastian Heuer", + "email": "sebastian@phpeople.de", + "role": "Developer" + }, + { + "name": "Sebastian Bergmann", + "email": "sebastian@phpunit.de", + "role": "Developer" + } + ], + "description": "Library for handling version information and constraints", + "support": { + "issues": "https://github.com/phar-io/version/issues", + "source": "https://github.com/phar-io/version/tree/3.2.1" + }, + "time": "2022-02-21T01:04:05+00:00" + }, + { + "name": "phpunit/php-code-coverage", + "version": "10.1.16", + "source": { + "type": "git", + "url": "https://github.com/sebastianbergmann/php-code-coverage.git", + "reference": "7e308268858ed6baedc8704a304727d20bc07c77" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/sebastianbergmann/php-code-coverage/zipball/7e308268858ed6baedc8704a304727d20bc07c77", + "reference": "7e308268858ed6baedc8704a304727d20bc07c77", + "shasum": "" + }, + "require": { + "ext-dom": "*", + "ext-libxml": "*", + "ext-xmlwriter": "*", + "nikic/php-parser": "^4.19.1 || ^5.1.0", + "php": ">=8.1", + "phpunit/php-file-iterator": "^4.1.0", + "phpunit/php-text-template": "^3.0.1", + "sebastian/code-unit-reverse-lookup": "^3.0.0", + "sebastian/complexity": "^3.2.0", + "sebastian/environment": "^6.1.0", + "sebastian/lines-of-code": "^2.0.2", + "sebastian/version": "^4.0.1", + "theseer/tokenizer": "^1.2.3" + }, + "require-dev": { + "phpunit/phpunit": "^10.1" + }, + "suggest": { + "ext-pcov": "PHP extension that provides line coverage", + "ext-xdebug": "PHP extension that provides line coverage as well as branch and path coverage" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-main": "10.1.x-dev" + } + }, + "autoload": { + "classmap": [ + "src/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "BSD-3-Clause" + ], + "authors": [ + { + "name": "Sebastian Bergmann", + "email": "sebastian@phpunit.de", + "role": "lead" + } + ], + "description": "Library that provides collection, processing, and rendering functionality for PHP code coverage information.", + "homepage": "https://github.com/sebastianbergmann/php-code-coverage", + "keywords": [ + "coverage", + "testing", + "xunit" + ], + "support": { + "issues": "https://github.com/sebastianbergmann/php-code-coverage/issues", + "security": "https://github.com/sebastianbergmann/php-code-coverage/security/policy", + "source": "https://github.com/sebastianbergmann/php-code-coverage/tree/10.1.16" + }, + "funding": [ + { + "url": "https://github.com/sebastianbergmann", + "type": "github" + } + ], + "time": "2024-08-22T04:31:57+00:00" + }, + { + "name": "phpunit/php-file-iterator", + "version": "4.1.0", + "source": { + "type": "git", + "url": "https://github.com/sebastianbergmann/php-file-iterator.git", + "reference": "a95037b6d9e608ba092da1b23931e537cadc3c3c" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/sebastianbergmann/php-file-iterator/zipball/a95037b6d9e608ba092da1b23931e537cadc3c3c", + "reference": "a95037b6d9e608ba092da1b23931e537cadc3c3c", + "shasum": "" + }, + "require": { + "php": ">=8.1" + }, + "require-dev": { + "phpunit/phpunit": "^10.0" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-main": "4.0-dev" + } + }, + "autoload": { + "classmap": [ + "src/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "BSD-3-Clause" + ], + "authors": [ + { + "name": "Sebastian Bergmann", + "email": "sebastian@phpunit.de", + "role": "lead" + } + ], + "description": "FilterIterator implementation that filters files based on a list of suffixes.", + "homepage": "https://github.com/sebastianbergmann/php-file-iterator/", + "keywords": [ + "filesystem", + "iterator" + ], + "support": { + "issues": "https://github.com/sebastianbergmann/php-file-iterator/issues", + "security": "https://github.com/sebastianbergmann/php-file-iterator/security/policy", + "source": "https://github.com/sebastianbergmann/php-file-iterator/tree/4.1.0" + }, + "funding": [ + { + "url": "https://github.com/sebastianbergmann", + "type": "github" + } + ], + "time": "2023-08-31T06:24:48+00:00" + }, + { + "name": "phpunit/php-invoker", + "version": "4.0.0", + "source": { + "type": "git", + "url": "https://github.com/sebastianbergmann/php-invoker.git", + "reference": "f5e568ba02fa5ba0ddd0f618391d5a9ea50b06d7" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/sebastianbergmann/php-invoker/zipball/f5e568ba02fa5ba0ddd0f618391d5a9ea50b06d7", + "reference": "f5e568ba02fa5ba0ddd0f618391d5a9ea50b06d7", + "shasum": "" + }, + "require": { + "php": ">=8.1" + }, + "require-dev": { + "ext-pcntl": "*", + "phpunit/phpunit": "^10.0" + }, + "suggest": { + "ext-pcntl": "*" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-main": "4.0-dev" + } + }, + "autoload": { + "classmap": [ + "src/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "BSD-3-Clause" + ], + "authors": [ + { + "name": "Sebastian Bergmann", + "email": "sebastian@phpunit.de", + "role": "lead" + } + ], + "description": "Invoke callables with a timeout", + "homepage": "https://github.com/sebastianbergmann/php-invoker/", + "keywords": [ + "process" + ], + "support": { + "issues": "https://github.com/sebastianbergmann/php-invoker/issues", + "source": "https://github.com/sebastianbergmann/php-invoker/tree/4.0.0" + }, + "funding": [ + { + "url": "https://github.com/sebastianbergmann", + "type": "github" + } + ], + "time": "2023-02-03T06:56:09+00:00" + }, + { + "name": "phpunit/php-text-template", + "version": "3.0.1", + "source": { + "type": "git", + "url": "https://github.com/sebastianbergmann/php-text-template.git", + "reference": "0c7b06ff49e3d5072f057eb1fa59258bf287a748" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/sebastianbergmann/php-text-template/zipball/0c7b06ff49e3d5072f057eb1fa59258bf287a748", + "reference": "0c7b06ff49e3d5072f057eb1fa59258bf287a748", + "shasum": "" + }, + "require": { + "php": ">=8.1" + }, + "require-dev": { + "phpunit/phpunit": "^10.0" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-main": "3.0-dev" + } + }, + "autoload": { + "classmap": [ + "src/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "BSD-3-Clause" + ], + "authors": [ + { + "name": "Sebastian Bergmann", + "email": "sebastian@phpunit.de", + "role": "lead" + } + ], + "description": "Simple template engine.", + "homepage": "https://github.com/sebastianbergmann/php-text-template/", + "keywords": [ + "template" + ], + "support": { + "issues": "https://github.com/sebastianbergmann/php-text-template/issues", + "security": "https://github.com/sebastianbergmann/php-text-template/security/policy", + "source": "https://github.com/sebastianbergmann/php-text-template/tree/3.0.1" + }, + "funding": [ + { + "url": "https://github.com/sebastianbergmann", + "type": "github" + } + ], + "time": "2023-08-31T14:07:24+00:00" + }, + { + "name": "phpunit/php-timer", + "version": "6.0.0", + "source": { + "type": "git", + "url": "https://github.com/sebastianbergmann/php-timer.git", + "reference": "e2a2d67966e740530f4a3343fe2e030ffdc1161d" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/sebastianbergmann/php-timer/zipball/e2a2d67966e740530f4a3343fe2e030ffdc1161d", + "reference": "e2a2d67966e740530f4a3343fe2e030ffdc1161d", + "shasum": "" + }, + "require": { + "php": ">=8.1" + }, + "require-dev": { + "phpunit/phpunit": "^10.0" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-main": "6.0-dev" + } + }, + "autoload": { + "classmap": [ + "src/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "BSD-3-Clause" + ], + "authors": [ + { + "name": "Sebastian Bergmann", + "email": "sebastian@phpunit.de", + "role": "lead" + } + ], + "description": "Utility class for timing", + "homepage": "https://github.com/sebastianbergmann/php-timer/", + "keywords": [ + "timer" + ], + "support": { + "issues": "https://github.com/sebastianbergmann/php-timer/issues", + "source": "https://github.com/sebastianbergmann/php-timer/tree/6.0.0" + }, + "funding": [ + { + "url": "https://github.com/sebastianbergmann", + "type": "github" + } + ], + "time": "2023-02-03T06:57:52+00:00" + }, + { + "name": "phpunit/phpunit", + "version": "10.5.38", + "source": { + "type": "git", + "url": "https://github.com/sebastianbergmann/phpunit.git", + "reference": "a86773b9e887a67bc53efa9da9ad6e3f2498c132" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/sebastianbergmann/phpunit/zipball/a86773b9e887a67bc53efa9da9ad6e3f2498c132", + "reference": "a86773b9e887a67bc53efa9da9ad6e3f2498c132", + "shasum": "" + }, + "require": { + "ext-dom": "*", + "ext-json": "*", + "ext-libxml": "*", + "ext-mbstring": "*", + "ext-xml": "*", + "ext-xmlwriter": "*", + "myclabs/deep-copy": "^1.12.0", + "phar-io/manifest": "^2.0.4", + "phar-io/version": "^3.2.1", + "php": ">=8.1", + "phpunit/php-code-coverage": "^10.1.16", + "phpunit/php-file-iterator": "^4.1.0", + "phpunit/php-invoker": "^4.0.0", + "phpunit/php-text-template": "^3.0.1", + "phpunit/php-timer": "^6.0.0", + "sebastian/cli-parser": "^2.0.1", + "sebastian/code-unit": "^2.0.0", + "sebastian/comparator": "^5.0.3", + "sebastian/diff": "^5.1.1", + "sebastian/environment": "^6.1.0", + "sebastian/exporter": "^5.1.2", + "sebastian/global-state": "^6.0.2", + "sebastian/object-enumerator": "^5.0.0", + "sebastian/recursion-context": "^5.0.0", + "sebastian/type": "^4.0.0", + "sebastian/version": "^4.0.1" + }, + "suggest": { + "ext-soap": "To be able to generate mocks based on WSDL files" + }, + "bin": [ + "phpunit" + ], + "type": "library", + "extra": { + "branch-alias": { + "dev-main": "10.5-dev" + } + }, + "autoload": { + "files": [ + "src/Framework/Assert/Functions.php" + ], + "classmap": [ + "src/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "BSD-3-Clause" + ], + "authors": [ + { + "name": "Sebastian Bergmann", + "email": "sebastian@phpunit.de", + "role": "lead" + } + ], + "description": "The PHP Unit Testing framework.", + "homepage": "https://phpunit.de/", + "keywords": [ + "phpunit", + "testing", + "xunit" + ], + "support": { + "issues": "https://github.com/sebastianbergmann/phpunit/issues", + "security": "https://github.com/sebastianbergmann/phpunit/security/policy", + "source": "https://github.com/sebastianbergmann/phpunit/tree/10.5.38" + }, + "funding": [ + { + "url": "https://phpunit.de/sponsors.html", + "type": "custom" + }, + { + "url": "https://github.com/sebastianbergmann", + "type": "github" + }, + { + "url": "https://tidelift.com/funding/github/packagist/phpunit/phpunit", + "type": "tidelift" + } + ], + "time": "2024-10-28T13:06:21+00:00" + }, + { + "name": "psr/container", + "version": "2.0.2", + "source": { + "type": "git", + "url": "https://github.com/php-fig/container.git", + "reference": "c71ecc56dfe541dbd90c5360474fbc405f8d5963" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/php-fig/container/zipball/c71ecc56dfe541dbd90c5360474fbc405f8d5963", + "reference": "c71ecc56dfe541dbd90c5360474fbc405f8d5963", + "shasum": "" + }, + "require": { + "php": ">=7.4.0" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "2.0.x-dev" + } + }, + "autoload": { + "psr-4": { + "Psr\\Container\\": "src/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "PHP-FIG", + "homepage": "https://www.php-fig.org/" + } + ], + "description": "Common Container Interface (PHP FIG PSR-11)", + "homepage": "https://github.com/php-fig/container", + "keywords": [ + "PSR-11", + "container", + "container-interface", + "container-interop", + "psr" + ], + "support": { + "issues": "https://github.com/php-fig/container/issues", + "source": "https://github.com/php-fig/container/tree/2.0.2" + }, + "time": "2021-11-05T16:47:00+00:00" + }, + { + "name": "psr/event-dispatcher", + "version": "1.0.0", + "source": { + "type": "git", + "url": "https://github.com/php-fig/event-dispatcher.git", + "reference": "dbefd12671e8a14ec7f180cab83036ed26714bb0" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/php-fig/event-dispatcher/zipball/dbefd12671e8a14ec7f180cab83036ed26714bb0", + "reference": "dbefd12671e8a14ec7f180cab83036ed26714bb0", + "shasum": "" + }, + "require": { + "php": ">=7.2.0" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "1.0.x-dev" + } + }, + "autoload": { + "psr-4": { + "Psr\\EventDispatcher\\": "src/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "PHP-FIG", + "homepage": "http://www.php-fig.org/" + } + ], + "description": "Standard interfaces for event handling.", + "keywords": [ + "events", + "psr", + "psr-14" + ], + "support": { + "issues": "https://github.com/php-fig/event-dispatcher/issues", + "source": "https://github.com/php-fig/event-dispatcher/tree/1.0.0" + }, + "time": "2019-01-08T18:20:26+00:00" + }, + { + "name": "roave/security-advisories", + "version": "dev-latest", + "source": { + "type": "git", + "url": "https://github.com/Roave/SecurityAdvisories.git", + "reference": "e63317470a1b96346be224a68f9e64567e1001c3" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/Roave/SecurityAdvisories/zipball/e63317470a1b96346be224a68f9e64567e1001c3", + "reference": "e63317470a1b96346be224a68f9e64567e1001c3", + "shasum": "" + }, + "conflict": { + "3f/pygmentize": "<1.2", + "admidio/admidio": "<4.3.12", + "adodb/adodb-php": "<=5.20.20|>=5.21,<=5.21.3", + "aheinze/cockpit": "<2.2", + "aimeos/ai-admin-graphql": ">=2022.04.1,<2022.10.10|>=2023.04.1,<2023.10.6|>=2024.04.1,<2024.07.2", + "aimeos/ai-admin-jsonadm": "<2020.10.13|>=2021.04.1,<2021.10.6|>=2022.04.1,<2022.10.3|>=2023.04.1,<2023.10.4|==2024.04.1", + "aimeos/ai-client-html": ">=2020.04.1,<2020.10.27|>=2021.04.1,<2021.10.22|>=2022.04.1,<2022.10.13|>=2023.04.1,<2023.10.15|>=2024.04.1,<2024.04.7", + "aimeos/ai-controller-frontend": "<2020.10.15|>=2021.04.1,<2021.10.8|>=2022.04.1,<2022.10.8|>=2023.04.1,<2023.10.9|==2024.04.1", + "aimeos/aimeos-core": ">=2022.04.1,<2022.10.17|>=2023.04.1,<2023.10.17|>=2024.04.1,<2024.04.7", + "aimeos/aimeos-typo3": "<19.10.12|>=20,<20.10.5", + "airesvsg/acf-to-rest-api": "<=3.1", + "akaunting/akaunting": "<2.1.13", + "akeneo/pim-community-dev": "<5.0.119|>=6,<6.0.53", + "alextselegidis/easyappointments": "<1.5", + "alterphp/easyadmin-extension-bundle": ">=1.2,<1.2.11|>=1.3,<1.3.1", + "amazing/media2click": ">=1,<1.3.3", + "ameos/ameos_tarteaucitron": "<1.2.23", + "amphp/artax": "<1.0.6|>=2,<2.0.6", + "amphp/http": "<=1.7.2|>=2,<=2.1", + "amphp/http-client": ">=4,<4.4", + "anchorcms/anchor-cms": "<=0.12.7", + "andreapollastri/cipi": "<=3.1.15", + "andrewhaine/silverstripe-form-capture": ">=0.2,<=0.2.3|>=1,<1.0.2|>=2,<2.2.5", + "apache-solr-for-typo3/solr": "<2.8.3", + "apereo/phpcas": "<1.6", + "api-platform/core": ">=2.2,<2.2.10|>=2.3,<2.3.6|>=2.6,<2.7.10|>=3,<3.0.12|>=3.1,<3.1.3", + "appwrite/server-ce": "<=1.2.1", + "arc/web": "<3", + "area17/twill": "<1.2.5|>=2,<2.5.3", + "artesaos/seotools": "<0.17.2", + "asymmetricrypt/asymmetricrypt": "<9.9.99", + "athlon1600/php-proxy": "<=5.1", + "athlon1600/php-proxy-app": "<=3", + "austintoddj/canvas": "<=3.4.2", + "auth0/wordpress": "<=4.6", + "automad/automad": "<2.0.0.0-alpha5", + "automattic/jetpack": "<9.8", + "awesome-support/awesome-support": "<=6.0.7", + "aws/aws-sdk-php": "<3.288.1", + "azuracast/azuracast": "<0.18.3", + "backdrop/backdrop": "<1.27.3|>=1.28,<1.28.2", + "backpack/crud": "<3.4.9", + "bacula-web/bacula-web": "<8.0.0.0-RC2-dev", + "badaso/core": "<2.7", + "bagisto/bagisto": "<2.1", + "barrelstrength/sprout-base-email": "<1.2.7", + "barrelstrength/sprout-forms": "<3.9", + "barryvdh/laravel-translation-manager": "<0.6.2", + "barzahlen/barzahlen-php": "<2.0.1", + "baserproject/basercms": "<=5.1.1", + "bassjobsen/bootstrap-3-typeahead": ">4.0.2", + "bbpress/bbpress": "<2.6.5", + "bcosca/fatfree": "<3.7.2", + "bedita/bedita": "<4", + "bigfork/silverstripe-form-capture": ">=3,<3.1.1", + "billz/raspap-webgui": "<=3.1.4", + "bk2k/bootstrap-package": ">=7.1,<7.1.2|>=8,<8.0.8|>=9,<9.0.4|>=9.1,<9.1.3|>=10,<10.0.10|>=11,<11.0.3", + "blueimp/jquery-file-upload": "==6.4.4", + "bmarshall511/wordpress_zero_spam": "<5.2.13", + "bolt/bolt": "<3.7.2", + "bolt/core": "<=4.2", + "born05/craft-twofactorauthentication": "<3.3.4", + "bottelet/flarepoint": "<2.2.1", + "bref/bref": "<2.1.17", + "brightlocal/phpwhois": "<=4.2.5", + "brotkrueml/codehighlight": "<2.7", + "brotkrueml/schema": "<1.13.1|>=2,<2.5.1", + "brotkrueml/typo3-matomo-integration": "<1.3.2", + "buddypress/buddypress": "<7.2.1", + "bugsnag/bugsnag-laravel": ">=2,<2.0.2", + "bytefury/crater": "<6.0.2", + "cachethq/cachet": "<2.5.1", + "cakephp/cakephp": "<3.10.3|>=4,<4.0.10|>=4.1,<4.1.4|>=4.2,<4.2.12|>=4.3,<4.3.11|>=4.4,<4.4.10", + "cakephp/database": ">=4.2,<4.2.12|>=4.3,<4.3.11|>=4.4,<4.4.10", + "cardgate/magento2": "<2.0.33", + "cardgate/woocommerce": "<=3.1.15", + "cart2quote/module-quotation": ">=4.1.6,<=4.4.5|>=5,<5.4.4", + "cart2quote/module-quotation-encoded": ">=4.1.6,<=4.4.5|>=5,<5.4.4", + "cartalyst/sentry": "<=2.1.6", + "catfan/medoo": "<1.7.5", + "causal/oidc": "<2.1", + "cecil/cecil": "<7.47.1", + "centreon/centreon": "<22.10.15", + "cesnet/simplesamlphp-module-proxystatistics": "<3.1", + "chriskacerguis/codeigniter-restserver": "<=2.7.1", + "civicrm/civicrm-core": ">=4.2,<4.2.9|>=4.3,<4.3.3", + "ckeditor/ckeditor": "<4.24", + "cockpit-hq/cockpit": "<2.7|==2.7", + "codeception/codeception": "<3.1.3|>=4,<4.1.22", + "codeigniter/framework": "<3.1.9", + "codeigniter4/framework": "<4.4.7", + "codeigniter4/shield": "<1.0.0.0-beta8", + "codiad/codiad": "<=2.8.4", + "composer/composer": "<1.10.27|>=2,<2.2.24|>=2.3,<2.7.7", + "concrete5/concrete5": "<9.3.4", + "concrete5/core": "<8.5.8|>=9,<9.1", + "contao-components/mediaelement": ">=2.14.2,<2.21.1", + "contao/comments-bundle": ">=2,<4.13.40|>=5.0.0.0-RC1-dev,<5.3.4", + "contao/contao": "<=5.4.1", + "contao/core": "<3.5.39", + "contao/core-bundle": "<4.13.49|>=5,<5.3.15|>=5.4,<5.4.3", + "contao/listing-bundle": ">=3,<=3.5.30|>=4,<4.4.8", + "contao/managed-edition": "<=1.5", + "corveda/phpsandbox": "<1.3.5", + "cosenary/instagram": "<=2.3", + "craftcms/cms": "<4.6.2|>=5,<=5.2.2", + "croogo/croogo": "<4", + "cuyz/valinor": "<0.12", + "czim/file-handling": "<1.5|>=2,<2.3", + "czproject/git-php": "<4.0.3", + "damienharper/auditor-bundle": "<5.2.6", + "dapphp/securimage": "<3.6.6", + "darylldoyle/safe-svg": "<1.9.10", + "datadog/dd-trace": ">=0.30,<0.30.2", + "datatables/datatables": "<1.10.10", + "david-garcia/phpwhois": "<=4.3.1", + "dbrisinajumi/d2files": "<1", + "dcat/laravel-admin": "<=2.1.3", + "derhansen/fe_change_pwd": "<2.0.5|>=3,<3.0.3", + "derhansen/sf_event_mgt": "<4.3.1|>=5,<5.1.1|>=7,<7.4", + "desperado/xml-bundle": "<=0.1.7", + "dev-lancer/minecraft-motd-parser": "<=1.0.5", + "devgroup/dotplant": "<2020.09.14-dev", + "directmailteam/direct-mail": "<6.0.3|>=7,<7.0.3|>=8,<9.5.2", + "doctrine/annotations": "<1.2.7", + "doctrine/cache": ">=1,<1.3.2|>=1.4,<1.4.2", + "doctrine/common": "<2.4.3|>=2.5,<2.5.1", + "doctrine/dbal": ">=2,<2.0.8|>=2.1,<2.1.2|>=3,<3.1.4", + "doctrine/doctrine-bundle": "<1.5.2", + "doctrine/doctrine-module": "<0.7.2", + "doctrine/mongodb-odm": "<1.0.2", + "doctrine/mongodb-odm-bundle": "<3.0.1", + "doctrine/orm": ">=1,<1.2.4|>=2,<2.4.8|>=2.5,<2.5.1|>=2.8.3,<2.8.4", + "dolibarr/dolibarr": "<19.0.2", + "dompdf/dompdf": "<2.0.4", + "doublethreedigital/guest-entries": "<3.1.2", + "drupal/core": ">=6,<6.38|>=7,<7.96|>=8,<10.2.9|>=10.3,<10.3.6|>=11,<11.0.5", + "drupal/core-recommended": ">=8,<10.2.9|>=10.3,<10.3.6|>=11,<11.0.5", + "drupal/drupal": ">=5,<5.11|>=6,<6.38|>=7,<7.80|>=8,<10.2.9|>=10.3,<10.3.6|>=11,<11.0.5", + "duncanmcclean/guest-entries": "<3.1.2", + "dweeves/magmi": "<=0.7.24", + "ec-cube/ec-cube": "<2.4.4|>=2.11,<=2.17.1|>=3,<=3.0.18.0-patch4|>=4,<=4.1.2", + "ecodev/newsletter": "<=4", + "ectouch/ectouch": "<=2.7.2", + "egroupware/egroupware": "<23.1.20240624", + "elefant/cms": "<2.0.7", + "elgg/elgg": "<3.3.24|>=4,<4.0.5", + "elijaa/phpmemcacheadmin": "<=1.3", + "encore/laravel-admin": "<=1.8.19", + "endroid/qr-code-bundle": "<3.4.2", + "enhavo/enhavo-app": "<=0.13.1", + "enshrined/svg-sanitize": "<0.15", + "erusev/parsedown": "<1.7.2", + "ether/logs": "<3.0.4", + "evolutioncms/evolution": "<=3.2.3", + "exceedone/exment": "<4.4.3|>=5,<5.0.3", + "exceedone/laravel-admin": "<2.2.3|==3", + "ezsystems/demobundle": ">=5.4,<5.4.6.1-dev", + "ezsystems/ez-support-tools": ">=2.2,<2.2.3", + "ezsystems/ezdemo-ls-extension": ">=5.4,<5.4.2.1-dev", + "ezsystems/ezfind-ls": ">=5.3,<5.3.6.1-dev|>=5.4,<5.4.11.1-dev|>=2017.12,<2017.12.0.1-dev", + "ezsystems/ezplatform": "<=1.13.6|>=2,<=2.5.24", + "ezsystems/ezplatform-admin-ui": ">=1.3,<1.3.5|>=1.4,<1.4.6|>=1.5,<1.5.29|>=2.3,<2.3.26|>=3.3,<3.3.39", + "ezsystems/ezplatform-admin-ui-assets": ">=4,<4.2.1|>=5,<5.0.1|>=5.1,<5.1.1", + "ezsystems/ezplatform-graphql": ">=1.0.0.0-RC1-dev,<1.0.13|>=2.0.0.0-beta1,<2.3.12", + "ezsystems/ezplatform-kernel": "<1.2.5.1-dev|>=1.3,<1.3.35", + "ezsystems/ezplatform-rest": ">=1.2,<=1.2.2|>=1.3,<1.3.8", + "ezsystems/ezplatform-richtext": ">=2.3,<2.3.7.1-dev|>=3.3,<3.3.40", + "ezsystems/ezplatform-solr-search-engine": ">=1.7,<1.7.12|>=2,<2.0.2|>=3.3,<3.3.15", + "ezsystems/ezplatform-user": ">=1,<1.0.1", + "ezsystems/ezpublish-kernel": "<6.13.8.2-dev|>=7,<7.5.31", + "ezsystems/ezpublish-legacy": "<=2017.12.7.3|>=2018.6,<=2019.03.5.1", + "ezsystems/platform-ui-assets-bundle": ">=4.2,<4.2.3", + "ezsystems/repository-forms": ">=2.3,<2.3.2.1-dev|>=2.5,<2.5.15", + "ezyang/htmlpurifier": "<=4.2", + "facade/ignition": "<1.16.15|>=2,<2.4.2|>=2.5,<2.5.2", + "facturascripts/facturascripts": "<=2022.08", + "fastly/magento2": "<1.2.26", + "feehi/cms": "<=2.1.1", + "feehi/feehicms": "<=2.1.1", + "fenom/fenom": "<=2.12.1", + "filament/actions": ">=3.2,<3.2.123", + "filament/infolists": ">=3,<3.2.115", + "filament/tables": ">=3,<3.2.115", + "filegator/filegator": "<7.8", + "filp/whoops": "<2.1.13", + "fineuploader/php-traditional-server": "<=1.2.2", + "firebase/php-jwt": "<6", + "fisharebest/webtrees": "<=2.1.18", + "fixpunkt/fp-masterquiz": "<2.2.1|>=3,<3.5.2", + "fixpunkt/fp-newsletter": "<1.1.1|>=2,<2.1.2|>=2.2,<3.2.6", + "flarum/core": "<1.8.5", + "flarum/flarum": "<0.1.0.0-beta8", + "flarum/framework": "<1.8.5", + "flarum/mentions": "<1.6.3", + "flarum/sticky": ">=0.1.0.0-beta14,<=0.1.0.0-beta15", + "flarum/tags": "<=0.1.0.0-beta13", + "floriangaerber/magnesium": "<0.3.1", + "fluidtypo3/vhs": "<5.1.1", + "fof/byobu": ">=0.3.0.0-beta2,<1.1.7", + "fof/upload": "<1.2.3", + "foodcoopshop/foodcoopshop": ">=3.2,<3.6.1", + "fooman/tcpdf": "<6.2.22", + "forkcms/forkcms": "<5.11.1", + "fossar/tcpdf-parser": "<6.2.22", + "francoisjacquet/rosariosis": "<=11.5.1", + "frappant/frp-form-answers": "<3.1.2|>=4,<4.0.2", + "friendsofsymfony/oauth2-php": "<1.3", + "friendsofsymfony/rest-bundle": ">=1.2,<1.2.2", + "friendsofsymfony/user-bundle": ">=1,<1.3.5", + "friendsofsymfony1/swiftmailer": ">=4,<5.4.13|>=6,<6.2.5", + "friendsofsymfony1/symfony1": ">=1.1,<1.5.19", + "friendsoftypo3/mediace": ">=7.6.2,<7.6.5", + "friendsoftypo3/openid": ">=4.5,<4.5.31|>=4.7,<4.7.16|>=6,<6.0.11|>=6.1,<6.1.6", + "froala/wysiwyg-editor": "<3.2.7|>=4.0.1,<=4.1.3", + "froxlor/froxlor": "<=2.2.0.0-RC3", + "frozennode/administrator": "<=5.0.12", + "fuel/core": "<1.8.1", + "funadmin/funadmin": "<=5.0.2", + "gaoming13/wechat-php-sdk": "<=1.10.2", + "genix/cms": "<=1.1.11", + "getformwork/formwork": "<1.13.1|==2.0.0.0-beta1", + "getgrav/grav": "<1.7.46", + "getkirby/cms": "<=3.6.6.5|>=3.7,<=3.7.5.4|>=3.8,<=3.8.4.3|>=3.9,<=3.9.8.1|>=3.10,<=3.10.1|>=4,<=4.3", + "getkirby/kirby": "<=2.5.12", + "getkirby/panel": "<2.5.14", + "getkirby/starterkit": "<=3.7.0.2", + "gilacms/gila": "<=1.15.4", + "gleez/cms": "<=1.3|==2", + "globalpayments/php-sdk": "<2", + "gogentooss/samlbase": "<1.2.7", + "google/protobuf": "<3.15", + "gos/web-socket-bundle": "<1.10.4|>=2,<2.6.1|>=3,<3.3", + "gree/jose": "<2.2.1", + "gregwar/rst": "<1.0.3", + "grumpydictator/firefly-iii": "<6.1.17", + "gugoan/economizzer": "<=0.9.0.0-beta1", + "guzzlehttp/guzzle": "<6.5.8|>=7,<7.4.5", + "guzzlehttp/psr7": "<1.9.1|>=2,<2.4.5", + "haffner/jh_captcha": "<=2.1.3|>=3,<=3.0.2", + "harvesthq/chosen": "<1.8.7", + "helloxz/imgurl": "<=2.31", + "hhxsv5/laravel-s": "<3.7.36", + "hillelcoren/invoice-ninja": "<5.3.35", + "himiklab/yii2-jqgrid-widget": "<1.0.8", + "hjue/justwriting": "<=1", + "hov/jobfair": "<1.0.13|>=2,<2.0.2", + "httpsoft/http-message": "<1.0.12", + "hyn/multi-tenant": ">=5.6,<5.7.2", + "ibexa/admin-ui": ">=4.2,<4.2.3|>=4.6.0.0-beta1,<4.6.9", + "ibexa/core": ">=4,<4.0.7|>=4.1,<4.1.4|>=4.2,<4.2.3|>=4.5,<4.5.6|>=4.6,<4.6.2", + "ibexa/fieldtype-richtext": ">=4.6,<4.6.10", + "ibexa/graphql": ">=2.5,<2.5.31|>=3.3,<3.3.28|>=4.2,<4.2.3", + "ibexa/post-install": "<=1.0.4", + "ibexa/solr": ">=4.5,<4.5.4", + "ibexa/user": ">=4,<4.4.3", + "icecoder/icecoder": "<=8.1", + "idno/known": "<=1.3.1", + "ilicmiljan/secure-props": ">=1.2,<1.2.2", + "illuminate/auth": "<5.5.10", + "illuminate/cookie": ">=4,<=4.0.11|>=4.1,<6.18.31|>=7,<7.22.4", + "illuminate/database": "<6.20.26|>=7,<7.30.5|>=8,<8.40", + "illuminate/encryption": ">=4,<=4.0.11|>=4.1,<=4.1.31|>=4.2,<=4.2.22|>=5,<=5.0.35|>=5.1,<=5.1.46|>=5.2,<=5.2.45|>=5.3,<=5.3.31|>=5.4,<=5.4.36|>=5.5,<5.5.40|>=5.6,<5.6.15", + "illuminate/view": "<6.20.42|>=7,<7.30.6|>=8,<8.75", + "imdbphp/imdbphp": "<=5.1.1", + "impresscms/impresscms": "<=1.4.5", + "impresspages/impresspages": "<=1.0.12", + "in2code/femanager": "<5.5.3|>=6,<6.3.4|>=7,<7.2.3", + "in2code/ipandlanguageredirect": "<5.1.2", + "in2code/lux": "<17.6.1|>=18,<24.0.2", + "in2code/powermail": "<7.5.1|>=8,<8.5.1|>=9,<10.9.1|>=11,<12.4.1", + "innologi/typo3-appointments": "<2.0.6", + "intelliants/subrion": "<4.2.2", + "inter-mediator/inter-mediator": "==5.5", + "ipl/web": "<0.10.1", + "islandora/islandora": ">=2,<2.4.1", + "ivankristianto/phpwhois": "<=4.3", + "jackalope/jackalope-doctrine-dbal": "<1.7.4", + "james-heinrich/getid3": "<1.9.21", + "james-heinrich/phpthumb": "<1.7.12", + "jasig/phpcas": "<1.3.3", + "jcbrand/converse.js": "<3.3.3", + "johnbillion/wp-crontrol": "<1.16.2", + "joomla/application": "<1.0.13", + "joomla/archive": "<1.1.12|>=2,<2.0.1", + "joomla/filesystem": "<1.6.2|>=2,<2.0.1", + "joomla/filter": "<1.4.4|>=2,<2.0.1", + "joomla/framework": "<1.5.7|>=2.5.4,<=3.8.12", + "joomla/input": ">=2,<2.0.2", + "joomla/joomla-cms": ">=2.5,<3.9.12", + "joomla/session": "<1.3.1", + "joyqi/hyper-down": "<=2.4.27", + "jsdecena/laracom": "<2.0.9", + "jsmitty12/phpwhois": "<5.1", + "juzaweb/cms": "<=3.4", + "jweiland/events2": "<8.3.8|>=9,<9.0.6", + "kazist/phpwhois": "<=4.2.6", + "kelvinmo/simplexrd": "<3.1.1", + "kevinpapst/kimai2": "<1.16.7", + "khodakhah/nodcms": "<=3", + "kimai/kimai": "<=2.20.1", + "kitodo/presentation": "<3.2.3|>=3.3,<3.3.4", + "klaviyo/magento2-extension": ">=1,<3", + "knplabs/knp-snappy": "<=1.4.2", + "kohana/core": "<3.3.3", + "krayin/laravel-crm": "<=1.3", + "kreait/firebase-php": ">=3.2,<3.8.1", + "kumbiaphp/kumbiapp": "<=1.1.1", + "la-haute-societe/tcpdf": "<6.2.22", + "laminas/laminas-diactoros": "<2.18.1|==2.19|==2.20|==2.21|==2.22|==2.23|>=2.24,<2.24.2|>=2.25,<2.25.2", + "laminas/laminas-form": "<2.17.1|>=3,<3.0.2|>=3.1,<3.1.1", + "laminas/laminas-http": "<2.14.2", + "lara-zeus/artemis": ">=1,<=1.0.6", + "lara-zeus/dynamic-dashboard": ">=3,<=3.0.1", + "laravel/fortify": "<1.11.1", + "laravel/framework": "<6.20.44|>=7,<7.30.6|>=8,<8.75", + "laravel/laravel": ">=5.4,<5.4.22", + "laravel/reverb": "<1.4", + "laravel/socialite": ">=1,<2.0.10", + "latte/latte": "<2.10.8", + "lavalite/cms": "<=9|==10.1", + "lcobucci/jwt": ">=3.4,<3.4.6|>=4,<4.0.4|>=4.1,<4.1.5", + "league/commonmark": "<0.18.3", + "league/flysystem": "<1.1.4|>=2,<2.1.1", + "league/oauth2-server": ">=8.3.2,<8.4.2|>=8.5,<8.5.3", + "lexik/jwt-authentication-bundle": "<2.10.7|>=2.11,<2.11.3", + "libreform/libreform": ">=2,<=2.0.8", + "librenms/librenms": "<2017.08.18", + "liftkit/database": "<2.13.2", + "lightsaml/lightsaml": "<1.3.5", + "limesurvey/limesurvey": "<6.5.12", + "livehelperchat/livehelperchat": "<=3.91", + "livewire/livewire": "<2.12.7|>=3.0.0.0-beta1,<3.5.2", + "lms/routes": "<2.1.1", + "localizationteam/l10nmgr": "<7.4|>=8,<8.7|>=9,<9.2", + "luyadev/yii-helpers": "<1.2.1", + "maestroerror/php-heic-to-jpg": "<1.0.5", + "magento/community-edition": "<2.4.5|==2.4.5|>=2.4.5.0-patch1,<2.4.5.0-patch10|==2.4.6|>=2.4.6.0-patch1,<2.4.6.0-patch8|>=2.4.7.0-beta1,<2.4.7.0-patch3", + "magento/core": "<=1.9.4.5", + "magento/magento1ce": "<1.9.4.3-dev", + "magento/magento1ee": ">=1,<1.14.4.3-dev", + "magento/product-community-edition": "<2.4.4.0-patch9|>=2.4.5,<2.4.5.0-patch8|>=2.4.6,<2.4.6.0-patch6|>=2.4.7,<2.4.7.0-patch1", + "magneto/core": "<1.9.4.4-dev", + "maikuolan/phpmussel": ">=1,<1.6", + "mainwp/mainwp": "<=4.4.3.3", + "mantisbt/mantisbt": "<=2.26.3", + "marcwillmann/turn": "<0.3.3", + "matyhtf/framework": "<3.0.6", + "mautic/core": "<4.4.13|>=5,<5.1.1", + "mautic/core-lib": ">=1.0.0.0-beta,<4.4.13|>=5.0.0.0-alpha,<5.1.1", + "maximebf/debugbar": "<1.19", + "mdanter/ecc": "<2", + "mediawiki/cargo": "<3.6.1", + "mediawiki/core": "<1.39.5|==1.40", + "mediawiki/matomo": "<2.4.3", + "mediawiki/semantic-media-wiki": "<4.0.2", + "melisplatform/melis-asset-manager": "<5.0.1", + "melisplatform/melis-cms": "<5.0.1", + "melisplatform/melis-front": "<5.0.1", + "mezzio/mezzio-swoole": "<3.7|>=4,<4.3", + "mgallegos/laravel-jqgrid": "<=1.3", + "microsoft/microsoft-graph": ">=1.16,<1.109.1|>=2,<2.0.1", + "microsoft/microsoft-graph-beta": "<2.0.1", + "microsoft/microsoft-graph-core": "<2.0.2", + "microweber/microweber": "<=2.0.16", + "mikehaertl/php-shellcommand": "<1.6.1", + "miniorange/miniorange-saml": "<1.4.3", + "mittwald/typo3_forum": "<1.2.1", + "mobiledetect/mobiledetectlib": "<2.8.32", + "modx/revolution": "<=2.8.3.0-patch", + "mojo42/jirafeau": "<4.4", + "mongodb/mongodb": ">=1,<1.9.2", + "monolog/monolog": ">=1.8,<1.12", + "moodle/moodle": "<4.3.6|>=4.4.0.0-beta,<4.4.2", + "mos/cimage": "<0.7.19", + "movim/moxl": ">=0.8,<=0.10", + "movingbytes/social-network": "<=1.2.1", + "mpdf/mpdf": "<=7.1.7", + "munkireport/comment": "<4.1", + "munkireport/managedinstalls": "<2.6", + "munkireport/munki_facts": "<1.5", + "munkireport/munkireport": ">=2.5.3,<5.6.3", + "munkireport/reportdata": "<3.5", + "munkireport/softwareupdate": "<1.6", + "mustache/mustache": ">=2,<2.14.1", + "namshi/jose": "<2.2", + "nategood/httpful": "<1", + "neoan3-apps/template": "<1.1.1", + "neorazorx/facturascripts": "<2022.04", + "neos/flow": ">=1,<1.0.4|>=1.1,<1.1.1|>=2,<2.0.1|>=2.3,<2.3.16|>=3,<3.0.12|>=3.1,<3.1.10|>=3.2,<3.2.13|>=3.3,<3.3.13|>=4,<4.0.6", + "neos/form": ">=1.2,<4.3.3|>=5,<5.0.9|>=5.1,<5.1.3", + "neos/media-browser": "<7.3.19|>=8,<8.0.16|>=8.1,<8.1.11|>=8.2,<8.2.11|>=8.3,<8.3.9", + "neos/neos": ">=1.1,<1.1.3|>=1.2,<1.2.13|>=2,<2.0.4|>=2.3,<3.0.20|>=3.1,<3.1.18|>=3.2,<3.2.14|>=3.3,<5.3.10|>=7,<7.0.9|>=7.1,<7.1.7|>=7.2,<7.2.6|>=7.3,<7.3.4|>=8,<8.0.2", + "neos/swiftmailer": "<5.4.5", + "netgen/tagsbundle": ">=3.4,<3.4.11|>=4,<4.0.15", + "nette/application": ">=2,<2.0.19|>=2.1,<2.1.13|>=2.2,<2.2.10|>=2.3,<2.3.14|>=2.4,<2.4.16|>=3,<3.0.6", + "nette/nette": ">=2,<2.0.19|>=2.1,<2.1.13", + "nilsteampassnet/teampass": "<3.0.10", + "nonfiction/nterchange": "<4.1.1", + "notrinos/notrinos-erp": "<=0.7", + "noumo/easyii": "<=0.9", + "novaksolutions/infusionsoft-php-sdk": "<1", + "nukeviet/nukeviet": "<4.5.02", + "nyholm/psr7": "<1.6.1", + "nystudio107/craft-seomatic": "<3.4.12", + "nzedb/nzedb": "<0.8", + "nzo/url-encryptor-bundle": ">=4,<4.3.2|>=5,<5.0.1", + "october/backend": "<1.1.2", + "october/cms": "<1.0.469|==1.0.469|==1.0.471|==1.1.1", + "october/october": "<=3.6.4", + "october/rain": "<1.0.472|>=1.1,<1.1.2", + "october/system": "<1.0.476|>=1.1,<1.1.12|>=2,<2.2.34|>=3,<3.5.15", + "omeka/omeka-s": "<4.0.3", + "onelogin/php-saml": "<2.10.4", + "oneup/uploader-bundle": ">=1,<1.9.3|>=2,<2.1.5", + "open-web-analytics/open-web-analytics": "<1.7.4", + "opencart/opencart": ">=0", + "openid/php-openid": "<2.3", + "openmage/magento-lts": "<20.10.1", + "opensolutions/vimbadmin": "<=3.0.15", + "opensource-workshop/connect-cms": "<1.7.2|>=2,<2.3.2", + "orchid/platform": ">=9,<9.4.4|>=14.0.0.0-alpha4,<14.5", + "oro/calendar-bundle": ">=4.2,<=4.2.6|>=5,<=5.0.6|>=5.1,<5.1.1", + "oro/commerce": ">=4.1,<5.0.11|>=5.1,<5.1.1", + "oro/crm": ">=1.7,<1.7.4|>=3.1,<4.1.17|>=4.2,<4.2.7", + "oro/crm-call-bundle": ">=4.2,<=4.2.5|>=5,<5.0.4|>=5.1,<5.1.1", + "oro/customer-portal": ">=4.1,<=4.1.13|>=4.2,<=4.2.10|>=5,<=5.0.11|>=5.1,<=5.1.3", + "oro/platform": ">=1.7,<1.7.4|>=3.1,<3.1.29|>=4.1,<4.1.17|>=4.2,<=4.2.10|>=5,<=5.0.12|>=5.1,<=5.1.3", + "oveleon/contao-cookiebar": "<1.16.3|>=2,<2.1.3", + "oxid-esales/oxideshop-ce": "<4.5", + "oxid-esales/paymorrow-module": ">=1,<1.0.2|>=2,<2.0.1", + "packbackbooks/lti-1-3-php-library": "<5", + "padraic/humbug_get_contents": "<1.1.2", + "pagarme/pagarme-php": "<3", + "pagekit/pagekit": "<=1.0.18", + "paragonie/ecc": "<2.0.1", + "paragonie/random_compat": "<2", + "passbolt/passbolt_api": "<4.6.2", + "paypal/adaptivepayments-sdk-php": "<=3.9.2", + "paypal/invoice-sdk-php": "<=3.9", + "paypal/merchant-sdk-php": "<3.12", + "paypal/permissions-sdk-php": "<=3.9.1", + "pear/archive_tar": "<1.4.14", + "pear/auth": "<1.2.4", + "pear/crypt_gpg": "<1.6.7", + "pear/pear": "<=1.10.1", + "pegasus/google-for-jobs": "<1.5.1|>=2,<2.1.1", + "personnummer/personnummer": "<3.0.2", + "phanan/koel": "<5.1.4", + "phenx/php-svg-lib": "<0.5.2", + "php-censor/php-censor": "<2.0.13|>=2.1,<2.1.5", + "php-mod/curl": "<2.3.2", + "phpbb/phpbb": "<3.3.11", + "phpems/phpems": ">=6,<=6.1.3", + "phpfastcache/phpfastcache": "<6.1.5|>=7,<7.1.2|>=8,<8.0.7", + "phpmailer/phpmailer": "<6.5", + "phpmussel/phpmussel": ">=1,<1.6", + "phpmyadmin/phpmyadmin": "<5.2.1", + "phpmyfaq/phpmyfaq": "<3.2.5|==3.2.5", + "phpoffice/common": "<0.2.9", + "phpoffice/phpexcel": "<1.8.1", + "phpoffice/phpspreadsheet": "<1.29.2|>=2,<2.1.1|>=2.2,<2.3", + "phpseclib/phpseclib": "<2.0.47|>=3,<3.0.36", + "phpservermon/phpservermon": "<3.6", + "phpsysinfo/phpsysinfo": "<3.4.3", + "phpunit/phpunit": ">=4.8.19,<4.8.28|>=5.0.10,<5.6.3", + "phpwhois/phpwhois": "<=4.2.5", + "phpxmlrpc/extras": "<0.6.1", + "phpxmlrpc/phpxmlrpc": "<4.9.2", + "pi/pi": "<=2.5", + "pimcore/admin-ui-classic-bundle": "<1.5.4", + "pimcore/customer-management-framework-bundle": "<4.0.6", + "pimcore/data-hub": "<1.2.4", + "pimcore/data-importer": "<1.8.9|>=1.9,<1.9.3", + "pimcore/demo": "<10.3", + "pimcore/ecommerce-framework-bundle": "<1.0.10", + "pimcore/perspective-editor": "<1.5.1", + "pimcore/pimcore": "<11.2.4", + "pixelfed/pixelfed": "<0.11.11", + "plotly/plotly.js": "<2.25.2", + "pocketmine/bedrock-protocol": "<8.0.2", + "pocketmine/pocketmine-mp": "<5.11.2", + "pocketmine/raklib": ">=0.14,<0.14.6|>=0.15,<0.15.1", + "pressbooks/pressbooks": "<5.18", + "prestashop/autoupgrade": ">=4,<4.10.1", + "prestashop/blockreassurance": "<=5.1.3", + "prestashop/blockwishlist": ">=2,<2.1.1", + "prestashop/contactform": ">=1.0.1,<4.3", + "prestashop/gamification": "<2.3.2", + "prestashop/prestashop": "<8.1.6", + "prestashop/productcomments": "<5.0.2", + "prestashop/ps_emailsubscription": "<2.6.1", + "prestashop/ps_facetedsearch": "<3.4.1", + "prestashop/ps_linklist": "<3.1", + "privatebin/privatebin": "<1.4|>=1.5,<1.7.4", + "processwire/processwire": "<=3.0.229", + "propel/propel": ">=2.0.0.0-alpha1,<=2.0.0.0-alpha7", + "propel/propel1": ">=1,<=1.7.1", + "pterodactyl/panel": "<1.11.8", + "ptheofan/yii2-statemachine": ">=2.0.0.0-RC1-dev,<=2", + "ptrofimov/beanstalk_console": "<1.7.14", + "pubnub/pubnub": "<6.1", + "pusher/pusher-php-server": "<2.2.1", + "pwweb/laravel-core": "<=0.3.6.0-beta", + "pxlrbt/filament-excel": "<1.1.14|>=2.0.0.0-alpha,<2.3.3", + "pyrocms/pyrocms": "<=3.9.1", + "qcubed/qcubed": "<=3.1.1", + "quickapps/cms": "<=2.0.0.0-beta2", + "rainlab/blog-plugin": "<1.4.1", + "rainlab/debugbar-plugin": "<3.1", + "rainlab/user-plugin": "<=1.4.5", + "rankmath/seo-by-rank-math": "<=1.0.95", + "rap2hpoutre/laravel-log-viewer": "<0.13", + "react/http": ">=0.7,<1.9", + "really-simple-plugins/complianz-gdpr": "<6.4.2", + "redaxo/source": "<=5.17.1", + "remdex/livehelperchat": "<4.29", + "reportico-web/reportico": "<=8.1", + "rhukster/dom-sanitizer": "<1.0.7", + "rmccue/requests": ">=1.6,<1.8", + "robrichards/xmlseclibs": ">=1,<3.0.4", + "roots/soil": "<4.1", + "rudloff/alltube": "<3.0.3", + "s-cart/core": "<6.9", + "s-cart/s-cart": "<6.9", + "sabberworm/php-css-parser": ">=1,<1.0.1|>=2,<2.0.1|>=3,<3.0.1|>=4,<4.0.1|>=5,<5.0.9|>=5.1,<5.1.3|>=5.2,<5.2.1|>=6,<6.0.2|>=7,<7.0.4|>=8,<8.0.1|>=8.1,<8.1.1|>=8.2,<8.2.1|>=8.3,<8.3.1", + "sabre/dav": ">=1.6,<1.7.11|>=1.8,<1.8.9", + "scheb/two-factor-bundle": "<3.26|>=4,<4.11", + "sensiolabs/connect": "<4.2.3", + "serluck/phpwhois": "<=4.2.6", + "sfroemken/url_redirect": "<=1.2.1", + "sheng/yiicms": "<=1.2", + "shopware/core": "<=6.5.8.12|>=6.6,<=6.6.5", + "shopware/platform": "<=6.5.8.12|>=6.6,<=6.6.5", + "shopware/production": "<=6.3.5.2", + "shopware/shopware": "<=5.7.17", + "shopware/storefront": "<=6.4.8.1|>=6.5.8,<6.5.8.7-dev", + "shopxo/shopxo": "<=6.1", + "showdoc/showdoc": "<2.10.4", + "silverstripe-australia/advancedreports": ">=1,<=2", + "silverstripe/admin": "<1.13.19|>=2,<2.1.8", + "silverstripe/assets": ">=1,<1.11.1", + "silverstripe/cms": "<4.11.3", + "silverstripe/comments": ">=1.3,<3.1.1", + "silverstripe/forum": "<=0.6.1|>=0.7,<=0.7.3", + "silverstripe/framework": "<5.2.16", + "silverstripe/graphql": ">=2,<2.0.5|>=3,<3.8.2|>=4,<4.3.7|>=5,<5.1.3", + "silverstripe/hybridsessions": ">=1,<2.4.1|>=2.5,<2.5.1", + "silverstripe/recipe-cms": ">=4.5,<4.5.3", + "silverstripe/registry": ">=2.1,<2.1.2|>=2.2,<2.2.1", + "silverstripe/reports": "<5.2.3", + "silverstripe/restfulserver": ">=1,<1.0.9|>=2,<2.0.4|>=2.1,<2.1.2", + "silverstripe/silverstripe-omnipay": "<2.5.2|>=3,<3.0.2|>=3.1,<3.1.4|>=3.2,<3.2.1", + "silverstripe/subsites": ">=2,<2.6.1", + "silverstripe/taxonomy": ">=1.3,<1.3.1|>=2,<2.0.1", + "silverstripe/userforms": "<3|>=5,<5.4.2", + "silverstripe/versioned-admin": ">=1,<1.11.1", + "simple-updates/phpwhois": "<=1", + "simplesamlphp/saml2": "<1.10.6|>=2,<2.3.8|>=3,<3.1.4|==5.0.0.0-alpha12", + "simplesamlphp/simplesamlphp": "<1.18.6", + "simplesamlphp/simplesamlphp-module-infocard": "<1.0.1", + "simplesamlphp/simplesamlphp-module-openid": "<1", + "simplesamlphp/simplesamlphp-module-openidprovider": "<0.9", + "simplesamlphp/xml-security": "==1.6.11", + "simplito/elliptic-php": "<1.0.6", + "sitegeist/fluid-components": "<3.5", + "sjbr/sr-freecap": "<2.4.6|>=2.5,<2.5.3", + "slim/psr7": "<1.4.1|>=1.5,<1.5.1|>=1.6,<1.6.1", + "slim/slim": "<2.6", + "slub/slub-events": "<3.0.3", + "smarty/smarty": "<4.5.3|>=5,<5.1.1", + "snipe/snipe-it": "<7.0.10", + "socalnick/scn-social-auth": "<1.15.2", + "socialiteproviders/steam": "<1.1", + "spatie/browsershot": "<3.57.4", + "spatie/image-optimizer": "<1.7.3", + "spipu/html2pdf": "<5.2.8", + "spoon/library": "<1.4.1", + "spoonity/tcpdf": "<6.2.22", + "squizlabs/php_codesniffer": ">=1,<2.8.1|>=3,<3.0.1", + "ssddanbrown/bookstack": "<24.05.1", + "starcitizentools/citizen-skin": ">=2.6.3,<2.31", + "statamic/cms": "<4.46|>=5.3,<5.6.2", + "stormpath/sdk": "<9.9.99", + "studio-42/elfinder": "<=2.1.64", + "studiomitte/friendlycaptcha": "<0.1.4", + "subhh/libconnect": "<7.0.8|>=8,<8.1", + "sukohi/surpass": "<1", + "sulu/form-bundle": ">=2,<2.5.3", + "sulu/sulu": "<1.6.44|>=2,<2.5.21|>=2.6,<2.6.5", + "sumocoders/framework-user-bundle": "<1.4", + "superbig/craft-audit": "<3.0.2", + "swag/paypal": "<5.4.4", + "swiftmailer/swiftmailer": "<6.2.5", + "swiftyedit/swiftyedit": "<1.2", + "sylius/admin-bundle": ">=1,<1.0.17|>=1.1,<1.1.9|>=1.2,<1.2.2", + "sylius/grid": ">=1,<1.1.19|>=1.2,<1.2.18|>=1.3,<1.3.13|>=1.4,<1.4.5|>=1.5,<1.5.1", + "sylius/grid-bundle": "<1.10.1", + "sylius/paypal-plugin": ">=1,<1.2.4|>=1.3,<1.3.1", + "sylius/resource-bundle": ">=1,<1.3.14|>=1.4,<1.4.7|>=1.5,<1.5.2|>=1.6,<1.6.4", + "sylius/sylius": "<1.12.19|>=1.13.0.0-alpha1,<1.13.4", + "symbiote/silverstripe-multivaluefield": ">=3,<3.1", + "symbiote/silverstripe-queuedjobs": ">=3,<3.0.2|>=3.1,<3.1.4|>=4,<4.0.7|>=4.1,<4.1.2|>=4.2,<4.2.4|>=4.3,<4.3.3|>=4.4,<4.4.3|>=4.5,<4.5.1|>=4.6,<4.6.4", + "symbiote/silverstripe-seed": "<6.0.3", + "symbiote/silverstripe-versionedfiles": "<=2.0.3", + "symfont/process": ">=0", + "symfony/cache": ">=3.1,<3.4.35|>=4,<4.2.12|>=4.3,<4.3.8", + "symfony/dependency-injection": ">=2,<2.0.17|>=2.7,<2.7.51|>=2.8,<2.8.50|>=3,<3.4.26|>=4,<4.1.12|>=4.2,<4.2.7", + "symfony/error-handler": ">=4.4,<4.4.4|>=5,<5.0.4", + "symfony/form": ">=2.3,<2.3.35|>=2.4,<2.6.12|>=2.7,<2.7.50|>=2.8,<2.8.49|>=3,<3.4.20|>=4,<4.0.15|>=4.1,<4.1.9|>=4.2,<4.2.1", + "symfony/framework-bundle": ">=2,<2.3.18|>=2.4,<2.4.8|>=2.5,<2.5.2|>=2.7,<2.7.51|>=2.8,<2.8.50|>=3,<3.4.26|>=4,<4.1.12|>=4.2,<4.2.7|>=5.3.14,<5.3.15|>=5.4.3,<5.4.4|>=6.0.3,<6.0.4", + "symfony/http-client": ">=4.3,<5.4.46|>=6,<6.4.14|>=7,<7.1.7", + "symfony/http-foundation": "<5.4.46|>=6,<6.4.14|>=7,<7.1.7", + "symfony/http-kernel": ">=2,<4.4.50|>=5,<5.4.20|>=6,<6.0.20|>=6.1,<6.1.12|>=6.2,<6.2.6", + "symfony/intl": ">=2.7,<2.7.38|>=2.8,<2.8.31|>=3,<3.2.14|>=3.3,<3.3.13", + "symfony/maker-bundle": ">=1.27,<1.29.2|>=1.30,<1.31.1", + "symfony/mime": ">=4.3,<4.3.8", + "symfony/phpunit-bridge": ">=2.8,<2.8.50|>=3,<3.4.26|>=4,<4.1.12|>=4.2,<4.2.7", + "symfony/polyfill": ">=1,<1.10", + "symfony/polyfill-php55": ">=1,<1.10", + "symfony/process": "<5.4.46|>=6,<6.4.14|>=7,<7.1.7", + "symfony/proxy-manager-bridge": ">=2.7,<2.7.51|>=2.8,<2.8.50|>=3,<3.4.26|>=4,<4.1.12|>=4.2,<4.2.7", + "symfony/routing": ">=2,<2.0.19", + "symfony/runtime": ">=5.3,<5.4.46|>=6,<6.4.14|>=7,<7.1.7", + "symfony/security": ">=2,<2.7.51|>=2.8,<3.4.49|>=4,<4.4.24|>=5,<5.2.8", + "symfony/security-bundle": ">=2,<4.4.50|>=5,<5.4.20|>=6,<6.0.20|>=6.1,<6.1.12|>=6.2,<6.4.10|>=7,<7.0.10|>=7.1,<7.1.3", + "symfony/security-core": ">=2.4,<2.6.13|>=2.7,<2.7.9|>=2.7.30,<2.7.32|>=2.8,<3.4.49|>=4,<4.4.24|>=5,<5.2.9", + "symfony/security-csrf": ">=2.4,<2.7.48|>=2.8,<2.8.41|>=3,<3.3.17|>=3.4,<3.4.11|>=4,<4.0.11", + "symfony/security-guard": ">=2.8,<3.4.48|>=4,<4.4.23|>=5,<5.2.8", + "symfony/security-http": ">=2.3,<2.3.41|>=2.4,<2.7.51|>=2.8,<2.8.50|>=3,<3.4.26|>=4,<4.2.12|>=4.3,<4.3.8|>=4.4,<4.4.7|>=5,<5.0.7|>=5.1,<5.2.8|>=5.3,<5.3.2|>=5.4,<5.4.31|>=6,<6.3.8", + "symfony/serializer": ">=2,<2.0.11|>=4.1,<4.4.35|>=5,<5.3.12", + "symfony/symfony": "<5.4.46|>=6,<6.4.14|>=7,<7.1.7", + "symfony/translation": ">=2,<2.0.17", + "symfony/twig-bridge": ">=2,<4.4.51|>=5,<5.4.31|>=6,<6.3.8", + "symfony/ux-autocomplete": "<2.11.2", + "symfony/validator": "<5.4.43|>=6,<6.4.11|>=7,<7.1.4", + "symfony/var-exporter": ">=4.2,<4.2.12|>=4.3,<4.3.8", + "symfony/web-profiler-bundle": ">=2,<2.3.19|>=2.4,<2.4.9|>=2.5,<2.5.4", + "symfony/webhook": ">=6.3,<6.3.8", + "symfony/yaml": ">=2,<2.0.22|>=2.1,<2.1.7|>=2.2.0.0-beta1,<2.2.0.0-beta2", + "symphonycms/symphony-2": "<2.6.4", + "t3/dce": "<0.11.5|>=2.2,<2.6.2", + "t3g/svg-sanitizer": "<1.0.3", + "t3s/content-consent": "<1.0.3|>=2,<2.0.2", + "tastyigniter/tastyigniter": "<3.3", + "tcg/voyager": "<=1.4", + "tecnickcom/tcpdf": "<=6.7.4", + "terminal42/contao-tablelookupwizard": "<3.3.5", + "thelia/backoffice-default-template": ">=2.1,<2.1.2", + "thelia/thelia": ">=2.1,<2.1.3", + "theonedemon/phpwhois": "<=4.2.5", + "thinkcmf/thinkcmf": "<6.0.8", + "thorsten/phpmyfaq": "<3.2.2", + "tikiwiki/tiki-manager": "<=17.1", + "timber/timber": ">=0.16.6,<1.23.1|>=1.24,<1.24.1|>=2,<2.1", + "tinymce/tinymce": "<7.2", + "tinymighty/wiki-seo": "<1.2.2", + "titon/framework": "<9.9.99", + "tobiasbg/tablepress": "<=2.0.0.0-RC1", + "topthink/framework": "<6.0.17|>=6.1,<=8.0.4", + "topthink/think": "<=6.1.1", + "topthink/thinkphp": "<=3.2.3|>=6.1.3,<=8.0.4", + "torrentpier/torrentpier": "<=2.4.3", + "tpwd/ke_search": "<4.0.3|>=4.1,<4.6.6|>=5,<5.0.2", + "tribalsystems/zenario": "<=9.7.61188", + "truckersmp/phpwhois": "<=4.3.1", + "ttskch/pagination-service-provider": "<1", + "twbs/bootstrap": "<=3.4.1|>=4,<=4.6.2", + "twig/twig": "<3.11.2|>=3.12,<3.14.1", + "typo3/cms": "<9.5.29|>=10,<10.4.35|>=11,<11.5.23|>=12,<12.2", + "typo3/cms-backend": "<4.1.14|>=4.2,<4.2.15|>=4.3,<4.3.7|>=4.4,<4.4.4|>=7,<=7.6.50|>=8,<=8.7.39|>=9,<=9.5.24|>=10,<10.4.46|>=11,<11.5.40|>=12,<12.4.21|>=13,<13.3.1", + "typo3/cms-core": "<=8.7.56|>=9,<=9.5.47|>=10,<=10.4.44|>=11,<=11.5.36|>=12,<=12.4.14|>=13,<=13.1", + "typo3/cms-extbase": "<6.2.24|>=7,<7.6.8|==8.1.1", + "typo3/cms-fluid": "<4.3.4|>=4.4,<4.4.1", + "typo3/cms-form": ">=8,<=8.7.39|>=9,<=9.5.24|>=10,<=10.4.13|>=11,<=11.1", + "typo3/cms-frontend": "<4.3.9|>=4.4,<4.4.5", + "typo3/cms-install": "<4.1.14|>=4.2,<4.2.16|>=4.3,<4.3.9|>=4.4,<4.4.5|>=12.2,<12.4.8", + "typo3/cms-rte-ckeditor": ">=9.5,<9.5.42|>=10,<10.4.39|>=11,<11.5.30", + "typo3/flow": ">=1,<1.0.4|>=1.1,<1.1.1|>=2,<2.0.1|>=2.3,<2.3.16|>=3,<3.0.12|>=3.1,<3.1.10|>=3.2,<3.2.13|>=3.3,<3.3.13|>=4,<4.0.6", + "typo3/html-sanitizer": ">=1,<=1.5.2|>=2,<=2.1.3", + "typo3/neos": ">=1.1,<1.1.3|>=1.2,<1.2.13|>=2,<2.0.4|>=2.3,<2.3.99|>=3,<3.0.20|>=3.1,<3.1.18|>=3.2,<3.2.14|>=3.3,<3.3.23|>=4,<4.0.17|>=4.1,<4.1.16|>=4.2,<4.2.12|>=4.3,<4.3.3", + "typo3/phar-stream-wrapper": ">=1,<2.1.1|>=3,<3.1.1", + "typo3/swiftmailer": ">=4.1,<4.1.99|>=5.4,<5.4.5", + "typo3fluid/fluid": ">=2,<2.0.8|>=2.1,<2.1.7|>=2.2,<2.2.4|>=2.3,<2.3.7|>=2.4,<2.4.4|>=2.5,<2.5.11|>=2.6,<2.6.10", + "ua-parser/uap-php": "<3.8", + "uasoft-indonesia/badaso": "<=2.9.7", + "unisharp/laravel-filemanager": "<2.6.4", + "unopim/unopim": "<0.1.4", + "userfrosting/userfrosting": ">=0.3.1,<4.6.3", + "usmanhalalit/pixie": "<1.0.3|>=2,<2.0.2", + "uvdesk/community-skeleton": "<=1.1.1", + "uvdesk/core-framework": "<=1.1.1", + "vanilla/safecurl": "<0.9.2", + "verbb/comments": "<1.5.5", + "verbb/formie": "<2.1.6", + "verbb/image-resizer": "<2.0.9", + "verbb/knock-knock": "<1.2.8", + "verot/class.upload.php": "<=2.1.6", + "villagedefrance/opencart-overclocked": "<=1.11.1", + "vova07/yii2-fileapi-widget": "<0.1.9", + "vrana/adminer": "<4.8.1", + "vufind/vufind": ">=2,<9.1.1", + "waldhacker/hcaptcha": "<2.1.2", + "wallabag/tcpdf": "<6.2.22", + "wallabag/wallabag": "<2.6.7", + "wanglelecc/laracms": "<=1.0.3", + "web-auth/webauthn-framework": ">=3.3,<3.3.4|>=4.5,<4.9", + "web-auth/webauthn-lib": ">=4.5,<4.9", + "web-feet/coastercms": "==5.5", + "webbuilders-group/silverstripe-kapost-bridge": "<0.4", + "webcoast/deferred-image-processing": "<1.0.2", + "webklex/laravel-imap": "<5.3", + "webklex/php-imap": "<5.3", + "webpa/webpa": "<3.1.2", + "wikibase/wikibase": "<=1.39.3", + "wikimedia/parsoid": "<0.12.2", + "willdurand/js-translation-bundle": "<2.1.1", + "winter/wn-backend-module": "<1.2.4", + "winter/wn-dusk-plugin": "<2.1", + "winter/wn-system-module": "<1.2.4", + "wintercms/winter": "<=1.2.3", + "wireui/wireui": "<1.19.3|>=2,<2.1.3", + "woocommerce/woocommerce": "<6.6|>=8.8,<8.8.5|>=8.9,<8.9.3", + "wp-cli/wp-cli": ">=0.12,<2.5", + "wp-graphql/wp-graphql": "<=1.14.5", + "wp-premium/gravityforms": "<2.4.21", + "wpanel/wpanel4-cms": "<=4.3.1", + "wpcloud/wp-stateless": "<3.2", + "wpglobus/wpglobus": "<=1.9.6", + "wwbn/avideo": "<14.3", + "xataface/xataface": "<3", + "xpressengine/xpressengine": "<3.0.15", + "yab/quarx": "<2.4.5", + "yeswiki/yeswiki": "<=4.4.4", + "yetiforce/yetiforce-crm": "<=6.4", + "yidashi/yii2cmf": "<=2", + "yii2mod/yii2-cms": "<1.9.2", + "yiisoft/yii": "<1.1.29", + "yiisoft/yii2": "<2.0.49.4-dev", + "yiisoft/yii2-authclient": "<2.2.15", + "yiisoft/yii2-bootstrap": "<2.0.4", + "yiisoft/yii2-dev": "<2.0.43", + "yiisoft/yii2-elasticsearch": "<2.0.5", + "yiisoft/yii2-gii": "<=2.2.4", + "yiisoft/yii2-jui": "<2.0.4", + "yiisoft/yii2-redis": "<2.0.8", + "yikesinc/yikes-inc-easy-mailchimp-extender": "<6.8.6", + "yoast-seo-for-typo3/yoast_seo": "<7.2.3", + "yourls/yourls": "<=1.8.2", + "yuan1994/tpadmin": "<=1.3.12", + "zencart/zencart": "<=1.5.7.0-beta", + "zendesk/zendesk_api_client_php": "<2.2.11", + "zendframework/zend-cache": ">=2.4,<2.4.8|>=2.5,<2.5.3", + "zendframework/zend-captcha": ">=2,<2.4.9|>=2.5,<2.5.2", + "zendframework/zend-crypt": ">=2,<2.4.9|>=2.5,<2.5.2", + "zendframework/zend-db": "<2.2.10|>=2.3,<2.3.5", + "zendframework/zend-developer-tools": ">=1.2.2,<1.2.3", + "zendframework/zend-diactoros": "<1.8.4", + "zendframework/zend-feed": "<2.10.3", + "zendframework/zend-form": ">=2,<2.2.7|>=2.3,<2.3.1", + "zendframework/zend-http": "<2.8.1", + "zendframework/zend-json": ">=2.1,<2.1.6|>=2.2,<2.2.6", + "zendframework/zend-ldap": ">=2,<2.0.99|>=2.1,<2.1.99|>=2.2,<2.2.8|>=2.3,<2.3.3", + "zendframework/zend-mail": "<2.4.11|>=2.5,<2.7.2", + "zendframework/zend-navigation": ">=2,<2.2.7|>=2.3,<2.3.1", + "zendframework/zend-session": ">=2,<2.2.9|>=2.3,<2.3.4", + "zendframework/zend-validator": ">=2.3,<2.3.6", + "zendframework/zend-view": ">=2,<2.2.7|>=2.3,<2.3.1", + "zendframework/zend-xmlrpc": ">=2.1,<2.1.6|>=2.2,<2.2.6", + "zendframework/zendframework": "<=3", + "zendframework/zendframework1": "<1.12.20", + "zendframework/zendopenid": "<2.0.2", + "zendframework/zendrest": "<2.0.2", + "zendframework/zendservice-amazon": "<2.0.3", + "zendframework/zendservice-api": "<1", + "zendframework/zendservice-audioscrobbler": "<2.0.2", + "zendframework/zendservice-nirvanix": "<2.0.2", + "zendframework/zendservice-slideshare": "<2.0.2", + "zendframework/zendservice-technorati": "<2.0.2", + "zendframework/zendservice-windowsazure": "<2.0.2", + "zendframework/zendxml": ">=1,<1.0.1", + "zenstruck/collection": "<0.2.1", + "zetacomponents/mail": "<1.8.2", + "zf-commons/zfc-user": "<1.2.2", + "zfcampus/zf-apigility-doctrine": ">=1,<1.0.3", + "zfr/zfr-oauth2-server-module": "<0.1.2", + "zoujingli/thinkadmin": "<=6.1.53" + }, + "default-branch": true, + "type": "metapackage", + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Marco Pivetta", + "email": "ocramius@gmail.com", + "role": "maintainer" + }, + { + "name": "Ilya Tribusean", + "email": "slash3b@gmail.com", + "role": "maintainer" + } + ], + "description": "Prevents installation of composer packages with known security vulnerabilities: no API, simply require it", + "keywords": [ + "dev" + ], + "support": { + "issues": "https://github.com/Roave/SecurityAdvisories/issues", + "source": "https://github.com/Roave/SecurityAdvisories/tree/latest" + }, + "funding": [ + { + "url": "https://github.com/Ocramius", + "type": "github" + }, + { + "url": "https://tidelift.com/funding/github/packagist/roave/security-advisories", + "type": "tidelift" + } + ], + "time": "2024-11-07T19:04:57+00:00" + }, + { + "name": "sebastian/cli-parser", + "version": "2.0.1", + "source": { + "type": "git", + "url": "https://github.com/sebastianbergmann/cli-parser.git", + "reference": "c34583b87e7b7a8055bf6c450c2c77ce32a24084" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/sebastianbergmann/cli-parser/zipball/c34583b87e7b7a8055bf6c450c2c77ce32a24084", + "reference": "c34583b87e7b7a8055bf6c450c2c77ce32a24084", + "shasum": "" + }, + "require": { + "php": ">=8.1" + }, + "require-dev": { + "phpunit/phpunit": "^10.0" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-main": "2.0-dev" + } + }, + "autoload": { + "classmap": [ + "src/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "BSD-3-Clause" + ], + "authors": [ + { + "name": "Sebastian Bergmann", + "email": "sebastian@phpunit.de", + "role": "lead" + } + ], + "description": "Library for parsing CLI options", + "homepage": "https://github.com/sebastianbergmann/cli-parser", + "support": { + "issues": "https://github.com/sebastianbergmann/cli-parser/issues", + "security": "https://github.com/sebastianbergmann/cli-parser/security/policy", + "source": "https://github.com/sebastianbergmann/cli-parser/tree/2.0.1" + }, + "funding": [ + { + "url": "https://github.com/sebastianbergmann", + "type": "github" + } + ], + "time": "2024-03-02T07:12:49+00:00" + }, + { + "name": "sebastian/code-unit", + "version": "2.0.0", + "source": { + "type": "git", + "url": "https://github.com/sebastianbergmann/code-unit.git", + "reference": "a81fee9eef0b7a76af11d121767abc44c104e503" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/sebastianbergmann/code-unit/zipball/a81fee9eef0b7a76af11d121767abc44c104e503", + "reference": "a81fee9eef0b7a76af11d121767abc44c104e503", + "shasum": "" + }, + "require": { + "php": ">=8.1" + }, + "require-dev": { + "phpunit/phpunit": "^10.0" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-main": "2.0-dev" + } + }, + "autoload": { + "classmap": [ + "src/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "BSD-3-Clause" + ], + "authors": [ + { + "name": "Sebastian Bergmann", + "email": "sebastian@phpunit.de", + "role": "lead" + } + ], + "description": "Collection of value objects that represent the PHP code units", + "homepage": "https://github.com/sebastianbergmann/code-unit", + "support": { + "issues": "https://github.com/sebastianbergmann/code-unit/issues", + "source": "https://github.com/sebastianbergmann/code-unit/tree/2.0.0" + }, + "funding": [ + { + "url": "https://github.com/sebastianbergmann", + "type": "github" + } + ], + "time": "2023-02-03T06:58:43+00:00" + }, + { + "name": "sebastian/code-unit-reverse-lookup", + "version": "3.0.0", + "source": { + "type": "git", + "url": "https://github.com/sebastianbergmann/code-unit-reverse-lookup.git", + "reference": "5e3a687f7d8ae33fb362c5c0743794bbb2420a1d" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/sebastianbergmann/code-unit-reverse-lookup/zipball/5e3a687f7d8ae33fb362c5c0743794bbb2420a1d", + "reference": "5e3a687f7d8ae33fb362c5c0743794bbb2420a1d", + "shasum": "" + }, + "require": { + "php": ">=8.1" + }, + "require-dev": { + "phpunit/phpunit": "^10.0" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-main": "3.0-dev" + } + }, + "autoload": { + "classmap": [ + "src/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "BSD-3-Clause" + ], + "authors": [ + { + "name": "Sebastian Bergmann", + "email": "sebastian@phpunit.de" + } + ], + "description": "Looks up which function or method a line of code belongs to", + "homepage": "https://github.com/sebastianbergmann/code-unit-reverse-lookup/", + "support": { + "issues": "https://github.com/sebastianbergmann/code-unit-reverse-lookup/issues", + "source": "https://github.com/sebastianbergmann/code-unit-reverse-lookup/tree/3.0.0" + }, + "funding": [ + { + "url": "https://github.com/sebastianbergmann", + "type": "github" + } + ], + "time": "2023-02-03T06:59:15+00:00" + }, + { + "name": "sebastian/comparator", + "version": "5.0.3", + "source": { + "type": "git", + "url": "https://github.com/sebastianbergmann/comparator.git", + "reference": "a18251eb0b7a2dcd2f7aa3d6078b18545ef0558e" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/sebastianbergmann/comparator/zipball/a18251eb0b7a2dcd2f7aa3d6078b18545ef0558e", + "reference": "a18251eb0b7a2dcd2f7aa3d6078b18545ef0558e", + "shasum": "" + }, + "require": { + "ext-dom": "*", + "ext-mbstring": "*", + "php": ">=8.1", + "sebastian/diff": "^5.0", + "sebastian/exporter": "^5.0" + }, + "require-dev": { + "phpunit/phpunit": "^10.5" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-main": "5.0-dev" + } + }, + "autoload": { + "classmap": [ + "src/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "BSD-3-Clause" + ], + "authors": [ + { + "name": "Sebastian Bergmann", + "email": "sebastian@phpunit.de" + }, + { + "name": "Jeff Welch", + "email": "whatthejeff@gmail.com" + }, + { + "name": "Volker Dusch", + "email": "github@wallbash.com" + }, + { + "name": "Bernhard Schussek", + "email": "bschussek@2bepublished.at" + } + ], + "description": "Provides the functionality to compare PHP values for equality", + "homepage": "https://github.com/sebastianbergmann/comparator", + "keywords": [ + "comparator", + "compare", + "equality" + ], + "support": { + "issues": "https://github.com/sebastianbergmann/comparator/issues", + "security": "https://github.com/sebastianbergmann/comparator/security/policy", + "source": "https://github.com/sebastianbergmann/comparator/tree/5.0.3" + }, + "funding": [ + { + "url": "https://github.com/sebastianbergmann", + "type": "github" + } + ], + "time": "2024-10-18T14:56:07+00:00" + }, + { + "name": "sebastian/complexity", + "version": "3.2.0", + "source": { + "type": "git", + "url": "https://github.com/sebastianbergmann/complexity.git", + "reference": "68ff824baeae169ec9f2137158ee529584553799" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/sebastianbergmann/complexity/zipball/68ff824baeae169ec9f2137158ee529584553799", + "reference": "68ff824baeae169ec9f2137158ee529584553799", + "shasum": "" + }, + "require": { + "nikic/php-parser": "^4.18 || ^5.0", + "php": ">=8.1" + }, + "require-dev": { + "phpunit/phpunit": "^10.0" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-main": "3.2-dev" + } + }, + "autoload": { + "classmap": [ + "src/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "BSD-3-Clause" + ], + "authors": [ + { + "name": "Sebastian Bergmann", + "email": "sebastian@phpunit.de", + "role": "lead" + } + ], + "description": "Library for calculating the complexity of PHP code units", + "homepage": "https://github.com/sebastianbergmann/complexity", + "support": { + "issues": "https://github.com/sebastianbergmann/complexity/issues", + "security": "https://github.com/sebastianbergmann/complexity/security/policy", + "source": "https://github.com/sebastianbergmann/complexity/tree/3.2.0" + }, + "funding": [ + { + "url": "https://github.com/sebastianbergmann", + "type": "github" + } + ], + "time": "2023-12-21T08:37:17+00:00" + }, + { + "name": "sebastian/diff", + "version": "5.1.1", + "source": { + "type": "git", + "url": "https://github.com/sebastianbergmann/diff.git", + "reference": "c41e007b4b62af48218231d6c2275e4c9b975b2e" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/sebastianbergmann/diff/zipball/c41e007b4b62af48218231d6c2275e4c9b975b2e", + "reference": "c41e007b4b62af48218231d6c2275e4c9b975b2e", + "shasum": "" + }, + "require": { + "php": ">=8.1" + }, + "require-dev": { + "phpunit/phpunit": "^10.0", + "symfony/process": "^6.4" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-main": "5.1-dev" + } + }, + "autoload": { + "classmap": [ + "src/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "BSD-3-Clause" + ], + "authors": [ + { + "name": "Sebastian Bergmann", + "email": "sebastian@phpunit.de" + }, + { + "name": "Kore Nordmann", + "email": "mail@kore-nordmann.de" + } + ], + "description": "Diff implementation", + "homepage": "https://github.com/sebastianbergmann/diff", + "keywords": [ + "diff", + "udiff", + "unidiff", + "unified diff" + ], + "support": { + "issues": "https://github.com/sebastianbergmann/diff/issues", + "security": "https://github.com/sebastianbergmann/diff/security/policy", + "source": "https://github.com/sebastianbergmann/diff/tree/5.1.1" + }, + "funding": [ + { + "url": "https://github.com/sebastianbergmann", + "type": "github" + } + ], + "time": "2024-03-02T07:15:17+00:00" + }, + { + "name": "sebastian/environment", + "version": "6.1.0", + "source": { + "type": "git", + "url": "https://github.com/sebastianbergmann/environment.git", + "reference": "8074dbcd93529b357029f5cc5058fd3e43666984" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/sebastianbergmann/environment/zipball/8074dbcd93529b357029f5cc5058fd3e43666984", + "reference": "8074dbcd93529b357029f5cc5058fd3e43666984", + "shasum": "" + }, + "require": { + "php": ">=8.1" + }, + "require-dev": { + "phpunit/phpunit": "^10.0" + }, + "suggest": { + "ext-posix": "*" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-main": "6.1-dev" + } + }, + "autoload": { + "classmap": [ + "src/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "BSD-3-Clause" + ], + "authors": [ + { + "name": "Sebastian Bergmann", + "email": "sebastian@phpunit.de" + } + ], + "description": "Provides functionality to handle HHVM/PHP environments", + "homepage": "https://github.com/sebastianbergmann/environment", + "keywords": [ + "Xdebug", + "environment", + "hhvm" + ], + "support": { + "issues": "https://github.com/sebastianbergmann/environment/issues", + "security": "https://github.com/sebastianbergmann/environment/security/policy", + "source": "https://github.com/sebastianbergmann/environment/tree/6.1.0" + }, + "funding": [ + { + "url": "https://github.com/sebastianbergmann", + "type": "github" + } + ], + "time": "2024-03-23T08:47:14+00:00" + }, + { + "name": "sebastian/exporter", + "version": "5.1.2", + "source": { + "type": "git", + "url": "https://github.com/sebastianbergmann/exporter.git", + "reference": "955288482d97c19a372d3f31006ab3f37da47adf" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/sebastianbergmann/exporter/zipball/955288482d97c19a372d3f31006ab3f37da47adf", + "reference": "955288482d97c19a372d3f31006ab3f37da47adf", + "shasum": "" + }, + "require": { + "ext-mbstring": "*", + "php": ">=8.1", + "sebastian/recursion-context": "^5.0" + }, + "require-dev": { + "phpunit/phpunit": "^10.0" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-main": "5.1-dev" + } + }, + "autoload": { + "classmap": [ + "src/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "BSD-3-Clause" + ], + "authors": [ + { + "name": "Sebastian Bergmann", + "email": "sebastian@phpunit.de" + }, + { + "name": "Jeff Welch", + "email": "whatthejeff@gmail.com" + }, + { + "name": "Volker Dusch", + "email": "github@wallbash.com" + }, + { + "name": "Adam Harvey", + "email": "aharvey@php.net" + }, + { + "name": "Bernhard Schussek", + "email": "bschussek@gmail.com" + } + ], + "description": "Provides the functionality to export PHP variables for visualization", + "homepage": "https://www.github.com/sebastianbergmann/exporter", + "keywords": [ + "export", + "exporter" + ], + "support": { + "issues": "https://github.com/sebastianbergmann/exporter/issues", + "security": "https://github.com/sebastianbergmann/exporter/security/policy", + "source": "https://github.com/sebastianbergmann/exporter/tree/5.1.2" + }, + "funding": [ + { + "url": "https://github.com/sebastianbergmann", + "type": "github" + } + ], + "time": "2024-03-02T07:17:12+00:00" + }, + { + "name": "sebastian/global-state", + "version": "6.0.2", + "source": { + "type": "git", + "url": "https://github.com/sebastianbergmann/global-state.git", + "reference": "987bafff24ecc4c9ac418cab1145b96dd6e9cbd9" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/sebastianbergmann/global-state/zipball/987bafff24ecc4c9ac418cab1145b96dd6e9cbd9", + "reference": "987bafff24ecc4c9ac418cab1145b96dd6e9cbd9", + "shasum": "" + }, + "require": { + "php": ">=8.1", + "sebastian/object-reflector": "^3.0", + "sebastian/recursion-context": "^5.0" + }, + "require-dev": { + "ext-dom": "*", + "phpunit/phpunit": "^10.0" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-main": "6.0-dev" + } + }, + "autoload": { + "classmap": [ + "src/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "BSD-3-Clause" + ], + "authors": [ + { + "name": "Sebastian Bergmann", + "email": "sebastian@phpunit.de" + } + ], + "description": "Snapshotting of global state", + "homepage": "https://www.github.com/sebastianbergmann/global-state", + "keywords": [ + "global state" + ], + "support": { + "issues": "https://github.com/sebastianbergmann/global-state/issues", + "security": "https://github.com/sebastianbergmann/global-state/security/policy", + "source": "https://github.com/sebastianbergmann/global-state/tree/6.0.2" + }, + "funding": [ + { + "url": "https://github.com/sebastianbergmann", + "type": "github" + } + ], + "time": "2024-03-02T07:19:19+00:00" + }, + { + "name": "sebastian/lines-of-code", + "version": "2.0.2", + "source": { + "type": "git", + "url": "https://github.com/sebastianbergmann/lines-of-code.git", + "reference": "856e7f6a75a84e339195d48c556f23be2ebf75d0" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/sebastianbergmann/lines-of-code/zipball/856e7f6a75a84e339195d48c556f23be2ebf75d0", + "reference": "856e7f6a75a84e339195d48c556f23be2ebf75d0", + "shasum": "" + }, + "require": { + "nikic/php-parser": "^4.18 || ^5.0", + "php": ">=8.1" + }, + "require-dev": { + "phpunit/phpunit": "^10.0" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-main": "2.0-dev" + } + }, + "autoload": { + "classmap": [ + "src/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "BSD-3-Clause" + ], + "authors": [ + { + "name": "Sebastian Bergmann", + "email": "sebastian@phpunit.de", + "role": "lead" + } + ], + "description": "Library for counting the lines of code in PHP source code", + "homepage": "https://github.com/sebastianbergmann/lines-of-code", + "support": { + "issues": "https://github.com/sebastianbergmann/lines-of-code/issues", + "security": "https://github.com/sebastianbergmann/lines-of-code/security/policy", + "source": "https://github.com/sebastianbergmann/lines-of-code/tree/2.0.2" + }, + "funding": [ + { + "url": "https://github.com/sebastianbergmann", + "type": "github" + } + ], + "time": "2023-12-21T08:38:20+00:00" + }, + { + "name": "sebastian/object-enumerator", + "version": "5.0.0", + "source": { + "type": "git", + "url": "https://github.com/sebastianbergmann/object-enumerator.git", + "reference": "202d0e344a580d7f7d04b3fafce6933e59dae906" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/sebastianbergmann/object-enumerator/zipball/202d0e344a580d7f7d04b3fafce6933e59dae906", + "reference": "202d0e344a580d7f7d04b3fafce6933e59dae906", + "shasum": "" + }, + "require": { + "php": ">=8.1", + "sebastian/object-reflector": "^3.0", + "sebastian/recursion-context": "^5.0" + }, + "require-dev": { + "phpunit/phpunit": "^10.0" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-main": "5.0-dev" + } + }, + "autoload": { + "classmap": [ + "src/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "BSD-3-Clause" + ], + "authors": [ + { + "name": "Sebastian Bergmann", + "email": "sebastian@phpunit.de" + } + ], + "description": "Traverses array structures and object graphs to enumerate all referenced objects", + "homepage": "https://github.com/sebastianbergmann/object-enumerator/", + "support": { + "issues": "https://github.com/sebastianbergmann/object-enumerator/issues", + "source": "https://github.com/sebastianbergmann/object-enumerator/tree/5.0.0" + }, + "funding": [ + { + "url": "https://github.com/sebastianbergmann", + "type": "github" + } + ], + "time": "2023-02-03T07:08:32+00:00" + }, + { + "name": "sebastian/object-reflector", + "version": "3.0.0", + "source": { + "type": "git", + "url": "https://github.com/sebastianbergmann/object-reflector.git", + "reference": "24ed13d98130f0e7122df55d06c5c4942a577957" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/sebastianbergmann/object-reflector/zipball/24ed13d98130f0e7122df55d06c5c4942a577957", + "reference": "24ed13d98130f0e7122df55d06c5c4942a577957", + "shasum": "" + }, + "require": { + "php": ">=8.1" + }, + "require-dev": { + "phpunit/phpunit": "^10.0" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-main": "3.0-dev" + } + }, + "autoload": { + "classmap": [ + "src/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "BSD-3-Clause" + ], + "authors": [ + { + "name": "Sebastian Bergmann", + "email": "sebastian@phpunit.de" + } + ], + "description": "Allows reflection of object attributes, including inherited and non-public ones", + "homepage": "https://github.com/sebastianbergmann/object-reflector/", + "support": { + "issues": "https://github.com/sebastianbergmann/object-reflector/issues", + "source": "https://github.com/sebastianbergmann/object-reflector/tree/3.0.0" + }, + "funding": [ + { + "url": "https://github.com/sebastianbergmann", + "type": "github" + } + ], + "time": "2023-02-03T07:06:18+00:00" + }, + { + "name": "sebastian/recursion-context", + "version": "5.0.0", + "source": { + "type": "git", + "url": "https://github.com/sebastianbergmann/recursion-context.git", + "reference": "05909fb5bc7df4c52992396d0116aed689f93712" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/sebastianbergmann/recursion-context/zipball/05909fb5bc7df4c52992396d0116aed689f93712", + "reference": "05909fb5bc7df4c52992396d0116aed689f93712", + "shasum": "" + }, + "require": { + "php": ">=8.1" + }, + "require-dev": { + "phpunit/phpunit": "^10.0" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-main": "5.0-dev" + } + }, + "autoload": { + "classmap": [ + "src/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "BSD-3-Clause" + ], + "authors": [ + { + "name": "Sebastian Bergmann", + "email": "sebastian@phpunit.de" + }, + { + "name": "Jeff Welch", + "email": "whatthejeff@gmail.com" + }, + { + "name": "Adam Harvey", + "email": "aharvey@php.net" + } + ], + "description": "Provides functionality to recursively process PHP variables", + "homepage": "https://github.com/sebastianbergmann/recursion-context", + "support": { + "issues": "https://github.com/sebastianbergmann/recursion-context/issues", + "source": "https://github.com/sebastianbergmann/recursion-context/tree/5.0.0" + }, + "funding": [ + { + "url": "https://github.com/sebastianbergmann", + "type": "github" + } + ], + "time": "2023-02-03T07:05:40+00:00" + }, + { + "name": "sebastian/type", + "version": "4.0.0", + "source": { + "type": "git", + "url": "https://github.com/sebastianbergmann/type.git", + "reference": "462699a16464c3944eefc02ebdd77882bd3925bf" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/sebastianbergmann/type/zipball/462699a16464c3944eefc02ebdd77882bd3925bf", + "reference": "462699a16464c3944eefc02ebdd77882bd3925bf", + "shasum": "" + }, + "require": { + "php": ">=8.1" + }, + "require-dev": { + "phpunit/phpunit": "^10.0" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-main": "4.0-dev" + } + }, + "autoload": { + "classmap": [ + "src/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "BSD-3-Clause" + ], + "authors": [ + { + "name": "Sebastian Bergmann", + "email": "sebastian@phpunit.de", + "role": "lead" + } + ], + "description": "Collection of value objects that represent the types of the PHP type system", + "homepage": "https://github.com/sebastianbergmann/type", + "support": { + "issues": "https://github.com/sebastianbergmann/type/issues", + "source": "https://github.com/sebastianbergmann/type/tree/4.0.0" + }, + "funding": [ + { + "url": "https://github.com/sebastianbergmann", + "type": "github" + } + ], + "time": "2023-02-03T07:10:45+00:00" + }, + { + "name": "sebastian/version", + "version": "4.0.1", + "source": { + "type": "git", + "url": "https://github.com/sebastianbergmann/version.git", + "reference": "c51fa83a5d8f43f1402e3f32a005e6262244ef17" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/sebastianbergmann/version/zipball/c51fa83a5d8f43f1402e3f32a005e6262244ef17", + "reference": "c51fa83a5d8f43f1402e3f32a005e6262244ef17", + "shasum": "" + }, + "require": { + "php": ">=8.1" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-main": "4.0-dev" + } + }, + "autoload": { + "classmap": [ + "src/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "BSD-3-Clause" + ], + "authors": [ + { + "name": "Sebastian Bergmann", + "email": "sebastian@phpunit.de", + "role": "lead" + } + ], + "description": "Library that helps with managing the version number of Git-hosted PHP projects", + "homepage": "https://github.com/sebastianbergmann/version", + "support": { + "issues": "https://github.com/sebastianbergmann/version/issues", + "source": "https://github.com/sebastianbergmann/version/tree/4.0.1" + }, + "funding": [ + { + "url": "https://github.com/sebastianbergmann", + "type": "github" + } + ], + "time": "2023-02-07T11:34:05+00:00" + }, + { + "name": "symfony/console", + "version": "v6.4.14", + "source": { + "type": "git", + "url": "https://github.com/symfony/console.git", + "reference": "897c2441ed4eec8a8a2c37b943427d24dba3f26b" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/symfony/console/zipball/897c2441ed4eec8a8a2c37b943427d24dba3f26b", + "reference": "897c2441ed4eec8a8a2c37b943427d24dba3f26b", + "shasum": "" + }, + "require": { + "php": ">=8.1", + "symfony/deprecation-contracts": "^2.5|^3", + "symfony/polyfill-mbstring": "~1.0", + "symfony/service-contracts": "^2.5|^3", + "symfony/string": "^5.4|^6.0|^7.0" + }, + "conflict": { + "symfony/dependency-injection": "<5.4", + "symfony/dotenv": "<5.4", + "symfony/event-dispatcher": "<5.4", + "symfony/lock": "<5.4", + "symfony/process": "<5.4" + }, + "provide": { + "psr/log-implementation": "1.0|2.0|3.0" + }, + "require-dev": { + "psr/log": "^1|^2|^3", + "symfony/config": "^5.4|^6.0|^7.0", + "symfony/dependency-injection": "^5.4|^6.0|^7.0", + "symfony/event-dispatcher": "^5.4|^6.0|^7.0", + "symfony/http-foundation": "^6.4|^7.0", + "symfony/http-kernel": "^6.4|^7.0", + "symfony/lock": "^5.4|^6.0|^7.0", + "symfony/messenger": "^5.4|^6.0|^7.0", + "symfony/process": "^5.4|^6.0|^7.0", + "symfony/stopwatch": "^5.4|^6.0|^7.0", + "symfony/var-dumper": "^5.4|^6.0|^7.0" + }, + "type": "library", + "autoload": { + "psr-4": { + "Symfony\\Component\\Console\\": "" + }, + "exclude-from-classmap": [ + "/Tests/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Fabien Potencier", + "email": "fabien@symfony.com" + }, + { + "name": "Symfony Community", + "homepage": "https://symfony.com/contributors" + } + ], + "description": "Eases the creation of beautiful and testable command line interfaces", + "homepage": "https://symfony.com", + "keywords": [ + "cli", + "command-line", + "console", + "terminal" + ], + "support": { + "source": "https://github.com/symfony/console/tree/v6.4.14" + }, + "funding": [ + { + "url": "https://symfony.com/sponsor", + "type": "custom" + }, + { + "url": "https://github.com/fabpot", + "type": "github" + }, + { + "url": "https://tidelift.com/funding/github/packagist/symfony/symfony", + "type": "tidelift" + } + ], + "time": "2024-11-05T15:34:40+00:00" + }, + { + "name": "symfony/event-dispatcher", + "version": "v7.1.6", + "source": { + "type": "git", + "url": "https://github.com/symfony/event-dispatcher.git", + "reference": "87254c78dd50721cfd015b62277a8281c5589702" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/symfony/event-dispatcher/zipball/87254c78dd50721cfd015b62277a8281c5589702", + "reference": "87254c78dd50721cfd015b62277a8281c5589702", + "shasum": "" + }, + "require": { + "php": ">=8.2", + "symfony/event-dispatcher-contracts": "^2.5|^3" + }, + "conflict": { + "symfony/dependency-injection": "<6.4", + "symfony/service-contracts": "<2.5" + }, + "provide": { + "psr/event-dispatcher-implementation": "1.0", + "symfony/event-dispatcher-implementation": "2.0|3.0" + }, + "require-dev": { + "psr/log": "^1|^2|^3", + "symfony/config": "^6.4|^7.0", + "symfony/dependency-injection": "^6.4|^7.0", + "symfony/error-handler": "^6.4|^7.0", + "symfony/expression-language": "^6.4|^7.0", + "symfony/http-foundation": "^6.4|^7.0", + "symfony/service-contracts": "^2.5|^3", + "symfony/stopwatch": "^6.4|^7.0" + }, + "type": "library", + "autoload": { + "psr-4": { + "Symfony\\Component\\EventDispatcher\\": "" + }, + "exclude-from-classmap": [ + "/Tests/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Fabien Potencier", + "email": "fabien@symfony.com" + }, + { + "name": "Symfony Community", + "homepage": "https://symfony.com/contributors" + } + ], + "description": "Provides tools that allow your application components to communicate with each other by dispatching events and listening to them", + "homepage": "https://symfony.com", + "support": { + "source": "https://github.com/symfony/event-dispatcher/tree/v7.1.6" + }, + "funding": [ + { + "url": "https://symfony.com/sponsor", + "type": "custom" + }, + { + "url": "https://github.com/fabpot", + "type": "github" + }, + { + "url": "https://tidelift.com/funding/github/packagist/symfony/symfony", + "type": "tidelift" + } + ], + "time": "2024-09-25T14:20:29+00:00" + }, + { + "name": "symfony/event-dispatcher-contracts", + "version": "v3.5.0", + "source": { + "type": "git", + "url": "https://github.com/symfony/event-dispatcher-contracts.git", + "reference": "8f93aec25d41b72493c6ddff14e916177c9efc50" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/symfony/event-dispatcher-contracts/zipball/8f93aec25d41b72493c6ddff14e916177c9efc50", + "reference": "8f93aec25d41b72493c6ddff14e916177c9efc50", + "shasum": "" + }, + "require": { + "php": ">=8.1", + "psr/event-dispatcher": "^1" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-main": "3.5-dev" + }, + "thanks": { + "name": "symfony/contracts", + "url": "https://github.com/symfony/contracts" + } + }, + "autoload": { + "psr-4": { + "Symfony\\Contracts\\EventDispatcher\\": "" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Nicolas Grekas", + "email": "p@tchwork.com" + }, + { + "name": "Symfony Community", + "homepage": "https://symfony.com/contributors" + } + ], + "description": "Generic abstractions related to dispatching event", + "homepage": "https://symfony.com", + "keywords": [ + "abstractions", + "contracts", + "decoupling", + "interfaces", + "interoperability", + "standards" + ], + "support": { + "source": "https://github.com/symfony/event-dispatcher-contracts/tree/v3.5.0" + }, + "funding": [ + { + "url": "https://symfony.com/sponsor", + "type": "custom" + }, + { + "url": "https://github.com/fabpot", + "type": "github" + }, + { + "url": "https://tidelift.com/funding/github/packagist/symfony/symfony", + "type": "tidelift" + } + ], + "time": "2024-04-18T09:32:20+00:00" + }, + { + "name": "symfony/filesystem", + "version": "v7.1.6", + "source": { + "type": "git", + "url": "https://github.com/symfony/filesystem.git", + "reference": "c835867b3c62bb05c7fe3d637c871c7ae52024d4" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/symfony/filesystem/zipball/c835867b3c62bb05c7fe3d637c871c7ae52024d4", + "reference": "c835867b3c62bb05c7fe3d637c871c7ae52024d4", + "shasum": "" + }, + "require": { + "php": ">=8.2", + "symfony/polyfill-ctype": "~1.8", + "symfony/polyfill-mbstring": "~1.8" + }, + "require-dev": { + "symfony/process": "^6.4|^7.0" + }, + "type": "library", + "autoload": { + "psr-4": { + "Symfony\\Component\\Filesystem\\": "" + }, + "exclude-from-classmap": [ + "/Tests/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Fabien Potencier", + "email": "fabien@symfony.com" + }, + { + "name": "Symfony Community", + "homepage": "https://symfony.com/contributors" + } + ], + "description": "Provides basic utilities for the filesystem", + "homepage": "https://symfony.com", + "support": { + "source": "https://github.com/symfony/filesystem/tree/v7.1.6" + }, + "funding": [ + { + "url": "https://symfony.com/sponsor", + "type": "custom" + }, + { + "url": "https://github.com/fabpot", + "type": "github" + }, + { + "url": "https://tidelift.com/funding/github/packagist/symfony/symfony", + "type": "tidelift" + } + ], + "time": "2024-10-25T15:11:02+00:00" + }, + { + "name": "symfony/finder", + "version": "v7.1.6", + "source": { + "type": "git", + "url": "https://github.com/symfony/finder.git", + "reference": "2cb89664897be33f78c65d3d2845954c8d7a43b8" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/symfony/finder/zipball/2cb89664897be33f78c65d3d2845954c8d7a43b8", + "reference": "2cb89664897be33f78c65d3d2845954c8d7a43b8", + "shasum": "" + }, + "require": { + "php": ">=8.2" + }, + "require-dev": { + "symfony/filesystem": "^6.4|^7.0" + }, + "type": "library", + "autoload": { + "psr-4": { + "Symfony\\Component\\Finder\\": "" + }, + "exclude-from-classmap": [ + "/Tests/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Fabien Potencier", + "email": "fabien@symfony.com" + }, + { + "name": "Symfony Community", + "homepage": "https://symfony.com/contributors" + } + ], + "description": "Finds files and directories via an intuitive fluent interface", + "homepage": "https://symfony.com", + "support": { + "source": "https://github.com/symfony/finder/tree/v7.1.6" + }, + "funding": [ + { + "url": "https://symfony.com/sponsor", + "type": "custom" + }, + { + "url": "https://github.com/fabpot", + "type": "github" + }, + { + "url": "https://tidelift.com/funding/github/packagist/symfony/symfony", + "type": "tidelift" + } + ], + "time": "2024-10-01T08:31:23+00:00" + }, + { + "name": "symfony/polyfill-ctype", + "version": "v1.31.0", + "source": { + "type": "git", + "url": "https://github.com/symfony/polyfill-ctype.git", + "reference": "a3cc8b044a6ea513310cbd48ef7333b384945638" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/symfony/polyfill-ctype/zipball/a3cc8b044a6ea513310cbd48ef7333b384945638", + "reference": "a3cc8b044a6ea513310cbd48ef7333b384945638", + "shasum": "" + }, + "require": { + "php": ">=7.2" + }, + "provide": { + "ext-ctype": "*" + }, + "suggest": { + "ext-ctype": "For best performance" + }, + "type": "library", + "extra": { + "thanks": { + "name": "symfony/polyfill", + "url": "https://github.com/symfony/polyfill" + } + }, + "autoload": { + "files": [ + "bootstrap.php" + ], + "psr-4": { + "Symfony\\Polyfill\\Ctype\\": "" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Gert de Pagter", + "email": "BackEndTea@gmail.com" + }, + { + "name": "Symfony Community", + "homepage": "https://symfony.com/contributors" + } + ], + "description": "Symfony polyfill for ctype functions", + "homepage": "https://symfony.com", + "keywords": [ + "compatibility", + "ctype", + "polyfill", + "portable" + ], + "support": { + "source": "https://github.com/symfony/polyfill-ctype/tree/v1.31.0" + }, + "funding": [ + { + "url": "https://symfony.com/sponsor", + "type": "custom" + }, + { + "url": "https://github.com/fabpot", + "type": "github" + }, + { + "url": "https://tidelift.com/funding/github/packagist/symfony/symfony", + "type": "tidelift" + } + ], + "time": "2024-09-09T11:45:10+00:00" + }, + { + "name": "symfony/polyfill-intl-grapheme", + "version": "v1.31.0", + "source": { + "type": "git", + "url": "https://github.com/symfony/polyfill-intl-grapheme.git", + "reference": "b9123926e3b7bc2f98c02ad54f6a4b02b91a8abe" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/symfony/polyfill-intl-grapheme/zipball/b9123926e3b7bc2f98c02ad54f6a4b02b91a8abe", + "reference": "b9123926e3b7bc2f98c02ad54f6a4b02b91a8abe", + "shasum": "" + }, + "require": { + "php": ">=7.2" + }, + "suggest": { + "ext-intl": "For best performance" + }, + "type": "library", + "extra": { + "thanks": { + "name": "symfony/polyfill", + "url": "https://github.com/symfony/polyfill" + } + }, + "autoload": { + "files": [ + "bootstrap.php" + ], + "psr-4": { + "Symfony\\Polyfill\\Intl\\Grapheme\\": "" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Nicolas Grekas", + "email": "p@tchwork.com" + }, + { + "name": "Symfony Community", + "homepage": "https://symfony.com/contributors" + } + ], + "description": "Symfony polyfill for intl's grapheme_* functions", + "homepage": "https://symfony.com", + "keywords": [ + "compatibility", + "grapheme", + "intl", + "polyfill", + "portable", + "shim" + ], + "support": { + "source": "https://github.com/symfony/polyfill-intl-grapheme/tree/v1.31.0" + }, + "funding": [ + { + "url": "https://symfony.com/sponsor", + "type": "custom" + }, + { + "url": "https://github.com/fabpot", + "type": "github" + }, + { + "url": "https://tidelift.com/funding/github/packagist/symfony/symfony", + "type": "tidelift" + } + ], + "time": "2024-09-09T11:45:10+00:00" + }, + { + "name": "symfony/polyfill-intl-normalizer", + "version": "v1.31.0", + "source": { + "type": "git", + "url": "https://github.com/symfony/polyfill-intl-normalizer.git", + "reference": "3833d7255cc303546435cb650316bff708a1c75c" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/symfony/polyfill-intl-normalizer/zipball/3833d7255cc303546435cb650316bff708a1c75c", + "reference": "3833d7255cc303546435cb650316bff708a1c75c", + "shasum": "" + }, + "require": { + "php": ">=7.2" + }, + "suggest": { + "ext-intl": "For best performance" + }, + "type": "library", + "extra": { + "thanks": { + "name": "symfony/polyfill", + "url": "https://github.com/symfony/polyfill" + } + }, + "autoload": { + "files": [ + "bootstrap.php" + ], + "psr-4": { + "Symfony\\Polyfill\\Intl\\Normalizer\\": "" + }, + "classmap": [ + "Resources/stubs" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Nicolas Grekas", + "email": "p@tchwork.com" + }, + { + "name": "Symfony Community", + "homepage": "https://symfony.com/contributors" + } + ], + "description": "Symfony polyfill for intl's Normalizer class and related functions", + "homepage": "https://symfony.com", + "keywords": [ + "compatibility", + "intl", + "normalizer", + "polyfill", + "portable", + "shim" + ], + "support": { + "source": "https://github.com/symfony/polyfill-intl-normalizer/tree/v1.31.0" + }, + "funding": [ + { + "url": "https://symfony.com/sponsor", + "type": "custom" + }, + { + "url": "https://github.com/fabpot", + "type": "github" + }, + { + "url": "https://tidelift.com/funding/github/packagist/symfony/symfony", + "type": "tidelift" + } + ], + "time": "2024-09-09T11:45:10+00:00" + }, + { + "name": "symfony/polyfill-php80", + "version": "v1.31.0", + "source": { + "type": "git", + "url": "https://github.com/symfony/polyfill-php80.git", + "reference": "60328e362d4c2c802a54fcbf04f9d3fb892b4cf8" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/symfony/polyfill-php80/zipball/60328e362d4c2c802a54fcbf04f9d3fb892b4cf8", + "reference": "60328e362d4c2c802a54fcbf04f9d3fb892b4cf8", + "shasum": "" + }, + "require": { + "php": ">=7.2" + }, + "type": "library", + "extra": { + "thanks": { + "name": "symfony/polyfill", + "url": "https://github.com/symfony/polyfill" + } + }, + "autoload": { + "files": [ + "bootstrap.php" + ], + "psr-4": { + "Symfony\\Polyfill\\Php80\\": "" + }, + "classmap": [ + "Resources/stubs" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Ion Bazan", + "email": "ion.bazan@gmail.com" + }, + { + "name": "Nicolas Grekas", + "email": "p@tchwork.com" + }, + { + "name": "Symfony Community", + "homepage": "https://symfony.com/contributors" + } + ], + "description": "Symfony polyfill backporting some PHP 8.0+ features to lower PHP versions", + "homepage": "https://symfony.com", + "keywords": [ + "compatibility", + "polyfill", + "portable", + "shim" + ], + "support": { + "source": "https://github.com/symfony/polyfill-php80/tree/v1.31.0" + }, + "funding": [ + { + "url": "https://symfony.com/sponsor", + "type": "custom" + }, + { + "url": "https://github.com/fabpot", + "type": "github" + }, + { + "url": "https://tidelift.com/funding/github/packagist/symfony/symfony", + "type": "tidelift" + } + ], + "time": "2024-09-09T11:45:10+00:00" + }, + { + "name": "symfony/polyfill-php81", + "version": "v1.31.0", + "source": { + "type": "git", + "url": "https://github.com/symfony/polyfill-php81.git", + "reference": "4a4cfc2d253c21a5ad0e53071df248ed48c6ce5c" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/symfony/polyfill-php81/zipball/4a4cfc2d253c21a5ad0e53071df248ed48c6ce5c", + "reference": "4a4cfc2d253c21a5ad0e53071df248ed48c6ce5c", + "shasum": "" + }, + "require": { + "php": ">=7.2" + }, + "type": "library", + "extra": { + "thanks": { + "name": "symfony/polyfill", + "url": "https://github.com/symfony/polyfill" + } + }, + "autoload": { + "files": [ + "bootstrap.php" + ], + "psr-4": { + "Symfony\\Polyfill\\Php81\\": "" + }, + "classmap": [ + "Resources/stubs" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Nicolas Grekas", + "email": "p@tchwork.com" + }, + { + "name": "Symfony Community", + "homepage": "https://symfony.com/contributors" + } + ], + "description": "Symfony polyfill backporting some PHP 8.1+ features to lower PHP versions", + "homepage": "https://symfony.com", + "keywords": [ + "compatibility", + "polyfill", + "portable", + "shim" + ], + "support": { + "source": "https://github.com/symfony/polyfill-php81/tree/v1.31.0" + }, + "funding": [ + { + "url": "https://symfony.com/sponsor", + "type": "custom" + }, + { + "url": "https://github.com/fabpot", + "type": "github" + }, + { + "url": "https://tidelift.com/funding/github/packagist/symfony/symfony", + "type": "tidelift" + } + ], + "time": "2024-09-09T11:45:10+00:00" + }, + { + "name": "symfony/process", + "version": "v7.1.7", + "source": { + "type": "git", + "url": "https://github.com/symfony/process.git", + "reference": "9b8a40b7289767aa7117e957573c2a535efe6585" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/symfony/process/zipball/9b8a40b7289767aa7117e957573c2a535efe6585", + "reference": "9b8a40b7289767aa7117e957573c2a535efe6585", + "shasum": "" + }, + "require": { + "php": ">=8.2" + }, + "type": "library", + "autoload": { + "psr-4": { + "Symfony\\Component\\Process\\": "" + }, + "exclude-from-classmap": [ + "/Tests/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Fabien Potencier", + "email": "fabien@symfony.com" + }, + { + "name": "Symfony Community", + "homepage": "https://symfony.com/contributors" + } + ], + "description": "Executes commands in sub-processes", + "homepage": "https://symfony.com", + "support": { + "source": "https://github.com/symfony/process/tree/v7.1.7" + }, + "funding": [ + { + "url": "https://symfony.com/sponsor", + "type": "custom" + }, + { + "url": "https://github.com/fabpot", + "type": "github" + }, + { + "url": "https://tidelift.com/funding/github/packagist/symfony/symfony", + "type": "tidelift" + } + ], + "time": "2024-11-06T09:25:12+00:00" + }, + { + "name": "symfony/service-contracts", + "version": "v3.5.0", + "source": { + "type": "git", + "url": "https://github.com/symfony/service-contracts.git", + "reference": "bd1d9e59a81d8fa4acdcea3f617c581f7475a80f" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/symfony/service-contracts/zipball/bd1d9e59a81d8fa4acdcea3f617c581f7475a80f", + "reference": "bd1d9e59a81d8fa4acdcea3f617c581f7475a80f", + "shasum": "" + }, + "require": { + "php": ">=8.1", + "psr/container": "^1.1|^2.0", + "symfony/deprecation-contracts": "^2.5|^3" + }, + "conflict": { + "ext-psr": "<1.1|>=2" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-main": "3.5-dev" + }, + "thanks": { + "name": "symfony/contracts", + "url": "https://github.com/symfony/contracts" + } + }, + "autoload": { + "psr-4": { + "Symfony\\Contracts\\Service\\": "" + }, + "exclude-from-classmap": [ + "/Test/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Nicolas Grekas", + "email": "p@tchwork.com" + }, + { + "name": "Symfony Community", + "homepage": "https://symfony.com/contributors" + } + ], + "description": "Generic abstractions related to writing services", + "homepage": "https://symfony.com", + "keywords": [ + "abstractions", + "contracts", + "decoupling", + "interfaces", + "interoperability", + "standards" + ], + "support": { + "source": "https://github.com/symfony/service-contracts/tree/v3.5.0" + }, + "funding": [ + { + "url": "https://symfony.com/sponsor", + "type": "custom" + }, + { + "url": "https://github.com/fabpot", + "type": "github" + }, + { + "url": "https://tidelift.com/funding/github/packagist/symfony/symfony", + "type": "tidelift" + } + ], + "time": "2024-04-18T09:32:20+00:00" + }, + { + "name": "symfony/stopwatch", + "version": "v7.1.6", + "source": { + "type": "git", + "url": "https://github.com/symfony/stopwatch.git", + "reference": "8b4a434e6e7faf6adedffb48783a5c75409a1a05" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/symfony/stopwatch/zipball/8b4a434e6e7faf6adedffb48783a5c75409a1a05", + "reference": "8b4a434e6e7faf6adedffb48783a5c75409a1a05", + "shasum": "" + }, + "require": { + "php": ">=8.2", + "symfony/service-contracts": "^2.5|^3" + }, + "type": "library", + "autoload": { + "psr-4": { + "Symfony\\Component\\Stopwatch\\": "" + }, + "exclude-from-classmap": [ + "/Tests/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Fabien Potencier", + "email": "fabien@symfony.com" + }, + { + "name": "Symfony Community", + "homepage": "https://symfony.com/contributors" + } + ], + "description": "Provides a way to profile code", + "homepage": "https://symfony.com", + "support": { + "source": "https://github.com/symfony/stopwatch/tree/v7.1.6" + }, + "funding": [ + { + "url": "https://symfony.com/sponsor", + "type": "custom" + }, + { + "url": "https://github.com/fabpot", + "type": "github" + }, + { + "url": "https://tidelift.com/funding/github/packagist/symfony/symfony", + "type": "tidelift" + } + ], + "time": "2024-09-25T14:20:29+00:00" + }, + { + "name": "symfony/string", + "version": "v7.1.6", + "source": { + "type": "git", + "url": "https://github.com/symfony/string.git", + "reference": "61b72d66bf96c360a727ae6232df5ac83c71f626" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/symfony/string/zipball/61b72d66bf96c360a727ae6232df5ac83c71f626", + "reference": "61b72d66bf96c360a727ae6232df5ac83c71f626", + "shasum": "" + }, + "require": { + "php": ">=8.2", + "symfony/polyfill-ctype": "~1.8", + "symfony/polyfill-intl-grapheme": "~1.0", + "symfony/polyfill-intl-normalizer": "~1.0", + "symfony/polyfill-mbstring": "~1.0" + }, + "conflict": { + "symfony/translation-contracts": "<2.5" + }, + "require-dev": { + "symfony/emoji": "^7.1", + "symfony/error-handler": "^6.4|^7.0", + "symfony/http-client": "^6.4|^7.0", + "symfony/intl": "^6.4|^7.0", + "symfony/translation-contracts": "^2.5|^3.0", + "symfony/var-exporter": "^6.4|^7.0" + }, + "type": "library", + "autoload": { + "files": [ + "Resources/functions.php" + ], + "psr-4": { + "Symfony\\Component\\String\\": "" + }, + "exclude-from-classmap": [ + "/Tests/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Nicolas Grekas", + "email": "p@tchwork.com" + }, + { + "name": "Symfony Community", + "homepage": "https://symfony.com/contributors" + } + ], + "description": "Provides an object-oriented API to strings and deals with bytes, UTF-8 code points and grapheme clusters in a unified way", + "homepage": "https://symfony.com", + "keywords": [ + "grapheme", + "i18n", + "string", + "unicode", + "utf-8", + "utf8" + ], + "support": { + "source": "https://github.com/symfony/string/tree/v7.1.6" + }, + "funding": [ + { + "url": "https://symfony.com/sponsor", + "type": "custom" + }, + { + "url": "https://github.com/fabpot", + "type": "github" + }, + { + "url": "https://tidelift.com/funding/github/packagist/symfony/symfony", + "type": "tidelift" + } + ], + "time": "2024-09-25T14:20:29+00:00" + }, + { + "name": "theseer/tokenizer", + "version": "1.2.3", + "source": { + "type": "git", + "url": "https://github.com/theseer/tokenizer.git", + "reference": "737eda637ed5e28c3413cb1ebe8bb52cbf1ca7a2" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/theseer/tokenizer/zipball/737eda637ed5e28c3413cb1ebe8bb52cbf1ca7a2", + "reference": "737eda637ed5e28c3413cb1ebe8bb52cbf1ca7a2", + "shasum": "" + }, + "require": { + "ext-dom": "*", + "ext-tokenizer": "*", + "ext-xmlwriter": "*", + "php": "^7.2 || ^8.0" + }, + "type": "library", + "autoload": { + "classmap": [ + "src/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "BSD-3-Clause" + ], + "authors": [ + { + "name": "Arne Blankerts", + "email": "arne@blankerts.de", + "role": "Developer" + } + ], + "description": "A small library for converting tokenized PHP source code into XML and potentially other formats", + "support": { + "issues": "https://github.com/theseer/tokenizer/issues", + "source": "https://github.com/theseer/tokenizer/tree/1.2.3" + }, + "funding": [ + { + "url": "https://github.com/theseer", + "type": "github" + } + ], + "time": "2024-03-03T12:36:25+00:00" + }, + { + "name": "xheaven/composer-git-hooks", + "version": "3.1.0", + "source": { + "type": "git", + "url": "https://github.com/xHeaven/composer-git-hooks.git", + "reference": "0d1aa77043532f6d235ad89525beebe75258e276" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/xHeaven/composer-git-hooks/zipball/0d1aa77043532f6d235ad89525beebe75258e276", + "reference": "0d1aa77043532f6d235ad89525beebe75258e276", + "shasum": "" + }, + "require": { + "php": ">=8.1", + "symfony/console": "^6.0" + }, + "require-dev": { + "ext-json": "*", + "friendsofphp/php-cs-fixer": "^3.0", + "phpunit/phpunit": "^10.0" + }, + "bin": [ + "cghooks" + ], + "type": "library", + "extra": { + "hooks": { + "pre-commit": "composer check-style" + } + }, + "autoload": { + "files": [ + "src/helpers.php" + ], + "psr-4": { + "BrainMaestro\\GitHooks\\": "src/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Ezinwa Okpoechi", + "email": "brainmaestro@outlook.com" + }, + { + "name": "Mark Magyar", + "email": "hello@mmark.me" + } + ], + "description": "Easily manage git hooks in your composer config", + "keywords": [ + "HOOK", + "composer", + "git" + ], + "support": { + "source": "https://github.com/xHeaven/composer-git-hooks/tree/3.1.0" + }, + "time": "2023-08-11T00:39:44+00:00" + } + ], + "aliases": [], + "minimum-stability": "stable", + "stability-flags": { + "team-reflex/discord-php": 20, + "roave/security-advisories": 20 + }, + "prefer-stable": false, + "prefer-lowest": false, + "platform": { + "php": ">=8.1" + }, + "platform-dev": [], + "plugin-api-version": "2.6.0" +} diff --git a/src/phpunit.xml b/src/phpunit.xml new file mode 100644 index 0000000..82efe79 --- /dev/null +++ b/src/phpunit.xml @@ -0,0 +1,17 @@ + + + + + Tests + + + + + + src + + + \ No newline at end of file diff --git a/src/version b/src/version new file mode 100644 index 0000000..369eb72 --- /dev/null +++ b/src/version @@ -0,0 +1 @@ +0.6.0.0.rc