mirror of
https://github.com/HabitRPG/habitica.git
synced 2025-12-17 14:47:53 +01:00
* WIP(privacy): start of banner * WIP(privacy): layout rough * WIP(privacy): mobile layout, add modal * fix(privacy): implement toggle disable and setting row fold * fix(privacy): clean up a couple of styles * fix(privacy): adjust banner width at mobile sizes * WIP(privacy): remove Loggly echo of Amplitude data * fix(banners): account for privacy in snackbar position * WIP(privacy): dismiss banner * chore(analytics): update to maintaned GA4 library * fix(tests): lint, misuse of apiError * fix(analytics): add debug mode * fix(analytics): load new library on client * WIP(privacy): gtag.js based implementation * fix(analytics): lint issues * fix(lint): one more unused * fix(lint): client errors * feat(privacy): draft workflows * fix(analytics): linting, send needed user values * fix(tests): use mock analytics service in test env * fix(tests): restore previous logic for node env * feat(intro): jump to page 2 onboarding * WIP(auth): revisions to registration flow * WIP(privacy): landing page and banner revisions * WIP(signup): added new username, tos, privacy state * fix(signup): revert debugging logic * WIP(signup): add defaulting and checkbox * wip(signup): move social auth behind username screen * Squashed commit of the following: commit ca0a238e5f008525ed154c5eaf12e44f2fc22b00 Author: Phillip Thelen <phillip@habitica.com> Date: Wed May 7 12:17:20 2025 +0200 make emails lowercase commit a2ce748558ce9134e6825208a7e66d78e720202e Author: Phillip Thelen <phillip@habitica.com> Date: Wed Apr 9 13:27:01 2025 +0200 remove unused import commit cc6ce6c388d9693cf192c4bea733931fc8c31c37 Author: Phillip Thelen <phillip@habitica.com> Date: Wed Apr 9 13:13:03 2025 +0200 add tests for new api route commit 0d40a6230b548625482aa9f6831c93ed9d62533a Author: Kalista Payne <sabrecat@gmail.com> Date: Wed Jun 18 15:50:22 2025 -0500 update social tests commit 79177d6754589b9e54682af8a531b63f60215dab Author: Phillip Thelen <phillip@habitica.com> Date: Wed Apr 9 10:21:51 2025 +0200 new api route to check if an email is available commit 11df73fe07eeb730c2a95593e18e14a931f52429 Author: Phillip Thelen <phillip@habitica.com> Date: Wed Apr 9 10:21:39 2025 +0200 Add field to not register social account when called * Squashed commit of the following: commitb8a2f0b8eeAuthor: CuriousMagpie <eilatan@gmail.com> Date: Thu Jun 20 17:18:30 2024 -0400 update privacy policy * fix(vite): import syntax * feat(auth): precheck on defaulted username * feat(auth): add store action for check-email * feat(auth): check email before proceeding * WIP(login): refactor username screen * WIP(auth): complete login/reg flow * fix(auth): filter out expected 404 * fix(login): use allowRegister with Apple and add z-index to component * fix(login): style corrections and email passthru * Fix edgecase Signed-off-by: Kalista Payne <sabrecat@gmail.com> * fix(auth): correct error behaviors * fix(auth): rewire Apple auth * make check-email check for restricted domains Signed-off-by: Kalista Payne <kalista@habitica.com> * fix(signup): all the style * fix(express): return when responding * fix(error): reduce specificity for restricted domain issue * fix apple auth Signed-off-by: Kalista Payne <kalista@habitica.com> * fix(signup): change from blur to 500ms debounce * fix(login): add missing 200 response in Apple flow * fix(signup): more reconciliation with @phillipthelen's work * fix(signup): now using token not code * fix(reg): don't bail on Apple if we're allowing reg * fix(auth): more reconciliation with @phillipthelen code * feat(copy): privacy policy updates * fix(copy): replace placeholder * fix(vue): use Vite syntax for scss import * fix(static): corrections to copy and css * chore(style): remove excess whitespace * use correct error Signed-off-by: Kalista Payne <kalista@habitica.com> * fix(layout): inputs, add privacy banner * fix(login): button hover, more validation states * fix(login): further layout and UX corrections * fix(static): add back containing div for show/hide * fix(apple): clean out Apple token * fix(settings): only change preference on save * fix(settings): correct save/cancel behavior * fix(layout): consistent use of header/footer * fix(layout): reposition mountains for reg/login/forgot * fix(signup): partial rollback of /username route * refactor(signup): move /username to page * fix(apple): don't overwrite reg method * fix(username): don't skip empty validation * fix(input): don't show valid if no username * fix(login): clean out Apple token if using another method * fix(apple): possible race with token * fix(tests): some housekeeping * fix(config): copypasta * fix(lint): various cleanup * fix(lint): line squeeze * fix(lint): one more v-for * fix(groups): funnel invite flow to new username page * Squashed commit of the following: commit 3c5ba4bf24e4bb7996786520101f27ad66405bce Author: Kalista Payne <kalista@habitica.com> Date: Mon Aug 18 14:38:31 2025 -0500 fix(privacy): update link ref commit9d216f623bAuthor: Kalista Payne <kalista@habitica.com> Date: Mon Aug 18 14:18:22 2025 -0500 fix(privacy-tos): copy edits cont'd commitd744f47140Author: Kalista Payne <kalista@habitica.com> Date: Mon Aug 18 13:43:22 2025 -0500 fix(privacy): copy edits and ToC reflow commit2c3c3fc9ceAuthor: Phillip Thelen <phillip@habitica.com> Date: Mon Aug 18 18:46:24 2025 +0200 lint commitcf363034d5Author: Phillip Thelen <phillip@habitica.com> Date: Mon Aug 18 18:34:54 2025 +0200 fix link commit3afacd2c05Author: Phillip Thelen <phillip@habitica.com> Date: Mon Aug 18 18:34:42 2025 +0200 add updated terms commit258b722499Author: Phillip Thelen <phillip@habitica.com> Date: Mon Aug 18 17:58:42 2025 +0200 put back button to show/hide third party info commit2992e0299bAuthor: Phillip Thelen <phillip@habitica.com> Date: Mon Aug 18 17:58:32 2025 +0200 minor edits commitbb5e252299Author: Kalista Payne <kalista@habitica.com> Date: Sun Aug 17 21:01:50 2025 -0500 fix(privacy): update Section 3 commitc79af7baa8Author: Kalista Payne <kalista@habitica.com> Date: Fri Aug 15 17:28:49 2025 -0500 fix(privacy): various copy edits commit100f2f4574Author: Phillip Thelen <phillip@habitica.com> Date: Fri Aug 15 11:37:37 2025 +0200 add newline commit11d1cfd0d9Author: Phillip Thelen <phillip@habitica.com> Date: Fri Aug 15 11:10:01 2025 +0200 update privacy policy commit59b99badf3Author: Kalista Payne <kalista@habitica.com> Date: Fri Aug 8 14:04:19 2025 -0500 5.38.2 commit78daeb4191Author: Kalista Payne <kalista@habitica.com> Date: Fri Aug 8 13:36:19 2025 -0500 fix(apple): don't run auth middleware during redirect commit93f8d60903Author: Weblate <noreply@weblate.org> Date: Fri Aug 8 10:12:25 2025 +0200 Translated using Weblate (German) Currently translated at 99.4% (185 of 186 strings) Translated using Weblate (Chinese (Simplified)) Currently translated at 100.0% (186 of 186 strings) Translated using Weblate (Spanish) Currently translated at 100.0% (186 of 186 strings) Translated using Weblate (Japanese) Currently translated at 100.0% (54 of 54 strings) Translated using Weblate (Japanese) Currently translated at 100.0% (243 of 243 strings) Translated using Weblate (Japanese) Currently translated at 100.0% (15 of 15 strings) Translated using Weblate (Japanese) Currently translated at 100.0% (47 of 47 strings) Translated using Weblate (Dutch) Currently translated at 78.0% (2643 of 3385 strings) Translated using Weblate (Dutch) Currently translated at 40.8% (100 of 245 strings) Translated using Weblate (Polish) Currently translated at 89.9% (233 of 259 strings) Translated using Weblate (Dutch) Currently translated at 67.5% (175 of 259 strings) Translated using Weblate (Japanese) Currently translated at 100.0% (914 of 914 strings) Translated using Weblate (Japanese) Currently translated at 100.0% (110 of 110 strings) Translated using Weblate (Japanese) Currently translated at 100.0% (914 of 914 strings) Translated using Weblate (Japanese) Currently translated at 100.0% (914 of 914 strings) Translated using Weblate (Chinese (Traditional)) Currently translated at 20.8% (51 of 245 strings) Translated using Weblate (Turkish) Currently translated at 65.9% (60 of 91 strings) Translated using Weblate (Turkish) Currently translated at 65.9% (60 of 91 strings) Translated using Weblate (Chinese (Traditional)) Currently translated at 17.9% (44 of 245 strings) Co-authored-by: FingerTiao <787170918@qq.com> Co-authored-by: Jaime Martí <jaumemarti77@icloud.com> Co-authored-by: Karmelkowy <kicimeow.karmelio@gmail.com> Co-authored-by: Linsey Dunya Pastoor <sekai.creations@gmail.com> Co-authored-by: Mete Olmez <metezori27@gmail.com> Co-authored-by: Sefa Uğurlu <ugurlusefa2@gmail.com> Co-authored-by: Summer_GUI <heyang94@163.com> Co-authored-by: Toro Mor <thomas.bizer@gmx.de> Co-authored-by: Weblate <noreply@weblate.org> Co-authored-by: innnko <ayakabooker@gmail.com> Translate-URL: https://translate.habitica.com/projects/habitica/backgrounds/ja/ Translate-URL: https://translate.habitica.com/projects/habitica/challenge/ja/ Translate-URL: https://translate.habitica.com/projects/habitica/communityguidelines/tr/ Translate-URL: https://translate.habitica.com/projects/habitica/contrib/ja/ Translate-URL: https://translate.habitica.com/projects/habitica/death/ja/ Translate-URL: https://translate.habitica.com/projects/habitica/defaulttasks/ja/ Translate-URL: https://translate.habitica.com/projects/habitica/faq/nl/ Translate-URL: https://translate.habitica.com/projects/habitica/faq/zh_Hant/ Translate-URL: https://translate.habitica.com/projects/habitica/front/de/ Translate-URL: https://translate.habitica.com/projects/habitica/front/es/ Translate-URL: https://translate.habitica.com/projects/habitica/front/zh_Hans/ Translate-URL: https://translate.habitica.com/projects/habitica/gear/nl/ Translate-URL: https://translate.habitica.com/projects/habitica/generic/ja/ Translate-URL: https://translate.habitica.com/projects/habitica/settings/nl/ Translate-URL: https://translate.habitica.com/projects/habitica/settings/pl/ Translation: Habitica/Backgrounds Translation: Habitica/Challenge Translation: Habitica/Communityguidelines Translation: Habitica/Contrib Translation: Habitica/Death Translation: Habitica/Defaulttasks Translation: Habitica/Faq Translation: Habitica/Front Translation: Habitica/Gear Translation: Habitica/Generic Translation: Habitica/Settings commiteb16fec41eAuthor: Phillip Thelen <phillip@habitica.com> Date: Wed Aug 6 22:08:07 2025 +0200 Add interface to block ip-addresses or clients due to abuse (#15484) * Read IP blocks from database * begin building general blocking solution * add new frontend files * Add UI for managing blockers * correctly reset local data after creating blocker * Tweak wording * Add UI for managing blockers * restructure admin pages * improve test coverage * Improve blocker UI * add blocker to block emails from registration * lint fix * fix * lint fixes * fix import * add new permission for managing blockers * improve permission check * fix managing permissions from admin * improve navbar display for non fullAccess admin * update block error strings * lint fix * add option to errorHandler to skip logging * validate blocker value during input * improve blocker form display * chore(subproj): reconcile habitica-images * fix(scripts): use same Mongo version for dev/test * fix(whitespace): eof * documentation improvements * remove nconf import * remove old test --------- Co-authored-by: Kalista Payne <kalista@habitica.com> Co-authored-by: Kalista Payne <sabrecat@gmail.com> commit47d832bf12Author: Fiz <34069775+Hafizzle@users.noreply.github.com> Date: Tue Aug 5 15:12:44 2025 -0500 Add backend support for Hydra mount (#15482) * chore: update time travelers shop to display seasonal backgrounds * chore: update time travelers banner (note CSS borken rn) * chore: fix borken CSS and update logic in shop * chore: added isSubscribed function, not working * chore: isSubscribed working but no bg for subscribers * chore: logic and css updates * chore: update habitica-images * chore: add check for trinket * chore: more time traveler shop logicking * Add backend support for Hydra mount - Add Dragon-Hydra to special mounts in stable.js - Configure as contributor level 7 reward with canFind: true - Add GIF format support for mount sprites - Enable admin panel granting capability * Fix Vue template errors in timeTravelers component * Fix duplicate template block in timeTravelers component * add CSS for Hydra mount GIF sprites Added CSS rules for Mount_Head_Dragon-Hydra and Mount_Body_Dragon-Hydra GIF sprites * Remove the separate Hydra mount dimension declaration --------- Co-authored-by: CuriousMagpie <eilatan@gmail.com> commitc03ab9855fAuthor: Kalista Payne <kalista@habitica.com> Date: Tue Aug 5 14:31:05 2025 -0500 5.38.1 commit8f96b7b7fdAuthor: Weblate <noreply@weblate.org> Date: Tue Aug 5 13:02:45 2025 +0200 Translated using Weblate (Chinese (Traditional)) Currently translated at 17.1% (42 of 245 strings) Translated using Weblate (Chinese (Traditional)) Currently translated at 16.7% (41 of 245 strings) Translated using Weblate (Chinese (Traditional)) Currently translated at 16.3% (40 of 245 strings) Translated using Weblate (Polish) Currently translated at 100.0% (914 of 914 strings) Translated using Weblate (Japanese) Currently translated at 98.8% (425 of 430 strings) Translated using Weblate (French) Currently translated at 99.4% (184 of 185 strings) Translated using Weblate (Chinese (Traditional)) Currently translated at 15.9% (39 of 245 strings) Translated using Weblate (Chinese (Simplified)) Currently translated at 100.0% (268 of 268 strings) Translated using Weblate (Chinese (Simplified)) Currently translated at 100.0% (3385 of 3385 strings) Translated using Weblate (Chinese (Simplified)) Currently translated at 100.0% (185 of 185 strings) Translated using Weblate (Chinese (Simplified)) Currently translated at 100.0% (914 of 914 strings) Translated using Weblate (Chinese (Simplified)) Currently translated at 99.8% (3379 of 3385 strings) Translated using Weblate (Polish) Currently translated at 95.5% (128 of 134 strings) Translated using Weblate (Japanese) Currently translated at 94.7% (254 of 268 strings) Translated using Weblate (Polish) Currently translated at 94.0% (126 of 134 strings) Translated using Weblate (Japanese) Currently translated at 98.6% (424 of 430 strings) Translated using Weblate (Japanese) Currently translated at 98.3% (423 of 430 strings) Translated using Weblate (Japanese) Currently translated at 92.5% (798 of 862 strings) Translated using Weblate (Japanese) Currently translated at 92.4% (797 of 862 strings) Translated using Weblate (Japanese) Currently translated at 90.6% (781 of 862 strings) Translated using Weblate (Japanese) Currently translated at 91.9% (3112 of 3385 strings) Translated using Weblate (Japanese) Currently translated at 91.9% (3111 of 3385 strings) Translated using Weblate (Japanese) Currently translated at 94.0% (174 of 185 strings) Translated using Weblate (Japanese) Currently translated at 100.0% (259 of 259 strings) Translated using Weblate (Japanese) Currently translated at 100.0% (8 of 8 strings) Translated using Weblate (Chinese (Traditional)) Currently translated at 15.5% (38 of 245 strings) Translated using Weblate (Japanese) Currently translated at 91.6% (3104 of 3385 strings) Translated using Weblate (Japanese) Currently translated at 93.5% (173 of 185 strings) Translated using Weblate (Japanese) Currently translated at 99.6% (279 of 280 strings) Translated using Weblate (Spanish) Currently translated at 100.0% (167 of 167 strings) Translated using Weblate (Japanese) Currently translated at 89.2% (769 of 862 strings) Translated using Weblate (Japanese) Currently translated at 100.0% (914 of 914 strings) Translated using Weblate (Japanese) Currently translated at 94.4% (253 of 268 strings) Translated using Weblate (Japanese) Currently translated at 91.8% (170 of 185 strings) Translated using Weblate (Japanese) Currently translated at 97.9% (421 of 430 strings) Translated using Weblate (Japanese) Currently translated at 91.6% (3104 of 3385 strings) Translated using Weblate (Japanese) Currently translated at 93.6% (251 of 268 strings) Translated using Weblate (Japanese) Currently translated at 90.8% (168 of 185 strings) Translated using Weblate (Japanese) Currently translated at 82.4% (202 of 245 strings) Translated using Weblate (French) Currently translated at 100.0% (268 of 268 strings) Translated using Weblate (French) Currently translated at 100.0% (3385 of 3385 strings) Translated using Weblate (Chinese (Traditional)) Currently translated at 15.1% (37 of 245 strings) Translated using Weblate (French) Currently translated at 100.0% (914 of 914 strings) Translated using Weblate (Japanese) Currently translated at 91.3% (3092 of 3385 strings) Translated using Weblate (Japanese) Currently translated at 92.5% (248 of 268 strings) Translated using Weblate (Japanese) Currently translated at 92.5% (248 of 268 strings) Translated using Weblate (Japanese) Currently translated at 100.0% (193 of 193 strings) Translated using Weblate (Croatian) Currently translated at 100.0% (15 of 15 strings) Translated using Weblate (Spanish) Currently translated at 100.0% (167 of 167 strings) Translated using Weblate (Korean) Currently translated at 22.8% (56 of 245 strings) Translated using Weblate (Korean) Currently translated at 47.7% (128 of 268 strings) Translated using Weblate (Croatian) Currently translated at 45.1% (121 of 268 strings) Translated using Weblate (Korean) Currently translated at 71.9% (620 of 862 strings) Translated using Weblate (Croatian) Currently translated at 70.6% (609 of 862 strings) Translated using Weblate (Croatian) Currently translated at 75.0% (6 of 8 strings) Translated using Weblate (Korean) Currently translated at 67.6% (291 of 430 strings) Translated using Weblate (Korean) Currently translated at 52.8% (1788 of 3385 strings) Translated using Weblate (Croatian) Currently translated at 50.3% (1706 of 3385 strings) Translated using Weblate (Croatian) Currently translated at 51.7% (134 of 259 strings) Translated using Weblate (Czech) Currently translated at 92.8% (130 of 140 strings) Translated using Weblate (English (United Kingdom)) Currently translated at 86.9% (233 of 268 strings) Translated using Weblate (Spanish) Currently translated at 100.0% (94 of 94 strings) Translated using Weblate (English (United Kingdom)) Currently translated at 100.0% (94 of 94 strings) Translated using Weblate (Danish) Currently translated at 92.1% (105 of 114 strings) Translated using Weblate (Czech) Currently translated at 89.4% (102 of 114 strings) Translated using Weblate (Czech) Currently translated at 83.5% (112 of 134 strings) Translated using Weblate (Spanish (Latin America)) Currently translated at 71.6% (308 of 430 strings) Translated using Weblate (Spanish (Latin America)) Currently translated at 100.0% (245 of 245 strings) Translated using Weblate (Serbian) Currently translated at 84.4% (49 of 58 strings) Translated using Weblate (Bulgarian) Currently translated at 51.4% (144 of 280 strings) Translated using Weblate (Swedish) Currently translated at 66.5% (286 of 430 strings) Translated using Weblate (Serbian) Currently translated at 65.5% (282 of 430 strings) Translated using Weblate (Slovak) Currently translated at 65.5% (282 of 430 strings) Translated using Weblate (Romanian) Currently translated at 66.7% (287 of 430 strings) Translated using Weblate (English (United Kingdom)) Currently translated at 100.0% (430 of 430 strings) Translated using Weblate (Danish) Currently translated at 66.0% (284 of 430 strings) Translated using Weblate (Czech) Currently translated at 69.7% (300 of 430 strings) Translated using Weblate (Chinese (Simplified)) Currently translated at 99.7% (3377 of 3385 strings) Translated using Weblate (Swedish) Currently translated at 54.1% (1834 of 3385 strings) Translated using Weblate (Serbian) Currently translated at 50.6% (1714 of 3385 strings) Translated using Weblate (Slovak) Currently translated at 50.0% (1695 of 3385 strings) Translated using Weblate (Romanian) Currently translated at 60.5% (2050 of 3385 strings) Translated using Weblate (Hebrew) Currently translated at 38.4% (1301 of 3385 strings) Translated using Weblate (Spanish) Currently translated at 100.0% (3385 of 3385 strings) Translated using Weblate (Danish) Currently translated at 54.0% (1829 of 3385 strings) Translated using Weblate (Czech) Currently translated at 59.6% (2020 of 3385 strings) Translated using Weblate (Swedish) Currently translated at 75.6% (140 of 185 strings) Translated using Weblate (Serbian) Currently translated at 73.5% (136 of 185 strings) Translated using Weblate (Slovak) Currently translated at 84.8% (157 of 185 strings) Translated using Weblate (Romanian) Currently translated at 78.9% (146 of 185 strings) Translated using Weblate (Portuguese) Currently translated at 82.1% (152 of 185 strings) Translated using Weblate (Italian) Currently translated at 91.8% (170 of 185 strings) Translated using Weblate (Spanish) Currently translated at 100.0% (185 of 185 strings) Translated using Weblate (English (United Kingdom)) Currently translated at 100.0% (185 of 185 strings) Translated using Weblate (English (United Kingdom)) Currently translated at 100.0% (185 of 185 strings) Translated using Weblate (German) Currently translated at 100.0% (184 of 184 strings) Translated using Weblate (Danish) Currently translated at 77.2% (143 of 185 strings) Translated using Weblate (English (United Kingdom)) Currently translated at 98.7% (242 of 245 strings) Translated using Weblate (Czech) Currently translated at 75.1% (139 of 185 strings) Translated using Weblate (Bulgarian) Currently translated at 74.5% (138 of 185 strings) Translated using Weblate (Czech) Currently translated at 8.1% (20 of 245 strings) Translated using Weblate (Swedish) Currently translated at 72.0% (621 of 862 strings) Translated using Weblate (Serbian) Currently translated at 65.1% (562 of 862 strings) Translated using Weblate (Slovak) Currently translated at 66.9% (577 of 862 strings) Translated using Weblate (Romanian) Currently translated at 77.7% (670 of 862 strings) Translated using Weblate (Portuguese) Currently translated at 70.0% (604 of 862 strings) Translated using Weblate (Polish) Currently translated at 67.1% (579 of 862 strings) Translated using Weblate (Italian) Currently translated at 86.8% (749 of 862 strings) Translated using Weblate (Indonesian) Currently translated at 86.0% (742 of 862 strings) Translated using Weblate (Hebrew) Currently translated at 66.1% (570 of 862 strings) Translated using Weblate (English (United Kingdom)) Currently translated at 98.0% (845 of 862 strings) Translated using Weblate (Danish) Currently translated at 69.9% (603 of 862 strings) Translated using Weblate (Czech) Currently translated at 69.7% (601 of 862 strings) Translated using Weblate (Bulgarian) Currently translated at 66.3% (572 of 862 strings) Translated using Weblate (Serbian) Currently translated at 74.0% (305 of 412 strings) Translated using Weblate (Turkish) Currently translated at 100.0% (193 of 193 strings) Translated using Weblate (Danish) Currently translated at 90.0% (371 of 412 strings) Translated using Weblate (Ukrainian) Currently translated at 100.0% (259 of 259 strings) Translated using Weblate (Swedish) Currently translated at 53.6% (139 of 259 strings) Translated using Weblate (Spanish) Currently translated at 100.0% (259 of 259 strings) Translated using Weblate (English (United Kingdom)) Currently translated at 100.0% (259 of 259 strings) Translated using Weblate (Danish) Currently translated at 62.1% (161 of 259 strings) Translated using Weblate (Bulgarian) Currently translated at 54.0% (140 of 259 strings) Translated using Weblate (English (United Kingdom)) Currently translated at 82.8% (222 of 268 strings) Translated using Weblate (English (United Kingdom)) Currently translated at 99.4% (184 of 185 strings) Translated using Weblate (English (United Kingdom)) Currently translated at 98.3% (241 of 245 strings) Translated using Weblate (Japanese) Currently translated at 91.3% (3092 of 3385 strings) Translated using Weblate (Japanese) Currently translated at 88.4% (237 of 268 strings) Translated using Weblate (Japanese) Currently translated at 100.0% (134 of 134 strings) Translated using Weblate (Japanese) Currently translated at 100.0% (259 of 259 strings) Translated using Weblate (Japanese) Currently translated at 100.0% (914 of 914 strings) Translated using Weblate (Japanese) Currently translated at 100.0% (243 of 243 strings) Translated using Weblate (Japanese) Currently translated at 82.4% (202 of 245 strings) Translated using Weblate (English (United Kingdom)) Currently translated at 100.0% (259 of 259 strings) Translated using Weblate (Japanese) Currently translated at 87.3% (234 of 268 strings) Translated using Weblate (Japanese) Currently translated at 86.4% (160 of 185 strings) Translated using Weblate (Japanese) Currently translated at 99.8% (913 of 914 strings) Translated using Weblate (German) Currently translated at 100.0% (268 of 268 strings) Translated using Weblate (German) Currently translated at 100.0% (3377 of 3377 strings) Translated using Weblate (German) Currently translated at 100.0% (914 of 914 strings) Translated using Weblate (German) Currently translated at 100.0% (259 of 259 strings) Translated using Weblate (German) Currently translated at 100.0% (259 of 259 strings) Translated using Weblate (German) Currently translated at 100.0% (259 of 259 strings) Translated using Weblate (Spanish) Currently translated at 100.0% (3385 of 3385 strings) Translated using Weblate (Spanish) Currently translated at 100.0% (914 of 914 strings) Translated using Weblate (Spanish) Currently translated at 100.0% (268 of 268 strings) Translated using Weblate (Russian) Currently translated at 88.5% (248 of 280 strings) Translated using Weblate (Spanish) Currently translated at 99.8% (3379 of 3385 strings) Translated using Weblate (German) Currently translated at 100.0% (862 of 862 strings) Co-authored-by: Ayaka Booker <ayakabooker@gmail.com> Co-authored-by: Chaotic Lawful <habitica@eusebius.fr> Co-authored-by: FingerTiao <787170918@qq.com> Co-authored-by: Jaime Martí <jaumemarti77@icloud.com> Co-authored-by: Jan Freihöfer <jan.stauch.is@gmail.com> Co-authored-by: Karmelkowy <kicimeow.karmelio@gmail.com> Co-authored-by: Lio Zam <zerofux@web.de> Co-authored-by: Mika <isekai.chr@gmail.com> Co-authored-by: Sophie LE MASLE <sophiesuff@gmail.com> Co-authored-by: Summer_GUI <heyang94@163.com> Co-authored-by: Vera <verasmolinap@gmail.com> Co-authored-by: Weblate <noreply@weblate.org> Co-authored-by: Zhi Hao Li <zhihaoli000@gmail.com> Co-authored-by: Zuz Q <zuzannakunik@gmail.com> Co-authored-by: innnko <ayakabooker@gmail.com> Co-authored-by: 吳昀錡 <J1120241@gm.fdhs.tyc.edu.tw> Co-authored-by: 潘致翰 <happyq0908@gmail.com> Translate-URL: https://translate.habitica.com/projects/habitica/achievements/es/ Translate-URL: https://translate.habitica.com/projects/habitica/backgrounds/de/ Translate-URL: https://translate.habitica.com/projects/habitica/backgrounds/es/ Translate-URL: https://translate.habitica.com/projects/habitica/backgrounds/fr/ Translate-URL: https://translate.habitica.com/projects/habitica/backgrounds/ja/ Translate-URL: https://translate.habitica.com/projects/habitica/backgrounds/pl/ Translate-URL: https://translate.habitica.com/projects/habitica/backgrounds/zh_Hans/ Translate-URL: https://translate.habitica.com/projects/habitica/character/ja/ Translate-URL: https://translate.habitica.com/projects/habitica/character/tr/ Translate-URL: https://translate.habitica.com/projects/habitica/content/da/ Translate-URL: https://translate.habitica.com/projects/habitica/content/sr/ Translate-URL: https://translate.habitica.com/projects/habitica/death/hr/ Translate-URL: https://translate.habitica.com/projects/habitica/faq/cs/ Translate-URL: https://translate.habitica.com/projects/habitica/faq/en_GB/ Translate-URL: https://translate.habitica.com/projects/habitica/faq/es_419/ Translate-URL: https://translate.habitica.com/projects/habitica/faq/ja/ Translate-URL: https://translate.habitica.com/projects/habitica/faq/ko/ Translate-URL: https://translate.habitica.com/projects/habitica/faq/zh_Hant/ Translate-URL: https://translate.habitica.com/projects/habitica/front/bg/ Translate-URL: https://translate.habitica.com/projects/habitica/front/cs/ Translate-URL: https://translate.habitica.com/projects/habitica/front/da/ Translate-URL: https://translate.habitica.com/projects/habitica/front/de/ Translate-URL: https://translate.habitica.com/projects/habitica/front/en_GB/ Translate-URL: https://translate.habitica.com/projects/habitica/front/es/ Translate-URL: https://translate.habitica.com/projects/habitica/front/fr/ Translate-URL: https://translate.habitica.com/projects/habitica/front/it/ Translate-URL: https://translate.habitica.com/projects/habitica/front/ja/ Translate-URL: https://translate.habitica.com/projects/habitica/front/pt/ Translate-URL: https://translate.habitica.com/projects/habitica/front/ro/ Translate-URL: https://translate.habitica.com/projects/habitica/front/sk/ Translate-URL: https://translate.habitica.com/projects/habitica/front/sr/ Translate-URL: https://translate.habitica.com/projects/habitica/front/sv/ Translate-URL: https://translate.habitica.com/projects/habitica/front/zh_Hans/ Translate-URL: https://translate.habitica.com/projects/habitica/gear/cs/ Translate-URL: https://translate.habitica.com/projects/habitica/gear/da/ Translate-URL: https://translate.habitica.com/projects/habitica/gear/de/ Translate-URL: https://translate.habitica.com/projects/habitica/gear/es/ Translate-URL: https://translate.habitica.com/projects/habitica/gear/fr/ Translate-URL: https://translate.habitica.com/projects/habitica/gear/he/ Translate-URL: https://translate.habitica.com/projects/habitica/gear/hr/ Translate-URL: https://translate.habitica.com/projects/habitica/gear/ja/ Translate-URL: https://translate.habitica.com/projects/habitica/gear/ko/ Translate-URL: https://translate.habitica.com/projects/habitica/gear/ro/ Translate-URL: https://translate.habitica.com/projects/habitica/gear/sk/ Translate-URL: https://translate.habitica.com/projects/habitica/gear/sr/ Translate-URL: https://translate.habitica.com/projects/habitica/gear/sv/ Translate-URL: https://translate.habitica.com/projects/habitica/gear/zh_Hans/ Translate-URL: https://translate.habitica.com/projects/habitica/generic/ja/ Translate-URL: https://translate.habitica.com/projects/habitica/groups/cs/ Translate-URL: https://translate.habitica.com/projects/habitica/groups/da/ Translate-URL: https://translate.habitica.com/projects/habitica/groups/en_GB/ Translate-URL: https://translate.habitica.com/projects/habitica/groups/es_419/ Translate-URL: https://translate.habitica.com/projects/habitica/groups/ja/ Translate-URL: https://translate.habitica.com/projects/habitica/groups/ko/ Translate-URL: https://translate.habitica.com/projects/habitica/groups/ro/ Translate-URL: https://translate.habitica.com/projects/habitica/groups/sk/ Translate-URL: https://translate.habitica.com/projects/habitica/groups/sr/ Translate-URL: https://translate.habitica.com/projects/habitica/groups/sv/ Translate-URL: https://translate.habitica.com/projects/habitica/limited/bg/ Translate-URL: https://translate.habitica.com/projects/habitica/limited/ja/ Translate-URL: https://translate.habitica.com/projects/habitica/limited/ru/ Translate-URL: https://translate.habitica.com/projects/habitica/messages/sr/ Translate-URL: https://translate.habitica.com/projects/habitica/npc/cs/ Translate-URL: https://translate.habitica.com/projects/habitica/npc/ja/ Translate-URL: https://translate.habitica.com/projects/habitica/npc/pl/ Translate-URL: https://translate.habitica.com/projects/habitica/overview/hr/ Translate-URL: https://translate.habitica.com/projects/habitica/overview/ja/ Translate-URL: https://translate.habitica.com/projects/habitica/pets/cs/ Translate-URL: https://translate.habitica.com/projects/habitica/pets/da/ Translate-URL: https://translate.habitica.com/projects/habitica/quests/en_GB/ Translate-URL: https://translate.habitica.com/projects/habitica/quests/es/ Translate-URL: https://translate.habitica.com/projects/habitica/questscontent/bg/ Translate-URL: https://translate.habitica.com/projects/habitica/questscontent/cs/ Translate-URL: https://translate.habitica.com/projects/habitica/questscontent/da/ Translate-URL: https://translate.habitica.com/projects/habitica/questscontent/de/ Translate-URL: https://translate.habitica.com/projects/habitica/questscontent/en_GB/ Translate-URL: https://translate.habitica.com/projects/habitica/questscontent/he/ Translate-URL: https://translate.habitica.com/projects/habitica/questscontent/hr/ Translate-URL: https://translate.habitica.com/projects/habitica/questscontent/id/ Translate-URL: https://translate.habitica.com/projects/habitica/questscontent/it/ Translate-URL: https://translate.habitica.com/projects/habitica/questscontent/ja/ Translate-URL: https://translate.habitica.com/projects/habitica/questscontent/ko/ Translate-URL: https://translate.habitica.com/projects/habitica/questscontent/pl/ Translate-URL: https://translate.habitica.com/projects/habitica/questscontent/pt/ Translate-URL: https://translate.habitica.com/projects/habitica/questscontent/ro/ Translate-URL: https://translate.habitica.com/projects/habitica/questscontent/sk/ Translate-URL: https://translate.habitica.com/projects/habitica/questscontent/sr/ Translate-URL: https://translate.habitica.com/projects/habitica/questscontent/sv/ Translate-URL: https://translate.habitica.com/projects/habitica/settings/bg/ Translate-URL: https://translate.habitica.com/projects/habitica/settings/da/ Translate-URL: https://translate.habitica.com/projects/habitica/settings/de/ Translate-URL: https://translate.habitica.com/projects/habitica/settings/en_GB/ Translate-URL: https://translate.habitica.com/projects/habitica/settings/es/ Translate-URL: https://translate.habitica.com/projects/habitica/settings/hr/ Translate-URL: https://translate.habitica.com/projects/habitica/settings/ja/ Translate-URL: https://translate.habitica.com/projects/habitica/settings/sv/ Translate-URL: https://translate.habitica.com/projects/habitica/settings/uk/ Translate-URL: https://translate.habitica.com/projects/habitica/subscriber/de/ Translate-URL: https://translate.habitica.com/projects/habitica/subscriber/en_GB/ Translate-URL: https://translate.habitica.com/projects/habitica/subscriber/es/ Translate-URL: https://translate.habitica.com/projects/habitica/subscriber/fr/ Translate-URL: https://translate.habitica.com/projects/habitica/subscriber/hr/ Translate-URL: https://translate.habitica.com/projects/habitica/subscriber/ja/ Translate-URL: https://translate.habitica.com/projects/habitica/subscriber/ko/ Translate-URL: https://translate.habitica.com/projects/habitica/subscriber/zh_Hans/ Translate-URL: https://translate.habitica.com/projects/habitica/tasks/cs/ Translation: Habitica/Achievements Translation: Habitica/Backgrounds Translation: Habitica/Character Translation: Habitica/Content Translation: Habitica/Death Translation: Habitica/Faq Translation: Habitica/Front Translation: Habitica/Gear Translation: Habitica/Generic Translation: Habitica/Groups Translation: Habitica/Limited Translation: Habitica/Messages Translation: Habitica/Npc Translation: Habitica/Overview Translation: Habitica/Pets Translation: Habitica/Quests Translation: Habitica/Questscontent Translation: Habitica/Settings Translation: Habitica/Subscriber Translation: Habitica/Tasks commit1dde2674f6Author: Kalista Payne <sabrecat@gmail.com> Date: Mon Jun 16 16:43:56 2025 -0500 fix(content): don't filter out the thing we want commit76122a8889Author: Kalista Payne <sabrecat@gmail.com> Date: Wed Jun 4 14:28:27 2025 -0500 fix(mobile): provide Challenge categories via API commit9e309a875eAuthor: Kalista Payne <kalista@habitica.com> Date: Mon Jul 28 14:15:00 2025 -0500 5.38.0 commit09e3a394b8Author: Kalista Payne <kalista@habitica.com> Date: Mon Jul 28 14:06:45 2025 -0500 5.37.3 commiteba263360fAuthor: Weblate <noreply@weblate.org> Date: Mon Jul 28 21:03:17 2025 +0200 Translated using Weblate (German) Currently translated at 100.0% (134 of 134 strings) Translated using Weblate (German) Currently translated at 100.0% (134 of 134 strings) Translated using Weblate (German) Currently translated at 100.0% (3377 of 3377 strings) Translated using Weblate (German) Currently translated at 100.0% (3377 of 3377 strings) Translated using Weblate (German) Currently translated at 100.0% (3377 of 3377 strings) Translated using Weblate (German) Currently translated at 100.0% (243 of 243 strings) Translated using Weblate (German) Currently translated at 100.0% (184 of 184 strings) Translated using Weblate (German) Currently translated at 98.6% (850 of 862 strings) Translated using Weblate (German) Currently translated at 99.8% (3373 of 3377 strings) Translated using Weblate (German) Currently translated at 99.8% (3373 of 3377 strings) Translated using Weblate (German) Currently translated at 99.8% (3373 of 3377 strings) Translated using Weblate (German) Currently translated at 99.5% (3361 of 3377 strings) Translated using Weblate (German) Currently translated at 99.5% (3361 of 3377 strings) Translated using Weblate (German) Currently translated at 99.5% (3361 of 3377 strings) Translated using Weblate (German) Currently translated at 99.4% (3360 of 3377 strings) Translated using Weblate (German) Currently translated at 100.0% (184 of 184 strings) Translated using Weblate (Spanish) Currently translated at 100.0% (185 of 185 strings) Translated using Weblate (Polish) Currently translated at 67.1% (579 of 862 strings) Translated using Weblate (Polish) Currently translated at 67.1% (579 of 862 strings) Translated using Weblate (Polish) Currently translated at 100.0% (91 of 91 strings) Translated using Weblate (Polish) Currently translated at 100.0% (91 of 91 strings) Translated using Weblate (German) Currently translated at 100.0% (184 of 184 strings) Translated using Weblate (German) Currently translated at 100.0% (184 of 184 strings) Translated using Weblate (German) Currently translated at 100.0% (184 of 184 strings) Translated using Weblate (German) Currently translated at 100.0% (184 of 184 strings) Translated using Weblate (German) Currently translated at 100.0% (184 of 184 strings) Translated using Weblate (German) Currently translated at 100.0% (245 of 245 strings) Translated using Weblate (German) Currently translated at 100.0% (47 of 47 strings) Translated using Weblate (German) Currently translated at 100.0% (193 of 193 strings) Translated using Weblate (Chinese (Traditional)) Currently translated at 14.2% (35 of 245 strings) Translated using Weblate (Chinese (Traditional)) Currently translated at 13.8% (34 of 245 strings) Translated using Weblate (Chinese (Traditional)) Currently translated at 13.0% (32 of 245 strings) Translated using Weblate (Hebrew) Currently translated at 2.0% (5 of 245 strings) Translated using Weblate (Hebrew) Currently translated at 66.1% (570 of 862 strings) Translated using Weblate (Portuguese) Currently translated at 54.1% (1830 of 3377 strings) Co-authored-by: FingerTiao <787170918@qq.com> Co-authored-by: Jaime Martí <jaumemarti77@icloud.com> Co-authored-by: Jan Freihöfer <jan.stauch.is@gmail.com> Co-authored-by: Jonathan Niessen <37.friedrich@gmail.com> Co-authored-by: Karmelkowy <kicimeow.karmelio@gmail.com> Co-authored-by: Katharina <katharinaanna.wilding@gmail.com> Co-authored-by: Laura Fleckenstein <fleckenstein_laura@web.de> Co-authored-by: Omer I.S <omeritzicschwartz@gmail.com> Co-authored-by: Remigiusz Haziak <haziakremigiusz@gmail.com> Co-authored-by: Uwe B <hbtca@tunixgut.de> Co-authored-by: Weblate <noreply@weblate.org> Co-authored-by: Wellinton Cardoso <wmcardoso1@hotmail.com> Co-authored-by: cloudzzy <truskawka412@gmail.com> Co-authored-by: 吳昀錡 <J1120241@gm.fdhs.tyc.edu.tw> Translate-URL: https://translate.habitica.com/projects/habitica/character/de/ Translate-URL: https://translate.habitica.com/projects/habitica/communityguidelines/pl/ Translate-URL: https://translate.habitica.com/projects/habitica/contrib/de/ Translate-URL: https://translate.habitica.com/projects/habitica/faq/de/ Translate-URL: https://translate.habitica.com/projects/habitica/faq/he/ Translate-URL: https://translate.habitica.com/projects/habitica/faq/zh_Hant/ Translate-URL: https://translate.habitica.com/projects/habitica/front/de/ Translate-URL: https://translate.habitica.com/projects/habitica/front/es/ Translate-URL: https://translate.habitica.com/projects/habitica/gear/de/ Translate-URL: https://translate.habitica.com/projects/habitica/gear/pt/ Translate-URL: https://translate.habitica.com/projects/habitica/generic/de/ Translate-URL: https://translate.habitica.com/projects/habitica/npc/de/ Translate-URL: https://translate.habitica.com/projects/habitica/questscontent/de/ Translate-URL: https://translate.habitica.com/projects/habitica/questscontent/he/ Translate-URL: https://translate.habitica.com/projects/habitica/questscontent/pl/ Translation: Habitica/Character Translation: Habitica/Communityguidelines Translation: Habitica/Contrib Translation: Habitica/Faq Translation: Habitica/Front Translation: Habitica/Gear Translation: Habitica/Generic Translation: Habitica/Npc Translation: Habitica/Questscontent commit9550eec718Author: Phillip Thelen <phillip@habitica.com> Date: Mon Jul 28 16:50:38 2025 +0200 Fix 500 when deleting a very old group plan account (#15481) commitf267eb67e9Author: Kalista Payne <kalista@habitica.com> Date: Tue Jul 29 14:12:35 2025 -0500 fix(static): add back missing div for show/hide commit28251f42abAuthor: Kalista Payne <kalista@habitica.com> Date: Thu Jul 24 22:59:01 2025 -0500 feat(privacy): preview page * feat(privacy): respect Global Privacy Control * fix(lint): remove unused component * fix(test): test user opts in to tracking * fix(test): add user pref to more contexts * fix(test): final spot in api-unit * fix(tests): update integrations * chore(privacy): add paragraph to s1, retire separate preview pages * fix(build): route copypasta * fix(router): lingering dead import --------- Signed-off-by: Kalista Payne <sabrecat@gmail.com> Signed-off-by: Kalista Payne <kalista@habitica.com> Co-authored-by: Phillip Thelen <phillip@habitica.com> Co-authored-by: CuriousMagpie <eilatan@gmail.com>
1816 lines
55 KiB
JavaScript
1816 lines
55 KiB
JavaScript
import cloneDeep from 'lodash/cloneDeep';
|
|
import forEach from 'lodash/forEach';
|
|
import isFunction from 'lodash/isFunction';
|
|
import pick from 'lodash/pick';
|
|
import nconf from 'nconf';
|
|
import get from 'lodash/get';
|
|
import { authWithHeaders } from '../../middlewares/auth';
|
|
import common from '../../../common';
|
|
import {
|
|
BadRequest,
|
|
NotAuthorized,
|
|
} from '../../libs/errors';
|
|
import {
|
|
basicFields as basicGroupFields,
|
|
model as Group,
|
|
} from '../../models/group';
|
|
import * as Tasks from '../../models/task';
|
|
import * as passwordUtils from '../../libs/password';
|
|
import {
|
|
userActivityWebhook,
|
|
} from '../../libs/webhook';
|
|
import {
|
|
getUserInfo,
|
|
sendTxn,
|
|
} from '../../libs/email';
|
|
import * as inboxLib from '../../libs/inbox';
|
|
import * as userLib from '../../libs/user';
|
|
import { model as UserHistory } from '../../models/userHistory';
|
|
|
|
const OFFICIAL_PLATFORMS = ['habitica-web', 'habitica-ios', 'habitica-android'];
|
|
const TECH_ASSISTANCE_EMAIL = nconf.get('EMAILS_TECH_ASSISTANCE_EMAIL');
|
|
const DELETE_CONFIRMATION = 'DELETE';
|
|
|
|
/**
|
|
* @apiDefine UserNotFound
|
|
* @apiError (404) {NotFound} UserNotFound The specified user could not be found.
|
|
*/
|
|
|
|
const api = {};
|
|
|
|
/* NOTE this route has also an API v4 version */
|
|
|
|
/**
|
|
* @api {get} /api/v3/user Get the authenticated user's profile
|
|
* @apiName UserGet
|
|
* @apiGroup User
|
|
*
|
|
* @apiDescription The user profile contains data related to the authenticated
|
|
* user including (but not limited to):
|
|
* Achievements;
|
|
* Authentications (including types and timestamps);
|
|
* Challenges memberships (Challenge IDs);
|
|
* Flags (including armoire, tutorial, tour etc...);
|
|
* Guilds memberships (Guild IDs);
|
|
* History (including timestamps and values, only for Experience and summed To Do values);
|
|
* Inbox;
|
|
* Invitations (to parties/guilds);
|
|
* Items (character's full inventory);
|
|
* New Messages (flags for party/guilds that have new messages; also reported in Notifications);
|
|
* Notifications;
|
|
* Party (includes current quest information);
|
|
* Preferences (user selected prefs);
|
|
* Profile (name, photo url, blurb);
|
|
* Purchased (includes subscription data and some gem-purchased items);
|
|
* PushDevices (identifiers for mobile devices authorized);
|
|
* Stats (standard RPG stats, class, buffs, xp, etc..);
|
|
* Tags;
|
|
* TasksOrder (list of all IDs for Dailys, Habits, Rewards and To Do's).
|
|
*
|
|
* @apiParam (Query) {String} [userFields] A list of comma-separated user fields to
|
|
* be returned instead of the entire document.
|
|
* Notifications are always returned.
|
|
*
|
|
* @apiExample {curl} Example use:
|
|
* curl -i https://habitica.com/api/v3/user?userFields=achievements,items.mounts
|
|
*
|
|
* @apiSuccess {Object} data The user object
|
|
*
|
|
* @apiSuccessExample {json} Result:
|
|
* {
|
|
* "success": true,
|
|
* "data": {
|
|
* -- User data included here, for details of the user model see:
|
|
* -- https://github.com/HabitRPG/habitica/tree/develop/website/server/models/user
|
|
* }
|
|
* }
|
|
*
|
|
*/
|
|
api.getUser = {
|
|
method: 'GET',
|
|
middlewares: [authWithHeaders()],
|
|
url: '/user',
|
|
async handler (req, res) {
|
|
await userLib.get(req, res, { isV3: true });
|
|
},
|
|
};
|
|
|
|
/**
|
|
* @api {get} /api/v3/user/inventory/buy
|
|
* Get equipment/gear items available for purchase for the authenticated user
|
|
* @apiName UserGetBuyList
|
|
* @apiGroup User
|
|
*
|
|
* @apiSuccessExample {json} Success-Response:
|
|
* {
|
|
* "success": true,
|
|
* "data": [
|
|
* {
|
|
* "text": "Training Sword",
|
|
* "notes": "Practice weapon. Confers no benefit.",
|
|
* "value": 1,
|
|
* "type": "weapon",
|
|
* "key": "weapon_warrior_0",
|
|
* "set": "warrior-0",
|
|
* "klass": "warrior",
|
|
* "index": "0",
|
|
* "str": 0,
|
|
* "int": 0,
|
|
* "per": 0,
|
|
* "con": 0
|
|
* }
|
|
* ]
|
|
* }
|
|
*/
|
|
api.getBuyList = {
|
|
method: 'GET',
|
|
middlewares: [authWithHeaders()],
|
|
url: '/user/inventory/buy',
|
|
async handler (req, res) {
|
|
const list = cloneDeep(common.updateStore(res.locals.user));
|
|
|
|
// return text and notes strings
|
|
forEach(list, item => {
|
|
forEach(item, (itemPropVal, itemPropKey) => {
|
|
if (
|
|
isFunction(itemPropVal)
|
|
&& itemPropVal.i18nLangFunc
|
|
) item[itemPropKey] = itemPropVal(req.language);
|
|
});
|
|
});
|
|
|
|
res.respond(200, list);
|
|
},
|
|
};
|
|
|
|
/**
|
|
* @api {get} /api/v3/user/in-app-rewards Get the in app items appearing in the user's reward column
|
|
* @apiName UserGetInAppRewards
|
|
* @apiGroup User
|
|
*
|
|
* @apiSuccessExample {json} Success-Response:
|
|
* {
|
|
* "success": true,
|
|
* "data": [
|
|
* {
|
|
* "key":"weapon_armoire_battleAxe",
|
|
* "text":"Battle Axe",
|
|
* "notes":"This fine iron axe is well-suited to battling your fiercest
|
|
* foes or your most difficult tasks. Increases Intelligence by 6 and
|
|
* Constitution by 8. Enchanted Armoire: Independent Item.",
|
|
* "value":1,
|
|
* "type":"weapon",
|
|
* "locked":false,
|
|
* "currency":"gems",
|
|
* "purchaseType":"gear",
|
|
* "class":"shop_weapon_armoire_battleAxe",
|
|
* "path":"gear.flat.weapon_armoire_battleAxe",
|
|
* "pinType":"gear"
|
|
* }
|
|
* ]
|
|
* }
|
|
*/
|
|
api.getInAppRewardsList = {
|
|
method: 'GET',
|
|
middlewares: [authWithHeaders({ userFieldsToInclude: ['items', 'pinnedItems', 'unpinnedItems', 'pinnedItemsOrder', 'stats.class', 'achievements', 'purchased'] })],
|
|
url: '/user/in-app-rewards',
|
|
async handler (req, res) {
|
|
const list = common.inAppRewards(res.locals.user);
|
|
|
|
// return text and notes strings
|
|
forEach(list, item => {
|
|
forEach(item, (itemPropVal, itemPropKey) => {
|
|
if (
|
|
isFunction(itemPropVal)
|
|
&& itemPropVal.i18nLangFunc
|
|
) item[itemPropKey] = itemPropVal(req.language);
|
|
});
|
|
});
|
|
|
|
res.respond(200, list);
|
|
},
|
|
};
|
|
|
|
/* NOTE this route has also an API v4 version */
|
|
|
|
/**
|
|
* @api {put} /api/v3/user Update the user
|
|
* @apiName UserUpdate
|
|
* @apiGroup User
|
|
*
|
|
* @apiDescription Some of the user items can be updated, such as preferences, flags and stats.
|
|
^
|
|
* @apiParamExample {json} Request-Example:
|
|
* {
|
|
* "achievements.habitBirthdays": 2,
|
|
* "profile.name": "MadPink",
|
|
* "stats.hp": 53,
|
|
* "flags.warnedLowHealth":false,
|
|
* "preferences.allocationMode":"flat",
|
|
* "preferences.hair.bangs": 3
|
|
* }
|
|
*
|
|
* @apiSuccess {Object} data The updated user object, the result is identical to the get user call
|
|
*
|
|
* @apiError (401) {NotAuthorized} messageUserOperationProtected Returned if the change
|
|
* is not allowed.
|
|
*
|
|
* @apiErrorExample {json} Error-Response:
|
|
* {
|
|
* "success": false,
|
|
* "error": "NotAuthorized",
|
|
* "message": "path `stats.class` was not saved, as it's a protected path."
|
|
* }
|
|
*/
|
|
api.updateUser = {
|
|
method: 'PUT',
|
|
middlewares: [authWithHeaders()],
|
|
url: '/user',
|
|
async handler (req, res) {
|
|
await userLib.update(req, res, { isV3: true });
|
|
},
|
|
};
|
|
|
|
/**
|
|
* @api {delete} /api/v3/user Delete an authenticated user's account
|
|
* @apiName UserDelete
|
|
* @apiGroup User
|
|
*
|
|
* @apiParam (Body) {String} password The user's password if the account uses local authentication,
|
|
* otherwise the localized word "DELETE"
|
|
* @apiParam (Body) {String} feedback User's optional feedback explaining reasons for deletion
|
|
*
|
|
* @apiSuccess {Object} data An empty Object
|
|
*
|
|
* @apiSuccessExample {json} Result:
|
|
* {
|
|
* "success": true,
|
|
* "data": {}
|
|
* }
|
|
*
|
|
* @apiError {BadRequest} MissingPassword Missing password.
|
|
* @apiError {BadRequest} NotAuthorized Wrong password.
|
|
* @apiError {BadRequest} NotAuthorized Please type DELETE in all capital letters to
|
|
* delete your account.
|
|
* @apiError {BadRequest} BadRequest Account deletion feedback is limited to 10,000 characters.
|
|
* For lengthy feedback, email ${TECH_ASSISTANCE_EMAIL}.
|
|
* @apiError {BadRequest} NotAuthorized You have an active subscription,
|
|
* cancel your plan before deleting your account.
|
|
*
|
|
* @apiErrorExample {json} Example error:
|
|
* {
|
|
* "success": false,
|
|
* "error": "BadRequest",
|
|
* "message": "Invalid request parameters.",
|
|
* "errors": [
|
|
* {
|
|
* "message": "Missing password.",
|
|
* "param": "password"
|
|
* }
|
|
* ]
|
|
* }
|
|
*
|
|
*/
|
|
api.deleteUser = {
|
|
method: 'DELETE',
|
|
middlewares: [authWithHeaders()],
|
|
url: '/user',
|
|
async handler (req, res) {
|
|
const { user } = res.locals;
|
|
const { plan } = user.purchased;
|
|
|
|
const { password } = req.body;
|
|
if (!password) throw new BadRequest(res.t('missingPassword'));
|
|
|
|
if (user.auth.local.hashed_password && user.auth.local.email) {
|
|
const isValidPassword = await passwordUtils.compare(user, password);
|
|
if (!isValidPassword) throw new NotAuthorized(res.t('wrongPassword'));
|
|
} else if (
|
|
(user.auth.facebook.id || user.auth.google.id || user.auth.apple.id)
|
|
&& password !== DELETE_CONFIRMATION
|
|
) {
|
|
throw new NotAuthorized(res.t('incorrectDeletePhrase', { magicWord: DELETE_CONFIRMATION }));
|
|
}
|
|
|
|
const { feedback } = req.body;
|
|
if (feedback && feedback.length > 10000) throw new BadRequest(`Account deletion feedback is limited to 10,000 characters. For lengthy feedback, email ${TECH_ASSISTANCE_EMAIL}.`); // @TODO localize this string
|
|
|
|
if (plan && plan.customerId && !plan.dateTerminated) {
|
|
throw new NotAuthorized(res.t('cannotDeleteActiveAccount'));
|
|
}
|
|
|
|
const types = ['party', 'guilds'];
|
|
const groupFields = basicGroupFields.concat(' leader memberCount purchased');
|
|
|
|
const groupsUserIsMemberOf = await Group.getGroups({ user, types, groupFields });
|
|
|
|
const groupLeavePromises = groupsUserIsMemberOf.map(group => group.leave(user, 'remove-all'));
|
|
|
|
await Promise.all(groupLeavePromises);
|
|
|
|
await Tasks.Task.deleteMany({
|
|
userId: user._id,
|
|
}).exec();
|
|
|
|
await user.deleteOne();
|
|
|
|
if (feedback) {
|
|
sendTxn({ email: TECH_ASSISTANCE_EMAIL }, 'admin-feedback', [
|
|
{ name: 'PROFILE_NAME', content: user.profile.name },
|
|
{ name: 'USERNAME', content: user.auth.local.username },
|
|
{ name: 'UUID', content: user._id },
|
|
{ name: 'EMAIL', content: getUserInfo(user, ['email']).email },
|
|
{ name: 'FEEDBACK_SOURCE', content: 'from deletion form' },
|
|
{ name: 'FEEDBACK', content: feedback },
|
|
]);
|
|
}
|
|
|
|
res.analytics.track('account delete', {
|
|
user: pick(user, ['preferences', 'registeredThrough']),
|
|
uuid: user._id,
|
|
hitType: 'event',
|
|
category: 'behavior',
|
|
});
|
|
|
|
res.respond(200, {});
|
|
},
|
|
};
|
|
|
|
function _cleanChecklist (task) {
|
|
forEach(task.checklist, (c, i) => {
|
|
c.text = `item ${i}`;
|
|
});
|
|
}
|
|
|
|
/**
|
|
* @api {get} /api/v3/user/anonymized Get anonymized user data
|
|
* @apiName UserGetAnonymized
|
|
* @apiGroup User
|
|
*
|
|
* @apiDescription Returns the user's data without:
|
|
* Authentication information,
|
|
* NewMessages/Invitations/Inbox,
|
|
* Profile,
|
|
* Purchased information,
|
|
* Contributor information,
|
|
* Special items,
|
|
* Webhooks,
|
|
* Notifications.
|
|
*
|
|
* @apiSuccess {Object} data.user
|
|
* @apiSuccess {Object} data.tasks
|
|
* */
|
|
api.getUserAnonymized = {
|
|
method: 'GET',
|
|
middlewares: [authWithHeaders()],
|
|
url: '/user/anonymized',
|
|
async handler (req, res) {
|
|
const user = await res.locals.user.toJSONWithInbox();
|
|
user.stats.toNextLevel = common.tnl(user.stats.lvl);
|
|
user.stats.maxHealth = common.maxHealth;
|
|
user.stats.maxMP = common.statsComputed(res.locals.user).maxMP;
|
|
|
|
delete user.apiToken;
|
|
if (user.auth) {
|
|
delete user.auth.local;
|
|
delete user.auth.facebook;
|
|
delete user.auth.google;
|
|
delete user.auth.apple;
|
|
}
|
|
delete user.newMessages;
|
|
delete user.profile;
|
|
delete user.purchased.plan;
|
|
delete user.contributor;
|
|
delete user.invitations;
|
|
delete user.items.special.nyeReceived;
|
|
delete user.items.special.valentineReceived;
|
|
delete user.webhooks;
|
|
delete user.achievements.challenges;
|
|
delete user.notifications;
|
|
delete user.secret;
|
|
delete user.permissions;
|
|
|
|
forEach(user.inbox.messages, msg => {
|
|
msg.text = 'inbox message text';
|
|
});
|
|
|
|
forEach(user.tags, tag => {
|
|
tag.name = 'tag';
|
|
tag.challenge = 'challenge';
|
|
});
|
|
|
|
const query = {
|
|
userId: user._id,
|
|
$or: [
|
|
{ type: 'todo', completed: false },
|
|
{ type: { $in: ['habit', 'daily', 'reward'] } },
|
|
],
|
|
};
|
|
const tasks = await Tasks.Task.find(query).exec();
|
|
|
|
forEach(tasks, task => {
|
|
task.text = 'task text';
|
|
task.notes = 'task notes';
|
|
if (task.type === 'todo' || task.type === 'daily') {
|
|
_cleanChecklist(task);
|
|
}
|
|
});
|
|
|
|
return res.respond(200, { user, tasks });
|
|
},
|
|
};
|
|
|
|
/**
|
|
* @api {post} /api/v3/user/sleep Make the user start / stop sleeping (resting in the Inn)
|
|
* @apiName UserSleep
|
|
* @apiGroup User
|
|
*
|
|
* @apiDescription Toggles the sleep key under user preference true and false.
|
|
*
|
|
* @apiSuccess {boolean} data user.preferences.sleep
|
|
*
|
|
* @apiSuccessExample {json} Return-example
|
|
* {
|
|
* "success": true,
|
|
* "data": false
|
|
* }
|
|
*/
|
|
api.sleep = {
|
|
method: 'POST',
|
|
middlewares: [authWithHeaders()],
|
|
url: '/user/sleep',
|
|
async handler (req, res) {
|
|
const { user } = res.locals;
|
|
const sleepRes = common.ops.sleep(user, req, res.analytics);
|
|
await user.save();
|
|
res.respond(200, ...sleepRes);
|
|
},
|
|
};
|
|
|
|
const buySpecialKeys = ['snowball', 'spookySparkles', 'shinySeed', 'seafoam'];
|
|
const buyKnownKeys = ['armoire', 'mystery', 'potion', 'quest', 'special'];
|
|
|
|
/**
|
|
* @api {post} /api/v3/user/buy/:key Buy gear, armoire or potion
|
|
* @apiDescription Under the hood uses UserBuyGear, UserBuyPotion and UserBuyArmoire
|
|
* @apiName UserBuy
|
|
* @apiGroup User
|
|
*
|
|
* @apiParam (Path) {String} key The item to buy
|
|
*
|
|
* @apiSuccess data User's data profile
|
|
* @apiSuccess message Item purchased
|
|
*
|
|
* @apiSuccessExample {json} Purchased a rogue short sword for example:
|
|
* {
|
|
* "success": true,
|
|
* "data": {
|
|
* ---TRUNCATED USER RECORD---
|
|
* },
|
|
* "message": "Bought Short Sword"
|
|
* }
|
|
*
|
|
* @apiError (400) {NotAuthorized} messageAlreadyOwnGear Already own equipment
|
|
* @apiError (400) {NotAuthorized} messageNotEnoughGold Not enough gold for the purchase
|
|
*
|
|
* @apiErrorExample {json} NotAuthorized Already own
|
|
* {"success":false,"error":"NotAuthorized","message":"You already own that piece of equipment"}
|
|
*
|
|
* @apiErrorExample {json} NotAuthorized Not enough gold
|
|
* {"success":false,"error":"NotAuthorized","message":"Not Enough Gold"}
|
|
*/
|
|
api.buy = {
|
|
method: 'POST',
|
|
middlewares: [authWithHeaders()],
|
|
url: '/user/buy/:key',
|
|
async handler (req, res) {
|
|
const { user } = res.locals;
|
|
|
|
// @TODO: Remove this when mobile passes type in body
|
|
const type = req.params.key;
|
|
if (buySpecialKeys.indexOf(type) !== -1) {
|
|
req.type = 'special';
|
|
} else if (buyKnownKeys.indexOf(type) === -1) {
|
|
req.type = 'marketGear';
|
|
}
|
|
|
|
// @TODO: right now common follow express structure, but we should decouple the dependency
|
|
if (req.body.type) req.type = req.body.type;
|
|
|
|
let quantity = 1;
|
|
if (req.body.quantity) quantity = req.body.quantity;
|
|
req.quantity = quantity;
|
|
if (OFFICIAL_PLATFORMS.indexOf(req.headers['x-client']) === -1) {
|
|
res.analytics = undefined;
|
|
}
|
|
const buyRes = await common.ops.buy(user, req, res.analytics);
|
|
|
|
await user.save();
|
|
|
|
if (type === 'armoire') {
|
|
await UserHistory.beginUserHistoryUpdate(user._id, req.headers)
|
|
.withArmoire(buyRes[0].armoire.dropKey || 'experience')
|
|
.commit();
|
|
}
|
|
|
|
res.respond(200, ...buyRes);
|
|
},
|
|
};
|
|
|
|
/**
|
|
* @api {post} /api/v3/user/buy-gear/:key Buy a piece of gear
|
|
* @apiName UserBuyGear
|
|
* @apiGroup User
|
|
*
|
|
* @apiParam (Path) {String} key The item to buy
|
|
*
|
|
* @apiSuccess {Object} data.items User's item inventory
|
|
* @apiSuccess {Object} data.flags User's flags
|
|
* @apiSuccess {Object} data.achievements User's achievements
|
|
* @apiSuccess {Object} data.stats User's current stats
|
|
* @apiSuccess {String} message Success message, item purchased
|
|
*
|
|
* @apiSuccessExample {json} Purchased a warrior's wooden shield for example:
|
|
* {
|
|
* "success": true,
|
|
* "data": {
|
|
* ---TRUNCATED USER RECORD---
|
|
* },
|
|
* "message": "Bought Wooden Shield"
|
|
* }
|
|
*
|
|
* @apiError (400) {NotAuthorized} messageNotEnoughGold Not enough gold for the purchase
|
|
* @apiError (400) {NotAuthorized} messageAlreadyOwnGear Already own equipment
|
|
* @apiError (404) {NotFound} messageNotFound Item does not exist.
|
|
*
|
|
* @apiErrorExample {json} NotAuthorized Already own
|
|
* {"success":false,"error":"NotAuthorized","message":"You already own that piece of equipment"}
|
|
*
|
|
* @apiErrorExample {json} NotAuthorized Not enough gold
|
|
* {"success":false,"error":"NotAuthorized","message":"Not Enough Gold"}
|
|
*
|
|
* @apiErrorExample {json} NotFound Item not found
|
|
* {"success":false,"error":"NotFound","message":"Item \"weapon_misspelled_1\" not found."}
|
|
*/
|
|
api.buyGear = {
|
|
method: 'POST',
|
|
middlewares: [authWithHeaders()],
|
|
url: '/user/buy-gear/:key',
|
|
async handler (req, res) {
|
|
const { user } = res.locals;
|
|
const buyGearRes = await common.ops.buy(user, req, res.analytics);
|
|
await user.save();
|
|
res.respond(200, ...buyGearRes);
|
|
},
|
|
};
|
|
|
|
/**
|
|
* @api {post} /api/v3/user/buy-armoire Buy an Enchanted Armoire item
|
|
* @apiName UserBuyArmoire
|
|
* @apiGroup User
|
|
*
|
|
* @apiSuccess {Object} data.items User's item inventory
|
|
* @apiSuccess {Object} data.flags User's flags
|
|
* @apiSuccess {Object} data.armoire Item given by the armoire
|
|
* @apiSuccess {String} message Success message
|
|
*
|
|
* @apiSuccessExample {json} Received a fish:
|
|
* {
|
|
* "success": true,
|
|
* "data": {
|
|
* ---DATA TRUNCATED---
|
|
* "armoire": {
|
|
* "type": "food",
|
|
* "dropKey": "Fish",
|
|
* "dropArticle": "a ",
|
|
* "dropText": "Fish"
|
|
* }
|
|
* },
|
|
*
|
|
* @apiError (400) {NotAuthorized} messageNotEnoughGold Not enough gold for the purchase
|
|
*
|
|
* @apiErrorExample {json} NotAuthorized Not enough gold
|
|
* {"success":false,"error":"NotAuthorized","message":"Not Enough Gold"}
|
|
*/
|
|
api.buyArmoire = {
|
|
method: 'POST',
|
|
middlewares: [authWithHeaders()],
|
|
url: '/user/buy-armoire',
|
|
async handler (req, res) {
|
|
const { user } = res.locals;
|
|
req.type = 'armoire';
|
|
req.params.key = 'armoire';
|
|
if (OFFICIAL_PLATFORMS.indexOf(req.headers['x-client']) === -1) {
|
|
res.analytics = undefined;
|
|
}
|
|
const buyArmoireResponse = await common.ops.buy(user, req, res.analytics);
|
|
await user.save();
|
|
await UserHistory.beginUserHistoryUpdate(user._id, req.headers)
|
|
.withArmoire(buyArmoireResponse[0].armoire.dropKey || 'experience')
|
|
.commit();
|
|
res.respond(200, ...buyArmoireResponse);
|
|
},
|
|
};
|
|
|
|
/**
|
|
* @api {post} /api/v3/user/buy-health-potion Buy a health potion
|
|
* @apiName UserBuyPotion
|
|
* @apiGroup User
|
|
*
|
|
* @apiSuccess {Object} data User's current stats
|
|
* @apiSuccess {String} message Success message
|
|
*
|
|
* @apiSuccessExample Example return:
|
|
* {
|
|
* "success": true,
|
|
* "data": {
|
|
* ---DATA TRUNCATED---
|
|
* },
|
|
* "message": "Bought Health Potion"
|
|
* }
|
|
*
|
|
* @apiError (400) {NotAuthorized} messageNotEnoughGold Not enough gold for the purchase
|
|
* @apiError (400) {NotAuthorized} messageHealthAlreadyMax Health is already full.
|
|
*
|
|
* @apiErrorExample {json} NotAuthorized Not enough gold
|
|
* {"success":false,"error":"NotAuthorized","message":"Not Enough Gold"}
|
|
* @apiErrorExample {json} NotAuthorized Already at max health
|
|
* {"success":false,"error":"NotAuthorized","message":"You already have maximum health."}
|
|
*
|
|
*/
|
|
api.buyHealthPotion = {
|
|
method: 'POST',
|
|
middlewares: [authWithHeaders()],
|
|
url: '/user/buy-health-potion',
|
|
async handler (req, res) {
|
|
const { user } = res.locals;
|
|
req.type = 'potion';
|
|
req.params.key = 'potion';
|
|
const buyHealthPotionResponse = await common.ops.buy(user, req, res.analytics);
|
|
await user.save();
|
|
res.respond(200, ...buyHealthPotionResponse);
|
|
},
|
|
};
|
|
|
|
/**
|
|
* @api {post} /api/v3/user/buy-mystery-set/:key Buy a Mystery Item set
|
|
* @apiName UserBuyMysterySet
|
|
* @apiDescription This buys a Mystery Item set using an Hourglass.
|
|
* @apiGroup User
|
|
*
|
|
* @apiParam (Path) {String} key The mystery set to buy
|
|
*
|
|
* @apiSuccess {Object} data.items user.items
|
|
* @apiSuccess {Object} data.purchasedPlanConsecutive user.purchased.plan.consecutive
|
|
* @apiSuccess {String} message Success message
|
|
*
|
|
* @apiSuccessExample{json} Successful purchase
|
|
* {
|
|
* "success": true,
|
|
* "data": {
|
|
* ---DATA TRUNCATED---
|
|
* },
|
|
* "message": "Purchased an item set using a Mystic Hourglass!"
|
|
* }
|
|
*
|
|
* @apiError (400) {NotAuthorized} notEnoughHourglasses Not enough Mystic Hourglasses.
|
|
* @apiError (400) {NotFound} mysterySetNotFound Specified item does not exist or already owned.
|
|
*
|
|
* @apiErrorExample {json} Not enough hourglasses
|
|
* {"success":false,"error":"NotAuthorized","message":"You don't have enough Mystic Hourglasses."}
|
|
* @apiErrorExample {json} Already own, or doesn't exist
|
|
* {"success":false,"error":"NotFound","message":"Mystery set not found, or set already owned."}
|
|
*/
|
|
api.buyMysterySet = {
|
|
method: 'POST',
|
|
middlewares: [authWithHeaders()],
|
|
url: '/user/buy-mystery-set/:key',
|
|
async handler (req, res) {
|
|
const { user } = res.locals;
|
|
req.type = 'mystery';
|
|
const buyMysterySetRes = await common.ops.buy(user, req, res.analytics);
|
|
await user.save();
|
|
res.respond(200, ...buyMysterySetRes);
|
|
},
|
|
};
|
|
|
|
/**
|
|
* @api {post} /api/v3/user/buy-quest/:key Buy a quest with gold
|
|
* @apiName UserBuyQuest
|
|
* @apiGroup User
|
|
*
|
|
* @apiParam (Path) {String} key The quest scroll to buy
|
|
*
|
|
* @apiSuccess {Object} data.quests User's quest list
|
|
* @apiSuccess {String} message Success message
|
|
*
|
|
* @apiSuccessExample {json} Success response:
|
|
* {
|
|
* "success": true,
|
|
* "data": {
|
|
* --- DATA TRUNCATED---
|
|
* },
|
|
* "message": "Bought Dilatory Distress, Part 1: Message in a Bottle"
|
|
* }
|
|
*
|
|
* @apiError (400) {NotFound} questNotFound Specified quest does not exist
|
|
* @apiError (400) {NotAuthorized} messageNotEnoughGold Not enough gold for the purchase
|
|
*
|
|
* @apiErrorExample {json} Quest chosen does not exist
|
|
* {"success":false,"error":"NotFound","message":"Quest \"dilatoryDistress99\" not found."}
|
|
* @apiErrorExample {json} You must first complete this quest's prerequisites
|
|
* {"success":false,"error":"NotAuthorized","message":"You must first complete dilatoryDistress2."}
|
|
* @apiErrorExample {json} NotAuthorized Not enough gold
|
|
* {"success":false,"error":"NotAuthorized","message":"Not Enough Gold"}
|
|
*
|
|
*/
|
|
api.buyQuest = {
|
|
method: 'POST',
|
|
middlewares: [authWithHeaders()],
|
|
url: '/user/buy-quest/:key',
|
|
async handler (req, res) {
|
|
const { user } = res.locals;
|
|
req.type = 'quest';
|
|
const buyQuestRes = await common.ops.buy(user, req, res.analytics);
|
|
await user.save();
|
|
res.respond(200, ...buyQuestRes);
|
|
},
|
|
};
|
|
|
|
/**
|
|
* @api {post} /api/v3/user/buy-special-spell/:key Buy special item (card, avatar transformation)
|
|
* @apiDescription Includes gift cards (e.g., birthday card), and avatar Transformation
|
|
* Items and their antidotes (e.g., Snowball item and Salt reward).
|
|
* @apiName UserBuySpecialSpell
|
|
* @apiGroup User
|
|
*
|
|
* @apiParam (Path) {String} key The special item to buy. Must be one of the keys
|
|
* from "content.special", such as birthday, snowball, salt.
|
|
*
|
|
* @apiSuccess {Object} data.stats User's current stats
|
|
* @apiSuccess {Object} data.items User's current inventory
|
|
* @apiSuccess {String} message Success message
|
|
*
|
|
* @apiSuccessExample {json} Purchased a greeting card:
|
|
* {
|
|
* "success": true,
|
|
* "data": {
|
|
* },
|
|
* "message": "Bought Greeting Card"
|
|
* }
|
|
*
|
|
* @apiError (400) {NotAuthorized} messageNotEnoughGold Not enough gold for the purchase
|
|
*
|
|
* @apiErrorExample {json} Not enough gold
|
|
* {"success":false,"error":"NotAuthorized","message":"Not Enough Gold"}
|
|
* @apiErrorExample {json} Item name not found
|
|
* {"success":false,"error":"NotFound","message":"Skill \"happymardigras\" not found."}
|
|
*/
|
|
api.buySpecialSpell = {
|
|
method: 'POST',
|
|
middlewares: [authWithHeaders()],
|
|
url: '/user/buy-special-spell/:key',
|
|
async handler (req, res) {
|
|
const { user } = res.locals;
|
|
req.type = 'special';
|
|
const buySpecialSpellRes = await common.ops.buy(user, req);
|
|
await user.save();
|
|
res.respond(200, ...buySpecialSpellRes);
|
|
},
|
|
};
|
|
|
|
/**
|
|
* @api {post} /api/v3/user/hatch/:egg/:hatchingPotion Hatch a pet
|
|
* @apiName UserHatch
|
|
* @apiGroup User
|
|
*
|
|
* @apiParam (Path) {String} egg The egg to use
|
|
* @apiParam (Path) {String} hatchingPotion The hatching potion to use
|
|
* @apiParamExample {URL} Example-URL
|
|
* https://habitica.com/api/v3/user/hatch/Dragon/CottonCandyPink
|
|
*
|
|
* @apiSuccess {Object} data user.items
|
|
* @apiSuccess {String} message
|
|
*
|
|
* @apiSuccessExample {json} Successfully hatched
|
|
* {
|
|
* "success": true,
|
|
* "data": {},
|
|
* "message": "Your egg hatched! Visit your stable to equip your pet."
|
|
* }
|
|
*
|
|
* @apiError {NotAuthorized} messageAlreadyPet Already have the specific pet combination
|
|
* @apiError {NotFound} messageMissingEggPotion One or both of the ingredients are missing.
|
|
* @apiError {NotFound} messageInvalidEggPotionCombo Cannot use that combination of egg and potion.
|
|
*
|
|
* @apiErrorExample {json} Already have that pet.
|
|
* {"success":false,"error":"NotAuthorized","message":"You already have that pet.
|
|
* Try hatching a different combination"}
|
|
* @apiErrorExample {json} Either potion or egg (or both) not in inventory
|
|
* {"success":false,"error":"NotFound","message":"You're missing either that egg or that potion"}
|
|
* @apiErrorExample {json} Cannot use that combination
|
|
* {"success":false,"error":"NotAuthorized","message":"You can't hatch Quest
|
|
* Pet Eggs with Magic Hatching Potions! Try a different egg."}
|
|
*/
|
|
api.hatch = {
|
|
method: 'POST',
|
|
middlewares: [authWithHeaders()],
|
|
url: '/user/hatch/:egg/:hatchingPotion',
|
|
async handler (req, res) {
|
|
const { user } = res.locals;
|
|
const hatchRes = common.ops.hatch(user, req, res.analytics);
|
|
|
|
await user.save();
|
|
|
|
res.respond(200, ...hatchRes);
|
|
|
|
// Send webhook
|
|
const petKey = `${req.params.egg}-${req.params.hatchingPotion}`;
|
|
|
|
userActivityWebhook.send(user, {
|
|
type: 'petHatched',
|
|
pet: petKey,
|
|
message: hatchRes[1],
|
|
});
|
|
},
|
|
};
|
|
|
|
/**
|
|
* @api {post} /api/v3/user/equip/:type/:key Equip or unequip an item
|
|
* @apiName UserEquip
|
|
* @apiGroup User
|
|
*
|
|
* @apiParam (Path) {String="mount","pet","costume","equipped"} type The type of item
|
|
* to equip or unequip.
|
|
* @apiParam (Path) {String} key The item to equip or unequip
|
|
*
|
|
* @apiParamExample {URL} Example-URL
|
|
* https://habitica.com/api/v3/user/equip/equipped/weapon_warrior_2
|
|
*
|
|
* @apiSuccess {Object} data user.items
|
|
* @apiSuccess {String} message Optional success message for unequipping an items
|
|
*
|
|
* @apiSuccessExample {json} Example return:
|
|
* {
|
|
* "success": true,
|
|
* "data": {---DATA TRUNCATED---},
|
|
* "message": "Training Sword unequipped."
|
|
* }
|
|
*
|
|
* @apiError {NotFound} notOwned Item is not in inventory, item doesn't
|
|
* exist, or item is of the wrong type.
|
|
*
|
|
* @apiErrorExample {json} Item not owned or doesn't exist.
|
|
* {"success":false,"error":"NotFound","message":"You do not own this item."}
|
|
* {"success":false,"error":"NotFound","message":"You do not own this pet."}
|
|
* {"success":false,"error":"NotFound","message":"You do not own this mount."}
|
|
*
|
|
*/
|
|
api.equip = {
|
|
method: 'POST',
|
|
middlewares: [authWithHeaders()],
|
|
url: '/user/equip/:type/:key',
|
|
async handler (req, res) {
|
|
const { user } = res.locals;
|
|
const equipRes = common.ops.equip(user, req);
|
|
await user.save();
|
|
res.respond(200, ...equipRes);
|
|
},
|
|
};
|
|
|
|
/**
|
|
* @api {post} /api/v3/user/feed/:pet/:food Feed a pet
|
|
* @apiName UserFeed
|
|
* @apiGroup User
|
|
*
|
|
* @apiParam (Path) {String} pet
|
|
* @apiParam (Path) {String} food
|
|
* @apiParam (Query) {Number} [amount] The amount of food to feed.
|
|
* Note: Pet can eat 50 units.
|
|
* Preferred food offers 5 units per food,
|
|
* other food 2 units.
|
|
*
|
|
* @apiParamExample {url} Example-URL
|
|
* https://habitica.com/api/v3/user/feed/Armadillo-Shade/Chocolate
|
|
* https://habitica.com/api/v3/user/feed/Armadillo-Shade/Chocolate?amount=9
|
|
*
|
|
* @apiSuccess {Number} data The pet value
|
|
* @apiSuccess {String} message Success message
|
|
*
|
|
* @apiSuccessExample {json} Example success:
|
|
* {"success":true,"data":10,"message":"Shade Armadillo
|
|
* really likes the Chocolate!","notifications":[]}
|
|
*
|
|
* @apiError {NotFound} PetNotOwned :pet not found in user.items.pets
|
|
* @apiError {BadRequest} InvalidPet Invalid pet name supplied.
|
|
* @apiError {NotFound} FoodNotOwned :food not found in user.items.food
|
|
* Note: also sent if food name is invalid.
|
|
* @apiError {NotAuthorized} notEnoughFood :Not enough food to feed the pet as requested.
|
|
* @apiError {NotAuthorized} tooMuchFood :You try to feed too much food. Action ancelled.
|
|
*
|
|
*
|
|
*/
|
|
api.feed = {
|
|
method: 'POST',
|
|
middlewares: [authWithHeaders()],
|
|
url: '/user/feed/:pet/:food',
|
|
async handler (req, res) {
|
|
const { user } = res.locals;
|
|
const feedRes = common.ops.feed(user, req, res.analytics);
|
|
|
|
await user.save();
|
|
|
|
res.respond(200, ...feedRes);
|
|
|
|
// Send webhook
|
|
const petValue = feedRes[0];
|
|
|
|
if (petValue === -1) { // evolved to mount
|
|
userActivityWebhook.send(user, {
|
|
type: 'mountRaised',
|
|
pet: req.params.pet,
|
|
message: feedRes[1],
|
|
});
|
|
}
|
|
},
|
|
};
|
|
|
|
/**
|
|
* @api {post} /api/v3/user/change-class Change class
|
|
* @apiDescription User must be at least level 10. If ?class is
|
|
* defined and user.flags.classSelected is false it'll change the class.
|
|
* If user.preferences.disableClasses it'll enable classes, otherwise it
|
|
* sets user.flags.classSelected to false (costs 3 gems).
|
|
* @apiName UserChangeClass
|
|
* @apiGroup User
|
|
*
|
|
* @apiParam (Query) {String} class Query parameter - ?class={warrior|rogue|wizard|healer}
|
|
*
|
|
* @apiSuccess {Object} data.flags user.flags
|
|
* @apiSuccess {Object} data.stats user.stats
|
|
* @apiSuccess {Object} data.preferences user.preferences
|
|
* @apiSuccess {Object} data.items user.items
|
|
*
|
|
* @apiError {NotAuthorized} Gems Not enough gems, if class was already
|
|
* selected and gems needed to be paid.
|
|
* @apiError {NotAuthorized} Level To change class you must be at least level 10.
|
|
*
|
|
* @apiErrorExample {json} Example error:
|
|
* {"success":false,"error":"NotAuthorized","message":"Not enough Gems"}
|
|
*/
|
|
api.changeClass = {
|
|
method: 'POST',
|
|
middlewares: [authWithHeaders()],
|
|
url: '/user/change-class',
|
|
async handler (req, res) {
|
|
const { user } = res.locals;
|
|
const changeClassRes = await common.ops.changeClass(user, req, res.analytics);
|
|
await user.save();
|
|
res.respond(200, ...changeClassRes);
|
|
},
|
|
};
|
|
|
|
/**
|
|
* @api {post} /api/v3/user/disable-classes Disable classes
|
|
* @apiName UserDisableClasses
|
|
* @apiGroup User
|
|
*
|
|
* @apiSuccess {Object} data.flags user.flags
|
|
* @apiSuccess {Object} data.stats user.stats
|
|
* @apiSuccess {Object} data.preferences user.preferences
|
|
*/
|
|
api.disableClasses = {
|
|
method: 'POST',
|
|
middlewares: [authWithHeaders()],
|
|
url: '/user/disable-classes',
|
|
async handler (req, res) {
|
|
const { user } = res.locals;
|
|
const disableClassesRes = common.ops.disableClasses(user, req);
|
|
await user.save();
|
|
res.respond(200, ...disableClassesRes);
|
|
},
|
|
};
|
|
|
|
/**
|
|
* @api {post} /api/v3/user/purchase/:type/:key Purchase Gem or Gem-purchasable item
|
|
* @apiName UserPurchase
|
|
* @apiGroup User
|
|
*
|
|
* @apiParam (Path) {String="gems","eggs","hatchingPotions","premiumHatchingPotions"
|
|
,"food","quests","gear","pets"} type Type of item to purchase.
|
|
* @apiParam (Path) {String} key Item's key (use "gem" for purchasing gems)
|
|
*
|
|
* @apiParam (Body) {Integer} [quantity=1] Count of items to buy.
|
|
* Defaults to 1 and is ignored
|
|
* for items where quantity is irrelevant.
|
|
*
|
|
* @apiSuccess {Object} data.items user.items
|
|
* @apiSuccess {Number} data.balance user.balance
|
|
* @apiSuccess {String} message Success message
|
|
*
|
|
* @apiError {NotAuthorized} NotAvailable Item is not available to be purchased
|
|
* (not unlocked for the user).
|
|
* @apiError {NotAuthorized} Gems Not enough gems
|
|
* @apiError {NotFound} Key Key not found for Content type.
|
|
* @apiError {NotFound} Type Type invalid.
|
|
*
|
|
* @apiErrorExample {json} Example error:
|
|
* {"success":false,"error":"NotAuthorized","message":
|
|
* "This item is not currently available for purchase."}
|
|
*/
|
|
api.purchase = {
|
|
method: 'POST',
|
|
middlewares: [authWithHeaders()],
|
|
url: '/user/purchase/:type/:key',
|
|
async handler (req, res) {
|
|
const { user } = res.locals;
|
|
const type = get(req.params, 'type');
|
|
const key = get(req.params, 'key');
|
|
|
|
// Some groups limit their members ability to obtain gems
|
|
// The check is async so it's done on the server only and not on the client,
|
|
// resulting in a purchase that will seem successful until the request hit the server.
|
|
if (type === 'gems' && key === 'gem') {
|
|
const canGetGems = await user.canGetGems();
|
|
if (!canGetGems) throw new NotAuthorized(res.t('groupPolicyCannotGetGems'));
|
|
}
|
|
|
|
// Req is currently used as options. Slightly confusing, but this will solve that for now.
|
|
let quantity = 1;
|
|
if (req.body.quantity) quantity = req.body.quantity;
|
|
req.quantity = quantity;
|
|
|
|
const purchaseRes = await common.ops.buy(user, req, res.analytics);
|
|
await user.save();
|
|
res.respond(200, ...purchaseRes);
|
|
},
|
|
};
|
|
|
|
/**
|
|
* @api {post} /api/v3/user/purchase-hourglass/:type/:key Purchase Hourglass-purchasable item
|
|
* @apiName UserPurchaseHourglass
|
|
* @apiDescription Purchases an Hourglass-purchasable item.
|
|
* Does not include Mystery Item sets (use /api/v3/user/buy-mystery-set/:key).
|
|
* @apiGroup User
|
|
*
|
|
* @apiParam (Path) {String="pets","mounts"} type The type of item to purchase
|
|
* @apiParam (Path) {String} key Ex: {Phoenix-Base}. The key for the mount/pet
|
|
*
|
|
* @apiParam (Body) {Integer} [quantity=1] Count of items to buy.
|
|
* Defaults to 1 and is ignored
|
|
* for items where quantity is irrelevant.
|
|
*
|
|
* @apiSuccess {Object} data.items user.items
|
|
* @apiSuccess {Object} data.purchasedPlanConsecutive user.purchased.plan.consecutive
|
|
* @apiSuccess {String} message Success message
|
|
*
|
|
* @apiError {NotAuthorized} NotAvailable Item is not available to be purchased or is not valid.
|
|
* @apiError {NotAuthorized} Hourglasses User does not have enough Mystic Hourglasses.
|
|
* @apiError {BadRequest} Quantity Quantity to purchase must be a number.
|
|
* @apiError {NotFound} Type Type invalid.
|
|
*
|
|
* @apiErrorExample {json} Example error:
|
|
* {"success":false,"error":"NotAuthorized","message":"You don't have enough Mystic Hourglasses."}
|
|
*/
|
|
api.userPurchaseHourglass = {
|
|
method: 'POST',
|
|
middlewares: [authWithHeaders()],
|
|
url: '/user/purchase-hourglass/:type/:key',
|
|
async handler (req, res) {
|
|
const { user } = res.locals;
|
|
const quantity = req.body.quantity || 1;
|
|
if (quantity < 1 || !Number.isInteger(quantity)) throw new BadRequest(res.t('invalidQuantity'), req.language);
|
|
const purchaseHourglassRes = await common.ops.buy(
|
|
user,
|
|
req,
|
|
res.analytics,
|
|
{ quantity, hourglass: true },
|
|
);
|
|
await user.save();
|
|
res.respond(200, ...purchaseHourglassRes);
|
|
},
|
|
};
|
|
|
|
/**
|
|
* @api {post} /api/v3/user/read-card/:cardType Read a card
|
|
* @apiName UserReadCard
|
|
* @apiGroup User
|
|
*
|
|
* @apiParam (Path) {String} cardType Type of card to read (e.g. - birthday,
|
|
* greeting, nye, thankyou, valentine).
|
|
*
|
|
* @apiSuccess {Object} data.specialItems user.items.special
|
|
* @apiSuccess {Boolean} data.cardReceived user.flags.cardReceived
|
|
* @apiSuccess {String} message Success message
|
|
*
|
|
* @apiSuccessExample {json} Example success:
|
|
* {
|
|
* "success": true,
|
|
* "data": {
|
|
* "specialItems": {
|
|
* "snowball": 0,
|
|
* "spookySparkles": 0,
|
|
* "shinySeed": 0,
|
|
* "seafoam": 0,
|
|
* "valentine": 0,
|
|
* "valentineReceived": [],
|
|
* "nye": 0,
|
|
* "nyeReceived": [],
|
|
* "greeting": 0,
|
|
* "greetingReceived": [
|
|
* "MadPink"
|
|
* ],
|
|
* "thankyou": 0,
|
|
* "thankyouReceived": [],
|
|
* "birthday": 0,
|
|
* "birthdayReceived": []
|
|
* },
|
|
* "cardReceived": false
|
|
* },
|
|
* "message": "valentine has been read"
|
|
* }
|
|
*
|
|
* @apiError {NotAuthorized} CardType Unknown card type.
|
|
*/
|
|
api.readCard = {
|
|
method: 'POST',
|
|
middlewares: [authWithHeaders()],
|
|
url: '/user/read-card/:cardType',
|
|
async handler (req, res) {
|
|
const { user } = res.locals;
|
|
const readCardRes = common.ops.readCard(user, req);
|
|
await user.save();
|
|
res.respond(200, ...readCardRes);
|
|
},
|
|
};
|
|
|
|
/**
|
|
* @api {post} /api/v3/user/open-mystery-item Open the Mystery Item box
|
|
* @apiName UserOpenMysteryItem
|
|
* @apiGroup User
|
|
*
|
|
* @apiSuccess {Object} data The item obtained
|
|
* @apiSuccess {String} message Success message
|
|
*
|
|
* @apiSuccessExample {json} Example success:
|
|
* { "success": true,
|
|
* "data": {
|
|
* "mystery": "201612",
|
|
* "value": 0,
|
|
* "type": "armor",
|
|
* "key": "armor_mystery_201612",
|
|
* "set": "mystery-201612",
|
|
* "klass": "mystery",
|
|
* "index": "201612",
|
|
* "str": 0,
|
|
* "int": 0,
|
|
* "per": 0,
|
|
* "con": 0
|
|
* },
|
|
* "message": "Mystery item opened."
|
|
*
|
|
* @apiError {BadRequest} Empty No mystery items to open.
|
|
*
|
|
* @apiErrorExample {json} Example error:
|
|
* {"success":false,"error":"BadRequest","message":"Mystery items are empty"}
|
|
*/
|
|
api.userOpenMysteryItem = {
|
|
method: 'POST',
|
|
middlewares: [authWithHeaders()],
|
|
url: '/user/open-mystery-item',
|
|
async handler (req, res) {
|
|
const { user } = res.locals;
|
|
const openMysteryItemRes = common.ops.openMysteryItem(user, req, res.analytics);
|
|
await user.save();
|
|
res.respond(200, ...openMysteryItemRes);
|
|
},
|
|
};
|
|
|
|
/* @api {post} /api/v3/user/release-pets Release pets
|
|
* @apiName UserReleasePets
|
|
* @apiGroup User
|
|
*
|
|
* @apiSuccess {Object} data.items `user.items.pets`
|
|
* @apiSuccess {String} message Success message
|
|
*
|
|
* @apiSuccessExample {json} Example success:
|
|
* {
|
|
* "success": true,
|
|
* "data": {
|
|
* },
|
|
* "message": "Pets released"
|
|
* }
|
|
*
|
|
* @apiError {NotAuthorized} Gems Not enough gems
|
|
*
|
|
* @apiErrorExample {json} Example error:
|
|
* {"success":false,"error":"NotAuthorized","message":"Not enough Gems"}
|
|
*/
|
|
api.userReleasePets = {
|
|
method: 'POST',
|
|
middlewares: [authWithHeaders()],
|
|
url: '/user/release-pets',
|
|
async handler (req, res) {
|
|
const { user } = res.locals;
|
|
const releasePetsRes = await common.ops.releasePets(user, req, res.analytics);
|
|
await user.save();
|
|
res.respond(200, ...releasePetsRes);
|
|
},
|
|
};
|
|
|
|
/**
|
|
* @api {post} /api/v3/user/release-both Release pets and mounts and grants Triad Bingo
|
|
* @apiName UserReleaseBoth
|
|
* @apiGroup User
|
|
*
|
|
* @apiSuccess {Object} data.achievements
|
|
* @apiSuccess {Object} data.items
|
|
* @apiSuccess {Number} data.balance
|
|
* @apiSuccess {String} message Success message
|
|
*
|
|
* @apiSuccessExample {json} Example success:
|
|
* {
|
|
* "success": true,
|
|
* "data": {
|
|
* "achievements": {
|
|
* "ultimateGearSets": {},
|
|
* "challenges": [],
|
|
* "quests": {},
|
|
* "perfect": 0,
|
|
* "beastMaster": true,
|
|
* "beastMasterCount": 1,
|
|
* "mountMasterCount": 1,
|
|
* "triadBingoCount": 1,
|
|
* "mountMaster": true,
|
|
* "triadBingo": true
|
|
* },
|
|
* "items": {}
|
|
* },
|
|
* "message": "Mounts and pets released"
|
|
* }
|
|
*
|
|
* @apiError {NotAuthorized} Gems Not enough gems
|
|
*
|
|
* @apiErrorExample {json} Example error:
|
|
* {"success":false,"error":"NotAuthorized","message":"Not enough Gems"}
|
|
|
|
*/
|
|
api.userReleaseBoth = {
|
|
method: 'POST',
|
|
middlewares: [authWithHeaders()],
|
|
url: '/user/release-both',
|
|
async handler (req, res) {
|
|
const { user } = res.locals;
|
|
const releaseBothRes = common.ops.releaseBoth(user, req, res.analytics);
|
|
await user.save();
|
|
res.respond(200, ...releaseBothRes);
|
|
},
|
|
};
|
|
|
|
/**
|
|
* @api {post} /api/v3/user/release-mounts Release mounts
|
|
* @apiName UserReleaseMounts
|
|
* @apiGroup User
|
|
*
|
|
* @apiSuccess {Object} data user.items.mounts
|
|
* @apiSuccess {String} message Success message
|
|
*
|
|
* @apiSuccessExample {json} Example success:
|
|
* {
|
|
* "success": true,
|
|
* "data": {
|
|
* },
|
|
* "items": {}
|
|
* },
|
|
* "message": "Mounts released"
|
|
* }
|
|
*
|
|
* @apiError {NotAuthorized} Gems Not enough gems
|
|
*
|
|
* @apiErrorExample {json} Example error:
|
|
* {"success":false,"error":"NotAuthorized","message":"Not enough Gems"}
|
|
*
|
|
*/
|
|
api.userReleaseMounts = {
|
|
method: 'POST',
|
|
middlewares: [authWithHeaders()],
|
|
url: '/user/release-mounts',
|
|
async handler (req, res) {
|
|
const { user } = res.locals;
|
|
const releaseMountsRes = await common.ops.releaseMounts(user, req, res.analytics);
|
|
await user.save();
|
|
res.respond(200, ...releaseMountsRes);
|
|
},
|
|
};
|
|
|
|
/**
|
|
* @api {post} /api/v3/user/sell/:type/:key Sell a gold-sellable item owned by the user
|
|
* @apiName UserSell
|
|
* @apiGroup User
|
|
*
|
|
* @apiParam (Path) {String="eggs","hatchingPotions","food"} type The type of item to sell.
|
|
* @apiParam (Path) {String} key The key of the item
|
|
* @apiParam (Query) {Number} [amount] The amount to sell
|
|
*
|
|
* @apiSuccess {Object} data.stats
|
|
* @apiSuccess {Object} data.items
|
|
*
|
|
* @apiError {NotFound} InvalidKey Key not found for user.items eggs
|
|
* (either the key does not exist or the
|
|
* user has none in inventory).
|
|
* @apiError {NotAuthorized} InvalidType Type is not a valid type.
|
|
*
|
|
* @apiErrorExample {json} Example error:
|
|
* {"success":false,"error":"NotAuthorized","message":"Type is not sellable.
|
|
* Must be one of the following eggs, hatchingPotions, food"}
|
|
*/
|
|
api.userSell = {
|
|
method: 'POST',
|
|
middlewares: [authWithHeaders()],
|
|
url: '/user/sell/:type/:key',
|
|
async handler (req, res) {
|
|
const { user } = res.locals;
|
|
const sellRes = common.ops.sell(user, req);
|
|
await user.save();
|
|
res.respond(200, ...sellRes);
|
|
},
|
|
};
|
|
|
|
/**
|
|
* @api {post} /api/v3/user/unlock Unlock item or set of items by purchase
|
|
* @apiName UserUnlock
|
|
* @apiGroup User
|
|
*
|
|
* @apiParam (Query) {String} path Full path to unlock. See "content" API call for list of items.
|
|
*
|
|
* @apiParamExample {curl} Example call:
|
|
* curl -X POST https://habitica.com/api/v3/user/unlock?path=background.midnight_clouds
|
|
* curl -X POST https://habitica.com/api/v3/user/unlock?path=hair.color.midnight
|
|
*
|
|
* @apiSuccess {Object} data.purchased
|
|
* @apiSuccess {Object} data.items
|
|
* @apiSuccess {Object} data.preferences
|
|
* @apiSuccess {String} message "Items have been unlocked"
|
|
*
|
|
* @apiSuccessExample {json} Example success:
|
|
* {
|
|
* "success": true,
|
|
* "data": {},
|
|
* "message": "Items have been unlocked"
|
|
* }
|
|
*
|
|
* @apiError {BadRequest} Path Path to unlock not specified
|
|
* @apiError {NotAuthorized} Gems Not enough gems available.
|
|
* @apiError {NotAuthorized} Unlocked Full set already unlocked.
|
|
*
|
|
* @apiErrorExample {json} Example error:
|
|
* {"success":false,"error":"BadRequest","message":"Path string is required"}
|
|
* {"success":false,"error":"NotAuthorized","message":"Full set already unlocked."}
|
|
*/
|
|
api.userUnlock = {
|
|
method: 'POST',
|
|
middlewares: [authWithHeaders()],
|
|
url: '/user/unlock',
|
|
async handler (req, res) {
|
|
const { user } = res.locals;
|
|
const unlockRes = await common.ops.unlock(user, req, res.analytics);
|
|
await user.save();
|
|
res.respond(200, ...unlockRes);
|
|
},
|
|
};
|
|
|
|
/**
|
|
* @api {post} /api/v3/user/revive Revive user from death
|
|
* @apiName UserRevive
|
|
* @apiGroup User
|
|
*
|
|
* @apiSuccess {Object} data user.items
|
|
* @apiSuccess {String} message Success message
|
|
*
|
|
*
|
|
* @apiError {NotAuthorized} NotDead Cannot revive player if player is not dead yet
|
|
*
|
|
* @apiErrorExample {json} Example error:
|
|
* {"success":false,"error":"NotAuthorized","message":"Cannot revive if not dead"}
|
|
*/
|
|
api.userRevive = {
|
|
method: 'POST',
|
|
middlewares: [authWithHeaders()],
|
|
url: '/user/revive',
|
|
async handler (req, res) {
|
|
const { user } = res.locals;
|
|
const reviveRes = common.ops.revive(user, req, res.analytics);
|
|
await user.save();
|
|
res.respond(200, ...reviveRes);
|
|
},
|
|
};
|
|
|
|
/* NOTE this route has also an API v4 version */
|
|
|
|
/**
|
|
* @api {post} /api/v3/user/rebirth Use Orb of Rebirth on user
|
|
* @apiName UserRebirth
|
|
* @apiGroup User
|
|
*
|
|
* @apiSuccess {Object} data.user
|
|
* @apiSuccess {Array} data.tasks User's modified tasks (no rewards)
|
|
* @apiSuccess {String} message Success message
|
|
*
|
|
* @apiSuccessExample {json} Example success:
|
|
* {
|
|
* "success": true,
|
|
* "data": {
|
|
* },
|
|
* "message": "You have been reborn!"
|
|
* {
|
|
* "type": "REBIRTH_ACHIEVEMENT",
|
|
* "data": {},
|
|
* "id": "424d69fa-3a6d-47db-96a4-6db42ed77a43"
|
|
* }
|
|
* ]
|
|
* }
|
|
*
|
|
* @apiError {NotAuthorized} Gems Not enough gems
|
|
*
|
|
* @apiErrorExample {json} Example error:
|
|
* {"success":false,"error":"NotAuthorized","message":"Not enough Gems"}
|
|
*/
|
|
api.userRebirth = {
|
|
method: 'POST',
|
|
middlewares: [authWithHeaders()],
|
|
url: '/user/rebirth',
|
|
async handler (req, res) {
|
|
await userLib.rebirth(req, res, { isV3: true });
|
|
},
|
|
};
|
|
|
|
/**
|
|
* @api {post} /api/v3/user/block/:uuid Block / unblock a user from sending you a PM
|
|
* @apiName BlockUser
|
|
* @apiGroup User
|
|
*
|
|
* @apiParam (Path) {UUID} uuid The uuid of the user to block / unblock
|
|
*
|
|
* @apiSuccess {Array} data user.inbox.blocks
|
|
*
|
|
* @apiSuccessExample {json} Example return:
|
|
* {"success":true,"data":["e4842579-g987-d2d2-8660-2f79e725fb79"],"notifications":[]}
|
|
*
|
|
* @apiError {BadRequest} InvalidUUID UUID is incorrect.
|
|
*
|
|
*/
|
|
api.blockUser = {
|
|
method: 'POST',
|
|
middlewares: [authWithHeaders()],
|
|
url: '/user/block/:uuid',
|
|
async handler (req, res) {
|
|
const { user } = res.locals;
|
|
const blockUserRes = common.ops.blockUser(user, req);
|
|
await user.save();
|
|
res.respond(200, ...blockUserRes);
|
|
},
|
|
};
|
|
|
|
/* NOTE this route has also an API v4 version */
|
|
|
|
/**
|
|
* @api {delete} /api/v3/user/messages/:id Delete a message
|
|
* @apiName deleteMessage
|
|
* @apiGroup User
|
|
*
|
|
* @apiParam (Path) {UUID} id The id of the message to delete
|
|
*
|
|
* @apiSuccess {Object} data user.inbox.messages
|
|
* @apiSuccessExample {json} Example return:
|
|
* {
|
|
* "success": true,
|
|
* "data": {
|
|
* "74d9a2e7-4c6e-4f3b-c3c4-517873f41592": {
|
|
* "sort": 0,
|
|
* "user": "MadPink",
|
|
* "backer": {},
|
|
* "contributor": {},
|
|
* "uuid": "b0413351-405f-416f-9999-947ec1c85199",
|
|
* "flagCount": 0,
|
|
* "flags": {},
|
|
* "likes": {},
|
|
* "timestamp": 1487276826704,
|
|
* "text": "Hi there!",
|
|
* "id": "74d9a2e7-4c6e-4f3b-c3c4-517873f41592"
|
|
* }
|
|
* }
|
|
* }
|
|
*/
|
|
api.deleteMessage = {
|
|
method: 'DELETE',
|
|
middlewares: [authWithHeaders()],
|
|
url: '/user/messages/:id',
|
|
async handler (req, res) {
|
|
const { user } = res.locals;
|
|
|
|
await inboxLib.deleteMessage(user, req.params.id);
|
|
|
|
res.respond(200, ...[await inboxLib.getUserInbox(user, { asArray: false })]);
|
|
},
|
|
};
|
|
|
|
/* NOTE this route has also an API v4 version */
|
|
|
|
/**
|
|
* @api {delete} /api/v3/user/messages Delete all messages
|
|
* @apiName clearMessages
|
|
* @apiGroup User
|
|
*
|
|
* @apiSuccess {Object} data user.inbox.messages which should be empty
|
|
*
|
|
* @apiSuccessExample {json} Example return:
|
|
* {"success":true,"data":{},"notifications":[]}
|
|
*/
|
|
api.clearMessages = {
|
|
method: 'DELETE',
|
|
middlewares: [authWithHeaders()],
|
|
url: '/user/messages',
|
|
async handler (req, res) {
|
|
const { user } = res.locals;
|
|
|
|
await inboxLib.clearPMs(user);
|
|
|
|
res.respond(200, ...[]);
|
|
},
|
|
};
|
|
|
|
/**
|
|
* @api {post} /api/v3/user/mark-pms-read Mark Private Messages as read
|
|
* @apiName markPmsRead
|
|
* @apiGroup User
|
|
*
|
|
* @apiSuccess {Object} data user.inbox.newMessages
|
|
*
|
|
* @apiSuccessExample {json} Example return:
|
|
* {"success":true,"data":[0,"Your private messages have been marked as read"],"notifications":[]}
|
|
*
|
|
*/
|
|
api.markPmsRead = {
|
|
method: 'POST',
|
|
middlewares: [authWithHeaders({ userFieldsToInclude: ['inbox'] })],
|
|
url: '/user/mark-pms-read',
|
|
async handler (req, res) {
|
|
const { user } = res.locals;
|
|
const markPmsResponse = common.ops.markPmsRead(user);
|
|
await user.save();
|
|
res.respond(200, markPmsResponse);
|
|
},
|
|
};
|
|
|
|
/* NOTE this route has also an API v4 version */
|
|
|
|
/**
|
|
* @api {post} /api/v3/user/reroll Reroll a user (reset tasks) using the Fortify Potion
|
|
* @apiName UserReroll
|
|
* @apiGroup User
|
|
*
|
|
* @apiSuccess {Object} data.user
|
|
* @apiSuccess {Object} data.tasks User's modified tasks (no rewards)
|
|
* @apiSuccess {Object} message Success message
|
|
*
|
|
* @apiSuccessExample {json} Example success:
|
|
* {
|
|
* "success": true,
|
|
* "data": {
|
|
* },
|
|
* "message": "Fortify complete!"
|
|
* }
|
|
*
|
|
* @apiError {NotAuthorized} Gems Not enough gems
|
|
*
|
|
* @apiErrorExample {json} Example error:
|
|
* {"success":false,"error":"NotAuthorized","message":"Not enough Gems"}
|
|
*/
|
|
api.userReroll = {
|
|
method: 'POST',
|
|
middlewares: [authWithHeaders()],
|
|
url: '/user/reroll',
|
|
async handler (req, res) {
|
|
await userLib.reroll(req, res, { isV3: true });
|
|
},
|
|
};
|
|
|
|
/* NOTE this route has also an API v4 version */
|
|
|
|
/**
|
|
* @api {post} /api/v3/user/reset Reset user
|
|
* @apiName UserReset
|
|
* @apiGroup User
|
|
*
|
|
* @apiSuccess {Object} data.user
|
|
* @apiSuccess {Array} data.tasksToRemove IDs of removed tasks
|
|
* @apiSuccess {String} message Success message
|
|
*
|
|
* @apiSuccessExample {json} Example success:
|
|
* {
|
|
* "success": true,
|
|
* "data": {--TRUNCATED--},
|
|
* "tasksToRemove": [
|
|
* "ebb8748c-0565-431e-9036-b908da25c6b4",
|
|
* "12a1cecf-68eb-40a7-b282-4f388c32124c"
|
|
* ]
|
|
* },
|
|
* "message": "Reset complete!"
|
|
* }
|
|
*/
|
|
api.userReset = {
|
|
method: 'POST',
|
|
middlewares: [authWithHeaders()],
|
|
url: '/user/reset',
|
|
async handler (req, res) {
|
|
await userLib.reset(req, res, { isV3: true });
|
|
},
|
|
};
|
|
|
|
/**
|
|
* @api {post} /api/v3/user/custom-day-start Set Custom Day Start time for user.
|
|
* @apiName setCustomDayStart
|
|
* @apiGroup User
|
|
*
|
|
* @apiParam (Body) {number} [dayStart=0] The hour number 0-23 for day to begin.
|
|
* If not supplied, will default to 0.
|
|
*
|
|
* @apiParamExample {json} Request-Example:
|
|
* {"dayStart":2}
|
|
*
|
|
* @apiSuccess {Object} data An empty Object
|
|
* @apiSuccess {String} message Success message
|
|
*
|
|
* @apiSuccessExample {json} Success-Example:
|
|
* {"success":true,"data":{"message":"Your custom day start has changed."},"notifications":[]}
|
|
*
|
|
* @apiError {BadRequest} Validation Value provided is not a number, or is outside the range of 0-23
|
|
*
|
|
* @apiErrorExample {json} Error-Example:
|
|
* {"success":false,"error":"BadRequest","message":"User validation failed",
|
|
* "errors":[{"message":"Path `preferences.dayStart` (25) is more than maximum allowed value (23)."
|
|
* ,"path":"preferences.dayStart","value":25}]}
|
|
*/
|
|
api.setCustomDayStart = {
|
|
method: 'POST',
|
|
middlewares: [authWithHeaders()],
|
|
url: '/user/custom-day-start',
|
|
async handler (req, res) {
|
|
const { user } = res.locals;
|
|
const { dayStart } = req.body;
|
|
|
|
user.preferences.dayStart = dayStart;
|
|
user.lastCron = new Date();
|
|
|
|
await user.save();
|
|
|
|
res.respond(200, {
|
|
message: res.t('customDayStartHasChanged'),
|
|
});
|
|
},
|
|
};
|
|
|
|
/**
|
|
* @api {get} /user/toggle-pinned-item/:key Toggle an item to be pinned
|
|
* @apiName togglePinnedItem
|
|
* @apiGroup User
|
|
*
|
|
* @apiSuccess {Object} data Pinned items array
|
|
*
|
|
* @apiSuccessExample {json} Result:
|
|
* {
|
|
* "success": true,
|
|
* "data": {
|
|
* "pinnedItems": [
|
|
* "type": "gear",
|
|
* "path": "gear.flat.weapon_1"
|
|
* ]
|
|
* }
|
|
* }
|
|
*
|
|
*/
|
|
api.togglePinnedItem = {
|
|
method: 'GET',
|
|
middlewares: [authWithHeaders()],
|
|
url: '/user/toggle-pinned-item/:type/:path',
|
|
async handler (req, res) {
|
|
const { user } = res.locals;
|
|
const path = get(req.params, 'path');
|
|
const type = get(req.params, 'type');
|
|
|
|
common.ops.pinnedGearUtils.togglePinnedItem(user, { type, path }, req);
|
|
|
|
await user.save();
|
|
|
|
const userJson = user.toJSON();
|
|
|
|
res.respond(200, {
|
|
pinnedItems: userJson.pinnedItems,
|
|
unpinnedItems: userJson.unpinnedItems,
|
|
});
|
|
},
|
|
};
|
|
|
|
/**
|
|
* @api {post} /api/v3/user/move-pinned-item/:type/:path/move/to/:position
|
|
* Move a pinned item in the rewards column to a new position after being sorted
|
|
* @apiName MovePinnedItem
|
|
* @apiGroup User
|
|
*
|
|
* @apiParam (Path) {String} path The unique item path used for pinning
|
|
* @apiParam (Path) {Number} position Where to move the task.
|
|
* 0 = top of the list ("push to top").
|
|
* -1 = bottom of the list ("push to bottom").
|
|
*
|
|
* @apiSuccess {Array} data The new pinned items order.
|
|
*
|
|
* @apiSuccessExample {json} Example success:
|
|
* {"success":true,"data":{"path":"quests.mayhemMistiflying3","type":"quests",
|
|
* "_id": "5a32d357232feb3bc94c2bdf"},"notifications":[]}
|
|
*
|
|
* @apiUse TaskNotFound
|
|
*/
|
|
api.movePinnedItem = {
|
|
method: 'POST',
|
|
url: '/user/move-pinned-item/:path/move/to/:position',
|
|
middlewares: [authWithHeaders()],
|
|
async handler (req, res) {
|
|
req.checkParams('path', res.t('taskIdRequired')).notEmpty();
|
|
req.checkParams('position', res.t('positionRequired')).notEmpty().isNumeric();
|
|
|
|
const validationErrors = req.validationErrors();
|
|
if (validationErrors) throw validationErrors;
|
|
|
|
const { user } = res.locals;
|
|
const { path } = req.params;
|
|
let position = Number(req.params.position);
|
|
|
|
// If something has been added or removed from the inAppRewards, we need
|
|
// to reset pinnedItemsOrder to have the correct length. Since inAppRewards
|
|
// Uses the current pinnedItemsOrder to return these in the right order,
|
|
// the new reset array will be in the right order before we do the swap
|
|
const currentPinnedItems = common.inAppRewards(user);
|
|
if (user.pinnedItemsOrder.length !== currentPinnedItems.length) {
|
|
user.pinnedItemsOrder = currentPinnedItems.map(item => item.path);
|
|
}
|
|
|
|
const officialItems = common.getOfficialPinnedItems(user);
|
|
|
|
const itemExistInPinnedArray = user.pinnedItems.findIndex(item => item.path === path);
|
|
const itemExistInOfficialItems = officialItems.findIndex(item => item.path === path);
|
|
|
|
if (itemExistInPinnedArray === -1 && itemExistInOfficialItems === -1) {
|
|
throw new BadRequest(res.t('wrongItemPath', { path }, req.language));
|
|
}
|
|
|
|
// Adjust the order
|
|
const currentIndex = user.pinnedItemsOrder.findIndex(item => item === path);
|
|
const currentPinnedItemPath = user.pinnedItemsOrder[currentIndex];
|
|
|
|
if (currentIndex !== -1) {
|
|
// Remove the one we will move
|
|
user.pinnedItemsOrder.splice(currentIndex, 1);
|
|
} else {
|
|
// usually the array would be already fixed by the inAppRewards call
|
|
// but it seems something didn't work out
|
|
position = Math.min(position, user.pinnedItemsOrder.length - 1);
|
|
}
|
|
|
|
// reinsert the item in position (or just at the end)
|
|
if (position === -1) {
|
|
user.pinnedItemsOrder.push(currentPinnedItemPath);
|
|
} else {
|
|
user.pinnedItemsOrder.splice(position, 0, currentPinnedItemPath);
|
|
}
|
|
|
|
await user.save();
|
|
const userJson = user.toJSON();
|
|
|
|
res.respond(200, userJson.pinnedItemsOrder);
|
|
},
|
|
};
|
|
|
|
/**
|
|
* @api {post} /api/v3/user/stat-sync
|
|
* Request a refresh of user stats, including processing of pending level-ups
|
|
* @apiName StatSync
|
|
* @apiGroup User
|
|
*
|
|
* @apiSuccess {Object} data The user object
|
|
*/
|
|
|
|
api.statSync = {
|
|
method: 'POST',
|
|
middlewares: [authWithHeaders()],
|
|
url: '/user/stat-sync',
|
|
async handler (req, res) {
|
|
const { user } = res.locals;
|
|
common.fns.updateStats(user, user.stats);
|
|
await user.save();
|
|
|
|
res.respond(200, user);
|
|
},
|
|
};
|
|
|
|
export default api;
|