musora-content-services 2.87.0 → 2.88.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.claude/settings.local.json +9 -3
- package/.coderabbit.yaml +0 -0
- package/.editorconfig +0 -0
- package/.github/pull_request_template.md +0 -0
- package/.github/workflows/conventional-commits.yaml +0 -0
- package/.github/workflows/docs.js.yml +0 -0
- package/.github/workflows/node.js.yml +0 -0
- package/.prettierignore +0 -0
- package/.prettierrc +0 -0
- package/.yarnrc.yml +1 -0
- package/CHANGELOG.md +9 -0
- package/README.md +0 -0
- package/docs/fonts/Montserrat/Montserrat-Bold.eot +0 -0
- package/docs/fonts/Montserrat/Montserrat-Bold.ttf +0 -0
- package/docs/fonts/Montserrat/Montserrat-Bold.woff +0 -0
- package/docs/fonts/Montserrat/Montserrat-Bold.woff2 +0 -0
- package/docs/fonts/Montserrat/Montserrat-Regular.eot +0 -0
- package/docs/fonts/Montserrat/Montserrat-Regular.ttf +0 -0
- package/docs/fonts/Montserrat/Montserrat-Regular.woff +0 -0
- package/docs/fonts/Montserrat/Montserrat-Regular.woff2 +0 -0
- package/docs/fonts/Source-Sans-Pro/sourcesanspro-light-webfont.eot +0 -0
- package/docs/fonts/Source-Sans-Pro/sourcesanspro-light-webfont.svg +0 -0
- package/docs/fonts/Source-Sans-Pro/sourcesanspro-light-webfont.ttf +0 -0
- package/docs/fonts/Source-Sans-Pro/sourcesanspro-light-webfont.woff +0 -0
- package/docs/fonts/Source-Sans-Pro/sourcesanspro-light-webfont.woff2 +0 -0
- package/docs/fonts/Source-Sans-Pro/sourcesanspro-regular-webfont.eot +0 -0
- package/docs/fonts/Source-Sans-Pro/sourcesanspro-regular-webfont.svg +0 -0
- package/docs/fonts/Source-Sans-Pro/sourcesanspro-regular-webfont.ttf +0 -0
- package/docs/fonts/Source-Sans-Pro/sourcesanspro-regular-webfont.woff +0 -0
- package/docs/fonts/Source-Sans-Pro/sourcesanspro-regular-webfont.woff2 +0 -0
- package/docs/forums_discussions.js.html +0 -0
- package/docs/forums_forum.js.html +0 -0
- package/docs/gamification_awards.js.html +0 -0
- package/docs/gamification_types.js.html +0 -0
- package/docs/module-Categories.html +0 -0
- package/docs/module-ForumCategories.html +0 -0
- package/docs/module-ForumDiscussions.html +0 -0
- package/docs/module-Threads.html +0 -0
- package/docs/scripts/collapse.js +0 -0
- package/docs/scripts/commonNav.js +0 -0
- package/docs/scripts/linenumber.js +0 -0
- package/docs/scripts/nav.js +0 -0
- package/docs/scripts/polyfill.js +0 -0
- package/docs/scripts/prettify/Apache-License-2.0.txt +0 -0
- package/docs/scripts/prettify/lang-css.js +0 -0
- package/docs/scripts/prettify/prettify.js +0 -0
- package/docs/scripts/search.js +0 -0
- package/docs/styles/jsdoc.css +0 -0
- package/docs/styles/prettify.css +0 -0
- package/docs/user_memberships.js.html +0 -0
- package/jest.config.js +0 -0
- package/package.json +1 -1
- package/src/contentMetaData.js +0 -0
- package/src/index.d.ts +7 -0
- package/src/index.js +7 -0
- package/src/infrastructure/http/HttpClient.ts +0 -0
- package/src/infrastructure/http/executors/FetchRequestExecutor.ts +0 -0
- package/src/infrastructure/http/index.ts +0 -0
- package/src/infrastructure/http/interfaces/HeaderProvider.ts +0 -0
- package/src/infrastructure/http/interfaces/HttpError.ts +0 -0
- package/src/infrastructure/http/interfaces/NetworkError.ts +0 -0
- package/src/infrastructure/http/interfaces/RequestExecutor.ts +0 -0
- package/src/infrastructure/http/interfaces/RequestOptions.ts +0 -0
- package/src/infrastructure/http/providers/DefaultHeaderProvider.ts +0 -0
- package/src/lib/httpHelper.js +0 -0
- package/src/services/api/types.js +0 -0
- package/src/services/api/types.ts +0 -0
- package/src/services/content-org/content-org.js +0 -0
- package/src/services/content-org/guided-courses.ts +0 -0
- package/src/services/content-org/playlists-types.js +0 -0
- package/src/services/content-org/playlists.js +0 -0
- package/src/services/dateUtils.js +0 -0
- package/src/services/eventsAPI.js +0 -0
- package/src/services/forums/forums.ts +0 -0
- package/src/services/forums/posts.ts +0 -0
- package/src/services/gamification/awards.ts +0 -0
- package/src/services/gamification/gamification.js +0 -0
- package/src/services/imageSRCBuilder.js +0 -0
- package/src/services/imageSRCVerify.js +0 -0
- package/src/services/liveTesting.ts +0 -0
- package/src/services/progress-row/method-card.js +0 -0
- package/src/services/recommendations.js +0 -0
- package/src/services/reporting/README.md +316 -0
- package/src/services/reporting/reporting.ts +189 -0
- package/src/services/reporting/types.ts +56 -0
- package/src/services/user/chat.js +0 -0
- package/src/services/user/interests.js +0 -0
- package/src/services/user/management.js +0 -0
- package/src/services/user/memberships.ts +0 -0
- package/src/services/user/notifications.js +0 -0
- package/src/services/user/payments.ts +0 -0
- package/src/services/user/profile.js +0 -0
- package/src/services/user/sessions.js +0 -0
- package/src/services/user/types.js +0 -0
- package/src/services/user/user-management-system.js +0 -0
- package/test/HttpClient.test.js +0 -0
- package/test/contentLikes.test.js +0 -0
- package/test/dataContext.test.js +0 -0
- package/test/imageSRCBuilder.test.js +0 -0
- package/test/imageSRCVerify.test.js +0 -0
- package/test/learningPaths.test.js +0 -0
- package/test/lib/lastUpdated.test.js +0 -0
- package/test/live/railcontentLive.test.js +0 -0
- package/test/localStorageMock.js +0 -0
- package/test/log.js +0 -0
- package/test/mockData/mockData_fetchByRailContentIds_one_content.json +0 -0
- package/test/mockData/mockData_progress_content.json +0 -0
- package/test/mockData/mockData_sanity_progress_content.json +0 -0
- package/test/mockData/mockData_user_practices.json +0 -0
- package/test/notifications.test.js +0 -0
- package/test/progressRows.test.js +0 -0
- package/test/reporting.test.js +132 -0
- package/test/sanityQueryService.test.js +0 -0
- package/test/streakMessage.test.js +0 -0
- package/test/user/permissions.test.js +0 -0
- package/test/userActivity.test.js +0 -0
|
@@ -1,8 +1,14 @@
|
|
|
1
1
|
{
|
|
2
2
|
"permissions": {
|
|
3
3
|
"allow": [
|
|
4
|
-
"
|
|
4
|
+
"Read(//app/musora-platform-backend/**)",
|
|
5
|
+
"Read(//app/musora-platform-frontend/**)",
|
|
6
|
+
"Bash(find:*)",
|
|
7
|
+
"Bash(sed:*)",
|
|
8
|
+
"Read(//app/**)",
|
|
9
|
+
"Bash(cat:*)"
|
|
5
10
|
],
|
|
6
|
-
"deny": []
|
|
11
|
+
"deny": [],
|
|
12
|
+
"ask": []
|
|
7
13
|
}
|
|
8
|
-
}
|
|
14
|
+
}
|
package/.coderabbit.yaml
CHANGED
|
File without changes
|
package/.editorconfig
CHANGED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
package/.prettierignore
CHANGED
|
File without changes
|
package/.prettierrc
CHANGED
|
File without changes
|
package/.yarnrc.yml
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
nodeLinker: node-modules
|
package/CHANGELOG.md
CHANGED
|
@@ -2,6 +2,15 @@
|
|
|
2
2
|
|
|
3
3
|
All notable changes to this project will be documented in this file. See [standard-version](https://github.com/conventional-changelog/standard-version) for commit guidelines.
|
|
4
4
|
|
|
5
|
+
## [2.88.0](https://github.com/railroadmedia/musora-content-services/compare/v2.86.1...v2.88.0) (2025-11-26)
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
### Features
|
|
9
|
+
|
|
10
|
+
* **BEH-1418:** refactor learning path endpoints ([#589](https://github.com/railroadmedia/musora-content-services/issues/589)) ([e3637f6](https://github.com/railroadmedia/musora-content-services/commit/e3637f6359ddf8f4295eead890de6457b942b4ea))
|
|
11
|
+
* Implement unified user reporting system ([7aa42ab](https://github.com/railroadmedia/musora-content-services/commit/7aa42ab8dcee09ce3f293d9fbb1a203dcbe94414))
|
|
12
|
+
* **MU2-1250:** Implement unified user reporting system ([3303282](https://github.com/railroadmedia/musora-content-services/commit/33032829b729efa9f9b1e40f31179127c8c74f9d))
|
|
13
|
+
|
|
5
14
|
## [2.87.0](https://github.com/railroadmedia/musora-content-services/compare/v2.86.1...v2.87.0) (2025-11-26)
|
|
6
15
|
|
|
7
16
|
|
package/README.md
CHANGED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
package/docs/module-Threads.html
CHANGED
|
File without changes
|
package/docs/scripts/collapse.js
CHANGED
|
File without changes
|
|
File without changes
|
|
File without changes
|
package/docs/scripts/nav.js
CHANGED
|
File without changes
|
package/docs/scripts/polyfill.js
CHANGED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
package/docs/scripts/search.js
CHANGED
|
File without changes
|
package/docs/styles/jsdoc.css
CHANGED
|
File without changes
|
package/docs/styles/prettify.css
CHANGED
|
File without changes
|
|
File without changes
|
package/jest.config.js
CHANGED
|
File without changes
|
package/package.json
CHANGED
package/src/contentMetaData.js
CHANGED
|
File without changes
|
package/src/index.d.ts
CHANGED
|
@@ -242,6 +242,11 @@ import {
|
|
|
242
242
|
recommendations
|
|
243
243
|
} from './services/recommendations.js';
|
|
244
244
|
|
|
245
|
+
import {
|
|
246
|
+
getReportIssueOptions,
|
|
247
|
+
report
|
|
248
|
+
} from './services/reporting/reporting.ts';
|
|
249
|
+
|
|
245
250
|
import {
|
|
246
251
|
buildEntityAndTotalQuery,
|
|
247
252
|
fetchAll,
|
|
@@ -597,6 +602,7 @@ declare module 'musora-content-services' {
|
|
|
597
602
|
getRecent,
|
|
598
603
|
getRecentActivity,
|
|
599
604
|
getRecommendedForYou,
|
|
605
|
+
getReportIssueOptions,
|
|
600
606
|
getResumeTimeSeconds,
|
|
601
607
|
getResumeTimeSecondsByIds,
|
|
602
608
|
getSanityDate,
|
|
@@ -662,6 +668,7 @@ declare module 'musora-content-services' {
|
|
|
662
668
|
removeContentAsNotInterested,
|
|
663
669
|
removeUserPractice,
|
|
664
670
|
replyToComment,
|
|
671
|
+
report,
|
|
665
672
|
reportComment,
|
|
666
673
|
reportPlaylist,
|
|
667
674
|
requestEmailChange,
|
package/src/index.js
CHANGED
|
@@ -242,6 +242,11 @@ import {
|
|
|
242
242
|
recommendations
|
|
243
243
|
} from './services/recommendations.js';
|
|
244
244
|
|
|
245
|
+
import {
|
|
246
|
+
getReportIssueOptions,
|
|
247
|
+
report
|
|
248
|
+
} from './services/reporting/reporting.ts';
|
|
249
|
+
|
|
245
250
|
import {
|
|
246
251
|
buildEntityAndTotalQuery,
|
|
247
252
|
fetchAll,
|
|
@@ -596,6 +601,7 @@ export {
|
|
|
596
601
|
getRecent,
|
|
597
602
|
getRecentActivity,
|
|
598
603
|
getRecommendedForYou,
|
|
604
|
+
getReportIssueOptions,
|
|
599
605
|
getResumeTimeSeconds,
|
|
600
606
|
getResumeTimeSecondsByIds,
|
|
601
607
|
getSanityDate,
|
|
@@ -661,6 +667,7 @@ export {
|
|
|
661
667
|
removeContentAsNotInterested,
|
|
662
668
|
removeUserPractice,
|
|
663
669
|
replyToComment,
|
|
670
|
+
report,
|
|
664
671
|
reportComment,
|
|
665
672
|
reportPlaylist,
|
|
666
673
|
requestEmailChange,
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
package/src/lib/httpHelper.js
CHANGED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
@@ -0,0 +1,316 @@
|
|
|
1
|
+
# Reporting Service
|
|
2
|
+
|
|
3
|
+
Service for submitting user reports about content, comments, forum posts, and playlists.
|
|
4
|
+
|
|
5
|
+
## Overview
|
|
6
|
+
|
|
7
|
+
This service provides two main functions for reporting content:
|
|
8
|
+
|
|
9
|
+
1. **`report()`** - Unified method to submit reports for any type of content (content, comments, forum posts, playlists)
|
|
10
|
+
2. **`getReportIssueOptions()`** - Helper function to get valid issue options for each reportable type, with support for platform-specific options (web vs mobile app)
|
|
11
|
+
|
|
12
|
+
Reports are sent to the appropriate team (support or mentors) based on the content type. Both functions are designed to be used by the web platform (MPF) and mobile app (MA) to ensure consistent reporting options and behavior.
|
|
13
|
+
|
|
14
|
+
## Features
|
|
15
|
+
|
|
16
|
+
- ✅ Unified `report()` method for all reportable types
|
|
17
|
+
- ✅ Type-safe issue selection with TypeScript generics
|
|
18
|
+
- ✅ Platform-specific options (web vs mobile app)
|
|
19
|
+
- ✅ Helper function to get valid issue options by type
|
|
20
|
+
- ✅ Rate limiting (10 requests per minute per user)
|
|
21
|
+
- ✅ Emails sent to appropriate teams (support or mentors)
|
|
22
|
+
- ✅ Full TypeScript support with mapped types
|
|
23
|
+
|
|
24
|
+
## Installation
|
|
25
|
+
|
|
26
|
+
The reporting service is included in `musora-content-services`. Simply import the functions you need:
|
|
27
|
+
|
|
28
|
+
```typescript
|
|
29
|
+
import { report, getReportIssueOptions } from 'musora-content-services'
|
|
30
|
+
```
|
|
31
|
+
|
|
32
|
+
Both functions are exported from `services/reporting/reporting.ts`.
|
|
33
|
+
|
|
34
|
+
## API Reference
|
|
35
|
+
|
|
36
|
+
### `report<T>(params)`
|
|
37
|
+
|
|
38
|
+
Unified method to submit a report for any type. Uses TypeScript generics for type-safe issue selection.
|
|
39
|
+
|
|
40
|
+
**Type Parameter:**
|
|
41
|
+
- `T extends ReportableType` - The type of content being reported
|
|
42
|
+
|
|
43
|
+
**Parameters:**
|
|
44
|
+
```typescript
|
|
45
|
+
type ReportParams<T extends ReportableType> = {
|
|
46
|
+
type: T // 'content' | 'comment' | 'forum_post' | 'playlist'
|
|
47
|
+
id: number // ID of the entity being reported
|
|
48
|
+
issue: IssueTypeMap[T] // Type-safe issue (varies by type)
|
|
49
|
+
details?: string // Required when issue is 'other', not sent otherwise
|
|
50
|
+
brand: string // Required: 'drumeo', 'pianote', 'guitareo', 'singeo', 'playbass'
|
|
51
|
+
}
|
|
52
|
+
```
|
|
53
|
+
|
|
54
|
+
**Returns:** `Promise<ReportResponse>`
|
|
55
|
+
```typescript
|
|
56
|
+
interface ReportResponse {
|
|
57
|
+
report_id: number // The ID of the submitted report
|
|
58
|
+
message: string // Success message
|
|
59
|
+
}
|
|
60
|
+
```
|
|
61
|
+
|
|
62
|
+
**Example - Report Content:**
|
|
63
|
+
```typescript
|
|
64
|
+
import { report } from 'musora-content-services'
|
|
65
|
+
|
|
66
|
+
const response = await report({
|
|
67
|
+
type: 'content',
|
|
68
|
+
id: 12345,
|
|
69
|
+
issue: 'video_issue',
|
|
70
|
+
brand: 'drumeo'
|
|
71
|
+
})
|
|
72
|
+
|
|
73
|
+
console.log(`Report submitted with ID: ${response.report_id}`)
|
|
74
|
+
console.log(response.message) // "Report submitted successfully"
|
|
75
|
+
```
|
|
76
|
+
|
|
77
|
+
**Example - Report Comment:**
|
|
78
|
+
```typescript
|
|
79
|
+
await report({
|
|
80
|
+
type: 'comment',
|
|
81
|
+
id: 67890,
|
|
82
|
+
issue: 'offensive_language',
|
|
83
|
+
brand: 'pianote'
|
|
84
|
+
})
|
|
85
|
+
```
|
|
86
|
+
|
|
87
|
+
**Example - Report Forum Post:**
|
|
88
|
+
```typescript
|
|
89
|
+
await report({
|
|
90
|
+
type: 'forum_post',
|
|
91
|
+
id: 45678,
|
|
92
|
+
issue: 'abusive',
|
|
93
|
+
brand: 'guitareo'
|
|
94
|
+
})
|
|
95
|
+
```
|
|
96
|
+
|
|
97
|
+
**Example - Report Playlist:**
|
|
98
|
+
```typescript
|
|
99
|
+
await report({
|
|
100
|
+
type: 'playlist',
|
|
101
|
+
id: 99999,
|
|
102
|
+
issue: 'incorrect_metadata',
|
|
103
|
+
brand: 'singeo'
|
|
104
|
+
})
|
|
105
|
+
```
|
|
106
|
+
|
|
107
|
+
**Example - Report with 'other' issue (details required):**
|
|
108
|
+
```typescript
|
|
109
|
+
await report({
|
|
110
|
+
type: 'content',
|
|
111
|
+
id: 12345,
|
|
112
|
+
issue: 'other',
|
|
113
|
+
details: 'The instructor audio is out of sync with the video',
|
|
114
|
+
brand: 'drumeo'
|
|
115
|
+
})
|
|
116
|
+
```
|
|
117
|
+
|
|
118
|
+
---
|
|
119
|
+
|
|
120
|
+
### `getReportIssueOptions(type, isMobileApp?)`
|
|
121
|
+
|
|
122
|
+
Helper function to get valid issue options for a specific reportable type. Returns both the value and user-friendly label for each option.
|
|
123
|
+
|
|
124
|
+
**Parameters:**
|
|
125
|
+
```typescript
|
|
126
|
+
type: ReportableType // 'content' | 'comment' | 'forum_post' | 'playlist'
|
|
127
|
+
isMobileApp?: boolean // Default: false. Set to true for mobile app options
|
|
128
|
+
```
|
|
129
|
+
|
|
130
|
+
**Returns:** `ReportIssueOption[]`
|
|
131
|
+
```typescript
|
|
132
|
+
interface ReportIssueOption {
|
|
133
|
+
value: string // Issue value to send to API
|
|
134
|
+
label: string // User-friendly label for display
|
|
135
|
+
}
|
|
136
|
+
```
|
|
137
|
+
|
|
138
|
+
**Example - Web Platform:**
|
|
139
|
+
```typescript
|
|
140
|
+
import { getReportIssueOptions } from 'musora-content-services'
|
|
141
|
+
|
|
142
|
+
// Get content issue options for web
|
|
143
|
+
const options = getReportIssueOptions('content')
|
|
144
|
+
// Returns:
|
|
145
|
+
// [
|
|
146
|
+
// { value: 'incorrect_metadata', label: 'The lesson image, title or description is incorrect' },
|
|
147
|
+
// { value: 'video_issue', label: 'Video issue' },
|
|
148
|
+
// { value: 'assignment_issue', label: 'An issue with lesson assignment' },
|
|
149
|
+
// { value: 'other', label: 'Other' }
|
|
150
|
+
// ]
|
|
151
|
+
options.map(opt => ({
|
|
152
|
+
...opt,
|
|
153
|
+
label: t(`report.${opt.value}`)
|
|
154
|
+
}))
|
|
155
|
+
```
|
|
156
|
+
|
|
157
|
+
**Example - Mobile App:**
|
|
158
|
+
```typescript
|
|
159
|
+
// Get content issue options for mobile app (includes download option)
|
|
160
|
+
const options = getReportIssueOptions('content', true)
|
|
161
|
+
// Returns:
|
|
162
|
+
// [
|
|
163
|
+
// { value: 'incorrect_metadata', label: 'The lesson image, title or description is incorrect' },
|
|
164
|
+
// { value: 'video_issue', label: 'Video issue' },
|
|
165
|
+
// { value: 'download_unavailable', label: 'Download is not available' },
|
|
166
|
+
// { value: 'assignment_issue', label: 'An issue with lesson assignment' },
|
|
167
|
+
// { value: 'other', label: 'Other' }
|
|
168
|
+
// ]
|
|
169
|
+
options.map(opt => ({
|
|
170
|
+
...opt,
|
|
171
|
+
label: t(`report.${opt.value}`)
|
|
172
|
+
}))
|
|
173
|
+
```
|
|
174
|
+
|
|
175
|
+
**Example - Comment Issues:**
|
|
176
|
+
```typescript
|
|
177
|
+
const commentOptions = getReportIssueOptions('comment')
|
|
178
|
+
// Returns:
|
|
179
|
+
// [
|
|
180
|
+
// { value: 'offensive_language', label: 'It contains offensive language or content' },
|
|
181
|
+
// { value: 'abusive', label: "It's abusive or harmful" },
|
|
182
|
+
// { value: 'personal_information', label: 'It contains personal information' },
|
|
183
|
+
// { value: 'misleading', label: "It's misleading or a false claim" },
|
|
184
|
+
// { value: 'other', label: 'Other reasons' }
|
|
185
|
+
// ]
|
|
186
|
+
```
|
|
187
|
+
|
|
188
|
+
---
|
|
189
|
+
|
|
190
|
+
## Report Issues by Type
|
|
191
|
+
|
|
192
|
+
### Content & Playlists:
|
|
193
|
+
- `incorrect_metadata` - The lesson image, title or description is incorrect
|
|
194
|
+
- `video_issue` - Video issue
|
|
195
|
+
- `download_unavailable` - Download is not available (mobile app only)
|
|
196
|
+
- `assignment_issue` - An issue with lesson assignment
|
|
197
|
+
- `other` - Other
|
|
198
|
+
|
|
199
|
+
### Comments & Forum Posts:
|
|
200
|
+
- `offensive_language` - It contains offensive language or content
|
|
201
|
+
- `abusive` - It's abusive or harmful
|
|
202
|
+
- `personal_information` - It contains personal information
|
|
203
|
+
- `misleading` - It's misleading or a false claim
|
|
204
|
+
- `other` - Other reasons
|
|
205
|
+
|
|
206
|
+
**Important:** The `details` field is **required** when `issue` is `'other'` and should **not be sent** for other issue types.
|
|
207
|
+
|
|
208
|
+
---
|
|
209
|
+
|
|
210
|
+
## Rate Limiting
|
|
211
|
+
|
|
212
|
+
API requests are rate-limited to **10 requests per minute per user**. If you exceed this limit, you'll receive a `429 Too Many Requests` error.
|
|
213
|
+
|
|
214
|
+
---
|
|
215
|
+
|
|
216
|
+
## Email Recipients
|
|
217
|
+
|
|
218
|
+
Reports are automatically routed to the appropriate team:
|
|
219
|
+
|
|
220
|
+
| Report Type | Email Recipient |
|
|
221
|
+
|-------------|----------------|
|
|
222
|
+
| Forum Posts | mentors@musora.com |
|
|
223
|
+
| Comments | mentors@musora.com |
|
|
224
|
+
| Content | support@musora.com |
|
|
225
|
+
| Playlists | support@musora.com |
|
|
226
|
+
|
|
227
|
+
---
|
|
228
|
+
|
|
229
|
+
## Platform-Specific Options
|
|
230
|
+
|
|
231
|
+
The `getReportIssueOptions()` function supports platform-specific options via the `isMobileApp` parameter:
|
|
232
|
+
|
|
233
|
+
**Web Platform (default):**
|
|
234
|
+
- Does NOT include "Download is not available" option
|
|
235
|
+
- Use: `getReportIssueOptions('content')`
|
|
236
|
+
|
|
237
|
+
**Mobile App:**
|
|
238
|
+
- INCLUDES "Download is not available" option for content and playlists
|
|
239
|
+
- Use: `getReportIssueOptions('content', true)`
|
|
240
|
+
|
|
241
|
+
|
|
242
|
+
---
|
|
243
|
+
|
|
244
|
+
## Error Handling
|
|
245
|
+
|
|
246
|
+
```typescript
|
|
247
|
+
import { report } from 'musora-content-services'
|
|
248
|
+
|
|
249
|
+
try {
|
|
250
|
+
const response = await report({
|
|
251
|
+
type: 'content',
|
|
252
|
+
id: 123,
|
|
253
|
+
issue: 'video_issue',
|
|
254
|
+
brand: 'drumeo'
|
|
255
|
+
})
|
|
256
|
+
|
|
257
|
+
// Handle success
|
|
258
|
+
showToast('Report submitted successfully')
|
|
259
|
+
} catch (error) {
|
|
260
|
+
if (error.status === 429) {
|
|
261
|
+
// Rate limit exceeded
|
|
262
|
+
showToast('Too many reports. Please try again later.')
|
|
263
|
+
} else if (error.status === 422) {
|
|
264
|
+
// Validation error
|
|
265
|
+
showToast(error.message || 'Invalid report data')
|
|
266
|
+
} else if (error.status === 401) {
|
|
267
|
+
// Not authenticated
|
|
268
|
+
showToast('Please log in to submit a report')
|
|
269
|
+
} else {
|
|
270
|
+
// Other errors
|
|
271
|
+
showToast('Failed to submit report')
|
|
272
|
+
}
|
|
273
|
+
}
|
|
274
|
+
```
|
|
275
|
+
|
|
276
|
+
---
|
|
277
|
+
|
|
278
|
+
## Type Safety
|
|
279
|
+
|
|
280
|
+
The reporting service uses TypeScript generics to ensure type-safe issue selection:
|
|
281
|
+
|
|
282
|
+
```typescript
|
|
283
|
+
// ✅ Type-safe: 'video_issue' is valid for content
|
|
284
|
+
await report({
|
|
285
|
+
type: 'content',
|
|
286
|
+
id: 123,
|
|
287
|
+
issue: 'video_issue', // TypeScript knows this is valid
|
|
288
|
+
brand: 'drumeo'
|
|
289
|
+
})
|
|
290
|
+
|
|
291
|
+
// ❌ Type error: 'offensive_language' is not valid for content
|
|
292
|
+
await report({
|
|
293
|
+
type: 'content',
|
|
294
|
+
id: 123,
|
|
295
|
+
issue: 'offensive_language', // TypeScript error!
|
|
296
|
+
brand: 'drumeo'
|
|
297
|
+
})
|
|
298
|
+
|
|
299
|
+
// ✅ Type-safe: 'offensive_language' is valid for comments
|
|
300
|
+
await report({
|
|
301
|
+
type: 'comment',
|
|
302
|
+
id: 456,
|
|
303
|
+
issue: 'offensive_language', // TypeScript knows this is valid
|
|
304
|
+
brand: 'drumeo'
|
|
305
|
+
})
|
|
306
|
+
```
|
|
307
|
+
|
|
308
|
+
The `IssueTypeMap` maps each reportable type to its valid issues:
|
|
309
|
+
```typescript
|
|
310
|
+
type IssueTypeMap = {
|
|
311
|
+
forum_post: ForumIssueType
|
|
312
|
+
comment: CommentIssueType
|
|
313
|
+
content: ContentIssueType
|
|
314
|
+
playlist: PlaylistIssueType
|
|
315
|
+
}
|
|
316
|
+
```
|
|
@@ -0,0 +1,189 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @module Reporting
|
|
3
|
+
* @description Service for submitting user reports about content, comments, forum posts, and playlists.
|
|
4
|
+
*
|
|
5
|
+
* This service provides a unified method to report any type of content when users encounter
|
|
6
|
+
* inappropriate behavior, technical issues, or other problems.
|
|
7
|
+
*
|
|
8
|
+
*/
|
|
9
|
+
|
|
10
|
+
import { HttpClient } from '../../infrastructure/http/HttpClient'
|
|
11
|
+
import { globalConfig } from '../config.js'
|
|
12
|
+
import {
|
|
13
|
+
ReportResponse,
|
|
14
|
+
ReportableType,
|
|
15
|
+
IssueTypeMap,
|
|
16
|
+
ReportIssueOption,
|
|
17
|
+
} from './types'
|
|
18
|
+
import {Brand} from "../../lib/brands";
|
|
19
|
+
|
|
20
|
+
/**
|
|
21
|
+
* Parameters for submitting a report with type-safe issue values
|
|
22
|
+
*/
|
|
23
|
+
export type ReportParams<T extends ReportableType = ReportableType> = {
|
|
24
|
+
/** Type of content being reported */
|
|
25
|
+
type: T
|
|
26
|
+
/** ID of the content being reported */
|
|
27
|
+
id: number
|
|
28
|
+
/** Issue category - type-safe based on reportable type */
|
|
29
|
+
issue: IssueTypeMap[T]
|
|
30
|
+
/** Details about the issue - required when issue is 'other', not sent otherwise */
|
|
31
|
+
details?: string
|
|
32
|
+
/** Brand context (required: drumeo, pianote, guitareo, singeo, playbass) */
|
|
33
|
+
brand: Brand
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
/**
|
|
37
|
+
* Submit a report for any type of content.
|
|
38
|
+
*
|
|
39
|
+
* This is a unified method that handles all report types (content, comment, forum_post, playlist).
|
|
40
|
+
* The issue parameter is type-safe based on the reportable type.
|
|
41
|
+
*
|
|
42
|
+
* @param {ReportParams} params - The report parameters
|
|
43
|
+
* @returns {Promise<ReportResponse>} The report submission response
|
|
44
|
+
* @throws {HttpError} Throws HttpError if the request fails
|
|
45
|
+
*
|
|
46
|
+
* @example
|
|
47
|
+
* // Report content with technical issue - no details needed
|
|
48
|
+
* const response = await report({
|
|
49
|
+
* type: 'content',
|
|
50
|
+
* id: 123,
|
|
51
|
+
* issue: 'video_issue',
|
|
52
|
+
* brand: 'drumeo'
|
|
53
|
+
* })
|
|
54
|
+
*
|
|
55
|
+
* console.log(`Report submitted with ID: ${response.report_id}`)
|
|
56
|
+
*
|
|
57
|
+
* @example
|
|
58
|
+
* // Report with 'other' issue - details required
|
|
59
|
+
* await report({
|
|
60
|
+
* type: 'content',
|
|
61
|
+
* id: 456,
|
|
62
|
+
* issue: 'other',
|
|
63
|
+
* details: 'The instructor audio is out of sync with the video',
|
|
64
|
+
* brand: 'drumeo'
|
|
65
|
+
* })
|
|
66
|
+
*
|
|
67
|
+
* @example
|
|
68
|
+
* // Report forum post
|
|
69
|
+
* await report({
|
|
70
|
+
* type: 'forum_post',
|
|
71
|
+
* id: 789,
|
|
72
|
+
* issue: 'abusive',
|
|
73
|
+
* brand: 'drumeo'
|
|
74
|
+
* })
|
|
75
|
+
*/
|
|
76
|
+
export async function report<T extends ReportableType>(params: ReportParams<T>): Promise<ReportResponse> {
|
|
77
|
+
const httpClient = new HttpClient(globalConfig.baseUrl)
|
|
78
|
+
|
|
79
|
+
// Build request body
|
|
80
|
+
const requestBody: any = {
|
|
81
|
+
reportable_type: params.type,
|
|
82
|
+
reportable_id: params.id,
|
|
83
|
+
issue: params.issue,
|
|
84
|
+
brand: params.brand,
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
// Add details only when provided (required for 'other' issue)
|
|
88
|
+
if (params.details) {
|
|
89
|
+
requestBody.details = params.details
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
const response = await httpClient.post<ReportResponse>(
|
|
93
|
+
'/api/user-reports/v1/reports',
|
|
94
|
+
requestBody
|
|
95
|
+
)
|
|
96
|
+
|
|
97
|
+
return response
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
/**
|
|
101
|
+
* Get valid report issue options for a specific reportable type
|
|
102
|
+
*
|
|
103
|
+
* @param {ReportableType} type - The type of content being reported
|
|
104
|
+
* @param {boolean} isMobileApp - Whether this is for mobile app (includes download option)
|
|
105
|
+
* @returns {ReportIssueOption[]} Array of valid issue options with their labels
|
|
106
|
+
*
|
|
107
|
+
* @example
|
|
108
|
+
* // Web options (default)
|
|
109
|
+
* const contentOptions = getReportIssueOptions('content')
|
|
110
|
+
* // Returns: [
|
|
111
|
+
* // { value: 'incorrect_metadata', label: 'The lesson image, title or description is incorrect' },
|
|
112
|
+
* // { value: 'video_issue', label: 'Video issue' },
|
|
113
|
+
* // { value: 'assignment_issue', label: 'An issue with lesson assignment' },
|
|
114
|
+
* // { value: 'other', label: 'Other' }
|
|
115
|
+
* // ]
|
|
116
|
+
*
|
|
117
|
+
* @example
|
|
118
|
+
* // Mobile app options (includes download)
|
|
119
|
+
* const contentOptions = getReportIssueOptions('content', true)
|
|
120
|
+
* // Returns: [
|
|
121
|
+
* // { value: 'incorrect_metadata', label: 'The lesson image, title or description is incorrect' },
|
|
122
|
+
* // { value: 'video_issue', label: 'Video issue' },
|
|
123
|
+
* // { value: 'download_unavailable', label: 'Download is not available' },
|
|
124
|
+
* // { value: 'assignment_issue', label: 'An issue with lesson assignment' },
|
|
125
|
+
* // { value: 'other', label: 'Other' }
|
|
126
|
+
* // ]
|
|
127
|
+
*/
|
|
128
|
+
export function getReportIssueOptions(type: ReportableType, isMobileApp: boolean = false): ReportIssueOption[] {
|
|
129
|
+
switch (type) {
|
|
130
|
+
case 'forum_post':
|
|
131
|
+
return [
|
|
132
|
+
{ value: 'offensive_language', label: 'It contains offensive language or content' },
|
|
133
|
+
{ value: 'abusive', label: "It's abusive or harmful" },
|
|
134
|
+
{ value: 'personal_information', label: 'It contains personal information' },
|
|
135
|
+
{ value: 'misleading', label: "It's misleading or a false claim" },
|
|
136
|
+
{ value: 'other', label: 'Other reasons' },
|
|
137
|
+
]
|
|
138
|
+
|
|
139
|
+
case 'comment':
|
|
140
|
+
return [
|
|
141
|
+
{ value: 'offensive_language', label: 'It contains offensive language or content' },
|
|
142
|
+
{ value: 'abusive', label: "It's abusive or harmful" },
|
|
143
|
+
{ value: 'personal_information', label: 'It contains personal information' },
|
|
144
|
+
{ value: 'misleading', label: "It's misleading or a false claim" },
|
|
145
|
+
{ value: 'other', label: 'Other reasons' },
|
|
146
|
+
]
|
|
147
|
+
|
|
148
|
+
case 'content':
|
|
149
|
+
const contentOptions = [
|
|
150
|
+
{ value: 'incorrect_metadata', label: 'The lesson image, title or description is incorrect' },
|
|
151
|
+
{ value: 'video_issue', label: 'Video issue' },
|
|
152
|
+
]
|
|
153
|
+
|
|
154
|
+
// Add download option only for mobile app
|
|
155
|
+
if (isMobileApp) {
|
|
156
|
+
contentOptions.push({ value: 'download_unavailable', label: 'Download is not available' })
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
contentOptions.push(
|
|
160
|
+
{ value: 'assignment_issue', label: 'An issue with lesson assignment' },
|
|
161
|
+
{ value: 'other', label: 'Other' }
|
|
162
|
+
)
|
|
163
|
+
|
|
164
|
+
return contentOptions
|
|
165
|
+
|
|
166
|
+
case 'playlist':
|
|
167
|
+
const playlistOptions = [
|
|
168
|
+
{ value: 'incorrect_metadata', label: 'The lesson image, title or description is incorrect' },
|
|
169
|
+
{ value: 'video_issue', label: 'Video issue' },
|
|
170
|
+
]
|
|
171
|
+
|
|
172
|
+
// Add download option only for mobile app
|
|
173
|
+
if (isMobileApp) {
|
|
174
|
+
playlistOptions.push({ value: 'download_unavailable', label: 'Download is not available' })
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
playlistOptions.push(
|
|
178
|
+
{ value: 'assignment_issue', label: 'An issue with lesson assignment' },
|
|
179
|
+
{ value: 'other', label: 'Other' }
|
|
180
|
+
)
|
|
181
|
+
|
|
182
|
+
return playlistOptions
|
|
183
|
+
|
|
184
|
+
default:
|
|
185
|
+
return [
|
|
186
|
+
{ value: 'other', label: 'Other' },
|
|
187
|
+
]
|
|
188
|
+
}
|
|
189
|
+
}
|
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @module Reporting Types
|
|
3
|
+
*/
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* Type of entity being reported
|
|
7
|
+
*/
|
|
8
|
+
export type ReportableType = 'content' | 'comment' | 'forum_post' | 'playlist'
|
|
9
|
+
|
|
10
|
+
/**
|
|
11
|
+
* Valid issue types for forum posts
|
|
12
|
+
*/
|
|
13
|
+
export type ForumIssueType = 'spam' | 'harassment' | 'inappropriate' | 'other'
|
|
14
|
+
|
|
15
|
+
/**
|
|
16
|
+
* Valid issue types for comments
|
|
17
|
+
*/
|
|
18
|
+
export type CommentIssueType = 'offensive_language' | 'abusive' | 'personal_information' | 'misleading' | 'other'
|
|
19
|
+
|
|
20
|
+
/**
|
|
21
|
+
* Valid issue types for content
|
|
22
|
+
*/
|
|
23
|
+
export type ContentIssueType = 'incorrect_metadata' | 'video_issue' | 'download_unavailable' | 'assignment_issue' | 'other'
|
|
24
|
+
|
|
25
|
+
/**
|
|
26
|
+
* Valid issue types for playlists
|
|
27
|
+
*/
|
|
28
|
+
export type PlaylistIssueType = 'incorrect_metadata' | 'video_issue' | 'download_unavailable' | 'assignment_issue' | 'other'
|
|
29
|
+
|
|
30
|
+
/**
|
|
31
|
+
* Map reportable type to its valid issue types
|
|
32
|
+
*/
|
|
33
|
+
export type IssueTypeMap = {
|
|
34
|
+
forum_post: ForumIssueType
|
|
35
|
+
comment: CommentIssueType
|
|
36
|
+
content: ContentIssueType
|
|
37
|
+
playlist: PlaylistIssueType
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
/**
|
|
41
|
+
* Response from submitting a report
|
|
42
|
+
*/
|
|
43
|
+
export interface ReportResponse {
|
|
44
|
+
/** The ID of the submitted report */
|
|
45
|
+
report_id: number
|
|
46
|
+
/** Success message */
|
|
47
|
+
message: string
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
/**
|
|
51
|
+
* Report issue option
|
|
52
|
+
*/
|
|
53
|
+
export interface ReportIssueOption {
|
|
54
|
+
value: string
|
|
55
|
+
label: string
|
|
56
|
+
}
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
package/test/HttpClient.test.js
CHANGED
|
File without changes
|
|
File without changes
|
package/test/dataContext.test.js
CHANGED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
package/test/localStorageMock.js
CHANGED
|
File without changes
|
package/test/log.js
CHANGED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
@@ -0,0 +1,132 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Tests for Reporting Service
|
|
3
|
+
*
|
|
4
|
+
* Note: These tests use mocked HTTP calls. For integration tests,
|
|
5
|
+
* ensure the backend API is running at the configured baseUrl.
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
import { initializeTestService } from './initializeTests.js'
|
|
9
|
+
import {
|
|
10
|
+
reportContent,
|
|
11
|
+
reportForumPost,
|
|
12
|
+
submitReport
|
|
13
|
+
} from '../src/services/reporting/reporting.ts'
|
|
14
|
+
|
|
15
|
+
// Mock HttpClient to avoid actual API calls in tests
|
|
16
|
+
jest.mock('../src/infrastructure/http/HttpClient', () => {
|
|
17
|
+
return {
|
|
18
|
+
HttpClient: jest.fn().mockImplementation(() => ({
|
|
19
|
+
post: jest.fn().mockResolvedValue({
|
|
20
|
+
report_id: 12345,
|
|
21
|
+
message: 'Report submitted successfully',
|
|
22
|
+
is_duplicate: false
|
|
23
|
+
})
|
|
24
|
+
}))
|
|
25
|
+
}
|
|
26
|
+
})
|
|
27
|
+
|
|
28
|
+
describe('Reporting Service', () => {
|
|
29
|
+
beforeEach(() => {
|
|
30
|
+
initializeTestService()
|
|
31
|
+
})
|
|
32
|
+
|
|
33
|
+
describe('reportContent', () => {
|
|
34
|
+
test('should submit a content report with video_not_working category', async () => {
|
|
35
|
+
const result = await reportContent({
|
|
36
|
+
contentId: 123,
|
|
37
|
+
category: 'video_not_working',
|
|
38
|
+
description: 'Video freezes at 2:30'
|
|
39
|
+
})
|
|
40
|
+
|
|
41
|
+
expect(result).toBeDefined()
|
|
42
|
+
expect(result.report_id).toBe(12345)
|
|
43
|
+
expect(result.message).toBe('Report submitted successfully')
|
|
44
|
+
expect(result.is_duplicate).toBe(false)
|
|
45
|
+
})
|
|
46
|
+
|
|
47
|
+
test('should submit a content report without description', async () => {
|
|
48
|
+
const result = await reportContent({
|
|
49
|
+
contentId: 456,
|
|
50
|
+
category: 'incorrect_content'
|
|
51
|
+
})
|
|
52
|
+
|
|
53
|
+
expect(result).toBeDefined()
|
|
54
|
+
expect(result.report_id).toBeDefined()
|
|
55
|
+
})
|
|
56
|
+
|
|
57
|
+
test('should submit a content report with other category and description', async () => {
|
|
58
|
+
const result = await reportContent({
|
|
59
|
+
contentId: 789,
|
|
60
|
+
category: 'other',
|
|
61
|
+
description: 'Audio quality is very poor'
|
|
62
|
+
})
|
|
63
|
+
|
|
64
|
+
expect(result).toBeDefined()
|
|
65
|
+
expect(result.report_id).toBeDefined()
|
|
66
|
+
expect(result.message).toBeDefined()
|
|
67
|
+
})
|
|
68
|
+
})
|
|
69
|
+
|
|
70
|
+
describe('reportForumPost', () => {
|
|
71
|
+
test('should submit a forum post report with required brand', async () => {
|
|
72
|
+
const result = await reportForumPost({
|
|
73
|
+
postId: 555,
|
|
74
|
+
brand: 'drumeo',
|
|
75
|
+
category: 'spam'
|
|
76
|
+
})
|
|
77
|
+
|
|
78
|
+
expect(result).toBeDefined()
|
|
79
|
+
expect(result.report_id).toBeDefined()
|
|
80
|
+
})
|
|
81
|
+
|
|
82
|
+
test('should submit a forum post report with description', async () => {
|
|
83
|
+
const result = await reportForumPost({
|
|
84
|
+
postId: 666,
|
|
85
|
+
brand: 'pianote',
|
|
86
|
+
category: 'harassment',
|
|
87
|
+
description: 'User is harassing other members'
|
|
88
|
+
})
|
|
89
|
+
|
|
90
|
+
expect(result).toBeDefined()
|
|
91
|
+
expect(result.report_id).toBeDefined()
|
|
92
|
+
expect(result.message).toBeDefined()
|
|
93
|
+
})
|
|
94
|
+
})
|
|
95
|
+
|
|
96
|
+
describe('submitReport', () => {
|
|
97
|
+
test('should submit a generic report for content', async () => {
|
|
98
|
+
const result = await submitReport({
|
|
99
|
+
reportableType: 'content',
|
|
100
|
+
reportableId: 999,
|
|
101
|
+
category: 'technical_issue',
|
|
102
|
+
description: 'Page not loading'
|
|
103
|
+
})
|
|
104
|
+
|
|
105
|
+
expect(result).toBeDefined()
|
|
106
|
+
expect(result.report_id).toBeDefined()
|
|
107
|
+
})
|
|
108
|
+
|
|
109
|
+
test('should submit a generic report for playlist', async () => {
|
|
110
|
+
const result = await submitReport({
|
|
111
|
+
reportableType: 'playlist',
|
|
112
|
+
reportableId: 777,
|
|
113
|
+
category: 'incorrect_content'
|
|
114
|
+
})
|
|
115
|
+
|
|
116
|
+
expect(result).toBeDefined()
|
|
117
|
+
expect(result.report_id).toBeDefined()
|
|
118
|
+
})
|
|
119
|
+
|
|
120
|
+
test('should submit a generic report for comment', async () => {
|
|
121
|
+
const result = await submitReport({
|
|
122
|
+
reportableType: 'comment',
|
|
123
|
+
reportableId: 888,
|
|
124
|
+
category: 'inappropriate',
|
|
125
|
+
description: 'Comment contains offensive language'
|
|
126
|
+
})
|
|
127
|
+
|
|
128
|
+
expect(result).toBeDefined()
|
|
129
|
+
expect(result.report_id).toBeDefined()
|
|
130
|
+
})
|
|
131
|
+
})
|
|
132
|
+
})
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|