utils.js 12 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321
  1. import './modals/occupant.js';
  2. import './modals/moderator-tools.js';
  3. import log from "@converse/headless/log";
  4. import tplSpinner from 'templates/spinner.js';
  5. import { __ } from 'i18n';
  6. import { _converse, api, converse } from "@converse/headless/core";
  7. import { html } from "lit";
  8. import { setAffiliation } from '@converse/headless/plugins/muc/affiliations/utils.js';
  9. const { Strophe, u } = converse.env;
  10. const COMMAND_TO_AFFILIATION = {
  11. 'admin': 'admin',
  12. 'ban': 'outcast',
  13. 'member': 'member',
  14. 'owner': 'owner',
  15. 'revoke': 'none'
  16. };
  17. const COMMAND_TO_ROLE = {
  18. 'deop': 'participant',
  19. 'kick': 'none',
  20. 'mute': 'visitor',
  21. 'op': 'moderator',
  22. 'voice': 'participant'
  23. };
  24. export function clearHistory (jid) {
  25. if (_converse.router.history.getFragment() === `converse/room?jid=${jid}`) {
  26. _converse.router.navigate('');
  27. }
  28. }
  29. export async function destroyMUC (model) {
  30. const messages = [__('Are you sure you want to destroy this groupchat?')];
  31. let fields = [
  32. {
  33. 'name': 'challenge',
  34. 'label': __('Please enter the XMPP address of this groupchat to confirm'),
  35. 'challenge': model.get('jid'),
  36. 'placeholder': __('name@example.org'),
  37. 'required': true
  38. },
  39. {
  40. 'name': 'reason',
  41. 'label': __('Optional reason for destroying this groupchat'),
  42. 'placeholder': __('Reason')
  43. },
  44. {
  45. 'name': 'newjid',
  46. 'label': __('Optional XMPP address for a new groupchat that replaces this one'),
  47. 'placeholder': __('replacement@example.org')
  48. }
  49. ];
  50. try {
  51. fields = await api.confirm(__('Confirm'), messages, fields);
  52. const reason = fields.filter(f => f.name === 'reason').pop()?.value;
  53. const newjid = fields.filter(f => f.name === 'newjid').pop()?.value;
  54. return model.sendDestroyIQ(reason, newjid).then(() => model.close());
  55. } catch (e) {
  56. log.error(e);
  57. }
  58. }
  59. export function getNicknameRequiredTemplate (model) {
  60. const jid = model.get('jid');
  61. if (api.settings.get('muc_show_logs_before_join')) {
  62. return html`<converse-muc-chatarea jid="${jid}"></converse-muc-chatarea>`;
  63. } else {
  64. return html`<converse-muc-nickname-form jid="${jid}"></converse-muc-nickname-form>`;
  65. }
  66. }
  67. export function getChatRoomBodyTemplate (o) {
  68. const view = o.model.session.get('view');
  69. const jid = o.model.get('jid');
  70. const RS = converse.ROOMSTATUS;
  71. const conn_status = o.model.session.get('connection_status');
  72. if (view === converse.MUC.VIEWS.CONFIG) {
  73. return html`<converse-muc-config-form class="muc-form-container" jid="${jid}"></converse-muc-config-form>`;
  74. } else {
  75. return html`
  76. ${ conn_status == RS.PASSWORD_REQUIRED ? html`<converse-muc-password-form class="muc-form-container" jid="${jid}"></converse-muc-password-form>` : '' }
  77. ${ conn_status == RS.ENTERED ? html`<converse-muc-chatarea jid="${jid}"></converse-muc-chatarea>` : '' }
  78. ${ conn_status == RS.CONNECTING ? tplSpinner() : '' }
  79. ${ conn_status == RS.NICKNAME_REQUIRED ? getNicknameRequiredTemplate(o.model) : '' }
  80. ${ conn_status == RS.DISCONNECTED ? html`<converse-muc-disconnected jid="${jid}"></converse-muc-disconnected>` : '' }
  81. ${ conn_status == RS.BANNED ? html`<converse-muc-disconnected jid="${jid}"></converse-muc-disconnected>` : '' }
  82. ${ conn_status == RS.DESTROYED ? html`<converse-muc-destroyed jid="${jid}"></converse-muc-destroyed>` : '' }
  83. `;
  84. }
  85. }
  86. export function getAutoCompleteListItem (text, input) {
  87. input = input.trim();
  88. const element = document.createElement('li');
  89. element.setAttribute('aria-selected', 'false');
  90. if (api.settings.get('muc_mention_autocomplete_show_avatar')) {
  91. const img = document.createElement('img');
  92. let dataUri = 'data:' + _converse.DEFAULT_IMAGE_TYPE + ';base64,' + _converse.DEFAULT_IMAGE;
  93. if (_converse.vcards) {
  94. const vcard = _converse.vcards.findWhere({ 'nickname': text });
  95. if (vcard) dataUri = 'data:' + vcard.get('image_type') + ';base64,' + vcard.get('image');
  96. }
  97. img.setAttribute('src', dataUri);
  98. img.setAttribute('width', '22');
  99. img.setAttribute('class', 'avatar avatar-autocomplete');
  100. element.appendChild(img);
  101. }
  102. const regex = new RegExp('(' + input + ')', 'ig');
  103. const parts = input ? text.split(regex) : [text];
  104. parts.forEach(txt => {
  105. if (input && txt.match(regex)) {
  106. const match = document.createElement('mark');
  107. match.textContent = txt;
  108. element.appendChild(match);
  109. } else {
  110. element.appendChild(document.createTextNode(txt));
  111. }
  112. });
  113. return element;
  114. }
  115. export async function getAutoCompleteList () {
  116. const models = [...(await api.rooms.get()), ...(await api.contacts.get())];
  117. const jids = [...new Set(models.map(o => Strophe.getDomainFromJid(o.get('jid'))))];
  118. return jids;
  119. }
  120. function setRole (muc, command, args, required_affiliations = [], required_roles = []) {
  121. const role = COMMAND_TO_ROLE[command];
  122. if (!role) {
  123. throw Error(`ChatRoomView#setRole called with invalid command: ${command}`);
  124. }
  125. if (!muc.verifyAffiliations(required_affiliations) || !muc.verifyRoles(required_roles)) {
  126. return false;
  127. }
  128. if (!muc.validateRoleOrAffiliationChangeArgs(command, args)) {
  129. return false;
  130. }
  131. const nick_or_jid = muc.getNickOrJIDFromCommandArgs(args);
  132. if (!nick_or_jid) {
  133. return false;
  134. }
  135. const reason = args.split(nick_or_jid, 2)[1].trim();
  136. // We're guaranteed to have an occupant due to getNickOrJIDFromCommandArgs
  137. const occupant = muc.getOccupant(nick_or_jid);
  138. muc.setRole(occupant, role, reason, undefined, e => muc.onCommandError(e));
  139. return true;
  140. }
  141. function verifyAndSetAffiliation (muc, command, args, required_affiliations) {
  142. const affiliation = COMMAND_TO_AFFILIATION[command];
  143. if (!affiliation) {
  144. throw Error(`verifyAffiliations called with invalid command: ${command}`);
  145. }
  146. if (!muc.verifyAffiliations(required_affiliations)) {
  147. return false;
  148. }
  149. if (!muc.validateRoleOrAffiliationChangeArgs(command, args)) {
  150. return false;
  151. }
  152. const nick_or_jid = muc.getNickOrJIDFromCommandArgs(args);
  153. if (!nick_or_jid) {
  154. return false;
  155. }
  156. let jid;
  157. const reason = args.split(nick_or_jid, 2)[1].trim();
  158. const occupant = muc.getOccupant(nick_or_jid);
  159. if (occupant) {
  160. jid = occupant.get('jid');
  161. } else {
  162. if (u.isValidJID(nick_or_jid)) {
  163. jid = nick_or_jid;
  164. } else {
  165. const message = __(
  166. "Couldn't find a participant with that nickname. " + 'They might have left the groupchat.'
  167. );
  168. muc.createMessage({ message, 'type': 'error' });
  169. return;
  170. }
  171. }
  172. const attrs = { jid, reason };
  173. if (occupant && api.settings.get('auto_register_muc_nickname')) {
  174. attrs['nick'] = occupant.get('nick');
  175. }
  176. setAffiliation(affiliation, muc.get('jid'), [attrs])
  177. .then(() => muc.occupants.fetchMembers())
  178. .catch(err => muc.onCommandError(err));
  179. }
  180. export function showModeratorToolsModal (muc, affiliation) {
  181. if (!muc.verifyRoles(['moderator'])) {
  182. return;
  183. }
  184. let modal = api.modal.get('converse-modtools-modal');
  185. if (modal) {
  186. modal.affiliation = affiliation;
  187. modal.render();
  188. } else {
  189. modal = api.modal.create('converse-modtools-modal', { affiliation, 'jid': muc.get('jid') });
  190. }
  191. modal.show();
  192. }
  193. export function showOccupantModal (ev, occupant) {
  194. api.modal.show('converse-muc-occupant-modal', { 'model': occupant }, ev);
  195. }
  196. export function parseMessageForMUCCommands (data, handled) {
  197. const model = data.model;
  198. if (handled ||
  199. model.get('type') !== _converse.CHATROOMS_TYPE || (
  200. api.settings.get('muc_disable_slash_commands') &&
  201. !Array.isArray(api.settings.get('muc_disable_slash_commands'))
  202. )) {
  203. return handled;
  204. }
  205. let text = data.text;
  206. text = text.replace(/^\s*/, '');
  207. const command = (text.match(/^\/([a-zA-Z]*) ?/) || ['']).pop().toLowerCase();
  208. if (!command) {
  209. return false;
  210. }
  211. const args = text.slice(('/' + command).length + 1).trim();
  212. const allowed_commands = model.getAllowedCommands() ?? [];
  213. if (command === 'admin' && allowed_commands.includes(command)) {
  214. verifyAndSetAffiliation(model, command, args, ['owner']);
  215. return true;
  216. } else if (command === 'ban' && allowed_commands.includes(command)) {
  217. verifyAndSetAffiliation(model, command, args, ['admin', 'owner']);
  218. return true;
  219. } else if (command === 'modtools' && allowed_commands.includes(command)) {
  220. showModeratorToolsModal(model, args);
  221. return true;
  222. } else if (command === 'deop' && allowed_commands.includes(command)) {
  223. // FIXME: /deop only applies to setting a moderators
  224. // role to "participant" (which only admin/owner can
  225. // do). Moderators can however set non-moderator's role
  226. // to participant (e.g. visitor => participant).
  227. // Currently we don't distinguish between these two
  228. // cases.
  229. setRole(model, command, args, ['admin', 'owner']);
  230. return true;
  231. } else if (command === 'destroy' && allowed_commands.includes(command)) {
  232. if (!model.verifyAffiliations(['owner'])) {
  233. return true;
  234. }
  235. destroyMUC(model).catch(e => model.onCommandError(e));
  236. return true;
  237. } else if (command === 'help' && allowed_commands.includes(command)) {
  238. model.set({ 'show_help_messages': false }, { 'silent': true });
  239. model.set({ 'show_help_messages': true });
  240. return true;
  241. } else if (command === 'kick' && allowed_commands.includes(command)) {
  242. setRole(model, command, args, [], ['moderator']);
  243. return true;
  244. } else if (command === 'mute' && allowed_commands.includes(command)) {
  245. setRole(model, command, args, [], ['moderator']);
  246. return true;
  247. } else if (command === 'member' && allowed_commands.includes(command)) {
  248. verifyAndSetAffiliation(model, command, args, ['admin', 'owner']);
  249. return true;
  250. } else if (command === 'nick' && allowed_commands.includes(command)) {
  251. if (!model.verifyRoles(['visitor', 'participant', 'moderator'])) {
  252. return true;
  253. } else if (args.length === 0) {
  254. // e.g. Your nickname is "coolguy69"
  255. const message = __('Your nickname is "%1$s"', model.get('nick'));
  256. model.createMessage({ message, 'type': 'error' });
  257. } else {
  258. model.setNickname(args);
  259. }
  260. return true;
  261. } else if (command === 'owner' && allowed_commands.includes(command)) {
  262. verifyAndSetAffiliation(model, command, args, ['owner']);
  263. return true;
  264. } else if (command === 'op' && allowed_commands.includes(command)) {
  265. setRole(model, command, args, ['admin', 'owner']);
  266. return true;
  267. } else if (command === 'register' && allowed_commands.includes(command)) {
  268. if (args.length > 1) {
  269. model.createMessage({
  270. 'message': __('Error: invalid number of arguments'),
  271. 'type': 'error'
  272. });
  273. } else {
  274. model.registerNickname().then(err_msg => {
  275. err_msg && model.createMessage({ 'message': err_msg, 'type': 'error' });
  276. });
  277. }
  278. return true;
  279. } else if (command === 'revoke' && allowed_commands.includes(command)) {
  280. verifyAndSetAffiliation(model, command, args, ['admin', 'owner']);
  281. return true;
  282. } else if (command === 'topic' && allowed_commands.includes(command) ||
  283. command === 'subject' && allowed_commands.includes(command)) {
  284. model.setSubject(args);
  285. return true;
  286. } else if (command === 'voice' && allowed_commands.includes(command)) {
  287. setRole(model, command, args, [], ['moderator']);
  288. return true;
  289. } else {
  290. return false;
  291. }
  292. }