// (C) Copyright 2015 Moodle Pty Ltd. // // Licensed under the Apache License, Version 2.0 (the "License"); // you may not use this file except in compliance with the License. // You may obtain a copy of the License at // // http://www.apache.org/licenses/LICENSE-2.0 // // Unless required by applicable law or agreed to in writing, software // distributed under the License is distributed on an "AS IS" BASIS, // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. // See the License for the specific language governing permissions and // limitations under the License. import { Injectable } from '@angular/core'; import { CoreSites, CoreSitesCommonWSOptions, CoreSitesReadingStrategy } from '@services/sites'; import { CoreWSExternalWarning, CoreWSExternalFile, CoreWSFile } from '@services/ws'; import { CoreTimeUtils } from '@services/utils/time'; import { CoreUtils } from '@services/utils/utils'; import { CoreSite, CoreSiteWSPreSets } from '@classes/site'; import { CoreCourseLogHelper } from '@features/course/services/log-helper'; import { CoreH5P } from '@features/h5p/services/h5p'; import { CoreH5PDisplayOptions } from '@features/h5p/classes/core'; import { CoreCourseCommonModWSOptions } from '@features/course/services/course'; import { makeSingleton, Translate } from '@singletons/index'; import { CoreWSError } from '@classes/errors/wserror'; import { CoreError } from '@classes/errors/error'; import { AddonModH5PActivityAutoSyncData, AddonModH5PActivitySyncProvider } from './h5pactivity-sync'; const ROOT_CACHE_KEY = 'mmaModH5PActivity:'; /** * Service that provides some features for H5P activity. */ @Injectable({ providedIn: 'root' }) export class AddonModH5PActivityProvider { static readonly COMPONENT = 'mmaModH5PActivity'; static readonly TRACK_COMPONENT = 'mod_h5pactivity'; // Component for tracking. /** * Format an attempt's data. * * @param attempt Attempt to format. * @return Formatted attempt. */ protected formatAttempt(attempt: AddonModH5PActivityWSAttempt): AddonModH5PActivityAttempt { const formattedAttempt: AddonModH5PActivityAttempt = attempt; formattedAttempt.timecreated = attempt.timecreated * 1000; // Convert to milliseconds. formattedAttempt.timemodified = attempt.timemodified * 1000; // Convert to milliseconds. formattedAttempt.success = formattedAttempt.success ?? null; if (!attempt.duration) { formattedAttempt.durationReadable = '-'; formattedAttempt.durationCompact = '-'; } else { formattedAttempt.durationReadable = CoreTimeUtils.formatTime(attempt.duration); formattedAttempt.durationCompact = CoreTimeUtils.formatDurationShort(attempt.duration); } return formattedAttempt; } /** * Format attempt data and results. * * @param attempt Attempt and results to format. */ protected formatAttemptResults(attempt: AddonModH5PActivityWSAttemptResults): AddonModH5PActivityAttemptResults { const formattedAttempt: AddonModH5PActivityAttemptResults = this.formatAttempt(attempt); formattedAttempt.results = formattedAttempt.results?.map((result) => this.formatResult(result)); return formattedAttempt; } /** * Format the attempts of a user. * * @param data Data to format. * @return Formatted data. */ protected formatUserAttempts(data: AddonModH5PActivityWSUserAttempts): AddonModH5PActivityUserAttempts { const formatted: AddonModH5PActivityUserAttempts = data; formatted.attempts = formatted.attempts.map((attempt) => this.formatAttempt(attempt)); if (formatted.scored) { formatted.scored.attempts = formatted.scored.attempts.map((attempt) => this.formatAttempt(attempt)); } return formatted; } /** * Format an attempt's result. * * @param result Result to format. */ protected formatResult(result: AddonModH5PActivityWSResult): AddonModH5PActivityWSResult { result.timecreated = result.timecreated * 1000; // Convert to milliseconds. return result; } /** * Get cache key for access information WS calls. * * @param id H5P activity ID. * @return Cache key. */ protected getAccessInformationCacheKey(id: number): string { return ROOT_CACHE_KEY + 'accessInfo:' + id; } /** * Get access information for a given H5P activity. * * @param id H5P activity ID. * @param options Other options. * @return Promise resolved with the data. */ async getAccessInformation(id: number, options: CoreCourseCommonModWSOptions = {}): Promise { const site = await CoreSites.getSite(options.siteId); const params: AddonModH5pactivityGetH5pactivityAccessInformationWSParams = { h5pactivityid: id, }; const preSets: CoreSiteWSPreSets = { cacheKey: this.getAccessInformationCacheKey(id), updateFrequency: CoreSite.FREQUENCY_OFTEN, component: AddonModH5PActivityProvider.COMPONENT, componentId: options.cmId, ...CoreSites.getReadingStrategyPreSets(options.readingStrategy), // Include reading strategy preSets. }; return site.read('mod_h5pactivity_get_h5pactivity_access_information', params, preSets); } /** * Get attempt results for all user attempts. * * @param id Activity ID. * @param options Other options. * @return Promise resolved with the results of the attempt. */ async getAllAttemptsResults( id: number, options?: AddonModH5PActivityGetAttemptResultsOptions, ): Promise { const userAttempts = await this.getUserAttempts(id, options); const attemptIds = userAttempts.attempts.map((attempt) => attempt.id); if (attemptIds.length) { // Get all the attempts with a single call. return this.getAttemptsResults(id, attemptIds, options); } else { // No attempts. return { activityid: id, attempts: [], warnings: [], }; } } /** * Get cache key for results WS calls. * * @param id Instance ID. * @param attemptsIds Attempts IDs. * @return Cache key. */ protected getAttemptResultsCacheKey(id: number, attemptsIds: number[]): string { return this.getAttemptResultsCommonCacheKey(id) + ':' + JSON.stringify(attemptsIds); } /** * Get common cache key for results WS calls. * * @param id Instance ID. * @return Cache key. */ protected getAttemptResultsCommonCacheKey(id: number): string { return ROOT_CACHE_KEY + 'results:' + id; } /** * Get attempt results. * * @param id Activity ID. * @param attemptId Attempt ID. * @param options Other options. * @return Promise resolved with the results of the attempt. */ async getAttemptResults( id: number, attemptId: number, options?: AddonModH5PActivityGetAttemptResultsOptions, ): Promise { options = options || {}; const site = await CoreSites.getSite(options.siteId); const params: AddonModH5pactivityGetResultsWSParams = { h5pactivityid: id, attemptids: [attemptId], }; const preSets: CoreSiteWSPreSets = { cacheKey: this.getAttemptResultsCacheKey(id, params.attemptids!), updateFrequency: CoreSite.FREQUENCY_SOMETIMES, component: AddonModH5PActivityProvider.COMPONENT, componentId: options.cmId, ...CoreSites.getReadingStrategyPreSets(options.readingStrategy), // Include reading strategy preSets. }; try { const response = await site.read( 'mod_h5pactivity_get_results', params, preSets, ); if (response.warnings?.[0]) { throw new CoreWSError(response.warnings[0]); // Cannot view attempt. } return this.formatAttemptResults(response.attempts[0]); } catch (error) { if (CoreUtils.isWebServiceError(error)) { throw error; } // Check if the full list of results is cached. If so, get the results from there. const cacheOptions: AddonModH5PActivityGetAttemptResultsOptions = { ...options, // Include all the original options. readingStrategy: CoreSitesReadingStrategy.OnlyCache, }; const attemptsResults = await AddonModH5PActivity.getAllAttemptsResults(id, cacheOptions); const attempt = attemptsResults.attempts.find((attempt) => attempt.id == attemptId); if (!attempt) { throw error; } return attempt; } } /** * Get attempts results. * * @param id Activity ID. * @param attemptsIds Attempts IDs. * @param options Other options. * @return Promise resolved with all the attempts. */ async getAttemptsResults( id: number, attemptsIds: number[], options?: AddonModH5PActivityGetAttemptResultsOptions, ): Promise { options = options || {}; const site = await CoreSites.getSite(options.siteId); const params: AddonModH5pactivityGetResultsWSParams = { h5pactivityid: id, attemptids: attemptsIds, }; const preSets: CoreSiteWSPreSets = { cacheKey: this.getAttemptResultsCommonCacheKey(id), updateFrequency: CoreSite.FREQUENCY_SOMETIMES, component: AddonModH5PActivityProvider.COMPONENT, componentId: options.cmId, ...CoreSites.getReadingStrategyPreSets(options.readingStrategy), // Include reading strategy preSets. }; const response = await site.read( 'mod_h5pactivity_get_results', params, preSets, ); response.attempts = response.attempts.map((attempt) => this.formatAttemptResults(attempt)); return response; } /** * Get deployed file from an H5P activity instance. * * @param h5pActivity Activity instance. * @param options Options * @return Promise resolved with the file. */ async getDeployedFile( h5pActivity: AddonModH5PActivityData, options?: AddonModH5PActivityGetDeployedFileOptions, ): Promise { if (h5pActivity.deployedfile) { // File already deployed and still valid, use this one. return h5pActivity.deployedfile; } if (!h5pActivity.package || !h5pActivity.package[0]) { // Shouldn't happen. throw new CoreError('No H5P package found.'); } options = options || {}; // Deploy the file in the server. return CoreH5P.getTrustedH5PFile( h5pActivity.package[0].fileurl, options.displayOptions, options.ignoreCache, options.siteId, ); } /** * Get cache key for H5P activity data WS calls. * * @param courseId Course ID. * @return Cache key. */ protected getH5PActivityDataCacheKey(courseId: number): string { return ROOT_CACHE_KEY + 'h5pactivity:' + courseId; } /** * Get an H5P activity with key=value. If more than one is found, only the first will be returned. * * @param courseId Course ID. * @param key Name of the property to check. * @param value Value to search. * @param options Other options. * @return Promise resolved with the activity data. */ protected async getH5PActivityByField( courseId: number, key: string, value: unknown, options: CoreSitesCommonWSOptions = {}, ): Promise { const site = await CoreSites.getSite(options.siteId); const params: AddonModH5pactivityGetByCoursesWSParams = { courseids: [courseId], }; const preSets: CoreSiteWSPreSets = { cacheKey: this.getH5PActivityDataCacheKey(courseId), updateFrequency: CoreSite.FREQUENCY_RARELY, component: AddonModH5PActivityProvider.COMPONENT, ...CoreSites.getReadingStrategyPreSets(options.readingStrategy), // Include reading strategy preSets. }; const response = await site.read( 'mod_h5pactivity_get_h5pactivities_by_courses', params, preSets, ); const currentActivity = response.h5pactivities.find((h5pActivity) => h5pActivity[key] == value); if (currentActivity) { return currentActivity; } throw new CoreError(Translate.instant('addon.mod_h5pactivity.errorgetactivity')); } /** * Get an H5P activity by module ID. * * @param courseId Course ID. * @param cmId Course module ID. * @param options Other options. * @return Promise resolved with the activity data. */ getH5PActivity(courseId: number, cmId: number, options: CoreSitesCommonWSOptions = {}): Promise { return this.getH5PActivityByField(courseId, 'coursemodule', cmId, options); } /** * Get an H5P activity by context ID. * * @param courseId Course ID. * @param contextId Context ID. * @param options Other options. * @return Promise resolved with the activity data. */ getH5PActivityByContextId( courseId: number, contextId: number, options: CoreSitesCommonWSOptions = {}, ): Promise { return this.getH5PActivityByField(courseId, 'context', contextId, options); } /** * Get an H5P activity by instance ID. * * @param courseId Course ID. * @param id Instance ID. * @param options Other options. * @return Promise resolved with the activity data. */ getH5PActivityById(courseId: number, id: number, options: CoreSitesCommonWSOptions = {}): Promise { return this.getH5PActivityByField(courseId, 'id', id, options); } /** * Get cache key for attemps WS calls. * * @param id Instance ID. * @param userIds User IDs. * @return Cache key. */ protected getUserAttemptsCacheKey(id: number, userIds: number[]): string { return this.getUserAttemptsCommonCacheKey(id) + ':' + JSON.stringify(userIds); } /** * Get common cache key for attempts WS calls. * * @param id Instance ID. * @return Cache key. */ protected getUserAttemptsCommonCacheKey(id: number): string { return ROOT_CACHE_KEY + 'attempts:' + id; } /** * Get attempts of a certain user. * * @param id Activity ID. * @param options Other options. * @return Promise resolved with the attempts of the user. */ async getUserAttempts( id: number, options: AddonModH5PActivityGetAttemptsOptions = {}, ): Promise { const site = await CoreSites.getSite(options.siteId); const params: AddonModH5pactivityGetAttemptsWSParams = { h5pactivityid: id, userids: [options.userId || site.getUserId()], }; const preSets: CoreSiteWSPreSets = { cacheKey: this.getUserAttemptsCacheKey(id, params.userids!), updateFrequency: CoreSite.FREQUENCY_SOMETIMES, component: AddonModH5PActivityProvider.COMPONENT, componentId: options.cmId, ...CoreSites.getReadingStrategyPreSets(options.readingStrategy), // Include reading strategy preSets. }; const response = await site.read('mod_h5pactivity_get_attempts', params, preSets); if (response.warnings?.[0]) { throw new CoreWSError(response.warnings[0]); // Cannot view user attempts. } return this.formatUserAttempts(response.usersattempts[0]); } /** * Invalidates access information. * * @param id H5P activity ID. * @param siteId Site ID. If not defined, current site. * @return Promise resolved when the data is invalidated. */ async invalidateAccessInformation(id: number, siteId?: string): Promise { const site = await CoreSites.getSite(siteId); await site.invalidateWsCacheForKey(this.getAccessInformationCacheKey(id)); } /** * Invalidates H5P activity data. * * @param courseId Course ID. * @param siteId Site ID. If not defined, current site. * @return Promise resolved when the data is invalidated. */ async invalidateActivityData(courseId: number, siteId?: string): Promise { const site = await CoreSites.getSite(siteId); await site.invalidateWsCacheForKey(this.getH5PActivityDataCacheKey(courseId)); } /** * Invalidates all attempts results for H5P activity. * * @param id Activity ID. * @param siteId Site ID. If not defined, current site. * @return Promise resolved when the data is invalidated. */ async invalidateAllResults(id: number, siteId?: string): Promise { const site = await CoreSites.getSite(siteId); await site.invalidateWsCacheForKey(this.getAttemptResultsCommonCacheKey(id)); } /** * Invalidates results of a certain attempt for H5P activity. * * @param id Activity ID. * @param attemptId Attempt ID. * @param siteId Site ID. If not defined, current site. * @return Promise resolved when the data is invalidated. */ async invalidateAttemptResults(id: number, attemptId: number, siteId?: string): Promise { const site = await CoreSites.getSite(siteId); await site.invalidateWsCacheForKey(this.getAttemptResultsCacheKey(id, [attemptId])); } /** * Invalidates all users attempts for H5P activity. * * @param id Activity ID. * @param siteId Site ID. If not defined, current site. * @return Promise resolved when the data is invalidated. */ async invalidateAllUserAttempts(id: number, siteId?: string): Promise { const site = await CoreSites.getSite(siteId); await site.invalidateWsCacheForKey(this.getUserAttemptsCommonCacheKey(id)); } /** * Invalidates attempts of a certain user for H5P activity. * * @param id Activity ID. * @param userId User ID. If not defined, current user in the site. * @param siteId Site ID. If not defined, current site. * @return Promise resolved when the data is invalidated. */ async invalidateUserAttempts(id: number, userId?: number, siteId?: string): Promise { const site = await CoreSites.getSite(siteId); userId = userId || site.getUserId(); await site.invalidateWsCacheForKey(this.getUserAttemptsCacheKey(id, [userId])); } /** * Delete launcher. * * @return Promise resolved when the launcher file is deleted. */ async isPluginEnabled(siteId?: string): Promise { const site = await CoreSites.getSite(siteId); return site.wsAvailable('mod_h5pactivity_get_h5pactivities_by_courses'); } /** * Report an H5P activity as being viewed. * * @param id H5P activity ID. * @param name Name of the activity. * @param siteId Site ID. If not defined, current site. * @return Promise resolved when the WS call is successful. */ logView(id: number, name?: string, siteId?: string): Promise { const params: AddonModH5pactivityViewH5pactivityWSParams = { h5pactivityid: id, }; return CoreCourseLogHelper.logSingle( 'mod_h5pactivity_view_h5pactivity', params, AddonModH5PActivityProvider.COMPONENT, id, name, 'h5pactivity', {}, siteId, ); } } export const AddonModH5PActivity = makeSingleton(AddonModH5PActivityProvider); /** * Basic data for an H5P activity, exported by Moodle class h5pactivity_summary_exporter. */ export type AddonModH5PActivityData = { id: number; // The primary key of the record. course: number; // Course id this h5p activity is part of. name: string; // The name of the activity module instance. timecreated?: number; // Timestamp of when the instance was added to the course. timemodified?: number; // Timestamp of when the instance was last modified. intro: string; // H5P activity description. introformat: number; // Intro format (1 = HTML, 0 = MOODLE, 2 = PLAIN or 4 = MARKDOWN). grade?: number; // The maximum grade for submission. displayoptions: number; // H5P Button display options. enabletracking: number; // Enable xAPI tracking. grademethod: number; // Which H5P attempt is used for grading. contenthash?: string; // Sha1 hash of file content. coursemodule: number; // Coursemodule. context: number; // Context ID. introfiles: CoreWSExternalFile[]; package: CoreWSExternalFile[]; deployedfile?: { filename?: string; // File name. filepath?: string; // File path. filesize?: number; // File size. fileurl: string; // Downloadable file url. timemodified?: number; // Time modified. mimetype?: string; // File mime type. }; }; /** * Params of mod_h5pactivity_get_h5pactivities_by_courses WS. */ export type AddonModH5pactivityGetByCoursesWSParams = { courseids?: number[]; // Array of course ids. }; /** * Data returned by mod_h5pactivity_get_h5pactivities_by_courses WS. */ export type AddonModH5pactivityGetByCoursesWSResponse = { h5pactivities: AddonModH5PActivityData[]; warnings?: CoreWSExternalWarning[]; }; /** * Params of mod_h5pactivity_get_h5pactivity_access_information WS. */ export type AddonModH5pactivityGetH5pactivityAccessInformationWSParams = { h5pactivityid: number; // H5p activity instance id. }; /** * Data returned by mod_h5pactivity_get_h5pactivity_access_information WS. */ export type AddonModH5pactivityGetH5pactivityAccessInformationWSResponse = { warnings?: CoreWSExternalWarning[]; canview?: boolean; // Whether the user has the capability mod/h5pactivity:view allowed. canaddinstance?: boolean; // Whether the user has the capability mod/h5pactivity:addinstance allowed. cansubmit?: boolean; // Whether the user has the capability mod/h5pactivity:submit allowed. canreviewattempts?: boolean; // Whether the user has the capability mod/h5pactivity:reviewattempts allowed. }; /** * Result of WS mod_h5pactivity_get_h5pactivity_access_information. */ export type AddonModH5PActivityAccessInfo = AddonModH5pactivityGetH5pactivityAccessInformationWSResponse; /** * Params of mod_h5pactivity_get_attempts WS. */ export type AddonModH5pactivityGetAttemptsWSParams = { h5pactivityid: number; // H5p activity instance id. userids?: number[]; // User ids. }; /** * Data returned by mod_h5pactivity_get_attempts WS. */ export type AddonModH5pactivityGetAttemptsWSResponse = { activityid: number; // Activity course module ID. usersattempts: AddonModH5PActivityWSUserAttempts[]; // The complete users attempts list. warnings?: CoreWSExternalWarning[]; }; /** * Params of mod_h5pactivity_get_results WS. */ export type AddonModH5pactivityGetResultsWSParams = { h5pactivityid: number; // H5p activity instance id. attemptids?: number[]; // Attempt ids. }; /** * Data returned by mod_h5pactivity_get_results WS. */ export type AddonModH5pactivityGetResultsWSResponse = { activityid: number; // Activity course module ID. attempts: AddonModH5PActivityWSAttemptResults[]; // The complete attempts list. warnings?: CoreWSExternalWarning[]; }; /** * Attempts results with some calculated data. */ export type AddonModH5PActivityAttemptsResults = Omit & { attempts: AddonModH5PActivityAttemptResults[]; // The complete attempts list. }; /** * Attempts data for a user as returned by the WS mod_h5pactivity_get_attempts. */ export type AddonModH5PActivityWSUserAttempts = { userid: number; // The user id. attempts: AddonModH5PActivityWSAttempt[]; // The complete attempts list. scored?: { // Attempts used to grade the activity. title: string; // Scored attempts title. grademethod: string; // Scored attempts title. attempts: AddonModH5PActivityWSAttempt[]; // List of the grading attempts. }; }; /** * Attempt data as returned by the WS mod_h5pactivity_get_attempts. */ export type AddonModH5PActivityWSAttempt = { id: number; // ID of the context. h5pactivityid: number; // ID of the H5P activity. userid: number; // ID of the user. timecreated: number; // Attempt creation. timemodified: number; // Attempt modified. attempt: number; // Attempt number. rawscore: number; // Attempt score value. maxscore: number; // Attempt max score. duration: number; // Attempt duration in seconds. completion?: number; // Attempt completion. success?: number | null; // Attempt success. scaled: number; // Attempt scaled. }; /** * Attempt and results data as returned by the WS mod_h5pactivity_get_results. */ export type AddonModH5PActivityWSAttemptResults = AddonModH5PActivityWSAttempt & { results?: AddonModH5PActivityWSResult[]; // The results of the attempt. }; /** * Attempt result data as returned by the WS mod_h5pactivity_get_results. */ export type AddonModH5PActivityWSResult = { id: number; // ID of the context. attemptid: number; // ID of the H5P attempt. subcontent: string; // Subcontent identifier. timecreated: number; // Result creation. interactiontype: string; // Interaction type. description: string; // Result description. content?: string; // Result extra content. rawscore: number; // Result score value. maxscore: number; // Result max score. duration?: number; // Result duration in seconds. completion?: number; // Result completion. success?: number | null; // Result success. optionslabel?: string; // Label used for result options. correctlabel?: string; // Label used for correct answers. answerlabel?: string; // Label used for user answers. track?: boolean; // If the result has valid track information. options?: { // The statement options. description: string; // Option description. id: number; // Option identifier. correctanswer: AddonModH5PActivityWSResultAnswer; // The option correct answer. useranswer: AddonModH5PActivityWSResultAnswer; // The option user answer. }[]; }; /** * Result answer as returned by the WS mod_h5pactivity_get_results. */ export type AddonModH5PActivityWSResultAnswer = { answer?: string; // Option text value. correct?: boolean; // If has to be displayed as correct. incorrect?: boolean; // If has to be displayed as incorrect. text?: boolean; // If has to be displayed as simple text. checked?: boolean; // If has to be displayed as a checked option. unchecked?: boolean; // If has to be displayed as a unchecked option. pass?: boolean; // If has to be displayed as passed. fail?: boolean; // If has to be displayed as failed. }; /** * User attempts data with some calculated data. */ export type AddonModH5PActivityUserAttempts = Omit & { attempts: AddonModH5PActivityAttempt[]; // The complete attempts list. scored?: { // Attempts used to grade the activity. title: string; // Scored attempts title. grademethod: string; // Scored attempts title. attempts: AddonModH5PActivityAttempt[]; // List of the grading attempts. }; }; /** * Attempt with some calculated data. */ export type AddonModH5PActivityAttempt = AddonModH5PActivityWSAttempt & { durationReadable?: string; // Duration in a human readable format. durationCompact?: string; // Duration in a "short" human readable format. }; /** * Attempt and results data with some calculated data. */ export type AddonModH5PActivityAttemptResults = AddonModH5PActivityAttempt & { results?: AddonModH5PActivityWSResult[]; // The results of the attempt. }; /** * Options to pass to getDeployedFile function. */ export type AddonModH5PActivityGetDeployedFileOptions = { displayOptions?: CoreH5PDisplayOptions; // Display options ignoreCache?: boolean; // Whether to ignore cache. Will fail if offline or server down. siteId?: string; // Site ID. If not defined, current site. }; /** * Options to pass to getAttemptResults function. */ export type AddonModH5PActivityGetAttemptResultsOptions = CoreCourseCommonModWSOptions & { userId?: number; // User ID. If not defined, user of the site. }; /** * Options to pass to getAttempts function. */ export type AddonModH5PActivityGetAttemptsOptions = AddonModH5PActivityGetAttemptResultsOptions; /** * Params of mod_h5pactivity_view_h5pactivity WS. */ export type AddonModH5pactivityViewH5pactivityWSParams = { h5pactivityid: number; // H5P activity instance id. }; declare module '@singletons/events' { /** * Augment CoreEventsData interface with events specific to this service. * * @see https://www.typescriptlang.org/docs/handbook/declaration-merging.html#module-augmentation */ export interface CoreEventsData { [AddonModH5PActivitySyncProvider.AUTO_SYNCED]: AddonModH5PActivityAutoSyncData; } }