mirror of
https://github.com/complexcaresolutions/cms.c2sgmbh.git
synced 2026-03-17 19:44:12 +00:00
feat(Community): add Community Management Phase 1
- Add 5 new collections: SocialPlatforms, SocialAccounts, CommunityInteractions, CommunityTemplates, CommunityRules - Add communityRole field to Users collection - Add YouTube API client for comment sync - Add Claude AI service for sentiment analysis - Add API endpoints: /api/community/sync-comments, /api/community/reply - Add communityAccess.ts for role-based access control - Add migrations for all new tables and community_role enum fix Fix: Make audit hooks non-blocking to prevent user save timeout Dependencies: @anthropic-ai/sdk, googleapis Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
parent
3294fbb506
commit
358920f442
20 changed files with 4056 additions and 245 deletions
|
|
@ -27,6 +27,7 @@
|
|||
"prepare": "test -d .git && (ln -sf ../../scripts/detect-secrets.sh .git/hooks/pre-commit 2>/dev/null || true) || true"
|
||||
},
|
||||
"dependencies": {
|
||||
"@anthropic-ai/sdk": "^0.71.2",
|
||||
"@payloadcms/db-postgres": "3.69.0",
|
||||
"@payloadcms/next": "3.69.0",
|
||||
"@payloadcms/plugin-form-builder": "3.69.0",
|
||||
|
|
@ -40,6 +41,7 @@
|
|||
"bullmq": "^5.65.1",
|
||||
"cross-env": "^7.0.3",
|
||||
"dotenv": "16.4.7",
|
||||
"googleapis": "^170.0.0",
|
||||
"ioredis": "^5.8.2",
|
||||
"next": "15.5.9",
|
||||
"node-cron": "^4.2.1",
|
||||
|
|
|
|||
402
pnpm-lock.yaml
402
pnpm-lock.yaml
|
|
@ -8,6 +8,9 @@ importers:
|
|||
|
||||
.:
|
||||
dependencies:
|
||||
'@anthropic-ai/sdk':
|
||||
specifier: ^0.71.2
|
||||
version: 0.71.2
|
||||
'@payloadcms/db-postgres':
|
||||
specifier: 3.69.0
|
||||
version: 3.69.0(payload@3.69.0(graphql@16.12.0)(typescript@5.9.3))
|
||||
|
|
@ -47,6 +50,9 @@ importers:
|
|||
dotenv:
|
||||
specifier: 16.4.7
|
||||
version: 16.4.7
|
||||
googleapis:
|
||||
specifier: ^170.0.0
|
||||
version: 170.0.0
|
||||
ioredis:
|
||||
specifier: ^5.8.2
|
||||
version: 5.8.2
|
||||
|
|
@ -132,6 +138,15 @@ importers:
|
|||
|
||||
packages:
|
||||
|
||||
'@anthropic-ai/sdk@0.71.2':
|
||||
resolution: {integrity: sha512-TGNDEUuEstk/DKu0/TflXAEt+p+p/WhTlFzEnoosvbaDU2LTjm42igSdlL0VijrKpWejtOKxX0b8A7uc+XiSAQ==}
|
||||
hasBin: true
|
||||
peerDependencies:
|
||||
zod: ^3.25.0 || ^4.0.0
|
||||
peerDependenciesMeta:
|
||||
zod:
|
||||
optional: true
|
||||
|
||||
'@apidevtools/json-schema-ref-parser@11.9.3':
|
||||
resolution: {integrity: sha512-60vepv88RwcJtSHrD6MjIL6Ta3SOYbgfnkHb+ppAVK+o9mXprRtulx7VlRl3lN3bbvysAfCS7WMVfhUYemB0IQ==}
|
||||
engines: {node: '>= 16'}
|
||||
|
|
@ -1009,6 +1024,10 @@ packages:
|
|||
'@ioredis/commands@1.4.0':
|
||||
resolution: {integrity: sha512-aFT2yemJJo+TZCmieA7qnYGQooOS7QfNmYrzGtsYd3g9j5iDP8AimYYAesf79ohjbLG12XxC4nG5DyEnC88AsQ==}
|
||||
|
||||
'@isaacs/cliui@8.0.2':
|
||||
resolution: {integrity: sha512-O8jcjabXaleOG9DQ0+ARXWZBTfnP4WNAqzuiJK7ll44AmxGKv/J2M4TPjxjY3znBCfvBXFzucm1twdyFybFqEA==}
|
||||
engines: {node: '>=12'}
|
||||
|
||||
'@jridgewell/gen-mapping@0.3.13':
|
||||
resolution: {integrity: sha512-2kkt/7niJ6MgEPxF0bYdQ6etZaA+fQvDcLKckhy1yIQOzaoKjBBjSj63/aLVjYE3qhRt5dvM+uUyfCg6UKCBbA==}
|
||||
|
||||
|
|
@ -1303,6 +1322,10 @@ packages:
|
|||
'@pinojs/redact@0.4.0':
|
||||
resolution: {integrity: sha512-k2ENnmBugE/rzQfEcdWHcCY+/FM3VLzH9cYEsbdsoqrvzAKRhUZeRNhAZvB8OitQJ1TBed3yqWtdjzS6wJKBwg==}
|
||||
|
||||
'@pkgjs/parseargs@0.11.0':
|
||||
resolution: {integrity: sha512-+1VkjdD0QBLPodGrJUeqarH8VAIvQODIbwh9XpP5Syisf7YoQgsJKPNFoqqLQlu+VQ/tVSshMR6loPMn8U+dPg==}
|
||||
engines: {node: '>=14'}
|
||||
|
||||
'@playwright/test@1.57.0':
|
||||
resolution: {integrity: sha512-6TyEnHgd6SArQO8UO2OMTxshln3QMWBtPGrOCgs3wVEmQmwyuNtB10IZMfmYDE0riwNR1cu4q+pPcxMVtaG3TA==}
|
||||
engines: {node: '>=18'}
|
||||
|
|
@ -1925,6 +1948,10 @@ packages:
|
|||
ajv@8.17.1:
|
||||
resolution: {integrity: sha512-B/gBuNg5SiMTrPkC+A2+cW0RszwxYmn6VYxB/inlBStS5nx6xHIt/ehKRhIMhqusl7a8LjQoZnjCs5vhwxOQ1g==}
|
||||
|
||||
ansi-regex@5.0.1:
|
||||
resolution: {integrity: sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==}
|
||||
engines: {node: '>=8'}
|
||||
|
||||
ansi-regex@6.2.2:
|
||||
resolution: {integrity: sha512-Bq3SmSpyFHaWjPk8If9yc6svM8c56dB5BAtW4Qbw5jHTwwXXcTLoRMkpDJp6VL0XzlWaCHTXrkFURMYmD0sLqg==}
|
||||
engines: {node: '>=12'}
|
||||
|
|
@ -2017,10 +2044,16 @@ packages:
|
|||
balanced-match@1.0.2:
|
||||
resolution: {integrity: sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==}
|
||||
|
||||
base64-js@1.5.1:
|
||||
resolution: {integrity: sha512-AKpaYlHn8t4SVbOHCy+b5+KKgvR4vrsD8vbvrbiQJps7fKDTkjkDry6ji0rUJjC0kzbNePLwzxq8iypo41qeWA==}
|
||||
|
||||
baseline-browser-mapping@2.8.31:
|
||||
resolution: {integrity: sha512-a28v2eWrrRWPpJSzxc+mKwm0ZtVx/G8SepdQZDArnXYU/XS+IF6mp8aB/4E+hH1tyGCoDo3KlUCdlSxGDsRkAw==}
|
||||
hasBin: true
|
||||
|
||||
bignumber.js@9.3.1:
|
||||
resolution: {integrity: sha512-Ko0uX15oIUS7wJ3Rb30Fs6SkVbLmPBAKdlm7q9+ak9bbIeFf0MwuBsQV6z7+X768/cHsfg+WlysDWJcmthjsjQ==}
|
||||
|
||||
binary-extensions@2.3.0:
|
||||
resolution: {integrity: sha512-Ceh+7ox5qe7LJuLHoY0feh3pHuUDHAcRUeyL2VYghZwfpkNIy/+8Ocg0a3UuSoYzavmylwuLWQOf3hl0jjMMIw==}
|
||||
engines: {node: '>=8'}
|
||||
|
|
@ -2049,6 +2082,9 @@ packages:
|
|||
bson-objectid@2.0.4:
|
||||
resolution: {integrity: sha512-vgnKAUzcDoa+AeyYwXCoHyF2q6u/8H46dxu5JN+4/TZeq/Dlinn0K6GvxsCLb3LHUJl0m/TLiEK31kUwtgocMQ==}
|
||||
|
||||
buffer-equal-constant-time@1.0.1:
|
||||
resolution: {integrity: sha512-zRpUiDwd/xk6ADqPMATG8vc9VPrkck7T07OIx0gnjmJAnHnTVXNQG3vfvWNuiZIkwu9KrKdA1iJKfsfTVxE6NA==}
|
||||
|
||||
buffer-from@1.1.2:
|
||||
resolution: {integrity: sha512-E+XQCRwSbaaiChtv6k6Dwgc+bx+Bs6vuKJHHl5kox/BaKbhiXzqQOwK4cO22yElGp2OCmjwVhT3HmxgyPGnJfQ==}
|
||||
|
||||
|
|
@ -2192,6 +2228,10 @@ packages:
|
|||
damerau-levenshtein@1.0.8:
|
||||
resolution: {integrity: sha512-sdQSFB7+llfUcQHUQO3+B8ERRj0Oa4w9POWMI/puGtuf7gFywGmkaLCElnudfTiKZV+NvHqL0ifzdrI8Ro7ESA==}
|
||||
|
||||
data-uri-to-buffer@4.0.1:
|
||||
resolution: {integrity: sha512-0R9ikRb668HB7QDxT1vkpuUBtqc53YyAwMwGeUFKRojY/NWKvdZ+9UYtRfGmhqNbRkTSVpMbmyhXipFFv2cb/A==}
|
||||
engines: {node: '>= 12'}
|
||||
|
||||
data-urls@5.0.0:
|
||||
resolution: {integrity: sha512-ZYP5VBHshaDAiVZxjbRVcFJpc+4xGgT0bK3vzy1HLN8jTO975HEbuYzZJcHoQEY5K1a0z8YayJkyVETa08eNTg==}
|
||||
engines: {node: '>=18'}
|
||||
|
|
@ -2391,12 +2431,21 @@ packages:
|
|||
resolution: {integrity: sha512-KIN/nDJBQRcXw0MLVhZE9iQHmG68qAVIBg9CqmUYjmQIhgij9U5MFvrqkUL5FbtyyzZuOeOt0zdeRe4UY7ct+A==}
|
||||
engines: {node: '>= 0.4'}
|
||||
|
||||
eastasianwidth@0.2.0:
|
||||
resolution: {integrity: sha512-I88TYZWc9XiYHRQ4/3c5rjjfgkjhLyW2luGIheGERbNQ6OY7yTybanSpDXZa8y7VUP9YmDcYa+eyq4ca7iLqWA==}
|
||||
|
||||
ecdsa-sig-formatter@1.0.11:
|
||||
resolution: {integrity: sha512-nagl3RYrbNv6kQkeJIpt6NJZy8twLB/2vtz6yN9Z4vRKHN4/QZJIEbqohALSgwKdnksuY3k5Addp5lg8sVoVcQ==}
|
||||
|
||||
electron-to-chromium@1.5.260:
|
||||
resolution: {integrity: sha512-ov8rBoOBhVawpzdre+Cmz4FB+y66Eqrk6Gwqd8NGxuhv99GQ8XqMAr351KEkOt7gukXWDg6gJWEMKgL2RLMPtA==}
|
||||
|
||||
emoji-regex@10.6.0:
|
||||
resolution: {integrity: sha512-toUI84YS5YmxW219erniWD0CIVOo46xGKColeNQRgOzDorgBi1v4D71/OFzgD9GO2UGKIv1C3Sp8DAn0+j5w7A==}
|
||||
|
||||
emoji-regex@8.0.0:
|
||||
resolution: {integrity: sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==}
|
||||
|
||||
emoji-regex@9.2.2:
|
||||
resolution: {integrity: sha512-L18DaJsXSUk2+42pv8mLs5jJT2hqFkFE4j21wOmgbUqsZ2hL72NsUU785g9RXgo3s0ZNgVl42TiHp3ZtOv/Vyg==}
|
||||
|
||||
|
|
@ -2600,6 +2649,9 @@ packages:
|
|||
resolution: {integrity: sha512-JhFGDVJ7tmDJItKhYgJCGLOWjuK9vPxiXoUFLwLDc99NlmklilbiQJwoctZtt13+xMw91MCk/REan6MWHqDjyA==}
|
||||
engines: {node: '>=12.0.0'}
|
||||
|
||||
extend@3.0.2:
|
||||
resolution: {integrity: sha512-fjquC59cD7CyW6urNXK0FBufkZcoiGG80wTuPujX590cB5Ttln20E2UB4S/WARVqhXffZl2LNgS+gQdPIIim/g==}
|
||||
|
||||
fast-copy@3.0.2:
|
||||
resolution: {integrity: sha512-dl0O9Vhju8IrcLndv2eU4ldt1ftXMqqfgN4H1cpmGV7P6jeB9FwpN9a2c8DPGE1Ys88rNUJVYDHq73CGAGOPfQ==}
|
||||
|
||||
|
|
@ -2638,6 +2690,10 @@ packages:
|
|||
picomatch:
|
||||
optional: true
|
||||
|
||||
fetch-blob@3.2.0:
|
||||
resolution: {integrity: sha512-7yAQpD2UMJzLi1Dqv7qFYnPbaPx7ZfFK6PiIxQ4PfkGPyNyl2Ugx+a/umUonmKqjhM4DnfbMvdX6otXq83soQQ==}
|
||||
engines: {node: ^12.20 || >= 14.13}
|
||||
|
||||
file-entry-cache@8.0.0:
|
||||
resolution: {integrity: sha512-XXTUwCvisa5oacNGRP9SfNtYBNAMi+RPwBFmblZEF7N7swHYQS6/Zfk7SRwx4D5j3CH211YNRco1DEMNVfZCnQ==}
|
||||
engines: {node: '>=16.0.0'}
|
||||
|
|
@ -2671,6 +2727,14 @@ packages:
|
|||
resolution: {integrity: sha512-dKx12eRCVIzqCxFGplyFKJMPvLEWgmNtUrpTiJIR5u97zEhRG8ySrtboPHZXx7daLxQVrl643cTzbab2tkQjxg==}
|
||||
engines: {node: '>= 0.4'}
|
||||
|
||||
foreground-child@3.3.1:
|
||||
resolution: {integrity: sha512-gIXjKqtFuWEgzFRJA9WCQeSJLZDjgJUOMCMzxtvFq/37KojM1BFGufqsCy0r4qSQmYLsZYMeyRqzIWOMup03sw==}
|
||||
engines: {node: '>=14'}
|
||||
|
||||
formdata-polyfill@4.0.10:
|
||||
resolution: {integrity: sha512-buewHzMvYL29jdeQTVILecSaZKnt/RJWjoZCF5OW60Z67/GmSLBkOFM7qh1PI3zFNtJbaZL5eQu1vLfazOwj4g==}
|
||||
engines: {node: '>=12.20.0'}
|
||||
|
||||
fsevents@2.3.2:
|
||||
resolution: {integrity: sha512-xiqMQR4xAeHTuB9uWm+fFRcIOgKBMiOBP+eXiyT7jsgVCq1bkVygt00oASowB7EdtpOHaaPgKt812P9ab+DDKA==}
|
||||
engines: {node: ^8.16.0 || ^10.6.0 || >=11.0.0}
|
||||
|
|
@ -2691,6 +2755,14 @@ packages:
|
|||
functions-have-names@1.2.3:
|
||||
resolution: {integrity: sha512-xckBUXyTIqT97tq2x2AMb+g163b5JFysYk0x4qxNFwbfQkmNZoiRHb6sPzI9/QV33WeuvVYBUIiD4NzNIyqaRQ==}
|
||||
|
||||
gaxios@7.1.3:
|
||||
resolution: {integrity: sha512-YGGyuEdVIjqxkxVH1pUTMY/XtmmsApXrCVv5EU25iX6inEPbV+VakJfLealkBtJN69AQmh1eGOdCl9Sm1UP6XQ==}
|
||||
engines: {node: '>=18'}
|
||||
|
||||
gcp-metadata@8.1.2:
|
||||
resolution: {integrity: sha512-zV/5HKTfCeKWnxG0Dmrw51hEWFGfcF2xiXqcA3+J90WDuP0SvoiSO5ORvcBsifmx/FoIjgQN3oNOGaQ5PhLFkg==}
|
||||
engines: {node: '>=18'}
|
||||
|
||||
generator-function@2.0.1:
|
||||
resolution: {integrity: sha512-SFdFmIJi+ybC0vjlHN0ZGVGHc3lgE0DxPAT0djjVg+kjOnSqclqmj0KQ7ykTOLP6YxoqOvuAODGdcHJn+43q3g==}
|
||||
engines: {node: '>= 0.4'}
|
||||
|
|
@ -2733,6 +2805,10 @@ packages:
|
|||
resolution: {integrity: sha512-XxwI8EOhVQgWp6iDL+3b0r86f4d6AX6zSU55HfB4ydCEuXLXc5FcYeOu+nnGftS4TEju/11rt4KJPTMgbfmv4A==}
|
||||
engines: {node: '>=10.13.0'}
|
||||
|
||||
glob@10.5.0:
|
||||
resolution: {integrity: sha512-DfXN8DfhJ7NH3Oe7cFmu3NCu1wKbkReJ8TorzSAFbSKrlNaQSKfIzqYqVY8zlbs2NLBbWpRiU52GX2PbaBVNkg==}
|
||||
hasBin: true
|
||||
|
||||
globals@14.0.0:
|
||||
resolution: {integrity: sha512-oahGvuMGQlPw/ivIYBjVSrWAfWLBeku5tpPE2fOPLi+WHffIWbuh2tCjhyQhTBPMf5E9jDEH4FOmTYgYwbKwtQ==}
|
||||
engines: {node: '>=18'}
|
||||
|
|
@ -2744,6 +2820,22 @@ packages:
|
|||
globrex@0.1.2:
|
||||
resolution: {integrity: sha512-uHJgbwAMwNFf5mLst7IWLNg14x1CkeqglJb/K3doi4dw6q2IvAAmM/Y81kevy83wP+Sst+nutFTYOGg3d1lsxg==}
|
||||
|
||||
google-auth-library@10.5.0:
|
||||
resolution: {integrity: sha512-7ABviyMOlX5hIVD60YOfHw4/CxOfBhyduaYB+wbFWCWoni4N7SLcV46hrVRktuBbZjFC9ONyqamZITN7q3n32w==}
|
||||
engines: {node: '>=18'}
|
||||
|
||||
google-logging-utils@1.1.3:
|
||||
resolution: {integrity: sha512-eAmLkjDjAFCVXg7A1unxHsLf961m6y17QFqXqAXGj/gVkKFrEICfStRfwUlGNfeCEjNRa32JEWOUTlYXPyyKvA==}
|
||||
engines: {node: '>=14'}
|
||||
|
||||
googleapis-common@8.0.1:
|
||||
resolution: {integrity: sha512-eCzNACUXPb1PW5l0ULTzMHaL/ltPRADoPgjBlT8jWsTbxkCp6siv+qKJ/1ldaybCthGwsYFYallF7u9AkU4L+A==}
|
||||
engines: {node: '>=18.0.0'}
|
||||
|
||||
googleapis@170.0.0:
|
||||
resolution: {integrity: sha512-UJz71WZ3ubMr4NhkEU+CFTS0CMrrXq+ltrFnAQo8Llf9M3cy0AIfKLyFQdUJyhqIpJ4jPW4SRCcBBntrLQ72/A==}
|
||||
engines: {node: '>=18'}
|
||||
|
||||
gopd@1.2.0:
|
||||
resolution: {integrity: sha512-ZUKRh6/kUFoAiTAtTYPZJ3hw9wNxx+BIBOijnlG9PnrJsCcSjs1wyyD6vJpaYtgnzDrKYRSqf3OO6Rfa93xsRg==}
|
||||
engines: {node: '>= 0.4'}
|
||||
|
|
@ -2767,6 +2859,10 @@ packages:
|
|||
resolution: {integrity: sha512-DKKrynuQRne0PNpEbzuEdHlYOMksHSUI8Zc9Unei5gTsMNA2/vMpoMz/yKba50pejK56qj98qM0SjYxAKi13gQ==}
|
||||
engines: {node: ^12.22.0 || ^14.16.0 || ^16.0.0 || >=17.0.0}
|
||||
|
||||
gtoken@8.0.0:
|
||||
resolution: {integrity: sha512-+CqsMbHPiSTdtSO14O51eMNlrp9N79gmeqmXeouJOhfucAedHw9noVe/n5uJk3tbKE6a+6ZCQg3RPhVhHByAIw==}
|
||||
engines: {node: '>=18'}
|
||||
|
||||
has-bigints@1.1.0:
|
||||
resolution: {integrity: sha512-R3pbpkcIqv2Pm3dUwgjclDRVmWpTJW2DcMzcIhEXEx1oh/CEMObMm3KLmRJOdvhM7o4uQBnwr8pzRK2sJWIqfg==}
|
||||
engines: {node: '>= 0.4'}
|
||||
|
|
@ -2931,6 +3027,10 @@ packages:
|
|||
resolution: {integrity: sha512-1pC6N8qWJbWoPtEjgcL2xyhQOP491EQjeUo3qTKcmV8YSDDJrOepfG8pcC7h/QgnQHYSv0mJ3Z/ZWxmatVrysg==}
|
||||
engines: {node: '>= 0.4'}
|
||||
|
||||
is-fullwidth-code-point@3.0.0:
|
||||
resolution: {integrity: sha512-zymm5+u+sCsSWyD9qNaejV3DFvhCKclKdizYaJUuHA83RLjb7nSuGnddCHGv0hk+KY7BMAlsWeK4Ueg6EV6XQg==}
|
||||
engines: {node: '>=8'}
|
||||
|
||||
is-generator-function@1.1.2:
|
||||
resolution: {integrity: sha512-upqt1SkGkODW9tsGNG5mtXTXtECizwtS2kA161M+gJPc1xdb/Ax629af6YrTwcOeQHbewrPNlE5Dx7kzvXTizA==}
|
||||
engines: {node: '>= 0.4'}
|
||||
|
|
@ -3026,6 +3126,9 @@ packages:
|
|||
resolution: {integrity: sha512-H0dkQoCa3b2VEeKQBOxFph+JAbcrQdE7KC0UkqwpLmv2EC4P41QXP+rqo9wYodACiG5/WM5s9oDApTU8utwj9g==}
|
||||
engines: {node: '>= 0.4'}
|
||||
|
||||
jackspeak@3.4.3:
|
||||
resolution: {integrity: sha512-OGlZQpz2yfahA/Rd1Y8Cd9SIEsqvXkLVoSw/cgwhnhFMDbsQFeZYoJJ7bIZBS9BcamUW96asq/npPWugM+RQBw==}
|
||||
|
||||
jose@5.9.6:
|
||||
resolution: {integrity: sha512-AMlnetc9+CV9asI19zHmrgS/WYsWUwCn2R7RzlbJWD7F9eWYUTGyBmU9o6PxngtLGOiDGPRu+Uc4fhKzbpteZQ==}
|
||||
|
||||
|
|
@ -3057,12 +3160,19 @@ packages:
|
|||
engines: {node: '>=6'}
|
||||
hasBin: true
|
||||
|
||||
json-bigint@1.0.0:
|
||||
resolution: {integrity: sha512-SiPv/8VpZuWbvLSMtTDU8hEfrZWg/mH/nV/b4o0CYbSxu1UIQPLdwKOCIyLQX+VIPO5vrLX3i8qtqFyhdPSUSQ==}
|
||||
|
||||
json-buffer@3.0.1:
|
||||
resolution: {integrity: sha512-4bV5BfR2mqfQTJm+V5tPPdf+ZpuhiIvTuAB5g8kcrXOZpTT/QwwVRWBywX1ozr6lEuPdbHxwaJlm9G6mI2sfSQ==}
|
||||
|
||||
json-parse-even-better-errors@2.3.1:
|
||||
resolution: {integrity: sha512-xyFwyhro/JEof6Ghe2iz2NcXoj2sloNsWr/XsERDK/oiPCfaNhl5ONfp+jQdAZRQQ0IJWNzH9zIZF7li91kh2w==}
|
||||
|
||||
json-schema-to-ts@3.1.1:
|
||||
resolution: {integrity: sha512-+DWg8jCJG2TEnpy7kOm/7/AxaYoaRbjVB4LFZLySZlWn8exGs3A4OLJR966cVvU26N7X9TWxl+Jsw7dzAqKT6g==}
|
||||
engines: {node: '>=16'}
|
||||
|
||||
json-schema-to-typescript@15.0.3:
|
||||
resolution: {integrity: sha512-iOKdzTUWEVM4nlxpFudFsWyUiu/Jakkga4OZPEt7CGoSEsAsUgdOZqR6pcgx2STBek9Gm4hcarJpXSzIvZ/hKA==}
|
||||
engines: {node: '>=16.0.0'}
|
||||
|
|
@ -3098,6 +3208,12 @@ packages:
|
|||
resolution: {integrity: sha512-ZZow9HBI5O6EPgSJLUb8n2NKgmVWTwCvHGwFuJlMjvLFqlGG6pjirPhtdsseaLZjSibD8eegzmYpUZwoIlj2cQ==}
|
||||
engines: {node: '>=4.0'}
|
||||
|
||||
jwa@2.0.1:
|
||||
resolution: {integrity: sha512-hRF04fqJIP8Abbkq5NKGN0Bbr3JxlQ+qhZufXVr0DvujKy93ZCbXZMHDL4EOtodSbCWxOqR8MS1tXA5hwqCXDg==}
|
||||
|
||||
jws@4.0.1:
|
||||
resolution: {integrity: sha512-EKI/M/yqPncGUUh44xz0PxSidXFr/+r0pA70+gIYhjv+et7yxM+s29Y+VGDkovRofQem0fs7Uvf4+YmAdyRduA==}
|
||||
|
||||
keyv@4.5.4:
|
||||
resolution: {integrity: sha512-oxVHkHR/EJf2CNXnWxRLW6mg7JyCCUcG0DtEGmL2ctUo1PNTin1PUil+r/+4r5MpVgC/fn1kjsx7mjSujKqIpw==}
|
||||
|
||||
|
|
@ -3290,6 +3406,10 @@ packages:
|
|||
minimist@1.2.8:
|
||||
resolution: {integrity: sha512-2yyAR8qBkN3YuheJanUpWC5U3bb5osDywNB8RzDVlDwDHbocAJveqqj1u8+SVD7jkWT4yvsHCpWqqWqAxb0zCA==}
|
||||
|
||||
minipass@7.1.2:
|
||||
resolution: {integrity: sha512-qOOzS1cBTWYF4BH8fVePDBOO9iptMnGUEZwNc/cMWnTV2nVLZ7VoNWEPHkYczZA0pdoA7dl6e7FL659nX9S2aw==}
|
||||
engines: {node: '>=16 || 14 >=14.17'}
|
||||
|
||||
monaco-editor@0.55.1:
|
||||
resolution: {integrity: sha512-jz4x+TJNFHwHtwuV9vA9rMujcZRb0CEilTEwG2rRSpe/A7Jdkuj8xPKttCgOh+v/lkHy7HsZ64oj+q3xoAFl9A==}
|
||||
|
||||
|
|
@ -3348,6 +3468,15 @@ packages:
|
|||
resolution: {integrity: sha512-lgimEHPE/QDgFlywTd8yTR61ptugX3Qer29efeyWw2rv259HtGBNn1vZVmp8lB9uo9wC0t/AT4iGqXxia+CJFg==}
|
||||
engines: {node: '>=6.0.0'}
|
||||
|
||||
node-domexception@1.0.0:
|
||||
resolution: {integrity: sha512-/jKZoMpw0F8GRwl4/eLROPA3cfcXtLApP0QzLmUT/HuPCZWyB7IY9ZrMeKw2O/nFIqPQB3PVM9aYm0F312AXDQ==}
|
||||
engines: {node: '>=10.5.0'}
|
||||
deprecated: Use your platform's native DOMException instead
|
||||
|
||||
node-fetch@3.3.2:
|
||||
resolution: {integrity: sha512-dRB78srN/l6gqWulah9SrxeYnxeddIG30+GOqK/9OlLVyLg3HPnr6SqOWTWOXKRwC2eGYCkZ59NNuSgvSrpgOA==}
|
||||
engines: {node: ^12.20.0 || ^14.13.1 || >=16.0.0}
|
||||
|
||||
node-gyp-build-optional-packages@5.2.2:
|
||||
resolution: {integrity: sha512-s+w+rBWnpTMwSFbaE0UXsRlg7hU4FjekKU4eyAih5T8nJuNZT1nNsskXpxmeqSK9UzkBl6UgRlnKc8hz8IEqOw==}
|
||||
hasBin: true
|
||||
|
|
@ -3433,6 +3562,9 @@ packages:
|
|||
resolution: {integrity: sha512-LaNjtRWUBY++zB5nE/NwcaoMylSPk+S+ZHNB1TzdbMJMny6dynpAGt7X/tl/QYq3TIeE6nxHppbo2LGymrG5Pw==}
|
||||
engines: {node: '>=10'}
|
||||
|
||||
package-json-from-dist@1.0.1:
|
||||
resolution: {integrity: sha512-UEZIS3/by4OC8vL3P2dTXRETpebLI2NiI5vIrjaD/5UtrkFX/tNbwjTSRAGC/+7CAo2pIcBaRgWmcBBHcsaCIw==}
|
||||
|
||||
parent-module@1.0.1:
|
||||
resolution: {integrity: sha512-GQ2EWRpQV8/o+Aw8YqtfZZPfNRWZYkbidE9k5rpl/hC3vtHHBfGm2Ifi6qWV+coDGkrUKZAxE3Lot5kcsRlh+g==}
|
||||
engines: {node: '>=6'}
|
||||
|
|
@ -3458,6 +3590,10 @@ packages:
|
|||
path-parse@1.0.7:
|
||||
resolution: {integrity: sha512-LDJzPVEEEPR+y48z93A0Ed0yXb8pAByGWo/k5YYdYgpY2/2EsOsksJrq7lOHxryrVOn1ejG6oAp8ahvOIQD8sw==}
|
||||
|
||||
path-scurry@1.11.1:
|
||||
resolution: {integrity: sha512-Xa4Nw17FS9ApQFJ9umLiJS4orGjm7ZzwUrwamcGQuHSzDyth9boKDaycYdDcZDuqYATXw4HFXgaqWTctW/v1HA==}
|
||||
engines: {node: '>=16 || 14 >=14.18'}
|
||||
|
||||
path-to-regexp@6.3.0:
|
||||
resolution: {integrity: sha512-Yhpw4T9C6hPpgPeA28us07OJeqZ5EzQTkbfwuhsUg0c237RomFoETJgmp2sa3F/41gfLE6G5cqcYwznmeEeOlQ==}
|
||||
|
||||
|
|
@ -3646,6 +3782,10 @@ packages:
|
|||
resolution: {integrity: sha512-D8NAthKSD7SGn748v+GLaaO6k08Mvpoqroa35PqIQC4gtUa8/Pb/k+r0m0NnGBVbHDP1gKZ2nVywqfMisRhV5A==}
|
||||
engines: {node: '>=18'}
|
||||
|
||||
qs@6.14.1:
|
||||
resolution: {integrity: sha512-4EK3+xJl8Ts67nLYNwqw/dsFVnCf+qR7RgXSK9jEEm9unao3njwMDdmsdvoKBKHzxd7tCYz5e5M+SnMjdtXGQQ==}
|
||||
engines: {node: '>=0.6'}
|
||||
|
||||
queue-microtask@1.2.3:
|
||||
resolution: {integrity: sha512-NuaNSa6flKT5JaSYQzJok04JzTL1CA6aGhv5rfLW3PgqA+M2ChpZQnAC8h8i4ZFkBS8X5RqkDBHA7r4hej3K9A==}
|
||||
|
||||
|
|
@ -3754,6 +3894,10 @@ packages:
|
|||
resolution: {integrity: sha512-g6QUff04oZpHs0eG5p83rFLhHeV00ug/Yf9nZM6fLeUrPguBTkTQOdpAWWspMh55TZfVQDPaN3NQJfbVRAxdIw==}
|
||||
engines: {iojs: '>=1.0.0', node: '>=0.10.0'}
|
||||
|
||||
rimraf@5.0.10:
|
||||
resolution: {integrity: sha512-l0OE8wL34P4nJH/H2ffoaniAokM2qSmrtXHmlpvYr5AVVX8msAyW0l8NVJFDxlSK4u3Uh/f41cQheDVdnYijwQ==}
|
||||
hasBin: true
|
||||
|
||||
rollup@4.53.3:
|
||||
resolution: {integrity: sha512-w8GmOxZfBmKknvdXU1sdM9NHcoQejwF/4mNgj2JuEEdRaHwwF12K7e9eXn1nLZ07ad+du76mkVsyeb2rKGllsA==}
|
||||
engines: {node: '>=18.0.0', npm: '>=8.0.0'}
|
||||
|
|
@ -3769,6 +3913,9 @@ packages:
|
|||
resolution: {integrity: sha512-AURm5f0jYEOydBj7VQlVvDrjeFgthDdEF5H1dP+6mNpoXOMo1quQqJ4wvJDyRZ9+pO3kGWoOdmV08cSv2aJV6Q==}
|
||||
engines: {node: '>=0.4'}
|
||||
|
||||
safe-buffer@5.2.1:
|
||||
resolution: {integrity: sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ==}
|
||||
|
||||
safe-push-apply@1.0.0:
|
||||
resolution: {integrity: sha512-iKE9w/Z7xCzUMIZqdBsp6pEQvwuEebH4vdpjcDWnyzaI6yl6O9FHvVpmGelvEHNsoY6wGblkxR6Zty/h00WiSA==}
|
||||
engines: {node: '>= 0.4'}
|
||||
|
|
@ -3860,6 +4007,10 @@ packages:
|
|||
siginfo@2.0.0:
|
||||
resolution: {integrity: sha512-ybx0WO1/8bSBLEWXZvEd7gMW3Sn3JFlW3TvX1nREbDLRNQNaeNN8WK0meBwPdAaOI7TtRRRJn/Es1zhrrCHu7g==}
|
||||
|
||||
signal-exit@4.1.0:
|
||||
resolution: {integrity: sha512-bzyZ1e88w9O1iNJbKnOlvYTrWPDl46O1bG0D3XInv+9tkPrxrN8jUUTiFlDkkmKWgn1M6CfIA13SuGqOa9Korw==}
|
||||
engines: {node: '>=14'}
|
||||
|
||||
simple-wcswidth@1.1.2:
|
||||
resolution: {integrity: sha512-j7piyCjAeTDSjzTSQ7DokZtMNwNlEAyxqSZeCS+CXH7fJ4jx3FuJ/mTW3mE+6JLs4VJBbcll0Kjn+KXI5t21Iw==}
|
||||
|
||||
|
|
@ -3917,6 +4068,14 @@ packages:
|
|||
resolution: {integrity: sha512-Mcc5wHehp9aXz1ax6bZUyY5afg9u2rv5cqQI3mRrYkGC8rW2hM02jWuwjtL++LS5qinSyhj2QfLyNsuc+VsExg==}
|
||||
engines: {node: '>=10.0.0'}
|
||||
|
||||
string-width@4.2.3:
|
||||
resolution: {integrity: sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==}
|
||||
engines: {node: '>=8'}
|
||||
|
||||
string-width@5.1.2:
|
||||
resolution: {integrity: sha512-HnLOCR3vjcY8beoNLtcjZ5/nxn2afmME6lhrDrebokqMap+XbeW8n9TXpPDOqdGK5qcI3oT0GKTW6wC7EMiVqA==}
|
||||
engines: {node: '>=12'}
|
||||
|
||||
string-width@7.2.0:
|
||||
resolution: {integrity: sha512-tsaTIkKW9b4N+AEj+SVA+WhJzV7/zMhcSu78mLKWSk7cXMOSHsBKFWUs0fWwq8QyK3MgJBQRX6Gbi4kYbdvGkQ==}
|
||||
engines: {node: '>=18'}
|
||||
|
|
@ -3947,6 +4106,10 @@ packages:
|
|||
stringify-entities@4.0.4:
|
||||
resolution: {integrity: sha512-IwfBptatlO+QCJUo19AqvrPNqlVMpW9YEL2LIVY+Rpv2qsjCGxaDLNRgeGsQWJhfItebuJhsGSLjaBbNSQ+ieg==}
|
||||
|
||||
strip-ansi@6.0.1:
|
||||
resolution: {integrity: sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==}
|
||||
engines: {node: '>=8'}
|
||||
|
||||
strip-ansi@7.1.2:
|
||||
resolution: {integrity: sha512-gmBGslpoQJtgnMAvOVqGZpEz9dyoKTCzy2nfz/n8aIFhN/jCE/rCmcxabB6jOOHV+0WNnylOxaxBQPSvcWklhA==}
|
||||
engines: {node: '>=12'}
|
||||
|
|
@ -4053,6 +4216,9 @@ packages:
|
|||
truncate-utf8-bytes@1.0.2:
|
||||
resolution: {integrity: sha512-95Pu1QXQvruGEhv62XCMO3Mm90GscOCClvrIUwCM0PYOXK3kaF3l3sIHxx71ThJfcbM2O5Au6SO3AWCSEfW4mQ==}
|
||||
|
||||
ts-algebra@2.0.0:
|
||||
resolution: {integrity: sha512-FPAhNPFMrkwz76P7cdjdmiShwMynZYN6SgOujD1urY4oNm80Ou9oMdmbR45LotcKOXoy7wSmHkRFE6Mxbrhefw==}
|
||||
|
||||
ts-api-utils@2.1.0:
|
||||
resolution: {integrity: sha512-CUgTZL1irw8u29bzrOD/nH85jqyc74D6SshFgujOIA7osm2Rz7dYH77agkx7H4FBNxDq7Cjf+IjaX/8zwFW+ZQ==}
|
||||
engines: {node: '>=18.12'}
|
||||
|
|
@ -4160,6 +4326,9 @@ packages:
|
|||
uri-js@4.4.1:
|
||||
resolution: {integrity: sha512-7rKUyy33Q1yc98pQ1DAmLtwX109F7TIfWlW1Ydo8Wl1ii1SeHieeh0HHfPeL2fMXK6z0s8ecKs9frCuLJvndBg==}
|
||||
|
||||
url-template@2.0.8:
|
||||
resolution: {integrity: sha512-XdVKMF4SJ0nP/O7XIPB0JwAEuT9lDIYnNsK8yGVe43y0AWoKeJNdv3ZNWh7ksJ6KqQFjOO6ox/VEitLnaVNufw==}
|
||||
|
||||
use-context-selector@2.0.0:
|
||||
resolution: {integrity: sha512-owfuSmUNd3eNp3J9CdDl0kMgfidV+MkDvHPpvthN5ThqM+ibMccNE0k+Iq7TWC6JPFvGZqanqiGCuQx6DyV24g==}
|
||||
peerDependencies:
|
||||
|
|
@ -4282,6 +4451,10 @@ packages:
|
|||
resolution: {integrity: sha512-o8qghlI8NZHU1lLPrpi2+Uq7abh4GGPpYANlalzWxyWteJOCsr/P+oPBA49TOLu5FTZO4d3F9MnWJfiMo4BkmA==}
|
||||
engines: {node: '>=18'}
|
||||
|
||||
web-streams-polyfill@3.3.3:
|
||||
resolution: {integrity: sha512-d2JWLCivmZYTSIoge9MsgFCZrt571BikcWGYkjC1khllbTeDlGqZ2D8vD8E/lJa8WGWbb7Plm8/XJYV7IJHZZw==}
|
||||
engines: {node: '>= 8'}
|
||||
|
||||
webidl-conversions@7.0.0:
|
||||
resolution: {integrity: sha512-VwddBukDzu71offAQR975unBIGqfKZpM+8ZX6ySk8nYhVoo5CYaZyzt3YBvYtRtO+aoGlqxPg/B87NGVZ/fu6g==}
|
||||
engines: {node: '>=12'}
|
||||
|
|
@ -4328,6 +4501,14 @@ packages:
|
|||
resolution: {integrity: sha512-BN22B5eaMMI9UMtjrGd5g5eCYPpCPDUy0FJXbYsaT5zYxjFOckS53SQDE3pWkVoWpHXVb3BrYcEN4Twa55B5cA==}
|
||||
engines: {node: '>=0.10.0'}
|
||||
|
||||
wrap-ansi@7.0.0:
|
||||
resolution: {integrity: sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q==}
|
||||
engines: {node: '>=10'}
|
||||
|
||||
wrap-ansi@8.1.0:
|
||||
resolution: {integrity: sha512-si7QWI6zUMq56bESFvagtmzMdGOtoxfR+Sez11Mobfc7tm+VkUckk9bW2UeffTGVUbOksxmSw0AA2gs8g71NCQ==}
|
||||
engines: {node: '>=12'}
|
||||
|
||||
wrap-ansi@9.0.2:
|
||||
resolution: {integrity: sha512-42AtmgqjV+X1VpdOfyTGOYRi0/zsoLqtXQckTmqTeybT+BDIbM/Guxo7x3pE2vtpr1ok6xRqM9OpBe+Jyoqyww==}
|
||||
engines: {node: '>=18'}
|
||||
|
|
@ -4395,6 +4576,10 @@ packages:
|
|||
|
||||
snapshots:
|
||||
|
||||
'@anthropic-ai/sdk@0.71.2':
|
||||
dependencies:
|
||||
json-schema-to-ts: 3.1.1
|
||||
|
||||
'@apidevtools/json-schema-ref-parser@11.9.3':
|
||||
dependencies:
|
||||
'@jsdevtools/ono': 7.1.3
|
||||
|
|
@ -5418,6 +5603,15 @@ snapshots:
|
|||
|
||||
'@ioredis/commands@1.4.0': {}
|
||||
|
||||
'@isaacs/cliui@8.0.2':
|
||||
dependencies:
|
||||
string-width: 5.1.2
|
||||
string-width-cjs: string-width@4.2.3
|
||||
strip-ansi: 7.1.2
|
||||
strip-ansi-cjs: strip-ansi@6.0.1
|
||||
wrap-ansi: 8.1.0
|
||||
wrap-ansi-cjs: wrap-ansi@7.0.0
|
||||
|
||||
'@jridgewell/gen-mapping@0.3.13':
|
||||
dependencies:
|
||||
'@jridgewell/sourcemap-codec': 1.5.5
|
||||
|
|
@ -5928,6 +6122,9 @@ snapshots:
|
|||
|
||||
'@pinojs/redact@0.4.0': {}
|
||||
|
||||
'@pkgjs/parseargs@0.11.0':
|
||||
optional: true
|
||||
|
||||
'@playwright/test@1.57.0':
|
||||
dependencies:
|
||||
playwright: 1.57.0
|
||||
|
|
@ -6637,6 +6834,8 @@ snapshots:
|
|||
json-schema-traverse: 1.0.0
|
||||
require-from-string: 2.0.2
|
||||
|
||||
ansi-regex@5.0.1: {}
|
||||
|
||||
ansi-regex@6.2.2: {}
|
||||
|
||||
ansi-styles@4.3.0:
|
||||
|
|
@ -6751,8 +6950,12 @@ snapshots:
|
|||
|
||||
balanced-match@1.0.2: {}
|
||||
|
||||
base64-js@1.5.1: {}
|
||||
|
||||
baseline-browser-mapping@2.8.31: {}
|
||||
|
||||
bignumber.js@9.3.1: {}
|
||||
|
||||
binary-extensions@2.3.0: {}
|
||||
|
||||
body-scroll-lock@4.0.0-beta.0: {}
|
||||
|
|
@ -6782,6 +6985,8 @@ snapshots:
|
|||
|
||||
bson-objectid@2.0.4: {}
|
||||
|
||||
buffer-equal-constant-time@1.0.1: {}
|
||||
|
||||
buffer-from@1.1.2: {}
|
||||
|
||||
bullmq@5.65.1:
|
||||
|
|
@ -6925,6 +7130,8 @@ snapshots:
|
|||
|
||||
damerau-levenshtein@1.0.8: {}
|
||||
|
||||
data-uri-to-buffer@4.0.1: {}
|
||||
|
||||
data-urls@5.0.0:
|
||||
dependencies:
|
||||
whatwg-mimetype: 4.0.0
|
||||
|
|
@ -7040,10 +7247,18 @@ snapshots:
|
|||
es-errors: 1.3.0
|
||||
gopd: 1.2.0
|
||||
|
||||
eastasianwidth@0.2.0: {}
|
||||
|
||||
ecdsa-sig-formatter@1.0.11:
|
||||
dependencies:
|
||||
safe-buffer: 5.2.1
|
||||
|
||||
electron-to-chromium@1.5.260: {}
|
||||
|
||||
emoji-regex@10.6.0: {}
|
||||
|
||||
emoji-regex@8.0.0: {}
|
||||
|
||||
emoji-regex@9.2.2: {}
|
||||
|
||||
end-of-stream@1.4.5:
|
||||
|
|
@ -7234,8 +7449,8 @@ snapshots:
|
|||
'@typescript-eslint/parser': 8.49.0(eslint@9.39.2)(typescript@5.9.3)
|
||||
eslint: 9.39.2
|
||||
eslint-import-resolver-node: 0.3.9
|
||||
eslint-import-resolver-typescript: 3.10.1(eslint-plugin-import@2.32.0(@typescript-eslint/parser@8.49.0(eslint@9.39.2)(typescript@5.9.3))(eslint@9.39.2))(eslint@9.39.2)
|
||||
eslint-plugin-import: 2.32.0(@typescript-eslint/parser@8.49.0(eslint@9.39.2)(typescript@5.9.3))(eslint-import-resolver-typescript@3.10.1(eslint-plugin-import@2.32.0(@typescript-eslint/parser@8.49.0(eslint@9.39.2)(typescript@5.9.3))(eslint@9.39.2))(eslint@9.39.2))(eslint@9.39.2)
|
||||
eslint-import-resolver-typescript: 3.10.1(eslint-plugin-import@2.32.0)(eslint@9.39.2)
|
||||
eslint-plugin-import: 2.32.0(@typescript-eslint/parser@8.49.0(eslint@9.39.2)(typescript@5.9.3))(eslint-import-resolver-typescript@3.10.1)(eslint@9.39.2)
|
||||
eslint-plugin-jsx-a11y: 6.10.2(eslint@9.39.2)
|
||||
eslint-plugin-react: 7.37.5(eslint@9.39.2)
|
||||
eslint-plugin-react-hooks: 5.2.0(eslint@9.39.2)
|
||||
|
|
@ -7254,7 +7469,7 @@ snapshots:
|
|||
transitivePeerDependencies:
|
||||
- supports-color
|
||||
|
||||
eslint-import-resolver-typescript@3.10.1(eslint-plugin-import@2.32.0(@typescript-eslint/parser@8.49.0(eslint@9.39.2)(typescript@5.9.3))(eslint@9.39.2))(eslint@9.39.2):
|
||||
eslint-import-resolver-typescript@3.10.1(eslint-plugin-import@2.32.0)(eslint@9.39.2):
|
||||
dependencies:
|
||||
'@nolyfill/is-core-module': 1.0.39
|
||||
debug: 4.4.3
|
||||
|
|
@ -7265,22 +7480,22 @@ snapshots:
|
|||
tinyglobby: 0.2.15
|
||||
unrs-resolver: 1.11.1
|
||||
optionalDependencies:
|
||||
eslint-plugin-import: 2.32.0(@typescript-eslint/parser@8.49.0(eslint@9.39.2)(typescript@5.9.3))(eslint-import-resolver-typescript@3.10.1(eslint-plugin-import@2.32.0(@typescript-eslint/parser@8.49.0(eslint@9.39.2)(typescript@5.9.3))(eslint@9.39.2))(eslint@9.39.2))(eslint@9.39.2)
|
||||
eslint-plugin-import: 2.32.0(@typescript-eslint/parser@8.49.0(eslint@9.39.2)(typescript@5.9.3))(eslint-import-resolver-typescript@3.10.1)(eslint@9.39.2)
|
||||
transitivePeerDependencies:
|
||||
- supports-color
|
||||
|
||||
eslint-module-utils@2.12.1(@typescript-eslint/parser@8.49.0(eslint@9.39.2)(typescript@5.9.3))(eslint-import-resolver-node@0.3.9)(eslint-import-resolver-typescript@3.10.1(eslint-plugin-import@2.32.0(@typescript-eslint/parser@8.49.0(eslint@9.39.2)(typescript@5.9.3))(eslint@9.39.2))(eslint@9.39.2))(eslint@9.39.2):
|
||||
eslint-module-utils@2.12.1(@typescript-eslint/parser@8.49.0(eslint@9.39.2)(typescript@5.9.3))(eslint-import-resolver-node@0.3.9)(eslint-import-resolver-typescript@3.10.1)(eslint@9.39.2):
|
||||
dependencies:
|
||||
debug: 3.2.7
|
||||
optionalDependencies:
|
||||
'@typescript-eslint/parser': 8.49.0(eslint@9.39.2)(typescript@5.9.3)
|
||||
eslint: 9.39.2
|
||||
eslint-import-resolver-node: 0.3.9
|
||||
eslint-import-resolver-typescript: 3.10.1(eslint-plugin-import@2.32.0(@typescript-eslint/parser@8.49.0(eslint@9.39.2)(typescript@5.9.3))(eslint@9.39.2))(eslint@9.39.2)
|
||||
eslint-import-resolver-typescript: 3.10.1(eslint-plugin-import@2.32.0)(eslint@9.39.2)
|
||||
transitivePeerDependencies:
|
||||
- supports-color
|
||||
|
||||
eslint-plugin-import@2.32.0(@typescript-eslint/parser@8.49.0(eslint@9.39.2)(typescript@5.9.3))(eslint-import-resolver-typescript@3.10.1(eslint-plugin-import@2.32.0(@typescript-eslint/parser@8.49.0(eslint@9.39.2)(typescript@5.9.3))(eslint@9.39.2))(eslint@9.39.2))(eslint@9.39.2):
|
||||
eslint-plugin-import@2.32.0(@typescript-eslint/parser@8.49.0(eslint@9.39.2)(typescript@5.9.3))(eslint-import-resolver-typescript@3.10.1)(eslint@9.39.2):
|
||||
dependencies:
|
||||
'@rtsao/scc': 1.1.0
|
||||
array-includes: 3.1.9
|
||||
|
|
@ -7291,7 +7506,7 @@ snapshots:
|
|||
doctrine: 2.1.0
|
||||
eslint: 9.39.2
|
||||
eslint-import-resolver-node: 0.3.9
|
||||
eslint-module-utils: 2.12.1(@typescript-eslint/parser@8.49.0(eslint@9.39.2)(typescript@5.9.3))(eslint-import-resolver-node@0.3.9)(eslint-import-resolver-typescript@3.10.1(eslint-plugin-import@2.32.0(@typescript-eslint/parser@8.49.0(eslint@9.39.2)(typescript@5.9.3))(eslint@9.39.2))(eslint@9.39.2))(eslint@9.39.2)
|
||||
eslint-module-utils: 2.12.1(@typescript-eslint/parser@8.49.0(eslint@9.39.2)(typescript@5.9.3))(eslint-import-resolver-node@0.3.9)(eslint-import-resolver-typescript@3.10.1)(eslint@9.39.2)
|
||||
hasown: 2.0.2
|
||||
is-core-module: 2.16.1
|
||||
is-glob: 4.0.3
|
||||
|
|
@ -7433,6 +7648,8 @@ snapshots:
|
|||
|
||||
expect-type@1.2.2: {}
|
||||
|
||||
extend@3.0.2: {}
|
||||
|
||||
fast-copy@3.0.2: {}
|
||||
|
||||
fast-deep-equal@3.1.3: {}
|
||||
|
|
@ -7465,6 +7682,11 @@ snapshots:
|
|||
optionalDependencies:
|
||||
picomatch: 4.0.3
|
||||
|
||||
fetch-blob@3.2.0:
|
||||
dependencies:
|
||||
node-domexception: 1.0.0
|
||||
web-streams-polyfill: 3.3.3
|
||||
|
||||
file-entry-cache@8.0.0:
|
||||
dependencies:
|
||||
flat-cache: 4.0.1
|
||||
|
|
@ -7501,6 +7723,15 @@ snapshots:
|
|||
dependencies:
|
||||
is-callable: 1.2.7
|
||||
|
||||
foreground-child@3.3.1:
|
||||
dependencies:
|
||||
cross-spawn: 7.0.6
|
||||
signal-exit: 4.1.0
|
||||
|
||||
formdata-polyfill@4.0.10:
|
||||
dependencies:
|
||||
fetch-blob: 3.2.0
|
||||
|
||||
fsevents@2.3.2:
|
||||
optional: true
|
||||
|
||||
|
|
@ -7520,6 +7751,23 @@ snapshots:
|
|||
|
||||
functions-have-names@1.2.3: {}
|
||||
|
||||
gaxios@7.1.3:
|
||||
dependencies:
|
||||
extend: 3.0.2
|
||||
https-proxy-agent: 7.0.6
|
||||
node-fetch: 3.3.2
|
||||
rimraf: 5.0.10
|
||||
transitivePeerDependencies:
|
||||
- supports-color
|
||||
|
||||
gcp-metadata@8.1.2:
|
||||
dependencies:
|
||||
gaxios: 7.1.3
|
||||
google-logging-utils: 1.1.3
|
||||
json-bigint: 1.0.0
|
||||
transitivePeerDependencies:
|
||||
- supports-color
|
||||
|
||||
generator-function@2.0.1: {}
|
||||
|
||||
gensync@1.0.0-beta.2: {}
|
||||
|
|
@ -7568,6 +7816,15 @@ snapshots:
|
|||
dependencies:
|
||||
is-glob: 4.0.3
|
||||
|
||||
glob@10.5.0:
|
||||
dependencies:
|
||||
foreground-child: 3.3.1
|
||||
jackspeak: 3.4.3
|
||||
minimatch: 9.0.5
|
||||
minipass: 7.1.2
|
||||
package-json-from-dist: 1.0.1
|
||||
path-scurry: 1.11.1
|
||||
|
||||
globals@14.0.0: {}
|
||||
|
||||
globalthis@1.0.4:
|
||||
|
|
@ -7577,6 +7834,37 @@ snapshots:
|
|||
|
||||
globrex@0.1.2: {}
|
||||
|
||||
google-auth-library@10.5.0:
|
||||
dependencies:
|
||||
base64-js: 1.5.1
|
||||
ecdsa-sig-formatter: 1.0.11
|
||||
gaxios: 7.1.3
|
||||
gcp-metadata: 8.1.2
|
||||
google-logging-utils: 1.1.3
|
||||
gtoken: 8.0.0
|
||||
jws: 4.0.1
|
||||
transitivePeerDependencies:
|
||||
- supports-color
|
||||
|
||||
google-logging-utils@1.1.3: {}
|
||||
|
||||
googleapis-common@8.0.1:
|
||||
dependencies:
|
||||
extend: 3.0.2
|
||||
gaxios: 7.1.3
|
||||
google-auth-library: 10.5.0
|
||||
qs: 6.14.1
|
||||
url-template: 2.0.8
|
||||
transitivePeerDependencies:
|
||||
- supports-color
|
||||
|
||||
googleapis@170.0.0:
|
||||
dependencies:
|
||||
google-auth-library: 10.5.0
|
||||
googleapis-common: 8.0.1
|
||||
transitivePeerDependencies:
|
||||
- supports-color
|
||||
|
||||
gopd@1.2.0: {}
|
||||
|
||||
graphql-http@1.22.4(graphql@16.12.0):
|
||||
|
|
@ -7594,6 +7882,13 @@ snapshots:
|
|||
|
||||
graphql@16.12.0: {}
|
||||
|
||||
gtoken@8.0.0:
|
||||
dependencies:
|
||||
gaxios: 7.1.3
|
||||
jws: 4.0.1
|
||||
transitivePeerDependencies:
|
||||
- supports-color
|
||||
|
||||
has-bigints@1.1.0: {}
|
||||
|
||||
has-flag@4.0.0: {}
|
||||
|
|
@ -7761,6 +8056,8 @@ snapshots:
|
|||
dependencies:
|
||||
call-bound: 1.0.4
|
||||
|
||||
is-fullwidth-code-point@3.0.0: {}
|
||||
|
||||
is-generator-function@1.1.2:
|
||||
dependencies:
|
||||
call-bound: 1.0.4
|
||||
|
|
@ -7863,6 +8160,12 @@ snapshots:
|
|||
has-symbols: 1.1.0
|
||||
set-function-name: 2.0.2
|
||||
|
||||
jackspeak@3.4.3:
|
||||
dependencies:
|
||||
'@isaacs/cliui': 8.0.2
|
||||
optionalDependencies:
|
||||
'@pkgjs/parseargs': 0.11.0
|
||||
|
||||
jose@5.9.6: {}
|
||||
|
||||
joycon@3.1.1: {}
|
||||
|
|
@ -7904,10 +8207,19 @@ snapshots:
|
|||
|
||||
jsesc@3.1.0: {}
|
||||
|
||||
json-bigint@1.0.0:
|
||||
dependencies:
|
||||
bignumber.js: 9.3.1
|
||||
|
||||
json-buffer@3.0.1: {}
|
||||
|
||||
json-parse-even-better-errors@2.3.1: {}
|
||||
|
||||
json-schema-to-ts@3.1.1:
|
||||
dependencies:
|
||||
'@babel/runtime': 7.28.4
|
||||
ts-algebra: 2.0.0
|
||||
|
||||
json-schema-to-typescript@15.0.3:
|
||||
dependencies:
|
||||
'@apidevtools/json-schema-ref-parser': 11.9.3
|
||||
|
|
@ -7947,6 +8259,17 @@ snapshots:
|
|||
object.assign: 4.1.7
|
||||
object.values: 1.2.1
|
||||
|
||||
jwa@2.0.1:
|
||||
dependencies:
|
||||
buffer-equal-constant-time: 1.0.1
|
||||
ecdsa-sig-formatter: 1.0.11
|
||||
safe-buffer: 5.2.1
|
||||
|
||||
jws@4.0.1:
|
||||
dependencies:
|
||||
jwa: 2.0.1
|
||||
safe-buffer: 5.2.1
|
||||
|
||||
keyv@4.5.4:
|
||||
dependencies:
|
||||
json-buffer: 3.0.1
|
||||
|
|
@ -8265,6 +8588,8 @@ snapshots:
|
|||
|
||||
minimist@1.2.8: {}
|
||||
|
||||
minipass@7.1.2: {}
|
||||
|
||||
monaco-editor@0.55.1:
|
||||
dependencies:
|
||||
dompurify: 3.2.7
|
||||
|
|
@ -8325,6 +8650,14 @@ snapshots:
|
|||
|
||||
node-cron@4.2.1: {}
|
||||
|
||||
node-domexception@1.0.0: {}
|
||||
|
||||
node-fetch@3.3.2:
|
||||
dependencies:
|
||||
data-uri-to-buffer: 4.0.1
|
||||
fetch-blob: 3.2.0
|
||||
formdata-polyfill: 4.0.10
|
||||
|
||||
node-gyp-build-optional-packages@5.2.2:
|
||||
dependencies:
|
||||
detect-libc: 2.1.2
|
||||
|
|
@ -8417,6 +8750,8 @@ snapshots:
|
|||
dependencies:
|
||||
p-limit: 3.1.0
|
||||
|
||||
package-json-from-dist@1.0.1: {}
|
||||
|
||||
parent-module@1.0.1:
|
||||
dependencies:
|
||||
callsites: 3.1.0
|
||||
|
|
@ -8448,6 +8783,11 @@ snapshots:
|
|||
|
||||
path-parse@1.0.7: {}
|
||||
|
||||
path-scurry@1.11.1:
|
||||
dependencies:
|
||||
lru-cache: 10.4.3
|
||||
minipass: 7.1.2
|
||||
|
||||
path-to-regexp@6.3.0: {}
|
||||
|
||||
path-type@4.0.0: {}
|
||||
|
|
@ -8669,6 +9009,10 @@ snapshots:
|
|||
|
||||
qs-esm@7.0.2: {}
|
||||
|
||||
qs@6.14.1:
|
||||
dependencies:
|
||||
side-channel: 1.1.0
|
||||
|
||||
queue-microtask@1.2.3: {}
|
||||
|
||||
quick-format-unescaped@4.0.4: {}
|
||||
|
|
@ -8786,6 +9130,10 @@ snapshots:
|
|||
|
||||
reusify@1.1.0: {}
|
||||
|
||||
rimraf@5.0.10:
|
||||
dependencies:
|
||||
glob: 10.5.0
|
||||
|
||||
rollup@4.53.3:
|
||||
dependencies:
|
||||
'@types/estree': 1.0.8
|
||||
|
|
@ -8828,6 +9176,8 @@ snapshots:
|
|||
has-symbols: 1.1.0
|
||||
isarray: 2.0.5
|
||||
|
||||
safe-buffer@5.2.1: {}
|
||||
|
||||
safe-push-apply@1.0.0:
|
||||
dependencies:
|
||||
es-errors: 1.3.0
|
||||
|
|
@ -8958,6 +9308,8 @@ snapshots:
|
|||
|
||||
siginfo@2.0.0: {}
|
||||
|
||||
signal-exit@4.1.0: {}
|
||||
|
||||
simple-wcswidth@1.1.2: {}
|
||||
|
||||
sisteransi@1.0.5: {}
|
||||
|
|
@ -9001,6 +9353,18 @@ snapshots:
|
|||
|
||||
streamsearch@1.1.0: {}
|
||||
|
||||
string-width@4.2.3:
|
||||
dependencies:
|
||||
emoji-regex: 8.0.0
|
||||
is-fullwidth-code-point: 3.0.0
|
||||
strip-ansi: 6.0.1
|
||||
|
||||
string-width@5.1.2:
|
||||
dependencies:
|
||||
eastasianwidth: 0.2.0
|
||||
emoji-regex: 9.2.2
|
||||
strip-ansi: 7.1.2
|
||||
|
||||
string-width@7.2.0:
|
||||
dependencies:
|
||||
emoji-regex: 10.6.0
|
||||
|
|
@ -9062,6 +9426,10 @@ snapshots:
|
|||
character-entities-html4: 2.1.0
|
||||
character-entities-legacy: 3.0.0
|
||||
|
||||
strip-ansi@6.0.1:
|
||||
dependencies:
|
||||
ansi-regex: 5.0.1
|
||||
|
||||
strip-ansi@7.1.2:
|
||||
dependencies:
|
||||
ansi-regex: 6.2.2
|
||||
|
|
@ -9151,6 +9519,8 @@ snapshots:
|
|||
dependencies:
|
||||
utf8-byte-length: 1.0.5
|
||||
|
||||
ts-algebra@2.0.0: {}
|
||||
|
||||
ts-api-utils@2.1.0(typescript@5.9.3):
|
||||
dependencies:
|
||||
typescript: 5.9.3
|
||||
|
|
@ -9295,6 +9665,8 @@ snapshots:
|
|||
dependencies:
|
||||
punycode: 2.3.1
|
||||
|
||||
url-template@2.0.8: {}
|
||||
|
||||
use-context-selector@2.0.0(react@19.2.3)(scheduler@0.25.0):
|
||||
dependencies:
|
||||
react: 19.2.3
|
||||
|
|
@ -9394,6 +9766,8 @@ snapshots:
|
|||
dependencies:
|
||||
xml-name-validator: 5.0.0
|
||||
|
||||
web-streams-polyfill@3.3.3: {}
|
||||
|
||||
webidl-conversions@7.0.0: {}
|
||||
|
||||
whatwg-encoding@3.1.1:
|
||||
|
|
@ -9459,6 +9833,18 @@ snapshots:
|
|||
|
||||
word-wrap@1.2.5: {}
|
||||
|
||||
wrap-ansi@7.0.0:
|
||||
dependencies:
|
||||
ansi-styles: 4.3.0
|
||||
string-width: 4.2.3
|
||||
strip-ansi: 6.0.1
|
||||
|
||||
wrap-ansi@8.1.0:
|
||||
dependencies:
|
||||
ansi-styles: 6.2.3
|
||||
string-width: 5.1.2
|
||||
strip-ansi: 7.1.2
|
||||
|
||||
wrap-ansi@9.0.2:
|
||||
dependencies:
|
||||
ansi-styles: 6.2.3
|
||||
|
|
|
|||
144
src/app/(payload)/api/community/reply/route.ts
Normal file
144
src/app/(payload)/api/community/reply/route.ts
Normal file
|
|
@ -0,0 +1,144 @@
|
|||
// src/app/(payload)/api/community/reply/route.ts
|
||||
|
||||
import { NextRequest, NextResponse } from 'next/server'
|
||||
import { getPayload } from 'payload'
|
||||
import config from '@payload-config'
|
||||
import { YouTubeClient } from '@/lib/integrations/youtube/YouTubeClient'
|
||||
|
||||
export async function POST(req: NextRequest) {
|
||||
try {
|
||||
const payload = await getPayload({ config })
|
||||
|
||||
// Auth prüfen
|
||||
const { user } = await payload.auth({ headers: req.headers })
|
||||
if (!user) {
|
||||
return NextResponse.json({ error: 'Unauthorized' }, { status: 401 })
|
||||
}
|
||||
|
||||
// Berechtigungen prüfen
|
||||
const userWithRoles = user as {
|
||||
id: number
|
||||
isSuperAdmin?: boolean
|
||||
youtubeRole?: string
|
||||
communityRole?: string
|
||||
}
|
||||
const hasAccess =
|
||||
userWithRoles.isSuperAdmin ||
|
||||
userWithRoles.youtubeRole === 'manager' ||
|
||||
userWithRoles.youtubeRole === 'creator' ||
|
||||
userWithRoles.communityRole === 'manager' ||
|
||||
userWithRoles.communityRole === 'moderator'
|
||||
|
||||
if (!hasAccess) {
|
||||
return NextResponse.json({ error: 'Forbidden' }, { status: 403 })
|
||||
}
|
||||
|
||||
const body = await req.json()
|
||||
const { interactionId, replyText, templateId } = body
|
||||
|
||||
if (!interactionId || !replyText) {
|
||||
return NextResponse.json(
|
||||
{ error: 'interactionId and replyText required' },
|
||||
{ status: 400 }
|
||||
)
|
||||
}
|
||||
|
||||
// 1. Interaction laden
|
||||
const interaction = await payload.findByID({
|
||||
collection: 'community-interactions',
|
||||
id: interactionId,
|
||||
depth: 2,
|
||||
})
|
||||
|
||||
if (!interaction) {
|
||||
return NextResponse.json(
|
||||
{ error: 'Interaction not found' },
|
||||
{ status: 404 }
|
||||
)
|
||||
}
|
||||
|
||||
// 2. Platform prüfen
|
||||
const platform = interaction.platform as { slug?: string }
|
||||
if (platform?.slug !== 'youtube') {
|
||||
return NextResponse.json(
|
||||
{ error: 'Nur YouTube wird derzeit unterstützt' },
|
||||
{ status: 400 }
|
||||
)
|
||||
}
|
||||
|
||||
// 3. YouTube Client initialisieren
|
||||
const account = interaction.socialAccount as {
|
||||
credentials?: {
|
||||
accessToken?: string
|
||||
refreshToken?: string
|
||||
}
|
||||
}
|
||||
|
||||
if (!account?.credentials?.accessToken || !account?.credentials?.refreshToken) {
|
||||
return NextResponse.json(
|
||||
{ error: 'Keine gültigen API-Credentials' },
|
||||
{ status: 400 }
|
||||
)
|
||||
}
|
||||
|
||||
const youtubeClient = new YouTubeClient(
|
||||
{
|
||||
clientId: process.env.YOUTUBE_CLIENT_ID!,
|
||||
clientSecret: process.env.YOUTUBE_CLIENT_SECRET!,
|
||||
accessToken: account.credentials.accessToken,
|
||||
refreshToken: account.credentials.refreshToken,
|
||||
},
|
||||
payload
|
||||
)
|
||||
|
||||
// 4. Reply senden
|
||||
const replyId = await youtubeClient.replyToComment(
|
||||
interaction.externalId as string,
|
||||
replyText
|
||||
)
|
||||
|
||||
// 5. Interaction aktualisieren
|
||||
await payload.update({
|
||||
collection: 'community-interactions',
|
||||
id: interactionId,
|
||||
data: {
|
||||
status: 'replied',
|
||||
response: {
|
||||
text: replyText,
|
||||
usedTemplate: templateId || null,
|
||||
sentAt: new Date().toISOString(),
|
||||
sentBy: userWithRoles.id,
|
||||
externalReplyId: replyId,
|
||||
},
|
||||
},
|
||||
})
|
||||
|
||||
// 6. Template Usage Counter erhöhen
|
||||
if (templateId) {
|
||||
const template = await payload.findByID({
|
||||
collection: 'community-templates',
|
||||
id: templateId,
|
||||
})
|
||||
if (template) {
|
||||
await payload.update({
|
||||
collection: 'community-templates',
|
||||
id: templateId,
|
||||
data: {
|
||||
usageCount: ((template as { usageCount?: number }).usageCount || 0) + 1,
|
||||
},
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
return NextResponse.json({
|
||||
success: true,
|
||||
replyId,
|
||||
})
|
||||
} catch (error: unknown) {
|
||||
console.error('Reply error:', error)
|
||||
const message = error instanceof Error ? error.message : 'Unknown error'
|
||||
return NextResponse.json({ error: message }, { status: 500 })
|
||||
}
|
||||
}
|
||||
|
||||
export const dynamic = 'force-dynamic'
|
||||
61
src/app/(payload)/api/community/sync-comments/route.ts
Normal file
61
src/app/(payload)/api/community/sync-comments/route.ts
Normal file
|
|
@ -0,0 +1,61 @@
|
|||
// src/app/(payload)/api/community/sync-comments/route.ts
|
||||
|
||||
import { NextRequest, NextResponse } from 'next/server'
|
||||
import { getPayload } from 'payload'
|
||||
import config from '@payload-config'
|
||||
import { CommentsSyncService } from '@/lib/integrations/youtube/CommentsSyncService'
|
||||
|
||||
export async function POST(req: NextRequest) {
|
||||
try {
|
||||
const payload = await getPayload({ config })
|
||||
|
||||
// Auth prüfen
|
||||
const { user } = await payload.auth({ headers: req.headers })
|
||||
if (!user) {
|
||||
return NextResponse.json({ error: 'Unauthorized' }, { status: 401 })
|
||||
}
|
||||
|
||||
// Berechtigungen prüfen
|
||||
const userWithRoles = user as {
|
||||
isSuperAdmin?: boolean
|
||||
youtubeRole?: string
|
||||
communityRole?: string
|
||||
}
|
||||
const hasAccess =
|
||||
userWithRoles.isSuperAdmin ||
|
||||
userWithRoles.youtubeRole === 'manager' ||
|
||||
userWithRoles.communityRole === 'manager' ||
|
||||
userWithRoles.communityRole === 'moderator'
|
||||
|
||||
if (!hasAccess) {
|
||||
return NextResponse.json({ error: 'Forbidden' }, { status: 403 })
|
||||
}
|
||||
|
||||
const body = await req.json()
|
||||
const { socialAccountId, sinceDate, maxComments, analyzeWithAI } = body
|
||||
|
||||
if (!socialAccountId) {
|
||||
return NextResponse.json(
|
||||
{ error: 'socialAccountId required' },
|
||||
{ status: 400 }
|
||||
)
|
||||
}
|
||||
|
||||
const syncService = new CommentsSyncService(payload)
|
||||
|
||||
const result = await syncService.syncComments({
|
||||
socialAccountId,
|
||||
sinceDate: sinceDate ? new Date(sinceDate) : undefined,
|
||||
maxComments: maxComments || 100,
|
||||
analyzeWithAI: analyzeWithAI ?? true,
|
||||
})
|
||||
|
||||
return NextResponse.json(result)
|
||||
} catch (error: unknown) {
|
||||
console.error('Sync error:', error)
|
||||
const message = error instanceof Error ? error.message : 'Unknown error'
|
||||
return NextResponse.json({ error: message }, { status: 500 })
|
||||
}
|
||||
}
|
||||
|
||||
export const dynamic = 'force-dynamic'
|
||||
552
src/collections/CommunityInteractions.ts
Normal file
552
src/collections/CommunityInteractions.ts
Normal file
|
|
@ -0,0 +1,552 @@
|
|||
// src/collections/CommunityInteractions.ts
|
||||
|
||||
import type { CollectionConfig } from 'payload'
|
||||
import {
|
||||
isCommunityModeratorOrAbove,
|
||||
isCommunityManager,
|
||||
canAccessAssignedInteractions,
|
||||
} from '../lib/communityAccess'
|
||||
|
||||
/**
|
||||
* CommunityInteractions Collection
|
||||
*
|
||||
* Speichert alle Social Media Interaktionen (Kommentare, DMs, Mentions).
|
||||
* Teil des Community Management Systems.
|
||||
*/
|
||||
export const CommunityInteractions: CollectionConfig = {
|
||||
slug: 'community-interactions',
|
||||
labels: {
|
||||
singular: 'Interaction',
|
||||
plural: 'Interactions',
|
||||
},
|
||||
admin: {
|
||||
group: 'Community',
|
||||
defaultColumns: ['platform', 'type', 'author.name', 'status', 'priority', 'createdAt'],
|
||||
listSearchableFields: ['author.name', 'author.handle', 'message'],
|
||||
pagination: {
|
||||
defaultLimit: 50,
|
||||
},
|
||||
},
|
||||
access: {
|
||||
read: canAccessAssignedInteractions,
|
||||
create: isCommunityModeratorOrAbove,
|
||||
update: canAccessAssignedInteractions,
|
||||
delete: isCommunityManager,
|
||||
},
|
||||
fields: [
|
||||
// === SOURCE ===
|
||||
{
|
||||
type: 'row',
|
||||
fields: [
|
||||
{
|
||||
name: 'platform',
|
||||
type: 'relationship',
|
||||
relationTo: 'social-platforms',
|
||||
required: true,
|
||||
label: 'Plattform',
|
||||
admin: { width: '33%' },
|
||||
},
|
||||
{
|
||||
name: 'socialAccount',
|
||||
type: 'relationship',
|
||||
relationTo: 'social-accounts',
|
||||
required: true,
|
||||
label: 'Account',
|
||||
admin: { width: '33%' },
|
||||
},
|
||||
{
|
||||
name: 'linkedContent',
|
||||
type: 'relationship',
|
||||
relationTo: 'youtube-content',
|
||||
label: 'Verknüpfter Content',
|
||||
admin: { width: '33%' },
|
||||
},
|
||||
],
|
||||
},
|
||||
|
||||
// === INTERACTION TYPE ===
|
||||
{
|
||||
type: 'row',
|
||||
fields: [
|
||||
{
|
||||
name: 'type',
|
||||
type: 'select',
|
||||
required: true,
|
||||
label: 'Typ',
|
||||
options: [
|
||||
{ label: 'Kommentar', value: 'comment' },
|
||||
{ label: 'Antwort', value: 'reply' },
|
||||
{ label: 'Direktnachricht', value: 'dm' },
|
||||
{ label: 'Erwähnung', value: 'mention' },
|
||||
{ label: 'Bewertung', value: 'review' },
|
||||
{ label: 'Frage', value: 'question' },
|
||||
],
|
||||
admin: { width: '50%' },
|
||||
},
|
||||
{
|
||||
name: 'externalId',
|
||||
type: 'text',
|
||||
label: 'External ID',
|
||||
required: true,
|
||||
unique: true,
|
||||
index: true,
|
||||
admin: {
|
||||
width: '50%',
|
||||
description: 'YouTube Comment ID, etc.',
|
||||
},
|
||||
},
|
||||
],
|
||||
},
|
||||
|
||||
// === PARENT (für Threads) ===
|
||||
{
|
||||
name: 'parentInteraction',
|
||||
type: 'relationship',
|
||||
relationTo: 'community-interactions',
|
||||
label: 'Parent (bei Replies)',
|
||||
admin: {
|
||||
condition: (data) => data?.type === 'reply',
|
||||
},
|
||||
},
|
||||
|
||||
// === AUTHOR INFO ===
|
||||
{
|
||||
name: 'author',
|
||||
type: 'group',
|
||||
label: 'Author',
|
||||
fields: [
|
||||
{
|
||||
type: 'row',
|
||||
fields: [
|
||||
{
|
||||
name: 'name',
|
||||
type: 'text',
|
||||
label: 'Name',
|
||||
admin: { width: '50%' },
|
||||
},
|
||||
{
|
||||
name: 'handle',
|
||||
type: 'text',
|
||||
label: 'Handle',
|
||||
admin: { width: '50%' },
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
type: 'row',
|
||||
fields: [
|
||||
{
|
||||
name: 'profileUrl',
|
||||
type: 'text',
|
||||
label: 'Profile URL',
|
||||
admin: { width: '50%' },
|
||||
},
|
||||
{
|
||||
name: 'avatarUrl',
|
||||
type: 'text',
|
||||
label: 'Avatar URL',
|
||||
admin: { width: '50%' },
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
type: 'row',
|
||||
fields: [
|
||||
{
|
||||
name: 'isVerified',
|
||||
type: 'checkbox',
|
||||
label: 'Verifiziert',
|
||||
admin: { width: '25%' },
|
||||
},
|
||||
{
|
||||
name: 'isSubscriber',
|
||||
type: 'checkbox',
|
||||
label: 'Subscriber/Follower',
|
||||
admin: { width: '25%' },
|
||||
},
|
||||
{
|
||||
name: 'isMember',
|
||||
type: 'checkbox',
|
||||
label: 'Channel Member',
|
||||
admin: { width: '25%' },
|
||||
},
|
||||
{
|
||||
name: 'subscriberCount',
|
||||
type: 'number',
|
||||
label: 'Ihre Subscriber',
|
||||
admin: { width: '25%' },
|
||||
},
|
||||
],
|
||||
},
|
||||
],
|
||||
},
|
||||
|
||||
// === MESSAGE CONTENT ===
|
||||
{
|
||||
name: 'message',
|
||||
type: 'textarea',
|
||||
label: 'Nachricht',
|
||||
required: true,
|
||||
admin: {
|
||||
rows: 4,
|
||||
},
|
||||
},
|
||||
{
|
||||
name: 'messageHtml',
|
||||
type: 'textarea',
|
||||
label: 'Original HTML',
|
||||
admin: {
|
||||
rows: 2,
|
||||
description: 'Falls Plattform HTML liefert',
|
||||
},
|
||||
},
|
||||
{
|
||||
name: 'attachments',
|
||||
type: 'array',
|
||||
label: 'Attachments',
|
||||
fields: [
|
||||
{
|
||||
type: 'row',
|
||||
fields: [
|
||||
{
|
||||
name: 'type',
|
||||
type: 'select',
|
||||
label: 'Typ',
|
||||
options: [
|
||||
{ label: 'Bild', value: 'image' },
|
||||
{ label: 'Video', value: 'video' },
|
||||
{ label: 'Link', value: 'link' },
|
||||
{ label: 'Sticker', value: 'sticker' },
|
||||
],
|
||||
admin: { width: '30%' },
|
||||
},
|
||||
{
|
||||
name: 'url',
|
||||
type: 'text',
|
||||
label: 'URL',
|
||||
admin: { width: '70%' },
|
||||
},
|
||||
],
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
name: 'publishedAt',
|
||||
type: 'date',
|
||||
label: 'Veröffentlicht am',
|
||||
required: true,
|
||||
admin: {
|
||||
date: {
|
||||
pickerAppearance: 'dayAndTime',
|
||||
},
|
||||
},
|
||||
},
|
||||
|
||||
// === AI ANALYSIS ===
|
||||
{
|
||||
name: 'analysis',
|
||||
type: 'group',
|
||||
label: 'AI Analyse',
|
||||
admin: {
|
||||
description: 'Automatisch via Claude API',
|
||||
},
|
||||
fields: [
|
||||
{
|
||||
type: 'row',
|
||||
fields: [
|
||||
{
|
||||
name: 'sentiment',
|
||||
type: 'select',
|
||||
label: 'Sentiment',
|
||||
options: [
|
||||
{ label: 'Positiv', value: 'positive' },
|
||||
{ label: 'Neutral', value: 'neutral' },
|
||||
{ label: 'Negativ', value: 'negative' },
|
||||
{ label: 'Frage', value: 'question' },
|
||||
{ label: 'Dankbarkeit', value: 'gratitude' },
|
||||
{ label: 'Frustration', value: 'frustration' },
|
||||
],
|
||||
admin: { width: '33%' },
|
||||
},
|
||||
{
|
||||
name: 'sentimentScore',
|
||||
type: 'number',
|
||||
label: 'Score (-1 bis 1)',
|
||||
min: -1,
|
||||
max: 1,
|
||||
admin: { width: '33%' },
|
||||
},
|
||||
{
|
||||
name: 'confidence',
|
||||
type: 'number',
|
||||
label: 'Confidence %',
|
||||
min: 0,
|
||||
max: 100,
|
||||
admin: { width: '33%' },
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
name: 'topics',
|
||||
type: 'array',
|
||||
label: 'Erkannte Themen',
|
||||
fields: [
|
||||
{
|
||||
name: 'topic',
|
||||
type: 'text',
|
||||
label: 'Thema',
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
name: 'language',
|
||||
type: 'text',
|
||||
label: 'Sprache',
|
||||
admin: {
|
||||
placeholder: 'de',
|
||||
},
|
||||
},
|
||||
{
|
||||
name: 'suggestedTemplate',
|
||||
type: 'relationship',
|
||||
relationTo: 'community-templates',
|
||||
label: 'Vorgeschlagenes Template',
|
||||
},
|
||||
{
|
||||
name: 'suggestedReply',
|
||||
type: 'textarea',
|
||||
label: 'AI-generierter Antwortvorschlag',
|
||||
},
|
||||
{
|
||||
name: 'analyzedAt',
|
||||
type: 'date',
|
||||
label: 'Analysiert am',
|
||||
},
|
||||
],
|
||||
},
|
||||
|
||||
// === FLAGS ===
|
||||
{
|
||||
name: 'flags',
|
||||
type: 'group',
|
||||
label: 'Flags',
|
||||
fields: [
|
||||
{
|
||||
type: 'row',
|
||||
fields: [
|
||||
{
|
||||
name: 'isMedicalQuestion',
|
||||
type: 'checkbox',
|
||||
label: 'Medizinische Frage',
|
||||
admin: {
|
||||
width: '25%',
|
||||
description: 'Erfordert ärztliche Review',
|
||||
},
|
||||
},
|
||||
{
|
||||
name: 'requiresEscalation',
|
||||
type: 'checkbox',
|
||||
label: 'Eskalation nötig',
|
||||
admin: { width: '25%' },
|
||||
},
|
||||
{
|
||||
name: 'isSpam',
|
||||
type: 'checkbox',
|
||||
label: 'Spam',
|
||||
admin: { width: '25%' },
|
||||
},
|
||||
{
|
||||
name: 'isFromInfluencer',
|
||||
type: 'checkbox',
|
||||
label: 'Influencer',
|
||||
admin: {
|
||||
width: '25%',
|
||||
description: '>10k Follower',
|
||||
},
|
||||
},
|
||||
],
|
||||
},
|
||||
],
|
||||
},
|
||||
|
||||
// === WORKFLOW ===
|
||||
{
|
||||
name: 'status',
|
||||
type: 'select',
|
||||
required: true,
|
||||
defaultValue: 'new',
|
||||
index: true,
|
||||
label: 'Status',
|
||||
options: [
|
||||
{ label: 'Neu', value: 'new' },
|
||||
{ label: 'In Review', value: 'in_review' },
|
||||
{ label: 'Warten auf Info', value: 'waiting' },
|
||||
{ label: 'Beantwortet', value: 'replied' },
|
||||
{ label: 'Erledigt', value: 'resolved' },
|
||||
{ label: 'Archiviert', value: 'archived' },
|
||||
{ label: 'Spam', value: 'spam' },
|
||||
],
|
||||
admin: {
|
||||
position: 'sidebar',
|
||||
},
|
||||
},
|
||||
{
|
||||
name: 'priority',
|
||||
type: 'select',
|
||||
required: true,
|
||||
defaultValue: 'normal',
|
||||
index: true,
|
||||
label: 'Priorität',
|
||||
options: [
|
||||
{ label: 'Urgent', value: 'urgent' },
|
||||
{ label: 'Hoch', value: 'high' },
|
||||
{ label: 'Normal', value: 'normal' },
|
||||
{ label: 'Niedrig', value: 'low' },
|
||||
],
|
||||
admin: {
|
||||
position: 'sidebar',
|
||||
},
|
||||
},
|
||||
{
|
||||
name: 'assignedTo',
|
||||
type: 'relationship',
|
||||
relationTo: 'users',
|
||||
label: 'Zugewiesen an',
|
||||
admin: {
|
||||
position: 'sidebar',
|
||||
},
|
||||
},
|
||||
{
|
||||
name: 'responseDeadline',
|
||||
type: 'date',
|
||||
label: 'Antwort-Deadline',
|
||||
admin: {
|
||||
position: 'sidebar',
|
||||
date: {
|
||||
pickerAppearance: 'dayAndTime',
|
||||
},
|
||||
},
|
||||
},
|
||||
|
||||
// === OUR RESPONSE ===
|
||||
{
|
||||
name: 'response',
|
||||
type: 'group',
|
||||
label: 'Unsere Antwort',
|
||||
fields: [
|
||||
{
|
||||
name: 'text',
|
||||
type: 'textarea',
|
||||
label: 'Antwort-Text',
|
||||
admin: { rows: 4 },
|
||||
},
|
||||
{
|
||||
name: 'usedTemplate',
|
||||
type: 'relationship',
|
||||
relationTo: 'community-templates',
|
||||
label: 'Verwendetes Template',
|
||||
},
|
||||
{
|
||||
name: 'sentAt',
|
||||
type: 'date',
|
||||
label: 'Gesendet am',
|
||||
},
|
||||
{
|
||||
name: 'sentBy',
|
||||
type: 'relationship',
|
||||
relationTo: 'users',
|
||||
label: 'Gesendet von',
|
||||
},
|
||||
{
|
||||
name: 'externalReplyId',
|
||||
type: 'text',
|
||||
label: 'Reply ID (extern)',
|
||||
},
|
||||
],
|
||||
},
|
||||
|
||||
// === ENGAGEMENT (Platform-spezifisch) ===
|
||||
{
|
||||
name: 'engagement',
|
||||
type: 'group',
|
||||
label: 'Engagement',
|
||||
admin: {
|
||||
description: 'Wird beim Sync aktualisiert',
|
||||
},
|
||||
fields: [
|
||||
{
|
||||
type: 'row',
|
||||
fields: [
|
||||
{
|
||||
name: 'likes',
|
||||
type: 'number',
|
||||
label: 'Likes',
|
||||
admin: { width: '25%' },
|
||||
},
|
||||
{
|
||||
name: 'replies',
|
||||
type: 'number',
|
||||
label: 'Replies',
|
||||
admin: { width: '25%' },
|
||||
},
|
||||
{
|
||||
name: 'isHearted',
|
||||
type: 'checkbox',
|
||||
label: 'Creator Heart',
|
||||
admin: { width: '25%' },
|
||||
},
|
||||
{
|
||||
name: 'isPinned',
|
||||
type: 'checkbox',
|
||||
label: 'Angepinnt',
|
||||
admin: { width: '25%' },
|
||||
},
|
||||
],
|
||||
},
|
||||
],
|
||||
},
|
||||
|
||||
// === INTERNAL NOTES ===
|
||||
{
|
||||
name: 'internalNotes',
|
||||
type: 'textarea',
|
||||
label: 'Interne Notizen',
|
||||
admin: {
|
||||
rows: 2,
|
||||
description: 'Nur für Team sichtbar',
|
||||
},
|
||||
},
|
||||
],
|
||||
|
||||
// === HOOKS ===
|
||||
hooks: {
|
||||
beforeChange: [
|
||||
// Auto-set priority based on flags
|
||||
async ({ data, operation }) => {
|
||||
if (!data) return data
|
||||
if (operation === 'create' || !data.priority) {
|
||||
if (data?.flags?.isMedicalQuestion) {
|
||||
data.priority = 'high'
|
||||
}
|
||||
if (data?.flags?.requiresEscalation) {
|
||||
data.priority = 'urgent'
|
||||
}
|
||||
if (data?.flags?.isFromInfluencer) {
|
||||
data.priority = data.priority === 'urgent' ? 'urgent' : 'high'
|
||||
}
|
||||
}
|
||||
return data
|
||||
},
|
||||
],
|
||||
afterChange: [
|
||||
// Send notification for urgent items
|
||||
async ({ doc, operation }) => {
|
||||
if (operation === 'create' && doc.priority === 'urgent') {
|
||||
// TODO: Notification Logic via YtNotifications
|
||||
console.log(`🚨 Urgent interaction: ${doc.id}`)
|
||||
}
|
||||
},
|
||||
],
|
||||
},
|
||||
timestamps: true,
|
||||
}
|
||||
262
src/collections/CommunityRules.ts
Normal file
262
src/collections/CommunityRules.ts
Normal file
|
|
@ -0,0 +1,262 @@
|
|||
// src/collections/CommunityRules.ts
|
||||
|
||||
import type { CollectionConfig } from 'payload'
|
||||
import { isCommunityManager, hasCommunityAccess } from '../lib/communityAccess'
|
||||
|
||||
/**
|
||||
* CommunityRules Collection
|
||||
*
|
||||
* Auto-Regeln für Community-Interaktionen basierend auf Keywords, Sentiment etc.
|
||||
* Teil des Community Management Systems.
|
||||
*/
|
||||
export const CommunityRules: CollectionConfig = {
|
||||
slug: 'community-rules',
|
||||
labels: {
|
||||
singular: 'Community Rule',
|
||||
plural: 'Community Rules',
|
||||
},
|
||||
admin: {
|
||||
group: 'Community',
|
||||
useAsTitle: 'name',
|
||||
defaultColumns: ['name', 'trigger.type', 'isActive', 'priority'],
|
||||
},
|
||||
access: {
|
||||
read: hasCommunityAccess,
|
||||
create: isCommunityManager,
|
||||
update: isCommunityManager,
|
||||
delete: isCommunityManager,
|
||||
},
|
||||
fields: [
|
||||
{
|
||||
type: 'row',
|
||||
fields: [
|
||||
{
|
||||
name: 'name',
|
||||
type: 'text',
|
||||
required: true,
|
||||
label: 'Name',
|
||||
admin: { width: '50%' },
|
||||
},
|
||||
{
|
||||
name: 'priority',
|
||||
type: 'number',
|
||||
required: true,
|
||||
defaultValue: 100,
|
||||
label: 'Priorität',
|
||||
admin: {
|
||||
width: '25%',
|
||||
description: 'Niedrigere Zahl = höhere Priorität',
|
||||
},
|
||||
},
|
||||
{
|
||||
name: 'isActive',
|
||||
type: 'checkbox',
|
||||
label: 'Aktiv',
|
||||
defaultValue: true,
|
||||
admin: { width: '25%' },
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
name: 'description',
|
||||
type: 'textarea',
|
||||
label: 'Beschreibung',
|
||||
admin: { rows: 2 },
|
||||
},
|
||||
|
||||
// Scope
|
||||
{
|
||||
type: 'row',
|
||||
fields: [
|
||||
{
|
||||
name: 'channel',
|
||||
type: 'relationship',
|
||||
relationTo: 'youtube-channels',
|
||||
label: 'Kanal (optional)',
|
||||
admin: {
|
||||
width: '50%',
|
||||
description: 'Leer = alle Kanäle',
|
||||
},
|
||||
},
|
||||
{
|
||||
name: 'platforms',
|
||||
type: 'relationship',
|
||||
relationTo: 'social-platforms',
|
||||
hasMany: true,
|
||||
label: 'Plattformen',
|
||||
admin: {
|
||||
width: '50%',
|
||||
description: 'Leer = alle Plattformen',
|
||||
},
|
||||
},
|
||||
],
|
||||
},
|
||||
|
||||
// Trigger
|
||||
{
|
||||
name: 'trigger',
|
||||
type: 'group',
|
||||
label: 'Trigger',
|
||||
fields: [
|
||||
{
|
||||
name: 'type',
|
||||
type: 'select',
|
||||
required: true,
|
||||
label: 'Trigger-Typ',
|
||||
options: [
|
||||
{ label: 'Keyword Match', value: 'keyword' },
|
||||
{ label: 'Sentiment', value: 'sentiment' },
|
||||
{ label: 'Frage erkannt', value: 'question_detected' },
|
||||
{ label: 'Medizinisch erkannt', value: 'medical_detected' },
|
||||
{ label: 'Influencer', value: 'influencer' },
|
||||
{ label: 'Alle neuen', value: 'all_new' },
|
||||
{ label: 'Enthält Link', value: 'contains_link' },
|
||||
{ label: 'Enthält Email', value: 'contains_email' },
|
||||
],
|
||||
},
|
||||
{
|
||||
name: 'keywords',
|
||||
type: 'array',
|
||||
label: 'Keywords',
|
||||
admin: {
|
||||
condition: (data, siblingData) => siblingData?.type === 'keyword',
|
||||
},
|
||||
fields: [
|
||||
{
|
||||
name: 'keyword',
|
||||
type: 'text',
|
||||
required: true,
|
||||
label: 'Keyword',
|
||||
},
|
||||
{
|
||||
name: 'matchType',
|
||||
type: 'select',
|
||||
label: 'Match-Typ',
|
||||
options: [
|
||||
{ label: 'Enthält', value: 'contains' },
|
||||
{ label: 'Exakt', value: 'exact' },
|
||||
{ label: 'Regex', value: 'regex' },
|
||||
],
|
||||
defaultValue: 'contains',
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
name: 'sentimentValues',
|
||||
type: 'select',
|
||||
hasMany: true,
|
||||
label: 'Sentiment-Werte',
|
||||
options: [
|
||||
{ label: 'Positiv', value: 'positive' },
|
||||
{ label: 'Negativ', value: 'negative' },
|
||||
{ label: 'Neutral', value: 'neutral' },
|
||||
{ label: 'Frage', value: 'question' },
|
||||
],
|
||||
admin: {
|
||||
condition: (data, siblingData) => siblingData?.type === 'sentiment',
|
||||
},
|
||||
},
|
||||
{
|
||||
name: 'influencerMinFollowers',
|
||||
type: 'number',
|
||||
label: 'Min. Follower',
|
||||
defaultValue: 10000,
|
||||
admin: {
|
||||
condition: (data, siblingData) => siblingData?.type === 'influencer',
|
||||
},
|
||||
},
|
||||
],
|
||||
},
|
||||
|
||||
// Actions
|
||||
{
|
||||
name: 'actions',
|
||||
type: 'array',
|
||||
label: 'Aktionen',
|
||||
required: true,
|
||||
minRows: 1,
|
||||
fields: [
|
||||
{
|
||||
type: 'row',
|
||||
fields: [
|
||||
{
|
||||
name: 'action',
|
||||
type: 'select',
|
||||
required: true,
|
||||
label: 'Aktion',
|
||||
options: [
|
||||
{ label: 'Priorität setzen', value: 'set_priority' },
|
||||
{ label: 'Zuweisen', value: 'assign_to' },
|
||||
{ label: 'Flag setzen', value: 'set_flag' },
|
||||
{ label: 'Template vorschlagen', value: 'suggest_template' },
|
||||
{ label: 'Notification senden', value: 'send_notification' },
|
||||
{ label: 'Medical Flag', value: 'flag_medical' },
|
||||
{ label: 'Eskalieren', value: 'escalate' },
|
||||
{ label: 'Als Spam markieren', value: 'mark_spam' },
|
||||
{ label: 'Deadline setzen', value: 'set_deadline' },
|
||||
],
|
||||
admin: { width: '40%' },
|
||||
},
|
||||
{
|
||||
name: 'value',
|
||||
type: 'text',
|
||||
label: 'Wert',
|
||||
admin: {
|
||||
width: '40%',
|
||||
description: 'Priority: urgent/high/normal/low, Deadline: Stunden',
|
||||
},
|
||||
},
|
||||
{
|
||||
name: 'targetUser',
|
||||
type: 'relationship',
|
||||
relationTo: 'users',
|
||||
label: 'User',
|
||||
admin: {
|
||||
width: '20%',
|
||||
condition: (data, siblingData) =>
|
||||
['assign_to', 'send_notification'].includes(siblingData?.action || ''),
|
||||
},
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
name: 'targetTemplate',
|
||||
type: 'relationship',
|
||||
relationTo: 'community-templates',
|
||||
label: 'Template',
|
||||
admin: {
|
||||
condition: (data, siblingData) => siblingData?.action === 'suggest_template',
|
||||
},
|
||||
},
|
||||
],
|
||||
},
|
||||
|
||||
// Stats
|
||||
{
|
||||
name: 'stats',
|
||||
type: 'group',
|
||||
label: 'Statistiken',
|
||||
fields: [
|
||||
{
|
||||
type: 'row',
|
||||
fields: [
|
||||
{
|
||||
name: 'timesTriggered',
|
||||
type: 'number',
|
||||
label: 'Ausgelöst',
|
||||
defaultValue: 0,
|
||||
admin: { width: '50%', readOnly: true },
|
||||
},
|
||||
{
|
||||
name: 'lastTriggeredAt',
|
||||
type: 'date',
|
||||
label: 'Zuletzt ausgelöst',
|
||||
admin: { width: '50%', readOnly: true },
|
||||
},
|
||||
],
|
||||
},
|
||||
],
|
||||
},
|
||||
],
|
||||
timestamps: true,
|
||||
}
|
||||
214
src/collections/CommunityTemplates.ts
Normal file
214
src/collections/CommunityTemplates.ts
Normal file
|
|
@ -0,0 +1,214 @@
|
|||
// src/collections/CommunityTemplates.ts
|
||||
|
||||
import type { CollectionConfig } from 'payload'
|
||||
import {
|
||||
isCommunityManager,
|
||||
isCommunityModeratorOrAbove,
|
||||
hasCommunityAccess,
|
||||
} from '../lib/communityAccess'
|
||||
|
||||
/**
|
||||
* CommunityTemplates Collection
|
||||
*
|
||||
* Antwort-Vorlagen für Community-Interaktionen mit Variablen.
|
||||
* Teil des Community Management Systems.
|
||||
*/
|
||||
export const CommunityTemplates: CollectionConfig = {
|
||||
slug: 'community-templates',
|
||||
labels: {
|
||||
singular: 'Response Template',
|
||||
plural: 'Response Templates',
|
||||
},
|
||||
admin: {
|
||||
group: 'Community',
|
||||
useAsTitle: 'name',
|
||||
defaultColumns: ['name', 'category', 'channel', 'usageCount'],
|
||||
},
|
||||
access: {
|
||||
read: hasCommunityAccess,
|
||||
create: isCommunityModeratorOrAbove,
|
||||
update: isCommunityModeratorOrAbove,
|
||||
delete: isCommunityManager,
|
||||
},
|
||||
fields: [
|
||||
{
|
||||
type: 'row',
|
||||
fields: [
|
||||
{
|
||||
name: 'name',
|
||||
type: 'text',
|
||||
required: true,
|
||||
localized: true,
|
||||
label: 'Name',
|
||||
admin: { width: '50%' },
|
||||
},
|
||||
{
|
||||
name: 'category',
|
||||
type: 'select',
|
||||
required: true,
|
||||
label: 'Kategorie',
|
||||
options: [
|
||||
{ label: 'Danke', value: 'thank_you' },
|
||||
{ label: 'Frage beantworten', value: 'question_answer' },
|
||||
{ label: 'Hotline-Verweis', value: 'redirect_hotline' },
|
||||
{ label: 'Medizinischer Disclaimer', value: 'medical_disclaimer' },
|
||||
{ label: 'Produkt-Info', value: 'product_info' },
|
||||
{ label: 'Content-Verweis', value: 'content_reference' },
|
||||
{ label: 'Follow-up', value: 'follow_up' },
|
||||
{ label: 'Negatives Feedback', value: 'negative_feedback' },
|
||||
{ label: 'Spam-Antwort', value: 'spam_response' },
|
||||
{ label: 'Begrüßung', value: 'welcome' },
|
||||
],
|
||||
admin: { width: '50%' },
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
type: 'row',
|
||||
fields: [
|
||||
{
|
||||
name: 'channel',
|
||||
type: 'relationship',
|
||||
relationTo: 'youtube-channels',
|
||||
label: 'Kanal (optional)',
|
||||
admin: {
|
||||
width: '50%',
|
||||
description: 'Leer = für alle Kanäle',
|
||||
},
|
||||
},
|
||||
{
|
||||
name: 'platforms',
|
||||
type: 'relationship',
|
||||
relationTo: 'social-platforms',
|
||||
hasMany: true,
|
||||
label: 'Plattformen',
|
||||
admin: {
|
||||
width: '50%',
|
||||
description: 'Leer = für alle Plattformen',
|
||||
},
|
||||
},
|
||||
],
|
||||
},
|
||||
|
||||
// Template Text mit Variablen
|
||||
{
|
||||
name: 'template',
|
||||
type: 'textarea',
|
||||
required: true,
|
||||
localized: true,
|
||||
label: 'Template Text',
|
||||
admin: {
|
||||
rows: 6,
|
||||
description:
|
||||
'Variablen: {{author_name}}, {{video_title}}, {{channel_name}}, {{hotline_number}}',
|
||||
},
|
||||
},
|
||||
|
||||
// Verfügbare Variablen
|
||||
{
|
||||
name: 'variables',
|
||||
type: 'array',
|
||||
label: 'Verfügbare Variablen',
|
||||
admin: {
|
||||
description: 'Dokumentation der Variablen in diesem Template',
|
||||
},
|
||||
fields: [
|
||||
{
|
||||
type: 'row',
|
||||
fields: [
|
||||
{
|
||||
name: 'variable',
|
||||
type: 'text',
|
||||
required: true,
|
||||
label: 'Variable',
|
||||
admin: {
|
||||
width: '30%',
|
||||
placeholder: '{{author_name}}',
|
||||
},
|
||||
},
|
||||
{
|
||||
name: 'description',
|
||||
type: 'text',
|
||||
label: 'Beschreibung',
|
||||
admin: {
|
||||
width: '50%',
|
||||
placeholder: 'Name des Kommentar-Autors',
|
||||
},
|
||||
},
|
||||
{
|
||||
name: 'defaultValue',
|
||||
type: 'text',
|
||||
label: 'Fallback',
|
||||
admin: {
|
||||
width: '20%',
|
||||
},
|
||||
},
|
||||
],
|
||||
},
|
||||
],
|
||||
},
|
||||
|
||||
// Auto-Suggest Keywords
|
||||
{
|
||||
name: 'autoSuggestKeywords',
|
||||
type: 'array',
|
||||
label: 'Auto-Suggest Keywords',
|
||||
admin: {
|
||||
description: 'Bei diesen Keywords wird das Template vorgeschlagen',
|
||||
},
|
||||
fields: [
|
||||
{
|
||||
name: 'keyword',
|
||||
type: 'text',
|
||||
required: true,
|
||||
label: 'Keyword',
|
||||
},
|
||||
],
|
||||
},
|
||||
|
||||
// Flags
|
||||
{
|
||||
type: 'row',
|
||||
fields: [
|
||||
{
|
||||
name: 'requiresReview',
|
||||
type: 'checkbox',
|
||||
label: 'Review erforderlich',
|
||||
admin: {
|
||||
width: '33%',
|
||||
description: 'Für medizinische Antworten',
|
||||
},
|
||||
},
|
||||
{
|
||||
name: 'isActive',
|
||||
type: 'checkbox',
|
||||
label: 'Aktiv',
|
||||
defaultValue: true,
|
||||
admin: { width: '33%' },
|
||||
},
|
||||
{
|
||||
name: 'usageCount',
|
||||
type: 'number',
|
||||
label: 'Verwendungen',
|
||||
defaultValue: 0,
|
||||
admin: {
|
||||
width: '33%',
|
||||
readOnly: true,
|
||||
},
|
||||
},
|
||||
],
|
||||
},
|
||||
|
||||
// Beispiel-Output
|
||||
{
|
||||
name: 'exampleOutput',
|
||||
type: 'textarea',
|
||||
label: 'Beispiel-Output',
|
||||
admin: {
|
||||
rows: 3,
|
||||
description: 'So sieht die Antwort mit ausgefüllten Variablen aus',
|
||||
},
|
||||
},
|
||||
],
|
||||
timestamps: true,
|
||||
}
|
||||
216
src/collections/SocialAccounts.ts
Normal file
216
src/collections/SocialAccounts.ts
Normal file
|
|
@ -0,0 +1,216 @@
|
|||
// src/collections/SocialAccounts.ts
|
||||
|
||||
import type { CollectionConfig } from 'payload'
|
||||
import { isCommunityManager, hasCommunityAccess } from '../lib/communityAccess'
|
||||
|
||||
/**
|
||||
* SocialAccounts Collection
|
||||
*
|
||||
* Verknüpft Social Media Accounts mit YouTube-Kanälen und speichert API-Credentials.
|
||||
* Teil des Community Management Systems.
|
||||
*/
|
||||
export const SocialAccounts: CollectionConfig = {
|
||||
slug: 'social-accounts',
|
||||
labels: {
|
||||
singular: 'Social Account',
|
||||
plural: 'Social Accounts',
|
||||
},
|
||||
admin: {
|
||||
group: 'Community',
|
||||
useAsTitle: 'displayName',
|
||||
defaultColumns: ['displayName', 'platform', 'linkedChannel', 'isActive'],
|
||||
},
|
||||
access: {
|
||||
read: hasCommunityAccess,
|
||||
create: isCommunityManager,
|
||||
update: isCommunityManager,
|
||||
delete: isCommunityManager,
|
||||
},
|
||||
fields: [
|
||||
{
|
||||
type: 'row',
|
||||
fields: [
|
||||
{
|
||||
name: 'platform',
|
||||
type: 'relationship',
|
||||
relationTo: 'social-platforms',
|
||||
required: true,
|
||||
label: 'Plattform',
|
||||
admin: { width: '50%' },
|
||||
},
|
||||
{
|
||||
name: 'linkedChannel',
|
||||
type: 'relationship',
|
||||
relationTo: 'youtube-channels',
|
||||
label: 'Verknüpfter YouTube-Kanal',
|
||||
admin: {
|
||||
width: '50%',
|
||||
description: 'Für Zuordnung zu Brand/Kanal',
|
||||
},
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
type: 'row',
|
||||
fields: [
|
||||
{
|
||||
name: 'displayName',
|
||||
type: 'text',
|
||||
required: true,
|
||||
label: 'Anzeigename',
|
||||
admin: {
|
||||
width: '50%',
|
||||
placeholder: 'BlogWoman YouTube',
|
||||
},
|
||||
},
|
||||
{
|
||||
name: 'accountHandle',
|
||||
type: 'text',
|
||||
label: 'Handle / Username',
|
||||
admin: {
|
||||
width: '50%',
|
||||
placeholder: '@blogwoman',
|
||||
},
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
type: 'row',
|
||||
fields: [
|
||||
{
|
||||
name: 'externalId',
|
||||
type: 'text',
|
||||
label: 'Platform Account ID',
|
||||
admin: {
|
||||
width: '50%',
|
||||
description: 'YouTube Channel ID, LinkedIn URN, etc.',
|
||||
},
|
||||
},
|
||||
{
|
||||
name: 'accountUrl',
|
||||
type: 'text',
|
||||
label: 'Account URL',
|
||||
admin: { width: '50%' },
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
name: 'isActive',
|
||||
type: 'checkbox',
|
||||
label: 'Aktiv',
|
||||
defaultValue: true,
|
||||
admin: {
|
||||
position: 'sidebar',
|
||||
},
|
||||
},
|
||||
|
||||
// OAuth Credentials (verschlüsselt speichern!)
|
||||
{
|
||||
name: 'credentials',
|
||||
type: 'group',
|
||||
label: 'API Credentials',
|
||||
admin: {
|
||||
description: 'Sensible Daten – nur für Super-Admins sichtbar',
|
||||
condition: (data, siblingData, { user }) =>
|
||||
Boolean((user as any)?.isSuperAdmin || (user as any)?.is_super_admin),
|
||||
},
|
||||
fields: [
|
||||
{
|
||||
name: 'accessToken',
|
||||
type: 'text',
|
||||
label: 'Access Token',
|
||||
admin: {
|
||||
description: 'OAuth Access Token',
|
||||
},
|
||||
},
|
||||
{
|
||||
name: 'refreshToken',
|
||||
type: 'text',
|
||||
label: 'Refresh Token',
|
||||
},
|
||||
{
|
||||
name: 'tokenExpiresAt',
|
||||
type: 'date',
|
||||
label: 'Token Ablauf',
|
||||
},
|
||||
{
|
||||
name: 'apiKey',
|
||||
type: 'text',
|
||||
label: 'API Key',
|
||||
admin: {
|
||||
description: 'Für API-Key basierte Auth',
|
||||
},
|
||||
},
|
||||
],
|
||||
},
|
||||
|
||||
// Stats (periodisch aktualisiert)
|
||||
{
|
||||
name: 'stats',
|
||||
type: 'group',
|
||||
label: 'Account Stats',
|
||||
admin: {
|
||||
position: 'sidebar',
|
||||
},
|
||||
fields: [
|
||||
{
|
||||
name: 'followers',
|
||||
type: 'number',
|
||||
label: 'Followers/Subscribers',
|
||||
admin: { readOnly: true },
|
||||
},
|
||||
{
|
||||
name: 'totalPosts',
|
||||
type: 'number',
|
||||
label: 'Total Posts/Videos',
|
||||
admin: { readOnly: true },
|
||||
},
|
||||
{
|
||||
name: 'lastSyncedAt',
|
||||
type: 'date',
|
||||
label: 'Letzter Sync',
|
||||
admin: { readOnly: true },
|
||||
},
|
||||
],
|
||||
},
|
||||
|
||||
// Sync Settings
|
||||
{
|
||||
name: 'syncSettings',
|
||||
type: 'group',
|
||||
label: 'Sync Settings',
|
||||
fields: [
|
||||
{
|
||||
name: 'autoSyncEnabled',
|
||||
type: 'checkbox',
|
||||
label: 'Auto-Sync aktiviert',
|
||||
defaultValue: true,
|
||||
},
|
||||
{
|
||||
name: 'syncIntervalMinutes',
|
||||
type: 'number',
|
||||
label: 'Sync-Intervall (Minuten)',
|
||||
defaultValue: 15,
|
||||
min: 5,
|
||||
max: 1440,
|
||||
},
|
||||
{
|
||||
name: 'syncComments',
|
||||
type: 'checkbox',
|
||||
label: 'Kommentare synchronisieren',
|
||||
defaultValue: true,
|
||||
},
|
||||
{
|
||||
name: 'syncDMs',
|
||||
type: 'checkbox',
|
||||
label: 'DMs synchronisieren',
|
||||
defaultValue: false,
|
||||
admin: {
|
||||
description: 'Nicht alle Plattformen unterstützen DM-API',
|
||||
},
|
||||
},
|
||||
],
|
||||
},
|
||||
],
|
||||
timestamps: true,
|
||||
}
|
||||
231
src/collections/SocialPlatforms.ts
Normal file
231
src/collections/SocialPlatforms.ts
Normal file
|
|
@ -0,0 +1,231 @@
|
|||
// src/collections/SocialPlatforms.ts
|
||||
|
||||
import type { CollectionConfig } from 'payload'
|
||||
import { isCommunityManager, hasCommunityAccess } from '../lib/communityAccess'
|
||||
|
||||
/**
|
||||
* SocialPlatforms Collection
|
||||
*
|
||||
* Definiert unterstützte Social Media Plattformen mit API-Konfiguration.
|
||||
* Teil des Community Management Systems.
|
||||
*/
|
||||
export const SocialPlatforms: CollectionConfig = {
|
||||
slug: 'social-platforms',
|
||||
labels: {
|
||||
singular: 'Social Platform',
|
||||
plural: 'Social Platforms',
|
||||
},
|
||||
admin: {
|
||||
group: 'Community',
|
||||
useAsTitle: 'name',
|
||||
defaultColumns: ['name', 'slug', 'isActive', 'apiStatus'],
|
||||
},
|
||||
access: {
|
||||
read: hasCommunityAccess,
|
||||
create: isCommunityManager,
|
||||
update: isCommunityManager,
|
||||
delete: isCommunityManager,
|
||||
},
|
||||
fields: [
|
||||
{
|
||||
type: 'row',
|
||||
fields: [
|
||||
{
|
||||
name: 'name',
|
||||
type: 'text',
|
||||
required: true,
|
||||
label: 'Name',
|
||||
admin: { width: '50%' },
|
||||
},
|
||||
{
|
||||
name: 'slug',
|
||||
type: 'text',
|
||||
required: true,
|
||||
unique: true,
|
||||
label: 'Slug',
|
||||
admin: { width: '50%' },
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
type: 'row',
|
||||
fields: [
|
||||
{
|
||||
name: 'icon',
|
||||
type: 'text',
|
||||
label: 'Icon (Emoji)',
|
||||
admin: {
|
||||
width: '25%',
|
||||
placeholder: '📺',
|
||||
},
|
||||
},
|
||||
{
|
||||
name: 'color',
|
||||
type: 'text',
|
||||
label: 'Brand Color',
|
||||
admin: {
|
||||
width: '25%',
|
||||
placeholder: '#FF0000',
|
||||
},
|
||||
},
|
||||
{
|
||||
name: 'isActive',
|
||||
type: 'checkbox',
|
||||
label: 'Aktiv',
|
||||
defaultValue: true,
|
||||
admin: { width: '25%' },
|
||||
},
|
||||
{
|
||||
name: 'apiStatus',
|
||||
type: 'select',
|
||||
label: 'API Status',
|
||||
options: [
|
||||
{ label: 'Verbunden', value: 'connected' },
|
||||
{ label: 'Eingeschränkt', value: 'limited' },
|
||||
{ label: 'Nicht verbunden', value: 'disconnected' },
|
||||
{ label: 'In Entwicklung', value: 'development' },
|
||||
],
|
||||
defaultValue: 'disconnected',
|
||||
admin: { width: '25%' },
|
||||
},
|
||||
],
|
||||
},
|
||||
|
||||
// API Configuration
|
||||
{
|
||||
name: 'apiConfig',
|
||||
type: 'group',
|
||||
label: 'API Konfiguration',
|
||||
admin: {
|
||||
condition: (data) => data?.isActive,
|
||||
},
|
||||
fields: [
|
||||
{
|
||||
name: 'apiType',
|
||||
type: 'select',
|
||||
label: 'API Type',
|
||||
options: [
|
||||
{ label: 'YouTube Data API v3', value: 'youtube_v3' },
|
||||
{ label: 'LinkedIn API', value: 'linkedin' },
|
||||
{ label: 'Instagram Graph API', value: 'instagram_graph' },
|
||||
{ label: 'Facebook Graph API', value: 'facebook_graph' },
|
||||
{ label: 'Custom/Webhook', value: 'custom' },
|
||||
],
|
||||
},
|
||||
{
|
||||
name: 'baseUrl',
|
||||
type: 'text',
|
||||
label: 'Base URL',
|
||||
admin: {
|
||||
placeholder: 'https://www.googleapis.com/youtube/v3',
|
||||
},
|
||||
},
|
||||
{
|
||||
name: 'authType',
|
||||
type: 'select',
|
||||
label: 'Auth Type',
|
||||
options: [
|
||||
{ label: 'OAuth 2.0', value: 'oauth2' },
|
||||
{ label: 'API Key', value: 'api_key' },
|
||||
{ label: 'Bearer Token', value: 'bearer' },
|
||||
],
|
||||
},
|
||||
{
|
||||
name: 'scopes',
|
||||
type: 'array',
|
||||
label: 'OAuth Scopes',
|
||||
admin: {
|
||||
condition: (data, siblingData) => siblingData?.authType === 'oauth2',
|
||||
},
|
||||
fields: [{ name: 'scope', type: 'text', label: 'Scope' }],
|
||||
},
|
||||
],
|
||||
},
|
||||
|
||||
// Interaction Types für diese Plattform
|
||||
{
|
||||
name: 'interactionTypes',
|
||||
type: 'array',
|
||||
label: 'Interaction Types',
|
||||
fields: [
|
||||
{
|
||||
type: 'row',
|
||||
fields: [
|
||||
{
|
||||
name: 'type',
|
||||
type: 'text',
|
||||
required: true,
|
||||
label: 'Type',
|
||||
admin: {
|
||||
width: '30%',
|
||||
placeholder: 'comment',
|
||||
},
|
||||
},
|
||||
{
|
||||
name: 'label',
|
||||
type: 'text',
|
||||
required: true,
|
||||
label: 'Label',
|
||||
admin: {
|
||||
width: '30%',
|
||||
placeholder: 'Kommentar',
|
||||
},
|
||||
},
|
||||
{
|
||||
name: 'icon',
|
||||
type: 'text',
|
||||
label: 'Icon',
|
||||
admin: {
|
||||
width: '20%',
|
||||
placeholder: '💬',
|
||||
},
|
||||
},
|
||||
{
|
||||
name: 'canReply',
|
||||
type: 'checkbox',
|
||||
label: 'Reply möglich',
|
||||
defaultValue: true,
|
||||
admin: { width: '20%' },
|
||||
},
|
||||
],
|
||||
},
|
||||
],
|
||||
},
|
||||
|
||||
// Rate Limits
|
||||
{
|
||||
name: 'rateLimits',
|
||||
type: 'group',
|
||||
label: 'Rate Limits',
|
||||
fields: [
|
||||
{
|
||||
type: 'row',
|
||||
fields: [
|
||||
{
|
||||
name: 'requestsPerMinute',
|
||||
type: 'number',
|
||||
label: 'Requests/Minute',
|
||||
admin: { width: '33%' },
|
||||
},
|
||||
{
|
||||
name: 'requestsPerDay',
|
||||
type: 'number',
|
||||
label: 'Requests/Tag',
|
||||
admin: { width: '33%' },
|
||||
},
|
||||
{
|
||||
name: 'quotaUnitsPerDay',
|
||||
type: 'number',
|
||||
label: 'Quota Units/Tag',
|
||||
admin: {
|
||||
width: '33%',
|
||||
description: 'YouTube: 10.000/Tag',
|
||||
},
|
||||
},
|
||||
],
|
||||
},
|
||||
],
|
||||
},
|
||||
],
|
||||
timestamps: true,
|
||||
}
|
||||
|
|
@ -90,5 +90,22 @@ export const Users: CollectionConfig = {
|
|||
condition: (data) => data?.youtubeRole && data.youtubeRole !== 'none',
|
||||
},
|
||||
},
|
||||
// Community Management Felder
|
||||
{
|
||||
name: 'communityRole',
|
||||
type: 'select',
|
||||
label: 'Community-Rolle',
|
||||
defaultValue: 'none',
|
||||
options: [
|
||||
{ label: 'Kein Zugriff', value: 'none' },
|
||||
{ label: 'Viewer (nur Lesen)', value: 'viewer' },
|
||||
{ label: 'Moderator', value: 'moderator' },
|
||||
{ label: 'Manager (Vollzugriff)', value: 'manager' },
|
||||
],
|
||||
admin: {
|
||||
position: 'sidebar',
|
||||
description: 'Zugriff auf Community Management Features',
|
||||
},
|
||||
},
|
||||
],
|
||||
}
|
||||
|
|
|
|||
|
|
@ -40,7 +40,8 @@ export const auditUserAfterChange: CollectionAfterChangeHook = async ({
|
|||
return sanitized
|
||||
}
|
||||
|
||||
await logUserChange(
|
||||
// Fire-and-forget: Nicht auf Audit-Log warten, um Save nicht zu blockieren
|
||||
logUserChange(
|
||||
req.payload,
|
||||
doc.id,
|
||||
operation,
|
||||
|
|
@ -51,7 +52,9 @@ export const auditUserAfterChange: CollectionAfterChangeHook = async ({
|
|||
newValue: sanitizeDoc(doc),
|
||||
},
|
||||
req,
|
||||
)
|
||||
).catch((error) => {
|
||||
console.error('[AuditHook] Error logging user change:', error)
|
||||
})
|
||||
|
||||
return doc
|
||||
}
|
||||
|
|
@ -70,7 +73,8 @@ export const auditUserAfterDelete: CollectionAfterDeleteHook = async ({ doc, req
|
|||
delete sanitizedDoc.salt
|
||||
delete sanitizedDoc.password
|
||||
|
||||
await logUserChange(
|
||||
// Fire-and-forget: Nicht auf Audit-Log warten
|
||||
logUserChange(
|
||||
req.payload,
|
||||
doc.id,
|
||||
'delete',
|
||||
|
|
@ -80,7 +84,9 @@ export const auditUserAfterDelete: CollectionAfterDeleteHook = async ({ doc, req
|
|||
previousValue: sanitizedDoc,
|
||||
},
|
||||
req,
|
||||
)
|
||||
).catch((error) => {
|
||||
console.error('[AuditHook] Error logging user delete:', error)
|
||||
})
|
||||
|
||||
return doc
|
||||
}
|
||||
|
|
|
|||
84
src/lib/communityAccess.ts
Normal file
84
src/lib/communityAccess.ts
Normal file
|
|
@ -0,0 +1,84 @@
|
|||
// src/lib/communityAccess.ts
|
||||
|
||||
import type { Access } from 'payload'
|
||||
|
||||
interface UserWithRoles {
|
||||
id: number
|
||||
isSuperAdmin?: boolean
|
||||
is_super_admin?: boolean
|
||||
youtubeRole?: 'none' | 'viewer' | 'editor' | 'producer' | 'creator' | 'manager'
|
||||
youtube_role?: 'none' | 'viewer' | 'editor' | 'producer' | 'creator' | 'manager'
|
||||
communityRole?: 'none' | 'viewer' | 'moderator' | 'manager'
|
||||
community_role?: 'none' | 'viewer' | 'moderator' | 'manager'
|
||||
}
|
||||
|
||||
const checkIsSuperAdmin = (user: UserWithRoles | null): boolean => {
|
||||
if (!user) return false
|
||||
return Boolean(user.isSuperAdmin || user.is_super_admin)
|
||||
}
|
||||
|
||||
const getCommunityRole = (user: UserWithRoles | null): string | undefined => {
|
||||
if (!user) return undefined
|
||||
return user.communityRole || user.community_role
|
||||
}
|
||||
|
||||
const getYouTubeRole = (user: UserWithRoles | null): string | undefined => {
|
||||
if (!user) return undefined
|
||||
return user.youtubeRole || user.youtube_role
|
||||
}
|
||||
|
||||
/**
|
||||
* Prüft ob User Community-Manager oder Super-Admin ist
|
||||
*/
|
||||
export const isCommunityManager: Access = ({ req }) => {
|
||||
const user = req.user as UserWithRoles | null
|
||||
if (!user) return false
|
||||
if (checkIsSuperAdmin(user)) return true
|
||||
// YouTube-Manager haben auch Community-Zugriff
|
||||
if (getYouTubeRole(user) === 'manager') return true
|
||||
return getCommunityRole(user) === 'manager'
|
||||
}
|
||||
|
||||
/**
|
||||
* Prüft ob User mindestens Moderator-Rechte hat
|
||||
*/
|
||||
export const isCommunityModeratorOrAbove: Access = ({ req }) => {
|
||||
const user = req.user as UserWithRoles | null
|
||||
if (!user) return false
|
||||
if (checkIsSuperAdmin(user)) return true
|
||||
if (['manager', 'creator'].includes(getYouTubeRole(user) || '')) return true
|
||||
return ['moderator', 'manager'].includes(getCommunityRole(user) || '')
|
||||
}
|
||||
|
||||
/**
|
||||
* Prüft ob User Zugriff auf Community-Features hat (mindestens Viewer)
|
||||
*/
|
||||
export const hasCommunityAccess: Access = ({ req }) => {
|
||||
const user = req.user as UserWithRoles | null
|
||||
if (!user) return false
|
||||
if (checkIsSuperAdmin(user)) return true
|
||||
// YouTube-Zugriff impliziert Community-Lesezugriff
|
||||
const ytRole = getYouTubeRole(user)
|
||||
if (ytRole && ytRole !== 'none') return true
|
||||
const commRole = getCommunityRole(user)
|
||||
return commRole !== 'none' && commRole !== undefined
|
||||
}
|
||||
|
||||
/**
|
||||
* Zugriff auf zugewiesene Interactions
|
||||
*/
|
||||
export const canAccessAssignedInteractions: Access = ({ req }) => {
|
||||
const user = req.user as UserWithRoles | null
|
||||
if (!user) return false
|
||||
if (checkIsSuperAdmin(user)) return true
|
||||
if (getYouTubeRole(user) === 'manager') return true
|
||||
if (getCommunityRole(user) === 'manager') return true
|
||||
|
||||
// Für andere Rollen: Nur zugewiesene Interactions
|
||||
return {
|
||||
or: [
|
||||
{ assignedTo: { equals: user.id } },
|
||||
{ 'response.sentBy': { equals: user.id } },
|
||||
],
|
||||
}
|
||||
}
|
||||
167
src/lib/integrations/claude/ClaudeAnalysisService.ts
Normal file
167
src/lib/integrations/claude/ClaudeAnalysisService.ts
Normal file
|
|
@ -0,0 +1,167 @@
|
|||
// src/lib/integrations/claude/ClaudeAnalysisService.ts
|
||||
|
||||
import Anthropic from '@anthropic-ai/sdk'
|
||||
|
||||
interface CommentAnalysis {
|
||||
sentiment: 'positive' | 'neutral' | 'negative' | 'question' | 'gratitude' | 'frustration'
|
||||
sentimentScore: number
|
||||
confidence: number
|
||||
topics: string[]
|
||||
language: string
|
||||
isMedicalQuestion: boolean
|
||||
requiresEscalation: boolean
|
||||
isSpam: boolean
|
||||
suggestedReply?: string
|
||||
}
|
||||
|
||||
interface ReplyContext {
|
||||
videoTitle: string
|
||||
channelName: string
|
||||
isBusinessChannel: boolean
|
||||
template?: string
|
||||
}
|
||||
|
||||
export class ClaudeAnalysisService {
|
||||
private client: Anthropic
|
||||
|
||||
constructor() {
|
||||
this.client = new Anthropic({
|
||||
apiKey: process.env.ANTHROPIC_API_KEY,
|
||||
})
|
||||
}
|
||||
|
||||
/**
|
||||
* Kommentar analysieren
|
||||
*/
|
||||
async analyzeComment(message: string): Promise<CommentAnalysis> {
|
||||
const systemPrompt = `Du bist ein Analyse-Assistent für YouTube-Kommentare eines deutschen Healthcare-Unternehmens (Medizinische Zweitmeinung) und eines Lifestyle-Kanals.
|
||||
|
||||
Analysiere den Kommentar und gib ein JSON-Objekt zurück mit:
|
||||
- sentiment: "positive", "neutral", "negative", "question", "gratitude", "frustration"
|
||||
- sentimentScore: Zahl von -1 (sehr negativ) bis 1 (sehr positiv)
|
||||
- confidence: Konfidenz der Analyse 0-100
|
||||
- topics: Array von erkannten Themen (max 3)
|
||||
- language: ISO-639-1 Sprachcode (z.B. "de", "en")
|
||||
- isMedicalQuestion: true wenn es um medizinische Fragen/Gesundheit geht
|
||||
- requiresEscalation: true wenn dringend/kritisch/negativ oder Beschwerden
|
||||
- isSpam: true wenn Spam/Werbung/Bot
|
||||
- suggestedReply: Kurzer Antwortvorschlag auf Deutsch (optional, nur wenn sinnvoll)
|
||||
|
||||
WICHTIG für isMedicalQuestion:
|
||||
- Fragen zu Intensivmedizin, Diagnosen, Behandlungen, Medikamenten = true
|
||||
- Fragen zu Angehörigen von Patienten = true
|
||||
- Allgemeine Lifestyle-Fragen (Mode, Zeitmanagement) = false
|
||||
|
||||
Antworte NUR mit dem JSON-Objekt, kein anderer Text.`
|
||||
|
||||
try {
|
||||
const response = await this.client.messages.create({
|
||||
model: 'claude-3-haiku-20240307',
|
||||
max_tokens: 500,
|
||||
system: systemPrompt,
|
||||
messages: [
|
||||
{
|
||||
role: 'user',
|
||||
content: `Analysiere diesen Kommentar:\n\n"${message}"`,
|
||||
},
|
||||
],
|
||||
})
|
||||
|
||||
const content = response.content[0]
|
||||
if (content.type !== 'text') {
|
||||
throw new Error('Unexpected response type')
|
||||
}
|
||||
|
||||
const analysis = JSON.parse(content.text) as CommentAnalysis
|
||||
return analysis
|
||||
} catch (error) {
|
||||
console.error('Claude analysis error:', error)
|
||||
|
||||
// Fallback bei Fehler
|
||||
return {
|
||||
sentiment: 'neutral',
|
||||
sentimentScore: 0,
|
||||
confidence: 0,
|
||||
topics: [],
|
||||
language: 'de',
|
||||
isMedicalQuestion: false,
|
||||
requiresEscalation: false,
|
||||
isSpam: false,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Antwort-Vorschlag generieren
|
||||
*/
|
||||
async generateReply(comment: string, context: ReplyContext): Promise<string> {
|
||||
const systemPrompt = `Du bist ein Community-Manager für ${context.channelName}.
|
||||
${
|
||||
context.isBusinessChannel
|
||||
? 'Dies ist ein Healthcare-Kanal für medizinische Zweitmeinungen. Antworten müssen professionell sein und dürfen keine medizinischen Ratschläge geben. Bei medizinischen Fragen immer auf die Hotline verweisen.'
|
||||
: 'Dies ist ein Lifestyle-Kanal für berufstätige Mütter. Antworten sollten warm, persönlich und hilfreich sein.'
|
||||
}
|
||||
|
||||
Erstelle eine passende, kurze Antwort auf den Kommentar.
|
||||
- Maximal 2-3 Sätze
|
||||
- Persönlich und authentisch
|
||||
- Auf Deutsch
|
||||
${context.template ? `\nVerwende dieses Template als Basis:\n${context.template}` : ''}
|
||||
|
||||
Antworte NUR mit dem Antworttext, kein anderer Text.`
|
||||
|
||||
try {
|
||||
const response = await this.client.messages.create({
|
||||
model: 'claude-3-haiku-20240307',
|
||||
max_tokens: 200,
|
||||
system: systemPrompt,
|
||||
messages: [
|
||||
{
|
||||
role: 'user',
|
||||
content: `Video: "${context.videoTitle}"\n\nKommentar: "${comment}"\n\nErstelle eine Antwort:`,
|
||||
},
|
||||
],
|
||||
})
|
||||
|
||||
const content = response.content[0]
|
||||
if (content.type !== 'text') {
|
||||
throw new Error('Unexpected response type')
|
||||
}
|
||||
|
||||
return content.text.trim()
|
||||
} catch (error) {
|
||||
console.error('Claude reply generation error:', error)
|
||||
throw error
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Batch-Analyse mehrerer Kommentare
|
||||
*/
|
||||
async analyzeCommentsBatch(
|
||||
messages: string[]
|
||||
): Promise<Map<string, CommentAnalysis>> {
|
||||
const results = new Map<string, CommentAnalysis>()
|
||||
|
||||
// Parallele Verarbeitung mit Rate-Limiting
|
||||
const batchSize = 5
|
||||
for (let i = 0; i < messages.length; i += batchSize) {
|
||||
const batch = messages.slice(i, i + batchSize)
|
||||
const analyses = await Promise.all(
|
||||
batch.map((msg) => this.analyzeComment(msg))
|
||||
)
|
||||
batch.forEach((msg, idx) => {
|
||||
results.set(msg, analyses[idx])
|
||||
})
|
||||
|
||||
// Kurze Pause zwischen Batches
|
||||
if (i + batchSize < messages.length) {
|
||||
await new Promise((resolve) => setTimeout(resolve, 500))
|
||||
}
|
||||
}
|
||||
|
||||
return results
|
||||
}
|
||||
}
|
||||
|
||||
export type { CommentAnalysis, ReplyContext }
|
||||
254
src/lib/integrations/youtube/CommentsSyncService.ts
Normal file
254
src/lib/integrations/youtube/CommentsSyncService.ts
Normal file
|
|
@ -0,0 +1,254 @@
|
|||
// src/lib/integrations/youtube/CommentsSyncService.ts
|
||||
|
||||
import type { Payload } from 'payload'
|
||||
import { YouTubeClient, type CommentThread } from './YouTubeClient'
|
||||
import { ClaudeAnalysisService } from '../claude/ClaudeAnalysisService'
|
||||
|
||||
interface SyncOptions {
|
||||
socialAccountId: number
|
||||
sinceDate?: Date
|
||||
maxComments?: number
|
||||
analyzeWithAI?: boolean
|
||||
}
|
||||
|
||||
interface SyncResult {
|
||||
success: boolean
|
||||
newComments: number
|
||||
updatedComments: number
|
||||
errors: string[]
|
||||
syncedAt: Date
|
||||
}
|
||||
|
||||
export class CommentsSyncService {
|
||||
private payload: Payload
|
||||
private claudeService: ClaudeAnalysisService
|
||||
|
||||
constructor(payload: Payload) {
|
||||
this.payload = payload
|
||||
this.claudeService = new ClaudeAnalysisService()
|
||||
}
|
||||
|
||||
/**
|
||||
* Kommentare für einen Social Account synchronisieren
|
||||
*/
|
||||
async syncComments(options: SyncOptions): Promise<SyncResult> {
|
||||
const {
|
||||
socialAccountId,
|
||||
sinceDate,
|
||||
maxComments = 100,
|
||||
analyzeWithAI = true,
|
||||
} = options
|
||||
|
||||
const result: SyncResult = {
|
||||
success: false,
|
||||
newComments: 0,
|
||||
updatedComments: 0,
|
||||
errors: [],
|
||||
syncedAt: new Date(),
|
||||
}
|
||||
|
||||
try {
|
||||
// 1. Social Account laden
|
||||
const account = await this.payload.findByID({
|
||||
collection: 'social-accounts',
|
||||
id: socialAccountId,
|
||||
depth: 2,
|
||||
})
|
||||
|
||||
if (!account) {
|
||||
result.errors.push('Social Account nicht gefunden')
|
||||
return result
|
||||
}
|
||||
|
||||
// 2. Platform prüfen
|
||||
const platform = account.platform as { slug?: string }
|
||||
if (platform?.slug !== 'youtube') {
|
||||
result.errors.push('Nur YouTube wird derzeit unterstützt')
|
||||
return result
|
||||
}
|
||||
|
||||
// 3. YouTube Client initialisieren
|
||||
const credentials = account.credentials as {
|
||||
accessToken?: string
|
||||
refreshToken?: string
|
||||
}
|
||||
|
||||
if (!credentials?.accessToken || !credentials?.refreshToken) {
|
||||
result.errors.push('Keine gültigen API-Credentials')
|
||||
return result
|
||||
}
|
||||
|
||||
const youtubeClient = new YouTubeClient(
|
||||
{
|
||||
clientId: process.env.YOUTUBE_CLIENT_ID!,
|
||||
clientSecret: process.env.YOUTUBE_CLIENT_SECRET!,
|
||||
accessToken: credentials.accessToken,
|
||||
refreshToken: credentials.refreshToken,
|
||||
},
|
||||
this.payload
|
||||
)
|
||||
|
||||
// 4. Kommentare abrufen
|
||||
const channelId = account.externalId as string
|
||||
const comments = await youtubeClient.getChannelComments(
|
||||
channelId,
|
||||
sinceDate,
|
||||
maxComments
|
||||
)
|
||||
|
||||
// 5. Kommentare verarbeiten
|
||||
for (const comment of comments) {
|
||||
try {
|
||||
const processResult = await this.processComment(
|
||||
comment,
|
||||
account,
|
||||
analyzeWithAI
|
||||
)
|
||||
if (processResult.isNew) {
|
||||
result.newComments++
|
||||
} else {
|
||||
result.updatedComments++
|
||||
}
|
||||
} catch (error) {
|
||||
result.errors.push(`Fehler bei Kommentar ${comment.id}: ${error}`)
|
||||
}
|
||||
}
|
||||
|
||||
// 6. Account-Stats aktualisieren
|
||||
await this.payload.update({
|
||||
collection: 'social-accounts',
|
||||
id: socialAccountId,
|
||||
data: {
|
||||
stats: {
|
||||
lastSyncedAt: new Date().toISOString(),
|
||||
},
|
||||
},
|
||||
})
|
||||
|
||||
result.success = true
|
||||
} catch (error) {
|
||||
result.errors.push(`Sync-Fehler: ${error}`)
|
||||
}
|
||||
|
||||
return result
|
||||
}
|
||||
|
||||
/**
|
||||
* Einzelnen Kommentar verarbeiten
|
||||
*/
|
||||
private async processComment(
|
||||
comment: CommentThread,
|
||||
account: any,
|
||||
analyzeWithAI: boolean
|
||||
): Promise<{ isNew: boolean }> {
|
||||
const snippet = comment.snippet.topLevelComment.snippet
|
||||
|
||||
// Prüfen ob Kommentar bereits existiert
|
||||
const existing = await this.payload.find({
|
||||
collection: 'community-interactions',
|
||||
where: {
|
||||
externalId: { equals: comment.id },
|
||||
},
|
||||
limit: 1,
|
||||
})
|
||||
|
||||
const isNew = existing.totalDocs === 0
|
||||
|
||||
// AI-Analyse durchführen
|
||||
let analysis = null
|
||||
if (analyzeWithAI) {
|
||||
analysis = await this.claudeService.analyzeComment(snippet.textOriginal)
|
||||
}
|
||||
|
||||
// Verknüpften Content finden (falls Video-ID vorhanden)
|
||||
let linkedContentId = null
|
||||
if (comment.snippet.videoId) {
|
||||
const content = await this.payload.find({
|
||||
collection: 'youtube-content',
|
||||
where: {
|
||||
youtubeVideoId: { equals: comment.snippet.videoId },
|
||||
},
|
||||
limit: 1,
|
||||
})
|
||||
if (content.docs[0]) {
|
||||
linkedContentId = content.docs[0].id
|
||||
}
|
||||
}
|
||||
|
||||
// Platform-ID holen
|
||||
const platform = await this.payload.find({
|
||||
collection: 'social-platforms',
|
||||
where: {
|
||||
slug: { equals: 'youtube' },
|
||||
},
|
||||
limit: 1,
|
||||
})
|
||||
const platformId = platform.docs[0]?.id
|
||||
|
||||
const interactionData = {
|
||||
platform: platformId,
|
||||
socialAccount: account.id,
|
||||
linkedContent: linkedContentId,
|
||||
type: 'comment' as const,
|
||||
externalId: comment.id,
|
||||
author: {
|
||||
name: snippet.authorDisplayName,
|
||||
handle: snippet.authorChannelId?.value,
|
||||
profileUrl: snippet.authorChannelUrl,
|
||||
avatarUrl: snippet.authorProfileImageUrl,
|
||||
isVerified: false,
|
||||
isSubscriber: false,
|
||||
isMember: false,
|
||||
},
|
||||
message: snippet.textOriginal,
|
||||
messageHtml: snippet.textDisplay,
|
||||
publishedAt: new Date(snippet.publishedAt).toISOString(),
|
||||
engagement: {
|
||||
likes: snippet.likeCount || 0,
|
||||
replies: comment.snippet.totalReplyCount || 0,
|
||||
isHearted: false,
|
||||
isPinned: false,
|
||||
},
|
||||
...(analysis && {
|
||||
analysis: {
|
||||
sentiment: analysis.sentiment,
|
||||
sentimentScore: analysis.sentimentScore,
|
||||
confidence: analysis.confidence,
|
||||
topics: analysis.topics.map((t) => ({ topic: t })),
|
||||
language: analysis.language,
|
||||
suggestedReply: analysis.suggestedReply,
|
||||
analyzedAt: new Date().toISOString(),
|
||||
},
|
||||
flags: {
|
||||
isMedicalQuestion: analysis.isMedicalQuestion,
|
||||
requiresEscalation: analysis.requiresEscalation,
|
||||
isSpam: analysis.isSpam,
|
||||
isFromInfluencer: false,
|
||||
},
|
||||
}),
|
||||
}
|
||||
|
||||
if (isNew) {
|
||||
await this.payload.create({
|
||||
collection: 'community-interactions',
|
||||
data: interactionData,
|
||||
})
|
||||
} else {
|
||||
await this.payload.update({
|
||||
collection: 'community-interactions',
|
||||
id: existing.docs[0].id,
|
||||
data: {
|
||||
...interactionData,
|
||||
// Behalte existierende Workflow-Daten
|
||||
status: existing.docs[0].status,
|
||||
priority: existing.docs[0].priority,
|
||||
assignedTo: existing.docs[0].assignedTo,
|
||||
},
|
||||
})
|
||||
}
|
||||
|
||||
return { isNew }
|
||||
}
|
||||
}
|
||||
|
||||
export type { SyncOptions, SyncResult }
|
||||
231
src/lib/integrations/youtube/YouTubeClient.ts
Normal file
231
src/lib/integrations/youtube/YouTubeClient.ts
Normal file
|
|
@ -0,0 +1,231 @@
|
|||
// src/lib/integrations/youtube/YouTubeClient.ts
|
||||
|
||||
import { google, youtube_v3 } from 'googleapis'
|
||||
import type { Payload } from 'payload'
|
||||
|
||||
interface YouTubeCredentials {
|
||||
clientId: string
|
||||
clientSecret: string
|
||||
accessToken: string
|
||||
refreshToken: string
|
||||
}
|
||||
|
||||
interface CommentThread {
|
||||
id: string
|
||||
snippet: {
|
||||
videoId: string
|
||||
topLevelComment: {
|
||||
id: string
|
||||
snippet: {
|
||||
textDisplay: string
|
||||
textOriginal: string
|
||||
authorDisplayName: string
|
||||
authorProfileImageUrl: string
|
||||
authorChannelUrl: string
|
||||
authorChannelId: { value: string }
|
||||
likeCount: number
|
||||
publishedAt: string
|
||||
updatedAt: string
|
||||
}
|
||||
}
|
||||
totalReplyCount: number
|
||||
}
|
||||
}
|
||||
|
||||
export class YouTubeClient {
|
||||
private youtube: youtube_v3.Youtube
|
||||
private oauth2Client: ReturnType<typeof google.auth.OAuth2>
|
||||
private payload: Payload
|
||||
|
||||
constructor(credentials: YouTubeCredentials, payload: Payload) {
|
||||
this.payload = payload
|
||||
|
||||
this.oauth2Client = new google.auth.OAuth2(
|
||||
credentials.clientId,
|
||||
credentials.clientSecret,
|
||||
process.env.YOUTUBE_REDIRECT_URI
|
||||
)
|
||||
|
||||
this.oauth2Client.setCredentials({
|
||||
access_token: credentials.accessToken,
|
||||
refresh_token: credentials.refreshToken,
|
||||
})
|
||||
|
||||
this.youtube = google.youtube({
|
||||
version: 'v3',
|
||||
auth: this.oauth2Client,
|
||||
})
|
||||
}
|
||||
|
||||
/**
|
||||
* Alle Kommentar-Threads eines Videos abrufen
|
||||
*/
|
||||
async getVideoComments(
|
||||
videoId: string,
|
||||
pageToken?: string,
|
||||
maxResults: number = 100
|
||||
): Promise<{
|
||||
comments: CommentThread[]
|
||||
nextPageToken?: string
|
||||
}> {
|
||||
try {
|
||||
const response = await this.youtube.commentThreads.list({
|
||||
part: ['snippet', 'replies'],
|
||||
videoId: videoId,
|
||||
maxResults: maxResults,
|
||||
pageToken: pageToken,
|
||||
order: 'time',
|
||||
textFormat: 'plainText',
|
||||
})
|
||||
|
||||
return {
|
||||
comments: response.data.items as CommentThread[],
|
||||
nextPageToken: response.data.nextPageToken || undefined,
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('Error fetching comments:', error)
|
||||
throw error
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Alle Kommentare eines Kanals abrufen (alle Videos)
|
||||
*/
|
||||
async getChannelComments(
|
||||
channelId: string,
|
||||
publishedAfter?: Date,
|
||||
maxResults: number = 100
|
||||
): Promise<CommentThread[]> {
|
||||
try {
|
||||
const params: youtube_v3.Params$Resource$Commentthreads$List = {
|
||||
part: ['snippet', 'replies'],
|
||||
allThreadsRelatedToChannelId: channelId,
|
||||
maxResults: maxResults,
|
||||
order: 'time',
|
||||
textFormat: 'plainText',
|
||||
}
|
||||
|
||||
const response = await this.youtube.commentThreads.list(params)
|
||||
return response.data.items as CommentThread[]
|
||||
} catch (error) {
|
||||
console.error('Error fetching channel comments:', error)
|
||||
throw error
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Auf einen Kommentar antworten
|
||||
*/
|
||||
async replyToComment(parentCommentId: string, text: string): Promise<string> {
|
||||
try {
|
||||
const response = await this.youtube.comments.insert({
|
||||
part: ['snippet'],
|
||||
requestBody: {
|
||||
snippet: {
|
||||
parentId: parentCommentId,
|
||||
textOriginal: text,
|
||||
},
|
||||
},
|
||||
})
|
||||
|
||||
return response.data.id!
|
||||
} catch (error) {
|
||||
console.error('Error replying to comment:', error)
|
||||
throw error
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Kommentar als Spam markieren
|
||||
*/
|
||||
async markAsSpam(commentId: string): Promise<void> {
|
||||
try {
|
||||
await this.youtube.comments.setModerationStatus({
|
||||
id: [commentId],
|
||||
moderationStatus: 'rejected',
|
||||
})
|
||||
} catch (error) {
|
||||
console.error('Error marking as spam:', error)
|
||||
throw error
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Kommentar löschen
|
||||
*/
|
||||
async deleteComment(commentId: string): Promise<void> {
|
||||
try {
|
||||
await this.youtube.comments.delete({
|
||||
id: commentId,
|
||||
})
|
||||
} catch (error) {
|
||||
console.error('Error deleting comment:', error)
|
||||
throw error
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Heart/Like auf einen Kommentar setzen
|
||||
*/
|
||||
async heartComment(commentId: string): Promise<void> {
|
||||
try {
|
||||
await this.youtube.comments.setModerationStatus({
|
||||
id: [commentId],
|
||||
moderationStatus: 'published',
|
||||
})
|
||||
// Note: YouTube API doesn't directly support "hearting" via API
|
||||
// This would require additional implementation
|
||||
} catch (error) {
|
||||
console.error('Error hearting comment:', error)
|
||||
throw error
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Access Token erneuern
|
||||
*/
|
||||
async refreshAccessToken(): Promise<{
|
||||
accessToken: string
|
||||
expiresAt: Date
|
||||
}> {
|
||||
try {
|
||||
const { credentials } = await this.oauth2Client.refreshAccessToken()
|
||||
|
||||
return {
|
||||
accessToken: credentials.access_token!,
|
||||
expiresAt: new Date(credentials.expiry_date!),
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('Error refreshing token:', error)
|
||||
throw error
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Kanal-Statistiken abrufen
|
||||
*/
|
||||
async getChannelStats(channelId: string): Promise<{
|
||||
subscriberCount: number
|
||||
videoCount: number
|
||||
viewCount: number
|
||||
}> {
|
||||
try {
|
||||
const response = await this.youtube.channels.list({
|
||||
part: ['statistics'],
|
||||
id: [channelId],
|
||||
})
|
||||
|
||||
const stats = response.data.items?.[0]?.statistics
|
||||
return {
|
||||
subscriberCount: parseInt(stats?.subscriberCount || '0', 10),
|
||||
videoCount: parseInt(stats?.videoCount || '0', 10),
|
||||
viewCount: parseInt(stats?.viewCount || '0', 10),
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('Error fetching channel stats:', error)
|
||||
throw error
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
export type { YouTubeCredentials, CommentThread }
|
||||
444
src/migrations/20260113_180000_add_community_phase1.ts
Normal file
444
src/migrations/20260113_180000_add_community_phase1.ts
Normal file
|
|
@ -0,0 +1,444 @@
|
|||
import { MigrateUpArgs, MigrateDownArgs, sql } from '@payloadcms/db-postgres'
|
||||
|
||||
/**
|
||||
* Migration: Community Phase 1
|
||||
*
|
||||
* Creates all Community Management collections:
|
||||
* - social_platforms
|
||||
* - social_accounts
|
||||
* - community_templates (before interactions, as it's referenced)
|
||||
* - community_interactions
|
||||
* - community_rules
|
||||
*
|
||||
* Also adds communityRole to users table.
|
||||
*/
|
||||
export async function up({ db }: MigrateUpArgs): Promise<void> {
|
||||
// Step 1: Add user column
|
||||
await db.execute(sql`
|
||||
ALTER TABLE "users"
|
||||
ADD COLUMN IF NOT EXISTS "community_role" varchar DEFAULT 'none';
|
||||
`)
|
||||
|
||||
// Step 2: Create social_platforms (no dependencies)
|
||||
await db.execute(sql`
|
||||
CREATE TABLE IF NOT EXISTS "social_platforms" (
|
||||
"id" serial PRIMARY KEY NOT NULL,
|
||||
"name" varchar NOT NULL,
|
||||
"slug" varchar NOT NULL UNIQUE,
|
||||
"icon" varchar,
|
||||
"color" varchar,
|
||||
"is_active" boolean DEFAULT true,
|
||||
"api_status" varchar DEFAULT 'disconnected',
|
||||
"api_config_api_type" varchar,
|
||||
"api_config_base_url" varchar,
|
||||
"api_config_auth_type" varchar,
|
||||
"rate_limits_requests_per_minute" numeric,
|
||||
"rate_limits_requests_per_day" numeric,
|
||||
"rate_limits_quota_units_per_day" numeric,
|
||||
"updated_at" timestamp(3) with time zone DEFAULT now() NOT NULL,
|
||||
"created_at" timestamp(3) with time zone DEFAULT now() NOT NULL
|
||||
);
|
||||
|
||||
CREATE INDEX IF NOT EXISTS "social_platforms_slug_idx" ON "social_platforms" USING btree ("slug");
|
||||
CREATE INDEX IF NOT EXISTS "social_platforms_is_active_idx" ON "social_platforms" USING btree ("is_active");
|
||||
CREATE INDEX IF NOT EXISTS "social_platforms_updated_at_idx" ON "social_platforms" USING btree ("updated_at");
|
||||
CREATE INDEX IF NOT EXISTS "social_platforms_created_at_idx" ON "social_platforms" USING btree ("created_at");
|
||||
`)
|
||||
|
||||
// Step 3: Create social_platforms array tables
|
||||
await db.execute(sql`
|
||||
CREATE TABLE IF NOT EXISTS "social_platforms_api_config_scopes" (
|
||||
"id" serial PRIMARY KEY NOT NULL,
|
||||
"_order" integer NOT NULL,
|
||||
"_parent_id" integer NOT NULL REFERENCES social_platforms(id) ON DELETE CASCADE,
|
||||
"scope" varchar
|
||||
);
|
||||
|
||||
CREATE INDEX IF NOT EXISTS "social_platforms_api_config_scopes_order_idx" ON "social_platforms_api_config_scopes" USING btree ("_order");
|
||||
CREATE INDEX IF NOT EXISTS "social_platforms_api_config_scopes_parent_idx" ON "social_platforms_api_config_scopes" USING btree ("_parent_id");
|
||||
|
||||
CREATE TABLE IF NOT EXISTS "social_platforms_interaction_types" (
|
||||
"id" serial PRIMARY KEY NOT NULL,
|
||||
"_order" integer NOT NULL,
|
||||
"_parent_id" integer NOT NULL REFERENCES social_platforms(id) ON DELETE CASCADE,
|
||||
"type" varchar NOT NULL,
|
||||
"label" varchar NOT NULL,
|
||||
"icon" varchar,
|
||||
"can_reply" boolean DEFAULT true
|
||||
);
|
||||
|
||||
CREATE INDEX IF NOT EXISTS "social_platforms_interaction_types_order_idx" ON "social_platforms_interaction_types" USING btree ("_order");
|
||||
CREATE INDEX IF NOT EXISTS "social_platforms_interaction_types_parent_idx" ON "social_platforms_interaction_types" USING btree ("_parent_id");
|
||||
`)
|
||||
|
||||
// Step 4: Create social_accounts (depends on social_platforms, youtube_channels)
|
||||
await db.execute(sql`
|
||||
CREATE TABLE IF NOT EXISTS "social_accounts" (
|
||||
"id" serial PRIMARY KEY NOT NULL,
|
||||
"platform_id" integer REFERENCES social_platforms(id) ON DELETE SET NULL,
|
||||
"linked_channel_id" integer REFERENCES youtube_channels(id) ON DELETE SET NULL,
|
||||
"display_name" varchar NOT NULL,
|
||||
"account_handle" varchar,
|
||||
"external_id" varchar,
|
||||
"account_url" varchar,
|
||||
"is_active" boolean DEFAULT true,
|
||||
"credentials_access_token" varchar,
|
||||
"credentials_refresh_token" varchar,
|
||||
"credentials_token_expires_at" timestamp(3) with time zone,
|
||||
"credentials_api_key" varchar,
|
||||
"stats_followers" numeric,
|
||||
"stats_total_posts" numeric,
|
||||
"stats_last_synced_at" timestamp(3) with time zone,
|
||||
"sync_settings_auto_sync_enabled" boolean DEFAULT true,
|
||||
"sync_settings_sync_interval_minutes" numeric DEFAULT 15,
|
||||
"sync_settings_sync_comments" boolean DEFAULT true,
|
||||
"sync_settings_sync_d_ms" boolean DEFAULT false,
|
||||
"updated_at" timestamp(3) with time zone DEFAULT now() NOT NULL,
|
||||
"created_at" timestamp(3) with time zone DEFAULT now() NOT NULL
|
||||
);
|
||||
|
||||
CREATE INDEX IF NOT EXISTS "social_accounts_platform_idx" ON "social_accounts" USING btree ("platform_id");
|
||||
CREATE INDEX IF NOT EXISTS "social_accounts_linked_channel_idx" ON "social_accounts" USING btree ("linked_channel_id");
|
||||
CREATE INDEX IF NOT EXISTS "social_accounts_is_active_idx" ON "social_accounts" USING btree ("is_active");
|
||||
CREATE INDEX IF NOT EXISTS "social_accounts_updated_at_idx" ON "social_accounts" USING btree ("updated_at");
|
||||
CREATE INDEX IF NOT EXISTS "social_accounts_created_at_idx" ON "social_accounts" USING btree ("created_at");
|
||||
`)
|
||||
|
||||
// Step 5: Create community_templates FIRST (before interactions, as it's referenced)
|
||||
await db.execute(sql`
|
||||
CREATE TABLE IF NOT EXISTS "community_templates" (
|
||||
"id" serial PRIMARY KEY NOT NULL,
|
||||
"category" varchar NOT NULL,
|
||||
"channel_id" integer REFERENCES youtube_channels(id) ON DELETE SET NULL,
|
||||
"requires_review" boolean DEFAULT false,
|
||||
"is_active" boolean DEFAULT true,
|
||||
"usage_count" numeric DEFAULT 0,
|
||||
"example_output" text,
|
||||
"updated_at" timestamp(3) with time zone DEFAULT now() NOT NULL,
|
||||
"created_at" timestamp(3) with time zone DEFAULT now() NOT NULL
|
||||
);
|
||||
|
||||
CREATE INDEX IF NOT EXISTS "community_templates_category_idx" ON "community_templates" USING btree ("category");
|
||||
CREATE INDEX IF NOT EXISTS "community_templates_channel_idx" ON "community_templates" USING btree ("channel_id");
|
||||
CREATE INDEX IF NOT EXISTS "community_templates_is_active_idx" ON "community_templates" USING btree ("is_active");
|
||||
CREATE INDEX IF NOT EXISTS "community_templates_updated_at_idx" ON "community_templates" USING btree ("updated_at");
|
||||
CREATE INDEX IF NOT EXISTS "community_templates_created_at_idx" ON "community_templates" USING btree ("created_at");
|
||||
`)
|
||||
|
||||
// Step 6: Create community_templates related tables
|
||||
await db.execute(sql`
|
||||
CREATE TABLE IF NOT EXISTS "community_templates_locales" (
|
||||
"name" varchar NOT NULL,
|
||||
"template" text NOT NULL,
|
||||
"id" serial PRIMARY KEY NOT NULL,
|
||||
"_locale" varchar NOT NULL,
|
||||
"_parent_id" integer NOT NULL REFERENCES community_templates(id) ON DELETE CASCADE,
|
||||
CONSTRAINT "community_templates_locales_locale_parent_id_unique" UNIQUE("_locale", "_parent_id")
|
||||
);
|
||||
|
||||
CREATE INDEX IF NOT EXISTS "community_templates_locales_locale_idx" ON "community_templates_locales" USING btree ("_locale");
|
||||
CREATE INDEX IF NOT EXISTS "community_templates_locales_parent_idx" ON "community_templates_locales" USING btree ("_parent_id");
|
||||
|
||||
CREATE TABLE IF NOT EXISTS "community_templates_variables" (
|
||||
"id" serial PRIMARY KEY NOT NULL,
|
||||
"_order" integer NOT NULL,
|
||||
"_parent_id" integer NOT NULL REFERENCES community_templates(id) ON DELETE CASCADE,
|
||||
"variable" varchar NOT NULL,
|
||||
"description" varchar,
|
||||
"default_value" varchar
|
||||
);
|
||||
|
||||
CREATE INDEX IF NOT EXISTS "community_templates_variables_order_idx" ON "community_templates_variables" USING btree ("_order");
|
||||
CREATE INDEX IF NOT EXISTS "community_templates_variables_parent_idx" ON "community_templates_variables" USING btree ("_parent_id");
|
||||
|
||||
CREATE TABLE IF NOT EXISTS "community_templates_auto_suggest_keywords" (
|
||||
"id" serial PRIMARY KEY NOT NULL,
|
||||
"_order" integer NOT NULL,
|
||||
"_parent_id" integer NOT NULL REFERENCES community_templates(id) ON DELETE CASCADE,
|
||||
"keyword" varchar NOT NULL
|
||||
);
|
||||
|
||||
CREATE INDEX IF NOT EXISTS "community_templates_auto_suggest_keywords_order_idx" ON "community_templates_auto_suggest_keywords" USING btree ("_order");
|
||||
CREATE INDEX IF NOT EXISTS "community_templates_auto_suggest_keywords_parent_idx" ON "community_templates_auto_suggest_keywords" USING btree ("_parent_id");
|
||||
|
||||
CREATE TABLE IF NOT EXISTS "community_templates_rels" (
|
||||
"id" serial PRIMARY KEY NOT NULL,
|
||||
"order" integer,
|
||||
"parent_id" integer NOT NULL REFERENCES community_templates(id) ON DELETE CASCADE,
|
||||
"path" varchar NOT NULL,
|
||||
"social_platforms_id" integer REFERENCES social_platforms(id) ON DELETE CASCADE
|
||||
);
|
||||
|
||||
CREATE INDEX IF NOT EXISTS "community_templates_rels_order_idx" ON "community_templates_rels" USING btree ("order");
|
||||
CREATE INDEX IF NOT EXISTS "community_templates_rels_parent_idx" ON "community_templates_rels" USING btree ("parent_id");
|
||||
CREATE INDEX IF NOT EXISTS "community_templates_rels_path_idx" ON "community_templates_rels" USING btree ("path");
|
||||
CREATE INDEX IF NOT EXISTS "community_templates_rels_social_platforms_idx" ON "community_templates_rels" USING btree ("social_platforms_id");
|
||||
`)
|
||||
|
||||
// Step 7: Create community_interactions (now community_templates exists)
|
||||
await db.execute(sql`
|
||||
CREATE TABLE IF NOT EXISTS "community_interactions" (
|
||||
"id" serial PRIMARY KEY NOT NULL,
|
||||
"platform_id" integer REFERENCES social_platforms(id) ON DELETE SET NULL,
|
||||
"social_account_id" integer REFERENCES social_accounts(id) ON DELETE SET NULL,
|
||||
"linked_content_id" integer REFERENCES youtube_content(id) ON DELETE SET NULL,
|
||||
"type" varchar NOT NULL,
|
||||
"external_id" varchar NOT NULL UNIQUE,
|
||||
"parent_interaction_id" integer,
|
||||
"author_name" varchar,
|
||||
"author_handle" varchar,
|
||||
"author_profile_url" varchar,
|
||||
"author_avatar_url" varchar,
|
||||
"author_is_verified" boolean DEFAULT false,
|
||||
"author_is_subscriber" boolean DEFAULT false,
|
||||
"author_is_member" boolean DEFAULT false,
|
||||
"author_subscriber_count" numeric,
|
||||
"message" text NOT NULL,
|
||||
"message_html" text,
|
||||
"published_at" timestamp(3) with time zone NOT NULL,
|
||||
"analysis_sentiment" varchar,
|
||||
"analysis_sentiment_score" numeric,
|
||||
"analysis_confidence" numeric,
|
||||
"analysis_language" varchar,
|
||||
"analysis_suggested_template_id" integer REFERENCES community_templates(id) ON DELETE SET NULL,
|
||||
"analysis_suggested_reply" text,
|
||||
"analysis_analyzed_at" timestamp(3) with time zone,
|
||||
"flags_is_medical_question" boolean DEFAULT false,
|
||||
"flags_requires_escalation" boolean DEFAULT false,
|
||||
"flags_is_spam" boolean DEFAULT false,
|
||||
"flags_is_from_influencer" boolean DEFAULT false,
|
||||
"status" varchar DEFAULT 'new' NOT NULL,
|
||||
"priority" varchar DEFAULT 'normal' NOT NULL,
|
||||
"assigned_to_id" integer REFERENCES users(id) ON DELETE SET NULL,
|
||||
"response_deadline" timestamp(3) with time zone,
|
||||
"response_text" text,
|
||||
"response_used_template_id" integer REFERENCES community_templates(id) ON DELETE SET NULL,
|
||||
"response_sent_at" timestamp(3) with time zone,
|
||||
"response_sent_by_id" integer REFERENCES users(id) ON DELETE SET NULL,
|
||||
"response_external_reply_id" varchar,
|
||||
"engagement_likes" numeric DEFAULT 0,
|
||||
"engagement_replies" numeric DEFAULT 0,
|
||||
"engagement_is_hearted" boolean DEFAULT false,
|
||||
"engagement_is_pinned" boolean DEFAULT false,
|
||||
"internal_notes" text,
|
||||
"updated_at" timestamp(3) with time zone DEFAULT now() NOT NULL,
|
||||
"created_at" timestamp(3) with time zone DEFAULT now() NOT NULL
|
||||
);
|
||||
`)
|
||||
|
||||
// Step 8: Add self-reference for parent_interaction after table exists
|
||||
await db.execute(sql`
|
||||
ALTER TABLE "community_interactions"
|
||||
ADD CONSTRAINT "community_interactions_parent_fk"
|
||||
FOREIGN KEY ("parent_interaction_id") REFERENCES community_interactions(id) ON DELETE SET NULL;
|
||||
`)
|
||||
|
||||
// Step 9: Create indexes for community_interactions
|
||||
await db.execute(sql`
|
||||
CREATE INDEX IF NOT EXISTS "community_interactions_platform_idx" ON "community_interactions" USING btree ("platform_id");
|
||||
CREATE INDEX IF NOT EXISTS "community_interactions_social_account_idx" ON "community_interactions" USING btree ("social_account_id");
|
||||
CREATE INDEX IF NOT EXISTS "community_interactions_linked_content_idx" ON "community_interactions" USING btree ("linked_content_id");
|
||||
CREATE INDEX IF NOT EXISTS "community_interactions_external_id_idx" ON "community_interactions" USING btree ("external_id");
|
||||
CREATE INDEX IF NOT EXISTS "community_interactions_parent_idx" ON "community_interactions" USING btree ("parent_interaction_id");
|
||||
CREATE INDEX IF NOT EXISTS "community_interactions_status_idx" ON "community_interactions" USING btree ("status");
|
||||
CREATE INDEX IF NOT EXISTS "community_interactions_priority_idx" ON "community_interactions" USING btree ("priority");
|
||||
CREATE INDEX IF NOT EXISTS "community_interactions_assigned_to_idx" ON "community_interactions" USING btree ("assigned_to_id");
|
||||
CREATE INDEX IF NOT EXISTS "community_interactions_updated_at_idx" ON "community_interactions" USING btree ("updated_at");
|
||||
CREATE INDEX IF NOT EXISTS "community_interactions_created_at_idx" ON "community_interactions" USING btree ("created_at");
|
||||
`)
|
||||
|
||||
// Step 10: Create community_interactions array tables
|
||||
await db.execute(sql`
|
||||
CREATE TABLE IF NOT EXISTS "community_interactions_attachments" (
|
||||
"id" serial PRIMARY KEY NOT NULL,
|
||||
"_order" integer NOT NULL,
|
||||
"_parent_id" integer NOT NULL REFERENCES community_interactions(id) ON DELETE CASCADE,
|
||||
"type" varchar,
|
||||
"url" varchar
|
||||
);
|
||||
|
||||
CREATE INDEX IF NOT EXISTS "community_interactions_attachments_order_idx" ON "community_interactions_attachments" USING btree ("_order");
|
||||
CREATE INDEX IF NOT EXISTS "community_interactions_attachments_parent_idx" ON "community_interactions_attachments" USING btree ("_parent_id");
|
||||
|
||||
CREATE TABLE IF NOT EXISTS "community_interactions_analysis_topics" (
|
||||
"id" serial PRIMARY KEY NOT NULL,
|
||||
"_order" integer NOT NULL,
|
||||
"_parent_id" integer NOT NULL REFERENCES community_interactions(id) ON DELETE CASCADE,
|
||||
"topic" varchar
|
||||
);
|
||||
|
||||
CREATE INDEX IF NOT EXISTS "community_interactions_analysis_topics_order_idx" ON "community_interactions_analysis_topics" USING btree ("_order");
|
||||
CREATE INDEX IF NOT EXISTS "community_interactions_analysis_topics_parent_idx" ON "community_interactions_analysis_topics" USING btree ("_parent_id");
|
||||
`)
|
||||
|
||||
// Step 11: Create community_rules
|
||||
await db.execute(sql`
|
||||
CREATE TABLE IF NOT EXISTS "community_rules" (
|
||||
"id" serial PRIMARY KEY NOT NULL,
|
||||
"name" varchar NOT NULL,
|
||||
"priority" numeric DEFAULT 100 NOT NULL,
|
||||
"is_active" boolean DEFAULT true,
|
||||
"description" text,
|
||||
"channel_id" integer REFERENCES youtube_channels(id) ON DELETE SET NULL,
|
||||
"trigger_type" varchar NOT NULL,
|
||||
"trigger_influencer_min_followers" numeric DEFAULT 10000,
|
||||
"stats_times_triggered" numeric DEFAULT 0,
|
||||
"stats_last_triggered_at" timestamp(3) with time zone,
|
||||
"updated_at" timestamp(3) with time zone DEFAULT now() NOT NULL,
|
||||
"created_at" timestamp(3) with time zone DEFAULT now() NOT NULL
|
||||
);
|
||||
|
||||
CREATE INDEX IF NOT EXISTS "community_rules_name_idx" ON "community_rules" USING btree ("name");
|
||||
CREATE INDEX IF NOT EXISTS "community_rules_priority_idx" ON "community_rules" USING btree ("priority");
|
||||
CREATE INDEX IF NOT EXISTS "community_rules_is_active_idx" ON "community_rules" USING btree ("is_active");
|
||||
CREATE INDEX IF NOT EXISTS "community_rules_channel_idx" ON "community_rules" USING btree ("channel_id");
|
||||
CREATE INDEX IF NOT EXISTS "community_rules_updated_at_idx" ON "community_rules" USING btree ("updated_at");
|
||||
CREATE INDEX IF NOT EXISTS "community_rules_created_at_idx" ON "community_rules" USING btree ("created_at");
|
||||
`)
|
||||
|
||||
// Step 12: Create community_rules array tables
|
||||
await db.execute(sql`
|
||||
CREATE TABLE IF NOT EXISTS "community_rules_trigger_keywords" (
|
||||
"id" serial PRIMARY KEY NOT NULL,
|
||||
"_order" integer NOT NULL,
|
||||
"_parent_id" integer NOT NULL REFERENCES community_rules(id) ON DELETE CASCADE,
|
||||
"keyword" varchar NOT NULL,
|
||||
"match_type" varchar DEFAULT 'contains'
|
||||
);
|
||||
|
||||
CREATE INDEX IF NOT EXISTS "community_rules_trigger_keywords_order_idx" ON "community_rules_trigger_keywords" USING btree ("_order");
|
||||
CREATE INDEX IF NOT EXISTS "community_rules_trigger_keywords_parent_idx" ON "community_rules_trigger_keywords" USING btree ("_parent_id");
|
||||
|
||||
CREATE TABLE IF NOT EXISTS "community_rules_actions" (
|
||||
"id" serial PRIMARY KEY NOT NULL,
|
||||
"_order" integer NOT NULL,
|
||||
"_parent_id" integer NOT NULL REFERENCES community_rules(id) ON DELETE CASCADE,
|
||||
"action" varchar NOT NULL,
|
||||
"value" varchar,
|
||||
"target_user_id" integer REFERENCES users(id) ON DELETE SET NULL,
|
||||
"target_template_id" integer REFERENCES community_templates(id) ON DELETE SET NULL
|
||||
);
|
||||
|
||||
CREATE INDEX IF NOT EXISTS "community_rules_actions_order_idx" ON "community_rules_actions" USING btree ("_order");
|
||||
CREATE INDEX IF NOT EXISTS "community_rules_actions_parent_idx" ON "community_rules_actions" USING btree ("_parent_id");
|
||||
CREATE INDEX IF NOT EXISTS "community_rules_actions_target_user_idx" ON "community_rules_actions" USING btree ("target_user_id");
|
||||
CREATE INDEX IF NOT EXISTS "community_rules_actions_target_template_idx" ON "community_rules_actions" USING btree ("target_template_id");
|
||||
|
||||
CREATE TABLE IF NOT EXISTS "community_rules_rels" (
|
||||
"id" serial PRIMARY KEY NOT NULL,
|
||||
"order" integer,
|
||||
"parent_id" integer NOT NULL REFERENCES community_rules(id) ON DELETE CASCADE,
|
||||
"path" varchar NOT NULL,
|
||||
"social_platforms_id" integer REFERENCES social_platforms(id) ON DELETE CASCADE
|
||||
);
|
||||
|
||||
CREATE INDEX IF NOT EXISTS "community_rules_rels_order_idx" ON "community_rules_rels" USING btree ("order");
|
||||
CREATE INDEX IF NOT EXISTS "community_rules_rels_parent_idx" ON "community_rules_rels" USING btree ("parent_id");
|
||||
CREATE INDEX IF NOT EXISTS "community_rules_rels_path_idx" ON "community_rules_rels" USING btree ("path");
|
||||
CREATE INDEX IF NOT EXISTS "community_rules_rels_social_platforms_idx" ON "community_rules_rels" USING btree ("social_platforms_id");
|
||||
|
||||
CREATE TABLE IF NOT EXISTS "community_rules_trigger_sentiment_values" (
|
||||
"id" serial PRIMARY KEY NOT NULL,
|
||||
"_order" integer NOT NULL,
|
||||
"_parent_id" integer NOT NULL REFERENCES community_rules(id) ON DELETE CASCADE,
|
||||
"value" varchar
|
||||
);
|
||||
|
||||
CREATE INDEX IF NOT EXISTS "community_rules_trigger_sentiment_values_order_idx" ON "community_rules_trigger_sentiment_values" USING btree ("_order");
|
||||
CREATE INDEX IF NOT EXISTS "community_rules_trigger_sentiment_values_parent_idx" ON "community_rules_trigger_sentiment_values" USING btree ("_parent_id");
|
||||
`)
|
||||
|
||||
// Step 13: Update system table
|
||||
await db.execute(sql`
|
||||
ALTER TABLE "payload_locked_documents_rels"
|
||||
ADD COLUMN IF NOT EXISTS "social_platforms_id" integer REFERENCES social_platforms(id) ON DELETE CASCADE;
|
||||
|
||||
ALTER TABLE "payload_locked_documents_rels"
|
||||
ADD COLUMN IF NOT EXISTS "social_accounts_id" integer REFERENCES social_accounts(id) ON DELETE CASCADE;
|
||||
|
||||
ALTER TABLE "payload_locked_documents_rels"
|
||||
ADD COLUMN IF NOT EXISTS "community_interactions_id" integer REFERENCES community_interactions(id) ON DELETE CASCADE;
|
||||
|
||||
ALTER TABLE "payload_locked_documents_rels"
|
||||
ADD COLUMN IF NOT EXISTS "community_templates_id" integer REFERENCES community_templates(id) ON DELETE CASCADE;
|
||||
|
||||
ALTER TABLE "payload_locked_documents_rels"
|
||||
ADD COLUMN IF NOT EXISTS "community_rules_id" integer REFERENCES community_rules(id) ON DELETE CASCADE;
|
||||
|
||||
CREATE INDEX IF NOT EXISTS "payload_locked_documents_rels_social_platforms_idx"
|
||||
ON "payload_locked_documents_rels" USING btree ("social_platforms_id");
|
||||
|
||||
CREATE INDEX IF NOT EXISTS "payload_locked_documents_rels_social_accounts_idx"
|
||||
ON "payload_locked_documents_rels" USING btree ("social_accounts_id");
|
||||
|
||||
CREATE INDEX IF NOT EXISTS "payload_locked_documents_rels_community_interactions_idx"
|
||||
ON "payload_locked_documents_rels" USING btree ("community_interactions_id");
|
||||
|
||||
CREATE INDEX IF NOT EXISTS "payload_locked_documents_rels_community_templates_idx"
|
||||
ON "payload_locked_documents_rels" USING btree ("community_templates_id");
|
||||
|
||||
CREATE INDEX IF NOT EXISTS "payload_locked_documents_rels_community_rules_idx"
|
||||
ON "payload_locked_documents_rels" USING btree ("community_rules_id");
|
||||
`)
|
||||
|
||||
// Step 14: Seed YouTube Platform
|
||||
await db.execute(sql`
|
||||
INSERT INTO "social_platforms" ("name", "slug", "icon", "color", "is_active", "api_status", "api_config_api_type", "api_config_base_url", "api_config_auth_type", "rate_limits_quota_units_per_day")
|
||||
VALUES ('YouTube', 'youtube', '📺', '#FF0000', true, 'development', 'youtube_v3', 'https://www.googleapis.com/youtube/v3', 'oauth2', 10000)
|
||||
ON CONFLICT (slug) DO NOTHING;
|
||||
`)
|
||||
}
|
||||
|
||||
export async function down({ db }: MigrateDownArgs): Promise<void> {
|
||||
// Drop in reverse order of creation
|
||||
await db.execute(sql`
|
||||
DROP INDEX IF EXISTS "payload_locked_documents_rels_community_rules_idx";
|
||||
DROP INDEX IF EXISTS "payload_locked_documents_rels_community_templates_idx";
|
||||
DROP INDEX IF EXISTS "payload_locked_documents_rels_community_interactions_idx";
|
||||
DROP INDEX IF EXISTS "payload_locked_documents_rels_social_accounts_idx";
|
||||
DROP INDEX IF EXISTS "payload_locked_documents_rels_social_platforms_idx";
|
||||
|
||||
ALTER TABLE "payload_locked_documents_rels" DROP COLUMN IF EXISTS "community_rules_id";
|
||||
ALTER TABLE "payload_locked_documents_rels" DROP COLUMN IF EXISTS "community_templates_id";
|
||||
ALTER TABLE "payload_locked_documents_rels" DROP COLUMN IF EXISTS "community_interactions_id";
|
||||
ALTER TABLE "payload_locked_documents_rels" DROP COLUMN IF EXISTS "social_accounts_id";
|
||||
ALTER TABLE "payload_locked_documents_rels" DROP COLUMN IF EXISTS "social_platforms_id";
|
||||
`)
|
||||
|
||||
await db.execute(sql`
|
||||
DROP TABLE IF EXISTS "community_rules_trigger_sentiment_values";
|
||||
DROP TABLE IF EXISTS "community_rules_rels";
|
||||
DROP TABLE IF EXISTS "community_rules_actions";
|
||||
DROP TABLE IF EXISTS "community_rules_trigger_keywords";
|
||||
DROP TABLE IF EXISTS "community_rules";
|
||||
`)
|
||||
|
||||
await db.execute(sql`
|
||||
DROP TABLE IF EXISTS "community_interactions_analysis_topics";
|
||||
DROP TABLE IF EXISTS "community_interactions_attachments";
|
||||
ALTER TABLE "community_interactions" DROP CONSTRAINT IF EXISTS "community_interactions_parent_fk";
|
||||
DROP TABLE IF EXISTS "community_interactions";
|
||||
`)
|
||||
|
||||
await db.execute(sql`
|
||||
DROP TABLE IF EXISTS "community_templates_rels";
|
||||
DROP TABLE IF EXISTS "community_templates_auto_suggest_keywords";
|
||||
DROP TABLE IF EXISTS "community_templates_variables";
|
||||
DROP TABLE IF EXISTS "community_templates_locales";
|
||||
DROP TABLE IF EXISTS "community_templates";
|
||||
`)
|
||||
|
||||
await db.execute(sql`
|
||||
DROP TABLE IF EXISTS "social_accounts";
|
||||
`)
|
||||
|
||||
await db.execute(sql`
|
||||
DROP TABLE IF EXISTS "social_platforms_interaction_types";
|
||||
DROP TABLE IF EXISTS "social_platforms_api_config_scopes";
|
||||
DROP TABLE IF EXISTS "social_platforms";
|
||||
`)
|
||||
|
||||
await db.execute(sql`
|
||||
ALTER TABLE "users" DROP COLUMN IF EXISTS "community_role";
|
||||
`)
|
||||
}
|
||||
63
src/migrations/20260114_200000_fix_community_role_enum.ts
Normal file
63
src/migrations/20260114_200000_fix_community_role_enum.ts
Normal file
|
|
@ -0,0 +1,63 @@
|
|||
import { MigrateUpArgs, MigrateDownArgs, sql } from '@payloadcms/db-postgres'
|
||||
|
||||
/**
|
||||
* Migration: Fix Community Role Enum
|
||||
*
|
||||
* The previous migration created community_role as varchar,
|
||||
* but Payload 3.x expects an enum type for select fields.
|
||||
*/
|
||||
export async function up({ db }: MigrateUpArgs): Promise<void> {
|
||||
// Step 1: Create the enum type
|
||||
await db.execute(sql`
|
||||
DO $$ BEGIN
|
||||
CREATE TYPE "enum_users_community_role" AS ENUM ('none', 'viewer', 'moderator', 'manager');
|
||||
EXCEPTION
|
||||
WHEN duplicate_object THEN null;
|
||||
END $$;
|
||||
`)
|
||||
|
||||
// Step 2: Drop the default first
|
||||
await db.execute(sql`
|
||||
ALTER TABLE "users"
|
||||
ALTER COLUMN "community_role" DROP DEFAULT;
|
||||
`)
|
||||
|
||||
// Step 3: Alter the column to use the enum type
|
||||
await db.execute(sql`
|
||||
ALTER TABLE "users"
|
||||
ALTER COLUMN "community_role" TYPE "enum_users_community_role"
|
||||
USING "community_role"::"enum_users_community_role";
|
||||
`)
|
||||
|
||||
// Step 4: Set the new default value with proper type
|
||||
await db.execute(sql`
|
||||
ALTER TABLE "users"
|
||||
ALTER COLUMN "community_role" SET DEFAULT 'none'::"enum_users_community_role";
|
||||
`)
|
||||
}
|
||||
|
||||
export async function down({ db }: MigrateDownArgs): Promise<void> {
|
||||
// Step 1: Drop default
|
||||
await db.execute(sql`
|
||||
ALTER TABLE "users"
|
||||
ALTER COLUMN "community_role" DROP DEFAULT;
|
||||
`)
|
||||
|
||||
// Step 2: Revert to varchar
|
||||
await db.execute(sql`
|
||||
ALTER TABLE "users"
|
||||
ALTER COLUMN "community_role" TYPE varchar
|
||||
USING "community_role"::varchar;
|
||||
`)
|
||||
|
||||
// Step 3: Set varchar default
|
||||
await db.execute(sql`
|
||||
ALTER TABLE "users"
|
||||
ALTER COLUMN "community_role" SET DEFAULT 'none';
|
||||
`)
|
||||
|
||||
// Step 4: Drop enum type
|
||||
await db.execute(sql`
|
||||
DROP TYPE IF EXISTS "enum_users_community_role";
|
||||
`)
|
||||
}
|
||||
|
|
@ -31,6 +31,8 @@ import * as migration_20260109_030000_add_blogwoman_block_tables from './2026010
|
|||
import * as migration_20260112_150000_add_youtube_operations_hub from './20260112_150000_add_youtube_operations_hub';
|
||||
import * as migration_20260112_220000_add_youtube_ops_v2 from './20260112_220000_add_youtube_ops_v2';
|
||||
import * as migration_20260113_140000_create_yt_series from './20260113_140000_create_yt_series';
|
||||
import * as migration_20260113_180000_add_community_phase1 from './20260113_180000_add_community_phase1';
|
||||
import * as migration_20260114_200000_fix_community_role_enum from './20260114_200000_fix_community_role_enum';
|
||||
|
||||
export const migrations = [
|
||||
{
|
||||
|
|
@ -198,4 +200,14 @@ export const migrations = [
|
|||
down: migration_20260113_140000_create_yt_series.down,
|
||||
name: '20260113_140000_create_yt_series'
|
||||
},
|
||||
{
|
||||
up: migration_20260113_180000_add_community_phase1.up,
|
||||
down: migration_20260113_180000_add_community_phase1.down,
|
||||
name: '20260113_180000_add_community_phase1'
|
||||
},
|
||||
{
|
||||
up: migration_20260114_200000_fix_community_role_enum.up,
|
||||
down: migration_20260114_200000_fix_community_role_enum.down,
|
||||
name: '20260114_200000_fix_community_role_enum'
|
||||
},
|
||||
];
|
||||
|
|
|
|||
File diff suppressed because it is too large
Load diff
|
|
@ -80,6 +80,13 @@ import { YtScriptTemplates } from './collections/YtScriptTemplates'
|
|||
import { YtChecklistTemplates } from './collections/YtChecklistTemplates'
|
||||
import { YtSeries } from './collections/YtSeries'
|
||||
|
||||
// Community Management Collections
|
||||
import { SocialPlatforms } from './collections/SocialPlatforms'
|
||||
import { SocialAccounts } from './collections/SocialAccounts'
|
||||
import { CommunityInteractions } from './collections/CommunityInteractions'
|
||||
import { CommunityTemplates } from './collections/CommunityTemplates'
|
||||
import { CommunityRules } from './collections/CommunityRules'
|
||||
|
||||
// Debug: Minimal test collection - DISABLED (nur für Tests)
|
||||
// import { TestMinimal } from './collections/TestMinimal'
|
||||
|
||||
|
|
@ -230,6 +237,12 @@ export default buildConfig({
|
|||
YtScriptTemplates,
|
||||
YtChecklistTemplates,
|
||||
YtSeries,
|
||||
// Community Management
|
||||
SocialPlatforms,
|
||||
SocialAccounts,
|
||||
CommunityInteractions,
|
||||
CommunityTemplates,
|
||||
CommunityRules,
|
||||
// Debug: Minimal test collection - DISABLED
|
||||
// TestMinimal,
|
||||
// Consent Management
|
||||
|
|
|
|||
Loading…
Reference in a new issue