// (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 { ILocalNotification } from '@awesome-cordova-plugins/local-notifications'; import { NotificationEventResponse, PushOptions, RegistrationEventResponse } from '@awesome-cordova-plugins/push/ngx'; import { CoreApp } from '@services/app'; import { CoreSites } from '@services/sites'; import { CorePushNotificationsDelegate } from './push-delegate'; import { CoreLocalNotifications } from '@services/local-notifications'; import { CoreUtils } from '@services/utils/utils'; import { CoreTextUtils } from '@services/utils/text'; import { CoreConfig } from '@services/config'; import { CoreConstants } from '@/core/constants'; import { CoreSite } from '@classes/sites/site'; import { makeSingleton, Badge, Device, Translate, ApplicationInit, NgZone } from '@singletons'; import { CoreLogger } from '@singletons/logger'; import { CoreEvents } from '@singletons/events'; import { APP_SCHEMA, BADGE_TABLE_NAME, PENDING_UNREGISTER_TABLE_NAME, REGISTERED_DEVICES_TABLE_NAME, CorePushNotificationsPendingUnregisterDBRecord, CorePushNotificationsRegisteredDeviceDBRecord, CorePushNotificationsBadgeDBRecord, } from './database/pushnotifications'; import { CoreError } from '@classes/errors/error'; import { CoreWSExternalWarning } from '@services/ws'; import { CoreSitesFactory } from '@services/sites-factory'; import { CoreMainMenuProvider } from '@features/mainmenu/services/mainmenu'; import { AsyncInstance, asyncInstance } from '@/core/utils/async-instance'; import { CoreDatabaseTable } from '@classes/database/database-table'; import { CoreDatabaseCachingStrategy, CoreDatabaseTableProxy } from '@classes/database/database-table-proxy'; import { CoreObject } from '@singletons/object'; import { lazyMap, LazyMap } from '@/core/utils/lazy-map'; import { CorePlatform } from '@services/platform'; import { CoreAnalytics, CoreAnalyticsEventType } from '@services/analytics'; import { CoreSiteInfo } from '@classes/sites/unauthenticated-site'; import { Push } from '@features/native/plugins'; /** * Service to handle push notifications. */ @Injectable({ providedIn: 'root' }) export class CorePushNotificationsProvider { static readonly COMPONENT = 'CorePushNotificationsProvider'; protected logger: CoreLogger; protected pushID?: string; protected badgesTable = asyncInstance>(); protected pendingUnregistersTable = asyncInstance>(); protected registeredDevicesTables: LazyMap>>; constructor() { this.logger = CoreLogger.getInstance('CorePushNotificationsProvider'); this.registeredDevicesTables = lazyMap( siteId => asyncInstance( () => CoreSites.getSiteTable( REGISTERED_DEVICES_TABLE_NAME, { siteId, config: { cachingStrategy: CoreDatabaseCachingStrategy.None }, primaryKeyColumns: ['appid', 'uuid'], onDestroy: () => delete this.registeredDevicesTables[siteId], }, ), ), ); } /** * Initialize the service. * * @returns Promise resolved when done. */ async initialize(): Promise { await Promise.all([ this.initializeDatabase(), this.initializeDefaultChannel(), ]); // Now register the device to receive push notifications. Don't block for this. this.registerDevice(); CoreEvents.on(CoreEvents.NOTIFICATION_SOUND_CHANGED, () => { // Notification sound has changed, register the device again to update the sound setting. this.registerDevice(); }); // Register device on Moodle site when login. CoreEvents.on(CoreEvents.LOGIN, async () => { if (!this.canRegisterOnMoodle()) { return; } try { await this.registerDeviceOnMoodle(); } catch (error) { this.logger.error('Can\'t register device', error); } }); CoreEvents.on(CoreEvents.SITE_DELETED, async (site) => { try { await Promise.all([ this.unregisterDeviceOnMoodle(site), this.cleanSiteCounters(site.getId()), ]); } catch (error) { this.logger.warn('Can\'t unregister device', error); } }); CoreEvents.on(CoreMainMenuProvider.MAIN_MENU_HANDLER_BADGE_UPDATED, (data) => { this.updateAddonCounter(data.handler, data.value, data.siteId); }); // Listen for local notification clicks (generated by the app). CoreLocalNotifications.registerClick( CorePushNotificationsProvider.COMPONENT, (notification) => { CoreAnalytics.logEvent({ eventName: 'moodle_notification_open', type: CoreAnalyticsEventType.PUSH_NOTIFICATION, data: notification, }); this.notificationClicked(notification); }, ); // Listen for local notification dismissed events. CoreLocalNotifications.registerObserver( 'clear', CorePushNotificationsProvider.COMPONENT, (notification) => { CoreAnalytics.logEvent({ eventName: 'moodle_notification_dismiss', type: CoreAnalyticsEventType.PUSH_NOTIFICATION, data: notification, }); }, ); } /** * Initialize the default channel for Android. * * @returns Promise resolved when done. */ protected async initializeDefaultChannel(): Promise { await CorePlatform.ready(); // Create the default channel. this.createDefaultChannel(); Translate.onLangChange.subscribe(() => { // Update the channel name. this.createDefaultChannel(); }); } /** * Initialize database. * * @returns Promise resolved when done. */ protected async initializeDatabase(): Promise { try { await CoreApp.createTablesFromSchema(APP_SCHEMA); } catch (e) { // Ignore errors. } const database = CoreApp.getDB(); const badgesTable = new CoreDatabaseTableProxy( { cachingStrategy: CoreDatabaseCachingStrategy.Eager }, database, BADGE_TABLE_NAME, ['siteid', 'addon'], ); const pendingUnregistersTable = new CoreDatabaseTableProxy( { cachingStrategy: CoreDatabaseCachingStrategy.Eager }, database, PENDING_UNREGISTER_TABLE_NAME, ['siteid'], ); await Promise.all([ badgesTable.initialize(), pendingUnregistersTable.initialize(), ]); this.badgesTable.setInstance(badgesTable); this.pendingUnregistersTable.setInstance(pendingUnregistersTable); } /** * Check whether the device can be registered in Moodle to receive push notifications. * * @returns Whether the device can be registered in Moodle. */ canRegisterOnMoodle(): boolean { return !!this.pushID && CorePlatform.isMobile(); } /** * Delete all badge records for a given site. * * @param siteId Site ID. * @returns Resolved when done. */ async cleanSiteCounters(siteId: string): Promise { try { await this.badgesTable.delete({ siteid: siteId }); } finally { this.updateAppCounter(); } } /** * Create the default push channel. It is used to change the name. * * @returns Promise resolved when done. */ protected async createDefaultChannel(): Promise { if (!CorePlatform.isAndroid()) { return; } try { await Push.createChannel({ id: 'PushPluginChannel', description: Translate.instant('core.misc'), importance: 4, }); } catch (error) { this.logger.error('Error changing push channel name', error); } } /** * Enable or disable analytics. * * @param enable Whether to enable or disable. * @returns Promise resolved when done. * @deprecated since 4.3. Use CoreAnalytics.enableAnalytics instead. */ async enableAnalytics(enable: boolean): Promise { return CoreAnalytics.enableAnalytics(enable); } /** * Returns options for push notifications based on device. * * @returns Promise with the push options resolved when done. */ protected async getOptions(): Promise { let soundEnabled = true; if (CoreLocalNotifications.canDisableSound()) { soundEnabled = await CoreConfig.get(CoreConstants.SETTINGS_NOTIFICATION_SOUND, true); } return { android: { sound: !!soundEnabled, icon: 'smallicon', iconColor: CoreConstants.CONFIG.notificoncolor, }, ios: { alert: 'true', badge: true, sound: !!soundEnabled, }, windows: { sound: !!soundEnabled, }, }; } /** * Get the pushID for this device. * * @returns Push ID. */ getPushId(): string | undefined { return this.pushID; } /** * Get required data to register the device in Moodle. * * @returns Data. */ protected getRequiredRegisterData(): CoreUserAddUserDeviceWSParams { if (!this.pushID) { throw new CoreError('Cannot get register data because pushID is not set.'); } return { appid: CoreConstants.CONFIG.app_id, name: Device.manufacturer || '', model: Device.model, platform: Device.platform + '-fcm', version: Device.version, pushid: this.pushID, uuid: Device.uuid, }; } /** * Get Sitebadge counter from the database. * * @param siteId Site ID. * @returns Promise resolved with the stored badge counter for the site. */ getSiteCounter(siteId: string): Promise { return this.getAddonBadge(siteId); } /** * Log an analytics event. * * @param eventName Name of the event. * @param data Data of the event. * @returns Promise resolved when done. This promise is never rejected. * @deprecated since 4.3. Use CoreAnalytics.logEvent instead. */ async logEvent(eventName: string, data: Record): Promise { if (eventName !== 'view_item' && eventName !== 'view_item_list') { return CoreAnalytics.logEvent({ type: CoreAnalyticsEventType.PUSH_NOTIFICATION, eventName, data, }); } const name = data.name ? String(data.name) : ''; delete data.name; return CoreAnalytics.logEvent({ type: eventName === 'view_item' ? CoreAnalyticsEventType.VIEW_ITEM : CoreAnalyticsEventType.VIEW_ITEM_LIST, ws: data.moodleaction ?? '', name, data, }); } /** * Log an analytics VIEW_ITEM_LIST event. * * @param itemId The item ID. * @param itemName The item name. * @param itemCategory The item category. * @param wsName Name of the WS. * @param data Other data to pass to the event. * @returns Promise resolved when done. This promise is never rejected. * @deprecated since 4.3. Use CoreAnalytics.logEvent instead. */ logViewEvent( itemId: number | string | undefined, itemName: string | undefined, itemCategory: string | undefined, wsName: string, data?: Record, ): Promise { data = data || {}; data.id = itemId; data.name = itemName; data.category = itemCategory; data.moodleaction = wsName; // eslint-disable-next-line deprecation/deprecation return this.logEvent('view_item', data); } /** * Log an analytics view item list event. * * @param itemCategory The item category. * @param wsName Name of the WS. * @param data Other data to pass to the event. * @returns Promise resolved when done. This promise is never rejected. * @deprecated since 4.3. Use CoreAnalytics.logEvent instead. */ logViewListEvent( itemCategory: string, wsName: string, data?: Record, ): Promise { data = data || {}; data.moodleaction = wsName; data.category = itemCategory; // eslint-disable-next-line deprecation/deprecation return this.logEvent('view_item_list', data); } /** * Function called when a push notification is clicked. Redirect the user to the right state. * * @param data Notification data. * @returns Promise resolved when done. */ async notificationClicked(data: CorePushNotificationsNotificationBasicData): Promise { await ApplicationInit.donePromise; CorePushNotificationsDelegate.clicked(data); } /** * This function is called when we receive a Notification from APNS or a message notification from GCM. * The app can be in foreground or background, * if we are in background this code is executed when we open the app clicking in the notification bar. * * @param notification Notification received. * @returns Promise resolved when done. */ async onMessageReceived(notification: NotificationEventResponse): Promise { const rawData: CorePushNotificationsNotificationBasicRawData = notification ? notification.additionalData : {}; // Parse some fields and add some extra data. const data: CorePushNotificationsNotificationBasicData = Object.assign(rawData, { title: notification.title, message: notification.message, customdata: typeof rawData.customdata == 'string' ? CoreTextUtils.parseJSON>(rawData.customdata, {}) : rawData.customdata, }); let site: CoreSite | undefined; if (data.site) { site = await CoreSites.getSite(data.site); } else if (data.siteurl) { site = await CoreSites.getSiteByUrl(data.siteurl); } data.site = site?.getId(); if (!CoreUtils.isTrueOrOne(data.foreground)) { // The notification was clicked. return this.notificationClicked(data); } const localNotif: ILocalNotification = { id: Number(data.notId) || 1, data: data, title: notification.title, text: notification.message, channel: 'PushPluginChannel', }; const isAndroid = CorePlatform.isAndroid(); const extraFeatures = CoreUtils.isTrueOrOne(data.extrafeatures); if (extraFeatures && isAndroid && CoreUtils.isFalseOrZero(data.notif)) { // It's a message, use messaging style. Ionic Native doesn't specify this option. // eslint-disable-next-line @typescript-eslint/no-explicit-any ( localNotif).text = [ { message: notification.message, person: data.sender ?? (data.conversationtype == '2' ? data.userfromfullname : ''), personIcon: data.senderImage, }, ]; } if (extraFeatures && isAndroid) { // Use a different icon if needed. localNotif.icon = notification.image; // This feature isn't supported by the official plugin, we use a fork. // eslint-disable-next-line @typescript-eslint/no-explicit-any ( localNotif).iconType = data['image-type']; localNotif.summary = data.summaryText; if (data.picture) { localNotif.attachments = [data.picture]; } } CoreLocalNotifications.schedule(localNotif, CorePushNotificationsProvider.COMPONENT, data.site || '', true); await this.notifyReceived(notification, data); } /** * Notify that a notification was received. * * @param notification Notification. * @param data Notification data. * @returns Promise resolved when done. */ protected async notifyReceived( notification: NotificationEventResponse, data: CorePushNotificationsNotificationBasicData, ): Promise { await ApplicationInit.donePromise; CorePushNotificationsDelegate.received(data); } /** * Unregisters a device from a certain Moodle site. * * @param site Site to unregister from. * @returns Promise resolved when device is unregistered. */ async unregisterDeviceOnMoodle(site: CoreSite): Promise { if (!site || !CorePlatform.isMobile()) { throw new CoreError('Cannot unregister device'); } this.logger.debug(`Unregister device on Moodle: '${site.getId()}'`); const data: CoreUserRemoveUserDeviceWSParams = { appid: CoreConstants.CONFIG.app_id, uuid: Device.uuid, }; let response: CoreUserRemoveUserDeviceWSResponse; try { response = await site.write('core_user_remove_user_device', data); } catch (error) { if (CoreUtils.isWebServiceError(error) || CoreUtils.isExpiredTokenError(error)) { // Cannot unregister. Don't try again. await CoreUtils.ignoreErrors(this.pendingUnregistersTable.delete({ token: site.getToken(), siteid: site.getId(), })); throw error; } // Store the pending unregister so it's retried again later. await this.pendingUnregistersTable.insert({ siteid: site.getId(), siteurl: site.getURL(), token: site.getToken(), info: JSON.stringify(site.getInfo()), }); return; } if (!response.removed) { throw new CoreError('Cannot unregister device'); } await CoreUtils.ignoreErrors(Promise.all([ // Remove the device from the local DB. this.registeredDevicesTables[site.getId()].delete(this.getRequiredRegisterData()), // Remove pending unregisters for this site. this.pendingUnregistersTable.deleteByPrimaryKey({ siteid: site.getId() }), ])); } /** * Update Counter for an addon. It will update the refered siteId counter and the total badge. * It will return the updated addon counter. * * @param addon Registered addon name to set the badge number. * @param value The number to be stored. * @param siteId Site ID. If not defined, use current site. * @returns Promise resolved with the stored badge counter for the addon on the site. */ async updateAddonCounter(addon: string, value: number, siteId?: string): Promise { if (!CorePushNotificationsDelegate.isCounterHandlerRegistered(addon)) { return 0; } siteId = siteId || CoreSites.getCurrentSiteId(); await this.saveAddonBadge(value, siteId, addon); await this.updateSiteCounter(siteId); return value; } /** * Update total badge counter of the app. * * @returns Promise resolved with the stored badge counter for the site. */ async updateAppCounter(): Promise { const sitesIds = await CoreSites.getSitesIds(); const counters = await Promise.all(sitesIds.map((siteId) => this.getAddonBadge(siteId))); const total = counters.reduce((previous, counter) => previous + counter, 0); if (CorePlatform.isMobile()) { // Set the app badge on mobile. await Badge.set(total); } return total; } /** * Update counter for a site using the stored addon data. It will update the total badge application number. * It will return the updated site counter. * * @param siteId Site ID. * @returns Promise resolved with the stored badge counter for the site. */ async updateSiteCounter(siteId: string): Promise { const addons = CorePushNotificationsDelegate.getCounterHandlers(); const counters = await Promise.all(Object.values(addons).map((addon) => this.getAddonBadge(siteId, addon))); const total = counters.reduce((previous, counter) => previous + counter, 0); // Save the counter on site. await this.saveAddonBadge(total, siteId); await this.updateAppCounter(); return total; } /** * Register a device in Apple APNS or Google GCM. * * @returns Promise resolved when the device is registered. */ async registerDevice(): Promise { try { // Check if sound is enabled for notifications. const options = await this.getOptions(); const pushObject = Push.init(options); pushObject.on('notification').subscribe((notification: NotificationEventResponse | {registrationType: string}) => { // Execute the callback in the Angular zone, so change detection doesn't stop working. NgZone.run(() => { if ('registrationType' in notification) { // Not a valid notification, ignore. return; } this.logger.log('Received a notification', notification); this.onMessageReceived(notification); }); }); pushObject.on('registration').subscribe((data: RegistrationEventResponse) => { // Execute the callback in the Angular zone, so change detection doesn't stop working. NgZone.run(() => { this.pushID = data.registrationId; if (!CoreSites.isLoggedIn() || !this.canRegisterOnMoodle()) { return; } this.registerDeviceOnMoodle().catch((error) => { this.logger.error('Can\'t register device', error); }); }); }); pushObject.on('error').subscribe((error: Error) => { // Execute the callback in the Angular zone, so change detection doesn't stop working. NgZone.run(() => { this.logger.warn('Error with Push plugin', error); }); }); } catch (error) { this.logger.warn(error); throw error; } } /** * Registers a device on a Moodle site if needed. * * @param siteId Site ID. If not defined, current site. * @param forceUnregister Whether to force unregister and register. * @returns Promise resolved when device is registered. */ async registerDeviceOnMoodle(siteId?: string, forceUnregister?: boolean): Promise { this.logger.debug('Register device on Moodle.'); if (!this.canRegisterOnMoodle()) { return Promise.reject(null); } const site = await CoreSites.getSite(siteId); try { const data = this.getRequiredRegisterData(); data.publickey = await this.getPublicKeyForSite(site); const neededActions = await this.getRegisterDeviceActions(data, site, forceUnregister); if (neededActions.unregister) { // Unregister the device first. await CoreUtils.ignoreErrors(this.unregisterDeviceOnMoodle(site)); } if (neededActions.register) { // Now register the device. const addDeviceResponse = await site.write('core_user_add_user_device', CoreUtils.clone(data)); const deviceAlreadyRegistered = addDeviceResponse[0] && addDeviceResponse[0].find(warning => warning.warningcode === 'existingkeyforthisuser'); if (deviceAlreadyRegistered && data.publickey) { // Device already registered, make sure the public key is up to date. await this.updatePublicKeyOnMoodle(site, data); } CoreEvents.trigger(CoreEvents.DEVICE_REGISTERED_IN_MOODLE, {}, site.getId()); // Insert the device in the local DB. await CoreUtils.ignoreErrors(this.registeredDevicesTables[site.getId()].insert(data)); } else if (neededActions.updatePublicKey) { // Device already registered, make sure the public key is up to date. const response = await this.updatePublicKeyOnMoodle(site, data); if (response?.warnings?.find(warning => warning.warningcode === 'devicedoesnotexist')) { // The device doesn't exist in the server. Remove the device from the local DB and try again. await this.registeredDevicesTables[site.getId()].delete({ appid: data.appid, uuid: data.uuid, name: data.name, model: data.model, platform: data.platform, }); await this.registerDeviceOnMoodle(siteId, false); } } } finally { // Remove pending unregisters for this site. await CoreUtils.ignoreErrors(this.pendingUnregistersTable.deleteByPrimaryKey({ siteid: site.getId() })); } } /** * Get the public key to register in a site. * * @param site Site to register * @returns Public key, undefined if the site or the device doesn't support encryption. */ protected async getPublicKeyForSite(site: CoreSite): Promise { if (!site.wsAvailable('core_user_update_user_device_public_key')) { return; } return await this.getPublicKey(); } /** * Get the device public key. * * @returns Public key, undefined if the device doesn't support encryption. */ async getPublicKey(): Promise { if (!CorePlatform.isMobile()) { return; } const publicKey = await Push.getPublicKey(); return publicKey ?? undefined; } /** * Update a public key on a Moodle site. * * @param site Site. * @param data Device data. * @returns WS response, undefined if no public key. */ protected async updatePublicKeyOnMoodle( site: CoreSite, data: CoreUserAddUserDeviceWSParams, ): Promise { if (!data.publickey) { return; } this.logger.debug('Update public key on Moodle.'); const params: CoreUserUpdateUserDevicePublicKeyWSParams = { uuid: data.uuid, appid: data.appid, publickey: data.publickey, }; return await site.write('core_user_update_user_device_public_key', params); } /** * Get the addon/site badge counter from the database. * * @param siteId Site ID. * @param addon Registered addon name. If not defined it will store the site total. * @returns Promise resolved with the stored badge counter for the addon or site or 0 if none. */ protected async getAddonBadge(siteId?: string, addon: string = 'site'): Promise { try { const entry = await this.badgesTable.getOne({ siteid: siteId, addon }); return entry?.number || 0; } catch (err) { return 0; } } /** * Retry pending unregisters. * * @param siteId If defined, retry only for that site if needed. Otherwise, retry all pending unregisters. * @returns Promise resolved when done. */ async retryUnregisters(siteId?: string): Promise { const results = await this.pendingUnregistersTable.getMany(CoreObject.withoutEmpty({ siteid: siteId })); await Promise.all(results.map(async (result) => { // Create a temporary site to unregister. const tmpSite = CoreSitesFactory.makeSite( result.siteid, result.siteurl, result.token, { info: CoreTextUtils.parseJSON(result.info, null) || undefined }, ); await this.unregisterDeviceOnMoodle(tmpSite); })); } /** * Save the addon/site badgecounter on the database. * * @param value The number to be stored. * @param siteId Site ID. If not defined, use current site. * @param addon Registered addon name. If not defined it will store the site total. * @returns Promise resolved with the stored badge counter for the addon or site. */ protected async saveAddonBadge(value: number, siteId?: string, addon: string = 'site'): Promise { siteId = siteId || CoreSites.getCurrentSiteId(); await this.badgesTable.insert({ siteid: siteId, addon, number: value, // eslint-disable-line id-blacklist }); return value; } /** * Get the needed actions to perform to register a device. * * @param data Data of the device. * @param site Site to use. * @param forceUnregister Whether to force unregister and register. * @returns Whether each action needs to be performed or not. */ protected async getRegisterDeviceActions( data: CoreUserAddUserDeviceWSParams, site: CoreSite, forceUnregister?: boolean, ): Promise { if (forceUnregister) { // No need to check if device is stored, always unregister and register the device. return { unregister: true, register: true, updatePublicKey: false, }; } // Check if the device is already registered. const records = await CoreUtils.ignoreErrors( this.registeredDevicesTables[site.getId()].getMany({ appid: data.appid, uuid: data.uuid, name: data.name, model: data.model, platform: data.platform, }), ); let isStored = false; let versionOrPushChanged = false; let updatePublicKey = false; (records || []).forEach((record) => { if (record.version == data.version && record.pushid == data.pushid) { // The device is already stored. isStored = true; updatePublicKey = !!data.publickey && record.publickey !== data.publickey; } else { // The version or pushid has changed. versionOrPushChanged = true; } }); return { register: !isStored, // No need to register if device is already stored. unregister: !isStored && !versionOrPushChanged, // No need to unregister first if only version or push changed. updatePublicKey, }; } } export const CorePushNotifications = makeSingleton(CorePushNotificationsProvider); /** * Additional data sent in push notifications. */ export type CorePushNotificationsNotificationBasicRawData = { customdata?: string; // Custom data. extrafeatures?: string; // "1" if the notification uses extrafeatures, "0" otherwise. foreground?: boolean; // Whether the app was in foreground. 'image-type'?: string; // How to display the notification image. moodlecomponent?: string; // Moodle component that triggered the notification. name?: string; // A name to identify the type of notification. notId?: string; // Notification ID. notif?: string; // "1" if it's a notification, "0" if it's a Moodle message. site?: string; // ID of the site sending the notification. siteurl?: string; // URL of the site the notification is related to. usertoid?: string; // ID of user receiving the push. conversationtype?: string; // Conversation type. Only if it's a push generated by a Moodle message. userfromfullname?: string; // Fullname of user sending the push. Only if it's a push generated by a Moodle message. userfromid?: string; // ID of user sending the push. Only if it's a push generated by a Moodle message. picture?: string; // Notification big picture. "Extra" feature. summaryText?: string; // Notification summary text. "Extra" feature. sender?: string; // Name of the user who sent the message. "Extra" feature. senderImage?: string; // Image of the user who sent the message. "Extra" feature. }; /** * Additional data sent in push notifications, with some calculated data. */ export type CorePushNotificationsNotificationBasicData = Omit & { title?: string; // Notification title. message?: string; // Notification message. customdata?: Record; // Parsed custom data. }; /** * Params of core_user_remove_user_device WS. */ export type CoreUserRemoveUserDeviceWSParams = { uuid: string; // The device UUID. appid?: string; // The app id, if empty devices matching the UUID for the user will be removed. }; /** * Data returned by core_user_remove_user_device WS. */ export type CoreUserRemoveUserDeviceWSResponse = { removed: boolean; // True if removed, false if not removed because it doesn't exists. warnings?: CoreWSExternalWarning[]; }; /** * Params of core_user_add_user_device WS. */ export type CoreUserAddUserDeviceWSParams = { appid: string; // The app id, usually something like com.moodle.moodlemobile. name: string; // The device name, 'occam' or 'iPhone' etc. model: string; // The device model 'Nexus4' or 'iPad1,1' etc. platform: string; // The device platform 'iOS' or 'Android' etc. version: string; // The device version '6.1.2' or '4.2.2' etc. pushid: string; // The device PUSH token/key/identifier/registration id. uuid: string; // The device UUID. publickey?: string; // @since 4.2. The app generated public key. }; /** * Data returned by core_user_add_user_device WS. */ export type CoreUserAddUserDeviceWSResponse = CoreWSExternalWarning[][]; /** * Params of core_user_update_user_device_public_key WS. */ export type CoreUserUpdateUserDevicePublicKeyWSParams = { uuid: string; appid: string; publickey: string; }; /** * Data returned by core_user_update_user_device_public_key WS. */ export type CoreUserUpdateUserDevicePublicKeyWSResponse = { status: boolean; warnings?: CoreWSExternalWarning[]; }; type RegisterDeviceActions = { register: boolean; // Whether device needs to be registered in LMS. unregister: boolean; // Whether device needs to be unregistered before register in LMS to make sure data is up to date. updatePublicKey: boolean; // Whether only public key needs to be updated. };