Converse converse.js

Source: headless/plugins/muc/affiliations/utils.js

  1. /**
  2. * @copyright The Converse.js contributors
  3. * @license Mozilla Public License (MPLv2)
  4. */
  5. import { AFFILIATIONS } from '@converse/headless/plugins/muc/index.js';
  6. import difference from 'lodash-es/difference';
  7. import indexOf from 'lodash-es/indexOf';
  8. import log from "@converse/headless/log";
  9. import { _converse, api, converse } from '@converse/headless/core.js';
  10. import { parseMemberListIQ } from '../parsers.js';
  11. const { Strophe, $iq, u } = converse.env;
  12. /**
  13. * Sends an IQ stanza to the server, asking it for the relevant affiliation list .
  14. * Returns an array of {@link MemberListItem} objects, representing occupants
  15. * that have the given affiliation.
  16. * See: https://xmpp.org/extensions/xep-0045.html#modifymember
  17. * @param { ("admin"|"owner"|"member") } affiliation
  18. * @param { String } muc_jid - The JID of the MUC for which the affiliation list should be fetched
  19. * @returns { Promise<MemberListItem[]> }
  20. */
  21. export async function getAffiliationList (affiliation, muc_jid) {
  22. const { __ } = _converse;
  23. const iq = $iq({ 'to': muc_jid, 'type': 'get' })
  24. .c('query', { xmlns: Strophe.NS.MUC_ADMIN })
  25. .c('item', { 'affiliation': affiliation });
  26. const result = await api.sendIQ(iq, null, false);
  27. if (result === null) {
  28. const err_msg = __('Error: timeout while fetching %1s list for MUC %2s', affiliation, muc_jid);
  29. const err = new Error(err_msg);
  30. log.warn(err_msg);
  31. return err;
  32. }
  33. if (u.isErrorStanza(result)) {
  34. const err_msg = __('Error: not allowed to fetch %1s list for MUC %2s', affiliation, muc_jid);
  35. const err = new Error(err_msg);
  36. log.warn(err_msg);
  37. log.warn(result);
  38. return err;
  39. }
  40. return parseMemberListIQ(result)
  41. .filter(p => p)
  42. .sort((a, b) => (a.nick < b.nick ? -1 : a.nick > b.nick ? 1 : 0));
  43. }
  44. /**
  45. * Given an occupant model, see which affiliations may be assigned by that user
  46. * @param { Model } occupant
  47. * @returns { Array<('owner'|'admin'|'member'|'outcast'|'none')> } - An array of assignable affiliations
  48. */
  49. export function getAssignableAffiliations (occupant) {
  50. let disabled = api.settings.get('modtools_disable_assign');
  51. if (!Array.isArray(disabled)) {
  52. disabled = disabled ? AFFILIATIONS : [];
  53. }
  54. if (occupant?.get('affiliation') === 'owner') {
  55. return AFFILIATIONS.filter(a => !disabled.includes(a));
  56. } else if (occupant?.get('affiliation') === 'admin') {
  57. return AFFILIATIONS.filter(a => !['owner', 'admin', ...disabled].includes(a));
  58. } else {
  59. return [];
  60. }
  61. }
  62. // Necessary for tests
  63. _converse.getAssignableAffiliations = getAssignableAffiliations;
  64. /**
  65. * Send IQ stanzas to the server to modify affiliations for users in this groupchat.
  66. * See: https://xmpp.org/extensions/xep-0045.html#modifymember
  67. * @param { Array<Object> } users
  68. * @param { string } users[].jid - The JID of the user whose affiliation will change
  69. * @param { Array } users[].affiliation - The new affiliation for this user
  70. * @param { string } [users[].reason] - An optional reason for the affiliation change
  71. * @returns { Promise }
  72. */
  73. export function setAffiliations (muc_jid, users) {
  74. const affiliations = [...new Set(users.map(u => u.affiliation))];
  75. return Promise.all(affiliations.map(a => setAffiliation(a, muc_jid, users)));
  76. }
  77. /**
  78. * Send IQ stanzas to the server to set an affiliation for
  79. * the provided JIDs.
  80. * See: https://xmpp.org/extensions/xep-0045.html#modifymember
  81. *
  82. * Prosody doesn't accept multiple JIDs' affiliations
  83. * being set in one IQ stanza, so as a workaround we send
  84. * a separate stanza for each JID.
  85. * Related ticket: https://issues.prosody.im/345
  86. *
  87. * @param { ('outcast'|'member'|'admin'|'owner') } affiliation - The affiliation to be set
  88. * @param { String|Array<String> } jids - The JID(s) of the MUCs in which the
  89. * affiliations need to be set.
  90. * @param { object } members - A map of jids, affiliations and
  91. * optionally reasons. Only those entries with the
  92. * same affiliation as being currently set will be considered.
  93. * @returns { Promise } A promise which resolves and fails depending on the XMPP server response.
  94. */
  95. export function setAffiliation (affiliation, muc_jids, members) {
  96. if (!Array.isArray(muc_jids)) {
  97. muc_jids = [muc_jids];
  98. }
  99. members = members.filter(m => [undefined, affiliation].includes(m.affiliation));
  100. return Promise.all(
  101. muc_jids.reduce((acc, jid) => [...acc, ...members.map(m => sendAffiliationIQ(affiliation, jid, m))], [])
  102. );
  103. }
  104. /**
  105. * Send an IQ stanza specifying an affiliation change.
  106. * @private
  107. * @param { String } affiliation: affiliation (could also be stored on the member object).
  108. * @param { String } muc_jid: The JID of the MUC in which the affiliation should be set.
  109. * @param { Object } member: Map containing the member's jid and optionally a reason and affiliation.
  110. */
  111. function sendAffiliationIQ (affiliation, muc_jid, member) {
  112. const iq = $iq({ to: muc_jid, type: 'set' })
  113. .c('query', { xmlns: Strophe.NS.MUC_ADMIN })
  114. .c('item', {
  115. 'affiliation': member.affiliation || affiliation,
  116. 'nick': member.nick,
  117. 'jid': member.jid
  118. });
  119. if (member.reason !== undefined) {
  120. iq.c('reason', member.reason);
  121. }
  122. return api.sendIQ(iq);
  123. }
  124. /**
  125. * Given two lists of objects with 'jid', 'affiliation' and
  126. * 'reason' properties, return a new list containing
  127. * those objects that are new, changed or removed
  128. * (depending on the 'remove_absentees' boolean).
  129. *
  130. * The affiliations for new and changed members stay the
  131. * same, for removed members, the affiliation is set to 'none'.
  132. *
  133. * The 'reason' property is not taken into account when
  134. * comparing whether affiliations have been changed.
  135. * @param { boolean } exclude_existing - Indicates whether JIDs from
  136. * the new list which are also in the old list
  137. * (regardless of affiliation) should be excluded
  138. * from the delta. One reason to do this
  139. * would be when you want to add a JID only if it
  140. * doesn't have *any* existing affiliation at all.
  141. * @param { boolean } remove_absentees - Indicates whether JIDs
  142. * from the old list which are not in the new list
  143. * should be considered removed and therefore be
  144. * included in the delta with affiliation set
  145. * to 'none'.
  146. * @param { array } new_list - Array containing the new affiliations
  147. * @param { array } old_list - Array containing the old affiliations
  148. * @returns { array }
  149. */
  150. export function computeAffiliationsDelta (exclude_existing, remove_absentees, new_list, old_list) {
  151. const new_jids = new_list.map(o => o.jid);
  152. const old_jids = old_list.map(o => o.jid);
  153. // Get the new affiliations
  154. let delta = difference(new_jids, old_jids).map(jid => new_list[indexOf(new_jids, jid)]);
  155. if (!exclude_existing) {
  156. // Get the changed affiliations
  157. delta = delta.concat(
  158. new_list.filter(item => {
  159. const idx = indexOf(old_jids, item.jid);
  160. return idx >= 0 ? item.affiliation !== old_list[idx].affiliation : false;
  161. })
  162. );
  163. }
  164. if (remove_absentees) {
  165. // Get the removed affiliations
  166. delta = delta.concat(difference(old_jids, new_jids).map(jid => ({ 'jid': jid, 'affiliation': 'none' })));
  167. }
  168. return delta;
  169. }