// (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 { CoreSite, CoreSiteWSPreSets } from '@classes/site'; import { CoreCourseCommonModWSOptions } from '@features/course/services/course'; import { CoreCourseLogHelper } from '@features/course/services/log-helper'; import { CoreUser } from '@features/user/services/user'; import { CoreApp } from '@services/app'; import { CoreFilepool } from '@services/filepool'; import { CoreGroups } from '@services/groups'; import { CoreSitesCommonWSOptions, CoreSites, CoreSitesReadingStrategy } from '@services/sites'; import { CoreUrlUtils } from '@services/utils/url'; import { CoreUtils } from '@services/utils/utils'; import { CoreStatusWithWarningsWSResponse, CoreWSExternalFile, CoreWSExternalWarning } from '@services/ws'; import { makeSingleton, Translate } from '@singletons'; import { AddonModForumOffline, AddonModForumReplyOptions } from './offline.service'; const ROOT_CACHE_KEY = 'mmaModForum:'; /** * Service that provides some features for forums. * * @todo Add all content. */ @Injectable({ providedIn: 'root' }) export class AddonModForumProvider { static readonly COMPONENT = 'mmaModForum'; static readonly DISCUSSIONS_PER_PAGE = 10; // Max of discussions per page. static readonly NEW_DISCUSSION_EVENT = 'addon_mod_forum_new_discussion'; static readonly REPLY_DISCUSSION_EVENT = 'addon_mod_forum_reply_discussion'; static readonly VIEW_DISCUSSION_EVENT = 'addon_mod_forum_view_discussion'; static readonly CHANGE_DISCUSSION_EVENT = 'addon_mod_forum_change_discussion_status'; static readonly MARK_READ_EVENT = 'addon_mod_forum_mark_read'; static readonly LEAVING_POSTS_PAGE = 'addon_mod_forum_leaving_posts_page'; static readonly PREFERENCE_SORTORDER = 'forum_discussionlistsortorder'; static readonly SORTORDER_LASTPOST_DESC = 1; static readonly SORTORDER_LASTPOST_ASC = 2; static readonly SORTORDER_CREATED_DESC = 3; static readonly SORTORDER_CREATED_ASC = 4; static readonly SORTORDER_REPLIES_DESC = 5; static readonly SORTORDER_REPLIES_ASC = 6; static readonly ALL_PARTICIPANTS = -1; static readonly ALL_GROUPS = -2; /** * Get cache key for can add discussion WS calls. * * @param forumId Forum ID. * @param groupId Group ID. * @return Cache key. */ protected getCanAddDiscussionCacheKey(forumId: number, groupId: number): string { return this.getCommonCanAddDiscussionCacheKey(forumId) + groupId; } /** * Get common part of cache key for can add discussion WS calls. * TODO: Use getForumDataCacheKey as a prefix. * * @param forumId Forum ID. * @return Cache key. */ protected getCommonCanAddDiscussionCacheKey(forumId: number): string { return ROOT_CACHE_KEY + 'canadddiscussion:' + forumId + ':'; } /** * Get prefix cache key for all forum activity data WS calls. * * @param forumId Forum ID. * @return Cache key. */ protected getForumDataPrefixCacheKey(forumId: number): string { return ROOT_CACHE_KEY + forumId; } /** * Get cache key for discussion post data WS calls. * * @param forumId Forum ID. * @param discussionId Discussion ID. * @param postId Course ID. * @return Cache key. */ protected getDiscussionPostDataCacheKey(forumId: number, discussionId: number, postId: number): string { return this.getForumDiscussionDataCacheKey(forumId, discussionId) + ':post:' + postId; } /** * Get cache key for forum data WS calls. * * @param courseId Course ID. * @return Cache key. */ protected getForumDiscussionDataCacheKey(forumId: number, discussionId: number): string { return this.getForumDataPrefixCacheKey(forumId) + ':discussion:' + discussionId; } /** * Get cache key for forum data WS calls. * * @param courseId Course ID. * @return Cache key. */ protected getForumDataCacheKey(courseId: number): string { return ROOT_CACHE_KEY + 'forum:' + courseId; } /** * Get cache key for forum access information WS calls. * TODO: Use getForumDataCacheKey as a prefix. * * @param forumId Forum ID. * @return Cache key. */ protected getAccessInformationCacheKey(forumId: number): string { return ROOT_CACHE_KEY + 'accessInformation:' + forumId; } /** * Get cache key for forum discussion posts WS calls. * TODO: Use getForumDiscussionDataCacheKey instead. * * @param discussionId Discussion ID. * @return Cache key. */ protected getDiscussionPostsCacheKey(discussionId: number): string { return ROOT_CACHE_KEY + 'discussion:' + discussionId; } /** * Get cache key for forum discussions list WS calls. * * @param forumId Forum ID. * @param sortOrder Sort order. * @return Cache key. */ protected getDiscussionsListCacheKey(forumId: number, sortOrder: number): string { let key = ROOT_CACHE_KEY + 'discussions:' + forumId; if (sortOrder != AddonModForumProvider.SORTORDER_LASTPOST_DESC) { key += ':' + sortOrder; } return key; } /** * Add a new discussion. It will fail if offline or cannot connect. * * @param forumId Forum ID. * @param subject New discussion's subject. * @param message New discussion's message. * @param options Options (subscribe, pin, ...). * @param groupId Group this discussion belongs to. * @param siteId Site ID. If not defined, current site. * @return Promise resolved when the discussion is created. */ async addNewDiscussionOnline( forumId: number, subject: string, message: string, options?: AddonModForumAddDiscussionWSOptionsObject, groupId?: number, siteId?: string, ): Promise { const site = await CoreSites.instance.getSite(siteId); const params: AddonModForumAddDiscussionWSParams = { forumid: forumId, subject: subject, message: message, // eslint-disable-next-line max-len options: CoreUtils.instance.objectToArrayOfObjects( options || {}, 'name', 'value', ), }; if (groupId) { params.groupid = groupId; } const response = await site.write('mod_forum_add_discussion', params); // Other errors ocurring. if (!response || !response.discussionid) { return Promise.reject(CoreUtils.instance.createFakeWSError('')); } else { return response.discussionid; } } /** * Check if a user can post to a certain group. * * @param forumId Forum ID. * @param groupId Group ID. * @param options Other options. * @return Promise resolved with an object with the following properties: * - status (boolean) * - canpindiscussions (boolean) * - cancreateattachment (boolean) */ async canAddDiscussion( forumId: number, groupId: number, options: CoreCourseCommonModWSOptions = {}, ): Promise { const params: AddonModForumCanAddDiscussionWSParams = { forumid: forumId, groupid: groupId, }; const preSets = { cacheKey: this.getCanAddDiscussionCacheKey(forumId, groupId), component: AddonModForumProvider.COMPONENT, componentId: options.cmId, ...CoreSites.instance.getReadingStrategyPreSets(options.readingStrategy), // Include reading strategy preSets. }; const site = await CoreSites.instance.getSite(options.siteId); const result = await site.read('mod_forum_can_add_discussion', params, preSets); if (!result) { throw new Error('Invalid response calling mod_forum_can_add_discussion'); } if (typeof result.canpindiscussions == 'undefined') { // WS doesn't support it yet, default it to false to prevent students from seeing the option. result.canpindiscussions = false; } if (typeof result.cancreateattachment == 'undefined') { // WS doesn't support it yet, default it to true since usually the users will be able to create them. result.cancreateattachment = true; } return result; } /** * Check if a user can post to all groups. * * @param forumId Forum ID. * @param options Other options. * @return Promise resolved with an object with the following properties: * - status (boolean) * - canpindiscussions (boolean) * - cancreateattachment (boolean) */ canAddDiscussionToAll(forumId: number, options: CoreCourseCommonModWSOptions = {}): Promise { return this.canAddDiscussion(forumId, AddonModForumProvider.ALL_PARTICIPANTS, options); } /** * Delete a post. * * @param postId Post id. * @param siteId Site ID. If not defined, current site. * @return Promise resolved when done. * @since 3.8 */ async deletePost(postId: number, siteId?: string): Promise { const site = await CoreSites.instance.getSite(siteId); const params: AddonModForumDeletePostWSParams = { postid: postId, }; await site.write('mod_forum_delete_post', params); } /** * Extract the starting post of a discussion from a list of posts. The post is removed from the array passed as a parameter. * * @param posts Posts to search. * @return Starting post or undefined if not found. */ extractStartingPost(posts: AddonModForumPost[]): AddonModForumPost | undefined { const index = posts.findIndex((post) => !post.parentid); return index >= 0 ? posts.splice(index, 1).pop() : undefined; } /** * There was a bug adding new discussions to All Participants (see MDL-57962). Check if it's fixed. * * @return True if fixed, false otherwise. */ isAllParticipantsFixed(): boolean { return !!CoreSites.instance.getCurrentSite()?.isVersionGreaterEqualThan(['3.1.5', '3.2.2']); } /** * Returns whether or not getDiscussionPost WS available or not. * * @return If WS is avalaible. * @since 3.8 */ isGetDiscussionPostAvailable(): boolean { return CoreSites.instance.wsAvailableInCurrentSite('mod_forum_get_discussion_post'); } /** * Returns whether or not getDiscussionPost WS available or not. * * @param site Site. If not defined, current site. * @return If WS is avalaible. * @since 3.7 */ isGetDiscussionPostsAvailable(site?: CoreSite): boolean { return site ? site.wsAvailable('mod_forum_get_discussion_posts') : CoreSites.instance.wsAvailableInCurrentSite('mod_forum_get_discussion_posts'); } /** * Returns whether or not deletePost WS available or not. * * @return If WS is avalaible. * @since 3.8 */ isDeletePostAvailable(): boolean { return CoreSites.instance.wsAvailableInCurrentSite('mod_forum_delete_post'); } /** * Returns whether or not updatePost WS available or not. * * @return If WS is avalaible. * @since 3.8 */ isUpdatePostAvailable(): boolean { return CoreSites.instance.wsAvailableInCurrentSite('mod_forum_update_discussion_post'); } /** * Format discussions, setting groupname if the discussion group is valid. * * @param cmId Forum cmid. * @param discussions List of discussions to format. * @return Promise resolved with the formatted discussions. */ formatDiscussionsGroups(cmId: number, discussions: any[]): Promise { discussions = CoreUtils.instance.clone(discussions); return CoreGroups.instance.getActivityAllowedGroups(cmId).then((result) => { const strAllParts = Translate.instant('core.allparticipants'); const strAllGroups = Translate.instant('core.allgroups'); // Turn groups into an object where each group is identified by id. const groups = {}; result.groups.forEach((fg) => { groups[fg.id] = fg; }); // Format discussions. discussions.forEach((disc) => { if (disc.groupid == AddonModForumProvider.ALL_PARTICIPANTS) { disc.groupname = strAllParts; } else if (disc.groupid == AddonModForumProvider.ALL_GROUPS) { // Offline discussions only. disc.groupname = strAllGroups; } else { const group = groups[disc.groupid]; if (group) { disc.groupname = group.name; } } }); return discussions; }).catch(() => discussions); } /** * Get all course forums. * * @param courseId Course ID. * @param options Other options. * @return Promise resolved when the forums are retrieved. */ async getCourseForums(courseId: number, options: CoreSitesCommonWSOptions = {}): Promise { const site = await CoreSites.instance.getSite(options.siteId); const params: AddonModForumGetForumsByCoursesWSParams = { courseids: [courseId], }; const preSets: CoreSiteWSPreSets = { cacheKey: this.getForumDataCacheKey(courseId), updateFrequency: CoreSite.FREQUENCY_RARELY, component: AddonModForumProvider.COMPONENT, ...CoreSites.instance.getReadingStrategyPreSets(options.readingStrategy), }; return site.read('mod_forum_get_forums_by_courses', params, preSets); } /** * Get a particular discussion post. * * @param forumId Forum ID. * @param discussionId Discussion ID. * @param postId Post ID. * @param options Other options. * @return Promise resolved when the post is retrieved. */ async getDiscussionPost( forumId: number, discussionId: number, postId: number, options: CoreCourseCommonModWSOptions = {}, ): Promise { const site = await CoreSites.instance.getSite(options.siteId); const params: AddonModForumGetDiscussionPostWSParams = { postid: postId, }; const preSets = { cacheKey: this.getDiscussionPostDataCacheKey(forumId, discussionId, postId), updateFrequency: CoreSite.FREQUENCY_USUALLY, component: AddonModForumProvider.COMPONENT, componentId: options.cmId, ...CoreSites.instance.getReadingStrategyPreSets(options.readingStrategy), // Include reading strategy preSets. }; const response = await site.read( 'mod_forum_get_discussion_post', params, preSets, ); if (!response.post) { throw new Error('Post not found'); } return response.post; } /** * Get a forum by course module ID. * * @param courseId Course ID. * @param cmId Course module ID. * @param options Other options. * @return Promise resolved when the forum is retrieved. */ async getForum(courseId: number, cmId: number, options: CoreSitesCommonWSOptions = {}): Promise { const forums = await this.getCourseForums(courseId, options); const forum = forums.find(forum => forum.cmid == cmId); if (!forum) { throw new Error('Forum not found'); } return forum; } /** * Get a forum by forum ID. * * @param courseId Course ID. * @param forumId Forum ID. * @param options Other options. * @return Promise resolved when the forum is retrieved. */ async getForumById(courseId: number, forumId: number, options: CoreSitesCommonWSOptions = {}): Promise { const forums = await this.getCourseForums(courseId, options); const forum = forums.find(forum => forum.id === forumId); if (!forum) { throw new Error(`Forum with id ${forumId} not found`); } return forum; } /** * Get access information for a given forum. * * @param forumId Forum ID. * @param options Other options. * @return Object with access information. * @since 3.7 */ async getAccessInformation( forumId: number, options: CoreCourseCommonModWSOptions = {}, ): Promise { const site = await CoreSites.instance.getSite(options.siteId); if (!site.wsAvailable('mod_forum_get_forum_access_information')) { // Access information not available for 3.6 or older sites. return {}; } const params: AddonModForumGetForumAccessInformationWSParams = { forumid: forumId, }; const preSets = { cacheKey: this.getAccessInformationCacheKey(forumId), component: AddonModForumProvider.COMPONENT, componentId: options.cmId, ...CoreSites.instance.getReadingStrategyPreSets(options.readingStrategy), // Include reading strategy preSets. }; return site.read( 'mod_forum_get_forum_access_information', params, preSets, ); } /** * Get forum discussion posts. * * @param discussionId Discussion ID. * @param options Other options. * @return Promise resolved with forum posts and rating info. */ async getDiscussionPosts(discussionId: number, options: CoreCourseCommonModWSOptions = {}): Promise<{ posts: AddonModForumPost[]; courseid?: number; forumid?: number; ratinginfo?: AddonModForumRatingInfo; }> { // Convenience function to translate legacy data to new format. const translateLegacyPostsFormat = (posts: any[]): any[] => posts.map((post) => { const newPost = { id: post.id , discussionid: post.discussion, parentid: post.parent, hasparent: !!post.parent, author: { id: post.userid, fullname: post.userfullname, urls: { profileimage: post.userpictureurl }, }, timecreated: post.created, subject: post.subject, message: post.message, attachments: post.attachments, capabilities: { reply: !!post.canreply, }, unread: !post.postread, isprivatereply: !!post.isprivatereply, tags: post.tags, }; if (post.groupname) { newPost.author['groups'] = [{ name: post.groupname }]; } return newPost; }); // For some reason, the new WS doesn't use the tags exporter so it returns a different format than other WebServices. // Convert the new format to the exporter one so it's the same as in other WebServices. const translateTagsFormatToLegacy = (posts: any[]): any[] => { posts.forEach((post) => { post.tags = post.tags.map((tag) => { const viewUrl = (tag.urls && tag.urls.view) || ''; const params = CoreUrlUtils.instance.extractUrlParams(viewUrl); return { id: tag.tagid, taginstanceid: tag.id, flag: tag.flag ? 1 : 0, isstandard: tag.isstandard, rawname: tag.displayname, name: tag.displayname, tagcollid: params.tc ? Number(params.tc) : undefined, taginstancecontextid: params.from ? Number(params.from) : undefined, }; }); }); return posts; }; const params: AddonModForumGetDiscussionPostsWSParams | AddonModForumGetForumDiscussionPostsWSParams = { discussionid: discussionId, }; const preSets = { cacheKey: this.getDiscussionPostsCacheKey(discussionId), component: AddonModForumProvider.COMPONENT, componentId: options.cmId, ...CoreSites.instance.getReadingStrategyPreSets(options.readingStrategy), // Include reading strategy preSets. }; const site = await CoreSites.instance.getSite(options.siteId); const isGetDiscussionPostsAvailable = this.isGetDiscussionPostsAvailable(site); const response = isGetDiscussionPostsAvailable ? await site.read('mod_forum_get_discussion_posts', params, preSets) : await site.read( 'mod_forum_get_forum_discussion_posts', params, preSets, ); if (!response) { throw new Error('Could not get forum posts'); } if (isGetDiscussionPostsAvailable) { response.posts = translateTagsFormatToLegacy((response as AddonModForumGetDiscussionPostsWSResponse).posts); } else { response.posts = translateLegacyPostsFormat((response as AddonModForumGetForumDiscussionPostsWSResponse).posts); } this.storeUserData(response.posts); return response as AddonModForumGetDiscussionPostsWSResponse; } /** * Sort forum discussion posts by an specified field. * * @param posts Discussion posts to be sorted in place. * @param direction Direction of the sorting (ASC / DESC). */ sortDiscussionPosts(posts: AddonModForumPost[], direction: string): void { // @todo: Check children when sorting. posts.sort((a, b) => { const timeCreatedA = Number(a.timecreated) || 0; const timeCreatedB = Number(b.timecreated) || 0; if (timeCreatedA == 0 || timeCreatedB == 0) { // Leave 0 at the end. return timeCreatedB - timeCreatedA; } if (direction == 'ASC') { return timeCreatedA - timeCreatedB; } else { return timeCreatedB - timeCreatedA; } }); } /** * Return whether discussion lists can be sorted. * * @param site Site. If not defined, current site. * @return True if discussion lists can be sorted. */ isDiscussionListSortingAvailable(site?: CoreSite): boolean { site = site || CoreSites.instance.getCurrentSite(); return !!site?.isVersionGreaterEqualThan('3.7'); } /** * Return the list of available sort orders. * * @return List of sort orders. */ getAvailableSortOrders(): AddonModForumSortOrder[] { const sortOrders = [ { label: 'addon.mod_forum.discussionlistsortbylastpostdesc', value: AddonModForumProvider.SORTORDER_LASTPOST_DESC, }, ]; if (this.isDiscussionListSortingAvailable()) { sortOrders.push( { label: 'addon.mod_forum.discussionlistsortbylastpostasc', value: AddonModForumProvider.SORTORDER_LASTPOST_ASC, }, { label: 'addon.mod_forum.discussionlistsortbycreateddesc', value: AddonModForumProvider.SORTORDER_CREATED_DESC, }, { label: 'addon.mod_forum.discussionlistsortbycreatedasc', value: AddonModForumProvider.SORTORDER_CREATED_ASC, }, { label: 'addon.mod_forum.discussionlistsortbyrepliesdesc', value: AddonModForumProvider.SORTORDER_REPLIES_DESC, }, { label: 'addon.mod_forum.discussionlistsortbyrepliesasc', value: AddonModForumProvider.SORTORDER_REPLIES_ASC, }, ); } return sortOrders; } /** * Get forum discussions. * * @param forumId Forum ID. * @param options Other options. * @return Promise resolved with an object with: * - discussions: List of discussions. Note that for every discussion in the list discussion.id is the main post ID but * discussion ID is discussion.discussion. * - canLoadMore: True if there may be more discussions to load. */ async getDiscussions( forumId: number, options: AddonModForumGetDiscussionsOptions = {}, ): Promise<{ discussions: AddonModForumDiscussion[]; canLoadMore: boolean }> { options.sortOrder = options.sortOrder || AddonModForumProvider.SORTORDER_LASTPOST_DESC; options.page = options.page || 0; const site = await CoreSites.instance.getSite(options.siteId); let method = 'mod_forum_get_forum_discussions_paginated'; const params: AddonModForumGetForumDiscussionsPaginatedWSParams | AddonModForumGetForumDiscussionsWSParams = { forumid: forumId, page: options.page, perpage: AddonModForumProvider.DISCUSSIONS_PER_PAGE, }; if (site.wsAvailable('mod_forum_get_forum_discussions')) { // Since Moodle 3.7. method = 'mod_forum_get_forum_discussions'; (params as AddonModForumGetForumDiscussionsWSParams).sortorder = options.sortOrder; } else { if (options.sortOrder !== AddonModForumProvider.SORTORDER_LASTPOST_DESC) { throw new Error('Sorting not supported with the old WS method.'); } (params as AddonModForumGetForumDiscussionsPaginatedWSParams).sortby = 'timemodified'; (params as AddonModForumGetForumDiscussionsPaginatedWSParams).sortdirection = 'DESC'; } const preSets = { cacheKey: this.getDiscussionsListCacheKey(forumId, options.sortOrder), component: AddonModForumProvider.COMPONENT, componentId: options.cmId, ...CoreSites.instance.getReadingStrategyPreSets(options.readingStrategy), // Include reading strategy preSets. }; let response: AddonModForumGetForumDiscussionsPaginatedWSResponse | AddonModForumGetForumDiscussionsWSResponse; try { // eslint-disable-next-line max-len response = await site.read( method, params, preSets, ); } catch (error) { // Try to get the data from cache stored with the old WS method. if ( CoreApp.instance.isOnline() || method !== 'mod_forum_get_forum_discussions' || options.sortOrder !== AddonModForumProvider.SORTORDER_LASTPOST_DESC ) { throw error; } const params: AddonModForumGetForumDiscussionsPaginatedWSParams = { forumid: forumId, page: options.page, perpage: AddonModForumProvider.DISCUSSIONS_PER_PAGE, sortby: 'timemodified', sortdirection: 'DESC', }; Object.assign(preSets, CoreSites.instance.getReadingStrategyPreSets(CoreSitesReadingStrategy.PreferCache)); response = await site.read( 'mod_forum_get_forum_discussions_paginated', params, preSets, ); } if (!response) { throw new Error('Could not get discussions'); } await this.storeUserData(response.discussions); return { discussions: response.discussions, canLoadMore: response.discussions.length >= AddonModForumProvider.DISCUSSIONS_PER_PAGE, }; } /** * Get forum discussions in several pages. * If a page fails, the discussions until that page will be returned along with a flag indicating an error occurred. * * @param forumId Forum ID. * @param cmId Forum cmid. * @param sortOrder Sort order. * @param forceCache True to always get the value from cache, false otherwise. * @param numPages Number of pages to get. If not defined, all pages. * @param startPage Page to start. If not defined, first page. * @param siteId Site ID. If not defined, current site. * @return Promise resolved with an object with: * - discussions: List of discussions. * - error: True if an error occurred, false otherwise. */ async getDiscussionsInPages( forumId: number, options: AddonModForumGetDiscussionsInPagesOptions = {}, ): Promise<{ discussions: AddonModForumDiscussion[]; error: boolean }> { options.page = options.page || 0; const result = { discussions: [] as AddonModForumDiscussion[], error: false, }; let numPages = typeof options.numPages == 'undefined' ? -1 : options.numPages; if (!numPages) { return Promise.resolve(result); } const getPage = (page: number): Promise<{ discussions: AddonModForumDiscussion[]; error: boolean }> => // Get page discussions. this.getDiscussions(forumId, options).then((response) => { result.discussions = result.discussions.concat(response.discussions); numPages--; if (response.canLoadMore && numPages !== 0) { return getPage(page + 1); // Get next page. } else { return result; } }).catch(() => { // Error getting a page. result.error = true; return result; }) ; return getPage(options.page); } /** * Invalidates can add discussion WS calls. * * @param forumId Forum ID. * @param siteId Site ID. If not defined, current site. * @return Promise resolved when the data is invalidated. */ async invalidateCanAddDiscussion(forumId: number, siteId?: string): Promise { const site = await CoreSites.instance.getSite(siteId); await site.invalidateWsCacheForKeyStartingWith(this.getCommonCanAddDiscussionCacheKey(forumId)); } /** * Invalidate the prefetched content except files. * To invalidate files, use AddonModForum#invalidateFiles. * * @param moduleId The module ID. * @param courseId Course ID. * @return Promise resolved when data is invalidated. */ async invalidateContent(moduleId: number, courseId: number): Promise { // Get the forum first, we need the forum ID. const forum = await this.getForum(courseId, moduleId); const promises: Promise[] = []; promises.push(this.invalidateForumData(courseId)); promises.push(this.invalidateDiscussionsList(forum.id)); promises.push(this.invalidateCanAddDiscussion(forum.id)); promises.push(this.invalidateAccessInformation(forum.id)); this.getAvailableSortOrders().forEach((sortOrder) => { // We need to get the list of discussions to be able to invalidate their posts. promises.push( this .getDiscussionsInPages(forum.id, { cmId: forum.cmid, sortOrder: sortOrder.value, readingStrategy: CoreSitesReadingStrategy.PreferCache, }) .then((response) => { // Now invalidate the WS calls. const promises: Promise[] = []; response.discussions.forEach((discussion) => { promises.push(this.invalidateDiscussionPosts(discussion.discussion, forum.id)); }); return CoreUtils.instance.allPromises(promises); }), ); }); if (this.isDiscussionListSortingAvailable()) { promises.push(CoreUser.instance.invalidateUserPreference(AddonModForumProvider.PREFERENCE_SORTORDER)); } return CoreUtils.instance.allPromises(promises); } /** * Invalidates access information. * * @param forumId Forum ID. * @param siteId Site ID. If not defined, current site. * @return Promise resolved when the data is invalidated. */ async invalidateAccessInformation(forumId: number, siteId?: string): Promise { const site = await CoreSites.instance.getSite(siteId); await site.invalidateWsCacheForKey(this.getAccessInformationCacheKey(forumId)); } /** * Invalidates forum discussion posts. * * @param discussionId Discussion ID. * @param forumId Forum ID. If not set, we can't invalidate individual post information. * @param siteId Site ID. If not defined, current site. * @return Promise resolved when the data is invalidated. */ async invalidateDiscussionPosts(discussionId: number, forumId?: number, siteId?: string): Promise { const site = await CoreSites.instance.getSite(siteId); const promises = [site.invalidateWsCacheForKey(this.getDiscussionPostsCacheKey(discussionId))]; if (forumId) { promises.push(site.invalidateWsCacheForKeyStartingWith(this.getForumDiscussionDataCacheKey(forumId, discussionId))); } await CoreUtils.instance.allPromises(promises); } /** * Invalidates discussion list. * * @param forumId Forum ID. * @param siteId Site ID. If not defined, current site. * @return Promise resolved when the data is invalidated. */ async invalidateDiscussionsList(forumId: number, siteId?: string): Promise { const site = await CoreSites.instance.getSite(siteId); await CoreUtils.instance.allPromises( this.getAvailableSortOrders() .map(sortOrder => site.invalidateWsCacheForKey(this.getDiscussionsListCacheKey(forumId, sortOrder.value))), ); } /** * Invalidate the prefetched files. * * @param moduleId The module ID. * @return Promise resolved when the files are invalidated. */ async invalidateFiles(moduleId: number): Promise { const siteId = CoreSites.instance.getCurrentSiteId(); await CoreFilepool.instance.invalidateFilesByComponent(siteId, AddonModForumProvider.COMPONENT, moduleId); } /** * Invalidates forum data. * * @param courseId Course ID. * @return Promise resolved when the data is invalidated. */ async invalidateForumData(courseId: number): Promise { const site = CoreSites.instance.getCurrentSite(); await site?.invalidateWsCacheForKey(this.getForumDataCacheKey(courseId)); } /** * Report a forum as being viewed. * * @param id Module ID. * @param name Name of the forum. * @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 = { forumid: id, }; return CoreCourseLogHelper.instance.logSingle( 'mod_forum_view_forum', params, AddonModForumProvider.COMPONENT, id, name, 'forum', {}, siteId, ); } /** * Report a forum discussion as being viewed. * * @param id Discussion ID. * @param forumId Forum ID. * @param name Name of the forum. * @param siteId Site ID. If not defined, current site. * @return Promise resolved when the WS call is successful. */ logDiscussionView(id: number, forumId: number, name?: string, siteId?: string): Promise { const params = { discussionid: id, }; return CoreCourseLogHelper.instance.logSingle( 'mod_forum_view_forum_discussion', params, AddonModForumProvider.COMPONENT, forumId, name, 'forum', params, siteId, ); } /** * Reply to a certain post. * * @param postId ID of the post being replied. * @param discussionId ID of the discussion the user is replying to. * @param forumId ID of the forum the user is replying to. * @param name Forum name. * @param courseId Course ID the forum belongs to. * @param subject New post's subject. * @param message New post's message. * @param options Options (subscribe, attachments, ...). * @param siteId Site ID. If not defined, current site. * @param allowOffline True if it can be stored in offline, false otherwise. * @return Promise resolved with a boolean indicating if the test was sent online or not. */ async replyPost( postId: number, discussionId: number, forumId: number, name: string, courseId: number, subject: string, message: string, options?: AddonModForumReplyOptions, siteId?: string, allowOffline?: boolean, ): Promise { siteId = siteId || CoreSites.instance.getCurrentSiteId(); // Convenience function to store a message to be synchronized later. const storeOffline = async (): Promise => { if (!forumId) { // Not enough data to store in offline, reject. throw new Error(Translate.instant('core.networkerrormsg')); } await AddonModForumOffline.instance.replyPost( postId, discussionId, forumId, name, courseId, subject, message, options, siteId, ); return false; }; if (!CoreApp.instance.isOnline() && allowOffline) { // App is offline, store the action. return storeOffline(); } // If there's already a reply to be sent to the server, discard it first. try { await AddonModForumOffline.instance.deleteReply(postId, siteId); await this.replyPostOnline(postId, subject, message, options, siteId); return true; } catch (error) { if (allowOffline && !CoreUtils.instance.isWebServiceError(error)) { // Couldn't connect to server, store in offline. return storeOffline(); } else { // The WebService has thrown an error or offline not supported, reject. return Promise.reject(error); } } } /** * Reply to a certain post. It will fail if offline or cannot connect. * * @param postId ID of the post being replied. * @param subject New post's subject. * @param message New post's message. * @param options Options (subscribe, attachments, ...). * @param siteId Site ID. If not defined, current site. * @return Promise resolved with the created post id. */ async replyPostOnline( postId: number, subject: string, message: string, options?: AddonModForumAddDiscussionPostWSOptionsObject, siteId?: string, ): Promise { const site = await CoreSites.instance.getSite(siteId); const params: AddonModForumAddDiscussionPostWSParams = { postid: postId, subject: subject, message: message, // eslint-disable-next-line max-len options: CoreUtils.instance.objectToArrayOfObjects( options || {}, 'name', 'value', ), }; const response = await site.write('mod_forum_add_discussion_post', params); if (!response || !response.postid) { throw new Error('Post id missing from response'); } return response.postid; } /** * Lock or unlock a discussion. * * @param forumId Forum id. * @param discussionId DIscussion id. * @param locked True to lock, false to unlock. * @param siteId Site ID. If not defined, current site. * @return Promise resolved when done. * @since 3.7 */ async setLockState(forumId: number, discussionId: number, locked: boolean, siteId?: string): Promise { const site = await CoreSites.instance.getSite(siteId); const params: AddonModForumSetLockStateWSParams = { forumid: forumId, discussionid: discussionId, targetstate: locked ? 0 : 1, }; await site.write('mod_forum_set_lock_state', params); } /** * Returns whether the set pin state WS is available. * * @param site Site. If not defined, current site. * @return Whether it's available. * @since 3.7 */ isSetPinStateAvailableForSite(): boolean { return CoreSites.instance.wsAvailableInCurrentSite('mod_forum_set_pin_state'); } /** * Pin or unpin a discussion. * * @param discussionId Discussion id. * @param locked True to pin, false to unpin. * @param siteId Site ID. If not defined, current site. * @return Promise resolved when done. * @since 3.7 */ async setPinState(discussionId: number, pinned: boolean, siteId?: string): Promise { const site = await CoreSites.instance.getSite(siteId); const params: AddonModForumSetPinStateWSParams = { discussionid: discussionId, targetstate: pinned ? 1 : 0, }; await site.write('mod_forum_set_pin_state', params); } /** * Star or unstar a discussion. * * @param discussionId Discussion id. * @param starred True to star, false to unstar. * @param siteId Site ID. If not defined, current site. * @return Promise resolved when done. * @since 3.7 */ async toggleFavouriteState(discussionId: number, starred: boolean, siteId?: string): Promise { const site = await CoreSites.instance.getSite(siteId); const params: AddonModForumToggleFavouriteStateWSParams = { discussionid: discussionId, targetstate: starred ? 1 : 0 as any, }; await site.write('mod_forum_toggle_favourite_state', params); } /** * Store the users data from a discussions/posts list. * * @param list Array of posts or discussions. */ protected storeUserData(list: any[]): void { const users = {}; list.forEach((entry) => { if (entry.author) { const authorId = Number(entry.author.id); if (!isNaN(authorId) && !users[authorId]) { users[authorId] = { id: entry.author.id, fullname: entry.author.fullname, profileimageurl: entry.author.urls.profileimage, }; } } const userId = parseInt(entry.userid); if (!isNaN(userId) && !users[userId]) { users[userId] = { id: userId, fullname: entry.userfullname, profileimageurl: entry.userpictureurl, }; } const userModified = parseInt(entry.usermodified); if (!isNaN(userModified) && !users[userModified]) { users[userModified] = { id: userModified, fullname: entry.usermodifiedfullname, profileimageurl: entry.usermodifiedpictureurl, }; } }); CoreUser.instance.storeUsers(CoreUtils.instance.objectToArray(users)); } /** * Update a certain post. * * @param postId ID of the post being edited. * @param subject New post's subject. * @param message New post's message. * @param options Options (subscribe, attachments, ...). * @param siteId Site ID. If not defined, current site. * @return Promise resolved with success boolean when done. */ async updatePost( postId: number, subject: string, message: string, options?: AddonModForumUpdateDiscussionPostWSOptionsObject, siteId?: string, ): Promise { const site = await CoreSites.instance.getSite(siteId); const params: AddonModForumUpdateDiscussionPostWSParams = { postid: postId, subject: subject, message: message, // eslint-disable-next-line max-len options: CoreUtils.instance.objectToArrayOfObjects( options || {}, 'name', 'value', ), }; const response = await site.write('mod_forum_update_discussion_post', params); return response && response.status; } } export class AddonModForum extends makeSingleton(AddonModForumProvider) {} /** * Params of mod_forum_get_forums_by_courses WS. */ type AddonModForumGetForumsByCoursesWSParams = { courseids?: number[]; // Array of Course IDs. }; /** * General forum activity data. */ export type AddonModForumData = { id: number; // Forum id. course: number; // Course id. type: string; // The forum type. name: string; // Forum name. intro: string; // The forum intro. introformat: number; // Intro format (1 = HTML, 0 = MOODLE, 2 = PLAIN or 4 = MARKDOWN). introfiles?: CoreWSExternalFile[]; duedate?: number; // Duedate for the user. cutoffdate?: number; // Cutoffdate for the user. assessed: number; // Aggregate type. assesstimestart: number; // Assess start time. assesstimefinish: number; // Assess finish time. scale: number; // Scale. // eslint-disable-next-line @typescript-eslint/naming-convention grade_forum: number; // Whole forum grade. // eslint-disable-next-line @typescript-eslint/naming-convention grade_forum_notify: number; // Whether to send notifications to students upon grading by default. maxbytes: number; // Maximum attachment size. maxattachments: number; // Maximum number of attachments. forcesubscribe: number; // Force users to subscribe. trackingtype: number; // Subscription mode. rsstype: number; // RSS feed for this activity. rssarticles: number; // Number of RSS recent articles. timemodified: number; // Time modified. warnafter: number; // Post threshold for warning. blockafter: number; // Post threshold for blocking. blockperiod: number; // Time period for blocking. completiondiscussions: number; // Student must create discussions. completionreplies: number; // Student must post replies. completionposts: number; // Student must post discussions or replies. cmid: number; // Course module id. numdiscussions?: number; // Number of discussions in the forum. cancreatediscussions?: boolean; // If the user can create discussions. lockdiscussionafter?: number; // After what period a discussion is locked. istracked?: boolean; // If the user is tracking the forum. unreadpostscount?: number; // The number of unread posts for tracked forums. }; /** * Forum discussion. */ export type AddonModForumDiscussion = { id: number; // Post id. name: string; // Discussion name. groupid: number; // Group id. timemodified: number; // Time modified. usermodified: number; // The id of the user who last modified. timestart: number; // Time discussion can start. timeend: number; // Time discussion ends. discussion: number; // Discussion id. parent: number; // Parent id. userid: number; // User who started the discussion id. created: number; // Creation time. modified: number; // Time modified. mailed: number; // Mailed?. subject: string; // The post subject. message: string; // The post message. messageformat: number; // Message format (1 = HTML, 0 = MOODLE, 2 = PLAIN or 4 = MARKDOWN). messagetrust: number; // Can we trust?. messageinlinefiles?: CoreWSExternalFile[]; attachment: string; // Has attachments?. attachments?: CoreWSExternalFile[]; totalscore: number; // The post message total score. mailnow: number; // Mail now?. userfullname: string; // Post author full name. usermodifiedfullname: string; // Post modifier full name. userpictureurl: string; // Post author picture. usermodifiedpictureurl: string; // Post modifier picture. numreplies: number; // The number of replies in the discussion. numunread: number; // The number of unread discussions. pinned: boolean; // Is the discussion pinned. locked: boolean; // Is the discussion locked. starred?: boolean; // Is the discussion starred. canreply: boolean; // Can the user reply to the discussion. canlock: boolean; // Can the user lock the discussion. canfavourite?: boolean; // Can the user star the discussion. }; /** * Legacy forum post data. */ export type AddonModForumLegacyPost = { id: number; // Post id. discussion: number; // Discussion id. parent: number; // Parent id. userid: number; // User id. created: number; // Creation time. modified: number; // Time modified. mailed: number; // Mailed?. subject: string; // The post subject. message: string; // The post message. messageformat: number; // Message format (1 = HTML, 0 = MOODLE, 2 = PLAIN or 4 = MARKDOWN). messagetrust: number; // Can we trust?. messageinlinefiles?: CoreWSExternalFile[]; attachment: string; // Has attachments?. attachments?: CoreWSExternalFile[]; totalscore: number; // The post message total score. mailnow: number; // Mail now?. children: number[]; canreply: boolean; // The user can reply to posts?. postread: boolean; // The post was read. userfullname: string; // Post author full name. userpictureurl?: string; // Post author picture. deleted: boolean; // This post has been removed. isprivatereply: boolean; // The post is a private reply. tags?: { // Tags. id: number; // Tag id. name: string; // Tag name. rawname: string; // The raw, unnormalised name for the tag as entered by users. isstandard: boolean; // Whether this tag is standard. tagcollid: number; // Tag collection id. taginstanceid: number; // Tag instance id. taginstancecontextid: number; // Context the tag instance belongs to. itemid: number; // Id of the record tagged. ordering: number; // Tag ordering. flag: number; // Whether the tag is flagged as inappropriate. }[]; }; /** * Forum post data. */ export type AddonModForumPost = { id: number; // Id. subject: string; // Subject. replysubject: string; // Replysubject. message: string; // Message. messageformat: number; // Message format (1 = HTML, 0 = MOODLE, 2 = PLAIN or 4 = MARKDOWN). author: { id?: number; // Id. fullname?: string; // Fullname. isdeleted?: boolean; // Isdeleted. groups?: { // Groups. id: number; // Id. name: string; // Name. urls: { image?: string; // Image. }; }[]; urls: { profile?: string; // The URL for the use profile page. profileimage?: string; // The URL for the use profile image. }; }; discussionid: number; // Discussionid. hasparent: boolean; // Hasparent. parentid?: number; // Parentid. timecreated: number; // Timecreated. unread?: boolean; // Unread. isdeleted: boolean; // Isdeleted. isprivatereply: boolean; // Isprivatereply. haswordcount: boolean; // Haswordcount. wordcount?: number; // Wordcount. charcount?: number; // Charcount. capabilities: { view: boolean; // Whether the user can view the post. edit: boolean; // Whether the user can edit the post. delete: boolean; // Whether the user can delete the post. split: boolean; // Whether the user can split the post. reply: boolean; // Whether the user can reply to the post. selfenrol: boolean; // Whether the user can self enrol into the course. export: boolean; // Whether the user can export the post. controlreadstatus: boolean; // Whether the user can control the read status of the post. canreplyprivately: boolean; // Whether the user can post a private reply. }; urls?: { view?: string; // The URL used to view the post. viewisolated?: string; // The URL used to view the post in isolation. viewparent?: string; // The URL used to view the parent of the post. edit?: string; // The URL used to edit the post. delete?: string; // The URL used to delete the post. // The URL used to split the discussion with the selected post being the first post in the new discussion. split?: string; reply?: string; // The URL used to reply to the post. export?: string; // The URL used to export the post. markasread?: string; // The URL used to mark the post as read. markasunread?: string; // The URL used to mark the post as unread. discuss?: string; // Discuss. }; attachments: { // Attachments. contextid: number; // Contextid. component: string; // Component. filearea: string; // Filearea. itemid: number; // Itemid. filepath: string; // Filepath. filename: string; // Filename. isdir: boolean; // Isdir. isimage: boolean; // Isimage. timemodified: number; // Timemodified. timecreated: number; // Timecreated. filesize: number; // Filesize. author: string; // Author. license: string; // License. filenameshort: string; // Filenameshort. filesizeformatted: string; // Filesizeformatted. icon: string; // Icon. timecreatedformatted: string; // Timecreatedformatted. timemodifiedformatted: string; // Timemodifiedformatted. url: string; // Url. urls: { export?: string; // The URL used to export the attachment. }; html: { plagiarism?: string; // The HTML source for the Plagiarism Response. }; }[]; tags?: { // Tags. id: number; // The ID of the Tag. tagid: number; // The tagid. isstandard: boolean; // Whether this is a standard tag. displayname: string; // The display name of the tag. flag: boolean; // Wehther this tag is flagged. urls: { view: string; // The URL to view the tag. }; }[]; html?: { rating?: string; // The HTML source to rate the post. taglist?: string; // The HTML source to view the list of tags. authorsubheading?: string; // The HTML source to view the author details. }; }; /** * Forum rating info. */ export type AddonModForumRatingInfo = { contextid: number; // Context id. component: string; // Context name. ratingarea: string; // Rating area name. canviewall?: boolean; // Whether the user can view all the individual ratings. canviewany?: boolean; // Whether the user can view aggregate of ratings of others. scales?: { // Different scales used information. id: number; // Scale id. courseid?: number; // Course id. name?: string; // Scale name (when a real scale is used). max: number; // Max value for the scale. isnumeric: boolean; // Whether is a numeric scale. items?: { // Scale items. Only returned for not numerical scales. value: number; // Scale value/option id. name: string; // Scale name. }[]; }[]; ratings?: { // The ratings. itemid: number; // Item id. scaleid?: number; // Scale id. userid?: number; // User who rated id. aggregate?: number; // Aggregated ratings grade. aggregatestr?: string; // Aggregated ratings as string. aggregatelabel?: string; // The aggregation label. count?: number; // Ratings count (used when aggregating). rating?: number; // The rating the user gave. canrate?: boolean; // Whether the user can rate the item. canviewaggregate?: boolean; // Whether the user can view the aggregated grade. }[]; }; /** * Options to pass to get discussions. */ export type AddonModForumGetDiscussionsOptions = CoreCourseCommonModWSOptions & { sortOrder?: number; // Sort order. page?: number; // Page. Defaults to 0. }; /** * Options to pass to get discussions in pages. */ export type AddonModForumGetDiscussionsInPagesOptions = AddonModForumGetDiscussionsOptions & { numPages?: number; // Number of pages to get. If not defined, all pages. }; /** * Forum access information. */ export type AddonModForumAccessInformation = { canaddinstance?: boolean; // Whether the user has the capability mod/forum:addinstance allowed. canviewdiscussion?: boolean; // Whether the user has the capability mod/forum:viewdiscussion allowed. canviewhiddentimedposts?: boolean; // Whether the user has the capability mod/forum:viewhiddentimedposts allowed. canstartdiscussion?: boolean; // Whether the user has the capability mod/forum:startdiscussion allowed. canreplypost?: boolean; // Whether the user has the capability mod/forum:replypost allowed. canaddnews?: boolean; // Whether the user has the capability mod/forum:addnews allowed. canreplynews?: boolean; // Whether the user has the capability mod/forum:replynews allowed. canviewrating?: boolean; // Whether the user has the capability mod/forum:viewrating allowed. canviewanyrating?: boolean; // Whether the user has the capability mod/forum:viewanyrating allowed. canviewallratings?: boolean; // Whether the user has the capability mod/forum:viewallratings allowed. canrate?: boolean; // Whether the user has the capability mod/forum:rate allowed. canpostprivatereply?: boolean; // Whether the user has the capability mod/forum:postprivatereply allowed. canreadprivatereplies?: boolean; // Whether the user has the capability mod/forum:readprivatereplies allowed. cancreateattachment?: boolean; // Whether the user has the capability mod/forum:createattachment allowed. candeleteownpost?: boolean; // Whether the user has the capability mod/forum:deleteownpost allowed. candeleteanypost?: boolean; // Whether the user has the capability mod/forum:deleteanypost allowed. cansplitdiscussions?: boolean; // Whether the user has the capability mod/forum:splitdiscussions allowed. canmovediscussions?: boolean; // Whether the user has the capability mod/forum:movediscussions allowed. canpindiscussions?: boolean; // Whether the user has the capability mod/forum:pindiscussions allowed. caneditanypost?: boolean; // Whether the user has the capability mod/forum:editanypost allowed. canviewqandawithoutposting?: boolean; // Whether the user has the capability mod/forum:viewqandawithoutposting allowed. canviewsubscribers?: boolean; // Whether the user has the capability mod/forum:viewsubscribers allowed. canmanagesubscriptions?: boolean; // Whether the user has the capability mod/forum:managesubscriptions allowed. canpostwithoutthrottling?: boolean; // Whether the user has the capability mod/forum:postwithoutthrottling allowed. canexportdiscussion?: boolean; // Whether the user has the capability mod/forum:exportdiscussion allowed. canexportforum?: boolean; // Whether the user has the capability mod/forum:exportforum allowed. canexportpost?: boolean; // Whether the user has the capability mod/forum:exportpost allowed. canexportownpost?: boolean; // Whether the user has the capability mod/forum:exportownpost allowed. canaddquestion?: boolean; // Whether the user has the capability mod/forum:addquestion allowed. canallowforcesubscribe?: boolean; // Whether the user has the capability mod/forum:allowforcesubscribe allowed. cancanposttomygroups?: boolean; // Whether the user has the capability mod/forum:canposttomygroups allowed. cancanoverridediscussionlock?: boolean; // Whether the user has the capability mod/forum:canoverridediscussionlock allowed. cancanoverridecutoff?: boolean; // Whether the user has the capability mod/forum:canoverridecutoff allowed. cancantogglefavourite?: boolean; // Whether the user has the capability mod/forum:cantogglefavourite allowed. cangrade?: boolean; // Whether the user has the capability mod/forum:grade allowed. }; /** * Can add discussion info. */ export type AddonModForumCanAddDiscussion = { status: boolean; // True if the user can add discussions, false otherwise. canpindiscussions?: boolean; // True if the user can pin discussions, false otherwise. cancreateattachment?: boolean; // True if the user can add attachments, false otherwise. }; /** * Sorting order. */ export type AddonModForumSortOrder = { label: string; value: number; }; /** * Params of mod_forum_get_forum_discussions WS. */ export type AddonModForumGetForumDiscussionsWSParams = { forumid: number; // Forum instance id. sortorder?: number; // Sort by this element: numreplies, , created or timemodified. page?: number; // Current page. perpage?: number; // Items per page. groupid?: number; // Group id. }; /** * Data returned by mod_forum_get_forum_discussions WS. */ export type AddonModForumGetForumDiscussionsWSResponse = { discussions: AddonModForumDiscussion[]; warnings?: CoreWSExternalWarning[]; }; /** * Params of mod_forum_get_forum_discussions_paginated WS. */ export type AddonModForumGetForumDiscussionsPaginatedWSParams = { forumid: number; // Forum instance id. sortby?: string; // Sort by this element: id, timemodified, timestart or timeend. sortdirection?: string; // Sort direction: ASC or DESC. page?: number; // Current page. perpage?: number; // Items per page. }; /** * Data returned by mod_forum_get_forum_discussions_paginated WS. */ export type AddonModForumGetForumDiscussionsPaginatedWSResponse = { discussions: AddonModForumDiscussion[]; warnings?: CoreWSExternalWarning[]; }; /** * Data returned by mod_forum_get_forums_by_courses WS. */ export type AddonModForumGetForumsByCoursesWSResponse = AddonModForumData[]; /** * Array options of mod_forum_add_discussion WS. */ export type AddonModForumAddDiscussionWSOptionsArray = { // Option name. name: 'discussionsubscribe' | 'discussionpinned' | 'inlineattachmentsid' | 'attachmentsid'; // Option value. // This param is validated in the external function, expected values are: // discussionsubscribe (bool) - subscribe to the discussion?, default to true // discussionpinned (bool) - is the discussion pinned, default to false // inlineattachmentsid (int) - the draft file area id for inline attachments // attachmentsid (int) - the draft file area id for attachments. value: string; }[]; /** * Object options of mod_forum_add_discussion WS. */ export type AddonModForumAddDiscussionWSOptionsObject = { discussionsubscribe?: string; discussionpinned?: string; inlineattachmentsid?: string; attachmentsid?: string; }; /** * Array options of mod_forum_add_discussion_post WS. */ export type AddonModForumAddDiscussionPostWSOptionsArray = { // Option name. name: 'discussionsubscribe' | 'private' | 'inlineattachmentsid' | 'attachmentsid' | 'topreferredformat'; // Option value. // This param is validated in the external function, expected values are: // discussionsubscribe (bool) - subscribe to the discussion?, default to true // private (bool) - make this reply private to the author of the parent post, default to false. // inlineattachmentsid (int) - the draft file area id for inline attachments // attachmentsid (int) - the draft file area id for attachments // topreferredformat (bool) - convert the message & messageformat to FORMAT_HTML, defaults to false. value: string; }[]; /** * Object options of mod_forum_add_discussion_post WS. */ export type AddonModForumAddDiscussionPostWSOptionsObject = { discussionsubscribe?: boolean; private?: boolean; inlineattachmentsid?: number; attachmentsid?: number; topreferredformat?: boolean; }; /** * Array options of mod_forum_update_discussion_post WS. */ export type AddonModForumUpdateDiscussionPostWSOptionsArray = { // Option name. name: 'pinned' | 'discussionsubscribe' | 'inlineattachmentsid' | 'attachmentsid'; // Option value. // This param is validated in the external function, expected values are: // pinned (bool) - (only for discussions) whether to pin this discussion or not // discussionsubscribe (bool) - whether to subscribe to the post or not // inlineattachmentsid (int) - the draft file area id for inline attachments in the text // attachmentsid (int) - the draft file area id for attachments. value: string; // The value of the option. }[]; /** * Object options of mod_forum_update_discussion_post WS. */ export type AddonModForumUpdateDiscussionPostWSOptionsObject = { pinned?: boolean; discussionsubscribe?: boolean; inlineattachmentsid?: number; attachmentsid?: number; }; /** * Params of mod_forum_add_discussion WS. */ export type AddonModForumAddDiscussionWSParams = { forumid: number; // Forum instance ID. subject: string; // New Discussion subject. message: string; // New Discussion message (only html format allowed). groupid?: number; // The group, default to 0. options?: AddonModForumAddDiscussionWSOptionsArray; }; /** * Data returned by mod_forum_add_discussion WS. */ export type AddonModForumAddDiscussionWSResponse = { discussionid: number; // New Discussion ID. warnings?: CoreWSExternalWarning[]; }; /** * Params of mod_forum_add_discussion_post WS. */ export type AddonModForumAddDiscussionPostWSParams = { postid: number; // The post id we are going to reply to (can be the initial discussion post). subject: string; // New post subject. message: string; // New post message (html assumed if messageformat is not provided). options?: AddonModForumAddDiscussionPostWSOptionsArray; messageformat?: number; // Message format (1 = HTML, 0 = MOODLE, 2 = PLAIN or 4 = MARKDOWN). }; /** * Data returned by mod_forum_add_discussion_post WS. */ export type AddonModForumAddDiscussionPostWSResponse = { postid: number; // New post id. warnings?: CoreWSExternalWarning[]; post: AddonModForumPost; messages?: { // List of warnings. type: string; // The classification to be used in the client side. message: string; // Untranslated english message to explain the warning. }[]; }; /** * Params of mod_forum_get_forum_access_information WS. */ export type AddonModForumGetForumAccessInformationWSParams = { forumid: number; // Forum instance id. }; /** * Data returned by mod_forum_get_forum_access_information WS. */ export type AddonModForumGetForumAccessInformationWSResponse = { warnings?: CoreWSExternalWarning[]; } & AddonModForumAccessInformation; /** * Params of mod_forum_can_add_discussion WS. */ export type AddonModForumCanAddDiscussionWSParams = { forumid: number; // Forum instance ID. groupid?: number; // The group to check, default to active group (Use -1 to check if the user can post in all the groups). }; /** * Data returned by mod_forum_can_add_discussion WS. */ export type AddonModForumCanAddDiscussionWSResponse = { warnings?: CoreWSExternalWarning[]; } & AddonModForumCanAddDiscussion; /** * Params of mod_forum_delete_post WS. */ export type AddonModForumDeletePostWSParams = { postid: number; // Post to be deleted. It can be a discussion topic post. }; /** * Data returned by mod_forum_delete_post WS. */ export type AddonModForumDeletePostWSResponse = CoreStatusWithWarningsWSResponse; /** * Params of mod_forum_get_discussion_post WS. */ export type AddonModForumGetDiscussionPostWSParams = { postid: number; // Post to fetch. }; /** * Data returned by mod_forum_get_discussion_post WS. */ export type AddonModForumGetDiscussionPostWSResponse = { post: AddonModForumPost; warnings?: CoreWSExternalWarning[]; }; /** * Params of mod_forum_get_discussion_posts WS. */ export type AddonModForumGetDiscussionPostsWSParams = { discussionid: number; // The ID of the discussion from which to fetch posts. sortby?: string; // Sort by this element: id, created or modified. sortdirection?: string; // Sort direction: ASC or DESC. }; /** * Data returned by mod_forum_get_discussion_posts WS. */ export type AddonModForumGetDiscussionPostsWSResponse = { posts: AddonModForumPost[]; forumid: number; // The forum id. courseid: number; // The forum course id. ratinginfo?: AddonModForumRatingInfo; // Rating information. warnings?: CoreWSExternalWarning[]; }; /** * Params of mod_forum_get_forum_discussion_posts WS. */ export type AddonModForumGetForumDiscussionPostsWSParams = { discussionid: number; // Discussion ID. sortby?: string; // Sort by this element: id, created or modified. sortdirection?: string; // Sort direction: ASC or DESC. }; /** * Data returned by mod_forum_get_forum_discussion_posts WS. */ export type AddonModForumGetForumDiscussionPostsWSResponse = { posts: AddonModForumLegacyPost[]; ratinginfo?: AddonModForumRatingInfo; // Rating information. warnings?: CoreWSExternalWarning[]; }; /** * Params of mod_forum_set_lock_state WS. */ export type AddonModForumSetLockStateWSParams = { forumid: number; // Forum that the discussion is in. discussionid: number; // The discussion to lock / unlock. targetstate: number; // The timestamp for the lock state. }; /** * Data returned by mod_forum_set_lock_state WS. */ export type AddonModForumSetLockStateWSResponse = { id: number; // The discussion we are locking. locked: boolean; // The locked state of the discussion. times: { locked: number; // The locked time of the discussion. }; }; /** * Params of mod_forum_set_pin_state WS. */ export type AddonModForumSetPinStateWSParams = { discussionid: number; // The discussion to pin or unpin. targetstate: number; // The target state. }; /** * Data returned by mod_forum_set_pin_state WS. */ export type AddonModForumSetPinStateWSResponse = { id: number; // Id. forumid: number; // Forumid. pinned: boolean; // Pinned. locked: boolean; // Locked. istimelocked: boolean; // Istimelocked. name: string; // Name. firstpostid: number; // Firstpostid. group?: { name: string; // Name. urls: { picture?: string; // Picture. userlist?: string; // Userlist. }; }; times: { modified: number; // Modified. start: number; // Start. end: number; // End. locked: number; // Locked. }; userstate: { subscribed: boolean; // Subscribed. favourited: boolean; // Favourited. }; capabilities: { subscribe: boolean; // Subscribe. move: boolean; // Move. pin: boolean; // Pin. post: boolean; // Post. manage: boolean; // Manage. favourite: boolean; // Favourite. }; urls: { view: string; // View. viewlatest?: string; // Viewlatest. viewfirstunread?: string; // Viewfirstunread. markasread: string; // Markasread. subscribe: string; // Subscribe. pin?: string; // Pin. }; timed: { istimed?: boolean; // Istimed. visible?: boolean; // Visible. }; }; /** * Params of mod_forum_toggle_favourite_state WS. */ export type AddonModForumToggleFavouriteStateWSParams = { discussionid: number; // The discussion to subscribe or unsubscribe. targetstate: boolean; // The target state. }; /** * Data returned by mod_forum_toggle_favourite_state WS. */ export type AddonModForumToggleFavouriteStateWSResponse = { id: number; // Id. forumid: number; // Forumid. pinned: boolean; // Pinned. locked: boolean; // Locked. istimelocked: boolean; // Istimelocked. name: string; // Name. firstpostid: number; // Firstpostid. group?: { name: string; // Name. urls: { picture?: string; // Picture. userlist?: string; // Userlist. }; }; times: { modified: number; // Modified. start: number; // Start. end: number; // End. locked: number; // Locked. }; userstate: { subscribed: boolean; // Subscribed. favourited: boolean; // Favourited. }; capabilities: { subscribe: boolean; // Subscribe. move: boolean; // Move. pin: boolean; // Pin. post: boolean; // Post. manage: boolean; // Manage. favourite: boolean; // Favourite. }; urls: { view: string; // View. viewlatest?: string; // Viewlatest. viewfirstunread?: string; // Viewfirstunread. markasread: string; // Markasread. subscribe: string; // Subscribe. pin?: string; // Pin. }; timed: { istimed?: boolean; // Istimed. visible?: boolean; // Visible. }; }; /** * Params of mod_forum_update_discussion_post WS. */ export type AddonModForumUpdateDiscussionPostWSParams = { postid: number; // Post to be updated. It can be a discussion topic post. subject?: string; // Updated post subject. message?: string; // Updated post message (HTML assumed if messageformat is not provided). messageformat?: number; // Message format (1 = HTML, 0 = MOODLE, 2 = PLAIN or 4 = MARKDOWN). options?: AddonModForumUpdateDiscussionPostWSOptionsArray; // Configuration options for the post. }; /** * Data returned by mod_forum_update_discussion_post WS. */ export type AddonModForumUpdateDiscussionPostWSResponse = CoreStatusWithWarningsWSResponse;