merging wiki into main (#388)

* #49, #51 feat(admin): persistent app configuration, nova settings tool, video streaming killswitch

* style: Fix StyleCI issues

* #41 feat(admin): add sluggable fields

* #40 refactor: alias fields should be named slug

* #62 feat(api): implementing announcement endpoints

* style: fix StyleCI finding

* #43 refactor: enum name should prefix model

* test: year endpoints & StyleCI findings

* fix(seed): resolve bucketed anime attributes, reduce collisions

* #67, #68 fix(seed): reduce theme collisions & order seasons chronologically, fix(admin): invitation & lens pruning, fix(api): year endpoint key sorting, style fixes

* #45 feat(seed): create anilist resources (#73)

* Update tests.yml

* #45 feat(seed): create anilist resources

* docs: fix shields & link to installation guide, chore: update fortify stubs (#75)

* Create CONTRIBUTING.md

* #46 Image Resource, seeding anime images and synopsis, fix lens queries, php8 readiness, onboarding compatibility (#76)

* #46 feat(seed): image resource, lens query fix, php8 readiness & onboarding

* style: StyleCI finding

* fix(api): allow themes to eager load images (#78)

* #49 fix(videos): use subdomain routing to stream video (#82)

* #74 feat(seed): implementing database dump artisan console command (#84)

* #74 feat(seed): implementing database dump artisan console command

* #74 style: fix StyleCI Finding

* fix(video): use int return type via spaceships for set difference callback function (#85)

* fix(admin): nova slug field now allows customization on update (#86)

* refactor(video): Allow video stream middleware (#87)

* refactor(video): is video streaming allowed as middleware

* style: StyleCI findings

* #88 feat(video): add view count for videos (#90)

* #88 feat(video): add view count for videos

* style: fix StyleCI findings

* Initial unit tests (#93)

* test: initial unit tests, community docs, test runner fixes

* test: fixing github actions yml

* test: fixing github actions yml

* test: incremental push for backfilling tests, some cleanup

* test: query parser & update jetstream stubs

* style: fix StyleCI findings

* test: don't expose image links

* test: github actions doesn't like composer 2

* test: setup explicit php-version

* test: make grill directory for test runner

* test: use artisan test runner

* test: handle no grills

* test: sitemap should test for expected routes

* test: install and publish assets before testing

* test: add php extensions

* test: rename sitemap route

* test: provide video subdomain

* #81 fix(api): use scout elastic driver package & refactor queries (#94)

* #81 fix(api): search resource should implement limiting

* fix(api): use new elastic scout driver package for search & refactor queries

* fix(api): forgot updates to dependencies

* style: fix StyleCI findings

* style: fix StyleCI findings

* fix(seed): add 2021 pages (#95)

* #77 Initial implementation of deep filtering through constrained eager loads (#96)

* feat(api): initial deep filtering implementation

* fix(test): valid query parser limit must be greater than 0

* fix(api): jsonapi error handler only handles exceptions

* style: fix StyleCI findings

* fix(api): fix relation reflection

* Fixing artist relation resolution (#97)

* feat(api): initial deep filtering implementation

* fix(test): valid query parser limit must be greater than 0

* fix(api): jsonapi error handler only handles exceptions

* style: fix StyleCI findings

* fix(api): fix relation reflection

* fix(api): artist endpoint relation resolution

* Adding Filter Unit Tests, bumping dependencies, updating stubs, actions secret test (#99)

* test: filters, chore: update fortify stubs & bump dependencies

* test: fix action

* test: try another format

* test: try another format

* test: try another format

* #45 feat(seed): create kitsu anime resources (#100)

* #45 feat(seed): seed kitsu anime resource

* style: fix StyleCI findings

* #45 feat(seed): create anidb anime resources (#101)

* [WIP] Discord Notifications for database updates, migrating to event dispatcher (#102)

* feat: initial draft for discord notifications on database updates, refactor: use model event dispatcher instead of observers

* style: fix StyleCI findings

* refactor: use interface for discord event, don't serialize models on update & delete, first attempt to clean up embedded fields

* style: fix StyleCI findings

* refactor: discord embed field traits

* fix: circument exception for empty values in embed fields by using a default value

* refactor: update related scout indices and cascade deletes

* style: fix StyleCI findings

* feat: add artist events

* feat: incremental commit for entry events

* properly cast and format attribute values for update embed fields

* feat: refresh video documents when deleting entries

* feat: adding external resource events

* feat: add events for images

* feat: adding invitation events

* feat: adding series events

* feat: adding series events

* feat: add song events

* feat: adding synonym events

* style: fix StyleCI findings

* feat: adding theme events

* feat: adding video events

* feat: add LaserDisc video source

* feat: add video entry events

* add anime series events

* add anime resource events

* add anime image events

* add artist image events

* add artist resource events

* feat: add artist member events

* feat: add artist song events

* refactor: use contract for readable model naming

* style: fix StyleCI findings

* feat: provide discord channel id in event, refactor: contracts, concerns, structural cleanup

* style: fix StyleCI findings

* style: fix StyleCI findings

* fix(admin): don't enforce uniqueness for slugs on creation where customization isn't available

* feat: throttle discord notifications

* feat: adding announcement events

* feat: adding invitation events

* feat: adding user events

* removing TODOs, localization will be evaluated later

* bump dependencies before merge

* Make anime index seeder rerunnable (#105)

* fix(seeder): make anime index seeder rerunnable

* style: fix StyleCI findings

* fix(seeder): make anime resource seeder rerunnable (#106)

* fix(seeders): make anime season seeder rerunnable (#108)

* fix(seeders): make mal seeder rerunnable (#110)

* fix(seeders): make anime themes rerunnable (#111)

* fix(seeders): make anime themes seeder rerunnable

* fix(seeders): handle business fish group

* fix(seeders): make artist and series seeders rerunnable (#112)

* fix(seeders): make artist song seeder rerunnable (#113)

* fix(seeders): make database seed action rerunnable and comprehensive (#114)

* Anilist artist seeders (#115)

* #45, #80 feat(seeders): add artist resource & cover seeders

* style: fix StyleCI findings

* test: backfilling event dispatcher tests (#116)

* test: backfilling event dispatcher tests

* fix: pivot factory directory name

* test: discord notifications (#117)

* test: policies, discord embed fields, notifications (#118)

* test: policies, discord embed fields, notifications

* style: fix StyleCI findings

* test: models (#119)

* test: models

* style: fix StyleCI findings

* Test: pivots, jobs, mail, json api (#120)

* test: pivots, jobs, mail, providers

* style: fix StyleCI findings

* test: add missing traits to persistent config test for github runner

* test: remove provider unit test, should be feature or acceptance test

* test: json api - anime, announcement, artist

* test: json api - entry endpoints

* test: json api - resource endpoints

* test: json api - image endpoints

* test: json api - series endpoints

* test: json api - song endpoints

* test: json api - synonym endpoints

* test: json api - theme endpoints

* test: json api - video endpoints

* test: json api - year endpoints

* Unit testing Nova (#128)

* test: nova unit

* style: fix StyleCI findings

* #124 chore: php 8 readiness (#129)

* #88 feat(api): add views to video resource, fix(api): collection transformation (#130)

* Elastic payload filters [incremental] (#131)

* #127 feat(api): add top-level filtering to search endpoint

* style: fix StyleCI findings

* Filter conditions (#132)

* feat(api): support for advanced filtering criteria

* style: fix StyleCI findings

* fix(api): allow elastic host configuration (#133)

* fix(api): date filters shall validate allowed formats, timestamp precision (#136)

* feat(api): date filters should handle high precision time (#137)

* fix(api): add tags to video resource for client slug (#138)

* Soft deletes (#139)

* feat(api): soft deletion for gatsby incremental builds

* style: fix StyleCI findings

* sensible defaults for config, test performance, enlightn (#140)

* fix(config): default video subdomain, test: performance, chore: enlightn

* test: performance fixes

* test: fake asset before streaming

* fix(config): updating out of date framework stubs, enlightn findings (#141)

* fix(config): addressing enlightn findings, updating framework stubs

* style: fix StyleCI findings

* fix(config): match framework stubs for test runner

* feat(config): add horizon dashboard (#142)

* fix(config): adding secure headers (#143)

* Enlightn pro (#144)

* fix(config): enlightn pro

* fix(config): fix tests

* fix(config): add telescope (#145)

* Upgrading jetstream config (#146)

* fix(auth): upgrading jetstream stubs, enable terms, fix telescope and tests

* style: fix StyleCI findings

* fix: laravel-mix 6.0 upgrade

* Jetstream ux refactor (#147)

* fix(ux): adopt jetstream styling for guest pages

* style: fix StyleCI findings

* fix(ux): add announcement margin (#148)

* feat(docs): adding encoding guides and guidelines (#149)

* fix(ux): proper link to encoding guides (#150)

* #153 fix: an attempt to address n+1 queries (#165)

* #153 fix: an attempt to address n+1 queries

* style: fix StyleCI findings

* style: fix StyleCI findings

* fix(seed): use guzzle client to retrieve contents of external pages (#167)

* fix(seed): use guzzle to retrieve external page contents

* style: fix StyleCI findings

* fix: workflow

* fix: workflow

* #152 fix: trait for content streaming to optimize image retrieval (#168)

* #152 fix: trait for content streaming to optimize image retrieval

* style: fix StyleCI finding

* fix: hotfix for elastic video index (#169)

* #164 feat: add pivot timestamps & qualify conditions (#170)

* feat: add pivot timestamps & qualify conditions

* fix: remove audits from pivots

* #157, #158 feat(docs): add Community & Event subreddit wiki pages (#171)

* #157, #158 feat(docs): add Community & Event subreddit wiki pages

* refactor: generate URLs

* fix(api): add condition for elastic search payload to prevent engine exception (#172)

* fix(api): add condition for elastic search payload to prevent engine exception

* style: fix StyleCI findings

* fix(seed): fail silently if an external service raises an internal exception (#173)

* #125 feat(auth): replace user role with jetstream teams (#176)

* #125 feat(auth): replace user role with jetstream teams

* style: fix StyleCI findings

* Onboarding friendliness (#180)

* fix: onboarding friendliness, video routing, config best practices, update stubs

* style: address StyleCI findings

* fix(discord): embed styling updates (#181)

* fix(discord): do not raise events on restore save operation (#182)

* fix(redis): detect redis enabled, optimize throttling, only use redis is enabled (#183)

* fix(redis): named rate limiters expect a specific number of arguments (#184)

* #155 fix(api): changing resource inclusion strategy defaults (#185)

* fix(api): changing resource inclusion strategy defaults

* style: fix StyleCI findings

* fix(tests): missed an instance of docs that incorrectly state that all allowed resources are included by default

* fix(api): adding missing checks for strictness of query string structure (#186)

* feat(api): pagination strategies and constrained eager loads for search (#187)

* feat(search): allow filter conditions in elasticsearch payload (#188)

* #155 fix(api): don't default to max allowed paging results, trim allowed includes (#190)

* fix(api): don't default to max allowed paging results, trim allowed includes

* style: fix StyleCI findings

* style: fix StyleCI findings

* chore: adding favicon, new common position, bumping dependencies (#191)

* chore: add favicon, bump dependencies, add new common position

* fix(test): specify max page size for announcement index tests

* feat(admin): add unlinked videos lens (#194)

* #159 fix(admin): adding date range filters for timestamps (#196)

* #159 fix(admin): adding date range filters for timestamps

* style: fix StyleCI finding

* chore: bump dependencies, framework updates, optimize events & policies & seeders (#198)

* chore: bump dependencies, framework updates, optimize events & policies & seeders

* style: fix StyleCI findings

* Transparency (#197)

* feat: initial commit for transparency models

* style: fix StyleCI findings

* feat(transparency): starting landing page

* style: fix StyleCI findings

* refactor: namespace for billing models

* style: fix StyleCI findings

* fix(transparency): adding subheadings and better styling for readability

* feat(transparency): add filtering and validation

* style: fix StyleCI findings

* refactor: reconcile against repositories, feat(transparency): reconcile balances, test: donate, transparency

* style: fix StyleCI findings

* fix: transaction reconcilation, selected month

* fix: balance/transaction reconcilation tests

* style: fix StyleCI findings

* chore: bump dependencies, update stubs, doc & naming cleanup (#199)

* chore: bump dependencies, update stubs, doc & naming cleanup

* fix: revert change to resend invitation messages

* chore: addressing phpstorm code inspection findings (#200)

* refactor: phpstorm linting

* style: fix StyleCI findings

* style: StyleCI should use PHP 8

* style: fix StyleCI findings

* feat: enable event discovery, fix: strict typing & lazy loading errors (#201)

* chore: bump dependencies (#202)

* fix: transparency filtering, fix: psalm findings, chore: plural types in docs (#205)

* fix: transparency filtering, fix: psalm findings, chore: plural types in docs

* style: fix StyleCI findings

* style: fix StyleCI findings

* fix: strict type errors in seeders

* fix(transparency): restore ordering & adjust reconcile job time, refactor: use lambdas in reconcile traits for comparison (#206)

* fix(transparency): restore ordering & adjust reconcile job time, refactor: use lambdas in reconcile traits for comparison

* style: fix StyleCI findings

* Chore 2021 06 07 (#210)

* chore: strict type exception, best OP IX, ordering, first-pass namespacing cleanup

* style: fix StyleCI findings

* style: fix StyleCI findings

* chore: bump dependencies (#212)

* Namespacing (#214)

* refactor: model namespacing

* style: fix StyleCI findings

* feat(api): validate filter values for integer columns, chore: update improvement & host docs (#215)

* feat(api): validate filter values for integer columns, chore: update improvement & host docs

* forgot the test class

* style fixes

* chore: bump dependencies (#216)

* #213 feat(config): support read and write connections (#217)

* fix(auth): remove registration link from team invitation mail (#218)

* #219 feat(queue): schedule pruned failed jobs command (#224)

* fix(api): support discovery of elasticsearch query payloads by matching property for namespacing concerns (#227)

* fix(admin): editors should be able to create songs for themes, resource strict type error (#228)

* #44 feat(api): migrating docs from swagger to vuepress (#230)

* Test nova namespacing (#235)

* chore: bump dependencies, refactor: test & nova namespacing

* style: fix StyleCI findings

* feat(db): schedule daily animethemes-web db-plugin dump (#236)

* feat(db): schedule daily animethemes-web db-plugin dump (#237)

* fix(api): addressing strict type errors for top-level elasticsearch filter value (#239)

* fix(api): allow sorting on searches (#241)

* #208, #220 feat(api): add like operator and additional filters [incremental] (#242)

* feat(api): add like and not like operators

* #208, #220 feat(api): add like operator and additional filters [incremental]

* fix(auth): use signed URLs for invite-only registrations (#243)

* #203 fix(auth): use signed URLs for invite-only registrations

* #203 style: fix StyleCI

* #203 fix(auth): remove psalm annotations

* #203 fix(auth): exclude sensitive data from parameter logging

* #203 fix(auth): exclude sensitive data from parameter logging

* #203 fix(auth): exclude sensitive data from parameter logging

* #208 feat(api): adding id filters (#244)

* feat(api): add id columns, fix(auth): remove fortify register route

* style: fix StyleCI findings

* fix(api): mismatched class comments

* fix(console): we have to assume that the video disk uses the s3 driver (#245)

* chore: use http facade instead of guzzle client (#246)

* chore: use http facade instead of guzzle

* style: fix StyleCI findings

* fix(transparency): change type for transaction external id column (#247)

* fix(views): hide view recording behind feature flag in middleware layer (#248)

* #162 feat(console): prune stale database dumps (#249)

* chore: bumping dependencies (#250)

* #77 feat(api): add random sort & allow sort key-column mutation, refactor: use data facade & extract criteria / parsers from api query (#251)

* feat(api): add random sort & allow sort key-column mutation, refactor: use date facade & extract criteria/parsers from api query

* style: fix StyleCI findings

* refactor: make criteria function names consistent

* fix(api): enum filters should use description instead of key (#252)

* chore: fix namespace of tests for PSR-4 compliance (#253)

* fix(api): sort criteria should be applied to sorts in order (#254)

* fix(api): circumvent exception caused by entry version field mapping (#255)

* #42 fix: namespace nested anime types [incremental] (#256)

* #42 fix: namespace nested anime types

* style: fix StyleCI findings

* fix: delete new migrations

* #42 fix(api): nested api resources (#257)

* #42 fix(api): nested api resources

* style: fix StyleCI findings

* fix(api): fix limit parsing for pagination strategy (#258)

* fix(api): rename search indices to match table names (#259)

* chore: bump dependencies (#260)

* #72 feat(api): initial commit for studio resource (#261)

* #72 feat(api): initial commit for studio resource

* style: fix StyleCI finding

* fix: rollback faker change (#262)

* fix(admin): addressing staging exceptions (#263)

* fix(api): make studio collection wrap plural (#264)

* feat(api): add relation sort (#265)

* feat(api): add relation sort

* style: fix StyleCI findings

* #160 feat(api): initial implementation of relation filter (#266)

* feat(api): initial implementation of relation filter

* style: fix StyleCI findings

* chore: apply select binding to query builder to reduce memory footprint (#267)

* chore: apply select binding to query builder to reduce memory footprint

* fix: always include name attributes

* feat(api): initial commit for api form requests (#268)

* feat(api): initial commit for api form requests

* style: fix StyleCI findings

* fix(api): correcting year index, abstracting search rules

* feat(api): validate sparse fieldsets (#269)

* feat(api): validate api sparse fieldsets

* style: fix StyleCI findings

* chore: bump & prune dependencies, update stubs, address static analysis findings (#270)

* chore: bump & prune dependencies, update stubs, address static analysis findings

* chore: bump static analysis level

* test: use new lazily refresh database trait (#271)

* test: use new lazily refresh database trait for test cases

* style: fix StyleCI findings

* fix(api): address exceptions for resource inclusion in search endpoint & minimize requests (#272)

* fix(api): address exceptions for resource inclusion in search endpoint & minimize requests

* style: fix StyleCI findings

* fix(api): revert search resource inclusion fix (#273)

* fix(api): plurality of resource wrap should be consistent (#274)

* chore: add static analysis github action (#275)

* chore: add static analysis action

* chore: test static analysis failure

* chore: revert

* chore: rename static analysis job

* chore: add badge to readme

* [WIP] StudioResource pivot table (#277)

* Added app.css and app.js to .gitignore

* Created migration for the studio resource pivot

* Created studio resource pivot class

* Created relations in Studio and ExternalResource for pivot table

* Added StudioResource table to database dump command

* Created Events for StudioResource pivot

* Added allowed include to Studio and External Resource schema

* Created StudioResourceFactory

* Added StudioResource attributes to the json resource

* Added relations to nova resources

* Created new policies for StudioResource pivot

* Created unit tests for StudioResource pivot

* Apply fixes from StyleCI

* Fixed $allowedTables sorting

* Fixed various mistakes found in code review.

* Apply fixes from StyleCI

* Fixed ordering of ATTRIBUTE constants

* Fixed mistakes found in 2nd code review.

* Apply fixes from StyleCI

* Added StudioResourcePivot to StudioSchema fields

* Fixed nova "studios" resource in ExternalResource

* Apply fixes from StyleCI

Co-authored-by: ProWeebDev <ProWeebDev@users.noreply.github.com>
Co-authored-by: vagrant <vagrant@homestead>

* chore: bump dependencies, stub updates, use framework casting for enums, arrow functions (#278)

* fix(admin): unlinked resource lens should consider studio relation (#279)

* fix(db): seed studio resources when seeding anime studios (#282)

* #72 fix(admin): add lenses for studio resources (#283)

* fix(admin): add lenses for studio resources

* style: fix StyleCI findings

* fix(admin): check feature flags before handling jobs or notifications (#284)

* fix(admin): check feature flags before handling job or notification, relation cleanup in events

* style: fix StyleCI findings

* feat(api): add config item endpoints & make featured theme configurable (#285)

* feat(api): add config item endpoints & make featured theme configurable

* feat(api): add config item endpoints & make featured theme configurable [missing config file]

* fix(seed): support 2022 & mal client ID header, chore: bump dependencies & stubs (#286)

* chore: adopt constructor property promotion prior to php 8.1 upgrade (#287)

* chore: adopt constructor property promotion

* style: fix StyleCI findings

* API Query Types (#288)

* fix(api): extract query & search service types, chore: bump dependencies & update stubs

* style: fix StyleCI findings

* fix: bump incompatible dependencies

* test: restore quietly (#289)

* fix(console): only allow includeTables option in db dump for sqlite v3.32.0+ (#290)

* chore: void typehint, static access fixes, reduce regex, unwrap variables, avoid magic method accessors, grammar & typos (#291)

* chore: prune middleware stubs (#292)

* chore: prune middleware stubs

* chore: prune middleware stubs - forgot modified files

* fix: prefer raising exceptions to failing silently or exiting early [incremental] (#293)

* fix: prefer raising exceptions to failing silently or exiting early

* style: address StyleCI findings

* fix: prefer raising exceptions to failing silently or exiting early (#294)

* feat: adding pages resource (#296)

* feat: add pages resource

* style: address StyleCI findings

* chore: fix links, missing admin config, buggy test (#297)

* Search validation (#298)

* feat(api): validate search endpoint parameters [incremental]

* style: fix StyleCI findings

* feat(api): add scoping to sorts & cleanup validation for endpoints (#299)

* fix(api): conditionally validate filter values [incremental] (#300)

* fix(api): conditionally validate filter values

* style: fix StyleCI findings

* feat: update to laravel 9 (#301)

* feat: update to Laravel 9

* fix(test): force update of phpstan by removing enlightn & addressing new static analysis findings

* style: address StyleCI finding

* upgrade to php 8.1 (#302)

* feat: upgrade to php 8.1

* test: fix test configuration

* feat(api): support select clause for api queries (#303)

* feat(api): support select clause for api queries

* style: fix StyleCI findings

* fix(api): some fields for video resources were the wrong type

* chore: update dependencies (#304)

* feat(console): add ability to dump document-related database tables (#305)

* feat(api): validate allowed filter types [incremental] (#306)

* feat(api): validate allowed filter types [incremental]

* style: fix StyleCI findings

* feat(api): support extended write endpoints [incremental: framework & anime implementation] (#307)

* feat(api): support extended write endpoints [incremental: framework & anime implementation]

* style: fix StyleCI findings

* fix(api): write endpoints need to check token ability as well as policy ability

* feat(api): support extended write endpoints [incremental: artists] (#308)

* feat(api): support extended write endpoints [incremental: external resources] (#309)

* feat(api): support extended write endpoints [incremental: external resources]

* feat(api): support extended write endpoints [incremental: external resources]

* style: fix StyleCI findings

* feat(api): support extended write endpoints [incremental: series] (#310)

* feat(api): support extended write endpoints [incremental: songs] (#311)

* feat(api): support extended write endpoints [incremental: studios] (#312)

* fix(api): support extended write endpoints [incremental: synonyms] (#313)

* feat(api): support extended write endpoints [incremental: synonyms]

* fix(tests): don't create slug that violates constraint

* feat(api): support extended write endpoints [incremental: themes] (#314)

* feat(api): support extended write endpoints [incremental: entries] (#315)

* feat(api): support extended write endpoints [incremental: entries]

* style: fix StyleCI findings

* feat(api): support extended write endpoints [incremental: announcements] (#316)

* feat(api): support extended write endpoints [incremental: announcements]

* style: fix StyleCI findings

* feat(api): support extended write endpoints [incremental: videos] (#317)

* feat(api): support extended write endpoints [incremental: videos]

* style: fix StyleCI findings

* feat(api): support extended write endpoints [incremental: pages] (#318)

* feat(api): support extended write endpoints [incremental: pages]

* style: fix StyleCI findings

* feat(api): support extended write endpoints [incremental: balances] (#319)

* feat(api): support extended write endpoints [incremental: transactions] (#320)

* feat(api): support extended write endpoints [incremental: images] (#321)

* Delete reddit seeders (#322)

* fix(db): remove reddit seeders

* fix(db): remove reddit seeders - update seeder

* fix(api): don't serve images from laravel

* chore: update dependencies & stubs (#323)

* chore: update dependencies & stubs

* fix: update dependency version constraints

* refactor: discover routes in controller attributes (#324)

* feat(admin): update to Nova V4 (#325)

* feat(admin): update to Nova V4

* style: fix StyleCI findings

* fix(admin): address exceptions & apply fixes from day 1 patches (#326)

* feat(admin): Added AnimeThemes branding to Nova (#327)

* fix(admin): explicitly declare searchable columns as columns (#328)

* fix(admin): explicitly declare searchable columns as columns

* fix(admin): missing removal of searchable column declaration on synonyms

* feat(admin): add subtitles to distinguish entries in searches (#329)

* feat(admin): backfill anime studios & make resources actionable (#330)

* feat(admin): backfill anime studios & make resources actionable

* style: fix StyleCI findings

* feat(admin): support db queue connection (#331)

* feat(admin): backfill anime kitsu resources (#332)

* feat(admin): backfill anime kitsu resources

* style: fix StyleCI findings

* fix(admin): add theme from song (#333)

* feat(elastic): Improved search query payload for anime (#334)

* feat(admin): backfill anidb resources (#335)

* feat(admin): backfill anidb resources

* style: address StyleCI findings

* fix(admin): address eager loading with resource fields (#336)

* feat(api): allow client to bypass API rate limiter (#337)

* refactor: backfill anime should not require a specific external resource site (#338)

* refactor: backfill anime should not require a specific external resource site

* style: fix StyleCI findings

* fix: fall-through anime studio backfilling

* feat(admin): backfill ann anime studios (#339)

* feat(api): Added allowed include paths needed for client (#340)

* feat(admin): backfill anilist anime resources (#341)

* feat(admin): backfill mal resources (#342)

* feat(admin): backfill mal resources

* style: fix StyleCI findings

* feat(admin): backfill ann resources (#343)

* fix(admin): fix admin & moderation gates (#344)

* feat(admin): backfill anime images (#347)

* feat(admin): backfill anime images

* style: fix StyleCI findings

* Update videos controller to serve videos through nginx (#346)

* Update videos controller to serve videos through nginx

The proposed change will pass the video through nginx instead of using PHP.

* refactor: use default response & update tests

* style: fix StyleCI findings

Co-authored-by: paranarimasu <33796518+paranarimasu@users.noreply.github.com>

* chore: nginx redirect hotfix, remove reconciliation scheduler task & bump nova (#348)

* fix(admin): make nested types discoverable & make relations filterable (#349)

* fix(admin): make nested types discoverable & make relations filterable

* fix(admin): policy functions for nested types were named incorrectly

* Admin deletion notifications (#350)

* feat(admin): notify admins of soft deletions for review

* style: fix StyleCI findings

* Chore 2022 04 27 (#351)

* chore: bump dependencies & address admin config errors

* style: fix StyleCI finding

* feat(admin): allow backfilling multiple anime & fixing resource reconciliation (#352)

* feat(console): allow video reconciliation against a specific path (#353)

* refactor(events): use covariance to build proper type hierarchy for event classes (#354)

* chore: bump dependencies (#355)

* chore: bump dependencies

* style: fix StyleCI findings

* chore: prune instance of make & update sanctum token expiration (#356)

* feat: replace jetstream teams with spatie permissions (#357)

* feat: replace jetstream teams with spatie permissions

* style: fix StyleCI findings

* style: fix StyleCI finding

* fix(tests): address performance issues with seeding permissions

* style: fix StyleCI finding

* fix(admin): disable wildcard permissions due to performance issues

* fix: remove abilities from permission resource

* fix: make migrations more fault proof, support db cache & fix user factory permission creation

* chore: bump dependencies, fix(admin): a few QoL changes (#358)

* fix(admin): make resource field dependency more nullsafe to accommodate official sites (#359)

* chore: bumping dependencies, updating stubs & cleanup sweep (#360)

* chore: bumping dependencies, updating stubs & cleanup sweep

* style: fix StyleCI findings

* fix(api): fix discovery of ES Query Payload classes after namespace refactor (#361)

* fix(api): has filter validation (#362)

* feat: log authentication events, chore: another cleanup sweep (#370)

* feat: log authentication events, chore: another cleanup sweep

* test: remove status code assertion for test runner env

* test: remove streaming test to prevent exception in test runner

* chore: bump minimum required laravel framework version

* fix(api): support sorting on text fields in elasticsearch queries (#371)

* feat(api): filter on keyword multifield for text mappings (#372)

* chore: bump dependencies (#373)

* chore: bump dependencies

* fix: suppress false positive static analysis finding

* feat: use permission to determine if api rate limiter can be bypassed (#374)

* fix(api): use animethemeentryvideo pivot for featured theme (#375)

* fix(api): add missing featured theme relations (#376)

* fix(api): song.title relation sort for theme search index (#377)

* fix(api): register all sort criteria at once to avoid overriding (#378)

* feat: support customization of domains and paths of route groups (#379)

* feat: support customization of domains and paths of route groups

* fix fallback test url

* style: fix StyleCI finding

* fix: video url should not append path by default (#380)

* fix: remove old database migrations & update robots.txt

Co-authored-by: ProWeebDev <34277874+ProWeebDev@users.noreply.github.com>
Co-authored-by: ProWeebDev <ProWeebDev@users.noreply.github.com>
Co-authored-by: vagrant <vagrant@homestead>
Co-authored-by: Manuel S <ManuelDevelopment@users.noreply.github.com>
This commit is contained in:
paranarimasu
2022-05-30 17:25:06 -05:00
committed by GitHub
parent a74490a3e2
commit 8c7363e95d
1412 changed files with 111723 additions and 9084 deletions
+3
View File
@@ -13,3 +13,6 @@ trim_trailing_whitespace = false
[*.{yml,yaml}]
indent_size = 2
[docker-compose.yml]
indent_size = 4
+173 -27
View File
@@ -1,50 +1,196 @@
APP_NAME=Laravel
# api
API_PATH=/api
API_URL=
# app
APP_NAME=AnimeThemes
APP_ENV=local
APP_KEY=
APP_DEBUG=true
APP_URL=http://localhost
ASSET_URL=null
APP_KEY=
LOG_CHANNEL=stack
# audit
AUDITING_ENABLED=true
# broadcasting
BROADCAST_DRIVER=log
PUSHER_APP_KEY=
PUSHER_APP_SECRET=
PUSHER_APP_ID=
PUSHER_APP_CLUSTER=mt1
ABLY_KEY=
MIX_PUSHER_APP_KEY="${PUSHER_APP_KEY}"
MIX_PUSHER_APP_CLUSTER="${PUSHER_APP_CLUSTER}"
# cache
CACHE_DRIVER=file
MEMCACHED_PERSISTENT_ID=
MEMCACHED_USERNAME=
MEMCACHED_PASSWORD=
MEMCACHED_HOST=127.0.0.1
MEMCACHED_PORT=11211
DYNAMODB_CACHE_TABLE=cache
DYNAMODB_ENDPOINT=
CACHE_PREFIX=animethemes
# database
DB_CONNECTION=mysql
DB_HOST=127.0.0.1
DATABASE_URL=
DB_HOST_READ=127.0.0.1
DB_HOST_WRITE=127.0.0.1
DB_STICKY=false
DB_PORT=3306
DB_DATABASE=laravel
DB_USERNAME=root
DB_PASSWORD=
DB_SOCKET=
MYSQL_ATTR_SSL_CA=null
BROADCAST_DRIVER=log
CACHE_DRIVER=file
QUEUE_CONNECTION=sync
SESSION_DRIVER=file
SESSION_LIFETIME=120
REDIS_CLIENT=predis
REDIS_CLUSTER=redis
REDIS_URL=null
REDIS_HOST=127.0.0.1
REDIS_PASSWORD=null
REDIS_PORT=6379
REDIS_SCHEME=tcp
REDIS_DB=0
REDIS_CACHE_DB=1
MAIL_MAILER=smtp
MAIL_HOST=smtp.mailtrap.io
MAIL_PORT=2525
# elastic client
ELASTIC_HOST=localhost
ELASTIC_PORT=9200
ELASTIC_SCHEME=http
ELASTIC_USER=
ELASTIC_PASS=
# elastic driver
ELASTIC_SCOUT_DRIVER_REFRESH_DOCUMENTS=false
# elastic migrations
ELASTIC_MIGRATIONS_TABLE=elastic_migrations
# enlightn
ENLIGHTN_SKIP_ENVIRONMENT_SPECIFIC=false
ENLIGHTN_USERNAME=
ENLIGHTN_API_TOKEN=
ENLIGHTN_GITHUB_REPO=
# feature flags
ALLOW_VIDEO_STREAMS=false
ALLOW_DISCORD_NOTIFICATIONS=false
ALLOW_VIEW_RECORDING=false
# filesystems
FILESYSTEM_DISK=local
IMAGE_ACCESS_KEY_ID=
IMAGE_SECRET_ACCESS_KEY=
IMAGE_DEFAULT_REGION=
IMAGE_ENDPOINT=
IMAGE_BUCKET=
IMAGE_STREAM_READS=
IMAGE_DISABLE_ASSERTS=
IMAGE_VISIBILITY=
IMAGE_URL=
VIDEO_ACCESS_KEY_ID=
VIDEO_SECRET_ACCESS_KEY=
VIDEO_DEFAULT_REGION=
VIDEO_ENDPOINT=
VIDEO_BUCKET=
VIDEO_STREAM_READS=
VIDEO_DISABLE_ASSERTS=
VIDEO_VISIBILITY=
# hashing
BCRYPT_ROUNDS=10
# horizon
HORIZON_DOMAIN=
HORIZON_PATH=horizon
# jetstream
TEAM_CREATOR_ID=
# logging
LOG_CHANNEL=daily
LOG_DEPRECATIONS_CHANNEL=null
LOG_LEVEL=debug
# mail
MAIL_MAILER=log
MAIL_HOST=smtp.mailgun.org
MAIL_PORT=587
MAIL_ENCRYPTION=tls
MAIL_USERNAME=null
MAIL_PASSWORD=null
MAIL_ENCRYPTION=null
MAIL_FROM_ADDRESS=null
MAIL_FROM_NAME="${APP_NAME}"
MAIL_SENDMAIL_PATH=/usr/sbin/sendmail
MAIL_LOG_CHANNEL=
AWS_ACCESS_KEY_ID=
AWS_SECRET_ACCESS_KEY=
AWS_DEFAULT_REGION=us-east-1
AWS_BUCKET=
AWS_ENDPOINT=
AWS_STREAM_READS=
# nova
NOVA_DOMAIN_NAME=null
NOVA_GUARD=null
NOVA_LICENSE_KEY=
NOVA_PASSWORDS=null
NOVA_PATH=/admin
PUSHER_APP_ID=
PUSHER_APP_KEY=
PUSHER_APP_SECRET=
PUSHER_APP_CLUSTER=mt1
# queue
QUEUE_CONNECTION=sync
REDIS_QUEUE=default
QUEUE_FAILED_DRIVER=database-uuids
MIX_PUSHER_APP_KEY="${PUSHER_APP_KEY}"
MIX_PUSHER_APP_CLUSTER="${PUSHER_APP_CLUSTER}"
# scout
SCOUT_DRIVER=null
SCOUT_PREFIX=
SCOUT_QUEUE=false
SCOUT_IDENTIFY=false
ALGOLIA_APP_ID=
ALGOLIA_SECRET=
GLOBAL_SITE_TAG=
# secure headers
ENABLE_CLEAR_SITE_DATA=false
ENABLE_HSTS=false
ENABLE_EXPECT_CT=false
ENABLE_PERMISSIONS_POLICY=false
ENABLE_CSP=false
# services
MAILGUN_DOMAIN=
MAILGUN_SECRET=
MAILGUN_ENDPOINT=api.mailgun.net
POSTMARK_TOKEN=
DISCORD_BOT_API_TOKEN=
DB_UPDATES_DISCORD_CHANNEL=
ADMIN_DISCORD_CHANNEL=
MAL_CLIENT_ID=null
DO_BEARER_TOKEN=null
# session
SESSION_DRIVER=database
SESSION_LIFETIME=120
SESSION_CONNECTION=null
SESSION_STORE=null
SESSION_DOMAIN=null
SESSION_SECURE_COOKIE=
# teams
HORIZON_TEAM_ID=
NOVA_TEAM_ID=
TELESCOPE_TEAM_ID=
# telescope
TELESCOPE_DOMAIN=null
TELESCOPE_PATH=telescope
TELESCOPE_DRIVER=database
TELESCOPE_ENABLED=true
# video
VIDEO_PATH=/video
VIDEO_URL=
# wiki
WIKI_FEATURED_ENTRY=
WIKI_FEATURED_VIDEO=
+8 -3
View File
@@ -1,5 +1,10 @@
* text=auto
*.css linguist-vendored
*.scss linguist-vendored
*.js linguist-vendored
*.blade.php diff=html
*.css diff=css
*.html diff=html
*.md diff=markdown
*.php diff=php
/.github export-ignore
CHANGELOG.md export-ignore
+3 -3
View File
@@ -4,7 +4,7 @@ Unless otherwise specified, any issue that does not have an assignee can be clai
Please leave a comment in the issue indicating interest so that you can be designated as the assignee by a member. This is a requirement in Github to allow asssignment of contributors outside of the organization by members.
If further discussion is needed before work can be started, please make use of the `#design` channel on the discord.
If further discussion is needed before work can be started, please make use of the programming channels on the discord.
## Development Environment
@@ -40,7 +40,7 @@ Switch to feature branch.
Make changes in feature branch. Once completed, run tests.
`php artisan config:clear && php artisan test`
`php artisan config:clear && php artisan test --parallel`
Execute static code analysis.
@@ -77,7 +77,7 @@ Create Pull Request from your new feature branch to upstream release branch.
Once the Pull Request is merged, delete the feature branch locally.
`git branch -d new-feature-branch`
`git branch --delete new-feature-branch`
Delete the remote feature branch.
+46
View File
@@ -0,0 +1,46 @@
name: Static Analysis
on: push
jobs:
static-analysis:
runs-on: ubuntu-latest
steps:
- uses: shivammathur/setup-php@v2
with:
php-version: '8.1'
tools: dom, curl, libxml, mbstring, zip, pcntl, pdo, sqlite, pdo_sqlite, phpunit
- uses: actions/checkout@v2
- name: Copy .env
run: php -r "file_exists('.env') || copy('.env.example', '.env');"
- name: Install Dependencies
env:
NOVA_USERNAME: ${{ secrets.NOVA_USERNAME }}
NOVA_PASSWORD: ${{ secrets.NOVA_PASSWORD }}
ENLIGHTN_USERNAME: ${{ secrets.ENLIGHTN_USERNAME }}
ENLIGHTN_PASSWORD: ${{ secrets.ENLIGHTN_PASSWORD }}
run: |
composer config "http-basic.nova.laravel.com" "$NOVA_USERNAME" "$NOVA_PASSWORD"
composer config "http-basic.satis.laravel-enlightn.com" "$ENLIGHTN_USERNAME" "$ENLIGHTN_PASSWORD"
composer install -q --no-ansi --no-interaction --no-scripts --no-progress --prefer-dist
- name: Generate key
run: php artisan key:generate
- name: Install NPM dependencies
run: npm install
- name: Compile assets
run: npm run production
- name: Directory Permissions
run: chmod -R 777 storage bootstrap/cache
- name: Create Database
run: |
mkdir -p database
touch database/database.sqlite
- name: Execute tests (Unit and Feature tests) via PHPUnit
env:
DB_CONNECTION: sqlite
DB_DATABASE: database/database.sqlite
run: |
php artisan optimize:clear
./vendor/bin/phpstan analyse --memory-limit=-1
+46
View File
@@ -0,0 +1,46 @@
name: Tests
on: push
jobs:
test:
runs-on: ubuntu-latest
steps:
- uses: shivammathur/setup-php@v2
with:
php-version: '8.1'
tools: dom, curl, libxml, mbstring, zip, pcntl, pdo, sqlite, pdo_sqlite, phpunit
- uses: actions/checkout@v2
- name: Copy .env
run: php -r "file_exists('.env') || copy('.env.example', '.env');"
- name: Install Dependencies
env:
NOVA_USERNAME: ${{ secrets.NOVA_USERNAME }}
NOVA_PASSWORD: ${{ secrets.NOVA_PASSWORD }}
ENLIGHTN_USERNAME: ${{ secrets.ENLIGHTN_USERNAME }}
ENLIGHTN_PASSWORD: ${{ secrets.ENLIGHTN_PASSWORD }}
run: |
composer config "http-basic.nova.laravel.com" "$NOVA_USERNAME" "$NOVA_PASSWORD"
composer config "http-basic.satis.laravel-enlightn.com" "$ENLIGHTN_USERNAME" "$ENLIGHTN_PASSWORD"
composer install -q --no-ansi --no-interaction --no-scripts --no-progress --prefer-dist
- name: Generate key
run: php artisan key:generate
- name: Install NPM dependencies
run: npm install
- name: Compile assets
run: npm run production
- name: Directory Permissions
run: chmod -R 777 storage bootstrap/cache
- name: Create Database
run: |
mkdir -p database
touch database/database.sqlite
- name: Execute tests (Unit and Feature tests) via PHPUnit
env:
DB_CONNECTION: sqlite
DB_DATABASE: database/database.sqlite
run: |
php artisan optimize:clear
php artisan test
+6 -1
View File
@@ -1,7 +1,6 @@
/node_modules
/public/hot
/public/storage
/public/img/grill
/storage/*.key
/vendor
.env
@@ -12,3 +11,9 @@ Homestead.yaml
npm-debug.log
yarn-error.log
.vscode
.idea
auth.json
*.sqlite
*.sqlite3
public/css/app.css
public/js/app.js
+1 -3
View File
@@ -1,7 +1,5 @@
version: 8.1
preset: laravel
disabled:
- unused_use
finder:
not-name:
- index.php
- server.php
+6 -2
View File
@@ -1,7 +1,9 @@
<p align="center">
<a href="https://github.styleci.io/repos/111264405"><img src="https://github.styleci.io/repos/111264405/shield?branch=main" alt="StyleCI"></a>
<a href="https://github.com/AnimeThemes/animethemes-server/actions/workflows/test.yml"><img src="https://github.com/AnimeThemes/animethemes-server/workflows/Tests/badge.svg?branch=wiki" alt="tests"></a>
<a href="https://github.com/AnimeThemes/animethemes-server/actions/workflows/static-analysis.yml"><img src="https://github.com/AnimeThemes/animethemes-server/workflows/Static%20Analysis/badge.svg?branch=wiki" alt="static-analysis"></a>
<a href="https://github.styleci.io/repos/111264405?branch=wiki"><img src="https://github.styleci.io/repos/111264405/shield?branch=wiki" alt="StyleCI"></a>
<a href="https://discordapp.com/invite/m9zbVyQ"><img src="https://img.shields.io/discord/354388306580078594.svg?label=&logo=discord&logoColor=ffffff&color=7389D8&labelColor=6A7EC2"></a>
<a href="https://github.com/AnimeThemes/animethemes-server/blob/master/LICENSE"><img src="https://img.shields.io/github/license/AnimeThemes/animethemes-server"></a>
<a href="https://github.com/AnimeThemes/animethemes-server/blob/wiki/LICENSE"><img src="https://img.shields.io/github/license/AnimeThemes/animethemes-server"></a>
<a href="https://reddit.com/r/AnimeThemes"><img src="https://img.shields.io/reddit/subreddit-subscribers/AnimeThemes?style=social"></a>
<a href="https://twitter.com/AnimeThemesMoe"><img src="https://img.shields.io/twitter/follow/AnimeThemesMoe?style=social"></a>
</p>
@@ -10,6 +12,8 @@
This is the repository for the server application that is responsible for AnimeThemes.moe resource management, API, and other services related to serving the AnimeThemes database.
This project is powered by [**Laravel**](https://laravel.com/), a PHP framework for web artisans.
## Installation
Please review the [**Installation Guide**](https://github.com/AnimeThemes/animethemes-server/wiki/Installation) in the wiki for detailed instructions.
+46
View File
@@ -0,0 +1,46 @@
<?php
declare(strict_types=1);
namespace App\Actions\Fortify;
use App\Models\Auth\User;
use Illuminate\Database\Eloquent\Model;
use Illuminate\Support\Arr;
use Illuminate\Support\Facades\Hash;
use Illuminate\Support\Facades\Validator;
use Illuminate\Validation\Rule;
use Illuminate\Validation\Rules\Password;
use Illuminate\Validation\ValidationException;
use Laravel\Fortify\Contracts\CreatesNewUsers;
use Laravel\Jetstream\Jetstream;
/**
* Class CreateNewUser.
*/
class CreateNewUser implements CreatesNewUsers
{
/**
* Validate and create a newly registered user.
*
* @param array $input
* @return Model
*
* @throws ValidationException
*/
public function create(array $input): Model
{
Validator::make($input, [
'name' => ['required', 'string', 'max:255'],
'email' => ['required', 'string', 'email', 'max:255', Rule::unique(User::TABLE)],
'password' => Password::required(),
'terms' => Jetstream::hasTermsAndPrivacyPolicyFeature() ? ['required'] : '',
])->validate();
return User::query()->create([
User::ATTRIBUTE_NAME => Arr::get($input, 'name'),
User::ATTRIBUTE_EMAIL => Arr::get($input, 'email'),
User::ATTRIBUTE_PASSWORD => Hash::make(Arr::get($input, 'password')),
]);
}
}
+39
View File
@@ -0,0 +1,39 @@
<?php
declare(strict_types=1);
namespace App\Actions\Fortify;
use App\Models\Auth\User;
use Illuminate\Support\Arr;
use Illuminate\Support\Facades\Hash;
use Illuminate\Support\Facades\Validator;
use Illuminate\Validation\Rules\Password;
use Illuminate\Validation\ValidationException;
use Laravel\Fortify\Contracts\ResetsUserPasswords;
/**
* Class ResetUserPassword.
*/
class ResetUserPassword implements ResetsUserPasswords
{
/**
* Validate and reset the user's forgotten password.
*
* @param mixed $user
* @param array $input
* @return void
*
* @throws ValidationException
*/
public function reset($user, array $input): void
{
Validator::make($input, [
'password' => Password::required(),
])->validate();
$user->forceFill([
User::ATTRIBUTE_PASSWORD => Hash::make(Arr::get($input, 'password')),
])->save();
}
}
@@ -0,0 +1,46 @@
<?php
declare(strict_types=1);
namespace App\Actions\Fortify;
use App\Models\Auth\User;
use Illuminate\Support\Arr;
use Illuminate\Support\Facades\Hash;
use Illuminate\Support\Facades\Validator;
use Illuminate\Validation\Rules\Password;
use Illuminate\Validation\ValidationException;
use Illuminate\Validation\Validator as IlluminateValidator;
use Laravel\Fortify\Contracts\UpdatesUserPasswords;
/**
* Class UpdateUserPassword.
*/
class UpdateUserPassword implements UpdatesUserPasswords
{
/**
* Validate and update the user's password.
*
* @param mixed $user
* @param array $input
* @return void
*
* @throws ValidationException
*/
public function update($user, array $input): void
{
Validator::make($input, [
'current_password' => ['required', 'string'],
'password' => Password::required(),
])->after(function (IlluminateValidator $validator) use ($user, $input) {
if (! isset($input['current_password']) || ! Hash::check($input['current_password'], $user->password)) {
$validator->errors()
->add('current_password', __('The provided password does not match your current password.'));
}
})->validateWithBag('updatePassword');
$user->forceFill([
User::ATTRIBUTE_PASSWORD => Hash::make(Arr::get($input, 'password')),
])->save();
}
}
@@ -0,0 +1,68 @@
<?php
declare(strict_types=1);
namespace App\Actions\Fortify;
use App\Models\Auth\User;
use Illuminate\Contracts\Auth\MustVerifyEmail;
use Illuminate\Support\Arr;
use Illuminate\Support\Facades\Validator;
use Illuminate\Validation\Rule;
use Illuminate\Validation\ValidationException;
use Laravel\Fortify\Contracts\UpdatesUserProfileInformation;
/**
* Class UpdateUserProfileInformation.
*/
class UpdateUserProfileInformation implements UpdatesUserProfileInformation
{
/**
* Validate and update the given user's profile information.
*
* @param mixed $user
* @param array $input
* @return void
*
* @throws ValidationException
*/
public function update(mixed $user, array $input): void
{
Validator::make($input, [
'name' => ['required', 'string', 'max:255'],
'email' => ['required', 'email', 'max:255', Rule::unique(User::TABLE)->ignore($user->id)],
'photo' => ['nullable', 'mimes:jpg,jpeg,png', 'max:1024'],
])->validateWithBag('updateProfileInformation');
if (isset($input['photo'])) {
$user->updateProfilePhoto($input['photo']);
}
if ($input['email'] !== $user->email && $user instanceof MustVerifyEmail) {
$this->updateVerifiedUser($user, $input);
} else {
$user->forceFill([
User::ATTRIBUTE_NAME => Arr::get($input, 'name'),
User::ATTRIBUTE_EMAIL => Arr::get($input, 'email'),
])->save();
}
}
/**
* Update the given verified user's profile information.
*
* @param mixed $user
* @param array $input
* @return void
*/
protected function updateVerifiedUser(mixed $user, array $input): void
{
$user->forceFill([
User::ATTRIBUTE_NAME => Arr::get($input, 'name'),
User::ATTRIBUTE_EMAIL => Arr::get($input, 'email'),
User::ATTRIBUTE_EMAIL_VERIFIED_AT => null,
])->save();
$user->sendEmailVerificationNotification();
}
}
+28
View File
@@ -0,0 +1,28 @@
<?php
declare(strict_types=1);
namespace App\Actions\Jetstream;
use Illuminate\Support\Facades\DB;
use Laravel\Jetstream\Contracts\DeletesUsers;
/**
* Class DeleteUser.
*/
class DeleteUser implements DeletesUsers
{
/**
* Delete the given user.
*
* @param mixed $user
* @return void
*/
public function delete($user): void
{
DB::transaction(function () use ($user) {
$user->tokens->each->delete();
$user->delete();
});
}
}
+26
View File
@@ -0,0 +1,26 @@
<?php
declare(strict_types=1);
namespace App\Concerns;
/**
* Trait DetectsRedis.
*/
trait DetectsRedis
{
/**
* Determine if the application uses any Redis services.
*
* @return bool
*/
protected function appUsesRedis(): bool
{
// We assume here that if the cache, session, broadcasting or queue is powered by Redis,
// then the application is using Redis.
return config('cache.stores.'.config('cache.default').'.driver') === 'redis'
|| config('broadcasting.connections.'.config('broadcasting.default').'.driver') === 'redis'
|| config('session.driver') === 'redis'
|| config('queue.connections.'.config('queue.default').'.driver') === 'redis';
}
}
@@ -0,0 +1,80 @@
<?php
declare(strict_types=1);
namespace App\Concerns\Discord;
use App\Discord\DiscordEmbedField;
use Illuminate\Database\Eloquent\Model;
use Illuminate\Support\Collection;
/**
* Trait HasAttributeUpdateEmbedFields.
*/
trait HasAttributeUpdateEmbedFields
{
use HasDiscordEmbedFields;
/**
* Initialize embed fields with inline attribute changes.
*
* @param Model $model
* @return void
*/
protected function initializeEmbedFields(Model $model): void
{
$original = $model->newInstance($model->getOriginal());
$changedAttributes = $this->getChangedAttributes($model);
$this->addEmbedFields($original, $model, $changedAttributes);
}
/**
* Get changed attributes.
*
* @param Model $model
* @return Collection
*/
protected function getChangedAttributes(Model $model): Collection
{
return collect($model->getChanges())
->forget($model->getCreatedAtColumn())
->forget($model->getUpdatedAtColumn())
->keys();
}
/**
* Get model attribute value.
*
* @param Model $model
* @param mixed $attribute
* @return mixed
*/
protected function getAttributeValue(Model $model, mixed $attribute): mixed
{
// Hide field from embed by obscuring the values
if (in_array($attribute, $model->getHidden())) {
return DiscordEmbedField::DEFAULT_FIELD_VALUE;
}
return $model->getAttribute($attribute);
}
/**
* Add Embed Fields.
*
* @param Model $original
* @param Model $changed
* @param Collection $changedAttributes
* @return void
*/
protected function addEmbedFields(Model $original, Model $changed, Collection $changedAttributes): void
{
foreach ($changedAttributes as $attribute) {
$this->addEmbedField(new DiscordEmbedField('Attribute', $attribute, true));
$this->addEmbedField(new DiscordEmbedField('Old', $this->getAttributeValue($original, $attribute), true));
$this->addEmbedField(new DiscordEmbedField('New', $this->getAttributeValue($changed, $attribute), true));
}
}
}
@@ -0,0 +1,41 @@
<?php
declare(strict_types=1);
namespace App\Concerns\Discord;
use App\Discord\DiscordEmbedField;
/**
* Trait HasDiscordEmbedFields.
*/
trait HasDiscordEmbedFields
{
/**
* The array of embed fields.
*
* @var DiscordEmbedField[]
*/
protected array $embedFields = [];
/**
* Add discord embed field.
*
* @param DiscordEmbedField $embedField
* @return void
*/
protected function addEmbedField(DiscordEmbedField $embedField): void
{
$this->embedFields[] = $embedField;
}
/**
* Get discord embed fields.
*
* @return DiscordEmbedField[]
*/
protected function getEmbedFields(): array
{
return $this->embedFields;
}
}
@@ -0,0 +1,53 @@
<?php
declare(strict_types=1);
namespace App\Concerns\Http\Requests\Api;
use Illuminate\Validation\Validator;
/**
* Trait ValidatesConditionally.
*/
trait ValidatesConditionally
{
/**
* Configure the validator instance.
* Note: This function is invoked by name if it exists.
* We define a decorator that provides better clarity for what conditional validation needs to be applied.
*
* @param Validator $validator
* @return void
*
* @noinspection PhpUnused
*/
public function withValidator(Validator $validator): void
{
$this->handleConditionalValidation($validator);
}
/**
* Configure conditional validation.
*
* @param Validator $validator
* @return void
*/
protected function handleConditionalValidation(Validator $validator): void
{
$this->conditionallyRestrictAllowedFilterValues($validator);
}
/**
* Filters shall be validated based on values.
* If the value contains a separator, we assume this is a multi-value filter that builds a where in clause.
* Otherwise, we assume this is a single-value filter that builds a where clause.
* Logical operators apply to specific clauses, so we must check formatted filter parameters against filter values.
*
* @param Validator $validator
* @return void
*/
protected function conditionallyRestrictAllowedFilterValues(Validator $validator): void
{
//
}
}
@@ -0,0 +1,33 @@
<?php
declare(strict_types=1);
namespace App\Concerns\Http\Requests\Api;
use App\Contracts\Http\Api\Field\FieldInterface;
use App\Contracts\Http\Api\Schema\SchemaInterface;
use App\Http\Api\Parser\FieldParser;
use Illuminate\Support\Arr;
use Illuminate\Support\Str;
/**
* Trait ValidatesFields.
*/
trait ValidatesFields
{
use ValidatesParameters;
/**
* Restrict the allowed values for the schema fields.
*
* @param SchemaInterface $schema
* @return array<string, array>
*/
protected function restrictAllowedFieldValues(SchemaInterface $schema): array
{
return $this->restrictAllowedValues(
Str::of(FieldParser::param())->append('.')->append($schema->type())->__toString(),
Arr::map($schema->fields(), fn (FieldInterface $field) => $field->getKey())
);
}
}
@@ -0,0 +1,140 @@
<?php
declare(strict_types=1);
namespace App\Concerns\Http\Requests\Api;
use App\Contracts\Http\Api\Schema\SchemaInterface;
use App\Enums\Http\Api\Filter\BinaryLogicalOperator;
use App\Enums\Http\Api\Filter\LogicalOperator;
use App\Enums\Http\Api\Filter\UnaryLogicalOperator;
use App\Http\Api\Criteria\Filter\Criteria;
use App\Http\Api\Filter\Filter;
use App\Http\Api\Parser\FilterParser;
use Illuminate\Support\Arr;
use Illuminate\Support\Fluent;
use Illuminate\Support\Str;
use Illuminate\Validation\Validator;
use Spatie\ValidationRules\Rules\Delimited;
/**
* Trait ValidatesFilters.
*/
trait ValidatesFilters
{
use ValidatesParameters;
/**
* Get the list of formatted filters for the schema.
*
* @param SchemaInterface $schema
* @return string[]
*/
protected function getSchemaFormattedFilters(SchemaInterface $schema): array
{
$schemaFilters = [];
foreach ($schema->filters() as $filter) {
$schemaFilters = array_merge(
$schemaFilters,
$this->getFilterFormats($filter, BinaryLogicalOperator::getInstances()),
$this->getFilterFormats($filter, UnaryLogicalOperator::getInstances())
);
}
return array_unique(array_filter($schemaFilters));
}
/**
* Get the allowed list of filter keys with possible conditions.
*
* @param Filter $filter
* @param LogicalOperator[] $logicalOperators
* @return string[]
*/
protected function getFilterFormats(Filter $filter, array $logicalOperators): array
{
$formattedFilters = [];
foreach ($logicalOperators as $binaryLogicalOperator) {
foreach ($filter->getAllowedComparisonOperators() as $allowedComparisonOperator) {
$formattedFilters[] = $filter->format($binaryLogicalOperator, $allowedComparisonOperator);
}
$formattedFilters[] = $filter->format($binaryLogicalOperator);
}
foreach ($filter->getAllowedComparisonOperators() as $allowedComparisonOperator) {
$formattedFilters[] = $filter->format(null, $allowedComparisonOperator);
}
$formattedFilters[] = $filter->format();
return array_unique(array_filter($formattedFilters));
}
/**
* Get possible qualified parameter values for formatted filter.
*
* @param SchemaInterface $schema
* @param string $formattedFilter
* @return string[]
*/
protected function getFormattedParameters(SchemaInterface $schema, string $formattedFilter): array
{
return [
Str::of(FilterParser::param())
->append('.')
->append($formattedFilter)
->__toString(),
Str::of(FilterParser::param())
->append('.')
->append($schema->type())
->append('.')
->append($formattedFilter)
->__toString(),
];
}
/**
* Restrict filter based on allowed formats and provided values.
*
* @param Validator $validator
* @param SchemaInterface $schema
* @param Filter $filter
* @return void
*/
protected function conditionallyRestrictFilter(Validator $validator, SchemaInterface $schema, Filter $filter): void
{
$singleValueFilterFormats = $this->getFilterFormats($filter, BinaryLogicalOperator::getInstances());
foreach ($singleValueFilterFormats as $singleValueFilterFormat) {
foreach ($this->getFormattedParameters($schema, $singleValueFilterFormat) as $formattedParameter) {
if (collect($validator->getRules())->keys()->doesntContain($formattedParameter)) {
$validator->sometimes(
$formattedParameter,
$filter->getRules(),
fn (Fluent $fluent) => is_string(Arr::get($fluent->toArray(), $formattedParameter)) && ! Str::of(Arr::get($fluent->toArray(), $formattedParameter))->contains(Criteria::VALUE_SEPARATOR)
);
}
}
}
$multiValueRules = [];
foreach ($filter->getRules() as $rule) {
$multiValueRules[] = new Delimited($rule);
}
$multiValueFilterFormats = $this->getFilterFormats($filter, UnaryLogicalOperator::getInstances());
foreach ($multiValueFilterFormats as $multiValueFilterFormat) {
foreach ($this->getFormattedParameters($schema, $multiValueFilterFormat) as $formattedParameter) {
if (collect($validator->getRules())->keys()->doesntContain($formattedParameter)) {
$validator->sometimes(
$formattedParameter,
$multiValueRules,
fn (Fluent $fluent) => is_string(Arr::get($fluent->toArray(), $formattedParameter)) && Str::of(Arr::get($fluent->toArray(), $formattedParameter))->contains(Criteria::VALUE_SEPARATOR)
);
}
}
}
}
}
@@ -0,0 +1,32 @@
<?php
declare(strict_types=1);
namespace App\Concerns\Http\Requests\Api;
use App\Contracts\Http\Api\Schema\SchemaInterface;
use App\Http\Api\Include\AllowedInclude;
use Illuminate\Support\Arr;
/**
* Trait ValidatesIncludes.
*/
trait ValidatesIncludes
{
use ValidatesParameters;
/**
* Restrict the allowed values for the schema includes.
*
* @param string $param
* @param SchemaInterface $schema
* @return array<string, array>
*/
protected function restrictAllowedIncludeValues(string $param, SchemaInterface $schema): array
{
return $this->restrictAllowedValues(
$param,
Arr::map($schema->allowedIncludes(), fn (AllowedInclude $include) => $include->path())
);
}
}
@@ -0,0 +1,71 @@
<?php
declare(strict_types=1);
namespace App\Concerns\Http\Requests\Api;
use App\Http\Api\Criteria\Paging\Criteria;
use App\Http\Api\Criteria\Paging\LimitCriteria;
use App\Http\Api\Criteria\Paging\OffsetCriteria;
use App\Http\Api\Parser\PagingParser;
use Illuminate\Support\Str;
/**
* Trait ValidatesPaging.
*/
trait ValidatesPaging
{
use ValidatesParameters;
/**
* Validate offset pagination.
*
* @return array<string, array>
*/
protected function offset(): array
{
return array_merge(
$this->restrictAllowedTypes(PagingParser::param(), [OffsetCriteria::NUMBER_PARAM, OffsetCriteria::SIZE_PARAM]),
$this->min(Str::of(PagingParser::param())->append('.')->append(OffsetCriteria::NUMBER_PARAM)->__toString()),
$this->range(Str::of(PagingParser::param())->append('.')->append(OffsetCriteria::SIZE_PARAM)->__toString()),
);
}
/**
* Validate limit pagination.
*
* @return array<string, array>
*/
protected function limit(): array
{
return array_merge(
$this->restrictAllowedTypes(PagingParser::param(), [LimitCriteria::PARAM]),
$this->range(Str::of(PagingParser::param())->append('.')->append(LimitCriteria::PARAM)->__toString()),
);
}
/**
* Validate minimum value for optional field.
*
* @param string $param
* @param int $min
* @return array<string, array>
*/
protected function min(string $param, int $min = 1): array
{
return $this->optional($param, ['integer', "min:$min"]);
}
/**
* Validate minimum and maximum value for optional field.
*
* @param string $param
* @param int $min
* @param int $max
* @return array<string, array>
*/
protected function range(string $param, int $min = 1, int $max = Criteria::MAX_RESULTS): array
{
return $this->optional($param, ['integer', "min:$min", "max:$max"]);
}
}
@@ -0,0 +1,105 @@
<?php
declare(strict_types=1);
namespace App\Concerns\Http\Requests\Api;
use Illuminate\Contracts\Support\Arrayable;
use Illuminate\Validation\Rule;
use Spatie\ValidationRules\Rules\Delimited;
/**
* Trait ValidatesParameters.
*/
trait ValidatesParameters
{
/**
* Restrict the allowed types for the parameter.
*
* @param string $param
* @param Arrayable<int, string>|string[] $types
* @return array<string, array>
*/
protected function restrictAllowedTypes(string $param, Arrayable|array $types): array
{
if ($types instanceof Arrayable) {
$types = $types->toArray();
}
$types = implode(',', $types);
return [
$param => [
'nullable',
"array:$types",
],
];
}
/**
* Restrict the allowed values for the parameter.
*
* @param string $param
* @param Arrayable<int, string>|string[]|string $values
* @param array $customRules
* @return array<string, array>
*/
protected function restrictAllowedValues(string $param, Arrayable|array|string $values, array $customRules = []): array
{
return [
$param => array_merge(
['bail', 'sometimes', 'required', 'string', new Delimited(Rule::in($values))],
$customRules,
),
];
}
/**
* Prohibit the parameter.
*
* @param string $param
* @return array<string, array>
*/
protected function prohibit(string $param): array
{
return [
$param => [
'prohibited',
],
];
}
/**
* Optional parameter.
*
* @param string $param
* @param array $customRules
* @return array<string, array>
*/
protected function optional(string $param, array $customRules = []): array
{
return [
$param => array_merge(
['sometimes', 'required'],
$customRules,
),
];
}
/**
* Require the parameter.
*
* @param string $param
* @param array $customRules
* @return array<string, array>
*/
protected function require(string $param, array $customRules = []): array
{
return [
$param => array_merge(
['required'],
$customRules,
),
];
}
}
@@ -0,0 +1,53 @@
<?php
declare(strict_types=1);
namespace App\Concerns\Http\Requests\Api;
use App\Contracts\Http\Api\Schema\SchemaInterface;
use App\Enums\Http\Api\Sort\Direction;
use App\Rules\Api\DistinctIgnoringDirectionRule;
use App\Rules\Api\RandomSoleRule;
/**
* Trait ValidatesSorts.
*/
trait ValidatesSorts
{
use ValidatesParameters;
/**
* Get allowed sorts for schema.
*
* @param SchemaInterface $schema
* @return string[]
*/
protected function formatAllowedSortValues(SchemaInterface $schema): array
{
$allowedSorts = [];
foreach ($schema->sorts() as $sort) {
foreach (Direction::getInstances() as $direction) {
$allowedSorts[] = $sort->format($direction);
}
}
return array_unique($allowedSorts);
}
/**
* Restrict allowed sorts for schema.
*
* @param string $param
* @param SchemaInterface $schema
* @return array<string, array>
*/
protected function restrictAllowedSortValues(string $param, SchemaInterface $schema): array
{
return $this->restrictAllowedValues(
$param,
$this->formatAllowedSortValues($schema),
[new DistinctIgnoringDirectionRule(), new RandomSoleRule()]
);
}
}
@@ -0,0 +1,87 @@
<?php
declare(strict_types=1);
namespace App\Concerns\Repositories\Billing;
use App\Concerns\Repositories\ReconcilesRepositories;
use App\Enums\Http\Api\Filter\AllowedDateFormat;
use App\Models\Billing\Balance;
use Closure;
use Illuminate\Database\Eloquent\Model;
use Illuminate\Support\Collection;
/**
* Trait ReconcilesBalanceRepositories.
*/
trait ReconcilesBalanceRepositories
{
use ReconcilesRepositories;
/**
* The columns used for create and delete set operations.
*
* @return string[]
*/
protected function columnsForCreateDelete(): array
{
return [
Balance::ATTRIBUTE_DATE,
Balance::ATTRIBUTE_ID,
Balance::ATTRIBUTE_SERVICE,
];
}
/**
* Callback for create and delete set operation item comparison.
*
* @return Closure
*/
protected function diffCallbackForCreateDelete(): Closure
{
return fn (Balance $first, Balance $second) => $first->date->format(AllowedDateFormat::YM) <=> $second->date->format(AllowedDateFormat::YM);
}
/**
* The columns used for update set operation.
*
* @return string[]
*/
protected function columnsForUpdate(): array
{
return [
Balance::ATTRIBUTE_BALANCE,
Balance::ATTRIBUTE_DATE,
Balance::ATTRIBUTE_ID,
Balance::ATTRIBUTE_SERVICE,
Balance::ATTRIBUTE_USAGE,
];
}
/**
* Callback for update set operation item comparison.
*
* @return Closure
*/
protected function diffCallbackForUpdate(): Closure
{
return fn (Balance $first, Balance $second) => [$first->date->format(AllowedDateFormat::YMD), $first->usage, $first->balance]
<=> [$second->date->format(AllowedDateFormat::YMD), $second->usage, $second->balance];
}
/**
* Get source model that has been updated for destination model.
*
* @param Collection $sourceModels
* @param Model $destinationModel
* @return Model|null
*/
protected function resolveUpdatedModel(Collection $sourceModels, Model $destinationModel): ?Model
{
$formattedDestinationDate = $destinationModel->getAttribute(Balance::ATTRIBUTE_DATE)->format(AllowedDateFormat::YM);
return $sourceModels->first(
fn (Balance $balance) => $balance->date->format(AllowedDateFormat::YM) === $formattedDestinationDate
);
}
}
@@ -0,0 +1,45 @@
<?php
declare(strict_types=1);
namespace App\Concerns\Repositories\Billing;
use App\Concerns\Repositories\ReconcilesRepositories;
use App\Enums\Http\Api\Filter\AllowedDateFormat;
use App\Models\Billing\Transaction;
use Closure;
/**
* Trait ReconcilesTransactionRepositories.
*/
trait ReconcilesTransactionRepositories
{
use ReconcilesRepositories;
/**
* The columns used for create and delete set operations.
*
* @return string[]
*/
protected function columnsForCreateDelete(): array
{
return [
Transaction::ATTRIBUTE_AMOUNT,
Transaction::ATTRIBUTE_DATE,
Transaction::ATTRIBUTE_EXTERNAL_ID,
Transaction::ATTRIBUTE_ID,
Transaction::ATTRIBUTE_SERVICE,
];
}
/**
* Callback for create and delete set operation item comparison.
*
* @return Closure
*/
protected function diffCallbackForCreateDelete(): Closure
{
return fn (Transaction $first, Transaction $second) => [$first->external_id, $first->date->format(AllowedDateFormat::YMD), $first->amount]
<=> [$second->external_id, $second->date->format(AllowedDateFormat::YMD), $second->amount];
}
}
@@ -0,0 +1,341 @@
<?php
declare(strict_types=1);
namespace App\Concerns\Repositories;
use App\Contracts\Repositories\Repository;
use App\Models\BaseModel;
use Closure;
use Exception;
use Illuminate\Database\Eloquent\Model;
use Illuminate\Support\Collection;
/**
* Trait ReconcilesRepositories.
*/
trait ReconcilesRepositories
{
/**
* The number of models created.
*
* @var int
*/
protected int $created = 0;
/**
* The number of models whose creation failed.
*
* @var int
*/
protected int $createdFailed = 0;
/**
* The number of models deleted.
*
* @var int
*/
protected int $deleted = 0;
/**
* The number of models whose deletion failed.
*
* @var int
*/
protected int $deletedFailed = 0;
/**
* The number of models updated.
*
* @var int
*/
protected int $updated = 0;
/**
* The number of models whose update failed.
*
* @var int
*/
protected int $updatedFailed = 0;
/**
* Callback for successful model creation.
*
* @param BaseModel $model
* @return void
*/
protected function handleCreated(BaseModel $model): void
{
//
}
/**
* Callback for failed model creation.
*
* @param BaseModel $model
* @return void
*/
protected function handleFailedCreation(BaseModel $model): void
{
//
}
/**
* Callback for successful model deletion.
*
* @param BaseModel $model
* @return void
*/
protected function handleDeleted(BaseModel $model): void
{
//
}
/**
* Callback for failed model deletion.
*
* @param BaseModel $model
* @return void
*/
protected function handleFailedDeletion(BaseModel $model): void
{
//
}
/**
* Callback for successful model update.
*
* @param BaseModel $model
* @return void
*/
protected function handleUpdated(BaseModel $model): void
{
//
}
/**
* Callback for failed model update.
*
* @param BaseModel $model
* @return void
*/
protected function handleFailedUpdate(BaseModel $model): void
{
//
}
/**
* Callback for exception.
*
* @param Exception $exception
* @return void
*/
protected function handleException(Exception $exception): void
{
//
}
/**
* Callback for handling completion of reconciliation.
*
* @return void
*/
protected function postReconciliationTask(): void
{
//
}
/**
* Determines if any changes, successful or not, were made during reconciliation.
*
* @return bool
*/
protected function hasResults(): bool
{
return $this->hasChanges() || $this->hasFailures();
}
/**
* Determines if any successful changes were made during reconciliation.
*
* @return bool
*/
protected function hasChanges(): bool
{
return $this->created > 0 || $this->deleted > 0 || $this->updated > 0;
}
/**
* Determines if any unsuccessful changes were made during reconciliation.
*
* @return bool
*/
protected function hasFailures(): bool
{
return $this->createdFailed > 0 || $this->deletedFailed > 0 || $this->updatedFailed > 0;
}
/**
* Perform set reconciliation between source and destination repositories.
*
* @param Repository $source
* @param Repository $destination
* @return void
*/
public function reconcileRepositories(Repository $source, Repository $destination): void
{
try {
$sourceModels = $source->get();
$destinationModels = $destination->get($this->columnsForCreateDelete());
$this->createModelsFromSource($destination, $sourceModels, $destinationModels);
$this->deleteModelsFromDestination($destination, $sourceModels, $destinationModels);
$destinationModels = $destination->get($this->columnsForUpdate());
$this->updateDestinationModels($destination, $sourceModels, $destinationModels);
} catch (Exception $exception) {
$this->handleException($exception);
} finally {
$this->postReconciliationTask();
}
}
/**
* The columns used for create and delete set operations.
*
* @return string[]
*/
protected function columnsForCreateDelete(): array
{
return ['*'];
}
/**
* Callback for create and delete set operation item comparison.
*
* @return Closure
*/
protected function diffCallbackForCreateDelete(): Closure
{
return fn () => 0;
}
/**
* Create models that exist in source but not in destination.
*
* @param Repository $destination
* @param Collection $sourceModels
* @param Collection $destinationModels
* @return void
*/
protected function createModelsFromSource(
Repository $destination,
Collection $sourceModels,
Collection $destinationModels
): void {
$createModels = $sourceModels->diffUsing($destinationModels, $this->diffCallbackForCreateDelete());
foreach ($createModels as $createModel) {
$createResult = $destination->save($createModel);
if ($createResult) {
$this->created++;
$this->handleCreated($createModel);
} else {
$this->createdFailed++;
$this->handleFailedCreation($createModel);
}
}
}
/**
* Delete models that exist in destination but not in source.
*
* @param Repository $destination
* @param Collection $sourceModels
* @param Collection $destinationModels
* @return void
*/
protected function deleteModelsFromDestination(
Repository $destination,
Collection $sourceModels,
Collection $destinationModels
): void {
$deleteModels = $destinationModels->diffUsing($sourceModels, $this->diffCallbackForCreateDelete());
foreach ($deleteModels as $deleteModel) {
$deleteResult = $destination->delete($deleteModel);
if ($deleteResult) {
$this->deleted++;
$this->handleDeleted($deleteModel);
} else {
$this->deletedFailed++;
$this->handleFailedDeletion($deleteModel);
}
}
}
/**
* The columns used for update set operation.
*
* @return string[]
*/
protected function columnsForUpdate(): array
{
return ['*'];
}
/**
* Callback for update set operation item comparison.
*
* @return Closure
*/
protected function diffCallbackForUpdate(): Closure
{
return fn () => 0;
}
/**
* Get source model that has been updated for destination model.
*
* @param Collection $sourceModels
* @param Model $destinationModel
* @return Model|null
*/
protected function resolveUpdatedModel(Collection $sourceModels, Model $destinationModel): ?Model
{
return null;
}
/**
* Update destination models that have changed in source.
*
* @param Repository $destination
* @param Collection $sourceModels
* @param Collection $destinationModels
* @return void
*/
protected function updateDestinationModels(
Repository $destination,
Collection $sourceModels,
Collection $destinationModels
): void {
$updatedModels = $destinationModels->diffUsing($sourceModels, $this->diffCallbackForUpdate());
foreach ($updatedModels as $updatedModel) {
$sourceModel = $this->resolveUpdatedModel($sourceModels, $updatedModel);
if ($sourceModel !== null) {
$updateResult = $destination->update($updatedModel, $sourceModel->toArray());
if ($updateResult) {
$this->updated++;
$this->handleUpdated($updatedModel);
} else {
$this->updatedFailed++;
$this->handleFailedUpdate($updatedModel);
}
}
}
}
}
@@ -0,0 +1,82 @@
<?php
declare(strict_types=1);
namespace App\Concerns\Repositories\Wiki;
use App\Concerns\Repositories\ReconcilesRepositories;
use App\Models\Wiki\Video;
use Closure;
use Illuminate\Database\Eloquent\Model;
use Illuminate\Support\Collection;
/**
* Trait ReconcilesVideoRepositories.
*/
trait ReconcilesVideoRepositories
{
use ReconcilesRepositories;
/**
* The columns used for create and delete set operations.
*
* @return string[]
*/
protected function columnsForCreateDelete(): array
{
return [
Video::ATTRIBUTE_BASENAME,
Video::ATTRIBUTE_ID,
];
}
/**
* Callback for create and delete set operation item comparison.
*
* @return Closure
*/
protected function diffCallbackForCreateDelete(): Closure
{
return fn (Video $first, Video $second) => $first->basename <=> $second->basename;
}
/**
* The columns used for update set operation.
*
* @return string[]
*/
protected function columnsForUpdate(): array
{
return [
Video::ATTRIBUTE_BASENAME,
Video::ATTRIBUTE_ID,
Video::ATTRIBUTE_PATH,
Video::ATTRIBUTE_SIZE,
];
}
/**
* Callback for update set operation item comparison.
*
* @return Closure
*/
protected function diffCallbackForUpdate(): Closure
{
return fn (Video $first, Video $second) => [$first->basename, $first->path, $first->size] <=> [$second->basename, $second->path, $second->size];
}
/**
* Get source model that has been updated for destination model.
*
* @param Collection $sourceModels
* @param Model $destinationModel
* @return Model|null
*/
protected function resolveUpdatedModel(Collection $sourceModels, Model $destinationModel): ?Model
{
return $sourceModels->firstWhere(
Video::ATTRIBUTE_BASENAME,
$destinationModel->getAttribute(Video::ATTRIBUTE_BASENAME)
);
}
}
@@ -1,86 +0,0 @@
<?php
namespace App\Console\Commands;
use App\Models\Announcement;
use Illuminate\Console\Command;
use Illuminate\Support\Facades\Log;
class AnnouncementCreateCommand extends Command
{
/**
* The name and signature of the console command.
*
* @var string
*/
protected $signature = 'create:announcement
{--alias= : An internal identifier with which an announcement can be referred}
{--content= : The Announcement Text}';
/**
* The console command description.
*
* @var string
*/
protected $description = 'Create Announcement for Welcome Screen';
/**
* Create a new command instance.
*
* @return void
*/
public function __construct()
{
parent::__construct();
}
/**
* Execute the console command.
*
* @return mixed
*/
public function handle()
{
// Alias is required
$alias = $this->option('alias');
if (empty($alias)) {
LOG::error('alias is required');
$this->error('alias is required');
return;
}
// Content is required
$content = $this->option('content');
if (empty($content)) {
LOG::error('content is required');
$this->error('content is required');
return;
}
// Alias should be unique
$announcement = Announcement::where('alias', $alias)->first();
if ($announcement) {
LOG::error("Announcement '{$alias}' already exists");
$this->error("Announcement '{$alias}' already exists");
return;
}
// Create the Announcement
$result = Announcement::create([
'alias' => $alias,
'content' => $content,
]);
// Confirm if Announcement was created
if ($result->exists()) {
LOG::info("Announcement '{$alias}' created");
$this->info("Announcement '{$alias}' created");
} else {
LOG::error("Announcement '{$alias}' was not created");
$this->error("Announcement '{$alias}' was not created");
}
}
}
@@ -1,73 +0,0 @@
<?php
namespace App\Console\Commands;
use App\Models\Announcement;
use Illuminate\Console\Command;
use Illuminate\Support\Facades\Log;
class AnnouncementDeleteCommand extends Command
{
/**
* The name and signature of the console command.
*
* @var string
*/
protected $signature = 'delete:announcement
{--alias= : An internal identifier to which an announcement can be referred}';
/**
* The console command description.
*
* @var string
*/
protected $description = 'Delete Announcement from Welcome Screen';
/**
* Create a new command instance.
*
* @return void
*/
public function __construct()
{
parent::__construct();
}
/**
* Execute the console command.
*
* @return mixed
*/
public function handle()
{
// Alias is required
$alias = $this->option('alias');
if (empty($alias)) {
LOG::error('alias is required');
$this->error('alias is required');
return;
}
// Announcement must exist to be deleted
$announcement = Announcement::where('alias', $alias)->first();
if (! $announcement) {
LOG::error("Announcement '{$alias}' does not exist");
$this->error("Announcement '{$alias}' does not exist");
return;
}
// Delete the Announcement
$result = $announcement->delete();
// Confirm deletion of announcement
if ($result) {
LOG::info("Announcement '{$alias}' deleted");
$this->info("Announcement '{$alias}' deleted");
} else {
LOG::error("Announcement '{$alias}' was not deleted");
$this->error("Announcement '{$alias}' was not deleted");
}
}
}
@@ -1,48 +0,0 @@
<?php
namespace App\Console\Commands;
use App\Models\Announcement;
use Illuminate\Console\Command;
class AnnouncementListCommand extends Command
{
/**
* The name and signature of the console command.
*
* @var string
*/
protected $signature = 'list:announcement';
/**
* The console command description.
*
* @var string
*/
protected $description = 'List Welcome Screen Announcements';
/**
* Create a new command instance.
*
* @return void
*/
public function __construct()
{
parent::__construct();
}
/**
* Execute the console command.
*
* @return mixed
*/
public function handle()
{
// Display Announcements as table
$headers = ['Alias', 'Content'];
$announcements = Announcement::all(['alias', 'content'])->toArray();
$this->table($headers, $announcements);
}
}
@@ -1,73 +0,0 @@
<?php
namespace App\Console\Commands;
use App\Models\Announcement;
use Illuminate\Console\Command;
use Illuminate\Support\Facades\Log;
class AnnouncementReadCommand extends Command
{
/**
* The name and signature of the console command.
*
* @var string
*/
protected $signature = 'read:announcement
{--alias= : An internal identifier to which an announcement can be referred}';
/**
* The console command description.
*
* @var string
*/
protected $description = 'Read Welcome Screen Announcement by Alias';
/**
* Create a new command instance.
*
* @return void
*/
public function __construct()
{
parent::__construct();
}
/**
* Execute the console command.
*
* @return mixed
*/
public function handle()
{
// Alias is required
$alias = $this->option('alias');
if (empty($alias)) {
LOG::error('alias is required');
$this->error('alias is required');
return;
}
// Announcement must exist to be read
$announcement = Announcement::where('alias', $alias)->first();
if (! $announcement) {
LOG::error("Announcement '{$alias}' does not exist");
$this->error("Announcement '{$alias}' does not exist");
return;
}
// Display Announcement as table
$headers = ['Alias', 'Content'];
$data = [
[
'alias' => $announcement->alias,
'content' => $announcement->content,
],
];
$this->table($headers, $data);
}
}
@@ -1,85 +0,0 @@
<?php
namespace App\Console\Commands;
use App\Models\Announcement;
use Illuminate\Console\Command;
use Illuminate\Support\Facades\Log;
class AnnouncementUpdateCommand extends Command
{
/**
* The name and signature of the console command.
*
* @var string
*/
protected $signature = 'update:announcement
{--alias= : An internal identifier to which an announcement can be referred}
{--content= : The Announcement Text}';
/**
* The console command description.
*
* @var string
*/
protected $description = 'Update Announcement for Welcome Screen';
/**
* Create a new command instance.
*
* @return void
*/
public function __construct()
{
parent::__construct();
}
/**
* Execute the console command.
*
* @return mixed
*/
public function handle()
{
// Alias is required
$alias = $this->option('alias');
if (empty($alias)) {
LOG::error('alias is required');
$this->error('alias is required');
return;
}
// Content is not required
$content = $this->option('content');
// Announcement must exist to be updated
$announcement = Announcement::where('alias', $alias)->first();
if (! $announcement) {
LOG::error("Announcement '{$alias}' does not exist");
$this->error("Announcement '{$alias}' does not exist");
return;
}
// Update Content if set
if (! empty($content)) {
$announcement->content = $content;
}
// Save changes if needed
$result = false;
if ($announcement->isDirty()) {
$result = $announcement->save();
}
// Confirm if changes were made
if ($result) {
LOG::info("Announcement '{$alias}' updated");
$this->info("Announcement '{$alias}' updated");
} else {
LOG::error("Announcement '{$alias}' was not updated");
$this->error("Announcement '{$alias}' was not updated");
}
}
}
@@ -0,0 +1,172 @@
<?php
declare(strict_types=1);
namespace App\Console\Commands\Billing\Balance;
use App\Concerns\Repositories\Billing\ReconcilesBalanceRepositories;
use App\Console\Commands\Billing\ServiceReconcileCommand;
use App\Contracts\Repositories\Repository;
use App\Enums\Models\Billing\Service;
use App\Models\BaseModel;
use App\Repositories\Eloquent\Billing\DigitalOceanBalanceRepository as DigitalOceanDestinationRepository;
use App\Repositories\Service\DigitalOcean\Billing\DigitalOceanBalanceRepository as DigitalOceanSourceRepository;
use Exception;
use Illuminate\Support\Facades\App;
use Illuminate\Support\Facades\Log;
/**
* Class BalanceReconcileCommand.
*/
class BalanceReconcileCommand extends ServiceReconcileCommand
{
use ReconcilesBalanceRepositories;
/**
* The name and signature of the console command.
*
* @var string
*/
protected $signature = 'reconcile:balance {service}';
/**
* The console command description.
*
* @var string
*/
protected $description = 'Perform set reconciliation between vendor billing API and balance database';
/**
* Print the result to console and log the results to the app log.
*
* @return void
*/
protected function postReconciliationTask(): void
{
if ($this->hasResults()) {
if ($this->hasChanges()) {
Log::info("$this->created Balances created, $this->deleted Balances deleted, $this->updated Balances updated");
$this->info("$this->created Balances created, $this->deleted Balances deleted, $this->updated Balances updated");
}
if ($this->hasFailures()) {
Log::error("Failed to create $this->createdFailed Balances, delete $this->deletedFailed Balances, update $this->updatedFailed Balances");
$this->error("Failed to create $this->createdFailed Balances, delete $this->deletedFailed Balances, update $this->updatedFailed Balances");
}
} else {
Log::info('No Balances created or deleted or updated');
$this->info('No Balances created or deleted or updated');
}
}
/**
* Handler for successful balance creation.
*
* @param BaseModel $model
* @return void
*/
protected function handleCreated(BaseModel $model): void
{
Log::info("Balance '{$model->getName()}' created");
$this->info("Balance '{$model->getName()}' created");
}
/**
* Handler for failed balance creation.
*
* @param BaseModel $model
* @return void
*/
protected function handleFailedCreation(BaseModel $model): void
{
Log::error("Balance '{$model->getName()}' was not created");
$this->error("Balance '{$model->getName()}' was not created");
}
/**
* Handler for successful balance deletion.
*
* @param BaseModel $model
* @return void
*/
protected function handleDeleted(BaseModel $model): void
{
Log::info("Balance '{$model->getName()}' deleted");
$this->info("Balance '{$model->getName()}' deleted");
}
/**
* Handler for failed balance deletion.
*
* @param BaseModel $model
* @return void
*/
protected function handleFailedDeletion(BaseModel $model): void
{
Log::error("Balance '{$model->getName()}' was not deleted");
$this->error("Balance '{$model->getName()}' was not deleted");
}
/**
* Handler for successful balance update.
*
* @param BaseModel $model
* @return void
*/
protected function handleUpdated(BaseModel $model): void
{
Log::info("Balance '{$model->getName()}' updated");
$this->info("Balance '{$model->getName()}' updated");
}
/**
* Handler for failed balance update.
*
* @param BaseModel $model
* @return void
*/
protected function handleFailedUpdate(BaseModel $model): void
{
Log::error("Balance '{$model->getName()}' was not updated");
$this->error("Balance '{$model->getName()}' was not updated");
}
/**
* Handler for exception.
*
* @param Exception $exception
* @return void
*/
protected function handleException(Exception $exception): void
{
Log::error($exception->getMessage());
$this->error($exception->getMessage());
}
/**
* Get source repository for service.
*
* @param Service $service
* @return Repository|null
*/
protected function getSourceRepository(Service $service): ?Repository
{
return match ($service->value) {
Service::DIGITALOCEAN => App::make(DigitalOceanSourceRepository::class),
default => null,
};
}
/**
* Get destination repository for service.
*
* @param Service $service
* @return Repository|null
*/
protected function getDestinationRepository(Service $service): ?Repository
{
return match ($service->value) {
Service::DIGITALOCEAN => App::make(DigitalOceanDestinationRepository::class),
default => null,
};
}
}
@@ -0,0 +1,80 @@
<?php
declare(strict_types=1);
namespace App\Console\Commands\Billing;
use App\Contracts\Repositories\Repository;
use App\Enums\Models\Billing\Service;
use Illuminate\Console\Command;
use Illuminate\Support\Facades\Log;
use Illuminate\Support\Str;
/**
* Class ServiceReconcileCommand.
*/
abstract class ServiceReconcileCommand extends Command
{
/**
* Execute the console command.
*
* @return int
*/
public function handle(): int
{
$key = $this->argument('service');
$service = Service::coerce(Str::upper($key));
if ($service === null) {
Log::error("Invalid Service '$key'");
$this->error("Invalid Service '$key'");
return 1;
}
$sourceRepository = $this->getSourceRepository($service);
if ($sourceRepository === null) {
Log::error("No source repository implemented for Service '$key'");
$this->error("No source repository implemented for Service '$key'");
return 1;
}
$destinationRepository = $this->getDestinationRepository($service);
if ($destinationRepository === null) {
Log::error("No destination repository implemented for Service '$key'");
$this->error("No destination repository implemented for Service '$key'");
return 1;
}
$this->reconcileRepositories($sourceRepository, $destinationRepository);
return 0;
}
/**
* Perform set reconciliation between source and destination repositories.
*
* @param Repository $source
* @param Repository $destination
* @return void
*/
abstract public function reconcileRepositories(Repository $source, Repository $destination): void;
/**
* Get source repository for service.
*
* @param Service $service
* @return Repository|null
*/
abstract protected function getSourceRepository(Service $service): ?Repository;
/**
* Get destination repository for service.
*
* @param Service $service
* @return Repository|null
*/
abstract protected function getDestinationRepository(Service $service): ?Repository;
}
@@ -0,0 +1,172 @@
<?php
declare(strict_types=1);
namespace App\Console\Commands\Billing\Transaction;
use App\Concerns\Repositories\Billing\ReconcilesTransactionRepositories;
use App\Console\Commands\Billing\ServiceReconcileCommand;
use App\Contracts\Repositories\Repository;
use App\Enums\Models\Billing\Service;
use App\Models\Billing\Transaction;
use App\Repositories\Eloquent\Billing\DigitalOceanTransactionRepository as DigitalOceanDestinationRepository;
use App\Repositories\Service\DigitalOcean\Billing\DigitalOceanTransactionRepository as DigitalOceanSourceRepository;
use Exception;
use Illuminate\Support\Facades\App;
use Illuminate\Support\Facades\Log;
/**
* Class TransactionReconcileCommand.
*/
class TransactionReconcileCommand extends ServiceReconcileCommand
{
use ReconcilesTransactionRepositories;
/**
* The name and signature of the console command.
*
* @var string
*/
protected $signature = 'reconcile:transaction {service}';
/**
* The console command description.
*
* @var string
*/
protected $description = 'Perform set reconciliation between vendor billing API and transaction database';
/**
* Print the result to console and log the results to the app log.
*
* @return void
*/
protected function postReconciliationTask(): void
{
if ($this->hasResults()) {
if ($this->hasChanges()) {
Log::info("$this->created Transactions created, $this->deleted Transactions deleted, $this->updated Transactions updated");
$this->info("$this->created Transactions created, $this->deleted Transactions deleted, $this->updated Transactions updated");
}
if ($this->hasFailures()) {
Log::error("Failed to create $this->createdFailed Transactions, delete $this->deletedFailed Transactions, update $this->updatedFailed Transactions");
$this->error("Failed to create $this->createdFailed Transactions, delete $this->deletedFailed Transactions, update $this->updatedFailed Transactions");
}
} else {
Log::info('No Transactions created or deleted or updated');
$this->info('No Transactions created or deleted or updated');
}
}
/**
* Handler for successful transaction creation.
*
* @param Transaction $model
* @return void
*/
protected function handleCreated(Transaction $model): void
{
Log::info("Transaction '{$model->getName()}' created");
$this->info("Transaction '{$model->getName()}' created");
}
/**
* Handler for failed transaction creation.
*
* @param Transaction $model
* @return void
*/
protected function handleFailedCreation(Transaction $model): void
{
Log::error("Transaction '{$model->getName()}' was not created");
$this->error("Transaction '{$model->getName()}' was not created");
}
/**
* Handler for successful transaction deletion.
*
* @param Transaction $model
* @return void
*/
protected function handleDeleted(Transaction $model): void
{
Log::info("Transaction '{$model->getName()}' deleted");
$this->info("Transaction '{$model->getName()}' deleted");
}
/**
* Handler for failed transaction deletion.
*
* @param Transaction $model
* @return void
*/
protected function handleFailedDeletion(Transaction $model): void
{
Log::error("Transaction '{$model->getName()}' was not deleted");
$this->error("Transaction '{$model->getName()}' was not deleted");
}
/**
* Handler for successful transaction update.
*
* @param Transaction $model
* @return void
*/
protected function handleUpdated(Transaction $model): void
{
Log::info("Transaction '{$model->getName()}' updated");
$this->info("Transaction '{$model->getName()}' updated");
}
/**
* Handler for failed transaction update.
*
* @param Transaction $model
* @return void
*/
protected function handleFailedUpdate(Transaction $model): void
{
Log::error("Transaction '{$model->getName()}' was not updated");
$this->error("Transaction '{$model->getName()}' was not updated");
}
/**
* Handler for exception.
*
* @param Exception $exception
* @return void
*/
protected function handleException(Exception $exception): void
{
Log::error($exception->getMessage());
$this->error($exception->getMessage());
}
/**
* Get source repository for service.
*
* @param Service $service
* @return Repository|null
*/
protected function getSourceRepository(Service $service): ?Repository
{
return match ($service->value) {
Service::DIGITALOCEAN => App::make(DigitalOceanSourceRepository::class),
default => null,
};
}
/**
* Get destination repository for service.
*
* @param Service $service
* @return Repository|null
*/
protected function getDestinationRepository(Service $service): ?Repository
{
return match ($service->value) {
Service::DIGITALOCEAN => App::make(DigitalOceanDestinationRepository::class),
default => null,
};
}
}
@@ -0,0 +1,157 @@
<?php
declare(strict_types=1);
namespace App\Console\Commands;
use Exception;
use Illuminate\Console\Command;
use Illuminate\Database\ConnectionInterface;
use Illuminate\Database\MySqlConnection;
use Illuminate\Database\PostgresConnection;
use Illuminate\Database\SQLiteConnection;
use Illuminate\Support\Facades\Date;
use Illuminate\Support\Facades\DB;
use Illuminate\Support\Facades\Log;
use Illuminate\Support\Facades\Storage;
use Illuminate\Support\Str;
use PDO;
use Spatie\DbDumper\Databases\MySql;
use Spatie\DbDumper\Databases\PostgreSql;
use Spatie\DbDumper\Databases\Sqlite;
use Spatie\DbDumper\DbDumper;
/**
* Class DatabaseDumpCommand.
*/
abstract class DatabaseDumpCommand extends Command
{
/**
* Execute the console command.
*
* @return int
*/
public function handle(): int
{
$create = $this->option('create');
try {
$connection = DB::connection();
$dumper = $this->getDumper($connection, $create);
if ($dumper === null) {
Log::error("Unrecognized connection '{$connection->getName()}'");
$this->error("Unrecognized connection '{$connection->getName()}'");
return 1;
}
if (! static::canIncludeTables($connection)) {
Log::error('DB connection does not support includeTables option');
$this->error('DB connection does not support includeTables option');
return 1;
}
$dumper->includeTables($this->allowedTables());
$dumper->setDbName($connection->getDatabaseName())
->setUserName(strval($connection->getConfig('username')))
->setPassword(strval($connection->getConfig('password')));
$host = $connection->getConfig('host');
if ($host !== null) {
$dumper->setHost(collect($host)->first());
}
$port = $connection->getConfig('port');
if (is_int($port)) {
$dumper->setPort($port);
}
$dumpFile = $this->getDumpFile($create);
$dumper->dumpToFile($dumpFile);
// Assume success if no exceptions were thrown
// The library will check if the file exists and is not empty
Log::info("Database dump '$dumpFile' has been created");
$this->info("Database dump '$dumpFile' has been created");
} catch (Exception $e) {
Log::error($e->getMessage());
$this->error($e->getMessage());
return 1;
}
return 0;
}
/**
* Get the dumper for the database connection.
*
* @param ConnectionInterface $connection
* @param bool $create
* @return DbDumper|null
*/
protected function getDumper(ConnectionInterface $connection, bool $create): ?DbDumper
{
return match (get_class($connection)) {
SQLiteConnection::class => Sqlite::create(),
MySqlConnection::class => $create ? MySql::create() : MySql::create()->doNotCreateTables(),
PostgresConnection::class => $create ? PostgreSql::create() : PostgreSql::create()->doNotCreateTables(),
default => null,
};
}
/**
* Determine if the database connection supports table inclusion.
*
* @param ConnectionInterface $connection
* @return bool
*/
public static function canIncludeTables(ConnectionInterface $connection): bool
{
// Sqlite version 3.32.0 is required when using the includeTables option
if ($connection instanceof SQLiteConnection) {
return version_compare($connection->getPdo()->getAttribute(PDO::ATTR_SERVER_VERSION), '3.32.0', '>=');
}
return true;
}
/**
* The target path for the database dump.
* Pattern: "/path/to/project/storage/db-dumps/{path}/animethemes-db-dump-{create?}-{year}-{month}-{day}.sql".
*
* @param bool $create
* @return string
*/
public function getDumpFile(bool $create = false): string
{
$filesystem = Storage::disk('db-dumps');
$filesystem->makeDirectory($this->getDumpFilePath());
return Str::of($filesystem->path($this->getDumpFilePath()))
->append(DIRECTORY_SEPARATOR)
->append('animethemes-db-dump-')
->append($create ? 'create-' : '')
->append(Date::now()->toDateString())
->append('.sql')
->__toString();
}
/**
* The directory that the file should be dumped to.
*
* @return string
*/
abstract protected function getDumpFilePath(): string;
/**
* The list of tables to include in the dump.
*
* @return array
*/
abstract protected function allowedTables(): array;
}
@@ -0,0 +1,50 @@
<?php
declare(strict_types=1);
namespace App\Console\Commands\Document;
use App\Console\Commands\DatabaseDumpCommand;
use App\Models\Document\Page;
/**
* Class DocumentDatabaseDumpCommand.
*/
class DocumentDatabaseDumpCommand extends DatabaseDumpCommand
{
/**
* The name and signature of the console command.
*
* @var string
*/
protected $signature = 'db:dump-document {--C|create : Whether the dumper should include create table statements}';
/**
* The console command description.
*
* @var string
*/
protected $description = 'Produces sanitized database dump, targeting document-related tables for seeding purposes';
/**
* The list of tables to include in the dump.
*
* @return array
*/
protected function allowedTables(): array
{
return [
Page::TABLE,
];
}
/**
* The directory that the file should be dumped to.
*
* @return string
*/
protected function getDumpFilePath(): string
{
return 'document';
}
}
@@ -0,0 +1,150 @@
<?php
declare(strict_types=1);
namespace App\Console\Commands;
use Carbon\Carbon;
use Illuminate\Console\Command;
use Illuminate\Contracts\Filesystem\Filesystem;
use Illuminate\Support\Facades\Date;
use Illuminate\Support\Facades\Log;
use Illuminate\Support\Facades\Storage;
use Illuminate\Support\Str;
/**
* Class PruneDatabaseDumpsCommand.
*/
class PruneDatabaseDumpsCommand extends Command
{
/**
* The name and signature of the console command.
*
* @var string
*/
protected $signature = 'db:prune-dumps {--H|hours=72 : The number of hours to retain sanitized database dumps}';
/**
* The console command description.
*
* @var string
*/
protected $description = 'Prune stale database dumps from local storage';
/**
* The number of dumps deleted.
*
* @var int
*/
protected int $deleted = 0;
/**
* The number of dumps whose deletion failed.
*
* @var int
*/
protected int $deletedFailed = 0;
/**
* Execute the console command.
*
* @return int
*/
public function handle(): int
{
$hours = $this->option('hours');
if (! is_numeric($hours)) {
Log::error("Invalid hours value '$hours'");
$this->error("Invalid hours value '$hours'");
return 1;
}
$this->prune(
Storage::disk('db-dumps'),
Date::now()->subHours(intval($hours))
);
$this->printResults();
return 0;
}
/**
* Prune database dumps in filesystem against date.
*
* @param Filesystem $filesystem
* @param Carbon $pruneDate
* @return void
*/
protected function prune(Filesystem $filesystem, Carbon $pruneDate): void
{
foreach ($filesystem->allFiles() as $path) {
$lastModified = Date::createFromTimestamp($filesystem->lastModified($path));
if (Str::contains($path, 'animethemes-db-dump') && $lastModified->isBefore($pruneDate)) {
$result = $filesystem->delete($path);
if ($result) {
$this->deleted++;
Log::info("Deleted database dump '$path'");
$this->info("Deleted database dump '$path'");
} else {
$this->deletedFailed++;
Log::error("Failed to delete database dump '$path'");
$this->error("Failed to delete database dump '$path'");
}
}
}
}
/**
* Print results to console and logs.
*
* @return void
*/
protected function printResults(): void
{
if ($this->hasResults()) {
if ($this->hasDeletions()) {
Log::info("$this->deleted database dumps deleted");
$this->info("$this->deleted database dumps deleted");
}
if ($this->hasFailures()) {
Log::error("Failed to delete $this->deletedFailed database dumps");
$this->error("Failed to delete $this->deletedFailed database dumps");
}
} else {
Log::info('No database dumps deleted');
$this->info('No database dumps deleted');
}
}
/**
* Determines if any deletions, successful or not, were made during pruning.
*
* @return bool
*/
protected function hasResults(): bool
{
return $this->hasDeletions() || $this->hasFailures();
}
/**
* Determines if any successful deletions were made during pruning.
*
* @return bool
*/
protected function hasDeletions(): bool
{
return $this->deleted > 0;
}
/**
* Determines if any unsuccessful deletions were attempted during pruning.
*
* @return bool
*/
protected function hasFailures(): bool
{
return $this->deletedFailed > 0;
}
}
@@ -1,156 +0,0 @@
<?php
namespace App\Console\Commands;
use App\Models\Video;
use Aws\S3\Exception\S3Exception;
use Illuminate\Console\Command;
use Illuminate\Support\Facades\Log;
use Illuminate\Support\Facades\Storage;
class VideoReconcileCommand extends Command
{
// Result Counts
public $created = 0;
public $created_failed = 0;
public $deleted = 0;
public $deleted_failed = 0;
/**
* The name and signature of the console command.
*
* @var string
*/
protected $signature = 'reconcile:video';
/**
* The console command description.
*
* @var string
*/
protected $description = 'Perform set reconcile between object storage and video database';
/**
* Create a new command instance.
*
* @return void
*/
public function __construct()
{
parent::__construct();
}
/**
* Execute the console command.
*
* @return mixed
*/
public function handle()
{
try {
// Get metadata for all objects in storage
$fs = Storage::disk('spaces');
$files = $fs->listContents('', true);
// Filter all objects for WebM metadata
// We don't want to filter on the remote filesystem for performance concerns
$files = array_filter($files, function ($fs_file) {
return $fs_file['type'] === 'file' && $fs_file['extension'] === 'webm';
});
// Create videos from metadata that we can later save if needed
$fs_videos = array_map(function ($file) {
$fs_video = new Video;
$fs_video->fill($file);
return $fs_video;
}, $files);
// Existing videos as array
$db_videos = Video::all()->all();
// Create videos that exist in storage but not in the database
$create_videos = array_udiff($fs_videos, $db_videos, [VideoReconcileCommand::class, 'compareVideos']);
foreach ($create_videos as $create_video) {
$create_result = $create_video->save();
if ($create_result) {
$this->created++;
Log::info("Video '{$create_video->basename}' created");
$this->info("Video '{$create_video->basename}' created");
} else {
$this->created_failed++;
Log::error("Video '{$create_video->basename}' was not created");
$this->error("Video '{$create_video->basename}' was not created");
}
}
// Delete videos that no longer exist in storage
$delete_videos = array_udiff($db_videos, $fs_videos, [VideoReconcileCommand::class, 'compareVideos']);
foreach ($delete_videos as $delete_video) {
$delete_result = $delete_video->delete();
if ($delete_result) {
$this->deleted++;
Log::info("Video '{$delete_video->basename}' deleted");
$this->info("Video '{$delete_video->basename}' deleted");
} else {
$this->deleted_failed++;
Log::error("Video '{$delete_video->basename}' was not deleted");
$this->error("Video '{$delete_video->basename}' was not deleted");
}
}
} catch (S3Exception $exception) {
Log::error($exception);
$this->error($exception->getMessage());
} finally {
// Output reconcilation results
$this->printResults();
}
}
// Callback for video comparison in set operation
public static function compareVideos($a, $b)
{
return strcmp(VideoReconcileCommand::reconciliationString($a), VideoReconcileCommand::reconciliationString($b));
}
// Represent video with attributes that correspond to WebM metadata
// For reconciliation purposes, other attributes such as ID and timestamps do not apply
public static function reconciliationString($video)
{
return "basename:{$video->basename},filename:{$video->filename},path:{$video->path},size:{$video->size}";
}
// Reconciliation Results
public function hasResults()
{
return $this->hasChanges() || $this->hasFailures();
}
public function hasChanges()
{
return $this->created > 0 || $this->deleted > 0;
}
public function hasFailures()
{
return $this->created_failed > 0 || $this->deleted_failed > 0;
}
public function printResults()
{
if ($this->hasResults()) {
if ($this->hasChanges()) {
Log::info("{$this->created} Videos created, {$this->deleted} Videos deleted");
$this->info("{$this->created} Videos created, {$this->deleted} Videos deleted");
}
if ($this->hasFailures()) {
Log::error("Failed to create {$this->created_failed} Videos, delete {$this->deleted_failed} Videos");
$this->error("Failed to create {$this->created_failed} Videos, delete {$this->deleted_failed} Videos");
}
} else {
Log::info('No Videos created or deleted');
$this->info('No Videos created or deleted');
}
}
}
@@ -0,0 +1,171 @@
<?php
declare(strict_types=1);
namespace App\Console\Commands\Wiki\Video;
use App\Concerns\Repositories\Wiki\ReconcilesVideoRepositories;
use App\Models\BaseModel;
use App\Repositories\Eloquent\Wiki\VideoRepository as VideoDestinationRepository;
use App\Repositories\Service\DigitalOcean\VideoRepository as VideoSourceRepository;
use Exception;
use Illuminate\Console\Command;
use Illuminate\Support\Facades\App;
use Illuminate\Support\Facades\Log;
/**
* Class VideoReconcileCommand.
*/
class VideoReconcileCommand extends Command
{
use ReconcilesVideoRepositories;
/**
* The name and signature of the console command.
*
* @var string
*/
protected $signature = 'reconcile:video
{--path= : The directory of videos to reconcile. Ex: 2022/Spring/. If unspecified, all directories will be listed.}';
/**
* The console command description.
*
* @var string
*/
protected $description = 'Perform set reconciliation between object storage and video database';
/**
* Execute the console command.
*
* @return int
*/
public function handle(): int
{
$sourceRepository = App::make(VideoSourceRepository::class);
$destinationRepository = App::make(VideoDestinationRepository::class);
$path = $this->option('path');
if ($path !== null) {
if (! $sourceRepository->validateFilter('path', $path) || ! $destinationRepository->validateFilter('path', $path)) {
$this->error("Invalid path '$path'");
return 1;
}
$sourceRepository->handleFilter('path', $path);
$destinationRepository->handleFilter('path', $path);
}
$this->reconcileRepositories($sourceRepository, $destinationRepository);
return 0;
}
/**
* Print the result to console and log the results to the app log.
*
* @return void
*/
protected function postReconciliationTask(): void
{
if ($this->hasResults()) {
if ($this->hasChanges()) {
Log::info("$this->created Videos created, $this->deleted Videos deleted, $this->updated Videos updated");
$this->info("$this->created Videos created, $this->deleted Videos deleted, $this->updated Videos updated");
}
if ($this->hasFailures()) {
Log::error("Failed to create $this->createdFailed Videos, delete $this->deletedFailed Videos, update $this->updatedFailed Videos");
$this->error("Failed to create $this->createdFailed Videos, delete $this->deletedFailed Videos, update $this->updatedFailed Videos");
}
} else {
Log::info('No Videos created or deleted or updated');
$this->info('No Videos created or deleted or updated');
}
}
/**
* Handler for successful video creation.
*
* @param BaseModel $model
* @return void
*/
protected function handleCreated(BaseModel $model): void
{
Log::info("Video '{$model->getName()}' created");
$this->info("Video '{$model->getName()}' created");
}
/**
* Handler for failed video creation.
*
* @param BaseModel $model
* @return void
*/
protected function handleFailedCreation(BaseModel $model): void
{
Log::error("Video '{$model->getName()}' was not created");
$this->error("Video '{$model->getName()}' was not created");
}
/**
* Handler for successful video deletion.
*
* @param BaseModel $model
* @return void
*/
protected function handleDeleted(BaseModel $model): void
{
Log::info("Video '{$model->getName()}' deleted");
$this->info("Video '{$model->getName()}' deleted");
}
/**
* Handler for failed video deletion.
*
* @param BaseModel $model
* @return void
*/
protected function handleFailedDeletion(BaseModel $model): void
{
Log::error("Video '{$model->getName()}' was not deleted");
$this->error("Video '{$model->getName()}' was not deleted");
}
/**
* Handler for successful video update.
*
* @param BaseModel $model
* @return void
*/
protected function handleUpdated(BaseModel $model): void
{
Log::info("Video '{$model->getName()}' updated");
$this->info("Video '{$model->getName()}' updated");
}
/**
* Handler for failed video update.
*
* @param BaseModel $model
* @return void
*/
protected function handleFailedUpdate(BaseModel $model): void
{
Log::error("Video '{$model->getName()}' was not updated");
$this->error("Video '{$model->getName()}' was not updated");
}
/**
* Handler for exception.
*
* @param Exception $exception
* @return void
*/
protected function handleException(Exception $exception): void
{
Log::error($exception->getMessage());
$this->error($exception->getMessage());
}
}
@@ -0,0 +1,90 @@
<?php
declare(strict_types=1);
namespace App\Console\Commands\Wiki;
use App\Console\Commands\DatabaseDumpCommand;
use App\Models\Wiki\Anime;
use App\Models\Wiki\Anime\AnimeSynonym;
use App\Models\Wiki\Anime\AnimeTheme;
use App\Models\Wiki\Anime\Theme\AnimeThemeEntry;
use App\Models\Wiki\Artist;
use App\Models\Wiki\ExternalResource;
use App\Models\Wiki\Image;
use App\Models\Wiki\Series;
use App\Models\Wiki\Song;
use App\Models\Wiki\Studio;
use App\Models\Wiki\Video;
use App\Pivots\AnimeImage;
use App\Pivots\AnimeResource;
use App\Pivots\AnimeSeries;
use App\Pivots\AnimeStudio;
use App\Pivots\AnimeThemeEntryVideo;
use App\Pivots\ArtistImage;
use App\Pivots\ArtistMember;
use App\Pivots\ArtistResource;
use App\Pivots\ArtistSong;
use App\Pivots\StudioResource;
/**
* Class WikiDatabaseDumpCommand.
*/
class WikiDatabaseDumpCommand extends DatabaseDumpCommand
{
/**
* The name and signature of the console command.
*
* @var string
*/
protected $signature = 'db:dump-wiki {--C|create : Whether the dumper should include create table statements}';
/**
* The console command description.
*
* @var string
*/
protected $description = 'Produces sanitized database dump, targeting wiki-related tables for seeding purposes';
/**
* The list of tables to include in the dump.
*
* @return array
*/
protected function allowedTables(): array
{
return [
Anime::TABLE,
AnimeImage::TABLE,
AnimeResource::TABLE,
AnimeSeries::TABLE,
AnimeStudio::TABLE,
AnimeSynonym::TABLE,
AnimeTheme::TABLE,
AnimeThemeEntry::TABLE,
AnimeThemeEntryVideo::TABLE,
Artist::TABLE,
ArtistImage::TABLE,
ArtistMember::TABLE,
ArtistResource::TABLE,
ArtistSong::TABLE,
ExternalResource::TABLE,
Image::TABLE,
Series::TABLE,
Song::TABLE,
Studio::TABLE,
StudioResource::TABLE,
Video::TABLE,
];
}
/**
* The directory that the file should be dumped to.
*
* @return string
*/
protected function getDumpFilePath(): string
{
return 'wiki';
}
}
+89 -19
View File
@@ -1,44 +1,114 @@
<?php
declare(strict_types=1);
namespace App\Console;
use App\Console\Commands\VideoReconcileCommand;
use App\Console\Commands\Billing\Balance\BalanceReconcileCommand;
use App\Console\Commands\Billing\Transaction\TransactionReconcileCommand;
use App\Console\Commands\Document\DocumentDatabaseDumpCommand;
use App\Console\Commands\PruneDatabaseDumpsCommand;
use App\Console\Commands\Wiki\WikiDatabaseDumpCommand;
use App\Enums\Models\Billing\Service;
use App\Models\BaseModel;
use Illuminate\Console\Scheduling\Schedule;
use Illuminate\Database\Console\PruneCommand as PruneModelsCommand;
use Illuminate\Foundation\Console\Kernel as ConsoleKernel;
use Illuminate\Queue\Console\PruneFailedJobsCommand;
use Laravel\Horizon\Console\SnapshotCommand;
use Laravel\Sanctum\Console\Commands\PruneExpired;
use Laravel\Telescope\Console\PruneCommand as PruneTelescopeEntriesCommand;
/**
* Class Kernel.
*/
class Kernel extends ConsoleKernel
{
/**
* The Artisan commands provided by your application.
*
* @var array
*/
protected $commands = [
\App\Console\Commands\AnnouncementCreateCommand::class,
\App\Console\Commands\AnnouncementReadCommand::class,
\App\Console\Commands\AnnouncementUpdateCommand::class,
\App\Console\Commands\AnnouncementDeleteCommand::class,
\App\Console\Commands\AnnouncementListCommand::class,
\App\Console\Commands\VideoReconcileCommand::class,
];
/**
* Define the application's command schedule.
*
* @param \Illuminate\Console\Scheduling\Schedule $schedule
* @param Schedule $schedule
* @return void
*
* @noinspection PhpMissingParentCallCommonInspection
*/
protected function schedule(Schedule $schedule)
protected function schedule(Schedule $schedule): void
{
$schedule->command(VideoReconcileCommand::class)->hourly();
$schedule->command(BalanceReconcileCommand::class, [Service::DIGITALOCEAN()->key])
->withoutOverlapping()
->runInBackground()
->storeOutput()
->hourly();
$schedule->command(DocumentDatabaseDumpCommand::class)
->withoutOverlapping()
->runInBackground()
->storeOutput()
->daily();
$schedule->command(WikiDatabaseDumpCommand::class)
->withoutOverlapping()
->runInBackground()
->storeOutput()
->daily();
$schedule->command(WikiDatabaseDumpCommand::class, ['--create'])
->withoutOverlapping()
->runInBackground()
->storeOutput()
->daily();
$schedule->command(PruneDatabaseDumpsCommand::class)
->withoutOverlapping()
->runInBackground()
->storeOutput()
->dailyAt('00:15');
$schedule->command(PruneExpired::class)
->withoutOverlapping()
->runInBackground()
->storeOutput()
->daily();
$schedule->command(PruneFailedJobsCommand::class)
->withoutOverlapping()
->runInBackground()
->storeOutput()
->daily();
$schedule->command(PruneModelsCommand::class, ['--except' => [BaseModel::class]])
->withoutOverlapping()
->runInBackground()
->storeOutput()
->daily();
$schedule->command(PruneTelescopeEntriesCommand::class)
->withoutOverlapping()
->runInBackground()
->storeOutput()
->daily();
$schedule->command(SnapshotCommand::class)
->withoutOverlapping()
->runInBackground()
->storeOutput()
->everyFiveMinutes();
$schedule->command(TransactionReconcileCommand::class, [Service::DIGITALOCEAN()->key])
->withoutOverlapping()
->runInBackground()
->storeOutput()
->hourly();
}
/**
* Register the commands for the application.
*
* @return void
*
* @noinspection PhpMissingParentCallCommonInspection
*/
protected function commands()
protected function commands(): void
{
$this->load(__DIR__.'/Commands');
+23
View File
@@ -0,0 +1,23 @@
<?php
declare(strict_types=1);
namespace App\Constants\Config;
/**
* Class FlagConstants.
*/
class FlagConstants
{
final public const ALLOW_VIDEO_STREAMS_FLAG = 'allow_video_streams';
final public const ALLOW_VIDEO_STREAMS_FLAG_QUALIFIED = 'flags.allow_video_streams';
final public const ALLOW_DISCORD_NOTIFICATIONS_FLAG = 'allow_discord_notifications';
final public const ALLOW_DISCORD_NOTIFICATIONS_FLAG_QUALIFIED = 'flags.allow_discord_notifications';
final public const ALLOW_VIEW_RECORDING_FLAG = 'allow_view_recording';
final public const ALLOW_VIEW_RECORDING_FLAG_QUALIFIED = 'flags.allow_view_recording';
}
+15
View File
@@ -0,0 +1,15 @@
<?php
declare(strict_types=1);
namespace App\Constants\Config;
/**
* Class ServiceConstants.
*/
class ServiceConstants
{
final public const ADMIN_DISCORD_CHANNEL_QUALIFIED = 'services.discord.admin_discord_channel';
final public const DB_UPDATES_DISCORD_CHANNEL_QUALIFIED = 'services.discord.db_updates_discord_channel';
}
+17
View File
@@ -0,0 +1,17 @@
<?php
declare(strict_types=1);
namespace App\Constants\Config;
/**
* Class WikiConstants.
*/
class WikiConstants
{
final public const FEATURED_ENTRY_SETTING_QUALIFIED = 'wiki.featured_entry';
final public const FEATURED_THEME_SETTING = 'featured_theme';
final public const FEATURED_VIDEO_SETTING_QUALIFIED = 'wiki.featured_video';
}
@@ -0,0 +1,18 @@
<?php
declare(strict_types=1);
namespace App\Contracts\Events;
/**
* Interface CascadesDeletesEvent.
*/
interface CascadesDeletesEvent
{
/**
* Perform cascading deletes.
*
* @return void
*/
public function cascadeDeletes(): void;
}
@@ -0,0 +1,18 @@
<?php
declare(strict_types=1);
namespace App\Contracts\Events;
/**
* Interface CascadesRestoresEvent.
*/
interface CascadesRestoresEvent
{
/**
* Perform cascading restores.
*
* @return void
*/
public function cascadeRestores(): void;
}
@@ -0,0 +1,27 @@
<?php
declare(strict_types=1);
namespace App\Contracts\Events;
use NotificationChannels\Discord\DiscordMessage;
/**
* Interface DiscordMessageEvent.
*/
interface DiscordMessageEvent
{
/**
* Get Discord message payload.
*
* @return DiscordMessage
*/
public function getDiscordMessage(): DiscordMessage;
/**
* Get Discord channel the message will be sent to.
*
* @return string
*/
public function getDiscordChannel(): string;
}
@@ -0,0 +1,35 @@
<?php
declare(strict_types=1);
namespace App\Contracts\Events;
use Illuminate\Support\Collection;
use Laravel\Nova\Notifications\NovaNotification;
/**
* Interface NovaNotificationEvent.
*/
interface NovaNotificationEvent
{
/**
* Determine if the notifications should be sent.
*
* @return bool
*/
public function shouldSend(): bool;
/**
* Get the nova notification.
*
* @return NovaNotification
*/
public function getNotification(): NovaNotification;
/**
* Get the users to notify.
*
* @return Collection
*/
public function getUsers(): Collection;
}
@@ -0,0 +1,18 @@
<?php
declare(strict_types=1);
namespace App\Contracts\Events;
/**
* Interface UpdateRelatedIndicesEvent.
*/
interface UpdateRelatedIndicesEvent
{
/**
* Perform updates on related indices.
*
* @return void
*/
public function updateRelatedIndices(): void;
}
@@ -0,0 +1,21 @@
<?php
declare(strict_types=1);
namespace App\Contracts\Http\Api\Field;
use Illuminate\Http\Request;
/**
* Interface CreatableField.
*/
interface CreatableField
{
/**
* Set the creation validation rules for the field.
*
* @param Request $request
* @return array
*/
public function getCreationRules(Request $request): array;
}
@@ -0,0 +1,18 @@
<?php
declare(strict_types=1);
namespace App\Contracts\Http\Api\Field;
/**
* Interface FieldInterface.
*/
interface FieldInterface
{
/**
* Get the field key.
*
* @return string
*/
public function getKey(): string;
}
@@ -0,0 +1,20 @@
<?php
declare(strict_types=1);
namespace App\Contracts\Http\Api\Field;
use App\Http\Api\Filter\Filter;
/**
* Interface FilterableColumn.
*/
interface FilterableField
{
/**
* Get the filters that can be applied to the field.
*
* @return Filter
*/
public function getFilter(): Filter;
}
@@ -0,0 +1,21 @@
<?php
declare(strict_types=1);
namespace App\Contracts\Http\Api\Field;
use App\Http\Api\Criteria\Field\Criteria;
/**
* Interface SelectableField.
*/
interface SelectableField
{
/**
* Determine if the field should be included in the select clause of our query.
*
* @param Criteria|null $criteria
* @return bool
*/
public function shouldSelect(?Criteria $criteria): bool;
}
@@ -0,0 +1,20 @@
<?php
declare(strict_types=1);
namespace App\Contracts\Http\Api\Field;
use App\Http\Api\Sort\Sort;
/**
* Interface SortableColumn.
*/
interface SortableField
{
/**
* Get the sort that can be applied to the field.
*
* @return Sort
*/
public function getSort(): Sort;
}
@@ -0,0 +1,21 @@
<?php
declare(strict_types=1);
namespace App\Contracts\Http\Api\Field;
use Illuminate\Http\Request;
/**
* Interface UpdatableField.
*/
interface UpdatableField
{
/**
* Set the update validation rules for the field.
*
* @param Request $request
* @return array
*/
public function getUpdateRules(Request $request): array;
}
@@ -0,0 +1,20 @@
<?php
declare(strict_types=1);
namespace App\Contracts\Http\Api\Query;
use App\Http\Api\Schema\Schema;
/**
* Interface QueryInterface.
*/
interface QueryInterface
{
/**
* Get the resource schema.
*
* @return Schema
*/
public function schema(): Schema;
}
@@ -0,0 +1,51 @@
<?php
declare(strict_types=1);
namespace App\Contracts\Http\Api\Schema;
use App\Contracts\Http\Api\Field\FieldInterface;
use App\Http\Api\Filter\Filter;
use App\Http\Api\Include\AllowedInclude;
use App\Http\Api\Sort\Sort;
/**
* Interface SchemaInterface.
*/
interface SchemaInterface
{
/**
* Get the type of the resource.
*
* @return string
*/
public function type(): string;
/**
* Get the allowed includes.
*
* @return AllowedInclude[]
*/
public function allowedIncludes(): array;
/**
* Get the direct fields of the resource.
*
* @return FieldInterface[]
*/
public function fields(): array;
/**
* Get the filters of the resource.
*
* @return Filter[]
*/
public function filters(): array;
/**
* Get the sorts of the resource.
*
* @return Sort[]
*/
public function sorts(): array;
}
@@ -0,0 +1,12 @@
<?php
declare(strict_types=1);
namespace App\Contracts\Http\Requests\Api;
/**
* Marker interface SearchableRequest.
*/
interface SearchableRequest
{
}
+18
View File
@@ -0,0 +1,18 @@
<?php
declare(strict_types=1);
namespace App\Contracts\Models;
/**
* Interface Nameable.
*/
interface Nameable
{
/**
* Get name.
*
* @return string
*/
public function getName(): string;
}
+23
View File
@@ -0,0 +1,23 @@
<?php
declare(strict_types=1);
namespace App\Contracts\Pipes;
use App\Models\Auth\User;
use Closure;
/**
* Interface Pipe.
*/
interface Pipe
{
/**
* Handle an incoming request.
*
* @param User $user
* @param Closure(User): mixed $next
* @return mixed
*/
public function handle(User $user, Closure $next): mixed;
}
+65
View File
@@ -0,0 +1,65 @@
<?php
declare(strict_types=1);
namespace App\Contracts\Repositories;
use Illuminate\Database\Eloquent\Model;
use Illuminate\Support\Collection;
/**
* Interface Repository.
*/
interface Repository
{
/**
* Get models from the repository.
*
* @param array $columns
* @return Collection
*/
public function get(array $columns = ['*']): Collection;
/**
* Save model to the repository.
*
* @param Model $model
* @return bool
*/
public function save(Model $model): bool;
/**
* Delete model from the repository.
*
* @param Model $model
* @return bool
*/
public function delete(Model $model): bool;
/**
* Update model in the repository.
*
* @param Model $model
* @param array $attributes
* @return bool
*/
public function update(Model $model, array $attributes): bool;
/**
* Validate repository filter.
*
* @param string $filter
* @param mixed $value
* @return bool
*/
public function validateFilter(string $filter, mixed $value = null): bool;
/**
* Filter repository models.
*
* @param string $filter
* @param mixed $value
* @return void
*/
public function handleFilter(string $filter, mixed $value = null): void;
}
+90
View File
@@ -0,0 +1,90 @@
<?php
declare(strict_types=1);
namespace App\Discord;
use BenSampo\Enum\Enum;
use Illuminate\Contracts\Support\Arrayable;
use Illuminate\Support\Str;
use JsonSerializable;
/**
* Class DiscordEmbedField.
*/
class DiscordEmbedField implements Arrayable, JsonSerializable
{
final public const DEFAULT_FIELD_VALUE = '-';
/**
* The value of the field.
*
* @var string
*/
protected readonly string $value;
/**
* Create a new field instance.
*
* @param string $name
* @param mixed $value
* @param bool $inline
*/
final public function __construct(
protected readonly string $name,
mixed $value,
protected readonly bool $inline = false
) {
$this->value = $this->formatEmbedFieldValue($value);
}
/**
* Get the instance as an array.
*
* @return array<string, mixed>
*/
public function toArray(): array
{
return [
'name' => $this->name,
'value' => $this->value,
'inline' => $this->inline,
];
}
/**
* Convert the object into something JSON serializable.
*
* @return array<string, mixed>
*/
public function jsonSerialize(): array
{
return $this->toArray();
}
/**
* Format embed value to circumvent exceptions caused by empty or null values.
*
* @param mixed $value
* @return string
*/
protected function formatEmbedFieldValue(mixed $value): string
{
// Use description for enums
if ($value instanceof Enum) {
return $value->description;
}
// Pretty print booleans
if (is_bool($value)) {
return $value ? 'true' : 'false';
}
// Encode to json for all other non-empty scalar values
if (is_scalar($value) && Str::length($value) > 0) {
return strval($value);
}
return self::DEFAULT_FIELD_VALUE;
}
}
+30
View File
@@ -0,0 +1,30 @@
<?php
declare(strict_types=1);
namespace App\Enums;
use BenSampo\Enum\Contracts\LocalizedEnum;
use BenSampo\Enum\Enum;
use Illuminate\Support\Arr;
use Illuminate\Support\Str;
/**
* Class BaseEnum.
*/
abstract class BaseEnum extends Enum implements LocalizedEnum
{
/**
* Make a new instance from an enum description.
*
* @param string $description
* @return static|null
*/
public static function fromDescription(string $description): ?static
{
return Arr::first(
static::getInstances(),
fn (BaseEnum $enum) => Str::lower($enum->description) === Str::lower($description)
);
}
}
+21
View File
@@ -0,0 +1,21 @@
<?php
declare(strict_types=1);
namespace App\Enums\Discord;
use App\Enums\BaseEnum;
/**
* Class EmbedColor.
*
* @method static static GREEN()
* @method static static YELLOW()
* @method static static RED()
*/
final class EmbedColor extends BaseEnum
{
public const GREEN = 3066993;
public const YELLOW = 16776960;
public const RED = 15158332;
}
@@ -0,0 +1,21 @@
<?php
declare(strict_types=1);
namespace App\Enums\Http\Api\Filter;
use App\Enums\BaseEnum;
/**
* Class AllowedDateFormat.
*/
final class AllowedDateFormat extends BaseEnum
{
public const YMDHISU = 'Y-m-d\TH:i:s.u';
public const YMDHIS = 'Y-m-d\TH:i:s';
public const YMDHI = 'Y-m-d\TH:i';
public const YMDH = 'Y-m-d\TH';
public const YMD = 'Y-m-d';
public const YM = 'Y-m';
public const Y = 'Y';
}
@@ -0,0 +1,17 @@
<?php
declare(strict_types=1);
namespace App\Enums\Http\Api\Filter;
/**
* Class BinaryLogicalOperator.
*
* @method static static AND()
* @method static static OR()
*/
final class BinaryLogicalOperator extends LogicalOperator
{
public const AND = 'and';
public const OR = 'or';
}
@@ -0,0 +1,31 @@
<?php
declare(strict_types=1);
namespace App\Enums\Http\Api\Filter;
use App\Enums\BaseEnum;
/**
* Class ComparisonOperator.
*
* @method static static EQ()
* @method static static NE()
* @method static static LT()
* @method static static GT()
* @method static static LTE()
* @method static static GTE()
* @method static static LIKE()
* @method static static NOTLIKE()
*/
final class ComparisonOperator extends BaseEnum
{
public const EQ = '=';
public const NE = '<>';
public const LT = '<';
public const GT = '>';
public const LTE = '<=';
public const GTE = '>=';
public const LIKE = 'like';
public const NOTLIKE = 'not like';
}
@@ -0,0 +1,15 @@
<?php
declare(strict_types=1);
namespace App\Enums\Http\Api\Filter;
use App\Enums\BaseEnum;
/**
* Class LogicalOperator.
*/
abstract class LogicalOperator extends BaseEnum
{
//
}
@@ -0,0 +1,17 @@
<?php
declare(strict_types=1);
namespace App\Enums\Http\Api\Filter;
use App\Enums\BaseEnum;
/**
* Class TrashedStatus.
*/
final class TrashedStatus extends BaseEnum
{
public const WITH = 'with';
public const WITHOUT = 'without';
public const ONLY = 'only';
}
@@ -0,0 +1,13 @@
<?php
declare(strict_types=1);
namespace App\Enums\Http\Api\Filter;
/**
* Class UnaryLogicalOperator.
*/
final class UnaryLogicalOperator extends LogicalOperator
{
public const NOT = 'not';
}
@@ -0,0 +1,21 @@
<?php
declare(strict_types=1);
namespace App\Enums\Http\Api\Paging;
use App\Enums\BaseEnum;
/**
* Class PaginationStrategy.
*
* @method static static NONE()
* @method static static LIMIT()
* @method static static OFFSET()
*/
final class PaginationStrategy extends BaseEnum
{
public const NONE = 0;
public const LIMIT = 1;
public const OFFSET = 2;
}
+19
View File
@@ -0,0 +1,19 @@
<?php
declare(strict_types=1);
namespace App\Enums\Http\Api\Sort;
use App\Enums\BaseEnum;
/**
* Class Direction.
*
* @method static static ASCENDING()
* @method static static DESCENDING()
*/
final class Direction extends BaseEnum
{
public const ASCENDING = 'asc';
public const DESCENDING = 'desc';
}
@@ -0,0 +1,19 @@
<?php
declare(strict_types=1);
namespace App\Enums\Models\Auth;
use App\Enums\BaseEnum;
/**
* Class InvitationStatus.
*
* @method static static OPEN()
* @method static static CLOSED()
*/
final class InvitationStatus extends BaseEnum
{
public const OPEN = 0;
public const CLOSED = 1;
}
@@ -0,0 +1,19 @@
<?php
declare(strict_types=1);
namespace App\Enums\Models\Billing;
use App\Enums\BaseEnum;
/**
* Class BalanceFrequency.
*/
final class BalanceFrequency extends BaseEnum
{
public const ONCE = 0;
public const ANNUALLY = 1;
public const BIANNUALLY = 2;
public const QUARTERLY = 3;
public const MONTHLY = 4;
}
+25
View File
@@ -0,0 +1,25 @@
<?php
declare(strict_types=1);
namespace App\Enums\Models\Billing;
use App\Enums\BaseEnum;
/**
* Class Service.
*
* @method static static OTHER()
* @method static static DIGITALOCEAN()
* @method static static AWS()
* @method static static HOVER()
* @method static static WALKERSERVERS()
*/
final class Service extends BaseEnum
{
public const OTHER = 0;
public const DIGITALOCEAN = 1;
public const AWS = 2;
public const HOVER = 3;
public const WALKERSERVERS = 4;
}
+18
View File
@@ -0,0 +1,18 @@
<?php
declare(strict_types=1);
namespace App\Enums\Models\Wiki;
use App\Enums\BaseEnum;
/**
* Class AnimeSeason.
*/
final class AnimeSeason extends BaseEnum
{
public const WINTER = 0;
public const SPRING = 1;
public const SUMMER = 2;
public const FALL = 3;
}
+21
View File
@@ -0,0 +1,21 @@
<?php
declare(strict_types=1);
namespace App\Enums\Models\Wiki;
use App\Enums\BaseEnum;
/**
* Class ImageFacet.
*
* @method static static COVER_SMALL()
* @method static static COVER_LARGE()
* @method static static GRILL()
*/
final class ImageFacet extends BaseEnum
{
public const COVER_SMALL = 0;
public const COVER_LARGE = 1;
public const GRILL = 2;
}
+137
View File
@@ -0,0 +1,137 @@
<?php
declare(strict_types=1);
namespace App\Enums\Models\Wiki;
use App\Enums\BaseEnum;
use Illuminate\Support\Arr;
use Illuminate\Support\Str;
/**
* Class ResourceSite.
*
* @method static static OFFICIAL_SITE()
* @method static static TWITTER()
* @method static static ANIDB()
* @method static static ANILIST()
* @method static static ANIME_PLANET()
* @method static static ANN()
* @method static static KITSU()
* @method static static MAL()
* @method static static WIKI()
*/
final class ResourceSite extends BaseEnum
{
// Official Media
public const OFFICIAL_SITE = 0;
public const TWITTER = 1;
// Tracking Sites
public const ANIDB = 2;
public const ANILIST = 3;
public const ANIME_PLANET = 4;
public const ANN = 5;
public const KITSU = 6;
public const MAL = 7;
// Compendia
public const WIKI = 8;
/**
* Get domain by resource site.
*
* @param int|null $value the resource site key
* @return string|null
*/
public static function getDomain(?int $value): ?string
{
return match ($value) {
self::TWITTER => 'twitter.com',
self::ANIDB => 'anidb.net',
self::ANILIST => 'anilist.co',
self::ANIME_PLANET => 'anime-planet.com',
self::ANN => 'animenewsnetwork.com',
self::KITSU => 'kitsu.io',
self::MAL => 'myanimelist.net',
self::WIKI => 'wikipedia.org',
default => null,
};
}
/**
* Get resource site by link, matching expected domain.
*
* @param string $link the link to test
* @return ResourceSite|null
*/
public static function valueOf(string $link): ?ResourceSite
{
$parsedHost = parse_url($link, PHP_URL_HOST);
return Arr::first(
ResourceSite::getInstances(),
fn (ResourceSite $site) => $parsedHost === ResourceSite::getDomain($site->value)
);
}
/**
* Attempt to parse External ID from Site Link.
*
* @param string $link
* @return string|null
*/
public static function parseIdFromLink(string $link): ?string
{
$site = ResourceSite::valueOf($link);
return match ($site?->value) {
ResourceSite::ANIDB,
ResourceSite::ANILIST,
ResourceSite::ANN,
ResourceSite::MAL => Str::match('/\d+/', $link),
default => null,
};
}
/**
* Get the URL of the site for anime resources.
*
* @param ResourceSite $site
* @param int $id
* @param string|null $slug
* @return string|null
*/
public static function formatAnimeResourceLink(ResourceSite $site, int $id, ?string $slug = null): ?string
{
return match ($site->value) {
ResourceSite::ANIDB => "https://anidb.net/anime/$id/",
ResourceSite::ANILIST => "https://anilist.co/anime/$id/",
ResourceSite::ANIME_PLANET => "https://anime-planet.com/anime/$slug",
ResourceSite::ANN => "https://animenewsnetwork.com/encyclopedia/anime.php?id=$id",
ResourceSite::KITSU => "https://kitsu.io/anime/$slug",
ResourceSite::MAL => "https://myanimelist.net/anime/$id/",
default => null,
};
}
/**
* Get the URL of the site for studio resources.
*
* @param ResourceSite $site
* @param int $id
* @param string|null $slug
* @return string|null
*/
public static function formatStudioResourceLink(ResourceSite $site, int $id, ?string $slug = null): ?string
{
return match ($site->value) {
ResourceSite::ANIDB => "https://anidb.net/creator/$id/",
ResourceSite::ANILIST => "https://anilist.co/studio/$id/",
ResourceSite::ANIME_PLANET => "https://anime-planet.com/anime/studios/$slug",
ResourceSite::ANN => "https://animenewsnetwork.com/encyclopedia/company.php?id=$id",
ResourceSite::MAL => "https://myanimelist.net/anime/producer/$id/",
default => null,
};
}
}
+16
View File
@@ -0,0 +1,16 @@
<?php
declare(strict_types=1);
namespace App\Enums\Models\Wiki;
use App\Enums\BaseEnum;
/**
* Class ThemeType.
*/
final class ThemeType extends BaseEnum
{
public const OP = 0;
public const ED = 1;
}
+21
View File
@@ -0,0 +1,21 @@
<?php
declare(strict_types=1);
namespace App\Enums\Models\Wiki;
use App\Enums\BaseEnum;
/**
* Class VideoOverlap.
*
* @method static static NONE()
* @method static static TRANS()
* @method static static OVER()
*/
final class VideoOverlap extends BaseEnum
{
public const NONE = 0;
public const TRANS = 1;
public const OVER = 2;
}
+20
View File
@@ -0,0 +1,20 @@
<?php
declare(strict_types=1);
namespace App\Enums\Models\Wiki;
use App\Enums\BaseEnum;
/**
* Class VideoSource.
*/
final class VideoSource extends BaseEnum
{
public const WEB = 0;
public const RAW = 1;
public const BD = 2;
public const DVD = 3;
public const VHS = 4;
public const LD = 5;
}
@@ -0,0 +1,46 @@
<?php
declare(strict_types=1);
namespace App\Events\Admin\Announcement;
use App\Events\Base\Admin\AdminCreatedEvent;
use App\Models\Admin\Announcement;
/**
* Class AnnouncementCreated.
*
* @extends AdminCreatedEvent<Announcement>
*/
class AnnouncementCreated extends AdminCreatedEvent
{
/**
* Create a new event instance.
*
* @param Announcement $announcement
*/
public function __construct(Announcement $announcement)
{
parent::__construct($announcement);
}
/**
* Get the model that has fired this event.
*
* @return Announcement
*/
public function getModel(): Announcement
{
return $this->model;
}
/**
* Get the description for the Discord message payload.
*
* @return string
*/
protected function getDiscordMessageDescription(): string
{
return "Announcement '**{$this->getModel()->getName()}**' has been created.";
}
}
@@ -0,0 +1,46 @@
<?php
declare(strict_types=1);
namespace App\Events\Admin\Announcement;
use App\Events\Base\Admin\AdminDeletedEvent;
use App\Models\Admin\Announcement;
/**
* Class AnnouncementDeleted.
*
* @extends AdminDeletedEvent<Announcement>
*/
class AnnouncementDeleted extends AdminDeletedEvent
{
/**
* Create a new event instance.
*
* @param Announcement $announcement
*/
public function __construct(Announcement $announcement)
{
parent::__construct($announcement);
}
/**
* Get the model that has fired this event.
*
* @return Announcement
*/
public function getModel(): Announcement
{
return $this->model;
}
/**
* Get the description for the Discord message payload.
*
* @return string
*/
protected function getDiscordMessageDescription(): string
{
return "Announcement '**{$this->getModel()->getName()}**' has been deleted.";
}
}
@@ -0,0 +1,46 @@
<?php
declare(strict_types=1);
namespace App\Events\Admin\Announcement;
use App\Events\Base\Admin\AdminRestoredEvent;
use App\Models\Admin\Announcement;
/**
* Class AnnouncementRestored.
*
* @extends AdminRestoredEvent<Announcement>
*/
class AnnouncementRestored extends AdminRestoredEvent
{
/**
* Create a new event instance.
*
* @param Announcement $announcement
*/
public function __construct(Announcement $announcement)
{
parent::__construct($announcement);
}
/**
* Get the model that has fired this event.
*
* @return Announcement
*/
public function getModel(): Announcement
{
return $this->model;
}
/**
* Get the description for the Discord message payload.
*
* @return string
*/
protected function getDiscordMessageDescription(): string
{
return "Announcement '**{$this->getModel()->getName()}**' has been restored.";
}
}
@@ -0,0 +1,47 @@
<?php
declare(strict_types=1);
namespace App\Events\Admin\Announcement;
use App\Events\Base\Admin\AdminUpdatedEvent;
use App\Models\Admin\Announcement;
/**
* Class AnnouncementUpdated.
*
* @extends AdminUpdatedEvent<Announcement>
*/
class AnnouncementUpdated extends AdminUpdatedEvent
{
/**
* Create a new event instance.
*
* @param Announcement $announcement
*/
public function __construct(Announcement $announcement)
{
parent::__construct($announcement);
$this->initializeEmbedFields($announcement);
}
/**
* Get the model that has fired this event.
*
* @return Announcement
*/
public function getModel(): Announcement
{
return $this->model;
}
/**
* Get the description for the Discord message payload.
*
* @return string
*/
protected function getDiscordMessageDescription(): string
{
return "Announcement '**{$this->getModel()->getName()}**' has been updated.";
}
}
@@ -0,0 +1,46 @@
<?php
declare(strict_types=1);
namespace App\Events\Auth\Invitation;
use App\Events\Base\Admin\AdminCreatedEvent;
use App\Models\Auth\Invitation;
/**
* Class InvitationCreated.
*
* @extends AdminCreatedEvent<Invitation>
*/
class InvitationCreated extends AdminCreatedEvent
{
/**
* Create a new event instance.
*
* @param Invitation $invitation
*/
public function __construct(Invitation $invitation)
{
parent::__construct($invitation);
}
/**
* Get the model that has fired this event.
*
* @return Invitation
*/
public function getModel(): Invitation
{
return $this->model;
}
/**
* Get the description for the Discord message payload.
*
* @return string
*/
protected function getDiscordMessageDescription(): string
{
return "Invitation '**{$this->getModel()->getName()}**' has been created.";
}
}
@@ -0,0 +1,46 @@
<?php
declare(strict_types=1);
namespace App\Events\Auth\Invitation;
use App\Events\Base\Admin\AdminDeletedEvent;
use App\Models\Auth\Invitation;
/**
* Class InvitationDeleted.
*
* @extends AdminDeletedEvent<Invitation>
*/
class InvitationDeleted extends AdminDeletedEvent
{
/**
* Create a new event instance.
*
* @param Invitation $invitation
*/
public function __construct(Invitation $invitation)
{
parent::__construct($invitation);
}
/**
* Get the model that has fired this event.
*
* @return Invitation
*/
public function getModel(): Invitation
{
return $this->model;
}
/**
* Get the description for the Discord message payload.
*
* @return string
*/
protected function getDiscordMessageDescription(): string
{
return "Invitation '**{$this->getModel()->getName()}**' has been deleted.";
}
}
@@ -0,0 +1,46 @@
<?php
declare(strict_types=1);
namespace App\Events\Auth\Invitation;
use App\Events\Base\Admin\AdminRestoredEvent;
use App\Models\Auth\Invitation;
/**
* Class InvitationRestored.
*
* @extends AdminRestoredEvent<Invitation>
*/
class InvitationRestored extends AdminRestoredEvent
{
/**
* Create a new event instance.
*
* @param Invitation $invitation
*/
public function __construct(Invitation $invitation)
{
parent::__construct($invitation);
}
/**
* Get the model that has fired this event.
*
* @return Invitation
*/
public function getModel(): Invitation
{
return $this->model;
}
/**
* Get the description for the Discord message payload.
*
* @return string
*/
protected function getDiscordMessageDescription(): string
{
return "Invitation '**{$this->getModel()->getName()}**' has been restored.";
}
}
@@ -0,0 +1,47 @@
<?php
declare(strict_types=1);
namespace App\Events\Auth\Invitation;
use App\Events\Base\Admin\AdminUpdatedEvent;
use App\Models\Auth\Invitation;
/**
* Class InvitationUpdated.
*
* @extends AdminUpdatedEvent<Invitation>
*/
class InvitationUpdated extends AdminUpdatedEvent
{
/**
* Create a new event instance.
*
* @param Invitation $invitation
*/
public function __construct(Invitation $invitation)
{
parent::__construct($invitation);
$this->initializeEmbedFields($invitation);
}
/**
* Get the model that has fired this event.
*
* @return Invitation
*/
public function getModel(): Invitation
{
return $this->model;
}
/**
* Get the description for the Discord message payload.
*
* @return string
*/
protected function getDiscordMessageDescription(): string
{
return "Invitation '**{$this->getModel()->getName()}**' has been updated.";
}
}
+47
View File
@@ -0,0 +1,47 @@
<?php
declare(strict_types=1);
namespace App\Events\Auth\User;
use App\Constants\Config\ServiceConstants;
use App\Contracts\Events\DiscordMessageEvent;
use App\Enums\Discord\EmbedColor;
use Illuminate\Foundation\Events\Dispatchable;
use Illuminate\Queue\SerializesModels;
use Illuminate\Support\Facades\Config;
use NotificationChannels\Discord\DiscordMessage;
/**
* Class UserCreated.
*/
class UserCreated extends UserEvent implements DiscordMessageEvent
{
use Dispatchable;
use SerializesModels;
/**
* Get Discord message payload.
*
* @return DiscordMessage
*/
public function getDiscordMessage(): DiscordMessage
{
$user = $this->getUser();
return DiscordMessage::create('', [
'description' => "User '**{$user->getName()}**' has been created.",
'color' => EmbedColor::GREEN,
]);
}
/**
* Get Discord channel the message will be sent to.
*
* @return string
*/
public function getDiscordChannel(): string
{
return Config::get(ServiceConstants::ADMIN_DISCORD_CHANNEL_QUALIFIED);
}
}
+45
View File
@@ -0,0 +1,45 @@
<?php
declare(strict_types=1);
namespace App\Events\Auth\User;
use App\Constants\Config\ServiceConstants;
use App\Contracts\Events\DiscordMessageEvent;
use App\Enums\Discord\EmbedColor;
use Illuminate\Foundation\Events\Dispatchable;
use Illuminate\Support\Facades\Config;
use NotificationChannels\Discord\DiscordMessage;
/**
* Class UserDeleted.
*/
class UserDeleted extends UserEvent implements DiscordMessageEvent
{
use Dispatchable;
/**
* Get Discord message payload.
*
* @return DiscordMessage
*/
public function getDiscordMessage(): DiscordMessage
{
$user = $this->getUser();
return DiscordMessage::create('', [
'description' => "User '**{$user->getName()}**' has been deleted.",
'color' => EmbedColor::RED,
]);
}
/**
* Get Discord channel the message will be sent to.
*
* @return string
*/
public function getDiscordChannel(): string
{
return Config::get(ServiceConstants::ADMIN_DISCORD_CHANNEL_QUALIFIED);
}
}
+33
View File
@@ -0,0 +1,33 @@
<?php
declare(strict_types=1);
namespace App\Events\Auth\User;
use App\Models\Auth\User;
/**
* Class UserEvent.
*/
abstract class UserEvent
{
/**
* Create a new event instance.
*
* @param User $user
* @return void
*/
public function __construct(protected User $user)
{
}
/**
* Get the user that has fired this event.
*
* @return User
*/
public function getUser(): User
{
return $this->user;
}
}
+47
View File
@@ -0,0 +1,47 @@
<?php
declare(strict_types=1);
namespace App\Events\Auth\User;
use App\Constants\Config\ServiceConstants;
use App\Contracts\Events\DiscordMessageEvent;
use App\Enums\Discord\EmbedColor;
use Illuminate\Foundation\Events\Dispatchable;
use Illuminate\Queue\SerializesModels;
use Illuminate\Support\Facades\Config;
use NotificationChannels\Discord\DiscordMessage;
/**
* Class UserRestored.
*/
class UserRestored extends UserEvent implements DiscordMessageEvent
{
use Dispatchable;
use SerializesModels;
/**
* Get Discord message payload.
*
* @return DiscordMessage
*/
public function getDiscordMessage(): DiscordMessage
{
$user = $this->getUser();
return DiscordMessage::create('', [
'description' => "User '**{$user->getName()}**' has been restored.",
'color' => EmbedColor::GREEN,
]);
}
/**
* Get Discord channel the message will be sent to.
*
* @return string
*/
public function getDiscordChannel(): string
{
return Config::get(ServiceConstants::ADMIN_DISCORD_CHANNEL_QUALIFIED);
}
}
+61
View File
@@ -0,0 +1,61 @@
<?php
declare(strict_types=1);
namespace App\Events\Auth\User;
use App\Concerns\Discord\HasAttributeUpdateEmbedFields;
use App\Constants\Config\ServiceConstants;
use App\Contracts\Events\DiscordMessageEvent;
use App\Enums\Discord\EmbedColor;
use App\Models\Auth\User;
use Illuminate\Foundation\Events\Dispatchable;
use Illuminate\Support\Facades\Config;
use NotificationChannels\Discord\DiscordMessage;
/**
* Class UserUpdated.
*/
class UserUpdated extends UserEvent implements DiscordMessageEvent
{
use Dispatchable;
use HasAttributeUpdateEmbedFields;
/**
* Create a new event instance.
*
* @param User $user
* @return void
*/
public function __construct(User $user)
{
parent::__construct($user);
$this->initializeEmbedFields($user);
}
/**
* Get Discord message payload.
*
* @return DiscordMessage
*/
public function getDiscordMessage(): DiscordMessage
{
$user = $this->getUser();
return DiscordMessage::create('', [
'description' => "User '**{$user->getName()}**' has been updated.",
'fields' => $this->getEmbedFields(),
'color' => EmbedColor::YELLOW,
]);
}
/**
* Get Discord channel the message will be sent to.
*
* @return string
*/
public function getDiscordChannel(): string
{
return Config::get(ServiceConstants::ADMIN_DISCORD_CHANNEL_QUALIFIED);
}
}
@@ -0,0 +1,28 @@
<?php
declare(strict_types=1);
namespace App\Events\Base\Admin;
use App\Constants\Config\ServiceConstants;
use App\Events\Base\BaseCreatedEvent;
use Illuminate\Support\Facades\Config;
/**
* Class AdminCreatedEvent.
*
* @template TModel of \App\Models\BaseModel
* @extends BaseCreatedEvent<TModel>
*/
abstract class AdminCreatedEvent extends BaseCreatedEvent
{
/**
* Get Discord channel the message will be sent to.
*
* @return string
*/
public function getDiscordChannel(): string
{
return Config::get(ServiceConstants::ADMIN_DISCORD_CHANNEL_QUALIFIED);
}
}
@@ -0,0 +1,28 @@
<?php
declare(strict_types=1);
namespace App\Events\Base\Admin;
use App\Constants\Config\ServiceConstants;
use App\Events\Base\BaseDeletedEvent;
use Illuminate\Support\Facades\Config;
/**
* Class AdminDeletedEvent.
*
* @template TModel of \App\Models\BaseModel
* @extends BaseDeletedEvent<TModel>
*/
abstract class AdminDeletedEvent extends BaseDeletedEvent
{
/**
* Get Discord channel the message will be sent to.
*
* @return string
*/
public function getDiscordChannel(): string
{
return Config::get(ServiceConstants::ADMIN_DISCORD_CHANNEL_QUALIFIED);
}
}
@@ -0,0 +1,28 @@
<?php
declare(strict_types=1);
namespace App\Events\Base\Admin;
use App\Constants\Config\ServiceConstants;
use App\Events\Base\BaseRestoredEvent;
use Illuminate\Support\Facades\Config;
/**
* Class AdminRestoredEvent.
*
* @template TModel of \App\Models\BaseModel
* @extends BaseRestoredEvent<TModel>
*/
abstract class AdminRestoredEvent extends BaseRestoredEvent
{
/**
* Get Discord channel the message will be sent to.
*
* @return string
*/
public function getDiscordChannel(): string
{
return Config::get(ServiceConstants::ADMIN_DISCORD_CHANNEL_QUALIFIED);
}
}
@@ -0,0 +1,28 @@
<?php
declare(strict_types=1);
namespace App\Events\Base\Admin;
use App\Constants\Config\ServiceConstants;
use App\Events\Base\BaseUpdatedEvent;
use Illuminate\Support\Facades\Config;
/**
* Class AdminUpdatedEvent.
*
* @template TModel of \App\Models\BaseModel
* @extends BaseUpdatedEvent<TModel>
*/
abstract class AdminUpdatedEvent extends BaseUpdatedEvent
{
/**
* Get Discord channel the message will be sent to.
*
* @return string
*/
public function getDiscordChannel(): string
{
return Config::get(ServiceConstants::ADMIN_DISCORD_CHANNEL_QUALIFIED);
}
}

Some files were not shown because too many files have changed in this diff Show More